初始化

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

1
tools/ui/__init__.py Normal file
View File

@@ -0,0 +1 @@
# UI界面模块

View File

@@ -0,0 +1 @@
# UI界面模块

View File

@@ -0,0 +1,67 @@
'''
pyqt6 文件列表分组
'''
from PyQt6.QtWidgets import QApplication, QVBoxLayout, QHBoxLayout, QPushButton, QListWidget, QGroupBox
from PyQt6.QtCore import pyqtSignal
class FileListGroup(QGroupBox):
load_files = pyqtSignal()
def __init__(self, parent=None, title="文件列表"):
super().__init__(title, parent)
self.init_ui()
def init_ui(self):
layout = QVBoxLayout(self)
# 文件列表
self.file_list = QListWidget()
self.file_list.setSelectionMode(QListWidget.SelectionMode.MultiSelection)
layout.addWidget(self.file_list)
# 文件列表操作按钮
file_list_btn_layout = QHBoxLayout()
# 加载文件按钮
self.load_file_btn = QPushButton("加载文件")
self.load_file_btn.clicked.connect(self.on_load_files)
# 全选/全不选按钮
select_all_btn = QPushButton("全选")
select_all_btn.clicked.connect(self.on_select_all_files)
select_none_btn = QPushButton("全不选")
select_none_btn.clicked.connect(self.on_select_none_files)
# 添加按钮到布局
file_list_btn_layout.addWidget(self.load_file_btn)
file_list_btn_layout.addWidget(select_all_btn)
file_list_btn_layout.addWidget(select_none_btn)
layout.addLayout(file_list_btn_layout)
def on_load_files(self):
# 通过信号槽机制,添加文件列表
self.load_files.emit()
def on_select_all_files(self):
# 全选文件
for i in range(self.file_list.count()):
item = self.file_list.item(i)
if item:
item.setSelected(True)
def on_select_none_files(self):
# 全不选文件
for i in range(self.file_list.count()):
item = self.file_list.item(i)
if item:
item.setSelected(False)
if __name__ == '__main__':
app = QApplication([])
app.setStyle("Fusion")
window = FileListGroup()
window.show()
app.exec()

View File

@@ -0,0 +1,64 @@
from PyQt6.QtWidgets import (QGroupBox, QVBoxLayout, QHBoxLayout,
QLabel, QLineEdit, QPushButton, QFileDialog)
class InputGroup(QGroupBox):
"""输入设置组件"""
def __init__(self, title="输入设置", parent=None):
super().__init__(title, parent)
self.init_ui()
def init_ui(self):
layout = QVBoxLayout(self)
# 模板文件选择
template_layout = QHBoxLayout()
template_layout.addWidget(QLabel("配置文件:"))
self.template_path = QLineEdit()
template_layout.addWidget(self.template_path)
template_btn = QPushButton("浏览...")
template_btn.clicked.connect(self.browse_template)
template_layout.addWidget(template_btn)
layout.addLayout(template_layout)
# 数据源选择
data_layout = QHBoxLayout()
data_layout.addWidget(QLabel("数据源:"))
self.data_source = QLineEdit()
data_layout.addWidget(self.data_source)
data_btn = QPushButton("浏览...")
data_btn.clicked.connect(self.browse_data_source)
data_layout.addWidget(data_btn)
layout.addLayout(data_layout)
def browse_template(self):
"""浏览选择模板文件"""
file_path, _ = QFileDialog.getOpenFileName(
self,
"选择模板文件",
"",
"ArcGIS Pro Project (*.aprx);;All Files (*.*)"
)
if file_path:
self.template_path.setText(file_path)
def browse_data_source(self):
"""浏览选择数据源"""
dir_path = QFileDialog.getExistingDirectory(
self,
"选择数据源目录"
)
if dir_path:
self.data_source.setText(dir_path)
def get_template_path(self):
"""获取模板文件路径"""
return self.template_path.text()
def get_data_source(self):
"""获取数据源路径"""
return self.data_source.text()
def clear(self):
"""清除输入"""
self.template_path.clear()
self.data_source.clear()

View File

View File

@@ -0,0 +1,60 @@
{
"export_map_settings": {
"config_file": "D:/ProgramData/ArcGis_Py/tools/config_json/广西_华南_地块_config.json",
"county_name": "武鸣区",
"template_aprx_file": "E:/@三普属性图出图/广西武鸣区/武鸣区模板/广西武鸣区中微量元素(地块).aprx",
"output_path": "E:/@三普属性图出图/广西武鸣区/成果图/20260411/中微量元素",
"data_source_path": "E:/@三普属性图出图/广西武鸣区/过程数据/三普重分类/按地块出图数据.gdb",
"symbol_path": "E:/@三普属性图出图/@通用数据/符号系统/广西_华南配色",
"pic_path": "E:/@三普属性图出图/广西武鸣区/过程数据/三普重分类/新建文件夹"
},
"export_image_settings": {
"input_aprx_path": "E:\\@三普属性图出图\\广西武鸣区\\成果图\\20260411\\中微量元素\\武鸣区_工作空间",
"output_image_path": "E:\\@三普属性图出图\\广西武鸣区\\成果图\\20260411\\中微量元素",
"default_format": "JPG",
"resolution": 300,
"force_regenerate": false,
"use_multiprocessing": true,
"process_count": 7
},
"raster_settings": {
"input_folder": "E:/@三普属性图出图/广西武鸣区/@基础数据/三普栅格/投影后",
"batch_output_folder": "E:/@三普属性图出图/广西武鸣区/过程数据/三普重分类/新建文件夹",
"config_file_path": "D:/ProgramData/ArcGis_Py/tools/config_json/广西_华南_地块_config.json",
"simplify": false,
"min_area": 10000.0,
"area_unit": "SQUARE_METERS",
"clip_features": "",
"clip_enabled": false
},
"area_stat_settings": {
"input_folder": "E:/@三普属性图出图/广西银海区/过程数据/面积统计用栅格面",
"batch_output_folder": "E:/@三普属性图出图/测试",
"config_file_path": "D:/ProgramData/ArcGis_Py/tools/config_json/广西_华南_地块_config.json",
"xzq_features": "E:\\@三普属性图出图\\广西银海区\\银海区土壤属性制表\\土壤属性制表数据.gdb\\行政区划",
"dltb_features": "E:\\@三普属性图出图\\广西银海区\\银海区土壤属性制表\\土壤属性制表数据.gdb\\地类图斑",
"xzqmc": "西畴县",
"is_by_xzq": false
},
"acid_stat_settings": {
"workspace_path": "E:/@三普属性图出图/广西银海区/酸化专题图/酸化专题数据库.gdb",
"batch_output_folder": "E:/@三普属性图出图/广西银海区/酸化专题图",
"xzq_features": "行政区划",
"dltb_features": "地类图斑",
"ph_samples": "三普PH",
"acid_ph_features": "二普至三普PH",
"assign_raster": "E:/@三普属性图出图/广西北海市/@基础数据/二普栅格/投影后/PH.tif",
"acid_raster": "E:/@三普属性图出图/广西北海市/北海市土壤属性制表/历史变化栅格/新建文件夹/二普-三普_PH.tif",
"soil_type_features": "土壤类型图",
"xzqmc": ""
},
"soil_prop_stat_settings": {
"xzqmc": "",
"config_file": "D:/ProgramData/ArcGis_Py/tools/config_json/广西_华南_地块_config.json",
"data_source_path": "E:/@三普属性图出图/广西武鸣区/武鸣区报告图表/制表数据.gdb",
"sanpu_prop_tif_folder": "E:/@三普属性图出图/广西武鸣区/@基础数据/三普栅格/投影后",
"reclassed_feature_folder": "E:/@三普属性图出图/广西武鸣区/过程数据/三普重分类/面积统计用栅格面",
"output_folder": "E:/@三普属性图出图/广西武鸣区/武鸣区报告图表",
"sample_list": []
}
}

