I've looked hard for any examples about .Net events revealing a change of the MainWindowTitle of a process (or a corresponding property).
For detecting changes to file content I can simply use
function Register-FileWatcherEvent {
param (
[string]$folder,
[string]$file,
[string]$SourceIdentifier = 'File_Changed'
)
$watcher = New-Object IO.FileSystemWatcher $folder, $file -Property @{
IncludeSubdirectories = $false
EnableRaisingEvents = $true
}
$watcher.NotifyFilter = 'LastWrite'
Register-ObjectEvent $watcher -EventName 'Changed' -SourceIdentifier $SourceIdentifier
}
But what object, properties and notifier should I use for registering an event for change of MainWindowTitle or a corresponding property?
I tried messing around with WinEventHooks (and I have no idea what I'm doing here ;)
Some event do get registered but it triggers on mouse over instead of title change...
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
public static class User32 {
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);
}
"@
$processId = (Get-Process -Name notepad).Id
$WinEventDelegate = [User32+WinEventDelegate]{
param (
$hWinEventHook,
$eventType,
$hwnd,
$idObject,
$idChild,
$dwEventThread,
$dwmsEventTime
)
if ($eventType -eq 0x800C) {
$windowTitle = Get-Process -Id $processId |
Select-Object -ExpandProperty MainWindowTitle
Write-Host "Window title changed to: $windowTitle"
}
}
$hWinEventHook = [User32]::SetWinEventHook(
0x800C,
0x800C,
[IntPtr]::Zero,
$WinEventDelegate,
$processId,
0,
0
)
$handler = {
param (
$sender,
$eventArgs
)
$windowTitle = Get-Process -Id $processId |
Select-Object -ExpandProperty MainWindowTitle
Write-Host "Window title changed to: $windowTitle"
}
Register-ObjectEvent -InputObject $null `
-EventName EventRecord `
-Action $handler `
-SourceIdentifier "WindowTitleChanged" `
-SupportEvent
# To unregister the event, use the following command:
# Unregister-Event -SourceIdentifier "WindowTitleChanged"
0x800C is EVENT_OBJECT_NAMECHANGE, but can be different kind of objects.
MS Learn - Event Constants (Winuser.h)
So I reckon I need to check what was changed at each event trigger?
An alternative answer that is similar to your own P/Invoke-based attempt involving ad hoc-compiled C# code using Add-Type, adapted from this C# answer:
The solution involves the SetWinEventHook WinAPI function and its EVENT_OBJECT_NAMECHANGE event
You'll pay a once-per-session compilation performance penalty.
After that, monitoring will be more responsive and less CPU-intensive than the timer-based solution in the other answer.
# Compile a helper class that uses the SetWinEventHook() WinAPI function
# to listen for name-change events, which includes window-title changes.
# Note that Add-Type -PassThru emits TWO types: the class and the delegate type.
# We don't need to reference the delegate type explicitly on the PowerShell side.
$helperClass, $null =
Add-Type -PassThru -ReferencedAssemblies System.Console, System.Windows.Forms @'
using System;
using System.Windows;
using System.Windows.Forms;
using System.Runtime.InteropServices;
public class NameChangeTracker
{
public delegate void WinEventDelegate(
IntPtr hWinEventHook, uint eventType,
IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime
);
[DllImport("user32.dll")]
static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr
hmodWinEventProc, WinEventDelegate lpfnWinEventProc, uint idProcess,
uint idThread, uint dwFlags);
[DllImport("user32.dll")]
static extern bool UnhookWinEvent(IntPtr hWinEventHook);
const uint EVENT_OBJECT_NAMECHANGE = 0x800C;
const uint WINEVENT_OUTOFCONTEXT = 0;
const uint WINEVENT_SKIPOWNPROCESS = 2;
public static IntPtr StartMonitoring(WinEventDelegate delegateProc)
{
// Listen for name change changes across all processes/threads on current desktop...
return SetWinEventHook(EVENT_OBJECT_NAMECHANGE, EVENT_OBJECT_NAMECHANGE, IntPtr.Zero,
delegateProc, 0, 0, WINEVENT_OUTOFCONTEXT);
}
public static void StopMonitoring(IntPtr hHook)
{
UnhookWinEvent(hHook);
}
}
'@
# Determine the process whose main window title to monitor.
$process = Get-Process Notepad
# Retrieve the process' current main-window title so as to *cache* it.
$null = $process.MainWindowTitle
# Define the event handler (delegate)
# *and store it in a variable*, so as to prevent it from
# getting garbage-collected prematurely.
$eventProc = {
param (
$hWinEventHook,
$eventType,
$hwnd,
$idObject,
$idChild,
$dwEventThread,
$dwmsEventTime
)
if ($hwnd -eq $process.MainWindowHandle) {
$oldTitle = $process.MainWindowTitle
$process.Refresh()
Write-Host @"
Window title of process '$($process.Name)' (PID $($process.ID)) changed:
From: $oldTitle
To : $($process.MainWindowTitle)
"@
}
}
# Start monitoring via the helper class.
$hHook = $helperClass::StartMonitoring($eventProc)
# Start a message loop, which is required for the event monitoring to work.
# Putting up a WinForms message box does that.
$null = [System.Windows.Forms.MessageBox]::Show(
"Monitoring for window-title changes of process '$($process.Name)' (PID $($process.ID))`nPress OK to stop"
)
# Stop monitoring.
$helperClass::StopMonitoring($hHook)
Note:
Because a message loop is required in order to receive events, the above code simply uses a call to [System.Windows.Forms.MessageBox]::Show(), which implicitly provides a message loop while the message box is being displayed.
A console-based mechanism for indefinite monitoring to be stopped at the user's discretion (unfortunately) requires a solution in which [System.Windows.Forms.Application]::DoEvents() is called in a loop, with intermittent, short sleep intervals to lessen the CPU load, which is the only way to keep PowerShell in control of when to exit; e.g., the following alternative to the [System.Windows.Forms.MessageBox]::Show() call above allows you to use Ctrl-C in the console window to terminate monitoring:
# Start a message loop.
# Note:
# Call [System.Windows.Forms.Application]::DoEvents()
# periodically, returning control to PowerShell in between,
# so that it gets a chance to handle Ctrl-C.
try {
while ($true) {
[System.Windows.Forms.Application]::DoEvents()
Start-Sleep -Milliseconds 100
}
}
finally {
# Stop monitoring.
$helperClass::StopMonitoring($hHook)
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With