A Logging ListBox Control

Home
Back To Tips Page

One of the most common questions I hear from programmers who have moved from console-type applications to the GUI world is "How do I use printf to do debug output?" In my first Windows app, a decade ago, I asked myself the same question, and came up with this answer. What I'm showing in this essay and the accompanying code download is the result of a decade of maturing the world's greatest (at least IMNSHO) printf-equivalent. I refer to this in my accompanying essay on the Graphical Developer Environment, but this is the full-blown control with all the features I seem to need.

This sample, in addition to containing the trace log, illustrates many other interesting techniques in MFC. This include

This is a CListBox-derived class which requires some string resources for completeness. Thus, you will have to copy a set of string resources from the sample project. They are in the 5000-range for the IDS_TRACELOG_ messages and in the 10000-range for the IDS_WSA Winsock error messages.

download.gif (1234 bytes)A complete project showing the use of these classes under MDI can be downloaded. It was compiled under VC++ 6.0.


The TraceEvent class

The class declarations

The heart of the Event Log system is an owner-draw ListBox. It draws items derived from the abstract TraceEvent class.

class TraceEvent : public CObject {
    DECLARE_DYNAMIC(TraceEvent)
protected:
    static UINT counter;
    TraceEvent(UINT id) { time = CTime::GetCurrentTime(); 
    			  filterID = id; 
			  threadID = ::GetCurrentThreadId(); 
			  seq = counter++;
			}
public:
    enum {None = -1};
    virtual ~TraceEvent() { }
    virtual COLORREF textcolor() = 0;
    virtual CString show();
    virtual CString showID();
    virtual CString showfile();
    virtual CString showThread();
    virtual int displayHeight() { return 1; } 
    static CString cvtext(BYTE ch); // useful for various subclasses
    static CString cvtext(const CString &s); // useful for various subclasses 
protected:
    CTime time;
    UINT  filterID;
    DWORD threadID;
    UINT  seq;
};

Note that the textcolor method is specified as  = 0, meaning it is impossible to instantiate an element of class TraceEvent. Instead, you subclass this and create new classes. 

I derive this class from CObject because I want to be able to do IsKindOf at some points in the processing.

A typical subclass is TraceComment, which has the form

/****************************************************************************
*                           class TraceComment
* Inputs:
*	UINT id: connection ID, or TraceEvent::None
*	CString s: String to display
* Inputs:
*	UINT id: connection ID, or TraceEvent::None
*	UINT u: IDS_ index of string to display
****************************************************************************/

class TraceComment: public TraceEvent {
    DECLARE_DYNAMIC(TraceComment)
public:
    TraceComment(UINT id, const CString &s) : TraceEvent(id) {comment = s;}
    TraceComment(UINT id, UINT u) : TraceEvent(id){ CString s; s.LoadString(u); comment = s; }
    virtual ~TraceComment() { }
    virtual COLORREF textcolor() { return RGB(0, 128, 0); }
    virtual CString show();
protected:
    CString comment;
   };

This is a typical subclass. It provides a color for the display, a show method for formatting the values which constitute the display, and the expected constructor and destructor. Another useful class is

/****************************************************************************
*                           class TraceFormatMessage
* Inputs:
*         UINT id: connection ID, or TraceEvent::None
*         DWORD error: Error code from GetLastError()
****************************************************************************/

class TraceFormatMessage: public TraceError {
DECLARE_DYNAMIC(TraceFormatMessage)
public:
TraceFormatMessage(UINT id, DWORD error);
virtual ~TraceFormatMessage() { }
};

Using the class

We'll introduce the Event Log class shortly, but the basic usage goes something like shown below. Note in the display I show above the "Connection ID" prints in the left column of all but one of the messages. Any message that is connection-related prints its connection ID, but an event that is unrelated uses the special code TraceEvent::None to indicate that there should be no printout. Your interpretation of these ideas may require different types of notation, and your parameters may vary.

c_Log.AddString(new TraceComment(TraceEvent::None, IDS_SAW_SOMETHING));

or a nice feature when you have an API failure, you can do something like

