镜像自地址
https://github.com/a13zhy/a13rollcall.git
已同步 2026-10-20 11:27:47 +08:00
235 行
8.2 KiB
Python
235 行
8.2 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
A13课堂点名系统 - 统计图表组件
|
|
使用QPainter绘制饼图、柱状图等可视化统计图表
|
|
"""
|
|
|
|
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QFrame, QSizePolicy
|
|
from PyQt5.QtCore import Qt, QRectF, QPointF
|
|
from PyQt5.QtGui import QFont, QColor, QPainter, QBrush, QPen, QLinearGradient, QPainterPath, QFontMetrics
|
|
|
|
|
|
class PieChartWidget(QWidget):
|
|
"""饼图组件"""
|
|
|
|
def __init__(self, title="", parent=None):
|
|
super().__init__(parent)
|
|
self.title = title
|
|
self.data = []
|
|
self.colors = [
|
|
"#107c10",
|
|
"#ca5010",
|
|
"#c42b1c",
|
|
"#0067c0",
|
|
"#8764b8",
|
|
"#999999",
|
|
]
|
|
self.setMinimumHeight(280)
|
|
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
|
|
def set_data(self, data):
|
|
"""
|
|
设置饼图数据
|
|
data: [(label, value), ...]
|
|
"""
|
|
self.data = data
|
|
self.update()
|
|
|
|
def paintEvent(self, event):
|
|
painter = QPainter(self)
|
|
painter.setRenderHint(QPainter.Antialiasing)
|
|
|
|
w = self.width()
|
|
h = self.height()
|
|
|
|
if self.title:
|
|
painter.setPen(QColor("#333333"))
|
|
painter.setFont(QFont("Microsoft YaHei", 13, QFont.Bold))
|
|
painter.drawText(0, 0, w, 30, Qt.AlignCenter, self.title)
|
|
|
|
chart_size = int(min(w - 200, h - 50))
|
|
chart_x = int((w - 200 - chart_size) / 2)
|
|
chart_y = int(35 + (h - 50 - chart_size) / 2)
|
|
|
|
total = sum(v for _, v in self.data) if self.data else 0
|
|
|
|
if total == 0:
|
|
painter.setPen(QColor("#999999"))
|
|
painter.setFont(QFont("Microsoft YaHei", 11))
|
|
painter.drawText(chart_x, chart_y, chart_size, chart_size, Qt.AlignCenter, "暂无数据")
|
|
else:
|
|
start_angle = 90 * 16
|
|
for i, (label, value) in enumerate(self.data):
|
|
if value <= 0:
|
|
continue
|
|
span_angle = int(-(value / total) * 360 * 16)
|
|
color = QColor(self.colors[i % len(self.colors)])
|
|
|
|
painter.setPen(Qt.NoPen)
|
|
painter.setBrush(QBrush(color))
|
|
painter.drawPie(QRectF(chart_x, chart_y, chart_size, chart_size), start_angle, span_angle)
|
|
start_angle += span_angle
|
|
|
|
center_x = chart_x + chart_size / 2
|
|
center_y = chart_y + chart_size / 2
|
|
inner_size = int(chart_size * 0.55)
|
|
inner_x = int(center_x - inner_size / 2)
|
|
inner_y = int(center_y - inner_size / 2)
|
|
|
|
painter.setBrush(QBrush(QColor("white")))
|
|
painter.drawEllipse(inner_x, inner_y, inner_size, inner_size)
|
|
|
|
painter.setPen(QColor("#333333"))
|
|
painter.setFont(QFont("Microsoft YaHei", 16, QFont.Bold))
|
|
painter.drawText(inner_x, inner_y, inner_size, inner_size, Qt.AlignCenter, str(total))
|
|
|
|
legend_x = chart_x + chart_size + 30
|
|
legend_y = chart_y + 10
|
|
for i, (label, value) in enumerate(self.data):
|
|
color = QColor(self.colors[i % len(self.colors)])
|
|
y = legend_y + i * 28
|
|
|
|
painter.setPen(Qt.NoPen)
|
|
painter.setBrush(QBrush(color))
|
|
painter.drawRoundedRect(legend_x, y, 14, 14, 3, 3)
|
|
|
|
painter.setPen(QColor("#333333"))
|
|
painter.setFont(QFont("Microsoft YaHei", 10))
|
|
percent = (value / total * 100) if total > 0 else 0
|
|
text = f"{label}: {value} ({percent:.1f}%)"
|
|
painter.drawText(legend_x + 22, y, 180, 14, Qt.AlignVCenter | Qt.AlignLeft, text)
|
|
|
|
painter.end()
|
|
|
|
|
|
class BarChartWidget(QWidget):
|
|
"""柱状图组件"""
|
|
|
|
def __init__(self, title="", parent=None, max_items=10):
|
|
super().__init__(parent)
|
|
self.title = title
|
|
self.data = []
|
|
self.max_items = max_items
|
|
self.bar_color = "#0067c0"
|
|
self.setMinimumHeight(300)
|
|
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
|
|
|
def set_data(self, data):
|
|
"""
|
|
设置柱状图数据
|
|
data: [(label, value), ...]
|
|
"""
|
|
self.data = data[:self.max_items]
|
|
self.update()
|
|
|
|
def set_bar_color(self, color):
|
|
self.bar_color = color
|
|
self.update()
|
|
|
|
def paintEvent(self, event):
|
|
painter = QPainter(self)
|
|
painter.setRenderHint(QPainter.Antialiasing)
|
|
|
|
w = self.width()
|
|
h = self.height()
|
|
|
|
if self.title:
|
|
painter.setPen(QColor("#333333"))
|
|
painter.setFont(QFont("Microsoft YaHei", 13, QFont.Bold))
|
|
painter.drawText(0, 0, w, 30, Qt.AlignCenter, self.title)
|
|
|
|
chart_top = 45
|
|
chart_bottom = h - 50
|
|
chart_left = 50
|
|
chart_right = w - 20
|
|
chart_height = chart_bottom - chart_top
|
|
chart_width = chart_right - chart_left
|
|
|
|
max_value = max((v for _, v in self.data), default=1)
|
|
if max_value <= 0:
|
|
max_value = 1
|
|
|
|
painter.setPen(QPen(QColor("#e0e0e0"), 1))
|
|
for i in range(5):
|
|
y = chart_top + chart_height * i / 4
|
|
painter.drawLine(chart_left, int(y), chart_right, int(y))
|
|
value = int(max_value * (4 - i) / 4)
|
|
painter.setPen(QColor("#999999"))
|
|
painter.setFont(QFont("Microsoft YaHei", 9))
|
|
painter.drawText(5, int(y) - 8, 40, 16, Qt.AlignRight | Qt.AlignVCenter, str(value))
|
|
painter.setPen(QPen(QColor("#e0e0e0"), 1))
|
|
|
|
if not self.data:
|
|
painter.setPen(QColor("#999999"))
|
|
painter.setFont(QFont("Microsoft YaHei", 11))
|
|
painter.drawText(chart_left, chart_top, chart_width, chart_height, Qt.AlignCenter, "暂无数据")
|
|
else:
|
|
bar_count = len(self.data)
|
|
bar_gap = 10
|
|
bar_width = max(20, (chart_width - bar_gap * (bar_count + 1)) // bar_count)
|
|
|
|
for i, (label, value) in enumerate(self.data):
|
|
x = chart_left + bar_gap + i * (bar_width + bar_gap)
|
|
bar_height = int(chart_height * value / max_value)
|
|
y = chart_bottom - bar_height
|
|
|
|
gradient = QLinearGradient(x, y, x, chart_bottom)
|
|
gradient.setColorAt(0, QColor(self.bar_color))
|
|
gradient.setColorAt(1, QColor(self.bar_color).lighter(130))
|
|
|
|
painter.setPen(Qt.NoPen)
|
|
painter.setBrush(QBrush(gradient))
|
|
painter.drawRoundedRect(int(x), int(y), int(bar_width), int(bar_height), 4, 4)
|
|
|
|
painter.setPen(QColor("#333333"))
|
|
painter.setFont(QFont("Microsoft YaHei", 10, QFont.Bold))
|
|
painter.drawText(int(x), int(y) - 20, int(bar_width), 16, Qt.AlignCenter, str(value))
|
|
|
|
painter.setPen(QColor("#666666"))
|
|
painter.setFont(QFont("Microsoft YaHei", 9))
|
|
short_label = label if len(label) <= 6 else label[:5] + "..."
|
|
painter.drawText(int(x) - 5, chart_bottom + 5, int(bar_width) + 10, 30, Qt.AlignHCenter | Qt.AlignTop, short_label)
|
|
|
|
painter.setPen(QPen(QColor("#cccccc"), 1))
|
|
painter.drawLine(chart_left, chart_bottom, chart_right, chart_bottom)
|
|
|
|
painter.end()
|
|
|
|
|
|
class StatsChartCard(QFrame):
|
|
"""统计图表卡片"""
|
|
|
|
def __init__(self, title="", chart_widget=None, parent=None):
|
|
super().__init__(parent)
|
|
self.setStyleSheet("""
|
|
QFrame {
|
|
background: white;
|
|
border: 1px solid #e5e5e5;
|
|
border-radius: 10px;
|
|
}
|
|
""")
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(16, 16, 16, 16)
|
|
layout.setSpacing(12)
|
|
|
|
if title:
|
|
title_label = QLabel(title)
|
|
title_label.setStyleSheet("font-size: 14px; font-weight: 700; color: #0067c0;")
|
|
layout.addWidget(title_label)
|
|
|
|
if chart_widget:
|
|
self.chart = chart_widget
|
|
layout.addWidget(self.chart, 1)
|
|
else:
|
|
self.chart = None
|
|
|
|
|
|
def create_pie_chart(title=""):
|
|
"""创建饼图的便捷函数"""
|
|
return PieChartWidget(title)
|
|
|
|
|
|
def create_bar_chart(title="", max_items=10):
|
|
"""创建柱状图的便捷函数"""
|
|
return BarChartWidget(title, max_items=max_items)
|