Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

.NET

Subclassing Applications


SP91: SUBCLASSING APPLICATIONS

SUBCLASSING APPLICATIONS

This article contains the following executables: PROGEDIT.ARC

Mike Klein

Mike is a software engineer and specializes in Microsoft Windows, HP New Wave, and Novell Netware. He is also the author of several books and numerous magazine articles, and can be reached at 500 Cole St., San Francisco, CA 94117, via CompuServe at 73750,2152, and on M&T Online as MikeKlein.


"Subclassing: A window or set of windows that belong to the same window class, and whose messages are intercepted and processed by another window function (or functions) before being passed to the class window function." -- Microsoft Windows SDK

"Subclassing: A legal means by which a programmer can appropriate and use code and objects developed by others." -- Mike Klein

Subclassing is a method of intercepting and possibly processing the messages going to an object, whether it be an application's menu bar or a custom control. Messages going to an object may be logged (examined and passed on), acted on and passed along, acted on and then discarded, or just discarded altogether.

You don't have to be using C++ or SmallTalk to benefit from subclassing techniques, since Microsoft Windows supports subclassing and several other object-oriented programming methods as well, including easy code reuse and inheritance. All you need is a Windows-approved C compiler and the Windows SDK to start programming in an object-oriented environment.

Any menu or window on the desktop can be hooked into and subclassed. This means that anything is fair game, whether it's the listbox in your application that you need to enhance, or the menu bar in Aldus Pagemaker that needs an extra command or two. I have to admit, at first this made me think a little about the legal ramifications. However, as long as you've actually purchased the application, nobody can really complain of any wrongdoing. After all, you haven't actually modified anybody's code -- just the way it interacts with Windows and other objects. One heck of a lot of control can be gained by subclassing an application or control. With the addition of Windows' EXE-HDR and Spy utilities to dump a program's internals and view a program's internal message processing, you can pretty much learn anything you need about an application and how it was developed.

Manipulating Objects

The key to subclassing is Windows' open architecture -- every window has an open and documented message-based interface through which creation, manipulation, display, and destruction can be accomplished. Not everything, however, can be done with a message; sometimes you need to modify a window's internal structure. The benefits of subclassing can be achieved in one of two ways: by hooking into a window function chain and passing unprocessed messages down the line; or by creating a new window class.

To illustrate subclassing, I'm including with this article two programs: ProgEdit, a spawned copy of Notepad (with the important distinction of having an extra menu option for selecting tab stops, see Figure 1); and BetterListBox, an example of a superclassed control.

All too often I've wanted to view program source code with Notepad and ended up with poorly formatted output. ProgEdit was a quick fix to an annoying problem. In fact, I think that this classifies as one of those few times that a "quick" project actually ends up being finished quickly, and turns into a useful utility.

BetterListBox, on the other hand, is an example of subclassing a listbox control to add data entry and other input enhancements. Too many Windows applications take the cheap way out with Windows' built-in listbox class, which is hardly designed for speedy data input. Windows itself is very inconsistent in how it handles listboxes and combo boxes in general. BetterListBox lets you build a control that will enhance future applications.

After reading this article, you'll see that subclassing is a powerful and easy technique for developing code. The benefit of not having to debug the other developer's code (which hopefully already works) alone justifies the simple interfacing required to subclass an object. Windows per se doesn't make subclassing difficult; it's the poorly laidout SDK manuals (not enough cross-referencing) and lack of complete descriptions for all the different window messages. The manuals just aren't clear enough when it comes to the Windows nitty-gritty, meaning heavy memory management, subclassing, owner draw, MDI, and complicated graphics issues.

The Inside Skinny

For both ProgEdit and BetterListBox, we need to take a look at the structure common to all windows. This structure WNDCLASS, is shown in Example 1.

Example 1: The WNDCLASS structure, common to all windows

  struct WNDCLASS
  {

    LPSTR    lpszClassName;                /* Window class name   */
    WORD     Style;                        /* Window class style  */
    long     (FAR PASCAL *lpfnWndProc)();  /* Window class func   */
    int      cbClsExtra;                   /* Class extra data    */
    int      cbWndExtra;                   /* Window extra data   */
    HANDLE   hInstance;                    /* program instance    */
    HICON    hIcon;                        /* Class icon to use   */
    HCURSOR  hCursor;                      /* Class cursor to use */
    HBRUSH   hBrushBackground;             /* Class bckgrnd brush */
    LPSTR    lpszMenuName;                 /* Class menu name     */
  };

We're most interested in the window's class function, which is responsible for processing messages for the window class. The functions in Example 2 manipulate a window's internal structure and provide the hooks needed for subclassing an object.

Example 2: Functions that manipulate a window's internal structure and provide the hooks needed for subclassing an object

  BOOL   GetClassInfo (HANDLE hInst, LPSTR lpClassName, LPWNDCLASS
                      lpWndClass);
  HMENU  GetMenu      (HWND  hWnd);
  HMENU  GetSubMenu   (HMENU hMenu,  int   nPos);
  HMENU  GetSystemMenu(HWND  hWnd,   BOOL  bRevert);
  LONG   GetWindowLong(HWND  hWnd,   int   nIndex);
  LONG   SetWindowLong(HWND  hWnd,   int   nIndex,      DWORD dwNewLong);
  WORD   GetWindowWord(HWND  hWnd,   int   nIndex);
  WORD   SetWindowWord(HWND  hWnd,   int   nIndex,      WORD  wNewWord);

