使用Win32主题的透明单选按钮控件

时间:2011-09-07 12:11:47

标签: c++ winapi radio-button visual-styles

我想在启用主题时仅使用Win32制作具有透明背景的单选按钮控件。这样做的原因是允许将单选按钮放在图像上并显示图像(而不是灰色默认控件背景)。

开箱即用的是控件将具有灰色默认控件背景,并且通过处理WM_CTLCOLORSTATICWM_CTLCOLORBTN来改变此操作的标准方法如下所示不起作用:

case WM_CTLCOLORSTATIC:
    hdcStatic = (HDC)wParam;

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

    return (LRESULT)GetStockObject(NULL_BRUSH);
    break;  

到目前为止,我的研究表明,所有者抽奖是实现这一目标的唯一途径。我已经设法通过所有者绘制单选按钮获得大部分 - 使用下面的代码我有一个单选按钮和一个透明背景(背景设置在WM_CTLCOLORBTN)。但是,使用这种方法切断了无线电检查的边缘 - 我可以通过取消对函数DrawThemeParentBackgroundEx的调用来取回它们,但这会破坏透明度。

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
   }

}

上面的方法是从WM_DRAWITEM调用的,如下所示:

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;

}                           

所以我的问题是我想的两个部分:

  1. 我是否错过了其他方法来达到我想要的结果?
  2. 是否可以使用我的代码修复剪裁的按钮角问题,并且仍然具有透明背景

5 个答案:

答案 0 :(得分:3)

看了这个开关近三个月后,我终于找到了一个我很满意的解决方案。我最终发现单选按钮边缘由于某种原因没有被WM_DRAWITEM中的例程绘制,但如果我在控件周围的矩形中使单选按钮控件的父项无效,它们就会出现。

由于我找不到一个好的例子,我提供完整的代码(在我自己的解决方案中,我已将我的所有者绘制的控件封装到他们自己的类中,因此您需要提供一些细节,例如是否是否选中了按钮)

这是radiobutton的创建(将其添加到父窗口),同时设置GWL_UserData并为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);

由于它是所有者绘制,我们需要在父窗口proc中处理WM_DRAWITEM消息。

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;      

}    

这是DrawControl方法 - 它可以访问类级别变量以允许管理状态,因为所有者绘制不会自动处理。

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);
    }           
}

接下来是用于子类化单选按钮控件的窗口过程 - 这个 调用所有Windows消息并处理几个然后传递未处理 那些是默认的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); 

}

最后,最后一小部分难题是你需要在正确的时间使控件无效(重绘它)。我最终发现,使父项无效可以使绘图正常工作100%。这导致了闪烁,直到我意识到我可以通过无效检查一个像无线电检查一样大的矩形来逃避,而不是像我一样大的整个控件包括文本。

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);                
}

许多代码和努力应该很简单 - 在背景图像上绘制主题单选按钮。希望答案能为别人带来一些痛苦!

* 这有一个很大的警告:对于背景上的所有者控件(例如填充矩形或图像),它只能100%正确地工作。没关系,因为只有在背景上绘制无线电控制时才需要它。

答案 1 :(得分:1)

我之前也做过这个。我记得关键是像往常一样创建(收音机)按钮。父级必须是对话框或窗口,而不是选项卡控件。您可以采用不同的方式,但我为对话框创建了一个内存直流(m_mdc)并在其上绘制了背景。然后为您的对话框添加OnCtlColorStaticOnCtlColorBtn

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);
}

代码使用了一些类似于MFC的内部类和函数,但我认为你应该明白这个想法。正如您所看到的,它从内存直流中绘制了这些控件的背景,这是关键。

尝试一下,看看它是否有效!

编辑:如果您向对话框添加选项卡控件并将控件放在选项卡上(我的应用程序就是这种情况),您必须捕获它的背景并将其复制到对话框的内存直流。这是一个丑陋的黑客,但它的工作原理,即使机器运行一些使用渐变标签背景的奢侈主题:

    // 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);

另一个有趣的观点,虽然我们现在很忙,为了让它全部无闪烁,使用WS_CLIPCHILDREN和WS_CLIPSIBLINGS样式创建父和子(按钮,静态,制表符等)。创建顺序至关重要:首先创建放在选项卡上的控件,然后创建选项卡控件。不是相反(尽管感觉更直观)。这是因为标签控件应该剪切被控件遮挡的区域:)

答案 2 :(得分:1)

我无法立即尝试这一点,但据我记忆,你不需要老板抽奖。你需要这样做:

  1. WM_ERASEBKGND返回1。
  2. DrawThemeParentBackground致电WM_CTLCOLORSTATIC以在那里绘制背景。
  3. GetStockObject(NULL_BRUSH)返回WM_CTLCOLORSTATIC

答案 3 :(得分:1)

  1. 知道尺寸和坐标单选按钮,我们将复制 图像给他们关闭。
  2. 然后我们通过创建一个画笔 BS_PATTERN样式CreateBrushIndirect
  3. 根据说再远 通常的方案 - 我们将这个画笔的句柄返回给COLOR - 消息(WM_CTLCOLORSTATIC)。

答案 4 :(得分:0)

我不知道你为什么这么难,这最好通过CustomDrawing来解决 这是我的MFC处理程序,用于在CTabCtrl控件上绘制Notebook。我不太确定为什么我需要给矩形充气,因为如果我不这样做,就会画出黑色边框。

MS制作的另一个概念错误是恕我直言,我必须覆盖PreErase绘图阶段而不是PostErase。但是,如果我做了以后复选框就不见了。

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;    
}