2009-03-07 00:24

PyQt en Ejemplos (Sesión 4)

Traducción: Leonardo De Luca

¡Acción!

Requirimientos

Si todavía no lo has hecho, lee las sesiones anteriores:

Todos los archivos de esta sesión están aquí: Sesión 4 en GitHub. Puedes usarlos o seguir las instrucciones comenzando con los archivos de la Sesión 3 y ver qué tan bien has trabajado.

¡Acción!

¿Qué es una acción?

Cuando terminamos la sesión 3 teníamos una aplicación básica de tareas pendientes, con funcionalidad muy limitada: puedes marcar tareas como ya hechas, pero no puedes editarlas, no puedes crear tareas nuevas, tampoco borrarlas, ni mucho menos filtrarlas.

/static/tut3-window5.png

Una aplicación muy limitada

Hoy vamos a comenzar a escribir código y a diseñar la IU para hacer esas cosas.

El concepto clave aquí son las Acciones.

  • ¿Ayuda? Esa es una acción
  • ¿Abrir un archivo? Esa es una acción
  • ¿Cortar / Copiar / Pegar? Esas también son acciones.

Citemos el manual:

La clase QAction provee una acción abstracta de la interfaz de usuario que se puede insertar en los widgets.

En las aplicaciones se pueden invocar muchas ordenes comunes a través de menúes, botones de barras de herramientas y atajos de teclado. Dado que el usuario espera que cada orden se ejecute de la misma forma, sin importar la interfaz utilizada, es útil representar cada orden como una acción.

Las acciones se pueden agregar a los menúes y a las barras de herramientas y estarán sincronizadas automáticamente. Por ejemplo, en un procesador de texto, si el usuario presiona el botón Negrita de la barra de herramientas, el elemento Negrita del menú se activará automáticamente.

Una QAction puede contener un ícono, texto de menú, un atajo, texto de estado, texto "¿Qué es esto?", y texto emergente.

La belleza de las acciones es que no tienes que escribir código dos veces. ¿Por qué agregar un botón "Copiar" a una barra de herramientas, luego una entrada de menú "Copiar" y luego escribir dos manejadores?

Crea acciones para todo lo que el usuario pueda hacer y luego conéctalas a tu IU en los lugares correctos. Si colocas la acción en un menú, es una entrada de menú; si la colocas en una barra de herramientas, es un botón. Luego escribe un manejador para la acción, conéctalo a la señal apropiada y listo.

Empecemos con una acción sencilla: Borrar una tarea. Haremos la primera parte del trabajo, crear la acción y la IU, con Designer.

Crear acciones con Designer

Primero iremos al Action Editor y obviamente haremos clic sobre el botón "New Action" y comenzaremos a crearla:

/static/tut4-action1.png

Crear una nueva acción

Algunos comentarios:

  • Si no sabes de dónde viene el ícono "X", no has leído la sesión 3 ;-)
  • El nombre de objeto actionDelete_Task es generado automáticamente desde el campo texto. En algunos casos eso provoca nombres muy feos. Si ese fuera el caso sencillamente puedes editar el nombre del objeto.
  • Se puede usar el mismo texto para las propiedades iconText y toolTip. De no ser correcto se puede cambiar más tarde.

Una vez que creas la acción, no se marcará como "Used" en el editor de acciones. Esto se debe a que existe, pero no está disponible para el usuario en ningún lado de la ventana que estamos creando.

Hay dos lugares obvios para esta acción: una barra de herramientas y un menú.

Añadir acciones a una barra de herramientas

Para añadir una acción a una barra de herramientas, primero asegúrate de que haya una. Si no tienes una en tu "Object Inspector" entonces haz clic derecho sobre MainWindow (ya sea sobre la propia ventana o sobre su entrada en el inspector) y elige "Add Tool Bar".

Puedes añadir tantas barras de herramientas como quieras, pero intenta utilizar una sola, salvo que tengas una muy buena razón (tendremos una en la sesión 5 ;-)

Luego de crear la barra de herramientas verás un espacio vacío entre el menú (que dice "Type Here") y el widget de la lista de tareas. Ese espacio es la barra de herramientas.

Arrastra el ícono de la acción desde el editor de acciones hasta la barra de herramientas.

¡Eso es todo!

/static/tut4-action2.png

La acción Borrar tarea ahora está en la barra de herramientas.

Añadir acciones al menú

Nuestro menú está vacío, sólo tiene un cartel que dice "Type Here". A pesar de que podríamos arrastrar la acción al menú, eso pondría "Borrar tarea" primera en el nivel de menúes y eso es una elección poco común.

Así que primero vamos a crear un menú "Tareas":

  • Haz clic sobre "Type Here".
  • Escribe "Tareas" (sin las comillas)
/static/tut4-action3.png

Creación de un menú

Si te fijas en la última imagen recién creamos un objeto QMenu llamado menuTask (como se dijo antes, el nombre del objeto se basa en lo que tipeamos).

