前言:进入学习Python开发上位机界面的第二阶段,学习如何开发自定义控件,从常用的控件入手学习,本期主要学习如何使用PyQt5绘制带有刻度的温度计控件。
1. 先找到一篇参考文章
参考文章:Qt编写自定义控件5-柱状温度计
参考文章代码是C++写的,改成python写的代码。修改代码成功后,本人生成的温度计控件显示效果如下所示:
2. 学习背后涉及的知识点
参考文章:实战PyQt5: 123-详解QPainter绘图
2.1 如何设置圆形图案的文本在圆形图案中居中显示
drawEllipse(): 绘制一个椭圆,当设置椭圆的宽度和高度相等,它实际上是一个圆形。
使用drawText(x, y, text_width, text_height, Qt.AlignCenter, text) 方法绘制文本,其中x是计算出的起始X坐标,y是你希望文本基线的位置(通常是你绘制区域的中心线或稍上方)。
def draw_centered_text(self, qp):
# 设置字体
font = QFont('Arial', 20)
qp.setFont(font)
# 要绘制的文本
text = "Hello, PyQt5!"
# 获取文本的宽度和高度
fm = QFontMetrics(font)
text_width = fm.width(text)
text_height = fm.height()
# 计算文本居中绘制的起始位置
# 注意:这里假设我们要在整个QWidget的区域内居中
x = (self.width() - text_width) // 2
y = (self.height() + text_height) // 2 # 向上偏移文本高度的一半以垂直居中
# 绘制文本
qp.drawText(x, y, text_width, text_height, Qt.AlignCenter, text)
2.2 绘制温度计刻度线
drawPoint(): 使用当前笔的颜色在给定位置绘制一个点。
drawLine(): 绘制一条线。
2.3 绘制标识当前温度值的三角形箭头
drawPolygon(): 绘制可填充一个多边形。
3. 温度计控件待优化点
3.1 温度计刻度显示的温度数值存在精度问题
温度计的数值范围是0℃-100℃,在某些数值下温度计的刻度箭头指示是不够准确的,例如下图所示:37℃的箭头应该指示在目前位置的偏下面一点。
原因分析:用于显示用户设置温度数值的三角形箭头Y轴位置计算是有精度误差的。
# 计算标尺的高度
rulerHeight = self.height() - 2 * self.radius
# 计算每一格移动多少
increment = rulerHeight / (self.maxValue - self.minValue)
self.barX = self._get_bar_x_pos()
self.barY = (self.maxValue - self.userValue + self.minValue) * increment
这个是因为python进行除法运算就存在计算精度问题:
3.2 用户设置温度为标尺的最大刻度值存在显示问题
原因分析:温度计标尺的最大刻度Y轴位置正好是温度计控件所在界面的高度。
4. 带有刻度的温度计控件的完整代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Author : Logintern09
import sys
from PyQt5.QtCore import QRectF, QPoint, Qt, QRect, QPointF
from PyQt5.QtGui import (
QPainter,
QColor,
QFont,
QPainterPath,
QPolygon,
)
from PyQt5.QtWidgets import QWidget, QApplication, QMainWindow, QVBoxLayout
"""
* 柱状温度计控件
* 1:可设置精确度(小数点后几位)
* 2:可设置背景色/柱状颜色/线条颜色
* 3:可设置长线条步长及短线条步长
* 4:可设置温度计数值显示范围值
* 5:支持负数刻度值
* 6:可设置刻度尺位置 无 左侧 右侧 两侧
* 7:可设置用户设定目标值
"""
class BasicRulerTemp(QWidget):
def __init__(self, parent=None):
super(BasicRulerTemp, self).__init__(parent)
self.TickPosition = {
"TickPosition_Null": 0, # 不显示
"TickPosition_Left": 1, # 左侧显示
"TickPosition_Right": 2, # 右侧显示
"TickPosition_Both": 3, # 两侧显示
}
self.minValue = 0 # 最小值
self.maxValue = 100 # 最大值
self.userValue = 80 # 用户设定值
self.userValueColor = QColor("red") # 用户设定值颜色
self.precision = 0 # 精确度, 小数点后几位
self.longStep = 10 # 长线条等分步长
self.shortStep = 2 # 短线条等分步长
self.showUserValue = True # 是否显示三角箭头显示用户设定值
self.lineColor = QColor("black") # 线条颜色
self.barBgColor = QColor("white") # 柱状背景色
self.barColor = QColor("red") # 柱状颜色
self.tickPosition = 3 # 刻度尺位置
self.barWidth = 15 # 水银柱宽度
self.barHeight = 100 # 水银柱高度
self.radius = 30 # 水银柱底部圆半径
self.circleX_pos = self.width() / 3 # 水银柱底部圆X轴坐标位置
def get_min_value(self):
"""
获取温度计刻度最小值
"""
return self.minValue
def get_max_value(self):
"""
获取温度计刻度最大值
"""
return self.maxValue
def get_current_value(self):
"""
获取用户设置的温度计当前刻度值
"""
return self.userValue
def get_precision(self):
"""
获取温度计刻度显示的数值精度(保留到小数点后几位)
"""
return self.precision
def set_range(self, min_value, max_value):
"""
设置温度计数值显示范围
"""
self.minValue = min_value
self.maxValue = max_value
def set_min_value(self, min_value):
"""
设置温度计刻度显示最小数值
"""
self.minValue = min_value
def set_max_value(self, max_value):
"""
设置温度计刻度显示最大数值
"""
self.maxValue = max_value
def set_value(self, value):
"""
设置温度计刻度显示的数值
"""
self.userValue = value
def set_precision(self, precision):
"""
设置温度计显示刻度数值的精度(保留到小数点后几位)
"""
self.precision = precision
def set_long_step(self, long_step):
"""
设置温度计刻度线的长线条步长
"""
self.longStep = long_step
def set_short_step(self, short_step):
"""
设置温度计刻度线的短线条步长
"""
self.shortStep = short_step
def set_show_user_value(self, show_user_value):
"""
设置是否显示用户设定值
"""
self.showUserValue = show_user_value
def set_bar_bg_color(self, color):
"""
设置水银柱背景颜色
"""
self.userValueColor = QColor(color)
def set_line_color(self, color):
"""
设置温度计刻度线的线条颜色
"""
self.lineColor = QColor(color)
def set_tick_position(self, tick_position):
"""
设置刻度尺位置
"""
self.tickPosition = self.TickPosition[tick_position]
class QRulerTemp(BasicRulerTemp):
def __init__(self, parent=None):
super(QRulerTemp, self).__init__(parent)
self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint)
self.setAttribute(Qt.WA_TranslucentBackground)
self.setFixedSize(500, 500)
def paintEvent(self, event):
# 绘制准备工作,启用反锯齿
painter = QPainter(self)
# 保存当前画笔状态
painter.save()
painter.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing)
# 绘制水银柱背景,包含水银柱底部圆
self.drawBarBg(painter)
# 绘制当前水银柱,包含水银柱底部圆
self.drawBar(painter)
# 绘制标尺及刻度尺
if self.tickPosition == self.TickPosition["TickPosition_Left"]:
self.drawRuler(painter, 0)
elif self.tickPosition == self.TickPosition["TickPosition_Right"]:
self.drawRuler(painter, 1)
elif self.tickPosition == self.TickPosition["TickPosition_Both"]:
self.drawRuler(painter, 0)
self.drawRuler(painter, 1)
# 绘制用于显示水银柱当前值的三角箭头
if self.showUserValue:
self.draw_value_flag(painter)
def _get_bar_x_pos(self):
# 计算在背景宽度的基础上缩小的百分比, 至少为 2
circlePercent = self.radius / 3
if circlePercent < 2:
circlePercent = 2
bar_x_pos = self.circleX_pos + circlePercent
return bar_x_pos
def drawBarBg(self, painter):
# 绘制水银柱背景颜色,包含水银柱底部圆
painter.save()
painter.setPen(Qt.NoPen)
painter.setBrush(self.barBgColor)
# 计算标尺的高度
offset = 4 # 背景形状偏移水银柱的像素
barHeight = self.height() - self.radius
bar_x_pos = self._get_bar_x_pos()
barX = bar_x_pos - offset
barY = 1
barRect = QRectF(barX, barY, self.barWidth + offset * 2, barHeight)
circleX = self.circleX_pos - 10
circleY = self.height() - self.radius * 2 - 1
circleWidth = self.radius * 2
circleRect = QRectF(circleX, circleY, circleWidth, circleWidth)
path = QPainterPath()
path.addRect(barRect)
path.addEllipse(circleRect)
path.setFillRule(Qt.WindingFill)
painter.drawPath(path)
painter.restore()
def drawBar(self, painter):
# 绘制水银柱及标识当前刻度的三角形箭头
painter.save()
painter.setPen(Qt.NoPen)
painter.setBrush(self.barColor)
# 计算在背景宽度的基础上缩小的百分比, 至少为 2
circlePercent = self.radius / 3
if circlePercent < 2:
circlePercent = 2
# 计算标尺的高度
rulerHeight = self.height() - 2 * self.radius
# 计算每一格移动多少
increment = rulerHeight / (self.maxValue - self.minValue)
self.barX = self._get_bar_x_pos()
self.barY = (self.maxValue - self.userValue + self.minValue) * increment
barRect = QRectF(
self.barX,
self.barY,
self.barWidth,
(self.userValue - self.minValue) * increment + self.radius,
)
circleX = self.circleX_pos
# 偏移 2 个像素,使得看起来边缘完整
circleY = self.height() - self.radius * 2 - 2
circleWidth = self.radius * 2 - circlePercent * 2
circleRect = QRectF(circleX, circleY + circlePercent, circleWidth, circleWidth)
path = QPainterPath()
path.addRect(barRect)
path.addEllipse(circleRect)
path.setFillRule(Qt.WindingFill)
painter.drawPath(path)
# 设置水银柱底部圆的文本和文本颜色
painter.save()
font = QFont()
font.setPixelSize(circleRect.width() * 0.55)
painter.setFont(font)
painter.setPen(Qt.white)
painter.drawText(circleRect, Qt.AlignCenter, "%s" % self.userValue)
painter.restore()
def drawRuler(self, painter, type):
# 绘制刻度线
painter.save()
painter.setPen(self.lineColor)
barPercent = self.barWidth / 8 # 水银柱宽度
if barPercent < 2:
barPercent = 2
# 绘制纵向标尺刻度
length = self.height() - 2 * self.radius
# 计算每一格移动多少
increment = length / (self.maxValue - self.minValue)
# 长线条短线条长度
longLineLen = 10
shortLineLen = 7
# 绘制纵向标尺线 偏移line_offset像素
self.line_offset = 8
offset = self.barWidth / 2 + self.line_offset
# 左侧刻度尺需要重新计算
if type == 0:
offset = -1 * offset
longLineLen = -1 * longLineLen
shortLineLen = -1 * shortLineLen
initX = self.barX + self.barWidth / 2 + offset
initY = barPercent
topPot = QPointF(initX, initY)
bottomPot = QPointF(initX, self.height() - 2 * self.radius)
painter.drawLine(topPot, bottomPot)
# 根据范围值绘制刻度线及刻度值
for i in range(
self.maxValue, self.minValue - self.shortStep, -1 * self.shortStep
):
if i % self.longStep == 0:
# 绘制长线条
leftPot = QPoint(initX + longLineLen, initY)
rightPot = QPoint(initX, initY)
painter.drawLine(leftPot, rightPot)
# 绘制文字
strValue = f"{i:.{self.precision}f}"
fontHeight = painter.fontMetrics().height()
if type == 0:
# 左侧刻度线
x_offset = 45 + self.precision * 10
text_width = 30 + self.precision * 10
textRect = QRect(
initX - x_offset, initY - fontHeight / 3, text_width, 15
)
painter.drawText(textRect, Qt.AlignRight, strValue)
elif type == 1:
# 右侧刻度线
text_width = 30 + self.precision * 10
textRect = QRect(
initX + longLineLen + self.line_offset,
initY - fontHeight / 3,
text_width,
15,
)
painter.drawText(textRect, Qt.AlignLeft, strValue)
else:
# 绘制短线条
leftPot = QPointF(initX + shortLineLen, initY)
rightPot = QPointF(initX, initY)
painter.drawLine(leftPot, rightPot)
initY += increment * self.shortStep
painter.restore()
def draw_value_flag(self, painter):
painter.save()
painter.setPen(Qt.NoPen)
# 绘制用户设定值三角号
if self.showUserValue:
if (
self.tickPosition == self.TickPosition["TickPosition_Left"]
or self.tickPosition == self.TickPosition["TickPosition_Both"]
):
pts = QPolygon()
offset = 15
initX = self.barX - self.line_offset
initY = self.barY
pts.append(QPoint(initX, initY))
pts.append(QPoint(initX - offset, initY - offset / 2))
pts.append(QPoint(initX - offset, initY + offset / 2))
painter.setBrush(self.userValueColor)
painter.drawPolygon(pts)
if (
self.tickPosition == self.TickPosition["TickPosition_Right"]
or self.tickPosition == self.TickPosition["TickPosition_Both"]
):
pts = QPolygon()
offset = 15
initX = self.barX + self.barWidth + self.line_offset
initY = self.barY
pts.append(QPoint(initX, initY))
pts.append(QPoint(initX + offset, initY - offset / 2))
pts.append(QPoint(initX + offset, initY + offset / 2))
painter.setBrush(self.userValueColor)
painter.drawPolygon(pts)
painter.restore()
def main():
app = QApplication(sys.argv)
window = QMainWindow()
window.setGeometry(100, 100, 100, 290)
window.setWindowTitle("RulerTemp Example")
RulerTemp = QRulerTemp()
layout = QVBoxLayout()
layout.addWidget(RulerTemp)
window.setCentralWidget(QWidget())
window.centralWidget().setLayout(layout)
window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
# app = QApplication([])
# widget = QRulerTemp()
# widget.show()
# app.exec_()