Explora Libros electrónicos
Categorías
Explora Audiolibros
Categorías
Explora Revistas
Categorías
Explora Documentos
Categorías
Este capítulo describe el proceso de instalación de su paquete de desarrollo de Xbase++. También lo provee a Ud.
con información importante sobre el entorno del sistema y la configuración.
Note que el programa de desinstalación sólo puede remover los archivos instalados. No remueve los archivos
creados después de la instalación, como los archivos OBJ o EXE en el directorio ..\SAMPLES, por ejemplo. Si
existen tales archivos, se despliega un mensaje de aviso de que el programa no puede removerse. En este caso
particular, puede ignorar el mensaje.
Cuando se remueve una versión previa debería chequear las entradas PATH, LIB e INCLUDE en el
AUTOEXEC.BAT (Windows 95) o en el registro HKEY_CURRENT_USER\Environment (Windows NT), respecti-
vamente, para los antiguos valores.
Después de una instalación nueva, debe recompilar todos los programas que ha compilado con una versión previa
de Xbase++.
Instalación típica
El paquete de desarrollo de Xbase++ se instala por predeterminación en la unidad C: con \ALASKA\XPPW32\
como el directorio raíz <RAIZ>:
<RAÍZ>
+--BIN Compilador y programas de servicio
+--BOOK Ayuda en línea
+--LIB Biblioteca de tiempo de ejecución
+--INCLUDE Archivos #include
+--RESOURCE Archivos de recursos
+--RUNTIME Bibliotecas de tiempo de ejecución
+--SOURCE Código fuente
+--SYS Fuente para los archivos del nivel Sistema
+--COMPAT funciones de compatibilidad para Clipper '87
+--SAMPLES Programas de ejemplo
Durante la instalación la configuración de su sistema se modifica para reflejar los nuevos directorios. Para Win-
dows NT, los cambios se aplican a la Base de Datos del registro bajo \HKEY_CURRENT_USER\Environment ,
mientras AUTOEXEC.BAT se altera para Windows 95. Las siguientes claves de registro o variables de entorno,
respectivamente, se modifican o crean si están ausentes:
PATH = C:\ALASKA\XPPW32\BIN;C:\ALASKA\XPPW32\LIB;
LIB = C:\ALASKA\XPPW32\LIB;
INCLUDE = C:\ALASKA\XPPW32\INCLUDE;
XPPRESOURCE = C:\ALASKA\XPPW32\RESOURCE;
Se definen asociaciones de archivo predeterminadas para los archivos PRG, CH, XPJ y XFF durante una insta-
lación típica. Use la instalación personalizada para suprimir esta característica automática.
Instalación personalizada
La instalación personalizada permite seleccionar los componentes individuales del paquete de Xbase++ a instalar.
Además, la definición de las asociaciones de archivo pueden seleccionarse individualmente.
CH Los archivos Include se asocian con el editor de notas de Windows y con el compilador de Xbase++. El
compilador lleva a cabo un chequeo de sintaxis para un archivo CH cuando se lo invoca.
PRG Los archivos de código fuente se asocian con el editor de notas de Windows y el compilador de Xbase++.
Se compila un archivo PRG.
XFF Los archivos de definición de formulario se asocian con el diseñador de formularios.
XPJ Los archivos Project de Xbase++ se asocian con el ProjectBuilder. Un doble clic con el botón izquierdo
sobre un archivo XPJ construye un proyecto.
Nota: El archivo XPP.REG está disponible en el directorio raíz de la instalación de Xbase++. Si las asociaciones
del archivo no se definen durante la instalación pueden definirse posteriormente importando XPP.REG al registro
de Windows usando REGEDIT.EXE. Las asociaciones del archivo predeterminado pueden modificarse también
editando el archivo XPP.REG. Por ejemplo, si quiere la asociación de archivos de código fuente con el editor de su
elección, reemplace la cadena "notepad.exe" en XPP.REG con el nombre del editor e importe el archivo modifi-
cado al registro.
Sea consciente que XPP.REG contiene una foto de HKEY_CURRENT_USER\Environment como lo era antes de
la instalación de Xbase++. Si posteriormente cambia el registro al instalar otro software, por ejemplo, estos cam-
bios no se reflejan en XPP.REG. Antes de importar XPP.REG al registro, asegúrese que
HKEY_CURRENT_USER\Environment es correcta o remueva esta sección enteramente desde el archivo
XPP.REG.
Antes de continuar
Si ha instalado Xbase++ bajo Windows 95 o trabaja con ambos Clipper y Xbase++ en la misma estación de trabajo,
por favor lea las siguientes secciones antes de comenzar usando Xbase++.
Note que los cambios en CONFIG.SYS se vuelven efectivos sólo cuando reinicia el equipo. Note también que
C:\windows es sólo un ejemplo donde se localiza el procesador de comandos COMMAND.COM. Puede encon-
trarse en un directorio diferente en su computadora.
Una indicación clara de un fichero de encabezado equivocado que se encuentra en uso se da cuando el enlazador
de Xbase++ reporta una función externa irresuelta que comienza con dos guiones bajos (a saber: __SetFormat(),
__Quit(), __Keyboard()). Estas llamadas a función se producen sólo si se incluye el archivo de Clipper STD.CH. En
general, no hay funciones en Xbase++ que comienzan con un guión doble.
Cambiando el entorno
Para cambiar el entorno de configuración del sistema para Xbase++ o Clipper con mayor facilidad, dos archivos de
lote de comandos están disponibles luego de la instalación de DINO.BAT y AUTOXPP.BAT . El archivo DINO.BAT
se localiza en el directorio raíz de su unidad C: y AUTOXPP.BAT se lo encuentra en el directorio raíz de la ins-
talación de Xbase++. DINO.BAT contiene una pre-instalación instantánea de las variables de entorno del sistema.
Cuando se las invoca, reinicia la configuración del sistema a los valores que eran actuales antes de la instalación
de Xbase++. Ejecutando AUTOXPP.BAT establece los caminos de búsqueda para Xbase++. Los dos archivos de
lote de comandos, por tanto, establecen las variables de entorno apropiadas para Xbase++ o Clipper y le permiten
a Ud. cambiar al instante el entorno para cualquiera de los compiladores.
Después de la instalación
Este capítulo da un rápido vistazo de como podrá prepararse para trabajar con Xbase++. Además, se dan aquí los
consejos de como obtener información sobre Xbase++ que no se incluyen en esta documentación introductoria.
Apps Esta categoría contiene aplicaciones únicas hechas a partir de archivos de código fuente
de múltiples PRG. Cada aplicación apunta al diseño del programa, como Interfaces Sim-
ples o Múltiples, por ejemplo.
Basics Cada programa de ejemplo en la categoría de Conceptos Básicos se focaliza en una ca-
racterística particular de Xbase++. La intención de los ejemplos no es su reutilización, pero
sino mostrar técnicas de programación que resuelven un problema en particular. "Cómo
desplegar texto usando una fuente itálica?", "Cómo crear una ventana modal?" o "Cómo
usar un thread?" son preguntas contestadas por ejemplos de esta categoría.
Migrate El código de Clipper que migra a GUI es uno de los puntos fuertes de Xbase++. Por con-
siguiente, los ejemplos que se incluyen en esta categoría muestran varias posibilidades y
pasos intermedios en la transición de la aplicación del modo de texto a una aplicación de
características GUI completas.
Solution Esta categoría junta programas de ejemplo que proveen soluciones a problemas de pro-
gramación comunes. Todos ellos son programas únicos. Sin embargo, incluyen códigos de
programas que implementan un escenario de prueba para el ejemplo actual. El ejemplo
está listo para la reutilización en sus programas cuando descarta al código para probar el
escenario.
Los ejemplos en la carpeta MIGRATE proveen un buen punto de comienzo cuando ya ha instalado Xbase++ por
primera vez. Entonces abra esta carpeta y cliquee en Login . Ahora puede actualmente ver los contenidos del
directorio ..\SAMPLES\SOURCE\MIGRATE\LOGIN que contienen diferentes tipos de archivo. Los tipos de archivo
se identifican por la extensión del archivo y se visualizan mediante íconos distintos.
Vuélvase familiar con las funciones y comandos disponibles en Xbase++. Los encontrará listados en orden alfa-
bético cuando abra la Tabla de Contenidos en la ayuda en línea.
Ud. será notificado por e-mail si el problema que informó está aprobado. El soporte técnico lo proveerá a Ud. con
información sobre cómo trabajar sobre el problema, si es posible. Todos los PDRs aprobados serán publicados
frecuentemente (aprox. bimensualmente).
Nota: Apreciamos muchísimo la contribución que ayuda a mejorar al producto y su robustez. Sin embargo, si
quiere contribuir con sus experiencias, debe ser un usuario registrado. Sin un número de registro, no es posible
asignarle un PDR-ID a un informe de problema.
El formulario PDR
Compañía / Remitente :
________________________________________________________
Síntomas :
________________________________________________________
Archivo(s) Adjunto(s):
________________________________________________________
Comentarios :
________________________________________________________
Todos los archivos DLL marcados con 1) en la tabla deben ser copiados juntos con el EXE a otra estación. De otro
modo el programa ejecutable no puede ser cargado en memoria. Los archivos marcados con 2) son solamente
necesarios si el EXE requiere la funcionalidad de la respectiva DLL. Los archivos DLL deben ser copiados a un
directorio que está listado en la variable de entorno PATH. Si el directorio no es parte del PATH, Ud deberá mo-
dificar esta variable de entorno para que lo incluya.
Nota: La tabla de mas arriba lista todos los archivos de Xbase++ que son de distribución libre para sus clientes.
Todos los archivos creados por Xbase++ también son de libre distribución (Por favor cumpla con el acuerdo de
Licencia).
Opciones de comando
Usa la función de nombres reservada como opciones en los comandos definidos por el usuario conduce a pro-
blemas con el preprocesador en Xbase++. Por ejemplo:
En este caso, la palabra clave se emplea como parte de la opción del comando idéntico a la palabra clave de los
parámetros opcionales. Esto no es permitido en Xbase++.
Constantes Numéricas
Se pueden programar las constantes numéricas en Xbase++ usando la notación decimal, hexadecimal o científica.
Clipper comprende solo comprende la notación decimal. Las constantes a continuación son válidas en Xbase++:
3.1415926 // decimal
0xFF // hexadecimal
10.1E-10 // científica
Declaración PARAMETERS
Con la declaración PARAMETERS, la compilación con Xbase++ es más estricta que Clipper. La declaración se
usa para declarar parámetros formales para funciones o procedimientos como variables de la clase de almace-
namiento PRIVATE (Clipper '87). Debido a que PARAMETERS es una declaración ejecutable, puede usarse
solamente después de las declaraciones de variables léxicas como LOCAL o STATIC. Por ejemplo:
// Permitido // No permitido
PROCEDURE MiProc PROCEDURE MiProc
LOCAL cCadena PARAMETERS p1, p2
PARAMETERS p1, p2 LOCAL cCadena
<Código> <Código>
RETURN RETURN
Nota: En Xbase++, los parámetros formales no deben declararse usando PARAMETERS. En cambio, una lista de
parámetros separadas por comas debería escribirse dentro de paréntesis cuando se declaran procedimientos,
funciones o métodos:
Operador Alias
En Xbase++, sólo una variable de identificación o el operador de macro puede especificarse luego del operador de
alias para variables de memoria dinámica M-> o MEMVAR-> y variables de campo FIELD->. Una expresión, con o
sin paréntesis, no puede usarse en estos casos.
En este caso, una "evaluación previa" es forzada porque el bloque de códigos se crea primero como una cadena
de caracteres y luego como un macro expandido. El bloque de código contiene entonces una referencia a la va-
riable de campo actual y no a la variable PRIVATE cNombreCampo .
La creación de nombres de archivo con números consecutivos como extensiones puede resolverse en Clipper de
la siguiente manera:
Esta expresión de macro no es valida en Xbase++. Debe recodificarse, por ejemplo, asignando un nombre de
archivo a una variable y luego usando la variable como un parámetro de comandos:
Si se reporta un error de sintaxis cuando un programa de Clipper se compila en Xbase++, la salida del prepro-
cesador de Xbase++ debe chequearse para verificar si ha generado el código correcto. La salida del preproce-
sador puede escribirse en un archivo PPO usando la opción del compilador /p.
USE &(Diccionario->cNombreArchivo)
La línea de arriba puede usarse en Clipper para abrir en archivo (cNombreArchivo) cuyo nombre se guarda en una
base de datos (diccionario). El preprocesador de Xbase++ convierte &(...) en una cadena de caracteres. El código
correcto de Xbase++ es:
USE (Diccionario->cNombreArchivo)
Operador de Arreglo [ ]
El operador de arreglo no esta restringido a los valores del tipo de datos Array, pero puede usarse también para las
cadenas de caracteres. En este caso, el operador recupera un único carácter en una posición especifica. Esto es
mucho más rápido que recuperar un único carácter con la función SubStr().
LOCAL cNombre := "Xbase++"
Operador de subcadenas $
El operador de subcadenas no está restringido a valores de tipo de datos Carácter, pero pueden usarse para
chequear el valor del tipo de dato Array. El operador busca el valor del operando izquierdo en el arreglo y de-
vuelve .T. (verdadero) si se encuentra el valor, o de lo contrario devuelve .F. (falso). Esto es un código válido en
Xbase++, pero no en Clipper:
Operador Referencia @
Clipper admite lo siguiente:
x := IIf( y, @param, z )
Cuando se compila este código y ejecuta bajo Clipper, x contiene referencias al parámetro si y es igual a .T.
(verdadero). Este no es el caso en Xbase++.
De lo contrario, con Xbase++, cualquier variable puede pasarse por referencia a las funciones, procedimientos y
métodos. Esto incluye elementos de arreglo, variables de miembro y variables de cambios. La siguiente llamada a
función se permite en Xbase++ pero no en Clipper:
MiFunc( @Clientes->Nombre )
MiFunc( @oTBrowse:colorSpec )
MiFunc( @aArreglo[3,12,5] )
#define __XPP__
El compilador de Xbase++ define la constante __XPP__. Esto permite el mantenimiento de diferentes códigos
fuente dentro de un archivo PRG único cuando debe compilarse usando Clipper y Xbase++.
#ifdef __XPP__
<Código Xbase++>
#else
< Código Clipper>
#endif
#pragma
El compilador de Xbase++ conoce pragmas que no existen en Clipper. Un pragma es una directiva que cambia
parámetros de compilación durante el tiempo de compilación. Esto permite que se active o desactive un parámetro
de compilación particular para una única línea en un archivo PRG.
Usar el parámetro /z del compilador para deshabilitar la optimización de acceso rápido cuando se compila con
programas de Clipper '87 con Xbase ++. Como alternativa, la optimización de la clave de acceso rápido puede
activarse|desactivarse para una sola línea de código usando #pragma Shortcut(OFF|ON) .
Nombres de Alias: Con Clipper '87, el nombre de alias que consiste solo de una única letra, puede especificarse
en el comando USE. Con Xbase ++, esto se permite solamente para las letras que le siguen a "M". Por ejemplo, las
siguientes líneas no pueden usarse con Xbase++ porque acarrean un error a partir del tiempo de ejecución:
SELECT A
USE Prueba1
SELECT K
USE Prueba2
En general, se recomienda usar los nombres de alias con más de una palabra.
Consultando al teclado en esta forma conduce a una encuesta permanente debido a que no hay un estado de
espera en el ciclo. Esto es admitido bajo DOS pero contradice la arquitectura de un sistema operativo con derecho
de prioridad. El ciclo antedicho debe codificarse con Xbase++ así:
Ahora, el ciclo tiene un estado de espera de una décima de segundo. Esto es suficiente para garantizar que todos
los threads de un programa sean ejecutados. Un programa Clipper debe buscarse para ciclos DO WHILE que
hacen encuestas permanentemente y no proveen un estado de espera. Algunas funciones para esto es la función
Sleep() .
La siguiente tabla da una vista general de los comandos que pueden conducir a incompatibilidades cuando un
programa de Clipper se compila usando Xbase ++.
Cuando se usan los comandos para importar y exportar datos, las características de un Motor de Base de Datos
(DataBaseEngine - DBE) de Xbase++ deben tenerse en consideración. Los DBEs se cargan implícitamente
cuando se utilizan los comandos. Esto se aplica a los comandos como COPY TO ... SDF o APPEND FROM ... SDF.
El archivo de importación o exportación, respectivamente, es mantenido por el Motor de Base de Datos que se
carga en la memoria si no se encontraba en ella. En el caso de un formato de SDF éste es el Motor de Base de
Datos de formato SDF. Crea un archivo de estructura extendida con la extensión 'SDF'. Por esta razón, no se
permite el código siguiente:
USE Clientes
COPY TO Clientes.sdf SDF
En este caso, el Motor SDFDBE de Xbase++ crea implícitamente el archivo de estructura extendida 'Cliente.sdf'.
Tiene el mismo nombre que el archivo destino. Como resultado, se produce un error durante el tiempo de ejecu-
ción. En este punto se recomienda a todos los programadores Clipper leer las especificaciones del Conceptos
Básicos de Motores de Bases de Datos en la documentación de Xbase++. Los DBEs de Xbase++ difieren en
muchos aspectos de los RDDs de Clipper.
Xbase++ difiere de Clipper en que las entradas del teclado son registradas en la cola de eventos. El comando SET
TYPEAHEAD TO 0 en Xbase++ causa que todos los eventos de la cola se borren. El numero de eventos en la cola
de eventos no puede establecerse en 0 (cero) ya que esto conduciría a una suspensión del sistema. El mínimo
número de eventos que pueden guardarse en la cola de eventos es 10 (diez).
El comando KEYBOARD se traduce a un evento de teclado que se agrega a la cola de eventos. El carácter Chr(0)
es un evento invalido y se lo ignora. Esto puede conducir a problemas en los programas de Clipper que usan Chr(0)
para establecer el código de retorno de la función Lastkey() en un valor definido. Si este es el caso, debe usarse
otro carácter para este propósito, por ejemplo Chr(255). El preprocesador da una alternativa también:
El formato de archivo MEM no esta respaldado por Xbase++. Esto significa que los archivos MEM existentes no
pueden leerse. En Xbase++, los archivos XPF reemplazan a los archivos MEM. Los objetos, arreglos y bloques de
códigos pueden guardarse usando este formato de archivo junto con los tipos de datos carácter, fecha, numérico y
lógico. Esto abre completamente una nueva dimensión para la comunicación entre las estaciones de trabajo en
una red. Empleando el formato de archivo XPF, los bloques de código pueden intercambiarse entre dos estaciones
de trabajo.
En Xbase++, la función AEval() acepta un quinto parámetro que es un valor lógico. Este especifica si un elemento
del arreglo o un valor lógico por referencia. La siguiente línea crea un arreglo y llena los 10 elementos con sus
correspondientes índices:
Las funciones CurDir() y File() se expandieron funcionalmente en Xbase++ (compararla a Clipper). La función
adicional CurDrive() se incluye también en Xbase++. El directorio actual y la unidad pueden modificarse usando
CurDir() y CurDrive() sin usar el comando RUN. Puesto que RUN opera de manera diferente bajo Xbase++ ini-
ciando otro interprete de comando, la técnica de Clipper no cambiara el directorio en la aplicación bajo Xbase++.
La función File() acepta un tipo de archivo como segundo parámetro. A continuación se muestra un ejemplo de la
función del archivo mejorado del código que chequea la existencia del directorio:
cDir := Space(64)
@ 10, 10 GET cDir
READ
IF ! File( Trim(cDir), "D")
Alert( "Directorio No válido")
ELSE
CurDir( cDir )
ENDIF
Se precede con un guión bajo a los nombres de los métodos :assign() y :end() de las clases Get y TBrowse en
Xbase++. Esto se concreta porque se reservan palabras claves para los identificadores previos. No es necesario
hacer cambios en los programas de Clipper porque el preprocesador de Xbase++ hace dicho cambio.
Clipper requiere la variable de instancia oGet:subScript para obtener las referencias correctas a los elementos del
arreglo utilizados como variables en el comando @...GET. Xbase++ usa, en cambio, el operador de referencia @
puesto que los elementos únicos del arreglo pueden pasarse por referencia. Por esta razón, oGet:subScript es
obsoleto y siempre contiene NIL. Para que se produzca en Xbase++ el mismo comportamiento que en Clipper,
debe asignarse un valor apropiado a oGet:subScript . Por ejemplo:
Hay una diferencia esencial entre la clase TBrowse en Xbase++ y Clipper. Las asignaciones a la variable de
instancia :rowPos no sincronizan automáticamente con el puntero de registros en la base de datos. Si el cursor
TBrowse se reposiciona cambiando :rowPos , el puntero de registros de la fuente de datos debe moverse explí-
citamente. Además, el objeto TBrowse se pasa como un parámetro a los bloques de cód i-
gos :skipBlock , :goTopBlock y :goBottomBlock .
También se han agregado métodos a la clase Get y a la clase TBrowse, que se requieren para respaldar el control
del mouse.
SET EXCLUSIVE
Valor Predefinido es ON Valor Predefinido en OFF
SET EXACT
No se considera con Se considera con
SEEK / DbSeek() SEEK / DbSeek()
CREATE INDEX
La secuencia de La secuencia de ordenamiento de caracteres
ordenamiento de caracteres depende de SET COLLATION
depende de los módulos y SET LEXICAL
NTX???.OBJ que deben estar
enlazados
COMMIT se ejecuta
implícitamente al final No se ejecuta implícitamente
del programa
SET EXCLUSIVE
SET EXCLUSIVE está predeterminado en OFF en Xbase++. Esto significa que por predeterminación, s e abren las
bases de datos en modo SHARED para una operación multi-usuario (red) en Xbase++. Si una aplicación de
Clipper que no fue diseñada para acceso multi-usuario se recompila bajo Xbase++, el comando SET EXCLUSIVE
ON necesita insertarse en la rutina de inicio antes que se abra cualquier base de datos.
SET EXACT
Cuando se usan SEEK y DbSeek() para buscar valores de carácter en una base de datos, Xbase++ usa el valor de
SET EXACT para determinar si los espacios en blanco al final de la cadena de caracteres debería ignorarse
cuando se comparan los valores de caracteres.
CREATE INDEX
Xbase++ soporta tablas de peso. Las tablas de peso asignan factores de peso a caracteres simples. Esto permite
que el ordenamiento de los caracteres sea definido por el usuario. Una tabla de peso se utiliza para comparación
de cadenas así como la creación de índices. Una tabla de peso es extremadamente importante cuando se crean
archivos de índice para acceder a los mismos desde Xbase++ y Clipper. En este caso, Xbase++ debe usar la
misma tabla de peso que Clipper. En Clipper, el orden de los caracteres se define en el tiempo de enlace por un
módulo de nación y no puede cambiarse durante el tiempo de ejecución.
Clipper incluye varios archivos NTX???.OBJ que deben enlazarse al EXE con el fin de definir el ordenamiento
pais-específico de caracteres. Para alcanzar el mismo sentido de ordenamiento con Xbase++, la tabla de peso
correspondiente debe activarse usando el comando de SET COLLATION TO. La siguiente tabla lista todas las
diferencias específicas del lenguaje entre Clipper y Xbase++.
El acceso de archivos de índice desde Xbase++ y Clipper al mismo tiempo requiere usar el mismo sentido de
ordenamiento para los caracteres. Cuando un programa de Clipper usa un sentido de ordenamiento diferente que
el de un programa de Xbase++, archivos de índice se corromperá tarde o temprano.
CREATE FROM
El formato de un archivo DBF de estructura extendida es diferente en Xbase++. La longitud del campo FIELD_LEN
es 5, no 3 como en Clipper. Esto permite el ingreso de una máxima longitud posible de campo directamente. No
será necesario usar el campo espacio decimal para ingresar longitudes de campo mas largas que 999 caracteres.
Archivos Memo
El máximo numero de caracteres que puede guardarse en un campo memo no esta limitado a 64 KB bajo Xbase++.
Si se acceden campos memo en un entorno multi-usuario desde Clipper y Xbase++, la limitación de 64 KB debe
implementarse en Xbase++.
RETURN y QUIT
Un parámetro numérico opcional puede especificarse para QUIT. Se lo transfiere a la función ErrorLevel() antes de
que termine una aplicación. Lo mismo puede alcanzarse si la función Main() devuelve in valor numérico con la
declaración RETURN.
Otras diferencias
Clipper Xbase++
Sin procedimiento Main PROCEDURE Main debe ser el primer
procedimiento de la aplicación
Procedimientos INIT Implícitos en:
APPSYS.PRG
ERRORSYS.PRG ERRORSYS.PRG
RDDSYS.PRG DBESYS.PRG
El método :init() de la superclase debe llamarse por este "molde" explícito en Xbase++. Cualquier método visible
en cualquier superclase puede llamarse explícitamente especificando el nombre de la clase en la llamada al
método.
Otra diferencia importante entre Class(y) y Xbase++ es en efecto que los métodos de clase y las variables de clase
pueden accederse a través del objeto de clase bajo la Class(y). Bajo Xbase++, cada instancia de una clase tiene
acceso a métodos de clase y a variables de clase. La llamada a un método de clase puede darse vía un objeto
(instancia) de una clase en Xbase++. Las variables de clase son accesibles a través de instancias. Xbase++ no
requiere la declaración de MESSAGE...TO CLASS en la clase, para delegar variables de clase y llamadas de
métodos a la clase objeto.
Ya que la sintaxis de Xbase++ para declarar clases y métodos es tan similar a la sintaxis de la biblioteca Class(y),
la migración del código de la Class(y) a Xbase++ es relativamente fácil con ayuda del preprocesador. Como
ejemplo, las directivas del preprocesador incluidas debajo traducen el código de la Class(y) a Xbase++:
/* Class(y) Xbase++ */
#ifdef __XPP__
#command CREATE CLASS <x> [FROM <y>] => CLASS <x> [FROM <y>]
#command MESSAGE <x> METHOD <y> => METHOD <x> IS <y>
#command CLASS MESSAGE <x> METHOD <y> => CLASS METHOD <x> IS <y>
// Nombre de la superclase
#xtrans :super => :<superClassName>
#else
#include "Class(y).ch"
#endif
Si el código de la biblioteca Class(y) ha de ser portado a Xbase++, el preprocesador puede usarse para acomodar
diferencias en la sintaxis. La directiva #ifdef admite la compilación del mismo código usando tanto Clipper/Class(y)
o Xbase++. El archivo PRG conteniendo la declaración de la Class(y) solo necesita compilarse usando las direc-
tivas del preprocesador que traducen el código existente al código valido de Xbase++.
Usando el mouse
El primer paso para migrar programas existentes de Clipper a un sistema operativo de 32 bit es integrar el mouse
para la navegación del programa. En Xbase++, todas las funciones y comandos del diálogo pueden controlarse
usando el mouse. Esto incluye @...SAY...GET, MENU TO, Alert(), TBrowse(), y Memoedit(). Para proveer soporte
para el mouse la función SetMouse(.T.) necesita insertarse en la rutina de inicio del programa. Si se incluye esto, la
función AppEvent() se utiliza para recuperar eventos en vez de la función Inkey(), que lee solo lo que ingresa del
teclado. Ambas funciones devuelven un valor numérico correspondiente tanto al código de tecla de la tecla pre-
sionada (Inkey()) o la identidad del último evento que ocurrió (AppEvent()). Con Clipper, Inkey() es la función usada
generalmente para recuperar lo que ingresa a través del teclado. Esta función sólo existe para compatibilidad en
Xbase++. Xbase++ reemplaza a Inkey() con la función AppEvent() para leer eventos de la cola de eventos.
Los valores de retorno de AppEvent() e Inkey() pueden ser diferentes cuando se presiona la misma tecla, porque el
código de DOS Inkey() no es compatible con el código de AppEvent(). Por esta razón, la transición al soporte del
mouse usando SetMouse(.T.) también requiere que las llamadas a SetKey() sean reemplazadas por llamadas a la
función SetAppEvent(). Los Bloques de códigos que están asociados a teclas usando SetKey() deben asociarse a
la misma tecla usando SetAppEvent(). La constante #define utilizada para identificar teclas también es diferente.
Por ejemplo:
SetKey( K_F10, {|| HacerAlgo() } ) // Clipper
SetAppEvent( xbeK_F10, {|| HacerAlgo() } ) // Xbase++
La primera línea contiene al código Clipper. Esta llamada asocia a la tecla de función F10 a un bloque de códigos.
Este código también trabaja correctamente en Xbase++ mientras el mouse no este activo. Luego que se llama a
SetMouse(.T.) para activar al mouse, el bloque de códigos {|| HacerAlgo() } no será ejecutado cuando se presiona
la tecla F10. Para garantizar la ejecución del bloque de códigos apropiado después de que se haya presionado la
tecla F10, el bloque de códigos debe reasociarse al evento de código xbeK_F10 mas que al código de tecla K_F10
usando SetAppEvent().
La ventana principal se divide en tres secciones: barra del menú, paleta de herramientas y línea de estado. Todas
las funciones del diseñador de formularios pueden seleccionarse vía la barra del menú. Las funciones principales
se integran en la paleta de herramientas y son accesibles clickeando los íconos correspondientes con el ratón.
Para un uso confortable, los íconos están contenidos en tres páginas de solapa (XbpTabpage).
La ventana principal se complementa por ventanas suplementarias que se abren vía el sistema del menú y no se
visualizan permanentemente. La siguiente tabla da un vistazo:
Options
- Settings Define las opciones predeterminadas para un formulario y
para el diseñador de formularios
Help
- Help index (F1) Despliega la ayuda en línea del diseñador de formularios
Nota: el diseñador de formularios tiene ayuda en línea sensible al contexto que se activa presionando la tecla F1.
Para obtener información detallada, la ventana debe tener foco antes que se presione F1.
El diseñador de formularios tiene dos modos de posicionar al recuadro de inserción: orientado-a-pixel y orienta-
do-a-grilla. El modo de posicionamiento se selecciona en el diálogo de opciones: "Use grid" ("Usar grilla") que se
abre vía los ítems del menú "Options->Settings". Este diálogo define opciones para configurar al diseñador de
formularios. Una opción, por ejemplo, es el tamaño de la grilla usado para el posicionamiento. La grilla debe
desactivarse para posicionar un Componente de Xbase exactamente en la punta del puntero del ratón.
Mientras se diseña un formulario, normalmente es necesario reposicionar los Componentes de Xbase insertados,
agruparlos usando los elementos estáticos como las líneas o los marcos, o llevar a cabo otros cambios. El for-
mulario puede modificarse confortablemente por medio de un recuadro de selección que permite modificar la
apariencia completa del formulario con sólo algunos clics del ratón. El recuadro de selección selecciona uno o más
Componentes de Xbase para ser modificados. La posición y el tamaño del recuadro de selección definen la po-
sición y el tamaño de los elementos marcados. Si se le altera el tamaño al recuadro, diferentes opciones estarán
disponibles para definir cómo los Componentes marcados de Xbase deben escalarse. Estas opciones se selec-
cionan en la ventana "Options->Settings". Por ejemplo, el espacio entre los Componentes de Xbase puede es-
calarse, o su tamaño, o ambos. Si los controles empotrados en los Componentes de Xbase (hijos) deberían o no
escalarse, es configurable también. Una variedad de opciones de alineamiento estandarizadas puede seleccio-
narse en la ventana "Alignment" (Alineación). Por ejemplo, el espaciado entre Componentes Xbase puede ser
escalado, o su tamaño, o ambos. Todos los Componentes de Xbase marcados pueden centrarse en el recuadro
de selección o dárseles el mismo ancho sólo clickeando al ícono correspondiente.
Las relaciones padre-hijo entre los Componentes de Xbase pueden definirse o modificarse fácilmente. Las op-
ciones varias existen para este propósito en el menú de contexto del formulario que se abre por un clic con el botón
derecho del ratón.
Los Componentes de Xbase se agrupan usando un Recuadro de Grupo por predeterminación (un elemento
XbpStatic del tipo XBPSTATIC_TYPE_GROUPBOX). Se lo utiliza como el padre para los Componentes de Xbase.
Después de la definición de un grupo sólo el padre puede marcarse de esta manera, y no los Componentes
empotrados de Xbase++. Para seleccionar o modificar los hijos de un Componente de Xbase, es necesario liberar
la relación padre-hijo.
Clickeando los ítems del menú "Options->Property Monitor" se abre una ventana que se utiliza para la visualiza-
ción de la jerarquía padre- hijo y se la equipa con un browser que despliega las propiedades del Componente
actual de Xbase. Si se modifican las propiedades en el browser el Componente de Xbase se actualiza con datos
nuevos.
Cuando se diseña completamente un formulario, puede almacenarse opcionalmente o puede generarse un código
en ambos estilos: funcional u orientado a objeto. Este código puede modificarse aún más usando un editor de texto.
En general se recomienda almacenar el formulario en caso que necesite modificarse posteriormente, o para que
sirva de templado para otros formularios. El diseñador de formularios almacena un formulario en archivos binarios
que pueden cargarse en un punto tardío en el tiempo. Se selecciona en el sistema del menú ("Form ->FUNCTION
Code" (Código orientado a Funciones) o "Form->CLASS Code" (Código Orientado a Objetos)) el tipo de código
fuente que genera el diseñador de formularios. Como alternativa, el tipo predeterminado se crea clickeando un
ícono en la paleta de herramientas. El tipo de código fuente predeterminado se selecciona en la ventana Settings.
El código FUNCTION
Antes que el diseñador de formularios genere el código fuente, es necesario ordenar los Componentes de Xbase
en el formulario. Deben aparecer en el orden que corresponda a la secuencia a la que accede un usuario usando
la tecla TAB. Normalmente el código fuente para los Componentes de Xbase++ se crea en la misma secuencia en
la que se lo inserta en el formulario. Esta secuencia puede no ser equivalente a la interacción con el usuario.
Puede chequearse o modificarse en la ventana de Secuencia (ítems del menú: Edit->Sequence). El código fuente
debería crearse sólo si la secuencia de Componentes de Xbase corresponde con la interacción con el usuario.
Un formulario de ejemplo en el directorio ..\SAMPLES\BASICS\XPPFD que fue creado por el diseñador de for-
mularios. Se lo utiliza como ejemplo para dar más explicaciones y se lo muestra debajo:
El formulario contiene dos XbpSLEs agrupadas para editar los campos de bases de datos. botones están orga-
nizados debajo de la caja Grupo para la navegación de bases de datos y para cerrar el formulario. Ambos tipos de
código fuente para este formulario, funcional y orientado a objeto, existen en el directo-
rio ..\SAMPLES\BASICS\XPPFD. Puede compilarse y ejecutarse. Para que el código fuente funciones correcta-
mente el diseñador de formularios hace algunas suposiciones simplificantes que deben considerarse cuando se
integra el código a un programa de aplicación.
El código funcional comienza con todas las directivas #include necesarias y declara el procedimiento Principal más
algunas variables LOCAL. Luego todas las bases de datos que son accesibles por un formulario se abren con el
comando USE. El formulario de ejemplo usa sólo una base de datos:
#include "Gra.ch"
#include "Xbp.ch"
#include "Appevent.ch"
PROCEDURE Main
LOCAL nEvent, mp1, mp2
LOCAL oDlg, oXbp, DrawingArea, oXbp1, aEditControls := {}
// Path está abreviado solamente en la documentación.
USE ..\SAMPLES\BASICS\XPPFD\CLIENTE.DBF NEW EXCLUSIVE
La variable LOCAL aEditControls se inicializa con un arreglo vacío. Se la utiliza para recolectar todos los Com-
ponentes de Xbase que acceden a los campos de una base de datos. Provee una forma cómoda de acceder a
todos los Componentes de Xbase en una función definida por el usuario que edita datos en el formulario.
Ya que diseñador de formularios no puede hacer suposiciones sobre las estrategias de bloqueo de un registro en
un entorno multi-usuario, se abren las bases de datos para acceso exclusivo. Por tanto, las líneas del programa
que abren las bases de datos con el comando USE están más inclinadas a modificarse cuando el código fuente
está integrado a un programa de aplicación.
Después de abrirse una base de datos, se crea el formulario como una instancia de la clase XbpDialog y se le
asigna a la variable LOCAL una referencia al área de dibujo.
Estas líneas de código definen padre, posición y tamaño del formulario más el tipo de borde, título de la ventana y
fuente. Este código necesita ser modificado probablemente, excepto por la posición y el tamaño. Especialmente la
ventana padre debería chequearse si es correcta. El diseñador de formularios asume que la ventana desktop será
empleada como padre y escribe la función AppDesktop() para recuperarlo.
La variable LOCAL drawingArea hace referencia al área de dibujo del formulario. Se la utiliza como padre para los
Componentes de Xbase subsecuentes:
Estas cuatro líneas de código crean la caja Grupo usada para agrupar la entrada a los campos en el formulario.
Como sirve de padre para el XbpSLE, se le asigna al objeto XbpStatic a una variable LOCAL separada oXbp1 .
Esta variable se usa en el código fuente para definir el padre para los objetos XbpSLE:
Cuando los Componentes de Xbase acceden a los campos de una base de datos pero no tienen descripción, un
objeto XbpStatic se crea siempre para desplegar texto a la izquierda del campo de entrada. El texto predetermina
al nombre del campo alineado a la derecha en caso mixto.
Para la lógica del programa, el bloque de códigos :dataLink es esencial ya que lleva a cabo el acceso a lectura y
escritura de un campo único. En el código fuente de estilo funcional, los literales aparecen en el bloque de códigos
para los nombres de campo y de alias. Por tanto, el código debe adaptarse, por ejemplo, cuando se especifica un
nombre de alias diferente en el comando USE.
La variable de instancia :bufferLength limita el número de caracteres que pueden ingresarse en el buffer de edición
de un objeto XbpSLE. Iguala la longitud del campo de la base de datos correspondiente, así un XbpSLE no podrá
editar más caracteres de los que pueden ser almacenados en un campo. Debido a que los caracteres blancos son
caracteres válidos para un XbpSLE, el bloque de códigos :dataLink debe remover los espacios precedentes
cuando lee el campo de una base de datos. Sino el buffer de edición está ya lleno luego del acceso a escritura y no
serán posibles los cambios cuando XbpSLE se encuentra en modo de inserción. Los caracteres pueden sobre-
escribirse solamente en este caso.
Nota: En el código funcional, la variable LOCAL oXbp siempre recibe la referencia al Componente de Xbase que
no se emplea como padre. En el ejemplo arriba, referencia primero un objeto XbpStatic. Después de la ejecución
del método :create() , se le asigna una referencia de un objeto nuevo a esta variable (oXbp := XbpSLE():new(...)).
Esto no sobreescribe o destruye el objeto XbpStatic. Cuando el método :create() finaliza, se le agrega el objeto
XbpStatic a la lista de hijos del padre, que es la caja Grupo referenciada en oXbp1 . Una referencia al objeto
XbpStatic puede recuperarse con la expresión oXbp1:childList()[1] . Luego de la creación de un objeto XbpSLE, se
le añade al arreglo aEditControls . Todos los Componentes de Xbase que acceden a los campos de bases de
datos vía el bloque de código :dataLink se le agregan a este arreglo y pueden accederse en un programa usando
esta variable.
El código fuente generado para la creación de los Componentes de Xbase puede ser bastante extenso y parecer
complejo al comienzo. No obstante, consiste sólo de un patrón simple que se repite:
Este patrón corresponde al "ciclo de vida" de los Componentes de Xbase. Se crea la instancia de una clase se crea
con el método :new() . La instancia (objeto) obtiene valores de configuración asignados. De acuerdo a la confi-
guración los recursos del sistema son requeridos desde el sistema operativo con el método :create() . Después
de :create() , el Componente de Xbase funciona y se lo referencia en la lista de hijos de su padre. Por tanto, la
variable LOCAL oXbp puede reutilizarse.
El diseñador de formularios genera códigos fuente estandarizados y libera al programador del cuantioso trabajo de
tipeo. El código fuente para el despliegue y la creación de un formulario, o para acceder a campos de bases de
datos (:dataLink ), puede estandarizarse. El límite del diseñador de formularios se alcanza, sin embargo, cuando
vuelve al flujo del programa o lógico. Esto no puede estandarizarse y constituye una tarea del programador
adaptar el código fuente generado con el fin de integrarlo en un programa de aplicación.
Pero en programación, hay situaciones comunes a muchas aplicaciones. Mover el puntero de registros es un
ejemplo. Para ello, un conjunto de Botones predefinidos pueden seleccionarse en el diseñador de formularios e
insertarse en el formulario. Las siguientes líneas de código son un ejemplo para un botón XbpPushbutton que
mueve el puntero de registros al siguiente registro:
En este caso, el programa se controla por el bloque de código :activate que se evalúa cuando se hace clic en el
botón. La función Gather() escribe los valores actuales desde el formulario en la base de datos. Después, el
puntero de registros se mueve por la función DbSkip() y finalmente los valores del nuevo registro se transfieren al
formulario con la función Scatter(). Los valores nuevos son visibles, entonces, en la pantalla.
Los aspectos más importantes del diseñador de formularios pueden resumirse así:
1. El diseñador de formularios genera el código fuente basado en la representación visual.
2. La secuencia de los Componentes de Xbase en el formulario debe ajustarse para que corresponda con la
interacción con el usuario.
3. El código fuente que controla el flujo del programa se crea en una extensión muy limitada. Hay sólo un
conjunto de botones que llevan a cabo tareas comunes con su bloque de códigos :activate .
4. El código fuente generado debe modificarse cuando se lo integra en un programa de aplicación.
El código CLASS
El diseñador de formularios de Xbase++ puede crear un código fuente para un formulario en el estilo orientado a
objeto. Esto incluye la declaración de una clase (cuyas instancias despliegan el formulario) más el código para los
métodos :init() y :create() . Para la creación del código orientado a objeto, las mismas reglas son válidas como para
el código funcional (referirse a la sección previa). Debe chequearse la secuencia de Componentes de Xbase en el
formulario. Además, los símbolos para las variables de instancia de la clase deben definirse. Si no están definidas,
el diseñador de formularios crea identificadores predeterminados para las variables de instancia desde el nombre
de la clase de un Componente de Xbase más un dígito, o usa los nombres de los campos como símbolos cuando
un Componente de Xbase accede a un campo de base de datos. Con el fin de obtener un código legible, los
nombres (símbolos) de las variables de instancia deberían editarse antes de generar el código fuente. esto se lleva
a cabo en la ventana de símbolos (ítems del menú: Edit->Symbols para iVar).
El código orientado a objeto saca ventaja de la herencia. Se crean dos archivos PRG, cada uno conteniendo el
código para una clase. El primer archivo contiene el nivel de implementación del formulario mientras que el se-
gundo provee al nivel de utilización. El código se divide en dos archivos y una clase (nivel de utilización) se deriva
a partir de la otra (nivel de implementación). Para el formulario del ejemplo discutido en la sección previa, existen
dos archivos en el directorio ..\SAMPLES\BASICS\XPPFD: SAMPLE2.PRG y _SAMPLE2.PRG. Ambos contienen
el código para crear una clase.
Nivel de Implementación
-----------------------
Archivo : _SAMPLE2.PRG
Declaración: CLASS _CustomerForm FROM XbpDialog
Nivel de Utilización
--------------------
Archivo : SAMPLE2.PRG
Declaración: CLASS CustomerForm FROM _CustomerForm
Para la creación del código de ejemplo se emplearon el nombre del archivo SAMPLE2.PRG y el nombre de la
clase CustomerForm . El diseñador de formularios prefija ambos nombres con un guión bajo y usa los identifi-
cadores resultantes para el código fuente que construye el nivel de implementación de un formulario. La clase del
nivel de implementación tiene variables de instancia opera cada Parte de Xbase contenida en el formulario.
Además, se declaran las variables de instancia para cada base de datos accedida más la variable de instan-
cia :editControls . el código fuente del formulario de ejemplo para el nivel de implementación se da a continuación:
METHOD init
METHOD create
ENDCLASS
Además de las variables de instancia, se declaran los métodos :init() y :create() . Ellos inicializan al formulario y
requieren recursos del sistema para él. El código fuente para dichos métodos se crea por el diseñador de for-
mularios. Ambos métodos tienen el mismo perfil de parámetros como otros Componentes de Xbase. Los valores
predeterminados para todos los parámetros se definen en el método :init() .
La tarea principal del método :init() es inicializar la superclase (XbpDialog) y todos los Componentes de Xbase
contenidos. El código para la inicialización de los Componentes de Xbase es similar al código funcional como
están descriptos en la sección previa. La única diferencia es que las referencias a los objetos se los asigna a las
variables de instancia en vez de a las variables LOCAL. Las siguientes líneas de código muestran una compara-
ción entre códigos funcionales y orientados a objeto que crean la caja Grupo en el formulario de ejemplo:
En el estilo funcional se utilizan las variables LOCAL oXbp1 y drawingArea , mientras que el código orientado a
objeto usa las variables de instancia ::GroupBox y ::drawingArea para almacenar referencias a Componentes de
Xbase. Se declara a ::GroupBox en la clase _CustomerForm y ::drawingArea es una variable de instancia de la
superclase XbpDialog (Nota: ::GroupBox es una abreviación de self:GroupBox , ::drawingArea es la abreviación de
self:drawingArea ).
el código que inicializa los Componentes de Xbase, que acceden a los campos de las bases de datos también es
similar en el estilo funcional y orientado a objeto:
En el código orientado a objeto, el objeto XbpSLE se referencia en la variable de instancia ::Apellido . Tiene el
mismo nombre, o símbolo, que el campo de base de datos editado por el objeto XbpSLE. Cuando una Parte de
Xbase accede a un campo, el diseñador de formularios usa el nombre del campo como identificador para la co-
rrespondiente variable de instancia por predeterminación.
El código para el bloque de códigos :dataLink también es diferente en el código orientado a objeto. El acceso a un
campo se lo programa como (Alias)->Fieldname y el área de trabajo se determina por los contenidos de una va-
riable de instancia. El código funcional usa al nombre de campo literal y alias como Alias->Fieldname .
La diferencia final entre el código funcional y orientado a objeto consiste en la llamada a los métodos :create()
y :setData() . En el código funcional, ambos métodos se llaman en la misma línea de programa. En el código
orientado a objeto, estos métodos se llaman en dos clases diferentes: el método :create() se lo llama en clase
_CustomerDialog (Nivel de implementación, _SAMPLE2.PRG) y :setData() se ejecuta en la clase CustomerDialog
(Nivel de utilización, SAMPLE2.PRG).
::CLIENTE := Select()
// Desplegar el formulario
::show()
RETURN self
El archivo _SAMPLE2.PRG contiene la declaración de clase para el formulario de ejemplo más el código requerido
para crear todos los Componentes de Xbase contenidos en el formulario. El archivo es el nivel de implementación
del formulario. Para utilizar un formulario es necesario abrir las bases de datos y transferir los valores desde el
registro actual al formulario. Esta es la tarea de una clase derivada. Se la programa en el archivo SAMPLE2.PRG
y provee el nivel de utilización del formulario.
El código fuente para el nivel de utilización puede (y debería) modificarse cuando se lo integra en un programa de
aplicación. Por ejemplo, el nombre del archivo de la base de datos aparece como el nombre literal luego del co-
mando USE e incluye la unidad y el camino. Esta línea definitivamente debe cambiarse para reflejar la situación en
un programa de aplicación. Si las bases de datos están o no abiertas en el método :create() , o ya estaban abiertas
cuando se llama a este método, debe ser una decisión del programador. Sólo asegura que el código generado
puede compilarse con el fin de probar el formulario en un archivo ejecutable aislado.
Para probar el código orientado a objeto de un formulario, es necesario declarar el procedimiento Principal (Main).
Esto lo realiza opcionalmente el diseñador de formularios cuando el parámetro correspondiente se establece en la
ventana de Settings (cuadro de confirmación: procedimiento Main para el código de clase). En este caso el pro-
cedimiento Main se escribe en el archivo que contiene el nivel de utilización de un formulario. En el ejemplo, este
es el archivo SAMPLE2.PRG. El procedimiento Main crea un formulario y procesa los eventos dentro de un ciclo
DO WHILE:
******************************************************************
* Procedimiento Principal para probar el formulario
******************************************************************
PROCEDURE Main
LOCAL nEvent, oXbp, mp1, mp2
CustomerForm():New():Create()
IF nEvent == xbeP_Quit
QUIT // AppQuit()
ENDIF
ENDDO
RETURN
#include "_SAMPLE2.PRG"
// EOF
La intención del procedimiento principal, como el generado por el diseñador de formularios, es probar un formu-
lario. Antes del ciclo de eventos, se instancia la clase sin siquiera asignar la referencia del objeto a una variable. La
expresión CustomerForm():New():Create() crea el formulario como fue definido en la clase y almacena la refe-
rencia del objeto en la lista hija de la ventana de aplicación (-> SetAppWindow():childList()[1]).
Para crear un archivo ejecutable que prueba un formulario, dos archivos PRG deben compilarse y enlazarse
(SAMPLE2.PRG y _SAMPLE2.PRG). Esto se consigue incluyendo al archivo que contiene el nivel de imple-
mentación (_SAMPLE2.PRG) en el archivo con el nivel de utilización (SAMPLE2.PRG). Cuando el compilador
compila al archivo SAMPLE2.PRG, el preprocesador ya ha procesado la directiva #include "_SAMPLE2.PRG" y
sólo un archivo necesita compilarse. Este archivo contiene la declaración para ambas clases, CustomerForm y
_CustomerForm .
Un ejemplo de como el código orientado a objeto puede modificarse es el archivo SAMPLE3.PRG que se localiza
en el directorio ..\SAMPLES\BASICS\XPPFD. La clase CustomerForm desde el SAMPLE2.PRG se modifica de
forma tal que el formulario puede usarse en un entorno multi-usuario. Se agregan tres métodos a la declaración de
clase y se implementa el código para ellos.
Antes que el puntero de registros se mueva, todos los datos cambiados en el formulario deben escribirse en la
base de datos. En un entorno multiusuario esto requiere que el puntero de registros sea bloqueado antes que
pueda escribirse en él. Esta lógica del programa se realiza como un código de aplicación específico en los nuevos
métodos :skip() , :setData() y :getData() y es la única diferencia entre los archivos SAMPLE2.PRG y
SAMPLE3.PRG. El método :init() asegura que se ejecuten los nuevos métodos:
Los botones contenidos en el formulario son referenciados en las variables de instancia que se declaran en la
clase _CustomerForm (_SAMPLE2.PRG). Se modifican los bloques de código :activate , entonces los nuevos
métodos serán ejecutados cuando un usuario clickea un botón. La implementación de los métodos nuevos re-
quiere sólo unas pocas líneas de código:
METHOD CustomerForm:setData
AEval( ::editControls, {|o| o:setData() } )
RETURN self
METHOD CustomerForm:getData
LOCAL lOk := ( AScan( ::editControls, {|o| o:changed } ) == 0 )
El método :skip() mueve el puntero de registros en este ejemplo sólo si los datos modificados en el formulario se
escriben a la base de datos. Esto, inclusive, depende del bloqueo exitoso de registros que se establece en el
método :getData() . Si los datos permanecen intactos, no se produce el bloqueo y el puntero de registros se mueve
siempre. Cuando el puntero de registros se modifica, el método :setData() transfiere los datos desde la base de
datos al formulario.
<archivo-PRG> indica el nombre de archivo del código fuente a compilar. Si el nombre del archivo se especifica sin
una extensión, se usa .PRG como una extensión de archivo. <Opciones> establece parámetros para controlar el
proceso de compilación. Un parámetro del compilador siempre comienza siempre con una barra ("/") o con un
guión ("-") (ver debajo).
El programa XPP.EXE contiene dos componentes: el preprocesador y el compilador actual. XPP.EXE traduce un
archivo de código fuente en formato ASCII a un archivo OBJ en formato binario. La traducción del código fuente
ocurre en dos pasos: Primero el preprocesador traduce el código del programa dentro del archivo PRG a código
modificado que el compilador "entiende". Luego el compilador crea un archivo OBJ que el enlazador puede enlazar
a un programa ejecutable.
El resultado del preprocesador puede salir como un archivo separado con la extensión PPO (PPO significa Salida
PreProcesador - PreProcessed Output) usando el parámetro de compilación /p. Un archivo PPO es un archivo
ASCII conteniendo el código que el compilador realmente compila. El preprocesador prepara el código del pro-
grama para el compilador, y éste último convierte el código preparado en un archivo OBJ.
XPP MAIN /s /p /q
El próximo ejemplo crea el archivo TEST.OBJ incluyendo información del debug. Se escribe el archivo OBJ en el
path \XPP\OBJ\. No se crea un procedimiento implícito MAIN() y se despliegan advertencias en la pantalla durante
la compilación:
Por ejemplo, si el mismo archivo PRG debe compilarse por Xbase++ y Clipper, pueden darse situaciones en las
que ambos compiladores se comporten de manera diferente. Estos conflictos pueden resolverse de la siguiente
forma:
#ifdef __XPP__
<Código Xbase++>
#else
<Código Clipper>
#endif
De esta manera se pueden mantener códigos del programa para diferentes compiladores en el mismo archivo
PRG.
Las constantes adicionales definidas por el compilador están disponibles para debugging y/o con propósito de
hacer versiones. Pueden usarse de la siguiente manera:
El compilador reemplaza estas constantes con la información correspondiente, o sea los valores de estas cons-
tantes se modifican cuando se compilan los archivos del código fuente.
__FILE__ Una cadena de caracteres conteniendo el nombre del archivo de código fuente.
__LINE__ Un valor numérico que indica el número de línea de la fuente donde aparece la constante.
__DATE__ Una cadena de caracteres conteniendo la fecha de compilación en el formato "MMM DD YYYY", con
MMM siendo el nombre abreviado del mes, DD es el dia y YYYY es el año.
__TIME__ Una cadena de caracteres conteniendo el tiempo de compilación en el formato HH:MM:SS.
Xppload mostrará el nombre de los archivos y la ubicación de los directorios de Xbase++ - archivos de ejecución
relevante.
En el modo de comando versión, XPPLOAD acepta los nombres de los archivos o listas de archivos.
XPPLOAD version XppRt1.dll
Busca a Xpprt1.dll, imprime el directorio en donde se usara el DLL y la versión. Si el directorio es suministrado
explícitamente, XPPLOAD tratara de encontrar el archivo allí y si encuentra la versión la imprimirá.
Una lista de archivos es un archivo de texto que contiene un nombre de archivo por línea, y pasado a XppLoad con
un signo precedente:
Si se ejecuta "Xppload Version" desde un archivo de lote o archivo make pueden verificarse sus códigos de re-
torno:
0 - ok
1 - error general
2 - error de línea de comandos
3 - error de versión DLL
4 - DLL no encontrada
El debugger exhibe primordialmente información de debug sobre el código fuente de la aplicación que está siendo
ejecutada y controlada por el debugger. Puede mostrarse cualquier variable de memoria y sus contenidos editados
durante el tiempo de ejecución usando el debugger. Esto es posible incluso cuando se ha declarado a la variable
LOCAL, STATIC, PRIVATE, o PUBLIC. El debugger puede mostrar la pila de llamadas que lista todas las fun-
ciones predeterminadas que define el usuario, procedimientos y métodos.
El debugger puede interrumpir la ejecución de una aplicación y controlarla por medio de los puntos de interrupción
o usando un modo de paso único (esto significa que el código fuente de una aplicación es procesado línea por
línea). Los puntos de interrupción detienen automáticamente la ejecución de la aplicación y reactivan el debugger.
Esto permite que se la ejecute paso a paso para limitar y localizar el lugar donde se produjo el error. El debugger
incluye también una línea de comandos donde pueden ejecutarse expresiones de Xbase++ dentro de una apli-
cación.
Se navega dentro del debugger usando el mouse o teclas de acceso rápido. De éstas últimas, las más importantes
son F8, F5 y la combinación de teclas Ctrl+S. F8 ejecuta la próxima línea del código fuente de la aplicación cuando
se está en el modo de paso único. La tecla F5 termina el modo de paso único del debugger y devuelve el control a
la aplicación hasta que el debugger se reactive usando Ctrl+S (aplicación stop) o se alcance algún punto de in-
terrupción. Todos los demás aspectos del debugger los maneja el menú. La actividad actual del debugger se
muestra por un área coloreada dentro de la barra del menú. El rojo significa que el debugger está inactivo y que la
aplicación está en ejecución. El verde significa que el debugger está activo y controla la aplicación.
Si la línea de código fuente excede el ancho de la ventana de código fuente, presione la barra espaciadora para
ver la línea completa.
Nota Importante: Cuando una aplicación se encuentra activa, ésta puede ser interrumpida usando la combinación
de teclas Ctrl+S. Si una aplicación está en un estado de espera no está ejecutando el código. Esto ocurre durante
WAIT, Inkey(0), y AppEvent(). Mientras la aplicación está esperando alguna entrada desde el teclado o algún
evento, el debugger no puede intervenir en la aplicación puesto que no se está ejecutando código en la aplicación.
En este caso, la ventana de la aplicación debe traerse al primer plano y apretarse una tecla si es necesario, así la
aplicación termina con el estado de espera. Sólo después el debugger puede ganar el control nuevamente.
El Menú de Sistema del Debugger
El menú principal del debugger es siempre visible como la línea superior de la ventana del debugger. Contiene los
siguientes ítems del menú:
File Activa el menú de archivos. Esto permite la navegación dentro de la ventana de código, seleccionar
archivos de aplicación del código fuente para mostrar en la ventana de código o terminar la función
del debugger.
Run Activa menú de ejecución. Las opciones del menú de ejecución controlan cómo se ejecuta la
aplicación, Reiniciar y cómo seleccionar otra aplicación de Xbase++ para debug.
Monitor Activa el menú del monitor. Esto abre o cierra la ventana del monitor para exhibir variables de
memoria y la ventana de la pila de llamadas para exhibir las funciones y los procedimientos activos.
Commands Activa el menú de comandos. Las funciones del menú de comandos controlan las ventanas de los
comandos del debugger y permiten que se establezcan o borren los puntos de interrupción.
Options Activa el menú de opciones. Este menú configura el debugger y permite que se guarde la confi-
guración actual.
Help Muestra una ventana de ayuda.
Menú File
El menú File (Archivos) contiene opciones que especifican el archivo del código fuente exhibidos en la ventana del
debugger y qué sección del código fuente es visible.
Open module Esta opción abre una ventana de selección de archivos que lista todos los archivos PRG
conteniendo el código fuente. Se hace una selección en la ventana usando la tecla Enter o
haciendo un doble clic sobre el ítem subrayado. El archivo PRG seleccionado es presen-
tado en la ventana código del debugger y pueden establecerse los puntos de interrupción
en este archivo usando la tecla F9 o Enter.
Los nombres de archivos serán desplegados con su camino completo (vista completa). La
tecla TAB conmuta entre esa vista y una vista de nombres de archivos (vista reducida).
Presionando una letra correspondiente a la primera letra de un archivo listado en esta vista
causará que el cursor salte al próximo nombre de archivo coincidente.
Puede abrirse también una ventana de selección. Contiene una lista de todas las funciones,
procedimientos y métodos comprendidos en un archivo PRG. Esta ventana de selección se
abre haciendo un clic en el botón derecho del mouse o usando la combinación de teclas
Ctrl+Return luego de la selección de un archivo PRG.
Goto Line Esta opción abre una ventana de entradas que permite la especificación de una línea de
números para que el debugger muestre en la ventana de código.
Goto Function Esta opción muestra el código fuente de cualquier función o procedimiento en la ventana de
código. Si el código fuente de la función no se encuentra en el archivo PRG que esta ex-
hibiéndose en ese momento en la ventana de código, el debugger abre el archivo PRG
apropiado. Si varias funciones existen con el mismo nombre (STATIC FUNCTION) el de-
bugger muestra el código fuente de la primer función que encuentra con el nombre espe-
cificado.
Si existen bastantes funciones con el mismo nombre (FUNCIONES STATIC) el debugger
desplegará una lista de archivos posibles donde la función conteniendo estas implemen-
tadas. Lo mismo se aplica al monitor de Pila de Llamadas.
Para ir a un método de una clase, ingrese el nombre de clase seguido de dos puntos y el
nombre del método, tal como "MiXbpMLE:init". Adicionalmente, para saltar a un número de
línea puede especificarse agregando el caracter "#" y el número de línea, tal como
"MAIN#567".
Where Esta opción muestra la línea de programa actual que se está ejecutando en la ventana de
código. Se carga al archivo PRG apropiado si ya no es visible. Esta opción cumple las
mismas funciones que "Open module" y "Goto ..." para reposicionar el código fuente en la
línea actual luego que la ventana de código es desplazada o se abre otra ventana.
Exit La opción termina con el debugger.
Menú Run
Este menú contiene opciones que controlan la aplicación de Xbase++. Esto incluye seleccionar y Reiniciar la
aplicación.
Restart Esta opción termina y luego reinicia la aplicación de Xbase++ actual. Todos los puntos de
interrupción establecidos y monitores activados permanecerán visibles luego del Reinicio.
Startup La opción termina la aplicación actual y abre una ventana por la que es posible entrar al
nombre de archivo de la aplicación. Esto permite hacer debugger de aplicaciones de Xbase++
diferentes sin terminar ni Reiniciar el debugger.
Step La tecla de acceso rápido para esta opción es F8. Tanto seleccionar la opción del menú cómo
presionar F8 ejecutan la línea del programa en el código fuente de la aplicación.
Trace La tecla de acceso rápido para esta opción es F10. Presionar F10 y seleccionar esta opción
del menú causan que todas las funciones y procedimientos llamados por la línea del programa
actual sean ejecutados completamente antes de que el debugger haga una pausa durante la
ejecución del programa en la próxima línea. Esto evita el paso a paso del código fuente de la
función o procedimiento llamador.
Go F5 es la tecla de acceso rápido para esta opción. Devuelve el control a la aplicación y apaga al
debugger. Este ultimo puede recuperar el control al ser reactivado con la combinación de te-
clas Ctrl+S o si se da un punto de interrupción. Si se presiona Ctrl+S, la ventana del debugger
recibe el foco, lo que significa que debe estar al frente. Si el debugger no muestra una "luz
verde" luego de Ctrl+S, la aplicación esta en la condición de estado de espera y no ejecuta
código alguno. En este caso la aplicación debe recibir un evento de forma tal que abandone el
estado de espera. La manera mas fácil de llevar esto a cabo es hacer un clic en la ventana de
la aplicación con el mouse y si es necesario presionar una tecla.
Goto Cursor F7 es la tecla de acceso rápido para esta opción. El debugger permite al código del programa
de la aplicación ejecutar hasta la línea en la ventana de código y luego interrumpe la ejecución
del programa. Esta opción del menú y la tecla F7 establecen la línea actual en la ventana de
código como un punto de interrupción temporario.
Step Back La tecla de acceso rápido para esta opción es F4. Esta opción emplea efectivamente la línea
después del procedimiento actual o función como punto de interrupción. Cuando se la pre-
siona, el procedimiento actual o función se ejecuta hasta el final. El debugger interrumpe el
programa tan rápido como termine la función actual. Luego posiciona el código en la ventana
de código en la próxima línea del código fuente que llamó a la función o procedimiento que
estuvo activo cuando se presionó F4.
Menú Monitor
Este menú incluye opciones para abrir ventanas para ver variables de memoria y/o la pila de llamadas. La in-
formación en las ventanas se actualiza automáticamente mientras que se está ejecutando la aplicación. Cuando la
ventana del monitor está activa, se puede seleccionar una variable con un doble clic y luego editarla. El archivo
PRG correspondiente se carga automáticamente en el código de la ventana cuando se hace doble clic con el botón
derecho a cualquiera de las funciones o procedimientos en la ventana de la pila de llamadas.
Un arreglo desplegará sus elementos, la salida es creada de acuerdo con Var2Char(), sin mostrar los tipos de
datos de cada elemento. Un bloque de código mostrará su representación literal y un objeto mostrará su nombre
de clase.
Menú Commands
La ventana de comandos se abre o cierra usando opciones de este menú. Esta ventana presenta el resultado de
todas las expresiones ingresadas en la línea de comandos del debugger. También registra la expresión ingresada
y su resultado. La flecha hacia arriba puede utilizarse para recuperar una expresión ingresada previamente que
puede ejecutarse nuevamente presionando Enter. Además del manejo de la ventana de comandos, el menú
también maneja puntos de interrupción. Un punto de interrupción designa una línea en el código fuente de la
aplicación en la que el programa se interrumpe automáticamente. Si se alcanza un punto de interrupción mientras
se esta ejecutando la aplicación, el debugger se reactiva y toma otra vez el control.
Toggle Breakpoint F9 es la tecla de acceso rápido de esta opción. Alternativamente se puede
presionar la tecla Enter. La opción define la línea actual en la código fuente
de la aplicación como un punto de interrupción o elimina todos los puntos de
interrupción previamente definidos en esta línea. Independientemente de que
se defina una línea como un punto de interrupción se la identifica con el ca-
racter ¯ , que aparece al comienzo de la línea del código fuente frente al
número de línea.
Delete All Breakpoints Esta opción borra todos los puntos de interrupción definidos en todos los
archivos de código fuente.
List Breakpoints Esta opción lista todos los puntos de interrupción definidos en todos los ar-
chivos de código fuente.
Open Command Window Esta opción abre la ventana de comandos. Las expresiones pueden ingre-
sarse luego en la línea de comandos del debugger. Cuando la ventana de
comandos fue abierta recientemente, esta opción del menú es reemplazada
con "Close Command Window".
Clear Command Window Esta opción borra todas las líneas previamente registradas en la ventana de
comandos.
Menú Options
Este menú admite la configuración de varios aspectos del debugger o que se guarde la actual configuración de
ventana. Los datos de la configuración se guardan en un archivo con la extensión .@@@ y el mismo nombre de
archivo que el archivo EXE. Si este archivo existe en el directorio actual lo lee el debugger y es empleado para su
configuración.
Debug Program Startup Si se selecciona esta opción el debugger también despliega el código del
programa ejecutado antes de la llamada al procedimiento Principal. Este des-
pliega las funciones AppSys(), DbeSys() y cualquier otra INIT PROCEDURE.
Check Time Stamps Esta función puede encenderse o apagarse. Si se la activa, el debugger
compara la estampa del tiempo de los archivos de código fuente de la aplica-
ción con la marca de tiempo en los archivos ejecutables y da consejos si estos
últimos (archivos DLL y EXE) son mas antiguos que alguno de los archivos de
código fuente.
Set Tab Width esta opción especifica el número de espacios en blanco utilizados para re-
emplazar caracteres de tabulación cuando se exhiben los códigos fuente en la
ventana de código del debugger.
Comandos Ejecutados al Arrancar!EF Si se establece esta opción, todos los
comandos ingresados en la ventana de comandos serán automáticamente
ejecutados la próxima vez que arranque usando la información guardada para
Reinicio (Save Restart Info).
Save Restart Info Esta opción guarda la configuración actual del debugger y todos los actuales
puntos de interrupción. El archivo creado tiene el mismo nombre de archivo
que la aplicación y la extensión ".@@@". Si se especifica XPPDBG -n <ar-
chivo EXE> en la línea de comandos, el debugger ignora la información
guardada y no carga al archivo @@@.
Menú Help
Este menú contiene opciones que muestran información de ayuda para el debugger.
Short Help Esta opción abre una pequeña ventana de ayuda y da teclas de acceso rápido para controlar
al debugger.
Dentro de la ventana de código debería resaltarse la línea donde ocurrió el error. Se establece un punto de inte-
rrupción haciendo doble clic en el botón izquierdo del mouse sobre la línea resaltada o presionando la tecla F9 o la
tecla Enter.
Una sesión de debug generalmente comienza con el establecimiento de puntos de interrupción. Puede estable-
cerse cualquier número de puntos de interrupción. La opción "Save Restart Info" puede seleccionarse en las op-
ciones del menú para guardar los puntos de interrupción desde la ejecución de una ejecución hasta la siguiente. El
número de líneas de los puntos de interrupción se guardan luego con el mismo nombre de archivo de la aplicación
y la extensión ".@@@".
Luego de establecer de puntos de interrupción la aplicación se ejecuta presionando la tecla F5. El área coloreada
en la barra del menú del debugger cambia de verde a roja. Esto indica que la aplicación y no el debugger tiene foco.
El debugger interrumpe la aplicación y recupera el control tan rápido como se alcance el punto de interrupción. El
área coloreada resaltada cambia entonces de rojo nuevamente a verde.
Inspeccionando errores
Ni bien el debugger interrumpió la aplicación en el punto de interrupción el estado actual de la aplicación puede
examinarse más de cerca. La ventana del monitor se abre a partir del menú Monitor para exhibir los contenidos de
todas las variables visibles. Las variables están organizadas basadas en su clase de almacenamiento (LOCAL,
STATIC, PRIVATE y PUBLIC). Si una variable posee un valor incorrecto puede cambiarse usando el debugger.
Para hacer esto, la ventana del monitor se elige haciendo clic con el botón izquierdo del mouse o utilizando la tecla
TAB. Dentro de la ventana Monitor debe posicionarse resaltando en la variable correspondiente. Una ventana de
edición se abre luego de hacer clic con el botón izquierdo del mouse o presionando Enter. El valor de la variable
puede editarse en esta ventana. La ventana de edición se cierra seleccionando OK.
Las variables con el tipo caracter, numérico, fecha o lógico pueden mostrarse en un campo de entradas en el cual
pueden editarse. Lo exhibido usa sintaxis de Xbase++. Los bloques de código no pueden desplegarse pero sí
modificarse. Cuando se cambia un valor, debe prestarse especial atención a la sintaxis. Por ejemplo, deben estar
presentes los delimitadores para una cadena de caracteres cuando se edita un valor del tipo de dato caracter. Si la
variable es un bloque de código, los caracteres {|| y } deben incluirse. Los caracteres ingresados son compilados
usando el operador de macro y se le asigna el resultado a la variable.
Si la variable que se edita es un arreglo, los contenidos de los elementos del arreglo pueden verse usando el botón
Ver. El máximo número de elementos desplegados es 64. Los elementos en un arreglo mas allá de este límite de
64 elementos puede examinarse ingresando el índice numérico para el primer elemento del arreglo que se
muestra en el campo de entrada "Start".
La ventana de edición presenta solo la información esencial sobre un objeto como el nombre de la clase al que el
objeto pertenece. Los contenidos de las variables de instancia de un objeto se exhiben presionando el botón Ver.
Solo se muestran las variables de instancia declaradas en la clase. Si la clase se deriva de otras clases, los
nombres de las clases de las superclases también se exhiben. Seleccionar una superclase despliega una ventana
de edición que contiene las variables de instancia para dicha clase.
Sólo las variables de instancia de los objetos pueden examinarse en la ventana de edición del debugger. En
muchos casos el valor de retorno de un método necesita chequearse. Por ejemplo, la pregunta podría ser "¨Cual
es el nombre de la clase del padre de esta Componente de Xbase?" El padre solo puede determinarse usando el
método.:setParent() : no aparece en la lista de variables de instancia. Los problemas como este se resuelven
usando la ventana de comandos del debugger. Para ver la ventana de comandos la opción "Open Command
Window" se selecciona para abrirla. Debajo de esta ventana se exhibe la línea de comando del debugger y puede
ingresarse cualquier expresión. Luego de presionar la tecla Enter se ejecuta la expresión y el resultado de la
misma se despliega en la ventana de comandos.
El padre de un Componente particular de Xbase puede determinarse en la línea de comandos del debugger de dos
maneras. Si la variable oXbp contiene un Componente de Xbase cuyo información paternal es requerida, cual-
quiera de la siguientes dos expresiones pueden emplearse en la línea de comandos del debugger para determinar
la clase de padre:
oXbp:setParent():className()
dummy := oXbp:setParent()
La primera expresión despliega el nombre de la clase del padre en la ventana de comandos. La segunda expresión
crea una variable PRIVATE referenciando al padre. Seleccionar una opción "Private" para la ventana del monitor
despliega variables de la clase de almacenamiento PRIVATE. El padre de oXbp se exhibe como la variable
"dummy". Esta variable puede examinarse con el debugger de la manera descripta anteriormente.
La salida del comando DIR se dirige al archivo PROYECTO.TXT. Este archivo puede modificarse con facilidad
usando un simple editor de textos para remover los nombres de los archivos PRG que se localizan en el directorio
actual pero no son parte del proyecto, por ejemplo. El ProjectBuilder (PBUILD.EXE) lee ese archivo y crea a partir
de él una plantilla para un archivo de proyecto con la extensión XPJ (esto significa Xbase++ ProJect).
Para pequeños proyectos, la estructura básica de un archivo de proyecto puede crearse también con un editor. El
siguiente ejemplo muestra un archivo de proyecto para el programa CLIENTE.EXE que necesita sólo cuatro ar-
chivos PRG:
01: [PROJECT]
02: DEBUG = yes // Definiciones que alcanzan a todo el proyecto
03: GUI = no
04: CLIENTE.XPJ // La raíz del proyecto
05:
06: [CLIENTE.XPJ] // Enunciar todos los archivos EXE y DLL
07: CLIENTE.EXE // del proyecto aquí
08:
09: [CLIENTE.EXE] // Listar todos los fuentes para cada
10: CLIENTE.PRG // archivo EXE y/o DLL separadamente
11: GETCLIE.PRG
12: PRINT.PRG
13: VIEWCLIE.PRG
El ejemplo muestra las principales características de un archivo XPJ: está dividido en diferentes secciones, cada
una comenzando con un nombre simbólico entre corchetes cuadrados. La primer sección debe siempre nom-
brarse [PROJECT]. Es el punto de entrada para el ProjectBuilder y lista las definiciones válidas para el proyecto
completo. En este ejemplo, un programa que contiene información del debug se crea como una aplicación a modo
de texto (líneas 2 y 3). Al final de las definiciones de lista (línea 4), aparece el nombre de la próxima sección
analizada por el ProjectBuilder. Esta sección es la raíz del proyecto y lista todos los archivos ejecutables que son
parte del proyecto, o que deben redistribuirse más tarde a los clientes, respectivamente. Esto incluye ambos ar-
chivos EXE y DLL. Cada nombre de un archivo ejecutable de los archivos fuentes se emplea nuevamente como el
nombre de otra sección que lista los nombre de los archivos fuente a partir de los cuales se crea el archivo eje-
cutable.
Como resultado la estructura de un archivo XPJ representa las dependencias que existen entre los archivos fuente
y objetivo de un proyecto. Por ejemplo, el archivo mencionado anteriormente CLIENTE.EXE es un archivo objetivo
que depende de cuatro archivos PRG, o fuentes, respectivamente. Cuando se modifica un archivo, el objetivo
debe actualizarse. El ProjectBuilder analiza las dependencias entre fuente y objetivo usando un archivo de pro-
yecto y lo actualiza para reflejar los últimos cambios.
El ejemplo arriba muestra la estructura básica de un archivo XPJ como puede crearse fácilmente con un editor. No
obstante, muchas más dependencias pueden existir entre los archivos del mismo proyecto. Esto incluye, por
ejemplo, las dependencias entre CH->PRG, PRG->OBJ, OBJ->EXE u OBJ->DLL y los archivos DEF->LIB. Pro-
jectBuilder puede detectar todas las dependencias automáticamente una vez que se crea la estructura básica de
un archivo de proyecto. Para ello, se inicia a PBUILD.EXE con el parámetro /g (g por "g"enerar):
PBUILD CLIENTE.xpj /g
El parámetro /g causa que PBUILD.EXE analice todas las dependencias que existen dentro de un proyecto.
ProjectBuilder expande entonces un archivo de proyecto (rudimentario) y agrega toda la información faltante al
archivo XPJ. Esto libera al programador de bastante tipeo, especialmente en grandes proyectos. Por ejemplo,
PBUILD.EXE expande al archivo XPJ de ejemplo por 20 líneas. Como resultado, el archivo de proyecto crece
desde 11 a 31 líneas:
01: [PROJECT]
02: COMPILE = xpp // Compilador faltante y enlazador
03: COMPILE_FLAGS = /q // Se agrega información
04: DEBUG = yes
05: GUI = no
06: LINKER = alink
07: LINK_FLAGS =
08: RC_COMPILE = arc
09: RC_FLAGS =
10: CLIENTE.XPJ
11:
12: [CLIENTE.XPJ]
13: CLIENTE.EXE
14:
15: [CLIENTE.EXE] // Creado automáticamente
16: // $START-AUTODEPEND // dependencias
17: COLLAT.CH
18: GET.CH
19: MEMVAR.CH
20: PROMPT.CH
21: SET.CH
22: STD.CH
23: CLIENTE.OBJ
24: GETCLIE.OBJ
25: PRINT.OBJ
26: VIEWCLIE.OBJ
27: // $STOP-AUTODEPEND
28: CLIENTE.PRG
29: GETCLIE.PRG
30: PRINT.PRG
31: VIEWCLIE.PRG
Los cambios aparecen en la sección [PROJECT], donde se agrega toda la información faltante para compilar y
enlazar. Además, la sección [CLIENTE.EXE] contiene ahora una lista de los archivos para el que se ha detectado
el ProjectBuilder. Esta lista automáticamente creada está encerrada entre los marcadores //
$START-AUTODEPEND y // $STOP-AUTODEPEND en las líneas 16 y 27. Ambos marcadores indican un área en
el archivo de proyecto que debe cambiarse automáticamente cuando se llama a PBUILD.EXE subsecuentemente
junto con el parámetro /g. Por tanto, un archivo de proyecto no debería alterarse entre los marcadores porque los
cambios en esta área pueden perderse.
Creando un proyecto
Cuando se genera un archivo de proyecto incluyendo todas las dependencias, se crea un proyecto, o actualiza,
respectivamente, invocando PBUILD.EXE y especificando al archivo de proyecto apropiado, si es necesario. Por
omisión, ProjectBuilder busca al archivo PROJECT.XPJ en el directorio actual. Por tanto, cada una de las si-
guientes posibilidades crea un proyecto:
1) PBUILD
2) PBUILD CLIENTE
3) PBUILD CLIENTE.prj
En la primer llamada, el ProjectBuilder crea al proyecto que se describe en el archivo PROJECT.XPJ. La segunda
llamada requiere que un archivo CLIENTE.XPJ esté disponible en directorio actual (XPJ es la extensión prede-
terminada para los archivos proyecto), mientras en la tercer llamada, el nombre de archivo completo especifica el
archivo de proyecto. Después de invocarlo, ProjectBuilder analiza las dependencias entre los archivos fuente y
final. Si se modifica un archivo fuente desde la última llamada a PBUILD.EXE, por ejemplo. si los archivos fuente
son más nuevos que los archivos objetivo, el ProjectBuilder crea los objetivos correspondientes una vez más
(archivos EXE o DLL). Puesto que sólo los fuentes son tratados por el ProjectBuilder, el tiempo para actualizar los
proyectos complejos es por tanto reducido en una gran extensión.
COFF_LINKER= Esta definición opcional puede usarse para definir al enlazador que debe invocarse por el
ProjectBuilder cuando se establece OBJ_FORMAT= en COFF (Formato Común de Ar-
chivo Objeto). Si COFF_LINKER= no se define en este caso, el ProjectBuilder utiliza al
enlazador definido con LINKER=.
COMPILE= Esto indica el nombre del compilador de Xbase++. Es siempre XPP.
COMPILE_FLAGS= Todos los parámetros de los compiladores a establecer para la compilación se definen aquí.
Note las definiciones separadas OBJ_DIR= y DEBUG= existen para los parámetros /o y /b.
DEBUG= Esta definición puede establecerse en YES (versión con debug) o NO (versión sin debug).
Un archivo ejecutable se crea así con o sin información del debug. Un archivo ejecutable
debe crearse con DEBUG=YES para monitorearse con el debugger.
GUI= Si un programa debe ejecutarse como una aplicación a modo de texto, debe establecerse
GUI=NO. cuando un programa lleva a cabo una salida gráfica, ya sea funciones Gra..() o
usando los Componentes de Xbase, debe definirse GUI=YES.
LINKER= Esta definición indica el nombre del enlazador que se emplea para crear los archivos EXE o
DLL desde los archivos OBJ. Este es el enlazador enviado con la versión de Xbase++
específica del sistema operativo.
LINK_FLAGS= Todas las banderas del enlazador que no están cubiertas por GUI= y DEBUG= se listan en
esta definición. Sin embargo, si se emplean las banderas /PM y /DE, ellas sustituyen a las
definiciones correspondientes GUI= y DEBUG=.
OBJ_DIR= Opcionalmente, puede definirse el directorio para los archivos OBJ. El compilador crea los
archivos OBJ en este directorio y el enlazador hace una búsqueda en el mismo de los
archivos. Sólo un directorio puede establecerse para cada objetivo.
Nota: La configuración OBJ_DIR afecta sólo el archivo OBJ listado en la sección del ar-
chivo de proyecto delimitado con // $START-AUTODEPEND y // $STOP-AUTODEPEND.
PBuild completa estos nombres de archivo OBJ con el directorio OBJ_DIR para que nin-
guna información de camino pueda especificarse para los archivos listados en la sección de
auto- dependencia. Archivos OBJ adicionales pueden especificarse con un camino com-
pletamente calificado afuera de la sección de auto-dependencia.
OBJ_FORMAT= pueden usarse dos valores para esta definición para seleccionar el formato de archivo
objeto: COFF (Formato Común de Archivo Objeto) o OMF (Formato del Módulo Objeto). El
ProjectBuilder luego inicia el Parámetros del Compilador e invoca El programa de utilidades
AIMPLIB.EXE, si es necesario, usando los parámetros correspondientes /coff o /omf.
Además, se emplea el enlazador especificado con COFF_LINKER= u OMF_LINKER= en el
proceso de construcción.
OMF_LINKER= Esta definición opcional puede usarse para definir al enlazador que el ProjectBuilder invoca
cuando se establece OBJ_FORMAT= a OMF (Formato de Módulo Objeto). Si no se define
OMF_LINKER= en este caso, el ProjectBuilder utiliza al enlazador definido con LINKER=.
RC_COMPILE= Esta definición contiene el nombre del compilador de recursos de la manera en que se lo
envía con Xbase++. Normalmente, recursos adicionales se utilizan para aplicaciones GUI
solamente.
OS/2 - Estas son notas adicionales al final de esta sección para la plataforma de OS/2.
RC_FLAGS= Las banderas para el compilador de recursos se establece con esta definición.
<SECCION> Todas las entradas en la sección [PROJECT] sin los signos iguales se usan como refe-
rencia para las secciones subsecuentes definidas por el usuario que analiza el Project-
Builder. Normalmente, sólo se hace referencia a una sección adicional (la sección de raíz).
Debe haber por lo menos una sección definida por el usuario.
Nota: Las definiciones que se listan en la sección [PROJECT] son válidas para el proyecto de entrada. Sin em-
bargo, ellas deben aparecer también en las secciones definidas por el usuario. En este caso, son válidas para una
sola sección. Si se pasan las definiciones a PBUILD.EXE sobre la línea de comandos usando el parámetro /d, se
ignoran todas las definiciones en un archivo de proyecto con el mismo nombre.
[PROJECT]
DEBUG = YES // definición a nivel de proyecto
[RAIZ]
archivo1.EXE
archivo2.DLL
[archivo1.EXE]
<archivos PRG para crear el EXE>
[archivo2.DLL]
DEBUG = NO // definición a nivel de sección
<archivos PRG para crear la DLL>
La primer sección definida por el usuario lista todos los archivos ejecutables (objetivos) que ProjectBuilder ha de
crear. Para cada objetivo, existe una sección definida por el usuario con el mismo nombre. Lista todos los archivos
PRG (fuentes) para ese objetivo. Los fuentes son normalmente archivos PRG, pero pueden ser archivos fuente así
como los que necesita una aplicación GUI. Los archivos de recursos se compilan con un compilador de recursos y
enlaza al archivo ejecutable.
Cuando un archivo objetivo se crea como una biblioteca dinámica (archivo DLL), el nombre del archivo corres-
pondiente debe incluirse en una extensión DLL. ProjectBuilder automáticamente crea todos los archivos necesa-
rios para crear una DLL. Esto incluye al archivo de definición de exportación (archivo DEF) y la biblioteca de im-
portación (archivo LIB). La última debe estar enlazada a un EXE que usa las funciones contenidas en un DLL.
Cuando un archivo de proyecto contienen secciones definidas por el usuario múltiples, es suficiente escribir sólo
los archivos fuente necesarios para cada sección (archivos PRG y archivos de recurso, si es necesario). El archivo
de proyecto puede expandirse después para describir todas las dependencias entre los fuentes y objetivos in-
vocando a PBUILD.EXE con el parámetro /g.
PBUILD @<archivo>
Nota: Si se crea una aplicación con ProjectBuilder (PBUILD.EXE) no es necesario llamar al enlazador explícita-
mente. ProjectBuilder invocará al enlazador automáticamente.
Cuando se llama a Alink, se deben especificar uno o más archivos OBJ como archivos de entrada. El enlazador
crea un archivo EXE como un programa ejecutable desde los archivos OBJ. Por omisión, el nombre del primer
archivo OBJ se emplea para nombrar el archivo EXE resultante. Los nombres de archivo de entrada se encuentran
separados por espacios en blanco y pueden aparecer en orden en la línea de comando. Por predeterminación
ALINK usa .OBJ como una extensión de archivo. Ello significa que es posible especificar archivos OBJ sin una
extensión al enlazador. ALINK sólo emplea extensiones para buscar archivos. No hace suposición alguna acerca
del contenido de los archivos desde sus extensiones.
Además de archivos OBJ, ALINK acepta archivos LIB y sólo un archivo RES como archivos de entrada. Se utilizan
archivos LIB para crear archivos DLL (ver Creando archivos DLL), y un archivo RES contiene recursos binarios,
como bitmaps o íconos, que deben estar disponibles durante la ejecución (ver El compilador de recursos de Alaska
- ARC.EXE).
Se puede hacer un listado de archivos de entrada para el enlazador en un archivo ASCII <Script> que debe ser
especificado con el signo @ precediéndolo, en la línea de comando. El enlazador lee este archivo y enlaza todos
los archivos listados con un archivo de salida ejecutable.
/BASE:<n>
Con la opción /BASE:<n>, el enlazador establece la dirección de base especificada para cargar el
archivo ejecutable. Los valores predeterminado son 0x400000 para archivos EXE y 0x10000000
para archivos DLL. Si un sistema operativo no está disponible para cargar un archivo en la loca-
lización especificada, calcula una nueva dirección y reubica al programa. La información respecto a
direcciones de base puede obtenerse usando la opción /MAP. Aparecen listas de direcciones de
base en un archivo MAP. El enlazador redondea el valor <n> a un múltiplo de 64kB.
/DE[BUG]
Con la opción /DEBUG, el enlazador introduce información de depuración con nombres simbólicos
y números de línea al archivo EXE. El archivo EXE puede ejecutarse aun fuera del debugger.
Dentro de éste último, tal información se encuentra disponible cuando la aplicación ha sido enla-
zada con la opción /DEBUG.
Importante: Para recibir la información debug desde el enlazador, se deben compilar archivos PRG
con la opción /b. Crear un archivo EXE habilitado para debug es posible únicamente con la opción
de compilado /b en conjunto con la opción /DEBUG del enlazador.
/DEFAULTLIB:<archivo>
Esta opción especifica un archivo LIB adicional en el cual el enlazador sirve para buscar como
resolver referencias externas. El enlazador primero busca las bibliotecas especificadas en la línea
de comando, luego la biblioteca predeterminadas con /DEFAULTLIB, y por ultimo aquellas nom-
bradas en los archivos OBJ.
/DLL
Con esta opción, el enlazador crea un archivo DLL en vez de un EXE. Todos los archivos OBJ a ser
enlazados a una DLL deben ser compilados usando el interruptor /DLL del compilador.
Nota: Cuando se emplea la opción /DLL, no sólo los archivos OBJ sino también archivos de defi-
nición para exportar (EXP) (EXPort definition file) deben especificarse para el enlazador. (ver
Creando archivos DLL).
/FORCE:MULTIPLE | UNRESOLVED
La opción /FORCE obliga al enlazador a producir un archivo ejecutable inclusive si ha detectado
símbolos externos irresueltos y/o declaraciones múltiples del mismo símbolo. El archivo ejecutable
no es cor recto por consiguiente, y sólo aquellas partes del programa que no usan los símbolos en
cuestión podrán ejecutarse sin errores.
Si el enlazador se ve en la necesidad de tolerar ambos, los símbolos no resueltos y declaraciones
múltiples de un único símbolo, el interruptor /FORCE debe emplearse dos veces (Alink <archivo>
/FORCE:MULTIPLE /FORCE:UNRESOLVED).
Precaución: Realmente lo hemos estimulado a evitar usar la opción /FORCE. Si la utiliza tenga en
cuenta que los archivos de salida pueden estar incorrectos, y que ejecutar un programa incorrecto
puede conducir al colapso del mismo y/o del sistema operativo.
/MAP[:<archivo>]
La opción /MAP causa que el enlazador cree un archivo MAP. Este archivo contiene información
sobre el archivo ejecutable. Opcionalmente, se puede especificar el nombre del archivo MAP con
<archivo> . Si se omite, el nombre del archivo de salida también sirve para el archivo MAP.
/NOD[EFAULTLIB]
Esta opción previene a ALINK de buscar bibliotecas de importación predeterminado que resuelven
referencias externas, como por ejemplo, el archivo XPPRT1.LIB. Cuando se usa esta opción, el
enlazador sólo recurre a los archivos LIB en la línea de comando.
/NOL[OGO]
Esta opción suprime el despliegue del logo del programa y el num ero de versión.
/OUT:<archivo>
Con esta opción es posible definir el nombre del archivo ejecutable. Por omisión, el nombre del
archivo se crea desde el primer archivo OBJ especificado al enlazador.
/PM[TYPE]:VIO|PM [<mayor>[.<menor>]]
/SUBSYSTEM:CONSOLE|WINDOWS [<mayor>[.<menor>]]
Las opciones /SUBSYSTEM y /PMTYPE tienen el mismo significado para el enlazador. Ambas
definen el tipo de aplicación. El predeterminado es VIO o CONSOLE, respectivamente, lo que in-
dica una aplicación a modo de texto. Si se especifica PM o WINDOWS, el enlazador crea una
aplicación GUI.
Opcionalmente se puede especificar el mínimo número de versión del sistema operativo requerido
para ejecutar una aplicación Xbase++. Si se configura <mayor> como 4 y <menor> como .1, por
ejemplo, un programa Xbase++ no podría iniciarse en un sistema operativo versión 3.x, pero lo
haría en una versión 5.x.
/ST[ACK]:<max>[,<min>]
Esta opción configura el tamaño de stack de una aplicación Xbase++. <max> es un valor numérico
indicativo del máximo tamaño de stack en bytes (predeterminado es 1 MB), mientras <min> op-
cionalmente define el tamaño de stack al comienzo del programa. Esto significa que el mínimo
tamaño de stack es <min> bytes y que puede crecer dinámicamente durante la ejecución hasta
<max> bytes.
/VERBOSE
La opción /VERBOSE causa que el enlazador exhiba información adicional durante el proceso de
enlace.
/VERSION <mayor>[.<menor>]
El enlazador coloca esta información de la versión en el titulo de los archivos EXE o DLL. <mayor>
y <menor> pueden contener un valor entre o y 65535.
Esto predetermina la creación de aplicaciones con interfaz gráfica habilitadas para debug. Todas las opciones
especificadas en la línea de comando son procesadas luego de las opciones SET ALINK=. Por lo tanto, las op-
ciones de la línea de comando se superponen con las opciones establecidas por la variable ambiental.
Además, el enlazador usa SET LIB= como un camino de búsqueda para archivos OBJ y LIB. ALINK busca ar-
chivos OBJ y LIB en el siguiente orden:
Si no se encuentra un archivo OBJ o LIB, ALINK emite un mensaje de error y concluye el proceso de enlace.
** Archivo DIGAHOLA.PRG **
PROCEDURE DigaHola
? "Hola gente!"
RETURN
El ProjectBuilder de Xbase++ provee la forma más fácil para la creación de un archivo DLL puesto que puede
desarrollar todos los pasos automáticamente. Sólo se requiere para crear un archivo de proyecto que contiene
secciones separadas para archivos EXE y DLL. Una plantilla de proyecto apropiado para el ProjectBuilder puede
verse así:
// Archivo: PROJECT.XPJ
[PROJECT]
PRINCIPAL
[PRINCIPAL]
MAIN.EXE
MISFUNCS.DLL
[MAIN.EXE]
MAIN.PRG
MISFUNCS.LIB
[MISFUNCS.DLL]
HOLA.PRG
DIGAHOLA.PRG
El Proyecto consiste en su totalidad de un archivo EXE y otro DLL. En la plantilla se listan los archivos fuentes PRG
correspondientes en dos secciones separadas. La sección para el archivo EXE incluye la biblioteca de importación
MISFUNCS.LIB necesaria para usar las funciones DLL. Esta plantilla debe ser expandida llamando a PBUILD con
la opción /g option y una segunda llamada - sin la opción /g - finalmente crea ambos EXE y DLL con su archivo de
biblioteca de importación.
Si se crea un archivo DLL sin el ProjectBuilder, usando por ejemplo una utilidad Make, han de seguirse un total de
cinco pasos diferentes:
1. Compilar todos los archivos PRG requeridos para el archivo DLL con la opción /DLL del compilador.
2. Crear un archivo con las definiciones para los módulos en el archivo DLL (archivo DEF). Este archivo con-
tiene una lista de todas las funciones o procedimientos que son exportadas desde el archivo DLL y que
pueden ser importadas a un archivo EXE. Esta tarea es llevada a cabo por el programa utilitario
XPPFILT.EXE.
3. Crear una biblioteca de importación LIB y un archivo de exportación (EXP) desde el archivo DEF. Esto lo
lleva a cabo el programa utilitario AIMPLIB.EXE.
4. Enlazar archivos OBJ con el archivo EXP a uno DLL.
5. Para poder usar un nuevo archivo DLL con un archivo EXE recientemente creado, la biblioteca de impor-
tación (LIB) debe enlazarse al archivo EXE.
El ejemplo a continuación describe los cinco pasos antedichos. Emplea los tres archivos PRG MAIN.PRG,
HOLA.PRG y DIGAHOLA.PRG:
Paso 1: Compilando
Los archivos fuente para un archivo EXE o una DLL deben ser compilados diferentemente:
XPP main /q /b
Se crean tres archivos OBJ. El archivo MAIN.OBJ sólo puede enlazarse a un archivo EXE y los archivos
HOLA.OBJ y DIGAHOLA.OBJ sólo pueden enlazarse a un archivo DLL.
XPPFILT.EXE crea el archivo MISFUNCS.DEF que contiene toda la información para crear el archivo
MISFUNCS.DLL. El archivo DEF que ejemplifica lo antedicho luce así:
En un archivo DEF, las líneas destinadas a los comentarios comienzan con punto y coma. Las demás líneas
contienen declaraciones para el enlazador. La declaración LIBRARY declara el nombre de archivo del archivo DLL
e indica si las rutinas de iniciación son ejecutados sólo una vez durante la carga, o cada vez que un proceso re-
quiere un archivo DLL. Todos los archivos DLL creados con Xbase++ deben ejecutar sus rutinas de especializa-
ción para cada proceso (cada programa) e INSTANCE (línea 1) debe especificarse siempre.
Cuando múltiples programas Xbase++ acceden al mismo archivo DLL, ellos sólo pueden compartir el código del
programa y no las variables declaradas en él. Deben darse los atributos MULTIPLE NONSHARED READWRITE a
un archivo DLL. Esto se concreta usando la declaración DATA (línea 2).
La opción LOADONCALL especificada en las declaraciones DATA y CODE especifican que un archivo DLL se
carga en la memoria sólo cuando se ejecuta un módulo contenido en el archivo DLL. Alternativamente, se usa la
opción PRELOAD que especifica que la carga se realiza al comienzo del archivo EXE (líneas 2 y 3).
Siguiendo la declaración EXPORTS (línea 5) se listan todos los identificadores (nombres) para funciones de ex-
portación y procedimientos. Cada identificador debe aparecer en una línea por si solo. Sólo es posible llamar
desde un archivo DLL a las funciones o procedimientos especificados siguiendo la declaración EXPORTS.
Nota: Si se declaran clases en un archivo DLL, sólo los nombres de las clases deben listarse, y no los nombres de
los métodos declarados para una clase. EL nombre de la clase es también el nombre de la función clase y esto
debe definirse como exportado.
AIMPLIB misfuncs.def
Como resultado, se crean los archivos MISFUNCS.LIB y MISFUNCS.EXP. El archivo LIB contiene información
sobre qué puede importarse por un archivo EXE, y el archivo EXP define qué se exporta de un archivo DLL.
La opción /DLL se utiliza para enlazar archivos OBJ. El nombre del archivo DLL se define usando la opción /OUT.
Este llamado a ALINK crea el archivo ejecutable MAIN.EXE como una aplicación en modo texto. No contiene
código desde el archivo DLL, pero referencia la biblioteca dinámica MISFUNCS.DLL. El código desde este archivo
se carga cuando una función contenida en la DLL es llamada desde el MAIN.EXE.
Tipos de Recursos
Palabra Descripción
BITMAP Recurso Imagen como Bitmap, ver XbpBitmap()
ICON Recurso Icono, usado por XbpDialog() o XbpTreeViewItem()
POINTER Recurso Puntero de Ratón, ver XbpWindow():setPointer()
CHARACTER Recurso Cadena, ver LoadResource()
La longitud está limitada a 65535 caracteres por cadena.
*
* TEST.ARC
*
/* Declaración de diferentes
tipos de recursos
*/
OS/2 - El compilador de recursos de OS/2, RC.EXE, requiere que se declaren los recursos de manera diferente.
Sin embargo, si se invoca a ARC.EXE con el parámetro /rc de la línea de comandos, éste traduce un archivo ARC
a un archivo RC que puede compilarse por otros compiladores de recursos como RC.EXE en la plataforma de
OS/2. De ésta manera, las declaraciones de recursos pueden hacerse independientemente del compilador de
recursos.
** EOF
-----------------------------------------------------------------
** archivo ARC: TEST.ARC // Declaración de recursos
** EOF
Este ejemplo muestra el código para un archivo CH que define constantes para el recurso numérico de IDs. Las
constantes se usan en el archivo ARC puesto que ARC.EXE puede resolver la directiva #include.
Sólo se declaran los recursos bitmap en el ejemplo. La palabra clave BITMAP se escribe en una línea separada y
seguida por un bloque de declaraciones de recursos del mismo tipo. ARC se apoya en esta forma sintáctica para
declarar recursos que resulta en una mejor lectura del archivo ARC. Si se escribe una palabra clave en una línea
por sí sola, todas las líneas siguientes declaran el mismo tipo de recurso sin repetir la palabra clave.
Las ultimas líneas en el ejemplo demuestran como los recursos específicos del sistema operativo pueden de-
clararse en uno y en el mismo archivo ARC. Se usan las directivas #ifdef , #else y #endif , permitiendo así la
compilación adicional. ARC.EXE utiliza las mismas constantes #define implícitas como el compilador de Xbase ++:
__OS2__ and __WIN32__.
ARC.EXE crea un archivo RES desde todos los archivos ARC especificados en la línea de comandos. Si la di-
rectiva #include aparece en un archivo ARC, el compilador de recursos busca los archivos #include en el directorio
actual y en los directorios listados en la variable de entorno INCLUDE. Se busca en los archivos que contienen los
recursos externos - como bitmaps o iconos - en el directorio actual y en los directorios listados en la variable de
entorno XPPRESOURCE.
El formato de archivo DBF es la base para todos los dialectos Xbase y permite la manipulación directa de con-
juntos de datos extensos. Los datos dentro de cada archivo DBF están organizados en forma de tabla organizada.
Cada columna corresponde a un campo de Base de Datos (campo, para sintetizar), y cada fila a un registro de
datos (registro, para sintetizar). El titulo (o fila superior) contiene los nombres de los archivos que identifican los
campos. La siguiente tabla ilustra como se ordenan los datos en un archivo DBF:
El archivo DBF (tabla) en este ejemplo contiene siete registros (filas). Cada registro incluye cuatro campos (co-
lumnas). Cada campo contiene información especifica sobre cada cliente.
En un archivo DBF la estructura de la tabla se define junto con los datos. La definición de la tabla incluye los
nombres de los campos (títulos de las columnas), su longitud (ancho de columna), tipos de datos y cantidad de
espacios decimales para los campos numéricos. Un archivo contiene datos tipeados precisamente, o sea los datos
de una columna han de tener siempre el mismo tipo datos. El formato de archivo DBF permite guardar un total de
cinco clases de datos diferentes: caracter, fecha, lógico, memo y numérico.
Es posible guardar la definición inicial de la estructura de una tabla DBF en la propia tabla DBF. Para ser empleada
de esta manera, la tabla debe contener los nombres de los campos, su longitud y el número de espacios decimales
(un total de cuatro columnas). La definición de la estructura de la tabla para el ejemplo antedicho seria el siguiente:
Esta tabla contiene la información necesaria para describir la estructura de un archivo DBF. Cada registro contiene
la definición para un campo determinado. Los títulos de las columnas en la tabla son los nombres de los campos
conocidos como un archivo DBF "de estructura extendida". Un archivo DBF "de estructura extendida" contiene las
definiciones para otro archivo DBF. Las definiciones de los campos comprenden la definición de la estructura de un
archivo DBF y son el punto de partida para la utilización de Bases de Datos.
Creando una Base de Datos
Un archivo DBF puede ser creado únicamente a partir de una definición de estructura. En Xbase++ los comandos
y funciones especificados en la próxima tabla pueden ser empleados para crear la definición a continuación:
Las primeras cuatro definiciones y comandos en la tabla existen únicamente debido a razones de compatibilidad y
no deberían continuar siendo usadas. La creación de un archivo DBF en Xbase++ debería hacerse utilizando la
función DBCreate(). DBCreate() toma como parámetro la definición de estructura para el archivo DBF en la forma
de un ordenador bidimensional. Aquello evita el mecanismo indirecto que pone en práctica el empleo de un archivo
de estructura extendida. El siguiente código demuestra el uso óptimo de la función DbCreate():
cNombreArchivo := "CUST.DBF"
aEstructura := { { "NRCLIENTE" , "C", 8, 0 }, ;
{ "APELLIDO" , "C", 20, 0 }, ;
{ "NOMBRE" , "C", 20, 0 }, ;
{ "TELEFONO" , "C", 15, 0 } }
DbCreate( cNombreArchivo, aEstructura )
El nombre del archivo en conjunto con la estructura del archivo DBF se los guarda en variables pasadas a la
función DbCreate(). La estructura del archivo es definida en un orden bidimensional. Todos los campos en este
ejemplo tiene el tipo de dato "caracter", identificado por la letra "C" (caracter). La longitud de los campos son 8, 20,
20 y 15 caracteres respectivamente, y todos los campos carecen de espacios decimales.
El nombre de un archivo puede contener hasta un máximo de 10 caracteres. Esta misma regla se aplica también a
nombres de campos que demandan nombres de variables. La primer letra debe ser un caracter alfabético un
subrayado y todos los demás caracteres deben ser alfanuméricos o un subrayado.
El tipo de dato de un campo se define por una única letra. Se reconocen la letra "C" (caracter),"D" (fecha),
"L"(lógico), "M" (memo) y "N" (numérico).
Un campo memo siempre ocupa 10 bytes, pero puede guardar un número ilimitado de caracteres. Esto es porque
los contenidos de campos memo se guardan en un archivo separado llamado archivo DBT.
Los campos numéricos guardan números con una máxima longitud de 19 lugares. El decimal y el prefijo cada uno
toma un byte. El número más largo que puede ser guardado es 2^32-1 y el máximo número de espacios decimales
es 15.
Guardando datos
Luego que un archivo DBF es creado utilizando DBCreate(), debe ser abierto antes de que se pueda guardar datos
o leerlos a partir de el. La siguiente tabla da una vista general de las funciones y comando que pueden emplearse
para las operaciones simples de base de datos "USE", "CLOSE", "APPEND BLANK" y "REPLACE".
En el ejemplo, un nuevo registro es añadido a una Base de Datos conteniendo información del cliente. Las mismas
operaciones aparecen debajo usando la función sintaxis:
En este ejemplo, los datos del cliente son incorporados a la Base de Datos por la función FieldPut() que identifica
el campo por su posición original. Al hacer esto el programa pierde claridad. Una mejor solución en este caso
consiste en dirigirse a los campos mediante el nombre de alias FIELD y realizar tareas directas:
En conjunto con estas operaciones elementales de Base de Datos Xbase++ provee funciones que obtienen in-
formación sobre Base de Datos o campos individuales. Una Base de Datos debe ser abierta en orden para las
funciones contenida en la siguiente tabla:
Estas funciones pueden ser empleadas para calcular el tamaño de una base de datos (las funciones RecCount() y
LastRec() ambas proporcionan el mismo valor). La formula para calcular el tamaño de una Base de Datos es:
Se usa frecuentemente esta formula junto con la función DiskSpace() durante rutinas backup, para determinar si
un archivo DBF cabe en la disquetera blanco a la cual ha de ser copiada.
Es importante la información de los campos en la programación de Base de Datos. No solamente son los conte-
nidos de un campo utilizados cuando se procesan datos sino también el tipo de dato, posición ordinal dentro de un
archivo DBF y el nombre del campo. Por ejemplo, esta información se torna útil cuando al escribir rutinas genéricas
o lecto-escritura dirigida por dato.
Funciones retornando información sobre campos en una Base de Datos
Función Descripción
FCount() Devuelve el número de campos en el archivo
FieldName() Devuelve el nombre de campo basado en la posición ordinal
FieldPos() Devuelve la posición ordinal del campo basado en nombre de campo
FieldGet() Devuelve el contenido de un campo basado en la posición ordinal
Type() Devuelve el tipo de datos de un campo basado en el nombre de campo
DbStruct() Devuelve el la estructura de archivo como un arreglo bidimensional
El siguiente código presenta un ejemplo utilizando una de las funciones de esta tabla. Este código incluye dos
funciones definidas por el usuario. Una de ellas lee todos los campos de un registro en un ordenador y la otra
escribe el ordenador nuevamente como archivo. Esta es una técnica común de programación para amortiguar la
edición de registros y es frecuentemente usada en programas diseñados para operaciones multi-uso en un am-
biente de red:
PROCEDURE Main
LOCAL aRegistro := LeeRegistro() // leer registro
// <editar datos>
Es posible recuperar información desde diversas áreas de trabajo mediante el operador alias (->) y desde el
número de áreas de trabajo o su nombre de alias. Inclusive, se puede acceder a los campos de múltiples Bases de
Datos vía la referencia al alias. Referenciar áreas de trabajo con alias es una operación fundamental y progra-
mación de uso frecuente con múltiples Bases de Datos o áreas de trabajo, respectivamente. Las líneas de código
a continuación nos muestran algunas operaciones básicas:
PROCEDURE Main
? Select() // Resulta: 1
? Used() // Resulta: .F.
? Alias() // Resulta: "" (cadena nula)
? Select() // Resulta: 3
? Used() // Resulta: .T.
? Alias() // Resulta: Ord
El programa presenta los aspectos más importantes para el uso de múltiples bases de datos. Al comienzo del
Principal procedimiento, se selecciona un área de trabajo número 1; es el área de trabajo actual. Ya que ninguna
Base de Datos se encuentra abierta desde el inicio la función Used() devuelve el valor .F. (falso), y el nombre de
alias correspondiente a la primer área de trabajo es una cadena vacía (""). Solo después de usar el comando USE
para abrir la Base de Datos, el área de trabajo recibe un nombre de alias. El valor de retorno .T. (verdadero) de la
función Used() luego indica el área de trabajo actual a ser empleada. Cuando se programa el comando USE con la
opción NEW, selecciona primeramente un área de trabajo y seguidamente abre una Base de Datos. Por lo tanto, el
valor de retorno de la función Select() es ahora 3 y no más 1 como en la primer llamada. Se abren tres Bases de
Datos y la tercer área de trabajo es actual. Su nombre de alias es "Ord".
Cuando se llama una función sin una referencia al alias es ejecutada en el área de trabajo actual. Una referencia al
alias causa que una función sea ejecutada en el área de trabajo correspondiente (o con mayor precisión el área de
trabajo se torna temporariamente la actual; entonces la función es ejecutada y al área de trabajo anterior es se-
leccionada otra vez. La llamada a función (1)->(Alias()) devuelve el nombre de alias de la primer área de trabajo.
Es la cadena "Cliente" la que se crea implícitamente desde el nombre del archivo.
El ejemplo también nos muestra diferencias de sintaxis que se requieren para referencias al alias para campos de
Bases de Datos (o símbolos) y llamadas a función (o expresiones). Si una expresión aparece sobre el lado derecho
del operador de alias, a ser escrita en paréntesis. Se programa un símbolo sin paréntesis. Sobre el lado izquierdo
del operador de alias se deben usar paréntesis cuando se utiliza un número o variable en vez de los nombres de
alias literales. Ergo, se codifican referencias al alias de formas diferentes:
cAlias := "Cliente"
nArea := 1
Se garantiza la unicidad de nombres de alias de áreas de trabajo, así como lo son los números de áreas de trabajo.
Si se abre varias veces el mismo archivo de la Base de Datos en diferentes áreas de trabajo, cada área de trabajo
recibirá un nombre de alias distinto, pero referenciará la misma Base de Datos física.
Cuando se intenta usar más de una vez el mismo nombre de alias, Xbase++ crea de manera implícita un identi-
ficador único al unir un caracter guión bajo "_" y el número de área de trabajo al nombre de alias original.
El espacio de trabajo de Xbase++
El concepto de áreas de trabajo existe en cada dialecto de Xbase++. Se extiende en Xbase++ por un espacio de
trabajo que provee un nivel de abstracción más elevado. El concepto de áreas de trabajo se describe así:
Un espacio de trabajo contiene áreas de trabajo y permite que se abran múltiples Bases de Datos en tiempo de
ejecución. El acceso a campos de Bases de Datos en un programa sigue un patrón jerárquico como el descripto
debajo:
Un espacio de trabajo y áreas de trabajo contenidas siempre existen durante el tiempo de ejecución de un pro-
grama, independientemente de si las Bases de Datos están o no abiertas. La operación fundamental en un es-
pacio de trabajo es la selección del área de trabajo actual. Mientras un área de trabajo no esta siendo utilizada
puede ser seleccionada por su número. Se pasa un número entre 1 y 65.000 al comando SELECT o función
DbSelectArea() con el fin de elegir un área de trabajo particular como la actual. Alternativamente, se especifica el
valor 0 (cero). Esto actualiza la próxima área de trabajo libre y con el número más pequeño. Por ejemplo,
PROCEDURE Main
USE Cliente
? Select() // Resulta: 1
SELECT 100
? Select() // Resulta: 100
SELECT 0
? Select() // Resulta: 2
RETURN
Cuando comienza el programa, se selecciona al área de trabajo 1 y la base de datos Cliente. Se abre
CLIENTE.DBF en este ejemplo con el comando USE en la primer área de trabajo. Luego, se selecciona el área de
trabajo 100 que se actualiza. Por ultimo, el comando SELECT 0 selecciona la próxima área de trabajo libre. Tiene
la posición ordinal 2.
Cuando se abre una Base de Datos en un área de trabajo no solo puede seleccionársela por su número sino por su
nombre de alias. Este ultimo es un nombre simbólico que facilita la programación con múltiples Bases de Datos:
PROCEDURE Main
USE Cliente
USE Facturas NEW
USE Partes NEW
? Select() // Resulta: 3
? Alias() // Resulta: Partes
SELECT 2
? Alias() // Resulta: Facturas
SELECT Cliente
? Select() // Resulta: 1
DbCloseAll() // Cierra todas las Bases de Datos
RETURN
En este ejemplo se abren tres Bases de Datos en tres áreas de trabajo diferentes. Demuestra como seleccionar
una Base de Datos utilizando un número o un nombre de alias. Al final todas las Bases de Datos se cierran.
Operaciones en un espacio de trabajo
La función DbCloseAll() constituye un ejemplo para las funciones que desarrollan operaciones en áreas de trabajo.
Afecta todas las áreas de trabajo usadas, no así las libres. Hay un conjunto de funciones en Xbase++ que opera en
una única área de trabajo y desarrolla operaciones con un área de trabajo única o todas las áreas de trabajo
usadas. La tabla siguiente da una lista de las estas funciones:
La función WorkspaceEval() de la tabla es la más poderosa puesto que evalúa un bloque de código en todas las
áreas de trabajo. Todas las funciones Db...All() podrían ser emuladas con esta función y las funciones nuevas para
todas las áreas de trabajo programadas.
En Xbase++, un nombre de alias referencia a una Base de Datos. El nombre de alias de un área de trabajo puede
ser intercambiado entre dos espacios de trabajo. Transferir nombres de alias requiere de un espacio de trabajo
virtual llamado el espacio Cero. Se usan para el intercambio las dos funciones DbRelease() y DbRequest() :
USE Cliente
? Used() // Resulta: .T.
DbRelease()
? Used() // Resulta: .F.
DbRequest()
? Used() // Resulta: .T.
CLOSE Cliente
Luego de llamar a DbRelease() , el área de trabajo actual se encuentra libre. No obstante, el archivo de Base de
Datos aun esta abierto. Referencia a la base de datos (el nombre de alias) que no existe mas en el espacio de
trabajo actual. Se la transfiere al espacio Cero y puede transferirse de vuelta a un área de trabajo de un espacio de
trabajo llamando a la función DbRequest() . Cuando se termina DbRequest() , un área de trabajo recibió un nombre
de alias y es utilizada nuevamente. Es importante comprender que el archivo de la Base de Datos permanece
abierto durante estas operaciones. Solo la referencia al archivo abierto cambia su lugar desde el espacio de tra-
bajo actual al espacio Cero de vuelta al mismo u otro espacio de trabajo.
Puntero de registros y campos de Base de Datos.
Cuando se usa un área de trabajo todos los datos guardados en un archivo de base de datos son accesibles.
Luego de abrir una Base de Datos el puntero de registros apunta inicialmente el primer registro. Durante el tiempo
de ejecución las variables de campo de un programa luego contienen los valores guardados en el primer registro.
Esto significa que un programa solo puede acceder a campos del registro actual. Con el fin de acceder a datos
guardados en diferentes registros, se debe mover el puntero de registros.La navegación por Base de Datos debe
ocurrir en un área de trabajo.
USE Cliente
? Recno() // Resulta: 1
? Cliente->Apellido // Resulta: Miller
? Recno() // Resulta: 10
? Cliente->Apellido // Resulta: Brown
Este ejemplo demuestra que los contenidos de variables de campo cambian cuando se mueve el puntero de
trabajo. Se seleccionan los registros al navegar por el puntero de registros, y esta es la única forma de acceder a
todos los datos guardados en un archivo de Base de Datos. La próxima tabla da una lista de las funciones mas
importantes utilizadas en el contexto de navegación en la Base de Datos.
Las funciones Recno() y DbSkip() son lejos las más importantes para la navegación en Base de Datos. La primera
regresan el puntero de registros y la segunda lo cambian. Además, las dos funciones Bof() and Eof() se emplean
muy a menudo. Indican si el puntero de registro ha alcanzado los limites de una Base de Datos (superior o inferior)
de un archivo de Base de Datos. El siguiente ejemplo de código muestra una construcción típica utilizada para
obtener información de todos los registros de un archivo de la Base de Datos:
Componente DATA Este componente describe el modelo de base de datos o cómo se alma-
cenan los propios datos. "Base de Datos" aquí puede considerarse análoga
a una "tabla" en el modelo de relaciones o de una "estructura" en el modelo
de bases de datos jerárquica.
Componente ORDER Este componente pone a disposición un mecanismo para permitir la defini-
ción del ordenamiento lógico basado en las expresiones. Esto permite dejar
intacto el ordenamiento físico preexistente de las tablas.
Componente RELATION Este componente define las relaciones entre los datos. Enlaza las tablas de
manera física o lógica basada en expresiones. La tarea de este compo-
nente puede considerarse análoga al "operador JOIN" en el modelo de re-
laciones o el "puntero de conjuntos" en el modelo de relaciones o el "pun-
tero de Conjuntos" en el modelo de bases de datos en red basada en ar-
chivos.
Componente DICTIONARY Este componente describe la estructura de bases de datos y/o las reglas de
integridad.
Una Motor de Base de Datos representa uno o más de los cuatro componentes y define como interaccionan con la
base de datos subyacente. El Motor de Base de Datos está contenido en un archivo DLL y se lo carga durante el
tiempo de ejecución del programa. Por ejemplo, el archivo DBFDBE.DLL contiene el Motor de Base de Datos que
provee el componente para manipular archivos DBF (componente DATA). El archivo NTXDBE.DLL contiene un
Motor de Base de Datos que representa un componente para los órdenes (componente ORDER) que es válido
para las bases de datos orientadas a registros (por ejemplo, archivos DBF). Este DBF maneja archivos índice en el
formato NTX. DBFDBE y el NTXDBE son Motores de Base de Datos los cuales contienen cada uno sólo un
componente para la manipulación de datos y archivos. Están designados cada uno como un "componente DBE".
Dos componentes DBEs que proveen diferentes componentes pueden ensamblarse en un nuevo Motor de Base
de Datos que controla entonces a ambos componentes y combina todas las características de los componentes
DBEs dentro del nuevo DBE. Tal Motor de Base de Datos ensamblado se lo llama "DBE compuesto". Un DBE
compuesto existe sólo en la memoria y no se guarda en un archivo en el Disco rígido. Los D BEs componentes, sin
embargo, se almacenan en archivos.
Los DBEs compuestos son necesarios generalmente cuando se programa con las bases de datos y deben crearse
al inicio del programa. Los componentes DBEs apropiado se cargan en la memoria usando la función DbeLoad() y
el DBE compuesto se crea usando la función DbeBuild(). El siguiente programa de ejemplo muestra la aproxi-
mación básica:
El ejemplo usa dos de las cuatro funciones básicas disponibles para la programación usando Motor de Base de
Datos. La siguiente tabla lista las cuatro funciones:
La función DbeLoad() carga un componente DBE en la memoria principal. Ese DBE puede cargarse de dos ma-
neras: oculto y visible. Un DBE componente que se carga como "oculto" sólo puede usarse para crear DBEs
compuestos y no directamente usados para la manipulación de datos y archivos.
En este ejemplo, el componente DBE se carga como visible (.F. significa no "oculto" aquí). Por tanto, el DBE puede
usarse para abrir un archivo DBF, y para ejecutar operaciones de archivos. El ejemplo a continuación muestra lo
opuesto:
Aquí el componente DBE se carga como "oculto" y cualquier intento de usarlo para manipulación de archivos
conduce a errores durante el tiempo de ejecución. El Motor de Base de Datos sólo puede usarse para crear DBE
compuestos usando la función DbeBuild().
Nota
El DBE compuesto "DBFNTX" se crea por predeterminación cada vez que un programa de Xbase++ se inicia. Esto
significa que todos los archivos y comandos de índice y funciones trabajan con este Motor de Base de Datos por
predeterminación. El DBFNTX Motor de Base de Datos se crea en el archivo DBESYS.PRG. Se llama a la función
DbeSys() contenida en este archivo cada vez que se inicia un programa de Xbase++.
El Motor de Base de Datos DBFNTX consiste de sólo dos componentes y puede hacer una mímica del modelo de
base de datos de Clipper de la forma en la que manipula archivos DBF y NTX. El componente RELATION es único
en este modelo de base de datos en que ningún DBE se requiere para el componente RELATION. Además, no hay
componente DICTIONARY, porque este no está respaldado por el modelo de base de datos.
Ciertos comandos y funciones existen dentro de los confines del lenguaje de programación y sólo pueden eje-
cutarse por componentes específicos del Motor de Base de Datos. Con estos comandos y funciones, el pedido
debe encaminarse al DBE apropiado capaz de ejecutar el comando. Esta tarea es llevada a cabo por un meca-
nismo interno llamado "Agente de Manipulación del Lenguaje de Base de Datos", abreviado como DMLB. El DMLB
encamina los pedidos desde el programa a un componente específico de una DBE.
El Motor de Base de Datos DBFDBE, por ejemplo, no es capaz de crear un índice. Debe cargarse el Motor de Base
de Datos NTXDBE para concretar esto. Asimismo, este DBE no puede abrir un archivo DBF. Para que el comando
USE y el comando INDEX ON sean ejecutables, debe estar disponible un DBE compuesto que contenga todos los
componentes requeridos por el programa. La tarea del DMLB es dirigir los pedidos que pueden llevar a cabo la
tarea pedida. Si ningún componente correspondiente está disponible el DMLB intenta llevar a cabo la tarea por sí
mismo basado en sus propias habilidades. Si esto no tiene éxito, se produce un error durante el tiempo de eje-
cución. Este modelo provee la habilidad de excluir los componentes que no son necesarios. Por ejemplo, es po-
sible escribir un programa que no use un índice. En este caso, el Motor de Base de Datos NTXDBE (más preci-
samente el componente ORDER) no necesita cargarse.
La distribución de funcionalidades específicas para componentes DBEs y la habilidad para hacer que todas las
funcionalidades estén disponibles dinámicamente usando un componente DBE ofrece mayor flexibilidad en la
manipulación de datos y archivos. Por ejemplo, puede crearse un índice para un archivo que existe en el formato
SDF (Sistema de Formato de Datos). El NTXDBE (componente ORDER) necesita principalmente combinarse con
el Motor de Base de Datos SDF (componente DATA). El resultado es un Motor de Base de Datos SDFNTX que
permite el ordenamiento lógico de los archivos ASCII que se encuentran en el formato SDF. El siguiente código del
programa ilustra esto (Nota: el ejemplo asume que el DBE DBFNTX compuesto ya se ha creado).
El hecho que los componentes DATA puedan combinarse con los componentes ORDER para crear DBE com-
puestos permite la funcionalidad de varias operaciones de archivo esté a disposición de los diferentes formatos de
archivo. En el ejemplo, un archivo TXT (archivo ASCII) se abre usando el comando (USE) previamente utilizado
para abrir un archivo DBF. Además las operaciones con bases de datos como DbAppend() y DbSeek(), pueden
ahora llevarse a cabo en el archivo TXT. La adición de un nuevo registro de datos usando DbAppend() se concreta
vía el componente SDF y la búsqueda de los datos en el archivo TXT usando DbSeek() ocurre vía el componente
NTX.
Hay, sin embargo, una limitación de que debe ser posible llevar a cabo una operación particular con archivos del
formato de archivos. En el caso de Motor de Base de Datos SDF, borrar registros de datos usando la función
DbDelete() no es posible. Estas funciones marcan un registro para borrarlo y no hay provisión en el formato de
archivo SDF para las marcas de borrado. Consecuentemente, llamar a la función DbDelete() para un archivo bo-
rrado por el Motor de Base de Datos SDF conducirá a un error durante el tiempo de ejecución porque esta ope-
ración no se encuentra respaldada.
Para una comprensión más profunda de estas funciones, se necesita conocimiento de los mecanismos internos
utilizados para abrir archivos con USE o DbUseArea(). Estos mecanismos no son reconocidos inmediatamente en
el nivel del lenguaje. Un archivo puede abrirse solamente cuando por lo menos se carga sólo un DBE que provee
al componente DATA para el formato de archivo pedido. Únicamente después que ese componente esté dispo-
nible puede abrirse un archivo en el área de trabajo, o de lo contrario se produce un error durante el tiempo de
ejecución. Si está o no disponible un DBE actual puede determinarse durante el tiempo de ejecución usando la
función DbeList(). DbeList() devuelve un arreglo de dos columnas: la primer columna contiene los nombres de los
DBEs y la segunda contiene los valores lógicos representando la "marca oculta" (ver la sección previa).
Abrir un archivo usando el DBE actual crea una instancia del DBE que se llama "objeto de base de datos" (DBO).
El DBO hace uso de todas las características del DBE actual y es el objeto de la base de datos realmente el que
abre y manipula los archivos. Un DBO representa el área de trabajo donde se abren los archivos.
Es de gran ayuda distinguir entre los Motores de Base de Datos, que proveen la funcionalidad que permite que los
archivos se abran en el área de trabajo y el objeto de base de datos que existe sólo cuando el archivo está abierto
en el área de trabajo. El objeto de base de datos es creado por el DBE cuando se abre el archivo. Manipula los
archivos y se lo descarta automáticamente cuando se cierra al archivo.
La función DbeInfo()
La función DbeInfo() provee información sobre el DBE actual. Por tanto, puede ejecutarse solamente cuando se
carga al DBE. Es similar a las funciones DbInfo() y FieldInfo() que puede llamarse cuando un archivo está abierto
en el área de trabajo actual. De lo contrario se produce un error durante el tiempo de ejecución.
Además de devolver información, DbeInfo() también permite que un DBE sea configurado de maneras específicas.
Varias configuraciones de un DBE son intercambiables y pueden redefinirse usando DbeInfo(). Como ejemplo, la
extensión predeterminada para los archivos puede alterarse:
#include "Dmlb.ch"
#include "DbfDbe.ch"
En este ejemplo, las extensiones predeterminadas para dos tipos diferentes de archivos se redefine usando
DbeInfo(). Esto afecta a los archivos manipulados por el componente DATA del DBFNTX DBE (el archivo DBF se
crea como un archivo FBD) y los archivos que son manipulados por el componente ORDER (los archivos NTX se
crean como archivos XTN). Todo esto ocurre en sólo las dos líneas del ejemplo:
Se muestra un aspecto especial de DbeInfo(): si se llama a la función con los parámetros, el primer parámetro
debe ser siempre una constante #define del archivo DMLB.CH especificando uno de los cuatro componentes que
pueden contenerse en un DBE. Además, el segundo parámetro debe ser una constante #define. Hay algunas
constantes que son universalmente válidas y pueden usarse con cada DBE y otras constantes que pueden usarse
sólo con un DBE específico. La diferencia puede reconocerse por el prefijo de la constante #define. El prefijo DBE_
identifica las constantes universalmente válidas y el prefijo DBFDBE_ identifica las constantes que son sólo vá-
lidas para el DBFDBE (más precisamente para el componente DATA que manipula los archivos DBF). Las
constantes que contienen el prefijo DBFDBE_ se definen en el archivo #include DBFDBE.CH. El tercer parámetro
especifica el valor que ha de establecerse para la configuración específica (en el ejemplo, la extensión del archivo)
de un DBE.
No todas las configuraciones de un DBE son alterables, así el tercer parámetro se lo procesa únicamente por
DbeInfo() cuando el DBE permite que se modifique la configuración correspondiente. La siguiente tabla da un
vistazo de las configuraciones universalmente válidas que existen para todos los DBEs. La única configuración
que puede modificarse es la extensión de archivo:
Constantes universales para las características de los DBEs
Constante *) Tipo de dato Descripción
DBE_DATATYPES ro C Tipos de datos soportados
DBE_EXTENSION a C Extensión de archivo predeterminado
DBE_MANUFACTURER ro C Productor del DBE
DBE_NAME ro C Nombre del DBE
DBE_VERSION ro C Versión del DBE
*) ro=sólo lectura (READONLY) , a=asignable (ASSIGNABLE)
Junto con estas constantes generales, la mayoría de los DBEs tienen constantes #define específicas que sólo
pueden usarse para el DBE específico (más precisamente, para un componente específico). En la sección Es-
pecificaciones del Motor de Base de Datos - formato de archivos, se describen constantes específicas del DBE
DbeInfo().
La función DbInfo()
Cuando se abre un archivo en un área de trabajo, un objeto de base de datos (DBO) es creado por el DBE actual
para manipular la apertura del archivo en el área de trabajo. El DBO representa al área de trabajo y la función
DbInfo() puede leer información sobre el DBO y puede modificar la configuración del DBO. DbInfo() requiere que
un archivo esté abierto en el área de trabajo correspondiente.
DbInfo(), así como DbeInfo(), recibe parámetros que son constantes definidas en el archivo #include. Las cons-
tantes universalmente válidas pueden encontrarse en el archivo DMLB.CH y se listan en la próxima tabla:
Las constantes universales para los objetos de base de datos (áreas de trabajo)
Constante *) Tipo de Dato Descripción
DBO_ALIAS ro C Nombre de alias
DBO_FILENAME ro C Nombre del archivo abierto
DBO_ORDERS ro N Número ordinal (índices)
DBO_RELATIONS ro N Número de relaciones
DBO_SHARED ro L .T. si el archivo está abierto en modo SHARED
DBO_REMOTE ro L .T. si el archivo no está almacenado
en la estación de trabajo local
DBO_DBENAME ro L Nombre del DBE que ha
abierto un archivo de la base de datos
*) ro=sólo lectura (READONLY) , a=asignable (ASSIGNABLE)
Las constantes #define universalmente válidas para DbInfo() comienzan con el prefijo DBO_ (por Objeto de Base
de Datos - DataBase Object). Hay además constantes que pueden usarse sólo con los DBOs creados por un DBE
específico). Un ejemplo de las constantes que pueden pasarse a DbInfo() se da en los siguientes códigos del
programa:
#include "Dmlb.ch"
#include "DbfDbe.ch"
// manejadores de archivos
? DbInfo( DBFDBO_DBFHANDLE ) // resulta: 8
? DbInfo( DBFDBO_DBTHANDLE ) // resulta: 9
El ejemplo ilustra que el primer parámetro pasado a DbInfo() es una constante #define que es o bien una constante
universalmente válida (prefijo DBO_) o una constante DBE específica. En el caso del DBFDBE, las constantes
específicas para la función DbInfo() comienzan con el prefijo DBFDBO_ y están contenidas en el archivo #include
DBFDBE. (Para resumir: las constantes que contienen DBE_ son válidas para un DBE, y para la función DbeInfo().
Las constantes que contienen DBO_ son válidas para los objetos de base de datos y para la función DbInfo()). Las
constantes para DbInfo() que son dependientes del DBE actual se listan en la sección Especificaciones del Motor
de Base de Datos - formato de archivos,
Se inicializa el DBO con todas las configuraciones actuales del DBE cuando el archivo está abierto. Si se hacen los
cambios más tarde al DBE usando DbeInfo(), todos los DBOs permanecen sin efecto por el cambio. Esto significa
que todas las áreas de trabajo donde los archivos están abiertos no están afectados por los cambios hechos a un
motor de base de datos. Cambios como aquellos afectan sólo las áreas de trabajo donde el archivo está abierto
luego de que se produjo el cambio.
La función FieldInfo()
Ni bien se abre un archivo en un área de trabajo, las variables de campo (campos) existen dentro de este área de
trabajo. Similar a Clipper, la información sobre un campo puede determinarse usando las funciones FieldName() o
FieldPos(). Xbase++ también incluye a la función FieldInfo() para leer o cambiar información sobre un campo in-
dividual en un área de trabajo. La función FieldInfo() se comporta de manera similar a DbeInfo() y DbInfo(), y toma
una constante #define como segundo parámetro. Las constantes válidas para FieldInfo() se listan en la siguiente
tabla:
FieldInfo() provee información importante sobre los campos en la base de datos como la longitud y el número de
espacios decimales. Ejemplo:
USE Partes
El primer parámetro de la función FieldInfo() es la posición ordinal de un campo (como fue retornada por FieldPos())
y el segundo parámetro es una constante #define que designa qué información es pedida. Para determinar la
longitud de un campo o sus espacios decimales, pueden definirse dos pseudofunciones simples para su traduc-
ción por el preprocesador de llamadas a FieldInfo():
El tipo de datos de un campo es también información importante y puede determinarse pasando la constante
FLD_TYPE o FLD_NATIVETYPE. En ambos casos, FieldInfo() devuelve un valor de caracter identificando al tipo
de dato. Usando las dos constantes el tipo de dato que está disponible para manipular por los comandos y las
funciones apropiadas de Xbase++ puede distinguirse del tipo de datos original almacenado en la base de datos.
Ellos son a veces, pero no siempre, idénticos. Por ejemplo, en el nivel de lenguaje de Xbase++ sólo existe un tipo
de dato numérico. Cuando se almacenan los números en los campos, sin embargo, los números enteros y de
coma flotante podrían tratarse de manera diferente. Xbase++ reconoce a los números y otros datos internamente
y distingue entre tipos de datos que pueden existir en el nivel del lenguaje y en el nivel de la base de datos. Co-
rrespondientemente, FieldInfo() puede leer los tipos de datos de un campo como existen en el nivel de lenguaje
(FLD_TYPE) o en el nivel de base de datos (FLD_NATIVETYPE). Para determinar el tipo de datos de un campo a
nivel de lenguaje de Xbase++, la constante FLD_TYPE se pasa a FieldInfo(). El tipo de datos es devuelto por
FieldInfo() equivalente a los valores de retorno de Valtype() o Type().
La identificación numérica de los tipos de datos usa constantes definidas en el archivo #include TYPES.CH. Las
constantes de la siguiente tabla están disponibles para determinar los tipos de datos usando FieldInfo() junto con
FLD_TYPE_AS_NUMERIC y FLD_NATIVETYPE_AS_NUMERIC:
*) configurable
Cuando se crea un archivo TXT en el formato SDF, o bien con COPY TO...SDF, DbExport(), o DbCreate(), se crea
un segundo que contiene la descripción de la estructura para los campos en el archivo TXT. Por omisión este
archivo tiene la extensión SDF y es un archivo ASCII que podría también crearse usando en editor de texto simple.
Se soporta a la definición de la estructura en el formato archivo familiar INI que puede escribirse en su forma
generalizada como sigue:
[<Sección>]
<Clave>=<Valor>
En un archivo SDF hay dos secciones: [INFO] y [FIELDS]. El siguiente ejemplo de programa crea un archivo TXT
en el formato SDF que ilustra lo siguiente:
PROCEDURE Main
LOCAL i
DbeLoad( "SDFDBE" ) // crea archivo TXT
DbCreate( "TEST", { {"CARACTER" , "C", 10, 0}, ;
{"FECHA" , "D", 8, 0}, ;
{"LOGICO" , "L", 1, 0}, ;
{"NUMERICO" , "N", 6, 2} },"SDFDBE" )
El ejemplo crea un archivo TXT con cuatro campos y añade diez registros al archivo. El archivo TEST.SDF se crea
como un archivo de estructura junto con el archivo de datos TEST.TXT que contiene los datos creados en el ciclo
FOR..NEXT. El archivo de estructura TEST.SDF se ve así:
[INFO]
file=TEST.TXT
fieldcount=4
recsize=27
reccount=10
[FIELDS]
CARACTER=C,10,0
FECHA=D,8,0
LOGICO=L,1,0
NUMERICO=N,6,2
[END]
La primer sección [INFO] describe al archivo TEST.TXT que contiene los datos. Esto incluye el nombre del archivo
sin el directorio (archivo), el número de campos (fieldcount), la longitud de un registro (recsize) y el número de
registros (reccount). La segunda sección [FIELDS] describe los campos en el archivo TEST.TXT. Los nombres de
los archivos aparecen sobre el lado izquierdo del operador igual y el tipo de datos, la longitud del campo y los
espacios decimales se especifican como una lista separada por comas en el lado derecho. La definición se termina
con la sección [END]. Note que ambos archivos (archivos SDF y TXT) deben localizarse en el mismo directorio.
Luego de la ejecución del código de ejemplo citado anteriormente, el archivo TEST.TXT contiene diez registros
con el siguiente contenido:
A 19950822F000.50
BB 19950823T002.00
CCC 19950824F004.50
DDDD 19950825T008.00
EEEEE 19950826F012.50
FFFFFF 19950827T018.00
GGGGGGG 19950828F024.50
HHHHHHHH 19950829T032.00
IIIIIIIII 19950830F040.50
JJJJJJJJJJ19950831T050.00
El DBE SDF manipula y permite que las operaciones con bases de datos se lleven a cabo en el archivo. Esto in-
cluye definir las condiciones de filtro usando SET FILTER o DbSetFilter(), así como crear las relaciones con
DbSetRelation() o SET RELATION. Si el SDFDBE está conectado a un DBE compuesto que contiene el NTXDBE
(SDFNTX), los índices pueden crearse también para un archivo ASCII que existe en el formato SDF.
El formato SDF impone ciertas restricciones en la programación. Por ejemplo, no es posible borrar registros,
porque no se proveen marcas de borrado en el formato de archivo SDF. Por esta razón, una llamada a función
DbDelete() provoca un error durante el tiempo de ejecución usando el SDFDBE. Además, el SDFDBE abre el
archivo exclusivamente lo cual significa que los archivo ASCII en el formato SDF no pueden usarse simultánea-
mente en la operación concurrente de varios programas. La siguiente tabla lista todas las operaciones comunes
que se encuentran soportadas por el SDFDBE:
Configuración de SDFDBE
El DBE SDF puede configurarse específicamente usando la función DbeInfo(). El formato de archivo SDF y el
mecanismo de acceso puede afectarse cuando se crean el archivo SDF o TXT. La siguiente tabla da un vistazo de
las constantes especiales #define que pueden pasarse a la función DbeInfo() cuando el DBE es SDFDBE:
SDFDBE_AUTOCREATION
Esta constante determina si sólo el archivo SDF o el archivo SDF y el archivo TXT son creados por
DbCreate(). El valor predeterminado para esta configuración es .F. (falso) y la función DbCreate() crea
únicamente el archivo SDF (archivo de estructura) y no al archivo TXT por predeterminación. Si el archivo
TXT no existe cuando USE o DbUseArea() abre al archivo, se lo crea.
Si esta configuración es .T. (verdadera, DbCreate() genera al archivo SDF así como un archivo TXT vacío.
Cualquier archivo TXT existente con este nombre está sobrescrito.
SDFDBE_DECIMAL_TOKEN
El delimitador de los espacios decimales para los valores numéricos puede establecerse usando este valor.
Lo que está predeterminado es el período. La coma se especifica como el separador de los espacios de-
cimales en los valores numéricos por el siguiente código:
Cuando se especifica al delimitador como el caracter ASCII 0 (= Chr(0)) el SDFDBE ignora cualquier deli-
mitador para los campos numéricos. Usa entonces las especificaciones de los campos como se lo encuentra
en el archivo de estructura extendida (archivo SDF). Por ejemplo:
archivo SDF:
CARACTER=C,4,0
NUMERICO=N,6,2
archivo TXT:
AAAA004321
BBBB987654
En este caso el campo NUMERICO tendrá el valor 43.21 para el primer registro y 9876.54 para el segundo.
para proveer una conversión de datos con facilidad entre los hosts de Xbase++ y VMS.
SDFDBE_LOGICAL_TOKEN
La configuración especifica los dos caracteres usados para los valores lógicos y especifica los valores que
representan verdadero y falso. Lo predeterminado es "TF". Una cadena de caracteres que contiene dos
caracteres se especifica para esta configuración. el primer caracter representa verdadero y el segundo re-
presenta falso. El siguiente código de programa especifica el caracter "1" para el valor lógico verdadero y al
caracter "0" para el valor lógico falso.
SDFDBE_STRUCTURE_EXT
Esta constante determina la extensión del archivo para el archivo de estructura (archivo SDF). Lo prede-
terminado es .SDF.
*) configurable
El DELDBE manipula archivos ASCII cuyas filas o registros tienen longitudes variables. Este DBE se utiliza con los
comandos COPY TO...DELIMITED y APPEND FROM...DELIMITED. Soporta también operaciones con base de
datos básicas, como DbCreate(), DbUseArea(), y DbSkip(). Los filtros y las relaciones pueden establecerse tam-
bién si el DELDBE está acoplado con el NTXDBE. El comando DELETE y la función DbDelete() borran registros
pero tienen apenas un significado diferente. El borrado no se produce vía una marca de borrado (como con
DBFDBE), sino que resulta en un borrado inmediatamente físico. Luego de llamar a DbDelete(), un registro no
puede llamarse nuevamente cuando el DELDBE manipula a un archivo. La siguiente tabla da un vistazo de las
operaciones con base de datos que no están soportadas por el DELDBE:
Un archivo ASCII en el formato delimitado no contiene datos de estructura. Por tanto, los nombres de los archivos
y la estructura del archivo (más precisamente, el arreglo DbStruct()) también se comportan de manera muy es-
pecial bajo DELDBE. El arreglo DbStruct() sólo determina el número de campos y los tipos de datos. Puesto que la
longitud de los archivos es variable en el formato delimitado, el arreglo de estructura no puede establece las
longitudes de los campos o el número de espacios decimales para los campos numéricos. Los campos de un
archivo delimitado pueden accederse usando un nombre de alias. Los nombres de los campos comienzan con
"FIELD" y se los enumera desde 1 hasta FCount(). El siguiente código del programa ilustra esto:
en este ejemplo un archivo TXT en el formato delimitado se crea usando el comando COPY TO. La descripción de
la estructura se toma desde un archivo DBF.
Modo: DELDBE_AUTOFIELD
El modo DELDBE_AUTOFIELD es el modo operativo predeterminado. El DELDBE se encuentra en este
modo luego de que se lo carga usando DbeLoad(). Si un archivo TXT se crea usando DbCreate() para un
archivo en formato delimitado, puede llenarse con los datos usando los comandos APPEND BLANK y
REPLACE.
#include "DelDbe.ch"
En este código, un archivo TXT con cuatro campos se crea en formato delimitado. Los campos no corres-
ponden a los nombres del arreglo de estructura pero se enumeran y prefijan con "FIELD". Además, se
configuran los tipos de datos para los campos. Luego que el código citado anteriormente se ejecuta, el ar-
chivo "Auto.txt" contiene:
"A","a",10.00,T
"BB","bb",100.00,F
"CCC","ccc",1000.00,T
Estas líneas demuestran los datos en formato delimitado: los valores de caracter están comprendidos entre
comillas, los valores numéricos consisten sólo de dígitos, y los valores lógicos consisten de un caracter
alfabético.
Modo: DELDBE_MULTIFIELD
El modo DELDBE_MULTIFIELD es un modo operativo que fue creado especialmente para controlar ar-
chivos para formularios carta (form letters). Los archivos de control son empleados por los programas
procesadores de texto y contienen los datos de las variables para los formularios carta. Generalmente la
primera línea de un archivo de control contiene los nombres de los campos, o los nombres de las variables
definidos en la primera línea de un formulario carta que se reemplazan por valores desde el archivo control.
En el modo operativo multi-campo, la primera línea del archivo TXT contiene los nombres de los campos. El
siguiente ejemplo crea un típico archivo de control en el que se modifican los caracteres delimitadores para
los valores de caracter y los separadores para los campos:
#include "DelDbe.ch"
? DbeSetDefault( "DELDBE" )
// conmuta a multi-campo
DbeInfo( COMPONENT_DATA, DELDBE_MODE, DELDBE_MULTIFIELD )
// punto y coma en vez de
// coma
DbeInfo( COMPONENT_DATA, DELDBE_FIELD_TOKEN, ";" )
// sin separador para
// valores caracter
DbeInfo( COMPONENT_DATA, DELDBE_DELIMITER_TOKEN, Chr(0) )
DbCreate( "Multi", aStruct, "DELDBE" )
En este código un archivo TXT en formato delimitado se crea conteniendo cuatro campos. Los nombre de
las variables corresponden a los nombres del arreglo de estructura y se almacenan en la primera línea del
archivo TXT. Se utiliza un punto y coma como separador de campos y los valores de los caracteres no están
comprendidos en los caracteres delimitadores. Después de la ejecución de este ejemplo, el archivo "Mul-
ti.txt" contiene:
CARAC1;CARAC2;NUM;LOGICO
A;a;10.00;T
BB;bb;100.00;F
CCC;ccc;1000.00;T
La primera línea contiene los nombres de los campos que corresponden a los nombres de las variables del
archivo control. Las líneas subsecuentes contienen datos para el formulario carta. La acción predeterminada
de delimitar los valores de caracteres se apaga. Esto se hace especificando al caracter Chr(0) como el
caracter delimitador. El punto y coma se define como el separador de campos.
En el modo operativo "multi-campo", los campos de un archivo TXT en formato delimitado pueden especi-
ficarse por sus nombres de campo, ya que los nombres de campo están definidos en el archivo TXT. Este es
un caso especial y es generalmente usado para crear archivos de control para el procesamiento de texto
desde los archivos DBF.
Modo: DELDBE_SINGLEFIELD
El modo DELDBE_SINGLEFIELD es un modo operativo que permite que cualquier archivo ASCII sea
abierto y manipulado por el DELDBE. En este modo, el DELDBE trata a un archivo ASCII como si el archivo
contuviera sólo un campo. El campo se trata como carente de delimitadores, pero los caracteres que indican
el final del registro indican el final del registro requerido. Para un archivo ASCII, esto corresponde gene-
ralmente a los caracteres del final de línea (Retorno de carro + avance de línea = Chr(13)+Chr(10)) En modo
"único campo", un campo es idéntico a un registro y el DELDBE lee un archivo ASCII línea por línea. Esto
permite que operaciones de una base de datos como DbSkip(), DbGoTop() o DbGoBottom() puedan eje-
cutarse en un archivo ASCII. en este modo operativo el DELDBE puede manipular muchos archivos ASCII.
Por ejemplo:
#include "DelDbe.ch"
? DbeSetDefault( "DELDBE" )
FOR i:=1 TO 3
DbAppend()
REPLACE FIELD->FIELD WITH Replicate(Chr(64+i),i)
NEXT
DbCloseArea()
Independientemente del arreglo de estructura provisto por DbCreate(), un archivo TXT en el modo de único
campo se crea se crea como un archivo vacío que carece de información. El DELDBE trata al archivo TXT
como si fuera un archivo con un único campo. El nombre del archivo es FIELD. Con excepción de los ca-
racteres que representan el final del registro, el DELDBE no reconoce cualquier otro caracter delimitador en
el modo operativo. Los contenidos del archivo "Single.txt" creados en el ejemplo citado anteriormente será
así:
A
BB
CCC
Cada línea de archivo contiene sólo los datos escritos en el archivo usando REPLACE luego que DbAppend()
agregó un nuevo registro.
El modo operativo de "único campo" es ideal para tener una vista de cualquier archivo ASCII con filas que
pueden identificarse por caracter(es) de fin de línea distintos.
DELDBE_RECORD_TOKEN
El final de un registro en un archivo TXT se lo identifica generalmente por los dos caracteres retorno de
carro+avance de línea (Chr(13)+Chr(10). El delimitador para un registro puede modificarse usando la fun-
ción DbeInfo(). Además debe contener al menos un caracter y un máximo de dos caracteres.
DELDBE_FIELD_TOKEN
En el formato delimitado (modo auto-campo y multi-campo), los campos están separados entre sí por un
separador. La coma es el separador por predeterminación. La siguiente línea de programa especifica al
punto y coma como el separador:
DELDBE_FIELD_TYPES
En el modo auto-campo, los tipos de datos de los campos se reconocen de acuerdo a si al formato de los
datos están almacenados en un archivo TXT. El reconocimiento tipo de datos predeterminado puede ser
omitido cuando se especifica el tipo de dato para cada campo:
En este ejemplo, los cinco campos están a disponibles en el archivo delimitado y sus valores se recuperan
teniendo los tipos de datos Fecha, Caracter, Numérico y Lógico . Note que los valores del tipo de Fecha sólo
pueden recuperarse cuando los datos consisten de 8 dígitos en el formato YYYYMMDD.
DELDBE_DELIMITER_TOKEN
Los valores del tipo "caracter" se encuentran entre ciertos caracteres con función de paréntesis en el formato
delimitado. La doble comilla está predeterminada. La siguiente llamada especifica la comilla simple como el
caracter delimitador para los valores de caracteres:
DELDBE_DECIMAL_TOKEN
El caracter utilizado para marcar los espacios decimales en los valores numéricos pueden especificarse
también. El punto está predeterminado. La siguiente línea del programa especifica a la coma como el se-
parador de espacios decimales en los valores numéricos:
Cuando el caracter para los espacios decimales se modifica, no debe ser el mismo que el separador de
campos. Si es el mismo, los lugares después de la coma se los ve como un campo separado que contiene
otro valor numérico.
DELDBE_LOGICAL_TOKEN
Para los valores lógicos, los caracteres que representan verdadero o falso pueden especificarse. Lo que
está predeterminado es "TF". si esta configuración se modifica, una cadena de caracteres que consiste de
dos caracteres debe especificarse. El primer caracter representa verdadero y el segundo caracter repre-
senta falso. La siguiente llamada especifica al caracter "Y" para el valor lógico verdadero y al caracter "N"
para el valor lógico falso:
Los caracteres alfabéticos deben indicarse para los valores lógicos utilizados con el DELDBE. Los carac-
teres "1" y "0" para "verdadero" y "falso" no están permitidos porque se los interpreta como valores numé-
ricos.
DELDBE_MAX_BUFFERSIZE
El formato delimitado permite que los campos y registros se agranden o alarguen. Un campo de caracteres
que contiene 10 caracteres puede asignársele una cadena de caracteres conteniendo caracteres de 20K. El
espacio de memoria ocupado por un campo en un registro debe agrandarse dinámicamente (y también
reducirse) después de ser agrandado. Esto ocurre automáticamente con el DELDBE. La ubicación dinámica
de espacio de memoria en un archivo (no en la memoria) implica una consideración limitada de la velocidad
en las operaciones con base de datos con DELDBE. Para optimizar la velocidad del DELDBE, el buffer de
lectura para un registro cargado usando el DELDBE está limitado a 1 KB (1024 bytes). Ni bien un registro
más largo que 1024 byte se lee, el buffer de lectura se duplica. Este comportamiento requiere que se defina
un límite superior, o la máxima longitud de un registro que no puede exceder ningún registro. El límite su-
perior para la longitud de un registro se establece en 64KB por predeterminación. Cuando se anticipa a
cualquier línea de un archivo ASCII manipulado por el DELDBE de no ser más larga que caracteres de 64KB,
el tamaño máximo del buffer de lectura debe incrementarse. La siguiente línea del programa aumenta el
máximo tamaño del buffer de lectura del DELDBE a 256KB:
El DBFDBE se usa como un componente DATA del DBE DBFNTX compuesto que se crea cada vez que se inicia
un programa de Xbase++. Esto ocurre en la función DbeSys() cuyo código fuente está contenido en el archi-
vo ..\SOURCE\SYS\DBESYS.PRG. El DBFDBE lleva a cabo todas las operaciones con las bases de datos que
pueden ejecutarse en un archivo DBF. Esto incluye la navegación usando DbSkip() así como la definición de los
filtros y la marcación de registros para borrado usando DbDelete(). Además, el DBFDBE manipula archivos de
memo que contienen el texto de campos de memo.
Las operaciones con base de datos que requieren un índice no pueden llevarse a cabo por el DBFDBE. Para estas
operaciones el DBE debe combinarse con el componente ORDER para crear un DBE compuesto. Xbase++
provee al NTXDBE que manipula los archivos índice (ver la próxima sección).
El DBFDBE tiene ciertas limitaciones causadas por el formato de los archivos DBF o por los factores asociados
con la operación concurrente con las aplicaciones de Clipper. La siguiente tabla da un vistazo:
Los campos en un archivo DBF se tipean fuertemente y tienen una longitud fija. Esto está predefinido en la es-
tructura del archivo. El valor máximo que puede almacenarse en los campos numéricos depende de la longitud del
campo y el punto decimal. Para los valores negativos un signo menos limita al valor más pequeño adicionalmente.
Cualquier intento de guardar datos numéricos demasiado grandes o demasiado chicos para caber en un campo de
una base de datos resulta en un error durante el tiempo de ejecución. Por tanto, es necesario considerar el rango
de los datos numéricos que se almacenan cuando los campos numéricos están especificados.
En el caso de los caracteres de campo, la longitud del campo limita el máximo número de caracteres que puede
almacenarse. Si el número de caracteres en una cadena excede la longitud del campo, la cadena de caracteres
está truncada y no se eleva ningún error durante el tiempo de ejecución.
Usando el DBFDBE no hay limitación respecto de la longitud de los campos y de los archivos de memo, como hay
con el limite basado en DOS de 64 KB. Los campos de memo pueden almacenar texto más largo que caracteres
de 64 KB, y los archivos de memo (archivos DBT) pueden volverse más grandes que 32 MB. No obstante, estos
temas están aún en consideración cuando las aplicaciones de Xbase++ utilizan la misma base de datos que las
aplicaciones de DOS Xbase.
Las cadenas de caracteres se almacenan en los campos de caracteres o de memo usando al conjunto de ca-
racteres OEM. Si se selecciona al conjunto de caracteres ANSI en un programa empleando SET CHARSET , se
produce una conversión automática ANSI-OEM cuando un valor del campo se lee desde o escribe a un archivo
DBF.
El tamaño máximo para un archivo depende del desplazamiento utilizado para el bloqueo de registros durante la
operación concurrente (ver la descripción de DBFDBE_LOCKOFFSET abajo). Bajo Clipper, el desplazamiento
varía desde la versión 5.01 a 5.2 y puede establecerse a un valor coincidente en Xbase++. Esto permite que
Xbase++ opere concurrentemente con las aplicaciones existentes de Clipper en un entorno de red heterogéneo en
el que algunas de las estaciones de trabajo se ejecutan bajo DOS y otras bajo un sistema operativo de 32 bit. El
número máximo de registros depende del desplazamiento de registros y de su longitud. Generalmente, el número
máximo de registros puede calcularse como el desplazamiento para los bloqueos de registros divididos por la
longitud de un registro.
El DBFDBE usa dos identificadores reservados para las variables de campo: _LOCK y _NULL. _LOCK se utiliza
para el bloqueo automático de registros y _NULL se reserva para propósitos futuros. Si el DBFDBE se ejecuta en
modo de autobloqueo, un archivo DBF tendrá el campo _LOCK (ver debajo).
Esta llamada a DbeInfo() establece la extensión de archivo predeterminada para los archivos DBF en "FBD".
DBFDBE_MEMOFILE_EXT
Esta constante permite que se determine la extensión de archivo predeterminada para los archivos de memo
(archivos DBT). La extensión de archivo predeterminado por el formato de archivo DBT y no pueden modi-
ficarse.
DBFDBE_MEMOBLOCKSIZE
Esta constante devuelve el tamaño mínimo de bloque utilizado para almacenar texto en un archivo de memo.
El tamaño de bloque predeterminado para el DBFDBE es 512 bytes. No puede modificarse. Dentro de un
archivo de memo los textos o una cadena de caracteres se almacenan en bloques múltiples cada uno de 512
bytes. Cuando el texto que tienen sólo 100 bytes de longitud se almacena, aún ocupa 512 bytes en el archivo
de memo.
DBFDBE_LOCKOFFSET
Esta constante devuelve o modifica el desplazamiento del área para los bloqueos de registros. El despla-
zamiento predeterminado es 2 gigabytes. Puede modificarse con el fin de garantizar la operación concu-
rrente de las aplicaciones de Xbase++ con las aplicaciones de Clipper. Lo predeterminado es 1 gigabyte
bajo Clipper 5.01 y 2 gigabytes bajo Clipper 5.2 cuando el archivo NTXLOCK2.OBJ está enlazado con la
aplicación. Cambiar el desplazamiento de los bloqueos de registros debería llevarse a cabo solamente con
un conocimiento detallado del mecanismo de bloqueo para los registros. Por ejemplo:
En esta línea de programa el desplazamiento para los bloqueos de registro manipulados por el componente
DATA del DBE DBFNTX compuesto se establece en 2 gigabytes.
Cuidado: Si el DBFDBE se establece como el componente DATA de un DBE compuesto, la alteración del
desplazamiento debe llevarse a cabo también para el componente ORDER. Por ejemplo, en el DBE
DBFNTX compuesto, el DBFDBE y el NTXDBE deben tener el mismo desplazamiento para los bloqueos de
registro. El desplazamiento para el NTXDBE se establece usando la constante NTXDBE_LOCKOFFSET.
DBFDBE_LOCKMODE
El DBFDBE soporta bloqueos de registros para permitir la operación concurrente de las aplicaciones de
base de datos. El DBFDBE tienen dos modos operativos para bloquear los registros. Un modo operativo es
compatible con Clipper y requiere el que se lleve a cabo el bloqueo y desbloqueo explícito de registros
cuando se actualizan los registros. El otro modo usa el mecanismo de bloqueo extendido de Xbase++ que
soporta el bloqueo automático de registros.
Modo: DBF_NOLOCK
El modo DBF_NOLOCK es el modo operativo por predeterminación. El DBFDBE está en este modo ope-
rativo luego de que se carga usando DbeLoad(). Este es también el modo del DBE DBFNTX compuesto
disponible cada vez que se inicia un programa de Xbase++. En la operación concurrente en este modo
operativo un bloqueo de registros debe establecerse explícitamente usando RLock() o DbRLock() antes de
que un valor en un campo se modifique. Si se asignan nuevos valores a los campos o las variables de campo
durante la operación concurrente sin el establecimiento de un bloqueo de registros, se produce un error
durante el tiempo de ejecución. Este comportamiento es compatible con Clipper.
Modo: DBF_AUTOLOCK
En el modo DBF_AUTOLOCK, el DBFDBE ejecuta automáticamente un bloqueo de registro cuando se
modifica un registro o un campo durante la operación concurrente. El bloqueo se remueve automáticamente
luego que se ha escrito el cambio a un archivo. Este modo no es compatible con los mecanismos de bloqueo
implementados en Clipper. Esto significa que el modo operativo no puede usarse en la operación concu-
rrente entre los programas de Xbase++ y Clipper. El problema surge porque el modo DBF_AUTOLOCK
emplea los mecanismos de bloqueo desde el sistema operativo en vez de un bloqueo de registros lógico
usando un desplazamiento virtual. El sistema operativo permite el bloqueo directamente.
El bloqueo de registros automático manipula suavemente un conflicto potencial que puede ocurrir durante la
operación concurrente: el problema de la "actualización perdida". Este problema puede darse cuando un
registro se procesa al mismo tiempo por dos programas A y B. El programa A lee un registro en la memoria,
edita los valores de los campos y escribe los cambios nuevamente a la base de datos. El programa B lee al
registro al mismo tiempo que el programa A, cambia los valores y escribe su propio conjunto de cambios
nuevamente a la base de datos luego que el programa A ya ha escrito sus datos. Esta situación causa un
escenario de "actualización perdida" porque los datos escritos en la base de datos por el programa A están
sobrescritos por los datos del programa B, y consecuentemente se pierden.
Con el fin de que el DBFDBE reconozca y evite el escenario de la "actualización perdida", una base de datos
debe tener un campo _LOCK de tipo "C" con la longitud del campo 4. Se usa para almacenar el contador en
un formato binario. El modo operativo DBF_AUTOLOCK no funciona sin el campo _LOCK. El DBFDBE usa
el valor de un campo _LOCK para reconocer este problema potencial y evitar el escenario de "actualización
perdida". Por ejemplo:
El programa A lee un registro, el valor de _LOCK debe ser 100. Al mismo tiempo el programa B lee al mismo
registro. El valor de _LOCK para el programa B es siempre 100. el programa A cambia los datos y escribe los
cambios nuevamente en el registro. Cuando se lleva a cabo esta actualización, el DBFDBE bloquea al re-
gistro, escribe los valores en los campos e incrementa los valores en un campo _LOCK. El DBFDBE luego
desbloquea al registro. el valor de _LOCK es ahora 101. Cuando el programa B intenta actualizar al registro
con sus propios cambios, la operación falla porque el valor de _LOCK es aún 100 para el programa B. No
coincide el valor de _LOCK en la base de datos. El programa B quiere cambiar los datos que han sido re-
cientemente modificados por el programa A. Los datos desde el programa A están retenidos y cualquier
intento del programa B de ganar acceso a escritura causa un error recuperable durante el tiempo de eje-
cución.
Este error durante el tiempo de ejecución siempre debería manipularse usando la estructura de control
BEGIN SEQUENCE...ENDSEQUENCE y un bloque de código de error. En este caso es la responsabilidad
del programador decidir el primer o el último cambi o que ha de ser válido en el escenario de la "actualización
perdida". Un ejemplo de como el programa de la "actualización perdida" puede manipularse se muestra en
las siguientes líneas de programa:
USE Cliente
DO WHILE .T.
BEGIN SEQUENCE
REPLACE Cliente->Apell WITH cApell , ;
Cliente->Nomb WITH cNomb
COMMIT
RECOVER
i := Alert( "Los datos han sido cambiados por otra estación", ;
{"No guardar", "Guardar"} )
IF i==2
LOOP
ENDIF
ENDSEQUENCE
EXIT
ENDDO
ErrorBlock( bError )
en este ejemplo, el error durante el tiempo de ejecución se produce en el caso que se capture el escenario
de la "actualización perdida" usando RECOVER, y la decisión acerca de si los datos modificados deberían
guardarse es responsabilidad del usuario. Si se produce el error durante el tiempo de ejecución de la "ac-
tualización perdida", se relee el campo _LOCK y el valor del buffer de datos (memoria) coincide con el valor
en el campo de base de datos. Es luego suficiente reescribir los valores modificados en la base de datos. En
el ejemplo, esto se lleva a cabo utilizando la declaración LOOP que sólo se alcanza si hay un error.
La función DbInfo() devuelve las configuraciones para la instancia de un DBE DBFDBE activo en un área de
trabajo. En vez de un DBFDBE_, las constantes #define para DbInfo() comienzan con DBFDBO_. En el caso de un
objeto de base de datos estas configuraciones pueden leerse únicamente y no redefinirse (READONLY en vez de
ASSIGNABLE).
La mayor diferencia con el DBFDBE es el formato de archivo para los archivos de memo que se manipula más
eficientemente. Los archivos de memo del FOXDBE tienen FPT como su extensión de archivo predeterminada, no
DBT. Con el formato de archivo FPT, es posible almacenar cualquier dato binario en los campos de memo,
mientras que sólo texto puede almacenarse en los campos de memo usando al formato de archivo DBT. Además,
el tamaño de bloque que representa el espacio mínimo utilizado para los datos almacenados puede definirse en un
campo de memo. En el formato DBT el tamaño del bloque es de 512 bytes mientras el FOXDBE soporte los ta-
maños de bloque en el rango de 33 bytes a 64 Kb.
El FOXDBE tiene en cuenta las características específicas de FoxPro del formato DBF además de otros factores
asociados con la operación concurrente con las aplicaciones de FoxPro. La siguiente tabla da un vistazo al res-
pecto:
FoxPro soporta hasta once tipos diferentes de datos para los campos cuando crea bases de datos. En el lenguaje
de definición de datos (DDL), cada tipo de campo se lo identifica con una letra. El DDL de Xbase++ usa las mismas
letras para la mayoría de los tipos de datos. Sin embargo, en algunos casos son diferentes a FoxPro:
Doble B 8 F N
Caracter (texto) *) C 1-254 C C
Caracter (binario) C 1-254 X C
Fecha D 8 D D
De coma flotante F 1-20 N N
Genérico G 4 O M
Entero largo con signo I 4 I N
Lógico L 1 L L
Memo (texto) *) M 4 o 10 M M
Memo (binario) M 4 o 10 V M
Numérico N 1-20 N N
Marca de Tiempo T 8 T C
Monetario Y 8 Y N
*) Los datos son convertidos de acuerdo con SET CHARSET
Si un campo numérico del tipo de Doble ha de crearse en un nuevo archivo de la base de datos, su tipo de campo
se especifica con la letra B en FoxPro, mientras Xbase++ utiliza la letra F para declarar un campo como Double en
la llamada a la función DbCreate(). Estos campos almacenan valores numéricos en el formato binario y requieren
siempre 8 bytes en la base de datos. De esta manera, ellos difieren de los campos numéricos teniendo el tipo DDL
"N" donde los valores numéricos se almacenan como una secuencia de dígitos y donde el rango está limitado por
la longitud del campo. Sin embargo, ambos tipos de campos almacenan datos numéricos y la función Valtype()
devuelve a la letra N en ambos casos. Por tanto, uno debe distinguir entre la definición de un campo (DDL) y el tipo
de dato almacenado en un campo. El FOXDBE extiende lenguaje de definición de datos para los archivos DBF
pero no añade nuevos tipos de datos utilizados para las operaciones durante el tiempo de ejecución de un pro-
grama de Xbase++.
El conjunto de caracteres utilizado para almacenar datos de texto depende de la configuración SET CHARSET
cuando el FOXDBE crea un archivo DBF. Esta información se almacena en el encabezado del archivo y no puede
modificarse después de la creación del archivo. Si un caracter diferente se selecciona posteriormente en un pro-
grama con SET CHARSET, los datos de texto están sujetos a una conversión automática del código de página
ANSI-OEM de acuerdo a la configuración modificada. Note que el FOXDBE distingue texto de los datos binarios
para los campos de caracteres y memo. Sólo el texto se lo convierte automáticamente, mientras los datos binarios
se leen desde o escriben a una base de datos sin esta conversión.
Por omisión, Xbase++ detecta qué esquema de bloqueo debe utilizarse. Sin embargo, bajo determinadas
raras ocasiones puede ocurrir que se seleccione un esquema de bloqueo automático. En este caso el pro-
gramador puede reforzar un esquema de bloqueo particular que sea compatible con la versión de FoxPro
existente.
La función DbInfo() regresa la configuración para la instancia del DBE FOXDBE activo en el área de trabajo. En
vez del FOXDBE_, las constantes #define para DbInfo() comienzan con FOXDBO_. En el caso de un objeto de
base de datos, estas configuraciones sólo pueden leerse y no redefinirse (READONLY en vez de ASSIGNABLE).
El NTXDBE utiliza el valor predeterminado del nombre del archivo índice sin extensión para la opción TAG en el
comando INDEX. Como consecuencia, los nombres del archivo índice no difieren sólo por la extensión salvo que la
opción TAG se use en el comando INDEX. Por ejemplo, esto es ilegal:
En este caso "Nombre" se usa implícitamente como el nombre de TAG para dos índices que resultan en un error
durante el tiempo de ejecución. Esto puede resolverse especificando dos nombre de tag diferentes:
La única configuración que puede establecerse usando DbeInfo() para el NTXDBE es el desplazamiento de los
bloqueos de registro. Puesto que los bloqueos de registro pueden establecerse en las bases de datos que se
ordenan por un índice, el archivo índice debe bloquearse también. Un bloqueo de registros comienza en un des-
plazamiento lógico y el desplazamiento predeterminado es 1 gigabyte. Este desplazamiento puede modificarse.
Cuando se lo modifica para el DBFDBE, debe ingresarse el mismo valor para el NTXDBE. un ejemplo del DBE
DBFNTX compuesto es:
En esta línea de código el desplazamiento para los bloqueos de registro manipulados por el componente ORDER
del DBE DBFNTX compuesto se establece en 2 gigabyte.
El Motor de Base de Datos CDXDBE manipula a los archivos índice en el formato CDX. Este formato de archivo
originado en FoxPro ha sufrido modificaciones. Por omisión el CDXDBE soporta al formato de archivo CDX como
se lo definió en FoxPro. Además, el CDXDBE provee la interoperabilidad completa con Clipper RDDs Six y Comix
(ver debajo).
La mayor ventaja del formato de archivo CDX es su capacidad de mantener índices múltiples en un archivo índice.
Soporta también índices condicionales donde sólo se referencia en el índice un subconjunto de registros de base
de datos. Esto se lleva a cabo por una condición FOR que puede especificarse en la creación de un índice. Se da
un vistazo de las características de CDXDBE en la tabla a continuación:
Si se selecciona una tabla de ordenamiento (ver SET COLLATION) , se la almacena en el archivo CDX. Por ello el
orden de los caracteres se define durante la creación del índice. Este es un cambio importante del formato de
archivo CDX que ha introducido el formato de Visual FoxPro. Xbase++ soporta esta característica así Visual
FoxPro y las aplicaciones de Xbase++ pueden compartir los mismos archivos CDX.
Note que la tabla de ordenamiento definida por Visual Foxpro difiere de la tabla de ordenamiento definida por el
sistema operativo. Cuando una tabla de ordenamiento está activa, el CDXDBE es compatible con la tabla de or-
denamiento de Visual Foxpro mientras el NTXDBE usa a la tabla de ordenamiento del sistema operativo. Por tanto,
el orden de los caracteres puede diferir apenas entre CDXDBE y NTXDBE si bien (aparentemente) la misma tabla
de ordenamiento se selecciona con SET COLLATION.
Además note que los índices en los caracteres de expresión se vuelven no sensibles a las mayúsculas una vez
que la tabla de ordenamiento se selecciona para el CDXDBE. Este comportamiento, nuevamente, es compatible
con Visual Foxpro.
Clipper y CDX: todas las unidades de las bases de datos de CDX (RDD) disponibles para Clipper usan el formato
de archivo CDX como se lo definió bajo FoxPro 2.x. Este formato de archivo no almacena una tabla de ordena-
miento. Por tanto, las aplicaciones de Clipper no pueden usar los archivos CDX creados por el Visual Foxpro o los
programas de Xbase++. Xbase++, sin embargo, puede leer correctamente y actualizar los archivos CDX creados
por Clipper. Si los programas de Clipper se utilizan para compartir los archivos CDX con las aplicaciones de
Xbase++, El programa Clipper debe crear los archivos índices .
La función RunShell() inicia una nueva instancia del procesador de comandos y le pasa una cadena de caracteres
que es ejecutada en la línea de comando. Bajo Windows 95 el procesador de comandos predeterminado es
COMMAND.COM y para OS/2 y Windows NT es CMD.EXE. La función RunShell() debe recibir al menos un pa-
rámetro para ser pasado al procesador de comandos, que puede ser una cadena nula (""):
En la línea de arriba, RunShell() inicia una nueva instancia del procesador de comandos, se abre una nueva
ventana y la aplicación Xbase++ se detiene hasta que la ventana se cierra. Este es el resultado de los valores
predeterminados para otros parámetros que pueden pasarse a RunShell(). Estos parámetros adicionales pueden
especificarse cuando una aplicación Xbase++ es dependiente o independiente del procesador de comandos re-
cientemente iniciado.
Esta línea de código ejecuta un programa asincrónicamente en una nueva ventana. Se especifica el nombre del
programa como el segundo parámetro y no se pasan comandos de línea. La ejecución de la aplicación Xbase++
(la cual invocó a RunShell()) continúa al hacer clic con el ratón en la ventana de la aplicación Xbase++. Cuando el
tercer parámetro contiene el valor lógico .T. (verdadero), el nuevo programa se inicia y la aplicación Xbase++
continúa ejecutando independientemente. Si el valor es .F. (falso), la aplicación Xbase++ espera hasta que el
programa recientemente iniciado haya terminado antes de continuar su ejecución.
El programa puede también iniciarse como un proceso de segundo plano y, en tal caso, la aplicación Xbase ++
permanece activa.
El cuarto parámetro determina cuando el nuevo proceso será iniciado en el segundo plano o en el frente de la
aplicación Xbase++. Por omisión, RunShell() inicia un nuevo proceso en el frente. Esto significa que la nueva
ventana se trae al frente y recibe el foco de entrada. Si una sesión de "pantalla completa" se inicia usando
RunShell(), la función conmuta automáticamente a modo caracter. Cuando el valor lógico .T. (verdadero) se es-
pecifica como el cuarto parámetro, el nuevo proceso inicia en el segundo plano de la aplicación Xbase++ y ésta
retiene el foco.
Los ejemplos de llamadas a programa muestran cómo una aplicación puede iniciarse pasando el nombre del
archivo. El nombre del archivo inclusive puede pasarse a la función RunShell() como un parámetro en la línea de
comandos. Las siguientes dos líneas son equivalentes:
En ambos casos, el programa se inicia asincrónicamente. En la segunda línea, el nombre de archivo está conte-
nido en el parámetro de la línea de comando. Para que esto trabaje, la línea de comando debe comenzar con "/C".
El siguiente código muestra ejemplos adicionales para el uso de la función RunShell():
// Sesión DOS de Pantalla Completa (/DOS) en segundo plano (/B) bajo OS/2
El comando START invoca a un nuevo procesador de comandos y le pasa una línea de comandos. Esto permite la
ejecución de cualquier programa incluyendo el pasaje de parámetros. Cuando se usa START, RunShell() ofrece
una flexibilidad considerable para la iniciación de cualquier programa desde una aplicación de Xbase++ así como
para el pasaje de parámetros (Nota: ingrese "Help Start" o "Start /?" en la línea de comandos para obtener in-
formación detallada sobre el comando START desde la ayuda en línea del sistema operativo).
Usando múltiples threads
Multi-threading es una característica especial del sistema operativo que permite que un programa de aplicación
sea dividido en varios componentes que pueden ejecutarse de manera independiente y simultánea. El ejemplo
clásico para ello es una aplicación que provee la habilidad de evaluar datos y de imprimir informes de una base de
datos mientras se ingresan datos en otra base de datos. La idea es que el usuario comienza una tarea que con-
sume mucho tiempo, e inmediatamente empieza a trabajar con otro procedimiento mientras se ejecuta el primero.
En este ejemplo, la evaluación y el informe de una base de datos se ejecuta en un thread diferente que la rutina
para el ingreso de datos. No obstante, la evaluación de la base de datos y el procedimiento de informe, y el de
ingreso de datos son componentes de la misma aplicación. Otro ejemplo es el programa para la colección de datos
en el que estos últimos se colectan de varias fuentes con cada fuente siendo controlada por un thread separado.
Inclusive cuando los datos se reciben simultáneamente de diferentes fuentes, puede registrarse con confianza.
La clase Thread de Xbase++ ofrece al programador una herramienta para sacar ventaja de multi-threading de una
manera simple y directa. Un objeto thread debe crearse y el objeto debe recibir información acerca de qué código
del programa se ejecuta en el thread. El siguiente ejemplo muestra la aproximación básica:
PROCEDURE Main
LOCAL oThread := Thread():new() // crear objeto thread
CLS
oThread:start( "Suma", 10000 ) // suma números desde 1 hasta 10000
DO WHILE .T. // muestra caracteres durante
?? "." // el cálculo
Sleep(10)
IF ! oThread:active // Verifica si el thread aún se ejecuta
EXIT
ENDIF
ENDDO
El programa tiene el único propósito de demostrar el uso un objeto thread en un corto ejemplo (de otra manera
sería insignificante). El objeto thread se crea usando el método :new() de la clase Thread. Ni bien se creó el objeto
thread, está disponible un thread nuevo. El thread está entonces listo para la ejecución. El código del programa a
ejecutarse en el thread nuevo se especifica llamando al método :start() del objeto thread. El primer parámetro es
una cadena de caracteres que especifica al identificador de la función o procedimiento que ha de ejecutarse en el
thread. En el ejemplo, se especifica la función definida por el usuario Suma(). Todos los demás parámetros (10000
en este caso) se pasan como argumentos a la función que está siendo ejecutada. Luego de llamar al método :start()
se ejecuta el código del programa en el thread nuevo.
En el programa de ejemplo se ejecutan dos ciclos simultáneamente. El ciclo DO WHILE en el procedimiento
Principal exhibe un punto en la pantalla por cada vez que se pasa por el ciclo. El ciclo FOR...NEXT en la función
Sum() calcula una suma simultáneamente. Cuando el ciclo FOR...NEXT en el thread nuevo se termina, el ciclo DO
WHILE se termina también porque la variable de instancia :active señala que un thread nuevo no ejecuta más
códigos. El resultado del cálculo está contenido en la variable de instancia :result del objeto thread.
Se crea el thread cuando se crea el objeto thread. La ejecución del código en el thread comienza usando el mé-
todo :start() . El identificador para la función o procedimiento a ejecutarse en el thread se pasa a este método como
una cadena de caracteres. El símbolo o identificador para la función o procedimiento debe estar disponible durante
el tiempo de ejecución. Esto significa que la función o el procedimiento comenzado en el thread nuevo se declara
como una STATIC FUNCTION o un STATIC PROCEDURE. Mientras el thread está ejecutando un código de
programa, la variable de instancia :active tiene el valor .T. (verdadero). Cuando el thread ha terminado, el valor de
retorno de la última función o procedimiento ejecutado en el nuevo thread se le asigna a la variable de instan-
cia :result .
Las variables declaradas como LOCAL o PRIVATE son visibles en el thread donde ocurrió la declaración. Las
variables declaradas con PUBLIC o STATIC son visibles en todos los threads de un proceso (programa de apli-
cación). Las variables de campo (FIELD) son visibles en un área de trabajo de un espacio de trabajo. Un espacio
de trabajo está ligado a un thread. Puesto que los espacios de trabajo pueden moverse entre threads, las variables
de campo pueden volverse visibles en threads diferentes. En un punto dado en el tiempo, una variable de campo
es visible sólo en un thread.
Cuando se divide el código de un programa en diferentes threads, debe evitarse la posibilidad de múltiples threads
de tener acceso simultáneo a la misma variable (PUBLIC o STATIC) y su modificación. Si múltiples threads están
modificando la misma variable, el valor de la variable no es predecible. El siguiente ejemplo demuestra esta si-
tuación:
PROCEDURE Main
LOCAL i, oThread := Thread():new() // crea objeto thread
PROCEDURE Decremento
LOCAL i
FOR i:=1 TO 10000
snNumero -- // decrementa snNumero
NEXT
RETURN
En el ejemplo, se ejecutan dos ciclos FOR...NEXT simultáneamente en dos threads diferentes. La misma variable
STATIC se accede en ambos threads. El primer thread incrementa la variable 10000 veces y el segundo thread
disminuye la variable 10000 veces. Teóricamente el resultado será el valor cero. En la práctica rara vez se llega a
este valor. Generalmente el valor de snNumero al final del programa es mayor que cero. Esto es porque el sistema
operativo ubica el tiempo del procesador independientemente para los dos threads. El ciclo FOR...NEXT en el
procedimiento Principal comienza incrementando ni bien se inicia el primer thread. Cambiar entre threads lleva su
tiempo y la variable STATIC aumenta varias veces antes de que el segundo thread comience realmente. Cuando
el ciclo FOR...NEXT en el primer thread termina el proceso entero se termina, incluyendo el segundo thread. Esto
significa que el ciclo FOR...NEXT en el segundo thread se cancela antes de que la variable i alcanza el valor 10000.
Por esta razón, el valor de snNumero al final del programa es casi siempre mayor que cero.
Este programa demuestra que la programación con múltiples threads requiere consideración de acontecimientos
especiales. El acceso simultáneo a las mismas variables o archivos por múltiples threads debería evitarse.
Cuando dos threads se encuentran usando las mismas variables, el resultado (o el valor de la variable) no es
predecible, puesto que el sistema operativo ubica que thread recibirá el tiempo disponible del procesador. El
thread que desarrolló una tarea por último establece el valor de la variable. Como regla general, la parte del pro-
grama que debe ejecutarse en un thread separado debería estar escrito de tal forma que pueda ser compilado y
enlazado como un programa independiente. Todas las variables en un thread deberían protegerse del acceso por
otros threads.
PROCEDURE Main
LOCAL i, oThread := Thread():new() // crea objeto thread
PROCEDURE Decremento
LOCAL i
FOR i:=1 TO 10000
snNumero -- // decrementa snNumero
NEXT
RETURN
En este ejemplo, la prioridad del nuevo thread se incrementa en relación al thread actual usando el méto-
do :setPriority() . Esto causa que el tiempo del procesador sea preferencialmente asignado al nuevo thread. Esto
significa que el ciclo FOR...NEXT en el procedimiento Decremento se procesa primero porque el thread en el que
este ciclo se ejecuta tiene una prioridad más alta que el thread en el que se ejecuta el procedimiento Principal. En
este caso, el ciclo FOR...NEXT en el procedimiento Decremento se ejecuta antes que el ciclo FOR...NEXT en el
procedimiento Principal. El resultado del programa es siempre cero porque snNumero disminuye 10000 veces y
luego aumenta 10000 veces.
El ejemplo representa un caso extremo en el cual el orden de la ejecución puede controlarse precisamente al
aumentar la prioridad de threads individuales. Generalmente, el orden de ejecución de los threads (la asignación
de tiempo del procesador) depende de varios factores que son controlados por el sistema operativo.
Por omisión los threads en los programas de Xbase++ tienen prioridad PRIORITY_NORMAL y esto es general-
mente adecuado. Esto resulta en la asignación de tiempo del procesador a una aplicación de Xbase++ con igual
precedencia que con la mayoría de los demás programas. En situaciones normales, la prioridad no debería alte-
rarse. Modificar la prioridad requiere un conocimiento detallado de la manera en la que el sistema operativo dis-
tribuye el tiempo del procesador. Los threads reciben tiempo del procesador basados en prioridad. Los threads de
baja prioridad reciben acceso al CPU si ningún thread con prioridad más alta está siendo ejecutado o si un thread
de prioridad más alta ha entrado en estado de espera. Temporalmente puede asignarse una prioridad alta a un
thread con prioridad baja para permitir que se ejecute.
El objeto thread permite que la prioridad de los threads sea modificada y es la responsabilidad del programador
usar este poder responsablemente. Aumentar la prioridad de los threads sólo provee más tiempo del procesador al
thread desde el sistema operativo. No causa que el programa se ejecute más rápido. En el peor de los casos, si la
prioridad se establece demasiado alto no es posible llevar a cabo los procesos multitarea y multi-threading porque
se le asigna todo el tiempo del procesador a una aplicación de Xbase++ (o un único thread en una aplicación de
Xbase++). En este caso los otros programas no pueden ejecutarse hasta que haya finalizado la aplicación de
Xbase++. Modificar la prioridad de los threads demanda un cuidado especial. Estos mandatos influencian direc-
tamente la ejecución por prioridad de varios programas, o procesos, respectivamente (multi-tareas). Ellas no
afectan el desempeño de una aplicación de Xbase++ individual.
Cada thread manejado por un objeto Thread puede identificarse por un ID numérico. Los IDs del Thread son
números consecutivos, o sea el primer thread tiene el ID 1 y ejecuta el procedimiento Principal. Un objeto Thread
almacena el ID del thread en su variable de instancia :threadID . Cuando se llama a la función ThreadID(), recupera
el objeto Thread del thread actual y devuelve el valor de la variable de instancia :threadID . Esto nuevamente es el
ID numérico del thread actual.
La función ThreadObject() se utiliza de manera similar. Pero en vez del ID numérico, devuelve el objeto Thread
completo que ejecuta la función. Por tanto, el resultado de las siguientes expresiones es siempre idéntico:
Un objeto Thread monitorea el reloj del sistema. Por consiguiente, la rutina MedioDia() en el ejemplo se ejecuta a
las doce en punto a pesar de que el thread se inicia más temprano. El thread actual que ha llamado al méto-
do :start() continua ejecutándose.
Otra forma de ejecución dependiente del tiempo del código del programa lo provee el método :setInterval() . Define
un intervalo para la ejecución repetida del código del programa por un objeto Thread. Cada vez que el intervalo
expira el objeto Thread automáticamente reinicia su thread. Esta funcionalidad también es provista de forma
simplificada por la función SetTimerEvent(). Un ejemplo típico para esto es la exhibición continua de la hora actual,
que puede programarse de diferentes manera:
PROCEDURE MuestraHora_B
DispOutAt( 0, 0, Time() )
RETURN
El resultado de los tres ejemplo es idéntico: el tiempo se despliega una vez que el segundo en la esquina superior
de la izquierda de la pantalla (la unidad para el intervalo es 1/100mo de segundo). La implementación más fácil es
dada por la función SetTimerEvent() que evalúa repetidamente al bloque de códigos.
Una comparación de los procedimientos MuestraHora_A() y MuestraHora_B() revela una importante implicación
que resulta de usar el método :setInterval() . Ejemplo nro. 2 utiliza el ciclo DO WHILE y un estado de espera ex-
plícito (función Sleep()) para el despliegue continuo, mientras que el ejemplo nro. 3 trabaja continuamente sin un
ciclo DO WHILE. En el ejemplo nro. 3, se define un intervalo de tiempo monitoreado por el objeto Thread. Con-
secuentemente, el procedimiento MuestraHora_B() se ejecuta cada vez que el intervalo ha transcurrido y que el
thread entra de manera implícita en el estado de espera entre dos ciclos de ejecución.
SKIP
IF Eof()
ThreadObject():setInterval( NIL )
ENDIF
RETURN
En este ejemplo, se programa una base de datos de consulta que lista los datos de todos los clientes que viven en
Nueva York. El archivo se abre cuando empieza un thread o sea antes de que comience el filtro, y se cierra antes
de que termine el thread. Esto ocurre en los bloques de código :atStart y :atEnd . El código del programa para la
evaluación de la base de datos se implementa sin el típico ciclo DO WHILE .NOT. El ciclo Eof() se vuelve posible
porque el intervalo de tiempo para la ejecución repetida de este código se establece en cero. Como resultado se
nincia el código inmediatamente una vez más cuando se alcanza la declaración RETURN. Cuando el puntero de
registros se mueve hasta el final del archivo (Eof() == .T.), se establece el intervalo en NIL lo que causa que el
thread no repita el código pero que termine.
El ejemplo para la Base de Datos de consulta en la sección previa se utiliza como base para la siguiente clase de
Thread que lleva a cabo las mismas operaciones de base de datos:
oThread := ListaDeClientes():new()
oThread:start( , {|| FIELD->CIUDAD = "New York" } )
******************************
CLASS ListaDeClientes FROM Thread
PROTECTED:
METHOD atStart, execute, atEnd
ENDCLASS
La clase definida por el usuario es instanciada y un bloque de códigos con la condición para la consulta se pasa al
método :start() . A los tres métodos :atStart() , :atEnd() y :execute() se los invoca automáticamente y ejecuta
dentro del thread. El bloque de códigos que se le pasa a :start() también se le pasa al método :execute() . El código
de este método se ejecuta repetidas veces hasta que se alcanza el final del archivo.
El ejemplo muestra los métodos que pueden o deben implementarse en una clase Thread definida por el usuario.
Usa un mecanismo de ejecución repetida de tiempo controlado del código en un thread. En vez de :setInterval( 0 |
NIL ) , también puede usarse un ciclo DO WHILE.
Las posibilidades para coordinar threads diferentes comienza con la detención del thread actual hasta que uno o
más threads hayan terminado. Las funciones ThreadWait(), ThreadWaitAll() y el método :synchronize() de la clase
Thread se utilizan con éste propósito. Cuando se llama a cualquiera de estas funciones o al método, el thread
actual detiene la ejecución del programa y entra en estado de espera. El thread espera para la finalización de uno
u otros threads y retoma la ejecución luego. Mientras espera, el thread no consume recursos de CPU. El siguiente
esquema demuestra esto:
Thread A Thread B
ejecutando
|
oThreadB:start()
| ejecutando
| | // ejecución simultánea
| | // del programa
oThreadB:synchronize(0) |
| // thread B ejecuta código
estado de espera |
RETURN // thread B termina
ejecutando // thread A retoma
|
El Thread A inicia al thread B y espera para su finalización en un punto particular en el programa llamando al
método :synchronize() . No es posible terminar el thread B explícitamente a partir del thread A.
Normalmente, la coordinación de threads vía los estados de espera es necesaria si un thread A necesita el re-
sultado de otro thread B. Por ejemplo, el cálculo de estadísticas extensivas puede hacerse en múltiples threads
donde cada uno recopila los datos desde una base de datos particular y calcula sólo una parte de la estadística. La
consolidación de la estadística entera luego se produce en un único thread que necesita esperar los resultados de
todos los demás threads. Así todos los threads ejecutan diferentes partes del programa al mismo tiempo y deben
coordinarse o sincronizarse en un punto particular del programa. La coordinación puede implementarse por un
thread que se encuentra esperando a los demás, o por un thread que les dice a los demás que abandonen el
estado de espera. La última posibilidad requiere de un objeto Señal para la comunicación entre threads.
Thread A Thread B
ejecutando ejecutando
| | // ejecución simultánea
| | // del programa
| oSignal:wait() // thread B se detiene
| |
| estado de espera // thread A ejecuta código
oSignal:signal() |
| | // thread B continúa ejecutando
ejecutando ejecutando
Cada vez que un thread ejecuta al método :wait() de un objeto Señal entra en un estado de espera y detiene la
ejecución del programa. El thread abandona su estado de espera sólo después de que otro thread llama al mé-
todo :signal() del mismo objeto Señal. De esta manera se establece una comunicación entre threads. Un thread le
informa a los demás threads que abandonen su estado de espera y reasuman la ejecución del programa.
PUBLIC aQueue := {}
**********************
FUNCTION Add( xValor )
RETURN AAdd( aQueue, xValor )
**************
FUNCTION Del()
LOCAL xValor
IF Len(aQueue) > 1
xValor := aQueue[1]
ADel ( aQueue, 1 )
ASize( aQueue, Len(aQueue)-1 )
ENDIF
RETURN xValor
En este ejemplo, el arreglo aQueue se usa para almacenar temporariamente valores arbitrarios. Los valores re-
cuperados desde el arreglo de acuerdo al principio FIFO (Primero Entrado Primero Salido-First In First Out). La
función Add() agrega un elemento al final del arreglo, mientras la función Del() lee el primer elemento y comprime
el arreglo en un elemento: (nota: este tipo de manipulación de datos se llama Cola).
Cuando las funciones Add() y Del() se ejecutan en threads distintos, el Arreglo PUBLIC aQueue se accede si-
multáneamente por múltiples threads. Esto conduce a una situación crítica en la función Del() cuando el Arreglo
tiene sólo un elemento. En este caso puede ocurrir un error durante el tiempo de ejecución:
Thread A Thread B
LOCAL xValor
IF Len(aQueue) > 1
El Thread es El Thread ejecuta su función completamente
interrumpido por el
Sistema Operativo LOCAL xValor
IF Len(aQueue) > 1
xValor := aQueue[1]
ADel ( aQueue, 1 )
ASize( aQueue, Len(aQueue)-1 )
ENDIF
RETURN xValor
xValor := aQueue[1]
El sistema operativo puede interrumpir un thread en cualquier momento para darle acceso a otro thread en la CPU.
Si los threads A y B ejecutan la función Del() al mismo tiempo, es posible que el thread A sea interrumpido in-
mediatamente después de la declaración IF. El thread B puede ejecutar una función hasta que se complete antes
de que el thread A se agenda otra vez para la ejecución del programa. En este caso, un error durante el tiempo de
ejecución en un thread antes que en otro ejecuta la misma función.
La función de ejemplo Del() representa aquellas situaciones en multi- threading que requieren la ejecución de
operaciones en un thread particular antes de que otro thread ejecute las mismas operaciones. Esto puede re-
solverse cuando el thread B se detiene mientras el thread A ejecuta la función Del(). Sólo después que el thread A
ha ejecutado completamente la función puede el thread B comenzar con la ejecución de la misma función. Tal
situación se llama "exclusión mutua" porque un thread excluye todos los demás threads de ejecutar el mismo
código al mismo tiempo.
La exclusión mutua de los threads se alcanza en Xbase++ no en el nivel PROCEDURE/FUNCTION sino con la
ayuda de los métodos SYNC. El atributo SYNC para los métodos garantiza que el código del método sea ejecu-
tado por un solo thread en cualquier momento. Sin embargo, esto está restringido a uno y al mismo objeto. Si un
objeto es visible en múltiples threads y al método se lo llama simultáneamente con ese objeto, la ejecución del
método se serializa entre threads. En contraste, si dos objetos de la misma clase se usan en dos threads y el
mismo método se lo llama con los dos objetos, el código del programa del método se ejecuta paralelamente en
ambos threads. Como resultado, la exclusión mutua sólo es posible si dos threads intentan ejecutar el mismo
método con el mismo objeto. El objeto debe ser una variable de instancia de una clase definida por el usuario que
implementa los métodos SYNC. Un método SYNC se ejecuta completamente en un thread. Todos los demás
threads están automáticamente detenidos cuando intentan ejecutar al mismo método con el mismo objeto.
El ejemplo con el anteriormente mencionado Arreglo PUBLIC aQueue debe programarse como una clase definida
por el usuario con el fin de acceder con seguridad al Arreglo desde múltiples threads:
PUBLIC oQueue := Queue():new()
***********
CLASS Queue // Clase para manejar
PROTECTED: // una Cola
VAR aQueue
EXPORTED:
INLINE METHOD init
::aQueue := {} // Inicializar arreglo
RETURN self
IF Len(::aQueue) > 1
xValor := aQueue[1]
ADel ( ::aQueue, 1 )
ASize( ::aQueue, Len(::aQueue)-1 )
ENDIF
RETURN xValor
En este ejemplo, la clase Queue se usa para manipular un Arreglo dinámico que pueda accederse y cambiarse
desde múltiples threads simultáneamente. Se referencia al Arreglo en la variable de instancia :aQueue y se lo
accede dentro de los métodos SYNC :add() y :del() . El objeto Queue que contiene al Arreglo globalmente visible.
La ejecución del método :del() se la serializa automáticamente entre múltiples threads:
Thread A Thread B
| |
oQueue:del() |
| oQueue:del()
<...>
ADel( ::aQueue, 1 ) el thread es detenido
<...>
RETURN xValor el thread retoma la ejecución
| <...>
| ADel( ::aQueue, 1 )
| <...>
| RETURN xValor
| |
Cuando el thread B quiere ejecutar al método :del() mientras éste método lo ejecuta el thread A, el thread B se
detiene porque quiere ejecutar al método con el mismo objeto Queue. Por tanto los métodos SYNC se utilizan
cuando las múltiples operaciones deben garantizarse para ser ejecutadas en un thread antes que otro thread
ejecute las mismas operaciones. Un método SYNC puede ejecutarse con el mismo objeto sólo en el mismo thread
al mismo tiempo (Nota: si un método de clase se declara con el atributo SYNC, su ejecución se serializa para todos
los objetos de la clase).
Interfaz de usuario y Conceptos de Diálogo
Una larga parte del código del programa para la mayoría de las aplicaciones se asocia con la interfaz del usuario y
permite su acceso a las características de la aplicación. El sistema operativo ofrece dos modos de operar para la
interfaz del usuario, el modo VIO (Video Input Output Mode) y modo GUI (Interfaz Gráfica del Usuario - Graphic
User Interface). El modo VIO es un modo texto basado en el modo operativo que se selecciona cuando está activa
la pantalla completa o cuando el programa de DOS se está ejecutando en una ventana de DOS. El modo GUI es
un modo de operación gráfica. Un ejemplo del modo GUI es el Escritorio. Xbase++ soporta a ambos modos
operativos los cuales hacen posible migrar con facilidad los programas existentes de Xbase escritos para DOS a
un sistema operativo de 32bits.
Para simplificar la migración a programas de DOS Xbase existentes desde el texto basado en modo VIO al modo
GUI, Xbase++ también incluye un modo "híbrido" especial. En este modo, los elementos de programa basados en
caracter pueden mezclarse con elementos de diálogo gráfico. Esto permite al programador migrar programas de
DOS Xbase basados en texto de una forma escalonada a las aplicaciones con interfaz gráfica de usuario com-
pleta.
Este capítulo describe cómo los programas pueden crearse para los diferentes modos operativos e ilustra varios
conceptos para la interfaz de programación de usuarios. Esto incluye aspectos del Ingreso de datos y Salida para
ambos: la programación procedural de las aplicaciones VIO y la Programación Orientada a Objetos de las apli-
caciones GUI, dirigido a eventos.
Estos tres comandos ACCEPT, INPUT y WAIT proveen entradas sin formato. WAIT acepta que se presione una
única tecla mientras que ACCEPT e INPUT permiten que sólo se ingrese un número cualquiera de caracteres. La
entrada vía ACCEPT e INPUT se termina cuando el usuario presiona la tecla Enter. Los caracteres que se in-
gresan usando INPUT se los considera como una expresión y se los compila usando el operador de macro (un
error en la expresión conduce a un error durante el tiempo de ejecución). Los caracteres ingresados usando al
comando ACCEPT permanecen sin modificación y pueden asignárseles a una variable de memoria como cadena
de caracteres.
Los comandos más comúnmente usados para la salida sin formato son los signos de interrogación simple y doble
(? o ??). Estos son equivalentes a las funciones QOut() y QQOut(), respectivamente. Puede producirse la salida de
los resultados de una o más expresiones usando estos comandos. El dispositivo predeterminado de salida es la
pantalla. Los resultados de las expresiones pueden guardarse en un archivo (luego de SET ALTERNATE ON) o
enviados a una impresora (luego de SET PRINTER ON). Si se llama al comando SET CONSOLE OFF antes de
que se produzca la salida, la salida por pantalla se suprime. Después de la supresión de la salida por pantalla, ésta
debe reactivarse usando SET CONSOLE ON después que se haya completado la salida a un archivo y/o a la
impresora.
Los comandos LIST y DISPLAY se usan ambos para la salida de registros desde un archivo de base de datos. El
comando TYPE produce la salida de los contenidos de cualquier archivo de texto. Las opciones TO PRINTER y TO
FILE son válidas con tres de estos comandos, así la salida simultánea a una impresora o a un archivo puede
llevarse a cabo sin llamar a SET PRINTER ON o SET ALTERNATE ON. La salida por pantalla de estos comandos
puede suprimirse llamando primero a SET CONSOLE OFF.
El comando SET COLOR modifica el color del despliegue de la salida por pantalla. Este comando no es realmente
un comando de salida, pero permite que el color de la salida se modifique.
Se encuentran descripciones detalladas de los comandos para las entradas y salidas sin formato, incluyendo los
ejemplo de programa, en la documentación de referencia.
Algunas de estas funciones y comandos afectan sólo la posición del cursor de pantalla pero se las incluye también
porque las coordenadas del cursor marcan la posición en la que se exhiben los datos. Otras funciones y comandos
manejan a la pantalla misma. En el modo VIO, el origen (0, 0) del sistema de coordenadas es la esquina superior
izquierda de la pantalla o de la ventana. La esquina inferior derecha (MaxRow(), MaxCol()) representa a los valores
más grandes de coordenadas que son visibles. La posición del cursor se establece al especificar los Row() y Col()
a ambos los comandos @ o a la función SetPos(). La pantalla se vacía usando CLEAR generalmente al comienzo
de cada programa antes de desplegar cualquier cosa en cualquier momento. Los comandos @...BOX y @...TO
dibujan cajas en la pantalla. Son válidas únicamente para la pantalla y no están disponibles para la impresora.
Los comandos más importantes para la salida formateada son @...SAY lo que produce la salida del resultado de
alguna expresión. La salida usando @...SAY puede producirse en la pantalla o en la impresora. La salida fo r-
mateada difiere de la salido no formateada en que la salida simultánea en la pantalla y en la impresora es impo-
sible. La selección de un dispositivo de salida se lleva a cabo usando al comando SET DEVICE (TO PRINTER o
TO SCREEN). La salida en la impresora se encuentra en la posición actual del encabezado de impresión que
puede establecerse usando las funciones PRow() y PCol(). La función SetPrc() restablece los valores internos a
las coordenadas de fila y de columna pero no reposiciona el cabezal de la impresora.
El comando @...SAY puede expandirse para incluir los datos de entrada usando la opción GET. Alternativamente,
un campo de entrada puede definirse usando al comando @...GET. Estos comandos se usan para definir uno o
más campos de entrada de datos que se producen dentro del comando READ o a la función ReadModal(). READ
y ReadModal() ambas activan el sistema predeterminado Get de Xbase++ (ver la siguiente sección sobre esto).
Teclado y ratón
Bajo un sistema operativo con la interfaz gráfica de usuario el ratón (no el teclado) es el dispositivo de entrada más
importante para controlar a la aplicación. Debido a esto, los programas que se están ejecutando o los módulos
individuales no se controlan por la lógica de programa sino por "eventos". Estos eventos se producen usualmente
por las acciones del usuario. Los eventos, que provienen desde fuera de la aplicación, están almacenados tem-
porariamente por el sistema operativo en una cola de eventos y son luego secuencialmente procesados por la
aplicación. Algunos ejemplos de eventos serían: el ratón se movió, el botón derecho del ratón fue presionado, el
botón izquierdo del ratón fue presionado dos veces, etc. Cuando se presiona una tecla también se lo considera un
evento, que muestra que los eventos pueden provenir desde diferentes dispositivos.
A cada evento se lo identifica dentro del programa por un código numérico. Cada tecla tiene un único valor nu-
mérico asociado y los diferentes eventos del ratón tienen diversos códigos numéricos. En Xbase++, los eventos se
manipulan con un grupo de funciones, independientemente de su origen. Hay además un grupo de funciones que
aseguran la compatibilidad del lenguaje con Clipper. Estas funciones de compatibilidad pueden responder sólo al
teclado y sólo consideran los códigos del teclado definidos en Clipper. Debería notarse que los valores numéricos
asociados con varias de las claves en Clipper no necesariamente coinciden con los códigos de eventos del sis-
tema operativo o de Xbase++ y que los códigos claves antiguos se ofrecen sólo en el rango de 0 a 255. Estas
funciones de compatibilidad sólo consideran a los códigos clave definidos en Clipper. La distinción entre "fun-
ciones de evento" y "funciones de teclado" se muestra también en la siguiente tabla:
Una descripción detallada de las funciones de compatibilidad puede encontrarse en la documentación de refe-
rencia. AppEvent() lee eventos desde la cola de eventos y los remueve también de la cola. El valor de retorno de
AppEvent() es el código numérico que identifica únicamente al evento. La función SetMouse(.T.) debe haber sido
previamente convocada para la función AppEvent() con el fin de registrar los eventos del ratón en el modo VIO. En
el siguiente ejemplo de programa, se termina una vez que se presiona el botón derecho del ratón:
#include "Appevent.ch"
PROCEDURE Main
LOCAL nEvento := 0
CLEAR
@ 0,0 SAY "Presione el botón derecho del mouse para terminar"
SetMouse(.T.) // registra eventos del mouse
El gran número de eventos posibles hace impráctico la programación directa de eventos usando los códigos
numéricos. En cambio, deberían usarse las constantes definidas en el archivo #include APPEVENT.CH. Estas
constantes comienzan con el prefijo xbe (que representa a xb ase e vent) seguido por una letra mayúscula o por
un guión bajo. La letra mayúscula identifica a la categoría para el evento y el guión bajo separa al prefijo del resto
del nombre del evento descriptivo:
Categorías de eventos
Categoría Prefijo Ejemplo
Ningún evento xbe_ xbe_None
Evento de Teclado xbeK_ xbeK_RETURN
Evento Base xbeB_ xbeB_Event
Evento de Mouse xbeM_ xbeM_LbDown
Evento de Componente Xbase xbeP_ xbeP_Activate
Además del valor de retorno que identifica al evento, la función AppEvent() modifica dos parámetros que se pasan
por referencia. Estos se llaman "parámetros de mensaje" y contienen información adicional sobre el evento. En un
sistema dirigido por evento como el de Xbase++, suele ser insuficiente recibir sólo un único código de evento. Por
ejemplo, es generalmente necesario conocer la posición del puntero del ratón si el evento es "clic del ratón".
Cuando se llama a la función AppEvent(), dos parámetros deben pasarse como referencia para la contención de
información adicional sobre el evento después de que regrese AppEvent(). Si el evento es un clic del ratón, las
coordenadas del puntero del ratón están contenidas en el primer parámetro de mensaje como un arreglo de dos
elementos. El siguiente ejemplo ilustra esto:
#include "Appevent.ch"
PROCEDURE Main
LOCAL nEvento := 0, mp1, nFila, nCol
CLEAR
@ 0,0 SAY "Presione el botón derecho del mouse para terminar"
SetMouse(.T.) // registrar evento del mouse
DO WHILE nEvento <> xbeM_RbDown // evento: botón derecho del mouse
nEvento := AppEvent(@mp1,,,0) // espera hasta que ocurra un evento
ENDIF
ENDDO
RETURN
La variable mp1 se pasa por referencia a la función AppEvent(). Después de cada evento del ratón contienen a las
coordenadas de la fila y de la columna del puntero del ratón en un arreglo de dos elementos. En el ejemplo, los
contenidos del arreglo están asignados a dos variables nFila y nCol . Se produce la salida del código de eventos en
esta posición salvo que se trate de un movimiento del ratón. El evento xbeM_Motion ocurre muy frecuentemente
durante el movimiento del ratón y obstruiría la pantalla cuando se despliega.
En el modo VIO, las coordenadas del ratón constituyen la información más importante, contenida en los paráme-
tros de mensaje. En muchos otros casos los parámetros contienen el valor NIL en modo VIO, pero contienen
también información adicional cuando se utilizan los Componentes de Xbase (por supuesto, los Componentes de
Xbase no están disponibles en el modo VIO) o cuando se crean los eventos definidos por el usuario usando la
función PostAppEvent(). El siguiente ejemplo ilustra las relaciones básicas entre las funciones AppEvent() y
PostAppEvent() que (junto con sus parámetros de mensaje) crean las bases para la programación dirigida por
eventos:
#include "Appevent.ch"
PROCEDURE Main
LOCAL nEvento := 0, mp1, mp2
CLEAR
@ 0,0 SAY " DibuCaja | Salir" // Region sensible al mouse
SetMouse(.T.) // registrar mouse
DO CASE
CASE nEvento == xbeU_DibuCaja // Evento de usuario
DibuCaja( mp1, mp2 ) // mp1 = Date(), mp2 = Time()
// desde PostAppEvent()
CASE nEvento == xbeU_Salir // segundo evento de usuario
QUIT
@ MaxRow(), 0
?? "El código de tecla es:", nEvento
@ MaxRow(), 0
?? "Las Coordenadas Son:", mp1[1], mp1[2]
ENDCASE
ENDDO
RETURN
SAVE SCREEN
@ 0, 0 SAY "Haga Clic en la esquina superior izquierda del recuadro"
nArriba := mp1[1]
nIzq := mp1[2]
nEvento := 0
@ nArriba, nIzq SAY "Haga Clic en la esquina inferior derecha del recuadro"
nAbajo := mp1[1]
nDer := mp1[2]
RESTORE SCREEN
@ nArriba, nIzq TO nAbajo, nDer // recuadro de salida
RETURN
En el ejemplo, uno de los dos eventos definidos por el usuario se genera cuando el ratón se clickea en la región
sensible de la primera fila de la pantalla. Las coordenadas del ratón (contenidas en el parámetro de mensaje mp1 )
se usan para distinguir entre el evento xbeU_DibuCaja o el evento xbeU_Salir y se posicionan en la cola mediante
PostAppEvent(). El evento definido por el usuario xbeU_DibuCaja recibe a los valores de retorno de Date() y Time()
como parámetros de mensaje. Cuando se recuperan estos eventos desde la cual mediante AppEvent(), los pa-
rámetros de mensaje se posicionan en las variables mp1 y mp2 . Estos valores se pasan al procedimiento Di-
buCaja() y despliegan en la pantalla con esta función.
El programa es un ejemplo simple que muestra la lógica de la programación dirigida por evento. Los eventos se
identifican por un valor numérico único (usando una constante #define) y contienen información adicional sobre el
evento en los dos parámetros de mensaje. Los valores contenidos en los dos parámetros mp1 y mp2 varían de-
pendiendo del evento. Los valores del parámetro de mensaje pueden variar desde NIL en el caso más simple
hasta datos complejos como arreglos u objetos. PostAppEvent() puede usarse para posicionar cualquier evento en
la cola que pueda recuperarse desde cualquier sitio del programa usando AppEvent().
Se crea más fácilmente un campo de entrada usando el comando @...SAY...GET. La entrada de datos mismas se
inicia usando el comando READ:
En estas cuatro líneas se abre un archivo de base de datos, se definen dos campos de entrada de datos, y se lleva
a cabo la entrada vía el comando READ. La entrada termina cuando lo que se ingresa en el segundo campo fi-
naliza usando la tecla Enter o si se presiona la tecla ESC durante la entrada.
La sintaxis de comando constituye la manera más fácil de programar los campos de entrada de datos. El pre-
procesador traduce la sintaxis a código que crea objetos Get y los almacena en el arreglo GetList . Los objetos Get
incluyen los métodos que permiten el despliegue formateado de los valores y la incorporación interactiva de datos.
Los valores de los datos de entrada pueden estar contenidos en las variables de memoria o en las variables de
campo. El acceso al contenido de las variables de los datos de entrada no es directo, pero se da mediante un
bloque de códigos. Este bloque de códigos se llama el bloque de códigos de datos. Este último es utilizado por el
objeto Get para leer el valor de una variable en el buffer del objeto Get y después para escribir el valor modificado
nuevamente en la variable.
cuando el objeto Get tiene el focos de entrada, el usuario puede modificar los contenidos del buffer de edición. Un
objeto Get usa varios métodos para controlar la navegación del cursor y transferir los caracteres al buffer de edi-
ción. Los objetos Get también pueden llevar a cabo la validación de los datos antes y después del ingreso de datos.
Las reglas para la validación de los datos se especifican en bloques de códigos contenidos en las variables de
instancia del objeto Get.
El comando @...GET crea al objeto que ya contenía un bloque de códigos para acceder a la variable especificada.
Cuando los objetos Get se crean directamente usando el método de clase Get():new() , debe especificarse un
bloque de códigos de datos. Los objetos Get se almacenan en un arreglo que se pasa a la función ReadModal(). Si
se emplea el comando @...GET, el arreglo que referencia a los objetos Get estará contenido en la variable GetList .
El comando READ pasa el arreglo de GetList a la función ReadModal(). La función ReadModal() es la rutina de
edición por predeterminación que accede a varios métodos de edición y despliegue de los objetos Get. El sistema
Get predeterminado de Xbase++ incluye a la función ReadModal() junto con otras rutinas de utilidad globalmente
visibles listadas en la tabla a continuación:
Dos de las funciones en el archivo GETSYS.PRG existen con el fin de asegurar la compatibilidad con el sistema
Get de Clippper. En estas funciones la función de compatibilidad Inkey() se utiliza para recuperar las entradas a
partir del teclado. Esto no es compatible con la función AppEvent() que se usa para leer los eventos de Xbase++.
Para facilitar la migración de las aplicaciones existentes de Clipper a Xbase++, las funciones de compatibilidad se
usan por predeterminación en el modo VIO. Esto significa que el sistema Get lee sólo del teclado usando Inkey() y
no procesa evento alguno. En este modo predeterminado el sistema Get es completamente compatible con
Clipper pero el ratón no está disponible.
La configuración predeterminada se activa cuando se llama a SetMouse(.T.) antes de que se produzca el primer
comando READ o la primer llamada a ReadModal(). Los eventos en el sistema Get se procesan luego usando las
funciones GetEventReader() y GetHandleEvent() en vez de las funciones GetReader() y GetApplykey(). En esta
configuración el ratón está disponible. La función SetAppEvent() debe usarse en vez de SetKey() para asociar el
momento en que se presiona una tecla con los bloques de código. La función GetEnableEvents( .T. | .F.) puede
usarse también para alternar entre el modo de compatibilidad y el modo dirigido por evento del sistema Get de
Xbase.
Las funciones de servicio del sistema Get raramente se requieren. El comando @...SAY...GET es suficiente para
permitir que complejas ventanas de entrada de datos se programen con facilidad. Se necesita un conocimiento
detallado de las funciones adicionales del sistema Get cuando el sistema Get predeterminado se modifica para
alcanzar requerimientos únicos.
#include "Get.ch"
**************
PROCEDURE Main
LOCAL cNombre1 := Space(20), cNombre2 := Space(20)
@ 8,10 SAY "Entrada sin diéresis y á"
@ 10,10 SAY " Nombre:" GET cNombre1 SEND reader := {|oGet| NoDieresis(oGet) }
@ 12,10 SAY "Apellido:" GET cNombre2 SEND reader := {|oGet| NoDieresis(oGet) }
READ
RETURN
La definición de estos campos de entrada se produce usando el comando de sintaxis. El bloque de códigos que
asigna al lector de Get definido por el usuario se especifica usando la opción SEND. En él, el bloque de códigos del
lector es asignado a la variable de instancia oGet:reader . Al lector Get mismo se lo programa en el siguiente
procedimiento:
***************************
PROCEDURE NoDieresis( oGet )
LOCAL bBloqueCodigo, cCaracter, nTecla
ENDDO
IF ! GetPostValidate( oGet )
oGet:exitState:= GE_NOEXIT // postvalidación
ENDIF // ha fallado
ENDDO
oGet:killFocus() // Devolver el foco de entrada
ENDIF
RETURN
Dentro del lector de Get definido por el usuario NoDieresis() , las teclas se leen usando Inkey(). Lo que se ingresa
mediante el teclado se prueba para ver si incluye uno de los caracteres del alemán Ä, Ö, Ü, ä, ö, ü y ß. Si uno de
estos caracteres está presente se lo traduce a los caracteres equivalentes Ae, Oe, Ue, ae, oe, ue, o ss. Todos los
demás caracteres se pasan junto con el objeto Get a la función GetApplykey() que define el comportamiento
predeterminado para la transferencia de caracteres al buffer de edición de los objetos Get.
Con el fin de modificar este lector Get para usar la función AppEvent() en la lectura de eventos (en vez de sólo
presionar las teclas), deben declararse variables adicionales para los parámetros de mensaje pasados por refe-
rencia a AppEvent(). En vez de GetApplykey(), la función GetHandleEvent() debe utilizarse para la manipulación
por predeterminación de otros eventos.
Despliegue de tablas
Junto con los datos de entrada formateados usando @...SAY...GET con READ o empleando los objetos get con
ReadModal(), el despliegue de una tabla completa es una característica de interfaz importante y frecuentemente
usada. Una tabla permite que el usuario visualice interactivamente grandes volúmenes de datos. Los datos en una
tabla están organizados en filas y columnas. Estos datos pueden pertenecer a un archivo de una base de datos
abierta en un área de trabajo o a partir de un arreglo almacenado en la memoria. La clase TBrowse y la clase
TBColumn se incluyen en Xbase++ para permitir que las tablas se desplieguen. Estas clases se utilizan juntas pero
cada una lleva a cabo un conjunto específico de tareas cuando se despliegan las tablas. El despliegue de tablas es
posible sólo bajo los esfuerzos combinados de los objetos de ambas clases. Un objeto TBrowse lleva a cabo el
despliegue de pantalla y controla las filas y columnas actuales de una tabla. Un objeto TBColumn provee los datos
para una columna de la tabla desplegada por el objeto Tbrowse. Los Objetos TBColumn deben asignarse al objeto
TBrowse y los utiliza el objeto TBrowse cuando un usuario visualiza una tabla.
Las clases TBrowse y TBColumn están asociadas con un conjunto de funciones que existen en Xbase++ sólo para
proveer las funciones de compatibilidad con Clipper. El código fuente para las funciones de compatibilidad que se
listan en la siguiente tabla están contenidas en los archivos DBEDIT.PRG, BROWSYS.PRG y BROWUTIL.PRG.
Compatibilidad:
Los objetos TBrowse proveen un mecanismo versátil para el despliegue de los datos en la forma de tabla. Los
objetos TBrowse despliegan los datos tabularmente en una sección definida de la pantalla (la ventana browse).
Las tablas son a veces muy grandes para desplegarse enteramente en la pantalla. Los objetos TBrowse proveen
métodos que permiten que las tablas se visualicen por pantalla. La clase TBrowse está diseñada para que un
objeto de esta clase no sepa nada acerca del origen de los datos que despliega. Para exhibir los datos, un objeto
TBrowse se apoya en más de un objeto de la clase TBColumn con el fin de proveer los datos para las columnas
individuales en la tabla. Un objeto TBrowse despliega los datos provistos en la pantalla. Cada objeto TBColumn
trabaja con sólo una columna de la tabla asociada. El objeto TBrowse maneja su propio cursor de celda y des-
pliega los datos en la fila y columna actual de la tabla (la celda actual) en un color resaltado.
Un usuario puede posicionar el cursor de celda en la tabla usando las teclas del cursor. Un objeto TBrowse incluye
varios métodos que mueven el cursor. Ni bien el usuario trata de mover el cursor de la celda fuera de la ventana
Browse, el objeto TBrowse automáticamente sincroniza los datos visibles en la pantalla con los datos de la tabla
subyacente y desplaza los datos en la ventana browse.
Puesto que el origen de los datos de la tabla no es conocido por el objeto TBrowse, deben especificarse tres
funciones para ejecutar las tres operaciones básicas que pueden llevarse a cabo en una tabla: saltar al comienzo
de la tabla, saltar al final de la tabla y cambiar la fila actual dentro de la tabla. Estas operaciones son comparables
a los comandos de fila GO TOP, GO BOTTOM y SKIP. Estas funciones se proveen a los objetos TBrowse como
bloques de código que se ejecutan automáticamente dentro de los métodos específicos del objeto TBrowse.
La ventana browse en la que el objeto TBrowse despliega a los datos en forma tabular puede dividirse en tres
áreas: títulos de encabezamiento o columnas. líneas de datos que contienen los datos, y pie de página al final de
cada columna. Cada área así como cada columna individual puede delimitarse opcionalmente por una línea se-
paradora.
En contraste con los objetos TBrowse, los objetos TBColumn son objetos muy simples que contienen variables de
instancia pero que no tienen sus propios métodos. Los objetos TBColumn se necesitan para proveer los datos
para las tablas desplegadas usando a los objetos TBrowse y son inútiles sin un objeto TBrowse asociado. Un
objeto TBColumn contiene en su variable de instancia toda la información requerida para el despliegue de una
única columna de una tabla en la ventana browse del objeto TBrowse.
Las variables de instancia de TBColumn más importantes (:block ) contienen un bloque de código de datos que
provee los datos desde el origen de datos para una columna. Este bloque de código podría acceder a una variable
de campo en un área de trabajo, o en una columna en un arreglo. Los objetos TBColumn pueden controlar también
el color de los datos desplegados basados en su valor.
Si se le asignan nuevos valores a las variables de instancia de un objeto TBColumn después que el objeto
TBrowse ha ya exhibido los datos usando al objeto TBColumn, el método oTBrowse:configure() debe ejecutarse
para actualizar las columnas en la ventana browse (ver clase TBrowse).
// define las variables para configurar los recursos del sistema: en este
// caso el texto que será mostrado en el botón.
oBoton:caption := " Borrar "
Estas líneas muestran el proceso básico para la creación de un COMPONENTE DE XBASE operacional. El objeto
se crea primero usando el método de clase :new() . Luego, se asignan los valores a las variables de instancia
usadas para configurar los recursos del sistema, y finalmente se emplea :create() para solicitar recursos del sis-
tema a partir del sistema operativo (en este caso, un botón). Es importante distinguir entre :new() y :create() .
Ambos métodos deben llamarse para generar un Componente de Xbase operacional. El método :new() genera al
objeto y el método :create() solicita los recursos del sistema. La mayoría de los COMPONENTES DE XBASE usan
las variables de instancia solamente para configurar el recurso del sistema. La asignación de valores a las varia-
bles de instancia usadas para la configuración debe ocurrir antes de que se llame al método :create() .
El recurso del sistema puede reconfigurarse después de que un XBP se crea y ha solicitado el recurso al sistema
a partir del sistema operativo. Si se asignan nuevos valores a las variables de instancia usadas para la configu-
ración del recurso del sistema, el método :configure() debe llamarse para implementar los cambios. A continuación
se da un ejemplo:
El texto (o caption) desplegado en el botón es parte de los recursos de sistema del botón. Con el fin de modificarlo,
el recurso del sistema debe reconfigurarse. El método DE LOS COMPONENTES DE XBASE :configure() se usa
para concretar esto (y es uno de los métodos del "ciclo de vida" de un XPB).
El último método en el "ciclo de vida" de un XPB es :destroy() . Este método libera los recursos del sistema que se
solicitaron desde el sistema operativo mediante el método :create() . Esto vuelve al XPB no operacional pero
permanece existente y puede solicitar recursos del sistema nuevamente. En otras palabras, el método :destroy()
carece de influencia sobre el objeto creado usando :new() , pero libera a los recursos del sistema solicitados
usando :create() . El siguiente diagrama ilustra el "ciclo de vida" de un Componente de Xbase y clarifica cuando se
invoca a cada uno de estos métodos:
El método :destroy() generalmente no es necesario. Sin embargo, puede usarse para liberar recursos del sistema
de memoria intensiva explícitamente (por ejemplo, bitmaps). Los recursos del sistema se liberan implícitamente ni
bien se acaban las referencias al objeto de la Parte de Xbase. En este caso, se manipula una Parte de Xbase como
cualquier otra variable de memoria y se la remueve de la memoria por el colector de basura.
Importante: Mientras se llama al método :create() , un componente de Xbase es incapaz de ejecutar cualquier
otro método (hay algunas raras excepciones que están destacadas explícitamente). Inmediatamente después de
la llamada a :new() es únicamente posible asignar valores a las variables de instancia que se usan para configurar
a los recursos del sistema. Sólo después que el recurso del sistema ha sido recuperado desde el sistema operativo
usando :create() un XPB es completamente funcional. Luego éste puede ejecutar todos los métodos disponibles
excepto por :create() . Este método puede llamarse otra vez solamente después de la llamada al método :destroy()
que libera a los recursos del sistema.
#include "Appevent.ch"
PROCEDURE Main
LOCAL nEvento, mp1, mp2, oXbp
En este ejemplo, se crean dos botones y el control completo del programa cae sobre un único ciclo DO WHILE (el
ciclo de eventos). En este ciclo de eventos, los mismos se leen desde la cola de eventos usando AppEvent(). El
tercer parámetro pasado por referencia a la función se le asigna una referencia al Destino para el evento. El
Destino podría ser la ventana o uno de los botones. El evento se procesa en el método :handleEvent() de este
objeto Destino. Cada botón tiene un bloque de código "activate" que se ejecuta automáticamente dentro del mé-
todo :handleEvent() cuando un botón se clickea con el ratón. Dentro de estos bloques de código se exhibe un
caracter en la pantalla usando QOut(). La siguiente ilustración muestra cómo puede lucir la salida por pantalla si
cada uno de los dos botones fue cliqueado varias veces:
Un punto importante que se muestra en el programa de ejemplo es que la función AppEvent() en el ciclo de
eventos no sólo lee al evento desde la cola de eventos sino que también determina el Destino para el evento. El
Destino es siempre un Componente de Xbase y cada XPB incluye al método I:handleEvent()!EF. Tres parámetros
se pasan siempre por referencia al método :handleEvent() : el código numérico de evento y dos parámetros de
mensaje. Cuando se procesa al mensaje dentro del método :handleEvent() , se ejecuta el bloque de código al-
macenado en la variable de instancia :activate del botón. Este bloque de códigos contiene la reacción del XPB al
evento específico. Cuando se hace clic con el botón izquierdo del ratón sobre un botón, se genera el evento
xbeP_Activate y el código numérico de eventos (correspondiente a una constante #define) se devuelve a AppE-
vent(). Cuando éste código numérico de eventos se pasa a :handleEvent() , el método ejecuta al bloque de código
contenido en la variable de instancia :activate .
Para cada XBP hay muchas variables de instancia que pueden contener los bloques de código que se ejecutan en
respuesta a los eventos específicos. El evento se lee primero desde la cola de eventos usando AppEvent(). Luego
la reacción ocurre cuando el bloque de código provisto para el evento se ejecuta. Esta secuencia de eventos se
llama una aproximación de "callback". Por ésta razón todas las variables de instancia que contienen los bloques de
código definiendo las reacciones de los eventos se los refiere como "conectores callback". Un conector callback
contiene tanto el valor NIL o un bloque de código que se ejecutará por el método :handleEvent() cuando se pro-
duce el método específico. La siguiente tabla lista a las constantes #define de los eventos predefinidos para la
interacción con el usuario y los conectores callback correspondientes a disposición del usuario:
Otros eventos
xbeP_Keyboard :Keyboard Ocurrió una entrada de teclado
xbeP_HelpRequest :HelpRequest Se ha pedido Ayuda
xbeP_SetInputFocus :SetInputFocus Foco de Entrada garantizado
xbeP_KillInputFocus :KillInputFocus Foco de Entrada perdido
xbeP_Move :Move XBP movido
xbeP_Paint :Paint XBP redibujado
xbeP_Quit :Quit Aplicación terminada
xbeP_Resize :Resize Tamaño de XBP cambiado
Los conectores callback en ésta tabla se incluyen en todos los XBPs y permiten la interacción del usuario mediante
el procesamiento de eventos. Muchos XBPs tienen conectores callback adicionales que ocurren sólo con XBPs
específicos. Por ejemplo, los botones tienen el conector callback :activate , que puede contener un bloque de
código para ejecutar en respuesta al evento xbeP_Activate. Este conector callback no está presente en otros
XBPs.
En resumen, el programa de ejemplo antedicho muestra el proceso básico para la programación usando los
procesos básicos para la programación de los Componentes de Xbase. Un XBP se crea y recibe el "conocimiento"
de como debería reaccionar ante los eventos a través de los bloques de código que se le asignan a los conectores
callbacks. El método :handleEvent() se controla en el ciclo de eventos en el que se leen los eventos usando
AppEvent(). Esta función también determina qué Componente de Xbase debe procesar cada evento. Al final, la
aplicación es controlada en su totalidad por el usuario que genera los eventos.
PROCEDURE Main
LOCAL nEvento, mp1, mp2, oXbp
SetColor("N/W")
CLS
// Ciclo de Eventos
nEvento := 0
DO WHILE nEvento <> xbeP_Close
nEvento := AppEvent( @mp1, @mp2, @oXbp )
oXbp:HandleEvent( nEvento, mp1, mp2 )
ENDDO
RETURN
Este programa difiere del ejemplo en la sección previa de dos maneras. El procedimiento Main se abrevia y se
instancia la clase MiBoton en vez de la clase XbpPushButton. El método :activate() ha sido redefinido en la clase
MiBoton definida por el usuario para producir una salida usando QOut(). En el ejemplo previo, la salida fue pro-
gramada dentro del bloque de código del callback. El rol de estos últimos en el ejemplo previo es reemplazado por
el método callback :activate() en este ejemplo. El método :activate() se ejecuta entonces cuando se clickea el
botón izquierdo del ratón. Sobre cualquiera de los botones. Los métodos callback se usan para procesar los
eventos en los Componentes de Xbase definidos por el usuario que se heredan a partir de XPB existentes. Ge-
neralmente reemplazan al código en bloques de código del callback y requieren que la clase nueva se derive
desde un Componente de Xbase existente. El método callback de la nueva clase contiene el código del programa
que se ejecuta cuando se produce un evento específico. Cuando se define un método callback, un bloque de
código no debería asignarse en general al conector callback del mismo nombre, puesto que sería ejecutado
después del método callback.
Los XBPS también incluyen al método :status() , que devuelve la condición actual de un XPB en su ciclo de vida.
Este método devuelve un valor numérico que corresponde a la constante #define a partir de un archivo XBP.CH.
Los valores posibles de retorno se listan en la siguiente tabla:
Ventanas y relaciones
Toda la salida en una aplicación GUI se produce en una ventana en la pantalla. El significado general del término
"ventana" es un área rectangular de la pantalla que contiene un despliegue gráfico. La ventana de aplicación vi-
sible en la pantalla consiste de un marco y un número de ventanas adicionales. Todos los Componentes de Xbase
que tienen una representación visible son ventanas en el sentido de ésta definición más amplia.
El programa mismo se ejecuta en una ventana de aplicación que limita al área que usa una aplicación en la
pantalla. La ventana de aplicación contiene elementos de diálogo (Componentes de Xbase) que también repre-
sentan ventanas. Cada aplicación GUI está constituida de varias ventanas. Existe una relación jerárquica entre
éstas ventanas y se la describe usando los términos "padre" e "hijo". Una ventana padre provee el área de des-
pliegue para las ventanas hijas y las ventanas hijas están contenidas en la ventana padre.
La relación padre-hijo entre ventanas representa una relación física y es la relación más importante en la pro-
gramación usando los Componentes de Xbase. Volviendo al ejemplo de la sección previa, los botones son hijos de
la ventana de aplicación y están contenidos en ella. Vista de otra manera, la ventana de aplicación es el padre de
los dos botones y contiene los Componentes de Xbase. Esta relación es muy significativa en el procesamiento de
los eventos del teclado. Cuando un hijo COMPONENTE DE XBASE recibe una tecla que no puede procesar o no
entiende, el evento se envía al padre para su procesamiento. Una modificación del ejemplo en la sección previa
ilustra tal relación:
#include "Appevent.ch"
PROCEDURE Main
LOCAL nEvento, mp1, mp2, oXbp
SetColor("N/W")
CLS
oXbp := SetAppWindow() // Obtener ventana de la aplicación
oBoton := oXbp:childList()[2]
? oBoton:caption // resultado: B
En el programa de ejemplo, el botón "B" se crea en segundo término. Se lo referencia en el segundo elemento del
arreglo de lista de hijos de la ventana de aplicación.
Superponiendo XBPs
Por un lado, el orden de la creación de los XBPs determina su posición en la ventana padre de la lista de hijos. El
XBP que ejecuta su método :create() se despliega primero, también. El que se crea al último se despliega último.
Por tanto, aparece al frente de los otros COMPONENTES DE XBASE. Este aspecto se vuelve importante cuando
los COMPONENTES DE XBASE tienen las mismas posiciones en una ventana o se superponen entre sí. Este es
el caso de cuando se agrupan los XBPs visualmente con un marco que los rodea. Por ejemplo, si una única línea
de campos de entrada (XbpSLE) ha de agruparse con un marco estático (XbpStatic), el marco ha de crearse
primero y luego los campos de entrada han de posicionarse dentro del marco.
Información adicional sobre las relaciones entre las ventanas puede encontrarse en la documentación de refe-
rencia para esta clase. En este punto deberán describirse primero los dos Componentes de Xbase que deben
usarse como el primer o padre mayor en la jerarquía padre-hijo de una aplicación GUI. Estas proveen a la ventana
de aplicación. La ventana de aplicación maneja el área en pantalla en la cual se produce la salida completa de una
aplicación PM y en la cual todos los otros Componentes de Xbase se despliegan (una excepción la constituye las
aplicaciones MDI que pueden tener varias ventanas de aplicación). La ventana de aplicación para una aplicación
GUI se crea en la función AppSys() cuando se inicia el programa de Xbase++. El código fuente para AppSys() está
contenido en el archivo ..\SOURCE\SYS\APPSYS.PRG. Una ventana de la clase XbpCrt se genera por prede-
terminación y permite las salidas basadas en texto y gráfico. Sin embargo, la función AppSys() puede modificarse
para que se provea una ventana de aplicación como un objeto de la clase XbpDialog. En este caso, sólo es posible
la salida gráfica.
Una ventana XbpCrt permite salidas orientadas a gráfico y texto. Además, los Componentes de Xbase pueden
desplegarse en ventanas XbpCrt así los programas desarrollados originalmente bajo DOS pueden migrarse paso
a paso a una aplicación GUI. Las adaptaciones especiales para el procesamiento de eventos no se requieren
puesto que una ventana XbpCrt lleva a cabo estas tareas independientemente. Por ejemplo, considere un pro-
grama simple creado usando una aproximación procedural que implementa la entrada de datos usando una serie
de objetos Get. El registro actual se modifica durante el ingreso vía las teclas de función F6 y F7:
#include "Inkey.ch"
PROCEDURE Main
SetColor( "N/W,W+/N" )
CLS
@ MaxRow()-1, 2 SAY "<F6>-Próximo <F7>-Previo"
PROCEDURE RefrescaListaGet
AEval( GetList, {|oGet| oGet:reset() , ;
oGet:display() } )
RETURN
Se implementa la lógica del programa para cambiar los registros vía los bloques de código que están asociados
con las teclas de función usando SetKey(). Las teclas de función que están enlazadas a bloques de código son
ideales candidatos para el reemplazo con botones en una aplicación de modo híbrido. Esto ayudará a la aplicación
modo de texto un aspecto más GUI. Los botones se agregan con facilidad a la ventana XbpCrt. Los dos botones
necesitan crearse en el procedimiento Main y reemplazan a la función SetKey():
#include "Inkey.ch"
PROCEDURE Main
LOCAL oXbp
SetColor( "N/W,W+/N" )
CLS
SetMouse(.T.)
oXbp:= XbpPushButton():new()
oXbp:caption := "Próximo"
oXbp:activate:= {|| DbSkip( 1), RefrescaListaGet() }
oXbp:create( ,, {10,10}, {90,30})
oXbp:= XbpPushButton():new()
oXbp:caption := "Previo"
oXbp:activate:= {|| DbSkip(-1), RefrescaListaGet() }
oXbp:create( ,, {110,10}, {90,30})
Este programa modificado muestra otra característica de una ventana XbpCrt: es posible emplear las coordenadas
de fila y de columna a modo de texto con un origen en la esquina superior de la ventana así como las coordenadas
gráficas xy con origen en la parte inferior izquierda (los botones están ubicados en la ventana basada en las co-
ordenadas xy). Dentro de una ventana XbpCrt, las funciones de diálogo como AChoice() o MemoEdit() pueden
usarse y lo que se despliega puede hacerlo después de la salida gráfica como los Componentes de Xbase, bit-
maps o un gráfico de barras. La ventana XbpCrt soporta también el sistema de colores del modo de texto junto con
el modo de colores del modo gráficos lo que significa que los colores pueden establecerse usando SetColor() o
GraSetColor().
Puesto que una ventana XbpCrt permite también la salida orientada a texto: su tamaño máximo se limita a los
valores de función de MaxRow() y MaxCol(). El tamaño de la ventana predeterminado es 25 líneas de texto y 80
columnas correspondientes al tamaño de la pantalla de texto. Sobre la plataforma OS/2, el tamaño de la pantalla
puede reducirse usando el ratón, pero no puede agrandarse más que MaxRow() y MaxCol(). Esto no es posible en
las plataformas de Windows. Sin embargo, el tamaño de una ventana en modo de texto puede establecerse
cuando se crea una ventana XbpCrt en cualquier número de filas y columnas deseadas.
En síntesis, todos los comandos y funciones en el alcance del lenguaje de Clipper y la funcionalidad adicional de
Xbase++ y los Componentes de Xbase puede usarse en una ventana XbpCrt. Se crea una ventana XbpCrt para el
propósito exclusivo de permitir una manera fácil de migrar aplicaciones existentes de DOS al entorno de GUI. Esto
tiene la ventaja de permitir la migración sin sobresaltos de aplicaciones existentes a modo de texto a GUI. La
desventaja de esta aproximación es el requerimiento de memoria de una ventana XbpCrt opuesta a la ventana de
diálogo que no permite salida basada en texto. Se paga el requerimiento aumentado de memoria de una ventana
XbpCrt inevitablemente en forma de una pérdida de performance. Por esta razón se recomienda que el nuevo
desarrollo de aplicaciones bajo Xbase++ no use a la ventana XbpCrt. Esto se lleva a cabo al modificar la función
AppSys() en APPSYS.PRG para crear un objeto de la clase XbpDialog() como la ventana de aplicación.
Antes de continuar con la explicación del objeto XbpDialog, vale la pena clarificar qué funciones y comandos no
están más disponibles. Los comandos y funciones listados en la siguiente tabla no pueden usarse si la ventana
actual es un objeto XbpDialog:
Todos los comandos y funciones que afectan al cursor no se encuentran disponibles puesto que el cursor como es
empleado en el modo de texto no existe en el modo gráfico. Similarmente, los comandos y funciones que mani-
pulan la ventana en modo de texto (como por ejemplo SaveScreen(), DispBegin(), Scroll() y RESTORE SCREEN)
no puede usarse en una ventana de diálogo que opera en el modo gráfico. Los Componentes de Xbase del motor
de GRA se utilizan en lugar de la salida de estas funciones y comandos basados en texto.
Junto con estos comandos y funciones hay otros dos pequeños grupos de funciones. El primero representa las
funciones de compatibilidad que no deberían seguir usándose. El segundo grupo de funciones sólo operan si la
impresora ha sido establecida como el dispositivo de salida usando el comando SET DEVICE TO PRINTER. La
siguiente tabla da un vistazo:
Para que se provea a la ventana de aplicación como una ventana de diálogo de gráficos, es necesario la creación
de un objeto XbpDialog en la función AppSys(). Esta función se ejecuta normalmente antes de la llamada del
procedimiento MAIN. El código fuente predeterminado para AppSys() aparece en el archivo APPSYS.PRG pero
puede incluirse en otro archivo si se lo enlaza al archivo ejecutable. Las siguientes líneas se toman a partir del
programa de ejemplo SDIDEMO.PRG (el ejemplo se reduce) y dan un ejemplo de las modificaciones concretadas
a AppSys():
********************************************************************
* Ejemplo para AppSys() en una aplicación SDI
* La función se ejecuta previo a Main()
********************************************************************
PROCEDURE AppSys
LOCAL oDlg, oXbp, aPos[2], aSize, nAltura:=400, nAnchura := 615
// Obtener tamaño de la ventana del escritorio (desktop)
// para centrar la ventana de la aplicación
aSize := SetAppWindow():currentSize()
aPos[1] := Int( (aSize[1]-nAnchura ) / 2 )
aPos[2] := Int( (aSize[2]-nAltura) / 2 )
// Seleccionar font
oDlg:drawingArea:SetFontCompoundName( "8.Helv.normal" )
Después que la ventana de diálogo ha sido creada se recomienda establecer el color de fondo para el área de
dibujo. Esto también establece el color de fondo para el texto desplegado como etiquetas para los XBPs como por
ejemplo XbpStatic, XbpRadioButton o XbpCheckBox. El método :setColorBG() devuelve el color usado para
desplegar todos los XBPs con etiquetas.
Importante: Cada elemento de diálogo (XBP) que se despliega en una ventana de diálogo debe tener
a :drawingArea como el padre en vez del objeto XbpDialog mismo. De otra manera, el borde de tamaño ajustable
de la ventana de diálogo se vuelve el padre y el XBP será visible solamente en este marco de ventana de 2-8 pixel
de ancho.
Por ejemplo, si se exhibe un botón en una ventana de diálogo debe ser un hijo del área de dibujo (:drawingArea )
del objeto XbpDialog. Esta es una diferencia entre cómo trabaja una ventana de diálogo y una ventana XbpCrt que
puede actuar como padre por sí misma. Un objeto XbpDialog es el padre de sólo las dos ventanas implícitas que
manejan el área de dibujo y la barra del título. Ambas ventanas son accesibles mediante las variables de instancia
del objeto XbpDialog. Un ejemplo del despliegue del botón en una ventana de diálogo se muestra en el siguiente
código de programa:
#include "Appevent.ch"
PROCEDURE Main
LOCAL nEvento, mp1, mp2, oXbp
// Crear boton
oXbp:= XbpPushButton():new( SetAppWindow():drawingArea )
oXbp:caption := "Cancelar"
oXbp:create( , , {10,20}, {100,40} )
oXbp:activate := {|| PostAppEvent( xbeP_Close) }
Aquí el padre para el botón se especifica en la llamada a un método. La función SetAppWindow() regresa al objeto
de diálogo XbpDialog creado en AppSys() y se pasa entonces al área de dibujo como referencia al método del
botón :new() . Si el área de dibujo no se especifica como el padre para los Componentes de Xbase, conduce
inevitablemente a un error en el despliegue o no se produce despliegue alguno dentro de la ventana de diálogo. Es
importante recordar que con los objetos XbpDialog el área de dibujo (oXbpDialog:drawingArea ) debe establecerse
como el padre, pero no el objeto de XbpDialog mismo.
Jerarquía de clases de los Componentes de Xbase++
Esta sección da un panorama de la jerarquía de clase de los Componentes de Xbase. La relación entre los dife-
rentes Componentes de Xbase se ilustra debajo así como cuales XBPs utilizan las capacidades de otra clase. Las
clases marcadas con "[A]" son abstractas y no pueden instanciarse por sí solas en el nivel de lenguaje de Xbase++.
Todos los Componentes de Xbase que tienen una representación visible se derivan de la clase XbpPartHandler.
La clase XbpPartHandler implementa la relación padre-hijo y define al método :handleEvent() .
DataRef() - La conexión entre XBP y DBE
En la jerarquía de clase de los Componentes de Xbase, nueve clases se derivan no sólo desde XbpPartHandler o
XbpWindow sino también desde la clase DataRef. La clase DataRef provee a los objetos que referencian datos.
Provee datos desde una variable de memoria o una variable de campo para su uso en un XBP. Todas los Com-
ponentes de Xbase que pueden modificar los datos tienen también DataRef como superclase. La clase DataRef
provee servicios para acceder a los datos. En el caso más simple, estos datos son un valor que existe en el buffer
de edición de un Componente de Xbase y se modifica allí. Los siguientes Componentes de Xbase usan un buffer
de edición para almacenar datos:
Componentes de Xbase derivados a partir de DataRef (XBPs con buffer de edición)
XBP Tipo de datos en el buffer de edición
XbpSLE Cadena de caracteres sin formato (Línea Simple)
XbpMLE Cadena de caracteres con formato (Múltiples Líneas)
Xbp3State Valor Numérico (0, 1, 2)
XbpCheckBox Valor Lógico
XbpRadioButton Valor Lógico
XbpSpinButton Valor Numérico (entre límites Min y Max)
XbpListBox Arreglo (Posición Numérica de las entradas seleccionadas)
XbpComboBox Arreglo y Cadena de caracteres (caso especial)
XbpScrollBar Valor Numérico (-2^15 a +2^15)
El acceso a los datos almacenados en el buffer de edición de un Componente de Xbase ocurre en el méto-
do :getData() que se implementa en la clase DataRef. :getData() devuelve el valor en el buffer de edición. En el
siguiente ejemplo se crea un campo de entrada y se despliegan luego los contenidos actuales del buffer de edición
cada vez que se presiona una tecla:
#include "Appevent.ch"
PROCEDURE Main
LOCAL nEvento, mp1, mp2, oXbp
En el ejemplo los contenidos del buffer de edición del objeto XbpSLE se leen en el bloque de código asignado al
conector callback :keyBoard . Este bloque de código es evaluado después de cada presión de tecla y recibe al
objeto XbpSLE como el tercer parámetro obj .
El ejemplo ilustra que el manejo y la modificación de los datos en un programa apenas requiere Componentes de
Xbase que incluyen un buffer de edición. El valor a editarse se almacena en éste buffer de edición. Generalmente
no es necesaria ninguna otra variable de memoria que referencia al valor.
La clase DataRef provee la variable de instancia :dataLink para enlazar un Componente de Xbase a un campo de
base de datos con el fin de desplegar o modificar variables de campo. Puede asignársele a esta variable de ins-
tancia un bloque de código que define cómo acceder a un campo de base de datos. Este bloque de código provee
la conexión entre un Componente de Xbase, una variable de campo y el DatabaseEngine. El siguiente ejemplo
emplea un objeto XbpSLE para acceder a un campo de base de datos:
#include "Appevent.ch"
PROCEDURE Main
LOCAL nEvento, mp1, mp2, oXbp, oSLE
FIELD APELLIDO
El ejemplo es un poco más complejo porque crea tres tipos de elementos de diálogo: un objeto XbpSLE que puede
editar un campo de base de datos, un botón para cancelar al programa y dos botones para llevar a cabo nave-
gación en la base de datos. Uno de los elementos más importantes en este ejemplo es el bloque de código de
asignado a la variable de instancia :dataLink . Este bloque de código define el acceso al campo APELLIDO de la
base de datos. Este bloque de código se ejecuta dentro de los métodos :setData() y :getData() que se imple-
mentan en la clase DataRef (la clase XbpSLE hereda también a partir de DataRef!). El método :setData() evalúa al
bloque de código sin pasarle el parámetro y asigna un valor de retorno del bloque de código al buffer de edición del
objeto XbpSLE. En otras palabras, el método :setData() lee el valor del campo APELLIDO de la base de datos y lo
copia al buffer de edición del campo de entrada. El método :getData() lleva a cabo la función inversa: lee el valor
desde el buffer de edición del objeto XbpSLE y pasa este valor al bloque de código en la variable de instan-
cia :dataLink . Se produce una asignación a la variable de campo dentro de este bloque de código. :getData() lee el
buffer de edición de un Componente de Xbase y lo escribe en el campo de la base de datos vía :dataLink .
En el ejemplo, se programan dos botones para proveer la navegación en la base de datos. En cualquier momento
dado pueden usarse para seleccionar el registro próximo o previo. Esto ocurre en los bloques de código asignados
a los conectores callback :activate de los botones. En ambos bloques de código se llama al método :getData()
antes que el puntero de registros se mueva y se llama a :setData() después de DbSkip(). Una de las características
especiales de una aplicación GUI orientada a eventos es que el usuario controla la ejecución del programa. En el
programa mismo no puede predecirse cuando un botón particular será presionado o cuando se llevará a cabo la
entrada de datos en el campo de entrada. Debido a esta incerteza, el valor actual en el buffer de edición debe
escribirse nuevamente a una base de datos antes que se mueva el puntero de registros. Y por el contrario, los
contenidos de un campo de la base de datos deben copiarse al buffer de edición inmediatamente después de que
se haya movido el puntero.
En síntesis, todos los Componentes de Xbase usan la funcionalidad de la clase DataRef por lo que pueden editar
los datos (todos aquellos que tienen un buffer de edición). Estos XBPs se derivan desde XbpWindow y las clases
DataRef. DataRef provee el mecanismo para la conexión entre XBP y las variables (tanto variables de campo o de
memoria). Para crear esta conexión, la clase DataRef usa el bloque de código :dataLink y los métodos :setData()
y :getData() . La clase DataRef también provee los métodos que llevan a cabo la validación de los datos (el
método :validate() ) o evitan cambios realizados al buffer de edición (el método :undo() ).
La primer decisión en la implementación de AppSys() es si usar ventanas XbpCrt o XbpDialog. En el caso de una
aplicación GUI debe considerarse también el tipo de aplicación. El concepto "tipo de aplicación" designa al tipo de
interfaz de usuario que proveerá la aplicación. El caso más simple es una aplicación SDI (Interfaz de Documento
Unico - Single Document Interface) en la cual la aplicación consiste de una única ventana. La alternativa es una
aplicación MDI (Interfaz de Documento Múltiple - Multiple Document Interface). Una aplicación MDI se ejecuta en
ventanas múltiples y AppSys() simplemente crea la ventana principal permitiendo que las ventanas adicionales se
generen más tarde en el programa. El tamaño de una ventana de aplicación generalmente depende del tipo de
aplicación. El tamaño de ventana de una aplicación SDI puede ser más pequeña que aquella de una aplicación
MDI, puesto que ninguna ventana de aplicación adicional se necesita en una aplicación SDI. Además, puede
arreglarse el tamaño de la ventana de una aplicación SDI sin permitir que el usuario modifique el tamaño. El ta-
maño de una ventana de una aplicación MDI debe ser modificable por el usuario.
El siguiente ejemplo es un procedimiento AppSys() desde el archivo SDIDEMO.PRG que presenta un ejemplo
completo de una aplicación SDI. Las tareas varias que lleva a cabo AppSys() se demuestran en el siguiente
procedimiento:
PROCEDURE AppSys
LOCAL oDlg, oXbp, aPos[2], aSize, nAltura:=400, nAnchura := 615
// Obtener tamaño de la ventana del escritorio (desktop)
// para centrar la ventana de la aplicación
aSize := SetAppWindow():currentSize()
aPos[1] := Int( (aSize[1]-nAnchura ) / 2 )
aPos[2] := Int( (aSize[2]-nAltura) / 2 )
// Seleccionar font
oDlg:drawingArea:SetFontCompoundName( "8.Helv.normal" )
RETURN
En este ejemplo se crea una ventana de diálogo con de tamaño 615 x 400 pixeles. Este tamaño permite que se
despliegue completamente inclusive en una pantalla de baja resolución. La ventana se provee para una aplicación
SDI y tienen un tamaño fijo (XBPDLG_THINBORDER). La primer llamada a SetAppWindow() provee una refe-
rencia al escritorio en el cual se despliega la ventana de aplicación. El método :currentSize() de este objeto provee
el tamaño de la ventana escritorio que corresponde a la resolución actual de la pantalla. Esta información se usa
para posicionar la ventana de aplicación cuando se llama a una aplicación de Xbase++. En el ejemplo, la ventana
de aplicación se despliega centrada en la pantalla.
Una vez que se establece el color de fondo para el área de dibujo de la ventana de diálogo, el sistema del menú se
genera en la función MenuCreate(). Esta función definida por el usuario (UDF) recibe el valor de retorno del mé-
todo :menuBar() como un argumento. El método :menuBar() crea un objeto XbpMenuBar y lo instala en la ventana
de aplicación. El sistema de menú debe construirse después en el UDF. Se recomienda esto porque la construc-
ción del sistema del menú puede llevarse a cabo antes que una ventana de aplicación sea visible. La mecánica de
la construcción de una sistema de menú se describe en la siguiente sección.
En este ejemplo AppSys(), el mecanismo para la ayuda en línea se implementa luego de la creación del sistema
del menú se crea en MenuCreate(). Esto incluye la generación de un objeto XbpHelpLabel que se le asigna a la
variable de instancia :helpLink . El objeto etiqueta de Ayuda referencia información de ayuda y activa la ventana de
la ayuda en línea. La ventana de ayuda en línea es manipulada por un objeto XbpHelp que ha de proveerse al
objeto XbpHelpLabel. Esto se logra al asignar un objeto XbpHelp a la variable de instancia :helpObject del objeto
XbpHelpLabel. El objeto XbpHelp maneja ventanas de ayuda en línea y debería existir una sola vez dentro de una
aplicación de Xbase++. Por esta razón, se la crea en la función definida por el usuario HelpObject() que se muestra
debajo:
********************************************************************
* Rutina para recuperar al objeto Ayuda. Maneja la ayuda en línea
********************************************************************
FUNCTION HelpObject( cArchivoAyuda, cTitulo )
STATIC soHelp
IF soHelp == NIL
soHelp := XbpHelp():new()
soHelp:resGeneralHelp := IPFID_HELP_GENERAL
soHelp:resKeysHelp := IPFID_HELP_KEYS
soHelp:create( SetAppWindow(), cArchivoAyuda, cTitulo )
ENDIF
RETURN soHelp
Un objeto XbpHelp se crea y almacena en una variable STATIC la primera vez que se llama a ésta función. El
objeto XbpHelp manipula la ventana de ayuda en línea de una aplicación de Xbase++ y es posible recuperar una
referencia a esto llamando a la función HelpObject() en cualquier tiempo en el programa. Esto permite que cual-
quier número de objetos XbpHelpLabel se creen, los cuales activan siempre el mismo objeto XbpHelp (o la misma
ayuda en línea).
La función HelpObject() necesita recibir al nombre del archivo para el archivo HLP y el título de la ventana para la
ayuda en línea. De lo contrario esta función es genérica. Usa también dos constantes #define que hacen refe-
rencia las dos ventanas de ayuda disponibles en cada aplicación. Estas constantes sólo pueden ser definidas por
el usuario y deben emplearse en el código fuente de la ayuda en línea como IDs numéricos con el fin de hacer
referencia a la ventana de ayuda específica.
El sistema de menú consiste de un objeto XbpMenuBar que manipula la barra de menú horizontal en la ventana de
aplicación y varios objetos XbpMenu que se insertan en la barra de menú como submenús. Hay varias maneras de
implementar el control del programa usando los menús. La forma más simple se muestra en el archivo
SDIMENU.PRG (que presenta una aplicación de ejemplo). Los pasos más importantes se muestran en el siguiente
código:
/* Llamada en AppSys() */
MenuCreate( oDlg:menuBar() )
********************************************************************
* Crear sistema de menú en la barra de menú del diálogo
********************************************************************
PROCEDURE MenuCreate( oMenuBar )
LOCAL oMenu
// Primer sub-menu
oMenu:addItem( { "Opciones", } )
oMenu:addItem( MENUITEM_SEPARATOR )
oMenu:addItem( { "~Salir" , NIL } )
/* Y así en adelante... */
Los objetos XbpMenu se crean para contener los ítems del menú. Estos submenúes se crean en la función defi-
nida por el usuario SubMenuNew() (que se muestra debajo), y los ítems del menú están adjuntos a los submenúes
usando el método :addItem() . Un ítem del menú es un arreglo que contiene entre dos y cuatro elementos. En el
caso más simple el primer elemento es una cadena de caracteres que se desplegará como el texto del ítem de
menú y el segundo elemento es NIL. Cualquier caracter en la cadena de caracteres puede identificarse como una
clave de acceso rápido al posicionar un tilde (~) al frente del mismo. El segundo elemento es el bloque de código
que se ejecuta cuando el usuario selecciona el ítem del menú. En este ejemplo, en vez de definir bloques de có-
digo individuales para cada ítem del menú, se asigna un bloque de código al conector callback para el submenú
entero. La posición numérica del ítem seleccionado y el objeto del menú mismo se pasan a la rutina de selección
MenuSelect(). En esta rutina una estructura simple DO CASE...ENDCASE provee ramificaciones al módulo del
programa apropiado.
La inserción de submenúes en el menú principal se lleva a cabo usando el método :addItem() ejecutado por el
objeto XbpMenuBar. El título de la barra del menú sirve como texto para el ítem del menú. En el programa de
ejemplo este texto se establece para un nuevo submenú de la siguiente manera:
********************************************************************
* Crear sub-menu en un menú
********************************************************************
FUNCTION SubMenuNew( oMenu, cTitulo )
LOCAL oSubMenu := XbpMenu():new( oMenu )
oSubMenu:title := cTitulo
RETURN oSubMenu:create()
En esta función el menú principal (o el menú inmediatamente arriba) se provee como el padre para el submenú. La
asignación del título debe ocurrir antes de la llamada al método :create() para el correcto posicionamiento:
Cada aplicación debería tener un ítem del menú de "Ayuda" que generalmente incluye el mismo conjunto de ítems
de menú. En el programa de ejemplo, este menú de ayuda se crea a través de un procedimiento separado que
crea los ítems predeterminados del menú. El control del programa se implementa por bloques de código que se
pasan al menú en el método :addItem() . En este caso el bloque de código del conector callback:itemSelected no
se usa.
********************************************************************
* Crear un menú de ayuda standard
********************************************************************
PROCEDURE HelpMenu( oMenuBar )
LOCAL oMenu := SubMenuNew( oMenuBar, "Ay~uda" )
oMenu:addItem( MENUITEM_SEPARATOR )
El objeto XbpHelp maneja la ayuda en línea que se almacena como una variable estática en la función definida por
el usuario HelpObject(). Esto significa que está siempre disponible cuando se llama a la función HelpObject(). La
información de ayuda predeterminada puede llamarse desde el menú de ayuda al ejecutar los métodos del objeto
XbpHelp provistos para estos propósitos. No existe un método especial para el ítem "Usando Ayuda". Aquí se
especifica al objeto XbpHelp una constante #define que designa el ID numérico para la ventana de ayuda apro-
piada en la ayuda en línea. El mismo ID debe también usarse en el código fuente IPF.
Un menú dinámico para manejar ventanas
Además del menú de ayuda que está disponible en las aplicaciones SDI y MDI, las aplicaciones MDI tienen un
segundo menú predeterminado de ayuda que se emplea para traer diferentes ventanas hijas de la aplicación MDI
al frente. El texto en el título de cada ventana abierta aparece como un ítem del menú y la selección del menú
asigna el foco a la ventana hija correspondiente. Esto requiere una aproximación dinámica al menú, porque el
número de los ítems del menú corresponde al número de ventanas abiertas. Un menú dinámico de ventana se
implementa para este propósito en el archivo MDIMENU.PRG (que es parte del código fuente para la aplicación de
ejemplo MDIDEMO). Es un buen ejemplo de derivar nuevas clases desde una Componente de Xbase. Para
concretar esto, es necesaria una manera de determinar fácilmente el menú principal de una ventana de aplicación
(la ventana padre). La función AppMenu() se incluye en MDIDEMO.PRG para este propósito y devuelve el menú
principal de la aplicación. Hay también sólo un menú de ventana por aplicación y entonces puede guardarse en
una variable STATIC. La función WinMenu() lleva a cabo esta tarea como se muestra en el siguiente código:
********************************************************************
* Crear un menú para manejar ventanas abiertas
********************************************************************
FUNCTION WinMenu()
STATIC soMenu
IF soMenu == NIL
soMenu := WindowMenu():new():create( AppMenu() )
ENDIF
RETURN soMenu
El menú de ventana es una instancia de la clase WindowMenu y recibe el valor de retorno de AppMenu() como su
padre. Esto significa que se despliega como un submenú del menú principal de la aplicación MDI. La clase definida
por el usuario WindowMenu se deriva desde XbpMenu:
********************************************************************
* Clase Menú para manejo de ventanas abiertas
********************************************************************
CLASS WindowMenu FROM XbpMenu
EXPORTED:
CLASS VAR windowStack
CLASS METHOD initClass
METHOD init, addItem, delItem, setItem
ENDCLASS
********************************************************************
// Pila para ventanas de diálogo abiertas como variable de clase
//
CLASS METHOD WindowMenu:initClass
::windowStack := {}
RETURN self
La variable de clase :windowStack se declara para referenciar a las ventanas abiertas. El método de cla-
se :initClass() , cuya única tarea es inicializar la variable de clase con un arreglo vacío también se incluye. Los
cuatro métodos de la clase XbpMenu están sobrecargados. El método :new() termina. El método :init() de una
clase XbpMenu debe llamarse incluso para inicializar las variables de miembro implementadas allí:
********************************************************************
// Seleccionar una ventana vía código de bloque de callback
//
METHOD WindowMenu:init( oParent, aPresParam, lVisible )
::xbpMenu:init( oParent, aPresParam, lVisible )
::title := "~Ventana"
::itemSelected := ;
{|nItem,mp2,obj| SetAppFocus( obj:windowStack[nItem] ) }
RETURN self
Después de la inicialización de la superclase, el título del menú le es asignado en :init() . Se le asigna un bloque de
código a la pila de llamadas :itemSelected . Este bloque de código establece el foco a la ventana cuyo título de
ventana se selecciona desde el menú. La posición numérica del ítem del menú seleccionado se pasa al bloque de
código como el parámetro nItem y obj contiene una referencia al objeto del menú mismo. Dentro de este bloque de
código se accede a la variable de clase :windowStack . :windowStack contiene referencia a todas las ventanas
hijas de la aplicación MDI. La ventana seleccionada se pasa a la función SetAppFocus() que la establece como la
ventana en primer plano.
Los tres últimos métodos de la clase de la ventana del menú permite la inserción, modificación o borrado de la
clase del menú de ventana. Estos métodos tienen los mismos nombres que los métodos de la clase XbpMenu pero
el parámetro pasado a los métodos es diferente. en vez de un arreglo con entre dos o cuatro elementos el pa-
rámetro pasado es un objeto XbpDialog o XbpCrt que ha de recibir foco si el ítem del menú se selecciona.
*************************************************************************
// Usar título de la ventana de diálogo como texto para el ítem de menú
//
METHOD WindowMenu:addItem( oDlg )
LOCAL cItem := oDlg:getTitle()
Se pasa una ventana abierta al método :addItem() . Dentro de este método la ventana se agrega a la variable de
clase :windowStack . El título de ventana se agrega como un ítem del menú al pasarlo al método :addItem() de la
clase XbpMenu. Una característica especial de la ventana del menú es que se despliega en el menú principal
solamente cuando al menos una ventana hija se encuentra abierta. De lo contrario, el ítem del menú de la "Ven-
tana" no aparece en el menú principal. El menú de ventana se inserta como un ítem del menú en su padre (el menú
principal) después de la primera vez en que se ejecuta el método :addItem() .
***************************************************************
// Transferir el título cambiado de la ventana al ítem de menú
//
METHOD WindowMenu:setItem( oDlg )
LOCAL aItem, i := AScan( ::windowStack, oDlg )
IF i == 0
::addItem( oDlg )
ELSE
aItem := ::xbpMenu:getItem(i)
aItem[1] := oDlg:getTitle()
::xbpMenu:setItem( i, aItem )
ENDIF
RETURN self
***************************************************************
// Borrar la ventana de diálogo de la pila de la pila de
// ventanas y del menú.
//
METHOD WindowMenu:delItem( oDlg )
LOCAL i := AScan( ::windowStack, oDlg )
LOCAL nPos := ::setParent():numItems()-1 // el menú de ventanas está
// siempre cerca del último item
IF i > 0
::xbpMenu:delItem( i )
ADel( ::windowStack, i )
Asize( ::windowStack, Len(::windowStack)-1)
IF ::numItems() == 0
::setParent():delItem( nPos )
ENDIF
ENDIF
RETURN self
El método :setItem() se usa cuando el título de la ventana de una ventana de diálogo abierta cambia. Este cambio
debe también hacerse en el ítem del menú de la ventana de menú dinámica. El método :delItem() se llama cuando
de cierra la ventana de diálogo. Este método remueve el título de la ventana de diálogo a partir de la ventana del
menú. Si ninguna ventana hija permanece abierta, el menú de ventana se remueve a sí mismo del menú principal
(el padre) y el ítem del menú "Ventana" no continúa siendo visible.
#include "Gra.ch"
#include "Xbp.ch"
#include "AppEvent.ch"
#include "Mdidemo.ch"
********************************************************************
* Procedimiento Principal y ciclo de eventos
********************************************************************
PROCEDURE Main
LOCAL nEvento, mp1, mp2, oXbp
FIELD NRCLIE, APELLIDO, NOMBRE, PARTENRO, PARTEDESC
CLOSE DATABASE
ENDIF
SET DELETED ON
********************************************************************
* Verifica si todos los archivos del arreglo 'aFicheros' existen
********************************************************************
FUNCTION ExistenLosArchivos( aFicheros )
LOCAL lExiste := .T., i:=0, imax := Len(aFicheros)
En este ejemplo, el procedimiento Main simplemente evalúa si todos los archivos de índice existen y recrean a los
archivos índice si cualquiera de estos no se encuentra. La existencia de los archivos se prueba en la función
ExistenLosArchivos(). Cuando ésta está completa. El procedimiento Main entra en un ciclo infinito que lee los
eventos de la cola usando AppEvent() y los envía al Destinatario llamando al método del Destinata-
rio :handleEvent() .
Mirando esta implementación, la pregunta inevitable es: ¨Dónde y cómo se termina el programa?. El ciclo infinito
en el procedimiento Main no puede terminar basándose en su condición DO WHILE .T.. Una rutina separada se
usa para terminar el programa. El código para esta rutina se muestra debajo:
********************************************************************
* Rutina para terminar el programa
********************************************************************
PROCEDURE AppQuit()
LOCAL nButton
nBoton := ConfirmBox( , ;
"Realmente desea salir ?", ;
"Finaliza Aplicación", ;
XBPMB_YESNO , ;
XBPMB_QUESTION+XBPMB_APPMODAL+XBPMB_MOVEABLE )
IF nBoton == XBPMB_RET_YES
COMMIT
CLOSE ALL
QUIT
ENDIF
RETURN
En la rutina de terminación AppQuit(), se recibe confirmación que el programa debería terminar por determinación
del usuario vía la función ConfirmBox(). Si la aplicación ha de terminarse, todos los buffers de datos se reescriben
en los archivos y todas las bases de datos se cierran usando CLOSE ALL. El comando QUIT luego le da fin al
programa. Si el usuario no confirma que el programa debería terminar, el ciclo infinito en el procedimiento Main
continúa.
Se recomienda generalmente que el código fuente para una aplicación GUI se divida en tres secciones: inicio del
programa, ejecución del programa y fin del programa. El inicio del programa está contenido en AppSys() así como
el código del programa ejecutado dentro del procedimiento Main previo al ciclo de eventos. El ciclo de eventos
mismo es la ejecución del programa. A veces dentro de este ciclo de el código del programa que se generó en
MenuCreate() durante el inicio del programa se produce en el procedimiento definido por el usuario AppQuit(),
donde la verificación por el usuario puede requerirse y cualquier dato puede guardarse.
Hay sólo dos sitios en un programa en los cuales se llama al procedimiento AppQuit(). AppQuit() se lo llama ge-
neralmente a partir de un ítem del menú y desde un bloque de código callback o desde un método de callback. Las
siguientes dos líneas ilustran esto:
En la primera línea, AppQuit() se ejecuta después de la selección de un ítem del menú por lo que deberá ob-
viamente haber un menú conteniendo un ítem del menú que termine la aplicación. La segunda línea define un
bloque de código de callback para que la ventana de diálogo ejecute después que el ícono del menú de sistema de
la ventana de diálogo se haga doble clic o el ítem del menú de "Cierre" se seleccione en el menú del sistema de la
ventana. Generalmente, la rutina para la terminación de una aplicación GUI debería estar disponible en el menú de
la aplicación así como en respuesta al evento xbeP_Close.
Las dos aplicaciones de ejemplo SDIDEMO y MDIDEMO están provistas como ejemplo para aplicaciones GUI
bajo Xbase++. Las dificultades que surgen al acceder bases de datos se toman en cuenta de diferentes maneras
en estos dos programas. En SDIDEMO, se implementa una aproximación procedural y se usa un estilo orientado a
objeto en MDIDEMO. Ambos estos programas de ejemplo resuelven el problema de la no-modalidad de los
campos de entrada resultando de la naturaleza dirigida por evento de una aplicación GUI. Los problemas de
no-modalidad se describen mediante las preguntas: "¨Dónde y cuándo se valida el ingreso de datos? Y "¨Cuándo
se escriben los datos en la base de datos?". Puesto que los campos de entrada de datos se activan con un clic del
ratón la prevalidación (validación antes del ingreso de los datos) no es posible (después de un clic del ratón un
campo de entrada tiene el foco de entrada). Esta condición requiere algunas consideraciones por parte de los
programadores que han desarrollado previamente sólo bajo DOS sin un ratón. La validación de los datos en una
aplicación GUI puede producirse en el marco de la postvalidación (validación después del ingreso de datos). El
método :validate() en la clase DataRef sirve para este propósito. Si la postvalidación falla, debería llamarse al
método :undo() del campo de entrada (Componente de Xbase). En una aplicación dirigida por evento, esta es la
única forma de asegurar que ningún dato inválido se escribe en la base de datos.
En este código, se crea un campo de entrada para la edición de los datos en el campo APELLIDO de la base de
datos. Llamando al método :setdata() en conexión con :create() se copian los datos desde el campo de la base de
datos al buffer de edición de un objeto XbpSLE. Dentro de una ventana de diálogo pueden existir cualquier número
de campos de entrada para acceder los campos de la base de datos. El buffer de edición de todos los campos de
entrada en la ventana de diálogo pueden modificarse en cualquier momento (un clic del ratón en un campo de
entrada es suficiente para comenzar con la edición). Por esta razón debería determinarse cuando deberían co-
piarse los cambios de los datos en un campo de entrada nuevamente al archivo. Hay dos aproximaciones: Las
modificaciones a los campos de entrada de datos individuales se escriben en el archivo ni bien se produce el
cambio o todos los cambios de todos los campos de entrada de datos en una ventana se escriben en el archivo ni
bien se llama a una rutina "Guardar" explícitamente o el puntero de registros se reposiciona.
La segunda aproximación se prefiere en las aplicaciones GUI que se designan para el acceso simultáneo en un
entorno de red. Esta aproximación permite que diversos campos de entrada de datos sean modificados en una
ventana de diálogo sin la copia de cada cambio individual a la base de datos. En la operación concurrente o en
entorno de red guardar cada cambio a la base de datos requerirá un bloqueo más largo y la liberación del registro
actual. Una aplicación GUI de performance optimizada sólo bloquea un registro cuando puede escribir varios
campos en la base de datos o cuando cambia el puntero de registros.
Los problemas de validación de guardado de datos en la base de datos está presente en cada aplicación. El si-
guiente código muestra varios aspectos de este problema y se basa en la aplicación de ejemplo MDIDEMO. En
este ejemplo de aplicación la clase DataDialog se usa para proveer ventanas de diálogo para el acceso al Motor de
Base de Datos. Un objeto DataDialog coordina un Motor de Base de Datos con una ventana de diálogo. El código
fuente para esta clase está contenido en el archivo DATADLG.PRG. Un ejemplo de una pantalla de entrada ba-
sado en DataDialog, se muestra en la siguiente ilustración:
Pantalla de Ingreso de datos de cliente
La clase DataDialog se deriva a partir de XbpDialog. Añade siete nuevas variables de instancia y once métodos
adicionales para la transferencia de datos a partir de una base de datos al diálogo y viceversa. Tres de estas va-
riables de instancia son para uso interno solamente y se declaran como PROTECTED:. Los cuatro méto-
dos :init() , :create() , :configure() y :destroy() llevan a cabo pasos en el "ciclo de vida" de un objeto DataDialog:
#include "Gra.ch"
#include "Xbp.ch"
#include "Dmlb.ch"
#include "Common.ch"
#include "Appevent.ch"
********************************************************************
* Declaración de Clase
********************************************************************
CLASS DataDialog FROM XbpDialog
PROTECTED:
VAR appendMode // Es un nuevo registro?
VAR editControls // Lista de XBPs para edición de datos
VAR appendControls // Lista de XBPs sólo habilitados durante APPEND
EXPORTED:
VAR area READONLY // Area de trabajo actual
VAR newTitle // Bloque de código para cambiar título de ventana
VAR contextMenu // Menú de contexto para diálogo de datos
VAR windowMenu // Menú de ventanas dinámico en ventana de aplicación
La variable de instancia protegida :appendMode contiene el valor lógico .T. (verdadero) sólo cuando el registro de
datos fantasmas (número de registro LastRec()+1) es el actual. Las otras dos variables de instancia protegi-
das :editControls y :appendControls son arreglos que contienen listas de los Componentes de Xbase que pueden
modificar los datos. Con el fin de crear un diálogo de datos, los XBPs editables que se requieren así como los
Componentes de Xbase que no pueden editar los datos sino que despliegan texto estático o cajas (objetos
XbpStatic). La variable de instancia :editControls contiene una lista de referencia a aquellos XBPs en la lista de
hijos (todos los XBPs que se despliegan en la ventana de diálogo están contenidos en esta lista) que puede edi-
tarse.
La tarea de la variable de instancia :appendControls es similar y contiene una lista de los XBPs que se habilitan
cuando se añade un nuevo registro. En todos los demás casos, estos XBPs están deshabilitados. Ellos sólo
despliegan datos y no permiten que los datos en ellos sean editados. Esto es útil para editar los campos de las
bases de datos que están contenidos en la clave primaria de la base de datos la cual no debería modificarse una
vez que se ingresan en la base de datos. :editControls y :appendControls se inicializan ambos con arreglos vacíos.
Esto se lleva a cabo en el método :init() después de llamar al método :init() de la clase XbpDialog como se muestra
debajo:
******************************************* *************************
* Inicializar data dialog
********************************************************************
METHOD DataDialog:init( oParent, oOwner , ;
aPos , aSize , ;
aPParam, lVisible )
::area := 0
::border := XBPDLG_THINBORDER
::maxButton := .F.
::editControls := {}
::appendControls := {}
::appendMode := .F.
::newTitle := {|obj| obj:getTitle() }
RETURN self
Todas las variables de instancia se establecen en valores con los tipos de datos válidos en el método :init() . Sólo
las variables de instancia:border y :maxButton cambian los valores predeterminados asignados en la clase
XbpDialog. La ventana de un objeto DataDialog se fija en tamaño y no puede agrandarse. El método tiene la
misma lista de parámetros que los métodos :new() y :init() en la clase XbpDialog. Esto le permite recibir parámetros
y simplemente pasarlos a la superclase. El DataDialog es diferente en el sentido que se crea oculto por prede-
terminación. Esto se recomienda cuando se desplegarán muchos XBPs en la ventana después que la ventana
haya sido generada. La construcción de la pantalla con el método :show() es más rápida si se despliega todo de
una vez después que todos los XBPs han sido añadidos a la ventana de diálogo.
Las variables de instancia :newTitle deben contener un bloque de código que se pasa al objeto DataDialog. Por
esta razón un bloque de código se define en el método :init() , pero debe redefinirse más tarde. Este bloque de
código cambia el título de la ventana mientras la ventana de diálogo es visible. El bloque de código predeterminado
se asigna a las variables de instancia en el método :init() para asegurar que la variable de instancia tiene el tipo de
dato correcto.
El próximo método en el "ciclo de vida" de un objeto DataDialog es :create() . Una base de datos debe abrirse en el
área de trabajo actual previo a este método que se está llamando. Un objeto DataDialog continúa usando el área
de trabajo actual cuando se ejecuta el método :create():
********************************************************************
* Cargar recursos del sistema
* Registrar a DataDialog en el área de trabajo actual
********************************************************************
METHOD DataDialog:create( oParent, oOwner , ;
aPos , aSize , ;
aPParam, lVisible )
::appendMode := Eof()
::area := Select()
La tarea más importante de :create() es el requerimiento de recursos de sistema para la ventana de diálogo. Esto
ocurre cuando se llama al método del mismo nombre en la superclase y los parámetros se pasan a él simplemente.
El color de fondo para el área de dibujo (:drawingArea ) de la ventana de diálogo se establece luego. La llamada
a :setColorBG() define a su vez el color de fondo para todos los XBPs desplegados más tarde en la ventana de
diálogo. Esto afecta todos los XBPs que tienen texto para exhibir. Esto simplifica la programación porque el color
de fondo de los XBPs individuales con contenidos no tienen que establecerse separadamente. Generalmente
cuando han de emplearse los colores del sistema definidos en la configuración, no es posible llamar
a :setColorBG() .
Las siguientes líneas son importantes porque enlazan al objeto DataDialog y al área de trabajo. Primero, se de-
termina si el puntero está actualmente en Eof(). Luego Select() determina el número del área actual de trabajo. Dos
bloques de código se asignan a los conectores callback :close y :setDisplayFocus . El método :destroy() (descripto
debajo) se llama después del evento xbeP_Close. Ni bien el objeto DataDialog recibe foco, se ejecuta el bloque de
código en :setDisplayFocus . En este bloque de código, el área de trabajo manejada por el objeto DataDialog se
selecciona como el área de trabajo actual usando DbSelectArea(). Esto significa que si el ratón se clickea en una
ventana de DataDialog, el área de trabajo correcta se selecciona correctamente.
La llamada a DbRegisterClient() es crítica para la lógica del programa. Esto registra el objeto DataDialog en el área
de trabajo por lo que es automáticamente notificado cuando se modifica algo en la misma. Esto incluye notificación
de los cambios en la posición del puntero de registros. Cuando el puntero de registros se modifica, los nuevos
datos deben desplegarse mediante los XBPs que se listan en la variable de instancia :editControls . Esto se hace
usando el método :notify() que se describe después de los métodos restantes discutidos en el "ciclo de vida". El
método :configure() se provee para manipular cambios en el área de trabajo manejados por el objeto DataDialog.
A saber:
********************************************************************
* Cargar recursos del sistema
* Registrar a DataDialog en el área de trabajo actual si es preciso.
********************************************************************
METHOD DataDialog:configure( oParent, oOwner , ;
aPos , aSize , ;
aPParam, lVisible )
LOCAL lRegistrar := (::area <> Select())
::area := Select()
::appendMode := Eof()
IF lRegistrar
DbRegisterClient( self )
ENDIF
RETURN self
Un objeto DataDialog siempre manipula el área de trabajo actual. Debido a esto, el método :configure() compara
las variables de instancia :area para Select() para determinar si el área actual ha sido modificada. Si así fue, el
objeto de desregistra en el área de trabajo más antigua. Además, los recursos del sistema para la ventana de
diálogo se reconfiguran en la llamada al método :configure() de la superclase.
El método final del ciclo de vida de DataDialog es :destroy() . Este método cierra la base de datos que usa el objeto
DataDialog y libera los recursos del sistema. Las variables de instancia declaradas en la clase DataDialog se
restablecen en los valores asignados en el método :init() :
********************************************************************
* Liberar recursos de sistema y desregistrar datadialog del área de
* trabajo.
********************************************************************
METHOD DataDialog:destroy()
::writeData()
::hide()
(::área)->( DbCloseArea() )
IF ! Empty( ::windowMenu )
::windowMenu:delItem( self ) // borrar item de menú en menú
// de ventanas.
::windowMenu := NIL
ENDIF
IF ! Empty( ::contextMenu )
::contextMenu:cargo := NIL // Borrar referencia de DataDialog
::contextMenu := NIL // y menú de contexto
ENDIF
RETURN self
El método :writeData() se llama en :destroy() con el fin de escribir todas las modificaciones de los datos en la base
de datos antes que se cierre usando DbCloseArea(). Después que la base de datos se cierra, el objeto DataDialog
se desregistra de manera implícita del área de trabajo y no es necesaria una llamada al área de trabajo. Si en la
variable de instancia :windowMenu está contenido un objeto del menú, el objeto DataDialog se remueve de la lista
de los ítems en este menú (la clase WindowMenu se describe en la sección previa). La variable de instan-
cia :contextMenu puede contener un menú de contexto que se activa al hacer clic en el botón derecho del ratón.
Este mecanismo se describe en la sección posterior. Es esencial que la referencia al objeto DataDialog en la va-
riable de instancia :cargo del menú de contexto sea borrada porque se espera que el método :destroy() elimine
todas las referencias al objeto DataDialog. Y si un objeto de diálogo permanece referenciado en cualquier parte ya
sea en una variable, un arreglo, o una variable de instancia, no se removerá de la memoria por el colector de re-
siduos. Esto concluye la discusión de los métodos que llevan a cabo tareas en el "ciclo de vida" del objeto Data-
Dialog.
Uno de los métodos más importantes de la clase DataDialog es el método :notify() . Se lo llama cuando algo se
modifica en el área de trabajo asociado con el objeto. Una versión abreviada de este método que resalta sus
elementos esenciales se muestra debajo:
********************************************************************
* Método Notify:
* - Escribe datos a los campos previo a mover el puntero de registros.
* - Lee datos desde los campos luego de mover el puntero de registros.
********************************************************************
METHOD DataDialog:notify( nEvento, mp1, mp2 )
DO CASE
CASE mp1 == DBO_MOVE_PROLOG // el puntero de registros
::writeData() // está por ser movido
CASE mp1 == DBO_MOVE_DONE .OR. ; // el salto fue hecho
mp1 == DBO_GOBOTTOM .OR. ;
mp1 == DBO_GOTOP
::readData()
ENDCASE
RETURN self
Llamando a la función DbRegisterClient() en el método :create() del objeto DataDialog registra al objeto en el área
de trabajo que usa. Ni bien algo cambia en esta área de trabajo, se llama al método :notify() . Para el movimiento
del puntero de registros es método es siempre el doble. La primera vez que el objeto DataDialog recibe el valor
representado por la constante DBO_MOVE_PROLOG (definida en el archivo DMLB.CH) como el parámetro mp1 .
Esto es una señal que significa "Cuidado que la posición del puntero de registros está por cambiar". Cuando el
objeto DataDialog recibe este mensaje ejecuta el método :writeData() que escribe los datos del registro actual en la
base de datos. En la segunda llamada a :notify() , el objeto recibe el valor de la constante DBO_MOVE_DONE.
Este mensaje le dice al objeto "Ok, el puntero ha sido modificado". En respuesta a este mensaje, el objeto ejecuta
al método :readData() que copia los campos del nuevo registro en los buffer de edición de los XBPs que están en
la variable de instancia :editControls de los diálogos de datos. Esto permite que se editen los datos en el nuevo
registro.
El método :notify() provee la lógica importante del programa para el objeto DataDialog. En este método el objeto
DataDialog reacciona a los mensajes enviados por el área de trabajo que usa. Se llama a este método sólo
después que se registra al objeto en el área de trabajo usando DbRegisterClient(). O, más precisamente, se lo
llama sólo cuando el objeto se registra en el objeto de la base de datos (DBO) que maneja el área de trabajo (un
DBO se crea automáticamente cuando se abre una base de datos). Basado en el evento pasado, el evento :notify()
determina si un registro debería leerse en los XBPs o si los datos en el XBPs deberían escribirse en la base de
datos. El objeto DataDialog no manipula directamente a los datos pero si a los XBPs contenidos en el arre-
glo :editControls . El agregado de los XBPs a este arreglo se lleva a cabo usando el método :addEditControl() .
********************************************************************
* Agregar un control de edición a la lista interna
********************************************************************
METHOD DataDialog:addEditControl( oXbp )
IF AScan( ::editControls, oXbp ) == 0
AAdd( ::editControls, oXbp )
ENDIF
RETURN self
********************************************************************
* Agregar un control de añadido a la lista interna
********************************************************************
METHOD DataDialog:addAppendControl( oXbp )
IF AScan( ::appendControls, oXbp ) == 0
AAdd( ::appendControls, oXbp )
ENDIF
RETURN self
Los dos métodos :addEditControl() y :addAppendControl() son prácticamente iguales. Uno agrega un Compo-
nente de Xbase al arreglo almacenado en la variable de instancia :editControls y el otro agrega un Componente de
Xbase a :appendControls . Cuando un objeto DataDialog ejecuta el método :readData() o :writeData() , procesa
secuencialmente los elementos en el arreglo :editControls y envía a cada elemento (cada Componente de Xbase)
el mensaje para leer o escribir sus datos. Se incluye más abajo un fragmento de código para ilustrar cómo los
Componentes de Xbase pueden agregarse a la ventana de un objeto DataDialog y a la variable de instan-
cia :editControls si es apropiado. La variable oDlg referencia a un objeto DataDialog.
*********************************************************************
* Leer registro actual y transferir datos a los controles de edición
*********************************************************************
METHOD DataDialog:readData()
LOCAL i, imax := Len( ::editControls )
RETURN
El método :setData() en el primer ciclo FOR...NEXT causa que todos los XBPs referenciados en la variable de
instancia :editControls relean sus buffers de edición al copiar el valor de retorno del bloque de código de datos
contenido en :dataLink a su buffer de edición. El código restante habilita y Deshabilita a los XBPs en la lis-
ta :appendControls . Además de la lectura de los datos en los campos de datos de las bases de datos, este método
es el sitio apropiado para habilitar o deshabilitar aquellos Componentes de Xbase que deberían editarse cuando
está siendo añadido un nuevo registro.
La contraparte de :readData() es el método :writeData() . En este método los datos en el buffer de edición de cada
Componente de Xbase listada en :editControls se reescribe en la base de datos. Este método involucra código de
programa relativamente extenso, porque lleva a cabo el bloqueo de registros e identifica si un registro nuevo
debería agregarse.
********************************************************************
* Escribe datos de los controles de edición al archivo
********************************************************************
METHOD DataDialog:writeData()
LOCAL i, imax
LOCAL lBloqueado := .F. , ; // ¨Está bloqueado el registro?
lAgregar := .F. , ; // ¨Es un nuevo registro?
aCambiados := {} , ; // XBPs conteniendo datos cambiados
nViejaArea := Select() // Area de trabajo actual
dbSelectArea( ::área )
IF ! lBloqueado
MsgBox( "El registro está bloqueado" )
DbSelectArea( nViejaArea ) // falló el bloqueo de registro
RETURN .F. // *** VOLVER ***
ENDIF
IF ! Empty( ::contextMenu )
::contextMenu:disableBottom()
::contextMenu:enableEof()
ENDIF
ENDIF
DbSelectArea( nViejaArea )
RETURN .T.
El agregado de un nuevo registro requiere una lógica especial en el método :writeData() del objeto DataDialog. Un
registro especial vacío (el registro fantasma) está automáticamente disponible cuando se posiciona el puntero de
registros en Eof(). Si el puntero se posiciona en Eof(), el método :readData() ha copiado valores "vacíos" desde los
campos de la base de datos a los buffers de edición XBP listados en la variable de instancia :editControls . Debido
a esto, todos los XBPs contienen tipos de datos válidos. Pero no hay garantía que los datos válidos están conte-
nidos también en los buffers de edición de cada XBP. Esto significa que la validación de los datos debe llevarse a
cabo antes que el registro sea agregado. Puesto que no hay un registro aún, todos los datos que van a guardarse
se encuentran sólo en el buffer de edición de los Componentes de Xbase correspondientes. Se llama al méto-
do :validateAll() antes de la añadidura de un registro nuevo, el cual recibirá datos a partir de los buffers de edición
de los Componentes de Xbase.
La validación de los datos es especialmente importante cuando se añade un registro nuevo porque previamente
no existen datos válidos que permitan que los cambios a los buffers de edición sean vaciados. Para los registros
que están siendo editados, el método :undo() permite modificaciones a los valores en los buffers de edición a
vaciar. Pero esta aproximación asume que hay un valor de campo original que es válido. Esto es verdad sólo si el
registro que está siendo editado existió previo a la edición. Cuando se agrega un registro, los valores originales
son valores "vacíos" que probablemente no son válidos.
En :writeData() , esta situación se maneja llamando al método :validateAll() antes que un nuevo registro se
agregue al archivo usando APPEND BLANK. Si la validación de los datos falla en un sólo campo, se despliega un
mensaje que contiene el texto "Datos No Válidos" y no se agrega un nuevo registro. Los datos no válidos per-
manecen en los buffers de edición de los Componentes de Xbase correspondientes (:editControls ) y el usuario
puede corregir. Cuando se edita un registro existente, la validación de los datos se produce individualmente para
cada Componente de Xbase. Si el método :validate() de un XBP devuelve el valor .F. (falso) (indicando datos no
válidos), se ejecuta el método :undo() del XBP, que copia de los valores originales los datos válidos nuevamente al
buffer de edición.
Si se modificó cualquier componente de Xbase listado en :editControls , se bloquea el registro usando DbRLock().
Después de la validación, los datos de los buffers de edición se escriben en las bases de datos llamando al mé-
todo :getData() . La función DbCommit() asegura que los datos en los buffers de archivo se escriban en la base
datos. Finalmente se libera el bloqueo de registros.
El método :writeData() maneja los problemas de la validación de datos y de la añadidura de registros como ocurren
en un entorno dirigido por eventos. Este proceso es controlado por el ratón o por el usuario quien causa los clics
del mouse. También piense que si bien se invoca :writeData() desde solamente un lugar en el método :notify() , es
imposible visualizar cuándo éste método será ejecutado. Mientras que es claro que será llamado cuando el
puntero de registros se mueve, no es posible predecir cuál registro será el actual cuando el método :writeData()
sea llamado. El caso especial ocurre cuando el puntero es posicionado en el registro fantasma. En este caso la
validación de datos no puede ser revertida usando el método :undo() porque existen datos previamente no vali-
dados. Por esta razón, todos los datos deben ser validados antes que sea agregado un nuevo registro. El método
que verifica que todos los datos sean válidos se llama :validateAll() y se muestra más abajo:
********************************************************************
* Valida datos de todos los controles de edición
* Esto es necesario previo a agregar
* un nuevo registro a la Base de Datos
********************************************************************
METHOD DataDialog:validateAll()
LOCAL i := 0, imax := Len( ::editControls )
LOCAL lValido := .T.
RETURN lValido
El método consiste solamente en un ciclo DO WHILE que es terminado tan pronto como el método :validate() envíe
una señal de datos no válidos. El método :validate() se ejecuta para de todos los Componente de Xbase listados
en :editControls . Este método siempre retorna el valor .T. (verdadero) a menos que haya un bloque de código
contenido en la variable de instancia :validate . Si hay un bloque de código en esta variable de instancia, será
ejecutado y recibirá al Componente de Xbase como primer parámetro. Este bloque de código hace la validación de
datos y devuelve .T. (verdadero) si los datos son válidos.
Se necesita Una validación de datos especial para la clave primaria en una base de datos. La clave primaria es el
valor en la base de datos que unívocamente identifica cada registro. Hay siempre un índice para la clave primaria.
El método :isIndexUnique() (que se muestra debajo) prueba si un valor ya existe como clave primaria en un archivo
índice de la base de datos. Este método demuestra un aspecto extremadamente importante para el uso de los
objetos DataDialog (más precisamente: para el uso de la función DbRegisterClient()):
********************************************************************
* Verificar cuándo un valor de índice *no* existe en un índice.
********************************************************************
METHOD DataDialog:isIndexUnique( nOrden, xValorEnIndice )
LOCAL nIndiceAnterior := OrdNumber()
LOCAL nRegistro := Recno()
LOCAL lEsUnico := .F.
La funcionalidad del método :isIndexUnique() es muy limitada. Todo lo que hace es buscar un valor en el índice
especificado y retornar el valor .T. (verdadero) si el valor no se encontró. Un punto importante que se muestra aquí
es que el objeto DataDialog que ejecuta al método debe desregistrarse del área de trabajo. Este fue inicialmente
registrado en el área de trabajo por el método :create() , causando la llamada del método :notify() cada vez que el
puntero de registros cambia. En este caso, se trata de un método del objeto DataDialog cambiando el puntero
cuando se llama a DbSeek(). Si el objeto DataDialog no ha sido desregistrado, resultaría una recursión implícita ya
que cada cambio al puntero vía DbSeek() llama al método :notify() . Por esta razón, se usa DbDeRegisterClient()
para desregistrar al objeto DataDialog previo a la llamada a DbSeek(). Se lo registra otra vez en el área de trabajo
usando DbRegisterClient() después DBSeek().
En síntesis, la clase DataDialog resuelve muchos problemas que deben considerarse cuando se programan
aplicaciones GUI que trabajan con bases de datos. Los movimientos del puntero de registros se identifican con
facilidad en el método :notify() que se llama automáticamente cuando se registra al objeto DataDialog en el área de
trabajo actual usando la función DbRegisterClient(). Antes que se mueva el puntero de registros, un objeto Da-
taDialog copia los datos modificados en :editControls nuevamente a la base de datos. Una vez que se cambió el
puntero de registros, un objeto DataDialog despliega los datos actuales. La validación de los datos se produce
antes de la escritura de los datos en la base de datos ya sea mediante la añadidura de un nuevo registro o la
modificación de datos existentes que se sobreescriben. Si datos nuevos se están guardando o los datos existentes
se están modificando lo determina el objeto DataDialog.
********************************************************************
* Diálogo de Cliente
********************************************************************
PROCEDURE Customer( nRegistro )
LOCAL oXbp, oStatic, drawingArea, oDlg
FIELD NRCLIE, SR_SRA, APELLIDO, NOMBRE, CALLE, CODPOST
FIELD CIUDAD, ESTADO, TELEFONO, FAX, BLOQUEADO, TOTALVENTA
El procedimiento Customer() crea una ventana hija nueva en la aplicación MDI donde los datos del cliente pueden
editarse. Las variables LOCAL se declaran primero para referenciar los Componentes de Xbase creados y todos
los campos de las bases de datos se identifican para el compilador como variables de campo. Antes de la creación
de la ventana hija (DataDialog), las bases de datos requeridas deben estar abiertas. Esto ocurre en la función
OpenCustomer() que retorna el valor .F. (falso) sólo si la base de datos del cliente no puede abrirse. La apertura de
la base de datos puede fallar porque otra estación de trabajo tiene al archivo exclusivamente abierto o si el archivo
simplemente no se encuentra. (Nota: la evaluación de la existencia de archivos debería haberse concretado al
inicio en el Procedimiento Main).
Cuando lo(s) archivo(s) requerido(s) pueden abrirse, se crea la ventana de diálogo. Esto se lleva a cabo usando el
método :new() de la clase DataDialog que genera una nueva variable de instancia de la clase DataDialog. El padre
del nuevo objeto es el área de dibujo (:drawingArea ) de la ventana de aplicación creada en AppSys() y devuelta
por la función definida por el usuario RootWindow(). Ni bien se crea la ventana hija, el ID de Recurso para un ícono
debe ingresarse en la variable de instancia :icon . Este ícono es desplegado dentro de la ventana de aplicación
cuando la ventana hija se minimiza. En este ejemplo, se usa la constante #define ICON_CUSTOMER. Se declara
a un ícono en un archivo de recursos y se lo debe enlazar a un archivo ejecutable usando el compilador de re-
cursos. Si ningún ícono ID es especificado para una ventana hija, los contenidos de la ventana en el rango desde
el punto {0,0} hasta el punto {32,32} se usan como el ícono cuando la ventana es minimizada. Esto significa que
todo lo visible en la esquina inferior izquierda de la ventana hija hasta el punto {32,32} aparece como el símbolo
para la ventana hija minimizada.
En el programa de ejemplo MDICUST.PRG, sólo el archivo de la base de datos CLIENTE.DBF necesita abrirse. Es
importante para reabrir la base de datos del cliente cada vez que se llama al procedimiento Customer(). Esto se
muestra en el código del programa de la función OpenCustomer ():
********************************************************************
* Abrir base de datos de Clientes
********************************************************************
FUNCTION OpenCustomer( nRegistro )
LOCAL nViejaArea := Select(), lHecho := .F.
RETURN lHecho
Cada instancia de la clase DataDialog (cada pantalla de ingreso de datos) maneja su propia área de trabajo. Si se
ejecuta el procedimiento 10 veces, se crean 10 pantallas de ingreso de datos para los datos de los clientes y el
archivo CLIENTE.DBF se abre 10 veces. Esta regla es estándar para las aplicaciones dirigidas a eventos GUI:
Cada diálogo abre su propia base de datos . Esto requiere algunas consideraciones para los programadores
que provienen de la programación procedural de DOS, puesto que la misma aproximación no es apropiada bajo
DOS debido al límite máximo de 255 archivos abiertos. Este límite no existe bajo un sistema operativo de 32bit. El
acceso a un único archivo de base de datos a partir de varios diálogos no requiere la implementación de los
mecanismos de protección en la aplicación de Xbase++. Los mecanismos para el bloqueo de registros o archivos
es suficiente bajo Xbase++ así cuando el programa permite el acceso simultáneo en un entorno de red, también
manejará al archivo que está siendo abierto múltiples veces dentro de una única aplicación.
Cada llamada a OpenCustomer() abre la base de datos del cliente en un área de trabajo nueva y el método Da-
taDialog:create() registra la ventana de diálogo en esta área nueva de trabajo. Desde este punto en adelante, el
objeto DataDialog es notificado sobre cada cambio en el área de trabajo (vía el método :notify() ) y puede asig-
nársele los componentes de Xbase apropiados (vía :editControls ) para que puedan manipularse automáticamente
por los métodos DataDialog. (Nota para los programadores Clipper: Se permite la expresión USE Cliente NEW en
Xbase sin especificación de un nombre de alias. Si se abre una base de datos varias veces, Xbase++ provee un
nombre de alias único formado a partir del nombre del archivo y el número del área de trabajo actual).
Cuando la base de datos está abierta y se crea DataDialog (la ventana hija), los procesos más importantes para la
programación de una pantalla de ingreso de datos están casi completos. Los Componentes de Xbase deben
añadirse aún a la ventana de diálogo. Esta incluye ambos componentes de Xbase que contribuyen a la organi-
zación visual de la pantalla de ingreso datos (bordes y textos) y los componentes de Xbase que permiten el acceso
a los campos de las bases de datos vía :dataLink . Este segundo grupo se compone principalmente de objetos de
las clases XbpSLE, XbpCheckBox y XbpMLE. Los objetos XbpSLE proveen campos de ingreso de datos de línea
única. Los objetos XbpCheckBox manipulan los valores lógicos y los objetos XbpMLE proveen campos de ingreso
de datos de líneas múltiples que permiten la edición de campos de memo. Los objetos de las clases XbpSLE,
XbpCheckBox y XbpMLE son suficientes para programar las secciones de las pantallas de ingreso de datos en las
que se editan los campos de las bases de datos.
Las cajas que se despliegan a través de los objetos XbpStatic se usan para proveer una organización visual de las
pantallas de ingreso de datos. Los campos de entrada no están sólo separados visualmente cuando aparecen en
una caja, sino que los campos pueden programarse también en la lógica del programa. La siguiente sección de
programa muestra otro ejemplo de código definiendo una parte de una pantalla de ingreso de datos. Este ejemplo
es una continuación del procedimiento Customer():
En el ejemplo anterior, el área de dibujo de la ventana de diálogo se recupera y pasa como el padre para los
elementos de diálogo para desplegarse en la ventana. El primer elemento de diálogo es un objeto XbpStatic
responsable de desplegar un recuadro de grupo. Esta caja despliega texto (el contenido) en la esquina superior de
la izquierda. Se emplea un recuadro de grupo para agrupar los campos de ingreso de datos y actúa como el padre
para todos los Componentes de Xbase que se exhiben dentro del recuadro de grupo. En otras palabras: el padre
para un recuadro de grupo es el :drawingArea y el padre para los Componentes de Xbase desplegados dentro del
recuadro de grupo es el objeto XbpStatic que representa una caja. Por esta razón el objeto XbpStatic se referencia
en la variable oStatic y se usa como el padre en el ejemplo de la creación de los campos de ingreso de datos que
se muestra debajo:
En el ejemplo anterior, se crea un campo de ingreso de datos para la exhibición dentro del recuadro de grupo (el
padre del campo de ingreso de datos es oStatic). El objeto XbpSLE accede al campo NOMBRE de la base de
datos y la longitud del buffer de edición se limita a la longitud de un campo de base de datos. El campo APELLIDO
tienen 20 caracteres en el ejemplo. Se maneja una compatibilidad general entre los campos de las bases de datos
y los objetos XbpSLE en el bloque de código :dataLink . Cuando se leen los datos del campo de la base de datos,
los espacios en blanco están incluidos. Si se copian los datos directamente del campo APELLIDO al buffer de
edición del objeto XbpSLE, se incluyen siempre 20 caracteres en el buffer de edición inclusive para un apellido
como "Smith" que tiene sólo cinco caracteres de largo. Los espacios en blanco almacenados en el campo de la
base de datos se copian al buffer de edición del objeto XbpSLE. El resultado es que el buffer de edición del objeto
de los componentes de Xbase ya está lleno y los caracteres pueden agregarse sólo al buffer de edición en el modo
"Sobreescribir". Un objeto XbpSLE considera a los espacios en blanco como caracteres completamente válidos y
para prevenir estos problemas, los espacios en blanco al final del nombre (espacios precedentes) se remueven
explícitamente usando Trim() cuando los datos se leen desde el campo de la base de datos APELLIDO dentro
de :dataLink .
Un objeto XbpSLE puede editar valores en su buffer de edición que son de tipo "carácter". El número máximo de
caracteres es 32KB. Los valores de tipo numérico o fecha deben convertirse en una cadena de caracteres cuando
se copian en el buffer de edición de un objeto XbpSLE y se convierten de nuevo al tipo correcto antes de escribirse
en el campo de la base de datos. Esto debe llevarse a cabo en el bloque de código contenido en :dataLink . Abajo
se muestran ejemplos para los bloques de código que llevan a cabo las conversiones de tipo:
oXbp:dataLink := {|x| IIf(x==NIL, Transform( FIELD->NUMERICO, "@N"), ;
FIELD->NUMERICO := Val(x) ) }
oXbp:dataLink := {|x| IIf(x==NIL, DtoC( FIELD->FECHA ), ;
FIELD->FECHA := CtoD(x) ) }
Cuando los campos de las bases de datos se leen en el buffer de edición de un objeto XbpSLE se deben suprimir
los espacios en blanco y convertir los valores numéricos y de fecha a cadenas de caracteres. Cuando los datos
modificados se guardan en los campos de base de datos, los valores de los campos fecha y numérico deben
reconvertirse al tipo de dato correcto. Esta tarea se leva a cabo por el bloque de código asignado a la variable de
instancia :dataLink .
Otra tarea del bloque de código de datos contenidos en :dataLink se produce cuando se requiere más de un ar-
chivo para las pantallas de ingreso de datos (el DataDialog). En este caso, los campos de diversas bases de datos
se editan en una pantalla de ingreso de datos única y el bloque de códigos de datos ha de seleccionar el área de
trabajo correcta para la variable de campo.
Debe hacerse una distinción entre el control de una ventana y el control de una aplicación. Se controla la completa
ejecución de la aplicación mediante el menú de ejecución instalado en la ventana de aplicación. En una aplicación
SDI, el control de una aplicación es básicamente el mismo que el control de la ventana de diálogo, puesto que la
aplicación consiste de sólo una ventana de diálogo. En la aplicación de ejemplo SDIDEMO, el control de la apli-
cación a través del sistema de menú incluye la selección de pantallas de ingreso de datos para los datos del cliente
o para datos de partes. El control dentro de las ventanas se produce usando botones que permiten el movimiento
del puntero de registros dentro del archivo del cliente o el archivo de partes, causan que se guarden los datos
actuales o que termine el ingreso de datos.
En la aplicación de ejemplo MDIDEMO, el control de la aplicación se limita a la apertura de las pantallas de ingreso
de datos del cliente o de partes. Ni bien se abre una ventana hija, la aplicación y el menú de aplicación no tienen
más control sobre la ventana recientemente abierta. El control del programa dentro de la ventana hija se lleva a
cabo en una aplicación MDI por un menú de contexto que es un elemento de control del programa esencial. Se
activa un menú de contexto generalmente al hacer clic con el botón derecho del ratón. Se despliega en la pantalla
como un menú instantáneo (PopUp). Los ítems del menú proveen una selección de las acciones que son apro-
piadas para ejecutar dentro de la ventana o en relación al elemento de diálogo donde se produjo un clic en el botón
derecho del ratón.
El menú de contexto en la aplicación de ejemplo MDIDEMO incluye al control del programa de la navegación de
bases de datos (DbSkip(), DbGoBottom(), DbGoTop()) y operaciones de bases de datos elementales como
"Buscar", "Borrar" y "Nuevo registro". La programación de un menú de contexto requiere la definición de un objeto
XbpMenu y es de otra manera similar a la programación de los objetos del menú de aplicación. Como ejemplo, el
código del programa para crear el menú de contexto para la base de datos del cliente usada en MDIDEMO se
muestra debajo:
********************************************************************
* crear menú de contexto para el diálogo de Cliente
********************************************************************
STATIC FUNCTION ContextMenu()
STATIC soMenu
IF soMenu == NIL
soMenu := DataDialogMenu():new()
soMenu:title := "Menú de contexto Cliente"
soMenu:create()
soMenu:addItem( MENUITEM_SEPARATOR )
soMenu:addItem( MENUITEM_SEPARATOR )
ENDIF
RETURN soMenu
Se define un bloque de código para cada ítem del menú en el menú de contexto. Muchos de los bloques de código
se ejecutan cuando el usuario selecciona al ítem del menú. Muchos de los bloques de código controlan la nave-
gación de la base de datos usando funciones como DbSkip(), DbGoTop(), y DbGoBottom(). El objeto DataDialog
se notifica automáticamente acerca de estas operaciones (se llama a su método :notify() ) puesto que se registra
en el área de trabajo. El menú de contexto mismo puede activarse solamente en el objeto DataDialog (ventana hija)
en la que se ha focalizado. El menú se activa con un clic en el botón derecho del ratón que ha de producirse dentro
del ventana DataDialog. La ventana DataDialog activa su menú de contexto mediante el siguiente bloque de có-
digo callback (ver el archivo MDICUST.PRG, función Customer()):
drawingArea:RbDown := {|mp1,mp2,obj| ;
ContextMenu():cargo := obj:setParent(), ;
ContextMenu():popup( obj, mp1 ) }
El :drawingArea es el área de dibujo de la ventana DataDialog. La función ContextMenu() se muestra debajo. Esta
función devuelve los contenidos de la variable STATIC soMenu , que es el menú de contexto. El parámetro del
bloque de código obj contiene una referencia al Componente de Xbase que está procesando al evento
xbeM_RbDown (se presiona al botón derecho del ratón). En este caso, esta es el área de dibujo del DataDialog
(:drawingArea ) y la expresión obj:setParent() retorna al objeto DataDialog que se le asigna a la variable de ins-
tancia :cargo del menú de contexto. Esto también ocurre antes de que el menú de contexto se despliegue usando
al método :popUp() . Las coordenadas actuales del ratón (relativas a obj ) están contenidas en mp1 . Esto permite
la exhibición del valor de retorno de ContextMenu() (el menú de contexto) en la posición del puntero del ratón.
Cuando se selecciona un ítem del menú en el menú de contexto, el objeto DataDialog donde se activa al menú de
contexto está siempre contenido en la variable de instancia :cargo . Este objeto DataDialog tiene el foco (de lo
contrario no reaccionaría a un clic del botón derecho del ratón). El objeto DataDialog con el foco fue seleccionado
previamente vía el bloque de código callback :setDisplayFocus que establece el área de trabajo apropiada como el
área de trabajo actual. La navegación en la base de datos puede producirse en el menú de contexto al llamar
simplemente a DbSkip() o DbGobottom() sin la especificación del área de trabajo donde se producirá el movi-
miento. Se selecciona el área de trabajo mediante el objeto DataDialog cuando recibe el foco. El menú de contexto
puede activarse en un objeto DataDialog que tiene el foco porque sólo el objeto DataDialog con foco reacciona al
evento xbeM_RbDown y el menú de contexto se activa sólo en el bloque de código callback :RbDown .
Esta discusión resalta el control del programa vía un menú de contexto como se lo usa en la aplicación de ejemplo
MDIDEMO (puede ser más fácil seguir los pasos a través del código en el debugger). En conclusión, un menú de
contexto puede ser un elemento de control importante en una aplicación GUI. Generalmente, un menú de contexto
no es específico a un área de trabajo pero llama a la funcionalidad que ha de operar independientemente del área
de trabajo. En breve: un menú de contexto controla un Componente de Xbase.
El motor gráfico de Xbase++ - Xbase++ Graphics Engine (GRA)
Este capítulo describe el motor gráfico de Xbase++ - Xbase++ Graphics Engine (motor GRA) que provee al pro-
gramador un entorno conveniente para la salida gráfica. El motor GRA provee acceso funcional al sistema de sa-
lida gráfica. Incluye un total de 32 funciones que pueden usarse para crear de manera sencilla gráficos de negocio
actualizados, como gráficos de barras y tortas. El motor GRA puede desplegar bitmaps y metaarchivos y modificar
la apariencia de elementos de diálogo de los Componentes de Xbase.
En modo de texto (modo VIO), las coordenadas del cursor de la pantalla están determinadas usando las funciones
Row() y Col(). El origen del sistema de coordenadas en modo de texto se ubica en la esquina izquierda de arriba,
en la ventana y el tamaño predeterminado de la ventana es 25 filas y 80 columnas. Cuando se programa usando
funciones gráficas, debe usarse un sistema de coordenadas gráficas diferente. Tiene un origen diferente que el
sistema de coordenadas del modo de texto. No se usan las coordenadas de las filas y de las columnas, sino las
coordenadas gráficas de X e Y. Las coordenadas XY están en relación geométrica al eje X (horizontal) y al eje Y
(vertical).
La salida gráfica no involucra un cursor de pantalla como aquel empleado en modo de texto para identificar la
posición actual en pantalla. En cambio, el modo gráfico incorpora un "lápiz". La posición del lápiz en modo gráfico
es similar a la posición del cursor en modo de texto. La posición del lápiz se define como un punto en el sistema de
coordenadas y la posición del cursor se define en términos de la posición de su fila y columna. En la programación
gráfica un punto se representa siempre por un arreglo que contiene dos elementos que representan las coorde-
nadas X e Y para un punto. El siguiente código muestra una comparación de las posiciones del cursor y del lápiz:
Nota: El origen del sistema de coordenadas para la salida gráfica es izquierda debajo mientras que para el modo
de texto es izquierda arriba. La unidad para el sistema de coordenadas es "pixel". Un pixel (o punto) se especifica
usando un arreglo de dos elementos.
La salida gráfica se produce generalmente en la ventana actual. Cuando no puede visualizarse la salida gráfica en
la ventana (por ejemplo, puede haber sido enviada a la impresora), debe tenerse en cuenta el mecanismo de sa-
lida para gráficos. La salida gráfica ocurre sólo en lo que se llama un "Espacio de Presentación" (Nota: en Xbase++,
se proveen los espacios de presentación por la clase XbpPresSpace()). Un Espacio de Presentación contiene la
parte de la definición para la salida gráfica que es independiente del dispositivo de salida. Un gráfico puede lucir
diferente dependiendo del dispositivo de salida usado para el despliegue. Por ejemplo, el despliegue de un gráfico
en la pantalla ocurre en una ventana. La unidad para el sistema de coordenadas es el "pixel". Cuando el gráfico se
envía para salir por la impresora puede seleccionarse la unidad "0.1 cm" en vez del pixel. La salida del sistema
gráfico en la impresora se diferenciara luego considerablemente de lo que sale en pantalla.
En la discusión acerca de salidas gráficas, debe distinguirse entre la salida independiente del dispositivo y la salida
del dependiente de dispositivo. Un Espacio de Presentación define cualquier cosa requerida para el dispositivo
independiente de despliegue de gráfico. Esto incluye, por ejemplo:
- Origen y unidad del sistema de coordenadas
- Colores para el despliegue
- Tipo de línea de las líneas
- Rellenar áreas con patrones
- Tipo de Font y tamaño de los caracteres
- Tamaño del área de despliegue
El Espacio de Presentación puede pensarse como un área de dibujo abstracto para salidas gráficas (puede
pensarse también como similar a un pedazo de papel en blanco sobre el que puede dibujarse). Define todos los
atributos para la salida gráfica que son independientes del dispositivo de salida. Un dispositivo de salida es, por
ejemplo, una ventana (la pantalla) o la impresora. Un dispositivo de salida físico se conoce como "contexto del
dispositivo". El contexto del dispositivo contiene todos los dispositivos dependientes de atributos. Para desplegar
salidas gráficas, deben combinarse un Espacio de Presentación (independiente del dispositivo) con un dispositivo
de contexto (dispositivo dependiente). Ningún dibujo en el Espacio de Presentación es visible. Sólo enlazando el
Espacio de Presentación con el dispositivo de contexto puede hacerse visible la salida gráfica del Espacio de
Presentación en el dispositivo físico de salida.
Mientras se produzca una salida gráfica en una ventana XbpCrt solamente, no es necesario lidiar con el Espacio
de Presentación porque esa ventana tiene un Espacio de Presentación asociado con él y permite que este dis-
ponible el dispositivo de contexto. En la descripción del motor GRA y de sus funciones no se consideran otras
ventanas o dispositivos de salida. Estos son descriptos en los capítulos Espacios de Presentación para Salidas
gráficas y Dispositivos de Salida Gráfica.
En todas las funciones gráficas el primer parámetro específica el espacio de presentación para dibujo. La ventana
XbpCrt actual es el espacio de presentación predeterminado por lo que no es necesario especificar un dispositivo
de contexto o un Espacio de Presentación cuando se dibuja en la ventana actual. Si se crea una ventana en el
tamaño predeterminado queda disponible un área de dibujo de 640x350 pixeles. La posición del lápiz está en el
punto {0,0} una vez que la ventana fue creada. La salida del dibujo siempre comienza en la posición actual del lápiz.
Puesto que establece la posición del lápiz la función GraPos() es una función importante, fundamental del motor
GRA. Otra función básica es GraError() que determina si la salida gráfica fue exitosa o ha ocurrido un error.
Función Descripción
GraPos() Devuelve o establece la posición del lápiz
GraError() Devuelve un código numérico de error para la última Salida Gráfica
El motor GRA depende del hardware y de los manejadores de dispositivo instalados de la misma forma que de-
pende el sistema operativo. Si la salida gráfica no se produce como lo esperado, GraError() chequea si la opera-
ción gráfica está respaldada o no por el hardware o por el manejador de dispositivos.
Primitivas gráficas
Función Descripción
GraMarker() Dibuja el marcador
GraLine() Dibuja una línea
GraBox() Dibuja un cuadrado o rectángulo
GraStringAt() Dibuja una cadena de caracteres
GraSpline() Dibuja una curva
GraArc() Dibuja un círculo, arco o elipse
Las funciones en esta tabla dibujan seis figuras gráficas elementales: marcadores, líneas, rectángulos, círculos,
curvas y cadenas de caracteres. La función GraQueryTextBox() no es una primitiva gráfica y no despliega nada.
Esta función determina la coordenadas del área ocupada por una cadena de caracteres cuando ésta última es
desplegada. Esto es significativo para el posicionamiento correcto de las cadenas de caracteres en el despliegue
especialmente con fonts proporcionales. La siguiente ilustración muestra el resultado de varias primitivas gráficas.
Esta ilustración muestra una pocas posibilidades provistas por el motor GRA a través de sus primitivas gráficas. La
ilustración se crea usando Xbase++ y contiene cuatro ejemplos. En el cuadrante superior de la izquierda se
muestran las líneas, curvas y marcadores (las funciones GraMarker(), GraLine() y GraSpline()). El cuadrante
superior de la derecha se dibuja usando GraStringAt(), GraQueryTextBox() y GraBox(). En la mitad de abajo de la
ilustración se muestran dos gráficos de negocios. El gráfico de barras se dibuja fundamentalmente utilizando
GraBox() y GraLine() mientras que el gráfico de torta emplea la función GraArc().
Este código ilustra la convención de nombres utilizada por el motor GRA para las constantes #define. Todas las
constantes comienzan con el prefijo GRA_. Generalmente se incluye otro prefijo para identificar el grupo de
constantes GRA_ y se incluye un sufijo al final que define únicamente a la constante. Los atributos pueden esta-
blecerse para marcadores, líneas, áreas y cadenas de caracteres. Por tanto, se crean cuatro posibles arreglos de
atributos cuando se emplean las cuatro constantes:
Las letras AM, AL, AA y AS son abreviaturas de "atributos de marcador", "atributos de línea", "atributos de área" y
"atributos de cadena". Cada elemento se identifica por una constante #define que tiene como prefijo un arreglo de
atributos como GRA_AM_, GRA_AL_, GRA_AA_ o GRA_AS_. Se establece un atributo asignando un valor al
elemento apropiado en el arreglo de atributos. Se establece un atributo al asignar un valor al elemento apropiado
en el arreglo de atributos. Para cada atributo hay sólo un numero limitado de valores válidos que se usan nue-
vamente en la forma de constantes #define.
En esta línea de códigos el atributo "tipo de línea" recibe un valor para un guión corto. Los valores válidos para
tipos de líneas (mas precisamente para el atributo GRA_AL_TYPE) son constantes que tienen como prefijo a
GRA_LINETYPE_. Para cada atributo hay un grupo de constantes #define como estas que pueden asignarse
como el valor para el atributo.
Se definen los atributos para un Espacio de Presentación y por tanto el arreglo de atributos deben pasarse al
Espacio de Presentación luego que el atributo se inserta en el arreglo. Sólo después se establecen realmente los
atributos y se los utiliza para primitivas gráfica mientras se dibuja. Las funciones usadas para asignar los arreglos
de atributos al espacio de presentación se listan en la siguiente tabla.
Las funciones GraSetAttr..() pasan un arreglo de atributos a un espacio de presentación (Nota: estas funciones se
codifican en el archivo GRASYS.PRG. Llaman a los métodos correspondientes de un objeto de XbpPresSpace).
Los atributos establecidos para los marcadores se utilizan por las primitivas gráficas GraMarker(). Los atributos
para las líneas se utilizan por GraLine() y GraSpline(). También se usan por GraArc() y GraBox() cuando se dibujan
un círculo o un rectángulo con un borde (el borde es una línea). Si se rellenan un círculo o un rectángulo con un
patrón, se utilizan los atributos para áreas. Estos últimos se definen por GraSetAttrArea(). Las primitivas gráficas
GraArc() y GraBox() usan atributos de líneas así como atributos de áreas. Los atributos para cadenas de carac-
teres los usan exclusivamente GraStringAt(). Las siguientes tablas dan un vistazo de los atributos que pueden
establecerse para marcadores, líneas, áreas y cadenas de caracteres.
En ésta ilustración los círculos se dibujan primero. El atributo mezcla para el color de fondo cambia desde
GRA_FGMIX_OVERPAINT (el valor predeterminado que causa que la nueva primitiva cubra la antigua) a
GRA_FGMIX_OR previo al despliegue del cuadrado más inferior. Cuando se establece este atributo de mezcla, el
color del frente del cuadrado se mezcla con el color de frente del dibujo existente. Cuando se establece
GRA_FGMIX_OR la mezcla se produce usando OR binario entre los valores de color de los colores existentes y
los colores del nuevo gráfico que se está dibujando. El área donde se combinan los círculos y el cuadrado es vi-
sible en el color de mezcla y ésta sección aparece de alguna manera más clara que el resto del círculo. El color del
resto del cuadrado en la ilustración es el resultado de la mezcla de colores entre gris oscuro (cuadrado) y un gris
pálido (color de ventana).
En la porción derecha de la ilustración las cadenas de caracteres se dibujan sobre un círculo. Dentro del círculo, el
color de fondo para los caracteres es visible. Causa que el patrón de llenado del círculo que aparezca más claro.
En este caso, el color de mezcla es el resultado de los valores GRA_BGMIX_OR para el atributo de mezcla del
color de fondo de GraStringAt(). Por predeterminación los atributos de mezcla se establecen para que el color de
fondo pinte sobre todo lo que ya ha sido desplegado (GRA_FGMIX_OVERPAINT) quedando desafectado el color
de fondo (GRA_BGMIX_LEAVEALONE). El atributo de mezcla para el color del frente es siempre significativo
cuando las primitivas gráficas se superponen entre ellas y la sección compartida debe permanecer visible. Cuando
las cadenas de caracteres se despliegan debe tenerse en cuenta al atributo de mezcla para el color de fondo,
puesto que se exhiben ambos el color del frente (letra) y el color de fondo (el rectángulo que rodea una letra).
Un valor especial para el atributo del color mixto para los colores del frente es GRA_FGMIX_XOR. El resultado del
despliegue de las primitivas gráficas cuando se establece este valor es que las primitivas gráficas se borran de la
pantalla si se dibujan en la misma localización una segunda vez. Además, el color de mezcla resultante de una
primitiva gráfica que pinta sobre un dibujo existente depende del color del dibujo existente y del color de la primitiva
gráfica. Las siguientes dos tablas listan las constantes que pueden usarse para los atributos mixtos del color del
frente y del fondo:
Se recomienda que los fonts sean cargados usando XbpFont():new() y luego modificados usando GraSetAttrS-
tring(). Los atributos para las cadenas de caracteres permiten la creación de diversas exhibiciones de fuentes.
Esto comienza con el tamaño de caracteres individuales e incluye el ángulo en que se despliega un caracter re-
lativo al eje horizontal.
Debido a diferencias internas entre XbpFont() y GraSetAttrString(), XbpFont() debería usarse sólo para cargar un
font y GraSetAttrString() debería usarse para definir los atributos para desplegar los caracteres. Debería notarse
que muchos atributos para desplegar los caracteres son válidos sólo para los fonts vectoriales. Un Font vectorial
se dibuja como la línea externa de las letras utilizando las primitivas gráficas y luego se llena la línea externa. Una
letra en un font de bitmap consiste de una imagen reticulada. Luego de esto, los caracteres de un font de bitmap no
puede convertirse automáticamente a itálicas. Con el fin de desplegar los caracteres de un font bitmap en itálicas,
se requiere por separado un font bitmap en itálicas.
Luego de la definición del segmento gráfico, todas las primitivas gráficas contenidas en el segmento son
re-dibujadas cuando se llama a la función GraSegDraw(). Las funciones usadas para programar los segmentos de
gráfico se listan en la siguiente tabla:
Un segmento de gráfico puede visualizarse como una primitiva compleja consistente de muchas primitivas gráficas
que se ejecutan entre GraSegOpen() y GraSegClose(). Dibujar, o hacer visible un segmento de gráfico, depende
del modo de dibujo definido en el espacio de presentación cuando se abre el segmento con GraSegOpen(). Hay
tres modos diferentes que se establecen usando la función GraSegDrawMode():
El modo GRA_DM_DRAW dibuja pero no almacena las primitivas definidas en el segmento. El segmento no puede
re-dibujarse usando GraSegDraw(). Este modo es útil solamente cuando debe almacenarse un dibujo en un
Metaarchivo (ver la subsección El Metaarchivo - XbpMetaFile() en la sección "Los dispositivos de salida gráfica").
El modo de dibujo GRA_DM_RETAIN almacena todas las primitivas gráficas en un segmento, pero no las des-
pliega. Esto permite que dibujos complejos se definan de manera invisible por el usuario, paso por paso, y luego se
hagan visibles todas al mismo tiempo usando GraSegDraw(). En el tercer modo de dibujo, la salida se produce
cuando el segmento está siendo definido y todas las primitivas gráficas son visibles cuando se cierra el segmento
usando GraSegClose(). En este modo, la función GraSegDraw() solo tiene que llamarse si el segmento necesita
ser re-dibujado.
Si se definen segmentos gráficos para una ventana, sólo las primitivas gráficas pueden almacenarse en el seg-
mento y no los atributos para las primitivas gráficas. Cuando se dibuja un segmento y no los atributos para puntos,
líneas, áreas y cadenas de caracteres que se establecen cuando se llama a GraSegDraw() este se uti liza en la
exhibición (Nota: cuando se almacenan los segmentos gráficos en un Metaarchivo, también se almacenan los
atributos).
Los segmentos de gráfico definidos se arreglan de acuerdo a la prioridad que aumenta en el orden en que fueron
definidos los segmentos. El primer segmento definido tiene la más baja prioridad y el ultimo segmento definido
tiene la prioridad más alta. La prioridad determina el orden en que se dibujaron los segmentos de gráfico. Si se
llama a la función GraSegDraw() sin los parámetros, todos los segmentos de gráfico se re- dibujan. El segmento
con la prioridad más baja se dibuja primero y el segmento con la prioridad más alta se dibuja ultimo. Esto es sig-
nificativo cuando los segmentos de gráfico se superponen durante el dibujo. Los segmentos con la prioridad más
alta pintan sobre los segmentos con la prioridad más alta. El resultado consiste en que los segmentos con la
prioridad más alta aparecen "al frente" y que los segmentos con la prioridad más baja aparecen "detrás" de los
segmentos con alta prioridad. La prioridad puede pensarse como el eje z en un sistema tridimensional de coor-
denadas. Mientras más alta es la prioridad, aparecen más hacia el frente los segmentos de gráfico si se super-
ponen con otros segmentos. La función GraSegPriority() sube o disminuye la prioridad de un segmento individual
en relación con un segundo segmento.
Una característica única de los segmentos de gráfico es el hecho que pueden "encontrarse" basándose en su
posición. Esto se lleva a cabo usando la función GraSegFind() a la que se le pasan las coordenadas para un punto
como ser un parámetro y que devuelve un arreglo conteniendo los IDs numéricos de todos los segmentos de
gráfico que abarcan a este punto. Los segmentos de gráfico pueden usarse entonces para interacción con el
usuario. Después de un clic con el mouse, por ejemplo, las coordenadas del puntero del mouse pueden pasarse a
la función GraSegFind() con el fin de identificar todos los segmentos de gráfico que incluyen el punto donde se hizo
un clic con el mouse. La función GraSegFind() no considera las coordenadas exactas del puntero del mouse (el
punto en la punta del puntero del mouse), pero trabaja con un rectángulo virtual que se mueve junto con el puntero
del mouse. Es posible encontrar un segmento de gráfico donde el puntero del mouse esta cerca del segmento pero
permanece apenas afuera. En este caso el rectángulo virtual alrededor del puntero del mouse se superpone con el
borde del segmento. El tamaño del rectángulo virtual puede establecerse luego usando la función GraSegPic-
kResolution() y determina la precisión o resolución usada por la resolución de la función GraSegFind() cuando se
buscan segmentos de gráfico.
Las primitivas gráficas que se llaman entre GraPathBegin() y GraPathEnd() definen el borde externo de un área
que forma el camino gráfico. Las primitivas no se dibujan y permanencen escondidas. El área definida puede
llenarse luego con un color o patrón usando GraPathFill(). Se utilizan los atributos para áreas establecidas con
GraSetAttrArea(). El borde del área definida puede dibujarse usando GraPathOutline() que hace visible a las
primitivas. En este caso se emplea el atributo de líneas establecido con GraSetAttrLine().
Después la ejecución de una de las dos operaciones (GraPathFill() o GraPathOutline()), el camino gráfico se
descarta y no queda disponible nuevamente. Cuando debe bordearse externamente un área así como rellenada,
un camino gráfico puede definirse dentro de un segmento de gráfico. El camino puede luego redefinirse usando
GraSegDraw(). A saber:
Al contrario de GraPathOutline(), la operación del camino GraPathFill() requiere que el área descripta por el c a-
mino se cierre. Si este no es el caso, la ventana entera es rellenada por GraPathFill().
Se lleva a cabo una operación especial usando GraPathClip(). Cualquier cosa que se dibuje en la ventana esta
limitado al área definida por el camino gráfico. La salida fuera de esta ventana no es posible mientras se establece
el camino de recorte. Esto incluye todas las funciones gráficas así como las funciones disponibles en modo híbrido,
como QOut() y Alert(). El camino de recorte se activa usando GraPathClip(, .T.) y debe desactivarse usando
GraPathClip(, .F.).
Esta ilustración muestra algunas maneras en las que pueden usarse los caminos gráficos. Sobre la izquierda se
define un camino de recorte por dos círculos concéntricos (usando la función GraArc()). Dentro del camino de
recorte, se dibuja varias veces una cadena de caracteres usando GraStringAt(). El "aeroplano de papel" consiste
de sólo siete puntos. Los puntos se conectan entre si usando GraLine() y los caminos gráficos se definen y bor-
dean externamente y rellenan con varios patrones. Tres patrones de llenado se utilizan para ilustrar los caminos
gráficos separados, (las dos "alas" son un camino gráfico). El texto "Alaska" se dibuja entre GraPathBegin() y
GraPathEnd() y se rellena después con un patrón usando GraPathFill(). Esto demuestra que un camino gráfico por
si sólo puede consistir de varias áreas incluidas individualmente. Cada letra forma un área cerrada separada.
Hay tres funciones en el motor GRA para transformar imágenes vectoriales y una única función para llevar a cabo
operaciones reticuladas. Estas cuatro funciones se listan en la siguiente tabla:
Las tres posibles transformaciones gráficas de una imagen vectorial involucran las operaciones de "rotación" (girar
una imagen", "escala" (agrandar o reducir una imagen) y "translación" (mover, copiar o invertir una imagen). Cada
una de estas operaciones requiere el cálculo de una matriz de transformación. Los cálculos para las tres posibles
transformaciones se hace usando las funciones GraRotate(), GraScale() y GraTranslate(). Las tres funciones sólo
calculan una matriz de transformación para una de estas tres posibles transformaciones gráficas, no despliegan
nada. La matriz de transformación gráfica misma es un arreglo bidimensional con tres filas y tres columnas. El
arreglo debe crearse antes de la transformación usando GraInitMatrix(). GraInitMatrix() es una pseudo función
definida en GRA.CH. Se traduce a un arreglo bidimensional que debe usarse para calcular una matriz de trans-
formación.
Para hacer visible la transformación, el segmento de gráfico debe re- dibujarse. Esto ocurre usando la función
GraSegDraw() y pasando a la matriz de transformación conteniendo el resultado de los cálculos para la trans-
formación deseada a ésta función. Aquí hay un ejemplo:
#include "Gra.ch"
PROCEDURE Main
LOCAL nSegmento, i, aMatriz := GraInitMatrix()
En este ejemplo, se crea un segmento de gráfico que contiene sólo una primitiva de gráfica (GraBox()). Dentro del
ciclo FOR..NEXT, el segmento se dibuja 12 veces, y cada vez que se calcula una rotación de -30 grados basadas
en un punto de giro en la esquina inferior izquierda (el punto {200,150}). El ángulo negativo de -30 grados causa
una rotación hacia la izquierda (un ángulo positivo causaría una rotación hacia la derecha). El ejemplo muestra
que es esencial calcular primero la matriz de transformación. Para calcular la matriz de transformación es nece-
saria una matriz creada por GraInitMatrix(). El resultado del cálculo de GraRotate() es visible sólo luego de llamar
a GraSegDraw(). La matriz de transformación debe pasarse a la función GraSegDraw().
Una transformación gráfica requiere un total de cuatro pasos. Primero, la matriz de transformación debe crearse
usando GraInitMatrix(). El segundo paso consiste en definir el segmento de gráfico que contiene el dibujo a
transformar. Tercero, la transformación se calcula usando GraRotate(), GraScale() o GraTranslate() para crear la
matriz de transformación. Finalmente, la transformación gráfica se despliega usando GraSegDraw(). La matriz de
transformación que se cálculo previamente usando GraRotate(), GraScale() o GraTranslate() debe pasarse a la
función GraSegDraw().
Nota
Las dos constantes #define GRA_TRANSFORM_ADD y GRA_TRANSFORM_REPLACE afectan el cálculo de
transformaciones repetidas. Si se especifica GRA_TRANSFORM_ADD, la matriz de transformación queda inal-
terada luego de emplearla para exhibir el segmento. GRA_TRANSFORM_REPLACE (predeterminada) produce
que se restablezca la matriz de transformación al valor inicial de GraInitMatrix() cuando está completo el des-
pliegue usando la matriz de cálculo. Usando GRA_TRANSFORM_ADD, diversas transformaciones pueden s u-
marse efectivamente. GRA_TRANSFORM_REPLACE comienza cada transformación gráfica con la matriz inicial
predeterminada por GraInitMatrix().
Importante: Estas transformaciones son válidas sólo para imágenes vectoriales. No pueden aplicarse a imágenes
reticuladas (bitmaps).
Para crear esta ilustración, se despliega un bitmap inicialmente en el lado superior izquierdo de la imagen y luego
se llama a la función GraBitBlt() cuatro veces usando diferentes coordenadas fuente y blanco. La imagen reticu-
lada se copia, agranda, y reduce entera y por secciones.
Una característica especial de la función GraBitBlt() es que las áreas fuente y blanco de la operación reticulada
pueden ocurrir en dos espacios de presentación diferentes. La salida de la imagen reticulada depende del dispo-
sitivo de contexto enlazado al Espacio de Presentación cuando ocurre el despliegue. Por ejemplo, es posible co-
piar imágenes reticuladas desde un Espacio de Presentación asociado con una ventana a un espacio de pre-
sentación asociado con un dispositivo de contexto como la impresora. De esta manera, las imágenes reticuladas
visibles en una ventana pueden visualizarse cuando se imprimen.
El Espacio de Presentación determina el origen del sistema de coordenadas (el punto {0,0}) junto con la unidad de
medida para la salida. Toda la salida gráfica esta en relación con el sistema de coordenadas del dispositivo de
salida. Cuando se visualizan los elementos gráficos dos sistemas de coordenadas son considerados: el sistema
de coordenadas del Espacio de Presentación y el del dispositivo de contexto.
El sistema de coordenadas del Espacio de Presentación esta predeterminado por el tamaño de la página. La
página determina el espacio disponible donde se puede producir la salida gráfica. Ya que toda la salida gráfica
ocurre en el Espacio de Presentación lo que sale usa las coordenadas de página del Espacio de Presentación.
El Espacio de Presentación muestra la pagina en el dispositivo de contexto. Usa lo que se conoce como "viewport".
Toda la pagina de un Espacio de Presentación se despliega completamente en el Area de Visualización. El Area
de Visualización está referido a las coordenadas del dispositivo de salida, significando que el tamaño del Area de
Visualización determina que es visible en el dispositivo de salida. El tamaño de la pagina determina que puede
exhibirse en el Espacio de Presentación.
El tamaño de la página de un Espacio de Presentación así como el tamaño del Area de Visualización pueden
establecerse para un Espacio de Presentación. El tamaño de la página se determina usando el méto-
do :setPageSize() y el tamaño del Area de Visualización se establece usando el método :setViewPort() . La página
determina el despliegue en el Espacio de Presentación y el Area de Visualización determina el despliegue en el
dispositivo de salida (en el dispositivo de contexto). La siguiente ilustración muestra el efecto de Areas de Visua-
lización de diferente tamaño con dos páginas de igual tamaño y dispositivos de salida de igual tamaño en dos
ventanas:
La ilustración muestra dos ventanas de igual tamaño con un área de dibujo de aproximadamente 260 * 200 pixeles
(excluyendo la barra del título) que presentan la misma imagen. La única diferencia entre las ventanas es el ta-
maño del Area de Visualización de sus respectivos espacios de presentación. El área gris en la ilustración re-
presenta al Area de Visualización. En la ventana izquierda el Area de Visualización es más pequeño que la ven-
tana. En la ventana derecha, la imagen se sale del borde de la ventana porque el Area de Visualización es más
grande que la ventana. Cualquier cosa que esté afuera del borde de la ventana de la ventana derecha no será
visible en la pantalla. El siguiente código del programa muestra como puede ser influenciado el Area de Visuali-
zación:
oPS1 := oWindowA:presSpace()
oPS1:setViewPort( {20, 20, 240, 180} ) // Area de Visualiz. < ventana
oPS2 := oWindowB:presSpace()
oPS2:setViewPort( {-20, -20, 280, 240} ) // Area de Visualiz. > ventana
DisplayGraphic( oPS2 )
En ambas ventanas, la imagen se dibuja con las mismas coordenadas. Sin embargo, en la ventana izquierda el
Area de Visualización del Espacio de Presentación es más pequeño que el dispositivo de salida (la ventana). De tal
manera, la imagen se reduce. En la ventana derecha, el Area de Visualización es más grande que el dispositivo de
salida. Lo que se despliega en la ventana se agranda y partes de la imagen se cortan. El área gris corresponde al
Area de Visualización. Este código del programa denota que las coordenadas del Area de Visualización están
especificadas como coordenadas de la ventana. Por tanto, el Area de Visualización en la ventana izquierda c o-
mienza con el punto {20,20}, que está comprendido dentro de la ventana porque empieza en el punto {-20,-20}.
Un Espacio de Presentación transfiere la salida gráfica a través del Area de Visualización en el dispositivo de
contexto. El Area de Visualización de un espacio de presentación está siempre relacionado al sistema de coor-
denadas del dispositivo de salida. En el ejemplo, el dispositivo de contexto es 260*200 pixeles de grande y se
definen dos Areas de Visualización de diferente tamaño. De esta manera, las imágenes pueden escalarse de
cualquier forma en el despliegue porque el Area de Visualización determina las coordenadas en el dispositivo de
salida donde se produce la salida gráfica. Si el Area de Visualización no tiene el mismo tamaño que el dispositivo
de salida, se lleva a cabo una transformación gráfica automáticamente durante la salida de una imagen al dispo-
sitivo de contexto.
Junto con el Area de Visualización, el tamaño del área de dibujo abstracta que representa al Espacio de Pre-
sentación puede establecerse para un espacio de presentación. Esto se produce usando el método :setPageSize()
para definir el tamaño de la "página" del Espacio de Presentación. El tamaño de la página afecta las dimensiones
del sistema de coordenadas en un Espacio de Presentación.
La ilustración nuevamente muestra dos ventanas del mismo tamaño con un área de dibujo de 260*200 pixeles, y
en ambas ventanas se despliega la misma imagen. Aquí el tamaño de las páginas es diferente:
oPS1 := oWindowA:presSpace()
oPS1:setViewPort( {0, 0, 260, 200} ) // Area de Visualiz. = ventana
oPS1:setPageSize( {260, 200} ) // tamaño de página = ventana
oPS2 := oWindowB:presSpace()
oPS2:setViewPort( {0, 0, 260, 200} ) // Area de Visualiz. = ventana
oPS2:setPageSize( {520, 400} ) // tamaño de página > ventana
DisplayGraphic( oPS2 )
En ambos casos, el Area de Visualización tiene el mismo tamaño que la ventana. Esto significa que todo lo que se
encuentra dibujado en el espacio de presentación también aparece en la ventana. Pero el tamaño de la página
para el Espacio de Presentación en la ventana derecha es cuatro veces más grande que en la ventana izquierda.
En la ventana izquierda el sistema de coordenadas del Espacio de Presentación extiende desde 0 a 200 en di-
rección del eje y (vertical), significando que la página es 260*200 pixeles de grande. En la ventana derecha el eje
x del espacio de presentación se extiende desde 0 a 520 y el eje y desde 0 a 400. Ya que la imagen en ambos
casos se despliega en las mismas coordenadas en el Espacio de Presentación es cuatro veces más pequeña en el
espacio de presentación de la ventana derecha que en la ventana izquierda.
El método :setPageSize() define el tamaño de la página o las dimensiones del sistema de coordenadas en el
Espacio de Presentación. El método :setViewPort() , sin embargo, especifica las coordenadas de dispositivo del
dispositivo de contexto donde se produce la salida. Ambos métodos causan una transformación gráfica cuando el
tamaño de la página del Espacio de Presentación no coincide con el tamaño del dispositivo de salida o cuando el
Area de Visualización no coincide con las dimensiones del sistema de coordenadas del dispositivo de contexto.
Los dos métodos :setPageSize() y :setViewPort() ofrecen dos formas de escalar un gráfico. Ambos métodos
pueden usarse para que se produzca una salida en una ventana. El método :setViewPort() puede usarse sola-
mente para la salida en pantalla y está limitado a la salida gráfica que contiene imágenes vectoriales. Ningún
sistema de escalado automático se lleva a cabo en el despliegue de imágenes reticuladas (bitmaps) u operaciones
reticuladas (la función GraBitBlt() ). El despliegue de bitmaps o de fuentes de bitmap ocurre siempre en las co-
ordenadas del dispositivo de contexto. Los caracteres que se exhiben con el fuente del bitmap no pueden esca-
larse y los bitmaps deben escalarse explícitamente usando GraBitBlt().
Como característica especial la ventana XbpCrt tiene ya un espacio de presentación y sirve como dispositivo de
contexto para él. Una ventana XbpCrt forma una unidad con su Espacio de Presentación y por tanto permite el uso
más fácil posible del GraphicsEngine. El método :presSpace() de una ventana XbpCrt devuelve su espacio de
presentación. Este método existe sólo en la clase XbpCrt. Mientras la ventana de aplicación es una ventana
XbpCrt, las funciones Gra..() pueden llamarse sin elegir un Espacio de Presentación como primer parámetro
puesto que predetermina a SetAppWindow():presSpace() .
Además de proveer al Espacio de Presentación predeterminado, una ventana XbpCrt tiene inteligencia interna que
encapsula complejidades del GUI con las que el programador se topa generalmente. El espacio de presentación
de un XbpCrt 'recuerda' lo que se está desplegando actualmente en la ventana. Retiene la salida gráfica. Esta
inteligencia se vuelve obvia cuando una ventana XbpCrt se cubre temporalmente por otra ventana y luego se re-
gresa al frente. En este caso, la parte cubierta de una ventana de XbpCrt debe refrescarse o pintarse una vez más,
respectivamente. Para repintarse, una ventana recibe al evento xbeP_Paint del sistema operativo. Es normal-
mente una tarea del programador implementar un código para un repintado de la ventana. Este no es el caso con
las ventanas de XbpCrt. Procesa mensajes xbeP_Paint por las suyas y causa que su Espacio de Presentación
despliegue nuevamente partes perdidas de los contenidos de la ventana.
Debido a su inteligencia interna, el Espacio de Presentación de una ventana XbpCrt garantiza a toda la salida vi-
sible que se despliegue nuevamente cuando la ventana XbpCrt se cubre temporalmente y trae al frente una vez
más. Asume el siguiente código de línea:
Esta llamada a la función GraLine() dibuja una línea, diagonal desde el lado inferior izquierdo hasta la esquina
superior de la derecha. Permanece visible inclusive si se les hace clic a las otras ventanas al frente temporaria-
mente. La línea se despliega nuevamente cuando una ventana XbpCrt regana foco y no debe implementarse
ningún código para procesar el evento xbeP_Paint.
Un Micro PS puede usarse por todos los Componentes de Xbase subclasificados a partir de la clase XbpWindow.
Se solicita y devuelve por el método :lockPS() . Un Componente de Xbase tiene acceso exclusivo a este Micro PS
hasta que lo libera con :unlockPS() y lo devuelve al sistema operativo. De lo contrario, con el nivel de lenguaje de
Xbase++, un Micro PS se representa por un objeto. Tiene los mismos métodos que la clase XbpPresSpace, ex-
cepto por los métodos :create() , :configure() y :destroy() . Debido a detalles técnicos del sistema operativo, un
Micro PS puede crearse solamente por :lockPS() y debe liberarse con :unlockPS() (nota: la clase XbpCrt no se
subclasifica desde XbpWindow. Por tanto, un Micro PS no puede usarse por una ventana XbpCrt). El uso general
de un Micro PS sigue con este patrón:
Se despliega un dibujo en un Micro PS de la misma manera que en un Espacio de Presentación de una ventana
XbpCrt. Sin embargo, debido a esta optimización de la performance no están reconocidos segmentos gráficos así
como caminos gráficos. La salida gráfica se limita a funciones Gra..() simples. Las funciones complejas GraSe-
gOpen() y GraPathBegin() además de las funciones correspondientes para manejar segmentos y caminos gráficos
no pueden usarse con un Micro PS. Además, un Micro PS no realiza un buffer de pantalla. Esto requiere de la
reacción de un Componente de Xbase con el evento xbeP_Paint. Cuando se usa un Micro PS, debe implemen-
tarse el código para la salida gráfica a desplegarse luego de eventos de re- pintado. Este código puede progra-
marse tanto en una función llamada por el bloque de códigos de la pila de llamadas de :paint , o debe programarse
en el método de pila de llamadas de :paint() de una clase derivada. Un ejemplo de uso de un Micro PS se en-
cuentra en ...\SAMPLES\XPARTS\MICROPS.PRG.
El ejemplo demuestra los pasos diferentes para usar un PS completo. El dispositivo de contexto (el dispositivo de
salida) lo devuelve el método :winDevice() , y es enlazado al PS cuando se lo pasa al método :create() . Luego es
posible crear un dibujo usando las funciones Gra..() y la salida aparece en la ventana de XbpDialog. Ya que el PS
no hace un buffer de la salida de pantalla, el código que crea un dibujo debe ejecutarse repetidamente luego de los
eventos de xbeP_Paint.
La Impresora - XbpPrinter()
Los objetos de la clase XbpPrinter se usan para salida impresa. Esto requiere la instalación de la unidad de im-
presión apropiada para una impresora.
La función definida por el usuario PrinterPS() crea un espacio de presentación asociado con el contexto del dis-
positivo impresora. Recibe como parámetro el nombre de uno de los objetos de impresión instalados en el sistema
como una cadena de caracteres y crea un objeto XbpPrinter que mantiene la conexión al objeto de impresión. En
la función, se crea un nuevo Espacio de Presentación y al objeto de impresión se lo provee como dispositivo de
contexto. El valor de retorno del método :paperSize() se utiliza para el tamaño de página del espacio de presen-
tación. Esto asegura que el Espacio de Presentación y el dispositivo de contexto tengan el mismo tamaño de
sistemas de coordenadas. Puesto que el tamaño del papel siempre se devuelve por el método :paperSize() en las
unidades de 1/0mo de milímetros, la constante GRA_PU_LOMETRIC se especifica en la llamada a oPS:create()
para establecer las mismas unidades en el Espacio de Presentación.
La salida de los gráficos en la impresora puede llevarse a cabo usando la función PrinterPS() como se muestra en
el siguiente ejemplo:
Estas cuatro líneas de código muestran el procedimiento gráfico para la impresión de gráficos. Un Espacio de
Presentación asociado con un objeto XbpPrinter (contexto del dispositivo impresora) deben estar disponibles. En
el ejemplo, el método :device() devuelve al objeto XbpPrinter y comienza la salida de la impresión usando la
llamada al :startDoc() . Generalmente, los trabajos de impresión son enviados a la cola de impresión y :startDoc()
abre el spooler. Toda la salida gráfica que se produce en el Espacio de Presentación se envía al spooler. El final
del trabajo de impresión se señala por el método :endDoc() que cierra al spooler.
La función PrinterPS() muestra cómo se crea un Espacio de Presentación para salidas en la impresora, y también
muestra la dificultad básica de la salida gráfica: el sistema de coordenadas de los dispositivos de salida "pantalla"
e "impresora" difieren en cosas como las unidades del sistema de coordenadas de los dos dispositivos. El si-
guiente código del programa ilustra esto:
Esta llamada a GraBox() dibuja un cuadrado con una longitud de eje de 1000 unidades. Cómo y dónde se des-
pliega el cuadrado depende del dispositivo de contexto asociado con el Espacio de Presentación oPS y en las
unidades para el sistema de coordenadas. La unidad del dispositivo para la pantalla es "pixel" y el cuadrado será
parcialmente visible sólo en la pantalla excepto cuando hay una extremadamente alta resolución de pantalla. Si la
impresora es el dispositivo de contexto, la unidad es 1/0mo de milímetros y el cuadrado será desplegado con una
longitud de eje de 10cm que se acomoda a los formatos de papel más comunes.
#include "Gra.ch"
#include "Xbp.ch"
PROCEDURE Main
LOCAL oWindowPS, oImpresoraPS, oFont, aListaDeFonts
LOCAL i, imax, nY, nPointSize, cTexto, aSize
oFont := aListaDeFonts[i]
oFont:nominalPointSize := ++nPointSize
ENDIF
NEXT
oImpresoraPS:device():endDoc()
RETURN
Este programa de ejemplo imprime el tamaño y el nombre de todos los fonts vectoriales en la impresora. Primero,
se determina el espacio de presentación de la ventana de aplicación. La función PrinterPS() crea un Espacio de
Presentación que se enlaza con un dispositivo de contexto de impresión. Un objeto XbpFont se genera luego para
manejar los fonts. La llamada oFont:list() devuelve un arreglo conteniendo los objetos Font para todos los fonts
que pueden desplegarse en una ventana. La salida de impresión comienza luego. El texto a ser impreso se crea en
el ciclo FOR..NEXT. Sólo se imprime cuando las variables de instancia :generic y :vector de un objeto Font en el
arreglo aListaDeFonts contiene .T. (verdadero). Este es el prerrequisito para los fonts que pueden desplegarse en
una ventana y puede también mostrarse a escala en la impresora. Dentro del ciclo FOR..NEXT, los objetos de
fuente que satisfacen el prerrequisito se pasan usando GraSetFont() para el Espacio de Presentación oImpre-
soraPS que se asocia con el contexto del dispositivo impresora. En este punto en el programa de ejemplo, el
Espacio de Presentación conoce que debe imprimirse y los datos métricos para el tamaño de las letras. Puesto
que el tamaño del punto para un Font se incrementa de a uno después que cada uno pasa a través del ciclo, la
posición y de la cadena de caracteres debe ajustarse continuamente a la altura de las letras. La altura de la cadena
de caracteres cTexto se determina usando la función GraQueryTextBox().
El programa de ejemplo trata todos los problemas que deben considerarse en la programación de la salida gráfica
del texto en una impresora. El texto se muestra en modo gráfico usando la función GraStringAt(). Con el fin de que
los caracteres sean impresos en el papel la salida debe ocurrir en un Espacio de Presentación enlazado con el
contexto del dispositivo impresora (un objeto XbpPrinter). el Font usada para la salida de la impresión se establece
usando un objeto XbpFont. los fonts disponibles se determinan usando el método :fontList() de un objeto fuente.
En el ejemplo, todas los fonts que pueden desplegarse en un Espacio de Presentación para una ventana se
prueban puesto que el espacio de presentación de la ventana de aplicación se provee al objeto XbpFont
(XbpFont():new(oWindowPS) ). Sólo los fonts de vector independientes de dispositivo (:generic y :vector son .T.)
pueden usarse para impresión. Cuando :generic y :vector son .T., esto indica que el Font puede escalarse co-
rrectamente durante la impresión. La posición exacta donde se imprime una cadena de caracteres cTexto en el
programa de ejemplo debe calcularse después de cualquier paso a través del ciclo usando la función GraQue-
rytextBox(), ya que el tamaño del punto de el Font está bajo cambio continuo.
Se puede resumir el uso de los objetos XbpPrinter para mostrar texto o gráficos de la siguiente manera:
Estos cinco puntos deben siempre considerarse usando objetos XbpPrinter o enviando la salida gráfica a una
impresora.
El Metaarchivo - XbpMetaFile()
Los Metaarchivos juegan un rol central en guardar e intercambiar datos gráficos y dibujos. Los Metaarchivos
guardan imágenes de la misma forma en la que se dibujan en el Espacio de Presentación y permiten que se re-
despliegue el dibujo como es necesario en la misma o en otra aplicación. Las imágenes pueden guardarse e im-
primirse en el formato de un Metaarchivo, intercambiarse entre dos estaciones de trabajo en este formato y
guardarse temporariamente en el Clipboard mientras se transfieren de una aplicación a la otra.
La secuencia de primitivas gráficas usadas para crear un dibujo se guardan en un Metaarchivo. Esto incluye las
coordenadas de las primitivas gráficas y sus atributos. un Metaarchivo generalmente contiene una imagen vecto-
rial porque guarda una secuencia de primitivas gráficas que pueden ser "recreadas" nuevamente luego que se
carga el Metaarchivo en un Espacio de Presentación. Los Metaarchivos se utilizan también para intercambiar
dibujos entre dos estaciones de trabajo. Mientras el Metaarchivo contenga un dibujo creado usando sólo primitivas
gráficas, puede mostrarse un dibujo en diferentes pantallas e impresoras. Las imágenes reticuladas pueden
guardarse también en un Metaarchivo (ver la función GraBitBlt() ), y en este caso no se garantiza el correcto re-
despliegue de un Metaarchivo en diferentes dispositivos de salida porque es dispositivo dependiente.
Xbase++ permite que se carguen, desplieguen, guarden y creen Metaarchivos. Para cargar y desplegar se usan
objetos de la clase XbpMetaFile(). Ellos manejan Metaarchivos que están ya guardados en el disco. Las si-
guientes líneas de código muestran cómo los Metaarchivos se exhiben en la ventana:
Este procedimiento se designa para guardar los Metaarchivos existentes en una aplicación de Xbase++. Pero
también es posible guardar las salidas gráficas creadas por una aplicación de Xbase++ en un Metaarchivo. Para
llevar a cabo esto, debe crearse un objeto XbpFileDev. Este representa al dispositivo de contexto donde la salida
gráfica de las funciones Gra...() se registran. Por ejemplo:
PROCEDURE Main
LOCAL oPS, oDC, oMF
En este ejemplo, un objeto XbpFileDev se crea como un dispositivo de contexto y se asocia con un Espacio de
Presentación. La salida gráfica se lleva a cabo en el Espacio de Presentación. Cuando se completa la salida, el
dispositivo de contexto se separa del Espacio de Presentación usando una llamada al método :configure() del
Espacio de Presentación. El objeto XbpFileDev puede ahora crear un objeto XbpMetaFile conteniendo la salida
gráfica de GraBox() y GraStringAt(). El método :save() guarda esta información gráfica en un nuevo Metaarchivo.
Si este archivo de recurso se compila por el compilador de recursos y enlaza al archivo ejecutable, un objeto
XbpBitmap puede cargar y desplegar el recurso 2000. Esto se realiza en el ejemplo a continuación:
PROCEDURE Main
LOCAL oBMP, oPS
SetColor( "N/W" )
CLS
// cargar bitmap
oBMP:load( NIL, 2000 )
// desplegar bitmap en el PS
oBMP:draw( oPS, {50,100} )
WAIT
RETURN
Antes de la creación de un objeto XbpBitmap, el Espacio de Presentación y su dispositivo de contexto deben existir
para usarse para la salida de la imagen reticulada. En el ejemplo, la salida ocurre en el espacio de presentación de
una ventana XbpCrt y esto representa al dispositivo de salida. El Espacio de Presentación debe especificarse en la
llamada al método :create() que prepara al objeto XbpBitmap por el despliegue en la pantalla. La imagen reticulada
desde el archivo PHOTO.BMP con el recurso ID 2000 se carga luego usando el método :load() y despliega en el
Espacio de Presentación de la ventana usando :draw() .
Se utiliza un objeto XbpBitmap inclusive cuando una imagen reticulada debe crearse temporariamente en la
memoria principal. Se proveen los métodos :presSpace() y :make() para esta situación. Un objeto XbpBitmap se
asocia con un Espacio de Presentación usando :presSpace() . La salida gráfica puede producirse luego direc-
tamente en la imagen reticulada, y no es visible en la pantalla. Después de que un objeto XbpBitmap ha recibido su
propio Espacio de Presentación usando el método :presSpace() , el tamaño de la imagen reticulada deben es-
pecificarse usando el método :make() . La memoria de la información de pantalla (pixeles) se ubica luego. La salida
gráfica puede producirse entonces en el Espacio de Presentación de un bitmap.
El área más importante para este uso de los objetos XbpBitmap se encuentra en buffering del modo de salida
Gráfica por pantalla. El despliegue entero en una ventana o en una sección pueden guardarse como una imagen
reticulada de un objeto XbpBitmap y redesplegada tiempo después. La información gráfica del Espacio de Pre-
sentación de una ventana se copia al Espacio de Presentación de un objeto XbpBitmap usando la función Gra-
BitBlt(). Esto se muestra en el siguiente ejemplo en el código para la función definida por el usuario GraSaveS-
creen():
PROCEDURE Main
LOCAL oPS, oBitmap
// dibujar recuadro
GraBox( oPS ,{10,10}, {100,100} )
// guardar recuadro
oBitmap := GraSaveScreen( oPS, {10,10}, {91,91} )
// limpiar la ventana
CLS
WAIT "El recuadro fue guardado"
oBitmap:presSpace( oTargetPS )
oBitmap:make( aSize[1], aSize[2] )
La función GraSaveScreen() recibe al Espacio de Presentación de una ventana en el parámetro oSourcePS . Éste
contiene la información gráfica a ser guardada para copiarla al Espacio de Presentación oTargetPS . Se crea un
objeto XbpBitmap como un dispositivo de salida que es útil para la salida en la pantalla (oSourcePS se pasa
a :create() ). Para que el objeto XbpBitmap pueda guardar la salida gráfica necesita su propio Espacio de Pre-
sentación que se pasa al objeto usando el método :presSpace() . El Espacio de Presentación creado reciente-
mente se referencia por oTargetPS . El tamaño de la imagen reticulada se especifica en :make() . El tamaño co-
rresponde al parámetro aSize que define las dimensiones de la sección de la pantalla a guardarse en la dirección
x y en la dirección y. Finalmente, la función GraBitBlt() copia los pixeles desde el espacio de presentación de la
ventana al Espacio de Presentación del objeto XbpBitmap.
La función definida por el usuario GraRestScreen() se utiliza para redesplegar la sección guardada de la pantalla.
La imagen reticulada manejada por un objeto XbpBitmap se redespliega en el Espacio de Presentación de una
ventana. Llamando al método :draw() es suficiente para llevar a cabo esta tarea:
Un objeto XbpBitmap necesita un Espacio de Presentación donde salga una imagen reticulada manejada por el
objeto. Cuando se añade un bitmap como un recurso a un archivo ejecutable, llamando al método :load() es su-
ficiente para permitir el despliegue de una imagen reticulada. este método requiere de manera implícita un Espacio
de Presentación para el objeto XbpBitmap. Si, sin embargo, una imagen reticulada necesita crearse (durante el
buffering de pantalla, por ejemplo), el objeto XbpBitmap debe tener su propio Espacio de Presentación que es
provisto al objeto por el método :presSpace() . Este método debe ejecutarse antes de la llamada a :make() . :make()
define las dimensiones para una imagen reticulada y la memoria requerida para sostener todos los pixeles de la
imagen reticulada. La memoria requerida puede determinarse sólo cuando se provee un Espacio de Presentación
asociado a un dispositivo de contexto.
Ya que una ventana XbpDialog no hace un buffer de la salida de pantalla, debe reaccionar al mensaje xbeP_Paint
si las funciones Gra..() se usan para dibujar en la pantalla. Lo mismo es cierto para todos los demás Componentes
de Xbase. A parte de la salida de pantalla, una ventana XbpDialog no difiere de un botón XbpPushButton o de un
objeto XbpStatic. La única diferencia es que una ventana XbpDialog es un objeto compuesto y el dibujo debe
realizarse en el área de dibujo debajo de la barra del título de la ventana (XbpDialog:drawingArea ). En un ele-
mento XbpStatic, por ejemplo, la salida de pantalla aparece en el objeto mismo.
Con el fin de permitir que un Componente de Xbase reaccione al evento xbeP_Paint, debe reasignarse un bloque
de código a la variable de instancia :paint . Este bloque de código debe llamar a una rutina donde se implementa
la salida gráfica. Como alternativa, una clase definida por el usuario puede derivarse de un Componente de Xbase.
En este caso, el método :paint() debe sobrecargarse y el código para la salida gráfica debe implementarse en este
método. Cualquiera de las dos posibilidades requiere de Espacio de Presentación asociado con un Componente
de Xbase cuando se llaman a las funciones Gra..(). Esto puede ser un Micro Espacio de Presentación devuelto por
el método :lockPS() o por un objeto XpbPresSpace enlazado al dispositivo de contexto del Componente de Xbase.
Es el valor de retorno del método :winDevice() .
Los diferentes aspectos de la salida gráfica en las ventanas, o Componentes de Xbase, se ilustran en el programa
de ejemplo CHARTS.PRG que se localiza en el directorio ..\SOURCE\SAMPLES\GRAPHIC. Este programa dibuja
una tabla de simple entrada - gráfico simple de líneas conectando un conjunto de puntos con líneas usando
GraLine():
LineChart( , aValores )
RETURN
Este código ya trabajaría de haberse estado usando una ventana XbpCrt para despliegue. El procedimiento Li-
neChart() simplemente recibe un arreglo de valores especificando las coordenadas xy para conectar los puntos
con líneas. Sin embargo, el programa de ejemplo CHART.PRG utiliza un objeto XbpDialog y otro XbpStatic para el
despliegue.
El programa dibuja la tabla de simple entrada en la ventana XbpDialog y luego en tamaño reducido en el objeto
XbpStatic (marco-frame). El código del programa que despliega el XbpDialog se da debajo. Consiste de la crea-
ción de la ventana, la definición de bloque de códigos :paint , el despliegue de la ventana y el ciclo de eventos:
// Crear ventana oculta
oDlg := XbpDialog():new(,,{10,10},{400,400},,.F.)
oDlg:title := "Gráfico de Líneas"
oDlg:create()
// Desplegar ventana
oDlg:show()
// Procesar eventos
DO WHILE nEvent <> xbeP_Close
nEvent := AppEvent( @mp1, @mp2, @oXbp )
oXbp:handleEvent( nEvent, mp1, mp2 )
ENDDO
El bloque de códigos :paint debe asignarse al área de dibujo del objeto XbpDialog (oDlg:drawingArea ) porque
exhibe el dibujo. Los bloques de código llaman al procedimiento LineChart() que dibuja la tabla. La evaluación del
bloque de código, sin embargo, tiene lugar en el método :handleEvent() cuando el evento xbeP_Paint es devuelto
a partir de AppEvent(). Los pasos siguientes conducen al despliegue de la tabla de entradas - Gráfico de Líneas.
El resultado final del evento xbeP_Paint es que oDlg:drawingArea se pasa al bloque de códigos :paint como tercer
parámetro. Dentro del bloque de códigos se requiere un Micro PS usando :lockPS() que pasa junto con aValores al
procedimiento LineChart(). Llamar a una rutina de dibujo de esta manera vía el bloque de código :paint también
garantiza que se ejecuta el procedimiento nuevamente cuando la ventana se cubre temporariamente por otra y
recupera foco. En este caso, el sistema operativo crea un nuevo evento de xbeP_Paint y evalúa una vez más al
bloque de código :paint .
El despliegue de la tabla de entrada en el objeto XbpStatic sigue la misma lógica: se llama a LineChart() desde el
bloque de código :paint . Pero en vez de un Micro PS, se usa un objeto XbpPresSpace que se enlaza a un dis-
positivo de contexto de ventana del objeto XbpStatic:
La tabla de entrada se dibuja dentro del objeto XbpStatic con las mismas coordenadas exactas que con el objeto
XbpDialog. La razón por qué es completamente visible se da por el tamaño de página y el Area de Visualización
del Espacio de Presentación. La página es lo suficientemente grande como para desplegar todos los puntos
(tamaño 310*310 pixel vs. rango de 0-300). La página se escala al Area de Visualización que es tan grande como
el marco estático (80*80 pixel). Por tanto, la tabla se reduce en tamaño pero es completamente visible.
La salida a la ventana y la impresora - WYSIWYG
Un Espacio de Presentación contiene toda la información del dispositivo independiente de un dibujo. Puede en-
lazarse a diferentes dispositivos de salida y por ende un dibujo puede desplegarse en una ventana o en una im-
presora. Dependiendo del dispositivo de contexto al que un espacio de presentación se enlaza, un dibujo puede
desplegarse en una ventana o imprimirse. Con el fin de cumplir con la regla WYSIWYG (Lo Que Ve Es Lo Que
Obtiene), debe desplegarse un dibujo en la ventana en la escala correcta. Las proporciones de un dibujo des-
plegado en una ventana deben ser las mismas que en el papel luego de la impresión.
El despliegue de lo impreso en una ventana en la escala correcta involucra tres diferentes niveles de abstracción:
un espacio de presentación y dispositivos de contexto para la ventana y la impresora. Además, los dispositivos de
contexto para los dos dispositivos de salida tienen diferentes unidades de medida para su sistema de coordenadas.
Una ventana despliega un dibujo basado en la medida pixel mientras la impresora usa unidades de 1/10mo de
milímetro.
Esta ilustración se basa en el programa de ejemplo PREVIEW.PRG. Demuestra varios aspectos para salidas
correctamente a escala en diferentes dispositivos. El programa lista los registros de una base de datos en una
ventana y puede imprimirlos. Los contenidos de los campos de la base de datos se dibujan con la función GraS-
tringAt().
Sobre el lado izquierdo de la ilustración, se muestra la ventana de aplicación que se crea por el programa de
ejemplo. Los registros de la base de datos se exhiben en esta ventana por una instancia de la clase definida por el
usuario XbpPreview, implementada en el programa de ejemplo. Su tamaño es de 270 x 380 pixel. Sobre el lado
derecho del objeto XbpPreview, hay botones que agrandan (zoom ++), reducen (zoom --) o imprimen lo que se
exhibe. Si el despliegue se agranda puede ser desplazado por medio de dos barras de desplazamiento. El objeto
XbpPreview provee al dispositivo de contexto para la salida en pantalla (dispositivo de ventana).
Se muestra un pedazo de papel en el lado derecho de la ilustración. Representa lo impreso (el dispositivo im-
presora) y tiene formato A4. Esto significa que su tamaño es 2100 x 2970 unidades, siendo una unidad 1/10mo de
milímetro.
Escalado
Una escala automática del dibujo y la transformación de los sistemas de coordenadas de ambos dispositivos de
salida los realiza el espacio de presentación. Este define las unidades y el tamaño del sistema de coordenadas.
Para lo impreso el sistema de coordenadas debe dimensionarse de acuerdo al tamaño del papel. Puede deter-
minarse con el método :paperSize() de un objeto XbpPrinter. Este método devuelve un arreglo conteniendo el
tamaño de una hoja en unidades de 1/10mo de milímetro. Para algunas impresoras especialmente las impresoras
láser el tamaño debe ajustarse puesto que hay márgenes sobre los que no pueden imprimirse.
Las coordenadas de la impresora se usan en el Espacio de Presentación. Esto significa que el tamaño de la página
del Espacio de Presentación es idéntico al área imprimible en un pedazo de papel. Con el fin de desplegar la
página completa de un Espacio de Presentación en la ventana, el Area de Visualización debe establecerse al
tamaño de la ventana. En este caso se usan las coordenadas de la ventana (pixel). Si el tamaño de la página y el
Area de Visualización se dimensionan apropiadamente, toda la salida gráfica dibujada en las coordenadas de la
impresora aparece en la escala correcta en una ventana.
Además del Area de Visualización y del tamaño de la página, es necesario seleccionar una vector fuente en el
Espacio de Presentación. Esto se requiere para la correcta escala del texto ya que las fuentes de bitmap no
pueden escalarse. Por tanto, un Espacio de Presentación debe prepararse en varios pasos para conseguir una
vista preliminar WYSIWYG de lo impreso. Estos pasos se discuten en el siguiente código (nota: se asume que la
variable oPrinter referencia a un objeto XbpPrinter y oWindow referencia a un Componente de Xbase):
Cuando se prepara un Espacio de Presentación de esta manera, las funciones Gra..() pueden usarse para pro-
ducir la salida gráfica. El despliegue en la ventana corresponde a lo impreso y aparece en la escala correcta. No
obstante, la salida por pantalla es extremadamente reducida en tamaño comparada con la salida impresa.
Ampliación
Si deben chequearse los detalles de una vista preliminar de lo que se va a imprimir, debe agrandarse el despliegue
en la ventana (ampliada). Esto se logra al agrandar el Area de Visualización del Espacio de Presentación. En este
caso, sólo una parte del Area de Visualización es visible en la ventana. Asuma el siguiente código:
Se asume que la ventana es 270 pixeles de ancho y 380 pixeles de alto. Si el Area de Visualización se agranda al
doble de ancho y alto (540 x 760 pixeles), la ventana despliega sólo el cuarto inferior de la izquierda del Area de
Visualización. Ya que el tamaño de la ventana permanece inalterado, se exhibe sólo un cuarto del dibujo en vez del
dibujo entero. Esta parte del dibujo aparece agrandada cuatro veces.
Desplazamiento
Mientras el Area de Visualización es más grande que la ventana, sólo un Componente de la ventana es visible y el
resto es recortado. Para ver las diferentes partes del dibujo, éste debe ser desplazado dentro de la ventana. El
desplazamiento se realiza cambiando el origen del Area de Visualización relativo al origen de la ventana. El origen
del Area de Visualización se establece en coordenadas negativas:
oPresSpace:setViewPort( aViewPort )
En este ejemplo el Area de Visualización de tamaño 540 x 760 pixel se establece en las coordenadas
{-270,-380,270,380}. Esto es relativo al punto {0,0} en la ventana. El origen del Area de Visualización se desplazó
para abajo del lado izquierdo mientras su tamaño permaneció inalterado. Como resultado, la parte superior de la
derecha del dibujo se torna visible en la ventana.
Si el origen del Area de Visualización se establece en coordenadas negativas, todas las partes de un dibujo
agrandado pueden visualizarse en una ventana. En el programa ejemplo de PREVIEW.PRG, esto se lleva a cabo
usando dos barras de desplazamiento así el dibujo puede desplazarse con clics del mouse.
Imprimiendo
Cuando se crea un Espacio de Presentación usando un coordenadas de impresora un dibujo visible en pantalla
puede imprimirse fácilmente al intercambiar el dispositivo de contexto del Espacio de Presentación. Para imprimir,
se usa un objeto XbpPrinter como dispositivo de contexto y el dibujo debe redesplegarse para su aparición en el
papel. Los siguientes pasos son necesarios para imprimir un dibujo:
// Abrir el spooler
oPrinter:startDoc()
// Redesplegar el dibujo
Gra..( oPresSpace )
// Cierra el spooler
oPrinter:endDoc()
El dispositivo de contexto se intercambia al pasar el objeto correspondiente al método :configure() del Espacio de
Presentación. Esto transfiere la salida gráfica desde la ventana a la impresora. Puesto que el Area de Visualiza-
ción se pierde cuando se cambian los dispositivos de contexto, debe guardarse y hacerse reset.
Esta función presume que ambos parámetros son siempre del tipo numérico y que el parámetro nDivisor jamás es
igual a cero. Si el programador puede garantizar estas condiciones la función es aceptable como se la muestra
aquí. Sin embargo, uno debe asumir que normalmente pueden pasarse argumentos incorrectos que podrían
conducir a un error del programa. Un caso como este puede evitarse si se prueban los parámetros.
En este ejemplo, la función cubre todas las posibles condiciones de error. Prueba los dos parámetros corrobo-
rando que sean del tipo de dato numérico y evita la división por cero. De esta manera, la función Divide() sigue la
estrategia defensiva del manejo de error porque la función evita todas las potenciales fuentes de error. Si se pasa
un parámetro incorrecto la función simplemente substituye un resultado numérico (el valor cero).
La estrategia de evitar la fuente de todos los posibles errores garantiza programas estables, pero puede conducir
a una reducción significativa en la performance del programa durante el tiempo de ejecución. En una programa-
ción cuidadosamente programada los errores no ocurren a menudo. No se requieren generalmente pruebas fre-
cuentes de errores potenciales; de hecho, estas pruebas consumen bastante tiempo.
Por tanto, la estrategia defensiva del manejo de errores tienen un efecto negativo en la performance de un pro-
grama durante el tiempo de ejecución. Esto es porque se ejecutan pruebas frecuentemente que son raras veces
necesarias y que son superfluas algunas veces. Es mas ventajoso usar una estrategia de manejo de error ofensiva
que evita pruebas de rutinas superfluas y que asumen que no ocurrirá error alguno. Esta filosofía es básica en un
sistema operativo de 32 bit y debería también seguirse cuando se programa usando Xbase++.
Si bien la función Divide() no es realmente necesaria demuestra las dos "filosofías" básicas respecto al manejo de
errores producidos durante el tiempo de ejecución. El tratamiento ofensivo de errores es generalmente ventajoso
ya que provee una performance mejorada durante el tiempo de ejecución. Para el programador, el manejo ofen-
sivo de errores significa que, desde el principio del desarrollo del proceso, el código del programa se organ iza en
dos partes. Estas partes están empotradas en la estructura BEGINSEQUENCE...ENDSEQUENCE. Una parte
contienen la lógica del programa normal. La otra parte captura las condiciones del error y retorna al programa a
una condición estable asegurando una ejecución continua, ininterrumpida. El manejo ofensivo del error protege al
código del programa principal que mejora la calidad y manutención del programa. Es posible atrapar a todos los
errores con el manejo ofensivo del error, lo que no es posible usando la estrategia defensiva salvo que todas las
condiciones posibles se prueben explícitamente. Debería notarse que no hay una regla universal para usar en la
determinación de que condiciones del programa se definen como errores y cuando debe llevarse a cabo un ma-
nejo simple del error. Las condiciones de error simples como la división por cero son la excepción en vez de la
regla. Cuando en duda, una decisión subjetiva debe hacerse respecto a que constituye un error y que forma de
manejo del error será empleada. En muchas condiciones del error tiene sentido no intentar la recuperación del
error, pero sencillamente abortar el programa. En la mayoría de las condiciones de error la terminación del pro-
grama es la reacción predeterminada del sistema de manejo de errores de Xbase++ (ver el archivo
ERRORSYS.PRG).
Con el fin de utilizar información en un objeto error, debe definirse un parámetro en el bloque de códigos de error.
En caso de un error, el parámetro recibe un objeto error que puede pasarse a cualquier función. El caso mas
simple es pasar el objeto error a la función Break() que continua con el flujo del programa luego de la declaración
RECOVER. Esto se implementa en el siguiente ejemplo de programa. Este programa prueba la disponibilidad
operativa de las unidades. La prueba ocurre en la función definida por el usuario DiscoEstaListo() llamada desde
Main() . En el procedimiento Main() , la letra de una unidad puede ingresarse y a esta unidad se la prueba para
corroborar su disponibilidad.
**************
PROCEDURE Main // probar Discos para
LOCAL cDisco := "C" , nListo // ver si están listos.
RETURN
*******************************
FUNCTION DiscoEstaListo( cDisco ) // Está listo el Disco ?
LOCAL nReturn := 0
LOCAL cVjoDisco := CurDrive() // recordar Disco actual
LOCAL bError := ErrorBlock( {|e| Break(e) } )
LOCAL oError
BEGIN SEQUENCE
IF oError:osCode == DRIVE_NOT_READY
nReturn := -1 // El Disco no está listo
ELSE
nReturn := -2 // Disco no disponible
ENDIF // o no válido
ENDSEQUENCE
RETURN nReturn
La función definida por el usuario DiscoEstaListo() incluye algunas técnicas importantes para el manejo de error.
Comienza asignando el bloque de códigos de error {|e| Break(e) } que se ejecuta si se produce un error. El bloque
de códigos pasa un objeto error a la función Break(). Por ejemplo, se produce un error cuando se ingresa uno de
los caracteres "&/(123" en Main() o cuando se llama a DiscoEstaListo("K") y no existe la unidad "K".
La función Break() continua al flujo del programa después de la declaración RECOVER. En el ejemplo de pro-
grama, la declaración RECOVER incluye la opción USING definiendo la variable oError . Esta ultima recibe el
parámetro que se le pasa a Break(), en este caso es el objeto error. La variable oError referencia un objeto error
solo luego que ocurre un error. Sino contiene el valor NIL y no se ejecuta el código del programa entre RECOVER
y ENDSEQUENCE.
Si ocurre un error, un objeto error se crea automáticamente y se pasa a un bloque de códigos de error. Este objeto
error contiene información sobre el error en los valores de sus variables de instancia. Dentro del bloque de códigos,
se pasa el objeto a la función Break() que se ramifica a RECOVER. El objeto error se le asigna finalmente a la
variable oError especificada en RECOVER USING. Luego que se produce un error, las variables de instancia del
objeto error pueden accederse luego por el código del programa entre RECOVER y ENDSEQUENCE. En este
ejemplo, solo la variable de instancia :osCode es inspeccionada porque contiene el código de error para los errores
que pueden darse durante la prueba para determinar la disposición operativa de la unidad (osCode = código del
Sistema Operativo - Operating System Code). Se prueba la unidad usando el sistema operativo que genera un
error cuando una unidad no esta lista o no existe. En el sistema de manejo de error de Xbase++, los errores ge-
nerados por el sistema operativo son capturados.
La función DiscoEstaListo() asigna los diversos valores de retorno en el código del programa de manejo de errores.
Este código del programa no se ejecuta cuando la unidad a ser probada esta lista. En este caso, el código esencial
del programa contienen solo dos líneas. La función DiscoEstaListo() es un buen ejemplo de la estrategia del
manejo ofensivo de error, demuestra como pueden capturarse errores, y muestra como puede utilizarse infor-
mación sobre errores durante el tiempo de ejecución.
El ítem mas esencial para un manejo exitoso del error es el bloque de códigos de error, que recibe un objeto error.
El objeto error contiene información sobre el error que ocurrió. El código del programa para el manejo del error
aparece entre RECOVER y ENDSEQUENCE. La declaración RECOVER ocurre entre BEGIN SEQUENCE y
ENDSEQUENCE y puede recibir opcionalmente un valor como parámetro. La opción RECOVER USING define
una variable a ser asignada el valor del argumento pasado a Break().
------------------------------------------------------------
ERROR LOG
El registro de error comienza con la información de versión sobre Xbase++ y del sistema operativo. A saber, dos
secciones principales: el contenido del objeto de error y de la pila de llamadas. La pila de llamadas lista la se-
cuencia de llamadas a funciones, procedimientos o métodos que condujeron a la condición de error. El nombre de
la función es seguido por el numero de línea del archivo PRG donde se llama a la función. En el ejemplo, el registro
de error se inicia en la línea 155 de la función StandardEH() . Esta función se llama desde un bloque de códigos
que se programa en la línea 24 de ErrorSys()!EF. Ambas funciones son parte del archivo ERRORSYS.PRG e
implementan el manejo del error predeterminado de Xbase++. No contribuyen a un error durante el tiempo de
ejecución pero son parte de la pila de llamadas cuando se crea el registro de error. El error durante el tiempo de
ejecución ocurrió en la línea 230 de la función AbreCliente() que se programa en el archivo MDICUST.PRG. La pila
de llamadas identifica claramente la línea del programa que provoca un error durante el tiempo de ejecución. No
puede desplegar el nombre del archivo PRG, solo el numero de línea y el nombre de la función.
Si la información contenida en el objeto error no es suficiente para identificar claramente un error de programación,
la situación del error puede analizarse fácilmente con el debugger de Xbase++. Para ello, la aplicación debe ini-
ciarse desde el debugger y debe establecerse un punto de interrupción en la línea 230 del archivo MDICUST.PRG.
En este ejemplo, sin embargo, el objeto error provee suficiente información para identificar al error: el archivo
CLIENTE.DBF no podría encontrarse. El siguiente código ha provocado el error durante el tiempo de ejecución:
Esta línea de código se traduce por el preprocesador y el código actual ejecutado durante el tiempo de ejecución
se ve así:
En su variable de instancia :operation , el objeto error contiene una cadena con el nombre de la operación fallida o
de la función, respectivamente, y la variable de instancia :args contiene todos los argumentos o parámetros for-
males pasados a la función. El registro de error lista los tipos de datos y los valores de los argumentos (VALTYPE
y VALUE). La variable de instancia :description da una corta descripción de la operación fallida. En el ejemplo, el
error se provoca por el sistema operativo, y no por la aplicación de Xbase++. Esto se indica también por la variable
de instancia :osCode que contiene el valor 2 en este caso. Si esta variable de instancia contiene un numero no
igual a cero, luego esto es un código de error del sistema operativo.
Otra información importante se guarda en la variable de instancia :genCode . Es el código de error genérico de
Xbase++ que corresponde a la constante #define encontradas en el archivo ERRORSYS.CH. El numero 40
mostrado en el ejemplo iguala a la constante XPP_ERR_DOS e indica el error a provocarse por el Sistema
Operativo del Disco - Disk Operating System.
Sumario
El registro de error registra los contenidos de un objeto error junto con la pila de llamadas. La pila de llamadas
indica DONDE ocurrió un error durante el tiempo de ejecución mientras que el objeto error provee información de
POR QUE se provoco. En muchos casos los contenidos de las variables de instan-
cia :arg, :description, :operation, :osCode y :genCode son suficientes para identificar claramente y resolver un error
durante el tiempo de ejecución (por favor, referirse a la documentación de referencia para una descripción de otras
variables de instancia del objeto error).