§ 1.4 · Section

控件 Widget

Widgets · The Lego Bricks of Interface

打开任何一个软件,你看到的东西看起来千差万别,其实拆开来只有二三十种零件在反复组合。按钮、输入框、下拉菜单、勾选框、滑块、列表、标签页——它们就是界面的乐高积木。学会认这些积木,再复杂的界面你都能一眼看穿它的骨架。

生活场景
🧱 宜家的家具其实只有几种零件

你去宜家逛一圈,几百件家具看得眼花。但翻开安装说明,会发现所有家具都是那几样东西拼出来的:板材、螺丝、插销、合页、封边条。
书架和衣柜的区别,不在于用了不同的零件,而在于零件的尺寸、数量和拼装方式不同
界面也是这样:微信和银行 App 看起来毫无关系,但把它们拆到最底层,都是按钮、文本、图片、列表这几样东西按不同方式摆出来的。

先把这些词翻成人话

这一节的英文词特别多,而且经常混着用。先一次性对齐,后面就顺了。

控件到底是什么:可复用的界面积木

先给一个不绕弯子的定义:控件是一个把"长什么样"和"怎么响应用户"打包在一起的可复用单元

注意"打包"这个词。一个按钮不只是屏幕上那个圆角矩形——如果只是画个矩形,那叫图形,不叫控件。按钮之所以是控件,是因为它同时还知道:鼠标移上来要变亮、按下去要变暗、被点击时要通知程序、被禁用时要变灰而且不再理你。外观和行为绑在一起,才叫控件

再注意"可复用"。这是控件存在的全部理由。想象一下如果没有控件:你要在界面上放 30 个按钮,就得手写 30 遍"画矩形、画文字、检测鼠标是否在矩形内、检测是否按下、改颜色、触发回调"。有了控件,你只需要说 30 次"给我一个按钮,文字是 X,点了以后做 Y"。剩下的脏活,控件库替你干过一遍,全世界共享。

这就是软件工程里最朴素也最有效的思想——把重复的东西抽象成零件,让人只关心零件之间的差异。控件是这个思想在界面领域的产物。

Analogy · 乐高积木与积木说明书

乐高的伟大之处不在于某一块积木特别精巧,而在于所有积木的接口是统一的——凸起和凹槽的尺寸标准化了,于是任意两块都能拼。
控件库就是一套标准化的界面积木:每个控件都有统一的"接口"(怎么设置大小、怎么放进容器、怎么绑定事件)。
所以你学会用一个按钮,基本就学会了用所有控件——因为它们的"凸起和凹槽"是一个模子刻的。

常见控件的五大类

控件的种类看起来很多,但按"它在界面里干什么活"来分,只有五类。记住这五类,以后见到任何陌生控件,都能先给它归个位。

分类它的职责典型成员
输入类把用户的意图收进来单行输入框、多行文本域、密码框、勾选框 Checkbox、单选组 Radio、开关 Switch、滑块 Slider、下拉选择 Select、日期选择器、数字步进器、文件选择器
展示类把信息摆出来给人看文本标签 Label、图片 Image、图标 Icon、分割线 Divider、徽标 Badge、头像 Avatar、表格 Table、树 Tree、代码块、二维码
容器类装别的控件,负责排布面板 Panel、卡片 Card、分组框 GroupBox、滚动区域 ScrollView、栅格 Grid、行/列布局 Row/Column、可折叠面板 Collapse、分栏 Splitter
导航类决定"你现在在哪、能去哪"菜单栏 Menu、右键菜单、标签页 Tabs、侧边栏 Sidebar、面包屑 Breadcrumb、分页器 Pagination、步骤条 Steps、工具栏 Toolbar
反馈类告诉用户"发生了什么"对话框 Dialog、轻提示 Toast、进度条 ProgressBar、加载转圈 Spinner、气泡提示 Tooltip、通知 Notification、骨架屏 Skeleton

这五类之间的关系很像一家餐厅的分工:输入类是点单的服务员,负责把你的要求记下来;展示类是端上桌的菜品和菜单,让你看到东西;容器类是桌子和托盘,决定东西怎么摆;导航类是门口的指引牌和楼层图,告诉你哪儿是包间哪儿是散台;反馈类是那句"您的菜已经在做了,请稍等十分钟"——不干实事,但少了它你会焦虑。

特别值得说的是反馈类。新手做界面最容易漏掉的就是这一类。功能都做对了,点击后要等三秒才有结果,中间什么提示都没有——用户会以为程序卡死了,然后疯狂重复点击。反馈类控件不产生任何"功能",但它决定了软件用起来是否让人安心

输入类控件详解:什么时候用哪个,用错了会怎样

输入类是最容易用错的一类。表面上"都能收集用户的选择",但选错一个,用户就得多点三下、或者干脆看不懂。下面按选项个数是否互斥两把尺子给它们排个座。

控件该用它的场合常见误用生活对应
单选组(Radio)2 到 5 个选项,只能选一个,而且你希望用户同时看见所有选项选项有二十个还硬用单选,把页面撑得老长点菜时"米饭 / 面条 / 馒头"三个格子摊开让你划
下拉选择(Select)选项超过 6 个、或者选项本身不重要不值得占地方只有两个选项还用下拉,害用户多点一次才看得见点菜时那本要翻开才看得到内容的菜单
勾选框(Checkbox)可以选多个、也可以一个都不选用来表示"是否开启某功能"却不立即生效,用户以为已经生效了快递单上的"需要保价 / 需要签收回单",各自独立勾
开关(Switch)只有开和关,而且一拨就立刻生效放在一个还需要点"保存"才生效的表单里,语义就矛盾了墙上的灯开关:一按就亮,没有"确定"这一步
滑块(Slider)连续量、而且用户在意的是"大概位置"而不是精确数字需要精确到个位数时用滑块,用户拖不准,抓狂洗衣机的水温旋钥:你要的是"温一点",不是"38.5 度"
数字步进器(Stepper)整数、小范围、需要精确范围到几千还让人一下一下点点菜时那个"份数 - 2 +"
单行输入框内容长度不确定但一定很短:姓名、手机号拿它收地址,用户看不见自己写全没写全快递单上那一小格"寄件人姓名"
多行文本域内容可能好几行:备注、留言、地址反过来,只填一个数字也给个大方框,用户以为要写作文快递单最下面那一大块"备注"
日期选择器凡是日期,都别让人手打让人手输"2024/3/5"还是"2024-03-05",格式错到崩医院挂号时那个可以翻月份的日历

这里面勾选框和开关的区别最值得单独讲,因为几乎所有新手都混过。判断标准只有一句话:开关一拨就生效,勾选框要等你点"保存"或"确定"才生效。

打个比方。开关是墙上的电灯开关:手一按,灯就亮,没有第二步。勾选框是快递单上打的勾:你在"需要保价"前面画了个勾,这时候什么都还没发生,得等你把单子递给快递员(点"提交"),事情才真正办成。

所以在一个带"保存"按钮的设置页里放开关,就会造成一种很微妙的困惑:用户拨完开关就以为改完了,直接关掉页面,结果设置没生效。这不是用户笨,是控件说错了话。控件的形状本身就在向用户承诺某种行为,承诺和实际不符,就是设计缺陷。

展示、导航、反馈三类的选择要点

输入类讲完,剩下三类各挑最容易搞错的说清楚。

关于反馈类再补一个能直接用的数字标准。人对等待的感受不是线性的,业界有一组经验值:

耗时用户感受你该给什么反馈
0.1 秒以内感觉是"瞬间",跟自己手的动作连在一起什么都不用给
0.1 到 1 秒能感觉到有延迟,但注意力还在按钮进入按下状态就够了
1 到 10 秒开始怀疑"是不是没点上",会重复点击必须给转圈或进度条,并且把按钮禁用掉
10 秒以上注意力已经离开,会去干别的事给能看到进展的进度条,最好加"预计剩余时间",允许取消

