25 de septiembre de 2002

Convertir Números a Letras

Usuarios de Visual Foxpro®: Espero que les sea util esta rutina, convierte números hasta 999,999,999,999.99 y con gusto la comparto con Uds.
Function Convnum(Total)
  * Autor: Anselmo Antonio Ortiz Alcocer
  * Corrreo: ortizanst@hotmail.com
  * 26/06/2001
  Dimension aUnidades(9), aDecenas(14), aCentenas(10)
  aUnidades(1) = 'UN'
  aUnidades(2) = 'DOS'
  aUnidades(3) = 'TRES'
  aUnidades(4) = 'CUATRO'
  aUnidades(5) = 'CINCO'
  aUnidades(6) = 'SEIS'
  aUnidades(7) = 'SIETE'
  aUnidades(8) = 'OCHO'
  aUnidades(9) = 'NUEVE'
  aDecenas(1) = 'DIEZ'
  aDecenas(2) = 'ONCE'
  aDecenas(3) = 'DOCE'
  aDecenas(4) = 'TRECE'
  aDecenas(5) = 'CATORCE'
  aDecenas(6) = 'QUINCE'
  aDecenas(7) = 'VEINTE'
  aDecenas(8) = 'TREINTA'
  aDecenas(9) = 'CUARENTA'
  aDecenas(10) = 'CINCUENTA'
  aDecenas(11) = 'SESENTA'
  aDecenas(12) = 'SETENTA'
  aDecenas(13) = 'OCHENTA'
  aDecenas(14) = 'NOVENTA'
  aCentenas(1) = 'CIEN'
  aCentenas(2) = 'DOSCIENTOS'
  aCentenas(3) = 'TRESCIENTOS'
  aCentenas(4) = 'CUATROCIENTOS'
  aCentenas(5) = 'QUINIENTOS'
  aCentenas(6) = 'SEISCIENTOS'
  aCentenas(7) = 'SETECIENTOS'
  aCentenas(8) = 'OCHOCIENTOS'
  aCentenas(9) = 'NOVECIENTOS'

  vTotal = str(int(Total), 12)

  Do case
    Case empty(val(vTotal))
      Texto = 'CERO PESOS'

    Case val(vTotal) = 1
      Texto = 'UN PESO'

    Otherwise
      tCientos     = obt_cant(substr(vTotal,10,3))
      tMiles       = obt_cant(substr(vTotal,7,3))
      tMillones    = obt_cant(substr(vTotal,4,3))
      tMilMillones = obt_cant(substr(vTotal,1,3))

      tCientos = tCientos
      tMiles = iif(empty(tMiles), '', ;
               iif(tMiles='UN', '', tMiles + ' ') + 'MIL ')
      tMillones = iif(empty(tMillones), '', ;
               tMillones + ' MILLON' + iif(tMillones='UN', ' ', 'ES ') +;
               iif(empty(tMiles + tCientos), 'DE', ''))
      tMilMillones = iif(empty(tMilMillones), '', ;
               iif(tMilMillones='UN', '', tMilMillones + ' ') + 'MIL ' +; 
               iif(empty(tMillones), 'MILLONES ', ' ') +;
               iif(empty(tMillones + tMiles + tCientos), 'DE', ''))

      Texto = strtran(tMilMillones + tMillones + tMiles + tCientos, '  ', ' ') + ' PESOS'
  Endcase
  
Return Texto + iif(!empty(Total), ' CON ' + ;
   strtran(transform(int((total - int(total)) * ;
   100), '**'), '*', '0') + '/100 M.N.', '')

