绘图操作主要通过QPainter类来进行,通过该类我们可以绘制许多种几何图形(点、线、矩形、椭圆、饼状图等等),当然也可以用来绘制图像和文字。本章首先来介绍QPainter类,之后再结合QPrinter类来了解下如何给程序加上打印功能。
31.1 画笔、画刷和字体
绘图操作通常在paintEvent()事件函数中完成。在该函数中,我们一般会先对画笔、画刷或者字体进行设置然后再调用相关方法进行绘制。
画笔用来画线和边缘,我们可以设置其颜色,线型和宽度等,下图是Qt提供的几种内置线型风格:
画刷用来对几何图形进行填充,我们同样可以设置其颜色和填充风格。下图是Qt提供的画刷风格:
可以看出最后个Qt::TexturePattern风格可以让我们刷出给定的图像。
字体用来绘制文字,我们可以设置其字体种类和大小,就是调用QPainter类的setFont()方法传入QFont类型参数(之前章节其实已经了解过该方法)
首先来了解下画笔QPen:
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPainter, QPen
from PyQt5.QtWidgets import QApplication, QWidget
class Demo(QWidget):
def __init__(self):
super(Demo, self).__init__()
self.resize(600, 600)
self.pen1 = QPen() # 1
self.pen1.setColor(Qt.green)
self.pen2 = QPen(Qt.SolidLine)
self.pen2.setWidth(6) # 2
# self.pen2.setWidthF(3.3)
self.pen3 = QPen(Qt.DashLine)
self.pen4 = QPen(Qt.DotLine)
self.pen5 = QPen(Qt.DashDotLine)
self.pen6 = QPen(Qt.DashDotDotLine)
self.pen7 = QPen(Qt.CustomDashLine) # 3
self.pen7.setDashPattern([6, 2, 18, 2])
self.pen8 = QPen(Qt.SolidLine) # 4
self.pen8.setWidth(6)
self.pen8.setCapStyle(Qt.RoundCap)
self.pen9 = QPen(Qt.SolidLine) # 5
self.pen9.setWidthF(6)
self.pen9.setJoinStyle(Qt.MiterJoin)
def paintEvent(self, QPaintEvent):
painter = QPainter(self) # 6
painter.setPen(self.pen1)
painter.drawLine(100, 10, 500, 10)
painter.setPen(self.pen2)
painter.drawLine(100, 30, 500, 30)
painter.setPen(self.pen3)
painter.drawLine(100, 50, 500, 50)
painter.setPen(self.pen4)
painter.drawLine(100, 70, 500, 70)
painter.setPen(self.pen5)
painter.drawLine(100, 90, 500, 90)
painter.setPen(self.pen6)
painter.drawLine(100, 110, 500, 110)
painter.setPen(self.pen7)
painter.drawLine(100, 130, 500, 130)
painter.setPen(self.pen8)
painter.drawLine(100, 150, 500, 150)
painter.setPen(self.pen2)
painter.drawRect(100, 170, 400, 200) # 7
painter.setPen(self.pen9)
painter.drawRect(100, 390, 400, 200)
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
-
实例化QPen对象,可直接传入画笔样式(默认为Qt.SolidLine),调用setColor()方法可设置画笔颜色;
-
调用setWidth()方法传入整型值,可设置画笔粗细(默认为1)。如果要传入浮点型的话可使用setWidthF();
-
使用Qt.CustomDashLine自定义样式的话,我们之后还需要调用setDashPattern()方法来设置虚线模式。只要传入一个迭代器即可,这里我们传入[6, 2, 18, 2]这个列表,意思是我们想将第一个虚线长度设为6个像素,再设置空白间隔长度为2个像素,之后再画一条长度为18像素的虚线,最后再加个长度为2像素的空白间隔,如此循环。
我们现在看下文档是怎么描述该方法的:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BCt0dZwP-1641378822831)(data:image/svg+xml;utf8, )]
- 调用setJoinStyle()设置线条连接方式为Qt.MiterJoin,一共有以下三种:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BsPYTFod-1641378822832)(data:image/svg+xml;utf8, )]
接下来是画刷(样式太多,笔者挑一些来讲):
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPainter, QBrush, QPixmap, QLinearGradient, QRadialGradient, QConicalGradient
from PyQt5.QtWidgets import QApplication, QWidget
class Demo(QWidget):
def __init__(self):
super(Demo, self).__init__()
self.resize(600, 600)
self.brush1 = QBrush(Qt.SolidPattern) # 1
self.brush2 = QBrush(Qt.Dense6Pattern) # 2
self.brush2.setColor(Qt.red)
gradient1 = QLinearGradient(200, 200, 300, 300) # 3
gradient1.setColorAt(0.2, Qt.red)
gradient1.setColorAt(0.8, Qt.green)
gradient1.setColorAt(1, Qt.blue)
self.brush3 = QBrush(gradient1)
gradient2 = QRadialGradient(350, 350, 50, 350, 350) # 4
gradient2.setColorAt(0, Qt.red)
gradient2.setColorAt(1, Qt.blue)
self.brush4 = QBrush(gradient2)
gradient3 = QConicalGradient(450, 450, 90) # 5
gradient3.setColorAt(0, Qt.red)
gradient3.setColorAt(1, Qt.blue)
self.brush5 = QBrush(gradient3)
self.brush6 = QBrush(Qt.TexturePattern) # 6
self.brush6.setTexture(QPixmap('images/smile.png'))
def paintEvent(self, QPaintEvent):
painter = QPainter(self)
painter.setBrush(self.brush1) # 7
painter.drawRect(0, 0, 100, 100)
painter.setBrush(self.brush2)
painter.drawRect(100, 100, 100, 100)
painter.setBrush(self.brush3)
painter.drawRect(200, 200, 100, 100)
painter.setBrush(self.brush4)
painter.drawRect(300, 300, 100, 100)
painter.setBrush(self.brush5)
painter.drawRect(400, 400, 100, 100)
painter.setBrush(self.brush6)
painter.drawRect(500, 500, 100, 100)
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
-
直接传入画刷样式进行实例化,默认的样式为Qt.NoBrush而不是Qt.SolidPattern;
-
同样可调用setColor()方法设置画刷颜色;
-
Qt一共提供三种渐变色样式,分别是线性渐变QLinearGradientPattern,径向渐变QRadialGradientPattern以及锥形渐变QConicalGradientPattern。要使用这三种渐变效果我们需要先实例化QLinearGradient,QRadialGradient或QConicalGradient对象,然后再实例化QBrush对象并传入相应的渐变类型。
我们在文档中查阅QLinearGradient发现其实例化所需要传入的参数为需要进行渐变的区域坐标,那其实只要把我们在paintEvent()事件函数中要绘制的矩形区域的坐标输入即可(输入两点坐标,若输入对角坐标,则在对角方向上渐变,笔者这里就是输入的左上和右下对角坐标):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LNLYaFwW-1641378822835)(data:image/svg+xml;utf8, )]
而QConicalGradient(450, 450, 90)中前两个值为中心点坐标,最后个为首个颜色开始处的角度值(范围为0-360):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TDVliC3p-1641378822836)(data:image/svg+xml;utf8, )]
字体的话就简单讲一下:
import sys
from PyQt5.QtGui import QPainter, QFont
from PyQt5.QtWidgets import QApplication, QWidget
class Demo(QWidget):
def __init__(self):
super(Demo, self).__init__()
self.resize(600, 600)
def paintEvent(self, QPaintEvent):
painter = QPainter(self)
painter.setFont(QFont('Times New Roman', 30))
painter.drawText(100, 100, 'Hello PyQt5!')
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
首先调用setFont设置好字体,然后调用drawText()方法传入坐标值以及要绘制的文字即可。
以上使用drawText()只是许多重载方法中的一种,Qt一共提供了以下几种(红框中的是我们使用的):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o9K4pRU3-1641378822838)(data:image/svg+xml;utf8, )]
31.2 利用双缓冲技术实现实时绘图
所谓的双缓冲技术,简单来说就是我们先在一张画布上画好我们想要的,然后再将这张画布上的内容在控件(屏幕)上呈现出来。稍微专业点来说就是先建立一个临时缓冲区用于存储我们要画的内容,之后拷贝到图像显示缓冲区中进行显示。相比直接在控件上绘图,双缓冲技术可以有效减少绘图时所产生的闪烁问题,也可以让绘制速度变得更快。自Qt4开始,控件就自带双缓冲绘图功能来消除闪烁问题了,所以没有必要再在paintEvent()事件函数中再编写双缓冲代码。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B7aOIv0h-1641378822839)(data:image/svg+xml;utf8, )]
但是其实是有问题的,在MacOS系统上我们每次小化窗口再显示时,所画的都会消失。
而以上代码在Windows和Linux(Ubuntu)系统中运行起来甚至还是下图这样的。一片漆黑,根本画不了:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GXyjLcV8-1641378822840)(data:image/svg+xml;utf8, )]
这不是我们想要的,还是自己编写双缓冲代码,加个画布。代码修改如下:
import sys
from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtGui import QPainter, QPixmap
from PyQt5.QtWidgets import QApplication, QWidget
class Demo(QWidget):
def __init__(self):
super(Demo, self).__init__()
self.resize(600, 600)
self.begin_point = QPoint()
self.end_point = QPoint()
self.pix = QPixmap(600, 600) # 1
self.pix.fill(Qt.white)
def paintEvent(self, QPaintEvent): # 2
painter = QPainter(self.pix)
painter.drawLine(self.begin_point, self.end_point)
self.begin_point = self.end_point
painter2 = QPainter(self)
painter2.drawPixmap(0, 0, self.pix)
def mousePressEvent(self, QMouseEvent):
if QMouseEvent.button() == Qt.LeftButton:
self.begin_point = QMouseEvent.pos()
self.end_point = self.begin_point
self.update()
def mouseMoveEvent(self, QMouseEvent):
if QMouseEvent.buttons() == Qt.LeftButton:
self.end_point = QMouseEvent.pos()
self.update()
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
-
实例化一个QPixmap()类作为画布,大小跟窗口一样。再调用fill()放法传入Qt.white让画布变白。
-
在paintEvent()事件函数中,我们先实例化一个以self.pix为绘画设备的QPainter实例,在这个画布上先画出自己想要的图案。再实例化一个以窗口为绘画设备的QPainter实例,然后调用drawPixmap()方法将self.pix画布一次性画在窗口(屏幕)上。
运行后我们发现已经可以正常作画了:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zKy73HoF-1641378822841)(data:image/svg+xml;utf8, )]
造成这样的原因是随着鼠标不断移动,self.update()不断被调用,也就是说paintEvent()事件函数在画布上不断的绘制矩形并被保留下来,接着再在窗口上被显示出来(这种代码逻辑对画线来说并没有什么影响),所以我们需要修改下代码,让程序能够在鼠标释放时只画出一个矩形来。
31.3 解决重影问题
解决思路就是:我们在鼠标移动时,让矩形直接绘制在窗口中(因为self.update()调用会清除窗口内容,可消除没确定好的矩形),而在鼠标释放时,才将我们确定好的矩形画在画布上。代码如下:
import sys
from PyQt5.QtCore import Qt, QPoint, QRect
from PyQt5.QtGui import QPainter, QPixmap
from PyQt5.QtWidgets import QApplication, QWidget
class Demo(QWidget):
def __init__(self):
super(Demo, self).__init__()
self.resize(600, 600)
self.begin_point = QPoint()
self.end_point = QPoint()
self.pix = QPixmap(600, 600)
self.pix.fill(Qt.white)
def paintEvent(self, QPaintEvent):
painter = QPainter(self) # 2
painter.drawPixmap(0, 0, self.pix)
if self.begin_point and self.end_point: # 3
rect = QRect(self.begin_point, self.end_point)
painter.drawRect(rect)
def mousePressEvent(self, QMouseEvent):
if QMouseEvent.button() == Qt.LeftButton:
self.begin_point = QMouseEvent.pos()
self.end_point = self.begin_point
self.update()
def mouseMoveEvent(self, QMouseEvent):
if QMouseEvent.buttons() == Qt.LeftButton:
self.end_point = QMouseEvent.pos()
self.update()
def mouseReleaseEvent(self, QMouseEvent): # 1
if QMouseEvent.button() == Qt.LeftButton:
painter = QPainter(self.pix)
rect = QRect(self.begin_point, self.end_point)
painter.drawRect(rect)
self.begin_point = self.end_point = QPoint()
self.update()
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
-
在mouseReleaseEvent()鼠标释放事件函数中,我们实例化一个以画布self.pix为绘图设备的QPainter对象,再将矩形绘制出来,此时鼠标已经释放,坐标都已经确定,所以只可能有一个矩形被绘制在画布上。释放后要设置self.begin_point和self.end_point变量为原点坐标(即设置为空)。
-
在paintEvent()事件函数中,实例化以窗口为绘图设备的QPainter对象,再调用drawPixmap()方法将画布呈现在窗口上;
-
首先要判断self.begin_point和self.end_point是否存在(即判断坐标是否都是(0, 0)),没有这句的话我们刚开始运行程序时会发现左上角被绘制了一个点。接着调用drawRect()方法绘制矩形就可以了(注:这里是直接绘制在窗口上)。
运行截图如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gwfZzjEg-1641378822842)(data:image/svg+xml;utf8, )]
代码现修改如下,只需要实例化一个按钮并设置图标然后完成布局即可:
图片下载地址,printer.png: https://www.easyicon.net/download/png/40007/24/
import sys
from PyQt5.QtCore import Qt, QPoint, QRect
from PyQt5.QtGui import QPainter, QPixmap, QIcon
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QHBoxLayout, QVBoxLayout
class Demo(QWidget):
def __init__(self):
super(Demo, self).__init__()
self.resize(600, 600)
self.begin_point = QPoint()
self.end_point = QPoint()
self.pix = QPixmap(600, 600)
self.pix.fill(Qt.white)
self.print_btn = QPushButton(self) # 1
self.print_btn.setIcon(QIcon('printer.png'))
self.h_layout = QHBoxLayout()
self.v_layout = QVBoxLayout()
self.h_layout.addWidget(self.print_btn)
self.h_layout.addStretch(1)
self.v_layout.addLayout(self.h_layout)
self.v_layout.addStretch(1)
self.setLayout(self.v_layout)
def paintEvent(self, QPaintEvent):
painter = QPainter(self)
painter.drawPixmap(0, 0, self.pix)
if self.begin_point and self.end_point:
rect = QRect(self.begin_point, self.end_point)
painter.drawRect(rect)
def mousePressEvent(self, QMouseEvent):
if QMouseEvent.button() == Qt.LeftButton:
self.begin_point = QMouseEvent.pos()
self.end_point = self.begin_point
self.update()
def mouseMoveEvent(self, QMouseEvent):
if QMouseEvent.buttons() == Qt.LeftButton:
self.end_point = QMouseEvent.pos()
self.update()
def mouseReleaseEvent(self, QMouseEvent):
if QMouseEvent.button() == Qt.LeftButton:
painter = QPainter(self.pix)
rect = QRect(self.begin_point, self.end_point)
painter.drawRect(rect)
self.begin_point = self.end_point = QPoint()
self.update()
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
接着我们要在类初始化函数中实例化一个用于打印的QPrinter对象,并将self.printer_btn按钮的信号和槽函数连接起来,分别是下面#1和#2处代码:
import sys
from PyQt5.QtCore import Qt, QPoint, QRect
from PyQt5.QtGui import QPainter, QPixmap, QIcon
from PyQt5.QtPrintSupport import QPrintDialog, QPrinter
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QHBoxLayout, QVBoxLayout
class Demo(QWidget):
def __init__(self):
super(Demo, self).__init__()
self.resize(600, 600)
self.begin_point = QPoint()
self.end_point = QPoint()
self.pix = QPixmap(600, 600)
self.pix.fill(Qt.white)
self.printer = QPrinter() # 1
self.print_btn = QPushButton(self)
self.print_btn.setIcon(QIcon('printer.png'))
self.print_btn.clicked.connect(self.open_printer_func) # 2
self.h_layout = QHBoxLayout()
self.v_layout = QVBoxLayout()
self.h_layout.addWidget(self.print_btn)
self.h_layout.addStretch(1)
self.v_layout.addLayout(self.h_layout)
self.v_layout.addStretch(1)
self.setLayout(self.v_layout)
槽函数实现如下:
def open_printer_func(self):
printer_dialog = QPrintDialog(self.print_btn)
if printer_dialog.exec_():
painter = QPainter(self.printer)
painter.drawPixmap(0, 0, self.pix)
首先传入self.printer实例化一个QPrintDialog打印设置对话框, 调用exec_()使之成为模态对话框,若成功打开,则传入self.printer作为绘制设备用于实例化一个QPainter对象,接着将要打印的内容绘制到self.printer上。
此时运行代码,我们先在窗口中随便画点什么,再点击打印按钮,显示如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uoOwjZGF-1641378822843)(data:image/svg+xml;utf8, )]
此时再点击打印就可以了,当然打印前我们可以先预览下:
上述完整代码如下:
import sys
from PyQt5.QtCore import Qt, QPoint, QRect
from PyQt5.QtGui import QPainter, QPixmap, QIcon
from PyQt5.QtPrintSupport import QPrintDialog, QPrinter
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QHBoxLayout, QVBoxLayout
class Demo(QWidget):
def __init__(self):
super(Demo, self).__init__()
self.resize(600, 600)
self.begin_point = QPoint()
self.end_point = QPoint()
self.pix = QPixmap(600, 600)
self.pix.fill(Qt.white)
self.printer = QPrinter()
self.print_btn = QPushButton(self)
self.print_btn.setIcon(QIcon('printer.png'))
self.print_btn.clicked.connect(self.open_printer_func)
self.h_layout = QHBoxLayout()
self.v_layout = QVBoxLayout()
self.h_layout.addWidget(self.print_btn)
self.h_layout.addStretch(1)
self.v_layout.addLayout(self.h_layout)
self.v_layout.addStretch(1)
self.setLayout(self.v_layout)
def open_printer_func(self):
printer_dialog = QPrintDialog(self.printer)
if printer_dialog.exec_():
painter = QPainter(self.printer)
painter.drawPixmap(0, 0, self.pix)
def paintEvent(self, QPaintEvent):
painter = QPainter(self)
painter.drawPixmap(0, 0, self.pix)
if self.begin_point and self.end_point:
rect = QRect(self.begin_point, self.end_point)
painter.drawRect(rect)
def mousePressEvent(self, QMouseEvent):
if QMouseEvent.button() == Qt.LeftButton:
self.begin_point = QMouseEvent.pos()
self.end_point = self.begin_point
self.update()
def mouseMoveEvent(self, QMouseEvent):
if QMouseEvent.buttons() == Qt.LeftButton:
self.end_point = QMouseEvent.pos()
self.update()
def mouseReleaseEvent(self, QMouseEvent):
if QMouseEvent.button() == Qt.LeftButton:
painter = QPainter(self.pix)
rect = QRect(self.begin_point, self.end_point)
painter.drawRect(rect)
self.begin_point = self.end_point = QPoint()
self.update()
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
有些控件自带print()方法,直接传入QPrinter对象即可,所以我们不需要再配合QPainter类先进行绘制。下面有个实例可以看下:
import sys
from PyQt5.QtPrintSupport import QPrintDialog, QPrinter
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QTextEdit, QVBoxLayout
class Demo(QWidget):
def __init__(self):
super(Demo, self).__init__()
self.text_edit = QTextEdit(self)
self.print_btn = QPushButton('Print', self)
self.print_btn.clicked.connect(self.open_printer_func)
self.printer = QPrinter()
self.v_layout = QVBoxLayout()
self.v_layout.addWidget(self.text_edit)
self.v_layout.addWidget(self.print_btn)
self.setLayout(self.v_layout)
def open_printer_func(self):
printer_dialog = QPrintDialog(self.printer)
if printer_dialog.exec_():
self.text_edit.print(self.printer)
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
运行截图如下:
31.5 小结
-
Qt在绘画方面的知识点远不止笔者上面讲的那些,更多的需要读者们自己去探索,多查阅文档。读者们可以自己来编写一个画图工具,做到切换画笔样式、粗细和颜色,也可以用来绘制各种形状;
-
注意self.update()的用法,其实类似的还有一个repaint(),读者可以去文档中看下两者的区别;
-
解决重影其实就是在鼠标点击和移动过程中将矩形绘制在窗口上,但是释放后会绘制到画布,并且再将画布绘制到窗口(屏幕)上,所以还是运用到了双缓冲技术的;
-
用没有带print()方法的控件时,我们可以将QPainter和QPrinter结合起来完成打印功能;对于带print()方法的控件,我们直接调用并传入QPrinter对象即可实现打印功能。