简介:这是一份基于Qt开发的虚拟软键盘源码包,主要面向在PC或ARM嵌入式开发板上构建触摸交互应用的开发者,尤其适合无物理键盘、需要屏幕输入的工控与物联网项目。程序在Qt5.6环境下编写完成,并向下兼容Qt 4系列,已在PC与ARM嵌入式开发板验证可稳定运行;核心实现只依赖少量文件,设计上充分考虑了集成便利性,可直接复制并挂接到现有工程中。压缩包内共10个文件,主要包含3个cpp源文件、2个h头文件、1个pro工程文件与1个ui界面描述文件,分别用于实现键盘逻辑、声明接口、配置构建环境以及定义界面布局,整体包体仅14KB,结构清爽。已有4921人学习下载,适合需要快速为Qt应用补充虚拟键盘能力的开发者,也适合借此学习键盘事件处理、信号槽通信和界面布局管理等进阶技巧。 做设备端开发的人,基本都绕不开一个需求:界面上总要有一块文本输入区,但设备本身没有物理键盘。仓库管理系统的手持PDA、工控机的触摸屏、自助查询一体机、HMI人机交互面板,这些场景里你不可能让用户临时插一把USB键盘,更不可能让操作员去记键位。这时候就需要在应用内部实现一个虚拟软键盘——直接利用QT的窗口体系,在屏幕上绘制出一套可以点击输入的键盘界面。
这个项目看起来简单,实际上坑不少。焦点怎么处理、按键事件怎么注入、为什么键盘一弹出来主界面就失焦、为什么在有些板子上窗口置顶不生效……这些问题如果不提前想清楚,写出来的软键盘要么按了没反应,要么一用就捣乱。本文我就结合自己做QT虚拟软键盘的完整过程,把这套东西从设计思路到具体代码、从事件机制到部署踩坑,一次性讲清楚。适合刚接触QT开发、正准备给嵌入式设备或触摸屏程序做输入组件的朋友参考。
1. 为什么要自己写一个QT虚拟软键盘
1.1 什么场景下必须自己开发
很多初学者会问:QT官方不是带了一个虚拟键盘模块吗?确实,如果用的是QT 5.7以上版本,自带一个Qt VirtualKeyboard模块,是基于QML实现的一套输入法框架,功能还挺全。但问题在于,它不是一个开箱即用的控件,而是作为系统级输入法插件接入的,要改QT_IM_MODULE环境变量,而且对窗体环境、平台插件的匹配有要求。在Windows桌面程序里折腾它还算温和,一旦到了嵌入式Linux、没有QML运行环境的精简系统、或者需要和现有Widgets界面深度绑定的场合,这套东西就往往水土不服,光编译依赖就能让人崩溃。
同样地,直接用系统自带的屏幕键盘(Windows下的osk.exe)也不是办法——它和你的程序是两个独立进程,位置对不齐、样式难统一、焦点管理完全不可控。设备操作员按一下系统键盘,反而可能弹出别的窗口把程序盖住。
所以当需求落进下面这些场景时,自己写一个基于QWidget的软键盘反而是最省事、最可控的方案:界面风格要和主程序保持一致、输入区域是固定的几个LineEdit、需要拦截或定制某些特殊按键行为、系统资源很紧不想引入额外的QML模块。只要明确需求是“给固定输入框做一个好看的可点击键盘”,自研Widgets方案绝对比套官方模块划得来。
1.2 自绘键盘和官方虚拟键盘怎么选
这里给一个比较直白的选型思路。假设你的目标机是常规Windows工控机,界面也是Widgets体系,那就优先自绘;假设你的程序本来就是QML为主,并且运行环境允许加载虚拟键盘模块,那就去用VirtualKeyboard;假设你的产品需要支持系统级输入法、多语言切换、手写输入这类复杂输入体验,那也应该优先考虑成熟的输入法方案而不是自己造轮子。
如果决定自研,核心选型也就俩:一是界面用什么承载,我选的方案是QWidget + QGridLayout,简单直接,QSS定制外观也很方便;二是按键事件怎么注入到目标控件,这也是整个项目最关键的设计点,后面专门讲。另外补充一句,如果项目里已经有QML模块了,也可以考虑用QML写一套独立键盘子界面,通过信号与Widgets主程序通信,但本质思路都一样:键盘要做得像一个“不抢焦点的输入工具”。
2. 核心机制:软键盘不抢焦点这件事是关键
2.1 焦点机制决定了键盘能不能正常工作
刚开始写软键盘的人,最容易踩的坑就是:我做了一排QPushButton,点击QLineEdit弹出键盘,结果点键盘按键的时候,QLineEdit立刻失去了焦点,键盘上按什么字符都进不去。原因就是——按钮抢走了焦点。
QT的焦点体系里,任何可聚焦的控件在用户点击时都会默认获得键盘焦点。QPushButton本身就是可以聚焦的控件,你点它,焦点就会跑到按钮上,原来停留在QLineEdit里的光标自然就没了。而软键盘的本质是“键盘服务于当前输入框”,它绝对不能自己去抢焦点。
解决思路有两条。一条是用Qt::WindowDoesNotAcceptFocus窗口标志,标明这个窗口不参与焦点获取,用户点它的时候焦点仍然保留在原来的输入控件上。另一条是用Qt::WA_ShowWithoutActivating属性,让窗口显示时不激活自己。两条配合使用,软键盘就能做到“看得见、点得动,但焦点永远留在输入框上”。
2.2 按键事件怎么注入到输入框
既然焦点始终在QLineEdit上,那么发事件就简单了:用QApplication::focusWidget()拿到当前持有焦点的控件,然后向它发送QKeyEvent。按下和松开各发一次,模拟一次完整的物理键盘输入。这个方法最通用,只要是标准输入控件都能接住,不用关心它到底是QLineEdit、QTextEdit还是QComboBox。
但也要说明一下,QKeyEvent向聚焦控件发送时,触发的结果依赖控件内部的按键处理逻辑。如果焦点控件是自绘的、没有实现键盘处理的控件,发了也白发。对于这种情况,可以再提供一套“直插文本”的接口,直接调用QLineEdit::insert()或QTextEdit::insertPlainText()。我给键盘类设计了两套输入通道:默认走事件注入,同时暴露一个可重写的虚函数,供极端情况下按控件类型做适配。
2.3 窗口标志和置顶效果的细节
软键盘一般需要悬浮在主界面之上,不抢焦点、又能始终可见。我常用的组合是下面这段:
setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::WindowDoesNotAcceptFocus); setAttribute(Qt::WA_ShowWithoutActivating); setAttribute(Qt::WA_TranslucentBackground);逐一说下为什么这样设。Qt::Tool表示这是一个工具窗口,通常不会出现在任务栏,比较适合做临时面板;FramelessWindowHint去掉系统标题栏,键盘看起来像一块面板;WindowStaysOnTopHint保证键盘不会被主窗口遮住;WindowDoesNotAcceptFocus配合WA_ShowWithoutActivating,就是前面强调的“不抢焦点”关键组合。最后一个半透明背景是为圆角键盘准备的,如果你对边框没要求可以不加。
这里有个需要记住的经验:在部分嵌入式平台插件(linuxfb、eglfs)下面,Qt::Tool可能表现不稳定,窗口显示不出来或者显示成黑色方块。如果遇到这种情况,把第一个标志换成Qt::Window,并且去掉WindowStaysOnTopHint,改为显示的时候手动调用raise()和activateWindow()(activateWindow不会触发焦点获取),实测在很多Linux板子上效果更稳。
3. 项目实操:做一个能用的基础软键盘
3.1 界面布局与按键映射
我以一个简洁的全键盘为例。键盘分三块区域:字母区、数字符号区、功能键区。整体用QGridLayout排布,所有按键用同一个点击槽函数处理,区分普通字符键和特殊功能键。下面是核心类的头文件:
class VirtualKeyboard : public QWidget { Q_OBJECT public: explicit VirtualKeyboard(QWidget *parent = nullptr); void setKeyboardVisible(bool visible); protected: bool eventFilter(QObject *obj, QEvent *event) override; private slots: void onKeyClicked(); private: void createKey(const QString &label, const QString &keyText = QString(), int row = 0, int col = 0, int rowSpan = 1, int colSpan = 1); void sendKeyText(const QString &text); void sendKeyEvent(int keyCode, Qt::KeyboardModifiers modifiers = Qt::NoModifier); void handleFunctionKey(const QString &label); void updateShiftState(); bool m_shift = false; bool m_caps = false; };布局部分我建议把按键数据抽成一个结构体数组:每个按键包含显示文本、实际输入文本、所在行列、跨行列数、是否功能键。数据驱动的好处是,后期想改成九宫格、支持多语言布局,只需要改数据不改代码。
void VirtualKeyboard::initLayout() { // 第一行:数字 const char *numRow = "1234567890"; for (int i = 0; i < 10; ++i) createKey(QString(numRow[i])); // 第二三行:字母(省略数组定义,这里示意逻辑) const char *letterRow1 = "qwertyuiop"; const char *letterRow2 = "asdfghjkl"; // 第四行特殊键 createKey("Shift", "", 3, 0, 1, 2); // 空格 createKey(" ", " ", 3, 2, 1, 6); createKey("Backspace", "", 3, 8, 1, 2); createKey("Enter", "", 3, 10, 1, 2); }注意按键上的label和实际输入的keyText分离。比如Shift状态时,字母键显示大写,但内部处理时由m_shift状态统一转换小写为大写,而不是每个键存两套文本。这样改状态只需要刷新一次全部按键文本,局部刷新也方便。
3.2 处理Shift、CapsLock、回车这些特殊键
普通字符键的逻辑最简单:点一下,拿到该键在当前Shift状态下的文本,注入到当前焦点控件。特殊键就必须单独拦截。Shift键点击后切换m_shift状态,同时刷新所有字母键的显示文本;Backspace发一个Qt::Key_Backspace;Enter发Qt::Key_Return或者Qt::Key_Enter,部分文本框和对话框对这两个键的处理有差异,建议在桌面环境用Key_Return,嵌入式环境如果发现回车不生效,试一下Key_Enter。
CapsLock键和Shift有点区别:Shift是临时状态,松开或者点击下一个字母后是否恢复取决于你的产品逻辑;Caps是切换锁存状态,不会自动恢复。我建议把两者分开处理,因为很多操作员会把CapsLock误当成大小写切换按钮来用,产品逻辑不清晰很容易被吐槽。
发特殊键的代码:
void VirtualKeyboard::handleFunctionKey(const QString &label) { if (label == "Shift") { m_shift = !m_shift; updateShiftState(); } else if (label == "Backspace") { sendKeyEvent(Qt::Key_Backspace); } else if (label == "Enter") { sendKeyEvent(Qt::Key_Return); } else if (label == "Space") { sendKeyText(" "); } }3.3 和输入框联动:焦点到哪,键盘跟到哪
软键盘不能一直杵在屏幕上占地方,比较合理的交互是:用户点击某个输入框时键盘显示,点击非输入区域或点击关闭按钮时键盘隐藏。最简单可靠的做法是给QApplication装一个事件过滤器,监听QEvent::FocusIn和QEvent::FocusOut。
这里有个需要小心的点:由于软键盘本身设置成了WindowDoesNotAcceptFocus,点击它不会触发FocusOut,所以正常情况下输入框不会因为弹键盘而丢焦点。但如果你用事件过滤器监听全程序焦点变化,要注意把软键盘自身的控件排除掉,防止内部按钮被点击时引发多余的显示隐藏逻辑。实际开发中我给VirtualKeyboard内部也装了事件过滤器,把键盘窗口范围的所有控件产生的FocusIn事件全部拦截过滤掉,不让它们参与全局焦点计算。
显示位置方面,软键盘默认占屏幕底部,通过move()到合适位置即可。如果希望键盘跟随输入框,可以监听输入框的QRect,把键盘移动到输入框下方。这块逻辑不难,但容易被忽略——很多设备屏幕只有7寸左右,输入框可能在中部,键盘如果固定在底部,用户要低头看半天。我的做法是提供一个setFollowWidget(QWidget*)接口,内部用QTimer定时检测目标控件位置并调整键盘窗口位置。
4. 实际部署过程中的问题排查
4.1 常见问题与解决方案速查
| 现象 | 原因 | 解决办法 |
|---|---|---|
| 点击软键盘按键,字符进不了输入框 | 键盘窗口抢走了焦点 | 检查是否设置了WindowDoesNotAcceptFocus和WA_ShowWithoutActivating |
| 键盘显示但总被主窗口遮挡 | 置顶标志未生效或平台不支持 | 改用Qt::Window,显示时手动调用raise() |
| 只有第一个字符能输入,后面没反应 | 事件发送后焦点被程序其他地方抢走 | 检查是否在输入框的textChanged里做了刷新焦点操作 |
| 回车键在对话框里不触发默认按钮 | Key_Return和Key_Enter行为不一致 | 尝试发Qt::Key_Enter,并注意输入法上下文 |
| 键盘按键有粘连感,偶尔丢失按键 | 触摸屏和鼠标事件同时触发 | 针对触摸平台开启setAttribute(Qt::WA_AcceptTouchEvents)并做去重处理 |
部署到目标机器提示no qt platform plugin could be initialized | 打包时插件目录缺失 | 使用windeployqt完整拷贝platforms插件,确认路径正确 |
no qt platform plugin could be initialized这个错在工控机上太常见了,很多人以为是QT环境没装好,其实是exe运行时找不到platforms目录里的qwindows.dll(Windows)或libqlinuxfb.so等插件。用windeployqt打包时,把生成的整个目录一起带过去,别只拷exe。
4.2 触摸场景和输入法兼容性
如果目标设备是电容触摸屏,需要关注两件事:触摸事件和鼠标事件是否重复触发。QT在部分平台会默认把触摸事件转成鼠标事件,如果按键在pressed和clicked里都发了字符,就可能一次输入两个字符。我建议所有按键一律只响应clicked()信号,不要自己另外处理鼠标按下,反正clicked已经包含了完整的点击逻辑。
如果程序还需要接入中文输入法,自绘键盘只能处理ASCII字符,中文输入得靠系统输入法。这套软键盘在设计上可以预留一个“打开输入法”的切换键,点击后调用QInputMethod::show()。但在实际项目里我很少接中文输入法——大多数嵌入式设备输入的是编号、数量、字母,中文需求很少。真需要中文的时候,还是老实评估官方VirtualKeyboard模块更靠谱。
4.3 为实际设备做的定制经验
我做的第一款软键盘是给一台7寸Linux触摸终端用的,当时踩过一个很特别的坑:键盘平时正常,但一旦主窗口进入全屏,键盘显示几次后按键就变迟钝。排查后发现是主窗口全屏时用了showFullScreen(),而键盘窗口的WidgetStaysOnTopHint在和全屏窗口叠加时,被某些窗口管理器压了层级。最后解决办法是在显示键盘后主动调用raise(),并且增加一个300毫秒的定时器周期性地查看键盘是否被其他窗口覆盖,被覆盖就再抬一次。
还有一次是把软键盘移植到国产化x86环境上,QT版本比较老,问题出在setWindowFlags必须先于show()调用,而show()之后再次修改标志位经常不生效。我的经验是,键盘的窗口标志在构造函数里一次性设好,不要在运行期反复切换,否则在不同平台的表现差异很大。
5. 拓展与收尾
这个项目做完之后,我顺手把键盘封装成了一个单例工具类,提供SoftKeyboard::instance()->showKeyboard()和SoftKeyboard::instance()->hideKeyboard()两个静态方法,主程序里直接调用就行,不需要关心键盘内部的焦点处理和布局逻辑。这样做的好处是多个页面复用的时候不用每次实例化,代码整洁很多。
另外补一个比较实用的经验:不要忽略QSS对按键反馈感的影响。软键盘按键没有物理键程,用户按下去如果没有任何视觉反馈,很容易产生“按了没反应”的错觉。我在样式中给QPushButton设置了:pressed状态的背景色变化,同时加了一点字体缩进,按下时按键会有一个轻微下陷的效果,操作员反馈明显觉得键盘“好用”了。这算是个不花一分钱、但能提升实际体验的小技巧。
路线图方面,这套基础软键盘后续可以扩展的方向还有不少:比如支持多语言布局切换、加入数字键盘模式、做成可停靠边栏、根据窗口大小自动缩放按键比例。核心骨架一旦稳定,往上加需求都是水磨工夫。做这套东西最大的心得是:不要把虚拟软键盘单纯当成“一组按钮”,它本质是一个要跟系统焦点机制、窗口管理、输入法体系同时打交道的交互组件,理解透这套运行逻辑,后面写什么输入类组件都不慌了。
本文还有配套的精品资源,点击获取