Introduction
I was surprised to find that Visual Studio 2003 .NET, the latest and greatest
Windows application development tool, doesn't include an easy way to create
XP-style buttons. Then I noticed that Office XP mostly uses the old-fashioned
3D buttons, suggesting that implementing the new style is not trivial. The new
buttons are mostly found in Windows XP accessories and Internet Explorer 6.
As a programmer, my first reaction was to jump in and reinvent the wheel, which
reminds me that I need to start waiting for my second reaction before
I waste any more time.
This article describes a way to create XP-like buttons by implementing the
button's Paint method. It's an (interesting, I hope) exploration of GDI+ graphics.
But you don't have to go to all this trouble, nor do you have to accept less
than perfect results, to achieve XP buttons. You simply need to link your Windows
Form application with the latest version (6.0) of ComCtl32.
The Real Way to Get XP Buttons
The way to get XP-style controls in your VB.NET application is to create a
manifest file that accompanies your .exe file. The manifest specifies
that your code depends upon ComCtl32.DLL. A manifest file is like a config file--it's
XML and it has the same name as your app, but with ".manifest" appended.
If your app is MyApp.exe, then you need to create MyApp.exe.manifest in the
same directory.
="1.0" ="UTF-8" ="yes"
<assembly xmlns="urn:schemas-microsoft-com:asm.v1"
manifestVersion="1.0">
<assemblyIdentity
version="1.0.0.0"
processorArchitecture="x86"
name="MyApp"
type="win32"
/>
<description>My Great Solution</description>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="X86"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
</assembly>
This information comes from an (apparently) old article (from when XP was called
Whistler) called How
to be ready for Windows XP's updated visuals, and from poking around the
system. The only other magic you need is to set the FlatStyle
of your buttons
(and other controls that support FlatStyle
) to "System
" instead of
the default "Standard
." My only explanation for Office XP and other
Microsoft applications not using the XP controls is that they were released
before ComCtl 6.0, I suppose.
Background
Having decided that I would have to subclass Button somehow and intercept
WinProc
messages, I was pleased to discover that creating owner-drawn buttons in VB.NET
is a simple matter of handling the button's Paint method.
I expected several things that were not true as it turned out. First, I thought
that I would need to inform the button that it would be owner-drawn. Not so--apparently,
the existence of a method that "Handles Button1.Paint" sets a private "owner-draw"
field in the button to True. Second, I thought that my Paint handler would be
called with explicit "state" instructions--inactive, focus, hover, pressed,
and so on (like an owner-draw Tab control gets DrawItem
events). Instead, the
paint method receives the bare minimum--the button object, a Graphics environment
and a clip rectangle--but it is called when the button's state changes.
Implementing the button shown here was not particularly difficult, thanks to
GDI+ methods that now include gradient brushes and graphics paths. There is
considerable room for improvement, and I welcome any and all comments or suggestions.
The 2D drawing primitives can be further optimized, perhaps. I tried to use
system colors as an attempt to be "theme-aware" but there are probably better
ways to do that, too. In general, I didn't sweat the details too much--my pressed
button indents more than the standard Windows XP button, for instance.
Using the code
To use this code in your application, paste the code (exclusive of the Form Designer generated code)
into your form and add "Handles
MyButton.Paint
, MyOtherButton.Paint
" to the paint method for each
button you want to style.
That is, instead of:
Private Sub PaintXPButton(ByVal sender As Object, _
ByVal e As System.Windows.Forms.PaintEventArgs) _
Handles Button1.Paint
To style your btnOK
and btnCancel
buttons, you'd say:
Private Sub PaintXPButton(ByVal sender As Object, _
ByVal e As System.Windows.Forms.PaintEventArgs) _
Handles btnOK.Paint, btnCancel.Paint
It's important that you change each button's FlatStyle
property to
Flat
(the default is Standard
).
If you don't, the button won't light up when you mouse over it.
This code illustrates one set of visual behaviors, but hopefully it will inspire others to try
more imaginative effects. There are only four states that matter to me--normal, pressed, hot (mouse over),
and disabled, but your application may need more. The gradient makes the normal button appear somewhat
convex. When the button is pressed, I change it
to concave by inverting the gradient and adding a highlight rectangle. This is more exaggerated than
the official XP button's behavior.
When the mouse is over the normal button (or the mouse has left the pressed button), I draw an orange
rectangle as a nod toward what XP does. It's more of a "glow" effect, really, so I draw extra highlight
lines around the rectangle. I think orange was chosen because it shows up against most background colors.
Since there is no orange in the system color palette, I assume that XP always uses orange, so I do, too.
Known Bugs
This code ignores focus. The focus rect is never drawn. I don't miss it, but if you want to implement
the focus rect, you'll have to figure out how to determine when the button has the focus and when it
doesn't. Drawing the rect probably involves a hatched brush, but that's as much as I want to contemplate.
Likewise, the code ignores the button's
TextAlign
and image properties (Image
, ImageAlign
).
In this code, text is centered on the button using a StringFormat
object which calls left and right near and far
(apparently in an attempt to be world-aware and to end the colonialist domination of left-to-right
languages). I just didn't want to mess with converting TextAlign's
TopRight
into the StringFormat's
Alignment
(far) and
LineAlignment
(near). I wanted only centered text, so it becomes an exercise for the
reader. Sorry.
I notice that the code assumes that the button's Form is its parent. If you are styling buttons in
other containers (such as a GroupBox
or Panel
), change any Me.
references in the code to the
appropriate container.
Points of Interest
Round-cornered rectangles have disappeared, it seems, which is somewhat troublesome when the button
you're trying to draw has eased corners. The new GraphicsPath
seems to be the answer here--just draw line,
arc, line, arc, etc. in the path, and GDI+ lets you fill and draw your roundrect just like
Bill Atkinson or Mark Cutter (antediluvian references to the graphics pioneers who gave us roundrects in the first place, or
at least the first that I ever saw). Again, I hope that the idea of using a path for your buttons may inspire you
to create some wildly imaginative visual behaviors.
And another thing: I'm slightly embarrassed to say that converting the mouse position to button
coordinates stumped me for several minutes. I looked for ScreenToClient()
, the old VB 6 way of doing it,
and I found nothing in the VS .NET help, MSDN, Code Project, or the web in general. When I couldn't find the
name of the
function that I knew was there, my first thrash was to subtract my form's TopLeft
from the mouse position.
Of course, that was off by the width of the form's frame and the height of the form's title bar. My face
was turning purple at this point, so I don't remember how I found it, but the answer is:
ButtonContainer.PointToClient(mousePos)
Jan Tielens' article, A simple
XP/VS.NET style button control, may be of interest, as well, as he describes a method for dimming and
brightening buttons--more prods toward imaginative visual behaviors.
History
- August 6, 2003 - Updated with the correct method for styling XP controls
- August 1, 2003 - Thrown together quickly in the single-digit hours to solve the "lack" of XP buttons