1362 lines
49 KiB
Python
1362 lines
49 KiB
Python
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("<<NotebookTabChanged>>", 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("<Configure>", 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("<<NotebookTabChanged>>", 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("<<NotebookTabChanged>>", 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() |