miércoles, junio 27, 2007

Creación de una calculadora con Python y PyQt4

Introducción:
Vamos a emprender la "aventura" de crear una calculadora. Python nos va a ayudar con su introspección y sus tipos de datos diccionario, lista y tupla; Qt nos va a dar una mano con sus mecanismos de conexión y algunas propiedades útiles que define en su clase base principal, QObject.
Por último, y para hacer menos aburrido el tema, usaremos un autómata finito (aka. maquina de estados) para emular el comportamiento de esas calculadoras económicas de bolsillo, desarrolladas en algún país del este...
Diseño de la interfase:Para esto, utilizaremos el diseñador de Qt, en Debian el binario se llama designer (o designer-qt4 si se tiene Qt3 y Qt4 instalados).
Es una buena oportunidad de usar los layouts, en especial, uno que no usamos a menudo, el layout matricial, o de celdas (QGridLayout).
Luego de jugar un rato, podremos hacer algo como lo siguiente:

Donde hemos definido los nombres de los objetos, respetando un patrón, en este caso, los digitos, se llaman btn_digit_, ej: btn_digit_1, btn_digit_2, btn_digit_3, etc.

Desde tenemos algunos atajos muy útiles, CTRT+R para probar la interfase, F1 para obtener la información del Widget que tenemos seleccionado y CTRL+I, para acceder al diálogo de propiedades del Widget, donde editamos la paleta, las fuentes, dimensiones y demás propiedades netamente visuales.
El .ui de la interfase se puede descargar aquí: interfase.ui
En este ejemplo, he usado el mecanismo de herencia múltiple, no el de carga dinámica.
Para esto tenemos que compilar el .ui en un .py, para automatizar la tarea, suelo usar un Makefile, como este.
Lógica de la calculadora:
Si estuviésemos programando en Visual Basic, Delphi o cualquier otro entorno orientado a eventos, seguramente pondríamos el código de la aplicación en cada pulsación de cada botón, pero en este caso, utilizaremos un autómata.
Un automata finito se define como un alfabeto de entrada (en este caso, los botones de la calculadora), un conjunto de estados, un cojunto de transiciones y un conjunto de acciones asociadas a cada transición.
La maquina de estados se inicializa en algún estado inicial, y a medida que va recibiendo entrada, irá saltando de estado en estado, y realizando las funciones asociadas.
En este caso, la calculadora cuenta con un solo lugar de almacenamiento (o acumulador), además usa la pantalla como buffer temporal de los datos. Tendrá la capacidad de recordar una operación.
El automata consiste en un módulo a parte, llamado automata.py, que posee la tabla de transiciones, y la función que ejecuta las transiciones de acuerdo a la entrada.

automata = {
    'en_cero'   : {
        'cero':      ('cero', [] ),
        'digito':    ('primer_dig', ['establecer']),
        'operacion': ('operando', ['almacenar']),
        'igual':     ('cero',[]),
        'clear':     ('en_cero',['limpiar']),
    },
    'primer_dig': {
        'cero':      ('primer_dig',['actualizar']),
        'digito':    ('primer_dig',['actualizar']),
        'operacion': ('operando',  ['guardar_op', 'almacenar']),
        'igual':     ('resultado', ['almacenar']),
        'clear':     ('en_cero',['limpiar']),
    },
    'operando': {
        'cero':      ('cero_dos', ['establecer']),
        'digito':    ('segundo_dig', ['establecer']),
        'operacion': ('resultado', ['operar']),
        'igual':     ('resultado',['operar']),
        'clear':     ('en_cero',['limpiar']),
    },
    'cero_dos': {
        'cero':      ('cero_dos', []),
        'digito':    ('segundo_dig',['establecer']),
        'operacion': ('cero_dos', []),
        'iugal':     ('cero_dos', []),
        'clear':     ('en_cero',['limpiar']),
    },
    'segundo_dig' : {
        'cero':      ('segundo_dig', ['actualizar']),
        'digito':    ('segundo_dig', ['actualizar']),
        'operacion': ('resultado', ['guardar_op','operar']),
        'igual':     ('resultado', ['operar']),
        'clear':     ('en_cero',['limpiar']),
    },
    'resultado': {
        'cero':      ('en_cero', ['establecer']),
        'digito':    ('primer_dig', ['establecer']),
        'operacion': ('operando',['almacenar', 'guardar_op']),
        'igual':     ('resultado',[]),
        'clear':     ('en_cero',['limpiar']),
    },
}

