Chapter 17
Storing and Printing Documents

  • How serialization works
  • How to make objects of a class serializable
  • The role of a CArchive object in serialization
  • How to implement serialization in your own classes
  • How to implement serialization in the Sketcher application
  • How printing works with MFC
  • Which view class functions support printing
  • What a CPrintInfo object contains and how it’s used in the printing process
  • How to implement multipage printing in Sketcher

You can find the wrox.com code downloads for this chapter on the Download Code tab at www.wrox.com/go/beginningvisualc. The code is in the Chapter 17 download and individually named according to the names throughout the chapter.

UNDERSTANDING SERIALIZATION

A document in an MFC-based program is not a simple entity — it’s a class object that can be very complicated. It typically contains a variety of objects, each of which may contain other objects, each of which may contain still more objects and that structure may continue for a number of levels.

You want to be able to save a document in a file, but writing a class object to a file represents something of a problem because it isn’t the same as a basic data item like an integer or a character string. A basic data item consists of a known number of bytes, so to write it to a file only requires that the appropriate number of bytes be written. Conversely, if you know a value of type int was written to a file, to get it back, you just read the appropriate number of bytes.

Writing objects to a file is different. Even if you write all the data members of an object to a file, that’s not enough to be able to get the original object back. Class objects contain members function as well as data members, and all the members, both data and functions, have access specifiers; therefore, to record an object in an external file, the information that is written to the file must contain complete specifications of all the class structures involved. The read process must also be clever enough to synthesize the original objects completely from the data in the file. MFC supports a mechanism called serialization to help you to implement input and output of your class objects with a minimum of time and effort.

The basic idea behind serialization is that any class that’s serializable should take care of storing and retrieving itself. This means that for your classes to be serializable — in the case of the Sketcher application, this will include the CElement class and the element classes you have derived from it — they must be able to write themselves to a file. This implies that for a class to be serializable, all the class types that are used to declare data members of the class must be serializable too.

SERIALIZING A DOCUMENT

This all sounds rather tricky, but the basic capability for serializing your document was built into the application by the Application Wizard right at the outset. The handlers for the File image Save, File image Save As, and File image Open menu items all assume that you want serialization implemented for your document, and already contain the code to support it. Take a look at the parts of the definition and implementation of CSketcherDoc that relate to creating a document using serialization.

Serialization in the Document Class Definition

The code in the definition of CSketcherDoc that enables serialization of a document object is shown shaded in the following fragment:

class CSketcherDoc : public CDocument
{
protected: // create from serialization only
   CSketcherDoc();
   DECLARE_DYNCREATE(CSketcherDoc)
        
// Rest of the class...
        
// Overrides
public:
   virtual BOOL OnNewDocument();
   virtual void Serialize(CArchive& ar);
        
// Rest of the class...
        
};

There are three things here that relate to serializing a document object:

  1. The DECLARE_DYNCREATE() macro.
  2. The Serialize() member function.
  3. The default class constructor.

DECLARE_DYNCREATE() is a macro that enables objects of the CSketcherDoc class to be created dynamically by the application framework during the serialization input process. It’s matched by a complementary macro, IMPLEMENT_DYNCREATE(), in the class implementation. These macros apply only to classes derived from CObject, but as you will see shortly, they aren’t the only pair of macros that can be used in this context. For any class that you want to serialize, CObject must be a direct or indirect base because it adds the functionality that enables serialization to work. This is why the CElement class was derived from CObject. Almost all MFC classes are derived from CObject and are serializable.

The CSketcherDoc class definition also includes a declaration for a virtual function Serialize(). Every class that’s serializable must include this function. It’s called to perform both input and output serialization operations on the data members of the class. The object of type CArchive that’s passed as an argument determines whether the operation that is to occur is input or output. You’ll explore this in more detail when considering the implementation of serialization for the document class.

Note that the class explicitly defines a default constructor. This is also essential for serialization to work because the default constructor is used by the framework to synthesize an object when reading it from a file. The synthesized object produced by the no-arg constructor is filled out with the data from the file to set the values of the data members of the object.

Serialization in the Document Class Implementation

There are two bits of the SketcherDoc.cpp file that relate to serialization. The first is the IMPLEMENT_DYNCREATE() macro that complements the DECLARE_DYNCREATE() macro:

// SketcherDoc.cpp : implementation of the CSketcherDoc class
//
        
#include "stdafx.h"
// SHARED_HANDLERS can be defined in an ATL project implementing preview, 
// thumbnail and search filter handlers and allows sharing of document code
// with that project.
#ifndef SHARED_HANDLERS
#include "Sketcher.h"
#endif
        
#include "SketcherDoc.h"
#include "PenDialog.h"
        
#include <propkey.h>
        
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
        
// CSketcherDoc
        
        
IMPLEMENT_DYNCREATE(CSketcherDoc, CDocument)
        
// Message map and the rest of the file...

This macro defines the base class for CSketcherDoc as CDocument. This is required for the proper dynamic creation of a CSketcherDoc object, including members that are inherited.

The Serialize() Function

The class implementation includes the definition of the Serialize() function:

void CSketcherDoc::Serialize(CArchive& ar)
{
   if (ar.IsStoring())
   {
      // TODO: add storing code here
   }
   else
   {
      // TODO: add loading code here
   }
}

This function serializes the data members of the class. The argument to the function, ar, is a reference to an object of the CArchive class. The IsStoring() member of this class object returns TRUE if the operation is to store data members in a file, and FALSE if the operation is to read back data members from a previously stored document.

Because the Application Wizard has no knowledge of what data your document contains, the process of writing and reading this information is up to you, as indicated by the comments. To understand how you do this, let’s look a little more closely at the CArchive class.

The CArchive Class

The CArchive class is the engine that drives the serialization mechanism. It provides an MFC-based equivalent of the stream operations in C++ that you used for reading from the keyboard and writing to the screen in the console program examples. A CArchive object provides a mechanism for streaming your objects to a file, or recovering them as an input stream, automatically reconstituting the objects of your class in the process.

A CArchive object has a CFile object associated with it that provides disk input/output capability for binary files, and provides the connection to the physical file. Within the serialization process, the CFile object takes care of all the specifics of the file input and output operations, and the CArchive object deals with the logic of structuring the object data to be written, or reconstructing the objects from the information read. You need to worry about the details of the associated CFile object only if you are constructing your own CArchive object. With the document in Sketcher, the framework has already taken care of it and passes the CArchive object that it constructs, ar, to the Serialize() function in CSketcherDoc. You’ll be able to use the same object in each of the Serialize() functions you add to the element classes when you implement serialization for them.

The CArchive class overloads the extraction and insertion operators (>> and <<) for input and output operations, respectively, on objects of classes derived from CObject, plus a range of basic data types. These overloaded operators work with the object types and primitive types shown in the following table.

TYPE DEFINITION
bool Boolean value, true or false
float Standard single precision floating point
double Standard double precision floating point
BYTE 8-bit unsigned integer
char 8-bit character
wchar_t 16-bit character
short 16-bit signed integer
int 32-bit signed integer
LONG and long 32-bit signed integer
LONGLONG 64-bit signed integer
ULONGLONG 64-bit unsigned integer
WORD 16-bit unsigned integer
DWORD and unsigned int 32-bit unsigned integer
CString A CString object defining a string
SIZE and CSize An object defining a size as a cx, cy pair
POINT and CPoint An object defining a point as an x, y pair
RECT and CRect An object defining a rectangle by its upper-left and lower-right corners
CObject* Pointer to CObject

