Quincy 2000: Customizing the Print Dialogs

While customizing Quincy 2000's Print dialogs isn't necessarily a day at the beach, it is still a boat load of fun.


December 01, 2000
URL:http://www.drdobbs.com/cpp/quincy-2000-customizing-the-print-dialo/184404360

Dec00: C Programming

Al is a DDJ contributing editor. He can be contacted at [email protected].


I'm writing this column from a most unlikely place to be thinking about C++ programming. I'm seated at the laptop in my stateroom on the Disney Magic -- that huge cruise ship you see so often in TV commercials, the one with chipmunks, mice, and ducks running around, not to mention Cinderella, Alice in Wonderland, Goofy, and other lovely creatures.

I'm here because I had a free week and the ship needed a piano player. It came about rather quickly. I got the call Friday and sailed Saturday, and here I sit in the lap of luxury pounding out a column. When you work on a cruise ship, you go under either crew status or guest status. Crew members dwell and dine below in the low-rent district and do not avail themselves of the ship's amenities. They can't even show themselves to the guests except when duty requires their appearance, waiting tables or dressed in a mouse getup. Guests, of course, spend their free time partaking of whatever the ship has to offer--restaurants, bars, a health spa, pools, hot tubs, the obligatory shuffle board deck, lounge chairs galore, even a basketball court. If you ever work on a cruise ship in any capacity, request guest status.

When I say "free week," that doesn't mean free from programming and writing duties. It means a week with no obligations that require me to be somewhere other than the Bermuda Triangle chugging along at 25 knots, heading for my first port of call, St. Maarten. Luxury's lap, cushy though it may be, has its price, though. Because of deadlines, I'll spend most of each morning in the stateroom banging away on the laptop. But not before a stroll around the upper decks and, if the muse demands it, a Bloody Mary to kickstart the creative juices. After a morning at the laptop, I have various afternoon and evening sessions at the old 88 keyboard, and, in between, enough food and drink to feed a small third-world country and pretty much the run of the place.

After St. Maarten, we sail for St. Thomas in the Virgin Islands and then Disney's Castaway Cay in the Bahamas. Guest status piano players get full use of the islands, bars, and beaches, too. I could get used to this. If memory serves, this was how Hemingway worked. Just call me Papa.

Quincy 2000 Grows

A few weeks ago I released Quincy 2000 Build 1. For the benefit of new readers, Quincy is an IDE integrating a programmer's editor and debugger with the gnu-mingw32 port of the GNU C/C++ compiler suite. You can download Quincy's source code from DDJ (see "Resource Center," page 5) and from http://www.midifitz .com/alstevens/quincy2000/ along with the binaries, installation instructions, and revision history.

Last month I was struggling with the new programmer's editor, problems with row and column numbers on the screen, and line and character positions in the document. I got it working, and although I cannot brag that the design is more orderly than when I started, at least it is now software that you don't mess with because it "ain't broke." I hope.

I keep a mailing list of Quincy users so I can announce new builds. This core group of dedicated users has helped out ever since I released Quincy 96, sending bug reports and suggestions and significantly influencing Quincy's evolution. Send e-mail to [email protected] and ask to be added to the list. Following the release of Build 1, Jill Kiminsky requested an option to print line numbers on source-code listings. Jill is a teacher and uses Quincy in the classroom. Line numbers would make it easier for Jill to discuss specific parts of a program with her students. Since Quincy 2000 is meant to be a free platform for teaching and learning C++, I added the option.

Printing line numbers is easy, but the program needs a place to turn the option on and off. Quincy 2000 has a tabbed dialog for options, and using that would have been easy. But traditional Windows applications put print options on the Print and Print Setup dialog boxes, implemented in MFC with the CPrintDialog class. Customizing the dialogs is not an easy task. First, you have to know how MFC interacts with CPrintDialog and your code to support printing.

MFC and Printing

MFC's document/view architecture provides CView member functions for printing. You override and call these functions to print a document. This assumes three things:

Given those assumptions, choosing the Print command on the File menu calls CView::OnFilePrint, which instantiates a CPrintDialog object on the heap with default values taken from current printer settings. The object's address goes into a CPrintInfo structure, and CView::OnFilePrint calls CView::OnPreparePrinting, passing the CPrintInfo structure. Up until the call to OnPreparePrinting, everything happens in the murky depths of MFC.

To print the document, your derived view class overrides CView::OnPreparePrinting, which must call CView::DoPreparePrinting, which, in the dark again, opens and processes the modal common Print dialog box. If you choose OK, CView::OnFilePrint calls CView::OnBeginPrinting, which you override to get the printing parameters from the CPrintInfo object, which was initialized from the user's selections in the Print dialog. The dark side takes over again when OnBeginPrinting returns.

Then, for each document page, CView::OnFilePrint calls CView::OnPrint, which you override to print the page. You get the current page number from the CPrintInfo* argument and use that value instead of counting pages, because you might have chosen a subset of the document's pages to print. After the last page is printed, CView::OnFilePrint calls CView::OnEndPrinting, which you override to do any cleanup.

After calling CView::OnEndPrinting, or if you choose Cancel on the dialog box during the execution of CView::DoPreparePrinting, CView::OnFilePrint deletes the CPrintDialog object on the heap.

Adding a Checkbox

That's how it works when everything is according to the book. The underneath stuff is uninteresting because it does what it is supposed to do. Applications that modify the Print dialog's behavior, however, usually do it by overriding the CPrintDialog class, which is what Quincy 2000 does. The documentation tells you to do that and to provide a custom dialog template. The documentation does not, however, tell you how to insinuate your custom dialog into the printing process or what control IDs to assign to the controls on the dialog so that everything in the base class still works. If you follow the directions in the documentation, you provide everything that CView::OnFilePrint already provides, and manage the internal coordination of the dialog's settings and the program's use of them, too. In other words, you have to do everything CView and CPrintDialog do, and it's all in the dark and undocumented.

Since all I want is a new checkbox, I shouldn't need a custom dialog template; there is room in the lower left corner of the existing dialog for one checkbox. Instead, I need to modify the behavior of CPrintDialog to keep its supporting code and the code in CView::OnFilePrint. Figure 1 shows the modified dialog box for printing from Quincy 2000. It ought to be easy, right? Wrong. Not easy. But doable.

I had three problems to solve:

Specializing CPrintDialog

When you use ClassWizard to define a class derived from CPrintDialog, you don't get much help. You don't get to assign an ID number to associate the dialog with the template that the documentation says you should build (which I decided not to do). You don't get member variables or the ability to add them. ClassWizard just builds the source code for a derived class. After that you do everything manually.

I derived a class from CPrintDialog and called it CQuincyPrintDialog. Listing One is QuincyPrintDialog.h, the header file that defines CQuincyPrintDialog and CLineNumberButton, which derives from CButton. CQuincyPrintDialog includes an instance of CLineNumberButton named lnobutton, which is the checkbox I am adding. The logfont data member is a LOGFONT structure that defines the font the dialog box uses for text labels. The printlinenumbers variable is a bool that represents your selection of the line number printing option.

Listing Two is QuincyPrintDialog.cpp. CQuincyPrintDialog::OnInitDialog calls GetFont and GetLogFont to get the dialog's font into the logfont structure. The reason for this soon becomes apparent. Then OnInitDialog calls Create for lnobutton using the dialog's client window coordinates to compute where to put the checkbox.

CLineNumberButton::OnCreate uses the dialog's logfont structure as the font for its own label, which explains why I needed a specialized CButton class. If you add a simple CButton object to a dialog this way, the framework stupidly assigns it the default system font rather than the default dialog font, and the button looks weird.

Substituting CQuincyPrintDialog for CPrintDialog

Building a specialized Print dialog is one thing. Getting the framework to use it is another. I had to reverse engineer some MFC code. Listing Three is an extract from Quincy 2000's CEditorView class member functions showing how it handles communicating with CView. I adapted the OnBeginPrinting, PrintPageHeader, and PrintPage functions from Jeff Prosise's code in Programming Windows 95 with MFC (Microsoft Press, 1996). This book, though unfortunately out of print, is one you should have if you are programming with MFC.

