Está en la página 1de 11

D:\Utiles\Motores\SQLite\PRUEBAS\ejemplo-vfp-accesando-sqlite.prg Martes, 02 de Julio del 2013 02:45 a.m.

** Ejemplo-VFP-accesando-SQLite.prg
** Jun-2013
** jhernancanom-at-hotmail-dot-com

*-- Author: Jesús Hernán Cano Martínez


*-- Este es un ejemplo de acceso a una base de datos SQLite utilizando el concepto
* de motor embebido, que ofrece este motor.
* La metodología parece ser similar a ADODB.RecordSet.
*
*-- Nota: Es el primero de varios documentos para accesar tanto este motor como otros
*
*-- Parámetros: Este ejemplo no usa parámetros
*
*-- Ejemplo de llamada: desde la ventana de comandos de VFP (o IDE compatible),
* o desde un .EXE ya compilado, ejecuta lo sgte:
* do "Ejemplo-VFP-accesando-SQLite.prg"
* (puede cambiar el nombre a este ejemplo si gusta)
*
*-- Requirimientos:
* Este ejemplo consta de varios archivos, todos incluidos en el .zip que usted descargó.
* Esos archivos son:
* 1. Ejemplo-VFP-accesando-SQLite.prg
* 2. NewObjectsPack1.dll
* 3. SQLITE3COMUTF8.dll
* 4. VB6STKIT.DLL
* 5. northwind.sqlite
* Ver el archivo con la descripción de ellos.
*
*-- Recomendación:
* Con el fin de probar adecuadamente este procedimiento, y cualq otro que quieras escribir,
* te recomiendo que abras la base de datos con un administrador de bases de datos.
* En mi caso tengo estos siete (la mayoría portales):
*
* SQLite Studio: muy bien!!! seleccionado y recomendado de mi parte a ustedes
(pero puedes usar el que uses, si te acostumbraste a otro)
*
* SQLite Admin : muy bien!!! (pero ya me está fallando: no me volvió a abrir NorthWind.sqlite)
* éste tiene el logo original de SQLite
* SQLiteMan : bien "Describe table" [PortableApps]
*
* Descarté los sgtes:
*
*x SQLite Database Browser: no muestra la estructura completa [PortableApps]
*x Database Browser: requiere ODBC [PortableApps]
*x HeidiSQL: requiere ODBC [PortableApps]
*x EasySQLite2: parece que es para SQLite2.
*
* El hecho de descartarlos por requerir de ODBC es sólo por probar primero la
* tecnología "motor embebido", es decir con el mínimo de recursos.
* El sgte paso será (2) conocer un "motor portable" como MariaDB (y probablemente MySQL)
* y luego (3) ODBC, pero haciendo la instalación del ODBC del motor desde VFP, para de
* esta forma requerir menos recursos (con "recursos" me refiero a los conceptos de
* embebido y portabilidad, es decir no necesitar instalar nada más que la app)

-1-
D:\Utiles\Motores\SQLite\PRUEBAS\ejemplo-vfp-accesando-sqlite.prg Martes, 02 de Julio del 2013 02:45 a.m.

*
* Pero claro antes de ello será optimizar y socializar la info que este ejemplo nos
* permita descubrir sobre SQLite y VFP.
*
* Nos vemos en el foro!!!
*

************************************************************************************************
****************** PROCEDURE MAIN **************************************************************
************************************************************************************************

** Primera parte: Registrando el motor

if .not.Registrar()
= MessageBox ( "No se pudo registrar NewObjectsPack1.dll " +chr(13)+chr(13);
+ "No puedo continuar" )
return
endif
CLEAR

** Segunda parte: Inicializando el motor SQLite embebido, SQLite3 COM

*' Using the embedded SQL database engine.


*' You use the SQLite COM object to work with it:
local db as "newObjectsPack1.sqlite.dbutf8"
local oR as "adodb.recordset"
local oObj as "adodb.recordset"

** db= CreateObject("newObjectsPack1.sqlite.dbutf8") --> así está en la doc, pero no me funciona


db = CreateObject("newObjects.sqlite3.dbutf8")
&& busqué en el registro de Windows y encontré newObjects.sqlite3.dbutf8 y otros
&& selecciono éste pues corresponde a la versión 3

M.cCad = chr(13) + "Version de SQLite: "+transform(db.SQLiteVersion) ;


+ chr(13) + "db.TypeInfoLevel : "+transform(db.TypeInfoLevel)
** TypeInfoLevel &&---> 0 -zero-
= MessageBox ( M.cCad, 0, '--', 5000)

db.NoErrorMode = .T. &&' True - no error mode, False(defaults) - error mode


&& Si se deja el standard (db.NoErrorMode = .f.), cuando se presente un
&& error en la ejecución del motor SQLite, se mostrará un error de VFP
&& al estilo del comando ERROR
&& Si hacemos db.NoErrorMode = .t., cuando se presente un error en la
&& ejecución del motor SQLite, podemos usar propiedades internas de
&& SQLite COM (motor embebido) para detección y manejo de errores

** Indicamos cuál es la base de datos que queremos abrir


local M.cDB
M.cDB = addbs(curdir())+"northwind.sqlite"

** Tercera parte: Abrimos la base de datos

-2-
D:\Utiles\Motores\SQLite\PRUEBAS\ejemplo-vfp-accesando-sqlite.prg Martes, 02 de Julio del 2013 02:45 a.m.

*' Opens or creates a SQLite3 database


If db.Open(M.cDB) Then

** Cuarta parte: Pedimos la estructura de uno de loa rchivos de datos (una de la tablas)

** ============================================================================================
** De forma didáctica veamos cómo capturamos la estructura de una de las tablas
**
local M.cSQL
M.cSQL = "PRAGMA table_info('Orders');"

* Executes one SQL statement (or several statements separated by ;)


oObj = db.Execute(M.cSQL)
do case
case !empty(db.LastError) or isnull(oObj)
*' Perform some error handling
*' An error has occured - deal with it
= MessageBox ( "Error occured on Execute: " +chr(13)+ db.LastError +chr(13)+ M.cSQL;
+chr(13)+ M.cDB )

case oObj.Count<1
= MessageBox ( "No hay datos luego de Execute: " +chr(13)+ db.LastError +chr(13)+ M.cSQL;
+chr(13)+ M.cDB )
** es decir que no hay registros
** la cuestión es que el recordset --aparte hablo de ésto-- no puede mostrar ni siquiera
** su estructura cuando no hay registros (o sea si no hay registros, tampoco hay campos)

otherwise

= MessageBox( 'RecCount: '+tran(oObj.Count)+chr(13)+'ColCount: '+;


tran(oObj.Item(1).Count), 0, '--db.Execute('+M.cSQL+')--' )

** Quinta parte: Procedemos a mostrar la estructura de ese archivo de datos

** Creemos un cursor temporal para recibir los datos que capturemos de SQLite3 (símil VFP)
create cursor '_Orders_' ( ;
FIELD_NUMBER C( 3),;
FIELD_NAME C(50),;
FIELD_TYPE C(20),;
FIELD_LEN C( 7),;
FIELD_NULL C( 5),;
FIELD_DEFA C(50),;
FIELD_AUTOINC C(10),;
PRIMARY_KEY C( 5),;
TABLE_NAME C(50),;
TABLE_RULE M( 4) )

cCad = ''
for K=1 to oObj.Count

** la vble cCad es para mostrar los campos en un MessageBox


** esta vble cCad se puede obviar, y usar sólo el cursor _Orders_

-3-
D:\Utiles\Motores\SQLite\PRUEBAS\ejemplo-vfp-accesando-sqlite.prg Martes, 02 de Julio del 2013 02:45 a.m.

for L=1 to oObj.Item(K).Count


cCad = cCad + chr(13)+tran(K) + ' - ' + tran(L) + ' - ' + ;
TRANSFORM(oObj.Item(K).Item(L))
next

** agreguemos un registro al cursor de la estructura


append blank

