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).
Create(NULL, "CScrollWnd",
WS_CHILD | WS_VISIBLE | WS_HSCROLL | WS_VSCROLL,
CRect(0,0,0,0), parentWnd, 0, NULL);
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:
class CScrollHelper
{
public:
CScrollHelper();
~CScrollHelper();
void AttachWnd(CWnd* pWnd);
void DetachWnd();
void SetDisplaySize(int displayWidth,
int displayHeight);
const CSize& GetDisplaySize() const;
const CSize& GetScrollPos() const;
const CSize& GetPageSize() const;
void ScrollToOrigin(bool scrollLeft,
bool scrollTop);
void OnHScroll(UINT nSBCode, UINT nPos,
CScrollBar* pScrollBar);
void OnVScroll(UINT nSBCode, UINT nPos,
CScrollBar* pScrollBar);
BOOL OnMouseWheel(UINT nFlags,
short zDelta, CPoint pt);
void OnSize(UINT nType, int cx, int cy);
};
To add scrolling support to your CDialog
-derived class (such as CScrollDlg
), we begin by adding a private
member to the dialog's class definition (include file):
class CScrollHelper;
class CScrollDlg : public CDialog
{
...
private:
CScrollHelper* m_scrollHelper;
};
Next, we add four message handlers to the class definition (include file) that are scrolling-related:
...
afx_msg void OnHScroll(UINT nSBCode,
UINT nPos, CScrollBar* pScrollBar);
afx_msg void OnVScroll(UINT nSBCode,
UINT nPos, CScrollBar* pScrollBar);
afx_msg BOOL OnMouseWheel(UINT nFlags,
short zDelta, CPoint pt);
afx_msg void OnSize(UINT nType, int cx,
int cy);
DECLARE_MESSAGE_MAP()
In the dialog source file, include the ScrollHelper.h file and also add the message map entries for the four message handlers:
#include "ScrollHelper.h"
...
BEGIN_MESSAGE_MAP(CScrollDlg, CDialog)
...
ON_WM_HSCROLL()
ON_WM_VSCROLL()
ON_WM_MOUSEWHEEL()
ON_WM_SIZE()
END_MESSAGE_MAP()
Then create an instance of the helper class in the dialog constructor and attach the dialog to the instance:
CScrollDlg::CScrollDlg(CWnd* pParent)
: CDialog(IDD_SCROLL_DLG, pParent)
{
m_scrollHelper = new CScrollHelper;
m_scrollHelper->AttachWnd(this);
}
Remember to delete the helper class instance in the dialog destructor:
CScrollDlg::~CScrollDlg()
{
delete m_scrollHelper;
}
Next, implement the four message handlers by simply delegating to the helper class, which has methods with the exact same signature as the message handlers:
void CScrollDlg::OnHScroll(UINT nSBCode,
UINT nPos, CScrollBar* pScrollBar)
{
m_scrollHelper->OnHScroll(nSBCode,
nPos, pScrollBar);
}
void CScrollDlg::OnVScroll(UINT nSBCode,
UINT nPos, CScrollBar* pScrollBar)
{
m_scrollHelper->OnVScroll(nSBCode,
nPos, pScrollBar);
}
BOOL CScrollDlg::OnMouseWheel(UINT nFlags,
short zDelta, CPoint pt)
{
BOOL wasScrolled =
m_scrollHelper->OnMouseWheel(nFlags,
zDelta, pt);
return wasScrolled;
}
void CScrollDlg::OnSize(UINT nType, int cx, int cy)
{
CDialog::OnSize(nType, cx, cy);
m_scrollHelper->OnSize(nType, cx, cy);
}
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)
{
m_scrollHelper = new CScrollHelper;
m_scrollHelper->AttachWnd(this);
m_scrollHelper->SetDisplaySize(701, 375);
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.
int CChildView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if ( CWnd::OnCreate(lpCreateStruct) == -1 )
return -1;
static int counter = 0;
if ( counter % 2 == 0 )
m_scrollWin = new CScrollWnd(this);
else
m_scrollWin = new CScrollDlg(this);
++counter;
return 0;
}
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
- July 6th, 2005
- Added
Get32BitScrollPos()
function as per MSDN article ID 152252, "How to Get 32-bit Scroll Position During Scroll Messages".
- Handles
SB_THUMBPOSITION
in OnHScroll
/OnVScroll
.
- Added VC++ 6.0 demo project. (Thanks to PJ Arends for the suggested fixes.)
- September 8th, 2005
- Added
GetClientRectSB()
helper function.
- Uses memory DC drawing in demo project (
CScrollWnd
class) to avoid flickering on resize.