如果创建一个自定义的窗口部件并重新实现它的绘制事件,就可以得到任何想要的图形。但如果需要绘制大量的单个项,或者是需要绘制用户能够进行单独交互的项(例如选中、移动、复制粘贴...) ,又或者需要对项进行动画处理,使用PyQt的图形视图类(QGraphicsView)比重新实现一个窗口部件的绘制事件更方便一些。
要使用图形视图类就必须创建一个场景(QGraphicsScene)。场景纯粹是数据,必须与至少一个QGraphicsView对象相关联才能实现可视化。在场景中绘制的项都是QGraphicsItem的子类(图形项)。
图形视图类的一个强大的功能是对图形项应用视图变换,例如缩放和旋转,这些变换可以影响场景的呈现方式,但不会改变图形项的自身类容。
各个视图类基本上是二维的;然而每个项都有一个z值,那些z值较高的项就会绘制在z值较低的项之上。但碰撞检测(collision detection)只基于项的(x,y)坐标。场景可以有一个前景层,例如可以为场景中每个项绘制网格;场景也可以有一个背景层,可提供一个背景图像或背景色。
项既可以是场景的一些子项,也可以是其它项的子项。当对一个项应用视图变换时,这些变换会递归地自动应用于该项的所有子项上去。
视图使用的是物理坐标系(viewport),场景使用的是逻辑坐标系(window),这个坐标系是在创建场景时选择的。在对项进行定位时,是使用逻辑坐标系的方式来放置它们。第三种坐标系是项所使用的坐标系,它的零点位于项的中心,也就是该项在场景中的位置。
下面的例子使用了三种图形项,文本图形项,普通图形项(边框)和像素图图形项。可以对它们进行独立的交互,可以将场景打印出来,还可以将 场景保存到一个自定义的文件以供后续打开。
代码如下:
import functools
import random
import sys
from PyQt5.QtCore import (QByteArray, QDataStream, QFile, QFileInfo,
QIODevice, QPoint, QPointF, QRectF, Qt)
from PyQt5.QtWidgets import (QApplication, QDialog,
QDialogButtonBox, QFileDialog, QFontComboBox,
QGraphicsItem, QGraphicsPixmapItem,
QGraphicsScene, QGraphicsTextItem, QGraphicsView, QGridLayout,
QHBoxLayout, QLabel, QMenu, QMessageBox,QPushButton, QSpinBox,
QStyle, QTextEdit, QVBoxLayout)
from PyQt5.QtGui import QFont,QCursor,QFontMetrics,QTransform,QPainter,QPen,QPixmap
from PyQt5.QtPrintSupport import QPrinter,QPrintDialog
MAC = True
try:
from PyQt5.QtGui import qt_mac_set_native_menubar
except ImportError:
MAC = False
PageSize = (595, 842) # A4 in points
#PageSize = (612, 792) # US Letter in points
PointSize = 10
MagicNumber = 0x70616765 #幻数
FileVersion = 1
Dirty = False #是否有未保存的更改
class TextItemDlg(QDialog):#添加文本的对话框
def __init__(self, item=None, position=None, scene=None, parent=None):
super(QDialog, self).__init__(parent)
self.item = item
self.position = position
self.scene = scene
self.editor = QTextEdit()
self.editor.setAcceptRichText(False)
self.editor.setTabChangesFocus(True)
editorLabel = QLabel("&Text:")
editorLabel.setBuddy(self.editor)
self.fontComboBox = QFontComboBox()
self.fontComboBox.setCurrentFont(QFont("Times", PointSize))
fontLabel = QLabel("&Font:")
fontLabel.setBuddy(self.fontComboBox)
self.fontSpinBox = QSpinBox()
self.fontSpinBox.setAlignment(Qt.AlignRight|Qt.AlignVCenter)
self.fontSpinBox.setRange(6, 280)
self.fontSpinBox.setValue(PointSize)
fontSizeLabel = QLabel("&Size:")
fontSizeLabel.setBuddy(self.fontSpinBox)
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok|
QDialogButtonBox.Cancel)
self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(False)
if self.item is not None:
self.editor.setPlainText(self.item.toPlainText())
self.fontComboBox.setCurrentFont(self.item.font())
self.fontSpinBox.setValue(self.item.font().pointSize())
layout = QGridLayout()
layout.addWidget(editorLabel, 0, 0)
layout.addWidget(self.editor, 1, 0, 1, 6)
layout.addWidget(fontLabel, 2, 0)
layout.addWidget(self.fontComboBox, 2, 1, 1, 2)
layout.addWidget(fontSizeLabel, 2, 3)
layout.addWidget(self.fontSpinBox, 2, 4, 1, 2)
layout.addWidget(self.buttonBox, 3, 0, 1, 6)
self.setLayout(layout)
self.fontComboBox.currentFontChanged.connect(self.updateUi)
self.fontSpinBox.valueChanged.connect(self.updateUi)
self.editor.textChanged.connect(self.updateUi)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.setWindowTitle("Page Designer - {0} Text Item".format(
"Add" if self.item is None else "Edit"))
self.updateUi()
def updateUi(self):
font = self.fontComboBox.currentFont()
font.setPointSize(self.fontSpinBox.value())
self.editor.document().setDefaultFont(font)
self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(
bool(self.editor.toPlainText()))
def accept(self):
if self.item is None:
self.item = TextItem("", self.position, self.scene)
font = self.fontComboBox.currentFont()
font.setPointSize(self.fontSpinBox.value())
self.item.setFont(font)
self.item.setPlainText(self.editor.toPlainText())
self.item.update()
global Dirty
Dirty = True
QDialog.accept(self)
class TextItem(QGraphicsTextItem): #文本图形项
def __init__(self, text, position, scene,
font=QFont("Times", PointSize), matrix=QTransform()):
super(TextItem, self).__init__(text)
#设置图形项为 可选中、可移动
self.setFlags(QGraphicsItem.ItemIsSelectable|QGraphicsItem.ItemIsMovable)
self.setFont(font)
self.setPos(position)
self.setTransform(matrix)
scene.clearSelection() #清除场景中的选择
scene.addItem(self)#将自身添加到场景中
self.setSelected(True)
global Dirty
Dirty = True
def parentWidget(self):
return self.scene().views()[0]
def itemChange(self, change, variant):
if change != QGraphicsItem.ItemSelectedChange:
global Dirty
Dirty = True
return QGraphicsTextItem.itemChange(self, change, variant)
def mouseDoubleClickEvent(self, event): #双击出现修改对话框
dialog = TextItemDlg(self, self.parentWidget())
dialog.exec_()
class GraphicsPixmapItem(QGraphicsPixmapItem): #像素图图形项
def __init__(self,pixmap):
super(QGraphicsPixmapItem, self).__init__(pixmap)
class BoxItem(QGraphicsItem): #方框(普通图形项)
def __init__(self, position, scene, style=Qt.SolidLine,
rect=None, matrix=QTransform()):
super(BoxItem, self).__init__()
#属性设为可选、可移动、可聚焦
self.setFlags(QGraphicsItem.ItemIsSelectable|
QGraphicsItem.ItemIsMovable|
QGraphicsItem.ItemIsFocusable)
if rect is None:
rect = QRectF(-10 * PointSize, -PointSize, 20 * PointSize,
2 * PointSize)
self.rect = rect
self.style = style
self.setPos(position)
self.setTransform(matrix)
scene.clearSelection()
scene.addItem(self)
self.setSelected(True)
self.setFocus()
global Dirty
Dirty = True
def parentWidget(self):
return self.scene().views()[0]
def boundingRect(self):
return self.rect.adjusted(-2, -2, 2, 2)
def paint(self, painter, option, widget):
pen = QPen(self.style)
pen.setColor(Qt.black)
pen.setWidth(1)
if option.state & QStyle.State_Selected:
pen.setColor(Qt.blue)
painter.setPen(pen)
painter.drawRect(self.rect)
def itemChange(self, change, variant):
if change != QGraphicsItem.ItemSelectedChange:
global Dirty
Dirty = True
return QGraphicsItem.itemChange(self, change, variant)
def contextMenuEvent(self, event): #添加右键菜单
wrapped = []
menu = QMenu(self.parentWidget())
for text, param in (
("&Solid", Qt.SolidLine),
("&Dashed", Qt.DashLine),
("D&otted", Qt.DotLine),
("D&ashDotted", Qt.DashDotLine),
("DashDo&tDotted", Qt.DashDotDotLine)):
wrapper = functools.partial(self.setStyle, param)
wrapped.append(wrapper)
menu.addAction(text, wrapper)
menu.exec_(event.screenPos())
def setStyle(self, style):
self.style = style
self.update()
global Dirty
Dirty = True
def keyPressEvent(self, event): #键盘事件
factor = PointSize / 4
changed = False
if event.modifiers() & Qt.ShiftModifier:
if event.key() == Qt.Key_Left:
self.rect.setRight(self.rect.right() - factor)
changed = True
elif event.key() == Qt.Key_Right:
self.rect.setRight(self.rect.right() + factor)
changed = True
elif event.key() == Qt.Key_Up:
self.rect.setBottom(self.rect.bottom() - factor)
changed = True
elif event.key() == Qt.Key_Down:
self.rect.setBottom(self.rect.bottom() + factor)
changed = True
if changed:
self.update()
global Dirty
Dirty = True
else:
QGraphicsItem.keyPressEvent(self, event)
class GraphicsView(QGraphicsView):#图形视图类
def __init__(self, parent=None):
super(GraphicsView, self).__init__(parent)
self.setDragMode(QGraphicsView.RubberBandDrag)
self.setRenderHint(QPainter.Antialiasing)
self.setRenderHint(QPainter.TextAntialiasing)
def wheelEvent(self, event):
#factor = 1.41 ** (-event.delta() / 240.0)
factor = event.angleDelta().y()/120.0
if event.angleDelta().y()/120.0 > 0:
factor=2
else:
factor=0.5
self.scale(factor, factor)
class MainForm(QDialog):
def __init__(self, parent=None):
super(MainForm, self).__init__(parent)
self.filename = ""
self.copiedItem = QByteArray()
self.pasteOffset = 5
self.prevPoint = QPoint()
self.addOffset = 5
self.borders = []
self.printer = QPrinter(QPrinter.HighResolution)
self.printer.setPageSize(QPrinter.Letter)
self.view = GraphicsView()#图形视图类
self.scene = QGraphicsScene(self)#场景
self.scene.setSceneRect(0, 0, PageSize[0], PageSize[1])#场景坐标和长宽
self.addBorders()#添加边框
self.view.setScene(self.scene)#为视图指定场景
self.wrapped = [] # Needed to keep wrappers alive
buttonLayout = QVBoxLayout()
for text, slot in (
("Add &Text", self.addText),
("Add &Box", self.addBox),
("Add Pi&xmap", self.addPixmap),
("&Align", None),
("&Copy", self.copy),
("C&ut", self.cut),
("&Paste", self.paste),
("&Delete...", self.delete),
("&Rotate", self.rotate),
("Pri&nt...", self.print_),
("&Open...", self.open),
("&Save", self.save),
("&Quit", self.accept)):
button = QPushButton(text)
if not MAC:
button.setFocusPolicy(Qt.NoFocus)
if slot is not None:
button.clicked.connect(slot)
if text == "&Align":
menu = QMenu(self)
for text, arg in (
("Align &Left", Qt.AlignLeft),
("Align &Right", Qt.AlignRight),
("Align &Top", Qt.AlignTop),
("Align &Bottom", Qt.AlignBottom)):
wrapper = functools.partial(self.setAlignment, arg)
self.wrapped.append(wrapper)
menu.addAction(text, wrapper)
button.setMenu(menu)
if text == "Pri&nt...":
buttonLayout.addStretch(5)
if text == "&Quit":
buttonLayout.addStretch(1)
buttonLayout.addWidget(button)
buttonLayout.addStretch()
layout = QHBoxLayout()
layout.addWidget(self.view, 1)
layout.addLayout(buttonLayout)
self.setLayout(layout)
fm = QFontMetrics(self.font())
self.resize(self.scene.width() + fm.width(" Delete... ") + 50,
self.scene.height() + 50)
self.setWindowTitle("Page Designer")
def addBorders(self):
self.borders = []
rect = QRectF(0, 0, PageSize[0], PageSize[1])
self.borders.append(self.scene.addRect(rect, Qt.yellow))
margin = 5.25 * PointSize
self.borders.append(self.scene.addRect(
rect.adjusted(margin, margin, -margin, -margin),
Qt.yellow))
def removeBorders(self):
while self.borders:
item = self.borders.pop()
self.scene.removeItem(item)
del item
def reject(self):
self.accept()
def accept(self):
self.offerSave()
QDialog.accept(self)
def offerSave(self):
if (Dirty and QMessageBox.question(self,
"Page Designer - Unsaved Changes",
"Save unsaved changes?",
QMessageBox.Yes|QMessageBox.No) ==
QMessageBox.Yes):
self.save()
def position(self):
point = self.mapFromGlobal(QCursor.pos())
if not self.view.geometry().contains(point):
coord = random.randint(36, 144)
point = QPoint(coord, coord)
else:
if point == self.prevPoint:
point += QPoint(self.addOffset, self.addOffset)
self.addOffset += 5
else:
self.addOffset = 5
self.prevPoint = point
return self.view.mapToScene(point)
def addText(self):
dialog = TextItemDlg(position=self.position(),
scene=self.scene, parent=self)
dialog.exec_()
def addBox(self):
BoxItem(self.position(), self.scene)
def addPixmap(self):
path = (QFileInfo(self.filename).path()
if self.filename else ".")
fname,filetype = QFileDialog.getOpenFileName(self,
"Page Designer - Add Pixmap", path,
"Pixmap Files (*.bmp *.jpg *.png *.xpm)")
if not fname:
return
self.createPixmapItem(QPixmap(fname), self.position())
def createPixmapItem(self, pixmap, position, matrix=QTransform()):
item = GraphicsPixmapItem(pixmap)
item.setFlags(QGraphicsItem.ItemIsSelectable|
QGraphicsItem.ItemIsMovable)
item.setPos(position)
item.setTransform(matrix)
self.scene.clearSelection()
self.scene.addItem(item)
item.setSelected(True)
global Dirty
Dirty = True
return item
def selectedItem(self):
items = self.scene.selectedItems()
if len(items) == 1:
return items[0]
return None
def copy(self):
item = self.selectedItem()
if item is None:
return
self.copiedItem.clear()
self.pasteOffset = 5
stream = QDataStream(self.copiedItem, QIODevice.WriteOnly)
self.writeItemToStream(stream, item)
def cut(self):
item = self.selectedItem()
if item is None:
return
self.copy()
self.scene.removeItem(item)
del item
def paste(self):
if self.copiedItem.isEmpty():
return
stream = QDataStream(self.copiedItem, QIODevice.ReadOnly)
self.readItemFromStream(stream, self.pasteOffset)
self.pasteOffset += 5
def setAlignment(self, alignment):#对齐
# Items are returned in arbitrary order
items = self.scene.selectedItems()
if len(items) <= 1:
return
# Gather coordinate data
leftXs, rightXs, topYs, bottomYs = [], [], [], []
for item in items:
rect = item.sceneBoundingRect()
leftXs.append(rect.x())
rightXs.append(rect.x() + rect.width())
topYs.append(rect.y())
bottomYs.append(rect.y() + rect.height())
# Perform alignment
if alignment == Qt.AlignLeft:
xAlignment = min(leftXs)
for i, item in enumerate(items):
item.moveBy(xAlignment - leftXs[i], 0)
elif alignment == Qt.AlignRight:
xAlignment = max(rightXs)
for i, item in enumerate(items):
item.moveBy(xAlignment - rightXs[i], 0)
elif alignment == Qt.AlignTop:
yAlignment = min(topYs)
for i, item in enumerate(items):
item.moveBy(0, yAlignment - topYs[i])
elif alignment == Qt.AlignBottom:
yAlignment = max(bottomYs)
for i, item in enumerate(items):
item.moveBy(0, yAlignment - bottomYs[i])
global Dirty
Dirty = True
def rotate(self): #旋转
for item in self.scene.selectedItems():
item.setRotation(item.rotation()+30)
def delete(self):
items = self.scene.selectedItems()
if (len(items) and QMessageBox.question(self,
"Page Designer - Delete",
"Delete {0} item{1}?".format(len(items),
"s" if len(items) != 1 else ""),
QMessageBox.Yes|QMessageBox.No) ==
QMessageBox.Yes):
while items:
item = items.pop()
self.scene.removeItem(item)
del item
global Dirty
Dirty = True
def print_(self):
dialog = QPrintDialog(self.printer)
if dialog.exec_():
painter = QPainter(self.printer)
painter.setRenderHint(QPainter.Antialiasing)
painter.setRenderHint(QPainter.TextAntialiasing)
self.scene.clearSelection()#清除场景选择
self.removeBorders()#打印前清除边框
self.scene.render(painter)
self.addBorders()#打印后恢复边框
def open(self):
self.offerSave()
path = (QFileInfo(self.filename).path()
if self.filename else ".")
fname,filetype = QFileDialog.getOpenFileName(self,
"Page Designer - Open", path,
"Page Designer Files (*.pgd)")
if not fname:
return
self.filename = fname
fh = None
try:
fh = QFile(self.filename)
if not fh.open(QIODevice.ReadOnly):
raise IOError(str(fh.errorString()))
items = self.scene.items()
while items:
item = items.pop()
self.scene.removeItem(item)
del item
self.addBorders()
stream = QDataStream(fh)
stream.setVersion(QDataStream.Qt_5_7)
magic = stream.readInt32()
if magic != MagicNumber:
raise IOError("not a valid .pgd file")
fileVersion = stream.readInt16()
if fileVersion != FileVersion:
raise IOError("unrecognised .pgd file version")
while not fh.atEnd():
self.readItemFromStream(stream)
except IOError as e:
QMessageBox.warning(self, "Page Designer -- Open Error",
"Failed to open {0}: {1}".format(self.filename, e))
finally:
if fh is not None:
fh.close()
global Dirty
Dirty = False
def save(self):
if not self.filename:
path = "."
#保存为自定义文件类型 .pgd
fname,filetype = QFileDialog.getSaveFileName(self,
"Page Designer - Save As", path,
"Page Designer Files (*.pgd)")
if not fname:
return
self.filename = fname
fh = None
try:
fh = QFile(self.filename)
if not fh.open(QIODevice.WriteOnly):
raise IOError(str(fh.errorString()))
self.scene.clearSelection()
stream = QDataStream(fh) #创建数据流
stream.setVersion(QDataStream.Qt_5_7)
stream.writeInt32(MagicNumber)
stream.writeInt16(FileVersion)
for item in self.scene.items(): #循环将图形项写进数据流
self.writeItemToStream(stream, item)
except IOError as e:
QMessageBox.warning(self, "Page Designer -- Save Error",
"Failed to save {0}: {1}".format(self.filename, e))
finally:
if fh is not None:
fh.close()
global Dirty
Dirty = False
def readItemFromStream(self, stream, offset=0):
#从数据流读取图形项
type = ""
position = QPointF()
matrix = QTransform()
rotateangle=0#add by yangrongdong
type=stream.readQString()
stream >> position >> matrix
if offset:
position += QPointF(offset, offset)
if type == "Text":
text = ""
font = QFont()
text=stream.readQString()
stream >> font
rotateangle=stream.readFloat()
tx=TextItem(text, position, self.scene, font, matrix)
tx.setRotation(rotateangle)
elif type == "Box":
rect = QRectF()
stream >> rect
style = Qt.PenStyle(stream.readInt16())
rotateangle=stream.readFloat()
bx=BoxItem(position, self.scene, style, rect, matrix)
bx.setRotation(rotateangle)
elif type == "Pixmap":
pixmap = QPixmap()
stream >> pixmap
rotateangle=stream.readFloat()
px=self.createPixmapItem(pixmap, position, matrix)
px.setRotation(rotateangle)
def writeItemToStream(self, stream, item):
#将项写进数据流的 实现
if isinstance(item, TextItem):
stream.writeQString("Text")
stream<<item.pos()<< item.transform()
stream.writeQString(item.toPlainText())
stream<< item.font()
stream.writeFloat(item.rotation())#add by yangrongdong
elif isinstance(item, GraphicsPixmapItem):
stream.writeQString("Pixmap")
stream << item.pos() << item.transform() << item.pixmap()
stream.writeFloat(item.rotation())#add by yangrongdong
elif isinstance(item, BoxItem):
stream.writeQString("Box")
stream<< item.pos() << item.transform() << item.rect
stream.writeInt16(item.style)
stream.writeFloat(item.rotation())#add by yangrongdong
app = QApplication(sys.argv)
form = MainForm()
rect = QApplication.desktop().availableGeometry()
form.resize(int(rect.width() * 0.6), int(rect.height() * 0.9))
form.show()
app.exec_()
本文分享自 Python可视化编程机器学习OpenCV 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体分享计划 ,欢迎热爱写作的你一起参与!