初始化

This commit is contained in:
2026-04-22 12:27:49 +08:00
commit 4857cb6e45
73 changed files with 20927 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# UI界面模块
from .export_layout_tab import ExportImageTab
from .raster_tab import RasterTab
from .export_map_tab import ExportMapTab
__all__ = ['ExportImageTab', 'RasterTab', 'ExportMapTab']

View File

@@ -0,0 +1,373 @@
# -*- coding: utf-8 -*-
"""
栅格处理界面: 提供栅格重分类、栅格转矢量和小面积图斑消除的界面操作
"""
import os
import json
import arcpy
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
QPushButton, QFileDialog, QGridLayout,
QGroupBox, QMessageBox)
from tools.ui.runners.script_runner import ScriptRunner
class AcidStatsTab(QWidget):
"""栅格处理窗口部件类"""
def __init__(self, parent=None):
super(AcidStatsTab, self).__init__(parent)
self.main_window = parent
# 创建的导出线程
self.script_runner = ScriptRunner()
self.connect_signals()
self.init_ui()
self.setWindowTitle("酸化状况统计")
def connect_signals(self):
# 连接ScriptRunner的信号
self.script_runner.task_started.connect(self.on_script_started)
self.script_runner.task_finished.connect(self.on_script_finished)
self.script_runner.task_error.connect(self.on_script_error)
self.script_runner.task_log.connect(self.on_script_log)
self.script_runner.manager_log.connect(self.log_message)
def init_ui(self):
"""初始化用户界面"""
main_layout = QVBoxLayout(self)
# 批量处理区域
self.batch_mode_group = QGroupBox("批量处理设置")
batch_layout = QGridLayout()
# 输入行政区名称
batch_layout.addWidget(QLabel("行政区名称:"), 0, 0)
self.input_xzqmc_edit = QLineEdit()
batch_layout.addWidget(self.input_xzqmc_edit, 0, 1)
# 设置工作空间
batch_layout.addWidget(QLabel("工作空间:"), 1, 0)
self.input_workspace_edit = QLineEdit()
batch_layout.addWidget(self.input_workspace_edit, 1, 1)
self.browse_input_workspace_btn = QPushButton("选择GDB")
self.browse_input_workspace_btn.clicked.connect(self.browse_input_workspace)
batch_layout.addWidget(self.browse_input_workspace_btn, 1, 2)
# 批量输出文件夹
batch_layout.addWidget(QLabel("输出文件夹:"), 2, 0)
self.batch_output_folder_edit = QLineEdit()
batch_layout.addWidget(self.batch_output_folder_edit, 2, 1)
self.browse_batch_output_btn = QPushButton("选择文件夹")
self.browse_batch_output_btn.clicked.connect(self.browse_batch_output_folder)
batch_layout.addWidget(self.browse_batch_output_btn, 2, 2)
# 选择乡镇界线
batch_layout.addWidget(QLabel("乡镇界线:"), 3, 0)
self.xzq_features_edit = QLineEdit()
batch_layout.addWidget(self.xzq_features_edit, 3, 1)
batch_layout.addWidget(QLabel("GDB中的要素类名"), 3, 2)
# 选择地类图斑
batch_layout.addWidget(QLabel("地类图斑:"), 4, 0)
self.dltb_features_edit = QLineEdit()
batch_layout.addWidget(self.dltb_features_edit, 4, 1)
batch_layout.addWidget(QLabel("GDB中的要素类名"), 4, 2)
# 选择历史PH样点图斑
batch_layout.addWidget(QLabel("历史PH样点:"), 5, 0)
self.ph_samples_edit = QLineEdit()
batch_layout.addWidget(self.ph_samples_edit, 5, 1)
batch_layout.addWidget(QLabel("GDB中的要素类名"), 5, 2)
# 选择重分类后的PH面要素
batch_layout.addWidget(QLabel("分类后酸化PH面:"), 6, 0)
self.acid_ph_edit = QLineEdit()
batch_layout.addWidget(self.acid_ph_edit, 6, 1)
batch_layout.addWidget(QLabel("GDB中的要素类名"), 6, 2)
# 选择土壤类型图斑
batch_layout.addWidget(QLabel("土壤类型图斑:"), 7, 0)
self.soil_type_features_edit = QLineEdit()
batch_layout.addWidget(self.soil_type_features_edit, 7, 1)
batch_layout.addWidget(QLabel("GDB中的要素类名"), 7, 2)
# 选择三普PH赋值栅格
batch_layout.addWidget(QLabel("三普PH栅格:"), 8, 0)
self.assign_raster_edit = QLineEdit()
batch_layout.addWidget(self.assign_raster_edit, 8, 1)
self.assign_raster_btn = QPushButton("选择TIF")
self.assign_raster_btn.clicked.connect(self.browse_assign_raster)
batch_layout.addWidget(self.assign_raster_btn, 8, 2)
# 选择酸化PH赋值栅格
batch_layout.addWidget(QLabel("酸化PH栅格:"), 9, 0)
self.acid_raster_edit = QLineEdit()
batch_layout.addWidget(self.acid_raster_edit, 9, 1)
self.acid_raster_btn = QPushButton("选择TIF")
self.acid_raster_btn.clicked.connect(self.browse_acid_raster)
batch_layout.addWidget(self.acid_raster_btn, 9, 2)
self.batch_mode_group.setLayout(batch_layout)
# 操作按钮
btn_layout = QHBoxLayout()
# 导出酸化统计表
self.generate_sh_stat_btn = QPushButton("生成酸化统计表")
self.generate_sh_stat_btn.clicked.connect(self.on_generate_area_stat)
self.cancel_btn = QPushButton("取消")
# self.cancel_btn.clicked.connect(self.close)
btn_layout.addWidget(self.generate_sh_stat_btn)
btn_layout.addWidget(self.cancel_btn)
# 添加所有组件到主布局
main_layout.addWidget(self.batch_mode_group)
main_layout.addLayout(btn_layout)
self.setLayout(main_layout)
def browse_input_workspace(self):
"""浏览选择输入GDB"""
folder_path = QFileDialog.getExistingDirectory(self, "选择GDB数据库")
if folder_path:
self.input_workspace_edit.setText(folder_path)
def browse_batch_output_folder(self):
"""浏览选择表格输出文件夹"""
folder_path = QFileDialog.getExistingDirectory(self, "选择表格输出文件夹")
if folder_path:
self.batch_output_folder_edit.setText(folder_path)
def browse_assign_raster(self):
"""浏览选择赋值栅格 (支持栅格数据)"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择赋值栅格", "",
"栅格文件 (*.tif *.img *.jpg);;所有文件 (*)"
)
if not file_path:
return
if file_path.lower().endswith(('.tif', '.img', '.jpg')):
self.assign_raster_edit.setText(file_path)
self.log_message(f"已选择栅格文件: {file_path}")
else:
QMessageBox.warning(self, "选择错误", "不支持的文件类型。请选择有效的栅格文件。")
self.assign_raster_edit.clear()
def browse_acid_raster(self):
"""浏览选择赋值栅格 (支持栅格数据)"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择赋值栅格", "",
"栅格文件 (*.tif *.img *.jpg);;所有文件 (*)"
)
if not file_path:
return
if file_path.lower().endswith(('.tif', '.img', '.jpg')):
self.acid_raster_edit.setText(file_path)
self.log_message(f"已选择栅格文件: {file_path}")
else:
QMessageBox.warning(self, "选择错误", "不支持的文件类型。请选择有效的栅格文件。")
self.acid_raster_edit.clear()
def validate_inputs(self):
"""验证输入参数"""
# 验证工作空间路径
if not self.input_workspace_edit.text() or not arcpy.Exists(self.input_workspace_edit.text()):
QMessageBox.warning(self, "输入错误", "请选择有效的工作空间")
return False
if not self.batch_output_folder_edit.text():
QMessageBox.warning(self, "输入错误", "请选择批量处理输出文件夹")
return False
# 行政界线验证
xzq_path = os.path.join(self.input_workspace_edit.text(),self.xzq_features_edit.text())
if not xzq_path or not arcpy.Exists(xzq_path):
QMessageBox.warning(self, "输入错误", "行政界线要素路径无效或不存在")
return False
try:
desc = arcpy.Describe(xzq_path)
if desc.dataType != 'FeatureClass':
QMessageBox.warning(self, "输入错误", f"行政界线不是一个要素类:\n{xzq_path}")
return False
except Exception as e:
QMessageBox.warning(self, "输入错误", f"未知要素,请检查是否为有效要素类:\n{xzq_path}\n错误: {e}")
return False
# 地类图斑验证
dltb_path = os.path.join(self.input_workspace_edit.text(),self.dltb_features_edit.text())
if not dltb_path or not arcpy.Exists(dltb_path):
QMessageBox.warning(self, "输入错误", "地类图斑要素路径无效或不存在")
return False
try:
desc = arcpy.Describe(dltb_path)
if desc.dataType != 'FeatureClass':
QMessageBox.warning(self, "输入错误", f"地类图斑不是一个要素类:\n{dltb_path}")
return False
except Exception as e:
QMessageBox.warning(self, "输入错误", f"未知要素,请检查是否为有效要素类:\n{dltb_path}\n错误: {e}")
return False
# 验证PH样点
samples_path = os.path.join(self.input_workspace_edit.text(), self.ph_samples_edit.text())
if not samples_path or not arcpy.Exists(samples_path):
QMessageBox.warning(self, "输入错误", "PH样点要素路径无效或不存在")
return False
try:
desc = arcpy.Describe(samples_path)
if desc.dataType != 'FeatureClass':
QMessageBox.warning(self, "输入错误", f"PH样点不是一个要素类:\n{samples_path}")
return False
except Exception as e:
QMessageBox.warning(self, "输入错误", f"未知要素,请检查是否为有效要素类:\n{samples_path}\n错误: {e}")
return False
# 重分类后酸化面要素验证
acid_ph_path = os.path.join(self.input_workspace_edit.text(), self.acid_ph_edit.text())
if not acid_ph_path or not arcpy.Exists(acid_ph_path):
QMessageBox.warning(self, "输入错误", "酸化PH要素路径无效或不存在")
return False
try:
desc = arcpy.Describe(acid_ph_path)
if desc.dataType != 'FeatureClass':
QMessageBox.warning(self, "输入错误", f"酸化PH不是一个要素类:\n{acid_ph_path}")
return False
except Exception as e:
QMessageBox.warning(self, "输入错误", f"未知要素,请检查是否为有效要素类:\n{acid_ph_path}\n错误: {e}")
return False
# 土壤类型验证
trlx_path = os.path.join(self.input_workspace_edit.text(), self.soil_type_features_edit.text())
if not self.soil_type_features_edit.text() or not arcpy.Exists(trlx_path):
QMessageBox.warning(self, "输入错误", "土壤类型要素路径无效或不存在")
return False
try:
desc = arcpy.Describe(trlx_path)
if desc.dataType != 'FeatureClass':
QMessageBox.warning(self, "输入错误", f"土壤类型不是一个要素类:\n{trlx_path}")
return False
except Exception as e:
QMessageBox.warning(self, "输入错误", f"未知要素,请检查是否为有效要素类:\n{trlx_path}\n错误: {e}")
return False
# 验证输入是否为栅格
if not self.acid_raster_edit.text() or not self.assign_raster_edit.text():
QMessageBox.warning(self, "输入错误", "请选择输入栅格")
return False
elif not arcpy.Exists(self.acid_raster_edit.text()) or not arcpy.Exists(self.assign_raster_edit.text()):
QMessageBox.warning(self, "输入错误", "输入栅格不存在")
return False
return True
# 执行生成酸化统计表
def on_generate_area_stat(self):
"""执行生成面积统计表"""
if not self.validate_inputs():
return
try:
if self.main_window and hasattr(self.main_window, 'save_settings'):
self.main_window.update_settings()
self.main_window.save_settings()
settings_file = self.main_window.settings_file
params = {
"settings_path": settings_file,
}
task_id = self.script_runner.run_suanhua_stat(params)
if task_id:
self.log_message(f"[GUI] 面积统计任务 {task_id} 已添加到列队")
except Exception as e:
error_msg = f"处理过程中出错: {str(e)}"
QMessageBox.critical(self, "处理错误", error_msg)
self.log_message(error_msg)
def get_acid_stat_settings(self):
"""保存当前配置到JSON文件"""
config = {
"workspace_path": self.input_workspace_edit.text(),
"batch_output_folder": self.batch_output_folder_edit.text(),
"xzq_features": self.xzq_features_edit.text(),
"dltb_features": self.dltb_features_edit.text(),
"ph_samples": self.ph_samples_edit.text(),
"acid_ph_features": self.acid_ph_edit.text(),
"assign_raster": self.assign_raster_edit.text(),
"acid_raster": self.acid_raster_edit.text(),
"soil_type_features": self.soil_type_features_edit.text(),
"xzqmc": self.input_xzqmc_edit.text()
}
return config
def load_settings(self, settings):
"""从字典加载配置"""
try:
# 批处理参数
self.input_workspace_edit.setText(settings.get("workspace_path", ""))
self.batch_output_folder_edit.setText(settings.get("batch_output_folder", ""))
self.xzq_features_edit.setText(settings.get("xzq_features", ""))
self.dltb_features_edit.setText(settings.get("dltb_features", ""))
self.ph_samples_edit.setText(settings.get("ph_samples", ""))
self.acid_ph_edit.setText(settings.get("acid_ph_features", ""))
self.assign_raster_edit.setText(settings.get("assign_raster", ""))
self.acid_raster_edit.setText(settings.get("acid_raster", ""))
self.soil_type_features_edit.setText(settings.get("soil_type_features", ""))
return True
except Exception as e:
QMessageBox.critical(self, "加载配置错误", f"加载配置时出错: {str(e)}")
return False
def set_buttons_enabled(self, enabled):
"""设置按钮是否可用"""
self.generate_sh_stat_btn.setEnabled(enabled)
def on_script_started(self, task_id, task_description):
"""脚本开始执行"""
self.set_buttons_enabled(False)
self.log_message(f"{task_id}: 正在运行 - {task_description}")
def on_script_finished(self, task_id:str, success:bool, message:str):
"""脚本执行完成"""
self.set_buttons_enabled(True)
status = "完成" if success else "失败"
self.log_message(f"[{task_id}] 脚本执行{status}: {message}")
def on_script_error(self,task_id, error_msg):
"""脚本执行出错"""
self.set_buttons_enabled(True)
self.log_message(f"错误:{task_id}-{error_msg}")
# QMessageBox.critical(self, "错误", error_msg)
def on_script_log(self, task_id, message):
"""脚本输出日志"""
self.log_message(f"{task_id}: {message}")
def log_message(self, message):
"""日志输出"""
if self.main_window and hasattr(self.main_window, 'log_signal'):
self.main_window.log_signal.emit(message)
else:
print(message)
if __name__ == "__main__":
from PyQt6.QtWidgets import QApplication
import sys
app = QApplication(sys.argv)
window = AcidStatsTab()
window.show()
sys.exit(app.exec())

View File

