CURSO BASICO DE CLIPPER 5.01 (II)      GRUPO  EIDOS        (Brief by HEST)


Una de las caractersticas de Clipper que  ms  llama  la  atencin  es  la 
capacidad  que  este  posee  para  asociar  la  ejecucin de un determinado 
proceso con la pulsacin de una  determinada  tecla  que  nosotros  hayamos 
definido previamente mediante el comando SET KEY <nCodigo> TO <Proceso>.
 
Esta  posibilidad  nos  permite poder "interrumpir" procesos automticos de 
Clipper  y  ejecutar  determinadas  tareas,  como  por  ejemplo activar una 
calculadora  cuando  se  est efectuando un READ. Cada vez que se pulse una 
tecla  durante  un  READ, Clipper comprueba si hemos definido alguna accin 
para ella y en tal caso comienza a ejecutar esa tarea especifica.
 
Esta  capacidad esta disponible en todas las situaciones de interaccin con 
el teclado ('wait states' = estados de espera), excepto al usar la  funcin 
InKey(). Curiosamente, la funcin InKey() es la ms utilizada para  disear 
bucles de control de pulsacin de teclas, cmo por ejemplo: 
 
   #include "inkey.ch" 

   local nTecla, lTerminar := .f.
 
   do while .not. lTerminar 
 
      nTecla = InKey( 0 ) 
 
      do case 
         case nTecla == K_ESC 
              lTerminar = .t.
 
         case nTecla == K_F1 
              ...
 
         case nTecla == K_F2 
              ...
      endcase 
   enddo 
 
Si  echamos  un vistazo al fichero de cabecera STD.CH podemos comprobar que 
es lo que Clipper hace en realidad con el comando SET KEY: 

#command SET KEY <n> TO <proc>                         ; 
      => SetKey( <n>, {|p, l, v| <proc>(p, l, v)} ) 
 
Sencillamente llama a la funcin  SetKey()  pasndole  dos  parmetros:  el 
cdigo de la tecla a asociar y un bloque de cdigo con la accin a ejecutar 
cuando se evale dicho bloque. Adems, ha preparado  el  bloque  de  cdigo 
para  que  pueda  recibir  tres  parmetros:  p,  l,  v.  Estos valores los 
suministrar Clipper y no son sino ProcName(), ProcLine() y ReadVar().
 
Por defecto, Clipper establece que la tecla K_F1 llamar a un procedimiento 
llamado   Help.   No  ocurrir  nada  en  caso  de  no  estar  definido  el 
procedimiento  Help.  La idea es establecer en F1 un sistema de ayudas para 
nuestro  programa (aprovecho para recomendarte HelpEdit de Francisco Morero 
es excelente!).
 
Es muy importante controlar desde dentro de las acciones que ejecutemos que 
el  usuario  no  pueda  volver  a  pulsar  la  misma  tecla  ya que iramos 
sobrecargando  el  stack  del  sistema hasta provocar su caida. Al final de 
este artculo te propongo una serie de soluciones muy cmodas ( PUSH  KEYS  
CLEAR, ... ).
 
La principal ventaja con respecto a la implementacin de este comando en la 
versin Summer 87 es que ahora es  posible  interrogar  a  una  determinada 
tecla  y  saber  que accin tiene asignada. La funcin SetKey() es del tipo 
SET/GET,  es  decir,  es una funcin que nos permite establecer o recuperar 
determinados valores. Si hacemos por ejemplo: 

        SetKey( K_F2, { |p,l,v| Calculadora( p, l, v ) } ) 
 
        al hacer: 
 
        SetKey( K_F2 )   // Clipper devuelve el bloque de cdigo que se 
                         // le asigno previamente.
 
Gracias a esta capacidad podemos salvar temporalmente el bloque  de  cdigo 
que  tena una tecla asignado previamente, cambiar la asignacin a un nuevo 
bloque de cdigo para ms tarde restaurar la situacin inicial: 
 
        local bOldF2 := SetKey( K_F2 ) 
 
        SetKey( K_F2, { || Calendario() } ) 
        ...
        SetKey( K_F2, bOldF2 ) 
 
Fijate  en que en la nueva asignacin que hemos realizado no hemos indicado 
los parmetros  en el bloque de cdigo que ms tarde recogeran ProcName(), 
ProcLine() y ReadVar(). Sino tenemos pensado usarlos  no  es  necesario  el 
especificarlos.  Fjate  en  esta caracterstica de la funcin SetKey() (un 
comportamiento bastante curioso  que  tambien  puede  observarse  en  otras 
funciones de Clipper): 

        local bOldF2 := SetKey( K_F2, { || Calendario() } ) 
 
        ...                           // Nos ahorramos una lnea de cdigo 
 
        SetKey( K_F2, bOldF2 ) 
 
Aunque en general este sistema de asignacin de  teclas  ha  sido  mejorado 
bastante  con  respecto  a  la  versin  Summer  87,  an  existen  algunas 
limitaciones que vamos a ver  cmo  superar  y  de  paso  crearnos  algunas 
funciones  nuevas  y  algunos comandos nuevos que nos hagan la programacin 
ms cmoda.
 
Una  limitacin que presenta este sistema es que no podemos tener ms de 32 
teclas  definidas simultaneamente. Pero el principal problema radica en que 
no hay forma de conocer a posteriori qu teclas estn definidas ya mediante 
SET  KEY.  Una primera solucin que puede ocurrrsenos es recorrer un bucle 
con  todos los posibles valores de teclas y comprobar si en alguno de ellos 
la funcin SetKey() nos devuelve un bloque de cdigo  nil. Algo as como: 
 
   local n, bBlock, aTeclas := {} 
 
   for n = -39 to 306 
       if ( bBlock := SetKey( n ) ) != nil 
          AAdd( aTeclas, { n, bBlock } )           // Esto no es solucin! 
       endif 
   next 

Sin embargo esto es totalmente  desaconsejable  ya  que  perderamos  mucho 
tiempo  cada  vez  que  quisiramos  salvar las teclas activas, borrarlas o 
cambiarlas,   y  restaurarlas  posteriormente.  Por  cierto,  la  forma  de 
desactivar una tecla es llamar a la funcin SetKey( nTecla, nil ). Hay  que 
distinguir  entre hacer esto y hacer SetKey( nTecla, {||nil} ). Esto ltimo 
lo  que  hara  sera inabilitar una determinada tecla y no liberarla de su 
accin asociada (toma nota de este truco para desabilitar teclas!:

 SetKey( nTecla, {||nil} ).
 

Antes  de ver cmo solucionar estas limitaciones que hemos comentado veamos 
cmo podemos hacer para resolver la limitacin que tenemos al usar  InKey() 
para realizar un bucle de control del teclado. La solucin es  bien  fcil, 
se  trata de hacer nosotros las comprobaciones que Clipper hace en un 'wait 
state' y ejecutar una determinada funcin en caso de estar definida para la 
tecla que se haya pulsado: 

   #include "inkey.ch" 
 
   local nTecla, lTerminar := .f.
   local bBlock 
 
   do while .not. lTerminar 
 
      nTecla = InKey( 0 ) 
 
      if ( bBlock := SetKey( nTecla ) ) != nil 
         Eval( bBlock ) 
      endif 
 
      do case 
         case nTecla == K_ESC 
              lTerminar = .t.
 
         case nTecla == K_F1 
              ...
 
         case nTecla == K_F2 
              ...
      endcase 
   enddo 
 
Ya tenemos el mismo comportamiento de  un  estado  de  espera  de  Clipper. 
Podramos escribir  una  funcin  que  hiciese  todo  eso  y  que  de  paso 
suministrase los mismos parmetros que Clipper suministra: 

function nWaitKey( nTiempo ) 
 
   local nTecla  := InKey( nTiempo ) 
   local bAccion := SetKey( nTecla ) 
 
   if bAccion != nil 
      Eval( bAccion, ProcName( 1 ), ProcLine( 1 ), ReadVar() ) 
   endif 
 
return nTecla 

 
Con  estas  ideas  ya  estamos  preparados para disear un nuevo sistema de 
control de teclas activas. Veamos el cdigo: 
 
// Keys.prg 
 
#define KEY_nCODE   1 
#define KEY_bACTION 2 
 
static aKeys := {}, aKeyStack := {} 
 
//----------------------------------------------------------------------------// 
 
function KeySet( nKey, bAction ) 
 
   local nAt := AScan( aKeys, { | aKey | aKey[ KEY_nCODE ] == nKey } ) 
 
   if nAt == 0 
      AAdd( aKeys, { nKey, bAction } ) 
   else 
      aKeys[ nAt ][ KEY_bACTION ] = bAction 
   endif 
 
return 
 
//----------------------------------------------------------------------------// 
 
function bKeyGet( nKey ) 
 
   local nAt := AScan( aKeys, { | aKey | aKey[ KEY_nCODE ] == nKey } ) 
 
return If( nAt == 0, nil, aKeys[ nAt ][ KEY_bACTION ] ) 
 
//----------------------------------------------------------------------------// 
 
function ClearKeys() 
 
   aKeys = {} 
 
return 
 
//----------------------------------------------------------------------------// 
 
function PushKeys() 
 
   AAdd( aKeyStack, AClone( aKeys ) ) 
 
return 
 
//----------------------------------------------------------------------------// 
 
function PopKeys() 
 
   if Len( aKeyStack ) > 0 
      aKeys = ATail( aKeyStack ) 
      ASize( aKeyStack, Len( aKeyStack ) - 1 ) 
   endif 
 
return 
 
//----------------------------------------------------------------------------// 
 
function aGetKeys() 
 
return aKeys 
 
//----------------------------------------------------------------------------// 
 

Este es un ejemplo tpico de uso de variables static. Las variables  static 
se  comportan  cmo  variables  public  con  la  diferencia  que  slo  son 
accesibles  desde  dentro  del  PRG  en  donde se definieron. Las variables 
static mantienen su valor an  cuando  salgamos  del  PRG  en  donde  estn 
definidas.  En  el  lenguaje C existen desde hace mucho. Lo fundamental que 
hay  que  comprender de una variable static es que se crea en el heap (zona 
de memoria global-permanente de trabajo del sistema) y no en el stack (zona 
de memoria local-temporal de trabajo del sistema). Y adems su smbolo slo 
es accesible desde dentro del PRG en donde se define, aunque si es  posible 
establecer determinadas funciones que podran facilitar la direccin de ese 
smbolo  a  otros  PRGs. Cmo ejemplo de esto ltimo he definido la funcin 
aGetKeys() que devolver aKeys a otros PRGs! 
 
Ahora,  en  vez  de  SetKey(),  utilizaremos  KeySet()  y  bKeyGet().  Cmo 
almacenamos los arrays { nTecla, bAccion } en un array, tenemos garantizado 
que podremos guardar tantas teclas cmo queramos hasta un  mximo  de  4096 
(que es el nmero mximo de elementos de un array en Clipper).
 
Si observas el  cdigo  fuente  comprobars  que  ahora  la  ejecucin  del 
programa aumenta en mucho, ya que manejamos todo el array de una vez  y  no 
elemento a elemento. Por ltimo, podemos definir  unos  nuevos  UDC  (User  
Defined  Commands = Comandos definidos por el usuario) para facilitarnos en 
mucho la programacin de estas teclas activas. !Adems, el sistema  que  te 
propongo es compatible con la sintaxis de FoxPro!. 
 
// Keys.ch 
 
#xcommand SET KEY <nKey> TO [<Action>] => KeySet( <nKey>, [{||<Action>}] ) 
 
#xcommand PUSH KEYS       => PushKeys() 
#xcommand PUSH KEYS CLEAR => PushKeys(); ClearKeys() 
#xcommand POP KEYS        => PopKeys() 
 
La sintaxis de estos nuevos comandos est ideada para que puedas especificar 
parmetros al indicar la funcin a ejecutar: 
 
Un ejemplo tpico de uso sera: 
 
#include "Keys.ch" 
 
   SET KEY K_F2 TO Calculadora( @nResultado )  // Observa que es posible 
                                               // utilizar variables por 
                                               // referencia. Te ser muy 
                                               // til para modificar 
                                               // variables locales 
   ...
 
   PUSH KEYS CLEAR 
 
   Calendario() 
 
   POP KEYS 
 
Practica con todo esto que te indico y estoy seguro  que  se  te  ocurrirn 
algunas ideas ms la prxima vez que quieras indicarle a  tus  usarios  que 
"pulsen una tecla".
 
Happy Clipping!   :-)



LA CLASE ERROR 
 
La gestin y control de errores de Clipper, es un tema  que  normalmente  es 
ignorado  por  los  programadores,  ya  que  puede llegar a resultar un poco 
engorroso su tratamiento. En este artculo, se pretende ver que gracias a la 
Clase ERROR que incorpora Clipper, el control de los errores puede  resultar 
bastante sencillo y de la misma forma eficaz.
 
Al  igual  que en la anterior versin de Clipper, la Summer '87, exista un 
control  de errores, en Clipper 5.1 pasa algo semejante, aunque la forma de 
realizarlo  es  distinta.  En  la  primera, cuando se produca un error, se 
invocaban  una  serie  de funciones que eran las encargadas de realizar las 
tareas  oportunas.  En  cambio, en Clipper 5.1, cuando se produce un error, 
automticamente se genera un objeto, y este ser la base  de  la  operativa 
del error.
 
Cuando se genera el objeto error, Clipper a su  vez,  hace  una  llamada  a 
ERRORSYS, en el cual se ejecuta la funcin ERRORBLOCK().
 
Atenindonos   al   control  de  errores  que  Clipper  trae  por  defecto, 
ERRORBLOCK() evaluar un codeblock sobre la funcin DEFERROR(), a  la  cual 
se le pasa como parmetro el objeto error generado.
 
No obstante, esto puede sufrir ciertas transformaciones, ya que  cuando  se 
produce  un  error, se evala el codeBlock definido en ERRORBLOCK(), con lo 
cual,  podemos  hacer  que  el codeblock que por defecto trae ERRORSYS, sea 
modificado. Para explicar mejor esto veamos un ejemplo: 

   FUNCTION MAIN() 
      LOCAL nA := 12, cB := "pepe" 
      CLS 
      ? nA + cB 
      INKEY( 0 ) 
   RETURN NIL 
 
Si ejecutamos estas sentencias, la suma de las variables nA y cB, al ser de 
diferente tipo, dar lugar a un error, visualizndose en pantalla: 
 
              Ŀ 
                 Error BASE/1081  Argument error: +    
                                                       
                                Quit                   
               
 
siendo   este   mensaje  el  que  devuelve  Clipper  por  defecto,  ya  que 
ERRORBLOCK() realiza una llamada a la funcin DefError() donde es tratado.
 
Ahora bien, si hacemos la siguiente modificacin en las rutinas anteriores: 
 
   FUNCTION MAIN() 
      LOCAL nA := 12, cB := "pepe" 
      CLS 
      ERRORBLOCK( {|| DISPBOX( 05,10,07,27 )      ,; 
                      SETPOS( 06,12 )             ,; 
                      DISPOUT('Error al canto')   ,; 
                      __QUIT()}) 
      ? nA + cB 
      INKEY( 0 ) 
   RETURN NIL 
 
en  pantalla,  ya  no  aparecer  la  caja  anterior, sino que aparecer el 
mensaje 'Error al canto' y finalizar nuestra aplicacin.
 
Como  vemos  en  estos ejemplos, podemos manejar los errores de Clipper con 
independencia  del  manejador  que trae por defecto, ya que todo depende en 
cierta medida de la funcin ERRORBLOCK(), y del CodeBlock que  se  le  pasa 
como parmetro.
 
Pero el problema que se nos puede plantear, es  que  solamente  sea  en  un 
lugar  de  la  aplicacin  donde  el  control  de  errores queremos que sea 
realizado  por  nosotros, y dejar, una vez pasado este punto, el control de 
los mismos al manejador estndar de Clipper. Esto no tiene ningn problema. 
ERRORBLOCK() devuelve el CodeBlock que estaba  definido  anteriormente,  de 
tal forma que si hacemos lo siguiente: 
 
    FUNCTION MAIN() 
       LOCAL nA := 12, cB := "pepe" 
       LOCAL AntErr := ERRORBLOCK( {|| DISPBOX( 05,10,07,22 ),; 
                                       SETPOS( 06,12 )       ,; 
                                       DISPOUT( 'Se acabo' ) } ) 
       CLS 
 
       ? 'Primer error -> Controlado por nosotros' 
       ? nA + cB 
 
       ? 'Pulsa una tecla.......';INKEY(0) 
 
       ERRORBLOCK( AntErr )  // Queda activo el manejador de 
                             // errores de Clipper 
 
       ? 'Segundo error -> Controlado por Clipper' 
       ? cB + nA 
    RETURN NIL 
 
vemos  que  el primer error (nA+cB) ser controlado por el primer CodeBlock 
que  tiene  ERRORBLOCK,  y  el segundo (cB+nA) ser ya gestionado por el de 
Clipper.
 
Con  lo  visto  hasta  ahora,  podemos  ver  que no estamos tan atados a la 
gestin de errores de Clipper. No obstante, veamos  como  gestiona  Clipper 
este objeto error que se produce.
 
En  primer  lugar,  diremos  que  no es un objeto propiamente dicho, ya que 
carece de mtodos, y de otras propiedades de los objetos. En cambio, lo que 
si posee son una serie de variables de instancia que son exportadas  cuando 
se produce el error.
 
Estas  variables,  contienen  toda  la  informacin   necesaria   para   la 
asimilacin del error y su posible recuperacin. Al final de este  artculo 
podris ver una lista y descripcin de cada una de ellas.
 
Mas adelante, veremos como  podemos  visualizar  los  distintos  datos  que 
contienen  estas  variables  desde  el  propio ERRORSYS o de la funcin que 
nosotros nos creemos para este control de errores.
 
 
FICHEROS QUE INTERVIENEN EN EL CONTROL DE ERRORES DE CLIPPER 
 
Fichero ERROR.CH 
 
Antes  de  ver  como  podemos  crear  nuestro  propio manejador de errores, 
debemos conocer todos los aspectos relacionados con el mismo.
 
En  el directorio INCLUDE, podemos ver que existe un fichero llamado ERROR.
CH, el cual esta estrechamente relacionado con la Clase Error.
 
Este fichero contiene una  serie  de  constantes  manifiestas,  las  cuales 
determina  los  niveles  de  severidad  para  la  variable e:SEVERITY y los 
cdigos de errores genricos para la variable e:GENCODE.
 
 
Fichero ERRORSYS.PRG 
 
Como ya hemos dicho anteriormente, cuando se produce un error, se genera un 
objeto error, y se llama a la funcin ERRORSYS() ( el cdigo fuente de esta 
funcin  podrs  encontrarlo  en  CLIPPER5\SOURCE\SYS  ).  Clipper de forma 
automtica, llama a ERRORSYS.PRG,  el  cual  se  encuentra  dentro  de  sus 
propias  libreras.  No  obstante,  deja  el  fichero fuente a fin de poder 
realizar  las  modificaciones  oportunas.  Este  deber  de ser compilado y 
enlazado con nuestras aplicaciones si sufre algn tipo de modificacin.
 
Est funcin lo nico que realiza es ERRORBLOCK() sobre un CodeBlock.
 
   proc ErrorSys() 
        ErrorBlock( {|e| DefError(e)} ) 
   return 
 
El  parmetro  que  se  le  pasa a la funcin DefError() es el objeto error 
generado.
 
Si nos atenemos  a  la  funcin  DefError(),  podemos  ver  los  siguientes 
controles: 
 
-  Posibles errores producidos de dividir un nmero por cero. En este caso, 
   no se produce error, ya que se devuelve 0.
 
- Control de la apertura e insercin de registros en red. Con esto  podemos 
  ver como se pueden realizar todas las operaciones de red  sin  tener  que 
  acudir a funciones externas.
 
- Visualizacin del mensaje de error  mediante  la  llamada  a  la  funcin 
  ErrorMessage().  En  realidad,  esta  funcin  generar  el  literal  que 
  aparecer en la pantalla.
 
- Eleccin de posibles caminos de  salida  del  error  producido.  Esto  se 
  realiza con la funcin ALERT(), y las variables de instancia e:canRetry y 
  e:canDefault.
 
- Visualizacin de los procedimientos de la aplicacin  que  se  han  visto 
  afectados por el error.
 
A groso modo, esto es lo que realiza la funcin  DefError().  No  obstante, 
veremos ms adelante, como modificando y aadiendo  nuevas  lneas  a  este 
cdigo, podemos hacer que los errores queden un poco ms personalizados.
 
 
MANDATOS Y FUNCIONES 
 
Alrededor de la gestin y control de errores en Clipper 5 existen una serie 
de mandatos y funciones que son necesarias para que este control sea lo ms 
ptimo posible.
 
 
BEGIN SEQUENCE 
 
Define una estructura de control diseada para el tratamiento y control  de 
errores.
 
La sintaxis es: 
 
    BEGIN SEQUENCE 
          < instrucciones > 
          [ BREAK [ <expr> ] ] 
          < instrucciones > 
          [ RECOVER [ USING < vMem > ] 
          < inatrucciones > 
    END SEQUENCE 
 
Aunque este mandato no sea muy utilizado,  nos  puede  aportar  gran  ayuda 
cuando estemos intentando controlar posibles errores.
 
Cuando en un bloque de cdigo, encerrado entre BEGIN/END SEQUENCE, el flujo 
del programa se encuentra una orden BREAK (no importa el nivel en el que se 
encuentre),  ste  se direcciona hasta la siguiente sentencia RECOVER, o en 
su defecto, a la siguiente sentencia a END.
 
Si  observamos  el cdigo fuente de DefError(), veremos que cuando se estn 
generando las distintas opciones de salida, interviene la  clusula  BREAK.
Esta, no tiene ningn sentido si anteriormente no existe una estructura del 
tipo BEGIN/END SEQUENCE.
 
Para explicar mejor la utilizacin  de  este  mandato  en  el  contexto  de 
errores observemos el siguiente ejemplo: 
 
   FUNCTION MAIN() 
      LOCAL nVar1 := 0, cVar2 := '', valor 
      LOCAL AntErr := ERRORBLOCK({|| QOUT('NO PUEDO REALIZAR OPERACION'),; 
                                     INKEY(0)                           ,; 
                                     BREAK(AntErr) } ) 
      CLS 
 
      BEGIN SEQUENCE 
            @ 10,12 SAY 'Visualizacin de Suma -> nVar1 + cVar2' 
            @ 12,12 SAY nVar1 + cVar2 
      RECOVER USING valor 
            ERRORBLOCK( valor ) 
            @ 12,12 SAY 'No pude visualizar Suma, no existe' 
      END SEQUENCE 
 
      @ 12,12 SAY nVar1 + cVar2 
   RETURN NIL 
 
Bien, vamos a intentar explicarlo. Lo primero  que  hacemos,  es  crear  un 
CodeBlock con un mensaje informativo, que se visualizar en pantalla cuando 
se produzca un error, esperando la pulsacin de una tecla, y realizando una 
llamada  a  BREAK.  Aqu,  observamos  como  BREAK() pasa cmo parmetro el 
CodeBlock anterior que haba por defecto para el control de errores.
 
Haciendo  uso  de  BEGIN/END,  escribimos  una  serie  de  sentencias, y si 
observamos  la segunda, veremos que se producir un error, ya que los tipos 
son distintos.

Pues  bien,  al  llegar el flujo del programa a este punto, y producirse el 
error, en pantalla se  visualizar  'NO  PUEDO  REALIZAR  OPERACION'  y  se 
ejecutar la funcin BREAK() ( semejante a BREAK ), pasando el flujo  a  la 
sentencia  siguiente  a  RECOVER. Vemos aqu como, RECOVER USING, recibe el 
parmetro  pasado  por  BREAK(),  y  este  no  es otro que el CodeBlock del 
control  de  errores  por  defecto,  el  cual  podremos  ver,  en la ltima 
operacin fallida de esta rutina.
 
Este ejemplo demuestra como BEGIN/END pueden desviar el flujo del  programa 
dependiendo del error producido.
 
Ahora  bien,  utilizando ERRORSYS, podemos ver tambin como puede funcionar 
este mandato. Para ello, supongamos que vamos  a  realizar  un  proceso  de 
impresin,  y  que  la impresora no se encuentra preparada. Normalmente, se 
abortara el programa y a "otra cosa mariposa". Pues bien, esto  se  podra 
controlar, haciendo las siguientes modificaciones: 
 
    - En la funcin DefError() de ERRORSYS.PRG, cambiar 
 
        // build options array 
        // aOptions := {"Break", "Quit"} 
        aOptions := {"Quit"} 
 
      por 
 
        // build options array 
        aOptions := {"Break", "Quit"} 
        //aOptions := {"Quit"} 
 
    - En nuestras rutinas 
 
        FUNCTION MAIN() 
           BEGIN SEQUENCE 
                 Imprime() 
           RECOVER 
                 Set( _SET_DEVICE, "SCREEN" ) 
                 ? "Sentimos no poder imprimir" 
           END SEQUENCE 
           @ 23,00 SAY "FIN DEL PROGRAMA" 
        RETURN NIL() 
 
        FUNCTION Imprime() 
           Set( _SET_DEVICE, "PRINTER" ) 
           @ 12,00 SAY "Imprimir: -> Prueba" 
           __EJECT() 
           Set( _SET_DEVICE, "SCREEN" ) 
        RETURN NIL 
 
El tema est claro. Al intentar realizar IMPRIME(), y no estar la impresora 
preparada,  saldra  una ventana (ALERT()) con tres opciones, siendo una de 
ellas BREAK. Si lo que deseamos es continuar con la ejecucin del programa, 
valdra  con  pulsar  esta  opcin,  no  realizndose  la  impresin   pero 
continuando la ejecucin  del  programa.  (  En  lugar  de  'Sentimos.....' 
podramos  crear,  por ejemplo, una funcin que desviara la impresin hacia 
un fichero .PRN y ms tarde imprimirlo ) 
 
Creo,  que este mandato puede ser el ms importante en cuanto al control de 
errores,  y  es conveniente probarlo y trabajar con el para sacarle todo el 
partido.
 
 
DOSERROR() 
 
Esta funcin, determina el nmero de error del DOS que se ha producido.
 
Si  deseamos  que  en nuestro control de errores, se visualice el error del 
DOS que se ha producido podemos hacer lo siguiente: 
 
    nErrDos := DOSERROR() 
 
    IF nErrDos > 0 
        // Tabla de errores del dos 
       aErrDos := { 'Nmero de Funcin no es vlido',; 
                    'Fichero no encontrado'         ,; 
                     ..
                     .. y a si hasta los 88 
                     ..
                    'Cdigo de error del DOS no definido'; 
                   } 
        ? 'Error del DOS -> ', e:osCode, ' ', ; 
         IIF( nErrDos > 88 .OR. aErrDos[ nErrDos ] == NIL,; 
              aErrDos[ LEN( aErrDos ) ], aErrDos[ nErrDos ]  ) 
    END IF 
 
En  este contexto la variable e:osCode, realiza la misma operacin que esta 
funcin.
 
Si DOSERROR() devuelve un valor igual a 0,  esto  significa  que  el  error 
producido no es debido al DOS, sino al propio Clipper, es decir  no  existe 
ningn error del DOS asociado.
 
( Si deseas ver la lista de errores completa del DOS, la encontrars en uno 
de  los  apndices  del libro Programacin y Utilidades de los manuales del 
producto ) 
 
 
ERRORLEVEL() 
 
Devuelve  el  actual  nivel  de  error  del  DOS.  Si  un programa finaliza 
correctamente esta funcin es igual a 0, en caso contrario devuelve 1.
 
A esta funcin se le puede pasar un nmero ( comprendido entre 0 y 255 ), y 
podr visualizarse desde el DOS.
 
 
FERROR() 
 
Determina cuando se produce un error al estar manejando un fichero del DOS.
 
FERROR() devuelve el nmero de error del DOS producido. Si tiene  un  valor 
igual a 0, es que todo ha ido correctamente.
 
 
NETERR() 
 
Esta  funcin  determina  si en un entorno de trabajo multiusuario, se est 
produciendo  algn  tipo  de  error  al  intentar acceder a los datos, bien 
intentado usar ficheros .DBF o registros.
 
Si observamos el cdigo fuente de ERRORSYS.PRG existen dos  controles  para 
las  operaciones  en  red.  Una  es la apertura de ficheros y la otra la de 
aadir nuevos registros. En  ambos,  si  se  produce  un  error,  e:genCode 
devuelve  el  nmero  del  error  de  Clipper,  y  NETERR()  asume un valor 
verdadero, es decir se detecta un fallo en la operacin realizada.
 
 
 
NUESTRO PROPIO ERRORSYS 
 
Una vez visto como podemos realizar el control de errores, vamos a crearnos 
nuestra propia funcin.
 
Aqu podemos hacer dos cosas, o bien modificar el  fichero  ERRORSYS.PRG  o 
bien crearnos nuestro propio fichero. En esta ocasin, vamos a realizar las 
modificaciones sobre el fichero ya existente.
 
En  primer  lugar,  hay  que tener en cuenta que crear un ERRORSYS estndar 
puede   resultar  un  tanto  engorroso,  ya  que  depender  del  mbito  y 
funcionalidad para la que estn creadas nuestras aplicaciones. Intentaremos 
hacer las modificaciones ms globales.
 
- Control de errores del propio ERRORSYS 
 
Ahora que vamos a modificar y aadir sentencias  a  este  fichero,  debemos 
tener  en  cuenta  que  alguna  puede dar lugar a error, y por lo tanto, la 
aplicacin entrara en un bucle,  ya  que  desde  dentro  de  ERRORSYS,  se 
llamara  a  l mismo.  Es  decir,  debemos   de   controlar   la   posible 
recursividad, ya que de lo contrario nos aparecera el mensaje: 
 
     'DEFERROR(0) Unrecoverable error 650: Processor stack fault' 
 
Para  ello,  deberemos  colocar  la  siguiente sentencia al principio de la 
funcin DefError(): 
 
     LOCAL nNivel := 0 
     LOCAL nProc  := 1 
 
     WHILE !EMPTY( PROCNAME( nProc ) ) 
           IF PROCNAME( nProc ) == 'DEFERROR'; nNivel++; END IF 
           nProc++ 
     END 
 
     IF nNivel > 4 
        ? 'Se esta produciendo un error en el propio ERRORSYS' 
        ? 'Procedimiento -> ', PROCNAME( 0 ) 
        ERRORLEVEL( 1 ) 
        QUIT 
     END IF 
 
Con estas sentencias lo que conseguimos es que si el error es generado  por 
el propio ERRORSYS(), no se produzca un desbordamiento de la pila, y a la 5 
vez, automticamente salga de la aplicacin indicndolo.
 
 
- Control de Impresora 
 
Controlaremos  los  fallos  posibles  que  puedan  suceder en la impresora, 
determinando el nmero de reintentos a 10.
 
Para ello colocaremos la variable NNUMERO al comienzo de ERRORSYS.
 
    STATIC nNumero := 0 
 
    proc ErrorSys() 
         ErrorBlock( {|e| DefError(e)} ) 
    return 
 
Antes  de  las  sentencias que controlan la divisin por cero aadiremos el 
siguiente cdigo: 
 
   // Control de Impresora 
   IF e:genCode == EG_PRINT .AND. e:canRetry .AND. nNumero++ <= 10 
      RETURN CtlImpre() 
   END IF 
 
y al final de ERRORSYS, aadiremos la siguiente funcin: 
 
   FUNCTION CtlImpre() 
      LOCAL nOpc := 0 
      LOCAL lValor := .F.
      LOCAL cFichero, nManip, nRow := ROW(), nCol := COL() 
 
      Set( _SET_DEVICE, "SCREEN" ) 
      nOpc := ALERT( "IMPRESORA NO ESTA EN LINEA", ; 
                     {"Reintentar","Cancelar Impresin","A fichero"} ) 
      DO CASE 
         CASE nOpc == 1 
              lValor := .T.
 
         CASE nOpc == 2 .OR. nOpc == 0 
              QUIT  // o BREAK 
 
         CASE nOpc == 3 
              cFichero := 'PRN' + SUBSTR( TIME(), 1, 2 ) +; 
                                  SUBSTR( TIME(), 4, 2 ) + '.PRN' 
              nManip   := FCREATE( cFichero ) 
              FCLOSE( nManip ) 
 
              SET( _SET_PRINTFILE, cFichero ) 
              lValor := .T.
 
             // ver 
      END CASE 
      SETPOS( nRow, nCol ) 
      SET( _SET_DEVICE, "PRINT" ) 
   RETURN lValor 
 
Cuando  se  produce  un  error de impresora, en pantalla se visualizar una 
caja ALERT() indicando tres opciones diferentes: 
 
  - 1 Opcin. Si intentamos reintentar y la impresora no tiene ningn tipo 
               de  problema,  continuar  la  impresin sin ms. Pero si la 
               impresora  continua  mal,  volver  a  visualizarse la misma 
               caja, y as hasta un nmero de 10 veces, acabadas las cuales 
               la ejecucin del programa se finalizar.
  
  - 2 Opcin. Se  cancelar  la  impresin.  Si  utilizamos en el programa 
               principal  la  estructura  de  control  BEGIN/END  SEQUENCE, 
               cambiaramos QUIT por BREAK.
 
  - 3 Opcin. Desviar la salida hacia un fichero. Si la impresin no tiene 
               xito,  y  elegimos esta opcin, se generar automticamente 
               un fichero y hacia el se desviar toda la impresin.
 

- Crear fichero de LOG 
 
Con  este  fichero,  se  pretende que todos los posibles errores que puedan 
surgir en nuestras aplicaciones queden almacenados, pudiendo de esta  forma 
reciclarlas.
 
El fichero en cuestin puede ser bien un fichero .DBF o un fichero de texto.
En este caso, vamos a crear un fichero .DBF.
 
Debemos tener en cuenta que la informacin que se desea  almacenar  sea  lo 
suficientemente  inteligible, es decir, que a la hora de comprobar el error 
no falte ningn tipo de dato.
 
El fichero que contendr esta informacin ser ERROR.LOG. La estructura del 
mismo es: 

            FECHA       D      8 
            HORA        C      8 
            DESCRIPCIO  C     30 
            PROC1       C     15 
            PROC2       C     15 
            PROC3       C     15 
 
En los campos PROC1, PROC2, PROC3, se guardar el nombre del  procedimiento 
en el que se produce el error en orden creciente. Si la aplicacin  realiza 
muchas  llamadas  a  procedimientos,  puede  ser  conveniente aadir a esta 
estructura mas campos, es decir, PROC4, PROC5, etc.
 
Tambin podramos crear una estructura semejante, pero en lugar de utilizar 
los  campos  para  el nombre de los procedimientos, sera crear un campo de 
tipo memo, donde almacenaramos el nombre de los mismos. He optado  por  la 
primera  solucin,  ya que de lo contrario, tendramos un fichero .DBT y se 
hara un poco ms complicado su tratamiento.
 
Veamos como podra quedar el cdigo fuente: 
 
    // Fichero de Log 
    // Si no existe se crea 
    IF !FILE( 'ERROR.LOG' ) 
       DBCREATE( 'ERROR.LOG',; 
                 {                         ; 
                   {'FECHA'     ,'D', 8,0},; 
                   {'HORA'      ,'C', 8,0},; 
                   {'DESCRIPCIO','C',50,0},; 
                   {'PROC1'     ,'C',15,0},; 
                   {'PROC2'     ,'C',15,0},; 
                   {'PROC3'     ,'C',15,0} ; 
                 }                         ; 
               ) 
    END IF 
 
    DBUSEAREA( .T.,,'ERROR.LOG') 
    ERROR->( DBAPPEND(0) ) 
    ERROR->FECHA      := DATE() 
    ERROR->HORA       := TIME() 
    ERROR->DESCRIPCIO := e:description + " (" + NTRIM(e:osCode) + ")" 
    ERROR->PROC1      := PROCNAME( 2 ) + " Lin. " + ; 
                         ALLTRIM( STR( PROCLINE( 2 ),6 ) ) 
    ERROR->PROC2      := IIF( !EMPTY( PROCNAME( 3 ) ),   ; 
                              PROCNAME( 3 ) + " Lin. " + ; 
                              ALLTRIM( STR( PROCLINE( 3 ),6 ) ), "" ) 
    ERROR->PROC3      := IIF( !EMPTY( PROCNAME( 4 ) ),   ; 
                              PROCNAME( 4 ) + " Lin. " + ; 
                              ALLTRIM( STR( PROCLINE( 4 ),6 ) ), "" ) 
    ERROR->(DBCLOSEAREA()) 
 
Estas sentencias, insertadas al comienzo  de  la  funcin  DefError(),  nos 
permitirn  saber  que  pas  realmente.  Si necesitsemos ms informacin, 
podramos aadir nuevos campos, incluso salvar la pantalla  en  la  que  se 
produjo  el  error  o  todos los valores que podran tener las variables de 
este objeto.
 
Si deseamos que la informacin del error no salga ya por pantalla,  bastar 
con eliminar todas las salidas ? o ?? que existan en la funcin DefError().
 
 
- Mensajes en Castellano ( u otro idioma ) 
 
La  cuestin  est  en  generar  una  tabla  que  cambie  el   literal   de 
visualizacin en la funcin ERRORMESSAGE(). Es decir, debemos de crear  una 
tabla de la siguiente forma: 
 
   static func ErrorMessage( e ) 
   local cMessage 
   local aTexto := {                                       ; 
                     'Error en parmetros'                ,; 
                     'Error de lmites'                   ,; 
                     'Desbordamiento de cadena'           ,; 
                     'Desbordamiento numrico'            ,; 
                     'Divisin por 0'                     ,; 
                     'Error numrico'                     ,; 
                     'Error de sintaxis'                  ,; 
                     'Operacin demasiado compleja'       ,; 
                     ''                                   ,; 
                     ''                                   ,; 
                     'No hay memoria'                     ,; 
                     'Funcin sin definir'                ,; 
                     'Mensaje sin definir'                ,; 
                     'La variable no existe'              ,; 
                     'No existe el alias'                 ,; 
                     ''                                   ,; 
                     ''                                   ,; 
                     ''                                   ,; 
                     ''                                   ,; 
                     'Error de creacin'                  ,; 
                     'Error de apertura'                  ,; 
                     'Error al cerrar'                    ,; 
                     'Error al leer'                      ,; 
                     'Error al escribir'                  ,; 
                     'Error al imprimir'                  ,; 
                     ''                                   ,; 
                     ''                                   ,; 
                     ''                                   ,; 
                     ''                                   ,; 
                     'Operacin no soportada'             ,; 
                     'Lmite excedido'                    ,; 
                     'Corrupcin detectada'               ,; 
                     'Error en el tipo de dato'           ,; 
                     'Error en la longitud de los datos'  ,; 
                     'El rea de trabajo no est en uso'  ,; 
                     'El rea de trabajo no est indexada',; 
                     'Se requiere exclusividad'           ,; 
                     'Se requiere bloqueo'                ,; 
                     'Escritura no permitida'              ; 
                   } 
 
y en la sentencia 
 
     // add error description if available 
     if ( ValType(e:description) == 'C' ) 
            cMessage += ('  ' + aTexto[e:genCode] ) 
     end 
 
visualizamos la posicin de la tabla que corresponde con el nmero de error 
producido.
 
Al final podris encontrar la traduccin de estos mensajes a varios idiomas.
 
 
 
CONCLUSION 
 
Como  hemos podido observar, el control de errores de Clipper 5, permite un 
mayor tratamiento que en Summer '87.
 
Aunque hemos trabajado con la propia  funcin  ERRORSYS,  podemos  crearnos 
nuestra  propia  funcin  y  en  ella  escribir  todas  las  modificaciones 
anteriormente reseadas, e incluir al principio de nuestras aplicaciones lo 
siguiente: 
 
         ERRORBLOCK( {|oError| MiError( oError ) } ) 
 
Solamente,  hemos modificado algunos aspectos del ERRORSYS. No obstante, la 
cantidad de modificaciones y de nuevas operaciones  que  podemos  realizar, 
dependern  en  cierta medida de cada uno. El camino ya esta abierto, ahora 
lo nico que queda es lo de siempre, probar y programar.
 
 
 
VARIABLES DE INSTANCIA DE LA CLASE ERROR 
 
Estas variables son: 
( antepuesto al nombre  de  cada  variable  va  el  nombre  del  objeto  en 
cuestin,  en este caso es 'e' por seguir el mismo criterio que el ERRORSYS 
de Clipper ) 
 
 
e:ARGS 
 
Es  una variable de tipo array que contiene los operadores de una operacin 
cuando ocurre un error de tipo de datos distintos, es decir, se intenta por 
ejemplo sumar valores numricos y caracteres.
 
Cuando  ocurre  otro tipo de error, la variable e:ARGS contiene un valor de 
tipo NIL.
 
Esta variable es asignable.
 
 
e:CANDEFAULT 
 
Es una variable de tipo lgico que indica si el subsistema  puede  efectuar 
el recobro del valor por omisin para dicha condicin de error.

Si el valor de dicha variable es verdadero (.T.), indica que el recobro  de 
errores implcito se encuentra disponible. Todo este manejo por  omisin  y 
el recobro, dependen de la condicin del error y del subsistema.
 
La accin mnima de esta omisin es simplemente ignorar el error.
 
El  valor  por  omisin  es  solicitado cuando se retorna valor falso (.F.) 
desde el ERRORSYS.
 
Esta variable esta estrechamente ligada a la variable  e:CANSUBSTITUTE,  ya 
que cuando esta  vale  verdadero  (.T.),  e:CANDEFAULT  nunca  podr  valer 
verdadero (.T.).
 
Esta variable es asignable.
 
 
e:CANRETRY

Esta  variable es de tipo lgico e indica al subsistema si la operacin que 
causo el error puede volver a intentarse.  Si  e:CANRETRY  tiene  un  valor 
verdadero (.T.), indica que la operacin se puede realizar  nuevamente.  En 
cambio, si tiene valor falso (.F.), un nuevo intento no estar  disponible, 
dependiendo del subsistema y de la condicin que determino el error.
 
El  nuevo  intento  se  solicitar  al  retornar desde el ERRORSYS un valor 
verdadero (.T.).
 
Esta variable esta estrechamente ligada a la variable  e:CANSUBSTITUTE,  ya 
que  cuando  esta  vale  verdadero  (.T.),  e:CANRETRY  nunca  podr  valer 
verdadero (.T.).
 
Esta variable es asignable.
 
 
e:CANSUBSTITUTE 
 
Esta  variable  es de tipo lgico que indica si se puede sustituir el valor 
del  resultado  de  la  operacin  que  produjo  el  error.  Los errores de 
argumentos   y  otra  serie  de  errores  simples,  permiten  al  manejador 
sustituirlos  por  un nuevo valor para la operacin en la que se produjo el 
error.
 
Un  valor  verdadero (.T.) de esta variable, indica que se puede proceder a 
la sustitucin del valor en cuestin.
 
La sustitucin se efecta al retornar el nuevo valor desde el ERRORSYS.
 
Como ya hemos dicho anteriormente, esta variable se encuentra estrechamente 
ligada a las variables e:CANDEFAULT y e:CANRETRY, ya que si alguna de estas 
dos  variables  es  verdadera  (.T.),  e:CANSUBSTITUTE,  nunca  podr valer 
verdadero (.T.).
 
Esta variable es asignable.
 
 
e:CARGO 
 
Esta  variable  puede  ser  de  cualquier  tipo,  ya que no es usada por el 
manejador  de  errores.  Es  definible  por  el  usuario  y  permite aadir 
informacin a un objeto error y recuperarla posteriormente.
 
Esta variable es asignable.
 
 
e:DESCRIPTION 
 
Es  una variable de tipo carcter que contiene la descripcin del error que 
se  ha  producido.  Si  esta  variable est vaca, significa que para dicho 
error no existe ningn tipo de descripcin.
 
Esta variable se encuentra estrechamente ligada a la variable e:GENCODE, ya 
que  si  esta  no tiene valor 0, siempre habr un valor para la variable e:
DESCRIPTION.
 
Esta variable es asignable.
 
 
e:FILENAME 
 
Es  una  variable de tipo carcter que contiene el nombre del fichero usado 
cuando  se produce el error. Si esta variable se encuentra vaca, significa 
que  no  existe  ningn  fichero  asociado al error, o que el subsistema no 
contiene la informacin sobre los nombres de los ficheros .DBF.
 
Esta variable es asignable.
 
 
e:GENCODE 
 
Es una variable de tipo numrico, que contiene  el  nmero  del  cdigo  de 
error genrico de Clipper. Esto errores genricos, permiten el  manejo  por 
defecto de errores similares provenientes de distintos subsistemas.
Si el valor de esta variable es 0, significa que el error no  es  genrico, 
sino que solamente es especfica del subsistema en cuestin.
 
Esta variable es asignable.
 
 
e:OPERATION 
 
Es  una  variable  de  tipo  carcter,  que  contiene  la descripcin de la 
operacin que se estaba efectuando cuando se produjo el  error.  Cuando  el 
error se produce en funciones  u  operadores,  esta  variable  contiene  el 
nombre del operador o de la funcin. Si el error se produce en una variable 
no definida, e:OPERATION contiene el nombre de dicha variable.
 
Si esta variable  se  encuentra  vaca,  significa  que  el  subsistema  no 
contiene ningn tipo  de  descripcin  que  pueda  ser  visualizada  de  la 
operacin o funcin.
 
Esta variable es asignable.
 
 
e:OSCODE 
 
Es una variable de tipo numrico, la cual contiene  un  nmero  entero  que 
corresponde con el cdigo de error del sistema operativo.
 
Si  e:OSCODE  tiene  un  valor  0,  significa que el error producido no fu 
debido  a  un  error  del sistema operativo. Del mismo modo, si e:OSCODE es 
distinto  de  0,  el  error  ha  sido  producido  por  un  error del DOS, y 
DOSERROR() se actualiza con el mismo valor.
 
Esta variable es asignable.
 
 
e:SEVERITY 
 
Es  una  variable de tipo numrico que contiene el valor de severidad de la 
condicin del error que se ha producido.
 
La variable e:SEVERITY, se encuentra representada por cuatro valores: 
 
     0     Indica que el error es meramente informativo.
     1     Indica que el error permite algunas operaciones, aunque 
           normalmente este valor repercute en errores mas graves.
     2     Indica que el error no permite mas operaciones de ningn tipo.
     3     Indica que el error es bastante serio, siendo necesaria la 
           conclusin inmediata de la aplicacin.
 
 
e:SUBCODE 
 
Es  una  variable  de  tipo  numrico  que  contiene  un  nmero entero que 
corresponde con el cdigo  de  error  especfico  al  subsistema.  Si  esta 
variable  tiene  un valor 0, indica que el subsistema no asigna ningn tipo 
de valor al error.
 
Esta variable es asignable.
 
 
e:SUBSYSTEM 
 
Es  una variable de tipo carcter que contiene el nombre del subsistema que 
produjo el error.
 
Si el error es por culpa de operadores o funciones bsicas de Clipper, esta 
variable es igual a "BASE". En cambio, si el error es generado por bases de 
datos, la variable contiene el nombre del driver de la base de datos.
 
Esta variable es asignable.
 
 
e:TRIES 
 
Es una variable de tipo numrico que determina el nmero de veces que se ha 
intentado realizar una  misma  operacin.  Si  e:CANRETRY  tiene  un  valor 
verdadero  (.T.)  y se intenta un nmero indeterminado de veces realizar la 
misma operacin, con e:TRIES, podemos limitar el nmero de reintentos.
 
Si  esta  variable tiene un valor igual a 0, significa que el subsistema no 
tiene constancia de las veces  que  se  ha  intentado  realizar  una  misma 
operacin fallida.
 
Esta variable es asignable.

GRUPO EIDOS. Oct '92.
