![]() |
Inside the VCL: Window
Creation Fundamentals |
When encountering the
word "window" in a Windows API reference, the first thought that may come to
mind is a VCL Form. Indeed, a Form
is a type of window—usually a top-level
window—but it's
rarely the only window that you'll use in an application. As it turns out, all
TWinControl descendants are windows of
some sort. For example, notice that
the Handle property is first
introduced in the TWinControl class. This was not a random design decision,
but rather due to the fact that all windowed controls, including Forms, possess
a window handle. This handle is
used throughout the API as an abstract layer between a window's actual memory
block and the functions that can be used to manipulate it. For example, we saw in the first article
("From Messages to Events") the use the of SendMessage API function. Recall that the first parameter to this
function specifies the handle of the window that is to receive the message. In fact, nearly all API functions that
serve to manipulate an aspect of a windowed control will require a window
handle. In many
situations, however, there's no need to use the Handle property; most of us resort
to it only when a VCL solution does not exist or is inadequate.
The TWinControl class serves as the "glue"
between the Windows API and all windowed VCL controls. For this reason, in order to understand
the TWinControl class, we'll need to first
examine the window creation process from an API point of view. As we did in the first article ("From
Messages to Events"), we'll make several fundamental connections between the
Windows API and the VCL. Here we'll focus specifically on the TWinControl class and more
specifically on the window creation
process.
I. Window
Creation
Most applications that are
designed for Windows require some sort of user interaction. Clearly, the predominant means of
communication between the user and the application is accomplished by a visual
object known as a window. In some cases, the window serves as
means of user input, while in others, windows are used to inform the user of a
particular affair. While this is
old news to anyone who has developed a graphical user interface, many seasoned
C++ programmers have strictly developed console or “text-based”
applications. Moreover, as the VCL
abstracts much of the window creation process, many proficient C++Builder
developers run into to difficulties when the need to customize or troubleshoot
arises. For these reasons, let's
now closely examine the process of creating a window directly via the Windows
API (i.e., without the use of the VCL).
As you'll see shortly, this process, while cumbersome, is relatively
straightforward and can be followed very much like a
recipe.
IA. The CreateWindow
Function
Creating any windowed
control is accomplished via the CreateWindow API function. In a raw API implementation, there is no
visual development—all controls are windowed
control, and all controls must be created through code. While this is true in C++Builder as
well, the level of abstraction that the VCL introduces oftentimes makes this
point a subtlety. For simple
controls such as buttons or edit controls, a single call to CreateWindow is all that's
necessary. This is simply due to
the fact that there are pre-registered classes for such controls. Let's examine the
signature of the CreateWindow function to clarify this
argument:
HWND
CreateWindow(
LPCTSTR
lpClassName,
LPCTSTR
lpWindowName,
DWORD
dwStyle,
int
x,
int
y,
int
nWidth,
int
nHeight,
HWND
hWndParent,
HMENU
hMenu,
HANDLE
hInstance,
LPVOID
lpParam
);
The function requires seven parameters, and returns a handle to the newly created window. The first parameter, lpClassName, is a pointer to a string representing the name of the class of window that's to be created. The stipulation here is that this class must be registered with Windows before the call to the CreateWindow function is made. Indeed, it would serve little purpose to call the CreateWindow function specifying a class of window that Windows itself doesn't know how to create. In fact, the Windows API makes available several types of predefined, pre-registered control classes. Among the most common examples are those that fall under the Standard and Common Control classes.
IB. Control Class
Registration
Again, the Windows API
presents several types of predefined classes of controls that can be used
without the need for explicit registration. These controls are pre-registered with
Windows and included as part of the API's user-interface service. Standard Controls such as buttons, edit
controls, static controls, combo boxes, list boxes, and the like, are already
registered. For example, to create
a button, you'd simply pass “BUTTON” as the lpClassName
argument of the
CreateWindow function. Similarly, Common Controls such a list
views, tree views, header controls, tab controls, toolbars, etc., are registered
by calling the InitCommonControlsEx API function. What if you want to create a custom control
class? Well, you'll have to first register your class with the system by
using the RegisterClass function:.
ATOM
RegisterClass(
const WNDCLASS
*lpWndClass
);
The
RegisterClass
function takes
only one parameter, a pointer to a WNDCLASS structure that defines the
attributes of the class of window. Here's
what the WNDCLASS
structure looks like:
typedef
struct _WNDCLASS
{
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HANDLE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR lpszMenuName;
LPCTSTR lpszClassName;
}
WNDCLASS;
Several of the members of
the WNDCLASS structure should look
familiar. For example, the
lpfnWndProc is a pointer to the window
procedure of the window. The
hIcon and hCursor members are the handles to
the default icon and cursor of the window, respectively. The hbrBackground member is the handle to the
brush that can be used to paint the background of the window. Most importantly, the last member,
lpszClassName, defines the name of the
window's class (e.g., “TForm1”).
Once a WNDCLASS structure is initialized, its address is passed into the RegisterClass function. Let's look at an example; specifically, let's examine the process of creating a new class of top-level window, “TMyForm”, as demonstrated in Listing 2.0.
|
//---------------------------------------------------------------------- #include
<windows.h> //----------------------------------------------------------------------
//
our new window class name const
char
NewClassName[] = "TMyForm"; //----------------------------------------------------------------------
//
main window procedure LRESULT
CALLBACK WndProc(HWND HWnd, unsigned int Msg, WPARAM
WParam, LPARAM
LParam) { // this window will actually do no special
processing // except terminate
the application when destroyed switch
(Msg)
{
case
WM_DESTROY:
{
// will place WM_QUIT in our message
queue
// (strictly speaking, the QS_QUIT flag)
PostQuitMessage(0);
return
0;
}
} return DefWindowProc(HWnd, Msg,
WParam, LParam); } //----------------------------------------------------------------------
int
APIENTRY WinMain(HINSTANCE HInstance, HINSTANCE
HPrevInstance, LPTSTR lpCmdLine, int
nCmdShow) { WNDCLASS
wc; memset(&wc, 0, sizeof(WNDCLASS));
// initialize the WNDCLASS
structure wc.style
= CS_VREDRAW | CS_HREDRAW | CS_DBLCLKS; wc.lpfnWndProc =
WndProc; wc.hInstance =
HInstance; wc.hCursor =
LoadCursor(NULL, IDC_ARROW); wc.hbrBackground =
GetStockObject(LTGRAY_BRUSH); wc.lpszClassName =
NewClassName;
// register our new class of
window if
(RegisterClass(&wc))
{
// once registered, we are free to create
the window
HWND HWnd = CreateWindow(NewClassName, "MyForm1",
WS_OVERLAPPEDWINDOW |
WS_CAPTION | WS_VISIBLE,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL,
NULL, HInstance, NULL);
if
(HWnd)
{
// show the new window
ShowWindow(HWnd, nCmdShow);
UpdateWindow(HWnd);
// main message
pump
MSG msg;
while
(GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
} return 0; } //----------------------------------------------------------------------
|
Listing
2.0 |
Note that the WinMain function presented in
Listing 2.0 is identical to the WinMain function that is created by
the VCL (i.e., what you see by viewing your Project's source code; cf, Listing
1.4 of the article "From Messages to Events"). In addition, notice that we provide a
default window procedure since our new class, TMyForm, is not based on an
existing window class. That is, the
window procedure that we specify will define the attributes and behavior of our
new class of window. In fact, our
simple window procedure (WndProc) definition of Listing 2.0
does little more than call the DefWindowProc API function. This latter call serves to ensure that
our new type of window will behave like any standard top-level
window.
Focus on the WinMain definition of Listing 2.0;
notice that the implementation boils down to three basic steps. First, we declare and initialize a
WNDCLASS structure. Next, we call the RegisterWindow API function, passing the
address of this structure. Finally,
after the class has been successfully registered, we call the CreateWindow function, passing the newly
registered class name (NewClassName in this case) as the
lpClassName parameter. These three steps must be followed for
every window of a unique class that is to be created.
Clearly, without some degree
of encapsulation, this process not only becomes extremely cumbersome, but it's
also prone to error and difficult to maintain. Fortunately, all of this is work is
encapsulated by the TWinControl class, whose specifics and
references to this section will be presented in the next. For now, let's return to the
CreateWindow function and examine its
other parameters.
HWND
CreateWindow(
LPCTSTR
lpClassName,
LPCTSTR
lpWindowName,
DWORD
dwStyle,
int
x,
int
y,
int
nWidth,
int
nHeight,
HWND
hWndParent,
HMENU
hMenu,
HANDLE
hInstance,
LPVOID
lpParam
);
The
second parameter, lpWindowName, is the actual caption of
the window. In C++Builder, the
first Form in an application, by default, uses “Form1” as this parameter. The dwStyle parameter is used to
indicate the various attributes of a window. For example, the WS_OVERLAPPED style is used to create a
standard top-level window. This
style is used by the TForm class by default as
well. Similarly, the WS_POPUP style can be used to create
a caption-less and border-less window.
You probably
already know that the TForm::BorderStyle property can be used to
create windows of varying border styles.
You now
know exactly what the manipulation of this property does; namely, it changes the
various style flags that are passed as the dwStyle parameter. Likewise, the x, y, nWidth, and nHeight parameters are changed when
you change the Top, Left, Width, and Height properties of the form at
design-time, respectively.
If the
window being created is a child window, The hWndParent parameter is used to
indicate the window's parent. For
top-level windows, this parameter is set to the handle of the desktop window,
implicitly by specifying NULL, or explicitly by a call to
the GetDesktopWindow API function. The TForm class uses the handle of
the zero dimension TApplication window as this
parameter. The hMenu parameter can be used to
indicate the handle of the window’s main menu, if present, and the hInstance parameter is specified as
the handle to the application’s instance queried via the global HInstance variable. The last parameter, lpParam, is used to store any
additional “user-defined” information.
We
have just examined how to create a new window class that is not based upon an
existing class. Let's now investigate
how to perform a technique called superclassing that's allow you to create
a new class that's based on an existing, pre-registered, window class. For instance, the VCL TButton class actually creates a
new window class of type TButton that's based on the BUTTON
Standard Control. In this case, the
TButton and TWinControl classes perform the
superclassing for us. Using a tool
such as Microsoft's Spy++, you can verify that the class name of a TButton control is indeed
"TButton" and not "BUTTON".
IC.
Superclassing
As
I mentioned, superclassing is a technique used to create a new window class from
an existing, pre-registered, window class (base class). Similar to the process of creating a new
window class from scratch, superclassing involves declaring and initializing a
WNDCLASS structure for the new
(super) class. The difference here,
is that the GetClassInfo API function is first
called to initialize the WNDCLASS structure with information
from the parent class.
bool
GetClassInfo(
HINSTANCE hInstance,
LPCTSTR lpClassName,
LPWNDCLASS lpWndClass
);
The address of the default window procedure of the parent class, stored in the lpfnWndProc data member of the WNDCLASS variable whose address is passed into GetClassInfo, is then stored for later use in the window procedure of the superclass. This step is very similar to what was presented in the example of instance subclassing in the first article ("From Messages to Events"), where the replacement window procedure directly called the original window procedure (cf, Listing 1.18). While for subclassing, the replacement window procedure was termed the "subclass procedure”, here, the new window procedure is called the "superclass procedure". After the address of the base class's window procedure is stored, this superclass procedure is specified as the lpfnWndProc member of the WNDCLASS structure for the superclass. Finally, the handle to the application's instance (hInstance) is specified as the hInstance parameter, and the new class name is specified as the lpClassName parameter.
Let's work through an example of superclassing the BUTTON Standard Control class. Listing 2.1 illustrates this process, where the name of the superclass is specified as "TMyButton".
|
//---------------------------------------------------------------------- #include
<windows.h> //-----------------------------------------------------------------------
//
our new window class name const
char
NewFormClassName[] = "TMyForm";
//
our new button class name const
char
NewButtonClassName[] = "TMyButton";
//
will hold the address of the base (BUTTON) //
class's default window procedure FARPROC
ButtonDefWndProc = NULL; //----------------------------------------------------------------------
//
main window procedure LRESULT
CALLBACK FormWndProc(HWND HWnd, unsigned int Msg, WPARAM
WParam, LPARAM
LParam) { // this window will actually do no special
processing // except terminate
the application when destroyed switch
(Msg)
{
case
WM_DESTROY:
{
// will place WM_QUIT in our message
queue
// (strictly speaking, the QS_QUIT flag)
PostQuitMessage(0);
return
0;
}
} return DefWindowProc(HWnd, Msg,
WParam, LParam); } //----------------------------------------------------------------------
//
button window procedure LRESULT
CALLBACK ButtonWndProc(HWND HWnd, unsigned int Msg,
WPARAM WParam, LPARAM
LParam) {
// // this window will
actually do no special processing
//
// call the original
BUTTON window procedure return
CallWindowProc(ButtonDefWndProc, HWnd, Msg, WParam,
LParam); } //----------------------------------------------------------------------
int
APIENTRY WinMain(HINSTANCE HInstance, HINSTANCE
HPrevInstance, LPTSTR lpCmdLine, int
nCmdShow) { WNDCLASS form_wc,
button_wc; memset(&form_wc,
0, sizeof(WNDCLASS)); memset(&button_wc,
0, sizeof(WNDCLASS));
// initialize the Form's WNDCLASS
structure form_wc.style
= CS_VREDRAW | CS_HREDRAW | CS_DBLCLKS; form_wc.lpfnWndProc =
FormWndProc; form_wc.hInstance =
HInstance; form_wc.hCursor =
LoadCursor(NULL, IDC_ARROW); form_wc.hbrBackground
= GetStockObject(LTGRAY_BRUSH); form_wc.lpszClassName
= NewFormClassName;
// initialize a WNDCLASS structure for the
"BUTTON" // base class via the
GetClassInfo API function if (!GetClassInfo(NULL, "BUTTON", &button_wc)) return false;
// store the address of the original window
procedure ButtonDefWndProc = reinterpret_cast<FARPROC>(button_wc.lpfnWndProc); // specify the address
of the subclass procedure button_wc.lpfnWndProc
= ButtonWndProc;
// initialize the rest of our button's
WNDCLASS // structure for
superclassing button_wc.hInstance =
HInstance;
button_wc.lpszClassName = NewButtonClassName;
//
register our new class of window and
button if (RegisterClass(&form_wc)
&& RegisterClass(&button_wc))
{
// once registered, we are free to create
the window
HWND HWnd = CreateWindow(NewFormClassName, "MyForm1",
WS_OVERLAPPEDWINDOW
|
WS_CAPTION | WS_VISIBLE,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, HInstance, NULL);
if
(HWnd)
{
// create the new
button
CreateWindow(NewButtonClassName, "MyButton1",
WS_CHILD | WS_VISIBLE |
BS_PUSHBUTTON,
10, 10, 200, 75,
HWnd, NULL, HInstance, NULL);
// show the new
window
ShowWindow(HWnd, nCmdShow);
UpdateWindow(HWnd);
// main message
pump
MSG msg;
while
(GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
} return 0; } //----------------------------------------------------------------------
|
Listing
2.1 |
Similar to the implementation in Listing 2.0, our WinMain function definition of Listing 2.1 first declares, then initializes a WNDCLASS variable for the TMyForm class. Here, we also declare a WNDCLASS variable, button_wc, for our new button class. Instead of explicitly initializing its data members, we first use the GetClassInfo API function, passing NULL as the hInstance parameter, the class name of the parent class, "BUTTON", as the lpClassName parameter, and the address of our WNDCLASS variable as the lpWndClass parameter. We pass NULL as the hInstance parameter since the BUTTON class is a defined by Windows itself. After a successful call to GetClassInfo,