[Previous] [Next]

A More Complex Printing Application

EZPrint is okay for a start, but it's hardly representative of the kinds of applications found in the real world. It doesn't have to deal with pagination because its documents contain one page each. It creates the GDI resources it needs each time OnDraw is called, so it doesn't use OnBeginPrinting and OnEndPrinting to allocate printer-specific resources. It doesn't override OnPrepareDC and OnPrint at all, because nothing in EZPrint distinguishes a printed view from an onscreen view.

The HexDump application shown in Figure 13-5 better represents the kinds of applications that you're likely to have to write. HexDump is a hexadecimal viewing program that displays the contents of any file in binary form. Printed pages have a header at the top that includes the file name (prefaced with a path name if there's room) and the page number. The header is underscored with a thin horizontal line. The line is drawn with CDC::MoveTo and CDC::LineTo; all other output is performed with CDC::TextOut. Figure 13-6 shows one page of a document in print preview mode. When printing a document, HexDump queries the printer for the dimensions of the printable page and adjusts its output accordingly. The page height is used to compute the number of lines printed per page, and the page width is used to center the output horizontally no matter what the page size or orientation.

Click to view at full size.

Figure 13-5. HexDump showing a binary view of a file.

Click to view at full size.

Figure 13-6. HexDump's print preview.

CHexView::OnDraw produces all of HexDump's screen output. To repaint the view, OnDraw calls CDC::GetClipBox to identify the rectangle that needs repainting, converts the y coordinates of the rectangle's top and bottom into starting and ending line numbers, and draws just those lines that need repainting. The font used in the output is a 10-point Courier New screen font initialized in CHexView::OnCreate. The current scroll position is factored into the output automatically because CHexView is derived from CScrollView. Because OnDraw does the minimum amount of painting necessary, scrolling performance is acceptable even if the document is very large. To see how sluggish a CScrollView can become when a large document is loaded and OnDraw isn't optimized, try rewriting OnDraw so that it attempts to draw the entire document each time it's called. All you have to do is replace these two lines:

UINT nStart = rect.top / m_cyScreen;
UINT nEnd = min (m_nLinesTotal - 1,
    (rect.bottom + m_cyScreen - 1) / m_cyScreen);

with these:

UINT nStart = 0;
UINT nEnd = m_nLinesTotal _ 1;

Then load a file whose size is 10 KB or 20 KB and do some scrolling up and down. It will quickly become apparent why OnDraw goes to the trouble of converting the clip box into a range of line numbers.

