.Net实现深色模式窗口

发布时间 2023-10-15 09:56:08作者: bsmith

深色模式窗口实现原理是遍历窗口的所有子控件,将子控件的字体颜色和背景色修改为深色模式,代码为:

public static void darkSubs(Control ui)
        {
            foreach (var obj in ui.Controls)
            {
                if (obj is Button btn)
                    dark(btn);
                else if (obj is TextBoxBase tx)
                    dark(tx);
                else if (obj is ListBox lb)
                    dark(lb);
                else if (obj is TreeView tr)
                    dark(tr);
                else if (obj is ListView lv)
                    dark(lv);
                else if (obj is ToolStrip tb)
                    dark(tb);
                else if (obj is PropertyGrid pg)
                    dark(pg);
                else if (obj is Panel panel)
                    darkSubs(panel);
                else if (obj is UserControl uc)
                    darkSubs(uc);
            }
        }

如果子窗口是Panel或者UserControl,则通过递归调用darkSubs,修改其子控件为深色模式。

对于普通控件,一般只需要修改字体颜色和背景色,即可实现深色模式,如TreeView:

public static void dark(TreeView ui)
        {
            ui.BackColor = CONTROL_BACK;
            ui.ForeColor = TEXT_FORE;
            ui.BorderStyle = FRAME_BORDER;
        }

对于ListView控件,当其处于Detail显示模式时,由于其表头Header不能直接修改背景颜色,需要自定义表头绘制方法:

public static void dark(ListView ui)
        {
            ui.BackColor = CONTROL_BACK;
            ui.ForeColor = TEXT_FORE;
            ui.BorderStyle = FRAME_BORDER;

            ui.OwnerDraw = true;
            ui.DrawItem += Ui_DrawItem;
            ui.DrawColumnHeader += ListView_DrawColumnHeader;
            ui.ColumnWidthChanged += ListView_ColumnWidthChanged;
            //ui.LostFocus += (s,e) => ui.autoSpan();
            ui.GotFocus += (s, e) => ui.autoSpan();
            ui.autoSpan();
        }

        private static void ListView_ColumnWidthChanged(object sender, ColumnWidthChangedEventArgs e)
        {
            var ui = sender as ListView;
            if (e.ColumnIndex == ui.Columns.Count - 1)
                return;
            var col = ui.Columns[ui.Columns.Count - 1];
            col.Width = -2;
        }

        private static void ListView_DrawColumnHeader(object sender, DrawListViewColumnHeaderEventArgs e)
        {
            //Fills one solid background for each cell.
            using (SolidBrush backBrush = new SolidBrush(FORM_BACK))
            {
                e.Graphics.FillRectangle(backBrush, e.Bounds);
            }
            //Draw the borders for the header around each cell.
            using (Pen backBrush = new Pen(CONTROL_BACK))
            {
                e.Graphics.DrawRectangle(backBrush, e.Bounds);
            }
            using (SolidBrush foreBrush = new SolidBrush(TEXT_FORE))
            {
                //Since e.Header.TextAlign returns 'HorizontalAlignment' with values of (Right, Center, Left).  
                //DrawString uses 'StringAlignment' with values of (Near, Center, Far). 
                //We must translate these and setup a vertical alignment that doesn't exist in DrawListViewColumnHeaderEventArgs.
                StringFormat stringFormat = GetStringFormat(e.Header.TextAlign);

                //Do some padding, since these draws right up next to the border for Left/Near.  Will need to change this if you use Right/Far
                Rectangle rect = e.Bounds; rect.X += 2;
                // e.Graphics.DrawString(e.Header.Text, e.Font, foreBrush, rect, stringFormat);
                e.Graphics.DrawString(e.Header.Text, e.Font, foreBrush, rect, stringFormat);
            }
        }

        private static StringFormat GetStringFormat(HorizontalAlignment ha)
        {
            StringAlignment align;

            switch (ha)
            {
                case HorizontalAlignment.Right:
                    align = StringAlignment.Far;
                    break;
                case HorizontalAlignment.Center:
                    align = StringAlignment.Center;
                    break;
                default:
                    align = StringAlignment.Near;
                    break;
            }

            return new StringFormat()
            {
                Alignment = align,
                LineAlignment = StringAlignment.Center
            };
        }

        private static void Ui_DrawItem(object sender, DrawListViewItemEventArgs e)
        {
            e.DrawDefault = true;
        }

对于Form的标题栏,也无法通过属性直接设置背景颜色,但是在Win10及以上版本的最新补丁中,有OS Api可以修改窗口标题栏颜色模式,所以直接调用OS Api实现:

[DllImport("DwmApi")]
        private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, int[] attrValue, int attrSize);

        public static void setDarkTitle(Form form)
        {
            IntPtr hwnd = form.Handle;
            //DwmSetWindowAttribute(hwnd, 19, new[] { 1 }, 4);
            //DwmSetWindowAttribute(hwnd, 20, new[] { 1 }, 4);
            if (DwmSetWindowAttribute(hwnd, 19, new[] { 1 }, 4) != 0)
                DwmSetWindowAttribute(hwnd, 20, new[] { 1 }, 4);
        }

为了方便应用调用,通过C#的静态扩展方法的方式,封装Form的扩展方法:

public static T dark<T>(this T form) where T : Form
        {
            setDarkTitle(form);
            form.BackColor = FORM_BACK;
            form.ForeColor = TEXT_FORE;

            darkSubs(form);

            return form;
        }

应用程序只要简单调用dark方法,即可实现窗口的深色模式显示:

var myForm = new MyForm().dark();
myForm.Show();

深色模式效果图如下:

 Github完整代码:https://github.com/bsmith-zhao/vfs/blob/main/util/ext/DarkFormEx.cs