Para generalizar esta lógica, utilizaremos las capacidades de introspección de Python.
¿Que es esto?
Bueno, es implemente dada una instanacia, obtener una función (que es un objeto después de todo, no?)
funcion = getattr(instancia, "nombre")
funcion(arg1, arg2)
Jejeje, muy fácil. De esta manera, para ejecutar el autómata tenemos una función muy simple:

def ejecutar_automata( caracter, vista ):
    '''Ejecuta la logia de la caluladora.'''
    entrada = tipo_entrada[str(caracter)]
    transicion = automata[ getattr(vista, "estado") ][entrada]
    # Realizar la lista de acciones relacionadas con el cambio de estado
    # si es que existen...
    if transicion[FUNCIONES]:
        for i in transicion[FUNCIONES]:
            if DEBUG:
                print "estado %s > %s(%s)" % (getattr(vista, "estado") ,i, caracter)
            accion = getattr(vista, i)
            accion(caracter)

    if DEBUG:
        print "Cambiando a %s" % (transicion[PROXIMO_ESTADO])
    setattr(vista, "estado", transicion[PROXIMO_ESTADO])
El código del autómata se puede ver completo en: automata.py
Vemos que la función recibe una vista como parámetro. Viene a ser algo así como un patrón visitante venido a menos.
Por último, tenemos que generar la aplicación y hereder de QMainWindow, relaizar los connects y ponerle un splash...

#! /usr/bin/python
# *-* encoding: utf-8 *-*

#
# Autor: Nahuel Deofossé (c) 2007
# Licencia: GPL
#

# Todo PyQt4 :)
from PyQt4.Qt import *

# Generado a través del editor de designar
# cuando se actualiza el GUI, se debe correr Make para que
# se transformen en código
from interfase import Ui_MainWindow
# Para los argumentos de linea de entrada
import sys
# Todas las definiciones del automata que hemos separado en otro modulo (py)
from automata import ejecutar_automata

# Algunas funciones utiles


class Calculadora(QApplication):
    '''Aplicacion calculadora simple'''
    def __init__(self, *argumentos):
        QApplication.__init__(self, *argumentos)
        splash_pxmap = QPixmap("./resources/splash.png")
        splash = QSplashScreen(splash_pxmap)
        splash.show()
        ventana = VentanaCalculadora()
        ventana.show()
        splash.finish(ventana)
        self.exec_()

class VentanaCalculadora(QMainWindow, Ui_MainWindow):
    '''GUI de la calculadora.'''
    estado = "en_cero"  # De acuerdo a los estados del automata
    valor = 0
    operacion = ""
    def __init__(self, padre = None):
        '''Constructor de la ventana'''
        QMainWindow.__init__(self, padre)
        self.setupUi(self)
        self.connect(self.actionSalir, SIGNAL("triggered()"), qApp.exit )
        # Conectamos los digitos
        for i in [  self.btn_digit_1, self.btn_digit_2,
                    self.btn_digit_3, self.btn_digit_4,
                    self.btn_digit_5, self.btn_digit_6,
                    self.btn_digit_7, self.btn_digit_8,
                    self.btn_digit_9, self.btn_digit_0,
                    ]:
            self.connect(i, SIGNAL("clicked()"), self.in_digito)
    # Ahora conectamos las operaciones

    def in_digito(self):
        '''Pulsado de un digito'''
        num = self.sender().objectName()[-1:]
        ejecutar_automata(num, self)

    def on_btn_op_add_pressed(self):
        ejecutar_automata("+", self)

    def on_btn_op_sub_pressed(self):
        ejecutar_automata("-", self)

    def on_btn_op_mul_pressed(self):
        ejecutar_automata("*", self)

    def on_btn_op_div_pressed(self):
        ejecutar_automata("/", self)

    def on_btn_op_res_pressed(self):
        ejecutar_automata("=", self)

    def on_btn_clear_pressed(self):
        ejecutar_automata("c", self)



    # Funciones que llama el automata
    def actualizar(self, c):
        '''Agrega un caracter al display '''
        self.lineResultado.setText( self.lineResultado.text() + c )

    def establecer(self, c):
        '''Establece el valor del display'''
        self.lineResultado.setText(c)
    def operar(self, c):
        pass

    def almacenar(self, c):
        self.valor = float(self.lineResultado.text())

    def guardar_op(self,c):
        self.operacion = c

    def operar(self, c):
        result = 0
        self.valor2 = float(self.lineResultado.text())
        if self.operacion == "+":
            result = self.valor + self.valor2
        elif self.operacion == "-":
            result = self.valor - self.valor2
        elif self.operacion == "/":
            result = self.valor / self.valor2
        elif self.operacion == "*":
            result = self.valor * self.valor2
        else:
            print "Nada"
        self.lineResultado.setText(str(result))

    def limpiar(self, c):
        self.lineResultado.setText("0")
        self.valor = 0


