Skip to content

Commit 6810953

Browse files
committed
feat(launcher): add native-glass widget mode and live theme refresh
Add a "native glass" setting that switches the desktop widget between the WPF capture blur (smooth rounded corners, slight drag lag) and the system DWM blur-behind (instant while dragging, but square corners). Toggling rebuilds the widget so the two can be compared live. Fix the widget's tint and captured background not updating on an app theme switch: they are set in code rather than via DynamicResource, so add a ThemeManager.ThemeChanged event the widget listens to and re-applies on. Document both in the v1.2.7 CHANGELOG entry.
1 parent 99e1a47 commit 6810953

11 files changed

Lines changed: 119 additions & 7 deletions

File tree

docs/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@
1616
- **桌面毛玻璃小组件(类 Windows 7 桌面 gadget)**:一个浮在桌面上的毛玻璃圆角小面板,**与「快速启动」页共用同一份清单、实时同步**,点其中的项即可一键打开。可拖动标题栏移动、自动记住位置、📌 图钉「永远置顶」、✕ 关闭(并同步关闭设置开关)、排除出 Alt+Tab(纯桌面 gadget 观感)。
1717
- **毛玻璃实现(纯 WPF 方案)**:经多轮取舍后采用「截取小组件背后的屏幕 → WPF `BlurEffect` 高斯模糊 → 单层平滑圆角几何裁剪」。选此方案的原因:Win11 的 DWM 系统背景(Mica / Acrylic `DWMWA_SYSTEMBACKDROP_TYPE`)在部分机器上对无边框 WPF 窗口渲染为纯色不模糊;而能真正模糊的分层窗口(`AllowsTransparency=True` + `ACCENT_ENABLE_BLURBEHIND`)又无法被 DWM 平滑圆角(分层窗口特性冲突,`SetWindowRgn` 只能硬裁出锯齿角)。纯 WPF 渲染让**模糊与圆角合为同一层、平滑抗锯齿**,且完全可控。小组件通过 `WDA_EXCLUDEFROMCAPTURE` 排除自身,避免截图自我反馈。
1818
- 背景**透视程度**可在设置页用滑块调节(数值越大越通透,越能看到背后模糊的内容)。
19+
- **毛玻璃模式可切换(原生 vs WPF)**:设置页新增「使用原生毛玻璃(无圆角,拖动更跟手)」开关。开 = 系统原生 DWM 毛玻璃(`ACCENT_ENABLE_BLURBEHIND`,拖动时背景即时跟随、零延迟,但四角为直角);关(默认)= WPF 截图模糊(平滑圆角,拖动时背景略有延迟)。切换即实时重建小组件,便于对比。
1920
- **设置页新增三项(开机与托盘卡片)**:① 启动时自动打开「快速启动」页(默认开启);② 在桌面显示快速启动小组件(默认关闭);③ 背景透视程度滑块。「快速启动」页也新增「打开桌面组件」按钮(已打开时禁用并显示「已打开」)。
2021
- 三语(中 / 英 / 德)文案与导航项同步补齐。
2122

2223
### 修复
2324
- **深色主题下快速启动项标题看不清**:卡片标题的内联 `Style`(用于「完成」时加删除线)没有 `BasedOn`,覆盖掉了主题默认前景色使其回退为黑色;补上 `Foreground="{DynamicResource TextPrimary}"` 后深 / 浅主题均清晰。
2425
- **左侧导航「快速启动 / 日志」图标错位**:新增导航项时的文本替换误将「日志」的 Segoe MDL2 图标(U+E7C3)挪给了「快速启动」、而「日志」变空;已改为「快速启动」= U+E8A7(OpenInNewWindow)、「日志」= U+E7C3。
26+
- **切换应用主题时小组件背景不实时刷新**:小组件的 tint 与截图背景是代码设置的(非 `DynamicResource`),主题切换时不会自动更新(需改动透明度才顺带刷新)。新增 `ThemeManager.ThemeChanged` 事件,主题切换后小组件立即重算 tint 并重新截图。
2527