291
tools/ui/main_window.py Normal file
View File

@@ -0,0 +1,291 @@
import traceback
import json
import os
import sys
from PyQt6.QtWidgets import (
QMainWindow,
QWidget,
QVBoxLayout,
QTabWidget,
QTextEdit,
QLabel,
QPushButton,
QHBoxLayout,
)
from PyQt6.QtCore import pyqtSignal, QThreadPool
from PyQt6.QtGui import QTextCursor
from .tabs.export_layout_tab import ExportImageTab
from .tabs.raster_tab import RasterTab
from .tabs.export_map_tab import ExportMapTab
from .tabs.area_stat_tab import XlsxToJpgTab
from .tabs.acid_stat_tab import AcidStatsTab # 酸化专题图表格统计
from .tabs.soil_prop_stat_tab import SoilPropStatsTab
class MainWindow(QMainWindow):
log_signal = pyqtSignal(str) # 定义日志信号
"""主窗口"""
def __init__(self):
super().__init__()
self.setWindowTitle("ArcGIS工具集")
self.resize(600, 800)
self.settings_file = os.path.join(
os.path.dirname(__file__), "config", "settings.json"
)
print(self.settings_file)
self.ensure_settings_file()
self.settings = self.load_settings()
self.init_ui()
# 在创建UI后加载设置
self.load_ui_settings()
def init_ui(self):
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
# 添加选项卡
tabs = QTabWidget()
self.export_map_tab = ExportMapTab(self)
self.export_map_tab.log_signal.connect(self.log_signal) # 连接日志信号
self.export_image_tab = ExportImageTab(self)
self.raster_tab = RasterTab(self)
self.area_stat_tab = XlsxToJpgTab(self)
self.acid_stat_tab = AcidStatsTab(self)
self.soil_prop_stat_tab = SoilPropStatsTab(self)
tabs.addTab(self.raster_tab, "栅格重分类")
tabs.addTab(self.area_stat_tab, "面积统计表制作")
tabs.addTab(self.export_map_tab, "导出工程文件")
tabs.addTab(self.export_image_tab, "导出成果图")
tabs.addTab(self.acid_stat_tab, "酸化专题图表格统计")
tabs.addTab(self.soil_prop_stat_tab, "土壤属性表制作")
main_layout.addWidget(tabs)
# 添加日志区域
self.log_area = QTextEdit()
self.log_area.setReadOnly(True)
self.log_area.setFixedHeight(120)
self.log_signal.connect(self.append_log) # 日志信号连接
main_layout.addWidget(QLabel("日志输出"))
main_layout.addWidget(self.log_area)
# 添加清除日志按钮
log_control_layout = QHBoxLayout()
clear_log_btn = QPushButton("清空日志")
clear_log_btn.clicked.connect(self.clear_log)
log_control_layout.addStretch()
log_control_layout.addWidget(clear_log_btn)
main_layout.addLayout(log_control_layout)
# 连接信号与槽
self.raster_tab.value_changed.connect(self.export_map_tab.update_config_file)
self.raster_tab.value_changed.connect(self.area_stat_tab.update_config_file)
self.raster_tab.value_changed.connect(self.soil_prop_stat_tab.update_config_file)
def ensure_settings_file(self):
"""确保配置文件存在,如不存在则创建"""
if not os.path.exists(self.settings_file):
default_settings = self.get_default_settings()
try:
os.makedirs(os.path.dirname(self.settings_file), exist_ok=True)
with open(self.settings_file, "w", encoding="utf-8") as f:
json.dump(default_settings, f, indent=4, ensure_ascii=False)
except Exception as e:
print(f"创建配置文件失败: {str(e)}")
def load_settings(self):
"""加载设置"""
try:
if os.path.exists(self.settings_file):
with open(self.settings_file, "r", encoding="utf-8") as f:
return json.load(f)
return self.get_default_settings()
except Exception as e:
print(f"加载设置失败: {str(e)}")
return self.get_default_settings()
def save_settings(self):
"""保存设置"""
try:
config_dir = os.path.dirname(self.settings_file)
if not os.path.exists(config_dir):
os.makedirs(config_dir, exist_ok=True)
with open(self.settings_file, "w", encoding="utf-8") as f:
json.dump(self.settings, f, indent=4, ensure_ascii=False)
print("配置已更新")
except Exception as e:
print(f"保存设置失败: {str(e)}")
def get_default_settings(self):
"""获取默认设置"""
return {
"export_map_settings": {
"template_aprx_file": "D:/工作/ArcGisPro/模板/工程模板.aprx",
"output_path": "D:/工作/ArcGisPro/输出文件夹",
"data_source_path": "D:/工作/ArcGisPro/数据源/土壤属性图矢量数据.gdb",
"config_file": "D:/arcpystudy/ArcGisPro/tools/ui/raster_test_config.json",
"county_name": "港南区",
"symbol_path": "D:/工作/ArcGisPro/模板/符号系统",
},
"export_image_settings": {
"input_aprx_path": "D:/工作/ArcGisPro/输出文件夹/港南区_工作空间",
"output_image_path": "D:/工作/ArcGisPro/输出文件夹",
"default_format": "PDF",
"resolution": 300,
'force_regenerate': True,
'use_multiprocessing': True,
'process_count': 3
},
"raster_settings": {
"input_folder": "D:/工作/三普成果编制/港南区土壤属性图成果最终",
"batch_output_folder": "D:/工作/ArcGisPro/输出文件夹/港南区_工作空间",
"config_file_path": "D:/arcpystudy/ArcGisPro/tools/ui/raster_test_config.json",
"simplify": True,
"min_area": 10000,
"area_unit": "平方米",
},
"area_stat_settings": {
"input_folder": "D:/工作/三普成果编制/港南区土壤属性图成果最终",
"batch_output_folder": "D:/工作/ArcGisPro/输出文件夹/港南区_工作空间",
"config_file_path": "D:/arcpystudy/ArcGisPro/tools/ui/raster_test_config.json",
"dltb_polygon": "D:/工作/三普成果编制/港南区土壤属性图成果最终/港南区_地类图_2021-09-01.shp",
"xzq_polygon": "D:/工作/三普成果编制/港南区土壤属性图成果最终/港南区_区县图_2021-09-01.shp",
},
}
def closeEvent(self, event):
"""窗口关闭处理"""
thread_pool = QThreadPool.globalInstance()
if thread_pool is not None:
thread_pool.waitForDone(2000)
self.update_settings()
self.save_settings()
super().closeEvent(event)
def update_settings(self):
"""更新设置"""
try:
# 更新导出工程设置
if hasattr(self.export_map_tab, "get_settings"):
export_map_settings = self.export_map_tab.get_settings()
if export_map_settings:
self.settings["export_map_settings"] = export_map_settings
# 更新导出成果图设置
if hasattr(self.export_image_tab, "get_layout_settings"):
raster_settings = self.export_image_tab.get_layout_settings()
if raster_settings:
self.settings["export_image_settings"]= raster_settings
# 更新栅格处理设置
if hasattr(self.raster_tab, "get_raster_settings"):
raster_settings = self.raster_tab.get_raster_settings()
if raster_settings:
self.settings["raster_settings"]= raster_settings
# 更新面积统计设置
if hasattr(self.area_stat_tab, "get_area_stat_settings"):
area_stat_settings = self.area_stat_tab.get_area_stat_settings()
if area_stat_settings:
self.settings["area_stat_settings"]= area_stat_settings
# 更新酸化统计设置
if hasattr(self.acid_stat_tab, "get_acid_stat_settings"):
acid_stat_settings = self.acid_stat_tab.get_acid_stat_settings()
if acid_stat_settings:
self.settings["acid_stat_settings"]= acid_stat_settings
# 更新土壤属性统计设置
if hasattr(self.soil_prop_stat_tab, "get_soil_prop_stat_settings"):
soil_prop_stat_settings = self.soil_prop_stat_tab.get_soil_prop_stat_settings()
if soil_prop_stat_settings:
self.settings["soil_prop_stat_settings"]= soil_prop_stat_settings
except Exception as e:
print(f"更新设置失败: {str(e)}")
# 回溯
traceback.print_exc()
def load_ui_settings(self):
"""加载UI设置到各个组件"""
try:
# 加载成果图导出标签页设置
if hasattr(self.export_image_tab, "load_settings"):
self.export_image_tab.load_settings()
# 加载栅格处理标签页设置
if hasattr(self.raster_tab, "load_settings"):
self.raster_tab.load_settings(self.settings["raster_settings"])
# 加载面积统计标签页设置
if hasattr(self.area_stat_tab, "load_settings"):
self.area_stat_tab.load_settings(self.settings["area_stat_settings"])
# 加载酸化专题统计标签页设置
if hasattr(self.acid_stat_tab, "load_settings"):
self.acid_stat_tab.load_settings(self.settings["acid_stat_settings"])
# 加载土壤属性统计标签页设置
if hasattr(self.soil_prop_stat_tab, "load_settings"):
self.soil_prop_stat_tab.load_settings(self.settings["soil_prop_stat_settings"])
except Exception as e:
print(f"加载UI设置失败: {str(e)}")
def append_log(self, message):
"""在日志区域追加信息"""
try:
from datetime import datetime
timestamp = datetime.now().strftime("%H:%M:%S")
# 如果传入字符串中已存在时间戳,则不添加时间戳
if "[" in message and "]" in message:
formatted_message = message
else:
formatted_message = f"[{timestamp}] {message}"
# 根据消息类型设置不同颜色
if "错误" in message or "失败" in message or "出错" in message:
formatted_message = f'<span style="color: #e74c3c;">{formatted_message}</span>'
elif "警告" in message:
formatted_message = f'<span style="color: #f39c12;">{formatted_message}</span>'
elif "成功" in message or "完成" in message:
formatted_message = f'<span style="color: #27ae60;">{formatted_message}</span>'
else:
formatted_message = f'<span style="color: #34495e;">{formatted_message}</span>'
self.log_area.append(formatted_message)
self.log_area.verticalScrollBar().setValue(
self.log_area.verticalScrollBar().maximum()
)
# 如果消息太多,清除旧消息
if self.log_area.document().lineCount() > 500:
cursor = self.log_area.textCursor()
cursor.movePosition(QTextCursor.MoveOperation.Start)
cursor.movePosition(
QTextCursor.MoveOperation.Down, QTextCursor.MoveMode.KeepAnchor, 100
)
cursor.removeSelectedText()
except Exception as e:
print(f"追加日志失败: {str(e)}")
def clear_log(self):
"""清空日志区域"""
self.log_area.clear()
# 如果直接运行该模块
if __name__ == "__main__":
from PyQt6.QtWidgets import QApplication
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())