Queremos agregar la acción para borrar tareas a ese menú. Para hacerlo arrastramos la acción hasta "Tareas" en el menú y luego sobre el menú que aparece al hacerlo.

/static/tut4-action4.png

La acción Borrar tarea ahora está en el menú

Ahora tenemos la acción en la barra de herramientas y en el menú. Pero, por supuesto, no hace nada. Así que a continuación vamos a trabajar sobre eso.

Guárdalo, ejecuta build.sh, y sigamos adelante.

Borrar una tarea

Seguramente recuerdes AutoConnect de la sesión 2. Si no lo recuerdas repasa esa sesión, porque ahora vamos a utilizar eso. Queremos hacer algo cuando el objeto actionDelete_Task emita su señal triggered.

Por lo tanto, necesitamos implementar Main.on_actionDelete_Task_triggered (¿notas por qué los nombres de los objetos son importantes? Podría haberlo llamado delete).

Una cuestión importante

Aquí nos vamos a desviar un poco porque hay un prolbema con PyQt que es un poco molesto.

Considera esta versión trivial de nuestro método:

def on_actionDelete_Task_triggered(self,checked=None):
    print "adtt",checked

¿Qué pasa si hago clic sobre el botón de la barra de herramientas?

[[email protected] session4]$ python main.py
adtt False
adtt None

Lo mismo sucede si seleccionas "Borrar tarea" desde el menú: se llama al slot dos veces. El problema sucede cuando se utiliza AutoConnect para señales con argumentos que también pueden ser emitidas sin argumentos.

¿Cómo puedes saber que ese es el caso? En los manuales de Qt estarán listados con argumentos por omisión.

Por ejemplo, esta señal tiene ese problema:

void triggered ( bool checked = false )

Esta no:

void toggled ( bool checked )

La explicación técnica para esto es... rebuscada pero la solución práctica es trivial:

Asegúrate de que "checked" no sea "None" en tu slot:

def on_actionDelete_Task_triggered(self,checked=None):
    if checked is None: return

De esta forma ignorarás la llamada de slot sin argumentos y se ejecutará el código real sólo una vez.

Y aquí está el código real, que es bastante corto:

def on_actionDelete_Task_triggered(self,checked=None):
    if checked is None: return
    # Primero veamos que está seleccionado.
    item=self.ui.list.currentItem()

    if not item: # Nada seleccionado, así que no sabemos que borrar
        return
    # Borrar la tarea
    item.task.delete()
    todo.saveData()

    # Y eliminar el elemento. Me parece que no queda lindo. ¿Es esta la única manera?
    self.ui.list.takeTopLevelItem(self.ui.list.indexOfTopLevelItem(item))

Excepto por la última línea este código debería ser obvio. ¿La última línea? Ni siquiera estoy seguro de que esté bien, pero funciona.

Ahora puedes probar la funcionalidad. Recuerda que si te quedas sin tareas puedes ejecutar python todo.py y obtener nuevas.

Puesta a punto de las acciones

Hay algunos problemas de interfaz en nuestro trabajo hasta ahora:

  1. El menú Tareas y la acción Borrar tarea carecen de atajos de teclado.

    Esto es muy importante; hace que la aplicación funcione mejor para los usuarios comunes. Además, en general los usuarios esperan que los atajos estén ahí y no hay razón para frustarlos.

    Por suerte es muy fácil arreglarlo. Sólo es necesario fijar la propiedad shortcut para action_Delete_Task y cambiar la propiedad text de menuTask a "&Tarea".

  2. La acción Borrar tarea está habilitada incluso cuando no hay motivo. Si el usuario no tiene una tarea seleccionada puede intentar llamar a la acción pero no hace nada. Eso sorprende un poco y, en mi opinión, sorprender a los usuarios no está muy bien.

    Hay otras opiniones sobre esto, en particular la de Joel Spolsky, así que quizás soy anticuado.

    Para habilitar o deshabilitar las acciones cuando un elemento de la lista de tareas esté o no seleccionado, necesitamos actuar en base a la señal currentItemChanged de nuestra lista de tareas. Aquí está el código:

    def on_list_currentItemChanged(self,current=None,previous=None):
        if current:
            self.ui.actionDelete_Task.setEnabled(True)
        else:
            self.ui.actionDelete_Task.setEnabled(False)
    

    Además, necesitamos que Borrar tarea comience desactivado porque cuando iniciamos la aplicación no hay ninguna tarea seleccionada. Esto se hace desde Designer utilizando la propiedad "enabled".

    Dado que hay una sola acción Borrar tarea, este código afecta a la barra de herramientas y también al menú. Esto ayuda a que tu IU sea consistente y se comporte como debe.

/static/tut4-window6.png

Una aplicación muy limitada

Próximamente

Bueno, esa fue una explicación bastante larga para una pequeña funcionalidad, ¿no es cierto? No te preocupes, será mucho más fácil añadir las próximas acciones, porque espero que cuando leas "añadí una acción llamada Nueva tarea" sepas de que se está hablando.

