PyQt5桌面应用开发(13):QGraphicsView框架

news2024/11/25 2:34:57

本文目录

  • PyQt5桌面应用系列
  • PyQt5 与艺术
  • code
  • QGraphicsView Framework
    • 几何
    • QGraphicsView应用
    • QGraphicsItem应用
    • keyPressEvent
  • QObject cross QThread/thread
  • 总计

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 与艺术

AI时代,计算创作艺术已经不是什么新闻。在前AI时代,为了讨好女朋友,秃子们也还是经常努力一下,比如用Turbo
C编个程序,显示一些闪来闪去一亮一亮的文字,效果不怎么样!别问我为什么知道。当时作为大一生的C语言老师,我指导了很多份这样的手工作品,但是班里成双结对的都是哪些Hello world!勉强能打印出来的帅小伙……

而今秃不秃什么的已经毫无心理波动,初心还是没有改变。那就来一次艺术与PyQt5的碰撞。

先看作品:

在这里插入图片描述

这个充满后现代主义、透着一种小清新、又带有亿点点叛逆的作品就是这次的快200多行代码的成果。当数量增加到20000后后,更有一种残酷感觉,一种欲言又止的感觉。Oh man, 那就是我的青春啊……

在这里插入图片描述

code

下面就是代码。

import random
import sys

from PyQt5.QtCore import Qt, QThread, pyqtSignal, pyqtSlot, QRectF
from PyQt5.QtGui import QColor, QFont, QPen, QFontDatabase, QPainter, QTransform
from PyQt5.QtGui import QImage, QKeyEvent, QKeySequence, QResizeEvent
from PyQt5.QtWidgets import QApplication, QGraphicsView, QGraphicsScene, QFileDialog, QMessageBox, QMainWindow
from PyQt5.QtWidgets import QInputDialog, qApp, QProgressBar, QGraphicsTextItem, QLabel


# def transform2str(tt: QTransform):
#     return f"""[{tt.m11():5.2f} {tt.m12():5.2f} {tt.m13():5.2f}\n{tt.m21():5.2f} {tt.m22():5.2f} {tt.m23():5.2f}\n{tt.m31():5.2f} {tt.m32():5.2f} {tt.m33():5.2f}]"""


class Worker(QThread):
    finished = pyqtSignal()
    tick_progress = pyqtSignal(int, tuple)
    start_tick = pyqtSignal(int)

    def __init__(self, n: int, r: QRectF):
        super(Worker, self).__init__()
        self.n = n
        self.r = r

    def run(self) -> None:
        self.start_tick.emit(self.n)
        for i in range(self.n):
            x, y = random.uniform(self.r.x(), self.r.x() + self.r.width()), \
                random.uniform(self.r.y(), self.r.y() + self.r.height())

            self.tick_progress.emit(i + 1, (
                random.randint(8, 28),  # fontSize
                chr(random.randint(ord('A'), ord('z'))),  # character
                random.randint(0, 255),  # red
                random.randint(0, 255),  # green
                random.randint(0, 255),  # blue
                random.randint(0, 255),  # alpha
                random.randint(0, 360),  # rotation
                x, y
            ))
        self.finished.emit()


class TextArtScene(QGraphicsScene):
    progress = pyqtSignal(int)

    def __init__(self, parent=None):
        super(TextArtScene, self).__init__(parent)
        self.fonts = None
        self.text_count = None
        self.fonts_choice = None

        self.setSceneRect(-200, -200, 400, 400)

    @pyqtSlot(int)
    def start_fill_text(self, n: int):
        self.clear()
        self.progress.emit(0)
        self.text_count = n
        self.fonts = [QFont(f) for f in QFontDatabase().families()]
        self.fonts_choice = random.choices(self.fonts, k=n)

    @pyqtSlot(int, tuple)
    def fill_text(self, i: int, text_property: tuple):
        font = self.fonts_choice[i - 1]
        progress = (i * 100) // self.text_count

        ps, text, red, green, blue, alpha, angle, x, y = text_property

        font.setPointSize(ps)
        text: QGraphicsTextItem = self.addText(text, font)
        text.setDefaultTextColor(QColor(red,
                                        green,
                                        blue,
                                        alpha))
        text.setRotation(angle)
        text.moveBy(x, y)

        # print(transform2str(text.sceneTransform()))

        self.progress.emit(progress)

        [current_view.viewport().update() for current_view in self.views()]

        if i == self.text_count:
            self.addRect(self.sceneRect(), QPen(QColor(255, 0, 0, 150), 4))
            self.progress.emit(100)


