英文:
Modeless dialog cannot be operated from taskbar (no SC_MINIMIZE message)
问题
如果我通过$WPFdialog.Show()
在我的PowerShell脚本中打开一个非模态对话框,窗口会显示但不会响应任务栏图标上的点击。我希望能够最小化并从那里还原。如果我通过ShowDialog()
打开它,就没有问题。
我不想使用模态对话框,因为我需要GUI线程来检查后台任务的活动。我打算在后台活动处理旁边调用DoEvents()
。这在调整大小或关闭窗口时运行良好,但对话框不响应任务栏上的点击。实际上,它似乎根本没有接收到任务栏的WM_SYSCOMMAND SC_MINIMIZE消息,所以我不知道在哪里添加适当的事件处理程序。
感谢帮助。我在Windows 7上使用PowerShell 5.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
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)
答案1
得分: 2
<!-- language-all: sh -->
你自己找到了一个务实的解决方案:
-
即使你正在创建一个 WPF 窗口,你也在循环中使用与 WinForms 相关的
[System.Windows.Forms.Application]::DoEvents()
方法,以保持窗口响应,因为 WPF 没有等价的方法。 -
然而,为了让这种方法的事件处理 完全 生效 - 特别是对于通过任务栏重新激活窗口 - 你 必须使用
[System.Threading.Thread]::Sleep()
而不是Start-Sleep
。- 原因不明确;GitHub issue #19796 讨论了意外的行为。
# 载入 WinForms 和 WPF 组件。
Add-Type -AssemblyName System.Windows.Forms, PresentationFramework
# ...
# 显示非模态窗口并激活它。
$null = $WPFdialog.Show(); $null = $WPFDialog.Activate()
# 循环中定期处理 GUI 事件,其他操作在中间进行。
while ($WPFdialog.IsVisible) {
# 处理 GUI 事件。
# 即使这是一个 *WinForms* 方法,
# 它也适用于 *WPF* 应用程序。
[System.Windows.Forms.Application]::DoEvents()
# 计划:在这里处理一些后台进度
# 注意:
# 这里的任何操作必须迅速完成,
# 否则会阻塞 GUI 事件处理。
# 稍微睡一会,避免创建一个紧密的循环。
# 重要提示:对于*所有*事件的处理,特别是
# 通过任务栏重新激活最小化窗口,使用
# [System.Threading.Thread]::Sleep() 而不是 Start-Sleep
[System.Threading.Thread]::Sleep(20)
}
'完成。'
然而,以上方法需要加载 WinForms 组件 - 虽然这在实践中可能不是问题。
阅读下面的内容以了解纯 WPF 解决方案。
有可能实现纯粹的 WPF 解决方案,即通过一个自定义实现类似于 WinForms 的 DoEvents()
的 WPF 版本,如下所示在 DispatcherFrame
类的帮助主题中所示,并在下面实现。
类似地,必顅使用 [System.Threading.Thread]::Sleep()
而不是 Start-Sleep
。
# 现在只需要 WPF。
Add-Type -AssemblyName PresentationFramework
# ...
# 定义一个类似 DoEvents() 的自定义函数,用于处理 GUI WPF 事件,并可以在前台线程的自定义事件循环中调用,如下所示。
# 改编自: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) {
# 调用自定义的 WPF DoEvents 函数
DoWpfEvents
# 计划:在这里处理一些后台进度
# 重要提示:使用 [System.Threading.Thread]::Sleep() 而不是 Start-Sleep
[Threading.Thread]::Sleep(20)
}
注意:你报告了在 Win 7 SP1 上出现的 重绘问题,使用的是 Windows PowerShell (.NET 4.8.03761);请参见动画截图中的复制步骤。
与此相反,我在我的 Windows 11 22H2 (ARM) VM 上没有看到这个问题,无论是在 Windows PowerShell v5.1.22621.1778 (.NET Framework 4.8.9166.0) 还是 PowerShell Core v7.4.0-preview.3 (.NET 8.0.0-preview.3.23174.8) 中都没有看到这个问题:
如果窗口是不活动的并且在前景窗口后面,
DoWpfEvents
无法调度WM_PAINT
消息,因此当前景窗口关闭时(仍然不活动),对话框需要重绘。DoEvents
似乎处理得很好。
(由我,Yacc,补充)这就是在 Win 7 上使用 DoWpfEvents 进行调度的样子。DoEvents 正常工作。唯一的区别是 WM_PAINT (消息代码 15) 消失了。
英文:
<!-- language-all: sh -->
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 thanStart-Sleep
.- The reasons are unclear; GitHub issue #19796 discusses the unexpected behavior.
# 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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论