Getting User Input: Message Map Functions
Chapter 3 does not accept user input (other than the standard Microsoft Windows resizing and window close commands). The window contains menus and a toolbar, but these are not "connected" to the view code. I won't discuss the menus and the toolbar until Part III of this book because they depend on the frame class, but plenty of other Windows input sources will keep you busy until then. However, before you can process any Windows event, even a mouse click, you must learn how to use the MFC library message map system.
The Message Map
When the user clicks the left mouse button in a view window, Windows sends a message—specifically, WM_LBUTTONDOWN—to that window. If your program needs to take action in response to WM_LBUTTONDOWN, your view class must have a member function that looks like this:
void CMyView::OnLButtonDown(UINT nFlags, CPoint point)
{
// event processing code here
}
Your class header file must also have the corresponding prototype:
afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
The afx_msg notation is a "no-op" that alerts you that this is a prototype for a message map function.Next, your code file needs a message map macro that connects your OnLButtonDown function to the application framework:
BEGIN_MESSAGE_MAP(CMyView, CView)
ON_WM_LBUTTONDOWN() // entry specifically for OnLButtonDown
// other message map entries
END_MESSAGE_MAP()
Finally, your class header file needs this statement:
DECLARE_MESSAGE_MAP()
How do you know which function goes with which Windows message? Chapter Appendix A (and the MFC library online documentation) includes a table that lists all standard Windows messages and corresponding member function prototypes. You can manually code the message-handling functions— indeed, you still have to do that for certain messages. But fortunately, the code wizards available from Class View's Properties window automate the coding of most message map functions.
Saving the View's State: Class Data Members
If your program accepts user input, you'll want the user to get some visual feedback. The view's OnDraw function draws an image based on the view's current state, and user actions can alter that state. In a full-blown MFC library application, the document object holds the state of the application, but you're not to that point yet. For now, we'll use two view class data members, m_rectEllipse and m_nColor. The first is an object of class CRect, which holds the current bounding rectangle of an ellipse, and the second is an integer that holds the current ellipse color value.
Note | By convention, MFC library nonstatic class data member names begin with m_. |
We'll make a message-mapped member function toggle the ellipse color (the view's state) between gray and white. (The toggle is activated by a click of the left mouse button.) The initial values of m_rectEllipse and m_nColor are set in the view's constructor, and the color is changed in the OnLButtonDown member function.
Note | Why not use a global variable for the view's state? Because if you do, you'll be in trouble if your application has multiple views. Besides, encapsulating data in objects is a big part of what object-oriented programming is all about. |
Initializing a View Class Data Member
The most efficient place to initialize a class data member is in the constructor, as shown here:
CMyView::CMyView() : m_rectEllipse(0, 0, 200, 200) {...}
You can initialize m_nColor with the same syntax. We're using a built-in type (integer), so the generated code is the same if you use an assignment statement in the constructor body.
Invalid Rectangle Theory
The OnLButtonDown function can toggle the value of m_nColor all day, but if that's all it did, the OnDraw function wouldn't get called (unless, for example, the user resized the view window). The OnLButtonDown function must call the InvalidateRect function (a member function that the view class inherits from CWnd). InvalidateRect triggers a Windows WM_PAINT message, which is mapped in the CView class to call to the virtual OnDraw function. If necessary, OnDraw can access the "invalid rectangle" parameter that was passed to InvalidateRect.You can optimize painting in Windows in two ways. First, you must be aware that Windows updates only those pixels that are inside the invalid rectangle. Thus, the smaller you make the invalid rectangle (in the OnLButtonDown handler, for instance), the more quickly it can be repainted. Second, it's a waste of time to execute drawing instructions outside the invalid rectangle. Your OnDraw function can call the CDC member function GetClipBox to determine the invalid rectangle, and then it can avoid drawing objects outside it. Remember that OnDraw is being called not only in response to your InvalidateRect call but also when the user resizes or exposes the window. Thus, OnDraw is responsible for all drawing in a window, and it has to adapt to whatever invalid rectangle it gets.
The MFC library makes it easy to attach your own state variables to a window through C++ class data members. In Win32 programming, the WNDCLASS members cbClsExtra and cbWndExtra are available for this purpose, but the code for using this mechanism is so complex that developers tend to use global variables instead.
The Window's Client Area
A window has a rectangular client area that excludes the border, caption bar, menu bar, and any docking toolbars. The CWnd member function GetClientRect supplies you with the client-area dimensions. Normally, you're not allowed to draw outside the client area, and most mouse messages are received only when the cursor is in the client area.
CRect, CPoint, and CSize Arithmetic
The CRect, CPoint, and CSize classes are derived from the Windows RECT, POINT, and SIZE structures, and thus they inherit public integer data members, as follows:
CRect | left, top, right, bottom |
CPoint | x, y |
CSize | cx, cy |
If you look in the MFC Library Reference, you'll see that these three classes have a number of overloaded operators. You can, among other things, do the following:
Add a CSize object to a CPoint object
Subtract a CSize object from a CPoint object
Subtract one CPoint object from another, yielding a CSize object
Add a CPoint or CSize object to a CRect object
Subtract a CPoint or CSize object from a CRect object
The CRect class has member functions that relate to the CSize and CPoint classes. For example, the TopLeft member function returns a CPoint object, and the Size member function returns a CSize object. From this, you can begin to see that a CSize object is the "difference between two CPoint objects" and that you can "bias" a CRect object by a CPoint object.
Determining Whether a Point Is Inside a Rectangle
The CRect class has a member function, PtInRect, that tests a point to see whether it falls inside a rectangle. The second OnLButtonDown parameter, point, is an object of class CPoint that represents the cursor location in the client area of the window. If you want to know whether that point is inside the m_rectEllipse rectangle, you can use PtInRect in this way:
if (m_rectEllipse.PtInRect(point)) {
// point is inside rectangle
}
As you'll soon see, however, this simple logic applies only if you're working in device coordinates (which you are at this stage).
The CRect LPCRECT Operator
If you read the MFC Library Reference carefully, you'll notice that CWnd::InvalidateRect takes an LPCRECT parameter (a pointer to a RECT structure), not a CRect parameter. A CRect parameter is allowed because the CRect class defines an overloaded operator, LPCRECT(), that returns the address of a CRect object, which is equivalent to the address of a RECT object. Thus, the compiler converts CRect arguments to LPCRECT arguments when necessary. You call functions as if they have CRect reference parameters.The following view member function code retrieves the client rectangle coordinates and stores them in rectClient:
CRect rectClient;
GetClientRect(rectClient);
Determining Whether a Point Is Inside an Ellipse
The Ex05a code determines whether the mouse hit is inside the rectangle. If you want to make a better test, you can find out whether the hit is inside the ellipse. To do this, you construct an object of class CRgn that corresponds to the ellipse and then use the PtInRegion function instead of PtInRect. Here's the code:
CRgn rgn;
rgn.CreateEllipticRgnIndirect(m_rectEllipse);
if (rgn.PtInRegion(point)) {
// point is inside ellipse
}
Note that the CreateEllipticRgnIndirect function is another function that takes an LPCRECT parameter. It builds a special region structure within Windows that represents an elliptical region inside a window. That structure is then attached to the C++ CRgn object in your program. (The same type of structure can also represent a polygon.)
The Ex05a Example
In the Ex05a example, an ellipse (which happens to be a circle) changes color when the user clicks the left mouse button while the mouse cursor is inside the rectangle that bounds the ellipse. You'll use the view class data members to hold the view's state, and you'll use the InvalidateRect function to cause the view to be redrawn.In the Chapter 3 example, drawing in the window depends on only one function, OnDraw. The Ex05a example requires three customized functions (including the constructor) and two data members. The complete CEx05aView header and source code files are shown below. (The steps for creating the program are listed after the code.) All changes to the original MFC Application Wizard output and OnLButtonDown are shown in boldface.
Ex05aView.H
// Ex05aView.h : interface of the Cex05aView class
//
#pragma once
class CEx05aView : public CView
{
protected: // create from serialization only
CEx05aView();
DECLARE_DYNCREATE(CEx05aView)
// Attributes
public:
CEx05aDoc* GetDocument() const
// Operations
public:
// Overrides
public:
virtual void OnDraw(CDC* pDC); // overridden to draw this view
virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
protected:
virtual BOOL OnPreparePrinting(CPrintInfo* pInfo);
virtual void OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo);
virtual void OnEndPrinting(CDC* pDC, CPrintInfo* pInfo);
// Implementation
public:
virtual ~CEx05aView();
#ifdef _DEBUG
virtual void AssertValid() const;
virtual void Dump(CDumpContext& dc) const;
#endif
protected:
// Generated message map functions
protected:
afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
DECLARE_MESSAGE_MAP()
private:
int m_nColor;
CRect m_rectEllipse;
#ifndef _DEBUG // debug version in Ex05aView.cpp
inline CEx05aDoc* CEx05aView::GetDocument() const
{ return reinterpret_cast<CEx05aDoc*>(m_pDocument); }
#endif
};
Ex05aView.cpp
// Ex05aView.cpp : implementation of the CEx05aView class
//
#include "stdafx.h"
#include "Ex05a.h"
#include "Ex05aDoc.h"
#include "Ex05aView.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
///////////////////////////////////////////////////////////////////////
// CEx05aView
IMPLEMENT_DYNCREATE(CEx05aView, CView)
BEGIN_MESSAGE_MAP(CEx05aView, CView)
ON_WM_LBUTTONDOWN()
// Standard printing commands
ON_COMMAND(ID_FILE_PRINT, CView::OnFilePrint)
ON_COMMAND(ID_FILE_PRINT_DIRECT, CView::OnFilePrint)
ON_COMMAND(ID_FILE_PRINT_PREVIEW, CView::OnFilePrintPreview)
END_MESSAGE_MAP()
///////////////////////////////////////////////////////////////////////
// CEx05aView construction/destruction
CEx05aView::CEx05aView() : m_rectEllipse(0, 0, 200, 200)
{
m_nColor = GRAY_BRUSH;
}
CEx05aView::~CEx05aView()
{
}
BOOL CEx05aView::PreCreateWindow(CREATESTRUCT& cs)
{
// TODO: Modify the Window class or styles here by modifying
// the CREATESTRUCT cs
return CView::PreCreateWindow(cs);
}
///////////////////////////////////////////////////////////////////////
// CEx05aView drawing
void CEx05aView::OnDraw(CDC* pDC)
{
pDC->SelectStockObject(m_nColor);
pDC->Ellipse(m_rectEllipse);
}
///////////////////////////////////////////////////////////////////////
// CEx05aView printing
BOOL CEx05aView::OnPreparePrinting(CPrintInfo* pInfo)
{
// default preparation
return DoPreparePrinting(pInfo);
}
void CEx05aView::OnBeginPrinting(CDC* /*pDC*/, CPrintInfo* /*pInfo*/)
{
// TODO: add extra initialization before printing
}
void CEx05aView::OnEndPrinting(CDC* /*pDC*/, CPrintInfo* /*pInfo*/)
{
// TODO: add cleanup after printing
}
///////////////////////////////////////////////////////////////////////
// CEx05aView diagnostics
#ifdef _DEBUG
void CEx05aView::AssertValid() const
{
CView::AssertValid();
}
void CEx05aView::Dump(CDumpContext& dc) const
{
CView::Dump(dc);
}
CEx05aDoc* CEx05aView::GetDocument() // non-debug version is inline
{
ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CEx05aDoc)));
return (CEx05aDoc*)m_pDocument;
}
#endif //_DEBUG
///////////////////////////////////////////////////////////////////////
// CEx05aView message handlers
void CEx05aView::OnLButtonDown(UINT nFlags, CPoint point)
{
if (m_rectEllipse.PtInRect(point)) {
if (m_nColor == GRAY_BRUSH) {
m_nColor = WHITE_BRUSH;
}
else {
m_nColor = GRAY_BRUSH;
}
InvalidateRect(m_rectEllipse);
}
}
Using Class View with Ex05a
Look at the following Ex05aView.h source code:
afx_msg void OnLButtonDown(UINT nFlags, Cpoint point);
Now look at the following Ex05aView.cpp source code:
ON_WM_LBUTTONDOWN()
The MFC Application Wizard used to generate comment lines for the benefit of the Class Wizard. Fortunately, these comments are no longer needed. Visual C++ .NET keeps track of the entire state of your code at all times, including mapping functions and maps to specific lines in your code. The code wizards available from the Class View's Properties window add message handler prototypes based on this internal information. In addition, the code wizards generate a skeleton OnLButtonDown member function in Ex05aView.cpp , complete with the correct parameter declarations and return type.Notice how the combination of the MFC Application Wizard and code wizards is different from a conventional code generator. You run a conventional code generator only once and then edit the resulting code. You run the MFC Application Wizard to generate the application only once, but you can run the code wizards as many times as necessary, and you can edit the code at any time.
Using the MFC Application Wizard and the Code Wizards Together
The following steps show how you use the MFC Application Wizard and the code wizards available from Class View's Properties window to create this application:
Run the MFC Application Wizard to create Ex05a. Use the wizard to generate an SDI project named Ex05a in the \vcppnet subdirectory. The default class names are shown here.
Add the m_rectEllipse and m_nColor data members to CEx05aView. Choose Class View from the View menu in Visual C++ .NET and right-click the CEx05aView class. Choose Add Variable and then insert the following two data members:
private:
CRect m_rectEllipse;
int m_nColor;
If you prefer, you can type the above code inside the class declaration in the file ex05aView.h .
Use the Class View's Properties window to add a CEx05aView class message handler. Select the CEx05aView class within Class View, as shown in the following illustration. Next, right-click on CEx05aView and choose Properties. Click the Messages button on the Properties window's toolbar. Scroll down and click on the WM_LBUTTONDOWN entry. You'll see a drop-down combo box appear next to the entry. Select <Add> OnLButtonDown. The OnLButtonDown function will be written into the code and will appear inside the Code Editor.
Edit the OnLButtonDown code in Ex05aView.cpp . Once you add the message handler, the file Ex05aView.cpp will open in the Code Editor and the cursor will be positioned to the newly generated OnLButtonDown member function. The following boldface code (that you type in) replaces the previous code:
void CEx05aView::OnLButtonDown(UINT nFlags, CPoint point)
{
if (m_rectEllipse.PtInRect(point)) {
if (m_nColor == GRAY_BRUSH) {
m_nColor = WHITE_BRUSH;
}
else {
m_nColor = GRAY_BRUSH;
}
InvalidateRect(m_rectEllipse);
}
}
Edit the constructor and the OnDraw function in Ex05aView.cpp . The following boldface code (that you type in) replaces the previous code:
CEx05aView::CEx05aView() : m_rectEllipse(0, 0, 200, 200)
{
m_nColor = GRAY_BRUSH;
}
void CEx05aView::OnDraw(CDC* pDC)
{
pDC->SelectStockObject(m_nColor);
pDC->Ellipse(m_rectEllipse);
}
Build and run the Ex05a program. Choose Build from the Build menu or, on the Build toolbar, click the button shown here.
Next, choose Start Without Debugging from the Debug menu. The resulting program will respond to clicks of the left mouse button by changing the color of the circle in the view window. (Don't click the mouse's left button quickly in succession; Windows will interpret this as a double-click rather than two single clicks.)
A conventional Windows-based application registers a series of window classes (not the same as C++ classes) and, in the process, assigns a unique function, known as a window procedure, to each class. Each time the application calls CreateWindow to create a window, it specifies a window class as a parameter and thus links the newly created window to a window procedure function. This function, which is called each time Windows sends a message to the window, tests the message code that is passed as a parameter and then executes the appropriate code to handle the message.The MFC application framework has a single window class and window procedure function for most window types. This window procedure function looks up the window handle (passed as a parameter) in the MFC handle map to get the corresponding C++ window object pointer. The window procedure function then uses the MFC runtime class system to determine the C++ class of the window object. Next, it locates the handler function in static tables created by the dispatch map functions, and finally it calls the handler function with the correct window object selected.