Qt5 绘制图像与鼠标绘图

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

效果:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350