QT表盘和折线图控件

导入

上篇博客分享了一个QT的带进度条的球型按键的控件模块,可以直接复制到你的QT UI项目下import导入,简单的几个API函数即可调用,兼容你的项目。这篇博客再分享2个最近写的美化控件模块,同样直接import,然后简单的API调用即可。

分别是折线图和汽车表盘的美化控件,效果如下:

Image 1 Image 2

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_())