基本界面达成

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

@@ -1,26 +1,535 @@
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,
QFrame, QGridLayout, QSizePolicy, QLineEdit,
)
from PyQt6.QtCore import Qt, QTimer, QPointF, QRectF
from PyQt6.QtGui import QFont, QPainter, QColor, QBrush, QPen, QDoubleValidator
from utils.config_parse import ConfigParse
MOVE_STEP = 10 # 每次点击移动 mm (保留备用)
MOVE_DETECT_MS = 100
class JoystickWidget(QWidget):
"""圆形 XY 摇杆 — 拖动小圆控制方向,松开回中,越远越快"""
def __init__(self, parent=None, max_speed=3000):
super().__init__(parent)
self.setFixedSize(320, 320)
self.setMouseTracking(True)
self._max_speed = max_speed # 最大进给速度 mm/min
self._radius_outer = 140 # 外圈半径
self._radius_inner = 30 # 内圈小圆半径
self._deadzone = 10 # 死区像素
# 当前小圆偏移(相对中心,像素)
self._dx = 0.0
self._dy = 0.0
self._pressed = False
# 定时发送 GCode
self._move_timer = QTimer(self)
self._move_timer.setInterval(120) # ~8次/秒
self._move_timer.timeout.connect(self._emit_move)
# 回调(上层设置)
self.on_move = None # callable(dx_norm, dy_norm, feedrate)
def _emit_move(self):
"""定时器触发:计算速度方向并回调"""
dist = (self._dx ** 2 + self._dy ** 2) ** 0.5
if dist < self._deadzone:
return
ratio = min(dist / (self._radius_outer - self._radius_inner), 1.0)
# 速度映射30% ~ 100% * max_speed
speed = int((0.3 + ratio * 0.7) * self._max_speed)
if dist > 0:
nx = self._dx / dist
ny = self._dy / dist
if self.on_move:
self.on_move(nx, ny, speed)
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
cx, cy = self.width() / 2, self.height() / 2
# 外圈背景
painter.setBrush(QBrush(QColor("#2a2a2a")))
painter.setPen(QPen(QColor("#666666"), 3))
painter.drawEllipse(QPointF(cx, cy), self._radius_outer, self._radius_outer)
# 十字准线(浅)
painter.setPen(QPen(QColor("#555555"), 1))
painter.drawLine(int(cx - self._radius_outer), int(cy),
int(cx + self._radius_outer), int(cy))
painter.drawLine(int(cx), int(cy - self._radius_outer),
int(cx), int(cy + self._radius_outer))
# 小圆(可拖动)
knob_x = cx + self._dx
knob_y = cy + self._dy
painter.setBrush(QBrush(QColor("#4a9fc8" if self._pressed else "#6fb8dd")))
painter.setPen(QPen(QColor("#2f6f91"), 2))
painter.drawEllipse(QPointF(knob_x, knob_y), self._radius_inner, self._radius_inner)
# 中心小点标记
painter.setBrush(QBrush(QColor("#888888")))
painter.setPen(Qt.PenStyle.NoPen)
painter.drawEllipse(QPointF(cx, cy), 4, 4)
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self._pressed = True
self._update_knob(event.position())
self._move_timer.start()
def mouseMoveEvent(self, event):
if self._pressed:
self._update_knob(event.position())
def mouseReleaseEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self._pressed = False
self._move_timer.stop()
self._dx = 0.0
self._dy = 0.0
self.update()
def _update_knob(self, pos):
cx, cy = self.width() / 2, self.height() / 2
dx = pos.x() - cx
dy = pos.y() - cy
max_r = self._radius_outer - self._radius_inner
dist = (dx ** 2 + dy ** 2) ** 0.5
if dist > max_r:
dx = dx / dist * max_r
dy = dy / dist * max_r
self._dx = dx
self._dy = dy
self.update()
class ZFaderWidget(QWidget):
"""Z 轴拨杆 — 上下拖动控制 Z 速度,松开回中,越远越快"""
def __init__(self, parent=None, max_speed=200):
super().__init__(parent)
self.setFixedSize(120, 320)
self.setMouseTracking(True)
self._max_speed = max_speed
self._track_h = 260 # 滑道高度
self._knob_h = 40 # 滑块高度
self._knob_w = 80 # 滑块宽度
self._deadzone = 8
self._dy = 0.0 # 相对中心的偏移
self._pressed = False
self._move_timer = QTimer(self)
self._move_timer.setInterval(MOVE_DETECT_MS)
self._move_timer.timeout.connect(self._emit_move)
self.on_move = None # callable(direction, feedrate) direction: +1 or -1
def _emit_move(self):
dist = abs(self._dy)
if dist < self._deadzone:
return
max_dist = (self._track_h - self._knob_h) / 2
ratio = min(dist / max_dist, 1.0)
# speed = int((0.3 + ratio * 0.7) * self._max_speed / 10)
speed = ratio * self._max_speed / (1000/MOVE_DETECT_MS)
direction = 1 if self._dy < 0 else -1 # dy<0 → 向上
if self.on_move:
self.on_move(direction, speed)
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
cx = self.width() / 2
# 滑道
rect = QRectF(cx - 12, (self.height() - self._track_h) / 2, 24, self._track_h)
painter.setBrush(QBrush(QColor("#2a2a2a")))
painter.setPen(QPen(QColor("#666666"), 2))
painter.drawRoundedRect(rect, 12, 12)
# 中心刻度线
mid_y = self.height() / 2
painter.setPen(QPen(QColor("#777777"), 2))
painter.drawLine(int(cx - 20), int(mid_y), int(cx + 20), int(mid_y))
# 滑块
knob_center_y = mid_y + self._dy
knob_y = knob_center_y - self._knob_h / 2
knob_rect = QRectF(cx - self._knob_w / 2, knob_y, self._knob_w, self._knob_h)
color = "#4a9fc8" if self._pressed else "#6fb8dd"
painter.setBrush(QBrush(QColor(color)))
painter.setPen(QPen(QColor("#2f6f91"), 2))
painter.drawRoundedRect(knob_rect, 10, 10)
# Z 标签
painter.setPen(QPen(QColor("#a0a0a0"), 1))
font = QFont("sans-serif", 14, QFont.Weight.Bold)
painter.setFont(font)
painter.drawText(knob_rect, Qt.AlignmentFlag.AlignCenter, "Z")
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self._pressed = True
self._update_knob(event.position().y())
self._move_timer.start()
def mouseMoveEvent(self, event):
if self._pressed:
self._update_knob(event.position().y())
def mouseReleaseEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self._pressed = False
self._move_timer.stop()
self._dy = 0.0
self.update()
def _update_knob(self, py):
mid_y = self.height() / 2
max_d = (self._track_h - self._knob_h) / 2
dy = py - mid_y
dy = max(-max_d, min(max_d, dy))
self._dy = dy
self.update()
class ControlPage(QWidget):
def __init__(self, api_client, parent=None):
super().__init__(parent)
self.api_client = api_client
self.config_parser = ConfigParse()
self.config_parser.config_changed.connect(self._on_config_changed)
self._load_limits()
self._load_speeds()
# 当前坐标(本地跟踪)
self.pos_x = 0.0
self.pos_y = 0.0
self.pos_z = 0.0
self._is_printing = False
self._homed = False # 是否已轴回零
self._motor_on = True # 电机是否已使能默认True关电机后置False
self.init_ui()
self._sync_inputs()
self._apply_state()
self._start_status_poll()
def _load_speeds(self):
ms = self.config_parser.move_max_speed or {}
self.move_speed_xy = ms.get("xy", 3000)
self.move_speed_z = ms.get("z", 200)
def _start_status_poll(self):
self._status_timer = QTimer(self)
self._status_timer.timeout.connect(self._poll_status)
self._status_timer.start(2000)
def _poll_status(self):
"""定时轮询打印机状态,更新按钮启用状态和坐标"""
try:
data = self.api_client.get_status()
flags = data.get("status", {}).get("state", {}).get("flags", {})
printing = flags.get("printing", False)
paused = flags.get("paused", False)
self._is_printing = printing or paused
except Exception:
self._is_printing = False
self._update_buttons()
def _load_limits(self):
ma = self.config_parser.move_axis_area or {}
self.x_min = ma.get("x_min", 0)
self.x_max = ma.get("x_max", 300)
self.y_min = ma.get("y_min", 0)
self.y_max = ma.get("y_max", 300)
self.z_min = ma.get("z_min", 0)
self.z_max = ma.get("z_max", 300)
def _on_config_changed(self, config_instance):
self._load_limits()
self._load_speeds()
print(f"z-max-s:{self.move_speed_z}")
# ── 状态管理 ──────────────────────────────────────────
def _is_control_enabled(self):
"""摇杆/拨杆/坐标输入是否可用:已回零 + 电机已使能 + 非打印中"""
return self._homed and self._motor_on and not self._is_printing
def _apply_state(self):
"""根据回零/电机状态同步所有控件"""
enabled = self._is_control_enabled()
self.joystick.setEnabled(enabled)
self.z_fader.setEnabled(enabled)
for inp in (self.input_x, self.input_y, self.input_z):
inp.setEnabled(enabled)
if not enabled:
inp.setText("---")
if enabled:
self._sync_inputs()
def _sync_inputs(self):
"""将 pos 值同步到输入框"""
if self._is_control_enabled():
self.input_x.setText(f"{self.pos_x:.3f}")
self.input_y.setText(f"{self.pos_y:.3f}")
self.input_z.setText(f"{self.pos_z:.3f}")
def _on_coord_changed(self):
"""输入框手动输入触发运动"""
if not self._is_control_enabled():
return
try:
tx = float(self.input_x.text())
ty = float(self.input_y.text())
tz = float(self.input_z.text())
except ValueError:
return
tx = max(self.x_min, min(self.x_max, tx))
ty = max(self.y_min, min(self.y_max, ty))
tz = max(self.z_min, min(self.z_max, tz))
gcode = f"G1 X{tx:.3f} Y{ty:.3f} Z{tz:.3f} F3000"
self.api_client.send_gcode(gcode)
self.pos_x, self.pos_y, self.pos_z = tx, ty, tz
self._sync_inputs()
def _update_buttons(self):
printing = self._is_printing
self.btn_pause.setEnabled(printing)
self.btn_stop.setEnabled(printing)
idle = not printing
self.btn_home.setEnabled(idle)
self.btn_level.setEnabled(idle)
self.btn_motor_off.setEnabled(idle)
self._apply_state()
# ── GCode 发送 ──────────────────────────────────────────
def _send_move(self, x=None, y=None, z=None, feedrate=None):
if not self._is_control_enabled():
return
target_x = self.pos_x if x is None else x
target_y = self.pos_y if y is None else y
target_z = self.pos_z if z is None else z
target_x = max(self.x_min, min(self.x_max, target_x))
target_y = max(self.y_min, min(self.y_max, target_y))
target_z = max(self.z_min, min(self.z_max, target_z))
f = feedrate or 3000
gcode = f"G1 X{target_x:.3f} Y{target_y:.3f} Z{target_z:.3f} F{f}"
self.api_client.send_gcode(gcode)
self.pos_x, self.pos_y, self.pos_z = target_x, target_y, target_z
self._sync_inputs()
# ── 摇杆/拨杆回调 ──
def _on_joystick_move(self, nx, ny, speed):
if not self._is_control_enabled():
return
step = 5.0
dx = nx * step
dy = ny * step
tx = max(self.x_min, min(self.x_max, self.pos_x + dx))
ty = max(self.y_min, min(self.y_max, self.pos_y + dy))
gcode = f"G1 X{tx:.1f} Y{ty:.1f} F{speed}"
self.api_client.send_gcode(gcode)
self.pos_x, self.pos_y = tx, ty
self._sync_inputs()
def _on_fader_move(self, direction, speed):
if not self._is_control_enabled():
return
print(f"d:{direction} s:{speed}")
step = speed * MOVE_DETECT_MS / 1000
tz = self.pos_z + direction * step
tz = max(self.z_min, min(self.z_max, tz))
gcode = f"G1 Z{tz:.3f} F{speed}"
self.api_client.send_gcode(gcode)
self.pos_z = tz
self._sync_inputs()
# ── 指令按钮 ──
def _cmd_pause(self):
self.api_client.pause_print()
def _cmd_stop(self):
self.api_client.stop_print()
def _cmd_home(self):
self.api_client.home_axes(["x", "y", "z"])
self.pos_x = self.pos_y = self.pos_z = 0.0
self._homed = True
self._motor_on = True
self._sync_inputs()
self._apply_state()
def _cmd_level(self):
self.api_client.auto_leveling()
def _cmd_motor_off(self):
self.api_client.off_motors()
self._motor_on = False
self._apply_state()
# ── UI 构建 ──────────────────────────────────────────────
def init_ui(self):
layout = QVBoxLayout()
label = QLabel("控制页面")
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(label)
# 添加一些控制按钮
button_layout = QHBoxLayout()
start_button = QPushButton("开始打印")
stop_button = QPushButton("停止打印")
button_layout.addWidget(start_button)
button_layout.addWidget(stop_button)
layout.addLayout(button_layout)
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
main_layout = QHBoxLayout(self)
main_layout.setContentsMargins(14, 14, 14, 14)
main_layout.setSpacing(14)
self.setLayout(layout)
# =========================== 左栏 ===========================
left_panel = QFrame()
left_panel.setStyleSheet("background-color: #3f3f3f; border-radius: 10px;")
left_panel.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
left_layout = QVBoxLayout(left_panel)
left_layout.setContentsMargins(16, 14, 16, 14)
left_layout.setSpacing(8)
left_title = QLabel("控制")
left_title.setAlignment(Qt.AlignmentFlag.AlignCenter)
left_title.setStyleSheet("color: #efefef; font-size: 22px; font-weight: 700;")
left_layout.addWidget(left_title)
btn_style = """
QPushButton {
min-height: 50px;
font-size: 18px;
font-weight: 600;
color: #f8f8f8;
background-color: #555555;
border: 2px solid #888888;
border-radius: 8px;
padding: 4px 12px;
}
QPushButton:hover {
background-color: #636363;
border-color: #aaaaaa;
}
QPushButton:pressed {
background-color: #3d3d3d;
border-color: #5a9fcf;
}
QPushButton:disabled {
color: #606060;
background-color: #3a3a3a;
border-color: #505050;
}
"""
grid_btns = QGridLayout()
grid_btns.setSpacing(8)
self.btn_pause = QPushButton("⏸ 暂停打印")
self.btn_pause.setStyleSheet(btn_style)
self.btn_pause.clicked.connect(self._cmd_pause)
grid_btns.addWidget(self.btn_pause, 0, 0)
self.btn_stop = QPushButton("⏹ 停止打印")
self.btn_stop.setStyleSheet(btn_style)
self.btn_stop.clicked.connect(self._cmd_stop)
grid_btns.addWidget(self.btn_stop, 0, 1)
self.btn_home = QPushButton("⌂ 轴回零")
self.btn_home.setStyleSheet(btn_style)
self.btn_home.clicked.connect(self._cmd_home)
grid_btns.addWidget(self.btn_home, 1, 0)
self.btn_level = QPushButton("◉ 自动调平")
self.btn_level.setStyleSheet(btn_style)
self.btn_level.clicked.connect(self._cmd_level)
grid_btns.addWidget(self.btn_level, 1, 1)
self.btn_motor_off = QPushButton("⏻ 关电机")
self.btn_motor_off.setStyleSheet(btn_style)
self.btn_motor_off.clicked.connect(self._cmd_motor_off)
grid_btns.addWidget(self.btn_motor_off, 2, 0, 1, 2)
left_layout.addLayout(grid_btns)
main_layout.addWidget(left_panel, 2)
# =========================== 右栏 ===========================
right_panel = QFrame()
right_panel.setStyleSheet("background-color: #3f3f3f; border-radius: 10px;")
right_panel.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
right_layout = QVBoxLayout(right_panel)
right_layout.setContentsMargins(16, 14, 16, 14)
right_layout.setSpacing(10)
# ── 坐标输入 ──
coord_row = QHBoxLayout()
coord_row.setSpacing(12)
coord_row.setAlignment(Qt.AlignmentFlag.AlignCenter)
input_style = """
QLineEdit {
min-width: 80px;
max-width: 120px;
min-height: 44px;
font-size: 24px;
font-weight: 700;
color: #e0e0e0;
background-color: #2a2a2a;
border: 2px solid #646464;
border-radius: 8px;
padding: 4px 10px;
text-align: center;
}
QLineEdit:focus {
border-color: #5a9fcf;
}
QLineEdit:disabled {
color: #606060;
background-color: #222222;
border-color: #444444;
}
"""
for axis, val in [("X", 0.0), ("Y", 0.0), ("Z", 0.0)]:
lbl = QLabel(f"{axis}:")
lbl.setStyleSheet("color: #d0d0d0; font-size: 24px; font-weight: 700;")
coord_row.addWidget(lbl)
inp = QLineEdit(f"{val:.1f}")
inp.setStyleSheet(input_style)
inp.setAlignment(Qt.AlignmentFlag.AlignCenter)
inp.setValidator(QDoubleValidator(-9999, 9999, 1))
inp.returnPressed.connect(self._on_coord_changed)
setattr(self, f"input_{axis.lower()}", inp)
coord_row.addWidget(inp)
right_layout.addLayout(coord_row)
# ── 操作器 ──
manip_row = QHBoxLayout()
manip_row.setSpacing(14)
manip_row.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.joystick = JoystickWidget(max_speed=self.move_speed_xy)
self.joystick.on_move = self._on_joystick_move
manip_row.addWidget(self.joystick)
self.z_fader = ZFaderWidget(max_speed=self.move_speed_z)
self.z_fader.on_move = self._on_fader_move
manip_row.addWidget(self.z_fader)
right_layout.addLayout(manip_row)
main_layout.addWidget(right_panel, 3)
self._update_buttons()