Click here to Skip to main content
Click here to Skip to main content

Get the real XP look with Tab Pages

, 27 Jul 2003
Rate this:
Please Sign up or sign in to vote.
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.

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 trough 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

The source files were last updated on 24 July 2003.

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

About the Author

Pierre Arnaud
Web Developer
Switzerland Switzerland

Pierre Arnaud got a Ph.D. in computer science at the Swiss Federal Institute of Technology; he currently works both as an independent contractor on hardware and software projects at OPaC bright ideas and as a senior software designer at EPSITEC.

Pierre was a key contributor to the Smaky computer, a real time, multitasking system based on the Motorola 680x0 processor family.

Now, Pierre works on his spare time for the Creative Docs .NET project: it is a vector based drawing and page layout software based on .NET and AGG.


Comments and Discussions

 
QuestionLicense PinmemberAlan Hayslep29-Aug-13 0:36 
AnswerRe: License PinmemberPierre Arnaud29-Aug-13 1:26 
GeneralRe: License [modified] PinmemberAlan Hayslep29-Aug-13 3:04 
GeneralAnother Simple Fix Pinmemberphanf15-Jun-07 7:56 
GeneralOpenThemeData HTHEME Limit PinmemberPaul F. Williams11-Apr-06 6:14 
Generaldumb question Pinmembermelanieab30-Mar-05 8:32 
QuestionA slightly less painful approach? PinmemberMclusky8-Feb-05 15:48 
AnswerRe: A slightly less painful approach? PinmemberPierre Arnaud8-Feb-05 19:25 
GeneralCheckBox error on scroll and resize PinmemberFrederik W. Johansen26-Aug-04 23:18 
QuestionAn even easier way? Pinmemberlyangsprint28-Jul-04 15:08 
AnswerRe: An even easier way? PinmemberKenneth Beckett3-Aug-04 10:06 
AnswerRe: An even easier way? PinmemberPierre Arnaud7-Aug-04 9:34 
GeneralUnable to load DLL (OPaC.uxTheme.Win32.dll) - RESOLVED Pinmembermatherb10-May-04 9:39 
GeneralRe: Unable to load DLL (OPaC.uxTheme.Win32.dll) - RESOLVED Pinmembermanishma25-Feb-05 9:14 
GeneralRe: Unable to load DLL (OPaC.uxTheme.Win32.dll) - RESOLVED Pinmembermanishma25-Feb-05 9:22 
GeneralSplitters PinmemberDraugluin19-Apr-04 11:35 
GeneralSplitters fix? PinmemberDraugluin19-Apr-04 11:49 
QuestionIs it possible to port it to native c++ (MFC based)? PinmemberAmit Gefen7-Apr-04 18:34 
GeneralProblem running on Win98 Pinmembergnosis15-Mar-04 16:10 
QuestionThis is a bit cleaner? Pinmembersichbo310-Mar-04 7:13 
AnswerRe: This is a bit cleaner? PinmemberPierre Arnaud10-Mar-04 9:49 
AnswerRe: This is a bit cleaner? PinmemberCole_S23-May-04 10:21 
AnswerRe: This is a bit cleaner? PinsussAnonymous2-Apr-05 13:23 
GeneralSetWindowTheme Pinmembershalnov15-Jan-04 4:39 
GeneralOPaC.uxTheme.Win32.dll not found PinmemberSchonki8-Jan-04 4:37 
GeneralRe: OPaC.uxTheme.Win32.dll not found PinmemberDraugluin19-Apr-04 11:39 
GeneralAn Easier Way PinsussAnonymous29-Dec-03 9:10 
GeneralRe: An Easier Way PinmemberASA00680628-Feb-04 14:50 
GeneralRe: An Easier Way PinmemberPierre Arnaud28-Feb-04 23:27 
GeneralCheckbox Caption always aligned to TopLeft PinmemberNeutrino6-Dec-03 6:25 
GeneralRe: Checkbox Caption always aligned to TopLeft PinmemberPierre Arnaud7-Dec-03 2:44 
GeneralRe: Checkbox Caption always aligned to TopLeft PinmemberASA00680628-Feb-04 14:44 
GeneralRe: Checkbox Caption always aligned to TopLeft PinmemberPinx17-May-04 0:49 
Generalplatform Pinmemberjesbeer6-Nov-03 18:25 
GeneralRe: platform PinmemberPierre Arnaud6-Nov-03 19:25 
GeneralLicense PinmemberMol2007-Oct-03 11:03 
GeneralRe: License PinmemberPierre Arnaud7-Oct-03 19:22 
GeneralITAB CONTROL Pinmemberj_satya17-Aug-03 23:01 
QuestionLabel doesn't support transparent color ?!? PinmemberLeo@XIO.CZ12-Aug-03 1:24 
AnswerRe: Label doesn't support transparent color ?!? PinmemberPierre Arnaud12-Aug-03 5:37 
AnswerRe: Label doesn't support transparent color ?!? PinsussGordon Brooks14-Aug-03 9:30 
GeneralTab Pages in XP PinsussAnonymous6-Aug-03 2:23 
GeneralRe: Tab Pages in XP PinmemberPierre Arnaud6-Aug-03 3:00 
GeneralRe: Tab Pages in XP PinsussAnonymous21-Aug-03 6:28 
GeneralPb dés l'ouverture Pinmemberkrazitchek28-Jul-03 9:17 
GeneralRe: Pb dés l'ouverture PinmemberJuicy28-Jul-03 21:31 
GeneralThis is an International Forum - Use English! PinmemberAmit Gefen7-Apr-04 18:29 
GeneralRe: This is an English Forum! This is not France! PinmemberAmit Gefen7-Apr-04 18:37 
GeneralLeft or right aligned tab control PinsussMarcel van Pinxteren22-Jul-03 5:49 
GeneralRe: Left or right aligned tab control PinmemberPierre Arnaud24-Jul-03 5:23 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140721.1 | Last Updated 28 Jul 2003
Article Copyright 2002 by Pierre Arnaud
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid