资讯动态

TouchGFX自定义容器:封装可复用键盘控件的完整实践

发布时间:2026/8/30 7:53:59 来源:尧图企业网站定制
接到新需求不难难的是同一个需求反复来。做嵌入式触屏产品的人应该都有体会屏幕就那么大点数字密码、工程名、服务器地址、校准参数哪个界面都可能要弹个键盘出来。我早先在TouchGFX里做第一个界面的输入功能时直接就是在界面上拖了一堆Button一个一个绑定回调写完还挺得意。直到第二个界面也要用键盘复制粘贴过去改了半天逻辑不说还因为少了两个按键差点没赶上测试节点。那时候我才意识到这个屏上键盘从一开始就该做成独立控件。后来我改用Custom Container把整个键盘封装起来内部自管按键布局和事件分发外部只对接一个回调接口。再往后的三四个界面需要输入时基本都是拖进去、接上回调、完事。这篇文章就把整个过程的思路、代码细节和踩过的坑摊开讲给同样在TouchGFX里做触屏交互的朋友一个可以直接抄作业的参考。1. 为什么是Custom Container从“临时拼凑”到“可复用控件”1.1 直接在Screen上堆Button的三大痛点先说我在早期项目里的实际体验。当时做的是一个工业设备的参数设置界面需要一个数字键盘来输IP地址。我的第一版实现很直接在Screen上放了12个按钮10个数字、一个小数点、一个退格每个按钮都在View里写一个回调方法比如btn0Clicked()、btn1Clicked()。屏幕下方放了一个TextArea作为输入框每个回调里拿到对应的字符追加到输入框缓冲区后invalidate()刷新。这套方案在第一个界面上跑通没问题但代码丑得不行十几个数字回调函数堆在一起逻辑上是一个键盘代码上却是完全割裂的。最要命的是后续两个变化第一个变化是第二个界面也要一个键盘但按键布局不同多了网段分隔符和回车键。我把上一个界面的按钮复制过来回调逻辑重新接了一遍花了半天。第二个变化是屏幕适配客户换了块更窄的屏按键间距要调整结果我在两个Screen里同时改了十几个按钮的坐标漏改了两个出货前才发现。说白了直接在Screen层堆控件的方式把“键盘”这个有完整业务逻辑的东西拆散成了一个一个没有生命力的按钮。任何布局调整、事件变更、界面复用都要动到Screen层代码。维护成本会随着界面数量线性增长甚至是乘法增长。1.2 Custom Container封装的边界在哪Custom Container本质上就是一个继承自Container的控件但它可以像Screen一样在TouchGFX Designer里可视化编辑内部能放任意数量的子控件还能有自己的交互逻辑。这就让它具备了“控件级复用”的能力我们可以把一个完整的键盘当成一个普通Button控件一样拖到任何一个Screen里使用。那封装的边界划在哪里我的原则是一切跟“键盘自身”相关的东西都留在容器内部一切跟“调用方”相关的东西都通过接口暴露出去。具体来说按键外观、布局、坐标、间距全部在Custom Container内部定义。外部只管放这个容器不用关心键盘长什么样。按键点击后输入了什么字符、是普通字符还是回车还是退格这个“键值语义”在容器内部定义。外部通过回调拿到一个字符值即可。是否显示Shift状态、是否处于数字模式、退格键能不能长按重复删除这些都是内部状态外部不感知。外部需要关心的只有一个用户按了哪个键拿到对应的键值决定怎么处理。这个边界划清楚以后键盘就变成了一个“输入设备”跟物理键盘在人机交互层面的定位是一样的。物理键盘后面连着主机但键盘本身不会去管主机上的文本缓冲、焦点框、输入法状态我们的Custom Container键盘也应该这样。1.3 可复用性的三条衡量标准不是把控件做成了Custom Container它就自动可复用了。我后来总结出三条比较实在的衡量标准用来判断一个自定义容器做得到不到位第一把容器拖进一个新Screen后除了绑定一个回调函数之外新界面的代码改动量有多少。如果还需要改容器的内部代码、调整按键坐标、加删按钮那就没有真正做到可复用。理想状态是零改动。第二同一个界面上同时放两个键盘实例时能不能正常工作。很多看似封装好的控件一旦有两个实例静态变量、全局回调指针就打架。如果做不到多实例共存那说明内部状态管理还有问题。第三换一个屏幕分辨率或换一套按键样式时容器的使用方式是否保持不变。复用的最高境界是不仅代码能复用UI资源和尺寸也能通过配置来复用。如果换屏就要改代码那只是“半复用”。这三条标准我在后文讲设计时都会反复用到也算是自己做判断的尺子。2. 设计一个可复用键盘容器布局、映射与回调缺一不可2.1 先想清楚键盘要输入什么按键布局与字符映射动手在Designer里拖控件之前先想想这个键盘在哪些场景下会被用到。不是所有输入场景都需要一个完整的QWERTY键盘这会把屏幕占满也浪费开发时间。我一般把嵌入式屏上键盘分成三类纯数字键盘0-9、小数点、退格、确认。用于IP地址、端口、数量、密码等场景按键少、布局紧凑是使用频率最高的一类。全功能字母键盘字母A-Z、数字、大小写切换、空格、退格、回车。用于工程命名、备注输入、账号密码等场景。定制键盘比如十六进制输入0-9、A-F、MAC地址输入带冒号分隔符、或者带特殊符号的密码键盘。这类键盘按键内容由业务决定。设计Custom Container时我建议在一个容器里把三类键盘的“能力”都做出来按键模块化组合通过外部接口切换模式。比如同一个KeyboardContainer里放一组字母键、一组数字键、一组功能键通过setKeyboardMode(Mode)来切换显示哪些按键隐藏哪些按键。这样不同场景复用的是一个容器而不是复制出三个容器各自维护。字符映射这一块是关键。按键在UI上是编号的比如btnKeyA、btnKeyB但按键背后的“值”一定要和数据分离。我习惯在容器内部维护一张映射表// 数字键盘模式 static const uint8_t NumKeys[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, ., 0, BACKSPACE_KEY, ENTER_KEY }; // 字母键盘模式 static const uint8_t AlphaKeys[] { a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z, SHIFT_KEY, BACKSPACE_KEY, ENTER_KEY };之所以要显式定义映射表而不是让每个Button的回调里写死一个字符是为了把“按键物理位置”和“按键逻辑值”解耦。后面如果要切换键盘布局、支持多语言、调整按键顺序只需要改这张表而不需要去动代码逻辑。这里有一个经验映射表中的“特殊键”要定义成常量不要用字符直接硬编码否则在判断时很容易漏掉边界情况。我自己在项目里定义了BACKSPACE_KEY、ENTER_KEY、SHIFT_KEY、ESC_KEY这些枚举值并保证它们不会和普通字符的ASCII码冲突。用uint8_t装ASCII字符时预留几个高位值给特殊功能键就行。2.2 事件回调机制如何把“按下某个键”这件事安全地告诉外界键盘容器内部管理了所有按键的点击事件但最终“用户按了什么键”这件事要通知到外部Screen。TouchGFX里最常用的机制是Callback。我给键盘容器设计的外部接口非常简单就三个方法class KeyboardContainer : public KeyboardContainerBase { public: KeyboardContainer(); // 设置按键监听器外部通过这个回调接收键值 void setKeyListener(touchgfx::GenericCallbackuint8_t callback); // 切换键盘模式 void setKeyboardMode(Mode mode); // 显示/隐藏键盘带滑入滑出动画 void showKeyboard(); void hideKeyboard(); private: touchgfx::GenericCallbackuint8_t* keyListener; };内部任何一个按键被点击时统一走一个分发函数void KeyboardContainer::dispatchKey(uint8_t key) { if (keyListener keyListener-isValid()) { keyListener-execute(key); } }在TouchGFX里GenericCallback是模板类用法比较简单外部Screen这样绑定// MainView.hpp class MainView : public MainViewBase { private: CallbackMainView, uint8_t keyReceivedCallback; void handleKeyReceived(uint8_t key); }; // MainView.cpp 构造或者setupScreen里 keyReceivedCallback CallbackMainView, uint8_t(this, MainView::handleKeyReceived); keyboard.setKeyListener(keyReceivedCallback);这里我在实际项目中吃过一次亏特别提醒一下不要在Custom Container的构造函数里把回调执行出来也不要让回调函数去操作UI控件。回调是从UI事件里触发的TouchGFX的单线程事件循环中回调里做重操作会导致界面卡顿。回调里应该只做简单逻辑比如记录键值、给文本缓冲追加一个字符、打一个标记等。2.3 显示与交互状态的内部管理键盘内部需要管理不少状态比如Shift是否按下、当前是字母模式还是数字模式、退格键长按重复的开关等。这些状态必须作为容器的成员变量不能在容器外部零散管理。我之前见过一个同事的做法把Shift状态放在Screen的成员变量里每次按Shift键后由Screen去通知所有字母Button更新显示文本。这个方案在单个界面可用但一旦两个界面都要用这个键盘就麻烦了——状态散落在两处逻辑也会重复写两遍。正确做法是把Shift状态收进键盘容器内部void KeyboardContainer::handleShiftPressed() { shiftActive !shiftActive; // 更新字母按键上的显示文本 for (int i 0; i LETTER_KEY_COUNT; i) { letterKeyTexts[i].setTypedText( shiftActive ? TypedText(T_ALPHABET_UPPER) : TypedText(T_ALPHABET_LOWER) ); letterKeyTexts[i].invalidate(); } }如果是用Button控件按钮上面有文本框的话切换大小写就更新文本并invalidate()。如果是用图片按键切换大小写就需要替换按钮的背景图。这里我也推荐一个细节按键的“按下态”和“释放态”要区分清楚视觉反馈在触屏设备上特别重要用户按下去如果没有任何视觉变化会以为没按到连续点好几次体验非常差。退格键的长按重复也是个容易被忽略的点。TouchGFX框架里有个RepeatButton控件按住不放会周期性触发点击事件。在键盘容器里把退格键做成RepeatButton用户长按删除时就不用一下一下点。而普通字符键做成常规Button避免误触时连续输入一堆字符。3. 手上过一遍TouchGFX Designer里的完整落地流程3.1 在Designer里创建KeyboardContainer第一步在TouchGFX Designer的“Containers”面板里添加一个Custom Container命名为KeyboardContainer。Designer会生成KeyboardContainerBase和KeyboardContainer两个类前者是UI生成代码每次资源变更都会重新生成后者是我们的业务逻辑层Designer不会覆盖它。然后往容器里拖控件。我的建议是先在Designer里画出一个大致的布局再根据实际屏幕尺寸微调坐标。比如设计一个15键数字键盘用三行四列第一行1、2、3、4第二行5、6、7、8第三行9、0、小数点、退格第四行左侧空白、回车。每个按钮命名要有规律比如btnKey1、btnKey2、btnKeyDot、btnKeyBackspace。命名规律化能避免后面写映射表时对不上号。布局时有一个小技巧先用Designer的“相等间距”工具把第一行摆好然后用“水平居中”和“垂直间距相等”功能复制出其他行。手动逐个调整坐标容易在换屏时出问题不如直接用Designer的对齐工具后续调整容器大小时所有按钮仍然保持相对位置。3.2 按键交互的绑定与代码生成在Designer中每个Button都可以配置Interaction。右键按钮选择“Add Interaction”把Trigger设为ClickResponse设为Call new virtual method然后给方法起个名字比如btnKey1Clicked()。Designer会在基类里生成对应的虚方法// KeyboardContainerBase.hpp virtual void btnKey1Clicked();我们只需要在KeyboardContainer.cpp里实现这个虚方法void KeyboardContainer::btnKey1Clicked() { dispatchKey(1); }这样一对一的绑定简单直白。但请注意如果按钮很多每个按钮一个虚方法代码量会很大。我有一个折中方案所有数字键都复用同一个处理方法在Designer里调用同一个virtual methoddispatchKey的哪个字符由按钮的映射表决定。比如数字键命名统一为btnKeyNumberX在构造时把一个数组初始化好// KeyboardContainer.cpp 构造函数里 numberButtons[0] btnKeyNumber0; numberButtons[1] btnKeyNumber1; // ... numberKeys[0] 0; numberKeys[1] 1; // ...点击任何一个数字键都走btnNumberClicked()然后用一个循环找到这个按钮在数组里的索引再从numberKeys[]里取出键值。这种方法比每个按钮一个虚方法省很多代码也方便通过修改数组来改变按键布局。3.3 在Screen中接入键盘监听器与文本处理键盘容器做好了放到Screen里接下来就是给它“通电”绑定监听事件、处理文本显示。在MainView里setupScreen阶段要做三件事void MainView::setupScreen() { // 1. 绑定键盘回调 keyReceivedCallback CallbackMainView, uint8_t(this, MainView::handleKeyReceived); keyboard.setKeyListener(keyReceivedCallback); // 2. 设置输入框缓冲区TextArea显示我们指定的内存区域 inputTextArea.setBuffer(textBuffer); // 3. 清空缓冲区保证初始状态干净 Unicode::strncpy(textBuffer, , TEXT_BUFFER_SIZE - 1); }handleKeyReceived是所有按键事件的总入口void MainView::handleKeyReceived(uint8_t key) { switch (key) { case BACKSPACE_KEY: { uint16_t len Unicode::strlen(textBuffer); if (len 0) { textBuffer[len - 1] 0; } } break; case ENTER_KEY: // 确认输入做后续业务处理 confirmInput(); break; default: { uint16_t len Unicode::strlen(textBuffer); if (len TEXT_BUFFER_SIZE - 2) { textBuffer[len] key; textBuffer[len 1] 0; } } break; } inputTextArea.invalidate(); }这里我对TouchGFX的文本机制多说两句。TouchGFX的TextArea展示的是Unicode::UnicodeChar数组不是传统C语言里的char数组。大多数情况下UnicodeChar是16位跟char混用时一定要小心。我们的映射表里存的键值是uint8_t的ASCII在default分支里直接赋值给textBuffer[len]时数据大小是兼容的但要保证键值确实落在ASCII可打印字符范围内不要混入特殊控制字符。文本缓冲区更新后必须调用invalidate()让框架知道这部分区域需要重绘。不调用invalidate()是最常见的“键盘按了没显示”的原因。3.4 复用到新界面的实际操作真正体现复用价值的时刻到了。现在第二个Screen也要用键盘比如一个设备命名界面需要一个全键盘。操作流程非常简单在Designer里从Containers面板把KeyboardContainer直接拖进新Screen。在新Screen的View里同样声明一个CallbackNewScreenView, uint8_t绑到容器的setKeyListener上。在新Screen的回调里实现对应界面的文本处理逻辑比如写入到另一个TextArea。整个过程不需要修改键盘容器的任何内部代码也不需要重新布局按键。如果新界面需要字母键盘就调用setKeyboardMode(KeyboardContainer::ALPHA_MODE)切换到字母布局如果不需要数字模式就只显示字母部分。所有键盘状态都在容器内部统一调度不会因为换了个界面就“失忆”。使用这种方案我在一个项目里维护了四个界面总共只写了一份键盘逻辑。后续改样式、换颜色、调整按键间距都是在容器内部改一处四个界面同时生效。4. 进阶让键盘模块跨工程复用4.1 把键盘容器做成一个独立模块同工程内复用只是第一步真正赚到的是跨工程复用。嵌入式产品的屏幕需求往往很相似今天做仪表盘要键盘明天做控制器也要键盘。把键盘容器抽成一个独立模块可以在新项目里直接使用。我在实际项目中会把以下文件放到一个独立目录比如components/Keyboard下KeyboardContainer.hpp/KeyboardContainer.cpp我们写的业务逻辑KeyboardContainerBase.hpp/KeyboardContainerBase.cppDesigner生成的基础类键盘用到的字体资源typographies键盘按键用到的图片资源textures注意一点TouchGFX的资源文件assets目录里的fonts和images在Designer工程里是统一管理的。跨工程复用时要么把资源也一并拷贝到新工程的assets目录要么在新工程里重新引用一遍。我倾向于把键盘的字体和图片单独命名比如Keyboard_Font_Regular、Keyboard_Key_Blue这样在新工程里一眼就能看出哪些资源属于键盘模块。4.2 跨工程复用的注意事项跨工程复用最容易踩的坑是版本不一致。TouchGFX各个大版本之间Callback、Container、TextArea这些API的细节有差异我遇到过4.18工程里好端端的代码升级到4.21之后编译报错的情况。所以跨工程复制时尽量保证新工程的TouchGFX版本和原工程一致或者至少是主版本号一致。还有一个容易出错的地方是TouchGFX Designer的自动生成机制。KeyboardContainerBase这个文件是Designer在每次生成代码时重写的所以跨工程复制时要连Designer生成的generated目录里的相关文件一起复制否则新工程里资源引用对不上。用下来跨工程复用大概能省掉整个键盘控件开发时间的一半以上。第一次做一个完整键盘可能需要一周包括布局、字体、交互、动画第二次在新工程里直接用基本就是半天内搞定接入和联调。这在项目周期紧的时候能省出一块非常可观的缓冲时间。5. 实操中常见的坑与排查技巧5.1 点击没反应Touchable与Z序问题键盘放在Screen上点击按键没反应这个问题我遇到太多次了。多数情况下是这两个原因之一第一个原因是容器没有收到触摸事件。Container默认是Touchable的但如果开发过程中不小心把容器的Touchable属性设成了false或者自定义容器内部某个透明遮挡区域没有正确处理触摸事件就传不下去。排查方法很简单在Designer里选中KeyboardContainer检查属性面板里Touchable是否为true。第二个原因是键盘被其他控件盖住了。TouchGFX的绘制顺序是从后往前后加入的控件会盖住先加入的控件。如果键盘上方还有一个全屏的透明容器哪怕它透明只要它的Touchable是true就会把触摸事件抢走。这种情况处理方式是把键盘容器bringToFront()或者在Designer里调整Z序让键盘位于顶层。还有一个比较隐蔽的情况Screen里如果有ModalWindow或者ToastView这类悬浮层它们关闭后没有正确释放触摸焦点也会导致后续点击事件不往下传。这种问题不好定位我的排查方法是先把所有动态创建的悬浮层都注释掉确认键盘能正常工作后再逐个打开快速定位是哪个控件导致。5.2 回调不触发、触发两次或延迟回调不触发先检查setKeyListener有没有在setupScreen里被调用。TouchGFX的界面生命周期比较复杂setupScreen在每次进入Screen时都会被调用如果你把绑定放到了构造函数里可能因为对象创建顺序问题导致回调没有被正确注册。回调触发两次最常见的原因是在两个地方绑定了同一个回调。比如在构造函数里绑了一次在setupScreen里又绑了一次。setKeyListener是覆盖式赋值第二次绑定时会替换掉第一次看起来没什么问题但如果你用的不是覆盖式而是“添加式”监听器就会重复触发。我自己的经验是setKeyListener要做成赋值操作并且每次在setupScreen里都重新调一遍保证界面状态正确。回调延迟多半是动画阻塞了UI线程。TouchGFX的动画比如MoveAnimation在每一帧都在做插值运算如果动画时间过长或者同时动画的控件太多点击事件的响应就会被延迟。键盘出现的动画我建议控制在150-250ms之间不要超过300ms否则就会感觉到明显的“点了没动静”。5.3 编辑框被键盘遮挡的处理屏幕就那么点大键盘弹出来编辑框很容易被挡住。我的处理方案是不让编辑框和键盘同时出现在屏幕底部而是把编辑框布局在键盘上方并且保证键盘显示时编辑框滚动到可见区域。实际代码里我在showKeyboard()方法里同时启动一个编辑框的MoveAnimationvoid MainView::handleFocusInput() { keyboard.showKeyboard(); inputArea.moveTo(inputArea.getX(), keyboard.getY() - inputArea.getHeight() - 10); inputArea.invalidate(); }如果界面内容比较多不只是编辑框一个控件可以考虑用一个ScrollableContainer把整个内容区包起来键盘弹出时把内容区向上滚动到键盘可见。这个实现方式复杂一点但在内容较多的表单页面确实更实用。5.4 内存与重绘性能优化嵌入式设备的内存非常宝贵。TouchGFX界面上每一个控件都要占用RAM一个键盘几十个按键每个按键的坐标、状态、绘图相关数据都要占内存。我建议只在当前需要显示的界面实例化键盘而不是在多个Screen的构造函数里各自放一个键盘。这样切换界面时键盘占用的内存可以被释放让给其他界面。重绘性能也要关心。低主频的MCU上如果每次按键都触发全屏invalidate()界面会明显闪烁。正确做法是只对变化区域invalidate()。比如按下一个数字键只有那个数字键的按下态变化和输入框文本变化需要刷新其他地方不重绘。用TouchGFX的invalidateRect(Rect)可以精确控制重绘区域。5.5 问题排查速查表现象可能原因排查方向点击按键无反应容器Touchable为false检查Designer属性面板点击按键无反应键盘被上层控件遮挡调用bringToFront()或调整Z序按键能点但输入框没字TextArea没有调用invalidate()检查文本更新后的重绘输入框文字乱码UnicodeChar和char混用检查textBuffer类型和键值类型回调触发两次回调被绑定两次检查构造函数和setupScreen中的绑定键盘弹出卡顿动画时间过长或invalidate范围过大精简动画限制重绘区域退格删除不了字符退格键没有走BACKSPACE_KEY分支检查映射表与switch逻辑切换到字母键盘后按键没变按键显示文本未更新Shift或模式切换后要调用invalidate()排查最多的还是前两行Touchable和遮挡。我处理类似问题时有一个固定套路先把键盘的Touchable属性单独检查一遍然后把它置顶再试点击。如果依然不行就把Screen里的其他容器挨个排查很快就能定位。写在最后的一点体会做Custom Container这件事最大的收获不是省了多少行代码而是把“输入框和键盘”这个在触屏设备上反复出现的交互模式固化成了一个可以随处放置的独立模块。每次接到新界面需要文本输入的需求我只需要拖一个键盘、绑一个回调剩下的事情都由容器自己处理。前期多花的那两天设计时间早就被后面几个界面的开发效率找补回来了。从实际操作来看我建议做的时候一定要把格局打开按键映射表、键盘模式和回调接口都做成可配置的不要图省事把某个界面的特殊逻辑写死在容器里。这样后续换产品、换屏幕尺寸、换交互样式付出的是参数调整成本而不是重写成本。如果你也在TouchGFX里被键盘这种“看起来简单做起来琐碎”的控件折磨过照着这个思路做一个自己的Custom Container会很值得。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价