Introduction

Implementing standard scrolling behavior for a custom CWnd or CDialog-derived class using MFC is fairly non-trivial. There are at least 3 or 4 message handlers to write as well as a number of scrolling parameters for each scrollbar that need to be adjusted whenever your window or dialog is resized. In this article, I present a C++ helper class that can be used to add both horizontal and/or vertical scrolling to any CWnd or CDialog class. Unlike other implementations you may have seen, there is no scrollable dialog base class that you need to derive from. No change in your inheritance hierarchy is required to use the helper class (CScrollHelper).

Background

Before going into the details regarding how to use the helper class, I want to cite a couple of references which explain the basics of implementing scrolling very well. The first reference is MSDN article ID 262954, "How to create a resizable dialog box with scroll bars in Visual C++". This article gives an example of how to add vertical scrollbar support to a CDialog class. The second reference is from the book, "Programming Windows with MFC, 2nd Edition" by Jeff Prosise (Microsoft Press). Chapter 2 of this book explains the details of implementing scrolling in a CWnd-derived class and also gives example code for a simple spreadsheet application that supports both horizontal and vertical scrolling.

For a CWnd-derived class, the first step in implementing scrolling is to make sure the window is created with the window styles, WS_HSCROLL (if you want a horizontal scrollbar) and/or WS_VSCROLL (if you want a vertical, right-attached scrollbar).

In the case of a dialog, window styles are typically set through the resource editor. In VS 2003, I usually make sure the following properties are set for any dialog that I want to be scrollable:

Border = "Resizing" if you have a popup dialog. If your dialog is a child window embedded within a container parent, you can choose another style such as "None" (the Demo project has an example of this).

Clip Children = "True". This setting can help to minimize display flickering as the dialog is being resized.

Horizontal Scrollbar = "True". This is equivalent to adding the window style, WS_HSCROLL.

Style = "Child" if your dialog is a child window embedded within a container parent.

Vertical Scrollbar = "True". This is equivalent to adding the window style, WS_VSCROLL.

Visible = "True". Visual Studio defaults to "False" in some cases, so you want to check this setting.

Using the code

The CScrollHelper class is implemented in two source files: ScrollHelper.h and ScrollHelper.cpp. The public interface of the class is shown below:

One final step to completing scrolling support is to set the "display size" in the helper class. This represents the virtual display area of your dialog and is a value that you can predetermine and fix in the code itself, or compute dynamically during run-time. If you are familiar with Windows Forms programming in C#, the concept of display size is similar to the DisplayRectangle property in scrollable controls. The display size represents the precise threshold or point where scrollbars either appear or disappear. For example, if the user resizes the dialog until it is smaller than the display size, then scrollbars will appear, allowing you to scroll the dialog to access the entire virtual display surface. If the user resizes the dialog larger than the display size, then the scrollbars will disappear. The display size is typically a value that is fixed after you set it the first time:

CScrollDlg::CScrollDlg(CWnd* pParent)
: CDialog(IDD_SCROLL_DLG, pParent)
{
// Create the scroll helper and
// attach it to this dialog.
m_scrollHelper = new CScrollHelper;
m_scrollHelper->AttachWnd(this);
// We also set the display size
// equal to the dialog size.
// This is the size of the dialog
// in pixels as laid out
// in the resource editor.
m_scrollHelper->SetDisplaySize(701, 375);
// Create the dialog.
Create(IDD_SCROLL_DLG, pParent);
}

In the interface of the CScrollHelper class, you may have noticed the GetPageSize() method. The page size is another important scrolling parameter that is managed internally by the helper class. Basically, the page size is the same as your dialog size (or more precisely, the size of the client area of your dialog). This is similar to the ClientRectangle property in Win Forms. The ratio of the page size to the display size is used by Windows to determine how big the "thumb" portion of the scrollbar should be. The size of the thumb gives you an indication of how much of the virtual display surface you are viewing (and the position of the thumb tells you what part of the virtual display surface you are looking at). For example, let's say you are viewing a very long document in Microsoft Word (100 pages). You will see that the thumb of the vertical scrollbar is very small. On the other hand, if you are editing a document that is only a bit longer than a single page, you will see that the thumb on the scrollbar is close to the maximum size it can have.

The TestScroll application

The demo project (TestScroll) illustrates the use of the helper class. It's a MDI application that I created from scratch using Visual Studio. To generate the project, I defaulted all of the VS wizard options except for the Document/View support checkbox, which I unchecked. I then wrote two new classes, CScrollDlg and CScrollWnd. CScrollDlg is a dialog class that uses the helper class to implement scrolling (as shown in the code sections above). It simply displays four buttons, one at each corner of the virtual display surface, along with a CListBox that shows the current scrolling parameters as the dialog is being resized. CScrollWnd is a custom CWnd-derived class that shows you how to add scrolling support to non-dialog classes. What's interesting about the implementation here is that this class paints a rectangle representing the fixed display size. So as you resize the window, you can see precisely when a scrollbar will appear or disappear.

