QPainter与Graphics View 架构的区别
PyQt5 提供了两种绘图方法。一种是使用 QPainter 类在 QWidget 类提供的画布上画图,可以 绘制点、线、圆等各种基本形状,从而组成自己需要的图形。所有界面组件都是 QWidget 的子类, 界面上的按钮、编辑框等各种组件的界面效果都是使用 QPainter 绘制出来的。用户从 QWidget 继 承定义自己的界面组件时,要绘制组件的界面效果也是使用这种方法。QPainter 绘制的图形如同 位图,是不可交互操作的图形。 PyQt5 另外提供一种基于 Graphics View 架构的绘图方法,这种方法使用 QGraphicsView、 QGraphicsScene 和各种 QGraphicsItem 图形项绘图,在一个场景中可以绘制大量图件,且每个图件 是可选择、可交互的,如同矢量图编辑软件那样可以操作每个图件。Graphics View 架构为用户绘制 复杂的组件化图形提供了便利。
关于QPainter跟Graphics View的基础知识请参考如下连接:
https://blog.csdn.net/zhengyanan815/article/details/127119421
https://blog.csdn.net/qq_40732350/article/details/90116319
需求说明
在主窗口中,存在一个按钮(或者label),点击该按钮,会弹出一个圆形菜单,如下图红框所示,我点击主窗口中的工具箱按钮,会弹出一个按钮菜单,跟悬浮桌面球的效果类似,虽然最终实现了,但是还存在两个小bug,这个后面会提到。当然使用QPainter也可以实现,这里主要是为了学习QGraphicsView
实现思路
新建一个主窗口,里面放置一个自定义的label(不想自定义也可以直接使用按钮,不过按钮也要重写,因为涉及到鼠标事件)
自定义一个QWidget继承自QGraphicsView,主要用来加载弹出菜单按钮,该自定义widget包含弹出动作,每次弹出时,会获取工具集按钮的全局坐标(相对于整个屏幕的),然后设置该widget的中心为工具集的中心坐标,最后做弹出动作,弹出菜单使用的是并行动画类:QParallelAnimationGroup()
当鼠标离开自定义widget区域时,菜单收起来,这里没有做收起动作,感兴趣的可以自己做一下,跟展开动作相反即可。
说明一下,为什么不做鼠标悬浮进入工具集区域,就弹出按钮,因为QGraphicsView设置为透明属性后,存在鼠标穿透问题,弹出菜单将会反复切换。
存在的问题:
存在两个问题如下:
QGraphicsView想要设置为背景透明,组件不透明,就必须设置透明背景区域不响应鼠标事件,这时,如果鼠标从透明区域出去,菜单将不会收起,如下图所示。
#设置为无边框模式,背景透明,组件不透明
self.view.setStyleSheet("background: transparent;border:0px")
self.view.setWindowFlags(Qt.FramelessWindowHint)
self.view.setAttribute(Qt.WA_TranslucentBackground) # 透明区域不响应鼠标事件
2. QGraphicsView组件中加载的按钮,样式设置为原型边框时,四角的白色区域仍然显示,不知道如何去除,正在的QWidget就不存在这个问题,大家看上面第一张图,那个大的原型按钮,就没有这个问题。
真对上面的两个问题,看到此篇文章的大佬,如果有解决办法,还请留言,感激不尽。
代码部分:
- 主窗口QDsigner设计的代码
mainWin.py
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'mainWin.ui'
#
# Created by: PyQt5 UI code generator 5.15.4
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(505, 346)
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
self.verticalLayout.setObjectName("verticalLayout")
spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout.addItem(spacerItem)
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_2.addItem(spacerItem1)
self.pushButton = QtWidgets.QPushButton(self.centralwidget)
self.pushButton.setMinimumSize(QtCore.QSize(100, 100))
self.pushButton.setStyleSheet("border-color: rgb(255, 255, 255);\n"
"font: 15pt \"MS Shell Dlg 2\";\n"
"background-color: rgb(255, 255, 255);")
icon = QtGui.QIcon()
icon.addPixmap(QtGui.QPixmap("tool.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.pushButton.setIcon(icon)
self.pushButton.setIconSize(QtCore.QSize(64, 64))
self.pushButton.setObjectName("pushButton")
self.horizontalLayout_2.addWidget(self.pushButton)
spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_2.addItem(spacerItem2)
self.verticalLayout.addLayout(self.horizontalLayout_2)
spacerItem3 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout.addItem(spacerItem3)
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
self.label = MyQLabel(self.centralwidget)
self.label.setText("")
self.label.setAlignment(QtCore.Qt.AlignCenter)
self.label.setObjectName("label")
self.horizontalLayout.addWidget(self.label)
self.verticalLayout.addLayout(self.horizontalLayout)
spacerItem4 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout.addItem(spacerItem4)
MainWindow.setCentralWidget(self.centralwidget)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "GraphicsView Demo"))
from myLabel import MyQLabel
2. 自定义label组件
myLabel.py
from PyQt5.QtWidgets import QLabel
from PyQt5.QtCore import pyqtSignal, QPoint, Qt
class MyQLabel(QLabel):
enterSignal = pyqtSignal(tuple)
def __init__(self, parent=None):
super(MyQLabel, self).__init__(parent)
# 鼠标左键点击触发
def mousePressEvent(self, event):
if (event.button() == Qt.LeftButton):
# label在屏幕的绝对位置
global_coordinates = self.mapToGlobal(QPoint(0, 0)).x(), self.mapToGlobal(QPoint(0, 0)).y()
# 获取label组件的中心绝对坐标(全屏幕范围内)
label_central = global_coordinates[0] + self.width() // 2, global_coordinates[1] + self.height() // 2
# print(label_central)
self.enterSignal.emit(label_central)
# 鼠标离开label
def leaveEvent(self, event):
# self.setAttribute(Qt.WA_TransparentForMouseEvents, True)
pass
3. 主程序入口以及自定义QGraphicsView
这两个写到了一起:runMainWin.py
import sys
import math
from PyQt5.QtCore import Qt, QPropertyAnimation, QParallelAnimationGroup, QPointF, QEasingCurve, QSize
from PyQt5.QtGui import QGuiApplication, QPixmap, QIcon
from PyQt5.QtWidgets import QMainWindow, QApplication, QGraphicsView, QGraphicsScene, QPushButton
from mainWin import Ui_MainWindow
# 自定义GraphicsView的子类,并实现按钮的添加
class MyGraphics(QGraphicsView):
def __init__(self, parent=None):
super(MyGraphics, self).__init__(parent)
self.__initView()
self.resize(166, 156)
def __initView(self):
self.scene = QGraphicsScene()
self.scene.setSceneRect(0, 0, 150, 150)
self.ref_x0 = self.scene.width() / 2 # 场景的中心x坐标
self.ref_y0 = self.scene.height() / 2 # 场景的中新y坐标
self.btnProxyList = []
btn1 = QPushButton(QIcon(QPixmap("openConfigFolder.png")), None)
btn2 = QPushButton(QIcon(QPixmap("newFile.png")), None)
btn2.setToolTip('New case file')
btn_size = 40
btn1.resize(btn_size, btn_size)
btn2.resize(btn_size, btn_size)
btn1_proxy = self.scene.addWidget(btn1)
btn1_proxy.setPos(self.ref_x0 - btn1_proxy.rect().width()/2, self.ref_y0 - btn1_proxy.rect().height()/2)
self.btnProxyList.append(btn1_proxy)
btn2_proxy = self.scene.addWidget(btn2)
btn2_proxy.setPos(self.ref_x0 - btn2_proxy.rect().width()/2, self.ref_y0 - btn2_proxy.rect().height()/2)
self.btnProxyList.append(btn2_proxy)
btn3 = QPushButton(QIcon("uploadWeight.png"), None)
btn4 = QPushButton(QIcon("coffee.png"), None)
btn3.resize(btn_size, btn_size)
btn4.resize(btn_size, btn_size)
btn3_proxy = self.scene.addWidget(btn3)
btn3_proxy.setPos(self.ref_x0 - btn3_proxy.rect().width() / 2, self.ref_y0 - btn3_proxy.rect().height() / 2)
self.btnProxyList.append(btn3_proxy)
btn4_proxy = self.scene.addWidget(btn4)
btn4_proxy.setPos(self.ref_x0 - btn4_proxy.rect().width() / 2, self.ref_y0 - btn4_proxy.rect().height() / 2)
self.btnProxyList.append(btn4_proxy)
btn5 = QPushButton(QIcon("punch.png"), None)
btn6 = QPushButton(QIcon("role.png"), None)
btn5.resize(btn_size, btn_size)
btn6.resize(btn_size, btn_size)
btn5_proxy = self.scene.addWidget(btn5)
btn5_proxy.setPos(self.ref_x0 - btn5_proxy.rect().width() / 2, self.ref_y0 - btn5_proxy.rect().height() / 2)
self.btnProxyList.append(btn5_proxy)
btn6_proxy = self.scene.addWidget(btn6)
btn6_proxy.setPos(self.ref_x0 - btn6_proxy.rect().width() / 2, self.ref_y0 - btn6_proxy.rect().height() / 2)
self.btnProxyList.append(btn6_proxy)
for x in range(6):
eval(f"btn{x+1}").setIconSize(QSize(30,30))
eval(f"btn{x+1}").setStyleSheet("background: #ffffff;border:2px solid #001721;border-radius: 20px;")
self.setScene(self.scene)
# 鼠标离开widget时执行的动作
def leaveEvent(self, event):
if self.isVisible():
self.hide()
pass
# 按钮动画,每次弹出时,都要调用一次
def btnAnimate(self):
R = 60 # 动画时,圆周的半径
item_number = len(self.items())
angle = [x*round(float(360/item_number), 2) for x in range(item_number)]
animateTime = 200 # 动画持续时间
self.group = QParallelAnimationGroup(self)
num = 0
for item in self.btnProxyList:
animation = QPropertyAnimation(item, b'pos')
animation.setStartValue(QPointF(self.ref_x0 - item.rect().width() / 2,
self.ref_y0 - item.rect().height() / 2))
animation.setEndValue(
QPointF(self.ref_x0 + R * math.cos(angle[num] * math.pi / 180) - item.rect().width() / 2,
self.ref_y0 - R * math.sin(angle[num] * math.pi / 180) - item.rect().height() / 2))
animation.setDuration(animateTime)
animation.setEasingCurve(QEasingCurve.InCubic)
self.group.addAnimation(animation)
num += 1
self.group.start()
self.update()
class MainWin(QMainWindow, Ui_MainWindow):
def __init__(self):
super(MainWin, self).__init__()
self.setupUi(self)
self.resize(730, 500)
self.__initUI()
self.handle()
def __initUI(self):
self.label.setFixedHeight(30)
self.label.setFixedWidth(30)
pix = QPixmap("tool.png")
pix.scaled(self.label.size(), Qt.IgnoreAspectRatio, Qt.SmoothTransformation) # 让图片填充满QLabel
# pix.scaled(self.label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
self.label.setPixmap(pix)
self.label.setScaledContents(True)
self.pushButton.setStyleSheet("background: #ffffff;border:3px solid #FF0000;border-radius:50px;")
self.view = MyGraphics()
def handle(self):
self.label.enterSignal.connect(self.showPopMenu)
def showPopMenu(self, labelPos):
# print(self.view.style().pixelMetric(QStyle.PM_TitleBarHeight)) # 获取标题栏高度
self.view.setStyleSheet("background: transparent;border:0px")
self.view.setWindowFlags(Qt.FramelessWindowHint)
self.view.setAttribute(Qt.WA_TranslucentBackground)
self.view.move(int(labelPos[0] - self.view.width() / 2), int(labelPos[1] - self.view.height()/2))
self.view.show()
self.view.btnAnimate()
self.view.setMouseTracking(True)
if __name__ == '__main__':
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps)
QGuiApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
app = QApplication(sys.argv)
main_win = MainWin()
main_win.show()
sys.exit(app.exec_())
资源链接
使用的资源以及代码打包放在一个文件夹,想学习的直接下载即可使用,CSDN有自动调整积分机制,如果不能下载,请联系我,我单独发给你们。
https://download.csdn.net/download/weixin_43054437/87361094