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