For basic data types in your objects, you use the insertion and extraction operators to serialize the data. To read or write an object of a serializable class that you have derived from CObject, you can either call the Serialize() function for the object, or use the extraction or insertion operator. Whichever way you choose must be used consistently for both input and output, so you should not output an object using the insertion operator and then read it back using the Serialize() function, or vice versa.

Where you don’t know the type of an object when you read it, as in the case of the pointers in the list of elements in our document, for example, you must only use the Serialize() function. This brings the virtual function mechanism into play, so the appropriate Serialize() function for the type of object pointed to is determined at run time.

A CArchive object is constructed either for storing objects or for retrieving objects. The CArchive function IsStoring() returns TRUE if the object is for output, and FALSE if the object is for input. You saw this used in the Serialize() member of CSketcherDoc.

There are many other member functions of the CArchive class that are concerned with the detailed mechanics of the serialization process, but you don’t usually need to know about them to use serialization.

Functionality of CObject-Based Classes

There are three levels of functionality available in your classes when they’re derived from the MFC class CObject. The level you get in your class is determined by which of three different macros you use in the definition of your class:

MACRO FUNCTIONALITY
DECLARE_DYNAMIC() Support for runtime class information
DECLARE_DYNCREATE() Support for runtime class information and dynamic object creation
DECLARE_SERIAL() Support for runtime class information, dynamic object creation, and serialization of objects

Each of these macros requires that a complementary macro, named with the prefix IMPLEMENT_ instead of DECLARE_, be placed in the file containing the class implementation. As the table indicates, the macros provide progressively more functionality, so I’ll concentrate on the third macro, DECLARE_SERIAL(), because it provides everything that the preceding macros do and more. This is the macro you should use to enable serialization in your own classes. It requires that the macro IMPLEMENT_SERIAL() be added to the file containing the class implementation.

You may be wondering why the document class uses DECLARE_DYNCREATE() and not DECLARE_SERIAL(). The DECLARE_DYNCREATE() macro provides the capability for dynamic creation of the objects of the class in which it appears. The DECLARE_SERIAL() macro provides the capability for serialization of the class, plus the dynamic creation of objects of the class, so it incorporates the effects of DECLARE_DYNCREATE(). Your document class doesn’t need serialization because the framework only has to synthesize the document object and then restore the values of its data members; however, the data members of a document do need to be serializable, because this is the process used to store and retrieve them.

The Macros that Add Serialization to a Class

With the DECLARE_SERIAL() macro in the definition of your CObject-based class, you get access to the serialization support provided by CObject. This includes special new and delete operators that incorporate memory leak detection in debug mode. You don’t need to do anything to use this because it works automatically. The macro requires the class name to be specified as an argument, so for serialization of the CElement class, you would add the following line to the class definition:

DECLARE_SERIAL(CElement)

It doesn’t matter where you put the macro within the class definition, but if you always put it as the first line, you’ll always be able to verify that it’s there, even when the class definition involves a lot of lines of code.

The IMPLEMENT_SERIAL() macro that you place in the implementation file for the class requires three arguments. The first argument is the class name, the second is the name of the direct base class, and the third argument is an unsigned 32-bit integer identifying a schema number, or version number, for your program. This schema number allows the serialization process to guard against problems that can arise if you write objects with one version of a program and read them with another, in which the classes may be different.

For example, you could add the following line to the source file containing the implementation of the CElement class:

IMPLEMENT_SERIAL(CElement, CObject, 1001)

If you subsequently modify the class definition, you would change the schema number to something different, such as 1002. If the program attempts to read data that was written with a different schema number from that in the currently active program, an exception is thrown. The best place for this macro is as the first line following the #include directives and any initial comments in the .cpp file.

Where CObject is an indirect base class, as in the case of the CLine class for example, each class in the hierarchy must have the serialization macros added for serialization to work in the top-level class. For serialization in CLine to work, the macros must also be added to CElement.

How Serialization Works

The overall process of serializing a document is illustrated in a simplified form in Figure 17-1.

image

FIGURE 17-1

The Serialize() function in the document object calls the Serialize() function (or uses an overloaded insertion operator) for each of its data members. Where a member is a class object, the Serialize() function for that object serializes each of its data members in turn until eventually, basic data types are written to the file. Because most classes in MFC ultimately derive from CObject, they contain serialization support, so you can almost always serialize MFC class objects.

The data that you’ll deal with in the Serialize() member functions of your classes and the application document object are just the data members. The structure of the classes that are involved and any other data necessary to reconstitute your original objects are automatically taken care of by the CArchive object.

Where you derive multiple levels of classes from CObject, the Serialize() function in a class must call the Serialize() member of its direct base class to ensure that the direct base class data members are serialized. Note that serialization doesn’t support multiple inheritance, so there can only be one base class for each class defined in a hierarchy.

How to Implement Serialization for a Class

From the previous discussion, I can summarize the steps that you need to take to add serialization to a class:

  1. Make sure that the class is derived directly or indirectly from CObject.
  2. Add the DECLARE_SERIAL() macro to the class definition (and to the direct base class if the direct base is not CObject or another standard MFC class).
  3. Declare the Serialize() function as a member function of your class.
  4. Add the IMPLEMENT_SERIAL() macro to the file containing the class implementation.
  5. Implement the Serialize() function for your class.

Now let’s look at how you can implement serialization for documents in Sketcher.

APPLYING SERIALIZATION

To implement serialization in Sketcher, you must implement the Serialize() function in CSketcherDoc so that it deals with all of the data members of that class. You need to add serialization to each of the classes that define objects that may be included in a document. Before you start adding serialization to your classes, you should make some small changes to the program to record when a user changes a sketch document. This isn’t essential, but it is highly desirable because it enables the program to guard against the document being closed without saving changes.

Recording Document Changes

There’s already a mechanism for noting when a document changes. It uses an inherited member of the CSketcherDoc class, SetModifiedFlag(). By calling this function consistently whenever the document changes, you can record the fact that the document has been altered in a data member of the document class object. This causes a prompt to be automatically displayed when you try to exit the application without saving the modified document. The argument to the SetModifiedFlag() function is a value of type BOOL, and the default value is TRUE. When you have occasion to specify that the document was unchanged, you can call this function with the argument FALSE.

At present, there are four occasions when you alter a sketch in the document object:

  • When you call the AddElement() member of CSketcherDoc to add a new element.
  • When you call the DeleteElement() member of CSketcherDoc to delete an element.
  • When you call SendToBack() for the document object.
  • When you move an element.

You can handle these situations easily. All you need to do is add a call to SetModifiedFlag() to each of the functions involved in these operations. The definition of AddElement() appears in the CSketcherDoc class definition. You can extend this to:

void AddElement(std::shared_ptr<CElement>& pElement)  // Add an element to the list
{
  m_Sketch.push_back(pElement); 
  UpdateAllViews(nullptr, 0, pElement.get());        // Tell all the views
  SetModifiedFlag();                                 // Set the modified flag
}

