Initial commit (python only)
This commit is contained in:
commit
a73a12859b
3 changed files with 2568 additions and 0 deletions
518
cods/GUI.py
Normal file
518
cods/GUI.py
Normal file
|
|
@ -0,0 +1,518 @@
|
|||
# GUI.py
|
||||
import threading
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, filedialog, messagebox
|
||||
from pathlib import Path
|
||||
|
||||
# =========================
|
||||
# Backend (robusto)
|
||||
# =========================
|
||||
try:
|
||||
import S_Langmuir as backend
|
||||
except Exception as e:
|
||||
backend = None
|
||||
_BACKEND_IMPORT_ERROR = str(e)
|
||||
|
||||
def _get_backend_attr(name: str):
|
||||
if backend is None:
|
||||
return None
|
||||
return getattr(backend, name, None)
|
||||
|
||||
run_full_catodos_tokens = _get_backend_attr("run_full_catodos_tokens")
|
||||
run_full_hilos_tokens = _get_backend_attr("run_full_hilos_tokens")
|
||||
run_check_ionico = _get_backend_attr("run_check_ionico")
|
||||
set_sonda = _get_backend_attr("set_sonda")
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# Helpers (agrupación tokens)
|
||||
# ---------------------------
|
||||
|
||||
def parse_token_map(s: str) -> dict[str, int]:
|
||||
s = (s or "").strip()
|
||||
if not s:
|
||||
return {}
|
||||
out: dict[str, int] = {}
|
||||
for chunk in s.split(","):
|
||||
chunk = chunk.strip()
|
||||
if not chunk:
|
||||
continue
|
||||
if "=" not in chunk:
|
||||
raise ValueError(f"Formato token_map inválido: '{chunk}'. Usa campo=indice")
|
||||
k, v = chunk.split("=", 1)
|
||||
k = k.strip()
|
||||
v = v.strip()
|
||||
if not k:
|
||||
raise ValueError("Hay un campo vacío en token_map.")
|
||||
out[k] = int(v)
|
||||
return out
|
||||
|
||||
def parse_order(s: str) -> list[str]:
|
||||
s = (s or "").strip()
|
||||
if not s:
|
||||
return []
|
||||
return [x.strip() for x in s.split(",") if x.strip()]
|
||||
|
||||
def parse_group_tokens(filename: str, delimiter: str, token_map: dict[str, int], order: list[str]) -> str:
|
||||
stem = Path(filename).stem
|
||||
parts = [p.strip() for p in stem.split(delimiter) if p.strip() != ""]
|
||||
|
||||
values = {}
|
||||
for k, idx in token_map.items():
|
||||
values[k] = parts[idx] if 0 <= idx < len(parts) else "NA"
|
||||
|
||||
folders = []
|
||||
for k in order:
|
||||
v = str(values.get(k, "NA")).strip().replace("\\", "-").replace("/", "-")
|
||||
folders.append(v if v else "NA")
|
||||
|
||||
return "/".join(folders) if folders else "otros"
|
||||
|
||||
def _has_na(group: str) -> bool:
|
||||
if not group:
|
||||
return False
|
||||
return ("NA/" in group) or ("/NA" in group) or group.startswith("NA") or group.endswith("/NA")
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# GUI
|
||||
# ---------------------------
|
||||
|
||||
class App(tk.Tk):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.title("IV Processor")
|
||||
self.geometry("1100x760")
|
||||
self.minsize(1000, 680)
|
||||
|
||||
if backend is None:
|
||||
messagebox.showerror(
|
||||
"Backend no disponible",
|
||||
"No se ha podido importar S_Langmuir.py.\n\n"
|
||||
f"Error:\n{_BACKEND_IMPORT_ERROR}\n\n"
|
||||
"Asegúrate de que GUI.py y S_Langmuir.py están en la misma carpeta."
|
||||
)
|
||||
|
||||
# Estado
|
||||
self.mode = tk.StringVar(value="catodo")
|
||||
self.folder = tk.StringVar(value="")
|
||||
self.status = tk.StringVar(value="Listo.")
|
||||
self.busy = tk.BooleanVar(value=False)
|
||||
|
||||
# ✅ Check iónico integrado (sin botón)
|
||||
self.do_ionico = tk.BooleanVar(value=False)
|
||||
|
||||
# Warning banner
|
||||
self.warn = tk.StringVar(value="")
|
||||
self._preview_summary = {"bad": 0, "total": 0}
|
||||
|
||||
# --------- CATODOS (TOKENS) ---------
|
||||
self.cat_delim = tk.StringVar(value="_")
|
||||
self.cat_token_map = tk.StringVar(value="tipo=0,presion=1,vcat=2,pol=3")
|
||||
self.cat_order = tk.StringVar(value="tipo,presion,vcat,pol")
|
||||
|
||||
# --------- HILOS (TOKENS) -----------
|
||||
self.hil_delim = tk.StringVar(value="_")
|
||||
self.hil_token_map = tk.StringVar(value="tipo=0,ich=1,vcp=2")
|
||||
self.hil_order = tk.StringVar(value="tipo,vcp,ich")
|
||||
|
||||
# --------- SONDA (GUI) --------------
|
||||
self.probe_geom = tk.StringVar(value="cilindrica")
|
||||
self.probe_r_mm = tk.StringVar(value="0.445")
|
||||
self.probe_L_mm = tk.StringVar(value="22.59")
|
||||
|
||||
self._preview_job = None
|
||||
|
||||
self._build()
|
||||
self._wire_traces()
|
||||
self._refresh_fields(initial=True)
|
||||
self._refresh_probe_fields()
|
||||
self._refresh_backend_state()
|
||||
|
||||
|
||||
# ---------------- UI build ----------------
|
||||
|
||||
def _build(self):
|
||||
pad = {"padx": 10, "pady": 6}
|
||||
|
||||
# Mode
|
||||
f_mode = ttk.LabelFrame(self, text="Modo")
|
||||
f_mode.pack(fill="x", **pad)
|
||||
|
||||
ttk.Radiobutton(
|
||||
f_mode, text="Cátodos", variable=self.mode, value="catodo",
|
||||
command=self._refresh_fields
|
||||
).pack(side="left", padx=10, pady=8)
|
||||
|
||||
ttk.Radiobutton(
|
||||
f_mode, text="Hilos", variable=self.mode, value="hilos",
|
||||
command=self._refresh_fields
|
||||
).pack(side="left", padx=10, pady=8)
|
||||
|
||||
# Folder
|
||||
f_folder = ttk.LabelFrame(self, text="Carpeta de datos")
|
||||
f_folder.pack(fill="x", **pad)
|
||||
row = ttk.Frame(f_folder)
|
||||
row.pack(fill="x", padx=10, pady=8)
|
||||
ttk.Entry(row, textvariable=self.folder).pack(side="left", fill="x", expand=True)
|
||||
ttk.Button(row, text="Elegir…", command=self._pick_folder).pack(side="left", padx=8)
|
||||
|
||||
# Warning label
|
||||
self.lbl_warn = ttk.Label(self, textvariable=self.warn, foreground="darkorange")
|
||||
self.lbl_warn.pack(fill="x", padx=14, pady=(0, 6))
|
||||
|
||||
# ---------------- SONDA SETTINGS ----------------
|
||||
self.f_probe = ttk.LabelFrame(self, text="Sonda (para n_e): geometría y dimensiones")
|
||||
self.f_probe.pack(fill="x", **pad)
|
||||
self.f_probe.columnconfigure(1, weight=1)
|
||||
self.f_probe.columnconfigure(3, weight=1)
|
||||
|
||||
ttk.Label(self.f_probe, text="Geometría:").grid(row=0, column=0, sticky="w", padx=10, pady=6)
|
||||
rowg = ttk.Frame(self.f_probe)
|
||||
rowg.grid(row=0, column=1, sticky="w", padx=10, pady=6)
|
||||
ttk.Radiobutton(rowg, text="Cilíndrica", variable=self.probe_geom, value="cilindrica",
|
||||
command=self._refresh_probe_fields).pack(side="left", padx=(0, 12))
|
||||
ttk.Radiobutton(rowg, text="Esférica", variable=self.probe_geom, value="esferica",
|
||||
command=self._refresh_probe_fields).pack(side="left")
|
||||
|
||||
ttk.Label(self.f_probe, text="Radio r (mm):").grid(row=0, column=2, sticky="w", padx=10, pady=6)
|
||||
ttk.Entry(self.f_probe, textvariable=self.probe_r_mm, width=10).grid(row=0, column=3, sticky="w", padx=10, pady=6)
|
||||
|
||||
ttk.Label(self.f_probe, text="Longitud L (mm):").grid(row=1, column=2, sticky="w", padx=10, pady=6)
|
||||
self.ent_L = ttk.Entry(self.f_probe, textvariable=self.probe_L_mm, width=10)
|
||||
self.ent_L.grid(row=1, column=3, sticky="w", padx=10, pady=6)
|
||||
|
||||
self.lbl_probe_hint = ttk.Label(
|
||||
self.f_probe,
|
||||
text="Nota: L solo aplica a sonda cilíndrica. El backend debe exponer set_sonda(...) para que esto tenga efecto.",
|
||||
foreground="gray"
|
||||
)
|
||||
self.lbl_probe_hint.grid(row=2, column=0, columnspan=4, sticky="w", padx=10, pady=(0, 6))
|
||||
|
||||
# ---------------- TOKENS SETTINGS: CATODOS ----------------
|
||||
self.f_catodos = ttk.LabelFrame(self, text="Cátodos: agrupación (Tokens)")
|
||||
self.f_catodos.columnconfigure(1, weight=1)
|
||||
self.f_catodos.columnconfigure(3, weight=1)
|
||||
|
||||
ttk.Label(self.f_catodos, text="Separador:").grid(row=0, column=0, sticky="w", padx=10, pady=6)
|
||||
ttk.Entry(self.f_catodos, textvariable=self.cat_delim, width=6).grid(row=0, column=1, sticky="w", padx=10, pady=6)
|
||||
|
||||
ttk.Label(self.f_catodos, text="Mapa campo=índice:").grid(row=0, column=2, sticky="w", padx=10, pady=6)
|
||||
ttk.Entry(self.f_catodos, textvariable=self.cat_token_map).grid(row=0, column=3, sticky="ew", padx=10, pady=6)
|
||||
|
||||
ttk.Label(self.f_catodos, text="Orden carpetas (coma):").grid(row=1, column=0, sticky="w", padx=10, pady=6)
|
||||
ttk.Entry(self.f_catodos, textvariable=self.cat_order).grid(row=1, column=1, columnspan=3, sticky="ew", padx=10, pady=6)
|
||||
|
||||
# ---------------- TOKENS SETTINGS: HILOS ----------------
|
||||
self.f_hilos = ttk.LabelFrame(self, text="Hilos: agrupación (Tokens)")
|
||||
self.f_hilos.columnconfigure(1, weight=1)
|
||||
self.f_hilos.columnconfigure(3, weight=1)
|
||||
|
||||
ttk.Label(self.f_hilos, text="Separador:").grid(row=0, column=0, sticky="w", padx=10, pady=6)
|
||||
ttk.Entry(self.f_hilos, textvariable=self.hil_delim, width=6).grid(row=0, column=1, sticky="w", padx=10, pady=6)
|
||||
|
||||
ttk.Label(self.f_hilos, text="Mapa campo=índice:").grid(row=0, column=2, sticky="w", padx=10, pady=6)
|
||||
ttk.Entry(self.f_hilos, textvariable=self.hil_token_map).grid(row=0, column=3, sticky="ew", padx=10, pady=6)
|
||||
|
||||
ttk.Label(self.f_hilos, text="Orden carpetas (coma):").grid(row=1, column=0, sticky="w", padx=10, pady=6)
|
||||
ttk.Entry(self.f_hilos, textvariable=self.hil_order).grid(row=1, column=1, columnspan=3, sticky="ew", padx=10, pady=6)
|
||||
|
||||
# ---------------- PREVIEW ----------------
|
||||
f_prev = ttk.LabelFrame(self, text="Preview de agrupación (primeros 12 archivos)")
|
||||
f_prev.pack(fill="both", expand=True, **pad)
|
||||
|
||||
self.txt_prev = tk.Text(f_prev, height=14, wrap="none")
|
||||
self.txt_prev.pack(fill="both", expand=True, padx=10, pady=8)
|
||||
|
||||
# ---------------- ACTIONS ----------------
|
||||
f_act = ttk.Frame(self)
|
||||
f_act.pack(fill="x", **pad)
|
||||
|
||||
self.btn_preview = ttk.Button(f_act, text="Actualizar preview", command=self._preview)
|
||||
self.btn_preview.pack(side="left", padx=(10, 6), pady=10)
|
||||
|
||||
self.btn_run = ttk.Button(f_act, text="Ejecutar", command=self._run)
|
||||
self.btn_run.pack(side="left", padx=6, pady=10)
|
||||
|
||||
# ✅ CHECKBOX (en vez de botón)
|
||||
self.chk_iones = ttk.Checkbutton(
|
||||
f_act,
|
||||
text="Hacer check iónico al terminar",
|
||||
variable=self.do_ionico,
|
||||
command=self._refresh_backend_state
|
||||
)
|
||||
self.chk_iones.pack(side="left", padx=(14, 6), pady=10)
|
||||
|
||||
ttk.Label(f_act, textvariable=self.status).pack(side="right", padx=10)
|
||||
|
||||
|
||||
# ---------------- Wiring / state ----------------
|
||||
|
||||
def _wire_traces(self):
|
||||
vars_to_watch = [
|
||||
self.folder, self.mode,
|
||||
self.cat_delim, self.cat_token_map, self.cat_order,
|
||||
self.hil_delim, self.hil_token_map, self.hil_order,
|
||||
self.probe_geom, self.probe_r_mm, self.probe_L_mm,
|
||||
]
|
||||
for v in vars_to_watch:
|
||||
v.trace_add("write", lambda *_: self._schedule_preview())
|
||||
|
||||
def _schedule_preview(self):
|
||||
if self._preview_job:
|
||||
try:
|
||||
self.after_cancel(self._preview_job)
|
||||
except Exception:
|
||||
pass
|
||||
self._preview_job = self.after(350, self._preview)
|
||||
|
||||
def _refresh_fields(self, initial: bool = False):
|
||||
if self.mode.get() == "catodo":
|
||||
self.f_catodos.pack(fill="x", padx=10, pady=6)
|
||||
self.f_hilos.pack_forget()
|
||||
else:
|
||||
self.f_hilos.pack(fill="x", padx=10, pady=6)
|
||||
self.f_catodos.pack_forget()
|
||||
|
||||
if not initial:
|
||||
self._schedule_preview()
|
||||
|
||||
self._refresh_backend_state()
|
||||
|
||||
def _refresh_probe_fields(self):
|
||||
self.ent_L.config(state=("normal" if self.probe_geom.get() == "cilindrica" else "disabled"))
|
||||
|
||||
def _refresh_backend_state(self):
|
||||
# Run principal
|
||||
if backend is None or self.busy.get():
|
||||
self.btn_run.config(state="disabled")
|
||||
self.chk_iones.config(state="disabled")
|
||||
return
|
||||
|
||||
ok_run = (run_full_catodos_tokens is not None) if self.mode.get() == "catodo" else (run_full_hilos_tokens is not None)
|
||||
self.btn_run.config(state=("normal" if ok_run else "disabled"))
|
||||
|
||||
# Checkbox iónico solo si existe backend iónico
|
||||
ok_iones = (run_check_ionico is not None)
|
||||
self.chk_iones.config(state=("normal" if ok_iones else "disabled"))
|
||||
|
||||
# Si no existe run_check_ionico, fuerza a False
|
||||
if not ok_iones:
|
||||
self.do_ionico.set(False)
|
||||
|
||||
def _pick_folder(self):
|
||||
d = filedialog.askdirectory()
|
||||
if d:
|
||||
self.folder.set(d)
|
||||
|
||||
def _set_busy(self, busy: bool):
|
||||
self.busy.set(busy)
|
||||
self.btn_run.config(state=("disabled" if busy else "normal"))
|
||||
self.btn_preview.config(state=("disabled" if busy else "normal"))
|
||||
self.chk_iones.config(state=("disabled" if busy else "normal"))
|
||||
if busy:
|
||||
self.status.set("Procesando…")
|
||||
else:
|
||||
self._refresh_backend_state()
|
||||
|
||||
|
||||
# ---------------- Probe handling ----------------
|
||||
|
||||
def _get_probe_config(self) -> tuple[str, float, float | None]:
|
||||
geom = (self.probe_geom.get() or "").strip().lower()
|
||||
if geom not in ("cilindrica", "esferica"):
|
||||
raise ValueError("Geometría de sonda inválida (cilindrica/esferica).")
|
||||
|
||||
try:
|
||||
r_mm = float(str(self.probe_r_mm.get()).replace(",", "."))
|
||||
except Exception:
|
||||
raise ValueError("Radio r (mm) no es numérico.")
|
||||
if r_mm <= 0:
|
||||
raise ValueError("Radio r (mm) debe ser > 0.")
|
||||
|
||||
L_mm = None
|
||||
if geom == "cilindrica":
|
||||
try:
|
||||
L_mm = float(str(self.probe_L_mm.get()).replace(",", "."))
|
||||
except Exception:
|
||||
raise ValueError("Longitud L (mm) no es numérica.")
|
||||
if L_mm <= 0:
|
||||
raise ValueError("Longitud L (mm) debe ser > 0.")
|
||||
|
||||
return geom, r_mm, L_mm
|
||||
|
||||
def _apply_probe_config(self):
|
||||
geom, r_mm, L_mm = self._get_probe_config()
|
||||
if set_sonda is None:
|
||||
raise RuntimeError("El backend no expone set_sonda(...).")
|
||||
set_sonda(geom=geom, rp_mm=r_mm, L_mm=L_mm)
|
||||
|
||||
|
||||
# ---------------- Preview ----------------
|
||||
|
||||
def _preview(self):
|
||||
folder = self.folder.get().strip()
|
||||
self.txt_prev.delete("1.0", "end")
|
||||
self.warn.set("")
|
||||
self._preview_summary = {"bad": 0, "total": 0}
|
||||
|
||||
if not folder:
|
||||
self.txt_prev.insert("end", "Elige una carpeta.\n")
|
||||
return
|
||||
|
||||
data_dir = Path(folder)
|
||||
if not data_dir.exists():
|
||||
self.txt_prev.insert("end", "Carpeta no existe.\n")
|
||||
return
|
||||
|
||||
files = sorted([p for p in data_dir.iterdir() if p.is_file() and p.suffix.lower() in (".txt", ".csv")])
|
||||
if not files:
|
||||
self.txt_prev.insert("end", "No hay .txt/.csv en la carpeta.\n")
|
||||
return
|
||||
|
||||
show = files[:12]
|
||||
total = len(files)
|
||||
|
||||
n_otros = 0
|
||||
n_na = 0
|
||||
bad_examples: list[str] = []
|
||||
|
||||
try:
|
||||
if self.mode.get() == "catodo":
|
||||
delim = self.cat_delim.get() or "_"
|
||||
tmap = parse_token_map(self.cat_token_map.get())
|
||||
order = parse_order(self.cat_order.get())
|
||||
else:
|
||||
delim = self.hil_delim.get() or "_"
|
||||
tmap = parse_token_map(self.hil_token_map.get())
|
||||
order = parse_order(self.hil_order.get())
|
||||
|
||||
for fp in files:
|
||||
g = parse_group_tokens(fp.name, delim, tmap, order)
|
||||
if g == "otros":
|
||||
n_otros += 1
|
||||
if len(bad_examples) < 6:
|
||||
bad_examples.append(fp.name)
|
||||
if _has_na(g):
|
||||
n_na += 1
|
||||
if len(bad_examples) < 6:
|
||||
bad_examples.append(fp.name)
|
||||
|
||||
for fp in show:
|
||||
g = parse_group_tokens(fp.name, delim, tmap, order)
|
||||
self.txt_prev.insert("end", f"{fp.name}\n -> {g}\n\n")
|
||||
|
||||
msgs = []
|
||||
bad = 0
|
||||
if n_otros > 0:
|
||||
msgs.append(f"{n_otros}/{total} -> 'otros'")
|
||||
bad += n_otros
|
||||
if n_na > 0:
|
||||
msgs.append(f"{n_na}/{total} contienen 'NA'")
|
||||
bad += n_na
|
||||
|
||||
self._preview_summary = {"bad": bad, "total": total}
|
||||
|
||||
if msgs:
|
||||
extra = (" | Ejemplos: " + "; ".join(bad_examples[:6])) if bad_examples else ""
|
||||
self.warn.set("⚠️ OJO: " + " · ".join(msgs) + extra)
|
||||
else:
|
||||
self.warn.set("✅ OK: patrón consistente.")
|
||||
|
||||
except Exception as e:
|
||||
self.warn.set(f"⚠️ Error preview: {e}")
|
||||
self.txt_prev.insert("end", f"Error preview: {e}\n")
|
||||
|
||||
|
||||
# ---------------- Run pipeline (+ opcional iónico) ----------------
|
||||
|
||||
def _run(self):
|
||||
if self.busy.get():
|
||||
return
|
||||
|
||||
if backend is None:
|
||||
messagebox.showerror("Backend", "S_Langmuir.py no está importable.")
|
||||
return
|
||||
|
||||
folder = self.folder.get().strip()
|
||||
if not folder:
|
||||
messagebox.showwarning("Falta carpeta", "Elige la carpeta de datos.")
|
||||
return
|
||||
|
||||
data_dir = Path(folder)
|
||||
if not data_dir.exists():
|
||||
messagebox.showerror("Error", "La carpeta no existe.")
|
||||
return
|
||||
|
||||
# preview + warning
|
||||
self._preview()
|
||||
bad = self._preview_summary.get("bad", 0)
|
||||
total = self._preview_summary.get("total", 0)
|
||||
if total > 0 and bad > 0:
|
||||
if not messagebox.askyesno(
|
||||
"Aviso",
|
||||
f"Hay {bad}/{total} archivos que NO encajan con el patrón.\n¿Ejecutar igualmente?"
|
||||
):
|
||||
return
|
||||
|
||||
# aplica sonda
|
||||
try:
|
||||
self._apply_probe_config()
|
||||
except Exception as e:
|
||||
messagebox.showerror("Sonda", str(e))
|
||||
return
|
||||
|
||||
# backend function
|
||||
if self.mode.get() == "catodo" and run_full_catodos_tokens is None:
|
||||
messagebox.showerror("Backend", "Falta run_full_catodos_tokens en S_Langmuir.py")
|
||||
return
|
||||
if self.mode.get() == "hilos" and run_full_hilos_tokens is None:
|
||||
messagebox.showerror("Backend", "Falta run_full_hilos_tokens en S_Langmuir.py")
|
||||
return
|
||||
|
||||
# si el usuario ha marcado iónico pero no existe función
|
||||
if self.do_ionico.get() and run_check_ionico is None:
|
||||
messagebox.showerror("Backend", "Has marcado check iónico pero falta run_check_ionico en S_Langmuir.py")
|
||||
return
|
||||
|
||||
self._set_busy(True)
|
||||
t = threading.Thread(target=self._run_real, args=(data_dir, self.do_ionico.get()), daemon=True)
|
||||
t.start()
|
||||
|
||||
def _run_real(self, data_dir: Path, do_ionico: bool):
|
||||
try:
|
||||
# 1) pipeline
|
||||
if self.mode.get() == "catodo":
|
||||
delim = self.cat_delim.get() or "_"
|
||||
tmap = parse_token_map(self.cat_token_map.get())
|
||||
order = parse_order(self.cat_order.get())
|
||||
run_full_catodos_tokens(data_dir, delim, tmap, order)
|
||||
else:
|
||||
delim = self.hil_delim.get() or "_"
|
||||
tmap = parse_token_map(self.hil_token_map.get())
|
||||
order = parse_order(self.hil_order.get())
|
||||
run_full_hilos_tokens(data_dir, delim, tmap, order)
|
||||
|
||||
# 2) iónico opcional (sin botón)
|
||||
if do_ionico:
|
||||
out_paths = run_check_ionico(data_dir)
|
||||
n = len(out_paths) if out_paths is not None else 0
|
||||
self.after(0, lambda: self.status.set("Terminado + Iones ✅"))
|
||||
self.after(0, lambda: messagebox.showinfo("OK", f"Proceso completado.\nCheck iónico: {n} Calc_iones.csv"))
|
||||
else:
|
||||
self.after(0, lambda: self.status.set("Terminado ✅"))
|
||||
self.after(0, lambda: messagebox.showinfo("OK", "Proceso completado."))
|
||||
|
||||
self.after(0, self._preview)
|
||||
|
||||
except Exception as e:
|
||||
msg = str(e)
|
||||
self.after(0, lambda: self.status.set("Error ❌"))
|
||||
self.after(0, lambda m=msg: messagebox.showerror("Error", m))
|
||||
|
||||
finally:
|
||||
self.after(0, lambda: self._set_busy(False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
App().mainloop()
|
||||
Loading…
Add table
Add a link
Reference in a new issue