PyQt5桌面应用开发(17):类结构+QWebEngineView

news/2024/12/24 20:45:13 标签: qt, python, 数据库, pyqt5, 界面设计

本文目录

  • PyQt5桌面应用系列
  • PyQt5学习
  • PyQt5类结构和帮助速查
  • 实现与解释
  • 最终界面和完整源代码
    • 界面
    • 完整的代码
  • 总结

PyQt5桌面应用系列

  • PyQt5桌面应用开发(1):需求分析
  • PyQt5桌面应用开发(2):事件循环
  • PyQt5桌面应用开发(3):并行设计
  • PyQt5桌面应用开发(4):界面设计
  • PyQt5桌面应用开发(5):对话框
  • PyQt5桌面应用开发(6):文件对话框
  • PyQt5桌面应用开发(7):文本编辑+语法高亮与行号
  • PyQt5桌面应用开发(8):从QInputDialog转进到函数参数传递
  • PyQt5桌面应用开发(9):经典布局QMainWindow
  • PyQt5桌面应用开发(10):界面布局基本支持
  • PyQt5桌面应用开发(11):摸鱼也要讲基本法,两个字,16
  • PyQt5桌面应用开发(12):QFile与线程安全
  • PyQt5桌面应用开发(13):QGraphicsView框架
  • PyQt5桌面应用开发(14):数据库+ModelView+QCharts
  • PyQt5桌面应用开发(15):界面动画
  • PyQt5桌面应用开发(16):定制化控件-QPainter绘图
  • PyQt5桌面应用开发(17):类结构+QWebEngineView

PyQt5学习

学习PyQt5有一小段时间了,看了大概四五本书的样子。

  • PyQt编程快速上手,人民邮电出版社,任路顺,2023-04
  • Qt for Python PySide6 GUI界面开发详解与实例,清华大学出版社,李增刚,2022-08
  • PyQt从入门到精通,清华大学出版社,明日科技,2021-06
  • Python Qt GUI与数据可视化编程,人民邮电出版社,王维波,2019-09
  • PyQt5快速开发与实战,电子工业出版社,王硕,2017-10

这几本书的结构简单整理如下,建议只看看目录或者电子版快速过一遍就行。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

国内还有一本书,微信读书上没有电子版。其实这本书看起来挺吸引人的。

  • Qt5/PyQt5实战指南:手把手教你掌握100个精彩案例

国外的PyQt5的书比较多,扫射了若干本,感觉比国内的书写得还要差。有一本Cookbook,都出到第三版,其内容非常简易,干货特别少,举的例子也不是很恰当。反而是国内的那几本书,没那么不堪。

要学习PyQt5,目的肯定是用PyQt5来做项目、做应用。其实最好的学习资料我感觉是qt.io
,如果对C++有一定的基础,很容易把这里面的内容结合到PyQt5中的stub(也就是*.pyi文件中描述的类接口),对具体开发的支持帮助很大。我基本上都是要用什么就看什么,然后去找Qt5
C++的文档来看。所以我写的这一系列教程,非常少罗列方法、信号和槽,因为这些信息在qt.io上非常齐全,介绍也很清晰。

这个系列基本上是从应用侧来观察PyQt5,落脚点始终在功能和应用上。国内的这几本我看过的书,相对来说,信息量也都是比较小,罗列接口的比较多,但是又不能作为完整的参考手册来使用,非常尴尬。其中那本从《PyQt5快速开发与实战》,我还不幸买了纸质版,很厚一本,挺烦人的。

PyQt5类结构和帮助速查

说起qt.io,有个小毛病,按类名查询慢吞吞的,主要是别的东西太多。那就自己撸一个。

开发需求,两个报表:

  • 显示PyQt5的类结构
  • 显示对应类的帮助

交互设计:

  • 显示一个类的树视图
  • 点击类名,显示对应的帮助

实现与解释

文件头很简单,导入包,这里专门用了*的方式导入,把所有类名都放到当前的空间中,目的是为了变了反射。

python">import re
import sys

# noinspection PyUnresolvedReferences
from PyQt5.QtCore import *
# noinspection PyUnresolvedReferences
from PyQt5.QtGui import *
from PyQt5.QtWebEngineWidgets import QWebEngineView
# noinspection PyUnresolvedReferences
from PyQt5.QtWidgets import *

这里就用一个魔术变量__name__和sys.modules把类名转换成类本身。从这里也可以看到,Python是如何找到一个类的。

python">def str_to_class(name):
    return getattr(sys.modules[__name__], name)

这个函数,就是找出pyi文件中的顶级类,也就是那些没有父类的类。在PyQt5的各个结构定义中,把父类设定为是sip.wrapper
或者PyQt5.sipsimplewrapper的哪些类。具体的路径,根据每个人PyQt5的安装位置不同。这里我们只针对"QtCore", “QtGui”, "
QtWidgets"
三个包。打开pyi文件,对每行进行模式匹配。这里有一点点Python的字符串匹配的内容"^class ([^\(\)]*)\(sip.wrapper\):$"
,这里就是匹配那些行开头是class,行结尾是:,中间把类名抓出来。方法最后,调用上面的函数,把字符串转为类。

python">def root_object_names(pyqt5_root="venv/Lib/site-packages/PyQt5-stubs",
                      class_fingerprint="^class ([^\(\)]*)\(sip.wrapper\):$"):
    files = [f"{pyqt5_root}/{fn}.pyi" for fn in ["QtCore", "QtGui", "QtWidgets"]]
    names = []
    for f in files:
        with open(f) as fid:
            for line in fid:
                ret = re.match(class_fingerprint, line)
                if ret is None:
                    continue
                captures = ret.groups()
                if len(captures) > 0:
                    names.append(captures[0])

    return [str_to_class(n) for n in sorted(names)]

此外还要定义一个方法,把类和子类构造成QTreeWidget中显示的形式。这个函数返回的是一个QTreeWidgetItem
,通过这个类的构造方法,自动把父子关系给建立起来了。这个函数的要点是两个。

  • __subclasses__魔术方法,得到子类;
  • 递归调用,构造父类-子类的完整的树
python">def walk_to_QTreeWdigetItem(self: object, parent: QTreeWidgetItem = None):
    sc = self.__subclasses__()
    item = QTreeWidgetItem(parent)
    item.setText(0, self.__name__)
    item.setText(1, f"{len(self.__dict__)}")
    item.setExpanded(True)
    for c in sc:
        walk_to_QTreeWdigetItem(c, item)
    return item

实现这个玩意的痛点就是要求能够搜索得比较快,那么这里我们定义一个包含搜索框和QTreeWidget的QWidget。构造函数中第一部分是构造界面,安排布局,上面一个搜索框,下面一个树控件。

第二部分就是树控件里面填充进PyQt5的类。

第三部分就是处理两个信号。

  • 树控件的点击
  • 搜索框的文字变化

可以看到,这个类还定义了一个信号,选择了一个项,这个信号,由树控件的点击事件触发,信号的参数就是类名。

python">class TreeWithSearch(QWidget):
    selectItem = pyqtSignal(str)

    def __init__(self, parent=None, classes=None):
        super(TreeWithSearch, self).__init__(parent=parent)
        self.layout = QVBoxLayout(self)
        self.searchBox = QLineEdit()
        self.classes = QTreeWidget()
        self.layout.addWidget(self.searchBox)
        self.layout.addWidget(self.classes)
        self.setLayout(self.layout)

        self.searchBox.setPlaceholderText("type class name")

        if classes is None:
            classes = root_object_names()
            classes.extend(
                root_object_names("venv/Lib/site-packages/PyQt5",
                                  "^class ([^\(\)]*)\(PyQt5.sipsimplewrapper\):$")
            )

            classes.sort(key=lambda iClass: iClass.__name__)

        for i, c in enumerate(classes):
            root = walk_to_QTreeWdigetItem(c)
            self.classes.addTopLevelItem(root)

        self.classes.setHeaderLabels(["name", "funcs"])
        self.classes.setColumnHidden(1, True)
        self.classes.setHeaderHidden(True)

        self.classes.clicked.connect(
            lambda index:
            self.selectItem.emit(self.classes.itemFromIndex(index).text(0))
        )

        self.searchBox.textChanged.connect(self._textChanged)

接下来就是有意思的一个点,搜索框的文字变化。这段代码是一个参数为字符串的槽函数。槽函数中,如果搜索框为空白,那么就遍历树控件中有子节点的节点,把他们全部设为折叠。

第二部分代码处理搜索。针对这个搜索字符串,我们遍历所有的节点,如果节点对应的类名包含搜索字符串,我们把这个节点的所有夫节点设为可见、展开。不包含的节点,设为隐藏。

python">    @pyqtSlot(str)


def _textChanged(self, name: str):
    # collapse parent nodes
    if name.isspace():
        iterator = QTreeWidgetItemIterator(self.classes, QTreeWidgetItemIterator.HasChildren)
        while (item := iterator.value()) is not None:
            item: QTreeWidgetItem
            item.setExpanded(False)
            iterator += 1
        return

    iterator = QTreeWidgetItemIterator(self.classes, QTreeWidgetItemIterator.All)
    while (item := iterator.value()) is not None:
        item: QTreeWidgetItem
        class_name: str = item.text(0).lower()
        is_show = name.strip().lower() in class_name
        item.setHidden(not is_show)
        # toggle to show and expand all its ancestors
        if is_show:
            p = item
            while p := p.parent():
                p.setHidden(False)
                p.setExpanded(True)
        iterator += 1