Function obt_cant(valor)
  Public Unidades, Decenas, Centenas

  If empty(val(valor))
    Return ''
  Endif

  Store '' to tUnidades, tDecenas, tCentenas
  Unidades = int(val(substr(valor,3,1)))  &&          123
  Decenas  = int(val(substr(valor,2,1)))  && vTotal = 999
  Centenas = int(val(substr(valor,1,1)))  &&          ^^^
  valor = int(val(valor))

  tUnidades = iif(!empty(unidades), aUnidades(Unidades), '')

  If !empty(decenas)
    If decenas = 1
      tDecenas = iif(val(right(str(valor,3),2)) >= 10 and ;
      val(right(str(valor,3),2)) <= 15, aDecenas(val(right(str(valor,3),2)) - 9), 'DIECI' + tUnidades)
      tUnidades = ''
    Else
      tDecenas = aDecenas(decenas + 5)
      if !empty(unidades)
        tDecenas = left(tDecenas, len(tDecenas) - 1) + 'I'
      Endif
    Endif
  Endif

  If !empty(centenas)
    tCentenas = aCentenas(centenas)
    If valor > 100
      If centenas = 1
        tCentenas = tCentenas + 'TO '
      Else
        tCentenas = tCentenas + ' '
      Endif
    Endif
  Endif
  
Return tCentenas + tDecenas + tUnidades
Anselmo Antonio Ortiz Alcocer

23 de septiembre de 2002

Problemas con tu OCX ?

¿Tienes problemas con las OCX's que funcionan perfectamente en tu máquina y cuando haces el ejecutable, pero que no funcionan en el equipo del usuario final?

El problema es que las ocx están diseñadas para que ellas mismas se registren durante el evento constructor. Se llama a una función que tienen todas las ocx llamada DLLRegisterServer. El problema está en que Power Builder no llama a esta función. Incluso si ejecutas la utilidad que viene con windows REGSRV, la ocx falla al registrarse a si misma. Para corregir este problema, en el objeto donde tu estás usando la ocx, en el evento constructor llama a la funcion DLLRegisterServer.

Declara una función externa local en el objeto:
Function long DllRegisterServer()Library "ocxname.OCX"

y en el evento Constructor:
LONG ll_RC ll_RC = DllRegisterServer()

Ramón Rodríguez Martínez

17 de septiembre de 2002

Búsquedas sencillas

Autor: Les Pinter

De cúantas maneras quieren tus usuarios hacer consultas? Probablemente demasiadas.

No te cuento cuántas veces mis usuarios me han dicho "quiero poder buscar lo que sea." Pero cuando les digo que esa posibilidad no viene incluido en el lenguaje, sino que va a costar dinero, y que dependiendo de como lo haga puede ser increiblemente lento, usualmente piden otra cosa - algo rápido y barato.

Afortunadamente hay por lo menos dos maneras de añadir un grupo de consultas a cualquier pantalla. Se hace en unos cuantos minutos, y dado el precio, la mayoría de los usuarios descubrirán que hace todo lo que quieren - o al menos, todo lo que están dispuestos a pagar. Lo cual es precisamente lo que queríamos saber en primer término...

1 - Usa un marco de página con una Listbox cuyas columnas pueden ser ordenadas en la última página.

