在基于对话框的应用中执行空闲状态处理(比如用ON_UPDATE_COMMAND_UI更新控件)

本文围绕MFC展开,探讨了基于对话框程序中OnIdle无法工作的问题,指出MFC在模态对话框处理上的变化及WM_ENTERIDLE的局限性,介绍了WM_KICKIDLE作为替代方案。还提及使ON_COMMAND_UPDATE_UI处理函数在对话框中工作的方法,需处理WM_KICKIDLE并调用UpdateDialogControls。

June 1995,Microsoft System Journal

 

Paul DiLascia 是一个自由软件顾问,专长是训练和软件开发(C++ and Windows).他是Windows ++: Writing Reusable Code in C++ (Addison-Wesley, 1992)的作者.

问:我的问题是OnIdle在通常的文档/视图程序中可以工作,但是看起来在基于对话框的程序中不行。我的CApp::InitInstance调用dlg.DoModal,调用一个函数:不调用OnIdle的CWnd::RunModalLoop。我想我应该在WM_ENTERIDLE中做一些后台处理,但是这个消息是发送到对话框的父窗口的。在我的这种情况下,父窗口不存在。请帮忙!

Jim Kallimani

如你所见,“模态”对话框在MFC4.0中实际上是非模态的。当你调用CDialog::DoModal的时候,MFC并不调用::DialogBox,像它以前所做的,而是调用CreateDialogIndirect (在三思之后)然后通过禁用父窗口并且进入自己的消息循环模拟模态行为。这是::DialogBox所做的本质工作。这样做的好处是MFC拥有对话框的消息循环,以前它被Windows API函数::DialogBox隐藏起来了。这样MFC通过通常MFC的消息泵CWinThread::PumpMessage取模态对话框消息,像其他窗口一样。特别的,你可以为模态对话框重载CWnd::PreTranslateMessage—例如实现加速键。MFC的早期版本允许你为模态对话框实现你自己的PreTranslateMessage,但是没有被系统调用,因为CDialog::DoModal直接执行::DialogBox,直到你的对话框消息处理函数调用EndDialog才返回。同样,使用::DialogBox, 使用通常的MFC方法进行消息处理是不可能的,因为程序控制消失在::DialogBox中,直到对话框结束才返回。

作为替代方案, Windows有它自己的机制, WM_ENTERIDLE, 在模态对话框中进行消息处理。当处理完一个或多个消息之后,Windows 发送 WM_ ENTERIDLE 到一个模态对话框或菜单的所有者,如果消息队列中没有等待的消息的话。只有模态对话框发送WM_ENTERIDLE,而非模态对话框不发送。因为MFC现在使用非模态对话框,甚至是在使用模态对话框的时候实际上也是使用非模态对话框,MFC不得不自己发送WM_ENTERIDLE以模仿模态对话框—-但是仅当对话框有父窗口的时候才这么干。Jim碰到麻烦,因为没有父窗口来接收WM_ENTERIDLE。你的头快昏了吗?

如果MFC通过标准消息泵取模态对话框消息,为什么不调用CWinApp::OnIdle作为自己消息处理的一部分?为题是CWnd::RunModalLoop 调用了CWinThread::PumpMessage但是OnIdle在CWinThread::Run中出现。当你的应用程序调用了InitInstance函数之后,MFC调用CWinThread::Run运行你的应用程序。CWinThread::Run的浓缩形式看起来像这样:

// (from THRDCORE.CPP)
int CWinThread::Run()
{
      // 为了空闲状态处理

      BOOL bIdle = TRUE;
      LONG lIdleCount = 0;

      for (;;) {
            while (bIdle && !::PeekMessage(...)) {
                  //当在bIdle状态时调用OnIdle

                      if (!OnIdle(lIdleCount++))
                        // 假设"非空闲" 状态
                        bIdle = FALSE;

            }

            // 获取/预处理/分派消息
            // (调用 CWinThread::PumpMessage)

      }
}

我砍掉了很多,以强调空闲处理如何工作。如果没有消息在等待,MFC重复调用CWinThread::OnIdle,传递给它每次增加的一个计数器参数。你可以使用这个参数区分不同种类的空闲处理的优先次序。你可能在空闲计数为1时作格式化,空闲计数为2时更新一个指示当天时间的时钟。当你的OnIdle返回FALSE时,MFC停止调用它并且等待,直到你的线程得到另一个消息,因此空闲循环从头开始。

模态对话框从不执行这个代码,因为CWnd::RunModalLoop直接在自己的消息循环中调用CWinThread::PumpMessage。它没有调用CWinThread::Run,因此从不调用CWinThread::OnIdle。 Redmond 的人员告诉我这是由设计上决定的。显然,在模态对话框中调用OnIdle是危险的,因为许多消息处理函数建立临时CWnd对象,它们被期望在对话框生存期中存在。默认空闲处理的一部分就是释放临时句柄映射。(译者注:临时CWnd对象依赖于临时句柄映射而存在。.)

(我不得不告诉你,依我所见,整个MFC用来连接HWND和CWnd的临时/永久句柄映射机制是整个架构中的灾难之一,甚至比它们的消息映射还要坏。临时映射机制的问题不断出现在程序中—特别是在多线程应用中,使得他们很难用MFC编写。)

这样看来,你如何在基于对话框的应用程序中进行消息处理,当对话框没有父窗口的时候?幸运的是,它易如反掌。MFC开发者提供一个钩子: WM_KICKIDLE。 RunModalLoop 不断发送这个MFC私有消息,当消息队列中没有消息的时候—就像CWinThread::Run调用OnIdle一样。 RunModalLoop甚至还为你提供一个计数器并且依次递增。实际上,WM_KICKIDLE是对话框的OnIdle替代品。 (历史信息:早期版本的MFC为属性表作这个模态/非模态切换和提供WM_KICKIDLE。显然它工作的如此之好,以至于他们决定使所有的模态对话框非模态化。)

要警告你的是:你可能在OnKickIdle函数中,想调用你的主应用程序的OnIdle函数

LRESULT CMyDlg::OnKickIdle(WPARAM, LPARAM lCount)
{
      return AfxGetApp()->OnIdle(lCount);
}

MFC人员告诉我这是危险的;因为临时映射问题。在OnKickIdle中执行你的空闲处理会更安全一些。如果有必要,你可以组合共有的空闲处理成为一个辅助函数,在CApp::OnIdle 和 CMyDlg::OnKickIdle中调用。

当我在处理空闲处理的问题的时候,发现不是所有程序员都知道CDocTemplate和CDocument的OnIdle函数! 如果你要在文档或文档模板中执行空闲处理,只需重载这些函数。


Internet:

Paul DiLascia
72400.2702@compuserve.com

From the June 1996 issue of Microsoft Systems Journal.

July 1997,Microsoft System Journal

......

下面我将指出如何使ON_COMMAND_UPDATE_UI处理函数在对话框中工作。在通常的MFC文档/视图应用中,MFC使用内部消息WM_IDLEUPDATECMDUI更新菜单项、工具栏按钮、状态栏格等用户界面对象。作为空闲处理的一部分,IDLEUPDATECMDUI广播到所有你的应用程序的窗口。工具栏、状态栏和对话栏的命令处理依次调用UpdateDialogControls广播另一个命令,CN_UPDATE_COMMAND_UI,到窗口上的所有控件。从你的程序员的角度来看,这些消息是不可见的。你只需实现ON_UPDATE_COMMAND_UI处理你的菜单项和按钮,然后,看? 它们被变魔法似的更新了。(需要更多信息的话,参见我的在1995年6月MSJ.上的文章 "Meandering Through the Maze of MFC Message and Command Routing" )
      不幸的是,这个奇妙的UI更新机制不能用于对话框—至少不是自动的。 你必须自己修补一下。幸好它很简单。你只需处理WM_KICKIDLE,一个MFC私有消息;当对话框空闲时发送出来(类似应用程序的OnIdle处理)给.你自己调用UpdateDialogControls。.


 LRESULT CTabDialog::OnKickIdle(WPARAM wp,
                                LPARAM lCount)
 {
     UpdateDialogControls(this, TRUE);
     return 0;
 }

