C++ At Work

Addin a Combobox Cancel Feature

Paul DiLascia

Code download available at:C++atWork2006_08.exe(193 KB)

Q I have a dialog with a combobox. If I press the Cancel button the dialog closes immediately, although the focus is on the combobox. However, if I start auto-completion by typing a letter or opening the dropdown list, and then press the Cancel button, the dropdown closes but the dialog box does not. This is somewhat nasty for the user. In Microsoft® Word, the file open menu manages to close the dropdown list and cancel the dialog. If I open the dropdown list and press Cancel, the file dialog closes. How can I make Cancel close my dialog immediately?

Q I have a dialog with a combobox. If I press the Cancel button the dialog closes immediately, although the focus is on the combobox. However, if I start auto-completion by typing a letter or opening the dropdown list, and then press the Cancel button, the dropdown closes but the dialog box does not. This is somewhat nasty for the user. In Microsoft® Word, the file open menu manages to close the dropdown list and cancel the dialog. If I open the dropdown list and press Cancel, the file dialog closes. How can I make Cancel close my dialog immediately?

Tahir Helvaci

A I wish I had ten dollars for every question I get like this: "Microsoft Office has feature X, but not my app—How can I add it?" The Redmondtonians have so many programmers in the Office division they have time to practically reinvent Windows®. The Office products have their own toolbars, dialogs, and other UI components. All you need is Spy++ to discover this. For example, the file open dialog in Word is not the standard dialog class you and I get, it's a window with class name "bosa_sdm_Microsoft Office Word 11.0". It kind of makes Windows look bad, as if it's not good enough for Office. And it's frustrating to programmers who expect the same features for their own apps.

A I wish I had ten dollars for every question I get like this: "Microsoft Office has feature X, but not my app—How can I add it?" The Redmondtonians have so many programmers in the Office division they have time to practically reinvent Windows®. The Office products have their own toolbars, dialogs, and other UI components. All you need is Spy++ to discover this. For example, the file open dialog in Word is not the standard dialog class you and I get, it's a window with class name "bosa_sdm_Microsoft Office Word 11.0". It kind of makes Windows look bad, as if it's not good enough for Office. And it's frustrating to programmers who expect the same features for their own apps.

Well, I can't change the world, but I can show you how to make your dropdown list close without losing the click that closed it. I've seen solutions for this on the Web, but they all involve changing the dialog box to simulate a Cancel or OK the appropriate time. This band-aid approach doesn't really solve the problem at its root, and therefore doesn't work in all situations—for example, when you click the close or minimize/restore buttons in the window's caption bar. And it requires modifying each dialog on an ad hoc basis. A better and more easily reusable solution would have the following characteristics:

  • Clicking outside the combobox dropdown should feed the mouse click to the underlying window, wherever the mouse happens to be—whether it's the Cancel button, OK button or some other button, control, or window.
  • It should be easy (for programmers) to use: just include some code and set a flag or call a function or something to get the feature. It shouldn't require writing message handlers or modifying dialog procs.
  • It should be universal for the entire app. That is, you should be able to turn on the fix-the-combobox feature for your entire app in one fell swoop. You shouldn't need to remember to add code every time you implement a new dialog. Either the whole app gets the feature or it doesn't.

Lucky you, I implemented a little class that does all this. It's called CComboClickFix for lack of a clever name. (I'm all in favor of long descriptive names, but CPassComboDropdownOutsideClicksToUnderlyingWindow is too much, even for me.) As promised, all you have to do to use CComboClickFix is add the source files to your project and a couple of lines in your application module:

// one-and-only global instance CComboClickFix combofix; ... BOOL CMyApp::InitInstance() { ... combofix.Install(); // init }

Now any time the user clicks the mouse outside a combobox dropdown anywhere in your app, the system closes the dropdown and passes the click through without eating it. How does CComboClickFix perform this magic?

As Windows wonks have no doubt already guessed, CComboClickFix uses window hooks to peer into the message stream. But which messages should I hook? To find out, I wrote a little class called CComboTrace that spies on combo notifications. CComboTrace uses CSubclassWnd, a class I frequently use to subclass a window in the Windows sense of subclassing its window procedure. CComboTrace displays TRACE diagnostics any time the dialog gets a combo notification—that is, a WM_COMMAND message from a combobox. To use CComboTrace, just instantiate the class and call HookWindow:

class CMyOpenDlg : public CFileDialog { protected: CComboTrace m_cbTrace; ... }; BOOL CMyOpenDlg::OnInitDialog() { m_cbTrace.HookWindow(GetParent()); }

CComboTrace displays CBN_XXX codes in human-readable format; Figure 1 shows a typical stream. After two minutes of testing with CComboTrace, it appeared that the key notification to trap was, not surprisingly, CBN_CLOSEUP. But further investigation revealed that by the time the combo sends CBN_CLOSEUP, it has already eaten the mouse message. So the notification to look for is actually CBN_SELENDCANCEL, which comes before CBN_CLOSEUP. There's just one snag: comboboxes also send CBN_SELENDCANCEL when the user first tabs to the combobox. The upshot is that you can't look for just one message; you have to look for the sequence CBN_SELENDCANCEL followed immediately by CBN_CLOSEUP.

Figure 1 Typical ComboTrace Stream

Figure 1** Typical ComboTrace Stream **

To see messages, CComboClickFix installs two Windows hooks: a WH_MOUSE hook to trap mouse messages and a WH_CALLWNDPROC hook to trap combo notifications. Hooks are always a delicate affair, you don't want to do much processing in your hook procedure or you'll bog the system down. You also don't want to send messages from your hook proc because the system is in a funky state, in the middle of processing a message. I've learned from experience that the best way to handle hooks is to save the information you want, then process it later when the message queue reaches a settled state. So my mouse hook simply records clicks. Any time it sees WM_LBUTTONDOWN, it saves the message code (WM_LBUTTONDOWN) in m_state and the cursor/mouse position in m_point. At any given time, CComboClickFix::m_point holds the coordinates of the most recent WM_LBUTTONDOWN message.

The call-window-proc hook is slightly more complex. It spies on combo notifications. If the notification is anything but CBN_SELENDCANCEL or CBN_CLOSEUP, it clears the state by setting m_state equal to zero. The purpose of this is to ignore the case of CBN_SELENDCANCEL followed by some other combo notification like CBN_KILLFOCUS or CBN_SETFOCUS. If the combo notification is CBN_CLOSEUP, and m_state is nonzero, the CBN_CLOSEUP can only have come from clicking the mouse outside the dropdown. In that case, CComboClickFix posts itself a private message, MYWM_COMBOTRAP, with LPARAM equal to the screen coordinates of the WM_LBUTTONDOWN message that caused the close. It's important to post, but not send this message, so it can be processed later, after the app finishes processing the current message (CBN_CLOSEUP).

But where does CComboClickFix post MYWM_COMBOTRAP? It needs a window. I could create an invisible window to talk to myself, but that's way overkill for a puny combo feature, so CComboClickFix uses the application's main window instead. If for some reason your app doesn't have a main window (in which case you probably don't have a UI, so why do you need CComboClickFix?), you can tell CComboClickFix which window to use when you call CComboClickFix::Install. To ensure MYWM_COMBOTRAP doesn't collide with other application message IDs, I call RegisterWindowMessage to create a unique ID.

Now whenever the user clicks outside the combo dropdown, CComboClickFix posts MYWM_COMBOTRAP to the main window, with lp equal to the screen coordinates where the mouse was when the user clicked. Does that mean you have to handle MYWM_COMBOTRAP in your main window? No, CComboClickFix uses my ubiquitous CSubclassWnd to subclass the main window and handle MYWM_COMBOTRAP all by itself. The handler calls ::SendInput to feed the WM_LBUTTONDOWN back into the message queue, in effect reposting the mouse click that caused the combo dropdown to close. Since the dropdown is now closed, Windows processes the posted click the normal way—in whatever fashion it would've been processed had the combo not eaten it.

To test CComboClickFix, I wrote a little program CComboTest that runs the standard file open dialog (CFileDialog). Figure 2 shows the dialog running with a combo dropdown open and the mouse poised over the Cancel button. It's a little hard to show the interaction in a screen capture; you'll have to download and build the project yourself to see that clicking outside the dropdown closes it without losing the click.

Figure 2 ComboTest in Action

Figure 2** ComboTest in Action **

Before closing the box on comboboxes, I want to underscore one minor detail. In Windows, there are two flavors of comboboxes: ComboBox and ComboBoxEx32. CComboClickFix uses a function—IsComboBox—to see if the WM_COMMAND is coming from a combobox. By comparing only the first eight characters of the class name, IsComboBox catches both "ComboBox" and "ComboBoxEx32"—or any other variations the Redmondtonians might add some future day. Figure 3 shows the code highlights. Download the full source from the MSDN®Magazine Web site.

