How do I get a radio button control to render its text transparently?


Commenter Andrei asks via the Suggestion Box for help with making the text transparent using WM_CTL­COLOR­STATIC. "Instead of the radio button now there's a black background."

Let's look at this problem in stages. First, let's ignore the transparent part and figure out how to render text without a black background. The background color of the text comes from the color you selected into the DC when handling the WM_CTL­COLOR­STATIC message. And if you forget to set a background color, then you get whatever color is lying around in the DC, which might very well be black. Start with the scratch program and make these changes, which I'm going to write in the way I think Andrei wrote it, even though it doesn't fit the style of the rest of the scratch program.

HBRUSH g_hbr;
BOOL
OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
{
    g_hwndChild = CreateWindow(TEXT("button"), TEXT("Bingo"),
        WS_CHILD | WS_VISIBLE | BS_RADIOBUTTON,
        0, 0, 0, 0, hwnd, (HMENU)1, g_hinst, 0);
    g_hbr = CreateSolidBrush(RGB(0xFF, 0x00, 0xFF)); // hot pink
    return TRUE;
}

void
OnDestroy(HWND hwnd)
{
    if (g_hbr) DeleteObject(g_hbr);
    PostQuitMessage(0);
}

// add to WndProc

  case WM_CTLCOLORSTATIC:
    if (GetDlgCtrlID(
             GET_WM_CTLCOLOR_HWND(wParam, lParam, uiMsg)) == 1) {
      return (LRESULT)g_hbr; // override default background color
    }
    break;

If you run this program, the radio button's background is indeed hot pink, well except for the text, where the color is, I dunno, it's white on my machine, but who knows what it is on yours. Since we didn't specify a color, the result is undefined. The bug here is that we handled the WM_CTL­COLOR­STATIC message incompletely. The WM_CTL­COLOR family of messages requires that the message handler do three things:

  1. Set the DC text color.
  2. Set the DC background color.
  3. Return a background brush.

We got so excited about the background brush that we forgot the other two steps. Let's fix that.

case WM_CTLCOLORSTATIC:
    if (GetDlgCtrlID(
             GET_WM_CTLCOLOR_HWND(wParam, lParam, uiMsg)) == 1) {
      HDC hdc = GET_WM_CTLCOLOR_HDC(wParam, lParam, uiMsg);
      SetTextColor(hdc, RGB(0xFF, 0xFF, 0x00)); // yellow
      SetBkColor(hdc, RGB(0xFF, 0x00, 0xFF)); // hot pink
      return (LRESULT)g_hbr; // override default background color
    }
    break;

(Just for fun, I chose yellow as the text color.) Now that we specified the text color and the background color, the text appears in the correct colors.

Note that we didn't actually do anything transparently here. We just made sure that the background color we told the control to use for text matches the color we told the control to use for erasing the background. The effect looks transparent since the two colors match.

But what if you really wanted transparency instead of fake transparency? To illustrate, let's give the control a background that is not a solid color:

BOOL
OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
{
    g_hwndChild = CreateWindow(TEXT("button"), TEXT("Bingo"),
        WS_CHILD | WS_VISIBLE | BS_RADIOBUTTON,
        0, 0, 0, 0, hwnd, (HMENU)1, g_hinst, 0);
    g_hbr = CreatePatternBrushFromFile(
                          TEXT("C:\\Windows\\Gone Fishing.bmp"));
    return TRUE;
}

When you run this version of the program, the radio button background consists of the Gone Fishing bitmap. (Of course, if you don't have that bitmap, then feel free to substitute another bitmap. I can't believe I had to write that.) But the text is still yellow on pink. How do we get it to be yellow on the complex background?

By setting the background mix mode to TRANSPARENT.

case WM_CTLCOLORSTATIC:
    if (GetDlgCtrlID(
             GET_WM_CTLCOLOR_HWND(wParam, lParam, uiMsg)) == 1) {
      HDC hdc = GET_WM_CTLCOLOR_HDC(wParam, lParam, uiMsg);
      SetTextColor(hdc, RGB(0xFF, 0xFF, 0x00)); // yellow
      SetBkColor(hdc, RGB(0xFF, 0x00, 0xFF)); // hot pink
      SetBkMode(hdc, TRANSPARENT);
      return (LRESULT)g_hbr; // override default background color
    }
    break;

