警告
本部分包含自动从 C++ 翻译成 Python 的代码片段,可能存在错误。
笔记示例#
笔记示例展示了如何重新实现一些 QWidget
的事件处理程序以接收应用程序控件生成的事件。
我们重新实现了鼠标事件处理程序以实现绘图,重新实现了绘制事件处理程序以更新应用程序,并重新实现了大小调整事件处理程序以优化应用程序的外观。此外,我们还重新实现了关闭事件处理程序以在终止应用程序之前拦截关闭事件。
示例还展示了如何使用 QPainter 在实时中绘制图像以及重绘小部件。
使用笔记应用程序,用户可以绘制图像。文件菜单允许用户打开和编辑现有图像文件,保存图像并退出应用程序。在绘图时,选项菜单允许用户选择笔颜色和笔宽度,以及清除屏幕。此外,帮助菜单为用户提供有关笔记示例特别是 Qt 的一般信息。
本例包含两个类
ScribbleArea
是一个自定义小部件,显示 QImage 并允许用户在其上绘制。
MainWindow
提供了一个在ScribbleArea
之上的菜单。
我们将首先回顾 ScribbleArea
类。然后我们将回顾使用 ScribbleArea
的 MainWindow
类。
ScribbleArea 类定义#
class ScribbleArea(QWidget): Q_OBJECT # public ScribbleArea(QWidget parent = None) openImage = bool(QString fileName) saveImage = bool(QString fileName, char fileFormat) def setPenColor(newColor): def setPenWidth(newWidth): bool isModified() { return modified; } QColor penColor() { return myPenColor; } int penWidth() { return myPenWidth; } # public slots def clearImage(): def print(): # protected def mousePressEvent(event): def mouseMoveEvent(event): def mouseReleaseEvent(event): def paintEvent(event): def resizeEvent(event): # private def drawLineTo(endPoint): def resizeImage(image, newSize): modified = False scribbling = False myPenWidth = 1 myPenColor = Qt.blue() image = QImage() lastPoint = QPoint()
ScribbleArea
类从 QWidget
继承。我们重新实现了 mousePressEvent()
、mouseMoveEvent()
和 mouseReleaseEvent()
函数以实现绘图。我们重新实现了 paintEvent()
函数以更新笔记区域,并重新实现了 resizeEvent()
函数以确保我们在任何时刻绘制的 QImage 至少与小部件一样大。
我们需要几个公共函数: openImage()
从文件中加载图像到草图区域,允许用户编辑图像; save()
将当前显示的图像写入文件; clearImage()
空闲槽清除草图区域显示的图像。我们需要私有的 drawLineTo()
函数来实际绘图,以及 resizeImage()
来更改 QImage 的大小。 print()
空闲槽处理打印。
我们还需要以下私有变量
modified
在草图区域显示的图像有未保存的更改时为true
。
scribbling
在用户在草图区域内按下鼠标左键时为true
。
penWidth
和penColor
存储当前应用程序中用于笔的设置宽度颜色。
image
存储用户绘制的图像。
lastPoint
保存最后一次鼠标点击或鼠标移动事件的光标位置。
草图区域类实现#
def __init__(self, parent): super().__init__(parent) setAttribute(Qt.WA_StaticContents)
在构造函数中,我们为小部件设置 Qt::WA_StaticContents 属性,表示小部件内容根植于左上角,并在小部件调整大小时不会改变。Qt 使用此属性来优化调整大小时的绘图事件。这纯粹是一种优化,并且仅应用于内容是静态且根植于左上角的小部件。
def openImage(self, QString fileName): loadedImage = QImage() if not loadedImage.load(fileName): return False newSize = loadedImage.size().expandedTo(size()) resizeImage(loadedImage, newSize) image = loadedImage modified = False update() return True
在 openImage()
函数中,我们加载给定的图像。然后使用私有的 resizeImage()
函数将加载的 QImage 调整大小,使其在两个方向上至少与小部件一样大,并将 image
成员变量设置为加载的图像。最后,我们调用 update()
来安排重绘。
def saveImage(self, QString fileName, char fileFormat): visibleImage = image resizeImage(visibleImage, size()) if visibleImage.save(fileName, fileFormat): modified = False return True return False
saveImage()
函数创建一个仅覆盖实际 image
可见部分的 QImage 对象,并使用 QImage::save() 进行保存。如果图像成功保存,我们将草图区域的 modified
变量设置为 false
,因为没有未保存的数据。
def setPenColor(self, newColor): myPenColor = newColor def setPenWidth(self, newWidth): myPenWidth = newWidth
setPenColor()
和 setPenWidth()
函数设置了当前的笔颜色和宽度。这些值将用于未来的绘图操作。
def clearImage(self): image.fill(qRgb(255, 255, 255)) modified = True update()
公共 clearImage()
空闲槽清除草图区域显示的图像。我们简单地使用白色填充整个图像,对应 RGB 值(255,255,255)。像通常修改图像一样,我们将 modified
设置为 true
并安排重绘。
def mousePressEvent(self, event): if event.button() == Qt.LeftButton: lastPoint = event.position().toPoint() scribbling = True def mouseMoveEvent(self, event): if (event.buttons() Qt.LeftButton) and scribbling: drawLineTo(event.position().toPoint()) def mouseReleaseEvent(self, event): if event.button() == Qt.LeftButton and scribbling: drawLineTo(event.position().toPoint()) scribbling = False
对于鼠标点击和鼠标释放事件,我们使用 QMouseEvent::button() 函数找出哪个按钮引发了事件。对于鼠标移动事件,我们使用 QMouseEvent::buttons() 找出当前按下的哪个按钮(作为 OR 组合)。
如果用户按下鼠标左键,我们将鼠标光标的位置存储在lastPoint
中。我们还记录用户目前正在涂鸦。(scribbling
变量是必要的,因为我们不能假设鼠标移动和鼠标释放事件总是由同一小部件上的鼠标按下事件引起的。)
如果用户按下左键移动鼠标或释放按钮,我们调用私有的drawLineTo()
函数进行绘制。
def paintEvent(self, event): painter = QPainter(self) dirtyRect = event.rect() painter.drawImage(dirtyRect, image, dirtyRect)
在重新实现paintEvent()
函数时,我们只需为涂鸦区域创建一个QPainter,并绘制图像。
此时,你可能会想知道为什么我们不直接在控件上绘制,而是在QImage中绘制并在paintEvent()
中将QImage复制到屏幕上。至少有以下三个很好的理由
操作系统要求我们能够随时重新绘制控件。例如,如果窗口被最小化并恢复,操作系统可能已经忘记了窗口控件的内容,并发送给我们一个绘制事件。换句话说,我们不能依赖操作系统记住我们的图像。
Qt通常不允许我们在
paintEvent()
之外进行绘制。特别是我们无法从鼠标事件处理器中进行绘制。(尽管可以通过使用Qt::WA_PaintOnScreen控件属性来更改此行为。)如果正确初始化,QImage每个颜色通道(红、绿、蓝和alpha)保证使用8位,而QWidget的颜色深度可能更低,取决于显示器的配置。这意味着如果我们加载一个24位或32位图像并将其绘制到一个QWidget中,然后再次将QWidget复制到QImage中,我们可能会丢失一些信息。
def resizeEvent(self, event): if width() > image.width() or height() > image.height(): newWidth = qMax(width() + 128, image.width()) newHeight = qMax(height() + 128, image.height()) resizeImage(image, QSize(newWidth, newHeight)) update() QWidget.resizeEvent(event)
当用户启动Scribble应用时,会生成一个调整大小事件,并在涂鸦区域创建并显示一个图像。我们使这个初始图像比应用程序的主窗口和涂鸦区域略微大一些,以避免在用户调整主窗口大小时一直调整图像大小(这将非常低效)。但是当主窗口变得比这个初始大小更大时,需要调整图像的大小。
def drawLineTo(self, endPoint): painter = QPainter(image) painter.setPen(QPen(myPenColor, myPenWidth, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)) painter.drawLine(lastPoint, endPoint) modified = True rad = (myPenWidth / 2) + 2 update(QRect(lastPoint, endPoint).normalized() .adjusted(-rad, -rad, +rad, +rad)) lastPoint = endPoint
在drawLineTo()
中,我们从上一次鼠标按下或鼠标移动发生时鼠标所在的位置画一条线,我们将modified
设为true,我们生成一个重绘事件,并更新lastPoint
,这样下次调用drawLineTo()
时,我们就从上次停止的地方继续绘制。
我们可以调用不带参数的update()
函数,但作为一个简单的优化,我们传递一个QRect,指定需要更新的涂鸦区域内的矩形,以避免整个控件的完整重绘。
def resizeImage(self, image, newSize): if image.size() == newSize: return newImage = QImage(newSize, QImage.Format_RGB32) newImage.fill(qRgb(255, 255, 255)) painter = QPainter(newImage) painter.drawImage(QPoint(0, 0), image) image = newImage
QImage没有好的API来进行图像缩放。存在一个QImage::copy()函数可以起到这个效果,但当用于扩展图像时,它会用黑色填充新的区域,而我们希望填充白色。
因此,秘诀是创建一个新的QImage对象,设置其正确的尺寸,用白色填充它,然后使用QPainter在上面绘制旧图像。新的图像被赋予QImage::Format_RGB32格式,这意味着每个像素都存储为0xffRRGGBB(其中RR、GG和BB分别是红、绿和蓝色通道,ff是十六进制的255)。
打印由print()
槽处理
def print(self): #if defined(QT_PRINTSUPPORT_LIB) and QT_CONFIG(printdialog) printer = QPrinter(QPrinter.HighResolution) printDialog = QPrintDialog(printer, self)
我们构造一个用于所需输出格式的High Resolution QPrinter对象,使用QPrintDialog询问用户指定页面大小并指示输出如何在页面上格式化。
如果对话框被接受,我们执行将任务打印到绘画设备的打印操作
if printDialog.exec() == QDialog.Accepted: painter = QPainter(printer) rect = painter.viewport() size = image.size() size.scale(rect.size(), Qt.KeepAspectRatio) painter.setViewport(rect.x(), rect.y(), size.width(), size.height()) painter.setWindow(image.rect()) painter.drawImage(0, 0, image) #endif // QT_CONFIG(printdialog)
以这种方式将图像打印到文件,只是简单地将它绘制到QPrinter上。在绘制到绘画设备之前,我们首先将图像缩放以适应页面上可用的空间。
MainWindow类定义#
class MainWindow(QMainWindow): Q_OBJECT # public MainWindow(QWidget parent = None) # protected def closeEvent(event): # private slots def open(): def save(): def penColor(): def penWidth(): def about(): # private def createActions(): def createMenus(): maybeSave = bool() saveFile = bool(QByteArray fileFormat) scribbleArea = ScribbleArea() saveAsMenu = QMenu() fileMenu = QMenu() optionMenu = QMenu() helpMenu = QMenu() openAct = QAction() *> = QList<QAction() exitAct = QAction() penColorAct = QAction() penWidthAct = QAction() printAct = QAction() clearScreenAct = QAction() aboutAct = QAction() aboutQtAct = QAction()
MainWindow
类从QMainWindow
继承而来。我们重新实现了来自QWidget
的closeEvent()
处理程序。与菜单条目对应的open()
、save()
、penColor()
和penWidth()
槽。此外,我们创建了四个私有函数。
我们使用布尔类型的maybeSave()
函数来检查是否有任何未保存的更改。如果有未保存的更改,我们允许用户保存这些更改。如果用户点击取消,该函数返回false
。我们使用saveFile()
函数让用户保存当前显示在涂鸦区域中的图像。
MainWindow类实现#
def __init__(self, parent): super().__init__(parent) self.scribbleArea = ScribbleArea(self) setCentralWidget(scribbleArea) createActions() createMenus() setWindowTitle(tr("Scribble")) resize(500, 500)
在构造函数中,我们创建了一个涂鸦区域,使其成为MainWindow
小部件的中心部件。然后我们创建了相关的动作和菜单。
def closeEvent(self, event): if maybeSave(): event.accept() else: event.ignore()
关闭事件被发送到用户想要关闭的小部件,通常通过点击文件|退出或点击X标题栏按钮来完成。通过实现事件处理程序,我们可以截获尝试关闭应用程序的请求。
在这个例子中,我们使用关闭事件来询问用户保存任何未保存的更改。这个逻辑位于maybeSave()
函数中。如果maybeSave()
返回true,则没有修改或用户成功保存了它们,此时我们接受事件。然后应用程序可以正常终止。如果maybeSave()
返回false,则用户点击了取消,因此我们“忽略”这个事件,让应用程序不受其影响。
def open(self): if maybeSave(): fileName = QFileDialog.getOpenFileName(self,() tr("Open File"), QDir.currentPath()) if not fileName.isEmpty(): scribbleArea.openImage(fileName)
在 open()
槽中,我们首先让用户有机会保存当前显示的图像的任何修改,然后再将新图像加载到涂鸦区域。然后我们要求用户选择一个文件,并在 ScribbleArea
中加载该文件。
def save(self): action = QAction(sender()) fileFormat = action.data().toByteArray() saveFile(fileFormat)
当用户选择“另存为”菜单项,并从格式菜单中选择一个条目时,会调用 save()
槽。我们需要做的第一件事是使用 QObject::sender()
函数找出是哪个操作发送了信号。这个函数返回一个作为 QObject 指针的发送者。既然我们知道发送者是一个操作对象,我们可以安全地将 QObject 强制转换。我们可以使用 C 风格的转换或 C++ 的 static_cast<>()
,但作为一种防御性编程技术,我们使用 qobject_cast()。优点是如果对象类型不正确,返回一个空指针。空指针导致的崩溃比不安全的转换导致的崩溃更容易诊断。
一旦我们有了操作,我们就使用 QAction::data()
提取所选的格式。(当创建动作时,我们使用 QAction::setData()
来设置与动作关联的自定义数据,作为一个 QVariant。更多关于这个内容将在我们回顾 createActions()
时介绍。)
现在我们知道格式后,我们调用私有的 saveFile()
函数来保存当前显示的图像。
def penColor(self): newColor = QColorDialog.getColor(scribbleArea.penColor()) if newColor.isValid(): scribbleArea.setPenColor(newColor)
我们使用 penColor()
槽通过一个 QColorDialog
来从用户处获取新颜色。如果用户选择了一个新颜色,我们就将其设置为涂鸦区域的颜色。
def penWidth(self): ok = bool() newWidth = QInputDialog.getInt(self, tr("Scribble"),() tr("Select pen width:"), scribbleArea.penWidth(), 1, 50, 1, ok) if ok: scribbleArea.setPenWidth(newWidth)
在 penWidth()
槽中获取新的笔宽,我们使用 QInputDialog
。QInputDialog
类提供了一个简单的便利对话框来从用户那里获取单个值。我们使用静态 getInt()
函数,结合了一个 QLabel
和一个 QSpinBox
。QSpinBox
初始化为涂鸦区域的笔宽,允许从 1 到 50 的范围,步长为 1(意味着上和下箭头键增加或减少值 1)。
布尔变量 ok
将在被设置为 true
如果用户点击了“确定”,如果用户按下了“取消”,则设置为 false
。
def about(self): QMessageBox.about(self, tr("About Scribble"), tr("<p>The <b>Scribble</b> example shows how to use QMainWindow as the " "base widget for an application, and how to reimplement some of " "QWidget's event handlers to receive the events generated for " "the application's widgets:</p><p> We reimplement the mouse event " "handlers to facilitate drawing, the paint event handler to " "update the application and the resize event handler to optimize " "the application's appearance. In addition we reimplement the " "close event handler to intercept the close events before " "terminating the application.</p><p> The example also demonstrates " "how to use QPainter to draw an image in real time, as well as " "to repaint widgets.</p>"))
我们实现了 about()
槽来创建一个消息框,描述示例设计用来展示的内容。
def createActions(self): openAct = QAction(tr("Open..."), self) openAct.setShortcuts(QKeySequence.Open) openAct.triggered.connect(self.open) imageFormats = QImageWriter.supportedImageFormats() for format in imageFormats: text = tr("%1...").arg(QString.fromLatin1(format).toUpper()) action = QAction(text, self) action.setData(format) action.triggered.connect(self.save) saveAsActs.append(action) printAct = QAction(tr("Print..."), self) printAct.triggered.connect(scribbleArea.print) exitAct = QAction(tr("Exit"), self) exitAct.setShortcuts(QKeySequence.Quit) exitAct.triggered.connect(self.close) penColorAct = QAction(tr("Pen Color..."), self) penColorAct.triggered.connect(self.penColor) penWidthAct = QAction(tr("Pen Width..."), self) penWidthAct.triggered.connect(self.penWidth) clearScreenAct = QAction(tr("Clear Screen"), self) clearScreenAct.setShortcut(tr("Ctrl+L")) clearScreenAct.triggered.connect( scribbleArea.clearImage) aboutAct = QAction(tr("About"), self) aboutAct.triggered.connect(self.about) aboutQtAct = QAction(tr("About Qt"), self) aboutQtAct.triggered.connect(qApp.aboutQt)
在 createAction()
函数中,我们创建了表示菜单条目并连接到相应槽位的动作。特别是我们创建了“另存为”子菜单中的动作。我们使用 QImageWriter::supportedImageFormats() 获取支持格式列表(作为一个 QList<QByteArray>)。
然后我们遍历列表,为每种格式创建一个动作。我们调用 QAction::setData() 并传入文件格式,这样我们可以在以后通过 QAction::data() 恢复它。我们还可以从动作的文本中推断文件格式,通过截断“…”,但这会有点不优雅。
def createMenus(self): saveAsMenu = QMenu(tr("Save As"), self) for action in saveAsActs: saveAsMenu.addAction(action) fileMenu = QMenu(tr("File"), self) fileMenu.addAction(openAct) fileMenu.addMenu(saveAsMenu) fileMenu.addAction(printAct) fileMenu.addSeparator() fileMenu.addAction(exitAct) optionMenu = QMenu(tr("Options"), self) optionMenu.addAction(penColorAct) optionMenu.addAction(penWidthAct) optionMenu.addSeparator() optionMenu.addAction(clearScreenAct) helpMenu = QMenu(tr("Help"), self) helpMenu.addAction(aboutAct) helpMenu.addAction(aboutQtAct) menuBar().addMenu(fileMenu) menuBar().addMenu(optionMenu) menuBar().addMenu(helpMenu)
在 createMenu()
函数中,我们将之前创建的格式动作添加到 saveAsMenu
。然后我们将其他动作以及 saveAsMenu
子菜单添加到“文件”、“选项”和“帮助”菜单。
QMenu
类为菜单栏、上下文菜单和其他弹出菜单提供了一个菜单小部件。 QMenuBar
类提供了一个水平菜单栏,包含一系列下拉 QMenu
。最后我们将文件和选项菜单放入 MainWindow
的菜单栏,我们使用 menuBar()
函数来获取它。
def maybeSave(self): if scribbleArea.isModified(): QMessageBox.StandardButton ret ret = QMessageBox.warning(self, tr("Scribble"), tr("The image has been modified.\n" "Do you want to save your changes?"), QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel) if ret == QMessageBox.Save: return saveFile("png") elif ret == QMessageBox.Cancel: return False return True
在 mayBeSave()
中,我们检查是否有未保存的更改。如果有,我们使用 QMessageBox
警告用户图像已被修改并有机会保存修改。
与 QColorDialog
和 QFileDialog
一样,创建一个 QMessageBox
最简单的方式是使用其静态函数。 QMessageBox
提供了不同类型的消息,排列在两个轴上:严重性(问题、信息、警告和严重)和复杂性(必要响应按钮的数量)。这里我们使用 warning()
函数,因为消息相当重要。
如果用户选择保存,我们将调用私有的 saveFile()
函数。为了简单起见,我们使用 PNG 作为文件格式;用户随时可以按取消并使用其他格式保存文件。
如果用户点击取消,maybeSave()
函数会返回 false
;否则返回 true
。
def saveFile(self, QByteArray fileFormat): initialPath = QDir.currentPath() + "/untitled." + fileFormat() fileName = QFileDialog.getSaveFileName(self, tr("Save As"),() initialPath, tr("%1 Files (*.%2);;All Files (*)") .arg(QString.fromLatin1(fileFormat.toUpper())) .arg(QString.fromLatin1(fileFormat))) if fileName.isEmpty(): return False return scribbleArea.saveImage(fileName, fileFormat.constData())
在 saveFile()
函数中,我们弹出一个文件对话框,并提出文件名建议。静态函数 getSaveFileName()
返回用户选择的一个文件名。该文件不必须存在。