From c6c0b8fdf5d872e996c21dad8872fbf3c1ad9e07 Mon Sep 17 00:00:00 2001 From: DXC Date: Mon, 29 Jun 2026 11:11:32 +0800 Subject: [PATCH] =?UTF-8?q?=E6=AD=A5=E9=AA=A4=E5=8D=81=E4=B8=80=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gui/panels/step11_map_panel.py | 287 +++++++++++++---------------- 1 file changed, 133 insertions(+), 154 deletions(-) diff --git a/src/gui/panels/step11_map_panel.py b/src/gui/panels/step11_map_panel.py index 132743f..35cdd53 100644 --- a/src/gui/panels/step11_map_panel.py +++ b/src/gui/panels/step11_map_panel.py @@ -21,7 +21,7 @@ from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QGroupBox, QFormLayout, QHBoxLayout, QLabel, QCheckBox, QPushButton, QLineEdit, QDoubleSpinBox, QRadioButton, QButtonGroup, QMessageBox, QFileDialog, QComboBox, - QProgressBar, + QProgressBar, QSizePolicy, ) from src.gui.components.custom_widgets import FileSelectWidget @@ -161,207 +161,191 @@ class Step11MapPanel(QWidget): def init_ui(self): layout = QVBoxLayout() + layout.setContentsMargins(24, 24, 24, 24) - hint = QLabel( - "独立运行:可选「单个 CSV」或「文件夹批量」(扫描目录下所有 .csv)。" - "GeoTIFF 栅格模式下,亦支持批量渲染步骤8输出的所有水色指数 GeoTIFF 文件。" - ) - hint.setWordWrap(True) - hint.setStyleSheet( - f"color: {ModernStylesheet.COLORS.get('text_secondary', '#666')};" - ) - layout.addWidget(hint) + # 【核心黑科技1】:定义一个闭包函数,彻底剥夺 FileSelectWidget 内部的各种默认间距,完美归一化 + def apply_fs_style(fs_widget, placeholder=None): + # 1. 锁死标签宽度,保证右侧输入框起跑线 100% 垂直对齐 + fs_widget.label.setMinimumWidth(120) + fs_widget.label.setMaximumWidth(120) + if placeholder: + fs_widget.line_edit.setPlaceholderText(placeholder) + fs_widget.browse_btn.setText("浏览...") + # 2. 剥夺组件内部的默认边距;锁定内部间距为 10px + fs_layout = fs_widget.layout() + fs_layout.setContentsMargins(0, 0, 0, 0) + fs_layout.setSpacing(10) + # 绝对不要再用 setStyleSheet!完全信任你的全局 ModernStylesheet 绝美主题! - mode_row = QHBoxLayout() - self.mode_single_rb = QRadioButton("单个 CSV 文件") - self.mode_folder_rb = QRadioButton("文件夹批量") - self._mode_group = QButtonGroup(self) - self._mode_group.addButton(self.mode_single_rb, 0) - self._mode_group.addButton(self.mode_folder_rb, 1) - mode_row.addWidget(self.mode_single_rb) - mode_row.addWidget(self.mode_folder_rb) - mode_row.addStretch() - layout.addLayout(mode_row) + # 【核心黑科技2】:把下拉框、数字框等也封装成同样规格的行 + def create_standard_row(label_text, widget): + row = QHBoxLayout() + row.setContentsMargins(0, 0, 0, 0) + row.setSpacing(10) # 间距锁死 10px,和上面的 FileSelectWidget 一模一样 + label = QLabel(label_text) + label.setMinimumWidth(120) + label.setMaximumWidth(120) # 标签宽度锁死 120px + row.addWidget(label) + row.addWidget(widget) + return row - # ---------- 渲染模式选择器(CSV vs GeoTIFF) ---------- - render_row = QHBoxLayout() - render_row.addWidget(QLabel("渲染模式:")) + # ========================================== + # 卡片 1:输入数据与模式 + # ========================================== + input_group = QGroupBox("📁 输入数据与模式") + input_layout = QVBoxLayout() + input_layout.setSpacing(16) + input_layout.setContentsMargins(20, 24, 20, 20) + + # ---------- 渲染模式选择器 ---------- self.render_mode_combo = QComboBox() self.render_mode_combo.addItems(["CSV 插值模式", "GeoTIFF 栅格模式"]) - self.render_mode_combo.setMinimumWidth(180) + self.render_mode_combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.render_mode_combo.currentTextChanged.connect(self._toggle_input_mode) - render_row.addWidget(self.render_mode_combo) - render_row.addStretch() - layout.addLayout(render_row) + input_layout.addLayout(create_standard_row("渲染模式:", self.render_mode_combo)) - # ---------- RadioButton 美化样式(选中状态为方形实心块,贴合主界面风格) ---------- - radio_style = """ - QRadioButton { - font-size: 14px; - spacing: 8px; - color: #333333; - } - QRadioButton::indicator { - width: 16px; - height: 16px; - border: 2px solid #999999; - border-radius: 3px; - background-color: white; - } - QRadioButton::indicator:checked { - border: 2px solid #0078d4; - background-color: #0078d4; - image: none; - } - QRadioButton::indicator:hover { - border: 2px solid #005a9e; - } - """ - self.mode_single_rb.setStyleSheet(radio_style) - self.mode_folder_rb.setStyleSheet(radio_style) + # ---------- 处理范围选择器 (回归你最爱的全局下拉框) ---------- + self.batch_mode_combo = QComboBox() + self.batch_mode_combo.addItems(["单个文件", "文件夹批量"]) + self.batch_mode_combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.batch_mode_combo.currentTextChanged.connect(self._toggle_input_mode) + input_layout.addLayout(create_standard_row("处理范围:", self.batch_mode_combo)) - self.prediction_csv_file = FileSelectWidget( - "预测结果CSV:", - "CSV Files (*.csv);;All Files (*.*)" - ) - layout.addWidget(self.prediction_csv_file) + # ---------- 统一组件:预测结果CSV (单文件) ---------- + self.prediction_csv_file = FileSelectWidget("预测结果 CSV:", "CSV Files (*.csv);;All Files (*.*)") + apply_fs_style(self.prediction_csv_file, "选择预测结果 CSV 文件…") + input_layout.addWidget(self.prediction_csv_file) - folder_row = QHBoxLayout() - self.prediction_csv_dir_label = QLabel("预测CSV目录:") - self.prediction_csv_dir_label.setMinimumWidth(120) - self.prediction_csv_dir_edit = QLineEdit() - self.prediction_csv_dir_edit.setPlaceholderText("选择含多个预测结果 CSV 的文件夹…") - pred_dir_btn = QPushButton("浏览…") - pred_dir_btn.setMaximumWidth(80) - pred_dir_btn.clicked.connect(self.browse_prediction_csv_dir) - folder_row.addWidget(self.prediction_csv_dir_label) - folder_row.addWidget(self.prediction_csv_dir_edit, 1) - folder_row.addWidget(pred_dir_btn) - self._folder_row_widget = QWidget() - self._folder_row_widget.setLayout(folder_row) - layout.addWidget(self._folder_row_widget) + # ---------- 统一组件:预测CSV目录 (批量) ---------- + self._folder_row_widget = FileSelectWidget("预测 CSV 目录:", "Directories") + apply_fs_style(self._folder_row_widget, "选择含多个预测结果 CSV 的文件夹…") + self._folder_row_widget.browse_btn.clicked.disconnect() + self._folder_row_widget.browse_btn.clicked.connect(self.browse_prediction_csv_dir) + self.prediction_csv_dir_edit = self._folder_row_widget.line_edit + input_layout.addWidget(self._folder_row_widget) - # ---------- GeoTIFF 栅格文件选择器 ---------- - self.geotiff_file = FileSelectWidget( - "水色指数 GeoTIFF:", - "GeoTIFF Files (*.tif);;All Files (*.*)" - ) - self.geotiff_file.line_edit.setPlaceholderText("选择步骤8输出的水色指数 GeoTIFF 文件…") + # ---------- 统一组件:水色指数 GeoTIFF (单文件) ---------- + self.geotiff_file = FileSelectWidget("水色指数 GeoTIFF:", "GeoTIFF Files (*.tif);;All Files (*.*)") + apply_fs_style(self.geotiff_file, "选择步骤8输出的水色指数 GeoTIFF 文件…") self.geotiff_file.setVisible(False) - layout.addWidget(self.geotiff_file) + input_layout.addWidget(self.geotiff_file) - # ---------- GeoTIFF 文件夹批量选择器(GeoTIFF + 文件夹模式时显示) ---------- - geotiff_dir_row = QHBoxLayout() - self.geotiff_dir_label = QLabel("水色指数目录:") - self.geotiff_dir_label.setMinimumWidth(120) - self.geotiff_dir_edit = QLineEdit() - self.geotiff_dir_edit.setPlaceholderText("选择 10_WaterIndex_Images 文件夹(批量渲染)…") - geotiff_dir_btn = QPushButton("浏览…") - geotiff_dir_btn.setMaximumWidth(80) - geotiff_dir_btn.clicked.connect(self.browse_geotiff_dir) - geotiff_dir_row.addWidget(self.geotiff_dir_label) - geotiff_dir_row.addWidget(self.geotiff_dir_edit, 1) - geotiff_dir_row.addWidget(geotiff_dir_btn) - self._geotiff_dir_widget = QWidget() - self._geotiff_dir_widget.setLayout(geotiff_dir_row) + # ---------- 统一组件:水色指数目录 (批量) ---------- + self._geotiff_dir_widget = FileSelectWidget("水色指数目录:", "Directories") + apply_fs_style(self._geotiff_dir_widget, "选择 10_WaterIndex_Images 文件夹(批量渲染)…") + self._geotiff_dir_widget.browse_btn.clicked.disconnect() + self._geotiff_dir_widget.browse_btn.clicked.connect(self.browse_geotiff_dir) + self.geotiff_dir_edit = self._geotiff_dir_widget.line_edit self._geotiff_dir_widget.setVisible(False) - layout.addWidget(self._geotiff_dir_widget) + input_layout.addWidget(self._geotiff_dir_widget) - self.recursive_csv_cb = QCheckBox("包含子文件夹(递归扫描 *.csv)") - layout.addWidget(self.recursive_csv_cb) + input_group.setLayout(input_layout) + layout.addWidget(input_group) - self.boundary_file = FileSelectWidget( - "边界文件:", - "Shapefiles (*.shp);;All Files (*.*)" - ) - layout.addWidget(self.boundary_file) + # ========================================== + # 卡片 2:生成参数配置 + # ========================================== + params_group = QGroupBox("⚙️ 生成参数配置") + params_layout = QVBoxLayout() + params_layout.setSpacing(16) + params_layout.setContentsMargins(20, 24, 20, 20) - # 参数设置 - params_group = QGroupBox("生成参数") - params_layout = QFormLayout() + # ---------- 统一组件:边界文件 ---------- + self.boundary_file = FileSelectWidget("边界文件:", "Shapefiles (*.shp);;All Files (*.*)") + apply_fs_style(self.boundary_file, "选择水域掩膜或边界 Shapefile…") + params_layout.addWidget(self.boundary_file) + # ---------- 统一组件:分辨率 ---------- self.resolution = QDoubleSpinBox() self.resolution.setRange(1, 1000) self.resolution.setValue(30) - params_layout.addRow("分辨率(米):", self.resolution) + self.resolution.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + params_layout.addLayout(create_standard_row("分辨率(米):", self.resolution)) + # ---------- 统一组件:输入坐标系 ---------- self.input_crs = QLineEdit() self.input_crs.setText("EPSG:32651") - params_layout.addRow("输入坐标系:", self.input_crs) + params_layout.addLayout(create_standard_row("输入坐标系:", self.input_crs)) + # ---------- 统一组件:输出坐标系 ---------- self.output_crs = QLineEdit() - # ★★★ 强制默认输出坐标系与输入一致,禁止从 GUI 误改为 EPSG:4326 ★★★ - # 历史默认值 'EPSG:4326' 会让 ContentMapper 把栅格重投影到经纬度, - # 与基于 EPSG:32651 的水域掩膜叠加时发生仿射变换撕裂(栅格错位、坐标轴扭曲)。 self.output_crs.setText("EPSG:32651") - params_layout.addRow("输出坐标系:", self.output_crs) + params_layout.addLayout(create_standard_row("输出坐标系:", self.output_crs)) + + # ---------- 复选框 ---------- + check_row = QHBoxLayout() + check_row.setContentsMargins(0, 0, 0, 0) + check_row.setSpacing(10) + check_placeholder = QLabel("") + check_placeholder.setMinimumWidth(120) + check_placeholder.setMaximumWidth(120) # 保持对齐方阵 + check_row.addWidget(check_placeholder) self.show_points = QCheckBox("显示采样点") - params_layout.addRow("", self.show_points) - self.use_diffusion = QCheckBox("启用距离扩散") self.use_diffusion.setChecked(True) - params_layout.addRow("", self.use_diffusion) + check_row.addWidget(self.show_points) + check_row.addWidget(self.use_diffusion) + check_row.addStretch() + params_layout.addLayout(check_row) params_group.setLayout(params_layout) layout.addWidget(params_group) - # 输出目录 - self.output_dir = FileSelectWidget( - "输出分布图目录:", - "Directories;;All Files (*.*)" - ) - self.output_dir.line_edit.setPlaceholderText("留空→工作目录/14_visualization") + # ========================================== + # 卡片 3:输出与执行 + # ========================================== + execute_group = QGroupBox("🚀 输出与执行") + execute_layout = QVBoxLayout() + execute_layout.setSpacing(16) + execute_layout.setContentsMargins(20, 24, 20, 20) + + self.output_dir = FileSelectWidget("输出分布图目录:", "Directories;;All Files (*.*)") + apply_fs_style(self.output_dir, "留空→工作目录/14_visualization") self.output_dir.browse_btn.clicked.disconnect() self.output_dir.browse_btn.clicked.connect(self.browse_output_dir) - layout.addWidget(self.output_dir) + execute_layout.addWidget(self.output_dir) - # 启用步骤 - self.enable_checkbox = QCheckBox("启用此步骤") - self.enable_checkbox.setChecked(True) - layout.addWidget(self.enable_checkbox) - - # 独立运行按钮 - self.run_button = QPushButton("独立运行此步骤") - self.run_button.setStyleSheet(ModernStylesheet.get_button_stylesheet('success')) - self.run_button.clicked.connect(self.run_step) - layout.addWidget(self.run_button) - - # 批量渲染进度条 self.progress_bar = QProgressBar() self.progress_bar.setVisible(False) self.progress_bar.setMinimum(0) self.progress_bar.setMaximum(100) self.progress_bar.setValue(0) - layout.addWidget(self.progress_bar) + execute_layout.addWidget(self.progress_bar) + action_layout = QHBoxLayout() + action_layout.addStretch() + self.run_button = QPushButton("独立运行步骤") + self.run_button.setStyleSheet(ModernStylesheet.get_button_stylesheet('primary')) + self.run_button.setMinimumWidth(140) + self.run_button.clicked.connect(self.run_step) + action_layout.addWidget(self.run_button) + + execute_layout.addLayout(action_layout) + execute_group.setLayout(execute_layout) + + layout.addWidget(execute_group) layout.addStretch() self.setLayout(layout) - # 信号绑定与初始状态 - self.mode_single_rb.toggled.connect(self._toggle_input_mode) - self.mode_folder_rb.toggled.connect(self._toggle_input_mode) - self.mode_single_rb.setChecked(True) # 默认选中"单个 CSV" - self._toggle_input_mode() # 根据默认值设置初始显示状态 + self.batch_mode_combo.setCurrentIndex(0) + self._toggle_input_mode() def _toggle_input_mode(self): """槽函数:根据渲染模式和输入模式动态显示/隐藏对应的输入组件。""" geotiff_mode = self.render_mode_combo.currentText() == "GeoTIFF 栅格模式" - folder_mode = self.mode_folder_rb.isChecked() + folder_mode = self.batch_mode_combo.currentText() == "文件夹批量" # CSV 插值模式 if not geotiff_mode: self.prediction_csv_file.setVisible(not folder_mode) self._folder_row_widget.setVisible(folder_mode) - self.recursive_csv_cb.setVisible(folder_mode) self.geotiff_file.setVisible(False) self._geotiff_dir_widget.setVisible(False) # GeoTIFF 栅格模式 else: self.prediction_csv_file.setVisible(False) self._folder_row_widget.setVisible(False) - self.recursive_csv_cb.setVisible(False) - # GeoTIFF + 文件夹批量 → 显示文件夹选择器;否则 → 显示单文件选择器 self.geotiff_file.setVisible(not folder_mode) self._geotiff_dir_widget.setVisible(folder_mode) @@ -387,10 +371,8 @@ class Step11MapPanel(QWidget): if not folder or not os.path.isdir(folder): return [] root = Path(folder) - if self.recursive_csv_cb.isChecked(): - files = sorted(root.rglob("*.csv")) - else: - files = sorted(root.glob("*.csv")) + # 统一取消递归,只扫描当前直接文件夹下的 CSV,防误操作 + files = sorted(root.glob("*.csv")) return [str(p) for p in files if p.is_file()] def browse_geotiff_dir(self): @@ -428,14 +410,13 @@ class Step11MapPanel(QWidget): def get_config(self): pred_csv = (self.prediction_csv_file.get_path() or "").strip() - folder_mode = self.mode_folder_rb.isChecked() + folder_mode = self.batch_mode_combo.currentText() == "文件夹批量" pred_dir = (self.prediction_csv_dir_edit.text() or "").strip() geotiff_path = (self.geotiff_file.get_path() or "").strip() config = { 'step10_batch_mode': 'folder' if folder_mode else 'single', 'render_mode': self.render_mode_combo.currentText(), 'prediction_csv_dir': pred_dir if pred_dir else None, - 'recursive_csv_scan': self.recursive_csv_cb.isChecked(), 'prediction_csv_path': None if folder_mode else (pred_csv if pred_csv else None), 'geotiff_path': geotiff_path if geotiff_path else None, 'geotiff_dir': (self.geotiff_dir_edit.text() or "").strip() or None, @@ -458,17 +439,15 @@ class Step11MapPanel(QWidget): def set_config(self, config): mode = config.get('step10_batch_mode', 'single') if mode == 'folder': - self.mode_folder_rb.setChecked(True) + self.batch_mode_combo.setCurrentIndex(1) else: - self.mode_single_rb.setChecked(True) + self.batch_mode_combo.setCurrentIndex(0) render_mode = config.get('render_mode', 'CSV 插值模式') idx = self.render_mode_combo.findText(render_mode) if idx >= 0: self.render_mode_combo.setCurrentIndex(idx) if config.get('prediction_csv_dir'): self.prediction_csv_dir_edit.setText(str(config['prediction_csv_dir'])) - if 'recursive_csv_scan' in config: - self.recursive_csv_cb.setChecked(bool(config['recursive_csv_scan'])) if 'prediction_csv_path' in config and config['prediction_csv_path']: self.prediction_csv_file.set_path(str(config['prediction_csv_path'])) if 'geotiff_path' in config and config['geotiff_path']: @@ -509,7 +488,7 @@ class Step11MapPanel(QWidget): path = step9_panel.output_file.get_path() if path: self.prediction_csv_dir_edit.setText(path) - self.mode_folder_rb.setChecked(True) + self.batch_mode_combo.setCurrentIndex(1) # 2. 安全抓取 Step 1 的真实掩膜文件(彻底拒绝瞎猜 roi.shp) step1_panel = factory.get_panel('step1') @@ -590,7 +569,7 @@ class Step11MapPanel(QWidget): # 修正:将后面代码中所有的 parent 替换为 main_win parent = main_win - if self.mode_folder_rb.isChecked(): + if self.batch_mode_combo.currentText() == "文件夹批量": # -------- CSV 插值批量 -------- if self.render_mode_combo.currentText() != "GeoTIFF 栅格模式": csv_list = self._collect_csv_paths_from_folder() @@ -599,7 +578,7 @@ class Step11MapPanel(QWidget): self, "输入验证失败", "所选文件夹中未找到 .csv 文件,或目录无效。\n" - "可勾选「包含子文件夹」以递归扫描。", + "请确认文件夹路径正确,且当前目录下存在 CSV 预测结果。", ) return if not PIPELINE_AVAILABLE: