Transparent radio button control with themes using Win32
Asked Answered
L

5

3

I am trying to make a radio button control with a transparent background using only Win32 when themes are enabled. The reason for doing this is to allow a radio button to be placed over an image and have the image show (rather than the grey default control background).

What happens out of the box is that the control will have the grey default control background and the standard method of changing this by handling either WM_CTLCOLORSTATIC or WM_CTLCOLORBTN as shown below does not work:

case WM_CTLCOLORSTATIC:
    hdcStatic = (HDC)wParam;

    SetTextColor(hdcStatic, RGB(0,0,0)); 
    SetBkMode(hdcStatic,TRANSPARENT);

    return (LRESULT)GetStockObject(NULL_BRUSH);
    break;  

My research so far indicates that Owner Draw is the only way to achieve this. I've managed to get most of the way with an Owner Draw radio button - with the code below I have a radio button and a transparent background (the background is set in WM_CTLCOLORBTN). However, the edges of the radio check are cut off using this method - I can get them back by uncommenting the call to the function DrawThemeParentBackgroundEx but this breaks the transparency.

void DrawRadioControl(HWND hwnd, HTHEME hTheme, HDC dc, bool checked, RECT rcItem)
{
    if (hTheme)
    {
      static const int cb_size = 13;

      RECT bgRect, textRect;
      HFONT font = (HFONT)SendMessageW(hwnd, WM_GETFONT, 0, 0);
      WCHAR *text = L"Experiment";

      DWORD state = ((checked) ? RBS_CHECKEDNORMAL : RBS_UNCHECKEDNORMAL) | ((bMouseOverButton) ? RBS_HOT : 0); 

      GetClientRect(hwnd, &bgRect);
      GetThemeBackgroundContentRect(hTheme, dc, BP_RADIOBUTTON, state, &bgRect, &textRect);

      DWORD dtFlags = DT_VCENTER | DT_SINGLELINE;

      if (dtFlags & DT_SINGLELINE) /* Center the checkbox / radio button to the text. */
         bgRect.top = bgRect.top + (textRect.bottom - textRect.top - cb_size) / 2;

      /* adjust for the check/radio marker */
      bgRect.bottom = bgRect.top + cb_size;
      bgRect.right = bgRect.left + cb_size;
      textRect.left = bgRect.right + 6;

      //Uncommenting this line will fix the button corners but breaks transparency
      //DrawThemeParentBackgroundEx(hwnd, dc, DTPB_USECTLCOLORSTATIC, NULL);

      DrawThemeBackground(hTheme, dc, BP_RADIOBUTTON, state, &bgRect, NULL);
      if (text)
      {
          DrawThemeText(hTheme, dc, BP_RADIOBUTTON, state, text, lstrlenW(text), dtFlags, 0, &textRect);

      }

   }
   else
   {
       // Code for rendering the radio when themes are not present
   }

}

The method above is called from WM_DRAWITEM as shown below:

case WM_DRAWITEM:
{
    LPDRAWITEMSTRUCT pDIS = (LPDRAWITEMSTRUCT)lParam;
    hTheme = OpenThemeData(hDlg, L"BUTTON");    

    HDC dc = pDIS->hDC;

    wchar_t sCaption[100];
    GetWindowText(GetDlgItem(hDlg, pDIS->CtlID), sCaption, 100);
    std::wstring staticText(sCaption);

    DrawRadioControl(pDIS->hwndItem, hTheme, dc, radio_group.IsButtonChecked(pDIS->CtlID), pDIS->rcItem, staticText);                               

    SetBkMode(dc, TRANSPARENT);
    SetTextColor(hdcStatic, RGB(0,0,0));                                
    return TRUE;

}                           

So my question is two parts I suppose:

  1. Have I missed some other way to achieve my desired result?
  2. Is it possible to fix the clipped button corners issue with my code and still have a transparent background
Lytic answered 7/9, 2011 at 12:11 Comment(3)
try moving the call to DrawThemeParentBackgroundEx to be inside the WM_CTLCOLORSTATIC handler.Pruitt
@David Heffernan didn't appear to make a difference (tried in both WM_CTLCOLORSTATIC and WM_CTRCOLORBTN but I will keep looking at that approach for a while longer. One thing I didn't make clear in my question (will now) is that the DrawRadioControl method is called from WM_DRAWITEMLytic
What version of Windows was this code tried on?Seedcase
L
4

