{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<p>\n",
    "<font size='5' face='Georgia, Arial'>IIC-2233 Apunte Programación Avanzada</font><br>\n",
    "<font size='1'> Editado por Equipo Docente IIC2233 2019-1 al 2025-2.</font>\n",
    "</p>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Tabla de contenidos\n",
    "\n",
    "1. [Conexiones entre múltiples ventanas en PyQt](#Conexiones-entre-múltiples-ventanas--en-PyQt)\n",
    "    1. [¡Señales al rescate!](#¡Señales-al-rescate!)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Conexiones entre múltiples ventanas  en PyQt\n",
    "\n",
    "Al crear interfaces con múltiples ventanas, es natural y común querer comunicarlas y específicamente, cambiar entre una ventana y otra. Desde los ejemplos iniciales, uno puede verse tentado a utilizar la interfaz de `show` y `hide` para mostrar y ocultar ventanas. \n",
    "\n",
    "No sería raro inicialmente intentar algo como lo siguiente, dónde una ventana instancia otra e intenta abrirla.\n",
    "\n",
    "Este código se encuentra en el archivo `5-ejemplo-conexion-entre-ventanas/1_no-abre-otra-ventana.py`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```python\n",
    "import sys\n",
    "from PyQt6.QtWidgets import QApplication, QWidget, QPushButton\n",
    "\n",
    "\n",
    "class Ventana(QWidget):\n",
    "    def __init__(self, titulo: str, x: int, y: int) -> None:\n",
    "        super().__init__()\n",
    "        self.setWindowTitle(titulo)\n",
    "        self.setGeometry(x, y, 200, 50)\n",
    "        self.boton = QPushButton(\"Abrir otra ventana\", self)\n",
    "        self.boton.clicked.connect(self.abrir_otra_ventana)\n",
    "        self.show()\n",
    "\n",
    "    def abrir_otra_ventana(self) -> None:\n",
    "        self.hide()  # Esconder la ventana actual\n",
    "        otra_ventana = Ventana(\"Otra ventana\", 300, 100)  # Crear otra\n",
    "        otra_ventana.show()  # Mostrar nueva ventana\n",
    "\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    def hook(type, value, traceback) -> None:\n",
    "        print(type)\n",
    "        print(traceback)\n",
    "\n",
    "    sys.__excepthook__ = hook\n",
    "\n",
    "    app = QApplication([])\n",
    "    ventana = Ventana(\"Inicial\", 100, 100)\n",
    "    sys.exit(app.exec())\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si pruebas ejecutar lo anterior en tu computador, notarás un comportamiento inesperado: **no se muestra la segunda ventana**.\n",
    "\n",
    "Intentemos un ángulo distinto, instanciemos la segunda ventana antes y la entregamos como un argumento al instanciar la ventana inicial.\n",
    "\n",
    "Este código se encuentra en el archivo `5-ejemplo-conexion-entre-ventanas/2_no-abre-ventana-inicial.py`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "scrolled": true
   },
   "source": [
    "```python\n",
    "import sys\n",
    "from PyQt6.QtWidgets import QApplication, QWidget, QPushButton\n",
    "\n",
    "\n",
    "class Ventana(QWidget):\n",
    "    def __init__(self, titulo: str, x: int, y: int, otra_ventana: QWidget | None = None) -> None:\n",
    "        super().__init__()\n",
    "        self.otra_ventana = otra_ventana\n",
    "        self.setWindowTitle(titulo)\n",
    "        self.setGeometry(x, y, 200, 50)\n",
    "        self.boton = QPushButton(\"Abrir otra ventana\", self)\n",
    "        self.boton.clicked.connect(self.abrir_otra_ventana)\n",
    "\n",
    "    def abrir_otra_ventana(self) -> None:\n",
    "        if self.otra_ventana is not None:\n",
    "            self.hide()  # Esconder la ventana actual\n",
    "            self.otra_ventana.show()  # Mostrar otra ventana\n",
    "\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    def hook(type, value, traceback) -> None:\n",
    "        print(type)\n",
    "        print(traceback)\n",
    "\n",
    "    sys.__excepthook__ = hook\n",
    "\n",
    "    app = QApplication([])\n",
    "    # Segunda ventana se crea antes de forma independiente\n",
    "    otra_ventana = Ventana(\"Otra ventana\", 300, 100)\n",
    "    # Ventana inicial recibe como argumento a otra_ventana\n",
    "    ventana = Ventana(\"Inicial\", 100, 100, otra_ventana)\n",
    "    ventana.show()\n",
    "    sys.exit(app.exec())\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Este sí funciona si se intenta ejecutar. 🤔 ¿Qué ocurrió en el primero entonces? 🤔\n",
    "\n",
    "El detalle, es que al instanciar un *widget* como una variable dentro de un método, como toda **variable local** del método, cuando se termine dicho método, Python **descarta** la variable.\n",
    "\n",
    "```python\n",
    "def abrir_otra_ventana(self):\n",
    "    self.hide()\n",
    "    otra_ventana = Ventana(\"Otra ventana\", 300, 100)\n",
    "    otra_ventana.show()\n",
    "```\n",
    "\n",
    "El método `abrir_otra_ventana` guarda en la variable `otra_ventana` el nuevo *widget* a mostrar. Pero, como `otra_ventana` es una variable local del método, cuando termine de ejecutarse, `otra_ventana` será descartada completamente, eliminando incluso el *widget* recién creado (y mostrado).\n",
    "\n",
    "La diferencia en el segundo código, es que `otra_ventana` existe fuera de la definición del método, por lo que el hecho de que termine el método, no genera que se descarte la variable y objeto.\n",
    "\n",
    "Siguiendo esa misma idea, entonces, almacenar la variable `otra_ventana` como un atributo de instancia debería también arreglar el problema. Y es cierto:\n",
    "\n",
    "Este código se encuentra en el archivo `5-ejemplo-conexion-entre-ventanas/3_abre-otras-ventanas-infinitas.py`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```python\n",
    "import sys\n",
    "from PyQt6.QtWidgets import QApplication, QWidget, QPushButton\n",
    "\n",
    "\n",
    "class Ventana(QWidget):\n",
    "    def __init__(self, titulo: str, x: int, y: int) -> None:\n",
    "        super().__init__()\n",
    "        self.setWindowTitle(titulo)\n",
    "        self.setGeometry(x, y, 200, 50)\n",
    "        self.boton = QPushButton(\"Abrir otra ventana\", self)\n",
    "        self.boton.clicked.connect(self.abrir_otra_ventana)\n",
    "        self.show()\n",
    "\n",
    "    def abrir_otra_ventana(self) -> None:\n",
    "        self.hide()\n",
    "        self.otra_ventana = Ventana(\"Otra ventana\", 300, 100)\n",
    "        self.otra_ventana.show()\n",
    "\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    def hook(type, value, traceback) -> None:\n",
    "        print(type)\n",
    "        print(traceback)\n",
    "\n",
    "    sys.__excepthook__ = hook\n",
    "\n",
    "    app = QApplication([])\n",
    "    ventana = Ventana(\"Inicial\", 100, 100)\n",
    "    sys.exit(app.exec())\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "🤔 ¿Por qué? 🤔\n",
    "\n",
    "Porque si se almacena la segunda instancia de ventana como un atributo de la primera, al salir del método `abrir_otra_ventana` no se descarta al objeto ventana; este quedó referenciado en un nivel mayor al método: en la  instancia de la primera ventana.\n",
    "\n",
    "Por lo tanto **siempre hay que guardar una referencia de nuestros objetos PyQt en memoria**, ya sea como atributo o dentro de una estructura de datos (lista por ejemplo) y que dicha lista está como atributo."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### ¿Es esa la mejor manera de modelar este comportamiento?\n",
    "\n",
    "La solución anterior funciona, y para casos pequeños, basta. El problema está en aplicaciones más grandes, donde múltiples ventanas pueden llamarse y hacerse aparecer. Es más, la modelación anterior muestra una dependencia entre las ventanas. En particular, la segunda solo existe bajo la existencia de la primera, y esta modelación no aplica para todos los casos. \n",
    "\n",
    "Múltiples ventanas pueden ser independientes pero aún así puede gatillarse un evento que haga aparecer una desde otra. \n",
    "\n",
    "🤔 ¿Cómo puede modelarse esto? 🤔\n",
    "\n",
    "### ¡Señales al rescate!\n",
    "\n",
    "Como a casi todos nuestros problemas, señales son la solución. De forma similar a como se mostró en el cuaderno sobre *front-end* y *back-end*, señales ayudan a des-acoplar programas. De la misma forma, el uso de señales en este contexto es ventajoso ya que nos permite independizar ventanas y conectarlas mediante señales.\n",
    "\n",
    "Este código se encuentra en el archivo `5-ejemplo-conexion-entre-ventanas/4_abre-ventanas-con-signal.py`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```python\n",
    "import sys\n",
    "from PyQt6.QtWidgets import QApplication, QWidget, QPushButton\n",
    "from PyQt6.QtCore import pyqtSignal\n",
    "\n",
    "\n",
    "class Ventana(QWidget):\n",
    "    # Otra señal para avisar a una segunda ventana.\n",
    "    senal_abrir_otra_ventana = pyqtSignal()\n",
    "\n",
    "    def __init__(self, titulo: str, x: int, y: int) -> None:\n",
    "        super().__init__()\n",
    "        # Definimos lo básico de la ventana.\n",
    "        self.setWindowTitle(titulo)\n",
    "        self.setGeometry(x, y, 200, 50)\n",
    "\n",
    "        # Creamos botón que se conecta a método self.abrir_otra_ventana.\n",
    "        self.boton = QPushButton(\"Abrir otra ventana\", self)\n",
    "        self.boton.clicked.connect(self.abrir_otra_ventana)\n",
    "\n",
    "    def abrir_otra_ventana(self) -> None:\n",
    "        self.hide()\n",
    "        self.senal_abrir_otra_ventana.emit()\n",
    "\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    def hook(type, value, traceback) -> None:\n",
    "        print(type)\n",
    "        print(traceback)\n",
    "\n",
    "    sys.__excepthook__ = hook\n",
    "\n",
    "    app = QApplication([])\n",
    "\n",
    "    # Instanciamos dos ventanas distintas. Cada una comienza con una señal\n",
    "    # propia que le permite ser abierta por otra.\n",
    "    ventana_1 = Ventana(\"Inicial\", 100, 100)\n",
    "    ventana_2 = Ventana(\"Alternativa\", 500, 100)\n",
    "\n",
    "    ventana_1.senal_abrir_otra_ventana.connect(ventana_2.show)\n",
    "    ventana_2.senal_abrir_otra_ventana.connect(ventana_1.show)\n",
    "\n",
    "    ventana_1.show()\n",
    "    sys.exit(app.exec())\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Así, cada instancia de `Ventana` es independiente. Es mediante señales que se comunican que una ventana debe aparecer luego de la otra, sin contener directamente una instancia dentro de otra instancia de `Ventana`."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.10"
  },
  "varInspector": {
   "cols": {
    "lenName": 16,
    "lenType": 16,
    "lenVar": 40
   },
   "kernels_config": {
    "python": {
     "delete_cmd_postfix": "",
     "delete_cmd_prefix": "del ",
     "library": "var_list.py",
     "varRefreshCmd": "print(var_dic_list())"
    },
    "r": {
     "delete_cmd_postfix": ") ",
     "delete_cmd_prefix": "rm(",
     "library": "var_list.r",
     "varRefreshCmd": "cat(var_dic_list()) "
    }
   },
   "types_to_exclude": [
    "module",
    "function",
    "builtin_function_or_method",
    "instance",
    "_Feature"
   ],
   "window_display": false
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