BOOL result = SomeAPIcall(...);
if(!result)
    { /* failed */
     DWORD err = ::GetLastError();
     c_Log.AddString(new TraceError(TraceEvent::None, IDS_WHATEVER_FAILED));
     c_Log.AddString(new TraceFormatMessage(TraceEvent::None, err));
    } /* failed */

The class implementation

Because the class is CObject-derived, I have to make the proper declarations:

IMPLEMENT_DYNAMIC(TraceComment, TraceEvent)
IMPLEMENT_DYNAMIC(TraceError, TraceEvent)
IMPLEMENT_DYNAMIC(TraceFormatError, TraceError)

Then I have to declare the methods. The most interesting one will be the show method. The base class, TraceEvent, displays the time.

CString TraceEvent::show()
    {
     CString s = time.Format(_T("%H:%M:%S  "));
     return s;
    }

I use a separate method to display the thread ID, for reasons we will see when we look at the display logic.

Each method will then format its data and append it to the basic show result:

CString TraceComment::show()
    {
     return TraceEvent::show() + comment;
    }

(I'm not going to show all the details because you can read them for yourself in the sample code).

Perhaps the most useful method is the TraceFormatMessage constructor, which nicely displays the error code from an API. This has a nice extension that I use to report WinSock errors as well. It uses the ::FormatMessage call for most errors, but not all values returned by ::GetLastError can be decoded by ::FormatMessage. It only really knows how to format a few kinds of messages. If your system has a DLL which provides error strings you can extend this to use it. The extension I made was that if ::FormatMessage fails, I use the error code as a string table index. For WinSock, I just put the strings into the string table with the WinSock error numbers. Of course, there's a risk here: what if an error number happens to correspond to some string already in the table? I handled this by requiring that all such error messages start with a distinguished character, that would not appear as the first character in normal text. I chose "®". 

TraceFormatMessage::TraceFormatMessage(UINT id, DWORD err) : TraceError(id)
    {
     LPTSTR s;
     if(::FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
			FORMAT_MESSAGE_FROM_SYSTEM,
			NULL,
			err,
			0,
			(LPTSTR)&s,
			0,
			NULL) == 0)
        { /* failed */
	 // See if it is a known error code
	 CString t;
	 t.LoadString(err); 
	 if(t.GetLength() == 0 || t[0] != _T('®')) 
	    { /* other error */ 
	     CString fmt; 
	     fmt.LoadString(IDS_UNKNOWN_ERROR); 
	     t.Format(fmt, err, err);
	    } /* other error */  
	 else 
	    if(t.GetLength() > 0 && t[0] == _T('®')) 
	       { /* drop prefix */  
		t = t.Mid(1); 
	       } /* drop prefix */ <
	 Error = t;
	} /* failed */
     else
        { /* success */
	 LPTSTR p = _tcschr(s, _T('\r'));
	 if(p != NULL)
	    { /* lose CRLF */
	     *p = _T('\0');
	    } /* lose CRLF */
	 Error = s;
	 ::LocalFree(s);
	} /* success */
    }

The Trace List Class

The Trace List class is a subclass of CListBox. To use this, put a CListBox control in your project, set it as owner-draw variable, leave Has Strings unchecked, and make sure Sort is unchecked. Check Horizontal Scroll to support scrolling. Check No Integral Height. Note that the PreCreateWindow method of the class will ensure that exactly these style bits are set. However if you place the control in a CDialog- or CFormView-derived class, the PreCreateWindow will not be called because the control is created before it is subclassed.

The owner-draw ListBox part falls into the Your Mileage May Vary category. The details of the drawing are up to you. The class I supply includes an ID, a thread ID, a timestamp, and some display text. It sets a single color for the whole line. But once you see the basic structure, you should be able to quickly do theme-and-variation on this.

What makes this class more complex than a simple CListBox are the ability to save the text, limits on the number of items, and fancy scrolling. We'll look at each of these in turn.

Logging to Disk

The disk saving can work in two modes. In the first mode, you issue a Save command and the entire contents of the CListBox are saved to a file. Note that if you have set a limit, say 100 items, then any items which have scrolled off the list box are lost forever. In the other mode, you set a flag that causes every line to be saved to the file as soon as it is written. This is, of course, less efficient, but it has two good features: 

CTraceList::setToDisk(BOOL v)