The definition of DeleteElement() is also in the CSketcherDoc definition. You should add one line to it, as follows:

void DeleteElement(std::shared_ptr<CElement>& pElement) 
{
  m_Sketch.remove(pElement);
  UpdateAllViews(nullptr, 0,  pElement.get());       // Tell all the views
  SetModifiedFlag();                                 // Set the modified flag
}

The SendToBack() function needs to have the same line added:

void SendToBack(std::shared_ptr<CElement>& pElement)
{
  if(pElement)
  {
    m_Sketch.remove(pElement);                // Remove the element from the list
    m_Sketch.push_back(pElement);             // Put a copy at the end of the list
    SetModifiedFlag();                        // Set the modified flag
  }
}

Moving an element occurs in the MoveElement() member of a view object that is called by the handler for the WM_MOUSEMOVE message, but you only change the document when the left mouse button is pressed. If there’s a right-button click, the element is put back to its original position, so you only need to call the SetModifiedFlag() function for the document in the OnLButtonDown() function:

void CSketcherView::OnLButtonDown(UINT nFlags, CPoint point)
{
  CClientDC aDC{this};                                // Create a device context
  OnPrepareDC(&aDC);                                  // Get origin adjusted
  aDC.DPtoLP(&point);                                 // convert point to Logical
  CSketcherDoc* pDoc {GetDocument()};                 // Get a document pointer
        
  if(m_MoveMode)
  { // In moving mode, so drop the element
    m_MoveMode = false;                               // Kill move mode
    auto pElement(m_pSelected);                       // Store selected address
    m_pSelected.reset();                              // De-select the element
    pDoc->UpdateAllViews(nullptr, 0, pElement.get()); // Redraw all the views
    pDoc->SetModifiedFlag();                          // Set the modified flag
  }
  // Rest of the function as before...
}

You call the inherited GetDocument() member of the view class to get access to a pointer to the document object and then use this pointer to call the SetModifiedFlag() function.

You now have all the places where you change the sketch. The document object also stores the element type, the element color and the pen width, so you need to track when they change too. Here’s how you can update OnColorBlack() for example:

void CSketcherDoc::OnColorBlack()
{
   m_Color = ElementColor::BLACK;                // Set the drawing color to black
   SetModifiedFlag();                            // Set the modified flag
}

Add the same statement to each of the handlers for the other colors and the element types. The handler for setting the pen width needs to be updated like this:

void CSketcherDoc::OnPenWidth()
{
     CPenDialog aDlg;                     // Create a local dialog object
    aDlg.m_PenWidth = m_PenWidth;         // Set pen width as that in the document
 
    if(aDlg.DoModal() == IDOK)             // Display the dialog as modal
    {
      m_PenWidth = aDlg.m_PenWidth;       // When closed with OK, get the pen width
     SetModifiedFlag();                   // Set the modified flag
    }
}

If you build and run Sketcher, and modify a document or add elements to it, you’ll now get a prompt to save the document when you exit the program. Of course, the File image Save menu option doesn’t do anything yet except clear the modified flag and save an empty file to disk. You must implement serialization to get the document written to disk, and that’s the next step.

Serializing the Document

The first step is to implement the Serialize() function for the CSketcherDoc class. Within this function, you must add code to serialize the data members of CSketcherDoc. The data members that you have declared in the class are as follows:

protected:
  ElementType m_Element{ElementType::LINE };        // Current element type
  ElementColor m_Color{ ElementColor::BLACK };      // Current drawing color
  std::list<std::shared_ptr<CElement>> m_Sketch;    // A list containing the sketch
  int m_PenWidth{};                                 // Current pen width
  CSize m_DocSize { CSize{ 3000, 3000 } };          // Document size

These have to be serialized to allow a CSketcherDoc object to be deserialized. You need to insert the statements to store and retrieve these data members in the Serialize() member of the class. However, there is a slight problem. The list< std::shared_ptr<CElement>> object is not serializable because the template is not derived from CObject. In fact, none of the STL containers are serializeable, so you always have to take care of serializing STL containers yourself.

All is not lost, however. If you can serialize the objects that the pointers in the container point to, you will be able to reconstruct the container when you read it back.

You can implement serialization for the document object with the following code:

void CSketcherDoc::Serialize(CArchive& ar)
{
  if (ar.IsStoring())
  {
    ar << static_cast<COLORREF>(m_Color)        // Store the current color
       << static_cast<int>(m_Element)           // the element type as an integer
       << m_PenWidth                            // and the current pen width
       << m_DocSize;                            // and the current document size
       
    ar << m_Sketch.size();             // Store the number of elements in the list
 
    // Now store the elements from the list
    for(const auto& pElement : m_Sketch)
      ar << pElement.get();                     // Store the element pointer
  }
  else
  {
    COLORREF color {};
    int elementType {};
    ar >> color                                 // Retrieve the current color
       >> elementType                           // the element type as an integer
       >> m_PenWidth                            // and the current pen width
       >> m_DocSize;                            // and the current document size
    m_Color = static_cast<ElementColor>(color);
    m_Element = static_cast<ElementType>(elementType);
 
    // Now retrieve all the elements and store in the list
    size_t elementCount {};                     // Count of number of elements
    ar >> elementCount;                         // retrieve the element count
    CElement* pElement;
    for(size_t i {} ; i < elementCount ; ++i)
    {
      ar >> pElement;
      m_Sketch.push_back(std::shared_ptr<CElement>(pElement));
    }
  }
}

For four of the data members, you just use the extraction and insertion operators that are overloaded in the CArchive class. This won’t work for m_Color because the ElementColor type is not serializable. However, you can cast it to type COLORREF, which is serializable because type COLORREF is the same as type long. The m_Element member is of type ElementType, and the serialization process won’t handle this directly either. However, you can cast it to an integer for serialization, and then just deserialize it as an integer before casting the value back to ElementType.

For the list of elements, m_Sketch, you first store the count of the number of elements in the list because you will need this to be able to read the elements back. You then write the element pointers that are contained in the shared_ptr objects from the list to the archive in the for loop. Something remarkable happens as a result. The serialization mechanism recognizes that the objects pointed to will be required to reconstruct the document, and will take care of writing those to the archive.

The else clause for the if deals with reading the document object back from the archive. You use the extraction operator to retrieve the first four members from the archive in the same sequence that they were written. The color and element type are read into the local integer variables, color and elementType, and then stored in the m_Color and m_Element members as the correct type.

You read the number of elements recorded in the archive and store it locally in elementCount. Finally, you use elementCount to control the for loop that reads the elements back from the archive and stores them in the list. Note that you don’t need to do anything special to take account of the fact that the elements were originally created on the heap. The serialization mechanism takes care of restoring the elements on the heap automatically; all you need to do is pass the pointer for each element to the shared_ptr<CElement> constructor.

In case you are wondering where the list<shared_ptr<CElement>> object comes from when you are deserializing an object, it will be created by the serialization process using the default constructor for the CSketcherDoc class. This is how the basic document object and its uninitialized data members get created. Like magic, isn’t it?

That’s all you need for serializing the document class data members, but serializing elements from the list causes the Serialize() functions for the element classes to be called to store and retrieve the elements themselves, so you also need to implement serialization for those classes.

