1

If I open a modeless dialog from within my Powershell script via $WPFdialog.Show(), the window appears but doesn't react to clicks on the taskbar icon. I want it to be able to minimize and restore from there. If I open it via ShowDialog() there's no problem with that.

I do not want to use a modal dialog because I need the GUI thread to check for background task activities. My approach to this is to call DoEvents() next to the background activity handling. This works fine on resizing or closing the window, but the dialog doesn't react to clicks on the taskbar. In fact it doesn't seem to receive the WM_SYSCOMMAND SC_MINIMIZE message from the taskbar at all, so I have no idea where to add an appropriate event handler.

Help appreciated. I am on Windows 7 with Powershell 5.1

enter image description here

Sample script:

param( $ARG1, $ARG2, $ARG3, $ARG4 )
Add-Type -AssemblyName System.Windows.Forms
[void][System.Reflection.Assembly]::LoadWithPartialName('presentationframework')

################### XAML-Code ###################
$inputXML = @"
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="Test App" Height="200" Width="300" ResizeMode="CanResize" WindowStartupLocation="CenterScreen">
<Grid>
<Label HorizontalAlignment="Center" VerticalAlignment="Center">Welcome</Label>
<Button x:Name="Close" Content="Close" HorizontalAlignment="Center" Margin="0,101,0,0" VerticalAlignment="Top"/>
</Grid>
</Window>
"@

############### Parse and use XAML ###############
$inputXML = $inputXML -replace 'mc:Ignorable="d"','' -replace "x:N",'N' -replace '^<Win.*', '<Window'
[xml]$xaml = $inputXML
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$WPFdialog=[Windows.Markup.XamlReader]::Load( $reader )
$xaml.SelectNodes("//*[@Name]") | %{Set-Variable -Name "WPF$($_.Name)" -Value $WPFdialog.FindName($_.Name)}

$WPFclose.Add_Click({
    $WPFdialog.Close()
})

#$res = $WPFdialog.ShowDialog()
$res = $WPFdialog.Show()
do
{
    [System.Windows.Forms.Application]::DoEvents()
    # planned: handle some background progress here
    Start-Sleep -Milliseconds 20
    $hwnd = [Windows.PresentationSource]::FromVisual($WPFdialog)
}
while ($hwnd)
yacc
  • 2,915
  • 4
  • 19
  • 33
  • 1
    Replacing `Start-Sleep` with `[System.Threading.Thread]::Sleep(20)` did it. The window gets all required messages now. I found no alternative to DoEvents though I know it's WinForms. @mklement0 – yacc Jun 13 '23 at 13:54
  • Great find re `[System.Threading.Thread]::Sleep()`. I've posted an answer that shows the WinForms + WPF solution and a WPF-only solution - _both_ need to use `[System.Threading.Thread]::Sleep()` in lieu of `Start-Sleep` – mklement0 Jun 13 '23 at 14:35

1 Answers1

2

You found a pragmatic solution yourself:

  • Even though you're creating a WPF window, you're using the WinForms-related [System.Windows.Forms.Application]::DoEvents() method in a loop, so as to keep the window responsive, given that WPF has no equivalent method.

  • However, for the event processing to fully work with this approach - notably also for window re-activation via the taskbar - you must use [System.Threading.Thread]::Sleep() rather than Start-Sleep.

# Load both the WinForms and the WPF assemblies.
Add-Type -AssemblyName System.Windows.Forms, PresentationFramework

# ...

# Show the window non-modally and activate it.
$null = $WPFdialog.Show(); $null = $WPFDialog.Activate()

# Process GUI events periodically in a loop, with other operations in between.
while ($WPFdialog.IsVisible) {
    # Process GUI events.
    #  Even though this is a *WinForms* method, it 
    #  also works with *WPF* applications.
    [System.Windows.Forms.Application]::DoEvents()

    # Planned: handle some background progress here
    # Note:
    #  Any operations here must finish quickly,
    #  otherwise GUI event processing will be blocked.

    # Sleep a little to so as not to create a tight loop.
    # IMPORTANT: For handling of *all* events, notably
    #            reactivation of a minimized window via the taskbar, use
    #            [System.Threading.Thread]::Sleep() instead 
    #            of Start-Sleep
    [System.Threading.Thread]::Sleep(20)
}

'Done.'

The above comes at the expense of having to load the WinForms assemblies too - though this may not be a problem in practice.
Read on for a WPF-only solution.


A pure WPF solution is possible, namely via a custom implementation of a WPF analog to WinForms' DoEvents(), as shown in the help topic for the DispatcherFrame class and implemented below.

Similarly, [System.Threading.Thread]::Sleep() instead of Start-Sleep must be used too.

# Only WPF is needed now.
Add-Type -AssemblyName PresentationFramework

# ...

# Define a custom DoEvents()-like function that processes GUI WPF events and can be 
# called in a custom event loop in the foreground thread, as shown below.
# Adapted from: https://learn.microsoft.com/en-us/dotnet/api/system.windows.threading.dispatcherframe
function DoWpfEvents {
  [Windows.Threading.DispatcherFrame] $frame = [Windows.Threading.DispatcherFrame]::new($True)
  $null = [Windows.Threading.Dispatcher]::CurrentDispatcher.BeginInvoke(
    'Background', 
    [Windows.Threading.DispatcherOperationCallback] {
      param([object] $f)
      ($f -as [Windows.Threading.DispatcherFrame]).Continue = $false
      return $null
    }, 
    $frame)
  [Windows.Threading.Dispatcher]::PushFrame($frame)
}


$null = $WPFdialog.Show(); $null = $WPFDialog.Activate()
while ($WPFdialog.IsVisible) {

    # Call the custom WPF DoEvents function
    DoWpfEvents

    # Planned: handle some background progress here

    # IMPORTANT: Use [System.Threading.Thread]::Sleep() instead  of Start-Sleep
    [Threading.Thread]::Sleep(20)
}

Note: You report a repainting problem on Win 7 SP1, with Windows PowerShell (.NET 4.8.03761); see the repro steps in the animated screenshot below.
By contrast, I do not see this on my Windows 11 22H2 (ARM) VM, neither in Windows PowerShell v5.1.22621.1778 (.NET Framework 4.8.9166.0) nor in PowerShell Core v7.4.0-preview.3 (.NET 8.0.0-preview.3.23174.8):

DoWpfEvents fails to dispatch the WM_PAINT message if the window was inactive and behind a foreground window, so that when the foreground window gets closed the (still inactive!) dialog needs a redraw. DoEvents seems to handle this fine.


(Addendum by me, Yacc) This is how dispatching with DoWpfEvents looks over here on Win 7. DoEvents works fine. The only difference is WM_PAINT (message code 15) is missing. DoWpfEvents - no WM_PAINT on Win7

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Thanks, @yacc, good to see the symptom visualized; I definitely tried what you're showing, and I don't see the same symptom on my machine. I suggest we clean up our comments here. – mklement0 Jun 15 '23 at 19:16