initial commit with script, doc and one sample dwg

This commit is contained in:
Salijoghli 2026-03-25 16:26:37 +04:00
commit 3ea09dccf8
5 changed files with 995 additions and 0 deletions

18
.gitignore vendored Normal file
View File

@ -0,0 +1,18 @@
# Python
.venv/
__pycache__/
*.pyc
# Build / generated
build/
dist/
*.spec
export_progress.json
# AutoCAD generated/temp
*.bak
*.dwl
*.dwl2
# CAD source files (keep out of git unless intentionally tracked)
*.dwg

BIN
2501-AMZ-TPA8-MCM01-600.dwg Normal file

Binary file not shown.

130
README.md Normal file
View File

@ -0,0 +1,130 @@
# CAD Layout Batch Export Tool
This project automates DWG layout preparation in AutoCAD.
It opens selected drawings, attaches a master drawing as XREF, imports a template layout if missing, applies viewport zoom positioning, updates title block attributes, removes default layouts, saves, and closes each file.
The app has two modes:
- IO
- NETWORK
The script is in export_layouts_ui.py.
## What This Tool Does
For each drawing in the selected batch, the tool:
1. Opens the DWG in AutoCAD (COM automation).
2. Uses the selected Master DWG as an XREF source (name: XREF1), except when processing the master itself.
3. Ensures the target template layout exists by importing from the selected Template DWG if needed.
4. Activates that layout and runs viewport zoom using mode-specific coordinates.
5. Updates title block attributes:
- IO mode: uses grouped names from the names file.
- NETWORK mode: uses one name per sheet and transforms naming for network labels.
6. Deletes Layout1 and Layout2.
7. Saves and closes the DWG.
8. Writes progress to export_progress.json for resume support.
9. Deletes matching .bak right after each successful DWG.
10. On full completion of all files, performs a final recursive cleanup of remaining .bak files.
## Requirements
System:
- Windows
- AutoCAD installed
- AutoCAD must be available through COM
Python:
- Python 3.x
- Package: pywin32
Standard library modules used:
- os
- re
- json
- threading
- time
- tkinter
## Installation
1. Open PowerShell in the project folder.
2. Install dependency:
python -m pip install pywin32
If you use a virtual environment, activate it first and run the same command.
## How To Run
Start the app:
python export_layouts_ui.py
In the UI, select:
1. Master DWG (XREF source)
2. Template DWG (layout source)
3. Drawing files to process
4. Block Names File (TXT)
Then click Run Batch.
You can also use the same DWG as both Master and Template if that drawing contains a valid paper-space layout.
## Input File Notes
Block Names TXT:
- One name per line
- Empty lines are ignored
Example names file is list.txt.
Progress file:
- export_progress.json is auto-created/updated.
- It stores completed DWG file names.
- Start Over in the UI resets this progress file.
## Zoom Logic Overview
The script computes viewport center per file index:
- center_x = base_x + (index * block_width)
- center_y = base_y
It then sends AutoCAD command:
- ZOOM C center_x,center_y height
IO and NETWORK modes use different base values and height.
## Resume and Cleanup Behavior
- If a batch is interrupted, rerun and it resumes using export_progress.json.
- After each successful file, the related .bak is removed.
- After full completion (all files done and not canceled), remaining .bak files are deleted recursively from the current working folder.
## Troubleshooting
ModuleNotFoundError: pythoncom
- Cause: pywin32 not installed in the same Python interpreter used to run the script.
- Fix:
1. Check python path:
where python
2. Install in that interpreter:
python -m pip install pywin32
AutoCAD COM errors (for example call rejected by callee)
- Keep AutoCAD open and responsive during batch processing.
- Retry the batch (the script already includes retry logic for open calls).
No layout found in template file
- Ensure the template DWG has at least one paper-space layout (not only Model).
## Main Project Files
- export_layouts_ui.py: main application
- list.txt: example block names input
- export_progress.json: generated progress log during runs
## Safety Notes
- This tool edits and saves DWG files in place.
- Test with a small file set first.
- Keep a separate backup of critical project drawings before first production run.

761
export_layouts_ui.py Normal file
View File