This sets the save-to-disk flag. If set to TRUE, every line that is written to the log is written to the file. When set to FALSE, logging stops. Note that you should only set it to TRUE once in the course of execution; each time it is set to TRUE it prompts for a file name and overwrites any file contents of any existing file. At the time it is called with TRUE any content which already exists in the listbox is written to the file.

CTraceList::doSave(BOOL saveAs)

This forces a save-to-disk operation. The control prompts for a file name and then writes the entire contents of the control to the file. If saveAs is TRUE then it will prompt for a filename; if saveAs is FALSE it will overwrite the previously-written file (or if there was no file, it prompts, the usual Save semantics).

Fancy Scrolling

The scrolling of such a control is tricky. If you are watching messages come out, you want to see them scroll up, so you have the obvious sort of trace-output routine. But if you've scrolled back to look at a message, you don't want the control jumping back to the end whenever a new message comes in. This control implements fancy scrolling. If the last line was visible before the new line is added, and the new line is not visible, or is partially obscured, after it is added, the control automatically scrolls up. If the last line was not visible, no scrolling occurs after a line is added.

In addition, this class supports to-top, to-end, and search operations, which are implemented in the dialog shown by the controls shown at the left:

The first control calls void CTraceList::toTop(), which makes the first line of the listbox visible. You can use the predicate BOOL CTraceList::canTop() to decide if this button should be enabled. The last control calls void CTraceList::toEnd() which makes the last line of the control visible, and BOOL CTraceList::canEnd() will say if you are showing the last line.

The < and > buttons call the functions int CTraceList::findNext(int start) and int CTraceList::findPrev(int start) to find the next instance of a TraceError event in the log. The value returned is the current position and will be passed in to the next call. Both calls return LB_ERR if no event was found in the desired direction. A more sophisticated logging system might take the RUNTIME_CLASS of the type of event to search for; I've wired in TraceError in this version.

Adding events

We've already shown the way events are added. The CTraceList::AddString method overrides CListBox::AddString. Instead of an LPCTSTR parameter, it takes a TraceEvent * parameter. It implements all the smart scrolling. Note that we can add strings by either giving a CString reference or the UINT of a string in the string resources. The function also handles setting the horizontal extent to allow for horizontal scrolling.

Note that an event can be displayed on multiple lines. Thus you can have an event that displays on two lines, or three lines. This is based on the virtual method int displayHeight() which in TraceEvent will return 1 but can be overridden by any event subclass requiring multiple lines.

     c_Log.AddString(new TraceError(TraceEvent::None, IDS_WHATEVER_FAILED));
     c_Log.AddString(new TraceFormatMessage(TraceEvent::None, err));

The DrawItem handler

This method takes advantage of the virtual methods that have been provided. It casts its ItemData value to a TraceEvent * event, then calls the virtual methods of the class to get the text color and the display text. To create a more sophisticated drawing scheme, you might want to add more virtual methods to the class. Here are some snippets of code to give you an idea of how these virtual methods are used.

void CTraceList::DrawItem(LPDRAWITEMSTRUCT dis) 
{
 CDC * dc = CDC::FromHandle(dis->hDC);
 COLORREF bkcolor;
 COLORREF txcolor;
 ...
int saved = dc->SaveDC();
...
if(dis->itemState & ODS_SELECTED)
    { /* selected */
     if(::GetFocus() == m_hWnd)
        { /* has focus */
	 bkcolor = ::GetSysColor(COLOR_HIGHLIGHT);
	 txcolor = ::GetSysColor(COLOR_HIGHLIGHTTEXT);
	} /* has focus */
     else
        { /* no focus */
	 bkcolor = ::GetSysColor(COLOR_BTNFACE);
	 txcolor = ::GetSysColor(COLOR_BTNTEXT);
	} /* no focus */
    } /* selected */
 else
    { /* not selected */
     txcolor = e->textcolor();
     bkcolor = ::GetSysColor(COLOR_WINDOW);
    } /* not selected */
...
CString id = e->showID();

 dc->TextOut(dis->rcItem.left, dis->rcItem.top, id);

 CString s = e->showThread();

 dc->TextOut(dis->rcItem.left + indent1, dis->rcItem.top, s);

 s = e->show();

 dc->TextOut(dis->rcItem.left + indent1 + indent2, dis->rcItem.top, s);

 if(dis->itemState & ODS_FOCUS)
    dc->DrawFocusRect(&dis->rcItem);

 dc->RestoreDC(saved);
}

