Skip to content

Commit 8afee82

Browse files
committed
feat: Windows UI 优化,禁止跳过
1 parent d3995e7 commit 8afee82

2 files changed

Lines changed: 83 additions & 83 deletions

File tree

src/windows/ui/countdown.rs

Lines changed: 78 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,43 @@ use std::time::{Duration, Instant};
55
use windows::Win32::Foundation::{COLORREF, HWND, LPARAM, LRESULT, RECT, WPARAM};
66
use windows::Win32::Graphics::Gdi::{
77
BeginPaint, CreateFontW, DeleteObject, EndPaint, FillRect, GetStockObject, HBRUSH,
8-
InvalidateRect, PAINTSTRUCT, SelectObject, SetBkMode, SetTextColor, TRANSPARENT, TextOutW,
9-
WHITE_BRUSH,
8+
InvalidateRect, PAINTSTRUCT, SelectObject, SetBkMode, SetTextColor, TRANSPARENT, WHITE_BRUSH,
109
};
1110
use windows::Win32::UI::WindowsAndMessaging::{
12-
CreateWindowExW, DefWindowProcW, DestroyWindow, GetClientRect, GetSystemMetrics, HWND_TOPMOST,
13-
KillTimer, RegisterClassW, SM_CXSCREEN, SM_CYSCREEN, SW_HIDE, SW_SHOW, SWP_NOMOVE, SWP_NOSIZE,
14-
SetTimer, SetWindowPos, ShowWindow, WM_CLOSE, WM_PAINT, WM_TIMER, WNDCLASSW, WS_EX_TOOLWINDOW,
15-
WS_EX_TOPMOST, WS_POPUP, WS_VISIBLE,
11+
CreateWindowExW, DT_CENTER, DT_SINGLELINE, DT_VCENTER, DefWindowProcW, DestroyWindow,
12+
DrawTextW, GetClientRect, GetSystemMetrics, HWND_TOPMOST, KillTimer, RegisterClassW,
13+
SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN, SW_HIDE, SW_SHOW,
14+
SWP_NOMOVE, SWP_NOSIZE, SetTimer, SetWindowPos, ShowWindow, WM_CLOSE, WM_KEYDOWN,
15+
WM_LBUTTONDOWN, WM_PAINT, WM_TIMER, WNDCLASSW, WS_EX_TOOLWINDOW, WS_EX_TOPMOST, WS_POPUP,
16+
WS_VISIBLE,
1617
};
1718
use windows::core::PCWSTR;
1819

1920
use super::super::constants::{APP_NAME_ZH, COUNTDOWN_TIMER_ID, COUNTDOWN_WINDOW_CLASS};
2021
use super::super::state::with_state;
2122
use super::super::utils::{SoundType, format_countdown, play_sound, to_wide_string};
2223

24+
fn clamp_i32(v: i32, min: i32, max: i32) -> i32 {
25+
if v < min {
26+
min
27+
} else if v > max {
28+
max
29+
} else {
30+
v
31+
}
32+
}
33+
34+
fn line_rect(full: &RECT, center_y: i32, line_height: i32) -> RECT {
35+
let top = center_y - (line_height / 2);
36+
let bottom = center_y + (line_height / 2);
37+
RECT {
38+
left: full.left,
39+
top,
40+
right: full.right,
41+
bottom,
42+
}
43+
}
44+
2345
/// 倒计时窗口过程
2446
#[allow(unsafe_op_in_unsafe_fn, clippy::too_many_lines)]
2547
pub unsafe extern "system" fn countdown_wndproc(
@@ -60,11 +82,23 @@ pub unsafe extern "system" fn countdown_wndproc(
6082
)
6183
});
6284

