I've spent most of the last year developing and playing with a skinning system, two elements of which I have previously posted to CodeProject (see 'A Revolutionary New Approach to Custom Drawn Menus' and 'A template class for easy implementation of Windows hooks').
Achieving good results in window skinning requires an exacting knowledge of exactly what Windows messages are sent to whom and when.
In the past, like many of you, I would reach for Spy++ and/or generate reams of TRACE statements and then stare for what seemed like hours trying to make sense of the output.
Just recently though, a number of specific issues had really begun to bug me:
- Spy++ doesn't allow you to spy on windows until after they have been created which means you generally miss a very interesting period in the window's life cycle.
- For the same reason, its very hard to spy on transient windows like menus, tooltips, combobox droplists, etc
- Spy++ doesn't easily (or at all?) allow you to spy on a whole bunch of windows all at the same time.
- using TRACE statements generally leads to output which has the message type expressed as a number rather than its usual
- Windows messages are hard to filter using TRACE statements, resulting usually in either too many or too few.
Out of this was born InterSpy, an integrated Windows message tracing utility, which solves all the problems I previously outlined and provided me with hours of enjoyment.
This utility requires you to build some source code into your application and cannot therefore be applied to a 3rd party application (unless you also have the source code).
Because this solution requires the inclusion of source code, the greatest challenge was to create something simple and accessible; that could be used with the minimum of fuss but giving the maximum result.
I happy to agree that I probably haven't reached that goal, but I find its always a useful place to aim for.
The following positive attributes were sought:
- Compact form
- Clearly delineated information
- Easily navigable
The first 'problem' to solve was how to get a handle on all the Windows messages flowing through an application.
The simplest solution to this I've found is to use application-wide Windows hooks.
Note: In the interest of those who do not care for Windows hooks I did spend sometime investigating the possible use of
PreTranslateMessage to see an application's messages but it was a dismal failure; MFC's implementation seems to leave out more messages than it shows and it certainly leaves out the interesting ones at the start and end of a window's life. (if anyone has anymore specific knowledge about exactly what
PreTranslateMessage() does and does not show I would be interested to learn more.)
To implement this I used CHookMgr which removes the ugly hooking details and simply requires the overriding of a few virtual functions to handle the message hooks.
As far as I can tell, the required Windows hooks to trap all Windows messages are
WH_GETMESSAGE (which translate to passing
HM_CALLWNDPROC | HM_GETMESSAGE to
The next step was to decide what to do with the messages.
Coming from an engineering background, I always favor figuring out the larger elements of a project before doing too much detail. That way, if I stumble up against something nasty in the woodshed (joke requiring some knowledge of an English dramatized play called 'cold comfort farm') I will not have wasted too much hard work.
It was already obvious that the messages would need to be sent to a separate window but the 'where, what and how' had not been decided.
My first prototype created a window in the same process as the application that was being traced, but the biggest problem was that the window was destroyed when the application terminated, which was an unacceptable 'feature'.
I was also interested in the possibility of outputting to a Visual Studio tool window, but not knowing the first thing about writing plug-ins for Visual Studio, rather put the dampeners on that idea.
So I was left with creating a separate application with a specially registered class name so that the code built into the client application could find it easily.
The only other architectural issue to resolve was the means of communicating between two, although strictly the communication is only one-way. I'd used
WM_COPYDATA a number of times before and so stuck with that because I was familiar with it.
Client-side (code built into the application)
One complication of having a sort-of client-server architecture is that the two sides do not share the same address space.
What this means simply is that the message processing would have to be done on the client side and not in the logging window, as I had initially wanted (remember: my first prototype created the logging window in the same process as the application to take advantage of the shared address space).
This was a nuisance because I had wanted to keep the source code to be compiled into the client application to be the absolute minimum necessary. Oh well, plans of mice and men and stuff ...
All Windows messages essentially take the same form: that of the MSG structure. However, after that, every one is different in how the
lParam values are used.
Some messages have no parameters, some use only one parameter and some use both parameters. Further, whilst most often the parameters are used to supply simple
UINT values, some use the
lParam (typically) to point to a memory structure supplying more detail than could otherwise be shoe-horned into the simple parameters.
I therefore first decided to implement a simple base-class message handler,
CISMsgHandler, which would do no more than crack each message into its constituent parameters.
Then, taking a pointer from MFC, I wrote a message map of sorts (implemented using macros) to assign an instance of this class to all the possible Windows messages I could find (and then some). These include: all the standard messages, the MFC specific messages in afxpriv.h and a bunch I have come across during my searches of the news groups and my own debugging.
Note: I am sure that some of the messages are obsolete but, since these will simply not be triggered if that is the case, I see little harm in leaving them in.
The obvious and intended benefit of this default message handler is that it allowed me to get the message map working so that I could then build the 'server-side' logging window to receive the messages.
Thereafter, it was plain old slog work to implement specific handlers for all the different message types. Fortunately, many messages such as
WM_LBUTTONDOWN share common parameters with other mouse button messages so that a single handler could be used for 6 or 7 messages. Elsewhere, its one for one, and in this case you'll see that I have not quite finished the task.
In cracking the messages, you may also note that I have not always gone as far as I might have. e.g. where a parameter describes a window handle (
HWND) I could have also retrieved the window's class and text to provide more information. The reason I have not done this at present is simply an issue of efficiency and performance.
Server-side (Message logging window)
There's not much more to say that cannot be gleaned from the screenshot above.
The main output view is a
CTreeCtrl, chosen simply because the data is hierarchical in nature and the tree control allows data to be hidden and displayed with such ease.
I also like the specific implementation of custom-draw on trees and the fact that you can embolden items just by setting a flag.
The logging window currently provides the following 'features' to simplify the output and navigation of messages:
- Facility to disable a specific message across all windows (disabled messages appear light-grey). Note: once disabled, the message will no longer appear until the logging window is restarted or you re-enable the message.
- Facility to clear all disabled messages from the tree.
- Facility to disable all messages for a specific window (disabled windows and their messages appear dark-grey).
- Single stepping forwards and backwards through the message list in the strict order in which the messages were received (indicated thus [
nn] in the output window).
- Facility to save message lists as plain text or XML.
Using the code
- Add the following source files to your project:
Note: in my demo project, these files are in a separate 'skinwindows' folder because they form a subset of a much larger skinning system, but there is no need for you to do the same.
CInterSpy (interspy.h/.cpp) - the derived hook manager which intercepts all messages, cracks them, and forwards the details to the logging window.
CHookMgr (hookmgr.h/.cpp) - the hook manager base class.
ISMsgManager/ISMsgHandler (ismsgmanager.h/.cpp, ismsghandlers.h) - message handler classes and manager which converts each MSG into an array of parameter strings. Note: these files can be dropped into any project where you need message trace output.
- Initialize Interspy in your
CWinApp derived application
InitInstance() method as follows:
// assumes files are in same folder as rest of the project
#include <span class="code-string">"interspy.h" </span>
// if there are specific messages that
// you always want excluded then
// these can be done statically in advance
The flags which may be passed to
CInterspy::Initialize() are as follows:
IS_NOKICKIDLE - excludes
IS_NODUPLICATES - culls out sequential messages of the same type.
IS_RESETONSTART - clears the message log window when the app (re)starts.
IS_AUTOSTARTOUTPUT - automatically starts the log window if it is not running.
- Provide built-in and user-definable filters to handle certain window types. e.g. a filter for menus would display only menu windows together with menu related messages sent to other windows.
- Provide more detail on
HWND parameters, perhaps with the ability to jump to the tree item representing the window being referenced.
The code is supplied here for you to use and abuse without restriction, except that you may not modify it and pass it off as your own. The concept and design, however, remains my intellectual property.
- 1.0 - Initial Release
- 1.1 - 40 or so further message handlers added (only obsolete and esoteric messages outstanding)