自定义组件的第一步,需要理解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种绘制线条的方法:
QLine
和QLineF
使用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个坐标点或者使用
QRect
、QRectF
都可以。
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组件
未完待续...