# Timer **Repository Path**: nixius/Timer ## Basic Information - **Project Name**: Timer - **Description**: No description available - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-19 - **Last Updated**: 2026-09-24 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Timer · 跨平台桌面计时工具 一个使用 **Python 3.8 + PySide2(Qt 5.15)** 开发的桌面计时工具,包含 **模拟时钟 / 数字时钟 / 倒计时 / 正计时** 四大功能模块。 内置**浅色(默认)/ 深色**两套主题,界面简洁现代、强调色统一,支持窗口任意缩放自适应、支持无边框窗口与窗口置顶,可打包为 Windows / Linux 单文件可执行程序。 > 如果喜欢网页版,可查看 [WebTimer 项目](https://gitee.com/nixius/web-timer),单HTML文件,无任何网络依赖,可离线使用,除窗口特定功能外,主要功能与本项目完全一致。 **程序截图:** ![模拟时钟](screenshorts/模拟时钟.png) ![数字时钟](screenshorts/数字时钟.png) ![倒计时](screenshorts/倒计时.png) ![正计时](screenshorts/正计时.png) --- ## 一、功能特性 | 模块 | 主要功能 | | --- | --- | | 模拟时钟 | 表盘 / 12 整点刻度 / 60 分钟刻度、时针 / 分针 / 秒针、秒针毫秒级平滑走字、中心圆点与指针尾巴、日期与星期显示;秒针与分钟刻度可通过配置关闭 | | 数字时钟 | 大字号时间(字号随窗口自动缩放)、日期 + 中文星期、12/24 小时制切换、冒号闪烁(不显示秒时自动禁用) | | 倒计时 | 配置化快速预设按钮、自定义时长输入(`HH:MM:SS` / `MM:SS` / 纯数字分钟 / `1h30m`)、开始 / 暂停 / 继续 / 重置、进度条、结束提示音 + 弹窗 + 数字闪烁 + 任务栏闪烁、计时中随时打点 | | 正计时 | 从 `00:00:00` 开始累加、开始 / 暂停 / 继续 / 重置、打点(Lap) | | 打点记录 | 序号 / 相对时间 / 绝对时间 / 与上次间隔,卡片式表格、按记录分组着色、最新一条高亮、空态提示,支持**双击条目添加文字备注**、右键编辑 / 清除备注、复制 / 删除单条、清空,导出 CSV / TXT(**含备注列**),重置时二次确认是否清空 | | 分栏与折叠 | 倒计时 / 正计时采用左右分栏:拖动中间的分隔条可调整比例(默认右侧只占最小可用宽度,比例写回配置);计时区右上角的悬浮箭头可一键收起右侧面板,让计时区占满窗口,收起后该处会补一个悬浮「打点」入口 | | 倒计时 | 环形进度 + 剩余时间大字,状态文字下方常驻显示**本次设定的总时长**(如「总时长 5 分钟」),避免「还剩多少 / 一共多少」分不清 | | 快捷开关 | 时钟页底部提供「秒针 / 平滑走字 / 日期」「24 小时制 / 秒 / 冒号闪烁 / 日期」,倒计时页提供「毫秒 / 提示音 / 结束弹窗」,正计时页提供「毫秒」——点一下即生效,不必手动改配置 | | 窗口能力 | **浅色 / 深色主题实时切换**、**色盘自选强调色**、无边框模式(自绘标题栏、拖动、边缘拉伸、双击最大化)、窗口置顶开关、**Win + 方向键与拖到屏幕边缘的分屏贴靠**、**「关于」对话框** | **界面交互细节** * 无边框模式下:按住标题栏空白处拖动窗口、鼠标移到窗口边缘 / 四角切换光标并可直接拉伸(左上 / 右下等六个方向都精确跟手)、双击标题栏最大化 / 还原。 * **顶栏三段式布局**:左品牌、中功能区、右窗口按钮。左右两个占位容器会被设成**等宽**(`TitleBar._balance_holders`),因此功能区是相对**整个窗口**居中,而不是「剩余空间的中点」。 * **窄窗口自适应**:宽度不足时功能区自动切换为**紧凑模式**——导航只显示图标、不显示文字(tooltip 仍在),避免挤成一团。阈值由字体度量解析式计算,不会随布局缓存抖动。 * **系统级贴靠**:`Win + ←/→` 左右分屏、`Win + ↑` 最大化、`Win + ↓` 还原,与普通窗口完全一致;把窗口拖到屏幕上边缘会最大化、拖到左右边缘会各占半屏。 * 标题栏右侧依次是 **主题开关**、**强调色(色盘)按钮**、**置顶开关**、**无边框开关**、**「关于」按钮**;无边框模式下还会显示自绘的最小化 / 最大化 / 关闭按钮。 * **强调色**:点开系统色盘任选颜色,立即应用到导航选中态、按钮、指针、进度弧、表格等全部强调位置,并写回 `config.json` 的 `theme.accent_color`。**自己挑的颜色在切换浅色 / 深色主题时会保留**(只有仍是内置预设色的项才跟随主题替换)。 * **关于**:显示程序名称、版本号、作者(chaos)、技术栈与开源协议。 * 强调色(`theme.accent_color`)统一作用于:导航栏选中态、按钮悬停与主按钮、模拟时钟指针与整点刻度、数字时钟时间文字、倒计时进度条、打点按钮、输入框聚焦边框、表格选中行、滚动条悬停。 * 切换主题时无需重启:样式表、标题栏自绘按钮、两个矢量时钟页面会立即重绘为新配色。 --- ## 二、环境要求 * **Python 3.8**(必须,项目未使用任何 3.9+ 语法特性) * PySide2 `5.15.2.1`(Qt 5.15)——见下方「为什么是 Qt 5 而不是 Qt 6」 * PyInstaller `>=5.13,<7.0`(仅在打包时需要) ### 安装依赖 ```bash pip install -r requirements.txt ``` > 如果同时存在多个 Python 版本,请显式指定解释器,例如: > `C:\Python38\python.exe -m pip install -r requirements.txt` > 或 `python3.8 -m pip install -r requirements.txt` --- ## 三、运行方式 ```bash python main.py ``` 首次运行会在**程序同级目录**自动生成 `config.json`;若该目录不可写(例如安装在 `Program Files`、`/usr/bin`), 会自动回退到用户目录 `~/.timer/config.json`。 ### 快捷键 | 快捷键 | 作用 | | --- | --- | | `Space`(空格) | 当前页面打点(倒计时 / 正计时);输入框获得焦点时空格正常输入 | | `Ctrl+1` ~ `Ctrl+4` | 依次切换到 模拟时钟 / 数字时钟 / 倒计时 / 正计时 | --- ## 四、配置文件说明(`config.json`) 配置文件为 JSON 格式,**修改后重启程序生效**;界面上的「置顶」「无边框」开关会实时写回该文件。 程序内置了**深度合并(deep merge)**与**类型 / 取值校验**: * 老配置文件缺少新版本新增的配置项时,会自动补齐默认值; * 文件损坏(非法 JSON)或字段类型错误时自动回退默认值,并把修复后的配置写回磁盘; * 颜色值格式非法(不是 `#RGB` / `#RRGGBB`)时回退到默认强调色。 ### 4.1 `window` —— 窗口 | 配置项 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | `width` | int | `900` | 启动时的窗口宽度(若小于 `min_width` 会被提升到 `min_width`) | | `height` | int | `640` | 启动时的窗口高度 | | `min_width` | int | `800` | 最小窗口宽度,拖动边缘拉伸时不会被突破 | | `min_height` | int | `600` | 最小窗口高度 | | `frameless` | bool | `true` | **是否启用无边框模式**。`true` 时使用自绘标题栏,支持拖动、边缘拉伸、双击最大化;也可用界面右上角的「无边框」按钮实时切换 | | `always_on_top` | bool | `false` | **窗口是否置顶**。可用界面右上角的「置顶」按钮实时切换,切换后状态写回本项 | > 关闭程序时,当前窗口尺寸会自动写回 `width` / `height`(最大化状态下不写回),下次启动沿用。 ### 4.2 `theme` —— 主题(浅色 / 深色) 程序内置**浅色**与**深色**两套主题,**默认浅色**。切换方式有三种(**只会替换仍是内置预设的颜色,你自己挑过的颜色会保留**): 1. 修改 `config.json` 里的 `theme.mode`(`"light"` / `"dark"`)后重启; 2. 点击窗口右上角的 **「深色」开关**(勾选 = 深色,取消 = 浅色),切换即时生效并写回配置; 3. 直接修改下面任意一个颜色项来自定义配色。 两套内置配色: | 配置项 | 深色(默认) | 浅色 | 说明 | | --- | --- | --- | --- | | `mode` | `"dark"` | `"light"` | 主题模式,决定使用哪套内置配色 | | `accent_color` | `#8FBC8F` | `#386338` | **强调色**。导航选中态、按钮悬停与主按钮、模拟时钟指针与整点刻度、数字时钟文字、环形进度弧、打点按钮、输入框聚焦边框、标签选中态、表格选中行、滚动条悬停等统一使用该颜色 | | `background_color` | `#14161B` | `#F1F3F8` | 窗口背景色。卡片色、凹槽色、分隔线色都会在此基础上自动推导:**抬升面永远朝白色混合**(浅色主题下卡片比背景更白,深色主题下卡片比背景更亮) | | `text_color` | `#F1F5F9` | `#0F172A` | 主文字颜色(标题、时间数字、表格内容) | | `secondary_text_color` | `#94A3B8` | `#64748B` | 次要文字颜色(日期、星期、状态、小节标题、未选中的导航项) | | `border_color` | `#2A303C` | `#E2E8F0` | 边框 / 分隔线颜色(窗口外框、卡片、输入框、表格、标题栏分隔线) | > 默认的浅色主题使用 `#2563EB` 作为强调色;深色主题用更亮的 `#60A5FA`。 > 两套强调色都经过对比度检查:作为文字或按钮底色时都能看清。 > 想换成别的颜色时,深浅两套主题可以各配一次。 **配色与 `mode` 的同步规则** * 只把 `mode` 从 `light` 改成 `dark`(或反过来)时,程序会识别出当前颜色正好是 另一套内置配色,从而**整体替换**为新模式的配色; * 若你手工改过某个颜色(例如 `"accent_color": "#FF5722"`),它既不等于当前模式的 预设、也不等于另一模式的预设,切换 `mode` 时会被**原样保留**; * 颜色格式非法(不是 `#RGB` / `#RRGGBB`)时回退到当前模式的预设色; * 界面上的「深色」开关属于显式切换主题,会把整套配色重置为该模式的预设值。 颜色必须为 `#RGB` 或 `#RRGGBB` 形式,例如把强调色改成橙色:`"accent_color": "#FF9800"`。 ### 4.3 `clock` —— 时钟 | 配置项 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | `hour_format` | int | `24` | 数字时钟的小时制,只能是 `12` 或 `24`;`12` 时右侧显示「上午 / 下午」。非法值回退为 `24` | | `analog.show_date` | bool | `false` | **模拟时钟**是否在表盘内显示日期与星期;与数字时钟**互相独立** | | `digital.show_date` | bool | `true` | **数字时钟**是否在时间下方显示日期与星期;与模拟时钟**互相独立** | | `smooth_second_hand` | bool | `true` | 模拟时钟秒针是否平滑走字(按微秒连续旋转);`false` 时为跳秒 | | `analog.show_seconds` | bool | `true` | **模拟时钟是否绘制秒针**。为 `false` 时不画秒针,且表盘只保留 12 个整点刻度(省略 60 分钟刻度) | | `digital.show_seconds` | bool | `true` | **数字时钟是否显示秒**。为 `true` 时显示 `HH:MM:SS`;为 `false` 时显示 `HH:MM`,且不绘制冒号闪烁。字号会自动适配,两种模式下都不会溢出 | | `digital.blink_colon` | bool | `false` | 冒号是否闪烁(仅在 `digital.show_seconds` 为 `true` 时生效) | > `analog.show_seconds` 与 `digital.show_seconds` 相互独立,可以只关掉其中一个。 ### 4.4 `countdown` —— 倒计时 | 配置项 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | `presets` | array | 见下 | **快速预设按钮列表**,可自由增删改。每项为 `{"label": "按钮文字", "seconds": 秒数}`,`seconds` 必须大于 0,非法条目会被自动忽略 | | `show_milliseconds` | bool | `false` | **是否显示毫秒**。`true` 显示 `HH:MM:SS.mmm`,`false` 显示 `HH:MM:SS`。打点记录**始终保存毫秒精度**,仅显示时截断;时间文字使用等宽字体,两种模式下都不会抖动 | | `sound_enabled` | bool | `true` | 倒计时结束时是否播放提示音(`resources/alert.wav`,缺失时退化为系统蜂鸣) | | `popup_on_finish` | bool | `true` | 倒计时结束时是否弹出提醒对话框(无论是否弹窗,时间数字都会闪烁、窗口会被提到最前并请求任务栏闪烁) | | `split_ratio` | float | `0.0` | 左侧计时区在左右分栏中的占比。**默认 `0.0` 表示「自动」:右侧面板只占它的最小可用宽度(约 320px),其余全部留给计时区**;拖动中间的分隔条可改成固定比例(`0.40`~`0.92`),松手后自动写回本项。想恢复自动,把它改回 `0` | | `side_panel` | bool | `true` | 右侧控制面板是否展开(倒计时默认展开)。计时区右上角的悬浮箭头可以随时收起右侧面板,让计时区占满窗口(收起状态会写回本项) | 默认预设: ```json "presets": [ { "label": "5 分钟", "seconds": 300 }, { "label": "10 分钟", "seconds": 600 }, { "label": "30 分钟", "seconds": 1800 }, { "label": "1 小时", "seconds": 3600 } ] ``` 自定义一个「番茄钟 25 分钟」预设: ```json { "label": "番茄钟", "seconds": 1500 } ``` ### 4.5 `stopwatch` —— 正计时 | 配置项 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | `show_milliseconds` | bool | `false` | **是否显示毫秒**。与 `countdown.show_milliseconds` 相互独立 | | `lap_enabled` | bool | `true` | 是否启用打点(Lap)功能;为 `false` 时「打点」按钮与记录区被禁用。**界面上不再提供该开关,只能改配置**(默认开启) | | `split_ratio` | float | `0.0` | 同 `countdown.split_ratio`,正计时页独立保存自己的分栏比例 | | `side_panel` | bool | `false` | 同 `countdown.side_panel`。**正计时默认收起右侧面板**,让计时区占满窗口,更贴合「专注计时」的用法;打分点记录时用右上角悬浮箭头展开 | ### 4.6 完整默认配置 ```json { "window": { "width": 900, "height": 640, "min_width": 800, "min_height": 600, "frameless": true, "always_on_top": false }, "theme": { "mode": "light", "accent_color": "#386338", "background_color": "#F1F3F8", "text_color": "#0F172A", "secondary_text_color": "#64748B", "border_color": "#E2E8F0" }, "clock": { "hour_format": 24, "smooth_second_hand": true, "analog": { "show_seconds": true, "show_date": false }, "digital": { "show_seconds": true, "blink_colon": false, "show_date": true } }, "countdown": { "presets": [ { "label": "5 分钟", "seconds": 300 }, { "label": "10 分钟", "seconds": 600 }, { "label": "20 分钟", "seconds": 1200 }, { "label": "30 分钟", "seconds": 1800 }, { "label": "45 分钟", "seconds": 2700 }, { "label": "1 小时", "seconds": 3600 } ], "show_milliseconds": false, "sound_enabled": true, "popup_on_finish": true, "split_ratio": 0.0, "side_panel": true }, "stopwatch": { "show_milliseconds": false, "lap_enabled": true, "split_ratio": 0.0, "side_panel": false } } ``` --- ## 五、项目结构 ``` Timer/ ├── main.py # 程序入口 ├── requirements.txt ├── README.md ├── config.json # 默认配置(首次运行自动生成) ├── timer.svg # 图标源文件 ├── app/ │ ├── __init__.py │ ├── config.py # 配置管理(深度合并 / 类型校验 / 回退 / 写回) │ ├── main_window.py # 主窗口 + 导航 + 无边框 / 置顶 / 边缘拉伸 + 全局样式表 │ ├── theme.py # 【辅助】由基础色推导整套 token 并生成全局样式表 │ ├── views/ │ │ ├── __init__.py │ │ ├── analog_clock.py # 模拟时钟 │ │ ├── digital_clock.py # 数字时钟 │ │ ├── countdown.py # 倒计时 │ │ ├── stopwatch.py # 正计时 │ │ └── timer_base.py # 【辅助】倒计时/正计时共用的计时基类 │ ├── widgets/ │ │ ├── __init__.py │ │ ├── title_bar.py # 自定义标题栏(自绘窗口按钮 + 拖动 + 吸附) │ │ ├── mark_table.py # 打点记录表格(清空 / 导出 CSV / TXT) │ │ ├── nav_bar.py # 凹槽轨道式导航栏(矢量图标 + 强调色 pill) │ │ ├── icons.py # 【辅助】QPainter 绘制的矢量图标工厂 │ │ ├── progress_ring.py # 【辅助】环形进度 + 大字号时间 + 状态 │ │ ├── chip.py # 【辅助】pill 形状快捷开关 │ │ ├── reflow.py # 【辅助】宽度不足时自动换行的按钮行 │ │ └── time_label.py # 【辅助】自适应字号等宽时间标签 │ └── utils/ │ ├── __init__.py │ ├── time_format.py # 时间格式化与时长解析(支持可选毫秒) │ ├── resource.py # 资源路径处理(兼容 PyInstaller) │ ├── win32.py # 【辅助】Windows 原生窗口样式(补回系统贴靠能力) │ ├── colors.py # 【辅助】颜色校验 / 解析 / 派生 + 内置浅色/深色配色 │ ├── fonts.py # 【辅助】等宽 / 界面字体挑选 │ └── sound.py # 【辅助】提示音播放(QSoundEffect + 系统蜂鸣兜底) ├── resources/ │ ├── fonts/ # 随包分发的 Ubuntu Mono(UFL 1.0)+ 许可证 │ ├── timer.svg # 图标源(矢量) │ ├── icon.ico # Windows 图标(由 timer.svg 生成) │ ├── icon.png # Linux 图标(由 timer.svg 生成) │ └── alert.wav # 倒计时提示音(由脚本合成) ├── build/ │ ├── build_win.bat # Windows 打包入口(转发给 build_app.py) │ ├── build_linux.sh # Linux 打包入口(转发给 build_app.py) │ ├── build_app.py # 打包主逻辑(跨平台共用,组装 PyInstaller 参数) │ ├── collect_ctypes_dlls.py # 收集 ctypes 的 ffi 运行库(Conda 布局必需) │ └── make_assets.py # 由 timer.svg 生成 icon.ico / icon.png / alert.wav └── tests/ ├── smoke_test.py # 冒烟自测(离屏运行,不需要显示器) └── gui_check.py # GUI 校验(离屏截图 + 交互校验) ``` > 标注【辅助】的模块是在需求给定的结构上增加的复用模块, > 目的是把「主题推导」「矢量图标」「计时状态机」「颜色计算」「自适应字号」「提示音」这些跨页面逻辑集中到一处,避免重复代码。 > `tests/` 下的两个脚本同样是额外补充的自测工具。 ### 自测 ```bash python tests/smoke_test.py # 配置 / 时间工具 / 计时逻辑 / 导出 python tests/gui_check.py # 界面构建、主题、绘制、交互 ``` 两个脚本都使用 Qt 的 `offscreen` 后端,在无显示器 / CI 环境下即可运行: 校验配置深度合并与颜色兜底、主题模式切换、时间格式化与解析、四个页面构建与切换、 倒计时完整走完一轮、打点与 CSV / TXT 导出等。`smoke_test.py` 运行结束会输出 `全部检查通过`, `gui_check.py` 会输出 `全部校验通过` 并把截图写到临时目录。 --- ## 六、打包 打包前请先确保依赖已安装(`pip install -r requirements.txt`),并且 `resources/` 目录存在。 ### 6.1 Windows ```bat build\build_win.bat ``` 脚本内部依次执行: ```bat python build\make_assets.py pyinstaller --noconfirm --onefile --windowed --name Timer --icon resources/icon.ico --add-data "resources;resources" main.py ``` 产物:`dist\Timer.exe`(单文件、无控制台窗口)。 > 若 `python` 未指向 Python 3.8,可先执行 `set PYTHON=C:\Python38\python.exe` 再运行脚本。 ### 6.2 Linux ```bash bash build/build_linux.sh ``` 脚本内部依次执行: ```bash python3 build/make_assets.py pyinstaller --noconfirm --onefile --windowed --name Timer --icon resources/icon.png --add-data "resources:resources" main.py ``` 产物:`dist/Timer`(单文件可执行程序)。 > 注意:Linux 下 `--add-data` 的分隔符是 `:`,Windows 下是 `;`。 > 可通过 `PYTHON=python3.8 bash build/build_linux.sh` 指定解释器。 ### 6.3 资源路径处理 `app/utils/resource.py` 统一处理源码运行与打包运行的差异: * 打包后只读资源位于 `sys._MEIPASS`; * `executable_dir()` 返回「程序同级目录」,配置文件写在这里(不可写则回退 `~/.timer/`); * 因此打包后的程序可以放在任意目录直接运行,`config.json` 会生成在可执行文件旁边。 > 仓库中已经附带了一份由上述命令构建、并实际启动验证过的 Windows 单文件程序 > `dist/Timer.exe`(约 50 MB,Qt 5.15 运行库已全部内嵌)。 > 它会在自己的同级目录生成 `config.json`;删除该目录下的 `config.json` 即可恢复默认设置(浅色主题)。 > 该文件是可选的构建产物,不需要时可直接删除,重新执行打包脚本即可再生成。 **手动打包命令**(与脚本中等价): ```bash # Windows pyinstaller --noconfirm --onefile --windowed --name Timer --icon resources/icon.ico --add-data "resources;resources" main.py # Linux pyinstaller --noconfirm --onefile --windowed --name Timer --icon resources/icon.png --add-data "resources:resources" main.py ``` --- ## 七、常见问题(FAQ) ### 1. `pip install` 装到了不兼容的 Qt 绑定版本 PySide6 6.7 起要求 Python 3.9+。请显式约束版本: ```bash python -m pip install "PySide2==5.15.2.1" ``` 推荐直接使用 `requirements.txt`,其中已写入该约束。 ### 2. PyInstaller 打包时报错 / 打包后无法启动 * **打包后启动即报 `ImportError: DLL load failed while importing _ctypes`**: `app/utils/win32.py` 用 `ctypes` 调整窗口样式,而 `_ctypes` 依赖的 `ffi-*.dll` 在 **Conda 环境**里位于 `\Library\bin`,PyInstaller 不会自动去那里找。 打包脚本已经通过 `build/collect_ctypes_dlls.py` 自动定位并 `--add-binary` 带上它, 用官方 CPython 时该脚本找不到也不会报错(PyInstaller 自己能处理 `DLLs` 目录)。 如果你改动过打包流程,请确认这条依赖没有丢。 * PyInstaller 7.0 起不再支持 Python 3.8,请安装 `PyInstaller>=5.13,<7.0`; * 打包后若提示缺少 Qt 插件,通常是 PySide2 安装不完整,重新执行 `python -m pip install --force-reinstall "PySide2==5.15.2.1"`; * 排查启动异常时可临时去掉 `--windowed`,让控制台输出错误堆栈。 ### 3. Linux 下运行提示 `qt.qpa.plugin: Could not load the Qt platform plugin "xcb"` 这是缺少 xcb 相关系统库导致的,安装对应的运行库即可: ```bash # Debian / Ubuntu sudo apt-get update sudo apt-get install -y libxcb-cursor0 libxcb-xinerama0 libxcb-icccm4 libxcb-image0 \ libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxcb-xkb1 \ libxkbcommon-x11-0 libgl1 libegl1 libfontconfig1 libdbus-1-3 # Fedora / RHEL sudo dnf install -y xcb-util-cursor xcb-util-wm xcb-util-image xcb-util-keysyms \ xcb-util-renderutil libxkbcommon-x11 mesa-libGL fontconfig # Arch sudo pacman -S --needed xcb-util-cursor xcb-util-wm xcb-util-image xcb-util-keysyms \ xcb-util-renderutil libxkbcommon-x11 mesa ``` 如果只是在无显示环境(SSH / 容器)里验证启动,可以先用离屏后端: ```bash QT_QPA_PLATFORM=offscreen python main.py ``` Wayland 桌面下如遇异常,可临时切回 xcb:`QT_QPA_PLATFORM=xcb python main.py`。 ### 4. Linux 下中文显示为方块 安装中文字体,例如: ```bash sudo apt-get install -y fonts-noto-cjk # Debian / Ubuntu sudo dnf install -y google-noto-sans-cjk-fonts ``` 程序已按 `Microsoft YaHei UI → PingFang SC → Noto Sans CJK SC → WenQuanYi Micro Hei` 的顺序自动挑选字体。 ### 5. 无边框模式下无法拖动 / 无法拉伸 * 拖动:需要按住**标题栏的空白区域**(图标、标题文字或空白处),按在按钮上不会拖动; * 拉伸:把鼠标移到窗口最外侧 10px 内(容器已铺满窗口,热区从窗口边缘往里算)。 拉伸光标在整个窗口范围内都会反馈(含自绘标题栏之上),所以边缘很容易找到; 光标会变成双向箭头,此时按住拖动即可; * 最大化状态下不支持拉伸(与系统窗口行为一致),还原后即可。 ### 5.1 无边框模式下拖动 / 拉伸不够顺滑 **现象**:拖动窗口时窗口跟不上光标,一顿一顿的。 **原因**:原先的实现是「在 ``mouseMoveEvent`` 里跟着鼠标 ``move()`` 窗口」。 每收到一次鼠标移动,Qt 都要重绘整个窗口,而 ``QGraphicsDropShadowEffect`` 还会把整窗内容渲染到离屏位图再做一次模糊 —— 两者叠加就是卡顿的来源。 **现在的做法**(`app/widgets/title_bar.py` / `app/main_window.py`): * 拖动优先调用 ``QWindow.startSystemMove()``,把拖动**交给操作系统**: 系统直接搬运已经渲染好的窗口,完全不经过 Qt 的重绘路径,因此与光标 1:1 跟手, 还自带系统级的边缘吸附与「拖拽最大化窗口自动还原」; * 边缘 / 四角拉伸同理改用 ``QWindow.startSystemResize()``; * 只有在系统不接受请求的平台才回退到自己的 ``move()`` / ``setGeometry()``, 并且拖动期间不再有多余的整窗重绘开销; * 在 Wayland 下原生接口是**唯一**可行方案(客户端无权自行移动 / 缩放窗口), 所以这一改动同时修好了 Wayland 下的拖动与拉伸。 ### 5.2 无边框模式下拉伸窗口没有反应 **原因**:Windows 上 ``WA_TranslucentBackground`` 会生成分层窗口,而 **完全透明(alpha = 0)的像素是鼠标穿透的**。历史上边框那圈阴影留白是全透明, 鼠标事件根本到不了窗口,边缘拉伸自然永远触发不了。 **修复**:无边框模式下给这圈留白铺一层 ``alpha = 1`` 的黑底 (`MainWindow.paintEvent`)。1/255 的透明度肉眼完全看不出来, 但这圈像素不再是「全透明」,鼠标事件就能正常落到窗口上。 > 历史上拉伸热区在窗口最外侧一圈(给阴影预留的透明边距), > 现在同时充当原生的拉伸热区。 ### 5.3 无边框模式下出现「边框重影 / 双标题栏」 现象:窗口顶部同时出现**系统标题栏**(带最小化 / 最大化 / 关闭)和**自绘标题栏**, 或窗口四周多出一圈细边框。 原因:**本方案已不使用圆角与阴影**,窗口是不透明的矩形(见 5.4)。以下是历史原因记录: 无边框圆角 + 阴影依赖 `WA_TranslucentBackground`(Windows 上的分层窗口), 而 Windows 只在**创建**窗口时才会应用该属性。如果只对已经存在的窗口调用 `setWindowFlags()`,原生标题栏与边框有时会残留。 程序的处理方式(`app/main_window.py`): 1. 切换「无边框 / 置顶」时先 `hide()`,再 `setWindowFlags()` + `setAttribute()`; 2. 调用 `QWindow.destroy()` **销毁原生窗口**,让随后的 `show()` 重新创建一个干净窗口; 3. 恢复切换前的窗口尺寸、最大化状态与键盘焦点; 4. 延迟一帧强制整窗重绘,抹掉驱动层可能残留的旧帧像素。 如果仍然遇到残影,通常是桌面窗口管理器(DWM)的合成缓存问题, 最小化再还原窗口,或重启程序即可,不影响功能。 ### 5.4 窗口没有阴影 / 想去掉灰边框 **当前版本不绘制阴影**:无边框窗口是「方角 + 灰边框」的不透明窗口, 窗口边界由 `RootContainer` 的 1px 灰边(`theme.py` 的 `window_border` 令牌) 界定,没有阴影,也没有透明边距。因此: * `window.shadow` / `window.shadow_strength` 两个配置项**已移除**, 旧配置文件里留着它们会被自动忽略; * 边缘拉伸热区固定为窗口最外侧 10px(常量 `EDGE_HOT_SIZE`)。 ### 5.5 深色主题下计时页整片发白 / 数字看不见 **原因**:计时页的内容放在 `QScrollArea` 里,而 `QScrollArea.setWidget()` 会把 内容控件自动设成 `autoFillBackground(True)`。这样它会用**系统调色板**(浅灰) 铺满整页,把主题底色盖掉 —— 表现就是「顶栏是深色的、页面却是白的」。 **修复**:`setWidget()` 之后显式 `page.setAutoFillBackground(False)` (见两个计时视图里的 `_make_scroll_area()`)。`tests/smoke_test.py` 里加了 一条回归用例,会在两种主题下取页面空白处的**实际像素**与 `theme.background_color` 做比对,防止再次回归。 ### 6. 切换「置顶 / 无边框」后窗口位置变化 程序在切换 `WindowStaysOnTopHint` / `FramelessWindowHint` 时会先保存窗口尺寸与最大化状态, `show()` 之后立即恢复,因此窗口大小不会跳动。从有边框切到无边框时,系统标题栏消失, 程序会保持**客户区左上角**不动(也就是内容不跳动),这是刻意选择的行为。 若在 Wayland 下仍出现位移,属于合成器不允许客户端自行 `move()` 的限制, 可切换到 X11(`QT_QPA_PLATFORM=xcb`)获得完整行为。 ### 7. 倒计时结束没有声音 * 确认 `countdown.sound_enabled` 为 `true`; * 确认 `resources/alert.wav` 存在(缺失时可运行 `python build/make_assets.py` 重新生成); * 若系统缺少 QtMultimedia 后端,程序会自动退化为系统提示音(`QApplication.beep()`)。 ### 8. 配置文件损坏了怎么办 直接用记事本打开 `config.json`,删掉内容或整个文件后重新启动程序,程序会自动生成一份完整的默认配置。 即使 JSON 语法错误,程序也不会崩溃,而是回退默认值并把修复后的配置写回磁盘。 ### 8.1 怎么切换浅色 / 深色主题 三种方式,任选其一: * 点击窗口右上角的 **「深色」开关**(勾选即深色,取消即浅色),**即时生效**并写回配置文件; * 编辑 `config.json`,把 `theme.mode` 改成 `"light"` 或 `"dark"`,重启程序; * 在 `config.json` 里直接指定 `theme.accent_color` / `background_color` 等颜色项来自定义配色 (自定义过的颜色在切换 `theme.mode` 时会被保留,不会被覆盖)。 > 如果切换后颜色没变化,先确认该颜色项是否被手工改成了「非预设值」—— > 这类颜色会被视为用户自定义而保留。 ### 8.2 浅色主题下界面看着太素 / 对比度不够 浅色主题的默认强调色是 `#386338`(深色主题用同色系提亮的 `#8FBC8F`), 这是为了保证按钮文字、数字时钟大字号在浅底上的对比度。想更活泼可以换成 `"accent_color": "#FF9800"` 之类的颜色,深浅两套主题可以各配一次。 ### 9. 窗口最小化后计时还准吗 准确。倒计时与正计时都基于「已累计秒数 + 本次启动时的 `time.monotonic()` 时间戳」计算, 不依赖定时器累加,因此即使窗口最小化导致界面刷新被系统降频,计时结果依然精确。 --- ## 八、界面设计说明 这一版界面按「现代桌面应用」的通用做法重新梳理过,几条主要规则: **1. 层次靠抬升,不靠描边。** 浅色主题下背景是浅冷灰(`#F1F3F8`)、卡片是近白(`#FDFDFE`);深色主题下背景近黑(`#14161B`)、卡片稍亮。 两者都由 `colors.elevate()` 朝白色混合得到,只是强度不同 —— 所以同一套样式表在两种主题下都能形成正确的层次。 **2. 强调色只用在真正需要引导视线的地方。** 选中的导航项、主按钮、环形进度弧、时钟指针、输入框聚焦边框。其余一律中性色,避免界面花哨。 **3. 计时用环形进度而不是细长进度条。** `ProgressRing` 把「时间数字」放在圆环正中,进度沿圆周扫过。 比例是连续可扫视的,余光就能判断还剩多少;最后 10 秒进度弧转为警示红。 正计时则用同一根圆环按 60 秒一圈扫动,相当于一根被放大的秒针。 **4. 计时页改成左右分栏。** 左栏是环形进度(视觉主体,窗口放大时同步变大),右栏是预设 / 输入 / 控制 / 打点记录。 这样 900×640 这类常见窗口下横向空间被充分利用,而不是把一堆控件竖着堆成一长条。 **5. 界面与计时统一使用 Ubuntu Mono,并随包分发。** `resources/fonts/` 内置了 `UbuntuMono-Regular.ttf` / `UbuntuMono-Bold.ttf` (Canonical 发布,Ubuntu Font Licence 1.0,许可证文本一并附在 `resources/fonts/UFL.txt`)。启动时用 `QFontDatabase.addApplicationFont()` 注册,**因此目标机器上没装这个字体也能正常显示**,不需要用户做任何事。 Ubuntu Mono 只覆盖拉丁字符,中文由 Qt 按 `QFont.setFamilies()` 给出的候选列表 逐字符回退到系统中文字体(`Microsoft YaHei UI` / `PingFang SC` / `Noto Sans CJK SC` …), 所以中英文混排不会出现方块。同时注册了 Bold 字重的独立文件, `setBold(True)` 拿到的是真正的粗体字形而不是 Qt 的描边加粗。 **6. 顶栏合并成一行。** 品牌(无边框模式下)、四个功能切换、以及主题 / 置顶 / 无边框三个开关 全部放在同一行:`[图标 Timer] [导航] [开关] [窗口按钮]`,省掉一整行高度。 品牌在有边框模式下自动隐藏(系统标题栏已经显示过了),窗口标题也去掉了 Qt 自动追加的应用名(`Timer·计时工具 - Timer` → `Timer·计时工具`)。 整行空白处都可以拖动窗口 —— 判定规则是「鼠标下方控件及其所有祖先都不是按钮」。 **7. 图标全部矢量自绘。** `app/widgets/icons.py` 用 `QPainter` 在 24×24 逻辑方格内绘制,按 4 倍超采样输出。 理由和时钟表盘一致:换主题颜色、换 DPI、任意缩放都不会模糊,也不依赖图标字体。 **8. 设置项前置到界面。** 原本只能改配置文件才能切换的选项(显示秒、显示毫秒、12/24 小时制、提示音……), 现在都以底部 pill 开关的形式直接可点,点完即生效并写回 `config.json`。 **9. 打点备注与「归属感」。** 双击列表里的任意一条打点即可为它写一句文字备注。备注不是简单另起一行就完事 —— 那样看不出它属于哪条记录 —— 而是做了三件事让归属一目了然: * **按记录分组着色**:斑马纹按「记录」而不是「表格行」交替, 一条记录和它下面的备注行共用同一个底色,天然成为一个视觉块; * **左侧留空 + 对齐相对时间**:序号列整列留空,``└``(矢量绘制的连接符) 连同备注正文一起从「相对时间」列开始排版,既表明从属关系,又让整张表左边界整齐; * **同组高亮**:最新一条记录的强调色底会同时覆盖它的备注行。 相关细节:备注随 CSV / TXT 导出(多出「备注」列)、右键菜单可以编辑或清除备注、 删除记录时备注一起消失。 **10. 收起侧栏后仍然可完整操作。** 计时区右上角的悬浮控制条始终贴住窗口最右端:面板展开时只有一个「收起」箭头; 面板收起后,右侧的按钮全都看不见了,于是这里补齐 **开始 / 停止(同一个按钮按状态 自动切换为继续)/ 打点 / 重置** 以及「展开」箭头,核心功能不会因为收起面板而丢失。 **11. 无边框拖动 / 拉伸与「功能区不跳字」。** Windows 下补过系统贴靠样式后(见第 14 条),系统不再接受 `WM_NCLBUTTONDOWN + HTCAPTION` 的移动请求,`startSystemMove()` / `startSystemResize()` 都失效,因此拖动与拉伸都改由 `TitleBar` / `MainWindow` 自己跟随鼠标(`MainWindow.uses_manual_drag()` 决定走哪条路)。六个方向的 拉伸几何都做了精确性测试,拖动则实测 1:1 跟手。 另外,**拖动期间不再开关 `QGraphicsDropShadowEffect`**:切换效果会改变根容器 的绘制范围,表现为窗口一动功能区文字就跳一下。阴影很轻,保持常开对跟手程度 的影响可以忽略。 **11.1 旧方案(供参考)。** ``startSystemMove()`` / ``startSystemResize()`` 让操作系统直接搬运已渲染好的窗口, 绕开 Qt 的整窗重绘(含阴影模糊),这是拖动跟手的关键;同时自带系统级吸附, 并且在 Wayland 下是唯一可行的方案。 **12. 小窗口友好。** 计时页内容放在 `QScrollArea` 里:640×480 这种最小尺寸下会自动出现细滚动条, 而不是把控件挤到裁切;正常尺寸下滚动条不出现。 **13. 提示文案只讲用户能做的事。** 所有 tooltip 只回答「点下去会发生什么」,例如「窗口置顶:让 Timer 始终显示在最前面」 「显示秒针;关闭后表盘只保留 12 个整点刻度」。**不写「状态会写入 config.json」** 这类只有开发者才关心、对用户毫无意义的说明。 **14. 无边框窗口也要有系统级贴靠。** `Qt.FramelessWindowHint` 会把窗口样式里的 `WS_THICKFRAME`、`WS_MAXIMIZEBOX`、 `WS_MINIMIZEBOX`、`WS_SYSMENU` 一并抹掉,Windows 于是不再把它当成「可贴靠窗口」, **Win + 方向键完全失效**。`app/utils/win32.py` 在窗口显示后把这四项样式补回去 (刻意**不加** `WS_CAPTION`,所以系统标题栏依旧不会出现),Win + 方向键随即恢复。 补上样式后系统不再接受 `HTCAPTION` 移动请求,拖动改由 `TitleBar` 自己跟随鼠标 (`MainWindow.uses_manual_drag()` 决定走哪条路),因此**拖到屏幕边缘的分屏** 也由我们实现:上边缘最大化、左右边缘各占半屏(`TitleBar._apply_edge_snap`)。 Linux / Wayland 不受影响,仍走 `startSystemMove()`。 > 实测四种样式组合后确定的方案:必须有 `WS_THICKFRAME`(少了它贴靠无效), > 不能有 `WS_CAPTION`(加了不会出现标题栏、对贴靠也没帮助,还要额外拦截 > `WM_NCCALCSIZE`,白白多一层平台代码)。 **15. 反馈就位、不打断。** 自定义时长解析失败时用「输入框红边 + 圆环下方文案」就地提示,不再弹模态对话框; 倒计时结束的提示框保持模态(这是需求要求的重要提醒),但警告类提示改为非阻塞 `show()`。 --- ## 九、开发说明 * 全部界面代码使用 `QVBoxLayout` / `QHBoxLayout` / `QGridLayout` 组合布局,无任何绝对坐标; * 模拟时钟、数字时钟、图标、环形进度全部由 `QPainter` 矢量绘制,不使用任何位图贴图,任意缩放不失真; * 计时逻辑与界面刷新解耦:`QTimer` 只负责按 33ms 触发重绘,时间值始终由时间戳计算; * 主题只有一个数据源:`colors.py` 里的两套内置配色 → `config.py` 的默认值 → `theme.py` 的 token 推导 → 样式表, 避免同一套颜色在多处各写一份而漂移; * 代码严格兼容 Python 3.8:统一使用 `typing.List` / `typing.Dict` / `typing.Optional`, 未使用 `list[str]`、`str | None`、`match/case` 等新语法。 ## 十、许可证 MIT License,可自由使用与修改。