从另一个运行空间向表单添加元素

Adding elements to a form from another runspace

我有一个表单,一旦准备好就会添加几个元素(例如,一个列表)。添加它们可能需要一些时间(从几分之一秒到几分钟)。因此,我想将处理添加到一个单独的线程(子线程)。元素的数量是事先不知道的(例如,文件夹中有多少文件),因此它们是在子流中创建的。当子流中的处理结束时,我想在主窗体上显示这些元素(之前窗体没有这些元素并执行其他任务)。

但是,我无法将这些元素从子流添加到主窗体中。我将举一个简单的例子作为例子。它确实有效:

$Main = New-Object System.Windows.Forms.Form

$Run = {

    # The form is busy while adding elements (buttons here)

    $Top = 0
    1..5 | % {

        $Button = New-Object System.Windows.Forms.Button
        $Button.Top = $Top        
        $Main.Controls.Add($Button)
        $Top += 30
        Sleep 1        
    }
}

$Main.Add_Shown($Run)

# Adding and performing other tasks on the form here

[void]$Main.ShowDialog()

但是,将相同的内容添加到子流后,我没有得到在主窗体上显示的按钮。我不明白为什么。

$Main = New-Object System.Windows.Forms.Form

$Run = {

    $RS = [Runspacefactory]::CreateRunspace()
    $RS.Open()
    $RS.SessionStateProxy.SetVariable('Main', $Main)
    $PS = [PowerShell]::Create().AddScript({

        # Many items will be added here. Their number and processing time are unknown in advance
        # Now an example with the addition of five buttons.

        $Top = 0        
        1..5 | % {

            $Button = New-Object System.Windows.Forms.Button
            $Button.Top = $Top        
            $Main.Controls.Add($Button)
            $Top += 30
            Sleep 1        
        }   
    })

    $PS.Runspace = $RS; $Null = $PS.BeginInvoke()
}

$Main.Add_Shown($Run)

[void]$Main.ShowDialog()

如何将在子流中创建的元素添加到主窗体?谢谢

把它放在这里,因为它对于常规评论部分来说太长了。

现在,我并没有花太多时间在生产中使用运行空间,因为在 class 中我对它们没有真正的需求(至少到目前为止),当然,但我离题了。

但是,根据我之前所有的阅读资料和我保留的笔记,这听起来像是 RunSpace 池的一个用例。这是我保存的三个资源。当然,在您可能没有看到所有这些的假设下工作。现在,我也会 post 他们的代码,但都很长,所以,就是这样。根据您的用例,它可以被视为与最后一个 link 资源的副本。

PowerShell and WPF: Writing Data to a UI From a Different Runspace

PowerShell Tip: Utilizing Runspaces for Responsive WPF GUI Applications

Sharing Variables and Live Objects Between PowerShell Runspaces

How to access a different powershell runspace without WPF-object

我认为您不能在不同的线程上创建表单和控件。但是您可以访问控件属性。因此,您可以在运行空间中创建带有控件占位符的表单,然后在计算完成后在主线程上更改它们。示例:

$form = New-Object System.Windows.Forms.Form

$rs = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
$rs.ApartmentState = [System.Threading.ApartmentState]::MTA
$ps = [powershell]::create()
$ps.Runspace = $rs 
$rs.Open()

$out = $ps.AddScript({param($form)

  $button1 = New-Object System.Windows.Forms.Button
  $button1.Name = "button1"
  $form.Controls.Add($button1) 
  $form.ShowDialog()

}).AddArgument($form).BeginInvoke()


#-----------------------------


sleep 1; 


$form.Controls["button1"].Text = "some button"

这就是我现在使用的方式。感谢 @mklement0 关于 Register-ObjectEvent 方法 () 的讨论。我在这里应用了它。该方法的本质是在子流中创建元素(在本例中为 Button),当子流 space 完成工作时,处理 Register-ObjectEventRegister-ObjectEvent 位于主 space 中,因此允许您向表单添加元素(此处为 Button)。

$Main = New-Object System.Windows.Forms.Form

$Run = {

    $RS = [Runspacefactory]::CreateRunspace()
    $RS.Open()
    $RS.SessionStateProxy.SetVariable('Main', $Main)
    $PS = [PowerShell]::Create().AddScript({        

            $Button = New-Object System.Windows.Forms.Button
        }   
    })

    $PS.Runspace = $RS

    $Null = Register-ObjectEvent -InputObject $PS -EventName InvocationStateChanged -Action {    
        if ($EventArgs.InvocationStateInfo.State -in 'Completed', 'Failed') {
            $Main.Controls.Add($Button)
        }
    }

    $Null = $PS.BeginInvoke()
}

$Main.Add_Shown($Run)

[void]$Main.ShowDialog()

这是我的解决方法。 但是,我仍然不知道原则上是否可以将子 space 的元素添加到子 space 的表单中。 这当然是, 是关于添加,而不是管理,因为从子 space 管理是成功的。

虽然您可以在线程 B 上创建 控件,但您不能将它们 添加到在线程 A 来自线程 B.

如果您尝试这样做,您将遇到以下异常:

Controls created on one thread cannot be parented to a control on a different thread.

Parenting to意味着在控件(窗体)上调用.Add().AddRange()方法将其他控件添加为子控件。

换句话说:为了向您的 $Main 表单添加控件,该表单创建并稍后显示在 原始 线程(PowerShell运行空间),$Main.Controls.Add() 调用 必须 发生在同一个线程中。

同样,您应该始终在同一个线程中附加事件委托(事件处理程序脚本块)。

虽然 尝试确保将按钮添加到原始运行空间中的表单,但它并没有像写的那样工作 - 请参阅底部。

我建议一个更简单的方法:

  • 使用 线程作业 通过 Start-ThreadJob.

    在后台创建控件
    • Start-ThreadJobThreadJob module 的一部分,它提供了一个轻量级的、基于 线程 的替代基于子进程的常规后台作业和也是通过 PowerShell SDK 创建运行空间的更方便的替代方法。 它随 PowerShell [Core] v6+ 一起提供,在 Windows 中,PowerShell 可以按需安装,例如 Install-Module ThreadJob -Scope CurrentUser。 在大多数情况下,线程作业是更好的选择,无论是性能还是类型保真度 - 请参阅 的底部部分了解原因。
  • 显示你的表单非模态.Show()而不是.ShowDialog())并在[=22=中处理GUI事件]循环。

    • 注意:[System.Windows.Forms.Application]::DoEvents() 通常可能会出现问题(本质上是阻塞 .ShowDialog() 调用在幕后所做的事情),但在这种受限情况下(假设只有 表格要显示)应该没问题。有关背景信息,请参阅 this answer
  • 在循环中,检查新创建的按钮作为线程作业的输出,附加事件处理程序,并将它们添加到表单中。

这是一个工作示例,它在使表单可见后向表单添加 3 个按钮,一个接一个地在其间休眠:

Add-Type -ea Stop -Assembly System.Windows.Forms

$Main = New-Object System.Windows.Forms.Form

# Start a thread job that will create the buttons.
$job = Start-ThreadJob {
    $top = 0
    1..3 | % {
        # Create and output a button object.
        ($btn = [System.Windows.Forms.Button] @{
            Name = "Button$_"
            Text = "Button$_"
            Top = $top
        })
        Start-Sleep 1
        $top += $btn.Height
    }
}

# Show the form asynchronously
$Main.Show()

# Process GUI events in a loop, and add
# buttons to the form as they're being created
# by the thread job.
while ($Main.Visible) {
    [System.Windows.Forms.Application]::DoEvents()
    if ($button = Receive-Job -Job $job) {
        # Add an event handler...
        $button.add_Click({ Write-Host "Button clicked: $($this.Name)" })
        # .. and it to the form.
        $Main.Controls.AddRange($button)
    }
}

# Clean up.
$Main.Dispose()
Remove-Job -Job $job -Force

'Done'

在撰写本文时, 尝试通过使用 Register-ObjectEvent 订阅其他线程(运行空间)的事件来实现将控件添加到原始运行空间中的表单,前提是 -Action 用于事件处理的脚本块运行(在动态模块里面)原始线程(运行空间),但是有两个问题:

  • 与您的回答所暗示的不同,-Action 脚本块既不直接看到原始运行空间中的 $Main 变量,也不直接看到其他运行空间的变量 - 这些问题是可以克服的,但是,通过 -MessageData$Main 传递给 Register-ObjectEvent 并通过脚本块中的 $Event.MessageData 访问它,并通过 $Sender.Runspace.SessionStateProxy.GetVariable() 调用访问其他运行空间的变量。

  • 然而,更重要的是,.ShowDialog()调用将阻止进一步处理;也就是说,您的事件不会触发,因此您的 -Action 脚本块将不会被调用,直到 表单关闭后

    • 更新:您提到了 解决方法 以便在显示表单时触发 PowerShell 的事件:

      • 使用虚拟事件处理程序订阅 MouseMove 事件,该事件处理程序的调用使 PowerShell 有机会在以模态方式显示表单时触发自己的事件;例如:$Main.Add_MouseMove({ Out-Host });请注意,此解决方法仅在脚本块时有效 调用 命令 ,例如本例中的 Out-Host(这实际上是一个空操作);单纯的表达式或 .NET 方法调用 不够。

      • 但是,此解决方法不是最优的,因为它依赖于用户(持续)将鼠标悬停在表单上以触发 PowerShell 事件;此外,它有些晦涩且效率低下。