HexDump does all its printing in OnPrint. CHexView::OnPrint calls CHexView::PrintPageHeader to print the header at the top of the page and CHexView::PrintPage to print the body of the page. OnBeginPrinting sets the stage by initializing m_fontPrinter with a 10-point Courier New font sized for the printer (notice the printer device context pointer passed in CreatePointFont's third parameter), m_cyPrinter with the interline spacing, m_nLinesPerPage with the number of lines per page based on the page height, m_cxOffset with the x indent required to center printed lines on the page, and m_cxWidth with the width of each line of text. PrintPage calculates starting and ending line numbers from the current page number and the number of lines per page. The for loop that does the drawing is similar to the for loop in OnDraw, differing only in how it aligns the text on the page and in the fact that it uses m_fontPrinter for its output instead of m_fontScreen. When printing (or print previewing) is complete, OnEndPrinting cleans up by deleting the printer font created by OnBeginPrinting.

Could OnDraw have been written to handle both screen and printer output? Sure. But HexDump's code (Figure 13-7) is arguably simpler and more straightforward the way it's written now. MFC programmers sometimes make the mistake of feeling that they have to do their printing as well as their screen updating in OnDraw. HexDump not only demonstrates that it doesn't have to be that way but also provides a working example of an application that does its printing and screen updating separately.

Figure 13-7. The HexDump Program.

HexDoc.h

// HexDoc.h : interface of the CHexDoc class
//
///////////////////////////////////////////////////////////////////////////

#if !defined(AFX_HEXDOC_H__3A83FDFE_A3E6_11D2_8E53_006008A82731__INCLUDED_)
#define AFX_HEXDOC_H__3A83FDFE_A3E6_11D2_8E53_006008A82731__INCLUDED_

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


class CHexDoc : public CDocument
{
protected: // create from serialization only
    CHexDoc();
    DECLARE_DYNCREATE(CHexDoc)

// Attributes
public:
// Operations
public:
    UINT GetBytes(UINT nIndex, UINT nCount, PVOID pBuffer);
    UINT GetDocumentLength();

// Overrides
    // ClassWizard generated virtual function overrides
    //{{AFX_VIRTUAL(CHexDoc)
    public:
    virtual BOOL OnNewDocument();
    virtual void Serialize(CArchive& ar);
    virtual void DeleteContents();
    //}}AFX_VIRTUAL

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

protected:

// Generated message map functions
protected:
    BYTE* m_pFileData;
    UINT m_nDocLength;
    //{{AFX_MSG(CHexDoc)
       // NOTE - the ClassWizard will add and remove member functions here.
       //    DO NOT EDIT what you see in these blocks of generated code !
    //}}AFX_MSG
    DECLARE_MESSAGE_MAP()
};

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

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

#endif 
// !defined(AFX_HEXDOC_H__3A83FDFE_A3E6_11D2_8E53_006008A82731__INCLUDED_)

HexDoc.cpp

// HexDoc.cpp : implementation of the CHexDoc class
//

#include "stdafx.h"
#include "HexDump.h"

#include "HexDoc.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

///////////////////////////////////////////////////////////////////////////
// CHexDoc

IMPLEMENT_DYNCREATE(CHexDoc, CDocument)

BEGIN_MESSAGE_MAP(CHexDoc, CDocument)
    //{{AFX_MSG_MAP(CHexDoc)
        // NOTE - the ClassWizard will add and remove mapping macros here.
        //    DO NOT EDIT what you see in these blocks of generated code!
    //}}AFX_MSG_MAP
END_MESSAGE_MAP()

///////////////////////////////////////////////////////////////////////////
// CHexDoc construction/destruction

CHexDoc::CHexDoc()
{
    m_nDocLength = 0;
    m_pFileData = NULL;
}

CHexDoc::~CHexDoc()
{
}

BOOL CHexDoc::OnNewDocument()
{
    if (!CDocument::OnNewDocument())
        return FALSE;
    return TRUE;
}

//////////////////////////////////////////////////////////////////////////
// CHexDoc serialization

void CHexDoc::Serialize(CArchive& ar)
{
    if (ar.IsLoading ()) {
        CFile* pFile = ar.GetFile ();
        m_nDocLength = (UINT) pFile->GetLength ();

        //
        // Allocate a buffer for the file data.
        //
        try {
            m_pFileData = new BYTE[m_nDocLength];
        }
        catch (CMemoryException* e) {
            m_nDocLength = 0;
            throw e;
        }

        //
        // Read the file data into the buffer.
        //
        try {
            pFile->Read (m_pFileData, m_nDocLength);
        }
        catch (CFileException* e) {
            delete[] m_pFileData;
            m_pFileData = NULL;
            m_nDocLength = 0;
            throw e;
        }
    }
}

///////////////////////////////////////////////////////////////////////////
// CHexDoc diagnostics

#ifdef _DEBUG
void CHexDoc::AssertValid() const
{
    CDocument::AssertValid();
}

void CHexDoc::Dump(CDumpContext& dc) const
{
    CDocument::Dump(dc);
}
#endif //_DEBUG
///////////////////////////////////////////////////////////////////////////
// CHexDoc commands

void CHexDoc::DeleteContents() 
{
    CDocument::DeleteContents();

    if (m_pFileData != NULL) {
        delete[] m_pFileData;
        m_pFileData = NULL;
        m_nDocLength = 0;
    }
}

UINT CHexDoc::GetBytes(UINT nIndex, UINT nCount, PVOID pBuffer)
{
    if (nIndex >= m_nDocLength)
        return 0;

    UINT nLength = nCount;
    if ((nIndex + nCount) > m_nDocLength)
        nLength = m_nDocLength - nIndex;

    ::CopyMemory (pBuffer, m_pFileData + nIndex, nLength);
    return nLength;
}

UINT CHexDoc::GetDocumentLength()
{
    return m_nDocLength;
}

HexView.h

// HexView.h : interface of the CHexView class
//
///////////////////////////////////////////////////////////////////////////

#if !defined(
    AFX_HEXVIEW_H__3A83FE00_A3E6_11D2_8E53_006008A82731__INCLUDED_)
