Code for this article: Wicked0697.exe (152KB)
Jeff Prosise is the author of Programming Windows 95 with MFC (Microsoft Press, 1996). He also teaches Visual C++®/MFC programming seminars. For more information, visit
Lately I've been suffering from a bad case of the
envies. Not over a car, a hot new laptop, or some-
one else's wife, but over a feature I've seen in a piece of software. A feature so cooland, in retrospect, so obviousthat I only wish I had thought of it first.
The feature I'm referring to is found in Microsoft® Internet Explorer 3.0 (IE 3.0). The first time I saw how smoothly IE 3.0 scrolls the contents of its window, I knew I had to borrow the idea and use it myself. If you don't know what I'm talking about, open a lengthy HTML document in IE 3.0 and click the vertical scroll bar below the thumb. Rather than jump down a page, IE 3.0 will quickly (but smoothly) scroll the length of one page as if the down arrow had been clicked several times in rapid succession. This feature is
so visually appealing that I can easily imagine it becoming a standard part of the Windows® UI. It's already showing up in other Microsoft applications, and as far as I can tell it's a smash hit with users.
How hard is it to mimic this feature in your own applications? Not hard at all, especially if you're comfortable programming with MFC. All you have to do is derive a class from CScrollView and override one virtual function with an implementation that I'll provide for you. The only other requirement is to optimize your view's OnDraw function so that repainting is done as quickly and efficiently as possible. I'll show you how to do that as well, in case you
haven't done it already. For most applications, smooth scrolling can be added in less than a day. If your OnDraw code is already optimized, your scrolling can be silky smooth in less than an hour.
The SMOOTH Application
The SMOOTH application shown in Figure 1 is an MFC 4.x (and tested under MFC 5.0) app that scrolls like IE 3.0. The scroll bars allow you to move around within a virtual workspace that measures 1,024 X 1,204 pixels. In the center of the workspace lies a string of text. The background is a textured bitmap that's tiled horizontally and vertically to fill the workspace. Clicking a scroll bar smoothly scrolls the view one "page" up, down, left, or right. Clicking a scroll bar arrow smoothly scrolls the view one "line." A page is defined to be 9/10 of the visible viewing area, and a line is 1/10 of the view's width or height.
|Figure 1: SMOOTH|
|You can get the source code for SMOOTH from the link at the top of this article. The source code for SMOOTH's CScrollView-derived view class, which I named CSmoothView, is reproduced in Figure 2.
Much of the source code is pretty standard stuff. An OnSize handler keys the page size and line size to the view size, for example, and OnInitialUpdate initializes the view's scrolling parameters. Palette-handling code is included to make the background bitmap look good even on 256-color screens. What's special about CSmoothView is its override of CScrollView's OnScroll function. Peek inside the source code for MFC's CScrollView class in Viewscrl.cpp and you'll find that it provides handlers for WM_HSCROLL and WM_VSCROLL messages. The handlers do little more than call OnScroll, which computes the distance that the view will be scrolled in the x or y direction, and then calls another CScrollView function named OnScrollBy. OnScrollBy does the actual scrolling by calling ScrollWindow to move the window contents and SetScrollPos to reposition the scroll bar thumb.
OnScroll is virtual and OnScrollBy is not, so the former is the ideal place to tap into the framework and modify the way a CScrollView behaves. CSmoothView::OnScroll implements smooth scrolling by playing a little trick on MFC. Rather than calling OnScrollBy once to scroll an entire page or line, it subdivides the page size or line size into smaller, more granular chunks and calls OnScrollBy several times. A CScrollView stores its page size and line size (the values passed in SetScrollSizes' third and fourth parameters, respectively) in a pair of protected CSize data members named m_pageDev and m_lineDev. CSmoothView adds a pair of member variables named m_nPageSlices and m_nLineSlices that store the number of discrete chunks each page or line is divided into.
If m_pageDev.cy is 100, m_nPageSlices is 12, and the user pages down with the scroll bar, CSmoothView::OnScroll temporarily sets m_pageDev.cy to 8 and calls OnScrollBy 12 times. To complete the scrolling operation, it then sets m_pageDev.cy to 4 and calls OnScrollBy a final time. The window is scrolled 100 pixels, but the scrolling is much smoother than it would have been otherwise. The application is none the wiser, other than the fact that its OnDraw function got called 13 times instead of once. That's why it's critical that OnDraw be optimized to do as little painting as it can get away with. Unnecessary GDI calls can slow the scrolling process dramatically, even if the bulk of the output is clipped.
The key to optimizing OnDraw performance (and by extension, smooth scrolling performance) is to use the CDC::GetClipBox function. Called from a view's OnDraw function, GetClipBox initializes a rectangle with logical coordinates describing what part of the view needs repainting. Pixels outside the rectangle are clipped by GDI, so drawing anywhere but the interior of the rectangle wastes CPU cycles. How you use GetClipBox to optimize drawing performance is highly application-specific. SMOOTH uses the clip box to minimize the number of times it calls CDC::BitBlt to tile the background bitmap. Its performance could be optimized even further by comparing a rectangle circumscribing the output text to the rectangle initialized by GetClipBox and skipping the DrawText calls if the rectangles don't overlap.
CSmoothView's constructor sets m_nPageSlices and m_nLineSlices to 12 and 4, respectively. I chose these values empirically, but feel free to experiment with different settings if you borrow my OnScroll code. Another CSmoothView member variable named m_dwMinTime specifies the minimum amount of time, in milliseconds, between successive calls to OnScrollBy. I set it to 10 milliseconds, mainly because testing showed that a typical call to OnScrollBy on my PC (a 200MHz Pentium Pro with a moderately fast video subsystem) took 12 to 15 milliseconds. Reasoning that video performance will soon be 10 times what it is today, I felt it was prudent to build in a guaranteed minimum so that smooth scrolling won't happen too quickly for the eye to see.
How do you put smooth scrolling to work in your own code? Easy. First override OnScroll in your CScrollView-derived class. You can copy my OnScroll function and use it as is. Second, add m_nPageSlices, m_nLineSlices, and m_dwMinTime data members to the derived class and initialize them in the class constructor. Finally, use GetClipBox to optimize your OnDraw function if you haven't already. You'll be pleased with the results. Your users will be, too.
Your Needs, Your Ideas
Are there tough Win32® programming questions you'd like to see answered in this column? If so, email them to me at the address listed below. I regret that time doesn't permit me to respond individually to all questions, but rest assured that I'll read every one and consider each for inclusion in a future installment of Wicked Code.
Have a tricky issue dealing with Windows? Send your questions via email to Jeff Prosise: email@example.com
© 1997 Microsoft Corporation. All rights reserved. Legal Notices.