The MFC Console Framework

Overview

gratuitous screen shot

The MFC Console Framework lets you write GUI programs that are almost as quick-and-dirty as console mode programs. Output goes to a scrolling listbox, something like a console mode window. Input comes from menu items and dialog boxes.

This framework isn't very useful for building end-user programs. I use it at work to make quick test programs, or test harnesses for libraries I write. The output window lets me write out debugging messages without having to toggle back to Visual Studio's Debug pane and look for ODS strings among all the other stuff in the Debug area.

Status Reporter

The framework comes with my Status Reporter library. Status Reporter is like Visual C++'s TRACE facility on steroids. It has several advantages over TRACE:

    1. It's based on C++ streams, not printf()-like functions. This is more than a style issue: streams are simpler to use, since you don't have to count arguments to know whether to use TRACE1, TRACE2, TRACE3...etc. Streams are also type-safe and can print out any type of object that has an operator<< defined for it.
    2. There are 5 levels of output:
      1. Fatal errors - Beep, pop up a message box, and kill the program.
      2. Problems - Beep and send the message to the output area.
      3. Normal status - Send the message to the output area.
      4. Internal status - Log the message internally.
      5. Verbose status - Ignore the message unless verbose logging is enabled.
    3. The mechanism preserves the file name and line number where errors occur. These are passed all the way down the call chain, so that your particular subclass can use them or not, as it sees fit. I use this feature to send file name and line numbers to the debug log file, but leave them off when I'm showing messages to the user, for example.
    4. The "error pre-handler" mechanism lets you define a global function that is called before any status message is displayed. The feature was created to handle the case when a fatal message (which comes up in a message box in my programs) happened while the splash screen is up. The pre-handler takes down the splash screen so the user can see the fatal error. You may find other uses for this feature.
    5. The mechanism allows you to define message wrapping rules, such as wrapping messages for Windows message boxes so the box is, say, 300 pixels wide.
    6. The library is very flexible and extensible.

The last point is key. Take, for example, the way the 5 output levels are handled. Your program is in complete control of where the "output area" is, whether a message gets sent there or is ignored, whether or not to use a log file to back up the Windows OutputDebugString() mechanism, etc. Each program derives a special status reporter to handle these local issues. Plus, because the Windows functionality is in a separate subclass, you can use this library on non-Windows platforms by deriving from the base class. We do this on our Linux-based server programs at work.

What is an "output area"? It depends on your program. For many modern GUI programs, it's a status bar. For the MFC Console Framework, it's the main UI's list box. For a Unix server program, it might be stdout, stderr, or the syslog() function, depending on the message's output level. It's completely up to you where to send output messages.

Multiple output levels is very nice. It lets your program have certain debug messages in it that normally the user never sees. Then if you need to get some debug info from a customer in the field, you can have them pass a command line option or something that turns on a higher debug level.

The "verbose status" level is handled separately, so you can put potentially embarrassing messages in your program without giving away the secret to trigger them to just anyone. (E.g. "Oooops, module FROBOZZ has a bug at line 42") Sure, it's preferrable not to have embarrassing messages in the program, but sometimes you just have to have them, like in default cases in C switch() statements. Those bits of code should never be executed, but pretending they won't ever be is just naïve.

Using the MFC Console Framework

Nothing could be simpler: copy the project, and start hacking on it!

Using the Status Reporter Separately

The MFC Console Framework is set up to build the Status Reporter as a separate library. Just copy that library and the files StatusReporter.h and WindowsStatusReporter.h to another directory.

In the project that uses the Status Reporter, you need to derive a subclass, probably of WindowsStatusReporter. Look in ConsoleStatusReporter.* for the code to the MFC Console Framework's version: you can probably copy this class, rename it, and have it working for your particular setup with very little effort. The most common change is to have it send messages to your particular program's output area. Other things you might consider are keeping a message log so the user can view past messages.

Somewhere early on in your application's initialization, you need to call MyStatusReporter::GetInstance() to create the global MyStatusReporter instance. The file that calls that function the first time will need to #include MyStatusReporter.h; all other modules can just #include StatusReporter.h, because once the global MyStatusReporter instance is created, you don't need to refer directly to the leaf class ever again. This makes it possible to make major changes to that leaf class without modifying all the code that uses the Status Reporter mechanism.

Sending messages through the Status Reporter mechanism works almost exactly like C++ stream I/O, except that you have to say what level of output you want. (Kind of like the difference between cout and cerr.) Some examples:

	int n = 42;
	REPORT_NORMAL_STATUS("n is " << n);
	if (n != 42) {
		REPORT_PROBLEM("Odd, n isn't 42.");
	}
	else if (n / 3 == 13) {
		REPORT_FATAL_ERROR("Uh, oh, mathematics is breaking down...");
	}
	
	if (n % 7 == 0) {
		REPORT_INTERNAL_STATUS("n is evenly divisible by 7");
	}
	else {
		complex c(n);
		REPORT_VERBOSE_STATUS("Just a streams test: " << c);
	}
	REPORT_STATUS(n);

The meaning of the last line is not obvious. The REPORT_STATUS macro lets you look up status strings by some number, such as a string ID in a Windows STRINGTAGBLE.

The Main Code Path

The Status Reporter mechanism is a little bit complex. This is because I've been using this mechanism for about four years now, so it has evolved to handle quite a few real-world situations that aren't obvious initially. To explain how this library works, let's trace take the "lookup status message by number" path: it happens to take us past most all the important bits of functionality.

    1. The user makes a REPORT_STATUS(n) call. This is a macro which basically just calls StatusReporter::ReportStatus(int, ...)
    2. ReportStatus(int, ...) calls GetStatusMessage. A subclass overrides this to provide its own lookup mechanism, such as looking messages up in a Windows STRINGTABLE resource. My programs often have a few hard-coded error values for things like "out of memory", because when you're out of memory, you probably can't load string table entries. My version of GetStatusMessage checks for these few hard-coded values, and if it doesn't find a match, it resorts to a stringtable lookup.
    3. The InferDebugLevel() function is called. The subclass must override this in order to assign a debug level to the message. I use the scheme "message_id / 1000". So, fatal errors are in the range 1000-1999, problems 2000-2999, etc. You can use any scheme that makes sense to you.
    4. ReportStatus(ostream&, ...) is called with the message we looked up and the inferred debug level. (For the other REPORT_* macros, like REPORT_PROBLEM, this is the normal entry point: the first argument is the debug stream the message got inserted into. The REPORT_* macros' main function, in fact, is to set up that stream.)
    5. The error pre-handler is called, if one exists.
    6. If this is the second or subsequent fatal error, we demote it to a verbose status message. Often when a program is dying due to some fatal error, several things go wrong at once, or the first fatal error causes several other things to go wrong. This feature quashes all but the first one, so the user doesn't get a big long string of message boxes they have to click on to get the program to die.
    7. The WrapMessage function is called on the message. (Wrapping is putting newlines in the message, because some output methods require it.) This is is a no-op by default, but your subclass can define its own message wrapping rules based on the debug level of the message. My programs wrap fatal errors, for example, because fatal errors in my programs go to Windows message boxes. You could use it in a Unix program to nicely wrap output lines to 70 characters, split on word boundaries, for example.
    8. The semi-finished message is passed to DisplayMessage. This is the main thing your subclass has to override. It's where you take the message and send it to your output area. You can also do things like tacking it onto a a message log, or sending it out to a log file, etc.

There's one other macro you can use with Status Reporter: END_PROGRAM(). This is basically just a REPORT_FATAL_ERROR() with no message: the mechanism understands that this just means "end the program without saying anything about it." It's up to your Status Reporter subclass to define what "ending the program" means, though.

Downloading

There is a zip file containing a Visual C++ 5.0 project here for you to download. It is intended that you copy this project and write your new code on top of it, but of course you could also pull bits and pieces of it into an existing project.

License

This mechanism is under a BSD-like license. That means you are basically free to do anything you want with the code, as long as you agree not to sue me or my employer. :) The full text of the license is here.


Last modified on 17 February 2001 at 06:06 UTC-7 Go to my home page