import tkinter as tk from tkinter import Menu, filedialog, simpledialog, messagebox from tkinter import ttk import threading import time import datetime import webbrowser import subprocess import os import collections import random import urllib.request import re import urllib.error # --- Importar y configurar Pygame para la música/alarma --- try: import pygame # Intentar inicializar Pygame Mixer. Si falla, deshabilitar la música. try: pygame.mixer.init() MUSIC_AVAILABLE = True except pygame.error as e: MUSIC_AVAILABLE = False print(f"Error al inicializar pygame mixer: {e}. Funcionalidad de audio deshabilitada.") MUSIC_FILE = "music.mp3" # Archivo música de fondo ALARM_FILE = "alarm.mp3" # <--- ¡NUEVO ARCHIVO DE ALARMA! except ImportError: MUSIC_AVAILABLE = False print("Pygame no está instalado. La funcionalidad de audio no estará disponible.") # --- Importación específica para el sonido de alarma (Solo Windows) --- try: import winsound WINSOUND_AVAILABLE = True except ImportError: WINSOUND_AVAILABLE = False # --------------------------------------------------------------------- # --- IMPORTACIONES PARA GRÁFICOS DE RECURSOS --- import psutil import matplotlib.pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg # ----------------------------------------------- # --- HILOS GLOBALES PARA CONTROL --- cpu_monitor = None memory_monitor = None disk_monitor = None net_monitor = None network_thread = None clock_thread = None status_bar_time_thread = None alarm_stop_event = threading.Event() countdown_after_id = None # --- VARIABLES GLOBALES DEL JUEGO DE COCHES --- race_threads = [] race_lock = threading.Lock() winner = None race_stop_event = threading.Event() # --- VARIABLES GLOBALES DE MÚSICA --- music_state = False # False = parada, True = sonando # --------------------------------------------- # --- CLASE DE GESTIÓN DE LA BARRA DE ESTADO --- class StatusBarManager: """Gestiona los tres labels dinámicos de la barra inferior (más los dos nuevos).""" def __init__(self, parent_frame, root): self.parent_frame = parent_frame self.root = root self.labels = {} self._initialize_labels() def _initialize_labels(self): label_configs = [ ("correos", "Correos sin leer (0)", "yellow"), ("temperatura", "Temperatura local (0°C)", "orange"), ("status_1", "Estado 1 (Libre)", "green"), ("status_2", "Estado 2 (Listo)", "blue"), ("status_3", "Estado 3", "cyan"), ] for name, initial_text, bg_color in label_configs[2:]: label = tk.Label( self.parent_frame, text=initial_text, bg=bg_color, anchor="w", width=20, bd=1, relief="sunken" ) label.pack(side="left", fill="x", expand=True) self.labels[name] = label for name, initial_text, bg_color in label_configs[:2]: label = tk.Label( self.parent_frame, text=initial_text, bg=bg_color, anchor="w", width=25, bd=1, relief="flat", font=("Helvetica", 10) ) label.pack(side="right", fill="x", padx=5) self.labels[name] = label def set_status(self, label_name, text, bg_color=None): if label_name in self.labels: label = self.labels[label_name] config = {"text": text} if bg_color: config["bg"] = bg_color self.root.after(0, label.config, config) else: print(f"Error: Label '{label_name}' no encontrado.") # --- CLASE PARA MONITORIZAR Y GRAFICAR RECURSOS --- class SystemMonitor: def __init__(self, master, resource_type, width=4, height=3): self.master = master self.resource_type = resource_type self.update_interval = 1000 if resource_type == 'net': self.data_history_sent = collections.deque([0] * 30, maxlen=30) self.data_history_recv = collections.deque([0] * 30, maxlen=30) self._last_net_io = psutil.net_io_counters() else: self.data_history = collections.deque([0] * 30, maxlen=30) self.fig, self.ax = plt.subplots(figsize=(width, height), dpi=100) self.canvas = FigureCanvasTkAgg(self.fig, master=master) self.canvas_widget = self.canvas.get_tk_widget() self.canvas_widget.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self.setup_plot() self.stop_event = threading.Event() self.monitor_thread = threading.Thread(target=self._data_collection_loop, daemon=True) self.monitor_thread.start() def setup_plot(self): title_map = {'cpu': "Uso de CPU", 'memory': "Uso de RAM", 'disk': "Uso de Disco (I/O)", 'net': "Tasa de Red (Kb/s)"} ylabel_map = {'cpu': "% Uso", 'memory': "% Uso", 'disk': "% Disco", 'net': "Kb/s"} self.ax.set_title(title_map.get(self.resource_type, "Recurso"), fontsize=10) self.ax.set_ylim(0, 100 if self.resource_type != 'net' else 500) self.ax.set_ylabel(ylabel_map.get(self.resource_type, "Valor"), fontsize=8) self.ax.set_xlabel("Tiempo (s)", fontsize=8) self.ax.tick_params(axis='both', which='major', labelsize=7) if self.resource_type == 'net': self.line_sent, = self.ax.plot(self.data_history_sent, label='Enviado (Kb)', color='red') self.line_recv, = self.ax.plot(self.data_history_recv, label='Recibido (Kb)', color='blue') self.ax.legend(loc='upper right', fontsize=7) else: self.line, = self.ax.plot(self.data_history, label=f'{self.resource_type.upper()} %', color='blue') self.ax.legend(loc='upper right', fontsize=7) self.fig.tight_layout(pad=1.0) def _get_data(self): if self.resource_type == 'cpu': return psutil.cpu_percent(interval=0.1) elif self.resource_type == 'memory': return psutil.virtual_memory().percent elif self.resource_type == 'disk': return psutil.disk_usage('/').percent elif self.resource_type == 'net': current_net_io = psutil.net_io_counters() time_diff = self.update_interval / 1000.0 bytes_sent = current_net_io.bytes_sent - self._last_net_io.bytes_sent bytes_recv = current_net_io.bytes_recv - self._last_net_io.bytes_recv kbs_sent = (bytes_sent / 1024) / time_diff kbs_recv = (bytes_recv / 1024) / time_diff self._last_net_io = current_net_io return kbs_sent, kbs_recv return 0 def _data_collection_loop(self): while not self.stop_event.is_set(): data_point = self._get_data() if self.resource_type == 'net': kbs_sent, kbs_recv = data_point self.data_history_sent.append(kbs_sent) self.data_history_recv.append(kbs_recv) else: self.data_history.append(data_point) self.master.after(0, self._update_gui) time.sleep(self.update_interval / 1000.0) def _update_gui(self): if self.resource_type == 'net': self.line_sent.set_ydata(self.data_history_sent) self.line_recv.set_ydata(self.data_history_recv) latest_sent = self.data_history_sent[-1] latest_recv = self.data_history_recv[-1] self.ax.set_title(f"Tasa de Red: S: {latest_sent:.1f} Kb/s, R: {latest_recv:.1f} Kb/s", fontsize=10) else: self.line.set_ydata(self.data_history) latest_value = self.data_history[-1] self.ax.set_title(f"Uso de {self.resource_type.upper()}: {latest_value:.1f}%", fontsize=10) self.canvas.draw_idle() def stop(self): self.stop_event.set() # --- GESTOR DE HILO DE RED --- class NetworkTrafficThread: """Hilo que monitorea y actualiza el tráfico total de red.""" def __init__(self, root, label_in, label_out): self.root = root self.label_in = label_in self.label_out = label_out self.update_interval = 1000 # 1 segundo self.stop_event = threading.Event() self.initial_net_io = psutil.net_io_counters() self.thread = threading.Thread(target=self._monitor_loop, daemon=True) self.thread.start() def _monitor_loop(self): while not self.stop_event.is_set(): current_net_io = psutil.net_io_counters() bytes_sent_total = current_net_io.bytes_sent - self.initial_net_io.bytes_sent bytes_recv_total = current_net_io.bytes_recv - self.initial_net_io.bytes_recv kb_sent_total = bytes_sent_total / 1024 kb_recv_total = bytes_recv_total / 1024 self.root.after(0, self._update_gui, kb_sent_total, kb_recv_total) time.sleep(self.update_interval / 1000.0) def _update_gui(self, kb_sent, kb_recv): self.label_in.config(text=f"KB Recibidos (Input): {kb_recv:,.2f} KB") self.label_out.config(text=f"KB Enviados (Output): {kb_sent:,.2f} KB") def stop(self): self.stop_event.set() # --- FUNCIONES DE MÚSICA --- def initialize_music(): """Carga el archivo de música. Llamar una vez al inicio o al cambiar a T2.""" global MUSIC_AVAILABLE # Solo intentamos cargar si Pygame está disponible y si no hay música ya cargada if MUSIC_AVAILABLE and not pygame.mixer.music.get_busy() and pygame.mixer.get_init(): try: pygame.mixer.music.load(MUSIC_FILE) status_manager.set_status("status_3", f"Música cargada: {MUSIC_FILE}", "purple") except pygame.error as e: messagebox.showerror("Error de Audio", f"No se pudo cargar el archivo '{MUSIC_FILE}'. Asegúrate de que existe y es compatible. Error: {e}") MUSIC_AVAILABLE = False status_manager.set_status("status_3", "Error al cargar música", "magenta") def toggle_music(button): """Inicia o detiene la reproducción de música en bucle.""" global music_state, MUSIC_AVAILABLE if not MUSIC_AVAILABLE: messagebox.showwarning("Advertencia", "La funcionalidad de música no está disponible (Pygame o archivo no encontrado).") return if not music_state: # Asegurarse de que el archivo esté cargado antes de intentar reproducir if pygame.mixer.music.get_pos() == 0 and not pygame.mixer.music.get_busy(): initialize_music() if not MUSIC_AVAILABLE: return # 1. Iniciar la música try: # -1 para reproducir en bucle (infinitamente) pygame.mixer.music.play(-1) music_state = True button.config(text="STOP MÚSICA (En Bucle)", style="Red.TButton") status_manager.set_status("status_3", "Música sonando en bucle", "purple") except pygame.error as e: messagebox.showerror("Error de Reproducción", f"Error al reproducir: {e}") MUSIC_AVAILABLE = False else: # 2. Detener la música pygame.mixer.music.stop() music_state = False button.config(text="MÚSICA (Play)", style="Green.TButton") status_manager.set_status("status_3", "Música detenida", "cyan") def stop_music_clean(): """Detiene la música de forma limpia al salir de la app o cambiar de pestaña.""" global music_state, MUSIC_AVAILABLE if MUSIC_AVAILABLE and music_state: pygame.mixer.music.stop() music_state = False # --- FIN DE FUNCIONES DE MÚSICA --- # --- FUNCIONES PARA LA PESTAÑA T2: SCRAPING (Implementación Books To Scrape) --- def run_simple_scraper(label_resultado): """ Inicia el scraping de la página de práctica de libros (books.toscrape.com). """ URL = "http://books.toscrape.com/" status_manager.set_status("status_3", "Iniciando scraping de libros...", "orange") label_resultado.config(text="Buscando títulos...", fg="orange") # 2. Hilo para no bloquear la GUI threading.Thread(target=_scrape_thread, args=(URL, label_resultado), daemon=True).start() def _scrape_thread(URL, label_resultado): """Ejecuta la lógica de scraping de libros dentro de un hilo.""" global root, status_manager # User-Agent no es estrictamente necesario aquí, pero es una buena práctica USER_AGENT = 'Mozilla/5.0' req = urllib.request.Request(URL, headers={'User-Agent': USER_AGENT}) try: # 1. Obtener la página with urllib.request.urlopen(req, timeout=10) as response: html = response.read().decode('utf-8') # 2. Buscamos todos los títulos de libros. # El patrón busca la etiqueta HTML: title="[TÍTULO DEL LIBRO]" title_matches = re.findall(r'title="(.*?)"', html) # Filtramos los títulos que no son de libros, buscando títulos largos # Esto es una heurística; el primer libro tiene un título corto ("A Light in the Attic") book_titles = [t for t in title_matches if len(t) > 30 and t != "A Light in the Attic"] if book_titles: resultado_text = "✅ **Títulos de Libros Encontrados (books.toscrape.com):**\n\n" for i, title in enumerate(book_titles[:20]): # Limitar a 20 resultados resultado_text += f"[{i+1:02d}] {title}\n" color = "green" status_text = f"Scraping OK: {len(book_titles)} títulos encontrados" else: resultado_text = "⚠️ Advertencia: No se pudo encontrar el patrón o el HTML ha cambiado." color = "blue" status_text = "Scraping parcial/patrón fallido" except urllib.error.HTTPError as e: resultado_text = f"❌ Error HTTP: El servidor denegó el acceso (Error {e.code})." color = "red" status_text = "Error HTTP (4xx/5xx)" except Exception as e: resultado_text = f"❌ Error de Conexión/Scraping: {e}" color = "red" status_text = "Error de Red/Otro" # 4. Actualizar la GUI desde el hilo principal root.after(0, label_resultado.config, {"text": resultado_text, "fg": color, "font": ("Consolas", 10)}) status_manager.set_status("status_3", status_text, color) def setup_scraping_tab(tab_frame): """Configura la interfaz para la pestaña de Scraping.""" for widget in tab_frame.winfo_children(): widget.destroy() main_frame = tk.Frame(tab_frame) main_frame.pack(expand=True, padx=20, pady=20, fill="both") tk.Label(main_frame, text="Web Scraping de Práctica (Books to Scrape)", font=("Arial", 16, "bold")).pack(pady=10) tk.Label( main_frame, text="Esto ejecuta un hilo que descarga la página de prueba 'books.toscrape.com' y extrae los 20 primeros títulos.", wraplength=500, justify="left" ).pack(pady=5) # Etiqueta para mostrar el resultado label_resultado = tk.Label( main_frame, text="Pulse el botón 'Scrapear' para comenzar...", font=("Consolas", 10), fg="gray", justify="left" ) label_resultado.pack(pady=20, padx=10) # Botón de ejecución ttk.Button( main_frame, text="SCRAPEAR LIBROS DE PRÁCTICA", style="Green.TButton", command=lambda: run_simple_scraper(label_resultado) ).pack(pady=15) # --- FIN DE FUNCIONES DE SCRAPING --- # --- FUNCIONES DE LA GUI GENERAL --- def update_time(status_bar, stop_event): """Bucle que actualiza la fecha y hora en la barra de estado.""" while not stop_event.is_set(): now = datetime.datetime.now() day_of_week = now.strftime("%A") time_str = now.strftime("%H:%M:%S") date_str = now.strftime("%Y-%m-%d") label_text = f"{day_of_week}, {date_str} - {time_str}" status_bar.after(1000, status_bar.config, {"text": label_text}) time.sleep(1) def stop_status_bar_time_thread(): """Detiene el hilo de la hora de la barra de estado.""" global status_bar_time_thread if status_bar_time_thread and hasattr(status_bar_time_thread, 'stop_event'): status_bar_time_thread.stop_event.set() status_bar_time_thread = None def open_external_url(url): status_manager.set_status("status_2", f"Abriendo URL...", "orange") webbrowser.open(url) root.after(2000, lambda: status_manager.set_status("status_2", "URL lanzada.", "blue")) def run_bash_backup(): def execute_script(): status_manager.set_status("status_3", "Copia en curso (Bash)...", "red") script_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "backup_script.sh") command = [script_path] try: subprocess.run(command, capture_output=True, text=True, check=True) status_manager.set_status("status_3", "Copia OK: ¡Éxito!", "lime") except subprocess.CalledProcessError as e: status_manager.set_status("status_3", f"Copia ¡FALLO! Código: {e.returncode}", "magenta") except FileNotFoundError: status_manager.set_status("status_3", "Error: Script no encontrado", "black") root.after(5000, lambda: status_manager.set_status("status_3", "Esperando tarea", "cyan")) threading.Thread(target=execute_script, daemon=True).start() def save_text_file(text_widget): text_content = text_widget.get("1.0", "end-1c") file_path = filedialog.asksaveasfilename( defaultextension=".txt", filetypes=[ ("Archivos de Texto", "*.txt"), ("Todos los archivos", "*.*") ], title="Guardar documento del editor" ) if file_path: try: with open(file_path, "w", encoding="utf-8") as file: file.write(text_content) status_manager.set_status("status_3", f"Archivo guardado: {os.path.basename(file_path)}", "lime") root.after(3000, lambda: status_manager.set_status("status_3", "Estado 3", "cyan")) except Exception as e: status_manager.set_status("status_3", f"Error al guardar: {e}", "magenta") root.after(3000, lambda: status_manager.set_status("status_3", "Estado 3", "cyan")) # --- CONTROL DE HILOS Y PESTAÑAS T1 --- class SystemMonitor: def __init__(self, master, resource_type, width=4, height=3): self.master = master self.resource_type = resource_type self.update_interval = 1000 if resource_type == 'net': self.data_history_sent = collections.deque([0] * 30, maxlen=30) self.data_history_recv = collections.deque([0] * 30, maxlen=30) self._last_net_io = psutil.net_io_counters() else: self.data_history = collections.deque([0] * 30, maxlen=30) self.fig, self.ax = plt.subplots(figsize=(width, height), dpi=100) self.canvas = FigureCanvasTkAgg(self.fig, master=master) self.canvas_widget = self.canvas.get_tk_widget() self.canvas_widget.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self.setup_plot() self.stop_event = threading.Event() self.monitor_thread = threading.Thread(target=self._data_collection_loop, daemon=True) self.monitor_thread.start() def setup_plot(self): title_map = {'cpu': "Uso de CPU", 'memory': "Uso de RAM", 'disk': "Uso de Disco (I/O)", 'net': "Tasa de Red (Kb/s)"} ylabel_map = {'cpu': "% Uso", 'memory': "% Uso", 'disk': "% Disco", 'net': "Kb/s"} self.ax.set_title(title_map.get(self.resource_type, "Recurso"), fontsize=10) self.ax.set_ylim(0, 100 if self.resource_type != 'net' else 500) self.ax.set_ylabel(ylabel_map.get(self.resource_type, "Valor"), fontsize=8) self.ax.set_xlabel("Tiempo (s)", fontsize=8) self.ax.tick_params(axis='both', which='major', labelsize=7) if self.resource_type == 'net': self.line_sent, = self.ax.plot(self.data_history_sent, label='Enviado (Kb)', color='red') self.line_recv, = self.ax.plot(self.data_history_recv, label='Recibido (Kb)', color='blue') self.ax.legend(loc='upper right', fontsize=7) else: self.line, = self.ax.plot(self.data_history, label=f'{self.resource_type.upper()} %', color='blue') self.ax.legend(loc='upper right', fontsize=7) self.fig.tight_layout(pad=1.0) def _get_data(self): if self.resource_type == 'cpu': return psutil.cpu_percent(interval=0.1) elif self.resource_type == 'memory': return psutil.virtual_memory().percent elif self.resource_type == 'disk': return psutil.disk_usage('/').percent elif self.resource_type == 'net': current_net_io = psutil.net_io_counters() time_diff = self.update_interval / 1000.0 bytes_sent = current_net_io.bytes_sent - self._last_net_io.bytes_sent bytes_recv = current_net_io.bytes_recv - self._last_net_io.bytes_recv kbs_sent = (bytes_sent / 1024) / time_diff kbs_recv = (bytes_recv / 1024) / time_diff self._last_net_io = current_net_io return kbs_sent, kbs_recv return 0 def _data_collection_loop(self): while not self.stop_event.is_set(): data_point = self._get_data() if self.resource_type == 'net': kbs_sent, kbs_recv = data_point self.data_history_sent.append(kbs_sent) self.data_history_recv.append(kbs_recv) else: self.data_history.append(data_point) self.master.after(0, self._update_gui) time.sleep(self.update_interval / 1000.0) def _update_gui(self): if self.resource_type == 'net': self.line_sent.set_ydata(self.data_history_sent) self.line_recv.set_ydata(self.data_history_recv) latest_sent = self.data_history_sent[-1] latest_recv = self.data_history_recv[-1] self.ax.set_title(f"Tasa de Red: S: {latest_sent:.1f} Kb/s, R: {latest_recv:.1f} Kb/s", fontsize=10) else: self.line.set_ydata(self.data_history) latest_value = self.data_history[-1] self.ax.set_title(f"Uso de {self.resource_type.upper()}: {latest_value:.1f}%", fontsize=10) self.canvas.draw_idle() def stop(self): self.stop_event.set() class NetworkTrafficThread: """Hilo que monitorea y actualiza el tráfico total de red.""" def __init__(self, root, label_in, label_out): self.root = root self.label_in = label_in self.label_out = label_out self.update_interval = 1000 # 1 segundo self.stop_event = threading.Event() self.initial_net_io = psutil.net_io_counters() self.thread = threading.Thread(target=self._monitor_loop, daemon=True) self.thread.start() def _monitor_loop(self): while not self.stop_event.is_set(): current_net_io = psutil.net_io_counters() bytes_sent_total = current_net_io.bytes_sent - self.initial_net_io.bytes_sent bytes_recv_total = current_net_io.bytes_recv - self.initial_net_io.bytes_recv kb_sent_total = bytes_sent_total / 1024 kb_recv_total = bytes_recv_total / 1024 self.root.after(0, self._update_gui, kb_sent_total, kb_recv_total) time.sleep(self.update_interval / 1000.0) def _update_gui(self, kb_sent, kb_recv): self.label_in.config(text=f"KB Recibidos (Input): {kb_recv:,.2f} KB") self.label_out.config(text=f"KB Enviados (Output): {kb_sent:,.2f} KB") def stop(self): self.stop_event.set() def setup_monitors_tab(tab_frame): tk.Label(tab_frame, text="Monitorización de Recursos del Sistema", font=("Arial", 14, "bold")).pack(pady=10) monitor_grid_frame = tk.Frame(tab_frame) monitor_grid_frame.pack(fill="both", expand=True, padx=10, pady=10) monitor_grid_frame.columnconfigure(0, weight=1) monitor_grid_frame.columnconfigure(1, weight=1) monitor_grid_frame.rowconfigure(0, weight=1) monitor_grid_frame.rowconfigure(1, weight=1) global cpu_monitor, memory_monitor, disk_monitor, net_monitor cpu_frame = tk.Frame(monitor_grid_frame, bd=1, relief="solid") cpu_frame.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) cpu_monitor = SystemMonitor(cpu_frame, 'cpu') memory_frame = tk.Frame(monitor_grid_frame, bd=1, relief="solid") memory_frame.grid(row=0, column=1, sticky="nsew", padx=5, pady=5) memory_monitor = SystemMonitor(memory_frame, 'memory') disk_frame = tk.Frame(monitor_grid_frame, bd=1, relief="solid") disk_frame.grid(row=1, column=0, sticky="nsew", padx=5, pady=5) disk_monitor = SystemMonitor(disk_frame, 'disk') net_frame = tk.Frame(monitor_grid_frame, bd=1, relief="solid") net_frame.grid(row=1, column=1, sticky="nsew", padx=5, pady=5) net_monitor = SystemMonitor(net_frame, 'net') def setup_editor_tab(tab_frame): tk.Label(tab_frame, text="Editor de Texto Simple", font=("Arial", 14, "bold")).pack(pady=10) editor_frame = tk.Frame(tab_frame) editor_frame.pack(fill="both", expand=True, padx=10, pady=10) editor_scrollbar = ttk.Scrollbar(editor_frame) editor_scrollbar.pack(side="right", fill="y") text_area = tk.Text( editor_frame, wrap="word", undo=True, font=("Consolas", 11), yscrollcommand=editor_scrollbar.set ) text_area.pack(fill="both", expand=True) editor_scrollbar.config(command=text_area.yview) ttk.Button( tab_frame, text="Guardar Documento", command=lambda: save_text_file(text_area) ).pack(pady=5) def setup_network_thread_tab(tab_frame): """Configura la pestaña 'Hilo' con el contador de tráfico de red, centrado y ampliado.""" global network_thread center_frame = tk.Frame(tab_frame) center_frame.pack(expand=True) tk.Label( center_frame, text="Monitorización Total de Tráfico de Red (KB)", font=("Arial", 18, "bold") ).pack(pady=20) label_in = tk.Label( center_frame, text="KB Recibidos (Input): -- KB", font=("Consolas", 20, "bold"), fg="blue" ) label_in.pack(pady=15, padx=50) label_out = tk.Label( center_frame, text="KB Enviados (Output): -- KB", font=("Consolas", 20, "bold"), fg="red" ) label_out.pack(pady=15, padx=50) tk.Label( center_frame, text="El conteo se reinicia al navegar fuera de la pestaña T1.", fg="gray", font=("Arial", 10) ).pack(pady=30) if not network_thread: network_thread = NetworkTrafficThread(root, label_in, label_out) def stop_network_thread(): global network_thread if 'network_thread' in globals() and network_thread: network_thread.stop() network_thread = None def setup_t1_processes(tab_frame, root_instance): """Configura el Notebook interno para la pestaña T1. Procesos.""" for widget in tab_frame.winfo_children(): widget.destroy() internal_notebook = ttk.Notebook(tab_frame) internal_notebook.pack(fill="both", expand=True, padx=5, pady=5) estado_tab = ttk.Frame(internal_notebook) internal_notebook.add(estado_tab, text="Estado") setup_monitors_tab(estado_tab) editor_tab = ttk.Frame(internal_notebook) internal_notebook.add(editor_tab, text="Editor de texto") setup_editor_tab(editor_tab) hilo_tab = ttk.Frame(internal_notebook) internal_notebook.add(hilo_tab, text="Hilo") def handle_internal_tab_change(event): selected_index = internal_notebook.index(internal_notebook.select()) current_tab_title = internal_notebook.tab(selected_index, "text") if internal_notebook.tab(0, "text") != "Estado": stop_monitors() if current_tab_title == "Hilo": if not network_thread: setup_network_thread_tab(hilo_tab) else: stop_network_thread() internal_notebook.bind("<>", handle_internal_tab_change) def stop_monitors(): """Detiene todos los monitores y el hilo de red de T1.""" global cpu_monitor, memory_monitor, disk_monitor, net_monitor if 'cpu_monitor' in globals() and cpu_monitor: cpu_monitor.stop() cpu_monitor = None if 'memory_monitor' in globals() and memory_monitor: memory_monitor.stop() memory_monitor = None if 'disk_monitor' in globals() and disk_monitor: disk_monitor.stop() disk_monitor = None if 'net_monitor' in globals() and net_monitor: net_monitor.stop() net_monitor = None stop_network_thread() # --- FUNCIONES PARA LA PESTAÑA T2: RELOJ Y ALARMA --- def stop_clock_thread(): """Detiene el hilo del reloj digital y el hilo de la alarma.""" global clock_thread if 'clock_thread' in globals() and clock_thread and hasattr(clock_thread, 'stop_event'): clock_thread.stop_event.set() clock_thread = None stop_alarm_countdown() def stop_alarm_countdown(): """Detiene la cuenta atrás de la alarma si está activa.""" global countdown_after_id, alarm_stop_event alarm_stop_event.set() if countdown_after_id: root.after_cancel(countdown_after_id) countdown_after_id = None def update_digital_clock(label_clock, stop_event): """Función de bucle para actualizar la hora digital en un hilo.""" while not stop_event.is_set(): now = datetime.datetime.now() time_str = now.strftime("%H:%M:%S") date_str = now.strftime("%Y-%m-%d") display_text = f"{date_str}\n{time_str}" root.after(0, label_clock.config, {"text": display_text}) time.sleep(1) def play_alarm_sound(): """Produce una notificación sonora.""" global MUSIC_AVAILABLE if MUSIC_AVAILABLE: # Detenemos la música de fondo para que la alarma se escuche clara pygame.mixer.music.stop() try: # Cargamos y reproducimos el archivo de alarma una sola vez alarm_sound = pygame.mixer.Sound(ALARM_FILE) alarm_sound.play() except pygame.error as e: messagebox.showerror("Error de Audio", f"No se pudo reproducir {ALARM_FILE}. Error: {e}") if WINSOUND_AVAILABLE: # Fallback de Windows si Pygame no funciona bien winsound.Beep(1000, 1000) winsound.MessageBeep(winsound.MB_ICONEXCLAMATION) else: print("\n*** ALARMA DISPARADA (Sin sonido de sistema) ***\n") def alarm_loop(wait_seconds, alarm_time_str, label_status): """Hilo de la alarma que espera y se dispara.""" global alarm_stop_event time.sleep(wait_seconds) if alarm_stop_event.is_set(): return root.after(0, play_alarm_sound) root.after(0, lambda: messagebox.showinfo("ALARMA", f"¡ALARMA TERMINADA! Son las {alarm_time_str}")) root.after(0, lambda: label_status.config(text="ALARMA TERMINADA", fg="red")) root.after(0, stop_alarm_countdown) def start_alarm_thread(alarm_time_str, label_status, label_countdown): """Calcula el tiempo de espera, inicia el hilo de alarma y la cuenta atrás.""" global alarm_stop_event try: alarm_hour, alarm_minute = map(int, alarm_time_str.split(':')) if not (0 <= alarm_hour <= 23 and 0 <= alarm_minute <= 59): raise ValueError("Hora fuera de rango.") except ValueError: messagebox.showerror("Error de Formato", "Por favor, introduce la hora en formato HH:MM (ej: 07:30)") return now = datetime.datetime.now() alarm_dt = now.replace(hour=alarm_hour, minute=alarm_minute, second=0, microsecond=0) if alarm_dt <= now: alarm_dt += datetime.timedelta(days=1) time_difference = alarm_dt - now wait_seconds = time_difference.total_seconds() stop_alarm_countdown() alarm_stop_event.clear() label_status.config(text=f"ALARMA ACTIVA: {alarm_time_str} (Hacia adelante)", fg="orange") update_countdown_label(alarm_dt, label_countdown, label_status) threading.Thread(target=alarm_loop, args=(wait_seconds, alarm_time_str, label_status), daemon=True).start() def update_countdown_label(alarm_dt, label_countdown, label_status): """Actualiza la etiqueta de cuenta atrás cada segundo.""" global countdown_after_id, alarm_stop_event now = datetime.datetime.now() if alarm_stop_event.is_set() or alarm_dt <= now: label_countdown.config(text="00:00:00") label_status.config(text="ALARMA INACTIVA", fg="gray") return time_remaining = alarm_dt - now total_seconds = int(time_remaining.total_seconds()) hours, remainder = divmod(total_seconds, 3600) minutes, seconds = divmod(remainder, 60) countdown_str = f"{hours:02d}:{minutes:02d}:{seconds:02d}" label_countdown.config(text=countdown_str) countdown_after_id = root.after(1000, lambda: update_countdown_label(alarm_dt, label_countdown, label_status)) def set_alarm(entry_hour, label_status, label_countdown): """Recoge la hora de entrada y lanza el hilo de alarma.""" alarm_time = entry_hour.get() if alarm_time: start_alarm_thread(alarm_time, label_status, label_countdown) entry_hour.delete(0, tk.END) def setup_clock_tab(tab_frame): """Configura la interfaz de la subpestaña Reloj/Alarma.""" for widget in tab_frame.winfo_children(): widget.destroy() center_frame = tk.Frame(tab_frame) center_frame.pack(expand=True, pady=20) tk.Label(center_frame, text="Reloj Digital y Alarma", font=("Arial", 16, "bold")).pack(pady=10) label_clock = tk.Label( center_frame, text="00:00:00", font=("Consolas", 60, "bold"), bg="black", fg="#00FF00", relief="sunken", padx=20, pady=10 ) label_clock.pack(pady=20) global clock_thread stop_clock_thread() stop_event = threading.Event() clock_thread = threading.Thread( target=update_digital_clock, args=(label_clock, stop_event), daemon=True ) clock_thread.stop_event = stop_event clock_thread.start() alarm_frame = ttk.LabelFrame(center_frame, text="Configurar Alarma", padding=10) alarm_frame.pack(pady=30, padx=20, fill="x") tk.Label(alarm_frame, text="Hora (HH:MM):", font=("Arial", 12)).pack(side="left", padx=5) entry_hour = ttk.Entry(alarm_frame, width=8, font=("Arial", 12)) entry_hour.pack(side="left", padx=5) label_alarm_status = tk.Label(alarm_frame, text="ALARMA INACTIVA", fg="gray", font=("Arial", 12, "italic")) label_alarm_status.pack(side="right", padx=15) label_countdown = tk.Label(alarm_frame, text="00:00:00", fg="red", font=("Consolas", 14, "bold")) label_countdown.pack(side="right", padx=10) ttk.Button( alarm_frame, text="Establecer Alarma", command=lambda: set_alarm(entry_hour, label_alarm_status, label_countdown) ).pack(side="right", padx=10) ttk.Button( alarm_frame, text="Cancelar", command=lambda: stop_alarm_countdown() or label_alarm_status.config(text="ALARMA CANCELADA", fg="gray") ).pack(side="right", padx=10) # --- FUNCIONES PARA LA PESTAÑA T2: COCHES --- def stop_race(): """Detiene la carrera actual, si está en curso.""" global race_stop_event, race_threads, winner if not race_stop_event.is_set(): race_stop_event.set() for thread in race_threads: if thread.is_alive(): pass race_threads = [] winner = None def car_movement(canvas, car_id, car_color, label_result, start_x, goal_x): """Hilo que mueve un coche de color.""" global winner, race_lock current_x = start_x delay = random.randint(10, 50) / 1000.0 step = 5 while current_x < goal_x and not race_stop_event.is_set(): current_x += step canvas.after(0, canvas.coords, car_id, current_x, canvas.coords(car_id)[1], current_x + 50, canvas.coords(car_id)[3]) time.sleep(delay) if not race_stop_event.is_set(): with race_lock: if winner is None: winner = car_color race_stop_event.set() root.after(0, label_result.config, {"text": f"🎉 ¡Ha ganado el coche {car_color.upper()}! 🎉", "fg": car_color, "font": ("Arial", 18, "bold")}) status_manager.set_status("status_3", f"Ganador: {car_color}", car_color) else: pass def start_race(canvas, cars, label_result): """Inicia la carrera, lanzando un hilo para cada coche.""" stop_race() global race_threads, winner, race_stop_event winner = None race_stop_event.clear() label_result.config(text="¡CARRERA EN CURSO!", fg="black", font=("Arial", 14, "italic")) START_X = 10 GOAL_X = canvas.winfo_width() - 60 for car_color, car_id in cars: canvas.coords(car_id, START_X, canvas.coords(car_id)[1], START_X + 50, canvas.coords(car_id)[3]) thread = threading.Thread( target=car_movement, args=(canvas, car_id, car_color, label_result, START_X, GOAL_X), daemon=True ) race_threads.append(thread) thread.start() status_manager.set_status("status_3", "Carrera iniciada...", "yellow") def setup_race_game(tab_frame): """Configura la interfaz del juego de la carrera de coches.""" for widget in tab_frame.winfo_children(): widget.destroy() control_frame = tk.Frame(tab_frame) control_frame.pack(pady=10, fill="x") tk.Label(control_frame, text="Simulación de Carrera Multihilo (Coches)", font=("Arial", 16, "bold")).pack(side="left", padx=15) label_result = tk.Label(control_frame, text="Presiona 'START' para comenzar", fg="black", font=("Arial", 14)) label_result.pack(side="right", padx=15) canvas_frame = tk.Frame(tab_frame, bd=2, relief="sunken") canvas_frame.pack(fill="both", expand=True, padx=10, pady=5) canvas = tk.Canvas(canvas_frame, bg="lightgray") canvas.pack(fill="both", expand=True) CARS_CONFIG = [ ("red", 50), ("blue", 150), ("green", 250) ] cars_data = [] def draw_cars_and_goal(): canvas_width = canvas.winfo_width() if canvas_width < 100: root.after(100, draw_cars_and_goal) return GOAL_X = canvas_width - 50 canvas.create_line(GOAL_X, 0, GOAL_X, canvas.winfo_height(), width=3, fill="black", dash=(5, 5)) canvas.create_text(GOAL_X - 20, 15, text="META", fill="black") for color, y_pos in CARS_CONFIG: car_id = canvas.create_rectangle(10, y_pos, 60, y_pos + 50, fill=color, outline="black") cars_data.append((color, car_id)) canvas.bind("", lambda event: draw_cars_and_goal() if not cars_data else None) button_frame = tk.Frame(tab_frame) button_frame.pack(pady=10) ttk.Button( button_frame, text="START RACE (Hilos)", command=lambda: start_race(canvas, cars_data, label_result) ).pack(side="left", padx=10) ttk.Button( button_frame, text="STOP/RESET", command=lambda: stop_race() or label_result.config(text="Carrera detenida y reiniciada.", fg="gray") ).pack(side="left", padx=10) def setup_t2_threads(tab_frame): """Configura el Notebook interno para la pestaña T2. Threads.""" for widget in tab_frame.winfo_children(): widget.destroy() internal_notebook = ttk.Notebook(tab_frame) internal_notebook.pack(fill="both", expand=True, padx=5, pady=5) clock_tab = ttk.Frame(internal_notebook) internal_notebook.add(clock_tab, text="Reloj / Alarma") race_tab = ttk.Frame(internal_notebook) internal_notebook.add(race_tab, text="Coches") # --- PESTAÑA DE SCRAPING --- scraping_tab = ttk.Frame(internal_notebook) internal_notebook.add(scraping_tab, text="Scraping") # --------------------------------- def handle_internal_tab_change_t2(event): selected_index = internal_notebook.index(internal_notebook.select()) current_tab_title = internal_notebook.tab(selected_index, "text") # Detener hilos al cambiar de pestaña if current_tab_title != "Reloj / Alarma": stop_clock_thread() else: setup_clock_tab(clock_tab) if current_tab_title != "Coches": stop_race() else: setup_race_game(race_tab) if current_tab_title == "Scraping": setup_scraping_tab(scraping_tab) internal_notebook.bind("<>", handle_internal_tab_change_t2) # Configuración inicial de todas las pestañas setup_clock_tab(clock_tab) setup_race_game(race_tab) setup_scraping_tab(scraping_tab) # --- FUNCIÓN DE NAVEGACIÓN PRINCIPAL --- def navigate_to_tab(event): """Maneja el cambio de pestaña principal (T1, T2, T3...).""" selected_index = notebook.index(notebook.select()) tab_name = notebook.tab(selected_index, "text") # 1. Limpiar el frame izquierdo (Botones) for widget in frame_izquierdo.winfo_children(): widget.destroy() # 2. Detener todos los hilos activos de otras pestañas stop_monitors() stop_clock_thread() stop_race() stop_music_clean() # Detener la música al cambiar de pestaña # 3. Configurar la pestaña principal y los botones laterales if selected_index == 0: # T1. Procesos setup_t1_processes(tabs[0], root) setup_buttons_t1(frame_izquierdo) elif selected_index == 1: # T2. Threads setup_t2_threads(tabs[1]) setup_buttons_t2(frame_izquierdo) initialize_music() # Cargar la música solo cuando entramos en T2 elif selected_index == 2: # T3. Sockets (Pendiente de implementación) setup_buttons_default(frame_izquierdo, tab_name) else: setup_buttons_default(frame_izquierdo, tab_name) status_manager.set_status("status_1", f"Navegando a: {tab_name}", "yellow") root.after(2000, lambda: status_manager.set_status("status_1", "Navegación completa", "green")) # --- FUNCIONES DE SETUP PARA BOTONES CONTEXTUALES --- def create_section(parent, title): """Crea un label de título de sección estilizado.""" tk.Label(parent, text=title, bg="#AAAAAA", fg="black", font=("Arial", 10, "bold"), anchor="w").pack(side="top", fill="x", pady=(10, 2), padx=5) def setup_buttons_t1(frame): """Configura los botones para la pestaña T1. Procesos.""" URL_GOOGLE = "https://www.google.com" URL_YOUTUBE = "https://www.youtube.com" URL_FILMIN = "https://www.filmin.es/" create_section(frame, "Aplicaciones") ttk.Button(frame, text="Google", command=lambda: open_external_url(URL_GOOGLE)).pack(pady=3, padx=10, fill="x") ttk.Button(frame, text="YouTube", command=lambda: open_external_url(URL_YOUTUBE)).pack(pady=3, padx=10, fill="x") ttk.Button(frame, text="Filmin", command=lambda: open_external_url(URL_FILMIN)).pack(pady=3, padx=10, fill="x") create_section(frame, "Procesos batch") ttk.Button( frame, text="Copias de seguridad", command=run_bash_backup ).pack(pady=3, padx=10, fill="x") def setup_buttons_t2(frame): """Configura los botones para la pestaña T2. Threads (Incluyendo el de Música).""" create_section(frame, "Hilos de Control") # Crear un botón con estilo y pasarlo a la función de toggle global music_state # Definir el estilo inicial initial_text = "MÚSICA (Play)" if not music_state else "STOP MÚSICA (En Bucle)" initial_style = "Green.TButton" if not music_state else "Red.TButton" music_button = ttk.Button( frame, text=initial_text, style=initial_style ) # Pasar el botón a la función toggle_music para que pueda cambiar su propio texto y estilo music_button.config(command=lambda: toggle_music(music_button)) music_button.pack(pady=10, padx=10, fill="x") create_section(frame, "Configuración General") tk.Label(frame, text="Controles de T2...", fg="gray", font=("Arial", 10)).pack(pady=10) def setup_buttons_default(frame, tab_title): """Configura botones genéricos para pestañas no implementadas.""" create_section(frame, f"Acciones para {tab_title}") tk.Label(frame, text="Funcionalidad pendiente...", fg="gray", font=("Arial", 10)).pack(pady=10) ttk.Button(frame, text="Ejecutar Acción Genérica").pack(pady=3, padx=10, fill="x") # --- CONFIGURACIÓN PRINCIPAL --- root = tk.Tk() root.title("Ventana Responsive - Proyecto Nico") root.geometry("1200x800") root.columnconfigure(0, weight=0) root.columnconfigure(1, weight=1) root.rowconfigure(0, weight=1) root.rowconfigure(1, weight=0) # Menú menu_bar = Menu(root) file_menu = Menu(menu_bar, tearoff=0) file_menu.add_command(label="Nuevo") file_menu.add_command(label="Abrir") file_menu.add_separator() file_menu.add_command(label="Salir", command=root.quit) edit_menu = Menu(menu_bar, tearoff=0) edit_menu.add_command(label="Copiar") edit_menu.add_command(label="Pegar") help_menu = Menu(menu_bar, tearoff=0) help_menu.add_command(label="Acerca de") menu_config = Menu(menu_bar, tearoff=0) menu_config.add_command(label="Preferencias") menu_bar.add_cascade(label="Archivo", menu=file_menu) menu_bar.add_cascade(label="Editar", menu=edit_menu) menu_bar.add_cascade(label="Configuración", menu=menu_config) menu_bar.add_cascade(label="Ayuda", menu=help_menu) root.config(menu=menu_bar) # Estilos personalizados style = ttk.Style() style.configure("CustomNotebook.TNotebook.Tab", font=("Arial", 12, "bold")) style.configure("Green.TButton", background="#4CAF50", foreground="black") style.map("Green.TButton", background=[('active', '#66BB6A')], foreground=[('active', 'black')] ) style.configure("Red.TButton", background="#F44336", foreground="black") style.map("Red.TButton", background=[('active', '#E57373')], foreground=[('active', 'black')] ) # Estructura de Frames frame_izquierdo = tk.Frame(root, bg="#DDDDDD", width=250) frame_central = tk.Frame(root, bg="white") frame_izquierdo.grid(row=0, column=0, sticky="ns") frame_central.grid(row=0, column=1, sticky="nsew") frame_izquierdo.grid_propagate(False) frame_central.rowconfigure(0, weight=1) frame_central.rowconfigure(1, weight=0) frame_central.columnconfigure(0, weight=1) frame_superior = tk.Frame(frame_central, bg="#F0F0F0") frame_inferior = tk.Frame(frame_central, bg="#D0FFD0", height=100) frame_superior.grid(row=0, column=0, sticky="nsew") frame_inferior.grid(row=1, column=0, sticky="ew") frame_inferior.grid_propagate(False) tk.Label(frame_inferior, text="Panel para notas informativas y mensajes sobre la ejecución de los hilos.", bg="#D0FFD0", fg="#333333", justify="left", padx=10, pady=5).pack(fill="both", expand=True) # Barra de estado barra_estado = tk.Frame(root, bg="lightgray") barra_estado.grid(row=1, column=0, columnspan=2, sticky="ew") barra_estado.grid_columnconfigure(0, weight=1) status_manager = StatusBarManager(barra_estado, root) label_fecha_hora = tk.Label(barra_estado, text="Hilo fecha-hora", font=("Helvetica", 10, "bold"), bd=1, fg="blue", relief="raised", anchor="center", width=25, padx=5) label_fecha_hora.pack(side="right", fill="x", padx=5) # INICIALIZACIÓN DEL HILO DE LA BARRA DE ESTADO stop_event_status_bar = threading.Event() status_bar_time_thread = threading.Thread(target=update_time, args=(label_fecha_hora, stop_event_status_bar), daemon=True) status_bar_time_thread.stop_event = stop_event_status_bar status_bar_time_thread.start() # Notebook principal notebook = ttk.Notebook(frame_superior, style="CustomNotebook.TNotebook") notebook.pack(fill="both", expand=True) TAB_TITLES = ["T1. Procesos", "T2. Threads", "T3. Sockets", "T4. Servicios", "T5. Seguridad"] tabs = [] for i, title in enumerate(TAB_TITLES): tab = ttk.Frame(notebook) tabs.append(tab) notebook.add(tab, text=title, padding=4) if i == 0: pass elif i == 1: pass else: ttk.Label(tab, text=f"Contenido de {title}", font=("Arial", 14)).pack(pady=20) notebook.bind("<>", navigate_to_tab) # Configuración inicial (inicia en T1) root.after(100, lambda: setup_t1_processes(tabs[0], root)) root.after(100, lambda: setup_buttons_t1(frame_izquierdo)) # FUNCIÓN DE CIERRE DE VENTANA def on_closing(): stop_monitors() stop_clock_thread() stop_race() stop_music_clean() stop_status_bar_time_thread() root.destroy() root.protocol("WM_DELETE_WINDOW", on_closing) root.mainloop()