** oObj.Item(K).Item(1) contiene el número del campo (zero-based, o sea en la


** forma extraña que maneja el lenguaje C y variantes: la
** posición del primer elemento de un vector es el número cero)
** claro que Item() no es basado en cero (!!!???)
** oObj.Item(K).Item(2) contiene el nombre del campo
** oObj.Item(K).Item(3) contiene el tipo de campo (INTEGER, NUMERIC, TEXT,
** REAL, BLOB, CHAR, VARCHAR y quizá otros), además si es VAR o
** VARCHAR capturo la logitud, pues está facilita de agarrar
** oObj.Item(K).Item(4) cuando no admite nulos, dice 99.
** oObj.Item(K).Item(5) contiene el valor por defecto; en caso contrario .null.
** oObj.Item(K).Item(6) cuando es clave primaria, contiene un 1.
**
** En las ayudas está el tema "How to obtain more useful information?"
** en SQLite - Version 3 - Object Reference - Execute (cerca del final)
**
** Which will return a SELECT-like result with the following columns:
** cid - the field id
** name - the field name
** type - the type specified
** notnull - 0/1 - 1 means the field must not be null
** dflt_value - the default value for the field (if present)
** pk - 0/1 - 1 means the field is primary key
**
**
replace FIELD_NUMBER with transform(oObj.Item(K).Item(1))
replace FIELD_NAME with transform(oObj.Item(K).Item(2))
replace FIELD_TYPE with transform(oObj.Item(K).Item(3))
if '('$oObj.Item(K).Item(3)
cNum = STREXTRACT(oObj.Item(K).Item(3),"(",")")
if len(alltrim(cNum))=1
cNum = '0'+cNum
endif
replace FIELD_LEN with cNum
endif
if (oObj.Item(K).Item(4)=99)
replace FIELD_NULL with 'No'
else
replace FIELD_NULL with 'Yes'
endif
if transform(oObj.Item(K).Item(5))='.NULL.'
replace FIELD_DEFA with ''
else
replace FIELD_DEFA with transform(oObj.Item(K).Item(5))
endif
if (oObj.Item(K).Item(6)=1)

-4-
D:\Utiles\Motores\SQLite\PRUEBAS\ejemplo-vfp-accesando-sqlite.prg Martes, 02 de Julio del 2013 02:45 a.m.

replace PRIMARY_KEY with 'PK'


else
replace PRIMARY_KEY with ''
endif
replace TABLE_NAME with alias() && 'Orders'
**

cCad = cCad + CHR(13)


if mod(K,6)=0
= MessageBox ( cCad,0,'CAMPOS DE '+alias() )
cCad = ''
endif
next

if !empty(cCad)
= MessageBox ( cCad,0,'CAMPOS DE '+alias() )
endif
cCad = ''

if .f.
browse nomo
else
** una forma más elegante de hacer un BROWSE
** (lástima que dependa de la vble lBrowseSetup)
private oBrowse, lBrowseSetup
lBrowseSetup=.F.
wait window [Visualizando datos...] nowait
BROWSE NAME oBrowse WHEN BrowseSetup() &&NOWAIT
oBrowse = .null.
wait clear
endif

endcase
** =============================================================================================

** Sexta parte: Procedemos a mostrar los datos de ese archivo de datos


** =============================================================================================
** vamos a visualizar los datos de una de las tablas
**
**local M.cSQL
M.cSQL = "SELECT * FROM Orders"
oR = db.Execute(M.cSQL)
*oR = db.Execute("SELECT * FROM 'Order Details'")

do case
case !empty(db.LastError) or isnull(oR)
*' Perform some error handling
*' An error has occured - deal with it
= MessageBox ( "Error occured on Execute: " +chr(13)+ db.LastError +chr(13)+ M.cSQL;
+chr(13)+ M.cDB )

case oR.Count<1
= MessageBox ( "No hay datos luego de Execute: " +chr(13)+ db.LastError +chr(13)+ M.cSQL;

-5-
D:\Utiles\Motores\SQLite\PRUEBAS\ejemplo-vfp-accesando-sqlite.prg Martes, 02 de Julio del 2013 02:45 a.m.

+chr(13)+ M.cDB )
** es decir que no hay registros

otherwise

*= MessageBox ( oR.ReadOnly ) && no existe


*db.TypeInfoLevel = 4
*messagebox ( oR.Item(1).Info ) &&--> object

** Esta es otra forma de visualizar los campos del recordset