View File

View File

@@ -0,0 +1,475 @@
# -*- coding: utf-8 -*-
"""
脚本运行器 (支持多进程)
用于从PyQt6界面调用独立脚本并管理并行执行
整合了多进程功能。
"""
import os
import sys
import json
import subprocess
import traceback
import uuid
from PyQt6.QtCore import (QObject, pyqtSignal, QProcess, QProcessEnvironment)
from pathlib import Path # 导入Path
import functools # 导入 functools 用于偏函数
class ScriptRunner(QObject):
"""脚本运行器类,用于调用独立脚本处理任务并管理并行执行"""
# 定义信号 (包含任务ID)
# 将原始的 started, finished, error, log 信号修改为包含任务ID
task_started = pyqtSignal(str, str) # 任务ID, 任务描述/消息
task_finished = pyqtSignal(str, bool, str) # 任务ID, 是否成功, 消息 (包含结果或错误)
task_error = pyqtSignal(str, str) # 任务ID, 错误信息
task_log = pyqtSignal(str, str) # 任务ID, 日志信息
manager_log = pyqtSignal(str) # Runner 管理器自身的日志
def __init__(self, parent=None, max_concurrent=3):
super().__init__(parent)
self.max_concurrent = max_concurrent # 控制最大并行进程数
self.running_processes = {} # {task_id: QProcess实例}
self.pending_tasks = ([]) # [(task_id, script_path, args_dict, task_description, working_dir), ...]
self._next_task_id_counter = 0 # 用于生成简单的任务ID (uuid 更推荐)
# 获取项目根目录
self.base_dir = self._get_base_dir()
self.manager_log.emit(f"项目根目录: {self.base_dir}")
self.manager_log.emit(f"最大并行任务数: {self.max_concurrent}")
def _get_base_dir(self):
"""获取项目根目录"""
# 根据你提供的项目结构图调整这里的逻辑
# 假设 script_runner.py 位于 ui/runners 目录下
current_dir = os.path.dirname(os.path.abspath(__file__))
# 向上两级找到项目根目录 (runners <- ui <- project_root)
# 调整为向上两级,因为 ui 和 tools 在同一级
return os.path.dirname(os.path.dirname(current_dir)) # 假设 runners 在 ui 目录下
def _find_script(self, script_name):
"""查找脚本文件"""
# 根据你提供的项目结构图调整可能的脚本路径
base_dir = Path(self.base_dir)
possible_paths = [
base_dir / "tools" / "core" / script_name,
Path(__file__).resolve().parents[2] / "core" / script_name,
Path.cwd() / "tools" / "core" / script_name
]
for path in possible_paths:
path = os.path.normpath(path)
if os.path.exists(path):
self.manager_log.emit(f"找到脚本: {path}")
return path
self.manager_log.emit(f"警告: 无法找到脚本 '{script_name}', 尝试过以下路径:")
for path in possible_paths:
self.manager_log.emit(f" - {path} (存在: {os.path.exists(path)})")
# 返回None表示找不到
return None
def _get_arcgis_python(self):
"""获取ArcGIS Pro的Python解释器路径"""
# 这里的逻辑与之前相同
try:
arcgis_python_paths = [
r"C:\Program Files\ArcGIS\Pro\bin\Python\envs\arcgispro-py3\python.exe",
r"D:\ProgramData\ArcGis_Py\.env\arcgispro-py3-clone\python.exe",
]
# 检查环境变量 ARCGISPRO_PYTHON 或 CONDA_PREFIX (如果使用Conda环境)
if "ARCGISPRO_PYTHON" in os.environ:
arcgis_python_paths.insert(0, os.environ["ARCGISPRO_PYTHON"])
for path in arcgis_python_paths:
path = os.path.normpath(path)
if os.path.exists(path):
self.manager_log.emit(f"使用ArcGIS Python解释器: {path}")
return path
self.manager_log.emit(
"警告: 无法自动找到ArcGIS Pro Python解释器尝试使用当前Python解释器."
)
return sys.executable
except Exception as e:
self.manager_log.emit(f"获取ArcGIS Python路径失败: {str(e)}")
return sys.executable
# 修正:将添加任务到队列并尝试启动的逻辑命名为 run_script
def run_script(self, script_name, args_dict, task_description=None, working_dir=None):
"""
将一个脚本任务添加到队列中进行管理和并行执行。
Args:
script_name (str): 要执行的脚本文件名。
args_dict (dict): 传递给脚本的参数字典。
task_description (str, optional): 任务的描述。
working_dir (str, optional): 脚本执行的工作目录。
返回:
str: 分配给该任务的唯一任务ID 或 None 如果脚本不存在。
"""
script_path = self._find_script(script_name)
if not script_path or not os.path.exists(script_path):
self.manager_log.emit(f"错误: 无法添加任务,脚本 '{script_name}' 不存在.")
return None
# 生成唯一的任务ID
task_id = str(uuid.uuid4().hex[:8])
if task_description is None:
task_description = os.path.basename(script_name)
# 将任务信息添加到待处理队列
self.pending_tasks.append((task_id, script_path, args_dict, task_description, working_dir))
self.manager_log.emit(f"已添加任务{task_id} - {task_description}到队列...")
# 尝试启动下一个任务
self._start_next_task()
return task_id
# 新增方法:启动下一个可用的任务
def _start_next_task(self):
"""检查队列和正在运行的进程数量,启动下一个可用的任务"""
while len(self.running_processes) < self.max_concurrent and self.pending_tasks:
task_id, script_path, args_dict, task_description, working_dir = (self.pending_tasks.pop(0))
self.manager_log.emit(f"正在启动任务: {task_id} - {task_description}")
self.task_started.emit(task_id, task_description)
# 调用内部方法实际启动单个进程
self._start_single_process(task_id, script_path, args_dict, task_description, working_dir)
# 新增方法:实际启动单个 QProcess 进程
def _start_single_process(self, task_id, script_path, args_dict, task_description, working_dir):
"""
启动一个 QProcess 进程来执行指定的脚本任务。
这是从原始 run_script 中提取的启动逻辑。
"""
python_executable = self._get_arcgis_python()
if not python_executable or not os.path.exists(python_executable):
error_msg = f"无法找到有效的 ArcGIS Pro Python 解释器: {python_executable or '未找到'}"
self.task_error.emit(task_id, error_msg)
self._on_process_finished(task_id, None, -1, QProcess.ExitStatus.NormalExit) # 模拟失败完成
return
# 构建环境变量
env = QProcessEnvironment.systemEnvironment()
env.insert("PATH", os.environ["PATH"]) # 继承系统PATH
env.insert("PYTHONPATH", ";".join(sys.path)) # 继承当前Python路径
cmd = [python_executable, "-u", script_path] # 使用 -u 参数禁用缓冲
# 将参数字典转换为命令行参数
for key, value in args_dict.items():
if not isinstance(key, str) or not key:
self.task_error.emit(task_id, f"警告: 跳过无效的参数键: {key}")
continue
param_key = f"--{key}"
if isinstance(value, bool):
if value:
cmd.append(param_key)
elif value is not None and value != "NONE":
cmd.append(param_key)
if isinstance(value, (list, dict)):
try:
cmd.append(json.dumps(value))
except Exception as e:
self.task_error.emit(task_id, f"警告: 无法将参数 '{key}' 序列化为 JSON: {str(e)}")
cmd.append(str(value))
else:
cmd.append(str(value))
self.task_log.emit(task_id, f"即将执行命令: {subprocess.list2cmdline(cmd)}")
proc = QProcess()
proc.setProcessEnvironment(env) # 关键设置
# 连接信号到处理函数,使用 functools.partial 传递 task_id 和 process 对象
# 注意_handle_stdout/_handle_stderr 信号本身不传递 process需要通过 task_id 从 running_processes 获取
# _on_process_finished 信号传递 exit_code, exit_status通过 partial 传递 task_id 和 proc
proc.readyReadStandardOutput.connect(
functools.partial(self._handle_stdout, task_id=task_id)
)
proc.readyReadStandardError.connect(
functools.partial(self._handle_stderr, task_id=task_id)
)
proc.finished.connect(
functools.partial(self._on_process_finished, task_id=task_id, proc=proc)
)
# 设置工作目录
if working_dir:
if os.path.exists(working_dir):
proc.setWorkingDirectory(working_dir)
self.task_log.emit(task_id, f"工作目录设置为: {working_dir}")
else:
self.task_error.emit(task_id, f"警告: 工作目录不存在: {working_dir}")
# 启动进程
try:
proc.start(cmd[0], cmd[1:])
self.running_processes[task_id] = proc # 将进程实例添加到正在运行列表
self.task_log.emit(task_id, f"进程已启动PID: {proc.processId()}")
# 可以将任务描述等信息附加到 QProcess 对象,方便在槽函数中使用
proc.setProperty("task_description", task_description)
except Exception as e:
error_msg = f"无法启动进程: {str(e)}\n命令: {subprocess.list2cmdline(cmd)}"
self.task_error.emit(task_id, error_msg)
# 在启动失败时,也需要调用 finished 槽来清理并触发下一个任务
self._on_process_finished(task_id, proc, -1, QProcess.ExitStatus.NormalExit) # 模拟失败完成
# 修改处理槽函数以接受 task_id 并从 running_processes 获取进程
def _handle_stdout(self, task_id):
"""处理指定任务的标准输出"""
proc = self.running_processes.get(task_id)
if not proc:
return # 如果进程已不在运行列表中,忽略信号
try:
data = proc.readAllStandardOutput().data()
# 尝试多种解码方式,确保能处理中文
try:
text = data.decode("utf-8")
except UnicodeDecodeError:
try:
text = data.decode("gbk") # Windows 常用
except UnicodeDecodeError:
text = data.decode("utf-8", errors="replace") # 保底
if text:
for line in text.splitlines():
if line.strip():
# 根据行的前缀进一步解析,例如 STATUS: 或 RESULT:
if line.startswith("STATUS:"):
status_message = line[len("STATUS:") :].strip()
self.task_log.emit(task_id, f"状态: {status_message}")
elif line.startswith("RESULT:"):
self.task_log.emit(task_id, f"原始结果行: {line.strip()}")
proc.setProperty("last_result_line", line.strip()) # 将结果行附加到 QProcess 对象
else:
self.task_log.emit(task_id, line.strip()) # 其他普通输出
except Exception as e:
self.task_error.emit(task_id, f"处理标准输出时出错: {str(e)}")
self.task_log.emit(task_id, traceback.format_exc()) # 记录详细错误
def _handle_stderr(self, task_id):
"""处理指定任务的错误输出"""
proc = self.running_processes.get(task_id)
if not proc:
return # 如果进程已不在运行列表中,忽略信号
try:
data = proc.readAllStandardError().data()
# 尝试多种解码方式
try:
text = data.decode("utf-8")
except UnicodeDecodeError:
try:
text = data.decode("gbk") # Windows 常用
except UnicodeDecodeError:
text = data.decode("utf-8", errors="replace") # 保底
if text:
for line in text.splitlines():
if line.strip():
# 错误信息直接作为错误日志发送
self.task_error.emit(task_id, f"脚本错误: {line.strip()}")
# 可以在这里将错误输出附加到 QProcess 对象,以便在 finished 中汇总
current_stderr = proc.property("accumulated_stderr") or ""
proc.setProperty("accumulated_stderr", current_stderr + line.strip() + "\n")
except Exception as e:
self.task_error.emit(task_id, f"处理错误输出时出错: {str(e)}")
self.task_log.emit(task_id, traceback.format_exc()) # 记录详细错误
# 修改处理槽函数以接受 task_id 和 proc 并进行清理和调度
def _on_process_finished(self, exit_code, exit_status, task_id, proc):
"""处理指定任务的进程完成事件"""
self.task_log.emit(task_id, f"进程完成. 退出码: {exit_code}, 退出状态: {exit_status}")
# 在处理完成信号时,立即断开输出信号连接
if proc:
try:
proc.readyReadStandardOutput.disconnect()
except TypeError: # 有时信号可能已经断开,捕获 TypeError
pass
try:
proc.readyReadStandardError.disconnect()
except TypeError:
pass
# 也可以断开 finished 信号本身,但通常不需要,因为它只触发一次
# try:
# proc.finished.disconnect()
# except TypeError:
# pass
success = False
message = "未知错误或脚本未返回明确结果" # 默认消息
# 从运行列表中移除进程
if task_id in self.running_processes:
# 确保删除的是正确的进程实例
if self.running_processes[task_id] is proc:
del self.running_processes[task_id]
else:
self.manager_log.emit(f"警告: 任务ID {task_id} 在 running_processes 中对应进程不匹配完成信号的进程实例.")
# 这种情况比较异常,可能需要更深入的调试
else:
# 如果任务ID不在运行列表中可能是重复的 finished 信号或逻辑错误
# 或者是在 _start_single_process 中模拟失败完成的情况
if (proc and proc.processId() != 0): # 检查 proc 是否是有效的 QProcess 实例且已启动
self.manager_log.emit(f"警告: 收到未知任务ID {task_id} 的完成信号,但 PID {proc.processId()} 已知。")
else:
self.manager_log.emit(f"警告: 收到未知任务ID {task_id} 的完成信号.")
# 尝试从 QProcess 对象属性中获取 RESULT 行
# 在 _start_single_process 中模拟失败完成时proc 可能为 None
result_line = proc.property("last_result_line") if proc else None
accumulated_stderr = proc.property("accumulated_stderr") or "" if proc else ""
if result_line:
try:
# 解析 RESULT 行
parts = result_line[len("RESULT:") :].split("|", 2) # 分割最多两次
if len(parts) >= 3:
success_from_script = parts[0] == "True"
output_path = parts[1]
error_msg_from_script = parts[2]
if success_from_script:
success = True
message = f"成功: {output_path}"
else:
# 如果脚本返回失败,使用脚本提供的错误信息
success = False
message = f"脚本返回失败: {error_msg_from_script}"
else:
# 如果 RESULT 行格式不正确
success = False
message = f"脚本返回结果格式错误: {result_line}"
except Exception as e:
success = False
message = f"解析脚本返回结果时出错: {str(e)}\n原始结果行: {result_line}"
else:
# 如果没有找到 RESULT 行,根据退出码判断
if exit_status == QProcess.ExitStatus.NormalExit and exit_code == 0:
success = True
message = "脚本正常退出,但未找到 RESULT 行。"
self.task_log.emit(task_id, message) # 记录为日志而不是错误
else:
success = False
message = f"脚本异常退出 (退出码: {exit_code})."
# 如果有累积的错误输出,也添加到消息中
if accumulated_stderr:
message += f"\n错误输出:\n{accumulated_stderr.strip()}"
self.task_error.emit(task_id, message) # 记录为错误
# 清理 QProcess 实例 (仅当 proc 对象有效时)
if proc:
proc.close()
proc.deleteLater() # 延迟删除对象,确保槽函数执行完毕
# 发射任务完成信号
if len(self.running_processes) == 0:
self.task_finished.emit(task_id, success, message)
# 尝试启动下一个任务
self._start_next_task()
def stop_all_tasks(self):
"""尝试停止所有正在运行的任务"""
self.manager_log.emit("正在尝试停止所有任务...")
# 复制字典的键,避免在迭代时修改
tasks_to_stop = list(self.running_processes.keys())
for task_id in tasks_to_stop:
proc = self.running_processes.get(task_id)
if proc and proc.state() == QProcess.ProcessState.Running:
self.task_log.emit(task_id, "尝试终止进程...")
proc.terminate() # 尝试优雅终止 (发送SIGTERM)
# proc.kill() # 强制杀死进程 (发送SIGKILL) - 更可靠,但可能导致数据损坏
# --- 保留原有 run_... 方法,它们现在会调用 run_script 来添加任务 ---
def run_export_map(self, params):
script_name = "export_map_v1.py"
task_desc = f"导出地图: {params.get('county_name', '未知区县')}"
return self.run_script(script_name, params, task_description=task_desc)
def run_export_layout(self, params):
script_name = "export_layout.py"
task_desc = f"导出布局: {os.path.basename(params.get('input_aprx_folder', '未知文件夹'))}"
return self.run_script(script_name, params, task_description=task_desc)
def run_batch_export_layout(self, params):
script_name = "batch_export_layout.py"
task_desc = f"批量导出布局: {os.path.basename(params.get('input_aprx_folder', '未知文件夹'))}"
return self.run_script(script_name, params, task_description=task_desc)
def run_process_raster(self, params):
script_name = ("raster_to_polygon.py")
task_desc = (f"处理栅格: {os.path.basename(params.get('input_raster', '未知栅格'))}")
return self.run_script(script_name, params, task_description=task_desc)
def run_area_stat(self, params):
script_name = "stats_area_to_excel.py"
task_desc = f"统计面积: {os.path.basename(params.get('reclassed_polygon', '未知面要素'))}"
return self.run_script(script_name, params, task_description=task_desc)
def run_suanhua_stat(self, params):
script_name = "stats_sh_to_excel.py"
task_desc = f"统计酸化: {os.path.basename(params.get('reclassed_polygon', '未知面要素'))}"
return self.run_script(script_name, params, task_description=task_desc)
def run_soil_prop_stat(self, params):
script_name = "stats_soil_prop_to_excel.py"
task_desc = f"统计酸化: {os.path.basename(params.get('reclassed_polygon', '未知面要素'))}"
return self.run_script(script_name, params, task_description=task_desc)
def run_excel_to_jpg(self, params):
script_name = "export_excel_to_jpg_v1.py"
task_desc = f"导出Excel: {os.path.basename(params.get('excel_path', '未知文件'))}"
return self.run_script(script_name, params, task_description=task_desc)
def run_test_script(self, params):
script_name = "test_script.py"
task_desc = f"测试脚本: {params.get('message', '无消息')}"
return self.run_script(script_name, params, task_description=task_desc)
# 可以添加其他通用的任务管理方法,例如:
def get_pending_tasks(self):
"""获取当前待处理任务列表"""
# 返回任务ID和描述的列表
return [(tid, desc) for tid, sp, args, desc, wd in self.pending_tasks]
def get_running_tasks(self):
"""获取当前正在运行任务的 task_id 和描述的列表"""
# 需要从 QProcess 对象中获取任务描述 (如果在启动时设置了属性)
running_list = []
for tid, proc in self.running_processes.items():
desc = proc.property("task_description") or "未知任务"
running_list.append((tid, desc))
return running_list
def get_max_concurrent(self):
"""获取最大并行任务数设置"""
return self.max_concurrent
def set_max_concurrent(self, count):
"""设置最大并行任务数,并尝试启动更多任务"""
if count > 0:
self.max_concurrent = count
self.manager_log.emit(f"最大并行任务数已更改为: {self.max_concurrent}")
self._start_next_task() # 尝试启动更多任务
else:
self.manager_log.emit("警告: 最大并行任务数必须大于 0。")

