Files
Feeding_control_system/view/widgets/system_settings_dialog.py

1435 lines
50 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import configparser
import os
import sys
# 添加项目根目录到Python路径确保可以直接运行此文件
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
if BASE_DIR not in sys.path:
sys.path.insert(0, BASE_DIR)
from PySide6.QtWidgets import (
QApplication,
QDialog,
QVBoxLayout,
QHBoxLayout,
QLabel,
QWidget,
QPushButton,
QCheckBox,
QLineEdit,
QComboBox,
QScrollArea,
QStackedWidget,
QSpacerItem,
QSizePolicy,
QMessageBox,
QFrame,
)
from PySide6.QtGui import QPixmap, QFont, QPainter, QIcon, QIntValidator
from PySide6.QtCore import Qt, Signal
from utils.image_paths import ImagePaths
from view.widgets.arrow_combo_box import ArrowComboBox
"""
系统设置弹窗: 包含投料参数设置、IP配置、AI算法参数设置等多个Tab页
"""
# 配置文件路径
FEED_PARAMS_CONFIG = os.path.join(BASE_DIR, "config", "feed_params_config.ini")
CAMERA_CONFIG = os.path.join(BASE_DIR, "config", "camera_config.ini")
OTHER_IP_CONFIG = os.path.join(BASE_DIR, "config", "other_ip_config.ini")
# ==================== 通用样式 ====================
COMMON_LABEL_STYLE = "color: #9fbfd4; font-size: 14px; background: transparent;"
COMMON_EDIT_STYLE = """
QLineEdit {
background-color: #0a1a3a;
color: #13fffc;
border: 1px solid #017cbc;
border-radius: 3px;
padding: 4px 8px;
font-size: 14px;
}
QLineEdit:focus {
border: 1px solid #13fffc;
}
"""
COMMON_BTN_STYLE = """
QPushButton {
background-color: #001c82;
color: #9fbfd4;
border: 1px solid #017cbc;
font-size: 14px;
font-weight: Bold;
border-radius: 3px;
padding: 6px 20px;
}
QPushButton:hover {
color: #2dcedb;
border: 1px solid #13fffc;
}
"""
TAB_BTN_NORMAL_STYLE = """
QPushButton {
background-color: #0a1a3a;
color: #9fbfd4;
border: none;
border-bottom: 2px solid transparent;
font-size: 15px;
font-weight: Bold;
text-align: left;
padding: 10px 16px;
}
QPushButton:hover {
color: #13fffc;
background-color: #0d2255;
}
"""
TAB_BTN_SELECTED_STYLE = """
QPushButton {
background-color: #0d2255;
color: #13fffc;
border: none;
border-left: 3px solid #13fffc;
font-size: 15px;
font-weight: Bold;
text-align: left;
padding: 10px 13px;
}
"""
CHECKBOX_STYLE = """
QCheckBox {
color: #9fbfd4;
font-size: 14px;
spacing: 8px;
background: transparent;
}
QCheckBox::indicator {
width: 18px;
height: 18px;
border: 1px solid #017cbc;
border-radius: 3px;
background-color: #0a1a3a;
}
QCheckBox::indicator:checked {
background-color: #13fffc;
border: 1px solid #13fffc;
}
"""
COMBO_STYLE = """
QComboBox {
background-color: #0a1a3a;
color: #13fffc;
border: 1px solid #017cbc;
border-radius: 3px;
padding: 4px 20px 4px 8px;
font-size: 14px;
min-width: 55px;
}
QComboBox:focus {
border: 1px solid #13fffc;
}
QComboBox::drop-down {
subcontrol-origin: padding;
subcontrol-position: right center;
width: 20px;
border: none;
border-left: 1px solid #017cbc;
}
QComboBox::down-arrow {
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 6px solid #13fffc;
}
QComboBox QAbstractItemView {
background-color: #0a1a3a;
color: #13fffc;
border: 1px solid #017cbc;
selection-background-color: #0d2255;
outline: none;
}
QComboBox QAbstractItemView::item {
height: 25px;
padding: 2px 6px;
}
"""
# 频率可编辑下拉框专用样式
FREQ_COMBO_STYLE = """
QComboBox {
background-color: #0a1a3a;
color: #13fffc;
border: 1px solid #017cbc;
border-radius: 3px;
padding: 4px 25px 4px 8px;
font-size: 14px;
font-weight: Bold;
min-width: 70px;
}
QComboBox:focus {
border: 1px solid #13fffc;
}
QComboBox::drop-down {
subcontrol-origin: padding;
subcontrol-position: right center;
width: 22px;
border: none;
border-left: 1px solid #017cbc;
}
QComboBox::down-arrow {
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 6px solid #13fffc;
}
QComboBox QAbstractItemView {
background-color: #0a1a3a;
color: #13fffc;
border: 1px solid #017cbc;
selection-background-color: #0d2255;
font-size: 14px;
font-weight: Bold;
outline: none;
}
QComboBox QAbstractItemView::item {
height: 28px;
padding: 4px 8px;
}
QComboBox QAbstractItemView::item:hover {
background-color: #0d2255;
}
"""
SECTION_TITLE_STYLE = "color: #13fffc; font-size: 16px; font-weight: Bold; background: transparent; padding: 4px 0px;"
SEPARATOR_STYLE = "background-color: #017cbc; max-height: 1px; border: none;"
class SystemSettingsDialog(QDialog):
"""系统设置对话框"""
# 投料参数立即生效信号: (multi_stage_enabled, stages_data)
# stages_data: list of dict, 每个dict包含 mode, value, frequency
feed_params_apply = Signal(bool, list)
# IP配置立即生效信号
ip_config_apply = Signal()
def __init__(self, parent=None):
super().__init__(parent)
self.setAttribute(Qt.WA_TranslucentBackground)
self.setWindowFlags(Qt.FramelessWindowHint)
# 投料参数的阶段条目列表
self.stage_rows = []
# 相机IP编辑控件
self.camera_edits = {}
# 其他IP条目列表
self.other_ip_rows = []
# 频率范围
self.freq_min = 200
self.freq_max = 230
# 值范围配置
self.ratio_min = 0
self.ratio_max = 100
self.ratio_step = 20
self.time_min = 0
self.time_max = 300
self.time_step = 30
# 左侧Tab按钮列表
self.tab_buttons = []
self._load_frequency_range()
self._load_value_range()
self._init_ui()
self._load_feed_params()
self._load_camera_config()
self._load_other_ip_config()
def _load_frequency_range(self):
"""从配置文件读取频率范围"""
config = configparser.ConfigParser()
if os.path.exists(FEED_PARAMS_CONFIG):
config.read(FEED_PARAMS_CONFIG, encoding="utf-8")
if config.has_section("frequency_range"):
self.freq_min = config.getint("frequency_range", "min", fallback=200)
self.freq_max = config.getint("frequency_range", "max", fallback=230)
def _load_value_range(self):
"""从配置文件读取值范围配置"""
config = configparser.ConfigParser()
if os.path.exists(FEED_PARAMS_CONFIG):
config.read(FEED_PARAMS_CONFIG, encoding="utf-8")
if config.has_section("value_range"):
self.ratio_min = config.getint("value_range", "ratio_min", fallback=0)
self.ratio_max = config.getint("value_range", "ratio_max", fallback=100)
self.ratio_step = config.getint("value_range", "ratio_step", fallback=20)
self.time_min = config.getint("value_range", "time_min", fallback=0)
self.time_max = config.getint("value_range", "time_max", fallback=300)
self.time_step = config.getint("value_range", "time_step", fallback=30)
def _init_ui(self):
self._load_background()
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(32, 20, 32, 30)
main_layout.setSpacing(0)
# 1. 顶部区域(标题 + 关闭按钮)
self._add_top_area(main_layout)
# 2. 内容区域左侧Tab按钮 + 右侧内容)
content_layout = QHBoxLayout()
content_layout.setSpacing(12)
# 先创建右侧内容区域使用QStackedWidget切换
self.stacked_widget = QStackedWidget()
self.stacked_widget.setStyleSheet("background: transparent;")
# 创建各Tab页
self.stacked_widget.addWidget(self._create_feed_params_page()) # 0: 投料参数设置
self.stacked_widget.addWidget(self._create_ip_config_page()) # 1: IP配置
self.stacked_widget.addWidget(self._create_ai_params_page()) # 2: AI算法参数设置
# 3-6: 预留空白页
for _ in range(4):
placeholder = QWidget()
placeholder.setStyleSheet("background: transparent;")
placeholder_layout = QVBoxLayout(placeholder)
placeholder_label = QLabel("功能开发中...")
placeholder_label.setStyleSheet("color: #9fbfd4; font-size: 18px; background: transparent;")
placeholder_label.setAlignment(Qt.AlignCenter)
placeholder_layout.addWidget(placeholder_label)
self.stacked_widget.addWidget(placeholder)
# 再创建左侧Tab按钮区域会调用 _on_tab_clicked需要 stacked_widget 已存在)
self._add_left_tabs(content_layout)
content_layout.addWidget(self.stacked_widget)
main_layout.addLayout(content_layout)
def _load_background(self):
self.bg_pixmap = QPixmap(ImagePaths.DESPATCH_DETAILS_POPUP_BG)
if self.bg_pixmap.isNull():
self.setFixedSize(1000, 700)
else:
self.setFixedSize(self.bg_pixmap.size())
def _add_top_area(self, parent_layout):
top_layout = QHBoxLayout()
top_layout.setContentsMargins(0, 0, 0, 16)
top_layout.addStretch()
title_label = QLabel("系统设置")
font = QFont()
font.setPixelSize(24)
font.setLetterSpacing(QFont.AbsoluteSpacing, 2)
font.setBold(True)
title_label.setFont(font)
title_label.setStyleSheet("color: #13fffc; font-weight: Bold; background: transparent;")
title_label.setAlignment(Qt.AlignCenter)
top_layout.addWidget(title_label)
self._create_close_button(top_layout)
parent_layout.addLayout(top_layout)
def _create_close_button(self, parent_layout):
self.close_btn = QPushButton()
self.close_btn.setFixedSize(36, 36)
close_icon = QPixmap(ImagePaths.DESPATCH_DETAILS_CLOSE_ICON)
if not close_icon.isNull():
self.close_btn.setIcon(QIcon(close_icon))
self.close_btn.setStyleSheet("""
QPushButton {
background-color: transparent;
border: none;
padding: 0px;
}
QPushButton:hover {
background-color: red;
border-radius: 2px;
}
""")
self.close_btn.clicked.connect(self.close)
parent_layout.addStretch()
parent_layout.addWidget(self.close_btn)
def _add_left_tabs(self, parent_layout):
tab_widget = QWidget()
tab_widget.setFixedWidth(150)
tab_widget.setStyleSheet("background: transparent;")
tab_layout = QVBoxLayout(tab_widget)
tab_layout.setContentsMargins(0, 0, 0, 0)
tab_layout.setSpacing(19) # 左侧按钮之间的距离
tab_names = [
"投料参数设置",
"IP配置",
"AI算法参数设置",
"设置四",
"设置五",
"设置六",
"设置七",
]
for i, name in enumerate(tab_names):
btn = QPushButton(name)
btn.setFixedHeight(42)
btn.setCursor(Qt.PointingHandCursor)
btn.setStyleSheet(TAB_BTN_NORMAL_STYLE)
btn.clicked.connect(lambda checked, idx=i: self._on_tab_clicked(idx))
tab_layout.addWidget(btn)
self.tab_buttons.append(btn)
tab_layout.addStretch()
parent_layout.addWidget(tab_widget)
# 默认选中第一个
self._on_tab_clicked(0)
def _on_tab_clicked(self, index):
"""切换Tab页"""
for i, btn in enumerate(self.tab_buttons):
if i == index:
btn.setStyleSheet(TAB_BTN_SELECTED_STYLE)
else:
btn.setStyleSheet(TAB_BTN_NORMAL_STYLE)
self.stacked_widget.setCurrentIndex(index)
# ==================== 投料参数设置页 ====================
def _create_feed_params_page(self):
page = QWidget()
page.setStyleSheet("background: transparent;")
layout = QVBoxLayout(page)
layout.setContentsMargins(12, 8, 12, 8)
layout.setSpacing(8)
# 复选框: 开启多段振捣
self.multi_stage_checkbox = QCheckBox("开启多段振捣")
self.multi_stage_checkbox.setStyleSheet(CHECKBOX_STYLE)
font = QFont()
font.setPixelSize(15)
font.setBold(True)
self.multi_stage_checkbox.setFont(font)
layout.addWidget(self.multi_stage_checkbox)
# 分隔线
sep = QFrame()
sep.setStyleSheet(SEPARATOR_STYLE)
sep.setFixedHeight(1)
layout.addWidget(sep)
# 阶段列表区域(可滚动)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setStyleSheet("""
QScrollArea { background: transparent; border: none; }
QScrollArea > QWidget > QWidget { background: transparent; }
/* 滚动条整体 */
QScrollBar:vertical {
background: #0a1a3a; width: 8px; border: none;
}
/* 滚动条滑块 */
QScrollBar::handle:vertical {
background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #039ec3, stop:1 #03f5ff);
border-radius: 4px;
min-height: 20px;
}
/* 滑块悬停效果 */
QScrollBar::handle:vertical:hover {
background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #16ffff, stop:1 #00347e);
}
/* 隐藏上下箭头按钮 */
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
height: 0;
}
/* 滑块上下的空白区域(透明) */
QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
background: transparent;
}
""")
self.stages_container = QWidget()
self.stages_container.setStyleSheet("background: transparent;")
self.stages_layout = QVBoxLayout(self.stages_container)
self.stages_layout.setContentsMargins(0, 4, 0, 4)
self.stages_layout.setSpacing(12) # 阶段条目之间的间距
self.stages_layout.addStretch()
scroll.setWidget(self.stages_container)
layout.addWidget(scroll)
# 添加阶段按钮
add_stage_btn = QPushButton("+ 添加阶段")
add_stage_btn.setStyleSheet(COMMON_BTN_STYLE)
add_stage_btn.setFixedSize(120, 32)
add_stage_btn.setCursor(Qt.PointingHandCursor)
add_stage_btn.clicked.connect(lambda: self._add_stage_row())
layout.addWidget(add_stage_btn, alignment=Qt.AlignLeft)
# 底部按钮区域
btn_layout = QHBoxLayout()
btn_layout.addStretch()
apply_btn = QPushButton("立即生效")
apply_btn.setStyleSheet(COMMON_BTN_STYLE)
apply_btn.setFixedSize(100, 34)
apply_btn.setCursor(Qt.PointingHandCursor)
apply_btn.clicked.connect(self._on_feed_params_apply)
btn_layout.addWidget(apply_btn)
save_btn = QPushButton("保存配置")
save_btn.setStyleSheet(COMMON_BTN_STYLE)
save_btn.setFixedSize(100, 34)
save_btn.setCursor(Qt.PointingHandCursor)
save_btn.clicked.connect(self._on_feed_params_save)
btn_layout.addWidget(save_btn)
btn_layout.addStretch()
layout.addLayout(btn_layout)
return page
def _add_stage_row(self, mode="ratio", value=None, frequency=220):
"""添加一个阶段条目
如果value为None则根据mode自动计算取前面同单位阶段的最大值+1
"""
# 自动计算默认值
if value is None:
max_val = 0
for row in self.stage_rows:
row_mode = row.mode_combo.currentIndex() # 0=%, 1=秒
target_mode = 0 if mode == "ratio" else 1
if row_mode == target_mode:
try:
val = int(row.value_combo.currentText())
max_val = max(max_val, val)
except ValueError:
pass
value = max_val + 1
else:
value = int(value)
stage_index = len(self.stage_rows) + 1
row_widget = QWidget()
row_widget.setStyleSheet("background: transparent;")
row_layout = QHBoxLayout(row_widget)
row_layout.setContentsMargins(0, 2, 0, 2)
row_layout.setSpacing(6)
# 阶段标签
stage_label = QLabel(f"{self._num_to_chinese(stage_index)}阶段")
stage_label.setStyleSheet(COMMON_LABEL_STYLE)
stage_label.setFixedWidth(80)
font = QFont()
font.setPixelSize(14)
font.setBold(True)
stage_label.setFont(font)
row_layout.addWidget(stage_label)
# 减按钮
minus_btn = QPushButton("-")
minus_btn.setFixedSize(28, 28)
minus_btn.setCursor(Qt.PointingHandCursor)
minus_btn.setStyleSheet("""
QPushButton {
background-color: #001c82;
color: #13fffc;
border: 1px solid #017cbc;
border-radius: 3px;
font-size: 16px;
font-weight: Bold;
}
QPushButton:hover {
background-color: #0d2255;
border: 1px solid #13fffc;
}
QPushButton:pressed {
background-color: #017cbc;
}
""")
row_layout.addWidget(minus_btn)
# 值下拉框(根据单位显示不同选项)
value_combo = ArrowComboBox()
value_combo.setStyleSheet(COMBO_STYLE)
value_combo.setEditable(True)
value_combo.setFixedWidth(60)
self._update_value_combo_items(value_combo, mode)
# 设置当前值
value_combo.setCurrentText(str(value))
row_layout.addWidget(value_combo)
# 加按钮
plus_btn = QPushButton("+")
plus_btn.setFixedSize(28, 28)
plus_btn.setCursor(Qt.PointingHandCursor)
plus_btn.setStyleSheet("""
QPushButton {
background-color: #001c82;
color: #13fffc;
border: 1px solid #017cbc;
border-radius: 3px;
font-size: 16px;
font-weight: Bold;
}
QPushButton:hover {
background-color: #0d2255;
border: 1px solid #13fffc;
}
QPushButton:pressed {
background-color: #017cbc;
}
""")
row_layout.addWidget(plus_btn)
# 单位选择(秒 或 %
mode_combo = ArrowComboBox()
mode_combo.addItems(["%", ""])
mode_combo.setStyleSheet(COMBO_STYLE)
mode_combo.setFixedWidth(55)
if mode == "ratio":
mode_combo.setCurrentIndex(0)
else:
mode_combo.setCurrentIndex(1)
row_layout.addWidget(mode_combo)
# 单位切换时更新值下拉框选项
mode_combo.currentIndexChanged.connect(lambda idx: self._on_mode_changed(row_widget, idx))
# 频率标签
freq_label = QLabel("频率:")
freq_label.setStyleSheet(COMMON_LABEL_STYLE)
freq_label.setFixedWidth(40)
row_layout.addWidget(freq_label)
# 频率减按钮
freq_minus_btn = QPushButton("-")
freq_minus_btn.setFixedSize(28, 28)
freq_minus_btn.setCursor(Qt.PointingHandCursor)
freq_minus_btn.setStyleSheet("""
QPushButton {
background-color: #001c82;
color: #13fffc;
border: 1px solid #017cbc;
border-radius: 3px;
font-size: 16px;
font-weight: Bold;
}
QPushButton:hover {
background-color: #0d2255;
border: 1px solid #13fffc;
}
QPushButton:pressed {
background-color: #017cbc;
}
""")
row_layout.addWidget(freq_minus_btn)
# 频率可编辑下拉框(既可下拉选择预设值,也可直接输入)
freq_combo = ArrowComboBox()
freq_items = [str(f) for f in range(self.freq_min, self.freq_max + 1, 10)]
freq_combo.addItems(freq_items)
freq_combo.setEditable(True) # 设置为可编辑
freq_combo.setInsertPolicy(QComboBox.NoInsert) # 不自动插入新条目
freq_combo.setStyleSheet(FREQ_COMBO_STYLE)
freq_combo.setFixedWidth(80)
freq_combo.setCurrentText(str(frequency)) # 设置当前值
freq_combo.lineEdit().setAlignment(Qt.AlignCenter) # 文字居中
row_layout.addWidget(freq_combo)
# 频率加按钮
freq_plus_btn = QPushButton("+")
freq_plus_btn.setFixedSize(28, 28)
freq_plus_btn.setCursor(Qt.PointingHandCursor)
freq_plus_btn.setStyleSheet("""
QPushButton {
background-color: #001c82;
color: #13fffc;
border: 1px solid #017cbc;
border-radius: 3px;
font-size: 16px;
font-weight: Bold;
}
QPushButton:hover {
background-color: #0d2255;
border: 1px solid #13fffc;
}
QPushButton:pressed {
background-color: #017cbc;
}
""")
row_layout.addWidget(freq_plus_btn)
# Hz标签
hz_label = QLabel("Hz")
hz_label.setStyleSheet(COMMON_LABEL_STYLE)
hz_label.setFixedWidth(25)
row_layout.addWidget(hz_label)
# 删除按钮
del_btn = QPushButton("×")
del_btn.setFixedSize(28, 28)
del_btn.setCursor(Qt.PointingHandCursor)
del_btn.setStyleSheet("""
QPushButton {
background-color: transparent;
color: #ff4444;
border: 1px solid #ff4444;
border-radius: 14px;
font-size: 16px;
font-weight: Bold;
}
QPushButton:hover {
background-color: #ff4444;
color: white;
}
""")
del_btn.clicked.connect(lambda: self._remove_stage_row(row_widget))
row_layout.addWidget(del_btn)
row_layout.addStretch()
# 存储引用
row_widget.stage_label = stage_label
row_widget.value_combo = value_combo
row_widget.mode_combo = mode_combo
row_widget.freq_combo = freq_combo
# 绑定加减按钮事件
minus_btn.clicked.connect(lambda: self._adjust_value(row_widget, -1)) # 值的减按钮
plus_btn.clicked.connect(lambda: self._adjust_value(row_widget, 1)) # 值的加按钮
freq_minus_btn.clicked.connect(lambda: self._adjust_freq(freq_combo, -1))
freq_plus_btn.clicked.connect(lambda: self._adjust_freq(freq_combo, 1))
self.stage_rows.append(row_widget)
# 插入到stretch之前
self.stages_layout.insertWidget(self.stages_layout.count() - 1, row_widget)
def _get_same_mode_constraint(self, row_widget):
"""获取当前阶段同单位的约束值,返回 (min_value, max_value)
min_value: 前面所有同单位阶段的最大值新值必须大于此值None表示前面没有同单位阶段
max_value: 后面所有同单位阶段的最小值新值必须小于此值None表示后面没有同单位阶段
"""
try:
current_idx = self.stage_rows.index(row_widget)
except ValueError:
return None, None
current_mode = row_widget.mode_combo.currentIndex() # 0=%, 1=秒
min_value = None # None 表示前面没有同单位阶段
max_value = None
# 遍历前面的阶段,找同单位的最大值
for i in range(current_idx):
prev_row = self.stage_rows[i]
if prev_row.mode_combo.currentIndex() == current_mode:
try:
val = int(prev_row.value_combo.currentText())
if min_value is None or val > min_value:
min_value = val
except ValueError:
pass
# 遍历后面的阶段,找同单位的最小值
for i in range(current_idx + 1, len(self.stage_rows)):
next_row = self.stage_rows[i]
if next_row.mode_combo.currentIndex() == current_mode:
try:
val = int(next_row.value_combo.currentText())
if max_value is None or val < max_value:
max_value = val
except ValueError:
pass
return min_value, max_value
def _update_value_combo_items(self, value_combo, mode):
"""根据单位更新值下拉框的选项
Args:
value_combo: 值下拉框控件
mode: "ratio" 表示百分比,"time" 表示秒数
"""
value_combo.clear()
if mode == "ratio":
# 百分比模式
items = [str(v) for v in range(self.ratio_min, self.ratio_max + 1, self.ratio_step)]
else:
# 秒数模式
items = [str(v) for v in range(self.time_min, self.time_max + 1, self.time_step)]
value_combo.addItems(items)
def _on_mode_changed(self, row_widget, mode_index):
"""单位切换时的处理
Args:
row_widget: 阶段行控件
mode_index: 0=百分比, 1=秒数
"""
mode = "ratio" if mode_index == 0 else "time"
value_combo = row_widget.value_combo
# 保存当前值
try:
current_value = int(value_combo.currentText())
except ValueError:
current_value = 0
# 更新选项
self._update_value_combo_items(value_combo, mode)
# 尝试找到最接近的值
if mode == "ratio":
# 在百分比范围内找最接近的值
new_value = min(max(current_value, self.ratio_min), self.ratio_max)
# 找到最近的步进值
step = self.ratio_step
new_value = round(new_value / step) * step
new_value = min(max(new_value, self.ratio_min), self.ratio_max)
else:
# 在秒数范围内找最接近的值
new_value = min(max(current_value, self.time_min), self.time_max)
# 找到最近的步进值
step = self.time_step
new_value = round(new_value / step) * step
new_value = min(max(new_value, self.time_min), self.time_max)
value_combo.setCurrentText(str(new_value))
def _adjust_value(self, row_widget, delta):
"""调整值秒或百分比加减1并验证约束"""
value_combo = row_widget.value_combo
mode = row_widget.mode_combo.currentIndex() # 0=%, 1=秒
# 根据单位确定范围
if mode == 0: # 百分比
min_limit = self.ratio_min
max_limit = self.ratio_max
else: # 秒数
min_limit = self.time_min
max_limit = self.time_max
try:
current = int(value_combo.currentText())
except ValueError:
current = 0
new_value = current + delta
# 获取同单位约束
min_val, max_val = self._get_same_mode_constraint(row_widget)
# 确保大于前面同单位阶段的最大值(如果有的话)
if min_val is not None and new_value <= min_val:
new_value = min_val + 1
# 确保小于后面同单位阶段的最小值(如果有的话)
if max_val is not None and new_value >= max_val:
new_value = max_val - 1
# 确保在配置范围内
new_value = max(min_limit, min(max_limit, new_value))
value_combo.setCurrentText(str(new_value))
def _validate_stages(self):
"""验证阶段数据是否有效,返回 (is_valid, error_message)
验证规则:相同单位的阶段,数值必须递增
"""
# 分别收集两种单位的所有阶段
percent_stages = [] # (index, value)
time_stages = []
for i, row in enumerate(self.stage_rows):
mode = row.mode_combo.currentIndex() # 0=%, 1=秒
try:
val = int(row.value_combo.currentText())
except ValueError:
return False, f"{self._num_to_chinese(i + 1)}阶段数值格式错误"
if mode == 0: # %
percent_stages.append((i, val))
else: # 秒
time_stages.append((i, val))
# 验证百分比阶段递增
for j in range(1, len(percent_stages)):
prev_idx, prev_val = percent_stages[j - 1]
curr_idx, curr_val = percent_stages[j]
if curr_val <= prev_val:
return False, f"{self._num_to_chinese(curr_idx + 1)}阶段的百分数必须大于第{self._num_to_chinese(prev_idx + 1)}阶段的百分数"
# 验证秒数阶段递增
for j in range(1, len(time_stages)):
prev_idx, prev_val = time_stages[j - 1]
curr_idx, curr_val = time_stages[j]
if curr_val <= prev_val:
return False, f"{self._num_to_chinese(curr_idx + 1)}阶段的秒数必须大于第{self._num_to_chinese(prev_idx + 1)}阶段的秒数"
return True, ""
def _adjust_freq(self, freq_combo, delta):
"""调整频率直接对数值加减1"""
try:
current_value = int(freq_combo.currentText())
except ValueError:
current_value = self.freq_min
new_value = current_value + delta
# 确保在范围内
new_value = max(self.freq_min, min(self.freq_max, new_value))
freq_combo.setCurrentText(str(new_value))
def _remove_stage_row(self, row_widget):
"""删除一个阶段条目"""
if row_widget in self.stage_rows:
self.stage_rows.remove(row_widget)
self.stages_layout.removeWidget(row_widget)
row_widget.deleteLater()
self._update_stage_labels()
def _update_stage_labels(self):
"""更新所有阶段的编号标签"""
for i, row in enumerate(self.stage_rows):
row.stage_label.setText(f"{self._num_to_chinese(i + 1)}阶段")
@staticmethod
def _num_to_chinese(num):
"""数字转中文"""
chinese = ["", "", "", "", "", "", "", "", "", "",
"十一", "十二", "十三", "十四", "十五", "十六", "十七", "十八", "十九", "二十"]
if 1 <= num <= 20:
return chinese[num - 1]
return str(num)
def _get_feed_params_data(self):
"""获取投料参数数据"""
multi_stage = self.multi_stage_checkbox.isChecked()
stages = []
for row in self.stage_rows:
mode = "ratio" if row.mode_combo.currentIndex() == 0 else "time" # 0=%=ratio, 1=秒=time
value = row.value_combo.currentText().strip()
frequency = row.freq_combo.currentText().strip()
stages.append({
"mode": mode,
"value": value,
"frequency": frequency,
})
return multi_stage, stages
def _on_feed_params_apply(self):
"""投料参数 立即生效"""
# 先验证
is_valid, error_msg = self._validate_stages()
if not is_valid:
QMessageBox.warning(self, "验证失败", error_msg)
return
multi_stage, stages = self._get_feed_params_data()
self.feed_params_apply.emit(multi_stage, stages)
def _on_feed_params_save(self):
"""投料参数 保存到配置文件"""
# 先验证
is_valid, error_msg = self._validate_stages()
if not is_valid:
QMessageBox.warning(self, "验证失败", error_msg)
return
multi_stage, stages = self._get_feed_params_data()
config = configparser.ConfigParser()
config.read(FEED_PARAMS_CONFIG, encoding="utf-8")
# 写入general
if not config.has_section("general"):
config.add_section("general")
config.set("general", "multi_stage_vibration", "true" if multi_stage else "false")
# 写入stages count
if not config.has_section("stages"):
config.add_section("stages")
config.set("stages", "count", str(len(stages)))
# 清除旧的stage_N段
for sec in list(config.sections()):
if sec.startswith("stage_"):
config.remove_section(sec)
# 写入每个阶段
for i, stage in enumerate(stages):
sec = f"stage_{i + 1}"
config.add_section(sec)
config.set(sec, "mode", stage["mode"])
config.set(sec, "value", stage["value"])
config.set(sec, "frequency", stage["frequency"])
# 保留频率范围
if not config.has_section("frequency_range"):
config.add_section("frequency_range")
config.set("frequency_range", "min", str(self.freq_min))
config.set("frequency_range", "max", str(self.freq_max))
with open(FEED_PARAMS_CONFIG, "w", encoding="utf-8") as f:
config.write(f)
def _load_feed_params(self):
"""从配置文件加载投料参数"""
config = configparser.ConfigParser()
if not os.path.exists(FEED_PARAMS_CONFIG):
return
config.read(FEED_PARAMS_CONFIG, encoding="utf-8")
# 加载多段振捣开关
if config.has_section("general"):
enabled = config.get("general", "multi_stage_vibration", fallback="false")
self.multi_stage_checkbox.setChecked(enabled.lower() == "true")
# 加载阶段
count = 0
if config.has_section("stages"):
count = config.getint("stages", "count", fallback=0)
for i in range(1, count + 1):
sec = f"stage_{i}"
if config.has_section(sec):
mode = config.get(sec, "mode", fallback="ratio")
value = config.get(sec, "value", fallback="10")
frequency = config.getint(sec, "frequency", fallback=210)
self._add_stage_row(mode, value, frequency)
# ==================== IP配置页 ====================
def _create_ip_config_page(self):
page = QWidget()
page.setStyleSheet("background: transparent;")
layout = QVBoxLayout(page)
layout.setContentsMargins(12, 8, 12, 8)
layout.setSpacing(8)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
# IP配置页滚动条样式
scroll.setStyleSheet("""
QScrollArea { background: transparent; border: none; }
QScrollArea > QWidget > QWidget { background: transparent; }
/* 滚动条整体 */
QScrollBar:vertical {
background: #0a1a3a; width: 8px; border: none;
}
/* 滚动条滑块 */
QScrollBar::handle:vertical {
background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #039ec3, stop:1 #03f5ff);
border-radius: 4px;
min-height: 20px;
}
/* 滑块悬停效果 */
QScrollBar::handle:vertical:hover {
background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #16ffff, stop:1 #00347e);
}
/* 隐藏上下箭头按钮 */
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
height: 0;
}
/* 滑块上下的空白区域(透明) */
QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
background: transparent;
}
""")
scroll_content = QWidget()
scroll_content.setStyleSheet("background: transparent;")
scroll_layout = QVBoxLayout(scroll_content)
scroll_layout.setContentsMargins(0, 0, 0, 0)
scroll_layout.setSpacing(10)
# --- 相机IP配置标题 ---
cam_title = QLabel("相机IP配置")
cam_title.setStyleSheet(SECTION_TITLE_STYLE)
scroll_layout.addWidget(cam_title)
sep1 = QFrame()
sep1.setStyleSheet(SEPARATOR_STYLE)
sep1.setFixedHeight(1)
scroll_layout.addWidget(sep1)
# 上位料斗相机
self.camera_edits["上位料斗"] = self._add_camera_ip_group(scroll_layout, "上位料斗相机IP")
# 下位料斗相机
self.camera_edits["下位料斗"] = self._add_camera_ip_group(scroll_layout, "下位料斗相机IP")
# 模具车相机
self.camera_edits["模具车"] = self._add_camera_ip_group(scroll_layout, "模具车相机IP")
# --- 其他IP配置标题 ---
other_title = QLabel("其他IP配置")
other_title.setStyleSheet(SECTION_TITLE_STYLE)
scroll_layout.addWidget(other_title)
sep2 = QFrame()
sep2.setStyleSheet(SEPARATOR_STYLE)
sep2.setFixedHeight(1)
scroll_layout.addWidget(sep2)
# 其他IP列表容器
self.other_ip_container = QWidget()
self.other_ip_container.setStyleSheet("background: transparent;")
self.other_ip_layout = QVBoxLayout(self.other_ip_container)
self.other_ip_layout.setContentsMargins(0, 0, 0, 0)
self.other_ip_layout.setSpacing(6)
self.other_ip_layout.addStretch()
scroll_layout.addWidget(self.other_ip_container)
# 添加条目按钮
add_ip_btn = QPushButton("+ 添加IP配置")
add_ip_btn.setStyleSheet(COMMON_BTN_STYLE)
add_ip_btn.setFixedSize(130, 32)
add_ip_btn.setCursor(Qt.PointingHandCursor)
add_ip_btn.clicked.connect(lambda: self._add_other_ip_row())
scroll_layout.addWidget(add_ip_btn, alignment=Qt.AlignLeft)
scroll_layout.addStretch()
scroll.setWidget(scroll_content)
layout.addWidget(scroll)
# 底部按钮
btn_layout = QHBoxLayout()
btn_layout.addStretch()
apply_btn = QPushButton("立即生效")
apply_btn.setStyleSheet(COMMON_BTN_STYLE)
apply_btn.setFixedSize(100, 34)
apply_btn.setCursor(Qt.PointingHandCursor)
apply_btn.clicked.connect(self._on_ip_config_apply)
btn_layout.addWidget(apply_btn)
save_btn = QPushButton("保存配置")
save_btn.setStyleSheet(COMMON_BTN_STYLE)
save_btn.setFixedSize(100, 34)
save_btn.setCursor(Qt.PointingHandCursor)
save_btn.clicked.connect(self._on_ip_config_save)
btn_layout.addWidget(save_btn)
btn_layout.addStretch()
layout.addLayout(btn_layout)
return page
def _add_camera_ip_group(self, parent_layout, title):
"""添加一组相机IP配置IP、端口、账号、密码"""
group_widget = QWidget()
group_widget.setStyleSheet("background: transparent;")
group_layout = QVBoxLayout(group_widget)
group_layout.setContentsMargins(8, 4, 0, 4)
group_layout.setSpacing(4)
# 标题
title_label = QLabel(title)
title_label.setStyleSheet("color: #13fffc; font-size: 14px; font-weight: Bold; background: transparent;")
group_layout.addWidget(title_label)
# IP和端口行
row1 = QHBoxLayout()
row1.setSpacing(8)
ip_label = QLabel("IP地址:")
ip_label.setStyleSheet(COMMON_LABEL_STYLE)
ip_label.setFixedWidth(55)
row1.addWidget(ip_label)
ip_edit = QLineEdit()
ip_edit.setStyleSheet(COMMON_EDIT_STYLE)
ip_edit.setFixedWidth(150)
ip_edit.setPlaceholderText("192.168.x.x")
row1.addWidget(ip_edit)
port_label = QLabel("端口:")
port_label.setStyleSheet(COMMON_LABEL_STYLE)
port_label.setFixedWidth(40)
row1.addWidget(port_label)
port_edit = QLineEdit()
port_edit.setStyleSheet(COMMON_EDIT_STYLE)
port_edit.setFixedWidth(70)
port_edit.setValidator(QIntValidator(1, 65535))
row1.addWidget(port_edit)
row1.addStretch()
group_layout.addLayout(row1)
# 账号和密码行
row2 = QHBoxLayout()
row2.setSpacing(8)
user_label = QLabel("账号:")
user_label.setStyleSheet(COMMON_LABEL_STYLE)
user_label.setFixedWidth(55)
row2.addWidget(user_label)
user_edit = QLineEdit()
user_edit.setStyleSheet(COMMON_EDIT_STYLE)
user_edit.setFixedWidth(150)
row2.addWidget(user_edit)
pwd_label = QLabel("密码:")
pwd_label.setStyleSheet(COMMON_LABEL_STYLE)
pwd_label.setFixedWidth(40)
row2.addWidget(pwd_label)
pwd_edit = QLineEdit()
pwd_edit.setStyleSheet(COMMON_EDIT_STYLE)
pwd_edit.setFixedWidth(150)
pwd_edit.setEchoMode(QLineEdit.Password)
row2.addWidget(pwd_edit)
row2.addStretch()
group_layout.addLayout(row2)
parent_layout.addWidget(group_widget)
return {
"ip": ip_edit,
"port": port_edit,
"username": user_edit,
"password": pwd_edit,
}
def _add_other_ip_row(self, name="", ip="", port=""):
"""添加一个其他IP配置条目"""
row_widget = QWidget()
row_widget.setStyleSheet("background: transparent;")
row_layout = QHBoxLayout(row_widget)
row_layout.setContentsMargins(8, 2, 0, 2)
row_layout.setSpacing(8)
name_label = QLabel("名称:")
name_label.setStyleSheet(COMMON_LABEL_STYLE)
name_label.setFixedWidth(40)
row_layout.addWidget(name_label)
name_edit = QLineEdit(name)
name_edit.setStyleSheet(COMMON_EDIT_STYLE)
name_edit.setFixedWidth(100)
row_layout.addWidget(name_edit)
ip_label = QLabel("IP:")
ip_label.setStyleSheet(COMMON_LABEL_STYLE)
ip_label.setFixedWidth(25)
row_layout.addWidget(ip_label)
ip_edit = QLineEdit(ip)
ip_edit.setStyleSheet(COMMON_EDIT_STYLE)
ip_edit.setFixedWidth(140)
ip_edit.setPlaceholderText("192.168.x.x")
row_layout.addWidget(ip_edit)
port_label = QLabel("端口:")
port_label.setStyleSheet(COMMON_LABEL_STYLE)
port_label.setFixedWidth(40)
row_layout.addWidget(port_label)
port_edit = QLineEdit(port)
port_edit.setStyleSheet(COMMON_EDIT_STYLE)
port_edit.setFixedWidth(70)
port_edit.setValidator(QIntValidator(1, 65535))
row_layout.addWidget(port_edit)
# 删除按钮
del_btn = QPushButton("×")
del_btn.setFixedSize(28, 28)
del_btn.setCursor(Qt.PointingHandCursor)
del_btn.setStyleSheet("""
QPushButton {
background-color: transparent;
color: #ff4444;
border: 1px solid #ff4444;
border-radius: 14px;
font-size: 16px;
font-weight: Bold;
}
QPushButton:hover {
background-color: #ff4444;
color: white;
}
""")
del_btn.clicked.connect(lambda: self._remove_other_ip_row(row_widget))
row_layout.addWidget(del_btn)
row_layout.addStretch()
row_widget.name_edit = name_edit
row_widget.ip_edit = ip_edit
row_widget.port_edit = port_edit
self.other_ip_rows.append(row_widget)
self.other_ip_layout.insertWidget(self.other_ip_layout.count() - 1, row_widget)
def _remove_other_ip_row(self, row_widget):
"""删除一个其他IP条目"""
if row_widget in self.other_ip_rows:
self.other_ip_rows.remove(row_widget)
self.other_ip_layout.removeWidget(row_widget)
row_widget.deleteLater()
def _on_ip_config_apply(self):
"""IP配置 立即生效"""
self.ip_config_apply.emit()
def _on_ip_config_save(self):
"""IP配置 保存到配置文件"""
self._save_camera_config()
self._save_other_ip_config()
def _save_camera_config(self):
"""保存相机IP到 camera_config.ini"""
config = configparser.ConfigParser()
config.read(CAMERA_CONFIG, encoding="utf-8")
section_map = {
"上位料斗": "上位料斗",
"下位料斗": "下位料斗",
"模具车": "模具车",
}
for key, section in section_map.items():
if key in self.camera_edits:
edits = self.camera_edits[key]
if not config.has_section(section):
config.add_section(section)
config.set(section, "ip", edits["ip"].text().strip())
config.set(section, "port", edits["port"].text().strip())
config.set(section, "username", edits["username"].text().strip())
config.set(section, "password", edits["password"].text().strip())
with open(CAMERA_CONFIG, "w", encoding="utf-8") as f:
config.write(f)
def _save_other_ip_config(self):
"""保存其他IP到 other_ip_config.ini"""
config = configparser.ConfigParser()
config.add_section("count")
config.set("count", "total", str(len(self.other_ip_rows)))
for i, row in enumerate(self.other_ip_rows):
sec = f"item_{i + 1}"
config.add_section(sec)
config.set(sec, "name", row.name_edit.text().strip())
config.set(sec, "ip", row.ip_edit.text().strip())
config.set(sec, "port", row.port_edit.text().strip())
with open(OTHER_IP_CONFIG, "w", encoding="utf-8") as f:
config.write(f)
def _load_camera_config(self):
"""从 camera_config.ini 加载相机IP"""
config = configparser.ConfigParser()
if not os.path.exists(CAMERA_CONFIG):
return
config.read(CAMERA_CONFIG, encoding="utf-8")
section_map = {
"上位料斗": "上位料斗",
"下位料斗": "下位料斗",
"模具车": "模具车",
}
for key, section in section_map.items():
if config.has_section(section) and key in self.camera_edits:
edits = self.camera_edits[key]
edits["ip"].setText(config.get(section, "ip", fallback=""))
edits["port"].setText(config.get(section, "port", fallback=""))
edits["username"].setText(config.get(section, "username", fallback=""))
edits["password"].setText(config.get(section, "password", fallback=""))
def _load_other_ip_config(self):
"""从 other_ip_config.ini 加载其他IP"""
config = configparser.ConfigParser()
if not os.path.exists(OTHER_IP_CONFIG):
return
config.read(OTHER_IP_CONFIG, encoding="utf-8")
count = 0
if config.has_section("count"):
count = config.getint("count", "total", fallback=0)
for i in range(1, count + 1):
sec = f"item_{i}"
if config.has_section(sec):
name = config.get(sec, "name", fallback="")
ip = config.get(sec, "ip", fallback="")
port = config.get(sec, "port", fallback="")
self._add_other_ip_row(name, ip, port)
# ==================== AI算法参数设置页 ====================
def _create_ai_params_page(self):
page = QWidget()
page.setStyleSheet("background: transparent;")
layout = QVBoxLayout(page)
layout.setContentsMargins(12, 8, 12, 8)
layout.setSpacing(8)
title = QLabel("AI算法参数设置")
title.setStyleSheet(SECTION_TITLE_STYLE)
layout.addWidget(title)
sep = QFrame()
sep.setStyleSheet(SEPARATOR_STYLE)
sep.setFixedHeight(1)
layout.addWidget(sep)
placeholder = QLabel("AI算法参数设置功能开发中...")
placeholder.setStyleSheet("color: #9fbfd4; font-size: 16px; background: transparent;")
placeholder.setAlignment(Qt.AlignCenter)
layout.addWidget(placeholder)
layout.addStretch()
return page
# ==================== 对外接口 ====================
def get_feed_params(self):
"""获取当前投料参数设置(供外部调用立即生效)"""
return self._get_feed_params_data()
def get_camera_ip_config(self):
"""获取当前相机IP配置"""
result = {}
for key, edits in self.camera_edits.items():
result[key] = {
"ip": edits["ip"].text().strip(),
"port": edits["port"].text().strip(),
"username": edits["username"].text().strip(),
"password": edits["password"].text().strip(),
}
return result
def get_other_ip_config(self):
"""获取当前其他IP配置"""
result = []
for row in self.other_ip_rows:
result.append({
"name": row.name_edit.text().strip(),
"ip": row.ip_edit.text().strip(),
"port": row.port_edit.text().strip(),
})
return result
def paintEvent(self, event):
if not self.bg_pixmap.isNull():
painter = QPainter(self)
painter.drawPixmap(self.rect(), self.bg_pixmap)
super().paintEvent(event)
# 测试代码
if __name__ == "__main__":
app = QApplication(sys.argv)
dialog = SystemSettingsDialog()
dialog.show()
sys.exit(app.exec())