Dark mode WIP

- The start of a dark mode theme
- Adds a button to toggle dark mode in the top options panel. (The buttons kind of ugly though and I may change this to a checkbox to align with the other existing options)
- Added library uxtheme.dll for handling scrollbar themeing (Dealing with scrollbars sucks)
- Added ThemeManager for handling of theme colors (Allows for potential expandability I'll probably never implement)
This commit is contained in:
Frog
2025-11-20 02:35:51 -08:00
parent bcf3ff84fe
commit e640b8b15d
7 changed files with 246 additions and 3 deletions
+12
View File
@@ -42,6 +42,18 @@ internal class CustomForm : Form
}
}
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
ThemeManager.Apply(this); // apply current theme (initial or toggled)
}
protected override void OnShown(EventArgs e)
{
base.OnShown(e);
ThemeManager.Apply(this); // ensure late-added controls also themed
}
private void OnHelpButtonClicked(object sender, EventArgs args)
{
using DialogForm helpDialog = new(this);
+20 -3
View File
@@ -46,6 +46,7 @@ namespace CreamInstaller.Forms
saveFlowPanel = new FlowLayoutPanel();
selectionTreeView = new CustomTreeView();
topOptionsTable = new TableLayoutPanel();
darkModeButton = new Button();
programsGroupBox.SuspendLayout();
proxyFlowPanel.SuspendLayout();
blockedGamesFlowPanel.SuspendLayout();
@@ -248,6 +249,19 @@ namespace CreamInstaller.Forms
selectionTreeView.Size = new System.Drawing.Size(604, 230);
selectionTreeView.TabIndex = 1001;
//
// darkModeButton
//
darkModeButton.AutoSize = true;
darkModeButton.AutoSizeMode = AutoSizeMode.GrowAndShrink;
darkModeButton.Location = new System.Drawing.Point(0, 0);
darkModeButton.Margin = new Padding(12, 0, 0, 0);
darkModeButton.Name = "darkModeButton";
darkModeButton.Size = new System.Drawing.Size(73, 25);
darkModeButton.TabIndex = 10010;
darkModeButton.Text = "Dark Mode";
darkModeButton.UseVisualStyleBackColor = true;
darkModeButton.Click += OnDarkModeToggle;
//
// progressBar
//
progressBar.Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
@@ -391,7 +405,8 @@ namespace CreamInstaller.Forms
topOptionsTable.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
topOptionsTable.AutoSize = true;
topOptionsTable.AutoSizeMode = AutoSizeMode.GrowAndShrink;
topOptionsTable.ColumnCount = 5;
topOptionsTable.ColumnCount = 6;
topOptionsTable.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
topOptionsTable.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
topOptionsTable.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
topOptionsTable.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); // spacer
@@ -406,8 +421,9 @@ namespace CreamInstaller.Forms
topOptionsTable.TabIndex = 10009;
topOptionsTable.Controls.Add(blockedGamesFlowPanel, 0, 0);
topOptionsTable.Controls.Add(useSmokeAPILayoutPanel, 1, 0);
topOptionsTable.Controls.Add(proxyFlowPanel, 3, 0);
topOptionsTable.Controls.Add(allCheckBoxLayoutPanel, 4, 0);
topOptionsTable.Controls.Add(darkModeButton, 2, 0);
topOptionsTable.Controls.Add(proxyFlowPanel, 4, 0);
topOptionsTable.Controls.Add(allCheckBoxLayoutPanel, 5, 0);
//
// SelectForm
//
@@ -479,6 +495,7 @@ namespace CreamInstaller.Forms
private Button resetButton;
private FlowLayoutPanel saveFlowPanel;
private TableLayoutPanel topOptionsTable;
private Button darkModeButton;
}
}
+12
View File
@@ -1239,4 +1239,16 @@ internal sealed partial class SelectForm : CustomForm
{
}
private void OnDarkModeToggle(object sender, EventArgs e)
{
ThemeManager.ToggleDarkMode(this);
darkModeButton.Text = Program.DarkModeEnabled ? "Light Mode" : "Dark Mode";
}
protected override void OnShown(EventArgs e)
{
base.OnShown(e);
ThemeManager.Apply(this); // ensure controls after dynamic operations get themed
}
}
+1
View File
@@ -40,6 +40,7 @@ internal sealed partial class UpdateForm : CustomForm
#if DEBUG
DebugForm.Current.Attach(form);
#endif
ThemeManager.Apply(form); // apply current theme when transitioning
}
private async void OnLoad()
+5
View File
@@ -43,6 +43,9 @@ internal static class Program
internal static readonly string[] ProtectedGameDirectories = [@"\EasyAntiCheat", @"\BattlEye"];
internal static readonly string[] ProtectedGameDirectoryExceptions = [];
// Dark mode toggle (default false). Can be toggled via UI button.
internal static bool DarkModeEnabled;
internal static bool IsGameBlocked(string name, string directory = null)
=> BlockProtectedGames && (ProtectedGames.Contains(name) || directory is not null &&
!ProtectedGameDirectoryExceptions.Contains(name)
@@ -70,6 +73,8 @@ internal static class Program
#if DEBUG
DebugForm.Current.Attach(form);
#endif
// Apply initial theme
Utility.ThemeManager.Apply(form);
Application.Run(form);
}
catch (Exception e)
+5
View File
@@ -21,4 +21,9 @@ internal static partial class NativeImports
[LibraryImport("user32.dll", SetLastError = true), DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
internal static partial void SetWindowPos(nint hWnd, nint hWndInsertAfter, int x, int y, int cx, int cy,
uint uFlags);
// Windows theming (scrollbars / dark mode for some controls)
[LibraryImport("uxtheme.dll", SetLastError = true)]
internal static partial int SetWindowTheme(nint hWnd, [MarshalAs(UnmanagedType.LPWStr)] string pszSubAppName,
[MarshalAs(UnmanagedType.LPWStr)] string pszSubIdList);
}
+191
View File
@@ -0,0 +1,191 @@
using System;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
namespace CreamInstaller.Utility;
internal static class ThemeManager
{
// VS-like dark colors
private static readonly Color DarkBack = ColorTranslator.FromHtml("#1E1E1E");
private static readonly Color DarkBackAlt = ColorTranslator.FromHtml("#252525");
private static readonly Color DarkBorder = ColorTranslator.FromHtml("#3F3F46");
private static readonly Color DarkFore = ColorTranslator.FromHtml("#D4D4D4");
private static readonly Color DarkForeDim = ColorTranslator.FromHtml("#9CA3AF");
private static readonly Color Accent = ColorTranslator.FromHtml("#0E639C");
private static readonly Color LightBack = SystemColors.Control;
private static readonly Color LightBackAlt = SystemColors.ControlLightLight;
private static readonly Color LightFore = SystemColors.ControlText;
private static readonly Color LightBorder = SystemColors.ControlDark;
internal static void ToggleDarkMode(Form anyForm)
{
Program.DarkModeEnabled = !Program.DarkModeEnabled;
ApplyToAllOpenForms();
}
internal static void Apply(Form form)
{
if (form is null) return;
if (!Program.DarkModeEnabled)
{
Reset(form);
return;
}
form.SuspendLayout();
form.BackColor = DarkBack;
form.ForeColor = DarkFore;
ApplyTitleBar(form);
foreach (Control c in form.Controls)
ApplyControlTheme(c, true);
form.ResumeLayout(true);
}
private static void ApplyControlTheme(Control control, bool dark)
{
if (control is null) return;
// recurse first to avoid overwriting multi-level backgrounds incorrectly
foreach (Control child in control.Controls)
ApplyControlTheme(child, dark);
if (dark)
{
switch (control)
{
case GroupBox gb:
gb.ForeColor = DarkFore;
gb.BackColor = DarkBackAlt;
break;
case Button b:
b.FlatStyle = FlatStyle.Flat;
b.FlatAppearance.BorderColor = DarkBorder;
b.BackColor = DarkBackAlt;
b.ForeColor = DarkFore;
break;
case CheckBox cb:
cb.BackColor = DarkBack;
cb.ForeColor = DarkFore;
break;
case Label lbl:
lbl.BackColor = DarkBack;
lbl.ForeColor = DarkFore;
break;
case ProgressBar pb:
pb.ForeColor = Accent;
pb.BackColor = DarkBackAlt;
break;
case TreeView tv:
tv.BackColor = DarkBackAlt;
tv.ForeColor = DarkFore;
tv.LineColor = DarkBorder;
break;
case RichTextBox rtb:
rtb.BackColor = DarkBackAlt;
rtb.ForeColor = DarkFore;
break;
case TableLayoutPanel tlp:
tlp.BackColor = DarkBack;
break;
case FlowLayoutPanel flp:
flp.BackColor = DarkBack;
break;
}
TryApplyScrollbarTheme(control, true);
}
else
{
// Light reset per control type
switch (control)
{
case GroupBox gb:
gb.BackColor = LightBack;
gb.ForeColor = LightFore;
break;
case Button b:
b.FlatStyle = FlatStyle.Standard;
b.BackColor = LightBack;
b.ForeColor = LightFore;
break;
case CheckBox cb:
cb.BackColor = LightBack;
cb.ForeColor = LightFore;
break;
case Label lbl:
lbl.BackColor = LightBack;
lbl.ForeColor = LightFore;
break;
case ProgressBar pb:
pb.BackColor = LightBack;
pb.ForeColor = LightFore;
break;
case TreeView tv:
tv.BackColor = LightBack;
tv.ForeColor = LightFore;
tv.LineColor = LightBorder;
break;
case RichTextBox rtb:
rtb.BackColor = LightBack;
rtb.ForeColor = LightFore;
break;
case TableLayoutPanel tlp:
tlp.BackColor = LightBack;
break;
case FlowLayoutPanel flp:
flp.BackColor = LightBack;
break;
}
TryApplyScrollbarTheme(control, false);
}
}
private static void Reset(Form form)
{
form.SuspendLayout();
form.BackColor = LightBack;
form.ForeColor = LightFore;
ApplyTitleBar(form);
foreach (Control c in form.Controls)
ApplyControlTheme(c, false);
form.ResumeLayout(true);
}
private static void ApplyToAllOpenForms()
{
foreach (Form openForm in Application.OpenForms.Cast<Form>())
Apply(openForm);
}
private static void ApplyTitleBar(Form form)
{
try
{
int useDark = Program.DarkModeEnabled ?1 :0;
NativeMethods.EnableDarkTitleBar(form.Handle, useDark);
}
catch { }
}
private static void TryApplyScrollbarTheme(Control control, bool dark)
{
// RichTextBox & TreeView host scrollbars internally; use window theme API
try
{
string theme = dark ? "DarkMode_Explorer" : null; // reset with null
NativeImports.SetWindowTheme(control.Handle, theme, null);
}
catch { }
}
}
internal static class NativeMethods
{
private const int DWMWA_USE_IMMERSIVE_DARK_MODE =20; // Windows102004+ /11
[System.Runtime.InteropServices.DllImport("dwmapi.dll")]
private static extern int DwmSetWindowAttribute(System.IntPtr hwnd, int attr, ref int attrValue, int attrSize);
internal static void EnableDarkTitleBar(System.IntPtr handle, int useDark)
{
_ = DwmSetWindowAttribute(handle, DWMWA_USE_IMMERSIVE_DARK_MODE, ref useDark, sizeof(int));
}
}