基本界面达成

This commit is contained in:
2026-05-11 00:21:16 +08:00
parent 649677f564
commit 65f221a5d8
13 changed files with 1818 additions and 347 deletions

View File

@@ -2,9 +2,12 @@ import os
import json
import re
from PyQt6.QtWidgets import (QWidget, QHBoxLayout, QVBoxLayout,
QPushButton, QLabel, QFrame, QGraphicsView, QGraphicsScene, QGraphicsPathItem)
from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal, QUrl, QObject, pyqtProperty, QRectF
from PyQt6.QtGui import QColor, QPen, QPainter, QPainterPath
QPushButton, QLabel, QFrame, QGraphicsView,
QGraphicsScene, QGraphicsPathItem, QProgressBar)
from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal, QUrl, QObject, pyqtProperty, QRectF, QSize
from PyQt6.QtGui import QColor, QPen, QPainter, QPainterPath, QFont, QLinearGradient, QBrush
from utils.config_parse import ConfigParse
def get_gcode_dir():
config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config.json")
@@ -17,167 +20,390 @@ def get_gcode_dir():
GCODE_DIR = get_gcode_dir()
class GCode2DPreviewWidget(QGraphicsView):
def __init__(self, parent=None):
# ── 状态主题色 ──────────────────────────────────────────
STATUS_COLORS = {
"Operational": "#4CAF50",
"Printing": "#2196F3",
"Paused": "#FF9800",
"Error": "#F44336",
"Offline": "#9E9E9E",
"Cancelling": "#FF5722",
"Finishing": "#8BC34A",
}
STATUS_LABELS = {
"Operational": "就绪",
"Printing": "打印中",
"Paused": "已暂停",
"Error": "错误",
"Offline": "离线",
"Cancelling": "取消中",
"Finishing": "整理中",
}
class CardFrame(QFrame):
"""统一的信息卡片样式"""
def __init__(self, title="", parent=None):
super().__init__(parent)
self.scene = QGraphicsScene(self)
self.setScene(self.scene)
self.setStyleSheet("background-color: #111111; border-radius: 5px; border: 1px solid #666;")
self.setRenderHint(QPainter.RenderHint.Antialiasing)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
# 翻转Y轴让(0,0)位于左下角,适配物理坐标系
self.scale(1, -1)
self.setStyleSheet("""
CardFrame {
background-color: #3a3a3a;
border: 1px solid #555555;
border-radius: 8px;
}
""")
layout = QVBoxLayout(self)
layout.setContentsMargins(12, 10, 12, 10)
layout.setSpacing(4)
def draw_paths(self, lines_data):
self.scene.clear()
bounding_rect = QRectF()
for color_str, data in lines_data.items():
points = data.get("points", [])
line_width = data.get("line_width", 2)
if not points:
continue
path = QPainterPath()
# 判断是一组连续点的坐标(单线条)[[x1,y1], [x2,y2]]
# 还是包含多根独立线条 [[[x1,y1], [x2,y2]], [[x3,y3], [x4,y4]]]
if isinstance(points[0][0], (int, float)):
path.moveTo(float(points[0][0]), float(points[0][1]))
for pt in points[1:]:
path.lineTo(float(pt[0]), float(pt[1]))
else:
for line_pts in points:
if not line_pts:
continue
path.moveTo(float(line_pts[0][0]), float(line_pts[0][1]))
for pt in line_pts[1:]:
path.lineTo(float(pt[0]), float(pt[1]))
path_item = QGraphicsPathItem(path)
# 安全获取颜色
pen_color = QColor(color_str) if QColor.isValidColor(color_str) else QColor("white")
pen = QPen(pen_color)
pen.setWidth(int(line_width))
# 保持 Cosmetic以确保视图缩放时线条粗细在屏幕上看起来一致
pen.setCosmetic(True)
path_item.setPen(pen)
self.scene.addItem(path_item)
bounding_rect = bounding_rect.united(path.boundingRect())
# 设置场景范围以便居中缩放
if bounding_rect.width() < 1 or bounding_rect.height() < 1:
bounding_rect = QRectF(0, 0, 220, 220)
bounding_rect.adjust(-10, -10, 10, 10)
self.scene.setSceneRect(bounding_rect)
self.fitInView(self.scene.sceneRect(), Qt.AspectRatioMode.KeepAspectRatio)
if title:
title_lbl = QLabel(title)
title_lbl.setStyleSheet("color: #aaaaaa; font-size: 13px; font-weight: 500; border: none;")
layout.addWidget(title_lbl)
self.content_layout = layout
def add_row(self, label, value_widget):
row = QHBoxLayout()
row.setSpacing(8)
lbl = QLabel(label)
lbl.setStyleSheet("color: #cccccc; font-size: 15px; border: none;")
lbl.setFixedWidth(70)
row.addWidget(lbl)
row.addWidget(value_widget, 1)
self.content_layout.addLayout(row)
class TempGauge(QWidget):
"""温度计指示器"""
def __init__(self, label="Tool", parent=None):
super().__init__(parent)
self.setFixedSize(100, 80)
self._label = label
self._actual = 0.0
self._target = 0.0
def set_value(self, actual, target):
self._actual = actual
self._target = target
self.update()
def paintEvent(self, event):
p = QPainter(self)
p.setRenderHint(QPainter.RenderHint.Antialiasing)
w, h = self.width(), self.height()
# 背景条
bar_x, bar_w = 16, 20
bar_y, bar_h = 10, 56
p.setPen(QPen(QColor("#555555"), 1))
p.setBrush(QBrush(QColor("#2a2a2a")))
p.drawRoundedRect(bar_x, bar_y, bar_w, bar_h, 4, 4)
# 填充柱(按温度比例,最高 300°C
ratio = min(max(self._actual / 300, 0), 1)
fill_h = int((bar_h - 4) * ratio)
if fill_h > 0:
grad = QLinearGradient(0, bar_y + bar_h, 0, bar_y)
grad.setColorAt(0, QColor("#f57c00"))
grad.setColorAt(1, QColor("#e53935") if self._actual > 200 else QColor("#ffb74d"))
p.setBrush(QBrush(grad))
p.setPen(Qt.PenStyle.NoPen)
p.drawRoundedRect(bar_x + 2, bar_y + bar_h - 2 - fill_h, bar_w - 4, fill_h, 3, 3)
# 目标值标记线
if self._target > 0:
tgt_y = bar_y + bar_h - int((bar_h - 4) * min(self._target / 300, 1))
p.setPen(QPen(QColor("#ffffff"), 2))
p.drawLine(bar_x - 2, tgt_y, bar_x + bar_w + 2, tgt_y)
# 文字
font = QFont("sans-serif", 11, QFont.Weight.Bold)
p.setFont(font)
p.setPen(QPen(QColor("#e0e0e0")))
p.drawText(44, 16, w - 44, 24, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter,
f"{self._actual:.0f}°")
if self._target > 0:
font2 = QFont("sans-serif", 10)
p.setFont(font2)
p.setPen(QPen(QColor("#888888")))
p.drawText(44, 34, w - 44, 20, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter,
f"{self._target:.0f}°")
font3 = QFont("sans-serif", 10, QFont.Weight.Bold)
p.setFont(font3)
p.setPen(QPen(QColor("#aaaaaa")))
p.drawText(0, h - 20, w, 20, Qt.AlignmentFlag.AlignCenter, self._label)
# ── GCode 2D 预览(暂注释,待开发)─────────────────────
# class GCode2DPreviewWidget(QGraphicsView):
# def __init__(self, parent=None):
# super().__init__(parent)
# self.scene = QGraphicsScene(self)
# self.setScene(self.scene)
# self.setStyleSheet("background-color: #111111; border-radius: 5px; border: 1px solid #666;")
# self.setRenderHint(QPainter.RenderHint.Antialiasing)
# self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
# self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
# self.scale(1, -1)
#
# def draw_paths(self, lines_data):
# self.scene.clear()
# bounding_rect = QRectF()
# for color_str, data in lines_data.items():
# points = data.get("points", [])
# line_width = data.get("line_width", 2)
# if not points:
# continue
# path = QPainterPath()
# if isinstance(points[0][0], (int, float)):
# path.moveTo(float(points[0][0]), float(points[0][1]))
# for pt in points[1:]:
# path.lineTo(float(pt[0]), float(pt[1]))
# else:
# for line_pts in points:
# if not line_pts:
# continue
# path.moveTo(float(line_pts[0][0]), float(line_pts[0][1]))
# for pt in line_pts[1:]:
# path.lineTo(float(pt[0]), float(pt[1]))
# path_item = QGraphicsPathItem(path)
# pen_color = QColor(color_str) if QColor.isValidColor(color_str) else QColor("white")
# pen = QPen(pen_color)
# pen.setWidth(int(line_width))
# pen.setCosmetic(True)
# path_item.setPen(pen)
# self.scene.addItem(path_item)
# bounding_rect = bounding_rect.united(path.boundingRect())
# if bounding_rect.width() < 1 or bounding_rect.height() < 1:
# bounding_rect = QRectF(0, 0, 220, 220)
# bounding_rect.adjust(-10, -10, 10, 10)
# self.scene.setSceneRect(bounding_rect)
# self.fitInView(self.scene.sceneRect(), Qt.AspectRatioMode.KeepAspectRatio)
# ── 状态页面 ────────────────────────────────────────────
class StatusPage(QWidget):
def __init__(self, api_client, parent=None):
super().__init__(parent)
self.api_client = api_client
self.file_name = "None"
self.progress = 0
self.progress = 0.0
self.display_name = "None"
self.state = "Unknown"
self.print_time = 0
self.print_time_left = 0
self.tool_temp_actual = 0.0
self.tool_temp_target = 0.0
self.bed_temp_actual = 0.0
self.bed_temp_target = 0.0
self.config_parser = ConfigParse()
self.config_parser.config_changed.connect(self._on_config_changed)
self.gcode_dir = self.config_parser.gcode_dir
self.init_ui()
self.timer = QTimer(self)
self.timer.timeout.connect(self.update_status)
self.timer.start(1000)
self.update_status()
def _on_config_changed(self, config_instance):
self.gcode_dir = self.config_parser.gcode_dir
def fresh_status_valve(self):
data = self.api_client.get_status()
# print("Status Data:", data)
if data:
status = data.get("status", {})
job = data.get("job", {})
self.file_name = job.get("job", {}).get("file", {}).get("name", "None")
self.progress = job.get("progress", {}).get("completion", 0)
self.progress = job.get("progress", {}).get("completion", 0) or 0
self.display_name = job.get("job", {}).get("file", {}).get("display_name", "None")
self.state = status.get("state", {}).get("text", "Offline")
self.print_time = job.get("progress", {}).get("printTime", 0)
self.print_time_left = job.get("progress", {}).get("printTimeLeft", 0)
self.print_time = job.get("progress", {}).get("printTime", 0) or 0
self.print_time_left = job.get("progress", {}).get("printTimeLeft", 0) or 0
temp = status.get("temperature", {})
self.tool_temp_actual = (temp.get("tool0", {}) or {}).get("actual", 0) or 0
self.tool_temp_target = (temp.get("tool0", {}) or {}).get("target", 0) or 0
self.bed_temp_actual = (temp.get("bed", {}) or {}).get("actual", 0) or 0
self.bed_temp_target = (temp.get("bed", {}) or {}).get("target", 0) or 0
# print(f"Updated Status: state={self.state}, file_name={self.file_name}, progress={self.progress:.1f}%, display_name={self.display_name}")
def format_time(self, seconds):
if seconds is None:
@staticmethod
def format_time(seconds):
if seconds is None or seconds == 0:
return "N/A"
m, s = divmod(seconds, 60)
m, s = divmod(int(seconds), 60)
h, m = divmod(m, 60)
if h > 0:
return f"{int(h)}h {int(m)}m {int(s)}s"
return f"{h}h {m:02d}m {s:02d}s"
elif m > 0:
return f"{int(m)}m {int(s)}s"
return f"{m}m {s:02d}s"
else:
return f"{int(s)}s"
return f"{s}s"
def _make_card(self, title=""):
card = CardFrame(title)
return card
def _truncate(self, text, max_len=22):
return text if len(text) <= max_len else text[:max_len - 2] + ""
def init_ui(self):
self.fresh_status_valve()
main_layout = QHBoxLayout(self)
self.left_frame = QFrame()
self.left_frame.setStyleSheet("background-color: #444444; border-radius: 10px; color: white;")
left_layout = QVBoxLayout(self.left_frame)
self.lbl_status = QLabel(f"Status: {self.state}")
left_layout.addWidget(self.lbl_status)
self.lbl_job = QLabel(f"File: {self.display_name}")
left_layout.addWidget(self.lbl_job)
self.lbl_progress = QLabel(f"Progress: {self.progress if self.progress else 0:.1f}%")
left_layout.addWidget(self.lbl_progress)
self.lbl_print_time = QLabel(f"Print Time: {self.format_time(self.print_time)}")
left_layout.addWidget(self.lbl_print_time)
self.lbl_print_time_left = QLabel(f"Print Time Left: {self.format_time(self.print_time_left)}")
left_layout.addWidget(self.lbl_print_time_left)
left_layout.addStretch()
#TODO: 3D Gcode View in right frame, use QML and QtQuick3D to render the Gcode vertices, pass the progress to QML to show the current layer
# Due to the complexity of parsing Gcode and rendering it in 3D, this part will be implemented in a separate thread to avoid blocking the UI, and the vertices will be passed to QML for rendering. The progress will also be passed to QML to show the current layer being printed.
# Load QtQuick3D View
# self.right_frame = QFrame()
# right_layout = QVBoxLayout(self.right_frame)
# self.view = QQuickView()
# self.view.setResizeMode(QQuickView.ResizeMode.SizeRootObjectToView)
# qml_file = os.path.join(os.path.dirname(__file__), "gcode_view2.qml")
# self.view.setSource(QUrl.fromLocalFile(qml_file))
# gcode_data = self.load_gcode_vertices(os.path.join(GCODE_DIR, self.file_name))
# self.view.rootContext().setContextProperty("gcodeData", gcode_data)
# container = QWidget.createWindowContainer(self.view, self)
# right_layout.addWidget(container)
main_layout.setContentsMargins(8, 8, 8, 8)
main_layout.setSpacing(8)
self.right_frame = QFrame()
self.right_frame.setStyleSheet("background-color: #444444; border-radius: 10px;")
right_layout = QVBoxLayout(self.right_frame)
self.gcode_view = GCode2DPreviewWidget()
right_layout.addWidget(self.gcode_view)
main_layout.addWidget(self.left_frame, 1)
main_layout.addWidget(self.right_frame, 2)
# ── 左侧信息面板 ──────────────────────────────────
left_frame = QFrame()
left_frame.setStyleSheet("background-color: #333333; border-radius: 10px;")
left_layout = QVBoxLayout(left_frame)
left_layout.setContentsMargins(10, 10, 10, 10)
left_layout.setSpacing(6)
# — 状态徽章 —
self._status_badge = QLabel()
self._status_badge.setFixedHeight(40)
self._status_badge.setAlignment(Qt.AlignmentFlag.AlignCenter)
font_badge = QFont("sans-serif", 18, QFont.Weight.Bold)
self._status_badge.setFont(font_badge)
left_layout.addWidget(self._status_badge)
# — 文件信息卡片 —
self._file_card = CardFrame("当前文件")
self._file_name_lbl = QLabel("--")
self._file_name_lbl.setStyleSheet("color: #ffffff; font-size: 16px; font-weight: 600; border: none;")
self._file_name_lbl.setWordWrap(True)
self._file_card.content_layout.addWidget(self._file_name_lbl)
left_layout.addWidget(self._file_card)
# — 进度卡片 —
self._progress_card = CardFrame("打印进度")
# 进度条
self._progress_bar = QProgressBar()
self._progress_bar.setTextVisible(True)
self._progress_bar.setFixedHeight(28)
self._progress_bar.setStyleSheet("""
QProgressBar {
background-color: #2a2a2a;
border: 1px solid #555;
border-radius: 6px;
text-align: center;
color: white;
font-size: 14px;
font-weight: bold;
}
QProgressBar::chunk {
background-color: #4CAF50;
border-radius: 5px;
}
""")
self._progress_card.content_layout.addWidget(self._progress_bar)
# 时间行
time_row = QHBoxLayout()
time_row.setSpacing(10)
self._time_elapsed_lbl = QLabel("已用: --")
self._time_elapsed_lbl.setStyleSheet("color: #bbbbbb; font-size: 14px; border: none;")
self._time_left_lbl = QLabel("剩余: --")
self._time_left_lbl.setStyleSheet("color: #bbbbbb; font-size: 14px; border: none;")
time_row.addWidget(self._time_elapsed_lbl)
time_row.addStretch()
time_row.addWidget(self._time_left_lbl)
self._progress_card.content_layout.addLayout(time_row)
left_layout.addWidget(self._progress_card)
# — 温度卡片 —
self._temp_card = CardFrame("温度")
temp_row = QHBoxLayout()
temp_row.setSpacing(8)
self._tool_gauge = TempGauge("喷头")
self._bed_gauge = TempGauge("热床")
temp_row.addWidget(self._tool_gauge)
temp_row.addWidget(self._bed_gauge)
temp_row.addStretch()
self._temp_card.content_layout.addLayout(temp_row)
left_layout.addWidget(self._temp_card)
left_layout.addStretch()
# ── 右侧预留区域 ─────────────────────────────
right_frame = QFrame()
right_frame.setStyleSheet("background-color: #3a3a3a; border-radius: 10px;")
right_layout = QVBoxLayout(right_frame)
right_layout.setContentsMargins(6, 6, 6, 6)
placeholder = QLabel("GCode 预览\n⚙ 待开发")
placeholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
placeholder.setStyleSheet("color: #666666; font-size: 24px; font-weight: 600; border: none;")
right_layout.addWidget(placeholder)
main_layout.addWidget(left_frame, 2)
main_layout.addWidget(right_frame, 3)
def update_status(self):
self.fresh_status_valve()
self.lbl_status.setText(f"Status: {self.state}")
self.lbl_job.setText(f"File: {self.display_name}")
self.lbl_progress.setText(f"Progress: {self.progress if self.progress else 0:.1f}%")
# Pass progress to QML
# root_obj = self.view.rootObject()
# if root_obj:
# root_obj.setProperty("progress", prog if prog else 0)
# 状态徽章
status_key = self.state.split()[0] if self.state else "Offline"
color = STATUS_COLORS.get(status_key, "#9E9E9E")
label = STATUS_LABELS.get(status_key, self.state)
self._status_badge.setText(f"{label}")
self._status_badge.setStyleSheet(
f"background-color: #2a2a2a; color: {color}; "
f"border: 2px solid {color}; border-radius: 8px; "
f"font-size: 18px; font-weight: bold; padding: 4px;"
)
# 文件
self._file_name_lbl.setText(self._truncate(self.display_name, 28))
# 进度
prog = min(max(self.progress, 0), 100)
self._progress_bar.setValue(int(prog))
self._progress_bar.setFormat(f"{prog:.1f}%")
self._time_elapsed_lbl.setText(f"已用: {self.format_time(self.print_time)}")
self._time_left_lbl.setText(f"剩余: {self.format_time(self.print_time_left)}")
# 打印中时进度条变蓝
if self.state.startswith("Printing"):
self._progress_bar.setStyleSheet("""
QProgressBar {
background-color: #2a2a2a;
border: 1px solid #555;
border-radius: 6px;
text-align: center;
color: white;
font-size: 14px;
font-weight: bold;
}
QProgressBar::chunk {
background-color: #2196F3;
border-radius: 5px;
}
""")
else:
self._progress_bar.setStyleSheet("""
QProgressBar {
background-color: #2a2a2a;
border: 1px solid #555;
border-radius: 6px;
text-align: center;
color: white;
font-size: 14px;
font-weight: bold;
}
QProgressBar::chunk {
background-color: #4CAF50;
border-radius: 5px;
}
""")
# 温度
self._tool_gauge.set_value(self.tool_temp_actual, self.tool_temp_target)
self._bed_gauge.set_value(self.bed_temp_actual, self.bed_temp_target)