TL;DR
Deze Python-app geeft je een snelle, point-and-click manier om metadata van Autodesk Inventor-bestanden (.ipt
, .iam
, .idw
, .dwg
) bulkmatig te bekijken en te bewerken. Het:
- Scant een map op Inventor-bestanden
- Leest kernvelden (Part Number, Title, Subject, Comments, Revision, Date Checked)
- Laat je de meeste velden inline bewerken in een tabel-UI (Dear PyGui)
- Schrijft updates terug naar de native Inventor-bestanden via COM
- Update
Date Checked
automatisch wanneer er iets wijzigt - Exporteert een Markdown “Part Registry”-tabel voor je notities/wiki
Inventor-automatisering draait veilig in een dedicated COM-thread, zodat de UI vlot blijft en niet vastloopt.
Welke problemen dit oplost
- Handmatig eigenschappen aanpassen is traag: Bestanden één voor één in Inventor openen is pijnlijk.
- Inconsistente metadata: Revision/Title/etc. lopen na verloop van tijd uiteen.
- Statusrapportage: Je wilt een nette Markdown-tabel om in docs of Notion/Wiki te plakken. Deze tool stopt dat allemaal in één, vriendelijke window.
Vereisten (de “ja, het is Windows”-sectie)
- Windows (COM +
win32com.client
) - Autodesk Inventor geïnstalleerd (zodat COM-automatisering werkt)
- Python-pakketten:
dearpygui
(UI)pywin32
(win32com.client
+pythoncom
)tkinter
(meestal ingebouwd in Python op Windows; gebruikt voor schermgrootte en bestandsdialoog)
Architectuur op hoofdlijnen
- UI (Dear PyGui): Toont een tabel voor inline bewerken, mapselectie en knoppen (Reload, Apply Changes, Close).
- COM-werkthread: Draait Inventor-automatisering in een eigen STA (single-threaded COM-apartment). De UI post jobs via een thread-veilige queue; resultaten komen terug via
concurrent.futures.Future
. - Bestand/metadata-laag: Opent elk bestand via Inventor, leest/schrijft property sets en sluit netjes af.
- Markdown-export: Serialiseert de huidige in-memory rijen naar een mooie Git-vriendelijke tabel.
Configuratieknoppen (boven in het bestand)
FOLDER
: Standaard scanlocatie voor CAD-bestanden.MD_OUT
: Waar het Markdown-register wordt weggeschreven.EXTS
: Welke bestandstypen worden meegenomen.COLUMNS
: Weergavevolgorde van de tabelkolommen.EDITABLE
: Welke kolommen de gebruiker in de UI kan wijzigen. Pas deze aan voor jouw projectstructuur of voeg extra properties toe.
De Inventor-COM-helpers (lezen/schrijven van properties)
Inventor openen
def open_inventor(): inv = win32.Dispatch("Inventor.Application") inv.Visible = True return inv
We starten (of verbinden met) Inventor en maken het venster zichtbaar. Zichtbaarheid helpt bij debuggen en maakt COM vaak voorspelbaarder.
Properties lezen
def get_prop(doc, set_name, prop_name): # Geeft "" terug als de property ontbreekt
Zoekt een property binnen een Property Set (bijv. "Inventor Summary Information"
, "Design Tracking Properties"
). Alle reads zijn safe: fouten worden lege strings, zodat een ontbrekende property de run niet laat crashen.
Properties schrijven
def set_prop(doc, set_name, prop_name, value): # Doet stilletjes niets bij falen
Werkt een property bij als die bestaat. Zo niet, dan faalt het geruisloos (bewust—geen harde stop in batchjobs).
Metadata van één bestand lezen
def read_metadata(inv, path: Path): ...
- Opent het document via Inventor (
Documents.Open
) zonder het venster te activeren - Leest: Revision, Title, Subject, Part Number, Comments, Date Checked
- Normaliseert
Date Checked
naarYYYY-MM-DD
wanneer mogelijk - Sluit het document altijd in een
finally
-blok
Updates toepassen
def apply_updates(inv, rows): ...
Voor elke rij:
- Open het document
- Vergelijk elke bewerkbare property; schrijf alleen bij verschil
- Als er iets is gewijzigd, zet
"Date Checked"
op vandaag (YYYY-MM-DD
) - Sla op en sluit
Datamodel: wat is een “rij”?
Een rij is een eenvoudige dict zoals:
{ "FilePath": "D:/.../part.ipt", "FileName": "part.ipt", "PartNumber": "OMG-001", "Title": "Handlebar Clamp", "Subject": "Front assembly", "Comments": "Updated for Rev B", "Revision": "B", "DateChecked": "2025-09-23", }
Deze sturen zowel de UI-tabel als de Markdown-export.
De UI (Dear PyGui)
Tabelopmaak
Kolommen krijgen vaste beginbreedtes voor leesbaarheid:
- FileName (alleen-lezen)
- PartNumber (bewerkbaar)
- Title (bewerkbaar)
- Subject (bewerkbaar)
- Comments (bewerkbaar)
- Revision (bewerkbaar via een button)
- DateChecked (alleen-lezen, automatisch beheerd; weergegeven als button voor rechts uitlijnen)
Waarom buttons voor
Revision
enDateChecked
?
Omdat je in Dear PyGui tekst makkelijk rechts kunt uitlijnen met een custom theme op buttons. Voor consistentie renderen zowel bewerkbare als read-only cellen in die kolommen via buttons die aanright_align_theme
zijn gebonden. VoorRevision
gebeurt het echte bewerken via inline input (de button-click toont nu enkel een statushint dat het veld auto-managed is; de daadwerkelijke waarde wordt bijgewerkt door Apply).
Inline bewerkgedrag
- Voor tekstvelden (PartNumber, Title, Subject, Comments) bewerk je direct in de tabel.
- De statusregel vertelt wat er veranderde.
Revision
enDateChecked
zijn automatisch beheerd:Revision
is bewerkbaar in de tabel via de input (de button is hier puur UI-aankleding).Date Checked
wordt automatisch gezet wanneer tijdens Apply een property wijzigt.
Mapselectie & herladen
- “Browse” opent een mapdialoog; “Reload” scant en vult de tabel opnieuw door de COM-worker
load_rows
te laten uitvoeren.
Apply & Save
- Apply Changes:
- Stuurt de huidige in-memory rijen naar de COM-worker, die wijzigingen naar elk bestand schrijft.
- Laadt opnieuw vanaf schijf (zodat je de definitieve waarheid ziet).
- Exporteert automatisch Markdown naar
MD_OUT
.
- Close: Stopt de Dear PyGui-loop en start de cleanup.
De COM-worker (waarom een aparte thread?)
COM + GUI op dezelfde thread is een recept voor deadlocks en “Application Not Responding”. Dus we:
- Starten een daemon-thread (
COMWorker
) - Initialiseren COM expliciet met
pythoncom.CoInitialize()
binnen die thread - Maken/ openen de Inventor-applicatie daar één keer
- Wisselen werk uit via een
queue.Queue()
met(op, args, future)
- Ondersteunen twee operaties:
"load_rows"
→ scant map en leest metadata"apply_updates"
→ schrijft wijzigingen terug naar bestanden
- Sluiten Inventor netjes met
Quit()
en doenCoUninitialize()
bij shutdown De UI-thread raakt COM nooit direct aan—alleen de worker. Deze scheiding houdt de app responsief en COM-correct.
Markdown-export
def export_markdown(rows, md_path): ...
- Maakt de bovenliggende map aan indien nodig
- Escapet pipes en nieuwe regels, zodat de tabel correct rendert
- Schrijft een simpele Git-vriendelijke tabel die je in documentatie kunt plakken
Voorbeeldkop:
| File Name | Part Number | Title | Subject | Comments | Revision | Date Checked | |---|---|---|---|---|---|---|
Opstart- & window-handigheidjes
- Gebruikt
tkinter
om schermgrootte te detecteren en het venster gecentreerd op 50% van breedte/hoogte te tonen (comment zegt 80%, code geeft nu 50%). - Haalt window-chrome weg (
no_title_bar=True
,no_resize=True
,no_move=True
) voor een “panel”-gevoel. - Custom theme lijnt de kolommen Revision en Date Checked rechts uit met transparante buttons.
Let op: In de docstring staat “Neem 80% van schermgrootte”, maar de code vermenigvuldigt met
0.5
. Pas aan als je echt 80% wilt.
Datastroom (end-to-end)
- App starten → COM-worker starten → Optioneel standaard
FOLDER
preloaden. - Reload → Worker leest bestanden → UI-tabel ververst.
- Inline bewerken → Rijen wijzigen in memory.
- Apply Changes → Worker schrijft naar Inventor, werkt
Date Checked
bij, slaat op → Herlaadt vanaf schijf → Exporteert Markdown. - Close → Schone shutdown van worker + COM.
Foutafhandelingsfilosofie
- Zachte fouten:
get_prop
/set_prop
slikken de meeste property-fouten in zodat een ontbrekend veld je batch niet omver blaast. - Altijd documenten sluiten:
finally
-blokken proberen hard omClose(True)
te doen om file locks te voorkomen. - Statusregel: Menselijke feedback bij de meeste gebruikersacties.
- Worker-exceptions: Komen via
Future
-resultaten naar boven; de gebruiker ziet een compacte melding in de statusbalk.
Tips voor maatwerk
- Nieuwe kolom/property toevoegen
- Voeg de naam toe aan
COLUMNS
. - Wil je ’m bewerkbaar, voeg toe aan
EDITABLE
. - Update
read_metadata
om ’m op te halen (kies de juiste Property Set + naam). - Update
apply_updates
om ’m terug te schrijven bij wijzigingen. - (Optioneel) Voeg toe aan de exportvolgorde voor Markdown.
- Voeg de naam toe aan
- Bestandstypen wijzigen
- Pas
EXTS
aan om extensies toe te voegen/te verwijderen.
- Pas
- Standaardmap & output
- Zet
FOLDER
enMD_OUT
passend bij jouw projectstructuur.
- Zet
- Uitlijning / breedtes
- Tweak
init_width_or_weight
per kolom voor jouw data.
- Tweak
Bekende beperkingen / aandachtspunten
- Inventor moet geïnstalleerd en gelicenseerd zijn op de machine die dit draait.
- Property-bestaan: Sommige bestanden missen een property of set—writes zijn best-effort en stil bij falen.
- Datum-parsing:
Date Checked
wordt genormaliseerd als het eruitziet alsYYYY-MM-DD ...
; anders blijft de ruwe waarde behouden. - UI-bewerking van Revision: De rechts uitgelijnde button is niet de invoer; de invoer is het aangrenzende veld. De button-click meldt enkel dat het veld automatisch wordt beheerd. (Je kunt dit ombouwen naar echte input als je wilt.)
- Performance: Héél grote mappen betekenen veel COM open/close-cycli; overweeg filteren of batchen.
Snelstart
- Installeer dependencies:
pip install dearpygui pywin32
- Zet
FOLDER
enMD_OUT
bovenaan het script. - Run:
python inventor_properties_editor.py
- Klik Reload om bestanden te laden.
- Bewerk velden inline.
- Klik Apply Changes. Je bestanden worden bijgewerkt en het Markdown-register wordt geschreven.
FAQ
V: Waarom niet properties bewerken zonder elk bestand te openen?
A: Inventor stelt properties via zijn COM-API beschikbaar op geopende documenten. We openen onzichtbaar en sluiten snel, wat de ondersteunde route is.
V: Kan ik Inventor verborgen houden?
A: Je kunt inv.Visible = False
zetten, maar zichtbaar is handig voor troubleshooting en stabiliseert COM soms.
V: Kan ik custom iProperties toevoegen?
A: Ja—breid read_metadata
/ apply_updates
uit met de juiste Property Set en naam. Veelgebruikte sets:
"Inventor Summary Information"
"Design Tracking Properties"
- Je eigen setnamen, indien aanwezig
V: Kan ik het automatisch bijwerken van Date Checked uitschakelen?
A: Verwijder of pas deproperties_changed
-logica aan inapply_updates
.
Toekomstige nice-to-haves
- Filter/zoekbalk voor grote assemblies
- Voortgangsbalk tijdens Apply
- Multi-map-ondersteuning en recursie
- CSV-export/import
- Diff-preview (highlight wat zal veranderen vóór Apply)
Slotopmerking
De kern van dit script is het scheiden van UI en COM. Door de COM-worker eigenaar te maken van Inventor en UI-edits te behandelen als row diffs, krijg je een soepele editor die netjes omgaat met Inventors threading-model. De Markdown-export is de kers op de taart voor documentatiehygiëne.
Full script
import os
from pathlib import Path
from datetime import datetime
import dearpygui.dearpygui as dpg
import win32com.client as win32
import pythoncom
from threading import local, Thread
from concurrent.futures import Future
import queue
import tkinter as tk
# --- CONFIG ---
FOLDER = r"D:\OneDrive\001. PROJECTEN\000. KUBUZ\OMG-Bikes\CAD"
MD_OUT = r"D:\OneDrive\005. NOTES\OMG Bikes\Part Registry.md"
EXTS = {".ipt", ".iam", ".idw", ".dwg"}
COLUMNS = ["FileName", "PartNumber", "Title", "Subject", "Comments", "Revision", "DateChecked"]
EDITABLE = {"PartNumber", "Title", "Subject", "Comments", "Revision"} # which columns can be edited
# --- Inventor helpers ---
def open_inventor():
inv = win32.Dispatch("Inventor.Application")
inv.Visible = True
return inv
def get_prop(doc, set_name, prop_name):
try:
return str(doc.PropertySets.Item(set_name).Item(prop_name).Value)
except:
return ""
def set_prop(doc, set_name, prop_name, value):
try:
doc.PropertySets.Item(set_name).Item(prop_name).Value = value
except:
pass
def read_metadata(inv, path: Path):
doc = None
try:
doc = inv.Documents.Open(str(path), False)
rev = get_prop(doc, "Inventor Summary Information", "Revision Number")
title = get_prop(doc, "Inventor Summary Information", "Title")
subject = get_prop(doc, "Inventor Summary Information", "Subject")
partnum = get_prop(doc, "Design Tracking Properties", "Part Number")
comments = get_prop(doc, "Inventor Summary Information", "Comments")
date_checked_raw = get_prop(doc, "Design Tracking Properties", "Date Checked")
# Format date to YYYY-MM-DD only
try:
if date_checked_raw:
# Try to parse and reformat the date
date_obj = datetime.strptime(date_checked_raw.split()[0], "%Y-%m-%d")
date_checked = date_obj.strftime("%Y-%m-%d")
else:
date_checked = ""
except:
# If parsing fails, try to extract just the date part
if date_checked_raw and " " in date_checked_raw:
date_checked = date_checked_raw.split()[0]
else:
date_checked = date_checked_raw or ""
return {
"FilePath": str(path),
"FileName": path.name,
"PartNumber": partnum,
"Title": title,
"Subject": subject,
"Comments": comments,
"Revision": rev,
"DateChecked": date_checked,
}
finally:
try:
if doc is not None: doc.Close(True)
except:
pass
def apply_updates(inv, rows):
current_date = datetime.now().strftime("%Y-%m-%d")
for r in rows:
p = Path(r["FilePath"])
if not p.exists():
continue
doc = None
try:
doc = inv.Documents.Open(str(p), False)
# Track if any properties changed to update Date Checked
properties_changed = False
# Props
if r["Revision"] != get_prop(doc, "Inventor Summary Information", "Revision Number"):
set_prop(doc, "Inventor Summary Information", "Revision Number", r["Revision"])
properties_changed = True
if r["Title"] != get_prop(doc, "Inventor Summary Information", "Title"):
set_prop(doc, "Inventor Summary Information", "Title", r["Title"])
properties_changed = True
if r["Subject"] != get_prop(doc, "Inventor Summary Information", "Subject"):
set_prop(doc, "Inventor Summary Information", "Subject", r["Subject"])
properties_changed = True
if r["PartNumber"] != get_prop(doc, "Design Tracking Properties", "Part Number"):
set_prop(doc, "Design Tracking Properties", "Part Number", r["PartNumber"])
properties_changed = True
if r["Comments"] != get_prop(doc, "Inventor Summary Information", "Comments"):
set_prop(doc, "Inventor Summary Information", "Comments", r["Comments"])
properties_changed = True
# Update Date Checked if any properties changed
if properties_changed:
set_prop(doc, "Design Tracking Properties", "Date Checked", current_date)
doc.Save()
finally:
try:
if doc is not None: doc.Close(True)
except:
pass
def export_markdown(rows, md_path):
os.makedirs(Path(md_path).parent, exist_ok=True)
def esc(s):
s = "" if s is None else str(s)
return s.replace("|", "\\|").replace("\r", " ").replace("\n", " ")
lines = []
lines.append("| File Name | Part Number | Title | Subject | Comments | Revision | Date Checked |")
lines.append("|---|---|---|---|---|---|---|")
for r in rows:
lines.append("| " + " | ".join([
esc(r["FileName"]),
esc(r["PartNumber"]),
esc(r["Title"]),
esc(r["Subject"]),
esc(r["Comments"]),
esc(r["Revision"]),
esc(r["DateChecked"]),
]) + " |")
Path(md_path).write_text("\n".join(lines), encoding="utf-8")
# --- Data ---
def list_inv_files(folder):
return [p for p in Path(folder).glob("*") if p.is_file() and p.suffix.lower() in EXTS]
def load_rows(inv, folder):
files = list_inv_files(folder)
return [read_metadata(inv, p) for p in files]
# --- DPG UI Helpers ---
DPG_TAGS = {
"folder_input": "-FOLDER-",
"folder_dialog": "-FOLDER_DIALOG-",
"table": "-TABLE-",
"status": "-STATUS-",
"edit_FileName": "-FileName-",
"edit_PartNumber": "-PartNumber-",
"edit_Title": "-Title-",
"edit_Subject": "-Subject-",
"edit_Comments": "-Comments-",
"edit_Revision": "-Revision-",
"edit_DateChecked": "-DateChecked-",
}
_rows_state = {
"rows": [],
"selected_index": None,
"worker": None,
}
_thread_local = local()
def _ensure_com_initialized():
try:
if not getattr(_thread_local, "com_initialized", False):
pythoncom.CoInitialize()
_thread_local.com_initialized = True
except Exception:
pass
class COMWorker:
def __init__(self):
self._q = queue.Queue()
self._thread = Thread(target=self._run, daemon=True)
self._started = False
def start(self):
if not self._started:
self._thread.start()
self._started = True
def call(self, op, *args):
fut = Future()
self._q.put((op, args, fut))
return fut.result()
def stop(self):
fut = Future()
try:
self._q.put(("_stop", (), fut))
fut.result(timeout=5)
except Exception:
pass
def _run(self):
inv = None
pythoncom.CoInitialize()
try:
inv = open_inventor()
while True:
op, args, fut = self._q.get()
if op == "_stop":
try:
fut.set_result(True)
except Exception:
pass
break
try:
if op == "load_rows":
folder, = args
res = load_rows(inv, folder)
fut.set_result(res)
elif op == "apply_updates":
rows, = args
apply_updates(inv, rows)
fut.set_result(True)
else:
fut.set_exception(RuntimeError(f"Unknown op: {op}"))
except Exception as ex:
try:
fut.set_exception(ex)
except Exception:
pass
except Exception:
# Swallow worker-level failures; they will surface via Future exceptions
pass
finally:
try:
if inv is not None:
inv.Quit()
except Exception:
pass
try:
pythoncom.CoUninitialize()
except Exception:
pass
def _set_status(text):
try:
dpg.set_value(DPG_TAGS["status"], text)
except Exception:
pass
def _populate_edit_fields(row_dict):
dpg.set_value(DPG_TAGS["edit_FileName"], row_dict.get("FileName", ""))
for c in EDITABLE:
dpg.set_value(DPG_TAGS[f"edit_{c}"], row_dict.get(c, ""))
def _get_edit_values(base_row):
updated = dict(base_row)
for c in EDITABLE:
updated[c] = dpg.get_value(DPG_TAGS[f"edit_{c}"]) or base_row.get(c, "")
return updated
def _rebuild_table():
table_tag = DPG_TAGS["table"]
# clear existing rows
children = dpg.get_item_children(table_tag, 1) or []
for child in children:
dpg.delete_item(child)
# add rows
for idx, r in enumerate(_rows_state["rows"]):
with dpg.table_row(parent=table_tag):
# FileName (read-only)
dpg.add_text(r.get("FileName", ""))
# Other columns: make editable where allowed
for c in COLUMNS[1:]:
if c in EDITABLE:
# Right align Revision and DateChecked columns using buttons
if c in ["Revision", "DateChecked"]:
# Use button with transparent background for right alignment
dpg.add_button(label=r.get(c, ""), width=-1, callback=_on_cell_edit, user_data=(idx, c))
dpg.bind_item_theme(dpg.last_item(), "right_align_theme")
else:
dpg.add_input_text(default_value=r.get(c, ""), callback=_on_cell_edit, user_data=(idx, c), width=-1)
else:
# Right align Revision and DateChecked columns for read-only text too
if c in ["Revision", "DateChecked"]:
# Use button with transparent background for right alignment
dpg.add_button(label=r.get(c, ""), width=-1)
dpg.bind_item_theme(dpg.last_item(), "right_align_theme")
else:
dpg.add_text(r.get(c, ""))
def _on_row_select(sender, app_data, user_data):
try:
idx = int(user_data)
except Exception:
return
if idx < 0 or idx >= len(_rows_state["rows"]):
return
_rows_state["selected_index"] = idx
_populate_edit_fields(_rows_state["rows"][idx])
_set_status(f"Selected row {idx+1} of {len(_rows_state['rows'])}")
def _on_cell_edit(sender, app_data, user_data):
# For buttons, app_data is the button label (current value)
# For input text, app_data is the new text value
try:
idx, col = user_data
except Exception:
return
if idx < 0 or idx >= len(_rows_state["rows"]):
return
if col not in EDITABLE:
return
# If it's a button (Revision or DateChecked), we need to handle it differently
if col in ["Revision", "DateChecked"]:
# For now, just show a message that these fields are auto-managed
_set_status(f"Row {idx+1} - {col} is auto-managed")
else:
# For input text fields
_rows_state["rows"][idx][col] = app_data or ""
_set_status(f"Edited row {idx+1} - {col}")
def _on_browse_folder():
dpg.configure_item(DPG_TAGS["folder_dialog"], show=True)
def _on_folder_chosen(sender, app_data):
# app_data contains selections dict: {"file_path_name": path}
try:
folder = app_data.get("file_path_name", "")
except Exception:
folder = ""
if folder:
dpg.set_value(DPG_TAGS["folder_input"], folder)
def _on_reload():
folder = dpg.get_value(DPG_TAGS["folder_input"]) or FOLDER
if not Path(folder).exists():
_set_status(f"Folder not found: {folder}")
return
try:
rows = _rows_state["worker"].call("load_rows", folder)
_rows_state["rows"] = rows
_rows_state["selected_index"] = None
_rebuild_table()
_set_status(f"Loaded {len(_rows_state['rows'])} files from {folder}")
except Exception as ex:
_set_status(f"Reload failed: {ex}")
def _on_update_row():
# No longer needed with inline editing, keep as no-op for compatibility
_set_status("Rows are edited directly in the table.")
def _on_apply_changes():
try:
_rows_state["worker"].call("apply_updates", _rows_state["rows"])
# Refresh rows from disk
folder = dpg.get_value(DPG_TAGS["folder_input"]) or FOLDER
_rows_state["rows"] = _rows_state["worker"].call("load_rows", folder)
_rows_state["selected_index"] = None
_rebuild_table()
# Automatically save markdown after applying changes
try:
export_markdown(_rows_state["rows"], MD_OUT)
_set_status("Changes applied to Inventor files and markdown saved.")
except Exception as ex:
_set_status(f"Changes applied to Inventor files, but markdown save failed: {ex}")
except Exception as ex:
_set_status(f"Apply failed: {ex}")
def _on_save_markdown():
try:
export_markdown(_rows_state["rows"], MD_OUT)
_set_status(f"Markdown saved: {MD_OUT}")
except Exception as ex:
_set_status(f"Save Markdown failed: {ex}")
def _on_exit():
try:
if _rows_state.get("worker") is not None:
_rows_state["worker"].stop()
except Exception:
pass
try:
pythoncom.CoUninitialize()
except Exception:
pass
def get_screen_size():
"""Get screen dimensions and return 80% of screen size"""
root = tk.Tk()
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
root.destroy()
# Return 50% of screen size
return int(screen_width * 0.5), int(screen_height * 0.5)
def center_window(window_width, window_height):
"""Calculate position to center window on screen"""
root = tk.Tk()
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
root.destroy()
# Calculate center position
x = (screen_width - window_width) // 2
y = (screen_height - window_height) // 2
return x, y
def main():
# Start COM worker thread that owns Inventor COM apartment
worker = COMWorker()
worker.start()
_rows_state["worker"] = worker
# Preload
if Path(FOLDER).exists():
try:
_rows_state["rows"] = worker.call("load_rows", FOLDER)
except Exception as ex:
_set_status(f"Initial load failed: {ex}")
else:
_set_status(f"Folder not found: {FOLDER}")
dpg.create_context()
# Create theme for right-aligned buttons
with dpg.theme(tag="right_align_theme"):
with dpg.theme_component(dpg.mvButton):
dpg.add_theme_style(dpg.mvStyleVar_ButtonTextAlign, 1.0, 0.5) # Right-align text
dpg.add_theme_color(dpg.mvThemeCol_Button, (0, 0, 0, 0)) # Transparent background
dpg.add_theme_color(dpg.mvThemeCol_ButtonActive, (0, 0, 0, 0))
dpg.add_theme_color(dpg.mvThemeCol_ButtonHovered, (0, 0, 0, 0))
# Get 80% of screen size and center position
window_width, window_height = get_screen_size()
window_x, window_y = center_window(window_width, window_height)
with dpg.window(label="Inventor Properties Editor", width=window_width, height=window_height, no_collapse=True, no_title_bar=True, no_resize=True, no_move=True) as main_window:
with dpg.group(horizontal=True):
dpg.add_text("Folder:")
dpg.add_input_text(tag=DPG_TAGS["folder_input"], default_value=FOLDER, width=700)
dpg.add_button(label="Browse", callback=_on_browse_folder)
dpg.add_button(label="Reload", callback=_on_reload)
dpg.add_spacer(height=5)
with dpg.table(tag=DPG_TAGS["table"], header_row=True, borders_innerH=True, borders_innerV=True, borders_outerH=True, borders_outerV=True, resizable=True, policy=dpg.mvTable_SizingFixedFit, scrollY=True, height=600):
# columns with specific widths
dpg.add_table_column(label="FileName", init_width_or_weight=150)
dpg.add_table_column(label="PartNumber", init_width_or_weight=150)
dpg.add_table_column(label="Title", init_width_or_weight=300)
dpg.add_table_column(label="Subject", init_width_or_weight=200)
dpg.add_table_column(label="Comments", init_width_or_weight=100)
dpg.add_table_column(label="Revision", init_width_or_weight=70)
dpg.add_table_column(label="DateChecked", init_width_or_weight=130)
dpg.add_spacer(height=5)
with dpg.group(horizontal=True):
dpg.add_button(label="Apply Changes", callback=_on_apply_changes)
dpg.add_button(label="Close", callback=lambda: dpg.stop_dearpygui())
dpg.add_spacer(height=5)
dpg.add_text("", tag=DPG_TAGS["status"]) # status line
# Folder dialog
with dpg.file_dialog(directory_selector=True, show=False, callback=_on_folder_chosen, tag=DPG_TAGS["folder_dialog"], width=700, height=400):
dpg.add_file_extension(".*", color=(255, 255, 255, 255))
dpg.create_viewport(title="Inventor Properties Editor", width=window_width, height=window_height, x_pos=window_x, y_pos=window_y)
dpg.setup_dearpygui()
dpg.set_exit_callback(_on_exit)
dpg.show_viewport()
dpg.set_primary_window(main_window, True)
# Populate table and clear edit fields
_rebuild_table()
if _rows_state["rows"]:
_set_status(f"Loaded {len(_rows_state['rows'])} files from {FOLDER}")
dpg.start_dearpygui()
dpg.destroy_context()
if __name__ == "__main__":
main()