简介一款基于Qt开发的虚拟小键盘实现面向需要无物理键盘输入或希望为界面内嵌软键盘的Qt开发者。它采用无边框嵌入方式不创建独立窗口而是接收自定义myspinbox继承自QSpinBox指针将键盘的按键输入直接作用于该控件从而灵活融入现有UI特别适合设置面板、登录框等场景也便于定制按键风格与布局。压缩包内共3个文件——cpp源码、h头文件与ui界面描述文件分别实现键盘逻辑、类接口和可视化布局整体大小仅2KB结构非常精简适合逐文件研读。已有266人浏览学习。通过这个小项目开发者可以掌握Qt自定义控件如QSpinBox派生类的创建与信号槽连接方式也能了解如何利用Qt Designer生成.ui文件并配合代码实现嵌入式UI。代码中体现的无边框控件嵌入与外部控件交互思路对开发触摸屏软键盘、工控机数字输入等场景有直接参考价值。资源虽小但包含了从类声明、实现到界面设计的完整环节适合中级Qt学习者作为实战样例练习。1. 做 Qt 开发的人绕不开的一个问题虚拟小键盘到底该不该自己造做 Qt 开发的人尤其是碰过嵌入式触摸屏项目的对虚拟键盘肯定有一肚子话。设备上没实体键盘系统自带输入法在嵌入式环境里要么太臃肿、要么压根没被移植最后的出路往往是自己写一个小键盘。这套qt虚拟小键盘.zip就是我拆过的一套典型实现基于 Qt Widgets 的按键网格、独立键盘窗口、信号槽回填文本不依赖第三方库。适合正在做触摸屏交互、需要快速集成键盘功能的人。下面按结构、接入、避坑、进阶的顺序拆一遍代码能直接抄。2. 拆包看结构键盘的三个模块与一条信号链拿到这套代码别急着编译先看文件结构。虚拟键盘看起来简单其实绕不开三层界面层负责画按键网格数据层负责键位表、shift 映射、中英文切换逻辑层负责决定把字符回填到哪个输入框。我见过不少人把所有按键写在主窗口里结果耦合在一起后面改一个键位要动三处代码。这套包把三层拆开键盘本体编译成一个独立的 widget 类对嵌入式集成特别友好。2.1 界面层QWidget 网格还是 QML 布局先回答一个绕不开的问题用 QWidget 还是 QML。如果项目是纯 Widgets 应用键盘单独用 QML 还得额外起一个 QQuickWindow渲染线程多一层跨语言调信号也麻烦如果主界面本来就是 QML那键盘直接用 GridView 更顺。这套包选的是 QWidget QGridLayout原因很简单目标板子不带 GPUQML 场景图在软件渲染下性能吃亏QPushButton 网格在嵌入式 Linux 环境里基本是零压力。按键网格的构建代码是这套包的核心逻辑上就是两层循环// KeyboardView.cpp 中构建按键网格 for (int row 0; row model-rowCount(); row) { for (int col 0; col model-columnCount(row); col) { QPushButton *btn new QPushButton(model-keyAt(row, col), this); btn-setFixedHeight(m_keyHeight); // 固定高度避免布局闪跳 btn-setFocusPolicy(Qt::NoFocus); // 键盘框体不吃焦点 m_buttons[row][col] btn; connect(btn, QPushButton::clicked, this, [this, row, col]() { QString text model-keyAt(row, col, m_shifted); emit keyPressed(text, m_shifted); }); m_grid-addWidget(btn, row, col); } }这里有个容易被忽略的点setFocusPolicy(Qt::NoFocus)和setFixedHeight必须配齐。NoFocus 保证按键点击后焦点不落在键盘按钮上否则键盘会跟输入框抢焦点后面第 4 章会细说固定高度避免键位表动态切换行数时整个键盘窗体重算尺寸也方便主界面按固定高度预留空间。emit keyPressed(text, m_shifted)里的m_shifted是当前是否处于 shift 状态这个标记要跟着信号走因为逻辑层要根据它判断是回填普通字符还是执行功能键。2.2 数据层键位表、shift 映射与回填目标键位表不要硬编码在构造函数里维护一份 JSON 配置是最稳的。这套包里keyboard_layout.json的结构大概是这样{ rows: [ [1, 2, 3, 4, 5, 6, 7, 8, 9, 0], [q, w, e, r, t, y, u, i, o, p], [a, s, d, f, g, h, j, k, l], [Shift, z, x, c, v, b, n, m, Backspace] ], shifted: [ [!, , #, $, %, ^, , *, (, )], [Q, W, E, R, T, Y, U, I, O, P], [A, S, D, F, G, H, J, K, L], [Caps, Z, X, C, V, B, N, M, Delete] ] }加载这部分由数据层完成解析只做一次后续按键查表走索引而不是字符串匹配bool KeyboardModel::loadFromFile(const QString path) { QFile f(path); if (!f.open(QFile::ReadOnly)) return false; const QJsonObject root QJsonDocument::fromJson(f.readAll()).object(); const QJsonArray rowArray root.value(rows).toArray(); const QJsonArray shiftArray root.value(shifted).toArray(); m_rows.clear(); for (int r 0; r rowArray.size(); r) { KeyRow row; for (const QJsonValue v : rowArray.at(r).toArray()) row.normal v.toString(); for (const QJsonValue v : shiftArray.at(r).toArray()) row.shifted v.toString(); m_rows.append(row); } return !m_rows.isEmpty(); }提示shifted的行数必须和rows一致否则keyAt(row, col, true)会越界翻车。加载函数里要加行数校验不等就直接返回 false宁可在启动时报错也不要运行到一半才崩。这个数据结构的选型理由是普通字符和功能键统一用字符串表达。功能键的字符串是约定好的比如Shift、Backspace、Caps它们在normal表里占了格子但按键时逻辑层识别到这些字符串就走功能分支不走文本插入。2.3 信号链从 clicked 到 insertText 的完整路径按键发出keyPressed(text)之后回填逻辑由键盘对象的持有方处理。完整链路是按钮 clicked → 键盘发出 keyPressed → 持有方拿到焦点控件 → 调用 insert()。这套包里的常见组织方式// MainWindow 里组织键盘 connect(m_keyboard, VirtualKeyboard::keyPressed, this, [this](const QString text) { QWidget *target QApplication::focusWidget(); auto *edit qobject_castQLineEdit *(target); if (!edit) return; // 焦点不在输入框上就丢弃 if (text Backspace) { QKeyEvent ev(QEvent::KeyPress, Qt::Key_Backspace, Qt::NoModifier); QApplication::sendEvent(edit, ev); } else if (text Shift) { m_keyboard-toggleShift(); } else { edit-insert(text); } });这里有两个设计取舍。第一用QApplication::focusWidget()实时取焦点控件而不是构造函数里存一个固定的QLineEdit *指针。界面上一般有多个输入框用户点到哪儿就输入到哪儿固定指针必然踩坑。第二Backspace 这类控制键用QKeyEvent发给输入框而不是直接删字符是为了让输入框内部的 undo 栈、光标逻辑保持一致。直接edit-setText(edit-text().left(...))会破坏光标位置也会让 undo 记录错乱。这条信号链是整个键盘最核心的路径后面所有避坑都围着它转。焦点只要断一环表现就是点了没反应或者字符上错位置。3. 两种接入姿势顶层浮窗与内嵌布局的实测参数虚拟键盘接进宿主界面有两种主流姿势独立顶层浮窗和内嵌布局。这套包两种都支持编译开关控制。实测下来没有谁绝对好取决于项目有几个输入框、键盘要不要跟随焦点移动。顶层浮窗适合单输入框场景键盘可以贴着输入框弹出内嵌布局适合工控面板那种键盘固定占下半屏的场景。3.1 顶层浮窗窗口标志位与焦点接管顺序顶层浮窗方式的窗口标志位组合是这套包最值得抄的参数// VirtualKeyboard.cpp 构造函数里 setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint); setAttribute(Qt::WA_ShowWithoutActivating); setFocusPolicy(Qt::ClickFocus);逐项解释一下。Qt::Tool代替Qt::Popup是关键Popup 窗口点键盘以外的区域会自动 hide做键盘会让开发者崩溃Tool 则作为工具窗独立存在不会抢主窗口的任务栏条目。WA_ShowWithoutActivating决定键盘弹出时是否激活自身——如果不设键盘弹出来会把输入框焦点抢走用户敲第一个字符前光标就已经消失了。ClickFocus保证键盘窗体本身能接收鼠标事件但按键按钮上设了NoFocus所以焦点不会落到单个按键上。弹出顺序的坑很多人踩过先show()键盘再让输入框获得焦点在某些 touch 驱动下焦点会被键盘抢回来。我一般用全局焦点变化信号来驱动键盘弹出// MainWindow 里监听全局焦点切换 connect(qApp, QApplication::focusChanged, this, [this](QWidget *old, QWidget *now) { Q_UNUSED(old); auto *edit qobject_castQLineEdit *(now); if (edit) { m_keyboard-moveTo(edit-mapToGlobal(QPoint(0, edit-height()))); m_keyboard-show(); } else { m_keyboard-hide(); } });注意moveTo()要在show()之前调用有些窗口管理器在 show 之后再 move 会闪一下位置。键盘默认定位在输入框正下方如果屏幕底部空间不够需要做一个倒置判断edit-mapToGlobal(QPoint(0, 0))的 y 坐标小于键盘高度时键盘放在输入框上方。3.2 内嵌布局stretch 因子与 resizeEvent 联动内嵌姿势适合那种键盘一直显示、不随焦点跳动的场景比如设备下半个区域固定是键盘。做法是把键盘当成普通 widget 塞进主布局// mainwindow.cpp 里组织布局 m_mainArea new QWidget(this); // 上面是可滚动的内容区 m_keyboard new VirtualKeyboard(this); QVBoxLayout *layout new QVBoxLayout(this); layout-setSpacing(0); layout-addWidget(m_mainArea, 3); // stretch3 layout-addWidget(m_keyboard, 1); // stretch1stretch 取值要实测3:1 是一个比较稳的起点。stretch4 会让键盘太矮、按键不好按stretch2 又会让内容区被压缩。键盘内部按键行高是固定的所以键盘区域实际高度是固定的stretch 生效的只是让内容区把剩余空间吃掉——这正好是想要的效果。内嵌模式下resizeEvent需要额外处理一件事窗口宽度变化时按键宽度要跟随否则右侧会留白。这套包的做法是按列数均分宽度void VirtualKeyboard::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); const int cols model-maxColumnCount(); const int spacing m_grid-spacing(); const int widthPerKey (event-size().width() - (cols - 1) * spacing) / cols; for (int row 0; row model-rowCount(); row) { const int colsInRow model-columnCount(row); for (int col 0; col colsInRow; col) { m_buttons[row][col]-setFixedWidth(widthPerKey); } } }这套计算在 800×480 和 1920×1080 两种分辨率下都实测过按键不会变形。注意别用等宽字体做键面字符宽度差异大用默认字体加Qt::AlignCenter最省事。键面上字符宽度差异大的时候可以在 JSON 键位表里给窄字符补空格这个属于微调看效果来。3.3 键位表与皮肤改 JSON 就能换布局直接改keyboard_layout.json里的字符串数组就能增删键不需要动 C。但有三个边界要记住每行键数不要超过 12 个包里的最小键宽按 12 列算超过会溢出窗口功能键标记名大小写敏感Backspace写成backspace会被当成普通字符上屏Shift 和 Caps 键在 shifted 表里那一格填空字符串表示该键按下时不产生字符、只切换状态。注意改完 JSON 要重启应用验证一次。键位表和 QSS 是两套缓存部分版本代码热更新不会重新加载 QSS界面显示旧布局但按键行为已经是新的这种表里不一的状态最容易让人误判。皮肤方面这套包默认用 QSS 控制按键外观背景色、按下色、圆角都集中在style.qss。实测嵌入式平台下 border-radius 会触发软件渲染的圆角裁剪建议只在 hover 和 pressed 状态做简单颜色变化不要加阴影和渐变那些在低端板子上会明显掉帧。两种接入姿势我的选择逻辑单输入框、键盘要跟随焦点用顶层浮窗多输入框、固定工位设备用内嵌。别做了一半再切换窗口标志和布局联动是两套逻辑混用翻车概率极高。4. 避坑与排查焦点、穿透和输入法打架的四个实录虚拟键盘的坑十个里有八个在焦点。下面四条都是实际项目里出现过的。前三条几乎每个做键盘的人都会撞上一两个最后一条是嵌入式平台的通病。4.1 第二次点击就没反应焦点被输入框的点击策略吃掉现象键盘弹出后第一次按键正常第二次点击任何键都没反应键盘边框高亮也消失了。原因键盘窗口的 focus policy 不对第一次点击后焦点被输入框接管键盘窗体进入失活状态。部分嵌入式 touch 驱动对失活窗口的鼠标事件直接丢弃表现就是点几下就死。解决键盘窗口setFocusPolicy(Qt::ClickFocus)并在 showEvent 里强制激活窗口void VirtualKeyboard::showEvent(QShowEvent *event) { QWidget::showEvent(event); raise(); activateWindow(); // 只激活窗口不抢输入框焦点 }这里要注意activateWindow()激活的是键盘所在窗口不是把键盘设为焦点控件。配合WA_ShowWithoutActivating一起用既保证键盘能收到鼠标事件又不影响输入框光标。检查顺序先确认窗口标志位不是 Popup再确认 showEvent 里做了 raise大多数第二次点击失效都能解决。4.2 键盘一弹出来主界面布局就被顶跳现象内嵌模式下键盘 widget 一加入布局主界面的列表、表格区域被压缩到半截甚至出现横向滚动条。原因键盘作为普通子 widget 时父布局会把它当普通组件参与分配更隐蔽的是部分平台会把全屏可见的键盘 widget 误判为系统输入面板触发预留空间逻辑。两件事叠加布局自然乱套。解决方案一是改独立窗口见 3.1方案二是内嵌时给键盘容器固定尺寸策略主界面的滚动区域在键盘显示时手动调整void MainWindow::onKeyboardShown(bool visible) { m_scrollArea-setMaximumHeight(visible ? QApplication::primaryScreen()-availableGeometry().height() - m_keyboard-height() : QWIDGETSIZE_MAX); }内嵌场景另一个易错点是QScrollArea::setWidgetResizable(true)会让子内容强制填满视口键盘显示后视口高度变化内容区高度被同步压缩。上面的setMaximumHeight是实测有效的办法比在布局里反复调 stretch 稳定得多。如果键盘显示/隐藏瞬间布局还会抖给高度变化加一个QPropertyAnimation做 150ms 的过渡视觉上平滑很多。4.3 中英文切换后回填的是上一个字符现象键盘上有中英文切换键切换后第一次按键上屏的是切换前的字符第二次才正确。原因切换语言只更新了按钮文字但输入法上下文composition里残留了上一次的拼音串。Qt 的回填走QLineEdit::insert()时输入法把残留串和当前按键一起提交了。多见于带拼音输入法移植的板子。解决切换语言时先重置输入法上下文再刷新键位表void VirtualKeyboard::switchLanguage() { QInputMethod *im qGuiApp-inputMethod(); if (im) im-reset(); // 清掉正在编辑的拼音串 model-switchLanguage(lang); refreshKeys(); // 按新语言重建按钮文字 }隐藏前置条件QInputMethod::reset()只在有输入框聚焦时有效所以切换语言的调用必须保证焦点仍然落在输入框上。如果切换入口在键盘内部、焦点已经掉到键盘上要先让输入框setFocus()再 reset最后把键盘重新激活。顺序错一步的表现就是切换无反应或者上次字符残留到这次输入里。4.4 嵌入式设备上按键延迟明显现象低配板子上按键按下到字符上屏有接近 300ms 延迟拖动窗口时掉帧明显。原因延迟基本来自槽函数里的重复劳动。常见三处每次按键都重新解析 JSON 键位表每次点击都qDeleteAll旧按钮重建键盘窗口用了带阴影渐变的 QSS软件渲染下每次重绘代价极高。解决键位表启动时解析一次存成 QList 结构按键时查索引按钮创建后复用切换语言只setText()不改对象QSS 去掉复杂阴影和渐变。另外如果键盘和主界面不在同一线程——这种情况不常见但确实有人这么干——确认信号连接是不是Qt::QueuedConnection跨线程事件排队累加也会有延迟感。排查这类问题用 IDE 的性能分析器抓主线程卡点抓完对照上面三处逐项排除。提示按键按下到字符上屏的延迟最后才怀疑键盘本体的信号效率。大部分工程案例里真正的瓶颈在输入框的验证器或同步的数据库查询阻塞了主线程键盘只是背锅。5. 进阶玩法shift 三态状态机与数字键位自适应键盘做到能稳定输入下一步是让它聪明一点。这里讲这套包里最能提升体验的两个机制shift 三态状态机和数字键位自适应。两者都只换键位表文字、不重建按钮对象性能开销可以忽略。shift 状态机的核心是定义三态循环enum ShiftState { Idle, Shifted, CapsLock }; void VirtualKeyboard::toggleShift() { switch (m_shiftState) { case Idle: m_shiftState Shifted; break; case Shifted: m_shiftState CapsLock; break; case CapsLock: m_shiftState Idle; break; } applyShiftVisual(m_shiftState); // 更新按钮文字与高亮 }三态而不是两态是因为触摸键盘上临时一个大写字母和连续输入一段大写文本是两种场景。Shift 键的视觉也要分三种Idle 显示ShiftShifted 显示加亮的ShiftCapsLock 显示Caps并常亮。状态切换后applyShiftVisual遍历所有按钮更新文字这个操作只调用setText和setStyleSheet不会触发布局重算实测在低端板子上也是毫秒级。数字键位自适应读取焦点控件的输入法提示自动切换到纯数字键位表void VirtualKeyboard::updateForTarget(QWidget *target) { auto *edit qobject_castQLineEdit *(target); if (!edit) return; const auto hints edit-inputMethodHints(); if (hints Qt::ImhDigitsOnly || hints Qt::ImhFormattedNumbersOnly) { model-setLayout(numeric); // 只保留数字、小数点、Backspace } else { model-setLayout(full); // 恢复全键盘 } refreshKeys(); }这个需求来自一个血泪场景设备上的拨号盘居然能打出字母客户直接截图投诉。接入后要验证的是ImhFormattedNumbersOnly和ImhDigitsOnly的优先级实测同一个输入框可能同时带多个 hint 位按位与判断不会漏。最后建议两个验证习惯。一是回归脚本用 Qt Test 把键盘所有键循环点一遍断言输入框文本等于期望值// 最小回归自动点完全部按键对比结果文本 for (int r 0; r kbd-rowCount(); r) { for (int c 0; c kbd-columnCount(); c) { QTest::mouseClick(kbd-buttonAt(r, c), Qt::LeftButton); } } QCOMPARE(lineEdit-text(), expected);二是实机验证嵌入式板上连续输入 200 个字符盯内存曲线和按键延迟曲线两个验证都过了再发布。从那以后我每次接键盘模块都强制走一遍这个回归脚本改键位表、改状态机之后跑不通就不提交。这套qt虚拟小键盘.zip的工程里已经带了一个能跑的测试工程把键盘接进你的界面改改 JSON 就能直接复现上面的全部逻辑。希望帮到你。本文还有配套的精品资源点击获取