Customize date-time controls
3 minute read •
This is quite a short tutorial. The purpose of this exercise to remove the spin-buttons from the edge of a date-time control. The result is a control with looks like an edit box, but behaves exactly like a date-time control. This picture will show you the effect.
The standard date-time control looks rather messy in my opinion, especially when several are clustored together like in the example shown above.
With a little subclassing we can make the date-time controls look much cleaner in appearance. The second dialog looks far less cluttered, and isn’t so overwhelming to look at. The new looking date-time control pictured above is achieved using two steps.
Hiding the Spin control
This is achieved by resizing the control horizontally by an amount large enough so that the spin control moves outside of the original bounding rectangle of the control. This happens because the date-time control reacts to the WM_SIZE
message, and when it is resized, it aligns the spin control to the right-hand side of the control.
Next the control is subclassed. The subclass procedure intercepts the WM_SIZE
message so that it performs no processing when that message is received. We then size the control back to its original position. Because the control no longer performs any WM_SIZE
processing, the spin child control is not aligned to the right-hand side any more, and becomes hidden because it is clipped outside the control’s window rectangle.
int MakeCoolDateTime(HWND hwndDT)
{
RECT rect;
WNDPROC oldproc;
//work out how big the date-time control is.
GetWindowRect(hwndDT, &rect);
OffsetRect(&rect, -rect.left, -rect.top);
//resize the date-time control horizontally. We don't
//want to move it, so we can use SetWindowPos.
SetWindowPos(hwndDT, 0, 0, 0, rect.right+100, rect.bottom,
SWP_NOMOVE | SWP_NOACTIVATE | SWP_NOZORDER);
//subclass the control so when we size it back to what it was,
//our wndproc will intersept the WM_SIZE, and the scrollbar will not
//get shifted back into the control
oldproc = (WNDPROC)SetWindowLong(hwndDT, GWL_WNDPROC, (LONG)NewDTProc);
//we need to remember the old window procedure. Just store
//the pointer in the USERDATA area of the window.
SetWindowLong(hwndDT, GWL_USERDATA, (LONG)oldproc);
//make it a little smaller too
rect.right -= 2;
rect.bottom -= 2;
//size the date-time control back to it's original place
SetWindowPos(hwndDT, 0, 0, 0, rect.right, rect.bottom,
SWP_NOMOVE | SWP_NOACTIVATE | SWP_NOZORDER);
return 0;
}
Creating a flat look
The flat look is achieved by handling the WM_NCPAINT
message in the subclass procedure. Instead of passing control to the old window procedure for the date-time control, we simply draw our own border using a single grey line.
case WM_NCPAINT:
hdc = GetWindowDC(hwnd);
//work out the coordinates of the window rectangle,
GetWindowRect(hwnd, &rect);
//make them relative to the top-left of the window
OffsetRect(&rect, -rect.left, -rect.top);
InflateRect(&rect, -1, -1);
//Draw a single line around the outside
FrameRect(hdc, &rect, GetSysColorBrush(COLOR_3DSHADOW));
ReleaseDC(hwnd, hdc);
return 0;
To give the control a different coloured back-ground, we could probably use the Custom Draw facility provided by the Date-Time control. However, it is much easier to handle the WM_ERASEBKGND
in this situation, because we already have a subclass procedure in place. All we do is fill the background using our own colour instead of letting the default window procedure use the standard window background.
case WM_ERASEBKGND:
hdc = (HDC)wParam;
GetClientRect(hwnd, &rect);
FillRect(hdc, &rect, GetSysColorBrush(COLOR_BTNHILIGHT));
return -1;
Conclusion
You can see that by just using a simple subclassing procedure we have dramatically changed the appearance of a control. There are many enhancements that we could add to the date-time control by just adding a little more logic to the control. One possiblity would be to only show the spin control when the mouse moves over a date-time control. This would require mouse hit-testing, and extending the WM_SIZE
logic.