Change the Style of a Control / Owner Drawn Controls
Often it would be nice to modify the behavior or style of Visual Basic’s many
controls. For example, changing the font of individual ListBox items. Well, you
can by changing the Style of the control.
Download Source Code
This sample was written in VB6 but uses the VB5 version of the ProgressBar and
TabStrip controls to provide backward compatibility. If you
use the VB6 version of these controls or compile this program you must change
the class names in procedure fAppHook from ProgressBarWndClass and
TabStripWndClass to ProgressBar20WndClass and TabStrip20WndClass respectively.
A control is defined by its window Class and Style. For example, a
CommandButton, CheckBox and OptionButton are all created from the same window
class but use different style attributes. Because a style setting is not
available natively through VB doesn’t mean it is not accessible.
You can change the style of a control before it is created. This means
hooking into the stream of messages Windows sends to your
application and watching to see when the control is created. Although Visual
Basic hooks cannot monitor system events like when another application is
started or receives a keystroke (system hooks must reside in a standard dll and
not VB’s ActiveX dlls) VB hooks can watch thread level events and that's
exactly what's needed here.
Just what is a hook? A hook lets you intercept messages that Windows sends to a
thread. Whenever an action such as pressing a key or moving a mouse occurs,
Windows generates a message that it passes through a chain of hook procedures
before sending the message to the target window.
This process is similar to subclassing which consists of replacing the window's
original message handling procedure with your own. For more info on
subclassing, see my Subclass a Form program.
The difference is that with a hook you do not replace the original procedure.
Instead you insert a new procedure at the top of a chain. Each thread includes
several hook chains of various types and each hook type handles a particular
category of messages, such as keyboard or mouse.
Each hook procedure is responsible for calling the next hook procedure in the
chain. Failure to do so does not cancel the message but merely prevents the
subsequent procedures in the chain from seeing the message. The advantage of
hooking over subclassing is that a hook can see all messages sent to an entire
thread, regardless of the active window.
Sample Program Discussion
|
This program shows how to change the window style of several controls.
A hook is set in Sub Main by passing the application's instance handle and
thread ID, both properties of VB's App object, to
the SetWindowHookEx API. Using the
AddressOf operator we also pass the address of our hook procedure
which is called fAppHook. Now, Windows will call fAppHook before it
sends any messages to the controls in our application.
fAppHook takes three parameters. Its lParam parameter receives a pointer
to a CWPSTRUCT structure that contains details
about the message that was sent. Using this address and the CopyMemory
API a local copy of CWPSTRUCT is made so we can access it.
If the message sent, indicated by CWPSTRUCT's message element, is
WM_CREATE we know a control is about to be created. Using the
GetClassName API we can see if its class name matches that of the
control we want to change.
Visual Basic uses its own class names for most of its controls. Typically these
names begin with the word "Thunder". To find out what the class name is
for a control we can insert a Debug.Print statement
in the hook procedure or use a tool like Spy++. Once we know a control of the
proper class is being created we must be sure that it is the correct instance
of the control. For example, we have 2 comboboxes and only want to modify one
of them. In our case a flag is set denoting whether it is the first combobox
being created or not. More about this in a moment.
Once we have the correct control, the existing style attributes are retrieved
via the GetWindowLong API with the
GWL_STYLE parameter and the style is modified to our liking. To set
the new style we must do so when the control receives the WM_CREATE
message. This means subclassing the control so we can catch that message. The
SetWindowLong API is called and passed the handle of the new control
and the address of our new window procedure (named fSetStyle).
It is important to note that when setting the new style on some controls, a
variation of the OwnerDrawn flag is used.
fSetStyle watches for a create message. When one arrives, its lParam
argument gets the address of the CreateStruct structure
which accompanies the message. This structure contains information on how to
create the new control including its location and style information. As hinted
at earlier, the location can be used to determine if we have the correct
control instance to modify. Because CreateStruct cannot be modified, a local
copy is made. The local copy's style is set to the desired value and the
structure is copied back.
A call to SetWindowLong with the GWL_STYLE
flag sets the new control's style. A second call to SetWindowLong,
this time with the GWL_WNDPROC flag and the
original window procedure's address, removes the subclassing from the control.
Finally, CallWindowProc is issued to call the
original window procedure associated with the control.
Owner Drawn controls have no predefined appearance. It is up to the
application, not the system, to draw them. Messages, such as WM_DRAWITEM,
which are normally sent to a control are instead sent to the control’s parent.
When the parent receives the message it is up to the parent to draw the control
however it wants.
Such controls let you create custom controls that look however you want. Like
using bitmaps for the captions on the TabStrip, tab stops in a ComboBox or
different fonts inside a listbox as shown in the sample project.
In the form load event the parent of the TabStrip, ComboBox and ListBox
controls are subclassed in order to trap the WM_DRAWITEM
message. The subclassing is accomplished as described above using the
SetWindowLong function and passing it the address of our new window
procedure -- named fAppWndProc.
fAppWndProc watches for the WM_DRAWITEM message. When one is sent the
accompanying DRAWITEMSTRUCT structure is copied to
local storage. If its CtlType element specifies one of our owner drawn controls
the appropriate code is executed to draw the control. See the commented source
code for further details.
This sample is a regurgitated version of the project written by Matt Hart that
appeared in the July, 1999 edition of Visual Basic Programmers Journal. I have
included it because it illustrates a number of very useful topics.
Download the source code and run it. As with any project that employs
subclassing, you should always start with a full compile by pressing Ctrl-F5 to
run it.
|