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

MFC对话框空闲处理与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 simpledialog, messagebox, scrolledtext, ttk import time import threading import json import os import platform from cryptography.fernet import Fernet from pynput import mouse, keyboard import sys import subprocess import csv import pandas as pd # 配置参数(常量使用大写) IDLE_THRESHOLD = 3 * 60 # 3分钟空闲时间(秒) COUNTDOWN_DURATION = 10 # 锁屏倒计时(秒) KEY_FILE = "system_secret.key" # 加密密钥文件名 LOG_FILE = "secure_usage_log.log" # 操作日志文件路径 ADMIN_PASSWORD = "123" # 管理员密码 LOCK_SCREEN_BG = "#000000" # 锁屏背景色(黑色) LOGIN_BG = "#2c3e50" # 登录界面背景色(深蓝色) BUTTON_BG = "#3498db" # 按钮背景色(亮蓝色) USER_FILE = r"C:\Users\x00708\PycharmProjects\mission\practice_from_work_pycharm\lock monitor\培训通过人员名单.csv" # 用户名单文件路径 USER_MANAGEMENT_PASSWORD = ADMIN_PASSWORD # 用户管理密码设置为管理员密码 # 培训通过人员名单(工号:密码) # 修改全局用户加载逻辑 def load_trained_users(file_path): """ 从CSV文件加载培训通过人员名单 文件格式要求:工号,密码 返回字典 {工号: 密码} """ users = {} try: # 检查文件是否存在 if not os.path.exists(file_path): print(f"用户文件 {file_path} 不存在,创建初始管理员账号") # 创建只包含管理员的初始用户名单 users = {"ADMIN": ADMIN_PASSWORD} # 保存加密的用户名单 save_user_file(users, file_path) return users # 加载加密密钥 if not os.path.exists(KEY_FILE): key = Fernet.generate_key() with open(KEY_FILE, "wb") as f: f.write(key) with open(KEY_FILE, "rb") as f: key = f.read() cipher = Fernet(key) # 解密用户数据 with open(file_path, "rb") as f: encrypted_data = f.read() decrypted_data = cipher.decrypt(encrypted_data) return json.loads(decrypted_data) except Exception as e: print(f"加载用户名单错误: {str(e)}") # 返回只包含管理员的用户名单 return {"ADMIN": ADMIN_PASSWORD} # 添加保存用户文件的函数 def save_user_file(users, file_path): """保存用户字典到CSV文件""" try: # 确保密钥存在 if not os.path.exists(KEY_FILE): key = Fernet.generate_key() with open(KEY_FILE, "wb") as f: f.write(key) with open(KEY_FILE, "rb") as f: key = f.read() cipher = Fernet(key) # 加密用户数据 encrypted_data = cipher.encrypt(json.dumps(users).encode()) with open(file_path, "wb") as f: f.write(encrypted_data) return True except Exception as e: print(f"保存用户文件时出错: {str(e)}") return False # 从文件加载培训通过人员名单 TRAINED_USERS = load_trained_users(USER_FILE) # 确保管理员账户存在 if "ADMIN" not in TRAINED_USERS: TRAINED_USERS["ADMIN"] = ADMIN_PASSWORD save_user_file(TRAINED_USERS, USER_FILE) class SecureDesktopMonitor: def __init__(self): # 创建主窗口 self.root = tk.Tk() # 创建主窗口对象 self.root.title("安全桌面监控系统") # 设置窗口标题 self.root.geometry("800x600") # 初始窗口尺寸 self.root.configure(bg=LOGIN_BG) # 设置背景色 # 设置窗口在最顶层 self.root.attributes("-topmost", True) # 系统状态变量,防止用户绕过监控 self.current_user = None # 当前登录用户(未登录时为None) self.login_time = 0 # 登陆时间(初始为0) self.last_activity = time.time() # 最后活动时间(初始化为当前时间) self.idle_timer = None # 空闲检测定时器(用于推迟锁屏) self.countdown_timer = None # 倒计时定时器(锁屏前提示) self.listening = False # 监听器状态标志 self.is_locked = False # 界面锁屏状态(初始为True,即未登录时锁定) # 创建鼠标键盘监听器 self.mouse_listener = mouse.Listener(on_move=self.activity_detected) # 鼠标移动触发活动检测 self.keyboard_listener = keyboard.Listener(on_press=self.activity_detected) # 键盘按键触发活动检测 # 显示初始登录界面 self.show_login_screen() # Tkinter事件循环(阻塞式,保持程序运行),mainloop()是Tkinter的核心,用于处理用户事件(如点击、输入) self.root.mainloop() def show_login_screen(self): """显示登录界面""" self.clear_screen() # 清空当前屏幕所有内容 self.is_locked = True # 标记系统为锁定状态 # 设置全屏模式(防止用户切换窗口) self.root.attributes('-fullscreen', True) # 创建居中主框架 main_frame = tk.Frame(self.root, bg=LOGIN_BG) # bg颜色深蓝色 main_frame.place(relx=0.5, rely=0.5, anchor=tk.CENTER) # 使用place布局精确居中 # 系统标题标签 title = tk.Label( main_frame, text="安全桌面登录", font=("Arial", 24, "bold"), # 字体族、大小、粗体 fg="white", # 前景色(白色) bg=LOGIN_BG # 背景色与界面一致 ) title.pack(pady=20) # 打包布局,垂直间距20像素 # 登录表单框架(使用grid布局对齐输入框) form_frame = tk.Frame(main_frame, bg=LOGIN_BG) form_frame.pack(pady=20) # 工号输入标签和输入框 tk.Label( form_frame, # 框架格式 text="工号:", font=("黑体", 14), fg="white", bg=LOGIN_BG ).grid(row=0, column=0, padx=10, pady=10, sticky="e") self.id_entry = tk.Entry(form_frame, font=("Arial", 14), width=20) # 单行文本输入框 self.id_entry.grid(row=0, column=1, padx=10, pady=10) self.id_entry.focus_set() # 自动聚焦到工号输入框(提升用户体验) # 密码输入标签和输入框(内容显示为*号) tk.Label( form_frame, text="密码:", font=("黑体", 14), fg="white", bg=LOGIN_BG ).grid(row=1, column=0, padx=10, pady=10, sticky="e") self.pw_entry = tk.Entry(form_frame, show="*", font=("Arial", 14), width=20) # show="*" 隐藏输入 self.pw_entry.grid(row=1, column=1, padx=10, pady=10) # 放置位置 # 绑定回车键(用户输入密码后按回车可直接登录) self.pw_entry.bind("<Return>", lambda event: self.authenticate_user()) # 登录按钮 login_btn = tk.Button( form_frame, text="登 录", command=self.authenticate_user, # 点击时触发认证方法 font=("黑体", 18, "bold"), bg=BUTTON_BG, # 按钮背景色 fg="white", # 按钮文字颜色 width=15, height=2 ) login_btn.grid(row=2, column=0, columnspan=2, pady=20) # 放置位置跨两列居中 # 系统信息 sys_info = tk.Label( main_frame, text="用户使用时长监测系统 | 仅限授权人员使用", font=("黑体", 10), fg="#bdc3c7", bg=LOGIN_BG ) sys_info.pack(side=tk.BOTTOM, pady=10) def authenticate_user(self): """验证用户身份(首次登录设置密码)""" user_id = self.id_entry.get().strip() # 获取用户id和密码 password = self.pw_entry.get().strip() # 检查是否是管理员登录且用户名单只有管理员 is_only_admin = len(TRAINED_USERS) == 1 and "ADMIN" in TRAINED_USERS # 检查用户是否存在 if user_id in TRAINED_USERS: # 如果是管理员且用户名单为空,直接进入用户管理界面 if is_only_admin and user_id == "ADMIN": self.current_user = user_id self.login_time = time.time() self.last_activity = self.login_time self.is_locked = False self.manage_users() # 直接进入用户管理 return # 处理首次登录(密码为空) if TRAINED_USERS[user_id] == "": self.set_initial_password(user_id) return # 如果id和密码匹配,则正常启动桌面会话 if TRAINED_USERS[user_id] == password: self.current_user = user_id self.login_time = time.time() self.last_activity = self.login_time self.is_locked = False self.start_desktop_session() # 开始桌面会话函数 return else: messagebox.showerror("访问拒绝", "输入密码有误,请重新输入!") self.pw_entry.delete(0, tk.END) return # 如果用户名单只有管理员,给出特定提示 if is_only_admin: messagebox.showerror("访问拒绝", "当前只有管理员可以登录系统,请使用管理员账号登录") else: messagebox.showerror("访问拒绝", "未授权操作!禁止访问系统!") # 清空密码框 self.pw_entry.delete(0, tk.END) # 新用户首次登陆设置密码 def set_initial_password(self, user_id): """新用户首次登录设置密码""" dialog = tk.Toplevel(self.root) dialog.title("设置初始密码") dialog.geometry("300x200") dialog.transient(self.root) dialog.grab_set() # 窗口居中设置 screen_width = dialog.winfo_screenwidth() screen_height = dialog.winfo_screenheight() x = (screen_width - 300) // 2 y = (screen_height - 200) // 2 dialog.geometry(f"300x200+{x}+{y}") tk.Label(dialog, text=f"欢迎新用户 {user_id}").pack(pady=(10, 0)) tk.Label(dialog, text="请设置您的初始密码:").pack(pady=(10, 0)) password_entry = tk.Entry(dialog, show="*", width=20) password_entry.pack() tk.Label(dialog, text="确认密码:").pack(pady=(10, 0)) confirm_entry = tk.Entry(dialog, show="*", width=20) confirm_entry.pack() def save_password(): password = password_entry.get().strip() confirm = confirm_entry.get().strip() if not password: messagebox.showerror("错误", "密码不能为空", parent=dialog) return if password != confirm: messagebox.showerror("错误", "两次输入的密码不一致", parent=dialog) return # 更新密码 TRAINED_USERS[user_id] = password save_user_file(TRAINED_USERS, USER_FILE) # 完成设置 dialog.destroy() messagebox.showinfo("成功", "密码设置成功!") # 自动登录 self.current_user = user_id self.login_time = time.time() self.last_activity = self.login_time self.is_locked = False self.start_desktop_session() tk.Button(dialog, text="保存", command=save_password).pack(pady=10) def start_desktop_session(self): """开始桌面会话""" self.clear_screen() self.root.attributes('-fullscreen', False) self.root.geometry("400x400") self.root.title(f"使用中 - 用户: {self.current_user}") # 忽略窗口关闭事件 self.root.protocol("WM_DELETE_WINDOW", self.handle_window_close) # 停止可能存在的旧监听器 self.stop_activity_monitoring() # 启动新的监听 self.start_activity_monitoring() # 显示桌面内容 desktop_frame = tk.Frame(self.root) desktop_frame.pack(fill=tk.BOTH, expand=True) # expand可将组件由其势力范围扩大到扩展范围 # 欢迎信息 welcome_msg = tk.Label( desktop_frame, text=f"欢迎, {self.current_user}", font=("Arial", 22, "bold"), pady=40 ) welcome_msg.pack() # 状态信息 status_text = tk.Label( desktop_frame, text="桌面已解锁 - 工作中...", font=("黑体", 14), fg="green" ) status_text.pack(pady=15) # 使用时间显示 self.time_label = tk.Label( desktop_frame, text="使用时间: 00:00", font=("黑体", 12) ) self.time_label.pack(pady=10) # 手动锁定按钮 lock_btn = tk.Button( desktop_frame, text="锁定系统", command=self.lock_system, # 调用锁定系统函数 font=("黑体", 12), bg="#e74c3c", fg="white" ) lock_btn.pack(pady=20) # lock_btn为变量名对象,pack为tkinter的几何布局管理器,可自动排列控件 # ⭐管理员按钮组框架 admin_frame = tk.Frame(desktop_frame) admin_frame.pack(pady=10) # 管理员查看记录按钮(仅管理员可见) if self.current_user == "ADMIN": admin_btn = tk.Button( admin_frame, text="管理员查看记录", command=self.admin_view, font=("黑体", 12), bg="#F5FFFA", fg="#e74c3c" ) admin_btn.pack(side=tk.LEFT, padx=5) # 新增:管理用户名单按钮(仅管理员可见) manage_users_btn = tk.Button( admin_frame, text="管理用户名单", command=self.manage_users, font=("黑体", 12), bg="#F5FFFA", fg="#e74c3c" ) manage_users_btn.pack(side=tk.LEFT, padx=5) # 启动鼠标键盘事件监听 self.start_activity_monitoring() # 鼠标键盘监听函数 # 开始空闲检测 self.start_idle_monitor() # 开始更新使用时间显示 self.update_usage_time() def handle_window_close(self): """处理窗口关闭事件 - 忽略关闭尝试并显示警告""" messagebox.showwarning("操作受限", "请使用\"锁定系统\"按钮退出桌面会话") # 管理员管理培训通过名单⭐ def manage_users(self): """管理员管理用户名单""" # 如果是首次登录(只有管理员),跳过密码验证 if len(TRAINED_USERS) > 1 or "ADMIN" not in TRAINED_USERS: # 验证管理员密码 password = simpledialog.askstring("❗", "请输入管理员密码:", show='*') if password != ADMIN_PASSWORD: messagebox.showerror("认证失败", "密码错误!") return # 如果是首次登录(只有管理员),显示特殊提示 is_only_admin = len(TRAINED_USERS) == 1 and "ADMIN" in TRAINED_USERS self.clear_screen() self.root.title("用户名单管理") self.root.geometry("800x600") # 标题 title = tk.Label( self.root, text="用户名单管理", font=("Arial", 24, "bold"), pady=20, bg=LOGIN_BG, fg="white" ) title.pack() # 如果是首次登录,显示提示信息 if is_only_admin: prompt = tk.Label( self.root, text="首次使用请添加至少一个普通用户账号", font=("黑体", 14), fg="red", pady=10 ) prompt.pack() # 框架容器 container = tk.Frame(self.root) container.pack(fill=tk.BOTH, expand=True, padx=20, pady=10) # both指定容器在X和Y两个方向上填充父容器分配的空间, expand容器会随着父容器的扩大而扩展, pady设置容器的外间距(边距) # 创建Treeview显示用户(只显示工号) columns = ("工号",) # 单元素元组需要加逗号 self.user_tree = ttk.Treeview(container, columns=columns, show="headings", selectmode="browse") # 设置列标题 for col in columns: self.user_tree.heading(col, text=col) self.user_tree.column(col, width=100, anchor=tk.CENTER) # 添加滚动条 scrollbar = ttk.Scrollbar(container, orient=tk.VERTICAL, command=self.user_tree.yview) self.user_tree.configure(yscroll=scrollbar.set) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) self.user_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) # 刷新用户列表 self.refresh_user_list() # 按钮框架 btn_frame = tk.Frame(self.root) btn_frame.pack(pady=20) # 添加用户按钮 add_btn = tk.Button( btn_frame, text="添加用户", command=self.add_user, font=("黑体", 12), bg="#2ecc71", fg="white" ) add_btn.pack(side=tk.LEFT, padx=10) # 删除用户按钮 delete_btn = tk.Button( btn_frame, text="删除用户", command=self.delete_user, font=("黑体", 12), bg="#e74c3c", fg="white" ) delete_btn.pack(side=tk.LEFT, padx=10) # 导出用户名单按钮 export_users_btn = tk.Button( btn_frame, text="导出文件", command=self.export_users_csv, font=("黑体", 12), bg=BUTTON_BG, fg="white" ) export_users_btn.pack(side=tk.LEFT, padx=10) # 返回按钮 back_btn = tk.Button( btn_frame, text="返回桌面", command=self.start_desktop_session, font=("黑体", 12), bg=BUTTON_BG, fg="white" ) back_btn.pack(side=tk.LEFT, padx=10) def refresh_user_list(self): """刷新用户列表显示(只显示工号)""" # 清除现有数据 for item in self.user_tree.get_children(): self.user_tree.delete(item) # 添加用户数据(只显示工号) for user_id in TRAINED_USERS.keys(): self.user_tree.insert("", tk.END, values=(user_id,)) def add_user(self): """添加新用户(只添加工号,密码为空)""" dialog = tk.Toplevel(self.root) dialog.title("添加新用户") dialog.geometry("300x200") dialog.transient(self.root) dialog.grab_set() # 窗口剧中设置 screen_width = dialog.winfo_screenwidth() screen_height = dialog.winfo_screenheight() x = (screen_width - 300) // 2 y = (screen_height - 200) // 2 dialog.geometry(f"300x200+{x}+{y}") # 只要求输入工号(不需要密码) tk.Label(dialog, text="工号:").pack(pady=(10, 0)) id_entry = tk.Entry(dialog, width=20) id_entry.pack() # tk.Label(dialog, text="密码:").pack(pady=(10, 0)) # password_entry = tk.Entry(dialog, show="*", width=20) # password_entry.pack() def save_new_user(): user_id = id_entry.get().strip() if not user_id: messagebox.showerror("错误", "工号不能为空", parent=dialog) return if user_id in TRAINED_USERS: messagebox.showerror("错误", "该工号已存在", parent=dialog) return # 添加到全局用户列表(密码初始化为空字符串) TRAINED_USERS[user_id] = "" # 设置初始密码为空 save_user_file(TRAINED_USERS, USER_FILE) # 刷新显示 self.refresh_user_list() dialog.destroy() messagebox.showinfo("成功", f"用户 {user_id} 添加成功") tk.Button(dialog, text="保存", command=save_new_user).pack(pady=20) def delete_user(self): """删除选中用户""" selected = self.user_tree.selection() if not selected: messagebox.showerror("错误", "请先选择一个用户") return item = selected[0] values = self.user_tree.item(item, "values") user_id = values[0] if user_id == "ADMIN": messagebox.showerror("错误", "不能删除管理员账户") return if messagebox.askyesno("确认", f"确定要删除用户 {user_id} 吗?"): # 从全局用户列表中删除 if user_id in TRAINED_USERS: del TRAINED_USERS[user_id] save_user_file(TRAINED_USERS, USER_FILE) self.refresh_user_list() messagebox.showinfo("成功", f"用户 {user_id} 已删除") def update_usage_time(self): """更新使用时间显示""" if not self.is_locked: # 如果没有锁 usage_seconds = time.time() - self.login_time minutes, seconds = divmod(int(usage_seconds), 60) self.time_label.config(text=f"使用时间: {minutes:02d}:{seconds:02d}") self.root.after(1000, self.update_usage_time) # 1s后更新时间显示 def start_activity_monitoring(self): """启动鼠标键盘事件监听""" # 如果已有监听器在运行,先停止 if self.listening: self.stop_activity_monitoring() # 创建新的监听器实例 self.mouse_listener = mouse.Listener(on_move=self.activity_detected) self.keyboard_listener = keyboard.Listener(on_press=self.activity_detected) # 启动线程 mouse_thread = threading.Thread(target=self.mouse_listener.start) keyboard_thread = threading.Thread(target=self.keyboard_listener.start) mouse_thread.daemon = True keyboard_thread.daemon = True mouse_thread.start() keyboard_thread.start() self.listening = True def stop_activity_monitoring(self): """停止鼠标键盘事件监听""" if self.listening: if self.mouse_listener: self.mouse_listener.stop() if self.keyboard_listener: self.keyboard_listener.stop() self.mouse_listener = None self.keyboard_listener = None self.listening = False def activity_detected(self, *args): """检测到用户活动""" if not self.is_locked: # 系统未被锁定 self.last_activity = time.time() # 更新最后活动时间 # ⭐ 修改点1:确保在倒计时期间检测到活动时重新启动监听 if self.countdown_timer: # 如果倒计时正在进行 self.root.after_cancel(self.countdown_timer) self.countdown_timer = None # 重新启动桌面会话和监听 self.stop_activity_monitoring() # 先停止当前监听 self.clear_screen() self.start_desktop_session() # 这会重新启动监听 def start_idle_monitor(self): """监控空闲状态""" if not self.is_locked: idle_time = time.time() - self.last_activity if idle_time > IDLE_THRESHOLD: # 空闲超时 self.start_lock_countdown() else: # 每秒检查一次 self.idle_timer = self.root.after(1000, self.start_idle_monitor) def start_lock_countdown(self): """开始锁屏倒计时""" self.clear_screen() self.root.configure(bg=LOCK_SCREEN_BG) self.root.attributes('-fullscreen', True) # 倒计时显示 self.countdown = COUNTDOWN_DURATION self.countdown_label = tk.Label( self.root, text=f"系统将在 {self.countdown} 秒后锁定...", font=("Arial", 36, "bold"), fg="red", bg=LOCK_SCREEN_BG ) self.countdown_label.place(relx=0.5, rely=0.4, anchor=tk.CENTER) # 提示信息 prompt = tk.Label( self.root, text="检测到系统空闲,移动鼠标或按键取消锁定", font=("Arial", 20), fg="#3498db", bg=LOCK_SCREEN_BG ) prompt.place(relx=0.5, rely=0.5, anchor=tk.CENTER) # 用户信息 user_info = tk.Label( self.root, text=f"当前用户: {self.current_user}", font=("Arial", 16), fg="white", bg=LOCK_SCREEN_BG ) user_info.place(relx=0.5, rely=0.6, anchor=tk.CENTER) # 开始倒计时 self.update_countdown() def update_countdown(self): """更新倒计时显示""" self.countdown -= 1 self.countdown_label.config(text=f"系统将在 {self.countdown} 秒后锁定...") if self.countdown <= 0: self.lock_system() else: # 每秒检查一次 self.countdown_timer = self.root.after(1000, self.update_countdown) def lock_system(self, manual=False): """锁定系统并保存使用记录""" logout_time = time.time() usage_seconds = logout_time - self.login_time # 格式化使用时间 minutes, seconds = divmod(int(usage_seconds), 60) usage_time = f"{minutes:02d}:{seconds:02d}" # 创建记录 record = { "user_id": self.current_user, "login_time": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(self.login_time)), "logout_time": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(logout_time)), "usage_duration": usage_time } # 加密保存记录 self.save_usage_record(record) # 停止监听和计时器 if self.idle_timer: self.root.after_cancel(self.idle_timer) if self.countdown_timer: self.root.after_cancel(self.countdown_timer) self.stop_activity_monitoring() # 重置用户状态 self.current_user = None self.is_locked = True # 显示登录界面 self.show_login_screen() def save_usage_record(self, record): """加密保存使用记录""" # 生成或加载加密密钥 if not os.path.exists(KEY_FILE): # 检查密钥文件是否存在 key = Fernet.generate_key() # 若不存在(首次运行),Fernet.generate_key()生成一个新的对称加密密钥(256位),并以二进制模式写入文件 with open(KEY_FILE, "wb") as f: f.write(key) with open(KEY_FILE, "rb") as f: # 如果已存在,直接读取密钥内容 key = f.read() cipher = Fernet(key) # 创建Fernet加密器实例.Fernet是AES加密的封装库,提供简单易用的对称加密功能 # 读取已有记录,避免新记录覆盖旧数据 records = [] if os.path.exists(LOG_FILE): # 检查日志文件是否存在 with open(LOG_FILE, "rb") as f: # 如果存在,以二进制模式读取加密数据 encrypted_data = f.read() decrypted_data = cipher.decrypt(encrypted_data) # 使用cipher.decrypt()解密 records = json.loads(decrypted_data) # 通过json.loads()将JSON字符串解析为Python列表 # 添加新记录 records.append(record) # 加密保存 encrypted_data = cipher.encrypt(json.dumps(records).encode()) # 将整个记录列表重新加密 with open(LOG_FILE, "wb") as f: f.write(encrypted_data) # 管理员管理使用记录⭐ def admin_view(self): """管理员查看使用记录""" password = simpledialog.askstring("❗", "请输入管理员密码:", show='*') if password == ADMIN_PASSWORD: records = self.get_usage_records() # 获取使用记录函数 self.display_records(records) # 显示使用记录函数 else: messagebox.showerror("认证失败", "密码错误!") # 密码框 def get_usage_records(self): """获取使用记录(解密)""" if not os.path.exists(KEY_FILE) or not os.path.exists(LOG_FILE): return [] try: with open(KEY_FILE, "rb") as f: key = f.read() cipher = Fernet(key) with open(LOG_FILE, "rb") as f: encrypted_data = f.read() decrypted_data = cipher.decrypt(encrypted_data) return json.loads(decrypted_data) except: return [] def display_records(self, records): """显示使用记录""" self.clear_screen() self.root.title("使用记录 - 管理员视图") self.root.geometry("1000x700") # 标题 title = tk.Label( self.root, text="电脑使用记录 - 安全报告", font=("Arial", 24, "bold"), pady=20, bg=LOGIN_BG, fg="white" ) title.pack() # 滚动文本框 text_area = scrolledtext.ScrolledText( self.root, wrap=tk.WORD, font=("Consolas", 12), width=120, height=30 ) text_area.pack(padx=20, pady=3, fill=tk.BOTH, expand=True) # 添加表头 header = "工号 登录时间 登出时间 使用时间\n" text_area.insert(tk.INSERT, header) text_area.insert(tk.INSERT, "-" * 65 + "\n") # 添加记录 for record in records: line = ( f"{record['user_id']:<8} " f"{record['login_time']:<20} " f"{record['logout_time']:<20} " f"{record['usage_duration']:<8}\n" # f"{record.get('lock_type', '自动'):<8}\n" ) text_area.insert(tk.INSERT, line) text_area.configure(state='disabled') # 设为只读 # 添加返回桌面按钮 btn_frame = tk.Frame(self.root) btn_frame.pack(pady=5) # pady参数在垂直方向(上下)添加10像素的填充,确保与其他界面元素有足够的间距 # 导出记录按钮 tk.Button( btn_frame, text="导出文件", command=lambda: self.export_records_csv(records), font=("黑体", 12), bg=BUTTON_BG, fg="white", width=15 ).pack(side=tk.LEFT, padx=10) # 返回按钮 tk.Button( btn_frame, text="返回桌面", command=self.start_desktop_session, font=("黑体", 12), bg=BUTTON_BG, fg="white", width=15 ).pack(side=tk.LEFT, padx=10) # # tk.Button( # btn_frame, # text="返回登录界面", # command=self.show_login_screen, # font=("黑体", 12), # bg=BUTTON_BG, # fg="white", # width=20 # ).pack(side=tk.LEFT, padx=10) # 将使用记录和人员名单导出 def export_records_csv(self, records): """导出使用记录为CSV文件""" try: # 获取当前时间作为文件名 timestamp = time.strftime("%Y%m%d_%H%M%S", time.localtime()) filename = f"使用记录_{timestamp}.csv" with open(filename, 'w', newline='', encoding='utf-8') as file: writer = csv.writer(file) # 写入表头 writer.writerow(["工号", "登录时间", "登出时间", "使用时间"]) # 写入每条记录 for record in records: writer.writerow([ record['user_id'], record['login_time'], record['logout_time'], record['usage_duration'] ]) # ⭐导出为excel表格 # try: # # 获取当前时间作为文件名 # timestamp = time.strftime("%Y%m%d_%H%M%S", time.localtime()) # filename = f"使用记录_{timestamp}.xlsx" # # # 创建DataFrame # data = { # "工号": [record['user_id'] for record in records], # "登录时间": [record['login_time'] for record in records], # "登出时间": [record['logout_time'] for record in records], # "使用时间": [record['usage_duration'] for record in records] # } # df = pd.DataFrame(data) # # # 导出到Excel # df.to_excel(filename, index=False, engine='openpyxl') # messagebox.showinfo("导出成功", f"使用记录已导出到: {os.path.abspath(filename)}") except Exception as e: messagebox.showerror("导出失败", f"导出使用记录时出错: {str(e)}") def export_users_csv(self): """导出用户名单为CSV文件(解密后导出只包含工号)""" try: # 获取当前时间作为文件名 timestamp = time.strftime("%Y%m%d_%H%M%S", time.localtime()) filename = f"用户名单_{timestamp}.csv" with open(filename, 'w', newline='', encoding='utf-8') as file: writer = csv.writer(file) # 写入表头(只包含工号) writer.writerow(["工号"]) # 写入每条记录(只导出工号) for user_id in TRAINED_USERS.keys(): writer.writerow([user_id]) messagebox.showinfo("导出成功", f"用户名单已解密并导出到: {os.path.abspath(filename)}") except Exception as e: messagebox.showerror("导出失败", f"导出用户名单时出错: {str(e)}") """导出excel文件""" # try: # timestamp = time.strftime("%Y%m%d_%H%M%S", time.localtime()) # filename = f"用户名单_{timestamp}.xlsx" # # # 创建dataframe # data = [] # for user_id, password in TRAINED_USERS.items(): # data.append([user_id, password]) # df = pd.DataFrame(data, columns=["工号", "密码"]) # # # 导出到excel # df.to_excel(filename, index=False, engine='openpyxl') def clear_screen(self): """清除当前屏幕所有内容""" for widget in self.root.winfo_children(): widget.destroy() def lock_on_close(self): """关闭窗口时锁定系统""" if self.current_user: self.lock_system(manual=True) self.root.destroy() # 启动系统 if __name__ == "__main__": app = SecureDesktopMonitor() # 绑定窗口关闭事件 app.root.protocol("WM_DELETE_WINDOW", app.lock_on_close)
11-01
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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值