Bring Powershell-Console to front from WinForms

Viewed 2167

im trying to bring my powershell console to front, even if it is minimized. I found following code:

function Show-Process($Process, [Switch]$Maximize)
{
  $sig = '
    [DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);
    [DllImport("user32.dll")] public static extern int SetForegroundWindow(IntPtr hwnd);
  '

  if ($Maximize) { $Mode = 3 } else { $Mode = 4 }
  $type = Add-Type -MemberDefinition $sig -Name WindowAPI -PassThru
  $hwnd = $process.MainWindowHandle
  $null = $type::ShowWindowAsync($hwnd, $Mode)
  $null = $type::SetForegroundWindow($hwnd) 
}
Show-Process -Process (Get-Process -Id $pid) 

It works fine, but when i call the function from a Button Click event, the console wont show. What is the Problem? Is there a way to bring the powershell Console to front when using a WinForms GUI?

Here is the example GUI Code:

function Show-Process($Process, [Switch]$Maximize)
{
  $sig = '
    [DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);
    [DllImport("user32.dll")] public static extern int SetForegroundWindow(IntPtr hwnd);
  '

  if ($Maximize) { $Mode = 3 } else { $Mode = 4 }
  $type = Add-Type -MemberDefinition $sig -Name WindowAPI -PassThru
  $hwnd = $process.MainWindowHandle
  $null = $type::ShowWindowAsync($hwnd, $Mode)
  $null = $type::SetForegroundWindow($hwnd) 
}

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

$Button1                         = New-Object system.Windows.Forms.Button
$Button1.text                    = "button"
$Button1.width                   = 60
$Button1.height                  = 30
$Button1.location                = New-Object System.Drawing.Point(75,29)
$Button1.Font                    = 'Microsoft Sans Serif,10'

$Button1.Add_Click({
    Show-Process -Process (Get-Process -Id $pid) 
})

$Form.controls.AddRange(@($Button1))

[void]$Form.ShowDialog()
3 Answers

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 $pid
Show-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 -PassThru

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

Related