导入
上篇博客分享了一个QT的带进度条的球型按键的控件模块,可以直接复制到你的QT UI项目下import导入,简单的几个API函数即可调用,兼容你的项目。这篇博客再分享2个最近写的美化控件模块,同样直接import,然后简单的API调用即可。
分别是折线图和汽车表盘的美化控件,效果如下:
![]() |
![]() |
API
折线图控件API:
-
create_dynamic_line_chart
函数""" @brief 创建动态折线图控件的公共API接口 @param parent_window: 父窗口对象(QWidget) @param theme: 颜色主题,可选 'blue'(默认), 'green', 'red', 'purple' @param max_points: 最大数据点数量(默认100) @param label : 标签(默认CPU) @return DynamicLineChart自定义实例 """ create_dynamic_line_chart(parent_window, theme='blue', max_points=100, label='CPU')
-
add_data_point
添加数据点,参数就是一个数值,范围0-100,表示百分比占比
表盘控件API :
-
create_car_gauge
函数""" @brief 创建动态折线图控件的公共API接口 @param parent_window: 父窗口对象(QWidget) @param theme: 可选 'classic'(默认), 'sport', 'luxury', 'racing' @param min_value: 最小值(默认0) @param max_value: 最大值(默认220) @param label : (默认"SPEED") @return CarGauge自定义实例 """ create_car_gauge(parent_window, theme='classic', min_value=0, max_value=220, label="SPEED")
-
set_value
设置表盘数值,范围就是上面函数参数中最小值到最大值之间
完整代码
折线图代码模块
# dynamic_line_chart.py
from PyQt5.QtWidgets import QWidget
from PyQt5.QtCore import Qt, QTimer, QRectF
from PyQt5.QtGui import QPainter, QColor, QPen, QFont, QPainterPath
class DynamicLineChart(QWidget):
"""动态折线图控件,用于显示CPU占用等实时数据"""
def __init__(self, parent=None, theme='blue', max_points=100, label='CPU'):
super().__init__(parent)
self.setFixedSize(300, 150)
# 主题颜色配置
self.themes = {
'blue': {
'background': QColor(25, 35, 50),
'grid': QColor(60, 80, 120, 120),
'line': QColor(64, 158, 255),
'fill': QColor(64, 158, 255, 80),
'text': QColor(200, 220, 255)
},
'green': {
'background': QColor(25, 40, 30),
'grid': QColor(60, 120, 80, 120),
'line': QColor(76, 175, 80),
'fill': QColor(76, 175, 80, 80),
'text': QColor(200, 255, 220)
},
'red': {
'background': QColor(40, 25, 25),
'grid': QColor(120, 60, 60, 120),
'line': QColor(244, 67, 54),
'fill': QColor(244, 67, 54, 80),
'text': QColor(255, 220, 220)
},
'purple': {
'background': QColor(35, 25, 50),
'grid': QColor(100, 60, 120, 120),
'line': QColor(156, 39, 176),
'fill': QColor(156, 39, 176, 80),
'text': QColor(240, 220, 255)
}
}
self.current_theme = theme if theme in self.themes else 'blue'
self.theme_colors = self.themes[self.current_theme]
# 数据相关
self.max_points = max_points
self.data_points = []
self.current_value = 0
self.label = label
# 动画定时器
self.animation_timer = QTimer(self)
self.animation_timer.timeout.connect(self.update_animation)
self.animation_timer.start(50) # 20 FPS
def update_animation(self):
"""更新动画效果"""
self.update()
def add_data_point(self, value):
"""添加数据点,value范围0-100"""
clamped_value = max(0, min(100, value))
self.data_points.append(clamped_value)
# 保持数据点数量不超过最大值
if len(self.data_points) > self.max_points:
self.data_points.pop(0)
self.current_value = clamped_value
self.update()
def clear_data(self):
"""清空所有数据"""
self.data_points = []
self.current_value = 0
self.update()
def paintEvent(self, event):
"""绘制事件"""
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
painter.setRenderHint(QPainter.SmoothPixmapTransform)
# 绘制背景
painter.fillRect(self.rect(), self.theme_colors['background'])
# 获取绘制区域
rect = self.rect()
margin = 15
chart_rect = QRectF(
margin, margin,
rect.width() - 2 * margin,
rect.height() - 2 * margin
)
# 绘制网格
self.draw_grid(painter, chart_rect)
# 绘制折线图
if self.data_points:
self.draw_line_chart(painter, chart_rect)
# 绘制当前值文本
self.draw_current_value(painter, rect)
def draw_grid(self, painter, chart_rect):
"""绘制网格线"""
pen = QPen(self.theme_colors['grid'])
pen.setWidth(1)
painter.setPen(pen)
# 水平网格线 (0%, 25%, 50%, 75%, 100%)
for i in range(5):
y = chart_rect.bottom() - (i * chart_rect.height() / 4)
painter.drawLine(
int(chart_rect.left()), int(y),
int(chart_rect.right()), int(y)
)
# 垂直网格线(每10个数据点一条)
if len(self.data_points) > 10:
step = max(10, len(self.data_points) // 5)
for i in range(0, len(self.data_points), step):
x = chart_rect.right() - (len(self.data_points) - 1 - i) * (chart_rect.width() / max(1, len(self.data_points) - 1))
painter.drawLine(
int(x), int(chart_rect.top()),
int(x), int(chart_rect.bottom())
)
def draw_line_chart(self, painter, chart_rect):
"""绘制折线图"""
if not self.data_points:
return
# 创建折线路径
path = QPainterPath()
fill_path = QPainterPath()
point_count = len(self.data_points)
if point_count == 1:
# 只有一个点的情况
x = chart_rect.right()
y = chart_rect.bottom() - (self.data_points[0] / 100.0) * chart_rect.height()
path.moveTo(x, y)
fill_path.moveTo(x, y)
fill_path.lineTo(x, chart_rect.bottom())
else:
# 多个点的情况
for i, value in enumerate(self.data_points):
# x坐标从右到左(最新的数据在右边)
x = chart_rect.right() - (point_count - 1 - i) * (chart_rect.width() / (point_count - 1))
y = chart_rect.bottom() - (value / 100.0) * chart_rect.height()
if i == 0:
path.moveTo(x, y)
fill_path.moveTo(x, y)
else:
path.lineTo(x, y)
fill_path.lineTo(x, y)
# 闭合填充路径
fill_path.lineTo(chart_rect.right(), chart_rect.bottom())
fill_path.lineTo(chart_rect.left(), chart_rect.bottom())
fill_path.lineTo(chart_rect.left(), chart_rect.bottom() - (self.data_points[0] / 100.0) * chart_rect.height())
# 绘制填充区域
painter.setBrush(self.theme_colors['fill'])
painter.setPen(Qt.NoPen)
painter.drawPath(fill_path)
# 绘制折线
pen = QPen(self.theme_colors['line'])
pen.setWidth(2)
painter.setPen(pen)
painter.setBrush(Qt.NoBrush)
painter.drawPath(path)
def draw_current_value(self, painter, rect):
"""绘制当前值文本"""
painter.setPen(self.theme_colors['text'])
painter.setFont(QFont("Arial", 12, QFont.Bold))
# 绘制百分比值
value_text = f"{int(self.current_value)}%"
value_rect = QRectF(10, 5, 80, 30)
painter.drawText(value_rect, Qt.AlignLeft | Qt.AlignVCenter, value_text)
# 绘制标签
label_text = self.label
label_rect = QRectF(rect.width() - 60, 5, 60, 30)
painter.drawText(label_rect, Qt.AlignRight | Qt.AlignVCenter, label_text)
def create_dynamic_line_chart(parent_window, theme='blue', max_points=100, label='CPU'):
"""
创建动态折线图控件的公共API接口
Args:
parent_window: 父窗口对象(QWidget)
theme: 颜色主题,可选 'blue', 'green', 'red', 'purple'(默认'blue')
max_points: 最大数据点数量(默认100)
label : 标签
Returns:
DynamicLineChart实例
"""
chart = DynamicLineChart(parent_window, theme=theme, max_points=max_points, label=label)
return chart
表盘代码模块
# car_gauge.py
import math
from PyQt5.QtWidgets import QWidget
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QPainter, QColor, QPen, QFont, QRadialGradient
class CarGauge(QWidget):
"""汽车表盘控件,用于显示速度、转速等数值"""
def __init__(self, parent=None, theme='classic', min_value=0, max_value=220, label="SPEED"):
super().__init__(parent)
self.setFixedSize(250, 250)
# 主题颜色配置
self.themes = {
'classic': {
'background': QColor(20, 20, 30),
'border': QColor(80, 80, 100),
'needle': QColor(255, 50, 50),
'scale_text': QColor(220, 220, 240),
'scale_lines': QColor(180, 180, 200),
'value_text': QColor(255, 200, 100),
'label_text': QColor(180, 200, 220)
},
'sport': {
'background': QColor(15, 15, 25),
'border': QColor(100, 50, 50),
'needle': QColor(255, 80, 80),
'scale_text': QColor(240, 200, 200),
'scale_lines': QColor(200, 150, 150),
'value_text': QColor(255, 150, 150),
'label_text': QColor(220, 180, 180)
},
'luxury': {
'background': QColor(25, 20, 20),
'border': QColor(120, 100, 80),
'needle': QColor(255, 215, 0),
'scale_text': QColor(240, 220, 200),
'scale_lines': QColor(200, 180, 160),
'value_text': QColor(255, 220, 180),
'label_text': QColor(220, 200, 180)
},
'racing': {
'background': QColor(10, 10, 15),
'border': QColor(60, 180, 60),
'needle': QColor(60, 220, 60),
'scale_text': QColor(200, 255, 200),
'scale_lines': QColor(150, 220, 150),
'value_text': QColor(180, 255, 180),
'label_text': QColor(200, 240, 200)
}
}
self.current_theme = theme if theme in self.themes else 'classic'
self.theme_colors = self.themes[self.current_theme]
# 表盘参数
self.min_value = min_value
self.max_value = max_value
self.current_value = min_value
self.label = label
# 动画相关
self.target_value = min_value
self.animation_step = 0
self.animation_timer = QTimer(self)
self.animation_timer.timeout.connect(self.animate_needle)
self.animation_timer.setInterval(16) # ~60 FPS
def set_value(self, value):
"""设置表盘数值,带动画效果"""
clamped_value = max(self.min_value, min(self.max_value, value))
self.target_value = clamped_value
if not self.animation_timer.isActive():
self.animation_timer.start()
def animate_needle(self):
"""针动画更新"""
# 计算动画步进(简单的线性插值)
diff = self.target_value - self.current_value
if abs(diff) < 0.1:
self.current_value = self.target_value
self.animation_timer.stop()
else:
self.current_value += diff * 0.1 # 10%步进
self.update()
def paintEvent(self, event):
"""绘制事件"""
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
painter.setRenderHint(QPainter.SmoothPixmapTransform)
rect = self.rect()
center = rect.center()
radius = min(rect.width(), rect.height()) // 2 - 10
# 绘制表盘背景
self.draw_gauge_background(painter, center, radius)
# 绘制刻度
self.draw_scale(painter, center, radius)
# 绘制指针
self.draw_needle(painter, center, radius)
# 绘制数值和标签
self.draw_value_and_label(painter, center, radius)
def draw_gauge_background(self, painter, center, radius):
"""绘制表盘背景"""
# 外圈边框
outer_radius = radius + 8
border_grad = QRadialGradient(center, outer_radius * 1.2, center)
border_grad.setColorAt(0.0, self.theme_colors['border'].lighter(150))
border_grad.setColorAt(0.7, self.theme_colors['border'])
border_grad.setColorAt(1.0, self.theme_colors['border'].darker(150))
painter.setBrush(border_grad)
painter.setPen(Qt.NoPen)
painter.drawEllipse(center, outer_radius, outer_radius)
# 内圈背景
background_grad = QRadialGradient(center, radius * 1.2, center)
background_grad.setColorAt(0.0, self.theme_colors['background'].lighter(120))
background_grad.setColorAt(1.0, self.theme_colors['background'])
painter.setBrush(background_grad)
painter.drawEllipse(center, radius, radius)
def draw_scale(self, painter, center, radius):
"""绘制刻度线和数字"""
# 表盘角度范围:-150度到150度(总共300度)
start_angle = -150
end_angle = 150
total_angle = end_angle - start_angle
# 绘制主刻度线和数字
main_ticks = 10
for i in range(main_ticks + 1):
angle = start_angle + (i * total_angle / main_ticks)
value = self.min_value + (i * (self.max_value - self.min_value) / main_ticks)
# 计算刻度线外端点
rad = math.radians(angle - 90) # 转换为弧度,-90度偏移使0度在顶部
outer_x = center.x() + (radius - 10) * math.cos(rad)
outer_y = center.y() + (radius - 10) * math.sin(rad)
# 计算刻度线内端点
inner_x = center.x() + (radius - 25) * math.cos(rad)
inner_y = center.y() + (radius - 25) * math.sin(rad)
# 绘制主刻度线
pen = QPen(self.theme_colors['scale_lines'])
pen.setWidth(3)
painter.setPen(pen)
painter.drawLine(int(outer_x), int(outer_y), int(inner_x), int(inner_y))
# 绘制数字
text_x = center.x() + (radius - 40) * math.cos(rad)
text_y = center.y() + (radius - 40) * math.sin(rad)
painter.setPen(self.theme_colors['scale_text'])
painter.setFont(QFont("Arial", 10, QFont.Bold))
painter.drawText(
int(text_x - 15), int(text_y - 8), 30, 16,
Qt.AlignCenter, str(int(value))
)
# 绘制次刻度线
sub_ticks = 50
for i in range(sub_ticks + 1):
angle = start_angle + (i * total_angle / sub_ticks)
if i % 5 != 0: # 跳过主刻度位置
rad = math.radians(angle - 90)
outer_x = center.x() + (radius - 15) * math.cos(rad)
outer_y = center.y() + (radius - 15) * math.sin(rad)
inner_x = center.x() + (radius - 22) * math.cos(rad)
inner_y = center.y() + (radius - 22) * math.sin(rad)
pen = QPen(self.theme_colors['scale_lines'])
pen.setWidth(1)
painter.setPen(pen)
painter.drawLine(int(outer_x), int(outer_y), int(inner_x), int(inner_y))
def draw_needle(self, painter, center, radius):
"""绘制指针"""
# 计算指针角度
if self.max_value == self.min_value:
angle = -150 # 避免除零错误
else:
ratio = (self.current_value - self.min_value) / (self.max_value - self.min_value)
angle = -150 + ratio * 300 # 300度范围
rad = math.radians(angle - 90)
# 指针主干
needle_length = radius - 30
needle_x = center.x() + needle_length * math.cos(rad)
needle_y = center.y() + needle_length * math.sin(rad)
pen = QPen(self.theme_colors['needle'])
pen.setWidth(4)
painter.setPen(pen)
painter.drawLine(int(center.x()), int(center.y()), int(needle_x), int(needle_y))
# 指针头部(圆形)
painter.setBrush(self.theme_colors['needle'])
painter.setPen(Qt.NoPen)
painter.drawEllipse(
int(needle_x - 6), int(needle_y - 6), 12, 12
)
# 中心圆点
painter.setBrush(QColor(50, 50, 60))
painter.drawEllipse(
int(center.x() - 8), int(center.y() - 8), 16, 16
)
def draw_value_and_label(self, painter, center, radius):
"""绘制当前数值和标签"""
# 绘制当前数值
painter.setPen(self.theme_colors['value_text'])
painter.setFont(QFont("Arial", 18, QFont.Bold))
value_text = str(int(self.current_value))
painter.drawText(
int(center.x() - 40), int(center.y() + 10), 80, 30,
Qt.AlignCenter, value_text
)
# 绘制标签
painter.setPen(self.theme_colors['label_text'])
painter.setFont(QFont("Arial", 10))
painter.drawText(
int(center.x() - 30), int(center.y() + 40), 60, 20,
Qt.AlignCenter, self.label
)
def create_car_gauge(parent_window, theme='classic', min_value=0, max_value=220, label="SPEED"):
"""
创建汽车表盘控件的公共API接口
Args:
parent_window: 父窗口对象(QWidget)
theme: 颜色主题,可选 'classic', 'sport', 'luxury', 'racing'(默认'classic')
min_value: 最小值(默认0)
max_value: 最大值(默认220)
label: 显示标签(默认"SPEED")
Returns:
CarGauge实例
"""
gauge = CarGauge(parent_window, theme=theme, min_value=min_value, max_value=max_value, label=label)
return gauge
使用样例
# example.py
import sys
import random
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QHBoxLayout
from PyQt5.QtCore import QTimer
# 导入自定义控件模块
from dynamic_line_chart import create_dynamic_line_chart
from car_gauge import create_car_gauge
class TestWindow(QWidget):
"""测试窗口,展示两个控件的效果"""
def __init__(self):
super().__init__()
self.setWindowTitle("高级美化QT控件测试")
self.setGeometry(100, 100, 800, 400)
# 创建主布局
main_layout = QVBoxLayout()
# 创建上层布局(两个控件并排)
top_layout = QHBoxLayout()
# 创建动态折线图(CPU监控样式)
self.cpu_chart = create_dynamic_line_chart(
parent_window=self,
theme='green', # 绿色主题
max_points=80,
label='CPU'
)
top_layout.addWidget(self.cpu_chart)
# 创建汽车表盘(速度表)
self.speed_gauge = create_car_gauge(
parent_window=self,
theme='sport', # 运动主题
min_value=0,
max_value=260,
label="SPEED"
)
top_layout.addWidget(self.speed_gauge)
# 创建下层布局(更多控件)
bottom_layout = QHBoxLayout()
# 创建内存使用折线图
self.memory_chart = create_dynamic_line_chart(
parent_window=self,
theme='blue',
max_points=60,
label='内存'
)
bottom_layout.addWidget(self.memory_chart)
# 创建转速表盘
self.rpm_gauge = create_car_gauge(
parent_window=self,
theme='racing', # 赛车主题
min_value=0,
max_value=8000,
label="RPM"
)
bottom_layout.addWidget(self.rpm_gauge)
# 添加布局到主布局
main_layout.addLayout(top_layout)
main_layout.addLayout(bottom_layout)
self.setLayout(main_layout)
# 启动模拟数据更新
self.simulation_timer = QTimer(self)
self.simulation_timer.timeout.connect(self.update_simulation_data)
self.simulation_timer.start(100) # 每100ms更新一次
# 初始化数据
self.cpu_value = 30
self.memory_value = 45
self.speed_value = 80
self.rpm_value = 2500
def update_simulation_data(self):
"""模拟实时数据更新"""
# 模拟CPU使用率(30-90%之间波动)
self.cpu_value += random.uniform(-5, 5)
self.cpu_value = max(30, min(90, self.cpu_value))
self.cpu_chart.add_data_point(self.cpu_value)
# 模拟内存使用率(40-85%之间波动)
self.memory_value += random.uniform(-3, 3)
self.memory_value = max(40, min(85, self.memory_value))
self.memory_chart.add_data_point(self.memory_value)
# 模拟车速(0-220之间变化)
self.speed_value += random.uniform(-8, 8)
self.speed_value = max(0, min(220, self.speed_value))
self.speed_gauge.set_value(self.speed_value)
# 模拟转速(1000-7000之间变化)
self.rpm_value += random.uniform(-200, 200)
self.rpm_value = max(1000, min(7000, self.rpm_value))
self.rpm_gauge.set_value(self.rpm_value)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = TestWindow()
window.show()
sys.exit(app.exec_())