初始化
This commit is contained in:
1
tools/ui/__init__.py
Normal file
1
tools/ui/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# UI界面模块
|
||||
1
tools/ui/components/__init__.py
Normal file
1
tools/ui/components/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# UI界面模块
|
||||
67
tools/ui/components/file_list_group.py
Normal file
67
tools/ui/components/file_list_group.py
Normal 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()
|
||||
64
tools/ui/components/input_group.py
Normal file
64
tools/ui/components/input_group.py
Normal 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()
|
||||
0
tools/ui/config/__init__.py
Normal file
0
tools/ui/config/__init__.py
Normal file
60
tools/ui/config/settings.json
Normal file
60
tools/ui/config/settings.json
Normal 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
291
tools/ui/main_window.py
Normal 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())
|
||||
0
tools/ui/runners/__init__.py
Normal file
0
tools/ui/runners/__init__.py
Normal file
475
tools/ui/runners/script_runner.py
Normal file
475
tools/ui/runners/script_runner.py
Normal 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。")
|
||||
655
tools/ui/runners/script_runner.txt
Normal file
655
tools/ui/runners/script_runner.txt
Normal 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
|
||||
|
||||
9
tools/ui/tabs/__init__.py
Normal file
9
tools/ui/tabs/__init__.py
Normal 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']
|
||||
373
tools/ui/tabs/acid_stat_tab.py
Normal file
373
tools/ui/tabs/acid_stat_tab.py
Normal 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())
|
||||
564
tools/ui/tabs/area_stat_tab.py
Normal file
564
tools/ui/tabs/area_stat_tab.py
Normal 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())
|
||||
597
tools/ui/tabs/config_editor_dialog.py
Normal file
597
tools/ui/tabs/config_editor_dialog.py
Normal 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())
|
||||
291
tools/ui/tabs/export_layout_tab.py
Normal file
291
tools/ui/tabs/export_layout_tab.py
Normal 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)
|
||||
385
tools/ui/tabs/export_map_tab.py
Normal file
385
tools/ui/tabs/export_map_tab.py
Normal 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)
|
||||
244
tools/ui/tabs/raster_processing_common.py
Normal file
244
tools/ui/tabs/raster_processing_common.py
Normal 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
519
tools/ui/tabs/raster_tab.py
Normal 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())
|
||||
353
tools/ui/tabs/soil_prop_stat_tab.py
Normal file
353
tools/ui/tabs/soil_prop_stat_tab.py
Normal 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
569
tools/ui/tabs/test_tab.py
Normal 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)}")
|
||||
496
tools/ui/tabs/xlsx_jpg_tab.py
Normal file
496
tools/ui/tabs/xlsx_jpg_tab.py
Normal 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())
|
||||
Reference in New Issue
Block a user