Skip to content

Commit

Permalink
#6: Dark scrollbars and #7: SetWindowThemeWpf: different themes for d…
Browse files Browse the repository at this point in the history
…ifferent windows? [appears to work, API is neither documented nor finalized]
  • Loading branch information
Aldaviva committed Aug 20, 2023
1 parent ac67206 commit 22d6614
Show file tree
Hide file tree
Showing 14 changed files with 319 additions and 70 deletions.
146 changes: 99 additions & 47 deletions DarkNet/DarkNet.cs

Large diffs are not rendered by default.

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.3.0</Version>
<Version>2.4.0</Version>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
Expand Down
38 changes: 38 additions & 0 deletions DarkNet/Events/WindowThemeChangedEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System;
using System.Windows;
using System.Windows.Forms;
using System.Windows.Interop;

namespace Dark.Net.Events;

public abstract class WindowThemeChangedEventArgs: EventArgs {

public IntPtr WindowHandle { get; }
public bool EffectiveWindowThemeIsDark { get; }

protected WindowThemeChangedEventArgs(IntPtr windowHandle, bool effectiveWindowThemeIsDark) {
WindowHandle = windowHandle;
EffectiveWindowThemeIsDark = effectiveWindowThemeIsDark;
}

}

public class WpfWindowThemeChangedEventArgs: WindowThemeChangedEventArgs {

public Window Window { get; }

public WpfWindowThemeChangedEventArgs(Window window, bool effectiveWindowThemeIsDark): base(new WindowInteropHelper(window).Handle, effectiveWindowThemeIsDark) {
Window = window;
}

}

public class FormsWindowThemeChangedEventArgs: WindowThemeChangedEventArgs {

public Form Window { get; }

public FormsWindowThemeChangedEventArgs(Form window, bool isEffectiveWindowThemeDark): base(window.Handle, isEffectiveWindowThemeDark) {
Window = window;
}

}
9 changes: 9 additions & 0 deletions DarkNet/IDarkNet.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Windows;
using System.Windows.Forms;
using Dark.Net.Events;

// ReSharper disable UnusedMemberInSuper.Global - it's a public library interface
// ReSharper disable UnusedMember.Global
Expand Down Expand Up @@ -110,4 +111,12 @@ public interface IDarkNet: IDisposable {
/// </summary>
event EventHandler<bool>? UserTaskbarThemeIsDarkChanged;

event EventHandler<WindowThemeChangedEventArgs>? EffectiveWindowThemeIsDarkChanged;

bool? GetWindowEffectiveThemeIsDarkWpf(Window window);

bool? GetWindowEffectiveThemeIsDarkForms(Form window);

bool? GetWindowEffectiveThemeIsDarkRaw(IntPtr window);

}
2 changes: 2 additions & 0 deletions DarkNet/ThemeOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,6 @@ public class ThemeOptions {
/// </summary>
public static readonly Color DefaultColor = Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF);

public bool? ApplyThemeToDescendentFormsScrollbars { get; set; } = null;

}
91 changes: 74 additions & 17 deletions DarkNet/WPF/SkinManager.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using Dark.Net.Events;

namespace Dark.Net.Wpf;

