问题描述
我正在创建一个带有 GUI 的 powershell 脚本,它将用户配置文件从选定的源磁盘复制到目标磁盘。我已经使用 VS Community 2019 在 XAML 中创建了 GUI。 该脚本的工作方式如下:您选择源磁盘、目标磁盘、用户配置文件和要复制的文件夹。当您按下“开始”按钮时,它会调用一个名为 Backup_data 的函数,在其中创建一个运行空间。在此运行空间中,只有一小部分copy-Item, 将您选择的内容作为参数。
脚本工作正常,所有想要的项目都被正确复制。问题是 GUI 在复制过程中冻结(没有“无响应”消息或其他任何消息,它只是完全冻结;无法点击任何地方,无法移动窗口)。我已经看到使用运行空间可以解决这个问题,但对我来说不是。我错过了什么吗?
这是函数Backup_Data
:
Function BackupData {
##CREATE RUNSPACE
$PowerShell = [powershell]::Create()
[void]$PowerShell.AddScript( {
Param ($global:ReturneddiskSource,$global:SelectedUser,$global:SelectedFolders,$global:ReturneddiskDestination)
##SCRIPT BLOCK
foreach ($item in $global:SelectedFolders) {
copy-Item -Path "$global:ReturneddiskSource\Users\$global:SelectedUser\$item" -Destination "$global:ReturneddiskDestination\Users\$global:SelectedUser\$item" -Force -Recurse
}
}).AddArgument($global:ReturneddiskSource).AddArgument($global:SelectedUser).AddArgument($global:SelectedFolders).AddArgument($global:ReturneddiskDestination)
#Invoke the command
$PowerShell.Invoke()
$PowerShell.dispose()
}
解决方法
PowerShell SDK 的PowerShell.Invoke()
方法是同步,因此被设计为块,而其他运行空间(线程)中的脚本运行。
您必须改用异步 PowerShell.BeginInvoke()
方法。
简单示例图片中没有 WPF(有关 WPF 解决方案,请参阅底部部分):
$ps = [powershell]::Create()
# Add the script and invoke it *asynchronously*
$asyncResult = $ps.AddScript({ Start-Sleep 3; 'done' }).BeginInvoke()
# Wait in a loop and check periodically if the script has completed.
Write-Host -NoNewline 'Doing other things..'
while (-not $asyncResult.IsCompleted) {
Write-Host -NoNewline .
Start-Sleep 1
}
Write-Host
# Get the script's success output.
"result: " + $ps.EndInvoke($asyncResult)
$ps.Dispose()
请注意,使用 PowerShell SDK 有一个更简单的替代方法:ThreadJob
模块的 Start-ThreadJob
cmdlet,一个线程基于 em> 的替代基于 子进程 的常规后台作业,以 Start-Job
开头,与所有其他 *-Job
cmdlet 兼容。
Start-ThreadJob
随 PowerShell [Core] 7+ 一起提供,可以从 Windows PowerShell 中的 PowerShell Gallery 安装({{1} }).
Install-Module ThreadJob
完整示例使用 WPF:
如果在您的情况下,代码需要从附加到 WPF 窗口中的控件的事件处理程序运行,则需要做更多的工作,因为 Start-Sleep
可以不 被使用,因为它阻止处理 GUI 事件并因此冻结窗口。
与 WinForms 不同,它具有用于按需处理挂起 GUI 事件的内置方法([System.Windows.Forms.Application]::DoEvents()
,WPF 没有 等效方法,但它可以手动添加,如DispatcherFrame
documentation中所示。
以下示例:
-
创建一个带有两个后台操作启动按钮和相应状态文本框的窗口。
-
使用按钮点击事件处理程序通过
# Requires module ThreadJob (preinstalled in v6+) # Start the thread job,always asynchronously. $threadJob = Start-ThreadJob { Start-Sleep 3; 'done' } # Wait in a loop and check periodically if the job has terminated. Write-Host -NoNewline 'Doing other things..' while ($threadJob.State -notin 'Completed','Failed') { Write-Host -NoNewline . Start-Sleep 1 } Write-Host # Get the job's success output. "result: " + ($threadJob | Receive-Job -Wait -AutoRemoveJob)
启动后台操作:注意:
Start-ThreadJob
也可以,但它会在子进程中运行代码,而不是在线程中运行代码,后者会慢得多并且有其他重要的后果。-
修改示例以使用 PowerShell SDK (
Start-Job
) 也不难,但线程作业更符合 PowerShell 习惯,并且更易于管理,通过常规的 {{ 1}} cmdlet。
-
以非模式显示WPF窗口并进入自定义事件循环:
-
一个自定义
[powershell]
类函数,*-Job
,改编自DispatcherFrame
documentation,在 GUI 事件处理的每个循环操作中都会被调用。- 注意:对于 WinForms 代码,您只需调用
DoEvents()
。
- 注意:对于 WinForms 代码,您只需调用
-
此外,后台线程作业的进度受到监控,接收到的输出会附加到作业特定的状态文本框。已完成的作业已清理完毕。
-
注意:就像您模态调用窗口(使用DoWpfEvents
)一样,前台线程和控制台会话是在窗口显示时被阻止。避免这种情况的最简单方法是在隐藏的子进程中运行代码;假设代码在脚本 [System.Windows.Forms.Application]::DoEvents()
中:
.ShowModal()
您也可以通过 SDK 执行此操作,这样速度会更快,但更加冗长和麻烦:wpfDemo.ps1
截图:
此示例屏幕截图显示了一个已完成的后台操作和一个正在进行的操作(支持并行运行它们);请注意启动正在进行的操作的按钮如何在操作期间被禁用,以防止重新进入:
源代码:
# In PowerShell [Core] 7+,use `pwsh` instead of `powershell`
Start-Process -WindowStyle Hidden powershell '-noprofile -file wpfDemo.ps1'
,
我一整天都在寻找解决方案,终于找到了,所以我会把它贴在那里,供遇到同样问题的人使用。
首先,查看这篇文章:https://smsagent.blog/2015/09/07/powershell-tip-utilizing-runspaces-for-responsive-wpf-gui-applications/ 它得到了很好的解释,并向您展示了如何通过 WPF GUI 正确使用运行空间。你只需要用 $Synchhash.Window 替换你的 $Window 变量:
$syncHash = [hashtable]::Synchronized(@{})
$reader = (New-Object System.Xml.XmlNodeReader $xaml)
$syncHash.window = [Windows.Markup.XamlReader]::Load( $reader )
在您的代码中插入运行空间函数:
function RunspaceBackupData {
$Runspace = [runspacefactory]::CreateRunspace()
$Runspace.ApartmentState = "STA"
$Runspace.ThreadOptions = "ReuseThread"
$Runspace.Open()
$Runspace.SessionStateProxy.SetVariable("syncHash",$syncHash)
$Runspace.SessionStateProxy.SetVariable("SelectedFolders",$global:SelectedFolders)
$Runspace.SessionStateProxy.SetVariable("SelectedUser",$global:SelectedUser)
$Runspace.SessionStateProxy.SetVariable("ReturnedDiskSource",$global:ReturnedDiskSource)
$Runspace.SessionStateProxy.SetVariable("ReturnedDiskDestination",$global:ReturnedDiskDestination)
$code = {
foreach ($item in $global:SelectedFolders) {
copy-item -Path "$global:ReturnedDiskSource\Users\$global:SelectedUser\$item" -Destination "$global:ReturnedDiskDestination\Users\$global:SelectedUser\$item" -Force -Recurse
}
}
$PSinstance = [powershell]::Create().AddScript($Code)
$PSinstance.Runspace = $Runspace
$job = $PSinstance.BeginInvoke()
}
并使用您指定的参数在您想要的事件处理程序中调用它:
$var_btnStart.Add_Click( {
RunspaceBackupData -syncHash $syncHash -SelectedFolders $global:SelectedFolders -SelectedUser $global:SelectedUser -ReturnedDiskSource $global:ReturnedDiskSource -ReturnedDiskDestination $global:ReturnedDiskDestination
})
不要忘记结束你的运行空间:
$syncHash.window.ShowDialog()
$Runspace.Close()
$Runspace.Dispose()