ProyectoPython/ProyectoAdrianHustea/Proyecto.py

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()