C++ At Work
Implement Triple-Click, Subclass the Main Window
Paul DiLascia
Code download available at:CAtWork0604.exe(163 KB)
Q In many Windows-based apps, clicking the mouse moves the cursor and double-clicking selects the word containing the cursor. In Microsoft® Internet Explorer, if I triple-click the mouse, Internet Explorer selects the entire paragraph. I want to do this in my app, but while there's a WM_LBUTTONDBLCK, there's no WM_LBUTTONTRIPLECLICK. How can I implement triple-click?
Q In many Windows-based apps, clicking the mouse moves the cursor and double-clicking selects the word containing the cursor. In Microsoft® Internet Explorer, if I triple-click the mouse, Internet Explorer selects the entire paragraph. I want to do this in my app, but while there's a WM_LBUTTONDBLCK, there's no WM_LBUTTONTRIPLECLICK. How can I implement triple-click?
Tony Veteri
A It's not just Internet Explorer, it's also Microsoft Word and Outlook®, though Outlook is slightly different in that triple-click selects lines instead of paragraphs. You're right, there's no WM_LBUTTONTRIPLECLICK, but it's not hard to implement one yourself. After all, what's a triple click but three clicks in rapid succession? Or a double-click and single-click in rapid succession. All you have to know is how quickly do the clicks have to arrive to count as a triple-click? To find out, you can call the appropriately named ::GetDoubleClickTime, which returns the double-click time in milliseconds. So if you get a double-click and then a single-click within this many milliseconds, it counts as a triple-click.
A It's not just Internet Explorer, it's also Microsoft Word and Outlook®, though Outlook is slightly different in that triple-click selects lines instead of paragraphs. You're right, there's no WM_LBUTTONTRIPLECLICK, but it's not hard to implement one yourself. After all, what's a triple click but three clicks in rapid succession? Or a double-click and single-click in rapid succession. All you have to know is how quickly do the clicks have to arrive to count as a triple-click? To find out, you can call the appropriately named ::GetDoubleClickTime, which returns the double-click time in milliseconds. So if you get a double-click and then a single-click within this many milliseconds, it counts as a triple-click.
To implement triple-clicks, I wrote a class, CTripleClick, that lets you easily handle triple-clicks in any window. All you have to do is instantiate a CTripleClick object and call CTripleClick::Install with a pointer to your window and the message codes you want for left, middle, and right-button triple-click:
m_tripleClicker.Install(this, MYWM_LBTRIPLECLICK, MYWM_MBTRIPLECLICK, MYWM_RBTRIPLECLICK);
The last two message codes are optional (default=0, ignore). Once you Install your CTripleClick, it sends the appropriate message code to your window whenever the user triple-clicks one of the mouse buttons. I wrote a test program, Click3, to verify that CTripleClick works as claimed. Click3 is a simple Single Document Interface (SDI) text editor based on CEditView. Triple-clicking selects an entire paragraph of text. Figure 1 shows the view class for Click3. It has a handler, OnLbTripleClick, that handles an application-defined message, MYWM_LBTRIPLECLICK, by selecting the paragraph that contains the mouse. See CMyView::SelectPara for details of how to select a paragraph.
Figure 1 View
View.h
#pragma once #include "Doc.h" #include "TripleClick.h" ///////////////// // Standard Edit view. Handles triple-click: select entire paragraph. // class CMyView : public CEditView { public: virtual ~CMyView(); virtual void OnDraw(CDC* pDC); CMyDoc* GetDocument() { return (CMyDoc*)m_pDocument; } protected: CTripleClick m_tripleClicker; CMyView(); void SelectPara(CEdit& edit); DECLARE_DYNCREATE(CMyView) DECLARE_MESSAGE_MAP() afx_msg LRESULT OnLbTripleClick(WPARAM wp, LPARAM lp); afx_msg LRESULT OnMbTripleClick(WPARAM wp, LPARAM lp); afx_msg LRESULT OnRbTripleClick(WPARAM wp, LPARAM lp); afx_msg int OnCreate(LPCREATESTRUCT lpcs); };
View.cpp
#include "StdAfx.h" #include "View.h" #include "Click3.h" ... const UINT MYWM_LBTRIPLECLICK = WM_APP + 1; const UINT MYWM_MBTRIPLECLICK = WM_APP + 2; const UINT MYWM_RBTRIPLECLICK = WM_APP + 3; LPCTSTR FINDPARA = _T("\r\n\r\n"); //////////////////////////////////////////////////////////////// // CMyView // IMPLEMENT_DYNCREATE(CMyView, CEditView) BEGIN_MESSAGE_MAP(CMyView, CEditView) ON_WM_CREATE() ON_MESSAGE(MYWM_LBTRIPLECLICK, OnLbTripleClick) ON_MESSAGE(MYWM_MBTRIPLECLICK, OnMbTripleClick) ON_MESSAGE(MYWM_RBTRIPLECLICK, OnRbTripleClick) END_MESSAGE_MAP() CMyView::CMyView() {} CMyView::~CMyView() {} int CMyView::OnCreate(LPCREATESTRUCT lpcs) { // install triple-click handler m_tripleClicker.Install(this, MYWM_LBTRIPLECLICK, MYWM_MBTRIPLECLICK, MYWM_RBTRIPLECLICK); return CEditView::OnCreate(lpcs); } void CMyView::OnDraw(CDC* pDC) { CEditView::OnDraw(pDC); } LRESULT CMyView::OnLbTripleClick(WPARAM wp, LPARAM lp) { SelectPara(GetEditCtrl()); return 0; } LRESULT CMyView::OnMbTripleClick(WPARAM wp, LPARAM lp) { TRACE(_T("CMyView::OnMbTripleClick\n")); return 0; } LRESULT CMyView::OnRbTripleClick(WPARAM wp, LPARAM lp) { TRACE(_T("CMyView::OnRbTripleClick\n")); return 0; } ////////////////// // This fn selects the paragraph containing the current selection // void CMyView::SelectPara(CEdit& edit) { int begin, end; edit.GetSel(begin, end); // Get edit control memory. HLOCAL h = edit.GetHandle(); LPCTSTR text = (LPCTSTR)::LocalLock(h); // Search backward for paragraph break. Wimpy algorithm. for (; begin>0; begin--) { if (_tcsncmp(&text[begin],FINDPARA,4)==0) break; } // Search forward for paragraph break. Wimpy algorithm. int max = edit.GetWindowTextLength(); for (; end<max; end++) { if (_tcsncmp(&text[end],FINDPARA,4)==0) break; } ::LocalUnlock(h); // unlock buffer edit.SetSel(begin, end); // select paragraph }
The code in Figure 2 shows how CTripleClick works. CTripleClick is derived from CSubclassWnd, a class I use frequently in my columns. CSubclassWnd lets you handle messages sent to another window without deriving a new window (CWnd) class. CSubclassWnd subclasses the window by installing its own window procedure that calls CSubclassWnd::WindowProc. Derived classes override this virtual function to intercept messages sent to the target window; CTripleClick overrides it to intercept mouse messages. When CTripleClick gets a double-click (WM_LBUTTONDBLCLK, WM_MBUTTONDBLCLK, or WM_RBUTTONDBLCLK), it sets a flag and calls clock() to note the clock time.
Figure 2 CTripleClick
TripleClick.h
#pragma once #include "Subclass.h" ////////////////// // Triple-click handler. To use, instantiate in your window and // call Install: // m_tripleClick.Install(this, MY_MESSAGE); // where MY_MESSAGE is the message code to send when the user // triple-clicks the left mouse button. To handle other buttons // (middle, right), call with additional message codes. // class CTripleClick : public CSubclassWnd { protected: enum { LEFT=1, MIDDLE, RIGHT }; // codes for which button UINT m_uMsgs[4]; // callback message codes (1-offset) UINT m_uWhichButton; // which button clicked? UINT m_uTimeLastDblClk; // clock time of last double click UINT m_uClocksPerDblClk; // max clocks for double-click // Return enum/button code or FALSE if not a double-click. UINT IsDoubleClick(UINT msg) { return msg==WM_LBUTTONDBLCLK ? LEFT : msg==WM_MBUTTONDBLCLK ? MIDDLE : msg==WM_RBUTTONDBLCLK ? RIGHT : FALSE; } // Return enum/button code or FALSE if not a button-down. UINT IsButtonDown(UINT msg) { return msg==WM_LBUTTONDOWN ? LEFT : msg==WM_MBUTTONDOWN ? MIDDLE : msg==WM_RBUTTONDOWN ? RIGHT : FALSE; } public: CTripleClick() { } ~CTripleClick() { } BOOL Install(CWnd* pWnd, UINT uMsgLeft, UINT uMsgMid=0, UINT uMsgRight=0); virtual LRESULT WindowProc(UINT msg, WPARAM wp, LPARAM lp); };
TripleClick.cpp
#include "StdAfx.h" #include "StdAfx.h" #include "TripleClick.h" ... ////////////////// // Install triple-click handler. Hooks window using base CSubclassWnd. // BOOL CTripleClick::Install(CWnd* pWnd, UINT uMsgLeft, UINT uMsgMiddle, UINT uMsgRight) { m_uMsgs[LEFT] = uMsgLeft; m_uMsgs[MIDDLE] = uMsgMiddle; m_uMsgs[RIGHT] = uMsgRight; return HookWindow(pWnd); } ////////////////// // Window got a message: Look for double-click followed quickly by // button-down. // LRESULT CTripleClick::WindowProc(UINT msg, WPARAM wp, LPARAM lp) { if (IsDoubleClick(msg)) { m_uWhichButton = IsDoubleClick(msg); // save which button m_uTimeLastDblClk = clock(); // ..and current time // update double-click time. This is inefficient, but // expedient because I don't have to worry about WM_SETTINGCHANGE. m_uClocksPerDblClk = GetDoubleClickTime() * CLOCKS_PER_SEC / 1000; } else if (IsButtonDown(msg) && IsButtonDown(msg)==m_uWhichButton) { CSubclassWnd::WindowProc(msg, wp, lp); // do default operation if ((clock() - m_uTimeLastDblClk) < m_uClocksPerDblClk) { // if this is a triple-click, send callback message to client msg = m_uMsgs[m_uWhichButton]; // callback message to send m_uWhichButton = FALSE; // reset state return SendMessage(m_hWnd, msg, wp, lp); // send the message! } } return CSubclassWnd::WindowProc(msg, wp, lp); }
The clock time is the number of clock ticks since the process started. How many clock ticks does a clock-ticker tick? The #define symbol CLOCKS_PER_SEC tells the answer: 1000. That is, a thousand ticks per second, or a millisecond per tick. On my system, the double-click time is 480, or about half a second.
If CTripleClick sees one of the button-down messages (WM_LBUTTONDOWN, WM_MBUTTONDOWN or WM_RBUTTONDOWN), and the button is the same one that was double-clicked, CTripleClick compares the clock times of the two events. If the elapsed time is less than the double-click time as defined by ::GetDoubleClickTime, CTripleClick sends a triple-click message to its client's window, whichever message code the client supplied. For details, see Figure 2.
Figure 3** Mouse Control Panel **
Performance-conscious readers have already noticed that CTripleClick calls ::GetDoubleClickTime every time it gets a double-click message. This is somewhat inefficient since the double-click time rarely changes. I said rarely, because it can change. The user can use the Mouse control panel applet to change the double-click time (see Figure 3). That means you can't call ::GetDoubleClickTime to grab the double-click time when your app first starts, because the user can change it while your app is running. To get the current value, you must call ::GetDoubleClickTime when you need it. Alternatively, you can store the double-click time and handle WM_SETTINGCHANGE to update your stored value whenever it changes. Which leads me naturally to the next question.
Q I'm trying to write a component (child control) that displays some text using the same font as the current menu font. My application calls SystemParametersInfo to get the NONCLIENTMENTRICS with the menu font in lfMenuFont. Currently my application calls SystemParametersInfo to get the menu font whenever I need to display it. It would be more efficient to load the font once when my control is initialized, then handle WM_SETTINGCHANGE to reload it if the user changes the menu font. The problem is that Windows® only sends WM_SETTINGCHANGE to the top-level window; my window is a child window. I can make the client app call my control when the font changes, but I would prefer to update my font automatically, without making clients call a function. How can I handle WM_SETTINGCHANGE from my child control?
Q I'm trying to write a component (child control) that displays some text using the same font as the current menu font. My application calls SystemParametersInfo to get the NONCLIENTMENTRICS with the menu font in lfMenuFont. Currently my application calls SystemParametersInfo to get the menu font whenever I need to display it. It would be more efficient to load the font once when my control is initialized, then handle WM_SETTINGCHANGE to reload it if the user changes the menu font. The problem is that Windows® only sends WM_SETTINGCHANGE to the top-level window; my window is a child window. I can make the client app call my control when the font changes, but I would prefer to update my font automatically, without making clients call a function. How can I handle WM_SETTINGCHANGE from my child control?
Tom Ng
A You're right to look for a way to handle WM_SETTINGCHANGE on your own. Polite programmers always strive to make their code as self-contained and easy to use as possible—even for other programmers. But alas, Windows was designed in the bygone time of monolithic apps that exercised total control. In those days, it made sense to send messages like WM_SYSCOLORCHANGE and WM_FONTCHANGE to top-level windows— what else was there? The idea of separately installable controls and components hadn't been conceived yet or, if it had, was beyond the technical capacity of Windows. But today we dwell in a Tinkertoy world where programmers cobble apps from pluggable pieces. If the user changes the menu font or system colors, you want to update your control automatically—and don't bother the app about it, please!
A You're right to look for a way to handle WM_SETTINGCHANGE on your own. Polite programmers always strive to make their code as self-contained and easy to use as possible—even for other programmers. But alas, Windows was designed in the bygone time of monolithic apps that exercised total control. In those days, it made sense to send messages like WM_SYSCOLORCHANGE and WM_FONTCHANGE to top-level windows— what else was there? The idea of separately installable controls and components hadn't been conceived yet or, if it had, was beyond the technical capacity of Windows. But today we dwell in a Tinkertoy world where programmers cobble apps from pluggable pieces. If the user changes the menu font or system colors, you want to update your control automatically—and don't bother the app about it, please!
So how do you handle WM_SETTINGCHANGE in your control when this message goes to the main window? By subclassing, of course. The same CSubclassWnd class I described in the previous question works here, too. Just derive from CSubclassWnd and override the virtual WindowProc function to handle WM_SETTINGCHANGE. If you don't want to use CSubclassWnd, you can write your own class and window proc—but one way or another you have to subclass the main window in order to intercept WM_SETTINGCHANGE. Then you can examine WPARAM to see what changed. If it's the font, update your font. In practice, many programmers don't bother to check WPARAM, but simply update all stored parameters whenever any one of them changes:
case WM_SETTINGCHANGE: UpdateMySystemSettings(); break;
Of course, to subclass the main window you have to find it first. If you have a handle or CWnd pointer to the client window, you can call GetParent repeatedly until you get the top-level window, or use MFC's CWnd::GetTopLevelParent. If you don't have a client window, you can call AfxGetMainWnd. And if that fails too, you can use the technique I described in my July 2002 column (see C++ Q&A: Get the Main Window, Get EXE Name): call EnumWindows to iterate all the top-level windows, looking for one whose process ID matches the running process. A process can have more than one top-level window, but for the purpose of catching WM_SETTINGCHANGE, any one will do. Just make sure you don't find a modeless dialog that's transient (hint: check the window class name).
Since handling WM_SETTINGCHANGE is a useful thing to do, I implemented a little class, CSystemChange, that encapsulates the grungies. In my March 2006 column, I described how to implement an event mechanism for C++ (see C++ At Work: Event Programming, Part 2). CSystemChange is a singleton C++ class that catches WM_SETTINGCHANGE and several other main-window system-change messages, and translates them into C++ events any class can handle—even non-window classes. Just derive from ISystemChangeEvents, override the handler functions for the events you want to handle, and call CSystemChange::Register to register your target class:
// during object initialization theSystem.Register(this);
Just like MFC's theApp, theSystem is a global singleton CSystemChange object. It raises the events when the system changes. Actually, there are two theSystem symbols: CSystemChange::theSystem is a static data member of CSystemChange; ::theSystem is a global reference to CSystemChange::theSystem. In code, it looks like this:
CSystemChange CSystemChange::theSystem; CSystemChange& theSystem = CSystemChange::theSystem;
This syntactic sleight-of-hand lets me keep the CSystemChange constructor private and thus prevents anyone from creating another instance. There's no need to create another CSystemChange object, since one global is all you need for any client to register.
Figure 4 shows the event interface ISystemChangeEvents. It has an event SettingChange (OnSettingChange method) that's raised when Windows sends WM_SETTINGCHANGE, as well as events for other system-changed messages like WM_SYSCOLORCHANGE, WM_FONTCHANGE, and WM_DISPLAYCHANGE. You can handle these events by overriding the corresponding ISystemChangeEvents methods (for example, OnDisplayChange).
Figure 4 SysChange
SysChange.h
#pragma once #include "Subclass.h" #include "EventMgr.h" ///////////////// // Interface for system change events. To handle system-change events, // derive from this and override the handlers you want. // class ISystemChangeEvents { DECLARE_EVENTS(ISystemChangeEvents); public: DEFINE_EVENT2(ISystemChangeEvents, SettingChange, UINT, LPCTSTR); DEFINE_EVENT0(ISystemChangeEvents, SysColorChange); DEFINE_EVENT0(ISystemChangeEvents, FontChange); DEFINE_EVENT0(ISystemChangeEvents, TimeChange); DEFINE_EVENT2(ISystemChangeEvents, InputLangChange, UINT, LANGID); DEFINE_EVENT0(ISystemChangeEvents, UserChanged); DEFINE_EVENT2(ISystemChangeEvents, DisplayChange, UINT, CSize); DEFINE_EVENT2(ISystemChangeEvents, DeviceChange, UINT, DWORD_PTR); DEFINE_EVENT0(ISystemChangeEvents, ThemeChanged); }; IMPLEMENT_EVENTS(ISystemChangeEvents); ////////////////// // Singleton class to raise system change events. // class CSystemChange : public CSubclassWnd { protected: CEventMgr<ISystemChangeEvents> m_eventmgr; CSystemChange() { } ~CSystemChange() { } static HWND FindMainWindow(); public: static CSystemChange theSystem; BOOL Register(ISystemChangeEvents* client, CWnd* pWnd=NULL); BOOL Unregister(ISystemChangeEvents* client) { m_eventmgr.Unregister(client); } virtual LRESULT WindowProc(UINT msg, WPARAM wp, LPARAM lp); }; // Global, like theApp. By making it a reference, I can keep ctor // private and prevent anyone from creating an instance. extern CSystemChange& theSystem;
SysChange.cpp
#include "StdAfx.h" #include "SysChange.h" #include "WinList.h" ... // Global system object raises changed events: CSystemChange CSystemChange::theSystem; CSystemChange& theSystem = CSystemChange::theSystem; ////////////////// // Register object to receive system-change events. Optional window used // to find main top-level window. Otherwise will try to find it myself. // BOOL CSystemChange::Register(ISystemChangeEvents* client, CWnd* pWnd) { if (m_hWnd==NULL) { pWnd = pWnd ? pWnd->GetTopLevelParent() : AfxGetMainWnd(); HWND hwnd = pWnd->GetSafeHwnd(); if (hwnd==NULL) { hwnd = FindMainWindow(); } ASSERT(hwnd); // app has no main window?! VERIFY(HookWindow(hwnd)); } m_eventmgr.Register(client); return TRUE; } ////////////////// // Find (a) main window for current running process: look for top-level // window with same process ID. // HWND CSystemChange::FindMainWindow() { DWORD myProcessId = GetCurrentProcessId(); CWinList wins; for (WINLIST::iterator it=wins.begin(); it!=wins.end(); it++) { HWND hwnd = *it; DWORD pid; GetWindowThreadProcessId(hwnd, &pid); if (pid==myProcessId) { return hwnd; } } return NULL; } ////////////////// // Main window got a message: Look for system change messages. // LRESULT CSystemChange::WindowProc(UINT msg, WPARAM wp, LPARAM lp) { switch (msg) { case WM_SETTINGCHANGE: m_eventmgr.Raise(ISystemChangeEvents::SettingChange((UINT)wp, (LPCTSTR)lp)); break; case WM_SYSCOLORCHANGE: m_eventmgr.Raise(ISystemChangeEvents::SysColorChange()); break; . . // etc. . } return CSubclassWnd::WindowProc(msg, wp, lp); }
To show how it works in practice, I wrote a test program, SysMon, that monitors all the different system-changed events. SysMon uses a class, CTraceSysChange, that handles ISystemChangeEvents by displaying a message in a client-supplied window.
Figure 5 shows the messages displayed when I changed the theme on my computer. The "what" parameter for OnSettingChange is the SystemParametersInfo code for the system setting that changed; for example, code 13 (0x000D) corresponds to SPI_ICONHORIZONTALSPACING.
Figure 5** Displaying Messages **
Internally, CSystemChange holds a data member, CEventMgr<ISystemChangeEvents>, that uses the template class CEventMgr defined in EventMgr.h to instantiate an event manager for ISystemChangeEvents. (Read my March 2006 column for a description of CEventMgr.) CSystemChange::Register calls CEventMgr::Register to register the client with the event manager. Register also calls a helper function FindMainWindow to find the main window and then subclasses it. You can pass a pointer to a CWnd to use for finding the main window; otherwise, FindMainWindow looks for AfxGetMainWnd or a top-level window with the same process ID as the running process. Once CSystemChange subclasses the main window, CSystemChange::WindowProc handles the various setting-change messages by converting WPARAM and LPARAM to type-safe parameters and raising the corresponding event. For example:
// in CSystemChange::WindowProc switch (msg) { case WM_SETTINGCHANGE: m_eventmgr.Raise(ISystemChangeEvents::SettingChange((UINT)wp, (LPCTSTR)lp)); break;
You can use CSystemChange for all your system-changed event needs. Figure 6 shows the messages handled. For full details, see Figure 4 or download the source at the MSDN®Magazine Web site.
Figure 6 Messages Handled by CSystemChange
Message | Description |
---|---|
WM_SETTINGCHANGE | SystemParametersInfo or policy settings have changed. WPARAM and LPARAM tell what changed. |
WM_SYSCOLORCHANGE | System colors have changed—these are the colors used with GetSysColor. |
WM_FONTCHANGE | A font was added or removed from the system. |
WM_TIMECHANGE | The system time was changed. |
WM_INPUTLANGCHANGE | The input language was changed. WPARAM is the new character set and LPARAM the locale ID. |
WM_USERCHANGED | The user logged on or off. |
WM_DISPLAYCHANGE | The display resolution changed. WPARAM is the new image depth (bits per pixel) and LPARAM is the new screen resolution. |
WM_DEVICECHANGE | The hardware configuration changed. See documentation for details. |
WM_THEMECHANGED | The Windows XP theme changed. |
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.