30 de septiembre de 2015

Rotar controles con VFP 9.0

Con la propiedad Rotation de los controles Label, Shape y Line podemos hacer rotar estos controles con Visual FoxPro 9.0.

La propiedad Rotation especifica un número de grados [0..360] para rotar un control en sentido antihorario.

Existen algunas normas a seguir para especificar la rotación en estos controles.

Para los objetos Label, las fuentes deben ser TrueType, la propiedad Style no debe ser igual a 3, la propiedad Alignment solo es respetada cuando Rotation es igual a 0 (cero), la propiedad AutoSize no vuelve a dimensionar el tamaño de la etiqueta.

Para los objetos Shape y Line, la propiedad Rotation solo se aplica a las formas y lineas creadas con la propiedad PolyPoints.

Para mas detalles es recomendable leer la ayuda de la propiedad Rotation.

El siguiente ejemplo crea un formulario con 3 controles que rotan en sentido horario al hacer "click" en el botón "Rotar".



PUBLIC goMiForm
goMiForm=CREATEOBJECT("MiForm")
goMiForm.SHOW(1)
RETURN

DEFINE CLASS MiForm AS FORM
  TOP = 0
  LEFT = 0
  HEIGHT = 480
  WIDTH = 380
  AUTOCENTER = .T.
  DOCREATE = .T.
  CAPTION = "Ejemplo de Rotation"
  NAME = "MiForm"
  ADD OBJECT Shape1 AS SHAPE WITH ;
    TOP = 24, ;
    LEFT = 24, ;
    HEIGHT = 150, ;
    WIDTH = 150, ;
    BORDERWIDTH = 2, ;
    BACKCOLOR = RGB(255,255,0), ;
    POLYPOINTS = "This.aPolyPoints", ;
    ROTATION = 360, ;
    NAME = "Shape1"
  ADD OBJECT Line1 AS LINE WITH ;
    BORDERWIDTH = 2, ;
    HEIGHT = 150, ;
    LEFT = 204, ;
    TOP = 24, ;
    WIDTH = 150, ;
    BORDERCOLOR = RGB(255,0,0), ;
    POLYPOINTS = "This.aPolyPoints", ;
    ROTATION = 360, ;
    NAME = "Line1"
  ADD OBJECT Label1 AS LABEL WITH ;
    FONTNAME = "Arial Black", ;
    FONTSIZE = 20, ;
    BACKSTYLE = 0, ;
    CAPTION = "Comunidad VFP", ;
    HEIGHT = 250, ;
    LEFT = 24, ;
    TOP = 204, ;
    WIDTH = 250, ;
    FORECOLOR = RGB(0,0,255), ;
    ROTATION = 360, ;
    NAME = "Label1"
  ADD OBJECT cmdRotar AS COMMANDBUTTON WITH ;
    TOP = 400, ;
    LEFT = 264, ;
    HEIGHT = 36, ;
    WIDTH = 84, ;
    CAPTION = "Rotar", ;
    NAME = "cmdRotar"
  PROCEDURE Shape1.INIT
    WITH THIS
      .ADDPROPERTY("aPolyPoints[4,2]")
      .aPolyPoints[1,1]= 50
      .aPolyPoints[1,2]= 0
      .aPolyPoints[2,1]= 100
      .aPolyPoints[2,2]= 50
      .aPolyPoints[3,1]= 50
      .aPolyPoints[3,2]= 100
      .aPolyPoints[4,1]= 0
      .aPolyPoints[4,2]= 50
    ENDWITH
  ENDPROC
  PROCEDURE Line1.INIT
    WITH THIS
      .ADDPROPERTY("aPolyPoints[2,2]")
      .aPolyPoints[1,1]= 20
      .aPolyPoints[1,2]= 20
      .aPolyPoints[2,1]= 80
      .aPolyPoints[2,2]= 80
    ENDWITH
  ENDPROC
  PROCEDURE cmdRotar.CLICK
    *-- Rota en sentido horario
    FOR lnI = 350 TO 10 STEP -10
      STORE lnI TO ;
        THISFORM.Shape1.ROTATION, ;
        THISFORM.Line1.ROTATION, ;
        THISFORM.Label1.ROTATION
      INKEY(.05,"H")
    ENDFOR
    *-- Retorno a la pocicion inicial
    STORE 360 TO ;
      THISFORM.Shape1.ROTATION, ;
      THISFORM.Line1.ROTATION, ;
      THISFORM.Label1.ROTATION
  ENDPROC
ENDDEFINE
Luis María Guayán

28 de septiembre de 2015

Dibujando polígonos con VFP 9.0

Ahora con Visual FoxPro 9.0 podemos dibujar polígonos con un objeto Shape y la nueva propiedad PolyPoints.

La propiedad PolyPoints especifica el nombre de una matriz que contiene las coordenadas para crear formas poligonales usando el control Shape. La matriz debe estar en el alcance del objeto Shape para que el polígono sea dibujado.

Las cordenadas son de la forma (X,Y). Las coordenadas especificadas son porcentajes relativos a las dimensiones del control Shape. La coordenada (0,0) corresponde al ángulo superior izquierdo del objeto Shape, como se muestra en la gráfica. El polígono se dibujará en el orden que estas coordenadas aparezcan en la matriz.



A continuación el código de un formulario de ejemplo con dos polígonos dibujados.

PUBLIC goForm
goForm = CREATEOBJECT("MiForm")
goForm.SHOW(1)
RETURN

DEFINE CLASS MiForm AS FORM
  DOCREATE = .T.
  AUTOCENTER = .T.
  CAPTION = "Dibujando polígonos con VFP 9.0"
  NAME = "MiForm"
  *-- Triángulo
  ADD OBJECT shpTriangulo AS SHAPE WITH ;
    TOP = 36, ;
    LEFT = 24, ;
    HEIGHT = 144, ;
    WIDTH = 144, ;
    BACKCOLOR = RGB(255,0,0), ;
    POLYPOINTS = "This.aTri", ;
    NAME = "shpTriangulo"
  *-- Octógono
  ADD OBJECT shpOctogono AS SHAPE WITH ;
    TOP = 36, ;
    LEFT = 204, ;
    HEIGHT = 144, ;
    WIDTH = 144, ;
    BACKCOLOR = RGB(0,0,255), ;
    POLYPOINTS = "This.aOct", ;
    NAME = "shpOctogono"
  PROCEDURE shpTriangulo.INIT
    THIS.ADDPROPERTY("aTri[3,2]")
    THIS.aTri[1,1]= 50
    THIS.aTri[1,2]= 0
    THIS.aTri[2,1]= 100
    THIS.aTri[2,2]= 100
    THIS.aTri[3,1]= 0
    THIS.aTri[3,2]= 100
  ENDPROC
  PROCEDURE shpOctogono.INIT
    THIS.ADDPROPERTY("aOct[8,2]")
    THIS.aOct[1,1]= 33
    THIS.aOct[1,2]= 0
    THIS.aOct[2,1]= 67
    THIS.aOct[2,2]= 0
    THIS.aOct[3,1]= 100
    THIS.aOct[3,2]= 33
    THIS.aOct[4,1]= 100
    THIS.aOct[4,2]= 67
    THIS.aOct[5,1]= 67
    THIS.aOct[5,2]= 100
    THIS.aOct[6,1]= 33
    THIS.aOct[6,2]= 100
    THIS.aOct[7,1]= 0
    THIS.aOct[7,2]= 67
    THIS.aOct[8,1]= 0
    THIS.aOct[8,2]= 33
  ENDPROC
ENDDEFINE
Luis María Guayán

25 de septiembre de 2015

Su "autocompletar" en sus cuadros de edición

Articulo original: You 'Auto' Complete Your Editboxes
https://msdn.microsoft.com/en-us/library/ms947603.aspx
Autores: Andy Kramek y Marcia Akins
Traducido por: Luis María Guayán


El autocompletar en cuadros de textos, introducido en VFP 9.0, está muy bueno, pero funciona únicamente para el control TextBox. Sin embargo, hay muchísimos otros escenarios donde la característica de autotexto, agregaría un gran valor a una aplicación. Andy Kramek y Marcia Akins muestran como pueden poner en práctica una funcionalidad similar para cuadros de edición (EditBox), tanto en VFP 9.0 como en versiones anteriores de VFP.

Marcia: Sabes, realmente me gusta la nueva característica de autocompletar para los cuadros de textos en Visual FoxPro 9.0. Es una verdadera pena que ellos no lo hicieran disponible también para cuadros de edición, porque realmente podría usarlo ahora mismo en la aplicación en la que trabajo.

Andy: Me parece que deberíamos ser capaces de crear una clase EditBox que duplique la funcionalidad. Al menos, podríamos si supiera exactamente lo que quieres hacer.

Marcia: Varios formularios en esta aplicación requieren que el usuario ingrese texto libre en forma en campos de comentarios. Cuando miro los datos, veo que la idea esencial de los comentarios es la mismo, pero como estos han sido ingresados por distintos usuarios, el modo de expresarse es ligeramente diferente.

Andy: ¿Cuál es tu idea? En primer lugar ¿no es esta la idea en forma de texto libre?. Si se permite a los usuarios utilizar solamente frases estándares, debes utilizar un control distinto (una clase de lista con múltiple selección, quizás).

Marcia: Pero estos campos deben permitir la forma de texto libre porque, aunque algunos fragmentos del texto contengan frases estándares, otros fragmentos son completamente únicos. Toma por ejemplo, el campo de comentarios para un representante de servicios al cliente. Este a menudo contiene frases como "Este cliente estuvo descontentado con el servicio porque", pero la razón de que los clientes estuvieron descontentados varía bastante.

Andy: Me parece que en este caso el campo debería ser algo como "Queja del Cliente" y la necesidad del "texto estándar" desaparece. (¡lo que habría que demostrar!).

Marcia: Como de costumbre, no captas mi idea completamente. ¿No se te ocurrió que el cliente podría llamar por varios motivos?. Preguntar sobre una entrega pendiente, preguntar una carga de una factura, o hasta elogiar a un empleado por el estupendo servicio?

Andy: Ah, sí ya veo. ¿Hay otros requisitos?

Marcia: Sí. Los usuarios necesitan la capacidad de agregar "fragmentos estándares" mientras están escribiendo, pueden darse cuenta que han escrito anteriormente varias veces la misma frase y quieren que la frase este disponible para usar más adelante, ahorrando pulsaciones de teclas. Además, las selecciones necesitan ser "sensibles al contexto."

Andy: ¿Eh? ¿Qué quieres decir con "sensibles al contexto"?

Marcia: Los tipos de fragmentos usados por ejemplo en un campo de comentarios del servicio del cliente serían diferentes a los usados en el campo de los comentarios en un formulario de solicitud de inscripción.

Andy: ¡Ah! Quieres decir de hacer accesibles sólo aquellos fragmentos que son relevantes en un momento dado.

Marcia: Pensé que esto es lo que dije.

Andy: Bien, la primer cosa que necesitaremos es una tabla para almacenar estos "fragmentos". Entonces, necesitaremos alguna manera de mostrar las opciones disponibles. ¿Cuánto texto quieres almacenar?

Marcia: ¿Por qué limitar esto? Sólo utilizaremos un campo Memo.

Andy: Supongo que podríamos, pero entonces el problema sería como mostrarlo. Yo pensaba que usaríamos un menú emergente o una lista para mostrar los fragmentos. Esencialmente, esto significará que las entradas deberían ser realmente una línea.

Marcia: Ahora que lo pienso, parece que el objetivo aquí debería ser permitir que los usuarios crearan fragmentos de texto reutilizables. De este modo, en la práctica, el texto será probablemente bastante corto. Elijamos un valor arbitrario de 120 caracteres por el momento.

Andy: Parece razonable, ya que el ejemplo anterior de "Este cliente estuvo descontentado con el servicio porque" es de sólo 56 caracteres de largo. De este modo, necesitaremos un campo clave y un campo caracter.

Marcia: También necesitaremos un campo que podemos usar para filtrar las entradas, recuerda, sensible al contexto que es una exigencia.

Andy: Ah, sí, me olvidaba de esto. De esta manera, la estructura de la tabla requerida va a ser como la mostrada en la Tabla 1.

CampoTipo
itxtpkI 4
ctxttextC 120
ctxtcodeC 3

Tabla 1. Estructura para la tabla de frases estándar (stdtext.dbf)

Marcia: Bien. Luego tenemos que decidir como activar la lista y como vamos a mostrar los fragmentos.

Andy: Yo pensaba en términos de un menú emergente llamado desde el evento RightClick del EditBox.

Marcia: ¿Quieres decir que quieres mostrar los fragmentos de texto directamente en el menú emergente?

Andy: Seguro, ¿por qué no?

Marcia: Bueno, en primer lugar, ¿Cómo agregarán un fragmento a la tabla?. Otra cosa, ¿Qué pasa si el EditBox específico apunta a 30 fragmentos de texto? ¿Esto no será muy incómodo en un menú emergente?

Andy: La primera cuestión se soluciona fácilmente: Sólo agrega una opción de "Agregar Item" al menú. Sin embargo, la segunda cuestión, es más de un problema. El menú emergente podría exceder la zona disponible de la pantalla.

Marcia: Además, encontrar ítems específicos navegando en menús emergentes largos, es un sufrimiento (miren el menú emergente haciendo clic derecho en la ventana de edición de algún método y verán lo que quiero decir).

Andy: Así que, ¿dices que no te gusta la idea de un menú emergente?

Marcia: No, digo que el menú emergente debería tener solo dos opciones. Una activará un formulario que muestra los fragmentos de texto disponibles, y el otro, agregará un ítem a la lista de frases estándares.

Andy: Esto aumenta otra pregunta. ¿Cómo se supone que los usuarios agreguen un fragmento? ¿A través de otro formulario?

Marcia: Esto es demasiado incómodo. Además, cuándo ellos decidan agregar un fragmento de texto, ellos ya lo han escrito. ¿Por qué hacerlos escribir otra vez?. Sólo dejemos seleccionar el texto que ellos quieran agregar y luego seleccionar la opción de "Agregar Item" del menú emergente.

Andy: Me gusta así. Podemos incluso hacer el EditBox bastante "inteligente" de modo que el menú emergente solo se muestre cuando hay texto seleccionado en él, sino, puede ir directamente a la lista de opciones. De hecho, podemos hacerlo tan "inteligente" que presente el menú emergente sólo cuando el EditBox tenga algunos fragmentos de texto definidos para él y el usuario tiene algún texto seleccionado. En el resto de las situaciones, podemos tomar la acción apropiada sin la intervención de usuario.

Marcia: ¡Opino que quiero ver la tabla de verdad para esa declaración! (ver la Tabla 2)

 Con fragmentos definido

Sin fragmentos definido
Con texto seleccionado en el EditBoxMuestra el menú emergenteAgrega el texto seleccionado a la tabla de fragmentos
Sin texto seleccionado en el EditBoxMuestra los fragmentos disponiblesMuestra el mensaje "Nada para hacer"

Tabla 2. Manejo del menú emergente con Clic Derecho

Andy: La clase EditBox va a necesitar algunas propiedades adicionales. Asumo que querrás contar con la posibilidad de tablas diferentes para almacenar los fragmentos.

Marcia: Sí, porque el autocompletar del TextBox solo permite que defina la tabla de origen, y tratamos de duplicar su funcionalidad. También necesitaremos propiedades para almacenar la lista de campos que serán seleccionados, y la condición de filtro para aplicar. Las propiedades que tenemos que añadir son mostradas en la Tabla 3.

Nombre de propiedadDescripciónValor por defecto
cTextSourceNombre de alias para usar como fuente para el texto a mostrar.stdtext
cFieldListLista de campos para ser seleccionados por el cursor. El primer campo será mostrado; el segundo campo debería ser el campo ID para la tabla de texto.cTxtText, iTxtPK
cTextKeyClave del filtro para limitar la lista de fragmentos de texto disponibles para insertar.cTxtCode = 'ALL'

Tabla 3. Propiedades personalizadas para la clase EditBox con autotexto

Andy: ¿Y los métodos personalizados? Obviamente, necesitaremos el evento RightClick() para llamar un método que decidirá que acción debe ser tomada, esto lo llamaremos el método OnRightClick().

Marcia: También necesitaremos métodos para añadir el texto a la tabla e insertar el texto seleccionado de la tabla. Además, necesitaremos un formulario emergente para permitir que el usuario seleccione los fragmentos disponibles (ver la Tabla 4).

Nombre del métodoDescripción
OnRightClickDetermina la acción a ser tomada cuando se dispara el evento RightClick.
AddTextToTableAgrega el texto seleccionado a la tabla especificada. Requiere que la primera entrada en cFieldList sea el nombre de la columna en el cual el texto debe ser insertado.
InsertTextInserta el texto seleccionado en el Value de la propiedad en la posición actual del cursor corriente.