#define AFX_HEXVIEW_H__3A83FE00_A3E6_11D2_8E53_006008A82731__INCLUDED_

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

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

// Attributes
public:
    CHexDoc* GetDocument();

// Operations
public:

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

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

protected:

// Generated message map functions
protected:
    void FormatLine(CHexDoc* pDoc, UINT nLine, CString& string);
    void PrintPageHeader(CHexDoc* pDoc, CDC* pDC, UINT nPageNumber);
    void PrintPage(CHexDoc* pDoc, CDC* pDC, UINT nPageNumber);
    UINT m_cxWidth;
    UINT m_cxOffset;
    UINT m_nLinesPerPage;
    UINT m_nLinesTotal;
    UINT m_cyPrinter;
    UINT m_cyScreen;
    CFont m_fontPrinter;
    CFont m_fontScreen;
    //{{AFX_MSG(CHexView)
    afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
    //}}AFX_MSG
    DECLARE_MESSAGE_MAP()
};

#ifndef _DEBUG  // debug version in HexView.cpp
inline CHexDoc* CHexView::GetDocument()
    { return (CHexDoc*)m_pDocument; }
#endif

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

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

#endif 
// !defined(AFX_HEXVIEW_H__3A83FE00_A3E6_11D2_8E53_006008A82731__INCLUDED_)

HexView.cpp

// HexView.cpp : implementation of the CHexView class
//

#include "stdafx.h"
#include "HexDump.h"

#include "HexDoc.h"
#include "HexView.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

#define PRINTMARGIN 2

///////////////////////////////////////////////////////////////////////////
// CHexView
IMPLEMENT_DYNCREATE(CHexView, CScrollView)

BEGIN_MESSAGE_MAP(CHexView, CScrollView)
    //{{AFX_MSG_MAP(CHexView)
    ON_WM_CREATE()
    //}}AFX_MSG_MAP
    // Standard printing commands
    ON_COMMAND(ID_FILE_PRINT, CScrollView::OnFilePrint)
    ON_COMMAND(ID_FILE_PRINT_DIRECT, CScrollView::OnFilePrint)
    ON_COMMAND(ID_FILE_PRINT_PREVIEW, CScrollView::OnFilePrintPreview)
END_MESSAGE_MAP()

///////////////////////////////////////////////////////////////////////////
// CHexView construction/destruction

CHexView::CHexView()
{
}

CHexView::~CHexView()
{
}

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

///////////////////////////////////////////////////////////////////////////
// CHexView drawing

void CHexView::OnDraw(CDC* pDC)
{
    CHexDoc* pDoc = GetDocument();
    ASSERT_VALID(pDoc);

    if (m_nLinesTotal != 0) {
        CRect rect;
        pDC->GetClipBox (&rect);

        UINT nStart = rect.top / m_cyScreen;
        UINT nEnd = min (m_nLinesTotal - 1,
            (rect.bottom + m_cyScreen - 1) / m_cyScreen);

        CFont* pOldFont = pDC->SelectObject (&m_fontScreen);
        for (UINT i=nStart; i<=nEnd; i++) {
            CString string;
            FormatLine (pDoc, i, string);
            pDC->TextOut (2, (i * m_cyScreen) + 2, string);
        }
        pDC->SelectObject (pOldFont);
    }
}

void CHexView::OnInitialUpdate()
{
    CScrollView::OnInitialUpdate();

    UINT nDocLength = GetDocument ()->GetDocumentLength ();
    m_nLinesTotal = (nDocLength + 15) / 16;

    SetScrollSizes (MM_TEXT, CSize (0, m_nLinesTotal * m_cyScreen),
        CSize (0, m_cyScreen * 10), CSize (0, m_cyScreen));
    ScrollToPosition (CPoint (0, 0));
}

///////////////////////////////////////////////////////////////////////////
// CHexView printing

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

