0% encontró este documento útil (0 votos)
21 vistas

TutorialDaboParte 2

El documento describe cómo crear una aplicación básica en Dabo mediante scripts de Python. Explica cómo definir una clase de formulario, agregar controles y enlazar eventos para interactuar con el usuario.

Cargado por

Emanuel Toro
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
21 vistas

TutorialDaboParte 2

El documento describe cómo crear una aplicación básica en Dabo mediante scripts de Python. Explica cómo definir una clase de formulario, agregar controles y enlazar eventos para interactuar con el usuario.

Cargado por

Emanuel Toro
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
Está en la página 1/ 33

DESARROLLANDO CON DABO

PARTE 2

CREACIÓN DE UNA APLICACIÓN


SENCILLA
Desarrollando con Dabo
Creando una Aplicación Sencilla
OK, suficiente teoría y antecedentes por ahora. Vamos a comenzar por crear la aplicación más básica.
Por ahora estaremos usando la línea de comandos y sencillos scripts de texto; posteriormente usaremos
las herramientas visuales para hacer una gran parte de esto más fácil. Entonces, por cada script de
ejemplo, cópielo en un archivo de texto plano, póngale un nombre, como ejemplo.py y córralo
escribiendo el comando 'python ejemplo' en la línea de comandos.
Este es el script mas sencillo que usted puede encontrar:
import dabo
app = dabo.dApp()
app.start()

Cuando corra esto, usted obtiene que se muestre un formulario vacío junto con un menú básico. Cierre
el formulario y la aplicación termina. Ahora, reemplacemos ese aburrido formulario con algo un poco
más interesante:
import dabo
dabo.ui.loadUI("wx")
class HelloPyConForm(dabo.ui.dForm):
def afterInit(self):
self.Caption = "Hello PyCon Form"
self.lblHello = dabo.ui.dLabel(self,
Caption="Hello PyCon", FontSize=24,
ForeColor="blue")
app = dabo.dApp()
app.MainFormClass = HelloPyConForm
app.start()

Aquí definimos una clase formulario que hereda de la clase formulario de Dabo: dForm. Todas las
clases base IU se encuentran en el módulo dabo.ui. Como Dabo está diseñado para soportar múltiples
conjuntos de herramientas IU, necesitamos decirle a Dabo cual de ellas queremos usar, así que
agregamos la instrucción dabo.ui.loadUI("wx") para cargar el conjunto de herramientas
wxPython en el espacio de nombres dabo.ui. Se debe llamar a dabo.ui.loadUI() antes de subclasar
cualquiera de las clases IU.
Personalizamos el formulario en su método afterInit(). Este método es llamado automáticamente
por la estructura de trabajo por cada objeto, de manera de proporcionarle a usted un lugar para hacer
esta suerte de personalización sin tener que sustituir ( y potencialmente dañar) el método nativo
__init__(). Hay disponible otro útil método “after”llamado afterInitAll(). La diferencia entre
los dos es que afterInitAll() es llamado después que todos los objetos contenidos han sido creados.
Luego, en un formulario con varios objetos hay un orden diferente en el cual estas distintas cosas
ocurren y cuando los métodos correspondientes son llamados. Para ilustrar esto imaginemos un
ejemplo ligeramente diferente: un formulario que contiene un editbox, un panel y un textbox. El panel
contiene una etiqueta. Este es el orden en que los método afterInit() y afterInitAll() son llamados por
cada uno de estos objetos cuando se crea este formulario.
1. Creación del formulario
2. form.afterInit()
a. creación del editbox
b. afterInit() del editbox
c. afterInitAll() del editbox
d. creación del panel
e. afterInit() del panel
i. creación de la etiqueta del panel
ii. afterInit() de la etiqueta del panel
f. afterInitAll() del panel
g. creación del textbox
h. afterInit() del textbox
i. afterInitAll() del textbox
3. form.afterInitAll()

La estructura básica de una aplicación es definir la clase MainFormClass para la aplicación, y


comenzar el ciclo de eventos llamando app.start(). Usted puede también subclasar la clase dApp para
añadir sus propios comportamientos personalizados, y luego hacer referencia a ellos desde cualquier
lugar de su aplicación usando la referencia a obj.Application que está disponible para todos los
objetos en Dabo.
¿Listo para algo realmente interesante? Corra de nuevo la aplicación 'Hello PyCon', y entonces, desde
el menú Archivo, seleccione 'Command Window'. Va a ver una nueva ventana emergente; lo más
probablemente esta será demasiado pequeña para trabajar con ella, entonces, redimensiónela hasta algo
más razonable y ubíquela de modo que pueda ver la ventana principal también. Dabo recordará el
tamaño y posición de sus diversas ventanas cuando las cierre, así que la próxima vez que usted corra la
aplicación, las ventanas aparecerán como cuando las dejó.
La ventana de comandos es un intérprete interactivo de python que usted puede usar para investigar y
modificar aplicaciones en ejecución. Para facilitar su uso, hemos mapeado el nombre 'self' detrás de la
escena para referenciar el formulario que estaba más al frente cuando usted levantó la ventana de
comandos. Puede comprobarlo escribiendo self.Caption y presionando Intro; en el marco de
salida abajo, debería ver desplegado 'Hello PyCon'. También habrá notado la ventana emergente de
completación de código tan pronto tecleó el punto después de 'self': todos los atributos conocidos,
métodos y propiedades del objeto referido por 'self' (en este caso, el formulario 'HelloPyCon') se
muestran, y mientras usted teclea letras sucesivas de la propiedad deseada ('Caption' en este caso), el
ítem seleccionado en la lista cambia para coincidir con lo que usted tecleó. Una vez que tiene
seleccionado el que quiere presione Intro para que sea introducido en el comando por usted.
También puede cambiar cosas en el formulario. Para ver esto, presione Control-FlechaArriba una vez
para ir hasta el comando previo, y luego, al final, escriba para modificar el comando de modo que este
se lea self.Caption = "Yo cambié esto", y presione Intro para ejecutar esa instrucción. Si
observa el formulario, su título habrá sido cambiado interactivamente.
No nos detengamos aquí ¡creemos un control al vuelo! Teclee esta instrucción:
dabo.ui.dButton(self, Caption="Click Me")

Debería ver aparecer un nuevo botón, pero desafortunadamente está exactamente sobre la etiqueta. Para
hacer las cosas peores, no guardamos una referencia al botón, así que ¿cómo podemos moverlo?
Bueno, existe la propiedad Children del formulario, pero una vía aun más práctica es llamar el método
IU getMouseObject(), y éste retornará una referencia a cualquier objeto Dabo que esté debajo del
cursor del ratón cuando el comando se ejecute. Por lo tanto, coloque el cursor de su ratón sobre el
nuevo botón sin presionar sobre el formulario, y en la ventana de comandos escriba:
btn = dabo.ui.getMouseObject()

Presiona Intro, y ahora 'btn' es una referencia al nuevo botón. Y en el futuro, trate de recordar asignar
una referencia local a los nuevos objetos creados. Por ejemplo:
btn = dabo.ui.dButton(self, Caption="Click Me")

Nota para usuarios de Windows: algunas versiones de Windows actúan de manera extraña con el
comando getMouseObject(), moviendo la ventana de comandos al fondo de la pila mientras todavía la
muestran normalmente. Sólo presione el botón de ella en la barra de tareas y ella volverá a ser normal.
Ahora que tenemos la referencia, podemos jugar con el botón. Intente introducir instrucciones como
estas:
btn.Left = 50
btn.Bottom = 150

y debería ver el botón moviéndose mientras el comando se ejecuta. Claro, si presiona en el botón, se
muestra como que él está siendo pulsado, pero no hace nada. Vamos a crear al vuelo un método sencillo
y ligarlo al evento Hit del botón, el cual es activado cuando se presiona el botón. Teclee lo siguiente en
la ventana de comandos:
def pulsador(evt):
import time
self.Caption = time.ctime()
btn.bindEvent(dabo.dEvents.Hit, pulsador)

Ahora cuando presione el botón, el evento Hit será generado, y dado que éste está ligado al método
pulsador, este método actúa. Todos los manejadores de evento reciben un parámetro de objeto evento;
aunque no lo usamos aquí, aun tenemos que aceptarlo. Y como este método fue definido en la Ventana
de Comandos, 'self' se refiere al formulario. De modo que cada vez que presione el botón. El Titulo del
formulario cambiará a la hora actual!
Esto es aun cosa muy básica; usted puede hacer formularios mucho más complejos añadiendo más y
más controles. Hacer que todos ellos aparezcan bien requerirá que usted especifique el tamaño y la
posición de cada uno explícitamente, o use dimensionadores (sizers) para que hagan todo por usted.
Los sizers son un tópico completo en si mismos, y lo cubriremos más adelante. Por ahora, no podemos
hacer suficiente hincapié en que, si bien puede parecer difícil de entender en un primer momento, la
inversión en aprender a usar dimensionadores dará sus frutos en gran medida en el largo plazo. Ellos
manejan cosas como la resolución de la pantalla, cambios del tamaño de la fuente, variaciones de SO, y
redimensionamiento del formulario por usted para que su aplicación siempre luzca 'correcta'. O, al
menos, como un reloj que está marchando, pero 20 minutos más rápido “consistentemente mal”