Tabla 4. Métodos personalizados para la clase EditBox con autotexto

Andy: Vamos al principio del código del método OnRightClick(). La primera tarea es ver si hay algún fragmento de texto ya definido para el EditBox. Básicamente, todo lo que tenemos que hacer es obtener la clave del filtro de la propiedad cTextKey, ejecutar una consulta, y establecer una bandera local según el resultado:

IF NOT EMPTY( .cTextSource )
  *** Tenemos algun dato de autotexto
  lcSql = "SELECT " + ALLTRIM( .cFieldList ) ;
    + " FROM " + .cTextSource ;
    + IIF( NOT EMPTY( .cTextKey ), ;
    + " WHERE " + ALLTRIM( .cTextKey ), "" );
    + " INTO CURSOR curText ORDER BY 1 NOFILTER"
  &lcSql
  llHasAutoText = (_TALLY > 0)
ENDIF 

Marcia: Bien. El siguiente paso debe comprobar el texto seleccionado en el EditBox y también poner una bandera para esto. Nosotros podemos leer la propiedad SelText del EditBox y deshacernos de cualquier carácter no deseado con CHRTRAN(), y luego colocar la bandera llHasSelected:

*** Tenemos algún texto seleccionado
IF .SelLength > 0
  *** Quitamos los caracteres CR, LF y TAB
  lcText = CHRTRAN( .SelText, CHR(13)+CHR(10)+CHR(9), '')
ENDIF
llHasSelected = NOT EMPTY( lcText )

Andy: Después, debemos decidir que acción tomar según lo definido en nuestra tabla de verdad (Tabla 2). No hay necesidad de mucho código para esto:

IF llHasSelected AND llHasAutoText
  *** Todas las opciones están disponibles,
  *** Muestro el menú emergente
  STORE 0 TO pnChoice
  DEFINE POPUP showtext SHORTCUT RELATIVE FROM MROW(),MCOL()
  DEFINE BAR 1 OF showtext PROMPT "Insert AutoText"
  ON SELECTION BAR 1 OF showtext pnChoice = 1
  DEFINE BAR 2 OF showtext PROMPT [\-]
  DEFINE BAR 3 OF showtext PROMPT "Add Selected Text"
  ON SELECTION BAR 3 OF showtext pnChoice = 3
  ACTIVATE POPUP showtext
ELSE
  IF NOT llHasSelected AND NOT llHasAutoText
    *** Nada seleccionado ni nada definido
    lcMsg = "There is no auto text for this field," + CRLF
    lcMsg = lcMsg + "and nothing is selected"
    MESSAGEBOX( lcMsg, 64, "There is nothing to do" )
    *** Establezco el valor de la selección como Nulo
    pnChoice = 0
  ELSE
    *** Solo una opción disponible, establezco pnChoice explícitamente
    pnChoice = IIF( llHasAutoText, 1, 3 )
  ENDIF
ENDIF

Marcia: Noto que pnChoice está establecido por el menú. Obviamente, esto significa que debe haber sido declarado como privado en la rutina que lo llama.

Andy: Sí, así es. No mostré todas las declaraciones aquí, pero ya que lo mencionas, deberíamos indicar que CRLF está definido en el código como "CHR(13)+CHR(10)".

Marcia: El paso final, se debe decidir según el valor de pnChoice:

DO CASE
  CASE pnChoice = 1
    *** Se quiere tomar del autotexto 
    *** Paso el nombre del cursor a usar en el formulario
    DO FORM frmautotext WITH 'curtext' TO lcText
    IF NOT EMPTY( lcText )
      *** Tenemos algún texto y agregamos el campo
      .InsertText( lcText )
    ENDIF
  CASE pnChoice = 3
    *** Agregamos el texto seleccionado a la lista de autotexto
    .AddTextToTable( lcText )
  OTHERWISE
    *** No hacemos nada
    lcText = ""
ENDCASE

Andy: Otra parte un poco complicada es el código que inserta el texto seleccionado en el EditBox. Esto es controlado por el método InsertText(), que recibe el texto del formulario emergente como un parámetro:

WITH This
  *** Ignoro espacios en blanco 
  tcText = ALLTRIM( tcText )
  lcText = ALLTRIM( NVL(.Value,"") )
  lnLen = LEN( lcText )
  *** Tomamos la posición del cursor
  lnPos = .SelStart
  DO CASE
    CASE lnPos <= 1
      *** Estamos al principio 
      *** agregamos un espacio después del texto
      .Value = tcText + " " + lcText
      lnNewPos = LEN( tcText ) + 1
    CASE lnPos < lnLen
      *** Estamos en el medio
      *** agregtamos espacios a ambos lados
      .Value = LEFT( lcText, lnPos ) + " " + tcText ;
        + " " + SUBSTR( lcText, lnPos + 1 )
      lnNewPos = lnPos + LEN( tcText ) + 2
    OTHERWISE
      *** Estamos el final 
      *** agregamos un espacio antes del texto
      .Value = lcText + " " + tcText
      lnNewPos = lnPos + LEN(tcText) + 1
  ENDCASE
  *** Posicionamos nuevamente el cursor
  .Refresh()
  .SelStart = lnNewPos 
ENDWITH 

Marcia: Bueno. Ahora tenemos que controlar la inserción de un fragmento de texto a la tabla. Este no es muy difícil, porque usamos un campo entero autoincremental en la tabla, y podemos tomar los nombres del texto y los Id de los campos de la propiedad cFieldList, y el nombre del campo código, de la propiedad cTextKey. De esta manera, el código trabajará sin importar la tabla fuente para los fragmentos de autotexto. Me gusta esto cuando las cosas pueden ser controladas por datos como este:

*** Inserto el texto en la tabla
lcTable = This.cTextSource
*** El campo objetivo es la primer palabra en la lista de campos
lcField = GETWORDNUM( This.cFieldlist, 1, ',' )
IF NOT EMPTY( This.cTextKey )
  *** El campo clave es la primer palabra en cTextKey
  lcField = lcField + "," + ALLTRIM( GETWORDNUM( This.cTextKey, 1, '=' ))
  *** Trato las comillas embebidas 
  *** (Primero convierto comillas al estándar)
  tcText = CHRTRAN( tcText, CHR(147)+CHR(148), CHR(39) )
  *** Quito los blancos y agrego corchetes delimitadores
  *** NOTA: Uso '[]' para evitar comillas embebidas
  tcText = "[" + RTRIM( tcText ) + "]"
  *** El valor del texto clave es la segunda palabra en cTextKey
  tcText = tcText + "," + ALLTRIM( GETWORDNUM( This.cTextKey, 2, '=' ))
ENDIF
*** Armo y ejecuto la sentencia Insert
lcSql = "INSERT INTO " + lcTable + " (" + lcField + ") VALUES (" + tcText + ")"
&lcSql 

Andy: Bien, confiamos realmente en que las propiedades son configuradas correctamente, pero esto es una cuestión de tiempo de diseño, suponenemos que que será hecho correctamente.

Marcia: ¡Por supuesto es! Tenemos que asumir que los desarrolladores harán las cosas correctamente, como tenemos que asumir que ellos probarán su código.

Andy: ¡Ahora, hay una suposición para ti! ¿Hemos terminado? ¿Podemos probarlo ahora?

Marcia: Sí, la figura 1 y la Figura 2 muestran una pequeño formulario (incluido con el archivo de descarga) que usa la clase EditBox con autotexto.

Nota: Aunque este código está diseñado y probado en VFP 8.0, correrá también en la versión 7.0 si a la tabla de origen se le quita el campo autoincremental de la clave primaria, y hasta en VFP 6.0 si las llamadas a la función GetWordNum() son sustituidos por llamadas al equivalente en la definición de la clase foxtools.fll.

Descarga: Para descargar el ejemplo haga clic aquí.


23 de septiembre de 2015

¿Cuál es el área de trabajo actual?

Por Jim Booth (Originalmente publicado en FoxTALK Marzo 1999)
Traducido por Roberto Alfredo Moré


Visual FoxPro tiene 32,767 áreas de trabajo disponibles... para cada sesión de datos. Dado que dada formulario o conjunto de formularios, barra de herramientas e informe  tiene su propia sesión de datos, el número total de áreas de trabajo disponibles es astronómico. Agreguemos a esto orientación a objetos con clases e interfaces que responden a eventos donde el desarrollador tiene muy poco control sobre qué código corre y cuándo, y usted tendrá la realización de una pesadilla. ¿Cómo puede usted tener bajo control el área de trabajo en este escenario?