第三行那个"把按钮禁用掉"是新手最常漏的一步,后果却很严重。用户点了没反应,就再点,再点——如果后端每次都真的执行,那就是重复下单三次。这就像窗口办事时递了申请单没听到回应,你就又抄一张递进去,最后系统里出现三份重复申请。解决办法很简单:点下去的那一瞬间就把按钮变灰,办完再恢复。

控件三要素:外观 + 状态 + 行为

任何一个控件,无论多简单多复杂,都可以拆成三个部分来理解。这是本节最值得记住的一个模型。

:hover 和 :active 两个伪类,顺序写错会互相覆盖。">

这三者的关系是有方向的:用户操作触发行为 → 行为改变状态 → 状态决定外观

拿一个勾选框举例。你点它一下(用户操作),触发了"切换选中"这个行为,行为把状态从"未选中"改成"选中",然后外观自动从空方框变成打钩的方框。整条链是单向流动的。现代界面框架(React、Vue、Flutter、SwiftUI 等)之所以都强调"状态驱动 UI",本质就是把这条链明确成规矩:你只管改状态,界面长什么样由框架自动推导,不许手动去改外观。

为什么这条规矩重要?因为一旦允许手动改外观,就会出现"状态说没选中,但界面上打着钩"这种自相矛盾的 bug。这类 bug 极难查,因为真相有两份,而且它们不一致。

// 控件三要素在代码里的样子(伪代码)

Button {
  // ① 外观
  text:     "提交订单",
  width:    120,
  color:    "#556b3d",
  radius:   6,

  // ② 状态
  enabled:  false,      // 现在是禁用的
  loading:  false,      // 没有在转圈

  // ③ 行为
  onClick:  function () {
      this.loading = true;      // 改状态
      submitOrder();            // 干实事
  }
}

控件树:界面是一棵嵌套的树

控件从来不是散落一地的,它们是嵌套的——容器类控件里可以放别的控件,而放进去的那个如果也是容器,里面又能再放。层层套下去,整个界面就成了一棵

这棵树有一个根(通常是窗口),中间是各种容器(面板、卡片、布局),叶子是那些实心的控件(按钮、文字、图片)。你在屏幕上看到的每一个像素,都归属于这棵树上的某个节点。

Window(窗口,根节点)
└── Column(竖向排布容器)
    ├── Toolbar(工具栏)
    │   ├── Button "新建"
    │   ├── Button "打开"
    │   └── Button "保存"
    ├── Row(横向排布容器)
    │   ├── Sidebar(侧边栏,宽 200)
    │   │   └── Tree(文件树)
    │   └── ScrollView(可滚动区域,占剩余空间)
    │       └── TextArea(正文编辑区)
    └── StatusBar(状态栏)
        ├── Label "第 12 行,第 4 列"
        └── Label "UTF-8"

树形结构不是为了好看,它带来三个实实在在的好处。

第一,位置可以继承推算。子控件的坐标通常是相对父控件的。父控件整体移动 100 像素,所有子孙自动跟着移动,你不需要逐个去改。这就像搬桌子——桌上的杯子盘子不用一个个搬。

第二,属性可以向下传递。父容器设置了字体、颜色主题、是否禁用,子控件默认继承。所以切换深色模式时,你只要改根节点的主题,整棵树跟着变。

第三,事件可以顺着树跑。你点在按钮上,这个点击事件先从根节点往下"捕获",找到最深的那个被点中的控件,再从它往上"冒泡"。这套机制让"点击子元素时父元素也能知道"变得非常自然——比如点卡片里的任意位置都算点了整张卡片。这一点在下一节讲事件驱动时会展开。

Analogy · 俄罗斯套娃与行政区划

控件树最像行政区划:国 → 省 → 市 → 区 → 街道 → 门牌号。
你的地址(坐标)是相对上一级说的,"3 号楼"只有在某个小区里才有意义。
政策(样式主题)从上往下发,省里定的规矩市里默认执行,除非市里特别声明例外——这就是"样式继承"和"局部覆盖"。
而一件事情上报(事件冒泡),是从门牌号一级级往上传的。