El Ayudante de Aplicación
Dabo tiene una herramienta llamada AppWizard que creará una aplicación CRUD (Create, Read,
Update, Delete) muy funcional en cerca de 30 segundos. Esta es ideal para aplicaciones de
mantenimiento de tablas, pero ciertamente no está limitada a lo básico: usted puede extender su
aplicación para que trabaje de la forma que quiera. Algunos gustan de comenzar su desarrollo con la
salida del ayudante de aplicaciones; él les da el inicio de una aplicación funcional, que ellos pueden
entonces personalizar.
Para crear una aplicación con el Ayudante de Aplicaciones, corra python <ruta a ide>/
wizards/AppWizard/ AppWizard.py, en donde <ruta a ide> es la ruta a su directorio “ide” Dabo.
Va a ver una pantalla como la de abajo a la izquierda. Presione el botón 'Next', y va a obtener la
pantalla para introducir los parámetros de conexión a su base de datos (abajo a la derecha). Para esta
demostración PyCon demo, Hemos dispuesto una base de datos pública en el servidor dabodev.com
llamada 'pycon', y usted puede conectarse con el nombre de usuario 'pycon' y la clave 'atlanta'. Una vez
que haya enterado esa información, presione 'Next'.

En este punto el ayudante conectará con la base de datos y obtendrá la información relevante de las
tablas. Si escribió algo mal en lo de arriba o si su servidor no está disponible, usted recibirá un mensaje
de error explicando lo que estuvo erróneo. Asumiendo que todo estuvo bien, verá la página mostrada en
la página siguiente, a la izquierda, contentiva de las tablas disponibles en la base de datos. Para este
ejemplo, sólo marque la tabla 'recipes' y luego pulse 'Next'. Esto le brindará la página de la derecha.
El asistente, de forma predeterminada, asignará el nombre de la base de datos como nombre de su
aplicación; vamos a hacerlo de manera más específica y cambiemos el nombre de la aplicación a
'pycon_appwizard'. Usted puede también seleccionar el directorio donde quiere que Dabo coloque su
aplicación.
Hay opciones al final que le permiten controlar aspectos de la aplicación generada. Primero,
generalmente usted no desea las columnas PK en sus tablas como parte de su IU; los usuarios no se
interesan por las Pks. Sin embargo, algunos sistemas viejos usaban Pks cuyos valores son
significantes, por lo que si está tratando con una tabla de esas, marque la primera caja al final.
La siguiente caja de verificación controla cómo Dabo se maneja con tipos de datos 'exóticos' que son
específicos a algún RDBMS. Si sus datos contienen tales columnas, y quiere que Dabo los convierta
automáticamente, marque la segunda caja.
La siguiente caja de chequeo controla cómo los campos se añaden a la IU. Algunos usan una
convención para nombrar que ordena los campos alfabéticamente de una manera significante; si esto es
importante para usted, marque esta caja. De otra manera, Dabo añade los campos a la IU en el orden
que es informado desde la base de datos. Usted siempre puede reordenarlos posteriormente para su
presentación en la IU.
Finalmente, hemos mencionado antes que predeterminadamente Dabo usa tabulaciones para la
indentación. Si esto es una herejía pura para usted, marque la última caja y Dabo obedientemente usa 4
espacios por nivel de indentación.
Ahora pulse 'Next', y podrá ver el diálogo de confirmación:
Asumiendo que la creación de este directorio es lo que desea, pulse 'Yes'. Si no es así, pulse 'No' y será
devuelto a la pantalla previa, donde puede elegir una ubicación diferente. Una vez que haya establecido
la ubicación, hay una pantalla final mostrada abajo a la izquierda. Esta es la confirmación final; si
presiona 'Finish', se crea la aplicación. Si necesita cambiar algo, puede pulsar 'Back' y modificar
cualquiera de los valores seleccionados antes. O sólo presione 'Cancel' si desea desertar. Asumiendo
que usted haya presionado 'Finish', verá la notificación de abajo a la derecha

Esto es todo! Ahora usted puede navegar al directorio que escogió para su aplicación y ejecútela! Aquí
está como luce (pág. Sig., izquierda). Note que se ha creado un selector de búsqueda para cada uno de
los campos. Para este ejemplo deseo todas las recetas que contienen la palabra 'Rice' en el título, por lo
tanto introduje esos valores y pulsé el botón 'Requery'. Entonces obtuve una rejilla donde puedo
explorar todas las coincidencias. Observe que la barra de estado informa que hay 56 registros
coincidentes.
Desde aquí yo puedo seleccionar el registro que desee editar, y aun pulsar la pestaña 'Edit Recipes', o
simplemente presionar 'Enter'. Entonces obtengo la pantalla de edición:

Desde aquí puedo modificar cualquiera de los valores y luego volver a guardarlos en la base de datos, o
puedo desplazarme a otro registro, o eliminar el registro, o cualquier otra cosa que tendría que hacer en
una aplicación sencilla de mantenimiento de tablas.
Así que vamos a dar un paso atrás por un minuto y pensemos en lo que se acaba de lograr: tenemos una
aplicación de mantenimiento de tablas completa que nos permite buscar en la base de datos, seleccionar
los resultados; a continuación, editar y guardar los cambios de nuevo. Contamos con una interfaz de
usuario completa de ella, que fue construida para nosotros con la información obtenida de la base de
datos. Y lo hicimos en menos de un minuto!
Personalización de una Aplicación Appwizard
AppWizard genera una aplicación total, con un sistema de menús, formularios, informes, bizobjs, y un
archivo para apoyar la conexión predeterminada a la base de datos. Usted puede incluso conseguir un
setup.py y los archivos de soporte para construir su aplicación para ser distribuida para Mac, Windows
y Linux en formato ejecutable nativo.

Usted puede quedarse ahí y simplemente aceptar lo que fue generado de forma automática - tal vez la
salida generada está muy bien para sus simples necesidades- o puede personalizar el código a su gusto.
El alcance de sus mejoras realmente no es limitado: en los últimos dos años, yo (Paul) he desarrollado
una aplicación multi-plataforma comercial con veinte módulos comenzando con la salida del Asistente
para Aplicaciones contra una tabla. Ella sirvió como punto de partida para un prototipo rápido, y no
había razón aparente para empezar desde cero al momento de comenzar a construir la aplicación real.
Trate el código generado utilizando el Asistente para Aplicaciones como punto de partida, avanzando a
partir de allí se puede utilizar cualquier parte de Dabo que usted desee.

Para las mejoras en este tutorial, vamos a concentrarnos primero en algunas "frutas maduras" en el
código generado por AppWizard, y luego pasemos a algunos temas más avanzados. En concreto, vamos
a modificar los títulos de los campos en la rejilla y en las páginas seleccionar y editar; añadir algunas
reglas de negocio, añadir algunos controles de interfaz de usuario para enlazar las categorías de recetas
a las recetas, y elaborar un informe para la impresión de nuestras recetas. Vamos a hacer esto en menos
de 30 minutos.

Preliminares

Primero ejecute el AppWizard de nuevo, pero esta vez incluya todas las 3 tablas relacionadas a las
recetas (recipes, reccats y reccat). Las tablas son:
• recipes : tabla de nombres de recipe, ingredientes, y procedimientos.
• reccats : tabla de categorías de recipe (Main Dishes, etc.)
• reccat : tabla intermediaria de 'unión' para una relación muchos a muchos entre recipes y
reccats.

Además, para ser consistente con los ejemplos que siguen, guarde la aplicación como "recipes_aw 'en
lugar de' pycon_appwizard" como lo hicimos la primera vez.
En segundo lugar, eliminar reccat de la mayor parte de la interfaz de usuario. Esta es una tabla de unión
de tablas así que no estaremos interactuando con ella directamente. El Ayudante de Aplicaciones no lo
sabe y obedientemente la incluyó. Abra ui/MenFileOpen.py y simplemente haga que la definición de
formulario en la línea 24 tenga el siguiente aspecto:
24 forms = (
25 ("&Recipes", app.ui.FrmRecipes),
26 ("&Categories", app.ui.FrmReccats))

Tenga en cuenta que he eliminado la entrada "reccats ', escrito "Categorías", cambié el orden de las
entradas de Recipes y Categorías, y agregué explícitamente teclas de acceso rápido (&). También
eliminé un espaciador extraño.
El objeto MenFileOpen es instanciado y añadido al menú File|Open cuando se instancia cualquiera de
los formularios de nuestra aplicación. Puede ver este código de llamada en, por ejemplo,
ui/FrmRecipes.py.
Cuando el Asistente para aplicaciones generó la aplicación, desafortunadamente, escogió "reccat 'como
el formulario predeterminado para abrir al ejecutar la aplicación. Pero, afortunadamente, esto nos da la
oportunidad de hurgar en el script principal. Cambie el valor predeterminado del archivo principal
(recipes_aw.py en el directorio raíz) en la línea 50:
50 default_form = ui.FrmRecipes

