Printing A Tree

Home
Back To Tips Page

This is a piece of code adapted from my "Class Map" project.  It is assumed that the DC coming in has already had the desired font selected into it.

There is no download because you can copy-and-paste directly from this page.  Or you can download the slight variant from the ClassMap project download

The tree printing algorithm

The printing is controlled by state managed in an instance of a GraphicInfo structure which is passed around the various calls.  The initial caller must set up the parameters that control the appearance, and if printing, the page size.

class GraphicInfo

#pragma once
class GraphicInfo {
    public:
       GraphicInfo() { VerticalLineOffset = 0;
                       ItemHeight = 0;
                       LineWidth = 0;
                       FontHeight = 0;
                       PageRect.SetRectEmpty();
                       Printing = FALSE;
                       PageStarted = FALSE;
                       CurrentPosition = CPoint(0,0);
                     }
       /********************************
         |    |    +----Class
         |    |    |    ..|
         The .. is the VerticalLineOffset
       ********************************/
       int VerticalLineOffset;

       /********************************
         |    |    +----Class 1___________________V
         |    |    |                           
         |    |    +----Class 2___________________
                                                  ^            

         ItemHeight is the vertical separation
       ********************************/
       int ItemHeight;

       /********************************
         |    |    +----Class
         |....|....|....|
         The .... is the LineWidth
       ********************************/
       int LineWidth;

       int FontHeight;

       BOOL Printing;
       BOOL PageStarted;

       CRect PageRect;  // printer page size when printing
       CPoint CurrentPosition;
};

DrawTree

void DrawTree(CDC & dc, CTreeCtrl & ctl, HTREEITEM item, GraphicInfo & info)
    {
     CPoint start = info.CurrentPosition;
     
     if(info.Printing && !info.PageStarted)
        { /* start new page */
         dc.StartPage();
         info.CurrentPosition.y = info.PageRect.top;
         info.PageStarted = TRUE;
        } /* start new page */

     IndentDrawing(dc, ctl, item, info);
     if(ctl.GetRootItem() != item)
        { /* vertical line */
         int vertoffset; // The vertical offset above the current line

         /****************
               drawn by IndentDrawing
                       drawn here
            ___^____   |
           /        \  V
           |  |  |  |  |
         or
           |  |  |  |  +
         ****************/

         CPoint pos = info.CurrentPosition;

         //************************************
         // Draws either a partial line if we are at the top or a full line if
         // we are within
         //    +--- name
         //         |     <-- partial line here
         //         +---  
         //         |     <-- full line here
         //         +---  
         // The 'offset' tells how high to draw the line
         // so it draws either
         //         xxxxxxx
         //         |
         // or
         //         |
         //         |
         //************************************

         if(ctl.GetPrevSiblingItem(item) == NULL)
            { /* case 1 */
             vertoffset = (info.ItemHeight / 2) - (3 * info.FontHeight) / 4; // case 1
            } /* case 1 */
         else
            { /* case 2 */
             vertoffset = (info.ItemHeight / 2);
            } /* case 2 */

         vertoffset += info.FontHeight / 2;
         
         dc.MoveTo(pos.x + info.VerticalLineOffset, pos.y - vertoffset);    // case 2
         dc.LineTo(pos.x + info.VerticalLineOffset, pos.y + (info.ItemHeight / 2));
         info.CurrentPosition = CPoint(start.x, pos.y + (info.ItemHeight / 2));
         IndentDrawing(dc, ctl, item, info);
        } /* vertical line */

     //****************************************************
     //  draw the horizontal line
     //          +---
     //      --->|   |<--- info.VerticalLineOffset
     //****************************************************

     CPoint pos = info.CurrentPosition;

     CPoint startline = CPoint(pos.x  + info.VerticalLineOffset, pos.y);
     dc.MoveTo(startline);
           
     CPoint endline = CPoint(pos.x + info.LineWidth, pos.y);
     dc.LineTo(endline);
     CPoint textpos = endline;
     dc.SetBkMode(TRANSPARENT);
     //---------------------------------------------------------------------------------------
     // I promised it would get worse.  For reasons equally unfathmomable (see the discussion
     // where I set m_hAttribDC), CDC::TextOut insists that the m_hAttribDC be *different*
     // than the m_hDC.  This makes no sense, either, but to avoid annoying ASSERT messages,
     // I set it to NULL here!  Then I have to set it back so CDC::GetCurrentPosition works!
     dc.m_hAttribDC = NULL;
     textpos.y -= info.FontHeight / 2;
     dc.TextOut(textpos.x, textpos.y, _T(" ") + ctl.GetItemText(item));
     dc.m_hAttribDC = dc.m_hDC;

     //---------------------------------------------------------------------------------------

     info.CurrentPosition = CPoint(start.x, textpos.y + info.ItemHeight);
     if(info.Printing && info.CurrentPosition.y > info.PageRect.bottom)
        { /* end page */
         dc.EndPage();
         info.PageStarted = FALSE;
         info.CurrentPosition.y = info.PageRect.top;
        } /* end page */

     HTREEITEM child = ctl.GetChildItem(item);
     if(child != NULL)
        { /* write childrew */
         DrawTree(dc, ctl, child, info);
        } /* write childrew */

     HTREEITEM sib = ctl.GetNextSiblingItem(item);
     if(sib != NULL)
        DrawTree(dc, ctl, sib, info);
    } // DrawTree

The IndentDrawing fuction

void IndentDrawing(CDC & dc, CTreeCtrl & ctl, HTREEITEM item, GraphicInfo & info)
    {
     HTREEITEM parent = ctl.GetParentItem(item);
     if(parent == NULL)
        { /* top level */
         return;
        } /* top level */

     IndentDrawing(dc, ctl, parent, info);
     if(ctl.GetNextSiblingItem(parent) != NULL)
        { /* draw vertical bar */
         int save = dc.SaveDC();

         CPoint pos = info.CurrentPosition;
         dc.MoveTo(pos.x + info.VerticalLineOffset, pos.y - (info.ItemHeight / 2) - (info.FontHeight / 2));
         dc.LineTo(pos.x + info.VerticalLineOffset, pos.y + (info.ItemHeight / 2));
         dc.RestoreDC(save);

         info.CurrentPosition.x += info.VerticalLineOffset;
        } /* draw vertical bar */

     int save = dc.SaveDC();
     dc.SelectStockObject(NULL_PEN);

     dc.LineTo(info.CurrentPosition.x + info.LineWidth, info.CurrentPosition.y);

     CPoint newpos = dc.GetCurrentPosition();
     dc.RestoreDC(save);

     info.CurrentPosition.x += info.LineWidth;
    } // IndentDrawing

Sample setup

This shows how I set up to print one particular output

void CCallGraphView::OnFilePrint()
    {
     CPrintDialog dlg(FALSE, PD_RETURNDC);
     if(dlg.DoModal() != IDOK)
        return;
     CDC dc;
     dc.Attach(dlg.GetPrinterDC());

     CFont font;
     font.Attach(::GetStockObject(DEFAULT_GUI_FONT));
     LOGFONT lf;
     font.GetLogFont(&lf);

     lf.lfHeight *= 10;
     font.Detach();
     
     font.CreatePointFontIndirect(&lf, &dc);
     dc.SelectObject(&font);

     font.GetLogFont(&lf);
     TEXTMETRIC tm;
     dc.GetTextMetrics(&tm);
     GraphicInfo info;
     info.FontHeight = tm.tmHeight + tm.tmInternalLeading;
     info.LineWidth = 3 * tm.tmAveCharWidth;  
     info.VerticalLineOffset = info.LineWidth / 2; 
     info.ItemHeight = (3 * info.FontHeight) / 2;

     CSize sz;
     sz.cx = dc.GetDeviceCaps(HORZRES);
     sz.cy = dc.GetDeviceCaps(VERTRES);

     CSize dpi;
     dpi.cx = dc.GetDeviceCaps(LOGPIXELSX);
     dpi.cy = dc.GetDeviceCaps(LOGPIXELSY);

     const double HorizontalMargin = 0.5;
     const double VerticalMargin = 0.5;
     
     CSize margin;
     margin.cx = (int)((double)dpi.cx * HorizontalMargin);
     margin.cy = (int)((double)dpi.cy * VerticalMargin);

     
     info.PageRect.SetRectEmpty();
     info.PageRect.left = margin.cx;
     info.PageRect.top = margin.cy;
     info.PageRect.right = sz.cx - 2 * margin.cx;
     info.PageRect.bottom = sz.cy - 2 * margin.cy;
     info.CurrentPosition = CPoint(info.PageRect.left, info.PageRect.top); 
     info.Printing = TRUE;
     
     /****************************************************************
                                                                  margin.cy
       |<--------------------- sz.cx ---------------------->|     |
    -->|    |<--margin.cx                  margin.cx-->|    |<--  V
       +----------------------------------------------------+-------------
       |                                                    |            ^
       |    (info.PageRect.left, info.PageRect.top)         |            |
       |    *------------------------------------------+    |----------  |
       |    |                                          |    |     ^      |
       |    |                                          |    |     |      |
       |    |                                          |    |            |
       :    :                                          :    :            |
       :    :                                          :    :            sz.cy
       :    :                                          :    :            |
       |    |                                          |    |     |      |
       |    |                                          |    |     V      |
       |    +------------------------------------------*    |----------  |
       |     (info.PageRect.right ,info.PageRect.bottom)    |            |
       |                                                    |            V
       +----------------------------------------------------+-------------
                                                                  ^
                                                                  |
                                                                  margin.cy
     ****************************************************************/

     CString docname;
     docname.LoadString(IDS_CALLGRAPH_DOC_NAME);
     dc.StartDoc(docname);
     DrawTree(dc, c_CallGraph, c_CallGraph.GetRootItem(), info);
     if(info.PageStarted)
        dc.EndPage();
     dc.EndDoc();
     dc->DeleteDC();
    } // CCallGraphView::OnFilePrint

Sample output

Why not use CWnd::Print?

There is a method, CWnd::Print, which will send a WM_PRINT message to the window.  This is not particularly useful for the purpose I needed.  I wanted to print the entire, expanded tree, whether it was visible in the window or not, and whether or not a node was expanded in the display.  The WM_PRINT mechanism renders exactly what is in the window, as displayed.  If all you need is the contents of the actual window, then WM_PRINT will work, but that is not the problem I needed to solve.

It should be obvious how to modify the code above to handle collapsed nodes.  When a recursion occurs, the state of the HTREEITEM can be checked, and if it is in a collapsed state, then the recursive call for its child nodes will not happen.  I leave this as an Exercise For The Reader.

What about printing bitmaps?

I didn't need to print bitmaps (as one might in a tree control that represented, say, file folders and files) so I did not include code to do that.

What about page headings and footings?

Page headings and footings are left as an Exercise For The Reader.

Date Change

2-Jun-07

Updated code to represent the latest version
  Added discussion of why CWnd::Print is inadequate for the task this solves
  Added an example of doing the setup


[Dividing Line Image]

The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft.

Send mail to newcomer@flounder.com with questions or comments about this web site.
Copyright 2007 FlounderCraft, Ltd.,  All Rights Reserved.
Last modified: May 14, 2011