原生控件 vs 自绘控件:一道永恒的取舍

做界面时有个绕不开的选择:按钮到底用操作系统提供的那个,还是自己画一个

原生控件(Native)是操作系统自带的。你要一个按钮,系统就给你一个 Windows 味或者 macOS 味的按钮,长相、动画、字体、无障碍支持全是系统那一套。
自绘控件(Custom-drawn)是框架自己从零画的——只借用系统的"一块空白画布",然后所有像素都自己填。Flutter、Qt 的 QML、游戏引擎里的 UI、浏览器里的绝大多数组件库,走的都是这条路。

对比维度原生控件自绘控件
视觉一致性和系统其他软件浑然一体和自己家产品在各平台一致
跨平台表现各平台长得不一样,得分别适配一套代码,各平台像素级相同
定制自由度低,改个圆角都可能很别扭极高,想画成什么样就什么样
无障碍 / 输入法免费获得屏幕阅读器、中文输入法、放大镜支持要自己实现,很容易做不全
性能与体积轻,系统已经加载好了需要带渲染引擎,包体更大
随系统进化系统换新设计语言,你自动跟上系统更新了,你还是老样子

怎么选?给一条实用的判断标准:工具类软件优先原生,品牌类软件优先自绘

系统设置、文件管理器、开发工具这类东西,用户希望它"像本地软件",快、稳、不出戏,原生控件最合适。而设计工具、音乐播放器、游戏、需要强品牌调性的 App,用户期待的是"独特的视觉体验",自绘更能表达设计。

顺便说一句,这两条路正在互相靠近。原生控件库越来越可定制,自绘框架也在拼命补齐无障碍和输入法这块短板。但这道取舍本身不会消失——它是"融入环境"和"保持自我"之间的永恒张力。

控件库与设计系统:连锁店的装修手册

你可能注意到一个现象:同一家公司的十个产品,长得像一家人;而两家不同公司的产品,就算功能一样,气质也完全不同。这背后不是设计师默契,是设计系统在起作用。

设计系统(Design System,一整套规定了颜色、间距、字号、圆角、动画时长、以及每个控件长什么样的规范文档 + 现成代码库)。它干的活儿相当于连锁餐厅的开店手册:桌椅型号、灯光色温、招牌字体、餐具尺寸、菜品摆盘全写死。于是不管在哪个城市,你一进门就知道"这是那家店"。

设计系统出自视觉气质它最看重什么
Material DesignGoogle纸片层叠、明显阴影、大色块、按下有水波纹用"纸和墨"的物理隐喻解释层级,动效必须有因果
Fluent DesignMicrosoft半透明毛玻璃、柔和光影、圆角兼顾键鼠、触摸、笔、语音多种输入方式
Human Interface GuidelinesApple克制、留白多、字重变化承担层级控件行为高度统一,尽量让人"不用学"
Ant Design蚂蚁集团信息密度高、表格和表单极强专为"一屏放很多数据"的企业后台优化

为什么 Ant Design 那一行说"信息密度高"?因为它面对的场景不同。面向消费者的 App 追求好看好上手,面向内部员工的后台系统追求一屏能看多少条数据。打个比方:前者是商场的展厅,货品摆得疏、灯光打得好、留白多;后者是仓库的货架,一寸空间都要用上,工作人员天天在这儿,效率比美观重要。设计系统之间的差异,本质上是它们服务的人群和使用频率的差异,不是审美高低。

用设计系统的三个实际好处,说明为什么团队几乎必须用:

控件的可访问性:那条必须留的坡道

可访问性(Accessibility,也写作 a11y——让视力、听力、行动能力不同的人也能正常使用软件)。这一节最容易被跳过,但它是区分"做完了"和"做好了"的分水岭。

先建立一个基本认知:需要它的人比你想的多得多。换算成能感知的数字——世界上有大量人存在色觉障碍(男性中比例约 8%,也就是每 12 个男性里有 1 个),视力严重受损的人以亿计,此外还有临时性的:手骨折了只能用一只手、坐在阳光下屏幕反光看不清对比度低的文字、抱着孩子只能用一只手操作手机。无障碍设计从来不只服务"残障人士",它服务的是"任何一个此刻不方便的人",而这个集合包括所有人。

控件层面要做的事,其实就四条,都不难:

最后给一个能立刻自查的土办法:把鼠标拔掉,只用键盘把你的界面从头走到尾,看能不能办完一件完整的事。这一招五分钟就能测,却能揪出九成的可访问性问题。

什么时候该自己造控件

上面一直在说"用现成的",但确实有该自己造的时候。给一个清单,凡是踩中其中一条,才考虑动手:

情况该不该自造为什么
现成控件只是颜色圆角不符合品牌不该改样式就行,不用重写行为。自造会白丢无障碍和输入法支持
现成控件的交互逻辑跟你的需求根本不同比如你要一个"可以同时选一段范围、还能拖动两端"的时间轴,标准滑块做不到
需要展示上万条数据还要流畅滚动标准列表会一次性画出所有行而卡死,得自己做"只画可见区域"的虚拟滚动
你的产品的核心竞争力就是这个交互比如设计工具的画布、音乐软件的波形编辑器,这些就是产品本身
只是觉得"自己写更简单"不该这是最常见的误判。简单的只是第一版,后面的状态组合、键盘支持、边界情况会淹死你

那句"自己写更简单"的错觉,值得用装修的例子破一下。你要装一扇门,看着好像"钉几块木板"就行——但成品门里其实包含了:合页的承重角度、门框的膨胀余量、锁舌的对位、密封条的防风、下沉后不刮地的余隙。你自己做的那扇门,第一天挂上去也能开合,一个月后就开始刮地、关不严、锁不上。控件一样:能显示能点击只是第一天的事。

移动端与桌面端的控件差异

同一个控件搬到手机上,往往必须换个形态。核心原因有三个,全是物理限制。

同一个需求桌面端做法移动端做法
主导航顶部菜单栏 / 左侧边栏底部标签栏(拇指能到)
展示补充说明鼠标悬停弹出气泡旁边放一个问号图标,点开显示
对某一条做操作右键弹出菜单左滑露出按钮,或长按进入多选
从很多选项里选一个下拉列表从底部升起的半屏选择面板(选项更大更好点)
填一张长表单一屏放完,多列排布拆成好几步,一屏一两个字段
确认危险操作居中的模态对话框底部升起的操作表,危险项标红放在最上

最后那一行的"危险项标红放最上"值得一提,它跟桌面习惯正好相反。桌面对话框通常把"取消"放在左边、"确定"放在右边;而移动端底部面板往往把最危险的那一项放在最上面、最下面留一个大大的"取消"。原因是拇指最容易误触的是屏幕最下方,所以那个位置要留给最安全的选项。这不是设计师换了品味,是同一条原则(把误触成本降到最低)在不同物理条件下推出的不同结论。

这就像地铁站的闸机和商场的旋转门:都是控制人流,但一个要求快速通过、一个要求防风保温,于是形态完全不同。你不会说其中一个"设计得不对"。

控件状态:一个按钮至少有五张脸

最后一个容易被新手忽略的点:控件不是只有一个样子。同一个按钮,在不同情况下要长得不一样,用户才能读懂"我现在能不能点、点了有没有反应"。

除这五个之外,很多控件还有更多状态:加载中(Loading)、选中(Selected / Checked)、错误(Error / Invalid)、只读(Readonly)、展开(Expanded)。而这些状态还会叠加——"禁用 + 选中"的勾选框、"错误 + 焦点"的输入框,都得有明确的样子。

所以做一个看起来简单的按钮,真正的工作量往往不在"默认那张脸",而在剩下那十几种组合。这也是为什么成熟的控件库很有价值:它替你把所有状态组合都推敲过、测试过了。