void CHexView::OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo)
{
    //
    // Create a printer font.
    //
    m_fontPrinter.CreatePointFont (100, _T ("Courier New"), pDC);

    //
    // Compute the width and height of a line in the printer font.
    //
    TEXTMETRIC tm;
    CFont* pOldFont = pDC->SelectObject (&m_fontPrinter);
    pDC->GetTextMetrics (&tm);
    m_cyPrinter = tm.tmHeight + tm.tmExternalLeading;
    CSize size = pDC->GetTextExtent (_T ("- - - - - - - -1- - - - - - - -2- - - - - - - -" \                       
        "3- - - - - - - -4- - - - - - - -5- - - - - - - -6- - - - - - - -7- - - - - - - -8-"), 81);
    pDC->SelectObject (pOldFont);
    m_cxWidth = size.cx;
    //
    // Compute the page count.
    //
    m_nLinesPerPage = (pDC->GetDeviceCaps (VERTRES) -
        (m_cyPrinter * (3 + (2 * PRINTMARGIN)))) / m_cyPrinter;
    UINT nMaxPage = max (1, (m_nLinesTotal + (m_nLinesPerPage - 1)) /
        m_nLinesPerPage);
    pInfo->SetMaxPage (nMaxPage);

    //
    // Compute the horizontal offset required to center 
    // each line of output.
    //
    m_cxOffset = (pDC->GetDeviceCaps (HORZRES) - size.cx) / 2;
}

void CHexView::OnPrint(CDC* pDC, CPrintInfo* pInfo) 
{
    CHexDoc* pDoc = GetDocument ();
    PrintPageHeader (pDoc, pDC, pInfo->m_nCurPage);
    PrintPage (pDoc, pDC, pInfo->m_nCurPage);
}

void CHexView::OnEndPrinting(CDC* pDC, CPrintInfo* pInfo)
{
    m_fontPrinter.DeleteObject ();
}

///////////////////////////////////////////////////////////////////////////
// CHexView diagnostics

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

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

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

int CHexView::OnCreate(LPCREATESTRUCT lpCreateStruct) 
{
    if (CScrollView::OnCreate(lpCreateStruct) == -1)
        return -1;
    
    //
    // Create a screen font.
    //
    m_fontScreen.CreatePointFont (100, _T ("Courier New"));

    //
    // Compute the height of one line in the screen font.
    //
    CClientDC dc (this);
    TEXTMETRIC tm;
    CFont* pOldFont = dc.SelectObject (&m_fontScreen);
    dc.GetTextMetrics (&tm);
    m_cyScreen = tm.tmHeight + tm.tmExternalLeading;
    dc.SelectObject (pOldFont);
    return 0;
}

void CHexView::FormatLine(CHexDoc* pDoc, UINT nLine, CString& string)
{
    //
    // Get 16 bytes and format them for output.
    //
    BYTE b[17];
    ::FillMemory (b, 16, 32);
    UINT nCount = pDoc->GetBytes (nLine * 16, 16, b);

    string.Format (_T ("%0.8X    %0.2X %0.2X %0.2X %0.2X %0.2X %0.2X " \
        "%0.2X %0.2X - %0.2X %0.2X %0.2X %0.2X %0.2X %0.2X %0.2X " \
        "%0.2X    "), nLine * 16,
        b[0], b[1],  b[2],  b[3],  b[4],  b[5],  b[6],  b[7],
        b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15]);

    //
    // Replace non-printable characters with periods.
    //
    for (UINT i=0; i<nCount; i++) {
        if (!::IsCharAlphaNumeric (b[i]))
            b[i] = 0x2E;
    }

    //
    // If less than 16 bytes were retrieved, erase to the end of the line.
    //
    b[nCount] = 0;
    string += b;

    if (nCount < 16) {
        UINT pos1 = 59;
        UINT pos2 = 60;
        UINT j = 16 - nCount;

        for (i=0; i<j; i++) {
            string.SetAt (pos1, _T (` `));
            string.SetAt (pos2, _T (` `));
            pos1 -= 3;
            pos2 -= 3;
            if (pos1 == 35) {
                string.SetAt (35, _T (` `));
                string.SetAt (36, _T (` `));
                pos1 = 33;
                pos2 = 34;
            }
        }
    }
}

