{
 "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</font>\n",
    "</p>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Tabla de contenidos\n",
    "\n",
    "1. [Interfaces gráficas](#Interfaces-gráficas)\n",
    "2. [PyQt](#PyQt)\n",
    "    1. [Instalación de PyQt](#Instalación-de-PyQt)\n",
    "    2. [Ejecución de código de ejemplo del material que usa PyQt](#Ejecución-de-código-de-ejemplo-del-material-que-usa-PyQt)\n",
    "    3. [Creación de una ventana](#Creación-de-una-ventana)\n",
    "    4. [Debuggeo en PyQt](#Debuggeo-en-PyQt)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Interfaces gráficas\n",
    "\n",
    "Hasta ahora, los programas que hemos desarrollado han interactuado con el usuario únicamente a través de la línea de comandos, esperando que el usuario ingrese datos de entrada, y generan datos de salida a través de una consola.\n",
    "\n",
    "Sin embargo, la interacción entre humano y computador puede ser mucho más rica. Esto ya lo entendía en los años 1960s un grupo del Stanford Research Institute (SRI) que, liderados por [Doug Engelbart](https://en.wikipedia.org/wiki/Douglas_Engelbart) (1925-2013), crearon las **GUI** (*Graphical User Interface*) o **interfaces gráficas de usuario** como una manera de interactuar con el computador mediante un conjunto de abstracciones gráficas como ventanas, íconos, menúes, hipertexto, y un novedoso dispositivo llamado *mouse* capaz de controlar un puntero en pantalla. Su trabajo fue presentado en un evento histórico que se conoció posteriormente como [The Mother of All Demos](https://www.youtube.com/watch?v=yJDv-zdhzMY) (vale la pena verlo con los ojos de 1968). Tuvo tanta influencia que este tipo de interfaces se empezó a usar en el centro de investigación de Xerox PARC (Xerox Palo Alto Research Center) y en particular en su computador Xerox Alto. En los años siguientes, los sistemas operativos para computadores personales incluirían paulatinamente a las GUI como su principal medio de interacción con el usuario. Doug Engelbart obtendría, en el año 1997, el Turing Award por su visión del futuro de la computación interactiva.\n",
    "\n",
    "Hasta nuestros tiempos, las GUI han evolucionado al punto que para la mayoría de los usuarios son la manera más natural de interactuar con un programa ya que ofrecen un entorno amigable y fácil de entender. Es más, la mayoría de los usuarios no conocen la interacción con computador previa a las GUI. Usando elementos de interfaces gráficas, los programas pueden facilitar el ingreso de parámetros, el despliegue de resultados y elementos de control de una aplicación. Podemos ver algunos ejemplos en los formularios web, los escritorios de trabajo de los sistemas operativos, los entornos de ventanas, y los menúes desplegables en aplicaciones de escritorio y de teléfonos móviles."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Pero, ¿cómo podemos proveer este tipo de interacción? Los programas que hemos desarrollado hasta ahora, y que funcionan siguiendo un único flujo de interacción, tendrían que ejecutar permanentemente un ciclo en que consulten a cada elemento de la interfaz si deben ejecutar alguna acción:\n",
    "\n",
    "```python\n",
    "while True:\n",
    "   Revisar si hay entrada desde el teclado\n",
    "   Revisar si hay entrada desde el mouse\n",
    "   Revisar si hay acción sobre ventana 1 y procesarla\n",
    "   Revisar si hay acción sobre ventana 2 y procesarla\n",
    "   ...\n",
    "```\n",
    "\n",
    "Este modo de operación se conoce como *polling* pues consiste en revisar activamente cada elemento de la interfaz para determinar si debe ejecutarse alguna acción. Este modelo, sin embargo, es bastante costoso para el sistema, ya que la mayoría de las veces no habrá ninguna acción que ejecutar.\n",
    "\n",
    "Las aplicaciones modernas, por el contrario, utilizan un modelo de **arquitectura basada en manejo de eventos** para implementar interfaces gráficas efectivas. En una arquitectura basada en manejo de eventos: un **evento** es una acción que ocurre en el programa, gatillada ya sea por el usuario o por otra parte del programa, y a la cual se le puede definir un comportamiento o un efecto. Para el caso de las interfaces gráficas, nos interesan los eventos que representan acciones realizadas por el usuario. Algunos ejemplos de eventos son:\n",
    "\n",
    "- El usuario hizo clic en el botón 1.\n",
    "- El usuario abrió una ventana.\n",
    "- El usuario cerró una ventana.\n",
    "- El usuario presionó una tecla.\n",
    "- El mouse se encuentra sobre una imagen.\n",
    "\n",
    "Usando esta arquitectura, podemos definir el comportamiento que debe tener el programa cada vez que ocurra un evento mediante funciones que se hacen cargo de un evento de manera **asíncrona** (esto es, no son manejados necesariamente por el flujo principal del programa). Esto ocurre de la siguiente forma:\n",
    "\n",
    "- Para cada evento `e` definimos una función `e_handler`, que se ejecutará cada vez que ocurra el evento `e`. *Handler* se traduce a **manejador**, que refleja que `e_handler` es una función que **maneja** el comportamiento del programa cuando el evento `e` ocurre.\n",
    "- El programa debe detectar la ocurrencia del evento `e` y ejecutar la función `e_handler` inmediatamente, es decir, sin tener que esperar a que otras acciones que se están ejecutando terminen.\n",
    "\n",
    "Existen múltiples módulos que proveen elementos gráficos genéricos como: botones, barras de estado, cuadros de texto, calendarios, etcétera. Estos módulos facilitan enormemente el desarrollo de aplicaciones con interfaces gráficas.\n",
    "\n",
    "Así como es posible desarrollar interfaces gráficas de escritorio, como veremos esta semana, también existe un campo completo de desarrollo de interfaces para entornos web, que incluyen sus propios patrones de programación de interfaces, pero en su mayoría también siguen la arquitectura basada en manejo de eventos.\n",
    "\n",
    "Existen muchos *frameworks* de desarrollo para **GUI** de escritorio. Un *framework* se puede entender como un entorno de desarrollo o conjunto de reglas estandarizadas y definidas para crear algo, en este caso, GUI. Cada *framework* se adapta a distintos sistemas operativos (Ubuntu, macOS, Windows, Android, entre otros). En este curso, nos centraremos en uno de estos *frameworks*: **PyQt**; específicamente **PyQt6**."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## PyQt\n",
    "\n",
    "[PyQt](https://riverbankcomputing.com/software/pyqt/intro) es un *framework* multi-plataforma (soportado en múltiples sistemas operativos) que permite construir interfaces gráficas. Está basado en la biblioteca de C++ [Qt](https://www.qt.io/download) para interfaces gráficas. PyQt se encuentra dividida en un conjunto de módulos que proveen distintas funcionalidades. Algunos de ellos son:\n",
    "\n",
    "- **QtWidgets**: contiene las clases que brindan los elementos clásicos de interfaces gráficas para aplicaciones en _desktop_ PCs.\n",
    "- **QtCore**: incluye las clases para funcionalidades no-GUI, como: ciclo de eventos, manejo de archivos, tiempo, _threads_, etc.\n",
    "- **QtGui**: contiene las classes con componentes para integración de ventanas, manejo de eventos, etc.\n",
    "- **QtNetwork**: provee las clases para crear aplicaciones gráficas en entornos de red basadas en TCP/IP, UDP.\n",
    "- **QtOpenGL**: incluye las clases para el uso de OpenGL durante renderizado 3D.\n",
    "- **QtSvg**: provee de clases para mostrar archivos de gráficos vectoriales (SVG).\n",
    "- **QtSql**: incluye funcionalidades para el trabajo con bases de datos SQL.\n",
    "- **QtBluetooth**: contiene clases que permiten la búsqueda e interacción con dispositivos a través de Bluetooth.\n",
    "\n",
    "Puedes encontrar el detalle de todas las funcionalidades de PyQt6 en la [documentación oficial](https://www.riverbankcomputing.com/static/Docs/PyQt6/introduction.html#pyqt6-components)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Instalación de PyQt\n",
    "\n",
    "Antes de comenzar a utilizar esta herramienta, se debe instalar. Como se trata como una librería externa en Python, se instala como cualquier otra librería. Puedes encontrar instrucciones de instalación en la [página del curso](https://github.com/IIC2233/Syllabus/wiki), sección **Software Obligatorio**. De tener cualquier problema de instalación, recomendamos escribirnos en las [discussions del curso](https://github.com/IIC2233/Syllabus/discussions).\n",
    "\n",
    "### Ejecución de código de ejemplo del material que usa PyQt\n",
    "\n",
    "Lamentablemente, Jupyter no fue creado con la intención de ejecutar código de GUI de escritorio, por lo que se hace difícil ejecutar e interactuar con las interfaces a través de esta herramienta. Para entender los ejemplos de código relacionados con PyQt, **recomendamos fuertemente** que ejecutes los *scripts* de interfaces gráficas desde tu propio computador, y **NO** en este *notebook*. Para esto, se agregó una carpeta llamada `scripts/` que tiene cada código que se mostrará en este y los siguientes _notebooks_ de esta semana.\n",
    "\n",
    "**Importante:** Al momento de ejecutar el código, asegúrate de que tu terminal esté posicionada en el mismo directorio donde está el archivo `.py` a ejecutar."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Creación de una ventana\n",
    "\n",
    "En PyQt, los elementos básicos que permiten recibir eventos para interactuar con el usuario y que permiten desplegar una representación gráfica en la pantalla se conocen como *widgets*. \n",
    "\n",
    "Para crear una ventana, usamos la clase `QWidget` desde el módulo `QtWidgets` en `PyQt6`. \n",
    "\n",
    "El primer paso es crear la aplicación que contendrá la ventana y todos los ***widgets*** dentro de esa ventana. Hacemos esto mediante la clase `QApplication`, también del módulo `QtWidgets`. Esta clase contiene el *loop* de eventos (*main loop*), y maneja, entre otras cosas, acciones para inicializar y cerrar los *widgets* de la aplicación.\n",
    "\n",
    "La clase `QApplication` debe ser instanciada **antes** que todos los demás *widgets*. Por cada aplicación que use PyQt, existe **solo una instancia** de `QApplication`, independientemente del número de ventanas que ésta tenga. `QApplication` recibe como argumento parámetros que por lo general son entregados desde la línea de comandos. En general no construiremos aplicaciones que reciban tales parámetros, así que generalmente instanciaremos `QApplication` con una lista vacía como `QApplication([])`.\n",
    "\n",
    "El siguiente ejemplo muestra cómo crear una ventana; este código se encuentra en el archivo `1-interfaces-graficas/ventana-simple.py`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```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",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La clase `QWidget`, de la cual hereda `MiVentana`, representa un elemento gráfico y es la clase base de todos los objetos de la interfaz. Esta clase puede recibir todos los eventos del sistema (*mouse*, teclado, y otros), y puede mostrar una representación de ella en pantalla. La representación en este caso es una ventana vacía. Al inicializar nuestro objeto `MiVentana()` mediante `__init__`, aprovechamos de definir las propiedades de la ventana mediante el método `setGeometry` y `setWindowTitle`, métodos de `QWidget`. Todos los *widgets* son rectangulares.\n",
    "\n",
    "En el programa principal (`'__main__'`), después de que creamos una instancia de `MiVentana`, esta solo existe en memoria. Para mostrar la ventana en la pantalla, usamos su método `show()`. Finalmente, el método `exec()` de `QApplication` ejecuta el *main loop*, que permite iniciar la detección de todos los eventos del sistema.\n",
    "\n",
    "Puedes probar por tu cuenta que si comentas la línea `app = QApplication([])` y ejecutas, tu programa lanzará error debido a que no hay una aplicación instanciada. De forma alternativa, si duplicas esa línea, también encontrarás un error, ya que habrán dos instancias de aplicación, cuando solo puede haber una."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El resultado del código anterior corresponde a una interfaz con una ventana vacía. A continuación, te mostramos cómo se vería en distintos sistemas operativos:\n",
    "\n",
    "| ![](img/PyQt-empty-window-generico.png) | ![](img/PyQt-empty-window-mac.png) | ![](img/PyQt-empty-window-windows.png) | ![](img/PyQt-empty-window-linux.png) |\n",
    "| - | - | - | - |\n",
    "| Ventana genérica | Ventana de MacOS (Big Sur) | Ventana de Windows 8.1 | Ventana de Linux (KDE) |\n",
    "\n",
    "En los próximos *notebooks* de esta semana, se utilizará la ventana genérica para mostrar posibles resultados esperados. Ten en consideración que dependiendo del entorno en que se ejecute el código, puede causar que las ventanas y sus componentes se vean ligeramente distintas en diferentes computadores. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### *Debuggeo* en PyQt\n",
    "\n",
    "Es posible que durante la creación de interfaces usando PyQt, errores inesperados ocurran y tu programa se **caiga**. Estamos acostumbrados a que esto ocurra, y generalmente nos enfocamos en el error que arroja la consola para guiarnos en el error que se comete. Pero ocurre seguido con PyQt que el programa **se cae y no aparece un error en pantalla** que nos pueda dar una pista de lo que ocurrió. \n",
    "\n",
    "Para evitar utilizar `print` para saber dónde está el error, puedes usar el siguiente código que para la mayoría de los casos logra obtener el error de origen y lo imprime en consola:\n",
    "\n",
    "```python\n",
    "if __name__ == '__main__':\n",
    "    def hook(type, value, traceback) -> None:\n",
    "        print(type)\n",
    "        print(traceback)\n",
    "    sys.__excepthook__ = hook\n",
    "\n",
    "    app = QtWidgets.QApplication([])\n",
    "    ventana = MiVentana(*args)\n",
    "    ventana.show()\n",
    "    sys.exit(app.exec())\n",
    "```\n",
    "> Código de cortesía de Felipe Pezoa, 2017."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "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.13.3"
  },
  "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
}