class TextArtGraphicsView(QGraphicsView):
    file_saved = pyqtSignal(str)

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

        self.setBackgroundBrush(QColor(125, 200, 0, 100))
        self.is_generating_in_progress = False

        n = int(400 * 1.618)
        self.setMinimumSize(n, n)
        self.count_of_text = 250

        self.worker = None
        self.progress_bar = QProgressBar(self)
        self.progress_bar.setRange(0, 100)
        self.progress_bar.setValue(0)
        self.progress_bar.setAlignment(Qt.AlignCenter)
        self.progress_bar.hide()

        self.text_scene = TextArtScene(self)
        self.setScene(self.text_scene)

        self.help_info = QLabel(self.info, self)
        self.help_info.setStyleSheet("color: darkgray;")

        self.file_saved.connect(self.show_saved_file)

    @property
    def info(self):
        return f"当前数目:{self.count_of_text}。 q: 退出;c/Space: 设置数目;r/F5: 生成;s/C-S:存储图片, ijkl↑↓←→h:自行探索。"

    @property
    def available(self):
        return not self.is_generating_in_progress

    @property
    def available(self):
        return not self.is_generating_in_progress

    def _request_textart_regeneration(self):
        if self.available:
            self.is_generating_in_progress = True

            self.worker = Worker(self.count_of_text, self.text_scene.sceneRect())
            self.worker.finished.connect(self.finish_textart_generation)
            self.worker.start_tick.connect(self.text_scene.start_fill_text)
            self.worker.tick_progress.connect(self.text_scene.fill_text)
            self.worker.start()

            self.text_scene.progress.connect(self.progress_bar.setValue)
            self.progress_bar.show()
            self.resetTransform()

    @pyqtSlot()
    def finish_textart_generation(self):
        self.is_generating_in_progress = False
        self.progress_bar.hide()

    @pyqtSlot(str)
    def show_saved_file(self, fn: str):
        QMessageBox.information(self, "Saved", f"<a href='{fn}'>{fn}</a>\nsaved.")

    def resizeEvent(self, event: QResizeEvent):
        self.progress_bar.setFixedWidth(event.size().width() + 2)
        self.help_info.setGeometry(10, self.height() - self.help_info.height(), self.width(), self.help_info.height())
        super(TextArtGraphicsView, self).resizeEvent(event)

    def keyPressEvent(self, event: QKeyEvent) -> None:
        if event.key() == Qt.Key_C or event.key() == Qt.Key_Space:
            count, flag = QInputDialog.getInt(self, "Count of QTextItem", "Please set an integer", self.count_of_text)
            if flag:
                self.count_of_text = count
                self.help_info.setText(self.info)

        elif event.matches(QKeySequence.Cancel) or event.key() == Qt.Key_Q:
            qApp.closeAllWindows()

        elif event.matches(QKeySequence.Refresh) or event.key() == Qt.Key_R:
            self._request_textart_regeneration()

        elif event.matches(QKeySequence.Save) or event.key() == Qt.Key_S:
            self.export_png()
        elif event.key() == Qt.Key_Left:
            self.rotate(-4)
            self.centerOn(0.0, 0.0)
        elif event.key() == Qt.Key_Right:
            self.rotate(4)
            self.centerOn(0.0, 0.0)
        elif event.key() == Qt.Key_Up:
            self.scale(1.1, 1.1)
            self.centerOn(0.0, 0.0)
        elif event.key() == Qt.Key_Down:
            self.scale(0.909, 0.909)
            self.centerOn(0.0, 0.0)
        elif event.key() == Qt.Key_J:
            self.shear(0.1, 0.0)
            self.centerOn(0.0, 0.0)
        elif event.key() == Qt.Key_L:
            self.shear(-0.1, 0.0)
            self.centerOn(0.0, 0.0)
        elif event.key() == Qt.Key_I:
            self.shear(0.0, 0.1)
            self.centerOn(0.0, 0.0)
        elif event.key() == Qt.Key_K:
            self.shear(0.0, -0.1)
            self.centerOn(0.0, 0.0)
        elif event.key() == Qt.Key_H:
            self.resetTransform()
        else:         
            super(TextArtGraphicsView, self).keyPressEvent(event)


    def export_png(self):
        fn, _ = QFileDialog.getSaveFileName(self,
                                            "Save Image", "./untitled.png",
                                            "PNG(*.png)")
        if fn:
            self.scene().clearSelection()

            image = QImage(self.size(), QImage.Format_RGBA64)
            painter = QPainter()

            painter.begin(image)
            painter.setRenderHint(QPainter.Antialiasing)
            self.render(painter)
            painter.end()  # must call it manually, or, an error will occur

            image.save(fn, "png", 100)
            self.file_saved.emit(fn)


