Files
Feeding_control_system/view/widgets/system_settings_dialog.py

1435 lines
50 KiB
Python
Raw Normal View History

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