Patchou's Cabana

The personal blog of Patchou

Theming Rich Edit and Custom Controls Page 2

Let’s 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 if 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 can also be used by the control to display other elements such as its scrollbars. We don’t want to take the responsibility of overriding more than what’s absolutely necessary to draw our theme and 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 we would end up with an important problem. Why can’t we simply detect the size of the original border and do some arithmetic to balance it with the size we need? Simply because we don’t know for sure what the control is calculating, how it adjusted its border in relation with other elements (such as its scrollbars), etc… preventing the control to draw its normal border solves the entire problem and leaves us with a clean way to do our job.

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

   //First, check if 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 if a theme is presently active
      if(pIsThemeActive())
      {
         //Remove the border style, we 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, we 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 us to the second juicy part of the class: handling the WM_NCCALCSIZE message. First, let’s take a look at how the message is received by our winproc.

if(uMsg == WM_NCCALCSIZE)
{
   //If wParam is FALSE, we 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 our 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 we’re changing some of its default behaviour which is the safer for both the control and our class. The Rich Edit control is a black box, we can’t presume to know it handles the WM_NCCALCSIZE message in every situation. We can now take a look at what CRichEditThemed::OnNCCalcSize() really does.

bool CRichEditThemed::OnNCCalcSize(NCCALCSIZE_PARAMS *csparam)
{
   //Here, we indicate to Windows that the non-client area of the richedit control is
   //not what it thinks it should be. This gives us 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;
}

A call to OpenThemeData() gets the handle to the theme we wish 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 as it automatically calculates the necessary space needed to draw the theme of a given element. The input rectangle parameter we 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 our 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 we 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 we process the WM_NCPAINT message which 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 we 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 we will draw. Then, the client area dimensions are calculated using the m_rcClientPos rectangle previously stored while processing the WM_NCCALCSIZE message. This gives us the possibility to completely exclude the client area from our future painting operations (using ExcludeClipRect()). This step is necessary as, technically speaking, the theme doesn’t draw a border but a background (which includes a border) and we don’t want this background to be displayed over the client area.

Before we proceed with drawing the background/border of our control, we need to perform one last verification. The UxTheme library supports transparency and alpha-bending effects which puts 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 let’s take an example to illustrate the problem. Let’s consider an empty window, with a white background, where you wish 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 which will cause 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 darken effect. For example, let’s consider a black pixel RGB(0, 0, 0) being displayed on a white background RGB(255, 255, 255) with an opacity of 50%. There resulting pixel will be grey as 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 and that’s why alpha-bending effects can cause so much trouble. Fortunately, the UxTheme library provides us with two helper functions to solve this issue: IsThemeBackgroundPartiallyTransparent() and DrawThemeParentBackground().

The IsThemeBackgroundPartiallyTransparent() function is called first to check if some parts of the control’s background is 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 our Rich Edit control which is done with a simple call to DrawThemeBackground(). The state parameter is determined by checking if the control is disabled or in read-only mode (this is required as the theme can provide different backgrounds for each of these states).

And… that’s it, we’re done! The only thing left to be done is to ensure that the class will adapt itself to theme changes and style modifications which 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 us to create a neat, encapsulated implementation of our 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!

Pages: 1 2    «

  • Archive

  • Categories

  • Blogroll