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

In Powershell how can I change Label text after a successful copy files?

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

这是我的代码:

[void][System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
[System.Windows.Forms.Application]::EnableVisualStyles()

Function Copy-Action{

    $Computername = "testclient"

    $Source_Path = "C:\temp\"
    $Destination_Path = "\$Computername\c$\temp"


    $job = Start-Job -Name "Copy" -ArgumentList $Source_Path,$Destination_Path –ScriptBlock {
           param($Source_Path,$Destination_Path) 
                    
           Copy-Item $Source_Path -Destination $Destination_Path -Recurse -Force
            
            } 
 
    Register-ObjectEvent $job StateChanged -MessageData $Status_Label -Action {
        [Console]::Beep(1000,500)
        $Status_Label.Text = "OK"
        $Status_Label.ForeColor = "#009900"
        $eventSubscriber | Unregister-Event
        $eventSubscriber.Action | Remove-Job
        } | Out-Null
}

# DRAW FORM
$form_MainForm = New-Object System.Windows.Forms.Form
$form_MainForm.Text = "Test Copy"
$form_MainForm.Size = New-Object System.Drawing.Size(200,200)
$form_MainForm.FormBorderStyle = "FixedDialog"
$form_MainForm.StartPosition = "CenterScreen"
$form_MainForm.MaximizeBox = $false
$form_MainForm.MinimizeBox = $true
$form_MainForm.ControlBox = $true

# Copy Button
$Copy_Button = New-Object System.Windows.Forms.Button
$Copy_Button.Location = "50,50"
$Copy_Button.Size = "75,30"
$Copy_Button.Text = "Copy"
$Copy_Button.Add_Click({Copy-Action})
$form_MainForm.Controls.Add($Copy_Button)

# Status Label
$Status_Label = New-Object System.Windows.Forms.Label
$Status_Label.Text = ""
$Status_Label.AutoSize = $true
$Status_Label.Location = "75,110"
$Status_Label.ForeColor = "black"
$form_MainForm.Controls.Add($Status_Label)

#show form
$form_MainForm.Add_Shown({$form_MainForm.Activate()})
[void] $form_MainForm.ShowDialog()

复制成功但显示“OK”标签不成功。我发出了哔哔声,但它也不起作用。 我究竟做错了什么 ?对此有什么解决办法吗? 谢谢。

Start-Job 创建一个单独的进程,当您的表单准备好接收事件时,它无法监听工作事件。您需要创建一个新的运行空间,它能够同步线程和表单控制。

我改编了 this answer 中的代码。你可以在那里阅读更好的解释。

[void][System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
[System.Windows.Forms.Application]::EnableVisualStyles()

Function Copy-Action{

    $SyncHash = [hashtable]::Synchronized(@{TextBox = $Status_Label})
    $Runspace = [runspacefactory]::CreateRunspace()
    $Runspace.ThreadOptions = "UseNewThread"
    $Runspace.Open()
    $Runspace.SessionStateProxy.SetVariable("SyncHash", $SyncHash)          
    $Worker = [PowerShell]::Create().AddScript({
        $SyncHash.TextBox.Text = "Copying..."
        
        # Copy-Item
        $Computername = "testclient"

        $Source_Path = "C:\temp\"
        $Destination_Path = "\$Computername\c$\temp"

        Copy-Item $Source_Path -Destination $Destination_Path -Recurse -Force

        
        $SyncHash.TextBox.ForeColor = "#009900"
        $SyncHash.TextBox.Text = "OK"

    })
    $Worker.Runspace = $Runspace
    $Worker.BeginInvoke()

}

# DRAW FORM
$form_MainForm = New-Object System.Windows.Forms.Form
$form_MainForm.Text = "Test Copy"
$form_MainForm.Size = New-Object System.Drawing.Size(200,200)
$form_MainForm.FormBorderStyle = "FixedDialog"
$form_MainForm.StartPosition = "CenterScreen"
$form_MainForm.MaximizeBox = $false
$form_MainForm.MinimizeBox = $true
$form_MainForm.ControlBox = $true

# Copy Button
$Copy_Button = New-Object System.Windows.Forms.Button
$Copy_Button.Location = "50,50"
$Copy_Button.Size = "75,30"
$Copy_Button.Text = "Copy"
$Copy_Button.Add_Click({Copy-Action})
$form_MainForm.Controls.Add($Copy_Button)

# Status Label
$Status_Label = New-Object System.Windows.Forms.Label
$Status_Label.Text = ""
$Status_Label.AutoSize = $true
$Status_Label.Location = "75,110"
$Status_Label.ForeColor = "black"
$form_MainForm.Controls.Add($Status_Label)

#show form
$form_MainForm.Add_Shown({$form_MainForm.Activate()})
[void] $form_MainForm.ShowDialog()

让我提供 的替代方案 - 有效但复杂。

核心问题是 当窗体被显示时 模态.ShowDialog(),WinForms 控制前台线程,而不是PowerShell.

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

两个基本解决方案:

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

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

    • 请参阅下面的基于 Start-ThreadJob

      的更简单的替代方案
  • 通过.Show() method, and enter a loop in which you can perform other operations while periodically calling [System.Windows.Forms.Application]::DoEvents()非模态显示表格以保持表格响应。

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

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

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

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

  • Start-ThreadJobThreadJob module 的一部分,它提供了 轻量级、基于 线程 的替代方案基于子进程的常规后台作业,也是通过 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