Y en la próxima sesión haremos justamente eso: crearemos nuestro primer diálogo.

Lectura adicional


Aquí puedes ver qué cambió entre la versión vieja y la nueva:

Modified lines:  None
Added line:  44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62
Removed line:  None
Generated by diff2html
© Yves Bailly, MandrakeSoft S.A. 2001
diff2html is licensed under the GNU GPL.

  session3/main.py     session4/main.py
  60 lines
1557 bytes
Last modified : Thu Mar 5 02:03:34 2009

    79 lines
2300 bytes
Last modified : Sat Mar 7 02:06:29 2009

1 # -*- coding: utf-8 -*-   1 # -*- coding: utf-8 -*-
2   2
3 """The user interface for our app"""   3 """The user interface for our app"""
4   4
5 import os,sys   5 import os,sys
6   6
7 # Import Qt modules   7 # Import Qt modules
8 from PyQt4 import QtCore,QtGui   8 from PyQt4 import QtCore,QtGui
9   9
10 # Import the compiled UI module   10 # Import the compiled UI module
11 from windowUi import Ui_MainWindow   11 from windowUi import Ui_MainWindow
12   12
13 # Import our backend   13 # Import our backend
14 import todo   14 import todo
15   15
16 # Create a class for our main window   16 # Create a class for our main window
17 class Main(QtGui.QMainWindow):   17 class Main(QtGui.QMainWindow):
18     def __init__(self):   18     def __init__(self):
19         QtGui.QMainWindow.__init__(self)   19         QtGui.QMainWindow.__init__(self)
20   20
21         # This is always the same   21         # This is always the same
22         self.ui=Ui_MainWindow()   22         self.ui=Ui_MainWindow()
23         self.ui.setupUi(self)   23         self.ui.setupUi(self)
24   24
25         # Let's do something interesting: load the database contents   25         # Let's do something interesting: load the database contents
26         # into our task list widget   26         # into our task list widget
27         for task in todo.Task.query().all():   27         for task in todo.Task.query().all():
28             tags=','.join([t.name for t in task.tags])   28             tags=','.join([t.name for t in task.tags])
29             item=QtGui.QTreeWidgetItem([task.text,str(task.date),tags])   29             item=QtGui.QTreeWidgetItem([task.text,str(task.date),tags])
30             item.task=task   30             item.task=task
31             if task.done:   31             if task.done:
32                 item.setCheckState(0,QtCore.Qt.Checked)   32                 item.setCheckState(0,QtCore.Qt.Checked)
33             else:   33             else:
34                 item.setCheckState(0,QtCore.Qt.Unchecked)   34                 item.setCheckState(0,QtCore.Qt.Unchecked)
35             self.ui.list.addTopLevelItem(item)   35             self.ui.list.addTopLevelItem(item)
36   36
37     def on_list_itemChanged(self,item,column):   37     def on_list_itemChanged(self,item,column):
38         if item.checkState(0):   38         if item.checkState(0):
39             item.task.done=True   39             item.task.done=True
40         else:   40         else:
41             item.task.done=False   41             item.task.done=False
42         todo.saveData()   42         todo.saveData()
43   43
      44     def on_actionDelete_Task_triggered(self,checked=None):
      45         if checked is None: return
      46         # First see what task is "current".
      47         item=self.ui.list.currentItem()
      48
      49         if not item: # None selected, so we don't know what to delete!
      50             return
      51         # Actually delete the task
      52         item.task.delete()
      53         todo.saveData()
      54
      55         # And remove the item. I think that's not pretty. Is it the only way?
      56         self.ui.list.takeTopLevelItem(self.ui.list.indexOfTopLevelItem(item))
      57
      58     def on_list_currentItemChanged(self,current=None,previous=None):
      59         if current:
      60             self.ui.actionDelete_Task.setEnabled(True)
      61         else:
      62             self.ui.actionDelete_Task.setEnabled(False)
44   63
45 def main():   64 def main():
46     # Init the database before doing anything else   65     # Init the database before doing anything else
47     todo.initDB()   66     todo.initDB()
48   67
49     # Again, this is boilerplate, it's going to be the same on   68     # Again, this is boilerplate, it's going to be the same on
50     # almost every app you write   69     # almost every app you write
51     app = QtGui.QApplication(sys.argv)   70     app = QtGui.QApplication(sys.argv)
52     window=Main()   71     window=Main()
53     window.show()   72     window.show()
54     # It's exec_ because exec is a reserved word in Python   73     # It's exec_ because exec is a reserved word in Python
55     sys.exit(app.exec_())   74     sys.exit(app.exec_())
56   75
57   76
58 if __name__ == "__main__":   77 if __name__ == "__main__":
59     main()   78     main()
60   79

Generated by diff2html on Sat Mar 7 02:08:22 2009
Command-line:
/home/ralsina/bin/diff2html session3/main.py session4/main.py

Comentarios

Comments powered by Disqus

Contents © 2000-2018 Roberto Alsina