Serializing the Element Classes

All the element classes are serializable in principle because their base class, CElement, is derived from CObject. You specified CObject as the base for CElement solely to get support for serialization. Make sure that the default constructor is defined for each of the element classes. The deserialization process requires that this constructor be defined.

You can add support for serialization to each of the element classes by adding the appropriate macros to the class definitions and implementations, and adding the code to the Serialize() member function of each class to serialize its data members. You can start with the base class, CElement, where you need to modify the class definition as follows:

class CElement: public CObject
{
DECLARE_SERIAL(CElement)
protected:
  CPoint m_StartPoint;                         // Element position      
  int m_PenWidth;                              // Pen width
  COLORREF m_Color;                              // Color of an element
  CRect m_EnclosingRect;                         // Rectangle enclosing an element
 
public:
  virtual ~CElement();
  virtual void Draw(CDC* pDC, std::shared_ptr<CElement> pElement=nullptr) {} 
  virtual void Move(const CSize& aSize) {}       // Move an element
  virtual void Serialize(CArchive& ar)  override; // Serialize object
 
 
  // Get the element enclosing rectangle 
  const CRect& GetEnclosingRect() const 
  {
    return m_EnclosingRect;
  }
 
protected:
  // Constructors protected so they cannot be called outside the class
  CElement();              
  CElement(const CPoint& start, COLORREF color, int penWidth = 1);
 
  // Create a pen
  void CreatePen(CPen& aPen, std::shared_ptr<CElement> pElement)
  {
  if(!aPen.CreatePen(PS_SOLID, m_PenWidth,
                              (this == pElement.get()) ? SELECT_COLOR : m_Color))
   {
        // Pen creation failed
        AfxMessageBox(_T("Pen creation failed."), MB_OK); 
        AfxAbort();
   }
 }
};

You add the DECLARE_SERIAL() macro and a declaration for the virtual function Serialize(). You already have the default constructor that was created by the Application Wizard. You changed it to protected in the class, although it doesn’t matter what its access specification is as long as it appears explicitly in the class definition. It can be public, protected, or private, and serialization still works. If you forget to include a default constructor in a class, though, you’ll get an error message when the IMPLEMENT_SERIAL() macro is compiled.

You should add the DECLARE_SERIAL() macro to each of the derived classes CLine, CRectangle, CCircle, CCurve, and CText, with the relevant class name as the argument. You should also add an override declaration for the Serialize() function as a public member of each class.

In the file Element.cpp, you must add the following macro at the beginning:

IMPLEMENT_SERIAL(CElement, CObject, VERSION_NUMBER)

You can define the static constant VERSION_NUMBER in the Element.h file by adding the definition after the other static constant:

static const UINT VERSION_NUMBER {1001};     // Version number for serialization

You can then use the same constant when you add the macro to the .cpp file for each of the other element classes. For instance, for the CLine class you should add the line,

IMPLEMENT_SERIAL(CLine, CElement, VERSION_NUMBER)

and similarly for the other element classes. When you modify any of the classes relating to the document, all you need to do is change the definition of VERSION_NUMBER in the Element.h file, and the new version number applies in all your Serialize() functions.

The Serialize() Functions for the Element Classes

You can now implement the Serialize() member function for each of the element classes. Start with the CElement class and add the following definition to Element.cpp:

void CElement::Serialize(CArchive& ar)
{
  CObject::Serialize(ar);              // Call the base class function
        
  if (ar.IsStoring())
   { // Writing to the file
     ar << m_StartPoint                // Element position 
        << m_PenWidth                  // The pen width
        << m_Color                     // The element color
        << m_EnclosingRect;            // The enclosing rectangle
  }
  else
  {  // Reading from the file
     ar >> m_StartPoint                // Element position 
        >> m_PenWidth                  // The pen width
        >> m_Color                     // The element color
        >> m_EnclosingRect;            // The enclosing rectangle
  }
}

This function is of the same form as the one supplied for you in the CSketcherDoc class. All of the data members defined in CElement are supported by the overloaded extraction and insertion operators and so everything is done using those operators. Note that you must call the Serialize() member for the CObject class to ensure that the inherited data members are serialized.

For the CLine class, you can code the function as:

void CLine::Serialize(CArchive& ar)
{
  CElement::Serialize(ar);             // Call the base class function
        
  if (ar.IsStoring())
  { // Writing to the file
    ar << m_EndPoint;                  // The end point
  }
  else
  { // Reading from the file
    ar >> m_EndPoint;                  // The end point 
  }
}

The data member is supported by the extraction and insertion operators of the CArchive object ar. You call the Serialize() member of the base class CElement to serialize its data members, and this calls the Serialize() member of CObject. You can see how serialization cascades through the class hierarchy.

The Serialize() member of the CRectangle class is simple:

void CRectangle::Serialize(CArchive& ar)
{
  CElement::Serialize(ar);             // Call the base class function
  if (ar.IsStoring())
  { // Writing to the file
    ar << m_BottomRight;               // Bottom-right point for the rectangle
  }
  else
  { // Reading from the file
    ar >> m_BottomRight;
  }
}

This calls the direct base class Serialize() function and serializes the bottom-right point for the rectangle.

The function implementation for the CCircle class is identical to that for the CRectangle class:

void CCircle::Serialize(CArchive& ar)
{
  CElement::Serialize(ar);             // Call the base class function
  if (ar.IsStoring())
  { // Writing to the file
    ar << m_BottomRight;               // Bottom-right point for the circle 
  }
  else
  { // Reading from the file
    ar >> m_BottomRight;
  }
}

For the CCurve class, you have rather more work to do. The CCurve class uses a vector<CPoint> container to store the defining points, and because this is not directly serializable, you must take care of it yourself. Having serialized the document, this is not going to be terribly difficult. You can code the Serialize() function as follows:

void CCurve::Serialize(CArchive& ar)
{
  CElement::Serialize(ar);             // Call the base class function
  // Serialize the vector of points
  if (ar.IsStoring())
  {
    ar << m_Points.size();             // Store the point count
    // Now store the points
    for)const auto& Point : m_Points)
      ar << point;
  }
  else
  {
    size_t nPoints {};                 // Stores number of points
    ar >> nPoints;                     // Retrieve the number of points
    // Now retrieve the points
    CPoint point;
    for(size_t i {} i < nPoints; ++i)
    {
      ar >> point;
      m_Points.push_back(point);
    }
  }
}

You first call the base class Serialize() function to deal with serializing the inherited members of the class. The technique for storing the contents of the vector is basically the same as you used for serializing the list for the document. You first write the number of elements in the container to the archive, then the elements themselves. The CPoint class is serializable, so it takes care of itself. Reading the points back is equally straightforward. You just store each object read from the archive in the vector, m_Points, in the for loop. The serialization process uses the no-arg constructor for the CCurve class to create the basic class object, so the vector<CPoint> member is created within this process.

The last class for which you need to add a Serialize() implementation is CText:

void CText::Serialize(CArchive& ar)
{
  CElement::Serialize(ar);             // Call the base class function
        
  if (ar.IsStoring())
  {
      ar << m_String;                  // Store the text string
  }
  else
  {
      ar >> m_String;                  // Retrieve the text string
  }
}