2628
### 调整
2729
- 无。

src/WinDeploy.App/MainWindow.xaml.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public MainWindow()
2828
// Desktop widget: react to the setting live, and show it on startup if enabled.
2929
vm.Settings.ShowDesktopWidgetChanged += SetDesktopWidget;
3030
vm.Settings.WidgetOpacityChanged += _ => _widget?.ApplyTint();
31+
vm.Settings.WidgetGlassModeChanged += RecreateDesktopWidget;
3132
Loaded += (_, _) => { if (SettingsStore.Load().ShowDesktopWidget) ShowDesktopWidget(); };
3233
// Returning to the app while a device keeps overheating → show the advanced ignore/adjust prompt.
3334
Activated += (_, _) => (DataContext as MainViewModel)?.ShowOverheatPromptIfPending();
@@ -90,6 +91,15 @@ private void ShowDesktopWidget()
9091
_widget.Show();
9192
}
9293

94+
/// <summary>The glass mode (native vs WPF) is chosen when the widget window is built, so switching it rebuilds
95+
/// the widget: close the current one and reopen it under the new mode (only if it should be visible).</summary>
96+
private void RecreateDesktopWidget()
97+
{
98+
_widget?.Close();
99+
_widget = null;
100+
if (SettingsStore.Load().ShowDesktopWidget) ShowDesktopWidget();
101+
}
102+
93103
/// <summary>Close-button behavior: ask (default) → prompt; tray → minimize to tray; exit → really quit.</summary>
94104
private void OnClosing(object? sender, CancelEventArgs e)
95105
{

src/WinDeploy.App/Services/Infra/ScreenBlur.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,41 @@ public static class ScreenBlur
2222
[DllImport("gdi32.dll")]
2323
private static extern bool DeleteObject(IntPtr hObject);
2424

25+
// ── native DWM blur-behind (the alternative "instant" glass, no WPF capture) ──
26+
private enum AccentState { Disabled = 0, EnableBlurBehind = 3 }
27+
28+
[StructLayout(LayoutKind.Sequential)]
29+
private struct AccentPolicy { public AccentState AccentState; public int Flags; public uint GradientColor; public int AnimationId; }
30+
31+
[StructLayout(LayoutKind.Sequential)]
32+
private struct WindowCompositionAttributeData { public int Attribute; public IntPtr Data; public int SizeOfData; }
33+
34+
[DllImport("user32.dll")]
35+
private static extern int SetWindowCompositionAttribute(IntPtr hwnd, ref WindowCompositionAttributeData data);
36+
37+
/// <summary>Turn on the native DWM blur-behind (Aero-style gaussian blur of what's behind the window). This is
38+
/// instant (no per-move screen grab) but the window can't be rounded smoothly while it's per-pixel transparent,
39+
/// so it reads as a square panel. Used by the widget's "native glass" comparison mode.</summary>
40+
public static void EnableBlurBehind(Window w)
41+
{
42+
try
43+
{
44+
var hwnd = new WindowInteropHelper(w).EnsureHandle();
45+
if (hwnd == IntPtr.Zero) return;
46+
var accent = new AccentPolicy { AccentState = AccentState.EnableBlurBehind };
47+
var size = Marshal.SizeOf(accent);
48+
var ptr = Marshal.AllocHGlobal(size);
49+
try
50+
{
51+
Marshal.StructureToPtr(accent, ptr, false);
52+
var data = new WindowCompositionAttributeData { Attribute = 19, Data = ptr, SizeOfData = size };
53+
SetWindowCompositionAttribute(hwnd, ref data);
54+
}
55+
finally { Marshal.FreeHGlobal(ptr); }
56+
}
57+
catch { /* blur unavailable — the tint panel remains */ }
58+
}
59+
2560
/// <summary>Make the window invisible to screen capture, so <see cref="Capture"/> grabs the content behind it.</summary>
2661
public static void ExcludeFromCapture(Window w)
2762
{

src/WinDeploy.App/Services/Infra/SettingsStore.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ public sealed class AppSettings
3939
public bool WidgetPinned { get; set; }
4040
/// <summary>小组件背景填充不透明度 0.1–0.85(越低越通透)。默认 0.4(透视程度 60%)。</summary>
4141
public double WidgetOpacity { get; set; } = 0.4;
42+
/// <summary>小组件使用原生 DWM 毛玻璃(即时、无圆角)而非 WPF 截图模糊(平滑圆角、拖动略延迟)。默认 false。</summary>
43+
public bool WidgetNativeGlass { get; set; }
4244

4345
// ── 硬件温度监控(后台定时检测 CPU / GPU / NVMe 硬盘,超阈值时通知 + 可选 TTS 语音)──────────
4446
/// <summary>启用硬件温度监控。默认关闭(opt-in)。</summary>

src/WinDeploy.App/Services/Infra/ThemeManager.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ private static readonly (string Key, string Light, string Dark)[] Palette =
4646
/// Read by windows that pick native effects (e.g. the desktop widget's glass tint) rather than brushes.</summary>
4747
public static bool IsDark => _dark;
4848

49+
/// <summary>Raised after the palette is swapped, so windows that pick colours in code (not via DynamicResource)
50+
/// — e.g. the desktop widget's glass tint — can refresh live instead of waiting for the next redraw.</summary>
51+
public static event Action? ThemeChanged;
52+
4953
public static void Apply(ThemeMode mode)
5054
{
5155
var res = Application.Current?.Resources;
@@ -62,6 +66,8 @@ public static void Apply(ThemeMode mode)
6266
// Recolor every open window's native title bar to match.
6367
if (Application.Current?.Windows is { } windows)
6468
foreach (Window w in windows) ApplyTitleBar(w);
69+
70+
ThemeChanged?.Invoke();
6571
}
6672

6773
/// <summary>Flip a single window's native title bar to the current theme. Safe to call any time;

src/WinDeploy.App/ViewModels/Shell/SettingsViewModel.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public SettingsViewModel()
3030
_showLauncherOnStartup = _s.ShowLauncherOnStartup;
3131
_showDesktopWidget = _s.ShowDesktopWidget;
3232
_widgetOpacity = _s.WidgetOpacity;
33+
_widgetNativeGlass = _s.WidgetNativeGlass;
3334
_tempMonitorEnabled = _s.TempMonitorEnabled;
3435
_tempTts = _s.TempTtsEnabled;
3536
_tempCpu = _s.TempCpuEnabled; _tempGpu = _s.TempGpuEnabled; _tempDisk = _s.TempDiskEnabled;
@@ -296,6 +297,24 @@ public double WidgetSeeThrough
296297
/// <summary>调整小组件透视程度时触发,让桌面小组件实时更新背景。</summary>
297298
public event Action<double>? WidgetOpacityChanged;
298299

300+
private bool _widgetNativeGlass;
301+
/// <summary>小组件毛玻璃:原生 DWM(即时、无圆角)vs WPF 截图模糊(平滑圆角)。即时生效(重建小组件)并持久化。</summary>
302+
public bool WidgetNativeGlass
303+
{
304+
get => _widgetNativeGlass;
305+
set
306+
{
307+
if (!Set(ref _widgetNativeGlass, value)) return;
308+
_s.WidgetNativeGlass = value;
309+
SettingsStore.Save(_s);
310+
AuditLog.Action($"小组件毛玻璃模式:{(value ? "原生 DWM(无圆角)" : "WPF 截图模糊(圆角)")}");
311+
WidgetGlassModeChanged?.Invoke();
312+
}
313+
}
314+
315+
/// <summary>切换小组件毛玻璃模式时触发,让主窗口按新模式重建桌面小组件。</summary>
316+
public event Action? WidgetGlassModeChanged;
317+
299318
// ── 硬件温度监控(即时生效并持久化)────────────────────────────────────
300319
public RelayCommand TestTtsCommand { get; }
301320

src/WinDeploy.App/Views/Launch/LauncherWidgetWindow.xaml.cs

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,15 @@ public partial class LauncherWidgetWindow : Window
2626
private bool _placed; // suppress position saves until initial placement is done
2727
private readonly DispatcherTimer _settle; // coalesce rapid moves/resizes into one capture
2828
private readonly DispatcherTimer _refresh; // periodic re-capture so the glass stays fresh while shown
29+
// Glass mode chosen at build time: true = native DWM blur-behind (instant, square); false = WPF capture blur
30+
// (smooth rounded, slight drag lag). Switching the setting rebuilds the window (MainWindow.RecreateDesktopWidget).
31+
private readonly bool _native;
2932

3033
public LauncherWidgetWindow(LaunchCenterViewModel vm)
3134
{
3235
InitializeComponent();
3336
DataContext = vm;
37+
_native = SettingsStore.Load().WidgetNativeGlass;
3438

3539
_settle = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(120) };
3640
_settle.Tick += (_, _) => { _settle.Stop(); CaptureBackground(); };
@@ -47,17 +51,41 @@ public LauncherWidgetWindow(LaunchCenterViewModel vm)
4751

4852
SourceInitialized += (_, _) =>
4953
{
50-
ScreenBlur.ExcludeFromCapture(this); // so we grab what's BEHIND us, not ourselves
5154
HideFromAltTab();
55+
if (_native) ScreenBlur.EnableBlurBehind(this); // native: instant DWM blur, square corners
56+
else ScreenBlur.ExcludeFromCapture(this); // wpf: grab what's BEHIND us, not ourselves
5257
};
53-
Loaded += (_, _) => { PlaceFromSettingsOrDefault(); _placed = true; UpdateClip(); CaptureBackground(); };
54-
SizeChanged += (_, _) => { UpdateClip(); ScheduleCapture(); };
55-
LocationChanged += (_, _) => { SavePosition(); ScheduleCapture(); };
58+
Loaded += (_, _) =>
59+
{
60+
PlaceFromSettingsOrDefault();
61+
_placed = true;
62+
if (_native) RootGrid.Clip = null; // native mode is square (no rounded clip)
63+
else { UpdateClip(); CaptureBackground(); }
64+
};
65+
SizeChanged += (_, _) => { if (_native) return; UpdateClip(); ScheduleCapture(); };
66+
LocationChanged += (_, _) => { SavePosition(); if (!_native) ScheduleCapture(); };
5667
IsVisibleChanged += (_, e) =>
5768
{
58-
if (e.NewValue is true) { ApplyTint(); CaptureBackground(); _refresh.Start(); }
59-
else _refresh.Stop();
69+
if (e.NewValue is true)
70+
{
71+
ApplyTint();
72+
if (_native) ScreenBlur.EnableBlurBehind(this);
73+
else { CaptureBackground(); _refresh.Start(); }
74+
}
75+
else if (!_native) _refresh.Stop();
6076
};
77+
78+
// Live-refresh the code-set tint (and the WPF blur capture) when the app theme changes — DynamicResource
79+
// brushes update themselves, but the tint fill and captured background are set in code, so re-apply here.
80+
ThemeManager.ThemeChanged += OnThemeChanged;
81+
Closed += (_, _) => ThemeManager.ThemeChanged -= OnThemeChanged;
82+
}
83+
84+
private void OnThemeChanged()
85+
{
86+
ApplyTint();
87+
if (_native) ScreenBlur.EnableBlurBehind(this);
88+
else CaptureBackground();
6189
}
6290

6391
/// <summary>Update the rounded clip that shapes every layer to the current window size.</summary>
@@ -71,7 +99,7 @@ public LauncherWidgetWindow(LaunchCenterViewModel vm)
7199
/// to the edges.</summary>
72100
private void CaptureBackground()
73101
{
74-
if (!IsVisible || ActualWidth <= 0 || ActualHeight <= 0) return;
102+
if (_native || !IsVisible || ActualWidth <= 0 || ActualHeight <= 0) return;
75103
var ps = PresentationSource.FromVisual(this);
76104
if (ps?.CompositionTarget == null) return;
77105

src/WinDeploy.App/Views/Shell/SettingsView.xaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@
140140
</Grid>
141141
<TextBlock FontSize="11" Foreground="{DynamicResource TextTertiary}" TextWrapping="Wrap" Margin="0,4,0,0"
142142
Text="{DynamicResource S.settings.widget.opacityHint}" />
143+
<CheckBox Content="{DynamicResource S.settings.widget.native}" IsChecked="{Binding WidgetNativeGlass}" FontSize="13" Margin="0,12,0,0"
144+
IsEnabled="{Binding ShowDesktopWidget}" />
145+
<TextBlock Text="{DynamicResource S.settings.widget.nativeDesc}" FontSize="12"
146+
Foreground="{DynamicResource TextSecondary}" TextWrapping="Wrap" Margin="0,4,0,0" />
143147
</StackPanel>
144148
</Border>
145149

src/WinDeploy.Core/I18n/Resources/de/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
"settings.widget.showDesc": "Ein Widget mit Milchglas-Optik und runden Ecken auf dem Desktop anzeigen (im Stil der Windows-7-Minianwendungen); ein Klick auf einen Eintrag öffnet ihn direkt. Verschiebbar, merkt sich die Position.",
4444
"settings.widget.opacity": "Hintergrund-Durchsicht",
4545
"settings.widget.opacityHint": "Höher = durchsichtiger (transparenter, stärkerer Milchglaseffekt); niedriger = solider.",
46+
"settings.widget.native": "Natives Milchglas verwenden (keine runden Ecken, flüssigeres Ziehen)",
47+
"settings.widget.nativeDesc": "Wechselt zum nativen DWM-Weichzeichner des Systems: der Hintergrund folgt beim Ziehen sofort ohne Verzögerung, die Ecken sind jedoch eckig (kein weiches Abrunden). Aus = WPF-Aufnahme-Weichzeichner: weiche runde Ecken, aber der Hintergrund hinkt beim Ziehen leicht nach.",
4648
"settings.autostart.fail": "Autostart konnte nicht gesetzt werden: {0}",
4749
"settings.tempmon.title": "Hardware-Temperaturüberwachung",
4850
"settings.tempmon.desc": "Prüft im Hintergrund alle 20 s die Temperatur von CPU / GPU / NVMe-Datenträger; bei Überschreiten eines Schwellwerts erscheint eine Systembenachrichtigung und optional eine Sprachwarnung (TTS).",

src/WinDeploy.Core/I18n/Resources/en/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
"settings.widget.showDesc": "Float a frosted-glass, rounded widget on the desktop (Windows 7 gadget style); click an item to open it in one click. Draggable, remembers its position.",
4444
"settings.widget.opacity": "Background see-through",
4545
"settings.widget.opacityHint": "Higher = more see-through (more transparent, stronger frosted-glass); lower = more solid.",
46+
"settings.widget.native": "Use native glass (no rounded corners, snappier drag)",
47+
"settings.widget.nativeDesc": "Switch to the system's native DWM blur: the background follows instantly while dragging with no lag, but the corners are square (can't be rounded smoothly). Off = WPF capture blur: smooth rounded corners, but the background lags slightly while dragging.",
4648
"settings.autostart.fail": "Failed to set launch at startup: {0}",
4749
"settings.tempmon.title": "Hardware temperature monitor",
4850
"settings.tempmon.desc": "Checks CPU / GPU / NVMe disk temperatures every 20s in the background; on exceeding a threshold it raises a system notification and can speak a voice alert (TTS).",

0 commit comments

Comments
 (0)