C PROGRAMMING

In the Doghouse Again

Al Stevens

I am not exactly an old dog. New tricks are within my grasp if they aren't too extreme. But sometimes they come to me reluctantly and don't want to stick. Maybe I'm a middle-aged dog. (Last year I bought a mid-life Chrysler.) Here's an example: Several years ago I devoted a full summer to learning the Rhapsody In Blue (RIB) on, of course, the piano. This was no treat for my condo neighbors, mostly retired, who had to listen to tedious, all-day practice sessions for three months. By the end of that summer, and just as my neighbors were about to form an elderly but organized lynch mob, I had RIB almost to performance standards. I could rip the whole thing out end to end, including that bothersome staccato passage (which I am convinced Gershwin included solely to show off his pianistic technique).

I've since moved to the country: Those neighbors enthusiastically offered to throw me a going-away party and help me move--especially the piano. Today, having neglected RIB for a few years, I couldn't get past the first five measures, and I can't imagine how anyone can play that stupid staccato passage. See what I mean? If I had memorized it when I was a teenager, RIB would be with me forever. But, since I waited until well after the decline of my formative years, RIB didn't stick to my, er, ribs. Which, remarkably, brings me to encapsulation.

At about the same time that I took on RIB, object-oriented programming (OOP) was seeping into the mainstream. I credit the 386-class PC for bringing C++ to the masses, and Windows programming for making it downright necessary. At night, while my neighbors gratefully slept, I studied the few books available on C++ and OOP and experimented with early MS-DOS C++ compilers. By the end of that same summer, I had a good understanding of the syntax, if not the implications, of C++, and I thought I understood OOP, particularly encapsulation. The power of the C++ class was clear; by allowing us to add data types to the programming language, the class mechanism makes C++ a truly extensible language. Encapsulation, which is one part of class design, is a more rigorous form of that old chestnut--informationhiding--and everyone knows how good that is.

I relearned that lesson every time I wrote a program. When you encapsulate the implementation and interface of a class, the class is more rugged and the program is more reliable. But that lesson, although learned by example and practice, does not persist willingly, at least not in my doghouse. I know the lesson, I teach it, I consult for clients and recommend that they apply it, and sometimes I forget to use it in my own programs.

Case in point. Download and study the source code for the first version of Quincy 96, and you will find a conspicuous dearth of encapsulation.

Quincy 96 is a Windows 95 IDE front end to the gnu-win32 port of the GNU C and C++ compilers. As such, Quincy 96 includes an editor, project manager, compiler driver, and debugger. I've discussed these features and their implementations in recent columns. During the development of Quincy 96, this dog learned a lot of new tricks, mostly about Windows 95 programming and MFC. But a close examination of the source code reveals that, while I used the application/document/view class architecture of MFC, I eschewed the use of encapsulation in my own design unless it was handed to me on a platter. Of the four components, only the editor and project manager enjoy encapsulated implementations because they derive from MFC's CEditView class for editing and CListView to list the source-code files in a project. The other two components, the compiler and debugger, are intertwined in the application class in exactly the fashion that you would expect to see employed by an old C programmer. The data members are, essentially, static and global because all the application's functions can see them and because the member objects persist for the life of the application, needing individual initialization for each new compiler or debug session.

And the program does not work all the time. And I could not figure out why. Something should have perched on my shoulder and yelled "Encapsulate, you dog!" in my ear. Nothing did.

The compiler and debugger components have similar operations up front. They both launch other programs and they both watch those programs execute. I used the application class's OnIdle function to watch those executions. In response to the column that explained how I implemented the compiler and debugger, NuMega's John Robbins, who is a debugger expert in his own right, wrote to suggest that I use a Win32 thread and some events to operate and manage the debugged program from inside the IDE. He kindly provided some pseudocode, which sent me scrambling to the dreaded MFC docs to figure out how threads and events work.

Another reader suggested that I use the WaitForSingleEvent function to determine that a compiler program has completed. I implemented both suggestions, and the program worked better, but there were still several predictable ways to crash not only Quincy 96, but Windows 95, too, during a debug session. Some of the crashes involved the user doing unexpected things to console windows--such as closing them using the X button on the title bar while the debugger thought the program was still running. Other crashes occurred following a set sequence of operations with the debugger in a set sequence of debugging sessions.

Something landed on my shoulder and whispered in my ear, "One of those many data members in that complex and tightly coupled design is not getting properly initialized between sessions." From my other shoulder, finally, came the yell, "And guess why? You dog!"

