In the Beginning..
There was the toolbar, and the developer said: "The toolbar is good, but.." Why are we stuck with just a few styles? How do I make fading buttons like in IE7? Can I customize the tooltips? How does this Renderer
class work, and what can I do with it?
First things first, I would like to thank Phil Wright for his Office 2007 renderer example; without his example, this would have taken far longer to write.
The first goal I had when creating this class proved to be the most difficult, that is, how to create a toolbar with fading buttons. The solution took a lot of experimenting to get right (but more on that later..). I also wanted to make a toolbar with buttons that looked raised when selected, and inset when pushed; for that matter, I wanted a flexibility that is lacking in the pre-fabricated implementations offered to us through the designer, and to expose properties that allowed for ad hoc customization of the toolbar's appearance. I started by researching the Renderer
class, (not much help there really), and then looking for more examples of the code here and on other developer sites. The only good example I could find was the Office 2007 renderer, so I took it apart, stepping through it line by line.
As with other aspects of .NET, I have come to like the idea of a renderer class, but was disappointed with the implementation. There are just too many aspects of the drawing process that are not adequately exposed. To do this over again, I would probably write a toolbar from scratch; less work, maybe even less code.
OK, enough preamble..
On with the Show..
This is not just an implementation of a renderer class, it is also several embedded classes that serve to facilitate fading buttons, create custom tooltips, and support custom drawing. At the heart of it all though, is a renderer class, which I think can be thought of as a series of events that are sent during various drawing stages for the toolbar elements. Depending on the state, you can either handle the event, or choose to pass it to the default handler.
protected override void OnRenderImageMargin(ToolStripRenderEventArgs e)
{
if ((e.ToolStrip is ContextMenuStrip) ||
(e.ToolStrip is ToolStripDropDownMenu))
drawImageMargin(e.Graphics, e.AffectedBounds);
else
base.OnRenderImageMargin(e);
}
The event arguments contain information pertaining to the item and the drawing stage; these can include size and co-ordinates, a graphics object, the object owner, and information particular to that event. You can bypass the default handler by simply not including the base portion, or in some cases, a handled flag is included in the event arguments.
The hard part can be in figuring out what some of these events actually do, and what to do with the event parameters. In the above snippet, if the toolstrip owner is a menu, the call is forwarded to a routine that draws the menu's image margin.
Here is an example of how a glass style button is drawn when the OnRenderButtonBackground
event is called (through an intermediate routine, drawButton
):
private void drawGlassButton(Graphics g, RectangleF bounds, int opacity)
{
bounds.Inflate(-1, -1);
using (GraphicsMode mode = new GraphicsMode(g, SmoothingMode.AntiAlias))
{
using (GraphicsPath buttonPath = createRoundRectanglePath(
g,
bounds.X, bounds.Y,
bounds.Width, bounds.Height,
1f))
{
using (LinearGradientBrush borderBrush = new LinearGradientBrush(
bounds,
Color.FromArgb(opacity * 20, ButtonGradientEnd),
Color.FromArgb(opacity * 20, ButtonGradientBegin),
90f))
{
borderBrush.SetSigmaBellShape(0.5f);
using (Pen borderPen = new Pen(borderBrush, .5f))
g.DrawPath(borderPen, buttonPath);
}
RectangleF clipBounds = bounds;
clipBounds.Inflate(-1, -1);
using (GraphicsPath clipPath = createRoundRectanglePath(
g,
clipBounds.X, clipBounds.Y,
clipBounds.Width, clipBounds.Height,
1f))
{
using (Region region = new Region(clipPath))
g.SetClip(region, CombineMode.Exclude);
}
using (LinearGradientBrush edgeBrush = new LinearGradientBrush(
bounds,
Color.FromArgb(opacity * 15, ButtonBorderColor),
Color.FromArgb(opacity * 5, Color.Black),
90f))
{
edgeBrush.SetBlendTriangularShape(0.1f);
g.FillPath(edgeBrush, buttonPath);
g.ResetClip();
bounds.Inflate(-1, -1);
}
using (LinearGradientBrush fillBrush =
new LinearGradientBrush(
bounds,
Color.FromArgb(opacity * 10, Color.White),
Color.FromArgb(opacity * 5, ButtonGradientBegin),
LinearGradientMode.ForwardDiagonal))
{
fillBrush.SetBlendTriangularShape(0.4f);
g.FillPath(fillBrush, buttonPath);
g.ResetClip();
}
}
}
}
As you can see, most of the code is for handling the drawing events, and using them to create the desired visual style. The above example uses clipping, blending, and gradients to create a button with a mirrored edge.
The Highlights
There are five styles in the example application that covers some of the range of this class. The Carbon and System Plus styles use an image for the toolstrip background, the rest use gradients. I left as many of the properties exposed from the class as I could, giving the user a wide range of style options.
Glass
This style uses a blended gradient of white and silver. The blend as well as the gradient type is exposed in properties, or can be set with the SetGlobalStyle
method, or SetStyle
method for the type of toolbar (e.g., SetStatusbarStyle()
). This example also uses the glass button style from the code above.
Chrome
This style uses a centered vertical gradient with Flat style menus, and the 'Glow' button style. The glow effect is achieved by insetting frames with differing transparencies into a rounded graphics path.
Carbon
Carbon uses a background image, with glass style buttons and the Office style connected menu.
System Plus
Pictured at the top of the page. This also uses a background image to mimic the IE style, though it could also be done with gradients. I prefer images though.. less processing. Did you know that much of the visual styles on controls (like this form, or a button..) are done by blitting an image over the control?
BlackOut
Another style example using a vertical gradient with a custom blend.
Menu Styles
I put four different menu style options into the class; Vista, Flat, Office, and Custom. The style also determines the menu bar button style. The custom option will draw the button with whatever the button style option is for that toolstrip.
ToolTips
The ToolTip
class is not really a tooltip at all, but rather a static window with a fade timer.
public ToolTip(IntPtr hParentWnd)
{
Type t = typeof(ToolTip);
Module m = t.Module;
_hInstance = Marshal.GetHINSTANCE(m);
_hParentWnd = hParentWnd;
_hTipWnd = CreateWindowEx(WS_EX_TOPMOST | WS_EX_TOOLWINDOW,
"STATIC", "",
SS_OWNERDRAW | WS_CHILD | WS_CLIPSIBLINGS | WS_OVERLAPPED,
0, 0,
0, 0,
GetDesktopWindow(),
IntPtr.Zero, _hInstance, IntPtr.Zero);
SetWindowPos(_hTipWnd, HWND_TOP,
0, 0,
0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOOWNERZORDER);
createFonts();
this.AssignHandle(_hTipWnd);
}
Note that you can create transparent tooltips simply by changing the alpha on the colors used. This works because the static window is itself transparent.
Button Fader
As I mentioned near the beginning of the article, I really wanted fading buttons.. gives the toolbar some panache, you know? I started by writing a timer class and creating an instance of the class for each tool item that would fade. The timer class is kept thread safe by synchronizing the timer with the parent so that the calls out from the timer thread exit on the parent thread.
_aTimer.SynchronizingObject = (ISynchronizeInvoke)sender;
The class houses a timer that counts up or down depending on the leave state, but it also contains a reference to the owner item, and an image dc of that item. This is because you can not simply erase the button between cycles as it goes from transparent to opaque, or it would flicker like a bad fluorescent bulb. Instead, you have to first blit an image of the clean button, then draw the semi-transparent button mask. This gives a flicker free illusion of fading. The painting is initiated via events fired from the timer class; events also manage resetting the timer and invalidating the tool item once the fade timer is spent.
What I found though, was that the buttons were invalidating themselves whenever the mouse moved off the item, causing a heavy flicker. I tried a number of ways to get around this, including subclassing the Paint
and Invalidate
methods, but no luck.. This is one of the problems with some of these subclassed methods, they just don't seem capable of doing certain tasks if those tasks are in any way outside the expected norm. You are forced to go to unmanaged code and dream up slimy hacks. Such is the case here.. after exhausting every inbuilt method, I went to API to find the solution. First, I override the message handler and intercept WM_PAINT
:
protected override void WndProc(ref Message m)
{
switch (m.Msg)
{
case WM_PAINT:
if (!bypassPaint())
base.WndProc(ref m);
else
m.Result = new IntPtr(1);
break;
default:
base.WndProc(ref m);
break;
}
}
The bypassPaint
method first gets the update rectangle, then if it intersects an item that has an active timer, it validates the item bounds and bypasses the paint call.
internal Rectangle updateRegion()
{
RECT updateRect;
GetUpdateRect(ToolStrip.Handle, out updateRect, false);
return new Rectangle(updateRect.Left, updateRect.Top,
updateRect.Right - updateRect.Left,
updateRect.Bottom - updateRect.Top);
}
internal bool bypassPaint()
{
Rectangle updateRect = updateRegion();
foreach (ToolStripItem item in ToolStrip.Items)
{
if ((_fader.ContainsKey(item)) &&
(_fader[item].TickCount > 0) &&
(!item.IsOnOverflow) &&
(!_fader[item].Invalidating) &&
(updateRect.IntersectsWith(item.Bounds)))
{
if (((_fader[item].FadeStyle == FadeType.FadeOut) ||
(_fader[item].FadeStyle == FadeType.FadeFast)))
{
RECT validRect = new RECT(updateRect.Left, updateRect.Top,
updateRect.Right, updateRect.Bottom);
ValidateRect(ToolStrip.Handle, ref validRect);
return true;
}
}
}
return false;
}
Well.. that's about it, enjoy the code, and stay out of trouble..