我试图在启用主题时仅使用Win32使具有透明背景的单选按钮控件。这样做的原因是允许将单选按钮放置在图像上并显示图像(而不是默认的灰色控件背景)。
开箱即用的情况是,控件将具有灰色默认控件背景,并且通过处理WM_CTLCOLORSTATIC
或WM_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_drawwitem调用的,如下所示:
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;
}
我想我的问题有两个部分:
- 我是否错过了实现预期结果的其他方法?
- 是否有可能修复与我的代码剪辑按钮角的问题,仍然有一个透明的背景
经过近三个月的反复研究,我终于找到了一个令我满意的解决方案。我最终发现的是,由于某种原因,单选按钮边缘没有被wm_drawwitem内的例程绘制,但如果我在控件周围的矩形中使单选按钮控件的父控件无效,它们就会出现。
因为我找不到一个很好的例子,所以我提供了完整的代码(在我自己的解决方案中,我已经将我的所有者绘制的控件封装到他们自己的类中,所以你需要提供一些细节,例如按钮是否被选中)
这是单选按钮的创建(将其添加到父窗口),也设置了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_drawwitem消息。
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方法-它可以访问类级变量来允许状态被管理,因为在owner draw中这不是自动处理的。
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);
}
}
下一个是窗口进程,它被用来子类化单选按钮控件- this在传递未处理的消息之前调用所有Windows消息并处理几个
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%正确的所有者控件的背景(如填充矩形或图像)。这是可以的,因为它只需要在背景上绘制无线电控件。
我以前也这样做过。我记得关键是像往常一样创建(单选)按钮。父控件必须是对话框或窗口,而不是选项卡控件。你可以做不同的,但我为对话框创建了一个内存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。这是一个有点丑陋的hack,但它工作,即使机器运行一些奢侈的主题,使用渐变选项卡背景:
// 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样式创建父元素和子元素(按钮、静态元素、选项卡等)。创建的顺序很重要:首先创建放在选项卡上的控件,然后创建选项卡控件。而不是反过来(尽管它感觉更直观)。这是因为选项卡控件应该剪切被其上的控件遮挡的区域:)
我不能立即尝试,但据我回忆,您不需要所有者绘制。您需要这样做:
- 从
WM_ERASEBKGND
返回1。 - 从
WM_CTLCOLORSTATIC
调用DrawThemeParentBackground
在那里绘制背景。 - 从
WM_CTLCOLORSTATIC
返回GetStockObject(NULL_BRUSH)
- 知道了单选按钮的大小和坐标,我们将复制图像对他们关闭了。
- 然后我们创建一个画笔的手段BS_PATTERN style CreateBrushIndirect
- 进一步按通常的方案-我们返回这个刷子的手柄作为对COLOR -的回应消息(WM_CTLCOLORSTATIC).
我不知道你为什么这么难,这是通过CustomDrawing最好的解决方案这是我在CTabCtrl控件上绘制笔记本的MFC处理程序。我不太确定为什么我需要膨胀矩形,因为如果我不这样做,就会画一个黑色的边框。
另一个概念上的错误是我不得不覆盖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;
}