if __name__ == "__main__":
    app = QApplication([])
    window = QMainWindow()
    window.setWindowTitle("Text art")
    view = TextArtGraphicsView(window)
    window.setCentralWidget(view)
    window.show()
    sys.exit(app.exec_())

QGraphicsView Framework

Qt5在画图这一块,QGraphicsView框架已经是比较成熟,在Qt6里面也还是原封不动。这个系统主要是由QGraphicsView、QGraphicsScene和QGraphicsItem三个部分构成的。

QGraphicsScene主要是是管理QGraphicsItem,不涉及实际的绘制部分,主要是处理View、Scene和Item的数学关系,其中View的坐标系以左上角为原点,x轴向右增长,y轴向下增长;Scene以其中心为原点坐标轴与View相同,所有的Item在被加到上面是,默认的位置也就是这个原点。Item的原点其中心,坐标方向同前。这里主要的数学变换设计两个,一个是Item的旋转和平移,最终体现为一个转换矩阵(2D的转移矩阵是 3 × 3 3\times3 3×3数组,直接加到Scene中间的Item,其转换矩阵用函数sceneTransform来获得。

QGraphicsScene可以连接到多个QGraphicsView,每个View和Scenne之间的关系,也是用同样的数学结构进行描述。

总结一句话:QGraphicsScene是一个数学模型,QGraphicsView是一个视图,QGraphicsItem是一个实体。数学模型恒久不变,提供一个空间;视图可以有多个,可以有不同的显示方式;实体可以有多个,可以有不同的形状,在数学模型中的位置、姿态也可以不同。

可以简单的说几句几何,但是下面的一小节无须关心。

几何

变化的计算大概是把旋转、平移、缩放、仿射集合成如下的矩阵 M M M

M = [ m 11 m 12 m 13 m 21 m 22 m 23 m 31 m 32 m 33 ] M = \begin{bmatrix} m_{11} & m_{12} & m_{13} \\ m_{21} & m_{22} & m_{23} \\ m_{31} & m_{32} & m_{33} \\ \end{bmatrix} M=m11m21m31m12m22m32m13m23m33

转换分为两步,首先,
[ x ′ y ′ w ] = [ x y 1 ] × M \left[ \begin{matrix} x' & y' & w \end{matrix} \right] = \left[ \begin{matrix} x & y & 1 \end{matrix} \right] \times M [xyw]=[xy1]×M

接下来, [ x ′ , y ′ , w ] [x', y', w] [x,y,w]再用 w w w归一化为, [ x t , y t , 1 ] [x_t, y_t, 1] [xt,yt,1]

程序伪码大概是:

x' = m11*x + m21*y + dx
y' = m22*y + m12*x + dy
if (!isAffine()) {
    w' = m13*x + m23*y + m33
    x' /= w'
    y' /= w'
}

这部分内容基本上就是线性代数、仿射几何的内容。不需要知道这些也没有关系。只需要能够对QGraphicsView和QGraphicsItem调用相应的函数就可以了。

QGraphicsView应用

QGraphicsView提供对QGraphicsScene的一个视图,这个视图可以缩放、可以拖动,可以旋转,还能够剪切。分别对应函数:

  • scale
  • move
  • rotate
  • shear

这所有函数是加上去的最终效果与之前的效果叠加。这些函数都是对QGraphicsView的,对QGraphicsScene没有影响。最终的效果是,QGraphicsView的坐标系发生了变化,但是QGraphicsScene的坐标系没有变化。对应的变换矩阵可以通过函数transform()获得。并且利用resetTransform()可以将QGraphicsView的变换矩阵恢复到初始状态。

其实,运行一下程序,就可以很容易理解上面的概念。下面是一些截图。

在这里插入图片描述

QGraphicsItem应用

前面大量演示了把一个TextItem移动、旋转的过程。对应的函数:

  • setPos
  • setRotation
  • setTransform
  • setTransformOriginPoint
  • setScale
  • setShear
  • setTransformations

具体的使用可以参考文档。

这里两个地方的用词就能看出区别,Item是本身的属性;而View是一个动作。

keyPressEvent

这个程序的操作基本上通过键盘来完成,没有涉及到鼠标点击、缩放。键盘事件在重载的函数keyPressEvent中处理。这个函数是在QGraphicsView中重载的。值得注意的式,QGraphicsView的事件处理函数,与QGraphicsScene没有关系。

这里有两种判断事件的方式:

  • event.matches(QKeySequence.Save)
  • event.key() == Qt.Key_S

前面这种方式对应操作系统中的键盘组合的概念,后面这种就是检测按键。这两种方式都可以,但是前面的方式更加灵活,可以检测到Ctrl+S和Ctrl+Shift+S,后面的方式只能检测到S。

QObject cross QThread/thread

这个问题的提出是在实现的早期版本中,发现QGraphicsScene中增加QGraphicsItem是一个很耗时的操作,所以就想着来个多线程,创建一个更新一个。但是发现,这样做不行,QGraphicsScene中的QGraphicsItem就不能显示了。而且,程序还一直报错:QObject::startTimer: Timers cannot be started from another thread,比如在终端里运行才能看到。

这个问题是QObject的线程再入特性导致的。这个特性的描述在文档中是这样的:

QObject reentry

结论一句话:

QObject created in one QThread, stays in the thread.

线程中创建的QObject,就在这个线程中使用。

那像这种情况,又要UI有响应,又要动态做一堆事情,怎么办?

整一个线程,模拟一个心跳,不停把这个事情放到主线程中去做。也就是,做事情的那个被当作事件加到主线程的事件循环中。那么,这里有没有更加直接的办法?就像是addEvent之类的方法?这样会节省很多时间,但在主线程中还需要有一个循环,如果这个循环有20000次,怎么办?就算是addEvent时间很短,主线程一样会卡顿。

上面程序的流程就是:

  1. 建立一个线程;
  2. 启动循环之前的准备工作;
  3. 启动循环,每次循环都是一个事件,这个事件会被加入到主线程的事件循环中;
  4. 结束循环之后的收尾工作。

这个做法不会增加程序的计算负载能力,但是用户的响应基本上在n比较小的时候还是存在的,我家的瓜娃子直接设成123123123,程序马上卡死。

总计

  1. QGraphicsView框架比较成熟,数学概念清晰。
  2. QGraphicsItem的使用比较简单,但是对于复杂的图形,很方便支持在局部坐标系中实现。
  3. QObject和线程的关系,通过事件机制实现。

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

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

相关文章

【Linux】进程控制(文件操作符收尾+重定向)

上一回进程与文件系统我们主要看了很多文件描述符的知识 1.如何理解一切皆文件&#xff1f; 每个设备被打开时&#xff0c;OS给每个文件创建一个自己的struct file 里面填充自己的属性以及自己的缓冲区&#xff0c;其中还有函数指针&#xff0c;里面保存函数地址&#xff0c;通…

京东CEO徐雷突然退休,CFO许冉接任成为首位女CEO

我是卢松松&#xff0c;点点上面的头像&#xff0c;欢迎关注我哦&#xff01; 京东集团重大人事变动&#xff0c;京东集团CEO徐雷将退休&#xff0c;才49岁&#xff0c;CFO许冉将成为京东集团新任CEO。京东将迎来首位女CEO。 徐雷在48岁被任命为京东CEO&#xff0c;49岁退休。…

IS220PRTDH1A固态过载继电器不会产生热量以方便跳闸

IS220PRTDH1A固态过载继电器不会产生热量以方便跳闸 一旦触发动作发生&#xff0c;双金属片冷却并自行重塑&#xff0c;自动重新启动电路。电机在过载未消除的情况下重新启动&#xff0c;并会一次又一次地点火和重新启动。&#xff08;假设自动重启 这种类型的继电器也可以配备…

Python的画图模块turtle使用详解

简介&#xff1a;Turtle是Python语言中一个很流行的简单的绘图工具。你可以把它理解为一个小海龟&#xff0c;只听得懂有限的指令。它在一个横轴为x、纵轴为y的坐标系原点&#xff0c;(0,0)位置开始&#xff0c;它根据一组函数指令的控制&#xff0c;在这个平面坐标系中移动&am…

【中级软件设计师】—(下午题)试题三精讲总结(四十二)

【中级软件设计师】—&#xff08;下午题&#xff09;试题三精讲总结&#xff08;四十二&#xff09; 一、关系 二、UML中的图 A包含B&#xff0c;那么A执行操作前必须要先执行B 试题一&#xff08;2021年下半年&#xff09; 试题2&#xff08;2021年上半年&#xff09; 官方…

【C++初阶】类和对象下篇

⭐博客主页&#xff1a;️CS semi主页 ⭐欢迎关注&#xff1a;点赞收藏留言 ⭐系列专栏&#xff1a;C初阶 ⭐代码仓库&#xff1a;C初阶 家人们更新不易&#xff0c;你们的点赞和关注对我而言十分重要&#xff0c;友友们麻烦多多点赞&#xff0b;关注&#xff0c;你们的支持是我…

板材激光切割机切割穿孔时注意的几个问题

激光切割设备广泛应用于钣金、五金制品、钢结构、汽车配件、广告、工艺品等行业&#xff0c;成为加工行业不可缺少的环节。在厚板加工中穿孔时间占很大比重&#xff0c;随着加工板材越来越厚&#xff0c;板材激光切割机切割穿孔也会相应地增加难度。 激光切割机两种常见的穿孔方…

druid 远程命令执行 (CVE-2021-25646)

漏洞原理 该漏洞主要就是根据Jackson解析特性(解析name为""时)会将value值绑定到对象(JavaScriptDimFilter&#xff0c;type为javascript时指定的)的对应参数(config)上&#xff0c;造成JavaScriptDimFilter中function属性中的javascript代码被执行。攻击者可以构造…

Redis高级数据结构HyperLogLog

HyperLogLog(Hyper[ˈhaɪpə(r)])并不是一种新的数据结构(实际类型为字符串类型)&#xff0c;而是一种基数算法,通过HyperLogLog可以利用极小的内存空间完成独立总数的统计&#xff0c;数据集可以是IP、Email、ID等。 如果你负责开发维护一个大型的网站&#xff0c;有一天产品…

pytorch移植华为mindspore记录

因为某个需求&#xff0c;需要把原来pytorch的神经网络移植到华为的mindspore上 这边记录下遇到的坑 附上mindspore的官方教程&#xff1a; https://mindspore.cn/tutorials/zh-CN/r2.0/advanced/compute_graph.html 这边附上需要移植的网络&#xff0c;以tensorflow和pytorch…

LeetCode 链表OJ分享

目录 删除排序链表中的重复元素回文链表剑指Offer 06.从尾到头打印链表复制带随机指针的链表 删除排序链表中的重复元素 链接: link 题目描述&#xff1a; 题目思路&#xff1a; 本题思路使用双指针&#xff0c;以示例二为例如下图&#xff1a; 如果head->val等于next-&…

ihateniggers:针对Python开发者的Windows远控木马分析

背景 墨菲安全实验室在持续监测开源软件仓库中的投毒行为&#xff0c;5 月 9 日起发现 4 个包含 “ihateniggers” 远程控制木马的 Python 包被 nagogygmail.com 邮箱关联的账号发布到 PyPI 仓库&#xff0c;试图针对Windows系统下 Python 开发者进行攻击。木马利用了discord、…

各种顺序表和链表的实现代码

目录 一、什么是线性表 二、顺序表 2.1什么是顺序表 2.2静态顺序表的代码实现 2.3动态顺序表的代码实现 三、链表 3.1什么是链表 3.2不带头单向不循环链表的代码实现 3.3带头双向循环链表的代码实现 四、顺序表和链表的区别 一、什么是线性表 线性表是n个具有相同特性…

(十五)数据编辑——图形编辑①

数据编辑——图形编辑① 数据编辑包括几何数据和属性数据的编辑。几何数据的编辑主要是针对图形的操作&#xff0c;即图形编辑&#xff0c;包括平行线复制、缓冲区生成、镜面反射、图层合并、结点操作、拓扑编辑等。属性编辑主要包括图层要素属性的添加、删除、修改、复制、粘…

谷歌落子,我们对中国大模型的期待应该是什么?

对中国大模型厂商而言&#xff0c;市场期待的&#xff0c;也恰是这些真正可落地的应用和实践。这些实践可以在社交&#xff0c;在电商&#xff0c;在低代码&#xff0c;在供应链&#xff0c;也更可以在一个个中国产业数字化转型的新洼地。 作者|思杭 皮爷 出品|产业家 在微软G…

裸辞5个月,面试了37家公司,终于.....

上半年裁员&#xff0c;下半年裸辞&#xff0c;有不少人高呼裸辞后躺平真的好快乐&#xff01;但也有很多人&#xff0c;裸辞后的生活五味杂陈。 面试37次终于找到心仪工作 因为工作压力大、领导PUA等各种原因&#xff0c;今年2月下旬我从一家互联网小厂裸辞&#xff0c;没想…

执行增删改查时的结果处理

查询最终走到PreparedStatementHandler类的query方法&#xff0c;执行查询后调用DefaultResultSetHandler类的handleResultSets方法 1.处理返回的普通实体类 DefaultResultSetHandler类的handleResultSets方法 继续本类的handleResultSet方法 通过 handleRowValues 方法来…

设计一个可靠的自动化测试框架需要考虑哪些问题呢?

随着软件开发的日益普及&#xff0c;自动化测试框架逐渐成为了保障软件质量的必备工具。然而&#xff0c;如何设计一个可靠的自动化测试框架并不是一件简单的事情&#xff0c;需要考虑多方面的问题。本文将从需求分析、架构设计、测试用例编写等多个角度&#xff0c;介绍设计一…

【SpringMVC】| SpringMVC拦截器

目录 一&#xff1a;SpringMVC拦截器 1. 拦截器介绍 2. HandlerInterceptor接口分析 3. 自定义拦截器实现权限验证 一&#xff1a;SpringMVC拦截器 SpringMVC 中的 Interceptor 拦截器&#xff0c;它的主要作用是拦截指定的用户请求&#xff0c;并进行相应的预处理与后处理…

C++面向对象(黑马程序员)

内存分区模型 #include<iostream> using namespace std;//栈区数据注意事项&#xff1a;不要返回局部变量的地址 //栈区的数据由编译器管理开辟和释放int* func(int b) //形参数据也会放在栈区 {b 100;int a 10; //局部变量存放在栈区&#xff0c;栈区的数据在函数执…