【原创】python3+PyQt5 图形项的自定义和交互

本文通过Python3+PyQt5实现《pythonQt Gui 快速编程》这本书的pageDesigner应用程序,采用QGraphicsView,QGraphicsScene,QGraphicsItem,这个程序包含有多个文本,图片和框的页面。有些图形类在PyQt5已过时,所以本代码改动幅度比较大。主要的类或方法的改变如下:

QMatrix==>QTransform

setMatrix==>setTransform

rotate ==> setRotation

本例中,由于event.delta()已过时,还重写了wheelEvent方法:

defwheelEvent(self, event):
       #factor = 1.41 ** (-event.delta() / 240.0) 
       #factor = 1.41 ** (-abs(event.startX()-event.y()) / 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)
为了保持代码可读行,增加了一个类:
classGraphicsPixmapItem(QGraphicsPixmapItem):       #add by yangrongdong
    def__init__(self,pixmap):
       super(QGraphicsPixmapItem, self).__init__(pixmap)
       
本例中还有包含菜单的按钮:
           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)

本例中还针对QStyleOptionGraphicsItem.levelOfDetail已过时,改写如下:
option.levelOfDetailFromTransform(self.transform())

下面为完整的代码:

#!/usr/bin/env python3

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 importQFont,QCursor,QFontMetrics,QTransform,QPainter,QPen,QPixmap
from PyQt5.QtPrintSupport import QPrinter,QPrintDialog

MAC = True
try:
    fromPyQt5.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()

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):
       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

defparentWidget(self):
       return self.scene().views()[0]

defitemChange(self, change, variant):
       if change != QGraphicsItem.ItemSelectedChange:
           global Dirty
           Dirty = True
       return QGraphicsTextItem.itemChange(self, change, variant)

defmouseDoubleClickEvent(self, event):
       dialog = TextItemDlg(self, self.parentWidget())
       dialog.exec_()

classGraphicsPixmapItem(QGraphicsPixmapItem):       #add by yangrongdong
    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

defparentWidget(self):
       return self.scene().views()[0]

defboundingRect(self):
       return self.rect.adjusted(-2, -2, 2, 2)

defpaint(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)

defitemChange(self, change, variant):
       if change != QGraphicsItem.ItemSelectedChange:
           global Dirty
           Dirty = True
       return QGraphicsItem.itemChange(self, change, variant)

defcontextMenuEvent(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())

defsetStyle(self, style):
       self.style = style
       self.update()
       global Dirty
       Dirty = True

defkeyPressEvent(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)

defwheelEvent(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")

defaddBorders(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))

defremoveBorders(self):
       while self.borders:
           item = self.borders.pop()
           self.scene.removeItem(item)
           del item

defreject(self):
       self.accept()

defaccept(self):
       self.offerSave()
       QDialog.accept(self)

defofferSave(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())
       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)

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()
           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())

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 = True
       return item

defselectedItem(self):
       items = self.scene.selectedItems()
       if len(items) == 1:
           return items[0]
       return None

defcopy(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)

defcut(self):
       item = self.selectedItem()
       if item is None:
           return
       self.copy()
       self.scene.removeItem(item)
       del item

defpaste(self):
       if self.copiedItem.isEmpty():
           return
       stream = QDataStream(self.copiedItem, QIODevice.ReadOnly)
       self.readItemFromStream(stream, self.pasteOffset)
       self.pasteOffset += 5

defsetAlignment(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

defrotate(self):
       for item in self.scene.selectedItems():
           item.setRotation(item.rotation()+30)

defdelete(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

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()
               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

defsave(self):
       if not self.filename:
           path = "."
           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

defreadItemFromStream(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)

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 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_()

运行结果:
(0)

相关推荐