Delen via


SpyWindowFinalizer and Windows Message Hooking

One very powerful API that some UI developers may not be aware of is SetWindowHookEx.  This api allows you to intercept all types of window messages before (or after) they are processed.

You can use this to do a whole range of neat things.  For instance, you can log all messages in your app.  You can use the windows journal record/playback utility to create a test harness.  You can also write code to validate your application -- such as whether your application disposes all of its windows properly.  If you allow a control to finalize (for example, if you remove the control from its container and do not call Dispose() on it), it can hurt the performance of your application or even worse -- cause an AV.

The sample class I wrote below (SpyWindowFinalizer) allows you to detect controls in your application that are not being properly Dispose()'d and could be leaking and causing performance problems.  It allows you to take snapshots of all the controls currently valid in the application.  You could use this in a UI intensive app to measure where you may have too many temporary controls.  It also is a good starting sample if you'd like to do some window message hooking on your own!

using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
using System.Runtime.InteropServices;

namespace WindowSpy {
/// <summary>
/// This class uses SetWindowsHookEx to gather statistics and information
/// on windows created in the application.
/// It can easily be extended to track detailed information on each control or
/// modify each control in some manner
/// </summary>
class SpyWindowFinalizer {

        /// <summary>
/// These P/Invokes are necessary to create and manage the hook
/// </summary>
public delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("User32.dll", ExactSpelling = true, CharSet = CharSet.Auto)]
public static extern IntPtr CallNextHookEx(HandleRef hhook, int code, IntPtr wparam, IntPtr lparam);
[DllImport("User32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr SetWindowsHookEx(int hookid, HookProc pfnhook, HandleRef hinst, int threadid);
[DllImport("Kernel32.dll", ExactSpelling = true, CharSet = System.Runtime.InteropServices.CharSet.Auto)]
public static extern int GetCurrentThreadId();
[DllImport("User32.dll", ExactSpelling = true, CharSet = CharSet.Auto)]
public static extern bool UnhookWindowsHookEx(HandleRef hhook);

        public const int WH_CALLWNDPROC = 4; //this tells SetWindowsHookEx to hook WndProc calls
public const int HC_ACTION = 0; //when this comes into our callback, we should handle the message
public const int WM_CREATE = 0x0001; //this window message is sent when a window is created
public const int WM_NCDESTROY = 0x0082; //this window message is sent when the nonclient area of a window is destroyed

        static HookProc hookProc; //this is the delegate to our message hook
static IntPtr messageHookHandle; //the handle used for unregistering
static Hashtable ctorTable = new Hashtable(); //our window table that holds all of the active window information we need
static ArrayList badWindows = new ArrayList(); //a list of windows that finalized with the window active. This means the control was not disposed properly

        /// <summary>
/// Sets up our hook
/// </summary>
public static void Initialize() {
if (messageHookHandle != IntPtr.Zero) {
return;
}
hookProc = new HookProc(MessageHookProc);

            messageHookHandle = SetWindowsHookEx(WH_CALLWNDPROC,
hookProc,
new HandleRef(null, IntPtr.Zero),
GetCurrentThreadId());
}

        /// <summary>
/// Terminates our hook
/// </summary>
public static void Terminate() {
GC.Collect();
GC.WaitForPendingFinalizers();
Application.DoEvents();

            if (messageHookHandle != IntPtr.Zero) {
UnhookWindowsHookEx(new HandleRef(null, messageHookHandle));
hookProc = null;
messageHookHandle = IntPtr.Zero;
}
foreach (string info in ctorTable.Values) {
badWindows.Add("**possible leak**" + info);
}
ctorTable.Clear();
}

        /// <summary>
/// Returns a list of the windows that were not disposed.
/// </summary>
public static IEnumerable WindowsFinalized {
get {
return badWindows;
}
}

        /// <summary>
/// Returns the current list of all windows forms controls
/// </summary>
public static IEnumerable WindowsStillAlive {
get {
ArrayList list = new ArrayList();
foreach (IntPtr handle in ctorTable.Keys) {
Control c = Control.FromHandle(handle);
if (c != null) {
list.Add(c);
}
}
return list;
}
}

        /// <summary>
/// The message hook itself.
/// It stores info when windows get created and destroyed.
/// </summary>
private static unsafe IntPtr MessageHookProc(int nCode, IntPtr wparam, IntPtr lparam) {
if (nCode == HC_ACTION) {
CWPSTRUCT* msg = (CWPSTRUCT*)lparam;
if (msg != null) {
if (msg->message == WM_CREATE) {
Control c = Control.FromHandle(msg->hwnd);
if (c != null) {
string controlentry = c.Name + " : " + c.GetType().FullName;
controlentry += new System.Diagnostics.StackTrace().ToString();
ctorTable.Add(msg->hwnd, controlentry);
}
}
else if (msg->message == WM_NCDESTROY) {
if (ctorTable.ContainsKey(msg->hwnd)) {
NativeWindow w = NativeWindow.FromHandle(msg->hwnd);
if (w == null) {
badWindows.Add(ctorTable[msg->hwnd]);
}
ctorTable.Remove(msg->hwnd);
}
}
}
}

            return CallNextHookEx(new HandleRef(null, messageHookHandle), nCode, wparam, lparam);
}

        /// <summary>
/// parameter type for message hook
/// </summary>
[StructLayout(LayoutKind.Sequential)]
[Serializable]
public struct CWPSTRUCT {
public IntPtr lParam;
public IntPtr wParam;
public int message;
public IntPtr hwnd;
}
}
}

To use this in your app, write this:

  [STAThread]
static void Main()
{
SpyWindowFinalizer.Initialize();

            Application.Run(new Form1());

            SpyWindowFinalizer.Terminate();
foreach(string s in SpyWindowFinalizer.WindowsFinalized) {
System.Windows.Forms.MessageBox.Show(s);
}
}

This will display the control names, types and contructor callstacks for all controls that finalized before being destroyed (meaning they were likely not properly disposed of).

You can test this by creating a simple windows form application with 2 buttons. In the button click handlers do the following:

        private void button1_Click(object sender, System.EventArgs e) {
this.button1.Parent = null;
this.button1 = null;
}

        private void button2_Click(object sender, System.EventArgs e) {
GC.Collect();
GC.WaitForPendingFinalizers();
Application.DoEvents();
}

Run the application,

Click button1  (it should disappear)

Click button2

Close Form1 and you should see a dialog indicating a potential bad window (being button1 which was never disposed).

 

For more info, see:

https://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/winui/windowsuserinterface/windowing/hooks/hookreference/hookfunctions/setwindowshookex.asp