Dark/Light WPF系统托盘图标模式

Dark/Light mode of system tray icon in WPF

我在我的 WPF 应用程序中使用通知图标。当我单击此图标时,上下文菜单总是有白色背景,但我想将其更改为暗模式。但是如何在通知图标中应用深色和浅色模式?

_notifyIcon = new Forms.NotifyIcon();
_notifyIcon.Icon = MySystray.Resources.Systray_icon;
_notifyIcon.Text = APP_NAME;

由于 WPF 使用旧的运行时,您无法像使用简单的 API 定位 UWP 时那样访问 Windows10 环境。您可以查询注册表以获取 Windows 是否处于黑暗模式。

要自定义上下文菜单,只需定义您在 System.Windows.Forms.NotifyIcon 鼠标交互时引用的 XAML 资源。

App.xaml
将上下文菜单的外观配置为黑色背景和白色前景。

<ContextMenu x:Key="NotifierContextMenu" 
             Placement="MousePoint" 
             Background="#1e1e1e" 
             Foreground="WhiteSmoke">
  <MenuItem Header="Close" Click="Menu_Close" />
</ContextMenu>

App.xaml.cs
配置 System.Windows.Forms.NotifyIcon 以在应用程序启动期间的某个时刻使用资源中的 ContxtMenu。

private async Task InitializeSystemTrayIconAsync()
{
  StreamResourceInfo streamResourceInfo = Application.GetResourceStream(new Uri("pack://application:,,,/Main.Resources;component/Icons/applicationIcon.ico", UriKind.Absolute));
  await using var iconFileStream = streamResourceInfo.Stream;
  this.SystemTrayIcon.Icon = new System.Drawing.Icon(iconFileStream);
  this.SystemTrayIcon.Visible = true;
  this.SystemTrayIcon.MouseClick += (sender, args) =>
  {
    switch (args.Button)
    {
      case System.Windows.Forms.MouseButtons.Right:
        ContextMenu menu = (ContextMenu)this.FindResource("NotifierContextMenu");
        menu.IsOpen = true;
        break;
    }
  };
}

如果您想将 WPF 上下文菜单与 windows 窗体的 NotifyIcon 一起使用,那么您可能需要调用鼠标挂钩来跟踪鼠标指针,以便在单击菜单区域外部时隐藏上下文菜单。否则,此上下文菜单将永远不会隐藏。这是另一个上下文。

我也遇到了同样的问题。通过这样做,长期的研发我发现解决这个问题的唯一方法是覆盖上下文菜单的渲染器。那里存在不同类型的渲染器。我使用过 ToolStripProfessionalRenderer。为了获得全部好处,我还继承了 ProfessionalColorTable。最后,我将这个自定义渲染器用作我的上下文菜单渲染面板。下面是步骤。

