CUSTOMDRAW in Win32 Header not appropriately drawing outside columns area - c++

I am currently subclassing a ListView control to support custom color schemes - especially Dark Mode themes.
So first, I changed the ListView and associated Header with:
SetWindowTheme(hwndListView, isDarkModeEnabled() ? L"Explorer" : nullptr, nullptr);
SetWindowTheme(hwndListViewHeader, isDarkModeEnabled() ? L"ItemsView" : nullptr, nullptr);
I can set the ListView background color to any custom color I choose, while answering to the WM_THEMECHANGED message and doing a ListView_SetBkColor() macro.
Unfortunately, it seems I can't do it with the associated Header Control, I must resort to Custom Draw. So I did. I subclassed the ListView, because this is the window handle that will receive WM_NOTIFY messages from the Header control, and when responding to NM_CUSTOMDRAW message, my code looks like this:
case WM_NOTIFY:
{
LPNMHDR nmhdr = reinterpret_cast<LPNMHDR>(lParam);
if (nmhdr->code == NM_CUSTOMDRAW)
{
LPNMCUSTOMDRAW lpcd = reinterpret_cast<LPNMCUSTOMDRAW>(lParam);
switch (lpcd->dwDrawStage)
{
case CDDS_PREPAINT:
{
if (!PluginDarkMode::isEnabled())
return CDRF_DODEFAULT;
FillRect(lpcd->hdc, &lpcd->rc, getDarkerBackgroundBrush());
return CDRF_NOTIFYITEMDRAW | CDRF_NOTIFYPOSTPAINT;
}
case CDDS_ITEMPREPAINT:
{
return DrawListViewHeaderItem(lParam); // This function draws the item entirely and then returns CDRF_SKIPDEFAULT.
}
case CDDS_POSTPAINT:
// Calculates the undrawn border outside columns
HWND hHeader = lpcd->hdr.hwndFrom;
int count = static_cast<int>(Header_GetItemCount(hHeader));
int colsWidth = 0;
RECT wRc = {};
for (int i = 0; i < count; i++)
{
Header_GetItemRect(hHeader, i, &wRc);
colsWidth += wRc.right - wRc.left;
}
RECT clientRect;
GetClientRect(hHeader, &clientRect);
if (clientRect.right > (colsWidth + 1))
{
clientRect.left = colsWidth + 1;
HDC hdc = GetDC(hHeader);
FillRect(hdc, &clientRect, getDarkerBackgroundBrush());
ReleaseDC(hHeader, hdc);
}
return CDRF_SKIPDEFAULT;
}
}
break;
}
Now, my problem is that, although this seems fine, my control will always end up with a black rectangle outside the column's area (the empty space where no column exists). And this effect persists up until I move the mouse outside the header control area.
For example:
And when the mouse leaves the area:
Any ideas of how to solve this?

Related

If I have five Win32 static controls, how can I set one of them with a specific foreground color? [duplicate]

