Skip to content

Commit

Permalink
#1: How to make the the title bar not too dark [custom Windows 11 col…
Browse files Browse the repository at this point in the history
…ors]
  • Loading branch information
Aldaviva committed Jul 31, 2023
1 parent 60cefc6 commit 5db1be2
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 26 deletions.
Binary file added .github/images/demo-wpf-customcolors.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
82 changes: 67 additions & 15 deletions DarkNet/DarkNet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Concurrent;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Runtime.InteropServices;
using System.Security;
Expand Down Expand Up @@ -34,12 +35,24 @@ public class DarkNet: IDarkNet {
/// </summary>
public static IDarkNet Instance => LazyInstance.Value;

/// <summary>
/// Mapping from a window handle to the most recently set <see cref="Theme"/> for that window, used to correctly reapply themes when a parent theme (process or OS) changes.
/// </summary>
protected readonly ConcurrentDictionary<IntPtr, Theme> PreferredWindowModes = new();

private bool? _userDefaultAppThemeIsDark;
private bool? _userTaskbarThemeIsDark;
/// <summary>
/// The most recently set theme for this process, used to correctly reapply themes to this process's windows when the user changes their OS settings.
/// </summary>
protected Theme? PreferredAppTheme;
protected bool? EffectiveProcessThemeIsDark;

/// <summary>
/// Most recent value for whether the process's theme is dark, after taking into account high contrast mode. Null if never set. Used to back the <see cref="EffectiveCurrentProcessThemeIsDark"/> property and fire <see cref="EffectiveCurrentProcessThemeIsDarkChanged"/> events.
/// </summary>
protected bool? EffectiveProcessThemeIsDark;

private bool? _userDefaultAppThemeIsDark;
private bool? _userTaskbarThemeIsDark;
private ThemeOptions? _processThemeOptions;

private volatile int _processThemeChanged; // int instead of bool to support Interlocked atomic operations

Expand All @@ -63,7 +76,7 @@ public DarkNet() {
}

/// <inheritdoc />
public virtual void SetCurrentProcessTheme(Theme theme) {
public virtual void SetCurrentProcessTheme(Theme theme, ThemeOptions? options = null) {
_processThemeChanged = 1;

try {
Expand All @@ -83,7 +96,8 @@ public virtual void SetCurrentProcessTheme(Theme theme) {
}
}

PreferredAppTheme = theme;
PreferredAppTheme = theme;
_processThemeOptions = options;

RefreshTitleBarThemeColor();

Expand All @@ -99,21 +113,21 @@ public virtual void SetCurrentProcessTheme(Theme theme) {

/// <inheritdoc />
// Not overloading one method to cover both WPF and Forms so that consumers don't have to add references to both PresentationFramework and System.Windows.Forms just to use one overloaded variant
public virtual void SetWindowThemeWpf(Window window, Theme theme) {
public virtual void SetWindowThemeWpf(Window window, Theme theme, ThemeOptions? options = null) {
bool isWindowInitialized = PresentationSource.FromVisual(window) != null;
if (!isWindowInitialized) {
ImplicitlySetProcessThemeIfFirstCall(theme);
window.SourceInitialized += OnSourceInitialized;

void OnSourceInitialized(object? _, EventArgs eventArgs) {
window.SourceInitialized -= OnSourceInitialized;
SetWindowThemeWpf(window, theme);
SetWindowThemeWpf(window, theme, options);
}
} else {
IntPtr windowHandle = new WindowInteropHelper(window).Handle;

try {
SetModeForWindow(windowHandle, theme);
SetModeForWindow(windowHandle, theme, options);
} catch (DarkNetException.LifecycleException) {
throw new InvalidOperationException($"Called {nameof(SetWindowThemeWpf)}() too late, call it in OnSourceInitialized or the Window subclass's constructor");
}
Expand All @@ -128,9 +142,9 @@ void OnClosing(object _, CancelEventArgs args) {
}

/// <inheritdoc />
public virtual void SetWindowThemeForms(Form window, Theme theme) {
public virtual void SetWindowThemeForms(Form window, Theme theme, ThemeOptions? options = null) {
try {
SetModeForWindow(window.Handle, theme);
SetModeForWindow(window.Handle, theme, options);
} catch (DarkNetException.LifecycleException) {
throw new InvalidOperationException($"Called {nameof(SetWindowThemeForms)}() too late, call it before Form.Show() or Application.Run(), and after " +
$"{nameof(IDarkNet)}.{nameof(SetCurrentProcessTheme)}()");
Expand All @@ -145,9 +159,9 @@ void OnClosing(object _, CancelEventArgs args) {
}

/// <inheritdoc />
public virtual void SetWindowThemeRaw(IntPtr windowHandle, Theme theme) {
public virtual void SetWindowThemeRaw(IntPtr windowHandle, Theme theme, ThemeOptions? options = null) {
try {
SetModeForWindow(windowHandle, theme);
SetModeForWindow(windowHandle, theme, options);
} catch (DarkNetException.LifecycleException) {
throw new InvalidOperationException($"Called {nameof(SetWindowThemeRaw)}() too late, call it before the window is visible.");
}
Expand All @@ -165,7 +179,7 @@ private void ImplicitlySetProcessThemeIfFirstCall(Theme theme) {
/// <para>if GetWindowInfo().style.WS_VISIBLE == true then it was called too late</para>
/// </summary>
/// <exception cref="DarkNetException.LifecycleException">if it is called too late</exception>
protected virtual void SetModeForWindow(IntPtr windowHandle, Theme windowTheme) {
protected virtual void SetModeForWindow(IntPtr windowHandle, Theme windowTheme, ThemeOptions? options = null) {
ImplicitlySetProcessThemeIfFirstCall(windowTheme);

bool isFirstRunForWindow = true;
Expand All @@ -179,9 +193,13 @@ protected virtual void SetModeForWindow(IntPtr windowHandle, Theme windowTheme)
}

Win32.AllowDarkModeForWindow(windowHandle, windowTheme != Theme.Light);
RefreshTitleBarThemeColor(windowHandle);
RefreshTitleBarThemeColor(windowHandle, options);
}

/// <summary>
/// Fired when a WPF or Forms window is about to close, so that we can release the entry in the <see cref="PreferredWindowModes"/> map and free its memory.
/// </summary>
/// <param name="windowHandle"></param>
protected void OnWindowClosing(IntPtr windowHandle) {
PreferredWindowModes.TryRemove(windowHandle, out _);
}
Expand All @@ -198,7 +216,12 @@ private void RefreshTitleBarThemeColor() {
}
}

protected virtual void RefreshTitleBarThemeColor(IntPtr windowHandle) {
/// <summary>
/// Apply all of the theme fallback/override logic and call the OS methods to apply the window theme. Handles the window theme, app theme, OS theme, high contrast, different Windows versions, Windows 11 colors, repainting visible windows, and updating context menus.
/// </summary>
/// <param name="windowHandle">A pointer to the window to update</param>
/// <param name="options">Windows 11 DWM color overrides</param>
protected virtual void RefreshTitleBarThemeColor(IntPtr windowHandle, ThemeOptions? options = null) {
if (!PreferredWindowModes.TryGetValue(windowHandle, out Theme windowTheme)) {
windowTheme = Theme.Auto;
}
Expand Down Expand Up @@ -244,6 +267,18 @@ protected virtual void RefreshTitleBarThemeColor(IntPtr windowHandle) {
Marshal.FreeHGlobal(attributeValueBuffer);
}

if ((options?.TitleBarBackgroundColor ?? _processThemeOptions?.TitleBarBackgroundColor) is { } titleBarBackgroundColor) {
SetDwmWindowColor(windowHandle, DwmWindowAttribute.DwmwaCaptionColor, titleBarBackgroundColor);
}

if ((options?.TitleBarTextColor ?? _processThemeOptions?.TitleBarTextColor) is { } titleBarTextColor) {
SetDwmWindowColor(windowHandle, DwmWindowAttribute.DwmwaTextColor, titleBarTextColor);
}

if ((options?.WindowBorderColor ?? _processThemeOptions?.WindowBorderColor) is { } windowBorderColor) {
SetDwmWindowColor(windowHandle, DwmWindowAttribute.DwmwaBorderColor, windowBorderColor);
}

/*
* Needed for subsequent (after the window has already been shown) theme changes, otherwise the title bar will only update after you later hide, blur, or resize the window.
* Not needed when changing the theme for the first time, before the window has ever been shown.
Expand All @@ -261,6 +296,23 @@ protected virtual void RefreshTitleBarThemeColor(IntPtr windowHandle) {
Win32.FlushMenuThemes();
}

// Windows 11 and later
private static int SetDwmWindowColor(IntPtr windowHandle, DwmWindowAttribute attribute, Color color) {
int attributeValueBufferSize = Marshal.SizeOf<ColorRef>();
IntPtr attributeValueBuffer = Marshal.AllocHGlobal(attributeValueBufferSize);
ColorRef colorRef = new(color, ThemeOptions.DefaultColor.Equals(color) || (attribute == DwmWindowAttribute.DwmwaBorderColor && ThemeOptions.NoWindowBorder.Equals(color)));

Marshal.StructureToPtr(colorRef, attributeValueBuffer, false);
try {
return Win32.DwmSetWindowAttribute(windowHandle, attribute, attributeValueBuffer, attributeValueBufferSize);
} catch (Exception e) when (e is not OutOfMemoryException) {
Trace.TraceInformation("Failed to set custom title bar color for window: {0}", e.Message);
return 1;
} finally {
Marshal.FreeHGlobal(attributeValueBuffer);
}
}

/// <inheritdoc />
public virtual bool UserDefaultAppThemeIsDark {
get {
Expand Down
2 changes: 1 addition & 1 deletion DarkNet/DarkNet.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<PropertyGroup>
<TargetFrameworks>netcoreapp3.1;net452</TargetFrameworks>
<Version>2.2.0</Version>
<Version>2.3.0</Version>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
Expand Down
12 changes: 8 additions & 4 deletions DarkNet/IDarkNet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ public interface IDarkNet: IDisposable {
/// <para>This method doesn't actually make your title bars dark. It defines the default theme to use if you set a window's theme to <see cref="Theme.Auto"/> using <see cref="SetWindowThemeWpf" />/<see cref="SetWindowThemeForms"/>.</para>
/// </summary>
/// <param name="theme">The theme that windows of your process should use. This theme overrides the user's settings and is overridden by the window theme you set later, unless you set the theme to <see cref="Theme.Auto"/>, in which case it inherits from the user's settings.</param>
void SetCurrentProcessTheme(Theme theme);
/// <param name="options">Optional extra parameters that can override the colors in the non-client areas for all of this process's windows. May also be specified on a per-window basis with the SetWindowTheme*() methods. Only affects Windows 11 and later.</param>
void SetCurrentProcessTheme(Theme theme, ThemeOptions? options = null);

/// <summary>
/// <para>Turn on dark mode for a window.</para>
Expand All @@ -39,8 +40,9 @@ public interface IDarkNet: IDisposable {
/// <remarks>The correct time to call this method is when the window has already been constructed, it has an HWND, but it has not yet been shown (i.e. its Win32 window style must not be visible yet). You can call this directly after the call to <c>Window.InitializeComponent</c> in the Window's constructor. Alternatively, a handler for the <see cref="Window.SourceInitialized" /> event will be fired at the correct point in the window lifecycle to call this method.</remarks>
/// <param name="window">A WPF window which has been constructed and is being SourceInitialized, but has not yet been shown.</param>
/// <param name="theme">The theme to use for this window. Can be <see cref="Theme.Auto"/> to inherit from the app (defined by the theme passed to <see cref="SetCurrentProcessTheme"/>), or from the user's default app settings if you also set the app to <see cref="Theme.Auto"/> (defined in Settings › Personalization › Colors).</param>
/// <param name="options">Optional extra parameters that can override the colors in the non-client area of this window. May also be specified on a per-process basis with <see cref="SetCurrentProcessTheme"/>. Only affects Windows 11 and later.</param>
/// <exception cref="InvalidOperationException">If this method was called too early (such as right after the Window constructor), or too late (such as after <see cref="Window.Show" /> returns).</exception>
void SetWindowThemeWpf(Window window, Theme theme);
void SetWindowThemeWpf(Window window, Theme theme, ThemeOptions? options = null);

/// <summary>
/// <para>Turn on dark mode for a window.</para>
Expand All @@ -50,8 +52,9 @@ public interface IDarkNet: IDisposable {
/// <remarks>The correct time to call this method is when the window has already been constructed, but it has not yet been shown (i.e. its Win32 window style must not be visible yet). You can call this after the <see cref="Form"/> constructor returns, but before <see cref="Control.Show" />.</remarks>
/// <param name="window">A Windows Forms window which has been constructed but has not yet been shown.</param>
/// <param name="theme">The theme to use for this window. Can be <see cref="Theme.Auto"/> to inherit from the app (defined by the theme passed to <see cref="SetCurrentProcessTheme"/>), or from the user's default app settings if you also set the app to <see cref="Theme.Auto"/> (defined in Settings › Personalization › Colors).</param>
/// /// <param name="options">Optional extra parameters that can override the colors in the non-client area of this window. May also be specified on a per-process basis with <see cref="SetCurrentProcessTheme"/>. Only affects Windows 11 and later.</param>
/// <exception cref="InvalidOperationException">If this method was called too late (such as after calling <see cref="Control.Show" /> returns).</exception>
void SetWindowThemeForms(Form window, Theme theme);
void SetWindowThemeForms(Form window, Theme theme, ThemeOptions? options = null);

/// <summary>
/// <para>Turn on dark mode for a window.</para>
Expand All @@ -62,8 +65,9 @@ public interface IDarkNet: IDisposable {
/// <remarks>The correct time to call this method is when the window has already been constructed, but it has not yet been shown (i.e. its Win32 window style must not be visible yet).</remarks>
/// <param name="windowHandle">A <c>HWND</c> handle to a Win32 window, which has been constructed but has not yet been shown.</param>
/// <param name="theme">The theme to use for this window. Can be <see cref="Theme.Auto"/> to inherit from the app (defined by the theme passed to <see cref="SetCurrentProcessTheme"/>), or from the user's default app settings if you also set the app to <see cref="Theme.Auto"/> (defined in Settings › Personalization › Colors).</param>
/// /// <param name="options">Optional extra parameters that can override the colors in the non-client area of this window. May also be specified on a per-process basis with <see cref="SetCurrentProcessTheme"/>. Only affects Windows 11 and later.</param>
/// <exception cref="InvalidOperationException">If this method was called too late.</exception>
void SetWindowThemeRaw(IntPtr windowHandle, Theme theme);
void SetWindowThemeRaw(IntPtr windowHandle, Theme theme, ThemeOptions? options = null);

/// <summary>
/// <para>Whether windows which follow the user's default operating system theme, such as Windows Explorer, Command Prompt, and Settings, will use dark mode in their title bars, context menus, and other themed areas. Also known as "app mode" or "default app mode".</para>
Expand Down
45 changes: 45 additions & 0 deletions DarkNet/ThemeOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System.Drawing;

namespace Dark.Net;

/// <summary>
/// Extra parameters that override the non-client area colors of a window in Windows 11 or later. On earlier versions of Windows, these have no effect.
/// </summary>
public class ThemeOptions {

/// <summary>
/// <para>Override the background color of the title bar in Windows 11 or later.</para>
/// <para>If <see langword="null"/>, the title bar color will be left unchanged, although it will still be affected by the chosen <see cref="Theme"/> and the previous value of this property.</para>
/// <para>If you previously set this property and want to undo it, setting this to <see cref="DefaultColor"/> will revert it to the standard OS color for your chosen <see cref="Theme"/>.</para>
/// <para>Setting this property has no effect on Windows 10 and earlier versions.</para>
/// </summary>
public Color? TitleBarBackgroundColor { get; set; }

/// <summary>
/// <para>Override the text color of the title bar in Windows 11 or later. Does not affect the minimize, maximize, or close buttons, just the caption text.</para>
/// <para>If <see langword="null"/>, the title bar will be left unchanged, although it will still be affected by the chosen <see cref="Theme"/> and the previous value of this property.</para>
/// <para>If you previously set this property and want to undo it, setting this to <see cref="DefaultColor"/> will revert it to the standard OS color for your chosen <see cref="Theme"/>.</para>
/// <para>Setting this property has no effect on Windows 10 and earlier versions.</para>
/// </summary>
public Color? TitleBarTextColor { get; set; }

/// <summary>
/// <para>Override the border color of the window in Windows 11 or later. The border goes all the way around the entire window, not just around the title bar.</para>
/// <para>To remove the window border entirely, set this to <see cref="NoWindowBorder"/>.</para>
/// <para>If <see langword="null"/>, the window's border color will be left unchanged, although it will still be affected by the chosen <see cref="Theme"/> and the previous value of this property.</para>
/// <para>If you previously set this property and want to undo it, setting this to <see cref="DefaultColor"/> will revert it to the standard OS color for your chosen <see cref="Theme"/>.</para>
/// <para>Setting this property has no effect on Windows 10 and earlier versions.</para>
/// </summary>
public Color? WindowBorderColor { get; set; }

/// <summary>
/// When set as the value of <see cref="WindowBorderColor"/>, removes the window border. Windows 11 or later only.
/// </summary>
public static readonly Color NoWindowBorder = Color.FromArgb(0xFF, 0xFE, 0xFF, 0xFF);

/// <summary>
/// When set as the value of <see cref="TitleBarTextColor"/>, <see cref="TitleBarBackgroundColor"/>, or <see cref="WindowBorderColor"/>, reverts the color to the standard Os light or dark color for the active <see cref="Theme"/>. Useful if you previously set a custom color, and then want to reset it.
/// </summary>
public static readonly Color DefaultColor = Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF);

}
Loading

0 comments on commit 5db1be2

Please sign in to comment.