Skip to content

Commit 7ef3cf7

Browse files
Added native dark mode support
1 parent 82e36db commit 7ef3cf7

27 files changed

+2698
-315
lines changed

Copyparty Launcher GUI.sln

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
1010
README.md = README.md
1111
EndProjectSection
1212
EndProject
13+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NativeDarkMode_Lib", "NativeDarkMode_Lib\NativeDarkMode_Lib.csproj", "{E67E658C-8452-4668-A07B-F4C1A75EA228}"
14+
EndProject
1315
Global
1416
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1517
Debug|Any CPU = Debug|Any CPU
@@ -26,6 +28,14 @@ Global
2628
{316524A9-C624-4C24-AF6B-EACE87B46D8B}.Release|Any CPU.Build.0 = Release|Any CPU
2729
{316524A9-C624-4C24-AF6B-EACE87B46D8B}.Release|x64.ActiveCfg = Release|x64
2830
{316524A9-C624-4C24-AF6B-EACE87B46D8B}.Release|x64.Build.0 = Release|x64
31+
{E67E658C-8452-4668-A07B-F4C1A75EA228}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
32+
{E67E658C-8452-4668-A07B-F4C1A75EA228}.Debug|Any CPU.Build.0 = Debug|Any CPU
33+
{E67E658C-8452-4668-A07B-F4C1A75EA228}.Debug|x64.ActiveCfg = Debug|Any CPU
34+
{E67E658C-8452-4668-A07B-F4C1A75EA228}.Debug|x64.Build.0 = Debug|Any CPU
35+
{E67E658C-8452-4668-A07B-F4C1A75EA228}.Release|Any CPU.ActiveCfg = Release|Any CPU
36+
{E67E658C-8452-4668-A07B-F4C1A75EA228}.Release|Any CPU.Build.0 = Release|Any CPU
37+
{E67E658C-8452-4668-A07B-F4C1A75EA228}.Release|x64.ActiveCfg = Release|Any CPU
38+
{E67E658C-8452-4668-A07B-F4C1A75EA228}.Release|x64.Build.0 = Release|Any CPU
2939
EndGlobalSection
3040
GlobalSection(SolutionProperties) = preSolution
3141
HideSolutionNode = FALSE

Copyparty Launcher GUI/Copyparty Launcher GUI.csproj

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@
118118
<ItemGroup>
119119
<None Include="App.config" />
120120
</ItemGroup>
121-
<ItemGroup />
121+
<ItemGroup>
122+
<ProjectReference Include="..\NativeDarkMode_Lib\NativeDarkMode_Lib.csproj">
123+
<Project>{e67e658c-8452-4668-a07b-f4c1a75ea228}</Project>
124+
<Name>NativeDarkMode_Lib</Name>
125+
</ProjectReference>
126+
</ItemGroup>
122127
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
123128
</Project>

Copyparty Launcher GUI/MainForm.Designer.cs

Lines changed: 342 additions & 313 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Copyparty Launcher GUI/MainForm.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System.Text;
1111
using System.Threading.Tasks;
1212
using System.Windows.Forms;
13+
using NativeDarkMode_Lib;
1314

