最近在做一个工业触摸屏项目,界面里要输入温度、转速、长度这类纯数值参数。当时第一反应是直接用系统软键盘,结果被现实狠狠教育了一顿:嵌入式 Linux 的镜像里根本没有可用的输入法,装了虚拟键盘又小又难看,按键戴着手套根本点不中。最后只能自己动手,用 Qt 从头写了一个纯数字虚拟键盘。
这篇分享打算把整个来龙去脉讲清楚,包括为什么不用现成方案、为什么选 Widgets 而不是 QML、键盘本体怎么搭、按键信号怎么准确送到输入框,以及嵌入式触摸屏环境下一堆实测踩坑记录。内容偏向可复现的工程细节,适合正在做 HMI、工控上位机或任何需要自定义数字键盘的 Qt 开发者。如果你也卡在“系统软键盘不可控”这个点上,重点看第五、六节,能少走不少弯路。
1. 为什么自研纯数字键盘:系统软键盘在工控场景的短板
1.1 先明确“纯数字键盘”到底指什么
很多朋友一听“软键盘”,第一反应是调系统输入法,或者搜一个虚拟键盘库装进去。但工控触摸屏里说的“纯数字键盘”,其实是一套按实际需求裁剪过的输入面板:只保留 0-9 数字键、小数点、删除、清空、确认这些键位,没有字母,没有符号切换,甚至连正负号都可能不需要单独做逻辑。
在我那个项目里,操作员需要输入的数值有几个非常鲜明的特点:只允许数字和小数点,部分字段还有小数位数限制;操作员普遍戴手套,按键热区必须比手机键盘大很多;键盘需要在主窗口内随时切入切出,不能让系统唤出一个和整体 UI 风格完全割裂的小面板。把这几条翻译成需求后,系统软键盘一个都满足不了。
1.2 系统软键盘在嵌入式 Linux 上的三个短板
第一,界面不可控。触摸屏上如果真有系统软键盘,多数是 X11 环境下的老式虚拟键盘或者应用容器自带的简易面板。按键大小、配色、键位布局都是别人定的,没办法为戴手套的场景把按键调大,也没办法把“小数点”和“00”这种在工业场景里高频使用的键单独突出。
第二,信号拦截难。系统软键盘往往通过全局事件注入来模拟按键,QLineEdit 会收到一堆键盘事件,但开发者很难判断哪些是真实物理按键、哪些来自软键盘。一旦输入法接管了 Ctrl、Shift 这类修饰键,业务层状态很容易被带偏,测试时真机行为和模拟器行为还经常不一致。
第三,平台没得选。我实际交叉编译时发现,很多 ARM 板子的 Linux 镜像根本没有安装输入法框架,连最基本的软键盘程序都不存在。项目不可能为了输入几个小数,先去给整个系统补一个输入法框架,维护成本直接失控。
1.3 为什么不靠现成第三方控件库
市面上不是没有第三方数字键盘控件,但引入一个外部库意味着要评估它的依赖树、授权协议和更新维护情况。为了一个数字输入场景背这些额外成本,在工程上非常不划算。自己做一个键盘控件,核心代码范围可控,出了问题随时能断点调试,跨平台编译也不会有惊喜。
2. 方案选择与整体结构:Widgets + QSS 搭出轻量键盘
2.1 Widgets 还是 QML:我为什么没有用 QML
技术选型时,QML 的确是很多人推荐的方案,做动画方便、组件风格现代。但数字键盘的交互本质就是“按一下,发一个信号”,没有转场动画,没有复杂的异步视图,没有需要绑定实时数据的动态列表。为此把 QML 运行时和资源管理逻辑引入工程,对目标 ARM 板子来说有点得不偿失。
我对两个方案做了对比:
| 对比维度 | Widgets + QSS | QML |
|---|---|---|
| 运行时依赖 | 轻,只用 Qt Widgets 模块 | 需要 QML 运行时,体积明显增加 |
| 键盘动态绘制 | QSS 足够,改颜色即时生效 | 能力更强,但用不上 |
| 焦点管理 | 用 QApplication::focusWidget() 很直接 | 需要管理 QML 内部焦点和 Qt 焦点两层结构 |
| 维护成本 | 纯 C++,跨版本迁移方便 | 新增 QML 文件和 JS 逻辑,排查链路变长 |
最终我选了 Widgets,工程接起来最省事,行为也最好预测。嵌入式项目里,稳定和可控永远优先于表达形式的花哨。
2.2 做成独立顶层窗口,还是普通 Widget
确定 Widgets 之后,下一个问题是键盘的表现形态。这里两条路线很典型:一条是把键盘做成独立顶层窗口,show 后浮在主窗口之上;另一条是把键盘做成普通 QWidget,嵌入主界面的布局管理器里。
独立窗口的优势是可以在任意时刻置顶,尤其适合那种键盘必须贴着输入框弹出的场景。但这套方案在嵌入式系统上有一个致命弱点:很多 ARM 板子的合成器对多层窗口的刷新支持不理想,置顶窗口频繁 show/hide 时会出现闪烁,甚至露白。而且独立窗口一旦焦点处理不当,点击键盘按钮会先把焦点从输入框抢走,输入框立刻失焦,又触发隐藏键盘的逻辑,出现“点一下键盘就消失”的诡异现象。
我最后选择的是普通 Widget 嵌入方案。键盘作为主窗口底部的一部分,平时隐藏,需要输入时通过布局项 show 出来。这样不存在窗口层级问题,焦点切换也简单得多。从使用者视角看,键盘稳稳待在屏幕底部,不悬空、不遮挡其它导航控件,视觉上更像“设备自带的一个输入面板”。
2.3 整体代码模块划分
为了让这套逻辑不至于全塞在一个类里,我按职责拆成了三个部分:
- NumKeyboard:键盘面板,负责生成按键、处理按压状态、发出统一的按键信号。它不关心当前输入框是谁。
- NumKeyManager:控制器,负责监听焦点变化,记录当前输入目标,接收 NumKeyboard 的信号并执行实际的内容插入。它是整个键盘的中枢。
- KeyOptions:输入约束,一个轻量数据类,记录整数位数、小数位数、是否允许小数点等信息,绑定到目标输入框上。
类之间的关系非常直接:界面上的输入框触发 FocusIn,NumKeyManager 通过事件过滤器感知到,于是让 NumKeyboard 显示;操作员按任意键,NumKeyboard 发出 keyPressed(KeyType, QString),NumKeyManager 拿到信号后处理插入、删除、清空等动作。整个链路里,键盘本身不持有任何业务数据,天然解耦。
3. 键盘控件实现:布局、按键状态与 QSS 细节
3.1 用 QGridLayout 做自适应性布局
数字键盘的按键很多,但基本都是等宽等高的小矩形。这时候如果用绝对坐标摆按钮,换分辨率就得重新算一遍,非常痛苦。QGridLayout 的 rowStretch 和 columnStretch 能让我用“比例”而不是“像素”来约束布局。
我的布局方案是 4 列 x 5 行,键位如下:
| 列1 | 列2 | 列3 | 列4 |
|---|---|---|---|
| 1 | 2 | 3 | Del |
| 4 | 5 | 6 | Clear |
| 7 | 8 | 9 | Enter |
| . | 0 | 00 | Enter(跨行) |
实际编码时用 QGridLayout 把按钮塞进格子,然后统一设定间距和拉伸系数:
QGridLayout* grid = new QGridLayout(this); grid->setSpacing(6); for (int i = 0; i < 5; ++i) grid->setRowStretch(i, 1); for (int j = 0; j < 4; ++j) grid->setColumnStretch(j, 1); // 添加按钮时直接指定行列和跨行跨列 grid->addWidget(delBtn, 0, 3); grid->addWidget(clearBtn, 1, 3); grid->addWidget(enterBtn, 2, 3, 3, 1);这样做的最大好处是键盘在 800x480 和 1920x1080 两种分辨率下不用改任何代码,按钮永远按比例铺满面板。同时布局管理器天然支持窗口缩放,不用手动处理 resizeEvent。
3.2 键位设计里的工业细节
数字键盘的核心键位用脚趾头都能想出来,但有几个键是纯粹从实际使用里逼出来的。
一个是“00”。设备参数里经常输入像 1000、2000 这种整百数值,如果没有 00 键,操作员要按三次 0,效率太低。加了 00 键后,按一个键就输两个 0,对戴手套操作的人来说体感差别非常大。另一个是小数点。很多数显设备的默认温度、液位值是带小数的,小数点如果藏在符号切换菜单里,操作员会崩溃。所以我一律把小数点直接放在主面板上。
删除和清空必须分开。删除是逐字符回退,清空是整栏重置。一开始偷懒只做了删除键,后来发现操作员输错时更希望直接清掉重来,于是把清空键加了回去。这个键位取舍其实反映了一个问题:虚拟键盘不是手机键盘的移植,而是目标用户操作习惯的映射,设计之前一定要把现场输入流程想清楚。
3.3 按下反馈:触摸屏上不能只靠 QSS :pressed
刚写第一版时,我给按钮 QSS 加了 :pressed 伪状态,按下变亮,松开恢复。实机测试发现触摸屏上会有迟滞,尤其在低性能板子上,反馈总感觉慢半拍。原因是触摸事件到样式刷新存在延迟,有时候手指已经抬起来了,高亮状态还没切回。
后面改成了用动态属性 + 主动样式刷新来做按压反馈。每个按钮响应按下事件时,设置一个 pressed 属性为 true,然后调用 style()->unpolish 和 style()->polish 强制刷新;松开时再属性改回 false 并刷新一次。处理逻辑放在键盘面板里统一做,而不是每个按钮各自重写:
bool NumKeyboard::eventFilter(QObject* obj, QEvent* ev) { if (auto* btn = qobject_cast<QPushButton*>(obj)) { if (ev->type() == QEvent::MouseButtonPress) { btn->setProperty("pressed", true); btn->style()->unpolish(btn); btn->style()->polish(btn); } else if (ev->type() == QEvent::MouseButtonRelease) { btn->setProperty("pressed", false); btn->style()->unpolish(btn); btn->style()->polish(btn); } } return QWidget::eventFilter(obj, ev); }这样反馈是即时且确定的,不受 QSS 状态伪类刷新机制的限制。体感上按键非常跟手。
3.4 QSS 细节:用动态属性区分键位样式
数字键盘里按键类型至少有数字键、小数点键、删除键、清空键、确认键五种。如果在代码里逐个 setStyleSheet,一方面冗余,另一方面后期调整配色要改一大堆。我在按钮创建时直接设置一个 keyType 属性,QSS 里用属性选择器选择样式。
QPushButton[keyType="digit"] { background-color: #3A3F47; color: #FFFFFF; border: none; border-radius: 6px; font-size: 24px; } QPushButton[keyType="function"] { background-color: #F0A030; color: #FFFFFF; border: none; border-radius: 6px; font-size: 20px; } QPushButton[keyType="digit"][pressed="true"] { background-color: #5A6070; } QPushButton[keyType="function"][pressed="true"] { background-color: #C57E1A; }这里有个非常容易忽略的点:即使 setProperty 改了值,如果 QSS 样式表里使用的选择器没有被重新计算,界面不会立即更新。所以前面我在 eventFilter 里手动调 unpolish/polish,就是为了强行触发样式系统重新匹配属性。纯代码里 setStyleSheet 常见,但动态属性配合 QSS 后必须主动刷样式,这是我实测后印象很深的一条经验。
4. 信号链路与焦点管理:按键怎么准确进入输入框
4.1 输入目标不靠“把信号连到某个输入框”
最容易犯的设计错误,是键盘显示时把 keyPressed 信号用 lambda 连到当前输入框上,键盘隐藏前再断开。这种做法在两个输入框之间来回切换时很容易漏连或错连,QObject::connect 解耦也麻烦,时间长了代码变得一团浆糊。
我的做法是控制器统一处理。NumKeyManager 自己维护一个 currentEdit 指针,键盘发信号时直接向这个指针操作:
void NumKeyManager::onKeyPressed(KeyType type, const QString& text) { if (!currentEdit) return; switch (type) { case KeyType::Digit: insertValidText(currentEdit, text); break; case KeyType::Dot: insertDotIfAllowed(currentEdit); break; case KeyType::Delete: currentEdit->backspace(); break; case KeyType::Clear: currentEdit->clear(); break; case KeyType::Enter: emit enterPressed(currentEdit); hideKeyboard(); break; } }currentEdit 的来源是焦点监听。在 eventFilter 里检测到任意 QLineEdit 的 FocusIn,就把它设为 currentEdit;检测到 FocusOut 并且新焦点不是当前键盘上的按钮,就把键盘隐藏。这个机制保证了无论界面有多少输入框,都不需要业务代码手动告诉键盘“你现在服务谁”。
4.2 合法键与校验策略:不能只靠 QLineEdit validator
数字输入的校验藏着一个坑:如果只给 QLineEdit 挂 QDoubleValidator,非法输入确实会被挡住,但用户完全不知道为什么不生效,删除键还会遇到光标卡死的问题。在屏幕上直接操作时,用户需要的是“这个键不生效”之前,系统先给出判断。
我的做法是先拼出候选文本,再用自定义规则决定是否调用 insert:
void NumKeyManager::insertValidText(QLineEdit* edit, const QString& text) { QString candidate = edit->text() + text; if (!isCandidateValid(currentOptions, candidate)) return; edit->insert(text); }候选合法性判断逻辑也很直白:
- 整数位累计不能超过设置的上限;
- 小数点只能出现一次;
- 最多允许的小数位数要限制;
- 如果当前选中的是一整段内容,直接替换而不是追加。
这套方式比 validator 实用很多,因为键盘上的按键是“自定义的信号”,我们完全有机会在进入输入框之前就把不合法的内容拦下来。操作员按了没反应时,我们还可以通过闪灯或者简短的状态提示告诉用户原因,这是原生 validator 很难做到的事情。
4.3 焦点切换与键盘驻留时序
虚拟键盘一个绕不开的麻烦点:键盘上的按钮一旦被点击,焦点就会从 QLineEdit 跳到按钮上。如果控制器在焦点变化时将“当前焦点不是输入框”当成输入完成的信号,键盘会立刻隐藏,按键事件还没发出去,交互就中断了。
网上很多方案是给按钮 setFocusPolicy(Qt::NoFocus),强制按钮不抢焦点。这是必须做的基础操作,但还不够。因为触摸点击时,即使按钮不抢焦点,某些平台仍然可能触发 FocusOut。我后来在控制器里做了延迟隐藏:收到 FocusOut 后启动一个 150ms 的定时器,如果期间收到新的 FocusIn,按下“取消隐藏”;如果没有其它焦点进来,才真正隐藏键盘。
这个 150ms 的窗口非常关键,它把“焦点短暂脱离输入框”和“用户不再需要键盘”这两个状态区分开了,从一个看似矛盾的问题里抽出了正确答案。
4.4 多个输入框不同规则:用一个登记表管理
同一个界面里,有的输入框最多 3 位整数和 1 位小数,有的只允许整数,有的甚至允许负号。这些规则如果不区分,键盘就退化成无脑把字符串塞进去,约束形同虚设。
我用了一个 QHash 把输入框指针映射到对应的输入选项上:
struct KeyOptions { int maxIntDigits = 6; int maxDecDigits = 0; bool allowDot = true; bool allowMinus = false; }; QHash<QLineEdit*, KeyOptions> m_options; void NumKeyManager::registerEdit(QLineEdit* edit, const KeyOptions& opt) { m_options.insert(edit, opt); edit->installEventFilter(this); }界面初始化的时候依次注册即可。控制器取 currentEdit 的同时,从哈希表里取得对应规则。这个表还顺带解决了“哪些输入框应该弹出数字键盘”的问题:只要注册过的输入框才显示键盘,没注册的保持系统原样。业务隔离得很舒服。
5. 嵌入式触摸场景的特殊处理
5.1 长按删除键的自动重复逻辑
数字键盘上删除键的使用频率很高,一个数可能删掉七八个字符,如果每次都要点一下删一位,效率太低。我一开始给删除键开了 QPushButton 的 autoRepeat,实测发现默认行为不理想:首次重复的延迟太长,按住大约一秒后才开始连续删,之后又太快,稍不注意就把整数位删光了。
处理方法是手动控制重复节奏。在 eventFilter 中识别按键按下时,第一次立即发一次删除,然后启动 QTimer,间隔 500ms 后开始连续触发,触发间隔保持 200ms。松开后立刻停掉定时器:
void NumKeyboard::startDeleteRepeat() { emit keyPressed(KeyType::Delete, QString()); m_deleteTimer.stop(); m_deleteTimer.start(200, this); } void NumKeyboard::stopDeleteRepeat() { m_deleteTimer.stop(); } void NumKeyboard::timerEvent(QTimerEvent* event) { if (event->timerId() == m_deleteTimer.timerId()) emit keyPressed(KeyType::Delete, QString()); QWidget::timerEvent(event); }这样操作员按住删除键时,前 500ms 只删掉第一位,之后稳定地每 200ms 删一位,直观且不容易误删。数字键我反而明确关闭了 autoRepeat,防止按住 1 时连续输出一大堆 11111,在多数输入场景里这是不希望的。
5.2 DPI 与最小点击区域
工业触摸屏的分辨率跨度极大,老的 800x480 电阻屏和新一点的 1920x1080 电容屏并存。如果代码里写死按钮最小高度 48px,在小屏上可能占掉大半界面,在大屏上又显得小气。
我直接用物理 DPI 作为参考基准:
qreal dpiRatio = qApp->primaryScreen()->logicalDotsPerInch() / 96.0; int btnMinHeight = qRound(48 * dpiRatio);基准值取的是 48 像素,实际给了 60 像素作为热区目标。原因是操作员戴手套时指尖接触面积大,48 是手机触控的最小值,工控上至少要 60 才稳。键盘整体宽度跟随屏幕宽度铺满,按钮间距保持 6px,视觉上紧凑但不拥挤。
5.3 主窗口避让与键盘遮挡
键盘嵌入主窗口底部后,输入框如果正好被键盘盖住,操作员就看不见自己输的内容了。为了保证最高的输入可见性,我在放键盘的主窗口里使用了一个占位布局:
- 主内容区使用 QVBoxLayout;
- 键盘作为布局最后一个元素;
- 键盘显示时,自动把主内容区剩余空间压缩到键盘以上的区域;
- 键盘隐藏时,主内容区恢复正常高度。
这个做法避免了两个问题:一是键盘不会盖住内容,因为布局自动避让;二是不用监听键盘高度去手动移动窗口,省掉了一堆边界处理。如果某些页面的输入框实在太多,必须靠着屏幕底部,我就在那个页面单独把键盘放进一个底部容器,做局部调整。整体思路一致,都是靠布局管理器参与避让,而不是野蛮地把整个窗口上移。
5.4 绘制性能:低端 ARM 上的涂黑与闪烁
刚开始做键盘时,我给面板加了半透明背景和圆角阴影,想在视觉上精致一点。实机一跑,发现每按一个键都有肉眼可见的闪烁,尤其按删除键连续触发时,整个键盘区域的重绘延迟非常明显。
后来把三个拖后腿的配置都去掉了:取消 WA_TranslucentBackground,阴影用纯色替换,键盘背景直接不透明。同时给键盘面板设置了 WA_OpaquePaintEvent,告诉 Qt 这块区域背景不透明,不用做复杂的混合处理。性能问题立刻缓解。这是一个很容易被忽视的优化,但嵌入式平台对透明合成的开销极其敏感,能不开透明就尽量不开。
6. 实测遇到的坑与排查记录
6.1 坑一:键盘 show 了却看不见,被主窗口盖住
第一版我确实用独立顶层窗口做的。调试时遇到一个诡异现象:调用 show() 之后,键盘在任务栏或者窗口切换列表里能看到,但在主窗口上显示不出来。排查过程分了三步:
- 先怀疑是父对象设置问题。把键盘的 parent 改为 nullptr,确认它是顶层窗口,没用;
- 再怀疑 Z 序问题。随手试了 Qt::WindowStaysOnTopHint,显示成功了,但在触摸屏上出现闪烁,而且遮挡其它弹窗的时候非常难看;
- 最后回到问题本质:嵌入式屏幕不需要一个单独的窗口来承载键盘,我为什么要造一个窗口层级问题出来?于是改成普通 Widget 嵌入主窗口布局方案,问题彻底消失。
这条经验的意义在于:很多问题不是“怎么调”而是“这个设计就不该存在”。独立置顶窗口在桌面应用里很常见,但到嵌入式触摸屏上,优先做嵌入式方案。
6.2 坑二:系统输入法面板跟我的键盘一起弹出来
接入项目时发现,点击一个 QLineEdit,我的键盘弹出来了,同时系统输入法面板也弹出来了。两个键盘叠在一起,界面瞬间乱掉。
排查时先看输入法的触发条件。Qt 对 QLineEdit 默认启用输入法支持,输入框获焦时,系统输入法框架会收到请求并弹出面板。要阻止它,最简单的是禁用输入法属性:
edit->setAttribute(Qt::WA_InputMethodEnabled, false);如果不想全局改输入框,也可以在键盘控制器里对输入框事件拦截,只在注册键盘服务的输入框上禁用。另外,我在 main 函数里关掉了输入法上下文:
qApp->setAttribute(Qt::AA_DisableShaderDiskCache); // 这条与本问题无关,只是顺带其实不需要启动系统输入法框架,只要禁用了 WA_InputMethodEnabled,点击已经注册的输入框就不会再唤起系统面板,两个键盘打架的问题立刻消失。
6.3 坑三:点击键盘按钮,键盘刚弹出就隐藏
这个坑非常典型,刚接入触摸屏时差点把我逼疯。现象是:点击输入框,键盘正常弹出;点击数字键“5”,键盘立刻隐藏。调试发现,键盘隐藏的触发点是 QLineEdit 的 FocusOut。
链条是这样的:点击“5”按钮时,按钮获得了焦点;焦点从输入框离开,控制器的焦点监听认为“输入结束”,于是隐藏键盘;键盘隐藏后按钮消失,焦点又回到输入框,但键盘已经关掉,操作员什么都没输进去。
解决动作分两层:按钮本身 setFocusPolicy(Qt::NoFocus),保证点击后焦点不转移;控制器里再做一次 150ms 的延迟隐藏,双保险。这个问题几乎是所有自定义虚拟键盘的必修课,建议一开始就把 NoFocus 写进所有键盘按钮的创建流程里,不要等测试来打脸。
6.4 坑四:触摸屏上长按触发系统上下文菜单
有一阵子客户反馈“按删除键偶尔会弹出一个奇怪小菜单”,排查后确认是触摸屏的长按行为被系统识别为鼠标右键,进而触发了 QPushButton 的 contextMenuEvent。Qt 在触摸长按时会自动合成右键事件,这在桌面平台上无所谓,在触控工控屏上就是误弹菜单。
修复方式很直接:键盘上的所有按钮都设置 setContextMenuPolicy(Qt::NoContextMenu),长按只会走我们自己的逻辑,不再触发系统菜单。如果还想适配某些设备,可以在 QApplication 层面调整触摸长按时间阈值,但是只对键盘按钮做策略设置已经足够,范围更可控。
6.5 坑五:小数点被重复插入
测试时常出现“12.5.8”这种非法数据。一开始以为是 insert 逻辑写错,后来才发现问题在于两个路径:
- 输入框自带输入法联想,点击输入框后弹出系统面板输小数;
- 用户手动又按一次键盘的小数点。
两个来源混在了一起。修复办法是收敛输入来源:既然已经强制禁用输入法,那就只保留键盘这一条输入通道。同时在控制器里判断,如果文本已经包含小数点,直接忽略再次点击小数点键的操作,不让非法字符进入候选方案。
到此,键盘的输入行为才算真正闭环。单靠 validator 拦数据,其实是在数据进入后再收拾残局;在源头把按键拦截掉,才是跟手又不吵闹的做法。
6.6 坑六:换触摸屏型号后按键样式全部失效
R&D 里换了一块新的触摸屏模组,屏幕分辨率一样,但按键的按压高亮效果在我更新的代码里莫名失效。刚开始怀疑是 QSS 加载问题,排查后定位到原因:我通过 setProperty 动态修改了 pressed 属性,却没有在属性变化后强制刷新样式。旧板子上执行顺序碰巧让样式还在,换了时序不同的平台后就原形毕露。
修复方式是把 pressed 状态变化的关键代码统一收拢,在事件过滤器里调用 unpolish/polish 强制刷新,并将这块逻辑整理成一个公共函数,防止其它地方绕过。以后再调按键反馈,只改一个函数就行。
7. 最后复盘:如果再给我一次机会,我会这么搭
这套数字键盘从最初的原型到最终稳定版本,中间折腾了不少版本。如果让我重新做,我会把几件事一开始就定下来:第一,键盘一律做成普通 Widget,绝不碰独立置顶窗口;第二,所有键盘按钮默认 setFocusPolicy(Qt::NoFocus),不用等出 bug 再加;第三,控制器和键盘界面必须拆成两个类,键盘只管发信号,控制器管输入目标和规则,这个解耦早晚都要做,早做早省心。
还有个听起来很小但实际很痛的经验:凡是改动按钮的 pressed 状态,一定要立刻 unpolish/polish,别指望样式系统自己反应过来。触控设备上,反馈延迟是灾难级别的体验问题,宁可多写一行刷新,也不要等系统慢慢悠悠地更新。
数字键盘这个需求本身不复杂,但它牵扯出来的焦点管理、事件过滤、触摸适配、嵌入式性能这些点,个个都值得仔细打磨。项目做完之后,这套代码已经成了后续所有 HMI 项目的标配控件,再遇到不同输入规则,只需注册表格里加一行,完全不用动键盘本体。如果各位在类似项目里也踩过,欢迎一起交流,我相信这类小工具在不同场景下能演化出更多有意思的细节。