☰
Qt虚拟键盘组件:基于QWidget的触摸屏嵌入式软键盘实现
2026/10/19 9:11:08 网站建设 项目流程

简介:这是一款基于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::onKeyClicked
  • onKeyClicked 从 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 用 QMap<QPushButton*, KeyItem> 关联起来,这样点击回调里能立刻反查按键的类型和输出文本:

void VirtualKeyboard::buildButtons() { QVector<KeyItem> 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_cast<QPushButton *>(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_cast<QLineEdit *>(obj); if (edit) { m_keyboard->setActiveEdit(edit); } } return QObject::eventFilter(obj, event); } private: VirtualKeyboard *m_keyboard; };

安装过滤器时用 QApplication::instance()->installEventFilter(watcher) 监听全局事件。这样不管主界面上有几个输入框,只要用户点击了某个输入框,键盘就会记住它。

为什么不直接用 QApplication::focusWidget()?因为虚拟键盘为了不抢焦点,所有按钮都设置了 NoFocus,focusWidget() 在很多场景下返回的依然是输入框,但一旦键盘里有自定义控件、组合键或嵌入式触摸屏驱动对焦点处理不标准,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 下直接用系统 Qt:

mkdir 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.exe

Linux 下用 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 里同时使用了 C++11 的 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_cast<KeyType>(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 和焦点过滤器的设置顺序,改起来就很简单了。希望这份笔记对你有用。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询