After looking at this on and off for nearly three months I've finally found a solution that I'm pleased with. What I eventually found was that the radio button edges were for some reason not being drawn by the routine within WM_DRAWITEM but that if I invalidated the radio button control's parent in a rectangle around the control, they appeared.

Since I could not find a single good example of this I'm providing the full code (in my own solution I have encapsulated my owner drawn controls into their own class, so you will need to provide some details such as whether the button is checked or not)

This is the creation of the radiobutton (adding it to the parent window) also setting GWL_UserData and subclassing the radiobutton:

HWND hWndControl = CreateWindow( _T("BUTTON"), caption, WS_CHILD | WS_VISIBLE | BS_OWNERDRAW, 
    xPos, yPos, width, height, parentHwnd, (HMENU) id, NULL, NULL);

// Using SetWindowLong and GWL_USERDATA I pass in the this reference, allowing my 
// window proc toknow about the control state such as if it is selected
SetWindowLong( hWndControl, GWL_USERDATA, (LONG)this);

// And subclass the control - the WndProc is shown later
SetWindowSubclass(hWndControl, OwnerDrawControl::WndProc, 0, 0);

Since it is owner draw we need to handle the WM_DRAWITEM message in the parent window proc.

case WM_DRAWITEM:      
{      
    LPDRAWITEMSTRUCT pDIS = (LPDRAWITEMSTRUCT)lParam;      
    hTheme = OpenThemeData(hDlg, L"BUTTON");          

    HDC dc = pDIS->hDC;      

    wchar_t sCaption[100];      
    GetWindowText(GetDlgItem(hDlg, pDIS->CtlID), sCaption, 100);      
    std::wstring staticText(sCaption);      

    // Controller here passes to a class that holds a map of all controls 
    // which then passes on to the correct instance of my owner draw class
    // which has the drawing code I show below
    controller->DrawControl(pDIS->hwndItem, hTheme, dc, pDIS->rcItem, 
        staticText, pDIS->CtlID, pDIS->itemState, pDIS->itemAction);    

    SetBkMode(dc, TRANSPARENT);      
    SetTextColor(hdcStatic, RGB(0,0,0));     

    CloseThemeData(hTheme);                                 
    return TRUE;      

}    

Here is the DrawControl method - it has access to class level variables to allow state to be managed since with owner draw this is not handled automatically.

void OwnerDrawControl::DrawControl(HWND hwnd, HTHEME hTheme, HDC dc, bool checked, RECT rcItem, std::wstring caption, int ctrlId, UINT item_state, UINT item_action)
{   
    // Check if we need to draw themed data    
    if (hTheme)
    {   
        HWND parent = GetParent(hwnd);      

        static const int cb_size = 13;                      

        RECT bgRect, textRect;
        HFONT font = (HFONT)SendMessageW(hwnd, WM_GETFONT, 0, 0);

        DWORD state;

        // This method handles both radio buttons and checkboxes - the enums here
        // are part of my own code, not Windows enums.
        // We also have hot tracking - this is shown in the window subclass later
        if (Type() == RADIO_BUTTON) 
            state = ((checked) ? RBS_CHECKEDNORMAL : RBS_UNCHECKEDNORMAL) | ((is_hot_) ? RBS_HOT : 0);      
        else if (Type() == CHECK_BOX)
            state = ((checked) ? CBS_CHECKEDNORMAL : CBS_UNCHECKEDNORMAL) | ((is_hot_) ? RBS_HOT : 0);      

        GetClientRect(hwnd, &bgRect);

        // the theme type is either BP_RADIOBUTTON or BP_CHECKBOX where these are Windows enums
        DWORD theme_type = ThemeType(); 

        GetThemeBackgroundContentRect(hTheme, dc, theme_type, state, &bgRect, &textRect);

        DWORD dtFlags = DT_VCENTER | DT_SINGLELINE;

        if (dtFlags & DT_SINGLELINE) /* Center the checkbox / radio button to the text. */
            bgRect.top = bgRect.top + (textRect.bottom - textRect.top - cb_size) / 2;

        /* adjust for the check/radio marker */
        // The +3 and +6 are a slight fudge to allow the focus rectangle to show correctly
        bgRect.bottom = bgRect.top + cb_size;
        bgRect.left += 3;
        bgRect.right = bgRect.left + cb_size;       

        textRect.left = bgRect.right + 6;       

        DrawThemeBackground(hTheme, dc, theme_type, state, &bgRect, NULL);          
        DrawThemeText(hTheme, dc, theme_type, state, caption.c_str(), lstrlenW(caption.c_str()), dtFlags, 0, &textRect);                    

        // Draw Focus Rectangle - I still don't really like this, it draw on the parent
        // mainly to work around the way DrawFocus toggles the focus rect on and off.
        // That coupled with some of my other drawing meant this was the only way I found
        // to get a reliable focus effect.
        BOOL bODAEntire = (item_action & ODA_DRAWENTIRE);
        BOOL bIsFocused  = (item_state & ODS_FOCUS);        
        BOOL bDrawFocusRect = !(item_state & ODS_NOFOCUSRECT);

        if (bIsFocused && bDrawFocusRect)
        {
            if ((!bODAEntire))
            {               
                HDC pdc = GetDC(parent);
                RECT prc = GetMappedRectanglePos(hwnd, parent);
                DrawFocus(pdc, prc);                
            }
        }   

    }
      // This handles drawing when we don't have themes
    else
    {
          TEXTMETRIC tm;
          GetTextMetrics(dc, &tm);      

          RECT rect = { rcItem.left , 
              rcItem.top , 
              rcItem.left + tm.tmHeight - 1, 
              rcItem.top + tm.tmHeight - 1};    

          DWORD state = ((checked) ? DFCS_CHECKED : 0 ); 

          if (Type() == RADIO_BUTTON) 
              DrawFrameControl(dc, &rect, DFC_BUTTON, DFCS_BUTTONRADIO | state);
          else if (Type() == CHECK_BOX)
              DrawFrameControl(dc, &rect, DFC_BUTTON, DFCS_BUTTONCHECK | state);

          RECT textRect = rcItem;
          textRect.left = rcItem.left + 19;

          SetTextColor(dc, ::GetSysColor(COLOR_BTNTEXT));
          SetBkColor(dc, ::GetSysColor(COLOR_BTNFACE));
          DrawText(dc, caption.c_str(), -1, &textRect, DT_WORDBREAK | DT_TOP);
    }           
}

