问题描述
我有一个表格,只要准备好,就会添加几个元素(例如,一个列表).添加它们可能需要一些时间(从几分之一秒到几分钟).因此,我想将处理添加到单独的线程(子线程).元素的数量是事先不知道的(例如,文件夹中有多少个文件),因此在子流中创建它们.当子流中的处理结束时,我想在主窗体上显示这些元素(之前窗体没有这些元素并执行了其他任务).
I have a form in which as soon as ready several elements will be added (for example, a list). It may take some time to add them (from fractions of a second to several minutes). Therefore, I want to add processing to a separate thread (child). The number of elements is not known in advance (for example, how many files are in the folder), so they are created in the child stream. When the processing in the child stream ends, I want to display these elements on the main form (before that the form did not have these elements and performed other tasks).
但是,我面临着无法将这些元素从子流添加到主表单的事实.我举一个简单的例子作为例子.它当然有效:
However, I am faced with the fact that I cannot add these elements to the main form from the child stream. I will give a simple example as an example. It certainly works:
$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()
但是,将相同的内容添加到子流中时,我没有在主窗体上显示按钮.我不明白为什么.
But, adding the same thing to the child stream I did not get the button to display on the main form. I do not understand why.
$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()
如何将元素添加到在子流中创建的主表单?谢谢
How can I add elements to the main form that are created in the child stream? thanks
推荐答案
虽然您可以在线程 B 上创建控件,但不能将它们添加到控件这是在线程 A 从线程 B 中创建的.
While you can create controls on thread B, you cannot add them to a control that was created in thread A from thread B.
如果你尝试这样做,你会得到以下异常:
If you attempt that, you'll get the following exception:
Controls created on one thread cannot be parented to a control on a different thread.
Parenting to 意味着调用控件(表单)上的 .Add()
或 .AddRange()
方法来添加其他控件作为子控件.
Parenting to means calling the .Add()
or .AddRange()
method on a control (form) to add other controls as child controls.
换句话说:为了向您的 $Main
表单添加控件,该表单被创建并稍后显示在 原始 线程(PowerShell 运行空间)中,$Main.Controls.Add()
调用必须发生在同一个线程中.
In other words: In order to add controls to your $Main
form, which is created and later displayed in the original thread (PowerShell runspace), the $Main.Controls.Add()
call must occur in that same thread.
同样,您也应该始终在同一线程中附加事件委托(事件处理程序脚本块).
Similarly, you should always attach event delegates (event-handler script blocks) in that same thread too.
虽然 您自己的答案 尝试确保将按钮添加到原始运行空间中的表单中,但它不起作用如所写 - 请参阅底部.
While your own answer attempts to ensure adding the buttons to the form in the original runspace, it doesn't work as written - see the bottom section.
我建议一种更简单的方法:
I suggest a simpler approach:
使用线程作业在后台创建控件,通过
Start-ThreadJob
.
Use a thread job to create the controls in the background, via
Start-ThreadJob
.
Start-ThreadJob
是ThreadJob
的一部分模块,它为基于子进程的常规后台作业提供了一种基于线程的轻量级替代方案,并且还是通过 PowerShell SDK 创建运行空间的更方便的替代方案.它随 PowerShell [Core] v6+ 一起提供,并且在 Windows PowerShell 中可以按需安装,例如Install-Module ThreadJob -Scope CurrentUser
.在大多数情况下,线程作业是更好的选择,无论是性能还是类型保真度 - 请参阅此答案的底部部分为什么.
Start-ThreadJob
is part of the theThreadJob
module that offers a lightweight, thread-based alternative to the child-process-based regular background jobs and is also a more convenient alternative to creating runspaces via the PowerShell SDK.It comes with PowerShell [Core] v6+ and in Windows PowerShell can be installed on demand with, e.g.,Install-Module ThreadJob -Scope CurrentUser
.In most cases, thread jobs are the better choice, both for performance and type fidelity - see the bottom section of this answer for why.
显示您的表单非模态(.Show()
而不是 .ShowDialog()
)并在一个 [System.Windows.Forms.Application]::DoEvents()
循环.
Show your form non-modally (.Show()
rather than .ShowDialog()
) and process GUI events in a [System.Windows.Forms.Application]::DoEvents()
loop.
- 注意:
[System.Windows.Forms.Application]::DoEvents()
通常可能有问题(它本质上是阻塞.ShowDialog()
调用在幕后做),但在这种受限的情况下(假设只显示一个表单)应该没问题.有关背景信息,请参阅此答案.
- Note:
[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 for background information.
在循环中,检查新创建的按钮作为线程作业的输出,附加事件处理程序,并将它们添加到您的表单中.
In the loop, check for newly created buttons as output by the thread job, attach an event handler, and add them to your form.
这是一个工作示例,它在使表单可见后向表单添加 3 个按钮,一个接一个,同时在两者之间睡觉:
Here is a working example that adds 3 buttons to the form after making it visible, one after the other while sleeping in between:
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
脚本块运行(在内部的动态模块中)原始线程(runspace),但有两个问题:
As of this writing, your own answer tries to achieve adding the controls to the form in the original runspace by using Register-ObjectEvent
to subscribe to the other thread's (runspace's) events, given that the -Action
script block used for event handling runs (in a dynamic module inside) the original thread (runspace), but there are two problems with that:
与您的回答不同,
-Action
脚本块既没有直接看到来自原始运行空间的$Main
变量,也没有直接看到其他运行空间的变量 - 这些问题但是,可以通过通过-MessageData
将$Main
传递给Register-ObjectEvent
并通过$Event.MessageData 在脚本块中,并通过
$Sender.Runspace.SessionStateProxy.GetVariable()
调用访问其他运行空间的变量.
Unlike your answer suggests, the
-Action
script block neither directly sees the$Main
variable from the original runspace, nor the other runspace's variables - these problems can be overcome, however, by passing$Main
toRegister-ObjectEvent
via-MessageData
and accessing it via$Event.MessageData
in the script block, and by accessing the other runspace's variables via$Sender.Runspace.SessionStateProxy.GetVariable()
calls.
然而,更重要的是,.ShowDialog()
调用将阻止进一步处理;也就是说,您的事件不会触发,因此您的 -Action
脚本块在表单关闭后之前不会被调用.
More importantly, however, the .ShowDialog()
call will block further processing; that is, your events won't fire and therefore your -Action
script block won't be invoked until after the form closes.
更新:您提到了一个解决方法,以便在显示表单时触发 PowerShell 的事件:
Update: You mention a workaround in order to get PowerShell's events to fire while the form is being displayed:
使用虚拟事件处理程序订阅
MouseMove
事件,该事件处理程序的调用使 PowerShell 有机会在以模态方式显示表单时触发自己的事件;例如:$Main.Add_MouseMove({ Out-Host })
;请注意,此解决方法仅在脚本块调用命令,例如本例中的Out-Host
(实际上是无操作);仅仅一个表达式或 .NET 方法调用是不够的.
Subscribe to the
MouseMove
event with a dummy event handler whose invocation gives PowerShell a chance to fire its own events while the form is being displayed modally; e.g.:$Main.Add_MouseMove({ Out-Host })
; note that this workaround is only effective if the script blockcalls a command, such asOut-Host
in this example (which is effectively a no-op); a mere expression or .NET method call is not enough.
但是,这种解决方法并不理想,因为它依赖于用户(持续)将鼠标悬停在表单上以触发 PowerShell 事件;此外,它有点晦涩和低效.
However, this workaround is suboptimal in that it relies on the user (continually) mousing over the form for the PowerShell events to fire; also, it is somewhat obscure and inefficient.
这篇关于从另一个运行空间向表单添加元素的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!