PyQtNode Editor 第二篇自定义可视化视图

图片[1] - PyQtNode Editor 第二篇自定义可视化视图 - 宋马
在第一篇博客中,我们已经完成了 PyQtNode Editor 的基础环境搭建,并深入解析了自定义图形场景QDMGraphicsScene的实现原理。那个带有网格背景的场景就像一张空白的图纸,现在我们要在这张图纸上开始绘制真正的节点系统。

今天我们将聚焦于节点编辑器的核心数据结构设计,实现 “节点类” 与 “图形节点类” 的分离,并完成第一个可交互节点的创建。这一步是整个编辑器的骨架,就像建造房屋时搭建承重结构一样关键。

一、自定义可视化视图函数

创建一个node_graphic_view.py文件
在 PyQtNode Editor 的开发过程中,图形视图的设置对于用户最终看到的界面效果和操作体验至关重要。接下来,我们深入解析QDMGraphicsView类的代码,看看它是如何定制图形场景的显示与交互的。
导入必要的模块

from PyQt5.QtWidgets import QGraphicsView​
from PyQt5.QtCore import *​
from PyQt5.QtGui import *

from PyQt5.QtWidgets import QGraphicsView:从PyQt5.QtWidgets模块中导入QGraphicsView类。QGraphicsView是 PyQt5 中用于显示QGraphicsScene(图形场景)的视图类,它就像是一个 “画框”,可以将在场景中绘制的图形、节点等内容展示出来,并且提供了缩放、滚动等与图形交互的功能基础。​
from PyQt5.QtCore import *:导入PyQt5.QtCore模块中的所有内容。这个模块包含了 PyQt5 的核心功能,例如信号与槽机制、定时器、元对象系统等,这些功能为图形视图的事件处理、状态管理等提供底层支持 。​
from PyQt5.QtGui import *:导入PyQt5.QtGui模块中的所有内容。该模块主要用于处理图形相关的操作,比如绘制图形、设置画笔和画刷样式、处理字体等,是实现图形视图美观显示效果的关键。

定义 QDMGraphicsView 类

class QDMGraphicsView(QGraphicsView):

这里定义了一个名为QDMGraphicsView的类,它继承自QGraphicsView。继承的作用在于,QDMGraphicsView类可以复用QGraphicsView类已有的功能,同时在此基础上添加或修改特定的功能,以满足 PyQtNode Editor 项目中对于图形视图的个性化需求。

    def __init__(self, grScene, parent=None):​
        super().__init__(parent)​
        self.grScene = grScene​
​
        self.initUI()​
​
        self.setScene(self.grScene)

super().init(parent):调用父类QGraphicsView的初始化方法,确保父类的属性和行为得到正确的初始化,就像建造房子时先搭建好基本的框架结构。​
self.grScene = grScene:将传入的图形场景对象grScene赋值给当前类的实例属性self.grScene。这样,QDMGraphicsView类的实例就与特定的图形场景建立了关联,后续视图展示的内容就是这个场景中的元素。​
self.initUI():调用自定义的initUI方法,用于对图形视图进行一些个性化的设置,比如渲染效果、滚动条策略等。​
self.setScene(self.grScene):将之前保存的图形场景对象self.grScene设置为当前图形视图要显示的场景。这一步就像是把一幅画放入画框中,图形视图会将场景中的图形、节点等内容展示出来供用户查看和交互。

def initUI(self):
    self.setRenderHints(QPainter.Antialiasing | QPainter.HighQualityAntialiasing | QPainter.TextAntialiasing | QPainter.SmoothPixmapTransform)

    self.setViewportUpdateMode(QGraphicsView.FullViewportUpdate)

    self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
    self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

self.setRenderHints(QPainter.Antialiasing | QPainter.HighQualityAntialiasing | QPainter.TextAntialiasing | QPainter.SmoothPixmapTransform):通过setRenderHints方法设置图形渲染的提示选项。这里使用了按位或操作组合了多个渲染提示:​
QPainter.Antialiasing:启用抗锯齿功能,使绘制的图形边缘更加平滑,避免出现锯齿状。​
QPainter.HighQualityAntialiasing:进一步提高抗锯齿的质量,让图形看起来更加精美。​
QPainter.TextAntialiasing:对文本进行抗锯齿处理,保证显示的文字清晰、美观。​
QPainter.SmoothPixmapTransform:使图像在缩放或变换时更加平滑,避免出现模糊或失真的情况。​
self.setViewportUpdateMode(QGraphicsView.FullViewportUpdate):使用setViewportUpdateMode方法设置视图端口的更新模式为QGraphicsView.FullViewportUpdate。这意味着每当场景发生变化时,整个视图端口都会被更新。虽然这种方式在性能上可能不如其他更精细的更新模式,但可以确保视图显示的内容始终是最新和完整的。​
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)和self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff):分别设置水平和垂直滚动条的策略为Qt.ScrollBarAlwaysOff,即始终隐藏滚动条。这表明在当前的设计中,不希望用户通过滚动条来滚动视图,可能是因为场景的尺寸设置以及整体交互设计不需要滚动条,或者后续会通过其他方式(如鼠标滚轮缩放等)来实现对场景的浏览。

