从 0 到 1 搞一个 Compose Desktop 版本的玩天气之绘制

news2024/11/23 8:33:25

从 0 到 1 搞一个 Compose Desktop 版本的玩天气之绘制

上一篇文章 “从 0 到 1 搞一个 Compose Desktop 版本的玩天气之踩坑” 中大概说了下刚开始使用 Compose Desktop 会遇到的一些问题,帮大家踩了踩坑,那么这一篇则会带大家一起来看下项目中绘制的一些东西,再来看下项目的最终实现效果吧!

视频

动画的使用

通过上面的 GIF 图可以看到项目中使用到了一些动画,效果还是非常不错的,其实实现起来非常简单!

可见性动画

首先来看下可见性动画的使用,之前我写过一个专栏,里面专门说了下 Compose 中的动画的使用及原理,有兴趣的大家可以去看下:Compose 动画开发艺术探索 。

可见性动画在页面左边用到了,点击添加按钮出现搜索页面的时候就使用的是可见性动画,简单看下代码:

@Composable
fun LeftInformation() {
    var showSearch by rememberSaveable { mutableStateOf(false) }
    Box(
        Modifier.fillMaxHeight().width(300.dp).padding(end = 10.dp)
    ) {
        WeatherDetails(onAddClick = {
            showSearch = true
        })

        AnimatedVisibility(
            visible = showSearch,
            enter = slideInHorizontally(),
            exit = slideOutHorizontally()
        ) {
            SearchCity()
        }
    }
}

可以看到这块在进入的时候使用了 slideInHorizontally 动画,顾名思义,就是水平滑动展开,退出的时候使用了 slideOutHorizontally ,就是水平滑动退出。

实现效果这里就不展示了,就是文章左边的动画效果。

无限重复动画

无限重复动画在左边展示天气信息的天气图标上用到了,这块的重复动画使用了两种,如果是晴天的话就修改 Modifier.rotate ,因为晴天是太阳,旋转的话好看一些,如果不是晴天的话旋转不好看,所以改为 Modifier.offset ,这样平移的话好看一些。来看下实现代码吧:

@Composable
private fun RotateWeatherIcon(icon: String) {
    val infiniteTransition = rememberInfiniteTransition()
    val modifier = if (icon == "100") {
        val rotate by infiniteTransition.animateFloat(
            initialValue = 0f,
            targetValue = 360f,
            animationSpec = infiniteRepeatable(
                animation = tween(3500, easing = LinearOutSlowInEasing),
                repeatMode = RepeatMode.Reverse
            )
        )
        Modifier.rotate(rotate)
    } else {
        val offsetX by infiniteTransition.animateValue(
            initialValue = (-30).dp, // 初始值
            targetValue = 30.dp, // 目标值
            typeConverter = TwoWayConverter(
                { AnimationVector1D(it.value) },
                { it.value.dp }), // 类型转换
            animationSpec = infiniteRepeatable(  // 动画规格!!!
                animation = tween(3500, easing = LinearOutSlowInEasing),
                repeatMode = RepeatMode.Reverse
            )
        )
        Modifier.offset(x = offsetX)
    }
    Image(
        painter = painterResource(getWeatherIcon(icon)),
        "",
        modifier = modifier.size(170.dp).padding(10.dp)
    )
}

无限重复动画的使用方式也不难,在之前的章节中说过,感兴趣的可以去上面所说的专栏中查看,大家放心,Jetpack Compose 中动画的使用方式和 Compose Desktop 一致。

空气质量

空气质量就是右边天气详情中的第一个模块,样子如下图所示:

在这里插入图片描述

这块是一个 “自定义 View”,为什么要加引号呢?因为这是 Compose 啊,不是安卓的 View 系统😂。

下面来看下这个 “自定义 View” 如何实现的吧!

