{
 "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'>&copy; 2015 Karim Pichara - Christian Pieringer. Todos los derechos reservados. Editado por Equipo Docente IIC2233 2018-1 al 2025-2, y extendido con material creado en 2017-2 por Hugo Navarrete e Ignacio Acevedo.</font>\n",
    "</p>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Tabla de contenidos\n",
    "\n",
    "1. [Sistema coordenado en PyQt](#Sistema-coordenado-en-PyQt)\n",
    "2. [_Widgets_](#Widgets)\n",
    "    1. [Etiquetas y cuadros de texto](#Etiquetas-y-cuadros-de-texto)\n",
    "    2. [Imágenes](#Imágenes)\n",
    "    3. [Botones](#Botones)\n",
    "    4. [_Layouts_](#Layouts)\n",
    "    5. [*Grid Layout*](#Grid-Layout)\n",
    "    6. [Aplicando POO para crear *widgets*](#Aplicando-POO-para-crear-widgets)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Sistema coordenado en PyQt\n",
    "\n",
    "Antes de aprender cómo agregar elementos gráficos a nuestras ventanas, debemos entender el sistema coordenado que utiliza PyQt para posicionar ventanas y su contenido. En el cuaderno anterior, revisamos el primer ejemplo de ventana con el siguiente código:\n",
    "\n",
    "```python\n",
    "import sys\n",
    "from PyQt6.QtWidgets import QWidget, QApplication\n",
    "\n",
    "\n",
    "class MiVentana(QWidget):\n",
    "    def __init__(self) -> None:\n",
    "        super().__init__()\n",
    "\n",
    "        # Definimos la geometría de la ventana.\n",
    "        # Parámetros: (x_superior_izq, y_superior_izq, ancho, alto)\n",
    "        self.setGeometry(200, 100, 300, 300)\n",
    "\n",
    "        # Podemos dar nombre a la ventana (Opcional).\n",
    "        self.setWindowTitle('Mi Primera Ventana')\n",
    "\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    def hook(type, value, traceback) -> None:\n",
    "        print(type)\n",
    "        print(traceback)\n",
    "    sys.__excepthook__ = hook\n",
    "\n",
    "    app = QApplication([])  # Creamos las base de la app: QApplication.\n",
    "    ventana = MiVentana()   # Construimos un QWidget que será nuestra ventana.\n",
    "    ventana.show()          # Mostramos la ventana.\n",
    "    codigo = app.exec()     # La aplicación se inicia con app.exec().\n",
    "                            # Esto habilita el loop de eventos y retorna un\n",
    "                            # código de salida\n",
    "    print(f\"Exit code: {codigo}\")\n",
    "    sys.exit(codigo)        # sys.exit termina el programa con el codigo de\n",
    "                            # salida que tuvo la \"app\"\n",
    "                            # 0 significa que se terminó sin errores\n",
    "\n",
    "```\n",
    "\n",
    "Si ejecutas varias veces el ejemplo anterior, te darás cuenta que la ventana que aparece siempre lo hace en la misma posición de tu pantalla, y tiene las mismas dimensiones. Esto no es coincidencia, y es gracias a que dentro del constructor de la clase `MiVentana`, se definió la geometría de la ventana, con la línea: `self.setGeometry(200, 100, 300, 300)`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Pero, ¿qué significan estos valores? Como dice el comentario antes de la línea, los parámetros de `setGeometry` son, en orden: posición horizontal de esquina superior izquierda; posición vertical de esquina superior izquierda; ancho de rectángulo y alto de rectángulo. Con estos valores, se definen completamente las dimensiones y posición de una ventana rectangular.\n",
    "\n",
    "Entonces, en el ejemplo, `self.setGeometry(200, 100, 300, 300)` crea una ventana de 300 por 300, y cuya esquina superior izquierda se encuentra en el punto (200, 100) de tu pantalla. El detalle, es que el sistema coordenado considerado para esta posición, tiene su origen en la **esquina superior izquierda de tu pantalla**. Donde además, los valores horizontales (eje X) crecen hacia la **derecha**, y los valores verticales (eje Y) crecen hacia **abajo**.\n",
    "\n",
    "![](img/PyQt-coordinates.png)\n",
    "\n",
    "Luego, si alteras la línea a `self.setGeometry(300, 200, 300, 300)`, notarás que la ventana aparecerá un poco más lejos de la esquina superior izquierda de tu pantalla, ya que antes estaba en (200, 100), y ahora está en (300, 200). Prueba modificando estos valores y viendo cómo se alteran las dimensiones y posición de la ventana."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### _Widgets_\n",
    "\n",
    "A continuación, explicamos algunas de los _widgets_ más básicos que existen en el mundo de PyQt, pues existen muchas otros más. Como se frecuenta en el mundo de programación, puedes encontrar información sobre otros *widgets* en la web, y buscar su documentación para entender cómo se comportan, y qué tipo de atributos y métodos poseen."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Etiquetas y cuadros de texto\n",
    "\n",
    "PyQt provee *widgets* para controlar el ingreso y salida de información. Los más comunes son **etiquetas** y los **cuadros de texto**. Las etiquetas o _labels_ permiten desplegar textos estáticos o dinámicos. PyQt representa etiquetas mediante el *widget* `QLabel`. Por otro lado, los cuadros de texto o _line edits_, se usan principalmente para recibir texto ingresado por el usuario. PyQt representa cuadros de texto mediante el *widget* `QLineEdit`. \n",
    "\n",
    "El siguiente ejemplo muestra cómo incluir ambos elementos dentro de la interfaz gráfica creada en el ejemplo anterior. Este código se encuentra en el archivo `2-elementos-graficos-basicos/1_label-y-lineedit.py`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```python\n",
    "import sys\n",
    "from PyQt6.QtWidgets import QApplication, QWidget, QLabel, QLineEdit\n",
    "\n",
    "\n",
    "class MiVentana(QWidget):\n",
    "    def __init__(self, *args, **kwargs) -> None:\n",
    "        \"\"\"\n",
    "        Este método inicializa la ventana.\n",
    "        \"\"\"\n",
    "        super().__init__(*args, **kwargs)\n",
    "\n",
    "        # Llamamos a un método propio que\n",
    "        # inicializa los elementos de la ventana.\n",
    "        self.init_gui()\n",
    "\n",
    "    def init_gui(self) -> None:\n",
    "        \"\"\"\n",
    "        Este método configura la interfaz y todos sus widgets,\n",
    "        posterior a __init__().\n",
    "        \"\"\"\n",
    "        # Ajustamos la geometría de la ventana y su título.\n",
    "        self.setGeometry(200, 100, 200, 300)\n",
    "        self.setWindowTitle('Ventana con label y cuadro de texto')\n",
    "\n",
    "        # Agregamos etiquetas usando el widget QLabel(texto_inicial, padre).\n",
    "        # Posteriormente, las ubicamos en la ventana.\n",
    "        self.label1 = QLabel('Texto:', self)\n",
    "        self.label1.move(10, 15)\n",
    "\n",
    "        self.label2 = QLabel('Esta etiqueta es variable', self)\n",
    "        self.label2.move(10, 50)\n",
    "\n",
    "        # Agregamos cuadros de texto mediante QLineEdit(texto_inicial, padre).\n",
    "        # Posteriormente, definimos su posición y porte en la ventana.\n",
    "        self.edit = QLineEdit('', self)\n",
    "        self.edit.setGeometry(45, 15, 100, 20)\n",
    "\n",
    "        # Una vez que fueron agregados todos los elementos a la ventana,\n",
    "        # la desplegamos en pantalla.\n",
    "        self.show()\n",
    "\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    def hook(type, value, traceback) -> None:\n",
    "        print(type)\n",
    "        print(traceback)\n",
    "    sys.__excepthook__ = hook\n",
    "\n",
    "    \"\"\"\n",
    "    Recordar que en el programa principal debe existir una instancia de\n",
    "    QApplication ANTES de crear los demás widgets, incluida la ventana\n",
    "    principal.\n",
    "    Si la aplicación no recibe parámetros desde la línea de comandos,\n",
    "    QApplication recibe una lista vacía como input.\n",
    "    \"\"\"\n",
    "\n",
    "    app = QApplication([])\n",
    "    ventana = MiVentana()\n",
    "    sys.exit(app.exec())\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Luego de ejecutar el código, se despliega una ventana con dos etiquetas y un cuadro de texto como la siguiente:\n",
    "\n",
    "![](img/PyQt-windows-labels.png)\n",
    "\n",
    "En el método `init_gui`, el *widget* principal crea y posiciona instancias de `QLabel` y de `QLineEdit`. Realiza esto instanciándolos con el texto que contendrán, y usando los métodos `move` y `setGeometry`. Estos dos últimos métodos son capaces de mover la posición de elementos dentro de la ventana, usando el mismo sistema coordenado mencionado anteriormente, pero cuyo origen (posición 0,0) es la esquina superior izquierda de la **ventana  principal**.\n",
    "\n",
    "Estos *widgets* deben estar contenidos dentro de otro *widget* (el *parent*, o *widget* padre). En el ejemplo, debido a que los *widgets* que creamos serán parte del *widget* principal (la ventana que estamos creando), se les entrega `self` como argumento en el inicializador de cada elemento. **Prueba no entregando `self` a las instancias de `QLabel` y de `QLineEdit`, y observarás que no aparecen los elementos dentro de la interfaz.** ¿Por qué? En ese caso, solo se crean instancias de los elementos, pero al no tener un *widget* padre al que pertenecen, no se visualizan."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Imágenes\n",
    "\n",
    "También es posible agregar imágenes propias a una ventana de PyQt. Una forma de hacerlo, es mediante la clase `QPixMap` del módulo `QtGui`. Este carga un conjunto de pixeles que pueden originarse de un archivo de imagen. Para agregarlo a la ventana, deben cargarse esos pixeles dentro de un elemento `QLabel`, que conocimos en el ejemplo anterior. \n",
    "\n",
    "El siguiente ejemplo muestra una pequeña ventana que carga y muestra una imagen de una *Python*. Este código se encuentra en el archivo `2-elementos-graficos-basicos/2_pixmap.py`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```python\n",
    "import sys\n",
    "import os\n",
    "from PyQt6.QtWidgets import QApplication, QWidget, QLabel\n",
    "from PyQt6.QtGui import QPixmap\n",
    "\n",
    "class MiVentana(QWidget):\n",
    "    def __init__(self, *args, **kwargs) -> None:\n",
    "        super().__init__(*args, **kwargs)\n",
    "        self.init_gui()\n",
    "\n",
    "    def init_gui(self) -> None:\n",
    "        \"\"\"\n",
    "        Este método inicializa la interfaz y todos sus widgets.\n",
    "        \"\"\"\n",
    "\n",
    "        # Ajustamos la geometría de la ventana y su título.\n",
    "        self.setGeometry(200, 100, 200, 200)\n",
    "        self.setWindowTitle('Ventana con imagen')\n",
    "\n",
    "        # Creamos el QLabel que contendrá la imagen y definimos su tamaño.\n",
    "        self.label = QLabel(self)\n",
    "        self.label.setGeometry(50, 50, 100, 100)\n",
    "\n",
    "        # Escribimos la ruta al archivo que contiene la imagen.\n",
    "        ruta_imagen = os.path.join('img', 'python.jpg')\n",
    "\n",
    "        # Cargamos la imagen como pixeles.\n",
    "        pixeles = QPixmap(ruta_imagen)\n",
    "\n",
    "        # Agregamos los pixeles al elemento QLabel.\n",
    "        self.label.setPixmap(pixeles)\n",
    "\n",
    "        # Finalmente, ajustamos el tamaño del contenido al\n",
    "        # tamaño del elemento (100 x 100).\n",
    "        self.label.setScaledContents(True)\n",
    "\n",
    "        # Una vez que fueron agregados todos los elementos a la ventana la\n",
    "        # desplegamos en pantalla.\n",
    "        self.show()\n",
    "\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    def hook(type, value, traceback) -> None:\n",
    "        print(type)\n",
    "        print(traceback)\n",
    "    sys.__excepthook__ = hook\n",
    "\n",
    "    app = QApplication([])\n",
    "    ventana = MiVentana()\n",
    "    sys.exit(app.exec())\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Podemos ver el resultado del código anterior en la siguiente imagen:\n",
    "\n",
    "![](img/PyQt-window-image.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Botones\n",
    "\n",
    "PyQt provee *widgets* útiles para controlar la interfaz. El más básico de ellos es el botón, que se construye con el *widget* `QPushButton`. Este *widget* recibe un texto inicial, y el *widget* que lo contiene (su *parent*).\n",
    "\n",
    "El siguiente ejemplo muestra una ventana con un botón. Este código se encuentra en el archivo `2-elementos-graficos-basicos/3_pushbutton.py`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```python\n",
    "import sys\n",
    "from PyQt6.QtWidgets import (QApplication, QWidget, QLabel, QLineEdit,\n",
    "                             QPushButton)\n",
    "\n",
    "\n",
    "class MiVentana(QWidget):\n",
    "    def __init__(self, *args, **kwargs) -> None:\n",
    "        super().__init__(*args, **kwargs)\n",
    "        self.init_gui()\n",
    "\n",
    "    def init_gui(self) -> None:\n",
    "        \"\"\"\n",
    "        Este método inicializa la interfaz y todos sus widgets.\n",
    "        \"\"\"\n",
    "\n",
    "        # Ajustamos la geometría de la ventana\n",
    "        self.setGeometry(200, 100, 200, 300)\n",
    "        self.setWindowTitle('Ventana con botón')\n",
    "\n",
    "        # Podemos agrupar conjuntos de widgets en alguna estructura\n",
    "        self.labels = {}\n",
    "        self.labels['label1'] = QLabel('Texto:', self)\n",
    "        self.labels['label1'].move(10, 15)\n",
    "        self.labels['label2'] = QLabel('Aquí se escribe la respuesta', self)\n",
    "        self.labels['label2'].move(10, 50)\n",
    "\n",
    "        self.edit1 = QLineEdit('', self)\n",
    "        self.edit1.setGeometry(45, 15, 100, 20)\n",
    "\n",
    "        \"\"\"\n",
    "        El uso del carácter '&' al inicio del texto de algún botón o menú\n",
    "        permite que la primera letra del mensaje mostrado esté destacada,\n",
    "        la visualización depende de la plataforma utilizada.\n",
    "        El método sizeHint provee un tamaño sugerido para el botón.\n",
    "        \"\"\"\n",
    "        self.boton1 = QPushButton('&Procesar', self)\n",
    "        self.boton1.resize(self.boton1.sizeHint())\n",
    "        self.boton1.move(5, 70)\n",
    "\n",
    "        # Una vez que fueron agregados todos los elementos a la ventana,\n",
    "        # la desplegamos en pantalla.\n",
    "        self.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 = MiVentana()\n",
    "    ventana.show()\n",
    "    sys.exit(app.exec())\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El resultado que genera el código anterior es una ventana con un botón como la mostrada en la siguiente figura:\n",
    "\n",
    "![](img/PyQt-window-button.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### *Layouts*\n",
    "\n",
    "Los *layouts* permiten manejar de manera más flexible y práctica la distribución de los *widgets* en una ventana. Hasta ahora hemos usado los métodos `setGeometry(x, y, ancho, alto)` y `move(x, y)` de cada *widget* para hacer un posicionamiento absoluto de cada objeto dentro de la ventana que lo contiene. Si bien esto funciona, también tiene limitantes que provocan que:\n",
    "\n",
    "- La posición de un *widget* no cambie si cambia el tamaño de la ventana que lo contiene, es decir, los objetos mantendrán su tamaño y posición. (Prueba modificando el tamaño de la ventana principal con el *mouse*.)\n",
    "- La aplicación se verá distinta en varias plataformas o configuraciones de pantalla.\n",
    "\n",
    "Para evitar rehacer una ventana cada vez que se cambian sus dimensiones, se utilizan ***box layouts***. Existen dos tipos básicos en PyQt que permiten alinear los *widgets* horizontal y verticalmente: ```QHBoxLayout``` y ```QVBoxLayout``` del módulo `QtWidgets`. En ambos casos, los *widgets* dentro del *layout* se organizan ocupando todo el espacio disponible, incluso si la ventana es maximizada. Los objetos deben ser agregados a cada *layout* mediante el método ```addWidget(widget)```. Finalmente, el *box* definido debe ser cargado a la ventana usando ```self.setLayout()```. Es posible agregar la alineación vertical de los objetos incluyendo el *layout* horizontal dentro de uno vertical. Además, como los *layouts* son un *widget*, se pueden colocar unos dentro de otros.\n",
    "\n",
    "![](img/expl-layouts.png)\n",
    "\n",
    "El siguiente ejemplo muestra cómo crear un *layout* para que tres *widgets* queden alineados en la esquina inferior derecha. Este código se encuentra en el archivo `2-elementos-graficos-basicos/4_layout.py`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```python\n",
    "import sys\n",
    "from PyQt6.QtWidgets import (QApplication, QWidget, QPushButton, QLabel,\n",
    "                             QLineEdit, QHBoxLayout, QVBoxLayout)\n",
    "\n",
    "\n",
    "class MiVentana(QWidget):\n",
    "    def __init__(self, *args, **kwargs) -> None:\n",
    "        super().__init__(*args, *kwargs)\n",
    "        self.init_gui()\n",
    "        self.show()\n",
    "\n",
    "    def init_gui(self) -> None:\n",
    "        \"\"\"\n",
    "        Este método configura todos los widgets de la ventana.\n",
    "        \"\"\"\n",
    "        self.setGeometry(100, 100, 300, 300)\n",
    "        self.label1 = QLabel('Texto:', self)\n",
    "        self.edit1 = QLineEdit('', self)\n",
    "        self.edit1.resize(100, 20)\n",
    "        self.boton1 = QPushButton('&Calcular', self)\n",
    "        self.boton1.resize(self.boton1.sizeHint())\n",
    "\n",
    "        \"\"\"\n",
    "        Creamos el layout horizontal y agregamos los widgets mediante el\n",
    "        método addWidget(). El método addStretch() nos permite incluir\n",
    "        opcionalmente espaciadores.\n",
    "        \"\"\"\n",
    "        hbox = QHBoxLayout()\n",
    "        hbox.addStretch(1)\n",
    "        hbox.addWidget(self.label1)\n",
    "        hbox.addWidget(self.edit1)\n",
    "        hbox.addWidget(self.boton1)\n",
    "        hbox.addStretch(1)\n",
    "\n",
    "        \"\"\"\n",
    "        Creamos el layout vertical y le agregamos el layout horizontal.\n",
    "        Opcionalmente agregamos espaciadores para distribuir los widgets.\n",
    "        Notar el juego entre el valor recibido por los espaciadores.\n",
    "        \"\"\"\n",
    "        vbox = QVBoxLayout()\n",
    "        vbox.addStretch(5)\n",
    "        vbox.addLayout(hbox)\n",
    "        vbox.addStretch(1)\n",
    "        self.setLayout(vbox)\n",
    "\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    def hook(type, value, traceback) -> None:\n",
    "        print(type)\n",
    "        print(traceback)\n",
    "    sys.__excepthook__ = hook\n",
    "\n",
    "    app = QApplication([])\n",
    "    ventana = MiVentana()\n",
    "    sys.exit(app.exec())\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La siguiente figura en (b) muestra el resultado de los dos ajustes, horizontal y vertical.\n",
    "Si se altera el código anterior y se comentan las lineas que actúan sobre `vbox` y se reemplaza `self.setLayout(vbox)` por `self.setLayout(hbox)`, se obtiene (a), que solo usa el ajuste horizontal:\n",
    "\n",
    "| ![](img/PyQt-mainwindow-layouts-a.png) | ![](img/PyQt-mainwindow-layouts-b.png) |\n",
    "| - | - |\n",
    "| (a) Solo usando `QHBoxLayout()` | (b) Usando `QHBoxLayout()` y `QVBoxLayout()` |\n",
    "\n",
    "\n",
    "Puedes volver a probar el efecto de ajustar el tamaño de la ventana como el *mouse* y observar cómo los *widgets* que pertenecen al *box layout* se acomodan."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### *Grid Layout*\n",
    "\n",
    "PyQt incluye otro tipo de *layout* que permite distribuir los *widgets* como elementos de un grilla. Éste se llama `QGridLayout`, y divide el espacio de la ventana en filas y columnas. Luego de esto, cada *widget* debe ser agregado a una casilla de la grilla mediante el método `addWidget(widget, i, j)`. Por ejemplo, si necesitamos crear una matriz con botones, similar al teclado de un teléfono móvil, podemos utilizar un *grid layout* como se muestra a continuación. Al igual que con los *layouts* anteriores, podemos componer este *widget* con otros.\n",
    "\n",
    "![](img/expl-grid.png)\n",
    "\n",
    "El siguiente ejemplo muestra cómo crear una ventana con este tipo de _layout_. Este código se encuentra en el archivo `2-elementos-graficos-basicos/5_gridlayout.py`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```python\n",
    "import sys\n",
    "from PyQt6.QtWidgets import QApplication, QWidget, QPushButton, QGridLayout\n",
    "\n",
    "\n",
    "class MiVentana(QWidget):\n",
    "    def __init__(self, *args, **kwargs) -> None:\n",
    "        super().__init__(*args, **kwargs)\n",
    "        self.init_gui()\n",
    "        self.show()\n",
    "\n",
    "    def init_gui(self) -> None:\n",
    "        # Creamos la grilla para ubicar los widgets de manera matricial.\n",
    "        grilla = QGridLayout()\n",
    "\n",
    "        valores = ['1', '2', '3',\n",
    "                   '4', '5', '6',\n",
    "                   '7', '8', '9',\n",
    "                   '*', '0', '#']\n",
    "\n",
    "        # Generamos las posiciones de los botones en la grilla y le asociamos\n",
    "        # el texto que debe desplegar cada botón guardados en la lista valores.\n",
    "        posiciones = [(i, j) for i in range(4) for j in range(3)]\n",
    "\n",
    "        for i in range(len(posiciones)):\n",
    "            posicion = posiciones[i]\n",
    "            valor = valores[i]\n",
    "            boton = QPushButton(valor, self)\n",
    "            grilla.addWidget(boton, *posicion)\n",
    "\n",
    "        self.setLayout(grilla)\n",
    "\n",
    "        self.move(300, 150)\n",
    "        self.setWindowTitle('Celular')\n",
    "\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    def hook(type, value, traceback) -> None:\n",
    "        print(type)\n",
    "        print(traceback)\n",
    "    sys.__excepthook__ = hook\n",
    "\n",
    "    app = QApplication([])\n",
    "    ventana = MiVentana()\n",
    "    sys.exit(app.exec())\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "![](img/PyQt-mainwindow-grid-layout.png)\n",
    "\n",
    "Una vez más, si cambias el tamaño de la ventana principal con el *mouse*, verás que los botones se acomodan y cambian de tamaño de acuerdo al *layout* de la ventana."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Aplicando POO para crear *widgets*"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Toda ventana, botón, u otro instrumento a usar en PyQt es un objeto. No ahondaremos más en cómo funcionan o se organiza la jerarquía entre las distintas clases de PyQt, pero es importante tener en cuenta que podemos aplicar todo el conocimiento de POO con nuestra interfaz gráfica. Podemos entregar características especiales a cada objeto y heredar lo que necesitemos, y así, poder personalizar aún más nuestros programas.\n",
    "\n",
    "Lo más básico, es la capacidad de asignarle atributos y comportamiento a *widgets* simples. El siguiente ejemplo crea una clase `MiBoton` que hereda de `QPushButton`, y es capaz de mantener un conteo de las veces que fue presionado. Este código se encuentra en el archivo `2-elementos-graficos-basicos/6_mi-boton.py`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```python\n",
    "import sys\n",
    "from PyQt6.QtWidgets import QApplication, QWidget, QPushButton\n",
    "\n",
    "\n",
    "class MiBoton(QPushButton):\n",
    "    # Recibe dos argumentos extra, además de los regulares de QPushButton:\n",
    "    # - Un nombre para identificar el botón.\n",
    "    # - Una posición para ubicarse en la ventana.\n",
    "    def __init__(self, nombre: str, pos: tuple, *args, **kwargs) -> None:\n",
    "        # Llama al constructor de la clase madre.\n",
    "        super().__init__(*args, **kwargs)\n",
    "\n",
    "        # Asigna el nombre a la instancia.\n",
    "        self.nombre = nombre\n",
    "\n",
    "        # Crea un contador de instancia inicialmente en 0.\n",
    "        self.contador = 0\n",
    "\n",
    "        # Fija su propia geometría\n",
    "        self.resize(self.sizeHint())\n",
    "        self.move(*pos)\n",
    "\n",
    "        # La siguiente línea conecta un clic con el método contar.\n",
    "        # Entenderemos mejor esta línea en el siguiente notebook\n",
    "        self.clicked.connect(self.contar)\n",
    "\n",
    "    # Agregamos comportamiento al botón, aumenta el contador en cada clic.\n",
    "    def contar(self) -> None:\n",
    "        self.contador += 1\n",
    "        print(f\"{self.nombre} apretado {self.contador} veces.\")\n",
    "\n",
    "\n",
    "class MiVentana(QWidget):\n",
    "    def __init__(self, *args, **kwargs) -> None:\n",
    "        super().__init__(*args, **kwargs)\n",
    "        self.init_gui()\n",
    "        self.show()\n",
    "\n",
    "    def init_gui(self) -> None:\n",
    "        # Fija la geometría de la ventana principal\n",
    "        self.setGeometry(200, 200, 120, 120)\n",
    "        self.setMaximumHeight(120)\n",
    "        self.setMaximumWidth(120)\n",
    "\n",
    "        # Instancia dos botones de nuestra clase, con atributos extra\n",
    "        # de los que QPushButton está acostumbrado: nombre y posición.\n",
    "        self.boton_1 = MiBoton(\"Botón 1\", (10, 20), \"Apriétame\", self)\n",
    "        self.boton_2 = MiBoton(\"Botón 2\", (10, 60), \"Apriétame\", self)\n",
    "\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "    def hook(type, value, traceback) -> None:\n",
    "        print(type)\n",
    "        print(traceback)\n",
    "    sys.__excepthook__ = hook\n",
    "\n",
    "    app = QApplication([])\n",
    "    ventana = MiVentana()\n",
    "    sys.exit(app.exec())\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El código genera una pequeña ventana con dos botones. Si se hace clic sobre ellos, se puede apreciar que se imprime en consola el resultado del método contar de cada instancia del botón, y que el conteo se mantiene por separado para cada botón.\n",
    "\n",
    "![](img/PyQt-mi-boton.png)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Aprovechar POO es conveniente para reutilizar mucho código. Crear una estructura compleja usando interfaces gráficas puede necesitar mucho código, pero si parte de esta estructura es regular, entonces se puede reutilizar el mismo código.\n",
    "\n",
    "Por ejemplo, el siguiente código muestra la ventana de un formulario. Como todo formulario, tiene un formato bastante regular: una etiqueta de texto que pide un campo y un cuadro de texto para ingresar *input*. Si se quiere agregar 6 campos sin utilizar clases, puede significar el escribir una sección de 6 líneas 6 veces, es decir, 36 líneas de pura repetición. En cambio, aquí se reutiliza el código para solo un campo, y se aplica varias veces. Este código se encuentra en el archivo `2-elementos-graficos-basicos/7_mi-formulario.py`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```python\n",
    "import sys\n",
    "from PyQt6.QtWidgets import (QApplication, QWidget, QLabel,\n",
    "                             QHBoxLayout, QVBoxLayout, QLineEdit)\n",
    "\n",
    "\n",
    "class CampoFormulario(QHBoxLayout):\n",
    "    # Heredamos de Layout Horizontal para colocar cada campo.\n",
    "    def __init__(self, texto: str, *args, **kwargs) -> None:\n",
    "        # Llama al constructor de la clase madre.\n",
    "        super().__init__(*args, **kwargs)\n",
    "\n",
    "        # Crea la etiqueta y cuadro correspondientes.\n",
    "        label = QLabel(f\"{texto}: \")\n",
    "        campo = QLineEdit(\"\")\n",
    "\n",
    "        # Los coloca dentro del Layout.\n",
    "        self.addStretch(1)\n",
    "        self.addWidget(label)\n",
    "        self.addWidget(campo)\n",
    "        self.addStretch(1)\n",
    "\n",
    "class Formulario(QWidget):\n",
    "    def __init__(self, *args, **kwargs) -> None:\n",
    "        super().__init__(*args, **kwargs)\n",
    "        # Fija datos de ventana.\n",
    "        self.setWindowTitle(\"Formulario\")\n",
    "        self.setGeometry(200, 200, 400, 400)\n",
    "\n",
    "        # Crea contenedor vertical para colocar los campos.\n",
    "        contenedor = QVBoxLayout()\n",
    "\n",
    "        # Coloca cada campo que creamos.\n",
    "        contenedor.addLayout(CampoFormulario(\"Nombre\"))\n",
    "        contenedor.addLayout(CampoFormulario(\"Apellido\"))\n",
    "        contenedor.addLayout(CampoFormulario(\"Dirección\"))\n",
    "        contenedor.addLayout(CampoFormulario(\"Correo\"))\n",
    "        contenedor.addLayout(CampoFormulario(\"Usuario\"))\n",
    "        contenedor.addLayout(CampoFormulario(\"Contraseña\"))\n",
    "\n",
    "        # Fijamos el Layout completo.\n",
    "        self.setLayout(contenedor)\n",
    "\n",
    "        self.show()\n",
    "\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    def hook(type, value, traceback) -> None:\n",
    "        print(type)\n",
    "        print(traceback)\n",
    "    sys.__excepthook__ = hook\n",
    "\n",
    "    app = QApplication([])\n",
    "    formulario = Formulario()\n",
    "    sys.exit(app.exec())\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Se obtiene la siguiente ventana, con los campos especificados:\n",
    "    \n",
    "![](img/PyQt-mi-form.png)"
   ]
  }
 ],
 "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
}
