1

I have a C# program that polls for changes to EnumDesktopWindows collection. If a user closes or opens a window the polling routine detects this and sends an updated list of available windows to another .Net windows forms project. However, I do not like the polling method. I would prefer that any change to the EnumDesktopWindows triggers an event so that responding to the change is done asynchronously.

The best I could come up with is what you see below. I tried out Scott C.'s suggestion to execute from a console window, but it did not work.

Currently what you see below captures CreateWnd=3 when the Windows Form loads (this is a windows form application). However it does not capture globally: it only captures the window events from the currently running executable. If anyone has eagle eyes and can spot how to make this code capture globally I will award the answer.

To try it out; first create a Windows Forms application project and add the following code to Form1.cs (you will need to add a ListBox to the Form named lstLog to compile correctly)

using System;
using System.Windows.Forms;

namespace Utilities
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            var gwh = new GlobalWindowHook();
            gwh.WindowCreated += onWindowCreated;
        }

        private void onWindowCreated()
        {
            lstLog.Items.Add("window creation event detected.");
        }
    }
}

Create a class file in the same project named GlobalWindowHook.cs and copy paste the following:

using System;
using System.Runtime.InteropServices;

namespace Utilities
{
    internal class GlobalWindowHook
    {
        private delegate IntPtr HookProc(int code, IntPtr wParam, IntPtr lParam);

        [DllImport("user32.dll")]
        private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

        public enum HookType
        {
            WH_JOURNALRECORD = 0,
            WH_JOURNALPLAYBACK = 1,
            WH_KEYBOARD = 2,
            WH_GETMESSAGE = 3,
            WH_CALLWNDPROC = 4,
            WH_CBT = 5,
            WH_SYSMSGFILTER = 6,
            WH_MOUSE = 7,
            WH_HARDWARE = 8,
            WH_DEBUG = 9,
            WH_SHELL = 10,
            WH_FOREGROUNDIDLE = 11,
            WH_CALLWNDPROCRET = 12,
            WH_KEYBOARD_LL = 13,
            WH_MOUSE_LL = 14
        }

        public enum HCBT
        {
            MoveSize = 0,
            MinMax = 1,
            QueueSync = 2,
            CreateWnd = 3,
            DestroyWnd = 4,
            Activate = 5,
            ClickSkipped = 6,
            KeySkipped = 7,
            SysCommand = 8,
            SetFocus = 9
        }

        private IntPtr hhook = IntPtr.Zero;

        public GlobalWindowHook()
        {
            hook();
        }


        ~GlobalWindowHook()
        {
            unhook();
        }


        public void hook()
        {
            IntPtr hInstance = LoadLibrary("User32");

            hhook = SetWindowsHookEx(HookType.WH_CBT, hookProc, hInstance, 0);
        }

        public void unhook()
        {
            UnhookWindowsHookEx(hhook);
        }

        public IntPtr hookProc(int code, IntPtr wParam, IntPtr lParam)
        {
            if (code != (int) HCBT.CreateWnd && code != (int) HCBT.DestroyWnd)
                return CallNextHookEx(IntPtr.Zero, code, wParam, lParam);

            //Do whatever with the created or destroyed window.

            return CallNextHookEx(IntPtr.Zero, code, wParam, lParam);
        }


        [DllImport("user32.dll")]
        private static extern IntPtr SetWindowsHookEx(HookType code, HookProc func, IntPtr hInstance, int threadId);

        [DllImport("user32.dll")]
        private static extern bool UnhookWindowsHookEx(IntPtr hInstance);

        [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Auto)]
        private static extern IntPtr LoadLibrary(string fileName);
    }
}

After performing the above steps execute the windows forms project. You should see that it detects one window being created, namely the one you just executed.

sapbucket
  • 6,795
  • 15
  • 57
  • 94

1 Answers1

0

If you want to be pushed the notifications instead of pulling them it is not too difficult. What you will need to do is P/Invoke SetWindowsHookEx and register for the WH_CBT event. Once you get those messages you can listen for messages with the HCBT_CREATEWND or HCBT_DESTROYWND codes. That will notify you when a new window is created or is destroyed.

Using the types defined at pinvoke.net

class HookingClass
{
    private delegate IntPtr CBTProc(HCBT nCode, IntPtr wParam, IntPtr lParam);

    private readonly CBTProc _callbackDelegate;

    public HookingClass()
    {
         _callbackDelegate = CallbackFunction;
    }

    private IntPtr _hook;

    private void CreateHook()
    {
        using (Process process = Process.GetCurrentProcess())
        using (ProcessModule module = process.MainModule)
        {
            IntPtr hModule = GetModuleHandle(module.ModuleName);

            _hook = SetWindowsHookEx(HookType.WH_CBT, _callbackDelegate, hModule, 0);
            if(_hook == IntPtr.Zero)
                throw new Win32Exception(); //The default constructor automatically calls Marshal.GetLastError()
        }

    }

    private void Unhook()
    {
        var success = UnhookWindowsHookEx(_hook);
        if(!success)
            throw new Win32Exception();
    }

     private IntPtr CallbackFunction(HCBT code, IntPtr wParam, IntPtr lParam)
     {
         if (code != HCBT.CreateWnd && code != HCBT.DestroyWnd) 
         {         
             return CallNextHookEx(IntPtr.Zero, code, wParam, lParam);
         }

         //Do whatever with the created or destroyed window.

         return CallNextHookEx(IntPtr.Zero, (int)code, wParam, lParam);
     }
}
Scott Chamberlain
  • 124,994
  • 33
  • 282
  • 431
  • @sapbucket Your problem is `hModule` needs to point at the assembly that contains `_callbackDelegate` function. `user32.dll` does not contain your function and that is why it is not working. Try doing it from a test console app before you go try running it from inside a unit test. Also your unit test program will quit and shutdown before the event ever fires. – Scott Chamberlain Sep 04 '14 at 20:54
  • Hans Passat explains that the hModule isn't used in the low level library and that passing in anything valid should do the trick. That is why I stuck with LoadLibrary. Is Hans incorrect? Either way, i am not getting success. – sapbucket Sep 04 '14 at 23:59
  • 1
    That's only true for the low-level hooks. Nothing low-level about a CBT hook, the module handle is *very* important. – Hans Passant Sep 05 '14 at 00:17
  • @Hans - I found a thread that indicates what I'm attempting to do isn't supporting by .Net; I trust your opinion - is the thread correct? http://stackoverflow.com/questions/1811383/setwindowshookex-in-c-sharp – sapbucket Sep 05 '14 at 00:49
  • That is correct, yes. You cannot inject C# code into another process. Afaik, Scott's code will only tell you about windows in your own app getting created and closed. Not typically what programmers are looking for, especially not in a console mode app :) Which is why I posted the alternative. Message loop required so avoid a console mode app. – Hans Passant Sep 05 '14 at 00:55
  • @Hans Passant - In your alternative that you posted I noticed that it refers to a C++ solution. To be clear, is your recommendation to use a C++ solution, one that is not .Net? I ask because I want to make sure that if I go down the path of a C++ solution that I use a version of C++ that is independent of .Net. (it is temping to try out the alternative using VS2012 and C++ .Net; which if I understand correctly will not work for the same reasons C# .Net will not work). – sapbucket Sep 06 '14 at 00:19
  • Just pinvoke the function, no different from pinvoking SetWindowsHookEx(). – Hans Passant Sep 06 '14 at 00:21