首先,通过继承ProfessionalColorTable创建MenuColorTable。

    public class MenuColorTable : ProfessionalColorTable
    {
    //Fields
    private Color backColor;
    private Color leftColumnColor;
    private Color borderColor;
    private Color menuItemBorderColor;
    private Color menuItemSelectedColor;
    private WindowsTheme systrayTheme;

    [Browsable(false)]
    public WindowsTheme SystrayTheme
    {
        get { return systrayTheme; }
        set { systrayTheme = value; }
    }

    //Constructor
    public MenuColorTable(bool isMainMenu, Color primaryColor, Color menuItemSelectedColor, Color menuItemBorderColor, WindowsTheme theme) : base()
    {
        this.UseSystemColors = false;
        this.systrayTheme = theme;
        
        if(menuItemSelectedColor == Color.Empty)
        {
            menuItemSelectedColor = Color.FromArgb(51, 102, 255);
        }

        if (menuItemBorderColor == Color.Empty)
        {
            menuItemBorderColor = Color.FromArgb(25, 51, 127);
        }

        if (isMainMenu)
        {
            switch (SystrayTheme)
            {
                case WindowsTheme.Light:
                    {
                        backColor = Color.FromArgb(255, 255, 255);
                        leftColumnColor = Color.FromArgb(242, 242, 242);
                        borderColor = Color.FromArgb(193, 193, 193);
                        this.menuItemBorderColor = menuItemBorderColor;
                        this.menuItemSelectedColor = menuItemSelectedColor;
                    }
                    break;
                case WindowsTheme.Dark:
                    {
                        backColor = Color.FromArgb(37, 39, 60);
                        leftColumnColor = Color.FromArgb(32, 33, 51);
                        borderColor = Color.FromArgb(32, 33, 51);
                        this.menuItemBorderColor = menuItemBorderColor;
                        this.menuItemSelectedColor = menuItemSelectedColor;
                    }
                    break;
                case WindowsTheme.HighContrast:
                    {
                        backColor = Color.FromArgb(37, 39, 60);
                        leftColumnColor = Color.FromArgb(32, 33, 51);
                        borderColor = Color.FromArgb(32, 33, 51);
                        this.menuItemBorderColor = menuItemBorderColor;
                        this.menuItemSelectedColor = menuItemSelectedColor;
                    }
                    break;
            }
        }
        else
        {
            backColor = Color.White;
            leftColumnColor = Color.LightGray;
            borderColor = Color.LightGray;
            this.menuItemBorderColor = menuItemBorderColor;
            this.menuItemSelectedColor = menuItemSelectedColor;
        }
    }

    //Overrides
    public override Color ToolStripDropDownBackground { get { return backColor; } }
    public override Color MenuBorder { get { return borderColor; } }
    public override Color MenuItemBorder { get { return menuItemBorderColor; } }
    public override Color MenuItemSelected { get { return menuItemSelectedColor; } }
    
    public override Color ImageMarginGradientBegin { get { return leftColumnColor; } }
    public override Color ImageMarginGradientMiddle { get { return leftColumnColor; } }
    public override Color ImageMarginGradientEnd { get { return leftColumnColor; } }
    
    public override Color ButtonSelectedHighlight { get { return menuItemSelectedColor; } }
    public override Color ButtonSelectedHighlightBorder { get { return menuItemBorderColor; } }
}

创建必要的实用程序class:

public enum WindowsTheme
{
    Default = 0,
    Light = 1,
    Dark = 2,
    HighContrast = 3
}

public static class Utility
{
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static System.Drawing.Color ToDrawingColor(this System.Windows.Media.Color mediaColor)
    {
        return System.Drawing.Color.FromArgb(mediaColor.A, mediaColor.R, mediaColor.G, mediaColor.B);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static System.Windows.Media.Color ToMediaColor(this System.Drawing.Color drawingColor)
    {
        return System.Windows.Media.Color.FromArgb(drawingColor.A, drawingColor.R, drawingColor.G, drawingColor.B);
    }
}

现在用自定义的 MenuColorTable 覆盖 ToolStripProfessionalRenderer。

public class MenuRenderer : ToolStripProfessionalRenderer
{
    //Fields
    private Color primaryColor;
    private Color textColor;
    private int arrowThickness;
    private WindowsTheme systrayTheme;

    [Browsable(false)]
    public WindowsTheme SystrayTheme
    {
        get { return systrayTheme; }
        set { systrayTheme = value; }
    }

    //Constructor
    public MenuRenderer(bool isMainMenu, Color primaryColor, Color textColor, Color menuItemMouseOverColor, Color menuItemMouseOverBorderColor, WindowsTheme theme)
        : base(new MenuColorTable(isMainMenu, primaryColor, menuItemMouseOverColor, menuItemMouseOverBorderColor, theme))
    {
        RoundedEdges = true;
        
        this.primaryColor = primaryColor;
        this.systrayTheme = theme;

        if (isMainMenu)
        {
            arrowThickness = 2;
            if (textColor == Color.Empty) //Set Default Color
                this.textColor = Color.Gainsboro;
            else//Set custom text color 
                this.textColor = textColor;
        }
        else
        {
            arrowThickness = 1;
            if (textColor == Color.Empty) //Set Default Color
                this.textColor = Color.DimGray;
            else//Set custom text color
                this.textColor = textColor;
        }
    }