Next is the window proc that is used to subclass the radio button control - this is called with all windows messages and handles several before then passing unhandled ones on to the default proc.

LRESULT OwnerDrawControl::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam,
                               LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData)
{
    // Get the button parent window
    HWND parent = GetParent(hWnd);  

    // The page controller and the OwnerDrawControl hold some information we need to draw
    // correctly, such as if the control is already set hot.
    st_mini::IPageController * controller = GetWinLong<st_mini::IPageController *> (parent);

    // Get the control
    OwnerDrawControl *ctrl = (OwnerDrawControl*)GetWindowLong(hWnd, GWL_USERDATA);

    switch (uMsg)
    {       
        case WM_LBUTTONDOWN:
        if (controller)
        {
            int ctrlId = GetDlgCtrlID(hWnd);

            // OnCommand is where the logic for things like selecting a radiobutton
            // and deselecting the rest of the group lives.
            // We also call our Invalidate method there, which redraws the radio when
            // it is selected. The Invalidate method will be shown last.
            controller->OnCommand(parent, ctrlId, 0);       

            return (0);
        }
        break;
        case WM_LBUTTONDBLCLK:
            // We just treat doubleclicks as clicks
            PostMessage(hWnd, WM_LBUTTONDOWN, wParam, lParam);
            break;
        case WM_MOUSEMOVE:
        {
            if (controller)                 
            {
                // This is our hot tracking allowing us to paint the control
                // correctly when the mouse is over it - it sets flags that get
                // used by the above DrawControl method
                if(!ctrl->IsHot())
                {
                    ctrl->SetHot(true);
                    // We invalidate to repaint
                    ctrl->InvalidateControl();

                    // Track the mouse event - without this the mouse leave message is not sent
                    TRACKMOUSEEVENT tme;
                    tme.cbSize = sizeof(TRACKMOUSEEVENT);
                    tme.dwFlags = TME_LEAVE;
                    tme.hwndTrack = hWnd;

                    TrackMouseEvent(&tme);
                }
            }    
            return (0);
        }
        break;
    case WM_MOUSELEAVE:
    {
        if (controller)
        {
            // Turn off the hot display on the radio
            if(ctrl->IsHot())
            {
                ctrl->SetHot(false);        
                ctrl->InvalidateControl();
            }
        }

        return (0);
    }
    case WM_SETFOCUS:
    {
        ctrl->InvalidateControl();
    }
    case WM_KILLFOCUS:
    {
        RECT rcItem;
        GetClientRect(hWnd, &rcItem);
        HDC dc = GetDC(parent);
        RECT prc = GetMappedRectanglePos(hWnd, parent);
        DrawFocus(dc, prc);

        return (0);
    }
    case WM_ERASEBKGND:
        return 1;
    }
    // Any messages we don't process must be passed onto the original window function
    return DefSubclassProc(hWnd, uMsg, wParam, lParam); 

}