完整代码node_graphic_view.py

# 导入PyQt5的QGraphicsView类,用于显示和操作QGraphicsScene中的内容
from PyQt5.QtWidgets import QGraphicsView
# 导入PyQt5的核心模块,包含信号与槽、事件系统等基础功能
from PyQt5.QtCore import *
# 导入PyQt5的GUI模块,包含绘图、字体、颜色等图形相关功能
from PyQt5.QtGui import *


# 自定义图形视图类,继承自QGraphicsView,用于显示和操作节点编辑器的图形场景
class QDMGraphicsView(QGraphicsView):
    # 类的构造函数,接收图形场景对象和可选的父对象
    def __init__(self, grScene, parent=None):
        # 调用父类的构造函数完成初始化
        super().__init__(parent)
        # 保存传入的图形场景对象的引用
        self.grScene = grScene

        # 初始化用户界面设置
        self.initUI()

        # 将图形场景设置到视图中,使视图显示该场景的内容
        self.setScene(self.grScene)


    # 初始化用户界面的方法,设置视图的各种属性和行为
    def initUI(self):
        # 设置渲染提示,启用多项抗锯齿和高质量渲染选项:
        # - Antialiasing:平滑图形边缘,减少锯齿
        # - HighQualityAntialiasing:使用更高质量的抗锯齿算法
        # - TextAntialiasing:平滑文本边缘,提高可读性
        # - SmoothPixmapTransform:平滑处理缩放和变换后的像素图
        self.setRenderHints(QPainter.Antialiasing | QPainter.HighQualityAntialiasing | 
                           QPainter.TextAntialiasing | QPainter.SmoothPixmapTransform)

        # 设置视图更新模式为完全更新,每次场景变化时重绘整个视图
        # 这种模式确保显示效果一致,但可能影响性能
        self.setViewportUpdateMode(QGraphicsView.FullViewportUpdate)

        # 设置水平滚动条策略为始终关闭,不显示水平滚动条
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        # 设置垂直滚动条策略为始终关闭,不显示垂直滚动条
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

二、调用自定义视图函数

回到node_graphic_wnd.py文件
导入我们自定义的画布样式:

from node_graphics_view import QDMGraphicsView

然后对原来系统的标准视图更改成我们自定义的视图:
这个是原来的

self.view = QGraphicsView(self)
self.view.setScene(self.grScene)

更改后的:

    self.view = QDMGraphicsView(self.grScene, self)

然后添加一个addDebugContent()方法向场景中添加了多种图形元素用于测试:

def addDebugContent(self):
    # 创建绿色画刷和黑色轮廓画笔
    greenBrush = QBrush(Qt.green)
    outlinePen = QPen(Qt.black)
    outlinePen.setWidth(2)

    # 添加可移动的矩形
    rect = self.grScene.addRect(-100, -100, 80, 100, outlinePen, greenBrush)
    rect.setFlag(QGraphicsItem.ItemIsMovable)

    # 添加可选择和移动的文本
    text = self.grScene.addText("This is my Awesome text!", QFont("Ubuntu"))
    text.setFlag(QGraphicsItem.ItemIsSelectable)
    text.setFlag(QGraphicsItem.ItemIsMovable)
    text.setDefaultTextColor(QColor.fromRgbF(1.0, 1.0, 1.0))

    # 添加可移动的按钮控件
    widget1 = QPushButton("Hello World")
    proxy1 = self.grScene.addWidget(widget1)
    proxy1.setFlag(QGraphicsItem.ItemIsMovable)
    proxy1.setPos(0, 30)

    # 添加可选择的文本编辑控件
    widget2 = QTextEdit()
    proxy2 = self.grScene.addWidget(widget2)
    proxy2.setFlag(QGraphicsItem.ItemIsSelectable)
    proxy2.setPos(0, 60)

    # 添加可选择和移动的线条
    line = self.grScene.addLine(-200, -200, 400, -100, outlinePen)
    line.setFlag(QGraphicsItem.ItemIsMovable)
    line.setFlag(QGraphicsItem.ItemIsSelectable)

最终完整的node_graphic_wnd.py文件

# 从PyQt5的QtWidgets模块导入所有的类和函数,该模块包含各种用于创建用户界面的组件,如窗口、按钮、布局等
from PyQt5.QtWidgets import *
# 从PyQt5的QtGui模块导入所有的类和函数,此模块用于处理图形相关操作,如画笔、画刷、字体、绘图等功能
from PyQt5.QtGui import *
# 从PyQt5的QtCore模块导入所有的类和函数,该模块提供了PyQt5的核心功能,包括信号与槽机制、事件循环、定时器等
from PyQt5.QtCore import *

# 从自定义模块node_graphics_scene中导入QDMGraphicsScene类,该类用于创建自定义的图形场景
from node_graphics_scene import QDMGraphicsScene
# 从自定义模块node_graphics_view中导入QDMGraphicsView类,该类用于创建自定义的图形视图
from node_graphics_view import QDMGraphicsView


# 定义NodeEditorWnd类,继承自QWidget,用于创建节点编辑器的主窗口
class NodeEditorWnd(QWidget):
    # 类的初始化方法,接收一个可选的父窗口参数parent,默认为None
    def __init__(self, parent=None):
        # 调用父类QWidget的初始化方法,确保父类的属性和行为得到正确初始化
        super().__init__(parent)

        # 调用initUI方法,初始化用户界面
        self.initUI()


    # 初始化用户界面的方法
    def initUI(self):
        # 设置窗口在屏幕上的位置和大小,参数依次为x坐标、y坐标、宽度、高度
        self.setGeometry(200, 200, 800, 600)

        # 创建一个垂直布局管理器,用于管理窗口内的子控件布局
        self.layout = QVBoxLayout()
        # 设置布局管理器的边距为0,即子控件将填充整个布局区域,不留边界空白
        self.layout.setContentsMargins(0, 0, 0, 0)
        # 将创建好的垂直布局设置为当前窗口的布局
        self.setLayout(self.layout)

        # 创建自定义的图形场景对象,用于承载图形元素
        self.grScene = QDMGraphicsScene()

        # 创建自定义的图形视图对象,传入图形场景对象和当前窗口作为参数
        # 图形视图用于显示图形场景中的内容,并提供交互功能
        self.view = QDMGraphicsView(self.grScene, self)
        # 将图形视图添加到之前创建的垂直布局中,使其显示在窗口内
        self.layout.addWidget(self.view)

        # 设置窗口的标题栏显示文本
        self.setWindowTitle("Node Editor")
        # 显示窗口,使其在屏幕上可见
        self.show()

        # 调用addDebugContent方法,向图形场景中添加用于调试的内容
        self.addDebugContent()



    # 向图形场景中添加调试内容的方法
    def addDebugContent(self):
        # 创建一个绿色的画刷对象,用于填充图形区域
        greenBrush = QBrush(Qt.green)
        # 创建一个黑色的画笔对象,并设置画笔宽度为2,用于绘制图形的轮廓
        outlinePen = QPen(Qt.black)
        outlinePen.setWidth(2)

        # 在图形场景中添加一个矩形,参数依次为矩形左上角x坐标、y坐标、宽度、高度、画笔、画刷
        # 并将返回的矩形图形项对象赋值给rect变量
        rect = self.grScene.addRect(-100, -100, 80, 100, outlinePen, greenBrush)
        # 设置矩形图形项为可移动,用户可以通过鼠标拖动它
        rect.setFlag(QGraphicsItem.ItemIsMovable)

        # 在图形场景中添加一段文本,传入文本内容和字体对象
        # 并将返回的文本图形项对象赋值给text变量
        text = self.grScene.addText("This is my Awesome text!", QFont("Ubuntu"))
        # 设置文本图形项为可选择,用户可以选中它
        text.setFlag(QGraphicsItem.ItemIsSelectable)
        # 设置文本图形项为可移动,用户可以通过鼠标拖动它
        text.setFlag(QGraphicsItem.ItemIsMovable)
        # 设置文本的默认颜色为白色,使用RGBF格式(红、绿、蓝分量值范围为0.0 - 1.0)
        text.setDefaultTextColor(QColor.fromRgbF(1.0, 1.0, 1.0))

        # 创建一个QPushButton按钮控件,显示文本为"Hello World"
        widget1 = QPushButton("Hello World")
        # 将按钮控件添加到图形场景中,并返回一个代理对象,用于在图形场景中管理该控件
        proxy1 = self.grScene.addWidget(widget1)
        # 设置按钮的代理对象为可移动,用户可以通过鼠标拖动按钮
        proxy1.setFlag(QGraphicsItem.ItemIsMovable)
        # 设置按钮在图形场景中的位置,参数为x坐标和y坐标
        proxy1.setPos(0, 30)

        # 创建一个QTextEdit文本编辑控件
        widget2 = QTextEdit()
        # 将文本编辑控件添加到图形场景中,并返回一个代理对象
        proxy2 = self.grScene.addWidget(widget2)
        # 设置文本编辑控件的代理对象为可选择,用户可以选中它
        proxy2.setFlag(QGraphicsItem.ItemIsSelectable)
        # 设置文本编辑控件在图形场景中的位置
        proxy2.setPos(0, 60)

        # 在图形场景中添加一条直线,参数依次为起点x坐标、y坐标、终点x坐标、y坐标、画笔
        # 并将返回的直线图形项对象赋值给line变量
        line = self.grScene.addLine(-200, -200, 400, -100, outlinePen)
        # 设置直线图形项为可移动
        line.setFlag(QGraphicsItem.ItemIsMovable)
        # 设置直线图形项为可选择
        line.setFlag(QGraphicsItem.ItemIsSelectable)