View File

@@ -0,0 +1,655 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
脚本运行器
用于从PyQt6界面调用独立脚本
"""
import os
import sys
import json
import subprocess
import traceback
from PyQt6.QtCore import QObject, pyqtSignal, QProcess, QThread, QRunnable, QThreadPool
class LambdaTask(QRunnable):
"""
用于在QThreadPool中执行任意函数的任务类
"""
def __init__(self, fn, *args, **kwargs):
super().__init__()
self.fn = fn
self.args = args
self.kwargs = kwargs
def run(self):
"""执行函数"""
try:
self.fn(*self.args, **self.kwargs)
except Exception as e:
print(f"任务执行错误: {str(e)}")
traceback.print_exc()
class ScriptRunner(QObject):
"""脚本运行器类,用于调用独立脚本处理任务"""
# 定义信号
started = pyqtSignal(str)
finished = pyqtSignal(object)
error = pyqtSignal(str)
log = pyqtSignal(str)
def __init__(self, parent=None):
super().__init__(parent)
# self.parent_ref = weakref.ref(parent) if parent else None
# 获取项目根目录
self.base_dir = self._get_base_dir()
self.log.emit(f"项目根目录: {self.base_dir}")
def _get_base_dir(self):
"""获取项目根目录"""
# 当前文件的目录
current_dir = os.path.dirname(os.path.abspath(__file__))
# 向上两级找到项目根目录 (ui -> tools -> project_root)
return os.path.dirname(os.path.dirname(current_dir))
def _find_script(self, script_name):
"""查找脚本文件"""
# 可能的脚本路径
possible_paths = [
os.path.join(self.base_dir, 'tools', 'core', script_name), # 从项目根目录查找
os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'core', script_name), # 从tools目录查找
os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'core', script_name)), # 相对于ui目录
os.path.join(os.getcwd(), 'tools', 'core', script_name) # 从当前工作目录查找
]
# 尝试每个可能的路径
for path in possible_paths:
path = os.path.normpath(path) # 规范化路径
if os.path.exists(path):
self.log.emit(f"找到脚本: {path}")
return path
# 如果找不到脚本,记录所有尝试的路径
self.log.emit("无法找到脚本,尝试过以下路径:")
for path in possible_paths:
path = os.path.normpath(path)
self.log.emit(f" - {path} (存在: {os.path.exists(path)})")
# 返回第一个路径(即使它不存在)
return possible_paths[0]
def run_script_external(self, script_path, args, working_dir=None):
"""
使用外部Python窗口运行脚本显示实时输出
Args:
script_path: 脚本路径
args: 命令行参数字典
working_dir: 工作目录
Returns:
返回启动是否成功
"""
try:
# 更详细的日志
self.log.emit(f"尝试在外部窗口运行脚本: {script_path}")
# 检查脚本是否存在
if not os.path.exists(script_path):
self.error.emit(f"脚本不存在: {script_path}")
self.log.emit(f"当前工作目录: {os.getcwd()}")
self.log.emit(f"尝试查找脚本...")
# 从脚本名尝试查找脚本
script_name = os.path.basename(script_path)
actual_path = self._find_script(script_name)
if not os.path.exists(actual_path):
self.error.emit(f"无法找到脚本: {script_name}")
return False
script_path = actual_path
self.log.emit(f"使用脚本路径: {script_path}")
# 获取ArcGIS Pro的Python解释器路径
python_exe = self._get_arcgis_python() or sys.executable
self.log.emit(f"使用Python解释器: {python_exe}")
# 构建命令行参数
cmd = [python_exe, script_path]
# 添加参数
for key, value in args.items():
if key.startswith('--'):
param_key = key
else:
param_key = f"--{key}"
if isinstance(value, bool):
# 布尔参数
if value:
cmd.append(param_key)
elif isinstance(value, list):
# 列表参数通过JSON序列化传递
cmd.append(param_key)
cmd.append(json.dumps(value))
elif value is not None:
# 其他参数
cmd.append(param_key)
cmd.append(str(value))
# 通知开始
self.started.emit(f"开始执行脚本: {os.path.basename(script_path)}")
# 创建临时批处理文件,为了能自动关闭窗口
batch_file_path = os.path.join(os.environ.get('TEMP', '.'), f"run_script_{os.getpid()}.bat")
# 构建批处理文件内容
batch_content = f"""@echo off
chcp 65001 > nul
echo 正在执行脚本: {os.path.basename(script_path)}...
echo 命令: {' '.join(cmd)}
echo.
"""
# 设置工作目录
if working_dir:
batch_content += f'cd /d "{working_dir}"\n'
# 添加执行命令
batch_content += f'"{python_exe}" "{script_path}"'
# 添加命令行参数
for i in range(2, len(cmd)):
# 检查参数是否需要引号
param = cmd[i]
if ' ' in param or ',' in param or ';' in param or '&' in param or '[' in param or ']' in param:
batch_content += f' "{param}"'
else:
batch_content += f' {param}'
# 添加执行完成后的提示和暂停
batch_content += "\necho.\necho 脚本执行完成窗口将在3秒后自动关闭...\ntimeout /t 3 >nul\n"
# 写入批处理文件
with open(batch_file_path, 'w', encoding='utf-8') as f:
f.write(batch_content)
self.log.emit(f"创建批处理文件: {batch_file_path}")
# 使用subprocess.Popen启动外部窗口
# 使用start命令在新窗口中运行批处理文件
start_cmd = f'start "执行脚本 - {os.path.basename(script_path)}" "{batch_file_path}"'
subprocess.Popen(start_cmd, shell=True)
# 模拟成功启动
self.log.emit("已在外部窗口启动脚本")
# 注意:由于是在外部窗口运行,我们无法获知脚本的实际结果
# 假设脚本已成功启动,但实际执行结果需要用户观察外部窗口
QThread.msleep(1000) # 等待1秒
self.finished.emit(True)
# 预定批处理文件的删除
def delete_batch_file():
try:
if os.path.exists(batch_file_path):
os.remove(batch_file_path)
self.log.emit(f"已删除临时批处理文件: {batch_file_path}")
except Exception as e:
self.log.emit(f"删除临时批处理文件失败: {str(e)}")
# 创建延迟删除任务
QThread.sleep(10) # 确保批处理文件有足够时间执行
delete_task = LambdaTask(delete_batch_file)
thread_pool = QThreadPool.globalInstance()
if thread_pool is not None:
thread_pool.start(delete_task)
return True
except Exception as e:
self.error.emit(f"启动外部脚本时出错: {str(e)}")
self.log.emit(traceback.format_exc())
return False
def run_script(self, script_path, args, working_dir=None):
"""使用非阻塞方式运行脚本"""
try:
# 检查脚本是否存在
if not os.path.exists(script_path):
self.error.emit(f"脚本不存在: {script_path}")
self.log.emit(f"当前工作目录: {os.getcwd()}")
self.log.emit(f"尝试查找脚本...")
# 从脚本名尝试查找脚本
script_name = os.path.basename(script_path)
actual_path = self._find_script(script_name)
if not os.path.exists(actual_path):
self.error.emit(f"无法找到脚本: {script_name}")
return False
script_path = actual_path
# 获取ArcGIS Pro的Python解释器路径
python_exe = self._get_arcgis_python() or sys.executable
self.log.emit(f"使用Python解释器: {python_exe}")
# 构建命令行参数
cmd = [python_exe, "-u", script_path]
# 添加参数
for key, value in args.items():
if key.startswith('--'):
param_key = key
else:
param_key = f"--{key}"
if isinstance(value, bool):
# 布尔参数
if value:
cmd.append(param_key)
elif value is not None:
# 其他参数
cmd.append(param_key)
cmd.append(str(value))
# 创建QProcess而不是subprocess.Popen
process = QProcess()
# 连接QProcess的信号
process.readyReadStandardOutput.connect(
lambda: self._handle_stdout(process)
)
process.readyReadStandardError.connect(
lambda: self._handle_stderr(process)
)
process.finished.connect(
lambda exit_code, exit_status: self._handle_finished(
exit_code, exit_status, process
)
)
# 设置工作目录
if working_dir:
process.setWorkingDirectory(working_dir)
# 通知开始
self.started.emit(f"开始执行脚本: {os.path.basename(script_path)}")
# 非阻塞启动进程
process.start(cmd[0], cmd[1:])
# 将进程对象保存在类变量中,防止被垃圾回收
self.current_process = process
# 返回True表示进程已启动结果将通过信号异步通知
return True
except Exception as e:
self.error.emit(f"执行脚本时出错: {str(e)}")
self.log.emit(traceback.format_exc())
return False
def _handle_stdout(self, process):
"""处理标准输出"""
try:
# 首先尝试使用utf-8解码
data = process.readAllStandardOutput().data()
try:
text = data.decode('utf-8')
except UnicodeDecodeError:
# utf-8解码失败尝试使用系统默认编码或GBK中文Windows常用
try:
text = data.decode('gbk')
except UnicodeDecodeError:
# 如果还是失败使用errors='replace'选项替换无法解码的字符
text = data.decode('utf-8', errors='replace')
if text:
for line in text.splitlines():
if line.strip():
self.log.emit(line.strip())
except Exception as e:
self.log.emit(f"处理标准输出时出错: {str(e)}")
def _handle_stderr(self, process):
"""处理错误输出"""
try:
# 首先尝试使用utf-8解码
data = process.readAllStandardError().data()
try:
text = data.decode('utf-8')
except UnicodeDecodeError:
# utf-8解码失败尝试使用系统默认编码或GBK中文Windows常用
try:
text = data.decode('gbk')
except UnicodeDecodeError:
# 如果还是失败使用errors='replace'选项替换无法解码的字符
text = data.decode('utf-8', errors='replace')
if text:
for line in text.splitlines():
if line.strip():
self.log.emit(f"错误: {line.strip()}")
except Exception as e:
self.log.emit(f"处理错误输出时出错: {str(e)}")
def _handle_finished(self, exit_code, exit_status, process):
"""处理进程结束"""
if exit_code == 0:
self.log.emit(f"脚本执行成功,返回代码: {exit_code}")
self.finished.emit(True)
else:
self.error.emit(f"脚本执行失败,返回代码: {exit_code}")
self.finished.emit(False)
# 清理进程
process.close()
if hasattr(self, 'current_process') and self.current_process == process:
self.current_process = None
def _get_arcgis_python(self):
"""获取ArcGIS Pro的Python解释器路径"""
try:
# 常见的ArcGIS Pro Python路径
arcgis_python_paths = [
r"C:\Program Files\ArcGIS\Pro\bin\Python\envs\arcgispro-py3\python.exe",
r"C:\Program Files\ArcGIS\Pro\bin\Python\python.exe"
]
# 检查环境变量
if 'ARCGISPRO_PYTHON' in os.environ:
arcgis_python_paths.insert(0, os.environ['ARCGISPRO_PYTHON'])
# 尝试每个可能的路径
for path in arcgis_python_paths:
if os.path.exists(path):
return path
return None # 如果找不到返回None
except Exception as e:
self.log.emit(f"获取ArcGIS Python路径失败: {str(e)}")
return None
def run_export_map(self, params):
"""
运行导出地图脚本
Args:
params: 参数字典,包含:
- config_file: 配置文件路径
- county_name: 区县名称
- polygon_list: 要导出的图层列表
- template_aprx_file: 模板文件路径
- output_path: 输出路径
- data_source_path: 数据源路径
- symbol_path: 符号文件路径
- force_regenerate: 是否强制重新生成
Returns:
返回脚本执行结果
"""
try:
# 检查必要参数是否存在
required_params = ['config_file', 'county_name', 'polygon_list',
'template_aprx_file', 'output_path',
'data_source_path', 'symbol_path']
for param in required_params:
if param not in params:
self.error.emit(f"缺少必要参数: {param}")
return False
# 查找脚本路径
script_path = self._find_script('export_map.py')
# 构建命令行参数
args = {
'config_file': params['config_file'],
'county_name': params['county_name'],
'polygon_list': json.dumps(params['polygon_list']), # 将图层列表序列化为JSON字符串
'template_aprx_file': params['template_aprx_file'],
'output_path': params['output_path'],
'data_source_path': params['data_source_path'],
'symbol_path': params['symbol_path']
}
# 添加可选参数
if 'force_regenerate' in params and params['force_regenerate']:
args['force_regenerate'] = True
# 使用外部窗口执行脚本
return self.run_script(script_path, args)
except Exception as e:
self.error.emit(f"执行导出地图脚本时出错: {str(e)}")
self.log.emit(traceback.format_exc())
return False
def run_batch_export_map(self, params):
"""运行批量导出地图脚本 """
try:
# 获取参数
if 'export_config' not in params:
self.error.emit(f"缺少必要参数: export_config")
return False
export_config = params['export_config']
county_name = params['county_name']
template_aprx_file = params['template_aprx_file']
output_folder = params['output_path']
data_source_path = params['data_source_path']
symbol_path = params['symbol_path']
polygon_list = params.get('polygon_list', []) # 如果指定了图层列表,则只导出这些图层
force_regenerate = params.get('force_regenerate', False) # 是否强制重新生成工程文件
# 如果没有指定图层列表,则使用配置文件中的所有图层
if not polygon_list:
polygon_list = list(export_config.keys())
self.log.emit(f"开始批量导出 {len(polygon_list)} 个图层")
# 准备脚本路径
script_path = self._find_script('export_map.py')
# 记录成功和失败的图层
success_count = 0
failed_count = 0
# 循环处理每个图层
for layer_name in polygon_list:
try:
self.log.emit(f"\n===== 开始处理图层: {layer_name} =====")
# 构建命令行参数
args = {
'config': params['config_file'],
'county': county_name,
'layer': layer_name,
'template': template_aprx_file,
'output': output_folder,
'data_source': data_source_path,
'symbol': symbol_path,
'force': force_regenerate
}
# 执行脚本
if self.run_script(script_path, args):
success_count += 1
else:
failed_count += 1
except Exception as e:
self.log.emit(f"处理图层 {layer_name} 时出错: {str(e)}")
failed_count += 1
# 通知完成
self.log.emit(f"\n===== 批量处理完成 =====")
self.log.emit(f"总计图层: {len(polygon_list)}")
self.log.emit(f"成功: {success_count} 个")
self.log.emit(f"失败: {failed_count} 个")
result = {
'total': len(polygon_list),
'success': success_count,
'failed': failed_count
}
self.finished.emit(result)
return result
except Exception as e:
self.error.emit(f"执行批量导出地图脚本时出错: {str(e)}")
self.log.emit(traceback.format_exc())
return False
def run_export_layout(self, params):
"""
运行导出布局脚本
Args:
params: 参数字典
Returns:
返回脚本执行结果
"""
try:
# 准备参数
script_path = self._find_script('export_layout.py')
# 构建命令行参数
args = {
'mode': 'single',
'aprx': params['aprx_path'],
'output': params['output_path'],
'format': params.get('export_format', 'PDF'),
'resolution': params.get('resolution', 300),
'name': params.get('output_name', '')
}
# 执行脚本
# return self.run_script(script_path, args)
except Exception as e:
self.error.emit(f"执行导出布局脚本时出错: {str(e)}")
self.log.emit(traceback.format_exc())
return False
def run_batch_export_layout(self, params):
"""
运行批量导出布局脚本
Args:
params: 参数字典
Returns:
返回脚本执行结果
"""
try:
# 准备参数
script_path = self._find_script('export_layout.py')
# 构建命令行参数
args = {
'aprx_file_list':json.dumps(params['aprx_file_list']),
'input_aprx_folder': params['input_aprx_folder'],
'output': params['output_image_path'],
'format': params.get('export_format', 'PDF'),
'resolution': params.get('resolution', 300),
'use_multiprocessing': params.get('use_multiprocessing', False),
'process_count': params.get('process_count', 2)
}
# 执行脚本
return self.run_script(script_path, args)
except Exception as e:
self.error.emit(f"执行批量导出布局脚本时出错: {str(e)}")
self.log.emit(traceback.format_exc())
return False
def run_process_raster(self, params):
"""
运行栅格处理脚本
Args:
params: 参数字典
Returns:
返回脚本执行结果
"""
try:
# 准备参数
script_path = self._find_script('raster_to_vector.py')
# 构建命令行参数
args = {
'command': 'process',
'input': params['input_raster'],
'output': params['output_raster'],
'format': params.get('output_format', 'TIFF'),
'compression': params.get('compression', 'LZW')
}
# 执行脚本
return self.run_script(script_path, args)
except Exception as e:
self.error.emit(f"执行栅格处理脚本时出错: {str(e)}")
self.log.emit(traceback.format_exc())
return False
def run_raster_batch_process(self, params):
"""
运行批量栅格处理脚本
Args:
params: 参数字典
Returns:
返回脚本执行结果
"""
try:
# 准备参数
script_path = self._find_script('raster_handler.py')
params["selected_files"] = json.dumps(params.get("selected_files"))
# 执行脚本
return self.run_script(script_path, params)
except Exception as e:
self.error.emit(f"执行批量栅格处理脚本时出错: {str(e)}")
self.log.emit(traceback.format_exc())
return False
def run_test_script(self, params):
try:
# 检查必要参数是否存在
required_params = ['message', 'count']
for param in required_params:
if param not in params:
self.error.emit(f"缺少必要参数: {param}")
return False
# 查找脚本路径
script_path = self._find_script('test_script.py')
# 构建命令行参数
args = {
'message': params['message'],
'count': params['count']
}
# 执行脚本
return self.run_script(script_path, args)
except Exception as e:
self.error.emit(f"执行导出地图脚本时出错: {str(e)}")
self.log.emit(traceback.format_exc())
return False

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