single.php

C# WinFormsのコンテキストメニューにダークモードを適用する

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プロジェクトでコンテキストメニューをダークモードに対応したい人の参考になれば幸いです。

スポンサーリンク

最後までご覧いただき、ありがとうございます。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です