PyQt5教程-Martin 自定义组件


自定义组件的第一步,需要理解PyQt5中的bitmap(pixel-based)graphic operations,因为所有的组件其实都是利用这个东西画出来的。

QPainter和Bitmap Graphics

bitmap是pixels的正方形区域,也就是我们常说的位图。

1. QPainter和Bitmap

QPainter用来实现Qt中的bitmap drawing operations。其接受一个QPixmap对象作为参数,可以在这个QPixmap进行绘制。

代码示例:

# py example1.py
import sys
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt5.QtCore import Qt


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()

        # 创建一个label,并将其放到整个窗口中
        self.label = QtWidgets.QLabel()
        self.setCentralWidget(self.label)
        # 创建一个画布,并将这个画布放到label上
        canvas = QtGui.QPixmap(400, 300)
        # 原教程中没有介绍,新创建的Pixmap对象是null的,需要先给底色,可以使用fill(QColor(.))来完成,其默认是白色
        canvas.fill()
        self.label.setPixmap(canvas)
        # 在画布上画一些东西
        self.draw_something()

    def draw_something(self):
        # 对一个QPixmap对象操作,需要使用QPainter
        painter = QtGui.QPainter(self.label.pixmap())
        painter.drawLine(10, 10, 300, 200)
        painter.end()


app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()

结果:


1.1 绘制用的方法

QPainter提供大量的方法来对bitmap进行操作,但幸运的是这些方法大多数是一些overloaded methods(即相同方法,适用于不同的输入)。

比如下面的5种绘制线条的方法:


QLineQLineF使用QLine(x1, y1, x2, y2)QLine(p1, p2)的方式定义,QPoint对象使用QPoint(x1, y1)的方式来定义,所以实际上这些是相同的方法。

QLineF是指坐标可以有float来指定。

去除这些重复的方法,则绘制方法可以简单的总结为下面的几个:

drawArc , drawChord, drawConvexPolygon, drawEllipse,drawLine, drawPath, drawPie, drawPoint, drawPolygon, drawPolyline, drawRect, drawRects drawRoundedRect.

1.2 drawPoint

我们改变drawsomething方法:

def draw_something(self):
    painter = QtGui.QPainter(self.label.pixmap())
    painter.drawPoint(200, 150)
    painter.end()

得到下面的结果:


可能当中的点不太容易看出来。

我们可以来更改绘制点的形状、颜色和大小,方法是使用QPen对象。

将这个过程想成你正在用ipad的手写笔。一次只能用一支笔来写。

我们进行如下修改:

def draw_something(self):
    painter = QtGui.QPainter(self.label.pixmap())
    pen = QtGui.QPen()
    pen.setWidth(40)
    pen.setColor(QtGui.QColor('red'))
    painter.setPen(pen)
    painter.drawPoint(200, 150)
    painter.end()

结果:


现在,我们将相当于使用程序控制一个电子笔进行绘制,比如:

def draw_something(self):
    from random import randint
    painter = QtGui.QPainter(self.label.pixmap())
    pen = QtGui.QPen()
    pen.setWidth(3)
    painter.setPen(pen)

    for n in range(10000):
        painter.drawPoint(
            200+randint(-100, 100),  # x
            150+randint(-100, 100)   # y
            )
    painter.end()


当然,也可以在绘制的图中更换“笔”的状态:

def draw_something(self):
    from random import randint, choice
    colors = ['#FFD141', '#376F9F', '#0D1F2D', '#E9EBEF', '#EB5160']

    painter = QtGui.QPainter(self.label.pixmap())
    pen = QtGui.QPen()
    pen.setWidth(3)
    painter.setPen(pen)

    for n in range(10000):
        # pen = painter.pen() you could get the active pen here
        pen.setColor(QtGui.QColor(choice(colors)))
        painter.setPen(pen)
        painter.drawPoint(
            200+randint(-100, 100),  # x
            150+randint(-100, 100)   # y
            )
    painter.end()

结果:


1.3 drawLine

和上面的一样,只是换成了线段而已

def draw_something(self):
    from random import randint
    painter = QtGui.QPainter(self.label.pixmap())
    pen = QtGui.QPen()
    pen.setWidth(15)
    pen.setColor(QtGui.QColor('blue'))
    painter.setPen(pen)
    painter.drawLine(
        QtCore.QPoint(100, 100),
        QtCore.QPoint(300, 200)
    )
    painter.end()

结果:


1.4 drawRect、drawRects、drawRoundedRect

分别是绘制一个矩形,多个矩形和圆角矩形。

和线段类似,使用4个坐标点或者使用QRectQRectF都可以。

def draw_something(self):
    from random import randint
    painter = QtGui.QPainter(self.label.pixmap())
    pen = QtGui.QPen()
    pen.setWidth(3)
    pen.setColor(QtGui.QColor("#EB5160"))
    painter.setPen(pen)
    painter.drawRect(50, 50, 100, 100)
    painter.drawRect(60, 60, 150, 100)
    painter.drawRect(70, 70, 100, 150)
    painter.drawRect(80, 80, 150, 100)
    painter.drawRect(90, 90, 100, 150)
    painter.end()

结果:


也可以使用drawRects完成:

painter.drawRects(
    QtCore.QRect(50, 50, 100, 100),
    QtCore.QRect(60, 60, 150, 100),
    QtCore.QRect(70, 70, 100, 150),
    QtCore.QRect(80, 80, 150, 100),
    QtCore.QRect(90, 90, 100, 150),
)

当然,对于几何图形来说,现在就有一个另一个问题,即填充的问题。这个需要使用另一种“画笔”--QBrush。指定此刷子后,通过赋予粉刷的范围(这里就是我们指定的QRect对象),其在其上赋予指定的style效果。

比如使用下面的代码:

def draw_something(self):
    from random import randint
    painter = QtGui.QPainter(self.label.pixmap())
    pen = QtGui.QPen()
    pen.setWidth(3)
    pen.setColor(QtGui.QColor("#376F9F"))
    painter.setPen(pen)

    brush = QtGui.QBrush()
    brush.setColor(QtGui.QColor("#FFD141"))
    brush.setStyle(Qt.DenselPattern)
    painter.setBrush(brush)

    painter.drawRects(
        QtCore.QRect(50, 50, 100, 100),
        QtCore.QRect(60, 60, 150, 100),
        QtCore.QRect(70, 70, 100, 150),
        QtCore.QRect(80, 80, 150, 100),
        QtCore.QRect(90, 90, 100, 150),
    )
    painter.end()

结果:


另外关于圆角矩形:

def draw_something(self):
    from random import randint
    painter = QtGui.QPainter(self.label.pixmap())
    pen = QtGui.QPen()
    pen.setWidth(3)
    pen.setColor(QtGui.QColor("#376F9F"))
    painter.setPen(pen)
    painter.drawRoundedRect(40, 40, 100, 100, 10, 10)
    painter.drawRoundedRect(80, 80, 100, 100, 10, 50)
    painter.drawRoundedRect(120, 120, 100, 100, 50, 10)
    painter.drawRoundedRect(160, 160, 100, 100, 50, 50)
    painter.end()

其和矩形基本一致,主要就是需要提供额外的2个参数,指定角上的(长短)半径。


1.5 绘制椭圆

def draw_something(self):
    from random import randint
    painter = QtGui.QPainter(self.label.pixmap())
    pen = QtGui.QPen()
    pen.setWidth(3)
    pen.setColor(QtGui.QColor(204,0,0))  # r, g, b
    painter.setPen(pen)

    painter.drawEllipse(10, 10, 100, 100)
    painter.drawEllipse(10, 10, 150, 200)
    painter.drawEllipse(10, 10, 200, 300)
    painter.end()


1.6 文本

def draw_something(self):
    from random import randint
    painter = QtGui.QPainter(self.label.pixmap())

    pen = QtGui.QPen()
    pen.setWidth(1)
    pen.setColor(QtGui.QColor('green'))
    painter.setPen(pen)

    font = QtGui.QFont()
    font.setFamily('Times')
    font.setBold(True)
    font.setPointSize(40)
    painter.setFont(font)

    painter.drawText(100, 100, 'Hello, world!')
    painter.end()


还可以指定显示文本的框的大小,实现截断的效果:

painter.drawText(100, 100, 100, 100, Qt.AlignHCenter, 'Hello, world!')


2. 自己的画板软件

根据上面的知识,我们就可以指定自己的画板软件了:

# py example2.py
import sys
import random
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt


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 QPaletteButton(QtWidgets.QPushButton):
    """
    一个自定义按钮,背景颜色代表其储存的颜色类别,
    """
    def __init__(self, color):
        super().__init__()
        self.setFixedSize(QtCore.QSize(24, 24))
        self.color = color
        self.setStyleSheet("background-color: %s" % color)
        # self.setCheckable(True)  # 有按下的效果


class Canvas(QtWidgets.QLabel):
    """
    自定义画布,实际上就是一个带有pixmap的label。
    可以侦测鼠标的移动从而进行绘制
    储存有当前画笔的颜色,改变当前画笔的颜色使用set_pen_color方法
    """
    def __init__(self):
        super().__init__()
        pixmap = QtGui.QPixmap(600, 300)
        pixmap.fill()
        self.setPixmap(pixmap)

        self.last_x, self.last_y = None, None
        self.pen_color = QtGui.QColor("#000000")
        self.pen_type = 'pen'

    def set_pen_color(self, c):
        self.pen_color = QtGui.QColor(c)

    def mouseMoveEvent(self, e):
        painter = QtGui.QPainter(self.pixmap())
        p = painter.pen()
        p.setColor(self.pen_color)
        if self.pen_type == "pen":
            if self.last_x is None:  # First event.
                # 当我们按下鼠标的一刻,会触发该方法,此时last_x和last_y都是None
                #   所以先记录一下当前的鼠标位置
                self.last_x = e.x()
                self.last_y = e.y()
                return  # Ignore the first time.

            # 当我们继续移动鼠标,技术触发该方法,此时,会执行下面的内容绘制线条并
            #   更新last_x和last_y
            p.setWidth(4)
            painter.setPen(p)
            # 这里使用line,因为如果是point的话,绘制的速度会跟不上鼠标移动的速度,
            #   从而出现断开的状态。如果是line的话就没有这个问题,但线条会比较毛糙。
            painter.drawLine(self.last_x, self.last_y, e.x(), e.y())
            painter.end()
            # 绘制图像的过程发生在show之后,所以如果想要把绘制的内容打印到屏幕上,
            #   需要使用update方法
            self.update()

            # Update the origin for next time.
            self.last_x = e.x()
            self.last_y = e.y()
        elif self.pen_type == "spray":
            p.setWidth(1)
            painter.setPen(p)
            for n in range(100):
                xo = random.gauss(0, 10)
                yo = random.gauss(0, 10)
                painter.drawPoint(e.x() + xo, e.y() + yo)

            self.update()

    def mouseReleaseEvent(self, e):
        # 当我们松开鼠标的时候,将last_x和last_y抹除
        self.last_x = None
        self.last_y = None


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)


app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()

可以通过改变pen_type来实现不同的画笔效果:

铅笔画:


油画:


自定义GUI组件

未完待续...


文章作者: Luyiyun
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Luyiyun !
评论
评论
  目录