/* 一个按钮的五种状态(CSS 写法) */

.btn                { background:#556b3d; color:#fff; }   /* 默认 */
.btn:hover          { background:#657d4a; }               /* 悬浮 */
.btn:active         { background:#44562f; transform:translateY(1px); } /* 按下 */
.btn:focus-visible  { outline:2px solid #a05a2c; outline-offset:2px; } /* 焦点 */
.btn:disabled       { background:#c9c6bd; cursor:not-allowed; opacity:.6; } /* 禁用 */

回到最开始的那个问题

为什么要花一整节讲控件?因为控件是界面世界的"词汇表"

你学一门外语,语法再熟,没词汇也说不出话。做界面同理:知道控件有哪几类、每类解决什么问题,你才能在拿到一个需求时立刻反应过来"这里该用下拉选择还是单选组"、"这个提示该用 Toast 还是 Dialog"。选对了控件,界面自然清晰;选错了控件,怎么调样式都别扭。

而更深一层的价值是:控件把界面从"画画"变成了"搭建"。画画依赖天赋,搭建可以靠工程方法。这是界面开发能够规模化、能够协作、能够沉淀设计系统的根本前提。

Analogy · 装修市场里的成品门窗

这一节最核心的概念——控件是"打包好的外观 + 行为"——用装修来讲最透。

假设你要给房子装一扇门。最笨的办法是自己做:买木料、刨平、开榫、拼板、砌门框、装合页、开锁孔、贴密封条。假设你手艺不错,一天能装一扇。要装八扇,八天,而且八扇的手感、缝隙、开合角度多少会有差别。这就是"没有控件"的世界:你在屏幕上画三十个按钮,就得把"画矩形、写文字、判断鼠标在不在里面、按下变色、松手触发"这套动作手写三十遍。

去装修市场买成品门就完全不同。你只说三件事:多宽、什么颜色、往哪边开。剩下所有东西厂里已经做完了:合页承重算好了,锁舌位置对好了,密封条压好了,木料留了膨胀余量所以夏天不卡。你花二十分钟往墙洞里一装就完事。这就是控件:你只声明差异(文字、颜色、点了干什么),共性部分全世界共享一份。

再往深一层,这个类比还解释了三件事。

第一,为什么"外观和行为必须打包"。成品门卖给你的不只是一块板,还包括"怎么转、转多少度会挡住、关上会不会响、锁上以后从外面推不动"这一整套行为。只卖板不卖行为,那叫木料,不叫门。同理,只画一个圆角矩形,那叫图形,不叫按钮。

第二,为什么控件要有那么多"状态"。一扇合格的门至少有:关着、半开、全开、锁死、把手被压下的瞬间。少了任何一个状态的处理,用户就会疑惑"它到底关上了没有"。按钮的默认、悬浮、按下、禁用、焦点五张脸,是同一个道理。

第三,为什么不建议自己造。你自己做的门第一天也能开合,但一个月后开始刮地、关不严、锁不上——因为你没考虑木料膨胀、门体下沉、五金磨损。控件也是:能显示能点击只是第一天的事,禁用态、超长文字、深色模式、键盘可达、中文输入法,是接下来三个月的事。成熟控件库的真正价值,从来不在"能用",而在"这些坑它都替你踩过一遍了"。

Recap · 收束

控件是把外观和行为打包在一起的可复用界面零件,按职责分为输入、展示、容器、导航、反馈五类。
理解任何控件都可以用三要素模型:外观(长什么样)、状态(现在怎么样)、行为(怎么响应你);而三者的流向是单向的——操作改状态,状态定外观。
控件通过容器层层嵌套成控件树,坐标、样式、事件都顺着这棵树传递。
原生与自绘之间选择,本质是在"融入系统"和"保持品牌"之间做取舍。
最后别忘了:一个控件至少有五张脸——默认、悬浮、按下、禁用、焦点。真正专业的界面,差距往往就藏在这些"非默认状态"里。

☰ 主页
学海无涯 · 界面篇 · § 1.4