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

Get the Real XP Look with Tab Pages

0.00/5 (No votes)
27 Jul 2003 1  
Real XP look with Tab Pages
The original implementation of class TabPage provided by Microsoft is broken: it displays a gray background color rather than the shiny smooth shade expected under Windows XP. I present a solution in this article..

Sample Image - themedtabpage.gif

Introduction

When you create an application which contains a tabbed view and define a manifest file (as explained on this MSDN page), you would expect the tabbed view to display with the active theme. But it does not! Why? System.Windows.Forms.TabPage does not support the XP visual style, as shown in the screen shot (left tabbed view), but System.Windows.Forms.TabControl does.

I searched the web, hoping to find a solution. I only found people who observed exactly the same problem as I did; and there was no solution readily available to C# programmers.

Trying to set the TabPage.BackColor to transparent did not help. I had to implement my own OnPaintBackground method and draw the themed background myself, if I wanted the tabbed view to display just like those found in non-.NET applications (standard applications don't have a TabPage control above their TabControl; the TabPage is mainly used to simplify the manipulation of the tab page contents).

Wrapping uxTheme

In order to draw the themed background, you have to call uxtheme.dll function DrawThemeBackground, which means calling into unmanaged code. I hacked the sources of Microsoft's sample ThemeExplorer in order to extract the C++ wrappers needed to call uxtheme.dll. I implemented the following functions in an unmanaged C++ DLL (that is, pure Win32 code) named OPaC.uxTheme.Win32.dll:

  • bool Wrapper_IsAppThemed ();

    Test if the application has to use themes to display itself. This function returns true on XP if the user has activated the "Windows XP style" in the display properties. On systems which do not support uxtheme.dll, the function returns false.

  • bool Wrapper_DrawBackground (const wchar_t* name, const wchar_t* part, const wchar_t* state, HDC hdc, ...);

    Draw the background of the specified widget. The name and part describe the widget ("BUTTON"/"CHECKBOX" and "TAB"/"BODY" would be valid names/parts, for instance).
    The meaning of state depends on the widget (for the checkbox, you could specify "UNCHECKEDNORMAL", "UNCHECKEDHOT", "UNCHECKEDPRESSED", "UNCHECKEDDISABLED", "CHECKEDNORMAL", etc.)

  • bool Wrapper_DrawThemeParentBackground (HWND hwnd, HDC hdc);
  • bool Wrapper_DrawThemeParentBackgroundRect (HWND hwnd, HDC hdc, int ox, int oy, int dx, int dy);

    Draw the background of the parent view showing through.

  • bool Wrapper_GetTextColor (const wchar_t* name, const wchar_t* part, const wchar_t* state, int* r, int* g, int* b);

    Get the color of the specified text. The name, part and state strings have the same meaning as with Wrapper_DrawBackground.

All these functions degrade gracefully if there is no uxtheme.dll present on the system (they just return false ). They are accessible from .NET as static functions published by the OPaC.uxTheme.Wrapper class. The C# wrapper is just a collection of P/Invoke functions declared with the DllImport attribute.

In my first version of this article, I implemented the wrapper functions in a managed C++ assembly, but the resulting DLL was way too large (more than 160 KB). By implementing the wrapper as an unmanaged DLL, with just very little C# glue in a separate assembly, I reduced the combined DLL size to about 35 KB, which is much more reasonable.

Here is the implementation of the wrapper used to get the color of a text element using uxtheme.dll:

bool __stdcall Wrapper_GetTextColor (const wchar_t* name,
                                     const wchar_t* part_name,
                                     const wchar_t* state_name,
                                     int* r, int* g, int* b)
{
    bool ok = false;
    if (xpStyle.IsAppThemed ())
    {
        HTHEME theme = xpStyle.OpenThemeData (NULL, name);
        if (theme != NULL)
        {
            try
            {
                int part;
                int state;
                
                if (FindVisualStyle (name, part_name, state_name, part, state))
                {
                    COLORREF color;
                    int prop = TMT_TEXTCOLOR;
                    if (S_OK == xpStyle.GetThemeColor (theme, part, state, prop, &color))
                    {
                        *r = GetRValue (color);
                        *g = GetGValue (color);
                        *b = GetBValue (color);
                        ok = true;
                    }
                }
            }
            catch (...)
            {
                // Swallow any exceptions - just in case, so we don't crash the caller 
                // if something gets really wrong.
            }
            xpStyle.CloseThemeData (theme);
        }
    }
    return ok;
}

Deriving TabPage

I derived System.Windows.Forms.TabPage and wrote the override for method OnPaintBackground, which called into the wrapper assembly. But there still was a problem I had to address: the text labels I put on the tabbed view did not display the proper background; when setting the control's property TabPage.BackColor to Colors.Transparent, the background has not exactly the expected shade. What happens, in fact, when you tell .NET that the background of a control is transparent, is that the erasing code simply calls the parent's OnPaintBackground method, specifying a clipping region matching the control's shape. When subsequently calling DrawThemeBackground, the theme gets drawn with the wrong origin (the origin of the clipping region is used, instead of the origin of the containing TabPage). Code to offset the theme is therefore required...

My first naive attempt was to just offset the background to match the origin of the clipping region. This worked fine as long as the view did not get partially obscured, and then partially repainted: in this case, the clipping origin no longer matched the origin of the label object and the background was, once more, painted with the wrong offset. I finally came up with a rather complex piece of code which walks through all children, trying to identify which one's background is being erased and using the appropriate offset:

public class TabPage : System.Windows.Forms.TabPage
{
    public TabPage()
    {
    }
    
    public bool UseTheme
    {
        get { return OPaC.uxTheme.Wrapper.IsAppThemed (); }
    }
    
    protected override void OnPaintBackground(System.Windows.Forms.PaintEventArgs e)
    {
        if (this.UseTheme)
        {
            int ox = (int) e.Graphics.VisibleClipBounds.Left;
            int oy = (int) e.Graphics.VisibleClipBounds.Top;
            int dx = (int) e.Graphics.VisibleClipBounds.Width;
            int dy = (int) e.Graphics.VisibleClipBounds.Height;
            
            if ((ox != 0) || (oy != 0) || (dx != this.Width) || (dy != this.Height))
            {
                this.PaintChildrenBackground (e.Graphics, this, 
                                new System.Drawing.Rectangle (ox, oy, dx, dy), 0, 0);
            }
            else
            {
                this.ThemedPaintBackground (e.Graphics, 0, 0, 
                                            this.Width, this.Height, 0, 0);
            }
        }
        else
        {
            base.OnPaintBackground (e);
        }
    }
    
    private bool PaintChildrenBackground(System.Drawing.Graphics graphics,
                                         System.Windows.Forms.Control control,
                                         System.Drawing.Rectangle rect,
                                         int ofx, int ofy)
    {
        foreach (System.Windows.Forms.Control child in control.Controls)
        {
            System.Drawing.Rectangle find
                         = new System.Drawing.Rectangle (child.Location, child.Size);
                    
            if (find.Contains (rect))
            {
                System.Drawing.Rectangle child_rect = rect;
                
                child_rect.Offset (- child.Left, - child.Top);
                if (this.PaintChildrenBackground (graphics, child, child_rect, 
                                                  ofx + child.Left, ofy + child.Top))
                {
                    return true;
                }
                
                this.ThemedPaintBackground (graphics, child.Left, child.Top, 
                                            child.Width, child.Height, ofx, ofy);
                return true;
            }
        }
        
        return false;
    }

    private void ThemedPaintBackground(System.Drawing.Graphics graphics,
                                       int ox, int oy, int dx, int dy, int ofx, int ofy)
    {
        System.IntPtr hdc  = graphics.GetHdc ();
        OPaC.uxTheme.Wrapper.DrawBackground ("TAB", "BODY", null, hdc, -ofx, -ofy,
                                             this.Width, this.Height, ox, oy, dx, dy);
        graphics.ReleaseHdc (hdc);
    }
} 

More Trouble... GroupBox and CheckBox

I soon discovered that the System.Windows.Forms.GroupBox did not paint its background with the proper color when using FlatStyle.System, which is quite annoying. It insisted on painting the background with the default background color. This meant that I had to derive GroupBox and provide my own OnPaint implementation, based on uxtheme.dll. The replacement class I provide can be used both with the classic and the XP styles (and it supports switching from one to the other).

After I started using my own GroupBox, it became obvious that I had to derive System.Windows.Forms.CheckBox too, since it too insisted on painting its background itself... Implementing the check box was not really complicated: I just had to make sure that the look and feel was exactly the same as the original version shipped by Microsoft (drawing the caption at the right place, drawing the focus rectangle, reflecting the state of the widget - hot/pressed/disabled/etc.)

If you are interested by the gory details, have a look at the sources...

Using OPaC.Themed.Forms.TabPage

To use the code provided in this article, you must compile the unmanaged DLL (OPaC.uxTheme.Win32.dll) and copy the contents of the bin\debug and bin\release directories found in the solution root to your application's bin\debug and bin\release directories. You then add a reference to OPaC.Themed.Forms.dll (which depends on OPaC.uxTheme.dll and OPaC.uxTheme.Win32.dll)...

Create your user interface and then replace System.Windows.Forms.TabPage with OPaC.Themed.Forms.TabPage, System.Windows.Forms.GroupBox with OPaC.Themed.Forms.GroupBox and System.Windows.Forms.CheckBox with OPaC.Themed.Forms.CheckBox. Do not set these object's BackColor to Color.Transparent or try to force their FlatStyle to anything else than FlatStyle.Standard.

To make sure your controls display as desired when used in a TabPage, follow these additional rules:

  • Button. FlatStyle = FlatStyle.System;
  • RadioButton.Fl atStyle = FlatStyle.System;
  • Label.B ackColor = Color.Transparent;

Keep the default values for the FlatStyle and BackColor properties for all other controls.

Final Note

I tried out every possible solution I could dream of in order to get the controls to paint properly in a tab control. Only one solution addressed all possible problems, and it is far from elegant. If only Microsoft made the sources of System.Windows.Forms available (as they did for the Rotor implementation of the .NET Framework), fixing the controls would have been simple, efficient and elegant.

A helpful reader hinted me to use function EnableThemeDialogTexture in uxtheme.dll, which can be used in Win32 to tell a window how to paint its background, but as expected, it does not work either with the Windows Forms implementation. So the solution presented in this article still seems to be the only way to go.

History

  • 24th July, 2003: Last updated source files

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.

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