After calling the base class function, you serialize the m_String data member using the insertion and extraction operators for ar. Although CString is not derived from CObject, the CString class is still fully supported by CArchive with these overloaded operators.

EXERCISING SERIALIZATION

That’s all you have to do to implement the storing and retrieving of documents in the Sketcher program! The Save and Open menu options in the file menu are now fully operational without adding any more code. If you build and run Sketcher after incorporating the changes I’ve discussed in this chapter, you’ll be able to save and restore files and be automatically prompted to save a modified document when you try to close it or exit from the program without saving, as shown in Figure 17-2.

image

FIGURE 17-2

The prompting works because of the SetModifiedFlag() calls that you added everywhere you updated the document. Assuming you have not saved the file previously, if you click the Yes button in the screen shown in Figure 17-2, you’ll see the File image Save As dialog shown in Figure 17-3.

image

FIGURE 17-3

This is the standard Windows dialog for this menu item. The dialog is fully working, supported by code supplied by the framework. The filename for the document has been generated from that assigned when the document was first opened, and the file extension is automatically defined as .ske. The application now has full support for file operations on documents. Easy, wasn’t it?

PRINTING A DOCUMENT

It’s time to take a look at how you can print a sketch. You already have a basic printing capability implemented in Sketcher, courtesy of the Application Wizard and the framework. The File image Print, File image Print Setup, and File image Print Preview menu items all work. Selecting the File image Print Preview menu item displays a window showing the current Sketcher document on a page, as shown in Figure 17-4.

image

FIGURE 17-4

Whatever is in the current document is placed on a single sheet of paper at the current view scale. If the document’s extent is beyond the boundary of the paper, the section of the document off the paper won’t be printed. If you select the Print button, this page is sent to your printer.

As a basic capability that you get for free, it’s quite impressive, but it’s not adequate for our purposes. A typical document in Sketcher may well extend beyond a page, so you would either want to scale the document to fit, or, perhaps more conveniently, print the whole document over as many pages as necessary. You can add your own print processing code to extend the capability of the facilities provided by the framework, but to implement this you first need to understand how printing has been implemented in MFC.

The Printing Process

Printing a document is controlled by the current view. The process is inevitably a bit messy because printing is inherently a messy business, and it potentially involves you in overriding quite a number of inherited functions in your view class. Figure 17-5 shows the logic of the process and the functions involved. It also shows how the sequence of events is controlled by the framework and how printing a document involves calling five inherited members of your view class, which you may need to override. The CDC member functions shown on the left side of the diagram communicate with the printer device driver and are called automatically by the framework.

image

FIGURE 17-5

The typical role of each of the functions in the current view during a print operation is specified in the notes alongside it. The sequence in which they are called is indicated by the numbers on the arrows. In practice, you don’t need to implement all of these functions, only those that you want to for your particular printing requirements. Typically, you’ll want at least to implement your own versions of OnPreparePrinting(), OnPrepareDC(), and OnPrint(). You’ll see an example of how these functions can be implemented in the context of Sketcher a little later in this chapter.

You write data to a printer in the same way as you write data to the display — through a device context. The GDI calls that you use to output text or graphics are device-independent, so they work just as well for a printer as they do for a display. The only difference is the physical output device to which the CDC object applies.

The CDC functions in Figure 17-5 communicate with the device driver for the printer. If the document to be printed requires more than one printed page, the process loops back to call the OnPrepareDC() function for each successive new page, as determined by the EndPage() function. All the functions in your view class that are involved in the printing process are passed a pointer to a CPrintInfo object that provides a link between all the functions that manage the printing process, so let’s take a look at the CPrintInfo class in more detail.

The CPrintInfo Class

A CPrintInfo object has a fundamental role in the printing process because it stores information about the print job being executed and details of its status at any time. It also provides functions for accessing and manipulating this data. This object is the means by which information is passed from one view function to another during printing, and between the framework and your view functions.

A CPrintInfo object is created whenever you select the File image Print or File image Print Preview menu options. It will be used by each of the functions in the current view that are involved in the printing process, and it is automatically deleted when the print operation ends.

All the data members of CPrintInfo are public. The ones we are interested in for printing sketches are shown in the following table.

MEMBER USAGE
m_pPD A pointer to the CPrintDialog object that displays the Print dialog.
m_bDirect This is set to TRUE by the framework if the print operation is to bypass the Print dialog; otherwise, FALSE.
m_bPreview A member of type BOOL that has the value TRUE if File image Print Preview was selected; otherwise, FALSE.
m_bContinuePrinting A member of type BOOL. If it is TRUE, the framework continues the printing loop shown in the diagram. If it’s FALSE, the printing loop ends. You only need to set this variable if you don’t pass a page count for the print operation to the CPrintInfo object (using the SetMaxPage() member). In this case, you’ll be responsible for signaling when you are finished by setting this variable to FALSE.
m_nCurPage A value of type UINT that stores the page number of the current page. Pages are usually numbered starting from 1.
m_nNumPreviewPages A value of type UINT that specifies the number of pages displayed in the Print Preview window. This can be 1 or 2.
m_lpUserData This is of type LPVOID and stores a pointer to an object that you create. This allows you to create an object to store additional information about the printing operation and associate it with the CPrintInfo object.
m_rectDraw A CRect object that defines the usable area of the page in logical coordinates.
m_strPageDesc A CString object containing a format string used by the framework to display page numbers during print preview.

A CPrintInfo object has the public member functions shown in the following table.

FUNCTION DESCRIPTION
SetMinPage(
UINT nMinPage)
The argument specifies the number of the first page of the document. There is no return value.
SetMaxPage(
UINT nMaxPage)
The argument specifies the number of the last page of the document. There is no return value.
GetMinPage() const Returns the number of the first page of the document as type UINT.
GetMaxPage() const Returns the number of the last page of the document as type UINT.
GetFromPage() const Returns the number of the first page of the document to be printed as type UINT. This value is set through the print dialog.
GetToPage() const Returns the number of the last page of the document to be printed as type UINT. This value is set through the print dialog.

When you’re printing a document consisting of several pages, you need to figure out how many printed pages the document will occupy, and store this information in the CPrintInfo object to make it available to the framework. You do this in your version of the OnPreparePrinting() member of the current view.

Page numbers are stored as type UINT. To set the number of the first page in the document, you call the SetMinPage() function for the CPrintInfo object, which accepts the page number as the argument. There’s no return value. To set the number of the last page, you call SetMaxPage(). If you later want to retrieve these values, you call the GetMinPage() and GetMaxPage() functions for the CPrintInfo object.

The page numbers that you supply are stored in the CPrintDialog object pointed to by the m_pPD member of the CPrintInfo object and displayed in the dialog that pops up when you select File image Print... from the menu. The user can then specify the numbers of the first and last pages that are to be printed. You can retrieve the page numbers entered by the user by calling the GetFromPage() and GetToPage() members of the CPrintInfo object. In each case, the value returned is of type UINT. The dialog automatically verifies that the numbers of the first and last pages to be printed are within the range you supplied by specifying the minimum and maximum pages of the document.