@Composable
private fun AirQualityProgress(aqiValue: Int) {
    Canvas {
        drawLine(
            brush = Brush.linearGradient(
                0.0f to Color(red = 139, green = 195, blue = 74),
                0.1f to Color(red = 255, green = 239, blue = 59),
                0.2f to Color(red = 255, green = 152, blue = 0),
                0.3f to Color(red = 244, green = 67, blue = 54),
                0.4f to Color(red = 156, green = 39, blue = 176),
                1.0f to Color(red = 143, green = 0, blue = 0),
            ),
            start = Offset.Zero,
            end = Offset(size.width, 0f),
            strokeWidth = 20f,
            cap = StrokeCap.Round,
        )
        drawPoints(
            points = arrayListOf(
                Offset(size.width / 500 * aqiValue, 0f)
            ),
            pointMode = PointMode.Points,
            color = Color.White,
            strokeWidth = 20f,
            cap = StrokeCap.Round,
        )
    }
}

因为我没有开发过桌面的应用,所以不太清楚在桌面程序中实现这样的一个控件需要写多少代码,我只开发过安卓,只能拿安卓原生 View 做对比,在安卓 View 中如果想实现这样的一个控件的话绝对不止这么一点代码。。。

来简单解释下这个控件吧:在 Compose 中绘制需要使用可组合项 Canvas ,然后来绘制下面的那条线,线上的颜色是渐变的,在 Compose 中只需要使用 Brush 就可以实现渐变,也可以控制在不同的进度显示不同颜色,空气质量一般分为六个等级:优、良、轻度污染、中度污染、重度污染和严重污染,所以上面对应有六种颜色。最后算出当前的 AQI 值应该绘制的地方进行绘制即可。

7 日天气预报

24 小时天气预报中没有什么需要说的,一个 LazyRow 就实现了,就直接跳过了。

接下来来看下 7 日天气预报,这里其实大部分也不难,但注意看右边的温度条,这是模仿苹果天气中的温度条实现的,下面来看下苹果的样子吧:

在这里插入图片描述

再来看下我模仿实现的效果:

在这里插入图片描述

不能说一模一样,只能说大差不离。

在模仿苹果这个小彩条的时候刚开始就犯了难,这是啥意思啊。。。这条里面都代表着什么啊,也看不太懂,后来网上找了半天才知道。

  • 小彩条的长度代表温差,彩条越长温差越大。
  • 根据最近 10 天的温度,分别设置最高值和最低值。例如上面的苹果截图,近十天的最高温度为4度,则这组彩条最右端代表 4 度。 近十天最低温为 -12 度,那么这组彩条最左端就代表 -12 度。左右两端的极值不是固定不变的。
  • 小白点代表了此时的温度。

搞明白这个小彩条的含义就好说了,来自定义下这个控件吧!

@Composable
private fun TemperatureChart(min: Int, max: Int, currentMin: Int, 
                             currentMax: Int, currentTemperature: Int = -100) {
    val currentMinColor: Color = getTemperatureColor(currentMin)
    val currentMaxColor: Color = getTemperatureColor(currentMax)
    // 计算周温差
    val num = max - min
    Canvas {
        // 绘制底条
        drawLine(
            color = Color.Gray,
            start = Offset.Zero,
            end = Offset(size.width, 0f),
            strokeWidth = 10f,
            cap = StrokeCap.Round,
        )
        // 绘制这一天的气温
        drawLine(
            brush = Brush.linearGradient(
                0.0f to currentMinColor,
                1.0f to currentMaxColor,
            ),
            start = Offset(size.width / num * (currentMin - min), 0f),
            end = Offset(size.width / num * (currentMax - min), 0f),
            strokeWidth = 10f,
            cap = StrokeCap.Round,
        )
        // 如果是当天,则绘制当前温度小白点
        if (currentTemperature > -100) {
            drawPoints(
                points = arrayListOf(
                    Offset(size.width / num * (currentTemperature - min), 0f)
                ),
                pointMode = PointMode.Points,
                color = Color.White,
                strokeWidth = 10f,
                cap = StrokeCap.Round,
            )
        }
    }
}

首先看下这个可组合项接收的几个参数:

  • min:未来几天最低温度
  • max:未来几天最高温度
  • currentMin:当前绘制天的最低温度
  • currentMax:当前绘制天的最高温度
  • currentTemperature:当前天的当前温度

再简单说下函数内容,先计算下这几天的温差,然后绘制温度底条,再然后绘制温度条,这个温度条是渐变的,需要根据不同温度换不同颜色,最后判断是不是当天,如果是当天的就绘制当前温度的小白点。

上面调用一个函数 getTemperatureColor ,这是为了计算不同温度的颜色的方法,来看下这个方法吧:

/**
 * 获取不同气温的颜色值,需要动态判断
 */
private fun getTemperatureColor(temperature: Int): Color {
    return if (temperature < -20) {
        Color(red = 26, green = 92, blue = 249)
    } else if (temperature < 30) {
        Color(red = 253, green = 138, blue = 11)
    } else {
        Color(red = 248, green = 60, blue = 30)
    }
}

这块没有写全这些颜色,其实写了挺多,篇幅原因就不写了,大家能理解就好。

太阳月亮

顾名思义,太阳月亮就是指的日出日落和月出月落,还是再来看下实现好的样式吧:

在这里插入图片描述

根据日出日落和月出月落的时间来展示当前太阳和月亮的状态。由上面图大概可以看出,需要使用到贝塞尔曲线,由于只是一段曲线,所以使用二阶贝塞尔曲线就可以了。

什么是贝塞尔曲线呢?来看下百度百科的描述吧:

贝塞尔曲线(Bézier curve),又称贝兹曲线或贝济埃曲线,是应用于二维图形应用程序的数学曲线。一般的矢量图形软件通过它来精确画出曲线,贝兹曲线由线段与节点组成,节点是可拖动的支点,线段像可伸缩的皮筋,我们在绘图工具上看到的钢笔工具就是来做这种矢量曲线的。

下面来简单看下二阶贝塞尔曲线的简单动画吧:

二阶贝塞尔曲线

二阶贝塞尔曲线的公式如下:

B(t)=(1−t)2P0+2t(1−t)P1+t2P2,t∈[0,1]

下面来看下在 Compose 中如何绘制二阶贝塞尔曲线吧:

Canvas {
    val path = Path()
    path.moveTo(0f, size.height)
    // 二阶贝塞尔曲线
    path.quadraticBezierTo(
        size.width / 2, -size.height,
        size.width, size.height
    )

    drawPath(
        path = path, color = Color(red = 255, green = 193, blue = 7, alpha = 255),
        style = Stroke(width = 3f)
    )
}

可以看到在 Compose 使用 PathquadraticBezierTo 函数来绘制二阶贝塞尔曲线,这块需要解释下,二阶贝塞尔曲线一共需要三个点,但 quadraticBezierTo 函数中只接收了两个点,那剩下一个点呢?其实 PathmoveTo 到的点就是第一个点,quadraticBezierTo 函数接收的第一个点是控制点,第二个参数是终点。绘制完后贝塞尔曲线后还要绘制曲线两边的圆点:

drawPoints(
    points = arrayListOf(
        Offset(0f, size.height),
        Offset(size.width, size.height)
    ),
    pointMode = PointMode.Points,
    color = Color(red = 255, green = 193, blue = 7, alpha = 255),
    strokeWidth = 20f,
    cap = StrokeCap.Round,
)

绘制完贝塞尔曲线和圆点之后就该绘制太阳和月亮图标了,这块需要使用贝塞尔曲线的公式来计算点的坐标了。绘制点之前需要计算当前时间占太阳或月亮在天上的百分比:

fun getAccounted(rise: String, set: String, isSun: Boolean = true): Double {
    val calendar = Calendar.getInstance()
    val currentMills = calendar.timeInMillis
    calendar.set(Calendar.HOUR_OF_DAY, getHour(rise))
    calendar.set(Calendar.MINUTE, getMinute(rise))
    val riseMills = calendar.timeInMillis
    if (!isSun) {
        calendar.set(Calendar.DAY_OF_MONTH, calendar.get(Calendar.DAY_OF_MONTH) + 1)
    }
    calendar.set(Calendar.HOUR_OF_DAY, getHour(set))
    calendar.set(Calendar.MINUTE, getMinute(set))
    val setMills = calendar.timeInMillis
    val result = (currentMills - riseMills) / (setMills - riseMills).toDouble()
    return if (currentMills < riseMills) 0.0 else if (result > 1) 1.0 else result
}

这块的代码不多,使用 Calendar 来获取当前毫秒值存下来,然后设置日出日落的小时分钟并记录下来毫秒值,最后进行计算即可。

现在百分比也有了,只剩下计算贝塞尔曲线上的坐标点了,先来看下计算坐标点的公式吧:

P0(起始点) , P1(控制点), P2 (终点)
P0(x1,y1),P2(x2,y2), P1(cx,cy)
val x = Math.pow(1-t, 2) * x1 + 2 * t * (1-t) * cx + Math.pow(t, 2) * x2
val y = Math.pow(1-t, 2) * y1 + 2 * t * (1-t) * cy + Math.pow(t, 2) * y2

公式是固定的,只需要往里套点即可:

val x = (1.0 - sunResult).pow(2.0) * 0f + 2 * sunResult * (1 - sunResult) * (size.width / 2) + sunResult.pow(2.0) * size.width

val y = (1.0 - sunResult).pow(2.0) * size.height + 2 * sunResult * (1 - sunResult) * (-size.height) + sunResult.pow(2.0) * size.height

计算出来贝塞尔曲线中的点后就该绘制月亮或太阳的图标了:

drawImage(
    image = sunImage,
    topLeft = Offset(
        x - sunImage.width / 2,
        x - sunImage.height / 2
    )
)

这块的图片需要 ImageBitmap 格式,直接使用上一篇文章中的 useResource 即可生成。drawImage 中的 topLeft 参数表示左上角的坐标,默认的话时(0,0),但图片有宽高,所以需要减去宽高的一半,这样太阳和月亮的图标才能显示在正中间。

跳转浏览器

在安卓中咱们可以使用 WebView 来展示网页,但是在桌面版的应用中就没有了,需要使用系统自带的浏览器,那使用 Compose Desktop 应该如何打开系统自带的浏览器呢?可以使用 Desktop 中的 browse 方法,下面是我写的一个扩展函数:

/**
 * 通过字符串打开系统默认浏览器
 */
fun String?.openBrowse() {
    if (this?.startsWith("http") == false && !this.startsWith("https")) {
        throw IllegalArgumentException("this illegal argument exception")
    }
    try {
        val uri = URI.create(this ?: "https://www.baidu.com")
        // 获取当前系统桌面
        val dp = Desktop.getDesktop()
        // 判断系统桌面是否支持要执行的功能
        if (dp.isSupported(Desktop.Action.BROWSE)) {
            // 获取系统默认浏览器打开链接
            dp.browse(uri)
        }
    } catch (e: Exception) {
        println(e.message)
    }
}

首先判断当前字符串前缀是否为 “http” 和 “https”,如果不是的话就证明这个字符串不是网络链接,就直接抛出异常,剩下代码中的注释写的已经比较全了,就不多说了。

函数有了再来看下如何调用吧:

Row {
    Image(painter = painterResource("image/ic_launcher.svg"), "", modifier = Modifier.size(15.dp))

    Spacer(modifier = Modifier.width(5.dp))

    Text(text = "数据来自和风天气", fontSize = 12.sp, modifier = Modifier.clickable {
        fxLink.openBrowse()
    })
}

很简单,直接调用即可。运行效果就不在这里进行展示了,大家可以下载代码运行看看。

对话框

在安卓中对话框的使用场景实在是太多了,就不一一列举了,随便打开一个应用里面都有一堆对话框,那么在 Compose Desktop 中该如何弹出对话框呢?先来看下 Dialog 的函数定义吧:

@Composable
fun Dialog(
    onCloseRequest: () -> Unit,
    state: DialogState = rememberDialogState(),
    visible: Boolean = true,
    title: String = "Untitled",
    icon: Painter? = null,
    undecorated: Boolean = false,
    transparent: Boolean = false,
    resizable: Boolean = true,
    enabled: Boolean = true,
    focusable: Boolean = true,
    onPreviewKeyEvent: ((KeyEvent) -> Boolean) = { false },
    onKeyEvent: ((KeyEvent) -> Boolean) = { false },
    content: @Composable DialogWindowScope.() -> Unit
)

看到这些参数眼熟么?和上一篇文章中提到的 Window 基本一致,不同的就是这块的 stateDialogState ,接下来看下 DialogState 吧:

interface DialogState {
    var position: WindowPosition

    var size: DpSize
}

可以看到通过定义 DialogState 可以定义对话框的位置和大小,大小可以直接通过 DpSize 设置,位置的话通过 WindowPosition 来设置,但 WindowPosition 可以通过绝对位置和相对位置来设置位置:

// 绝对位置,绝对坐标
fun WindowPosition(x: Dp, y: Dp) = WindowPosition.Absolute(x, y)

// 相对位置
fun WindowPosition(alignment: Alignment) = WindowPosition.Aligned(alignment)

可以看到对话框也可以设置标题和图标,剩下的参数都见过,就不过多介绍了。

来看看在 Compose Desktop 中如何使用对话框吧:

val alertDialog = rememberSaveable { mutableStateOf(false) }
Dialog(
    onCloseRequest = { alertDialog.value = false }, visible = alertDialog.value,
    state = rememberDialogState(size = DpSize(300.dp, 200.dp)),
    title = "Weather", icon = buildPainter("image/ic_launcher.svg")
) {
    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier.padding(top = 20.dp)
    ) {
        Text(
            text = title,
            fontSize = 16.sp,
            maxLines = 1,
            fontWeight = FontWeight.Bold,
            color = MaterialTheme.colors.onSecondary,
            modifier = Modifier.padding(horizontal = 20.dp)
        )
    }
}

代码中设置了下对话框的大小,对话框使用方式和 Jetpack Compose 基本一致,看下运行效果吧:

对话框

可以看到对话框使用很简单,有需要的可以在 Dialog 中添加一些别的可组合项进行使用。

桌面的 PopopWindow

在安卓中咱们经常使用的 PopopWindow 如何在 Compose Desktop 中使用呢?

Compose 中可以直接使用 Popup 来构建类似于安卓中 PopupWindow 的弹框,但我试着直接使用了下 Popup ,不太好控制弹出的地方,所以我就想着有没有能更简单控制弹出位置的方法,仔细找了下,果然有!可以使用 CursorDropdownMenu ,它可以将 Popup 在鼠标点击的地方弹出。

@Composable
fun CursorDropdownMenu(
    expanded: Boolean,
    onDismissRequest: () -> Unit,
    focusable: Boolean = true,
    modifier: Modifier = Modifier,
    content: @Composable ColumnScope.() -> Unit
) {
		......
        Popup(
            focusable = focusable,
            onDismissRequest = onDismissRequest,
            popupPositionProvider = rememberCursorPositionProvider(),
            onKeyEvent = {
                handlePopupOnKeyEvent(it, onDismissRequest, focusManager!!, inputModeManager!!)
            },
        )
  	......
}

上面就是 CursorDropdownMenu 进行了一些删减的源码,可以看到里面也调用了 Popup

接下来看下使用方式吧:

var showPopupWindow by remember { mutableStateOf(false) }

CursorDropdownMenu(
    showPopupWindow,
    onDismissRequest = { showPopupWindow = false },
    modifier = modifier.width(300.dp).padding(horizontal = 15.dp).padding(bottom = 10.dp)
) {
    Row(
        modifier = Modifier.fillMaxWidth(),
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.CenterVertically,
    ) {
        Text(
            text = data.titleDetails,
            fontSize = 15.sp,
            fontWeight = FontWeight.Bold,
            color = MaterialTheme.colors.onSecondary
        )
        IconButton(onClick = { showPopupWindow = false }) {
            Icon(Icons.Sharp.Close, "Close")
        }
    }
}

其实使用方法和对话框是类似的,都是通过定义一个是否展开的变量,然后通过这个变量来确定当前弹框是否显示。

下面来看下运行效果:

Popup

可以看到还是挺好看的,哈哈哈!

系统菜单

在 Mac 中右上角会显示应用的菜单,如下图所示:

bar

别的应用有,我们当然也想要!那咱们的 Compose Desktop 应该如何展示呢?

放心,Jetbrains 都为我们想到了!来看看如何使用吧!

Window(onCloseRequest = ::exitApplication, title = "天青色等烟雨") {
    MenuBar {
        Menu("文件", mnemonic = 'F') {
            Item("复制(假的)", onClick = { action = "Last action: Copy" }, shortcut = KeyShortcut(Key.C, ctrl = true))
            Item("粘贴(假的)", onClick = { action = "Last action: Paste" }, shortcut = KeyShortcut(Key.V, ctrl = true))
        }
        Menu("帮助", mnemonic = 'H') {
            Item("天气帮助", onClick = { action = "Last action: Help" })
        }
    }
    App()
}