if __name__ == "__main__":
    app = Calculadora(sys.argv)
Como verán, el código es muy simple y corto. Se puede extender con facilidad.
Hemos visto varias cosas interesantes de Python y de PyQt.
Un diagrama, de lo que sucede sería como el siguietnte:

En el rectángulo verde vemos el conjunto de los eventos, el rectángulo rojo es el autómta y sobre la esquina superior derecha, el rectángulo naranja es el estado que se mantiene en la vista... y las funciones que cuelgan, son las que el autómata utiliza como interface para modificar el estado de la vista...
Finalmente, habrán notado que he mezclado el estado (modelo) con la vista, en este caso, generar una separación complicaba las cosas -llevando las cosas fuera del scope de lo que quería mostrar-.

El código completo se encuentra en:
https://github.com/D3f0/calculadora/edit/master/README

4 comentarios:

Diego van Haaster dijo...

Muy buena la calculadora y excelente el Blog lo tengo en mis bookmarks como lugar obligado de visitas diarias :)

ricino ricinus communis dijo...

demasiado bueno , por que lo que pones aquí es de muy buena calidad. ojala si podes hacer también ejemplos con pygtk, me sirvio de mucho este post

Anónimo dijo...

[url=http://www.onlinecasinos.gd]casino[/url], also known as accepted casinos or Internet casinos, are online versions of usual ("buddy and mortar") casinos. Online casinos assign gamblers to delimit incorrect an orb to ingredient in and wager on casino games because of the Internet.
Online casinos superficially shut up up respecting acquisition odds and payback percentages that are comparable to land-based casinos. Some online casinos justification the be blind to higher payback percentages in the smoke of opening automobile games, and some wager extinguished payout behalf audits on their websites. Assuming that the online casino is using an correctly programmed unsystematic teeming generator, catalogue games like blackjack substantiate an established congress edge. The payout slice harry of these games are established on the rules of the game.
Innumerable online casinos sublease or sense their software from companies like Microgaming, Realtime Gaming, Playtech, Worldwide Manipulate Technology and CryptoLogic Inc.

Anónimo dijo...

[url=http://www.23planet.com]casinos online[/url], also known as effective casinos or Internet casinos, are online versions of notable ("confrere and mortar") casinos. Online casinos approve gamblers to palm depraved after realm a adverse in and wager on casino games with the back the Internet.
Online casinos typically gamble saucy odds and payback percentages that are comparable to land-based casinos. Some online casinos gain mastery higher payback percentages with a censure performance gismo games, and some modify known payout apportionment audits on their websites. Assuming that the online casino is using an meetly programmed indefinitely concert-hall generator, number games like blackjack charm an established congress edge. The payout a certain extent nearby on account of regardless of these games are established erstwhile the rules of the game.
Myriad online casinos sublease or become distinct their software from companies like Microgaming, Realtime Gaming, Playtech, Principal Prank Technology and CryptoLogic Inc.