** pero la info no es suficiente: (1) con .Key() obtengo el nombre del campo,
** y (2) con .Info() obtengo un número que representa el tipo, que debo pasar
** por la UDF GetType() para que me dé la descripción, y (3) con la
** función LEN() de VFP obtengo la longitud de los campos TEXT
** Esta forma es demasiado simple y sin suficiente info (para dar), así que
** podemos descartarla
For FieldN = 1 To oR.Item(1).Count && Número de Campos (columnas)
** se muestra campo a campo
messagebox ( 'Num.Campo: '+ tran(FieldN) +chr(13);
+'Nom.Campo: '+ oR.Item(1).Key (FieldN) +chr(13);
+'nTipo : '+ tran(oR.Item(1).Info(FieldN)) +chr(13);
+'Desc.Tipo: '+ GetType(oR.Item(1).Info(FieldN)) +chr(13);
+'LongCampo: '+ iif(oR.Item(1).Info(FieldN)=8,;
TRAN(LEN(oR.Item(1).Item(FieldN))),'.'), ;
0, '--CAMPOS---'+transform(oR.Item(1).Count) ) && , 2000
Next

*' Display the results for example


*For I = 1 To oR.Count
* = MessageBox ( oR(I).Name )
*Next

** Sets the level of type info reported by the Execute methods


db.TypeInfoLevel = 1
** El manual indica que hay cinco valores posibles (de 0 a 4), para tres formas
** diferentes en que el motor me devuelve los datos:
** 0 - (default) Type info is returned as type constants
** 1 or 3 - Type info is returned as a single string containing semicolon ";"
** delimited list of type names for each column in the result.
** 2 or 4 - The type info is returned as a collection of type names.
** Me interesa de la segunda forma: lista con valores separados por punto y coma
** pero siempre me devuelve una colección, sea cual sea el valor que ponga
** ¿Será que debo inicializar esta propiedad antes de ejecutar el método Execute()?
** (suponía que era entes de "pedir los datos" --antes de ejecutar .Info()--)
**

*' Cycle through all the rows


For I = 1 To oR.Count

** oR.Count && Número de Registros


