Bring Powershell-Console to front from WinForms
Thanks to @iRon's answer, i was able to figure it out, how i want it.For anyone curious, the problem is, you only can get the consoles MainwindowHandle as long as ShowDialog wasn't called.So i save the console Handle in a variable and i use the Form_Shown event to get the Form WindowHandle, since Form_Load still returns the Console Handle.
$sig = '[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);[DllImport("user32.dll")] public static extern int SetForegroundWindow(IntPtr hwnd);'$type = Add-Type -MemberDefinition $sig -Name WindowAPI -PassThru[IntPtr]$handleConsole = (Get-Process -Id $pid).MainWindowHandle[void]$type::ShowWindowAsync($handleConsole, 4);[void]$type::SetForegroundWindow($handleConsole)Add-Type -AssemblyName System.Windows.Forms[System.Windows.Forms.Application]::EnableVisualStyles()$Form = New-Object system.Windows.Forms.Form$Form.ClientSize = '446,266'$Form.text = "Form"$Form.TopMost = $false$Form.Add_Shown({ $global:handleForm = (Get-Process -Id $pid).MainWindowHandle})$Button1 = New-Object system.Windows.Forms.Button$Button1.text = "Clone ad-USer"$Button1.width = 60$Button1.height = 30$Button1.location = New-Object System.Drawing.Point(75,29)$Button1.Font = 'Microsoft Sans Serif,10'$Button1.Add_Click({ [void]$type::ShowWindowAsync($handleConsole, 4);[void]$type::SetForegroundWindow($handleConsole) Read-Host -Prompt "Please Enter a Value" [void]$type::ShowWindowAsync($global:handleForm, 4);[void]$type::SetForegroundWindow($global:handleForm)})$Form.controls.AddRange(@($Button1))[void]$Form.ShowDialog()
Now, if i press the Button, to console pops up in front.After the User enter something into the Console, the Form comes to front again.
Unfortunately, I can't completely fix it, but maybe others might help you further based on my findings:
First of all, the process within the button click event is a different process space as where the parent PowerShell host runs in. This can be easily proven but revealing the $hwhd
with Write-Host $hwnd
in the Show-Process
function and also calling the Show-Process
function prior calling the ShowDialog
:
Show-Process -Process (Get-Process -Id $pid) [void]$Form.ShowDialog()
In other words: to fix this part, you will need to the catch the parent $Pid
from the PowerShell window first:
$Button1.Add_Click({ Show-Process -Process $MyProcess })$Form.controls.AddRange(@($Button1))$MyProcess = Get-Process -Id $pidShow-Process -Process $MyProcess[void]$Form.ShowDialog()
The above snippet works, but as soon I remove (or comment out) the line Show-Process -Process $MyProcess
(at the host level), it breaks again...
As you've discovered, .MainWindowHandle
is not a static property (from the linked docs; emphasis added):
The main window is the window opened by the process that currently has the focus [...]
Therefore, what the value of the current process' .MainWindowHandle
property changes from the console-window handle to the WinForms window while the form is being displayed.[1]
Caching the console-window handle before you display the form is definitely an option, but there's an easier way, given that you're already using Add-Member
with WinAPI P/Invoke declarations: The GetConsoleWindow()
WinAPI function always returns the current process' console-window handle.
Additionally, your $Forms
form instance has a .Handle
property, which directly returns the form's window handle - no (Get-Process -Id $pid).MainWindowHandle
call needed.
The following solution therefore needs no global or script-level variables and confines querying the window handles to the button-click event handler:
# P/Invoke signatures - note the addition of GetConsoleWindow():$sig = '[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);[DllImport("user32.dll")] public static extern int SetForegroundWindow(IntPtr hwnd);[DllImport("kernel32.dll")] public static extern IntPtr GetConsoleWindow();'$type = Add-Type -MemberDefinition $sig -Name WindowAPI -PassThruAdd-Type -AssemblyName System.Windows.Forms$Form = New-Object system.Windows.Forms.Form -Property @{ ClientSize = '446,266' text = "Form"}$Button1 = New-Object system.Windows.Forms.Button -Property @{ text = "Test" location = New-Object System.Drawing.Point(75, 29)}$Button1.Add_Click({ # Get this form's window handle. $handleForm = $Form.Handle # More generically: $this.FindForm().Handle # Get the console window's handle. $handleConsole = $type::GetConsoleWindow() # Activate the console window and prompt the user. $null = $type::ShowWindowAsync($handleConsole, 4); $null = $type::SetForegroundWindow($handleConsole) Read-Host -Prompt "Please Enter a Value" # Reactivate this form. $null = $type::ShowWindowAsync($handleForm, 4); $null = $type::SetForegroundWindow($handleForm) })$Form.controls.AddRange(@($Button1))$null = $Form.ShowDialog()
[1] Note that a cached process object doesn't dynamically update its .MainWindowHandle
value; you have to call .Refresh()
manually. Because iRon's solution caches the current-process object before displaying the form, it still happens to reflect the console-window handle inside the button-click handler.