The generated MDI application provides a class called CChildView, which is contained within the MDI child frame window. CChildView is really the starting point of integration with my two new classes above. Instead of CChildView providing its own content, I modified it to either create a CScrollDlg or a CScrollWnd instance that covers its entire client area.

To test the demo application, just use the File | New menu item to open MDI child Windows. The first time you will get a CScrollWnd instance. The second time, a CScrollDlg will be created. Each time you select "New", it will alternate between the two types of examples.

The snapshot below shows scrolling in a CWnd-derived class. The top MDI child window shows scrollbars since the window size is smaller than the display size. The bottom MDI child has no scrollbars and you can clearly see that the window size is larger than the fixed display size (represented by the blue rectangle). Also, in the bottom MDI child, notice that the page size is reported as 0 x 0. This is how the helper class is able to hide the scrollbars - by setting internal scrolling parameters such as scroll position and page size to zero values.

The snapshot below shows an example of scrolling in a CDialog. Notice that the caption bar of the bottom MDI child window displays the current scroll position, and that the dialog is scrolled all the way to the bottom-right. When scrolled to the maximum position, an interesting thing to note is that the scroll position (222, 230) added to the page size (479, 145) equals the display size (701, 375).

To summarize, the CScrollHelper class makes it easy to add scrolling support to your CWnd or CDialog classes, as it takes care of implementing all of the necessary message handlers. The key to using the helper class is to be able to set the display size properly. In the case of CWnd classes, you may also need to look further into GDI mapping modes and conversions between logical and device coordinates.

History

July 5th, 2005

Initial revision.

July 6th, 2005

Added Get32BitScrollPos() function as per MSDN article ID 152252, "How to Get 32-bit Scroll Position During Scroll Messages".

Thanks for the great article, but I've encountered problem with the Spin Control.Rotating the mouse wheel inside the Edit control connected to the Spin control scrolls the whole window to the top, which shouldn't. It should change the numbers inside the Edit control but not scroll the whole dialog window itself.To duplicate the problem, just try to place on scrollable dialog's very bottom area the Edit control and cosequtively Spin control, and link them together to represent an scrollable integer.After shrinking the dialog and scrolling dialog down to the Edit-Spin control, place cursor inside Edit control and scroll the mouse wheel.

It would be nice to have a member function GetMinMaxInfo to use in the client's OnGetMinMaxInfo handler to prevent the window from being resized past the PageSize set in the scrollbar helper. GetPageSize does not take into account the scollbars and therefore cannot be used directly.

I created my CWnd-Derived class, if I created CScrollHelper inside constructor or overrode CMyWnd::Create(), it worked perfectly.However, when I created CScrollHelper sometime after my window already got displayed, the scrollbar never showed up.I'm sure that displaySize was properly set.

I find that even if I create my cwnd without the styles WS_VERT or WS_HORZ that they still appear. I can only get them to hide by setting the DisplaySize to 0. This means I've had to update SetDisplaySize to the following:

I'm trying to add in square screen support to my WinMobile application.The easy & logical way to do this is to add scrollbars to my portrait-mode dialog.

For the record, I've already implemented all the handlers, and the scrollbars all update properly, even when switching back and forth from portrait to landscape, and opening and closing the Sip panel.

How & where would I add code to detect that the screen height is smaller than the currently loaded dialog height? InitDialog? OnSize? I've tried a few things, but I managed to mangle the scrollbars for the Portrait & Landscape modes. Any ideas would be appreciated

// Get the width/height of the attached wnd that includes the area // covered by the scrollbars (if any). The reason we need this is // because when scrollbars are present, both cx/cy and GetClientRect() // when accessed from OnSize() do not include the scrollbar covered // areas. In other words, their values are smaller than what you would // expect. CRect rect; GetClientRectSB(m_attachWnd, m_displaySize, rect);

I found some strange behavior together with spin controls.I checked it also with your demo program and got the same problem.

You just have to add a spin control in the demo dialog and try the following:

In DEBUG mode: Everything is fine

In RELEASE mode: I just press the spin button and the complete dialog content disappears. In my application it moves nearly complete to the left, so just the most right part of my dialog remains in the window.

But it's just in the RELEASE mode. Therefore I'm not sure if it's a real bug or if it's due to some weird property settings in my VS 2005.

Hi,I added a spin control to the dialog and resized the window smaller so that scrollbars appeared. Then I scrolled to the bottom-right corner of the dialog and pressed one of the spin control buttons. When I debugged this, I found pressing the spin control causes a VSCROLL message to be sent to the dialog. You can put a breakpoint in the dialog's OnVScroll handler to see this. I'm guessing this is what you are seeing. It happens in both release and debug for me (no difference) in VS 2008. As a simple way to avoid this I think in your dialog OnVScroll handler, just add a first statement like: if ( pScrollBar != NULL ) return;

