如何检查后台作业是否已完成但在 PowerShell 中保持 Windows 表单响应

How to check if a background job completed but keep the Windows form responsive in PowerShell

我创建了一个 Windows 表单,您可以在其中单击一个按钮来启动大约 15 分钟的备份过程(使用 Start-Job)。 我使用 Start-Job 是为了在备份过程中保持表单响应(所谓响应,我的意思是您可以移动它、最小化它等等)。 但是,我希望表单在作业完成后弹出一个消息框,但我无法获得正确的结果。

起初我尝试了一个 While 循环,每 10 秒检查一次作业是否完成:

$BackupButton.Add_Click( {

        $BackupJob = Start-Job -ScriptBlock { ... }
        $Completed = $false
        while (!($Completed)) {
            if ($BackupJob.State -ne "Running") {
                $Completed = $true
            }
            Start-Sleep -Seconds 10
        }
        [System.Windows.Forms.MessageBox]::Show('Successfully completed the backup process.', 'Backup Tool', 'OK', 'Info')
    })

这在作业完成后给了我消息框,但在此过程中表单没有响应,可能是因为它仍在使用 While 循环的线程资源。

然后,我尝试使用 Register-ObjectEvent 来调用消息框以显示作业状态何时更改:

$BackupButton.Add_Click( {

        $BackupJob = Start-Job -ScriptBlock { ... }
        Register-ObjectEvent $BackupJob StateChanged -Action {
            [System.Windows.Forms.MessageBox]::Show('Successfully completed the backup process.', 'Backup Tool', 'OK', 'Info')
        }
    })

此选项确实使表单在此过程中保持响应,但消息框(事件的操作块)从未启动,直到我关闭 Windows 表单。

是否有任何选项既可以使消息框按时出现(不是在表单关闭时)又不使用表单的线程(保持响应)?

编辑: 或者,有没有办法从后台作业控制我的表单?我试图将表单的 buttons/controls 作为参数发送给作业,然后从作业中控制表单的事件,但没有成功。 如果有办法从后台作业以某种方式访问​​表单,这也将解决我的问题。

提前致谢。

Start-Sleep cmdlet 使您的表单无响应。为了克服这个问题,请改用 System.Windows.Forms.Timer 对象。

类似于:

$timer = New-Object System.Windows.Forms.Timer
$timer.Interval = 1000   # for demo 1 second
$timer.Enabled = $false  # disabled at first
$timer.Add_Tick({
    # check every 'Interval' milliseconds to see if the backup job is still running
    # if not, stop the timer (this will set the Enabled property to $false)
    if ($script:BackupJob.State -ne "Running") { $timer.Stop() }
})

$BackupButton = New-Object System.Windows.Forms.Button
$BackupButton.Anchor = 'Top','Left'
$BackupButton.Size = [System.Drawing.Size]::new(120, 31)
$BackupButton.Location = [System.Drawing.Point]::new(($form.Width - $BackupButton.Width) / 2, 150)
$BackupButton.Text = 'Start Backup'

$BackupButton.Add_Click( {
    Write-Host "Job started"
    $this.Enabled = $false  # disable the button, to prevent multiple clicks

    # use the script: scope, otherwise the timer event will not have access to it
    # for demo, the job does nothing but wait..
    $script:BackupJob = Start-Job -ScriptBlock { Start-Sleep -Seconds 5 }
    $timer.Start()
    while ($timer.Enabled) {
        [System.Windows.Forms.Application]::DoEvents()
    }
    Write-Host "Job ended"
    # show the messagebox
    [System.Windows.Forms.MessageBox]::Show('Successfully completed the backup process.', 'Backup Tool', 'OK', 'Info')

    # and enable the button again
    $this.Enabled = $true
})

希望对您有所帮助

解释了您的方法存在的问题并展示了一个有效的解决方案(您自己对此进行了改进,注意到实际上不需要使用 [System.Windows.Forms.Timer] 实例)。

如图所示,为了在执行其他任务时保持表单响应,您必须在循环中不断调用 [System.Windows.Forms.Application]::DoEvents(),并且该循环的主体必须整体快速执行;换句话说:您只能在循环 .

内执行 轮询 类活动
  • 注意:[System.Windows.Forms.Application]::DoEvents() can be problematic in general (it is essentially what the blocking .ShowDialog() call does behind the scenes), but in this constrained scenario (assuming only one form is to be shown) it should be fine. See this answer 了解背景信息。

运行 在给定控件的事件处理程序 中的 DoEvents() 循环的替代方法是显示形式 non-modally - 使用 .Show() 而不是 .ShowDialog() - 然后将 DoEvents() 循环 直接放在 此调用之后, 在脚本的主要范围内.

这种方法的优点是:

  • 您只需要一个 单个 DoEvents 循环,将其放置在主脚本范围内也有助于使整体控制流程更加清晰。

  • 这会启用可重入作业创建(如果您希望允许在 运行 备份完成之前启动另一个备份)并且通常使通过多个控件创建并发后台作业更容易,由于只需要一个循环来监视所有需要的循环。

这是一个独立示例(需要 PowerShell 版本 5 或更高版本);它创建一个带有单个 Start Job 按钮和反映作业状态的状态标签的表单:

  • 初始状态:

  • While 运行(单击 Start Job 后;表单在此状态下仍然响应 - 例如,您可以移动它,如果有其他(启用) 控件,您可以单击它们):

  • 已完成:

# PSv5+
using namespace System.Windows.Forms
using namespace System.Drawing

Add-Type -AssemblyName System.Windows.Forms

# Create a sample form.
$form = [Form] @{ 
  Text = 'Form with Background Job'
  ClientSize = [Point]::new(200, 80)
  FormBorderStyle = 'FixedToolWindow'
}

# Create the controls and add them to the form.
$form.Controls.AddRange(@(

    ($btnStartJob = [Button] @{
        Text     = "Start Job"
        Location = [Point]::new(10, 10)
      })
    
    [Label] @{
      Text     = "Status:"
      AutoSize = $true
      Location = [Point]::new(10, 40)
      Font     = [Font]::new('Microsoft Sans Serif', 10)
    }

    ($lblStatus = [Label] @{
        Text     = "(Not started)"
        AutoSize = $true
        Location = [Point]::new(80, 40)
        Font     = [Font]::new('Microsoft Sans Serif', 10)
      })

  ))

# The script-level variable that receives the job-info
# object when the user clicks the job-starting button. 
$job = $null

# Add an event handler to the button that starts 
# the background job.
$btnStartJob.add_Click( {
    $this.Enabled = $false # To prevent re-entry while the job is still running.
    # Signal the status.
    $lblStatus.Text = 'Running...'
    $form.Refresh() # Update the UI.
    # Start the job, and store the job-info object in 
    # the *script-level* $job variable.
    # The sample job simply sleeps for 3 seconds.
    $script:job = Start-Job { Start-Sleep -Seconds 3 }
  })

# Show the form *non*-modally.
# That is, this statement is *not* blocking and execution continues below.
$form.Show()

# While the form is visible, process events.
while ($form.Visible) {
  [Application]::DoEvents() # Process form (UI) events.
  # Check if the job has terminated, for whatever reason.
  if ($job.State -in 'Completed', 'Failed', 'Stopped', 'Suspended', 'Disconnected') {
    # Show the termination state in the label.
    $lblStatus.Text = $job.State
    # Re-enable the button.
    $btnStartJob.Enabled = $true
  }
}