CWnd::UpdateDialogControls发送魔术般的CN_ UPDATE_COMMAND_UI 消息给所有对话框控件,结果是现在ON_COMMAND_ UPDATE_UI处理突然在对话框中可以工作了。
import tkinter as tk from tkinter import ttk, scrolledtext, messagebox import subprocess import threading import re import time import os from datetime import datetime class DeviceManager: """设备管理类""" def __init__(self): self.devices = {} # {device_id: {'status': 'idle', 'session_id': None, 'last_test': None}} self.lock = threading.Lock() def refresh_devices(self): """刷新设备列表""" try: result = subprocess.run( ["adb", "devices"], capture_output=True, text=True, encoding="utf-8" ) new_devices = {} for line in result.stdout.splitlines()[1:]: if line.strip() and "device" in line: device_id = line.split("\t")[0] new_devices[device_id] = self.devices.get( device_id, {'status': 'idle', 'session_id': None, 'last_test': None} ) with self.lock: self.devices = new_devices return True except Exception as e: return False, str(e) def update_device_status(self, device_id, status, session_id=None): """更新设备状态""" with self.lock: if device_id in self.devices: self.devices[device_id]['status'] = status if session_id: self.devices[device_id]['session_id'] = session_id if status == 'completed': self.devices[device_id]['last_test'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") def get_selected_devices(self, selected_ids): """获取选中的设备信息""" with self.lock: return {did: info for did, info in self.devices.items() if did in selected_ids} class GMSTestAssistant(tk.Tk): def __init__(self): super().__init__() self.title("GMS测试助手 v3.0 - 多设备管理") self.geometry("1100x800") self.device_manager = DeviceManager() # 创建主框架 self.create_widgets() self.refresh_devices() # 测试状态 self.test_running = False self.active_threads = {} # 定时刷新设备状态 self.after(5000, self.periodic_refresh) def create_widgets(self): """创建界面组件""" # 设备管理面板 device_frame = ttk.LabelFrame(self, text="设备管理") device_frame.pack(fill="x", padx=15, pady=10) # 设备列表控件 self.device_tree = ttk.Treeview( device_frame, columns=("status", "session", "last_test"), show="headings", height=8 ) self.device_tree.heading("#0", text="设备ID") self.device_tree.heading("status", text="状态") self.device_tree.heading("session", text="Session ID") self.device_tree.heading("last_test", text="最后测试时间") self.device_tree.column("#0", width=200) self.device_tree.column("status", width=100) self.device_tree.column("session", width=150) self.device_tree.column("last_test", width=150) # 添加滚动条 scrollbar = ttk.Scrollbar(device_frame, orient="vertical", command=self.device_tree.yview) self.device_tree.configure(yscrollcommand=scrollbar.set) # 布局 self.device_tree.pack(side="left", fill="both", expand=True, padx=(0, 5)) scrollbar.pack(side="right", fill="y") # 设备操作按钮 btn_frame = ttk.Frame(device_frame) btn_frame.pack(side="right", fill="y", padx=5) ttk.Button(btn_frame, text="刷新设备", command=self.refresh_devices).pack(pady=5) ttk.Button(btn_frame, text="全选", command=self.select_all).pack(pady=5) ttk.Button(btn_frame, text="取消全选", command=self.deselect_all).pack(pady=5) # 测试控制面板 control_frame = ttk.LabelFrame(self, text="测试控制") control_frame.pack(fill="x", padx=15, pady=10) # 测试类型选择 test_type_frame = ttk.Frame(control_frame) test_type_frame.pack(fill="x", pady=5) self.test_type = tk.StringVar(value="full") ttk.Radiobutton(test_type_frame, text="完整测试", variable=self.test_type, value="full").pack(side="left", padx=10) ttk.Radiobutton(test_type_frame, text="单模块测试", variable=self.test_type, value="module").pack(side="left", padx=10) ttk.Radiobutton(test_type_frame, text="重测失败", variable=self.test_type, value="retry").pack(side="left", padx=10) # 测试参数输入 param_frame = ttk.Frame(control_frame) param_frame.pack(fill="x", pady=5) ttk.Label(param_frame, text="模块:").pack(side="left", padx=(5,0)) self.module_var = tk.StringVar() ttk.Entry(param_frame, textvariable=self.module_var, width=25).pack(side="left") ttk.Label(param_frame, text="测试项:").pack(side="left", padx=(10,0)) self.test_case_var = tk.StringVar() ttk.Entry(param_frame, textvariable=self.test_case_var, width=25).pack(side="left") ttk.Label(param_frame, text="重试次数:").pack(side="left", padx=(20,5)) self.retry_count_var = tk.IntVar(value=1) ttk.Spinbox(param_frame, from_=1, to=10, width=5, textvariable=self.retry_count_var).pack(side="left") # 执行按钮 ttk.Button(control_frame, text="执行测试", command=self.execute_tests, width=15).pack(pady=10) # 日志显示区域 log_frame = ttk.LabelFrame(self, text="测试日志") log_frame.pack(fill="both", expand=True, padx=15, pady=10) self.log_text = scrolledtext.ScrolledText( log_frame, wrap="word", font=("Consolas", 10), bg="#1e1e1e", fg="#d4d4d4" ) self.log_text.pack(fill="both", expand=True, padx=5, pady=5) # 状态栏 self.status_var = tk.StringVar(value="就绪") status_bar = ttk.Label(self, textvariable=self.status_var, relief="sunken") status_bar.pack(side="bottom", fill="x") def refresh_devices(self): """刷新设备列表""" self.log("刷新设备列表中...", "info") success, message = self.device_manager.refresh_devices() if not success: self.log(f"刷新设备失败: {message}", "error") return # 清空现有列表 for item in self.device_tree.get_children(): self.device_tree.delete(item) # 添加新设备 for device_id, info in self.device_manager.devices.items(): status = "空闲" if info['status'] == 'idle' else "测试中" session = info['session_id'] or "无" last_test = info['last_test'] or "从未测试" item = self.device_tree.insert( "", "end", text=device_id, values=(status, session, last_test), tags=(info['status'],) ) # 设置标签颜色 self.device_tree.tag_configure( 'idle', background='#d9ead3' # 空闲状态绿色 ) self.device_tree.tag_configure( 'testing', background='#fce5cd' # 测试中黄色 ) self.device_tree.tag_configure( 'completed', background='#c9daf8' # 完成状态蓝色 ) self.log(f"找到 {len(self.device_manager.devices)} 台设备", "success") def periodic_refresh(self): """定时刷新设备状态""" if not self.test_running: self.refresh_devices() self.after(5000, self.periodic_refresh) def select_all(self): """全选设备""" for item in self.device_tree.get_children(): self.device_tree.selection_add(item) def deselect_all(self): """取消全选""" self.device_tree.selection_set([]) def execute_tests(self): """执行测试""" selected_items = self.device_tree.selection() if not selected_items: self.log("错误: 请至少选择一个设备", "error") return # 获取选中的设备ID device_ids = [self.device_tree.item(item, "text") for item in selected_items] # 更新设备状态为测试中 for device_id in device_ids: self.device_manager.update_device_status(device_id, "testing") # 刷新设备列表显示 self.refresh_devices() # 根据测试类型执行 test_type = self.test_type.get() self.test_running = True self.status_var.set("测试执行中...") if test_type == "full": self.run_full_test(device_ids) elif test_type == "module": module = self.module_var.get().strip() if not module: self.log("错误: 请输入测试模块名称", "error") return test_case = self.test_case_var.get().strip() self.run_module_test(device_ids, module, test_case) elif test_type == "retry": retry_count = self.retry_count_var.get() self.retry_failed(device_ids, retry_count) def run_full_test(self, device_ids): """执行完整测试""" for device_id in device_ids: thread = threading.Thread( target=self._run_device_test, args=(device_id, "run cts --shard-count 3"), daemon=True ) self.active_threads[device_id] = thread thread.start() self.log(f"设备 {device_id} 开始完整测试", "info") def run_module_test(self, device_ids, module, test_case=None): """执行模块测试""" command = f"run cts -m {module}" if test_case: command += f" -t {test_case}" for device_id in device_ids: thread = threading.Thread( target=self._run_device_test, args=(device_id, command), daemon=True ) self.active_threads[device_id] = thread thread.start() self.log(f"设备 {device_id} 开始测试模块: {module}", "info") def retry_failed(self, device_ids, retry_count): """重试失败用例""" for device_id in device_ids: # 获取设备的上次Session ID session_id = self.device_manager.devices.get(device_id, {}).get('session_id') if not session_id: self.log(f"设备 {device_id} 无可用Session ID,跳过重试", "warning") continue for i in range(retry_count): thread = threading.Thread( target=self._run_device_test, args=(device_id, f"run retry --retry {session_id}"), daemon=True ) self.active_threads[device_id] = thread thread.start() self.log(f"设备 {device_id} 开始第 {i+1}/{retry_count} 次重试 (Session: {session_id})", "info") # 等待当前重试完成 while thread.is_alive(): time.sleep(1) def _run_device_test(self, device_id, command): """在设备上执行测试命令""" try: # 模拟测试执行过程 self.log(f"设备 {device_id}: 开始执行命令: {command}", "info") # 在实际应用中,这里应替换为真正的测试命令执行 # 例如: subprocess.run(f"adb -s {device_id} shell {command}", ...) # 模拟测试过程 for i in range(1, 11): if not self.test_running: break time.sleep(1) progress = i * 10 self.log(f"设备 {device_id}: 测试进度 {progress}%", "info") # 模拟捕获Session ID session_id = f"{device_id[:4]}-{int(time.time())}" self.device_manager.update_device_status(device_id, "completed", session_id) self.log(f"设备 {device_id} 测试完成! Session ID: {session_id}", "success") except Exception as e: self.log(f"设备 {device_id} 测试错误: {str(e)}", "error") self.device_manager.update_device_status(device_id, "idle") finally: # 从活动线程中移除 if device_id in self.active_threads: del self.active_threads[device_id] # 如果没有活动线程,标记测试完成 if not self.active_threads: self.test_running = False self.status_var.set("测试完成") # 刷新设备状态 self.refresh_devices() def log(self, message, level="info"): """添加带颜色编码的日志到文本框""" tag = level self.log_text.configure(state="normal") if level == "error": self.log_text.insert("end", message + "\n", "error") self.log_text.tag_config("error", foreground="#f48771") elif level == "success": self.log_text.insert("end", message + "\n", "success") self.log_text.tag_config("success", foreground="#6a9955") elif level == "warning": self.log_text.insert("end", message + "\n", "warning") self.log_text.tag_config("warning", foreground="#dcdcaa") else: self.log_text.insert("end", message + "\n", "info") self.log_text.tag_config("info", foreground="#d4d4d4") self.log_text.see("end") self.log_text.configure(state="disabled") self.update_idletasks() if __name__ == "__main__": app = GMSTestAssistant() app.mainloop()
07-30
import sys from PyQt5.QtWidgets import QScrollArea, QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, QLineEdit, QLabel, QFileDialog, QRadioButton, QComboBox, QCheckBox, QGroupBox, QListWidget, QProgressBar from PyQt5.QtCore import QUrl, QRegExp, QTimer, Qt from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage from PyQt5.QtGui import QIcon, QIntValidator, QRegExpValidator from PyQt5.QtWebChannel import QWebChannel # 导入 QWebChannel import configparser import os import subprocess from PyQt5.QtWidgets import QMessageBox import json import time import docker import ctypes import logging import tkinter as tk from tkinter import messagebox import threading import psutil import shutil import 文件服务2 from 服务校验 import validate_service # 外部函数 sys.path.append(os.path.dirname(__file__)) # 添加当前文件的目录到路径 # import 连接数据库添加历史倾斜摄影 # 直接 if getattr(sys, 'frozen', False): # 如果是打包后的exe,使用sys.executable base_dir = os.path.dirname(sys.executable) else: # 如果是普通脚本,使用__file__ base_dir = os.path.dirname(os.path.abspath(__file__)) # 将'\'替换为'/' base_dir = base_dir.replace('\\', '/') # 将第一个字母盘符写为大写 base_dir = base_dir[0].upper() + base_dir[1:] print('文件路径',base_dir) class MainWindow(QMainWindow): def __init__(self): super().__init__() self.task_started = True # 标志任务是否开始 self.current_img_index = 0 # 当前处理的图片索引 self.setWindowTitle("实时三维") self.setGeometry(100, 100, 1366, 768) self.setWindowIcon(QIcon('./icon.ico')) self.process = None # 用于存储子进程的引用 check_docker_running() # 检查docker是否运行 self.node_process = None # 用于存储 node 进程的引用 self.start_node_process() # 启动 node 进程 # 初始化变量存储路径和选择值 self.paths = { "project": base_dir, "sensor": "", "1": "", "texture": os.path.join(base_dir, "ModelingScope.kml"), "localtions":'' } self.command_line = "" self.data_source = "影像" self.current_progress_key = None # 主窗口布局 main_layout = QHBoxLayout() # 左侧控件布局 left_layout = QVBoxLayout() # 示例控件 self.start_button = QPushButton("开始任务") # 开始任务按钮点击事件绑定run_command方法 self.start_button.clicked.connect(lambda:self.run_command(self.command_line)) left_layout.addWidget(self.start_button) # 只准点击一次开始任务按钮,点击后状态显示停止任务 # stop_button = QPushButton("停止任务") # stop_button.clicked.connect(lambda:self.stopTask()) # left_layout.addWidget(stop_button) # left_layout.addWidget(QLabel("空闲")) # 项目工程路径 # self.createPathInput(left_layout, "项目工程路径:", "project") stop_button2 = QPushButton("文件传输") stop_button2.clicked.connect(lambda:self.file_transfer()) left_layout.addWidget(stop_button2) # 平台上传 stop_button3 = QPushButton("上传平台") stop_button3.clicked.connect(lambda:self.file_transfer_to_platform()) left_layout.addWidget(stop_button3) # 任务队列路径 self.createPathInput(left_layout, "任务队列路径:", "sensor") # 图片文件路径 self.createPathInput(left_layout, "图片文件路径:", "localtions") # # 相机文件路径 # self.createPathInput_file(left_layout, "相机文件路径(.json):", "1", "json") left_layout.addWidget(QLabel("数据源:")) radiobuttons_layout = QHBoxLayout() radio_image = QRadioButton("影像") # radio_video = QRadioButton("视频") radio_image.setChecked(True) radio_image.toggled.connect(lambda: self.setDataSource("影像", radio_image.isChecked())) # radio_video.toggled.connect(lambda: self.setDataSource("视频", radio_video.isChecked())) radiobuttons_layout.addWidget(radio_image) # radiobuttons_layout.addWidget(radio_video) left_layout.addLayout(radiobuttons_layout) # 建模范围 # self.createPathInput_file(left_layout, "建模范围(.kml):", "texture", "kml") # 经纬度输入部分 # left_layout.addWidget(QLabel("输入建模范围:")) # self.coordinates_layout = QVBoxLayout() # # 添加一个默认的坐标输入 # for _ in range(4): # self.add_coordinate_input() # add_button = QPushButton("+") # add_button.clicked.connect(self.add_coordinate_input) # self.coordinates_layout.addWidget(add_button) # left_layout.addLayout(self.coordinates_layout) # 添加经纬度输入框 # 初始化经纬度输入框布局 self.coordinates_layout = QVBoxLayout() self.input_widgets = [] # 添加四组默认的输入框 for _ in range(4): self.add_coordinate_input() # 添加和删除按钮 button_layout = QHBoxLayout() add_button = QPushButton("+") remove_button = QPushButton("-") add_button.clicked.connect(self.add_coordinate_input) remove_button.clicked.connect(self.remove_coordinate_input) button_layout.addWidget(add_button) button_layout.addWidget(remove_button) # 滚动区域 scroll_area = QScrollArea() scroll_content = QWidget() scroll_content.setLayout(self.coordinates_layout) scroll_area.setWidget(scroll_content) scroll_area.setWidgetResizable(True) scroll_area.setFixedHeight(180) left_layout.addWidget(QLabel("输入建模范围:")) left_layout.addWidget(scroll_area) left_layout.addLayout(button_layout) # 相对航高(米) # left_layout.addWidget(QLabel("相对航高(米):")) # self.elevation_input = QLineEdit("") # left_layout.addWidget(self.elevation_input) # 地面分辨率 # left_layout.addWidget(QLabel("像元大小(um):")) # self.minphotonSize = QLineEdit("") # left_layout.addWidget(self.minphotonSize) # 建模结果文件名 left_layout.addWidget(QLabel("建模结果文件名:")) self.result_file_name = QLineEdit("") left_layout.addWidget(self.result_file_name) self.result_file_name.textChanged.connect(self.update_json) # 建模精度 left_layout.addWidget(QLabel("建模精度:")) self.precision_combo = QComboBox() self.precision_combo.addItem("快速") self.precision_combo.addItem("普通") self.precision_combo.addItem("精细") left_layout.addWidget(self.precision_combo) # 高级设置 advanced_group = QGroupBox("高级设置") advanced_layout = QVBoxLayout() advanced_layout.addWidget(QLabel("图片集大小(张):")) self.tile_size_input = QLineEdit("20") self.tile_size_input.setValidator(QIntValidator()) # 限制输入为整数 advanced_layout.addWidget(self.tile_size_input) advanced_layout.addWidget(QLabel("照片获取等待时间(秒):")) self.interval_input = QLineEdit("10") self.interval_input.setValidator(QIntValidator()) # 限制输入为整数 advanced_layout.addWidget(self.interval_input) advanced_layout.addWidget(QLabel("边飞边建:")) self.checkbox1 = QRadioButton("正射") self.checkbox2 = QRadioButton("倾斜") self.checkbox3 = QRadioButton("全建") self.checkbox1.setChecked(False) self.checkbox2.setChecked(True) self.checkbox3.setChecked(False) # advanced_layout.addWidget(checkbox1) # advanced_layout.addWidget(checkbox2) advanced_layout.addWidget(self.checkbox1) advanced_layout.addWidget(self.checkbox2) advanced_layout.addWidget(self.checkbox3) # 按钮 # Merge_models = QPushButton("合并模型") # connection = 连接数据库添加历史倾斜摄影.create_connection("localhost", "root", "ysxx_0407H123", "product-backsage2") # Merge_models.clicked.connect(lambda:连接数据库添加历史倾斜摄影.query_users(connection)) # advanced_layout.addWidget(Merge_models) advanced_group.setLayout(advanced_layout) left_layout.addWidget(advanced_group) # 实时状态显示 self.status_group = QGroupBox("实时状态") status_layout = QVBoxLayout() self.file_count_label = QLabel("文件数量: 0") self.file_names_list = QListWidget() self.docker_count_label = QLabel("Docker容器数量: 0") # 添加进度条和文字标签 self.progress_stage_label = QLabel("进度阶段: 第一块进度") self.progress_bar = QProgressBar() self.progress_bar.setAlignment(Qt.AlignCenter) self.progress_bar.setRange(0, 100) self.progress_bar.setValue(0) status_layout.addWidget(self.file_count_label) status_layout.addWidget(self.file_names_list) status_layout.addWidget(self.docker_count_label) status_layout.addWidget(self.progress_stage_label) status_layout.addWidget(self.progress_bar) self.status_group.setLayout(status_layout) left_layout.addWidget(self.status_group) # 设置左侧宽度 left_widget = QWidget() left_widget.setLayout(left_layout) left_widget.setFixedWidth(300) # 创建Web引擎视图 self.web_view = QWebEngineView() self.web_view.load(QUrl("http://localhost:3123")) web_channel = QWebChannel(self.web_view.page()) # 创建 WebChannel 实例 self.web_view.page().setWebChannel(web_channel) # 关联 WebChannel self.web_view.page().runJavaScript("setInterval(function(){location.reload()}, 1000);") # 将左侧控件和Web视图添加到主布局 main_layout.addWidget(left_widget) main_layout.addWidget(self.web_view) # 设置主widget central_widget = QWidget() central_widget.setLayout(main_layout) self.setCentralWidget(central_widget) # 定时更新状态 self.timer = QTimer() self.timer.timeout.connect(self.update_status) self.timer.start(1000) # 每秒更新一次状态 # 启动进度条更新循环 self.update_progress_timer = QTimer() self.update_progress_timer.timeout.connect(self.update_progress_bar) self.update_progress_timer.start(1000) # 每秒检查一次进度 def update_status(self): # 更新文件数量和文件名 folder_path = self.paths["sensor"] if os.path.exists(folder_path): files = os.listdir(folder_path) self.file_count_label.setText(f"文件数量: {len(files)}") self.file_names_list.clear() for file_name in files: self.file_names_list.addItem(file_name) else: self.file_count_label.setText("文件数量: 0") self.file_names_list.clear() # 更新Docker容器数量 try: result = subprocess.run( ["docker", "ps", "--filter", "ancestor=opendronemap/odm:gpu", "--format", "{{.ID}}"], capture_output=True, text=True, shell=False, creationflags=subprocess.CREATE_NO_WINDOW # 确保不弹出窗口 ) container_ids = result.stdout.strip().split('\n') if container_ids and container_ids[0] == '': container_ids = [] # 如果结果为空字符串,将其设为空列表 logging.debug(f"Container IDs: {container_ids}") self.docker_count_label.setText(f"Docker容器数量: {len(container_ids)}") except subprocess.CalledProcessError as e: logging.error(f"获取Docker容器信息时出错: {e}") self.docker_count_label.setText("Docker容器数量: 0") except FileNotFoundError as e: logging.error(f"Docker命令未找到: {e}") self.docker_count_label.setText("Docker容器数量: 0") def add_coordinate_input(self): """添加一个新的经纬度输入行""" layout = QHBoxLayout() lng_input = QLineEdit() lat_input = QLineEdit() lng_input.setPlaceholderText("经度") lat_input.setPlaceholderText("纬度") # 使用正则表达式限制输入为正负加小数 validator = QRegExpValidator(QRegExp(r"^-?\d+(\.\d+)?$")) lng_input.setValidator(validator) lat_input.setValidator(validator) layout.addWidget(lng_input) layout.addWidget(lat_input) self.coordinates_layout.addLayout(layout) self.input_widgets.append((lng_input, lat_input)) # 收集经纬度坐标 coordinates = [] for lng_input, lat_input in self.input_widgets: lng = lng_input.text() lat = lat_input.text() print(lng, lat) if lng and lat: coordinates.append(f"{lng},{lat}") # 将所有经纬度坐标合并为一个字符串,用分号分隔 coordinates_str = ";".join(coordinates) print("经纬度",coordinates_str) # 将经纬度加入kml文件 def generate_kml(self): """生成KML内容""" kml_content = f"""<?xml version="1.0" encoding="utf-8"?> <kml xmlns="http://www.opengis.net/kml/2.2"> <Document> <Schema id="Dataset_203sup" name="Dataset_203sup"> <SimpleField name="SmUserID" type="int"/> </Schema> <Folder> <Placemark> <name/> <ExtendedData> <SchemaData schemaUrl="#Dataset_203sup"> <SimpleData name="SmUserID">0</SimpleData> </SchemaData> </ExtendedData> <Polygon> <outerBoundaryIs> <LinearRing> <coordinates> """ coordinates = [] for lng_input, lat_input in self.input_widgets: lng = lng_input.text() lat = lat_input.text() if lng and lat: coordinates.append(f"{lng},{lat}") # 在最后再添加第一个坐标点 if coordinates: coordinates.append(coordinates[0]) kml_content += "\n".join(coordinates) + "\n </coordinates>\n </LinearRing>\n </outerBoundaryIs>\n </Polygon>\n </Placemark>\n </Folder>\n </Document>\n</kml>" # 写入KML文件 kml_path = self.paths["texture"] with open(kml_path, 'w', encoding='utf-8') as kml_file: kml_file.write(kml_content) print("KML文件已写入:", kml_path) def remove_coordinate_input(self): """删除最下面的一个经纬度输入行""" if len(self.input_widgets) > 4: layout = self.coordinates_layout.takeAt(len(self.input_widgets) - 1) for i in range(layout.count()): widget = layout.itemAt(i).widget() if widget: widget.deleteLater() self.input_widgets.pop() else: QMessageBox.warning(self, "提示", "至少需要保留四组坐标。") def start_node_process(self): """启动 node 进程并保存其引用""" self.node_process = subprocess.Popen('node result_server_logs.js', shell=False, creationflags=subprocess.CREATE_NO_WINDOW) def update_json(self): # 获取建模结果文件名 result_filename = self.result_file_name.text() # 创建要写入的 JSON 数据 data = { "name": result_filename } # 写入 JSON 文件 with open('./result/resultName.json', 'w', encoding='utf-8') as json_file: json.dump(data, json_file, ensure_ascii=False, indent=4) def create_config_file(self,): """生成配置文件 config.ini""" config = configparser.ConfigParser() # 裁剪相机文件目录,若裁剪失败,弹窗提示用户重新选择文件 if self.paths["1"] is not None and self.paths["project"] is not None: project_path = self.paths["project"] + "/" if project_path in self.paths["1"]: self.paths["1"] = self.paths["1"].replace(project_path, "") # else: # QMessageBox.warning(self, "提示", "项目路径未包含在相机文件路径中。") else: # QMessageBox.warning(self, "提示", "路径不能为空。") print("路径不能为空。") self.paths["1"] = self.paths["1"].replace(self.paths["project"]+"/", "") config['settings'] = { 'kmlPath': self.paths["texture"], # 建模范围 # 'elevation': self.elevation_input.text(), # 相对航高 'precision': self.precision_combo.currentText(), # 建模精度 'interval': '2.0', # 照片获取等待时间 'imagesnum': self.tile_size_input.text(), # 每多少张图片生成一个文件夹 'imagesFolderPath':self.paths["project"]+'/'+ self.paths["sensor"], # 图像文件夹路径 # 'minphotonSize':self.minphotonSize.text(), # 地面分辨率 'projectPath':self.paths["project"], # 项目工程路径 'taskQueuePath':self.paths["sensor"], # 任务队列路径 'cameraFile':self.paths["1"], # 相机文件路径 'dataSource':self.data_source, # 数据源 'customTileSize':self.tile_size_input.text(), # 自定义瓦片大小 'imagesWaitTime':self.interval_input.text(), # 照片获取等待时间 'DEM':False, # 是否生成DEM 'DSM':False, # 是否生成DSM 'zhengshe':self.checkbox1.isChecked(), # 是否生成正射图 'qingxie':self.checkbox2.isChecked(), # 是否生成倾斜图 'quanjian':self.checkbox3.isChecked(), # 是否生成全建图 'minImagesPerSquare':4 , #建模一块所需数量 'resultNameDir':'result/MODEL/'+self.result_file_name.text(), # 建模结果文件名 "overlapRatio":0.7, #重叠率 "sideOverlapRatio":0.8, #侧重叠率 "localtions":self.paths["localtions"], "centerpointlongitude":114.25, "centerpointlatitude":30.58, "imagesnum":0, "ziplocaltions":self.paths["project"]+'/'+ "ziplocaltions" } with open('config.ini', 'w', encoding='utf-8') as configfile: config.write(configfile) print("配置文件 config.ini 已创建。") def run_command(self, command): for lng_input, lat_input in self.input_widgets: lng = lng_input.text() lat = lat_input.text() if not lng or not lat: QMessageBox.warning(self, "提示", "请填写完整的建模经纬度范围 。") return # 检查输入框是否为空 if not self.paths["sensor"]: QMessageBox.warning(self, "提示", "任务队列路径不能为空。") return self.task_started = True # 设置任务开始标志为 True # # 任务路径不能和之前的相同 # config = configparser.ConfigParser() # try: # with open('config.ini', 'r', encoding='utf-8') as configfile: # config.read_file(configfile) # except Exception as e: # print(f"读取配置文件时出错: {e}") # # return # folder_path = config.get('settings', 'imagesFolderPath', fallback=None) # print(folder_path) # if folder_path == self.paths["project"]+"/"+self.paths["sensor"]: # QMessageBox.warning(self, "提示", "任务队列路径不能重复。") # return # 清空目标文件夹,如果存在 # destination_folder = self.paths["project"]+"/"+self.paths["sensor"] # if os.path.exists(destination_folder): # shutil.rmtree(destination_folder) # if not self.paths["camera_file"]: # QMessageBox.warning(self, "提示", "相机文件路径不能为空。") # return if not self.paths["texture"]: QMessageBox.warning(self, "提示", "建模范围路径不能为空。") return if not self.paths["localtions"]: QMessageBox.warning(self, "提示", "图片文件路径不能为空。") return if not self.result_file_name.text(): QMessageBox.warning(self, "提示", "建模结果文件名不能为空。") return # if not self.elevation_input.text(): # QMessageBox.warning(self, "提示", "相对航高不能为空。") # return # if not self.minphotonSize.text(): # QMessageBox.warning(self, "提示", "像元大小不能为空。") # return if not self.tile_size_input.text(): QMessageBox.warning(self, "提示", "自定义瓦片大小不能为空。") return if not self.interval_input.text(): QMessageBox.warning(self, "提示", "照片获取等待时间不能为空。") return self.create_config_file() # 调用生成配置文件的方法 # 将按钮状态设为不可用 self.start_button.setEnabled(False) # self.start_button.setText("正在处理...") self.start_button.repaint() self.start_button.update() # 调用文件服务每多少张图生成一个文件夹 # 直接调用 main 函数 # 创建一个新的线程来运行文件服务 file_service_thread = threading.Thread(target=文件服务2.main) file_service_thread.start() # 启动线程 self.generate_kml() # 生成KML文件 # 生成配置文件后,调用startBuilding.exe def is_exe_running(exe_name): # 检查是否有指定的exe正在运行 for proc in psutil.process_iter(['pid', 'name']): try: if proc.info['name'] == exe_name: return True except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): pass return False # 先判断是否有startbuilding.exe在运行,如果有则关闭所有docker容器,然后关闭startbuilding.exe,再启动startbuilding.exe if is_exe_running("startBuilding.exe"): # 关闭所有基于"opendronemap/odm:gpu"的容器 self.stop_docker_container("opendronemap/odm:gpu") # 关闭startBuilding.exe self.kill_exe("startBuilding.exe") self.process =subprocess.Popen("startBuilding.exe", shell=False,creationflags=subprocess.CREATE_NO_WINDOW) time.sleep(3) # 等待1秒,等待startBuilding.exe启动完成 # 刷新页面 self.web_view.reload() # 使用 QWebEngineView 的 reload 方法 # print(command) # import subprocess # subprocess.Popen(command, shell=True) def file_transfer(self): # 调用文件传输模块 self.process =subprocess.Popen("地表建模-三维航测数据同步.exe", shell=False,creationflags=subprocess.CREATE_NO_WINDOW) # 平台上传 def file_transfer_to_platform(self): # 调用文件传输模块 self.process =subprocess.Popen("结果保存.exe", shell=False,creationflags=subprocess.CREATE_NO_WINDOW) def kill_exe(self, exe_name): """ 杀死exe进程 :param exe_name:进程名字 :return:无 """ os.system('taskkill /f /t /im '+exe_name)#MESMTPC.exe程序名字 print("杀死进程{}".format(exe_name)) def stopTask(self): """停止任务并关闭占用端口3123的进程,同时关闭Docker容器""" # 创建进度对话框 # 创建一个新的窗口 # 显示关闭提示框 msg_box = QMessageBox() msg_box.setWindowTitle("关闭提示") msg_box.setText("软件正在关闭中,请稍候...") msg_box.setStandardButtons(QMessageBox.NoButton) msg_box.setModal(True) # 模态对话框 msg_box.show() QApplication.processEvents() # 处理事件,确保对话框显示 # 关闭 node 进程 if self.node_process: # 检查 node 进程是否存在 self.node_process.kill() # 终止 node 进程 print("已关闭 node 进程") # 查找使用3123的进程 result = subprocess.run("netstat -ano | findstr :3123", capture_output=True, text=True, shell=False, creationflags=subprocess.CREATE_NO_WINDOW) lines = result.stdout.splitlines() if lines: for line in lines: if line: # 确保不是空行 parts = line.split() pid = parts[-1] # 假设最后一部分是PID if pid.isdigit(): os.system(f"taskkill /PID {pid} /F") # 强制关闭该进程 print(f"已关闭占用端口3123的进程 {pid}") # 停止 startBuilding.exe exe_name = 'startBuilding.exe' self.kill_exe(exe_name) # 循环关闭Docker容器,直到3秒没有关闭为止 stop_attempts = 0 while stop_attempts < 5: self.stop_docker_container("opendronemap/odm:gpu") # 更新进度对话框 # progress_dialog.setLabelText("正在关闭Docker容器...请稍候...") # 等待1秒 time.sleep(1) # 检查容器是否还在运行 result = subprocess.run( ["docker", "ps", "--filter", "ancestor=opendronemap/odm:gpu", "--format", "{{.ID}}"], capture_output=True, text=True ) if not result.stdout.strip(): # 如果没有运行的容器,退出循环 break stop_attempts += 1 print("任务停止完成。") # 任务停止完成后关闭对话框 msg_box.close() # 停止dockers def stop_docker_container(self, image_name): # 获取所有基于image_name的容器ID result = subprocess.run( ["docker", "ps", "-f", f"ancestor={image_name}", "-q"], capture_output=True, text=True ) container_ids = result.stdout.strip().split('\n') # 停止每个容器 for container_id in container_ids: subprocess.run(["docker", "stop", container_id]) # 所有容器都停止后,返回True,否则返回False return not container_ids # def createPathInput(self, layout, label_text, path_key): # """创建一个路径输入组件,包括标签、输入框和选择按钮""" # layout.addWidget(QLabel(label_text)) # path_layout = QHBoxLayout() # path_edit = QLineEdit(self.paths[path_key]) # path_button = QPushButton("...") # path_button.setFixedWidth(30) # path_button.clicked.connect(lambda: self.selectPath(path_edit, path_key)) # path_edit.textChanged.connect(lambda text: self.savePath(text, path_key)) # path_layout.addWidget(path_edit) # path_layout.addWidget(path_button) # layout.addLayout(path_layout) # # 选择文件路径 # def createPathInput_file(self, layout, label_text, path_key,type): # """创建一个路径输入组件,包括标签、输入框和选择按钮""" # layout.addWidget(QLabel(label_text)) # path_layout = QHBoxLayout() # path_edit = QLineEdit(self.paths[path_key]) # path_button = QPushButton("...") # path_button.setFixedWidth(30) # path_button.clicked.connect(lambda: self.setPath(path_edit, path_key,type)) # path_edit.textChanged.connect(lambda text: self.savePath(text, path_key)) # path_layout.addWidget(path_edit) # path_layout.addWidget(path_button) # layout.addLayout(path_layout) # def selectPath(self, path_edit, path_key): # """打开文件对话框,设置路径到输入框""" # folder_path = QFileDialog.getExistingDirectory(self, "选择文件夹") # if folder_path: # path_edit.setText(folder_path) # self.savePath(folder_path, path_key) # # 打开文件对话框,选择文件路径,设置到输入框中,并保存到变量中 # def setPath(self, path_edit, path_key,type): # # file_path, _ = QFileDialog.getOpenFileName(self, "选择文件", "", "All Files (*)") # # 只能选json文件 # if type=="json": # file_path, _ = QFileDialog.getOpenFileName(self, "选择文件", "", "json Files (*.json)") # elif type=="kml": # file_path, _ = QFileDialog.getOpenFileName(self, "选择文件", "", "kml Files (*.kml)") # if file_path: # path_edit.setText(file_path) # self.savePath(file_path, path_key) def createPathInput(self, layout, label_text, path_key): """创建一个路径输入组件,包括标签、输入框和选择按钮""" layout.addWidget(QLabel(label_text)) path_layout = QHBoxLayout() path_edit = QLineEdit(self.paths[path_key]) path_edit.setReadOnly(True) # 设置为只读状态 path_button = QPushButton("...") path_button.setFixedWidth(30) path_button.clicked.connect(lambda: self.selectPath(path_edit, path_key)) path_edit.textChanged.connect(lambda text: self.savePath(text, path_key)) path_layout.addWidget(path_edit) path_layout.addWidget(path_button) layout.addLayout(path_layout) def createPathInput_file(self, layout, label_text, path_key, type): """创建一个路径输入组件,包括标签、输入框和选择按钮""" layout.addWidget(QLabel(label_text)) path_layout = QHBoxLayout() path_edit = QLineEdit(self.paths[path_key]) path_button = QPushButton("...") path_button.setFixedWidth(30) path_button.clicked.connect(lambda: self.setPath(path_edit, path_key, type)) path_edit.textChanged.connect(lambda text: self.savePath(text, path_key)) path_layout.addWidget(path_edit) path_layout.addWidget(path_button) layout.addLayout(path_layout) def selectPath(self, path_edit, path_key): """打开文件夹选择对话框,设置路径到输入框""" folder_path = QFileDialog.getExistingDirectory(self, "选择文件夹", base_dir) base_path = base_dir if folder_path: # 假如path_key是localtions,则不需要判断是否在C:/RealTimeModeling下 if path_key == "localtions": path_edit.setText(folder_path) self.savePath(folder_path, path_key) else: print(folder_path,base_dir,folder_path.startswith(base_dir)) # 检查选择的目录是否在 C:/RealTimeModeling 下 if folder_path.startswith(base_dir): path_edit.setText(folder_path) self.savePath(folder_path, path_key) else: QMessageBox.warning(self, "提示", f"只能选择123 {base_path} 目录下的文件夹。") def setPath(self, path_edit, path_key, type): """打开文件对话框,选择文件路径,设置到输入框中,并保存到变量中""" base_path = base_dir if type == "json": file_path, _ = QFileDialog.getOpenFileName(self, "选择文件", base_dir, "JSON Files (*.json)") elif type == "kml": file_path, _ = QFileDialog.getOpenFileName(self, "选择文件", base_dir, "KML Files (*.kml)") # Check if the selected path starts with the allowed directory if file_path and file_path.startswith(base_dir): path_edit.setText(file_path) self.savePath(file_path, path_key) else: QMessageBox.warning(self, "提示", f"只能选择 {base_path} 目录下的文件夹。") def savePath(self, path, path_key): """保存路径到变量""" self.paths[path_key] = path print(self.paths) self.updateCommand() def updateCommand(self): # 原始命令 docker run -ti --rm -v C:\3d\源文件\皂角林:/datasets --gpus all opendronemap/odm:gpu --project-path /datasets 皂角林 --3d-tiles --pc-rectify --pc-ept --pc-quality low --feature-quality medium --boundary "/datasets/皂角林/boundary.json" --orthophoto-png --orthophoto-cutline line1=":/datasets --gpus all opendronemap/odm:gpu --project-path /datasets " line2= " --3d-tiles --pc-rectify --orthophoto-png --orthophoto-cutline --pc-ept --pc-quality low --feature-quality medium --boundary /datasets" # self.paths["sensor"]去掉包含 self.paths["project"] 的字符串 self.paths["sensor"] = self.paths["sensor"].replace(self.paths["project"]+"/", "") # self.paths["texture"]=self.paths["texture"].replace(self.paths["project"], "") """更新命令行""" # self.command_line = "docker run --rm -ti -v " + self.paths["project"] + ":/data/project -v " + self.paths["sensor"] + ":/data/sensor -v " + self.paths["camera_file"] + ":/data/camera_file -v " + self.paths["texture"] + ":/data/texture -p 3123:3123 zyf/3dmodel:latest " + self.data_source + " " + self.elevation_input.text() + " " + self.precision_combo.currentText() + " " + self.tile_size_input.text() + " " + self.interval_input.text() + " " + " ".join([str(checkbox.isChecked()) for checkbox in [checkbox1, checkbox2, checkbox3]]) print("工程目录", self.paths["project"] , "任务队列", self.paths["sensor"], "相机文件", # self.paths["1"], "建模范围", self.paths["texture"], "数据源", self.data_source , # "相对航高", # self.elevation_input.text(), "建模精度", # self.precision_combo.currentText() , "自定义瓦片大小", self.tile_size_input.text(), "照片获取等待时间", self.interval_input.text() , "是否生成正射", self.checkbox3.isChecked() ) # " ".join([str(checkbox.isChecked()) for checkbox in [self.checkbox1, self.checkbox2, self.checkbox3]])) def setDataSource(self, source, checked): """保存数据源选择""" if checked: self.data_source = source print(self.data_source) def closeEvent(self, event): """重写关闭事件以在退出时停止任务""" self.stopTask() # 调用停止任务的方法 event.accept() # 允许窗口关闭 def update_progress_bar(self): """根据Progress.ini文件更新进度条,支持多个 imgs 阶段处理""" if not self.task_started: return # 如果任务未开始,不更新进度条 config = configparser.ConfigParser() config.read('d:\\实时建模\\实时建模V3\\Progress.ini') # 获取当前要处理的 imgsi current_index = getattr(self, 'current_img_index', 0) current_key = f'imgs{current_index + 1}' # 检查是否有新的 imgsi=1 出现 if 'Progress' in config and current_key in config['Progress'] and config['Progress'][current_key] == '1': print(f"检测到 {current_key}=1,立即进入完成阶段") self.current_img_index += 1 self.progress_stage_label.setText(f"进度阶段: 第{self.current_img_index}块进度") self._start_immediate_complete() return # 如果还没开始自动增长,并且没有检测到任何 imgsi=1 if not hasattr(self, '_auto_grow_started'): self._auto_grow_started = True self._auto_grow_start_time = time.time() self._auto_grow_target = 80 self._auto_grow_duration = 120 # 120秒增长到80% self._auto_grow_start_value = 0 self.progress_bar.setValue(0) self.progress_stage_label.setText(f"进度阶段: 第{self.current_img_index}块进度") # 计算当前时间进度 elapsed = time.time() - self._auto_grow_start_time if elapsed >= self._auto_grow_duration: self.progress_bar.setValue(self._auto_grow_target) return # 线性增长 progress = int((elapsed / self._auto_grow_duration) * self._auto_grow_target) self.progress_bar.setValue(min(progress, self._auto_grow_target)) # 每秒检查一次进度 QTimer.singleShot(1000, self.update_progress_bar) def _start_immediate_complete(self): """处理 imgsi=1 的情况:5秒内增长到100%,5秒后归零""" start_value = self.progress_bar.value() target_value = 100 duration = 5000 # 5秒 steps = 50 # 每次更新间隔 delta = (target_value - start_value) / (duration / steps) def update(): current = self.progress_bar.value() if current >= target_value: QTimer.singleShot(5000, self._reset_progress) # 5秒后归零 return # 强制转换为整数 next_value = int(min(current + delta, target_value)) self.progress_bar.setValue(next_value) QTimer.singleShot(steps, update) update() def run_commandss(command): logging.info(f"运行命令: {command}") result = 0 result = subprocess.run(command, shell=True) return result.returncode def check_docker_running(): """检查 Docker 是否正在运行,如果没有运行则显示警告弹窗""" # try: # client = docker.from_env() # 尝试获取 Docker 客户端 # client.ping() # 发送 ping 请求确认 Docker 是否可用 # except docker.errors.DockerException as e: # # 如果发生异常,说明 Docker 没有运行 # show_docker_warning() # 显示 Docker 未运行的警告 # sys.exit() # 退出程序 # except Exception as e: # # 捕获其他类型的异常 # show_docker_warning() # sys.exit() try: # 运行 docker info 命令 result = subprocess.run(['docker', 'info'], capture_output=True, text=True, check=True) # 打印输出信息 print(result.stdout) print("Docker 正常启动") except subprocess.CalledProcessError as e: # 如果命令执行失败,打印错误信息 print("Docker 未正常启动") show_docker_warning() # 显示 Docker 未运行的警告 print(e.stderr) sys.exit() except FileNotFoundError: # 如果 docker 命令未找到,说明 Docker 未安装 print("Docker 未安装或未正确配置") show_docker_warning() # 显示 Docker 未运行的警告 sys.exit() def show_docker_warning(): """显示 Docker 未运行的警告""" QMessageBox.warning(None, "警告", "请打开 Docker 并确保它正常运行!重新开始建模任务!") def start_service(): # 验证许可证 if not validate_service(): return # 如果验证失败,退出服务 # 验证成功,继续运行服务 run_service() def run_service(): # 您的服务主逻辑 # 打开软件时清空结果文件名 # 创建要写入的 JSON 数据 data = { "name": '' } # 写入 JSON 文件 with open('./result/resultName.json', 'w', encoding='utf-8') as json_file: json.dump(data, json_file, ensure_ascii=False, indent=4) ctypes.windll.user32.ShowWindow(ctypes.windll.kernel32.GetConsoleWindow(), 0) # 运行一个命令node result_server_logs.js # subprocess.Popen('node result_server_logs.js', shell=False,creationflags=subprocess.CREATE_NO_WINDOW) app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec_()) if __name__ == "__main__": start_service()
最新发布
08-28
import os import subprocess import shutil import time import tkinter as tk from tkinter import filedialog, ttk, scrolledtext, messagebox, PhotoImage import win32com.client as win32 import threading import tempfile import queue import traceback class DiffProcessorApp: def __init__(self, root): self.root = root root.title("高级文件夹比较工具") root.geometry("1000x700") root.configure(bg="#f5f5f5") # 创建现代风格主题 self.style = ttk.Style() self.style.theme_use('clam') # 自定义主题颜色 self.style.configure('TButton', font=('Segoe UI', 10, 'bold'), borderwidth=1, foreground="#333", background="#4CAF50", bordercolor="#388E3C", relief="flat", padding=8, anchor="center") self.style.map('TButton', background=[('active', '#388E3C'), ('disabled', '#BDBDBD')], foreground=[('disabled', '#9E9E9E')]) self.style.configure('TLabel', font=('Segoe UI', 9), background="#f5f5f5") self.style.configure('TLabelframe', font=('Segoe UI', 10, 'bold'), background="#f5f5f5", relief="flat", borderwidth=2) self.style.configure('TLabelframe.Label', font=('Segoe UI', 10, 'bold'), background="#f5f5f5", foreground="#2E7D32") self.style.configure('Treeview', font=('Segoe UI', 9), rowheight=25) self.style.configure('Treeview.Heading', font=('Segoe UI', 9, 'bold')) # 创建主框架 main_frame = ttk.Frame(root, padding="15") main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 标题区域 header_frame = ttk.Frame(main_frame) header_frame.pack(fill=tk.X, pady=(0, 15)) # 添加标题图标 try: icon = PhotoImage(file="folder_icon.png") self.icon_label = ttk.Label(header_frame, image=icon) self.icon_label.image = icon self.icon_label.pack(side=tk.LEFT, padx=(0, 10)) except: self.icon_label = ttk.Label(header_frame, text="📁", font=("Arial", 24)) self.icon_label.pack(side=tk.LEFT, padx=(0, 10)) title_label = ttk.Label(header_frame, text="高级文件夹比较工具", font=("Segoe UI", 18, "bold"), foreground="#2E7D32") title_label.pack(side=tk.LEFT) # 文件选择区域 file_frame = ttk.LabelFrame(main_frame, text="文件夹选择", padding="12") file_frame.pack(fill=tk.X, pady=5) # 文件夹选择 self.old_folder_entry, self.new_folder_entry = self.create_folder_selector(file_frame, "原始文件夹:") self.new_folder_entry = self.create_folder_selector(file_frame, "修改后文件夹:")[0] # 比较选项区域 options_frame = ttk.LabelFrame(main_frame, text="比较选项", padding="12") options_frame.pack(fill=tk.X, pady=5) # 递归比较选项 self.recursive_var = tk.BooleanVar(value=True) recursive_check = ttk.Checkbutton(options_frame, text="递归比较子文件夹", variable=self.recursive_var) recursive_check.grid(row=0, column=0, padx=10, pady=5, sticky=tk.W) # 文件过滤 filter_frame = ttk.Frame(options_frame) filter_frame.grid(row=0, column=1, padx=10, pady=5, sticky=tk.W) ttk.Label(filter_frame, text="文件过滤:").pack(side=tk.LEFT, padx=(0, 5)) self.filter_var = tk.StringVar(value="*.*") filter_entry = ttk.Entry(filter_frame, textvariable=self.filter_var, width=15) filter_entry.pack(side=tk.LEFT) # 目标Excel选择 excel_frame = ttk.LabelFrame(main_frame, text="输出设置", padding="12") excel_frame.pack(fill=tk.X, pady=5) ttk.Label(excel_frame, text="目标Excel文件:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5) self.excel_file_entry = ttk.Entry(excel_frame, width=60) self.excel_file_entry.grid(row=0, column=1, padx=5, pady=5) ttk.Button(excel_frame, text="浏览...", command=lambda: self.select_file(self.excel_file_entry, [("Excel文件", "*.xlsx *.xlsm")])).grid(row=0, column=2, padx=5, pady=5) # 执行按钮区域 button_frame = ttk.Frame(main_frame) button_frame.pack(fill=tk.X, pady=10) self.run_button = ttk.Button(button_frame, text="执行比较", command=self.start_processing, width=20, style='TButton') self.run_button.pack(side=tk.LEFT) # 停止按钮 self.stop_button = ttk.Button(button_frame, text="停止", command=self.stop_processing, width=10, state=tk.DISABLED) self.stop_button.pack(side=tk.LEFT, padx=10) # 进度条 self.progress = ttk.Progressbar(main_frame, orient=tk.HORIZONTAL, length=700, mode='determinate') self.progress.pack(fill=tk.X, pady=5) # 状态信息 status_frame = ttk.Frame(main_frame) status_frame.pack(fill=tk.X, pady=5) self.status_var = tk.StringVar(value="准备就绪") status_label = ttk.Label(status_frame, textvariable=self.status_var, font=("Segoe UI", 9), foreground="#2E7D32") status_label.pack(side=tk.LEFT) # 日志和预览区域 notebook = ttk.Notebook(main_frame) notebook.pack(fill=tk.BOTH, expand=True, pady=5) # 文件夹结构标签 tree_frame = ttk.Frame(notebook, padding="5") notebook.add(tree_frame, text="文件夹结构") # 创建树形视图 self.tree = ttk.Treeview(tree_frame, columns=("Status"), show="tree") self.tree.heading("#0", text="文件夹结构", anchor=tk.W) self.tree.heading("Status", text="状态", anchor=tk.W) self.tree.column("#0", width=400) self.tree.column("Status", width=100) vsb = ttk.Scrollbar(tree_frame, orient="vertical", command=self.tree.yview) hsb = ttk.Scrollbar(tree_frame, orient="horizontal", command=self.tree.xview) self.tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set) self.tree.grid(row=0, column=0, sticky="nsew") vsb.grid(row=0, column=1, sticky="ns") hsb.grid(row=1, column=0, sticky="ew") # 日志标签 log_frame = ttk.Frame(notebook, padding="5") notebook.add(log_frame, text="执行日志") self.log_text = scrolledtext.ScrolledText(log_frame, height=10, wrap=tk.WORD, font=("Consolas", 9)) self.log_text.pack(fill=tk.BOTH, expand=True) self.log_text.config(state=tk.DISABLED) # 设置网格权重 tree_frame.grid_rowconfigure(0, weight=1) tree_frame.grid_columnconfigure(0, weight=1) # 线程控制 self.processing = False self.queue = queue.Queue() # 启动队列处理 self.root.after(100, self.process_queue) def create_folder_selector(self, parent, label_text): """创建文件夹选择器组件""" frame = ttk.Frame(parent) frame.pack(fill=tk.X, pady=5) ttk.Label(frame, text=label_text).grid(row=0, column=0, sticky=tk.W, padx=5, pady=5) entry = ttk.Entry(frame, width=70) entry.grid(row=0, column=1, padx=5, pady=5) button = ttk.Button(frame, text="浏览文件夹...", command=lambda: self.select_folder(entry)) button.grid(row=0, column=2, padx=5, pady=5) return entry, button def select_folder(self, entry): """选择文件夹""" foldername = filedialog.askdirectory() if foldername: entry.delete(0, tk.END) entry.insert(0, foldername) # 自动填充文件夹结构 self.populate_folder_tree(foldername) def select_file(self, entry, filetypes=None): """选择文件""" if filetypes is None: filetypes = [("所有文件", "*.*")] filename = filedialog.askopenfilename(filetypes=filetypes) if filename: entry.delete(0, tk.END) entry.insert(0, filename) def populate_folder_tree(self, path): """填充文件夹结构树""" self.tree.delete(*self.tree.get_children()) if not os.path.isdir(path): return # 添加根节点 root_node = self.tree.insert("", "end", text=os.path.basename(path), values=("文件夹",), open=True) self.add_tree_nodes(root_node, path) def add_tree_nodes(self, parent, path): """递归添加树节点""" try: for item in os.listdir(path): item_path = os.path.join(path, item) if os.path.isdir(item_path): node = self.tree.insert(parent, "end", text=item, values=("文件夹",)) self.add_tree_nodes(node, item_path) else: self.tree.insert(parent, "end", text=item, values=("文件",)) except PermissionError: self.log_message(f"权限错误: 无法访问 {path}") def log_message(self, message): """记录日志消息""" self.queue.put(("log", message)) def update_progress(self, value): """更新进度条""" self.queue.put(("progress", value)) def update_status(self, message): """更新状态信息""" self.queue.put(("status", message)) def process_queue(self): """处理线程队列中的消息""" try: while not self.queue.empty(): msg_type, data = self.queue.get_nowait() if msg_type == "log": self.log_text.config(state=tk.NORMAL) self.log_text.insert(tk.END, data + "\n") self.log_text.see(tk.END) self.log_text.config(state=tk.DISABLED) elif msg_type == "progress": self.progress['value'] = data elif msg_type == "status": self.status_var.set(data) except queue.Empty: pass self.root.after(100, self.process_queue) def process_folders(self, old_path, new_path, excel_file): """处理文件夹比较的线程函数 - 简化流程:生成HTML后直接触发按钮""" output_html = None try: # 步骤1: 生成HTML差异文件 self.update_status("生成HTML差异文件...") self.update_progress(30) # 使用临时文件存储HTML报告 with tempfile.NamedTemporaryFile(suffix=".html", delete=False) as temp_file: output_html = temp_file.name if not self.run_winmerge(old_path, new_path, output_html): self.update_status("WinMerge执行失败") return # 步骤2: 将HTML文件与Excel放在同一目录 self.update_status("复制HTML报告到目标目录...") self.update_progress(60) excel_dir = os.path.dirname(excel_file) if excel_dir: target_html = os.path.join(excel_dir, "diff_report.html") try: shutil.copy(output_html, target_html) self.log_message(f"已将HTML文件复制到: {target_html}") except Exception as e: self.log_message(f"文件复制失败: {str(e)}") # 即使复制失败,我们仍然尝试触发按钮 # 步骤3: 直接触发目标Excel中的"作成"按钮 self.update_status("触发Excel按钮...") self.update_progress(80) if not self.trigger_excel_button(excel_file): self.update_status("触发按钮失败") return # 完成 self.update_progress(100) self.update_status("处理完成!") self.log_message("文件夹比较流程执行完毕") messagebox.showinfo("完成", "已生成HTML报告并触发Excel处理") except Exception as e: error_msg = f"执行过程中发生错误: {str(e)}\n{traceback.format_exc()}" self.log_message(error_msg) self.update_status("执行失败") messagebox.showerror("错误", f"处理失败: {str(e)}") finally: # 重新启用执行按钮 if self.processing: self.stop_processing() # 清理临时文件 if output_html and os.path.exists(output_html): try: os.remove(output_html) except: pass def start_processing(self): """启动处理线程 - 修复无响应问题""" if self.processing: self.log_message("警告: 处理正在进行中") return # 获取路径 old_path = self.old_folder_entry.get() new_path = self.new_folder_entry.get() excel_file = self.excel_file_entry.get() # 详细路径验证 validation_errors = [] if not old_path: validation_errors.append("原始文件夹路径为空") elif not os.path.isdir(old_path): validation_errors.append(f"原始文件夹路径无效: {old_path}") if not new_path: validation_errors.append("新文件夹路径为空") elif not os.path.isdir(new_path): validation_errors.append(f"新文件夹路径无效: {new_path}") if not excel_file: validation_errors.append("Excel文件路径为空") elif not excel_file.lower().endswith(('.xlsx', '.xlsm')): validation_errors.append("Excel文件必须是.xlsx或.xlsm格式") if validation_errors: self.log_message("错误: " + "; ".join(validation_errors)) messagebox.showerror("输入错误", "\n".join(validation_errors)) return # 检查WinMerge安装 winmerge_path = r"E:\App\WinMerge\WinMerge2.16.12.0\WinMergeU.exe" if not os.path.exists(winmerge_path): self.log_message(f"错误: WinMerge未安装在默认位置 {winmerge_path}") messagebox.showwarning("WinMerge未安装", "请确保WinMerge已安装或更新路径配置") return # 禁用执行按钮,启用停止按钮 self.run_button.config(state=tk.DISABLED) self.stop_button.config(state=tk.NORMAL) self.processing = True # 启动处理线程 thread = threading.Thread(target=self.process_folders, args=(old_path, new_path, excel_file)) thread.daemon = True thread.start() self.log_message("处理线程已启动") def run_winmerge(self, path1, path2, output_html): """调用WinMerge生成HTML差异文件""" winmerge_path = r"E:\App\WinMerge\WinMerge2.16.12.0\WinMergeU.exe" # 验证WinMerge可执行文件 if not os.path.exists(winmerge_path): self.log_message(f"错误: WinMerge路径不存在 {winmerge_path}") return False winmerge_cmd = [ winmerge_path, '/u', '/dl', 'Base', '/dr', 'Modified', '/or', output_html, path1, path2 ] # 添加递归选项 if self.recursive_var.get(): winmerge_cmd.insert(1, '/r') # 添加文件过滤 file_filter = self.filter_var.get() if file_filter and file_filter != "*.*": winmerge_cmd.extend(['-f', file_filter]) self.log_message(f"执行WinMerge命令: {' '.join(winmerge_cmd)}") try: result = subprocess.run( winmerge_cmd, capture_output=True, text=True, timeout=120, creationflags=subprocess.CREATE_NO_WINDOW ) if result.returncode == 0: self.log_message(f"HTML差异报告生成完成: {output_html}") return True else: error_msg = f"WinMerge执行失败(退出码{result.returncode}): {result.stderr}" self.log_message(error_msg) return False except subprocess.TimeoutExpired: self.log_message("WinMerge执行超时(120秒),请检查输入文件大小") return False except Exception as e: self.log_message(f"WinMerge执行错误: {str(e)}") return False def trigger_excel_button(self, excel_path): """触发目标Excel文件中的按钮""" self.log_message("正在打开Excel并触发按钮...") excel = None workbook = None try: # 验证Excel文件存在 if not os.path.exists(excel_path): self.log_message(f"错误: Excel文件不存在 {excel_path}") return False # 使用win32com打开Excel excel = win32.gencache.EnsureDispatch('Excel.Application') excel.Visible = True excel.DisplayAlerts = False # 打开工作簿 try: workbook = excel.Workbooks.Open(os.path.abspath(excel_path)) except Exception as e: self.log_message(f"打开Excel文件失败: {str(e)}") return False # 检查工作表是否存在 sheet_names = [sheet.Name for sheet in workbook.Sheets] if "一覧" not in sheet_names: self.log_message("错误: Excel文件中缺少'一覧'工作表") return False sheet = workbook.Sheets("一覧") # 触发"作成"按钮 self.log_message("正在触发'作成'按钮...") try: # 查找按钮并点击 button = sheet.Buttons("作成") button.OnAction = "作成按钮的处理" button.Click() self.log_message("已触发'作成'按钮") # 等待处理完成 self.update_status("处理中...请等待") wait_time = 0 max_wait = 60 # 最大等待60秒 while self.processing and wait_time < max_wait: if excel.CalculationState == 0: # 0 = xlDone break time.sleep(1) wait_time += 1 self.log_message(f"处理中...({wait_time}秒)") if wait_time >= max_wait: self.log_message("警告: 处理超时") else: self.log_message("处理完成") return True except Exception as e: self.log_message(f"按钮操作失败: {str(e)}. 请手动点击'作成'按钮") return False except Exception as e: self.log_message(f"Excel操作失败: {str(e)}\n{traceback.format_exc()}") return False finally: # 确保正确关闭Excel try: if workbook: workbook.Close(SaveChanges=True) # 保存更改 if excel: excel.Quit() except Exception as e: self.log_message(f"关闭Excel时出错: {str(e)}") def stop_processing(self): """停止处理""" self.processing = False self.stop_button.config(state=tk.DISABLED) self.run_button.config(state=tk.NORMAL) self.update_status("操作已停止") if __name__ == "__main__": root = tk.Tk() app = DiffProcessorApp(root) root.mainloop() 这是我的代码,我希望完成的操作是 1、用WinMerge生成文件夹比较报告及文件比较报告 2、将生成的报告与目标Excel文件放在同一目录下 3、点击目标Excel文件“一覧”工作表上的“作成”按钮 4、等待处理完成
07-11
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值