PyQt5设计圆形水位指示器(QPainter画正弦线,画圆画弧画弦;QFont字体设置;QPainterPath裁剪,QSlider,QCheckbox)

项目重难点

    • 概要
    • 成品图
    • 信号槽连接
    • paintEvent重绘
      • drawText字体设置
      • 水平指示线(poolStyle)drawChord画弦
      • 画正弦波波浪动态曲线
        • 绘制正弦曲线
        • QPainterPath.intersected()裁剪
        • 设置定时器让波浪有动画效果
    • 经验总结
    • PyQt5内置的绘图函数和效果

概要

该项目展示了若干样式的圆形水位指示器,包括外圆边框的设计,水位样式设计(水平线or波浪线),弧形进度条的旋转等。该系列样式图外观美观大方,指示清晰。其中波浪线动画的绘制是难点,需特别注意。

成品图

先上成品图

  1. 拖拉“当前进度”滑动条,水位指示器会随进度条值的变化而相应变化。
  2. 勾选“顺时针”,圆弧进度条沿顺时针方向旋转。反之沿逆时针旋转。
  3. 勾选“外边框”,圆形指示器外圈显示有边框。
  4. 拖拉“起始角度”滑动条,圆弧进度条的起始位置会有所变化。
  5. 共有“圆弧”,“水池”,“波纹”,“圆弧水池”以及“圆弧波纹”五中水位指示器样式供用户选择。

该项目重难点:

  1. 六个圆的坐标定位
  2. QPainter各种绘图函数
  3. 正弦波浪动画的绘制

信号槽连接

信号槽的连接应该放在整个窗口类中,主要的几个信号-槽函数初始化代码如下:

# 初始化信号槽
def initSignal(self)# 两个checkbox
    self.clockwiseCkBox.stateChanged.connect(self.setRotateDirection)
    self.outerFrameCkBox.stateChanged.connect(self.setOuterFrame)
    
    # 5个radiobutton
    self.arcRdBtn.toggled.connect(self.setArc)
    self.poolRdBtn.toggled.connect(self.setPool)
    self.waveRdBtn.toggled.connect(self.setWave)
    self.arcpoolRdBtn.toggled.connect(self.setArcPool)
    self.arcwaveRdBtn.toggled.connect(self.setArcWave)
   
    # 两个slider
    self.processSlider.valueChanged.connect(self.setProcess)
    self.angleSlider.valueChanged.connect(self.setStartAngle)

paintEvent重绘

painterEvent设置坐标轴,并调用绘制各部分元素的函数。

def paintEvent(self, event):

    # 1. 坐标变换
    width = self.width()
    height = self.height()
    side = min(width, height)

    # 2. 绘制准备工作, 启用反锯齿, 平移坐标轴中心, 等比例缩放
    painter = QtGui.QPainter(self)
    painter.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.TextAntialiasing)
    painter.translate(width / 2, height / 2)
    painter.scale(side / 200.0, side / 200.0)
    
    # 3. 绘制固定元素
    self.drawInnerCircle(painter)    # 绘制内圆
    
    # 4. 绘制可变元素,即勾选不同选项所应绘制的样式
    if self.arcStyle:                              # 如果画的是arcStyle,则1看方向,2看是否有外边框
        if self.clockwise:
            self.drawClockwiseArc(painter)         # 顺时针
        else:
            self.drawCounterclockwiseArc(painter)  # 逆时针
        if self.outerFrame:                        # 有无边框
            self.drawOuterFrame(painter)

    elif self.poolStyle:                           # 如果是poolStyle,则默认有边框
        self.drawOuterFrame(painter)
        self.drawBigPool(painter)

    elif self.waveStyle:                           # 如果是waveStyle,则默认有边框
        self.drawOuterFrame(painter)
        self.drawBigWave(painter)                  # 大圆波浪样式

    elif self.arc_poolStyle:                       # 4种情况,顺时针逆时针,有无圆弧。最后要画pool
        if self.clockwise:
            self.drawClockwiseArc(painter)
        else:
            self.drawCounterclockwiseArc(painter)
        if self.outerFrame:
            self.drawOuterFrame(painter)
        self.drawSmallPool(painter)                # 小圆水池样式
        
    elif self.arc_waveStyle:
        if self.clockwise:
            self.drawClockwiseArc(painter)
        else:
            self.drawCounterclockwiseArc(painter)
        if self.outerFrame:
            self.drawOuterFrame(painter)
        self.drawSmallWave(painter)                # 小圆波浪样式
    self.drawText(painter)
    self.update()                                  # 重绘