I want to contribute a little: I had a problem because a wanted to embed a child dialog in a CView derived class, but my child dialog always showed its vertical scroll bar as though it was sized-down although it wasn't. The problem was that I was passing the size of my dialog obtained by calling GetClientRect method on my dialog class to the SetDialogSize method of your helper instead of GetWindowRect.

Hi,If I understand correctly, you have a dialog-based application in which you have enabled a status bar. Since the status bar is a child of the dialog, and the scrollbars are enabled for the dialog, I don't think you can avoid having them overlap. This doesn't really have anything to do with the helper class. The helper class merely assists you in updating the scroll information - it is just a wrapper around existing scrollbar functions etc. The helper class does not deal with actual layout/positioning of the scrollbar as this is something internal to the window/dialog.

To get the effect you want, one possibility might be to use two dialogs: your existing dialog with status bar, and a second dialog (childframe) which has scrollbars enabled and contains all of your controls, etc. This second dialog is created as child of the first, covering the client area minus the status bar.

I have noticed (in two applications) that when I use ScrollHelper, I have a (big!) problem when I try to move the cursor of a progress bar (that uses CProgressBar).

When I click on the left or on the right of the cursor, there is no problem, the cursor moves.

But if I click on the cursor, and try to move it before releasing the mouse button, then the application is scrolled horizontally and all the controls disappear so that I don't have any other choice than closing the window.

Hi,I'm not exactly sure what a CProgressBar is...but maybe you could, as a test, add your progress bar control to the dialog in the Demo app for scroll helper. And then try debug versus release to see if you see the same issue there. This could help to isolate where the problem may be.

Nschan, thanks! This is a nice helper class. I took it a step further and encapsulated it into a control you can plop into a dialog and draw into. It uses a CScrollHelper to manage the scrollbars, and has its own zooming capabilities, so can be used to display large pictures.

Thx Jim,Good article...I tried out your demo code and it works well. I did notice one thing though with a scrollbar being hidden temporarily while the other one was being scrolled. I was using VS 2008 on XP, not sure if that makes any difference.

JD's project modifies CScrollHelper: it adds a DoTheScroll wrapper around CWnd::ScrollWindow, which uses a clip rectangle (m_pageSize) that includes the scrollbars. I fixed this in my copy of it by using GetClientRect instead. I just realized that ScrollWindow does that by default if you don't specify a clip rect.

Thanks to both of you for a great starting point for a custom control. I've added functions like ScrollBy(int,int), since sometimes I want to scroll a certain part of my diagram on screen... I should post the code somewhere...

Hi,When scrollbars are created by adding the WS_HSCROLL/WS_VSCROLL styles to a CWnd, I believe those scrollbars are managed internally by the window, and you can't access it directly (e.g., can't get the CScrollBar pointer).

If you only want to change the position of the scrollbar thumb, use CWnd::SetScrollPos(). And use CWnd::ScrollWindow() to actually scroll the window.

Hi, I tried changing the test code to add scroll bars to the main window (CMainFrame).However, I have the following problems: - The tool bar and the bottom bar are not anchored and get scrolled.- The border area that is being scrolled isn’t repainted correctly

Hi,I think you don't want to scroll the mainframe window itself, because its area includes the toolbars and status bar. It sounds like you want to scroll just the MDI client area, like you want to implement scrolling MDI child windows as in .NET Windows Forms. I've never tried this myself but I think you might be able to do this by subclassing the MDI client window (write a CWnd class that subclasses the mdiclient and handles its messages). There's another codeproject article which might be useful:

This article shows how to subclass the MDI client window in order to implement tabs in the MDI client area. Obviously, this is not to do with scrolling, but the techniques might be useful for what you want to do.

I need to put up a verticle scroll bar pn a static control box,I tried with the code myStaticBox.Create("some huge text",WS_VISIBLE|WS_CHILD|WS_VSCROLL|WS_BORDER ,myRect,::AfxGetMainWnd(),AFX_IDW_FIRST);Scroll bar is visible but its disabled. not moving.How to go ahead with it?please suggest.......

I am also facing similar problem. I am using a picture control (basically a static control) with notify set to true . I have connected the control to a CStatic variable (m_ctlPic) and set it to show the scroll bars:

Testing the CScrollHelper class on Windows Mobile 5 - I found the window was not bing moved - thought the scroll bar acted as if it was. I replaced the ScrollWindow call(s): in the OnHScroll and OnVScroll methods.

First, thanks for this article. I want to use your class in my SDI project. What I want is a SDI view interface with some child dialogs in it. I added your class to my view class. When I scroll, the child dialogs do get scrolled together with the view, but they become unaccessible from the mouse. Could you help me to solve this problem?

Hi,I created a simple SDI app (either with or without doc-view) to try to reproduce this problem but I couldn't. In my app, I create a couple of instances of dialogs (with style set to Child and border set to None) as children of the view class. The view class uses the scrollhelper and handles the various scroll-related messages. If possible, perhaps you could do something similar and create a smaller project that still shows the problem but would allow you to post relevant pieces of code that we could look at.