高级 UxTheme 包装器






4.92/5 (39投票s)
如何使用和理解用于通过 C# 包装器 (uxtheme.dll) 绘制的自定义控件的视觉样式
下载次数
- 下载演示 - 117.0 KB (请确保已安装示例主题)
- 下载源代码 - 361.1 KB (请确保已安装示例主题)
- 示例主题 - 1,902.9 KB
使用自定义控件(按钮、复选框、单选按钮、进度条等)绘制的示例应用程序,这些控件是通过包装器绘制的。
引言
我加入 CodeProject 已经很久了。我学到了很多,是时候分享我的知识了。
我想这也不是你第一次阅读关于 uxtheme 包装器的文章了。和我一样,你可能已经在 Google 上搜索了大量关于 uxtheme(也称为 Visual Styles 或 Windows themes)的信息和 C# 示例,但找不到符合你需求的内容。
然而,CodeProject 上有很多优秀的相關文章,例如 Don Kackman 的 "A Managed C++ Wrapper Around the Windows XP Theme API",或者 David Zhao 的 "Add XP Visual Style Support to OWNERDRAW Controls" 和 Mathew Hall 的 "Themed Windows XP style Explorer Bar",它们描述了视觉样式并提供了有用的技巧。但是,在我看来,它们并非为通用的 C# 使用而设计,并且没有提供一种简单的方法在我们的应用程序中使用视觉样式。
我想有一种方法来枚举和切换计算机上可用的主题,所以我决定自己做一个 C# 包装器。
本文
那么,它是如何工作的呢?
我将本文分为三个部分:
- 简要介绍 uxtheme,
- 视觉样式格式,你将从中了解 .theme 和 .msstyles 文件隐藏的内容,
- 以及包装器设计,我将在此描述如何使用它。
背景需求
以下是我希望在包装器中找到的功能:
- Windows XP 主题支持 - 即使在 XP 之前的版本上也能支持,
- 一个简单通用的 C# 包装器,
- 枚举计算机上可用的/已安装的主题,
- 分析和解密 .msstyles 文件,以便按我想要的方式使用它们,
- 使用主题数据切换自定义组件的外观和感觉,
- 提供一种在自定义控件之间共享主题数据的方法。
特点
您将在本示例中找到什么
- 主题支持,即使对于 XP 之前的版本(正常),
- 一个“即用型”C# uxtheme 包装器(包含大量注释),
- 一种获取当前使用的主题信息的方法,
- 一种枚举你计算机上视觉样式的方法,
- 一种获取视觉样式所有信息的方法,
- 一种保存 .msstyles 文件中嵌入的 .ini 文件的方法,
- 一种保存 .msstyles 文件中嵌入的位图的方法,
- 一个特定的 PE 文件读取器,将帮助你提取主题数据,
- 自定义控件的示例,以及它们各自的渲染器实现。
你还将找到自定义设计器/编辑器,如果你想学习如何创建自己的设计时支持。
一、视觉样式和 UxTheme.dll
我不会解释 uxtheme.dll 是如何工作的,因为本文篇幅有限(而且我也不想重写 msdn)。
有趣的是如何编写 C# 包装器。原理很简单:找到你想要处理的 dll(或模块),查找其函数的签名,然后以 C# 的等价形式编写它们。
uxtheme 函数的签名定义在 uxtheme.h 文件中,枚举/常量定义在 tmschema.h 文件中。如果你使用 VS 2003,这些文件位于 "C:\Program Files\Microsoft Visual Studio .NET 2003\Vc7\PlatformSDK\Include" 目录中。
在 uxtheme.h 中,你会找到类似这样的内容:
//-----------------------------------------------------------------------
// DrawThemeText() - draws the text using the theme-specified
// color and font for the "iPartId" and
// "iStateId".
//
// hTheme - theme data handle
// hdc - HDC to draw into
// iPartId - part number to draw
// iStateId - state number (of the part) to draw
// pszText - actual text to draw
// dwCharCount - number of chars to draw (-1 for all)
// dwTextFlags - same as DrawText() "uFormat" param
// dwTextFlags2 - additional drawing options
// pRect - defines the size/location of the part
//-----------------------------------------------------------------------
THEMEAPI DrawThemeText(HTHEME hTheme, HDC hdc, int iPartId,
int iStateId, LPCWSTR pszText, int iCharCount, DWORD dwTextFlags,
DWORD dwTextFlags2, const RECT *pRect);
下一步是编写 C# 等价方法。例如,DrawThemeText()
函数可以翻译为:
/// <summary>
/// Draws text using the color and font defined by the visual style.
/// </summary>
/// <param name="hTheme">Handle to a window's specified
/// theme data.</param>
/// <param name="hdc">Handle to a device context (HDC) used for
/// drawing the theme-defined background image.</param>
/// <param name="iPartId">Value that specifies the part to draw.</param>
/// <param name="iStateId">Value that specifies the state of the
/// part to draw.</param>
/// <param name="pszText">Pointer to a string that contains the text
/// to draw.</param>
/// <param name="iCharCount">Value that contains the number of
/// characters to draw. If the parameter is set to -1, all
/// the characters in the string are drawn.</param>
/// <param name="dwTextFlags">A bitwise combination
/// of <see cref="TextFormatFlags"/> values to specify the text
/// formatting.</param>
/// <param name="dwTextFlags2">Not used. Set to 0.</param>
/// <param name="pRect">Pointer to a RECT structure that contains
/// the rectangle, in logical coordinates, in which the text
/// is to be drawn.</param>
[DllImport("UxTheme.dll", CharSet=CharSet.Unicode, SetLastError=true)]
public static extern System.Int32 DrawThemeText(
IntPtr hTheme,
IntPtr hdc,
UInt32 iPartId,
UInt32 iStateId,
String pszText,
Int32 iCharCount,
UInt32 dwTextFlags,
UInt32 dwTextFlags2,
ref RECT pRect
);
翻译函数时,你需要使用 DllImport
属性,并将要导入的模块的名称(或绝对路径)作为第一个参数。
DllImport
属性有许多其他可选参数(请参阅 msdn 上的描述),但你应该始终将 SetLastError
设置为 true
。此参数指示被调用方调用 SetLastError
函数,并告知运行时封送拆收器保留最后一条 win32 错误信息的副本(当你不知道函数调用是否成功时,这非常有用)。
要获取最后一条 win32 错误,你可以使用以下代码示例:
//using System.Runtime.InteropServices;
//using System.ComponentModel;
//...
// call an imported function using SetLastError=true before
// using this piece of code
int errorCode = Marshal.GetLastWin32Error();
Console.WriteLine("The last win32 error code was: "+errorCode);
// depends on the called function's error codes
if(errorCode < 0) throw new Win32Exception(errorCode);
下表提供了 c++ 类型与 C# 之间的快速等价对照:
C++ | C# |
BOOL | System.Boolean 或 bool |
BYTE | byte |
CSize | System.Drawing.Size 或 System.Drawing.SizeF |
CString, LPCWSTR | string 或 System.String |
DWORD | System.UInt32 或 uint |
HBITMAP | System.IntPtr 或 System.Drawing.Bitmap |
HBRUSH | System.IntPtr |
HDC | System.IntPtr 或 System.Drawing.Graphics |
HPEN | System.IntPtr |
LONG | System.Int32 或 int |
WORD | System.UInt16 或 ushort |
大多数其他 c++ 类型可以转换为 System.IntPtr
,但结构体需要用 C# 定义。
二、视觉样式理念
主题可以定义为按“颜色方案”和“大小方案”分组的用户界面属性。然后,通过其颜色和大小方案来检索主题。
这些属性分布在三个文件中:
- `.theme` 文件:你将在其中找到对 `.msstyles` 文件的引用。
- `.msstyles` 文件:这是一个老式的 PE 文件(例如,像 DLL 文件一样),嵌入了 .ini 文件和位图。
- `shellstyle.dll` 文件:它扩展了主题的属性以用于 Explorer(例如,explorer 条位图等)。
主题文件夹树
基本上,可用的主题位于 "%windir%\Resources\Themes" 目录中,如下所示:
每个 .theme 文件都有自己的目录,以及一个同名的 .msstyles 文件(例如,**panther.theme** 在名为 **panther** 的目录中包含其 .msstyles 文件)。
你还会在主题目录中找到一个名为 **shell** 的文件夹,其中包含每个主题方案的文件夹。
最后,方案目录包含一个 shellstyle.dll。你也可以在此目录中找到由 shellstyle.dll 引用的位图(如壁纸)或其他资源。
.theme 文件
`.theme` 文件是一个初始化文件,包含定义许多信息的节和键/值对。以下示例显示了 .theme 文件的内容。
[Theme] ; Recycle Bin [CLSID\{645FF040-5081-101B-9F08-00AA002F954E}\DefaultIcon] full=%SystemRoot%\SYSTEM32\shell32.dll,32 empty=%SystemRoot%\SYSTEM32\shell32.dll,31 [Control Panel\Desktop] Wallpaper=%WinDir%Resources\Themes\Panther\Wallpaper\Aqua_Blue.jpg TileWallpaper=0 WallpaperStyle=2 Pattern= ScreenSaveActive=0 [boot] SCRNSAVE.EXE=%WinDir%system32\logon.scr [VisualStyles] Path=%ResourceDir%\Themes\Panther\Panther.msstyles ColorStyle=NormalColor Size=NormalSize [MasterThemeSelector] MTSM=DABJDKT ThemeColorBPP=4
最有趣的部分是 [VisualStyles]。你将在其中找到三个重要提示:指向 .msstyles 文件的相对路径(Path)、主题使用的颜色方案(ColorStyle)和大小行为(Size)。
注意: 如果你的环境变量中没有定义 %ResourceDir%,你可以使用下面的示例代码。
System.Collections.IDictionary vars =
System.Environment.GetEnvironmentVariables();
// extract the "Path" value
System.Text.StringBuilder val =
new System.Text.StringBuilder(MAX_PATH); // MAX_PATH = 255
Kernel32.GetPrivateProfileString(sectionName, keyName, "", val,
MAX_PATH, iniFile); // MAX_PATH = 255
// remove comments
String path = val.ToString();
if(path.IndexOf(";") != -1) path = path.Substring(0,
result.IndexOf(";"));
path = path.Replace("%WinDir%", @"%windir%\");
path = path.Replace(@"\\", @"\");
path = path.Replace("%ResourceDir%", @"%windir%\Resources");
path = path.Replace("%windir%", Convert.ToString(vars["windir"]));
.msstyles 文件
`.msstyles` 文件基本上是一个 PE 文件(例如,一个 DLL)。我们寻找的是资源部分,位于 TEXTFILE 和 BITMAP 资源目录之下。
注意: 你可以使用像 ResEdit 这样的软件来探索你的 .msstyles 文件。
最重要的文件是 THEME_INI。它为我们提供了:
- 主题的文档属性(例如,Author、DisplayName 等),
- 主题的颜色方案,
- 主题的大小方案,
- 用于提取位图的前缀名称,
- 包含当前颜色方案和大小主题数据的 .ini 文件的名称。
如果我们使用
[VisualStyles] Path=%ResourceDir%\Themes\Panther\Panther.msstyles ColorStyle=NormalColor Size=NormalSize
`.theme` 文件中指定的,我们将会在名为 [File.]["color scheme"]["theme name"] 的节中找到我们的参考提示,例如:
[File.Normalpanther] ColorSchemes = panther Sizes = NormalSize
在我们的示例中,包含主题数据的 .ini 文件名为 NORMALPANTHER_INI,并且前缀值在 ColorSchemes 键中设置(位图的名称将以 PANTHER 开头)。
主题数据初始化文件
这些文件包含每个类和视觉样式的各个部分的信息。
在阅读了许多这类文件后,节的规则似乎是:
- 标准节的格式为 [ClassName].[ClassPart][(ClassPartState)]
- 类节定义了默认的 UI 值。
- 带有状态的类节定义了该状态下的 UI 值。
- 特殊节的格式为 [ClassName]::[ClassName].[ClassPart][(ClassPartState)]
- [ClassPart] 和 [(ClassPartState)] 是可选的。
- 节名称(以及键名称)不区分大小写。
那么,如果你想要复选框数据,你需要在 [Button.Checkbox] 或 [button.checkbox] 节中查找。
shellstyle.dll
Mathew Hall 的文章描述了该文件的内部结构。如果你有兴趣了解它,请查看他在 Code Project 上的文章 "Themed Windows XP style Explorer Bar"。
三、包装器
下图描述了包装器的设计:
简要说明:
UxTheme
类是通用包装器。它被VisualStyleInformation
和VisualStyleRenderer
使用,以获取属性值或使用当前主题的数据绘制特定控件。- 主要类是
VisualStyleInformation
、VisualStyleRenderer
和VisualStyleFile
。VisualStyleInformation
提供对当前主题信息的访问,例如主题的作者或版权。VisualStyleRenderer
用于绘制控件。它调用UxTheme
包装器函数来获取当前视觉样式的数据,并调用VisualStyleFile
来处理其他主题。VisualStyleFile
处理主题数据。你可以使用它来提取位图,或者仅仅是为了获取组件属性。
MemoryIniFile
用于将嵌入在 .msstyles 主题文件中的 .ini 文件映射到内存中。PEFile
用于读取 .msstyles 文件,并访问其资源部分。
我不会列出每个类提供的所有方法,因为这不属于本文的主题。我将重点介绍 VisualStyleFile
及其关联的类/结构。
VisualStyleFile
这个类是最重要的:它的作用是提供一个主题的对象表示。它也是一个可以被你的自定义控件共享的组件。
它通过 .theme 文件检索视觉样式的信息,例如,它检索主题的 .mssstyles,映射使用的 .ini 文件,然后将主题的属性映射到八个结构中:
VisualStyleDocumention
,提供在视觉样式文件中指定的文档的基本信息;VisualStyleMetrics
,提供在视觉样式文件中指定的颜色、字体、大小的基本信息;VisualStyleMetricColors
,用于视觉样式中定义的系统颜色,例如ActiveCaption
;VisualStyleMetricFonts
,用于视觉样式中定义的系统字体,例如CaptionFont
;VisualStyleMetricSizes
,用于视觉样式中定义的系统大小,例如CaptionBarHeight
;
VisualStyleProperties
,映射给定组件(例如,类+部分+状态)的属性;VisualStyleScheme
,映射给定颜色方案的属性;VisualStyleSize
,映射给定大小方案的属性。
所以,如果你想访问一个主题的原始信息/数据,你只需要创建一个新的 VisualStyleFile
并按如下方式获取你想要的内容:
// create a new VisualStyleFile
String themeFile = @"C:\WINDOWS\Resources\Themes\Luna.theme";
using(VisualStyleFile theme = new VisualStyleFile(themeFile))
{
// get the documentation informations
VisualStyleDocumention doc = theme.Documentation;
Console.WriteLine(doc.Author + " - " + doc.Copyright);
// get the color/size scheme
Console.WriteLine("Color scheme: "+theme.ThemeSchemeName);
Console.WriteLine("Size scheme: "+theme.ThemeSizeName);
// get the properties of a component
VisualStyleProperties buttonProps = theme.GetElementProperties(
"BUTTON", (uint)ButtonPart.PushButton);
Console.WriteLine(
"Button is transparent : "+buttonProps.Transparent);
Console.WriteLine("Background image: "+buttonProps.ImageFile);
// etc...
}
使用代码
我知道,这有点繁琐。但好消息是,你读到的所有内容都已在本示例中实现。我将这一部分作为 FAQ 来构建,也许你会找到你想知道的。
如何枚举已安装的主题文件
String[] themes = VisualStyleInformation.GetThemeFiles();
foreach(String theme in themes)
{
Console.WriteLine(theme);
}
如何获取主题信息
如果你想要当前视觉样式的信息:
// Documentation properties
Console.WriteLine(
"Current theme file: "+VisualStyleInformation.CurrentThemeFileName);
Console.WriteLine(
"Application themed? "+VisualStyleInformation.IsApplicationThemed);
Console.WriteLine("Current theme author: "+VisualStyleInformation.Author);
Console.WriteLine("Current theme company: "+VisualStyleInformation.Company);
// etc...
// Theme raw properties for a "Button"
VisualStyleRenderer renderer = VisualStyleRenderer("BUTTON",
(uint)ButtonPart.PushButton, (uint)PushButtonState.Normal);
// bool properties
bool isButtonTransparent = renderer.GetBoolean(BooleanProperty.Transparent);
bool isBacgroundFilled = renderer.GetBoolean(BooleanProperty.BackgroundFill);
// Color properties
Color borderColor = renderer.GetColor(ColorProperty.BorderColor);
Color fillColor = renderer.GetColor(ColorProperty.FillColor);
Color textColor = renderer.GetColor(ColorProperty.TextColor);
// String properties
String backgroundImage = GetFilename(FilenameProperty.ImageFile);
String glyph = GetFilename(FilenameProperty.GlyphImageFile);// for combo or
// caption button
// etc...
如果你想要特定主题(非当前主题)的属性:
String themeFile = @"C:\WINDOWS\Resources\Themes\Luna.theme";
using(VisualStyleFile theme = new VisualStyleFile(themeFile))
{
// get the documentation informations
VisualStyleDocumention doc = theme.Documentation;
Console.WriteLine(doc.Author + " - " + doc.Copyright);
// get the color/size scheme
Console.WriteLine("Color scheme: "+theme.ThemeSchemeName);
Console.WriteLine("Size scheme: "+theme.ThemeSizeName);
// get the properties of a component
VisualStyleProperties buttonProps = theme.GetElementProperties("BUTTON",
(uint)ButtonPart.PushButton);
Console.WriteLine("Button is transparent : "+buttonProps.Transparent);
Console.WriteLine("Background image: "+buttonProps.ImageFile);
// etc...
}
注意: 即使是当前主题,我也更倾向于第二种解决方案。
如何提取主题的 .ini 文件
// save the .ini files of a specific theme
String themeFile = @"C:\WINDOWS\Resources\Themes\Luna.theme";
String savePath = Environment.CurrentDirectory + @"\inifiles\";
using(VisualStyleFile theme = new VisualStyleFile(themeFile))
{
theme.SaveIniFiles(savePath);
}
如何获取主题的位图
// gets an identified bitmap
String themeFile = @"C:\WINDOWS\Resources\Themes\Luna.theme";
String savePath = Environment.CurrentDirectory;
using(VisualStyleFile theme = new VisualStyleFile(themeFile))
{
// gets the properties
VisualStyleProperties buttonProps = theme.GetElementProperties("BUTTON",
(uint)ButtonPart.PushButton);
// save the bitmap to disk
using(Bitmap bmp = theme.GetBitmap(buttonProps.ImageFile))
{
bmp.Save(savePath+@"\buttonBitmaps.bmp");
}
}
如何与你的自定义控件共享一个主题
如何使用(非当前)主题的数据与你的自定义控件?
你只需通过设计器将 VisualStyleFile
添加到你的窗体,并更新/设置其 ThemeFile
属性。
然后,将你的自定义控件的 VisualStyleFile
属性设置为新的 VisualStyleFile
。
如何创建你自己的自定义渲染器
实现取决于你:它取决于你想绘制什么(例如,一个列表视图的标题、一个按钮、一个滚动条、一个标题栏等)。技巧是清楚地将绘制行为与组件逻辑分开。我的观点是,创建你的自定义控件作为“bean”,它计算正确的值(边界、大小等),并将它们作为参数传递给你的渲染器方法。
让我们做一个 15 分钟的教程:为窗口的标题按钮创建一个渲染器。
- 第一步是查看 uxtheme 的文件,其中包含窗口按钮的列表及其可能的各种状态:你将找到
WindowPart
和WindowButtonState
枚举。
WindowPart
包含很多值,所以最好的方法是只使用窗口按钮的值,创建一个像这样的枚举:
public enum WindowButtonType : int
{
CloseButton = (int)WindowPart.CloseButton,
MaxButton = (int)WindowPart.MaxButton,
MinButton = (int)WindowPart.MinButton,
HelpButton = (int)WindowPart.HelpButton,
RestoreButton = (int)WindowPart.RestoreButton,
SysButton = (int)WindowPart.SysButton
};
- 创建一个基本的渲染器,复制其中一个示例渲染器(例如
RadioButtonRenderer
),然后删除不必要的方法。
/// <summary>
/// Provides methods for drawing a WindowButton control (eg. Help, Close,
/// Minize, etc.).
/// </summary>
public sealed class WindowButtonRenderer
{
/// <summary>
/// Gets a value indicating whether the WindowButtonRenderer class
/// can be used to draw a window button control with visual styles.
/// </summary>
public static bool IsSupported
{
get
{
return VisualStyleInformation.IsApplicationThemed;
}
}
private WindowButtonRenderer(){ }
// ...
}
- 在其中编写简单的将被你的组件调用的方法(
DrawButton
方法),并记住你的渲染器将使用一个Graphics
对象,并且需要你的组件边界和类型。
/// <summary>
/// Provides methods for drawing a WindowButton control (eg. Help, Close,
/// Minize, etc.).
/// </summary>
public sealed class WindowButtonRenderer
{
/// <summary>
/// Gets a value indicating whether the WindowButtonRenderer class
/// can be used to draw a window button control with visual styles.
/// </summary>
public static bool IsSupported
{
get
{
return VisualStyleInformation.IsApplicationThemed;
}
}
private WindowButtonRenderer(){ }
#region Methods
#region Misc
/// <summary>
/// Gets a VisualStyleRenderer for the specified button state.
/// </summary>
/// <param name="button">The button type.</param>
/// <param name="state">The button state.</param>
private static VisualStyleRenderer GetButtonRenderer(
WindowButtonType button, WindowButtonState state)
{
switch(state)
{
case WindowButtonState.Normal:
if(button == WindowButtonType.CloseButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.CloseButton.Normal);
else if(button == WindowButtonType.MaxButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.MaxButton.Normal);
else if(button == WindowButtonType.MinButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.MinButton.Normal);
else if(button == WindowButtonType.HelpButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.HelpButton.Normal);
else return new VisualStyleRenderer(
VisualStyleElement.Window.RestoreButton.Normal);
case WindowButtonState.Hot:
if(button == WindowButtonType.CloseButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.CloseButton.Hot);
else if(button == WindowButtonType.MaxButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.MaxButton.Hot);
else if(button == WindowButtonType.MinButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.MinButton.Hot);
else if(button == WindowButtonType.HelpButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.HelpButton.Hot);
else return new VisualStyleRenderer(
VisualStyleElement.Window.RestoreButton.Hot);
case WindowButtonState.Pushed:
if(button == WindowButtonType.CloseButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.CloseButton.Pressed);
else if(button == WindowButtonType.MaxButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.MaxButton.Pressed);
else if(button == WindowButtonType.MinButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.MinButton.Pressed);
else if(button == WindowButtonType.HelpButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.HelpButton.Pressed);
else return new VisualStyleRenderer(
VisualStyleElement.Window.RestoreButton.Pressed);
case WindowButtonState.Disabled:
default:
if(button == WindowButtonType.CloseButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.CloseButton.Disabled);
else if(button == WindowButtonType.MaxButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.MaxButton.Disabled);
else if(button == WindowButtonType.MinButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.MinButton.Disabled);
else if(button == WindowButtonType.HelpButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.HelpButton.Disabled);
else return new VisualStyleRenderer(
VisualStyleElement.Window.RestoreButton.Disabled);
}
}
/// <summary>
/// Gets a <see cref="VisualStyleRenderer"> for the specified
/// button state.
/// </summary>
/// <param name="style">The visual style file to use.</param>
/// <param name="button">The button type.</param>
/// <param name="state">The button state.</param>
private static VisualStyleRenderer GetButtonRenderer(
VisualStyleFile style, WindowButtonType button,
WindowButtonState state)
{
if(button == WindowButtonType.CloseButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.CloseButton.GetElement(
style, state));
else if(button == WindowButtonType.MaxButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.MaxButton.GetElement(
style, state));
else if(button == WindowButtonType.MinButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.MinButton.GetElement(
style, state));
else if(button == WindowButtonType.HelpButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.HelpButton.GetElement(
style, state));
else return new VisualStyleRenderer(
VisualStyleElement.Window.RestoreButton.GetElement(
style, state));
}
#endregion
#region Drawing
public static void DrawButton(Graphics g, Rectangle bounds,
WindowButtonType button, WindowButtonState state)
{
if(!IsSupported) throw new InvalidOperationException();
VisualStyleRenderer renderer = GetButtonRenderer(button, state);
if(renderer != null) renderer.DrawBackground(g, bounds);
}
/// <summary>
/// Draws a window Close button control in the specified state
/// and bounds.
/// </summary>
/// <param name="g">The Graphics used to draw the button.</param>
/// <param name="bounds">The button bounds.</param>
/// <param name="state">One of the WindowButtonState values that
/// specifies the visual state of the button.</param>
public static void DrawCloseButton(Graphics g, Rectangle bounds,
WindowButtonState state)
{
DrawButton(g, bounds, WindowButtonType.CloseButton, state);
}
/// <summary>
/// Draws a window Help button control in the specified state and bounds.
/// </summary>
/// <param name="g">The Graphics used to draw the button.</param>
/// <param name="bounds">The button bounds.</param>
/// <param name="state">One of the WindowButtonState values that
/// specifies the visual state of the button.</param>
public static void DrawHelpButton(Graphics g, Rectangle bounds,
WindowButtonState state)
{
DrawButton(g, bounds, WindowButtonType.HelpButton, state);
}
/// <summary>
/// Draws a window Minize button control in the specified state
/// and bounds.
/// </summary>
/// <param name="g">The Graphics used to draw the button.</param>
/// <param name="bounds">The button bounds.</param>
/// <param name="state">One of the WindowButtonState values that
/// specifies the visual state of the button.</param>
public static void DrawMinizeButton(Graphics g, Rectangle bounds,
WindowButtonState state)
{
DrawButton(g, bounds, WindowButtonType.MinButton, state);
}
/// <summary>
/// Draws a window Maximize button control in the specified state
/// and bounds.
/// </summary>
/// <param name="g">The Graphics used to draw the button.</param>
/// <param name="bounds">The button bounds.</param>
/// <param name="state">One of the WindowButtonState values that
/// specifies the visual state of the button.</param>
public static void DrawMaximizeButton(Graphics g, Rectangle bounds,
WindowButtonState state)
{
DrawButton(g, bounds, WindowButtonType.MaxButton, state);
}
/// <summary>
/// Draws a window Restore button control in the specified state
/// and bounds.
/// </summary>
/// <param name="g">The Graphics used to draw the button.</param>
/// <param name="bounds">The button bounds.</param>
/// <param name="state">One of the WindowButtonState values that
/// specifies the visual state of the button.</param>
public static void DrawRestoreButton(Graphics g, Rectangle bounds,
WindowButtonState state)
{
DrawButton(g, bounds, WindowButtonType.RestoreButton, state);
}
#endregion
#endregion
}
</see>
- 为你的渲染器添加
VisualStyleFile
支持。
/// <summary>
/// Provides methods for drawing a WindowButton control (eg. Help, Close,
/// Minize, etc.).
/// </summary>
public sealed class WindowButtonRenderer
{
/// <summary>
/// Gets a value indicating whether the WindowButtonRenderer class
/// can be used to draw a window button control with visual styles.
/// </summary>
public static bool IsSupported
{
get
{
return VisualStyleInformation.IsApplicationThemed;
}
}
private WindowButtonRenderer(){ }
#region Methods
#region Misc
/// <summary>
/// Gets a VisualStyleRenderer for the specified button state.
/// </summary>
/// <param name="button">The button type.</param>
/// <param name="state">The button state.</param>
private static VisualStyleRenderer GetButtonRenderer(
WindowButtonType button, WindowButtonState state)
{
switch(state)
{
case WindowButtonState.Normal:
if(button == WindowButtonType.CloseButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.CloseButton.Normal);
else if(button == WindowButtonType.MaxButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.MaxButton.Normal);
else if(button == WindowButtonType.MinButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.MinButton.Normal);
else if(button == WindowButtonType.HelpButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.HelpButton.Normal);
else return new VisualStyleRenderer(
VisualStyleElement.Window.RestoreButton.Normal);
case WindowButtonState.Hot:
if(button == WindowButtonType.CloseButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.CloseButton.Hot);
else if(button == WindowButtonType.MaxButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.MaxButton.Hot);
else if(button == WindowButtonType.MinButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.MinButton.Hot);
else if(button == WindowButtonType.HelpButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.HelpButton.Hot);
else return new VisualStyleRenderer(
VisualStyleElement.Window.RestoreButton.Hot);
case WindowButtonState.Pushed:
if(button == WindowButtonType.CloseButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.CloseButton.Pressed);
else if(button == WindowButtonType.MaxButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.MaxButton.Pressed);
else if(button == WindowButtonType.MinButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.MinButton.Pressed);
else if(button == WindowButtonType.HelpButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.HelpButton.Pressed);
else return new VisualStyleRenderer(
VisualStyleElement.Window.RestoreButton.Pressed);
case WindowButtonState.Disabled:
default:
if(button == WindowButtonType.CloseButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.CloseButton.Disabled);
else if(button == WindowButtonType.MaxButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.MaxButton.Disabled);
else if(button == WindowButtonType.MinButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.MinButton.Disabled);
else if(button == WindowButtonType.HelpButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.HelpButton.Disabled);
else return new VisualStyleRenderer(
VisualStyleElement.Window.RestoreButton.Disabled);
}
}
/// <summary>
/// Gets a <see cref="VisualStyleRenderer"> for the specified
/// button state.
/// </summary>
/// <param name="style">The visual style file to use.</param>
/// <param name="button">The button type.</param>
/// <param name="state">The button state.</param>
private static VisualStyleRenderer GetButtonRenderer(
VisualStyleFile style, WindowButtonType button,
WindowButtonState state)
{
if(button == WindowButtonType.CloseButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.CloseButton.GetElement(
style, state));
else if(button == WindowButtonType.MaxButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.MaxButton.GetElement(
style, state));
else if(button == WindowButtonType.MinButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.MinButton.GetElement(style,state));
else if(button == WindowButtonType.HelpButton)
return new VisualStyleRenderer(
VisualStyleElement.Window.HelpButton.GetElement(style,state));
else return new VisualStyleRenderer(
VisualStyleElement.Window.RestoreButton.GetElement(style,state));
}
#endregion
#region Drawing
public static void DrawButton(Graphics g, Rectangle bounds,
WindowButtonType button, WindowButtonState state)
{
if(!IsSupported) throw new InvalidOperationException();
VisualStyleRenderer renderer = GetButtonRenderer(button, state);
if(renderer != null) renderer.DrawBackground(g, bounds);
}
public static void DrawButton(VisualStyleFile style, Graphics g,
Rectangle bounds, WindowButtonType button, WindowButtonState state)
{
if(!IsSupported) throw new InvalidOperationException();
VisualStyleRenderer renderer = GetButtonRenderer(style, button,
state);
if(renderer != null) renderer.DrawBackground(g, bounds);
}
/// <summary>
/// Draws a window Close button control in the specified state
/// and bounds.
/// </summary>
/// <param name="g">The Graphics used to draw the button.</param>
/// <param name="bounds">The button bounds.</param>
/// <param name="state">One of the WindowButtonState values that
/// specifies the visual state of the button.</param>
public static void DrawCloseButton(Graphics g, Rectangle bounds,
WindowButtonState state)
{
DrawButton(g, bounds, WindowButtonType.CloseButton, state);
}
/// <summary>
/// Draws a window Close button control in the specified state
/// and bounds.
/// </summary>
/// <param name="style">The visual style file to use.</param>
/// <param name="g">The Graphics used to draw the button.</param>
/// <param name="bounds">The button bounds.</param>
/// <param name="state">One of the WindowButtonState values that
//// specifies the visual state of the button.</param>
public static void DrawCloseButton(VisualStyleFile style, Graphics g,
Rectangle bounds, WindowButtonState state)
{
DrawButton(style, g, bounds, WindowButtonType.CloseButton, state);
}
/// <summary>
/// Draws a window Help button control in the specified state and bounds.
/// </summary>
/// <param name="g">The Graphics used to draw the button.</param>
/// <param name="bounds">The button bounds.</param>
/// <param name="state">One of the WindowButtonState values that
/// specifies the visual state of the button.</param>
public static void DrawHelpButton(Graphics g, Rectangle bounds,
WindowButtonState state)
{
DrawButton(g, bounds, WindowButtonType.HelpButton, state);
}
/// <summary>
/// Draws a window Help button control in the specified state and bounds.
/// </summary>
/// <param name="style">The visual style file to use.</param>
/// <param name="g">The Graphics used to draw the button.</param>
/// <param name="bounds">The button bounds.</param>
/// <param name="state">One of the WindowButtonState values that
/// specifies the visual state of the button.</param>
public static void DrawHelpButton(VisualStyleFile style, Graphics g,
Rectangle bounds, WindowButtonState state)
{
DrawButton(style, g, bounds, WindowButtonType.HelpButton, state);
}
/// <summary>
/// Draws a window Minize button control in the specified
/// state and bounds.
/// </summary>
/// <param name="g">The Graphics used to draw the button.</param>
/// <param name="bounds">The button bounds.</param>
/// <param name="state">One of the WindowButtonState values that
/// specifies the visual state of the button.</param>
public static void DrawMinizeButton(Graphics g, Rectangle bounds,
WindowButtonState state)
{
DrawButton(g, bounds, WindowButtonType.MinButton, state);
}
/// <summary>
/// Draws a window Minize button control in the specified state
/// and bounds.
/// </summary>
/// <param name="style">The visual style file to use.</param>
/// <param name="g">The Graphics used to draw the button.</param>
/// <param name="bounds">The button bounds.</param>
/// <param name="state">One of the WindowButtonState values that
/// specifies the visual state of the button.</param>
public static void DrawMinizeButton(VisualStyleFile style, Graphics g,
Rectangle bounds, WindowButtonState state)
{
DrawButton(style, g, bounds, WindowButtonType.MinButton, state);
}
/// <summary>
/// Draws a window Maximize button control in the specified state
/// and bounds.
/// </summary>
/// <param name="g">The Graphics used to draw the button.</param>
/// <param name="bounds">The button bounds.</param>
/// <param name="state">One of the WindowButtonState values that
/// specifies the visual state of the button.</param>
public static void DrawMaximizeButton(Graphics g, Rectangle bounds,
WindowButtonState state)
{
DrawButton(g, bounds, WindowButtonType.MaxButton, state);
}
/// <summary>
/// Draws a window Maximize button control in the specified state and
/// bounds.
/// </summary>
/// <param name="style">The visual style file to use.</param>
/// <param name="g">The Graphics used to draw the button.</param>
/// <param name="bounds">The button bounds.</param>
/// <param name="state">One of the WindowButtonState values that
/// specifies the visual state of the button.</param>
public static void DrawMaximizeButton(VisualStyleFile style, Graphics g,
Rectangle bounds, WindowButtonState state)
{
DrawButton(style, g, bounds, WindowButtonType.MaxButton, state);
}
/// <summary>
/// Draws a window Restore button control in the specified
/// state and bounds.
/// </summary>
/// <param name="g">The Graphics used to draw the button.</param>
/// <param name="bounds">The button bounds.</param>
/// <param name="state">One of the WindowButtonState values that
/// specifies the visual state of the button.</param>
public static void DrawRestoreButton(Graphics g, Rectangle bounds,
WindowButtonState state)
{
DrawButton(g, bounds, WindowButtonType.RestoreButton, state);
}
/// <summary>
/// Draws a window Restore button control in the specified
//// state and bounds.
/// </summary>
/// <param name="style">The visual style file to use.</param>
/// <param name="g">The Graphics used to draw the button.</param>
/// <param name="bounds">The button bounds.</param>
/// <param name="state">One of the WindowButtonState values that
/// specifies the visual state of the button.</param>
public static void DrawRestoreButton(VisualStyleFile style, Graphics g,
Rectangle bounds, WindowButtonState state)
{
DrawButton(style, g, bounds, WindowButtonType.RestoreButton, state);
}
#endregion
#endregion
}
</see>
- 创建你的
WindowButton
组件,就像渲染器一样(例如,复制CustomRadioButton
组件,然后删除不必要的字段和方法)。
6 - 清理后,添加你的字段,如WindowButtonState
和WindowButtonType
,并在OnPaint()
事件中用你全新的WindowButtonRenderer
替换渲染器。
7 - 添加你自己的组件行为,最后,你将得到类似下面的示例代码:
public class WindowButton : System.Windows.Forms.Control,
IVisualStyleSwitchable
{
/// <summary>Event fired when a control's property changes.</summary>
[Category("Action"), Description(
"Occurs when a control's property changes."),]
public event EventHandler PropertyChanged = null;
#region Fields
private System.ComponentModel.IContainer components = null;
private WindowButtonType type = WindowButtonType.CloseButton;
private WindowButtonState state = WindowButtonState.Normal;
private Color backColor;
private Rectangle realBounds = Rectangle.Empty;
private Size realSize = Size.Empty;
private VisualStyleFile file = null;
#endregion
#region Accessors
#region Runtime
/// <summary>
/// Gets or sets the button state.
/// </summary>
private WindowButtonState State
{
get
{
return this.state;
}
set
{
if(this.state == value) return;
this.state = value;
OnPropertyChanged();
}
}
/// <summary>
/// Get the control real size.
/// </summary>
[Browsable(false),]
[DesignerSerializationVisibility(
DesignerSerializationVisibility.Hidden),]
private Size RealSize
{
get
{
if(this.realSize==Size.Empty) realSize = Size;
return this.realSize;
}
set
{
this.realSize = value;
}
}
/// <summary>
/// Get the control bound's rectangle.
/// </summary>
[Browsable(false),]
[DesignerSerializationVisibility(
DesignerSerializationVisibility.Hidden),]
public new Rectangle ClientRectangle
{
get
{
if(this.realBounds.Width != RealSize.Width ||
this.realBounds.Height != RealSize.Height)
{
this.realBounds = new Rectangle(0, 0, RealSize.Width,
RealSize.Height);
}
return this.realBounds;
}
}
#endregion
#region Appearance
/// <summary>
/// Gets or sets the visual style to use.
/// </summary>
[Browsable(true), Category("Appearance"),]
[DefaultValue(null),]
public Devcorp.Controls.VisualStyles.VisualStyleFile VisualStyle
{
get
{
return this.file;
}
set
{
if(this.file == value) return;
if(this.file != null) this.file.ThemeFileChanged -=
new EventHandler(file_ThemeFileChanged);
this.file = value;
if(this.file != null) this.file.ThemeFileChanged +=
new EventHandler(file_ThemeFileChanged);
OnPropertyChanged();
}
}
/// <summary>
/// Gets or sets the visual style to use.
/// </summary>
[Browsable(true), Category("Appearance"),]
[DefaultValue(typeof(WindowButtonType),"CloseButton"),]
public WindowButtonType Type
{
get
{
return this.type;
}
set
{
if(this.type == value) return;
this.type = value;
OnPropertyChanged();
}
}
/// <summary>
/// Gets or sets the background color.
/// </summary>
[Browsable(true), Category("Appearance"),]
[DefaultValue(typeof(Color), "Control"),]
public new Color BackColor
{
get
{
return this.backColor;
}
set
{
if(this.backColor == value) return;
this.backColor = value;
base.BackColor = value;
OnPropertyChanged();
}
}
#endregion
#endregion
#region Constructor(s)
/// <summary>
/// Default constructor.
/// </summary>
public WindowButton()
{
SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.DoubleBuffer, true);
SetStyle(ControlStyles.ResizeRedraw, true);
SetStyle(ControlStyles.SupportsTransparentBackColor, true);
SetStyle(ControlStyles.Selectable, true);
InitializeComponent();
}
/// <summary>
/// Default constructor.
/// </summary>
public WindowButton(System.ComponentModel.IContainer container) : this()
{
container.Add(this);
}
#endregion
#region Methods
#region VS generated code
/// <summary>Clean up any resources being used.</summary>
protected override void Dispose( bool disposing )
{
if( disposing )
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
//
// WindowButton
//
this.Size = new System.Drawing.Size(20, 20);
}
#endregion
#region Drawing
/// <summary>
/// Handles Onpaint event.
/// </summary>
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
if(!Disposing && !Parent.Disposing)
{
if(this.file != null && this.file.StyleFile!=String.Empty)
{
WindowButtonRenderer.DrawButton(this.file, e.Graphics,
ClientRectangle, this.type, this.state);
}
else
{
WindowButtonRenderer.DrawButton(e.Graphics,
ClientRectangle, this.type, this.state);
}
}
}
#endregion
#region Events
private void file_ThemeFileChanged(object sender, EventArgs e)
{
Invalidate();
}
/// <summary>
/// Fires the PropertyChange event.
/// </summary>
protected virtual void OnPropertyChanged()
{
if(PropertyChanged!=null) PropertyChanged(this, EventArgs.Empty);
Invalidate();
}
/// <summary>
/// Handles WM messages to set the right button states.
/// </summary>
/// <param name="m"></param>
protected override void WndProc(ref Message m)
{
base.WndProc(ref m);
if(m.HWnd == Handle)
{
switch(m.Msg)
{
case (int)Messages.WM_NCCALCSIZE:
if(m.WParam==IntPtr.Zero || m.WParam==new IntPtr(1))
{
NCCALCSIZE_PARAMS csp = (
NCCALCSIZE_PARAMS)Marshal.PtrToStructure(
m.LParam, typeof(NCCALCSIZE_PARAMS));
RealSize = new Size((csp.rgrc1.Right-csp.rgrc1.Left),
(csp.rgrc1.Bottom-csp.rgrc1.Top));
Marshal.StructureToPtr(csp, m.LParam, false );
}
break;
case (int) Messages.WM_CREATE:
this.state = (Enabled)? WindowButtonState.Normal:
WindowButtonState.Disabled;
break;
case (int)Messages.WM_LBUTTONDBLCLK:
case (int)Messages.WM_LBUTTONDOWN:
if(Enabled)
{
if(!Focused) Focus();
State = WindowButtonState.Pushed;
}
break;
case (int)Messages.WM_LBUTTONUP:
if(Enabled)
{
State = WindowButtonState.Hot;
}
break;
case (int)Messages.WM_MOUSEHOVER:
case (int)Messages.WM_MOUSEMOVE:
if(Enabled)
{
if(this.state != WindowButtonState.Pushed) State =
WindowButtonState.Hot;
}
break;
case (int)Messages.WM_MOUSELEAVE:
case (int)Messages.WM_KILLFOCUS:
if(Enabled)
{
State = WindowButtonState.Normal;
}
break;
case (int)Messages.WM_SETFOCUS:
case (int)Messages.WM_ENABLE:
if(Enabled)
{
State = WindowButtonState.Normal;
}
else
{
State = WindowButtonState.Disabled;
}
break;
}
}
}
#endregion
#endregion
}
恭喜!你的自定义 WindowButton
准备就绪!(哦!你应该先把它添加到你的窗体上)
注意: 我在示例中包含了另外四个渲染器(ButtonRenderer
、CheckboxRenderer
、RadioButtonRenderer
和 ProgressBarRenderer
)。你可以使用它们来创建自己的渲染器(或者问我是否已经实现了你需要的东西)。
已知问题
- 我并没有为所有的视觉样式部分创建渲染器,所以你在自己的实现过程中可能会发现错误(或缺少功能支持)。
- 在
VisualStyleFile
中使用当前活动的 <$> 主题数据可能会导致应用程序崩溃:不要将这类VisualStyleFile
与你的自定义控件关联。 - 在项目中使用了过多的
VisualStyleFile
会导致加载时窗体闪烁(我正在研究一种在所有主题数据都已加载完成后触发事件的方法)。 - 在项目中使用了过多的
VisualStyleFile
会导致内存占用过高(当然,因为我们是在托管环境中)。 - 渲染器的实现相当麻烦:你应该复制/修改示例渲染器来创建你自己的。
如果你发现错误或不一致之处,请不要犹豫告诉我。我会修复它并更新这篇文章。
关注点
也许第一个有趣的点是包装器本身。最复杂的部分是位图的绘制和拉伸操作。你可以查看 VisualStyleRenderer
类中的 DrawBackground()
方法以及 VisualStyleHelper
类中的 StretchBitmap()
方法。
实现一个 PE 文件读取器是一项艰巨的任务。如果你想自己构建一个 PE 文件读取器,可以阅读 PECOFF 格式规范,或者你可以使用/扩展示例中的 PEFile
类。另一个想法是,你可以扩展 PEFile
类来创建一个像 ResEdit 这样的资源编辑器。
在我进行大量重构和性能测试时,我实现了一个快速的 .ini 文件内存映射器。目标是避免处理备份文件(嵌入在 .msstyles 中的 .ini 文件),这涉及到大量的 I/O(并使用 win32 内核函数)。你可以在 MemoryIniFile
和 MemoryIniSection
类中找到它。
另一个有趣的地方是包装器的设计。对于那些仍然使用 .NET 1.1 并将要使用 .NET 2.0(或 3.0)的人来说,这将相当容易,因为他们会发现完全相同的类(但功能较少)。将他们的项目迁移将很简单,只需将命名空间 "Devcorp.Controls.VisualStyles" 的引用重命名为 "System.Windows.Forms.VisualStyles",并复制缺失的类。
历史
- 2007年5月13日 - 版本 0.85
- 移除了
MemoryIniHelper
(冗余重复的方法)。 - 在
VisualStyleRenderer.DrawBackground()
中添加了填充背景(非位图)支持。 - 添加了完整的视觉样式属性继承(参见
VisualStyleProperties
)。 - 添加了
GroupBoxRenderer
。 - 添加了
ComboBoxRenderer
。 - 添加了
TextBoxRenderer
。 - 一些错误修复和少量优化。
- 移除了
- 2007年5月9日 - 仅修正文章中的拼写错误。
- 2007年5月6日 - 版本 0.82
- 修复了在绘制拉伸位图时丢失 1px 宽度/高度的问题。
- 修复了
GetBackgroundContentRectangle()
方法中的VisualStyleFile
支持。 - 在
DrawEdge()
方法中添加了部分VisualStyleFile
支持。 - 添加了
ExplorerBarRenderer
渲染器(及其示例)。 - 添加了
IEBarRenderer
渲染器(及其示例)。 - 一些错误修复。
- 2007年5月1日 - 版本 0.81
- 修复了
MemoryIniFile
中的解析错误:一些包含键/值和注释的行未被正确处理。 - 修复了带有图标的组件的绘制不一致问题。
- 修复了使用超过三个
VisualStyleFile
时的高内存占用问题。 - 添加了对主题大小规则的真正支持。
- 在示例中添加了
WindowButton
和WindowButtonRenderer
。 - 一些错误修复。
- 修复了
- 2007年4月29日 - 版本 0.8(发布于 The Code Project)
- 添加了 .ini 文件内存映射器。
- 添加了对嵌入式主题 .ini 文件的保存支持。
- 将独立的渲染器作为示例添加。
- 进行了大量设计重构和优化。
- 一些错误修复。
- 2005年11月20日 - 初始项目发布。