简介:这是一份面向Qt开发者与嵌入式界面开发者的虚拟软键盘实现资源,针对触摸屏或无实体键盘场景下需要文本输入的界面需求,提供可直接嵌入现有工程的轻量级键盘组件。资源在Qt5.6环境下编写,作者说明理论上兼容Qt4,并已在PC机与ARM嵌入式开发板上验证运行,适合具备一定Qt基础、希望快速补齐输入模块的开发者参考。压缩包共10个文件,约14KB,以cpp与h源码为主体,配合pro工程文件、ui界面文件及用户配置类文件,结构精简,核心键盘逻辑与主窗口示例分离清晰,便于按需裁剪移植。目前已有4930人学习下载,读者可从中获得键盘布局与按键响应的实现思路、信号槽交互方式以及跨平台移植的参考做法,减少从零搭建软键盘的试错成本。
1. 从一块触摸屏说起:QT虚拟软键盘能解决什么
去年帮某公司做一个工控一体机的项目,硬件到位、系统跑起来,结果卡在一个特别朴素的问题上:设备没有物理键盘,只有一块电阻触摸屏,用户要输入参数、填工单号,系统自带的输入法要么弹不出来,要么弹出来挡住半个界面,要么在嵌入式环境里根本没装。那几天翻来覆去改,最后落到一个很直接的方案上——自己用 QT 写一套虚拟软键盘,跟着输入框走,点哪儿弹哪儿,用完自动收。这份 QT 虚拟软键盘资源,解决的就是这类场景:嵌入式触摸设备、自助终端、医疗仪器面板、收银机,凡是「有屏幕没键盘」的地方,都需要它。它适合两类人:一类是刚接手 QT 项目、被输入法问题卡住的新手,照着能跑通;另一类是要把软键盘嵌进自己业务界面的熟手,关心的是架构怎么拆、按键怎么映射、焦点怎么管。
2. 技术选型与工程结构:为什么是 QWidget 而不是 QML
2.1 选型逻辑:QWidget 路线在嵌入式场景的性价比
拿到这个项目第一件事是看它用什么技术栈。结论很明确:纯 C++ + Qt Widgets,没有引入 QML,也没有依赖第三方输入法框架。这个选择在嵌入式触摸场景里是有道理的。QML 做动画和触摸手势确实漂亮,但代价是运行时多一层 Qt Quick 引擎,内存占用和启动时间都上去了,很多工控板子跑的是精简版 Linux,Qt Quick 模块未必完整编进去。Widgets 路线的好处是编译产物小、依赖少、和现有业务界面(大概率也是 Widgets 写的)无缝混排,一个QWidget子类就能塞进任何布局里。
软键盘本身的核心诉求其实不复杂:一组按钮、一个按键信号、一个文本提交接口、一套焦点跟随逻辑。用 Widgets 实现,代码量可控,调试直观,出问题看堆栈就能定位。常见做法是把键盘做成一个独立的顶层窗口(Qt::Tool或Qt::Popup类型),而不是嵌在主窗口布局里,这样它不会挤压业务界面,弹出位置也能自由控制。
提示:如果目标板子的 Qt 是裁剪编译的,先确认
widgets模块在,quick模块不在也没关系,这个方案不依赖它。
2.2 目录结构:一个可独立编译的键盘模块长什么样
这类资源通常按「键盘模块 + 演示主程序」两层组织,目录大致如下。不同版本细节会有出入,但骨架基本一致:
VirtualKeyboard/ ├── VirtualKeyboard.pro # qmake 工程文件 ├── src/ │ ├── keyboardwidget.h/.cpp # 键盘主窗口,布局与按键管理 │ ├── keybutton.h/.cpp # 单个按键控件,处理按下/长按 │ ├── keyboardlayout.h/.cpp # 键位表定义(字母/数字/符号页) │ └── inputcontext.h/.cpp # 焦点跟踪与文本提交桥接 ├── demo/ │ ├── main.cpp # 演示入口 │ └── mainwindow.h/.cpp # 带若干输入框的示例界面 └── resources/ └── keyboard.qrc # 图标、样式表资源这个拆法的关键在inputcontext这一层。键盘本身不该知道「当前是哪个输入框」,它只负责产生字符和功能键事件;谁在接收、光标在哪、要不要限制长度,交给一个中间层去管。这样键盘可以复用到任何界面,业务代码只需要在输入框获得焦点时通知键盘「目标变了」。
2.3 工程文件与编译依赖
.pro文件是理解依赖关系最直接的地方,典型内容如下:
# VirtualKeyboard.pro QT += core gui widgets # 只依赖基础模块,不引入 quick TARGET = VirtualKeyboard TEMPLATE = lib # 编成静态库或动态库供业务引用 CONFIG += staticlib c++11 INCLUDEPATH += src SOURCES += \ src/keyboardwidget.cpp \ src/keybutton.cpp \ src/keyboardlayout.cpp \ src/inputcontext.cpp HEADERS += \ src/keyboardwidget.h \ src/keybutton.h \ src/keyboardlayout.h \ src/inputcontext.h RESOURCES += resources/keyboard.qrcTEMPLATE = lib加CONFIG += staticlib意味着这个键盘是作为库产出的,业务工程在.pro里LIBS += -L... -lVirtualKeyboard就能链进来。如果只是想快速看效果,把TEMPLATE改成app、把demo/main.cpp加进SOURCES直接编译成可执行程序也行。c++11是底线,代码里用到了std::function做按键回调,老编译器会报错。
编译命令按 qmake 的标准流程走:
qmake VirtualKeyboard.pro make -j4如果板子上是交叉编译环境,先 source 对应的 qmake 环境,再执行上面两条。编译报「找不到 widgets 模块」,说明 Qt 安装不完整,需要补装qtbase5-dev或对应版本的开发包。
3. 核心模块拆解:按键、布局与焦点跟踪三条线
3.1 按键控件:一个按钮要处理的不只是 clicked
很多人以为软键盘的按键就是QPushButton加个clicked信号,真做起来会发现不够用。触摸屏上手指按下去有抖动,快速连打会丢字符,长按删除要连续触发,这些都得在按键控件里处理。核心实现大致是这样:
// keybutton.cpp KeyButton::KeyButton(const QString &text, KeyType type, QWidget *parent) : QPushButton(text, parent), m_type(type) { setFocusPolicy(Qt::NoFocus); // 关键:按键本身不抢焦点 setAutoRepeat(true); // 开启自动重复,支持长按删除 setAutoRepeatDelay(400); // 首次重复延迟 400ms setAutoRepeatInterval(60); // 之后每 60ms 触发一次 connect(this, &QPushButton::clicked, this, [this]() { emit keyPressed(m_type, text()); }); }setFocusPolicy(Qt::NoFocus)是血泪经验。默认按钮会抢焦点,一点键盘,输入框就失焦了,光标消失,后续字符不知道往哪儿送。把按键设成不接收焦点,焦点始终留在业务输入框上,键盘只做「事件转发」。setAutoRepeat配合clicked信号,长按退格键就能连续删字符,不用自己写定时器。keyPressed信号带两个参数:按键类型(字符键、退格、回车、切换页、隐藏)和文本内容,上层根据类型分流处理。
3.2 键位布局:用数据表驱动,别把按钮写死
键位如果一个个new QPushButton写死,加一个符号页就得改一堆代码。合格的做法是用数据表描述每一页的键位,布局代码只负责遍历生成:
// keyboardlayout.cpp const QList<QList<KeyDef>> &KeyboardLayout::pages() { static const QList<QList<KeyDef>> data = { // 第 0 页:小写字母 { {"q"},{"w"},{"e"},{"r"},{"t"},{"y"},{"u"},{"i"},{"o"},{"p"}, {"a"},{"s"},{"d"},{"f"},{"g"},{"h"},{"j"},{"k"},{"l"}, {"Shift", KeyType::Func},{"z"},{"x"},{"c"},{"v"},{"b"},{"n"},{"m"}, {"Del", KeyType::Backspace},{"123", KeyType::SwitchPage},{"Space", KeyType::Space} }, // 第 1 页:数字与符号 { {"1"},{"2"},{"3"},{"4"},{"5"},{"6"},{"7"},{"8"},{"9"},{"0"}, {"-"},{"_"},{"="},{"+"},{"@"},{"#"},{"."},{"comma"}, {"Del", KeyType::Backspace},{"ABC", KeyType::SwitchPage},{"Space", KeyType::Space} } }; return data; }KeyDef是个简单结构体,含显示文本和按键类型。KeyboardWidget在构造时遍历当前页的KeyDef,按每行固定列数塞进QGridLayout。切页时清空布局重新生成,或者预先建好所有页用QStackedWidget切换,后者切换更快、不闪屏,推荐后者。这样加一页德文、加一排功能键,只改数据表,布局代码一行不动。
3.3 焦点跟踪与文本提交:键盘和输入框之间的桥
这是整个项目最容易翻车的地方。键盘要往「当前有焦点的输入框」里写字,但 Qt 没有全局的「焦点变化」信号可以直接订阅。常见做法是装一个应用级事件过滤器,监听QEvent::FocusIn:
// inputcontext.cpp bool InputContext::eventFilter(QObject *obj, QEvent *ev) { if (ev->type() == QEvent::FocusIn) { // 只关心能接收文本输入的控件 if (auto *edit = qobject_cast<QLineEdit *>(obj)) { m_target = edit; m_keyboard->showFor(edit); // 键盘定位到输入框下方 } else if (auto *text = qobject_cast<QTextEdit *>(obj)) { m_target = text; m_keyboard->showFor(text); } } return QObject::eventFilter(obj, ev); } void InputContext::onKeyPressed(KeyType type, const QString &text) { if (!m_target) return; switch (type) { case KeyType::Char: m_target->insertPlainText(text); // 统一走 QTextCursor 接口 break; case KeyType::Backspace: m_target->textCursor().deletePreviousChar(); break; case KeyType::Enter: m_keyboard->hide(); emit submitted(m_target->toPlainText()); break; default: break; } }m_target用QPointer更稳妥,输入框被销毁时自动置空,避免野指针。insertPlainText和textCursor().deletePreviousChar()是QLineEdit和QTextEdit都有的接口(通过QTextCursor统一),所以不用为两种控件写两套逻辑。showFor里根据输入框的mapToGlobal位置算键盘弹出坐标,屏幕下半部分就往上弹,避免被虚拟键盘自己挡住——这个判断不做,在小屏设备上键盘会盖住正在输入的框,用户看不见自己打了什么。
4. 跑起来与接进业务:从 demo 到实际界面的落地步骤
4.1 先跑 demo:验证环境和基本交互
拿到资源别急着往业务里塞,先跑 demo 确认环境没问题。步骤很直接:用 Qt Creator 打开.pro,选好套件,编译运行;或者命令行qmake && make。跑起来应该看到一个带两三个输入框的窗口,点输入框,软键盘从下方弹出,点字母能输入,点 Del 能删,点回车键盘收起。
如果键盘弹不出来,先查两件事:一是InputContext有没有在main.cpp里通过qApp->installEventFilter(context)装上,漏了这行焦点事件根本进不来;二是键盘窗口的Qt::WindowFlags有没有设对,设成Qt::Tool时在某些窗口管理器下不显示,改成Qt::Popup或普通无边框窗口试试。如果按键点了没反应,多半是setFocusPolicy(Qt::NoFocus)漏了,焦点被按钮抢走,m_target变成空。
4.2 接进业务界面:三行代码的集成方式
集成到自己的工程,核心就三步。第一步,把src目录和库文件加进业务.pro;第二步,在main.cpp创建InputContext并安装事件过滤器;第三步,业务界面正常写QLineEdit,不用做任何特殊处理:
// main.cpp int main(int argc, char *argv[]) { QApplication app(argc, argv); MainWindow w; auto *ctx = new InputContext(&w); // 父对象设为窗口,随窗口销毁 app.installEventFilter(ctx); // 全局焦点跟踪 w.show(); return app.exec(); }InputContext构造时内部创建KeyboardWidget,键盘是独立顶层窗口,不占业务布局。业务代码完全无感,输入框该怎么写还怎么写。这种「零侵入」是这套设计最大的价值——老项目改造时,不用动几十个界面文件,加三行就全站生效。
4.3 参数调优:位置、尺寸、动画
实际设备上默认参数往往要调。键盘高度建议按屏幕高度的 35%~40% 设,太小按键点不准,太大遮挡内容。弹出位置用输入框的全局坐标判断:输入框中心在屏幕上半部,键盘贴底弹;在下半部,键盘贴顶弹。动画用QPropertyAnimation做 150ms 的位移过渡,比直接show()生硬弹出体验好很多,但嵌入式低端板子上动画可能掉帧,可以留个开关关掉。
注意:电阻屏没有多点触控,按键尺寸别小于 40×40 像素,否则误触率会高到没法用。这是硬件决定的,不是代码能救的。
5. 避坑与排查:五个真实踩过的坑
5.1 输入框失焦后键盘不消失
现象:点完输入框弹出键盘,再点界面空白处,键盘还挂在那儿。原因:只监听了FocusIn,没处理FocusOut,而且点空白处焦点可能转移到某个不可输入的控件上。解决:在事件过滤器里同时处理FocusOut,判断新焦点对象是否仍是文本输入控件,不是就延迟 100ms 隐藏键盘(延迟是为了避免输入框之间切换时键盘闪一下)。
5.2 中文输入法场景下字符重复
现象:接了系统输入法后,软键盘打的字母既进了输入法候选,又直接进了输入框。原因:软键盘直接调insertPlainText,绕过了输入法框架,两套输入路径打架。解决:如果业务需要中文,软键盘只负责产生按键事件,通过QInputMethodEvent提交给 Qt 的输入法体系,而不是直接改文本;纯英文数字场景则关掉系统输入法,避免冲突。
5.3 长按删除删过头
现象:长按退格,松手后还在继续删。原因:setAutoRepeat的重复信号在松手后可能还有一两个排队事件。解决:在KeyButton的mouseReleaseEvent里主动setAutoRepeat(false)再恢复,或者用定时器自己控制重复节奏,松手即停,比依赖内置机制可控。
5.4 键盘弹出位置算错
现象:多屏或高 DPI 设备上,键盘弹到屏幕外面去了。原因:用了控件相对坐标而不是全局坐标,或者没考虑devicePixelRatio。解决:统一用mapToGlobal转全局坐标,再和QGuiApplication::primaryScreen()->availableGeometry()比较,高 DPI 下坐标计算交给 Qt 的QScreen接口,别自己乘缩放系数。
5.5 编译过了运行崩在构造
现象:程序启动就崩,堆栈指向键盘构造。原因:KeyboardLayout里的静态数据表在全局构造阶段被访问,此时 Qt 的某些单例还没初始化好。解决:把静态数据改成函数内static局部变量(第一次调用时初始化),或者干脆在KeyboardWidget构造完成后再加载布局数据,避开初始化顺序问题。
6. 二次开发:把软键盘改成你业务需要的样子
这套代码真正值钱的地方在于它的可改造性,几个方向可以直接上手。第一,加业务专用键。工控场景常要「清空」「确认提交」「扫码触发」,在KeyDef里加类型,在onKeyPressed的switch里加分支,发个自定义信号出去,业务层接住做处理,键盘模块本身不用改结构。第二,换皮肤。所有按键样式集中在keyboard.qrc里的 qss,改颜色、圆角、按下态背景,不动 C++ 代码。第三,做输入校验联动。InputContext拿到m_target后可以读它的validator或自定义属性,比如某个框只允许数字,就在showFor时自动切到数字页,用户体验直接上一个台阶。
改造后的验证方法我一般这么走:先在 demo 里加一个和业务同类型的输入框,把新键位、新皮肤、新校验都跑一遍;再拿真机(或同分辨率模拟器)测触摸精度和弹出位置;最后才合进业务工程。跳过 demo 直接上业务,出问题时分不清是键盘的锅还是业务的锅,排查成本翻倍。
| 改造点 | 改动位置 | 验证方式 |
|---|---|---|
| 新增功能键 | keyboardlayout.cpp+inputcontext.cpp | demo 里点按看信号是否发出 |
| 换皮肤 | resources/*.qss | 真机看对比度和触摸反馈 |
| 输入类型联动 | inputcontext.cpp的showFor | 数字框自动切数字页 |
| 多语言键位 | keyboardlayout.cpp加页 | 切页不闪屏、不错位 |
有个习惯是从那以后养成的:任何软键盘改动,我都先在 demo 工程里跑一遍完整输入流程——点框、打字、删字、切页、回车提交、点空白收起——六步全过才往业务里合。这套流程帮我挡掉过好几次「改了一个键位结果整页布局错乱」的低级问题。希望这份拆解帮到你,拿到资源先跑 demo,再按自己的业务往里加东西,比从头写一套省太多时间。
本文还有配套的精品资源,点击获取