Subclassing is an important--and misunderstood--method of intercepting and processing messages going to an object in Windows 3.
January 01, 1991
URL:http://www.drdobbs.com/windows/subclassing-applications/184408685
Copyright © 1991, Dr. Dobb's Journal
This article contains the following executables: PROGEDIT.ARC
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.
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.
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.
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.
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)?
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?
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.
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.
Copyright © 1991, Dr. Dobb's Journal
Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.
Mike Klein
Manipulating Objects
The Inside Skinny
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 */
};
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
BetterListBox: Making the Bad Better
Subclass and Live to Tell the Story!
Conclusion
_SUBCLASSING APPLICATIONS_
by Mike Klein
[LISTING ONE]
# 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
[LISTING TWO]
#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);
[LISTING THREE]
; 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
[LISTING FOUR]
#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
[LISTING FIVE]
/**************************************************************************
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);
}
[LISTING SIX]
# 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
[LISTING SEVEN]
/* 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);
[LISTING EIGHT]
; 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
[LISTING NINE]
/* 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
[LISTING TEN]
/**************************************************************************
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));
}