ProgEdit: Hooking Into a Foreign Application

ProgEdit demonstrates how to attach to a window function, passing any unprocessed messages down the chain to other handlers, and eventually to Windows itself. It doesn't require too much description because it's a pretty simple program. Listings One through Five show the actual source code for ProgEdit, including the header file, make file, definition file, and so on.

ProgEdit kicks in by validating its window function that will intercept messages destined for Notepad's window function. Validation, normalization, and a proc instance, or "thunk," is how Windows resolves its dynamic linking problems, whereby function addresses are computed at runtime. Next, ProgEdit uses one of Notepad's window handles to set a new value for Notepad's edit window function. We're essentially POKEing in a new value for the window class function to pick up and use. In return, we get a PEEK indicating the old value of whatever we changed, which in this case is the original edit window's class function. We'll use this function pointer later in our own window proc when we need to pass on message processing to the window's original function.

Next, a couple of menu-related functions query the Notepad main window for a menu handle, and append a new menu item on the menu bar. It's the "item ID" of this menu item that we're filtering and trapping for our custom window procedure.

It's a simple as that. Any messages saying somebody clicked on the "Tab" menu item are processed by us; any other messages are passed onto the window's original procedure. Tabs are set by a simple dialog box that pops up and asks the user for a tab amount. The tab amount is remembered by a custom profile statement. I have to admit, the profile setting initially seemed like a good idea, but now doesn't really make much sense. I mean, how often do I switch from my default (which is four)?

BetterListBox: Making the Bad Better

Windows is an incredible "software tinkertoys" set. The building blocks may be extremely simple, but then again so is the atom, and look what can be created from enough of them! From Windows' base object window classes, several new classes may be created, including combo boxes (at a low level) and spreadsheets (at a slightly higher level). The beauty of Windows is that it usually provides several ways to approach a problem, each with its own trade-offs. Windows' listboxes are a perfect starting point for building a new control.

At first glance, it would seem that listboxes are extremely powerful, but in fact it's quite the opposite, as they depend upon Windows' GDI for most of their flash. Listboxes have several serious shortcomings, first of which is primitive data entry capabilities. Listboxes are by default read-only -- they can't be edited and traversed like an Excel or Wingz spreadsheet. Second, there are too many inconsistencies between single-, multiple-, and extended-selection listboxes. Single-selection listboxes don't have an initially highlighted item; however, once you set the highlight you can't remove it (using the keyboard). The API is also lacking a few key listbox messages. So what does this mean?

Subclass and Live to Tell the Story!

Although creating BetterListBox only took a couple of days, getting used to the different messaging quirks made programming a nightmare. (Listings Six through Ten, page 24, show the actual source code for ProgEdit, including the header file, make file, definition file, and so on.) All too many listboxes in commercial Windows applications have the same three buttons to the right of them: Add, Delete, and Edit. While this is fine for the keyboard illiterate and mouse retentive, it isn't so good for clerks updating inventory or temps being paid for their productivity. Not only does Microsoft need to make Windows work fast, but it needs Windows to allow people to work fast too -- an important difference that's been overlooked by too many GUI applications. BetterListBox was written specifically for single-selection, single-column listboxes. However, with the addition of probably ten lines of code, it should function with multicolumn and multiple-selection listboxes as well.

The key to creating a good listbox is to trap for some common-sense characters, such as DEL, INS, ENTER, PGUP, PGDN, and so on. Although Windows has a listbox style called LBS _ WANT - KEYBOARDINPUT, it will only send you keystrokes when it has the input focus. Note that listboxes don't get the focus when they are empty, which is a big problem when you want to add something to an empty listbox.

Besides allowing a lot of keyboard shortcuts, I wanted it to be easy to edit items inside the listbox. Whereas most applications pop up a dialog box to enter a new item, BetterListBox creates an edit control exactly over the currently highlighted listbox cell and copies the contents of the listbox entry into the edit window, afterwards setting input focus to itself. With BetterListBox, pressing DEL deletes the current listbox cell, pressing INS copies the current entry and inserts a new listbox cell at your current position, and pressing the ENTER key switches in and out of edit mode for the listbox cell. The nice thing about the edit mode is that all the normal listbox navigation keys function as normal, including the up and down arrow, PGUP, and PGDN. Another added benefit is that you're kept in edit mode the whole time, as in a spreadsheet. The middle and right mouse buttons (single- or double-clicking) act just like the left mouse button, putting them to good use. The other edit control window class that I created, BetterEditCtrl, is also a good start into designing an even more enhanced version.

If multiple controls are present in the same dialog box, you'll need to respond to the window message WM_GETDLG-CODE, which is used by Windows to allow a particular control to process the navigation and interaction between the other controls in the dialog. There can be several window procedures in a chain filtering messages for the same window, so Windows has to ask which one will be responsible for processing this input. This means that you too can be in charge of processing the TAB, reverse TAB, and arrow keys. This really doesn't present a problem, though. In the case of the TAB key, you'd want to call GetNextDlgTabItem and set the focus to whatever window handle is returned. What this function does is tell you, based on the ID of the control you pass, which controls are ahead of and behind you (controls with the WS_TAB-STOP setting, that is). In this manner, you can process the TAB and reverse TAB keys quite effortlessly.

