Detect when a specific window in another process opens or closes
.net, c#, winforms
Solution
You can use either of these options:
- Using SetWinEventHook method
- Handling UI Automation Events (Preferred) (Suggested by Hans in comments)
Solution 1 - Using SetWinEventHook method
Using `SetWinEventHook` you can listen to some events from other processes and register a `WinEventProc` callback method to receive the event when the event raised.
Here `EVENT_SYSTEM_FOREGROUND` can help us.
We limit the event receiver to receive this event from a specific process and then we check if the text of the window which causes the event is equals to `Page Setup` then we can say the `Page Setup` window in the target process is open, otherwise we can tell the `Page Setup` dialog is not open.
To keep things simple in below example I supposed a `notepad` instance is open when your application starts, but you can also use `Win32_ProcessStartTrace` to detect when a `notepad` application runs.
To be more specific and say when the dialog is closed, you can listen to `EVENT_OBJECT_DESTROY` and detect if the message is for the window which we are interested in.
public const uint EVENT_SYSTEM_FOREGROUND = 0x0003;
public const uint EVENT_OBJECT_DESTROY = 0x8001;
public const uint WINEVENT_OUTOFCONTEXT = 0;
public delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd,
int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);
[DllImport("user32.dll")]
public static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr
hmodWinEventProc, WinEventDelegate lpfnWinEventProc, uint idProcess,
uint idThread, uint dwFlags);
[DllImport("user32.dll")]
public static extern bool UnhookWinEvent(IntPtr hWinEventHook);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
IntPtr hook = IntPtr.Zero;
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
var p = System.Diagnostics.Process.GetProcessesByName("notepad").FirstOrDefault();
if (p != null)
hook = SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND,
IntPtr.Zero, new WinEventDelegate(WinEventProc),
(uint)p.Id, 0, WINEVENT_OUTOFCONTEXT);
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
UnhookWinEvent(hook);
base.OnFormClosing(e);
}
void WinEventProc(IntPtr hWinEventHook, uint eventType,
IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
{
string s = "Page Setup";
StringBuilder sb = new StringBuilder(s.Length + 1);
GetWindowText(hwnd, sb, sb.Capacity);
if (sb.ToString() == s)
this.Text = "Page Setup is Open";
else
this.Text = "Page Setup is not open";
}
Solution 2 - Handling UI Automation Events
As suggested in comments by Hans, you can use UI Automation APIs to subscribe for `WindowOpenedEvent` and `WindowClosedEvent`.
In below example I supposed there is an open instance of `notepad` and detected opening and closing of its `Page Setup` dialog:
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
var notepad = System.Diagnostics.Process.GetProcessesByName("notepad")
.FirstOrDefault();
if (notepad != null)
{
var notepadMainWindow = notepad.MainWindowHandle;
var notepadElement = AutomationElement.FromHandle(notepadMainWindow);
Automation.AddAutomationEventHandler(
WindowPattern.WindowOpenedEvent, notepadElement,
TreeScope.Subtree, (s1, e1) =>
{
var element = s1 as AutomationElement;
if (element.Current.Name == "Page Setup")
{
//Page setup opened.
this.Invoke(new Action(() =>
{
this.Text = "Page Setup Opened";
}));
Automation.AddAutomationEventHandler(
WindowPattern.WindowClosedEvent, element,
TreeScope.Subtree, (s2, e2) =>
{
//Page setup closed.
this.Invoke(new Action(() =>
{
this.Text = "Closed";
}));
});
}
});
}
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
Automation.RemoveAllEventHandlers();
base.OnFormClosing(e);
}
Don't forget to add reference to `UIAutomationClient` and `UIAutomationTypes` assemblies and add `using System.Windows.Automation;`.
Problem
So I made a win app and I want it to pop in front of the screen whenever I open the "Page Setup" from notepad and close it whenever I close the notepad. I tried this: ``` [DllImport("user32.dll", SetLastError = true)] static extern IntPtr FindWindow(string lpClassName, string lpWindowName); [DllImport("user32.dll")] public static extern IntPtr GetParent(IntPtr hWnd); [DllImport("User32", CharSet = CharSet.Auto)] public static extern int ShowWindow(IntPtr hWnd, int cmdShow); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool IsIconic(IntPtr hwnd); [DllImport("user32.dll")] public static extern int SetForegroundWindow(IntPtr hWnd); ManagementEventWatcher watcher; public Form1() { InitializeComponent(); var query = new WqlEventQuery("SELECT * FROM Win32_ProcessStartTrace WHERE ProcessName = 'Notepad.exe'"); var mew = new ManagementEventWatcher(query) { Query = query }; mew.EventArrived += (sender, args) => { AppStarted(); }; mew.Start(); } protected override void OnLoad(EventArgs e) { base.OnLoad(e); watcher = new ManagementEventWatcher("Select * From Win32_ProcessStopTrace"); watcher.EventArrived += new EventArrivedEventHandler(watcher_EventArrived); watcher.Start(); } protected override void OnFormClosed(FormClosedEventArgs e) { watcher.Stop(); watcher.Dispose(); base.OnFormClosed(e); } void watcher_EventArrived(object sender, EventArrivedEventArgs e) { // var _windowHandle = FindWindow(null, "Page Setup"); if ((string)e.NewEvent["ProcessName"] == "notepad.exe") { Invoke((MethodInvoker)delegate { TopMost = false; Location = new System.Drawing.Point(1000, 1); }); } } async void AppStarted() { await Task.Delay(300); BeginInvoke(new System.Action(PoPInFront)); } void PoPInFront() { var _notepadProcess = Process.GetProcesses().Where(x => x.ProcessName.ToLower().Contains("notepad")).DefaultIfEmpty(null).FirstOrDefault(); if (_notepadProcess != null) { var _windowHandle = FindWindow(null, "Page Setup"); var _parent = GetParent(_windowHandle); if (_parent == _notepadProcess.MainWindowHandle) { Invoke((MethodInvoker)delegate { Location = new System.Drawing.Point(550, 330); TopMost = true; }); } } //var ExternalApplication = Process.GetProcessesByName("Notepad").FirstOrDefault(p => p.MainWindowTitle.Contains("Page Setup")); //Location = new System.Drawing.Point(550, 330); //TopMost = true; } ``` Until now my app pops in front of the screen and sets `TopMost = true` whenever I open notepad,not the "Page Setup" of the notepad and it moves back to the corner of the screen whenever I close the notepad. All I want to do is: I would like to move the app to the center of the screen and set `TopMost = true` whenever the "Page Setup" is opened and back to the corner and `TopMost = false` when the "Page Setup" is closed. `Location = new System.Drawing.Point(550, 330);` - for center of the screen `Location = new System.Drawing.Point(1000, 1);` - for corner Edit: I tried also this but no luck. ``` void PoPInFront() { //IntPtr hWnd = IntPtr.Zero; //foreach (Process pList in Process.GetProcesses()) //{ // if (pList.MainWindowTitle.Contains("Page Setup")) // { // hWnd = pList.MainWindowHandle; // Location = new System.Drawing.Point(550, 330); // TopMost = true; // } //} var ExternalApplication = Process.GetProcessesByName("Notepad").FirstOrDefault(p => p.MainWindowTitle.Contains("Page Setup")); Location = new System.Drawing.Point(550, 330); TopMost = true; } ``` Edit 2: I think the problem is here.I don't know how to do something like this: ``` var query = new WqlEventQuery("SELECT * FROM Win32_ProcessStartTrace WHERE ProcessName = 'Notepad.exe' AND MainWindowTitle = 'Page Setup'"); ``` `AND MainWindowTitle = 'Page Setup'`