Detect when a separate window in another process is opened or closed
So, I made a win application and I want it to appear in front of the screen whenever I open Page Settings from notepad and close it when I close notepad.
I've 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;
}
So far, my app pops up in front of the screen and installs TopMost = true
whenever I open notepad, not "Page Setup" in notepad, and when I close notepad it goes back to the corner of the screen.
All I want to do is:
I would like to move the app to the center of the screen and set TopMost = true
whenever Page Setup opens and returns to the corner and TopMost = false
when Page Setup is closed.
Location = new System.Drawing.Point(550, 330);
- for the center of the screen
Location = new System.Drawing.Point(1000, 1);
- for the corner
Edit:
I tried this too, 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 do not know how to do that:
var query = new WqlEventQuery("SELECT * FROM Win32_ProcessStartTrace WHERE ProcessName = 'Notepad.exe' AND MainWindowTitle = 'Page Setup'");
AND MainWindowTitle = 'Page Setup'
source to share
You can use any of these options:
- Using the SetWinEventHook method
- Handling UI Automation events (preferred) (suggested by Hans in the comments )
Solution 1. Using the SetWinEventHook method
Using SetWinEventHook
, you can listen for some events from other processes and register a callback method WinEventProc
to receive the event when the event has occurred.
Here EVENT_SYSTEM_FOREGROUND
can help us.
We restrict the event receiver to receive this event from a specific process and then check if the text of the window that raises the event is equal Page Setup
, then we can say that the window Page Setup
in the target process is open, otherwise we can say that the dialog is Page Setup
not open.
For the sake of simplicity, in the example below, I've assumed the instance is notepad
open when the application starts, but you can also use Win32_ProcessStartTrace
to detect when the application starts notepad
.
To be more specific and tell when the dialog is closed, you can listen to EVENT_OBJECT_DESTROY
and determine if the message is for the window of interest.
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 Hans pointed out in the comments, you can use the UI Automation APIs to subscribe to WindowOpenedEvent
and WindowClosedEvent
.
In the example below, I assumed there was an open instance notepad
and found its dialog opening and closing Page Setup
:
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 assembly link UIAutomationClient
and UIAutomationTypes
and add using System.Windows.Automation;
.
source to share
For this I have to use the import of user32.dll.
First, for your purposes, make sure you have:
using System.Runtime.InteropServices;
using System.Linq;
Then in your class at the top, paste this code to import methods from the DLL.
[DllImport("user32.dll", SetLastError = true)]
static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[DllImport("user32.dll")]
public static extern IntPtr GetParent(IntPtr hWnd);
Now, in your own method, the following code should work:
var _notepadProcess = System.Diagnostics.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 )
{
//We found our Page Setup window, and it belongs to Notepad.exe - yay!
}
}
This should get you started.
***** EDIT ******
So there you have it:
mew.EventArrived += (sender, args) => { AppStarted(); };
This will ensure that the AppStarted () method runs when the notepad.exe process starts.
Then you wait 300ms (for some reason ?!) and then you call PopInFront:
async void AppStarted()
{
await Task.Delay(300);
BeginInvoke(new System.Action(PoPInFront));
}
Inside PopInFront () you are trying to find the Page Setup window
var _windowHandle = FindWindow(null, "Page Setup");
However my request is here: in the 0.3 seconds that have passed, can you say that you were able to open notepad, wait for the GUI to start and go to File -> Page Setup in .3 second for the next code region to find a window? - I think not, what you need is a cycle.
What should you do:
- WMI query event completed
- Start a background worker with a while loop that loops while the notepad.exe process is alive
- In the while loop, keep checking the Page Setup window
- After it detects your own dialog popup, mark another variable to keep track of the display of your dialog.
- After the page setup dialog is no longer displayed (FindWindow returns zero), re-check variale in step 4 and run the page setup window again.
source to share