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

hof1towb  于 2022-11-16  发布在  Shell
关注(0)|答案(2)|浏览(180)

我创建了一个Windows窗体,您可以在其中单击一个按钮来启动备份过程(使用Start-Job)大约15分钟。我使用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表单为止。
是否有任何选项既可以使消息框按时显示(不在表单关闭时显示)又可以不使用表单的线程(保持其响应性)?

**编辑:**或者,有没有办法从后台作业控制窗体?我尝试将窗体的按钮/控件作为参数发送给作业,然后从作业控制窗体的事件,但没有成功。如果有办法从后台作业访问窗体,也可以解决我的问题。

先谢谢你。

5t7ly7z5

5t7ly7z51#

Start-Sleep指令程式会使您的表单没有回应。若要克服这个问题,请改用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
})

希望能有所帮助

tzdcorbm

tzdcorbm2#

Theo's helpful answer解释了您的方法中存在的问题,并给出了一个有效的解决方案(您自己对此进行了改进,注意到实际上并不需要使用[System.Windows.Forms.Timer]示例)。
如图所示,为了在执行其他任务的同时保持窗体的响应性,必须在循环中不断调用[System.Windows.Forms.Application]::DoEvents(),并且该循环体必须快速执行;换句话说就是:您只能在循环内执行类似 * 轮询 * 的活动。

  • 注意:[System.Windows.Forms.Application]::DoEvents()通常会有问题(这基本上是阻塞.ShowDialog()调用在后台所做的),但在这种受限的场景中(假设只显示 * 一个 * 表单),它应该是好的。请参见this answer了解背景信息。
    从 * 给定控件的事件处理程序 * 内部运行DoEvents()循环的另一种方法是 * 非 -模态地-使用.Show()而不是.ShowDialog()-显示表单,然后将DoEvents()循环 * 直接放在 * 此调用之后, 在脚本的主作用域 * 中。

这种方法的优点是:

  • 您只需要一个 * 单个 * DoEvents循环,将其放置在主脚本作用域中也有助于使整个控制流更加清晰。
  • 这样可以创建可重新进入的作业(如果您希望在一个正在运行的备份完成之前启动另一个备份),并且通常可以更轻松地通过多个控制创建并发后台作业,因为只需要一个循环来监视所有这些作业。

下面是一个自包含的示例(需要PowerShell版本5或更高版本);它将创建一个表单,该表单具有一个Start Job按钮和一个反映作业状态的状态标签:

  • 初始状态:

  • 运行时(点击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
  }
}

有关此解决方案的变体持续收集作业的 * 输出将其显示在多行文本框中),请参见this answer

相关问题