Nunca dependa de cuál es el área de trabajo actual.

Ya sea si usted está diseñando una clase y está escribiendo código para uno de sus eventos o métodos, o si usted está escribiendo código en un evento o método de un objeto en un formulario, no dependa de que sea seleccionada ningún área de trabajo en particular. Siempre, explícitamente, selecciones el área de trabajo que su código necesita. Por ejemplo, si en el click de un botón de comando usted necesita mover el puntero de registros en la tabla Customer, usted debería escribir el código de esta manera,

SELECT Customer

IF NOT EOF()

  SKIP

ELSE

  GO BOTTOM

ENDIF

Este código no hace ninguna suposición sobre cuál es el área de trabajo actual. ¿Por qué es importante?. Aquí tenemos un escenario expandido. Usted diseñó este formulario para que sea el formulario de edición del cliente y los únicos controles que hay allí son algunos cuadros de texto para editar los campos de la tabla Customer.  El código del click del botón no tiene un select explícito en él porque sólo hay una tabla en el entorno de datos del formulario.

No obstante, y los usuarios son magníficos creando "no obstantes", el usuario requiere más tarde que el mismo formulario sea usado para mostrar contactos de un cliente. Usted adiciona un grid para mostrar registros de la tabla de contactos. Entonces comienza el problema. Si el usuario hace click sobre el botón desde uno de los cuadros de texto todo anda bien, pero si hacen click sobre el botón cuando el foco estaba en el grid el puntero de registros se moverá en la tabla de contactos en lugar de la tabla de clientes.

Usando la técnica de la selección explícita mostrada más arriba, este problema jamás ocurre.

Siempre vuelva las cosas atrás cuando termine.

El ejemplo de código de antes es bueno para asegurarse que el botón trabaja en el área de trabajo correcta pero, ¿qué pasa con todos los otros eventos que pueden dispararse?. ¿No necesitan también ocurrir en un entorno conocido?. Sí, lo necesitan.

Hay una simple máxima que es extremadamente importante aquí, nunca suponga nada acerca del entorno en el cual su código se ejecutará y siempre ponga el entorno en el estado en el que lo encontró cuando comenzó su código. El código de arriba podría ser expandido como el ejemplo que sigue para lograr esto.

LOCAL lcAlias

lcAlias = ALIAS()

SELECT Customer

IF NOT EOF()

  SKIP

ELSE

  GO BOTTOM

ENDIF

IF EMPTY( lcAlias )

  SELECT 0

ELSE

  SELECT ( lcAlias )

ENDIF

¿Qué trabajo adicional está haciendo este código?. Primero declara una variable local, lcAlias.  ¿Por qué local?- Porque existe siempre la posibilidad de que este código llame a otro código que también esté usando la misma variable y no queremos que esa variable sea modificada.

Luego, el código guarda el nombre del área de trabajo actualmente seleccionada en esta variable local. Al final, el código retorna el área de trabajo actual a lo que era cuando el código comenzó.

¿Qué hay acerca de otras cuestiones del entorno de datos?

Está bien, el código puede manjar el tema del área de trabajo pero, ¿qué pasa con otras cosas?. ¿Qué pasa si un código necesita asignar un índice específico o si requiere que el puntero de registros se mueva temporariamente?.

Estas cosas también pueden manejarse a través del mismo mecanismo. El código de más abajo muestra el proceso de guardar el puntero de registros, el índice, el área de trabajo y el retorno a los valores originales al final.

LOCAL lcAlias, lcOrder, lnRecno

* Guarda el área de trabajo

lcAlias = ALIAS()

SELECT Customer

* Guarda el número del índice actual

lcOrder = ORDER()

SET ORDER TO Name

* Guarda el número de registro actual

lnRecno = RECNO()

SEEK “Jones”

* Ejecuta algún proceso

* Ahora repone los valores iniciales

* Repone el número de índice para Customer

IF EMPTY( lcOrder )

  SET ORDER TO 0

ELSE

  SET ORDER TO &lcOrder

ENDIF

* Repone el puntero de registros

IF lnRecNo <> 0 AND NOT lnRecNo > RECCOUNT()

  GOTO lnRecNo

ENDIF

* Repone el área de trabajo.

IF EMPTY( lcAlias )

  SELECT 0

ELSE

  SELECT ( lcAlias )

ENDIF

¿Cuál es el punto central de esto?

Codificando cada método, evento y procedimiento de esta manera usted nunca tendrá que preocuparse por cuál es el entorno de datos actual. Cualquier fragmento de código creará el entorno que él requiere y repondrá el entorno que encontró cuando empezó. Esta técnica hará su código mucho más fácil y no será afectado por la secuencia de eventos en tiempo de corrida. Las subrutinas no dependerán de qué rutina las llamó y éstas, a su vez, no serán afectadas por lo que hagan las subrutinas al entorno de datos.

Todo esto es muy bueno pero, ¿qué tiene que ver esto con la manera en que Visual FoxPro trabaja internamente?. Visual FoxPro es un sistema que reacciona a eventos. Las cosas ocurren y el código se ejecuta en base a aquellas cosas que ocurren. Los usuarios interactúan con los controles y formularios y su interacción dispara métodos de eventos. No hay realmente ningún buen método para que un desarrollador pueda saber exactamente que entorno de datos existirá cuando se ejecuta un fragmento particular de código. Diseñando los métodos de sus eventos, sus métodos y otro código de esta manera usted evitará que el entorno de datos sea una preocupación.

Estas técnicas pueden ser extendidas a otras cuestiones como los comandos SET. La función SET() le permite a usted obtener los valores actuales de estos comandos, los que pueden ser guardados y repuestos luego de su código haya cambiado las asignaciones y terminado su trabajo.

Jim Booth

21 de septiembre de 2015

Escribir código en métodos, no en eventos; pero ¿por qué?

Artículo original: Code in Methods, not Events. But why ???
http://weblogs.foxite.com/andykramek/2005/05/30/code-in-methods-not-events-but-why
Autor: Andy Kramek
Traducido por: Ana María Bisbé York


Una de nuestras máximas favoritas es "siempre escribe el código en los métodos, no en los eventos" y yo siempre me hago exactamente la misma pregunta, ¿qué significa eso exactamente y por qué? Después de todo, VFP nos proporciona la clase CommandButton perfectamente programable, que tiene un evento Click() en el cual podemos colocar código que deseamos ejecutar al hacer Click sobre el botón. ¿Qué hay de malo en eso? La respuesta corta es que no hay nada malo en eso. Pero no es buena idea - he aquí algunas razones por las que no lo es.

Primero, y lo más obvio, es que el nombre del evento describe la situación en la cual se asocia el código a ser ejecutado, (ejemplo Click(), GotFocus(), Error(), etc. ) Sin embargo, no nos da ninguna indicación de qué es lo que ocurre cuando se dispara este evento y por lo tanto no es una vía fácil para luego examinar el código, para determinar que es lo que está mal. Considere el siguiente código que debe ser llamado cuando hace click sobre un botón en particular.

LOCAL lcSceDir, lcDstDir, lcQuoDir, lnCnt, lcToDo, loErr
WITH ThisForm
  *** Pedir las localizaciones de las carpetas
  lcSceDir = .txtSceDir.Value
  lcDstDir = .txtDestDir.Value
  lcQuoDir = .txtQuotDir.Value
  .oMig = NEWOBJECT('xMigrator', 'datamigrator.prg', null, lcSceDir, lcDstDir, lcQuoDir, .T. )
  IF VARTYPE( .oMig ) # "O"
    RETURN
  ELSE
    .UpdProgress( 'La inicialización comienza en ' + TIME())
    .oMig.oParent = ThisForm
  ENDIF
  FOR lnCnt = 1 TO ALEN( .aProcs, 1 )
    IF .aProcs[ lnCnt, 2 ]
      lcToDo = .aProcs[ lnCnt, 1 ]
      .UpdProgress( 'Start Process [' + lcToDo + '] at ' + TIME())
      .oMig.RunProcess( lcToDo )
      .UpdProgress( 'Ended Process [' + lcToDo + '] at ' + TIME())
    ENDIF
  NEXT  
  loErr = .oMig.GetErrors()
  IF loErr.nerrors > 0
     loErr.ShowErrors()
  ENDIF
ENDWITH

Tuve que quitar deliberadamente todos los comentarios de este bloque; pero es posible inferir que es algo relativo a la migración de datos. Contraste eso con el código que en realidad aparece en el evento Click de mi CommandButton

