在 Powershell 中,成功复制文件后如何更改标签文本?

Tec*_*hno 2 powershell winforms

我开始构建一个带有“复制”按钮和该按钮下的标签的小型 winform。\n当我单击“复制”按钮时,它开始将文件从源复制到目标。\n我想异步运行此操作,因此我不希望表单复制操作运行时被冻结。这就是我使用 Job 的原因。成功复制后,我需要复制反馈并显示绿色的“OK”文本,但它不起作用。

\n

这是我的代码:

\n
[void][System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")\n[System.Windows.Forms.Application]::EnableVisualStyles()\n\nFunction Copy-Action{\n\n    $Computername = "testclient"\n\n    $Source_Path = "C:\\temp\\"\n    $Destination_Path = "\\\\$Computername\\c$\\temp"\n\n\n    $job = Start-Job -Name "Copy" -ArgumentList $Source_Path,$Destination_Path \xe2\x80\x93ScriptBlock {\n           param($Source_Path,$Destination_Path) \n                    \n           Copy-Item $Source_Path -Destination $Destination_Path -Recurse -Force\n            \n            } \n \n    Register-ObjectEvent $job StateChanged -MessageData $Status_Label -Action {\n        [Console]::Beep(1000,500)\n        $Status_Label.Text = "OK"\n        $Status_Label.ForeColor = "#009900"\n        $eventSubscriber | Unregister-Event\n        $eventSubscriber.Action | Remove-Job\n        } | Out-Null\n}\n\n# DRAW FORM\n$form_MainForm = New-Object System.Windows.Forms.Form\n$form_MainForm.Text = "Test Copy"\n$form_MainForm.Size = New-Object System.Drawing.Size(200,200)\n$form_MainForm.FormBorderStyle = "FixedDialog"\n$form_MainForm.StartPosition = "CenterScreen"\n$form_MainForm.MaximizeBox = $false\n$form_MainForm.MinimizeBox = $true\n$form_MainForm.ControlBox = $true\n\n# Copy Button\n$Copy_Button = New-Object System.Windows.Forms.Button\n$Copy_Button.Location = "50,50"\n$Copy_Button.Size = "75,30"\n$Copy_Button.Text = "Copy"\n$Copy_Button.Add_Click({Copy-Action})\n$form_MainForm.Controls.Add($Copy_Button)\n\n# Status Label\n$Status_Label = New-Object System.Windows.Forms.Label\n$Status_Label.Text = ""\n$Status_Label.AutoSize = $true\n$Status_Label.Location = "75,110"\n$Status_Label.ForeColor = "black"\n$form_MainForm.Controls.Add($Status_Label)\n\n#show form\n$form_MainForm.Add_Shown({$form_MainForm.Activate()})\n[void] $form_MainForm.ShowDialog()\n
Run Code Online (Sandbox Code Playgroud)\n

复制成功,但不会显示“OK”标签。我已经设置了蜂鸣声,但它也不起作用。\n我做错了什么?有什么解决办法吗?\n谢谢。

\n

mkl*_*nt0 5

让我提供balrundel 有用的解决方案的替代方案- 这是有效的,但很复杂。

核心问题是,当表单以模态方式显示时,.ShowDialog()WinForms 控制着前台线程,而不是 PowerShell

也就是说,PowerShell 代码 - 在表单的事件处理程序中 - 仅响应用户操作Register-ObjectEvent而执行,这就是为什么传递给的参数的作业状态更改事件处理-Action程序不会触发(它最终会在关闭表单后触发) 。

两个基本解决方案

  • 坚持.ShowDialog()不同的 PowerShell 运行空间(线程)中并行执行操作

    • balrundel 的解决方案使用PowerShell SDK来实现这一点,不幸的是,它的使用绝非微不足道。

    • 请参阅下面的更简单的替代方案Start-ThreadJob

  • 通过方法以非模式方式显示表单.Show(),并进入一个循环,您可以在其中执行其他操作,同时定期调用[System.Windows.Forms.Application]::DoEvents()以保持表单响应。

    • 有关此技术的示例,请参阅此答案。

    • 混合方法是坚持并在表单事件处理程序.ShowDialog()进入[System.Windows.Forms.Application]::DoEvents()循环。

      • 这最好限于应用此技术的单个事件处理程序,因为使用额外的同时[System.Windows.Forms.Application]::DoEvents()循环会带来麻烦。
      • 有关此技术的示例,请参阅此答案。

更简单的Start-ThreadJob基于 - 的解决方案

  • Start-ThreadJob是该模块的一部分,该ThreadJob模块为基于子进程的常规后台作业提供了一种轻量级的、基于线程的替代方案,并且也是通过 PowerShell SDK 创建运行空间的更方便的替代方案

    • 附带PowerShell (Core) 7+,可以根据需要在Windows PowerShell中安装,例如Install-Module ThreadJob -Scope CurrentUser.
    • 在大多数情况下,线程作业是更好的选择,无论是性能还是类型保真度 - 请参阅此答案的底部部分了解原因。
  • 除了语法上的便利之外,Start-ThreadJob由于它是基于线程的(而不是使用子进程,这就是Start-Job它的作用),因此允许操作调用线程的活动对象。

    • 请注意,为了简洁起见,下面的示例代码没有执行显式线程同步,而这在某些情况下可能是需要的。

以下简化的独立示例代码演示了该技术:

  • 该示例显示了一个带有按钮的简单表单,该按钮启动线程作业,并在操作(通过 3 秒睡眠模拟)完成后从该线程作业内部更新表单,如以下屏幕截图所示:

    • 初始状态:
      • 开始状态
    • 按下后Start Job(表单保持响应):
      • 运行状态
    • 作业结束后:
      • 结束状态
  • 事件处理程序.add_Click()包含解决方案的核心内容;希望源代码注释能够提供足够的文档。

# 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 Thread 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 helper variable that maintains a collection of
# thread-job objects created in event-handler script blocks,
# which must be cleaned up after the form closes.
$script:jobs = @()

# 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 thread job, and add the job-info object to 
    # the *script-level* $jobs collection.
    # The sample job simply sleeps for 3 seconds to simulate a long-running operation.
    # Note:
    #  * The $using: scope is required to access objects in the caller's thread.
    #  * In this simple case you don't need to maintain a *collection* of jobs -
    #    you could simply discard the previous job, if any, and start a new one,
    #    so that only one job object is ever maintained.
    $script:jobs += Start-ThreadJob { 
      # Perform the long-running operation.
      Start-Sleep -Seconds 3 
      # Update the status label and re-enable the button.
      ($using:lblStatus).Text = 'Done'
      ($using:btnStartJob).Enabled = $true 
    }
  })

$form.ShowDialog()

# Clean up the collection of jobs.
$script:jobs | Remove-Job -Force
Run Code Online (Sandbox Code Playgroud)