初始化

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

View File

View File

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

View File

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