这里的关键知识点就是QTreeWidgetItemIterator,这个遍历器的访问方法就是it.value()函数和it += 1。如果漏掉了这个加一,那这个循环就成了无限循环。

最后就是整一个浏览视图。

python">class QtHelpView(QWebEngineView):

    def __init__(self, parent=None):
        super(QtHelpView, self).__init__(parent)
        self.base_url = "https://doc.qt.io/qt-5"
        self.load(QUrl(self.base_url))

    @pyqtSlot(str)
    def show_class(self, name):
        self.load(QUrl(f"{self.base_url}/{name.lower()}.html"))

主函数为一个要做的就是把界面搭起来,并且把TreeWithSearch的选择一项的信号与QtHelpView中的槽函数连起来。

python">if __name__ == '__main__':
    app = QApplication([])

    main_window = QMainWindow()

    tree = TreeWithSearch(main_window)

    dock = QDockWidget()
    dock.setWidget(tree)

    main_window.addDockWidget(Qt.LeftDockWidgetArea, dock)

    # pip install PyQtWebEngine
    view = QtHelpView(main_window)
    tree.selectItem.connect(view.show_class)

    main_window.setCentralWidget(view)

    main_window.resize(1440, 900)
    main_window.show()
    sys.exit(app.exec_())

最终界面和完整源代码

界面

最终实现的界面如下。通过这个app,可以浏览PyQt5主要的类结构,比如QObject和QPaintDevice这两个最主要的基类。点击一个类,就可以在右边看到对应的帮助(前提得上网)。

在这里插入图片描述

完整的代码

python">import re
import sys

# noinspection PyUnresolvedReferences
from PyQt5.QtCore import *
# noinspection PyUnresolvedReferences
from PyQt5.QtGui import *
from PyQt5.QtWebEngineWidgets import QWebEngineView
# noinspection PyUnresolvedReferences
from PyQt5.QtWidgets import *


def str_to_class(name):
    return getattr(sys.modules[__name__], name)


def root_object_names(pyqt5_root="venv/Lib/site-packages/PyQt5-stubs",
                      class_fingerprint="^class ([^\(\)]*)\(sip.wrapper\):$"):
    files = [f"{pyqt5_root}/{fn}.pyi" for fn in ["QtCore", "QtGui", "QtWidgets"]]
    names = []
    for f in files:
        with open(f) as fid:
            for line in fid:
                ret = re.match(class_fingerprint, line)
                if ret is None:
                    continue
                captures = ret.groups()
                if len(captures) > 0:
                    names.append(captures[0])

    return [str_to_class(n) for n in sorted(names)]


def walk_to_QTreeWdigetItem(self: object, parent: QTreeWidgetItem = None):
    sc = self.__subclasses__()
    item = QTreeWidgetItem(parent)
    item.setText(0, self.__name__)
    item.setText(1, f"{len(self.__dict__)}")
    item.setExpanded(True)
    for c in sc:
        walk_to_QTreeWdigetItem(c, item)
    return item


class TreeWithSearch(QWidget):
    selectItem = pyqtSignal(str)

    def __init__(self, parent=None, classes=None):
        super(TreeWithSearch, self).__init__(parent=parent)
        self.layout = QVBoxLayout(self)

        self.searchBox = QLineEdit()
        self.searchBox.setPlaceholderText("type class name")
        self.classes = QTreeWidget()

        if classes is None:
            classes = root_object_names()
            classes.extend(
                root_object_names("venv/Lib/site-packages/PyQt5",
                                  "^class ([^\(\)]*)\(PyQt5.sipsimplewrapper\):$")
            )

            classes.sort(key=lambda iClass: iClass.__name__)

        for i, c in enumerate(classes):
            root = walk_to_QTreeWdigetItem(c)

            self.classes.addTopLevelItem(root)
        self.classes.setHeaderLabels(["name", "funcs"])
        self.classes.setColumnHidden(1, True)
        self.classes.setHeaderHidden(True)
        self.layout.addWidget(self.searchBox)
        self.layout.addWidget(self.classes)

        self.setLayout(self.layout)

        self.classes.clicked.connect(
            lambda index:
            self.selectItem.emit(self.classes.itemFromIndex(index).text(0))
        )

        self.searchBox.textChanged.connect(self._textChanged)

    @pyqtSlot(str)
    def _textChanged(self, name: str):
        # collapse parent nodes
        if name.isspace():
            iterator = QTreeWidgetItemIterator(self.classes, QTreeWidgetItemIterator.HasChildren)
            while (item := iterator.value()) is not None:
                item: QTreeWidgetItem
                item.setExpanded(False)
                iterator += 1
            return

        iterator = QTreeWidgetItemIterator(self.classes, QTreeWidgetItemIterator.All)
        while (item := iterator.value()) is not None:
            item: QTreeWidgetItem
            class_name: str = item.text(0).lower()
            is_show = name.strip().lower() in class_name
            item.setHidden(not is_show)
            # toggle to show and expand all its ancestors
            if is_show:
                p = item
                while p := p.parent():
                    p.setHidden(False)
                    p.setExpanded(True)
            iterator += 1


class QtHelpView(QWebEngineView):

    def __init__(self, parent=None):
        super(QtHelpView, self).__init__(parent)
        self.base_url = "https://doc.qt.io/qt-5"
        self.load(QUrl(self.base_url))

    @pyqtSlot(str)
    def show_class(self, name):
        self.load(QUrl(f"{self.base_url}/{name.lower()}.html"))


if __name__ == '__main__':
    app = QApplication([])

    main_window = QMainWindow()

    tree = TreeWithSearch(main_window)

    dock = QDockWidget()
    dock.setWidget(tree)

    main_window.addDockWidget(Qt.LeftDockWidgetArea, dock)

    # pip install PyQtWebEngine
    view = QtHelpView(main_window)
    tree.selectItem.connect(view.show_class)

    main_window.setCentralWidget(view)

    main_window.resize(1440, 900)
    main_window.show()
    sys.exit(app.exec_())

总结

  1. qt.io是最好的学习资源;
  2. Python的良好反射特性(魔术方法)是探索实际代码和机制的很好工具;
  3. 学一个东西拿来用好过学一百个东西。

http://www.niftyadmin.cn/n/347498.html

相关文章

iOS-Telegraph异步响应实现

背景 Telegraph该库只支持管理本地同步请求,为了长远打算,需要研究是否能使response异步回调的方法 参考gitHub-Telegraph文档 现象:根据文档说明和示例,以及查看源码实现确认该第三方库确实只支持管理本地同步的请求响应 它的…

类初始化过程

文章目录 类初始化过程实例初始化过程方法的重写Override代码测试 类初始化过程 一个类要创建实例需要先加载并初始化该类 main方法所在的类需要先加载和初始化 一个子类要初始化需要先初始化父类一个类初始化就是执行()方法 ()方法由静态类变量显示赋值代码和静态代码块组成类…

Jupyter notebook 和 Jupyter lab 的区别

Jupyter Notebook和JupyterLab都是用于交互式计算和数据科学的开源工具 它们都是基于Jupyter项目构建的,提供了一种以笔记本形式创建、运行和共享代码、文本和可视化结果的方式。然而,Jupyter Notebook和JupyterLab在用户界面、功能和扩展性方面存在一些…

探究企业角色权限管理的重要性及实践方法

角色权限管理是企业网盘工具中的重要功能。它是指将特定角色分配给用户,然后根据用户的工作要求为这些角色分配访问权限的过程。通过使用基于角色的权限,组织可以确保员工只能访问执行工作职责所需的文件和文件夹。那么企业角色权限管理有必要吗&#xf…

【实施】畅捷通T+软件生产成本结转的理解

【项目场景】 在公司涉及到有生产的业务过程中, 针对生产升本结转的各项环节与理解。 【实施讲解】 在整个生产过程中,针对生产成本结转主要分为三个步骤; 步骤一: 日常发生的所有制造费用,借制造费用,比…

【Linux】权限管理,谁动了我代码?!

目录 一,shell命令以及运行原理 二 ,Linux用户权限 1. su —— 用户切换 三,权限管理 1. 理解 2. 用户 3. 文件类型 4. 文件基本权限 5. 设置文件权限方法 1. chmod —— 修改文件访问权限 2. chown —— 修改文件拥有者 3. chg…

根据视频直播流获取一个视频下载到本地

根据视频直播流获取一个视频下载到本地 视频流为一段视频流,比如直播的历史数据,我们获取的是摄像头的历史数据 import java.awt.Dimension; import java.awt.image.BufferedImage;import com.tlzn.tkwl.util.StringUtil; import org.bytedeco.javacpp.a…

煤矿电子封条系统 yolov7网络模型

煤矿电子封条系统通过yolov7网络模型算法,煤矿电子封条系统可以实现对煤矿井下人员的出入管理,提高对煤矿井下人员的监管效果。YOLOv7 的策略是使用组卷积来扩展计算块的通道和基数。研究者将对计算层的所有计算块应用相同的组参数和通道乘数。然后&…