import os
import subprocess
import platform
import tkinter as tk
from tkinter import ttk, messagebox, simpledialog, filedialog, Menu
from openpyxl import Workbook, load_workbook
from openpyxl.styles import Alignment, Font, Border, Side
import json
import webbrowser
import datetime
import time
EXCEL_FILE = '零件登记.xlsx'
PAIR_FILE = '零件配对.json'
TITLE_FONT = Font(name='微软雅黑', size=18, bold=True)
BODY_FONT = Font(name='微软雅黑', size=11)
TITLE_ALIGN = Alignment(horizontal='center', vertical='center')
BODY_ALIGN = Alignment(horizontal='center', vertical='center')
thin = Side(border_style='thin', color='000000')
all_border = Border(top=thin, left=thin, right=thin, bottom=thin)
class PairManager:
"""零件配对管理类"""
def __init__(self, master):
self.master = master
self.master.title("零件配对管理")
self.master.geometry("600x400")
self.master.resizable(False, False)
# 设置为模态窗口,禁用主窗口
self.master.grab_set()
self.master.transient(master.master)
# 加载现有配对
self.pairs = self.load_pairs()
# 创建界面
self.create_widgets()
self.update_tree()
# 设置窗口居中
self.center_window()
def center_window(self):
"""使窗口居中显示"""
self.master.update_idletasks()
width = self.master.winfo_width()
height = self.master.winfo_height()
x = (self.master.winfo_screenwidth() // 2) - (width // 2)
y = (self.master.winfo_screenheight() // 2) - (height // 2)
self.master.geometry(f'+{x}+{y}')
def create_widgets(self):
# 主框架
main_frame = ttk.Frame(self.master)
main_frame.pack(fill='both', expand=True, padx=10, pady=10)
# 使用Treeview替代Listbox
tree_frame = ttk.Frame(main_frame)
tree_frame.pack(fill='both', expand=True)
# 创建带滚动条的Treeview
scrollbar = ttk.Scrollbar(tree_frame)
scrollbar.pack(side='right', fill='y')
self.tree = ttk.Treeview(
tree_frame,
columns=('零件编号', '零件名称'),
show='headings',
selectmode='extended',
yscrollcommand=scrollbar.set
)
self.tree.pack(fill='both', expand=True)
scrollbar.config(command=self.tree.yview)
# 设置列
self.tree.heading('零件编号', text='零件编号', anchor='center')
self.tree.heading('零件名称', text='零件名称', anchor='center')
self.tree.column('零件编号', width=150, anchor='center')
self.tree.column('零件名称', width=400, anchor='center')
# 按钮框架
btn_frame = ttk.Frame(main_frame)
btn_frame.pack(fill='x', pady=10)
ttk.Button(btn_frame, text="添加", command=self.add_pair).pack(side='left', padx=5)
ttk.Button(btn_frame, text="编辑", command=self.edit_pair).pack(side='left', padx=5)
ttk.Button(btn_frame, text="删除", command=self.delete_pair).pack(side='left', padx=5)
ttk.Button(btn_frame, text="保存并关闭", command=self.save_and_close).pack(side='right', padx=5)
def load_pairs(self):
"""加载零件配对数据"""
try:
if os.path.exists(PAIR_FILE):
with open(PAIR_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
messagebox.showerror("错误", f"加载配对文件失败: {str(e)}")
return {}
def save_pairs(self):
"""保存零件配对数据"""
try:
with open(PAIR_FILE, 'w', encoding='utf-8') as f:
json.dump(self.pairs, f, ensure_ascii=False, indent=2)
return True
except Exception as e:
messagebox.showerror("错误", f"保存配对文件失败: {str(e)}")
return False
def update_tree(self):
"""更新Treeview显示"""
# 清空现有数据
for item in self.tree.get_children():
self.tree.delete(item)
# 添加新数据
for code, name in self.pairs.items():
self.tree.insert('', 'end', values=(code, name))
def add_pair(self):
"""添加新配对"""
dialog = PairDialog(self.master, "添加零件配对")
if dialog.result:
code, name = dialog.result
if code in self.pairs:
messagebox.showwarning("警告", f"零件编号 {code} 已存在!")
else:
self.pairs[code] = name
self.update_tree()
def edit_pair(self):
"""编辑现有配对"""
selection = self.tree.selection()
if not selection:
messagebox.showwarning("提示", "请先选择一个配对进行编辑")
return
# 如果只选了一个,执行单编辑
if len(selection) == 1:
item = selection[0]
code = self.tree.item(item, 'values')[0]
name = self.pairs[code]
dialog = PairDialog(self.master, "编辑零件配对", code, name)
if dialog.result:
new_code, new_name = dialog.result
if new_code != code and new_code in self.pairs:
messagebox.showwarning("警告", f"零件编号 {new_code} 已存在!")
else:
if new_code != code:
del self.pairs[code]
self.pairs[new_code] = new_name
self.update_tree()
else:
# 多选编辑 - 批量编辑名称
selected_codes = [self.tree.item(item, 'values')[0] for item in selection]
dialog = BatchEditDialog(self.master, "批量编辑零件名称", selected_codes, self.pairs)
if dialog.result:
for code in selected_codes:
self.pairs[code] = dialog.result
self.update_tree()
def delete_pair(self):
"""删除配对"""
selection = self.tree.selection()
if not selection:
messagebox.showwarning("提示", "请先选择一个配对")
return
selected_codes = [self.tree.item(item, 'values')[0] for item in selection]
if messagebox.askyesno("确认", f"确定要删除选中的 {len(selected_codes)} 个配对吗?"):
for code in selected_codes:
del self.pairs[code]
self.update_tree()
def save_and_close(self):
"""保存并关闭窗口"""
if self.save_pairs():
self.master.destroy()
class PairDialog(simpledialog.Dialog):
"""零件配对对话框"""
def __init__(self, parent, title, code="", name=""):
self.initial_code = code
self.initial_name = name
super().__init__(parent, title)
def body(self, frame):
ttk.Label(frame, text="零件编号:").grid(row=0, column=0, padx=5, pady=5, sticky='e')
self.code_var = tk.StringVar(value=self.initial_code)
code_entry = ttk.Entry(frame, textvariable=self.code_var, width=25)
code_entry.grid(row=0, column=1, padx=5, pady=5, sticky='we')
ttk.Label(frame, text="零件名称:").grid(row=1, column=0, padx=5, pady=5, sticky='e')
self.name_var = tk.StringVar(value=self.initial_name)
name_entry = ttk.Entry(frame, textvariable=self.name_var, width=25)
name_entry.grid(row=1, column=1, padx=5, pady=5, sticky='we')
return code_entry # 初始焦点
def validate(self):
code = self.code_var.get().strip()
name = self.name_var.get().strip()
if not code or not name:
messagebox.showwarning("输入错误", "零件编号和名称都不能为空")
return False
return True
def apply(self):
self.result = (self.code_var.get().strip(), self.name_var.get().strip())
class BatchEditDialog(simpledialog.Dialog):
"""批量编辑对话框"""
def __init__(self, parent, title, codes, pair_dict):
self.codes = codes
self.pair_dict = pair_dict
super().__init__(parent, title)
def body(self, frame):
ttk.Label(frame, text=f"批量编辑 {len(self.codes)} 个零件").grid(row=0, column=0, columnspan=2, pady=10)
# 显示前几个零件名称
preview_text = "\n".join([f"{code}: {self.pair_dict[code]}" for code in self.codes[:3]])
if len(self.codes) > 3:
preview_text += f"\n...等 {len(self.codes)} 个零件"
preview = tk.Text(frame, height=6, width=40, font=('微软雅黑', 9))
preview.grid(row=1, column=0, columnspan=2, padx=10, pady=5)
preview.insert(tk.END, preview_text)
preview.config(state=tk.DISABLED)
ttk.Label(frame, text="新零件名称:").grid(row=2, column=0, padx=5, pady=10, sticky='e')
self.name_var = tk.StringVar()
name_entry = ttk.Entry(frame, textvariable=self.name_var, width=25)
name_entry.grid(row=2, column=1, padx=5, pady=10, sticky='we')
return name_entry
def validate(self):
name = self.name_var.get().strip()
if not name:
messagebox.showwarning("输入错误", "零件名称不能为空")
return False
return True
def apply(self):
self.result = self.name_var.get().strip()
class HelpDialog:
"""帮助对话框"""
def __init__(self, parent):
self.parent = parent
self.window = tk.Toplevel(parent)
self.window.title("使用帮助")
self.window.geometry("600x450")
self.window.resizable(False, False)
# 设置为模态窗口,禁用主窗口
self.window.grab_set()
self.window.transient(parent)
# 创建标签页
self.notebook = ttk.Notebook(self.window)
self.notebook.pack(fill='both', expand=True, padx=10, pady=10)
# 基本操作标签
basic_frame = ttk.Frame(self.notebook)
self.notebook.add(basic_frame, text="基本操作")
self.create_basic_tab(basic_frame)
# 快捷键标签
shortcut_frame = ttk.Frame(self.notebook)
self.notebook.add(shortcut_frame, text="快捷键")
self.create_shortcut_tab(shortcut_frame)
# 零件管理标签
pair_frame = ttk.Frame(self.notebook)
self.notebook.add(pair_frame, text="零件管理")
self.create_pair_tab(pair_frame)
# 底部按钮
btn_frame = ttk.Frame(self.window)
btn_frame.pack(fill='x', padx=10, pady=10)
ttk.Button(btn_frame, text="关闭", command=self.window.destroy).pack(side='right')
self.center_window()
def center_window(self):
"""使窗口居中显示"""
self.window.update_idletasks()
width = self.window.winfo_width()
height = self.window.winfo_height()
x = (self.window.winfo_screenwidth() // 2) - (width // 2)
y = (self.window.winfo_screenheight() // 2) - (height // 2)
self.window.geometry(f'+{x}+{y}')
def create_basic_tab(self, frame):
content = """
1. 添加零件
- 在左侧输入零件编号、名称和数量
- 点击【添加并写入 Excel】按钮或按 Ctrl+Enter
- 零件信息将添加到右侧列表并保存到Excel
2. 编辑零件
- 在右侧列表右键点击零件
- 选择【编辑】修改单个零件
- 选择【批量编辑数量】修改多个零件数量
3. 删除零件
- 在右侧列表右键点击零件
- 选择【删除】删除选中零件
- 或按 Delete 键删除
4. 加载Excel
- 点击【加载Excel】按钮导入已有数据
- 支持覆盖当前数据或追加数据
5. 打开Excel
- 点击【打开 Excel】查看生成的Excel文件
"""
text = tk.Text(frame, wrap=tk.WORD, font=('微软雅黑', 10))
text.pack(fill='both', expand=True, padx=10, pady=10)
text.insert(tk.END, content.strip())
text.config(state=tk.DISABLED)
def create_shortcut_tab(self, frame):
content = """
常用快捷键列表:
Ctrl + Enter : 添加当前零件并写入Excel
Delete : 删除选中的零件
Ctrl + E : 打开零件配对管理
Ctrl + O : 打开Excel文件
Ctrl + L : 加载Excel数据
Ctrl + H : 打开帮助文档
"""
text = tk.Text(frame, wrap=tk.WORD, font=('微软雅黑', 10))
text.pack(fill='both', expand=True, padx=10, pady=10)
text.insert(tk.END, content.strip())
text.config(state=tk.DISABLED)
def create_pair_tab(self, frame):
content = """
零件配对管理:
1. 添加配对
- 点击【添加】按钮创建新零件配对
- 输入零件编号和名称
2. 编辑配对
- 选择单个配对点击【编辑】修改
- 选择多个配对点击【编辑】批量修改名称
3. 删除配对
- 选择单个或多个配对
- 点击【删除】按钮删除
4. 自动填充
- 在主界面输入零件编号时自动填充名称
- 输入零件名称时自动填充编号
"""
text = tk.Text(frame, wrap=tk.WORD, font=('微软雅黑', 10))
text.pack(fill='both', expand=True, padx=10, pady=10)
text.insert(tk.END, content.strip())
text.config(state=tk.DISABLED)
class LogDialog:
"""日志对话框(只包含操作日志)"""
def __init__(self, parent, logs):
self.parent = parent
self.logs = logs
self.window = tk.Toplevel(parent)
self.window.title("操作日志")
self.window.geometry("800x500")
self.window.resizable(True, True)
# 设置为模态窗口,禁用主窗口
self.window.grab_set()
self.window.transient(parent)
# 创建框架
frame = ttk.Frame(self.window)
frame.pack(fill='both', expand=True, padx=10, pady=10)
# 创建带滚动条的文本框
scrollbar = ttk.Scrollbar(frame)
scrollbar.pack(side='right', fill='y')
self.log_text = tk.Text(frame, wrap=tk.WORD, font=('微软雅黑', 10), yscrollcommand=scrollbar.set)
self.log_text.pack(fill='both', expand=True, padx=10, pady=10)
scrollbar.config(command=self.log_text.yview)
# 添加日志内容
for log in self.logs:
self.log_text.insert(tk.END, log + "\n")
self.log_text.config(state=tk.DISABLED)
# 底部按钮
btn_frame = ttk.Frame(self.window)
btn_frame.pack(fill='x', padx=10, pady=10)
ttk.Button(btn_frame, text="关闭", command=self.window.destroy).pack(side='right')
self.center_window()
def center_window(self):
"""使窗口居中显示"""
self.window.update_idletasks()
width = self.window.winfo_width()
height = self.window.winfo_height()
x = (self.window.winfo_screenwidth() // 2) - (width // 2)
y = (self.window.winfo_screenheight() // 2) - (height // 2)
self.window.geometry(f'+{x}+{y}')
class PartRegApp:
def __init__(self, root):
self.root = root
root.title("零件登记工具")
root.geometry("950x580")
root.resizable(False, False)
# 创建菜单栏
self.create_menu()
# 加载零件配对数据
self.pair_dict = self.load_pair_data()
# 初始化日志
self.logs = []
self.log("程序启动")
# ---------- 顶部:设备型号 ----------
top_frame = ttk.Frame(root)
top_frame.pack(fill='x', padx=5, pady=5)
ttk.Label(top_frame, text="设备型号:").pack(side='left')
self.device_model = tk.StringVar(value='DS-9C')
ttk.Entry(top_frame, textvariable=self.device_model, width=15).pack(side='left', padx=5)
# ---------- 主体 ----------
paned = ttk.PanedWindow(root, orient='horizontal')
paned.pack(fill='both', expand=True, padx=5, pady=5)
# 左侧
left = ttk.Frame(paned, width=300)
paned.add(left)
left.pack_propagate(False)
# 零件信息输入框
frm = ttk.LabelFrame(left, text="零件信息", padding=10)
frm.pack(fill='x')
ttk.Label(frm, text="零件编号:").pack(anchor='w')
self.code_var = tk.StringVar()
# 使用Combobox替代Entry
self.code_combo = ttk.Combobox(frm, textvariable=self.code_var, width=20)
self.code_combo.pack(fill='x', pady=2)
self.code_combo.bind('<KeyRelease>', self.on_code_keyrelease)
self.code_combo.bind('<<ComboboxSelected>>', self.on_code_selected)
# 绑定变量变化事件
self.code_var.trace_add('write', self.on_code_var_changed)
self.code_timer = None # 用于延迟检索的计时器
ttk.Label(frm, text="零件名称:").pack(anchor='w', pady=(8, 0))
self.name_var = tk.StringVar()
# 使用Combobox替代Entry
self.name_combo = ttk.Combobox(frm, textvariable=self.name_var, width=20)
self.name_combo.pack(fill='x', pady=2)
self.name_combo.bind('<KeyRelease>', self.on_name_keyrelease)
self.name_combo.bind('<<ComboboxSelected>>', self.on_name_selected)
# 绑定变量变化事件
self.name_var.trace_add('write', self.on_name_var_changed)
self.name_timer = None # 用于延迟检索的计时器
ttk.Label(frm, text="数量:").pack(anchor='w', pady=(8, 0))
self.qty_var = tk.StringVar()
qty_entry = ttk.Entry(frm, textvariable=self.qty_var)
qty_entry.pack(fill='x', pady=2)
# 添加快捷键绑定
qty_entry.bind('<Return>', lambda event: self.add_and_write())
qty_entry.bind('<KP_Enter>', lambda event: self.add_and_write()) # 小键盘回车
btn = ttk.Button(frm, text="添加并写入 Excel (Ctrl+Enter)", command=self.add_and_write)
btn.pack(pady=(15, 0))
# 操作按钮 - 垂直排列
btn_frame = ttk.Frame(left)
btn_frame.pack(fill='x', pady=(10, 5))
# 按钮垂直排列
ttk.Button(btn_frame, text="加载Excel (Ctrl+L)", command=self.load_excel).pack(side='top', fill='x', padx=5, pady=2)
ttk.Button(btn_frame, text="打开 Excel (Ctrl+O)", command=self.open_excel).pack(side='top', fill='x', padx=5, pady=2)
ttk.Button(btn_frame, text="零件配对管理 (Ctrl+E)", command=self.open_pair_manager).pack(side='top', fill='x', padx=5, pady=2)
# 右侧预览
right = ttk.Frame(paned)
paned.add(right)
self.tree = ttk.Treeview(right, columns=('零件编号', '零件名称', '数量'), show='headings', height=22)
self.tree.pack(fill='both', expand=True)
for col in ('零件编号', '零件名称', '数量'):
self.tree.heading(col, text=col)
self.tree.column(col, anchor='center', stretch=True)
# 添加滚动条
scrollbar = ttk.Scrollbar(right, orient="vertical", command=self.tree.yview)
scrollbar.pack(side='right', fill='y')
self.tree.configure(yscrollcommand=scrollbar.set)
self.tree.bind('<Button-3>', self.show_menu)
self.menu = tk.Menu(root, tearoff=0)
self.menu.add_command(label="编辑", command=self.edit_selected)
self.menu.add_command(label="批量编辑数量", command=self.batch_edit_qty)
self.menu.add_command(label="删除", command=self.delete_selected)
# 状态栏
status_frame = ttk.Frame(root)
status_frame.pack(side='bottom', fill='x')
# 左侧状态信息
self.status = tk.StringVar(value="就绪")
status_bar = ttk.Label(status_frame, textvariable=self.status, relief='sunken', anchor='w', padding=(5, 2))
status_bar.pack(side='left', fill='x', expand=True)
# 右侧时间和公司信息
self.time_var = tk.StringVar()
time_label = ttk.Label(status_frame, textvariable=self.time_var, relief='sunken', anchor='e', padding=(5, 2))
time_label.pack(side='right', fill='x')
# 更新时间显示
self.update_time()
self.data = []
self.updating = False # 防止自动填充时触发循环事件
self.cached_codes = list(self.pair_dict.keys())
self.cached_names = list(self.pair_dict.values())
# 绑定全局快捷键
self.root.bind('<Control-Return>', lambda event: self.add_and_write())
self.root.bind('<Control-o>', lambda event: self.open_excel())
self.root.bind('<Control-O>', lambda event: self.open_excel())
self.root.bind('<Control-l>', lambda event: self.load_excel())
self.root.bind('<Control-L>', lambda event: self.load_excel())
self.root.bind('<Control-e>', lambda event: self.open_pair_manager())
self.root.bind('<Control-E>', lambda event: self.open_pair_manager())
self.root.bind('<Control-h>', lambda event: self.show_help())
self.root.bind('<Control-H>', lambda event: self.show_help())
self.root.bind('<Delete>', lambda event: self.delete_selected())
self.root.bind('<KP_Delete>', lambda event: self.delete_selected()) # 小键盘Delete
# 初始焦点
self.code_combo.focus_set()
def create_menu(self):
"""创建菜单栏"""
menubar = Menu(self.root)
self.root.config(menu=menubar)
# 文件菜单
file_menu = Menu(menubar, tearoff=0)
file_menu.add_command(label="打开Excel (Ctrl+O)", command=self.open_excel)
file_menu.add_command(label="加载Excel (Ctrl+L)", command=self.load_excel)
file_menu.add_separator()
file_menu.add_command(label="退出", command=self.root.quit)
menubar.add_cascade(label="文件", menu=file_menu)
# 日志菜单
log_menu = Menu(menubar, tearoff=0)
log_menu.add_command(label="操作日志", command=lambda: self.show_logs())
menubar.add_cascade(label="日志", menu=log_menu)
# 帮助菜单
help_menu = Menu(menubar, tearoff=0)
help_menu.add_command(label="使用帮助 (Ctrl+H)", command=self.show_help)
help_menu.add_command(label="关于", command=self.show_about)
menubar.add_cascade(label="帮助", menu=help_menu)
def log(self, message):
"""记录日志"""
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log_entry = f"[{timestamp}] {message}"
self.logs.append(log_entry)
# 保持日志数量不超过100条
if len(self.logs) > 100:
self.logs.pop(0)
def show_logs(self):
"""显示日志对话框"""
LogDialog(self.root, self.logs)
def load_pair_data(self):
"""加载零件配对数据"""
if os.path.exists(PAIR_FILE):
try:
with open(PAIR_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
except:
return {}
return {}
# ---------- 自动补全功能(优化版)----------
def filter_items(self, items, pattern):
"""根据输入模式过滤项目"""
if not pattern:
return items
return [item for item in items if pattern.lower() in item.lower()]
def on_code_keyrelease(self, event):
"""零件编号输入框按键释放事件(优化版)"""
# 忽略方向键和回车键
if event.keysym in ('Up', 'Down', 'Return', 'Escape'):
return
# 取消之前的定时器(如果存在)
if self.code_timer:
self.root.after_cancel(self.code_timer)
# 设置新的定时器(900毫秒后执行)
self.code_timer = self.root.after(900, self.process_code_input)
def process_code_input(self):
"""处理零件编号输入(延迟执行)"""
code = self.code_var.get().strip()
# 过滤并更新下拉列表
filtered_codes = self.filter_items(self.cached_codes, code)
self.code_combo['values'] = filtered_codes
# 如果输入框不为空,显示下拉列表
if code:
self.code_combo.event_generate('<Down>')
# 自动填充零件名称
if not self.updating and code in self.pair_dict:
self.updating = True
self.name_var.set(self.pair_dict[code])
self.updating = False
def on_name_keyrelease(self, event):
"""零件名称输入框按键释放事件(优化版)"""
# 忽略方向键和回车键
if event.keysym in ('Up', 'Down', 'Return', 'Escape'):
return
# 取消之前的定时器(如果存在)
if self.name_timer:
self.root.after_cancel(self.name_timer)
# 设置新的定时器(500毫秒后执行)
self.name_timer = self.root.after(500, self.process_name_input)
def process_name_input(self):
"""处理零件名称输入(延迟执行)"""
name = self.name_var.get().strip()
# 过滤并更新下拉列表
filtered_names = self.filter_items(self.cached_names, name)
self.name_combo['values'] = filtered_names
# 如果输入框不为空,显示下拉列表
if name:
self.name_combo.event_generate('<Down>')
# 自动填充零件编号
if not self.updating:
for code, n in self.pair_dict.items():
if n == name:
self.updating = True
self.code_var.set(code)
self.updating = False
break
def on_code_selected(self, event):
"""零件编号下拉选项选择事件"""
code = self.code_var.get().strip()
if code in self.pair_dict:
self.name_var.set(self.pair_dict[code])
def on_name_selected(self, event):
"""零件名称下拉选项选择事件"""
name = self.name_var.get().strip()
for code, n in self.pair_dict.items():
if n == name:
self.code_var.set(code)
break
# ---------- 输入框变化事件 ----------
def on_code_var_changed(self, *args):
"""零件编号变化时的事件处理"""
if self.updating:
return
code = self.code_var.get().strip()
# 如果零件编号被清空,则同时清空零件名称
if not code:
self.name_var.set('')
def on_name_var_changed(self, *args):
"""零件名称变化时的事件处理"""
if self.updating:
return
name = self.name_var.get().strip()
# 如果零件名称被清空,则同时清空零件编号
if not name:
self.code_var.set('')
# ---------- 配对管理 ----------
def open_pair_manager(self):
"""打开零件配对管理窗口"""
manager_window = tk.Toplevel(self.root)
PairManager(manager_window)
# 当管理窗口关闭后重新加载配对数据
manager_window.wait_window()
self.pair_dict = self.load_pair_data()
# 更新缓存
self.cached_codes = list(self.pair_dict.keys())
self.cached_names = list(self.pair_dict.values())
# 更新下拉列表
self.code_combo['values'] = self.cached_codes
self.name_combo['values'] = self.cached_names
self.log("零件配对数据已更新")
# ---------- 工具 ----------
def set_status(self, msg):
self.status.set(msg)
self.root.update_idletasks()
def update_time(self):
"""更新时间显示"""
current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.time_var.set(f"广州图森机械设备有限公司 | {current_time}")
self.root.after(1000, self.update_time) # 每秒更新一次
def add_and_write(self):
code = self.code_var.get().strip()
name = self.name_var.get().strip()
qty_str = self.qty_var.get().strip()
if not all([code, name, qty_str]):
messagebox.showwarning("提示", "请完整填写零件信息!")
return
try:
qty = int(qty_str)
except ValueError:
messagebox.showwarning("提示", "数量必须是整数!")
return
# 检查是否已存在相同零件
existing_index = None
for i, item in enumerate(self.data):
if item[0] == code and item[1] == name:
existing_index = i
break
if existing_index is not None:
# 更新现有零件的数量
old_qty = self.data[existing_index][2]
new_qty = old_qty + qty
self.data[existing_index] = (code, name, new_qty)
self.set_status(f"已更新:{code} 数量从 {old_qty} 增加到 {new_qty}")
self.log(f"更新零件: {code} ({name}) 数量 {old_qty} → {new_qty}")
else:
# 添加新零件
self.data.append((code, name, qty))
self.set_status(f"已添加:{code} × {qty}")
self.log(f"添加零件: {code} ({name}) × {qty}")
self.refresh_tree()
self.write_excel()
self.clear_inputs()
def clear_inputs(self):
self.code_var.set('')
self.name_var.set('')
self.qty_var.set('')
self.code_combo.focus_set() # 焦点回到零件编号输入框
def refresh_tree(self):
for item in self.tree.get_children():
self.tree.delete(item)
for row in self.data:
self.tree.insert('', 'end', values=row)
def write_excel(self):
wb = Workbook()
ws = wb.active
ws.title = "零件登记"
# 标题
title = self.device_model.get().strip() or "零件登记"
ws.merge_cells('A1:C1')
ws['A1'] = title
ws['A1'].font = TITLE_FONT
ws['A1'].alignment = TITLE_ALIGN
ws.row_dimensions[1].height = 45 * 0.75
# 表头
headers = ['零件编号', '零件名称', '数量']
for idx, h in enumerate(headers, 1):
cell = ws.cell(row=2, column=idx, value=h)
cell.font = BODY_FONT
cell.alignment = BODY_ALIGN
cell.border = all_border
# 数据(数量列写入为数字)
for r, (code, name, qty) in enumerate(self.data, 3):
ws.cell(row=r, column=1, value=code).font = BODY_FONT
ws.cell(row=r, column=2, value=name).font = BODY_FONT
ws.cell(row=r, column=3, value=qty).font = BODY_FONT
for c in range(1, 4):
ws.cell(row=r, column=c).alignment = BODY_ALIGN
ws.cell(row=r, column=c).border = all_border
# 固定列宽
ws.column_dimensions['A'].width = 15
ws.column_dimensions['B'].width = 20
ws.column_dimensions['C'].width = 10
try:
wb.save(EXCEL_FILE)
self.log(f"Excel文件已保存: {EXCEL_FILE}")
except PermissionError:
messagebox.showerror("错误", "文件被占用,请关闭后重试!")
self.log("保存Excel失败: 文件被占用")
def open_excel(self):
if not os.path.isfile(EXCEL_FILE):
messagebox.showwarning("提示", f"未找到 {EXCEL_FILE}\n请先添加数据生成文件。")
return
system = platform.system()
try:
if system == 'Windows':
os.startfile(EXCEL_FILE)
elif system == 'Darwin':
subprocess.run(['open', EXCEL_FILE])
else:
subprocess.run(['xdg-open', EXCEL_FILE])
self.set_status(f"已打开 Excel:{EXCEL_FILE}")
self.log(f"打开Excel文件: {EXCEL_FILE}")
except Exception as e:
messagebox.showerror("错误", f"无法打开文件:\n{e}")
self.log(f"打开Excel失败: {str(e)}")
# ---------- 加载Excel文件 ----------
def load_excel(self):
"""加载已有的Excel文件到预览区"""
# 询问用户加载方式
if self.data:
choice = messagebox.askquestion("加载选项", "请选择加载方式:\n\n'是' - 覆盖当前数据\n'否' - 追加到当前数据\n'取消' - 中止操作",
icon='question', type='yesnocancel')
if choice == 'cancel':
return
else:
choice = 'yes' # 没有数据时默认覆盖
# 弹出文件选择对话框
file_path = filedialog.askopenfilename(
title="选择零件登记表",
filetypes=[("Excel文件", "*.xlsx"), ("所有文件", "*.*")]
)
if not file_path:
return # 用户取消选择
try:
# 加载工作簿
wb = load_workbook(file_path)
ws = wb.active
# 如果是追加模式,保留当前数据
if choice == 'no':
new_data = self.data.copy()
self.log(f"开始追加Excel数据: {file_path}")
else:
# 覆盖模式 - 清空当前数据
new_data = []
# 读取标题(设备型号)
if ws['A1'].value:
self.device_model.set(ws['A1'].value)
self.log(f"开始覆盖加载Excel数据: {file_path}")
# 读取数据行(从第三行开始)
for row in ws.iter_rows(min_row=3, values_only=True):
# 跳过空行
if not any(row[:3]):
continue
# 确保数据格式正确
code = str(row[0]) if row[0] else ""
name = str(row[1]) if row[1] else ""
qty = row[2] if row[2] is not None else 0
# 如果是数字类型,直接使用
if isinstance(qty, (int, float)):
qty = int(qty)
else:
try:
qty = int(qty)
except:
qty = 0
# 添加到数据列表
new_data.append((code, name, qty))
# 更新数据
self.data = new_data
# 刷新Treeview
self.refresh_tree()
# 更新状态
self.set_status(f"已加载文件: {os.path.basename(file_path)},共 {len(self.data)} 条记录")
# 保存到默认位置
self.write_excel()
self.log(f"成功加载Excel: {file_path},共 {len(self.data)} 条记录")
except Exception as e:
messagebox.showerror("加载错误", f"无法加载Excel文件:\n{str(e)}")
self.log(f"加载Excel失败: {str(e)}")
# ---------- 右键菜单功能 ----------
def show_menu(self, event):
if self.tree.identify_row(event.y):
self.menu.post(event.x_root, event.y_root)
def edit_selected(self):
"""编辑单个选中的零件"""
selected = self.tree.selection()
if not selected or len(selected) > 1:
return
item = selected[0]
values = self.tree.item(item, 'values')
idx = self.tree.index(item)
# 创建编辑对话框
dialog = tk.Toplevel(self.root)
dialog.title("编辑零件")
dialog.geometry("300x200")
dialog.resizable(False, False)
# 设置为模态窗口,禁用主窗口
dialog.grab_set()
dialog.transient(self.root)
# 零件编号
ttk.Label(dialog, text="零件编号:").place(x=20, y=20)
code_var = tk.StringVar(value=values[0])
code_entry = ttk.Entry(dialog, textvariable=code_var, width=20)
code_entry.place(x=100, y=20)
# 零件名称
ttk.Label(dialog, text="零件名称:").place(x=20, y=60)
name_var = tk.StringVar(value=values[1])
name_entry = ttk.Entry(dialog, textvariable=name_var, width=20)
name_entry.place(x=100, y=60)
# 数量
ttk.Label(dialog, text="数量:").place(x=20, y=100)
qty_var = tk.StringVar(value=values[2])
qty_entry = ttk.Entry(dialog, textvariable=qty_var, width=10)
qty_entry.place(x=100, y=100)
def save_changes():
code = code_var.get().strip()
name = name_var.get().strip()
qty_str = qty_var.get().strip()
if not all([code, name, qty_str]):
messagebox.showwarning("输入错误", "所有字段都必须填写!")
return
try:
qty = int(qty_str)
except ValueError:
messagebox.showwarning("输入错误", "数量必须是整数!")
return
# 记录原始值
original_code, original_name, original_qty = self.data[idx]
# 更新数据
self.data[idx] = (code, name, qty)
self.refresh_tree()
self.write_excel()
self.set_status(f"已更新: {code}")
self.log(f"编辑零件: {original_code}({original_name})×{original_qty} → {code}({name})×{qty}")
dialog.destroy()
ttk.Button(dialog, text="保存", command=save_changes).place(x=120, y=140)
ttk.Button(dialog, text="取消", command=dialog.destroy).place(x=200, y=140)
# 居中对话框
dialog.update_idletasks()
width = dialog.winfo_width()
height = dialog.winfo_height()
x = (dialog.winfo_screenwidth() // 2) - (width // 2)
y = (dialog.winfo_screenheight() // 2) - (height // 2)
dialog.geometry(f'+{x}+{y}')
code_entry.focus_set()
def batch_edit_qty(self):
"""批量编辑选中零件的数量"""
selected = self.tree.selection()
if not selected:
return
# 创建批量编辑对话框
dialog = tk.Toplevel(self.root)
dialog.title("批量编辑数量")
dialog.geometry("300x150")
dialog.resizable(False, False)
# 设置为模态窗口,禁用主窗口
dialog.grab_set()
dialog.transient(self.root)
# 显示选中零件数量
ttk.Label(dialog, text=f"选中 {len(selected)} 个零件").pack(pady=10)
# 新数量输入框
ttk.Label(dialog, text="新数量:").pack()
qty_var = tk.StringVar()
qty_entry = ttk.Entry(dialog, textvariable=qty_var, width=10)
qty_entry.pack()
def apply_changes():
qty_str = qty_var.get().strip()
if not qty_str:
messagebox.showwarning("输入错误", "数量不能为空!")
return
try:
new_qty = int(qty_str)
except ValueError:
messagebox.showwarning("输入错误", "数量必须是整数!")
return
# 更新所有选中零件的数量
for item in selected:
idx = self.tree.index(item)
code, name, old_qty = self.data[idx]
self.data[idx] = (code, name, new_qty)
self.log(f"批量更新: {code}({name}) 数量 {old_qty} → {new_qty}")
self.refresh_tree()
self.write_excel()
self.set_status(f"已批量更新 {len(selected)} 个零件的数量为 {new_qty}")
dialog.destroy()
btn_frame = ttk.Frame(dialog)
btn_frame.pack(pady=10)
ttk.Button(btn_frame, text="应用", command=apply_changes).pack(side='left', padx=10)
ttk.Button(btn_frame, text="取消", command=dialog.destroy).pack(side='left', padx=10)
# 居中对话框
dialog.update_idletasks()
width = dialog.winfo_width()
height = dialog.winfo_height()
x = (dialog.winfo_screenwidth() // 2) - (width // 2)
y = (dialog.winfo_screenheight() // 2) - (height // 2)
dialog.geometry(f'+{x}+{y}')
qty_entry.focus_set()
def delete_selected(self):
"""删除选中的零件"""
selected = self.tree.selection()
if not selected:
return
# 获取所有要删除的零件信息
to_delete = []
for item in selected:
idx = self.tree.index(item)
to_delete.append(self.data[idx])
if not messagebox.askyesno("确认", f"确定要删除选中的 {len(selected)} 个零件吗?"):
return
# 从后往前删除,避免索引变化问题
for item in sorted(selected, reverse=True):
idx = self.tree.index(item)
deleted_part = self.data.pop(idx)
self.tree.delete(item)
self.log(f"删除零件: {deleted_part[0]}({deleted_part[1]}) × {deleted_part[2]}")
# 更新状态
if to_delete:
deleted_codes = [item[0] for item in to_delete]
if len(deleted_codes) > 3:
codes_str = f"{', '.join(deleted_codes[:3])} 等共 {len(deleted_codes)} 个零件"
else:
codes_str = ", ".join(deleted_codes)
self.set_status(f"已删除: {codes_str}")
self.write_excel()
# ---------- 帮助功能 ----------
def show_help(self):
"""显示帮助对话框"""
HelpDialog(self.root)
self.log("打开帮助文档")
def show_about(self):
"""显示关于对话框"""
about = tk.Toplevel(self.root)
about.title("关于")
about.geometry("360x250")
about.resizable(False, False)
# 设置为模态窗口,禁用主窗口
about.grab_set()
about.transient(self.root)
# 主框架
main_frame = ttk.Frame(about)
main_frame.pack(fill='both', expand=True, padx=20, pady=20)
# 公司名称
ttk.Label(main_frame, text="广州图森机械设备有限公司",
font=('微软雅黑', 14, 'bold')).pack(pady=(10, 5))
# 软件名称和版本
ttk.Label(main_frame, text="零件登记工具",
font=('微软雅黑', 12)).pack(pady=5)
ttk.Label(main_frame, text="版本: 2.0",
font=('微软雅黑', 10)).pack()
# 分隔线
ttk.Separator(main_frame, orient='horizontal').pack(fill='x', pady=10)
# 网站链接
website_frame = ttk.Frame(main_frame)
website_frame.pack(pady=5)
ttk.Label(website_frame, text="公司网站:", font=('微软雅黑', 9)).pack(side='left')
# 创建可点击的链接
link = ttk.Label(website_frame, text="www.tusen.cn",
font=('微软雅黑', 9, 'underline'),
foreground="blue", cursor="hand2")
link.pack(side='left', padx=5)
link.bind("<Button-1>", lambda e: webbrowser.open("http://www.tusen.cn/"))
# 版权信息
ttk.Label(main_frame, text="© 2025 广州图森机械设备有限公司",
font=('微软雅黑', 9)).pack(side='bottom', pady=10)
# 居中窗口
about.update_idletasks()
width = about.winfo_width()
height = about.winfo_height()
x = (about.winfo_screenwidth() // 2) - (width // 2)
y = (about.winfo_screenheight() // 2) - (height // 2)
about.geometry(f'+{x}+{y}')
# ---------- 其他功能 ----------
def clear_data(self):
"""清空当前数据"""
if not self.data:
return
if messagebox.askyesno("确认", "确定要清空所有零件数据吗?"):
self.log(f"清空所有零件数据,共 {len(self.data)} 条记录")
self.data = []
self.refresh_tree()
self.write_excel()
self.set_status("已清空所有数据")
# ---------------- 运行 ----------------
if __name__ == '__main__':
root = tk.Tk()
app = PartRegApp(root)
root.mainloop() 这是我写的一个工具,现在的功能基本已经完善好了,你再帮我分析看下,我这个工具还需要哪些功能呢?或者哪些地方还需要优化呢?给我个建议看看
最新发布