[Previous] [Next]

Scroll Views

CScrollView adds basic scrolling capabilities to CView. It includes handlers for WM_VSCROLL and WM_HSCROLL messages that allow MFC to do the bulk of the work involved in scrolling a window in response to scroll bar messages. It also includes member functions that you can call to perform fundamental tasks such as scrolling to a specified position and retrieving the current scroll position. Because CScrollView handles scrolling entirely on its own, you have to do very little to make it work other than implement OnDraw. You can usually implement OnDraw in a CScrollView exactly as you do in a CView. Unless you want to tweak it to optimize scrolling performance, OnDraw requires little or no special logic to support scrolling.

CScrollView Basics

Using CScrollView to create a scrolling view is simplicity itself. Here are the three basic steps. The term physical view refers to the view window and the space that it occupies on the screen, and logical view describes the virtual workspace that can be viewed by using the scroll bars:

  1. Derive your application's view class from CScrollView. If you use AppWizard to create the project, you can select CScrollView from the list of base classes presented in AppWizard's Step 6 dialog box, as shown in Figure 10-1.
  2. Click to view at full size.

    Figure 10-1. Using AppWizard to create a CScrollView-based application.

  3. Override OnInitialUpdate in the view class, and call SetScrollSizes to specify the view's logical dimensions. This is your means of telling MFC how large an area the scrollable view should cover. If you use AppWizard to create the project and choose CScrollView in the Step 6 dialog box, AppWizard overrides OnInitialUpdate for you and inserts a call to SetScrollSizes that sets the view's logical width and height to 100 pixels.
  4. Implement OnDraw as if the view were a conventional CView.

A scroll view created in this manner automatically scrolls in response to scroll bar events. It automatically factors the scroll position into the output from OnDraw. It also hides its scroll bars if the physical view size exceeds the logical view size and sizes the scroll bar thumbs to reflect the relative proportions of the physical and logical views when the scroll bars are visible.

CScrollView::SetScrollSizes accepts four parameters, two of which are optional. In order, here are those parameters:

If you omit either or both of the final two parameters, MFC uses sensible defaults for the page size and the line size. Here's an OnInitialUpdate function that sets the logical view size to 1,280 pixels wide and 1,024 pixels high:

void CMyView::OnInitialUpdate ()
{
    CScrollView::OnInitialUpdate ();
    SetScrollSizes (MM_TEXT, CSize (1280, 1024));
}

And here's one that sets the view's dimensions to those of an 8½-by-11-inch page:

void CMyView::OnInitialUpdate ()
{
    CScrollView::OnInitialUpdate ();
    SetScrollSizes (MM_LOENGLISH, CSize (850, 1100));
}

The next one does the same as the last one, but it also programs the view to scroll 2 inches in response to SB_PAGEUP/DOWN/LEFT/RIGHT events and ¼ inch in response to SB_LINEUP/DOWN/LEFT/RIGHT events:

void CMyView::OnInitialUpdate ()
{
    CScrollView::OnInitialUpdate ();
    SetScrollSizes (MM_LOENGLISH, CSize (850, 1100),
        CSize (200, 200), CSize (25, 25));
}

The mapping mode specified in SetScrollSizes' first parameter determines the units of measurement for the second, third, and fourth parameters. You can specify any mapping mode except MM_ISOTROPIC and MM_ANISOTROPIC. When OnDraw is called, the mapping mode has already been set to the one specified in the call to SetScrollSizes. Therefore, you needn't call SetMapMode yourself when you implement OnDraw.

Is that all there is to creating a scrolling view? Almost. You should remember two basic principles when using a CScrollView:

A bit of background on how a CScrollView works will clarify why these principles are important—and why an ordinary OnDraw function that knows nothing about scrolling magically adjusts its output to match the current scroll position when it's part of a CScrollView.

When a scroll event occurs, CScrollView captures the ensuing message with its OnVScroll or OnHScroll message handler and calls ::ScrollWindow to scroll the view horizontally or vertically. Soon after, the view's OnPaint function is called to paint the portion of the window that was invalidated by ::ScrollWindow. Here's the OnPaint handler that CScrollView inherits from CView:

CPaintDC dc(this);
OnPrepareDC(&dc);
OnDraw(&dc);

Before it calls OnDraw, CView::OnPaint calls the virtual OnPrepareDC function. CScrollView overrides OnPrepareDC and in it calls CDC::SetMapMode to set the mapping mode and CDC::SetViewportOrg to translate the viewport origin an amount that equals the horizontal and vertical scroll positions. Consequently, the scroll positions are automatically factored in when OnDraw repaints the view. Thanks to CScrollView::OnPrepareDC, a generic OnDraw function ported from a CView to a CScrollView automatically adapts to changes in the scroll position.

Now think about what happens if you instantiate a device context class on your own, outside the view's OnDraw function, and draw something in a CScrollView. Unless you first call OnPrepareDC to prepare the device context as OnPaint does, SetViewportOrg won't get called and drawing will be performed relative to the upper left corner of the physical view rather than to the upper left corner of the logical view. Views of a document get out of kilter pretty quickly if they're drawn using two different coordinate systems. Therefore, when you draw in a CScrollView window outside of OnDraw like this:

CClientDC dc (this);
// Draw something with dc.

Make it a habit to pass the device context to OnPrepareDC first, like this:

CClientDC dc (this);
OnPrepareDC (&dc);
// Draw something with dc.

By the same token, if you have the coordinates of a point in a CScrollView in device coordinates and want to find the corresponding position in the logical view, use CDC::DPtoLP to convert the device coordinates to logical coordinates. Call OnPrepareDC first to set the mapping mode and factor in the scroll position. Here's a WM_LBUTTONDOWN handler that performs a simple hit-test to determine whether the click point lies in the upper or lower half of the logical view:

void CMyView::OnLButtonDown (UINT nFlags, CPoint point)
{
    CPoint pos = point;
    CClientDC dc (this);
    OnPrepareDC (&dc);
    dc.DPtoLP (&pos);

    CSize size = GetTotalSize ();
    if (::abs (pos.y) < (size.cy / 2)) {
        // Upper half was clicked.
    }
    else {
        // Lower half was clicked.
    }
}

CPoint objects passed to OnLButtonDown and other mouse message handlers always contain device coordinates, so conversion is essential if you want to know the coordinates of the corresponding point in logical view space.

CScrollView Operations

CScrollView includes a handful of member functions that you can use to operate on a scroll view programmatically. For example, you can retrieve the current horizontal or vertical scroll position from a CScrollView by calling GetScrollPosition:

CPoint pos = GetScrollPosition ();

You can scroll to a given position programmatically with ScrollToPosition:

ScrollToPosition (CPoint (100, 100));

And you can measure the view's logical width and height with GetTotalSize:

CSize size = GetTotalSize ();
int nWidth = size.cx;
int nHeight = size.cy;

One of CScrollView's more interesting member functions is SetScaleToFit-Size. Suppose you'd like to implement a Zoom To Fit command in your application that scales the entire logical view to fit the physical view. It's easy with SetScaleToFitSize:

SetScaleToFitSize (GetTotalSize ());

To restore the view to its default scrollable form, simply call SetScrollSizes again. Incidentally, you can call SetScrollSizes multiple times throughout the life of an application to adjust scrolling parameters on the fly. For example, if the size of the logical view grows as data is added to the document, it's perfectly legal to use SetScrollSizes to increase the view's logical dimensions each time the document grows.

Optimizing Scrolling Performance

CScrollView is architected in such a way that the OnDraw code you write doesn't have to explicitly factor in the scroll position. Consequently, an OnDraw function borrowed from a CView generally works without modification in a CScrollView. But "works" and "performs acceptably" are two different things.

CScrollView stresses a view's OnDraw function far more than a CView does because scrolling precipitates more calls to OnDraw. Very often, a call to OnDraw induced by a scroll bar event requires only a few rows of pixels to be painted. If OnDraw attempts to paint the entire view, the GDI eliminates unnecessary output by clipping pixels outside the invalid rectangle. But clipping takes time, with the result that scrolling performance can range from fine to abysmal depending on how many CPU cycles OnDraw wastes trying to paint outside the invalid rectangle.

After you get a scroll view working, you should test its performance by dragging the scroll bar thumb. If the window scrolls acceptably, you're done. But if it doesn't (and in practice, it probably won't more often than it will), you should modify the view's OnDraw function so that it identifies the invalid rectangle and, to the extent possible, limits its painting to those pixels that fall inside the rectangle.

The key to optimizing OnDraw is a CDC function named GetClipBox. Called on the device context object passed to OnDraw, GetClipBox initializes a RECT structure or CRect object with the size and location, in logical coordinates, of the invalid rectangle, as shown here:

CRect rect;
pDC->GetClipBox (&rect);

A CRect initialized in this manner tells you what part of the view needs redrawing. How you use this information is highly application-specific. The sample program in the next section, which displays a spreadsheet in a scrollable view, translates the coordinates returned by GetClipBox into row and column numbers and uses the results to paint only those cells that fall within (either in whole or in part) the invalid rectangle. This is just one example of how GetClipBox can be used to optimize painting by eliminating unnecessary output. You'll see additional examples in subsequent chapters.

The ScrollDemo Application

The ScrollDemo application shown in Figure 10-2 demonstrates many of the principles discussed in the preceding sections. ScrollDemo displays a spreadsheet that measures 26 columns wide and 99 rows high. One cell in the spreadsheet—the "current cell"—is highlighted in light blue. Clicking a cell with the left mouse button makes that cell the current cell and moves the highlight. The spreadsheet is displayed in a scrollable view defined by the CScrollView-derived class named CScrollDemoView. CScrollDemoView's source code appears in Figure 10-3.

Click to view at full size.

Figure 10-2. The ScrollDemo window.

Figure 10-3. The ScrollDemo application

ScrollDemoView.h

// ScrollDemoView.h : interface of the CScrollDemoView class
//
//////////////////////////////////////////////////////////////////////////

#if !defined(AFX_SCROLLDEMOVIEW_H__DCCF4E0D_9735_11D2_8E53_006008A82731__INCLUDED_)
#define AFX_SCROLLDEMOVIEW_H__DCCF4E0D_9735_11D2_8E53_006008A82731__INCLUDED_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000


class CScrollDemoView : public CScrollView
{
protected: // create from serialization only
    CScrollDemoView();
    DECLARE_DYNCREATE(CScrollDemoView)

// Attributes
public:
    CScrollDemoDoc* GetDocument();

// Operations
public:

// Overrides
    // ClassWizard generated virtual function overrides
    //{{AFX_VIRTUAL(CScrollDemoView)
    public:
    virtual void OnDraw(CDC* pDC);  // overridden to draw this view
    virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
    protected:
    virtual void OnInitialUpdate(); // called first time after construct
    //}}AFX_VIRTUAL

// Implementation
public:
    virtual ~CScrollDemoView();
#ifdef _DEBUG
    virtual void AssertValid() const;
    virtual void Dump(CDumpContext& dc) const;
#endif

protected:

// Generated message map functions
protected:
    BOOL m_bSmooth;
    void GetCellRect (int row, int col, LPRECT pRect);
    void DrawAddress (CDC* pDC, int row, int col);
    void DrawPointer (CDC* pDC, int row, int col, BOOL bHighlight);
    CFont m_font;
    int m_nCurrentCol;
    int m_nCurrentRow;
    int m_nRibbonWidth;
    int m_nCellHeight;
    int m_nCellWidth;
    //{{AFX_MSG(CScrollDemoView)
    afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
    //}}AFX_MSG
    DECLARE_MESSAGE_MAP()
};

#ifndef _DEBUG  // debug version in ScrollDemoView.cpp
inline CScrollDemoDoc* CScrollDemoView::GetDocument()
   { return (CScrollDemoDoc*)m_pDocument; }
#endif

///////////////////////////////////////////////////////////////////////////

//{{AFX_INSERT_LOCATION}}
// Microsoft Visual C++ will insert additional declarations immediately
// before the previous line.

#endif 
// !defined(
//     AFX_SCROLLDEMOVIEW_H__DCCF4E0D_9735_11D2_8E53_006008A82731__INCLUDED_)

ScrollDemoView.cpp

// ScrollDemoView.cpp : implementation of the CScrollDemoView class
//

#include "stdafx.h"
#include "ScrollDemo.h"
#include "ScrollDemoDoc.h"

#include "ScrollDemoView.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

///////////////////////////////////////////////////////////////////////////
// CScrollDemoView

IMPLEMENT_DYNCREATE(CScrollDemoView, CScrollView)

BEGIN_MESSAGE_MAP(CScrollDemoView, CScrollView)
    //{{AFX_MSG_MAP(CScrollDemoView)
    ON_WM_LBUTTONDOWN()
    //}}AFX_MSG_MAP
END_MESSAGE_MAP()

///////////////////////////////////////////////////////////////////////////
// CScrollDemoView construction/destruction

CScrollDemoView::CScrollDemoView()
{
    m_font.CreatePointFont (80, _T ("MS Sans Serif"));
}

CScrollDemoView::~CScrollDemoView()
{
}

BOOL CScrollDemoView::PreCreateWindow(CREATESTRUCT& cs)
{
    return CScrollView::PreCreateWindow(cs);
}

///////////////////////////////////////////////////////////////////////////
// CScrollDemoView drawing

void CScrollDemoView::OnDraw(CDC* pDC)
{
    CScrollDemoDoc* pDoc = GetDocument();
    ASSERT_VALID(pDoc);

    //
    // Draw the grid lines.

//
    CSize size = GetTotalSize ();

    CPen pen (PS_SOLID, 0, RGB (192, 192, 192));
    CPen* pOldPen = pDC->SelectObject (&pen);
    for (int i=0; i<99; i++) {
        int y = (i * m_nCellHeight) + m_nCellHeight;
        pDC->MoveTo (0, y);
        pDC->LineTo (size.cx, y);
    }

    for (int j=0; j<26; j++) {
        int x = (j * m_nCellWidth) + m_nRibbonWidth;
        pDC->MoveTo (x, 0);
        pDC->LineTo (x, size.cy);
    }

    pDC->SelectObject (pOldPen);
    
    //
    // Draw the bodies of the rows and column headers.
    //
    CBrush brush;
    brush.CreateStockObject (LTGRAY_BRUSH);

    CRect rcTop (0, 0, size.cx, m_nCellHeight);
    pDC->FillRect (rcTop, &brush);
    CRect rcLeft (0, 0, m_nRibbonWidth, size.cy);
    pDC->FillRect (rcLeft, &brush);

    pDC->MoveTo (0, m_nCellHeight);
    pDC->LineTo (size.cx, m_nCellHeight);
    pDC->MoveTo (m_nRibbonWidth, 0);
    pDC->LineTo (m_nRibbonWidth, size.cy);

    pDC->SetBkMode (TRANSPARENT);

    //
    // Add numbers and button outlines to the row headers.
    //
    for (i=0; i<99; i++) {
        int y = (i * m_nCellHeight) + m_nCellHeight;
        pDC->MoveTo (0, y);
        pDC->LineTo (m_nRibbonWidth, y);

        CString string;
        string.Format (_T ("%d"), i + 1);

        CRect rect (0, y, m_nRibbonWidth, y + m_nCellHeight);
        pDC->DrawText (string, &rect, DT_SINGLELINE ¦
            DT_CENTER ¦ DT_VCENTER);

        rect.top++;
        pDC->Draw3dRect (rect, RGB (255, 255, 255),
            RGB (128, 128, 128));
    }

    //
    // Add letters and button outlines to the column headers.
    //
    for (j=0; j<26; j++) {
        int x = (j * m_nCellWidth) + m_nRibbonWidth;
        pDC->MoveTo (x, 0);
        pDC->LineTo (x, m_nCellHeight);

        CString string;
        string.Format (_T ("%c"), j + `A');

        CRect rect (x, 0, x + m_nCellWidth, m_nCellHeight);
        pDC->DrawText (string, &rect, DT_SINGLELINE ¦
            DT_CENTER ¦ DT_VCENTER);

        rect.left++;
        pDC->Draw3dRect (rect, RGB (255, 255, 255),
            RGB (128, 128, 128));
    }

    //
    // Draw address labels into the individual cells.
    //
    CRect rect;
    pDC->GetClipBox (&rect);
    int nStartRow = max (0, (rect.top - m_nCellHeight) / m_nCellHeight);
    int nEndRow = min (98, (rect.bottom - 1) / m_nCellHeight);
    int nStartCol = max (0, (rect.left - m_nRibbonWidth) / m_nCellWidth);
    int nEndCol = min (25, ((rect.right + m_nCellWidth - 1) -
        m_nRibbonWidth) / m_nCellWidth);

    for (i=nStartRow; i<=nEndRow; i++)
        for (j=nStartCol; j<=nEndCol; j++)
            DrawAddress (pDC, i, j);

    //
    // Draw the cell pointer.
    //
    DrawPointer (pDC, m_nCurrentRow, m_nCurrentCol, TRUE);
}
void CScrollDemoView::OnInitialUpdate()
{
    CScrollView::OnInitialUpdate();

    m_nCurrentRow = 0;
    m_nCurrentCol = 0;
    m_bSmooth = FALSE;

    CClientDC dc (this);
    m_nCellWidth = dc.GetDeviceCaps (LOGPIXELSX);
    m_nCellHeight = dc.GetDeviceCaps (LOGPIXELSY) / 4;
    m_nRibbonWidth = m_nCellWidth / 2;

    int nWidth = (26 * m_nCellWidth) + m_nRibbonWidth;
    int nHeight = m_nCellHeight * 100;
    SetScrollSizes (MM_TEXT, CSize (nWidth, nHeight));
}

///////////////////////////////////////////////////////////////////////////
// CScrollDemoView diagnostics

#ifdef _DEBUG
void CScrollDemoView::AssertValid() const
{
    CScrollView::AssertValid();
}

void CScrollDemoView::Dump(CDumpContext& dc) const
{
    CScrollView::Dump(dc);
}

CScrollDemoDoc* CScrollDemoView::GetDocument() // non-debug version is
                                               inline
{
    ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CScrollDemoDoc)));
    return (CScrollDemoDoc*)m_pDocument;
}
#endif //_DEBUG

///////////////////////////////////////////////////////////////////////////
// CScrollDemoView message handlers

void CScrollDemoView::OnLButtonDown(UINT nFlags, CPoint point) 
{
    CScrollView::OnLButtonDown(nFlags, point);

    //
    // Convert the click point to logical coordinates.
    //
    CPoint pos = point;
    CClientDC dc (this);
    OnPrepareDC (&dc);
    dc.DPtoLP (&pos);

    //
    // If a cell was clicked, move the cell pointer.
    //
    CSize size = GetTotalSize ();
    if (pos.x > m_nRibbonWidth && pos.x < size.cx &&
        pos.y > m_nCellHeight && pos.y < size.cy) {

        int row = (pos.y - m_nCellHeight) / m_nCellHeight;
        int col = (pos.x - m_nRibbonWidth) / m_nCellWidth;
        ASSERT (row >= 0 && row <= 98 && col >= 0 && col <= 25);

        DrawPointer (&dc, m_nCurrentRow, m_nCurrentCol, FALSE);
        m_nCurrentRow = row;
        m_nCurrentCol = col;
        DrawPointer (&dc, m_nCurrentRow, m_nCurrentCol, TRUE);
    }
}

void CScrollDemoView::DrawPointer(CDC *pDC, int row, int col, 
    BOOL bHighlight)
{
    CRect rect;
    GetCellRect (row, col, &rect);
    CBrush brush (bHighlight ? RGB (0, 255, 255) :
        ::GetSysColor (COLOR_WINDOW));
    pDC->FillRect (rect, &brush);
    DrawAddress (pDC, row, col);
}

void CScrollDemoView::DrawAddress(CDC *pDC, int row, int col)
{
    CRect rect;
    GetCellRect (row, col, &rect);

    CString string;
    string.Format (_T ("%c%d"), col + _T (`A'), row + 1);

    pDC->SetBkMode (TRANSPARENT);
    CFont* pOldFont = pDC->SelectObject (&m_font);
    pDC->DrawText (string, rect, DT_SINGLELINE ¦ DT_CENTER ¦ DT_VCENTER);
    pDC->SelectObject (pOldFont);
}

void CScrollDemoView::GetCellRect(int row, int col, LPRECT pRect)
{
    pRect->left = m_nRibbonWidth + (col * m_nCellWidth) + 1;
    pRect->top = m_nCellHeight + (row * m_nCellHeight) + 1;
    pRect->right = pRect->left + m_nCellWidth - 1;
    pRect->bottom = pRect->top + m_nCellHeight - 1;
}

Because CScrollView manages most aspects of scrolling, ScrollDemo includes remarkably little code to explicitly support scrolling operations. It does, however, use GetClipBox to optimize OnDraw's performance. Rather than attempt to paint all 2,574 spreadsheet cells every time it's called, OnDraw translates the clip box into starting and ending row and column numbers and paints only those cells that fall within these ranges. The pertinent code is near the end of OnDraw:

CRect rect;
pDC->GetClipBox (&rect);
int nStartRow = max (0, (rect.top - m_nCellHeight) / m_nCellHeight);
int nEndRow = min (98, (rect.bottom - 1) / m_nCellHeight);
int nStartCol = max (0, (rect.left - m_nRibbonWidth) / m_nCellWidth);
int nEndCol = min (25, ((rect.right + m_nCellWidth - 1) -
    m_nRibbonWidth) / m_nCellWidth);

for (i=nStartRow; i<=nEndRow; i++)
    for (j=nStartCol; j<=nEndCol; j++)
        DrawAddress (pDC, i, j);

As an experiment, try modifying the for loop to paint every cell:

for (i=0; i<99; i++)
    for (j=0; j<26; j++)
        DrawAddress (pDC, i, j);

Then try scrolling the spreadsheet. You'll quickly see why optimizing OnDraw is a necessity rather than an option in many scroll views.

Another interesting experiment involves the view's OnLButtonDown function, which moves the cell highlight in response to mouse clicks. Before using the CPoint object passed to it to determine the row and column number in which the click occurred, OnLButtonDown converts the CPoint's device coordinates to logical coordinates with the following statements:

CPoint pos = point;
CClientDC dc (this);
OnPrepareDC (&dc);
dc.DPtoLP (&pos);

To see what happens if OnLButtonDown fails to take the scroll position into account in a CScrollView, delete the call to DPtoLP and try clicking around in the spreadsheet after scrolling it a short distance horizontally or vertically.

Converting an Ordinary View into a Scroll View

What happens if you use AppWizard to generate a CView-based application and later decide you want a CScrollView? You can't use the MFC wizards to convert a CView into a CScrollView after the fact, but you can perform the conversion by hand. Here's how:

  1. Search the view's header file and CPP file and change all occurrences of CView to CScrollView, except where CView* occurs in a function's parameter list.
  2. Override OnInitialUpdate if it isn't overridden already, and insert a call to SetScrollSizes.

If you perform step 1 but forget step 2, you'll know it as soon as you run the application because MFC will assert on you. MFC can't manage a scroll view if it doesn't know the view's logical dimensions.

The CHM file was converted to HTML by chm2web software.