You now know what functions you can implement in the view class to manage printing for yourself, with the framework doing most of the work. You also know what information is available through the CPrintInfo object that is passed to the functions concerned with printing. You’ll get a much clearer understanding of the detailed mechanics of printing by implementing a basic multipage print capability for Sketcher documents.

IMPLEMENTING MULTIPAGE PRINTING

With the mapping mode in Sketcher set to MM_ANISOTROPIC, the unit of measure for the elements and the view extent is one hundredth of an inch. With the unit of size being a fixed physical measure, ideally you want to print objects at their actual size.

With the document size specified as 3000 × 3000 units, you can create documents up to 30 inches square, which spreads over quite a few sheets of paper if you fill the whole area. It requires a little more effort to work out the number of pages necessary to print a sketch than with a typical text document because in most instances you’ll need a two-dimensional array of pages to print a complete sketch document.

To avoid overcomplicating the problem, we will assume that you’re printing on a normal sheet of paper (either A4 size or 8 1/2 × 11 inches) and that you are printing in portrait orientation (which means the long edge is vertical). With either paper size, you’ll print the document in a central portion of the paper measuring 7.5 inches × 10 inches. With these assumptions, you don’t need to worry about the actual paper size; you just need to chop the document into 750 × 1000–unit chunks, where a unit is 0.01 inches. For a document larger than one page, you’ll divide up the document as illustrated in the example in Figure 17-6.

image

FIGURE 17-6

As you can see, you’ll be numbering the pages row-wise, so in this case, pages 1 to 4 are in the first row and pages 5 to 8 are in the second.

Getting the Overall Document Size

To figure out how many pages a particular document occupies, you need to know how big the sketch is, and for this, you want the rectangle that encloses everything in the document. You can do this easily by adding a function GetDocExtent() to the document class, CSketcherDoc. Add the following declaration to the public interface for CSketcherDoc:

CRect GetDocExtent() const;   // Get the bounding rectangle for the whole document

The implementation is no great problem. The code for it is:

// Get the rectangle enclosing the entire document
CRect CSketcherDoc::GetDocExtent()const
{
  if(m_Sketch.empty())                                   // Check for empty sketch
    return CRect {0,0,1,1};
  CRect docExtent {m_Sketch.front()->GetEnclosingRect()}; // Initial doc extent
  for(auto& pElement : m_Sketch) 
    docExtent.UnionRect(docExtent, pElement->GetEnclosingRect());
        
  docExtent.NormalizeRect();
  return docExtent;
}

You can add this function definition to the SketcherDoc.cpp file.

If the sketch is empty, you return a very small CRect object. The initial size of the document extent is the rectangle enclosing the first element in the list. The process then loops through every element in the document, getting the bounding rectangle for each element and combining it with docExtent. The UnionRect() member of the CRect class calculates the smallest rectangle that contains the two rectangles you pass as arguments, and stores that value in the CRect object for which the function is called. Therefore, docExtent keeps increasing in size until all the elements are contained within it.

Storing Print Data

The OnPreparePrinting() function in the view class is called by the application framework to enable you to initialize the printing process for your document. The basic initialization that’s required is to provide information about how many pages are in the document, which is information that the print dialog will display. You should also store information about the pages that your document requires, so you can use it later in the other view functions involved in the printing process. You can create an object of your own class type in the OnPreparePrinting() member of the view class to store this information and store a pointer to the object in the CPrintInfo object that the framework makes available. I adopted this approach primarily to show you how this mechanism works; in many cases you’ll find it easier just to store the data in your view object.

You’ll need to store the number of pages running the width of the document, m_nWidths, and the number of rows of pages down the length of the document, m_nLengths. You’ll also store the upper-left corner of the rectangle enclosing the document data as a CPoint object, m_DocRefPoint, because you’ll need this when you work out the position of a page to be printed from its page number. You can store the filename for the document in a CString object, m_DocTitle, so that you can add it as a title to each page. It will also be useful to record the size of the printable area within the page. The definition of the class to accommodate these is:

#pragma once
        
class CPrintData
{
public:
  UINT printWidth {1000};           // Printable page width - units 0.01 inches
  UINT printLength {1000};          // Printable page length - units 0.01 inches
  UINT m_nWidths;                   // Page count for the width of the document
  UINT m_nLengths;                  // Page count for the length of the document
  CPoint m_DocRefPoint;             // Top-left corner of the document contents
  CString m_DocTitle;               // The name of the document
        
};

The class definition specifies default values for the printable area that corresponds to an A4 page with half-inch margins all round. You can change this to suit your environment, and, of course, you can change the values programmatically in a CPrintData object.

Of course, you could define a constructor for the class and initialize the values of printWidth and printLength there. However, all the data members are public so you can always set their values directly. The CPrintData class is just a vehicle for packaging data items relating to the printing process so there is no necessity for complicating it. Specifying default values for members obviates the need to write constructors and thus simplifies the class definition.

You can add a new header file with the name PrintData.h to the project by right-clicking the Header Files folder in the Solution Explorer pane and then selecting Add image New Item from the pop-up. You can now enter the class definition in the new file. You don’t need an implementation file for this class. Because an object of this class is only going to be used transiently, you don’t need to use CObject as a base or to consider any other complication.

The printing process starts with a call to OnPreparePrinting() so I’ll explore how you should implement that next.

Preparing to Print

The Application Wizard has already added versions of OnPreparePrinting(), OnBeginPrinting(), and OnEndPrinting() to CSketcherView. The base code provided for OnPreparePrinting() calls DoPreparePrinting() in the return statement, as you can see:

BOOL CSketcherView::OnPreparePrinting(CPrintInfo* pInfo)
{
   // default preparation
   return DoPreparePrinting(pInfo);
}

The DoPreparePrinting() function displays the Print dialog using information about the number of pages to be printed that is defined in the CPrintInfo object. Whenever possible, you should calculate the number of pages to be printed and store it in the CPrintInfo object before this call occurs. Of course, in many circumstances, you may need information from the device context for the printer before you can do this — when you’re printing a document where the number of pages is going to be affected by the size of the font to be used for example — in which case it won’t be possible to get the page count before you call DoPreparePrinting(). In these circumstances you can compute the number of pages in the OnBeginPrinting() member, which receives a pointer to the device context as an argument. This function is called by the framework after OnPreparePrinting(), so the information entered in the Print dialog is available. This means that you can also take account of the paper size selected by the user in the Print dialog.

Assuming that the page size is large enough to accommodate a 7.5-inch × 10-inch area to draw the document data, you can calculate the number of pages in OnPreparePrinting(). The code to do this is:

BOOL CSketcherView::OnPreparePrinting{CPrintInfo* pInfo}
{
  CPrintData* printData {new CPrintData};   // Create a print data object
  CSketcherDoc* pDoc {GetDocument()};       // Get a document pointer
  CRect docExtent {pDoc->GetDocExtent()};   // Get the whole document area

  printData->m_DocRefPoint = docExtent.TopLeft();// Save document reference point 
  printData->m_DocTitle = pDoc->GetTitle();      // Save the document filename


  // Calculate how many printed page widths are required
  // to accommodate the width of the document
  printData->m_nWidths = static_cast<UINT>(ceil(
                 static_cast<double>(docExtent.Width())/printData->printWidth));

  // Calculate how many printed page lengths are required
  // to accommodate the document length
  printData->m_nLengths = static_cast<UINT>(
            ceil(static_cast<double>(docExtent.Height())/printData->printLength));

  // Set the first page number as 1 and
  // set the last page number as the total number of pages
  pInfo->SetMinPage(1);
  pInfo->SetMaxPage(printData->m_nWidths*printData->m_nLengths);
  pInfo->m_lpUserData = printData;         // Store address of PrintData object
  return DoPreparePrinting(pInfo);
}

