终端图形用户界面开发 dotnet-terminal-gui

这个技能用于使用.NET的Terminal.Gui v2库开发全终端用户界面(TUI)应用,包括视图布局、菜单对话框、事件处理、颜色主题和鼠标支持,支持跨平台Windows、macOS和Linux终端开发。关键词:.NET、Terminal.Gui、TUI、终端应用、用户界面、跨平台、C#、布局、菜单、对话框、事件处理、颜色主题。

前端开发 0 次安装 0 次浏览 更新于 3/6/2026

名称:dotnet-terminal-gui 描述:“构建完整的TUI应用。Terminal.Gui v2:视图、布局(Pos/Dim)、菜单、对话框、绑定、主题。” 用户可调用:false

dotnet-terminal-gui

Terminal.Gui v2用于构建完整的终端用户界面,具有窗口、菜单、对话框、视图、布局、事件处理、颜色主题和鼠标支持。跨平台支持Windows、macOS和Linux终端。

版本假设: .NET 8.0+基线。Terminal.Gui 2.0.0-alpha(v2 Alpha是新项目的活跃开发线——API稳定,功能全面;在Beta之前可能有破坏性更改,但核心架构稳固)。v1.x(1.19.0)处于维护模式,无新功能。

范围

  • Terminal.Gui v2应用生命周期和初始化
  • 视图、布局(Pos/Dim)、菜单、对话框、事件处理
  • 数据绑定、颜色主题、鼠标支持

超出范围

  • 富控制台输出(表格、进度条、提示)——参见[技能:dotnet-spectre-console]
  • CLI命令行解析——参见[技能:dotnet-system-commandline]
  • CLI应用架构和分发——参见[技能:dotnet-cli-architecture]和[技能:dotnet-cli-distribution]

交叉引用:[技能:dotnet-spectre-console]用于富控制台输出替代方案,[技能:dotnet-csharp-async-patterns]用于异步TUI模式,[技能:dotnet-native-aot]用于AOT编译注意事项,[技能:dotnet-system-commandline]用于CLI解析,[技能:dotnet-csharp-dependency-injection]用于TUI应用中的DI,[技能:dotnet-accessibility]用于TUI可访问性限制和屏幕阅读器注意事项。


包引用

<ItemGroup>
  <!-- v2 Alpha ——推荐用于新项目 -->
  <PackageReference Include="Terminal.Gui" Version="2.0.0-alpha.*" />
</ItemGroup>

Terminal.Gui v2目标框架为.NET 8+和.NET Standard 2.0/2.1。对于v1维护项目,使用Version="1.19.*"


应用生命周期

Terminal.Gui v2使用基于实例的模型,带有IApplicationIDisposable用于正确的资源清理。这取代了v1的静态Application.Init() / Application.Run() / Application.Shutdown()模式。

基本应用

using Terminal.Gui;

// 创建并初始化应用(v2中基于实例)
using IApplication app = Application.Create().Init();

var window = new Window
{
    Title = "我的TUI应用",
    Width = Dim.Fill(),
    Height = Dim.Fill()
};

var label = new Label
{
    Text = "你好,Terminal.Gui!",
    X = Pos.Center(),
    Y = Pos.Center()
};
window.Add(label);

app.Run(window);

带类型结果的应用

using IApplication app = Application.Create().Init();

// 运行对话框并获取类型结果
app.Run<MyInputDialog>();
string? result = app.GetResult<string>();

生命周期事件

// IsRunningChanging ——可取消,状态更改前触发
// IsRunningChanged ——不可取消,状态更改后触发
window.IsRunningChanged += (sender, args) =>
{
    if (!args.NewValue)
    {
        // 窗口正在关闭——清理资源
    }
};

布局系统

Terminal.Gui v2将布局统一为单一模型(移除了v1的Absolute/Computed区分)。位置由Pos(X, Y)控制,大小由Dim(Width, Height)控制,两者都相对于SuperView的内容区域。

Pos类型(定位)

// Absolute ——固定坐标
view.X = 5;                          // Pos.Absolute(5)

// Percent ——父级百分比
view.X = Pos.Percent(25);            // 从左25%

// Center ——在父级中居中
view.X = Pos.Center();

// AnchorEnd ——从右/底边缘锚定
view.X = Pos.AnchorEnd(10);          // 从右边缘10