Finally the last little piece of the puzzle is that you need to invalidate the control (redraw it) at the right times. I eventually found that invalidating the parent allowed the drawing to work 100% correctly. This was causing flicker until I realised that I could get away by only invalidating a rectangle as big as the radio check, rather than as big as the whole control including text as I had been.

void InvalidateControl()
{
    // GetMappedRectanglePos is my own helper that uses MapWindowPoints 
    // to take a child control and map it to its parent
    RECT rc = GetMappedRectanglePos(ctrl_, parent_);

    // This was my first go, that caused flicker
    // InvalidateRect(parent_, &rc_, FALSE);    

    // Now I invalidate a smaller rectangle
    rc.right = rc.left + 13;
    InvalidateRect(parent_, &rc, FALSE);                
}

A lot of code and effort for something that should be simple - drawing a themed radio button over a background image. Hopefully the answer will save someone else some pain!

* One big caveat with this is it only works 100% correctly for owner controls that are over a background (such as a fill rectangle or an image). That is ok though, since it is only needed when drawing the radio control over a background.

Lytic answered 24/11, 2011 at 16:47 Comment(0)
T
1

I've done this some time ago as well. I remember the key was to just create the (radio) buttons as usual. The parent must be the dialog or window, not a tab control. You could do it differently but I created a memory dc (m_mdc) for the dialog and painted the background on that. Then add the OnCtlColorStatic and OnCtlColorBtn for your dialog:

virtual HBRUSH OnCtlColorStatic(HDC hDC, HWND hWnd)
{
    RECT rc;
    GetRelativeClientRect(hWnd, m_hWnd, &rc);
    BitBlt(hDC, 0, 0, rc.right - rc.left, rc.bottom - rc.top, m_mdc, rc.left, rc.top, SRCCOPY);
    SetBkColor(hDC, GetSysColor(COLOR_BTNFACE));
    if (IsAppThemed())
        SetBkMode(hDC, TRANSPARENT);
    return (HBRUSH)GetStockObject(NULL_BRUSH);
}

virtual HBRUSH OnCtlColorBtn(HDC hDC, HWND hWnd)
{
    return OnCtlColorStatic(hDC, hWnd);
}

The code uses some in-house classes and functions similar to MFC, but I think you should get the idea. As you can see it draws the background of these controls from the memory dc, that's key.

Give this a try and see if it works!

EDIT: If you add a tab control to the dialog and put the controls on the tab (that was the case in my app) you must capture it's background and copy it to the memory dc of the dialog. It's a bit of an ugly hack but it works, even if the machine is running some extravagant theme that uses a gradient tab background:

    // calculate tab dispay area

    RECT rc;
    GetClientRect(m_tabControl, &rc);
    m_tabControl.AdjustRect(false, &rc);
    RECT rc2;
    GetRelativeClientRect(m_tabControl, m_hWnd, &rc2);
    rc.left += rc2.left;
    rc.right += rc2.left;
    rc.top += rc2.top;
    rc.bottom += rc2.top;

    // copy that area to background

    HRGN hRgn = CreateRectRgnIndirect(&rc);
    GetRelativeClientRect(m_hWnd, m_tabControl, &rc);
    SetWindowOrgEx(m_mdc, rc.left, rc.top, NULL);
    SelectClipRgn(m_mdc, hRgn);
    SendMessage(m_tabControl, WM_PRINTCLIENT, (WPARAM)(HDC)m_mdc, PRF_CLIENT);
    SelectClipRgn(m_mdc, NULL);
    SetWindowOrgEx(m_mdc, 0, 0, NULL);
    DeleteObject(hRgn);

Another interesting point, while we're busy now, to get it all non-flickering create the parent and children (buttons, statics, tabs etc) with the WS_CLIPCHILDREN and WS_CLIPSIBLINGS style. The the order of creation is essential: First create the controls you put on the tabs, then create the tab control. Not the other way around (although it feels more intuitive). That because the tab control should clip the area obscured by the controls on it :)

Transience answered 7/9, 2011 at 12:49 Comment(1)
This almost worked for me - in fact I thought it was close enough to live with but I never got the mapping of the rectangles 100% (there was always some weird ghosting - almost imperceptible but there, and the radio check was sometimes clipped) what made me abandon it entirely was when running under aero it didn't really work stably at all.Lytic
P
1

I can't immediately try this out, but so far as I recall, you don't need owner draw. You need to do this:

  1. Return 1 from WM_ERASEBKGND.
  2. Call DrawThemeParentBackground from WM_CTLCOLORSTATIC to draw the background there.
  3. Return GetStockObject(NULL_BRUSH) from WM_CTLCOLORSTATIC.
Pruitt answered 7/9, 2011 at 13:20 Comment(2)
Thanks for that - I've tried it and can't get it to work but I've only been using WinApi for two weeks so it could very well be me. If you do have time to quickly prototype it later, that would be much appreciated. I've going to keep trying the approach you suggest and will reply again if I find what I'm doing wrong. (would it make a difference that I have a background static bitmap which is why I want the transparency on the radio button?)Lytic
@David That could make a difference I guess.Pruitt
G
1
  1. Knowing the sizes and coordinates radio button, we will copy the image to them closed.
  2. Then we create a brush by means of BS_PATTERN style CreateBrushIndirect
  3. Farther according to the usual scheme - we return handle to this brush in reply to COLOR - the message (WM_CTLCOLORSTATIC).
Gorey answered 14/10, 2013 at 11:37 Comment(0)
M
0

I have no idea why you are doing it so difficult, this is best solved via CustomDrawing This is my MFC Handler to draw a Notebook on a CTabCtrl control. I'm not really sure why i need to Inflate the Rectangle, because if i don't do it a black border is drawn.

And another conceptional bug MS made is IMHO that i have to overwrite the PreErase drawing phase instead of the PostErase. But if i do the later the checkbox is gone.

afx_msg void AguiRadioButton::OnCustomDraw(NMHDR* notify, LRESULT* res) {
    NMCUSTOMDRAW* cd  = (NMCUSTOMDRAW*)notify;            
    if (cd->dwDrawStage == CDDS_PREERASE) {
        HTHEME theme = OpenThemeData(m_hWnd, L"Button");
        CRect r = cd->rc; r.InflateRect(1,1,1,1);
        DrawThemeBackground(theme, cd->hdc, TABP_BODY, 0, &r,NULL);
        CloseThemeData(theme);
        *res = 0;
    }
    *res = 0;    
} 
Maunder answered 26/1, 2012 at 6:23 Comment(7)
Oh yes i should notice that our app only supports Windows7 themed applications. Thats why there is not error checking around OpenThemeData - that is done once in the main function.Maunder
I've just tried the above and it doesn't appear to work. This could be me however, since I converted the code to win32 and am not using syncfusion (I guess this is where the TABP_BODY part_id comes from?) so just use BP_RADIOBUTTON. Also in the documentation for NM_CUSTOMDRAW radiobutton is not in the list of controls that custom draw works for. I would love if you have a simple solution that works for native win32 radiobutton controls, since as you say my current solution is difficult, but given what you've currently provided I can't see how it works.Lytic
What is syncfusion? I just use plain MFC with "#define _WIN32_WINNT 0x0601" and #include <uxtheme.h>. Thats where you get TABP_BODY. Works for XP, Vista, Win7.Maunder
And Custom Draw on Buttons is supported and documented. Yes it is missing in the list of the overview page of CustomDraw. But you you really need to learn to read more on MSDN and always check twice. msdn.microsoft.com/en-us/library/windows/desktop/…Maunder
I'm not using MFC so am having to convert your answer into plain win32 (so the problem could well be my conversion). This still doesn't work for me - now instead of the required background bitmap I just get a white rectangle and radio button does not respond to clicks. Is there more code you aren't showing where you manage the button state?Lytic
I just tried this in MFC and have the same problem where the button with the given customdraw code shows a white background when over a bitmap (in my trial simply a picture control)Lytic
Yes this is because the white background is the theme from the TabCtrl for which this is coded. Of course you have to add our Background Drawing code in this example. It is not transparent, it is just the right place where you can draw your image.Maunder

© 2022 - 2024 — McMap. All rights reserved.