C#のWinFormsプロジェクトで、コンテキストメニューの配色をWindowsの[個人用設定]で設定できる[カラーモード]に対応する方法について備忘録的に投稿します。
メニューの[Renderer]を適用
配色の変更は[ProfessionalColorTable]を継承したクラスを作成して、内部でメニューの描画に利用されるColorクラスをオーバーライドします。
[ContextMenuStrip]で作成したインスタンスの[Renderer]に、作成したクラスを適用します。
具体的には、こんな感じに新しいクラスを作ります。
using System;
using System.Collections.Generic;
using System.Text;
namespace KeyLayerView
{
internal class ProfessionalDarkColorTable : ProfessionalColorTable
{
// メニュー全体の背景色
public override Color ToolStripDropDownBackground => UIColorTypeish.Background;
// アイコンなどを配置する左側の帯の背景色
public override Color ImageMarginGradientBegin => UIColorTypeish.Background;
public override Color ImageMarginGradientMiddle => UIColorTypeish.Background;
public override Color ImageMarginGradientEnd => UIColorTypeish.Background;
// メニューアイテムの選択時(ホバー時)の背景色
public override Color MenuItemSelected => UIColorTypeish.MenuItemSelected;
public override Color MenuItemSelectedGradientBegin => UIColorTypeish.MenuItemSelected;
public override Color MenuItemSelectedGradientEnd => UIColorTypeish.MenuItemSelected;
public override Color MenuItemBorder => UIColorTypeish.MenuItemSelected;
// 枠線の色
public override Color MenuBorder => UIColorTypeish.Background;
}
}
適用される色情報は、直接[Color]クラスで直接してすることもできますが、別件で作成したダークモードに対応する色情報を提供するクラスを作りました。
WinFormsは、ダークモード用に用意された色情報を取得する方法が無いので、ほぼ独自で色を決める必要があります。
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Text;
using static KeyLayerView.ThemeMessageListener;
namespace KeyLayerView
{
internal class UIColorTypeish
{
private static bool _isDark;
public static void UpdateTheme(ThemeInfo theme)
{
_isDark = theme.IsDark;
}
// ===== ベースカラー =====
public static Color Background =>
_isDark ? Color.FromArgb(30, 30, 30) : Color.White;
public static Color Foreground =>
_isDark ? Color.White : Color.Black;
public static Color MenuItemSelected => _isDark ? Lighten(Background, 0.2f) : Darken(Background, 0.2f);
// ===== ユーティリティ =====
private static Color Lighten(Color color, float amount)
{
return Blend(color, Color.White, amount);
}
private static Color Darken(Color color, float amount)
{
return Blend(color, Color.Black, amount);
}
private static Color Blend(Color c1, Color c2, float amount)
{
byte r = (byte)(c1.R + (c2.R - c1.R) * amount);
byte g = (byte)(c1.G + (c2.G - c1.G) * amount);
byte b = (byte)(c1.B + (c2.B - c1.B) * amount);
return Color.FromArgb(r, g, b);
}
}
}
メニューに表示されるテキストは[ProfessionalColorTable]クラスには該当する部分が無いので[ToolStripProfessionalRenderer]を派生したクラスの[OnRenderItemText]で色を変更します。
class ToolStripProfessionalDarkRenderer : ToolStripProfessionalRenderer
{
public ToolStripProfessionalDarkRenderer() : base(new ProfessionalDarkColorTable()){}
protected override void OnRenderItemText(ToolStripItemTextRenderEventArgs e)
{
// メニューアイテムのテキストカラー
e.TextColor = UIColorTypeish.Foreground;
base.OnRenderItemText(e);
}
}
適用する、Windows側で設定されているモードを取得します。
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Windows.Forms;
namespace KeyLayerView
{
internal class ThemeMessageListener : NativeWindow, IDisposable
{
private bool _isDark;
private Color _AccentColor;
public event Action<ThemeInfo>? ThemeChanged;
public class ThemeInfo
{
public bool IsDark { get; set; }
public Color AccentColor { get; set; }
}
public ThemeMessageListener()
{
_isDark = IsDarkMode();
CreateHandle(new CreateParams());
}
protected override void WndProc(ref Message m)
{
if (m.Msg == Win32.WM_SETTINGCHANGE || m.Msg == Win32.WM_THEMECHANGED)
{
bool themeChanged = false;
// 設定変更の中でもテーマやアクセントカラー変更の可能性があるものをフィルタリング
//string? changedSetting = System.Runtime.InteropServices.Marshal.PtrToStringUni(m.LParam);
//if (!string.IsNullOrEmpty(changedSetting) &&
// (changedSetting.Equals("ImmersiveColorSet", StringComparison.OrdinalIgnoreCase) ||
// changedSetting.Equals("WindowsThemeElement", StringComparison.OrdinalIgnoreCase)))
//{
// Color newColor = GetAccentColor();
// if (newColor != _AccentColor)
// {
// _AccentColor = newColor;
// themeChanged = true;
// }
//}
bool isThemeMessage =
m.Msg == Win32.WM_THEMECHANGED ||
(m.Msg == Win32.WM_SETTINGCHANGE &&
m.LParam != IntPtr.Zero);
if(isThemeMessage)
{
Color newColor = GetAccentColor();
if (newColor != _AccentColor)
{
_AccentColor = newColor;
themeChanged = true;
}
}
bool newMode = IsDarkMode();
if (newMode != _isDark)
{
_isDark = newMode;
themeChanged = true;
}
if (themeChanged)
{
ThemeChanged?.Invoke(new ThemeInfo { IsDark = _isDark, AccentColor = _AccentColor });
}
}
base.WndProc(ref m);
}
public void Dispose()
{
if (this.Handle != IntPtr.Zero)
{
this.DestroyHandle();
}
}
public Color GetAccentColor()
{
using var key = Registry.CurrentUser.OpenSubKey(
@"Software\Microsoft\Windows\DWM");
object? val = key?.GetValue("ColorizationColor");
uint colorValue;
if (val is int vi)
{
colorValue = unchecked((uint)vi);
}
else if (val is uint vu)
{
colorValue = vu;
}
else if (val is long vl)
{
colorValue = unchecked((uint)vl);
}
else if (val is short vs)
{
colorValue = (uint)vs;
}
else if (val is byte vb)
{
colorValue = vb;
}
else if (val is string s && uint.TryParse(s, out uint parsed))
{
colorValue = parsed;
}
else
{
colorValue = 0xFF0078D7u; // デフォルトはWindows 10の青
}
return Color.FromArgb(
(int)((colorValue >> 16) & 0xFF), // R
(int)((colorValue >> 8) & 0xFF), // G
(int)(colorValue & 0xFF)); // B
}
public ThemeInfo GetCurrentTheme()
{
return new ThemeInfo
{
IsDark = IsDarkMode(),
AccentColor = GetAccentColor()
};
}
private bool IsDarkMode()
{
using var key = Registry.CurrentUser.OpenSubKey(
@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize");
object? val = key?.GetValue("AppsUseLightTheme");
if (val is int i) return i == 0;
if (val is byte b) return b == 0;
if (val is string s && int.TryParse(s, out int parsed)) return parsed == 0;
return false;
}
}
}
呼び出し側では、こんな感じで作成した[ContextMenuStrip]インスタンスの[Renderer]に[ProfessionalColorTable]を継承した独自クラスを設定します。
private readonly ThemeMessageListener? _themeMessageListener = new ThemeMessageListener();
private void ApplyTheme(ThemeInfo theme)
{
UIColorTypeish.UpdateTheme(theme);
if(_Keymapform != null)
_Keymapform.ThemeChanged();
}
private ContextMenuStrip CreateContextMenu()
{
var winappstartUp = new WinAppStartUp("KeyLayerView");
ApplyTheme(_themeMessageListener!.GetCurrentTheme());
bool isStartUp = winappstartUp.IsStartUp();
string startUpText = isStartUp ? WinStartUpDisableText : WinStartUpEnableText;
var menu = new ContextMenuStrip();
menu.Renderer = new ToolStripProfessionalDarkRenderer();
menu.Items.Add("設定再読み込み", null, (_, _) => ReloadConfig());
menu.Items.Add("-");
menu.Items.Add(startUpText, null, (s, _) => SetWinStartUp(s));
menu.Items.Add("-");
menu.Items.Add("終了", null, (_, _) => Exit());
return menu;
}
実行すると、ポップアップメニューの配色が
Windowsの[個人用設定|色|モード]で指定したモードで表示されます。
今回は最低限、ダークモードっぽく見える部分のみを変更していますが、[ProfessionalColorTable]クラスでは、枠線や影のような細かな部分まで変更できます。
細かく配色する場合の色は、自分で調べる必要があるので、あしからず。
まとめ
今回は、C#のWinFormsプロジェクトで、コンテキストメニューの配色をWindowsの[個人用設定]で設定できる[カラーモード]に対応する方法について書きました。
WinFormsプロジェクトを利用する場合、Windowsのダークモードに対応するには、フォームやメニュー部分の配色を”ほぼ”独自で変更する必要があります。
WinFormsプロジェクトでコンテキストメニューをダークモードに対応したい人の参考になれば幸いです。
スポンサーリンク
最後までご覧いただき、ありがとうございます。

