【问题标题】:Transparent radio button control with themes using Win32使用 Win32 的带有主题的透明单选按钮控件
【发布时间】:2011-11-12 02:50:01
【问题描述】:

我正在尝试在启用主题时仅使用 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;  

到目前为止,我的研究表明 Owner Draw 是实现这一目标的唯一方法。我已经设法通过 Owner Draw 单选按钮获得了大部分方法 - 使用下面的代码,我有一个单选按钮和一个透明背景(背景设置在 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. 是否可以用我的代码修复剪裁的按钮角问题并且仍然具有透明背景

【问题讨论】:

  • 尝试将调用移动到DrawThemeParentBackgroundExWM_CTLCOLORSTATIC 处理程序中。
  • @David Heffernan 似乎没有什么不同(在 WM_CTLCOLORSTATIC 和 WM_CTRCOLORBTN 中都尝试过,但我会继续研究这种方法一段时间。我在问题中没有说清楚的一件事(现在会)是从WM_DRAWITEM调用DrawRadioControl方法
  • 这段代码是在哪个版本的 Windows 上试用的?

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


【解决方案1】:

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

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

这是单选按钮的创建(将其添加到父窗口),同时设置 GWL_UserData 和子类化单选按钮:

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

由于是所有者绘制,我们需要在父窗口过程中处理 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% 正确。不过没关系,因为只有在背景上绘制无线电控件时才需要它。

【讨论】:

    【解决方案2】:

    我前段时间也做过。我记得关键是像往常一样创建(单选)按钮。父级必须是对话框或窗口,而不是选项卡控件。你可以做不同的事情,但我为对话框创建了一个内存 dc (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 的内部类和函数,但我认为您应该明白这一点。如您所见,它从内存 dc 中绘制这些控件的背景,这是关键。

    试试这个,看看它是否有效!

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

        // 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 样式创建父级和子级(按钮、静态、选项卡等)。创建的顺序很重要:首先创建你放在选项卡上的控件,然后创建选项卡控件。不是相反(尽管感觉更直观)。那是因为选项卡控件应该剪切被其上的控件遮挡的区域:)

    【讨论】:

    • 这几乎对我有用——事实上我认为它已经足够接近了,但我从来没有得到 100% 的矩形映射(总是有一些奇怪的重影——几乎无法察觉,但在那里,并且无线电检查有时会被剪掉)让我完全放弃它的原因是在航空条件下运行时它根本无法稳定工作。
    【解决方案3】:

    我不能立即尝试这个,但据我记得,你不需要所有者抽签。你需要这样做:

    1. WM_ERASEBKGND返回1。
    2. WM_CTLCOLORSTATIC 调用DrawThemeParentBackground 以在此处绘制背景。
    3. WM_CTLCOLORSTATIC返回GetStockObject(NULL_BRUSH)

    【讨论】:

    • 谢谢 - 我已经尝试过了,但无法让它工作,但我只使用了两周的 WinApi,所以很可能是我。如果您以后有时间快速对其进行原型制作,那将不胜感激。我将继续尝试您建议的方法,如果我发现我做错了什么,我会再次回复。 (我有一个背景静态位图会有所不同,这就是我想要单选按钮透明度的原因吗?)
    • @David 我猜这可能会有所作为。
    【解决方案4】:
    1. 知道尺寸和坐标单选按钮,我们将复制 图像给他们关闭。
    2. 然后我们通过 BS_PATTERN 样式 CreateBrushIndirect
    3. 进一步根据 通常的方案 - 我们返回这个画笔的手柄以回复颜色 - 消息 (WM_CTLCOLORSTATIC)。

    【讨论】:

      【解决方案5】:

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

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

      【讨论】:

      • 哦,是的,我应该注意到我们的应用程序仅支持 Windows7 主题应用程序。这就是为什么没有围绕 OpenThemeData 进行错误检查的原因——这在主函数中完成一次。
      • 我刚刚尝试了上述方法,但似乎不起作用。然而,这可能是我,因为我将代码转换为 win32 并且没有使用同步融合(我猜这是 TABP_BODY part_id 的来源?)所以只需使用 BP_RADIOBUTTON。同样在 NM_CUSTOMDRAW 的文档中,单选按钮不在自定义绘制适用的控件列表中。如果您有一个适用于本机 win32 单选按钮控件的简单解决方案,我会很高兴,因为正如您所说,我当前的解决方案很困难,但鉴于您目前提供的内容,我看不出它是如何工作的。
      • 什么是同步融合?我只是使用带有“#define _WIN32_WINNT 0x0601”和#include 的普通MFC。这就是你得到 TABP_BODY 的地方。适用于 XP、Vista、Win7。
      • 支持自定义绘制按钮并记录在案。是的,它在 CustomDraw 的概述页面列表中丢失。但是您确实需要学习在 MSDN 上阅读更多内容并始终检查两次。 msdn.microsoft.com/en-us/library/windows/desktop/…
      • 我没有使用 MFC,所以必须将您的答案转换为普通的 win32(所以问题很可能是我的转换)。这对我仍然不起作用 - 现在我只得到一个白色矩形而不是所需的背景位图,并且单选按钮不响应点击。是否还有更多代码没有显示您管理按钮状态的位置?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-11-19
      • 2020-12-14
      • 1970-01-01
      • 2011-05-24
      • 1970-01-01
      • 2021-09-18
      • 2011-09-15
      相关资源
      最近更新 更多