1415
namespace Copyparty_Launcher_GUI
1516
{
@@ -20,8 +21,11 @@ public partial class MainForm : Form
2021

2122
public MainForm()
2223
{
24+
// OG SIZE: 810, 502
2325
InitializeComponent();
2426
_appSettings = SettingsManager.Load();
27+
28+
NativeDarkMode_Lib.Converter.DarkModeEnable(this);
2529
}
2630

2731
private void MainForm_Load(object sender, EventArgs e)
@@ -331,7 +335,7 @@ private void btnLaunchCli_Click(object sender, EventArgs e)
331335
_redirector = new HighPerformanceConsoleRedirector(serverLogBox, _appSettings.CopyPartyExePath, cliArgs);
332336
_redirector.Start();
333337

334-
tabControl1.SelectedTab = serverLogPage;
338+
darkTabControl1.SelectedTab = serverLoggingPage;
335339
}
336340
else
337341
{

Copyparty Launcher GUI/MainForm.resx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,13 @@
123123
<metadata name="statusStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
124124
<value>17, 17</value>
125125
</metadata>
126+
<metadata name="mainTooltip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
127+
<value>132, 17</value>
128+
</metadata>
126129
<metadata name="timer1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
127130
<value>250, 17</value>
128131
</metadata>
132+
<metadata name="$this.TrayHeight" type="System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
133+
<value>91</value>
134+
</metadata>
129135
</root>

NativeDarkMode_Lib/App.config

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<configuration>
3+
<startup>
4+
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
5+
</startup>
6+
</configuration>
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
using System;
2+
using System.ComponentModel;
3+
using System.Drawing;
4+
using System.Drawing.Drawing2D;
5+
using System.Reflection;
6+
using System.Runtime.InteropServices;
7+
using System.Windows.Forms;
8+
9+
10+
namespace NativeDarkMode_Lib
11+
{
12+
public class ComboBoxPainter : NativeWindow, IDisposable
13+
{
14+
// Public Properties for Customization
15+
[DefaultValue(typeof(Color), "Gray")]
16+
public Color BorderColor { get; set; } = Color.Gray;
17+
18+
[DefaultValue(typeof(Color), "LightGray")]
19+
public Color ButtonColor { get; set; } = Color.FromArgb(99, 99, 99);
20+
21+
[DefaultValue(typeof(Color), "White")]
22+
public Color ArrowColor { get; set; } = Color.White;
23+
24+
private readonly ComboBox originalComboBox;
25+
private Bitmap buffer; // Our double buffer for flicker-free drawing
26+
27+
public ComboBoxPainter(ComboBox comboBox)
28+
{
29+
this.originalComboBox = comboBox;
30+
31+
// Hook events to manage the buffer and force redraws
32+
originalComboBox.SizeChanged += OnSizeChanged;
33+
this.AssignHandle(comboBox.Handle);
34+
35+
// *** THE FIX: Use reflection to call the protected SetStyle method ***
36+
SetControlStyle(originalComboBox, ControlStyles.OptimizedDoubleBuffer, true);
37+
SetControlStyle(originalComboBox, ControlStyles.ResizeRedraw, true);
38+
39+
CreateBuffer();
40+
}
41+
42+
/// <summary>
43+
/// Uses reflection to call the protected SetStyle method on a control.
44+
/// </summary>
45+
private static void SetControlStyle(Control control, ControlStyles style, bool value)
46+
{
47+
MethodInfo method = typeof(Control).GetMethod("SetStyle", BindingFlags.NonPublic | BindingFlags.Instance);
48+
if (method != null)
49+
{
50+
method.Invoke(control, new object[] { style, value });
51+
}
52+
}
53+
54+
55+
private void OnSizeChanged(object sender, EventArgs e)
56+
{
57+
CreateBuffer();
58+
originalComboBox.Invalidate();
59+
}
60+
61+
private void CreateBuffer()
62+
{
63+
// Dispose of the old buffer if it exists
64+
buffer?.Dispose();
65+
66+
// Create a new buffer if the control is valid
67+
if (originalComboBox.IsHandleCreated && originalComboBox.Width > 0 && originalComboBox.Height > 0)
68+
{
69+
buffer = new Bitmap(originalComboBox.Width, originalComboBox.Height);
70+
}
71+
}
72+
73+
protected override void WndProc(ref Message m)
74+
{
75+
switch (m.Msg)
76+
{
77+
// We are taking full control of the WM_PAINT message.
78+
// We will not call base.WndProc here.
79+
case WM_PAINT:
80+
if (originalComboBox.DropDownStyle != ComboBoxStyle.Simple && buffer != null)
81+
{
82+
// Start the painting process
83+
PAINTSTRUCT ps = new PAINTSTRUCT();
84+
IntPtr hdc = BeginPaint(originalComboBox.Handle, ref ps);
85+
86+
// 1. Draw the entire control's appearance to our in-memory buffer
87+
using (Graphics g = Graphics.FromImage(buffer))
88+
{
89+
PaintComboBox(g);
90+
}
91+
92+
// 2. Draw the completed buffer onto the screen in one operation
93+
using (Graphics screenG = Graphics.FromHdc(hdc))
94+
{
95+
screenG.DrawImage(buffer, Point.Empty);
96+
}
97+
98+
// End the painting process
99+
EndPaint(originalComboBox.Handle, ref ps);
100+
}
101+
else
102+
{
103+
base.WndProc(ref m);
104+
}
105+
// Message is handled, do not call base.
106+
return;
107+
108+
// Prevent the OS from drawing its default non-client area (border/button)
109+
case WM_NCPAINT:
110+
// By not calling base.WndProc, we suppress the default painting
111+
// that causes the flicker.
112+
break;
113+
114+
default:
115+
base.WndProc(ref m);
116+
break;
117+
}
118+
}
119+
120+
private void PaintComboBox(Graphics g)
121+
{
122+
g.SmoothingMode = SmoothingMode.AntiAlias;
123+
124+
// --- Define Rectangles ---
125+
var clientRect = originalComboBox.ClientRectangle;
126+
var dropDownButtonWidth = SystemInformation.HorizontalScrollBarArrowWidth + 2;
127+
var borderRect = new Rectangle(clientRect.Location, new Size(clientRect.Width - 1, clientRect.Height - 1));
128+
var dropDownRect = new Rectangle(clientRect.Width - dropDownButtonWidth, 0, dropDownButtonWidth, clientRect.Height);
129+
// Define a text area with a small padding
130+
var textRect = new Rectangle(2, 2, clientRect.Width - dropDownButtonWidth - 4, clientRect.Height - 4);
131+
132+
// --- Determine Colors based on state ---
133+
var currentBorderColor = originalComboBox.Enabled ? this.BorderColor : SystemColors.ControlDark;
134+
var currentButtonColor = originalComboBox.Enabled ? this.ButtonColor : SystemColors.Control;
135+
var currentArrowColor = originalComboBox.Enabled ? this.ArrowColor : SystemColors.ControlDark;
136+
var currentBackColor = originalComboBox.Enabled ? originalComboBox.BackColor : SystemColors.Control;
137+
var currentTextColor = originalComboBox.Enabled ? originalComboBox.ForeColor : SystemColors.GrayText;
138+
139+
// --- Perform Drawing operations sequentially ---
140+
141+
// 1. Draw Background
142+
using (var b = new SolidBrush(currentBackColor))
143+
{
144+
g.FillRectangle(b, clientRect);
145+
}
146+
147+
// 2. Draw Text (This is now our responsibility)
148+
TextRenderer.DrawText(g,
149+
originalComboBox.Text,
150+
originalComboBox.Font,
151+
textRect,
152+
currentTextColor,
153+
TextFormatFlags.Left | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis);
154+
155+
// 3. Draw Button Background
156+
using (var b = new SolidBrush(currentButtonColor))
157+
{
158+
g.FillRectangle(b, dropDownRect);
159+
}
160+
161+
// 4. Draw Dropdown Arrow
162+
var middle = new Point(dropDownRect.Left + dropDownRect.Width / 2, dropDownRect.Top + dropDownRect.Height / 2);
163+
var arrow = new Point[]
164+
{
165+
new Point(middle.X - 3, middle.Y - 2),
166+
new Point(middle.X + 4, middle.Y - 2),
167+
new Point(middle.X, middle.Y + 2)
168+
};
169+
using (var b = new SolidBrush(currentArrowColor))
170+
{
171+
g.FillPolygon(b, arrow);
172+
}
173+
174+
// 5. Draw Border
175+
using (var p = new Pen(currentBorderColor))
176+
{
177+
g.DrawRectangle(p, borderRect);
178+
}
179+
}
180+
181+
// --- P/Invoke for custom painting ---
182+
private const int WM_PAINT = 0xF;
183+
private const int WM_NCPAINT = 0x85;
184+
185+
[StructLayout(LayoutKind.Sequential)]
186+
public struct PAINTSTRUCT { public IntPtr hdc; public bool fErase; public RECT rcPaint; public bool fRestore; public bool fIncUpdate; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] public byte[] rgbReserved; }
187+
[StructLayout(LayoutKind.Sequential)]
188+
public struct RECT { public int Left, Top, Right, Bottom; }
189+
190+
[DllImport("user32.dll")]
191+
private static extern IntPtr BeginPaint(IntPtr hWnd, ref PAINTSTRUCT lpPaint);
192+
[DllImport("user32.dll")]
193+
private static extern bool EndPaint(IntPtr hWnd, ref PAINTSTRUCT lpPaint);
194+
195+
// --- IDisposable for cleaning up resources ---
196+
public void Dispose()
197+
{
198+
buffer?.Dispose();
199+
// This stops the message interception
200+
this.ReleaseHandle();
201+
}
202+
}
203+
}

0 commit comments

Comments
 (0)