The first entry into your code after you choose the Print command is the view class's overridden OnPreparePrinting function. The framework has already allocated a CPrintDialog object on the heap and stored its address in the CPrintInfo object. Consequently, you have to delete the one the framework built and instantiate an object of your derived class, putting the object's address in the CPrintInfo's m_pPD data member. The framework uses the base CPrintDialog class's implementation and behavior to manage the printer selection and configuration process. The framework also takes care of deleting your object, thinking it is deleting its own object.

That's all there is to putting a specialized Print dialog in an application as long as you can modify the common dialog's template at run time. (Programs that need drastic changes to the dialog's format and appearance can't take this easy way out.)

Specializing CPrintSetupDialog

Applications with Print commands usually have Print Setup commands, too. (An optional Page Setup command for applications controls margins and such, and the documentation suggests you use it instead of the Print Setup command. More about this later.)

The Print Setup command uses the CPrintDialog class, and the whole thing works in the dark. When users choose the command, the framework opens the dialog, gets the choices, and saves them away somewhere in the deep woods. When users choose the Print command, the most recent Print Setup choices are displayed in the Print dialog and vice versa.

The two dialogs use the same class, but different dialog templates. The first constructor argument is a BOOL that specifies which dialog to display. It follows that the CQuincyPrintDialog specialization ought to work with Print Setup, too. And so it does, but you have to know how to get inside CView's inner workings to apply the specialized dialog class in place of the regular one. The secret is to find out what the framework does when the user chooses the Print Setup command. Remember, menu commands can be processed by the application class, frame class, document class, view class, or any combination of all of them. Print Setup ought to be unrelated to any particular instance of a document or its view and unrelated to the frame window, too. Consequently, I started a search for related code in the CWinApp class and, sure enough, I found the undocumented function in Example 1. It's undocumented because the online help that tells you all about the CWinApp overridable functions does not list it. Yet there it is, and your derived application class can override it. It instantiates a CPrintDialog object on the stack frame, specifying a Print Setup dialog, and passes the object to another undocumented CWinApp member function named DoPrintDialog. Somehow, down in the depths of the relationship between CWinApp and CView, the user's choices get passed along. I didn't care how, though, once I realized I just needed to override CWinApp::OnFilePrintSetup in my application class. Example 2 is that overriding function. Figure 2 is the modified Print Setup dialog box.

And Now for the Gotcha

Every success story has a gotcha, and this one is no exception. First some background.

A Win32 program has a lot of ID codes floating around. Developer Studio tries to maintain them as mnemonic global symbols. The standard ones, ID_FILE_OPEN and ID_EDIT_COPY, for example, have fixed numeric values assigned. Your program adds new command values as it needs them. There are IDs for commands to identify dialogs, to identify controls, and so on. The program communicates the user's actions by passing these ID codes around and, again, a lot of it happens in the dark.

It follows that the common dialog classes, which use common dialog templates, appropriate a range of codes that application programs avoid. Developer Studio tries to keep all these codes separate by maintaining independent ranges of values. When you use the Resource Editor to add a control to a dialog and give it a name, such as IDC_AUTOINDENT, Developer Studio assigns a number to the name, puts a #define statement in resource.h, and uses the name throughout the code that Developer Studio generates to support the control.

Recall that ClassWizard and Developer Studio don't help much when you derive from common dialog classes. The CLineNumberButton object needs an ID code. I chose the name IDC_LINENUMBERS and manually added a #define entry to resource.h, using the next available numeric value, which was 1060. Resource.h has #defines at the end that specify the next highest available numbers in each of the ranges, which Developer Studio uses to assign new numbers. I updated that number to 1061, which is when the trouble started.

While testing, I opened the Print Setup dialog and changed the printer selection to the HP Laserjet 4MP, whereupon the new checkbox was checked, disabled, and grayed out. All by itself. That was the gotcha, and it was an odd one indeed.

I won't bother you with the details of what I went through trying to unget the gotcha, but after a lot of code tweaking, I realized that the Print Setup dialog must be doing something to a control that had the same ID value as my checkbox, 1060. I found an available number well away from that one, changed the #define of IDC_LINENUMBERS, and the problem went away.

This was a mystery. None of the controls on the Print Setup dialog are changed when I select that particular printer. After some head scratching I concluded that Print Setup must use printer-device capabilities to enable and disable a bunch of controls, and that the procedures for Print Setup and Page Setup use common code. Something on the page setup dialog must use ID number 1060. I'm not using a Page Setup dialog, because Quincy users don't need to be changing margins and such, but the hidden code got in the way, nonetheless, and I got gotten by a gotcha.

Pushing the Envelope

Between getting the button installed, integrating the dialog, and fixing the phantom checkbox gotcha, I had a lot of false starts and made a lot of wrong turns during what ought to have been a simple job -- adding a checkbox to a dialog. I won't belabor the details because there's little to be learned from my mistakes, but I do complain that the literature fails me when it comes to explaining stuff like this. All the books that promise in their titles and on their back covers to reveal so-called secrets of Win32 MFC programming reveal very few actual secrets. Mostly what they do is explain how to write programs that stay within the boundaries that the designers of the Win32 API and MFC assumed you could live with.

Usually, I find those boundaries pertain only to the most trivial of applications, which is what books tend to use as examples, anyway. But when you want to push the envelope, break the bonds of convention, and escape those boundaries, forget the books. Your best bet is to trace the MFC source code and see what it's doing. Then you can decide what to override, what to specialize, what to replace, and how to do all that. Tracing through MFC's source code is easy enough. You can trace into it when your code calls a framework function, and you can set breakpoints in your own code and use the call stack to find where your code was called from in MFC. From there you can navigate into the MFC source code.

Back to Reality

Well, it looked for a while like I'd be here on the Disney Magic another week. The piano player I replaced is still on sick call, poor fellow, and they asked me to stay on. As part of the deal, I requested that Judy be permitted to join me so she could share my windfall of luxurious guest status tourism. But Disney management, operating on a tight budget, short on cash, and needing to reserve every possible space for paying guests, turned her down. They waited until the last day to let me know, too. Not knowing me very well, they figured I'm your typical desperate piano player who can't pass up a week's employment. Yeah, right.

In the meantime, I remain a busy guy with a full schedule. Let's see, right after the tour of the nude beaches of St. John Island I think I'm due for my personal massage. Or is it the wine-tasting party? Oh, bother, there's just too much debauchery for a guest piano player to keep track of.

Next month we add a second free compiler, Borland C++ 5.5, to Quincy 2000. But for now, I'll be on the ninth deck in the Quiet Cove bar sipping on a Mai Tai, tinkling "As Time Goes By," and dreaming about my next great work of literature.

DDJ

Listing One

#if !defined(AFX_QUINCYPRINTDIALOG_H__
                            64D73F03_7B38_11D4_B7A3_00207815827F__INCLUDED_)
#define AFX_QUINCYPRINTDIALOG_H__
                            64D73F03_7B38_11D4_B7A3_00207815827F__INCLUDED_
#if _MSC_VER >= 1000
#pragma once
#endif // _MSC_VER >= 1000
// QuincyPrintDialog.h
////////////////////////////
// CLineNumberButton class
class CLineNumberButton : public CButton
{
    DECLARE_DYNAMIC(CLineNumberButton)
    CFont font;
public:
    CLineNumberButton();
protected:
    //{{AFX_MSG(CLineNumberButton)
    afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
    //}}AFX_MSG
    DECLARE_MESSAGE_MAP()
};
/////////////////////////////////////////////////////////////////////////////
// CQuincyPrintDialog dialog
class CQuincyPrintDialog : public CPrintDialog
{
    DECLARE_DYNAMIC(CQuincyPrintDialog)
    CLineNumberButton lnobutton;    // print line number checkbox
    static LOGFONT logfont;
public:
    bool printlinenumbers;  // publicly exposed for setting and testing
    CQuincyPrintDialog(BOOL bSetup = FALSE);
// Attributes
public:
// Operations
public:
// Overrides
    // ClassWizard generated virtual function overrides
    //{{AFX_VIRTUAL(CMainFrame)
    public:
    //}}AFX_VIRTUAL
protected:
    //{{AFX_MSG(CQuincyPrintDialog)
    virtual void OnOK();
    virtual BOOL OnInitDialog();
    //}}AFX_MSG
    DECLARE_MESSAGE_MAP()
};
//{{AFX_INSERT_LOCATION}}
// Microsoft Developer Studio will insert additional declarations 
//                                  immediately before the previous line.
#endif // !defined(AFX_QUINCYPRINTDIALOG_H__
                           64D73F03_7B38_11D4_B7A3_00207815827F__INCLUDED_)