ThisForm.RunMigration()

¿Ve la diferencia? Ahora es perfectamente obvio qué es lo que ocurre cuando se hace clic sobre este botón de comandos; un método llamado "RunMigration" es llamado en su clase padre.

La segunda razón para no colocar código directamente en un evento es relativa a la primera. En el ejemplo que yo tengo, es extremadamente improbable que el método RunMigration() vaya a ser llamado desde otro lugar, excepto del botón de comandos en un formulario. Sin embargo, ¿qué hay sobre el código que agrega un registro a una tabla? ¿O el código para controlar el guardar o revertir los cambios? Este código es completamente genérico (por ejemplo, el código no depende de cuál de las tablas es la seleccionada cuando están usando comandos y funciones tipo APPEND BLANK, TABLEUPDATE() o TABLEREVERT() - asumiendo, por supuesto que es el alias correcto para hacerlo).

He aquí dos soluciones que veo generalmente en el código. La primera es que simplemente cada formulario (y frecuentemente cada página de formulario!) tenga su propio botón de comando Guardar con el código que se muestra a continuación en su evento Click()

IF NOT TABLEUPDATE( 2, .F., ‘customer’)
  MESSAGEBOX( "No se pueden guardar los cambios", 16, "Fallo en el sistema" )
ENDIF

La segunda razón es cuando se crea una clase genérica "Botón Guardar" que es creada con el siguiente código:

*** Debemos estar en el área de trabajo correcta
IF NOT EMPTY( ALIAS() )
  IF NOT TABLEUPDATE( 2, .F., ALIAS())
  MESSAGEBOX( "No se pueden guardar los cambios", 16, "Fallo en el sistema" )
  ENDIF
ENDIF

Entonces, nos tenemos que preguntar nuevamente, ¿dónde debe estar el código? Claramente, en pocos lugares lo mejor será la primera solución (escribir directamente el código en el evento Click() no es bueno). La idea de utilizar una clase es mejor; pero la cuestión en esto, como en muchas otras cuestiones en POO es ¿Dónde descansa la responsabilidad?

Para ponerlo de otra forma, ¿es realmente trabajo del botón de comandos, controlar la actualización de la tabla? Yo diría ¡ definitivamente, no! La función de un botón de comandos es informar al sistema de que el usuario quiere hacer algo - pero no significa que el botón de comando es el mejor lugar para HACER cualquier cosa que se requiera y el código para guardar es un buen ejemplo en este punto. Obviamente, el objeto que es responsable de guardar los datos del formulario, es el formulario, entonces ¿no tendría sentido guardar allí su código? Si todo esto es cierto, si se requiere entonces el código que mostré antes, vaya en un método sencillo en la clase formulario y simplemente heredar por todas los formularios basados en esta clase.

Una vez que el código está en el formulario no necesitaremos preocuparnos en pensar desde dónde será llamado. Asumiendo que el objeto que llama es el responsable de establecer el área de trabajo, podemos llamar ThisForm.SaveData() desde cualquier control, en cualquier lugar del formulario y aun saber que es lo que ocurre cuando revisemos el código 6 meses después.

La tercera razón para evadir la colocación de código directamente en un evento, a favor de colocarlo en un método, es la relativa a la naturaleza de los métodos y eventos. La diferencia fundamental entre un método en VFP y un evento, está en realidad en cómo y cuándo son llamados. Un método puede ser llamado explícitamente, en código con la adecuada referencia al objeto donde ha sido definido, así:

ThisForm.Refresh()
_Screen.ActiveForm.AddProperty( ‘junkprop’, NULL )

Este no es el caso para los eventos. Un evento se dispara como resultado de alguna acción y la llamada de un método del mismo nombre en realidad no dispara un evento. Entonces el código que ejecuta cuando se hace clic sobre el botón de comandos se puede disparar de dos formas:

Explícitamente, al llamar por código al método Click() del botón. No obstante aunque se ejecuta el código, no significa que se dispara el evento.

Implícitamente como resultado de que el usuario haga Clic sobre el botón, se dispara el evento y se ejecuta el código asociado.

Posiblemente, la vía más sencilla para ilustrar esto es con un evento que se utiliza con frecuencia, pero que pocas veces se invoca por código - KeyPress(). Arrastre un textbox base a un formulario y agregue el siguiente código a su evento KeyPress()

LPARAMETERS nKeyCode, nShiftAltCtrl
IF VARTYPE( nKeyCode ) = "N"
  WAIT "Esto fue llamado como un EVENTO" WINDOW 
ELSE
  WAIT "Esto fue llamado como un METODO" WINDOW 
ENDIF  

Ahora, añada un botón de comandos y en su evento click (si, lo se...) sólo agregue:

ThisForm.Text1.Value = "A"
ThisForm.Text1.KeyPress()

Al ejecutar el formulario verá que al oprimir la tecla "A" en el textbox se dispara el evento KeyPress() y por tanto tiene el mensaje  "EVENTO". Sin embargo, si hace clic sobre el botón, incluso agrega la letra A en el textbox (creando por supuesto el mismo resultado que si la hubiera tecleado por si mismo)-, pero el evento KeyPress() no se dispara! Es más, llamar al evento KeyPress() hace que se ejecute el código que está en el evento; pero, como no se han pasado correctamente los parámetros sólo se verá MÉTODO en la ventana del mensaje.

Por supuesto, podríamos simular que el evento pase explícitamente los parámetros correctos; pero este no es el punto. El punto es que el evento siempre se va a disparar cuando en realidad ocurra - queramos o no. He escogido deliberadamente KeyPress porque veo con mucha frecuencia código como ese en ese evento:

DO CASE
  CASE nKeyCode = 18 OR nKeyCode = 57 OR nKeyCode = 31 OR nKeyCode = 153 && Page Up
       *** Hacer algo especial y luego desactivar el procedimiento nativo
    NODEFAULT   
  CASE nKeyCode = 3 OR nKeyCode = 51 OR nKeyCode = 30 OR nKeyCode = 161  && Page Dn
       *** Hacer algo especial y luego desactivar el procedimiento nativo
    NODEFAULT   
  CASE nKeyCode = 5 && Up Arrow
       etc etc etc ...
  OTHERWISE
       *** No son teclas de navegación - dejar que lo procese
ENDCASE

A mi me parece una vía muy ineficiente de procesar las pulsaciones sobre el teclado (recuerde que este evento se va a disparar luego de cada pulsación). Probablemente DO CASE sea la peor opción para este tipo de procesos y es relativamente lento. Una solución mejor puede ser utilizar una función INLIST() que llame y redireccione la ejecución a un método si se ha pulsado una tecla que es de nuestro interés. Entonces, el método se podría reescribir de esta forma:

IF INLIST( nKeyCode, 18,57,31,153,3,51,30,161,5 )
  ThisForm.ControlaTeclas( nKeyCode )
  NODEFAULT
ENDIF

Independientemente de si es más rápido o no, a mi me hace sentir mejor, no sólo debido a que sigue el hilo del código desde un evento, sino además, porque es mucho más fácil de ver que es lo que ocurre. Está claro que algunas teclas serán interceptadas y se les pasará algún código especial. Lo que no es inmediatamente obvio es colocar una instrucción DO CASE embebida en un evento KeyPress() de cualquier control.

El beneficio adicional es, que si necesitamos una forma diferente para manipular las mismas teclas es vías diferentes, no tendremos que hacer cambio alguno en el código de nuestros controles - solamente en el método como tal. Mucho más limpio.

Como siempre, no existe una forma absolutamente "correcta" o "incorrecta" para hacer este tipo de cosas; pero yo estoy firmemente convencido de que, mantenerlo lo más sencillo posible, teniendo mi código bajo mi propio control, donde yo sepa que es lo que se hace y cuando es llamado, es la forma más sencilla. Para mi, está claro, debido a que de esta forma, es más fácil centralizar y mantener mi código. Pero... si aun encuentra alguna razón para escribir su código de la siguiente forma, es posible que sea el momento de volver a pensar ....

ThisForm.PageFrame1.Page1.Container1.OptionGroup1.Optionbutton1.Click()

¡A pasarla bien!


20 de septiembre de 2015

Cuidado con las trampas del ActiveForm

Autor: Mike Lewis
Texto original: Watch out for the ActiveForm trap
http://www.ml-consult.co.uk/foxst-08.htm
Traducido por: Ana María Bisbé York


Algunos buenos consejos a tener en mente cuando utilicemos la propiedad ActiveForm.

