简介:面向需要为触摸屏或无物理键盘设备提供文本输入方案的Qt开发者,这份压缩包提供了一套基于Qt4.8实现的软键盘工程。核心思路是通过点击LineEdit输入框弹出虚拟键盘,模拟安卓系统键盘交互方式,帮助理解和掌握自定义控件、布局管理、焦点事件与信号槽消息传递等关键机制。压缩包共20个文件,体积仅39KB,包含5个cpp源码、5个o编译中间文件、4个头文件以及2个ui界面文件等,其中.pro与Makefile可支撑重新构建,ui与cpp/h组合便于直接查看界面设计和逻辑实现。已有1466人学习下载,适合初学者对照工程源码逐步梳理软键盘的显示隐藏流程,上手后也能快速迁移到其他支持Qt4.8的平台。资源简洁完整,是一个轻量但能有效说明虚拟键盘实现原理的参考案例。
1. 为什么需要在Qt4.8里自己做一个软键盘
1.1 项目背景:一台没有物理键盘的触摸屏设备
接手这个项目时,我拿到的压缩包是“qt4.8软键盘(通过点击输入框呼出).zip”,解压后是一套完整的Qt4.8工程。设备本身是一台带电阻触摸屏的工业终端,跑的是嵌入式Linux,系统里没有物理键盘,所有需要录入数据的地方都只能依赖触控操作。实际使用场景包括:配置网络IP、录入设备编号、输入操作员姓名、填写生产批次号。这些输入框在很多页面里都有,如果不做软键盘,整台设备在客户现场基本没法正常使用。
这类问题在Qt4.8时代很常见。当时很多工控设备还停留在Qt 4.7/4.8,触摸屏分辨率不高,内存也紧张,没有条件去跑一套完整的桌面环境。系统自带的可执行程序可能很大,依赖图形会话,从Qt应用里弹出来又慢又难看。更麻烦的是,嵌入式Linux上根本没有现成的“中文软键盘”可调,就算有,也不一定和Qt的窗口系统配合得好。所以项目组最终决定:自己做一个软键盘,直接嵌入Qt应用里,用户点输入框时自动弹出,这个方案最可控,也最适合直接打包成zip分发。
这一篇就围绕这个压缩包里的实现思路,把“点击输入框呼出软键盘”的关键链路完整拆开:事件怎么捕获、软键盘怎么定位、按键怎么回填到输入框、打包部署有哪些坑。如果你也在给Qt4.8老项目做虚拟键盘,可以直接参考这套做法。
1.2 为什么不用系统输入法和桌面软键盘
很多第一次接触这个需求的人会问:系统里不是有输入法吗?直接调不行吗?在普通PC上当然可以,但在这种设备上不行,原因很现实。
嵌入式Linux通常只装了必要的图形库,根本没有独立的输入法进程。Qt4.8自带的输入法框架依赖QInputContext,但不同平台下的实现差异很大:在X11环境下可以接ibus、fcitx,在Qt for Embedded Linux(QWS)环境下又要走另一套QWSServer的输入法接口。如果你的Qt版本、编译选项、插件目录任何一个地方对不上,输入法插件就加载不出来,界面倒是正常,点输入框却弹不出任何键盘。
Windows端的系统软键盘osk.exe也不行。它是独立进程,弹出来之后和Qt应用不在同一个窗口层级,很难做到“点输入框自动呼出”。而且现场设备很多是Windows XP Embedded或者精简过的工业Windows,osk.exe被裁剪掉的情况很常见。想通过命令行启动一个外部软键盘,再跟Qt的输入焦点联动,等于同时维护两个程序的状态,非常容易出问题。
所以自己实现一个悬浮软键盘,从应用层直接控制,是当时最稳妥的路线。它对系统环境要求低,只要Qt能正常跑,键盘就能跑,zip拷到哪都能用。
2. 软键盘实现方案对比:别一上来就写QInputContext插件
2.1 三种可行的实现路线
软键盘的“键盘本体”其实很简单,无非是一堆按钮排列在窗口里。真正的分歧在于:它要怎么感知输入框,怎么把按键内容送进去。
第一种是正规的QInputContext插件。原理是写一个QInputContext子类,实现输入法状态管理、按键过滤、候选词处理,然后编译成插件放到Qt的plugins/inputmethods目录下。应用侧完全不用改,只要QLineEdit获得焦点,系统输入法框架就会自动拉起软键盘。听起来很完美,但QInputContext在Qt4.8里的API非常绕,需要处理mouseHandler、filterEvent、microFocusChange等一堆回调,而且同一套代码在X11和QWS下表现还不一样。调试成本高,出了问题还很难快速定位。
第二种是普通悬浮窗口方案,这也是项目里最终采用的方案。自己写一个无边框QWidget作为键盘面板,放上按钮,通过事件过滤器或焦点事件捕捉“用户点击了哪个输入框”,然后键盘窗口移动到合适位置显示出来。按键点击后,直接把字符插入到目标输入框里。这个方案没有全局输入法的概念,所有交互都是“显示/隐藏键盘窗口 + 向QLineEdit插入文本”,逻辑直白,调试容易。
第三种是把键盘直接嵌入主界面布局里,和输入框写在同一个页面上,点击输入框时切换页面状态,把键盘区域显示出来。好处是不用关心窗口层级,坏处是每个用到输入框的页面都要预留键盘位置,页面布局会非常难看,后期维护也麻烦。适合固定输入区域的简单场景,不适合多页面跳转的工业软件。
2.2 我为什么最终选了“事件过滤器 + 无边框窗口”
把三种方案放在一起对比:
| 方案 | 集成难度 | 跨平台风险 | 后期维护 | 适用场景 |
|---|---|---|---|---|
| QInputContext插件 | 高 | 高,X11/QWS差异明显 | 难 | 需要给系统所有应用提供输入法 |
| 悬浮窗口 + 事件过滤 | 低 | 低,只依赖Qt自身 | 容易 | 单应用内的软键盘需求 |
| 内嵌键盘面板 | 低 | 低 | 页面改动大 | 固定页面、输入框少 |
这个项目只需要给当前这一个Qt应用服务,不需要做系统级输入法,所以我选了第二种。它有几个很实际的好处:不依赖Qt编译时是否带插件、不依赖系统输入法环境、不依赖窗口管理器。只要主程序能起来,软键盘就能起来。压缩包里带的运行库也不用那么全,发布时省心很多。
另外一个重要原因是,Qt4.8项目大多比较陈旧,代码结构已经定了,不适合为了输入法插件大改主流程。事件过滤器加悬浮窗口,只需在main函数里初始化一次,再把过滤器安装到QApplication上,所有输入框自动生效,侵入性最低。
3. 点击输入框呼出软键盘的操作链路
3.1 用事件过滤器捕获输入框点击,而不是重写focusInEvent
很多人第一反应是重写QLineEdit的focusInEvent,在获得焦点时弹出键盘。这种做法在“首次点击”时有效,但有一个明显问题:当某个输入框已经处于焦点状态,用户再次点击它时,不会再次触发focusInEvent,键盘就不会重新显示。如果用户把软键盘拖到了旁边,或者键盘被其他窗口盖住,点同一个输入框是弹不回来的。
更可靠的做法是安装全局事件过滤器,在鼠标按下事件发生时主动检查目标控件是不是QLineEdit。核心代码不长:
bool KeyboardFilter::eventFilter(QObject *obj, QEvent *ev) { if (ev->type() == QEvent::MouseButtonPress) { QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(ev); if (mouseEvent->button() == Qt::LeftButton) { QLineEdit *edit = qobject_cast<QLineEdit *>(obj); if (edit && edit->isEnabled() && !edit->isReadOnly()) { m_keyboard->showForWidget(edit); } } } return QObject::eventFilter(obj, ev); }这段代码要装在QApplication上,这样整个应用里所有输入框都被覆盖,不需要在每个页面里单独connect。注意qobject_cast的判断:事件对象obj在鼠标按下时通常就是QLineEdit本身,但如果界面上放了QLineEdit的clearButton,或者用样式表给输入框塞了子控件,事件可能先发到子控件上,需要向上查找父级。稳妥做法是循环取parent,直到找到QLineEdit为止,这里不展开,实际项目里我加了一个向上查找的小函数。
3.2 软键盘显示位置:既不能挡住输入框,也不能超出屏幕
键盘窗口显示之前要做两件事:记录当前目标是哪个输入框,计算键盘应该显示在什么位置。Qt4.8里获取屏幕区域用的是QDesktopWidget:
void SoftKeyboard::showForWidget(QWidget *target) { m_target = target; QRect screen = QApplication::desktop()->availableGeometry(target); QPoint globalPos = target->mapToGlobal(QPoint(0, 0)); int x = globalPos.x(); int y = globalPos.y() + target->height() + 4; if (y + height() > screen.bottom()) { y = globalPos.y() - height() - 4; } x = qBound(screen.left(), x, screen.right() - width()); move(x, y); show(); raise(); m_target->setFocus(); }这里的核心逻辑是“优先显示在输入框下方,下方放不下就放到输入框上方”。如果输入框本来就靠近屏幕底部,键盘在下方展开会直接盖住它,用户看不清自己输入了什么,所以必须做边界判断。mapToGlobal返回的是输入框在屏幕坐标系里的位置,注意如果输入框在QScrollArea里,mapToGlobal得到的坐标已经包含了滚动偏移,不需要额外处理。
有一个需要实际操作才知道的坑:在Qt for Embedded Linux的QWS模式下,availableGeometry返回的区域可能和物理屏幕有偏移,尤其是带任务栏或状态栏的定制系统。建议把键盘窗口设置成Qt::Tool类型,并加上Qt::WindowStaysOnTopHint,保证它显示在普通窗口之上。
3.3 焦点问题:点击软键盘按钮不能让输入框失焦
软键盘弹出后,下一个需要处理的问题是焦点。正常情况下,用户点击软键盘上的按钮,点击事件会落到软键盘窗口上,输入框会失去焦点。而输入框一旦失焦,页面可能会做校验,也可能会触发其他逻辑,用户的输入过程就被打断了。
解决办法是组合使用窗口标志和属性。创建软键盘窗口时设置:
setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint); setAttribute(Qt::WA_ShowWithoutActivating, true);WA_ShowWithoutActivating的作用是让窗口显示时不抢焦点。不过这个属性在部分嵌入式窗口系统下并不可靠,所以还要在按钮点击处理里主动把焦点交还给输入框。软键盘每个按钮的clicked信号对应的槽里,在处理完字符插入后调用一次m_target->setFocus(),这样即使点击瞬间焦点飘到了软键盘上,也能马上拉回来。
在实际设备上还遇到过另一种情况:软键盘弹出后,用户先点了输入框A,再点输入框B,此时A和B之间切换正常,但软键盘没有更新定位。这是因为点击B时鼠标事件被过滤器捕获,showForWidget重新执行了,位置跟着更新,说明全局事件过滤器方案在“输入框切换”场景下天然有效,不需要额外处理input focus日志。
4. 按键输出与输入框回填的细节处理
4.1 用insert回填字符,而不是伪造QKeyEvent
软键盘按钮点击后,最简单的回填方式就是调用m_target->insert(key)。一开始我曾想过构造QKeyEvent投递给输入框,模拟真实物理键盘的行为,实测下来问题很多:很多QLineEdit的输入验证器依赖键盘事件的细节状态,比如按下与释放的配对;中文输入法状态下,普通英文字符要通过input method context处理;QKeyEvent的key值和text值在不同的platform下可能不一致。伪造按键事件很容易出现“字符没进去”或者“进去了两次”的怪问题。
QLineEdit::insert()就干净得多。它会把字符插入到当前光标所在位置,有选中文本时先替换选中内容,同时进入撤销栈,用户可以用Ctrl+Z撤销,这比自己去操作字符串再setText可靠得多。
每个按钮在创建时通过setProperty保存自己的输出内容,槽函数里用sender()获取:
void SoftKeyboard::onKeyClicked() { QToolButton *btn = qobject_cast<QToolButton *>(sender()); if (!btn || !m_target) { return; } const QString key = btn->property("keyText").toString(); if (key == QStringLiteral("BACKSPACE")) { m_target->backspace(); } else if (key == QStringLiteral("SHIFT")) { toggleLayout(); } else if (key == QStringLiteral("HIDE")) { hide(); } else { m_target->insert(key); } }Backspace单独处理,因为QLineEdit没有直接暴露“删除光标前一个字符”的槽,backspace()正好是QWidget公共接口,直接调用即可。Shift和数字符号层切换也在这里做,不往输入框里传内容。
4.2 键盘布局用数据驱动,不要每个按钮写一遍connect
软键盘不是一个固定不变的键盘,它至少需要小写字母、大写字母、数字符号三个布局。如果把每种布局都画一个面板,代码量会翻三倍,而且切换逻辑很绕。实际做法是把布局定义成数据:
struct KeyRow { QStringList keys; }; struct KeyLayout { QString layoutName; QList<KeyRow> rows; };创建键盘时遍历布局数据,每个按键生成一个QToolButton,设置固定最小尺寸,按钮之间的间距通过布局管理器控制。按钮的text和keyText属性在创建时写入。切换布局时,只需要清空按钮文字并更新keyText,不需要销毁重建按钮。这样维护键盘布局就是维护一个字符串数组,想加一个冒号按键,在对应位置加一个“:”就行。
4.3 中文输入的最小实现:简单候选词表
如果设备只需要输入英文和数字,上面的实现已经够用。但很多现场需要输入中文用户名或批注,这就涉及中文输入方案。
QInputContext的中文输入法是另外一个世界,本文不展开。针对这个软键盘,我采用了最简单的方式:在软键盘上切到拼音模式,用户依次点击拼音字母,内部维护一个“拼音到候选汉字”的映射表,点击某个候选字时再插入输入框。候选词显示用一行QListWidget,放在键盘上方。例如输入“zhong”,映射表里对应“中、重、众、钟”,点一下“中”就把“中”insert到输入框。
这种实现没有智能组词,但对于录入姓名、地址这种低频场景完全够用。映射表可以做成外部文本文件,按行加载,随zip包一起分发。需要注意字体问题,中文候选词渲染需要设备上有中文字体,否则显示全是方块,这一点下一节专门说。
4.4 光标位置和输入框内容变化后的处理
输入框内的光标位置不需要手动控制,因为insert/backspace都会自动处理。但你可能会遇到软键盘需要“感知输入框内容长度”的场景,比如限制密码框最多6位,满了之后软键盘还要继续弹。一般不建议在软键盘里做长度校验,QLineEdit自己加QValidator就能拦。软键盘只需在insert后保持焦点,不做业务判断。
另一个小技巧是:当输入框类型是密码时,输入框会设置echoMode为Password,软键盘键盘布局仍然是普通布局,不需要额外改变。用户输入的字符显示成圆点,这是QLineEdit自身行为,软键盘完全不用感知。
5. zip发布与部署中真正会卡住人的地方
5.1 Qt4.8运行库:这个zip解压出来缺了什么
Qt4.8工程编译出来是一个exe或二进制文件,但发布时只拷贝这个文件绝对跑不起来。至少需要带上QtCore4.dll和QtGui4.dll(Windows)或对应的libQtCore.so.4、libQtGui.so.4(Linux)。如果用了网络、数据库、XML模块,还要带QtNetwork4.dll、QtSql4.dll、QtXml4.dll。
Qt4.8还没有Qt5的windeployqt工具,所有依赖都要手动从Qt安装目录的bin里拷贝。压缩包里如果只放了源码,那你接手后还要自己编译,编译期需要注意:目标机器上的Qt库版本要和编译环境一致,比如在Qt 4.8.6下编译,就别拿去配4.8.0的机器,不同补丁版本之间的ABI可能不兼容。
这里还要提一个常见问题:如果你从网上下到的zip包在解压时报“invalid zip archive: could not find eocd”,先不要怀疑解压软件。这个错误表示压缩包末尾的中央目录记录不完整,绝大多数情况下是下载中断导致文件不完整。先看压缩包大小是否和发布页面标注一致,不一致就重新下载。如果压缩包本身只差最后几KB,用7-Zip打开后选择“修复”偶尔能救回来,但源码项目建议还是重新下载完整包,修复出来的文件结构不一定完整,编译到一半才发现缺文件更浪费时间。
5.2 插件目录问题:程序能启动但键盘不弹
悬浮窗口方案不依赖输入法插件,但Qt4.8本身在运行时还可能需要平台插件。Windows下Qt4.8通常默认使用原生窗口,不需要额外的platforms插件;嵌入式Linux下如果用的是QWS,则需要确保libqwindowsmedia等插件在正确的目录。调试插件加载问题,设置环境变量QT_DEBUG_PLUGINS=1,启动程序时控制台会打印每个插件尝试加载的路径和结果,这是最快定位手段。
如果你在别人的代码基础上改用QInputContext插件方案,还要注意inputmethods插件目录的位置。Qt插件搜索路径默认是编译时写死的,目录一旦不存在就静默跳过,不报错。可以用QCoreApplication::addLibraryPath()在main函数里追加插件目录,这是最稳妥的写法:
QCoreApplication::addLibraryPath(QCoreApplication::applicationDirPath() + "/plugins");5.3 中文字体缺失:按键和候选词全是方块
嵌入式设备上最容易忽略的是字体。英文软键盘用自带的字体没问题,一旦切到中文布局,候选词显示成方框或者乱码,通常不是程序问题,而是系统没有中文字体。解决方法是把中文字体文件(比如wqy-microhei.ttc)复制到目标设备,在程序启动时加载:
QFontDatabase::addApplicationFont(":/fonts/wqy-microhei.ttc");把字体放到Qt资源文件里还有一个好处:字体随应用打包进可执行文件,不会因为系统字体目录变更而丢失。压缩包里如果带了fonts目录,发布时不要漏掉这个目录,很多人就是拷了exe忘记拷字体,结果英文正常、中文全是豆腐块。
6. 实测中的交互细节与可扩展方向
6.1 触摸屏操作体验:按钮尺寸和按下反馈
这个软键盘在7寸电阻屏上做了一段时间的实测,控制屏有效区域通常只有800x480,软键盘不可能占太大面积。按钮最小触摸区域建议做到32像素以上,否则手指点击经常误触。按键之间的间距不要设成0,至少留3到4像素,电阻屏的触点定位误差比电容屏大,太密的按键很难点准。
实测下来,一个接近全屏宽度的键盘,包含字母区、数字区、Backspace和Shift,总共四行布局比较合适。按钮高度按40像素设计,屏幕剩余空间还能露出上方的输入框,方便用户看到输入结果。点击按钮时通过QSS设置按下态背景:
QToolButton { border: 1px solid #8a8a8a; background: #f0f0f0; } QToolButton:pressed { background: #c0c0c0; }这个按下反馈在电阻屏上尤其重要,因为电阻屏没有触觉反馈,用户只能靠视觉确认“这键按到位了”。按下时切换背景色,松开后恢复,感觉就自然很多。
6.2 还能往哪个方向扩展
这套软键盘做完之后,基础功能已经很稳定。如果你接下来要把它用在更复杂的项目里,有几个扩展方向可以考虑。
一个是把键盘从悬浮窗口改成QML实现。Qt4.8本身支持QML,但和Widgets混用要处理窗口句柄和事件转发问题,改动量不小。如果项目本身已经有QML界面,用Qt Quick实现虚拟键盘更顺滑,动画过渡也更流畅。
另一个是支持QTextEdit、QPlainTextEdit等富文本输入框,不只是QLineEdit。事件过滤器里只需要把目标类型从QLineEdit扩展成QTextEdit,插入接口从insert变成insertPlainText,其余逻辑几乎不用动。
再一个是把中文候选词表换成更大词库,或者接入第三方输入法引擎。但这就绕回到QInputContext方案了,适合真正需要全系统输入法的项目,不建议在单个应用软键盘里做重投入。
我个人的建议是:无论后续怎么扩展,先把“点击输入框呼出、防遮挡、焦点不丢失、插入回填”这四个核心链路跑稳,再考虑好看和智能。软键盘的本质是替代物理键盘,第一要务是让用户能高效地把文字打进去,其他都是加分项。
本文还有配套的精品资源,点击获取