// 相对于另一个视图
view.X = Pos.Right(otherView) + 1;   // 另一个视图右侧1
view.Y = Pos.Bottom(otherView) + 1;  // 另一个视图下方1
view.X = Pos.Left(otherView);        // 与另一个视图左对齐
view.Y = Pos.Top(otherView);         // 与另一个视图顶对齐

// Align ——对齐视图组
view.X = Pos.Align(Alignment.End);   // 右对齐(例如,对话框按钮)

// Func ——自定义函数
view.X = Pos.Func(() => CalculateX());

// 算术运算
view.X = Pos.Center() - 10;
view.Y = Pos.Bottom(label) + 2;

Dim类型(尺寸)

// Absolute ——固定大小
view.Width = 40;                       // Dim.Absolute(40)

// Percent ——父级百分比
view.Width = Dim.Percent(50);          // 父级宽度50%

// Fill ——填充剩余空间
view.Width = Dim.Fill();               // 填充到右边缘
view.Width = Dim.Fill(2);              // 填充减去2(边距)

// Auto ——基于内容的大小(取代v1的AutoSize)
view.Width = Dim.Auto();
view.Width = Dim.Auto(minimumContentDim: 20);

// 相对于另一个视图
view.Width = Dim.Width(otherView);
view.Height = Dim.Height(otherView);

// Func ——自定义函数
view.Width = Dim.Func(() => CalculateWidth());

// 算术运算
view.Width = Dim.Fill() - 10;
view.Height = Dim.Height(label) + 2;

框架 vs. 视口

  • 框架 ——最外层矩形:相对于SuperView的位置和大小
  • 视口 ——内容区域的可见部分:作为可滚动门户进入视图的内容
// 设置内容大小大于视口以启用滚动
view.SetContentSize(new Size(200, 100));
// 视口自动提供滚动行为

核心视图

容器视图

// Window ——带标题栏和边框的顶级容器
var window = new Window
{
    Title = "主窗口",
    Width = Dim.Fill(),
    Height = Dim.Fill()
};

// FrameView ——带边框的容器,无标题栏行为
var frame = new FrameView
{
    Title = "设置",
    X = 1, Y = 1,
    Width = Dim.Fill(1),
    Height = 10
};
window.Add(frame);

文本和输入视图

// Label ——静态文本显示
var label = new Label
{
    Text = "用户名:",
    X = 1, Y = 1
};

// TextField ——单行文本输入
var textField = new TextField
{
    X = Pos.Right(label) + 1,
    Y = Pos.Top(label),
    Width = 30,
    Text = ""
};

// TextView ——多行文本编辑器
var textView = new TextView
{
    X = 1, Y = 3,
    Width = Dim.Fill(1),
    Height = Dim.Fill(1),
    Text = "多行
编辑区域"
};

按钮

var button = new Button
{
    Text = "确定",
    X = Pos.Center(),
    Y = Pos.Bottom(textField) + 1
};

// Accept事件(v2取代v1的Clicked)
button.Accepting += (sender, args) =>
{
    MessageBox.Query(button.App!, "信息", $"你输入了:{textField.Text}", "确定");
    args.Handled = true; // 防止事件冒泡
};

ListView和TableView

// ListView ——可滚动列表
var items = new List<string> { "项目1", "项目2", "项目3" };
var listView = new ListView
{
    X = 1, Y = 1,
    Width = Dim.Fill(1),
    Height = Dim.Fill(1),
    Source = new ListWrapper<string>(new ObservableCollection<string>(items))
};

listView.SelectedItemChanged += (sender, args) =>
{
    // args.Value是选中的项目索引
};

CheckBox和RadioGroup

var checkbox = new CheckBox
{
    Text = "启用通知",
    X = 1, Y = 1
};

checkbox.CheckedStateChanging += (sender, args) =>
{
    // args.NewValue是新的CheckState
};

var radioGroup = new RadioGroup
{
    X = 1, Y = 3,
    RadioLabels = ["选项A", "选项B", "选项C"]
};

radioGroup.SelectedItemChanged += (sender, args) =>
{
    // args.SelectedItem是选中的索引
};

其他v2视图