The use of the variables indent1 and indent2 can be seen from the sample code; assume they represent tab positions and have been properly computed. The virtual methods appear in boldface in the above example. Note that this code keeps the highlight visible, but changes it from the normal background to a nominally-gray background if the control does not have focus.

Using it in an MDI application

Using this in an MDI application requires a lot of subtle coding issues. The CTraceList window is a member of a CFormView, which is a child window of a CMDIChildWnd, which lives inside the MDI Client window, which lives inside the main frame. This introduces some additional complexity. What I've done is package this up nicely in a sample which does all the complex work. To see what I've added to the CWinApp class, look for my initials (JMN) in that file. I've also added some sample test code to the CMainFrame class. The result of running this and executing some tests is shown below.

This is a fully-configured implementation that supports copy, cut, delete, print, save, and save-as operations invoked from the standard menu items. It will also print the data on a printer; if the printer is a color printer, you'll see the output in the same colors used for the display.

Tracer Reference Manual


CTraceEvent

CTraceEvent::CTraceEvent(UINT id)

This is called by subclasses as part of their constructor. It associates the value id which appears in the leftmost column with the event. The value TraceEvent::None when used as id the leftmost column will be blank.

static CString TraceEvent::cvtext(CString s)

This converts a string to printable form. In particular, characters less than ASCII "space" and greater than or equal to 127 will be converted to "\x" notation. In Unicode mode, if the high-order Unicode byte is nonzero, it will convert the string showing the high-order Unicode byte in hexadecimal. Because this is a static method it can conveniently be called at any time to convert a string.

static CString TraceEvent::cvtext(TCHAR ch)

This converts a single character to a printable form. It is called repeatedly by the string form of cvtext to convert each byte. Because this is a static method it can be conveniently called at any time to convert a character.

virtual int TraceEvent::displayHeight()

This method is overridden only for classes which require a multiline display. The default value returned by the base class implementation is 1, for a single-line display. The value always represents an integral number of lines for the entry.

virtual CFont * TraceEvent::getFont();

This method can be overridden by a subclass to allow a different font to be used for display purposes. This is not currently implemented, and always returns NULL.

virtual UINT TraceEvent::getID()

This method returns the value of the id for the entry.

TraceEvent::None

This is used as the first parameter to the constructors to indicate there is no id value and the leftmost column of the display should be blank.

virtual CString TraceEvent::show()

This method is overridden by each subclass to return a formatted string for display. This method, in the base class, returns an empty string. It may optionally be modified to return the timestamp value.

virtual CString TraceEvent::showfile()

This method returns a string which is used to compute the values for the clipboard or for writing to a file. This format includes a sequence number formatted as a 6-digit decimal number. This is useful because the number is assigned at the time the entries are created, and thus gaps in the sequence will indicate deletions. It is usually not overridden by a subclass, but could be.

virtual CString TraceEvent::showID()

This method is usually not overridden; it returns the id value formatted as a decimal number. The the id was TraceEvent::None, it returns a blank string the same size as the decimal number it would have returned.

virtual CString TraceEvent::showThread()

This method is usually not overridden; it returns a formatting thread handle.

virtual COLORREF TraceEvent::textcolor()

This method must be overridden by a subclass to provide the desired display color for that subclass.


TraceComment

TraceComment::TraceComment(UINT id, const CString & s)

Contructs a TraceComment whose id is as specified and whose contents are specified by the string s.

TraceComment::TraceComment(UINT id, const UINT strid)

Constructs a TraceComment whose id is as specified and whose contents is specified by the string resource whose id is strid.


TraceData

TraceData::TraceData(UINT id, LPBYTE data, UINT length)

Creates a TraceData whose id is as specified and whose contents are the binary data bytes specified by data and length. The data will be displayed as hexadecimal bytes separated by spaces.

TraceData::TraceData(UINT id, LPCTSTR data)

