Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Detect when a specific window in another process opens or closes

Tags:

c#

.net

winforms

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'

like image 413
John Pietrar Avatar asked Oct 25 '16 13:10

John Pietrar


2 Answers

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;.

like image 111
Reza Aghaei Avatar answered Oct 30 '22 02:10

Reza Aghaei


You need to use user32.dll imports for this i would say.

Firstly, in your usings ensure you have:

using System.Runtime.InteropServices;
using System.Linq;

Then, in your class at the top insert 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 ******

Okay so you have got this:

mew.EventArrived += (sender, args) => { AppStarted(); };

This will ensure that the AppStarted() method gets fired when the notepad.exe process has started.

You then wait for 300ms (for some reason?!) and then call PopInFront:

async void AppStarted()
{         
    await Task.Delay(300);
    BeginInvoke(new System.Action(PoPInFront));
}

Inside PopInFront() you attempt to find the "Page Setup" window

var _windowHandle = FindWindow(null, "Page Setup");

However, my query here is: within the 0.3 seconds that have passed, can you safly say that you have been able to open notepad, wait for the GUI to init and navigate to the File -> Page Setup menu in .3 of a second for the next code area to find the window? - My guess is not, what you need here is a loop.

What you should be doing is:

  1. WMI Query Event Fired
  2. Start a background worker with a while loop that loops while the process notepad.exe is alive
  3. In the while loop, keep checking for the Page Setup window
  4. Once it's found, popup your own dialog, mark another variable to keep track that your dialog is shown
  5. Once the page setup dialog is no longer shown (FindWindow will return zero), re-mark the variale in stage 4 and allow for the Page Setup window to be found again.
like image 26
Mark Phillips Avatar answered Oct 30 '22 01:10

Mark Phillips