关于6个圆的圆心坐标,别忘记了圆和圆之间有间隔。同种颜色上下一组,其横坐标总是相同;不同颜色但同排的一组,其纵坐标总是相同。六个圆的大概位置以及核心代码如下:
PyQt5设计圆形水位指示器(QPainter画正弦线,画圆画弧画弦;QFont字体设置;QPainterPath裁剪,QSlider,QCheckbox)_第1张图片

# 3. 6个圆的圆心坐标点
self.radius = 40   # 内圆半径
self.radiusL = 44  # 大波浪/水池样式所在大圆的半径
self.spacer = 10   # 两圆之间应间隔2*spacer
self.delta = 4     # 44-40 = 4
self.pp = 8        # 波浪浪高
self.halfPen = 4   # 外边框(圆弧)弧线宽度为2 * halfPen

#  蓝色两内圆圆心坐标
self.upBlueXPoint, self.upBlueYPoint = -3 * self.radius - 2 * self.spacer, -2 * self.radius-self.spacer
self.lowBlueXPoint, self.lowBlueYPoint = self.upBlueXPoint, self.spacer

#  红色两内圆圆心坐标
self.upRedXPoint, self.upRedYPoint = -self.radius, self.upBlueYPoint
self.lowRedXPoint, self.lowRedYPoint = self.upRedXPoint, self.lowBlueYPoint

#  绿色两内圆圆心坐标
self.upGreenXPoint, self.upGreenYPoint = self.radius + 2 * self.spacer, self.upBlueYPoint
self.lowGreenXPoint, self.lowGreenYPoint = self.upGreenXPoint, self.lowBlueYPoint
def drawInnerCircle(self, painter):
    painter.save()            # 保存当前坐标系
    # 画笔设置
    painter.setPen(QtCore.Qt.NoPen)
    painter.setBrush(self.colorInnerCircle)
    painter.setOpacity(0.2)   # 透明度

    painter.drawEllipse(self.upBlueXPoint, self.upBlueYPoint, self.radius * 2, self.radius * 2)           # blue upper
    painter.drawEllipse(self.upBlueXPoint, self.lowBlueYPoint, self.radius * 2, self.radius * 2)         # blue lower
    painter.drawEllipse(self.upRedXPoint, self.upBlueYPoint, self.radius * 2, self.radius * 2)           # red upper
    painter.drawEllipse(self.upRedXPoint, self.lowBlueYPoint, self.radius * 2, self.radius * 2)           # red lower
    painter.drawEllipse(self.upGreenXPoint, self.upBlueYPoint, self.radius * 2, self.radius * 2)         # green upper
    painter.drawEllipse(self.upGreenXPoint, self.lowBlueYPoint, self.radius * 2, self.radius * 2)         # green lower
    
    painter.restore()         #  恢复正常坐标系

drawText字体设置

核心代码:

def drawText(self, painter):
    painter.save()
    painter.setPen(self.colorText)                         # 设置字体颜色
    font = QtGui.QFont()
    font.setFamily("Microsoft YaHei")                      # 字体种类
    font.setLetterSpacing(QtGui.QFont.AbsoluteSpacing, 0)  # 间距
    font.setPixelSize(28)                                  # 像素大小设置,注意不是字号大小。
    painter.setFont(font) 

    fontMetrics = QtGui.QFontMetrics(font)                 # 字体尺寸
    upValue = str(self.process) + "%"
    lowValue = str(self.process)
    upTextW = fontMetrics.width(upValue)                   # 字符串宽度和高度设置
    upTextH = fontMetrics.height()
    lowTextW = fontMetrics.width(lowValue)
    lowTextH = fontMetrics.height()
    painter.drawText(QtCore.QRect(-2 * self.radius - 2 * self.lowBlueYPoint - upTextW/2, -self.radius - self.lowBlueYPoint - upTextH/2,
                              upTextW, upTextH), 0, upValue)      # blue upper 注意设置坐标的时候考虑到字符宽度影响
    painter.drawText(QtCore.QRect(-2 * self.radius - 2 * self.lowBlueYPoint - lowTextW/2, self.lowBlueYPoint + self.radius - lowTextH/2,
                              lowTextW, lowTextH), 0, lowValue)   # blue lower
   ...
   ...

