import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
import os
import time
import datetime
import threading
import win32com.client
import win32api
import win32con
import pythoncom
import traceback
import re
import subprocess
import psutil
import json
import logging
import shutil
from logging.handlers import RotatingFileHandler
class PC_DMIS_Automator:
def __init__(self, root):
self.root = root
self.root.title("PC-DMIS 自动化工具 v1.0.7")
self.root.geometry("1100x800")
# 设置日志记录器
self.setup_logger()
# 初始化变量
self.pcdmis_path = tk.StringVar()
self.programs = [] # 存储程序路径和运行次数
self.pcdmis_version = "Unknown"
self.running = False
self.pcdmis_app = None # 用于存储PC-DMIS应用程序实例
self.config = self.load_config() # 加载配置
# 当前编辑的单元格信息
self.editing_cell = None
self.edit_widget = None
# 创建UI
self.create_widgets()
# 检测PC-DMIS
self.detect_pcdmis()
# 设置关闭事件
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
def setup_logger(self):
"""设置日志记录器"""
self.logger = logging.getLogger("PC_DMIS_Automator")
self.logger.setLevel(logging.DEBUG)
# 创建日志目录
log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
if not os.path.exists(log_dir):
os.makedirs(log_dir)
# 文件日志处理器
log_file = os.path.join(log_dir, "pcdmis_automation.log")
file_handler = RotatingFileHandler(
log_file, maxBytes=5*1024*1024, backupCount=3
)
file_handler.setFormatter(logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
))
self.logger.addHandler(file_handler)
# 控制台日志处理器
console_handler = logging.StreamHandler()
console_handler.setFormatter(logging.Formatter(
'%(asctime)s - %(levelname)s - %(message)s'
))
self.logger.addHandler(console_handler)
self.logger.info("PC-DMIS自动化工具启动")
def load_config(self):
"""加载配置文件"""
config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json")
default_config = {
"default_path": "",
"recent_programs": [],
"run_count": 1,
"log_level": "INFO",
"timeout": 7200, # 2小时超时
"auto_save": True,
"auto_report": True,
"auto_backup": True,
"use_excel_report": False,
"excel_template": "",
"email_notifications": False,
"email_recipients": "",
"email_server": ""
}
try:
if os.path.exists(config_path):
with open(config_path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
self.logger.error(f"加载配置文件失败: {str(e)}")
return default_config
def save_config(self):
"""保存配置文件"""
config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json")
try:
self.config["recent_programs"] = [prog[0] for prog in self.programs][:10] # 保存最近10个程序
with open(config_path, 'w', encoding='utf-8') as f:
json.dump(self.config, f, indent=4)
except Exception as e:
self.logger.error(f"保存配置文件失败: {str(e)}")
def create_widgets(self):
# 创建主框架
main_frame = ttk.Frame(self.root, padding="10")
main_frame.pack(fill=tk.BOTH, expand=True)
# PC-DMIS路径设置
path_frame = ttk.LabelFrame(main_frame, text="PC-DMIS设置")
path_frame.pack(fill=tk.X, padx=5, pady=5)
# 版本信息
version_frame = ttk.Frame(path_frame)
version_frame.pack(fill=tk.X, padx=5, pady=2)
ttk.Label(version_frame, text="检测到版本:").pack(side=tk.LEFT, padx=5)
self.version_label = ttk.Label(version_frame, text=self.pcdmis_version, foreground="blue")
self.version_label.pack(side=tk.LEFT, padx=5)
# 状态指示灯
self.status_indicator = tk.Canvas(version_frame, width=20, height=20, bg="gray")
self.status_indicator.pack(side=tk.RIGHT, padx=10)
self.draw_status_indicator("gray")
# 路径选择
path_frame_inner = ttk.Frame(path_frame)
path_frame_inner.pack(fill=tk.X, padx=5, pady=2)
ttk.Label(path_frame_inner, text="PC-DMIS路径:").pack(side=tk.LEFT, padx=5)
path_entry = ttk.Entry(path_frame_inner, textvariable=self.pcdmis_path, width=70)
path_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
ttk.Button(path_frame_inner, text="浏览", command=self.browse_pcdmis).pack(side=tk.LEFT, padx=5)
# 程序文件选择
program_frame = ttk.LabelFrame(main_frame, text="PC-DMIS程序管理")
program_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# 程序列表表格
columns = ("序号", "程序文件", "运行次数", "状态", "X", "↑", "↓")
self.program_tree = ttk.Treeview(
program_frame,
columns=columns,
show="headings",
selectmode="browse",
height=12
)
# 设置列宽
col_widths = [50, 450, 80, 100, 40, 40, 40]
for col, width in zip(columns, col_widths):
self.program_tree.column(col, width=width, anchor=tk.CENTER)
# 设置列标题
for col in columns:
self.program_tree.heading(col, text=col)
# 添加滚动条
scrollbar = ttk.Scrollbar(program_frame, orient=tk.VERTICAL, command=self.program_tree.yview)
self.program_tree.configure(yscroll=scrollbar.set)
self.program_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 添加"+"行用于添加新程序
self.add_plus_row()
# 控制面板 - 垂直排列设置项
settings_frame = ttk.Frame(program_frame)
settings_frame.pack(fill=tk.X, padx=5, pady=5)
# 运行次数编辑 - 第一行
run_count_frame = ttk.Frame(settings_frame)
run_count_frame.pack(fill=tk.X, padx=5, pady=2)
ttk.Label(run_count_frame, text="运行次数:").pack(side=tk.LEFT, padx=5)
self.run_count_var = tk.IntVar(value=self.config.get("run_count", 1))
run_count_spin = ttk.Spinbox(run_count_frame, from_=1, to=100,
textvariable=self.run_count_var, width=5)
run_count_spin.pack(side=tk.LEFT, padx=5)
# 超时设置 - 第二行(在运行次数下方)
timeout_frame = ttk.Frame(settings_frame)
timeout_frame.pack(fill=tk.X, padx=5, pady=2)
ttk.Label(timeout_frame, text="超时时间(分钟):").pack(side=tk.LEFT, padx=5)
self.timeout_var = tk.IntVar(value=self.config.get("timeout", 120) // 60)
timeout_spin = ttk.Spinbox(timeout_frame, from_=1, to=360,
textvariable=self.timeout_var, width=5)
timeout_spin.pack(side=tk.LEFT, padx=5)
# 高级选项 - 第三行
advanced_frame = ttk.Frame(settings_frame)
advanced_frame.pack(fill=tk.X, padx=5, pady=2)
# 自动备份
self.auto_backup_var = tk.BooleanVar(value=self.config.get("auto_backup", True))
backup_check = ttk.Checkbutton(advanced_frame, text="自动备份",
variable=self.auto_backup_var)
backup_check.pack(side=tk.LEFT, padx=5)
# 自动报告
self.auto_report_var = tk.BooleanVar(value=self.config.get("auto_report", True))
report_check = ttk.Checkbutton(advanced_frame, text="生成报告",
variable=self.auto_report_var)
report_check.pack(side=tk.LEFT, padx=5)
# Excel报告
self.excel_report_var = tk.BooleanVar(value=self.config.get("use_excel_report", False))
excel_check = ttk.Checkbutton(advanced_frame, text="使用Excel报告",
variable=self.excel_report_var)
excel_check.pack(side=tk.LEFT, padx=5)
# 日志框
log_frame = ttk.LabelFrame(main_frame, text="运行日志")
log_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.log_text = scrolledtext.ScrolledText(log_frame, wrap=tk.WORD, height=12)
self.log_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.log_text.config(state=tk.DISABLED)
# 添加日志级别标签
log_level_frame = ttk.Frame(log_frame)
log_level_frame.pack(fill=tk.X, padx=5, pady=2)
ttk.Label(log_level_frame, text="日志级别:").pack(side=tk.LEFT, padx=5)
self.log_level = tk.StringVar(value=self.config.get("log_level", "INFO"))
levels = [("详细", "DEBUG"), ("一般", "INFO"), ("警告", "WARNING"), ("错误", "ERROR")]
for text, value in levels:
ttk.Radiobutton(log_level_frame, text=text, variable=self.log_level,
value=value, command=self.update_log_level).pack(side=tk.LEFT, padx=5)
# 进度条
progress_frame = ttk.Frame(main_frame)
progress_frame.pack(fill=tk.X, padx=5, pady=5)
self.progress_label = ttk.Label(progress_frame, text="就绪")
self.progress_label.pack(side=tk.LEFT, padx=5)
self.progress = ttk.Progressbar(progress_frame, orient=tk.HORIZONTAL, length=700, mode='determinate')
self.progress.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
# 按钮
button_frame = ttk.Frame(main_frame)
button_frame.pack(fill=tk.X, padx=5, pady=5)
# 左侧按钮组
left_btn_frame = ttk.Frame(button_frame)
left_btn_frame.pack(side=tk.LEFT, fill=tk.X)
ttk.Button(left_btn_frame, text="清除日志", command=self.clear_log).pack(side=tk.LEFT, padx=5)
ttk.Button(left_btn_frame, text="导出日志", command=self.export_log).pack(side=tk.LEFT, padx=5)
ttk.Button(left_btn_frame, text="保存配置", command=self.save_config).pack(side=tk.LEFT, padx=5)
# 右侧按钮组
right_btn_frame = ttk.Frame(button_frame)
right_btn_frame.pack(side=tk.RIGHT, fill=tk.X)
self.start_button = ttk.Button(right_btn_frame, text="开始执行", command=self.start_execution)
self.start_button.pack(side=tk.RIGHT, padx=5)
self.stop_button = ttk.Button(right_btn_frame, text="停止", command=self.stop_execution, state=tk.DISABLED)
self.stop_button.pack(side=tk.RIGHT, padx=5)
# 绑定事件
self.program_tree.bind("<ButtonRelease-1>", self.on_tree_click)
self.program_tree.bind("<Double-1>", self.on_double_click)
# 状态栏
self.status_bar = ttk.Label(self.root, text="就绪", relief=tk.SUNKEN, anchor=tk.W)
self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)
# 加载最近使用的程序
self.load_recent_programs()
# 创建用于编辑的Spinbox(初始隐藏)
self.spinbox = ttk.Spinbox(self.program_tree, from_=1, to=100, width=5)
self.spinbox.bind("<Return>", self.save_edit)
self.spinbox.bind("<FocusOut>", self.save_edit)
self.spinbox.place_forget() # 初始隐藏
def update_log_level(self):
"""更新日志级别并保存配置"""
self.config["log_level"] = self.log_level.get()
self.save_config()
def load_recent_programs(self):
"""加载最近使用的程序"""
for program in self.config.get("recent_programs", []):
if os.path.exists(program):
self.add_program([program])
def draw_status_indicator(self, color):
"""绘制状态指示灯"""
self.status_indicator.delete("all")
self.status_indicator.create_oval(2, 2, 18, 18, fill=color, outline="black")
def add_plus_row(self):
"""添加'+'行用于添加新程序"""
if "plus_row" not in self.program_tree.get_children():
self.program_tree.insert("", "end", iid="plus_row", values=("+", "", "", "", "", "", ""))
def detect_pcdmis(self):
"""尝试自动检测PC-DMIS安装路径和版本"""
try:
# 尝试通过COM获取已运行的PC-DMIS实例
pythoncom.CoInitialize()
app = win32com.client.GetActiveObject("PC-DMIS.Application")
self.pcdmis_path.set(app.FullName)
self.pcdmis_version = f"{app.Version} (已运行)"
self.version_label.config(text=self.pcdmis_version)
self.draw_status_indicator("green")
self.log_message("INFO: 已连接到正在运行的PC-DMIS实例", "INFO")
return
except Exception as e:
self.log_message(f"DEBUG: 无法通过COM获取PC-DMIS实例: {str(e)}", "DEBUG")
# 尝试从注册表获取安装路径
reg_paths = [
r"SOFTWARE\Hexagon Metrology\PC-DMIS",
r"SOFTWARE\WOW6432Node\Hexagon Metrology\PC-DMIS" # 64位系统
]
for reg_path in reg_paths:
try:
key = win32api.RegOpenKey(win32con.HKEY_LOCAL_MACHINE,
reg_path,
0, win32con.KEY_READ)
path, _ = win32api.RegQueryValueEx(key, "InstallationDirectory")
exe_path = os.path.join(path, "PCDMIS.exe")
if os.path.exists(exe_path):
self.pcdmis_path.set(exe_path)
# 尝试获取版本信息
try:
info = win32api.GetFileVersionInfo(exe_path, "\\")
version = f"{info['FileVersionMS']//65536}.{info['FileVersionMS'] % 65536}.{info['FileVersionLS']//65536}"
self.pcdmis_version = version
self.log_message(f"INFO: 从注册表检测到PC-DMIS: {version}", "INFO")
except Exception as e:
self.pcdmis_version = "检测到 (未知版本)"
self.log_message(f"WARNING: 无法获取版本信息: {str(e)}", "WARNING")
self.version_label.config(text=self.pcdmis_version)
self.draw_status_indicator("yellow")
return
except Exception as e:
self.log_message(f"DEBUG: 无法从注册表获取PC-DMIS路径 ({reg_path}): {str(e)}", "DEBUG")
# 如果所有方法都失败
self.log_message("WARNING: 无法自动检测PC-DMIS安装路径,请手动选择", "WARNING")
self.draw_status_indicator("red")
def browse_pcdmis(self):
path = filedialog.askopenfilename(
title="选择PC-DMIS可执行文件",
filetypes=[("可执行文件", "*.exe"), ("所有文件", "*.*")]
)
if path:
self.pcdmis_path.set(path)
try:
# 尝试获取版本信息
info = win32api.GetFileVersionInfo(path, "\\")
version = f"{info['FileVersionMS']//65536}.{info['FileVersionMS'] % 65536}.{info['FileVersionLS']//65536}"
self.pcdmis_version = version
self.version_label.config(text=self.pcdmis_version)
self.draw_status_indicator("green")
self.log_message(f"INFO: 已选择PC-DMIS: {version}", "INFO")
except Exception as e:
self.pcdmis_version = "用户选择"
self.version_label.config(text=self.pcdmis_version)
self.log_message(f"WARNING: 无法获取版本信息: {str(e)}", "WARNING")
self.draw_status_indicator("yellow")
# 保存配置
self.config["default_path"] = path
self.save_config()
def add_program(self, files=None):
if not files:
files = filedialog.askopenfilenames(
title="选择PC-DMIS程序文件",
filetypes=[("PC-DMIS文件", "*.prg;*.pcf;*.pgm;*.dmis;*.pcm"), ("所有文件", "*.*")]
)
if not files:
return
for file in files:
if any(file == item[0] for item in self.programs):
self.log_message(f"INFO: 程序已存在,跳过: {os.path.basename(file)}", "INFO")
continue # 跳过已存在的文件
self.programs.append((file, self.run_count_var.get()))
row_id = len(self.programs)
self.program_tree.insert("", row_id-1, iid=f"row_{row_id-1}",
values=(row_id, os.path.basename(file),
self.run_count_var.get(), "等待", "X", "↑", "↓"))
self.log_message(f"INFO: 已添加程序: {os.path.basename(file)} (运行次数: {self.run_count_var.get()})", "INFO")
self.add_plus_row()
self.update_row_numbers()
self.save_config()
def on_tree_click(self, event):
item = self.program_tree.identify_row(event.y)
column = self.program_tree.identify_column(event.x)
if item == "plus_row":
self.add_program()
return
if item and item != "plus_row":
try:
row_idx = int(item.split("_")[1])
except (IndexError, ValueError):
return # 无效的 item,跳过
if row_idx < 0 or row_idx >= len(self.programs):
return # 索引超出范围
col_index = int(column[1:]) - 1 # 列索引从0开始
columns = ["序号", "程序文件", "运行次数", "状态", "X", "↑", "↓"]
if col_index == 4: # 删除
self.remove_program(row_idx)
elif col_index == 5 and row_idx > 0: # 上移
self.move_program(row_idx, -1)
elif col_index == 6 and row_idx < len(self.programs) - 1: # 下移
self.move_program(row_idx, 1)
def on_double_click(self, event):
"""处理双击事件,用于编辑运行次数"""
region = self.program_tree.identify("region", event.x, event.y)
if region == "cell":
item = self.program_tree.identify_row(event.y)
column = self.program_tree.identify_column(event.x)
# 只允许编辑运行次数列(第3列)
if column == "#3" and item != "plus_row":
try:
row_idx = int(item.split("_")[1])
except (IndexError, ValueError):
return
# 获取当前值
values = self.program_tree.item(item, "values")
current_value = values[2] # 运行次数在第3列
# 获取单元格位置
bbox = self.program_tree.bbox(item, column)
if bbox:
# 创建并显示Spinbox
self.spinbox.delete(0, tk.END)
self.spinbox.insert(0, current_value)
self.spinbox.place(x=bbox[0], y=bbox[1], width=bbox[2], height=bbox[3])
self.spinbox.focus_set()
# 保存编辑信息
self.editing_cell = (item, row_idx)
def save_edit(self, event):
"""保存编辑的运行次数"""
if not self.editing_cell:
return
item, row_idx = self.editing_cell
new_value = self.spinbox.get()
try:
# 转换为整数
new_count = int(new_value)
if new_count < 1:
new_count = 1
elif new_count > 100:
new_count = 100
# 更新Treeview
values = list(self.program_tree.item(item, "values"))
values[2] = new_count
self.program_tree.item(item, values=values)
# 更新数据存储
if 0 <= row_idx < len(self.programs):
self.programs[row_idx] = (self.programs[row_idx][0], new_count)
self.log_message(f"INFO: 已更新程序运行次数: {os.path.basename(self.programs[row_idx][0])} -> {new_count}次", "INFO")
# 保存配置
self.save_config()
except ValueError:
self.log_message("ERROR: 运行次数必须是整数", "ERROR")
# 隐藏Spinbox并清除编辑状态
self.spinbox.place_forget()
self.editing_cell = None
def remove_program(self, idx):
if 0 <= idx < len(self.programs):
program_name = os.path.basename(self.programs[idx][0])
self.programs.pop(idx)
self.program_tree.delete(f"row_{idx}")
self.add_plus_row()
self.update_row_numbers()
self.log_message(f"INFO: 已移除程序: {program_name}", "INFO")
self.save_config()
def update_row_numbers(self):
"""更新所有行的序号(排除 '+' 行)"""
children = self.program_tree.get_children()
for i, child in enumerate(children):
if child != "plus_row":
values = list(self.program_tree.item(child, "values"))
if values: # 确保有值
values[0] = i # 更新序号
self.program_tree.item(child, values=values)
def move_program(self, idx, direction):
if idx < 0 or idx >= len(self.programs):
return
new_idx = idx + direction
if new_idx < 0 or new_idx >= len(self.programs):
return
# 交换数据
self.programs[idx], self.programs[new_idx] = self.programs[new_idx], self.programs[idx]
# 清除 Treeview 中所有项(保留 "+" 行)
self.delete_all_program_rows()
# 重新生成所有程序行
for i, (file, count) in enumerate(self.programs):
self.program_tree.insert("", i, iid=f"row_{i}",
values=(i+1, os.path.basename(file), count, "等待", "X", "↑", "↓"))
# 重新添加 "+" 行
self.add_plus_row()
program_name = os.path.basename(self.programs[new_idx][0])
direction_str = "上移" if direction < 0 else "下移"
self.log_message(f"INFO: 已将程序 '{program_name}' {direction_str}", "INFO")
self.save_config()
def delete_all_program_rows(self):
"""删除所有程序项(保留 '+' 行)"""
for child in self.program_tree.get_children():
if child != "plus_row":
self.program_tree.delete(child)
def clear_log(self):
"""清除日志内容"""
self.log_text.config(state=tk.NORMAL)
self.log_text.delete(1.0, tk.END)
self.log_text.config(state=tk.DISABLED)
self.log_message("INFO: 日志已清除", "INFO")
def export_log(self):
"""导出日志到文件"""
file_path = filedialog.asksaveasfilename(
title="导出日志文件",
filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")],
defaultextension=".txt"
)
if file_path:
try:
with open(file_path, "w", encoding="utf-8") as f:
f.write(self.log_text.get(1.0, tk.END))
self.log_message(f"INFO: 日志已导出到: {file_path}", "INFO")
except Exception as e:
self.log_message(f"ERROR: 导出日志失败: {str(e)}", "ERROR")
def log_message(self, message, level="INFO"):
"""线程安全的日志更新"""
# 记录到文件日志
log_func = {
"DEBUG": self.logger.debug,
"INFO": self.logger.info,
"WARNING": self.logger.warning,
"ERROR": self.logger.error
}.get(level, self.logger.info)
log_func(message)
# 根据日志级别设置决定是否显示在UI
log_levels = {"DEBUG": 1, "INFO": 2, "WARNING": 3, "ERROR": 4}
current_level = log_levels.get(self.log_level.get(), 2)
msg_level = log_levels.get(level, 2)
if msg_level < current_level:
return
def update_log():
self.log_text.config(state=tk.NORMAL)
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 根据日志级别设置颜色
tag = level.lower()
self.log_text.tag_config(tag, foreground=self.get_log_color(level))
self.log_text.insert(tk.END, f"[{timestamp}] ", "timestamp")
self.log_text.insert(tk.END, f"{level}: ", tag)
self.log_text.insert(tk.END, f"{message}\n")
self.log_text.see(tk.END)
self.log_text.config(state=tk.DISABLED)
# 更新状态栏
self.status_bar.config(text=f"{level}: {message}")
self.root.after(0, update_log)
def get_log_color(self, level):
"""获取日志级别对应的颜色"""
colors = {
"DEBUG": "gray",
"INFO": "black",
"WARNING": "orange",
"ERROR": "red"
}
return colors.get(level, "black")
def update_progress(self, current, total, message):
"""线程安全的进度条更新"""
def update_progress_bar():
self.progress_label.config(text=message)
if total > 0:
progress_value = (current / total) * 100
self.progress['value'] = progress_value
self.root.after(0, update_progress_bar)
def update_program_status(self, row_idx, status, color="black"):
"""更新程序状态"""
def update_status():
children = self.program_tree.get_children()
if row_idx < len(children) and children[row_idx] != "plus_row":
values = list(self.program_tree.item(f"row_{row_idx}", "values"))
if values and len(values) > 3:
values[3] = status # 更新状态列
self.program_tree.item(f"row_{row_idx}", values=values, tags=(color,))
self.program_tree.tag_configure(color, foreground=color)
self.root.after(0, update_status)
def start_execution(self):
if not self.pcdmis_path.get() or not os.path.exists(self.pcdmis_path.get()):
messagebox.showerror("错误", "无效的PC-DMIS路径")
return
if not self.programs:
messagebox.showerror("错误", "请至少添加一个程序文件")
return
# 计算总任务数
total_tasks = sum(count for _, count in self.programs)
if total_tasks == 0:
messagebox.showerror("错误", "总运行次数不能为零")
return
# 更新配置
self.config["run_count"] = self.run_count_var.get()
self.config["timeout"] = self.timeout_var.get() * 60 # 转换为秒
self.config["auto_backup"] = self.auto_backup_var.get()
self.config["auto_report"] = self.auto_report_var.get()
self.config["use_excel_report"] = self.excel_report_var.get()
self.save_config()
# 禁用按钮防止重复点击
self.start_button.config(state=tk.DISABLED)
self.stop_button.config(state=tk.NORMAL)
self.running = True
# 在新线程中执行任务
threading.Thread(
target=self.execute_programs,
args=(total_tasks,),
daemon=True
).start()
def stop_execution(self):
if not self.running:
return
self.running = False
self.log_message("WARNING: 正在停止执行...", "WARNING")
# 尝试停止当前正在运行的程序
if self.pcdmis_app and hasattr(self.pcdmis_app, 'ActiveProgram'):
try:
if self.pcdmis_app.ActiveProgram.IsRunning:
self.pcdmis_app.ActiveProgram.StopExecution()
self.log_message("INFO: 已发送停止命令", "INFO")
except Exception as e:
self.log_message(f"ERROR: 停止命令发送失败: {str(e)}", "ERROR")
def execute_programs(self, total_tasks):
"""执行所有添加的测量程序"""
pythoncom.CoInitialize() # 初始化COM线程
completed_tasks = 0
timeout = self.config.get("timeout", 7200) # 默认2小时超时
try:
self.log_message("INFO: 正在初始化PC-DMIS连接...", "INFO")
# 尝试连接到正在运行的PC-DMIS实例
try:
self.pcdmis_app = win32com.client.GetActiveObject("PC-DMIS.Application")
self.log_message(f"INFO: 已连接到正在运行的PC-DMIS (版本: {self.pcdmis_app.Version})", "INFO")
self.pcdmis_version = self.pcdmis_app.Version
except Exception as e:
self.log_message(f"WARNING: 无法连接到已运行的PC-DMIS实例: {str(e)}", "WARNING")
# 启动新实例
try:
self.log_message("INFO: 正在启动PC-DMIS...", "INFO")
self.pcdmis_app = win32com.client.Dispatch("PC-DMIS.Application")
self.pcdmis_app.Visible = True
self.pcdmis_version = self.pcdmis_app.Version
# 等待应用程序启动
start_time = time.time()
while not self.is_pcdmis_ready():
if time.time() - start_time > 30: # 30秒超时
raise TimeoutError("PC-DMIS启动超时")
time.sleep(1)
self.log_message(f"INFO: PC-DMIS已启动 (版本: {self.pcdmis_version})", "INFO")
except Exception as e:
self.log_message(f"ERROR: 无法启动PC-DMIS: {str(e)}", "ERROR")
self.running = False
self.start_button.config(state=tk.NORMAL)
self.stop_button.config(state=tk.DISABLED)
self.progress['value'] = 0
self.progress_label.config(text="就绪")
return
# 保存当前版本信息
self.version_label.config(text=self.pcdmis_version)
# 执行所有程序
for idx, (program_path, run_count) in enumerate(self.programs):
if not self.running:
break
program_name = os.path.basename(program_path)
base_name, ext = os.path.splitext(program_name)
self.update_program_status(idx, "准备中", "blue")
# 创建结果目录
save_dir = os.path.join(os.path.dirname(program_path), "自动化结果")
if not os.path.exists(save_dir):
os.makedirs(save_dir)
self.log_message(f"INFO: 已创建结果目录: {save_dir}", "INFO")
# 如果是DMIS脚本,使用命令行执行
if program_path.lower().endswith(('.dms', '.dmis')):
self.log_message(f"INFO: 检测到DMIS脚本: {program_name}", "INFO")
for run_idx in range(1, run_count + 1):
if not self.running:
break
completed_tasks += 1
status_msg = f"正在执行DMIS脚本: {program_name} (第{run_idx}/{run_count}次)"
self.update_progress(completed_tasks, total_tasks, status_msg)
self.update_program_status(idx, f"执行中({run_idx}/{run_count})", "orange")
try:
# 执行DMIS脚本
success = self.execute_dmis_script(program_path)
if success:
self.update_program_status(idx, f"完成({run_idx}/{run_count})", "green")
else:
self.update_program_status(idx, f"失败({run_idx}/{run_count})", "red")
except Exception as e:
self.log_message(f"ERROR: 执行DMIS脚本时出错: {str(e)}", "ERROR")
self.log_message(f"DEBUG: {traceback.format_exc()}", "DEBUG")
self.update_program_status(idx, f"错误({run_idx}/{run_count})", "red")
continue
for run_idx in range(1, run_count + 1):
if not self.running:
break
# 更新进度
completed_tasks += 1
status_msg = f"正在处理: {program_name} (第{run_idx}/{run_count}次运行)"
self.update_progress(completed_tasks, total_tasks, status_msg)
self.update_program_status(idx, f"运行中({run_idx}/{run_count})", "orange")
try:
# 备份程序文件
if self.config.get("auto_backup", True):
backup_path = self.backup_program(program_path)
if backup_path:
self.log_message(f"INFO: 程序已备份到: {backup_path}", "INFO")
# 打开程序
self.log_message(f"INFO: 正在打开程序: {program_name} (运行 {run_idx}/{run_count})", "INFO")
# 关闭当前打开的程序(如果有)
if self.pcdmis_app.Programs.Count > 0:
try:
self.pcdmis_app.ActiveProgram.Close(True) # True表示不保存
time.sleep(1)
except Exception as e:
self.log_message(f"WARNING: 关闭当前程序失败: {str(e)}", "WARNING")
# 打开新程序
self.pcdmis_app.OpenProgram(program_path)
# 等待程序加载
start_time = time.time()
while not hasattr(self.pcdmis_app, 'ActiveProgram') or not self.pcdmis_app.ActiveProgram:
if time.time() - start_time > 30: # 30秒超时
raise TimeoutError("程序加载超时")
time.sleep(1)
self.log_message("INFO: 程序已加载", "INFO")
# 执行程序
active_program = self.pcdmis_app.ActiveProgram
self.log_message("INFO: 开始执行测量程序...", "INFO")
active_program.ExecuteProgram()
# 等待执行完成
start_time = time.time()
while active_program.IsExecuting:
if not self.running:
self.log_message("WARNING: 用户中断程序执行", "WARNING")
active_program.StopExecution()
raise Exception("用户中断")
# 检查超时
if time.time() - start_time > timeout:
self.log_message("ERROR: 程序执行超时", "ERROR")
active_program.StopExecution()
raise TimeoutError(f"程序执行超时 ({timeout}秒)")
time.sleep(1)
self.log_message("INFO: 程序执行完成", "INFO")
# 生成检测报告
report_path = None
if self.config.get("auto_report", True):
self.log_message("INFO: 正在生成检测报告...", "INFO")
# 根据版本选择报告类型
if self.config.get("use_excel_report") and self.pcdmis_version >= "2019":
report_path = self.generate_excel_report(active_program)
else:
report_content = self.generate_text_report(active_program)
# 保存文本报告
report_filename = f"{base_name}_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}_{run_idx:03d}.txt"
report_path = os.path.join(save_dir, report_filename)
with open(report_path, "w", encoding="utf-8") as f:
f.write(report_content)
self.log_message(f"INFO: 文本报告已保存: {report_path}", "INFO")
# 保存程序副本
if self.config.get("auto_save", True):
new_filename = f"{base_name}_{datetime.datetime.now().strftime('%Y%m%d')}_{run_idx:03d}{ext}"
new_path = os.path.join(save_dir, new_filename)
self.log_message(f"INFO: 正在保存程序副本为: {new_filename}", "INFO")
active_program.SaveAs(new_path)
# 关闭当前程序
self.log_message("INFO: 关闭当前程序", "INFO")
active_program.Close(False) # False表示不保存(因为我们已保存)
time.sleep(1)
self.update_program_status(idx, f"完成({run_idx}/{run_count})", "green")
except pythoncom.com_error as e:
# 处理COM错误
hr, msg, exc, arg = e.args
self.log_message(f"COM错误: {msg} (错误代码: 0x{hr:X})", "ERROR")
if hasattr(e, 'excepinfo') and len(e.excepinfo) > 5:
self.log_message(f"PC-DMIS错误代码: {e.excepinfo[5]}", "ERROR")
self.log_message(f"DEBUG: {traceback.format_exc()}", "DEBUG")
self.update_program_status(idx, f"COM错误({run_idx}/{run_count})", "red")
except Exception as e:
self.log_message(f"ERROR: 处理程序时出错: {str(e)}", "ERROR")
self.log_message(f"DEBUG: {traceback.format_exc()}", "DEBUG")
self.update_program_status(idx, f"错误({run_idx}/{run_count})", "red")
# 尝试恢复状态
try:
if hasattr(self.pcdmis_app, 'ActiveProgram') and self.pcdmis_app.ActiveProgram:
self.pcdmis_app.ActiveProgram.Close(False)
except:
pass
continue
if self.running:
self.log_message("INFO: 所有任务已完成!", "INFO")
self.update_program_status(idx, "全部完成", "green")
else:
self.log_message("WARNING: 执行被用户中断", "WARNING")
except pythoncom.com_error as e:
hr, msg, exc, arg = e.args
self.log_message(f"严重COM错误: {msg} (错误代码: 0x{hr:X})", "ERROR")
self.log_message(f"DEBUG: {traceback.format_exc()}", "DEBUG")
except Exception as e:
self.log_message(f"ERROR: 严重错误: {str(e)}", "ERROR")
self.log_message(f"DEBUG: {traceback.format_exc()}", "DEBUG")
finally:
# 清理
try:
if self.pcdmis_app:
# 关闭所有打开的程序
while self.pcdmis_app.Programs.Count > 0:
try:
self.pcdmis_app.Programs(1).Close(False)
time.sleep(0.5)
except:
pass
# 如果我们启动的实例,则退出应用程序
if not self.is_pcdmis_running_before():
self.log_message("INFO: 退出PC-DMIS应用程序", "INFO")
self.pcdmis_app.Quit()
except Exception as e:
self.log_message(f"WARNING: 清理过程中出错: {str(e)}", "WARNING")
# 确保释放COM对象
try:
del self.pcdmis_app
self.pcdmis_app = None
except:
pass
pythoncom.CoUninitialize()
# 恢复界面状态
self.running = False
self.start_button.config(state=tk.NORMAL)
self.stop_button.config(state=tk.DISABLED)
self.progress['value'] = 0
self.progress_label.config(text="就绪")
if completed_tasks == total_tasks:
self.status_bar.config(text="所有任务已完成")
elif completed_tasks > 0:
self.status_bar.config(text=f"部分完成: {completed_tasks}/{total_tasks}")
else:
self.status_bar.config(text="任务失败")
self.save_config()
def is_pcdmis_ready(self):
"""检查PC-DMIS是否已准备就绪"""
try:
# 尝试获取版本信息作为就绪检查
_ = self.pcdmis_app.Version
return True
except:
return False
def is_pcdmis_running_before(self):
"""检查PC-DMIS是否在启动前已经在运行"""
try:
# 尝试连接到正在运行的实例
temp_app = win32com.client.GetActiveObject("PC-DMIS.Application")
return True
except:
return False
def backup_program(self, program_path):
"""自动备份程序文件"""
try:
backup_dir = os.path.join(os.path.dirname(program_path), "备份")
os.makedirs(backup_dir, exist_ok=True)
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
backup_name = f"{os.path.basename(program_path)}_{timestamp}"
backup_path = os.path.join(backup_dir, backup_name)
shutil.copy2(program_path, backup_path)
self.log_message(f"INFO: 程序已备份到: {backup_path}", "INFO")
return backup_path
except Exception as e:
self.log_message(f"ERROR: 备份失败: {str(e)}", "ERROR")
return None
def execute_dmis_script(self, script_path):
"""通过命令行执行 DMIS 脚本"""
try:
if not os.path.exists(script_path):
self.log_message(f"ERROR: DMIS 脚本不存在: {script_path}", "ERROR")
return False
cmd = f'"{self.pcdmis_path.get()}" /RUN "{script_path}"'
self.log_message(f"INFO: 执行命令: {cmd}", "INFO")
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=3600)
if result.returncode == 0:
self.log_message(f"INFO: DMIS 脚本执行成功: {script_path}", "INFO")
return True
else:
self.log_message(f"ERROR: DMIS 脚本执行失败: {result.stderr}", "ERROR")
return False
except subprocess.TimeoutExpired:
self.log_message("ERROR: DMIS 脚本执行超时", "ERROR")
return False
except Exception as e:
self.log_message(f"ERROR: 执行 DMIS 脚本时出错: {str(e)}", "ERROR")
return False
def generate_text_report(self, program):
"""生成文本格式报告"""
try:
report_content = f"PC-DMIS 检测报告\n生成时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
report_content += f"程序名称: {program.Name}\n"
report_content += f"零件编号: {program.PartNumber}\n"
report_content += f"序列号: {program.SerialNumber}\n"
report_content += f"操作员: {program.Operator}\n"
report_content += f"测量设备: {program.MachineName}\n\n"
report_content += "===== 测量结果摘要 =====\n"
# 添加测量特征结果
for i in range(1, program.Features.Count + 1):
feature = program.Features(i)
report_content += f"特征: {feature.Name}\n"
report_content += f"理论值: {feature.Nominal}\n"
report_content += f"实测值: {feature.Actual}\n"
report_content += f"偏差: {feature.Deviation}\n"
report_content += f"公差: {feature.Tolerance}\n"
report_content += f"状态: {'合格' if feature.WithinTol else '超差'}\n"
report_content += "------------------------\n"
report_content += "\n===== 详细信息请查看PC-DMIS报告文件 =====\n"
return report_content
except Exception as e:
self.log_message(f"WARNING: 无法获取完整报告: {str(e)}", "WARNING")
return f"PC-DMIS 检测报告\n程序: {program.Name}\n生成时间: {datetime.datetime.now()}\n错误: 无法获取完整报告详情"
def generate_excel_report(self, program):
"""生成 Excel 格式检测报告"""
try:
# 添加 Excel 报告命令
excel_report = program.ReportCommands.Add("Excel")
# 设置报告模板(如果提供)
if self.config.get("excel_template"):
excel_report.TemplatePath = self.config["excel_template"]
# 设置输出路径
save_dir = os.path.join(os.path.dirname(program.FullName), "自动化报告")
os.makedirs(save_dir, exist_ok=True)
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
report_path = os.path.join(save_dir, f"{program.Name}_{timestamp}.xlsx")
excel_report.OutputPath = report_path
# 执行报告导出
excel_report.Refresh()
self.log_message(f"INFO: Excel 报告已导出: {report_path}", "INFO")
return report_path
except Exception as e:
self.log_message(f"ERROR: Excel 报告导出失败: {str(e)}", "ERROR")
return None
def on_closing(self):
"""窗口关闭事件处理"""
if self.running:
if messagebox.askyesno("确认", "任务正在运行,确定要退出吗?"):
self.stop_execution()
self.root.destroy()
else:
self.root.destroy()
self.save_config()
if __name__ == "__main__":
root = tk.Tk()
app = PC_DMIS_Automator(root)
root.mainloop()
分析一下我的这个python自动化PC DMIS的代码功能,检查问题,并修复问题,特别注意对弈pcdmis各版本的接口支持问题,修复后要自我验证,确保无异常和bug,提供修复后的完整代码
最新发布