【Qt实现虚拟键盘】

news2024/12/31 6:34:02

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 gui

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

CONFIG += 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.0

DESTDIR += $$PWD/bin

INCLUDEPATH += $$PWD/include

SOURCES += \
    main.cpp \
    keyboard.cpp

HEADERS += \
    keyboard.h

FORMS += \
    keyboard.ui

# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target



win32:CONFIG(release, debug|release): LIBS += -L$$PWD/lib/ -lpinyinime
else:win32:CONFIG(debug, debug|release): LIBS += -L$$PWD/lib/ -lpinyinimed

INCLUDEPATH += $$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;
	//存放候选词ListWidget
	CandidateWidget *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.coloradmin.cn/o/2240902.html

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

相关文章

APT 参与者将恶意软件嵌入 macOS Flutter 应用程序中

发现了一些恶意软件样本&#xff0c;这些样本据信与朝鲜民主主义人民共和国 (DPRK)&#xff08;又称北朝鲜&#xff09;有关&#xff0c;这些样本使用 Flutter 构建&#xff0c;Flutter 的设计可以对恶意代码进行混淆。JTL 深入研究了恶意代码的工作原理&#xff0c;以帮助保护…

双十一抢券风波:大学生300元提6000元电动车遭拒,谁该负责?

双十一购物狂欢节&#xff0c;本应是消费者享受优惠、商家提升销量的双赢时刻&#xff0c;但在河南郑州&#xff0c;发生了一起哭笑不得的抢券风波。一名大学生在双十一期间&#xff0c;通过某平台抢到了原价6099元电动车的直降优惠&#xff0c;只需支付300元就能将车骑回家。然…

三周精通FastAPI:37 包含 WSGI - Flask,Django,Pyramid 以及其它

官方文档&#xff1a;https://fastapi.tiangolo.com/zh/advanced/wsgi/ 包含 WSGI - Flask&#xff0c;Django&#xff0c;其它 您可以挂载多个 WSGI 应用&#xff0c;正如您在 Sub Applications - Mounts, Behind a Proxy 中所看到的那样。 为此, 您可以使用 WSGIMiddlewar…

【汇编语言】包含多个段的程序(二)—— 将数据、代码、栈放入不同的段

文章目录 前言1. 存在的两个问题2. 解决办法3. 示例代码3.1 程序说明3.1.1 定义多个段的方法3.1.2 对段地址的引用3.1.3 各种段完全是我们的安排 4. 总结结语 前言 &#x1f4cc; 汇编语言是很多相关课程&#xff08;如数据结构、操作系统、微机原理&#xff09;的重要基础。但…

初识Linux · 共享内存

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

【竞技宝】CS2-上海majorRMR:美洲区最后门票争夺战

北京时间2024年11月15日&#xff0c;上海major美洲区RMR正在如火如荼的进行之中。昨日一共进行了三场2-1组的比赛以及三场1-2组的比赛&#xff0c;决出三个正赛参赛名额的同时也确定了今日2-2组的参赛队伍&#xff0c;那么昨日的比赛战果如何呢&#xff1f;接下来小宝就为大家带…

实战:深入探讨 MySQL 和 SQL Server 全文索引的使用及其弊端

在数据库中处理大量文本数据时,包含搜索(例如查找包含特定单词的文本)往往是必需的。然而,直接使用 LIKE %text% 的方式在大数据量中进行模糊查询会造成性能瓶颈。为了解决这一问题,MySQL 和 SQL Server 提供了全文索引(Full-Text Indexing)功能,可以显著加速文本数据的…

蓝桥杯——数组