直接使用 MenuBar 就可以展示类似于上方图片中的菜单了,需要注意的是 MenuBar 需要 FrameWindowScope ,上一篇文章中所说 Windowcontent 就是 FrameWindowScope ,所以可以进行使用,要直接拿出来就不行了,如果想拿出来的话需要添加一个扩展函数:

private fun FrameWindowScope.DemoMenu() {
    MenuBar {
        Menu("文件", mnemonic = 'F') {
            Item("复制(假的)", onClick = { action = "Last action: Copy" }, shortcut = KeyShortcut(Key.C, ctrl = true))
            Item("粘贴(假的)", onClick = { action = "Last action: Paste" }, shortcut = KeyShortcut(Key.V, ctrl = true))
        }
        Menu("帮助", mnemonic = 'H') {
            Item("天气帮助", onClick = { action = "Last action: Help" })
        }
    }
}

简单说下吧,先来看下 Menu 吧:

@Composable
fun Menu(
    text: String,
    mnemonic: Char? = null,
    enabled: Boolean = true,
    content: @Composable MenuScope.() -> Unit
)

函数参数并不多,只有 mnemonic 不太好理解,它对应于键盘上某个键的字符,当这个键和 Alt 被按下时菜单将打开。然后需要重点看下 content ,它的参数类型为 MenuScope ,那就来看下 MenuScope 中都能添加什么可组合项吧!

class MenuScope internal constructor(private val impl: MenuScopeImpl) {

    @Composable
    fun Menu()
  
  	@Composable
    fun Separator() = impl.Separator()

    @Composable
    fun Item()

    @Composable
    fun CheckboxItem()

    @Composable
    fun RadioButtonItem()
}

可以看到,还能再添加 Menu ,剩下可添加的还有 ItemSeparatorCheckboxItemRadioButtonItem ,故名思义,分别是条目、分隔符、复选框和单选框。

废话不多说,运行看下效果吧!

bar

大家在使用的时候可以根据需求选择需要使用的可组合项来组合系统菜单。

托盘及通知

托盘是什么呢?在 Mac 中右上角展示的就是托盘,如下图所示;Windows 中在右下角。

在这里插入图片描述

托盘

同样的,Jetbrains 也为我们想到了,使用方法也不难,直接来看下吧:

Tray(
    state = rememberTrayState(),
    icon = painterResource("image/launcher.png"),
    menu = {
        Item(
            "天气预报",
            onClick = {}
        )
        Separator()
        Item(
            "退出",
            onClick = {}
        )
    }
)

Compose Desktop 中使用 Tray 来为应用添加系统托盘,这里的 Menu 其实和上面系统菜单中的 Menu 是一回事,所以上面所描述的 ItemSeparatorCheckboxItemRadioButtonItem 都可以进行使用。

下面来运行看下实际效果吧:

tray

这块还有一个小知识点,咱们有时候使用的一些工具其实都没有真正页面,只是在系统托盘中存在,Tray 也可以在没有窗口的情况下创建托盘应用程序:

fun main() = application {
    Tray(
        icon = painterResource("image/launcher.png"),
        menu = {
            Item(
                "退出",
                onClick = ::exitApplication
            )
        }
    )
}

这样就可以创建出一个没有窗口的程序了。

通知

咱们还可以使用系统托盘,也就是 Tray 向用户发送通知。一共有 3 种类型的通知:

  1. notify - 简单的通知
  2. warn - 警告通知
  3. Error - 错误通知

下面来看下使用方法:

val trayState = rememberTrayState()
val infoNotification = rememberNotification("天气预报", "明天的天气很好,建议出门遛弯", Notification.Type.Info)

Tray(
    state = trayState,
    icon = painterResource("image/launcher.png"),
    menu = {
        Item(
            "天气预报",
            onClick = {
                trayState.sendNotification(infoNotification)
            }
        )
        Separator()
        Item(
            "退出",
            onClick = {
                isOpen.value = false
            }
        )
    }
)

使用起来很简单,先使用 rememberNotification 来构建出一个 Notification ,然后直接使用 trayState 中的 sendNotification 进行发送通知即可。

