Monday, August 4, 2008

Machinations to Set Text Color in Win32

Copyright © 2008, Steven E. Houchin
Recently, I wanted to set the text color for a Win32 Static control within a dialog window. I'm a relative novice when it comes to old fashioned Win32 message pump applications, so I had to scrounge around for the right API calls to do this. I assumed there must be a simple one-time call to set the text attributes for a given control. Silly me.
It took some digging to discover that the answer was to handle the WM_CTLCOLORSTATIC message. The M*soft documentation states that this message is sent when "a static control, or an edit control that is read-only or disabled, ... is about to be drawn." In other words, the color is modified right before the text gets drawn. It seems you have to do this each and every time you wish to draw the text, not just as a one-time setup. So I wrote this:

case WM_CTLCOLORSTATIC:
{
     HDC hDC = (HDC)wParam;
     ::SetTextColor(hDC, RGB(255,0,0)); // red text
     return TRUE;
}

Seems simple enough. The result? Plain old black text. It turns out that WM_CTLCOLORSTATIC requires an HBRUSH as its return value. Since TRUE isn't a valid brush, it ignored the whole thing. So, where do I get a brush? The documentation says the brush will be used to paint the control's background. I tried this:

return (LRESULT)::GetStockObject(HOLLOW_BRUSH);

The result was red text with each character on a white background. Plus, the old text wasn't erased when new text was written to the control. I realized that HOLLOW_BRUSH is essentially no brush, so the old text wasn't getting wiped. I needed the brush for the control's plain old gray background. So, I tried this:

...
COLORREF bg = ::GetBkColor(hDC);
HBRUSH hb = ::CreateSolidBrush(bg);
return (LRESULT)hb;

Surely this would work. Nice try, but no cigar. This fixes the problem of ghost text hanging around. But it has the side effect of turning the control's background entirely white. In other words, GetBkColor() is returning white, even though the background of the dialog is the usual gray. So, two problems still remain: the character background is white, and now the control's background is white. For the first problem, I discovered this:

...
::SetBkMode(hDC, TRANSPARENT);
::SetTextColor(hDC, RGB(255,0,0)); // red text
...

By temporarily forcing the returned brush to a stock light gray (using GetStockObject()), I could see that the text backgound was now what I wanted: red text on the control's background color (stock light gray).
So, where could I get the dialog's usual gray background to return as a brush? After lots of searching around, I discovered the answer: send a WM_CTLCOLORDLG message. This message returns the background brush for the dialog box, as long as I'm not handling that message myself in DlgProc() - which could cause a chicken-and-egg problem.

...
HBRUSH hBrush = (HBRUSH)SendMessage(
     hDlg,
     WM_CTLCOLORDLG,
     (WPARAM)hDC,
     lParam /* the control's HWND */);
     return (LRESULT)hBrush;

I executed this and ... ugh! This set me all the way to black text on the gray background. My previous attempts told me that the problem was an invalid brush. Later, I was moving around the code into separate functions, and it accentally started to work. I had done this:

...
HBRUSH hBrush = (HBRUSH)SendMessage(
     hDlg,
     WM_CTLCOLORDLG,
     GetWindowDC((HWND)lParam),
     lParam /* the control's HWND */);

Voila! A miracle occured. I finally see the result I wanted from the start: red text on the standard gray background. All of that work just to draw RED TEXT!
I'm guessing that the HDC provided via 'wParam' is for the text
glyphs, which is different than the HDC associated with the control's HWND via 'lParam'. Here's the final magic code:

DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
...
case WM_CTLCOLORSTATIC:
{   
     // handle just for my MSG control
     if ((HWND)lParam == GetDlgItem(m_hDlg, IDC_STATIC_MSG))
     {
          HDC hDC = (HDC)wParam;  // use for the text
          ::SetBkMode(hDC, TRANSPARENT);
          ::SetTextColor(hDC, RGB(255,0,0)); // red text
          HBRUSH hb = (HBRUSH)SendMessage(
               hDlg,
               WM_CTLCOLORDLG,
               (WPARAM)GetWindowDC((HWND)lParam),
               lParam /* the control's HWND */);
          return (LRESULT)hb;
     }
}

The WM_CTLCOLORDLG also works by using 'hDlg' instead of 'lParam', even though it results in different hDC's. It would also be possible to send a WM_CTLCOLORSTATIC message itself to get an HBRUSH, but that seems like it could result in an infinite loop - sending a message from the same message's handler. I suppose a different target HWND might avoid that. So, it now works, but I don't know all the details under the hood.