Figure 3 Two Types of ComboBoxes

ComboFix.h

#pragma once #include "subclass.h" ////////////////// // Test for combo notification code (CBN_XXX) // inline BOOL IsComboCode(UINT code) { return (CBN_SELCHANGE<=code && code<=CBN_SELENDCANCEL); } ////////////////// // Test for hwnd = a combobox. // Note class name can be ComboBox or ComboBoxEx32! // inline BOOL IsComboBox(HWND hwnd) { TCHAR buf[32]; GetClassName(hwnd,buf,32); return _tcsncmp(buf,_T("ComboBox"),8)==0; } ////////////////// // Class to implement ability to click-outside-the-combo-dropdown. // class CComboClickFix { protected: static CComboClickFix* g_trap; // one-and-only global closer // main window hook to handle private messages class CMainWndHook : public CSubclassWnd { public: virtual LRESULT WindowProc(UINT msg, WPARAM wp, LPARAM lp); } m_mainWndHook; CWnd* m_pMainWnd; // main app window HHOOK m_hhMouse; // mouse hook HHOOK m_hhCallWnd; // CallWndProc hook UINT m_state; // current state (mouse message) CPoint m_point; // ..and cursor pos // hook procedures static LRESULT WINAPI MouseProc(int nCode, WPARAM wp, LPARAM lp); static LRESULT WINAPI CallWndProc(int nCode, WPARAM wp, LPARAM lp); public: CComboClickFix(); ~CComboClickFix(); void Install(CWnd* pMainWnd=NULL); void Remove(); };

ComboFix.cpp

#include "stdafx.h" #include "ComboFix.h" // private message posted to myself const UINT MYWM_COMBOTRAP = RegisterWindowMessage(_T("MYWM_COMBOTRAP")); // Ptr to THE combo trap CComboClickFix* CComboClickFix::g_trap=NULL; CComboClickFix::CComboClickFix() { ASSERT(g_trap==NULL); // singleton class, only one instance allowed! g_trap=this; // set global m_hhMouse = m_hhCallWnd = NULL; } CComboClickFix::~CComboClickFix() { Remove(); } ////////////////// // Install combo trap. Set hooks and clear state. // Arg is main window—or any window used to communicate. // void CComboClickFix::Install(CWnd* pMainWnd) { if (pMainWnd==NULL) pMainWnd = AfxGetMainWnd(); ASSERT(pMainWnd); // need main window! m_pMainWnd = pMainWnd; m_mainWndHook.HookWindow(m_pMainWnd); HMODULE hmod = GetModuleHandle(NULL); DWORD id = GetCurrentThreadId(); m_hhMouse = ::SetWindowsHookEx(WH_MOUSE, MouseProc, hmod, id); m_hhCallWnd = ::SetWindowsHookEx(WH_CALLWNDPROC, CallWndProc, hmod, id); m_state=0; } ////////////////// // Remove combo trap: Remove hooks. // void CComboClickFix::Remove() { m_mainWndHook.Unhook(); // unhook subclass if (m_hhMouse) { UnhookWindowsHookEx(m_hhMouse); m_hhMouse=NULL; } if (m_hhCallWnd) { UnhookWindowsHookEx(m_hhCallWnd); m_hhCallWnd=NULL; } } ////////////////// // Mouse hook: Every time I get WM_LBUTTONDOWN, remember it and pos too. // LRESULT CComboClickFix::MouseProc(int code, WPARAM wp, LPARAM lp) { if (code==HC_ACTION && wp==WM_LBUTTONDOWN) { MOUSEHOOKSTRUCT* pmhs = (MOUSEHOOKSTRUCT*)lp; g_trap->m_state = wp; // save message code g_trap->m_point = pmhs->pt; // ..and point } return ::CallNextHookEx(g_trap->m_hhMouse, code, wp, lp); } ////////////////// // CallWndProc hook: if message is a combobox close message and close was // due to left-button-down, post private message to resend the // left-button-down. For some reason trying to catch the mouse message // here doesn’t work. It never arrives. // LRESULT CComboClickFix::CallWndProc(int code, WPARAM wp, LPARAM lp) { const CWPSTRUCT& cwp = *((CWPSTRUCT*)lp); if (code==HC_ACTION) { if (cwp.message==WM_COMMAND) { if (IsComboBox((HWND)cwp.lParam)) { UINT nCode = HIWORD(cwp.wParam); if (IsComboCode(nCode)) { if (nCode==CBN_CLOSEUP && g_trap->m_state) { // dropdown box was closed via left-button-down: // post private message. // I will handle it myself with my own // CSubclassWnd hook below CPoint& pt = g_trap->m_point; g_trap->m_pMainWnd->PostMessage(MYWM_COMBOTRAP, 0, MAKELONG(pt.x,pt.y)); } if (nCode!=CBN_SELENDCANCEL && nCode!=CBN_CLOSEUP) g_trap->m_state=0; } } } } return ::CallNextHookEx(g_trap->m_hhCallWnd, code, wp, lp); } ////////////////// // Window proc for main window hook: handle private // MYWM_COMBOTRAP messages. // LRESULT CComboClickFix::CMainWndHook::WindowProc(UINT msg, WPARAM wp, LPARAM lp) { if (msg==MYWM_COMBOTRAP) { // got MYWM_COMBOTRAP: lp has the mouse coords. // Call SendInput to feed the lost mouse message back to the app. CPoint pt(LOWORD(lp),HIWORD(lp)); INPUT input; memset(&input,1,sizeof(input)); input.type=INPUT_MOUSE; input.mi.dx=pt.x; input.mi.dy=pt.y; input.mi.dwFlags=MOUSEEVENTF_ABSOLUTE|MOUSEEVENTF_LEFTDOWN; SendInput(1,&input,sizeof(input)); return 0; } return CSubclassWnd::WindowProc(msg,wp,lp);

