Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

vtExtender: ToolStrip Extender, Renderer, and Customization Class

0.00/5 (No votes)
16 Feb 2009 2  
Create ToolStrips with fading buttons, custom tooltips, and expose renderer styles..

system.jpg

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)
{
    // draw the margin area on a menu
    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)
{
    // initial bounds
    bounds.Inflate(-1, -1);

    // draw using anti alias
    using (GraphicsMode mode = new GraphicsMode(g, SmoothingMode.AntiAlias))
    {
        // draw the border around the button
        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);
            }

            // create a clipping region
            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);
            }

            // fill in the edge accent
            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);
            }

            // fill the button with a subtle glow
            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

glass.jpg

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

chrome.jpg

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.jpg

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

blackout.jpg

Another style example using a vertical gradient with a custom blend.

Menu Styles

menu.jpg

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

tooltip.jpg

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;

    // create window
    _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);
    // set starting position
    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

buttonstyle.jpg

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)
    {
        // bypasses an invalidation thrown by parent when mouse leaves
        // a button, this causes a slight flicker when drawing fader
        // if someone knows a better way, post a message..
        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..

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here