diff --git a/Controlador.py b/Controlador.py index 811bc95..0fbbe6d 100644 --- a/Controlador.py +++ b/Controlador.py @@ -5,29 +5,47 @@ from server import start_server class ControladorChat: def __init__(self, vista): self.vista = vista - self.modelo = ModeloCliente() + self.modelos = {} def iniciar_servidor(self): """Inicia el servidor en un hilo separado.""" threading.Thread(target=start_server, daemon=True).start() - self.vista.mostrar_mensaje("[SERVIDOR] Servidor iniciado en segundo plano...\n") + #vista.mostrar_mensaje("[SERVIDOR] Servidor iniciado en segundo plano...\n") + + + def enviar_mensaje(self, host): + """Envía un mensaje al servidor y lo muestra en el chat.""" + if host in self.modelos: + mensaje = self.vista.chats_frames[host]["entry"].get() + if mensaje: + self.vista.mostrar_mensaje(host, f"[TÚ] {mensaje}") + self.modelos[host].enviar_mensaje(mensaje) + self.vista.chats_frames[host]["entry"].delete(0, 'end') + + + def actualizar_host_port(self): + """Obtiene valores de host y port desde la interfaz y los actualiza en el modelo.""" + host = self.vista.host_entry.get() + port = int(self.vista.port_entry.get()) + self.modelo.set_host_port(host, port) + self.vista.mostrar_mensaje(f"[CONFIG] Host y puerto actualizados: {host}:{port}") def conectar_cliente(self): - """Intenta conectar el cliente al servidor.""" - mensaje = self.modelo.conectar( - on_message_received=self.vista.mostrar_mensaje, - on_error=self.vista.mostrar_mensaje - ) - self.vista.mostrar_mensaje(mensaje) - if self.modelo.connected: - self.vista.habilitar_envio() + """Conecta un nuevo cliente y agrega su botón en el menú.""" + host = self.vista.host_entry.get() + port = int(self.vista.port_entry.get()) - def enviar_mensaje(self): - """Obtiene el mensaje de la vista y lo envía al servidor, además lo imprime en la interfaz.""" - mensaje = self.vista.message_entry.get() - if mensaje: - self.vista.mostrar_mensaje(f"[TÚ] {mensaje}") # Agregar el mensaje a la vista - error = self.modelo.enviar_mensaje(mensaje) - if error: - self.vista.mostrar_mensaje(error) - self.vista.message_entry.delete(0, 'end') # Limpiar el campo de entrada + if host not in self.modelos: + modelo = ModeloCliente() + modelo.set_host_port(host, port) + mensaje = modelo.conectar( + on_message_received=lambda msg: self.vista.mostrar_mensaje(host, msg), + on_error=lambda err: self.vista.mostrar_mensaje(host, err) + ) + + if modelo.connected: + self.modelos[host] = modelo + self.vista.agregar_boton_chat(host) + self.vista.habilitar_envio(host) + + self.vista.mostrar_mensaje(host, mensaje) \ No newline at end of file diff --git a/Modelo.py b/Modelo.py index 1b4f496..39f699b 100644 --- a/Modelo.py +++ b/Modelo.py @@ -1,26 +1,30 @@ import socket import threading -SERVER_HOST = '127.0.0.1' -SERVER_PORT = 3333 - class ModeloCliente: def __init__(self): self.client_socket = None self.connected = False + self.host = '127.0.0.1' # Valores por defecto + self.port = 3333 + + def set_host_port(self, host, port): + """Actualiza los valores de host y port.""" + self.host = host + self.port = port def conectar(self, on_message_received, on_error): - """Conecta el cliente al servidor.""" + """Conecta el cliente al servidor con el host y puerto definidos.""" if self.connected: on_error("[CLIENTE] Ya estás conectado al servidor.\n") return self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: - self.client_socket.connect((SERVER_HOST, SERVER_PORT)) + self.client_socket.connect((self.host, self.port)) self.connected = True threading.Thread(target=self.recibir_mensajes, args=(on_message_received, on_error), daemon=True).start() - return f"[CONECTADO] Conectado al servidor {SERVER_HOST}:{SERVER_PORT}\n" + return f"[CONECTADO] Conectado al servidor {self.host}:{self.port}\n" except ConnectionRefusedError: self.client_socket = None return "[ERROR] El servidor no está disponible. Inícialo primero.\n" diff --git a/README.md b/README.md index e69de29..4e0c475 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,38 @@ +# 🗨️ Chat Cliente-Servidor en Python + +Este proyecto es una aplicación de chat basada en una arquitectura cliente-servidor utilizando Python. Implementa `socket` para la comunicación en red, `threading` para manejar múltiples clientes simultáneamente y `tkinter` para la interfaz gráfica. Permite a los usuarios conectarse a un servidor de chat y enviar mensajes en tiempo real. + +## 📌 Características + +- ✅ **Servidor multicliente** basado en `socket` y `threading`. +- ✅ **Clientes con interfaz gráfica** (Tkinter) para conectarse y chatear. +- ✅ **Interfaz amigable** con opciones de conexión y envío de mensajes. +- ✅ **Soporte para múltiples clientes** en una misma sesión de chat. +- ✅ **Servidor ejecutable en segundo plano** desde la interfaz del cliente. + + +## 🔧 Dependencias + +Este proyecto usa módulos estándar de Python, por lo que no es necesario instalar paquetes adicionales. Sin embargo, se recomienda usar un entorno virtual para mantener el aislamiento del proyecto. + +Módulos utilizados: +- [`socket`](https://docs.python.org/3/library/socket.html) - Para la comunicación en red. +- [`threading`](https://docs.python.org/3/library/threading.html) - Para manejar múltiples clientes en paralelo. +- [`tkinter`](https://docs.python.org/3/library/tkinter.html) - Para la interfaz gráfica de la aplicación. + +## 🎥 Video Tutorial + +[![Ver en YouTube](https://img.youtube.com/vi/VIDEO_ID/maxresdefault.jpg)](https://youtu.be/GvhLM6T-Zhg) + + +## 🚀 Instalación y Ejecución + +### 1️⃣ Clonar el repositorio +```bash +git git clone https://git.ieslamar.org/DonWilliam/ChatPersonas.git +cd ChatPersonas + + + + + diff --git a/Vista.py b/Vista.py index 925dcff..5d72fc2 100644 --- a/Vista.py +++ b/Vista.py @@ -1,43 +1,144 @@ import tkinter as tk from tkinter import scrolledtext +from server import start_server class VistaChat: def __init__(self, root, controlador): self.root = root self.root.title("Chat Cliente-Servidor") + self.root.geometry("500x400") # Establece tamaño fijo + self.root.resizable(False, False) # Bloquea el redimensionamiento + self.root.configure(bg="#2C3E50") self.controlador = controlador - # Botón para iniciar el servidor - self.server_button = tk.Button(root, text="Iniciar Servidor", command=self.controlador.iniciar_servidor) - self.server_button.grid(row=0, column=0, padx=10, pady=10) + # Estilos de los botones + btn_style = {"font": ("Arial", 10, "bold"), "fg": "white", "bg": "#2980B9", "bd": 0, "relief": "flat"} - # Botón para conectar el cliente - self.connect_button = tk.Button(root, text="Conectar Cliente", command=self.controlador.conectar_cliente) - self.connect_button.grid(row=0, column=1, padx=10, pady=10) + # FRAME LATERAL PARA EL MENÚ + self.menu_frame = tk.Frame(root, width=100, bg="#34495E") + self.menu_frame.grid(row=0, column=0, rowspan=3, sticky="ns") - # Área de chat - self.chat_display = scrolledtext.ScrolledText(root, wrap=tk.WORD, state='disabled', width=50, height=20) - self.chat_display.grid(row=1, column=0, padx=10, pady=10, columnspan=2) + #self.button_inicio = tk.Button(self.menu_frame, text="Inicio", command=self.mostrar_inicio, width=12, **btn_style) + #self.button_inicio.pack(pady=10) - # Campo de entrada de mensajes - self.message_entry = tk.Entry(root, width=40) - self.message_entry.grid(row=2, column=0, padx=10, pady=10) + self.button_conf = tk.Button(self.menu_frame, text="Conf", command=self.mostrar_config, width=12, **btn_style) + self.button_conf.pack(pady=10) - # Botón para enviar mensajes - self.send_button = tk.Button(root, text="Enviar", command=self.controlador.enviar_mensaje, state='disabled') - self.send_button.grid(row=2, column=1, padx=10, pady=10) + # Almacenar botones de clientes conectados + self.chat_buttons = {} + self.chats_frames = {} + + # FRAME PRINCIPAL DONDE SE MUESTRA EL CHAT O LA CONFIGURACIÓN + self.main_frame = tk.Frame(root, bg="#2C3E50") + self.main_frame.grid(row=0, column=1, padx=10, pady=10, rowspan=3) - def mostrar_mensaje(self, mensaje): - """Muestra un mensaje en el chat.""" - self.chat_display.config(state='normal') - self.chat_display.insert(tk.END, mensaje + '\n') - self.chat_display.config(state='disabled') - self.chat_display.yview(tk.END) + # INICIALIZA LOS CHATS Y CONFIGURACIÓN + self.inicializar_config() + #self.inicializar_chat("127.0.0.1") # Chat local por defecto + #self.mostrar_chat("127.0.0.1") + + - def habilitar_envio(self): - """Habilita el botón de enviar mensajes.""" - self.send_button.config(state='normal') + def inicializar_chat(self, host): + """Crea un nuevo chat con un host específico.""" + chat_frame = tk.Frame(self.main_frame, bg="#2C3E50") + chat_display = scrolledtext.ScrolledText(chat_frame, wrap=tk.WORD, state='disabled', width=50, height=15, bg="#ECF0F1", fg="black") + chat_display.pack(padx=10, pady=10) + + entry_frame = tk.Frame(chat_frame, bg="#2C3E50") + entry_frame.pack(pady=5) + + message_entry = tk.Entry(entry_frame, width=40, font=("Arial", 10)) + message_entry.pack(side=tk.LEFT, padx=10, pady=5) + + send_button = tk.Button(entry_frame, text="Enviar", command=lambda: self.controlador.enviar_mensaje(host), state='disabled', font=("Arial", 10, "bold"), fg="white", bg="#27AE60", bd=0, relief="flat") + send_button.pack(side=tk.RIGHT, padx=5, pady=5) + + self.chats_frames[host] = { + "frame": chat_frame, + "display": chat_display, + "entry": message_entry, + "send_button": send_button + } + + def inicializar_config(self): + """Configura la interfaz de configuración.""" + self.config_frame = tk.Frame(self.main_frame, bg="#2C3E50") + + tk.Label(self.config_frame, text="HOST:", bg="#2C3E50", fg="white", font=("Arial", 10, "bold")).pack(pady=2) + self.host_entry = tk.Entry(self.config_frame, width=20, font=("Arial", 10)) + self.host_entry.pack(pady=5) + self.host_entry.insert(0, "127.0.0.1") + + tk.Label(self.config_frame, text="PORT:", bg="#2C3E50", fg="white", font=("Arial", 10, "bold")).pack(pady=2) + self.port_entry = tk.Entry(self.config_frame, width=10, font=("Arial", 10)) + self.port_entry.pack(pady=5) + self.port_entry.insert(0, "3333") + + btn_style = {"font": ("Arial", 10, "bold"), "fg": "white", "bg": "#2980B9", "bd": 0, "relief": "flat"} + + self.connect_button = tk.Button(self.config_frame, text="Conectar Cliente", command=self.controlador.conectar_cliente, **btn_style) + self.connect_button.pack(pady=10) + + self.start_server = tk.Button(self.config_frame, text="Abrir servidor", command=self.controlador.iniciar_servidor, **btn_style) + self.start_server.pack(pady=20) + + + + def mostrar_inicio(self): + """Muestra la pantalla de chat predeterminado (127.0.0.1).""" + self.config_frame.pack_forget() + if "127.0.0.1" in self.chats_frames: + self.chats_frames["127.0.0.1"]["frame"].pack() + + def mostrar_chat(self, host): + """Muestra un chat específico y oculta los anteriores, incluyendo la configuración.""" + # Ocultar la configuración si está visible + self.config_frame.pack_forget() + + # Ocultar todos los chats antes de mostrar el nuevo + for chat_host, chat in self.chats_frames.items(): + chat["frame"].pack_forget() + + # Mostrar el chat seleccionado + if host in self.chats_frames: + self.chats_frames[host]["frame"].pack() + + def mostrar_config(self): + """Muestra la pantalla de configuración y oculta cualquier chat abierto.""" + # Ocultar todos los chats + for chat in self.chats_frames.values(): + chat["frame"].pack_forget() + + # Mostrar la configuración + self.config_frame.pack() + + + + def mostrar_mensaje(self, host, mensaje): + """Muestra un mensaje en el chat correspondiente.""" + if host in self.chats_frames: + chat_display = self.chats_frames[host]["display"] + chat_display.config(state='normal') + chat_display.insert(tk.END, mensaje + '\n') + chat_display.config(state='disabled') + chat_display.yview(tk.END) + + def habilitar_envio(self, host): + """Habilita el botón de enviar mensajes para el chat correspondiente.""" + if host in self.chats_frames: + self.chats_frames[host]["send_button"].config(state='normal') + def deshabilitar_envio(self): """Deshabilita el botón de enviar mensajes.""" self.send_button.config(state='disabled') + + def agregar_boton_chat(self, host): + """Agrega un nuevo botón al menú lateral cuando se conecta un nuevo cliente.""" + if host not in self.chat_buttons: + btn_style = {"font": ("Arial", 10, "bold"), "fg": "white", "bg": "#2980B9", "bd": 0, "relief": "flat"} + chat_button = tk.Button(self.menu_frame, text=f"Chat {host}", command=lambda: self.mostrar_chat(host), width=12, **btn_style) + chat_button.pack(pady=5) + self.chat_buttons[host] = chat_button + self.inicializar_chat(host) diff --git a/__pycache__/Controlador.cpython-313.pyc b/__pycache__/Controlador.cpython-313.pyc new file mode 100644 index 0000000..243cd19 Binary files /dev/null and b/__pycache__/Controlador.cpython-313.pyc differ diff --git a/__pycache__/Modelo.cpython-313.pyc b/__pycache__/Modelo.cpython-313.pyc new file mode 100644 index 0000000..b249da5 Binary files /dev/null and b/__pycache__/Modelo.cpython-313.pyc differ diff --git a/__pycache__/Vista.cpython-313.pyc b/__pycache__/Vista.cpython-313.pyc new file mode 100644 index 0000000..310f05b Binary files /dev/null and b/__pycache__/Vista.cpython-313.pyc differ diff --git a/__pycache__/server.cpython-313.pyc b/__pycache__/server.cpython-313.pyc new file mode 100644 index 0000000..5c2a6cb Binary files /dev/null and b/__pycache__/server.cpython-313.pyc differ