Owner Drawn Spin Control

Environment: The code was
compiled with VC6 SP2, Win95 SP2 and tested with Win95 SP2,Win98 and NT4 SP3.

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



  1. Add LBSpinButtonCtrl.h and LBSpinButtonCtrl.cpp to your project.
  2. Include LBSpinButtonCtrl.h file to the desired class header file.

    #include "LBSpinButtonCtrl.h";

  3. Add a member variable to the desired

    class (CDialog, CFormView, etc.)
    CLBSpinButtonCtrl m_Spin;


  4. Subclass the spin control variable just
    created.

    void CYourDlg::DoDataExchange(CDataExchange* pDX)
    {
    CDialog::DoDataExchange(pDX);
    //{{AFX_DATA_MAP(CYourDlg)
    DDX_Control(pDX, IDC_SPIN, m_Spin);
    //}}AFX_DATA_MAP
    }


    Note: The custom behaivor of
    CLBSpinButtonCtrl switching 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:


  • 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


a.) 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);

// As so we are bypassing WM_ERASEBCKGND, by returning
// TRUE from its handler – let’s do its job here
dc.FillSolidRect(&rctPaint,::GetSysColor(COLOR_BTNFACE));


b.) Then we draw control using DrawFrameControl API



dc.DrawFrameControl(&rcPaintUp,
DFC_SCROLL,DFCS_SCROLLUP); // or DFCS_SCROLLLEFT

dc.DrawFrameControl(&rcPaintDown,
DFC_SCROLL,DFCS_SCROLLDOWN); // or DFCS_SCROLLRIGHT

c.) After that, we check control alignment
and if we it is inside buddy, then we have to draw buddy’s border around
CLBSpinButtonCtrl.


d.) 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.


e.) 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.


f.) 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"))
{
//The class of buddy is Edit
m_bBuddyIsEdit=true;

//Test if WH_CALLWNDPROC hook already was installed.
// If no, set up it.
if(ghHook==NULL)
ghHook=SetWindowsHookEx(WH_CALLWNDPROC,
FilterBuddyMsgProc,NULL,GetCurrentThreadId());

//Try to find m_hWndBuddy in the gHandleMap
HWNDMAP::iterator iterHwnd=gHandleMap.find(m_hWndBuddy);
if(iterHwnd != gHandleMap.end())
{
if((*iterHwnd).second != m_hWnd)
{
//If in the gHandleMap already defined
//another CLBSpinButtonCtrl for that buddy
//(m_hWndBuddy), then redefine it .
gHandleMap.erase(iterHwnd);
gHandleMap.insert(HWNDMAP::value_type(m_hWndBuddy,m_hWnd));
}
}
else
//If the CLBSpinButtonCtrl assosiated with
//m_hWndBuddy is not found in the gHandleMap, then add it.
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))
{
//If the incoming message is EN_UPDATE
HWNDMAP::iterator iterHwnd =
gHandleMap.find(reinterpret_cast<HWND>(pData->lParam));

if(iterHwnd != gHandleMap.end())
{
//The incoming EN_UPDATE message has been sent
//to the edit control, defined in the gHandleMap.

//So, let’s get the value, entered into the edit control
//and send it to the CLBSpinButtonCtrl, assosiated
//with the edit control.
CString strText;
int nLen=::GetWindowTextLength((*iterHwnd).first);

::GetWindowText((*iterHwnd).first,
strText.GetBufferSetLength(nLen),nLen+1);

strText.ReleaseBuffer();

//In case UDS_NOTHOUSANDS style not applied
//we have to delete thousands delimiter from string
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)
{
//If the buddy is edit, then try to find out if
//it was added to gHandleMap.
HWNDMAP::iterator iterHwnd=gHandleMap.find(m_hWndBuddy);

if(iterHwnd != gHandleMap.end()
&& (*iterHwnd).second == m_hWnd)
{
//If m_hWndBuddy found and is assosiated with
// current window, then delete it from gHandleMap.
iterHwnd = gHandleMap.erase(iterHwnd);
if(!gHandleMap.size() && ghHook!=NULL)
{
//If just deleted from the gHandleMap m_hWndBuddy was
//the last for current application,
//then remove a hook procedure.
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 persist 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.

Downloads

Download demo project – 18 Kb

Download source – 8 Kb

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read