Este truco se aprovecha del hecho de que en el caso de las ListBox con RowSourceType = 2 (Alias), el puntero de la tabla se mueve a medida que el usuario cambia el registro seleccionado en la ListBox. (RowSourceType = 2 quiere decir usa los primeros n campos de la tabla como fuente de datos para las columnas del control, mientras que RowSourceType = 6 (Campos) significa que vas a especificar los nombres y el orden de los campos que aparecen en las columnas del control. Al pasar el usuario por los registros mostrados en el control, el puntero de la tabla o el cursor se mueve correspondientemente. Así que el puntero de la tabla siempre está de acuerdo con el muestreo. Pero, me preguntas, esto ¿cómo me ayuda con las búsquedas?

Consideremos qué debe lograr una consulta. Se usa una consulta para localizar algo rápidamente. Tradicionalmente, usando SQL, hacemos una consulta que le muestra al usuario todo lo que se asemeja a lo que busca, y le dejamos escoger el registro exacto de esa selección reducida. Pero esto implica una TextBox para permitir que escriban lo que quieren buscar, lo cual puede contener errores ortográficos; y ¿buscamos mayúsculas o minúsculas? ¿Hay cupo en la pantalla para entrar la clave? ¿Donde pondremos la lista de candidatos a seleccionar? Y ¿qué haremos con los registros que no hacen juego con lo que entró el usuario pero sí son lo que está buscando?

En vez de hacer todo esto, aprovechemos el hecho de que una tabla FoxPro con todos sus registros ya está disponible. Sencillamente vamos a mostrar una grid con las columnas más importantes que le permitirá al usuario seleccionar visualmente, cambiando el orden de una u otra columna hasta ver lo que estaba buscando. Y lo mejor es que se puede hacer esto con sólo unas cuantas líneas de código.

Como siempre, FoxPro proporciona un mecanismo muy sencillo para hacer esto. Primero, añadimos un método llamado ColumnSort al formulario. Si usas una clase base para todos tus formularios, práctica que recomiendo sin reservaciones, añade esta clase a la clase de formulario que usas como plantilla. La sentencia PARAMETER debe ser la primer línea de código en el método:
* ColumnSort method
PARAMETER IndexTagName
WITH THISFORM
SELECT (.DataEnvironment.InitialSelectedAlias)
SET ORDER TO ( IndexTagName )
.LockScreen   = .T.
.PageFrame1.Pages[.PageCount].MyList1.Refresh
.LockScreen   = .F.
ENDWITH
Yo siempre escribo el nombre de la tabla o alias principal de cada formulario en la propiedad InitialSelectedAlias del Entorno de Datos del mismo. Esto me permite escribir código genérico para habilitar métodos como BorrarRegistro, ProximoRegistro, etc.

Luego, como lo implica el códogo, arriba, añade un marco de página (PageFrame) al formulario y pon una ListBox en la última página. Como RowSourceType usa 6, y como RowSource el nombre de la tabla principal, más los nombres de las columnas que aparecen en la tabla, e.g. "CLIENTES.Nombre,Telefono,Ciudad,Estado,Contact", usando los nombres que aparecen en los encabezados de las columnas de la ListBox. Se usa la propiedad ColumnCount de la ListBox para precisar el número de columnas, y el número de píxeles de anchura de cada columna, separadas con comas en la propiedad ColumnWidth. Se suele usar Max(anchura del valor más ancho que aparecerá en la columna,anchura del título que aparece en el encabezado) como la anchura para cada columna.



Después de poner una etiqueta encima de cada columna, usa lo que sigue en el evento Click de cada una de las etiquetas:
THISFORM.SetIndex ( THIS.Caption )
El método SetIndex cambia el orden de la tabla y refresca el contenido que aparece en la lista. (Si tu índice no tiene el mismo nombre que la etiqueta, sigue leyendo - no es difícil manejar tales casos.)
PROCEDURE SetIndex
PARAMETERS   IdxName
WITH THISFORM
SELECT (.DataEnvironment.InitialSelectedAlias )
SET ORDER TO ( IdxName )
.PageFrame1.Page2.MyList1.Requery
ENDWITH
Finalmente, esto es el código DblClick de la ListBox:
PROCEDURE mylist1.DblClick
WITH THISFORM
.LockScreen   = .T.
.PageFrame1.ActivePage   = 1
.PageFrame1.Page1.Refresh
.LockScreen   = .F.
ENDWITH
Esto activa la primera página, la cual muestra el contenido de los demás campos del registro seleccionado mediante el puntero de la ListBox.

Como indicamos, esto asume que se usan TAGS de índice cuyos nombres son exactamente los mismos que las capciones de las etiquetas que sirven de encabezados para la ListBox en la última página. Si prefieres usar nombres de índices que no se pueden usar como TAGS de índice (por ejemplo, capciones que contienen espacios), el método SetIndex debe incluir una forma de usar la capción para encontrar el nombre del TAG, a saber:
SET ORDER TO TAG ( ;
IIF(IdxName=[Nombre del Cliente],[Nombre], ;
IIF(IdxName=[Apellido del Cliente],[Apellido] , IdxName)))

Un Doble-Click en la lista nos lleva a la primera página:



Qué pasa si hay muchísimos registros?

Desafortunadamente, este método es demasiado lento cuando se usa con tablas grandes en una máquina con memoria limitada. En una P266 con 32MB de memoria, trabaja bien hasta llegar a los 10,000 registros. Pero si a FoxPro le falta memoria para guardar el índice actual en la memoria, se vuelve lenta la búsqueda - cosa que los usuarios acostumbrados a la rapidez de FoxPro no toleran muy bien. Por esto, para efectuar búsquedas de tablas con muchos registros, es mejor crear una pantalla de búsqueda separada.

2 - Usa una pantalla de consulta para retornar la clave deseada

Para lograr esto, añade al forulario un botón de comando que a su vez abre otro formulario que contiene una ListBox para mostrar los resultados de la consulta. Pon una combobox y una textbox al fondo de la pantalla, para permitirle al usuario precisar las características que quiere usar para localicar registros. El combobox especifica el nombre del campo a usar, y el textbox el valor a buscar en el campo especificado.

El evento Valid del textbox es entonces responsable por cargar todos los registros que contienen el valor especificado en el campo seleccionado. El usuario usa una DobleClick en uno de los registros que aparecen en la lista para seleccionar un valor y retornar su código. El código del botón de consulta del formulario que abrió la pantalla de consulta entonces usa el valor retornado para encontrar y mostrar el registro seleccionado.



Dentro del formulario de consulta, el código Valid del combobox asegura el formateo correcto de la clave entrada por el usuario:
THISFORM.ActiveTag = THIS.Value
WITH THISFORM.MyText1
DO CASE
  CASE THIS.Value = "Name"
     .Format = "K"
     .InputMask = "!!!!!!!!!!"
     THISFORM.KeyField = [UPPER(LastName)]
     THISFORM.OrderField = [LastName]
  CASE THIS.Value = "Company"
     .Format = "K"
     .InputMask = "!!!!!!!!!!"
     THISFORM.KeyField = [UPPER(Company)]
     THISFORM.OrderField = [Company]
  CASE THIS.Value = "Zip"
     .Format = "K"
     .InputMask = "#####"
     THISFORM.KeyField = [Zip]
     THISFORM.OrderField = [Zip]
  CASE THIS.Value = "Phone"
     .Format = "K"
     .InputMask = "(###)########"
     THISFORM.KeyField = [Phone]
     THISFORM.OrderField = [Phone]
  CASE THIS.Value = "CustNo"
     .Format = "K"
     .InputMask = "#####"
     THISFORM.KeyField = [CustNo]
     THISFORM.OrderField = [CustNo]
ENDCASE
.Visible  = .T.
ENDWITH
THISFORM.Input = SPACE(30)
KEYBOARD [{RightArrow}]
El código del método Valid de la TextBox carga y muestra todos los registros que cumplen con los requisitos del usuario:
WITH THISFORM.MyList1
.RowSourceType  = 0
.Clear
ENDWITH

WITH THIS
* Crea la clave para el número de teléfono,
*  quitándole todo lo que no sea un dígito:
IF .Value  = [(]
   IF LEN(TRIM(.Value)) > 5
    .Value = LEFT(THIS.Value,8) + [-] ;
     + IIF(SUBS(THIS.Value,9,1)=[-], ;
           SUBS(THIS.Value,9), ;
           [-]+SUBS(THIS.Value,9))
   ENDIF
ENDIF
Key = TRIM(.Value)
SELECT CUSTOMER
SET ORDER TO TAG ( THISFORM.Mycombo1.Value )
SET EXACT OFF   && Importante! Permite claves parciales
* will not match if SET("EXACT") = "ON"
SEEK Key
DO WHILE ( NOT FOUND() ) AND ( LEN(Key) > 0 )
   Key = LEFT ( Key, LEN(Key)-1 )
   SEEK Key
ENDDO
ENDWITH

IF ( NOT FOUND() ) OR ( LEN(Key) = 0 )
   IF ( NOT FOUND() )
      MessageBox ( "No match", 64, "My App" )
   ENDIF
   THISFORM.Init
   RETURN
ENDIF
WITH THISFORM
.LockScreen   = .T.
.MousePointer = 11
ENDWITH
WITH THISFORM.MyList1
.RowSourceType = 0
cmd = [SELECT lastname,firstname,company,phone,zip,custno] ;
    + [ FROM CUSTOMER] ;
    + [ WHERE ]+THISFORM.KeyField+[ = "]+Key+["] ;
    + [ INTO CURSOR Matches] ;
    + [ ORDER BY ] + THISFORM.OrderField
&cmd
.RowSource     = "Matches"
.RowSourceType = 2
.Requery
.Refresh
.Selected[1]   = .T.
ENDWITH
WITH THISFORM
.SetAll  ( "ForeColor", RGB(0,0,0),"MyLabel")
* SORT NO LONGER APPLIES
.LockScreen    = .F.
.MousePointer  = 1
ENDWITH
Yo le cambio el color de los encabezados de las columnas para indicar visualmente que los datos ya están ordenados:

He aquí el código del evento DblClick de la ListBox:
THISFORM.Release
Y el código del método UNLOAD del formulario es esto:
RETURN THISFORM.MyList1.Value
En el código Click del botón de comando de Búsqueda del formulario original hay que tener esto:
DO FORM Search TO m.CustCode
IF NOT EMPTY ( m.CustCode )
   SEEK m.CustCode
   THISFORM.Refresh
ENDIF
Existen decenas de variaciones que pueden ser derivadas de este método básico. En mi ejemplo, deshabilité el mecanismo de consulta una vez que hayan seleccionado un campo de búsqueda de la ComboBox. Si quieren efectuar otra consulta, tienen que cerrar el formulario y volverlo a seleccionar, del mismo formulario, de otro, o del menú. En algunas aplicaciones sería preferible permitirle al usuario quedarse en la misma pantalla y hacer cuantas consultas quiere antes de seleccionar un registro y retornar - por ejemplo, para buscar padrones en una tabla de facturas antiguas para una auditoría.

Se pueden construir consultas sofisticadas para otros casos. Por ejemplo, para seleccionar todos los registros padres que tiene al menos un registro hijo con determinada característica - todas las facturas en las cuales una de las cosas compradas fué un podador de césped, por ejemplo - se puede crea una subconsulta:
SELECT &FldList. FROM INVOICES ;
 WHERE INVNO IN                     ;
 ( SELECT INVNO FORM INDETAIL  ;
    WHERE [LAWNMOWER] $ UPPER(DESC) ) ;
    ORDER BY INVDATE DESC INTO CURSOR Matches
También se pueden crear subconsultas que incluyen una lista determinada de valores - por ejemplo, las facturas de los tres clientes más importantes:
SELECT &FldList. FROM INVOICES   ;
WHERE CustNo IN ("00102","00352","00601")

14 de septiembre de 2002

Calcular la edad de una persona

Función para calcular la edad de una persona.

*-----------------------------------------------------
* FUNCTION Edad(tdNac, tdHoy)
*-----------------------------------------------------
* Calcula la edad pasando como parámetros:
*  tdNac = Fecha de nacimiento
*  tdHoy = Fecha a la cual se calcula la edad.
*          Por defecto toma la fecha actual.
*-----------------------------------------------------
FUNCTION Edad(tdNac, tdHoy)
  LOCAL lnAnio
  IF EMPTY(tdHoy)
    tdHoy = DATE()
  ENDIF
  lnAnio = YEAR(tdHoy) - YEAR(tdNac)
  IF GOMONTH(tdNac, 12 * lnAnio) > tdHoy
    lnAnio = lnAnio - 1
  ENDIF
  RETURN lnAnio
ENDFUNC

Luis María Guayán

11 de septiembre de 2002

Campos "Autoincrementales" en VFP (anteriores a la versión 8)

Ejemplo de como podemos implementar campos autoincrementales en nuestras tablas de Visual FoxPro® contenidas en una Base de Datos.
Nota: Esto es para versiones anteriores a VFP8, ya que esta versión ya posee nativamente la característica de campos autoincrementales en sus tablas.
Para lograr esto seguimos los siguientes pasos:
  • Creamos una Base de Datos de ejemplo: "EjemploDBC.DBC"
  • Creamos dos Tablas de ejemplo: "Ejemplo1.DBF" y "Ejemplo2.DBF"
  • Creamos una Tabla que almacenará los valores Id's Autoincremetales de cada tabla que lo necesite. Esta tendrá un registro por tabla: "Tabla_Ids.DBF"
  • Creamos un Procedimiento Almacenado: "NuevoId(tcAlias)"

El siguiente código crea la DBC y DBF's.

CREATE DATABASE EjemploDBC
SET DATABASE TO EjemploDBC

*-- Tabla de Ejemplo
CREATE TABLE Ejemplo1 ;
  (uId I NOT NULL DEFAULT NuevoId("Ejemplo1"), ;
  cCampo C(10) NOT NULL)

*-- Tabla de Ejemplo
CREATE TABLE Ejemplo2 ;
  (uId I NOT NULL DEFAULT NuevoId("Ejemplo2"), ;
  nCampo N(10,2) NOT NULL)

*-- Tabla con los nombres de tablas y sus Id's
CREATE TABLE Tabla_Ids ;
  (NombreTabla C(30) NOT NULL, ;
  uId I NOT NULL)
INDEX ON UPPER(NombreTabla) TAG NomTab

*-- Por cada tabla que requiera un campo
*   Id Autoincremental añado un registro
INSERT INTO Tabla_Ids VALUES("Ejemplo1", 0)
INSERT INTO Tabla_Ids VALUES("Ejemplo2", 0)

CLOSE DATABASES
RETURN

Para crear el Procedimiento Almacenado, modificamos la Base de Datos "EjemploDBC.DBC" con:
MODIFY DATABASE EjemploDBC
y en la ventana de Procedimientos Almacenados (Menú -> Base de datos -> Modificar procedimientos almacenados) copiamos la siguiente función:

FUNCTION NuevoID(tcAlias)
  LOCAL lcAlias, lnID, lnAreaAnt, lcReprAnt
  lnID = 0
  lnAreaAnt = SELECT()
  lcReprAnt = SET('REPROCESS')
  SET REPROCESS TO AUTOMATIC
  lcAlias = UPPER(ALLTRIM(tcAlias))
  IF NOT USED("Tabla_Ids")
    USE EjemploDBC!Tabla_Ids IN 0
  ENDIF
  SELECT Tabla_Ids
  IF SEEK(lcAlias, "Tabla_Ids", "NomTab")
    IF RLOCK()
      REPLACE uId WITH Tabla_Ids.uId + 1 IN Tabla_Ids
      lnID = Tabla_Ids.uId
      UNLOCK
    ENDIF
  ENDIF
  SELECT (lnAreaAnt)
  SET REPROCESS TO lcReprAnt
  RETURN lnID
ENDFUNC

Las tablas "Ejemplo1.DBF" y "Ejemplo2.DBF" contienen el campo uId que automaticamente se incrementa por el Procedimiento Almacenado cada vez que hacemos un INSERT.

Ahora miremos el ejemplo de como funcionan:

IF NOT USED("Ejemplo1")
  USE Ejemplo1 IN 0
ENDIF 
IF NOT USED("Ejemplo2")
  USE Ejemplo2 IN 0
ENDIF 
FOR ln = 1 TO 100
  INSERT INTO Ejemplo1 (cCampo) ;
    VALUES(SYS(2015))
  INSERT INTO Ejemplo2 (nCampo) ;
    VALUES(RAND()*1000)
ENDF

Note que no se incluye el campo uId en la lista de los campos. El campo uId contine un valor por defecto. Este valor por defecto es realmente una llamada al procedimiento almacenado (NuevoId()) que retorna un número único que se agrega al campo uId de la tabla pasada como parámetro cuando añadimos un nuevo registro.
Nota: Parte del código de este artículo fue tomado y ligeramente modificado del artículo de la MSKB http://support.microsoft.com/kb/316910/es