XP Theme Support for Rich Edit and Custom Controls

Introduction

The class distributed with this article is intended to be used by any program, developed in C++, that's using a Rich Edit control. For some reason, Microsoft never updated its Rich Edit library after the release of Windows XP to make the control theme-aware. If your program supports themes (with the use of a manifest, for example) and is displaying a Rich Edit box in a window containing other controls, chances are that you already noticed how bad the Rich Edit looks compared to the other controls. I always thought that newer releases of the Rich Edit library would bring theme support, but because it's been more than two years already, I decided to create CRichEditThemed to take care of the problem. This little class has been designed to be incorporated in almost any Visual C++ project, painlessly, and necessitates only one function call to make your Rich Edit controls look like any other native control of Windows XP.

It is surprisingly difficult to find the proper documentation for applying a theme to a non-native control. It is true that improving the look of the borders of your edit controls was probably never a top priority in your schedule; however, it doesn't change the fact that Rich Edit controls look totally out of place in Windows XP and make any program's GUI lose some of its appeal. Some programs solved the situation by using Rich Edit controls in windowless modes (which is complete overkill if you're only trying to achieve theme support) whereas some others resorted in using weird GUI tricks that completely break down when users are not using the native theme of Windows XP. I searched a lot for some proper indication about adding real theme support to a control and the only thing I found was replies such as "use DrawThemeEdge," which, if you've been considering the problem for more than 10 minutes, is not very helpful (and wrong anyway). The CRichEditThemed class presented here solves this one and only problem and adds genuine theme support to any Rich Edit controls. I tried to comment the code as much as possible for those who want to understand how it works, and made the usage as simple as possible for those who don't have more than five minutes to spend on the issue.

Comments, suggestions, and bug reports are always appreciated. I'm not used to sharing code in this manner. However, I do think that nowadays, no programmer should have to be bothered improving the look of a Rich Edit control; you all have more important problems to take care of. As you will see, the class could also be easily modified to support other kinds of controls (including custom controls and ActiveX) that should make this article a good read for anybody interested in using the Windows XP theme library for its own purpose. This class is here to be used, shared, and redistributed.

Using the Class

The CRichEditThemed class was designed from the ground up to to be as simple to use as possible. You won't need to control the lifetime of a new object, you won't need to relay window messages, and you won't need to include another library (such as MFC or ATL) in your project to make the class work (with the exception of STL, which is a standard C++ library, included with any major C++ compiler, and is not supposed to create any conflict with the rest of your program). The class uses TCHAR to ensure Unicode compatibility and is totally backward compatible (meaning that it will stay silent when used in older versions of Windows or in an environment where themes are not enabled). Here are the files that will be automatically included in your project by RichEditThemed.h:

#include <tchar.h>      //Used for ANSI/Unicode compatibility
#include <map>          //Used internally by the class
#include <uxtheme.h>    //Used to access the UxTheme library
#include <tmschema.h>   //Definitions used by some of the UxTheme
                        //library functions

In addition, you must ensure that your Platform SDK is up to date and that _WIN32_WINNT is defined to at least 0x0501. If it isn't, the compiler won't give you access to the UxTheme API and you will get a compilation error. This can be done by adding a preprocessor definition in the settings of your projects or by adding the proper #define statement before the first inclusion of windows.h in your project.

//stdafx.h
#define _WIN32_WINNT 0x0501
#include <windows.h>

Every theme-specific function is imported at runtime; this means that your EXE/DLL won't have any additional external dependency caused by CRichEditThemed. The only files you must include in your project are RichEditThemed.cpp and RichEditThemed.h. The class has only one public function that creates a new object in memory and attaches the specified Rich Edit control to the newly created object. Everything else, including destruction and memory management, is taken care of internally by the class.

public static bool CRichEditThemed::Attach(HWND hRichEdit);

The CRichEditThemed::Attach() function must be called during the creation of the Rich Edit control's parent window, before the control is displayed onscreen for the first time. This is generally achieved by making the call when processing WM_CREATE or WM_INITDIALOG. The object is not designed to attach on a control that is already displayed onscreen and usable; that's about the only thing you will have to keep in mind. Here is a code snippet showing how to use the class:

HWND hRichEdit = GetDlgItem(m_hParent, IDC_RICHEDIT);
CRichEditThemed::Attach(hRichEdit);

After CRichEditThemed::Attach() has been called, the Rich Edit control will be subclassed by CRichEditThemed until its destruction. The control is subclassed only and always in Windows XP (or above, as long as the UxTheme.dll library can be found). The control will be subclassed even if themes are not enabled at the time of creation; this allows proper theme support in case of a theme change while the parent window is still open. The class also takes care of redrawing the control after it has been enabled/disabled or when its style changes (to alter the border after creation, for example). The class will only draw the theme if the Rich Edit control has the WS_BORDER style because it supposes that only the edges of the control are themed (which should always be the case for any "editbox" theme that makes sense).

The only known limitation concerns the use of the "Auto HScroll" style in conjunction with both "Horizontal Scroll" and "Vertical Scroll." When both scrollbars are displayed, the intersection of the two bars is drawn automatically by the control and, as a result, it doesn't look as good as it would look in a native themed control. Because most programmers prefer not to use "Auto HScroll" with Rich Edit controls displayed in dialog boxes (it often makes the control more difficult to use), this shouldn't be too much of a problem. (Sorry if it is for you; however, the control will still look much better with the class than without.) Apart from that, you shouldn't notice any difference between native edit controls and your newly transformed Rich Edit controls. If you do, it's a bug and it will need to be reported and be fixed.

Dissecting the Class

This section is for curious people who want to know how the class works internally (or, if you're like me, you just want to check the code yourself before you include it in your software, which is just as justified). The explanations given here are not needed to use the class; however, they should give you enough insights to be able to use the same technique for other kinds of controls. A lot of comments have been added in the code. It should be self explanatory for the most part; however, if you're not familiar with the concept of themes and non-client areas, this section of the article should fill the gaps. The following tutorial has been written in chronological manner and is meant to be read from beginning to end, every sub-section supposing that the previous section has been read and understood.

Personal note: Subclassing is one of my favourite ways of working (which you already know if you're a user of Messenger Plus!). Great things can be done in no time with this technique and almost anything in Windows can be shortcut to attain one's goal. It is thanks to subclassing that CRichEditThemed doesn't need to be instantiated the "usual way." Once created, the object monitors the actions of the Rich Edit control it's been attached to. Because the control is guaranteed to receive a WM_DESTROY message when its parent is destroyed, you know you can count on it to destroy the attached CRichEditThemed object as well. You don't need to be familiar with subclassing techniques to follow this tutorial; however, you do need to know what a message loop is and how Windows communicates with the many windows/controls created in its environment.

I'll start the tutorial with the public entry point of the class. As stated previously, the only public method of CRichEditThemed is CRichEditThemed::Attach() and it is responsible for doing two things: First, it tries to import the functions it will require later on from the UxTheme library. If the user is running an older version of Windows, this library will not be available and CRichEditThemed won't attach itself to the Rich Edit control. Then, after a couple of basic verifications, a new CRichEditThemed object is created:

bool CRichEditThemed::Attach(HWND hRichEdit)
{
   if(IsWindow(hRichEdit))
   {
      //Prevent double subclassing
      if(CRichEditThemed::m_aInstances.find(hRichEdit) ==
         CRichEditThemed::m_aInstances.end())
      {
         //If this function fails, this version of Windows doesn't
         //support themes
         if(InitLibrary())
         {
            //Note: The object will be automatically deleted when
            //the richedit control dies.
            CRichEditThemed *obj = new CRichEditThemed(hRichEdit);
            return true;
         }
      }
   }
   return false;
}

The creation of a new object involves two very distinct operations. First, you need to subclass the Rich Edit control with a call to SetWindowLong(). Subclassing is an operation consisting of replacing the message loop procedure of a window (also called winproc) with a new custom one. The new winproc is responsible for relaying all the messages it receives to the original procedure; this gives the flexibility needed to override any message or silent some of them when needed.

The winproc is a static function, so you need a way to associate the Rich Edit control with its attached CRichEditThemed object. For that purpose, an STL map is used, taking the handle of the control as key and a pointer to the CRichEditThemed object as an element. Because subclassing is a subject on its own, I will not spend more time talking about it. You are encouraged to check how this part of the class works and get yourself a good book on the Windows API if the matter interests you.

The second operation performed by the constructor consists of verifying the current state of the Rich Edit control by calling CRichEditThemed::VerifyThemedBorderState(). This function is in charge of deciding whether or not a themed border should be drawn around the Rich Edit control. It is called every time the status of the control changes.

CRichEditThemed::CRichEditThemed(HWND hRichEdit)
   : m_hRichEdit(hRichEdit), m_bThemedBorder(false)
{
   //Subclass the richedit control; this way, the caller doesn't
   //have to relay the messages by itself
   m_aInstances[hRichEdit] = this;
   m_pOriginalWndProc = (WNDPROC)SetWindowLong(hRichEdit,
                        GWL_WNDPROC,
                        (LONG)&RichEditStyledProc);

   //Check the current state of the richedit control
   ZeroMemory(&m_rcClientPos, sizeof(RECT));
   VerifyThemedBorderState();
}

XP Theme Support for Rich Edit and Custom Controls

Now, take a look at the first juicy part of the class, the CRichEditThemed::VerifyThemedBorderState() method. This method is very short; it doesn't do anything out of the ordinary, but it is vital to the rest of the class. Its main purpose is to verify whether the control has a border style and, if so, to remember it and to remove the style. Actually, checking for the style enables the class to function properly in the event you would like one of your controls to be displayed without any border, themed or not. Removing the style prevents the Rich Edit control from displaying its default border. This step is very important due to the nature of the class: CRichEditThemed draws the theme in the non-client area of the control; however, you must remember that this area also can be used by the control to display other elements such as its scrollbars.

You don't want to take the responsibility of overriding more than what's absolutely necessary to draw your theme; the best way to achieve that is to let the Rich Edit control do everything it is supposed to do. When a border style is set, the control reserves the necessary space in its non-client area to display its border (see WM_NCCALCSIZE next); however, this space is not guaranteed to be the same space needed to draw a themed border so you would end up with an important problem. Why can't you simply detect the size of the original border and do some arithmetic to balance it with the size you need? Simply because you don't know for sure what the control is calculating, how it adjusted its border in relation with other elements (such as its scrollbars), and so forth. Preventing the control from drawing its normal border solves the entire problem and leaves you with a clean way to do your job.

void CRichEditThemed::VerifyThemedBorderState()
{
   bool bCurrentThemedBorder = m_bThemedBorder;
   m_bThemedBorder = false;

   //First, check whether the control is supposed to have a border
   if(bCurrentThemedBorder
      || (GetWindowLong(m_hRichEdit, GWL_STYLE) & WS_BORDER
      || GetWindowLong(m_hRichEdit, GWL_EXSTYLE) & WS_EX_CLIENTEDGE))
   {
      //Check whether a theme is presently active
      if(pIsThemeActive())
      {
         //Remove the border style; you don't want the control to
         //draw its own border
         m_bThemedBorder = true;
         if(GetWindowLong(m_hRichEdit, GWL_STYLE) & WS_BORDER)
         {
            SetWindowLong(m_hRichEdit, GWL_STYLE,
               GetWindowLong(m_hRichEdit, GWL_STYLE)^WS_BORDER);
         }
         if(GetWindowLong(m_hRichEdit, GWL_EXSTYLE) &
                          WS_EX_CLIENTEDGE)
         {
            SetWindowLong(m_hRichEdit, GWL_EXSTYLE,
                          GetWindowLong(m_hRichEdit, GWL_EXSTYLE)^
                          WS_EX_CLIENTEDGE);
         }
      }
   }

   //Recalculate the NC area and repaint the window
   SetWindowPos(m_hRichEdit, NULL, NULL, NULL, NULL, NULL,
      SWP_NOMOVE|SWP_NOSIZE|SWP_NOZORDER|SWP_NOACTIVATE|
                 SWP_FRAMECHANGED);
   RedrawWindow(m_hRichEdit, NULL, NULL,
      RDW_INVALIDATE|RDW_NOCHILDREN|RDW_UPDATENOW|RDW_FRAME);
}

Now that the border style has been removed, you need to ask the control to recalculate the dimensions of its non-client area. This operation is achieved by a special call to SetWindowPos() with the SWP_FRAMECHANGED parameter. This function will send a WM_NCCALCSIZE message to the control which brings you to the second juicy part of the class: handling the WM_NCCALCSIZE message. First, take a look at how the message is received by your winproc.

if(uMsg == WM_NCCALCSIZE)
{
   //If wParam is FALSE, you don't need to make any calculation
   if(wParam)
   {
      //Ask the control to first calculate the space it needs
      LRESULT nOriginalReturn =
         CallWindowProc(pObj->m_pOriginalWndProc, hwnd, uMsg,
                        wParam, lParam);

      //Alter the size for your own border, if necessary
      NCCALCSIZE_PARAMS *csparam = (NCCALCSIZE_PARAMS*)lParam;
      if(pObj->OnNCCalcSize(csparam))
         return WVR_REDRAW;
      else
         return nOriginalReturn;
   }
}

It is important to notice that the class manipulates the dimensions of the non-client area only after it's been changed by the control. This is done to ensure that the control keeps ignoring the fact that you're changing some of its default behaviour, which is safer for both the control and your class. The Rich Edit control is a black box; you can't presume to know it handles the WM_NCCALCSIZE message in every situation. You now can take a look at what CRichEditThemed::OnNCCalcSize() really does.

bool CRichEditThemed::OnNCCalcSize(NCCALCSIZE_PARAMS *csparam)
{
   //Here, you indicate to Windows that the non-client area of the
   //richedit control is not what it thinks it should be. This
   //gives you the necessary space to draw the special border
   //later on.
   if(m_bThemedBorder)
   {
      //Load the theme associated with edit boxes
      HTHEME hTheme = pOpenThemeData(m_hRichEdit, L"edit");
      if(hTheme)
      {
         bool bToReturn = false;

         //Get the size required by the current theme to be
         //displayed properly
         RECT rcClient; ZeroMemory(&rcClient, sizeof(RECT));
         HDC hdc = GetDC(GetParent(m_hRichEdit));
         if(pGetThemeBackgroundContentRect(hTheme, hdc,
                                           EP_EDITTEXT, ETS_NORMAL,
                                           &csparam->rgrc[0],
                                           &rcClient) == S_OK)
         {
            //Add a pixel to every edge so that the client area is
            //not too close to the border drawn by the theme (thus
            //simulating a native edit box)
            InflateRect(&rcClient, -1, -1);

            m_rcClientPos.left =
               rcClient.left-csparam->rgrc[0].left;
            m_rcClientPos.top =
               rcClient.top-csparam->rgrc[0].top;
            m_rcClientPos.right =
               csparam->rgrc[0].right-rcClient.right;
            m_rcClientPos.bottom =
               csparam->rgrc[0].bottom-rcClient.bottom;

            memcpy(&csparam->rgrc[0], &rcClient, sizeof(RECT));
            bToReturn = true;
         }
         ReleaseDC(GetParent(m_hRichEdit), hdc);
         pCloseThemeData(hTheme);

         return bToReturn;
      }
   }

   return false;
}

XP Theme Support for Rich Edit and Custom Controls

A call to OpenThemeData() gets the handle to the theme you want to use which, in turn, is used to make a call to the most important function of the CRichEditThemed::OnNCCalcSize method: GetThemeBackgroundContentRect(). This function of the UxTheme library is extremely useful because it automatically calculates the necessary space needed to draw the theme of a given element. The input rectangle parameter you use here, csparam->rgrc[0], is the first of three rectangles sent in the WM_NCCALCSIZE message. The second and third rectangles are given as reference only (more information on these parameters can be found in the MSDN Library) and are not needed by your class. The output of GetThemeBackgroundContentRect() goes in rcClient and represents the final dimensions of the client area (which is equal to the dimensions of the entire control window minus the space required by the theme borders). The result is finally sent back to the caller as result of the WM_NCCALCSIZE message, in csparam->rgrc[0].

The last thing you need to do before the function returns is to remember the size of the gap between the edges of the non-client area and the edges of the client area. This piece of information will be useful later on when you process the WM_NCPAINT message that represents the third and last juicy part of this article. This message is sent to the control to request non-client painting. It is handled by CRichEditThemed quite the same way WM_NCCALCSIZE is: It is first sent to the control for default processing. The class then completes the job with a call to CRichEditThemed::OnNCPaint(), which is shown here:

bool CRichEditThemed::OnNCPaint()
{
   if(m_bThemedBorder)
   {
      HTHEME hTheme = pOpenThemeData(m_hRichEdit, L"edit");
      if(hTheme)
      {
         HDC hdc = GetWindowDC(m_hRichEdit);

         //Clip the DC so that you only draw on the non-client area
         RECT rcBorder;
         GetWindowRect(m_hRichEdit, &rcBorder);
         rcBorder.right -= rcBorder.left; rcBorder.bottom
                        -= rcBorder.top;
         rcBorder.left = rcBorder.top = 0;

         RECT rcClient; memcpy(&rcClient, &rcBorder, sizeof(RECT));
         rcClient.left   += m_rcClientPos.left;
         rcClient.top    += m_rcClientPos.top;
         rcClient.right  -= m_rcClientPos.right;
         rcClient.bottom -= m_rcClientPos.bottom;
         ExcludeClipRect(hdc, rcClient.left, rcClient.top,
                         rcClient.right, rcClient.bottom);

         //Make sure the background is in a proper state
         if(pIsThemeBackgroundPartiallyTransparent(hTheme,
            EP_EDITTEXT, ETS_NORMAL))
            pDrawThemeParentBackground(m_hRichEdit, hdc, &rcBorder);

         //Draw the border of the edit box
         int nState;
         if(!IsWindowEnabled(m_hRichEdit))
            nState = ETS_DISABLED;
         else if(SendMessage(m_hRichEdit, EM_GETOPTIONS, NULL, NULL)
                 & ECO_READONLY)
            nState = ETS_READONLY;
         else
            nState = ETS_NORMAL;

         pDrawThemeBackground(hTheme, hdc, EP_EDITTEXT, nState,
                              &rcBorder, NULL);
         pCloseThemeData(hTheme);

         ReleaseDC(m_hRichEdit, hdc);
         return true;
      }
   }

   return false;
}

The function starts just like CRichEditThemed::OnNCCalcSize() by calling OpenThemeData() to obtain a handle to the theme you will draw. Then, the client area dimensions are calculated using the m_rcClientPos rectangle previously stored while processing the WM_NCCALCSIZE message. This gives you the possibility to completely exclude the client area from your future painting operations (using ExcludeClipRect()). This step is necessary because, technically speaking, the theme doesn't draw a border but a background (which includes a border) and you don't want this background to be displayed over the client area.

Before you proceed with drawing the background/border of your control, you need to perform one last verification. The UxTheme library supports transparency and alpha-bending effects that put additional responsibility on the shoulders of the control: The control needs to make sure that the background of its parent has been redrawn before it can draw its own transparent background. If you're not familiar with transparent painting, this concept may sound a little strange, so here's an example to illustrate the problem. Consider an empty window, with a white background, where you want to display a picture containing alpha-bending effects (such as a PNG file). This picture has three kinds of parts: Some pixels are totally transparent, some pixels are partially transparent, and some pixels are completely opaque. What do you think will happen if, for some reason, your picture is drawn on top of itself before the background had a chance to be filled with white? This kind of situation can happen for a lot of reasons, including optimisation and anti-flickering tricks. In that situation, the pixels that are totally transparent will stay white, the pixels that are completely opaque will replace what was already displayed, but the pixels that are partially transparent will be drawn incorrectly, causing a display bug.

Each pixel of the picture has its own percentage of opacity. When drawn for the first time, the opacity is applied to the color of the background pixel to generate the proper color. If the picture is drawn a second time on top of itself, the background color used to compute the look of each pixel becomes the color resulting from the first draw, causing an undesired darkened effect. For example, consider a black pixel RGB(0, 0, 0) being displayed on a white background RGB(255, 255, 255) with an opacity of 50%. The resulting pixel will be grey because it will represent 50% of the white color, which is RGB(127, 127, 127). Now, if you draw the same pixel again on top of the existing one, the 50% of opacity will be applied on RGB(127, 127, 127), resulting in a darker grey pixel RGB(63, 63, 63). Windows is not designed to let a child control have this kind of consideration; that's why alpha-bending effects can cause so much trouble. Fortunately, the UxTheme library provides you with two helper functions to solve this issue: IsThemeBackgroundPartiallyTransparent() and DrawThemeParentBackground().

The IsThemeBackgroundPartiallyTransparent() function is called first to check whether some parts of the control's background are transparent. If it returns TRUE, the portion of the parent obscured by the control must be redrawn. In that situation, DrawThemeParentBackground() is called to do the drawing, which solves the issue of mixed transparency effects. The only thing left to do in CRichEditThemed::OnNCPaint() is to draw the actual background/border of your Rich Edit control; this is done with a simple call to DrawThemeBackground(). The state parameter is determined by checking whether the control is disabled or in read-only mode (this is required because the theme can provide different backgrounds for each of these states).

And... that's it, you're done! The only thing left to be done is to ensure that the class will adapt itself to theme changes and style modifications. This is done easily by intercepting the following messages:

if(uMsg == WM_THEMECHANGED || uMsg == WM_STYLECHANGED)
{
   //Someone just changed the style of the richedit control or the
   //user changed its theme. Make sure the control is being kept
   //up to date by verifying its state
   pObj->VerifyThemedBorderState();
}

if(uMsg == WM_ENABLE)
{
   //Redraw the border depending on the state of the richedit control
   RedrawWindow(hwnd, NULL, NULL, RDW_INVALIDATE|RDW_NOCHILDREN|
                RDW_UPDATENOW|RDW_FRAME);
}

I hope this little tutorial will be of some help to those of you who want to experiment with subclassing, theme support, and non-client areas. Subclassing allowed you to create a neat, encapsulated implementation of your Themed RichEdit class. Letting the parent draw the theme is another "solution;" however, it would most certainly result in bad, hard to manage, non-reusable code. I encourage anybody wanting to add theme support to his/her own creations to use the code presented in this article. Even if my goal was to create a specialised class for Rich Edit controls, the code listed here is still pretty much generic and only a couple of small modifications would be required to support other controls. I hope the reading of this article was enjoyable. I'll be waiting for your comments. There's always room for improvement!

Patchou, November 27th 2004



Downloads

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • A global data storage provider whose business is booming needed a best-in-class data center to serve as the backbone of its technical operations going forward—and it needed it delivered within a year.

  • Live Event Date: September 16, 2014 @ 11:00 a.m. ET / 8:00 a.m. PT Are you starting an on-premise-to-cloud data migration project? Have you thought about how much space you might need for your online platform or how to handle data that might be related to users who no longer exist? If these questions or any other concerns have been plaguing you about your migration project, check out this eSeminar. Join our speakers Betsy Bilhorn, VP, Product Management at Scribe, Mike Virnig, PowerSucess Manager and Michele …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds