Introduction
Windows is essentially a message driven Operating System in the sense that,
the majority of actions that take place are responses to messages sent to the
main window procedure of an application. Whether you press a key, or move the
mouse or drag a window the application receives messages through it's message
queue and reacts accordingly. Now this results in a rather interesting corollary
that can be taken advantage of by us, developers; by sending the correct
messages to a window or it's child windows in the proper order, we can actually
simulate human actions on an application. And this has it's uses in various
scenarios. Obviously the first one that comes to mind is the ability to automate
a task, like for example opening a document in word, left justifying the entire
text and taking a print out.
But for me a more interesting usage of this technique is when it's applied to
take advantage of the Windows user interface to quickly do tasks, which might
otherwise require a lot of programming calls and access to undocumented
information. This includes changing various system properties, making changes
through the control panel applets or even changing display properties for the
desktop. In this article I will randomly select one such scenario ( a test case
scenario ) and see how to go about automating the task by using some simple
windows techniques like posting messages to a window, enumerating child windows
and elementary CBT hooking.
Test scenario
I am going to be using Windows XP Professional as my test platform and
therefore my example scenario is only meaningful in an XP context. Users of
other Operating Systems might have to make suitable changes to my example code
snippets to get the same end result as in this article.
By default, the keyboard navigation short cuts for menus are not shown in the
XP operating system ( something that both puzzled and annoyed a lot of users
when they first encountered this in Windows 2000 ). Having long abandoned
Windows 2000, I do not remember if there was a documented way of changing this
setting in 2000, other than by editing the registry or using some tweaking
application, but in XP this setting can be easily changed by using the Display
Properties control panel applet. All you need to do is select the Appearances
tab from the Display Properties dialog box, bring up the Effects sub-window and
uncheck the check box that says "Hide underlined letters for keyboard navigation
until I press the Alt key". Now I am wholly sure that this setting can probably
be changed by modifying a trivial registry entry; but for the sake of this test
case scenario and the article, let's assume that we do not know how to achieve
the same programmatically.
The human approach
Let's see how we'd do this had we done this manually sitting in front of the
machine. We'd probably have to follow these steps ( or something very similar )
:-
- Right click on the desktop and bring up the Display Properties control
panel applet
- Chose the Appearances tab
- Bring up the Effects sub-window by clicking on the Effects button
- Check/Uncheck the corresponding check box depending on what we are trying
to do
- Click OK to dismiss the Effects sub-window
- Click OK to dismiss the Display Properties and apply our changes globally
The solution in code
Now we need to decide what we need to do to achieve the same sequence of
events through code.
- Bringing up the Display Properties window and choosing the Appearances tab
can be done in just one step because we know that the Display Properties
control panel applet is called desk.cpl and that it takes command line
arguments that can be used to dictate which tab comes up by default. In fact
we need to call it like this :-
control.exe desk.cpl Display,@Appearance
Control.exe is used to bring up the control panel applet passed to
it as first argument, and the additional arguments are used to force it to
start with the Appearance tab selected.
- Now we need to enumerate the child windows ( controls ) on the Appearance
tab till we find the Effects button and this can be achieved using
EnumChildWindows
. Once we obtain the Effects button's handle we can send a
button click message to it and bring up the Effects sub-window.
- To locate the required check box on the Effects sub-window we would need
to first get the handle to the sub-window that just popped up. We achieve this
by setting up a global CBT hook ( this means we'd need to put all put code
into a DLL ), and watching for all newly activated windows. We know the title
text for the Effect sub-window and thus we obtain the handle to the window the
moment it gets activated. Now we do the same as previous, i.e. we use
EnumChildWindows
to retrieve the handle to the check box, and then send
a button click message to it.
- We post a
WM_COMMAND
message to the Effects sub-window with a
command ID of IDOK
which is the equivalent of closing the window
by clicking on the OK button.
- We do the same for the main Display Properties window.
Implementation details
Bringing up the Display Properties window
BOOL BringUpDisplayAppearance()
{
return reinterpret_cast<int>(ShellExecute(GetDesktopWindow(),
"open","control.exe","desk.cpl Display,@Appearance",
"",SW_SHOW )) > 32 ? TRUE : FALSE;
}
The code is quite simple and straightforward, we simply use
ShellExecute
to bring up the Display Properties applet window with the
default tab set to the Appearances tab. I have used SW_SHOW
here
because using SW_HIDE
will have no effect on the display properties
window ( I believe the control.exe program or perhaps desk.cpl
itself will later call ShowWindow(hWnd, SW_SHOW)
somewhere in the
code ). We do our window hiding in the hook procedure ( but even this is not
fully effective and there is a short flash on screen, but then our aim is not
really to hide what we are doing from our end user, but to try and make things as
lucid as possible, which we achieve by reducing the time the window
remains visible to a few milliseconds ).
Getting the handle to the Effects button
HWND GetEffectsButton(HWND hWndParent)
{
HWND hWnd = NULL;
EnumChildWindows(hWndParent, EnumAppearanceChildProc,
(LPARAM)&hWnd);
return hWnd;
}
BOOL CALLBACK EnumAppearanceChildProc(HWND hwnd, LPARAM lParam)
{
TCHAR buff[512];
GetWindowText(hwnd,buff,512);
if(_tcscmp(buff,_T("&Effects...")) == 0)
{
*reinterpret_cast<HWND*>(lParam) = hwnd;
return FALSE;
}
return TRUE;
}
Using Spy++ we extract the exact text associated with the Effects button
which happens to be "&Effects..." and we use this knowledge to compare the text
of each child control with this text repeatedly till we get the button control
we want.
Getting the check box handle
HWND GetMenuUnderlineCheck(HWND hWndParent)
{
HWND hWnd = NULL;
EnumChildWindows(hWndParent, EnumEffectsChildProc,
(LPARAM)&hWnd);
return hWnd;
}
BOOL CALLBACK EnumEffectsChildProc(HWND hwnd, LPARAM lParam)
{
TCHAR buff[512];
GetWindowText(hwnd,buff,512);
if(_tcsstr(buff,_T("&Hide underlined")))
{
*reinterpret_cast<HWND*>(lParam) = hwnd;
return FALSE;
}
return TRUE;
}
This is very similar to how we obtained the handle to the Effects button.
The CBT hook procedure
LRESULT CALLBACK CBTProc(int nCode,
WPARAM wParam, LPARAM lParam)
{
if(nCode == HCBT_ACTIVATE)
{
HWND hWnd = (HWND) wParam;
TCHAR buff[512];
GetWindowText(hWnd,buff,512);
if(_tcscmp(buff,_T("Effects")) == 0)
{
ShowWindow(hWnd,SW_HIDE);
g_hWndEffects = hWnd;
UnhookWindowsHookEx(g_hook);
}
if(_tcscmp(buff,_T("Display Properties")) == 0)
{
ShowWindow(hWnd,SW_HIDE);
}
}
return 0;
}
The HCBT_ACTIVATE
code indicates that a
window is about to be activated. We compare the title text of this window with
"Effects" and if they match, we know that this is the window we were searching
for. If you are wondering why we had to install a CBT hook, instead of using
FindWindow
using the title text; this is to make sure that even if
there was already an existing window with the same title text, it won't
interfere with our search because we are only checking newly activated windows.
We set the CBT hook quite late into the code and uninstall it the moment we get
the window we want. The duration the hook is active is from the time we bring up
the Display Properties window till the Effects sub-window is just about to be activated, and under most
circumstances this shouldn't be more than a few milliseconds.
We also use the hook procedure to hide the windows that pop up, which include
both the main Display Properties window as well as the Effects sub-window. The
infinitesimal flash still exists and if anyone has any ideas on further reducing
this, they are welcome to make suggestions.
The main function ( exported )
DEMODLL_API BOOL ToggleMenuUnderline(void)
{
HWND hWndAppearance = NULL;
BOOL ret = TRUE;
g_hook = SetWindowsHookEx(WH_CBT,
CBTProc, g_hModule, 0);
ret = BringUpDisplayAppearance();
Sleep(500);
if(ret)
{
hWndAppearance = FindWindow(NULL,
_T("Display Properties"));
ret = hWndAppearance != NULL;
if(ret)
{
HWND hWndEffectsButton = GetEffectsButton(
hWndAppearance);
ret = hWndEffectsButton != NULL;
if(ret)
{
PostMessage(hWndEffectsButton,BM_CLICK,0,0);
while(!IsWindow(g_hWndEffects))
Sleep(100);
HWND hWndCheck = GetMenuUnderlineCheck(
g_hWndEffects);
ret = hWndCheck != NULL;
if(ret)
{
PostMessage(hWndCheck,BM_CLICK,0,0);
PostMessage(g_hWndEffects,WM_COMMAND,
IDOK,NULL);
PostMessage(hWndAppearance,WM_COMMAND,
IDOK,NULL);
while(IsWindow(hWndAppearance))
Sleep(100);
}
}
}
}
if(IsWindow(g_hWndEffects))
PostMessage(g_hWndEffects,WM_CLOSE,0,0);
if(IsWindow(hWndAppearance))
PostMessage(hWndAppearance,WM_CLOSE,0,0);
return ret;
}
We first set up our CBT hook and then call the BringUpDisplayAppearance
function to bring up
the Display Properties window with the Appearance tab selected. Once the window
comes up we obtain the handle to the Effects button using the
GetEffectsButton
function, and then post a
BM_CLICK
message to the Effects button using the handle we just
obtained. Almost instantly the Effect sub-window pops up and we retrieve it's
handle through our CBT hook procedure which also uninstalls the hook since it's
no longer of any use to us. We wait for the Effects window to come up, using the
following while loop :-
while(!IsWindow(g_hWndEffects))
Sleep(100);
This way we avoid sleeping for too long or for too less. Now we obtain the
handle to the check box using the GetMenuUnderlineCheck
function
and post a BM_CLICK
message to the check box which effectively
toggles it's state which is just what we are trying to do. Now we simply post
WM_COMMAND
messages with wParam
set to IDOK
to the Effects sub-window as well as to the Display Properties main window.
That's all; we have now successfully toggled the state of the "Underline
keyboard navigation shortcuts for menus" system-wide property.
Conclusion
The test scenario we considered was perhaps too simplistic to reveal the
actual power of this technique, but when you consider that you can now do
anything from your program that a user can do manually using the Windows GUI,
you'll be slowly impressed by the awesome possibilities of the technique. You can use this
technique to enumerate Windows themes, change the current theme, change display
settings, change system settings, automate your own applications etc. The only
tool you'd need in addition to Everett is Spy++ or some such similar
application. Good luck with your own message based Windows automation attempts.
History
- August 8th 2003 - First written