PowerShell
PowerShell copied to clipboard
Start-Sleep - unlike System.Threading.Thread.Sleep - in a WinForms DoEvents loop prevents reactivating a minimized window
Prerequisites
- [X] Write a descriptive title.
- [X] Make sure you are able to repro it on the latest released version
- [X] Search the existing issues.
- [X] Refer to the FAQ.
- [X] Refer to Differences between Windows PowerShell 5.1 and PowerShell.
Steps to reproduce
Note:
- This is clearly not a common use case, but it would still be good to know the cause of the problem.
- WPF windows are equally affected.
- Using
[System.Threading.Thread]::Sleep()
in lieu ofStart-Sleep
makes the problem go away. - See below for additional details regarding what messages are missed.
Repro steps:
- Run the following code, which shows a WinForms form non-modally and then uses a
DoEvents()
loop with periodicStart-Sleep
calls to process GUI events until the form is closed:
Add-Type -AssemblyName System.Windows.Forms
# Create a sample form.
$form = [System.Windows.Forms.Form] @{
Text = "Sample"; Size = [System.Drawing.Size]::new(600, 200); StartPosition = 'CenterScreen'
}
# Show the form *non-modally*
$null = $form.Show()
while ($form.Visible) {
# Allow the form to process events.
[System.Windows.Forms.Application]::DoEvents()
# Sleep a little, to avoid a near-tight loop.
# !! THIS CAUSES THE PROBLEM.
# !! If you replace the Start-Sleep call with a call
# !! to [System.Threading.Thread]::Sleep(), the problem goes away.
Start-Sleep -Milliseconds 50
}
-
Minimize the form.
-
Hover over the PowerShell instance's taskbar button and try to reactivate the form by clicking on its thumbnail.
Expected behavior
The form should be restored and activated.
Actual behavior
Nothing happens (while you hover, the form is previewed in its original position after a brief delay, but clicking the thumbnail is ineffective in restoring and activating it; clicking on the thumbnail's close button does close the form).
Also, trying to Alt-Tab to the window doesn't activate it either.
Error details
No response
Environment data
PowerShell 7.4.0-preview.3 on Windows 11 22H2
Visuals
No response
First of all, thank you for sharing your experience. My experience is using timers to do this.
$timer1 = New-Object System.Windows.Forms.Timer
$timer1.Interval = 1000 * 3
$timer1.add_tick($timer1_code)
I can confirm this behavior for Windows 7 SP1, Powershell 5.1, .NET 4.8 (cannot test on newer OS right now). With Start-Sleep, the dialog misses the (WM_SYSCOMMAND, SC_MINIMIZE=0xF020) message that is sent if the taskbar icon is clicked while the dialog is open - likewise it misses the (WM_SYSCOMMAND, SC_RESTORE=0xF120) when the dialog was minimized, leaving it pretty inoperable in the taskbar. This does not happen when waiting with [Threading.Thread]::Sleep(20).
I also want to note that this behavior is independent from running the Winforms event handler or a WPF pendant.
It would be better to replace Windows.Forms with cross-platformed TUI-toolkit out of the box.
Thanks for the additional details, @lippmaje - I've edited the initial post to include some of them and added a link to your comment.
@kasini3000, yes, in addition to switching from Start-Sleep
to [System.Threading.Thread]::Sleep()
, using a System.Windows.Forms.Timer
instance combined with showing the form modally (.ShowDialog()
) is another way to avoid the problem, given that WinForms / WPF is then in full control of the message loop, I presume. (It does, however, require you to organize the PowerShell code to be run while the form is shown differently, given that the code then runs in a child scope.)
@mklement0
- You can't use System.Windows.Forms.Application.DoEvents if you don't run a System.Windows.Forms.Application instance
- DoEvents is not a recommanded API. See the caution on the remarks system.windows.forms.application.doevents and a google search indicate that it's a problematic API inherited from VBA.
- A lot of features in WinForm/WPF don't work without managing a two threads model (you need a separate runspace for the thread UI and a dispatcher to execute some actions from the main thread to the UI thread )
The Cmdlet Start-Sleep uses System.Threading.ManualResetEvent API instead of System.Threading.Thread.Sleep I can reproduce an issue between the method DoEvents and the ManualResetEvent class. It can also be resolved it by using a unique ManualResetEvent instance. (something that Start-Sleep is not doing I guess)
Not Working :
Add-Type -AssemblyName System.Windows.Forms
$form = [System.Windows.Forms.Form]@{
Text = 'Sample'
Size = [System.Drawing.Size]::new(600, 200)
StartPosition = 'CenterScreen'
}
[void] $form.Show()
while ($form.Visible) {
[void] [System.Windows.Forms.Application]::DoEvents()
$waitHandle = [System.Threading.ManualResetEvent]::new(<# initialState: #> $false)
[void] $waitHandle.WaitOne(<# millisecondsTimeout: #> 50, $true)
[void] $waitHandle.Set()
$waitHandle.Close()
}
Working :
Add-Type -AssemblyName System.Windows.Forms
$form = [System.Windows.Forms.Form]@{
Text = 'Sample'
Size = [System.Drawing.Size]::new(600, 200)
StartPosition = 'CenterScreen'
}
[void] $form.Show()
$waitHandle = [System.Threading.ManualResetEvent]::new(<# initialState: #> $false)
while ($form.Visible) {
[void] [System.Windows.Forms.Application]::DoEvents()
[void] $waitHandle.WaitOne(<# millisecondsTimeout: #> 50, $true)
[void] $waitHandle.Set()
}
$waitHandle.Close()
Working :
Add-Type -AssemblyName System.Windows.Forms
$form = [System.Windows.Forms.Form]@{
Text = 'Sample'
Size = [System.Drawing.Size]::new(600, 200)
StartPosition = 'CenterScreen'
}
[void] $form.Show()
while ($form.Visible) {
[void] [System.Windows.Forms.Application]::DoEvents()
if ($form.Focused) {
$waitHandle = [System.Threading.ManualResetEvent]::new(<# initialState: #> $false)
[void] $waitHandle.WaitOne(<# millisecondsTimeout: #> 50, $true)
[void] $waitHandle.Set()
$waitHandle.Close()
}
}
Working :
Add-Type -AssemblyName System.Windows.Forms
$form = [System.Windows.Forms.Form]@{
Text = 'Sample'
Size = [System.Drawing.Size]::new(600, 200)
StartPosition = 'CenterScreen'
}
[void] $form.Show()
while ($form.Visible) {
[void] [System.Windows.Forms.Application]::DoEvents()
$waitHandle = [System.Threading.ManualResetEvent]::new(<# initialState: #> $false)
if ($form.WindowState -ne 'Minimized') {
[void] $waitHandle.WaitOne(<# millisecondsTimeout: #> 50, $true)
}
[void] $waitHandle.Set()
$waitHandle.Close()
}