Back to Article

Listing Two

// QuincyPrintDialog.cpp
#include "stdafx.h"
#include "quincy.h"
#include "QuincyPrintDialog.h"

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

LOGFONT CQuincyPrintDialog::logfont;

////////////////////////////
// CLineNumberButton class
IMPLEMENT_DYNAMIC(CLineNumberButton, CButton)
CLineNumberButton::CLineNumberButton()
{
}
BEGIN_MESSAGE_MAP(CLineNumberButton, CButton)
    //{{AFX_MSG_MAP(CLineNumberButton)
    ON_WM_CREATE()
    //}}AFX_MSG_MAP
END_MESSAGE_MAP()
int CLineNumberButton::OnCreate(LPCREATESTRUCT lpCreateStruct) 
{
    if (CButton::OnCreate(lpCreateStruct) == -1)
        return -1;
    font.CreateFontIndirect(&CQuincyPrintDialog::logfont);
    SetFont(&font);
    return 0;
}
/////////////////////////////////////////////////////////////////////////////
// CQuincyPrintDialog
IMPLEMENT_DYNAMIC(CQuincyPrintDialog, CPrintDialog)
CQuincyPrintDialog::CQuincyPrintDialog(BOOL bSetup) :
   CPrintDialog(bSetup, PD_ALLPAGES | PD_USEDEVMODECOPIES | PD_NOSELECTION, 0)
{
    m_pd.nMinPage = 1;
    m_pd.nMaxPage = 0xffff;
    printlinenumbers = false;
}
BEGIN_MESSAGE_MAP(CQuincyPrintDialog, CPrintDialog)
    //{{AFX_MSG_MAP(CQuincyPrintDialog)
    //}}AFX_MSG_MAP
END_MESSAGE_MAP()
BOOL CQuincyPrintDialog::OnInitDialog() 
{
    CPrintDialog::OnInitDialog();
    GetFont()->GetLogFont(&logfont);

    CRect rc;
    GetClientRect(&rc);

    rc.top = rc.bottom - 30;
    rc.bottom = rc.top + 20;
    rc.left = 15;
    rc.right = rc.left + 110;

    BOOL b = lnobutton.Create(  "Print line numbers:", 
                    BS_AUTOCHECKBOX | 
                    BS_LEFTTEXT     | 
                    WS_VISIBLE      | 
                    WS_TABSTOP      |
                    WS_CHILD,
                    rc,
                    this,
                    IDC_LINENUMBERS);
    lnobutton.SetCheck(printlinenumbers);
    return TRUE;
}
void CQuincyPrintDialog::OnOK()
{
    printlinenumbers = lnobutton.GetCheck() != 0;
    CPrintDialog::OnOK();
}

Back to Article

Listing Three

