This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.


December 1999

Microsoft Systems Journal Homepage

C++ Q&A

Code for this article: Dec99CQA.exe (274KB)

Paul DiLascia is the author of Windows ++: Writing Reusable Code in C++ (Addison-Wesley, 1992) and a freelance consultant and writer-at-large. He can be reached at askpd@pobox.com or http://pobox.com/~askpd.

Q How can I disable the scrolling of a virtual list control when the scroll box (thumb) is being dragged? I tried to handle WM_VSCROLL in my CListCtrl-derived class


 void CMyListCtrl::OnVScroll(UINT nSBCode, UINT nPos, 
                             CScrollBar* pScrollBar)
 {
   if (nSBCode == SB_THUMBTRACK) {
     return;
   }
   CListCtrl::OnVScroll(nSBCode, nPos, pScrollBar);
 }
but this disables thumb dragging completely! All I want is an old-fashioned behavior like in Windows® 3.1.
Pavel Chuchuva
Kazakhstan
A I'm so happy to meet other retro people from time to time. I mean, trying to emulate Windows 3.1—what a concept! You're on the right track, but as usual the details are a little sticky. Before I get into nuts and bolts, let me first warn you: implement live scrolling if you can because it's always better to give the user some feedback about where he's at. The behavior in Windows 3.1 was a concession to the pokey hardware of the day.
      That said, let's assume you've decided—either because your list control does some heavily hairy computations or you need to run on super slothful platforms—that you have to implement "deferred" scrolling. How to do it? Like I said, you were on the right track when you tried to disable SB_THUMBTRACK. But as you discovered, that disables scrolling altogether. Oops. The problem is, you don't want to ignore all SB_THUMBTRACK notifications; you want to disable all of them except the last one. How do you know which is the last SB_THUMBTRACK notification? It's like that joke where a man on the bus asks the woman sitting next to him, "Do you know how to get to Fooble Street?" and she replies, "Oh, that's easy. Get off one stop before I do."
      Fortunately (spirits up), Windows provides just the ticket for this situation: SB_THUMBPOSITION. It sends this notification when the user lets go of the scroll box. In other words, SB_THUMBPOSITION is the last SB_THUMBTRACK. There's just one glitch (spirits down): MFC doesn't handle it. CScrollView handles all the normal scroll notifications—SB_TOP/BOTTOM, SB_LINEUP/DOWN, SB_ PAGEUP/DOWN, and SB_THUMBTRACK—but not SB_ THUMBPOSITION. Whoever wrote the code probably thought, "I don't have to implement SB_THUMBPOSITION because I handle SB_THUMBTRACK." And, in 99 percent of the cases, that's a good assumption. No one ever anticipated atavistic programmers from Kazakhstan.
      No problem—in programming you can do anything. All you have to do in this case is handle SB_THUMBPOSITION. You could do it by mimicking the code in CScrollView, but that's too much work, especially when there's an opportunity to be more clever. Instead of doing the scrolling drudgework yourself, why not get MFC to do it?

 // in OnLVScroll handler
 if (nSBCode==SB_THUMBTRACK) {
   if (m_bIgnoreThumb)
     return 0; // ignore
 } else if nSBCode==SB_THUMBPOSITION) {
   m_bIgnoreThumb=FALSE; // don't ignore
   SendMessage(WM_VSCROLL, MAKEWPARAM(SB_THUMBTRACK, 
                                      nPos));
   m_bIgnoreThumb=TRUE; // keep ignoring
   return 0; // handled
 }
 // default: pass to MFC
 CScrollView::OnLVScroll(...);
      Assuming m_bIgnoreThumb is initialized to TRUE, this code ignores all SB_THUMBTRACK notifications, but when SB_THUMBPOSITION comes along, it temporarily turns off its ignore switch and sends SB_THUMBTRACK to itself. In other words, this code tricks MFC into handling SB_THUMBPOSITION by converting it to SB_THUMBTRACK. Pretty sneaky, eh?
Figure 1 ListView Scroll
      Figure 1 ListView Scroll
      Since that was too easy, I decided to add another feature to this deferred scrolling business: a scroll tip that pops up during deferred scrolling to at least give the user some feedback (see Figure 1). Microsoft® Excel and Word have the same feature, so you can easily scroll to a particular spreadsheet cell or page. The text looks like a tooltip, but I implemented it as a completely separate standalone class, CPopupText, because I'm too stupid to figure out how tooltips work (I can never get it right on the first try—I always have to futz around). Since the scroll tip doesn't time out the way a tooltip does, it remains displayed until the user lets go of the mouse. Figure 2 shows the code, which is entirely straightforward, so I won't bore you with details.
      To make life as easy as possible for you, I encapsulated the scrolling action together with the popup scroll tip in a class, CDeferScrollHook (see Figure 3). All you have to do is instantiate one of these gizmos in your scroll view class and call Install. Presto—instant retro scrolling behavior. CDeferScrollHook is derived from my ubiquitous CSubclassWnd class, which lets you subclass (in the Windows sense) any CWnd. It intercepts WM_VSCROLL messages to perform the required magic shown previously.
      The vanilla CDeferScrollHook has no scroll tips. To add them, you have to derive your own class and implement the virtual function OnGetScrollTipText.

 BOOL CMyDeferScrollHook::
   OnGetScrollTipText(CString& s, UINT nPos)
 {
   s.Format(_T("Item #%d"), nPos);
   return TRUE; // handled
 }
To show how it all works in practice, I wrote a little app called LVScroll (which is shorthand for ListView Scroll). It has a main window (see Figure 1) with a typical scrolling view derived from CScrollView. It also has a dialog (see Figure 4) with a CListCtrl. Both windows use CDeferScrollHook to implement deferred scrolling. The view (CMyView) has a companion class (CMyViewDeferScrollHook) with an OnGetScrollTipText function that displays the line number.

 BOOL CMyViewDeferScrollHook::
   OnGetScrollTipText(CString& s, 
                      UINT nPos)
 {
   s.Format(_T("Line %d"),
            nPos/m_cyLine);
   return TRUE; // handled
 }
      CMyView::OnInitialUpdate initializes m_cyLine to the height of one line of text, which it gets by calling DrawText with DT_CALCRECT. The list control dialog (CListCtrlDialog) has a similar class, CMyListCtrlDeferScrollHook, that uses the raw scroll position as an item number. The view installs the hook in OnInitialUpdate; the dialog does it in OnInitDialog.

 BOOL CListCtrlDialog::OnInitDialog()
 {
   // subclass list contrl
   m_lcData.SubclassDlgItem(IDC_LIST1, this);
   
      // (add items)
 .
 .
 .
   // install hook
   m_deferScrollHook.Install(&lc);
   return TRUE;
 }
      In summary: to add deferred scrolling to your app, derive from CDeferScrollHook, implement OnGetScrollTipText, instantiate your hook in your window class, and call Install. It's all totally straightforward. If you want to disable deferred scrolling, simply call CDeferScrollHook::Unhook to unhook the hook.
Figure 4 Deferred Scrolling
      Figure 4 Deferred Scrolling

      As you can see in Figure 4, LVScroll has a checkbox that lets the user turn deferred scrolling on or off. The on/off feature is implemented trivially in the command handler for the checkbox.


 void CListCtrlDialog::OnToggleDefer()
 {
   m_butnDefer.GetCheck() ? 
     m_deferScrollHook.Install(&m_lcData)
   : m_deferScrollHook.Unhook();
 }
      If you decide to implement deferred scrolling for your app, I encourage you to likewise make it a user-selectable feature. Whether to do real-time or deferred scrolling is purely a performance matter, and hardware evolution continues to follow the Moore curve. What runs slowly today might zip tomorrow. If you make deferred scrolling an option, your customers will love you when they upgrade. You don't have to tell them you got the code from MSJ.

Q I'm developing a specialized tree control that does custom painting using NM_CUSTOMDRAW. The painting works fine, but my control needs to initialize some fonts and other structures. At first I did the initialization in my OnCreate handler, which worked fine, but now someone at work wants to use my control in a dialog. They call SubclassDlgItem to subclass my control, and my OnCreate handler is never called because the window is already created. How can I initialize my control so it will work for either creating the control or subclassing it? Right now I'm using a m_bInitialized flag, which I check in every function. It works, but it seems inefficient.

Marcel Didier
Paris, France
A I love questions like this that have short, one-line answers. You're right, the set-a-flag thing is kludgy. So put your initialization code in the little-known virtual function CWnd::PreSubclassWindow. MFC calls this function when the window is created or when it's subclassed. Problem solved. Next, please.

Have a question about programming in C or C++? Send it to Paul DiLascia at askpd@pobox.com


From the December 1999 issue of Microsoft Systems Journal.