1435 lines
50 KiB
Python
1435 lines
50 KiB
Python
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())
|