3

我正在尝试在启用主题时仅使用 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. 是否可以使用我的代码修复剪裁的按钮角问题并且仍然具有透明背景
4

5 回答 5

3

在断断续续地观察了近三个月后,我终于找到了一个令我满意的解决方案。我最终发现单选按钮边缘由于某种原因没有被 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);

由于它是所有者绘制,我们需要在父窗口 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);
    }           
}

接下来是用于对单选按钮控件进行子类化的窗口 proc - 它使用所有窗口消息调用并处理几个,然后将未处理的消息传递给默认 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% 正确。不过没关系,因为只有在背景上绘制无线电控制时才需要它。

于 2011-11-24T16:47:00.437 回答
1

我前段时间也这样做过。我记得关键是像往常一样创建(单选)按钮。父级必须是对话框或窗口,而不是选项卡控件。你可以用不同的方式做,但我为对话框创建了一个内存 dc (m_mdc) 并在其上绘制了背景。然后为您的对话框添加OnCtlColorStatic和:OnCtlColorBtn

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

于 2011-09-07T12:49:37.150 回答
1

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

  1. 从 中返回 1 WM_ERASEBKGND
  2. DrawThemeParentBackground从那里调用WM_CTLCOLORSTATIC以绘制背景。
  3. 从. GetStockObject(NULL_BRUSH)_WM_CTLCOLORSTATIC
于 2011-09-07T13:20:28.360 回答
1
  1. 知道尺寸和坐标单选按钮后,我们将图像复制到它们关闭。
  2. 然后我们通过 BS_PATTERN 样式 CreateBrushIndirect 创建一个画笔
  3. 再按照通常的方案——我们返回这个画笔的句柄以回复 COLOR——消息(WM_CTLCOLORSTATIC)。
于 2013-10-14T11:37:25.950 回答
0

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

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;    
} 
于 2012-01-26T06:23:00.720 回答