YAOTU INSIGHTS

Qt虚拟小键盘开发实战:触屏设备自定义输入组件

Qt虚拟小键盘开发实战:触屏设备自定义输入组件
简介基于Qt开发的虚拟小键盘组件面向需要为嵌入式或桌面应用补充软键盘输入的开发者尤其适合在无物理键盘环境下处理数字与字符输入场景。组件采用无边框设计不单独创建窗口通过传入自定义的myspinbox控件指针即可将按键内容直接作用于该输入控件便于灵活融入既有界面布局提升交互体验。资源包共3个文件包括实现按键逻辑的cpp源码、声明类接口的h头文件以及由Qt Designer生成的ui界面描述文件整体仅2KB结构精简适合快速阅读与二次改造。目前已有266人学习下载。从中可掌握Qt自定义控件派生如继承QSpinBox、事件处理与信号槽机制、ui文件到C代码的编译流程以及嵌入式键盘与宿主控件的联动设计思路是一份小而实用的Qt控件开发参考。1. Qt虚拟小键盘触屏设备上的最后一块输入拼图某次接手一个触屏工控机的界面改造对话框里要填 IP 地址和操作员编号现场却找不到一把能用的实体键盘触摸屏上的原生输入法又总是把界面顶得七零八落。后来我用 Qt 写了一个虚拟小键盘做成无边框悬浮窗点击任意输入框自动弹起输入完成自动收走从此告别“带键盘去现场”的日子。这个 Qt 项目就是那套方案的完整工程适合正在做触屏 HMI、嵌入式设备或者临时需要自定义输入组件的开发者。它不复杂但把“键盘如何感知焦点”和“输入如何回填”这两件看似简单的事讲得很透解压即看跑起来就能体会整套交互链路。2. 技术选型与整体架构QML 组件和 C 后端的配合2.1 为什么用 QML 做面板而不是纯 Widget拿到这个项目先翻代码发现键盘面板是 QML 写的控制逻辑在 C 里。这个选型很合理小键盘的按键数量多、布局频繁调整用 QML 声明式布局比 Widget 手写坐标省太多事。你改一个按键大小、加一行功能键只需动 JSON 或 QML不需要重新编译 C 部分。而输入回填和窗口管理这种跟系统交互紧密的活儿交给 C 更稳。项目里键盘面板本身是一个 QML 组件具体文件是KeyboardPanel.qml它不直接处理按键业务只负责把数据模型里的键位渲染出来并且把每次点击通过信号发给 C 侧的KeyboardController。控制器收到信号后再决定怎么把字符发送给当前聚焦的输入控件。这种界面与逻辑分离的写法让键盘在多个页面之间复用变得很容易。2.2 目录结构与模块职责解压后目录结构大致是这样方便你快速定位要改的地方qt-virtual-keyboard/ ├── KeyboardDemo.pro # qmake 工程文件 ├── CMakeLists.txt # 备用的 CMake 配置两份都在 ├── src/ │ ├── main.cpp # 入口初始化 QML 引擎和控制器 │ ├── keyboardcontroller.h # 键盘控制器头文件 │ ├── keyboardcontroller.cpp# 核心逻辑焦点感知、输入回填 │ └── inputfilter.h/.cpp # 全局事件过滤器捕获点击和焦点 ├── qml/ │ ├── KeyboardPanel.qml # 键盘面板主组件 │ └── KeyButton.qml # 单颗按键的组件支持按压反馈 ├── layouts/ │ ├── default.json # 默认字母数字键盘布局 │ └── numeric.json # 纯数字键盘布局演示用 └── resources/ └── images/ # 按键背景、删除键图标等资源KeyboardController是核心它维护键盘窗口的显示与隐藏持有当前输入控件的指针还把 JSON 布局解析成 QML 能用的数据模型。InputFilter是一个继承自QObject的事件过滤器安装在 application 上用于监听全局鼠标点击和焦点变化是键盘能“自动跟随输入框”的关键。2.3 事件分发链路从点击输入框到键盘弹出整个键盘的触发链路是这样的用户在页面上点击某个QLineEdit或TextInput。全局事件过滤器InputFilter捕获到鼠标按下事件从事件对象中拿到当前焦点控件。控制器根据焦点控件的属性决定是否弹键盘比如只对允许输入的控件弹出。键盘窗口显示在屏幕底部或输入框下方位置随焦点控件移动。用户点击键盘按键KeyButton 发出keyClicked信号控制器把键值转换为QKeyEvent发给焦点控件。这套链路把“键盘”和“输入框”解耦了键盘本身不需要关心页面上有哪些输入框任何获得焦点的控件都能自动获得键盘支持。这就是为什么页面代码里看不到任何跟键盘相关的调用属于“黑匣子”式接入。3. 核心模块拆解按键布局、事件捕获与输入回填3.1 JSON 布局数据键位定义与可配置性布局文件决定了键盘长什么样。默认的default.json采用数组嵌套每个元素代表一行行内每个对象代表一颗按键{ rows: [ [ { label: q, value: q, type: char, width: 1 }, { label: w, value: w, type: char, width: 1 }, { label: e, value: e, type: char, width: 1 }, { label: del, value: backspace, type: control, width: 2 } ], [ { label: 123, value: switch_numeric, type: control, width: 2 }, { label: space, value: , type: char, width: 5 }, { label: enter, value: enter, type: control, width: 2 } ] ] }每个按键对象包含label显示内容、value实际要发送的字符或控制指令、type区分普通字符与控制键width决定按键在网格中占几列。控制器加载 JSON 后将每一行转换成QListQMapQString, QVariant再把整个二维列表作为 context property 注册给 QMLQML 端用Repeater按行渲染。控制键的value是固定指令比如backspace、enter、switch_numeric。控制器解析到type为control时不走普通字符发送逻辑而是执行对应动作。这也是扩展功能键的入口新增一种控制指令只需要在控制器里加一个else if分支。3.2 QML 键盘面板组件复用与触摸反馈KeyButton.qml是单颗按键组件它根据传入的模型数据决定显示样式。为了模拟实体按键的按压手感组件里处理了MouseArea的按下与释放状态按下时背景颜色加深、文字下沉一点。import QtQuick 2.12 import QtQuick.Controls 2.5 Rectangle { id: keyButton property string label property string value readonly property bool isControl: value backspace || value enter width: 80 * keyButton.widthFactor height: 60 radius: 6 color: isControl ? #d0d0d0 : #f5f5f5 border.color: #a0a0a0 signal keyClicked(string value, string type) Text { anchors.centerIn: parent text: keyButton.label font.pixelSize: isControl ? 14 : 16 } MouseArea { anchors.fill: parent onPressed: { keyButton.color #c0c0c0 keyButton.scale 0.95 } onReleased: { keyButton.color isControl ? #d0d0d0 : #f5f5f5 keyButton.scale 1.0 keyButton.keyClicked(keyButton.value, keyButton.isControl ? control : char) } } }这里有个细节值得注意按下的视觉反馈和点击信号分离。onPressed只做 UI 变化onReleased才发信号避免用户按下后手指滑出按键仍触发输入。如果改成onClicked有些触摸设备上会因为移动误触发这是一个容易翻车的点。KeyboardPanel.qml负责读取布局数据并逐个创建按键。它通过model属性接收来自 C 的二维数组外层Repeater遍历行内层Repeater生成列。因为布局中不同按键宽度不同所以用GridLayout配合Layout.fillWidth和Layout.preferredWidth来分配空间。GridLayout { id: keysLayout columns: 10 rowSpacing: 4 Repeater { model: keyboardModel delegate: Repeater { model: modelData KeyButton { label: modelData.label value: modelData.value Layout.preferredWidth: modelData.width * 80 Layout.fillHeight: true } } } }外层Repeater的keyboardModel来自 C 注册的布局数据。注意modelData是内层 Repeater 的隐式变量指代当前行里的单个按键对象。这个双层 Repeater 的写法比较绕但能保证 JSON 里新增按键行列时QML 端不用改动。3.3 输入回填模拟按键与剪贴板两套方案键盘最终要把字符送进输入框项目里提供两套方案。方案一是模拟系统按键事件适合标准的QLineEdit和QTextEdit。控制器构造一个QKeyEvent然后通过QCoreApplication::sendEvent发送给当前焦点控件。void KeyboardController::sendChar(const QString ch) { QWidget* focusWidget QApplication::focusWidget(); if (!focusWidget) return; QKeyEvent pressEvent(QEvent::KeyPress, 0, Qt::NoModifier, ch); QKeyEvent releaseEvent(QEvent::KeyRelease, 0, Qt::NoModifier, ch); QCoreApplication::sendEvent(focusWidget, pressEvent); QCoreApplication::sendEvent(focusWidget, releaseEvent); }这段代码的逻辑是拿到系统当前焦点控件构造按下和抬起两个事件顺序发送。大部分控件在收到KeyPress时就会插入文本但为了模拟真实按键行为两个事件必须成对出现。参数说明第一个参数是事件类型第二个是按键编码这里传 0 表示不指定物理键靠第四参数的文本内容触发输入。对支持QInputMethod的控件这种方式依然有效因为它走的是 Qt 的事件分发机制。方案二是剪贴板粘贴用在某些自定义控件不走标准事件的情况下。先将字符写入剪贴板再发送QClipboardEvent调起粘贴操作。这个方案有一个很明显的坑剪贴板内容被覆盖了。所以项目默认不启用只在特殊控件上手动开启。如果你遇到键盘按键没反应优先排查是不是焦点控件重写了keyPressEvent却没有调用父类实现这种情况模拟事件会被吞掉。3.4 信号槽连接一次完整按键旅程把前面几块串起来看一次按键的完整路径用户在键盘上点击“a”键KeyButton发出keyClicked(a,char)。KeyboardPanel.qml把这个信号转发到控制器的handleKeyClicked槽函数。控制器判断类型普通字符调用sendChar控制键调用handleControlKey。sendChar构造事件并发送给焦点控件输入框出现字符“a”。信号槽连接在main.cpp中完成QQmlApplicationEngine engine; KeyButtonReceiver receiver; qmlRegisterSingletonInstance(app.controllers, 1, 0, KeyboardController, controller); QObject::connect(controller, KeyboardController::keyClicked, receiver, KeyButtonReceiver::onKeyClicked);实际项目为了避免 QML 与 C 信号槽名冲突通常会定义一个中间KeyButtonReceiver类接收 QML 信号再转发给控制器。这种“双跳”写法看着多余但能避免键盘面板直接持有控制器实例后续想换成独立键盘进程也方便。4. 编译运行从零到把键盘弹出来4.1 环境准备Qt 版本与编译器项目基于 Qt 5.15 开发带 QML 和 Quick Controls 模块。如果你本机没有 Qt建议先装一个 Qt 5.15 或更新的 5.15 系列版本编译器用 MSVC 2019 / MinGW 8 或更高版本都行。项目里同时提供了.pro和CMakeLists.txt二者任选其一个人更推荐 Qt Creator 直接打开.pro文件省去配置 CMake 的额外步骤。确认环境时最怕版本“玄学”建议先在 Qt Creator 里新建一个包含 QML 的空工程运行一次排除 Qt 环境本身的问题再打开这个项目。若你的 Qt 版本是 6.x部分 QML 模块路径有变动跑起来报module not found是正常的后文会讲怎么处理。4.2 编译步骤qmake 与 CMake 两种路径用 Qt Creator 打开KeyboardDemo.pro选择已安装的 Qt 5.15 套件构建类型选 Release然后直接点击构建。如果喜欢命令行也可以这样操作mkdir build cd build qmake ../KeyboardDemo.pro make -j4qmake会自动根据.pro文件里的QT qml quick和CONFIG c11生成 Makefile。构建成功后会在 build 目录生成可执行文件。用 CMake 的话命令稍有不同mkdir build cd build cmake .. cmake --build .CMake 配置里需要指定 Qt 安装路径第一次跑 CMake 如果找不到 Qt大概率是CMAKE_PREFIX_PATH未设置加上-DCMAKE_PREFIX_PATH你的Qt安装目录即可。运行程序后屏幕中央会显示一个简单的测试窗口里面有一个输入框。点击输入框底部就会弹出虚拟键盘这时可以直接用鼠标点击键盘按键输入内容。如果键盘没弹出来先看控制台有没有报错信息——最常见的是 JSON 布局文件路径找不到程序默认从当前工作目录加载layouts/default.json建议在 Qt Creator 的“运行设置”中将工作目录设为项目根目录。4.3 部署到触屏设备字体与分辨率适配开发机上跑通只是第一步部署到触屏设备时会遇到分辨率适配问题。项目里键盘窗口的宽度默认按屏幕宽度百分比计算高度则按按键行数自适应。如果你的设备屏幕比较小建议修改KeyboardPanel.qml中的keyboardWidth比例比如从 0.9 改成 0.7。字体问题更隐蔽。开发机上用普通字体看起来没问题但触屏设备可能没有安装相应字体文字变成方块。解决办法是把字体资源打包进resources并在main.cpp里用QFontDatabase::addApplicationFont加载。另外触屏设备上光标闪烁可能会异常需要在输入框上设置cursorVisible属性或调整QApplication::setCursorFlashTime。5. 避坑记录五条最疼的踩坑5.1 键盘窗口抢走焦点导致输入框失焦现象点击键盘上的按键后键盘窗口本身获得了焦点输入框失去焦点后续按键全部无效。原因键盘窗口虽然是Qt.ToolTip类型但某些情况下系统仍然会把焦点分配给新出现的窗口。解决在键盘窗口 show 之后立即调用Qt::WA_ShowWithoutActivating属性或者在控制器中保存并恢复焦点。我一般这样做keyWindow-setAttribute(Qt::WA_ShowWithoutActivating); keyWindow-setFocusPolicy(Qt::NoFocus);这样键盘窗口不会主动抢焦点输入框始终保持激活状态。5.2 无边框窗口在触屏上拖不动现象键盘窗口没有标题栏触摸按住想拖动位置窗口纹丝不动。原因无边框窗口需要手动处理移动事件项目初始版本没有实现拖动逻辑。解决在鼠标按下时记录全局坐标和窗口坐标的差值移动时设置窗口新位置。这个逻辑写在KeyboardController的鼠标事件过滤中注意触屏设备上MouseArea也能收到拖动手势只要保证阈值大于系统长按时间即可。5.3 JSON 布局加载失败导致键盘一片空白现象程序运行正常但键盘面板不显示任何按键。原因布局文件路径写死为相对路径从不同工作目录启动时找不到文件。解决把 JSON 文件加入 Qt 资源系统用qrc路径加载。这样无论从哪个目录启动都能找到布局文件。资源路径的加载代码要放在控制器初始化阶段并检查返回的 QByteArray 是否为空。5.4 删除键在输入框内没有反应现象除删除键外的其他按键都正常删除键点击后输入框没反应。原因模拟 Backspace 事件时需要设置正确的按键码而不是只传字符文本。解决发送 QKeyEvent 时对 Backspace 需要传入Qt::Key_Backspace作为按键码而普通字符传空按键码。项目初始版本统一用字符文本导致控制键失效。修正后的代码if (key backspace) { QKeyEvent press(QEvent::KeyPress, Qt::Key_Backspace, Qt::NoModifier); QKeyEvent release(QEvent::KeyRelease, Qt::Key_Backspace, Qt::NoModifier); sendEvent(focusWidget, press); sendEvent(focusWidget, release); }5.5 输入法弹出来盖住键盘现象在部分 Linux 触屏设备上点击输入框后系统自带的屏幕键盘也会弹出来和虚拟键盘叠在一起。原因系统开启了自带的输入法面板与自定义键盘冲突。解决在程序初始化时禁用系统输入法或者让自定义键盘优先。我一般这样处理qputenv(QT_IM_MODULE, qtvirtualkeyboard);这个环境变量如果系统支持会强制使用指定输入模块如果不支持则可以再设置Qt::ImQueryInput过滤掉系统输入法请求。这个坑在不同发行版上表现不一样建议在目标设备上多验证两个版本。6. 二次开发技巧把面板改成数字小键盘并换肤这个项目留了一个很好的扩展入口布局文件与面板组件分离。如果想做一个纯数字键盘只需在layouts目录下新增numeric.json内容类似这样{ rows: [ [ 1, 2, 3, del ], [ 4, 5, 6, enter ], [ 7, 8, 9, 0 ] ] }然后在KeyboardController里增加一个switchToNumeric动作加载新布局并刷新 QML 面板。具体做法是重新设置 context property并调用engine.rootContext()-setContextProperty(keyboardModel, numericModel)。为了让布局切换更平滑可以给键盘面板加一个淡入淡出动画但这属于锦上添花。换肤更简单所有颜色和字体都在KeyButton.qml和KeyboardPanel.qml里改成 QML 的property暴露出来比如backgroundColor、borderColor、keyRadius。把主题参数抽到一个单独的配置类里就能做夜间模式或不同品牌设备定制。测试时建议写一个小页面放多个类型的输入框普通文本、密码、数字。密码输入框要做掩码处理但模拟按键事件不会破坏掩码因为事件是发给控件的控件自身会处理掩码逻辑。验证方法也不复杂在键盘上依次按一遍所有按键确认每个字符都落在正确的输入框再测试删除键、回车键和布局切换。不过这个项目也有它的回报率直到现在我每接一个触屏项目都会先问一句“有没有现成的键盘方案”如果没有就把这套代码翻出来改改布局和图标直接上。从那以后每次给客户演示前我都强制走一遍“全键盘逐键测试 回车键验证 输入法冲突检查”这套流程反而再没在现场出过丑。希望帮到你。本文还有配套的精品资源点击获取