@@ -0,0 +1,564 @@
# -*- coding: utf-8 -*-
"""
栅格处理界面: 提供栅格重分类、栅格转矢量和小面积图斑消除的界面操作
"""
import os
import json
import arcpy
import traceback
import multiprocessing
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
QPushButton, QFileDialog, QGridLayout, QCheckBox,
QGroupBox, QMessageBox, QSpinBox)
from tools.core.utils import common_utils
from tools.ui.runners.script_runner import ScriptRunner
from .config_editor_dialog import ConfigEditorDialogVisual
from ..components.file_list_group import FileListGroup
yunnan_dlbm = """
def yunnan_dlbm(dlbm):
if dlbm.startswith("0101"):
return "0101"
elif dlbm.startswith("0102"):
return "0102"
elif dlbm.startswith("0103"):
return "0103"
else:
return dlbm[:2]
"""
class XlsxToJpgTab(QWidget):
"""栅格处理窗口部件类"""
def __init__(self, parent=None):
super(XlsxToJpgTab, self).__init__(parent)
self.main_window = parent
# 创建的导出线程
self.script_runner = ScriptRunner()
self.connect_signals()
self.init_ui()
self.setWindowTitle("面积统计")
def connect_signals(self):
# 连接ScriptRunner的信号
self.script_runner.task_started.connect(self.on_script_started)
self.script_runner.task_finished.connect(self.on_script_finished)
self.script_runner.task_error.connect(self.on_script_error)
self.script_runner.task_log.connect(self.on_script_log)
self.script_runner.manager_log.connect(self.log_message)
def init_ui(self):
"""初始化用户界面"""
main_layout = QVBoxLayout(self)
# 批量处理区域
self.batch_mode_group = QGroupBox("批量处理设置")
batch_layout = QGridLayout()
# 配置文件选择 (公用)
batch_layout.addWidget(QLabel("配置文件:"), 0, 0)
self.config_file_edit = QLineEdit()
self.config_file_edit.setEnabled(False)
batch_layout.addWidget(self.config_file_edit, 0, 1,1,2)
# self.browse_config_btn = QPushButton("浏览...")
# self.browse_config_btn.clicked.connect(self.browse_config_file)
# self.browse_config_btn.setEnabled(False)
# batch_layout.addWidget(self.browse_config_btn, 0, 2)
# # 添加编辑配置文件的按钮 <--- Add this button
# self.edit_config_btn = QPushButton("编辑配置内容...")
# self.edit_config_btn.setEnabled(False)
# self.edit_config_btn.clicked.connect(self.open_config_editor)
# batch_layout.addWidget(self.edit_config_btn, 0, 3)
# 输入文件夹
batch_layout.addWidget(QLabel("重分类面要素:"), 1, 0)
self.input_folder_edit = QLineEdit()
batch_layout.addWidget(self.input_folder_edit, 1, 1)
self.browse_input_folder_btn = QPushButton("浏览...")
self.browse_input_folder_btn.clicked.connect(self.browse_input_folder)
batch_layout.addWidget(self.browse_input_folder_btn, 1, 2)
# 批量输出文件夹
batch_layout.addWidget(QLabel("输出文件夹:"), 2, 0)
self.batch_output_folder_edit = QLineEdit()
batch_layout.addWidget(self.batch_output_folder_edit, 2, 1)
self.browse_batch_output_btn = QPushButton("浏览...")
self.browse_batch_output_btn.clicked.connect(self.browse_batch_output_folder)
batch_layout.addWidget(self.browse_batch_output_btn, 2, 2)
# 选择乡镇界线
batch_layout.addWidget(QLabel("乡镇界线:"), 3, 0)
self.xzq_features_edit = QLineEdit()
batch_layout.addWidget(self.xzq_features_edit, 3, 1)
self.xzq_features_btn = QPushButton("浏览")
self.xzq_features_btn.clicked.connect(self.browse_xzq_features)
batch_layout.addWidget(self.xzq_features_btn, 3, 2)
# 选择地类图斑
batch_layout.addWidget(QLabel("地类图斑:"), 4, 0)
self.dltb_features_edit = QLineEdit()
batch_layout.addWidget(self.dltb_features_edit, 4, 1)
self.dltb_features_btn = QPushButton("浏览")
self.dltb_features_btn.clicked.connect(self.browse_dltb_features)
batch_layout.addWidget(self.dltb_features_btn, 4, 2)
# 输入行政区名称
batch_layout.addWidget(QLabel("行政区名称:"), 5, 0)
self.xzqmc_edit = QLineEdit()
batch_layout.addWidget(self.xzqmc_edit, 5, 1, 1, 2)
# 单选框
ext_layout = QHBoxLayout()
ext_layout.addWidget(QLabel("是否同时对行政区和地类进行平差(需要xlsx文件有其他地类的平差面积):"))
self.is_adjust_xzq_and_landuse_checkbox = QCheckBox()
ext_layout.addWidget(self.is_adjust_xzq_and_landuse_checkbox)
ext_layout.addStretch()
batch_layout.addLayout(ext_layout, 6, 0, 1, 2)
self.batch_mode_group.setLayout(batch_layout)
# 文件列表显示组
self.file_list_group = FileListGroup(self, "选择重分类后面要素")
self.file_list_group.load_files.connect(self.on_load_raster)
# --- 处理参数设置 ---
param_group = QGroupBox("处理参数")
param_layout = QVBoxLayout(param_group)
# 多进程设置
export_layout = QHBoxLayout()
mutiprocess_layout = QHBoxLayout()
mutiprocess_layout.addWidget(QLabel("使用多进程导出:"))
self.mutiprocess_check = QCheckBox()
self.mutiprocess_check.setChecked(True)
mutiprocess_layout.addWidget(self.mutiprocess_check)
mutiprocess_layout.addStretch()
export_layout.addLayout(mutiprocess_layout)
# 进程数量设置
process_count_layout = QHBoxLayout()
process_count_layout.addWidget(QLabel("进程数量:"))
self.process_count = QSpinBox()
self.process_count.setRange(1, multiprocessing.cpu_count())
self.process_count.setValue(max(1, multiprocessing.cpu_count() - 1)) # 默认使用CPU核心数-1
self.process_count.setFixedWidth(180)
process_count_layout.addWidget(self.process_count)
export_layout.addLayout(process_count_layout)
param_layout.addLayout(export_layout)
# 操作按钮
btn_layout = QHBoxLayout()
self.process_btn = QPushButton("导出到Excel")
self.process_btn.clicked.connect(self.on_start_processing)
self.excel_to_jpg_btn = QPushButton("Eexcel导出到JPG")
self.excel_to_jpg_btn.clicked.connect(self.on_export_excel_to_jpg)
self.cancel_btn = QPushButton("取消")
# self.cancel_btn.clicked.connect(self.close)
btn_layout.addWidget(self.process_btn)
btn_layout.addWidget(self.excel_to_jpg_btn)
btn_layout.addWidget(self.cancel_btn)
# 添加所有组件到主布局
main_layout.addWidget(self.batch_mode_group)
main_layout.addWidget(self.file_list_group)
main_layout.addWidget(param_group)
main_layout.addLayout(btn_layout)
self.setLayout(main_layout)
def browse_input_folder(self):
"""浏览选择输入文件夹"""
folder_path = QFileDialog.getExistingDirectory(self, "选择重分类后面要素文件夹")
if folder_path:
self.input_folder_edit.setText(folder_path)
self.on_load_raster()
def browse_batch_output_folder(self):
"""浏览选理输出文件夹"""
folder_path = QFileDialog.getExistingDirectory(self, "选择输出文件夹")
if folder_path:
self.batch_output_folder_edit.setText(folder_path)
def browse_config_file(self):
"""浏览选择配置文件"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择配置文件", "",
"JSON 文件 (*.json);;所有文件 (*)"
)
if file_path:
self.config_file_edit.setText(file_path)
# self.validate_config_file(file_path)
# self.edit_config_btn.setEnabled(True)
def browse_xzq_features(self):
"""浏览选择乡镇界线要素 (支持 shapefile)"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择乡镇界线要素 (Shapefile)", "",
"Shapefile 文件 (*.shp);;所有文件 (*)"
)
if not file_path:
return
if file_path.lower().endswith(".shp"):
self.xzq_features_edit.setText(file_path)
self.log_message(f"已选择 Shapefile: {file_path}")
else:
QMessageBox.warning(self, "选择错误", "不支持的文件类型。请选择 .shp 文件。")
self.xzq_features_edit.clear()
def browse_dltb_features(self):
"""浏览选择地类图斑要素 (支持 shapefile)"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择地类图斑要素 (Shapefile)", "",
"Shapefile 文件 (*.shp);;所有文件 (*)"
)
if not file_path:
return
if file_path.lower().endswith(".shp"):
self.dltb_features_edit.setText(file_path)
self.log_message(f"已选择 Shapefile: {file_path}")
else:
QMessageBox.warning(self, "选择错误", "不支持的文件类型。请选择 .shp 文件。")
self.dltb_features_edit.clear()
def validate_config_file(self, config_file_path):
"""对文件进行快速检查,以查看它是否类似于预期的配置结构。"""
if not os.path.exists(config_file_path):
return False
try:
with open(config_file_path, 'r', encoding='utf-8') as f:
config = json.load(f)
if "export_config" in config and isinstance(config["export_config"], dict):
if config["export_config"]:
first_item_value = next(iter(config["export_config"].values()))
if isinstance(first_item_value, dict) and all(k in first_item_value for k in ["项目名称", "标准等级"]):
self.log_message(f"配置文件 '{os.path.basename(config_file_path)}' 加载成功。")
return True
else:
self.log_message(f"警告: 配置文件 '{os.path.basename(config_file_path)}' 内容结构可能不匹配预期。")
return False
else:
self.log_message(f"警告: 配置文件 '{os.path.basename(config_file_path)}' 中的 'export_config' 部分为空。")
return False
else:
self.log_message(f"警告: 配置文件 '{os.path.basename(config_file_path)}' 缺少 'export_config' 键或格式不正确。")
return False
except json.JSONDecodeError:
self.log_message(f"警告: 配置文件 '{os.path.basename(config_file_path)}' 不是有效的 JSON 格式。")
return False
except Exception as e:
self.log_message(f"警告: 检查配置文件 '{os.path.basename(config_file_path)}' 时出错: {str(e)}")
return False
def open_config_editor(self):
"""打开配置文件编辑器对话框"""
config_path = self.config_file_edit.text()
if not config_path:
reply = QMessageBox.question(self, "编辑配置", "没有选择配置文件,是否创建一个新配置文件?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
if reply == QMessageBox.StandardButton.No:
return
config_path = ""
# 使用配置编辑对话框
dialog = ConfigEditorDialogVisual(config_path, self)
if dialog.exec() == ConfigEditorDialogVisual.DialogCode.Accepted:
new_path = dialog.get_new_config_path()
if new_path and new_path != self.config_file_edit.text():
self.config_file_edit.setText(new_path)
self.validate_config_file(new_path)
def on_load_raster(self):
"""加载图层列表"""
try:
data_source_paths = self.input_folder_edit.text()
self.file_list_group.load_file_btn.setEnabled(False)
# 清空列表
self.file_list_group.file_list.clear()
if not data_source_paths or not arcpy.Exists(data_source_paths):
self.log_message(f"输入路径不存在或不是有效的ArcGIS工作空间/文件夹: {data_source_paths}")
return
arcpy.env.workspace = data_source_paths
# 获取所有SHP 文件
shp_files = arcpy.ListFeatureClasses("*.shp")
if shp_files:
for raster in shp_files:
self.file_list_group.file_list.addItem(raster)
self.log_message(f"已加载 {self.file_list_group.file_list.count()} 个图层")
else:
self.log_message(f"文件夹 '{os.path.basename(data_source_paths)}' 中没有找到重分类后面要素文件。")
except Exception as e:
self.log_message(f"加载图层列表失败: {str(e)}")
traceback.print_exc()
finally:
self.file_list_group.load_file_btn.setEnabled(True)
def validate_inputs(self):
"""验证输入参数"""
# 验证配置文件路径
config_path = self.config_file_edit.text()
if not config_path or not os.path.exists(config_path):
QMessageBox.warning(self, "输入错误", "请选择有效的配置文件")
return False
try:
with open(config_path, 'r', encoding='utf-8') as f:
config_data = json.load(f)
except Exception as e:
QMessageBox.warning(self, "输入错误", f"无法加载或解析配置文件: {e}")
return False
if "export_config" not in config_data or not isinstance(config_data["export_config"], dict):
QMessageBox.warning(self, "输入错误", "配置文件的内容无效或缺少 'export_config' 部分。")
return False
standards_dict = config_data["export_config"]
if not standards_dict:
QMessageBox.warning(self, "输入错误", "配置文件中的 'export_config' 部分为空,没有定义任何处理参数。")
return False
# 验证'标准等级'用于配置重分类表
error_keys_remap = []
for key, value in standards_dict.items():
try:
levels_data = value.get("标准等级", {})
if not isinstance(levels_data, dict):
error_keys_remap.append(f"{key} ('标准等级' 格式不正确)")
continue
if not common_utils.create_remap_table(levels_data):
error_keys_remap.append(f"{key} ('标准等级' 内容无效)")
except Exception:
error_keys_remap.append(f"{key} (验证出错)")
if error_keys_remap:
QMessageBox.warning(self, "输入错误", "配置文件中部分参数的 '标准等级' 数据无效:\n" + "\n".join(error_keys_remap))
return False
# 验证输入、输出文件夹
if not self.input_folder_edit.text():
QMessageBox.warning(self, "输入错误", "请选择输入重分类后面要素文件夹")
return False
if not self.batch_output_folder_edit.text():
QMessageBox.warning(self, "输入错误", "请选择批量处理输出文件夹")
return False
if not self.xzqmc_edit.text():
QMessageBox.warning(self, "输入错误", "请输入县区名称")
return False
# 验证地类图斑文件
dltb_path = self.dltb_features_edit.text()
if not dltb_path and not arcpy.Exists(dltb_path):
QMessageBox.warning(self, "输入错误", "请选择有效的地类图斑文件")
return False
# 3. 验证乡镇界线文件
xzjx_path = self.xzq_features_edit.text()
if not xzjx_path or not arcpy.Exists(xzjx_path):
QMessageBox.warning(self, "输入错误", "请选择有效的乡镇界线文件。")
return False
# Optional: Check if the path points to a Feature Class (SHP or inside GDB)
try:
desc = arcpy.Describe(xzjx_path)
if desc.dataType != 'FeatureClass' and desc.dataType != 'ShapeFile':
QMessageBox.warning(self, "输入错误", f"选择的乡镇界线文件不是一个要素类:\n{xzjx_path}")
return False
except Exception as e:
QMessageBox.warning(self, "输入错误", f"无法描述乡镇界线文件,请检查是否为有效要素类:\n{xzjx_path}\n错误: {e}")
return False
# 验证是否选择文件
selected_files = [file.text() for file in self.file_list_group.file_list.selectedItems()]
if not selected_files:
QMessageBox.warning(self, "输入错误", "请在栅格列表中勾选要处理的文件。")
return False
return True
def on_start_processing(self):
"""开始处理栅格数据"""
if not self.validate_inputs():
return
try:
if self.main_window and hasattr(self.main_window, 'save_settings'):
self.main_window.update_settings()
self.main_window.save_settings()
settings_file = self.main_window.settings_file
# 获取公共参数 (从 VectorParamsWidget 获取)
selected_files = [file.text() for file in self.file_list_group.file_list.selectedItems()]
if self.mutiprocess_check.isChecked():
self.script_runner.set_max_concurrent(self.process_count.value())
else:
self.script_runner.set_max_concurrent(1)
# 计算DLTB字段
try:
dltb_polygon = self.dltb_features_edit.text()
if arcpy.Exists(dltb_polygon) and not arcpy.ListFields(dltb_polygon,"YJDLBM"):
arcpy.management.CalculateField(dltb_polygon, "YJDLBM", "!DLBM![:2]", "PYTHON3")
if arcpy.Exists(dltb_polygon) and not arcpy.ListFields(dltb_polygon,"YNDLBM"):
arcpy.management.CalculateField(dltb_polygon, "YNDLBM", "yunnan_dlbm(!DLBM!)", "PYTHON3", yunnan_dlbm)
# else:
# QMessageBox.critical(self, "处理错误", "地类图斑文件不存在")
# return
except Exception as e:
print(f"计算SHFJ字段时发生错误: {e}")
return
# 调用批量处理脚本
for raster_file in selected_files:
params = {
"settings_path": settings_file,
"reclassed_polygon": raster_file,
}
# 统计面积
task_id = self.script_runner.run_area_stat(params)
# if task_id:
# self.log_message(f"[GUI] 任务 {task_id} 处理 {raster_file} 已添加到列队")
except Exception as e:
error_msg = f"处理过程中出错: {str(e)}"
QMessageBox.critical(self, "处理错误", error_msg)
self.log_message(error_msg)
def on_export_excel_to_jpg(self):
self.log_message("[GUI] 开始处理")
if not self.validate_inputs():
return
try:
if self.main_window and hasattr(self.main_window, 'save_settings'):
self.main_window.update_settings()
self.main_window.save_settings()
settings_file = self.main_window.settings_file
params = {
"settings_path":settings_file
}
task_id = self.script_runner.run_excel_to_jpg(params)
if task_id:
self.log_message(f"[GUI] 批量处理任务 {task_id} 已添加到列队")
except Exception as e:
error_msg = f"处理过程中出错: {str(e)}"
QMessageBox.critical(self, "处理错误", error_msg)
self.log_message(error_msg)
def get_area_stat_settings(self):
"""保存当前配置到JSON文件"""
config = {
"input_folder": self.input_folder_edit.text(),
"batch_output_folder": self.batch_output_folder_edit.text(),
"config_file_path": self.config_file_edit.text(),
"xzq_features": self.xzq_features_edit.text(),
"dltb_features": self.dltb_features_edit.text(),
"xzqmc": self.xzqmc_edit.text(),
"is_by_xzq": self.is_adjust_xzq_and_landuse_checkbox.isChecked(),
}
return config
def load_settings(self, settings):
"""从字典加载配置"""
try:
# 批处理参数
self.input_folder_edit.setText(settings.get("input_folder", ""))
self.batch_output_folder_edit.setText(settings.get("batch_output_folder", ""))
# 配置文件路径和加载
config_file_path = settings.get("config_file_path", "")
self.config_file_edit.setText(config_file_path)
# self.edit_config_btn.setEnabled(bool(config_file_path) and os.path.exists(config_file_path))
if config_file_path:
self.validate_config_file(config_file_path)
self.xzq_features_edit.setText(settings.get("xzq_features", ""))
self.dltb_features_edit.setText(settings.get("dltb_features", ""))
if self.input_folder_edit.text():
self.on_load_raster()
return True
except Exception as e:
QMessageBox.critical(self, "加载配置错误", f"加载配置时出错: {str(e)}")
return False
def set_buttons_enabled(self, enabled):
"""设置按钮是否可用"""
self.process_btn.setEnabled(enabled)
self.excel_to_jpg_btn.setEnabled(enabled)
def update_config_file(self, config_file_path):
"""更新配置文件路径"""
self.config_file_edit.setText(config_file_path)
def on_script_started(self, task_id, task_description):
"""脚本开始执行"""
self.set_buttons_enabled(False)
self.log_message(f"{task_id}: 正在运行 - {task_description}")
def on_script_finished(self, task_id:str, success:bool, message:str):
"""脚本执行完成"""
self.set_buttons_enabled(True)
status = "完成" if success else "失败"
self.log_message(f"[{task_id}] 脚本执行{status}: {message}")
def on_script_error(self,task_id, error_msg):
"""脚本执行出错"""
self.set_buttons_enabled(True)
self.log_message(f"错误:{task_id}-{error_msg}")
# QMessageBox.critical(self, "错误", error_msg)
def on_script_log(self, task_id, message):
"""脚本输出日志"""
self.log_message(f"{task_id}: {message}")
def log_message(self, message):
"""日志输出"""
if self.main_window and hasattr(self.main_window, 'log_signal'):
self.main_window.log_signal.emit(message)
else:
print(message)
if __name__ == "__main__":
from PyQt6.QtWidgets import QApplication
import sys
app = QApplication(sys.argv)
window = XlsxToJpgTab()
window.show()
sys.exit(app.exec())

View File

@@ -0,0 +1,597 @@
# --- START OF FILE config_editor_dialog_visual.py ---
import json
import os
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QTextEdit,
QPushButton, QFileDialog, QMessageBox, QLabel,
QListWidget, QListWidgetItem, QStackedWidget,
QWidget, QTableWidget, QTableWidgetItem,
QHeaderView, QSizePolicy, QSplitter, QGroupBox, QGridLayout,
QLineEdit, QInputDialog) # Added QInputDialog
from PyQt6.QtCore import Qt
class ConfigEditorDialogVisual(QDialog):
"""
用于可视化编辑JSON配置文件的模态对话框
"""
def __init__(self, config_file_path, parent=None):
super().__init__(parent)
self.setWindowTitle("编辑配置文件 (可视化)")
self.resize(800, 600)
self.config_file_path = config_file_path
self._config_data = {} # Internal dictionary to hold the parsed JSON
self.original_content_hash = None # To track changes
self.init_ui()
self.load_config_data()
# Connect signals after UI is initialized and data is loaded
self.parameter_list_widget.currentItemChanged.connect(self._load_parameter_details)
self._load_parameter_details(self.parameter_list_widget.currentItem(), None) # Load initial item details if any
def init_ui(self):
"""初始化界面"""
main_layout = QVBoxLayout(self)
# Splitter for parameter list on the left and details on the right
splitter = QSplitter(Qt.Orientation.Horizontal)
main_layout.addWidget(splitter)
# Left Panel: Parameter List
left_widget = QWidget()
left_layout = QVBoxLayout(left_widget)
left_layout.addWidget(QLabel("参数列表:"))
self.parameter_list_widget = QListWidget()
left_layout.addWidget(self.parameter_list_widget)
# Buttons for adding/removing parameters
param_btn_layout = QHBoxLayout()
self.add_param_btn = QPushButton("添加参数")
self.remove_param_btn = QPushButton("移除参数")
param_btn_layout.addWidget(self.add_param_btn)
param_btn_layout.addWidget(self.remove_param_btn)
left_layout.addLayout(param_btn_layout)
self.add_param_btn.clicked.connect(self._add_parameter)
self.remove_param_btn.clicked.connect(self._remove_parameter)
splitter.addWidget(left_widget)
# Right Panel: Parameter Details (StackedWidget to potentially show empty state)
self.details_stack = QStackedWidget()
self.empty_details_widget = QLabel("请从左侧选择一个参数或添加新参数")
self.empty_details_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.details_stack.addWidget(self.empty_details_widget) # Index 0
self.parameter_details_widget = QWidget()
self._setup_parameter_details_ui(self.parameter_details_widget)
self.details_stack.addWidget(self.parameter_details_widget)
splitter.addWidget(self.details_stack)
# Set initial sizes for splitter panes
splitter.setSizes([200, 600])
# Bottom Buttons
button_layout = QHBoxLayout()
self.save_btn = QPushButton("保存")
self.save_as_btn = QPushButton("另存为...")
self.cancel_btn = QPushButton("取消")
self.save_btn.clicked.connect(self.save_config)
self.save_as_btn.clicked.connect(self.save_config_as)
self.cancel_btn.clicked.connect(self.reject)
button_layout.addWidget(self.save_btn)
button_layout.addWidget(self.save_as_btn)
button_layout.addStretch()
button_layout.addWidget(self.cancel_btn)
main_layout.addLayout(button_layout)
# Initial state: Disable save buttons until something is loaded/changed
self.save_btn.setEnabled(False)
self.save_as_btn.setEnabled(False)
self.remove_param_btn.setEnabled(False)
def _setup_parameter_details_ui(self, parent_widget):
"""Setup the UI for the parameter details panel"""
layout = QVBoxLayout(parent_widget)
# Fixed fields
fixed_fields_group = QGroupBox("基本信息")
fixed_fields_layout = QGridLayout(fixed_fields_group)
self.name_edit = QTextEdit()
self.name_edit.setFixedHeight(40) # Allow multi-line but not too tall
self.method_edit = QTextEdit()
self.method_edit.setFixedHeight(40)
self.param_level_edit = QTextEdit()
self.param_level_edit.setFixedHeight(40)
self.standard_edit = QTextEdit()
self.standard_edit.setFixedHeight(40)
fixed_fields_layout.addWidget(QLabel("项目名称:"), 0, 0)
fixed_fields_layout.addWidget(self.name_edit, 0, 1)
fixed_fields_layout.addWidget(QLabel("分析方法:"), 1, 0)
fixed_fields_layout.addWidget(self.method_edit, 1, 1)
fixed_fields_layout.addWidget(QLabel("项目分级:"), 2, 0)
fixed_fields_layout.addWidget(self.param_level_edit, 2, 1)
fixed_fields_layout.addWidget(QLabel("分级标准名称:"), 3, 0) # Renamed for clarity
fixed_fields_layout.addWidget(self.standard_edit, 3, 1)
layout.addWidget(fixed_fields_group)
# Standard Levels Table
levels_group = QGroupBox("标准等级")
levels_layout = QVBoxLayout(levels_group)
self.levels_table = QTableWidget(0, 2)
self.levels_table.setHorizontalHeaderLabels(["等级名称 (Key)", "标准值 (Value)"])
self.levels_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
self.levels_table.verticalHeader().setVisible(False)
levels_layout.addWidget(self.levels_table)
# Buttons for adding/removing levels
level_btn_layout = QHBoxLayout()
self.add_level_btn = QPushButton("添加等级")
self.remove_level_btn = QPushButton("移除等级")
level_btn_layout.addWidget(self.add_level_btn)
level_btn_layout.addWidget(self.remove_level_btn)
levels_layout.addLayout(level_btn_layout)
self.add_level_btn.clicked.connect(self._add_standard_level)
self.remove_level_btn.clicked.connect(self._remove_standard_level)
layout.addWidget(levels_group)
layout.addStretch() # Push everything to the top
# Connect signals for tracking changes (set dirty flag)
self.name_edit.textChanged.connect(self._set_dirty)
self.method_edit.textChanged.connect(self._set_dirty)
self.param_level_edit.textChanged.connect(self._set_dirty)
self.standard_edit.textChanged.connect(self._set_dirty)
self.levels_table.cellChanged.connect(self._set_dirty)
# Adding/removing items/levels also makes it dirty
self.add_param_btn.clicked.connect(self._set_dirty)
self.remove_param_btn.clicked.connect(self._set_dirty)
self.add_level_btn.clicked.connect(self._set_dirty)
self.remove_level_btn.clicked.connect(self._set_dirty)
def load_config_data(self):
"""加载文件内容到内部字典并填充UI"""
self._config_data = {} # Clear previous data
self.parameter_list_widget.clear()
if not self.config_file_path or not os.path.exists(self.config_file_path):
QMessageBox.warning(self, "加载错误", f"配置文件不存在或路径无效:\n{self.config_file_path}")
self.details_stack.setCurrentIndex(0) # Show empty message
return
try:
with open(self.config_file_path, 'r', encoding='utf-8') as f:
self._config_data = json.load(f)
# Store a hash or string representation for change detection
# Use json.dumps to handle ordering consistency
self.original_content_hash = hash(json.dumps(self._config_data, sort_keys=True, ensure_ascii=False))
# Populate the list widget from the loaded data
if "export_config" in self._config_data and isinstance(self._config_data["export_config"], dict):
for key in sorted(self._config_data["export_config"].keys()):
self.parameter_list_widget.addItem(key)
if self.parameter_list_widget.count() > 0:
self.parameter_list_widget.setCurrentRow(0)
self.details_stack.setCurrentIndex(1)
else:
self.details_stack.setCurrentIndex(0)
else:
QMessageBox.warning(self, "加载错误", "配置文件缺少 'export_config' 键或格式不正确。")
self.details_stack.setCurrentIndex(0)
except json.JSONDecodeError as e:
QMessageBox.critical(self, "加载错误", f"配置文件JSON格式无效:\n{e}")
self._config_data = {}
self.details_stack.setCurrentIndex(0)
except Exception as e:
QMessageBox.critical(self, "加载错误", f"无法读取或解析配置文件:\n{str(e)}")
self._config_data = {}
self.details_stack.setCurrentIndex(0)
def _load_parameter_details(self, current_item, previous_item):
"""Load details of the newly selected parameter"""
# Before loading new details, save changes from the previous item
if previous_item:
self._save_parameter_details(previous_item.text())
# Clear the details UI
self.name_edit.clear()
self.method_edit.clear()
self.param_level_edit.clear()
self.standard_edit.clear()
self.levels_table.setRowCount(0) # Clear table
if current_item:
key = current_item.text()
param_data = self._config_data.get("export_config", {}).get(key, {})
self.name_edit.setPlainText(param_data.get("项目名称", ""))
self.method_edit.setPlainText(param_data.get("分析方法", ""))
self.param_level_edit.setPlainText(param_data.get("项目分级", ""))
self.standard_edit.setPlainText(param_data.get("分级标准", ""))
# Load standard levels into the table
levels = param_data.get("标准等级", {})
if isinstance(levels, dict):
self.levels_table.setRowCount(len(levels))
# Sort levels by key (等级一, 等级二, etc.) for consistency
# for i, (level_key, level_value) in enumerate(sort(levels.items())):
for i, (level_key, level_value) in enumerate(levels.items()):
self.levels_table.setItem(i, 0, QTableWidgetItem(str(level_key)))
self.levels_table.setItem(i, 1, QTableWidgetItem(str(level_value).replace("\n", "\\n")))
else:
print(f"警告: 参数 '{key}''标准等级' 不是一个有效的字典。")
self.details_stack.setCurrentIndex(1)
self.remove_param_btn.setEnabled(True)
self._set_dirty(False)
else:
self.details_stack.setCurrentIndex(0)
self.remove_param_btn.setEnabled(False)
self._set_dirty(False)
def _save_parameter_details(self, key):
"""Save details from the UI widgets back to the internal dictionary for the given key"""
if not key or "export_config" not in self._config_data or key not in self._config_data["export_config"]:
return # Nothing to save if key is invalid or not in data
param_data = self._config_data["export_config"][key]
param_data["项目名称"] = self.name_edit.toPlainText()
param_data["分析方法"] = self.method_edit.toPlainText()
param_data["项目分级"] = self.param_level_edit.toPlainText()
param_data["分级标准"] = self.standard_edit.toPlainText()
# Save standard levels from the table
levels = {}
for row in range(self.levels_table.rowCount()):
key_item = self.levels_table.item(row, 0)
value_item = self.levels_table.item(row, 1)
if key_item and value_item:
level_key = key_item.text().strip()
level_value = value_item.text().strip().replace("\\n", "\n")
if level_key: # Only save if level key is not empty
levels[level_key] = level_value
else:
print(f"警告: 参数 '{key}' 中存在空的等级名称,该行将被忽略。")
param_data["标准等级"] = levels
# print(f"Saved details for parameter: {key}") # Debugging
def _add_parameter(self):
"""Add a new parameter item"""
key, ok = QInputDialog.getText(self, "添加新参数", "输入新的参数键 (例如: NEW):")
if ok and key:
key = key.strip()
if not key:
QMessageBox.warning(self, "输入错误", "参数键不能为空。")
return
if "export_config" not in self._config_data:
self._config_data["export_config"] = {}
if key in self._config_data["export_config"]:
QMessageBox.warning(self, "输入错误", f"参数键 '{key}' 已存在。")
return
# Add a default structure for the new parameter
self._config_data["export_config"][key] = {
"项目名称": "新项目名称",
"分析方法": "新分析方法",
"项目分级": "新项目分级",
"分级标准": "新分级标准",
"标准等级": {}
}
# Add to list widget and select it
self.parameter_list_widget.addItem(key)
# Re-sort the list
self.parameter_list_widget.sortItems()
# Find and select the new item
items = self.parameter_list_widget.findItems(key, Qt.MatchFlag.MatchExactly)
if items:
self.parameter_list_widget.setCurrentItem(items[0])
self._set_dirty()
def _remove_parameter(self):
"""Remove the currently selected parameter item"""
current_item = self.parameter_list_widget.currentItem()
if not current_item:
return # Nothing selected
key_to_remove = current_item.text()
reply = QMessageBox.question(
self, "移除参数", f"确定要移除参数 '{key_to_remove}' 吗?\n此操作不可撤销。",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
# Save details of the item being removed *before* removing it from data
# (This handles case where user edits an item then immediately removes it)
self._save_parameter_details(key_to_remove)
# Remove from internal data
if "export_config" in self._config_data and key_to_remove in self._config_data["export_config"]:
del self._config_data["export_config"][key_to_remove]
# Remove from list widget
row = self.parameter_list_widget.row(current_item)
self.parameter_list_widget.takeItem(row)
# Clear details panel and maybe select next item or show empty
self._load_parameter_details(self.parameter_list_widget.currentItem(), current_item)
self._set_dirty()
def _add_standard_level(self):
"""Add a new row to the standard levels table"""
current_item = self.parameter_list_widget.currentItem()
if not current_item:
QMessageBox.warning(self, "添加等级", "请先选择一个参数。")
return
row_count = self.levels_table.rowCount()
self.levels_table.insertRow(row_count)
# Add default items (editable)
self.levels_table.setItem(row_count, 0, QTableWidgetItem("新等级"))
self.levels_table.setItem(row_count, 1, QTableWidgetItem(""))
self._set_dirty()
def _remove_standard_level(self):
"""Remove selected rows from the standard levels table"""
# selected_rows = sorted(list(set(index.row() for index in self.levels_table.selectedIndexes())), reverse=True)
selected_rows = list(set(index.row() for index in self.levels_table.selectedIndexes()))
if not selected_rows:
QMessageBox.warning(self, "移除等级", "请选择要移除的等级行。")
return
for row in selected_rows:
self.levels_table.removeRow(row)
self._set_dirty()
def _set_dirty(self, dirty=True):
"""Set or clear the dirty flag and update button states"""
# This slot might receive a bool from cellChanged, or be called with True/False
# Ensure it's treated as a boolean
self._is_dirty = bool(dirty)
# Enable save buttons if dirty
self.save_btn.setEnabled(self._is_dirty)
self.save_as_btn.setEnabled(True)
# print(f"Dirty state: {self._is_dirty}") # Debugging
def is_dirty(self):
"""Check if the configuration has been modified"""
if not self._is_dirty:
return False
# Perform a more thorough check by saving current state to a temp dict
# and comparing its hash with the original hash
try:
# Save details of the currently selected item first
current_key = self.parameter_list_widget.currentItem()
if current_key:
self._save_parameter_details(current_key.text())
current_hash = hash(json.dumps(self._config_data, sort_keys=True, ensure_ascii=False))
return current_hash != self.original_content_hash
except Exception as e:
print(f"Error checking dirty state: {e}")
# If hashing fails, assume it's dirty to be safe
return True
def validate_config(self):
"""Validate the structure of the internal config data before saving"""
if "export_config" not in self._config_data or not isinstance(self._config_data["export_config"], dict):
QMessageBox.warning(self, "验证失败", "配置缺少主键 'export_config' 或其格式不正确。")
return False
standards_dict = self._config_data["export_config"]
error_keys = []
for key, value in standards_dict.items():
# Check for required fields and types
if not isinstance(value, dict):
error_keys.append(f"{key}: 结构不是字典")
continue
required_keys = ["项目名称", "分析方法", "项目分级", "分级标准", "标准等级"]
if not all(k in value for k in required_keys):
error_keys.append(f"{key}: 缺少必要字段")
continue
if not isinstance(value.get("标准等级"), dict):
error_keys.append(f"{key}: '标准等级' 不是字典")
continue
if error_keys:
QMessageBox.warning(self, "验证失败", "配置文件结构或内容存在问题:\n" + "\n".join(error_keys))
return False
return True
def save_config(self):
"""保存配置到当前文件路径"""
# Save details of the currently selected item before saving the whole config
current_item = self.parameter_list_widget.currentItem()
if current_item:
self._save_parameter_details(current_item.text())
if not self.validate_config():
return False
if not self.config_file_path:
# If no path is set (e.g., started with no file and added items), force Save As
return self.save_config_as()
return self._perform_save(self.config_file_path)
def save_config_as(self):
"""另存配置"""
file_path, _ = QFileDialog.getSaveFileName(
self, "另存配置文件", self.config_file_path if self.config_file_path else "",
"JSON 文件 (*.json);;所有文件 (*)"
)
if file_path:
# Save details of the currently selected item before saving the whole config
current_item = self.parameter_list_widget.currentItem()
if current_item:
self._save_parameter_details(current_item.text())
if not self.validate_config():
return False
if self._perform_save(file_path):
# Update the current file path if Save As was successful
self.config_file_path = file_path
# Reload from the new path to reset original_content_hash and dirty state
self.load_config_data()
QMessageBox.information(self, "另存成功", f"配置文件已另存为:\n{self.config_file_path}")
self.accept()
return True
return False
return False
def _perform_save(self, path):
"""Execute the saving of the config data to a file"""
try:
# Ensure directory exists
dir_name = os.path.dirname(path)
if dir_name and not os.path.exists(dir_name):
os.makedirs(dir_name, exist_ok=True)
with open(path, 'w', encoding='utf-8') as f:
# Use indent=4 for readability and ensure_ascii=False for Chinese chars
json.dump(self._config_data, f, indent=4, ensure_ascii=False)
# Update the original content hash after saving
self.original_content_hash = hash(json.dumps(self._config_data, sort_keys=True, ensure_ascii=False))
self._set_dirty(False) # Clear dirty flag
# For standard save, don't close the dialog, just inform
# QMessageBox.information(self, "保存成功", f"配置文件已保存:\n{path}") # Removed msgbox for standard save
return True
except Exception as e:
QMessageBox.critical(self, "保存失败", f"保存文件时出错:\n{str(e)}")
return False
def closeEvent(self, event):
"""Handle close event, ask to save if dirty"""
if self.is_dirty():
reply = QMessageBox.question(
self, "保存更改",
"配置文件已修改,是否保存?",
QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel,
QMessageBox.StandardButton.Save
)
if reply == QMessageBox.StandardButton.Save:
if self.save_config():
event.accept()
else:
event.ignore()
elif reply == QMessageBox.StandardButton.Discard:
event.accept()
else:
event.ignore()
else:
event.accept()
# Method to retrieve the potentially new file path after saving (e.g., Save As)
def get_new_config_path(self):
return self.config_file_path
# Example running (for testing this dialog itself)
if __name__ == '__main__':
from PyQt6.QtWidgets import QApplication
import sys
app = QApplication(sys.argv)
# Create a dummy config file for testing if it doesn't exist
test_dir = "temp_config_editor_test"
os.makedirs(test_dir, exist_ok=True)
test_config_path = os.path.join(test_dir, "test_config.json")
dummy_data = {
"export_config": {
"AB": {
"项目名称": "测试硼",
"分析方法": "测试方法",
"项目分级": "硼级别",
"分级标准": "硼标准",
"标准等级": {
"等级1": ">2.00",
"等级2": "1.00~2.00"
}
},
"ZN": {
"项目名称": "测试锌",
"分析方法": "锌方法",
"项目分级": "锌级别",
"分级标准": "锌标准",
"标准等级": {
"A级": ">3.00",
"B级": "2.00~3.00",
"C级": "<2.00"
}
}
}
}
if not os.path.exists(test_config_path):
try:
with open(test_config_path, 'w', encoding='utf-8') as f:
json.dump(dummy_data, f, indent=4, ensure_ascii=False)
print(f"Created dummy config file: {test_config_path}")
except Exception as e:
print(f"Error creating dummy file: {e}")
test_config_path = "" # Clear path if creation fails
dialog = ConfigEditorDialogVisual(test_config_path)
result = dialog.exec()
if result == QDialog.DialogCode.Accepted:
final_path = dialog.get_new_config_path()
print(f"Dialog accepted. Final config path: {final_path}")
else:
print("Dialog rejected or cancelled.")
# Optional: Clean up test files/directory
# import shutil
# if os.path.exists(test_dir):
# shutil.rmtree(test_dir)
sys.exit(app.exec())

View File

@@ -0,0 +1,291 @@
import multiprocessing
import os
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
QLineEdit, QFileDialog, QMessageBox, QListWidget,
QComboBox, QSpinBox, QGroupBox, QFormLayout, QCheckBox)
from ..runners.script_runner import ScriptRunner
from ..components.file_list_group import FileListGroup
class ExportImageTab(QWidget):
"""导出成果图标签页"""
def __init__(self, parent=None):
super().__init__(parent)
self.main_window = parent
self.runner = ScriptRunner(self)
self.connect_signals()
self.init_ui()
self.load_settings()
self.on_load_files()
def connect_signals(self):
"""连接脚本运行器信号"""
# 连接ScriptRunner的信号
self.runner.task_started.connect(self.on_script_started)
self.runner.task_finished.connect(self.on_script_finished)
self.runner.task_error.connect(self.on_script_error)
self.runner.task_log.connect(self.on_script_log)
def init_ui(self):
main_layout = QVBoxLayout(self)
# 输入输出设置组
io_group = QGroupBox("输入输出设置")
io_layout = QFormLayout()
# 地图文档文件夹选择
data_layout = QHBoxLayout()
self.input_aprx_path_edit = QLineEdit()
data_layout.addWidget(self.input_aprx_path_edit)
browse_data_btn = QPushButton("浏览...")
browse_data_btn.clicked.connect(self.browse_data_source)
data_layout.addWidget(browse_data_btn)
io_layout.addRow("工程文件夹:", data_layout)
# 输出路径选择
output_layout = QHBoxLayout()
self.output_image_path_edit = QLineEdit()
output_layout.addWidget(self.output_image_path_edit)
browse_output_btn = QPushButton("浏览...")
browse_output_btn.clicked.connect(self.browse_output)
output_layout.addWidget(browse_output_btn)
io_layout.addRow("输出文件夹:", output_layout)
io_group.setLayout(io_layout)
main_layout.addWidget(io_group)
# 添加文件列表布局
self.file_list_group = FileListGroup(self, "选择要导出的文件")
self.file_list_group.load_files.connect(self.on_load_files)
main_layout.addWidget(self.file_list_group)
# 导出设置组
export_group = QGroupBox("导出设置")
export_layout = QFormLayout()
# 格式选择
self.format_combo = QComboBox()
self.format_combo.addItems(["PDF", "PNG", "JPG", "TIFF", "EPS", "SVG", "AI"])
export_layout.addRow("导出格式:", self.format_combo)
# 分辨率设置
self.resolution_spinbox = QSpinBox()
self.resolution_spinbox.setRange(72, 1200)
self.resolution_spinbox.setSingleStep(12)
self.resolution_spinbox.setValue(300)
self.resolution_spinbox.setSuffix(" DPI")
export_layout.addRow("分辨率:", self.resolution_spinbox)
# 强制重新生成选项
self.image_force_regenerate_check = QCheckBox("强制重新生成工程文件")
self.image_force_regenerate_check.setChecked(False)
self.image_force_regenerate_check.setToolTip("勾选后将忽略已存在的工程文件,重新生成")
export_layout.addRow("处理选项:", self.image_force_regenerate_check)
# 多进程设置
self.use_multiprocessing = QCheckBox("使用多进程导出")
self.use_multiprocessing.setChecked(True)
export_layout.addRow("批量模式:", self.use_multiprocessing)
# 进程数量设置
self.process_count = QSpinBox()
self.process_count.setRange(1, multiprocessing.cpu_count())
self.process_count.setValue(max(1, multiprocessing.cpu_count() - 1)) # 默认使用CPU核心数-1
export_layout.addRow("进程数量:", self.process_count)
export_group.setLayout(export_layout)
main_layout.addWidget(export_group)
# 操作按钮
self.export_existing_btn = QPushButton("导出成果图")
self.export_existing_btn.setToolTip("仅导出输出文件夹中已存在的工程文件")
self.export_existing_btn.clicked.connect(self.on_export_existing)
main_layout.addWidget(self.export_existing_btn)
# main_layout.addStretch()
def browse_data_source(self):
"""浏览工程目录"""
initial_dir = ""
if os.path.exists(self.input_aprx_path_edit.text()):
initial_dir = os.path.dirname(self.input_aprx_path_edit.text())
dir_path = QFileDialog.getExistingDirectory(self, "选择工程目录", initial_dir)
if dir_path:
self.input_aprx_path_edit.setText(dir_path)
self.on_load_files()
def browse_output(self):
"""选择输出路径"""
initial_dir = ""
if os.path.exists(self.output_image_path_edit.text()):
initial_dir = os.path.dirname(self.output_image_path_edit.text())
dir_path = QFileDialog.getExistingDirectory(self, "选择输出路径", initial_dir)
if dir_path:
self.output_image_path_edit.setText(dir_path)
def on_load_files(self):
"""加载图层列表"""
try:
input_aprx_path = self.input_aprx_path_edit.text()
if not os.path.exists(input_aprx_path):
QMessageBox.warning(self, "错误", "请先选择输入文件夹")
return
# 清空列表
self.file_list_group.file_list.clear()
# 添加文件
for file_name in os.listdir(input_aprx_path):
file_path = os.path.join(input_aprx_path, file_name)
if file_name.endswith(".aprx") and os.path.isfile(file_path):
self.file_list_group.file_list.addItem(file_path)
self.log_message(f"已加载 {self.file_list_group.file_list.count()} 个文件")
except Exception as e:
self.log_message(f"加载文件失败: {str(e)}")
def on_script_started(self, task_id, task_description):
"""脚本开始执行回调"""
self.set_buttons_enabled(False)
self.log_message(f"{task_id}: 正在运行 - {task_description}")
def on_script_finished(self, task_id:str, success:bool, message:str):
"""脚本执行完成回调"""
self.set_buttons_enabled(True)
if success:
QMessageBox.information(self, "成功", "成果图导出完成!")
else:
QMessageBox.warning(self, "失败", "导出过程中出现错,请查看日志详情")
def on_script_error(self, error_msg):
"""脚本执行错误回调"""
self.set_buttons_enabled(True)
self.log_message(f"错误: {error_msg}")
QMessageBox.critical(self, "错误", error_msg)
def on_script_log(self, task_id, message):
"""脚本输出日志"""
self.log_message(f"{task_id}: {message}")
def log_message(self, message):
"""日志输出"""
if self.main_window and hasattr(self.main_window, 'log_signal'):
self.main_window.log_signal.emit(message)
else:
print(message)
def set_buttons_enabled(self, enabled):
"""设置按钮启用状态"""
self.export_existing_btn.setEnabled(enabled)
def validate_and_params(self):
"""验证输入参数并返回参数"""
# 验证工程文件
aprx_files = self.file_list_group.file_list.selectedItems()
if not aprx_files or len(aprx_files) == 0:
QMessageBox.warning(self, "警告", "请选择要导出的文件")
return None
# 验证工程目录
input_aprx_folder = self.input_aprx_path_edit.text()
if not os.path.exists(input_aprx_folder):
QMessageBox.warning(self, "警告", "请选择有效的工程文件目录")
return None
# 验证输出路径
output_image_path = self.output_image_path_edit.text()
if not os.path.exists(output_image_path):
QMessageBox.warning(self, "警告", "请选择输出路径")
return None
aprx_file_list = [file.text() for file in aprx_files]
output_image_path = os.path.join(output_image_path, self.format_combo.currentText().lower())
return {
'input_aprx_folder': input_aprx_folder,
'aprx_file_list': aprx_file_list,
'output_image_path': output_image_path
}
def get_layout_settings(self):
""" 获取布局设置 """
config = {
'input_aprx_path': self.input_aprx_path_edit.text(),
'output_image_path': self.output_image_path_edit.text(),
'default_format': self.format_combo.currentText(),
'resolution': self.resolution_spinbox.value(),
'force_regenerate': self.image_force_regenerate_check.isChecked(),
'use_multiprocessing': self.use_multiprocessing.isChecked(),
'process_count': self.process_count.value()
}
return config
def load_settings(self):
"""加载设置"""
if not self.main_window:
return
try:
settings = self.main_window.settings
export_image_settings = settings.get('export_image_settings', {})
self.input_aprx_path_edit.setText(export_image_settings.get('input_aprx_path', ''))
self.output_image_path_edit.setText(export_image_settings.get('output_image_path', ''))
# 设置导出格式
format_index = self.format_combo.findText(export_image_settings.get('default_format', 'PDF'))
if format_index >= 0:
self.format_combo.setCurrentIndex(format_index)
# 设置分辨率
self.resolution_spinbox.setValue(export_image_settings.get('resolution', 300))
except Exception as e:
self.log_message(f"加载设置失败: {str(e)}")
def on_export_layout(self):
"""仅导出布局按钮点击事件"""
# 验证并获取参数
layout_params = self.validate_and_params()
if not layout_params:
return
# 准备导出布局参数
layout_params['export_format'] = self.format_combo.currentText()
layout_params['resolution'] = self.resolution_spinbox.value()
layout_params['image_force_regenerate'] = self.image_force_regenerate_check.isChecked()
print(layout_params)
# 调用导出布局脚本
# self.runner.run_export_layout(layout_params)
def on_export_existing(self):
"""导出已有工程按钮点击事件"""
# 验证并获取参数
layout_params = self.validate_and_params()
if not layout_params:
return
# 确认是否导出所有工程文件
result = QMessageBox.question(
self,
"确认",
f"将导出 {len(layout_params['aprx_file_list'])} 个工程文件的布局,是否继续?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No
)
if result == QMessageBox.StandardButton.No:
return
# 准备批量导出布局参数
layout_params['export_format'] = self.format_combo.currentText()
layout_params['resolution'] = self.resolution_spinbox.value()
layout_params['image_force_regenerate'] = self.image_force_regenerate_check.isChecked()
layout_params['use_multiprocessing'] = self.use_multiprocessing.isChecked()
layout_params['process_count'] = self.process_count.value()
# 调用批量导出布局脚本
# print(layout_params)
self.runner.run_export_layout(layout_params)

View File

@@ -0,0 +1,385 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
导出地图选项卡模块
"""
import os
import arcpy
import traceback
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QFormLayout, QLabel,
QLineEdit, QPushButton, QCheckBox, QFileDialog, QMessageBox)
from PyQt6.QtCore import pyqtSignal
from ..components.file_list_group import FileListGroup
from ..runners.script_runner import ScriptRunner
class ExportMapTab(QWidget):
"""导出地图选项卡"""
# 定义日志信号,用于将日志消息传递给主窗口
log_signal = pyqtSignal(str)
def __init__(self, parent=None):
super().__init__(parent)
self.main_window = parent
self.setupUi()
self.load_settings()
def setupUi(self):
"""设置界面"""
# 主布局
self.main_layout = QVBoxLayout(self)
# 参数区域
self.form_layout = QFormLayout()
# 区县名称
self.county_name_edit = QLineEdit()
self.form_layout.addRow("区县名称:", self.county_name_edit)
# 配置文件
config_layout = QHBoxLayout()
self.config_file_edit = QLineEdit()
self.config_file_edit.setEnabled(False)
config_layout.addWidget(self.config_file_edit)
# self.config_file_browse = QPushButton("浏览...")
# self.config_file_browse.clicked.connect(self.on_browse_config_file)
# config_layout.addWidget(self.config_file_browse)
self.form_layout.addRow("配置文件:", config_layout)
# 模板文件
self.template_aprx_file_edit = QLineEdit()
self.template_path_browse = QPushButton("浏览...")
self.template_path_browse.clicked.connect(self.on_browse_template_path)
template_layout = QHBoxLayout()
template_layout.addWidget(self.template_aprx_file_edit)
template_layout.addWidget(self.template_path_browse)
self.form_layout.addRow("模板文件:", template_layout)
# 数据源路径
self.data_source_path_edit = QLineEdit()
self.data_source_path_browse = QPushButton("浏览...")
self.data_source_path_browse.clicked.connect(self.on_browse_data_source_path)
data_source_layout = QHBoxLayout()
data_source_layout.addWidget(self.data_source_path_edit)
data_source_layout.addWidget(self.data_source_path_browse)
self.form_layout.addRow("数据源路径:", data_source_layout)
# 符号系统路径
self.symbol_path_edit = QLineEdit()
self.symbol_path_browse = QPushButton("浏览...")
self.symbol_path_browse.clicked.connect(self.on_browse_symbol_path)
symbol_layout = QHBoxLayout()
symbol_layout.addWidget(self.symbol_path_edit)
symbol_layout.addWidget(self.symbol_path_browse)
self.form_layout.addRow("符号系统路径:", symbol_layout)
# 统计表格图片路径
self.pic_path_edit = QLineEdit()
self.pic_path_browse = QPushButton("浏览...")
self.pic_path_browse.clicked.connect(self.on_browse_pic_path)
pic_layout = QHBoxLayout()
pic_layout.addWidget(self.pic_path_edit)
pic_layout.addWidget(self.pic_path_browse)
self.form_layout.addRow("面积统计图片路径:", pic_layout)
# 输出路径
self.output_path_edit = QLineEdit()
self.output_path_browse = QPushButton("浏览...")
self.output_path_browse.clicked.connect(self.on_browse_output_path)
output_layout = QHBoxLayout()
output_layout.addWidget(self.output_path_edit)
output_layout.addWidget(self.output_path_browse)
self.form_layout.addRow("输出路径:", output_layout)
# 文件列表布局
self.file_list_group = FileListGroup(self, "选择要导出的要素类:")
self.file_list_group.load_files.connect(self.on_load_polygon)
# 选项区域
options_layout = QVBoxLayout()
# 强制重新生成选项
self.force_regenerate_check = QCheckBox("强制重新生成工程文件")
options_layout.addWidget(self.force_regenerate_check)
# 添加到主布局
self.main_layout.addLayout(self.form_layout)
self.main_layout.addWidget(self.file_list_group)
self.main_layout.addLayout(options_layout)
# 导出按钮
self.export_button = QPushButton("导出地图")
self.export_button.clicked.connect(self.on_export_map)
self.main_layout.addWidget(self.export_button)
# 状态标签
self.status_label = QLabel("状态: 就绪")
self.main_layout.addWidget(self.status_label)
def on_browse_config_file(self):
"""浏览配置文件"""
initial_dir = ""
if os.path.exists(self.config_file_edit.text()):
initial_dir = self.config_file_edit.text()
file_path, _ = QFileDialog.getOpenFileName(self, "选择配置文件", initial_dir, "JSON文件 (*.json);;所有文件 (*.*)")
if file_path:
self.config_file_edit.setText(file_path)
def on_browse_template_path(self):
"""浏览模板文件"""
initial_dir = ""
if os.path.exists(self.template_aprx_file_edit.text()):
initial_dir = self.template_aprx_file_edit.text()
file_path, _ = QFileDialog.getOpenFileName(self, "选择模板文件", initial_dir, "ArcGIS Pro工程文件 (*.aprx)")
if file_path:
self.template_aprx_file_edit.setText(file_path)
def on_browse_data_source_path(self):
"""浏览数据源路径"""
initial_dir = ""
if os.path.exists(self.data_source_path_edit.text()):
initial_dir = os.path.dirname(self.data_source_path_edit.text())
folder_path = QFileDialog.getExistingDirectory(self, "选择数据源文件夹", initial_dir)
if folder_path and folder_path.endswith(".gdb"):
self.data_source_path_edit.setText(folder_path)
# 自动加载图层列表
self.on_load_polygon()
else:
QMessageBox.warning(self, "错误", "请选择GDB的数据源。")
def on_browse_symbol_path(self):
"""浏览符号系统路径"""
initial_dir = ""
if os.path.exists(self.symbol_path_edit.text()):
initial_dir = os.path.dirname(self.symbol_path_edit.text())
folder_path = QFileDialog.getExistingDirectory(self, "选择符号系统文件夹", initial_dir)
if folder_path:
self.symbol_path_edit.setText(folder_path)
def on_browse_pic_path(self):
"""浏览符号系统路径"""
initial_dir = ""
if os.path.exists(self.pic_path_edit.text()):
initial_dir = os.path.dirname(self.pic_path_edit.text())
folder_path = QFileDialog.getExistingDirectory(self, "选择符号系统文件夹", initial_dir)
if folder_path:
self.pic_path_edit.setText(folder_path)
def on_browse_output_path(self):
"""浏览输出路径"""
initial_dir = ""
if os.path.exists(self.output_path_edit.text()):
initial_dir = os.path.dirname(self.output_path_edit.text())
folder_path = QFileDialog.getExistingDirectory(self, "选择输出文件夹", initial_dir)
if folder_path.endswith(".gdb"):
QMessageBox.warning(self, "警告", "请选择有效的输出路径")
return
self.output_path_edit.setText(folder_path)
def on_load_polygon(self):
"""加载图层列表"""
try:
data_source_paths = self.data_source_path_edit.text()
# 清空列表
self.file_list_group.file_list.clear()
# 添加图层
if arcpy.Exists(data_source_paths):
arcpy.env.workspace = data_source_paths
for polygon in arcpy.ListFeatureClasses(feature_type="Polygon"):
self.file_list_group.file_list.addItem(polygon)
self.log_message(f"已加载 {self.file_list_group.file_list.count()} 个图层")
except Exception as e:
self.log_error(f"加载图层列表失败: {str(e)}")
traceback.print_exc()
def get_selected_polygon(self):
"""获取选中的图层列表"""
selected_items = self.file_list_group.file_list.selectedItems()
return [item.text() for item in selected_items]
def on_export_map(self):
"""导出地图按钮点击事件"""
try:
# 验证并获取参数
params = self.validate_and_params()
if not params:
return
polygon_list = params.get('polygon_list')
# 清空日志
self.log_message("开始导出工程文件...")
self.log_message(f"选中的图层: {polygon_list}")
# 创建导出线程
self.runner = ScriptRunner()
self.runner.task_log.connect(self.on_script_log)
self.runner.task_error.connect(self.log_error)
self.runner.task_finished.connect(self.on_script_finished)
# 禁用导出按钮
self.export_button.setEnabled(False)
self.status_label.setText("状态: 正在导出...")
# 运行导出脚本(使用外部窗口)
# print(params)
self.runner.run_export_map(params)
except Exception as e:
self.log_error(f"导出准备过程出错: {str(e)}")
traceback.print_exc()
def log_message(self, message):
"""添加日志消息"""
# 如果父窗口存在,使用父窗口的日志功能
if self.main_window and hasattr(self.main_window, 'log_signal'):
self.main_window.log_signal.emit(message)
else:
# 否则发射自己的信号
self.log_signal.emit(message)
def log_error(self, message):
"""添加错误日志"""
self.log_message(f"错误: {message}")
def on_script_log(self, task_id, message):
"""脚本输出日志"""
self.log_message(f"{task_id}: {message}")
def on_script_finished(self, task_id:str, success:bool, message:str):
"""导出完成事件处理"""
# 恢复导出按钮
self.export_button.setEnabled(True)
if success:
self.status_label.setText("状态: 导出完成")
QMessageBox.information(self, "成功", "地图导出完成!")
else:
self.status_label.setText("状态: 导出失败")
QMessageBox.warning(self, "失败", "地图导出过程中出现错误,请查看日志详情")
def validate_and_params(self):
# 获取输入参数
county_name = self.county_name_edit.text().strip()
if not county_name:
QMessageBox.warning(self, "错误", "请输入区县名称")
return
# 获取面要素列表
polygon_list = self.get_selected_polygon()
if not polygon_list:
QMessageBox.warning(self, "错误", "请选择至少一个要素")
return
# 获取配置文件
config_file = self.config_file_edit.text()
if not os.path.exists(config_file):
QMessageBox.warning(self, "错误", "配置文件不存在")
return
# 获取模板文件
template_aprx_file = self.template_aprx_file_edit.text()
if not os.path.exists(template_aprx_file):
QMessageBox.warning(self, "错误", "模板文件不存在")
return
# 获取输出路径
output_path = self.output_path_edit.text()
if not output_path:
QMessageBox.warning(self, "错误", "请选择输出路径")
return
# 获取数据源路径
data_source_path = self.data_source_path_edit.text()
if not os.path.exists(data_source_path) and not data_source_path.endswith(".gdb"):
QMessageBox.warning(self, "错误", "请确认是否是有效数据源")
return
# 获取符号系统路径
symbol_path = self.symbol_path_edit.text()
if not os.path.exists(symbol_path):
QMessageBox.warning(self, "错误", "符号系统路径不存在")
return
# 获取图片源路径
pic_path = self.pic_path_edit.text()
if not os.path.exists(pic_path):
QMessageBox.warning(self, "错误", "符号系统路径不存在")
return
# 是否强制重新生成
force_regenerate = self.force_regenerate_check.isChecked()
# 准备参数
return {
'config_file': config_file,
'county_name': county_name,
'polygon_list': polygon_list,
'template_aprx_file': template_aprx_file,
'output_path': output_path,
'data_source_path': data_source_path,
'symbol_path': symbol_path,
'force_regenerate': force_regenerate,
'pic_path': pic_path
}
def load_settings(self):
"""从主窗口加载设置"""
try:
if not self.main_window or not hasattr(self.main_window, 'settings'):
return
settings = self.main_window.settings
if 'export_map_settings' in settings:
paths = settings['export_map_settings']
# 直接使用get方法设置默认值
self.config_file_edit.setText(paths.get("config_file", ""))
self.county_name_edit.setText(paths.get("county_name", ""))
self.template_aprx_file_edit.setText(paths.get("template_aprx_file", ""))
self.output_path_edit.setText(paths.get("output_path", ""))
self.data_source_path_edit.setText(paths.get("data_source_path", ""))
self.symbol_path_edit.setText(paths.get("symbol_path", ""))
self.pic_path_edit.setText(paths.get("pic_path", ""))
except Exception as e:
self.log_error(f"加载设置失败: {str(e)}")
def get_settings(self):
"""获取当前设置,供主窗口保存使用"""
return {
'config_file': self.config_file_edit.text(),
'county_name': self.county_name_edit.text(),
'template_aprx_file': self.template_aprx_file_edit.text(),
'output_path': self.output_path_edit.text(),
'data_source_path': self.data_source_path_edit.text(),
'symbol_path': self.symbol_path_edit.text(),
'pic_path': self.pic_path_edit.text()
}
def update_config_file(self, config_file):
self.config_file_edit.setText(config_file)

