The Accel Application
Let's put this newfound knowledge to work by writing an application that scrolls. Accel draws a window that resembles Microsoft Excel. (See Figure 2-13.) The spreadsheet depicted in the window is 26 columns wide and 99 rows high—much too large to be displayed all at once. However, scroll bars allow the user to view all parts of the spreadsheet. In addition to providing a hands-on look at the principles discussed in the preceding sections, Accel demonstrates another way that a program can scale its output. Rather than use a non-MM_TEXT mapping mode, it uses CDC::GetDeviceCaps to query the display device for the number of pixels per inch displayed horizontally and vertically. Then it draws each spreadsheet cell so that it's 1 inch wide and ¼ inch tall using raw pixel counts.
Figure 2-13. The Accel window.
Figure 2-14. The Accel application.
Accel.h#define LINESIZE 8
class CMyApp : public CWinApp
{
public:
virtual BOOL InitInstance ();
};
class CMainWindow : public CFrameWnd
{
protected:
int m_nCellWidth; // Cell width in pixels
int m_nCellHeight; // Cell height in pixels
int m_nRibbonWidth; // Ribbon width in pixels
int m_nViewWidth; // Workspace width in pixels
int m_nViewHeight; // Workspace height in pixels
int m_nHScrollPos; // Horizontal scroll position
int m_nVScrollPos; // Vertical scroll position
int m_nHPageSize; // Horizontal page size
int m_nVPageSize; // Vertical page size
public:
CMainWindow ();
protected:
afx_msg void OnPaint ();
afx_msg int OnCreate (LPCREATESTRUCT lpCreateStruct);
afx_msg void OnSize (UINT nType, int cx, int cy);
afx_msg void OnHScroll (UINT nCode, UINT nPos,
CScrollBar* pScrollBar);
afx_msg void OnVScroll (UINT nCode, UINT nPos,
CScrollBar* pScrollBar);
DECLARE_MESSAGE_MAP ()
};
Accel.cpp#include <afxwin.h>
#include "Accel.h"
CMyApp myApp;
/////////////////////////////////////////////////////////////////////////
// CMyApp member functions
BOOL CMyApp::InitInstance ()
{
m_pMainWnd = new CMainWindow;
m_pMainWnd->ShowWindow (m_nCmdShow);
m_pMainWnd->UpdateWindow ();
return TRUE;
}
/////////////////////////////////////////////////////////////////////////
// CMainWindow message map and member functions
BEGIN_MESSAGE_MAP (CMainWindow, CFrameWnd)
ON_WM_CREATE ()
ON_WM_SIZE ()
ON_WM_PAINT ()
ON_WM_HSCROLL ()
ON_WM_VSCROLL ()
END_MESSAGE_MAP ()
CMainWindow::CMainWindow ()
{
Create (NULL, _T ("Accel"),
WS_OVERLAPPEDWINDOW ¦ WS_HSCROLL ¦ WS_VSCROLL);
}
int CMainWindow::OnCreate (LPCREATESTRUCT lpCreateStruct)
{
if (CFrameWnd::OnCreate (lpCreateStruct) == -1)
return -1;
CClientDC dc (this);
m_nCellWidth = dc.GetDeviceCaps (LOGPIXELSX);
m_nCellHeight = dc.GetDeviceCaps (LOGPIXELSY) / 4;
m_nRibbonWidth = m_nCellWidth / 2;
m_nViewWidth = (26 * m_nCellWidth) + m_nRibbonWidth;
m_nViewHeight = m_nCellHeight * 100;
return 0;
}
void CMainWindow::OnSize (UINT nType, int cx, int cy)
{
CFrameWnd::OnSize (nType, cx, cy);
//
// Set the horizontal scrolling parameters.
//
int nHScrollMax = 0;
m_nHScrollPos = m_nHPageSize = 0;
if (cx < m_nViewWidth) {
nHScrollMax = m_nViewWidth - 1;
m_nHPageSize = cx;
m_nHScrollPos = min (m_nHScrollPos, m_nViewWidth -
m_nHPageSize - 1);
}
SCROLLINFO si;
si.fMask = SIF_PAGE ¦ SIF_RANGE ¦ SIF_POS;
si.nMin = 0;
si.nMax = nHScrollMax;
si.nPos = m_nHScrollPos;
si.nPage = m_nHPageSize;
SetScrollInfo (SB_HORZ, &si, TRUE);
//
// Set the vertical scrolling parameters.
//
int nVScrollMax = 0;
m_nVScrollPos = m_nVPageSize = 0;
if (cy < m_nViewHeight) {
nVScrollMax = m_nViewHeight - 1;
m_nVPageSize = cy;
m_nVScrollPos = min (m_nVScrollPos, m_nViewHeight -
m_nVPageSize - 1);
}
si.fMask = SIF_PAGE ¦ SIF_RANGE ¦ SIF_POS;
si.nMin = 0;
si.nMax = nVScrollMax;
si.nPos = m_nVScrollPos;
si.nPage = m_nVPageSize;
SetScrollInfo (SB_VERT, &si, TRUE);
}
void CMainWindow::OnPaint ()
{
CPaintDC dc (this);
//
// Set the window origin to reflect the current scroll positions.
//
dc.SetWindowOrg (m_nHScrollPos, m_nVScrollPos);
//
// Draw the grid lines.
//
CPen pen (PS_SOLID, 0, RGB (192, 192, 192));
CPen* pOldPen = dc.SelectObject (&pen);
for (int i=0; i<99; i++) {
int y = (i * m_nCellHeight) + m_nCellHeight;
dc.MoveTo (0, y);
dc.LineTo (m_nViewWidth, y);
}
for (int j=0; j<26; j++) {
int x = (j * m_nCellWidth) + m_nRibbonWidth;
dc.MoveTo (x, 0);
dc.LineTo (x, m_nViewHeight);
}
dc.SelectObject (pOldPen);
//
// Draw the bodies of the rows and the column headers.
//
CBrush brush;
brush.CreateStockObject (LTGRAY_BRUSH);
CRect rcTop (0, 0, m_nViewWidth, m_nCellHeight);
dc.FillRect (rcTop, &brush);
CRect rcLeft (0, 0, m_nRibbonWidth, m_nViewHeight);
dc.FillRect (rcLeft, &brush);
dc.MoveTo (0, m_nCellHeight);
dc.LineTo (m_nViewWidth, m_nCellHeight);
dc.MoveTo (m_nRibbonWidth, 0);
dc.LineTo (m_nRibbonWidth, m_nViewHeight);
dc.SetBkMode (TRANSPARENT);
//
// Add numbers and button outlines to the row headers.
//
for (i=0; i<99; i++) {
int y = (i * m_nCellHeight) + m_nCellHeight;
dc.MoveTo (0, y);
dc.LineTo (m_nRibbonWidth, y);
CString string;
string.Format (_T ("%d"), i + 1);
CRect rect (0, y, m_nRibbonWidth, y + m_nCellHeight);
dc.DrawText (string, &rect, DT_SINGLELINE ¦
DT_CENTER ¦ DT_VCENTER);
rect.top++;
dc.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;
dc.MoveTo (x, 0);
dc.LineTo (x, m_nCellHeight);
CString string;
string.Format (_T ("%c"), j + `A');
CRect rect (x, 0, x + m_nCellWidth, m_nCellHeight);
dc.DrawText (string, &rect, DT_SINGLELINE ¦
DT_CENTER ¦ DT_VCENTER);
rect.left++;
dc.Draw3dRect (rect, RGB (255, 255, 255),
RGB (128, 128, 128));
}
}
void CMainWindow::OnHScroll (UINT nCode, UINT nPos, CScrollBar* pScrollBar)
{
int nDelta;
switch (nCode) {
case SB_LINELEFT:
nDelta = -LINESIZE;
break;
case SB_PAGELEFT:
nDelta = -m_nHPageSize;
break;
case SB_THUMBTRACK:
nDelta = (int) nPos - m_nHScrollPos;
break;
case SB_PAGERIGHT:
nDelta = m_nHPageSize;
break;
case SB_LINERIGHT:
nDelta = LINESIZE;
break;
default: // Ignore other scroll bar messages
return;
}
int nScrollPos = m_nHScrollPos + nDelta;
int nMaxPos = m_nViewWidth - m_nHPageSize;
if (nScrollPos < 0)
nDelta = -m_nHScrollPos;
else if (nScrollPos > nMaxPos)
nDelta = nMaxPos - m_nHScrollPos;
if (nDelta != 0) {
m_nHScrollPos += nDelta;
SetScrollPos (SB_HORZ, m_nHScrollPos, TRUE);
ScrollWindow (-nDelta, 0);
}
}
void CMainWindow::OnVScroll (UINT nCode, UINT nPos, CScrollBar* pScrollBar)
{
int nDelta;
switch (nCode) {
case SB_LINEUP:
nDelta = -LINESIZE;
break;
case SB_PAGEUP:
nDelta = -m_nVPageSize;
break;
case SB_THUMBTRACK:
nDelta = (int) nPos - m_nVScrollPos;
break;
case SB_PAGEDOWN:
nDelta = m_nVPageSize;
break;
case SB_LINEDOWN:
nDelta = LINESIZE;
break;
default: // Ignore other scroll bar messages
return;
}
int nScrollPos = m_nVScrollPos + nDelta;
int nMaxPos = m_nViewHeight - m_nVPageSize;
if (nScrollPos < 0)
nDelta = -m_nVScrollPos;
else if (nScrollPos > nMaxPos)
nDelta = nMaxPos - m_nVScrollPos;
if (nDelta != 0) {
m_nVScrollPos += nDelta;
SetScrollPos (SB_VERT, m_nVScrollPos, TRUE);
ScrollWindow (0, -nDelta);
}
}
GetDeviceCaps is called from CMainWindow's OnCreate handler, which is called upon receipt of a WM_CREATE message. WM_CREATE is the first message a window receives. It is sent just once, and it arrives very early in the window's lifetime—before the window is even visible on the screen. An ON_WM_CREATE entry in the window's message map connects WM_CREATE messages to the member function named OnCreate. OnCreate is the ideal place to initialize member variables whose values can only be determined at run time. It is prototyped as follows:
afx_msg int OnCreate (LPCREATESTRUCT lpCreateStruct)
lpCreateStruct is a pointer to a structure of type CREATESTRUCT, which contains useful information about a window such as its initial size and location on the screen. The value returned by OnCreate determines what Windows does next. If all goes as planned, OnCreate returns 0, signaling to Windows that the window was properly initialized. If OnCreate returns -1, Windows fails the attempt to create the window. A prototype OnCreate handler looks like this:
int CMainWindow::OnCreate (LPCREATESTRUCT lpCreateStruct)
{
if (CFrameWnd::OnCreate (lpCreateStruct) == -1)
return -1;
return 0;
}
OnCreate should always call the base class's OnCreate handler to give the framework the opportunity to execute its own window-creation code. This is especially important when you write document/view applications, because it is a function called by CFrameWnd::OnCreate that creates the view that goes inside a frame window.
You'll find the code that does the scrolling in the window's OnHScroll and OnVScroll handlers. switch-case logic converts the notification code passed in nCode into a signed nDelta value that represents the number of pixels the window should be scrolled. Once nDelta is computed, the scroll position is adjusted by nDelta pixels and the window is scrolled with the statements
m_nVScrollPos += nDelta;
SetScrollPos (SB_VERT, m_nVScrollPos, TRUE);
ScrollWindow (0, -nDelta);
for the vertical scroll bar and
m_nHScrollPos += nDelta;
SetScrollPos (SB_HORZ, m_nHScrollPos, TRUE);
ScrollWindow (-nDelta, 0);
for the horizontal scroll bar.
How are the scroll positions stored in m_nHScrollPos and m_nVScrollPos factored into the program's output? When OnPaint is called to paint the part of the workspace that was exposed by the scrolling operation, it repositions the window origin with the statement
dc.SetWindowOrg (m_nHScrollPos, m_nVScrollPos);
Recall that CDC::SetWindowOrg tells Windows to map the logical point (x,y) to the device point (0,0), which, for a client-area device context, corresponds to the upper left corner of the window's client area. The statement above moves the origin of the coordinate system left m_nHScrollPos pixels and upward m_nVScrollPos pixels. If OnPaint tries to paint the pixel at (0,0), the coordinate pair is transparently transformed by the GDI into (_m_nHScrollPos,_m_nVScrollPos). If the scroll position is (0,100), the first 100 rows of pixels are clipped from the program's output and the real output—the output the user can see—begins with the 101st row. Repositioning the origin in this manner is a simple and effective way to move a scrollable window over a virtual display surface.
If you could enlarge the window enough to see the entire spreadsheet, you would see the scroll bars disappear. That's because CMainWindow::OnSize sets the scroll bar range to 0 if the window size equals or exceeds the size of the virtual workspace. The OnSize handler also updates the scrolling parameters whenever the window size changes so that the thumb size accurately reflects the relative proportions of the window and the virtual workspace.
And with that, all the pieces are in place. The user clicks a scroll bar or drags a scroll bar thumb; OnHScroll or OnVScroll receives the message and responds by updating the scroll position and scrolling the window; and OnPaint redraws the window, using SetWindowOrg to move the drawing origin an amount that equals the current scroll position. The program's entire workspace is now accessible, despite the physical limitations that the window size imposes on the output. And all for less than 100 additional lines of code. How could it be any easier?
Funny you should ask. Because that's exactly what MFC's CScrollView class is for: to make scrolling easier. CScrollView is an MFC class that encapsulates the behavior of a scrolling window. You tell CScrollView how large a landscape you wish to view, and it handles everything else. Among other things, CScrollView processes WM_VSCROLL and WM_HSCROLL messages for you, scrolls the window in response to scroll bar events, and updates the thumb size when the window size changes.
While it's perfectly possible to wire a CScrollView into an application like Accel, CScrollView was designed primarily for document/view applications. Chapter 10 examines CScrollView more closely and also introduces some of the other view classes that MFC provides.