我录制了一个完整的显示系统菜单、托盘以及通知的 GIF ,大家来看下效果吧。

菜单及通知

小结

本文大概描述了下我在编写这个天气应用时遇到的一些问题及难点,还有自定义绘制的一些避坑点到此就告一段落了。此项目所有代码都放到了 Github 中。

Github 地址:https://github.com/zhujiang521/PlayWeather/tree/desktop

如果文中写的有误,欢迎在评论区提出,咱们一起探讨。

文章如果能帮助到大家,哪怕是一点,我也非常高兴,先这样。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/91231.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

网易开发三年,现跳槽蚂蚁花呗,4面顺利通过,拿下Java岗offer

面试准备 不论是校招还是社招都避免不了各种面试、笔试&#xff0c;如何去准备这些东西就显得格外重要。 运筹帷幄之后&#xff0c;决胜千里之外&#xff01;不打毫无准备的仗&#xff0c;我觉得大家可以先从下面几个方面来准备面试&#xff1a; 1. 自我介绍。&#xff08;介…

ubuntu22.04LTS 内核源码编译,安装,卸载

下载内核源码 到网站 https://www.kernel.org/ 下载你自己版本的内核源码。 使用如下命令查看自己的内核版本 uname -r编译前准备 安装工具 sudo apt-get install libncurses5-dev libssl-dev build-essential openssl zlibc minizip libidn11-dev libidn11 libelf-dev bc…

困扰程序员50年的问题终于解决了,但好像又没完全解决......

闰秒&#xff0c;这个唯一能够让Meta、谷歌、微软等巨头同暴躁的Linux之父Linus Torvalds达成一致的存在&#xff0c;这个让无数程序员为之头疼的存在&#xff0c;终于要取消了&#xff01; 今年第27届国际计量大会上&#xff0c;与会代表通过了一项决议——从2035年起暂停在官…

【车辆计数】光流法行驶车辆检测计数【含Matlab源码 627期】

⛄一、光流场简介 1 案例背景 运动视觉研究的内容是如何从变化场景中的一系列不同时刻的图像中提取有关场景中物体的形状、位置和运动的信息。根据研究的方法&#xff0c;它可以分为两类&#xff1a;基于特征的方法和基于光流场的方法。基于特征的方法抽取特征点&#xff0c;是…

动态磨砂玻璃渐变背景

网页特效代码合集 动态磨砂玻璃渐变背景 妙用滤镜构建高级感拉满的磨砂玻璃渐变背景 一个磨砂&#xff08;毛玻璃&#xff09;质感效果的渐变背景图&#xff0c;看上去是比较高级的。 这个效果使用 CSS 其实也可以非常轻松制作出来。本文就讨论讨论&#xff1a; 使用 CSS …

WIN10环境下 MYSQL免安装版配置

之前用的旧版本Mysql&#xff0c;还安装Workbench,感觉很冗余&#xff0c;卸了重装一个免安装版&#xff0c; 1、 MYSQL下载解压 MySQL官网下载地址&#xff1a;https://downloads.mysql.com/archives/community/ 点击Download下载免安装版&#xff0c;并进行解压 2、配置环…

02.Ioc容器加载过程-Bean的生命周期源码深度剖析

Spring源码编译教程 Spring IoC容器的加载过程 1.实例化化容器&#xff1a;AnnotationConfigApplicationContext &#xff1a; // 加载spring上下文 AnnotationConfigApplicationContext context new AnnotationConfigApplicationContext(MainConfig.class);AnnotationConfi…

24岁程序媛实现了人生第一个小目标 | 2022年终总结

前言 大家好&#xff0c;我是伊人a。2022这一年我实现了人生中的第一个小目标-25岁前能够全款拿下宝马3系。耶比耶比&#x1f389;&#x1f389;&#x1f389; 2022年我是一个满眼星辰的的攀登者。 满眼星辰指的是我对未来充满希望且笃定不移&#xff0c; 攀登者指的是我在…

策略模式(State)

参考&#xff1a; 策略设计模式 (refactoringguru.cn) [5. 策略模式 — Graphic Design Patterns (design-patterns.readthedocs.io)](https://design-patterns.readthedocs.io/zh_CN/latest/behavioral_patterns/state.html) [design-patterns-cpp/Strategy.cpp at master …

