上接 Qt5 绘制图形
1 图像类
在 PyQt5/PySide2 中常用的图像类有 4 个,即 QPixmap、QImage、QPicture 和 QBitmap:
- QPixmap 是专门为绘图而设计的,在绘制图片时需要使用 QPixmap。
- QImage 提供了一个与硬件无关的图像表示函数,可以用于图片的像素级访问。
- QPicture 是一个绘图设备类,它继承自 QPainter 类。可以使用 QPainter 的
begin()
函数在 QPicture 上绘图,使用end()
函数结束绘图,使用 QPicture 的save()
函数将 QPainter 所使用过的绘图指令保存到文件中。 - QBitmap 是一个继承自 QPixmap 的简单类,它提供了 1 bit 深度的二值图像的类。QBitmap 提供的单色图像,可以用来制作游标(QCursor)或者笔刷(QBrush)。
1.1 QPixmap
QPixmap 类用于绘图设备的图像显示,它可以作为一个 QPaintDevice 对象,也可以加载到一个控件中,通常是标签或按钮,用于在标签或按钮上显示图像。
QPixmap 可以读取的图像文件类型有 BMP、GIF、JPG、JPEG、PNG、PBM、PGM、PPM、XBM、XPM等。
QPixmap 类中的常用方法:
from xinet.Qt.qt5 import QtCore, QtGui, QtWidgets
from xinet.run_qt import run
class Window(QtWidgets.QWidget):
def __init__(self, image_path):
super().__init__()
lab1 = QtWidgets.QLabel()
lab1.setPixmap(QtGui.QPixmap(image_path))
lab1.setScaledContents(True) # 让图片自适应 label 大小
vbox = QtWidgets.QVBoxLayout()
vbox.addWidget(lab1)
self.setLayout(vbox)
self.setWindowTitle("QPixmap 例子")
# lbl.setPixmap(QPixmap("")) #移除label上的图片
if __name__ == '__main__':
image_path = r'D:\share\python.jpg'
run(Window, image_path)
效果:
等比例缩放(这块暂时没有研究,下面是别人的代码):
class LabelFrame(QLabel):
def __init__(self, parent=None):
super().__init__()
self.main_window = parent
self.setAlignment(Qt.AlignCenter) # 居中显示
self.setMinimumSize(640, 480)
# self.setScaledContents(True)
def update_frame(self, frame):
if self.height() > self.width():
width = self.width()
height = int(frame.shape[0] * (width / frame.shape[1]))
else:
height = self.height()
width = int(frame.shape[1] * (height / frame.shape[0]))
frame = cv2.resize(frame, (width, height))
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # bgr -> rgb
h, w, c = frame.shape # 获取图片形状
image = QImage(frame, w, h, 3 * w, QImage.Format_RGB888)
pix_map = QPixmap.fromImage(image)
self.setPixmap(pix_map)
1.2 鼠标绘图
下面的代码重写了 QtWidgets.QMainWindow 的鼠标左键移动事件,令鼠标移动画出点:
from xinet.Qt.qt5 import QtCore, QtGui, QtWidgets
from xinet.run_qt import run
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.label = QtWidgets.QLabel()
canvas = QtGui.QPixmap(800, 800)
self.label.setPixmap(canvas)
self.setCentralWidget(self.label)
def mouseMoveEvent(self, e):
painter = QtGui.QPainter(self.label.pixmap())
painter.setPen(QtCore.Qt.green)
painter.drawPoint(e.x(), e.y())
painter.end()
self.update()
if __name__ == '__main__':
run(MainWindow)
效果:
虽然,一定程度上实现了鼠标绘图,但是,鼠标移动的速度很快的话,点之间的间隔会很大。为此,需要使用 line 代替 point 来记录鼠标移动的轨迹。利用变量 self.last_x
, self.last_y
记录鼠标的最新位置,且鼠标释放释放变量:
from xinet.Qt.qt5 import QtCore, QtGui, QtWidgets
from xinet.run_qt import run
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.label = QtWidgets.QLabel()
canvas = QtGui.QPixmap(800, 800)
self.label.setPixmap(canvas)
self.setCentralWidget(self.label)
self.last_x, self.last_y = None, None
def mouseMoveEvent(self, e):
if self.last_x is None: # First event.
self.last_x = e.x()
self.last_y = e.y()
return # Ignore the first time.
painter = QtGui.QPainter(self.label.pixmap())
painter.setPen(QtCore.Qt.green)
painter.drawLine(self.last_x, self.last_y, e.x(), e.y())
painter.end()
self.update()
# Update the origin for next time.
self.last_x = e.x()
self.last_y = e.y()
def mouseReleaseEvent(self, e):
self.last_x = None
self.last_y = None
if __name__ == '__main__':
run(MainWindow)
效果:
效果还是不错的。但该代码存在冗余,我们借助 QPoint 进一步简化代码(为了让涂鸦的线更平滑,加入了反走样):
from xinet.Qt.qt5 import QtCore, QtGui, QtWidgets
from xinet.run_qt import run
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.label = QtWidgets.QLabel() # 设置占位符
canvas = QtGui.QPixmap(800, 800)
canvas.fill(QtGui.QColor(200, 200, 20, 50)) # 设置画布的背景
#self.label.setScaledContents(True) # 让画布自适应 label 大小
self.label.setPixmap(canvas)
self.setCentralWidget(self.label)
self.lastPoint = QtCore.QPoint()
def draw_something(self, event):
painter = QtGui.QPainter(self.label.pixmap())
painter.setPen(QtCore.Qt.green)
painter.setRenderHint(QtGui.QPainter.Antialiasing) # 设定反走样
painter.drawLine(self.lastPoint, event.pos())
painter.end()
self.update()
def mouseMoveEvent(self, event):
'''重写鼠标移动事件'''
if self.lastPoint.isNull():
self.lastPoint = event.pos()
else:
self.draw_something(event)
# Update the origin for next time.
self.lastPoint = event.pos()
def mouseReleaseEvent(self, e):
self.lastPoint = QtCore.QPoint()
if __name__ == '__main__':
run(MainWindow)
画笔的颜色只有一种,为此,可以添加一个调色板(palette):
from xinet.Qt.qt5 import QtCore, QtGui, QtWidgets
from xinet.run_qt import run
COLORS = [
# 17 undertones https://lospec.com/palette-list/17undertones
'#000000', '#141923', '#414168', '#3a7fa7', '#35e3e3', '#8fd970', '#5ebb49',
'#458352', '#dcd37b', '#fffee5', '#ffd035', '#cc9245', '#a15c3e', '#a42f3b',
'#f45b7a', '#c24998', '#81588d', '#bcb0c2', '#ffffff',
]
class Canvas(QtWidgets.QLabel): # 设置占位符
def __init__(self):
super().__init__()
pixmap = QtGui.QPixmap(800, 600)
# 设置画布背景
pixmap.fill(QtGui.QColor(200, 200, 20, 50))
self.setPixmap(pixmap)
self.lastPoint = QtCore.QPoint()
self.pen_color = QtGui.QColor('white')
def set_pen_color(self, c):
self.pen_color = QtGui.QColor(c)
def draw_something(self, event):
painter = QtGui.QPainter(self.pixmap())
p = painter.pen()
p.setWidth(2)
p.setColor(self.pen_color)
painter.setPen(p)
painter.setRenderHint(QtGui.QPainter.Antialiasing) # 设定反走样
painter.drawLine(self.lastPoint, event.pos())
painter.end()
self.update()
def mouseMoveEvent(self, event):
'''重写鼠标移动事件'''
if self.lastPoint.isNull():
self.lastPoint = event.pos()
else:
self.draw_something(event)
# Update the origin for next time.
self.lastPoint = event.pos()
def mouseReleaseEvent(self, e):
self.lastPoint = QtCore.QPoint()
class QPaletteButton(QtWidgets.QPushButton):
def __init__(self, color):
super().__init__()
self.setFixedSize(QtCore.QSize(24, 24))
self.color = color
self.setStyleSheet(f"background-color: {color};")
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.canvas = Canvas()
w = QtWidgets.QWidget()
l = QtWidgets.QVBoxLayout()
w.setLayout(l)
l.addWidget(self.canvas)
palette = QtWidgets.QHBoxLayout()
self.add_palette_buttons(palette)
l.addLayout(palette)
self.setCentralWidget(w)
def add_palette_buttons(self, layout):
for c in COLORS:
b = QPaletteButton(c)
b.pressed.connect(lambda c=c: self.canvas.set_pen_color(c))
layout.addWidget(b)
if __name__ == "__main__":
run(MainWindow)
效果:
也可以使用 QWidget
的方式实现:按下鼠标左键在白色画布上进行绘制,实现了简单的涂鸦板功能。
from xinet.Qt.qt5 import QtCore, QtGui, QtWidgets
from xinet.run_qt import run
class MainWindow(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("绘图例子")
self.lastPoint = QtCore.QPoint()
self.endPoint = QtCore.QPoint()
self.initUi()
def initUi(self):
#窗口大小设置为600*500
self.resize(600, 500)
# 画布大小为400*400,背景为白色
self.pix = QtGui.QPixmap(400, 400)
self.pix.fill(QtCore.Qt.white)
def paintEvent(self, event):
pp = QtGui.QPainter(self.pix)
# 根据鼠标指针前后两个位置绘制直线
pp.drawLine(self.lastPoint, self.endPoint)
# 让前一个坐标值等于后一个坐标值,
# 这样就能实现画出连续的线
self.lastPoint = self.endPoint
painter = QtGui.QPainter(self)
painter.drawPixmap(0, 0, self.pix)
def mousePressEvent(self, event):
# 鼠标左键按下
if event.button() == QtCore.Qt.LeftButton:
self.lastPoint = event.pos()
self.endPoint = self.lastPoint
def mouseMoveEvent(self, event):
# 鼠标左键按下的同时移动鼠标
if event.buttons() and QtCore.Qt.LeftButton:
self.endPoint = event.pos()
#进行重新绘制
self.update()
def mouseReleaseEvent(self, event):
# 鼠标左键释放
if event.button() == QtCore.Qt.LeftButton:
self.endPoint = event.pos()
#进行重新绘制
self.update()
if __name__ == '__main__':
run(MainWindow)
效果如下:
2 QPrinter
打印图像是图像处理软件中的一个常用功能。打印图像实际上是在 QPaintDevice 中画图,与平常在QWidget、QPixmap 和 QImage 中画图一样,都是创建一个 QPainter 对象进行画图的,只是打印使用的是 QPrinter,它本质上也是一个 QPaintDevice(绘图设备)。
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QImage, QIcon, QPixmap
from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QSizePolicy, QAction
from PyQt5.QtPrintSupport import QPrinter, QPrintDialog
import sys
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle(self.tr("打印图片"))
# 创建一个放置图像的QLabel对象imageLabel,并将该QLabel对象设置为中心窗体。
self.imageLabel = QLabel()
self.imageLabel.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
self.setCentralWidget(self.imageLabel)
self.image = QImage()
# 创建菜单,工具条等部件
self.createActions()
self.createMenus()
self.createToolBars()
# 在imageLabel对象中放置图像
if self.image.load("./images/screen.png"):
self.imageLabel.setPixmap(QPixmap.fromImage(self.image))
self.resize(self.image.width(), self.image.height())
def createActions(self):
self.PrintAction = QAction(
QIcon("./images/printer.png"), self.tr("打印"), self)
self.PrintAction.setShortcut("Ctrl+P")
self.PrintAction.setStatusTip(self.tr("打印"))
self.PrintAction.triggered.connect(self.slotPrint)
def createMenus(self):
PrintMenu = self.menuBar().addMenu(self.tr("打印"))
PrintMenu.addAction(self.PrintAction)
def createToolBars(self):
fileToolBar = self.addToolBar("Print")
fileToolBar.addAction(self.PrintAction)
def slotPrint(self):
'''
判断打印对话框显示后用户是否单击“打印”按钮,若单击“打印”按钮,
则相关打印属性可以通过创建QPrintDialog对象时使用的QPrinter对象获得,
若用户单击“取消”按钮,则不执行后续的打印操作。
'''
# 新建一个QPrinter对象
printer = QPrinter()
# 创建一个QPrintDialog对象,参数为QPrinter对象
printDialog = QPrintDialog(printer, self)
if printDialog.exec_():
# 创建一个QPainter对象,并指定绘图设备为一个QPrinter对象。
painter = QPainter(printer)
# 获得QPainter对象的视口矩形
rect = painter.viewport()
# 获得图像的大小
size = self.image.size()
# 按照图形的比例大小重新设置视口矩形
size.scale(rect.size(), Qt.KeepAspectRatio)
painter.setViewport(rect.x(), rect.y(),
size.width(), size.height())
# 设置QPainter窗口大小为图像的大小
painter.setWindow(self.image.rect())
# 打印
painter.drawImage(0, 0, self.image)
if __name__ == "__main__":
app = QApplication(sys.argv)
main = MainWindow()
main.show()
sys.exit(app.exec_())
效果:
3 双缓冲绘图
本节讲解在画板上绘制矩形,还会讲解双缓冲绘图的概念。
3.1 绘制矩形,出现重影
演示绘制矩形的功能。其完整代码如下:
from xinet.Qt.qt5 import QtCore, QtGui, QtWidgets
from xinet.run_qt import run
class MainWindow(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("绘制矩形图形例子")
self.lastPoint = QtCore.QPoint()
self.endPoint = QtCore.QPoint()
self.initUi()
def initUi(self):
# 窗口大小设置为600*500
#self.resize(600, 500)
# 画布大小为 400*400,背景为白色
self.pix = QtGui.QPixmap(400, 400)
self.pix.fill(QtGui.QColor('white'))
def paintEvent(self, event):
painter = QtGui.QPainter(self)
x = self.lastPoint.x()
y = self.lastPoint.y()
w = self.endPoint.x() - x
h = self.endPoint.y() - y
pp = QtGui.QPainter(self.pix)
pp.drawRect(x, y, w, h)
painter.drawPixmap(0, 0, self.pix)
def mousePressEvent(self, event):
# 鼠标左键按下
if event.button() == QtCore.Qt.LeftButton:
self.lastPoint = event.pos()
self.endPoint = self.lastPoint
def mouseMoveEvent(self, event):
# 鼠标左键按下的同时移动鼠标
if event.buttons() and QtCore.Qt.LeftButton:
self.endPoint = event.pos()
# 进行重新绘制
self.update()
def mouseReleaseEvent(self, event):
# 鼠标左键释放
if event.button() == QtCore.Qt.LeftButton:
self.endPoint = event.pos()
# 进行重新绘制
self.update()
if __name__ == "__main__":
run(MainWindow)
效果如下:
可以尝试分别快速和慢速拖动鼠标来绘制矩形,结果发现,拖动速度越快,重影越少。其实,在拖动鼠标的过程中,屏幕已经刷新了很多次,也可以理解为 paintEvent()
函数执行了多次,每执行一次就会绘制一个矩形。知道了原因,就可以想办法来避免出现重影了。
3.2 使用双缓冲技术绘制矩形,避免出现重影
演示使用双缓冲技术绘制矩形,避免出现重影。其完整代码如下:
from xinet.Qt.qt5 import QtCore, QtGui, QtWidgets
from xinet.run_qt import run
class MainWindow(QtWidgets.QWidget):
def __init__(self, *args, **kw):
super().__init__(*args, **kw)
self.setWindowTitle("双缓冲绘图例子")
self.lastPoint = QtCore.QPoint()
self.endPoint = QtCore.QPoint()
# 辅助画布
self.tempPix = QtGui.QPixmap()
# 标志是否正在绘图
self.isDrawing = False
self.initUi()
def initUi(self):
# 窗口大小设置为600*500
self.resize(600, 500)
# 画布大小为400*400,背景为白色
self.pix = QtGui.QPixmap(400, 400)
self.pix.fill(QtCore.Qt.white)
def paintEvent(self, event):
painter = QtGui.QPainter(self)
x = self.lastPoint.x()
y = self.lastPoint.y()
w = self.endPoint.x() - x
h = self.endPoint.y() - y
# 如果正在绘图,就在辅助画布上绘制
if self.isDrawing:
# 将以前pix中的内容复制到tempPix中,保证以前的内容不消失
self.tempPix = self.pix
pp = QtGui.QPainter(self.tempPix)
pp.drawRect(x, y, w, h)
painter.drawPixmap(0, 0, self.tempPix)
else:
pp = QtGui.QPainter(self.pix)
pp.drawRect(x, y, w, h)
painter.drawPixmap(0, 0, self.pix)
def mousePressEvent(self, event):
# 鼠标左键按下
if event.button() == QtCore.Qt.LeftButton:
self.lastPoint = event.pos()
self.endPoint = self.lastPoint
self.isDrawing = True
def mouseReleaseEvent(self, event):
# 鼠标左键释放
if event.button() == QtCore.Qt.LeftButton:
self.endPoint = event.pos()
# 进行重新绘制
self.update()
self.isDrawing = False
if __name__ == "__main__":
run(MainWindow)
效果如下:
在这个例子中,按下鼠标左键时标志正在绘图,当释放鼠标左键时则取消正在绘图的标志。运行程序,绘图正常,没有重影。
在这个例子中,需要添加一个辅助画布,如果正在绘图,也就是还没有释放鼠标左键时,就在这个辅助画布上进行;只有释放鼠标左键时,才在真正的画布上绘图。
双缓冲技术总结:
在这个例子中,要实现使用鼠标在界面上绘制一个任意大小的矩形而不出现重影,需要两个画布,它们都是 QPixmap 实例,其中 tempPix 作为临时缓冲区,当拖动鼠标绘制矩形时,将内容先绘制到 tempPix 上,然后再将 tempPix 绘制到界面上;pix 作为缓冲区,用来保存已经完成的绘制。当释放鼠标按键完成矩形的绘制后,则将 tempPix 的内容复制到 pix 上。为了在绘制时不出现重影,而且保证以前绘制的内容不消失,那么每一次绘制都是在原来的图形上进行的,所以需要在绘制 tempPix 之前,先将 pix 的内容复制到 tempPix 上。因为这里有两个 QPixmap 对象,也可以说有两个缓冲区,所以称之为“双缓冲绘图”。
4 设置图片为窗口的背景
import sys
from PyQt5.QtCore import QSize
from PyQt5.QtGui import QImage, QPalette, QBrush, QFont
from PyQt5.QtWidgets import QWidget, QApplication, QLabel
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.setGeometry(100,100,300,200)
oImage = QImage("D:/data/cat.jpg")
sImage = oImage.scaled(QSize(300,200)) # resize Image to widgets size
palette = QPalette()
palette.setBrush(QPalette.Window, QBrush(sImage))
self.setPalette(palette)
self.label = QLabel('测试', self) # test, if it's really backgroundimage
self.label.setFont(QFont("SimSun", 30, QFont.Bold))
self.show()
if __name__ == "__main__":
app = QApplication(sys.argv)
oMainwindow = MainWindow()
sys.exit(app.exec_())
效果: