Files
Unlucky-Launcher/crates/app-iced/src/platform/win32/transparency/dwm.rs
T
AskaEth 91f723a019 docs(app-iced): M116 r4 —— 日志/模板示例改用真进程实测值(frame=overlapped)
真进程两跑(`--software-renderer`,同一个 bin,只改整窗 α)拿到:
  α=1.0 → … frame=overlapped layered=off
  α<1   → … frame=overlapped layered=on suppressed_by=window_alpha
所以把两处仍拿 `frame=popup` 当示例的文档改成实测值:
- `pages/appearance/mod.rs` 的取证模板:`frame=overlapped layered=on … suppressed_by=window_alpha`,
  并注明真进程两条在 `DevDocs/artifacts/m116/README.md`、`frame=` 恒为 `overlapped`/`other`。
- `transparency/dwm.rs` 的 `style_summary` 示例:同上,并点明 `popup` 只属于带 owner 的弹出窗。

注释级改动,行为与断言零改动。
2026-11-05 06:35:20 +08:00

395 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! DWM 圆角(M116):把 [`CornerPlan`](super::super::corner::CornerPlan) 落成一次属性下发。
//!
//! ## 本文件零 `unsafe`、零 `extern`
//!
//! `DwmSetWindowAttribute` / `DwmGetWindowAttribute` 的声明与调用点在 [`super`]
//! (全仓唯一 `unsafe` 落点);这里只做「哪一档 → 哪个属性值」的映射、`HRESULT` 解释与回读。
//!
//! ## 为什么「直角」也要真的下发一次
//!
//! `DWMWCP_DONOTROUND` 是**显式**的直角请求。不下发也能得到直角(Win10 就没有这个属性),
//! 但那样日志里「方」与「没生效」长得一模一样。下发 + 回读之后,
//! `corner=… source=square excluded_reason=user` 与 `excluded_reason=os-build` 是两条
//! 可区分的事实,而不是一句猜测。
//!
//! ## 回读为什么是证据而不是自证
//!
//! `DwmSetWindowAttribute` 返回 `S_OK` 只说明「参数被接受了」。属性值由 DWM 保存,
//! 回读拿到的是 **DWM 侧的当前值**——它能抓住「设了 A 属性、读回还是旧值」这类事
//! (比如把 `DWMWA_WINDOW_CORNER_PREFERENCE` 写成别的号,回读会给出 `None` 或旧值)。
/// `DWMWA_WINDOW_CORNER_PREFERENCE`:窗口圆角偏好的属性号(Win11 / Server 2025,build ≥ 22000)。
pub const DWMWA_WINDOW_CORNER_PREFERENCE: u32 = 34;
/// `DWMWCP_DONOTROUND`:显式直角。
pub const DWMWCP_DONOTROUND: u32 = 1;
/// `DWMWCP_ROUND`:显式圆角(半径由系统决定,**不暴露**给应用)。
pub const DWMWCP_ROUND: u32 = 2;
/// `DWMWA_VISIBLE_FRAME_BORDER_THICKNESS`:可见边框厚度(像素,Win11 起)。
///
/// ⚠️ **本机实测它不能当「圆不圆」的判据**(我最初以为能,被探针否掉了):
/// 三种窗口样式 × 三种状态(默认 / `DWMWCP_ROUND` / `DWMWCP_DONOTROUND`)读出来**全是 `1`**,
/// 没有任何区分度。保留它只作诊断读数(万一将来在别的机器上有变化),不参与判定。
/// 证据:`DevDocs/artifacts/m116/probe-table.md` 的 `bdef` / `brnd` / `bsq` 三列。
pub const DWMWA_VISIBLE_FRAME_BORDER_THICKNESS: u32 = 37;
/// `DWMWA_EXTENDED_FRAME_BOUNDS`:DWM 认为的窗口边界(**含**不可见的调整带)。
pub const DWMWA_EXTENDED_FRAME_BOUNDS: u32 = 9;
/// 一次圆角下发的结局(日志与测试共用)。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CornerOutcome {
/// 非 Windows:空实现,什么都没做。
UnsupportedPlatform,
/// 句柄为 0:还没拿到窗口,或窗口已销毁。
NoWindow,
/// 已下发且回读一致。`readback == None` 表示**这一版 DWM 不接受回读**
/// (属性不被认识时 `DwmGetWindowAttribute` 返回 `E_INVALIDARG`),此时只有 `S_OK` 作证。
Applied {
/// 下发下去的偏好值([`DWMWCP_ROUND`] / [`DWMWCP_DONOTROUND`])。
preference: u32,
/// DWM 侧回读到的值(`None` = 读不回来)。
readback: Option<u32>,
/// 回读那一次的 `HRESULT`(`0` = `S_OK`)。
read_hresult: u32,
},
/// 属性不被这个 Windows 版本认识(`E_INVALIDARG`):Win10 就是这个结局。
Unsupported {
/// `DwmSetWindowAttribute` 的原始 `HRESULT`。
hresult: u32,
},
/// 其他失败(句柄无效等),`hresult` 来自 DWM。
Failed {
/// `DwmSetWindowAttribute` 的原始 `HRESULT`。
hresult: u32,
},
}
impl CornerOutcome {
/// 一行日志尾巴:`corner=source=dwm pref=2 readback=2`。
///
/// `readback=?` 与 `pref=none`(平台不支持)是不同的坏消息:前者是「设了但读不回来」,
/// 后者是「这里根本没有这个能力」。
#[must_use]
pub fn describe(&self) -> String {
match self {
Self::UnsupportedPlatform => "pref=none readback=n/a platform=unsupported".to_owned(),
Self::NoWindow => "pref=none readback=n/a window=none".to_owned(),
Self::Applied {
preference,
readback,
read_hresult,
} => format!(
"pref={preference} readback={} read_hr=0x{read_hresult:08X}",
readback.map_or_else(|| "?".to_owned(), |value| value.to_string())
),
Self::Unsupported { hresult } => {
format!("pref=none readback=n/a hresult=0x{hresult:08X} (attribute unsupported)")
}
Self::Failed { hresult } => format!("pref=none readback=n/a hresult=0x{hresult:08X}"),
}
}
}
/// `HRESULT` 是不是 `E_INVALIDARG`(`0x8007_0057`):Win10 上设这个属性就是这个返回值。
#[cfg(target_os = "windows")]
const E_INVALIDARG: u32 = 0x8007_0057;
/// 把「来源」映射成要下发的偏好值(纯函数,测试直接钉它)。
///
/// `Dwm` → [`DWMWCP_ROUND`];`Square` → [`DWMWCP_DONOTROUND`]。
/// 刻意**不用**默认值(`0`):那在 Win11 上等于「请 DWM 自己决定」,而无边框窗口
/// 到底算不算「有框」由 DWM 的启发式决定——那正是我们要么拿到、要么明确放弃的东西。
#[must_use]
pub const fn preference_for(source: super::super::corner::CornerSource) -> u32 {
use super::super::corner::CornerSource;
match source {
CornerSource::Dwm => DWMWCP_ROUND,
CornerSource::Square => DWMWCP_DONOTROUND,
}
}
/// 下发一次圆角偏好(幂等:重复调用结果相同)。
pub fn apply_corner(hwnd: isize, plan: &super::super::corner::CornerPlan) -> CornerOutcome {
if hwnd == 0 {
return CornerOutcome::NoWindow;
}
set_preference(hwnd, preference_for(plan.source))
}
/// 下发一个具体的偏好值并回读(拆开是为了让探针能逐格遍历)。
pub fn set_preference(hwnd: isize, preference: u32) -> CornerOutcome {
#[cfg(not(target_os = "windows"))]
{
let _ = (hwnd, preference);
return CornerOutcome::UnsupportedPlatform;
}
#[cfg(target_os = "windows")]
{
if hwnd == 0 {
return CornerOutcome::NoWindow;
}
let hresult = super::dwm_set_u32(hwnd, DWMWA_WINDOW_CORNER_PREFERENCE, preference);
if !super::hresult_ok(hresult) {
// 原始位,不是 `unsigned_abs()`(见 `dwm_get_u32_hr` 的注释:后者印不出 HRESULT)。
let code = hresult as u32;
return if code == E_INVALIDARG {
CornerOutcome::Unsupported { hresult: code }
} else {
CornerOutcome::Failed { hresult: code }
};
}
let (readback, read_hresult) = corner_preference_hr(hwnd);
CornerOutcome::Applied {
preference,
readback,
read_hresult,
}
}
}
/// 读圆角偏好**以及它的 `HRESULT`**(`(None, code)` = 读不回来,`code` 是 DWM 给的原因)。
///
/// 为什么要暴露 `HRESULT`:M116 本机实测——**写成功、读回 `E_INVALIDARG`**。只报 `?`
/// 会让「设上了吗」变成猜谜;有了码就能说清是「属性不支持读」而不是「没设上」。
#[must_use]
pub fn corner_preference_hr(hwnd: isize) -> (Option<u32>, u32) {
#[cfg(target_os = "windows")]
{
super::dwm_get_u32_hr(hwnd, DWMWA_WINDOW_CORNER_PREFERENCE)
}
#[cfg(not(target_os = "windows"))]
{
let _ = hwnd;
(None, 0)
}
}
/// 读可见边框厚度(像素)。**诊断用,不可作判据**(本机三样式三态实测全是 1,见常量文档)。
#[must_use]
pub fn visible_border_thickness(hwnd: isize) -> Option<u32> {
#[cfg(target_os = "windows")]
{
super::dwm_get_u32(hwnd, DWMWA_VISIBLE_FRAME_BORDER_THICKNESS)
}
#[cfg(not(target_os = "windows"))]
{
let _ = hwnd;
None
}
}
/// 读 DWM 的窗口边界(`left, top, right, bottom`,屏幕坐标)。
#[must_use]
pub fn extended_frame_bounds(hwnd: isize) -> Option<(i32, i32, i32, i32)> {
#[cfg(target_os = "windows")]
{
super::dwm_get_rect(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS)
.map(|rect| (rect.left, rect.top, rect.right, rect.bottom))
}
#[cfg(not(target_os = "windows"))]
{
let _ = hwnd;
None
}
}
/// 读 `GetWindowRect`(含 DWM 加出来的不可见边框),用于和 [`extended_frame_bounds`] 求差。
#[must_use]
pub fn window_rect(hwnd: isize) -> Option<(i32, i32, i32, i32)> {
#[cfg(target_os = "windows")]
{
super::window_rect(hwnd).map(|rect| (rect.left, rect.top, rect.right, rect.bottom))
}
#[cfg(not(target_os = "windows"))]
{
let _ = hwnd;
None
}
}
/// 读窗口样式位(`GWL_STYLE` = `-16`):日志里 `frame=` 那一栏的取值来源。
///
/// ⚠️ 产品窗口**不是** `WS_POPUP`(M116 r4 复审订正):winit 在 `decorations = false` 下给的是
/// `WS_CAPTION|WS_BORDER|...`(见 `probe::PRODUCT_STYLE`),真进程的 `frame=` 应为 `overlapped`
/// (关掉 min/max 则 `other`)。「无边框」是 `WM_NCCALCSIZE` 的效果,不是样式位。
#[must_use]
pub fn window_style(hwnd: isize) -> Option<isize> {
#[cfg(target_os = "windows")]
{
(hwnd != 0).then(|| super::window_style(hwnd))
}
#[cfg(not(target_os = "windows"))]
{
let _ = hwnd;
None
}
}
/// 读窗口扩展样式位(`GWL_EXSTYLE`):配合 [`window_style`] 回答「我们是不是 `WS_EX_LAYERED`」。
#[must_use]
pub fn window_ex_style(hwnd: isize) -> Option<isize> {
#[cfg(target_os = "windows")]
{
(hwnd != 0).then(|| super::raw_ex_style(hwnd))
}
#[cfg(not(target_os = "windows"))]
{
let _ = hwnd;
None
}
}
/// `WS_POPUP` 位(`0x8000_0000`):**带 owner 的弹出窗**才置它;产品主窗口不用这一位。
pub const WS_POPUP: isize = 0x8000_0000_u32 as isize;
/// 帧样式的一句话标签(纯函数,日志用):`popup` / `overlapped` / `other`。
///
/// 为什么要它:用户的现象是「别的窗口有圆角,我们的没有」。把它打进日志,才能一眼看出
/// 窗口形状这一项**不是**变量(产品恒为 `overlapped`)——真正让系统忽略圆角请求的是整窗 α
/// 引入的 `WS_EX_LAYERED`(见 `layered=` 与 `suppressed_by=window_alpha`),不是 `popup` 形状。
#[must_use]
pub const fn frame_label(style: isize) -> &'static str {
const WS_OVERLAPPEDWINDOW_MASK: isize = 0x00CF_0000;
if style & WS_POPUP != 0 {
"popup"
} else if style & WS_OVERLAPPEDWINDOW_MASK == WS_OVERLAPPEDWINDOW_MASK {
"overlapped"
} else {
"other"
}
}
/// 分层位的一句话标签(纯函数,日志用):`on` / `off`。
#[must_use]
pub const fn layered_label(ex_style: isize) -> &'static str {
if ex_style & super::WS_EX_LAYERED != 0 {
"on"
} else {
"off"
}
}
/// 一个窗口的样式摘要(`frame=overlapped layered=off`,产品真进程实测值);读不到时如实写
/// `frame=? layered=?`。「`frame=popup`」只会出现在**带 owner 的弹出窗**上,产品不会。
#[must_use]
pub fn style_summary(hwnd: isize) -> String {
let frame = window_style(hwnd).map_or("?", frame_label);
let layered = window_ex_style(hwnd).map_or("?", layered_label);
format!("frame={frame} layered={layered}")
}
#[cfg(test)]
mod tests {
use super::{
apply_corner, corner_preference_hr, preference_for, CornerOutcome,
DWMWA_VISIBLE_FRAME_BORDER_THICKNESS, DWMWA_WINDOW_CORNER_PREFERENCE, DWMWCP_DONOTROUND,
DWMWCP_ROUND,
};
use crate::platform::win32::corner::{CornerPlan, CornerSource};
use unlucky_presenter::prefs::WindowsVersion;
/// **抓得住**:属性号写错(34 写成 33 / 37 写成 36 —— 这两个都在 Win11 上存在,改错不会崩,
/// 只会静默读出别的东西)、两个偏好值互换。
/// **抓不住**:这些值在真窗口上是否真的生效(那要探针读数,见 `probe` 与报告)。
#[test]
fn attribute_numbers_follow_the_win32_header() {
assert_eq!(DWMWA_WINDOW_CORNER_PREFERENCE, 34);
assert_eq!(DWMWA_VISIBLE_FRAME_BORDER_THICKNESS, 37);
assert_eq!(DWMWCP_ROUND, 2);
assert_eq!(DWMWCP_DONOTROUND, 1);
}
/// **抓得住**:`preference_for` 把圆角/直角映射反了(这一格反了就是「设置里选直角反而更圆」)。
#[test]
fn the_preference_mapping_cannot_be_swapped() {
assert_eq!(preference_for(CornerSource::Dwm), DWMWCP_ROUND);
assert_eq!(preference_for(CornerSource::Square), DWMWCP_DONOTROUND);
}
/// 无效句柄:**不下发**、也不 panic。句柄 0 走 `NoWindow`(还没拿到窗口 ≠ 平台不支持)。
#[test]
fn a_missing_window_is_reported_not_guessed() {
let plan = CornerPlan {
source: CornerSource::Dwm,
excluded: None,
os: Some(WindowsVersion::new(10, 0, 26_100)),
};
assert_eq!(apply_corner(0, &plan), CornerOutcome::NoWindow);
assert_eq!(corner_preference_hr(0).0, None);
// 1 号句柄永远无效:非 Windows 上是「平台不支持」,Windows 上是 DWM 的失败码,
// 两种都不许表现成「成功」。
let outcome = apply_corner(1, &plan);
assert!(
matches!(
outcome,
CornerOutcome::Failed { .. }
| CornerOutcome::Unsupported { .. }
| CornerOutcome::UnsupportedPlatform
),
"无效句柄应报失败,实际 {outcome:?}"
);
}
/// 日志尾巴三态可区分:设了 / 设了但读不回 / 属性不支持。
#[test]
fn the_log_tail_separates_applied_from_unsupported() {
let applied = CornerOutcome::Applied {
preference: DWMWCP_ROUND,
readback: Some(DWMWCP_ROUND),
read_hresult: 0,
};
assert!(
applied.describe().starts_with("pref=2 readback=2"),
"{applied:?}"
);
let blind = CornerOutcome::Applied {
preference: DWMWCP_ROUND,
readback: None,
read_hresult: 0x8000_4005,
};
assert!(
blind.describe().contains("readback=?"),
"{}",
blind.describe()
);
let old = CornerOutcome::Unsupported {
hresult: 0x8007_0057,
};
assert!(
old.describe().contains("attribute unsupported"),
"{}",
old.describe()
);
assert!(CornerOutcome::UnsupportedPlatform
.describe()
.contains("platform=unsupported"));
}
}
/// **抓得住**:`frame_label` 把 `WS_POPUP` 与 `WS_OVERLAPPEDWINDOW` 判反(那会让日志把
/// 「我们自己的窗口」写成「普通窗口」,这行日志的全部价值就是分辨这个)。
/// **抓不住**:真窗口上到底置了哪一位(那属于运行时读数)。
#[test]
fn the_frame_label_separates_popup_from_a_decorated_window() {
assert_eq!(WS_POPUP, 0x8000_0000_u32 as isize);
assert_eq!(frame_label(WS_POPUP | 0x1000_0000), "popup");
assert_eq!(frame_label(0x00CF_0000), "overlapped");
assert_eq!(frame_label(0), "other");
}
/// 分层位标签:只认 `WS_EX_LAYERED`,别把 `WS_EX_TRANSPARENT` 之类的邻居混进来。
#[test]
fn the_layered_label_reads_the_right_bit() {
let layered = crate::platform::win32::transparency::WS_EX_LAYERED;
assert_eq!(layered_label(layered), "on");
assert_eq!(layered_label(WS_POPUP), "off");
assert_eq!(layered_label(0), "off");
}
/// 读不到句柄时摘要不许编:`frame=? layered=?`(「不知道」与「没有」是两件事)。
#[test]
fn the_style_summary_never_guesses() {
assert_eq!(style_summary(0), "frame=? layered=?");
}