Creates a TraceData whose id is as specified and whose contents are the NUL-terminated string specified by data. The data will be displayed as textual data using TraceData::cvtext.

TraceData::TraceData(UINT id, const CString & data)

Creates a TraceData whose id is as specified and whose contents are the specified CString. The data will be displayed as textual data using TraceData::cvtext.


TraceError

TraceError::TraceError(UINT id, const CString & s)

Constructs a TraceError whose id is as specified and whose contents are specified by the string s.

TraceError::TraceError(UINT id, UINT strid)

Constructs a TraceError whose id is as specified and whose contents are specified by the string resource whose id is strid.

See also: TraceFormatError


TraceFormatComment

TraceFormatComment::TraceFormatComment(UINT id
                                                           const CString & fmt, ...)

Constructs a TraceComment whose id is as specified and whose contents represent a set of arguments formatted according to the string fmt

TraceFormatComment::TraceFormatConnment(UINT id,
                                                        UINT fmtid, ...)

Constructs a TraceComment whose id is as specified and whose contents are a set of arguments formatting according to the string resource represented by fmtid.


TraceFormatError

TraceFormatError::TraceFormatError(UINT id
                                                           const CString & fmt, ...)

Constructs a TraceError whose id is as specified and whose contents represent a set of arguments formatted according to the string fmt

TraceFormatError::TraceFormatError(UINT id,
                                                        UINT fmtid, ...)

Constructs a TraceError whose id is as specified and whose contents are a set of arguments formatting according to the string resource represented by fmtid.


TraceFormatMessage

TraceFormatMessage::TraceFormatMessage(UINT id, DWORD err)

Constructs a CTraceError whose id is as specified and whose contents represent an error code err formatted according to ::FormatMessage(..., FORMAT_MESSAGE_FROM_SYSTEM, ...). 


CTraceList

int CTraceList::AddString(TraceEvent * event)

Appends a TraceEvent to the log window. If this would cause the number of items in the log window to exceed a limit set by CTraceList::setLimit, then some number of items, starting just past the boundary value (see CTraceList::setBoundary) are deleted until there is space to insert the new item.

BOOL CTraceList::canClearAll()

Returns TRUE if there are any elements in the list. You can override this in a subclass to always return FALSE to support a read-only list.

BOOL CTraceList::canCopy()

Returns TRUE if a Copy operation is possible. For this to work, there must be at least one item selected in the list.

BOOL CTraceList::canCut()

Returns TRUE if a Cut operation is possible. For this to work, there must be at least one item selected in the list. You can override this in a subclass to support a read-only control by always returning FALSE.

BOOL CTraceList::canDelete()

Returns TRUE if a Delete operation is possible. You can override this in a subclass to always return FALSE to support a read-only control.

BOOL CTraceList::canEnd()

Returns TRUE if there is more than one item in the list. Typically used to determine if a "To End" button should be enabled.

BOOL CTraceList::canNext()

Returns TRUE if there is a CTraceError item following the item that has the caret, and FALSE if not. Typically used to determine if a "Next Error" button should be enabled.

BOOL CTraceList::canPrev()

Returns TRUE if there is a CTraceError item preceding the item that has the caret, and FALSE if not. Typically used to determine if a "Previous Error" button should be enabled.

BOOL CTraceList::canPrint()

Returns TRUE if the list is nonempty.

BOOL CTraceList::canSave()

Returns TRUE if the list is nonempty and has been modified since the last Save.

BOOL CTraceList::canSaveAs()

Returns TRUE if the list is nonempty.

BOOL CTraceList::canSelectAll()

Returns TRUE if the list is nonempty.

BOOL CTraceList::canTop()

Returns TRUE if there is more than one item in the list. Typically used to determine if a "To Beginning" button should be enabled.

BOOL CTraceList::Copy()

If any items are selected, they are formatting according to the CTraceEvent::showfile() method and placed on the clipboard in CF_TEXT format. Returns FALSE if no items are selected or the clipboard cannot be accessed. Typically called from an OnCopy handler in a view.

BOOL CTraceList::Cut()

If any items are selected, they are copied to the clipboard as in CTraceList::Copy and then they are deleted. If any operation fails to successfully place them on the clipboard, they are not deleted. Typically called from an OnCut handler in a view.

