实现QT中的virtualkeyboard(适用于PySide2、Pyqt5)

文章目录

      • 前言:
    • 一、虚拟键盘的实现
      • 综合代码:
    • 二、为什么选用QWidget而不适用QDialog实现键盘
    • 三、从窗体a拉起窗体b后,窗体b闪退问题的探讨

前言:

本文章主要包含三部分:1. 虚拟键盘的实现(基于Pyside2) 2. 为什么选用QWidget而不适用QDialog实现键盘 3. 关于从一个窗体a中拉起另一个窗体b后,窗体b闪退的问题探讨
由于qt5官方的virtualkeyboard库无法适用于公司的应用场景,于是需要手写一个virtual keyboard,目前是初步实现了,但是没有办法全局监听,希望后续能有实现全局监听的办法。
已实现功能:

  1. 作为组件以keyboard.py 形式集成到项目中
  2. 可切换大小写
  3. 可按比例附着于屏幕下方
  4. 项目内多组件共用(Keyboard需要作为单例类)
  5. 符合真实键盘键位,支持部分特殊字符
    待实现功能:
  • 全局监听

一、虚拟键盘的实现

从虚拟键盘的交互可以感知其应当作为一个窗体与用户进行交互,通常窗体用的较多的是QDialog和QWidget,由于QDialog会阻塞其他窗体的交互事件(似乎可以通过setModal设置模态或非模态,但是笔者尝试后不起作用),故这里选择QWidget。
—————
实现思路:

  1. QWidget作为键盘窗体
  2. QPushButton作为虚拟键盘的每个按键
  3. 模拟真实键盘键位,为每一行提供一个QHBoxLayout,设置一个父layout即QVBoxLayout将所有QHBoxLayout添加进去。
  4. 通过setGeometry设置窗体位于指定位置,使键盘每次出现即附着于屏幕下方
  5. 设置该类为单例类
  6. 通过组件点击事件激活虚拟键盘
  7. keyboard工具类
  8. 单例类装饰器

综合代码:

代码结构:

  • keyboard.py 键盘实现
  • keyboard_util.py 键盘工具类
  • singleton_util.py 单例类装饰器类
  • enjoy_edit.py QLineEdit示例组件
  • main.py 主程序入口

注:以下代码虽使用PySide2,但修改PySide2为PyQt5也可运行

# keyboard.py
from functools import partial
from PySide2 import QtCore, QtWidgets
from PySide2.QtCore import QRect, Signal, QPoint
from PySide2.QtGui import QFont, Qt
from PySide2.QtWidgets import QPushButton, QVBoxLayout, QHBoxLayout, QWidget, QDialog, QSizePolicy

from util.keyboard_util import KeyBoardUtil
from util.singleton_util import singleton


@singleton
class KeyBoard(QWidget):
    close_signal = Signal(str)
    press_signal = Signal(str)

    def __init__(self, edit):
        super().__init__()
        self.layout = None
        self.flag = False
        self.keyboard_util = KeyBoardUtil()
        self.chars = {}
        self.keys = []
        self.edit = edit  # 获取当前鼠标点击的控件
        self.setWindowTitle('EnjoyKeyBoard')
        self.update_keys()
        self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.WindowCloseButtonHint)


    def on_key_clicked(self, s):
        if s == "close":
            self.hide()
        elif s == "shift":
            self.update_keys()
        else:
            self.press_signal.emit(s)

    def update_keys(self):
        self.layout = QVBoxLayout()
        self.normal_dict = self.keyboard_util.get_lower_to_upper_dict()
        self.shift_dict = self.keyboard_util.get_upper_to_lower_dict()

        # print(self.chars)
        if len(self.keys) == 0:
            # 将键盘按钮添加到布局中
            row = QHBoxLayout()
            for key,value in self.normal_dict.items():
                key_btn = QPushButton(key)
                key_btn.setFont(QFont("Arial",20))
                # key_btn.setFixedSize(70,50)
                key_btn.setSizePolicy(QSizePolicy.Expanding,QSizePolicy.Expanding)
                self.keys.append(key_btn)
                row.addWidget(key_btn)
                sz = len(self.keys)
                if sz == 14 or sz == 27 or sz == 39 or sz == 51:
                    self.layout.addLayout(row)
                    row = QHBoxLayout()
        else:
            if not self.flag:
                self.flag = True
                for i in range(len(self.keys)):
                    self.keys[i].setText(self.normal_dict[self.keys[i].text()])
            else:
                self.flag = False
                for i in range(len(self.keys)):
                    self.keys[i].setText(self.shift_dict[self.keys[i].text()])

        # 设置按键事件
        for key in self.keys:
            key.clicked.connect(partial(self.on_key_clicked, key.text()))
            key.clicked.disconnect()
            key.clicked.connect(partial(self.on_key_clicked, key.text()))

        self.setLayout(self.layout)

# keyboard_util.py 
from util.singleton_util import singleton

