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í.


1 comentario :