BOOL CTraceList::Delete()

If any items are selected, they are deleted. Returns TRUE if any items were deleted. Typically called by the handler for the Del key or the Clear menu item.

int CTraceList::DeleteString(int n)

Deletes the element n from the list. Note that n is not restricted to be above the boundary (see CTraceList::setBounary).

BOOL CTraceList::doSave(BOOL saveas)

If the saveas parameter is TRUE, or there is no previously saved filename, prompts the user for a filename. If the saveas parameter is FALSE and there is a previous save operation, the file is rewritten. The file is always overwritten, not appended to. The contents of the control will be written to the file according to the virtual CTraceEvent::fileshow() method.

void CTraceList::enableLimit(BOOL enable)

Enables the limit. This will force any elements beyond the limit to be deleted until the count is brought into conformance with the limit. The limit is always a certain minimum size beyond the boundary.

void CTraceList::Print()

Prompts the user for a printer, then prints either the selected items or the entire list, depending on what the user has chosen to do.

void CTraceList::SelectAll()

Selects all the elements of the list. Typically called from the Select All/Ctrl+A handler of a view.

void CTraceList::setBoundary(int boundary)

Sets a boundary below which no elements will be deleted to make room for others as the specified limit is exceeded. Typically, this is used after a startup where basic parameters are recorded and should not be lost. The default boundary is -1, meaning there is no boundary and the first element in the list will be deleted to make space.

void CTraceList::SetFont(CFont * font)

This sets the default font for the entire display. Individual trace items may provide their own fonts (this feature is not implemented). In the absence of a specified font, this default font will be used for display.

void CTraceList::setLimit(int limit)

Sets the limit for the list box to contain no more than limit elements. An attempt to put more than limit elements into the list will result in an earlier one being deleted. If no boundary has been set (see setBoundary), then the first element of the list will be deleted to make space. If the boundary has been set, the boundary+1st element will be deleted to make space. If the value of limit is negative, limiting is disabled but the limit number is not changed (this is the same as calling enableLimit(FALSE)). If the limit is positive, it is set and enableLimit(TRUE) is effected. However, elements will not be deleted until a new element is added. Calling enableLimit(TRUE) will force the limit to be enforced. The limit cannot be set "too small". There is a minimum size; if a boundary is set, the limit is the minimum amount past the boundary that will be set.

void CTraceList::setShowThreadID(BOOL show)

If this is called with FALSE, the thread ID will not be displayed. The default is that the thread ID is displayed. 

BOOL CTraceList::setToDisk(BOOL enable)

Enables direct-to-disk logging. In this mode, every AddString operation will immediately write the item to a disk file. Each time this function is called with enable changing from FALSE to TRUE, the user is prompted for a filename. The filename specified will replace any file that already exists. If the user cancels the file dialog or there was an error opening the file, this function returns FALSE. In this mode, the file is repeatedly opened and closed, thus guaranteeing, insofar as the underlying system file caching permits, that the file contents will be on disk and consistent if the program crashes or even if the system crashes.

BOOL CTraceList::toEnd()

Moves to the last item in the list, makes it visible, and selects it. Any existing selections are removed. The result is TRUE. Usually called by the handler for a "Go to end" button.

BOOL CTraceList::toNext()

Moves forward from the current caret position to the next TraceError object in the list, makes it visible, and selects it. Any existing selections are removed. The result is TRUE if such an entry was found and FALSE if no such entry could be found. Usually called by the handler for a "Next error" button.

BOOL CTraceList::toPrev()

Moves backward from the current caret position to the previous TraceError object in the list, makes it visible, and selects it. Any existing selections are removed. The result is TRUE if such an entry was found and FALSE if no such entry could be found. Usually called by the handler for a "Previous error" button.

BOOL CTraceList::toTop()

Moves to the first item in the list, makes it visible, and selects it. Any existing selections are removed. The result is TRUE. Usually called by the handler for a "Go to beginning" button.

Code Download

download.gif (1234 bytes)A complete project showing the use of these classes under MDI can be downloaded. It was compiled under VC++ 6.0.

[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 © 2000, The Joseph M. Newcomer Co. All Rights Reserved
Last modified: May 14, 2011