Objetivo: crear una interfaz con Tkinter que permita seleccionar múltiples archivos, comprimirlos en un .zip y actualizar el progreso sin congelar la GUI. Se practica el patrón hilo de trabajo + queue.Queue + after() para comunicar avances al hilo principal.
threading.Thread. El hilo de la GUI sólo dibuja y procesa la cola de eventos.ttk.Progressbar de 0 a N archivos.queue.Queue el avance (número, nombre, errores).root.after(...), actualiza progreso y detecta fin o cancelación.threading.Event y el hilo corta el bucle.Event para cancelación.zipfile.ZipFile y envía eventos.Guarda el siguiente archivo como compresor_gui.py. Usa solo biblioteca estándar.
import queue
import threading
import zipfile
from pathlib import Path
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
class CompresorGUI:
def __init__(self):
self.root = tk.Tk()
self.root.title("Compresor concurrente")
self.root.geometry("620x420")
self.archivos = []
self.destino = None
self.trabajando = False
self.cancel_event = threading.Event()
self.mensajes = queue.Queue()
self._build_widgets()
self.root.after(150, self._procesar_cola)
def _build_widgets(self):
frm = ttk.Frame(self.root, padding=10)
frm.pack(fill="both", expand=True)
botones = ttk.Frame(frm)
botones.pack(fill="x", pady=(0, 6))
ttk.Button(botones, text="Seleccionar archivos", command=self.seleccionar_archivos).pack(side="left", padx=4)
ttk.Button(botones, text="Elegir carpeta destino", command=self.seleccionar_destino).pack(side="left", padx=4)
self.btn_comprimir = ttk.Button(botones, text="Comprimir", command=self.iniciar_compresion, state="disabled")
self.btn_comprimir.pack(side="right", padx=4)
self.btn_cancelar = ttk.Button(botones, text="Cancelar", command=self.cancelar, state="disabled")
self.btn_cancelar.pack(side="right", padx=4)
ttk.Label(frm, text="Archivos seleccionados:").pack(anchor="w")
self.lista = tk.Listbox(frm, height=8)
self.lista.pack(fill="both", expand=True, pady=4)
barra = ttk.Frame(frm)
barra.pack(fill="x", pady=6)
self.progreso = ttk.Progressbar(barra, length=100, mode="determinate")
self.progreso.pack(fill="x")
self.estado = ttk.Label(frm, text="Esperando archivos")
self.estado.pack(anchor="w")
def seleccionar_archivos(self):
rutas = filedialog.askopenfilenames(title="Elige archivos a comprimir")
if rutas:
self.archivos = list(rutas)
self.lista.delete(0, tk.END)
for ruta in self.archivos:
self.lista.insert(tk.END, ruta)
self.estado.config(text=f"{len(self.archivos)} archivo(s) listo(s).")
self.btn_comprimir.config(state="normal")
def seleccionar_destino(self):
carpeta = filedialog.askdirectory(title="Carpeta destino del ZIP")
if carpeta:
self.destino = Path(carpeta)
self.estado.config(text=f"Destino: {self.destino}")
def iniciar_compresion(self):
if not self.archivos:
messagebox.showwarning("Sin archivos", "Selecciona al menos un archivo.")
return
if self.trabajando:
return
self.trabajando = True
self.cancel_event.clear()
self.progreso["value"] = 0
self.progreso["maximum"] = len(self.archivos)
self.estado.config(text="Iniciando compresi\u00f3n...")
self.btn_comprimir.config(state="disabled")
self.btn_cancelar.config(state="normal")
destino = self.destino or Path(self.archivos[0]).parent
nombre_zip = destino / "comprimido.zip"
hilo = threading.Thread(
target=self._trabajo_compresion,
args=(self.archivos.copy(), nombre_zip, self.cancel_event, self.mensajes),
daemon=True,
)
hilo.start()
def cancelar(self):
if self.trabajando:
self.cancel_event.set()
self.estado.config(text="Cancelando... espera al cierre limpio.")
@staticmethod
def _trabajo_compresion(archivos, ruta_zip, cancel_event, mensajes):
errores = []
try:
with zipfile.ZipFile(ruta_zip, mode="w", compression=zipfile.ZIP_DEFLATED) as zf:
for i, ruta in enumerate(archivos, start=1):
if cancel_event.is_set():
mensajes.put({"tipo": "cancelado", "procesados": i - 1})
return
try:
zf.write(ruta, arcname=Path(ruta).name)
mensajes.put({"tipo": "progreso", "procesados": i, "total": len(archivos), "actual": Path(ruta).name})
except Exception as exc: # pragma: no cover - gui
errores.append(f"{ruta}: {exc}")
mensajes.put({"tipo": "progreso", "procesados": i, "total": len(archivos), "actual": Path(ruta).name, "error": True})
except Exception as exc: # pragma: no cover - gui
mensajes.put({"tipo": "error_fatal", "detalle": str(exc)})
return
mensajes.put({"tipo": "final", "zip": str(ruta_zip), "errores": errores})
def _procesar_cola(self):
try:
while True:
msg = self.mensajes.get_nowait()
self._manejar_mensaje(msg)
except queue.Empty:
pass
finally:
self.root.after(150, self._procesar_cola)
def _manejar_mensaje(self, msg):
tipo = msg.get("tipo")
if tipo == "progreso":
self.progreso["value"] = msg["procesados"]
nombre = msg.get("actual", "")
texto = f"Comprimiendo {msg['procesados']} de {msg['total']}: {nombre}"
if msg.get("error"):
texto += " (error, se omite)"
self.estado.config(text=texto)
elif tipo == "final":
self.estado.config(text=f"Listo. ZIP en: {msg['zip']}")
self._fin_trabajo()
if msg["errores"]:
messagebox.showwarning("Finalizado con errores", "\n".join(msg["errores"]))
elif tipo == "cancelado":
self.estado.config(text=f"Operaci\u00f3n cancelada. Procesados: {msg.get('procesados', 0)}")
self._fin_trabajo()
elif tipo == "error_fatal":
self.estado.config(text="Error al crear el ZIP.")
messagebox.showerror("Error", msg.get("detalle", ""))
self._fin_trabajo()
def _fin_trabajo(self):
self.trabajando = False
self.btn_comprimir.config(state="normal")
self.btn_cancelar.config(state="disabled")
def run(self):
self.root.mainloop()
if __name__ == "__main__":
app = CompresorGUI()
app.run()
queue.Queue: canal seguro entre hilo de trabajo y GUI; evita tocar widgets desde el hilo secundario.after(150,...): sondea la cola sin bloquear; 150 ms es suficiente para feedback fluido.threading.Event se consulta en cada iteración; si está activado se abandona el bucle y se notifica.errores; al finalizar se avisa al usuario sin detener toda la compresión.zf.open con shutil.copyfileobj).log.txt con logging para depuración.selection_set.