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 + tUnidadesAnselmo Antonio Ortiz Alcocer
25 de septiembre de 2002
Convertir Números a Letras
23 de septiembre de 2002
Problemas con tu OCX ?
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
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. ENDWITHYo 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 ENDWITHFinalmente, esto es el código DblClick de la ListBox:
PROCEDURE mylist1.DblClick WITH THISFORM .LockScreen = .T. .PageFrame1.ActivePage = 1 .PageFrame1.Page1.Refresh .LockScreen = .F. ENDWITHEsto 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 ENDWITHYo 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.ReleaseY el código del método UNLOAD del formulario es esto:
RETURN THISFORM.MyList1.ValueEn 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 ENDIFExisten 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 MatchesTambié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)
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 EjemploDBCy 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
7 de septiembre de 2002
Cursor al final del texto de un TextBox
Un truco para que cada vez que un TextBox recibe el foco, el cursor se ubique al final del texto. Solo tienes que poner en el evento GotFocus del TextBox el siguiente código ...
This.SelStart = LEN(RTRIM(This.Value))
... y listo, el cursor se colocará al final del texto del TextBox
Luis María Guayán
4 de septiembre de 2002
Un efecto "GROOVY" en la aplicación
Podemos agregar un efecto de cambio de colores que reaccionen al mouse
1. Agregar al InitEvent de un formulario el código de "efecto degradado por colores" de Ramón Rodriguez Ramírez:
*!* Rutina para generar degradados en Visual FoxPro *!* ----------------------------------------------- *!* Para probar este ejemplo, pegue el código *!* en el método init de un formulario y *!* presione CTRL+D. *!* Autor: L.C.C. Ramón Rodríguez martínez *!* Pais de procedencia: México. *!* Actualizacion: 7 de agosto de 2002 *!* Version 1.0 Local i,color1, R, G, B With Thisform .scalemode = 3 &&pixeles .drawstyle = 0 && sólido .drawwidth = 2 R=0 G=255 B=0 FOR i = 1 to 255 *-- Disminuimos valor de colores If r > 0 then r = r - 1 endif IF g > 0 then g = g - 1 endif IF b > 0 Then b = b - 1 endif color1 = RGB(R,G,B) *Establesco el color .forecolor=Color1 *- Dibujo linea sobre el fomulario .line (0,.ViewPortHeight * (i - 1) / 255, .ViewPortwidth, .ViewPortheight * i / 255) NEXT i EndWith
2. En el MouseMoveEvent del formulario agragar lo siguiente:
LPARAMETERS nButton, nShift, nXCoord, nYCoord _screen.backcolor=this.Point(nXCoord,nYCoord)
Y listo, un efecto tipo "disco" para el Screen principal.
Quiero agradecer a Ramón por enseñarme el "efecto degradado". Espero te guste lo que hice con tu código.
Lucio Manuel Núñez Ramírez