Q I am trying to make Ctrl+A select all of the items in a listbox. How can I accomplish this?

Q I am trying to make Ctrl+A select all of the items in a listbox. How can I accomplish this?

Mark Fowler

A If all you want to do is implement Ctrl+A for one listbox, you only need a WM_CHAR handler that looks for Ctrl+A (0x01). When you see it, you can select all the items by sending LB_SETSEL with nItem equal to -1:

void CMyListBox::OnChar(UINT nChar, ...) { if (nChar==CONTROL_A) { SetSel(-1,TRUE); } }

But if you were paying attention to the previous question, you might think to ask: Why not make Ctrl+A/Select All a global feature that works for any listbox throughout your whole app? Why not, indeed.

A If all you want to do is implement Ctrl+A for one listbox, you only need a WM_CHAR handler that looks for Ctrl+A (0x01). When you see it, you can select all the items by sending LB_SETSEL with nItem equal to -1:

void CMyListBox::OnChar(UINT nChar, ...) { if (nChar==CONTROL_A) { SetSel(-1,TRUE); } }

But if you were paying attention to the previous question, you might think to ask: Why not make Ctrl+A/Select All a global feature that works for any listbox throughout your whole app? Why not, indeed.

The same hook tricks I used to fix the combobox can be employed to implement your Ctrl+A feature for all listboxes in your app. I wrote a little class, CListBoxAccel, that does it. Just instantiate the class and call Install:

CListBoxAccelerator lba; ... BOOL CMyApp::InitInstance() { ... lba.Install(pMainWnd); }

Install sets a WH_GETMESSAGE hook whose hook procedure looks for WM_CHAR messages sent to a listbox. If the listbox allows multiple selection—that is, if it has the LBS_MULTIPLESEL or LBS_EXTENDEDSEL style—the hook posts LB_SETSEL with LPARAM equal to-1 to select all items:

// in hook proc const MSG& msg = *((MSG*)lp); if (code==HC_ACTION && wp==PM_REMOVE && msg.message==WM_CHAR && IsListBox(msg.hwnd)) { if (IsLBMultiSel(msg.hwnd)) { ::PostMessage(msg.hwnd, LB_SETSEL, TRUE, -1); } }

With WH_GETMESSAGE hooks, it's very important to check for WPARAM equal to PM_REMOVE because otherwise you may end up processing the event twice. Windows calls your WH_GETMESSAGE hook whenever the app calls ::GetMessage or ::PeekMessage. Many apps call ::PeekMessage to check for messages before fetching them, in order to do idle processing between messages. When the app calls ::PeekMessage, Windows calls your hook with WPARAM set to PM_NOREMOVE. Most times you want to ignore this. As in the previous example for comboboxes, CListBoxAccelerator::HookProc posts LB_SETSEL, rather than sending it, so the listbox can process it after the app finishes the current message.