@ -0,0 +1,761 @@
import os
import re
import json
import threading
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import pythoncom
import win32com.client
import time
from win32com.client import pythoncom as pycom
PROGRESS_LOG = 'export_progress.json'
def safe_open_doc(docs, dwg_path, retries=5, delay=3):
for attempt in range(retries):
try:
return docs.Open(dwg_path)
except Exception as e:
if 'Call was rejected by callee' in str(e) and attempt < retries - 1:
time.sleep(delay)
else:
raise
# --- Backend Logic ---
def set_block_attribute(doc, block_name, attr_tag, value):
# Search PaperSpace for block references
for entity in doc.PaperSpace:
if entity.ObjectName == 'AcDbBlockReference' and entity.Name == block_name:
try:
for attrib in entity.GetAttributes():
if attrib.TagString.upper() == attr_tag.upper():
attrib.TextString = value
return True
except Exception:
continue
return False
def set_wyncorp_block(doc, chunk, log_operation=None):
# First try to find UPS.AE 11X17 (V0) block and set DWGDESC1
for entity in doc.PaperSpace:
if entity.ObjectName == 'AcDbBlockReference' and entity.Name == "UPS.AE 11X17 (V0)":
try:
value = "IO LAYOUT " + ", ".join(chunk)
for attrib in entity.GetAttributes():
if attrib.TagString.upper() == "DWGDESC1":
attrib.TextString = value
if log_operation:
log_operation(f"Inputting block DWGDESC1: {value}")
return True
except Exception:
continue
# If UPS.AE block not found, try WYNCORP-DSIZE_AS block and set TITLE2 and TITLE3
for entity in doc.PaperSpace:
if entity.ObjectName == 'AcDbBlockReference' and entity.Name == "WYNCORP-DSIZE_AS":
try:
title2_val = "/".join(chunk[:2]) if chunk[:2] else ""
title3_val = "/".join(chunk[2:4]) if chunk[2:4] else ""
if title3_val:
title3_val += " IO DRAWINGS"
else:
title3_val = "IO DRAWINGS"
for attrib in entity.GetAttributes():
if attrib.TagString.upper() == "TITLE2":
attrib.TextString = title2_val
if log_operation:
log_operation(f"Inputting block TITLE2: {title2_val}")
if attrib.TagString.upper() == "TITLE3":
attrib.TextString = title3_val
if log_operation:
log_operation(f"Inputting block TITLE3: {title3_val}")
return True
except Exception:
continue
return False
def transform_dpm_name_for_network(raw_name):
try:
import re as _re
m = _re.search(r"^(.*)_(DPM\d+)$", raw_name, _re.IGNORECASE)
if m:
rest = m.group(1)
dpm = m.group(2).upper()
return f"{dpm}_{rest}_ENET"
return f"{raw_name}_ENET"
except Exception:
return f"{raw_name}_ENET"
def set_network_block(doc, raw_name, log_operation=None):
value_title3 = transform_dpm_name_for_network(raw_name)
# Prefer WYNCORP-DSIZE_AS: TITLE2 = NETWORK DETAILS, TITLE3 = transformed
for entity in doc.PaperSpace:
if entity.ObjectName == 'AcDbBlockReference' and entity.Name == "WYNCORP-DSIZE_AS":
try:
for attrib in entity.GetAttributes():
if attrib.TagString.upper() == "TITLE2":
attrib.TextString = "NETWORK DETAILS"
if log_operation:
log_operation("Inputting block TITLE2: NETWORK DETAILS")
if attrib.TagString.upper() == "TITLE3":
attrib.TextString = value_title3
if log_operation:
log_operation(f"Inputting block TITLE3: {value_title3}")
return True
except Exception:
continue
# Fallback to UPS.AE 11X17 (V0) -> DWGDESC1 (use raw DPM name, no transform)
for entity in doc.PaperSpace:
if entity.ObjectName == 'AcDbBlockReference' and entity.Name == "UPS.AE 11X17 (V0)":
try:
for attrib in entity.GetAttributes():
if attrib.TagString.upper() == "DWGDESC1":
fallback_val = f"NETWORK DETAILS {raw_name}"
attrib.TextString = fallback_val
if log_operation:
log_operation(f"Inputting block DWGDESC1: {fallback_val}")
return True
except Exception:
continue
return False
def process_single_file(dwg_path, base_dwg_path, layout_template, template_layout_name, index, total, acad, docs, block_names, mode="IO", log_operation=None, error_callback=None):
new_doc = None
# Auto-close if already open
for doc in list(docs):
try:
if os.path.abspath(doc.FullName).lower() == os.path.abspath(dwg_path).lower():
doc.Close(False)
# time.sleep(2)
except Exception:
continue
try:
if log_operation:
log_operation(f"Opening file: {os.path.basename(dwg_path)}")
new_doc = safe_open_doc(docs, dwg_path)
# time.sleep(2)
# Check if Xref already exists (skip attaching when processing the master file itself)
is_master_file = os.path.abspath(dwg_path).lower() == os.path.abspath(base_dwg_path).lower()
xref_exists = False
if not is_master_file:
for item in new_doc.ModelSpace:
if item.ObjectName == "AcDbBlockReference":
try:
if item.Name == "XREF1":
xref_exists = True
break
except:
continue
if not xref_exists:
if log_operation:
log_operation("Attaching Xref...")
# Use relative path - just the filename if in same folder
master_filename = os.path.basename(base_dwg_path)
insertion_point = win32com.client.VARIANT(pythoncom.VT_ARRAY | pythoncom.VT_R8, [0, 0, 0])
new_doc.ModelSpace.AttachExternalReference(
master_filename, "XREF1", insertion_point, 1, 1, 1, 0, False)
# Check if layout exists
layout_exists = False
for layout in new_doc.Layouts:
if layout.Name.upper() == template_layout_name.upper():
layout_exists = True
break
if not layout_exists:
if log_operation:
log_operation("Adding template layout...")
filedia_original = new_doc.GetVariable("FILEDIA")
new_doc.SetVariable("FILEDIA", 0)
command_sequence = (
"_-LAYOUT\n"
"T\n"
f'"{layout_template}"\n'
f"{template_layout_name}\n"
"\n"
)
acad.ActiveDocument.SendCommand(command_sequence)
# time.sleep(2)
new_doc.SetVariable("FILEDIA", filedia_original)
# Activate layout
new_doc.ActiveLayout = new_doc.Layouts.Item(template_layout_name)
# time.sleep(0.5)
# Pan/zoom viewport
if log_operation:
log_operation("Zooming to layout area...")
# Set viewport values based on mode
if mode == "IO":
base_x = 19.13
base_y = 0.7881
block_width = 38.5
height = 22.2955
else: # NETWORK mode
# NETWORK mode needs different values - working backwards from your feedback
# For 19 files with total width 826.5: block_width = 826.5/19 = 43.5
# For 10th file (index 9) to be 369: 369 = base_x + (9 * 43.5)
# So base_x = 369 - 391.5 = -22.5
base_x = -22
base_y = 13.25
block_width = 43.5
height = 26.1748
acad.ActiveDocument.SendCommand("MSPACE\n")
# time.sleep(0.5)
# For first file (index 0), use base position. For subsequent files, add block_width * index
center_x = base_x + (index * block_width)
center_y = base_y
zoom_command = f'ZOOM C {center_x},{center_y} {height}\n'
acad.ActiveDocument.SendCommand(zoom_command)
# time.sleep(1)
acad.ActiveDocument.SendCommand("PSPACE\n")
# time.sleep(0.5)
# --- Set block attribute for this layout ---
if mode == "IO":
# IO mode: use chunks of 4 names
start_idx = index * 4
chunk = block_names[start_idx:start_idx+4]
block_set = False
if chunk:
value = "IO LAYOUT " + "/".join(chunk)
if log_operation:
log_operation(f"Inputting block names: {value}")
block_set = set_block_attribute(new_doc, "UPS.AE 11X17 (V0)", "DWGDESC1", value)
# If not found, try WYNCORP-DSIZE_AS logic
if not block_set and chunk:
if log_operation:
log_operation(f"Trying WYNCORP-DSIZE_AS block for chunk: {chunk}")
set_wyncorp_block(new_doc, chunk, log_operation=log_operation)
else: # NETWORK mode
# NETWORK mode: use single name transformed: DPMx_<rest>_ENET,
# and set TITLE2/TITLE3 on WYNCORP when present; fallback to UPS.AE
if block_names and index < len(block_names):
raw_name = block_names[index]
if log_operation:
log_operation(f"Preparing NETWORK labels for: {raw_name}")
set_network_block(new_doc, raw_name, log_operation=log_operation)
# Delete default layouts
if log_operation:
log_operation("Deleting default layouts (Layout1, Layout2)...")
for layout_name in ["Layout1", "Layout2"]:
delete_command = f"_-LAYOUT\nD\n{layout_name}\n"
acad.ActiveDocument.SendCommand(delete_command)
time.sleep(1)
# Save and restore FILEDIA, then close
if log_operation:
log_operation("Saving file and restoring FILEDIA...")
new_doc.Save()
try:
new_doc.SetVariable("FILEDIA", 1)
except Exception:
pass
except Exception as e:
if error_callback:
error_callback(str(e))
raise
finally:
try:
if new_doc is not None:
new_doc.Close(False)
# time.sleep(2)
except Exception:
pass
return True
# --- Progress Log ---
def load_progress():
if os.path.exists(PROGRESS_LOG):
with open(PROGRESS_LOG, 'r') as f:
return set(json.load(f))
return set()
def save_progress(done_files):
with open(PROGRESS_LOG, 'w') as f:
json.dump(list(done_files), f)
def is_autocad_running():
try:
win32com.client.GetActiveObject("AutoCAD.Application")
return True
except Exception:
return False
def read_block_names(names_file):
if not names_file or not os.path.exists(names_file):
return []
with open(names_file, 'r', encoding='utf-8') as f:
return [line.strip() for line in f if line.strip()]
def delete_bak_for_dwg(dwg_path, log_operation=None, error_callback=None):
bak_path = os.path.splitext(dwg_path)[0] + '.bak'
try:
if os.path.exists(bak_path):
os.remove(bak_path)
if log_operation:
log_operation(f"Deleted backup file: {os.path.basename(bak_path)}")
except Exception as e:
if error_callback:
error_callback(f"Failed to delete backup file {os.path.basename(bak_path)}: {e}")
def delete_all_bak_files(root_dir, log_operation=None, error_callback=None):
deleted_count = 0
try:
for current_root, _, files in os.walk(root_dir):
for file_name in files:
if file_name.lower().endswith('.bak'):
bak_path = os.path.join(current_root, file_name)
try:
os.remove(bak_path)
deleted_count += 1
except Exception as e:
if error_callback:
error_callback(f"Failed to delete backup file {file_name}: {e}")
if log_operation:
log_operation(f"Deleted {deleted_count} remaining .bak file(s) after full completion.")
except Exception as e:
if error_callback:
error_callback(f"Failed during final backup cleanup: {e}")
# --- UI ---
def main():
root = tk.Tk()
root.title("Export Layouts Batch Processor")
root.geometry("750x750")
root.configure(bg="#181818")
root.resizable(False, False)
# Modern font
FONT = ("Segoe UI", 11)
FONT_BOLD = ("Segoe UI", 12, "bold")
GOLD = "#FFD700"
BLACK = "#181818"
WHITE = "#FFFFFF"
style = ttk.Style()
style.theme_use('clam')
style.configure("TButton", font=FONT, background=GOLD, foreground=BLACK, borderwidth=0, focusthickness=3, focuscolor=GOLD, padding=8)
style.map("TButton", background=[('active', GOLD), ('pressed', GOLD)])
style.configure("TEntry", fieldbackground=BLACK, foreground=GOLD, borderwidth=1)
style.configure("TLabel", background=BLACK, foreground=GOLD, font=FONT)
style.configure("TProgressbar", troughcolor=BLACK, bordercolor=BLACK, background=GOLD, lightcolor=GOLD, darkcolor=GOLD, thickness=20)
master_path = tk.StringVar()
template_path = tk.StringVar()
drawings_files = [] # List of selected DWG files
drawings_files_var = tk.StringVar(value="No drawings selected.")
progress_var = tk.IntVar(value=0)
status_var = tk.StringVar(value="Idle.")
current_file_var = tk.StringVar(value="No file processing yet.")
error_log_var = tk.StringVar(value="")
names_file = tk.StringVar()
block_names = []
# --- Logging variables ---
operations_log_var = tk.StringVar(value="")
error_log_var = tk.StringVar(value="")
def log_operation(msg):
prev = operations_log_var.get()
operations_log_var.set((prev + "\n" if prev else "") + msg)
def append_error(msg):
prev = error_log_var.get()
error_log_var.set((prev + "\n" if prev else "") + msg)
def select_master():
path = filedialog.askopenfilename(title="Select Master DWG", filetypes=[("DWG Files", "*.dwg")])
if path:
master_path.set(path)
def select_template():
path = filedialog.askopenfilename(title="Select Template DWG", filetypes=[("DWG Files", "*.dwg")])
if path:
template_path.set(path)
def select_drawings_files():
nonlocal drawings_files
files = filedialog.askopenfilenames(title="Select Drawing Files", filetypes=[("DWG Files", "*.dwg")])
if files:
drawings_files = list(files)
drawings_files_var.set(f"{len(drawings_files)} files selected.")
else:
drawings_files = []
drawings_files_var.set("No drawings selected.")
def select_names_file():
path = filedialog.askopenfilename(title="Select Block Names File", filetypes=[("Text Files", "*.txt")])
if path:
names_file.set(path)
cancel_requested = tk.BooleanVar(value=False)
def set_buttons_state(state):
# IO tab buttons
try:
run_btn.config(state=state)
start_over_btn.config(state=state)
cancel_btn.config(state="normal" if state == "disabled" else "disabled")
except Exception:
pass
# NETWORK tab buttons
try:
n_run_btn.config(state=state)
n_start_over_btn.config(state=state)
n_cancel_btn.config(state="normal" if state == "disabled" else "disabled")
except Exception:
pass
def start_batch():
nonlocal block_names
if not master_path.get() or not template_path.get() or not drawings_files or not names_file.get():
messagebox.showerror("Error", "Please select master, template DWG files, drawing files, and block names file.")
return
block_names = read_block_names(names_file.get())
if not block_names:
messagebox.showerror("Error", "Block names file is empty or not found.")
return
# Determine which tab is active
current_tab = main_notebook.select()
if "IO" in main_notebook.tab(current_tab, "text"):
mode = "IO"
else:
mode = "NETWORK"
progress_var.set(0)
status_var.set("Processing...")
current_file_var.set("")
operations_log_var.set("")
error_log_var.set("")
cancel_requested.set(False)
set_buttons_state("disabled")
threading.Thread(target=lambda: run_batch(mode), daemon=True).start()
def start_over():
if os.path.exists(PROGRESS_LOG):
os.remove(PROGRESS_LOG)
progress_var.set(0)
status_var.set("Progress reset. Ready.")
current_file_var.set("")
error_log_var.set("")
operations_log_var.set("")
def cancel_batch():
cancel_requested.set(True)
status_var.set("Cancelling... (will stop after current file)")
def run_batch(mode="IO"):
try:
# Build processing order: master first, then sorted drawings (excluding master if present)
master = master_path.get()
drawings = [f for f in list(drawings_files) if os.path.abspath(f).lower() != os.path.abspath(master).lower()]
def sort_key(file_name):
match = re.search(r'LAYOUT\s*(\d+)', os.path.basename(file_name), re.IGNORECASE)
if match:
return int(match.group(1))
return float('inf')
drawings.sort(key=sort_key)
files = [master] + drawings
total = len(files)
done_files = load_progress()
pythoncom.CoInitialize()
acad = win32com.client.Dispatch("AutoCAD.Application")
docs = acad.Documents
template_doc = docs.Open(template_path.get())
time.sleep(2) # Give AutoCAD time to fully load the template
template_layout_name = None
try:
for layout in template_doc.Layouts:
if layout.Name.upper() != "MODEL":
template_layout_name = layout.Name
break
except Exception as e:
append_error(f"Error reading template layouts: {e}")
template_layout_name = "TITLE PAGE" # Fallback to default name
finally:
try:
template_doc.Close(False)
except Exception:
pass
if not template_layout_name:
status_var.set("No layout found in template file!")
pythoncom.CoUninitialize()
set_buttons_state("normal")
return
idx = 0
while idx < total:
if cancel_requested.get():
status_var.set("Batch cancelled by user.")
break
fname = files[idx]
base_fname = os.path.basename(fname)
if base_fname in done_files:
progress_var.set(int(100 * (idx + 1) / total))
idx += 1
continue
dwg_path = fname
current_file_var.set(f"Processing: {base_fname} ({idx+1}/{total})")
try:
ok = process_single_file(
dwg_path, master_path.get(), template_path.get(), template_layout_name, idx, total, acad, docs, block_names, mode, log_operation=log_operation, error_callback=append_error
)
if ok:
done_files.add(base_fname)
save_progress(done_files)
delete_bak_for_dwg(dwg_path, log_operation=log_operation, error_callback=append_error)
progress_var.set(int(100 * (idx + 1) / total))
idx += 1
else:
append_error(f"Failed: {base_fname}")
idx += 1 # Continue to next file
except Exception as e:
append_error(f"Error processing {base_fname}: {e}")
idx += 1 # Continue to next file
if not cancel_requested.get() and len(done_files) >= total:
delete_all_bak_files(os.getcwd(), log_operation=log_operation, error_callback=append_error)
if not cancel_requested.get():
status_var.set("All files processed (or resumed to last file).")
current_file_var.set("")
except Exception as e:
append_error(f"Batch processing error: {e}")
status_var.set("Batch processing failed.")
finally:
set_buttons_state("normal")
try:
pythoncom.CoUninitialize()
except Exception:
pass
# --- Modern UI Layout ---
pad = {'padx': 16, 'pady': 8}
frame = tk.Frame(root, bg=BLACK)
frame.pack(fill="both", expand=True)
tk.Label(frame, text="Export Layouts Batch Processor", font=FONT_BOLD, fg=GOLD, bg=BLACK).pack(pady=(18, 10))
# Main tabbed interface
main_notebook = ttk.Notebook(frame)
main_notebook.pack(fill="both", expand=True, padx=16, pady=(0, 10))
# IO Tab
io_frame = tk.Frame(main_notebook, bg=BLACK)
network_frame = tk.Frame(main_notebook, bg=BLACK)
main_notebook.add(io_frame, text="IO")
main_notebook.add(network_frame, text="NETWORK")
# IO Tab Content
# Master DWG
row1 = tk.Frame(io_frame, bg=BLACK)
row1.pack(fill="x", **pad)
row1.grid_columnconfigure(1, weight=1)
tk.Label(row1, text="Master DWG (Xref):", font=FONT, fg=GOLD, bg=BLACK, width=18, anchor="w").grid(row=0, column=0, sticky="w")
master_entry = tk.Entry(row1, textvariable=master_path, font=FONT, fg=GOLD, bg=BLACK, insertbackground=GOLD, highlightbackground=GOLD, highlightcolor=GOLD)
master_entry.grid(row=0, column=1, sticky="ew", padx=(0,8))
ttk.Button(row1, text="Browse...", command=select_master).grid(row=0, column=2, sticky="e")
# Template DWG
row2 = tk.Frame(io_frame, bg=BLACK)
row2.pack(fill="x", **pad)
row2.grid_columnconfigure(1, weight=1)
tk.Label(row2, text="Template DWG:", font=FONT, fg=GOLD, bg=BLACK, width=18, anchor="w").grid(row=0, column=0, sticky="w")
template_entry = tk.Entry(row2, textvariable=template_path, font=FONT, fg=GOLD, bg=BLACK, insertbackground=GOLD, highlightbackground=GOLD, highlightcolor=GOLD)
template_entry.grid(row=0, column=1, sticky="ew", padx=(0,8))
ttk.Button(row2, text="Browse...", command=select_template).grid(row=0, column=2, sticky="e")
# Drawings files (multi-select)
row3 = tk.Frame(io_frame, bg=BLACK)
row3.pack(fill="x", **pad)
row3.grid_columnconfigure(1, weight=1)
tk.Label(row3, text="Drawings:", font=FONT, fg=GOLD, bg=BLACK, width=18, anchor="w").grid(row=0, column=0, sticky="w")
tk.Label(row3, textvariable=drawings_files_var, font=FONT, fg=GOLD, bg=BLACK, anchor="w").grid(row=0, column=1, sticky="ew", padx=(0,8))
ttk.Button(row3, text="Browse...", command=select_drawings_files).grid(row=0, column=2, sticky="e")
# Block names file
row4 = tk.Frame(io_frame, bg=BLACK)
row4.pack(fill="x", **pad)
row4.grid_columnconfigure(1, weight=1)
tk.Label(row4, text="Block Names File:", font=FONT, fg=GOLD, bg=BLACK, width=18, anchor="w").grid(row=0, column=0, sticky="w")
tk.Entry(row4, textvariable=names_file, font=FONT, fg=GOLD, bg=BLACK, insertbackground=GOLD, highlightbackground=GOLD, highlightcolor=GOLD).grid(row=0, column=1, sticky="ew", padx=(0,8))
ttk.Button(row4, text="Browse...", command=select_names_file).grid(row=0, column=2, sticky="e")
# Progress bar with white border
tk.Label(io_frame, text="Progress:", font=FONT, fg=GOLD, bg=BLACK, anchor="w").pack(fill="x", **pad)
pb_border = tk.Frame(io_frame, bg=WHITE, padx=2, pady=2)
pb_border.pack(pady=(0,10), padx=32, fill="x")
ttk.Progressbar(pb_border, variable=progress_var, maximum=100, length=500, style="TProgressbar").pack(fill="x")
# Status and current file
tk.Label(io_frame, textvariable=status_var, font=FONT, fg=WHITE, bg=BLACK).pack(fill="x", **pad)
tk.Label(io_frame, textvariable=current_file_var, font=FONT, fg=GOLD, bg=BLACK).pack(fill="x", **pad)
# Tabbed log area
log_notebook = ttk.Notebook(io_frame)
log_notebook.pack(padx=32, pady=(10,10), fill="both", expand=False)
op_frame = tk.Frame(log_notebook, bg=BLACK)
err_frame = tk.Frame(log_notebook, bg=BLACK)
log_notebook.add(op_frame, text="Operations Log")
log_notebook.add(err_frame, text="Error Log")
# Operations log with auto-hiding scrollbar
op_scroll = tk.Scrollbar(op_frame)
op_text = tk.Text(op_frame, height=7, width=80, font=FONT, fg=GOLD, bg=BLACK, insertbackground=GOLD, highlightbackground=GOLD, highlightcolor=GOLD, borderwidth=2, relief="groove", state="disabled", yscrollcommand=op_scroll.set, wrap="word")
op_scroll.config(command=op_text.yview)
op_text.pack(side="left", fill="both", expand=True)
# Error log with auto-hiding scrollbar
err_scroll = tk.Scrollbar(err_frame)
err_text = tk.Text(err_frame, height=7, width=80, font=FONT, fg=GOLD, bg=BLACK, insertbackground=GOLD, highlightbackground=GOLD, highlightcolor=GOLD, borderwidth=2, relief="groove", state="disabled", yscrollcommand=err_scroll.set, wrap="word")
err_scroll.config(command=err_text.yview)
err_text.pack(side="left", fill="both", expand=True)
def show_scrollbar_if_needed(text_widget, scrollbar):
text_widget.update_idletasks()
if text_widget.yview()[0] > 0 or text_widget.yview()[1] < 1:
scrollbar.pack(side="right", fill="y")
else:
scrollbar.pack_forget()
def update_op_log(*args):
op_text.config(state="normal")
op_text.delete(1.0, tk.END)
op_text.insert(tk.END, operations_log_var.get())
op_text.see(tk.END)
op_text.config(state="disabled")
show_scrollbar_if_needed(op_text, op_scroll)
def update_err_log(*args):
err_text.config(state="normal")
err_text.delete(1.0, tk.END)
err_text.insert(tk.END, error_log_var.get())
err_text.see(tk.END)
err_text.config(state="disabled")
show_scrollbar_if_needed(err_text, err_scroll)
operations_log_var.trace_add('write', update_op_log)
error_log_var.trace_add('write', update_err_log)
op_text.bind('<Configure>', lambda e: show_scrollbar_if_needed(op_text, op_scroll))
err_text.bind('<Configure>', lambda e: show_scrollbar_if_needed(err_text, err_scroll))
# Style the selected tab
style.layout("TNotebook.Tab", [
('Notebook.tab', {'sticky': 'nswe', 'children': [
('Notebook.padding', {'side': 'top', 'sticky': 'nswe', 'children': [
('Notebook.focus', {'side': 'top', 'sticky': 'nswe', 'children': [
('Notebook.label', {'side': 'top', 'sticky': ''})
]})
]})
]})
])
style.map("TNotebook.Tab",
background=[('selected', GOLD), ('!selected', BLACK)],
foreground=[('selected', BLACK), ('!selected', GOLD)])
# Buttons
btn_frame = tk.Frame(io_frame, bg=BLACK)
btn_frame.pack(pady=(10, 18))
run_btn = ttk.Button(btn_frame, text="Run Batch", command=start_batch)
run_btn.pack(side="left", padx=12)
start_over_btn = ttk.Button(btn_frame, text="Start Over", command=start_over)
start_over_btn.pack(side="left", padx=12)
cancel_btn = ttk.Button(btn_frame, text="Cancel", command=cancel_batch, state="disabled")
cancel_btn.pack(side="left", padx=12)
# NETWORK Tab Content
# Master DWG
n_row1 = tk.Frame(network_frame, bg=BLACK)
n_row1.pack(fill="x", **pad)
n_row1.grid_columnconfigure(1, weight=1)
tk.Label(n_row1, text="Master DWG (Xref):", font=FONT, fg=GOLD, bg=BLACK, width=18, anchor="w").grid(row=0, column=0, sticky="w")
n_master_entry = tk.Entry(n_row1, textvariable=master_path, font=FONT, fg=GOLD, bg=BLACK, insertbackground=GOLD, highlightbackground=GOLD, highlightcolor=GOLD)
n_master_entry.grid(row=0, column=1, sticky="ew", padx=(0,8))
ttk.Button(n_row1, text="Browse...", command=select_master).grid(row=0, column=2, sticky="e")
# Template DWG
n_row2 = tk.Frame(network_frame, bg=BLACK)
n_row2.pack(fill="x", **pad)
n_row2.grid_columnconfigure(1, weight=1)
tk.Label(n_row2, text="Template DWG:", font=FONT, fg=GOLD, bg=BLACK, width=18, anchor="w").grid(row=0, column=0, sticky="w")
n_template_entry = tk.Entry(n_row2, textvariable=template_path, font=FONT, fg=GOLD, bg=BLACK, insertbackground=GOLD, highlightbackground=GOLD, highlightcolor=GOLD)
n_template_entry.grid(row=0, column=1, sticky="ew", padx=(0,8))
ttk.Button(n_row2, text="Browse...", command=select_template).grid(row=0, column=2, sticky="e")
# Drawings files (multi-select)
n_row3 = tk.Frame(network_frame, bg=BLACK)
n_row3.pack(fill="x", **pad)
n_row3.grid_columnconfigure(1, weight=1)
tk.Label(n_row3, text="Drawings:", font=FONT, fg=GOLD, bg=BLACK, width=18, anchor="w").grid(row=0, column=0, sticky="w")
tk.Label(n_row3, textvariable=drawings_files_var, font=FONT, fg=GOLD, bg=BLACK, anchor="w").grid(row=0, column=1, sticky="ew", padx=(0,8))
ttk.Button(n_row3, text="Browse...", command=select_drawings_files).grid(row=0, column=2, sticky="e")
# Block names file
n_row4 = tk.Frame(network_frame, bg=BLACK)
n_row4.pack(fill="x", **pad)
n_row4.grid_columnconfigure(1, weight=1)
tk.Label(n_row4, text="Block Names File:", font=FONT, fg=GOLD, bg=BLACK, width=18, anchor="w").grid(row=0, column=0, sticky="w")
tk.Entry(n_row4, textvariable=names_file, font=FONT, fg=GOLD, bg=BLACK, insertbackground=GOLD, highlightbackground=GOLD, highlightcolor=GOLD).grid(row=0, column=1, sticky="ew", padx=(0,8))
ttk.Button(n_row4, text="Browse...", command=select_names_file).grid(row=0, column=2, sticky="e")
# Progress bar with white border
tk.Label(network_frame, text="Progress:", font=FONT, fg=GOLD, bg=BLACK, anchor="w").pack(fill="x", **pad)
n_pb_border = tk.Frame(network_frame, bg=WHITE, padx=2, pady=2)
n_pb_border.pack(pady=(0,10), padx=32, fill="x")
ttk.Progressbar(n_pb_border, variable=progress_var, maximum=100, length=500, style="TProgressbar").pack(fill="x")
# Status and current file
tk.Label(network_frame, textvariable=status_var, font=FONT, fg=WHITE, bg=BLACK).pack(fill="x", **pad)
tk.Label(network_frame, textvariable=current_file_var, font=FONT, fg=GOLD, bg=BLACK).pack(fill="x", **pad)
# Tabbed log area
n_log_notebook = ttk.Notebook(network_frame)
n_log_notebook.pack(padx=32, pady=(10,10), fill="both", expand=False)
n_op_frame = tk.Frame(n_log_notebook, bg=BLACK)
n_err_frame = tk.Frame(n_log_notebook, bg=BLACK)
n_log_notebook.add(n_op_frame, text="Operations Log")
n_log_notebook.add(n_err_frame, text="Error Log")
# Operations log with auto-hiding scrollbar
n_op_scroll = tk.Scrollbar(n_op_frame)
n_op_text = tk.Text(n_op_frame, height=7, width=80, font=FONT, fg=GOLD, bg=BLACK, insertbackground=GOLD, highlightbackground=GOLD, highlightcolor=GOLD, borderwidth=2, relief="groove", state="disabled", yscrollcommand=n_op_scroll.set, wrap="word")
n_op_scroll.config(command=n_op_text.yview)
n_op_text.pack(side="left", fill="both", expand=True)
# Error log with auto-hiding scrollbar
n_err_scroll = tk.Scrollbar(n_err_frame)
n_err_text = tk.Text(n_err_frame, height=7, width=80, font=FONT, fg=GOLD, bg=BLACK, insertbackground=GOLD, highlightbackground=GOLD, highlightcolor=GOLD, borderwidth=2, relief="groove", state="disabled", yscrollcommand=n_err_scroll.set, wrap="word")
n_err_scroll.config(command=n_err_text.yview)
n_err_text.pack(side="left", fill="both", expand=True)
def n_show_scrollbar_if_needed(text_widget, scrollbar):
text_widget.update_idletasks()
if text_widget.yview()[0] > 0 or text_widget.yview()[1] < 1:
scrollbar.pack(side="right", fill="y")
else:
scrollbar.pack_forget()
def n_update_op_log(*args):
n_op_text.config(state="normal")
n_op_text.delete(1.0, tk.END)
n_op_text.insert(tk.END, operations_log_var.get())
n_op_text.see(tk.END)
n_op_text.config(state="disabled")
n_show_scrollbar_if_needed(n_op_text, n_op_scroll)
def n_update_err_log(*args):
n_err_text.config(state="normal")
n_err_text.delete(1.0, tk.END)
n_err_text.insert(tk.END, error_log_var.get())
n_err_text.see(tk.END)
n_err_text.config(state="disabled")
n_show_scrollbar_if_needed(n_err_text, n_err_scroll)
operations_log_var.trace_add('write', n_update_op_log)
error_log_var.trace_add('write', n_update_err_log)
n_op_text.bind('<Configure>', lambda e: n_show_scrollbar_if_needed(n_op_text, n_op_scroll))
n_err_text.bind('<Configure>', lambda e: n_show_scrollbar_if_needed(n_err_text, n_err_scroll))
# Buttons
n_btn_frame = tk.Frame(network_frame, bg=BLACK)
n_btn_frame.pack(pady=(10, 18))
n_run_btn = ttk.Button(n_btn_frame, text="Run Batch", command=start_batch)
n_run_btn.pack(side="left", padx=12)
n_start_over_btn = ttk.Button(n_btn_frame, text="Start Over", command=start_over)
n_start_over_btn.pack(side="left", padx=12)
n_cancel_btn = ttk.Button(n_btn_frame, text="Cancel", command=cancel_batch, state="disabled")
n_cancel_btn.pack(side="left", padx=12)
root.mainloop()
if __name__ == "__main__":
main()