水平指示线(poolStyle)drawChord画弦

核心代码如下:

painter.drawChord(QtCore.QRect(self.upBlueXPoint, self.upBlueYPoint, self.radius * 2,
                               self.radius * 2), (-self.process * 1.8 + 270) * 16, self.process * 3.6 * 16)      # blue upper drawChord(x,y, 起始角度,跨越角度)度数要*16.
painter.drawChord(QtCore.QRect(self.upBlueXPoint, self.lowBlueYPoint, self.radius * 2,
                               self.radius * 2), (-self.process * 1.8 + 270) * 16, self.process * 3.6 * 16)      # blue lower

画正弦波波浪动态曲线

该部分是整个项目的难点,主要步骤包括:

  1. 绘制正弦曲线
  2. Qpainterpath裁剪形成闭合的,带波浪的水位形状
  3. 设置定时器让曲线动起来

绘制正弦曲线

核心代码如下:

# 通过QPainterpath.lineTo连接这些正弦分布的点集
for i in range(2 * self.radiusL+1):
    pathUpBlueWave1.lineTo(startUpBlueX + i, startUpBlueY + self.pp / 2 * math.sin(
        2 * i / self.radiusL * math.pi + self.startWave + self.startAngle / 360 * 2 * math.pi) - self.pp * self.process / 100)
    pathUpBlueWave2.lineTo(startUpBlueX + i, startUpBlueY - self.pp / 2 * math.sin(
        2 * i / self.radiusL * math.pi + self.startWave + self.startAngle / 360 * 2 * math.pi) - self.pp * self.process / 100)

QPainterPath.intersected()裁剪

笔者是绘制了一个上边沿为波浪线的矩形,然后和外圆进行裁剪操作,得到波浪水位的。核心代码如下:

pathInnerCircle = QtGui.QPainterPath()
pathInnerCircle.addEllipse(QtCore.QRectF(startUpBlueX, -2 * self.radiusL-self.spacer + self.delta,
                                         self.radiusL * 2, self.radiusL * 2))    # 把圆加到路径中
pathUpBlueWater1 = pathInnerCircle.intersected(pathUpBlueWave1)     # 裁剪操作
pathUpBlueWater2 = pathInnerCircle.intersected(pathUpBlueWave2)
painter.drawPath(pathUpBlueWater1)                                  # 绘制最终裁剪好的路径
painter.drawPath(pathUpBlueWater2)  

设置定时器让波浪有动画效果

在本项目中,笔者用了最简单的方式——while循环来定时。核心代码如下:

# 定时器
count = 0
while count < 30000:  # count每到30000,就return。然后painterEvent进入update时再次启用计时器。
    count += 1
self.startWave += math.pi/20
return

至此,该项目所有重难点总结完毕~?

经验总结

  1. 绘制图线较为复杂的控件or动画图,强烈建议先在纸上用笔把各个坐标和函数计算出来,并画一个大概图纸。切勿直接写代码胡乱试数,这样做耗时费力,即便是试出结果来,自己也未必对基本的数学原理和逻辑清楚,不利于成长和学习。
  2. 复杂的代码,一次很难写的完美清晰。不妨多次重构升级,并尝试用不同的方法/函数/原理实现。重构的过程就是学习的过程。

PyQt5内置的绘图函数和效果

PyQt5绝大多数内置的绘图函数,其效果和用法与Qt是相同的。开发者不妨参考Qt5的官方手册,或是PyQt5的官方文档来查看。
PyQt官方文档:QPainter
PyQt官方文档:QPen
PyQt官方文档:QBrush

最后,上海的Qter们,欢迎关注公众号 “Qter欢聚在上海” ,让我们共同学习,共同成长?

你可能感兴趣的:(PyQt5,Python,Qt)