// CEditorView printing
BOOL CEditorView::OnPreparePrinting(CPrintInfo* pInfo)
{
    delete pInfo->m_pPD;        // framework has one built on heap
    pInfo->m_pPD = new CQuincyPrintDialog;  
                                // framework will delete this object
    static_cast<CQuincyPrintDialog*>(pInfo->m_pPD)->printlinenumbers = 
                    theApp.PrintLineNumbers();
    return DoPreparePrinting(pInfo);
}
void CEditorView::OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo)
{
    printlinenos = 
           static_cast<CQuincyPrintDialog*>(pInfo->m_pPD)->printlinenumbers;
    int nHeight = -((pDC->GetDeviceCaps(LOGPIXELSY) * 10) / 72);
    m_printerfont.CreateFont(nHeight, 0, 0, 0, FW_NORMAL, 0, 0, 0,
        DEFAULT_CHARSET, OUT_CHARACTER_PRECIS, CLIP_CHARACTER_PRECIS,
        DEFAULT_QUALITY, DEFAULT_PITCH | FF_DONTCARE, "Courier");
    TEXTMETRIC tm;
    CFont* pOldFont = pDC->SelectObject(&m_printerfont);
    pDC->GetTextMetrics(&tm);
    m_cyPrinter = tm.tmHeight + tm.tmExternalLeading;
    pDC->SelectObject(pOldFont);

    m_nLinesPerPage =(pDC->GetDeviceCaps(VERTRES) -
       (m_cyPrinter * (3 + (2 * PRINTMARGIN)))) / m_cyPrinter;
    UINT nMaxPage = max(1, (GetDocument()->linecount() + 
                           (m_nLinesPerPage - 1)) / m_nLinesPerPage);
    pInfo->SetMaxPage(nMaxPage);
}
void CEditorView::OnPrint (CDC* pDC, CPrintInfo* pInfo)
{
    PrintPageHeader(pDC, pInfo->m_nCurPage);
    PrintPage(pDC, pInfo->m_nCurPage);
}
void CEditorView::OnEndPrinting(CDC* /*pDC*/, CPrintInfo* pInfo)
{
    m_printerfont.DeleteObject ();
    bool plno = 
          static_cast<CQuincyPrintDialog*>(pInfo->m_pPD)->printlinenumbers;
    theApp.SetPrintLineNumbers(plno);
}
void CEditorView::PrintPageHeader(CDC* pDC, UINT nPageNumber)
{
    CString strHeader = GetDocument()->GetTitle();
    CString strPageNumber;
    strPageNumber.Format(" (Page %d)", nPageNumber);
    strHeader += strPageNumber;
    UINT y = m_cyPrinter * PRINTMARGIN;
    CFont* pOldFont = pDC->SelectObject(&m_printerfont);
    pDC->TextOut(0, y, strHeader);
    pDC->SelectObject(pOldFont);
}
void CEditorView::PrintPage (CDC* pDC, UINT nPageNumber)
{
    int lines = GetDocument()->linecount();
    if (GetDocument()->linecount() != 0) {
        UINT nStart = (nPageNumber - 1) * m_nLinesPerPage;
        UINT nEnd = min(lines - 1, nStart + m_nLinesPerPage - 1);
        CFont* pOldFont = pDC->SelectObject(&m_printerfont);
        for (int i = nStart; i <= nEnd; i++) {
            std::string str = GetDocument()->textline(i);
            for (int j = 0; j < str.length(); j++)
                if (str[j] == '\t')
                    str[j] = ' ';
            int y = ((i - nStart) + PRINTMARGIN + 3) * m_cyPrinter;
            CString line;
            if (printlinenos)
                line.Format("%-4d ", i + 1);
            line += str.c_str();
            pDC->TextOut(0, y, line);
        }
        pDC->SelectObject(pOldFont);
    }
}


Back to Article

Dec00: C Programming


void CWinApp::OnFilePrintSetup()
{
   CPrintDialog pd(TRUE);
   DoPrintDialog(&pd);
}

Example 1: The CWinApp::OnFilePrintSetup function.

Dec00: C Programming


void CQuincyApp::OnFilePrintSetup() 
{
   CQuincyPrintDialog pd(TRUE);
   pd.printlinenumbers = m_bPrintLineNos;
   if (DoPrintDialog(&pd) == IDOK)
       m_bPrintLineNos = pd.printlinenumbers;
}

Example 2: The CQuincyApp::OnFilePrintSetup function.

Dec00: C Programming

Figure 1: Modified dialog box for printing from Quincy 2000.

Dec00: C Programming

Figure 2: Modified Print Setup dialog box.

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.