窗口控制与画面比例/尺寸切换
本页覆盖 SpinningMomo 预览窗口(SpinningMomoPreviewWindowClass)的窗口控制与画面比例/尺寸切换机制:窗口的创建、外观(分层/置顶/圆角)、基于捕获画面纵横比的尺寸计算、ideal_size 边界约束、拖拽时的比例保持,以及跨线程的捕获尺寸切换事务(resize_pending / pending_extent)。
Purpose and Scope
本页覆盖的内容(capture.window-control 主题边界):
- 预览窗口的窗口类注册、窗口过程(
preview_window_proc)与 GWLP_USERDATA 状态挂载模式 - 窗口样式与 DWM 外观设置(分层透明、置顶、无边框
WS_POPUP、Windows 11 小圆角) WindowSizeState尺寸状态模型:aspect_ratio、ideal_size、min_ideal_size/max_ideal_sizeset_preview_window_size()的比例驱动尺寸计算算法(竖屏/横屏两分支)- 首次显示时的屏幕矩形定位与边界初始化
- 交互(拖拽缩放)过程中的比例保持逻辑
- 捕获尺寸切换的事务化并发模型:
WM_APPLY_CAPTURE_SIZE消息、resize_pending、pending_extent_mutex - DPI 感知:
DpiDependentSizes与update_preview_window_dpi() - 用户自定义比例/分辨率的配置入口(
[AspectRatioItems]/[ResolutionItems]节)
留给兄弟页面的内容:
- 捕获管线本身(
utils/graphics/capture的CaptureSession、编码器)属于捕获采集主题 - D3D11 渲染与着色器、视口框绘制属于预览渲染主题(
features/preview/rendering.cpp) - 设置界面、配置文件解析与持久化属于设置/配置主题;本页只涉及配置节中与比例/分辨率相关的入口语义
- 悬浮窗、托盘图标、通知窗口等其他 UI 窗口由各自页面覆盖
Overview
SpinningMomo 是一个将游戏画面捕获并以小型悬浮预览窗口展示的工具。预览窗口不是普通的可缩放 Win32 窗口,而是一个 WS_POPUP 无边框分层窗口(WS_EX_TOOLWINDOW | WS_EX_TOPMOST | WS_EX_LAYERED),其尺寸完全由捕获画面的纵横比与用户选定的 ideal_size(理想边长)共同推导,而非由用户自由拖拽决定。这带来三个核心设计目标:
- 画面不变形:无论捕获源是竖屏(
aspect_ratio >= 1.0f,即 高/宽 ≥ 1)还是横屏,窗口宽高始终严格按capture_height / capture_width的比例推导,预览与源画面保持几何一致。 - 尺寸有界:
ideal_size被夹在[min_ideal_size, max_ideal_size]区间内,该区间在首次显示时依据工作显示器的屏幕矩形动态推导(min = 短边/10,max = 长边),避免窗口小到不可用或大到铺满整屏。 - 切换原子化:当用户在比例/分辨率列表间切换时,捕获线程与窗口线程之间通过"事务权"(
resize_pending)+ 互斥锁(pending_extent_mutex)协调,保证最新的捕获尺寸不丢失、渲染资源在尺寸调整期间不被并发使用(resources_busy)。
用户通过设置中的自定义比例([AspectRatioItems])与自定义分辨率([ResolutionItems])节扩展可选的切换项,保存并重启后生效(见本地化文案 message.settings_config_help)。
Architecture
窗口控制能力集中在 features::preview 模块内,由以下协作单元构成:
分层解读:
window.cpp是窗口生命周期的唯一入口:注册窗口类 → 创建WS_POPUP分层窗口 → 应用 DWM 外观 → 由set_preview_window_size()承担所有"比例/尺寸切换"的几何计算与SetWindowPos()落地。types.hpp定义本能力的全部状态模型:WindowSizeState(比例与边界)、DpiDependentSizes(DPI 缩放常量)、CaptureState(含resize_pending/pending_extent_mutex的切换事务字段),以及内部消息常量WM_APPLY_CAPTURE_SIZE。interaction.cpp处理窗口过程转发来的消息,在拖拽缩放时读取state.preview->size.aspect_ratio复算宽高,保证用户交互也不破坏比例。- 窗口过程采用 GWLP_USERDATA 模式:
WM_NCCREATE时从CREATESTRUCT.lpCreateParams取回core::AppState*并写入窗口句柄的用户数据槽,之后每条消息直接GetWindowLongPtr取回,避免全局变量。
核心实现详解
1. 窗口创建与外观(window.cpp)
窗口类 SpinningMomoPreviewWindowClass 使用 CS_HREDRAW | CS_VREDRAW 注册,创建时带有三个扩展样式,决定了预览窗的"悬浮小窗"形态:
1auto create_preview_window(HINSTANCE instance, int width, int height, core::AppState* state)
2 -> HWND {
3 return CreateWindowExW(WS_EX_TOOLWINDOW | WS_EX_TOPMOST | WS_EX_LAYERED,
4 features::preview::PREVIEW_WINDOW_CLASS, L"PreviewWindow", WS_POPUP, 0, 0,
5 width, height, nullptr, nullptr, instance, state);
6}Source: window.cpp
设计意图:WS_EX_TOOLWINDOW 使其不出现在任务栏与 Alt-Tab 列表;WS_EX_TOPMOST 保证悬浮预览始终可见;WS_EX_LAYERED + WS_POPUP 组合允许无系统边框的自绘外观与整体透明度控制。core::AppState* 作为 lpCreateParams 传入,是 GWLP_USERDATA 挂载模式的数据来源。
外观设置集中在 setup_window_appearance():
1auto setup_window_appearance(HWND hwnd) -> void {
2 // 设置透明度
3 SetLayeredWindowAttributes(hwnd, 0, 255, LWA_ALPHA);
4
5 // 设置DWM属性
6 MARGINS margins = {1, 1, 1, 1};
7 DwmExtendFrameIntoClientArea(hwnd, &margins);
8
9 DWMNCRENDERINGPOLICY policy = DWMNCRP_ENABLED;
10 DwmSetWindowAttribute(hwnd, DWMWA_NCRENDERING_POLICY, &policy, sizeof(policy));
11
12 BOOL value = TRUE;
13 DwmSetWindowAttribute(hwnd, DWMWA_ALLOW_NCPAINT, &value, sizeof(value));
14
15 // Windows 11 圆角
16 DWM_WINDOW_CORNER_PREFERENCE corner = DWMWCP_ROUNDSMALL;
17 DwmSetWindowAttribute(hwnd, DWMWA_WINDOW_CORNER_PREFERENCE, &corner, sizeof(corner));
18}Source: window.cpp
不透明度初始为 255(完全不透明),为运行期透明度调节预留通道;DwmExtendFrameIntoClientArea 把 1px DWM 边框延伸进客户区,配合 DWMWA_NCRENDERING_POLICY = DWMNCRP_ENABLED 与 DWMWA_ALLOW_NCPAINT 让自绘标题区与系统合成共存;DWMWCP_ROUNDSMALL 在 Windows 11 上获得小圆角,与"精致悬浮窗"的产品定位一致。
2. 窗口过程与消息分发(GWLP_USERDATA 模式)
1LRESULT CALLBACK preview_window_proc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
2 core::AppState* state = nullptr;
3
4 if (message == WM_NCCREATE) {
5 const auto* cs = reinterpret_cast<CREATESTRUCT*>(lParam);
6 state = reinterpret_cast<core::AppState*>(cs->lpCreateParams);
7 SetWindowLongPtr(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(state));
8 } else {
9 state = reinterpret_cast<core::AppState*>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
10 }
11
12 if (!state) {
13 return DefWindowProcW(hwnd, message, wParam, lParam);
14 }
15
16 // 使用交互模块处理消息
17 auto [handled, lresult] =
18 features::preview::interaction::handle_preview_message(*state, hwnd, message, wParam, lParam);
19
20 if (handled) {
21 return lresult;
22 }
23
24 return DefWindowProcW(hwnd, message, wParam, lParam);
25}Source: window.cpp
这是整个窗口控制能力的消息入口。WM_NCCREATE 是窗口创建期唯一能安全拿到 lpCreateParams 的时机(且早于任何可能用到状态的绘制消息),因此在此一次性挂载;后续消息统一解引用并转交 handle_preview_message,返回结构化二元组 (handled, lresult),让窗口过程保持极薄——所有交互/尺寸逻辑都在交互模块内实现,窗口过程只做路由。比例/尺寸切换相关的内部消息(WM_APPLY_CAPTURE_SIZE 等 WM_USER + n 常量)也经由这条链路进入交互模块处理。
3. 比例驱动的尺寸计算(set_preview_window_size)
这是"画面比例/尺寸切换"的算法核心:
1auto set_preview_window_size(core::AppState& app_state, int capture_width, int capture_height)
2 -> void {
3 auto* const state = app_state.preview ? app_state.preview.get() : nullptr;
4 if (!state) {
5 return;
6 }
7
8 if (state->is_first_show && state->has_screen_rect) {
9 const int screen_width = utils::display::rect_width(state->screen_rect);
10 const int screen_height = utils::display::rect_height(state->screen_rect);
11 state->size.min_ideal_size = std::min(screen_width, screen_height) / 10;
12 state->size.max_ideal_size = std::max(screen_width, screen_height);
13 state->size.ideal_size = screen_height / 2;
14 }
15
16 state->size.aspect_ratio = static_cast<float>(capture_height) / capture_width;
17
18 if (state->size.aspect_ratio >= 1.0f) {
19 // 高度大于等于宽度
20 state->size.window_height = state->size.ideal_size;
21 state->size.window_width =
22 static_cast<int>(state->size.window_height / state->size.aspect_ratio);
23 } else {
24 // 宽度大于高度
25 state->size.window_width = state->size.ideal_size;
26 state->size.window_height =
27 static_cast<int>(state->size.window_width * state->size.aspect_ratio);
28 }
29
30 if (state->is_first_show) {
31 state->is_first_show = false;
32 int left = 20;
33 int top = 20;
34 if (state->has_screen_rect) {
35 left = state->screen_rect.left + 20;
36 top = state->screen_rect.top + 20;
37 }
38 SetWindowPos(state->hwnd, nullptr, left, top, state->size.window_width,
39 state->size.window_height, SWP_NOZORDER | SWP_NOACTIVATE);
40 } else {
41 SetWindowPos(state->hwnd, nullptr, 0, 0, state->size.window_width, state->size.window_height,
42 SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
43 }
44}Source: window.cpp
算法逐段解读:
aspect_ratio的语义:高 / 宽。>= 1.0f表示竖屏(含正方形),< 1.0f表示横屏。ideal_size是"较长边"的长度:竖屏时ideal_size直接赋给高度,宽度 = 高度 ÷ 比例;横屏时ideal_size直接赋给宽度,高度 = 宽度 × 比例。这样无论比例如何,窗口的最长边始终等于用户设定的理想边长,交互语义统一("窗口多大"由一个标量控制)。- 边界初始化只在首次显示执行一次(
is_first_show && has_screen_rect):min = min(屏幕宽,高)/10(例如 1080p 下为 108),max = max(屏幕宽,高)(如 1920),ideal_size默认为屏幕高度的一半(如 540)。这些值在initialize_preview_window()中先被写入保守默认(108 / 1920 / 540),并注释说明会在start_preview()解析工作显示器后按实际屏幕重写。 - 首次显示定位:放在工作显示器屏幕矩形的
(left+20, top+20)处,避免 (0,0) 贴角;之后的尺寸切换用SWP_NOMOVE只改大小不动位置,防止用户已手动拖动窗口后被"弹回"。 - 强制使用
int截断:static_cast<int>对浮点比例运算结果截断,代价是亚像素误差(<1px),换来SetWindowPos需要的整数像素,避免每帧闪烁。
4. 交互中的比例保持(interaction.cpp)
用户拖拽缩放时同样不允许破坏比例——缩放后的新 ideal_size 会依据当前 aspect_ratio 重新推导宽高:
1 int newWidth, newHeight;
2 if (state.preview->size.aspect_ratio >= 1.0f) {
3 newHeight = newIdealSize;
4 newWidth = static_cast<int>(newHeight / state.preview->size.aspect_ratio);
5 } else {
6 newWidth = newIdealSize;
7 newHeight = static_cast<int>(newWidth * state.preview->size.aspect_ratio);
8 }Source: interaction.cpp
注意这里读取的是 state.preview->size.aspect_ratio(与窗口尺寸模块共享同一份状态),且复用了与 set_preview_window_size() 完全相同的两分支公式——单一数据源 + 单一公式是画面不变形的关键保障:无论尺寸变化来自"切换捕获分辨率"还是"用户拖拽缩放",最终几何都由同一个 aspect_ratio 推导。
状态模型与数据流
Source: types.hpp
关键类型说明:
| 类型 | 职责 |
|---|---|
WindowSizeState | 窗口几何的唯一事实来源:当前宽高、比例、理想边长及上下界 |
DpiDependentSizes | 以 96 DPI 为基准(BASE_TITLE_HEIGHT=24 等)按 dpi/96 线性缩放的 UI 常量 |
CaptureState | 业务层捕获会话封装,含跨线程尺寸切换事务字段 |
CaptureExtent | 尺寸切换事务的目标捕获尺寸(带默认 == 比较,便于跳过无变化切换) |
尺寸切换端到端数据流
流程要点(依据 types.hpp 中 CaptureState 的注释与字段):
- 事务权(
resize_pending):注释明确其语义是"窗口线程已拥有或即将拥有一次捕获尺寸切换事务"。它防止多个切换请求并发重建捕获会话与渲染资源。 pending_extent_mutex串行化:注释指出"事务权与pending_extent的更新由pending_extent_mutex串行化,避免遗漏最新尺寸"——即当切换请求在事务进行中再次到达时,不丢弃而是把最新目标尺寸写进pending_extent,事务完成后读取的是最新值,保证快速连续切换(如按住方向键循环比例)不出现"落在中间档"的错位。WM_APPLY_CAPTURE_SIZE(WM_USER + 4):内部消息把"应用新捕获尺寸"的动作转移到窗口线程的消息队列执行,天然与窗口几何操作(SetWindowPos)串行在同一 UI 线程,规避跨线程直接操作 HWND 的风险。resources_busy(RenderingResources内的 atomic):注释标注"标记渲染资源是否正忙(如尺寸调整等)",即尺寸调整期间渲染资源被独占,避免重建缓冲区/SRV 时被渲染线程读取。
Core Flow:首次显示与运行期切换
运行期 DPI 变化(如跨不同缩放比例的显示器拖动)由 update_preview_window_dpi() 处理:先 GetWindowRect 读取当前几何,再更新 DPI 相关尺寸常量,最后重建窗口更新(源码 192-200 行起)。初始 DPI 在 initialize_preview_window() 中通过 GetDeviceCaps(hdc, LOGPIXELSX) 采样并传入 update_dpi_scaling(dpi)。
Configuration Options
| 配置项 / 常量 | 类型 | 默认值 | 说明 |
|---|---|---|---|
[AspectRatioItems] 节 | 配置节(INI) | 空 | 用户自定义画面比例切换项,保存并重启后生效 |
[ResolutionItems] 节 | 配置节(INI) | 空 | 用户自定义分辨率切换项,保存并重启后生效 |
kInitialPreviewSize | constexpr int | 540 | 窗口创建时的初始边长(随后被比例推导覆盖) |
min_ideal_size 初始占位 | int | 108 | initialize_preview_window() 中的保守默认,屏幕解析后重写 |
max_ideal_size 初始占位 | int | 1920 | 同上 |
ideal_size 初始占位 | int | 540 | 同上,屏幕解析后为屏幕高/2 |
min_ideal_size 推导 | int | min(屏幕宽,高)/10 | 首次显示时由工作显示器屏幕矩形推导 |
max_ideal_size 推导 | int | max(屏幕宽,高) | 首次显示时推导,窗口最大不超过屏幕长边 |
ideal_size 推导 | int | 屏幕高/2 | 首次显示时的默认理想边长 |
BASE_TITLE_HEIGHT | constexpr int | 24 | 96 DPI 基准标题高度 |
BASE_FONT_SIZE | constexpr int | 12 | 96 DPI 基准字体大小 |
BASE_BORDER_WIDTH | constexpr int | 8 | 96 DPI 基准边框宽度 |
BASE_VIEWPORT_LINE_WIDTH | constexpr int | 3 | 96 DPI 基准视口框线宽(3dp) |
SetLayeredWindowAttributes alpha | BYTE | 255 | 分层窗口初始不透明度(完全不透明) |
MARGINS DWM 延伸 | 四向 int | {1,1,1,1} | DWM 边框延伸进客户区的厚度 |
DWMWA_WINDOW_CORNER_PREFERENCE | enum | DWMWCP_ROUNDSMALL | Windows 11 小圆角偏好 |
| 首次显示偏移 | int | 20 | 相对屏幕矩形 (left+20, top+20) 的初始位置 |
配置节的使用方式来自设置页本地化文案(zh-CN / en-US 的 message.settings_config_help),说明该配置文件由设置功能负责解析与持久化,本能力只消费其结果(可选比例/分辨率列表)。配置文件的加载与校验细节见设置相关页面。
API Reference
set_preview_window_size(app_state: core::AppState&, capture_width: int, capture_height: int) -> void
依据捕获画面尺寸重算并应用预览窗口几何。
Parameters:
app_state(core::AppState&): 全局状态聚合,app_state.preview为空时直接返回capture_width(int): 新捕获画面宽度(像素)capture_height(int): 新捕获画面高度(像素)
Returns: 无。副作用为更新 WindowSizeState 全部字段并对 state->hwnd 执行 SetWindowPos。
行为细节: 首次调用(is_first_show == true 且 has_screen_rect)会先推导 min/max/ideal_size 并定位到 (left+20, top+20);否则仅以 SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE 调整尺寸。
Source: window.cpp
create_preview_window(instance: HINSTANCE, width: int, height: int, state: core::AppState*) -> HWND
创建预览窗口,扩展样式 WS_EX_TOOLWINDOW | WS_EX_TOPMOST | WS_EX_LAYERED,普通样式 WS_POPUP,标题 L"PreviewWindow"。state 通过 lpCreateParams 传递,供窗口过程在 WM_NCCREATE 时挂载。
Source: window.cpp
setup_window_appearance(hwnd: HWND) -> void
应用分层透明度(alpha=255, LWA_ALPHA)、DWM 边框延伸、NC 渲染策略、允许 NC 绘制、Windows 11 小圆角。无返回值,失败被静默忽略(DWM 属性为尽力而为的外观增强)。
Source: window.cpp
preview_window_proc(hwnd: HWND, message: UINT, wParam: WPARAM, lParam: LPARAM) -> LRESULT CALLBACK
窗口过程。WM_NCCREATE 时从 CREATESTRUCT.lpCreateParams 取 core::AppState* 写入 GWLP_USERDATA;其余消息读取挂载状态,转交 features::preview::interaction::handle_preview_message 处理;未处理或无状态时回落 DefWindowProcW。
Returns: 交互模块 handled == true 时返回其 lresult,否则 DefWindowProcW 结果。
Source: window.cpp
register_preview_window_class(instance: HINSTANCE) -> bool
注册 PREVIEW_WINDOW_CLASS(L"SpinningMomoPreviewWindowClass"),样式 CS_HREDRAW | CS_VREDRAW,光标 IDC_ARROW,背景 (COLOR_WINDOW + 1)。
Returns: RegisterClassExW 成功与否。
Source: window.cpp
create_window(instance: HINSTANCE, state: core::AppState&) -> std::expected<HWND, std::string>
按顺序:注册窗口类(失败返回 "Failed to register preview window class")→ 以 kInitialPreviewSize = 540 创建 540×564 窗口(失败返回 "Failed to create preview window")→ setup_window_appearance。成功返回 HWND。
Source: window.cpp
initialize_preview_window(state: core::AppState&, instance: HINSTANCE) -> std::expected<void, std::string>
窗口初始化总入口:调用 create_window → 写入 state.preview->hwnd 并置 is_first_show = true → 以 GetDeviceCaps(hdc, LOGPIXELSX) 采样 DPI 并 update_dpi_scaling → 写入占位尺寸边界(108/1920/540,待 start_preview() 重写)。
Source: window.cpp
show_preview_window / hide_preview_window(state: core::AppState&) -> void
对 state.preview->hwnd 执行 ShowWindow(SW_SHOW / SW_HIDE),句柄为空时不操作。
Source: window.cpp
DpiDependentSizes::update_dpi_scaling(new_dpi: UINT) -> void
内联方法。记录 dpi,按 scale = new_dpi / 96.0 重算 title_height / font_size / border_width / viewport_line_width。
Source: types.hpp
Failure Modes, Edge Cases & Concurrency
失败模式
| 场景 | 检测点 | 处理方式 |
|---|---|---|
| 窗口类注册失败 | register_preview_window_class 返回 false | create_window 返回 std::unexpected("Failed to register preview window class"),错误沿 initialize_preview_window 上抛 |
| 窗口创建失败 | CreateWindowExW 返回 null | 返回 std::unexpected("Failed to create preview window") |
preview 状态为空 | set_preview_window_size 开头的 if (!state) return; | 静默返回,避免初始化早期崩溃 |
| DWM 属性设置失败 | DwmSetWindowAttribute 返回值未检查 | 尽力而为:外观增强缺失不影响几何功能(旧系统无圆角等) |
| 快速连续切换比例 | resize_pending == true 时新请求到达 | 不重复开事务,仅以互斥锁更新 pending_extent 为最新值 |
边界情况
- 比例等于 1(正方形):
aspect_ratio >= 1.0f分支按"竖屏"处理,ideal_size赋给高度,宽度 = 高度 ÷ 1 = 同值,行为正确。 - 极窄/极宽捕获源:宽高由单一
ideal_size边长推导,另一边可能远小于min_ideal_size——边界只约束ideal_size本身(最长边),不约束短边,属预期设计。 is_first_show但无屏幕矩形(has_screen_rect == false):跳过边界推导,回退到 (20,20) 定位,保证窗口仍可见可用。- 重复 SetWindowPos:
InteractionState.last_game_window_pos字段注释"用于跳过重复的 SetWindowPos 调用",说明交互层对游戏窗口存在去重优化,避免无变化的位置更新引发闪烁。 - 任务栏重绘干扰:
TIMER_ID_TASKBAR_REDRAW定时器(200ms 延迟)与taskbar_redraw_suppressed标志用于抑制/延迟任务栏重绘,避免置顶窗口操作带来的任务栏残影。
并发模型
并发保障来自四层机制(依据 types.hpp 源码与注释):
std::atomic<bool> resize_pending:以原子布尔作为"事务权",窗口线程独占一次尺寸切换事务,防止并发重建。std::mutex pending_extent_mutex:把"事务权更新 + 目标尺寸写入"整体串行化,源码注释明确其目的是"避免遗漏最新尺寸"。- 窗口消息队列串行化:
WM_APPLY_CAPTURE_SIZE是WM_USER + 4内部消息,几何落地天然在窗口线程顺序执行,与SetWindowPos同线程,无 HWND 跨线程竞争。 std::atomic<bool> resources_busy:尺寸调整期间渲染资源被标记为忙,渲染线程据此避让,防止重建中的顶点缓冲/SRV 被并发读取。
另外 CaptureState 中的 last_frame_width / last_frame_height 也为 std::atomic<int>,供各线程无锁读取最近帧尺寸;RenderingResources::initialized 同样是原子标志。
Performance & Operational Notes
- 热路径成本极低:尺寸切换只涉及 O(1) 浮点运算与一次
SetWindowPos;aspect_ratio的static_cast<int>截断带来 <1px 误差,无可感知影响。 SetWindowPos标志裁剪:非首次调整始终带SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE,避免触发不必要的重排/激活/置顶副作用(SWP_NOACTIVATE对WS_EX_TOPMOST悬浮窗尤其重要,防止尺寸切换抢焦点)。- 首次显示的定位策略(屏幕矩形 +20 偏移)保证新窗口不遮挡屏幕原点区域的系统 UI,同时在多显示器场景下跟随工作显示器。
- DPI 处理时机:初始化时采样一次;运行期由
update_preview_window_dpi在 DPI 变化时重建窗口尺寸相关常量并更新窗口,保证高 DPI 下边框/标题/视口线宽视觉一致(viewport_line_width注释明确为 3dp 基准)。 - 运行期操作:
show_preview_window/hide_preview_window提供显示/隐藏开关;透明度通道已就绪(LWA_ALPHA),但初始固定 255。 - 渲染资源忙标记的开销:原子 bool 读写,尺寸调整与渲染互斥几乎零成本。
Extension Points
- 新增切换档位:向配置文件的
[AspectRatioItems]/[ResolutionItems]节添加条目即可扩展用户可选的比例/分辨率档位;语义上保存并重启后生效(见message.settings_config_help文案),解析与 UI 归设置功能负责。 - 新增内部消息:
types.hpp已占用WM_USER + 1..4(WM_SCHEDULE_PREVIEW_CLEANUP/WM_CANCEL_PREVIEW_CLEANUP/WM_IMMEDIATE_PREVIEW_CLEANUP/WM_APPLY_CAPTURE_SIZE),新增窗口线程任务应从WM_USER + 5起编号,并在interaction::handle_preview_message中增加对应处理分支。 - 新增 DPI 依赖尺寸:在
DpiDependentSizes中加BASE_*常量并在update_dpi_scaling()中同步缩放,即可自动获得 DPI 一致性。 - 调整尺寸边界策略:修改
set_preview_window_size()中首屏边界推导公式(短边/10、长边、高/2)即可改变允许的窗口尺寸范围;公式集中一处,改动影响面可控。 - 更换比例公式:若未来需要支持非等比/信箱模式,
set_preview_window_size()与interaction.cpp中复用的同一公式需同步修改——两处必须保持一致,否则交互拖拽与切换结果将分叉。
Tests
源码证据未发现 features/preview 相关的自动化测试文件;本页所有行为描述均直接来自实现源码(window.cpp、types.hpp、interaction.cpp)与其中的中文注释。并发模型(事务权 + 互斥锁 + 消息队列串行化)的语义以 types.hpp 内注释为权威说明。
Related Links
- 源码:window.cpp(窗口创建、外观、比例驱动尺寸计算、DPI 更新)
- 源码:types.hpp(
WindowSizeState/DpiDependentSizes/CaptureState状态模型与内部消息常量) - 源码:interaction.cpp(拖拽缩放的比例保持)
- 源码:app_state.hpp(
core::AppState聚合中preview状态的声明) - 兄弟主题:捕获采集管线(
utils/graphics/capture的CaptureSession)、预览渲染(features/preview/rendering.cpp)、设置与配置解析([AspectRatioItems]/[ResolutionItems]的加载与持久化)分别在各自目录页覆盖