Skip to content

Commit

Permalink
Updated documentation to reflect SetCurrentProcessTheme being optiona…
Browse files Browse the repository at this point in the history
…l instead of mandatory now. Added package icon.
  • Loading branch information
Aldaviva committed Sep 24, 2022
1 parent 7d5ab89 commit c172c49
Show file tree
Hide file tree
Showing 8 changed files with 58 additions and 73 deletions.
Binary file added DarkNet/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 16 additions & 29 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ Enable native Windows dark mode for your WPF and Windows Forms title bars.
- Windows
- Windows 10 version 1809 (October 2018 Update) or later
- Windows 11 or later
- You can also run your app on earlier Windows versions as well, but the title bar won't turn dark.
- Windows Presentation Foundation or Windows Forms
- You can still run your program on earlier Windows versions as well, but the title bar won't turn dark.
- Windows Presentation Foundation, Windows Forms, or access to the native window handle of other windows in your process

<a id="installation"></a>
## Installation

[DarkNet is available on NuGet Gallery](https://www.nuget.org/packages/DarkNet/).
[DarkNet is available in NuGet Gallery.](https://www.nuget.org/packages/DarkNet/)

```ps1
dotnet add package DarkNet
Expand All @@ -55,18 +55,16 @@ Install-Package DarkNet

#### Entry point

The top-level interface of this library is <code>Dark.Net.<strong>IDarkNet</strong></code>, which is implemented by the **`DarkNet`** class. A shared instance of this class is available from **`DarkNet.Instance`**, or you can construct a new instance with `new DarkNet()`.
The top-level interface of this library is **`Dark.Net.IDarkNet`**, which is implemented by the **`DarkNet`** class. A shared instance of this class is available from **`DarkNet.Instance`**, or you can construct a new instance with `new DarkNet()`.

#### Methods

You must call **both** of the methods below to make a window use the dark theme. See the following sections for specific examples.

1. First, call **`SetCurrentProcessTheme()`** to allow your process to change the themes of its windows, although it doesn't apply any themes on its own.
1. First, you may optionally call **`SetCurrentProcessTheme(Theme)`** to define a default theme for your windows, although it doesn't actually apply the theme to any windows on its own.

If you don't call this method, all windows in your application will use the light theme, even if you call `SetWindowTheme*`.
2. Next, call **`SetWindowTheme*()`** to actually apply the theme to each window. There are 3 methods to handle WPF, Forms, and raw `HWND` handles.
If you don't call this method, any window on which you call `SetWindowTheme*(myWindow, Theme.Auto)` will inherit its theme from the operating system's default app theme, skipping this app-level default.
2. Next, you must call one of the **`SetWindowTheme*(Window, Theme)`** methods to actually apply a theme to each window. There are 3 methods to handle WPF, Forms, and raw `HWND` handles.

If you don't call one of these methods on a given window, that window will use the light theme, even if you called `SetCurrentProcessTheme`.
If you don't call one of these methods on a given window, that window will always use the light theme, even if you called `SetCurrentProcessTheme` and set the OS default app mode to dark.

#### Themes

Expand All @@ -85,12 +83,9 @@ Try the [demo apps](#demos) to see this behavior in action.
<a id="wpf"></a>
### WPF

You must do both of the following steps.

<a id="on-application-startup"></a>
#### On application startup

Before showing **any** windows in your application, you must call
Before showing **any** windows in your application, you may optionally call
```cs
IDarkNet.SetCurrentProcessTheme(theme);
```
Expand All @@ -112,18 +107,17 @@ public partial class App: Application {
}
```

<a id="before-showing-a-new-window"></a>
#### Before showing a new window

Before showing each window in your application, you have to set the theme for that window.
Before showing **each** window in your application, you have to set the theme for that window.

```cs
IDarkNet.SetWindowThemeWpf(window, theme);
```

A good place to call this is in the window's constructor **after the call to `InitializeComponent`**, or in an event handler for the window's **`SourceInitialized`** event.

If you call it too late (such as after the window is shown), the calls will have no effect on Windows.
If you call it too late (such as after the window is shown), the calls will have no effect.

```cs
// MainWindow.xaml.cs
Expand All @@ -144,12 +138,9 @@ You must perform this step for **every** window you show in your application, no
<a id="windows-forms"></a>
### Windows Forms

You must do both of the following steps.

<a id="on-application-startup-1"></a>
#### On application startup

Before showing **any** windows in your application, you must call
Before showing **any** windows in your application, you may optionally call
```cs
IDarkNet.SetCurrentProcessTheme(theme);
```
Expand All @@ -171,16 +162,15 @@ internal static class Program {
}
```

<a id="before-showing-a-new-window-1"></a>
#### Before showing a new window

Before showing each window in your application, you have to set the theme for that window.
Before showing **each** window in your application, you have to set the theme for that window.

```cs
IDarkNet.SetWindowThemeForms(window, theme);
```

You must do this **before calling `Show()` or `Application.Run()`** to show the window. If you call it too late (such as after the window is shown), the calls will have no effect on Windows.
You must do this **before calling `Show()` or `Application.Run()`** to show the window. If you call it too late (such as after the window is shown), the calls will have no effect.

```cs
Form mainForm = new Form1();
Expand All @@ -189,7 +179,6 @@ DarkNet.Instance.SetWindowThemeForms(mainForm, Theme.Auto);

You must perform this step for **every** window you show in your application, not just the first one.

<a id="complete-example"></a>
#### Complete example

```cs
Expand Down Expand Up @@ -218,21 +207,19 @@ internal static class Program {

Windows introduced a preference to choose a dark or light taskbar in Windows 10 version 1903. This is controlled by Settings › Personalization › Colors › Choose your default Windows mode.

DarkNet exposes the value of this preference with the <code>IDarkNet.<strong>UserTaskbarThemeIsDark</strong></code> property, as well as the change event <code>IDarkNet.<strong>UserTaskbarThemeIsDarkChanged</strong></code>. You can use these to render a tray icon in the notification area that matches the taskbar's theme, and re-render it when the user preference changes.
DarkNet exposes the value of this preference with the **`UserTaskbarThemeIsDark`** property, as well as the change event **`UserTaskbarThemeIsDarkChanged`**. You can use these to render a tray icon in the notification area that matches the taskbar's theme, and re-render it when the user preference changes.

<a id="demos"></a>
## Demos

You can download the following precompiled demos, or clone this repository and build the demo projects yourself using Visual Studio Community 2022.

<a id="wpf-1"></a>
#### WPF

Download and run `darknet-demo-wpf.exe` from the [latest release](https://github.com/Aldaviva/DarkNet/releases).

![WPF window with dark title bar](https://raw.githubusercontent.com/Aldaviva/DarkNet/master/.github/images/demo-wpf.png)

<a id="windows-forms-1"></a>
#### Windows Forms

Download and run `darknet-demo-winforms.exe` from the [latest release](https://github.com/Aldaviva/DarkNet/releases).
Expand All @@ -242,7 +229,7 @@ Download and run `darknet-demo-winforms.exe` from the [latest release](https://g
<a id="limitations"></a>
## Limitations
- This library only changes the theme of the title bar/window chrome/non-client area, as well as the system context menu (the menu that appears when you right click on the title bar, or left click on the title bar icon, or hit `Alt`+`Space`). It does not change the theme of the client area of your window. It is up to you to make that look different when dark mode is enabled.
- This library currently does not help you persist a user choice for the mode they want your application to use across separate process executions. You can expose an option and persist that yourself, then pass the desired `Theme` value to the methods in this library.
- This library currently does not help you persist a user's choice for the mode they want your application to use across separate process executions. You can expose an option and persist that yourself, then pass the desired `Theme` value to the methods in this library.

<a id="acknowledgements"></a>
## Acknowledgements
Expand Down
1 change: 0 additions & 1 deletion darknet-demo-winforms/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ private static void Main() {
Application.SetCompatibleTextRenderingDefault(false);

IDarkNet darkNet = DarkNet.Instance;
// darkNet.SetCurrentProcessTheme(Theme.Auto);

Form mainForm = new Form1();
darkNet.SetWindowThemeForms(mainForm, Theme.Auto);
Expand Down
8 changes: 2 additions & 6 deletions darknet-demo-wpf/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,12 @@ protected override void OnStartup(StartupEventArgs e) {
base.OnStartup(e);

IDarkNet darkNet = DarkNet.Instance;
// darkNet.SetCurrentProcessTheme(Theme.Auto);

Console.WriteLine($"System is in {(darkNet.UserDefaultAppThemeIsDark ? "Dark" : "Light")} mode");
Console.WriteLine($"Taskbar is in {(darkNet.UserTaskbarThemeIsDark ? "Dark" : "Light")} mode");

darkNet.UserDefaultAppThemeIsDarkChanged += (_, isSystemDarkTheme) => {
Console.WriteLine($"System changed to {(isSystemDarkTheme ? "Dark" : "Light")} theme");
// darkNet.SetCurrentProcessTheme(isSystemDarkTheme ? Theme.Light : Theme.Dark); // after first render, make title bar opposite of default app theme
};
darkNet.UserTaskbarThemeIsDarkChanged += (_, isTaskbarDarkTheme) => { Console.WriteLine($"Taskbar changed to {(isTaskbarDarkTheme ? "Dark" : "Light")} theme"); };
darkNet.UserDefaultAppThemeIsDarkChanged += (_, isSystemDarkTheme) => { Console.WriteLine($"System changed to {(isSystemDarkTheme ? "Dark" : "Light")} theme"); };
darkNet.UserTaskbarThemeIsDarkChanged += (_, isTaskbarDarkTheme) => { Console.WriteLine($"Taskbar changed to {(isTaskbarDarkTheme ? "Dark" : "Light")} theme"); };
}

}
37 changes: 19 additions & 18 deletions darknet/DarkNet.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Concurrent;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
Expand All @@ -13,12 +14,12 @@
namespace Dark.Net;

/// <summary>
/// <para>Implementation of the DarkNet library. Used for making title bars of your windows dark in Windows 10 and later.</para>
/// <para>Implementation of the DarkNet library. Used for making title bars of your windows dark in Windows 10 1809 and later.</para>
/// <para>Usage:</para>
/// <list type="number">
/// <item><description>Construct a new instance with <c>new DarkNet()</c>, or use the shared singleton <see cref="Instance"/>.</description></item>
/// <item><description>Call <see cref="SetCurrentProcessTheme"/> before showing any windows in your process, such as in a <see cref="System.Windows.Application.Startup"/> event handler for your WPF process, or at the beginning of Main in your Forms process.</description></item>
/// <item><description>Call <see cref="SetWindowThemeWpf"/> or <see cref="SetWindowThemeForms"/> before showing any windows in your process. For WPF, you should do this in <see cref="Window.SourceInitialized"/>. For Forms, you should do this after constructing the <see cref="Form"/> instance.</description></item>
/// <item><description>Optionally, call <see cref="SetCurrentProcessTheme"/> before showing any windows in your process, such as in a <see cref="System.Windows.Application.Startup"/> event handler for your WPF program, or at the beginning of <c>Main</c> in your Forms program.</description></item>
/// <item><description>Call <see cref="SetWindowThemeWpf"/> or <see cref="SetWindowThemeForms"/> for each window before you show it. For WPF, you should do this in <see cref="Window.SourceInitialized"/>. For Forms, you should do this after constructing the <see cref="Form"/> instance.</description></item>
/// </list>
/// </summary>
public class DarkNet: IDarkNet {
Expand All @@ -35,8 +36,8 @@ public class DarkNet: IDarkNet {

private readonly ConcurrentDictionary<IntPtr, Theme> _preferredWindowModes = new();

private bool? _preferredDefaultAppThemeIsDark;
private bool? _preferredTaskbarThemeIsDark;
private bool? _userDefaultAppThemeIsDark;
private bool? _userTaskbarThemeIsDark;
private Theme? _preferredAppMode;
private int _processThemeChanged; // int instead of bool to support Interlocked atomic operations

Expand Down Expand Up @@ -72,7 +73,7 @@ public void SetCurrentProcessTheme(Theme theme) {
// Windows 10 1809 only
Win32.AllowDarkModeForApp(true);
} catch (Exception e2) when (e2 is not OutOfMemoryException) {
throw new Exception("Failed to set dark mode for process", e1); //TODO throw a different class
Trace.TraceWarning("Failed to set dark mode for process: {0}", e1.Message);
}
}
}
Expand Down Expand Up @@ -167,7 +168,7 @@ private void OnWindowClosing(IntPtr windowHandle) {
}

private void OnSettingsChanged(object sender, UserPreferenceChangedEventArgs args) {
if (args.Category == UserPreferenceCategory.General && (_preferredDefaultAppThemeIsDark != UserDefaultAppThemeIsDark || _preferredTaskbarThemeIsDark != UserTaskbarThemeIsDark)) {
if (args.Category == UserPreferenceCategory.General && (_userDefaultAppThemeIsDark != UserDefaultAppThemeIsDark || _userTaskbarThemeIsDark != UserTaskbarThemeIsDark)) {
RefreshTitleBarThemeColor();
}
}
Expand Down Expand Up @@ -219,29 +220,29 @@ private void RefreshTitleBarThemeColor(IntPtr windowHandle) {
/// <inheritdoc />
public bool UserDefaultAppThemeIsDark {
get {
bool? oldValue = _preferredDefaultAppThemeIsDark;
bool? oldValue = _userDefaultAppThemeIsDark;
// Unfortunately, the corresponding undocumented uxtheme.dll function (#132) always returns Dark in .NET Core runtimes for some reason, so we check the registry instead.
// Verified on Windows 10 21H2 and Windows 11 21H2.
_preferredDefaultAppThemeIsDark = !Convert.ToBoolean(Registry.GetValue(PersonalizeKey, "AppsUseLightTheme", 1));
if (oldValue is not null && _preferredDefaultAppThemeIsDark != oldValue) {
UserDefaultAppThemeIsDarkChanged?.Invoke(this, _preferredDefaultAppThemeIsDark.Value);
_userDefaultAppThemeIsDark = !Convert.ToBoolean(Registry.GetValue(PersonalizeKey, "AppsUseLightTheme", 1));
if (oldValue is not null && _userDefaultAppThemeIsDark != oldValue) {
UserDefaultAppThemeIsDarkChanged?.Invoke(this, _userDefaultAppThemeIsDark.Value);
}

return _preferredDefaultAppThemeIsDark.Value;
return _userDefaultAppThemeIsDark.Value;
}
}

/// <inheritdoc />
public bool UserTaskbarThemeIsDark {
get {
bool? oldValue = _preferredTaskbarThemeIsDark;
// In Windows 10 1809, including Server 2019, the taskbar is always dark, and this registry value does not exist.
_preferredTaskbarThemeIsDark = !Convert.ToBoolean(Registry.GetValue(PersonalizeKey, "SystemUsesLightTheme", 0));
if (oldValue is not null && _preferredTaskbarThemeIsDark != oldValue) {
UserTaskbarThemeIsDarkChanged?.Invoke(this, _preferredTaskbarThemeIsDark.Value);
bool? oldValue = _userTaskbarThemeIsDark;
// In Windows 10 1809 and Server 2019, the taskbar is always dark, and this registry value does not exist.
_userTaskbarThemeIsDark = !Convert.ToBoolean(Registry.GetValue(PersonalizeKey, "SystemUsesLightTheme", 0));
if (oldValue is not null && _userTaskbarThemeIsDark != oldValue) {
UserTaskbarThemeIsDarkChanged?.Invoke(this, _userTaskbarThemeIsDark.Value);
}

return _preferredTaskbarThemeIsDark.Value;
return _userTaskbarThemeIsDark.Value;
}
}

Expand Down
9 changes: 7 additions & 2 deletions darknet/DarkNet.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<TargetFrameworks>netcoreapp3.1;net452</TargetFrameworks>
<Version>2.0.0</Version>
<Version>2.0.0-SNAPSHOT1</Version>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
Expand All @@ -22,10 +22,15 @@
<IncludeSource>true</IncludeSource>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageIcon>icon.png</PackageIcon>

<!-- Not using the default DarkNet NS so that consumers don't have to qualify DarkNet.DarkNet, as NSes and types with the same name in C# are ambiguous, even with a using statement -->
<RootNamespace>Dark.Net</RootNamespace>
</PropertyGroup>

<ItemGroup>
<None Include="icon.png" Pack="true" PackagePath="\" />
</ItemGroup>

</Project>
Loading

0 comments on commit c172c49

Please sign in to comment.