This question already has answers here:
Set static text color Win32
(1 answer)
In Win32, how can the colour of STATIC text be changed?
(2 answers)
Closed 4 years ago.
As far as I can tell, this is not a duplicate question because it's dealing with a collection of static (label) controls. I want to set a foreground color to a specific one that I call in my thin OOP library.
I call a static control a "Label" in my library. This is how I set the color:
void Label::setForeColor(const BYTE red, const BYTE green, const BYTE blue)
{
m_foreColor = RGB(red, green, blue);
}
This just sets a COLORRREF that the control should have. I'm having trouble finding a solution to send a message for that specific static control without affecting others.
Many say to use WM_CTLCOLORSTATIC, but I already am for transparency of controls:
case WM_CTLCOLORBTN:
case WM_CTLCOLORSTATIC:
{
char class_Name[100];
WNDCLASS lpcls{};
SetBkMode((HDC)wParam, TRANSPARENT);
// SetTextColor((HDC)wParam, RGB(0, 0, 255)); This works and can set all statics as blue, but I need just one control blue.
GetClassName(hWnd, class_Name, 100);
GetClassInfo(frm.getInstance(), class_Name, &lpcls);
return (LRESULT)lpcls.hbrBackground;
}
But here's the issue: I may have more than one label on a window, so this goes beyond than just setting a single label as most examples show. There may be 5 labels with 5 different colors.
This is the top layer:
Label lblName("This is a label.", 330, 303);
lblName.setVisible(true);
lblName.setForeColor(0, 0, 255);
lblName.setFont("Garamond", 24, false, false, false);
lblName.OnMouseOver(lblName_onMouseOver);
Ideally, I would like to set the color in my setFont() function by sending a message.
bool Label::setFont(const std::string &fontName, const int size, const bool bold,
const bool italic, const bool underlined)
{
DWORD dwItalic;
DWORD dwBold;
DWORD dwUnderlined;
SIZE linkSize;
HFONT old_font;
dwItalic = (italic) ? TRUE : FALSE;
dwBold = (bold) ? FW_BOLD : FW_DONTCARE;
dwUnderlined = (underlined) ? TRUE : FALSE;
m_font = CreateFont(size, 0, 0, 0, dwBold, dwItalic, dwUnderlined, FALSE,
ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY,
DEFAULT_PITCH | FF_SWISS, fontName.c_str());
SendMessage(m_handle, WM_SETFONT, WPARAM(m_font), TRUE);
// Calculate the correct width and height size
HDC hDC = GetDC(m_handle);
old_font = SelectFont(hDC, m_font);
GetTextExtentPoint32(hDC, m_text.c_str(), (int)m_text.length(), &linkSize);
setSize(linkSize.cx, size);
DeleteFont(old_font);
ReleaseDC(m_handle, hDC);
return true;
}
Finally, this is how I retrieve my labels that I'm interested in. I wonder if I need to set the font color similarly.
case WM_MOUSEMOVE:
{
X3D::Windows::Control *ctrl = (X3D::Windows::Control*) dwRefData;
// Check if this is a X3D Label control.
X3D::Windows::Label *lbl = dynamic_cast<X3D::Windows::Label*>(ctrl);
if (lbl)
{
lbl->setHovering(true);
lbl->invokeOnMouseHover();
}
else
{
lbl->setHovering(false);
}
break;
}
Overall question: If I have five Win32 static controls, how can I set one of them with a specific foreground color?
Update:
This is my current code. Assert() is barking at me: Expression: map/set iterator not dereferencable
case WM_CTLCOLORBTN:
case WM_CTLCOLORSTATIC:
{
char class_Name[100];
WNDCLASS lpcls{};
SetBkMode((HDC)wParam, TRANSPARENT);
GetClassName(hWnd, class_Name, 100);
GetClassInfo(frm.getInstance(), class_Name, &lpcls);
for (int i = 0; i < frm.getControlCount(); i++)
{
if (frm.getControls().find(i)->second->getHandle() == (HWND)lParam)
{
// Obtain the control associated with the id.
X3D::Windows::Control *ctrl = frm.getControls().find(i)->second;
if (ctrl == NULL)
return 0;
// Check if this is a X3D Label control.
Label *lbl = dynamic_cast<X3D::Windows::Label*>(ctrl);
if (lbl != NULL)
{
SetTextColor((HDC)wParam, lbl->getForeColor());
break;
}
}
}
return (LRESULT)lpcls.hbrBackground;
}
Update:
App runs, but the font color isn't updating. Something wrong in this?
int id = GetDlgCtrlID((HWND)lParam);
// Obtain the control associated with the id.
X3D::Windows::Control *ctrl = frm.getControls().at(id);
if (ctrl == NULL)
return 0;
// Check if this is a X3D Label control.
Label *lbl = dynamic_cast<X3D::Windows::Label*>(ctrl);
if (lbl != NULL)
{
SetTextColor((HDC)wParam, lbl->getForeColor());
break;
}
Tried this too:
int id = GetDlgCtrlID((HWND)lParam);
// Obtain the control associated with the id.
X3D::Windows::Control *ctrl = frm.getControls().find(id)->second;
if (ctrl == NULL)
return 0;
If all of that looks correct, I'll just have to debug it tomorrow.
WM_CTLCOLORSTATIC is sent multiple times, you can choose to take different actions depending on which child window is generating it.
As shown on MSDN, the lParam that arrives with the message is the HWND for the control. Compare it directly, or GetDlgCtrlID() if you want to work with dialog item IDs.
No need to do anything special in the subclass wndproc, because WM_CTLCOLORSTATIC is sent to the parent window.
But there is no message to send to change the color or font, because the STATIC window class doesn't use a permanent device context per label (which is a good thing, because many programs have a lot of labels). So the text configuration like color and font need to be reapplied to the DC each time it is borrowed from the pool. WM_CTLCOLORSTATIC is sent to the parent window at the ideal time to do this.
If your setter for the color is called, be sure to use InvalidateRect() to trigger a repaint. That repaint will send WM_CTLCOLORSTATIC again, giving you the opportunity to act on your updated color.

How to block resizing after a double-click on a dialog window's top or bottom edges in Windows 10?

I'm coding a customized popup window with C++ using Win32. The condition for this popup window is that it can be only resized from the bottom down. The following is the implementation of such restriction:
RECT rcInitialWindowRectangle = {0};
//The dialog has WS_THICKFRAME style
LRESULT CALLBACK DlgWndProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
case WM_INITDIALOG:
{
//Set minimum window size
::GetWindowRect(hDlg, &rcInitialWindowRectangle);
}
break;
case WM_SIZING:
{
//Restrict sizing on all sides but bottom
if(wParam != WMSZ_BOTTOM)
{
RECT* pRcWnd = (RECT*)lParam;
//Preserve all sides but bottom
int b = pRcWnd->bottom;
*pRcWnd = rcInitialWindowRectangle;
pRcWnd->bottom = b;
return TRUE;
}
}
break;
case WM_GETMINMAXINFO:
{
//The following is needed to restrict minimum window size
int w = rcInitialWindowRectangle.right - rcInitialWindowRectangle.left;
if(w != 0)
{
MINMAXINFO* pMMI = (MINMAXINFO*)lParam;
pMMI->ptMinTrackSize.x = w;
pMMI->ptMinTrackSize.y = rcInitialWindowRectangle.bottom - rcInitialWindowRectangle.top;
pMMI->ptMaxTrackSize.x = w;
}
}
break;
case WM_NCHITTEST:
{
//The following is needed to display correct cursor for resizing
POINT pnt = {GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)};
RECT rcWnd;
::GetWindowRect(hDlg, &rcWnd);
//L, T, R, B
RECT rcBtm = {rcInitialWindowRectangle.left,
rcWnd.bottom - 16, //Some arbitrary border
rcInitialWindowRectangle.right,
rcWnd.bottom};
return ::PtInRect(&rcBtm, pnt) ? HTNOWHERE : HTBORDER;
}
break;
return 0;
}
So this works except one thing. On Windows 10, there's evidently a new feature -- when someone double-clicks on the bottom (or top) edge of a window -- here's an example with Notepad so that you can try:
that window is resized (stretched) to the top and bottom of the screen (akin to maximization, but only vertically.)
So my question is how do I block this double-click resizing? (In my case the top of the popup window should not move.)
PS. My first instinct was to block all double-clicks on the window's edge, but then I thought that maybe there's a less barbaric way to achieve this?
You are already handling WM_NCHITTEST. Handle WM_NCLBUTTONDBLCLK and don't forward to DefWindowProc unless the hit test (in wParam) indicates the lower frame.
here's the workaround that seems to work for me. It is not really about blocking the double-clicks. (Apart from subclassing the WndProc of a dialog box, which I haven't tried, I failed to block that double-click effect in DlgProc alone.) My workaround presented below is to achor the top of the popup window and let it drop down to the bottom of the screen, if the user who double-clicks the bottom border wants similar stuff to happen.
Since this does not answer my original question, I won't mark it as such.
//Add this case statement to my original code
case WM_WINDOWPOSCHANGING:
{
WINDOWPOS* pWP = (WINDOWPOS*)lParam;
if(!(pWP->flags & (SWP_NOMOVE | SWP_NOSIZE)))
{
int w = rcInitialWindowRectangle.right - rcInitialWindowRectangle.left;
if(w > 0)
{
//Anchor the top of the popup window
pWP->x = rcInitialWindowRectangle.left;
pWP->y = rcInitialWindowRectangle.top;
pWP->cx = w;
int h = pWP->cy;
//Make sure that the height fits the screen
POINT pnt = {pWP->x, pWP->y};
MONITORINFO mi = {0};
mi.cbSize = sizeof(mi);
if(::GetMonitorInfo(::MonitorFromPoint(pnt, MONITOR_DEFAULTTONEAREST), &mi))
{
if(pWP->y + h > mi.rcWork.bottom)
{
int nMinDefaultH = rcInitialWindowRectangle.bottom -
rcInitialWindowRectangle.top;
int nAh = mi.rcWork.bottom - y;
if(nAh >= nMinDefaultH)
h = nAh;
else
h = nMinDefaultH;
}
}
pWP->cy = h;
}
}
}
break;

CMFCListCtrl force selected item to have red color

I have my own CMFCListCtrl derived class where I implemented the
virtual COLORREF OnGetCellTextColor(int nRow, int nColum)
{
CMyClass* pMyClass = (CMyClass*)GetItemData(nRow);
if (pMyClass && pMyClass->m_bDeleted)
return RGB(255, 0, 0);
return __super::OnGetCellTextColor(nRow, nColum);
}
function for marking red the deleted entries. This works except when a item is selected.
I went to the void CMFCListCtrl::OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult) function and placed a breakpoint with the condition iRow==selected item on the line
lplvcd->clrText = OnGetCellTextColor(iRow, iColumn);
executed it, then I created a new Data Breakpoint for &lplvcd->clrText.
The Data Breakpoint got hit on function
comctl32.dll!SHThemeComputeTextColors()
, as the callstack image shows:
, which is clearly overriding the variable value.
As I search for SHThemeComputeTextColors on Internet and nothing appears, can somebody help me on forcing the selected items text to be red?
Changing the colors and fonts of highlighted items is trickier than it seems, because highlighting selected items is a system-wide issue. In fact, it was something I had delayed for a long time, and just today I've finally tackled...
There are at least two ways to change the looks of a list control: owner draw (you have to do all the drawing yourself) and custom draw (the system tells you when it is about to do some steps of the drawing and lets you change the color, the font, etc.). This answer is about custom draw. This article covers the basics of using Custom Draw with CListCtrl.
How to change the highlight color in a CListCtrl (not a CMFCListCtrl, we'll get to that soon) is explained in this other article. These are the steps you have to do:
Intercept the listview draw routine just before it is about to draw a highlighted row (item).
Turn off the row highlight.
Set the row colors to whatever you want.
Let the listview draw the row.
Intercept the listview draw routine after it has drawn the row (post-draw item).
Turn this row's highlighting back on.
So, when the listview is about to draw a highlighted row, you have to telll the system the row is not highlighted so it doesn't use the system colors to paint it, tell what color to use, and set the item as highlighted again, so the selection works as usual.
Here is the code from that article:
COLORREF g_MyClrFgHi; // My foreground hilite color
COLORREF g_MyClrBgHi; // My background hilite color
HWND g_hListView; // Window handle of listview control
void EnableHighlighting(HWND hWnd, int row, bool bHighlight)
{
ListView_SetItemState(hWnd, row, bHighlight? 0xff: 0, LVIS_SELECTED);
}
bool IsRowSelected(HWND hWnd, int row)
{
return ListView_GetItemState(hWnd, row, LVIS_SELECTED) != 0;
}
bool IsRowHighlighted(HWND hWnd, int row)
{
// We check if row is selected.
// We also check if window has focus. This was because the original listview
// control I created did not have style LVS_SHOWSELALWAYS. So if the listview
// does not have focus, then there is no highlighting.
return IsRowSelected(hWnd, row) && (::GetFocus(hWnd) == hWnd);
}
BOOL OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult)
{
static bool bIsHighlighted = false;
*pResult = 0;
NMHDR *p = (NMHDR *)lParam;
switch (p->code)
{
...
case NM_CUSTOMDRAW:
NMLVCUSTOMDRAW *lvcd = (NMLVCUSTOMDRAW *)p;
NMCUSTOMDRAW &nmcd = lvcd->nmcd;
switch (nmcd.dwDrawStage)
{
case CDDS_PREPAINT:
// We want item prepaint notifications, so...
*pResult = CDRF_NOTIFYITEMDRAW;
break;
case CDDS_ITEMPREPAINT:
{
int iRow = (int)nmcd.dwItemSpec;
bHighlighted = IsRowHighlighted(g_hListView, iRow);
if (bHighlighted)
{
lvcd->clrText = g_MyClrFgHi; // Use my foreground hilite color
lvcd->clrTextBk = g_MyClrBgHi; // Use my background hilite color
// Turn off listview highlight otherwise it uses the system colors!
EnableHighlighting(g_hListView, iRow, false);
}
// We want item post-paint notifications, so...
*pResult = CDRF_DODEFAULT | CDRF_NOTIFYPOSTPAINT;
break;
}
case CDDS_ITEMPOSTPAINT:
{
if (bHighlighted)
{
int iRow = (int)nmcd.dwItemSpec;
// Turn listview control's highlighting back on now that we have
// drawn the row in the colors we want.
EnableHighlighting(g_hListView, iRow, true);
}
*pResult = CDRF_DODEFAULT;
break;
}
default:
*pResult = CDRF_DODEFAULT;
break;
}
break;
...
}
}
This works fine with a CListCtrl, but you are asking about a CMFCListCtrl. The problem is that CMFCListCtrl is already asking to be notified about NM_CUSTOMDRAW notifications (that's when it calls the OnGetCellTextColor function, as you've seen). If you create a handler for that in your own CMFCListCtrl-derived class, those notifications won't get to CMFCListCtrl and you lose that functionality (it may fine, depending on your needs).
So here is what I've done: I've created a NM_CUSTOMDRAW handler in my list control and, if I'm dealing with a highlighted row, I change the colors, otherwise, I call CMFCListCtrl::OnNMCustomDraw(). As you'll see, I just use the normal highlight colors; that's because I just wanted to see the selected items even when the control doesn't have the focus:
void CMyListCtrl::OnNMCustomdraw(NMHDR* pNMHDR, LRESULT* pResult)
{
bool callParent = true;
static bool bHighlighted = false;
LPNMLVCUSTOMDRAW lpLVCustomDraw = reinterpret_cast<LPNMLVCUSTOMDRAW>(pNMHDR);
NMCUSTOMDRAW nmcd = lpLVCustomDraw->nmcd;
*pResult = CDRF_DODEFAULT;
switch (lpLVCustomDraw->nmcd.dwDrawStage)
{
case CDDS_PREPAINT:
*pResult = CDRF_NOTIFYITEMDRAW;
break;
case CDDS_ITEMPREPAINT:
{
int row = nmcd.dwItemSpec;
bHighlighted = IsRowHighlighted(row);
if (bHighlighted)
{
lpLVCustomDraw->clrText = GetSysColor(COLOR_HIGHLIGHTTEXT);
lpLVCustomDraw->clrTextBk = GetSysColor(COLOR_HIGHLIGHT);
EnableHighlighting(row, false);
*pResult = CDRF_DODEFAULT | CDRF_NOTIFYPOSTPAINT;
callParent = false;
}
}
break;
case CDDS_ITEMPOSTPAINT:
if (bHighlighted)
{
int row = nmcd.dwItemSpec;
EnableHighlighting(row, true);
callParent = false;
}
*pResult = CDRF_DODEFAULT;
break;
default:
break;
}
if (callParent)
{
__super ::OnCustomDraw(pNMHDR, pResult);
}
}
bool CMyListCtrl::IsRowHighlighted(int row)
{
bool selected = GetItemState(row, LVIS_SELECTED) != 0;
return selected;
}
void CMyListCtrl::EnableHighlighting(int row, bool enable)
{
SetItemState(row, enable ? 0xff : 0, LVIS_SELECTED);
}
I haven't tested it thoroughly, but it seems to work.
UPDATE:
There is a little problem. When you call EnableHigilighting(), the dialog will get LVN_ITEMCHANGED notifications, which can trigger a redraw, which will trigger a LVN_ITEMCHANGED notification... So if you are listening to LVN_ITEMCHANGED notifications, you might need to do something like this:
void CMyListCtrl::EnableHighlighting(int row, bool enable)
{
m_internalStateChange = true;
SetItemState(row, enable ? 0xff : 0, LVIS_SELECTED);
m_internalStateChange = false;
}
void CWhateverDialog::OnLvnItemchangedListaEjesPane(NMHDR *pNMHDR, LRESULT *pResult)
{
LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
if (!c_List.InternalStateChange() && /* other conditions */)
{
// Respond to state changes
}
}

The entire scrollbar moves in a CWnd when calling ScrollWindowEx with SW_SCROLLCHILDREN

I've added a CScrollBar to my Cwnd named CPanel. But when I scroll the page the whole scrollbar moves. Any idea how I can solve this? Changing CPanel to a CScrollView or CFormView is sadly not an option.
CPanel::CPanel()
{
CreateEx(WS_EX_CONTROLPARENT, _T("Static"), NULL, WS_CHILD | WS_TABSTOP | WS_BORDER, m_clRect, pwndParent, IDC_PANEL_FORM);
ScrollBarInit();
}
void CPanel::ScrollBarInit()
{
//Before this i calculate size of scrollbar and size of scrollarea
m_pclScrollBar = new CScrollBar();
m_pclScrollBar->Create(WS_CHILD | WS_VISIBLE | SBS_VERT | SBS_RIGHTALIGN, clRectScrollbar, this, IDC_SCROLLBAR_FORM);
m_pclScrollBar->SetScrollRange(VSCROLL_RANGE_MIN, VSCROLL_RANGE_MAX);
//After this I add scrollbar info
}
void CPanel::OnVScroll(UINT iSBCode, UINT iPos, CScrollBar* pclScrollBar)
{
switch(pclScrollBar->GetDlgCtrlID())
{
case IDC_SCROLLBAR_FORM:
ScrollBarScroll(iSBCode, iPos, pclScrollBar);
break;
}
}
void CPanel::ScrollBarScroll(UINT iSBCode, UINT iPos, CScrollBar *pclScrollBar)
{
int iScrollPositionPrevious;
int iScrollPosition;
int iScrollPositionOriginal;
iScrollPositionOriginal = m_pclScrollBar->GetScrollPos();
iScrollPosition = iScrollPositionOriginal;
if(m_pclScrollBar != NULL)
{
SCROLLINFO info = {sizeof( SCROLLINFO ), SIF_ALL};
pclScrollBar->GetScrollInfo(&info, SB_CTL);
pclScrollBar->GetScrollRange(&info.nMin, &info.nMax);
info.nPos = pclScrollBar->GetScrollPos();
iScrollPositionPrevious = info.nPos;
switch(iSBCode)
{
case SB_TOP: // Scroll to top
iScrollPosition = VSCROLL_RANGE_MIN;
break;
case SB_BOTTOM: // Scroll to bottom
iScrollPosition = VSCROLL_RANGE_MAX;
break;
case SB_ENDSCROLL: // End scroll
break;
case SB_LINEUP: // Scroll one line up
if(iScrollPosition - VSCROLL_LINE >= VSCROLL_RANGE_MIN)
iScrollPosition -= VSCROLL_LINE;
else
iScrollPosition = VSCROLL_RANGE_MIN;
break;
case SB_LINEDOWN: // Scroll one line down
if(iScrollPosition + VSCROLL_LINE <= VSCROLL_RANGE_MAX)
iScrollPosition += VSCROLL_LINE;
else
iScrollPosition = VSCROLL_RANGE_MAX;
break;
case SB_PAGEUP: // Scroll one page up
{
// Get the page size
SCROLLINFO scrollInfo;
m_pclScrollBar->GetScrollInfo(&scrollInfo, SIF_ALL);
if(iScrollPosition > VSCROLL_RANGE_MIN)
iScrollPosition = max(VSCROLL_RANGE_MIN, iScrollPosition - VSCROLL_PAGE);
break;
}
case SB_PAGEDOWN: // Scroll one page down
{
// Get the page size
SCROLLINFO scrollInfo;
m_pclScrollBar->GetScrollInfo(&scrollInfo, SIF_ALL);
if(iScrollPosition < VSCROLL_RANGE_MAX)
iScrollPosition = min(VSCROLL_RANGE_MAX, iScrollPosition + VSCROLL_PAGE);
break;
}
case SB_THUMBPOSITION: // Scroll to the absolute position. The current position is provided in nPos
case SB_THUMBTRACK: // Drag scroll box to specified position. The current position is provided in nPos
iScrollPosition = iPos;
break;
default:
break;
}
if(iScrollPositionOriginal != iScrollPosition)
{
m_pclScrollBar->SetScrollPos(iScrollPosition);
ScrollWindowEx(0, iScrollPositionOriginal - iScrollPosition, NULL, NULL, NULL, NULL, SW_SCROLLCHILDREN | SW_INVALIDATE | SW_ERASE);
}
}
}
Since you specified SW_SCROLLCHILDREN in your call to ScrollWindowEx (see also the Windows API documentation for ScrollWindowEx; it is usually better than MFC's), and requested the entire client area to be scrolled by passing NULL for the lpRectScroll parameter, the system does just that. The scrollbar is a child window as well, so it gets moved like all other child controls.
The solution is hinted to in the documentation for SW_SCROLLCHILDREN:
Scrolls all child windows that intersect the rectangle pointed to by lpRectScroll by the number of pixels specified in dx and dy.
To prevent the scrollbar from moving alongside the other child windows, it must be excluded from the rectangle passed as the lpRectScroll parameter. To do so, query the client area, and subtract the area occupied by the scrollbar. Assuming that the scrollbar is at the right and covers the entire height, the following code will solve your issue:
if(iScrollPositionOriginal != iScrollPosition) {
m_pclScrollBar->SetScrollPos(iScrollPosition);
// Query the window's client area
CRect clientArea;
GetClientRect(clientArea);
// Find the area occupied by the scrollbar
CRect scrollbarArea;
m_pclScrollBar->GetWindowRect(scrollbarArea);
// Adjust the client area to exclude the scrollbar area
CRect scrollArea(clientArea);
scrollArea.DeflateRect(0, 0, scrollbarArea.Width(), 0);
ScrollWindowEx(0, iScrollPositionOriginal - iScrollPosition, scrollArea, NULL,
NULL, NULL, SW_SCROLLCHILDREN | SW_INVALIDATE | SW_ERASE);
}
Also keep in mind the remark on using the SW_SCROLLCHILDREN flag:
If the SW_SCROLLCHILDREN flag is specified, Windows will not properly update the screen if part of a child window is scrolled. The part of the scrolled child window that lies outside the source rectangle will not be erased and will not be redrawn properly in its new destination. Use the DeferWindowPos Windows function to move child windows that do not lie completely within the lpRectScroll rectangle.
Since you don't have control over the amount of pixels the user will scroll, there will be situations where the screen will not properly update. To work around this, implement a solution following the procedure outlined in the quote above: Replace the call to ScrollWindowEx with a series of calls to DeferWindowPos, repositioning all child windows manually.

Transparent radio button control with themes using Win32

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:
Have I missed some other way to achieve my desired result?
Is it possible to fix the clipped button corners issue with my code and still have a transparent background
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.
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 :)
I can't immediately try this out, but so far as I recall, you don't need owner draw. You need to do this:
Return 1 from WM_ERASEBKGND.
Call DrawThemeParentBackground from WM_CTLCOLORSTATIC to draw the background there.
Return GetStockObject(NULL_BRUSH) from WM_CTLCOLORSTATIC.
Knowing the sizes and coordinates radio button, we will copy the
image to them closed.
Then we create a brush by means of
BS_PATTERN style CreateBrushIndirect
Farther according to the
usual scheme - we return handle to this brush in reply to COLOR -
the message (WM_CTLCOLORSTATIC).
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;
}