QML 实现好看的时钟卡片

发布时间:2026/11/5 17:42:25

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/11/3 12:35:09

ABAP报表程序通用模板

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

2026/11/3 19:18:20

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

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

2026/11/5 16:51:33

EasyU优启通PE维护盘实战指南:制作、启动与核心操作全解析

1. 为什么一个PE维护盘值得单独拿出来聊装系统、修引导、救数据、清病毒,这些活儿但凡干过几次的人都知道,真正决定效率的往往不是你手速多快,而是手边那套PE环境靠不靠谱。EasyU(圈内习惯叫优启通)就是在这个场景下被…

2026/11/5 16:26:13

Rust原来很简单二:所有权系统

Part 2 核心篇 —— 所有权系统 本篇覆盖 第 6 ~ 10 章:所有权、借用与引用、生命周期、字符串与切片、内存模型。 ⚠️ 这是全书最重要的一篇。 把它吃透,Rust 从此“很简单”;跳过它,你会处处碰壁。请慢下来,动手敲每一段代码。 第 6 章 所有权:Rust 的第一性原理 6…

2026/11/5 15:00:49

基于Hadoop+Spark的民宿智能推荐系统设计与实现

1. 项目概述:大数据时代的民宿智能推荐这个毕业设计项目融合了当前大数据领域最主流的技术栈,构建了一个完整的民宿推荐系统解决方案。作为一名长期从事大数据系统开发的工程师,我认为这个选题非常贴合实际应用场景——随着旅游业的复苏和共享…

2026/11/5 14:10:30

ADS122U04IPWR+STM32F427ZI工业级温度测量信号链设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/5 14:10:30

UART接口24位ADC搭配RS485,解决远距离温度监控难题

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/5 14:10:30

UART接口24位ADC搭配PIC18F46K22的RTD与热电偶测温实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/5 15:39:06

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/4 21:04:25

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/5 0:02:07

多智能体协作系统架构设计与实践优化

1. 多智能体协作系统的时代背景2017年Transformer架构的诞生彻底改变了人工智能的发展轨迹。当行业还沉浸在单一大模型带来的性能突破时,Anthropic的研究团队已经将目光投向了更前沿的领域——如何让多个智能体通过协作产生超越单体能力的系统智能。这就像交响乐团中…

2026/11/5 0:02:07

大模型系统性入门实战指南:从原理到部署微调的全链路路线

提到大模型的系统性入门,我先说个结论:这事真不难,难的是资料太散、路线太多,今天这个框架明天那个工具,新人很容易在“收藏夹吃灰”和“教程看到一半放弃”之间反复横跳。市面上关于大模型的内容少说有几十万篇&#…

2026/11/5 0:02:07

鸿蒙文件系统沙箱机制解析与开发实践

1. 鸿蒙文件系统沙箱机制深度解析作为一名从Android转向鸿蒙开发的工程师,我深刻体会到文件系统访问是迁移过程中最先遇到的"拦路虎"。在Android上运行良好的文件读写代码,在鸿蒙真机环境却频频出现文件写入后神秘消失或直接抛出权限拒绝错误的…

2026/11/5 15:39:06

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/4 21:04:25

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