Esta guía no te enseña PyQt. Te enseña a empezar a usarlo, a experimentar y a averiguar lo que todavía no sabés. Vas a escribir código antes de entenderlo del todo — y está bien: así se aprende una librería nueva.
Un programador no memoriza una librería.
Aprende a investigarla y utilizarla.
Qué es PyQt y por qué existen varias herramientas gráficas
PyQt es una librería que nos permite crear aplicaciones de escritorio con interfaz gráfica desde Python: ventanas, botones, formularios, tablas, menús. Por dentro usa Qt, un framework escrito en C++ con el que están hechos programas que seguramente usaste. PyQt es el puente que nos deja manejarlo desde Python.
Aplicaciones con ventanas: un gestor, un panel de control, un reproductor, una herramienta interna, un formulario de carga.
Como un programa del sistema operativo. No parece un ejercicio de cátedra: parece software.
Un videojuego con movimiento fluido, física y sprites. Para eso ya conocemos Pygame.
| Herramienta | Qué construye | Cuándo la elegís |
|---|---|---|
| Consola | texto que entra y sale | Cuando lo importante es la lógica y nadie más va a usarlo. |
| Tkinter | ventanas simples | Viene con Python, no se instala nada. Ideal para algo chico y rápido. |
| PyQt | aplicaciones de escritorio | Cuando querés algo más completo, prolijo y que crezca. |
| Pygame | videojuegos | Cuando hay movimiento, colisiones, sonido y un bucle de juego. |
No hay una herramienta «mejor». Hay herramientas distintas para necesidades distintas. Parte de programar es elegir con cuál resolver el problema que tenés enfrente.
PyQt no viene con Python: hay que traerlo
pip install PyQt6
pip no funciona, probá
pip3 install PyQt6 o python -m pip install PyQt6.Descarga el código de la librería y lo guarda en tu Python, una sola vez por computadora (o por entorno virtual).
Trae ese código a este programa, cada vez que lo ejecutás. Sin instalar, el import falla.
Creá un archivo, escribí esta única línea y ejecutalo. Si no aparece ningún error, ya está:
import PyQt6 print("PyQt6 instalado")
Existen las dos versiones y en internet vas a encontrar ejemplos de ambas.
Esta guía usa PyQt6 de principio a fin. Si copiás un ejemplo que dice
PyQt5 y no anda, no está roto: está escrito para otra versión.
En la sección 10 vas a encontrar cómo traducirlo.
pip hasta ahora? ¿Cuáles no hizo falta instalar?Escribilo, ejecutalo, y recién después entendelo
Este es el programa PyQt más chico que hace algo visible. Copialo en un archivo
ventana.py y ejecutalo.
import sys from PyQt6.QtWidgets import QApplication, QWidget app = QApplication(sys.argv) ventana = QWidget() ventana.setWindowTitle("Mi primera aplicación") ventana.resize(500, 300) ventana.show() sys.exit(app.exec())
QApplication? ¿Por qué creés que necesita sys.argv?QWidget?resize()? ¿Y qué pasa si después agrandás la ventana con el mouse?show()?show()? ¿El programa da error, o hace otra cosa?ventana.show() y ejecutá. ¿Qué ves? ¿El programa terminó?sys.exit(app.exec())). ¿Cuánto dura la ventana?resize(500, 300) por resize(200, 800). ¿Qué número es cuál?¿Y si creás dos ventanas en el mismo programa? Probá: creá ventana2,
dale otro título y llamá a show() en las dos. ¿Funciona? ¿Y si al cerrar una
se cierra la otra?
Las piezas con las que se arma una interfaz
Cada pieza visible de la interfaz es un widget: una etiqueta, un botón, un campo
de texto. Todos son objetos — los creás, guardás en una variable y les pedís cosas
con métodos. Esto ya lo sabés hacer: es exactamente lo mismo que hiciste con
Personaje o Arma.
Guardá este archivo como banco.py. Cada widget que quieras probar lo pegás
en el medio, entre las líneas de guiones.
import sys from PyQt6.QtWidgets import QApplication, QWidget, QLabel app = QApplication(sys.argv) ventana = QWidget() ventana.setWindowTitle("Banco de pruebas") ventana.resize(420, 320) # ---------- pegá acá los widgets que pruebes ---------- etiqueta = QLabel("Hola mundo", ventana) etiqueta.move(20, 20) # ------------------------------------------------------ ventana.show() sys.exit(app.exec())
ventana) dice adentro de qué
vive el widget, y move(x, y) lo ubica a mano en esa posición.
Acordate de move(): en la sección 7 vamos a querer sacárnoslo de encima.Si creás un widget y te olvidás el , ventana, no aparece nada — o se abre en una
ventana suelta. No es un bug: el widget no sabe dónde vivir. Y todo widget que uses hay que
importarlo arriba, en la línea del from PyQt6.QtWidgets import ....
Muestra un texto (o una imagen) que el usuario lee pero no edita. Es la salida de tu app.
etiqueta = QLabel("Hola mundo", ventana)
Modificá: ¿cómo le cambiarías el texto después de haberlo creado?
Pista: casi todos los widgets tienen métodos que empiezan
con set. Probá escribir etiqueta.set y mirá qué te sugiere el editor.
Investigá: ¿cómo se centra el texto de un QLabel?
Lo que el usuario aprieta. Por ahora no hace nada al hacer clic: eso llega en la sección 5.
boton = QPushButton("Saludar", ventana) boton.move(20, 60)
Modificá: poné tres botones con textos distintos, uno debajo del otro.
Investigá: ¿cómo se le cambia el tamaño a un botón?
Un campo de texto de una sola línea: un nombre, una edad, una búsqueda. Es la entrada de tu app.
from PyQt6.QtWidgets import QLineEdit campo = QLineEdit(ventana) campo.move(20, 100)
Modificá: hacé que arranque con un texto ya escrito adentro.
Investigá: ¿cómo se lee lo que el usuario escribió? ¿Y cómo se muestra un texto gris de ayuda que desaparece al escribir (placeholder)?
Como el anterior, pero para textos largos: una descripción, notas, un registro de lo que pasó.
notas = QTextEdit(ventana) notas.move(20, 140)
Modificá: agrandalo con resize() y escribí varias líneas adentro.
Investigá: para leer su contenido no sirve text(). ¿Cuál es el método?
¿Y cómo se hace para que sea de solo lectura?
Una opción que se activa o desactiva. Guarda un estado: True o False.
acepta = QCheckBox("Acepto los términos", ventana) acepta.move(20, 260)
Modificá: hacé que arranque ya tildado.
Investigá: ¿cómo se pregunta desde el código si está tildado o no?
Una lista desplegable. Sirve cuando las opciones son fijas y no querés que el usuario escriba mal.
clase = QComboBox(ventana) clase.addItems(["Guerrero", "Mago", "Arquero"]) clase.move(20, 290)
Modificá: agregale una cuarta opción.
Investigá: ¿cómo se averigua cuál opción eligió el usuario? ¿Y en qué se diferencia de preguntar por el índice de la opción?
Mirá los seis widgets juntos. Casi todos los métodos empiezan igual:
set...() para cambiar algo, y un método sin set para
preguntar algo. setText() / text().
Si entendés ese patrón, no necesitás memorizar la lista de métodos: podés adivinar
el nombre y verificarlo en la documentación.
El usuario hace algo → nuestro programa responde
En consola nosotros decidíamos el orden: primero esto, después aquello, y el
programa esperaba con input(). En una interfaz gráfica es al revés:
el usuario decide qué toca y cuándo. Nuestro trabajo es dejar preparado
qué hacer si pasa cada cosa.
boton.clicked.connect(mi_funcion)
clicked es algo que puede ocurrir. connect() dice
qué función queremos que se ejecute cuando ocurra.Se escribe connect(saludar), sin paréntesis al final.
Con connect(saludar()) Python ejecuta la función ahí mismo y le pasa el
resultado a connect. No estamos llamando a la función: estamos
entregándola para que PyQt la llame más tarde.
import sys from PyQt6.QtWidgets import QApplication, QWidget, QLabel, QPushButton app = QApplication(sys.argv) ventana = QWidget() ventana.setWindowTitle("Evento") ventana.resize(320, 160) etiqueta = QLabel("Todavía no me saludaste", ventana) etiqueta.move(30, 40) etiqueta.resize(250, 24) boton = QPushButton("Saludar", ventana) boton.move(30, 90) def saludar(): etiqueta.setText("¡Hola!") boton.clicked.connect(saludar) ventana.show() sys.exit(app.exec())
saludar? ¿Cuando se escribe la línea del
connect, o después?saludar usa etiqueta sin recibirla como parámetro.
¿Por qué funciona? ¿Te parece una buena idea a medida que el programa crezca?
(Guardá esa duda: la sección 8 la resuelve.)Ahora sí: datos que entran, datos que salen
Hasta acá el botón siempre decía lo mismo. Vamos a hacer que la respuesta dependa de lo que el usuario escribió.
------------------------------
MI APLICACIÓN
------------------------------
Nombre: [____________]
Edad: [____________]
[ SALUDAR ]
Hola Pedro, tenés 20 años.
QLineEdit. Es un método corto, sin set.QLabel. Esto ya lo usaste en la sección 5..clicked.connect(...).Buscá «PyQt6 QLineEdit get text» o mirá la documentación de QLineEdit.
Vas a encontrar la respuesta en menos de un minuto. Eso es parte de la actividad:
esta guía no te va a dar todos los métodos, porque el objetivo es que aprendas a encontrarlos.
def saludar(): nombre = campo_nombre.??? # ← leer el nombre edad = campo_edad.??? # ← leer la edad resultado.setText(f"Hola {nombre}, tenés {edad} años.") boton.clicked.connect(saludar)
???. El resto ya lo sabés escribir.veinte en la edad. ¿Rompe? ¿Y si además querés calcular
edad + 1?type() antes de suponerlo.¿Y si en vez de saludar, la app calcula el año de nacimiento? ¿Qué línea cambia y qué línea queda igual?
Ya tenemos varios elementos. ¿Cómo los organizamos?
Agrandá con el mouse la ventana del formulario que acabás de hacer. ¿Qué pasa con los widgets?
Con move(x, y) vos clavás cada widget en un punto fijo. Si la ventana cambia de
tamaño, si el texto es más largo, si lo abrís en otra computadora: queda todo
desacomodado. Y cada vez que agregás algo en el medio, tenés que recalcular a mano las
coordenadas de todo lo que está abajo.
Un layout es un organizador: vos le decís en qué orden van los widgets y él calcula las posiciones y las recalcula solo cuando algo cambia. Nunca más coordenadas a mano.
A B C
La V es de vertical. El más usado.
A B C
La H es de horizontal. Ideal para una fila de botones.
A B C D
Una grilla. Ideal para formularios y calculadoras.
from PyQt6.QtWidgets import QVBoxLayout layout = QVBoxLayout() # 1. creo el organizador layout.addWidget(etiqueta) # 2. le agrego los widgets, en orden layout.addWidget(campo) layout.addWidget(boton) ventana.setLayout(layout) # 3. se lo pongo a la ventana
move() deja de hacer falta: borralo.
Y ya no necesitás pasarle ventana al crear cada widget — el layout se encarga.fila = QHBoxLayout() fila.addWidget(QLabel("Nombre:")) fila.addWidget(campo_nombre) layout = QVBoxLayout() layout.addLayout(fila) # ← addLayout, no addWidget layout.addWidget(boton)
grilla = QGridLayout() grilla.addWidget(QLabel("Nombre:"), 0, 0) # fila 0, columna 0 grilla.addWidget(campo_nombre, 0, 1) # fila 0, columna 1 grilla.addWidget(QLabel("Edad:"), 1, 0) # fila 1, columna 0 grilla.addWidget(campo_edad, 1, 1)
addWidget lleva dos números más: fila y columna. Se empieza a contar
desde 0, como en las listas. Mi aplicación
Nombre: [____________]
Edad: [____________]
[ ACEPTAR ]
move().QGridLayout, o con un QVBoxLayout que contenga
dos QHBoxLayout. Las dos son válidas — probá las dos y quedate con la que
te resulte más clara.¿Qué hace layout.addStretch()? ¿Y layout.addSpacing(20)?
Probalos en distintos lugares de tu layout y mirá qué cambia.
Acá se junta todo lo que ya sabés
Tu formulario ya tiene variables sueltas, funciones sueltas y widgets sueltos. ¿Qué pasa cuando sean cincuenta?
Una ventana es una cosa: tiene estado (qué escribió el usuario, cuántos clics
lleva) y comportamiento (saludar, limpiar, calcular). Eso es exactamente un
objeto. Entonces la escribimos como una clase, igual que
Personaje o Jugador.
class Ventana(QWidget): # HERENCIA: nuestra Ventana ES UN QWidget def __init__(self): super().__init__() # que QWidget haga lo suyo primero self.setWindowTitle("Mi aplicación")
super() son los mismos de
la clase 2. Lo único distinto es que la clase de la que heredamos no la escribimos nosotros:
viene de PyQt.| En PyQt | Es lo que ya sabías |
|---|---|
QWidget | Una clase — hecha por otra persona, pero una clase común y corriente. |
Ventana | Nuestra clase, que hereda de QWidget: ES UN QWidget, más lo nuestro. |
super().__init__() | Lo mismo que en Jugador: dejar que el padre se inicialice antes de agregar lo propio. |
self | La ventana concreta con la que estamos trabajando. |
self.campo_nombre | Atributo: el estado del objeto. Por eso lo ven todos los métodos. |
def saludar(self) | Método: el comportamiento del objeto. |
import sys from PyQt6.QtWidgets import (QApplication, QWidget, QLabel, QLineEdit, QPushButton, QVBoxLayout) class Ventana(QWidget): def __init__(self): super().__init__() self.setWindowTitle("Mi aplicación") self.resize(320, 200) # los widgets son atributos: self.algo self.campo_nombre = QLineEdit() self.boton = QPushButton("Saludar") self.resultado = QLabel("") layout = QVBoxLayout() layout.addWidget(QLabel("Nombre:")) layout.addWidget(self.campo_nombre) layout.addWidget(self.boton) layout.addWidget(self.resultado) self.setLayout(layout) self.boton.clicked.connect(self.saludar) # un método, no una función suelta def saludar(self): nombre = self.campo_nombre.text() self.resultado.setText(f"¡Hola {nombre}!") app = QApplication(sys.argv) ventana = Ventana() ventana.show() sys.exit(app.exec())
Ventana() es nuestra clase.La función saludar usaba variables globales. Funcionaba... hasta que el programa creciera.
self.campo_nombre es un atributo: todos los métodos lo ven, sin globales y sin pasarlo por parámetro.
PyQt no reemplaza la POO que aprendimos. PyQt está hecho de objetos, y nosotros organizamos nuestro programa con clases propias. Es la misma herramienta de siempre, aplicada a algo nuevo.
limpiar() que vacíe el campo y el resultado, y un segundo
botón que lo llame.self.clics = 0 y contá cuántas veces saludaron.
¿Dónde lo inicializás? ¿Dónde lo aumentás?self. a self.campo_nombre en el __init__
(dejalo como variable local) y ejecutá. ¿Qué error da saludar? ¿Por qué?Tu primera aplicación propia, de punta a punta
El tema lo elegís vos. Lo que tiene que tener, sí o sí:
move().Todo lo demás es libre: los textos, los tamaños, los nombres de las variables, cómo se distribuyen los widgets, qué hace el botón. No copies el ejemplo de la sección 8 tal cual — partí de él y convertilo en algo tuyo.
Un contador de repeticiones de gimnasio. Un sorteador de nombres. Un generador de contraseñas. Un «¿qué como hoy?». Un calculador de promedio de notas. Cualquier cosa que reciba algo, haga una cuenta y muestre un resultado.
show() y app.exec()?addWidget()?connect sin paréntesis en la función?NameError dentro de un método → ¿te faltó el self.?ImportError → ¿agregaste ese widget a la línea del import?La parte más importante de la guía
Ninguna de estas respuestas está en esta guía. A propósito. Nadie sabe PyQt de memoria: se investiga cada vez que hace falta. Elegí al menos cuatro y resolvelas con código que funcione, no solo leyendo.
¿Cómo cambio el color de fondo de una ventana?
¿Cómo cambio el tamaño de la fuente de una etiqueta?
¿Cómo le cambio el tamaño a un botón?
¿Cómo muestro una imagen en la ventana?
¿Cómo muestro un mensaje emergente? ¿Qué es QMessageBox?
¿Cómo evito que la ventana se pueda redimensionar?
¿Qué hace QSpinBox? ¿En qué es mejor que un QLineEdit para pedir una edad?
¿Qué otros widgets existen? Encontrá tres que no vimos y contá para qué sirve cada uno.
boton. y mirá la lista que te ofrece. Es la documentación más rápida que tenés y ya está instalada.print(dir(boton)) lista todo lo que ese objeto puede hacer. help(QPushButton.setText) lo explica.doc.qt.io) o de PyQt6. Está escrita para C++, pero los nombres de los métodos son los mismos: aprendé a leerla igual.PyQt6 te ahorra la mitad de los problemas.from PyQt5... → from PyQt6...app.exec_() → app.exec()Qt.AlignCenter → Qt.AlignmentFlag.AlignCenter — en PyQt6 las
opciones fijas van con su nombre completo.Con esas tres reglas, la mayoría de los ejemplos de PyQt5 funcionan en PyQt6. Saber adaptar un ejemplo viejo es una habilidad que vas a usar toda tu carrera.
No PyQt. Estás aprendiendo a pararte frente a una librería que no conocés, encontrar lo que necesitás y ponerlo a funcionar. Eso sirve para PyQt, para Django, para una librería que todavía no existe.
Elegí una. Una sola, pero terminada.
Todas se resuelven con lo que ya está en esta guía, más una o dos cosas que vas a tener que investigar. Usá una clase propia y layouts.
Dos números, las cuatro operaciones, un resultado.
A investigar: cómo evitar que se rompa al dividir por cero.
Nombre, clase, nivel y vida. Un botón muestra la ficha completa.
Conecta directo con la sección 13.
Celsius → Fahrenheit, km → millas, pesos → dólares. Elegí uno o hacelos todos con un
QComboBox.
A investigar: cómo redondear el resultado a dos decimales.
Escribís una tarea, apretás «Agregar» y aparece en una lista.
A investigar: QListWidget no lo vimos. Y después: ¿cómo se borra la tarea seleccionada?
Nombre del objeto, cantidad y un botón para agregarlo al inventario.
A pensar: ¿dónde guardás el inventario? ¿Alcanza con lo que se ve en pantalla?
Si tenés una idea propia del mismo tamaño, hacela. Avisale al profesor qué vas a construir.
Empezá por la ventana vacía. Después un widget. Después el layout. Después el botón. Después la lógica. Ejecutá en cada paso: si escribís todo de una y falla, no vas a saber dónde. Es el mismo método que usaste en Arena de Combate.
Si terminaste antes, seguí por acá
...agregamos un segundo botón que haga otra cosa?
...un botón limpia el formulario y lo deja como recién abierto?
...mostramos un mensaje de error cuando falta un dato, en vez de saludar a nadie?
...agregamos una imagen: un logo, un avatar, un ícono?
...le cambiamos los estilos: colores, fuentes, bordes? (Buscá setStyleSheet.)
...guardamos los datos en un archivo y siguen ahí la próxima vez que abrimos la app?
¿Y si nuestra aplicación tuviera varias pantallas?
Un menú de inicio, una pantalla de juego, una de resultados. Todo en un solo archivo de 300 líneas empieza a ser un problema: ¿dónde va cada cosa? ¿Cómo pasamos de una pantalla a otra? ¿Qué parte del código se repite?
No la resuelvas ahora. Si te la hiciste sola, ya llegaste a donde quería llegar esta guía: eso es organización de proyectos y modularización, y es el tema que viene.
Arena de Combate, visto desde acá
Hasta ahora nuestro juego funciona por consola: print() para mostrar,
input() para elegir. ¿Qué pasaría si reemplazáramos la consola por una
interfaz gráfica?
ARENA DE COMBATE
┌─────────────────────────┐
│ JUGADOR │
│ Vida: ████████░░ │
│ │
│ [ Atacar ] │
│ [ Pociones ] │
│ [ Inventario ] │
└─────────────────────────┘
Personaje, Jugador, Enemigo, Arma,
Pocion. Quién tiene cuánta vida, cuánto daño hace un ataque, cuánto cura una
poción, si alguien sigue vivo. Nada de esto sabe que existe una pantalla.
print() → un QLabel que se actualiza.
input() → un QPushButton que el usuario aprieta.
El while del combate → el bucle de eventos de PyQt.
La interfaz y la lógica son dos cosas distintas. Si tus clases no usan
print() ni input() — si devuelven resultados en vez de
mostrarlos — podés ponerles arriba una consola, un Tkinter, un PyQt o un Pygame
sin tocarlas. Es exactamente la diferencia entre print() y
return que vimos en la clase 2, y recién ahora se ve para qué servía.
Abrí tu Arena de Combate y buscá los print() que están adentro de los
métodos de tus clases. Cada uno de esos es un lugar donde la lógica y la interfaz se
mezclaron. ¿Cuántos hay? ¿Cuáles podrías convertir en return?
En la carpeta arena-de-combate/ está el mismo juego resuelto tres veces:
juego_consola.py, juego_tkinter.py y juego_pygame.py.
Abrilos y mirá qué cambia y qué se mantiene igual entre los tres. Entre consola y Tkinter
las clases del principio son casi idénticas; en Pygame cambian un poco más (el
Personaje además es un sprite), pero las reglas del juego —vida, daño,
pociones— siguen siendo las mismas.
Una pantalla de Arena de Combate, en PyQt
No hace falta convertir todo. Una sola pantalla que funcione vale más que un juego entero a medio hacer.
QLabel que muestre el resultado de la última acción.Reutilizá tus clases. Copiá el archivo de tu Arena de Combate al lado del nuevo y
hacé from arena import Jugador, Enemigo, Arma. Si no te deja, es porque el
archivo original ejecuta el juego al importarlo: eso también es información valiosa sobre
cómo está organizado tu proyecto.
ArenaGUI(QWidget). Ejecutala.Jugador y el Enemigo como atributos: self.jugador = Jugador(...).QLabel con los nombres y las vidas. Acomodalos con un layout.self.atacar().atacar() llame a tus métodos de siempre y después actualice las etiquetas.QProgressBar.)setEnabled.)QTextEdit?Marcá solo lo que podrías hacer sin mirar la guía
set...() para cambiar, sin set para preguntar.algo.clicked.connect(mi_funcion), sin paréntesis.move() fue solo para arrancar.QWidget.Un programador no memoriza una librería.
Aprende a investigarla y utilizarla.
Si terminás pensando «no sé todo PyQt, pero sé cómo empezar a construir con PyQt y cómo averiguar lo que no sé», la actividad cumplió su objetivo.