运行第一篇的主函数main()
得到测试结果:
从图片中依次显示了一个文字、线段、绿色矩形、节点窗口,这些都可以进行自由拖动。
图片[2] - PyQtNode Editor 第二篇自定义可视化视图 - 宋马
鼠标拖动后:
图片[3] - PyQtNode Editor 第二篇自定义可视化视图 - 宋马目前文章的目录结构:

三、添加了鼠标中键按钮实现画布拖拽处理功能编写

回到编写的node_graphic_view.py文件
新增以下代码:

3.1 鼠标按键键函数功能

def mousePressEvent(self, event):
    if event.button() == Qt.MiddleButton:
        self.middleMouseButtonPress(event)
    elif event.button() == Qt.LeftButton:
        self.rightMouseButtonPress(event)  
    elif event.button() == Qt.RightButton:
        self.leftMouseButtonPress(event)
    else:
        super().mousePressEvent(event)

def mouseReleaseEvent(self, event):
    if event.button() == Qt.MiddleButton:
        self.middleMouseButtonRelease(event)
    elif event.button() == Qt.LeftButton:
        self.leftMouseButtonRelease(event)

mousePressEvent:根据按下的鼠标按键(左 / 中 / 右)调用对应的处理方法
mouseReleaseEvent:根据释放的鼠标按键调用对应的处理方法

3.2 中键拖动实现核心逻辑

def middleMouseButtonPress(self, event):
    # 1. 模拟左键释放事件
    releaseEvent = QMouseEvent(QEvent.MouseButtonRelease, event.localPos(), event.screenPos(),
                               Qt.LeftButton, Qt.NoButton, event.modifiers())
    super().mouseReleaseEvent(releaseEvent)
    
    # 2. 设置拖动模式为ScrollHandDrag(手形拖动)
    self.setDragMode(QGraphicsView.ScrollHandDrag)
    
    # 3. 模拟左键按下事件
    fakeEvent = QMouseEvent(event.type(), event.localPos(), event.screenPos(),
                            Qt.LeftButton, event.buttons() | Qt.LeftButton, event.modifiers())
    super().mousePressEvent(fakeEvent)

def middleMouseButtonRelease(self, event):
    # 1. 模拟左键释放事件
    fakeEvent = QMouseEvent(event.type(), event.localPos(), event.screenPos(),
                            Qt.LeftButton, event.buttons() & ~Qt.LeftButton, event.modifiers())
    super().mouseReleaseEvent(fakeEvent)
    
    # 2. 恢复拖动模式为NoDrag
    self.setDragMode(QGraphicsView.NoDrag)

这两个方法实现了中键拖动浏览场景的功能,采用了一种巧妙的事件模拟机制:

按下中键时:

先模拟释放左键(避免与其他左键操作冲突)
将视图的拖动模式设置为ScrollHandDrag(手形拖动模式)
再模拟按下左键(触发手形拖动)
释放中键时:
模拟释放左键(结束拖动)

将视图的拖动模式恢复为NoDrag

3.3 其他鼠标按键处理

def leftMouseButtonPress(self, event):
    return super().mousePressEvent(event)

def leftMouseButtonRelease(self, event):
    return super().mouseReleaseEvent(event)

def rightMouseButtonPress(self, event):
    return super().mousePressEvent(event)

def rightMouseButtonRelease(self, event):
    return super().mouseReleaseEvent(event)

这些方法目前只是简单地调用父类的实现,保留了默认的鼠标行为。在后续开发中,可以根据需要扩展这些方法,例如:

在右键按下时显示上下文菜单
实现左键选择节点的功能
添加鼠标滚轮缩放功能

此时完整的node_graphic_view.py代码文件

# 导入PyQt5的QGraphicsView类,用于显示和操作QGraphicsScene中的内容
from PyQt5.QtWidgets import QGraphicsView
# 导入PyQt5的核心模块,包含信号与槽、事件系统等基础功能
from PyQt5.QtCore import *
# 导入PyQt5的GUI模块,包含绘图、字体、颜色等图形相关功能
from PyQt5.QtGui import *


