Windows Terminal 设置 UI 设计解析:导航结构、外观预览窗口与级联继承在源码中的落地
【免费下载链接】terminalThe new Windows Terminal and the original Windows console host, all in the same place!项目地址: https://gitcode.com/GitHub_Trending/term/terminal
本文围绕 设计文档(issue #1564 的 Settings UI Design)展开,讲清楚 Windows Terminal 设置界面的页面导航布局、各页面的完整设置项清单、带实时预览窗口的 Appearance 页设计以及键盘绑定编辑弹窗形态,并结合仓库中 TerminalSettingsEditor 与 TerminalSettingsModel 的实际源码,印证这些设计如何演化为今天可以运行的设置 UI——读完后你既能理解设计稿中每个页面与控件的意图,也能定位到实现它的 XAML、导航标签与继承机制代码。

一、设计文档的定位:从 issue #1564 到三份配套规格
design.md 的文档头元数据表明,它由 Kayla Cinnamon 于 2020-07-13 创建、2020-08-11 最后更新,对应 issue #1564。文档摘要明确了两点:
- 它描述设置 UI 每个页面的布局,并给出设计稿(mockup)展示 UI 的外观;
- 设计稿只用于示意外观,其中的布局与命名可能与最终实现不同,该文档被视为最终裁决("This doc should be considered the final say")。
同一目录下还有两份配套文档,与 design.md 互为补充:
- spec.md:定义设置 UI 的基本功能——如何禁用 UI、导航项、启动方式与设置编辑保存机制;
- cascading-settings.md:探讨级联设置(cascading settings)与
profiles.defaults如何在设置 UI 中表示。
值得注意的是,当时 Windows Terminal 的默认设置体验是用文本编辑器打开settings.json,设置 UI 是全新引入的界面,因此设计文档同时给出了导航方案对比(更细粒度的分类导航 vs 与 JSON 结构对齐的导航),并在 spec.md 中记录了备选方案(新窗口启动、自动保存等)及各自的取舍。
二、顶层导航设计:General / Appearance / Profiles / Keyboard
design.md 给出的左侧导航栏顶层结构如下(带星号的页面在对应功能实现后才加入):
- General
- Startup
- Interaction
- Rendering
- Appearance
- Global
- Color schemes
- Themes*
- Profiles
- Defaults
- Enumerate profiles
- Add new
- Keyboard
- Mouse*
- Command Palette*
- Marketplace*
在 spec.md 中,还记录了另一套被否决的导航方案——"与 JSON 对齐":顶层只有 Globals、Profiles、Color schemes、Bindings 四项(Bindings 下挂 key bindings、mouse bindings 与 command palette)。最终产品选择了更细分、更易消化的分类导航,理由是"与其他终端更一致"。
对照源码:今天的导航项与导航标签
从 MainPage.xaml 的NavigationView.MenuItems可以看到,现行导航项已演进为平铺的页面列表:Launch(对应原 General-Startup)、Interaction、Appearance、Color Schemes、Rendering、Compatibility、Actions(带 New 徽标)、New Tab Menu、Extensions,加上 Profiles 分组头与 Base Layer(x:Uid="Nav_ProfileDefaults",即级联设置文档中把 "Global" 页改名后的 "Base layer")。导航标签常量集中定义在 NavConstants.h,其中til::static_map把每个NavigationTag映射到 Segoe MDL2 Assets 图标字形,例如launchTag → \xE7B5、actionsTag → \xE765(Keyboard Classic)。
spec.md 中"导航菜单底部放一个 'Open the JSON file' 按钮"的设计也已落地:MainPage.xaml 的FooterMenuItems中有OpenJsonNavItem(Tag="OpenJson_Nav"),用户随时可以从 UI 回到原始 JSON 文件编辑。
三、Appearance 页设计:内嵌 TerminalControl 的实时预览窗口
design.md 指出 Profile appearance 页需要特殊设计,因为它内嵌了一个 TerminalControl 预览窗口,用于实时预览外观变更。该预览窗口出现在两个页面:
- Appearance - Color Schemes
- Profiles - Appearance

源码印证:预览窗口不启动真实进程
PreviewConnection.cpp 实现了这个预览窗口背后的数据源:它实现了一个TerminalConnection,但WriteInput/Resize/Close全部为空操作,Start()只通过TerminalOutput.raise(...)向渲染层写出一段固定的示例文本——包括硬重置序列(RIS)、Windows Terminal标题、一段仿git diff -w的彩色输出(红- Windows Console、绿+ Windows Terminal!、灰Write-Host "🛃!")。它还根据字体是否支持 powerline 字形在PromptTextPlain(C:\>)与PromptTextPowerline(\x1b[49;34m\xe0b6…等 ANSI 序列拼出的C:\提示符)之间切换并重新发送。
从源码结构看,预览窗口与真实终端共用同一个 TerminalControl 渲染管线,只是数据源换成PreviewConnection:用户每改一项颜色、字体或 acrylic 参数,渲染层就会用新的设置重绘这段固定样例,从而在点击保存前看到外观效果——这正是 spec.md 中"在写回 settings.json 之前预览变更"这一决策的实现路径。
四、Keyboard 页设计:绑定列表与动态生长的编辑弹窗
design.md 对键盘页的描述包含三个要点:
- 页面列出所有已启用的按键绑定,并提供添加与删除入口;
- 悬停某一项时出现 Edit 与 Delete 按钮;
- 点击 Edit 弹出模态框:对于无参数/无 actions 的命令,模态框只显示命令与按键输入区;若命令带有额外参数或 actions,模态框会随参数/actions 的添加动态增高。设计稿同时注明,希望未来的输入框能"监听"按键组合,即界面上增加一个 "listen" 按钮。

今天该页面已演进为Actions 页:Actions.xaml 与 EditAction 承担"绑定列表 + 编辑弹窗"的职责,而 "listen" 构想则由 KeyChordListener 组件落地——它暴露一个Keys依赖属性(DEPENDENCY_PROPERTY(Control::KeyChord, Keys))并在KeyChordTextBox_KeyDown中捕获按键事件把组合键写入KeyChord,即"点击输入框后直接按键即可录入组合"。
五、完整设置清单:八个页面的控件布局
design.md 的核心内容是一张覆盖全部设置项的布局表。标题行与 UI 左侧的导航视图对齐,列中加粗的标题(如General、Appearance、Advanced)对应页面顶部的 pivot 导航。以下完整继承原文档表格:
| General - Startup | General - Interaction | General - Rendering | Appearance - Global | Appearance - Color Schemes | Profiles - Global | Profiles - Enumerate profiles | Profiles - Add new | |
|---|---|---|---|---|---|---|---|---|
| Default profile (dropdown) | Copy after selection is made (checkbox) | Software rendering (checkbox) | Theme (radio) | Name (text box) | General | General | General | General |
| Launch on startup (checkbox) | Copy formatting (checkbox) | Screen redrawing (checkbox) | Show/Hide the title bar (checkbox) | Cursor color (color picker) | Command line (text box) | Scrollbar visibility (radio) | Scrollbar visibility (radio) | |
| Launch size (radio) | Word delimiters (text box) | Show terminal title in title bar (checkbox) | Selection background (color picker) | Starting directory (browse button) | Command line (browse button) | Command line (browse button) | ||
| Launch position (text box) | Window resize behavior (checkbox) | Always show tabs (checkbox) | Background (color picker) | Icon (browse button) | Starting directory (browse button) | Starting directory (browse button) | ||
| Columns on first launch (number picker) | Tab width mode (radio) | Foreground (color picker) | Tab title (text box) | Name (text box) | Name (text box) | |||
| Rows on first launch (number picker) | Hide close all tabs popup (checkbox) | Black (color picker) | Scrollbar visibility (radio) | Icon (browse button) | Icon (browse button) | |||
| Automatically create new profiles when new shells are installed (checkbox) | Blue (color picker) | Appearance | Tab title (text box) | Tab title (text box) | ||||
| Cyan (color picker) | Font face (text box) | Appearance | Appearance | |||||
| Green (color picker) | Font size (number picker) | Retro terminal effects (checkbox) | Retro terminal effects (checkbox) | |||||
| Purple (color picker) | Font weight (dropdown) | Font face (text box) | Font face (text box) | |||||
| Red (color picker) | Padding (text box) | Font size (number picker) | Font size (number picker) | |||||
| White (color picker) | Cursor shape (radio) | Font weight (dropdown) | Font weight (dropdown) | |||||
| Yellow (color picker) | Cursor color (color picker) | Padding (text box) | Padding (text box) | |||||
| Bright black (color picker) | Cursor height (number picker) | Cursor shape (radio) | Cursor shape (radio) | |||||
| Bright blue (color picker) | Color scheme (dropdown) | Cursor color (color picker) | Cursor color (color picker) | |||||
| Bright cyan (color picker) | Foreground color (color picker) | Cursor height (number picker) | Cursor height (number picker) | |||||
| Bright green (color picker) | Background color (color picker) | Color scheme (dropdown) | Color scheme (dropdown) | |||||
| Bright purple (color picker) | Selection background color (color picker) | Foreground color (color picker) | Foreground color (color picker) | |||||
| Bright red (color picker) | Enable acrylic (checkbox) | Background color (color picker) | Background color (color picker) | |||||
| Bright white (color picker) | Acrylic opacity (number picker) | Selection background color (color picker) | Selection background color (color picker) | |||||
| Bright yellow (color picker) | Background image (browse button) | Enable acrylic (checkbox) | Enable acrylic (checkbox) | |||||
| Background image stretch mode (radio) | Acrylic opacity (number picker) | Acrylic opacity (number picker) | ||||||
| Background image alignment (dropdown) | Background image (browse button) | Background image (browse button) | ||||||
| Background image opacity (number picker) | Background image stretch mode (radio) | Background image stretch mode (radio) | ||||||
| Retro terminal effects (checkbox) | Background image alignment (dropdown) | Background image alignment (dropdown) | ||||||
| Advanced | Background image opacity (number picker) | Background image opacity (number picker) | ||||||
| Hide profile from dropdown (checkbox) | Advanced | Advanced | ||||||
| Suppress title changes (checkbox) | GUID (text box) | GUID (text box) | ||||||
| Antialiasing text (radio) | Hide profile from dropdown (checkbox) | Hide profile from dropdown (checkbox) | ||||||
| AltGr aliasing (checkbox) | Suppress title changes (checkbox) | Suppress title changes (checkbox) | ||||||
| Scroll to input when typing (checkbox) | Antialiasing text (radio) | Antialiasing text (radio) | ||||||
| History size (number picker) | AltGr aliasing (checkbox) | AltGr aliasing (checkbox) | ||||||
| How the profile closes (radio) | Scroll to input when typing (checkbox) | Scroll to input when typing (checkbox) | ||||||
| History size (number picker) | History size (number picker) | |||||||
| How the profile closes (radio) | How the profile closes (radio) |
表格与真实默认值 JSON 的对应关系
表中每一项都对应 defaults.json 中的一个 JSON 键。该文件是系统级默认值层(设计文档中"system set value"的来源),节选可验证上述表格的取值形态:
- Startup 组:
initialCols: 120、initialRows: 30、launchMode: "default"对应 "Columns/Rows on first launch" 与 "Launch size" 控件; - Interaction 组:
copyOnSelect: false、copyFormatting: true、wordDelimiters: " /\\()\"'-.,:;<>~!@#$%^&*|+=[]{}~?\u2502"对应 "Copy after selection is made / Copy formatting / Word delimiters" 三个控件; - Appearance - Global:
alwaysShowTabs: true、showTerminalTitleInTitlebar: true、tabWidthMode: "equal"、theme: "dark"对应表中 "Always show tabs / Show terminal title in title bar / Tab width mode / Theme" 控件; - Profiles 层:
historySize: 9001、closeOnExit: "automatic"、antialiasingMode: "grayscale"、altGrAliasing: true、snapOnInput: true等对应 Advanced pivot 下的 History size、How the profile closes、Antialiasing text、AltGr aliasing、Scroll to input when typing 控件;useAcrylic: false对应 Enable acrylic。
这验证了 cascading-settings.md 中所述的三级取值链:用户 settings.json 值 → defaults.json 值 → 系统内建值。
六、保存机制:Save 按钮、未保存提示与 Portable 模式
spec.md 对编辑保存的决策是"实现一个 Save 按钮":
- 用户只有点击 Save 后,变更才写回
settings.json——与今天用文本编辑器改 JSON 并保存的行为对齐; - 被否掉的备选方案是"边改边自动保存",实时生效但失去了与 JSON 工作流的对等性。
当前实现与该决策一致,且功能更多:MainPage.xaml 底部条包含SaveButton(AccentButtonStyle 强调按钮)与ResetButton,左侧有一个默认Collapsed的Settings_UnsavedSettingsWarning文本(Goldenrod 颜色)用于提示存在未保存变更;当CascadiaSettings.IsPortableMode为真时还会显示 Portable 模式说明与链接。保存/重置的实际处理逻辑在 MainPage.cpp 的SaveButton_Click/ResetButton_Click中。
spec.md 同时要求整个 UI 通过完整的无障碍测试:所有条目必须可被屏幕阅读器和键盘访问,并全量本地化。从 TerminalSettingsEditor/Resources 下覆盖 de-DE、zh-CN、ja-JP 等十余种语言的Resources.resw目录结构看,本地化要求已被落实。
七、级联设置在 UI 中的表示:Base layer、重置与跨 Profile 复制
cascading-settings.md(Carlos Zamora 与 Kayla Cinnamon,2020-11-10)探讨的是:既然每个设置的值可能来自 profile 自身、profiles.defaults或 defaults.json 多层声明,设置 UI 应如何表达"覆盖/继承"关系。文档给出了一组将组合使用的 UI 提案:
- 控件下方文字标注:把 Profiles 下的 "Global" 页改名为 "Base layer";覆盖了 base layer 的设置,控件下方显示 "Overrides Base layer." 文字,标题旁提供一个带 "Reset" 提示的重置按钮;
- Add New → Duplicate Profile:新增 Profile 入口改为一个选择页,用户可以在"复制某个已有 profile"与"从默认设置新建"之间单选,随后进入的 Profile 页会按所选来源预填字段;
- Reset Profile 按钮:在 Profile 页的 Advanced pivot 底部提供 "Reset to default settings",清除该 profile 对象内的自定义设置,优先级回落到
profile.defaults然后 defaults.json; - "Apply to all profiles":每个 Profile 的 Advanced 页提供 "Copy settings to..." 按钮,弹出对话框以树视图列出全部 profile 设置项供勾选,底部以复选框列出所有 profile 作为复制目标。
文档还为每个提案对比了同类终端的做法(ConEmu/Cmder 的 Clone profile、iTerm2 的 Bulk Copy from Selected Profile 等),并记录了两个被否决的方案及其理由:
<inherit>下拉项:每个设置变成可编辑 ComboBox、附带 inherit 选项。优点是界面不杂乱,缺点是每个设置都是下拉框,且颜色选择器在该模型下无法自洽;- 锁按钮:锁住 = 继承自 Global 且控件禁用。缺点是语义易误解("锁住"直觉上表示"固定在本 profile",与当前设计恰好相反)。
源码印证:Has/Clear/OverrideSource 三件套
cascading-settings.md 指出 XAML 层会为每个设置引入一个ContentControl包装控件,并利用 TerminalSettingsModel 提供的四件套 API:
// Note: String and "Name" are replaced for each setting bool HasName(); void ClearName(); String Name(); void Name(String val);这套接口在 IInheritable.h 中由宏体系完整实现:
INHERITABLE_SETTING宏为每个设置生成Has<NAME>()(判断用户是否显式设置过值)、<NAME>OverrideSource()(返回提供当前解析值的那一层对象)、Clear<NAME>()(清回继承态)与 getter/setter;- getter 的解析注释直接写着 fallback 顺序:
user set value --> inherited value --> system set value——与文档摘要中"settings.json 值 → defaults.json 值 → 系统值"的级联链一一对应; - 每个设置存储为
std::optional,nullopt表示"必须向父层继承";_parents是一个std::vector<com_ptr<T>>,AddLeastImportantParent/AddMostImportantParent维护父子优先级,<NAME>OverrideSource()会沿父链回溯找到第一个显式设置该值的祖先; - 文档特别提到
INHERITABLE_NULLABLE_SETTING用于Profile.Foreground这类"null 本身是合法值"的可空设置,用双层 optional(NullableSetting<T> = std::optional<std::optional<T>>)区分"未设置需继承"与"显式清空为 null"两种状态。
正是OverrideSource()让 UI 能精确回答"这个值到底来自哪一层",从而驱动 "Overrides Base layer." 标注与每行重置按钮——设计文档中抽象的 API 需求在这里有了逐行对应的实现。
八、能力与兼容性:文档中的边界声明
spec.md 的 Capabilities 一节明确了该功能的能力边界,写作与评审设置 UI 相关代码时值得参照:
- 无障碍:全新 UI 元素,必须完成屏幕阅读器与键盘全路径测试,且全量本地化;
- 安全 / 可靠性 / 性能功耗:均不受影响;
- 兼容性:默认体验从"文本编辑器打开 JSON"变为"打开设置 UI",且该行为可通过修改
openSettings键绑定加settingsUI选项回退——对应今天的 Actions 页中 openSettings 的settingsUI参数; - 未来考量:
hidden属性需要特殊处理(理想情况下所有 profile 无论hidden与否都应出现在设置中)、需要撤销功能、Marketplace 实现后加入顶层导航、导航结构随功能增长持续调整。
cascading-settings.md 另指出一个兼容性取舍:设置 UI 只提供 JSON 的"部分对等"(partial parity)是有意的——它是面向普通用户的简化工具,若为追求全量对等而堆满选项,会牺牲设置 UI 的简洁性这一核心价值。
九、延伸阅读路径
围绕本文主题,仓库内可按以下路径继续深入:
- 设计稿三件套:design.md、spec.md、cascading-settings.md;
- UI 实现:MainPage.xaml(导航、搜索框、Save/Reset 底栏)、NavConstants.h(导航标签与图标映射)、PreviewConnection.cpp(外观预览数据源)、KeyChordListener.h(组合键录入)、ActionEntry.cpp 所在的 Actions 模型;
- 设置模型与继承机制:IInheritable.h(继承链与可清空设置宏)、Profile.h、defaults.json(系统默认值层,文件头注明为自动生成,修改会被忽略);
- 更宏观的模型背景可参阅 Terminal Settings Model 规格 与 TerminalSettings-spec.md。
需要说明适用前提:design.md 成文于 2020 年,文中 "Launch on startup"、"Launch position" 等 Startup 组控件与现行 Launch.xaml 的项集合已有差异(现行版本新增了 startupActions、alwaysOnTop 等,defaults.json 中可见);但导航分层思路、Appearance 预览窗口、Save 保存模型与级联继承表达这四大设计决策,在今天的源码中仍然成立。
【免费下载链接】terminalThe new Windows Terminal and the original Windows console host, all in the same place!项目地址: https://gitcode.com/GitHub_Trending/term/terminal
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考