Dienstag, 2. Oktober 2012

Event bei der Erstellung von neuen Fenstern

In komplexen Anwendungen ist es manchmal notwendig herauszufinden, wann eine MessageBox erscheint oder ein neues Fenster im aktuellen Prozess erzeugt wird. Oder gar, wann in einem anderen Prozess ein Fenster geöffnet wurde.

Das .Net Framework bringt von Hause aus leider kein solches Event mit, daher müssen wir hier etwas basteln. Die Windows API hat hier alles, was wir brauchen (nähere Infos zu unmanaged APIs unter http://www.pinvoke.net ) Um genau zu sein sind die Funktionen, die wir benötigen in der user32.dll.

EnumDesktopWindows

static private delegate bool EnumWindowsDelegate(IntPtr hWnd, int lParam);

[DllImport("user32.dll")]
static extern bool EnumDesktopWindows(IntPtr hDesktop, EnumWindowsDelegate lpfn, IntPtr lParam);

Diese Funktion iteriert durch alle Fenster, die dem Fenster mit dem handle hDesktop untergeordnet sind. Für jede Iteration wird der delegate lpfn aufgerufen. Dadurch können wir uns zu jedem Zeitpunkt eine Liste mit den aktuell offenen WindowHandles aufbauen:
static List<intptr> _currentWindows = new List<intptr>();
IntPtr hwndDesktop = IntPtr.Zero; // current desktop
bool success = EnumDesktopWindows(hwndDesktop, 
                new EnumWindowsDelegate(
                 (hWnd, lparam) => 
                 { 
                   currentWindows.Add(hWnd); 
                   return true; 
                 }), IntPtr.Zero);

GetWindowThreadProcessId

Da wir nicht zwingend alle neu erstellten Fenster melden möchten, sondern wahlweise auch nur die Fenster, die zum aktuellen Prozess gehören,  wird die ProzessID des WindowHandle benötigt. Das können wir folgendermaßen auflösen:
[DllImport("user32.dll", SetLastError = true)]
static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
// get the process id of a window handle
uint lpdwProcessId;
GetWindowThreadProcessId(winHndl, out lpdwProcessId);

GetWindowText

Zu guter Letzt sollten wir neben dem WindowHandle vielleicht noch den Text des Fensters herausfinden, denn mit dem Handle kann man vermutlich im verwalteten Code nicht viel anfangen.

[DllImport("user32.dll")]
private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpWindowText, int nMaxCount);
// get the window title
StringBuilder windowName = new StringBuilder(256);
GetWindowText(hWnd, windowName, windowName.Capacity);

Alle Drei zusammen

Der Ablauf des Programms sollte nun eigentlich relativ klar sein (hier ein kurzer Abschnitt des essentiellen Ablaufs als Pseudocode):
erzeuge neuen Thread mit folgendem Code:
Liste bekannteFenster := leere Liste
Liste aktuelleFenster := leere Liste
do
  • aktuelleFenster <- EnumDesktopWindows
  • für alle f in aktuelleFenster , aber nicht in bekannteFenster
    • GetWindowThreadProcessId ?= CurrentProcess.Id
    • -> Raise WindowCreatedEvent(f, GetWindowText(f))
while (true)

Und für alle, die es gleich mal ausprobieren wollen, hier mein Implementierungsvorschlag:

static class ChildWindowOpenHook
{
  #region private fields / delegates
  private delegate bool EnumWindowsDelegate(IntPtr hWnd, int lParam);
  static private Thread _workerThread = null;
  static List _currentWindows = new List();
  static List _knownWindows = new List();
  static bool _isRunning = true;
  static bool _onlyOwnWindows = false;
  #endregion 

  #region DLLImport
  [DllImport("user32.dll", SetLastError = true)]
  static extern bool EnumDesktopWindows(IntPtr hDesktop, EnumWindowsDelegate lpfn, IntPtr lParam);
  
  [DllImport("user32.dll", SetLastError = true)]
  static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
  