    //Overrides
    protected override void OnRenderItemText(ToolStripItemTextRenderEventArgs e)
    {
        base.OnRenderItemText(e);
        e.Item.ForeColor = e.Item.Selected ? Color.White : textColor;
    }

    protected override void OnRenderArrow(ToolStripArrowRenderEventArgs e)
    {
        //Fields
        var graph = e.Graphics;
        var arrowSize = new Size(5, 10);
        var arrowColor = e.Item.Selected ? Color.White : primaryColor;
        var rect = new Rectangle(e.ArrowRectangle.Location.X, (e.ArrowRectangle.Height - arrowSize.Height) / 2,
            arrowSize.Width, arrowSize.Height);
        using (GraphicsPath path = new GraphicsPath())
        using (Pen pen = new Pen(arrowColor, arrowThickness))
        {
            //Drawing
            graph.SmoothingMode = SmoothingMode.AntiAlias;
            path.AddLine(rect.Left, rect.Top, rect.Right, rect.Top + rect.Height / 2);
            path.AddLine(rect.Right, rect.Top + rect.Height / 2, rect.Left, rect.Top + rect.Height);
            graph.DrawPath(pen, path);
        }
    }        
                 
    public GraphicsPath RoundedRect(Rectangle bounds, int radius)
    {
        int diameter = radius * 2;
        Size size = new Size(diameter, diameter);
        Rectangle arc = new Rectangle(bounds.Location, size);
        GraphicsPath path = new GraphicsPath();

        if (radius == 0)
        {
            path.AddRectangle(bounds);
            return path;
        }

        // top left arc  
        path.AddArc(arc, 180, 90);

        // top right arc  
        arc.X = bounds.Right - diameter;
        path.AddArc(arc, 270, 90);

        // bottom right arc  
        arc.Y = bounds.Bottom - diameter;
        path.AddArc(arc, 0, 90);

        // bottom left arc 
        arc.X = bounds.Left;
        path.AddArc(arc, 90, 90);

        path.CloseFigure();
        return path;
    }
}

现在是时候通过继承 ContextMenuStrip 创建您自己的自定义 StripMenu 了。通过 覆盖 OnHandleCreated

将您的自定义 MenuRenderer 指定为 ContextMenus 的渲染
public class CustomContextMenu : ContextMenuStrip
{
    //Fields
    private bool isMainMenu;
    private int menuItemHeight = 20;
    private int menuItemWidth = 20;
    private Color menuItemTextColor = Color.Empty;
    private Color primaryColor = Color.Empty;
    private Color MouseOverColor = Color.Empty;
    private Color MouseOverBorderColor = Color.Empty;
    private WindowsTheme systrayTheme = WindowsTheme.Light;
    private Bitmap menuItemHeaderSize;

    //Constructor
    public CustomContextMenu() 
    {
        
    }

    //Properties
    [Browsable(false)]
    public bool IsMainMenu
    {
        get { return isMainMenu; }
        set { isMainMenu = value; }
    }

    [Browsable(false)]
    public int MenuItemHeight
    {
        get { return menuItemHeight; }
        set { menuItemHeight = value; }
    }
    
    [Browsable(false)]
    public int MenuItemWidth
    {
        get { return menuItemWidth; }
        set { menuItemWidth = value; }
    }

    [Browsable(false)]
    public Color MenuItemTextColor
    {
        get { return menuItemTextColor; }
        set { menuItemTextColor = value; }
    }

    [Browsable(false)]
    public Color PrimaryColor
    {
        get { return primaryColor; }
        set { primaryColor = value; }
    }

    [Browsable(false)]
    public Color MenuItemMouseOverColor
    {
        get { return MouseOverColor; }
        set { MouseOverColor = value; }
    }
    
    [Browsable(false)]
    public Color MenuItemMouseOverBorderColor
    {
        get { return MouseOverBorderColor; }
        set { MouseOverBorderColor = value; }
    }

    [Browsable(false)]
    public WindowsTheme SystrayTheme
    { 
        get { return systrayTheme; }
        set { systrayTheme = value; }
    }

    //Private methods
    private void LoadMenuItemHeight()
    {
        if (isMainMenu)
            menuItemHeaderSize = new Bitmap(menuItemWidth, menuItemHeight);
        else menuItemHeaderSize = new Bitmap(menuItemWidth-5, menuItemHeight);

        foreach (Forms.ToolStripMenuItem menuItemL1 in this.Items)
        {
            menuItemL1.ImageScaling = ToolStripItemImageScaling.None;
            if (menuItemL1.Image == null) menuItemL1.Image = menuItemHeaderSize;

            foreach (Forms.ToolStripMenuItem menuItemL2 in menuItemL1.DropDownItems)
            {
                menuItemL2.ImageScaling = ToolStripItemImageScaling.None;
                if (menuItemL2.Image == null) menuItemL2.Image = menuItemHeaderSize;

                foreach (Forms.ToolStripMenuItem menuItemL3 in menuItemL2.DropDownItems)
                {
                    menuItemL3.ImageScaling = ToolStripItemImageScaling.None;
                    if (menuItemL3.Image == null) menuItemL3.Image = menuItemHeaderSize;

                    foreach (Forms.ToolStripMenuItem menuItemL4 in menuItemL3.DropDownItems)
                    {
                        menuItemL4.ImageScaling = ToolStripItemImageScaling.None;
                        if (menuItemL4.Image == null) menuItemL4.Image = menuItemHeaderSize;
                        ///Level 5++
                    }
                }
            }
        }
    }

    //Overrides
    protected override void OnHandleCreated(EventArgs e)
    {
        base.OnHandleCreated(e);
        if (this.DesignMode == false)
        {
            switch (SystrayTheme)
            {
                case WindowsTheme.Light:
                    {
                        menuItemTextColor = Color.Black;
                    }
                    break;
                case WindowsTheme.Dark:
                    {
                        menuItemTextColor = Color.White;
                    }
                    break;
                case WindowsTheme.HighContrast:
                    {
                        menuItemTextColor = Utility.ToDrawingColor(System.Windows.SystemColors.MenuTextColor);
                    }
                    break;
            }

            this.Renderer = new MenuRenderer(isMainMenu, primaryColor, menuItemTextColor, MouseOverColor, MouseOverBorderColor, SystrayTheme);
            LoadMenuItemHeight();
        }
    }

 }

现在是时候通过继承 ContextMenuStrip 创建您自己的自定义 StripMenu 了。通过覆盖 OnHandleCreated

将您的自定义 MenuRenderer 指定为 ContextMenus 的渲染器

最后,为不同的主题创建不同类型的菜单,并根据 windows' 当前主题将它们分配为 NotiIcon 的 ContextMenuStrip。

顺便说一下,您可以通过 WMI 查询观察器从 wpf 检测系统主题更改事件。 Here 你从 wpf 找到了我关于运行时主题变化检测的答案。

    //Create Different menu's for different theme
    private CustomContextMenu contextMenuLight;
    private CustomContextMenu contextMenuDark;
    private CustomContextMenu contextMenuHiContrast;
    
    //Udpate notifyIcon.ContextMenuStrip based on Theme changed event.
    private void UpdateContextMenuOnWindowsThemeChange(WindowsTheme windowsTheme)
    {
        switch (windowsTheme)
        {
            case WindowsTheme.Default:
            case WindowsTheme.Light:
                {
                    notifyIcon.ContextMenuStrip = contextMenuLight;
                }
                break;
            case WindowsTheme.Dark:
                {
                    notifyIcon.ContextMenuStrip = contextMenuDark;
                }
                break;
            case WindowsTheme.HighContrast:
                {
                    notifyIcon.ContextMenuStrip = contextMenuHiContrast;
                }
                break;
        }

        InvalidateNotiIcon();
    }

    //Don't forget to Invalidate the notifyIcon after re-assigning new context menu
    private void InvalidateNotiIcon()
    {
        try
        {
            notifyIcon.ContextMenuStrip.Invalidate(true);
            notifyIcon.ContextMenuStrip.Update();
            notifyIcon.ContextMenuStrip.Refresh();
        }
        catch (Exception ex)
        {
        
        }
    }

根据 windows 当前主题重新分配新的上下文菜单后,不要忘记使 NotifyIcon 无效。