Conclusion

I welcome any suggestions for improving BetterListBox, and will be posting any changes I make to the code to the Online BBS, which contains quite a number of other Windows programming utilities and source as well. The listings for both ProgEdit and BetterListBox are commented quite extensively, so start reading, playing, and incorporating subclassing techniques into your own applications.


_SUBCLASSING APPLICATIONS_
by Mike Klein


[LISTING ONE]
<a name="02bb_000d">

# Standard Windows make file.  The utility MAKE.EXE compares the
# creation date of the file to the left of the colon with the file(s)
# to the right of the colon.  If the file(s) on the right are newer
# then the file on the left, Make will execute all of the command lines
# following this line that are indented by at least one tab or space.
# Any valid MS-DOS command line may be used.

# This line allows NMAKE to work as well
all: progedit.exe

# Update the resource if necessary
progedit.res: progedit.rc progedit.h progedit.ico
    rc -r progedit.rc

# Update the object file if necessary
progedit.obj: progedit.c progedit.h
    cl -W4 -c -AS -Gsw -Oad -Zp progedit.c

# Update the executable file if necessary, and if so, add the resource back in.
progedit.exe: progedit.obj progedit.def
    link /NOD progedit,,, libw slibcew, progedit.def
    rc progedit.res

# If the .res file is new and the .exe file is not, update the resource.
# Note that the .rc file can be updated without having to either
# compile or link the file.
progedit.exe: progedit.res
    rc progedit.res





<a name="02bb_000e">
<a name="02bb_000f">
[LISTING TWO]
<a name="02bb_000f">

#define IDC_TABAMT 100

int  PASCAL WinMain(HANDLE, HANDLE, LPSTR, int);

LONG FAR PASCAL MyMainWndProc(HWND, unsigned, WORD, LONG);
BOOL FAR PASCAL TabAmount(HWND, unsigned, WORD, LONG);







<a name="02bb_0010">
<a name="02bb_0011">
[LISTING THREE]
<a name="02bb_0011">

; module-definition file for Progeddit -- used by LINK.EXE

NAME         PROGEDIT   ; application's module name
DESCRIPTION  'Progedit - Programming Editor'
EXETYPE      WINDOWS       ; required for all Windows applications
STUB         'WINSTUB.EXE' ; Generates error message if application
               ; is run without Windows

;CODE can be moved in memory and discarded/reloaded
CODE  PRELOAD MOVEABLE DISCARDABLE

;DATA must be MULTIPLE if program can be invoked more than once
DATA  PRELOAD MOVEABLE MULTIPLE DISCARDABLE

HEAPSIZE     1024
STACKSIZE    5120      ; recommended minimum for Windows applications

; All functions that will be called by any Windows routine
; MUST be exported.

EXPORTS
    MyMainWndProc @1
    TabAmount     @2







<a name="02bb_0012">
<a name="02bb_0013">
[LISTING FOUR]
<a name="02bb_0013">

#include <windows.h>
#include "progedit.h"

ProgEdit ICON PROGEDIT.ICO

TabAmount DIALOG 11, 25, 75, 24
CAPTION "Tab Amount"
STYLE WS_POPUPWINDOW | WS_CAPTION
BEGIN
    CONTROL "Tab Amt:", -1, "static",
        SS_RIGHT | WS_CHILD,
        10, 6, 30, 12
    CONTROL "4", IDC_TABAMT, "edit",
        ES_LEFT | WS_BORDER | WS_TABSTOP | WS_CHILD,
        45, 6, 20, 12
END








<a name="02bb_0014">
<a name="02bb_0015">
[LISTING FIVE]
<a name="02bb_0015">

/**************************************************************************
PROGRAM: ProgEdit -- AUTHOR: Mike Klein -- VERSION: 1.0
FILE: progedit.exe -- REQUIREMENTS: Windows 3.x
PURPOSE: Example of adding a menu item to a "foreign" application. In this
case, the program is Windows' NotePad, and the extension added is a definable
tab stop setting to Notepad's menu bar.
**************************************************************************/

#define _WINDOWS
#define NOCOMM

#include <windows.h>
#include <stdio.h>
#include <string.h>

#include "progedit.h"

/* Handles & vars needed for ProgEdit */
HANDLE  hInstProgEdit;
FARPROC lpfnMyMainWndProc;
/* Handles & vars needed for Notepad */
HMENU   hMenuNotepad;
HWND    hWndNotepadMain;
HWND    hWndNotepadEdit;
FARPROC lpfnNotepadMainWndProc;

int TabAmt;

BYTE Text[100];