Probablemente conoce que _Screen.ActiveForm contiene una referencia sobre cualquier formulario que está activo en su aplicación. Puede sacar provecho de esto para escribir código genérico. Por ejemplo, si desea una forma genérica para cerrar el formulario actual, puede hacer lo siguiente:

_ Screen.ActiveForm.Release

Este código por ejemplo, se puede colocar en el procedimiento asociado con el comando Cerrar del menú Ficheros.

Sin embargo, cuando no hay ningún formulario activo, cualquier referencia a _Screen.ActiveForm va a producir un mensaje de error: “ACTIVEFORM no es un objeto” ("ACTIVEFORM is not an object"). Entonces si el usuario selecciona el comando Cerrar cuando no hay formulario activo que cerrar, ocurrirá un error.

Puede verificar esta condición utilizando la función TYPE(). De esa manera va a devolver U (de indefinido - undefined), si no existen formularios abiertos. Alternativamente, se puede verificar _SCREEN.FormCount, el que contendrá cero cuando no hay formularios activos.

En el caso del menú de comandos, un buen lugar para colocar esta verificación será en la cláusula Saltar si (Skip for) del elemento del menú (ejemplo) De esta forma el comando aparecerá en un tono grisáceo indicando que en ese momento está sin efecto. Si usa el generador de Menú, puede colocar la condición para el “Saltar si” en la ventana de texto de la opción de condición para el salto. La encontrará oprimiendo del botón de la extrema derecha.

Mike Lewis Consultants Ltd.

18 de septiembre de 2015

El Generador de informes de VFP 9.0 en acción - Parte 3

Autor: Cathy Pountney
Original en inglés: Visual FoxPro 9.0 Report Writer In Action
http://msdn.microsoft.com/library/en-us/dnfoxgen9/html/VFP9Reports2.asp
Traducido por: Ana María Bisbé York
Se aplica a: Visual FoxPro 9.0


Listener personalizado: Paginación

VFP ofrece la opción de comenzar cada grupo en una nueva página. Sin embargo, no ofrece la opción de comenzar condicionalmente un grupo de datos en una página nueva. VFP no permite tener una banda de detalles entera en la misma página cuando hay un campo alargable estirable (stretchable) involucrado. Siguiendo la misma línea, VFP no brinda la posibilidad de mantener una banda entera en la misma página cuando están implicados los campos.

Estos dos problemas pueden ser solucionados con VFP 9.0 y alguna manipulación ingeniosa. El truco para hacer este trabajo es formar objetos shape objects. La clase ReportListener tiene un método llamado AdjustObjectSize(), el cual se llama desde todas las formas. En este método se puede alterar la altura del objeto. Sabiendo eso, una forma puede ser colocada estratégicamente en el informe justo antes del conjunto de información que necesita permanecer en la misma página. Esta forma puede extenderse por programación para llenar el resto de la página cuando el conjunto de información no quepa. Al hacer que la forma llene el resto de la página, la información que necesita estar junta automáticamente queda en la siguiente página.

Otra clave para hacer que trabaje este concepto es que el informe necesita ser preprocesado para calcular cuanto espacio hace falta para cada conjunto de información que necesite colocar junta. Una vez que se ha completado el preproceso, se ejecuta el informe para el real, utilizando la información acopiadas durante el pase del preproceso.

Definición del informe

Comience por diseñar un informe como necesite, incluir todas las bandas, objetos, y variables de informe. Una vez que el informe esté complete, agregue los grupos de datos, formas, y directivas necesarias para la paginación.

Grupos de datos

El primer elemento especial para la paginación es tener grupos de datos. Envuelva el conjunto de información que necesita colocar junta en dos grupos de datos. La Figura 24 muestra un ejemplo de cómo envolver la banda de detalle en dos grupos de datos.

Figura 24. Para el control de paginación se utilizan formas y grupos.

El nivel superior de grupos de datos se basa en una expresión que se evalúa en tiempo de ejecución. Esta expresión evalúa cualquier valor que identifique un conjunto de información que tiene que estar junta, RECNO() en esta situación, o se evalúa como nada. Durante el pase pre-proceso de ejecución del informe, esta expresión evalúa a RECNO() de tal forma que cada banda se imprime en una nueva página. Esto es necesario para contar la altura de toda la banda. Durante el proceso real de este informe, el valor de esta expresión, es blanco, por tanto, esto no afecta la paginación del informe.

El Segundo grupo de datos agregados al informe se basa en el conjunto de información que necesita estar junta. Al mantener toda la banda de detalle junta en una página, utilice RECNO() como la expresión para este grupo de dato. La banda encabezado del grupo y pie de grupo creada por este segundo grupo se utiliza para sujetar (hold) los objetos forma utilizados por la clase ReportListener.

Para mantener un grupo de datos entero en una página, el significado del encabezado, detalle y pie de grupo sigue los mismos pasos que se han descrito. Agregue dos nuevos grupos sobre el grupo de datos regular, que significa un total de tres grupos de datos existentes en el informe, como se muestra en la Figura 25.

Figura 25. Utilice tres grupos de datos para mantener un conjunto de grupo de datos entero en una página.

Objetos Forma (Shape Objects)

Una vez que se han definido los grupos de datos, el siguiente paso es crear tres objetos forma (shape) utilizados como desencadenantes de la clase ReportListener. El primer objeto shape es colocado en la banda encabezado del segundo grupo de datos. El tercer objeto shape se coloca al inicio de la banda pie de página. Cree esta forma con una altura de .02 pulgadas y color (Forecolor) igual a rojo.

Durante el pase pre-proceso del informe, la posición del objeto forma en el encabezado de grupo y el objeto shape en el pie del grupo de dato se almacena en un cursor para cada conjunto de dato. El valor del objeto en el pie del grupo de datos solo necesita guardarse una vez, ya que se guarda en una propiedad.

Durante el Segundo pase del informe, el valor guardado en el cursor y la propiedad se referencian para determinar si existe suficiente espacio disponible en la página para imprimir cada nuevo conjunto de datos. Si no hay suficiente espacio, el objeto shape del encabezado de grupo es manipulado para que se apropie del resto de la página. Entonces, cuando el dato se comienza a imprimir, comienza automáticamente en la página nueva.

Ahora que se han definido los grupos de datos y que se ha agregado el objeto shape, el paso final es agregar las directivas para varios objetos de tal forma que la clase ReportListener sepa qué tiene que hacer.

Directivas

El paso final en la creación del informe es agregar las directivas al campo USER de cada objeto shape.

  • Objeto Shape en el encabezado de grupo de datos:
*:LISTENER||PAGINATION||START||1 
  • Objeto Shape en el pie de grupo de datos:
*:LISTENER||PAGINATION||END||1 
  • Objeto Shape en el pie de página:
*:LISTENER||PAGINATION||PAGEFOOTER 

Observe que los dos primeros shapes utilizan dos parámetros después del nombre de la directiva. El primer parámetro identifica qué tipo de registro de paginación es. El segundo parámetro identifica a qué conjunto (set) pertenecen estos registros para asignar este mismo valor a los registros START y END. Teóricamente, esto significa más de un set de directiva de paginación puede ser utilizada en un informe. La directiva PAGEFOOTER no necesita un segundo parámetro porque se aplica a todo el registro.

Clase ReportListener_Pagination

Creamos una nueva clase llamada MyReportListener_Pagination, y la basamos en la clase MyReportListener_Directives. Luego, agregamos algunas propiedades y algún código para varios métodos.

Propiedades

A esta clase se añaden dos propiedades nuevas:

  • lPreProcess (predeterminado .f.): Esta propiedad se utiliza como bandera para determinar si el informe se está ejecutando en el pase de pre-proceso o en el segundo pase real.
  • nPageFooterPos (predeterminado 0): Esta propiedad se utiliza para guardar la posición del pie de página, utilizado en los cálculos para determinar si existe suficiente área imprimible en la página.

Solamente son necesarias estas dos propiedades. Ahora se agregan algunos métodos.

Método BeforeReport()

Además de todo el código heredado desde la clase MyReportListener_Directive, esta clase necesita código adicional en más de un método, BeforeReport(). Es necesario para crear el cursor utilizado para guardar la posición de los objetos shape.

*-- MyReportListener_Pagination::BeforeReport()
DODEFAULT() 
IF NOT This.lPreProcess
  RETURN
ENDIF 
LOCAL lnSession, lcAlias 
*-- Se asegura de que esté la sesión de datos correcta
lcAlias = ALIAS()
lnSession = SET('DATASESSION')
SET DATASESSION TO This.CurrentDataSession 
*-- Generar el cursor temporal
CREATE CURSOR tmpPagination ;
  (nFRXRecNo I, nCounter I, cType C(1), nSet I, nPage I, nPos I)
