Better to build on what MFC gives you than reinvent a whole new wheel.
Introduction
Windows controls are impressive creatures. Plenty of functionality is right there for very little extra typing. But if you want your controls to be even a little bit different from "the Microsoft way," you've got some work cut out for you. Part of the work is to wade through Microsoft's scattered and disorganized documentation. Looking through various Windows programming books will give you more ideas. Unfortunately, some of the expert advice, such as "subclass the window," normally requires spreading code across what should be unrelated source files.
There's really little need for this run around. Do you know C++? Can you derive new classes? If so, then sprinkle in a little knowledge of how Windows passes messages and you can make custom Windows controls. A control's MFC class is just like any other C++ class. And just like any other class, public derivation from it will add new functionality while keeping the base class members available. In this article I show how to extend a Windows control, through encapsulation, inheritance, and judicious use of the Standard Template Library.
The example application presented here is a customized list box. It's easy to add new keyboard-driven commands as well as other member functions to this list box. I use the STL components just like any other useful tool. I consider the STL tools to be for everyday use and not to be held in reserve for some special application. So in this application, I show some simple, everyday uses for the STL. I also show how adding an STL component can help support an abstraction, thereby making it easier to use.
A Better List Box
The standard MFC list box, CListBox, does a lot for free. You can use the arrow keys to go through the list. You can click a list box item and the system will highlight the clicked entry. There are member functions for adding and deleting strings. But for creating and maintaining the list box data, the CListBox class falls far short. Hitting the delete key to remove an entry sure is handy, but how about double clicking a list box entry to edit that particular string? Why won't the Insert key allow us to enter a new string? And if we are going to enhance the keyboard interface, shouldn't the new list box understand the only truly intuitive keyboard interface yet invented and understood: Wordstar commands? [I thought it was emacs! --mb] The list box presented here, ListBoxWithExtras (LBWE) does all this and more.
When I started, I set two critical goals for the LBWE implementation. First and most obvious, it had to "work right." The other, almost equally important consideration was that the new box had to be utterly simple for a programmer to install and use in dialog boxes. For this project, "utterly simple" means that all the programmer need do is #include "ListBoxWithExtras.h" and then start declaring variables. I didn't (at least not yet!) achieve "utterly simple," but I think this comes darn close.
Implementing the Listbox
Some Constraints
Take a look at Listing 1, ListBoxWithExtras.h. The "utterly simple" goal puts one important restriction on the implementation: the constructor must have no arguments. Why? Because more likely than not, an LBWE will be stuck inside a dialog box. If you add constructor arguments to ListBoxWithExtras, you'll be forced to modify the parent dialog's constructor in order to pass along the derived control's constructor arguments. So an LBWE can't be told anything about itself. If the LBWE needs info, it alone must know how to find it.
Another restriction is that when using the MFC framework, the enhanced control can't know immediately when it has been activated. In other words, no WM_CREATE (or one of its close relatives) gets sent to tell the enhanced control that it is now a window and can do window type stuff. That's because the application code gets to look at things only after the list box receives its WM_CREATE. ListBoxWithExtras gets around this limitation by scanning in DefWindowProc for a "first time through" and uses this to trigger initial population of the box and such.
The last constraint is that since more than one LBWE can be active, the application can't use static variables to store any aspects of the listbox state.
Class Declarations
The enhanced control's class declaration looks pretty much like any derived class declaration. The structure declared outside of the LBWE declaration, ListBoxState, is logically a part of the LBWE class. The application uses ListBoxWithExtras to keep track of the current state of an LWBE. To do this, the application pushes ListBoxState objects onto an STL previous-state stack. Pushing the state allows Undo to pop the stack and restore the previous state. This also explains why ListBoxState has a constructor -- only because a constructor is required to place an object into an STL collection.
Ideally, ListBoxState would be part of the LBWE class. But STL's <deque.h> complains that ListBoxState can't be found (no matter how I qualify it) if ListBoxState is declared within the LBWE class header. (See the sidebar for more detail. It will likely take Microsoft one or two more releases of VC++ before it integrates seamlessly with the STL.)
Note also the private structure within the LBWE declaration near the end called CompareCStrings. This structure is a function object, which is used with the STL sort template. For whatever reason, the operator< that MFC uses for CStrings cramps STL's style when used with the two-parameter sort call. By enclosing the < operator for CStrings within a function object, the three-parameter sort works.
I consider the list box to be a visual representation of a list of strings. If the list box implementation supports this abstraction, the list box will be easier to use. LBWE directly supports this abstraction via its lbweBuffer public member.
As an object of type lbweRep, lbweBuffer is an instantiated STL container. Listing 2, LbweRep.h and LbweRep.cpp, shows that an lbweRep object is an STL vector of CStrings with some conversion functions added. You can assign an STL list of CStrings, an STL vector of CStrings, or for those who enjoy the MFC CStringList, a CStringList of Strings to the lbweRep. Conversion functions are also available to extract the list box data in a manner that better fits how you want the abstraction to work.
With a little help from the STL, you can initialize the list box as a list (or vector) and extract the strings from the box in a similar manner at any time. If you are working with lists of strings, there's no need to break apart your abstraction to use a list box and then recreate the abstraction when you need to extract the strings from the box.
Message Handlers
All that's left to discuss about the LBWE header are the message handlers that Class Wizard maintains. The OnChar and OnKeyDown handlers (see Listing 3) are for the enhanced keyboard controls. The OnDblclk handler allows an application to do something when a list box selection is double clicked. The last added handler, OnParentNotify, supports an in-place editing box. Due to space limitations, this article does not discuss in-place editing, but all the mechanics are there in the full source code archives (see p. 3 for details). Class Wiz sets up shells for the handlers. It's the programmer's job to fill in the details.
Let's start with the simplest message handler, OnDblclk. The last half or so of Listing 3, ListBoxWithExtras.cpp, shows the message handler implementations. OnDblclk just calls EditEntry. Adding message handlers with the help of Class Wiz is a breeze.
The keyboard handlers are almost as trivial. OnChar handles "real characters" while OnKeyDown handles "non character" keys that OnChar won't trap. If you look at OnChar near the end of Listing 3 you'll see how "Wordstar emulation" is added. OnKeyDown handles the "special keys" such as Insert, Delete, and function keys. (One note of warning: some keypresses will never make it to your OnKeyDown handler. For example, a control's OnKeyDown never gets a crack at Enter or Esc if the control is inside a dialog box.)
Enhancements
What's left? Just the implementation of the new control enhancements. AppendEntry and InsertEntry are the easiest. When the user enters a string, the code has only to stick the string into the right spot in the list box. Not surprisingly, AppendEntry and InsertEntry are the smallest of the LBWE enhancements. Other than starting up the edit/entry process, AppendEntry adds an empty string to the bottom of the list -- and this is simply for cosmetic purposes. It appends and highlights this extra slot just to make sure that the list box has a window available for where the editing will take place. InsertEntry is equally trivial.
DeleteEntry introduces a complication that is a hidden a part of other LBWE content-changing members. As changes to the box contents are made, the content-changing member functions save a history of those changes into an STL private stack. Undo pops the stack in order to get the parameters necessary to restore the list box's previous state. Note how easy it is to add something like a stack and all its supporting code. You don't need to be writing some fancy program to make effective use of the STL. Quite the contrary, the STL is something that has been designed as an everyday work tool.
ResetUndo has a somewhat deceptive name. While it does reset the undo stack, it also performs a resychronization of the list box contents with its STL-derived representation. ResetUndo becomes useful when a program changes the box contents outside of the LBWE functions, by calling the CListBox::AddString or DeleteString members directly. This synchronization occurs when the edit box is activated so you can populate the box manually or via its somewhat polymorphic buffer. The resynch works in one direction only. The LBWE internal container changes to accurately reflect the contents of the list box, but there is no complimentary "apply now" member which would update the box to represent the container (except as mentioned for box startup). The "apply now" capability isn't needed because the underlying container abstraction is automatically updated within the enhanced controls.
ResetUndo is also used in conjunction with SortList (not shown here -- available in the full source code archives), so you can't unsort a list box after you've sorted it. (The ability to restore a sorted box to its previous unsorted state seemed a feature of dubious utility!)
SortList itself may seem out of place. After all, you can declare a list box as sorted or not, in either the resource editor or as part of the window style if you Create a list box directly. But I've used LBWE in applications where sometimes the box needs to be a sorted box and sometimes not. Adding the option of letting the user sort it if need be just made things easier. (Besides, it's not as if sorting an STL container takes more than a few seconds of programming time.)
Extra Goodies Not Shown
We are essentially done. The easiest way to finish up would be to integrate a dialog box into the LBWE and just get/edit the new entry. That would not ony work fine, it would also still keep LBWE totally self contained. But I did something a little fancier with LBWE. It does "in-place editing." The list box's selected entry slot will be the editing window. Also, the edit box will have modal dialog-like properties. That means that the user can (and must!) press Enter after typing the string. If the user presses Esc, the edit is aborted and the list box state won't change. I don't have room here to show how this is implemented, but the full source is available on the CUJ code disk and online sources (see p. 3 for details).
Using an LBWE
So just how do you use an LBWE? If you need an LBWE inside a dialog box, fire up your resource editor and add an appropriately shaped list box to a dialog box resource. Then use Class Wiz to add a control type member variable for your list box.
When that's done, you need to edit the dialog box's header file and add #include "ListBoxWithExtras.h". The last step is to find the control variable for the list box which you added with Class Wiz. Change the variable's type from CListBox to ListBoxWithExtras and then move the declaration "out of Class Wizard's way."
You can initially populate the list box the old fashioned way, with AddString etc. in the parent dialog's OnInitDialog. But there's no need to do this since LBWE directly supports the "list (or vector) of strings" abstraction. After the dialog box is defined and before it's displayed, you can just assign an STL list or vector of CStrings (or an MFC CStringList) to the dialog's lwbeBuffer and LWBE will take care of getting everybody into the box. You don't have to spread code over unrelated source files just to satisfy the demands of the MFC framework.
When you need to extract the strings from the list box, the containerized representation is ready to assist. LBWE already supplies member functions to turn the internal representation into an STL list, an STL vector, or an MFC CStringList, and it is quite easy to add support for other data structures.
Conclusion
As far as the "utter simplicity" goal goes, this program comes mighty close. In addition to adding the header, all you need do to use it is make a minor change to the dialog header file.
So if you know how to derive classes, you too can create custom window controls. Not only that, but using derived control classes can make difficult jobs more tractable. We can encapsulate certain features and operations within its own class and just sit back and wait for messages.
Finally, I would like to thank two of the brightest guys I've come across, Tim Gregson of Microsoft in Redmond, WA and Jerry Coffin of TAEUS in Colorado Springs, CO, for their help in my quest to understand Windows. o
Since receiving his BS in Chemical Engineering from the University of Missouri-Columbia in 1980, Chris Downs has developed software applications ranging from process control to networking, text processing, business apps, and even programs for scouting athletic teams. He is currently developing Sales Force Automation software for Saleskit Software, St. Louis, MO. You can reach him at Chris.Downs@slug.org.