Every old dog programmer knows that you can make some bugs go away simply by terminating the program and starting with a clean copy--like a new flea collar. The program will run at least one iteration without a problem. The bug comes later in subsequent iterations of the same execution, when the flea collar starts to show some wear. This is one problem that encapsulation specifically attacks. An object, properly constructed when instantiated and properly destroyed when it exits scope, exhibits the same behavior as a program that runs only one iteration. Objects of a reusable class do not have to be reexecutable. Finding a solution to my problem became obvious. Encapsulate those independent components of the IDE into classes, instantiate objects of those classes only while using the components, and the problems stemming from component reexecution will disappear. It's as if the dog gets a new flea collar for each new flea.

The new version of Quincy 96 encapsulates the debugger operations into a Debugger class, and the problems are mostly cleared up. I have not encapsulated the compiler operations because they work, and I hesitate to mess with something that works. But once again, I have relearned the encapsulation lesson, one that should never have been forgotten.

Now let's ask why some of us keep writing code the old-fashioned way. Having given it a great deal of thought, and being strongly motivated to attribute the trait to something more acceptable than an inability to learn new tricks, I have reached the following conclusion: We always underestimate the size and complexity of an unwritten program. In our minds, the code is going to be trivial because we have a clear picture of the process. This is particularly true of today's interactive GUI programs that are mostly user interface modules using canned component objects. Now, because we view the program as being small, we want to get it all down in as few source code files as possible. Seems reasonable. Each new class is a bother because it means a new header file, a new implementation file, and all those pesky interface functions. First we have to create a couple of new source-code files. Then we have to keep loading them into the editor to add functionality and to see how the classes work in the rest of the program. Since the program is going to be small, why not keep all that stuff together? Only later, when the program becomes large and unwieldy, do we rue that decision. I'm not saying that it happens every time. I'm not saying that it happens to everyone. I'm not saying that it happens to me every time. I'm just saying that it happens; therefore I am not an old dog. Woof.

Herman: An Odyssey

Herman is a new "C Programming" column project. Herman is a small document viewer program written in Visual C++ with MFC. I call it "Herman" because I doubt that anyone would want that name for a commercial software product title, and I am, therefore, unlikely to be treading on some obscure trademark. (That is precisely the reason I did not call it "BookWorm," my first choice. Someone is already using that moniker for a multimedia viewer program.) If, as can happen, I turn out to be mistaken, please let me know, and I will, as usual, wimp out and change the name to something else.

Herman's purpose is to display the contents of three books that I wrote. You will recall that Quincy 96 supports a C/C++ programming tutorial CD-ROM that I am developing to be published by Dr. Dobb's Journal. The lessons and exercises are taken from those books, and one of the features of the CD-ROM is that students will be able to read the full text of the books onscreen. The tutorial program, written in Asymetrix OpenScript, calls Herman with DDE protocols. It tells Herman to open a particular chapter in one of the books, and to page to and display a specified topic. That way, students can navigate easily between the interactive tutorial and the books. Once in the Herman text viewer, students can use the menus and tables of contents to read anything that interests them.

Herman is a general-purpose viewer, not hard-coded to the three books that I am using it for. When it starts up, the program scans the current subdirectory for files with a .toc extension. The names of those files, which use long filenames with spaces, are the same as the titles of the books that they represent. Herman uses the names of these files for two purposes: to enumerate the books on a menu and to derive the name of a subdirectory that contains the book's text files.

In addition to providing the book's name, the .toc file contains a list of files that represent the chapter text, and an indented table of contents for the entire book. Figure 1 is an abbreviated version of the file named "Al Stevens Teaches C.toc," which, for this example, has only a preface and two chapters. From Figure 1 you can deduce that there are three text files (named preface.rtf, chapter1.rtf, and chapter2.rtf) and that those files are in a subdirectory named "Al Stevens Teaches C."

Herman's text files use the rich-text format. This was an obvious choice for three reasons: First, I already have the text files in Word for Windows format, with graphics, paragraph styles for text, several levels of headings, italics, and boldface already in place. Second, Word for Windows can export a document to RTF. Third, MFC includes a custom edit control that supports RTF.

Finding a particular paragraph in a book is a straightforward process. Herman accepts the book title, chapter name, and paragraph heading as arguments. The program opens the book's .toc file if necessary, then associates the chapter name with its file name (based on the position of the text among the other chapter names, and the position of the filename in the filename list). The program opens the chapter file and searches the text for a match on the paragraph heading. The matching text must consist of a single line in the heading text color. This method assumes that chapter headings are unique within the chapter. If time permits, I'll add hypertext links and a search engine. Time, however, draws short due to problems I am about to describe.

I hope that Herman evinces all the sound design virtues extolled in my discussions on Quincy 96. The real story, however, is found not so much in how the final program works, as in what I went through to get it to work. The Odyssey begins.

Figure 2 displays Herman with an open chapter. Refer to that figure as you read the story of how it came to be.