# 自定义图形视图类,继承自QGraphicsView,用于显示和操作节点编辑器的图形场景
class QDMGraphicsView(QGraphicsView):
    # 类的构造函数,接收图形场景对象和可选的父对象
    def __init__(self, grScene, parent=None):
        # 调用父类的构造函数完成初始化
        super().__init__(parent)
        # 保存传入的图形场景对象的引用
        self.grScene = grScene

        # 初始化用户界面设置
        self.initUI()

        # 将图形场景设置到视图中,使视图显示该场景的内容
        self.setScene(self.grScene)


    # 初始化用户界面的方法,设置视图的各种属性和行为
    def initUI(self):
        # 设置渲染提示,启用多项抗锯齿和高质量渲染选项:
        # - Antialiasing:平滑图形边缘,减少锯齿
        # - HighQualityAntialiasing:使用更高质量的抗锯齿算法
        # - TextAntialiasing:平滑文本边缘,提高可读性
        # - SmoothPixmapTransform:平滑处理缩放和变换后的像素图
        self.setRenderHints(QPainter.Antialiasing | QPainter.HighQualityAntialiasing | 
                           QPainter.TextAntialiasing | QPainter.SmoothPixmapTransform)

        # 设置视图更新模式为完全更新,每次场景变化时重绘整个视图
        # 这种模式确保显示效果一致,但可能影响性能
        self.setViewportUpdateMode(QGraphicsView.FullViewportUpdate)

        # 设置水平滚动条策略为始终关闭,不显示水平滚动条
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        # 设置垂直滚动条策略为始终关闭,不显示垂直滚动条
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        
    def mousePressEvent(self, event):
        # 判断鼠标按下的按键是否为中键
        if event.button() == Qt.MiddleButton:
            # 如果是中键按下,调用自定义的中键按下处理方法
            self.middleMouseButtonPress(event)
        # 判断鼠标按下的按键是否为左键
        elif event.button() == Qt.LeftButton:
            self.leftMouseButtonPress(event)
        # 判断鼠标按下的按键是否为右键
        elif event.button() == Qt.RightButton:
            # 如果是右键按下,调用自定义的右键按下处理方法
            self.rightMouseButtonPress(event)
        else:
            # 如果按下的是其他按键,调用父类的鼠标按下事件处理方法,
            # 保证默认的事件处理逻辑能够执行
            super().mousePressEvent(event)

    def mouseReleaseEvent(self, event):
        # 判断鼠标释放的按键是否为中键
        if event.button() == Qt.MiddleButton:
            # 如果是中键释放,调用自定义的中键释放处理方法
            self.middleMouseButtonRelease(event)
        # 判断鼠标释放的按键是否为左键
        elif event.button() == Qt.LeftButton:
            # 如果是左键释放,调用自定义的左键释放处理方法
            self.leftMouseButtonRelease(event)
        # 判断鼠标释放的按键是否为右键
        elif event.button() == Qt.RightButton:
            # 如果是右键释放,调用自定义的右键释放处理方法
            self.rightMouseButtonRelease(event)
        else:
            # 如果释放的是其他按键,调用父类的鼠标释放事件处理方法,
            # 维持默认的事件处理行为
            super().mouseReleaseEvent(event)


    def middleMouseButtonPress(self, event):
        # 创建一个模拟的鼠标左键释放事件对象,用于模拟先释放可能存在的左键按下状态
        # 参数依次为:事件类型(鼠标按键释放)、事件在视图局部坐标系的位置、在屏幕坐标系的位置、
        # 释放的按键(左键)、未按下的按键、键盘修饰键状态
        releaseEvent = QMouseEvent(QEvent.MouseButtonRelease, event.localPos(), event.screenPos(),
                                   Qt.LeftButton, Qt.NoButton, event.modifiers())
        # 调用父类的鼠标释放事件处理方法,处理模拟的左键释放事件
        super().mouseReleaseEvent(releaseEvent)
        # 设置图形视图的拖动模式为手形拖动模式,用户可以通过拖动来平移视图中的场景
        self.setDragMode(QGraphicsView.ScrollHandDrag)
        # 创建一个模拟的鼠标左键按下事件对象,用于触发手形拖动模式的操作
        # 参数依次为:事件类型(与原始事件类型相同)、事件在视图局部坐标系的位置、在屏幕坐标系的位置、
        # 按下的按键(左键)、当前按下的按键状态(添加左键按下状态)、键盘修饰键状态
        fakeEvent = QMouseEvent(event.type(), event.localPos(), event.screenPos(),
                                Qt.LeftButton, event.buttons() | Qt.LeftButton, event.modifiers())
        # 调用父类的鼠标按下事件处理方法,处理模拟的左键按下事件,从而启动手形拖动
        super().mousePressEvent(fakeEvent)



    def middleMouseButtonRelease(self, event):
        # 创建一个模拟的鼠标左键释放事件对象,用于模拟释放左键以结束手形拖动
        # 参数依次为:事件类型(鼠标按键释放)、事件在视图局部坐标系的位置、在屏幕坐标系的位置、
        # 释放的按键(左键)、当前按下的按键状态(移除左键按下状态)、键盘修饰键状态
        fakeEvent = QMouseEvent(event.type(), event.localPos(), event.screenPos(),
                                Qt.LeftButton, event.buttons() & ~Qt.LeftButton, event.modifiers())
        # 调用父类的鼠标释放事件处理方法,处理模拟的左键释放事件,结束手形拖动
        super().mouseReleaseEvent(fakeEvent)
        # 将图形视图的拖动模式设置为无拖动模式,恢复到默认的交互状态
        self.setDragMode(QGraphicsView.NoDrag)


    def leftMouseButtonPress(self, event):
        # 调用父类的鼠标左键按下事件处理方法,使用默认的左键按下处理逻辑
        return super().mousePressEvent(event)

    def leftMouseButtonRelease(self, event):
        # 调用父类的鼠标左键释放事件处理方法,使用默认的左键释放处理逻辑
        return super().mouseReleaseEvent(event)

    def rightMouseButtonPress(self, event):
        # 调用父类的鼠标右键按下事件处理方法,使用默认的右键按下处理逻辑
        return super().mousePressEvent(event)

    def rightMouseButtonRelease(self, event):
        # 调用父类的鼠标右键释放事件处理方法,使用默认的右键释放处理逻辑
        return super().mouseReleaseEvent(event)