  [DllImport("user32.dll")]
  private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpWindowText, int nMaxCount);
  #endregion

  #region Starter
  /// 
  /// C'tor
  /// starts the enumeration thread
  /// 
  public static void Start(bool onlyOwnWindows)
  {
   _onlyOwnWindows = onlyOwnWindows;
   // 1. start enumerating the first time
   EnumerateWindows();
   // 2. copy those current windows in out old window list
   _knownWindows.AddRange(_currentWindows);
   // 3. start the enumeration thread, which will enumerate through the
   // current windows periodically
   ThreadStart tStart = new ThreadStart(ThreadRunner);
   _workerThread = new Thread(tStart);
   _workerThread.Name = "window watcher thread";
   _workerThread.Priority = ThreadPriority.BelowNormal;
   _workerThread.IsBackground = true; // important, so the mainthread will not wait on this one

   _workerThread.Start();
  }
  #endregion

  #region ThreadRunner
  /// 
  /// Thread Runner 
  ///  - iterates through all desktop windows
  ///  - fires new window event, when a new window has been detected
  /// 
  static private void ThreadRunner()
  {
   try
   {
    while (_isRunning)
    {
     // clear the current window list
     _currentWindows.Clear();
     // populate the currentwindow list
     EnumerateWindows();
     // find out which windows are not known already
     foreach (IntPtr winHndl in _currentWindows.Where(hndl => !_knownWindows.Contains(hndl)))
     {
      // add the windowhandle of a "NEW WINDOW" to our list of known windows
      _knownWindows.Add(winHndl);
      
      // get the process id of that new window
      uint lpdwProcessId;
      GetWindowThreadProcessId(winHndl, out lpdwProcessId);
      // find out if its on the same process, as we are
      if (
       !_onlyOwnWindows
       || (uint)System.Diagnostics.Process.GetCurrentProcess().Id == lpdwProcessId)
      {
       // raise the event
       OnWindowCreated(winHndl);
      }

     }
     Thread.Sleep(500);
    }


   }
   catch (Exception aException)
   {
    // write down the exeption that occured
    Console.WriteLine("exception in thread:" + aException);
   }
  }
  #endregion

  #region EnumerateWindows
  static private void EnumerateWindows()
  {
   IntPtr hwndDesktop = IntPtr.Zero; // current desktop
   bool success = EnumDesktopWindows(hwndDesktop, 
    new EnumWindowsDelegate(
     (hWnd, lparam) => 
     { 
      _currentWindows.Add(hWnd); 
      return true; 
     }), IntPtr.Zero);

   // if we have no success, lets throw an error
   if (!success)
   {
    // get the last win32 error code
    int errorCode = Marshal.GetLastWin32Error();
    string errorMessage = string.Format("EnumDesktopWindows failed, error code: {0}.", errorCode);
    throw new Exception(errorMessage);
   }
  }
  #endregion

  #region Stopper
  /// 
  /// Stops the ChildWindowOpenHook
  /// 
  public static void Stop()
  {
   _isRunning = false;
   _workerThread.Abort();
  }
  #endregion

  #region Event Handling
  /// 
  /// delegate for WindowCreated event
  /// 
  /// name of the window, that has been created
  /// window handle
  public delegate void WindowCreatedEventHandler(string windowName, IntPtr hWnd);
  
  /// 
  /// handles window created events
  /// 
  public static event WindowCreatedEventHandler WindowCreated;

  /// 
  /// being called, when a window has been created
  /// 
  /// the window handle
  private static void OnWindowCreated(IntPtr hWnd)
  {
   if (WindowCreated!=null && _isRunning)
   { 
    // get the window title
    StringBuilder windowName = new StringBuilder(256);
    GetWindowText(hWnd, windowName, windowName.Capacity);
    // handle the event
    WindowCreated(windowName.ToString(), hWnd);
   }
  }
  #endregion
}

Man kann sich folgendermaßen an das event hängen:
public MainWindow()
{
 InitializeComponent();
 ChildWindowOpenHook.Start(true);
 ChildWindowOpenHook.WindowCreated += ChildWindowOpenHook_WindowCreated;
}

void ChildWindowOpenHook_WindowCreated(string windowName, IntPtr hWnd)
{
 Console.WriteLine(windowName);
}

Keine Kommentare:

Kommentar veröffentlichen