[附源码]Node.js计算机毕业设计高校教务管理系统Express

项目运行 环境配置&#xff1a; Node.js最新版 Vscode Mysql5.7 HBuilderXNavicat11Vue。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分离等等。 环境需要 1.运行环境&#xff1a;最好是Nodejs最新版&#xff0c;我…

从零开始搭建Vue3.0项目

从零开始搭建Vue3.0项目所使用的软件及工具&#xff0c;环境1.确保本机已安装nodejs和npm2.Vue-cli项目搭建所使用的软件及工具&#xff0c;环境 软件vscode&#xff1a; vscode地址下载&#xff0c; svn集中式管理&#xff1a; 是一个开源的代码版本控制系统&#xff0c;用于…

谷歌PR权重是什么意思?如何查询网站的谷歌PR权重

谷歌PR权重是什么意思&#xff1f; Google权重是SEO中的一个常见名词&#xff0c;谷歌权重最早的概念指的是GooglePageRank&#xff0c;简称谷歌的PR值&#xff0c;由网站的外链数据计算得出。 PR值的出现&#xff0c;导致很多人只注重做外链&#xff0c;忽略了网站自身…

认识文件、文件路径、File类

认识文件、文件路径、File类一、认识文件1.1 狭义与广义1.2 树型结构组织和目录1.3 其他相关知识二、文件路径 (Path)三、File类3.1 构造方法3.2 文件元信息操作方法3.3 代码示例一、认识文件 1.1 狭义与广义 狭义的文件&#xff1a; 存储在硬盘上的数据&#xff0c;以"…

DBSyncer

DBSyncer是一款开源的数据同步中间件&#xff0c;提供MySQL、Oracle、SqlServer、PostgreSQL、Elasticsearch(ES)、Kafka、File、SQL等同步场景。支持上传插件自定义同步转换业务&#xff0c;提供监控全量和增量数据统计图、应用性能预警等。 特点 组合驱动&#xff0c;自定义…

Git——Windows平台创建github私有仓库详解

目录 1. 注册github&#xff0c;登录 1.1 注册 1.2 登录 2. 创建仓库 3. GitBash使用 3.1 UI界面下载 3.2 配置本地 3.3 配置github 4. 托管 1. 注册github&#xff0c;登录 官网 GitHub 1.1 注册 1.2 登录 2. 创建仓库 根据需要选择&#xff1a; 3. GitBash使用 3…

【从零开始学习深度学习】24.神经网络中池化层的作用:最大池化与平均池化

在之前二维卷积层介绍的图像物体边缘检测应用中&#xff0c;我们构造卷积核从而精确地找到了像素变化的位置。设任意二维数组X的i行j列的元素为X[i, j]。如果我们构造的卷积核输出Y[i, j]1&#xff0c;那么说明输入中X[i, j]和X[i, j1]数值不一样。这可能意味着物体边缘通过这两…

[附源码]计算机毕业设计的网上点餐系统Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; Springboot mybatis MavenVue等等组成&#xff0c;B/S模式…

自动控制原理笔记-自动控制的一般概念

目录 概念&#xff1a; 自动控制系统的基本组成&#xff1a; 基本控制方式: 一个控制系统的方框图:&#xff08;由工作原理图绘制方框图&#xff09; 控制系统的组成&#xff1a; 小结: ​​​​​​​ ​​​​​​​ ​​​​​​​ 梦开始的地方…

微火:AI绘图网站程序源码搭建,定制专属的ai绘画小程序

随着AI绘画的火热&#xff0c;群众对于AI绘画的需求与日俱增&#xff0c;目前已有的小程序、ai绘图软件已不能很好地满足当下用户的画图需求&#xff0c;经常排队生图&#xff0c;一排就是几个小时&#xff0c;或者前面直接8万人排队的现象早日屡见不鲜。 新的优秀的AI绘画小程…

java的序列化与反序列化

什么是序列化与反序列化 序列化&#xff1a; 说白了&#xff0c;就是把一个实现了Serializable接口&#xff08;只有实现了这个接口的类才能被序列化与反序列化&#xff09;的类去变成本地文件保存。 反序列化&#xff1a; 说白了&#xff0c;就是把上面保存的本地文件&#…