INDEX ON BINTOC(nFRXRecNo) + BINTOC(nCounter) TAG FRXCounter
INDEX ON cType + BINTOC(nSet) + BINTOC(nCounter) TAG TypeSetCtr ADDITIVE 
*-- Restablece la sesión de datos
SET DATASESSION TO lnSession
SELECT (lcAlias) 

Clase directiva Pagination

Creamos una nueva clase llamada MyDirectives_Pagination, y la basamos en la clase MyDirectives. Luego, agregamos algunas propiedades y algún código para varios métodos.

Propiedades

A esta clase se añaden tres propiedades nuevas:

  • cPaginationType (predeterminado ""): Esta propiedad se utiliza para identificar qué tipo de registro de asignación es.
  • lAddHeight (predeterminado.f.): Esta propiedad determina si la altura del objeto shape se añade a la posición de inicio (starting position) cuando se guarda en el cursor.
  • nCounter (predeterminado 0): Esta propiedad es un contador que identifica únicamente cada conjunto (set) de datos que es impreso. Por ejemplo, cuando la banda de detalle se estira con las formas especiales Start y End shapes, esto cuenta cada banda de detalle. Cuando un grupo de datos será estirado, esto representa una cantidad de grupos de datos procesados.

16 de septiembre de 2015

El Generador de informes de VFP 9.0 en acción - Parte 2

Autor: Cathy Pountney
Original en inglés: Visual FoxPro 9.0 Report Writer In Action
http://msdn.microsoft.com/library/en-us/dnfoxgen9/html/VFP9Reports2.asp
Traducido por: Ana María Bisbé York
Se aplica a: Visual FoxPro 9.0


Clase ReportListener abstracta

Creamos una clase nueva llamada basada MyReportListener en la clase base de VFP 90 ReportListener. Esta clase va a contener algunos métodos que son necesarios en algunas clases ReportListener que se desarrollaran en el futuro. Por consiguiente, es mejor crear una clase abstracta tal que todas las clases ReportListener restantes sean subclases de ella. Será necesario agregar código en los métodos Init(), BeforeBand(), y crear un nuevo método GetFRXRecord().

Método Init()

En el método Init() de la clase abstracta ReportListener, aprovechamos dos clases que encontramos en la carpeta HOME() + 'FFC'. Estas clases se utilizarán posteriormente y ahorrarán mucho tiempo y código.

*-- MyReportListener::Init()
DODEFAULT()
*-- Crear un objeto gráfico para usar después
This.AddProperty('ogpGraphics', ;
NEWOBJECT('gpGraphics', HOME() + 'FFC&#092;_GDIPlus')) 
*-- Crear un objeto FRXCursor para usar después
This.AddProperty('oFRXCursor', ;
NEWOBJECT('frxCursor', HOME() + 'FFC&#092;_FRXCursor'))

En el método BeforeBand(), agregar código para establecer el controlador gráfico utilizado en la llamada al GDI+.

*-- MyReportListener::BeforeBand()
LPARAMETERS nBandObjCode, nFRXRecno 
DODEFAULT(nBandObjCode, nFRXRecNo) 
* Debido a que el controlador GDI+ cambia en cada página,
* es necesario configurar el controlador para nuestro objeto GPGraphics.
This.ogpGraphics.SetHandle(This.GDIPlusGraphics) 

Luego, crear un método nuevo.

Método GetFRXRecord()

Existen varias ocasiones donde es necesario referenciar el registro FRX del objeto a imprimir. Creamos un método nuevo, llamado GetFRXRecord(), que pase a la sesión de datos apropiada, ponga los datos del registro FRX en un objeto, restablezca la sesión de datos, y luego, devuelva el objeto que hace la llamada (the caller)

*-- MyReportListener::GetFRXRecord()
LPARAMETERS pnFRXRecNo
LOCAL lnSession, loFRX 
*-- Establecer la nueva sesión de datos - el FRX
lnSession = SET("Datasession")
SET DATASESSION TO This.FRXDataSession 
*-- Ir al registro
GOTO pnFRXRecNo 
*-- Tomar los datos
SCATTER MEMO NAME loFRX 
*-- Restablecer la sesión de datos
SET DATASESSION TO lnSession 
*-- Devolver el dato
RETURN loFRX 

Clase ReportListener_Directives

Creamos una clase nueva, llamada MyReportListener_Directives, basada en la clase abstracta MyReportListener. Luego, agregamos algunas propiedades nuevas y código a algunos de sus métodos.

Propiedades

Agregamos siete propiedades nuevas a la clase:

  • aFRXRecords[1]: Este arreglo unidimensional puede ser eventualmente redimensionado con la misma cantidad de números de filas que coinciden con el total de números de registro en la tabla FRX. Para cada registro con directivas, se crea una colección (collection) en el registro que guarda la referencia a cada clase directiva que necesita.
  • cDelimiter (predeterminado ||): Esta propiedad es utilizada para identificar el carácter delimitador utilizado en la directiva del campo de un objeto de informe. El delimitador puede cambiarse con facilidad, al agregar este dato a una propiedad en lugar de escribir código duro.
    Precaución: No utilice el delimitador que sea común a los comandos y expresiones de VFP. Por ejemplo, no utilice la coma.
  • lRender (predeterminado .t.): Esta propiedad puede ser utilizada, en caso de ser necesario, para suprimir la generación actual del objeto.
  • nAdjustHeight (predeterminado 0): Esta propiedad puede ser utilizada para manipular la altura de un objeto durante el método Render().
  • nAdjustLeft (predeterminado 0): Esta propiedad puede ser utilizada para manipular el extremo izquierdo de un objeto durante el método Render().
  • nAdjustTop (predeterminado 0): Esta propiedad puede ser utilizada para manipular el extremo superior durante el método Render().
  • nAdjustWidth (predeterminado 0): Esta propiedad puede ser utilizada para manipular el ancho de un objeto durante el método Render().

Ahora que están definidas las propiedades, creamos algunos métodos nuevos y agregamos código a algunos métodos existentes.

14 de septiembre de 2015

El Generador de informes de VFP 9.0 en acción - Parte 1

Autor: Cathy Pountney
Original en inglés: Visual FoxPro 9.0 Report Writer In Action
http://msdn.microsoft.com/library/en-us/dnfoxgen9/html/VFP9Reports2.asp
Traducido por: Ana María Bisbé York
Se aplica a: Visual FoxPro 9.0


Resumen

Como respuesta a la retroalimentación hecha por los usuarios, Microsoft ha mejorado significativamente el Generador de informes en VFP 9.0. Una de las mejoras más importantes es la extensibilidad, la que proporciona ganchos al Diseñador de informes y control sobre los mecanismos de salida: presentación preliminar e impresión. Este articulo muestra como entrar en estos ganchos y utilizarlos de la forma más ventajosa. Mi objetivo es mostrar un grupo de cosas ingeniosas que se pueden hacer y darle que pensar sobre rutas que puede explorar incorporando sus propias ideas.

Otra característica muy importante incorporada al producto es la capacidad de crear múltiples bandas de detalle de forma nativa. Este artículo explica algunos ejemplos de cómo utilizar informes con las múltiples bandas de detalle. Algunos son muy sencillos y nos ayudan a solucionar situaciones comunes que eran muy difíciles antes del Generador de informes de VFP 9.0

Contenido

  • Introducción
  • Múltiples Bandas de detalle
  • Sobre extensibilidad
  • Utilizar un Generador de informe
  • Utilizar Listeners
  • Utilizar Contenedor preliminar
  • Conclusión
  • Biografía

Introducción