You first create a CPrintData object on the heap and store its address locally in the pointer. After getting a pointer to the document, you get the rectangle enclosing all of the elements in the document by calling the function GetDocExtent() that you added to the document class earlier in this chapter. You then store the corner of this rectangle in the m_DocRefPoint member of the CPrintData object, and put the name of the file that contains the document in m_DocTitle.

The next two lines of code calculate the number of pages across the width of the document, and the number of pages required to cover the length. The number of pages to cover the width is computed by dividing the document width by the width of the print area on a page and rounding up to the next highest integer using the ceil() function that is declared in the cmath header. For example, ceil(2.1) returns 3.0, ceil(2.9) also returns 3.0, and ceil(-2.1) returns -2.0. A similar calculation to that for the number of pages across the width of a document produces the number to cover the length. The product of these two values is the total number of pages to be printed, and this is the value that you’ll supply for the maximum page number. The last step is to store the address of the CPrintData object in the m_lpUserData member of the pInfo object.

Don’t forget to add an #include directive for PrintData.h to the SketcherView.cpp file.

Cleaning Up after Printing

Because you created the CPrintData object on the heap, you must ensure that it’s deleted when you’re done with it. You do this by adding code to the OnEndPrinting() function:

void CSketcherView::OnEndPrinting(CDC* /*pDC*/, CPrintInfo* pInfo)
{
   // Delete our print data object
   delete static_cast<CPrintData*>(pInfo->m_lpUserData);
}

That’s all that’s necessary for this function in the Sketcher program, but in some cases, you’ll need to do more. Your one-time final cleanup should be done here. Make sure that you remove the comment delimiters (/* */) from the second parameter name; otherwise, your function won’t compile. The default implementation comments out the parameter names because you may not need to refer to them in your code. Because you use the pInfo parameter, you must uncomment it; otherwise, the compiler reports it as undefined. You don’t need to add anything to the OnBeginPrinting() function in the Sketcher program, but you’d need to add code to allocate any GDI resources, such as pens, if they were required throughout the printing process. You would then delete these as part of the clean-up process in OnEndPrinting().

Preparing the Device Context

At the moment, Sketcher calls OnPrepareDC()for the view object which sets up the mapping mode as MM_ANISOTROPIC to take account of the scaling factor. You must make some additional changes so that the device context is properly prepared in the case of printing:

void CSketcherView::OnPrepareDC(CDC* pDC, CPrintInfo* pInfo)
{
  CScrollView::OnPrepareDC(pDC, pInfo);
  CSketcherDoc* pDoc {GetDocument()};
  pDC->SetMapMode(MM_ANISOTROPIC);           // Set the map mode
  CSize DocSize {pDoc->GetDocSize()};        // Get the document size
  pDC->SetWindowExt(DocSize);                // Now set the window extent
        
  // Get the number of pixels per inch in x and y
  int xLogPixels {pDC->GetDeviceCaps(LOGPIXELSX)};
  int yLogPixels {pDC->GetDeviceCaps(LOGPIXELSY)};
        
  // Calculate the viewport extent in x and y
  int scale {pDC->IsPrinting() ? 1 : m_Scale}; // If we are printing, use scale 1
  int xExtent {(DocSize.cx*scale*xLogPixels)/100};
  int yExtent {(DocSize.cy*scale*yLogPixels)/100};
        
  pDC->SetViewportExt(xExtent, yExtent);     // Set viewport extent
}

This function is called by the framework for output to the printer as well as to the screen. You should make sure that a scale of 1 is used to set the mapping from logical coordinates to device coordinates when you’re printing. If you left everything as it was, the output would be at the current view scale, but you’d need to take account of the scale when calculating how many pages were required, and how you set the origin for each page.

You determine whether or not you have a printer device context by calling the IsPrinting() member of the current CDC object, which returns TRUE if you are printing. When you have a printer device context you set the scale to 1. Of course, you must change the statements calculating the viewport extent to use the local variable scale rather than the m_Scale member of the view.

Printing the Document

You write the data to the printer device context in the OnPrint() function. This is called once for each page. You need to add an override for this function to CSketcherView, using the Properties window for the class. Select OnPrint from the list of overrides and then click <Add> OnPrint in the right column.

You can obtain the page number of the page to be printed from the m_nCurPage member of the CPrintInfo object that is passed to the function. You can then use this value to work out the coordinates of the point in the document that corresponds to the upper-left corner of the current page. The way to do this is best understood using an example, so imagine that you are printing page 7 of an 8-page document, as illustrated in Figure 17-7.

image

FIGURE 17-7

Remember that these are in logical coordinates and x is positive from left to right and y is increasingly negative from top to bottom. You can get an index to the horizontal position of the page by decrementing the page number by 1 and taking the remainder after dividing by the number of page widths required for the width of the document. Multiplying the result by printWidth produces the x-coordinate of the upper-left corner of the page, relative to the upper-left corner of the rectangle enclosing the elements in the document. Similarly, you can determine the index to the vertical position of the document by dividing the current page number reduced by 1 by the number of page widths required for the horizontal width of the document. By multiplying the result by printLength, you get the relative y-coordinate of the upper-left corner of the page. You can express this in the following statements:

