为自定义 .NET 控件添加 XP 主题






4.76/5 (34投票s)
使用 Windows XP UxTheme API 渲染您自己的主题部分。
背景
这项工作实际上是我白天工作的延伸。我现在正在处理的项目是一个现有应用程序的新 .NET 版本。现有版本是标准的 MFC 应用程序,具有所有熟悉的 Windows 95 时代的 GUI 习语。作为新版本的一部分,我们正在尝试为用户界面进行一次急需的更新。
这项工作的一部分是希望在应用程序的所有部分都符合 Windows XP 用户界面样式和指南。
用 .NET 编写用户界面的一大优点是控件的创建和连接非常方便。与 MFC 相比,MFC 有其消息映射、SubclassDlgItem
和 WndProcs
,终于可以专注于用户的需求而不是编译器的需求了。
尽管我更喜欢这种创建强大用户界面功能的新方法,但我确实觉得 .NET 框架的 System.Windows.Form
命名空间有一个明显的疏漏:对主题的支持。
当然,Button
类会选择正确的样式,并且许多其他控件也会采用主题外观,但正如Code Project 上的其他文章所指出的那样,.NET 控件对主题的支持仅是表面上的。
特别是如果您正在创建自己的控件,而这些控件需要定义自己的图形功能,这一点尤其明显。
为了方便访问 Windows XP 的主题 API,我创建了一些 UxTheme DLL 的包装类。这组控件使用了该库。
Using the Code
创建主题感知自定义控件的诀窍在于,您确实必须经历两个绘画过程。一个过程必须在非 XP 操作系统上以及在未启用主题的 XP 上工作。另一个过程必须在 Windows XP 上对应用程序进行主题化时工作。
ExplorerBar
、ExplorerGroup
、SideBar
及其通用基类 ThemeablePanel
控件演示了这种方法。
它们都重写了 OnPaintBackground
和/或 OnPaint
,并根据当前是否启用了主题来决定执行何种渲染。如果没有,它们基本上只调用基类功能,一切正常。如果启用了,它们将使用托管主题 API 来获取它们想要用于其主题表示的正确 ThemePart
对象,并使用它来绘制适当的背景和前景功能。
为了做到这一点,绘画代码会找到正确的主题部分和状态,并使用该对象进行自我渲染。
protected override OnPaint( PaintEventArgs e )
{
ThemeInfo info = new ThemeInfo();
WindowTheme window = info["EXPLORERBAR"];
if ( UxTheme.IsAppThemed && window != null )
{
ThemePart part = window.Parts["HEADERCLOSE"];
part.DrawBackground( e.Graphics,
new Rectangle( 0, 0, Bounds.Width, Bounds.Height );
}
else
base.OnPaint( e );
}
大致就是这样了。
您还会注意到,这些控件通过在其 Paint
事件中进行自定义渲染来接管某些 constituent 控件的渲染。可以再进一步,通过创建一些派生类,让那些 constituent 控件执行自己的主题渲染。随着事情变得更加复杂,这可能是我将要走的路线,但这两种方法都有效。
您还会立即注意到另一件事,即有许多类派生自常见的 .NET 控件,如 Label
和 PictureBox
,名称类似于 ThemeLabel
和 ThemePictureBox
。原因在于,.NET 控件类不喜欢在主题纹理窗口上渲染自身(或者至少我还没有弄清楚如何让它们这样做)。即使设置为透明,在主题背景上渲染时,它们的边缘周围也会有可见的瑕疵。
为了纠正这个问题,您会注意到所有这些控件都有一个重写 OnPaintBackground
的单个方法。
protected override void OnPaintBackground( PaintEventArgs pevent )
{
if ( UxTheme.IsThemeDialogTextureEnabled( this.Parent ) && !DesignMode )
UxTheme.DrawThemeParentBackground( this,
pevent.Graphics, new Rectangle( 0, 0, Width, Height ) );
else
base.OnPaintBackground (pevent);
}
此重写通过渲染父窗口的背景而不是窗口本身来在主题背景上正确地绘制控件。
另一点很重要,用户可以在您的控件生命周期的任何时候更改或禁用主题。因此,最好不要将任何 WindowTheme
、ThemePart
或 ThemePartState
对象保存的时间超过单个绘制操作。这确保了即使环境发生变化,您的控件也能始终正确绘制。如果重新获取这些对象会带来性能问题,请附加到 Microsoft.Win32.SystemEvents.DisplaySettingsChanged
事件,并在那里处理并刷新您的主题对象。
关注点
我最先处理的控件是 ExplorerBar
类。在一个完整的周末工作后(当时我本应在外面享受明尼苏达州美丽的夏日时光),我觉得我差不多搞定了。我一直在标准 XP 蓝色 Luna 主题和非主题版本的 XP 之间切换。所以,只是为了看看它在 Luna 的绿色版本中是什么样子,我切换到了 Olive 配色方案。
猜猜怎么着!我的 ExplorerBar
还是蓝色的。经过几个小时的挠头、挖掘和反复挖掘 API,我开始明白了:Windows Explorer 不使用 UxTheme
来渲染其 ExplorerBar
,尽管主题 API 为该类型的控件定义了部分和状态。结果是 Windows Explorer 使用另一个名为 _ShellStyles.dll_ 的 DLL 来渲染其 ExplorerBar
,显然完全绕过了 UxTheme
。
我的猜测是 UxTheme
并没有真正完全完善,并且它不符合 Windows Explorer 在开发它时的要求,也许现在仍然不符合。
无论如何,这让我们其他人陷入困境,因为我查看过的大多数自定义主题都不重新定义 UxTheme
中定义的 ExplorerBar
主题,而是使用 _VisualStyles.dll_ 中定义的主题。因此,无论您选择什么主题,此库中的 ExplorerBar
都将是蓝色 Luna 版本的外观。
这是我的实现中的一个 bug 吗?我想说,不是,因为 UxTheme
API 存在明显的抽象泄露,而且这确实是它和 Windows Explorer 中的一个 bug。但我的现金储备没有 490 亿美元,所以从用户的角度来看,答案可能确实是肯定的。
不幸的是,解决方法是弄清楚如何从 _VisualStyles.dll_ 中提取位图并在正确的位置进行渲染(如果有人已经完成这项工作并愿意分享代码,我很想看看)。
历史
- 2003 年 8 月 26 日 - 首次发布