Una vez que guarde los cambios y corra la aplicación, el formulario Recipes abrirá por defecto.
El archivo principal es el punto de entrada de la aplicación. Él importa dabo, e instancia la clase App
de nuestra aplicación, la cual es una subclase de dabo.dApp(). Hay que recordar que Dabo tiene 3
niveles (db, biz, ui). El objeto aplicación abarca todos esos niveles, así que no importa qué nivel se está
codificando, siempre se puede llegar a la instancia dApp. El archivo principal que genera el Asistente
para aplicaciones hace algunos enlaces convenientes para usted, como la adición de los espacios de
nombres de interfaz de usuario y negocio a la instancia de dApp, y guardar la automáticamente abierta
conexión db al atributo 'dbConnection ". Por lo tanto, para crear instancias de un bizobj, usted no
necesita importar nada, sólo necesitaría algo así como:
app = self.Application
mybiz = app.biz.Recipes(app.dbConnection)

El código en la parte inferior del script principal es lo que levanta su aplicación. Es lo que se ejecuta en
primer lugar al escribir "python recipes_aw.py'.

Leyendas
Las leyendas de los diversos elementos de la interfaz son los más fáciles de cambiar, y la actividad de
cambio de las leyendas sirve como punto de partida para descubrir cómo está organizado el código. Use
una utilidad de búsqueda de texto como grep para encontrar la leyenda que desea cambiar. Una vez que
la haya cambiado, revise el archivo que ha abierto para cambiar también otras cosas a su gusto.
Por ejemplo, es probable que desee buscar y reemplazar "ingred" con "ingredientes". En primer lugar,
busque en el directorio IU los archivos que contienen esta cadena, y luego ábralos con vim (o su editor
de texto favorito) para hacer los cambios.
mac:ui pmcnett$ grep -i ingred *.py
GrdRecipes.py: self.addColumn(dabo.ui.dColumn(self, DataField="ingred",
Caption="Ingred",
PagEditRecipes.py: ## Field recipes.ingred
PagEditRecipes.py: label = self.addObject(dabo.ui.dLabel,
NameBase="lblingred",
PagEditRecipes.py: Caption="ingred")
PagEditRecipes.py: objectRef = self.addObject(dabo.ui.dEditBox,
NameBase="ingred",
PagEditRecipes.py: DataSource="recipes", DataField="ingred")
PagSelectRecipes.py: ## Field recipes.ingred
PagSelectRecipes.py: lbl.Caption = "ingred:"
PagSelectRecipes.py: lbl.relatedDataField = "ingred"
PagSelectRecipes.py: self.selectFields["ingred"] = {
PagSelectRecipes.py: dabo.errorLog.write("No control class found for
field 'ingred'.")
Rápidamente encontramos que 'ingred' (tanto en mayúsculas como en minúsculas) se encuentra en
GrdRecipes.py, PagEditRecipes.py, y en PagSelectRecipes.py. Estos representan las clases para la
rejilla de búsqueda, la pagina de edición, y la página de selección, respectivamente. Luego abra
GrdRecipes.py y haga el cambio, tomándose tiempo para mirar también el resto del archivo para
comprender lo que está haciendo. Primero, hay una línea que define la codificación a utilizar. Esto es
un estándar en python. Luego, se importa dabo, y se establece wxPython como el conjunto de
herramientas de IU. Después se importa la superclase de definición de la rejilla desde GrdBase.py, que
a su vez es una subclase de dabo.lib.datanav.Grid. Finalmente se define nuestra clase GrdRecipes
basada en GrdBase.
1 # -*- coding: utf-8 -*-
2
3 import dabo
4 if __name__ == "__main__":
5 dabo.ui.loadUI("wx")
6 from GrdBase import GrdBase
7
8
9 class GrdRecipes(GrdBase):

El resto del archivo sustituye al método predeterminado afterInit(), y añade las columnas a la rejilla
que usted ve en la página de explorar cuando corre la aplicación recipes. Una de esas definiciones tiene
el aspecto:
21 self.addColumn(dabo.ui.dColumn(self, DataField="ingred",
22 Caption="Ingred", Sortable=True, Searchable=True,
23 Editable=False))

Y, si bien inicialmente nos propusimos cambiar el título de "Ingred" a "Ingredients", en realidad no


tiene mucho sentido incluir este campo en la rejilla de búsqueda, en absoluto, ya que la intención de la
rejilla es recoger los registros a mostrar en la página de edición, y si bien es posible configurar la rejilla
para mostrar varias líneas de texto para cada campo, hacerlo haría que fuera menos eficaz como
selector de registro.

Los ingredientes no son una información crítica a mostrar, de modo que debería suprimirse la columna.
Elimine totalmente esa llamada a addColumn(), así como también aquellas para subtitle, proced, e
image. Por diversión, añada un poco de configuración de propiedades propias para las columnas
restantes, como HeaderBackColor, ForeColor, HeaderFontItalic, y FontSize. Este es mi completo
afterInit() después de hacer algunos de esos cambios:
11 def afterInit(self):
12 self.super()
13
14 self.addColumn(dabo.ui.dColumn(self, DataField="title",
15 Caption="Title", HeaderFontItalic=True,
16 FontBold=True)
17 self.addColumn(dabo.ui.dColumn(self, DataField="date",
18 Caption="Date", HeaderBackColor="yellow",
19 FontSize=12))

Note que removí algunos de los valores generados de las propiedades (Sortable, Searchable, y
Editable). Estos no son necesarios porque ya ellas adquieren por defecto los valores especificados y el
código debe ser lo más claro y conciso como sea posible. Ellos fueron incluidos en el código generado
para ayudarle a empezar, por la misma razón es que los archivos están abundantemente comentados.
Hay un poco de código de prueba al final de GrdRecipes.py:
21 if __name__ == "__main__":
22 from FrmRecipes import FrmRecipes
23 app = dabo.dApp(MainFormClass=None)
24 app.setup()
25 class TestForm(FrmRecipes):
26 def afterInit(self): pass
27 frm = TestForm(Caption="Test Of GrdRecipes", Testing=True)
28 test = frm.addObject(GrdRecipes)
29 frm.Sizer.append1x(test)
30 frm.show()
31 app.start()

Poniendo código de prueba como este en sus scripts python le permite correr rápidamente una prueba
de su diseño sin ejecutar la aplicación completa, acelerando el ciclo de desarrollo. En este caso, como
no hay datos, usted va a ver sólo los cambios de las propiedades del encabezado; continúe y ejecute
'python GrdRecipes.py' para ver como luce. Debería ver algo como:

Bueno, así fuimos desviados desde la búsqueda inicial de "ingred", y terminamos con un archivo
GrdRecipes.py editado, que probablemente es lo que queremos. ¿Por qué no completar la idea y editar
los otros archivos Grd *. py mientras estamos en esto?
mac:ui pmcnett$ ll Grd*.py
-rw-r--r-- 1 pmcnett pmcnett 97 Feb 8 11:19 GrdBase.py
-rw-r--r-- 1 pmcnett pmcnett 808 Feb 8 11:19 GrdReccat.py
-rw-r--r-- 1 pmcnett pmcnett 944 Feb 8 11:19 GrdReccats.py
-rw-r--r-- 1 pmcnett pmcnett 735 Feb 8 11:39 GrdRecipes.py

GrdBase.py es sólo una simple subclase de dabo.lib.datanav.Grid de la cual las otras clases grid
heredan. Si usted desea un aspecto, sensación y funcionalidad común en todas sus rejillas, ponga eso en
GrdBase en lugar de duplicarlo en todas las clases rejilla.
No vamos a usar GrdReccat.py porque no hay una razón inmediata para explorar directamente esa
tabla intermediaria. Sin embargo, no daña dejarla allí, en caso de que se convierta en conveniente en el
futuro. Por ahora, prosiga y edite GrdReccats.py para remover las columnas extrañas y cambiar la
leyenda 'Descript' a 'Title', que describe mejor el campo al usuario. Esta es la definición completa de la
clase después de esos cambios:
9 class GrdReccats(GrdBase):
10
11 def afterInit(self):
12 self.super()
13 self.addColumn(dabo.ui.dColumn(self, DataField="descrp",
14 Caption="Title"))

Ahora que todas las rejillas están conformadas como queremos, continuemos con nuestra aventura
Cambia-Leyenda con PagEditRecipes.py. Este archivo define la clase de la página para mostrar y
editar sus registros en la fila actual de los bisobj de recipes. Observa que cuando usted corre la
aplicación, la IU básica es una página marco con tres páginas: Select, Browse, y Edit. Usted usa esas
páginas para introducir sus criterios de selección y requerir al bizobj que busque o navegue los
registros en el bizobj , y para editar el registro actual, respectivamente. PagEditRecipes es la página de
edición del módulo recipes.

Abra PagEditRecipes en su editor de texto y va a ver el código que diseña las diferentes dLabels,
dTextBoxes y dEditBoxes en la página. Puede ver el código para crear el dGridSizer que se utiliza
para controlar el posicionamiento automático, de manera que no necesita predecir cómo manejar
pantallas de diferentes tamaños o problemas de plataforma cruzada. Busque "ingred" y localice la línea
39:

39 ## Field recipes.ingred
40 label = self.addObject(dabo.ui.dLabel, NameBase="lblingred",
41 Caption="ingred")
42 objectRef = self.addObject(dabo.ui.dEditBox,NameBase="ingred",
43 DataSource="recipes", DataField="ingred")

Esta es la definición de la etiqueta que queremos cambiar. Adicionalmente al cambio de la leyenda de


esta etiqueta a 'Ingredients', diríjase a los otros objetos y haga que sus leyendas tengan mayúscula
inicial. Ejecute la prueba escribiendo 'python PagEditRecipes' en el terminal.
El diseño automático, aunque aceptable, puede ser ajustado fácilmente para que coincida mejor con la
importancia relativa de los datos. Mientras que un mayor control podría ser adquirido cambiando desde
el dGridSizer generado a dSizers anidados, se puede ganar un poco con los cambios deseados de
forma rápida y fácil haciendo algunos ajustes sencillos:

Hacer que la leyenda de la receta destaque más haciendo la fuente negrita y más grande (linea 22):
17 ## Field recipes.title
18 label = self.addObject(dabo.ui.dLabel, NameBase="lbltitle",
19 Caption="Title")
20 objectRef = self.addObject(dabo.ui.dTextBox, NameBase="title",
21 DataSource="recipes", DataField="title",
22 FontBold=True, FontSize=12)

Hacer que el subtítulo tenga una fuente más pequeña, y fiar una altura menor en vez de que crezca y se
encoja con los otros campos cuando el formulario sea redimensionado (yo se que este campo no
necesita mucho espacio en la práctica) (linea 33 establece la altura mínima mientras que la línea 37 le
dice al dimensionador rejilla que no haga crecer esa línea en particular):
28 ## Field recipes.subtitle
29 label = self.addObject(dabo.ui.dLabel, NameBase="lblsubtitle",
30 Caption="Subtitle")
31 objectRef = self.addObject(dabo.ui.dEditBox,
32 NameBase="subtitle", DataSource="recipes",
33 DataField="subtitle", Height=35, FontSize=9)
34
35 gs.append(label, alignment=("top", "right") )
36 currRo = gs.findFirstEmptyCell()[0]
37 gs.setRowExpand(False, currRow)

La última cosa a tratar es el campo de fecha de las recetas, que es demasiado ancho para mostrar una
fecha en él, y realmente deberíamos estar usando un dabo.ui.dDateTextBox en lugar de un dTextBox,
ya que el primero añade algunas características interesantes, como un widget calendario desplegable: la
línea 68 cambia la clase que estamos instanciando, la línea 72 elimina la palabra clave 'expand' lo cual
mantendrá el control en su Anchura predeterminada. Sustituya el comportamiento predeterminado si le
gusta, especificando la propiedad Width):

65 ## Field recipes.date
66 label = self.addObject(dabo.ui.dLabel, NameBase="lbldate",
67 Caption="Date")
68 objectRef = self.addObject(dabo.ui.dDateTextBox,
69 NameBase="date", DataSource="recipes",
70 DataField="data")
71 gs.append(label, alignment=("top", "right") )
72 gs.append(objectRef)

Aquí está la página de edición ahora, con algunos datos reales. Mucho mejor con un pequeño ajuste:
También es posible tomarse un minuto para cambiar PagEditReccats.py a nuestro gusto, también.
Cambie la leyenda de 'descrp' a “Title” y “html” a " Description ". Los nombres de campo en este caso
no reflejan información significativa, y este cambio es de suma importancia para no confundir al
usuario.

Ahora, para las páginas seleccionar: El código generado allí es repetitivo y obtuso. Volveremos a él
más tarde - por ahora sólo cambiar los títulos como "ingred" a "Ingredientes", etc Busque en "Caption"
para encontrarlos. Haga lo mismo para PagSelectRecipes.py y PagSelectReccats.py.

Reglas de Negocio
Por el momento, a usted se le permite hacer cualquier cambio que desee a las recetas usando la IU,
incluyendo agregar nuevos registros en blanco, borrar registros existentes, y lo que sea. Por defecto, los
bizobjs permiten todo, todo el tiempo, por lo tanto vamos a añadir algunas restricciones en el ámbito de
cambios que un usuario normal es capaz de hacer. Dabo está diseñado para permitir un control muy
preciso sobre todas operaciones de base de datos, por lo que usted debería poder hacer una aplicación,
por ejemplo, que permita a una cierta clase de usuarios -digamos 'usuarios poderosos' – mayor acceso
que a los usuarios 'normales'.

Usted podria escribir el código para esto con la ayuda del SecurityProvider de Aplicación y la pantalla
de inicio de sesión, lo que está fuera del alcance de este tutorial. A los efectos de este ejemplo, vamos a
establecer algunas reglas de negocio sencillas que se aplican a todos los que usan la aplicación, y
mostrarlos en la operación.

Continúe para que vea como edité biz/Recipes.py.


Una de las reglas de negocio es que todos los registros deben tener una fecha válida. Escribe tus objetos
de negocio para que sean tan agradables para el usuario como sea posible, lo que incluye el
establecimiento de parámetros por defecto sensibles y válidos. En el caso del campo de fecha, un buen
valor inicial es la fecha del día actual. Establécelo usando la propiedad DefaultValues del Recipes
bizobj (lineas 3 y 25):
1 # -*- coding: utf-8 -*-
2
3 import datetime
4 from Base import Base
5
6
7 class Recipes(Base):
8
9 def initProperties(self):
10 self.super()
11 self.Caption = "Recipes"
12 self.DataSource = "recipes"
13 self.KeyField = "id"
14
15 self.DataStructure = (
16 ("id", "I", True, "recipes", "id"),
17 ("title", "C", False, "recipes", "title"),
18 ("subtitle", "M", False, "recipes", "subtitle"),
19 ("ingred", "M", False, "recipes", "ingred"),
20 ("proced", "M", False, "recipes", "proced"),
21 ("date", "D", False, "recipes", "date"),
22 ("image", "C", False, "recipes", "image"),
23 )
24
25 self.DefaultValues["date"] = datetime.date.today

Observe que pusimos el valor predeterminado del campo de fecha como la función objeto
datetime.date.today, no al valor de retorno de la llamada a la función. Dabo obtendrá el valor de
retorno de la función al momento en que cualesquiera nuevos registros se añadan usando este estilo.
En este caso esto no importa mucho porque ¿quién va a ejecutar la función por más de un día?, pero es
bueno recordar esta característica. Yo la uso en algunas de mis aplicaciones para recuperar el próximo
número de orden, número de cliente, etc.

Otra sensible regla es no permitir títulos de recetas vacíos o casi vacíos. Abajo nosotros tratamos un
par de métodos de validación, finalmente asentándolos en validateRecord() en vez de validateField(),
porque queremos darle al usuario tanto control como sea posible sobre la experiencia en el flujo de la
entrada de datos. Realmente nos preocupamos por que los datos se ajusten a las reglas en el momento
de guardarlos, luego validateRecord() es realmente la vía para hacerlo. Pero prosiga y comente la linea
29 para que vea a validateField() en acción, también.

28 def validateField(self, fld, val):


29 return "" ## validateRecord is better
30 if fld == "title":
31 return self.validateTitle(val)
32
33
34 def validateRecord(self):
35 msgs = []
36 msg = self.validateTitle()
37 if msg:
38 msgs.append(msg)
39 if not self.Record.date or self.Record.date < datetime.date(1990, 1, 1):
40 msgs.append("There must be a valid date.")
41
42 if msgs:
43 return "The recipe '%s' could not be saved because:\n" \
44 % self.Record.title.strip() + "\n".join(msgs)
45 return "" ## all okay
46
47
48 def validateTitle(self, val=None):
49 oldVal = self.oldVal("title") ## as it was after requery()
50 curVal = self.Record.title ## as it is now
51 if val is None:
52 # val is only sent in validateField, and is the user-entered value
53 # not yet in the bizobj at all.
54 val = curVal
55 if oldVal.strip() and len(val.strip()) < 2:
56 return "Title too short.."

Arriba sustituimos los métodos validateField() y validateRecord() de todas las clases derivadas de
dBizobj añadimos nuestro propio método, validateTitle(), el cual es llamado desde los otros dos
métodos. Cualquier valor de retorno se muestra en la IU de cierta manera, pero aquí, en la capa de
bizobj no nos preocupamos por lo que haga la interfaz con la información: sólo nos preocupamos por
escribir la lógica para determinar si la regla de negocio pasa o falla, y si falla, retornar un mensaje
significativo.
Una de las reglas que establecimos fue que la fecha debe ser mayor que Enero 1, 1990. Vaya y corra la
aplicación, agregue un registro nuevo (usando la barra de navegación, el elemento de menú File|New, o
presionando Ctrl+N (Cmd+N en Mac)), y trate de guardarlo. Obtendrá un diálogo "Save Failed" con el
mensaje que el título del mensaje es muy corto. El campo de fecha pasó la validación debido al valor
predeterminado que establecimos en la linea 25. Bien, ahora introduzca un título de receta
significativo, pero cambie a una fecha anterior a 1990. De nuevo no le dejará guardarlo. Sólo cuando
todas las reglas establecidas en validateRecord() pasan, se permitirá que el guardado continúe.
Si este bizobj hubiera sido un hijo de otro bizobj, y el método validateRecord() hubiera sido llamado
como resultado del método guardar del bizobj paterno, el fallo habría resultado en deshacer todos los
cambios flujo arriba. Todos los métodos involucrados en cambios de datos (actualizaciones e
inserciones) están envueltos en transacciones (una sola transacción por cada salvamento, lo que abarca
los salvamentos resultantes de bizobjs abajo de la línea).
La edición de estas reglas le da a usted la oportunidad de examinar la definición de la clase bizobj
Recipe. Note que definimos explícitamente una Estructura de Datos: haciendo esto se evita que Dabo
esté suponiendo los tipos de datos por usted – esto le asigna a usted el control porque usted lo sabe
mejor. Probablemente, los predeterminados generados por el Ayudante de Aplicaciones están bien
(ellos son los que Dabo supondría durante la ejecución), pero en el caso de que uno de ellos esté
reflejando el tipo incorrecto, usted puede cambiarlo aquí.
Dabo usa identificadores de un carácter de tipo short en este contexto, y puede ver como ellos mapean
a tipos python echándole una mirada a dabo.db.getPythonType() y a dabo.db.getDaboType(). Cada
uno de ellos descansa en un mapeado, que también puede usted imprimir para entender lo que está
sucediendo por detrás de la escena:
mac:biz pmcnett$ python
Python 2.5.4 (r254:67917, Dec 23 2008, 14:57:27)
[GCC 4.0.1 (Apple Computer, Inc. build 5363)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import dabo
>>> print dabo.db.getPythonType("C")
<type 'unicode'>
>>> print dabo.db.getDaboType(bool)
B
>>> print dabo.db.daboTypes
{'C': <type 'unicode'>, 'B': <type 'bool'>, 'D': <type 'datetime.date'>, 'G': <type 'long'>, 'F':
<type 'float'>, 'I': <type 'int'>, 'M': <type 'unicode'>, 'L': <type 'str'>, 'N': <class
'decimal.Decimal'>, 'T': <type 'datetime.datetime'>}
Luego usted puede encontrar los códigos a introducir en la Estructura de Datos inspeccionando
dabo.db.daboTypes.

Creación de la Relación Muchos:Muchos entre Recipes y Categories


Actualmente podemos editar, agregar y eliminar recetas. Podemos editar, agregar y eliminar categorías.
Pero no podemos ver cuales categorías están vinculadas a una receta determinada, ni podemos ver
cuales recetas están ligadas a una categoría dada. Y tal vez más importante, no podemos hacer cambios
a cuales recipes están en una categoría y viceversa.
Añadir la deseada funcionalidad requiere establecer relaciones entre los objetos de negocio, apropiados
y añadir controles a la interfaz para interactuar con los objetos de negocio hijos. Dado que la relación
objetos de negocio necesita estar lista antes de que algo funcione en la capa ui, vamos a hacer esto
primero.
Recuerde que actualmente hay definidos tres objetos de negocio: Recipes, Reccat y Reccats. Reccat es
la tabla unión muchos a muchos que contiene las claves foráneas de ambas Recipes y Reccat. Para
darle a la IU la capacidad de mostrar cuales categorías están ligadas a cada receta, necesitamos crear un
objeto de negocio hijo adecuado para Recipes. Como esto es fácil y es posible que desee esta
funcionalidad en el futuro, haremos una cosa similar a la inversa con el objeto de negocio Reccats
también, de modo que podamos ver las recetas vinculadas a cierta categoría.
En el listado de abajo, he quitado los amplios comentarios y añadido algún espacio en blanco, y
añadido dos subclases de Reccat: CategoriesForRecipe y RecipeForCategory. Emplee un tiempo para
examinar este código pues hay ciertos cambios sutiles.
# -*- coding: utf-8 -*-

from Base import Base

class Reccat(Base):
def initProperties(self):
self.super()
self.Caption = "Reccat"
self.DataSource = "reccat"
self.KeyField = "id"
self.DataStructure = [
("id", "I", True, "reccat", "id"),
("recid", "I", False, "reccat", "recid"),
("catid", "I", False, "reccat", "catid"),
]
self.FillLinkFromParent = True

def afterInit(self):
self.super()

def setBaseSQL(self):
self.addFrom("reccat")
self.setLimitClause("500")
self.addFieldsFromDataStructure()

class CategoriesForRecipe(Reccat):
def initProperties(self):
self.LinkField = "recid"
self.super()
self.DataStructure.append(("catname", "C", False, "reccats", "descrp"))
def setBaseSQL(self):
self.super()
self.addJoin("reccats", "reccats.id = reccat.catid", "inner")

class RecipesForCategory(Reccat):
def initProperties(self):
self.LinkField = "catid"
self.super()
self.DataStructure.append(("recname", "C", False, "recipes", "title"))
def setBaseSQL(self):
self.super()
self.addJoin("recipes", "recipes.id = reccat.recid", "inner")

Primero, he modificado la propiedad DataStructure en subclase añadiendo columnas específicas.


Segundo, he puesto la propiedad FillLinkFromParent a verdadero, para no tener que preocuparnos por
añadir el valor de la clave foránea cuando se inserten nuevos registros. Tercero, en las subclases he
establecido la propiedad LinkField para decirle a Dabo cual es la clave foránea al padre, de modo que
cuando el padre requery()s, el hijo pueda reconsultar junto con él automáticamente. Cuarto, he añadido
uniones internas para obtener el nombre de la receta desde el objeto de negocio categories y el nombre
de la categoría desde el objeto de negocio recipes respectivamente. Utilicé subclases para maximizar la
reutilización de código.
A continuación, hay algunos cambios en los objetos de negocio Recipes y Reccats para agregar el hijo
apropiado a cada uno. Voy a mostrar aquí sólo los cambios a Recipes, pues esto es en lo que estamos
enfocados y los cambios son similares.
1 # -*- coding: utf-8 -*-
2
3 import datetime
4 from Base import Base
5 from Reccat import CategoriesForRecipe
Luego, (lineas 27-33, comenzando al final del método initProperties()), agregue el código para añadir
el hijo. O lo codifiqué de esta forma para que en caso de futuras mejoras resulten en que más de un hijo
sea añadido..
26 self.DefaultValues["date"] = datetime.date.today
27 self.addChildren()
28
29
30 def addChildren(self):
31 app = self.Application
32 self.bizReccat = CategoriesForRecipe(app.dbConnection)
33 self.addChild(self.bizReccat)

Ahora el bizobj Recipes tiene un hijo que puede contener cero a muchos registros representando las
categorías asignadas a cada receta. Todo está en su lugar para que la interfaz pueda mostrar esta
información. Después añadimos un par de métodos a FrmRecipes, incluyendo la instanciación del
bizobj Reccats con el propósito de mantener una lista de todas las posibles categorías.
24 def afterInit(self):
25 if not self.Testing:
26 app = self.Application
27 self.addBizobj(app.biz.Recipes(app.dbConnection))
28
29 # Añadir el bizobj categorías separado, para darnos la lista
30 # de todas las categorías de recetas:
31 self.addBizobj(app.biz.Reccats(app.dbConnection))
32 self.requeryCategories()
33 self.super()
34
35
36 def requeryCategories(self):
37 biz = self.getBizobj("reccats")
38 biz.UserSQL = "select * from reccats order by descrp"
39 biz.requery()
40
41
42 def getCategoryChoicesAndKeys(self):
43 """Returna dos listas, una para todos los valores de descrp y una para
todos los valores de."""
44 choices, keys = [], []
45 biz = self.getBizobj("reccats")
46 for rownum in biz.bizIterator():
47 choices.append(biz.Record.descrp)
48 keys.append(biz.Record.id)
49 return (choices, keys)

Estos son métodos a nivel de formulario por que potencialmente ellos pueden ser llamados por
cualquier control en el formulario. En general, conserve el código que interactúe con bizobjs a nivel de
formulario -es más fácil de mantener a la larga. A continuación creamos un nuevo archivo en el
directorio UI, CklCategories.py, el cual define una subclase de dCheckList, el cual permite a la IU
mostrar un listado de todas las categorias posibles, a la par que coloca una marca en todas las
categorías ligadas a la receta actual.
# -*- coding: utf-8 -*-

import dabo
if __name__ == "__main__":
dabo.ui.loadUI("wx")
class CklCategories(dabo.ui.dCheckList):
def initProperties(self):
self.Width = 200
self._needChoiceUpdate = True

def updateChoices(self):
self._needChoiceUpdate = False
if self.Form.Testing:
self.Choices = ("Test", "Appetizers", "Greens")
self.Keys = (1, 2, 3)
return
self.Choices, self.Keys = self.Form.getCategoryChoicesAndKeys()

def update(self):
self.super()
if self._needChoiceUpdate:
self.updateChoices()
if self.Form.Testing:
return
bizRecipes = self.Form.getBizobj()
bizReccat = bizRecipes.bizReccat
keyvalues = []
for rownum in bizReccat.bizIterator():
keyvalues.append(bizReccat.Record.catid)
self.KeyValue = keyvalues

def onHit(self, evt):


print "onHit", self.KeyValue

if __name__ == "__main__":
from FrmRecipes import FrmRecipes
app = dabo.dApp(MainFormClass=None)
app.setup()
class TestForm(FrmRecipes):
def afterInit(self): pass
frm = TestForm(Caption="Test Of CklCategories", Testing=True)
test = frm.addObject(CklCategories)
frm.Sizer.append(test, 1)
frm.show()
frm.update()
app.start()

La propiedad Testing nos permite codificar nuestras clases de tal manera que posibilita probar el
control sin tener que cargar la aplicación completa, o datos reales. Vaya y corra este archivo cuando
haya copiado y pegado el código, y verá una salida como la siguiente:
Cuando coloque una marca en un ítem, el evento onHit se dispara y usted puede ver la impresión en la
consola. Volveremos y llenaremos onHit() con una funcionalidad real después – cuando se haga, él
añadirá un nuevo registro al objeto de negocio hijo, o borrará en ese objeto uno existente.
Ahora es el momento de añadir este nuevo control CklCategories a la página de edición del formulario
recipes. Entonces, edite ui/PagEditRecipes.py y añada la importación de la línea 7, y cambie la
orientación del sizer principal de vertical a horizontal (línea 16) de modo que cuando agreguemos el
nuevo control este se coloque a la derecha del sizer de rejilla en vez de debajo de él:
6 from PagEditBase import PagEditBase
7 from CklCategories import CklCategories
8
9
10
11 class PagEditRecipes(PagEditBase):
12
13 def createItems(self):
14 """Called by the datanav framework, when it is time to create the
controls."""
15
16 mainSizer = self.Sizer = dabo.ui.dSizer("h")

A continuación cambiamos la inserción del dGridSizer(gs) para quitar el borde de la derecha (línea 79),
instanciamos y agregamos al dimensionador principal un nuevo dimensionador vertical (líneas 81 y 82)
sin borde izquierdo, y finalmente, agregamos el nuevo control CklCategories al nuevo dimensionador.
Descubrir las propiedades del dimensionador fue el resultado de editar, ejecutando PagEditRecipes.py
para comprobar el diseño y editar de nuevo. Conseguir los valores expansión y borde correctos nunca
me ocurrió al primer intento, después de cinco años de tratar con dimensionadores. Afortunadamente es
fácil y rápido probar-editar-repetir hasta que el diseño se vea bien. Más adelante verá como utilizar el
Diseñador de Clases para ver interactivamente cuales valores de las propiedades se tienen en el diseño
de dimensionadores.
79 mainSizer.insert(0, gs, 1, "expand", border=20, borderSides=("top",
"bottom", "left"))
80
81 vsRight = dabo.ui.dSizer("v")
82 mainSizer.append(vsRight, "expand", border=20, borderSides=("top",
"bottom", "right"))
83
84 vsRight.append(CklCategories(self), 1, "expand")
85
86 self.Sizer.layout()
87 self.itemsCreated = True
88
89 self.super()
Con estos cambios, podemos correr la aplicación, seleccionar algún criterio de búsqueda, consultar, y
cuando vayamos a la página de edición veríamos algunas categorías marcadas por cada receta. A
medida que naveguemos por la lista de recetas veríamos cambiar las categorías marcadas. Esta es una
toma de pantalla de mi sistema en este momento.

Marcar o desmarcar no tiene ningún efecto en el objeto de negocio, aun, porque no hemos codificado
eso todavía. Vamos a hacerlo ahora.
Añadiremos una cadena de responsabilidad desde el manejador onHit de dCheckList, al formulario, y
luego al objeto de negocio para que haga el trabajo. Cada eslabón de la cadena conoce su parte, y
delegar al siguiente eslabón apropiadamente. En CklCategories.onHit(), conseguimos el índice del ítem
de checklist que fue pulsado, y luego recogemos el valor de la clave de ese ítem. Luego determinamos,
basados en la presencia o ausencia de esa clave en la lista KeyValue si añadir o quitar esa categoría.
Delegamos al formulario el añadir o quitar la categoría:
41 def onHit(self, evt):
42 idx = evt.EventData["index"]
43 idxKey = self.Keys[idx]
44 if idxKey in self.KeyValue:
45 self.Form.addCategory(idxKey)
46 else:
47 self.Form.delCategory(idxKey)

Agregue los dos nuevos métodos addCategory() y delCategory() a FrmRecipes.py:


52 def addCategory(self, catid):
53 self.getBizobj().bizReccat.addLink(catid)
54
55 def delCategory(self, catid):
56 self.getBizobj().bizReccat.delLink(catid)

Puede parecer innecesario agregar tal capa. Después de todo, la lista de verificación podría haber
conseguido también fácilmente la referencia al bizobj reccat, y hacer esas llamadas a addLink() o
delLink() sin involucrar al formulario. Sin embargo, no se necesita mucho para agregar esta capa, y
ahora estamos preparados para en el futuro si deseamos para el usuario algún otro método que no sea la
lista de control, programar agregar o remover enlaces de categorías.
A continuación, define los nuevos métodos, más un método de soporte, en el objeto de negocio
CategoriesForRecipe en biz/Reccat.py:
45 def addLink(self, catid):
46 if self.hasLink(catid):
47 raise ValueError, "catid %s is already linked to this recipe." % catid
48 self.new()
49 self.Record.catid = catid
50 self.save()
51
52
53 def delLink(self, catid):
54 if not self.hasLink(catid):
55 raise ValueError, "catid %s not found, so can't delete." % catid
56 self.seek(catid, "catid") ## not technically needed
57 self.delete()
58
59
60 def hasLink(self, catid):
61 return self.seek(catid, "catid") >= 0

Note que cada método comprueba primero si el enlace existe ya o no, y luego actúa adecuadamente. Si
el resultado no es el que esperamos, levantamos un ValueError para que el programador (usted) sepa
donde está el problema y pueda encontrar la solución apropiada. El seek() en la línea 56 no es
necesario, porque el seek() en hasLink() ya tendrá colocado el puntero de registro en el registro
correspondiente, pero lo hacemos de todas maneras en caso de que cambiemos la implementación de
hasLink() en el futuro, haciendo falsa esta suposición.
También escogí guardar el nuevo registro en la línea 50. El delete() de Dabo ya se produce
inmediatamente en el trasfondo, luego podemos también hacer un save() inmediato en este contexto.
No tendría sentido para el usuario que desmarcar una categoría salve el cambio inmediatamente,
mientras que marcar una categoria requiera salvar explícitamente.
Magnífico; ahora tenemos al objeto de negocio hijo mostrándonos las categorías y guardando los
cambios de asignación de categorías en una interfaz intuitiva que no fue difícil de codificar toda. Había
otras opciones para la interfaz de las recetas: categorías distintas a dCheckList, tal como una rejilla hija
que liste solamente las categorías vinculadas, con una interfaz para seleccionar categorías desde una
suerte de selector de registros, tal como otra rejilla que muestre todas las categorías. O podríamos haber
usado una especie de interfaz de arrastrar y soltar. La dChkList pareció como la que sería más fácil de
entender y utilizar por el usuario, entonces me fui con esa. Cuando diseñe cosas para el usuario, piense
en la interfaz más sencilla para ellos, en la interfaz más sencilla que sea suficiente. Piensa en el iPhone
y no en Visual Studio.
Arreglando la página Select
La página select contiene controles generados para seleccionar en campos diferentes. Es un buen
comienzo para una aplicación CRUD sencilla, especialmente para un administrador de base de datos
manteniendo, digamos, la base de datos para una aplicación web o algo así. Sin embargo, el usuario
final probablemente la hallará un poco confusa, pues fue escrita para facilidad de implementación.
Vamos a reducir la búsqueda en campos individuales a una sola caja de búsqueda (y buscar en todos los
campos automáticamente la cadena que el usuario introduzca). Vamos a incluir también la lista
categorías que ya creamos para la página edit, de modo que el usuario pueda pulsar en algunas
categorías para filtrar los resultados de esa forma.
Aquí está la nueva PagSelectRecipes en su integridad. Ella introduce algunas dependencias sobre cosas
que no hemos agregado aun, pero leyendo a través de ella ahora, ayudará a iluminar por que vamos a
cambiar clases existentes y añadir un par de ellas.
# -*- coding: utf-8 -*-

import dabo
if __name__ == "__main__":
dabo.ui.loadUI("wx")
from dabo.dLocalize import _, n_
from PagSelectBase import PagSelectBase
from ChkSelectOption import ChkSelectOption
from CklCategories import CklCategoriesSelect

class PagSelectRecipes(PagSelectBase):
def setFrom(self, biz):
biz.setJoinClause("")
if self.chkCategories.Value:
# User has enabled category filtering. So dynamically add the
# join clause here.
biz.addJoin("reccat", "reccat.recid = recipes.id", "inner")

def setWhere(self, biz):


self.super(biz)
if self.chkCategories.Value:
# User has enabled category filtering, so build the where clause
# on the selected categories.
WhereString = ""
for catid in self.cklCategories.KeyValue:
if whereString:
whereString += "\n OR "
whereString += "reccat.catid = %s" % catid
if whereString:
biz.addWhere("(%s)" % whereString)
else:
# User chose to filter on categories, but then didn't choose any:
biz.addWhere("1=0")
if self.chkSearch.Value:
whereString = ""
for fld in ("title", "subtitle", "ingred", "proced"):
if whereString:
whereString += "\n OR "
whereString += "recipes.%s like '%%%s%%'" % (fld,
self.txtSearch.Value)
biz.addWhere("(%s)" % whereString)
def getSelectOptionsPanel(self):
"""Return the panel to contain all the select options."""

panel = dabo.ui.dPanel(self, Sizer=dabo.ui.dSizer("v"))


gsz = dabo.ui.dGridSizer(VGap=5, HGap=10)
gsz.MaxCols = 2
label = dabo.ui.dLabel(panel)
label.Caption = _("Please enter your record selection criteria:")
label.FontSize = label.FontSize + 2
label.FontBold = True
gsz.append(label, colSpan=3, alignment="center")
## Search all text fields:
self.chkSearch = ChkSelectOption(panel, Caption="Search:")
self.txtSearch = dabo.ui.dSearchBox(panel, Name="txtSearch", \
SaveRestoreValue=True)
self.chkSearch.Control = self.txtSearch
gsz.append(self.chkSearch, halign="right", valign="middle")
gsz.append(self.txtSearch, "expand")
## Categories:
self.chkCategories = ChkSelectOption(panel, Caption="Categories:")
self.cklCategories = CklCategoriesSelect(panel)
self.chkCategories.Control = self.cklCategories
dabo.ui.callAfterInterval(200, self.cklCategories.updateChoices)
dabo.ui.callAfterInterval(200, self.cklCategories.restoreValue)
gsz.append(self.chkCategories, halign="right")
gsz.append(self.cklCategories, "expand")
# Make the last column growable
gsz.setColExpand(True, 1)
panel.Sizer.append(gsz, 1, "expand")
hsz = dabo.ui.dSizer("h")
# Custom SQL checkbox:
chkCustomSQL = panel.addObject(dabo.ui.dCheckBox, Caption="Use Custom
SQL",
OnHit=self.onCustomSQL)
hsz.append(chkCustomSQL, 1)
# Requery button:
requeryButton = dabo.ui.dButton(panel, Caption=_("&Requery"),
DefaultButton=True, OnHit=self.onRequery)
hsz.append(requeryButton)
panel.Sizer.append(hsz, "expand", border=10)
return panel

if __name__ == "__main__":
from FrmRecipes import FrmRecipes
app = dabo.dApp(MainFormClass=None)
app.setup()
class TestForm(FrmRecipes):
def afterInit(self): pass
frm = TestForm(Caption="Test Of PagSelectRecipes", Testing=True)
test = frm.addObject(PagSelectRecipes)
test.createItems()
frm.Sizer.append1x(test)
frm.show()
app.start()
Básicamente, removimos casi todo el código generado y agregamos nuestro propio código que
explícitamente añade una caja de búsqueda (basada en dabo.ui.dSearchBox en lugar de un dTextBox
simple) y una levemente diferente versión de CklCategories llamada CklCategoriesSelect que no hace
ningún vínculo a datos. Sustituimos algunos métodos de la datanav.PagSelect que PagSelectRecipes
define, con el fin de añadir las necesarias adiciones de cláusulas join y where. .
Tome en cuenta que nosotros enlazamos con 'OR' la selección de categorías entre si. En otras palabras,
si se marca 'aperitivos' y 'mejicana', todas las recetas aperitivos y todos las recetas mejicana serán
seleccionadas. Yo puedo ver casos de uso para uno u otro modo, de modo que probablemente
querremos una forma de que el usuario especifique esto, como una dRadioBox o dDropDownList. Voy
a dejar esta mejora como un ejercicio para ele estudiante. Por ahora, y usted prefiere solo aperitivos
mejicanos, basta cambiar ese 'OR' a 'AND' en el método addWhere()
Referimos a un nuevo control, ChkSelectOption, el cual proporciona el comportamiento automático de
habilitar/deshabilitar el control asociado cuando la caja de comprobación es marcada o desmarcada por
usuario. Un criterio de marcado se añadirá a la cláusula where; el criterio de desmarcado se ignorará. El
estado de las cajas de comprobación y los controles asociados serán guardados en el archivo
DaboPreferences para conveniencia del usuario por la asignación de Verdadero a SaveRestoreValue.
Defina la nueva clase en ui/ChkSelectOption.py:
import dabo

class ChkSelectOption(dabo.ui.dCheckBox):
def initProperties(self):
self.Alignment = "Right"
self.SaveRestoreValue = True

def onHit(self, evt):


self.setEnabled()
def setEnabled(self):
ctrl = self.Control
if ctrl:
ctrl.Enabled = self.Value

def _getControl(self):
return getattr(self, "_control", None)
def _setControl(self, val):
self._control = val
self.setEnabled()
Control = property(_getControl, _setControl)

A continuación, hemos cambiado la definición de clase para caja de chequeo categorías. Donde antes
había una clase (CklCategories), ahora hay tres (CklCategoriesBase, CklCategoriesEdit, y
CklCategoriesSelect). CklCategoriesBase nunca es instanciada directamente – ella existe para definir
la funcionalidad común usada por sus dos subclases . Aquí está completo el nuevo listado de
CklCategories.py:

# -*- coding: utf-8 -*-

import dabo
if __name__ == "__main__":
dabo.ui.loadUI("wx")
class CklCategoriesBase(dabo.ui.dCheckList):
def initProperties(self):
self._needChoiceUpdate = True

def updateChoices(self):
self._needChoiceUpdate = False
if self.Form.Testing:
self.Choices = ("Test", "Appetizers", "Greens")
self.Keys = (1, 2, 3)
return
self.Choices, self.Keys = self.Form.getCategoryChoicesAndKeys()
def update(self):
self.super()
dabo.ui.callAfterInterval(200, self.doUpdate)
def doUpdate(self):
if self._needChoiceUpdate:
self.updateChoices()
class CklCategoriesSelect(CklCategoriesBase):
def initProperties(self):
self.super()
self.Width = 200
self.Height = 250
self.FontSize = 7
self.SaveRestoreValue = True

class CklCategoriesEdit(CklCategoriesBase):
def initProperties(self):
self.super()
self.Width = 200

def doUpdate(self):
self.super()
if self.Form.Testing:
return
bizRecipes = self.Form.getBizobj()
bizReccat = bizRecipes.bizReccat
keyvalues = []
for rownum in bizReccat.bizIterator():
keyvalues.append(bizReccat.Record.catid)
self.KeyValue = keyvalues

def onHit(self, evt):


idx = evt.EventData["index"]
idxKey = self.Keys[idx]
if idxKey in self.KeyValue:
self.Form.addCategory(idxKey)
else:
self.Form.delCategory(idxKey)

if __name__ == "__main__":
from FrmRecipes import FrmRecipes
app = dabo.dApp(MainFormClass=None)
app.setup()
class TestForm(FrmRecipes):
def afterInit(self): pass
frm = TestForm(Caption="Test Of CklCategories", Testing=True)
test = frm.addObject(CklCategories)
frm.Sizer.append(test, 1)
frm.show()
frm.update()
app.start()

Hemos encapsulado los bit de vínculo de datos dentro de CklCategoriesEdit, mientras que
CklCategoriesEdit y CklCategoriesSelect retienen el código para poblar la lista con todas las opciones
de categorías.. También hemos envuelto el ciclo update() dentro de un dabo.ui.callAfterInterval()
(lineas 23-26). Esto deja que el usuario navegue más rápido las recetas, dado que la actualización de
las categorías marcadas sólo ocurrirá una vez, después que hayan pasado 200ms. No hay necesidad de
que este código se ejecute si el usuario sólo desea navegara alrededor rápidamente. Tan pronto como él
se detenga en una receta, la categoría marcada será actualizada. ¡Esta aplicación se está haciendo más y
más amigable para el usuario!
Haga los cambios necesarios a las referencias a clase en PagEditRecipes.py, por medio de buscar y
reemplazar 'CklCategories' con 'CklCategoriesEdit'. Yo veo dos instancias de esa cadena, en las líneas
7 y 84.
He hecho algún mejoramiento a FrmRecipes.py relativo al desempeño. Removí la llamada a
requeryCategories() del afterInit() del formulario, lo coloqué dentro alguna lógica ampliada del método
getCategoryChoicesAndKeys(). Esta nueva versión guarda una versión en caché de las opciones y
claves , así que nosotros solo construimos siempre esa lista una vez, a menos que la forcemos a
reconstruirse:
41 def getCategoryChoicesAndKeys(self, forceRequery=True):
42 """Return two lists, one for all descrp values and one for all id
values."""
43 cache = getattr(self, "_cachedCategories", None)
44 if not forceRequery and cache is not None:
45 return cache
46 choices, keys = [], []
47 biz = self.getBizobj("reccats")
48 if biz.RowCount <= 0 or forceRequery:
49 self.requeryCategories()
50 for rownum in biz.bizIterator():
51 choices.append(biz.Record.descrp)
52 keys.append(biz.Record.id)
53 self._cachedCategories = (choices, keys)
54 return self._cachedCategories

Con un pequeño trabajo, la aplicación predeterminada generada por AppWizard puede convertirse
mucho mas robusta y amigable con el usuario. Aquí se muestra la página selec recipes, después de los
cambios de arriba
Imprimiendo una Receta
El Ayudante de Aplicaciones generó un formulario Informe, pero no es apropiado a las recetes del todo.
Usted puede verlo presionando en Reports|Sample Report. El menú es definido en ui/MenReports.py.
Los datos son obtenidos desde db/getSampleDataSet.py. La definición del Informe está en
reports/sampleReport.rfxml. Este es un xml, visible en cualquier editor de texto o abriéndolo con
dabo/ide/ReportDesigner.py.

Crearemos nuestro propio informe desde cero y quitaremos el acceso al informe de ejemplo, pero antes
de hacerlo, echémosle un vistazo a otra de las cosas que el Ayudante de Aplicaciones le da a usted:
Quick Reports.
Introduzca algún criterio de selección en la página Recipes Select , reconsulte, y después corra
Reports|Quick Report. Seleccione 'list format', y 'all records in the data set'. Presione 'OK'. Usted
obtendrá un listado de títulos de recetas y fechas en su visor PDF predeterminado. Este informe fue
generado desde las datos actuales, con un formato definido por las columnas actuales en la rejilla de
exploración. Cambie el ancho de las columnas en esa rejilla, y la próxima vez que usted ejecute el
informe rápido el nuevo ancho de columnas se reflejará allí. Seleccionar 'expanded format' da como
resultado una receta por página, imprimiendo los campos con el tamaño que tienen en la página edit.
Así que estos informes son bastante feos, pero son generados desde las páginas explorar y editar tal y
como existen ahora, y le dan a usted algo para empezar. Vamos a utilizar el formato expandido como un
punto de partida para la impresión de nuestras recetas. Presione en 'expanded', 'all records" y
"advanced ". Responda "sí" al diálogo. Ahora presione "OK". Usted tiene ahora en el directorio reports/
de su aplicación, un nuevo informe titulado 'datanav-recipes-expanded.rfxml. Ahora podemos editarlo
en el ReportDesigner, haciendo que se vea tan bonito como nos podamos imaginar, y después de
guardarlo, a partir de entonces la aplicación recetas va a utilizar ese informe en lugar de de crear
automáticamente el formato cada vez.

Distribuyendo su Aplicación Nativamente


Dabo no proporciona un mecanismo para construir e implementar su aplicación. No lo necesita: ya
existen varias opciones para empaquetar o agrupar, su aplicación y finalmente distribuirla o
implementarla. Todos los empaquetadores trabajan dentro de la estructura de trabajo setuptools/
distutils de python (trabajan con los scripts estándares setup.py). Los empaquetadores incluyen a los
cx_Freeze, py2app, and py2exe. El Ayudante para Aplicaciones genera un archivo setup.py que usará
uno de los anteriores empaquetadores para construir, o compilar, su aplicación en código nativo para
que corra en Linux, Macintosh, y Windows. El Ayudante para Aplicaciones también genera scripts de
ayuda en su directorio principal llamado 'buildlin', 'buildmac', y 'buildwin.bat'. Si usted está en Mac y
quiere empaquetar su aplicación para distribuirla en esa plataforma, deberá correr 'buildmac'. LA
carpeta para distribuir después de correr el constructor es el directorio 'dist'. La aplicación deberá correr
en otra computadora con un sistema operativo razonablemente de la misma cosecha como en el sistema
operativo en que usted la construyó. Así que, ejecutando 'buildwin.bat' en Vista deberá producir un
EXE que corre sobre Windows7, XP, Vista, y tal vez incluso en Windows2000. Corriendo 'buildmac'
en OS X 10.5 produciría un .app que corre sobre 10.4, 10.5, and 10.6.
Para Windows, el Ayudante incluye un mecanismo esqueleto para hacer un archivo de instalación, de
modo que el usuario puede correr un familiar setup.exe para instalar la aplicación. AppWizard genera
un archivo .iss y luego invoca InnoSetup (si está instalado) con ese archivo .iss como un argumento.

Hay varios archivos apropiados para personalización: App.py, __version__.py, setup.py, y setup.iss.txt.
El último es sólo apropiado para, sin embargo.
App.py es la definición de la clase de su aplicación, es subclase de dabo.dApp. Ella contiene algunas
declaraciones de asignación del usuario que son sacadas y utilizadas por el setup.py generado. Aquí
está como aparecen esa declaraciones, y afortunadamente es aparente que tipo de información debería
ir en cada una
7 class App(dabo.dApp): 8 def initProperties(self): 9 # Administra como son
guardadas las preferencias 10 self.BasePrefKey = "dabo.app.recipes_aw"
11
12 ## La siguiente informació puede ser usada en varios lugares en su app:
13 self.setAppInfo("appShortName", "Recipes_Aw")
14 self.setAppInfo("appName", "Recipes_Aw")
15 self.setAppInfo("copyright", "(c) 2008")
16 self.setAppInfo("companyName", "Nombre de su compañía")
17 self.setAppInfo("companyAddress1", "Dirección de su compañía")
18 self.setAppInfo("companyAddress2", "CSZ de su compañía")
19 self.setAppInfo("companyPhone", "Teléfono de su compañía")
20 self.setAppInfo("companyEmail", "Email de su compañía")
21 self.setAppInfo("companyUrl", "URL de su compañía")
22
23 self.setAppInfo("appDescription", "Describa su app.")
24
25 ## Información sobre el desarrollador del software:
26 self.setAppInfo("authorName", "Su nombre")
27 self.setAppInfo("authorEmail", "Su email")
28 self.setAppInfo("authorURL", "Su URL")
29
30 ## Asigna la appVersion y la appRevision desde __version__.py:
31 self.setAppInfo("appVersion", version["version"])
32 self.setAppInfo("appRevision", version["revision"])

dApp.setAppInfo() guarda un valor arbitrario para la instancia de la aplicación, usando la clave dada.
La única pieza de información realmente importante de cambiar aquí es el nombre de su aplicación,
dado que esa determina el nombre de su aplicación en tiempo de ejecución. Cámbiela de “Recipes_aw”
a algo como “Recipe Filer”.
La línea 31 automáticamente guarda la versión de la aplicación, para una fácil referencia en cualquier
parte en la aplicación: sencillamente llame a self.Application.getAppInfo("appVersion"). El número
real de la versión es encapsulado en el archivo __version__.py.
Realmente no hay casi nada que personalizar dentro de setup.py, porque toda la información pertinente
es tomada desde la instancia de la aplicación. Sin embargo, si su aplicación inicia descansando en otras
librerías, usted puede hallar que py2exe, py2app, o cx_Freeze necesitan algo de ayuda incluyendo
dentro de la distribución todos los archivos necesitados en la aplicación. Esto es para lo que son los
"includes", "packages", "data_files" y parecidos. La mejor cosa que hacer es buscar con google la
respuesta específica, o sólo realice varios ensayo y error hasta que obtenga un script de
empaquetamiento exitoso. Tenga en cuenta que hay paréntesis de código específicos de plataforma que
llaman a setup() y construyen la lista de opciones.
Usted querrá personalizar el icono, sin embargo. No se incluye un icono predeterminado, lo que
resultará en que sea usado un icono específico predeterminado por plataforma. Los iconos tienden a ser
bestias específicas de la plataforma y esto está más allá del ámbito del tutorial, pero para darle un
ejemplo, he incluido logo_gree.icns de mi aplicación comercial en el directorio recursos del código
fuente en linea. Este es la versión Macintosh del icono. Sencillamente descomente la declaración en la
porción Mac de setup.py. La siguiente es una captura de pantalla de nuestra Recipe Filer corriendo
sobre Mac. Observe el uso de el icono en la vista de Finder, así como también en la conmutadora de
tareas que he he habilitado con Cmd-Tab.

También podría gustarte