此时重新运行主函数:按下鼠标中键可以实现对画布的自由拖拽以及移动。

四、鼠标滚轮实现对画布的自由缩放

在node_graphic_view.py代码文件下
首先在__init__函数下添加:

# 定义放大倍数因子,每次鼠标滚轮向上滚动时,视图将按照这个倍数进行放大
self.zoomInFactor = 1.25
# 定义是否开启缩放限制,当前设置为False,即暂不限制缩放范围
self.zoomClamp = False
# 定义当前的缩放级别,初始值设为10
self.zoom = 10
# 定义每次缩放时,缩放级别的变化量
self.zoomStep = 1
# 定义缩放范围,是一个列表,包含最小值和最大值,当前范围是[0, 10]
self.zoomRange = [0, 10]

在initUI函数下添加:

# 设置视图的变换锚点为鼠标所在位置,这意味着视图将以鼠标指针下方的点为中心进行缩放
self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)

在文件的末端添加子函数:

def wheelEvent(self, event):
    # 计算缩小倍数因子,为放大倍数因子的倒数,用于鼠标滚轮向下滚动时缩小视图
    zoomOutFactor = 1 / self.zoomInFactor

    # 根据鼠标滚轮滚动方向计算缩放倍数和更新当前缩放级别
    if event.angleDelta().y() > 0:
        # 如果鼠标滚轮向上滚动(angleDelta().y() > 0),使用放大倍数因子
        zoomFactor = self.zoomInFactor
        # 增加当前缩放级别
        self.zoom += self.zoomStep
    else:
        # 如果鼠标滚轮向下滚动,使用缩小倍数因子
        zoomFactor = zoomOutFactor
        # 减少当前缩放级别
        self.zoom -= self.zoomStep

    # 根据计算得到的缩放倍数,对视图进行缩放操作,x和y方向以相同倍数缩放
    self.scale(zoomFactor, zoomFactor)

此时重新运行主函数
得到画面:
图片[4] - PyQtNode Editor 第二篇自定义可视化视图 - 宋马
滚动鼠标滚轮可是实现对画布的自由放大与缩小:

五、画布缩放进行优化

在node_graphic_view.py代码文件下进行编写
首先将缩放限制打开

# 定义是否开启缩放限制,当前设置为False,即暂不限制缩放范围
self.zoomClamp = True

在wheelEvent函数下添加以下进行编写:

# 初始化一个变量clamped,用于标记当前缩放级别是否超出了预设范围并被修正
clamped = False
# 判断当前的缩放级别self.zoom是否小于预设的缩放范围下限self.zoomRange[0]
# 如果小于下限,则将当前缩放级别self.zoom设置为范围下限self.zoomRange[0],
# 并将clamped标记为True,表示缩放级别已被修正
if self.zoom < self.zoomRange[0]: self.zoom, clamped = self.zoomRange[0], True
# 判断当前的缩放级别self.zoom是否大于预设的缩放范围上限self.zoomRange[1]
# 如果大于上限,则将当前缩放级别self.zoom设置为范围上限self.zoomRange[1],
# 并将clamped标记为True,表示缩放级别已被修正
if self.zoom > self.zoomRange[1]: self.zoom, clamped = self.zoomRange[1], True

# 判断是否需要执行视图缩放操作
# 条件为clamped为False(即当前缩放级别未超出范围并被修正)
# 或者self.zoomClamp为False(即未启用缩放限制功能)
# 只有满足上述条件之一时,才会执行后续的视图缩放代码
if not clamped or self.zoomClamp is False:
    # 根据计算得到的缩放倍数,对视图进行缩放操作,x和y方向以相同倍数缩放
    self.scale(zoomFactor, zoomFactor)

此时重新运行主函数:
发现画布并不能限的放大和缩小,具有一定的限制。
最大为:
图片[5] - PyQtNode Editor 第二篇自定义可视化视图 - 宋马
最小为:
图片[6] - PyQtNode Editor 第二篇自定义可视化视图 - 宋马
完整的node_graphic_view.py