@singleton
class KeyBoardUtil(object):
    def __init__(self):
        super().__init__()
        self.all_char = [[]]
        self.lower_to_upper = {}
        self.upper_to_lower = {}

    def get_normal_all(self):
        line1 = [
            '`', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', '<-'
        ]
        line2 = [
            'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\\'
        ]
        line3 = [
            'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', '\'', 'close'
        ]
        line4 = [
            'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/', 'shift', 'clear'
        ]
        self.all_char = [line1, line2, line3, line4]
        return self.all_char

    def get_shift_all(self):
        line1 = [
            '~', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '+', '<-'
        ]
        line2 = [
            'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '{', '}', '|'
        ]
        line3 = [
            'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':', '\"', 'close'
        ]
        line4 = [
            'Z', 'X', 'C', 'V', 'B', 'N', 'M', '<', '>', '?', 'shift', 'clear'
        ]
        self.all_char = [line1, line2, line3, line4]
        return self.all_char

    def get_lower_to_upper_dict(self):
        normal_chars = self.get_normal_all()
        shift_chars = self.get_shift_all()
        for i in range(len(normal_chars)):
            for j in range(len(normal_chars[i])):
                self.lower_to_upper[normal_chars[i][j]] = shift_chars[i][j]
        return self.lower_to_upper

    def get_upper_to_lower_dict(self):
        normal_chars = self.get_normal_all()
        shift_chars = self.get_shift_all()
        for i in range(len(normal_chars)):
            for j in range(len(normal_chars[i])):
                self.upper_to_lower[shift_chars[i][j]] = normal_chars[i][j]
        return self.upper_to_lower

# singleton_util.py
def singleton(cls):
    instances = {}

    def _singleton(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return _singleton
# enjoy_edit.py
from PySide2 import QtGui, QtCore, QtWidgets
from PySide2.QtWidgets import QLineEdit, QWidget, QVBoxLayout, QSizePolicy

from view.base.enjoy_keyboard import EnjoyKeyBoard


class EnjoyEdit(QLineEdit):

    def __init__(self, parent=None):
        super(EnjoyEdit, self).__init__(parent)
        self.keyboard = None

    def mousePressEvent(self, event):
        super(EnjoyEdit, self).mousePressEvent(event)
        # 自定义点击事件
        if self.keyboard is None:
            self.keyboard = EnjoyKeyBoard(self)
        self.update_position()
        self.keyboard.press_signal.connect(self.update_edit)
        self.keyboard.press_signal.disconnect()
        self.keyboard.press_signal.connect(self.update_edit)
        self.keyboard.show()

    def update_position(self):
        desktop = QtWidgets.QApplication.desktop()
        self.keyboard.setMinimumWidth(desktop.width())
        self.keyboard.setMaximumWidth(desktop.width())
        self.keyboard.setMaximumHeight(desktop.height() / 5)
        self.keyboard.setMinimumHeight(desktop.height() / 5)
        self.keyboard.setGeometry(0, desktop.height()-self.keyboard.height(), self.keyboard.width(), self.keyboard.height())

    def update_edit(self, s):
        if s == '<-':
            self.setText(self.text()[:-1])
        elif s == 'clear':
            self.setText('')
        else:
            self.setText(self.text() + s)

# main.py
import sys

from PySide2.QtWidgets import QWidget, QVBoxLayout, QApplication

from view.base.enjoy_edit import EnjoyEdit


class MainWidget(QWidget):
    def __init__(self):
        super().__init__()
        self.edit1 = EnjoyEdit()
        self.edit2 = EnjoyEdit()
        self.layout = QVBoxLayout()
        self.layout.addWidget(self.edit1)
        self.layout.addWidget(self.edit2)
        self.setLayout(self.layout)

    def closeEvent(self, event):
        # 逐个关闭所有子窗口
        for widget in QApplication.instance().allWidgets():
            if widget != self and isinstance(widget, QWidget):
                widget.close()
        event.accept()

if __name__ == '__main__':
    app = QApplication()
    w = MainWidget()
    w.show()
    sys.exit(app.exec_())

二、为什么选用QWidget而不适用QDialog实现键盘

由于QT程序一般有多个窗体,我们希望键盘不会被其他窗体阻塞,且我们也不希望键盘阻塞其他窗体,故上面代码所有窗体均为QWidget,而未使用QDialog。 使用QDialog要通过dialog.exec_()来拉起,否则不会处理窗口的事件。
模态对话框和非模态对话框的区别在于用户与对话框的交互方式和对应用程序的影响。
通过键盘程序的编写,我更倾向于使用QWidget来代替QDialog。

三、从窗体a拉起窗体b后,窗体b闪退问题的探讨

由于在窗体a中,我通过w = QWidget() ,w.show()闪退,故考虑使用QDialog: q = QDialog() ,q.exec_()
但这样会产生一个问题,当处理键盘点击事件完成后,想要点击主窗口或其他子窗口事件需要先关闭键盘,这不符合我们平常的使用场景,我们不希望键盘阻塞其他窗口,于是又放弃了QDialog,回到QWidget研究闪退问题。

这里给出一段解决闪退问题的示例:
发生闪退问题的代码:

class ×××(QWidget):
    def __init__():
        super().__init()

    def ×××(self):
		w = QWidget()
		w.show()

未发生闪退问题的代码:

class ×××(QWidget):
    def __init__():
        super().__init()
        self.w = None

    def ×××(self):
       if self.w is None:
           self.w = QWidget(self)
       self.w.show()

推测:第一个代码由于w.show()后,整个解释器中就不存在w的引用,则w.show()后会清除w的实例
第二个代码由于有self.w = None的引用,故self.w.show()后不会清除self.w的实例

你可能感兴趣的:(虚拟键盘,pyqt5,pyside2)