View File

@@ -0,0 +1,244 @@
# -*- coding: utf-8 -*-
"""
栅格处理共享组件: 提供栅格重分类、栅格转矢量和小面积图斑消除的共享UI组件
"""
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QCheckBox,
QGroupBox, QTableWidget, QTableWidgetItem, QHeaderView,
QPushButton, QSpinBox, QDoubleSpinBox, QComboBox
)
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtGui import QDoubleValidator
class ReclassificationWidget(QWidget):
"""重分类参数组件"""
paramsChanged = pyqtSignal() # 参数变化信号
def __init__(self, parent=None):
super(ReclassificationWidget, self).__init__(parent)
self.init_ui()
def init_ui(self):
"""初始化UI"""
layout = QVBoxLayout(self)
group_box = QGroupBox("重分类映射表")
group_layout = QVBoxLayout(group_box)
self.reclass_table = QTableWidget()
self.reclass_table.setColumnCount(3)
self.reclass_table.setHorizontalHeaderLabels(["", "", "新值"])
# PyQt6 中horizontalHeader().setSectionResizeMode 接受 QHeaderView.ResizeMode 枚举
self.reclass_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) # type: ignore
# PyQt6 中setSelectionBehavior 接受 QAbstractItemView.SelectionBehavior 枚举
from PyQt6.QtWidgets import QAbstractItemView # 需要导入 QAbstractItemView
self.reclass_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) # 整行选择
# 连接单元格变化信号
self.reclass_table.itemChanged.connect(self.on_item_changed)
# 添加默认行
self.add_row()
group_layout.addWidget(self.reclass_table)
# 添加/删除行按钮
btn_layout = QHBoxLayout()
add_row_btn = QPushButton("添加行")
del_row_btn = QPushButton("删除行")
# PyQt6 信号槽连接
add_row_btn.clicked.connect(self.add_row)
del_row_btn.clicked.connect(self.del_row)
btn_layout.addWidget(add_row_btn)
btn_layout.addWidget(del_row_btn)
group_layout.addLayout(btn_layout)
layout.addWidget(group_box)
def add_row(self, from_value=None, to_value=None, new_value=None):
"""添加一行重分类规则"""
row_count = self.reclass_table.rowCount()
self.reclass_table.insertRow(row_count)
# 设置默认值或传入的值
from_item = QTableWidgetItem(str(from_value) if from_value is not None else "")
to_item = QTableWidgetItem(str(to_value) if to_value is not None else "")
new_value_item = QTableWidgetItem(str(new_value) if new_value is not None else "")
# 设置数值验证器 (QTableWidgetItem 本身没有 setValidator 方法)
# 如果需要验证,可以考虑使用代理 (Delegate)
self.reclass_table.setItem(row_count, 0, from_item)
self.reclass_table.setItem(row_count, 1, to_item)
self.reclass_table.setItem(row_count, 2, new_value_item)
def del_row(self):
"""删除选中的行"""
selected_rows = self.reclass_table.selectedIndexes()
if not selected_rows:
return
# 从最后一个开始删除,以避免索引变化问题
rows_to_delete = sorted(list(set([index.row() for index in selected_rows])), reverse=True)
for row in rows_to_delete:
self.reclass_table.removeRow(row)
# 确保至少有一行
if self.reclass_table.rowCount() == 0:
self.add_row()
self.paramsChanged.emit() # 参数变化
def on_item_changed(self, item):
"""当表格单元格内容变化时触发"""
# 在这里可以添加一些简单的验证,例如检查是否为数字
try:
text = item.text()
if text:
if item.column() in [0, 1]: # "从" 和 "到" 列
float(text) # 尝试转换为浮点数
elif item.column() == 2: # "新值" 列
int(text) # 尝试转换为整数
except ValueError:
# 如果转换失败,可以给用户提示或清空单元格
# item.setText("") # 或者其他处理方式
pass # 暂时不做处理依赖后期的validate_inputs
self.paramsChanged.emit() # 参数变化
def get_remap_table(self):
"""从表格获取重分类映射表"""
remap_table = []
for row in range(self.reclass_table.rowCount()):
try:
from_item = self.reclass_table.item(row, 0)
to_item = self.reclass_table.item(row, 1)
new_value_item = self.reclass_table.item(row, 2)
# 检查是否为空或无效
if not from_item or not from_item.text() or \
not to_item or not to_item.text() or \
not new_value_item or not new_value_item.text():
continue # 跳过空行
from_value_str = from_item.text()
to_value_str = to_item.text()
new_value_str = new_value_item.text()
# 处理特殊值,例如 "inf" 表示无穷大
from_value = float(from_value_str) if from_value_str.lower() not in ('inf', '-inf') else (float('-inf') if from_value_str.lower() == '-inf' else float('inf'))
to_value = float(to_value_str) if to_value_str.lower() not in ('inf', '-inf') else (float('-inf') if to_value_str.lower() == '-inf' else float('inf'))
new_value = int(new_value_str)
# 验证范围的有效性
if from_value > to_value:
print(f"警告: 无效范围 {from_value} > {to_value},跳过此行")
continue # 跳过无效范围的行
remap_table.append([from_value, to_value, new_value])
except (ValueError, TypeError) as e:
print(f"警告: 读取重分类表格时遇到无效数值或类型,跳过此行. 错误: {e}")
continue
return remap_table
def set_table_data(self, data):
"""设置重分类表格数据"""
self.reclass_table.clearContents()
self.reclass_table.setRowCount(0)
if not data:
self.add_row() # 添加一行默认行
return
for row_data in data:
if len(row_data) == 3:
from_value, to_value, new_value = row_data
self.add_row(from_value, to_value, new_value)
class VectorParamsWidget(QWidget):
"""矢量化参数组件"""
paramsChanged = pyqtSignal() # 参数变化信号
def __init__(self, parent=None):
super(VectorParamsWidget, self).__init__(parent)
self.init_ui()
def init_ui(self):
"""初始化UI"""
layout = QHBoxLayout(self)
# 矢量化参数
# 简化多边形边界
simplify_layout = QHBoxLayout()
simplify_layout.addWidget(QLabel("简化多边形边界:"))
self.simplify_check = QCheckBox()
self.simplify_check.setChecked(False) # 默认不选中
# PyQt6 信号槽连接
self.simplify_check.stateChanged.connect(self.on_simplify_changed)
simplify_layout.addWidget(self.simplify_check)
simplify_layout.addStretch()
layout.addLayout(simplify_layout)
# 最小面积阈值
min_area_layout = QHBoxLayout()
min_area_layout.addWidget(QLabel("最小面积:"))
self.min_area_spin = QDoubleSpinBox()
self.min_area_spin.setRange(0.1, 1000000)
self.min_area_spin.setValue(100)
self.min_area_spin.setSingleStep(10)
# PyQt6 信号槽连接
self.min_area_spin.valueChanged.connect(lambda: self.paramsChanged.emit())
min_area_layout.addWidget(self.min_area_spin)
# 面积单位
self.area_unit_combo = QComboBox()
self.area_unit_combo.addItems(["平方米", "公顷", "平方公里"])
self.area_unit_map = {
"平方米": "SQUARE_METERS",
"公顷": "HECTARES",
"平方公里": "SQUARE_KILOMETERS"
}
# PyQt6 信号槽连接
self.area_unit_combo.currentIndexChanged.connect(lambda: self.paramsChanged.emit())
min_area_layout.addWidget(self.area_unit_combo)
layout.addLayout(min_area_layout)
def on_simplify_changed(self, state):
"""当简化选项状态变化时触发"""
# PyQt6 中 Qt.Checked 仍然可用
is_checked = state == Qt.CheckState.Checked
self.paramsChanged.emit()
def get_params(self):
"""获取矢量化参数"""
is_simplify = self.simplify_check.isChecked()
return {
"simplify": is_simplify,
"min_area": self.min_area_spin.value(),
"area_unit": self.area_unit_map[self.area_unit_combo.currentText()]
}
def set_params(self, params):
"""设置矢量化参数"""
if "simplify" in params:
simplify_value = params["simplify"]
self.simplify_check.setChecked(simplify_value)
if "min_area" in params:
self.min_area_spin.setValue(params["min_area"])
if "area_unit" in params:
for text, value in self.area_unit_map.items():
if value == params["area_unit"]:
index = self.area_unit_combo.findText(text)
if index >= 0:
self.area_unit_combo.setCurrentIndex(index)
break

