El elemento pricipal de la interfaz de usuario en PyQt es un objeto de la clase QMainWindow. Crearemos una clase hija de ésta llamada VentanaPrincipal, que se encaragará de manejar toda la interfaz de usuario.
class VentanaPrincipal(QMainWindow):
Método __init__
El método __init__ llevará una gran cantidad de código, pues es donde se inicializan todos los elementos de la interfaz. Sin embargo, no contiene ningun tipo de lógica complicada. Todo el código posterior está dentro de __init__.
def __init__(self):
super().__init__(parent=None)
En PyQt, se lleva a cabo una estructura de árbol, donde el elemnto raíz es la ventana principal, y todos los objetos que se muestren en ella serán sus hijos o hijos lejanos, ya que un elemento puede ser padre de otro. Es por esto que el constructor tiene al padre por defecto asignado a None.
Propiedades de la ventana principal
self.setWindowTitle("Transformaciones")
self.setGeometry(400, 200, 600, 400)
self.hboxlayout = QHBoxLayout()
self.centro = QWidget(self)
self.centro.setLayout(self.hboxlayout)
self.tf = Transformador()
Se declara el título y las dimensiones de la ventana principal. Se instancia un objeto genérico QWidget que servirá como el contenido central de la ventana, y se le asigna un QHBoxLayout. Este layout indica que los elementos hijos consecuentes se irán agrupando horizontalmente uno detrás de otro.
Canvas
self.canvas = QGraphicsView(self.centro)
self.canvas.setMinimumWidth(150)
self.canvas.setGeometry(0, 0, 550, 400)
self.canvas_scene = QGraphicsScene()
self.canvas.setScene(self.canvas_scene)
QGraphicsView es el elemento que permite dibujar gráficos no predeterminados, y representará nuestro plano cartesiano. QGraphicsView requiere de una escene QGraphicsScene la que provee los métodos de dibujado en sí.
Barra lateral
self.sidebar = QWidget(self.centro)
self.sidebar.setFixedWidth(150)
self.sidebarlayout = QVBoxLayout(self.sidebar)
self.sidebar.setLayout(self.sidebarlayout)
self.gridlayout = QGridLayout(self.sidebar)
self.sidebarlayout.addLayout(self.gridlayout)
Usaremos una barra lateral al lado del canvas para ubicar a los demás elementos en un QGridLayout, que ubica a los elementos en una rejilla y permite especificar la casilla en donde se ubicará cada elemento.
El punto principal P
self.p_label = QLabel("P")
self.px_edit = QLineEdit(self.sidebar)
self.px_edit.setPlaceholderText("x")
self.py_edit = QLineEdit(self.sidebar)
self.py_edit.setPlaceholderText("y")
self.p_boton = QPushButton("Dibujar", self.sidebar)
self.p_boton.clicked.connect(self.dibujarP)
Estos son los elementos que se usarán para que el usuario pueda definir el punto P al que se le aplicarán las transformaciones. QLabel muestra una cadena de texto, QLineEdit es un campo para ingresar texto y QPushButton es un botón común y corriente. Al boton se le suscribe una función llamada dibujarP, que será llamada cada vez que este sea presionado. Más adelante se definirá este método.
El punto central C
self.c_label = QLabel("C")
self.cx_edit = QLineEdit(self.sidebar)
self.cx_edit.setPlaceholderText("x")
self.cy_edit = QLineEdit(self.sidebar)
self.cy_edit.setPlaceholderText("y")
self.c_boton = QPushButton("Dibujar", self.sidebar)
self.c_boton.clicked.connect(self.dibujarC)
Al igual que con P, estos elementos permiten definir al punto de pivote C que se usa como referencia en la rotación y el escalamiento. Su botón está suscrito a un método diferente llamado dibujarC.
El vector de traslación T
self.t_label = QLabel("T")
self.tx_edit = QLineEdit(self.sidebar)
self.tx_edit.setPlaceholderText("x")
self.ty_edit = QLineEdit(self.sidebar)
self.ty_edit.setPlaceholderText("y")
self.t_boton = QPushButton("Trasladar", self.sidebar)
self.t_boton.clicked.connect(self.trasladar)
Los elementos del vector de traslación son prácticamente los mismos que los de los puntos anteriores. El botón está suscrito a trasladar.
El ángulo de rotación R
self.r_label = QLabel("R")
self.r_edit = QLineEdit(self.sidebar)
self.r_edit.setPlaceholderText("°")
self.r_boton = QPushButton("Rotar", self.sidebar)
self.r_boton.clicked.connect(self.rotar)
La rotación únicamente necesita un campo de texto para indicar el ángulo en grados al que se desea rotar P. Su botón llama al método rotar.
El vector de escalamiento S
self.s_label = QLabel("S")
self.sx_edit = QLineEdit(self.sidebar)
self.sx_edit.setPlaceholderText("x")
self.sy_edit = QLineEdit(self.sidebar)
self.sy_edit.setPlaceholderText("y")
self.s_boton = QPushButton("Escalar", self.sidebar)
self.s_boton.clicked.connect(self.escalar)
Estos elementos siguen el mismo patrón que la traslación. Su método es escalar.
El punto resultante P'
self.p_prima_label = QLabel("P'")
self.p_primax_edit = QLineEdit(self.sidebar)
self.p_primax_edit.setPlaceholderText("x")
self.p_primay_edit = QLineEdit(self.sidebar)
self.p_primay_edit.setPlaceholderText("y")
También se declaran elementos de interfaz para mostrar el punto obtenido de aplicar las transformaciones. Aquí no se requiere de un botón de dibujo ya que los métodos de transformación se encargan de dibujarlo.
Colocando los elementos en la interfaz
self.gridlayout.addWidget(self.p_label, 0, 0)
self.gridlayout.addWidget(self.px_edit, 0, 1)
self.gridlayout.addWidget(self.py_edit, 0, 2)
self.gridlayout.addWidget(self.p_boton, 0, 3, 1, 2)
self.gridlayout.addWidget(self.c_label, 1, 0)
self.gridlayout.addWidget(self.cx_edit, 1, 1)
self.gridlayout.addWidget(self.cy_edit, 1, 2)
self.gridlayout.addWidget(self.c_boton, 1, 3, 1, 2)
self.gridlayout.addWidget(self.t_label, 2, 0)
self.gridlayout.addWidget(self.tx_edit, 2, 1)
self.gridlayout.addWidget(self.ty_edit, 2, 2)
self.gridlayout.addWidget(self.t_boton, 2, 3, 1, 2)
self.gridlayout.addWidget(self.r_label, 3, 0)
self.gridlayout.addWidget(self.r_edit, 3, 1, 1, 2)
self.gridlayout.addWidget(self.r_boton, 3, 3, 1, 2)
self.gridlayout.addWidget(self.s_label, 4, 0)
self.gridlayout.addWidget(self.sx_edit, 4, 1)
self.gridlayout.addWidget(self.sy_edit, 4, 2)
self.gridlayout.addWidget(self.s_boton, 4, 3, 1, 2)
self.gridlayout.addWidget(QLabel(""), 5, 0)
self.gridlayout.addWidget(self.p_prima_label, 6, 0)
self.gridlayout.addWidget(self.p_primax_edit, 6, 1, 1, 2)
self.gridlayout.addWidget(self.p_primay_edit, 6, 3, 1, 2)
self.hboxlayout.addWidget(self.canvas)
self.hboxlayout.addWidget(self.sidebar)
self.setCentralWidget(self.centro)
Los elementos que pertenecen a la barra lateral se van agregando a su QGridLayout. La función addWidget de ésta requiere el elemento a ser añadido, la fila donde será colocado, la columna, y opcionalmente el rango de filas que ocupará y el de columnas. Esto se usa para algunos elementos que buscamos que se vean más grandes. Finalmente se añade el canvas y la barra al QWidget central y se asigna a la ventana.
Pluma y pincel
self.p_pen = QPen(QColorConstants.DarkBlue)
self.p_brush = QBrush(QColorConstants.DarkBlue)
self.c_pen = QPen(QColorConstants.DarkYellow)
self.c_brush = QBrush(QColorConstants.DarkYellow)
self.p_prima_pen = QPen(QColorConstants.DarkMagenta)
self.p_prima_brush = QBrush(QColorConstants.DarkMagenta)
Ahora declaramos elementos relativos a la apariencia de la aplicación. La pluma especifica el contorno de los gráficos, mientras que el pincel especifica su fondo. Se declara uno de ellos por cada punto que será graficado: P, C y P'.
Las elipses P, C y P'
self.p = QGraphicsEllipseItem(0.0, 0.0, 6.0, 6.0)
self.p.setPen(self.p_pen)
self.p.setBrush(self.p_brush)
self.p.setVisible(False)
self.c = QGraphicsEllipseItem(0.0, 0.0, 6.0, 6.0)
self.c.setPen(self.c_pen)
self.c.setBrush(self.c_brush)
self.c.setVisible(False)
self.p_prima = QGraphicsEllipseItem(0.0, 0.0, 6.0, 6.0)
self.p_prima.setPen(self.p_prima_pen)
self.p_prima.setBrush(self.p_prima_brush)
self.p_prima.setVisible(False)
self.canvas_scene.addItem(self.p)
self.canvas_scene.addItem(self.c)
self.canvas_scene.addItem(self.p_prima)
Aquí declaramos el gráfico que representará cada punto en sí. Para representar los puntos usamos QGraphicsEllipseItem, una elipse con un diámetro relativamente alto para que sea visible más fácilmente. A cada una se le asignan la pluma y pincel creados anteriormente. Tras esto son añadidos al canvas, pero no serán visibles hasta que se llamen a sus métodos de dibujo.
Los ejes
self.canvas_scene.addEllipse(0, 0, 0, 0)
self.canvas_scene.addLine(0, -650, 0, 650)
self.canvas_scene.addLine(-650, 0, 650, 0)
Para finalizar el método __init__, declaramos un punto en el centro del canvas, y dos líneas rectas que servirán para representar los ejes x y y.
Los métodos de dibujo (El Controlador)
Estos son los métodos que realizan la graficación de los puntos en sí. En nuestro proyecto, son los que hacen el papel del Controlador, pues son el intermediario entre la Vista y el Modelo, permitiendo la comunicación entre éstos.
def dibujarP(self):
self.p.setVisible(True)
self.p.setX(int(self.px_edit.text()) - 3)
self.p.setY(-int(self.py_edit.text()) - 3)
def dibujarC(self):
self.c.setVisible(True)
self.c.setX(int(self.cx_edit.text()) - 3)
self.c.setY(-int(self.cy_edit.text()) - 3)
Para dibujar P y C en sí, primero se hacen visibles las elipses respectivas y se configuran sus valores x y y tres desplazadas tres unidades para tomar en cuenta el diámetro de las elipses, ya que se dibujan desde la esquina superior izquierda. Para estos valores se recupera el texto de los campos de texto correspondientes.
Para el eje y, se asigna el valor negativo del valor dado, ya que en esta librería, y en la graficación de computadoras en general, el eje y incrementa de arriba hacia abajo.
def trasladar(self):
px = int(self.px_edit.text())
py = int(self.py_edit.text())
tx = int(self.tx_edit.text())
ty = int(self.ty_edit.text())
p_prima = self.tf.trasladar(px, py, tx, ty)
p_primax = p_prima[0]
p_primay = p_prima[1]
self.p_prima.setVisible(True)
self.p_prima.setX(p_primax - 3)
self.p_prima.setY(-p_primay - 3)
self.p_primax_edit.setText(str(p_primax))
self.p_primay_edit.setText(str(p_primay))
def rotar(self):
px = int(self.px_edit.text())
py = int(self.py_edit.text())
angulo = int(self.r_edit.text())
cx = int(self.cx_edit.text())
cy = int(self.cy_edit.text())
p_prima = self.tf.rotar(px, py, angulo, cx, cy)
p_primax = p_prima[0]
p_primay = p_prima[1]
self.p_prima.setVisible(True)
self.p_prima.setX(p_primax - 3)
self.p_prima.setY(-p_primay - 3)
self.p_primax_edit.setText(str(p_primax))
self.p_primay_edit.setText(str(p_primay))
def escalar(self):
px = int(self.px_edit.text())
py = int(self.py_edit.text())
sx = int(self.sx_edit.text())
sy = int(self.sy_edit.text())
cx = int(self.cx_edit.text())
cy = int(self.cy_edit.text())
p_prima = self.tf.escalar(px, py, sx, sy, cx, cy)
p_primax = p_prima[0]
p_primay = p_prima[1]
self.p_prima.setVisible(True)
self.p_prima.setX(p_primax - 3)
self.p_prima.setY(-p_primay - 3)
self.p_primax_edit.setText(str(p_primax))
self.p_primay_edit.setText(str(p_primay))
Estos son los métodos que se llaman al presionar el botón correspondiente de la transformación. Básicamente, recuperan los datos de los campos de texto, y los envían a la instancia del Modelo (el objeto Transformado) para que realice las operaciones y devuelva el punto resultante, el cual es asignado a los valores x y y de la elipse P'.