Expand Down Expand Up @@ -35,10 +38,10 @@ public interface ISkinManager: IDisposable {
/// <inheritdoc />
public class SkinManager: ISkinManager {

private ResourceDictionary? _skinResources;
private Uri? _lightThemeResources;
private Uri? _darkThemeResources;
private readonly IDarkNet _darkNet;
private Skin? _appSkin;

private readonly IDarkNet _darkNet;
private readonly IDictionary<Window, Skin> _windowSkins = new Dictionary<Window, Skin>();

/// <summary>
/// Create a new instance that uses the default <see cref="DarkNet"/> instance.
Expand All @@ -51,26 +54,57 @@ public SkinManager(): this(DarkNet.Instance) { }
/// <param name="darkNet"></param>
public SkinManager(IDarkNet darkNet) {
_darkNet = darkNet;

_darkNet.EffectiveCurrentProcessThemeIsDarkChanged += UpdateResource;
_darkNet.EffectiveWindowThemeIsDarkChanged += UpdateWindowResource;
}

/// <inheritdoc />
public virtual void RegisterSkins(Uri lightThemeResources, Uri darkThemeResources) {
_darkThemeResources = darkThemeResources;
_lightThemeResources = lightThemeResources;
if (_appSkin == null) {
Collection<ResourceDictionary> appResources = Application.Current.Resources.MergedDictionaries;
ResourceDictionary? appSkinResources = appResources.FirstOrDefault(r => r.Source.Equals(lightThemeResources) || r.Source.Equals(darkThemeResources));

if (appSkinResources == null) {
appSkinResources = new ResourceDictionary();
appResources.Add(appSkinResources);
}

_appSkin = new Skin(lightThemeResources, darkThemeResources, appSkinResources);
}

if (_skinResources == null) {
Collection<ResourceDictionary> appResources = Application.Current.Resources.MergedDictionaries;
_skinResources = appResources.FirstOrDefault(r => r.Source.Equals(lightThemeResources) || r.Source.Equals(darkThemeResources));
UpdateResource(null, _darkNet.EffectiveCurrentProcessThemeIsDark);
}

public virtual void RegisterSkins(Uri lightThemeResources, Uri darkThemeResources, Window window) {
// not thread safe
_windowSkins.TryGetValue(window, out Skin? skin);

if (skin == null) {
Collection<ResourceDictionary> windowResources = window.Resources.MergedDictionaries;
ResourceDictionary? windowSkinResources = windowResources.FirstOrDefault(r => r.Source.Equals(lightThemeResources) || r.Source.Equals(darkThemeResources));

if (_skinResources == null) {
_skinResources = new ResourceDictionary();
appResources.Add(_skinResources);
if (windowSkinResources == null) {
windowSkinResources = new ResourceDictionary();
windowResources.Add(windowSkinResources);
}

UpdateResource(null, _darkNet.EffectiveCurrentProcessThemeIsDark);
skin = new Skin(lightThemeResources, darkThemeResources, windowSkinResources);

_darkNet.EffectiveCurrentProcessThemeIsDarkChanged += UpdateResource;
_windowSkins[window] = skin;

window.Closing += OnCloseWindow;
} else {
skin.DarkThemeResources = darkThemeResources;
skin.LightThemeResources = lightThemeResources;
}

UpdateWindowResource(null, new WpfWindowThemeChangedEventArgs(window, _darkNet.GetWindowEffectiveThemeIsDarkWpf(window) ?? false));
}

private void OnCloseWindow(object sender, CancelEventArgs e) {
Window window = (Window) sender;
_windowSkins.Remove(window);
}

/// <summary>
Expand All @@ -81,15 +115,24 @@ public virtual void RegisterSkins(Uri lightThemeResources, Uri darkThemeResource
/// <param name="eventSource">unused</param>
/// <param name="isDarkTheme"><see langword="true" /> if the process is set to <see cref="Theme.Dark"/>, or <see langword="false"/> if it is set to <see cref="Theme.Light"/>.</param>
protected virtual void UpdateResource(object? eventSource, bool isDarkTheme) {
if (_skinResources != null) {
_skinResources.Source = isDarkTheme ? _darkThemeResources : _lightThemeResources;
if (_appSkin != null) {
_appSkin.WindowSkinResources.Source = isDarkTheme ? _appSkin.DarkThemeResources : _appSkin.LightThemeResources;
}
}

/// <inheritdoc />
private void UpdateWindowResource(object? sender, WindowThemeChangedEventArgs e) {
if (e is WpfWindowThemeChangedEventArgs wpfArgs) {
if (_windowSkins.TryGetValue(wpfArgs.Window, out Skin? skin)) {
skin.WindowSkinResources.Source = e.EffectiveWindowThemeIsDark ? skin.DarkThemeResources : skin.LightThemeResources;
}
}
}

/// <inheritdoc cref="IDisposable.Dispose" />
protected virtual void Dispose(bool disposing) {
if (disposing) {
_darkNet.EffectiveCurrentProcessThemeIsDarkChanged -= UpdateResource;
_darkNet.EffectiveWindowThemeIsDarkChanged -= UpdateWindowResource;
}
}

Expand All @@ -99,4 +142,18 @@ public void Dispose() {
GC.SuppressFinalize(this);
}

private class Skin {

public Uri LightThemeResources;
public Uri DarkThemeResources;
public readonly ResourceDictionary WindowSkinResources;

public Skin(Uri lightThemeResources, Uri darkThemeResources, ResourceDictionary windowSkinResources) {
WindowSkinResources = windowSkinResources;
LightThemeResources = lightThemeResources;
DarkThemeResources = darkThemeResources;
}

}

}
3 changes: 3 additions & 0 deletions DarkNet/Win32.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ internal static class Win32 {
[DllImport(User32)]
internal static extern IntPtr GetForegroundWindow();

[DllImport(UxTheme, CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern int SetWindowTheme(IntPtr window, string? substituteAppName, string? substituteIdList);

}

[StructLayout(LayoutKind.Sequential)]
Expand Down
37 changes: 37 additions & 0 deletions DarkNet/WindowThemeState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System;
using System.Windows;
using System.Windows.Forms;

namespace Dark.Net;

internal class WindowThemeState {

internal Theme PreferredTheme { get; set; }
internal ThemeOptions? Options { get; set; }
internal Window? WpfWindow { get; set; }
internal Form? FormsWindow { get; set; }
internal IntPtr WindowHandle { get; set; }

private bool _effectiveThemeIsDark;

internal event WindowThemeStateEventHandler? EffectiveThemeChanged;

internal WindowThemeState(IntPtr windowHandle, Theme preferredTheme, ThemeOptions? options = null) {
WindowHandle = windowHandle;
PreferredTheme = preferredTheme;
Options = options;
}

internal bool EffectiveThemeIsDark {
get => _effectiveThemeIsDark;
set {
if (value != _effectiveThemeIsDark) {
_effectiveThemeIsDark = value;
EffectiveThemeChanged?.Invoke(this, value);
}
}
}

internal delegate void WindowThemeStateEventHandler(WindowThemeState windowThemeState, bool isDarkMode);

}
43 changes: 43 additions & 0 deletions darknet-demo-winforms/Form1.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion darknet-demo-winforms/Form1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ private void onDarkModeCheckboxChanged(object sender, EventArgs e) {
CheckState.Indeterminate => Theme.Auto,
_ => Theme.Auto
};
DarkNet.Instance.SetCurrentProcessTheme(theme);
DarkNet.Instance.SetCurrentProcessTheme(theme, Program.ThemeOptions);
Console.WriteLine($"Process theme is {theme}");
}

Expand Down
4 changes: 3 additions & 1 deletion darknet-demo-winforms/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ namespace darknet_demo_winforms;

internal static class Program {

public static readonly ThemeOptions ThemeOptions = new() { ApplyThemeToDescendentFormsScrollbars = true };

[STAThread]
private static void Main() {
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);

IDarkNet darkNet = DarkNet.Instance;
Theme processTheme = Theme.Auto;
darkNet.SetCurrentProcessTheme(processTheme);
darkNet.SetCurrentProcessTheme(processTheme, ThemeOptions);
Console.WriteLine($"Process theme is {processTheme}");

Form mainForm = new Form1();
Expand Down
5 changes: 4 additions & 1 deletion darknet-demo-wpf/App.xaml
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
<Application x:Class="darknet_demo_wpf.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:darknet="clr-namespace:Dark.Net.Wpf;assembly=DarkNet"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!-- This will be replaced with the correct skin to match the title bar theme by SkinManager -->
<ResourceDictionary Source="Skins/Skin.Dark.xaml" />
<!-- <ResourceDictionary Source="Skins/Skin.Dark.xaml" /> -->
<!-- <ResourceDictionary Source="Skins/Skin.Light.xaml" /> -->
</ResourceDictionary.MergedDictionaries>

<darknet:SkinManager x:Key="skinManager"/>
</ResourceDictionary>
</Application.Resources>
</Application>
Loading

0 comments on commit 22d6614

Please sign in to comment.