// DatePicker ——基于日历的日期输入
var datePicker = new DatePicker
{
    X = 1, Y = 1,
    Date = DateTime.Today
};

// NumericUpDown ——数字微调器
var spinner = new NumericUpDown<int>
{
    X = 1, Y = 3,
    Value = 42
};

// ColorPicker ——TrueColor选择
var colorPicker = new ColorPicker
{
    X = 1, Y = 5,
    SelectedColor = new Color(0, 120, 215)
};

菜单和状态栏

MenuBar

在v2中,MenuBar接受一个MenuBarItem[]构造参数。MenuItem支持位置构造器和对象初始化器语法。

var menuBar = new MenuBar([
    new MenuBarItem("_文件",
    [
        new MenuItem("_新建", "创建新文件", () => NewFile()),
        new MenuItem("_打开", "打开现有文件", () => OpenFile()),
        new MenuBarItem("_最近",
        [
            new MenuItem("文件1.txt", "", () => Open("文件1.txt")),
            new MenuItem("文件2.txt", "", () => Open("文件2.txt"))
        ]),
        null,  // 分隔符
        new MenuItem
        {
            Title = "_退出",
            HelpText = "退出应用",
            Key = Application.QuitKey,
            Command = Command.Quit
        }
    ]),
    new MenuBarItem("_编辑",
    [
        new MenuItem("_复制", "", () => Copy(), Key.C.WithCtrl),
        new MenuItem("_粘贴", "", () => Paste(), Key.V.WithCtrl)
    ]),
    new MenuBarItem("_帮助",
    [
        new MenuItem("_关于", "关于此应用", () =>
            MessageBox.Query(app, "", "我的TUI应用 v1.0", "确定"))
    ])
]);
window.Add(menuBar);

StatusBar

在v2中,StatusBar使用Shortcut对象而不是v1的StatusItem(已移除)。通过statusBar.Add()添加快捷键。

var statusBar = new StatusBar();

var helpShortcut = new Shortcut
{
    Title = "帮助",
    Key = Key.F1,
    CanFocus = false
};
helpShortcut.Accepting += (sender, args) =>
{
    ShowHelp();
    args.Handled = true;
};

var saveShortcut = new Shortcut
{
    Title = "保存",
    Key = Key.F2,
    CanFocus = false
};
saveShortcut.Accepting += (sender, args) =>
{
    Save();
    args.Handled = true;
};

var quitShortcut = new Shortcut
{
    Title = "退出",
    Key = Application.QuitKey,
    CanFocus = false
};

statusBar.Add(helpShortcut, saveShortcut, quitShortcut);
window.Add(statusBar);

对话框和消息框

对话框

// 带按钮的对话框
var dialog = new Dialog
{
    Title = "确认",
    Width = 50,
    Height = 10
};

var label = new Label
{
    Text = "你确定吗?",
    X = Pos.Center(),
    Y = 1
};
dialog.Add(label);

var okButton = new Button { Text = "确定" };
okButton.Accepting += (sender, args) =>
{
    dialog.RequestStop();
    args.Handled = true;
};

var cancelButton = new Button { Text = "取消" };
cancelButton.Accepting += (sender, args) =>
{
    dialog.RequestStop();
    args.Handled = true;
};

dialog.AddButton(okButton);
dialog.AddButton(cancelButton);

app.Run(dialog);

MessageBox

在v2中,MessageBox.QueryMessageBox.ErrorQuery首先接受一个IApplication参数。

// 简单查询对话框(返回按钮索引)
// 在v2中,将应用实例作为第一个参数传递
int result = MessageBox.Query(app, "确认删除",
    "永久删除此文件?",
    "是", "否");

if (result == 0)
{
    // 用户点击了“是”
}

// 错误消息
MessageBox.ErrorQuery(app, "错误",
    "保存文件失败。
检查权限。",
    "确定");

FileDialog

var fileDialog = new FileDialog
{
    Title = "打开文件",
    AllowedTypes = [new AllowedType("C# 文件", ".cs", ".csx")],
    MustExist = true
};

app.Run(fileDialog);

if (!fileDialog.Canceled)
{
    string selectedPath = fileDialog.FilePath;
    // 处理选中的文件
}

事件处理和键绑定

带命令的键绑定

Terminal.Gui v2使用命令模式进行键绑定。视图声明支持的命令,然后将键映射到这些命令。

