【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.rhkb.cn/news/472286.html

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

相关文章

领夹麦克风哪个品牌好,手机领夹麦克风哪个牌子好,选购推荐

​无线麦克风凭借其无与伦比的便携性与灵活性&#xff0c;成为在演讲、表演以及会议等多种场合中不可或缺的有力帮手。它挣脱了线缆的束缚&#xff0c;使得声音的传播更加自由自在。其操作十分简便&#xff0c;只需简单配对就能投入使用&#xff0c;从而可以轻松地适应各类场景…

ADC输出码和输入电压转换关系

ADC输出码和输入电压转换关系 转换公式&#xff1a;ADC输出码(Vin / Vref) *2n 。其中Vin 是输入ADC芯片的电压&#xff0c;Vref是参考电压&#xff0c;n是ADC芯片的位数。 举个例子MS5182是一个16bit的ADC&#xff08;21665536&#xff09;&#xff0c;参考电压Vref4.096V&a…

IROS讲座:如何写出受欢迎的论文

讲座原名称&#xff1a;How to write papers people love reading 时间地点&#xff1a;2024年10月中旬&#xff0c;阿布扎比国家展览中心&#xff0c;阿联酋 演讲嘉宾照片&#xff1a; 以下是拍摄的部分PPT&#xff0c;并添加了中文笔记&#xff1a;

【Docker】Mac安装Docker Desktop导致磁盘剩余空间较少问题如何解决?

目录 一、背景描述 二、解决办法 三、清理效果 四、理论参考 解决方法 1. 清理未使用的 Docker 镜像、容器和卷 2. 查看 Docker 使用的磁盘空间 3. 调整 Docker 的存储位置 4. 增加磁盘空间 5. 调整 Docker Desktop 配置 6. 使用 Docker 清理工具&#xff08;例如 D…

Tiktok对接和内容发布申请流程

这段时间在搞AI生成视频&#xff0c;希望用户能一键发布到Tiktok&#xff0c;因此研究了一下Tiktok的开发者申请流程&#xff0c;发现好复杂&#xff0c;同时也发现Tiktok的开发也跟我一样&#xff0c;挺草台班子的 0、流程简述 废话不多说&#xff0c;Tiktok的开发者申请和…

Android ART知多少?

Android 虚拟机 ART&#xff08;Android Runtime&#xff09;是 Android 平台上的应用程序运行时环境&#xff0c;用于执行应用程序的字节码。ART 自 Android 5.0&#xff08;Lollipop&#xff09;开始取代了 Dalvik&#xff0c;成为 Android 的默认运行时环境。本文将从以下几…

黑马嵌入式开发入门模电基础学习笔记

学习视频: 黑马程序员嵌入式开发入门模电&#xff08;模拟电路&#xff09;基础 文章目录 背景介绍电流电压组件仿真三极管ne555PCBEDA案例&#xff1a;非接触式电笔案例&#xff1a;电子琴 背景介绍 电流 电压 组件 仿真 三极管 mos管 ne555 PCB EDA 案例&#xff1a;非接触…

Ubuntu 18 EDK2 环境编译

视频&#xff1a;在全新的Ubuntu上从零搭建UEFI的EDK2开发环境 开始&#xff1a;git clone https://github.com/tianocore/edk2.git 开始编译BaseTools前先更新一下子模块&#xff1a;git submodule update --init &#xff0c;然后&#xff1a;make -C BaseTools/ 问题1&a…

第9章 DIV+CSS布局

9.1 DIVCSS概述 DIVCSS是Web设计标准&#xff0c;它是一种网页的布局方法。与传统中通过表格&#xff08;table)布局定位的方式不同&#xff0c;它可以实现网页页面内容与表现相分离。DIV组成了网页的格局&#xff0c;CSS则装饰了格局&#xff0c;比如建一栋房子&#xff0c;…

QT QLineEdit失去焦点事件问题与解决

本文介绍如何获得QLineEdit的失去焦点事件和获得焦点的输入框也会触发失去焦点事件的问题&#xff01; 目录 一、QLineEdit获得失去焦点事件 1.自定义类继承自QLineEdit 2.重写 focusOutEvent 3.使用 二、失去焦点事件问题 1.问题描述 2.问题解决 三、源码分享 lineed…

