百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

Compose Desktop 初体验之绘制 composer 教程

bigegpt 2024-10-12 05:15 9 浏览

从 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")
        }
    }
}

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

下面来看下运行效果:

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

系统菜单

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

别的应用有,我们当然也想要!那咱们的 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 ,故名思义,分别是条目、分隔符、复选框和单选框。

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

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

托盘及通知

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

托盘

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

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

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

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

这块还有一个小知识点,咱们有时候使用的一些工具其实都没有真正页面,只是在系统托盘中存在,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

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

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

相关推荐

得物可观测平台架构升级:基于GreptimeDB的全新监控体系实践

一、摘要在前端可观测分析场景中,需要实时观测并处理多地、多环境的运行情况,以保障Web应用和移动端的可用性与性能。传统方案往往依赖代理Agent→消息队列→流计算引擎→OLAP存储...

warm-flow新春版:网关直连和流程图重构

本期主要解决了网关直连和流程图重构,可以自此之后可支持各种复杂的网关混合、多网关直连使用。-新增Ruoyi-Vue-Plus优秀开源集成案例更新日志[feat]导入、导出和保存等新增json格式支持...

扣子空间体验报告

在数字化时代,智能工具的应用正不断拓展到我们工作和生活的各个角落。从任务规划到项目执行,再到任务管理,作者深入探讨了这款工具在不同场景下的表现和潜力。通过具体的应用实例,文章展示了扣子空间如何帮助用户...

spider-flow:开源的可视化方式定义爬虫方案

spider-flow简介spider-flow是一个爬虫平台,以可视化推拽方式定义爬取流程,无需代码即可实现一个爬虫服务。spider-flow特性支持css选择器、正则提取支持JSON/XML格式...

solon-flow 你好世界!

solon-flow是一个基础级的流处理引擎(可用于业务规则、决策处理、计算编排、流程审批等......)。提供有“开放式”驱动定制支持,像jdbc有mysql或pgsql等驱动,可...

新一代开源爬虫平台:SpiderFlow

SpiderFlow:新一代爬虫平台,以图形化方式定义爬虫流程,不写代码即可完成爬虫。-精选真开源,释放新价值。概览Spider-Flow是一个开源的、面向所有用户的Web端爬虫构建平台,它使用Ja...

通过 SQL 训练机器学习模型的引擎

关注薪资待遇的同学应该知道,机器学习相关的岗位工资普遍偏高啊。同时随着各种通用机器学习框架的出现,机器学习的门槛也在逐渐降低,训练一个简单的机器学习模型变得不那么难。但是不得不承认对于一些数据相关的工...

鼠须管输入法rime for Mac

鼠须管输入法forMac是一款十分新颖的跨平台输入法软件,全名是中州韵输入法引擎,鼠须管输入法mac版不仅仅是一个输入法,而是一个输入法算法框架。Rime的基础架构十分精良,一套算法支持了拼音、...

Go语言 1.20 版本正式发布:新版详细介绍

Go1.20简介最新的Go版本1.20在Go1.19发布六个月后发布。它的大部分更改都在工具链、运行时和库的实现中。一如既往,该版本保持了Go1的兼容性承诺。我们期望几乎所...

iOS 10平台SpriteKit新特性之Tile Maps(上)

简介苹果公司在WWDC2016大会上向人们展示了一大批新的好东西。其中之一就是SpriteKitTileEditor。这款工具易于上手,而且看起来速度特别快。在本教程中,你将了解关于TileE...

程序员简历例句—范例Java、Python、C++模板

个人简介通用简介:有良好的代码风格,通过添加注释提高代码可读性,注重代码质量,研读过XXX,XXX等多个开源项目源码从而学习增强代码的健壮性与扩展性。具备良好的代码编程习惯及文档编写能力,参与多个高...

Telerik UI for iOS Q3 2015正式发布

近日,TelerikUIforiOS正式发布了Q32015。新版本新增对XCode7、Swift2.0和iOS9的支持,同时还新增了对数轴、不连续的日期时间轴等;改进TKDataPoin...

ios使用ijkplayer+nginx进行视频直播

上两节,我们讲到使用nginx和ngixn的rtmp模块搭建直播的服务器,接着我们讲解了在Android使用ijkplayer来作为我们的视频直播播放器,整个过程中,需要注意的就是ijlplayer编...

IOS技术分享|iOS快速生成开发文档(一)

前言对于开发人员而言,文档的作用不言而喻。文档不仅可以提高软件开发效率,还能便于以后的软件开发、使用和维护。本文主要讲述Objective-C快速生成开发文档工具appledoc。简介apple...

macOS下配置VS Code C++开发环境

本文介绍在苹果macOS操作系统下,配置VisualStudioCode的C/C++开发环境的过程,本环境使用Clang/LLVM编译器和调试器。一、前置条件本文默认前置条件是,您的开发设备已...