Geeks With Blogs
MightyZot

Purpose of Article

I’m sure that there are a ton of articles out there regarding how to subclass windows. The purpose of this article is to walk through the entire process of why you might want to subclass a window, how to look at the window hierarchy, and then how to actually do the subclassing.

At PaperWise (http://www.paperwise.com), we have a new architecture that is .NET and an older architecture that is COM, C++, and VB6. While VB6 is a powerful language, I am sure that you would agree that there are significant limitations with the platform. We have stretched VB6 to its limits. And, when we’ve reached those limits, we have filled the holes with Windows API calls and subclassing.

More recently, we’ve started extending the VB6 architecture with .NET plug-ins. This gives us the advantage of using .NET and teaching it to the group that implements our solutions, while we’re working on rewriting our core product suite in managed code. In the next section, I’ll give a little bit of an overview of our plug-in architecture, and then we’ll write a managed plug-in that extends the capabilities of the product using subclassing.

Introduction to Query

PaperWise (http://www.paperwise.com) is a document management and workflow solution that I’ve been involved with for the past eight or nine years. As mentioned previously, the current architecture is COM, C++, and VB6. We are in the process of re-engineering the suite in managed code with a whole new (and significantly advanced) automation model. While the automation model in PaperWise is extremely flexible, and indeed that is our competitive advantage, there are limitations that are largely imposed by COM. For the purposes of this article, I’ll show you how to break through some of the limitations in our Query application.

Query’s Role

Query is one of the client applications at the heart of the PaperWise Document Management Suite. It is a VB6 application made up of many VB and C++ components. Users search for and index documents using our Query application, so it presents them with a viewer and a way to maintain metadata.

COM Plug-in Architecture

Over the years, Query has become very extensible, sporting an automation and plug-in model very similar to Microsoft Office. The extensibility of our client applications is what makes our solution so attractive amongst the plethora of document management and workflow solutions out there. Our implementation staff and our customers write plug-ins, scripts, and programs to extend the functionality of Query. Examples of extensions include integrations to other line of business applications, such as TMW, Maddocks, Innovative, CSR24, TAM, Sagitta, Vision, Great Plains, QuickBooks, Microsoft CRM, etc.  Let’s concentrate on plug-ins.

Plug-ins can be written in any COM-compatible language. There are a handful of interfaces that our client applications use to notify plug-ins that events have occurred. Implementing a plug-in is as simple as creating a COM component, setting a reference to the library containing our interfaces, and then implementing those interfaces. The interface that I want to talk about in this case is IQueryNotify. IQueryNotify provides events to tell a plug-in when a file cabinet has changed, when the user has selected a new image or document, or when changes to documents are being submitted; however, there are no methods in the interface to tell a plug-in when the user has changed focus from one field to another.

The Limitation

The fields in Query that are used for input are wrapped in a COM component. Aside from that, these fields are just textboxes and other controls with the typical properties and events that you’d expect them to expose. Figure 1 shows a screen shot of our Query application. Notice the input fields along the left.

Figure 1 - PaperWise Query

Figure 1 – PaperWise Query application.

As mentioned above, there are no events to tell a plug-in when the focus changes from one field to another. To add these events, we would need to provide another IQueryNotify-like interface that includes new methods, since we can’t modify the IQueryNotify interface due to the immutability of COM interfaces. Adding a new interface requires redistribution of the client, as well as an arduous testing and release cycle. We can solve this problem, though, with a bit of reverse engineering and API magic.

Overview of Subclassing

Subclassing is the process of extending or overriding the functionality of a window. Windows have functions or procedures associated with them called WndProc functions. The responsibility of the WndProc function is to respond to messages that are posted to a window’s message queue. Messages include drawing events, mouse events, sizing events, etc. When you subclass a window, you replace the window’s WndProc function with your own. You may optionally pass messages along to the original WndProc associated with the window.

To override the functionality of a window, you simply provide your own handler for a particular message or set of messages. For this example, I’m going to extend the controls in that group of fields you see along the left in our Query application and add a LostFocus event. My plug-in, which I will write in C#, will response to this LostFocus event.

Locating Window Handles

The first step in subclassing a window is finding the window you want to subclass. I’ve found over the years that Spy++ is a great tool to use to examine windows. Spy++ is included with Visual Studio and you can see a screen shot of the tool in Figures 2 and 3 below. To find a window, you can use the Find (Alt-F3) feature. Spy++ gives you a crosshair cursor that you can drag over the windows you want to examine. As you drag the crosshair, Spy++ gives you detailed information about the windows under the cursor.

clip_image002[6]
Figure 2 - Spy++ showing the window hierarchy.

 

clip_image004
Figure 3 - Spy++ and the Query application.

The windows that I’m interested in are those containing the input fields, since the input fields are what I want to extend. To find them, you simply use the Find feature of Spy++. When you hover over the first input field (Name) and drop the crosshair, Spy++ gives you information about the window in the dialog shown in Figure 4. Clicking on OK locates the window in the hierarchy, as shown in Figure 5.

clip_image006
Figure 4 - Information in this dialog describes the window.

 

clip_image008
Figure 5 - Spy++ shows the window in the hierarchy.

You can learn a lot about a Windows application by examining its windows. The key to extending the functionality of an application using subclassing is deciphering how to get to the window you want to overload. Several key pieces of information are pertinent in making this work: the position of the window within the hierarchy, the control ID of the window, and the class name of the window.

Knowing which piece of information to use when is a matter of investigation and experimentation. Windows enter and exit the hierarchy as they’re created, so you often can’t rely on a window’s position in the hierarchy unless you’re sure it’s static. Or, if it’s not static within the hierarchy, it may be static from some relative position within the hierarchy (such as from its parent.) To find a window programmatically, you end up using a combination of searching for a window by name, then searching relative from that window by position, then searching by class name, or various combinations of that methodology.

Luckily, the Query application returns a handle to its main window through its automation model, which you will see shortly. To get to the input controls in the main Query window, I’ll use the main window handle from the automation model, and use that to get the handle to the splitter. Next, I’ll use the handle of the splitter to get the user control containing the tabs, the tabs, and the panel containing the field list. From the field list, I’ll get the scroller containing the fields. Once I have the scroller, I’ll use a class that I’ve created to subclass each of the child controls and extend their functionality.

ChildEventWatcher

My ChildEventWatcher class is responsible for replacing the window procedures for each of the child windows of a specified parent. To create an instance of the class, you pass in the parent containing the children you want to subclass. Following is a code example.

public ChildEventWatcher(IntPtr parent)
{
     Debug.Assert(parent != null, "Parent window can't be null!");

     // Allocate the procedure list...all prior window procs will be stored here.
     _proclist = new Dictionary<IntPtr, IntPtr>(50);

     // Enumerate child windows and install hooks.
     EnumChildWindows(parent, new EnumChildProc(MyEnumChildProc), 42u);

}

Window handles are best represented by IntPtr. _proclist is a dictionary that will cross-reference window handles with window procs, so that the original window handles can be restored at the end of the event watcher’s lifecycle. EnumChildWindows is called to enumerate through the child windows of the parent passed into the contructor. A delegate is passed along as a callback and the value of 42 that you see in that call is superfluous in this example.

EnumChildWindows is, of course, a Windows API function. I’m importing it with the DllImport attribute and the extern keyword. The code for the import looks like this:

[DllImport("user32.dll")] static extern bool EnumChildWindows(IntPtr window, EnumChildProc callback, UInt32 lparam);

The delegate declaration for EnumChildProc comes from the Windows API and looks like this:

delegate bool EnumChildProc(IntPtr hwnd, UInt32 lparam);

Following is the EnumChildWindows call, the MyEnumChildProc method is called for every child window of the specified parent until MyEnumChildProc returns false. You can see the code for the callback below. My implementation of this method uses the SetWindowLong Windows API method, aliased as SetWindowLongWndProc, to set the window handler for each child to a method that I’ve defined called MyWndProc. Returning true tells EnumChildWindows to continue enumeration.

bool MyEnumChildProc(IntPtr window, UInt32 lparam)
{

     // Subclass the enumerated child window.
     IntPtr oldhook = IntPtr.Zero;
     oldhook = SetWindowLongWndProc(window, GWL_WNDPROC, new WndProc(MyWndProc));

     _proclist[window] = oldhook;

     return true;

}

The import declaration for SetWindowLongWndProc and GWL_WNDPROC is as follows:

const Int32 GWL_WNDPROC = -4;
[DllImport("user32.dll", EntryPoint = "SetWindowLong")] static extern IntPtr SetWindowLongWndProc(IntPtr window, Int32 id, WndProc wndproc);

Calling SetWindowLong with GWL_WNDPROC replaces the specified window’s handler and then returns a pointer to the old handler. I’ve aliased this method, because I’ll need another import to handle restoring the original window handlers. Pointers to the original window handlers are stored in _proclist.

My replacement window handler, MyWndProc, is responsible for doing the overloading, or extending the functionality of the child windows. My implementation of this method is shown below, along with the declaration for WM_KILLFOCUS and the import for CallWindowProc.

const UInt32 WM_KILLFOCUS = 0x08;

[DllImport("user32.dll")] static extern UInt32 CallWindowProc(
     IntPtr wndproc, IntPtr window, UInt32 msg, UInt32 wparam, UInt32 lparam);

UInt32 MyWndProc(IntPtr window, UInt32 msg, UInt32 wparam, UInt32 lparam)
{

     if(msg == WM_KILLFOCUS)
     {
          OnKillFocus(new ChildEventWatcherEventArgs() { window = window });
     }

     return CallWindowProc(_proclist[window], window, msg, wparam, lparam);

}

Note the call to CallWindowProc. With some messages, you may want to call the original window handler before overloading functionality, after overloading functionality, or not at all. Since I’m overloading the WM_KILLFOCUS behavior, I’ll call the original window handler after dispatching the focus change event. Recall that the pointer to the original window handler is in the _proclist dictionary. The parameters passed into the replacement handler are simply passed along to the original handler. OnKillFocus dispatches the event after checking to see if any delegates are assigned to the event. The event declaration and implementation of OnKillFocus are shown below. ChildEventWatcherEventArgs is just an EventArgs with a window property, which I’m using to pass along the window handle of the control.

public EventHandler<ChildEventWatcherEventArgs> KillFocus;

virtual protected void OnKillFocus(ChildEventWatcherEventArgs childEventWatcherEventArgs)
{
     EventHandler<ChildEventWatcherEventArgs> handler = this.KillFocus;
     if (handler != null)
          handler(this, childEventWatcherEventArgs);

}

That’s a pretty standard implementation for the event handler. To use it, you assign your handlers to the KillFocus event. Implementing IDisposable is the last step. IDisposable is used to restore the original window handlers for all of the child windows I’ve subclassed and this is what its implementation looks like:

public void Dispose()
{
     // Loop through all of the windows previously
     // hooked and restore their wndprocs.
     foreach (IntPtr w in _proclist.Keys)
          SetWindowLongPointer(w, GWL_WNDPROC, _proclist[w]);

     // Suppress finalization.
     GC.SuppressFinalize(this);

}

SetWindowLongPointer is another alias for SetWindowLong. Instead of a callback, it takes the IntPtr values returned from the SetWindowLongWndProc alias. This is what it looks like:

[DllImport("user32.dll", EntryPoint = "SetWindowLong")]
static extern IntPtr SetWindowLongPointer(IntPtr window, Int32 id, IntPtr p);

Putting it to Use

Now that I’ve created the ChildEventWatcher class, I’m ready to use it. I won’t go into the details of implementing a plug-in for the PaperWise Query application, beyond the background I’ve already given you; however, I would like to show the implementation of the IQueryNotify.CabinetSelected method and the lost focus event handler.

public void CabinetSelected(object Cabinet, object FieldList)
{

     // Get a reference to Query's application object 
     // and get a reference to the main window.

     if (_query == null)
     {
          _query = new Query.ApplicationClass();
          _mainwnd = (IntPtr)_query.GuiChildHwnd;

          // Get the window handle to the SSSplitterWndClass.
          IntPtr splitter = GetWindow(_mainwnd, GW_CHILD);

          if (splitter != IntPtr.Zero)
          {
               // Get a handle to the user control containing the tabs.
               IntPtr container = GetDlgItem(splitter, 3);
               if (container != IntPtr.Zero)
               {
                    // Get a handle to the tab control.
                    IntPtr tabs = GetWindow(container, GW_CHILD);
                    if (tabs != IntPtr.Zero)
                    {
                         // Get the handle to the tab panel containing the field list.
                         IntPtr panel = GetWindow(tabs, GW_CHILD);
                         if (panel != null) panel = GetWindow(panel, GW_HWNDNEXT);
                         if (panel != null) panel = GetWindow(panel, GW_HWNDNEXT);
                         if (panel != null)
                         {
                              // Get the handle to the user control 
                              // containing the scroller.
                              IntPtr scrollparent = GetWindow(panel, GW_CHILD);
                              if (scrollparent != null)
                              {
                                   // Get the handle to the scroller.
                                   IntPtr scroller = GetWindow(scrollparent, GW_CHILD);
                                   if (scroller != null)
                                   {
                                        // Install a watcher to watch for
                                        // changes in the children of the 
                                        // mega controls.
                                        _eventwatcher = new ChildEventWatcher(scroller);
                                        _eventwatcher.KillFocus += new EventHandler
                                             <ChildEventWatcherEventArgs>(Child_KillFocus);

                                   } // scroller

                              } // scrollparent

                         } // panel

                    } // tabs

               } // container

          } // splitter

     }

}

That’s a lot of stuff! But, essentially, it does what I alluded to earlier. Query’s automation model is used to get the window handle of the main window. Once I have the window handle of the main window, I use that to get the splitter, the splitter, to get the user control containing the tabs, that to get the tabs, and the tabs to get the scroller. I use the scroller as the parent window when constructing the ChildEventWatcher object. I’m assigning an event handler to the KillFocus event exposed by ChildEventWatcher. Now anytime a field loses focus, the KillFocus event handler in my plug-in is called.

IQueryNotify.CabinetSelected is called anytime the user changes their file cabinet in the PaperWise Query application. Plug-ins run as DLLs in Query’s process space. Voila! I have extended the functionality of those input controls to include a kill focus event without modifying the original COM components!

Conclusion

We used subclassing very commonly back in the early 90s while working on the Procomm Plus project at DataStorm. This is a very handy skill for you to have in your toolset. Not only is this technique handy for adding extended functionality to components you’ve already deployed, you can use it to extend functionality in components and applications for which you don’t control the source code!

Posted on Friday, June 26, 2009 10:48 PM Windows API | Back to top


Comments on this post: Subclassing to Extend Controls and Windows

No comments posted yet.
Your comment:
 (will show your gravatar)


Copyright © MightyZot | Powered by: GeeksWithBlogs.net