找回密码
 立即注册
Qt开源社区 门户 查看内容

PyQt5 图形项的定义和交互(一)

2019-10-2 13:18| 发布者: admin| 查看: 2135| 评论: 0

摘要: 如果创建一个自定义的窗口部件并重新实现它的绘制事件,就可以得到任何想要的图形。但如果需要绘制大量的单个项,或者是需要绘制用户能够进行单独交互的项(例如选中、移动、复制粘贴...),又或者需要对项进行动画处 ...
如果创建一个自定义的窗口部件并重新实现它的绘制事件,就可以得到任何想要的图形。但如果需要绘制大量的单个项,或者是需要绘制用户能够进行单独交互的项(例如选中、移动、复制粘贴...) ,又或者需要对项进行动画处理,使用PyQt的图形视图类(QGraphicsView)比重新实现一个窗口部件的绘制事件更方便一些。

要使用图形视图类就必须创建一个场景(QGraphicsScene)。场景纯粹是数据,必须与至少一个QGraphicsView对象相关联才能实现可视化。在场景中绘制的项都是QGraphicsItem的子类(图形项)。

图形视图类的一个强大的功能是对图形项应用视图变换,例如缩放和旋转,这些变换可以影响场景的呈现方式,但不会改变图形项的自身类容。

各个视图类基本上是二维的;然而每个项都有一个z值,那些z值较高的项就会绘制在z值较低的项之上。但碰撞检测(collision detection)只基于项的(x,y)坐标。场景可以有一个前景层,例如可以为场景中每个项绘制网格;场景也可以有一个背景层,可提供一个背景图像或背景色。

项既可以是场景的一些子项,也可以是其它项的子项。当对一个项应用视图变换时,这些变换会递归地自动应用于该项的所有子项上去。

视图使用的是物理坐标系(viewport),场景使用的是逻辑坐标系(window),这个坐标系是在创建场景时选择的。在对项进行定位时,是使用逻辑坐标系的方式来放置它们。第三种坐标系是项所使用的坐标系,它的零点位于项的中心,也就是该项在场景中的位置。

下面的例子使用了三种图形项,文本图形项,普通图形项(边框)和像素图图形项。可以对它们进行独立的交互,可以将场景打印出来,还可以将 场景保存到一个自定义的文件以供后续打开。



代码如下:

    import functoolsimport randomimport sysfrom 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,QPixmapfrom PyQt5.QtPrintSupport import QPrinter,QPrintDialogMAC = Truetry: from PyQt5.QtGui import qt_mac_set_native_menubarexcept ImportError: MAC = FalsePageSize = (595, 842) # A4 in points#PageSize = (612, 792) # US Letter in pointsPointSize = 10MagicNumber = 0x70616765#幻数FileVersion = 1Dirty = False #是否有未保存的更改
    classTextItemDlg(QDialog):#添加文本的对话框def__init__(self, item=None, position=None, scene=None, parent=None):super(QDialog, self).__init__(parent)self.item = itemself.position = positionself.scene = sceneself.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)ifself.item is notNone: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"ifself.item is None else"Edit"))self.updateUi()
    defupdateUi(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()))
    defaccept(self):ifself.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)
    classTextItem(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):returnself.scene().views()[0]
    def itemChange(self, change, variant):if change != QGraphicsItem.ItemSelectedChange: global Dirty Dirty = Truereturn 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 = rectself.style = styleself.setPos(position)self.setTransform(matrix) scene.clearSelection() scene.addItem(self)self.setSelected(True)self.setFocus() global Dirty Dirty = True
    def parentWidget(self):returnself.scene().views()[0]
    def boundingRect(self):returnself.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 = Truereturn 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 = styleself.update() global Dirty Dirty = True
    def keyPressEvent(self, event): #键盘事件 factor = PointSize / 4 changed = Falseif 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 = Trueif changed:self.update() global Dirty Dirty = Trueelse: 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.0if event.angleDelta().y()/120.0 > 0: factor=2else: factor=0.5self.scale(factor, factor)
    class MainForm(QDialog): def __init__(self, parent=None):super(MainForm, self).__init__(parent)self.filename = ""self.copiedItem = QByteArray()self.pasteOffset = 5self.prevPoint = QPoint()self.addOffset = 5self.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)ifnot 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 * PointSizeself.borders.append(self.scene.addRect( rect.adjusted(margin, margin, -margin, -margin), Qt.yellow))
    def removeBorders(self):whileself.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()
    defposition(self): point = self.mapFromGlobal(QCursor.pos())ifnotself.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 += 5else:self.addOffset = 5self.prevPoint = pointreturnself.view.mapToScene(point)
    defaddText(self): dialog = TextItemDlg(position=self.position(), scene=self.scene, parent=self) dialog.exec_()
    defaddBox(self): BoxItem(self.position(), self.scene)
    defaddPixmap(self): path = (QFileInfo(self.filename).path()ifself.filename else".") fname,filetype = QFileDialog.getOpenFileName(self,"Page Designer - Add Pixmap", path,"Pixmap Files (*.bmp *.jpg *.png *.xpm)")ifnotfname:returnself.createPixmapItem(QPixmap(fname), self.position())
    defcreatePixmapItem(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 = Truereturn 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:returnself.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:returnself.copy()self.scene.removeItem(item) del item
    def paste(self):ifself.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 alignmentif 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 inself.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):whileitems: item = items.pop()self.scene.removeItem(item) del item global Dirty Dirty = True
    defprint_(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()#打印后恢复边框
    defopen(self):self.offerSave() path = (QFileInfo(self.filename).path()ifself.filename else".") fname,filetype = QFileDialog.getOpenFileName(self,"Page Designer - Open", path,"Page Designer Files (*.pgd)")ifnotfname:returnself.filename = fname fh = Nonetry: fh = QFile(self.filename)ifnot fh.open(QIODevice.ReadOnly): raise IOError(str(fh.errorString())) items = self.scene.items()whileitems: item = items.pop()self.scene.removeItem(item) del itemself.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")whilenot 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 notNone: fh.close() global Dirty Dirty = False
    defsave(self):ifnotself.filename: path = "."#保存为自定义文件类型 .pgd fname,filetype = QFileDialog.getSaveFileName(self,"Page Designer - Save As", path,"Page Designer Files (*.pgd)")ifnotfname:returnself.filename = fname fh = Nonetry: fh = QFile(self.filename)ifnot 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 inself.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 notNone: fh.close() global Dirty Dirty = False
    defreadItemFromStream(self, stream, offset=0):#从数据流读取图形项 type = "" position = QPointF() matrix = QTransform() rotateangle=0#add by yangrongdong type=stream.readQString() stream >> position >> matrixifoffset: 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)
    defwriteItemToStream(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 yangrongdongapp = 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,建议关注公众号查看原文。如若侵权请联系qter@qter.org。
    ----------------------------------------------------------------------------------------------------------------------

    鲜花

    握手

    雷人

    路过

    鸡蛋
    
    公告
    可以关注我们的微信公众号yafeilinux_friends获取最新动态,或者加入QQ会员群进行交流:190741849、186601429(已满) 我知道了