发布时间:2026/7/14 19:21:22
QML 实现好看的时钟卡片 前言在网上看到一个好看的时钟效果之前在华为手机上的时钟也有同样类似的效果觉得挺漂亮用qml做一个。中心显示hh:mm格式时间圆环外圈由密集刻度组成一个小黑点作为“秒针”绕圈运动并且在它经过的位置外边框会同步产生柔和的凸起动画。可以作为小组件放到项目中可用的地方。动态效果两个版本右图更细腻的高光、玻璃感和黑点层次更接近“苹果风”。 静态图效果拆解这个效果本质上可以拆成四部分一张圆角渐变卡片一个显示当前时间的居中文字一圈均匀分布的刻度线一个按秒旋转的小黑点以及它驱动的局部凸起动画实现时最关键的不是Text而是圆环和凸起部分。因为这部分更适合用Canvas动态绘制而不是用大量Rectangle拼接。1. 时间如何驱动时间数据用一个Timer每 16ms 刷新一次这样可以让黑点沿圆周连续运动而不是每秒跳一下。核心思路是把“当前秒数 毫秒”换算成0 ~ 1的进度再转成弧度property date now: new Date() property real tau: Math.PI * 2 property real secondsProgress: { var current now return (current.getSeconds() current.getMilliseconds() / 1000.0) / 60.0 } property real secondAngle: secondsProgress * tau - Math.PI / 2 property string timeText: { var hours now.getHours() var minutes now.getMinutes() return (hours 10 ? 0 : ) hours : (minutes 10 ? 0 : ) minutes } Timer { interval: 16 running: true repeat: true onTriggered: { root.now new Date() ringCanvas.requestPaint() } }这里的- Math.PI / 2是为了让 0 秒从正上方开始而不是从三点钟方向开始。2. 为什么用 Canvas 画圆环如果只画固定刻度用Repeater Rectangle也能做。但这个需求有两个动态变化小黑点要连续旋转黑点附近的外轮廓和刻度要出现“局部鼓起”的动画这类局部变形更适合用Canvas因为你可以直接基于角度、半径和曲线公式逐帧计算每个点的位置。基础版里Canvas主要画三样东西外圈轮廓120 根刻度线黑点和它的阴影3. 凸起效果怎么做核心做法是先计算圆环上任意一个点和“当前秒针角度”的角度差然后让这个差值经过一个高斯函数得到一个平滑的影响值。代码如下function wrapAngle(value) { while (value Math.PI) value - root.tau while (value -Math.PI) value root.tau return value } function gaussian(delta, spread) { return Math.exp(-(delta * delta) / (2 * spread * spread)) }wrapAngle()用来把角度差归一化到[-PI, PI]避免跨越 0/360 度时出现跳变。gaussian()返回一个 0 到 1 之间的值。角度越靠近黑点这个值越接近 1离得越远值就越快衰减到 0。然后把这个值用于控制外轮廓半径增加多少刻度线向外拉长多少刻度线宽度和透明度增加多少例如基础版的外轮廓for (var i 0; i 180; i) { var t i / 180 var arcAngle t * root.tau - Math.PI / 2 var delta wrapAngle(arcAngle - angle) var bulge gaussian(delta, 0.28) * 9 var radius tickOuter - 5 bulge var px cx Math.cos(arcAngle) * radius var py cy Math.sin(arcAngle) * radius if (i 0) ctx.moveTo(px, py) else ctx.lineTo(px, py) }这样得到的凸起不是硬切换而是像果冻一样从中心向两边平滑过渡。4. 小黑点如何绘制黑点位置直接由当前角度和轨道半径计算得到var dotOrbit baseRadius 3 var dotX cx Math.cos(angle) * dotOrbit var dotY cy Math.sin(angle) * dotOrbit基础版只给它加了一个柔和阴影。增强版额外做了三层处理一层投影让它更像悬浮在界面上一层微弱白色辉光让局部更有通透感一层深灰渐变实体再加一个小高光点这三层叠起来后黑点会更接近系统级 UI 的细节质感。5. 苹果风版本做了哪些增强main_apple.qml的结构没有变重点是把质感往前推了一步背景渐变更细过渡更平滑顶部增加半透明高光模拟玻璃反射中间增加一层很淡的内圈描边让中心区域更干净外圈凸起更锐一点但仍然保持平滑黑点增加投影、渐变和高光时间文字改成更紧凑的字距看起来更像系统时钟组件也就是说增强版不是换一种实现方式而是在同一套几何逻辑上继续做材质和层次优化。基础版完整代码import QtQuick 2.15 import QtQuick.Window 2.15 Window { id: root visible: true width: 440 height: 440 color: transparent title: Clock Card // flags: Qt.FramelessWindowHint property date now: new Date() property real tau: Math.PI * 2 property real secondsProgress: { var current now return (current.getSeconds() current.getMilliseconds() / 1000.0) / 60.0 } property real secondAngle: secondsProgress * tau - Math.PI / 2 property string timeText: { var hours now.getHours() var minutes now.getMinutes() return (hours 10 ? 0 : ) hours : (minutes 10 ? 0 : ) minutes } Timer { id: clockTimer interval: 16 running: true repeat: true onTriggered: { root.now new Date() ringCanvas.requestPaint() } } Rectangle { id: card anchors.fill: parent anchors.margins: 4 radius: 24 antialiasing: true clip: true gradient: Gradient { GradientStop { position: 0.0; color: #32d12d } GradientStop { position: 0.52; color: #3fd35e } GradientStop { position: 1.0; color: #58d6cf } } Canvas { id: ringCanvas anchors.fill: parent anchors.margins: 22 antialiasing: true renderStrategy: Canvas.Threaded function wrapAngle(value) { while (value Math.PI) value - root.tau while (value -Math.PI) value root.tau return value } function gaussian(delta, spread) { return Math.exp(-(delta * delta) / (2 * spread * spread)) } onPaint: { var ctx getContext(2d) ctx.reset() var w width var h height var cx w / 2 var cy h / 2 var baseRadius Math.min(w, h) * 0.36 var tickInner baseRadius 6 var tickOuter baseRadius 32 var angle root.secondAngle var halo ctx.createRadialGradient(cx, cy, baseRadius * 0.2, cx, cy, tickOuter 28) halo.addColorStop(0.0, rgba(255,255,255,0.00)) halo.addColorStop(0.7, rgba(255,255,255,0.05)) halo.addColorStop(1.0, rgba(255,255,255,0.00)) ctx.fillStyle halo ctx.fillRect(0, 0, w, h) ctx.beginPath() for (var i 0; i 180; i) { var t i / 180 var arcAngle t * root.tau - Math.PI / 2 var delta wrapAngle(arcAngle - angle) var bulge gaussian(delta, 0.28) * 9 var radius tickOuter - 5 bulge var px cx Math.cos(arcAngle) * radius var py cy Math.sin(arcAngle) * radius if (i 0) ctx.moveTo(px, py) else ctx.lineTo(px, py) } ctx.strokeStyle rgba(255,255,255,0.18) ctx.lineWidth 3 ctx.lineJoin round ctx.lineCap round ctx.stroke() var totalTicks 120 for (var tick 0; tick totalTicks; tick) { var tickAngle tick / totalTicks * root.tau - Math.PI / 2 var tickDelta wrapAngle(tickAngle - angle) var emphasis gaussian(tickDelta, 0.20) var extraOuter emphasis * 17 var extraInner emphasis * 4 var lineWidth 2.0 emphasis * 1.8 var alpha 0.92 emphasis * 0.08 var innerRadius tickInner - extraInner var outerRadius tickOuter extraOuter var x1 cx Math.cos(tickAngle) * innerRadius var y1 cy Math.sin(tickAngle) * innerRadius var x2 cx Math.cos(tickAngle) * outerRadius var y2 cy Math.sin(tickAngle) * outerRadius ctx.beginPath() ctx.moveTo(x1, y1) ctx.lineTo(x2, y2) ctx.strokeStyle rgba(255,255,255, alpha.toFixed(3) ) ctx.lineWidth lineWidth ctx.lineCap round ctx.stroke() } var dotRadius 9 var dotOrbit baseRadius 3 var dotX cx Math.cos(angle) * dotOrbit var dotY cy Math.sin(angle) * dotOrbit var glow ctx.createRadialGradient(dotX, dotY, 0, dotX, dotY, 28) glow.addColorStop(0.0, rgba(0,0,0,0.18)) glow.addColorStop(1.0, rgba(0,0,0,0.00)) ctx.fillStyle glow ctx.beginPath() ctx.arc(dotX, dotY, 28, 0, root.tau, false) ctx.fill() ctx.beginPath() ctx.arc(dotX, dotY, dotRadius, 0, root.tau, false) ctx.fillStyle #2c2c2c ctx.fill() } } Repeater { model: 90 Rectangle { width: 1.4 height: 1.4 radius: width / 2 color: Qt.rgba(255,255,255,0.035) x: Math.random() * card.width y: Math.random() * card.height } } Text { anchors.centerIn: parent // anchors.verticalCenterOffset: 14 text: root.timeText color: white font.pixelSize: 102 // font.bold: true font.weight: Font.Light font.family: Segoe UI renderType: Text.NativeRendering } } }苹果质感版完整代码import QtQuick 2.15 import QtQuick.Window 2.15 Window { id: root visible: true width: 440 height: 440 color: #f5f5f7 title: Clock Card Apple property date now: new Date() property real tau: Math.PI * 2 property real secondsProgress: { var current now return (current.getSeconds() current.getMilliseconds() / 1000.0) / 60.0 } property real secondAngle: secondsProgress * tau - Math.PI / 2 property string timeText: { var hours now.getHours() var minutes now.getMinutes() return (hours 10 ? 0 : ) hours : (minutes 10 ? 0 : ) minutes } Timer { interval: 16 running: true repeat: true onTriggered: { root.now new Date() ringCanvas.requestPaint() } } Rectangle { id: card anchors.fill: parent anchors.margins: 6 radius: 26 antialiasing: true clip: true gradient: Gradient { GradientStop { position: 0.0; color: #39cf42 } GradientStop { position: 0.38; color: #4ad064 } GradientStop { position: 0.72; color: #58d59b } GradientStop { position: 1.0; color: #67d9d2 } } Rectangle { anchors.fill: parent color: transparent opacity: 0.95 gradient: Gradient { GradientStop { position: 0.0; color: #33ffffff } GradientStop { position: 0.22; color: #14ffffff } GradientStop { position: 0.55; color: #00ffffff } GradientStop { position: 1.0; color: #0a000000 } } } Rectangle { width: card.width * 0.92 height: card.height * 0.48 anchors.horizontalCenter: parent.horizontalCenter y: -card.height * 0.05 radius: width / 2 color: transparent rotation: -8 opacity: 0.42 gradient: Gradient { GradientStop { position: 0.0; color: #3dffffff } GradientStop { position: 0.5; color: #0dffffff } GradientStop { position: 1.0; color: #00ffffff } } } Rectangle { width: card.width * 0.72 height: card.height * 0.72 anchors.centerIn: parent radius: width / 2 color: transparent border.width: 1 border.color: #1fffffff opacity: 0.6 } Canvas { id: ringCanvas anchors.fill: parent anchors.margins: 20 antialiasing: true renderStrategy: Canvas.Threaded function wrapAngle(value) { while (value Math.PI) value - root.tau while (value -Math.PI) value root.tau return value } function gaussian(delta, spread) { return Math.exp(-(delta * delta) / (2 * spread * spread)) } onPaint: { var ctx getContext(2d) ctx.reset() var w width var h height var cx w / 2 var cy h / 2 var baseRadius Math.min(w, h) * 0.355 var ringInner baseRadius 8 var ringOuter baseRadius 34 var angle root.secondAngle var ambient ctx.createRadialGradient(cx, cy, baseRadius * 0.15, cx, cy, ringOuter 36) ambient.addColorStop(0.0, rgba(255,255,255,0.00)) ambient.addColorStop(0.62, rgba(255,255,255,0.05)) ambient.addColorStop(1.0, rgba(255,255,255,0.00)) ctx.fillStyle ambient ctx.fillRect(0, 0, w, h) ctx.beginPath() for (var i 0; i 220; i) { var t i / 220 var arcAngle t * root.tau - Math.PI / 2 var delta wrapAngle(arcAngle - angle) var bulge gaussian(delta, 0.24) * 11 var radius ringOuter - 6 bulge var px cx Math.cos(arcAngle) * radius var py cy Math.sin(arcAngle) * radius if (i 0) ctx.moveTo(px, py) else ctx.lineTo(px, py) } ctx.strokeStyle rgba(255,255,255,0.16) ctx.lineWidth 3.5 ctx.lineJoin round ctx.lineCap round ctx.stroke() ctx.beginPath() ctx.arc(cx, cy, ringInner - 3, 0, root.tau, false) ctx.strokeStyle rgba(255,255,255,0.10) ctx.lineWidth 1.2 ctx.stroke() var totalTicks 120 for (var tick 0; tick totalTicks; tick) { var tickAngle tick / totalTicks * root.tau - Math.PI / 2 var deltaTick wrapAngle(tickAngle - angle) var emphasis gaussian(deltaTick, 0.18) var stretch emphasis * 18 var pull emphasis * 5 var lineWidth 2.1 emphasis * 1.7 var alpha 0.86 emphasis * 0.14 var innerRadius ringInner - pull var outerRadius ringOuter stretch var x1 cx Math.cos(tickAngle) * innerRadius var y1 cy Math.sin(tickAngle) * innerRadius var x2 cx Math.cos(tickAngle) * outerRadius var y2 cy Math.sin(tickAngle) * outerRadius ctx.beginPath() ctx.moveTo(x1, y1) ctx.lineTo(x2, y2) ctx.strokeStyle rgba(255,255,255, alpha.toFixed(3) ) ctx.lineWidth lineWidth ctx.lineCap round ctx.stroke() } var dotOrbit baseRadius 5 var dotX cx Math.cos(angle) * dotOrbit var dotY cy Math.sin(angle) * dotOrbit var shadow ctx.createRadialGradient(dotX 2, dotY 3, 0, dotX 2, dotY 3, 24) shadow.addColorStop(0.0, rgba(0,0,0,0.22)) shadow.addColorStop(1.0, rgba(0,0,0,0.00)) ctx.fillStyle shadow ctx.beginPath() ctx.arc(dotX 2, dotY 3, 24, 0, root.tau, false) ctx.fill() var glow ctx.createRadialGradient(dotX, dotY, 0, dotX, dotY, 18) glow.addColorStop(0.0, rgba(255,255,255,0.22)) glow.addColorStop(1.0, rgba(255,255,255,0.00)) ctx.fillStyle glow ctx.beginPath() ctx.arc(dotX, dotY, 18, 0, root.tau, false) ctx.fill() ctx.beginPath() ctx.arc(dotX, dotY, 8.5, 0, root.tau, false) var dotFill ctx.createLinearGradient(dotX - 8, dotY - 8, dotX 8, dotY 8) dotFill.addColorStop(0.0, #4a4a4d) dotFill.addColorStop(0.45, #2d2d30) dotFill.addColorStop(1.0, #161618) ctx.fillStyle dotFill ctx.fill() ctx.beginPath() ctx.arc(dotX - 2.5, dotY - 2.8, 2.2, 0, root.tau, false) ctx.fillStyle rgba(255,255,255,0.20) ctx.fill() } } Repeater { model: 120 Rectangle { width: 1.2 height: 1.2 radius: width / 2 color: #07ffffff x: Math.random() * card.width y: Math.random() * card.height } } Text { anchors.centerIn: parent // anchors.verticalCenterOffset: 16 text: root.timeText color: #faffffff font.pixelSize: 102 font.weight: Font.Light font.family: Helvetica Neue font.letterSpacing: -2.2 renderType: Text.NativeRendering } } }总结这个效果的关键不是“画一个圆”而是把角度、半径和时间进度关联起来再通过一个平滑函数把局部变化控制住。这样你就可以得到连续运动的秒针黑点跟随黑点移动的局部凸起不突兀的过渡动画可以持续迭代的视觉层次如果后面还想继续做可以再往这几个方向扩展加上小时和分钟的环形层把黑点拖尾做成流体光晕用ShaderEffect进一步加强玻璃和折射质感做成桌面时钟或车机小组件

相关新闻

2026/7/14 19:21:22

ABAP报表程序通用模板

主程序INCLUDE zmm001_top. " 全局数据声明与常量 INCLUDE zmm001_sel. " 选择屏幕定义 INCLUDE zmm001_f01. " 子例程与业务逻辑INITIALIZATION.PERFORM frm_init_data. " 初始化默认值START-OF-SELECTION.PERFORM frm_get_data. " 数据抽取…

2026/7/14 19:16:21

STM32——库函数版——12864点阵屏图形绘制与波形显示实战

1. 12864点阵屏基础与STM32驱动准备第一次用STM32驱动12864点阵屏时,我对着数据手册研究了整整三天。这种单色液晶屏虽然看起来简单,但要想玩转图形绘制和波形显示,得先摸清它的脾气。市面上常见的12864屏主要分两种:带字库的ST79…

2026/7/14 20:31:27

Windows组策略审计功能修复:AuditPolicyGPInterop.dll丢失解决方案

1. AuditPolicyGPInterop.dll文件丢失问题的本质解析当系统提示"AuditPolicyGPInterop.dll文件丢失"时,这通常意味着Windows组策略审计功能的关键组件出现了问题。这个dll文件是Windows操作系统中的一个动态链接库,主要负责组策略对象&#xf…

2026/7/14 20:31:27

ROS 使用笔记

1. 什么是 ROS? ROS (Robot Operating System) 是一个适用于机器人的开源的元操作系统。它提供了操作系统应有的服务,包括 硬件抽象底层设备控制常用函数的实现进程间消息传递包管理用于获取、编译、编写、和跨计算机运行代码所需的工具和库函数 2. 为…

2026/7/14 20:31:27

STM32F101ZG与CMT-8540S-SMT音频系统设计与优化

1. STM32F101ZG与CMT-8540S-SMT的硬件协同设计1.1 微控制器选型考量STM32F101ZG作为STMicroelectronics的Cortex-M3内核MCU,其144引脚封装和1024KB闪存容量为音频处理提供了充足资源。在实际项目中,我特别看重它的以下特性:72MHz主频&#xf…

2026/7/14 20:31:27

CIMPro视频融合宽高比调整:解决画面拉伸变形的实战指南

在数字孪生项目开发中,视频融合是连接现实世界与虚拟空间的关键技术环节。很多开发者在CIMPro平台上进行视频融合时,经常会遇到画面拉伸变形、黑边显示不全或比例失调等问题,这些问题的核心往往源于宽高比配置不当。本文将系统讲解CIMPro视频…

2026/7/14 20:31:27

基于事件驱动架构的Java外卖霸王餐试吃流程状态变更通知系统设计

基于事件驱动架构的Java外卖霸王餐试吃流程状态变更通知系统设计 在外卖霸王餐试吃活动中,订单状态(如“已报名”、“审核中”、“已中选”、“待评价”)的变更需要实时通知用户、运营后台、积分系统等多个下游模块。若采用传统的同步调用&am…

2026/7/14 20:26:26

GitHub爆火HTML教程:交互式学习与实战解析

1. 项目背景与火爆现象 上周GitHub上一个名为"html-tutorial"的开源项目突然爆火,短短几天内就斩获6100 Star,成为近期最受开发者关注的学习资源库。这个由国内开发者jaywcjlove维护的HTML教程仓库,用最接地气的方式系统梳理了从HT…

2026/7/14 12:47:32

3步解锁音乐自由:ncmdumpGUI终极NCM文件解密转换指南

3步解锁音乐自由:ncmdumpGUI终极NCM文件解密转换指南 【免费下载链接】ncmdumpGUI C#版本网易云音乐ncm文件格式转换,Windows图形界面版本 项目地址: https://gitcode.com/gh_mirrors/nc/ncmdumpGUI 你是否曾在网易云音乐下载了心爱的歌曲&#…

2026/7/14 19:23:19

CANoe 19 SP3 配置 GB/T 27930-2023 A类系统:3步搭建BMS仿真测试环境

CANoe 19 SP3 配置 GB/T 27930-2023 A类系统:3步搭建BMS仿真测试环境随着新能源汽车行业的快速发展,充电通信协议的标准化和测试验证变得尤为重要。GB/T 27930-2023作为中国智能充电协议的最新版本,对充电机与电动汽车之间的通信提出了更严格…

2026/7/14 18:30:06

3步搞定RTL8852BE驱动:从零开始配置Wi-Fi 6网卡

3步搞定RTL8852BE驱动:从零开始配置Wi-Fi 6网卡 【免费下载链接】rtl8852be Realtek Linux WLAN Driver for RTL8852BE 项目地址: https://gitcode.com/gh_mirrors/rt/rtl8852be 还在为Linux系统无法识别RTL8852BE Wi-Fi 6网卡而烦恼吗?&#x1f…

2026/7/14 0:04:21

5分钟掌握足球PBR材质制作:Photoshop与Unity高效工作流

1. 项目概述:为什么是足球PBR材质?在游戏开发,尤其是体育竞技类游戏的制作中,一个看起来“对味”的足球,往往比我们想象中更重要。它不仅是赛场上的核心道具,更是玩家视觉焦点和沉浸感的重要来源。一个塑料…

2026/7/14 0:04:21

ChatGPT联网搜索失败,92%开发者误判为网络问题——真实根因竟是LLM推理会话上下文污染导致Search Agent进程静默退出(含strace复现脚本)

更多请点击: https://intelliparadigm.com 第一章:ChatGPT 联网搜索失败 当 ChatGPT 的联网搜索功能无法正常工作时,用户常遇到“搜索不可用”“未连接到互联网”或空白响应等现象。该问题并非模型本身缺陷,而是由权限配置、网络…

2026/7/14 12:47:31

3个高效策略:快速掌握Axure中文界面配置

3个高效策略:快速掌握Axure中文界面配置 【免费下载链接】axure-cn Chinese language file for Axure RP. Axure RP 简体中文语言包。支持 Axure 11、10、9。不定期更新。 项目地址: https://gitcode.com/gh_mirrors/ax/axure-cn 还在为Axure RP的英文界面感…