CPrintData* p {static_cast<CPrintData*>(pInfo->m_lpUserData)};
int xOrg {p->m_DocRefPoint.x {static_cast<int>( p->printWidth*
                                        ((pInfo->m_nCurPage - 1)%(p->m_nWidths)))};
int yOrg {p->m_DocRefPoint.y {static_cast<int>( p->printLength*
                                         ((pInfo->m_nCurPage - 1)/(p->m_nWidths)))};

It would be nice to print the filename of the document at the top of each page and, perhaps, a page number at the bottom, but you want to be sure you don’t print the document data over the filename and page number. You also want to center the printed area on the page. You can do this by moving the origin of the coordinate system in the printer device context after you have printed the filename. This is illustrated in Figure 17-8.

image

FIGURE 17-8

Figure 17-8 illustrates the correspondence between the printed page area in the device context and the page to be printed in the reference frame of the document data. The diagram shows the expressions for the offsets from the page origin for the printWidth by printLength area where you are going to print the page. You want to print the information from the document in the dashed area shown on the printed page in Figure 17-8, so you need to map the xOrg, yOrg point in the document to the position shown in the printed page, which is displaced from the page origin by the offset values xOffset and yOffset.

By default, the origin in the coordinate system that you use to define elements in the document is mapped to the origin of the device context, but you can change this. The CDC object provides a SetWindowOrg() function for this purpose. This enables you to define a point in the document’s logical coordinate system that you want to correspond to the origin in the device context, in this case the point where (0,0) for the printer output will be. It’s important to save the old origin that’s returned from the SetWindowOrg() function. You must restore the old origin when you’ve finished drawing the current page; otherwise the m_rectDraw member of the CPrintInfo object is not set up correctly when you print the next page.

The point in the document that you want to map to the origin of the page has the coordinates xOrg-xOffset,yOrg-yOffset. This may not be easy to visualize, but remember that by setting the window origin, you’re defining the point that maps to the viewport origin. If you think about it, you should see that the xOrg, yOrg point in the document is where you want it on the page.

The complete code for printing a page of the document is:

// Print a page of the document
void CSketcherView::OnPrint(CDC* pDC, CPrintInfo* pInfo)
{
  CPrintData* p{static_cast<CPrintData*>(pInfo->m_lpUserData)};

  // Output the document filename
  pDC->SetTextAlign(TA_CENTER);        // Center the following text
  pDC->TextOut(pInfo->m_rectDraw.right/2, 20, p->m_DocTitle);
  CString str; 
  str.Format(_T("Page %u") , pInfo->m_nCurPage);
  pDC->TextOut(pInfo->m_rectDraw.right/2, pInfo->m_rectDraw.bottom-20, str);
  pDC->SetTextAlign(TA_LEFT);          // Left justify text

  // Calculate the origin point for the current page
  int xOrg {static_cast<int>(p->m_DocRefPoint.x + 
                           p->printWidth*((pInfo->m_nCurPage - 1)%(p->m_nWidths)))};

  int yOrg {static_cast<int>( p->m_DocRefPoint.y +
                          p->printLength*((pInfo->m_nCurPage - 1)/(p->m_nWidths)))};


  // Calculate offsets to center drawing area on page as positive values
  int xOffset {static_cast<int>((pInfo->m_rectDraw.right - p->printWidth)/2)};
  int yOffset {static_cast<int>((pInfo->m_rectDraw.bottom - p->printLength)/2)};

  // Change window origin to correspond to current page & save old origin
  CPoint OldOrg {pDC->SetWindowOrg(xOrg - xOffset, yOrg - yOffset)};

  // Define a clip rectangle the size of the printed area
  pDC->IntersectClipRect(xOrg, yOrg, xOrg + p->printWidth, yOrg + p->printLength);

  OnDraw(pDC);                         // Draw the whole document
  pDC->SelectClipRgn(nullptr);         // Remove the clip rectangle
  pDC->SetWindowOrg(OldOrg);           // Restore old window origin
}

The first step is to initialize the local pointer, p, with the address of the CPrintData object that is stored in the m_lpUserData member of the object to which pInfo points. You then output the file-name that you squirreled away in the CPrintData object. The SetTextAlign() member function of the CDC object allows you to define the alignment of subsequent text output in relation to the reference point you supply for the text string in the TextOut() function. The alignment is determined by the constant passed as an argument to the function. You have three possibilities for specifying the horizontal alignment of the text, as shown in the following table.

CONSTANT ALIGNMENT
TA_LEFT The point is at the left of the bounding rectangle for the text, so the text is to the right of the point specified. This is the default alignment.
TA_RIGHT The point is at the right of the bounding rectangle for the text, so the text is to the left of the point specified.
TA_CENTER The point is at the center of the bounding rectangle for the text.

You define the x-coordinate of the filename on the page as half the page width, and the y-coordinate as 20 units, which is 0.2 inches, from the top of the page.

After outputting the name of the document file as centered text, you output the page number centered at the bottom of the page. You use the Format() member of the CString class to format the page number stored in the m_nCurPage member of the CPrintInfo object. This is positioned 20 units up from the bottom of the page. You then reset the text alignment to the default setting, TA_LEFT.

The SetTextAlign() function also allows you to change the position of the text vertically by OR-ing a second flag with the justification flag. The second flag can be any of those shown in the following table.

CONSTANT ALIGNMENT
TA_TOP Aligns the top of the rectangle bounding the text with the point defining the position of the text. This is the default.
TA_BOTTOM Aligns the bottom of the rectangle bounding the text with the point defining the position of the text.
TA_BASELINE Aligns the baseline of the font used for the text with the point defining the position of the text.

The next action in OnPrint() uses the method that I discussed for mapping an area of the document to the current page. You get the document drawn on the page by calling the OnDraw() function that is used to display the document in the view. This potentially draws the entire document, but you can restrict what appears on the page by defining a clip rectangle. A clip rectangle encloses a rectangular area in the device context within which output appears. Output is suppressed outside of the clip rectangle. It’s also possible to define irregularly shaped areas for clipping, called regions.

The initial default clipping area defined in the print device context is the page boundary. You define a clip rectangle that corresponds to the printWidth by printLength area centered in the page. This ensures that you draw only in this area and the filename and page number will not be overwritten.

After the current page has been drawn by the OnDraw() function call, you call SelectClipRgn() with a nullptr argument to remove the clip rectangle. If you don’t do this, output of the document title is suppressed on all pages after the first, because it lies outside the clip rectangle that would otherwise remain in effect in the print process until the next time IntersectClipRect() gets called.

Your final action is to call SetWindowOrg() again to restore the window origin to its original location, as discussed earlier in this chapter.

Getting a Printout of the Document

To get your first printed Sketcher document, you just need to build the project and execute the program (once you’ve fixed any typos). If you try File image Print Preview and click on the Two Page button, you should get something similar to the window shown in Figure 17-9.

image

FIGURE 17-9

You get print preview functionality completely for free. The framework uses the code that you’ve supplied for the normal multipage printing operation to produce page images in the Print Preview window. What you see in the Print Preview window should be exactly the same as appears on the printed page.

SUMMARY

In this chapter, you have implemented the capability to store a document on disk in a form that allows you to read it back and reconstruct its constituent objects using the serialization processes supported by MFC. You have also implemented the capability to print sketches in the Sketcher application. If you are comfortable with how serialization and printing work in Sketcher, you should have little difficulty with implementing serialization and printing in any MFC application.

EXERCISES

  1. Update the code in the OnPrint() function in Sketcher so that the page number is printed at the bottom of each page of the document in the form “Page n of m,” where n is the current page number and m is the total number of pages.
  2. As a further enhancement to the CText class in Sketcher, change the implementation so that scaling works properly with text. (Hint: Look up the CreatePointFont() function in the online help.)

WHAT YOU LEARNED IN THIS CHAPTER

TOPIC CONCEPT
Serialization Serialization is the process of transferring objects to a file. Deserialization reconstructs objects from data in a file.
MFC Serialization To serialize objects in an MFC application, you must identify the class as serializable. To do this you use the DECLARE_SERIAL()macro in the class definition and the IMPLEMENT_SERIAL()macro in the file containing the class implementation.
Serializable classes in an MFC application For a class to be serializable in an MFC application, it must have CObject as a direct or indirect base class, it must implement a no-arg constructor and implement the Serialize() function as a class member.
Printing with the MFC To provide the ability to print a document in an MFC application, you must implement your versions of the OnPreparePrinting(), OnBeginPrinting(), OnPrepareDC(), OnPrint(), and OnEndPrinting() functions in the document view class.
The CPrintInfo object A CPrintInfo object is created by the MFC framework to store information about the printing process. You can store the address of your own class object that contains print information in a pointer in the CPrintInfo object.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.58.246