Herman is not a typical MFC document/view program. Nor is it an OLE server or container. You don't open files or edit the text. The chapters are self contained, so there are no embedded objects. There is no Herman document, per se, that can be embedded in other applications. For those reasons, I decided not to use the MFC document/view architecture. But I wanted the MFC application architecture to handle menus, window titles, and so on. Consequently, I used the Visual C++ Developer Studio to build an SDI program with no bells and whistles. Then I removed the default-derived CDocument and CView classes, took the command line stuff out of the overloaded InitInstance function, instantiated an object of the CMainFrame class, and put a call to the Create function into its constructor. Now I had a skeleton application with nothing in it.

Next I derived a class from CTreeCtrl to manage the table of contents and a class from CRichEditCtrl to be the text viewer. I put instances of these classes into the CMainFrame class and allowed them to share space in the CMainFrame object's client window. Observe Figure 2 again. Herman uses a so-called "splitter" window. You can slide the splitter back and forth to change the ratio of the two views in the main frame window. I was unable to use the MFC CSplitter class to implement the splitter in the first version of the program because that class depends on an orderly document/view architecture, which I had eliminated. No problem. I instantiated two bordered windows as children of CMainFrame to represent the panes. Then I implemented my own splitter window by intercepting mouse drags and resizing those two panes. That worked great, although the confluence of the two window borders is not as slick-looking as MFC's splitter bar (shown in Figure 2).

Onward to implementing the text viewer.... I tried to use MFC's CRichEditView class, but ran into the first wall. Objects of CRichEditView insist on being views of document classes. So I created a document class from CDocument and found that CRichEditView insists on being a view of a CRichEditDoc class. CRichEditDoc is buried in the MFC class hierarchy way down under all the OLE stuff, which I didn't want. I subsequently decided to throw out the CRichEditView code and use only a CRichEditCtrl object for the text viewer. That choice involved a day of fooling around with that class's StreamIn function and all its stupid callback nonsense. The MFC docs are wrong in their discussion of the return value from the callback function. Eventually, I tried enough different combinations, and the text viewer worked.

Next came the table of contents. Following my earlier success with using a control as a child of the main frame window, I simply used the CTreeCtrl class as the base class for a table of contents class. Everything worked like a charm. I added the DDE stuff to allow the tutorial to call the program requesting a particular paragraph in a particular chapter of a particular book. I added the navigation code that chooses the right text when the user expands and selects items in the table of contents, and all was well.

Until the other shoe dropped.

I had been testing with small files of text from the first draft of a manuscript. Just to be sure everything worked, I built a full chapter that included figures and loaded the chapter into Herman. Guess what? Everything didn't work. Amazingly, the CRichEditCtrl class totally ignores graphics. The graphical metafile text is in the .RTF file, but apparently the control bypasses it when loading data. As an experiment, I built a quick-and-dirty MFC application with all the OLE stuff and the CRichEditView and CRichEditDoc classes, loaded the .RTF file, and the graphics displayed just fine.

I rushed to the MFC forum on CompuServe and posted an urgent message asking anyone to tell me how to get CRichEditCtrl to display graphics. That was two weeks ago. I'm still waiting for at least an acknowledgment, not to mention an answer. Forget CompuServe if you need help with MFC.

Much time and effort was burned as I tried many different combinations. It was clear that I had to use CRichEditView and CRichEditDoc if I wanted graphics in my displays. I built a single-document version of the application without the table of contents and got it working. Then I added the CSplitter object to the main frame window and put CreateStatic and CreateView calls into the main frame's OnCreateClient function, the way the docs tell you to do. Now I needed to add the table of contents again. It seemed obvious that I should use the CTreeView class since the editor needed document/view support. I'll jump over all the false starts and tell you this: If you have a single-document application and one of the views uses CRichEditView, forget using any other canned view. As near as I can tell, CRichEditDoc doesn't care to host any view that is not derived from CRichEditView.

Just as I was about to surrender, Addison-Wesley sent me a copy of a new book called MFC Internals written by fellow DDJ columnists Scot Wingo and George Shepherd. At first glance, it looks very good. I looked into what they said about the CSplitter class and read that a pane can host a window class other than one derived from CView. That information got me pointed in the right direction at last.

I tried using my CTreeCtrl-derived table of contents class as the table of contents pane, which worked in the text-only version of Herman. Everything compiled and ran, but the table of contents did not display anything and appeared to be empty. As an experiment, I instantiated a second object of the class, superimposed it over the dynamically created one, and the second tree displayed, but the CSplitter object would not allow this second object to receive any mouse clicks. This problem drove me to extremes; I used the Developer Studio debugger to trace through MFC code and observe the differences between how the two objects were treated. I saw that the CSplitter class's dynamic creation of the derived CTreeCtrl class causes the new object to lose its identity in MFC's kludgy pre-RTTI type-identification implementation. Without its identity, the object refused to accept and process the message that was supposed to add text to the control's tree-data representation.

By this time, I was slathering and babbling and running around outside and beating my head against the fender of my mid-life Chrysler. Judy came running out and shook me and slapped me severely several times to make me regain my composure and come to my senses. She seemed to enjoy it. If memory serves, she was smiling. I did not say, "Thanks, I needed that."

Today, calmed and with the restraints removed, I reached the final solution, and it seems simple now that I know it. Example 1 shows the main frame's OnCreateClient function, which associates the table of contents and editor views with the main frame's splitter window. The CTocWnd class is derived from CWnd and has an embedded CTreeCtrl object to implement the table of contents. To permit that object to receive keystrokes and mouse clicks, I had to make the CTreeCtrl window a child window of the CSplitter window. To allow the CTreeCtrl object to communicate with the text display, I had to let the main frame window receive the WM_NOTIFY messages that the CTreeCtrl object sends to report its activity. That was all there was to it. Today was a good day.

Howland OWL

You may recall that a while ago I got embroiled in controversy when I stated my preference for MFC over OWL. The OWL mavens came from everywhere to defend their favorite framework. Watch what happens now. Some smart OWL programmer is bound to write and tell me that not only are all of Herman's features possible by writing about three lines of OWL code, but that the OWL documentation includes comprehensive explanations and at least 12 different examples of precisely what I was trying to do. Make me howl and bark.

About: Much Ado About Nothing

When you use the Visual C++ Developer's Studio to construct the skeleton of a new application, the menu always includes a Help popdown with an About command. The source-code file for the implementation of the derived CWinApp class includes about 50 lines of generated source code to declare a CAboutDlg class complete with an empty message map, a do-nothing DoDataExchange function, lots of comments, and an OnAppAbout command function to instantiate and execute an object of the class when the user chooses the About command. All that code clutters the program because it is unnecessary. Example 2 shows the minimum code needed to achieve precisely the same effect.

The implementation in Example 2, which I installed in Quincy 96 and Herman, supports a dumb About dialog box--one that simply displays itself and waits for the user to close the box, which is exactly the kind of default About dialog box that the Developer's Studio generates. If you want a more intelligent About dialog box, you should start with what the Developer's Studio generates and build upon that code instead of using the minimum implementation that Example 2 shows.

Source Code

The source code files for Quincy 96 and Herman are free. You can download them from the DDJ Forum on CompuServe and on the Internet by anonymous ftp; see "Availability," page 3. To run Quincy, you'll need the GNU Win32 executables from the Cygnus port. They can be found on ftp.cygnus.com in pub/sac. Get Quincy 96 first and check its README file to see which version of gnu-win32 you need. Every time they release a new beta, I have to make significant changes to Quincy 96. As I write this, the latest beta is Version 13 and Quincy 96 works with Version 10.

If you cannot get to one of the online sources, send a 3.5-inch high-density diskette and a self-addressed, stamped mailer to me at Dr. Dobb's Journal, 411 Borel Avenue, San Mateo, CA 94402, and I'll send you the Quincy and Herman source code (not the GNU stuff, however--it's too big). Make sure that you include a note that says which programs you want. The code is free, but if you care to support my Careware charity, include a dollar for the Brevard Community Food Bank.

Figure 1: Abbreviated version of "Al Stevens Teaches C.toc."

preface.rtf
chapter1.rtf
chapter2.rtf
<end>
Table of Contents
    Preface
    Chapter 1. An Introduction to Programming in C
        A Brief History of C
        C's Continuing Role
    Chapter 2. Writing Simple C Programs
        Your First Program
            Identifiers
            Keywords
        Variables
            Characters
            Integers
        Constants
        Expressions
        Assignments
        Summary

Figure 2: The Herman application.

Example 1: CMainFrame::OnCreateClient function.

BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT,CCreateContext* pCtxt)
{
    m_wndSplitter.CreateStatic(this, 1, 2);
    m_wndSplitter.CreateView(0,0,RUNTIME_CLASS(CTocWnd),
                                    CSize(130, 50),0);
    m_wndSplitter.CreateView(0,1,RUNTIME_CLASS(CViewView),
                                    CSize(0,0),pCtxt);
    m_pTocWnd = (CTocWnd*)m_wndSplitter.GetPane(0,0);
    m_pViewer = (CViewView*)m_wndSplitter.GetPane(0,1);
    return TRUE;
}

Example 2: Minimum OnAppAbout implementation.

void CYourApp::OnAppAbout()
{
  CDialog aboutDlg(IDD_ABOUTBOX);
  aboutDlg.DoModal();
}