86
list.txt Normal file
View File

@ -0,0 +1,86 @@
PDP01_FIO1
PDP01_FIOH1
PDP06_FIO1
PDP06_FIOH1
PS1_1_VFD
PS1_2_FIO1
PS1_2_VFD
PS1_3_VFD
PS1_4_VFD
PS1_5_VFD
PS2_1_VFD
PS2_2_VFD
PS2_3_VFD
PS2_4_VFD
PS2_5_VFD
PS2_6_VFD
UL1_3_SIO1
UL1_3_VFD
UL1_4_VFD
UL1_5_VFD
UL1_6_VFD
UL1_7_VFD
UL1_8_VFD
UL1_9_VFD
UL1_10_FIO1
UL1_10_VFD
UL1_11A_VFD
UL1_11B_VFD
UL1_12_VFD
UL1_13_VFD
UL2_3_SIO1
UL2_3_VFD
UL2_4_VFD
UL2_5_VFD
UL2_6_VFD
UL2_7_VFD
UL2_8_VFD
UL2_9_VFD
UL2_10_VFD
UL2_11_VFD
UL3_1_VFD
UL3_2_FIO1
UL3_2_VFD
UL3_3_VFD
UL3_4_VFD
UL3_5_VFD
UL3_6_VFD
UL3_7_VFD
UL3_8_VFD
UL3_9_VFD
UL3_10_VFD
UL4_3_SIO1
UL4_3_VFD
UL4_4_VFD
UL4_5_VFD
UL4_6_VFD
UL4_7_VFD
UL4_8_VFD
UL4_9_VFD
UL4_10_FIO1
UL4_10_VFD
UL4_11A_VFD
UL4_11B_VFD
UL4_12_VFD
UL4_13_VFD
UL5_3_SIO1
UL5_3_VFD
UL5_4_VFD
UL5_5_VFD
UL5_6_VFD
UL5_7_VFD
UL5_8_VFD
UL5_9_VFD
UL5_10_VFD
UL5_11_VFD
UL6_1_VFD
UL6_2_FIO1
UL6_2_VFD
UL6_3_VFD
UL6_4_VFD
UL6_5_VFD
UL6_6_VFD
UL6_7_VFD
UL6_8_VFD
UL6_9_VFD
UL6_10_VFD