// 添加自定义命令并绑定键到它
view.AddCommand(Command.Accept, (args) =>
{
    // 处理接受命令
    return true; // 已处理
});
view.KeyBindings.Add(Key.Enter, Command.Accept);

// 绑定Ctrl+S到保存操作
view.KeyBindings.Add(Key.S.WithCtrl, Command.Save);
view.AddCommand(Command.Save, (args) =>
{
    SaveDocument();
    return true;
});

键事件处理

// KeyDown ——当键按下时触发
view.KeyDown += (sender, args) =>
{
    if (args.KeyCode == Key.F5)
    {
        RefreshData();
        args.Handled = true;
    }
};

// KeyUp ——当键释放时触发
view.KeyUp += (sender, args) =>
{
    // 处理键释放
};

应用级键

尽管v2使用基于实例的IApplicationApplication.QuitKeyInit()之前仍是一个静态配置属性。这些是框架级设置,不是每个实例的状态。

// 在Init之前配置全局退出键(默认:Esc)
Application.QuitKey = Key.Q.WithCtrl;

IApplication app = Application.Create().Init();
// Application.QuitKey现在对此应用实例生效

鼠标事件

// 鼠标事件提供视口相对坐标
view.MouseClick += (sender, args) =>
{
    int col = args.Position.X;
    int row = args.Position.Y;
    // 处理视口相对位置的点击
};

view.MouseEvent += (sender, args) =>
{
    if (args.Flags.HasFlag(MouseFlags.Button1DoubleClicked))
    {
        // 处理双击
    }
};

颜色主题和样式

Terminal.Gui v2默认使用24位TrueColor,并自动回退到16色模式以支持有限终端。

颜色和属性

// 通过RGB值使用TrueColor
var customColor = new Color(0xFF, 0x99, 0x00); // 橙色

// 创建一个属性(前景 + 背景 + 样式)
var attr = new Attribute(
    new Color(255, 255, 255),  // 前景:白色
    new Color(0, 0, 128)       // 背景:深蓝色
);

// 应用到视图
view.ColorScheme = new ColorScheme
{
    Normal = attr,
    Focus = new Attribute(Color.Black, Color.BrightCyan),
    HotNormal = new Attribute(Color.Red, Color.Blue),
    HotFocus = new Attribute(Color.BrightRed, Color.BrightCyan)
};

文本样式

// v2支持文本效果(终端依赖)
// 粗体、斜体、下划线、删除线、闪烁、反色、淡色

主题配置

Terminal.Gui v2通过ConfigurationManager支持基于JSON的主题持久化。用户无需更改代码即可自定义主题、键绑定和视图属性。

// 在Init之前通过运行时配置设置内置主题
ConfigurationManager.RuntimeConfig = """{ "Theme": "Amber Phosphor" }""";
ConfigurationManager.Enable(ConfigLocations.All);

IApplication app = Application.Create().Init();
// 主题现已应用

装饰(边框、边距、内边距)

Terminal.Gui v2提供一个装饰系统用于视觉间距和边框。

var view = new View
{
    X = 1, Y = 1,
    Width = 40, Height = 10
};

// 边框样式:单线、双线、粗线、圆角、虚线、点线
view.Border.LineStyle = LineStyle.Rounded;
view.Border.Thickness = new Thickness(1);

// 边距 ——边框外的透明间距
view.Margin.Thickness = new Thickness(1);

// 内边距 ——边框内的内部间距
view.Padding.Thickness = new Thickness(1, 0); // 上/下=1,左/右=0

跨平台考虑

Terminal.Gui支持Windows、macOS和Linux终端,带有自动驱动程序选择。

终端兼容性

功能 Windows Terminal macOS Terminal.app Linux (xterm/gnome)
TrueColor (24-bit) 是(大多数)
鼠标支持
Unicode/emoji 变化
Sixel图像 一些 一些
键修饰符 完整 有限 完整

平台特定陷阱

  • macOS Terminal.app ——修饰键支持有限;Alt键组合可能被终端拦截。iTerm2和WezTerm提供更好的修饰键支持。
  • SSH会话 ——终端能力取决于客户端终端,不是服务器。通过SSH测试TUI应用以验证渲染。
  • Windows Console Host ——遗留conhost的Unicode支持有限。Windows Terminal提供完整支持。
  • tmux/screen ——可能拦截某些键组合。设置TERM=xterm-256color以获得最佳颜色支持。