微信小程序navigateTo:fail webview count limit exceed

theme: nico 你们好&#xff0c;我是金金金。 场景 uniapp编写微信小程序&#xff0c;使用uni.navigateTo跳转的过程中报错如下&#xff1a; 报错意思也非常明显了&#xff1a;errMsg":"navigateTo:fail webview 数量超出限制 排查 排查之前我先贴一下代码 代码非…

HarmonyOS ArkTs 解决流式传输编码问题

工作日志 日期&#xff1a;2024-11-15 标题&#xff1a;HarmonyOS ArkTs 解决流式传输编码问题 问题描述 问题&#xff1a;在处理流式数据的 HTTP 请求时&#xff0c;服务器返回的数据存在编码问题&#xff0c;导致数据无法正确地解码为字符串。部分数据在解码后出现了乱码…

Android Settings 单元测试 | 如何运行单元测试?

背景 在Android Settings 单元测试 | Telephony Network 模块 APN 案例中粗略介绍了单元测试逻辑内容&#xff0c;但是在独立APK里面如何将单元测试跑起来还是有疑问&#xff0c;因为APP不能直接install&#xff0c;无法借助Android Studio直接Run&#xff0c;在安装的一步会报…

flutter字体大小切换案例 小字体,标准字体,大字体,超大字体案例

flutter字体大小切换案例 小字体&#xff0c;标准字体&#xff0c;大字体&#xff0c;超大字体案例 Android iOS设备带有选择记录 我的flutter项目版本 environment: sdk: ‘>3.4.4 <4.0.0’ 图片案例 pubspec.yaml 添加依赖 # 屏幕尺寸适配 https://github.com/OpenF…

编译原理(手绘)

大家好&#xff0c;今天给大家分享一下我自己对c语言编译链接的一点见解&#xff08;本人是学生&#xff0c;有记笔记的习惯&#xff09;&#xff0c;那么今天就给大家分享我的笔记。 以上只是我本人的一些见解&#xff0c;并非绝对&#xff0c;欢迎大家一起交流。 那么今天分…

跳房子(弱化版)

题目描述 跳房子&#xff0c;也叫跳飞机&#xff0c;是一种世界性的儿童游戏&#xff0c;也是中国民间传统的体育游戏之一。 跳房子的游戏规则如下&#xff1a; 在地面上确定一个起点&#xff0c;然后在起点右侧画 n 个格子&#xff0c;这些格子都在同一条直线上。每个格子内…

初识Linux · 共享内存

目录 理解共享内存 Shared memmory code 理解共享内存 前文介绍的管道方式的通信&#xff0c;本文介绍的是进程通信的另外一种方式&#xff0c;即共享内存。但是这种通信方式的特点是只能本地通信&#xff0c;并且不像管道那样有保护机制&#xff0c;这里是没有的。 我们通…

机器学习day5-随机森林和线性代数1

十 集成学习方法之随机森林 集成学习的基本思想就是将多个分类器组合&#xff0c;从而实现一个预测效果更好的集成分类器。大致可以分为&#xff1a;Bagging&#xff0c;Boosting 和 Stacking 三大类型。 &#xff08;1&#xff09;每次有放回地从训练集中取出 n 个训练样本&…

Essential Cell Biology--Fifth Edition--Chapter one (6)

1.1.4.4 Internal Membranes Create Intracellular Compartments with Different Functions [细胞膜形成具有不同功能的细胞内隔室] 细胞核、线粒体和叶绿体并不是真核细胞中唯一的膜包围细胞器。细胞质中含有大量的[ a profusion of]其他细胞器&#xff0c;这些细胞器被单层膜…

基于VUE实现语音通话:边录边转发送语言消息、 播放pcm 音频

文章目录 引言I 音频协议音频格式:音频协议:II 实现协议创建ws对象初始化边录边转发送语言消息 setupPCM按下通话按钮时开始讲话,松开后停止讲话播放pcm 音频III 第三库recorderplayer调试引言 需求:电台通讯网(电台远程遥控软件-超短波)该系统通过网络、超短波终端等无线…