# 导入PyQt5的QGraphicsView类,用于显示和操作QGraphicsScene中的内容
from PyQt5.QtWidgets import QGraphicsView
# 导入PyQt5的核心模块,包含信号与槽、事件系统等基础功能
from PyQt5.QtCore import *
# 导入PyQt5的GUI模块,包含绘图、字体、颜色等图形相关功能
from PyQt5.QtGui import *


# 自定义图形视图类,继承自QGraphicsView,用于显示和操作节点编辑器的图形场景
class QDMGraphicsView(QGraphicsView):
    # 类的构造函数,接收图形场景对象和可选的父对象
    def __init__(self, grScene, parent=None):
        # 调用父类的构造函数完成初始化
        super().__init__(parent)
        # 保存传入的图形场景对象的引用
        self.grScene = grScene

        # 初始化用户界面设置
        self.initUI()

        # 将图形场景设置到视图中,使视图显示该场景的内容
        self.setScene(self.grScene)

        # 定义放大倍数因子,每次鼠标滚轮向上滚动时,视图将按照这个倍数进行放大
        self.zoomInFactor = 1.25
        # 定义是否开启缩放限制,当前设置为False,即暂不限制缩放范围
        self.zoomClamp = True
        # 定义当前的缩放级别,初始值设为10
        self.zoom = 10
        # 定义每次缩放时,缩放级别的变化量
        self.zoomStep = 1
        # 定义缩放范围,是一个列表,包含最小值和最大值,当前范围是[0, 10]
        self.zoomRange = [0, 10]

    # 初始化用户界面的方法,设置视图的各种属性和行为
    def initUI(self):
        # 设置渲染提示,启用多项抗锯齿和高质量渲染选项:
        # - Antialiasing:平滑图形边缘,减少锯齿
        # - HighQualityAntialiasing:使用更高质量的抗锯齿算法
        # - TextAntialiasing:平滑文本边缘,提高可读性
        # - SmoothPixmapTransform:平滑处理缩放和变换后的像素图
        self.setRenderHints(QPainter.Antialiasing | QPainter.HighQualityAntialiasing | 
                           QPainter.TextAntialiasing | QPainter.SmoothPixmapTransform)

        # 设置视图更新模式为完全更新,每次场景变化时重绘整个视图
        # 这种模式确保显示效果一致,但可能影响性能
        self.setViewportUpdateMode(QGraphicsView.FullViewportUpdate)

        # 设置水平滚动条策略为始终关闭,不显示水平滚动条
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        # 设置垂直滚动条策略为始终关闭,不显示垂直滚动条
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        # 设置视图的变换锚点为鼠标所在位置,这意味着视图将以鼠标指针下方的点为中心进行缩放
        self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)

    def mousePressEvent(self, event):
        # 判断鼠标按下的按键是否为中键
        if event.button() == Qt.MiddleButton:
            # 如果是中键按下,调用自定义的中键按下处理方法
            self.middleMouseButtonPress(event)
        # 判断鼠标按下的按键是否为左键
        elif event.button() == Qt.LeftButton:
            self.leftMouseButtonPress(event)
        # 判断鼠标按下的按键是否为右键
        elif event.button() == Qt.RightButton:
            # 如果是右键按下,调用自定义的右键按下处理方法
            self.rightMouseButtonPress(event)
        else:
            # 如果按下的是其他按键,调用父类的鼠标按下事件处理方法,
            # 保证默认的事件处理逻辑能够执行
            super().mousePressEvent(event)

    def mouseReleaseEvent(self, event):
        # 判断鼠标释放的按键是否为中键
        if event.button() == Qt.MiddleButton:
            # 如果是中键释放,调用自定义的中键释放处理方法
            self.middleMouseButtonRelease(event)
        # 判断鼠标释放的按键是否为左键
        elif event.button() == Qt.LeftButton:
            # 如果是左键释放,调用自定义的左键释放处理方法
            self.leftMouseButtonRelease(event)
        # 判断鼠标释放的按键是否为右键
        elif event.button() == Qt.RightButton:
            # 如果是右键释放,调用自定义的右键释放处理方法
            self.rightMouseButtonRelease(event)
        else:
            # 如果释放的是其他按键,调用父类的鼠标释放事件处理方法,
            # 维持默认的事件处理行为
            super().mouseReleaseEvent(event)


    def middleMouseButtonPress(self, event):
        # 创建一个模拟的鼠标左键释放事件对象,用于模拟先释放可能存在的左键按下状态
        # 参数依次为:事件类型(鼠标按键释放)、事件在视图局部坐标系的位置、在屏幕坐标系的位置、
        # 释放的按键(左键)、未按下的按键、键盘修饰键状态
        releaseEvent = QMouseEvent(QEvent.MouseButtonRelease, event.localPos(), event.screenPos(),
                                   Qt.LeftButton, Qt.NoButton, event.modifiers())
        # 调用父类的鼠标释放事件处理方法,处理模拟的左键释放事件
        super().mouseReleaseEvent(releaseEvent)
        # 设置图形视图的拖动模式为手形拖动模式,用户可以通过拖动来平移视图中的场景
        self.setDragMode(QGraphicsView.ScrollHandDrag)
        # 创建一个模拟的鼠标左键按下事件对象,用于触发手形拖动模式的操作
        # 参数依次为:事件类型(与原始事件类型相同)、事件在视图局部坐标系的位置、在屏幕坐标系的位置、
        # 按下的按键(左键)、当前按下的按键状态(添加左键按下状态)、键盘修饰键状态
        fakeEvent = QMouseEvent(event.type(), event.localPos(), event.screenPos(),
                                Qt.LeftButton, event.buttons() | Qt.LeftButton, event.modifiers())
        # 调用父类的鼠标按下事件处理方法,处理模拟的左键按下事件,从而启动手形拖动
        super().mousePressEvent(fakeEvent)



    def middleMouseButtonRelease(self, event):
        # 创建一个模拟的鼠标左键释放事件对象,用于模拟释放左键以结束手形拖动
        # 参数依次为:事件类型(鼠标按键释放)、事件在视图局部坐标系的位置、在屏幕坐标系的位置、
        # 释放的按键(左键)、当前按下的按键状态(移除左键按下状态)、键盘修饰键状态
        fakeEvent = QMouseEvent(event.type(), event.localPos(), event.screenPos(),
                                Qt.LeftButton, event.buttons() & ~Qt.LeftButton, event.modifiers())
        # 调用父类的鼠标释放事件处理方法,处理模拟的左键释放事件,结束手形拖动
        super().mouseReleaseEvent(fakeEvent)
        # 将图形视图的拖动模式设置为无拖动模式,恢复到默认的交互状态
        self.setDragMode(QGraphicsView.NoDrag)


    def leftMouseButtonPress(self, event):
        # 调用父类的鼠标左键按下事件处理方法,使用默认的左键按下处理逻辑
        return super().mousePressEvent(event)

    def leftMouseButtonRelease(self, event):
        # 调用父类的鼠标左键释放事件处理方法,使用默认的左键释放处理逻辑
        return super().mouseReleaseEvent(event)

    def rightMouseButtonPress(self, event):
        # 调用父类的鼠标右键按下事件处理方法,使用默认的右键按下处理逻辑
        return super().mousePressEvent(event)

    def rightMouseButtonRelease(self, event):
        # 调用父类的鼠标右键释放事件处理方法,使用默认的右键释放处理逻辑
        return super().mouseReleaseEvent(event)
    def wheelEvent(self, event):
        # 计算缩小倍数因子,为放大倍数因子的倒数,用于鼠标滚轮向下滚动时缩小视图
        zoomOutFactor = 1 / self.zoomInFactor

        # 根据鼠标滚轮滚动方向计算缩放倍数和更新当前缩放级别
        if event.angleDelta().y() > 0:
            # 如果鼠标滚轮向上滚动(angleDelta().y() > 0),使用放大倍数因子
            zoomFactor = self.zoomInFactor
            # 增加当前缩放级别
            self.zoom += self.zoomStep
        else:
            # 如果鼠标滚轮向下滚动,使用缩小倍数因子
            zoomFactor = zoomOutFactor
            # 减少当前缩放级别
            self.zoom -= self.zoomStep
        # 初始化一个变量clamped,用于标记当前缩放级别是否超出了预设范围并被修正
        clamped = False
        # 判断当前的缩放级别self.zoom是否小于预设的缩放范围下限self.zoomRange[0]
        # 如果小于下限,则将当前缩放级别self.zoom设置为范围下限self.zoomRange[0],
        # 并将clamped标记为True,表示缩放级别已被修正
        if self.zoom < self.zoomRange[0]: self.zoom, clamped = self.zoomRange[0], True
        # 判断当前的缩放级别self.zoom是否大于预设的缩放范围上限self.zoomRange[1]
        # 如果大于上限,则将当前缩放级别self.zoom设置为范围上限self.zoomRange[1],
        # 并将clamped标记为True,表示缩放级别已被修正
        if self.zoom > self.zoomRange[1]: self.zoom, clamped = self.zoomRange[1], True

        # 判断是否需要执行视图缩放操作
        # 条件为clamped为False(即当前缩放级别未超出范围并被修正)
        # 或者self.zoomClamp为False(即未启用缩放限制功能)
        # 只有满足上述条件之一时,才会执行后续的视图缩放代码
        if not clamped or self.zoomClamp is False:
            # 根据计算得到的缩放倍数,对视图进行缩放操作,x和y方向以相同倍数缩放
            self.scale(zoomFactor, zoomFactor)
© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容