Un prerrequisito para este artículo es haber leído previamente el artículo Lo nuevo del Generador de informes de VFP 9.0 (What's New in the Visual FoxPro 9.0 Report Writer), que se encuentra disponible en el sitio Web MSDN. Este artículo trata de las nuevas características de VFP 9.0 y brinda las instrucciones básicas para utilizarlas. Este artículo retoma lo que dejó aquel. Este artículo toma los aspectos específicos para utilizar esas nuevas características y avanzar creando informes que solucionen problemas de la vida real.

Primero, aprenderá algunas cosas sobre múltiples bandas de detalle. Luego, aprenderá sobre las nuevas características de extensibilidad en las secciones Utilizar Listeners y Utilizar Contenedor preliminar. Estas secciones muestran cómo crear informes que solucionen situaciones específicas como son forzar paginación y crear Tabla de contenidos al vuelo.

Utilizar múltiples bandas de detalle

Contar con la posibilidad de crear múltiples bandas de detalle es una de las mejoras más importantes y más solicitadas, así como las mejoras en extensibilidad en el Generador de informes de VFP 9.0. Las nuevas características permiten procesar múltiples tablas hijas para cada registro de la tabla padre. Existen posibilidades ilimitadas de lo que se puede hacer con esta nueva característica.

Informes sencillos

Un informe sencillo con múltiples bandas de detalle consiste en una tabla padre que conduce el informe y dos o más tablas hijas que están relacionadas con la tabla padre. El ejemplo mostrado en la Figura 1 es un ejemplo de informe sencillo con múltiples bandas de detalle.

Figura 1. Este ejemplo de informe con múltiples bandas de detalle tiene tres bandas separadas para cada usuario.

La tabla Customer es la tabla padre y contiene un registro para cada cliente de la compañía de seguros. Las tablas Members, Vehicles y Homes son las tablas hijas de la tabla Customer. La tabla Members guarda un registro para cada miembro de la familia del cliente. La tabla Vehicles guarda un registro para cada vehículo del cliente. La tabla Homes guarda un registro para cada vivienda del cliente. Cada tabla hija tiene relación con la tabla padre.

En este ejemplo, la tabla Customer es la tabla conductora. Si utiliza el Entorno de datos del informe para definir las tablas, establezca la propiedad InitialSelectedAlias igual a Customer. Si utiliza código para definir las tablas, asegúrese de que la tabla Customer está en el área de trabajo actual en el momento de ejecutar el informe.

Target alias – Alias destino - es el término utilizado para describir qué tabla es la tabla conductora para una banda de detalle en particular. En este ejemplo, la tabla Members es target alias para la banda de detalle 1, la tabla Vehicles es target alias para la banda de detalle 2, y la tabla Homes es target alias para la banda de detalle 3. Utilice el cuadro de diálogo Propiedades (Properties) de la correspondiente banda de detalle para establecer el target alias.

Cálculos

El ejemplo anterior mostró como imprimir datos de tres tablas hijas diferentes en un mismo informe. Sin embargo un informe con múltiples bandas de detalle no necesariamente necesita tener múltiples tablas hijas. La misma tabla hija puede ser utilizada en más de una banda de detalle.

Totales de grupos

Antes de VFP 9.0, era muy difícil imprimir subtotales en la banda encabezado de grupo (mostrado en la Figura 2). El dato tenía que ser procesado previamente para calcular los totales antes de ejecutar el informe. Con VFP9.0, no se requiere procesamiento previo.

Figura 2. Utilice un informe con múltiples bandas de detalle para imprimir subtotales de grupos al inicio del grupo.

Para crear el informe mostrado en la Figura 2, siga los siguientes pasos:

  1. Definir entorno de datos
    1. Agregar tabla Customer
    2. Agregar tabla Vehicles
    3. Establecer la propiedad InitialSelectedAlias a la tabla Customer.
  2. Crear un grupo de datos
    1. Hacer Expresiones de Agrupación (Data Group expresión) igual a Customer PK
    2. No coloque ningún objeto en la banda encabezado de grupo ni en banda pie de grupo.
  3. Crear bandas múltiples de detalle.
    1. Seleccione Bandas opcionales… (Optional Bands...) desde el menú Informe (Report)
    2. Haga Clic en el botón Agregar (Add) para agregar una o más bandas de detalle al informe.
    3. Haga Clic en Aceptar (OK) para salir del cuadro de diálogo.
  4. Definir una banda de detalle
    1. Haga doble clic en la barra gris de la banda de detalle 1 para invocar al cuadro de diálogo Propiedades (Properties)
    2. Establezca target alias igual a "Vehicles", recordando utilizar comillas.
    3. Marque la casilla de verificación Bandas de encabezado y pie asociadas (Associated header and footer bands)
    4. No coloque objetos en la banda de detalle 1
  5. Definir la banda pie para la banda de detalle 1
    1. Agregue a la banda los objetos customer name y address.
    2. Agregue a la banda los objetos total vehicles y total premiums.
    3. Agregue un objeto campo para el total de vehículos, establezca la expresión (expression) igual a vehicles.premium, establezca Tipo de cálculo (Calculation type) igual a Recuento (Count) y Basado en (Reset based on) igual a Detail1.
    4. Agregue un objeto campo para el total de premios, establezca la expresión (expression) igual a vehicles.premium, establezca Tipo de cálculo (Calculation type) igual a Suma (Sum) y Basado en (Reset based on) igual a Detail1.
  6. Definir la banda de detalle 2
    1. Hacer doble clic en la banda gris de la banda de detalle 2 para invocar el cuadro de diálogo Propiedades (Properties)
    2. Establezca target alias igual a "Vehicles", recordando utilizar comillas.
    3. Marque la casilla de verificación Bandas de encabezado y pie asociadas (Associated header and footer bands)
    4. Agregue cualquier otro objeto necesario en la banda de detalle 2.

La definición de informe descrita anteriormente, dice al Generador de informes de VFP 9.0, que procese dos veces la tabla Vehicles para cada cliente en la tabla Customer. La primera vez, calcula el total de registros y el monto en dólares para el cliente (customer) y lo imprime. El segundo pase de la tabla Vehicles imprime el detalle. Este proceso se repite para cada customer en la tabla Customer.

Porcentajes

Otro concepto en los informes es mostrar el porcentaje del total de cada línea de detalle, en la medida en que se imprimen las líneas. Esto puede ser realizado también con múltiples bandas de detalle como muestra la Figura 3.

Figura 3. Utilice un informe con múltiples bandas de detalle para imprimir porcentajes con cada línea de detalle.

Para crear el informe mostrado en la Figura 2, siga los siguientes pasos:

  1. Definir entorno de datos
    1. Agregar tabla Customer
    2. Agregar tabla Vehicles
    3. Establecer la propiedad InitialSelectedAlias a la tabla Customer.
  2. Crear un grupo de datos
    1. Hacer Expresiones de Agrupación (Data Group expresión) igual a Customer PK
    2. Coloque los objetos name y adress en la banda encabezado de grupo.
  3. Crear bandas múltiples de detalle.
    1. Seleccione Bandas opcionales… (Optional Bands...) desde el menú Informe (Report)
    2. Haga Clic en el botón Agregar (Add) para agregar una o más bandas de detalle al informe.
    3. Haga Clic en Aceptar (OK) para salir del cuadro de diálogo.
  4. Definir banda de detalle 1
    1. Haga doble clic en la barra gris de la banda de detalle 1 para invocar al cuadro de diálogo Propiedades (Properties)
    2. Establezca target alias igual a "Vehicles", recordando utilizar comillas.
    3. Marque la casilla de verificación Bandas de encabezado y pie asociadas (Associated header and footer bands)
    4. No coloque objetos en la banda de detalle 1
  5. Crear algunas variables de informe
    1. Cree una variable llamada rnTotalPremium, establezca Valor a almacenar (Value to store) igual a Vehicles.premium establezca Tipo de cálculo (Calculation type) igual a Suma (Sum) y Reiniciar basado en (Reset based on) igual a Detail 1
    2. Cree una variable llamada rnPercent, establezca Valor a almacenar (Value to store) igual a ROUND(100 * vehicles.premium / rnTotalPremium, 2) establezca Tipo de cálculo (Calculation type) igual a Ninguno (None)
  6. Definir la banda de detalle 2
    1. Hacer doble clic en la banda gris de la banda de detalle 2 para invocar el cuadro de diálogo Propiedades (Properties)
    2. Establezca target alias igual a "Vehicles", recordando utilizar comillas.
    3. Marque la casilla de verificación Bandas de encabezado y pie asociadas (Associated header and footer bands)
    4. Agregue cualquier otro objeto necesario en la banda de detalle 2.
    5. Agregue el objeto percent a la banda de detalle 2 con la expresión rnPercent.
  7. Definir la banda pie para la banda de detalle 2
    1. Agregar el objeto total premium, establecer la expresión (expression) igual a vehicles.premium, establezca Tipo de cálculo (Calculation type) igual a Suma (Sum) y Reiniciar basado en (Reset based on) igual a Detail 2
    2. Agregar el objeto total percent, establecer la expresión (expression) igual a rnPercent, establezca Tipo de cálculo (Calculation type) igual a Suma (Sum) y Reiniciar basado en (Reset based on) igual a Detail 2