void CHexView::PrintPageHeader(CHexDoc* pDoc, CDC* pDC, UINT nPageNumber)
{
    //
    // Formulate the text that appears at the top of page.
    //
    CString strHeader = pDoc->GetPathName ();
    if (strHeader.GetLength () > 68)
        strHeader = pDoc->GetTitle ();

    CString strPageNumber;
    strPageNumber.Format (_T ("Page %d"), nPageNumber);

    UINT nSpaces = 
        81 - strPageNumber.GetLength () - strHeader.GetLength ();
    for (UINT i=0; i<nSpaces; i++)
        strHeader += _T (` `);
    strHeader += strPageNumber;

    //
    // Output the text.
    //
    UINT y = m_cyPrinter * PRINTMARGIN;
    CFont* pOldFont = pDC->SelectObject (&m_fontPrinter);
    pDC->TextOut (m_cxOffset, y, strHeader);

    //
    // Draw a horizontal line underneath the line of text.
    //
    y += (m_cyPrinter * 3) / 2;
    pDC->MoveTo (m_cxOffset, y);
    pDC->LineTo (m_cxOffset + m_cxWidth, y);

    pDC->SelectObject (pOldFont);
}

void CHexView::PrintPage(CHexDoc* pDoc, CDC* pDC, UINT nPageNumber)
{
    if (m_nLinesTotal != 0) {
        UINT nStart = (nPageNumber - 1) * m_nLinesPerPage;
        UINT nEnd = min (m_nLinesTotal - 1, nStart + m_nLinesPerPage - 1);

        CFont* pOldFont = pDC->SelectObject (&m_fontPrinter);
        for (UINT i=nStart; i<=nEnd; i++) {
            CString string;
            FormatLine (pDoc, i, string);
            UINT y = ((i - nStart) + PRINTMARGIN + 3) * m_cyPrinter;
            pDC->TextOut (m_cxOffset, y, string);
        }
        pDC->SelectObject (pOldFont);
    }
}

A Unique Approach to Serialization

One aspect of HexDump that deserves special mention is the unusual way in which it serializes documents. When CHexDoc::Serialize is called to read a document from disk, it doesn't read from an archive. Instead, it allocates a buffer whose size equals the file size and reads the file into the buffer with CFile::Read. With exception handling statements removed, here's how it looks in code:

if (ar.IsLoading ()) {
    CFile* pFile = ar.GetFile ();
    m_nDocLength = (UINT) pFile->GetLength ();
    m_pFileDate = new BYTE[m_nDocLength];
    pFile->Read (m_pFileDate, m_nDocLength);
}

CArchive::GetFile returns a CFile pointer for the file associated with the archive so that an application can call CFile functions on it directly. This is one way an MFC application can read and write binary documents stored by someone else. When the document's DeleteContents function is called, HexDump frees the buffer containing the raw file data:

delete[] m_pFileData;

HexDump doesn't serialize the contents of a file back to disk because it's a hex viewer and not a hex editor, but if it did allow documents to be edited and saved, it would use CFile::Write to write modified documents back to disk the same way it uses CFile::Read to read them into memory.

Allocating a buffer whose size equals the file size isn't the most efficient approach to serializing and viewing large documents because it means that an entire document has to fit into memory at once. There are workarounds (memory-mapped files being one solution that comes to mind), but in HexDump's case it turns out to be a moot point because the limitations imposed by the CScrollView are typically more constraining than the limitations imposed by available memory. To see what I mean, find a file that's a few hundred kilobytes in length and load it into HexDump. If it's running on Windows 95 or Windows 98, HexDump won't display more than about a thousand lines of the file. How come?

The problem arises from the 16-bit heritage of Windows 95 and Windows 98. In both these operating systems, scroll bar ranges are 16-bit values. Before CHexView::OnInitialUpdate calls SetScrollSizes, it computes the view's virtual height by multiplying the number of lines in the document by the number of pixels per line. If the height of a line is 16 pixels and the document contains 1,000 lines, the resulting height is 16,000. For small documents, that's fine; but a CScrollView can't handle heights greater than 32,767—the largest positive value that can be represented with a signed 16-bit integer—because that's the upper limit of a scroll bar's range. The result? If you load a document that contains too many lines, the CScrollView shows only part of the document even though printing and previewing work adequately. To modify HexDump to handle large documents, your best bet is to create a CView with a scroll bar and process scroll bar messages yourself. For more information about processing scroll bar messages in MFC applications, refer to Chapter 2.

The CHM file was converted to HTML by chm2web software.