1、移动数组元素 package day3;import java.util.Arrays;public class Demo1 {public static void main(String[] args) {int[] arr {1,2,3,4,5,6};int k 2;int[] arr_new f(arr,k);for (int i : arr_new) {System.out.print(i",");}//或System.out.println();St…

人体存在感应器设置时间开启感应人存在开灯,失效

环境&#xff1a; 领普人体存在感应器 问题描述&#xff1a; 人体存在感应器设置时间开启感应人存在开灯,失效&#xff0c;设置下午5点&#xff0c;如果有人在5点前一直在这个区域&#xff0c;这个时候到了5点&#xff0c;就触发不了感应自动打开灯光。 解决方案&#xff1a…

常用命令之LinuxOracleHivePython

1. 用户改密 passwd app_adm chage -l app_adm passwd -x 90 app_adm -> 执行操作后&#xff0c;app_adm用户的密码时间改为90天有效期--查看该euser用户过期信息使用chage命令 --chage的参数包括 ---m 密码可更改的最小天数。为零时代表任何时候都可以更改密码。 ---M 密码…

基于yolov8、yolov5的车型检测识别系统(含UI界面、训练好的模型、Python代码、数据集)

摘要&#xff1a;车型识别在交通管理、智能监控和车辆管理中起着至关重要的作用&#xff0c;不仅能帮助相关部门快速识别车辆类型&#xff0c;还为自动化交通监控提供了可靠的数据支撑。本文介绍了一款基于YOLOv8、YOLOv5等深度学习框架的车型识别模型&#xff0c;该模型使用了…

解决因为TortoiseSVN未安装cmmand line client tools组件,导致idea无法使用svn更新、提交代码

一.错误信息 1.更新代码时&#xff1a;SVN: 更新错误 找不到要更新的版本管理目录。 2.提交代码&#xff1a;检测不到任何更新&#xff08;实际上有代码修改&#xff09;。 3.Cannot run program "svn"。 二.原因分析 在电脑上新安装的的客户端TortoiseSVN、ide…

高效稳定!新加坡服务器托管方案助力企业全球化布局

在全球化的商业环境中&#xff0c;企业对于高效、稳定的服务器托管方案的需求日益迫切。作为亚洲的服务器托管中心&#xff0c;新加坡凭借其独特的地理位置、稳定的政治环境、先进的科技设施以及开放的市场政策&#xff0c;为企业提供了理想的服务器托管解决方案&#xff0c;助…

NVR管理平台EasyNVR多品牌NVR管理工具/设备:为什么IPC白天图像正常,夜视漆黑?

在安防监控系统中&#xff0c;IPC&#xff08;网络摄像机&#xff09;扮演着至关重要的角色。然而&#xff0c;有时用户可能会遇到这样的问题&#xff1a;IPC在白天时图像清晰正常&#xff0c;但到了夜晚却变得漆黑一片&#xff0c;无法看清监控画面。 为什么IPC白天图像正常&a…

安卓aab包的安装教程,附带adb环境的配置

一、ADB环境配置 安装aab包的前提是需要有adb环境&#xff0c;下面先介绍adb环境的配置 ADB通常位于/platform-tools/。 在Windows上&#xff0c;你可以通过以下步骤添加到环境变量&#xff1a; 右键点击“我的电脑”或“此电脑”&#xff0c;选择“属性”。 点击“高级系…

研究生如何远控实验室电脑?远程办公功能使用教程

如果你是研究生&#xff0c;是不是会遇到需要远程控制实验室电脑进行查看文献、调代码和拉数据的时候&#xff1f;有时候就是这么棘手&#xff0c;不过你可以借助一些工具来帮助你随时随地远控实验室电脑。这样就不用担心导师催促&#xff0c;无法及时完成科研了。常见的工具比…

计算机视觉和机器人技术中的下一个标记预测与视频扩散相结合

一种新方法可以训练神经网络对损坏的数据进行分类&#xff0c;同时预测下一步操作。 它可以为机器人制定灵活的计划&#xff0c;生成高质量的视频&#xff0c;并帮助人工智能代理导航数字环境。 Diffusion Forcing 方法可以对嘈杂的数据进行分类&#xff0c;并可靠地预测任务的…

云计算研究实训室建设方案

一、引言 随着云计算技术的迅速发展和广泛应用&#xff0c;职业院校面临着培养云计算领域专业人才的迫切需求。本方案旨在构建一个先进的云计算研究实训室&#xff0c;为学生提供一个集理论学习、实践操作、技术研发与创新于一体的综合性学习平台&#xff0c;以促进云计算技术…

React Native 全栈开发实战班 - 核心组件与导航

在 React Native 中&#xff0c;组件是构建用户界面的基本单元。React Native 提供了丰富的内置组件&#xff0c;涵盖了从基础布局到复杂交互的各种需求。本章节将详细介绍常用的内置组件&#xff0c;并重点讲解列表与滚动视图的使用。 1. 常用内置组件详解 React Native 提供…

【2025最新计算机毕业设计】基于SpringBoot+Vue电脑在线装机指南教程网站【源码+文档】

作者简介&#xff1a;✌CSDN新星计划导师、Java领域优质创作者、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和学生毕业项目实战,高校老师/讲师/同行前辈交流。✌ 主要内容&#xff1a;&#x1f31f;Java项目、Python项目、前端项目、PHP、ASP.NET、人工智能…