63-
// 绘制标题
85+
let width = rect.right - rect.left;
86+
let height = rect.bottom - rect.top;
87+
let base = width.min(height).max(1);
88+
89+
let title_px = clamp_i32(base / 18, 28, 56);
90+
let countdown_px = clamp_i32(base / 9, 64, 144);
91+
let hint_px = clamp_i32(base / 28, 18, 36);
92+
93+
let title_center_y = rect.top + height * 32 / 100;
94+
let countdown_center_y = rect.top + height * 50 / 100;
95+
let hint_center_y = rect.top + height * 68 / 100;
96+
97+
// 绘制标题(自动居中,避免 DPI/字体导致的错位)
6498
let title = format!("{APP_NAME_ZH} · 休息倒计时");
6599
let title_wide = to_wide_string(&title);
66100
let title_font = CreateFontW(
67-
48,
101+
-title_px,
68102
0,
69103
0,
70104
0,
@@ -77,18 +111,22 @@ pub unsafe extern "system" fn countdown_wndproc(
77111
0,
78112
0,
79113
0,
80-
windows::core::w!("Microsoft YaHei"),
114+
windows::core::w!("Segoe UI"),
81115
);
82116
let old_font = SelectObject(hdc, title_font);
83-
let title_y = rect.bottom / 2 - 100;
84-
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
85-
let title_x = (rect.right - (title.chars().count() as i32 * 24)) / 2;
86-
let _ = TextOutW(hdc, title_x, title_y, &title_wide[..title_wide.len() - 1]);
117+
let mut title_rect = line_rect(&rect, title_center_y, title_px * 2);
118+
let _ = DrawTextW(
119+
hdc,
120+
PCWSTR(title_wide.as_ptr()),
121+
-1,
122+
&raw mut title_rect,
123+
DT_CENTER | DT_VCENTER | DT_SINGLELINE,
124+
);
87125

88126
// 绘制倒计时数字
89127
let countdown_wide = to_wide_string(&countdown_text);
90128
let countdown_font = CreateFontW(
91-
96,
129+
-countdown_px,
92130
0,
93131
0,
94132
0,
@@ -101,24 +139,23 @@ pub unsafe extern "system" fn countdown_wndproc(
101139
0,
102140
0,
103141
0,
104-
windows::core::w!("Microsoft YaHei"),
142+
windows::core::w!("Segoe UI"),
105143
);
106144
let _ = SelectObject(hdc, countdown_font);
107-
let countdown_y = rect.bottom / 2 - 30;
108-
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
109-
let countdown_x = (rect.right - (countdown_text.chars().count() as i32 * 48)) / 2;
110-
let _ = TextOutW(
145+
let mut countdown_rect = line_rect(&rect, countdown_center_y, countdown_px * 2);
146+
let _ = DrawTextW(
111147
hdc,
112-
countdown_x,
113-
countdown_y,
114-
&countdown_wide[..countdown_wide.len() - 1],
148+
PCWSTR(countdown_wide.as_ptr()),
149+
-1,
150+
&raw mut countdown_rect,
151+
DT_CENTER | DT_VCENTER | DT_SINGLELINE,
115152
);
116153

117154
// 绘制提示文本
118155
let hint = "放松眼睛,伸展身体";
119156
let hint_wide = to_wide_string(hint);
120157
let hint_font = CreateFontW(
121-
32,
158+
-hint_px,
122159
0,
123160
0,
124161
0,
@@ -131,46 +168,24 @@ pub unsafe extern "system" fn countdown_wndproc(
131168
0,
132169
0,
133170
0,
134-
windows::core::w!("Microsoft YaHei"),
171+
windows::core::w!("Segoe UI"),
135172
);
136173
let _ = SelectObject(hdc, hint_font);
137174
let _ = SetTextColor(hdc, COLORREF(0x0080_8080)); // 灰色
138-
let hint_y = rect.bottom / 2 + 80;
139-
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
140-
let hint_x = (rect.right - (hint.chars().count() as i32 * 16)) / 2;
141-
let _ = TextOutW(hdc, hint_x, hint_y, &hint_wide[..hint_wide.len() - 1]);
142-
143-
// 绘制跳过按钮提示
144-
let skip_hint = "按 ESC 或点击任意位置跳过";
145-
let skip_wide = to_wide_string(skip_hint);
146-
let skip_font = CreateFontW(
147-
24,
148-
0,
149-
0,
150-
0,
151-
400,
152-
0,
153-
0,
154-
0,
155-
0,
156-
0,
157-
0,
158-
0,
159-
0,
160-
windows::core::w!("Microsoft YaHei"),
175+
let mut hint_rect = line_rect(&rect, hint_center_y, hint_px * 2);
176+
let _ = DrawTextW(
177+
hdc,
178+
PCWSTR(hint_wide.as_ptr()),
179+
-1,
180+
&raw mut hint_rect,
181+
DT_CENTER | DT_VCENTER | DT_SINGLELINE,
161182
);
162-
let _ = SelectObject(hdc, skip_font);
163-
let skip_y = rect.bottom / 2 + 140;
164-
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
165-
let skip_x = (rect.right - (skip_hint.chars().count() as i32 * 12)) / 2;
166-
let _ = TextOutW(hdc, skip_x, skip_y, &skip_wide[..skip_wide.len() - 1]);
167183

168184
// 清理
169185
let _ = SelectObject(hdc, old_font);
170186
let _ = DeleteObject(title_font);
171187
let _ = DeleteObject(countdown_font);
172188
let _ = DeleteObject(hint_font);
173-
let _ = DeleteObject(skip_font);
174189

175190
let _ = EndPaint(hwnd, &raw const ps);
176191
LRESULT(0)
@@ -187,19 +202,10 @@ pub unsafe extern "system" fn countdown_wndproc(
187202
}
188203
LRESULT(0)
189204
}
190-
// 处理鼠标点击和键盘事件 - 跳过休息
191-
0x0201 | 0x0100 => {
192-
// WM_LBUTTONDOWN | WM_KEYDOWN
193-
// ESC 键或鼠标点击跳过
194-
if msg == 0x0100 && wparam.0 != 0x1B {
195-
// 不是 ESC 键
196-
return DefWindowProcW(hwnd, msg, wparam, lparam);
197-
}
198-
close_countdown_window();
199-
LRESULT(0)
200-
}
205+
// 禁止跳过休息:吞掉鼠标点击 / 键盘事件
206+
WM_LBUTTONDOWN | WM_KEYDOWN => LRESULT(0),
201207
WM_CLOSE => {
202-
close_countdown_window();
208+
// 禁止用户关闭窗口;程序结束/计时结束时会主动 DestroyWindow
203209
LRESULT(0)
204210
}
205211
_ => DefWindowProcW(hwnd, msg, wparam, lparam),
@@ -230,9 +236,11 @@ pub fn show_countdown_window(seconds: u64, play_start_sound: bool) {
230236
play_sound(SoundType::BreakStart);
231237
}
232238

233-
// 获取屏幕尺寸
234-
let screen_width = unsafe { GetSystemMetrics(SM_CXSCREEN) };
235-
let screen_height = unsafe { GetSystemMetrics(SM_CYSCREEN) };
239+
// 覆盖所有显示器(虚拟屏幕),避免多屏幕下窗口尺寸/位置错乱
240+
let screen_x = unsafe { GetSystemMetrics(SM_XVIRTUALSCREEN) };
241+
let screen_y = unsafe { GetSystemMetrics(SM_YVIRTUALSCREEN) };
242+
let screen_width = unsafe { GetSystemMetrics(SM_CXVIRTUALSCREEN) };
243+
let screen_height = unsafe { GetSystemMetrics(SM_CYVIRTUALSCREEN) };
236244

237245
let class_name = to_wide_string(COUNTDOWN_WINDOW_CLASS);
238246

@@ -243,8 +251,8 @@ pub fn show_countdown_window(seconds: u64, play_start_sound: bool) {
243251
PCWSTR(class_name.as_ptr()),
244252
PCWSTR::null(),
245253
WS_POPUP | WS_VISIBLE,
246-
0,
247-
0,
254+
screen_x,
255+
screen_y,
248256
screen_width,
249257
screen_height,
250258
None,

src/windows/ui/tray.rs

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use windows::Win32::UI::Shell::{
88
Shell_NotifyIconW,
99
};
1010
use windows::Win32::UI::WindowsAndMessaging::{
11-
AppendMenuW, CreatePopupMenu, DestroyMenu, GetCursorPos, IMAGE_ICON, LR_SHARED, LoadImageW,
11+
AppendMenuW, CreatePopupMenu, DestroyMenu, GetCursorPos, IDI_APPLICATION, LoadIconW,
1212
MF_DISABLED, MF_GRAYED, MF_SEPARATOR, MF_STRING, SetForegroundWindow, TPM_BOTTOMALIGN,
1313
TPM_LEFTALIGN, TrackPopupMenu,
1414
};
@@ -139,20 +139,12 @@ pub fn setup_tray_icon(hwnd: HWND) {
139139

140140
/// 加载托盘图标
141141
fn load_tray_icon() -> windows::Win32::UI::WindowsAndMessaging::HICON {
142-
// 尝试加载自定义图标,失败则使用系统默认图标
143-
// 这里使用系统默认图标作为后备
142+
// 使用稳定的系统默认图标,避免因资源加载失败导致托盘图标缺失。
143+
// 需要更换为应用图标时,建议通过资源文件(.rc)或在安装包中提供 .ico。
144144
unsafe {
145-
let icon = LoadImageW(
146-
None,
147-
windows::core::w!("shell32.dll,13"), // 使用系统图标
148-
IMAGE_ICON,
149-
16,
150-
16,
151-
LR_SHARED,
152-
);
153-
icon.map_or_else(
145+
LoadIconW(None, IDI_APPLICATION).map_or_else(
154146
|_| windows::Win32::UI::WindowsAndMessaging::HICON::default(),
155-
|h| windows::Win32::UI::WindowsAndMessaging::HICON(h.0),
147+
|h| h,
156148
)
157149
}
158150
}

0 commit comments

Comments
 (0)