高级玩家

- 贡献度
- 6
- 金元
- 4974
- 积分
- 521
- 精华
- 0
- 注册时间
- 2015-6-1
|
import os
import json
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, simpledialog
from pathlib import Path
import threading
import re
import sys
import shutil
# 尝试导入 openpyxl
try:
from openpyxl import Workbook
from openpyxl.styles import Font
EXCEL_SUPPORT = True
except ImportError:
EXCEL_SUPPORT = False
# --- 配置常量 ---
VERSION = "V2.0-Unique"
DB_FILE_NAME = "starfield_data.json"
WINDOW_TITLE = f" 星空物品查询器 {VERSION}"
FONT_FAMILY = "Microsoft YaHei UI"
FONT_SIZE = 10
CODE_FONT = ("Consolas", 10)
class StarfieldLiteApp:
def __init__(self, root):
self.root = root
self.root.title(WINDOW_TITLE)
self.root.geometry("950x700")
# 高 DPI 支持
try:
from ctypes import windll
windll.shcore.SetProcessDpiAwareness(1)
except:
pass
self.database = {}
self.filtered_data = []
# 【新增】代码反向索引:{ "00035A48": "战刀" }
# 用于在导入时快速判断代码是否已存在
self.code_index = {}
# 智能获取运行目录
if getattr(sys, 'frozen', False):
self.base_dir = Path(sys.executable).parent.resolve()
else:
self.base_dir = Path(__file__).parent.resolve()
self.db_path = self.base_dir / DB_FILE_NAME
self.setup_styles()
self.create_ui()
self.load_database()
def setup_styles(self):
style = ttk.Style()
if 'vista' in style.theme_names():
style.theme_use('vista')
elif 'clam' in style.theme_names():
style.theme_use('clam')
style.configure(".", font=(FONT_FAMILY, FONT_SIZE))
style.configure("Treeview", rowheight=28, font=(FONT_FAMILY, 10))
style.configure("Treeview.Heading", font=(FONT_FAMILY, 10, "bold"))
style.configure("Status.TLabel", foreground="#555555", background="#f0f0f0")
style.configure("Tool.TButton", padding=(10, 5))
def create_ui(self):
# === 顶部:搜索与操作 ===
top_frame = ttk.Frame(self.root, padding="10")
top_frame.pack(side=tk.TOP, fill=tk.X)
search_frame = ttk.LabelFrame(top_frame, text=" 搜索 (名称/代码/分类)", padding=5)
search_frame.pack(side=tk.LEFT, fill=tk.X, expand=True)
self.search_var = tk.StringVar()
self.search_var.trace_add("write", lambda *args: self.perform_search())
self.search_entry = ttk.Entry(search_frame, textvariable=self.search_var, font=(FONT_FAMILY, 12))
self.search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
self.search_entry.focus_set()
file_frame = ttk.Frame(top_frame)
file_frame.pack(side=tk.RIGHT)
ttk.Button(file_frame, text=" 手动加载库", command=self.manual_load_database).pack(side=tk.LEFT, padx=5)
ttk.Button(file_frame, text=" 智能导入 TXT", command=self.import_data_threaded).pack(side=tk.LEFT, padx=2)
if EXCEL_SUPPORT:
ttk.Button(file_frame, text=" 导出 Excel", command=self.export_to_excel).pack(side=tk.LEFT, padx=2)
else:
ttk.Button(file_frame, text="❌ 无 Excel", command=self.show_excel_error).pack(side=tk.LEFT, padx=2)
ttk.Button(file_frame, text=" 导出备份", command=self.export_data).pack(side=tk.LEFT, padx=2)
ttk.Button(file_frame, text="️ 清空", command=self.clear_data).pack(side=tk.LEFT, padx=2)
# === 中部:列表展示区 ===
list_frame = ttk.Frame(self.root, padding="10")
list_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
columns = ("cat", "name", "code")
self.tree = ttk.Treeview(list_frame, columns=columns, show="headings", selectmode="browse")
self.tree.heading("cat", text="分类", anchor="center")
self.tree.heading("name", text="物品名称")
self.tree.heading("code", text="代码 (ID)", anchor="center")
self.tree.column("cat", width=90, minwidth=70, anchor="center")
self.tree.column("name", width=350, minwidth=200)
self.tree.column("code", width=150, minwidth=100, anchor="center")
scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.tree.yview)
self.tree.configure(yscrollcommand=scrollbar.set)
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.tree.bind("<Double-1>", self.on_double_click)
self.tree.bind("<Button-3>", self.on_right_click)
# === 下部:代码工具箱 ===
tool_frame = ttk.LabelFrame(self.root, text="️ 代码生成工具箱", padding=10)
tool_frame.pack(side=tk.TOP, fill=tk.X, padx=10, pady=5)
row1 = ttk.Frame(tool_frame)
row1.pack(fill=tk.X, pady=(0, 5))
ttk.Label(row1, text="原始代码 ID:").pack(side=tk.LEFT, padx=(0, 5))
self.input_raw = ttk.Entry(row1, width=20, font=CODE_FONT)
self.input_raw.pack(side=tk.LEFT, padx=(0, 10))
ttk.Button(row1, text=" 物品", command=lambda: self.generate_code("player.additem")).pack(side=tk.LEFT, padx=2)
ttk.Button(row1, text="⭐ 技能", command=lambda: self.generate_code("player.addperk")).pack(side=tk.LEFT, padx=2)
ttk.Button(row1, text=" 属性", command=lambda: self.generate_code("player.setav")).pack(side=tk.LEFT, padx=2)
row2 = ttk.Frame(tool_frame)
row2.pack(fill=tk.X)
ttk.Label(row2, text="生成命令:").pack(side=tk.LEFT, padx=(0, 5))
self.input_result = ttk.Entry(row2, width=50, font=CODE_FONT, state="readonly")
self.input_result.pack(side=tk.LEFT, padx=(0, 10), fill=tk.X, expand=True)
self.btn_copy_tool = ttk.Button(row2, text=" 复制命令", command=self.copy_tool_result)
self.btn_copy_tool.pack(side=tk.RIGHT)
# === 底部:状态栏 ===
self.status_var = tk.StringVar()
self.status_var.set(f"就绪 | 版本 {VERSION}")
status_bar = ttk.Label(self.root, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W, style="Status.TLabel")
status_bar.pack(side=tk.BOTTOM, fill=tk.X)
# --- 核心逻辑 ---
def load_database(self):
if not self.db_path.exists():
self.status_var.set(f"未找到数据库 | 版本 {VERSION} (请导入 TXT)")
self.database = {}
self.code_index = {}
self.update_status()
return
try:
with open(self.db_path, 'r', encoding='utf-8') as f:
content = f.read().strip()
self.database = json.loads(content) if content else {}
# 【关键】加载时重建代码索引
self.rebuild_code_index()
self.perform_search()
self.status_var.set(f"已加载:{len(self.database)}条数据 | 版本 {VERSION}")
except Exception as e:
messagebox.showerror("数据库错误", f"读取失败:\n{e}\n建议清空或重新导入。")
self.database = {}
self.code_index = {}
self.update_status()
def rebuild_code_index(self):
"""遍历数据库,重建 code -> name 映射"""
self.code_index = {}
for name, info in self.database.items():
code = info.get('code', '').upper()
if code:
# 如果发现有多个名字对应同一个代码(脏数据),保留第一个遇到的
if code not in self.code_index:
self.code_index[code] = name
def manual_load_database(self):
file_path = filedialog.askopenfilename(
title="选择数据库文件 (JSON)",
filetypes=[("JSON Files", "*.json"), ("All Files", "*.*")],
initialdir=self.base_dir
)
if not file_path: return
try:
with open(file_path, 'r', encoding='utf-8') as f:
temp_data = json.load(f)
count = len(temp_data)
if not messagebox.askyesno("确认加载", f"找到 {count} 条数据。\n确定加载并设为默认?\n注意:这将覆盖当前内存中的所有数据。"):
return
shutil.copy(file_path, self.db_path)
self.database = temp_data
self.rebuild_code_index()
self.perform_search()
self.status_var.set(f"✅ 已加载 {count} 条 | 版本 {VERSION}")
messagebox.showinfo("成功", "数据库加载成功!")
except json.JSONDecodeError:
messagebox.showerror("格式错误", "文件不是有效的 JSON。")
except Exception as e:
messagebox.showerror("加载失败", str(e))
def save_database(self):
try:
with open(self.db_path, 'w', encoding='utf-8') as f:
json.dump(self.database, f, ensure_ascii=False, indent=2)
# 保存后不需要重建索引,因为只是修改现有数据,除非新增了条目
# 但为了保险,如果在导入过程中调用了 save,索引已经在内存中维护好了
self.update_status()
except Exception as e:
messagebox.showerror("保存错误", str(e))
def normalize_category(self, cat_str):
cat_str = cat_str.strip()
aliases = {
"weapon": "武器", "suit": "太空服", "armor": "太空服",
"helmet": "头盔", "bag": "背包", "skill": "技能",
"perk": "技能", "stat": "属性", "ammo": "弹药",
"throwable": "投掷物", "note": "笔记", "resource": "资源",
"misc": "杂项", "rescue": "救援", "med": "救援"
}
lower_cat = cat_str.lower()
if lower_cat in aliases:
return aliases[lower_cat]
return cat_str if cat_str else "杂项"
def perform_search(self):
query = self.search_var.get().lower()
self.filtered_data = []
for name, info in self.database.items():
cat = info.get('cat', '杂项')
code = info['code']
fav = info.get('fav', False)
match = not query or (query in name.lower() or query in code.lower() or query in cat.lower())
if match:
self.filtered_data.append({"name": name, "code": code, "cat": cat, "fav": fav})
self.filtered_data.sort(key=lambda x: (not x['fav'], x['cat'], x['name']))
self.refresh_tree()
self.update_status()
def refresh_tree(self):
self.tree.delete(*self.tree.get_children())
for item in self.filtered_data:
name = item['name']
code = item['code']
cat = item['cat']
fav = item['fav']
display_name = f"⭐ {name}" if fav else name
display_cat = f"[{cat}]"
self.tree.insert("", tk.END, values=(display_cat, display_name, code))
def update_status(self):
total = len(self.database)
current = len(self.filtered_data)
self.status_var.set(f"就绪 | 版本 {VERSION} | 总数:{total} | 结果:{current}")
# --- 交互事件 ---
def on_double_click(self, event):
selection = self.tree.selection()
if not selection: return
code = self.tree.item(selection[0])['values'][2]
self.copy_to_clipboard(str(code))
self.status_var.set(f"✅ 已复制:{code}")
def on_right_click(self, event):
item_id = self.tree.identify_row(event.y)
if not item_id: return
self.tree.selection_set(item_id)
item = self.tree.item(item_id)
vals = item['values']
real_cat = vals[0].strip("[]")
name_raw = vals[1]
code = vals[2]
is_fav = name_raw.startswith("⭐ ")
real_name = name_raw[2:] if is_fav else name_raw
menu = tk.Menu(self.root, tearoff=0)
menu.add_command(label=" 复制代码", command=lambda: self.copy_to_clipboard(code))
menu.add_command(label="✏️ 修改代码", command=lambda: self.edit_item(real_name, 'code'))
menu.add_command(label="️ 修改分类", command=lambda: self.edit_item(real_name, 'cat'))
menu.add_separator()
fav_text = "❌ 取消收藏" if is_fav else "⭐ 加入收藏"
menu.add_command(label=fav_text, command=lambda: self.toggle_favorite(real_name))
menu.add_separator()
menu.add_command(label="️ 删除此项", command=lambda: self.delete_item(real_name), foreground="red")
menu.post(event.x_root, event.y_root)
def edit_item(self, name, field):
if name not in self.database: return
current_val = self.database[name].get(field, '杂项' if field == 'cat' else '')
prompt = f"请输入新的{field}:" if field == 'code' else "请输入新的分类 (如:武器):"
new_val = simpledialog.askstring(f"修改 {field}", prompt, initialvalue=current_val)
if new_val and new_val.strip():
old_code = self.database[name].get('code', '')
if field == 'code':
# 如果修改了代码,需要更新索引
new_code = new_val.strip().upper()
if new_code != old_code:
# 移除旧索引
if old_code in self.code_index and self.code_index[old_code] == name:
del self.code_index[old_code]
# 添加新索引(如果新代码没被占用)
if new_code in self.code_index:
messagebox.showwarning("代码冲突", f"代码 {new_code} 已存在于库中(属于 {self.code_index[new_code]})。\n已强制更新当前物品的代码。")
self.code_index[new_code] = name
if field == 'cat':
new_val = self.normalize_category(new_val)
self.database[name][field] = new_val
self.save_database()
self.perform_search()
def toggle_favorite(self, name):
if name not in self.database: return
self.database[name]['fav'] = not self.database[name].get('fav', False)
self.save_database()
self.perform_search()
def delete_item(self, name):
if messagebox.askyesno("确认删除", f"确定删除「{name}」?"):
code = self.database[name].get('code', '')
# 从索引中移除
if code in self.code_index and self.code_index[code] == name:
del self.code_index[code]
del self.database[name]
self.save_database()
self.perform_search()
def copy_to_clipboard(self, text):
self.root.clipboard_clear()
self.root.clipboard_append(text)
self.root.update()
def generate_code(self, prefix):
raw = self.input_raw.get().strip()
if not raw:
messagebox.showwarning("提示", "请先输入代码 ID")
return
clean_raw = re.sub(r'^player\.\w+\s+', '', raw)
result = f"{prefix} {clean_raw}"
self.input_result.config(state="normal")
self.input_result.delete(0, tk.END)
self.input_result.insert(0, result)
self.input_result.config(state="readonly")
def copy_tool_result(self):
val = self.input_result.get()
if val:
self.copy_to_clipboard(val)
self.status_var.set(f"✅ 已复制:{val}")
# --- 导入/导出逻辑 (核心修改部分) ---
def import_data_threaded(self):
file_path = filedialog.askopenfilename(title="选择数据文件 (TXT)", filetypes=[("Text Files", "*.txt")])
if not file_path: return
win = tk.Toplevel(self.root)
win.title("正在智能导入...")
win.geometry("300x100")
win.transient(self.root)
win.grab_set()
ttk.Label(win, text="正在解析文件...\n检测 Sorting 标记").pack(pady=20)
pb = ttk.Progressbar(win, mode='indeterminate')
pb.pack(fill=tk.X, padx=20)
pb.start(10)
def worker():
try:
stats = self.process_import_file_smart(file_path)
self.root.after(0, lambda: self.finish_import(win, stats))
except Exception as e:
self.root.after(0, lambda: messagebox.showerror("导入失败", str(e)))
self.root.after(0, win.destroy)
threading.Thread(target=worker, daemon=True).start()
def process_import_file_smart(self, path):
"""
智能导入逻辑:
1. 检测 'sorting 分类名' 行来切换当前分类。
2. 检测代码唯一性,如果代码已存在,跳过该行(防止重复)。
"""
new_count = 0
dup_code_count = 0
dup_name_count = 0
current_category = "杂项"
categories_found = set()
with open(path, 'r', encoding='utf-8') as f:
lines = f.readlines()
if not lines:
return {"new": 0, "dup_code": 0, "dup_name": 0, "cats": []}
for line in lines:
line = line.strip()
if not line or line.startswith('#'):
continue
# 【策略 1】检测 Sorting 标记
# 匹配 "sorting 武器" 或 "Sorting:头盔" 等格式
sort_match = re.match(r'^sorting\s*[::]?\s*(.+)$', line, re.IGNORECASE)
if sort_match:
current_category = self.normalize_category(sort_match.group(1))
categories_found.add(current_category)
continue
# 【策略 2】解析数据行
# 支持 Tab 或多个空格分隔
parts = re.split(r'\t+|\s{2,}', line)
if len(parts) >= 2:
code = parts[0].strip().upper() # 统一转大写方便比对
name = parts[1].strip()
if not code or not name:
continue
# 【策略 3】代码唯一性检查
if code in self.code_index:
# 代码已存在
existing_name = self.code_index[code]
if existing_name == name:
# 完全重复(名和码都一样)
dup_code_count += 1
else:
# 代码相同但名字不同(如:战刀 vs 战斗刀)
# 策略:保留旧数据,跳过新数据,计数为“代码重复”
dup_code_count += 1
continue
# 【策略 4】名字重复检查(可选,防止同名不同码,但通常代码唯一更重要)
# 这里我们主要依赖代码唯一性。如果名字重复但代码不同,视为不同物品(可能是翻译差异)
# 如果希望名字也唯一,可以解开下面注释
"""
if name in self.database:
dup_name_count += 1
continue
"""
# 入库
self.database[name] = {
"code": code,
"cat": current_category,
"fav": False
}
# 更新索引
self.code_index[code] = name
new_count += 1
self.save_database()
return {
"new": new_count,
"dup_code": dup_code_count,
"dup_name": dup_name_count,
"cats": sorted(list(categories_found))
}
def finish_import(self, win, stats):
win.destroy()
cat_list = ", ".join(stats['cats']) if stats['cats'] else "无新分类"
msg = (f"✅ 智能导入完成!\n\n"
f" 涉及分类:【{cat_list}】\n"
f"➕ 新增物品:{stats['new']}\n"
f"⚠️ 跳过代码重复:{stats['dup_code']} (代码已存在,保留旧数据)\n")
# f"⚠️ 跳过名字重复:{stats['dup_name']}") # 如果启用了名字检查
messagebox.showinfo("导入结果", msg)
self.perform_search()
self.status_var.set(f"导入成功 | 新增 {stats['new']} 条 | 版本 {VERSION}")
def export_data(self):
path = filedialog.asksaveasfilename(defaultextension=".txt", filetypes=[("Text Files", "*.txt")])
if not path: return
try:
grouped = {}
for n, i in self.database.items():
c = i.get('cat', '杂项')
grouped.setdefault(c, []).append((n, i))
with open(path, 'w', encoding='utf-8') as f:
f.write("# Starfield Database Backup\n")
for c, items in sorted(grouped.items()):
f.write(f"sorting {c}\n") # 导出时也带上 sorting 标记,方便下次导入
for n, i in items:
f.write(f"{i['code']}\t{n}\n")
f.write("\n")
messagebox.showinfo("成功", f"文本备份已导出:\n{path}")
except Exception as e:
messagebox.showerror("错误", str(e))
def show_excel_error(self):
messagebox.showwarning("缺少依赖",
"未检测到 'openpyxl' 库。\n\n"
"如需导出 Excel,请在命令行运行:\n"
"pip install openpyxl\n"
"然后重新打包程序。")
def export_to_excel(self):
if not EXCEL_SUPPORT:
self.show_excel_error()
return
path = filedialog.asksaveasfilename(
defaultextension=".xlsx",
filetypes=[("Excel Files", "*.xlsx")],
initialfile=f"Starfield_Data_{VERSION}.xlsx"
)
if not path: return
try:
wb = Workbook()
ws = wb.active
ws.title = "Items"
headers = ["分类", "物品名称", "代码 ID", "收藏"]
ws.append(headers)
for name, info in self.database.items():
cat = info.get('cat', '杂项')
code = info['code']
fav = "是" if info.get('fav', False) else "否"
ws.append([cat, name, code, fav])
for col in ws.columns:
max_length = 0
column = col[0].column_letter
for cell in col:
if cell.value:
max_length = max(max_length, len(str(cell.value)))
ws.column_dimensions[column].width = min(max_length + 2, 50)
bold_font = Font(bold=True)
for cell in ws[1]:
cell.font = bold_font
wb.save(path)
messagebox.showinfo("成功", f"Excel 已导出:\n{path}\n共 {len(self.database)} 条数据。")
except Exception as e:
messagebox.showerror("导出失败", str(e))
def clear_data(self):
if messagebox.askyesno("高危警告", "确定要清空所有数据吗?\n此操作不可恢复!"):
self.database = {}
self.code_index = {}
self.save_database()
self.perform_search()
messagebox.showinfo("已清空", "数据库已重置。")
if __name__ == "__main__":
root = tk.Tk()
app = StarfieldLiteApp(root)
root.mainloop() |
|