** oR.Item(1).Count && Número de Campos (columnas)
** oR.Item(I).Key(#) && Nombre del Campo
** oR.Item(I).Item(#) && Valor del Campo

-6-
D:\Utiles\Motores\SQLite\PRUEBAS\ejemplo-vfp-accesando-sqlite.prg Martes, 02 de Julio del 2013 02:45 a.m.

** Tenemos dos formas de pedir el dato de un campo (el valor, lo que se alamcena)
** 1. Utilizando oR.Item(I).Item("OrderID")
** donde OrderID es el nombre del campo que necesite
** 2. Utilizando oR.Item(I).Item(14)
** donde 14 es el número del campo que necesite

** Utilizando oR.Item(I).Key(14) le pido al sistema el nombre del campo


** Nótese: oR es el recordset
** .Item(I) I es el número del registro dentro del recordset
** .Key(14) me devuelve el nombre del campo número 14 dentro del recordset

** Vamos a visualizar sólo los primeros cinco campos de cada registro

M.cCad = chr(13) + "OrderID : "+transform(oR.Item(I).Item("OrderID" )) ;


+ chr(13) + "CustomerID : "+transform(oR.Item(I).Item("CustomerID" )) ;
+ chr(13) + "EmployeeID : "+transform(oR.Item(I).Item("EmployeeID" )) ;
+ chr(13) + "OrderDate : "+transform(oR.Item(I).Item("OrderDate" )) ;
+ chr(13) + "RequiredDate: "+transform(oR.Item(I).Item("RequiredDate")) ;
+ chr(13) + "Freight : "+transform(oR.Item(I).Item("Freight" )) ;
+ chr(13) + "ShipCity : "+transform(oR.Item(I).Item("ShipCity" )) ;
+ chr(13) + "ShipRegion : "+transform(oR.Item(I).Item("ShipRegion" )) ;
+ chr(13) ;
+ chr(13) + padr(oR.Item(I).Key(01),20)+": "+transform(oR.Item(I).Item(01)) ;
+ chr(13) + padr(oR.Item(I).Key(02),20)+": "+transform(oR.Item(I).Item(02)) ;
+ chr(13) + padr(oR.Item(I).Key(03),20)+": "+transform(oR.Item(I).Item(03)) ;
+ chr(13) + padr(oR.Item(I).Key(04),20)+": "+transform(oR.Item(I).Item(04)) ;
+ chr(13) + padr(oR.Item(I).Key(05),20)+": "+transform(oR.Item(I).Item(05)) ;
+ chr(13) + padr(oR.Item(I).Key(08),20)+": "+transform(oR.Item(I).Item(08)) ;
+ chr(13) + padr(oR.Item(I).Key(11),20)+": "+transform(oR.Item(I).Item(11)) ;
+ chr(13) + padr(oR.Item(I).Key(12),20)+": "+transform(oR.Item(I).Item(12)) ;
+ chr(13)
wait window left(M.cCad,255) nowait noclear
= MessageBox ( M.cCad, 0, transform(I), 5000 )

** Como estamos viendo los datos en MessageBoxes, paremos en 30 por si nos aburrimos...
if I>30
exit
endif
** si deseas ver los datos en un Browse y/o guardarlos en un cursor o en un DBF, puedes
** hacer append blnk/replace o insert por aquí...
** ¿alguien nos apoya con algo como un ToDBF o ToCursor o FromRS2DBF, o similar, más o
** menos genérico?
Next

endcase

*' Finish everything


db.Close
Else
*' Deal with the error further
= MessageBox ( "Error occured on Open: " +chr(13)+ db.LastError +chr(13)+ M.cDB )
EndIf

-7-
D:\Utiles\Motores\SQLite\PRUEBAS\ejemplo-vfp-accesando-sqlite.prg Martes, 02 de Julio del 2013 02:45 a.m.

** Séptima parte: Limpiamos el sistema operativo, des-registrando las DLL que registramos
** nosotros mismos

wait window 'Desregistrando...' timeout 2&&nowait noclear &&


if !Registrar('U')
= MessageBox ( "No se pudo des-registrar NewObjectsPack1.dll " +chr(13)+chr(13) +;
"Pero podemos continuar..." )
endif
wait clear
************************************************************************************************
****************** ENDPROC MAIN ****************************************************************
************************************************************************************************

** De acuerdo a las ayudas --imagen 5-- necesitamos registrar sólo dos DLLs: NewObjectsPack1.dll
** y SQLITE3COMUTF8.dll para activar el motor embabido de SQLite en nustras app
** (así nos evitamos tener que hacer una instalación adicional --la del motor--)
** Para ello utilizamos la DLL genérica para registrar que es Vb6stkit.DLL
** Veamos...
function Registrar(M.pDes)
if pcount()=0
declare integer DLLSelfRegister in [Vb6stkit.DLL] string lpDllName
local M.lOk
M.lOk = .t.
M.lOk = M.lOk and ( DLLSelfRegister([NewObjectsPack1.dll]) = 0 )
M.lOk = M.lOk and ( DLLSelfRegister([SQLITE3COMUTF8.dll] ) = 0 )
return ( M.lOk )
endif

if pcount()=1 and M.pDes=='U'


wait window 'Desregistrando' nowait noclear
local M.lOkN, M.lOkS
store .t. to M.lOkN, M.lOkS
if !UnregisterControl([NewObjectsPack1.dll])
M.lOkN = .f.
= MessageBox ( "No se pudo des-registrar NewObjectsPack1.dll " +chr(13)+chr(13) +;
"Continuemos..." )
endif
if !UnregisterControl([SQLITE3COMUTF8.dll] )
M.lOkS = .f.
= MessageBox ( "No se pudo des-registrar SQLITE3COMUTF8.dll " +chr(13)+chr(13) +;
"Continuemos..." )
endif
** el "des-registro" se hace al finalizar la app; si alguno de los dos no se puede
** des-registrar, realmente no hay problema... ¿cierto?
return ( .t. )
endif
wait window '¿Qué pasó?' &&nowait noclear
** si llegó hasta aquí, sí hay problema... sólo se ejecuta sin parámetro (para registrar)
** o con el parámetro U (para des-registrar)...
return .f.

-8-
D:\Utiles\Motores\SQLite\PRUEBAS\ejemplo-vfp-accesando-sqlite.prg Martes, 02 de Julio del 2013 02:45 a.m.

FUNCTION UnregisterControl
*-- Author: Paul Vlad Tatavu
*-- This function unregisters an OCX/ActiveX control
* or set of OCX/ActiveX controls based on the name of file.
*-- Note: It works for OLE servers too.
*-- Parameters:
* tcFileName = the name of the file that contains
* the control(s), including the path.
*-- Returns:
* Logical TRUE if successful, FALSE otherwise.
* Also returns FALSE if the file doesn't exist.
*-- Call sample:
* llUnregistered = UnregisterControl("c:\windows\system\comctl32.ocx")

LPARAMETERS tcFileName
LOCAL llSuccess

IF FILE(tcFileName)
DECLARE INTEGER DllUnregisterServer IN (tcFileName) AS __DllUnregisterServer__
*-- This function returns 0 if successful
llSuccess = ( __DllUnregisterServer__() = 0 )

local lnError
lnError = 0
if !llSuccess
DECLARE INTEGER GETLASTERROR IN WIN32API
lnError = GetLastError()
endif

*MessageBox ( "Mirando"+chr(13)+"UnregisterControl("+tcFileName+")" + chr(13)+;


transform(llSuccess) + chr(13)+transform(lnError) )

ELSE
llSuccess = .F.
*MessageBox ( "No existe"+chr(13)+"UnregisterControl("+tcFileName+")" )
ENDIF

RETURN llSuccess
ENDFUNC

** Escribí esta función para mostrar en "letras" el tipo de campo, ya que SQLite3COM sólo me
** entrega un número cuando utilizo oR.Item(1).Info(FieldN), pero recordemos que con PRAGMA
** obtenemos mejor info
function GetType(M.pType)
M.cRet = ""
do case
case pcount()=0 or empty(M.pType)
M.cRet = "ERROR"
case M.pType = 1
M.cRet = "NULL - vbNull"
case M.pType = 3

-9-
D:\Utiles\Motores\SQLite\PRUEBAS\ejemplo-vfp-accesando-sqlite.prg Martes, 02 de Julio del 2013 02:45 a.m.

M.cRet = "INTEGER - vbLong"


case M.pType = 5
M.cRet = "NUMERIC - vbDouble" && Real
case M.pType = 8
M.cRet = "TEXT - vbString" && DateTime, Date
*case M.pType = 0
* M.cRet = "REAL - vbDouble"
case M.pType = 8209
M.cRet = "BLOB - binary (vbArray Or vbByte)" && Picture
*case M.pType = 0
* M.cRet = "??"
otherwise
M.cRet = "UNKNOWN < "+transform(M.pType)+" >" && Desconocido
endcase
return M.cRet

*----------------------------------------
** Una idea interesante para visualizar datos, encontrada en Internet
proc Browse_Trick_v2
** Browse_Trick_v2.PRG
=MessageBox([No me gusta, pues cae a la ventana de comandos]+chr(13);
+[Uno tiene que dar click dentro del browse para volverlo activo]+chr(13);
+[],0,[--],4000)
RELEASE oBr
private oBr
use AUXILIAR shared noupdate
wait window [Visualizando datos...] nowait

BROWSE LAST NOWAIT NAME oBr

with oBr
.ReadOnly = .T.
.Columns(1).Header1.Caption = "User Name"
.Columns(3).Header1.Caption = "File ID"
.Columns(5).Header1.Caption = "File Name"
.Columns(6).Header1.Caption = "Server File Name"
.ZOrder (0)
.AutoFit()
*wait wind 'no funcionaba .SetFocus() cuando hice pruebas'
.SetFocus()
endwith
*----------------------------------------

*----------------------------------------
** Es una idea interesante para visualizar datos y no me aguanté presentárselas
** obtenida de Internet, pero no recuerdo la fuente (la buscaré)
PROCEDURE BrowseSetup
IF NOT lBrowseSetup
WITH oBrowse AS GRID
.LEFT = 50
.TOP = 50

-10-
D:\Utiles\Motores\SQLite\PRUEBAS\ejemplo-vfp-accesando-sqlite.prg Martes, 02 de Julio del 2013 02:45 a.m.

**** Row coloring ------------------------------


* Only works good with no index:
*.SETALL("DynamicBackColor","IIF(RECNO() % 2 = 0, 16777215, 16777088)","Column")
* This one works much better
.SETALL("DynamicBackColor","IIF(oBrowse.ActiveRow%2=0,16777215,16777088)","Column")

*** Fix the first column -----


.LOCKCOLUMNS = 1
.Columns(1).DynamicBackColor= ""
.Columns(1).BackColor = RGB(255,255,168)

.AUTOFIT()
.HIGHLIGHTSTYLE= 2
.ALLOWCELLSELECTION = .F.
.HIGHLIGHTBACKCOLOR = RGB(255,128,64)
.HIGHLIGHTFORECOLOR = RGB(0,0,0)
.Refresh

ENDWITH
lBrowseSetup = .T.
ENDIF
*----------------------------------------
return
ENDPROC

**

-11-

También podría gustarte