日志集成

// Terminal.Gui v2支持Microsoft.Extensions.Logging
// 适用于调试渲染和事件问题,而不干扰TUI显示

完整示例:简单编辑器

using Terminal.Gui;

using IApplication app = Application.Create().Init();

var window = new Window
{
    Title = $"简单编辑器({Application.QuitKey}退出)",
    Width = Dim.Fill(),
    Height = Dim.Fill()
};

// 首先声明textView以便菜单lambda可以捕获它
var textView = new TextView
{
    X = 0, Y = 1,  // 菜单栏下方
    Width = Dim.Fill(),
    Height = Dim.Fill(1),  // 为状态栏留出空间
    Text = ""
};

// 菜单栏
var menuBar = new MenuBar([
    new MenuBarItem("_文件",
    [
        new MenuItem("_新建", "清除编辑器", () => textView.Text = ""),
        null,
        new MenuItem
        {
            Title = "_退出",
            HelpText = "退出",
            Key = Application.QuitKey,
            Command = Command.Quit
        }
    ])
]);

// 带Shortcut对象的状态栏(v2 API)
var statusBar = new StatusBar();
var helpShortcut = new Shortcut { Title = "帮助", Key = Key.F1, CanFocus = false };
helpShortcut.Accepting += (s, e) =>
{
    MessageBox.Query(app, "帮助", "简单文本编辑器。", "确定");
    e.Handled = true;
};
statusBar.Add(helpShortcut);

window.Add(menuBar, textView, statusBar);
app.Run(window);

代理陷阱

  1. 不要使用v1静态生命周期模式。 v2使用基于实例的Application.Create().Init()IDisposable。v1的Application.Init() / Application.Run() / Application.Shutdown()模式已过时。始终在using语句中包装应用。
  2. 不要使用View.AutoSize 它在v2中被移除。使用Dim.Auto()进行基于内容的尺寸调整。
  3. 不要混淆框架和视口。 框架是最外层矩形(相对于SuperView的位置/大小)。视口是可见内容区域(支持滚动)。使用视口处理内容相对坐标。
  4. 不要使用Button.Clicked 它在v2中被Button.Accepting取代。语义变化反映了命令模式——Accepting在按钮的接受动作触发时触发。
  5. 不要从后台线程调用UI操作。 Terminal.Gui是单线程的。使用Application.Invoke()从异步代码中将调用编组回UI线程。参见[技能:dotnet-csharp-async-patterns]用于异步模式。
  6. 不要忘记使用RequestStop()来关闭窗口。 直接在运行窗口上调用Dispose()会损坏终端状态。使用RequestStop()干净地退出运行循环,这会触发适当的清理。
  7. 不要硬编码终端尺寸。 使用Dim.Fill()Dim.Percent()Pos.Center()进行响应式布局,以适应终端调整大小。绝对坐标在不同终端大小上会破坏布局。
  8. 不要在崩溃时忽略终端状态。 如果应用没有适当处置就崩溃,终端可能被留在原始模式中。在try/catch中包装app.Run(),并确保using块处置应用以恢复终端状态。
  9. 不要使用ScrollView 它在v2中被移除。所有视图现在都原生支持通过SetContentSize()Viewport属性进行滚动。
  10. 不要使用NStack.ustring 它在v2中被移除。在整个过程中使用标准System.String
  11. 不要使用StatusItem 它在v2中被移除。改用Shortcut对象和StatusBar.Add()。在状态栏快捷键上设置CanFocus = false,并用args.Handled = true处理Accepting

先决条件

  • NuGet包: Terminal.Gui 2.0.0-alpha(v2)或1.19.x(v1维护)
  • 目标框架: net8.0或更高版本(也支持netstandard2.0/2.1)
  • 终端: 任何支持ANSI转义序列的终端仿真器。推荐Windows Terminal、iTerm2或现代Linux终端以获得最佳体验(TrueColor、鼠标、Unicode)。
  • 无需GUI运行时: Terminal.Gui在任何终端中运行——不需要X11、Wayland或桌面环境。

参考