/**************************************************************************
    FUNCTION: WinMain
    PURPOSE : Calls initialization function, processes message loop
**************************************************************************/
int PASCAL WinMain(HANDLE hInstance, HANDLE hPrevInstance, LPSTR lpCmdLine,
    int nCmdShow)
{
    MSG msg;
    struct
    {
        WORD wAlwaysTwo;
        WORD wHowShown;
    }
    HowToShow;

    struct
    {
        WORD   wEnvSeg;
        LPSTR  lpCmdLine;
        LPVOID lpCmdShow;
        DWORD  dwReserved;
    }
    ParameterBlock;
    HowToShow.wAlwaysTwo    = 2;
    HowToShow.wHowShown     = SW_SHOWNORMAL;
    ParameterBlock.wEnvSeg    = 0;
    ParameterBlock.lpCmdLine  = "";
    ParameterBlock.lpCmdShow  = (LPVOID) &HowToShow;
    ParameterBlock.dwReserved = NULL;
    hInstProgEdit = hInstance;

    /* Run a copy of NotePad */
    if(LoadModule("notepad.exe", (LPVOID) &ParameterBlock) < 32)
    {
        MessageBox(NULL, "Running instance of NotePad", "ERROR",
            MB_OK | MB_ICONSTOP);
        return(FALSE);
    }

    /* Get handles to Notepad's two main windows */
    hWndNotepadMain = GetActiveWindow();
    hWndNotepadEdit = GetFocus();

/* Set up different function pointers. Get a ptr to my hWnd func, then
** plug it into the other application's struct so it calls my func. Of
** course, at end of my func, I call func that I stole in first place. */
    lpfnMyMainWndProc=MakeProcInstance((FARPROC) MyMainWndProc, hInstProgEdit);
    lpfnNotepadMainWndProc=(FARPROC) SetWindowLong(hWndNotepadMain,GWL_WNDPROC,
                                               (DWORD) lpfnMyMainWndProc);

    /* Get handle to Notepad's menu and add Tabs to main menu */
    hMenuNotepad = GetMenu(hWndNotepadMain);
    AppendMenu(hMenuNotepad, MF_STRING, IDC_TABAMT, "&Tabs");
    DrawMenuBar(hWndNotepadMain);

    /* Read in tab amt from win.ini */
       GetProfileString("ProgEdit", "Tabs", "4", Text, 2);
       TabAmt = (HIWORD(GetDialogBaseUnits()) * (Text[0] - '0')) / 4;
       SendMessage(hWndNotepadEdit, EM_SETTABSTOPS, 1, (LONG) (LPINT) &TabAmt);

       /* Acquire and dispatch messages until a WM_QUIT message is received. */
    while(GetMessage(&msg, NULL, NULL, NULL))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    FreeProcInstance(lpfnMyMainWndProc);
    return(FALSE);
}

/**************************************************************************
    FUNCTION: MyMainWndProc
    PURPOSE : Filter/replacement function for Notepad's MainWndProc()
**************************************************************************/
LONG FAR PASCAL MyMainWndProc(HWND hWnd,unsigned wMsg,WORD wParam,LONG lParam)
{
    FARPROC lpProc;
    switch(wMsg)
    {
        case WM_COMMAND :
            switch(wParam)
            {
                case IDC_TABAMT :
                    /* Set tab stops in edit window */
              lpProc = MakeProcInstance(TabAmount, hInstProgEdit);
              DialogBox(hInstProgEdit, "TabAmount", hWnd, lpProc);
              FreeProcInstance(lpProc);
                break;;
            default :
                break;
            }
            break;
        case WM_DESTROY :
            SendMessage(hWndNotepadMain, WM_QUIT, 0, 0L);
            PostQuitMessage(0);
            break;
        default :
            break;
    }
   return(CallWindowProc(lpfnNotepadMainWndProc, hWnd, wMsg, wParam, lParam));
}

/**************************************************************************
    FUNCTION: TabAmount
    PURPOSE : Processes messages for edit window that gets tab amount
**************************************************************************/
BOOL FAR PASCAL TabAmount(HWND hWnd, unsigned wMsg, WORD wParam, LONG lParam)
{
    switch(wMsg)
    {
        case WM_INITDIALOG :
            /* Display the current tab setting */
                     SendDlgItemMessage(hWnd, IDC_TABAMT, EM_LIMITTEXT, 1, 0L);
             SetDlgItemInt(hWnd, IDC_TABAMT, (TabAmt * 4) /
                HIWORD(GetDialogBaseUnits()), FALSE);
            return(TRUE);
        case WM_COMMAND    :
            switch(wParam)
            {
                case IDOK         :
                case IDCANCEL     :

        /* Get number of tabs and calculate it in dialog units */
        GetDlgItemText(hWnd, IDC_TABAMT, Text, sizeof(Text));
         TabAmt = (HIWORD(GetDialogBaseUnits()) * (Text[0] - '0')) / 4;
        /* Set the tab stops in the edit window */
        SendMessage(hWndNotepadEdit, EM_SETTABSTOPS, 1,
                                                      (LONG) (LPINT) &TabAmt);
        InvalidateRect(hWndNotepadEdit, NULL, TRUE);
        UpdateWindow(hWndNotepadEdit);
        /* Save the tab amt in WIN.INI profile */
        WriteProfileString("ProgEdit", "Tabs", Text);
        EndDialog(hWnd, TRUE);
                    return(TRUE);
                default          :
                    break;
            }
            break;
        default            :
            break;
    }
    return(FALSE);
}






<a name="02bb_0016">
<a name="02bb_0017">
[LISTING SIX]
<a name="02bb_0017">

# Standard Windows make file.  The utility MAKE.EXE compares the
# creation date of the file to the left of the colon with the file(s)
# to the right of the colon.  If the file(s) on the right are newer
# then the file on the left, Make will execute all of the command lines
# following this line that are indented by at least one tab or space.
# Any valid MS-DOS command line may be used.

# This line allows NMAKE to work as well
all: subclass.exe

# Update the resource if necessary
subclass.res: subclass.rc subclass.h subclass.ico
    rc -r subclass.rc

# Update the object file if necessary
subclass.obj: subclass.c subclass.h
    cl -W4 -c -AS -Gsw -Oad -Zip subclass.c

# Update the executable file if necessary, and if so, add the resource back in.
subclass.exe: subclass.obj subclass.def
    link /NOD /CO subclass,,, libw slibcew, subclass.def
    rc subclass.res

# If the .res file is new and the .exe file is not, update the resource.
# Note that the .rc file can be updated without having to either
# compile or link the file.
subclass.exe: subclass.res
    rc subclass.res







<a name="02bb_0018">
<a name="02bb_0019">
[LISTING SEVEN]
<a name="02bb_0019">


/* Standard defines */
#define FIRST       (0L)
#define LAST        (0x7fff7fffL)
#define ALL         (0x00007fffL)

#define IDC_LISTBOX     100
#define IDC_INPUTBOX    100

/* Function prototypes */
int  PASCAL WinMain(HANDLE, HANDLE, LPSTR, int);

LONG FAR PASCAL MainWndProc(HWND, unsigned, WORD, LONG);
LONG FAR PASCAL HandleListBoxes(HWND, unsigned, WORD, LONG);
LONG FAR PASCAL HandleEditCtrls(HWND, unsigned, WORD, LONG);
VOID PASCAL CloseEditWindow(VOID);
VOID PASCAL OpenEditWindow(DWORD);







<a name="02bb_001a">
<a name="02bb_001b">
[LISTING EIGHT]
<a name="02bb_001b">

; module-definition file for Megaphone -- used by LINK.EXE

NAME         Test   ; application's module name
DESCRIPTION  'Test'
EXETYPE      WINDOWS       ; required for all Windows applications

STUB         'WINSTUB.EXE' ; Generates error message if application
               ; is run without Windows

;CODE can be moved in memory and discarded/reloaded
CODE  PRELOAD MOVEABLE DISCARDABLE

;DATA must be MULTIPLE if program can be invoked more than once
DATA  PRELOAD MOVEABLE MULTIPLE

HEAPSIZE     1024
STACKSIZE    5120      ; recommended minimum for Windows applications

; All functions that will be called by any Windows routine
; MUST be exported.

EXPORTS
    MainWndProc     @1
    HandleListBoxes @2
    HandleEditCtrls @3






<a name="02bb_001c">
<a name="02bb_001d">
[LISTING NINE]
<a name="02bb_001d">

/* Include files needed for .RC file */
#include "windows.h"
#include "subclass.h"

/* The program's icon (not that it needs one) */
SubClass   ICON SUBCLASS.ICO

/* Main dialog w/listbox used by SubClass */
SubClass DIALOG 36, 34, 100, 100
CAPTION "SubClass"
CLASS   "SubClass"
STYLE   WS_POPUPWINDOW | WS_CAPTION | WS_MINIMIZEBOX | DS_LOCALEDIT
BEGIN
    CONTROL "", IDC_LISTBOX, "BetterListBox",
        LBS_HASSTRINGS | LBS_NOTIFY | LBS_NOINTEGRALHEIGHT |
        WS_BORDER | WS_VSCROLL | WS_CHILD,
        10, 10, 80, 80
END





<a name="02bb_001e">
<a name="02bb_001f">
[LISTING TEN]
<a name="02bb_001f">

/**************************************************************************
    PROGRAM: SubClass -- AUTHOR: Mike Klein -- VERSION: 1.0
    FILE : subclass.exe -- REQUIREMENTS: Windows 3.x
    PURPOSE: An example of a subclassed listbox, providing enhanced input
    and data-entry facilities.
**************************************************************************/

/* Some std defines needed */
#define _WINDOWS
#define NOCOMM

/* INCLUDE files */
#include <windows.h>
#include "subclass.h"

/* Global variables */
HANDLE  hInstSubClass;
HWND    hDlgSubClass;
HWND    hWndListBox;
HWND    hWndEdit;

int     CurrentIndex;
int     NumListBoxItems;
RECT    CurrentItemRect;

BOOL    InsideEditMode = FALSE;
DWORD   dwEditPos;

BYTE    InputString[50];

/* Far pointers to Windows' class functions for listboxes and edit ctrls */
FARPROC lpfnListBox;
FARPROC lpfnEditCtrl;

