There are many occasions where it's nice to have spin button control with autodisabling arrow buttons. The CLBSpinButtonCtrl
class adds this feature to the default behavior of standard CSpinButtonCtrl
.
How to use CLBSpinButtonCtrl
- Add LBSpinButtonCtrl.h and LBSpinButtonCtrl.cpp to your project.
- Include LBSpinButtonCtrl.h file to the desired class header file.
#include "LBSpinButtonCtrl.h";
- Add a member variable to the desired class (
CDialog
, CFormView
, etc.) CLBSpinButtonCtrl m_Spin;
- Subclass the spin control variable just created.
void CYourDlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
DDX_Control(pDX, IDC_SPIN, m_Spin);
}
Note: The custom behaivor of CLBSpinButtonCtrl
is to switch off automatically if the style UDS_WRAP
was applied to the control.
How it works
1. Overview
The CLBSpinButtonCtrl
is owner drawn up-down control. To make its job this control handles the following messages:
<LI><CODE>WM_PAINT
UDN_DELTAPOS
UDM_SETRANGE32
UDM_SETRANGE
UDM_SETBUDDY
WM_ERASEBKGND
WM_LBUTTONDOWN
WM_LBUTTONUP
WM_DESTROY
It also overrides virtual member functions of CSpinButtonCtrl:
PreSubclassWindow
WindowProc
The WH_CALLWNDPROC
hook is used to update control's state when user enters or paste new values into corresponding buddy window (if such window exist).
Note:There is an possibility to switch off /on the custom behaivor of CLBSpinButtonCtrl
, using its public member function [bool SetAutoDisable(bool bSetOn)
].
2. Handling of notification message UDN_DELTAPOS
The UDN_DELTAPOS notification is sent before the WM_VSCROLL or WM_HSCROLL message, which actually changes the control's position. This lets me examine, allow, modify, or disallow the change.
The UDN_DELTAPOS is processed as reflected message.
If new position of spin control is the same as previous one, what can happen only when user clicked a disable arrow, then the handler eat UDN_DELTAPOS message.
Otherwise the control updates its state, depending on its new position and let UDN_DELTAPOS to be processed by system.
3. Handling of WM_PAINT message
- First of all to get rid of flickering we are drawing to memory DC (dc). So we have to create compatible memory DC and select bitmap into it.
CPaintDC RealDC(this);
CDC dc;
CBitmap bmpMem,*pOldMemBmp;
dc.CreateCompatibleDC(&RealDC);
bmpMem.CreateCompatibleBitmap(&RealDC, rctPaint.Width(),
rctPaint.Height());
pOldMemBmp=dc.SelectObject(&bmpMem);
dc.FillSolidRect(&rctPaint,::GetSysColor(COLOR_BTNFACE));
- Then we draw control using DrawFrameControl API
dc.DrawFrameControl(&rcPaintUp,DFC_SCROLL,DFCS_SCROLLUP);
dc.DrawFrameControl(&rcPaintDown,DFC_SCROLL,DFCS_SCROLLDOWN);
- After that, we check control alignment and if we it is inside buddy, then we have to draw buddy's border around
CLBSpinButtonCtrl
.
- Then we check the current position of control, and if it reached the limit, we draw corresponding part of control as disabled, using BitBlt and advanced ROP codes.
- On the next step we check if control's position has changed,since previous call to OnPaint,if so we have to draw corresponding part of control as pressed. It is done using BitBlt and dc.MoveTo/LineTo API.
- At the final step we copy the resulting bitmap from memory DC to the screen, using BitBlt with
SRCCOPY
ROP.
4. The WH_CALLWNDPROC hook
If control has buddy window and it is of 'Edit' class, then control have to update its enabled/disabled state when contents of buddy window is changing. For instance, if user enters into buddy window the value greater than upper possible limit - it is obvious that the increasing arrow should switch to disable state. The best way to do it - create WH_CALLWNDPROC hook and test within it if EN_UPDATE message came from buddy window. Another posibility is in setting keyboard hook, but then user can foolish the control, using clipboard Copy/Cut/Paste functions.
As so I use a single WH_CALLWNDPROC hook for all existing in an application controls of class CLBSpinButtonCtrl, I need to distinguish between these controls in the static hook handler FilterBuddyMsgProc. For this purpose I used static std:map<HWND,HWND> gHandleMap, where key is handle to buddy window and assosiated value is handle to the spin control window.
Here is source code , used to install hook:
if(::IsWindow(m_hWndBuddy))
{
char buf[5];
::GetClassName(m_hWndBuddy,buf,sizeof(buf)/sizeof(buf[0]));
if(!strcmp(buf,"Edit"))
{
m_bBuddyIsEdit=true;
if(ghHook==NULL)
ghHook=SetWindowsHookEx(WH_CALLWNDPROC,FilterBuddyMsgProc,NULL,
GetCurrentThreadId());
HWNDMAP::iterator iterHwnd=gHandleMap.find(m_hWndBuddy);
if(iterHwnd != gHandleMap.end())
{
if((*iterHwnd).second != m_hWnd)
{
gHandleMap.erase(iterHwnd);
gHandleMap.insert(HWNDMAP::value_type(m_hWndBuddy,m_hWnd));
}
}
else
gHandleMap.insert(HWNDMAP::value_type(m_hWndBuddy,m_hWnd));
}
}
Here is how my WH_CALLWNDPROC hook works:
LRESULT CALLBACK FilterBuddyMsgProc( int code, WPARAM wParam,
LPARAM lParam )
{
CWPSTRUCT* pData=reinterpret_cast<CWPSTRUCT*>(lParam);
if(WM_COMMAND==pData->message && EN_UPDATE==HIWORD(pData->wParam))
{
HWNDMAP::iterator iterHwnd =
gHandleMap.find(reinterpret_cast<HWND>(pData->lParam));
if(iterHwnd != gHandleMap.end())
{
CString strText;
int nLen=::GetWindowTextLength((*iterHwnd).first);
::GetWindowText((*iterHwnd).first,
strText.GetBufferSetLength(nLen),nLen+1);
strText.ReleaseBuffer();
strText.Remove((TCHAR)0xA0);
NMUPDOWN nmUpDn;
nmUpDn.iDelta=0;
nmUpDn.iPos=atoi(strText);
nmUpDn.hdr.code=UDN_DELTAPOS;
nmUpDn.hdr.hwndFrom=(*iterHwnd).second;
nmUpDn.hdr.idFrom=::GetDlgCtrlID((*iterHwnd).second);
HWND hWndSpinParent=::GetParent((*iterHwnd).second);
::SendMessage(::GetParent((*iterHwnd).second),
WM_NOTIFY,(WPARAM)nmUpDn.hdr.idFrom,
(LPARAM)&nmUpDn);
}
}
return CallNextHookEx(ghHook,code,wParam,lParam);
}
5. Handling WM_DESTROY message
When the CLBSpinButtonCtrl
window is being destroyed, we have to remove buddy/spin association from the gHandleMap
and if it is the last control, defined in the gHandleMap
, then remove a hook procedure. This is done in member function CleanUpHook().
void CLBSpinButtonCtrl::CleanUpHook() const
{
if(m_bBuddyIsEdit)
{
HWNDMAP::iterator iterHwnd=gHandleMap.find(m_hWndBuddy);
if(iterHwnd != gHandleMap.end() && (*iterHwnd).second == m_hWnd)
{
iterHwnd = gHandleMap.erase(iterHwnd);
if(!gHandleMap.size() && ghHook!=NULL)
{
UnhookWindowsHookEx( ghHook);
ghHook=NULL;
}
}
}
}
6. Handling of UDM_SETRANGE, UDM_SETRANGE32 messages
As so these messages can be used at runtime to set/change the minimum and maximum positions (limits) for a spin control. We have to to reinit
CLBSpinButtonCtrl
.
7. Handling of UDM_SETBUDDY message
This message sets/changes the buddy window for an up-down control, so we need to make corresponding updates to CLBSpinButtonCtrl too (call to CleanUpHook and reinit ).
7.1. Buggy handling UDM_SETBUDDY by common up-down control
When I was testing the UDM_SETBUDDY handling by CLBSpinButtonCtrl
I've discovered a visual bug. If buddy window (m_hWndBuddy
) is placed after this CLBSpinButtonCtrl
in Z-order , then m_hWndBuddywindow
will get WM_PAINT message after this CLBSpinButtonCtrl
control and in case CLBSpinButtonCtrl
is attached to buddy, it will be overpainted by buddy's border.
This undocumented bug persists for CSpinButtonCtrl
as well.
To reproduce it create on dialog template CEdit control and right (with UDS_ALIGNRIGHT style) or left ( with UDS_ALIGNLEFT style) attached CSpinButtonCtrl
control. Then make tab order so, that for CSpinButtonCtrl
the tab position was less then for CEdit
. After that in OnInitDiaolg
call SetBuddy(pointerToEdit)
function of CSpinButtonCtrl
. Finally you will see the bug:
Below the same picture is enlarged:
I've worked around this by placing
CSpinButtonCtrl
after it's buddy in Z-order, using
SetWindowPos
API:
::SetWindowPos(m_hWndOfSpin,m_hWndOfBuddy,0,0,0,0, SWP_NOMOVE|SWP_NOSIZE);
Standard Disclaimer
These files may be redistributed unmodified by any means providing it is not sold for profit without the authors written consent, and providing that the authors name and all copyright notices remains intact. This code may be used in compiled form in any way you wish with the following conditions:
If the source code is used in any commercial product then a statement along the lines of "Portions Copyright (C) 1999 Oleg Lobach" must be included in the startup banner or "About" box or printed documentation. The source code may not be compiled into a standalone library and sold for profit. In any other cases the code is free to whoever wants it anyway!
This software is provided "as is" without express or implied warranty. Use it at you own risk! The author accepts no liability for any damages to your computer or data these products may cause.