519
tools/ui/tabs/raster_tab.py Normal file
View File

@@ -0,0 +1,519 @@
# -*- coding: utf-8 -*-
"""
栅格处理界面: 提供栅格重分类、栅格转矢量和小面积图斑消除的界面操作
"""
import os
import json
import arcpy
import traceback
import multiprocessing
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
QPushButton, QFileDialog, QGridLayout, QCheckBox,
QGroupBox, QMessageBox, QSpinBox)
from PyQt6.QtCore import pyqtSignal
from tools.core.utils import common_utils
from tools.ui.runners.script_runner import ScriptRunner
from .config_editor_dialog import ConfigEditorDialogVisual
from .raster_processing_common import VectorParamsWidget
from ..components.file_list_group import FileListGroup
class RasterTab(QWidget):
value_changed = pyqtSignal(str)
"""栅格处理窗口部件类"""
def __init__(self, parent=None):
super(RasterTab, self).__init__(parent)
self.main_window = parent
self.init_ui()
self.setWindowTitle("栅格处理")
def init_ui(self):
"""初始化用户界面"""
main_layout = QVBoxLayout(self)
# 批量处理区域
self.batch_mode_group = QGroupBox("重分类并消除小面积图斑")
batch_layout = QGridLayout()
# 配置文件选择 (公用)
batch_layout.addWidget(QLabel("配置文件:"), 0, 0)
self.config_file_edit = QLineEdit()
self.config_file_edit.textChanged.connect(self.on_config_file_changed)
batch_layout.addWidget(self.config_file_edit, 0, 1)
self.browse_config_btn = QPushButton("浏览...")
self.browse_config_btn.clicked.connect(self.browse_config_file)
batch_layout.addWidget(self.browse_config_btn, 0, 2)
# 添加编辑配置文件的按钮 <--- Add this button
self.edit_config_btn = QPushButton("编辑配置内容...")
self.edit_config_btn.setEnabled(False)
self.edit_config_btn.clicked.connect(self.open_config_editor)
batch_layout.addWidget(self.edit_config_btn, 0, 3)
# 输入文件夹
batch_layout.addWidget(QLabel("栅格文件夹:"), 1, 0)
self.input_folder_edit = QLineEdit()
batch_layout.addWidget(self.input_folder_edit, 1, 1, 1, 2)
self.browse_input_folder_btn = QPushButton("浏览...")
self.browse_input_folder_btn.clicked.connect(self.browse_input_folder)
batch_layout.addWidget(self.browse_input_folder_btn, 1, 3)
# 批量输出文件夹
batch_layout.addWidget(QLabel("输出文件夹:"), 2, 0)
self.batch_output_folder_edit = QLineEdit()
batch_layout.addWidget(self.batch_output_folder_edit, 2, 1, 1, 2)
self.browse_batch_output_btn = QPushButton("浏览...")
self.browse_batch_output_btn.clicked.connect(self.browse_batch_output_folder)
batch_layout.addWidget(self.browse_batch_output_btn, 2, 3)
# 选择裁剪面checkbox
self.clip_checkbox = QCheckBox("启用裁剪")
self.clip_checkbox.setChecked(False)
self.clip_checkbox.stateChanged.connect(self.on_clip_checkbox_changed)
batch_layout.addWidget(self.clip_checkbox, 3, 0)
self.clip_features_edit = QLineEdit()
self.clip_features_edit.setVisible(False)
batch_layout.addWidget(self.clip_features_edit, 3, 1, 1, 2)
self.clip_features_btn = QPushButton("选择裁剪面")
self.clip_features_btn.clicked.connect(self.browse_clip_features)
self.clip_features_btn.setVisible(False)
batch_layout.addWidget(self.clip_features_btn, 3, 3)
self.batch_mode_group.setLayout(batch_layout)
# 文件列表显示组
self.file_list_group = FileListGroup(self, "选择要处理的栅格")
self.file_list_group.load_files.connect(self.on_load_raster)
# --- 处理参数设置 ---
param_group = QGroupBox("处理参数")
param_layout = QVBoxLayout(param_group)
# 矢量化参数组件 (包含简化和最小面积阈值)
self.vector_params = VectorParamsWidget()
param_layout.addWidget(self.vector_params)
# 多进程设置
export_layout = QHBoxLayout()
mutiprocess_layout = QHBoxLayout()
mutiprocess_layout.addWidget(QLabel("使用多进程导出:"))
self.mutiprocess_check = QCheckBox()
self.mutiprocess_check.setChecked(True)
mutiprocess_layout.addWidget(self.mutiprocess_check)
mutiprocess_layout.addStretch()
export_layout.addLayout(mutiprocess_layout)
# 进程数量设置
process_count_layout = QHBoxLayout()
process_count_layout.addWidget(QLabel("进程数量:"))
self.process_count = QSpinBox()
self.process_count.setRange(1, multiprocessing.cpu_count())
self.process_count.setValue(max(1, multiprocessing.cpu_count() - 1)) # 默认使用CPU核心数-1
self.process_count.setFixedWidth(180)
process_count_layout.addWidget(self.process_count)
export_layout.addLayout(process_count_layout)
param_layout.addLayout(export_layout)
# 操作按钮
btn_layout = QHBoxLayout()
self.process_btn = QPushButton("开始处理")
self.process_btn.clicked.connect(self.on_start_processing)
self.cancel_btn = QPushButton("取消")
# self.cancel_btn.clicked.connect(self.close)
btn_layout.addWidget(self.process_btn)
btn_layout.addWidget(self.cancel_btn)
# 添加所有组件到主布局
main_layout.addWidget(self.batch_mode_group)
main_layout.addWidget(self.file_list_group)
main_layout.addWidget(param_group)
main_layout.addLayout(btn_layout)
self.setLayout(main_layout)
def browse_input_folder(self):
"""浏览选择输入文件夹"""
folder_path = QFileDialog.getExistingDirectory(self, "选择栅格文件夹")
if folder_path:
self.input_folder_edit.setText(folder_path)
self.on_load_raster()
def browse_batch_output_folder(self):
"""浏览选择批量处理输出文件夹"""
folder_path = QFileDialog.getExistingDirectory(self, "选择批量处理输出文件夹")
if folder_path:
if folder_path.endswith(".gdb"):
QMessageBox.warning(self, "错误", "请选择一个文件夹,而不是一个数据库文件")
return
self.batch_output_folder_edit.setText(folder_path)
def browse_config_file(self):
"""浏览选择配置文件"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择配置文件", "",
"JSON 文件 (*.json);;所有文件 (*)"
)
if file_path:
self.config_file_edit.setText(file_path)
self.validate_config_file(file_path)
self.edit_config_btn.setEnabled(True)
def browse_clip_features(self):
"""浏览选择裁剪面 (支持 shapefile)"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择裁剪要素 (Shapefile)", "",
"Shapefile 文件 (*.shp);;所有文件 (*)"
)
if not file_path:
return
if file_path.lower().endswith(".shp"):
self.clip_features_edit.setText(file_path)
self.log_message(f"已选择 Shapefile: {file_path}")
else:
QMessageBox.warning(self, "选择错误", "不支持的文件类型。请选择 .shp 文件。")
self.clip_features_edit.clear()
def validate_config_file(self, config_file_path):
"""对文件进行快速检查,以查看它是否类似于预期的配置结构。"""
if not os.path.exists(config_file_path):
return False
try:
with open(config_file_path, 'r', encoding='utf-8') as f:
config = json.load(f)
if "export_config" in config and isinstance(config["export_config"], dict):
if config["export_config"]:
first_item_value = next(iter(config["export_config"].values()))
if isinstance(first_item_value, dict) and all(k in first_item_value for k in ["项目名称", "标准等级"]):
self.log_message(f"配置文件 '{os.path.basename(config_file_path)}' 加载成功。")
return True
else:
self.log_message(f"警告: 配置文件 '{os.path.basename(config_file_path)}' 内容结构可能不匹配预期。")
return False
else:
self.log_message(f"警告: 配置文件 '{os.path.basename(config_file_path)}' 中的 'export_config' 部分为空。")
return False
else:
self.log_message(f"警告: 配置文件 '{os.path.basename(config_file_path)}' 缺少 'export_config' 键或格式不正确。")
return False
except json.JSONDecodeError:
self.log_message(f"警告: 配置文件 '{os.path.basename(config_file_path)}' 不是有效的 JSON 格式。")
return False
except Exception as e:
self.log_message(f"警告: 检查配置文件 '{os.path.basename(config_file_path)}' 时出错: {str(e)}")
return False
def open_config_editor(self):
"""打开配置文件编辑器对话框"""
config_path = self.config_file_edit.text()
if not config_path:
reply = QMessageBox.question(self, "编辑配置", "没有选择配置文件,是否创建一个新配置文件?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
if reply == QMessageBox.StandardButton.No:
return
config_path = ""
# 使用配置编辑对话框
dialog = ConfigEditorDialogVisual(config_path, self)
if dialog.exec() == ConfigEditorDialogVisual.DialogCode.Accepted:
new_path = dialog.get_new_config_path()
if new_path and new_path != self.config_file_edit.text():
self.config_file_edit.setText(new_path)
self.validate_config_file(new_path)
def on_config_file_changed(self):
text = self.config_file_edit.text()
self.value_changed.emit(text)
def on_clip_checkbox_changed(self):
clip_checkbox_state = self.clip_checkbox.isChecked()
self.clip_features_edit.setVisible(clip_checkbox_state)
self.clip_features_btn.setVisible(clip_checkbox_state)
def on_load_raster(self):
"""加载图层列表"""
try:
data_source_paths = self.input_folder_edit.text()
self.file_list_group.load_file_btn.setEnabled(False)
# 清空列表
self.file_list_group.file_list.clear()
if not data_source_paths or not arcpy.Exists(data_source_paths):
self.log_message(f"输入路径不存在或不是有效的ArcGIS工作空间/文件夹: {data_source_paths}")
return
arcpy.env.workspace = data_source_paths
rasters = arcpy.ListRasters()
if rasters:
for raster in rasters:
self.file_list_group.file_list.addItem(raster)
self.log_message(f"已加载 {self.file_list_group.file_list.count()} 个图层")
else:
self.log_message(f"文件夹 '{os.path.basename(data_source_paths)}' 中没有找到栅格图层。")
except Exception as e:
self.log_message(f"加载图层列表失败: {str(e)}")
traceback.print_exc()
finally:
self.file_list_group.load_file_btn.setEnabled(True)
def validate_inputs(self):
"""验证输入参数"""
# 验证配置文件路径
config_path = self.config_file_edit.text()
if not config_path or not os.path.exists(config_path):
QMessageBox.warning(self, "输入错误", "请选择有效的配置文件")
return False
try:
with open(config_path, 'r', encoding='utf-8') as f:
config_data = json.load(f)
except Exception as e:
QMessageBox.warning(self, "输入错误", f"无法加载或解析配置文件: {e}")
return False
if "export_config" not in config_data or not isinstance(config_data["export_config"], dict):
QMessageBox.warning(self, "输入错误", "配置文件的内容无效或缺少 'export_config' 部分。")
return False
standards_dict = config_data["export_config"]
if not standards_dict:
QMessageBox.warning(self, "输入错误", "配置文件中的 'export_config' 部分为空,没有定义任何处理参数。")
return False
# 验证'标准等级'用于配置重分类表
error_keys_remap = []
for key, value in standards_dict.items():
try:
levels_data = value.get("标准等级", {})
if not isinstance(levels_data, dict):
error_keys_remap.append(f"{key} ('标准等级' 格式不正确)")
continue
if not common_utils.create_remap_table(levels_data):
error_keys_remap.append(f"{key} ('标准等级' 内容无效)")
except Exception:
error_keys_remap.append(f"{key} (验证出错)")
if error_keys_remap:
QMessageBox.warning(self, "输入错误", "配置文件中部分参数的 '标准等级' 数据无效:\n" + "\n".join(error_keys_remap))
return False
# 验证输入、输出文件夹
if not self.input_folder_edit.text():
QMessageBox.warning(self, "输入错误", "请选择输入栅格文件夹")
return False
if not self.batch_output_folder_edit.text() or self.batch_output_folder_edit.text().endswith('.gdb'):
QMessageBox.warning(self, "输入错误", "请选择批量处理输出文件夹")
return False
if self.clip_checkbox.isChecked():
clip_path = self.clip_features_edit.text()
if not clip_path or not arcpy.Exists(clip_path):
QMessageBox.warning(self, "输入错误", "已启用裁剪,但裁剪要素路径无效或不存在")
return False
# 3. 验证裁剪要素是否可用
if self.clip_checkbox.isChecked():
clip_path = self.clip_features_edit.text()
if not clip_path:
QMessageBox.warning(self, "输入错误", "已启用裁剪,但裁剪要素路径为空。")
return False
# Use arcpy.Exists and Describe to validate the path regardless of whether it's SHP or GDB/FeatureClass
if not arcpy.Exists(clip_path):
QMessageBox.warning(self, "输入错误", f"已启用裁剪,但裁剪要素路径不存在:\n{clip_path}")
return False
# Optional: Check if the path points to a Feature Class (SHP or inside GDB)
try:
desc = arcpy.Describe(clip_path)
if desc.dataType != 'ShapeFile':
QMessageBox.warning(self, "输入错误", f"已启用裁剪,但选择的路径不是一个要素类:\n{clip_path}")
return False
except Exception as e:
QMessageBox.warning(self, "输入错误", f"无法描述裁剪要素路径,请检查是否为有效要素类:\n{clip_path}\n错误: {e}")
return False
# 验证是否选择文件
selected_files = [file.text() for file in self.file_list_group.file_list.selectedItems()]
if not selected_files:
QMessageBox.warning(self, "输入错误", "请在栅格列表中勾选要处理的文件。")
return False
return True
def on_start_processing(self):
"""开始处理栅格数据"""
if not self.validate_inputs():
return
try:
if self.main_window and hasattr(self.main_window, 'save_settings'):
self.main_window.update_settings()
self.main_window.save_settings()
settings_file = self.main_window.settings_file
# 创建的导出线程
self.script_runner = ScriptRunner()
# 连接ScriptRunner的信号
self.script_runner.task_started.connect(self.on_script_started)
self.script_runner.task_finished.connect(self.on_script_finished)
self.script_runner.task_error.connect(self.on_script_error)
self.script_runner.task_log.connect(self.on_script_log)
self.script_runner.manager_log.connect(self.log_message)
# 获取公共参数 (从 VectorParamsWidget 获取)
selected_files = [file.text() for file in self.file_list_group.file_list.selectedItems()]
if self.mutiprocess_check.isChecked():
self.script_runner.set_max_concurrent(self.process_count.value())
else:
self.script_runner.set_max_concurrent(1)
# 调用批量处理脚本
for raster_file in selected_files:
params = {
"settings_path": settings_file,
"input_raster": raster_file,
}
task_id = self.script_runner.run_process_raster(params)
# if task_id:
# self.log_message(f"[GUI] 任务 {task_id} 处理 {raster_file} 已添加到列队")
except Exception as e:
error_msg = f"处理过程中出错: {str(e)}"
QMessageBox.critical(self, "处理错误", error_msg)
self.log_message(error_msg)
def get_raster_settings(self):
"""保存当前配置到JSON文件"""
# 从 VectorParamsWidget 获取参数
vector_params = self.vector_params.get_params()
config = {
"input_folder": self.input_folder_edit.text(),
"batch_output_folder": self.batch_output_folder_edit.text(),
"config_file_path": self.config_file_edit.text(),
"simplify": vector_params["simplify"], # 从 VectorParamsWidget 获取
"min_area": vector_params["min_area"], # 从 VectorParamsWidget 获取
"area_unit": vector_params["area_unit"], # 从 VectorParamsWidget 获取
"clip_features": self.clip_features_edit.text() if self.clip_checkbox.isChecked() else "",
"clip_enabled": self.clip_checkbox.isChecked()
}
return config
def load_settings(self, settings):
"""从字典加载配置"""
try:
# 批处理参数
self.input_folder_edit.setText(settings.get("input_folder", ""))
self.batch_output_folder_edit.setText(settings.get("batch_output_folder", ""))
# 配置文件路径和加载
config_file_path = settings.get("config_file_path", "")
self.config_file_edit.setText(config_file_path)
self.edit_config_btn.setEnabled(bool(config_file_path) and os.path.exists(config_file_path))
if config_file_path:
self.validate_config_file(config_file_path)
clip_enabled = settings.get("clip_enabled", False)
self.clip_checkbox.setChecked(clip_enabled)
self.clip_features_edit.setText(settings.get("clip_features", ""))
self.on_clip_checkbox_changed()
# 矢量化参数 (使用 VectorParamsWidget 的 set_params 方法)
vector_params_config = {
"simplify": settings.get("simplify", False), # 注意默认值需要与 VectorParamsWidget 匹配
"min_area": settings.get("min_area", 1000),
"area_unit": settings.get("area_unit", "平方米")
}
self.vector_params.set_params(vector_params_config)
if self.input_folder_edit.text():
self.on_load_raster()
return True
except Exception as e:
QMessageBox.critical(self, "加载配置错误", f"加载配置时出错: {str(e)}")
return False
def set_buttons_enabled(self, enabled):
"""设置按钮是否可用"""
self.process_btn.setEnabled(enabled)
def on_script_started(self, task_id, task_description):
"""脚本开始执行"""
self.set_buttons_enabled(False)
self.log_message(f"{task_id}: 正在运行 - {task_description}")
def on_script_finished(self, task_id:str, success:bool, message:str):
"""脚本执行完成"""
try:
# 将shp文件导入到GDB数据库
if self.script_runner.get_running_tasks() == []:
output_path = self.batch_output_folder_edit.text()
shape_files = []
for file in os.listdir(output_path):
if file.endswith("eliminate.shp"):
shape_files.append(os.path.join(output_path, file))
if shape_files:
try:
path_result = arcpy.management.CreateFileGDB(output_path, "过程数据(消除小图斑).gdb", "CURRENT")
# 批量导入数据
arcpy.conversion.FeatureClassToGeodatabase(shape_files, path_result)
arcpy.management.Delete(shape_files)
QMessageBox.information(self, "成功", f"数据已保存到{path_result}")
self.set_buttons_enabled(True)
except Exception as e:
self.log_message(f"批量导入数据出错: {str(e)}")
except Exception as e:
self.set_buttons_enabled(True)
self.log_message(f"批量导入数据出错: {str(e)}")
def on_script_error(self, error_msg):
"""脚本执行出错"""
self.set_buttons_enabled(True)
self.log_message(f"错误: {error_msg}")
QMessageBox.critical(self, "错误", error_msg)
def on_script_log(self, task_id, message):
"""脚本输出日志"""
self.log_message(f"{task_id}: {message}")
def log_message(self, message):
"""日志输出"""
if self.main_window and hasattr(self.main_window, 'log_signal'):
self.main_window.log_signal.emit(message)
else:
print(message)
if __name__ == "__main__":
from PyQt6.QtWidgets import QApplication
import sys
app = QApplication(sys.argv)
window = RasterTab()
window.show()
sys.exit(app.exec())

View File

@@ -0,0 +1,353 @@
# -*- coding: utf-8 -*-
"""
栅格处理界面: 提供栅格重分类、栅格转矢量和小面积图斑消除的界面操作
"""
import os
import traceback
import arcpy
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
QPushButton, QFileDialog, QGridLayout,
QGroupBox, QMessageBox)
from tools.ui.components.file_list_group import FileListGroup
from tools.ui.runners.script_runner import ScriptRunner
from tools.ui.tabs.config_editor_dialog import ConfigEditorDialogVisual
class SoilPropStatsTab(QWidget):
"""栅格处理窗口部件类"""
def __init__(self, parent=None):
super(SoilPropStatsTab, self).__init__(parent)
self.main_window = parent
# 创建的导出线程
self.script_runner = ScriptRunner()
self.connect_signals()
self.init_ui()
self.setWindowTitle("土壤属性统计")
def connect_signals(self):
# 连接ScriptRunner的信号
self.script_runner.task_started.connect(self.on_script_started)
self.script_runner.task_finished.connect(self.on_script_finished)
self.script_runner.task_error.connect(self.on_script_error)
self.script_runner.task_log.connect(self.on_script_log)
self.script_runner.manager_log.connect(self.log_message)
def init_ui(self):
"""初始化用户界面"""
main_layout = QVBoxLayout(self)
# 批量处理区域
self.batch_mode_group = QGroupBox("批量处理设置")
batch_layout = QGridLayout()
# 配置文件选择 (公用)
batch_layout.addWidget(QLabel("配置文件:"), 0, 0)
self.config_file_edit = QLineEdit()
self.config_file_edit.setEnabled(False)
batch_layout.addWidget(self.config_file_edit, 0, 1,1,2)
# self.browse_config_btn = QPushButton("浏览...")
# self.browse_config_btn.clicked.connect(self.browse_config_file)
# self.browse_config_btn.setEnabled(False)
# batch_layout.addWidget(self.browse_config_btn, 0, 2)
# # 添加编辑配置文件的按钮 <--- Add this button
# self.edit_config_btn = QPushButton("编辑配置内容...")
# self.edit_config_btn.setEnabled(False)
# self.edit_config_btn.clicked.connect(self.open_config_editor)
# batch_layout.addWidget(self.edit_config_btn, 0, 3)
# 输入行政区名称
batch_layout.addWidget(QLabel("行政区名称:"), 1, 0)
self.input_xzqmc_edit = QLineEdit()
batch_layout.addWidget(self.input_xzqmc_edit, 1, 1)
# 工作空间路径 - 使用水平布局容器
label_container = QWidget()
label_layout = QHBoxLayout(label_container)
label_layout.setContentsMargins(0, 0, 0, 0) # 去掉边距
label = QLabel("")
label.setToolTip("""<font color='blue'>各属性样点<br>地类图斑<br>土壤类型图<br>母岩母质</font>""")
text_label = QLabel("数据源路径:")
label_layout.addWidget(label)
label_layout.addWidget(text_label)
batch_layout.addWidget(label_container, 2, 0) # 将整个容器放在第2行第0列
self.data_source_path_edit = QLineEdit()
batch_layout.addWidget(self.data_source_path_edit, 2, 1)
self.browse_input_workspace_btn = QPushButton("选择GDB")
self.browse_input_workspace_btn.clicked.connect(self.browse_input_workspace)
batch_layout.addWidget(self.browse_input_workspace_btn, 2, 2)
# 选择三普属性栅格文件夹
batch_layout.addWidget(QLabel("三普属性栅格:"), 3, 0)
self.input_sanpu_prop_tif_edit = QLineEdit()
batch_layout.addWidget(self.input_sanpu_prop_tif_edit, 3, 1)
self.browse_input_sanpu_prop_tif_btn = QPushButton("选择文件夹")
self.browse_input_sanpu_prop_tif_btn.clicked.connect(self.browse_input_sanpu_prop_tif)
batch_layout.addWidget(self.browse_input_sanpu_prop_tif_btn, 3, 2)
# 选择三普土壤属性重分类后的面要素
batch_layout.addWidget(QLabel("属性重分类面:"), 4, 0)
self.input_reclassed_feature_folder_edit = QLineEdit()
batch_layout.addWidget(self.input_reclassed_feature_folder_edit, 4, 1)
self.browse_input_reclassed_feature_folder_btn = QPushButton("选择文件夹")
self.browse_input_reclassed_feature_folder_btn.clicked.connect(self.browse_input_reclassed_feature_folder)
batch_layout.addWidget(self.browse_input_reclassed_feature_folder_btn, 4, 2)
# 批量输出文件夹
batch_layout.addWidget(QLabel("输出文件夹:"), 5, 0)
self.batch_output_folder_edit = QLineEdit()
batch_layout.addWidget(self.batch_output_folder_edit, 5, 1)
self.browse_batch_output_btn = QPushButton("选择文件夹")
self.browse_batch_output_btn.clicked.connect(self.browse_batch_output_folder)
batch_layout.addWidget(self.browse_batch_output_btn, 5, 2)
self.batch_mode_group.setLayout(batch_layout)
# 文件列表布局
self.file_list_group = FileListGroup(self, "选择要导出的三普属性样点:")
self.file_list_group.load_files.connect(self.on_load_polygon)
# 操作按钮
btn_layout = QHBoxLayout()
# 导出酸化统计表
self.generate_sh_stat_btn = QPushButton("生成土壤属性统计表")
self.generate_sh_stat_btn.clicked.connect(self.on_generate_area_stat)
self.cancel_btn = QPushButton("取消")
# self.cancel_btn.clicked.connect(self.close)
btn_layout.addWidget(self.generate_sh_stat_btn)
btn_layout.addWidget(self.cancel_btn)
# 添加所有组件到主布局
main_layout.addWidget(self.batch_mode_group)
main_layout.addWidget(self.file_list_group)
main_layout.addLayout(btn_layout)
self.setLayout(main_layout)
def browse_config_file(self):
"""浏览选择配置文件"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择配置文件", "",
"JSON 文件 (*.json);;所有文件 (*)"
)
if file_path:
self.config_file_edit.setText(file_path)
# self.validate_config_file(file_path)
# self.edit_config_btn.setEnabled(True)
def on_load_polygon(self):
"""加载图层列表"""
try:
data_source_paths = self.data_source_path_edit.text()
# 清空列表
self.file_list_group.file_list.clear()
# 添加图层
if arcpy.Exists(data_source_paths):
arcpy.env.workspace = data_source_paths
for polygon in arcpy.ListFeatureClasses(feature_type="Point"):
self.file_list_group.file_list.addItem(polygon)
self.log_message(f"已加载 {self.file_list_group.file_list.count()} 个图层")
except Exception as e:
self.log_message(f"加载图层列表失败: {str(e)}")
traceback.print_exc()
def browse_input_workspace(self):
"""浏览选择输入GDB"""
folder_path = QFileDialog.getExistingDirectory(self, "选择GDB数据库")
if folder_path:
self.data_source_path_edit.setText(folder_path)
def browse_input_sanpu_prop_tif(self):
"""浏览选择输入GDB"""
folder_path = QFileDialog.getExistingDirectory(self, "选择文件夹啊")
if folder_path:
self.input_sanpu_prop_tif_edit.setText(folder_path)
def browse_input_reclassed_feature_folder(self):
"""浏览选择输入GDB"""
folder_path = QFileDialog.getExistingDirectory(self, "选择GDB数据库")
if folder_path:
self.input_reclassed_feature_folder_edit.setText(folder_path)
def browse_batch_output_folder(self):
"""浏览选择表格输出文件夹"""
folder_path = QFileDialog.getExistingDirectory(self, "选择表格输出文件夹")
if folder_path:
self.batch_output_folder_edit.setText(folder_path)
def validate_inputs(self):
"""验证输入参数"""
# 验证行政区名称
if not self.input_xzqmc_edit.text():
QMessageBox.warning(self, "输入错误", "请输入行政区名称")
return False
# 验证工作空间路径
if not self.data_source_path_edit.text() or not arcpy.Exists(self.data_source_path_edit.text()):
QMessageBox.warning(self, "输入错误", "请选择有效的工作空间")
return False
if not self.batch_output_folder_edit.text() or not arcpy.Exists(self.batch_output_folder_edit.text()):
QMessageBox.warning(self, "输入错误", "请选择批量处理输出文件夹")
return False
if not self.input_sanpu_prop_tif_edit.text() or not arcpy.Exists(self.input_sanpu_prop_tif_edit.text()):
QMessageBox.warning(self, "输入错误", "请选择有效的三普属性栅格文件夹")
return False
# 验证选择的要素类是否有对应的属性栅格
missing_items = []
for item in self.file_list_group.file_list.selectedItems():
if not arcpy.Exists(os.path.join(self.input_sanpu_prop_tif_edit.text(), f"{item.text()}.tif")):
missing_items.append(item.text())
if missing_items:
QMessageBox.warning(self, "输入错误", f"选择的样点中无相应的栅格文件: {missing_items}")
return False
# 验证数据源中是否存在 地类图斑、土壤类型图斑、母岩母质图斑 等要素
if not arcpy.Exists(os.path.join(self.data_source_path_edit.text(), "地类图斑")):
QMessageBox.warning(self, "输入错误", "工作空间中不存在 地类图斑.shp")
return False
if not arcpy.Exists(os.path.join(self.data_source_path_edit.text(), "土壤类型图")):
QMessageBox.warning(self, "输入错误", "工作空间中不存在土壤类型图.shp")
return False
# if not arcpy.Exists(os.path.join(self.data_source_path_edit.text(), "母岩母质图斑")):
# QMessageBox.warning(self, "输入错误", "工作空间中不存在母岩母质图斑.shp")
# return False
# 验证文件列表中是否已选中项目
if not self.file_list_group.file_list.selectedItems():
QMessageBox.warning(self, "输入错误", "请至少选择一个属性")
return False
return True
def open_config_editor(self):
"""打开配置文件编辑器对话框"""
config_path = self.config_file_edit.text()
if not config_path:
reply = QMessageBox.question(self, "编辑配置", "没有选择配置文件,是否创建一个新配置文件?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
if reply == QMessageBox.StandardButton.No:
return
config_path = ""
# 使用配置编辑对话框
dialog = ConfigEditorDialogVisual(config_path, self)
if dialog.exec() == ConfigEditorDialogVisual.DialogCode.Accepted:
new_path = dialog.get_new_config_path()
if new_path and new_path != self.config_file_edit.text():
self.config_file_edit.setText(new_path)
# self.validate_config_file(new_path)
# 执行生成酸化统计表
def on_generate_area_stat(self):
"""执行生成面积统计表"""
if not self.validate_inputs():
return
try:
if self.main_window and hasattr(self.main_window, 'save_settings'):
self.main_window.update_settings()
self.main_window.save_settings()
settings_file = self.main_window.settings_file
params = {
"settings_path": settings_file,
}
task_id = self.script_runner.run_soil_prop_stat(params)
if task_id:
self.log_message(f"[GUI] 属性表格生成任务 {task_id} 已添加到列队")
except Exception as e:
error_msg = f"处理过程中出错: {str(e)}"
QMessageBox.critical(self, "处理错误", error_msg)
self.log_message(error_msg)
def get_soil_prop_stat_settings(self):
"""保存当前配置到JSON文件"""
config = {
"xzqmc": self.input_xzqmc_edit.text(),
"config_file": self.config_file_edit.text(),
"data_source_path": self.data_source_path_edit.text(),
"sanpu_prop_tif_folder": self.input_sanpu_prop_tif_edit.text(),
"reclassed_feature_folder": self.input_reclassed_feature_folder_edit.text(),
"output_folder": self.batch_output_folder_edit.text(),
"sample_list": [item.text() for item in self.file_list_group.file_list.selectedItems()]
}
return config
def load_settings(self, settings):
"""从字典加载配置"""
try:
# 批处理参数
self.data_source_path_edit.setText(settings.get("data_source_path", ""))
self.input_sanpu_prop_tif_edit.setText(settings.get("sanpu_prop_tif_folder", ""))
self.batch_output_folder_edit.setText(settings.get("output_folder", ""))
self.input_reclassed_feature_folder_edit.setText(settings.get("reclassed_feature_folder", ""))
return True
except Exception as e:
QMessageBox.critical(self, "加载配置错误", f"加载配置时出错: {str(e)}")
return False
def set_buttons_enabled(self, enabled):
"""设置按钮是否可用"""
self.generate_sh_stat_btn.setEnabled(enabled)
def update_config_file(self, config_file_path):
"""更新配置文件路径"""
self.config_file_edit.setText(config_file_path)
def on_script_started(self, task_id, task_description):
"""脚本开始执行"""
self.set_buttons_enabled(False)
self.log_message(f"{task_id}: 正在运行 - {task_description}")
def on_script_finished(self, task_id:str, success:bool, message:str):
"""脚本执行完成"""
self.set_buttons_enabled(True)
self.log_message("脚本执行完成")
def on_script_error(self,task_id, error_msg):
"""脚本执行出错"""
self.set_buttons_enabled(True)
self.log_message(f"错误:{task_id}-{error_msg}")
# QMessageBox.critical(self, "错误", error_msg)
def on_script_log(self, task_id, message):
"""脚本输出日志"""
self.log_message(f"{task_id}: {message}")
def log_message(self, message):
"""日志输出"""
if self.main_window and hasattr(self.main_window, 'log_signal'):
self.main_window.log_signal.emit(message)
else:
print(message)
if __name__ == "__main__":
from PyQt6.QtWidgets import QApplication
import sys
app = QApplication(sys.argv)
window = SoilPropStatsTab()
window.show()
sys.exit(app.exec())

569
tools/ui/tabs/test_tab.py Normal file
View File

@@ -0,0 +1,569 @@
import json
import os
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
QLabel, QLineEdit, QFileDialog, QMessageBox,
QComboBox, QSpinBox, QGroupBox, QFormLayout,
QTableWidget, QHeaderView, QSizePolicy, QTableWidgetItem,
QCheckBox)
import arcpy
from ..runners.script_runner import ScriptRunner
class TestTab(QWidget):
"""导出成果图标签页"""
def __init__(self, parent=None):
super().__init__(parent)
self.main_window = parent
self.script_runner = ScriptRunner(self)
self.connect_signals()
self.init_ui()
self.load_settings()
def connect_signals(self):
"""连接脚本运行器信号"""
# 连接ScriptRunner的信号
self.script_runner.started.connect(self.on_script_started)
self.script_runner.finished.connect(self.on_script_finished)
self.script_runner.error.connect(self.on_script_error)
self.script_runner.log.connect(self.log_message)
def init_ui(self):
layout = QVBoxLayout(self)
# 配置文件设置组
config_group = QGroupBox("配置设置")
config_layout = QVBoxLayout(config_group)
# 配置文件选择
config_file_layout = QHBoxLayout()
config_file_layout.addWidget(QLabel("配置文件:"))
self.config_file_path = QLineEdit()
config_file_layout.addWidget(self.config_file_path)
config_file_btn = QPushButton("浏览...")
config_file_btn.clicked.connect(self.browse_config_file)
config_file_layout.addWidget(config_file_btn)
config_layout.addLayout(config_file_layout)
# 区县名字设置
county_name_layout = QHBoxLayout()
county_name_layout.addWidget(QLabel("区县名称:"))
self.county_name = QLineEdit()
self.county_name.setPlaceholderText("请输入区县名称")
county_name_layout.addWidget(self.county_name)
config_layout.addLayout(county_name_layout)
# 图层名称选择
layer_select_layout = QHBoxLayout()
layer_select_layout.addWidget(QLabel("选择图层:"))
self.layer_name_combo = QComboBox()
self.layer_name_combo.setMinimumWidth(200)
self.layer_name_combo.currentIndexChanged.connect(self.on_layer_name_changed)
layer_select_layout.addWidget(self.layer_name_combo)
layer_select_layout.addStretch()
config_layout.addLayout(layer_select_layout)
# 配置文件设置显示区域
export_layout = QVBoxLayout()
export_layout.addWidget(QLabel("配置文件设置:"))
export_layout.minimumHeightForWidth(200)
self.export_config_display = QTableWidget(0, 2)
self.export_config_display.setHorizontalHeaderLabels(["元素名称", "元素内容"])
header = self.export_config_display.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Interactive)
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
header.setDefaultSectionSize(150)
self.export_config_display.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
export_layout.addWidget(self.export_config_display)
config_layout.addLayout(export_layout)
layout.addWidget(config_group)
# 输入输出设置组
io_group = QGroupBox("输入输出设置")
io_layout = QVBoxLayout()
# 地图文档选择
doc_layout = QHBoxLayout()
doc_layout.addWidget(QLabel("模板文件:"))
self.template_aprx_file = QLineEdit()
doc_layout.addWidget(self.template_aprx_file)
browse_doc_btn = QPushButton("浏览...")
browse_doc_btn.clicked.connect(self.browse_doc)
doc_layout.addWidget(browse_doc_btn)
io_layout.addLayout(doc_layout)
# 数据源选择
data_layout = QHBoxLayout()
data_layout.addWidget(QLabel("数据源:"))
self.data_source_path = QLineEdit()
data_layout.addWidget(self.data_source_path)
browse_data_btn = QPushButton("浏览...")
browse_data_btn.clicked.connect(self.browse_data_source)
data_layout.addWidget(browse_data_btn)
io_layout.addLayout(data_layout)
# 符号系统文件夹选择
symbol_layout = QHBoxLayout()
symbol_layout.addWidget(QLabel("符号系统:"))
self.symbol_path = QLineEdit()
symbol_layout.addWidget(self.symbol_path)
browse_symbol_btn = QPushButton("浏览...")
browse_symbol_btn.clicked.connect(self.browse_symbol_file)
symbol_layout.addWidget(browse_symbol_btn)
io_layout.addLayout(symbol_layout)
# 输出路径选择
output_layout = QHBoxLayout()
output_layout.addWidget(QLabel("输出路径:"))
self.output_path = QLineEdit()
output_layout.addWidget(self.output_path)
browse_output_btn = QPushButton("浏览...")
browse_output_btn.clicked.connect(self.browse_output)
output_layout.addWidget(browse_output_btn)
io_layout.addLayout(output_layout)
io_group.setLayout(io_layout)
layout.addWidget(io_group)
# 导出设置组
export_group = QGroupBox("导出设置")
export_layout = QFormLayout()
# 格式选择
self.format_combo = QComboBox()
self.format_combo.addItems(["PDF", "PNG", "JPG", "TIFF", "EPS", "SVG", "AI"])
export_layout.addRow("导出格式:", self.format_combo)
# 分辨率设置
self.resolution_spinbox = QSpinBox()
self.resolution_spinbox.setRange(72, 1200)
self.resolution_spinbox.setSingleStep(12)
self.resolution_spinbox.setValue(300)
self.resolution_spinbox.setSuffix(" DPI")
export_layout.addRow("分辨率:", self.resolution_spinbox)
# 强制重新生成选项
self.force_regenerate = QCheckBox("强制重新生成工程文件")
self.force_regenerate.setChecked(False)
self.force_regenerate.setToolTip("勾选后将忽略已存在的工程文件,重新生成")
export_layout.addRow("处理选项:", self.force_regenerate)
export_group.setLayout(export_layout)
layout.addWidget(export_group)
# 操作按钮
button_layout = QHBoxLayout()
self.export_layout_btn = QPushButton("仅导出布局")
self.export_layout_btn.clicked.connect(self.on_export_layout)
self.export_btn = QPushButton("导出")
self.export_btn.clicked.connect(self.on_export)
self.batch_export_btn = QPushButton("批量导出")
self.batch_export_btn.clicked.connect(self.on_batch_export)
self.export_existing_btn = QPushButton("导出已有工程")
self.export_existing_btn.setToolTip("仅导出输出文件夹中已存在的工程文件")
self.export_existing_btn.clicked.connect(self.on_export_existing)
self.test_btn = QPushButton("测试功能")
self.test_btn.setToolTip("测试功能")
self.test_btn.clicked.connect(self.on_test_functions)
button_layout.addStretch()
button_layout.addWidget(self.export_layout_btn)
button_layout.addWidget(self.export_btn)
button_layout.addWidget(self.batch_export_btn)
button_layout.addWidget(self.export_existing_btn)
button_layout.addWidget(self.test_btn)
layout.addLayout(button_layout)
layout.addStretch()
def browse_config_file(self):
"""浏览配置文件"""
initial_path = os.getcwd()
file_path, _ = QFileDialog.getOpenFileName(self, "选择配置文件", initial_path, "JSON Files (*.json);;All Files (*.*)")
if file_path:
self.config_file_path.setText(file_path)
self.load_export_config(file_path)
def browse_doc(self):
"""浏览地图文档"""
file_path, _ = QFileDialog.getOpenFileName(
self,
"选择地图文档",
"",
"ArcGIS Pro Project (*.aprx);;All Files (*.*)"
)
if file_path:
self.template_aprx_file.setText(file_path)
def browse_data_source(self):
"""浏览数据源"""
dir_path = QFileDialog.getExistingDirectory(
self,
"选择数据源目录"
)
if dir_path:
self.data_source_path.setText(dir_path)
def browse_symbol_file(self):
"""浏览符号系统文件夹"""
dir_path = QFileDialog.getExistingDirectory(
self,
"选择符号系统文件夹"
)
if dir_path:
self.symbol_path.setText(dir_path)
def browse_output(self):
"""选择输出路径"""
dir_path = QFileDialog.getExistingDirectory(
self,
"选择输出路径"
)
if dir_path:
self.output_path.setText(dir_path)
def load_export_config(self, file_path):
"""加载导出配置文件"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
config = json.load(f)
export_config = config.get('export_config', {})
if not export_config:
QMessageBox.warning(self, "警告", "配置文件中未找到导出设置")
return
# 保存配置到实例变量
self._config = config
self._export_config = export_config
# 清空并添加图层名称到下拉框
self.layer_name_combo.clear()
self.layer_name_combo.addItems(export_config.keys())
# 如果有图层,自动选择第一个
if self.layer_name_combo.count() > 0:
self.layer_name_combo.setCurrentIndex(0)
self.log_message(f"已加载配置文件: {file_path}")
except Exception as e:
QMessageBox.critical(self, "错误", f"加载配置文件失败: {str(e)}")
def on_layer_name_changed(self, index):
"""图层名称变更时更新配置显示"""
if index < 0 or not hasattr(self, '_export_config'):
return
layer_name = self.layer_name_combo.currentText()
layer_config = self._export_config.get(layer_name, {})
# 清空并重新添加行
self.export_config_display.setRowCount(0)
for i, (key, value) in enumerate(layer_config.items()):
self.export_config_display.insertRow(i)
self.export_config_display.setItem(i, 0, QTableWidgetItem(key))
self.export_config_display.setItem(i, 1, QTableWidgetItem(str(value)))
def on_script_started(self, message):
"""脚本开始执行回调"""
self.set_buttons_enabled(False)
self.log_message(message)
def on_script_finished(self, task_id:str, success:bool, message:str):
"""脚本执行完成回调"""
self.set_buttons_enabled(True)
self.log_message("脚本执行完成")
def on_script_error(self, error_msg):
"""脚本执行错误回调"""
self.set_buttons_enabled(True)
self.log_message(f"错误: {error_msg}")
QMessageBox.critical(self, "错误", error_msg)
def set_buttons_enabled(self, enabled):
"""设置按钮启用状态"""
self.export_btn.setEnabled(enabled)
self.batch_export_btn.setEnabled(enabled)
self.export_layout_btn.setEnabled(enabled)
self.export_existing_btn.setEnabled(enabled)
self.test_btn.setEnabled(enabled)
def on_export(self):
"""导出按钮点击事件"""
# 验证输入
if not self.validate_inputs():
return
# 获取参数
params = self.get_export_params()
if not params:
return
# 添加配置文件参数
params['config_file'] = self.config_file_path.text()
# 调用导出地图脚本
self.script_runner.run_export_map(params)
def on_batch_export(self):
"""批量导出按钮点击事件"""
# 验证输入
if not self.validate_inputs(check_layer=False):
return
# 获取参数
params = self.get_export_params(include_layer=False)
if not params:
return
# 检查配置是否包含图层
if not hasattr(self, '_export_config') or not self._export_config:
QMessageBox.warning(self, "警告", "请先加载配置文件")
return
# 添加配置文件路径参数
params['config_file'] = self.config_file_path.text()
params['export_config'] = self._export_config
# 检查可用图层
polygon_list = list(self._export_config.keys())
available_layers = []
for layer_name in polygon_list:
layer_path = os.path.join(params.get("data_source_path"), layer_name)
if arcpy.Exists(layer_path):
available_layers.append(layer_name)
params['polygon_list'] = available_layers
# 确认是否批量导出所有图层
layer_count = len(available_layers)
result = QMessageBox.question(
self,
"确认",
f"数据源可用图层 {layer_count} 个,是否继续?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if result == QMessageBox.No:
return
# 调用批量导出脚本
self.script_runner.run_batch_export_map(params)
def validate_inputs(self, check_layer=True):
"""验证输入参数"""
if check_layer and self.layer_name_combo.currentIndex() < 0:
QMessageBox.warning(self, "警告", "请选择一个图层")
return False
if not self.county_name.text():
QMessageBox.warning(self, "警告", "请输入区县名称")
return False
if not self.template_aprx_file.text() or not os.path.exists(self.template_aprx_file.text()):
QMessageBox.warning(self, "警告", "请选择有效的模板文件")
return False
if not self.data_source_path.text() or not os.path.exists(self.data_source_path.text()):
QMessageBox.warning(self, "警告", "请选择有效的数据源")
return False
if not self.output_path.text():
QMessageBox.warning(self, "警告", "请选择输出路径")
return False
return True
def get_export_params(self, include_layer=True):
"""获取导出参数"""
try:
params = {
'county_name': self.county_name.text(),
'template_aprx_file': self.template_aprx_file.text(),
'data_source_path': self.data_source_path.text(),
'symbol_path': self.symbol_path.text(),
'output_path': self.output_path.text(),
'force_regenerate': self.force_regenerate.isChecked()
}
if include_layer:
params['layer_name'] = self.layer_name_combo.currentText()
return params
except Exception as e:
QMessageBox.critical(self, "错误", f"获取参数失败: {str(e)}")
return None
def load_settings(self):
"""加载设置"""
if not self.main_window:
return
try:
settings = self.main_window.settings
last_paths = settings.get('last_paths', {})
if last_paths.get('config_file'):
self.config_file_path.setText(last_paths.get('config_file', ''))
if os.path.exists(last_paths.get('config_file', '')):
self.load_export_config(last_paths.get('config_file', ''))
self.county_name.setText(last_paths.get('county_name', ''))
self.template_aprx_file.setText(last_paths.get('template_aprx_file', ''))
self.data_source_path.setText(last_paths.get('data_source_path', ''))
self.symbol_path.setText(last_paths.get('symbol_path', ''))
self.output_path.setText(last_paths.get('output_path', ''))
export_settings = settings.get('export_settings', {})
# 设置导出格式
format_index = self.format_combo.findText(export_settings.get('default_format', 'PDF'))
if format_index >= 0:
self.format_combo.setCurrentIndex(format_index)
# 设置分辨率
self.resolution_spinbox.setValue(export_settings.get('resolution', 300))
except Exception as e:
self.log_message(f"加载设置失败: {str(e)}")
def on_export_layout(self):
"""仅导出布局按钮点击事件"""
# 验证输入
if not self.validate_inputs():
return
# 获取参数
export_params = self.get_export_params()
if not export_params:
return
# 检查导出配置和图层名称
layer_name = self.layer_name_combo.currentText()
if not hasattr(self, '_export_config') or layer_name not in self._export_config:
QMessageBox.warning(self, "警告", "请先加载配置文件并选择图层")
return
# 准备工程文件路径
single_export_config = self._export_config.get(layer_name, {})
temp_file_name = single_export_config['项目名称'].split('\n')[1]
file_name = temp_file_name.replace('{区县占位符}', export_params['county_name'])
aprx_path = os.path.join(export_params['output_path'], f"{file_name}.aprx")
# 检查工程文件是否存在
if not os.path.exists(aprx_path):
result = QMessageBox.question(
self,
"确认",
f"工程文件不存在: {aprx_path}\n是否先生成工程文件?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes
)
if result == QMessageBox.Yes:
# 先生成工程文件
# 添加配置文件路径参数
export_params['config_file'] = self.config_file_path.text()
# 执行生成工程文件
self.script_runner.run_export_map(export_params)
return
else:
return
# 准备导出布局参数
layout_params = {
'aprx_path': aprx_path,
'output_path': export_params['output_path'],
'export_format': self.format_combo.currentText(),
'resolution': self.resolution_spinbox.value(),
'output_name': file_name
}
# 调用导出布局脚本
self.script_runner.run_export_layout(layout_params)
def on_export_existing(self):
"""导出已有工程按钮点击事件"""
# 验证输出路径
if not self.output_path.text() or not os.path.exists(self.output_path.text()):
QMessageBox.warning(self, "警告", "请选择有效的输出路径")
return
output_path = self.output_path.text()
# 查找所有aprx文件
aprx_files = []
for file in os.listdir(output_path):
if file.lower().endswith('.aprx'):
aprx_files.append(os.path.join(output_path, file))
if not aprx_files:
QMessageBox.warning(self, "警告", f"在指定目录中未找到aprx文件: {output_path}")
return
# 确认是否导出所有工程文件
result = QMessageBox.question(
self,
"确认",
f"将导出 {len(aprx_files)} 个工程文件的布局,是否继续?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if result == QMessageBox.No:
return
# 准备批量导出布局参数
batch_params = {
'aprx_folder': output_path,
'output_path': os.path.join(output_path, self.format_combo.currentText().lower()),
'export_format': self.format_combo.currentText(),
'resolution': self.resolution_spinbox.value()
}
# 调用批量导出布局脚本
self.script_runner.run_batch_export_layout(batch_params)
def log_message(self, message):
"""日志输出"""
if self.main_window and hasattr(self.main_window, 'log_signal'):
self.main_window.log_signal.emit(message)
else:
print(message)
def on_test_functions(self):
"""测试功能按钮点击事件"""
self.log_message("正在执行测试功能...")
try:
# 调用测试脚本
test_params = {
'message': "测试成功!这是来自脚本的消息。",
'count': 1
}
# 使用script_runner的run_test_script方法
if hasattr(self.script_runner, 'run_test_script'):
self.script_runner.run_test_script(test_params)
self.log_message("测试脚本调用已启动,请查看日志")
else:
# 如果没有run_test_script方法尝试调用test_script.py
script_path = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'core', 'test_script.py'))
if os.path.exists(script_path):
self.log_message(f"找到测试脚本: {script_path}")
args = {
'message': test_params['message'],
'count': test_params['count']
}
self.script_runner.run_script(script_path, args)
else:
QMessageBox.information(self, "测试", "找不到测试脚本: test_script.py")
self.log_message(f"找不到测试脚本: {script_path}")
except Exception as e:
self.log_message(f"测试功能调用失败: {str(e)}")
QMessageBox.critical(self, "错误", f"测试功能调用失败: {str(e)}")

View File

@@ -0,0 +1,496 @@
# -*- coding: utf-8 -*-
"""
栅格处理界面: 提供栅格重分类、栅格转矢量和小面积图斑消除的界面操作
"""
import os
import json
import arcpy
import traceback
import multiprocessing
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
QPushButton, QFileDialog, QGridLayout, QCheckBox,
QGroupBox, QMessageBox, QSpinBox)
from tools.core.utils import common_utils
from tools.ui.runners.script_runner import ScriptRunner
from .config_editor_dialog import ConfigEditorDialogVisual
from ..components.file_list_group import FileListGroup
class XlsxToJpgTab(QWidget):
"""栅格处理窗口部件类"""
def __init__(self, parent=None):
super(XlsxToJpgTab, self).__init__(parent)
self.main_window = parent
self.init_ui()
self.setWindowTitle("面积统计")
def init_ui(self):
"""初始化用户界面"""
main_layout = QVBoxLayout(self)
# 批量处理区域
self.batch_mode_group = QGroupBox("批量处理设置")
batch_layout = QGridLayout()
# 配置文件选择 (公用)
batch_layout.addWidget(QLabel("配置文件:"), 0, 0)
self.config_file_edit = QLineEdit()
batch_layout.addWidget(self.config_file_edit, 0, 1)
self.browse_config_btn = QPushButton("浏览...")
self.browse_config_btn.clicked.connect(self.browse_config_file)
batch_layout.addWidget(self.browse_config_btn, 0, 2)
# 添加编辑配置文件的按钮 <--- Add this button
self.edit_config_btn = QPushButton("编辑配置内容...")
self.edit_config_btn.setEnabled(False)
self.edit_config_btn.clicked.connect(self.open_config_editor)
batch_layout.addWidget(self.edit_config_btn, 0, 3)
# 输入文件夹
batch_layout.addWidget(QLabel("重分类面要素:"), 1, 0)
self.input_folder_edit = QLineEdit()
batch_layout.addWidget(self.input_folder_edit, 1, 1, 1, 2)
self.browse_input_folder_btn = QPushButton("浏览...")
self.browse_input_folder_btn.clicked.connect(self.browse_input_folder)
batch_layout.addWidget(self.browse_input_folder_btn, 1, 3)
# 批量输出文件夹
batch_layout.addWidget(QLabel("输出文件夹:"), 2, 0)
self.batch_output_folder_edit = QLineEdit()
batch_layout.addWidget(self.batch_output_folder_edit, 2, 1, 1, 2)
self.browse_batch_output_btn = QPushButton("浏览...")
self.browse_batch_output_btn.clicked.connect(self.browse_batch_output_folder)
batch_layout.addWidget(self.browse_batch_output_btn, 2, 3)
# 选择乡镇界线
batch_layout.addWidget(QLabel("乡镇界线:"), 3, 0)
self.xzq_features_edit = QLineEdit()
batch_layout.addWidget(self.xzq_features_edit, 3, 1, 1, 2)
self.xzq_features_btn = QPushButton("浏览")
self.xzq_features_btn.clicked.connect(self.browse_xzq_features)
batch_layout.addWidget(self.xzq_features_btn, 3, 3)
# 选择地类图斑
batch_layout.addWidget(QLabel("地类图斑:"), 4, 0)
self.dltb_features_edit = QLineEdit()
batch_layout.addWidget(self.dltb_features_edit, 4, 1, 1, 2)
self.dltb_features_btn = QPushButton("浏览")
self.dltb_features_btn.clicked.connect(self.browse_dltb_features)
batch_layout.addWidget(self.dltb_features_btn, 4, 3)
# 输入行政区名称
batch_layout.addWidget(QLabel("行政区名称:"), 5, 0)
self.xzqmc_edit = QLineEdit()
batch_layout.addWidget(self.xzqmc_edit, 5, 1, 1, 2)
# 单选框
batch_layout.addWidget(QLabel("是否同时平差行政区和地类:"), 6, 0)
self.is_adjust_xzq_and_landuse_checkbox = QCheckBox()
batch_layout.addWidget(self.is_adjust_xzq_and_landuse_checkbox, 6, 1)
self.batch_mode_group.setLayout(batch_layout)
# 文件列表显示组
self.file_list_group = FileListGroup(self, "选择要处理的栅格")
self.file_list_group.load_files.connect(self.on_load_raster)
# --- 处理参数设置 ---
param_group = QGroupBox("处理参数")
param_layout = QVBoxLayout(param_group)
# 多进程设置
export_layout = QHBoxLayout()
mutiprocess_layout = QHBoxLayout()
mutiprocess_layout.addWidget(QLabel("使用多进程导出:"))
self.mutiprocess_check = QCheckBox()
self.mutiprocess_check.setChecked(True)
mutiprocess_layout.addWidget(self.mutiprocess_check)
mutiprocess_layout.addStretch()
export_layout.addLayout(mutiprocess_layout)
# 进程数量设置
process_count_layout = QHBoxLayout()
process_count_layout.addWidget(QLabel("进程数量:"))
self.process_count = QSpinBox()
self.process_count.setRange(1, multiprocessing.cpu_count())
self.process_count.setValue(max(1, multiprocessing.cpu_count() - 1)) # 默认使用CPU核心数-1
self.process_count.setFixedWidth(180)
process_count_layout.addWidget(self.process_count)
export_layout.addLayout(process_count_layout)
param_layout.addLayout(export_layout)
# 操作按钮
btn_layout = QHBoxLayout()
self.process_btn = QPushButton("开始处理")
self.process_btn.clicked.connect(self.on_start_processing)
self.cancel_btn = QPushButton("取消")
# self.cancel_btn.clicked.connect(self.close)
btn_layout.addWidget(self.process_btn)
btn_layout.addWidget(self.cancel_btn)
# 添加所有组件到主布局
main_layout.addWidget(self.batch_mode_group)
main_layout.addWidget(self.file_list_group)
main_layout.addWidget(param_group)
main_layout.addLayout(btn_layout)
self.setLayout(main_layout)
def browse_input_folder(self):
"""浏览选择输入文件夹"""
folder_path = QFileDialog.getExistingDirectory(self, "选择栅格文件夹")
if folder_path:
self.input_folder_edit.setText(folder_path)
self.on_load_raster()
def browse_batch_output_folder(self):
"""浏览选择批量处理输出文件夹"""
folder_path = QFileDialog.getExistingDirectory(self, "选择批量处理输出文件夹")
if folder_path:
self.batch_output_folder_edit.setText(folder_path)
def browse_config_file(self):
"""浏览选择配置文件"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择配置文件", "",
"JSON 文件 (*.json);;所有文件 (*)"
)
if file_path:
self.config_file_edit.setText(file_path)
self.validate_config_file(file_path)
self.edit_config_btn.setEnabled(True)
def browse_xzq_features(self):
"""浏览选择裁剪面 (支持 shapefile)"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择裁剪要素 (Shapefile)", "",
"Shapefile 文件 (*.shp);;所有文件 (*)"
)
if not file_path:
return
if file_path.lower().endswith(".shp"):
self.xzq_features_edit.setText(file_path)
self.log_message(f"已选择 Shapefile: {file_path}")
else:
QMessageBox.warning(self, "选择错误", "不支持的文件类型。请选择 .shp 文件。")
self.xzq_features_edit.clear()
def browse_dltb_features(self):
"""浏览选择裁剪面 (支持 shapefile)"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择裁剪要素 (Shapefile)", "",
"Shapefile 文件 (*.shp);;所有文件 (*)"
)
if not file_path:
return
if file_path.lower().endswith(".shp"):
self.dltb_features_edit.setText(file_path)
self.log_message(f"已选择 Shapefile: {file_path}")
else:
QMessageBox.warning(self, "选择错误", "不支持的文件类型。请选择 .shp 文件。")
self.dltb_features_edit.clear()
def validate_config_file(self, config_file_path):
"""对文件进行快速检查,以查看它是否类似于预期的配置结构。"""
if not os.path.exists(config_file_path):
return False
try:
with open(config_file_path, 'r', encoding='utf-8') as f:
config = json.load(f)
if "export_config" in config and isinstance(config["export_config"], dict):
if config["export_config"]:
first_item_value = next(iter(config["export_config"].values()))
if isinstance(first_item_value, dict) and all(k in first_item_value for k in ["项目名称", "标准等级"]):
self.log_message(f"配置文件 '{os.path.basename(config_file_path)}' 加载成功。")
return True
else:
self.log_message(f"警告: 配置文件 '{os.path.basename(config_file_path)}' 内容结构可能不匹配预期。")
return False
else:
self.log_message(f"警告: 配置文件 '{os.path.basename(config_file_path)}' 中的 'export_config' 部分为空。")
return False
else:
self.log_message(f"警告: 配置文件 '{os.path.basename(config_file_path)}' 缺少 'export_config' 键或格式不正确。")
return False
except json.JSONDecodeError:
self.log_message(f"警告: 配置文件 '{os.path.basename(config_file_path)}' 不是有效的 JSON 格式。")
return False
except Exception as e:
self.log_message(f"警告: 检查配置文件 '{os.path.basename(config_file_path)}' 时出错: {str(e)}")
return False
def open_config_editor(self):
"""打开配置文件编辑器对话框"""
config_path = self.config_file_edit.text()
if not config_path:
reply = QMessageBox.question(self, "编辑配置", "没有选择配置文件,是否创建一个新配置文件?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
if reply == QMessageBox.StandardButton.No:
return
config_path = ""
# 使用配置编辑对话框
dialog = ConfigEditorDialogVisual(config_path, self)
if dialog.exec() == ConfigEditorDialogVisual.DialogCode.Accepted:
new_path = dialog.get_new_config_path()
if new_path and new_path != self.config_file_edit.text():
self.config_file_edit.setText(new_path)
self.validate_config_file(new_path)
def on_load_raster(self):
"""加载图层列表"""
try:
data_source_paths = self.input_folder_edit.text()
self.file_list_group.load_file_btn.setEnabled(False)
# 清空列表
self.file_list_group.file_list.clear()
if not data_source_paths or not arcpy.Exists(data_source_paths):
self.log_message(f"输入路径不存在或不是有效的ArcGIS工作空间/文件夹: {data_source_paths}")
return
arcpy.env.workspace = data_source_paths
# 获取所有SHP 文件
shp_files = arcpy.ListFeatureClasses("*.shp")
if shp_files:
for raster in shp_files:
self.file_list_group.file_list.addItem(raster)
self.log_message(f"已加载 {self.file_list_group.file_list.count()} 个图层")
else:
self.log_message(f"文件夹 '{os.path.basename(data_source_paths)}' 中没有找到栅格图层。")
except Exception as e:
self.log_message(f"加载图层列表失败: {str(e)}")
traceback.print_exc()
finally:
self.file_list_group.load_file_btn.setEnabled(True)
def validate_inputs(self):
"""验证输入参数"""
# 验证配置文件路径
config_path = self.config_file_edit.text()
if not config_path or not os.path.exists(config_path):
QMessageBox.warning(self, "输入错误", "请选择有效的配置文件")
return False
try:
with open(config_path, 'r', encoding='utf-8') as f:
config_data = json.load(f)
except Exception as e:
QMessageBox.warning(self, "输入错误", f"无法加载或解析配置文件: {e}")
return False
if "export_config" not in config_data or not isinstance(config_data["export_config"], dict):
QMessageBox.warning(self, "输入错误", "配置文件的内容无效或缺少 'export_config' 部分。")
return False
standards_dict = config_data["export_config"]
if not standards_dict:
QMessageBox.warning(self, "输入错误", "配置文件中的 'export_config' 部分为空,没有定义任何处理参数。")
return False
# 验证'标准等级'用于配置重分类表
error_keys_remap = []
for key, value in standards_dict.items():
try:
levels_data = value.get("标准等级", {})
if not isinstance(levels_data, dict):
error_keys_remap.append(f"{key} ('标准等级' 格式不正确)")
continue
if not common_utils.create_remap_table(levels_data):
error_keys_remap.append(f"{key} ('标准等级' 内容无效)")
except Exception:
error_keys_remap.append(f"{key} (验证出错)")
if error_keys_remap:
QMessageBox.warning(self, "输入错误", "配置文件中部分参数的 '标准等级' 数据无效:\n" + "\n".join(error_keys_remap))
return False
# 验证输入、输出文件夹
if not self.input_folder_edit.text():
QMessageBox.warning(self, "输入错误", "请选择输入栅格文件夹")
return False
if not self.batch_output_folder_edit.text():
QMessageBox.warning(self, "输入错误", "请选择批量处理输出文件夹")
return False
clip_path = self.xzq_features_edit.text()
if not clip_path or not arcpy.Exists(clip_path):
QMessageBox.warning(self, "输入错误", "已启用裁剪,但裁剪要素路径无效或不存在")
return False
# 3. 验证乡镇界限
clip_path = self.xzq_features_edit.text()
if not clip_path:
QMessageBox.warning(self, "输入错误", "已启用裁剪,但裁剪要素路径为空。")
return False
# Use arcpy.Exists and Describe to validate the path regardless of whether it's SHP or GDB/FeatureClass
if not arcpy.Exists(clip_path):
QMessageBox.warning(self, "输入错误", f"已启用裁剪,但裁剪要素路径不存在:\n{clip_path}")
return False
# Optional: Check if the path points to a Feature Class (SHP or inside GDB)
try:
desc = arcpy.Describe(clip_path)
if desc.dataType != 'FeatureClass' and desc.dataType != 'ShapeFile':
QMessageBox.warning(self, "输入错误", f"已启用裁剪,但选择的路径不是一个要素类:\n{clip_path}")
return False
except Exception as e:
QMessageBox.warning(self, "输入错误", f"无法描述裁剪要素路径,请检查是否为有效要素类:\n{clip_path}\n错误: {e}")
return False
# 验证是否选择文件
selected_files = [file.text() for file in self.file_list_group.file_list.selectedItems()]
if not selected_files:
QMessageBox.warning(self, "输入错误", "请在栅格列表中勾选要处理的文件。")
return False
return True
def on_start_processing(self):
"""开始处理栅格数据"""
if not self.validate_inputs():
return
try:
if self.main_window and hasattr(self.main_window, 'save_settings'):
self.main_window.update_settings()
self.main_window.save_settings()
settings_file = self.main_window.settings_file
# 创建的导出线程
self.script_runner = ScriptRunner()
# 连接ScriptRunner的信号
self.script_runner.task_started.connect(self.on_script_started)
self.script_runner.task_finished.connect(self.on_script_finished)
self.script_runner.task_error.connect(self.on_script_error)
self.script_runner.task_log.connect(self.on_script_log)
self.script_runner.manager_log.connect(self.log_message)
# 获取公共参数 (从 VectorParamsWidget 获取)
selected_files = [file.text() for file in self.file_list_group.file_list.selectedItems()]
if self.mutiprocess_check.isChecked():
self.script_runner.set_max_concurrent(self.process_count.value())
else:
self.script_runner.set_max_concurrent(1)
# 调用批量处理脚本
for raster_file in selected_files:
params = {
"settings_path": settings_file,
"reclassed_polygon": raster_file,
}
# 统计面积
task_id = self.script_runner.run_area_stat(params)
# if task_id:
# self.log_message(f"[GUI] 任务 {task_id} 处理 {raster_file} 已添加到列队")
except Exception as e:
error_msg = f"处理过程中出错: {str(e)}"
QMessageBox.critical(self, "处理错误", error_msg)
self.log_message(error_msg)
def get_area_stat_settings(self):
"""保存当前配置到JSON文件"""
config = {
"input_folder": self.input_folder_edit.text(),
"batch_output_folder": self.batch_output_folder_edit.text(),
"config_file_path": self.config_file_edit.text(),
"xzq_features": self.xzq_features_edit.text(),
"dltb_features": self.dltb_features_edit.text(),
}
return config
def load_settings(self, settings):
"""从字典加载配置"""
try:
# 批处理参数
self.input_folder_edit.setText(settings.get("input_folder", ""))
self.batch_output_folder_edit.setText(settings.get("batch_output_folder", ""))
# 配置文件路径和加载
config_file_path = settings.get("config_file_path", "")
self.config_file_edit.setText(config_file_path)
self.edit_config_btn.setEnabled(bool(config_file_path) and os.path.exists(config_file_path))
if config_file_path:
self.validate_config_file(config_file_path)
self.xzq_features_edit.setText(settings.get("xzq_features", ""))
self.dltb_features_edit.setText(settings.get("dltb_features", ""))
if self.input_folder_edit.text():
self.on_load_raster()
return True
except Exception as e:
QMessageBox.critical(self, "加载配置错误", f"加载配置时出错: {str(e)}")
return False
def set_buttons_enabled(self, enabled):
"""设置按钮是否可用"""
self.process_btn.setEnabled(enabled)
def on_script_started(self, task_id, task_description):
"""脚本开始执行"""
self.set_buttons_enabled(False)
self.log_message(f"{task_id}: 正在运行 - {task_description}")
def on_script_finished(self, task_id:str, success:bool, message:str):
"""脚本执行完成"""
self.set_buttons_enabled(True)
self.log_message("脚本执行完成")
def on_script_error(self,task_id, error_msg):
"""脚本执行出错"""
self.set_buttons_enabled(True)
self.log_message(f"错误:{task_id}-{error_msg}")
# QMessageBox.critical(self, "错误", error_msg)
def on_script_log(self, task_id, message):
"""脚本输出日志"""
self.log_message(f"{task_id}: {message}")
def log_message(self, message):
"""日志输出"""
if self.main_window and hasattr(self.main_window, 'log_signal'):
self.main_window.log_signal.emit(message)
else:
print(message)
if __name__ == "__main__":
from PyQt6.QtWidgets import QApplication
import sys
app = QApplication(sys.argv)
window = XlsxToJpgTab()
window.show()
sys.exit(app.exec())