/**************************************************************************
    FUNCTION: WinMain
    PURPOSE : Calls initialization function, processes message loop
**************************************************************************/
int PASCAL WinMain(HANDLE hInstance, HANDLE hPrevInstance, LPSTR lpCmdLine,
    int nCmdShow)
{
    WNDCLASS wc;
    MSG msg;
    if(!hPrevInstance)
    {
        hInstSubClass = hInstance;
           /* Fill in window class structure with parameters that describe the
       ** main window */
        wc.style         = CS_DBLCLKS;
        wc.lpfnWndProc   = MainWndProc;
        wc.cbClsExtra    = 0;
        wc.cbWndExtra    = DLGWINDOWEXTRA;
        wc.hInstance     = hInstSubClass;
        wc.hIcon         = LoadIcon(hInstSubClass, "SubClass");
        wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
        wc.hbrBackground = GetStockObject(WHITE_BRUSH);
        wc.lpszMenuName  = NULL;
        wc.lpszClassName = "SubClass";
        if(!RegisterClass(&wc))
            return(FALSE);
           /* Fill in window class structure with parameters that describe our
       ** custom list box -- BetterListBox */
        wc.style         = CS_DBLCLKS;
        wc.lpfnWndProc   = HandleListBoxes;
        wc.cbClsExtra    = 0;
        wc.cbWndExtra    = 0;
        wc.hInstance     = hInstSubClass;
        wc.hIcon         = NULL;
        wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
        wc.hbrBackground = GetStockObject(WHITE_BRUSH);
        wc.lpszMenuName  = NULL;
        wc.lpszClassName = "BetterListBox";
        if(!RegisterClass(&wc))
            return(FALSE);
        /* Fill in window class structure with parameters that describe our
        ** custom edit control -- BetterEditCtrl */
        wc.style         = CS_DBLCLKS;
        wc.lpfnWndProc   = HandleEditCtrls;
        wc.cbClsExtra    = 0;
        wc.cbWndExtra    = 0;
        wc.hInstance     = hInstSubClass;
        wc.hIcon         = NULL;
        wc.hCursor       = LoadCursor(NULL, IDC_IBEAM);
        wc.hbrBackground = GetStockObject(WHITE_BRUSH);
        wc.lpszMenuName  = NULL;
        wc.lpszClassName = "BetterEditCtrl";
        if(!RegisterClass(&wc))
            return(FALSE);
              /* Get information on listbox class, so we can find out what its
          ** class window function is. */
        if(GetClassInfo(NULL, "listbox", &wc) == FALSE)
            return(FALSE);
        else
            lpfnListBox = (FARPROC) wc.lpfnWndProc;
      /* Get information on edit control class, so we can find out what its
      ** class window function is. */
        if(GetClassInfo(NULL, "edit", &wc) == FALSE)
            return(FALSE);
        else
            lpfnEditCtrl = (FARPROC) wc.lpfnWndProc;

        /* Create the main window */
        if((hDlgSubClass = CreateDialog(hInstSubClass, "SubClass",
            NULL, 0L)) == NULL)
        {
            return(FALSE);
        }
        /* Get an oft used handle */
        hWndListBox = GetDlgItem(hDlgSubClass, IDC_LISTBOX);
        /* Put in some test strings. */
        SendMessage(hWndListBox, LB_ADDSTRING, 0, (LONG) (LPSTR) "computer");
    SendMessage(hWndListBox, LB_ADDSTRING, 0, (LONG) (LPSTR) "telephone");
    SendMessage(hWndListBox, LB_ADDSTRING, 0, (LONG) (LPSTR) "lcd");
    SendMessage(hWndListBox, LB_ADDSTRING, 0, (LONG) (LPSTR) "ochessica");
    SendMessage(hWndListBox, LB_ADDSTRING, 0, (LONG) (LPSTR) "heeyah");
    SendMessage(hWndListBox, LB_ADDSTRING, 0, (LONG) (LPSTR) "video");
    SendMessage(hWndListBox, LB_ADDSTRING, 0, (LONG) (LPSTR) "smoke");
    SendMessage(hWndListBox, LB_ADDSTRING, 0, (LONG) (LPSTR) "sky");
    SendMessage(hWndListBox, LB_ADDSTRING, 0, (LONG) (LPSTR) "lovely");
    SendMessage(hWndListBox, LB_ADDSTRING, 0, (LONG) (LPSTR) "windows");
    SendMessage(hWndListBox, LB_ADDSTRING, 0, (LONG) (LPSTR) "nunez");
    SendMessage(hWndListBox, LB_ADDSTRING, 0, (LONG) (LPSTR) "beer");
    SendMessage(hWndListBox, LB_ADDSTRING, 0, (LONG) (LPSTR) "pug");
    SendMessage(hWndListBox, LB_ADDSTRING, 0, (LONG) (LPSTR) "query");
    SendMessage(hWndListBox, LB_ADDSTRING, 0, (LONG) (LPSTR) "remote");
    SendMessage(hWndListBox, LB_ADDSTRING, 0, (LONG) (LPSTR) "party");
    SendMessage(hWndListBox, LB_ADDSTRING, 0, (LONG) (LPSTR) "mixer");
    SendMessage(hWndListBox, LB_ADDSTRING, 0, (LONG) (LPSTR) "skate");
    SendMessage(hWndListBox, LB_ADDSTRING, 0, (LONG) (LPSTR) "varied");
    SendMessage(hWndListBox, LB_ADDSTRING, 0, (LONG) (LPSTR) "interests");
        /* Give the listbox an initial selection */
        SendMessage(hWndListBox, LB_SETCURSEL, 0, 0L);
        ShowWindow(hDlgSubClass, nCmdShow);
        UpdateWindow(hDlgSubClass);
    }
    else
    {
    /* If there was another instance of SubClass running, then switch
    ** to it by finding any window of class = "SubClass". Then, if it's
    ** an icon, open the window, otherwise just make it active. */
        hDlgSubClass = FindWindow("SubClass", NULL);
        if(IsIconic(hDlgSubClass))
            ShowWindow(hDlgSubClass, SW_SHOWNORMAL);
        SetActiveWindow(hDlgSubClass);
        return(FALSE);
    }
       /* Acquire and dispatch messages until a WM_QUIT message is received. */
    while(GetMessage(&msg, NULL, NULL, NULL))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
}

/**************************************************************************
    FUNCTION: MainWndProc
    PURPOSE : Processes messages for SubClass dialog box
**************************************************************************/
LONG FAR PASCAL MainWndProc(HWND hWnd, unsigned wMsg, WORD wParam,
    LONG lParam)
{
    switch(wMsg)
    {
        case WM_CLOSE :
            DestroyWindow(hDlgSubClass);
            return(0L);
        case WM_SETFOCUS :
            SetFocus(hWndListBox);
            return(0L);
        case WM_DESTROY :
            PostQuitMessage(0);
            return(0L);
        default :
            break;
    }
    return(DefDlgProc(hWnd, wMsg, wParam, lParam));
}

/**************************************************************************
    FUNCTION: OpenEditWindow
    PURPOSE : Opens edit window inside listbox
**************************************************************************/
VOID PASCAL OpenEditWindow(DWORD CharSel)
{
    /* Flag telling us were in edit mode */
    InsideEditMode = TRUE;
    /* Find out current index into listbox */
    CurrentIndex = (int) SendMessage(hWndListBox, LB_GETCURSEL, 0, 0L);
    if(CurrentIndex == LB_ERR)
        CurrentIndex = 0;
    /* Find out what the text is in selected listbox cell */
    SendMessage
    (
        hWndListBox,
        LB_GETTEXT,
        CurrentIndex,
        (LONG) (LPSTR) InputString
    );
    /* Get client dimensions of listbox cell with respect to the entire
    ** listbox and create an edit window right inside of it. */
    SendMessage
    (
        hWndListBox,
        LB_GETITEMRECT,
        CurrentIndex,
        (DWORD) (LPRECT) &CurrentItemRect
    );
    hWndEdit = CreateWindow
    (
        "BetterEditCtrl",
        "",
        ES_AUTOHSCROLL | ES_LEFT | WS_VISIBLE | WS_CHILD,
        CurrentItemRect.left + 2,
        CurrentItemRect.top,
        CurrentItemRect.right - CurrentItemRect.left - 2,
        CurrentItemRect.bottom - CurrentItemRect.top,
        hWndListBox,
        IDC_INPUTBOX,
        hInstSubClass,
        0L
    );
    /* Pre-fill the edit control with what was in the listbox cell */
    SetWindowText(hWndEdit, InputString);
    SetFocus(hWndEdit);
    SendMessage(hWndEdit, EM_SETSEL, 0, CharSel);
}

/***************************************************************************
    FUNCTION: CloseEditWindow
    PURPOSE : Closes edit window inside listbox
***************************************************************************/
VOID PASCAL CloseEditWindow(VOID)
{
    /* Flag telling us were aren't in edit mode anymore */
    InsideEditMode = FALSE;
    /* Get text of what was entered into edit control */
    GetWindowText(hWndEdit, InputString, sizeof(InputString));
    if(!GetWindowTextLength(hWndEdit))
    {
        DestroyWindow(hWndEdit);
        SendMessage(hWndListBox, WM_KEYDOWN, VK_DELETE, 0L);
        return;
    }
    /* Turn redrawing for the listbox off. */
    SendMessage(hWndListBox, WM_SETREDRAW, 0, 0L);
    /* Find out the RECT of the currently selected listbox item */
    SendMessage(hWndListBox, LB_GETITEMRECT, CurrentIndex, (DWORD) (LPRECT)
        &CurrentItemRect);
    /* Delete the old string and add the new one */
    SendMessage(hWndListBox, LB_INSERTSTRING, CurrentIndex,
        (LONG) (LPSTR) InputString);
    SendMessage(hWndListBox, LB_DELETESTRING, CurrentIndex + 1, 0L);
    /* Destroy the old edit window. */
    DestroyWindow(hWndEdit);
      /* Validate the whole listbox and then invalidate only the list box rect
    ** that we put the edit window into. */
    ValidateRect(hWndListBox, NULL);
    InvalidateRect(hWndListBox, &CurrentItemRect, TRUE);

    /* Turn re-drawing for the listbox back on and send a WM_PAINT for the
    ** entry changes to take effect. */
    SendMessage(hWndListBox, WM_SETREDRAW, 1, 0L);
    UpdateWindow(hWndListBox);
    SetFocus(hWndListBox);
}

