资讯动态

Qt虚拟键盘组件:基于QWidget的触摸屏嵌入式软键盘实现

发布时间:2026/10/19 9:11:14 来源:尧图企业网站定制
简介这是一款基于Qt框架开发的虚拟小键盘组件面向需要在应用界面中临时嵌入数字或字符输入场景的开发者解决无物理键盘时无法便捷输入的问题。组件采用无边框设计不创建独立显示窗口而是作为可嵌入控件与其他UI元素共存调用时通过传入自定义myspinbox控件指针即可将输入直接作用于目标控件。压缩包体积仅2KB共3个文件包括1个.ui界面描述文件、1个.h头文件和1个.cpp源文件结构清晰适合快速阅读和移植。目前已有266人学习下载适合具备基础Qt/C知识、想了解自定义控件开发的读者。资源完整给出键盘的界面布局、类接口声明及按键事件处理逻辑重点演示了如何让虚拟键盘与派生自QSpinBox的myspinbox交互可帮助理解Qt Designer的.ui文件编译方式、继承重写以及组件间通信的思路。1. 项目定位一个 Qt 虚拟小键盘组件到底解决了什么问题做触摸屏界面时最大的痛点不是布局而是输入。一体机、自助查询机、工控屏上没有实体键盘系统自带的屏幕键盘要么弹得慢要么样式跟界面完全不搭。这份资源中的 Qt 虚拟小键盘解决了这个问题它用纯 Qt Widgets 实现了一个无边框、置顶悬浮的虚拟键盘组件键位可以自由定制支持中英文切换、Shift 大小写、退格和回车单击按键自动把文本插入当前聚焦的输入框完全不需要操作系统级别的输入法接入。适合两类人一类在做触摸屏项目需要嵌入式软键盘的 Qt 工程师另一类是桌面应用开发想要一个能随主窗口移动、可换肤的辅助输入工具。2. 技术选型与架构设计QWidget 组件化、信号槽链路与焦点管理2.1 为什么用 QWidget 而不是 QML看到虚拟键盘的第一反应很多人会想到用 QML 里的 VirtualKeyboard 模块。但在实际项目里这个选择往往会让事情变复杂。QML 键盘库体量大依赖 Qt VirtualKeyboard 模块嵌入式交叉编译要额外移植 QML 插件而且跟既有 QWidget 界面混用时paint 引擎切换容易出显示问题。这个项目选的是 QWidget 组件化方案原因很直接目标场景是把键盘当作一个可以“提升”进任意 QWidget 界面的普通控件它能 attach 到某个输入框业务方不用关心键盘内部实现。QWidget 方案还有一个优势部署时不需要额外的 QML 运行时程序编译出来就是一套标准 Qt Widgets 依赖拷到板子上只要系统里装了 Qt5Core、Qt5Gui、Qt5Widgets 就能跑。内存占用也比 QML 键盘可控对嵌入式设备更友好。2.2 整体架构无边框悬浮窗 键位表驱动架构上分为三层。底层是键位表用一个数据结构描述每个按键的文本、位置、类型中间层是键盘面板控件 VirtualKeyboard负责根据键位表创建按钮、接收信号上层是分发逻辑决定按键内容最终发送到哪里。窗口层按 Qt::Tool 类型创建配合 Qt::FramelessWindowHint、Qt::WindowStaysOnTopHint 两个窗口标志得到一个不显示在任务栏、无边框、总在最前的悬浮窗。这个窗口不主动抢占焦点所有内部按钮的 focusPolicy 都设为 Qt::NoFocus目的是让键盘在“打字”时不把焦点从输入框抢走。2.3 信号槽链路一个按键按下的完整通路按键点击后Qt 的信号槽系统负责把事件传出去。链路不长但每一步都有明确分工QPushButton::clicked 信号绑定到 VirtualKeyboard::onKeyClickedonKeyClicked 从 QGridLayout 中反查出发出信号的按钮再通过按钮到文本的映射表查出实际内容文本内容通过 edit-insert() 插入到当前活跃输入框如果外部业务层需要拦截按键则通过 keyPressed(QString) 和 enterPressed() 两个信号转发出去这套链路把 UI 渲染和业务逻辑分开了。键盘按钮只负责发出 clicked不关心它该输入到哪里哪里接收输入由焦点管理模块决定。后面改中英文键盘、数字键盘、收银专用键盘都只是改键位表不动分发逻辑。3. 核心模块拆解键位表驱动布局、按键事件分发与焦点管理3.1 键位表结构把键位从代码里彻底抽出来键盘布局如果写在硬代码里每加一个按键就要重新调整坐标和布局维护成本很高。这个项目的做法是定义一个 KeyItem 结构体用纯数据描述键盘enum KeyType { Key_Normal, // 普通字符键字母、数字、符号 Key_Space, // 空格键需要跨列显示 Key_Backspace, // 退格键 Key_Shift, // Shift 切换键 Key_Enter, // 回车键 Key_SwitchLang // 中英文切换键 }; struct KeyItem { QString text; // 按键默认显示的文本 QString shiftedText; // 按下 Shift 后显示的文本 QString outputText; // 实际输出到输入框的内容 int row; // 在 QGridLayout 中的行 int col; // 在 QGridLayout 中的列 int rowSpan; // 行跨度空格键用 int colSpan; // 列跨度回车键用 KeyType type; };初始化键位表就是往数组里填数据我一般会写一个 initKeyTable() 函数返回 QVector 把字母区、数字区、功能键区按行排列。字母键盘常见布局是三行字母加一行功能键第一行 QWERTYUIOP第二行 ASDFGHJKL第三行 Shift 加 ZXCVBNM 加退格第四行是空格和回车。每个按键的 row、col 对应 QGridLayout 里的格子位置colSpan 让空格键占 5 个格子。3.2 按键生产与事件分发从按钮点击到文本插入根据键位表创建按钮的逻辑很直接。遍历 KeyItem 数组为每一项 new 一个 QPushButton设置文本和固定大小放进 QGridLayout 指定位置然后把按钮对象和 KeyItem 用 QMapQPushButton*, KeyItem 关联起来这样点击回调里能立刻反查按键的类型和输出文本void VirtualKeyboard::buildButtons() { QVectorKeyItem keys initKeyTable(); for (const KeyItem item : keys) { QPushButton *btn new QPushButton(item.text, this); btn-setFocusPolicy(Qt::NoFocus); // 关键不能让按钮抢走输入框焦点 btn-setFixedSize(64, 44); m_layout-addWidget(btn, item.row, item.col, item.rowSpan, item.colSpan); m_keyMap.insert(btn, item); connect(btn, QPushButton::clicked, this, VirtualKeyboard::onKeyClicked); } }focusPolicy 设为 NoFocus 是必不可少的一步。如果不设置触摸屏上点击键盘按钮后焦点会从 QLineEdit 跑到 QPushButton 上后续所有按键都输入不进去。onKeyClicked 是事件入口逻辑不复杂但要把类型分清楚void VirtualKeyboard::onKeyClicked() { QPushButton *btn qobject_castQPushButton *(sender()); if (!btn) return; KeyItem item m_keyMap.value(btn); switch (item.type) { case Key_Backspace: if (m_activeEdit) m_activeEdit-backspace(); break; case Key_Shift: m_shifted !m_shifted; updateKeyTexts(); // 刷新按钮上的显示文本 break; case Key_Enter: // 优先发给业务层业务层不处理则模拟回车 emit enterPressed(); if (m_activeEdit) m_activeEdit-returnPressed(); break; default: QString out m_shifted !item.shiftedText.isEmpty() ? item.shiftedText : item.outputText; if (m_activeEdit) { m_activeEdit-insert(out); } emit keyPressed(out); break; } }用 sender() 反查按钮对象是 Qt 里处理多个同类控件事件的惯用做法避免了给每个按钮单独写一个 lambda 回调。参数说明m_shifted 是成员变量控制大小写切换m_activeEdit 是当前活跃输入框指针由焦点管理模块维护keyPressed 信号是给业务层用的比如密码输入框不想直接 insert你可以拦截这个信号做自定义处理。3.3 焦点管理用事件过滤器维护当前活跃输入框虚拟键盘真正麻烦的不是建按钮而是搞清楚“当前要往哪个输入框里输入”。最常见的问题是界面里同时有账号输入框和密码输入框点击输入框后焦点切到键盘按钮导致分不清该给谁输入。资源里用的方案是事件过滤器。在键盘组件里安装一个全局事件过滤器监听所有 QLineEdit 的 FocusIn 事件把当前获得焦点的输入框记录到 m_activeEdit 成员变量里class FocusWatcher : public QObject { public: explicit FocusWatcher(VirtualKeyboard *keyboard) : QObject(keyboard), m_keyboard(keyboard) {} protected: bool eventFilter(QObject *obj, QEvent *event) override { if (event-type() QEvent::FocusIn) { QLineEdit *edit qobject_castQLineEdit *(obj); if (edit) { m_keyboard-setActiveEdit(edit); } } return QObject::eventFilter(obj, event); } private: VirtualKeyboard *m_keyboard; };安装过滤器时用 QApplication::instance()-installEventFilter(watcher) 监听全局事件。这样不管主界面上有几个输入框只要用户点击了某个输入框键盘就会记住它。为什么不直接用 QApplication::focusWidget()因为虚拟键盘为了不抢焦点所有按钮都设置了 NoFocusfocusWidget() 在很多场景下返回的依然是输入框但一旦键盘里有自定义控件、组合键或嵌入式触摸屏驱动对焦点处理不标准focusWidget() 的结果就会变得不可靠。显式维护 m_activeEdit 更稳排查问题时思路也简单键盘往哪里输出看 m_activeEdit 的值就知道。3.4 键盘窗口拖动与置顶细节键盘作为悬浮窗必须能拖动否则会挡住界面上重要的按钮。拖动通过重写 mousePressEvent 和 mouseMoveEvent 实现void VirtualKeyboard::mousePressEvent(QMouseEvent *event) { if (event-button() Qt::LeftButton) { m_dragPosition event-globalPos() - frameGeometry().topLeft(); } QWidget::mousePressEvent(event); } void VirtualKeyboard::mouseMoveEvent(QMouseEvent *event) { if (event-buttons() Qt::LeftButton) { move(event-globalPos() - m_dragPosition); } QWidget::mouseMoveEvent(event); }值得注意的细节是 m_dragPosition 存的是“按下位置与窗口左上角的偏移”不是窗口位置本身。移动时用鼠标全局坐标减去偏移量得到新窗口坐标这样拖动时窗口不会相对鼠标“跳一下”。置顶方面除了 WindowStaysOnTopHint 标志在 Linux 桌面环境下还需要窗口管理器配合部分轻量级 WM 会忽略这个标志我通常会在 showEvent 里额外调用 raiseAndActivate() 方法强制置顶。4. 编译与运行从 qmake/CMake 到触摸屏上的完整部署4.1 工程文件配置这套虚拟键盘工程同时兼容 qmake 和 CMake 两种构建方式。qmake 的 pro 文件很精简QT core gui widgets TARGET VirtualKeyboardDemo TEMPLATE app SOURCES main.cpp \ virtualkeyboard.cpp HEADERS virtualkeyboard.h \ keyitem.h如果用 Qt6需要把 widgets 模块单独列出Qt6 把 QWidget 相关类拆到了 Qt6::Widgets 里。如果项目里同时用了 Qt5 和 Qt6 两套环境建议用条件判断greaterThan(QT_MAJOR_VERSION, 4): QT widgets工程的关键点是 AUTOMOC 不能被关掉。Qt 的信号槽依赖 moc 元对象编译器如果源文件里有 Q_OBJECT 宏但没开启 AUTOMOC链接时会报 undefined reference这是新手第一次跑 Qt 工程最常遇到的同学。4.2 编译流程在 Qt Creator 里打开 pro 文件选择构建套件后直接点构建即可。Qt Creator 会自动调用 qmake 生成 Makefile再交给编译器完成构建。命令行编译也很简单。Windows 下要先用 Qt 自带的“Qt 5.15.2 (MSVC 64-bit)”命令行环境Linux 下直接用系统 Qtmkdir build cd build qmake ../VirtualKeyboardDemo.pro make -j4交叉编译嵌入式版本时主要是给 qmake 指定交叉工具链的 mkspec。用树莓派或类似 ARM 平台的交叉环境时命令会变成类似 qmake -spec linux-arm-gnueabi-g 的方式具体取决于目标平台和 Qt 编译时生成的 mkspec 名称。4.3 运行与部署桌面环境直接跑可执行文件就能看效果。部署时最主要的工作是带上 Qt 的插件目录Windows 上我一般用 windeployqt 工具自动收集依赖windeployqt VirtualKeyboardDemo.exeLinux 下用 linuxdeployqt 或直接手动拷贝 libQt5Widgets.so.5 等动态库到可执行文件同级目录。嵌入式设备上部署时还要确认板子里有没有挂在 /usr/lib/qt5/plugins/platforms 下的 linuxfb 或 eglfs 平台插件否则会出现“could not find or load the Qt platform plugin”的错误。运行起来的效果是一个无边框键盘浮在主界面上点击界面上任意 QLineEdit键盘自动记录焦点点击键盘上的按键字符插入对应的输入框整个过程键盘不会抢走输入焦点。键盘默认固定在屏幕底部居中可以用鼠标拖动到任意位置。5. 避坑实录五个触摸键盘开发中常见的“玄学”问题5.1 点击键盘按键后输入框的光标消失了这是虚拟键盘最容易翻车的点。现象是键盘能弹出来但在键盘上按第一个键没问题按下第二个键后输入框的光标消失后续按键全部无效。原因很简单第一个按键点击后焦点从输入框转移到了键盘按钮上m_activeEdit 在 FocusIn 事件里记录到的活跃输入框变成了某个按钮的父窗口在 insert 时内容插不进去。解决方式是双管齐下。第一键盘面板和所有按键统一设置 setFocusPolicy(Qt::NoFocus)让键盘自身没有焦点获取能力第二在 FocusWatcher 的事件过滤里不仅记录 FocusIn还把鼠标点击键盘区域的情况单独判断确保键盘内部控件永远不会成为“当前活跃输入框”。从那以后我每次都是先设置 NoFocus再装过滤器顺序反了会漏掉第一次失焦。5.2 中英文切换时按键文字没错输进去的内容错了现象是键盘上显示的是英文字母但 insert 进输入框的是拼音或中文符号。原因通常是只改了按钮的显示文本没有同步改输出映射表。例如中文字符需要走输入法上屏逻辑不是简单 insert 能解决的。解决方法是把“显示文本”和“输出文本”剥离开。KeyItem 里 text 管显示outputText 管输出切换布局时同时更新按钮文本和映射表。中英文切换如果是输入法级别的功能建议只做一个“切换键”真正的中文输入交给系统输入法键盘只负责把 raw keyevent 发给输入法进程而不是自己拼中文。5.3 无边框键盘在部分 Windows 机器上无法置顶Fluctuating 的现象是同一份编译好的 exe在一台 Windows 机器上键盘能始终浮在最前面另一台机器上只要主窗口一聚焦键盘就被压到后面。原因是 WindowStaysOnTopHint 在部分 GPU 合成器驱动下不可靠。解决方法是给键盘窗口额外设置 Qt::Tool 类型窗口属性同时小心处理一个冲突当主窗口设置了 Tool 属性时子窗口再设置 Tool 会干扰 Windows 窗口管理器的层级判定。我一般在 setWindowFlags 里组合使用setWindowFlags(Qt::Dialog | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::Tool);Qt::Tool 让系统把它当浮动工具窗处理和主窗口交互时不抢焦点置顶的优先级也更高。5.4 嵌入式 ARM 板子上键盘背景变黑按钮看不见现象是交叉编译后的程序在板子上运行键盘弹出来整个背景是黑色按钮文字也看不清。原因通常和透明背景有关。很多开发者习惯给键盘窗口设置 Qt::WA_TranslucentBackground 做圆角效果但 linuxfb 平台驱动的合成器不支持 alpha 通道透明合成退化成黑色填充。解决方法是检测运行平台在 linuxfb 下禁用透明背景if (QGuiApplication::platformName() linuxfb) { setAttribute(Qt::WA_TranslucentBackground, false); }同时用不透明的纯色做背景圆角阴影效果只在 Windows/macOS 上启用。这个问题排查起来很费时间因为桌面版完全正常交叉编译后才暴露。5.5 qmake 链接错误虚函数表未定义现象是 moc 相关的头文件没被处理报错类似 undefined reference to vtable for VirtualKeyboard。大多数情况是两种情况要么忘记了 Q_OBJECT 宏所在头文件没加入 HEADERS 列表要么在 Qt5 里同时使用了 C11 的 final 关键字导致 moc 生成代码无法正常覆盖虚函数表。解决方式是检查 pro 文件里 HEADERS 是否齐全特别是只写了 SOURCES 没写 HEADERS 的情况moc 默认只处理 HEADERS 里的头文件。用 CMake 构建时则需要确保 set(CMAKE_AUTOMOC ON) 写在 project() 之后。6. 二次开发把键位表、皮肤和业务逻辑进一步剥离开来这个组件可以继续往两个方向拆一是键位表独立成配置文件二是业务逻辑回调化。键位表如果还写在 C 代码里每换一个客户、每适配一种设备就要重新编译。更通用的做法是把 KeyItem 数组序列化成 QJsonArray运行时加载QJsonArray keys QJsonDocument::fromJson(jsonStr).array(); for (const QJsonValue val : keys) { QJsonObject obj val.toObject(); KeyItem item; item.text obj[text].toString(); item.outputText obj[output].toString(); item.row obj[row].toInt(); item.col obj[col].toInt(); item.type static_castKeyType(obj[type].toInt()); m_keys.append(item); }这样改键位布局就不再碰主代码把 JSON 文件丢给实施人员都能搞定。我在实际项目里就把默认键盘、字母键盘、数字小键盘三套布局放在三个 JSON 文件里启动时根据配置加载对应文件一套代码维护三种键盘。皮肤方面控件只要支持 QSS就能做到运行时换肤。给键盘面板和按钮加上一个可选的 objectName配合 QSS 样式表文件实现一套浅色、一套深色皮肤QPushButton#keyButton { border-radius: 6px; background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #f7f7f7, stop:1 #dedede); font-size: 16px; color: #333; }业务逻辑集成时注意 Enter 键的分发。资源里的 enterPressed 信号是暴露给外部业务层的关键接口比如登录界面里用户按完回车应该直接触发登录校验而不只是让输入框发一个 returnPressed。我一般会在主窗口里 connect 这个信号把它接到登录按钮的点击逻辑上。这个组件我迭代过几次后来每做一个触摸屏案子都会强制把键位表单独抽出 JSON、把皮肤单独放 QSS、把信号回调暴露给业务层再验证一遍 NoFocus 和焦点过滤器的设置顺序改起来就很简单了。希望这份笔记对你有用。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