sources/Google.Solutions.Mvvm/Theme/WindowsRuleSet.cs (211 lines of code) (raw):

// // Copyright 2023 Google LLC // // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. // using Google.Solutions.Common.Util; using Google.Solutions.Mvvm.Controls; using Google.Solutions.Mvvm.Drawing; using Google.Solutions.Mvvm.Interop; using Google.Solutions.Platform.Interop; using System; using System.ComponentModel; using System.Diagnostics; using System.Runtime.InteropServices; using System.Windows.Forms; #pragma warning disable CA1822 // Mark members as static namespace Google.Solutions.Mvvm.Theme { /// <summary> /// Theming rules for using dark mode. /// </summary> public class WindowsRuleSet : ControlTheme.IRuleSet { /// <summary> /// Check if this application uses dark mode. /// </summary> public bool IsDarkModeEnabled { get; } public WindowsRuleSet(bool darkMode) { Debug.Assert(!darkMode || SystemTheme.IsDarkModeSupported); this.IsDarkModeEnabled = darkMode; if (darkMode) { // // Force Win32 controls to use dark mode. // _ = NativeMethods.SetPreferredAppMode(NativeMethods.APPMODE.FORCEDARK); } } //--------------------------------------------------------------------- // Theming rules. //--------------------------------------------------------------------- /// <summary> /// Use dark title bar for top-level windows, see /// https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/apply-windows-themes /// </summary> /// <param name="form"></param> private void StyleTitleBar(Form form) { if (!this.IsDarkModeEnabled || !form.TopLevel) { return; } // // Use dark title bar, see // https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/apply-windows-themes // var darkMode = 1; var hr = NativeMethods.DwmSetWindowAttribute( form.Handle, NativeMethods.DWMWA_USE_IMMERSIVE_DARK_MODE, ref darkMode, sizeof(int)); if (hr != HRESULT.S_OK) { throw new Win32Exception( "Updating window attributes failed"); } } /// <summary> /// Opt-in window to use dark mode. /// </summary> private void StyleControl(Control control) { if (this.IsDarkModeEnabled) { NativeMethods.AllowDarkModeForWindow(control.Handle, true); } } private void StyleTreeView(TreeView treeView) { treeView.HotTracking = true; treeView.BorderStyle = BorderStyle.None; // // NB. When called after AllowDarkModeForWindow, this also applies // dark mode-style scrollbars, etc. // _ = NativeMethods.SetWindowTheme(treeView.Handle, "Explorer", null); } private void StyleListView(ListView listView) { listView.HotTracking = false; listView.BorderStyle = BorderStyle.None; // // NB. When called after AllowDarkModeForWindow, this also applies // dark mode-style scrollbars, etc. // _ = NativeMethods.SetWindowTheme(listView.Handle, "Explorer", null); var designMode = (LicenseManager.UsageMode == LicenseUsageMode.Designtime); if (this.IsDarkModeEnabled && !designMode) { // // In dark mode, we also need to apply a theme to the header, // otherwise it stays in light mode. Note that this doesn't // set the header text color correctly yet. // var headerHandle = listView.GetHeaderHandle(); Debug.Assert(headerHandle != IntPtr.Zero); NativeMethods.AllowDarkModeForWindow(headerHandle, true); _ = NativeMethods.SetWindowTheme(headerHandle, "ItemsView", null); // // Subclass the list view (not the header) to adjust the text color // in the column header. Adapted from // https://github.com/ysc3839/win32-darkmode/blob/master/win32-darkmode/ListViewUtil.h // var subclass = new SubclassCallback(listView, (ref Message m) => { switch (m.Msg) { case NativeMethods.WM_NOTIFY: var hdr = Marshal.PtrToStructure<NativeMethods.NMHDR>(m.LParam); if (hdr.code == NativeMethods.NM_CUSTOMDRAW) { var custDraw = Marshal.PtrToStructure<NativeMethods.NMCUSTOMDRAW>(m.LParam); switch (custDraw.dwDrawStage) { case NativeMethods.CDDS_PREPAINT: m.Result = new IntPtr(NativeMethods.CDRF_NOTIFYITEMDRAW); break; case NativeMethods.CDDS_ITEMPREPAINT: _ = NativeMethods.SetTextColor( custDraw.hdc, listView.ForeColor.ToCOLORREF()); m.Result = new IntPtr(NativeMethods.CDRF_DODEFAULT); break; } } else if (hdr.code == NativeMethods.HDN_ITEMCHANGEDW) { // // When resizing a column header, the list isn't redrawn // automatically. Thus, force a redraw. // listView.Invalidate(); } break; default: SubclassCallback.DefaultWndProc(ref m); break; } }); subclass.UnhandledException += (_, args) => Debug.Fail(args.FullMessage()); } } private void StyleTextBox(TextBox text) { _ = NativeMethods.SetWindowTheme(text.Handle, "Explorer", null); } private void StyleComboBox(ComboBox combo) { if (this.IsDarkModeEnabled) { _ = NativeMethods.SetWindowTheme(combo.Handle, "CFD", null); } } private void StyleScrollbar(ScrollBar bar) { if (this.IsDarkModeEnabled) { _ = NativeMethods.SetWindowTheme(bar.Handle, "Explorer", null); } } internal static void ResetWindowTheme(Control control) { _ = NativeMethods.SetWindowTheme(control.Handle, string.Empty, string.Empty); } //--------------------------------------------------------------------- // IRuleSet //--------------------------------------------------------------------- /// <summary> /// Register rules. /// </summary> public void AddRules(ControlTheme controlTheme) { controlTheme.ExpectNotNull(nameof(controlTheme)); controlTheme.AddRule<Form>( c => StyleTitleBar(c), ControlTheme.Options.ApplyWhenHandleCreated); controlTheme.AddRule<Control>( c => StyleControl(c), ControlTheme.Options.ApplyWhenHandleCreated); controlTheme.AddRule<TreeView>( c => StyleTreeView(c), ControlTheme.Options.ApplyWhenHandleCreated); controlTheme.AddRule<ListView>( c => StyleListView(c), ControlTheme.Options.ApplyWhenHandleCreated); controlTheme.AddRule<TextBox>(c => StyleTextBox(c)); controlTheme.AddRule<ComboBox>(c => StyleComboBox(c)); controlTheme.AddRule<ScrollBar>(c => StyleScrollbar(c)); } //--------------------------------------------------------------------- // P/Invoke. // // NB. Most APIs are undocumented. See // https://github.com/microsoft/WindowsAppSDK/issues/41 for details. //--------------------------------------------------------------------- private static class NativeMethods { public const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20; public const int WM_NOTIFY = 0x004E; public const int NM_CUSTOMDRAW = -12; public const int CDDS_PREPAINT = 1; public const int CDDS_ITEM = 0x10000; public const int CDDS_ITEMPREPAINT = CDDS_ITEM | CDDS_PREPAINT; public const int CDRF_NOTIFYITEMDRAW = 0x20; public const int CDRF_DODEFAULT = 0x00000000; public const int HDN_ITEMCHANGEDW = -321; [StructLayout(LayoutKind.Sequential)] public struct RECT { public int left; public int top; public int right; public int bottom; } [StructLayout(LayoutKind.Sequential)] public struct NMHDR { public IntPtr hwndFrom; public IntPtr idFrom; public int code; } [StructLayout(LayoutKind.Sequential)] public struct NMCUSTOMDRAW { public NMHDR hdr; public int dwDrawStage; public IntPtr hdc; public RECT rc; public IntPtr dwItemSpec; public int uItemState; public IntPtr lItemlParam; } public enum APPMODE : int { DEFAULT = 0, ALLOWDARK = 1, FORCEDARK = 2, FORCELIGHT = 3, MAX = 4 } [DllImport("uxtheme.dll", EntryPoint = "#133")] public static extern bool AllowDarkModeForWindow(IntPtr hWnd, bool allow); [DllImport("uxtheme.dll", EntryPoint = "#135")] public static extern int SetPreferredAppMode(APPMODE appMode); [DllImport("dwmapi.dll")] public static extern HRESULT DwmSetWindowAttribute( IntPtr hwnd, int attr, ref int attrValue, int attrSize); [DllImport("uxtheme", ExactSpelling = true, CharSet = CharSet.Unicode)] public static extern int SetWindowTheme( IntPtr hWnd, string textSubAppName, string? textSubIdList); [DllImport("gdi32.dll")] public static extern uint SetTextColor(IntPtr hdc, uint color); } } }