According to the documentation, the background mix mode "is used with text, hatched brushes, and pen styles that are not solid lines." It's the text part we care about here. When the control does its Text­Out to draw the control text, the background mix mode causes the text to be rendered transparently.

Exercise: There's actually one more thing you need to do, but I conveniently arranged the program so you didn't notice. What other step did I forget?

Comments (20)
  1. Lonnie McCullough says:

    You still need to translate the brush origin with a call to SetBrushOrgEx.  Because the radio button is located at (0,0) this happened to be unnessecary in your sample, but it will look horrible if the radio button is located anywhere but at (0,0).

  2. Wizou says:

    Why did you use WM_CTLCOLORSTATIC rather than WM_CTLCOLORBTN for handling the color of a "button" ?

    (Maybe that's the point of the exercise)

  3. Chris Becke says:

    I'm going to feel stupid im sure when I see the actual missing step. My potential missing steps are: Didn't set the brush as the window's background brush? Didn't add the manifestdependency to use xp-themed controls? Didn't provide the code for CreatePatternBrushFromFile? The radio button also has a zero width and height. All easy to fix. Damnit I actually compiled and ran this, and thats all I did.

  4. Sebastian says:

    Would it be possible to just return a handle to the NULL_BRUSH?

    return (LRESULT)g_hbr = GetStockObject(NULL_BRUSH);

  5. Chris Becke says:

    No – passing a NULL brush looks ok, until the window is invalidated. Then the area of the control is simply not repainted leading to ugly artifacts.

  6. Anonymous Coward says:

    Actually, this is not at all close to how you should do this. You can run into all kinds of problems, especially when in your current style the background is not uniform.

    You must have the parent control help your radio button erase its background. If you do it any other way you end up in well known anti-pattern: whitelist programming (so called because for every combination of situations you encounter you implement a fix, adding it as it were to the ‘whitelist’, hoping that eventually you'll have covered every eventuality).

  7. Lonnie McCullough says:

    The code to call SetBrushOrgEx looks something like this:

    POINT ptChild = { 0, 0 };

    MapWindowPoints( g_hwndChild, hwnd, &ptChild, 1 );

    SetBrushOrgEx( hdc, ptChild.x, prChild.y, NULL );

  8. Sebastian says:

    Now let's assume I have the radio button in a themed tab control. The tab control is painted with using DrawThemeBackground. This has a gradient in Windows XP.

    I often see code that paints the background of the radio button using DrawThemeParentBackground inside the WM_CTLCOLORSTATIC message and then returns the NULL_BRUSH. That is why I was asking about returning the NULL_BRUSH. That code appears to work, but painting inside WM_CTLCOLORSTATIC doesn't feel quite right. I suppose I would have to return a brush that paints the parent background. But I don't have a brush, because the parent is painted using DrawThemeBackground without using a brush. How do I get the brush that paints the background correctly?

  9. Chris Becke says:

    This is why I brought the xp-theming up. I havn't found any documentation, but it appears that when common controls 6 are applying xp theming, the rules for handling WM_CTLCOLORxxx change: I have a dialog handler that looks a lot like this :-

     HDC hdc;

     case WM_CTLCOLORSTATIC:

       hdc = (HDC)wParam;

       SetTextColor(hdc,RGB(0xff,00,00));

       SetBkMode(hdc,TRANSPARENT);

       if(IsAppThemed() && IsComctl6InManifest())

         return FALSE;

       return (BOOL)GetSysColorBrush(COLOR_BTNFACE);

    returning FALSE means that DefDlgProc should pass the message on to DefWindowProc() – comctl6 seems to ensure that the bk mode and text color in the HDC are preserved rather than reset in the case of a NULL HBRUSH return. I'm not happy with this code without any official note confirming it – but its way better than returning NULL_BRUSH.

    ["returning FALSE means that DefDlgProc should pass the message on to DefWindowProc" – true in general but not true for WM_CTLCOLORSTATIC, which is on the special exceptions list. -Raymond]
  10. Limited_Atonement says:

    I looked for a post on which I could comment where this would be more relevant, but couldn't find it.  So, now you can delete this off-topic question.

     Every time I'm confronted with a resizeable window in MS Windows (the new open file dialog), I breathe a small sigh of, well, I guess it's not relief, but it's happiness of some sort.  Why aren't all Windows windows resizeable by default?  By this I mean that most windows ought to be resizeable unless there's a good reason not to.  And I don't mean to just make the border resizeable with no extra logic, I can do that.  For instance, Visual Studio 2008 Tools > Options window.  How hard would it have been to write a little extra code for resizing those panels in there?  Especially windows with humongous listboxes like the one in General > Keyboard which, on my screen, only shows about 3.8 items on it, but it contains … upwards of two hundred??  I'm sure people have complained about this particular dialog before, but I can't find that dialogue.

     What are your thoughts?

    [Somebody who says "How hard would it have been to write a little extra code for resizing those panels in there?" has never tried writing that little extra code for resizing panels. -Raymond]
  11. Anonymous Coward says:

    According to the docs for WM_CTLCOLORSTATIC:

    If a dialogue box procedure handles this message, it should cast the desired return value to a INT_PTR and return the value directly. If the dialogue box procedure returns FALSE, then default message handling is performed. The DWL_MSGRESULT value set by the SetWindowLong function is ignored.

    Regarding the resizeable dialogues discussion:

    By the early nineties other operating systems / window managers, as well as user interface tool-kits, generally supported this. And it was sorely needed – I still remember the pain of trying to access bits of Windows 98 dialogues that were hidden from view even though the resolution was normally set from within Windows; not even a scrollbar to help me out. And conversely I still bump into dialogues that have tiny lists to accommodate small screens / low resolutions. Resizing dialogues were added to Windows proper in 2006 or so, but only for Dotnet, not for native applications.

  12. Chris Becke says:

    @The resizable dialog discussion:

    Waving Spy++ over various windows dialogs that show "neat" resizing behaviours – the Windows XP fast user switching screen, various parts of explorer (including some control panel views) use a window class called DirectUI to get some kind of subset of "Easy layout & animation" to non-managed win32 apps. [2x size AA Sarcastic statements blaming Raymond personally for Microsoft not making some internal technology available to developers not included].

  13. steveg says:

    @Limited Atonement: Like Raymond says it's just not that easy using the Win32 API, you have manually recalculate the positions of all your sub controls (and sub-sub controls), move them, repaint them, deal with flickering, bounds checking, inevitable "off by 1" errors, minimum size, maximum size etc… It's really quite involved. Other windows UI frameworks (built on top of Win32 at somepoint) do provide resizing functionality for free.

    What I do when I *really* hate a dialog box is to edit the dialog template in the DLL or EXE that it resides. Sometimes this isn't possible because the application uses its own template system (hello MS Office), but quite often you can make those list boxes bigger.

  14. mdw says:

    Resizing dialog(ue)s…

    The fundamental problem here is that, unless you do a bunch of work yourself, Windows dialogues are just bunches of controls with specified positions and sizes.  There's no structure there to tell anything how controls should move and/or resize when you resize the dialogue.

    Resizing dialogues (called `geometry management') has been a standard part of user interface toolkits for the X Window System for many years.  Good ol' Xt has a notion of container widgets which manage their children.  Other toolkits follow the tradition: Tk, Gtk, Qt, Wx, and even Java.

    There's a tradeoff here.  Absolute positioning of controls makes design simple, and you can get exactly the result you wanted.  Building hierarchies of container widgets is more complicated, and it can be tricky (or even impossible) to get the precise layout you want.

    On the other hand, the more abstract, structured layout usually only needs doing once.  You can change the text in label widgets and the geometry management will magically arrange everything more-or-less sensibly.  More modern toolkits have notions of both physical directions (left, right, above, below) and reading directions (before, after), so you don't even need to worry about that.

    If you redesign your absolutely positioned dialogue for each language, you can lay them out differently, e.g., if some label is annoyingly long in German.  Doing this sort of thing with managed geometry is messy: unless you have very sophisticated (read: hard to configure) geometry managers, you'll end up writing custom geometry management code, building different designs for different languages anyway, or (most likely) compromising the aesthetics of some translations for the benefit of having an easy life.

    Of course, as Raymond described years ago, Windows doesn't have simple absolute positioning: it uses `dialog units' which scale with the font in use, so this is a compromise position between simple absolute positioning and pure managed geometry.

    You don't have to redesign for every typeface and every size, but you do have to redesign for different languages.

    I'm sure Raymond would argue that this was good compromise in the context of 1980mumble when it was first dreamt up, or even in 1990mumble when NT was being built; I think I'd agree for 198x, probably not for 199x.  Times are certainly different now, though, and I think the X11 tradition of managed geometry is the right answer.

    When I looked at SWF's geometry management (a couple of years ago now) it looked pretty poor compared to Gtk, but it was certainly a move in the right direction.  I'm sure things are better now.

  15. Chris Becke says:

    hmmm. Just as an aside, you can't render the background of a radio button with commctl 6 at all. WM_CTLCOLORSTATIC messages are sent, but seem to be ignored.

  16. Anonymous Coward says:

    Chris, I'm not exactly sure what you were trying to do, let alone how, but you can draw arbitrary stuff behind radio buttons, at least in VB4, even with modern, styled controls. I know this because I had to work around a deficiency in the run-time library once, where radio buttons looked odd in tabs. I'm pretty sure that I handled WM_ERASEBKGND (= 20) to solve the problem.

  17. LR says:

    In .NET and in Delphi, you can use the "Anchors" and the "Dock" (.NET) respective "Anchors" and "Align" (Delphi) properties of the visual controls to manage simple resizing without writing any code. By using Panels and similar container controls to cover the various areas on the form, you can also design more complicated scenarios.

    It's a really powerful and easy-to-understand concept.

  18. Limited_Atonement says:

    Thanks for entertaining my question!

    >>unless you do a bunch of work yourself…

    and the Moderator:

    >>has never tried writing that little extra code

    I write in C# (.Net) (perhaps 90% of my would be readers stop here…) and try to make all my forms resizable, and after doing about ten or twenty of them, the geometry involved becomes quite intuitive, even for very complex panels (like the ones I referenced in my article).  For instance, if you want a form with two listboxes, side-by-size, overriding the OnResize event of the System.Windows.Forms.Control of your Form with the following works well:

    const int big_break = 10, small_break = 3;

    lst1.Size = lst2.Size = new Size((this.ClientSize.Width – big_break*2 – small_break)/2, this.ClientSize.Height – big_break*2);

    lst1.Location = new Point(big_break, big_break);

    lst2.Location = new Point(lst1.Right + small_break, lst1.Top);

    Like I said, I use .Net almost exclusively, and I don't know the gyrations that are necessary for doing something like this in Win32, but I suppose that people like the Moderator who are accustomed to Win32 ought to find it straightforward.

    I am familiar with the tools in Java (SpringLayout, GridBagLayout, etc.) for UI design that seem somewhat magical, but haven't found any (or created any that I'm proud of) for my own use.

    Am I way off in assuming that it is comparable in C++?  What am I missing?

    [Imagine a dialog with 50 controls. Oh, and what if the localizers want to change the dialog box layout? Now they have to modify your OnResize method and recompile? -Raymond]
  19. Anonymous Coward says:

    >Imagine a dialog with 50 controls.

    There have been many systems mentioned in this thread that have no problem dealing with this scenario.

    >Oh, and what if the localizers want to change the dialog box layout? Now they have to modify your OnResize method and recompile?

    It is common practice to use a dialogue designer to generate the necessary code, so this is not an issue. Besides, we're talking about Windows' support for this, so Microsoft could have simply added the necessary support to the dialogue resource format.

    This whole discussion is so nineties.

    [Yes, there are many systems which have no problem dealing with this scenario, so use one of those systems. Remember, Windows historically didn't provide functions for things you can already do yourself. No single resizable dialog system will work for everybody, so you are given the freedom to choose the one that works for you. -Raymond]

Comments are closed.

Skip to main content