CListBoxAccelerator is even simpler than CComboClickFix. Since it sends no messages to itself, it doesn't need CSubclassWnd or registered messages. Everything happens in the hook proc. This code is shown in Figure 4. The test program populates a list with file names; you can Tab to the list and type Ctrl+A to select them all. If you decide to use CListBoxAccelerator in your own app, you could add more accelerator keys for other functions.

Figure 4 ListBox Accelerators

ListBoxAccel.h

#pragma once // Test for hwnd = a list box. inline BOOL IsListBox(HWND hwnd) { TCHAR buf[32]; GetClassName(hwnd,buf,32); return _tcsncmp(buf,_T(«ListBox»),7)==0; } ////////////////// // Test for multi-select styles. // inline BOOL IsLBMultiSel(HWND hwnd) { DWORD dwStyle = ::GetWindowLong(hwnd,GWL_STYLE); return dwStyle & (LBS_MULTIPLESEL|LBS_EXTENDEDSEL) ? TRUE : FALSE; } ////////////////// // Class to implement Ctrl+A = Select All in listboxes. // You can add more accelerators if you like. // class CListBoxAccelerator { static CListBoxAccelerator* g_lba; // one-and-only listbox accelerator HHOOK m_hHook; // call wnd proc hook // hook procedure static LRESULT WINAPI HookProc(int nCode, WPARAM wp, LPARAM lp); public: CListBoxAccelerator(); ~CListBoxAccelerator(); void Install(); void Remove(); };

ListBoxAccel.cpp

#include "stdafx.h" #include "ListBoxAccel.h" // Ptr to THE listbox accelerator CListBoxAccelerator* CListBoxAccelerator::g_lba=NULL; CListBoxAccelerator::CListBoxAccelerator() { ASSERT(g_lba==NULL); // singleton class, only one instance allowed! g_lba=this; // set global m_hHook = NULL; } CListBoxAccelerator::~CListBoxAccelerator() { Remove(); } ////////////////// // Install listbox accelerator. Set hooks and clear state. // void CListBoxAccelerator::Install() { HMODULE hmod = GetModuleHandle(NULL); DWORD id = GetCurrentThreadId(); m_hHook = ::SetWindowsHookEx(WH_GETMESSAGE, HookProc, hmod, id); } ////////////////// // Remove listbox accelerator: Remove hooks. // void CListBoxAccelerator::Remove() { if (m_hHook) { UnhookWindowsHookEx(m_hHook); m_hHook=NULL; } } ////////////////// // Mouse hook: Every time I get WM_LBUTTONDOWN, remember it and pos too. // LRESULT CListBoxAccelerator::HookProc(int code, WPARAM wp, LPARAM lp) { const MSG& msg = *((MSG*)lp); if (code==HC_ACTION && wp==PM_REMOVE && msg.message==WM_CHAR && IsListBox(msg.hwnd)) { if (IsLBMultiSel(msg.hwnd)) { ::PostMessage(msg.hwnd, LB_SETSEL, TRUE, -1); } } return ::CallNextHookEx(g_lba->m_hHook, code, wp, lp); }

If you were paying close attention, you may have noticed CComboClickFix uses two kind of hooks, WH_MOUSE and WH_CALLWNDPROC; and CListBoxAccelerator uses yet another kind of hook, WH_GETMESSAGE. How do you know which hook to use? There's no simple answer. You have to know how Windows transmits each kind of message. I know from experience that Windows synthesizes WM_CHAR from WM_KEYDOWN and WM_KEYUP messages (::TranslateMessage is the function that does it), so I need a WH_GETMESSAGE hook.

WH_MOUSE works better for mouse messages. (I could've used WH_GETMESSAGE to trap WM_LBUTTONDOWN, but by the time it arrives via GetMessage, Windows has already converted the cursor to client coordinates and for CComboClickFix I wanted screen coordinates.) Until you become an experienced hookmaster, you may have to experiment with different kinds of hooks to find the right one. Fortunately, it's not hard to do since hook procs all have the same structure.

Happy programming!

Send your questions and comments for Paul to cppqa@microsoft.com.

Paul DiLascia is a freelance software consultant and Web/UI designer-at-large. He is the author of Windows++: Writing Reusable Windows Code in C++ (Addison-Wesley, 1992). In his spare time, Paul develops PixieLib, an MFC class library available from his Web site, www.dilascia.com.