Steve Lee's helpful answer 提供关键指针;让我补充一下背景信息:
PowerShell 提供了两种基本的事件订阅机制:
-
(a) .NET-native,如 Steve 的回答中所示,您将 script block ({ ... }) 作为 委托 附加到对象的 @ 987654339@ 事件通过.add_<Name>() 实例方法(委托是一段用户提供的回调代码,在事件触发时被调用) - 请参阅下一节。
-
(b) PowerShell 介导,使用 Register-ObjectEvent 和相关 cmdlet:
- 类似于 (a) 的基于回调的方法可通过将脚本块传递给
-Action 参数来获得。
- 或者,可以通过
Get-Event cmdlet 按需检索排队的事件。
方法 (b) 的回调方法仅在 PowerShell 控制前台线程时及时起作用,而 不是 这里的情况,因为[Terminal.Gui.Application]::Run() 调用阻止它。
因此,必须使用方法(a)。
回复(a):
C# 以运算符+= 和-= 的形式提供语法糖,用于附加和分离事件处理程序委托,这看起来像assignments,但实际上被翻译成add_<Event>()和remove_<Event>()方法调用。
可以看到这些方法名如下,以[powerShell]类型为例:
PS> [powershell].GetEvents() | select Name, *Method, EventHandlerType
Name : InvocationStateChanged
AddMethod : Void add_InvocationStateChanged(System.EventHandler`1[System.Management.Automation.PSInvocationStateChangedEventArgs])
RemoveMethod : Void remove_InvocationStateChanged(System.EventHandler`1[System.Management.Automation.PSInvocationStateChangedEventArgs])
RaiseMethod :
EventHandlerType : System.EventHandler`1[System.Management.Automation.PSInvocationStateChangedEventArgs]
PowerShell 没有提供这样的语法糖来附加/删除事件处理程序,因此必须直接调用这些方法。
不幸的是,Get-Member 和 tab-completion 都不知道这些方法,而相反,原始事件 names 令人困惑地do 得到 tab-completed,即使你不能直接对他们采取行动。
Github suggestion #12926 旨在解决这两个问题。
用于事件定义的约定:
上面的EventHandlerType 属性显示了事件处理程序委托的类型名称,在这种情况下,它正确地遵守了使用基于泛型类型System.EventHandler<TEventArgs> 的委托的约定,其签名是:
public delegate void EventHandler<TEventArgs>(object? sender, TEventArgs e);
TEventArgs 表示包含事件特定信息的实例的类型。
另一个约定是这样的事件参数类型派生自 System.EventArgs 类,而手头的类型 PSInvocationStateChangedEventArgs 就是这样。
提供no事件特定信息的事件按照约定使用非通用System.EventHandler委托:
public delegate void EventHandler(object? sender, EventArgs e);
大概是因为这个委托在历史上被用于所有委托,即使是那些带有事件参数的委托——在generics出现在.NET之前2 - EventArgs 参数仍然存在,约定是传递 EventArgs.Empty 而不是 null 来表示没有参数。
类似地,长期建立的框架类型使用其特定的事件参数类型定义非泛型 custom 委托,例如System.Windows.Forms.KeyPressEventHandler.
CLR 没有强制执行这些约定,但是,正如所讨论的事件被定义为public event Action Clicked; 所证明的那样,它使用无参数 委托作为事件处理程序。
通常建议遵守约定,以免违背用户的期望,尽管这样做有时不太方便。
PowerShell 在将脚本块 ({ ... }) 用作委托时非常灵活,尤其是 不 强制执行特定的参数签名 通过param(...):
脚本块被接受,不管它是否声明了任何、太多或太少的参数,尽管那些绑定到脚本块参数的事件发起对象实际传递的参数必须是类型兼容的(假设脚本块的参数是显式类型的)。
因此,史蒂夫的代码:
$btn.Add_Clicked({
param($sender, $e)
[Terminal.Gui.Application]::RequestStop()
})
尽管参数声明无用,但仍然有效,因为没有参数被传递给脚本块,因为 System.Action 委托类型是无参数。
以下内容就足够了:
$btn.Add_Clicked({
[Terminal.Gui.Application]::RequestStop()
})
注意:即使不声明参数,您也可以通过automatic $this variable(在本例中与$btn 相同)引用事件发送者(触发事件的对象)。
简化的示例代码:
1.0.0-pre.4 版本的注释:
-
至少在 macOS 上,要让终端在退出应用程序后将终端恢复到可用状态,还需要以下附加操作:
-
[Terminal.Gui.Application]::Shutdown()。没有它,在同一个会话中重新调用应用程序将不起作用。
-
tput init。没有它,后来的命令行编辑就会中断(特别是向上和向下箭头)。
-
Terminal.Gui 类型对 PowerShell 不友好,原因有两个:
-
[View] 及其子类实现 IEnumerable 接口,这会导致 PowerShell 的默认输出格式尝试枚举,从而导致 no 输出。
- 解决方法:
$btn.psobject.Properties | select Name, Value, TypeNameOfValue
-
什么是概念上的 text 属性不是作为[string] 类型实现的,而是作为[NStack.ustring] 实现的;虽然您可以透明地使用[string] 实例来分配此类属性,但显示它们再次执行枚举并呈现底层字符的代码点 个别。
-
tig(OP)已提交 GitHub issue #951 以可能修复此行为。
-
从 PowerShell 7.1 开始,没有与 NuGet 包的直接集成,因此将已安装包的程序集加载到 PowerShell 会话中非常麻烦 - 请参阅 this answer,其中展示了如何使用 .NET Core SDK 下载包并使其依赖项可用。
-
请注意,Add-Type -AssemblyName 仅适用于位于 当前 目录(与 脚本的 目录相反)或随 PowerShell 本身提供的程序集(PowerShell [Core ] v6+) / 在 GAC (Windows PowerShell) 中。
-
鉴于目前在 PowerShell 中使用 NuGet 包非常繁琐,GitHub feature suggestion #6724 要求增强 Add-Type 以直接支持 NuGet 包。
using namespace Terminal.Gui
# Load the Terminal.Gui assembly and its dependencies (assumed to be in the
# the same directory).
# NOTE: `using assmembly <path>` seemingly only works with full, literal paths
# as of PowerShell Core 7.1.0-preview.7.
# The assumption here is that all relevant DLLs are stored in subfolder
# assemblies/bin/Release/*/publish of the script directory, as shown in
# https://stackoverflow.com/a/50004706/45375
Add-Type -Path $PSScriptRoot/assemblies/bin/Release/*/publish/Terminal.Gui.dll
# Initialize the "GUI".
# Note: This must come before creating windows and controls.
[Application]::Init()
$win = [Window] @{
Title = 'Hello World'
}
$btn = [Button] @{
X = [Pos]::Center()
Y = [Pos]::Center()
Text = 'Quit'
}
$win.Add($btn)
[Application]::Top.Add($win)
# Attach an event handler to the button.
# Note: Register-ObjectEvent -Action is NOT an option, because
# the [Application]::Run() method used to display the window is blocking.
$btn.add_Clicked({
# Close the modal window.
# This call is also necessary to stop printing garbage in response to mouse
# movements later.
[Application]::RequestStop()
})
# Show the window (takes over the whole screen).
# Note: This is a blocking call.
[Application]::Run()
# As of 1.0.0-pre.4, at least on macOS, the following two statements
# are necessary on in order for the terminal to behave properly again.
[Application]::Shutdown() # Clears the screen too; required for being able to rerun the application in the same session.
tput init # Reset the terminal to make PSReadLine work properly again, notably up- and down-arrow.