【Qt实现虚拟键盘】

Qt实现虚拟键盘

  • 🌟项目分析
  • 🌟实现方式
  • 🌟开发流程

在这里插入图片描述

🌟项目分析

  • 需求:为Linux环境下提供可便捷使用的虚拟键盘
  • OS环境:Windows 7/11、CentOS 7
  • 开发语言:Qt/C++
  • IDE:QtCreator 、Qt5.14.2
  • 功能:支持中/英文/数字键盘输入、支持中文拼音输入法、支持半/全角标点输入

🌟实现方式

  • 方式一:调用第三方程序。windows下调用系统自带虚拟键盘。Linux下通过安装第三方虚拟键盘OnKeyboard,在程序中调用
    在这里插入图片描述

  • 方式二:使用Qt自带虚拟键盘。通过引入虚拟键盘模块,检测到程序内的输入控件焦点后,自动显示。
    在这里插入图片描述

  • 方式三:自定义虚拟键盘。设计虚拟键盘布局,实现键盘按键效果,接入谷歌中文拼音库。
    在这里插入图片描述
    本文接下来主要讲解方式三的实现。

🌟开发流程

  • 谷歌中文拼音库编译:
    1. 点击此处下载项目源码。
      在这里插入图片描述
    2. 打开QtCreator,导入该项目。在pinyinime.pro中添加下面命令CONFIG += staticlib。选择qmake并构建,在构建目录中可以看到,生成两个库文件:pinyinime.dll和pinyinime.lib,这两个库文件即为使用谷歌拼音库所需。
      在这里插入图片描述
  • 新建虚拟键盘项目:
    1. 创建QWidget项目。
      在这里插入图片描述
      2.选择构建套件。
      在这里插入图片描述
    2. 在项目路径下,创建文件夹lib、include、build、bin。拷贝生成的pinyinime.dll放入bin文件夹下,pinyinime.dll放入lib文件夹下。将pinyinime项目的src文件夹中所有文件拷贝到include文件夹中。
      在这里插入图片描述
    3. 修改VirtualKeyboard.pro文件,设置项目构建配置、可执行文件生成路径、第三方依赖引用、头文件引用。
QT       += core guigreaterThan(QT_MAJOR_VERSION, 4): QT += widgetsCONFIG += c++17
CONFIG -= debug_and_release# You can make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0DESTDIR += $$PWD/binINCLUDEPATH += $$PWD/includeSOURCES += \main.cpp \keyboard.cppHEADERS += \keyboard.hFORMS += \keyboard.ui# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += targetwin32:CONFIG(release, debug|release): LIBS += -L$$PWD/lib/ -lpinyinime
else:win32:CONFIG(debug, debug|release): LIBS += -L$$PWD/lib/ -lpinyinimedINCLUDEPATH += $$PWD/include
DEPENDPATH += $$PWD/include
  • 虚拟键盘UI设计:

    1. 利用StackedWidget分别存放中/英文、数字、符号所对应的界面。
    2. 留出一个布局控件,用于存放候选词显示控件。
    3. 候选词数量级较小,通过使用QListWidget横向展示即可实现需求。
    4. 自定义标题栏,为了方便后期更换样式,适应使用场景。下面展示三种方式的键盘布局。
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
  • 候选词类设计:

    1. 继承QListWidget,重写接口和信号。
    2. 集成对于谷歌拼音库的调用。
  • 下面是关键代码展示:

//调用谷歌拼音库搜索,获取候选词
QList<QString> CandidateWidget::search(const QString &pinyinPrefix)
{QList<QString> candidateList;// 1. 转换拼音前缀为小写QString normalizedPrefix = pinyinPrefix.toLower();// 重置搜索im_reset_search();// 4. 搜索拼音对应的汉字候选QByteArray bytearray(normalizedPrefix.toUtf8());char *pinyin(bytearray.data());size_t cand_num = im_search(pinyin, bytearray.size());// 5. 获取候选词char16 *cand_buf = new char16[max_decoded_length];for (size_t i = 0; i < cand_num; i++){char16 *cand = im_get_candidate(i, cand_buf, max_decoded_length);if (cand){QString candidateStr = QString::fromUtf16(cand);if (i == 0) candidateStr.remove(0, im_get_fixed_len()); // 去除固定拼音部分candidateList.append(candidateStr); // 加入候选词列表}}// 6. 释放资源delete[] cand_buf;// 7. 返回候选词列表return candidateList;
}
  • 项目主体类设计:
  1. 考虑到对于内存的管理以及场景的评估,这里将其封装成了单例工具类,方便在项目中使用。
  2. 实现候选词模块的嵌入。
  3. 实现UI中按键的响应函数,以及不同键盘布局之间的跳转。
  4. 处理焦点变化,实现实时检测焦点,可以动态更换交互控件。(需要在主应用中重写事件过滤器,检测焦点进入事件。并区分使用交互控件,发出全局焦点变化信号

展示关键部分代码:

//单例类的实现方式:
//Keyboard.h
public:// 获取单例实例static Keyboard *getInstance(QWidget *parent = nullptr);// 销毁单例实例static void destroyInstance();
private://声明静态实例static Keyboard* m_instance;//存放候选词ListWidgetCandidateWidget *m_pChineseWidget;  //Keyboard.cpp
// 定义静态实例
Keyboard* Keyboard::m_instance = nullptr;
// 获取单例实例
Keyboard* Keyboard::getInstance(QWidget *parent)
{if (m_instance == nullptr) {m_instance = new Keyboard(parent);}return m_instance;
}void Keyboard::destroyInstance()
{if (m_instance != nullptr){delete m_instance;m_instance = nullptr;}
}void initWidget(){....m_pChineseWidget = new CandidateWidget;m_pChineseWidget->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);ui->horizontalLayout_Chinese->addWidget(m_pChineseWidget);//嵌入候选词显示列表...connect(GlobalSignalManager::getInstance(),&GlobalSignalManager::focusChanged,this,&Keyboard::onFocusReceived,Qt::UniqueConnection);//绑定全局的焦点变化信号和焦点处理函数}void Keyboard::onFocusReceived(QWidget* newWidget,int type) {//我的主应用中有5中类型的交互控件,需要分别处理if (newWidget && m_currentTarget!=newWidget ) {if(m_currentTarget){disconnect(this,nullptr,m_currentTarget,nullptr);}// 保存当前聚焦控件LogDebug << "receive new WIDGET"<<newWidget->objectName();m_currentTarget = newWidget;switch (type) {case 0:{connect(this,&Keyboard::operateText,dynamic_cast<CustomLineEdit*>(m_currentTarget),&CustomLineEdit::operateTextSlot);break;}case 1:{connect(this,&Keyboard::operateText,dynamic_cast<CustomPlainTextEdit*>(m_currentTarget),&CustomPlainTextEdit::operateTextSlot);break;}case 2:{connect(this,&Keyboard::operateText,dynamic_cast<QLineEdit*>(m_currentTarget),[=](QString opt,QString text){
//                static QElapsedTimer timer;
//                if (timer.elapsed() < 100) {
//                    return;  // 节流机制:忽略100毫秒内的多次输入
//                }
//                timer.restart();if(opt == "insert"){dynamic_cast<QLineEdit*>(m_currentTarget)->insert(text);}else if(opt == "backspace"){if(!dynamic_cast<QLineEdit*>(m_currentTarget)->text().isEmpty()){dynamic_cast<QLineEdit*>(m_currentTarget)->backspace();}}});break;}case 3:{connect(this,&Keyboard::operateText,dynamic_cast<QPlainTextEdit*>(m_currentTarget),[=](QString opt,QString text){
//                static QElapsedTimer timer;
//                if (timer.elapsed() < 100) {
//                    return;  // 节流机制:忽略100毫秒内的多次输入
//                }
//                timer.restart();if(opt == "insert"){dynamic_cast<QPlainTextEdit*>(m_currentTarget)->insertPlainText(text);}else if(opt == "backspace"){if(!dynamic_cast<QPlainTextEdit*>(m_currentTarget)->toPlainText().isEmpty()){dynamic_cast<QPlainTextEdit*>(m_currentTarget)->undo();}} else if(opt == "enter"){dynamic_cast<QPlainTextEdit*>(m_currentTarget)->insertPlainText("\n");}});break;}case 4:{connect(this,&Keyboard::operateText,dynamic_cast<QTextEdit*>(m_currentTarget),[=](QString opt,QString text){
//                static QElapsedTimer timer;
//                if (timer.elapsed() < 100) {
//                    return;  // 节流机制:忽略100毫秒内的多次输入
//                }
//                timer.restart();if(opt == "insert"){dynamic_cast<QTextEdit*>(m_currentTarget)->insertPlainText(text);} else if(opt == "backspace"){if(!dynamic_cast<QTextEdit*>(m_currentTarget)->toPlainText().isEmpty()){dynamic_cast<QTextEdit*>(m_currentTarget)->undo();}} else if(opt == "enter"){dynamic_cast<QTextEdit*>(m_currentTarget)->insertPlainText("\n");}});break;}default:break;}}//MianWindow.cpp
//主程序的事件过滤
bool MainWindow::eventFilter(QObject *obj, QEvent *event)
{if (event->type() == QEvent::FocusIn) {if (dynamic_cast<CustomLineEdit*>(obj)) {LogDebug << obj->objectName() << " gained focus.";// 这里可以处理焦点事件emit GlobalSignalManager::getInstance()->focusChanged(qobject_cast<QWidget*>(obj),0);}else if(dynamic_cast<CustomPlainTextEdit*>(obj)){LogDebug << obj->objectName() << " gained focus.";emit GlobalSignalManager::getInstance()->focusChanged(qobject_cast<QWidget*>(obj),1);}else if(dynamic_cast<QLineEdit*>(obj)){LogDebug << obj->objectName() << " gained focus.";emit GlobalSignalManager::getInstance()->focusChanged(qobject_cast<QWidget*>(obj),2);}else if(dynamic_cast<QPlainTextEdit*>(obj)){LogDebug << obj->objectName() << " gained focus.";emit GlobalSignalManager::getInstance()->focusChanged(qobject_cast<QWidget*>(obj),3);}else if(dynamic_cast<QTextEdit*>(obj)){LogDebug << obj->objectName() << " gained focus.";emit GlobalSignalManager::getInstance()->focusChanged(qobject_cast<QWidget*>(obj),4);}} else if (event->type() == QEvent::FocusOut) {if (dynamic_cast<CustomLineEdit*>(obj)) {
//            LogDebug << obj->objectName() << "lost focus.";}}return QWidget::eventFilter(obj, event); // 确保事件传递给基类
}
}

🌟PinYinIMe接口描述

  1. 启动解码引擎
bool im_open_decoder(const char *fn_sys_dict, const char *fn_usr_dict);
  • fn_sys_dict: 系统字典,可以直接使用谷歌自带的字典dict_pinyin.dat
  • fn_usr_dict: 用户字典,用户自己定义的字典
    成功启动引擎时接口返回true。
bool im_open_decoder_fd(int sys_fd, long start_offset, long length,const char *fn_usr_dict);

上一个函数的变形。

  • sys_fd: 系统字典的文件描述符
  • start_offset: 系统字典文件描述符的偏移位置
  • length: 系统字典文件读取的长度
  • fn_usr_dict: 用户字典
  1. 关闭解码引擎
void im_close_decoder();
  1. 设置输入输出上限
void im_set_max_lens(size_t max_sps_len, size_t max_hzs_len);

如果本函数未被调用,则使用默认参数。举例说明该函数的作用,对于显示屏幕大小受限制,
显示部件可以只显示确定数量的输入字母来解码, 以及确定数量的中文来显示。
如果用户添加一个新字母之母, 输入的所有字母或输出的中文数量超过了设置的上限, 则引擎会不理踩
新添加的字母。

  • max_sps_len: 输入拼音字母的最大长度
  • max_hzs_len: 解码中文字符的最大长度
  1. 清除缓冲
void im_flush_cache();

因为引擎在运行时,为是达到最好的性能,一些数据有保存至内存中,所以有必要时需要清除掉。

  1. 搜索
size_t im_search(const char* sps_buf, size_t sps_len);

本函数用于搜索匹配字母的候选中文。当要搜索的字母的前缀与先前的搜索字母一样,引擎默认会在先前的
搜索结果中进行搜索。如果用户需要开启新的搜索,可以先调用im_reset_search()接口。

  • sps_buf: 拼音字母
  • sps_len: 字母长度
  • 返回候选数
  1. 删除
size_t im_delsearch(size_t pos, bool is_pos_in_splid, bool clear_fixed_this_step);

对当前查找结果执行删除操作, 然后再重新查找。

  • pos: 拼音字母的位置或者是搜索结果ID
  • is_pos_in_splid: 申明pos参数是字母位置还是搜索结果ID
  • clear_fixed_this_step:
  • 返回候选数量
  1. 初始化查找结果
void im_reset_search();
  1. 获取输入的拼音字母
const char *im_get_sps_str(size_t *decoded_len);
  • decoded_len: 保存返回拼音字母的长度arsed.
  • 返回拼音字母
  1. 获取候选字符串
char16* im_get_candidate(size_t cand_id, char16* cand_str, size_t max_len);
  • cand_id: 获取候选字符串的ID号,从0开始,通常ID为0是匹配度最高的
  • cand_str: 用于保存选择字符串的缓冲区
  • max_len: 缓冲区最大长度
  • 成功返回缓冲区地址cand_str,失败返回NULL.

🌟项目展示
在这里插入图片描述
在这里插入图片描述
如果有兴趣,下载源码,并尝试优化 :)
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/60916.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

react中如何在一张图片上加一个灰色蒙层,并添加事件?

最终效果&#xff1a; 实现原理&#xff1a; 移动到图片上的时候&#xff0c;给img加一个伪类 &#xff01;&#xff01;此时就要地方要注意了&#xff0c;因为img标签是闭合的标签&#xff0c;无法直接添加 伪类&#xff08;::after&#xff09;&#xff0c;所以 我是在img外…

电子应用产品设计方案-11:全自动智能全屋智能系统设计方案

一、设计目标 打造便捷、舒适、安全且节能的全屋智能环境。 二、系统组成 1. 智能灯光系统 - 在客厅、卧室、厨房、卫生间等各处安装智能灯具&#xff0c;可通过手机 APP、语音控制实现开关、调光调色。如客厅设置多种场景模式&#xff0c;如“观影模式”&#xff08;灯光…

服务端高并发分布式结构进阶之路

序言 在技术求知的旅途中&#xff0c;鉴于多数读者缺乏在中大型系统实践中的亲身体验&#xff0c;难以从宏观角度把握某些概念&#xff0c;因此&#xff0c;本文特选取“电子商务应用”作为实例&#xff0c;详细阐述从百级至千万级并发场景下服务端架构的逐步演变历程。同时&am…

WebRTC视频 02 - 视频采集类 VideoCaptureModule

WebRTC视频 01 - 视频采集整体架构 WebRTC视频 02 - 视频采集类 VideoCaptureModule&#xff08;本文&#xff09; WebRTC视频 03 - 视频采集类 VideoCaptureDS 上篇 WebRTC视频 04 - 视频采集类 VideoCaptureDS 中篇 WebRTC视频 05 - 视频采集类 VideoCaptureDS 下篇 一、前言…

POI实现根据PPTX模板渲染PPT

目录 1、前言 2、了解pptx文件结构 3、POI组件 3.1、引入依赖 3.2、常见的类 3.3、实现原理 3.4、关键代码片段 3.4.1、获取ppt实例 3.4.2、获取每页幻灯片 3.4.3、循环遍历幻灯片处理 3.4.3.1、文本 3.4.3.2、饼图 3.4.3.3、柱状图 3.4.3.4、表格 3.4.3.5、本地…

sqli-labs靶场17-20关(每日四关)持续更新!!!

Less-17 打开靶场&#xff0c;发现页面比之前多了一行字 翻译过来就是&#xff0c;密码重置&#xff0c;大家肯定会想到&#xff0c;自己平时在日常生活中怎么密码重置&#xff0c;肯定是输入自己的用户名&#xff0c;输入旧密码&#xff0c;输入新密码就可以了&#xff0c;但…

node.js下载安装步骤整理

>> 进入node.js下载页面下载 | Node.js 中文网 >>点击 全部安装包 >>删除网址node后面部分&#xff0c;只保留如图所示部分&#xff0c;回车 >>点击进入v11.0.0/版本 >>点击下载node-v11.0.0-win-x64.zip(电脑是windows 64位操作系统适用) >…

ThinkServer SR658H V2服务器BMC做raid与装系统

目录 前提准备 一. 给磁盘做raid 二. 安装系统 前提准备 磁盘和系统BMC地址都已经准备好&#xff0c;可正常使用。 例&#xff1a; 设备BMC地址&#xff1a;10.99.240.196 一. 给磁盘做raid 要求&#xff1a; 1. 将两个894G的磁盘做成raid1 2. 将两块14902G的磁盘各自做…

SpringBoot配置类

在Spring Boot中&#xff0c;配置类是一种特殊的类&#xff0c;用于定义和配置Spring应用程序的各种组件、服务和属性。这些配置类通常使用Java注解来声明&#xff0c;并且可以通过Spring的依赖注入机制来管理和使用。 Spring 容器初始化时会加载被Component、Service、Reposi…

SpringBoot教程(二十五) | SpringBoot配置多个数据源

SpringBoot教程&#xff08;二十五&#xff09; | SpringBoot配置多个数据源 前言方式一&#xff1a;使用dynamic-datasource-spring-boot-starter引入maven依赖配置数据源动态切换数据源实战 方式二&#xff1a;使用AbstractRoutingDataSource1. 创建数据源枚举类2. 创建数据源…

ZooKeeper单机、集群模式搭建教程

单点配置 ZooKeeper在启动的时候&#xff0c;默认会读取/conf/zoo.cfg配置文件&#xff0c;该文件缺失会报错。因此&#xff0c;我们需要在将容器/conf/挂载出来&#xff0c;在制定的目录下&#xff0c;添加zoo.cfg文件。 zoo.cfg logback.xml 配置文件的信息可以从二进制包…

【大数据学习 | HBASE高级】hbase-phoenix 与二次索引应用

1. hbase-phoenix的应用 1.1 概述&#xff1a; 上面我们学会了hbase的操作和原理&#xff0c;以及外部集成的mr的计算方式&#xff0c;但是我们在使用hbase的时候&#xff0c;有的时候我们要直接操作hbase做部分数据的查询和插入&#xff0c;这种原生的方式操作在工作过程中还…

拆解测试显示Mac Mini (2024)固态硬盘并未锁定 互换硬盘后仍可使用

此前已经有维修达人尝试将 Mac Mini (2024) 固态硬盘上的 NAND 闪存拆下并替换实现扩容&#xff0c;例如可以从 256GB 扩容到 2TB。虽然接口类似于 NVMe M.2 SSD 但直接安装普通硬盘是无效的&#xff0c;苹果仍然通过某种机制检测硬盘是否能够兼容。 不过知名拆解网站 iFixit 的…

主界面获取个人信息客户端方

主界面获取个人信息客户端方 前言 上一集我们完成了websocket身份验证的内容&#xff0c;那么这一集开始我们将要配合MockServer来完成主界面获取个人信息的内容。 需求分析 我们这边是完成客户端那方的内容&#xff0c;当客户端登录成功之后&#xff0c;我们就要从服务器获…

Spring整合Redis

前言 在Spring项目中整合Redis&#xff0c;能显著提升数据缓存、分布式锁、会话管理等操作的效率。Jedis作为轻量级的Java Redis客户端&#xff0c;搭配Spring Data Redis模块&#xff0c;能够简化Redis的连接和数据操作&#xff0c;实现更高性能的读写与灵活的缓存管理。本文…

爬虫——Requests库的使用

在爬虫开发中&#xff0c;HTTP请求是与服务器进行交互的关键操作。通过发送HTTP请求&#xff0c;爬虫可以获取目标网页或接口的数据&#xff0c;而有效地处理请求和响应是爬虫能够高效且稳定运行的基础。Requests库作为Python中最常用的HTTP请求库&#xff0c;因其简洁、易用和…

LinkedHashMap实现LRU

LRU 环境&#xff1a;JDK11 最近接触LRU(Least Recently Used)&#xff0c;即最近最少使用&#xff0c;也称淘汰算法&#xff0c;在JDK中LinkedHashMap有相关实现 LRU的LinkedHashMap实现 LinkedHashMap继承HashMap。所以内存的存储结构和HashMap一样&#xff0c;但是LinkedH…

IDEA部署AI代写插件

前言 Hello大家好&#xff0c;当下是AI盛行的时代&#xff0c;好多好多东西在AI大模型的趋势下都变得非常的简单。 比如之前想画一幅风景画得先去采风&#xff0c;然后写实什么的&#xff0c;现在你只需描述出你想要的效果AI就能够根据你的描述在几分钟之内画出一幅你想要的风景…

27-压力测试

测试目标 & 测试数据 ● 测试目标 ○ 测试集群的读写性能 / 做集群容量规划 ○ 对 ES 配置参数进行修改&#xff0c;评估优化效果 ○ 修改 Mapping 和 Setting&#xff0c;对数据建模进行优化&#xff0c;并测试评估性能改进 ○ 测试 ES 新版本&#xff0c;结合实际场…

4. Spring Cloud Ribbon 实现“负载均衡”的详细配置说明

4. Spring Cloud Ribbon 实现“负载均衡”的详细配置说明 文章目录 4. Spring Cloud Ribbon 实现“负载均衡”的详细配置说明前言1. Ribbon 介绍1.1 LB(Load Balance 负载均衡) 2. Ribbon 原理2.2 Ribbon 机制 3. Spring Cloud Ribbon 实现负载均衡算法-应用实例4. 总结&#x…