/***************************************************************************
    FUNCTION: HandleListBoxes
    PURPOSE : Process keystrokes and mouse for list boxes
***************************************************************************/
LONG FAR PASCAL HandleListBoxes(HWND hWnd, unsigned wMsg, WORD wParam,
    LONG lParam)
{
    switch(wMsg)
    {
        case WM_LBUTTONDBLCLK :

            /* Go into edit mode and put caret at end of edit ctrl. */
            if(SendMessage(hWnd, LB_GETCOUNT, 0, 0L))
            {
                OpenEditWindow(LAST);
            }
            return(0L);
        case WM_LBUTTONDOWN :
            if(InsideEditMode)
            {
            /* Find out cursor pos from the edit ctrl, so we can
            ** use same positioning for cell were moving into */
            dwEditPos = SendMessage(hWndEdit, EM_GETSEL, 0, 0L);
                CloseEditWindow();
            /* Tell listbox to move cur ptr up or down based on
            ** the mouse position, and open a new edit window */
                SendMessage(hWndListBox, wMsg, wParam, lParam);
                OpenEditWindow
                (
                MAKELONG(LOWORD(dwEditPos), LOWORD(dwEditPos))
                );
                return(0L);
            }
            break;
        case WM_MBUTTONDBLCLK :
        case WM_RBUTTONDBLCLK :
        /* Make middle & right mouse buttons like left mouse button. */
            SendMessage(hWnd, WM_LBUTTONDBLCLK, wParam, lParam);
            break;
        case WM_MBUTTONDOWN :
        case WM_RBUTTONDOWN :
        /* Make middle & right mouse buttons like left mouse button. */
            SendMessage(hWnd, WM_LBUTTONDOWN, wParam, lParam);
            SendMessage(hWnd, WM_LBUTTONUP, wParam, lParam);
            break;
        case WM_KEYDOWN :
            switch(wParam)
            {
                case VK_RETURN :
            /* Enter was pressed, so go into edit mode and put the
            ** caret at the end of the edit ctrl */
                if(SendMessage(hWnd, LB_GETCOUNT, 0, 0L))
                    {
                        OpenEditWindow(LAST);
                    }
                    return(0L);
                case VK_INSERT :
            /* The INS key (add a new string). First, get currently
            ** selected entry. If none exists, assume that focus is on
            ** the first cell */
            CurrentIndex =
                     (int) SendMessage(hWnd, LB_GETCURSEL, 0, 0L);
                    if(CurrentIndex == LB_ERR)
                    {
                        CurrentIndex = 0;
                    }
               /* Find out what the text is in selected listbox cell */
                        if(SendMessage(hWnd, LB_GETCOUNT, 0, 0L))
                    {
                        SendMessage
                        (
                            hWnd,
                            LB_GETTEXT,
                            CurrentIndex,
                            (LONG) (LPSTR) InputString
                        );
                    }
                    else
                    {
                /* If nothing's in the listbox, then copy a null to
                ** the edit control. */
                InputString[0] = '\0';
                    }

                    /* Insert new entry */
                    SendMessage
                    (
                        hWnd,
                        LB_INSERTSTRING,
                        CurrentIndex,
                        (LONG) (LPSTR) InputString
                    );
               /* Let our "edit current cell" function take over */
                    OpenEditWindow(ALL);
                    return(0L);
                case VK_DELETE :
              /* The DEL key. If no items are in the listbox, then
              ** return. Else, get the currently selected item. */
                if(!(NumListBoxItems = (int)
                      SendMessage(hWnd, LB_GETCOUNT, 0, 0L)))
                    {
                        break;
                    }
                    if((CurrentIndex = (int)
                SendMessage(hWnd, LB_GETCURSEL, 0, 0L)) == LB_ERR)
                    {
                        CurrentIndex = 0;
                    }
            /* Delete the string. Tried to get rid of annoying
            ** focus rect; couldn't. Too many inconsitencies in
            ** the way Windows handles list and combo boxes. */
            SendMessage(hWnd, LB_DELETESTRING, CurrentIndex, 0L);
                    if(CurrentIndex == NumListBoxItems - 1)
                    {
                        --CurrentIndex;
                    }
                    /* Reset our listbox selection. */
                 SendMessage(hWnd, LB_SETCURSEL, CurrentIndex, 0L);
                    return(0L);
                default :
                    break;
            }
            break;
        default :
            break;
    }
   /* Return any unprocessed messages to window's original class procedure. */
    return(CallWindowProc(lpfnListBox, hWnd, wMsg, wParam, lParam));
}

/***************************************************************************
    FUNCTION: HandleEditCtrls
    PURPOSE : Process keystrokes and mouse for edit controls
***************************************************************************/
LONG FAR PASCAL HandleEditCtrls(HWND hWnd, unsigned wMsg, WORD wParam,
    LONG lParam)
{
    switch(wMsg)
    {
        case WM_LBUTTONDBLCLK :
             /* Turn of edit mode, closing the edit window */
            CloseEditWindow();
            return(0L);
        case WM_KEYDOWN :
            switch(wParam)
            {
                case VK_RETURN :
                   /* Turn of edit mode, closing the edit window */
                    CloseEditWindow();
                    return(0L);
                case VK_DELETE :
              /* Delete a character if one exists in the edit ctrl.
              ** Otherwise, if the cell is blank, delete the entire
              ** cell. */
                if(!GetWindowTextLength(hWnd))
                {
                CloseEditWindow();
                SendMessage(hWndListBox, wMsg, wParam, lParam);
                return(0L);
                }
                break;
                case VK_DOWN    :
                case VK_UP      :
                case VK_PRIOR   :
                case VK_NEXT    :
              /* Find out cursor pos from edit ctrl, so we can
              ** use same positioning for cell we're moving into */
              dwEditPos = SendMessage(hWndEdit, EM_GETSEL, 0, 0L);
                    CloseEditWindow();
                /* Tell listbox to move cur ptr up or down, and
                ** open an edit window at the new pos. */
                SendMessage(hWndListBox, wMsg, wParam, lParam);
                OpenEditWindow
                    (
                 MAKELONG(LOWORD(dwEditPos), LOWORD(dwEditPos))
                    );
                    return(0L);
                default :
                    break;
            }
            break;
        default :
            break;
    }
   /* Return any unprocessed messages to window's original class procedure. */
    return(CallWindowProc(lpfnEditCtrl, hWnd, wMsg, wParam, lParam));
}


Copyright © 1991, Dr. Dobb's Journal


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.