更改单个应用程序的系统颜色(Windows,.NET)

6
我知道通常应该避免更改此类系统设置,但我的应用程序已经使用了非标准颜色,我对此没有影响。我想在某些地方添加标准的.NET控件,但它们的颜色不匹配。我想要一个方法来替换这个应用程序的系统颜色。还有一件重要的事情需要注意,它是一个.NET应用程序。
我目前的(不完整)想法是:
- 创建一个代理User32.dll库,并替换GetSysColor,但这将非常繁琐(需要重定向731个函数,替换1个函数),而且我不知道如何强制我的应用程序使用特定的副本。 - 以某种方式拦截对GetSysColors的调用(不幸的是,我认为它在CLR中)。 - 修改.NET类SystemColors(在内存中?是否可能?)。
你有什么想法,最好的(并完整的)实现方式是什么?

我已经成功地钩取了GetSysColor和GetSysColorBrush。不幸的是,这还不足以给控件着色,而且我仍然不知道原因。 - olpa
这还不够,因为:
  1. Gdi+ 在内部缓存系统颜色值和画笔,您需要清除这些缓存或确保您的钩子在 Gdi+ 初始化之前发生。
  2. 一些控件会缓存画笔。通过触摸颜色属性(如 control.BackColor = control.BackColor)来解决此问题。
  3. 一些控件需要设置 FlatStyle.Flat 才能完全依赖于 BackColor / ForeColor 属性。
- Nikolay Hidalgo Diaz
3个回答

4
我希望能够在某些地方添加标准的.NET控件,但它们的颜色不匹配。我想要一个技巧来替换这个应用程序的系统颜色。
这就像用大锤子钉钉子一样。与其在系统内部搞乱颜色,您可以从每个您想要使用的库存控件中继承一个新控件。因此,您可以从库存TextBox控件中继承,创建自己的ThemedTextBox。您可以默认设置此新控件以使用应用程序的颜色方案,并且因为在继承结构方面它仍然是一个TextBox,所以您可以在任何您使用普通文本框的地方使用它,包括在winforms设计器中。

不幸的是,在.NET中无法更改继承类中的颜色。我必须从头开始进行所有的绘制。即使这样也不够,因为有各种可能的视觉风格(例如XP与Vista)。 - Michal Czardybon
1
当然可以更改颜色。只需更新继承类的构造函数以设置适当的属性即可。 - Joel Coehoorn
3
问题在于,在WinForms中没有“适当的属性”。控件类只有ForeColor和BackColor属性,但这并不是所有的。对于TextBox,我们只能更改背景颜色,但对于ScrollBar,我们无法更改任何内容。 - Michal Czardybon

2
在我的早期,我开发了一个程序,注册了一个全局消息钩子来绘制窗口边框 - 我可以为所有窗口设置主题。这对于单个应用程序也应该是可能的。然而,这不是一个简单的任务。
否则,我认为这将是不可能的。使用可主题化的第三方控件如Krypton Toolkit如何?

实际上我使用了一些Divelements控件,但它们并没有提供像按钮或滚动条这样的常规控件。我会检查Krypton。谢谢你的建议。你能否把你所说的这个着色程序发给我?我将感激任何额外的细节。 - Michal Czardybon
抱歉,无法向您发送该工具的源代码 - 我应该是在1992年用Borland Pascal编写的。 这些源代码已经遗失很久了... - Thorsten Dittmar
我已经检查过了 - 我确实可以使用系统钩子拦截WM_PAINT事件,但是它并没有给我任何东西。我可以在我的C#代码中简单地重写OnPaint来完成。但是我仍然无法更改颜色。也许只有边框是可能的(知道客户端矩形),但这还不够。 - Michal Czardybon

-1

System.Drawing.SystemColor的值被缓存在KnownColorsTable类中的私有数组colorTable中。该数组的内容是通过Win32 API请求填充和更新的系统颜色更改。

要在应用程序范围内更改系统颜色,请将存储在colorTable中的颜色值更改为所需的值。

然后,您还需要清除SystemBrushesSystemPens类的缓存,因为笔刷和画笔不会自行重新读取RBG值。

using System.Drawing;
using System.Reflection;

namespace Example
{
    public class SystemColorsUtility
    {
        public SystemColorsUtility()
        {
            // force init color table
            byte unused = SystemColors.Window.R;

            var colorTableField = typeof(Color).Assembly.GetType("System.Drawing.KnownColorTable")
                .GetField("colorTable", BindingFlags.Static | BindingFlags.NonPublic);

            _colorTable = (int[]) colorTableField.GetValue(null);

            _threadDataProperty = systemDrawingAssembly.GetType("System.Drawing.SafeNativeMethods")
                .GetNestedType("Gdip", BindingFlags.NonPublic)
                .GetProperty("ThreadData", BindingFlags.Static | BindingFlags.NonPublic);

            SystemBrushesKey = typeof(SystemBrushes)
                .GetField("SystemBrushesKey", BindingFlags.Static | BindingFlags.NonPublic)
                .GetValue(null);

            SystemPensKey = typeof(SystemPens)
                .GetField("SystemPensKey", BindingFlags.Static | BindingFlags.NonPublic)
                .GetValue(null);
        }

        public void SetColor(KnownColor knownColor, Color value)
        {
            _colorTable[(int) knownColor] = value.ToArgb();
            ThreadData[SystemBrushesKey] = null;
            ThreadData[SystemPensKey] = null;
        }

        private int[] _colorTable;
        private object SystemBrushesKey { get; }
        private object SystemPensKey { get; }
    }
}

这是我在我的业余项目中实现的更完整的示例

系统颜色应该在应用程序加载之前更改,以确保控件在初始化时看到更改后的值。对于直接使用Win32 API渲染自身的控件(例如ListViewTreeView),您需要使用其他技巧。

您可以在此处阅读详细信息。这是从俄语进行的机器翻译。


将上述方法应用于GitExtensions时,我发现通过钩取Win32API调用GetSysColorGetSysColorBrush方法可以获得更好的结果。使用EasyHook库进行钩取。 - Nikolay Hidalgo Diaz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接