Downloads
Sample application using custom controls (Button, CheckBox, RadioButton, ProgressBar, etc.) drawn with the wrapper.
Introduction
It's been a long time since I became a member of The Code Project. I've learned a lot and it's time for me to share my knowledge.
I think this is not the first time you are reading an article on an uxtheme wrapper. Like me, you have probably done a lot of Googling, seeking information and C# samples about uxtheme (also known as Visual Styles or Windows themes) and found nothing that matches what you want.
However, there are many great articles on The Code Project, like "A Managed C++ Wrapper Around the Windows XP Theme API" by Don Kackman or "Add XP Visual Style Support to OWNERDRAW Controls" by David Zhao and "Themed Windows XP style Explorer Bar" by Mathew Hall, which describe visual styles and provide useful tips. But, in my point of view, they were not designed for a generic C# usage and they don't offer a simple way to use visual styles in our applications.
I wanted to have a way to enumerate and switch between available themes on a computer, so I decided to make my own C# wrapper.
The article
So, how does this work?
I've split this artcile in three parts:
- a quick explanation about uxtheme,
- the visual style format in which you'll find what .theme and .msstyles files hide,
- and the wrapper design in which I'll describe how to use it.
Background needs
Here are the features I wanted to find in a wrapper:
- Windows XP theme support - even on pre XP versions,
- A simple and generic C# wrapper,
- Enumerate available/installed themes on a computer,
- Analyse and decrypt .msstyles files to use them as I want,
- Use a theme's data to switch the look and feel of custom components,
- Provide a way to share a theme's data among custom controls.
Features
What you will find in this sample:
- Theme support even for pre XP versions (normal),
- A "ready to use" C# uxtheme wrapper (with many comments),
- A way to get information about the current used theme,
- A way to enumerate visual styles on your computer,
- A way to get all the information about a visual style,
- A way to save .ini files embedded in .msstyles files,
- A way to save bitmaps embedded in .msstyles files,
- A specific PE file reader which will help you to extract a theme's data,
- Samples of custom controls with their own renderer implementation.
You will also find custom Designers/Editors if you want to learn how to create your own design-time support.
I - Visual Style and UxTheme.dll
I will not explain how the uxtheme.dll works, I do not have enough space in this article (an I'm not going to rewrite the msdn).
The interesting thing is how to write a C# wrapper. The principle is quite simple: find the dll (or module) you want to handle, seek its function's signatures, and then write them in a C# equivalent form.
The uxtheme function's signatures are defined in the uxtheme.h file, and enumerations/constants in the tmschema.h file. Those files are located in "C:\Program Files\Microsoft Visual Studio .NET 2003\Vc7\PlatformSDK\Include" directory if you're using VS 2003.
In uxtheme.h, you'll find something like:
THEMEAPI DrawThemeText(HTHEME hTheme, HDC hdc, int iPartId,
int iStateId, LPCWSTR pszText, int iCharCount, DWORD dwTextFlags,
DWORD dwTextFlags2, const RECT *pRect);
The next step is to write the C# equivalent method. For example, the DrawThemeText()
function can be translated as:
[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
);
When translating a function, you need to use the DllImport
attribute setting the name of the module (or the absolute path) you're going to import as the first parameter.
There are many other optional parameters for the DllImport
attribute (look at its description at msdn), but you should always use SetLastError
set to true
. This parameter indicates the callee to call the SetLastError
function and advise the runtime marshaler to keep a copy of the last win32 error (it's very usefull when you don't know if a function call has succeed or not).
To get the last win32 error, you can use the following code sample:
int errorCode = Marshal.GetLastWin32Error();
Console.WriteLine("The last win32 error code was: "+errorCode);
if(errorCode < 0) throw new Win32Exception(errorCode);
The next table provides quick equivalents between c++ types and C# :
C++ |
C# |
BOOL |
System.Boolean or bool |
BYTE |
byte |
CSize |
System.Drawing.Size or System.Drawing.SizeF |
CString, LPCWSTR |
string or System.String |
DWORD |
System.UInt32 or uint |
HBITMAP |
System.IntPtr or System.Drawing.Bitmap |
HBRUSH |
System.IntPtr |
HDC |
System.IntPtr or System.Drawing.Graphics |
HPEN |
System.IntPtr |
LONG |
System.Int32 or int |
WORD |
System.UInt16 or ushort |
Most of the other c++ types can be translated as System.IntPtr
, except for structs which have to be defined in C#.
II - Visual Style philosophy
A Theme can be defined as UI properties grouped by a "Color scheme" and a "Size scheme". Then, a theme is retrieved by its color and size scheme.
Those properties are dispatched into three files:
- the .theme file : in which you will find a reference to the .msstyles file
- the .msstyles file : which is an old fashioned PE file (eg. like a DLL file) embedding .ini files and bitmaps
- the shellstyle.dll files : which extend the theme's properties for Explorer (eg. explorer bar bitmaps, etc...)
Theme's folder tree
Basically, available themes are located in "%windir%\Resources\Themes" directory, as shown below :
Each .theme file has its own directory, and a .msstyles file with the same name (eg. panther.theme has its .msstyles file in the directory named panther).
You will also find inside the theme's directory a folder named shell, which has a folder for each theme's scheme.
Finally, scheme's directory contains a shellstyle.dll. You can also find bitmaps (like wallpapers) or other resources referenced by the shellstyle.dll in this directory.
.theme file
A .theme file is an initialization file, with sections and key/value pairs defining many informations. The following sample shows the content of a .theme file.
[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
SCRNSAVE.EXE=%WinDir%system32\logon.scr
[VisualStyles]
Path=%ResourceDir%\Themes\Panther\Panther.msstyles
ColorStyle=NormalColor
Size=NormalSize
[MasterThemeSelector]
MTSM=DABJDKT
ThemeColorBPP=4
The interesting section is [VisualStyles]. You'll find in it three important tips : the relative path to the .msstyles file (Path), the color scheme used by the theme (ColorStyle) and the size behavior (Size).
Note: if you don't have the %ResourceDir% defined in the environment variables, you can use the sample code below :
System.Collections.IDictionary vars =
System.Environment.GetEnvironmentVariables();
System.Text.StringBuilder val =
new System.Text.StringBuilder(MAX_PATH);
Kernel32.GetPrivateProfileString(sectionName, keyName, "", val,
MAX_PATH, iniFile);
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 file
A .msstyles file is basically a PE file (eg. a DLL). What we are looking for is in the resource section, under the TEXTFILE and BITMAP resource directories.
Note: You can use a software like ResEdit to explore your .msstyles file.
The most important file is THEME_INI. It gives use :
- the documentation properties of the theme (eg. Author, DisplayName, etc.),
- the color schemes of the theme,
- the size schemes of the theme,
- the prefix name to extract bitmaps,
- the name of the .ini file which contains the theme datas for the current color scheme and size.
If we use the informations
[VisualStyles]
Path=%ResourceDir%\Themes\Panther\Panther.msstyles
ColorStyle=NormalColor
Size=NormalSize
specified in the .theme file, we will find our reference tips into the section named [File.]["color scheme"]["theme name"] like
[File.Normalpanther]
ColorSchemes = panther
Sizes = NormalSize
In our example, the .ini file which contains the theme datas is named NORMALPANTHER_INI and the prefix value is set in the ColorSchemes key (the bitmap's names will start with PANTHER).
The theme data initialization file
These files contain informations for each class and parts of the visual style.
After reading numerous of this kind of file, section's rules seem to be :
- A standard section have the format [ClassName].[ClassPart][(ClassPartState)]
- A class section defines the default ui values
- A class section with a state defines ui values for this state
- A special section have the format [ClassName]::[ClassName].[ClassPart][(ClassPartState)]
- [ClassPart] and [(ClassPartState)] are optional
- The section names (and key names) are not case sensitive
Then, if you want the checkbox data you will have to look in the section [Button.Checkbox] or [button.checkbox].
shellstyle.dll
The article of Mathew Hall describes the internals of this file. If you are interested in understanding it, look "Themed Windows XP style Explorer Bar" his article in The Code Project.
III - The wrapper
The following figure describes how the wrapper is designed :
A quick description :
- The
UxTheme
class is the generic wrapper. It is used by VisualStyleInformation
and VisualStyleRenderer
to get property values or to draw a specific control with the current theme's data.
- The main classes are
VisualStyleInformation
, VisualStyleRenderer
, and VisualStyleFile
.
VisualStyleInformation
provides access to the current theme information, like the theme's author or the copyright.
VisualStyleRenderer
is used to paint controls. It calls the UxTheme
wrapper functions for the current visual style, and VisualStyleFile
to handle other themes.
VisualStyleFile
handle theme's datas. You can use it to extract bitmaps, or just to get a component properties.
- The
MemoryIniFile
is used to map into memory .ini files embedded in the .msstyles theme's file.
- The
PEFile
is used to read a .msstyles file, and to access its resource section.
I'm not going to enumerate all the methods provided by each classes because it's not the topic. I will focus on VisualStyleFile
and its associated classes/structs.
VisualStyleFile
This class is the more important one : its role is to provide an object representation of a Theme. It's also a component that can be shared among your custom controls.
It retrieves the visual style's information through a .theme file, eg. it retrieves the theme's .mssstyles, maps the used .ini files and then maps the theme's properties into eight structures :
VisualStyleDocumention
which provides basic information about the documentation specified in a visual style file,
VisualStyleMetrics
which provides basic information about colors, fonts, sizes specified in a visual style file,
VisualStyleMetricColors
for the system colors defined in the visual style, like ActiveCaption
VisualStyleMetricFonts
for the system fonts defined in the visual style, like CaptionFont
VisualStyleMetricSizes
for the system sizes defined in the visual style, like CaptionBarHeight
VisualStyleProperties
which maps properties for a given component (eg. class+part+state),
VisualStyleScheme
which maps properties for a given color scheme,
VisualStyleSize
which maps properties for a given size scheme.
So, if you want to access a Theme's raw information/data, you just have to create an new VisualStyleFile
and get what you want as shown below :
String themeFile = @"C:\WINDOWS\Resources\Themes\Luna.theme";
using(VisualStyleFile theme = new VisualStyleFile(themeFile))
{
VisualStyleDocumention doc = theme.Documentation;
Console.WriteLine(doc.Author + " - " + doc.Copyright);
Console.WriteLine("Color scheme: "+theme.ThemeSchemeName);
Console.WriteLine("Size scheme: "+theme.ThemeSizeName);
VisualStyleProperties buttonProps = theme.GetElementProperties(
"BUTTON", (uint)ButtonPart.PushButton);
Console.WriteLine(
"Button is transparent : "+buttonProps.Transparent);
Console.WriteLine("Background image: "+buttonProps.ImageFile);
}
Using the code
I know, it is quite tedious. But the good news are that all you've read is implemented in this sample. I built this part as an FAQ, maybe you'll find what you want to know.
How to enumerate installed theme files
String[] themes = VisualStyleInformation.GetThemeFiles();
foreach(String theme in themes)
{
Console.WriteLine(theme);
}
How to get a theme information
If you want the current visual style information :
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);
VisualStyleRenderer renderer = VisualStyleRenderer("BUTTON",
(uint)ButtonPart.PushButton, (uint)PushButtonState.Normal);
bool isButtonTransparent = renderer.GetBoolean(BooleanProperty.Transparent);
bool isBacgroundFilled = renderer.GetBoolean(BooleanProperty.BackgroundFill);
Color borderColor = renderer.GetColor(ColorProperty.BorderColor);
Color fillColor = renderer.GetColor(ColorProperty.FillColor);
Color textColor = renderer.GetColor(ColorProperty.TextColor);
String backgroundImage = GetFilename(FilenameProperty.ImageFile);
String glyph = GetFilename(FilenameProperty.GlyphImageFile);
If you want the properties for a specific theme (not the current one) :
String themeFile = @"C:\WINDOWS\Resources\Themes\Luna.theme";
using(VisualStyleFile theme = new VisualStyleFile(themeFile))
{
VisualStyleDocumention doc = theme.Documentation;
Console.WriteLine(doc.Author + " - " + doc.Copyright);
Console.WriteLine("Color scheme: "+theme.ThemeSchemeName);
Console.WriteLine("Size scheme: "+theme.ThemeSizeName);
VisualStyleProperties buttonProps = theme.GetElementProperties("BUTTON",
(uint)ButtonPart.PushButton);
Console.WriteLine("Button is transparent : "+buttonProps.Transparent);
Console.WriteLine("Background image: "+buttonProps.ImageFile);
}
Note: Even for the current theme, my preference is the second solution.
How to extract a theme .ini files
String themeFile = @"C:\WINDOWS\Resources\Themes\Luna.theme";
String savePath = Environment.CurrentDirectory + @"\inifiles\";
using(VisualStyleFile theme = new VisualStyleFile(themeFile))
{
theme.SaveIniFiles(savePath);
}
How to get a theme's bitmap
String themeFile = @"C:\WINDOWS\Resources\Themes\Luna.theme";
String savePath = Environment.CurrentDirectory;
using(VisualStyleFile theme = new VisualStyleFile(themeFile))
{
VisualStyleProperties buttonProps = theme.GetElementProperties("BUTTON",
(uint)ButtonPart.PushButton);
using(Bitmap bmp = theme.GetBitmap(buttonProps.ImageFile))
{
bmp.Save(savePath+@"\buttonBitmaps.bmp");
}
}
How to share a theme among your custom controls
How to use a theme's data (not the current one) with your custom control?
You just have to add a VisualStyleFile
to your form via the designer, and update/set its ThemeFile
property.
Then, set the VisualStyleFile
property of your custom control with the new VisualStyleFile
.
How to create your custom renderer
Implementation is up to you : it depends on what you want to paint (eg. a listview header, a button, a scrollbar, a caption, etc.). The tip is to clearly separate the painting behavior from the component logic. My point of view is to create your custom controls as a "bean" which computes the right values (bounds, sizes, etc.) and pass them as parameters to your renderer methods.
Let's have a 15 minutes tutorial : create a renderer for a window's caption button.
- The first step is to look into the uxtheme's files where you have the window button's list and their possible states : you'll find
WindowPart
and WindowButtonState
enumerations.
The WindowPart
contains a lot of values, so the best way to use only the window's button values is to create an enumeration like :
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
};
- Create a basic renderer copying one of the sample renderers (
RadioButtonRenderer
for example), and remove unusefull methods.
public sealed class WindowButtonRenderer
{
public static bool IsSupported
{
get
{
return VisualStyleInformation.IsApplicationThemed;
}
}
private WindowButtonRenderer(){ }
}
- Write in it simple methods that will be called by your component (
DrawButton
methods) and remember that your renderer will use a Graphics
object and will need your component bounds and type.
public sealed class WindowButtonRenderer
{
public static bool IsSupported
{
get
{
return VisualStyleInformation.IsApplicationThemed;
}
}
private WindowButtonRenderer(){ }
#region Methods
#region Misc
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);
}
}
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 DrawCloseButton(Graphics g, Rectangle bounds,
WindowButtonState state)
{
DrawButton(g, bounds, WindowButtonType.CloseButton, state);
}
public static void DrawHelpButton(Graphics g, Rectangle bounds,
WindowButtonState state)
{
DrawButton(g, bounds, WindowButtonType.HelpButton, state);
}
public static void DrawMinizeButton(Graphics g, Rectangle bounds,
WindowButtonState state)
{
DrawButton(g, bounds, WindowButtonType.MinButton, state);
}
public static void DrawMaximizeButton(Graphics g, Rectangle bounds,
WindowButtonState state)
{
DrawButton(g, bounds, WindowButtonType.MaxButton, state);
}
public static void DrawRestoreButton(Graphics g, Rectangle bounds,
WindowButtonState state)
{
DrawButton(g, bounds, WindowButtonType.RestoreButton, state);
}
#endregion
#endregion
}
</see>
- Add
VisualStyleFile
support to your renderer
public sealed class WindowButtonRenderer
{
public static bool IsSupported
{
get
{
return VisualStyleInformation.IsApplicationThemed;
}
}
private WindowButtonRenderer(){ }
#region Methods
#region Misc
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);
}
}
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);
}
public static void DrawCloseButton(Graphics g, Rectangle bounds,
WindowButtonState state)
{
DrawButton(g, bounds, WindowButtonType.CloseButton, state);
}
public static void DrawCloseButton(VisualStyleFile style, Graphics g,
Rectangle bounds, WindowButtonState state)
{
DrawButton(style, g, bounds, WindowButtonType.CloseButton, state);
}
public static void DrawHelpButton(Graphics g, Rectangle bounds,
WindowButtonState state)
{
DrawButton(g, bounds, WindowButtonType.HelpButton, state);
}
public static void DrawHelpButton(VisualStyleFile style, Graphics g,
Rectangle bounds, WindowButtonState state)
{
DrawButton(style, g, bounds, WindowButtonType.HelpButton, state);
}
public static void DrawMinizeButton(Graphics g, Rectangle bounds,
WindowButtonState state)
{
DrawButton(g, bounds, WindowButtonType.MinButton, state);
}
public static void DrawMinizeButton(VisualStyleFile style, Graphics g,
Rectangle bounds, WindowButtonState state)
{
DrawButton(style, g, bounds, WindowButtonType.MinButton, state);
}
public static void DrawMaximizeButton(Graphics g, Rectangle bounds,
WindowButtonState state)
{
DrawButton(g, bounds, WindowButtonType.MaxButton, state);
}
public static void DrawMaximizeButton(VisualStyleFile style, Graphics g,
Rectangle bounds, WindowButtonState state)
{
DrawButton(style, g, bounds, WindowButtonType.MaxButton, state);
}
public static void DrawRestoreButton(Graphics g, Rectangle bounds,
WindowButtonState state)
{
DrawButton(g, bounds, WindowButtonType.RestoreButton, state);
}
public static void DrawRestoreButton(VisualStyleFile style, Graphics g,
Rectangle bounds, WindowButtonState state)
{
DrawButton(style, g, bounds, WindowButtonType.RestoreButton, state);
}
#endregion
#endregion
}
</see>
- Create your
WindowButton
component, just like the renderer (copy the CustomRadioButton
component for example, and remove unusefull fields and methods).
6 - After cleaning, add your fields like WindowButtonState
and WindowButtonType
and change the renderer in the OnPaint()
event with your brand new WindowButtonRenderer
.
7 - Add your own component behavior and, in the end, you will have something like the sample code below :
public class WindowButton : System.Windows.Forms.Control,
IVisualStyleSwitchable
{
[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
private WindowButtonState State
{
get
{
return this.state;
}
set
{
if(this.state == value) return;
this.state = value;
OnPropertyChanged();
}
}
false),>
[DesignerSerializationVisibility(
DesignerSerializationVisibility.Hidden),]
private Size RealSize
{
get
{
if(this.realSize==Size.Empty) realSize = Size;
return this.realSize;
}
set
{
this.realSize = value;
}
}
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
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();
}
}
true), Category("Appearance"),>
[DefaultValue(typeof(WindowButtonType),"CloseButton"),]
public WindowButtonType Type
{
get
{
return this.type;
}
set
{
if(this.type == value) return;
this.type = value;
OnPropertyChanged();
}
}
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)
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();
}
public WindowButton(System.ComponentModel.IContainer container) : this()
{
container.Add(this);
}
#endregion
#region Methods
#region VS generated code
protected override void Dispose( bool disposing )
{
if( disposing )
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}
private void InitializeComponent()
{
this.Size = new System.Drawing.Size(20, 20);
}
#endregion
#region Drawing
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();
}
protected virtual void OnPropertyChanged()
{
if(PropertyChanged!=null) PropertyChanged(this, EventArgs.Empty);
Invalidate();
}
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
}
Omedeto! Your custom WindowButton
is ready to use! (Hoops! You should add it on your form before)
Note: I've included four more renderers in the sample (a ButtonRenderer
, a CheckboxRenderer
, a RadioButtonRenderer
and a ProgressBarRenderer
). You can use them to create your own renderers (or ask me if I've already implement what you need).
Known issues
- I've not created a renderer for all the visual style parts, so you can find bugs (or a lack of function support) during your own implementation.
- Using current active theme data in a
VisualStyleFile
can cause your application to crash : don't associate this kind of VisualStyleFile
to your custom controls.
- Using too much
VisualStyleFile
s in your project will cause your form(s) to flicker when loading (I'm working on a way to throw an event when all of a theme's data would have been populated)
- Using too much
VisualStyleFile
s in your project will cause a high memory usage (because we are in a managed environment of course)
- Renderer implementation is quite annoying : you should copy/modify the sample renderers to make your own.
If you find a bug or an incoherence, don't be afraid to tell me. I'll fix it and update this article.
Points of Interest
Maybe the first interesting point is the wrapper itself. The more complicated part is bitmap drawing and stretch operations. You can look at the DrawBackground()
methods in the VisualStyleRenderer
class and StretchBitmap()
methods in VisualStyleHelper
class.
Implementing a PE file reader is quite a hard task. I you want to build your own PE file reader, you can read the PECOFF format specification, or you can use/extend the PEFile
class of the sample. Another idea, you can extend the PEFile
class to create a resource editor like ResEdit.
While I was doing a lot a refactoring and performance tests, I've implemented a fast .ini file memory mapper. The aim target was to avoid working with backep-up files (the .ini embedded in the .msstyles) which involved a lot of I/O (and to use win32 Kernel functions). You will find it as the MemoryIniFile
and MemoryIniSection
classes.
Another interesting thing is the wrapper design. For those who are still using .NET 1.1 and are going to use .NET 2.0 (or 3.0), it will be quite easy because they will find exactly the same classes (but with less possibilities). Converting their projects will be as simple as rename references to the namespace "Devcorp.Controls.VisualStyles" to "System.Windows.Forms.VisualStyles" and copy missing classes.
History
- 13 May 2007 - Version 0.85
- Remove
MemoryIniHelper
(redundant duplicated methods).
- Added filled background (non bitmap) support in
VisualStyleRenderer.DrawBackground()
.
- Added full visual style property inheritance (see
VisualStyleProperties
).
- Added
GroupBoxRenderer
.
- Added
ComboBoxRenderer
.
- Added
TextBoxRenderer
.
- Some bug fixes and few optimizations.
- 9 May 2007 - just corrections of the article (orthography)
- 6 May 2007 - Version 0.82
- Fixed lost 1px width/height when drawing stretched bitmaps.
- Fixed
VisualStyleFile
support in GetBackgroundContentRectangle()
method.
- Added partial
VisualStyleFile
support in DrawEdge()
method.
- Added
ExplorerBarRenderer
renderer (and its sample).
- Added
IEBarRenderer
renderer (and its sample).
- Some bug fixes.
- 01 May 2007 - Version 0.81
- Fixed parsing error in
MemoryIniFile
: some lines with key/value and a comment were not computed.
- Fixed drawing incoherence for components with a glyph.
- Fixed high memory usage when using more than three
VisualStyleFile
.
- Added real support of theme's sizing rules.
- Added
WindowButton
and WindowButtonRenderer
in the sample.
- Some bug fixes.
- 29 April 2007 - Version 0.8 (posted on The Code Project)
- Added .ini file memory mapper.
- Added embedded theme's .ini files save support.
- Added stand-alone renderers as samples.
- Lot of design refactoring and optimization.
- Some bug fixes.
- 20 November 2005 - Initial project release.