CursoClip7JSL
De Clip en Castellano
Contenido |
Curso Clip: lenguaje xBase para GNU/Linux
Séptimas Jornadas de Software Libre - Córdoba, Argentina
Autor: Gustavo A. Courault
Temario del curso:
- Instalación de Clip en Ubuntu/Debian
- Características y bondades de Clip
- Mi primer programa en Clip
- Observaciones y cuidados con las localización
- Migrar un programa escrito en CA-Clipper a Clip
- Usando Clip con MySQL como ejemplo de base de datos relacional
- Usar Clip para el desarrollo de aplicaciones gráficas con Glade y libglade.
Instalación de Clip
Primero debe descargar desde ITK la última versión de Clip que viene en un tarball del estilo:
clip-prg-1.2.0-1.tgz
Y lo descompactamos:
$ tar xzvf clip-prg-1.2.0-1.tgz
Para instalar Clip en Ubuntu Dapper, Edgy o Feisty Fawn se necesitan tener instaldos los siguiente paquetes, los cuales se pueden instalar mediate apt-get o synaptic:
- gcc
- make
- bison
- flex
- libc6-dev
- libncurses5-dev
Si uno desea además soporte para MySQL, PostrgreSQL, ODBC o Firebird necesita instalar los siguiente paquetes respectivamente:
- libmysqlclient15-dev
- libpq-dev
- libodbc2-dev
- firebird2-dev
Para que Clip tenga soporte gráfico, se deben instalar los siguientes paquetes:
- libgtk1.2-dev
- libglade2-dev
Para la instalación de estos paquete y otros podemos utilizar apt-get o synaptic. Estas instrucciones también funcionan para Debian Sarge y Etch.
Nos cambiamos al directorio que vamos a compilar:
$ cd clip-prg-1.2.0-1
Cambiamos la variable LANG a una que entienda Clip:
$ export LANG=es_ES
Compilamos para todo el sistema:
$ sudo make system
Como Clip no soporta UTF-8, para que se vean correctamente el set de caracteres en el ambiente X se puede usar el emulador de terminal rxvt:
$ sudo apt-get install rxvt
Una vez instalado Clip, podemos ver si funciona correctamente con el comando:
$ clip -V
Nos da el número de versión, la licencia GPL y termina.
Características y bonades de Clip
Breve Historia
Clip es un superset de CA-Clipper 5.3, el legendario compilador de xBase que hizo furor en los 80. La compañía que creó Clipper: Nantucket Corporation, vendió este compilador a Computer Associates (CA), empresa que iba a desarrollar el así llamado "Proyecto Aspen", una aproximación del mundo xBase a la programación bajo MS-Windows. Es así que nace el malogrado CA-Visual Objects, un fracaso desde todo punto de vista; dejando sin más desarrollo a Clipper, el cual detiene su desarrollo en la versión 5.3.
A esto se suma la compra por parte de Microsoft de FoxPro, en ese momento por la versión 2.5. Microsoft libera FoxPro 2.6 como última versión orientada a caracteres y con una adaptación a MS-Windows. Sin embargo deja de lado todos esos desarrollos en pos de Visual-Fox, el cual gracias a las quejas de los usuarios y su gran plataforma de desarrolladores no es dejado de lado por la versión 7.
Es así que Clipper y otros lenguajes como FoxPro quedan sin soporte ni continuidad, dejando huérfanos a una enorme cantidad de desarrolladores, bibliotecas y sistemas.
En los 90 y con el auge del software libre se gestan varios proyectos para suplir este vacío, Clip es uno de ellos.
Características
Clip tiene licencia GPL, es decir que a menos que compremos el compilador a la empresa ITK: http://www.itk.ru/english/index.shtml nuestro desarrollo deberá tener esa licencia.
Es multiplataforma debido a que usa GCC para generar los binarios. En estos momentos funciona muy bien con GNU/Linux pero no tan bien con MS-Windows debido a que se debe usar Cygwin. El desarrollo de Clip para Cygwin está detenido.
* Es orientado a objetos, a diferencia de Clipper uno puede crear sus propias clases. * Se conecta a bases de datos relacionales: PostgreSQL, MySQL, Firebird, Oracle y ODBC * Puede usar DBFCDX, DBFSIX, DBFNTX, DBFMDX * Tiene la tecnología de FoxPro: "rushmore" para las tablas DBF * Puede utilizar "sockets" TCP * Es muy rápido * Permite la internacionalización * Genera ejecutables muy pequeños. * Es fácil para migrar de Clipper a Clip * Soporta alguna extensiones FoxPro * Soporta sintaxis Foxplus * Mediante bibliotecas permite crear interfaces gráficas.
Actividad Nro 1: Descargar e instalar Clip en cada estación de trabajo
Mi primer programa Clip
Vayamos con el consabido "Hola Mundo"
¿Con qué editar? Clip no posee una IDE, de modo que hay que editar con un editor tal como vi, vim, emacs, kedit, nano, pico, gedit, etc.
/* Mi primer progama Clip hola.prg */ function main() clear // pone a Clip en modo "pantalla" ? "hola mundo" return NIL
Compilamos:
$ export LANG=es_ES #sino nos dice que no tiene el lenguaje utf-8 $ clip -e hola.prg
ejecutamos dentro de una terminal rxvt (de nuevo por las internacionalizaciones)
$ ./hola
Veamos un ejemplo con un menu usando la clase omenu:
/*
Demostración de un menu
menu.prg
*/
#include "button.ch" // Menu definitions
#include "inkey.ch" // Key definitions
local oInfo // Es el nombre del menu a crear
SET( _SET_EVENTMASK, INKEY_ALL ) //Para que funcionen en distintas terminales las tecla de funcion
msetcursor(.T.) // muestra el cursor
cls //Borra la pantalla
oInfo=MENU_Create() // Creamos el menu
DO WHILE MenuModal(oInfo,1,24,1,79,"r/w") <> 999 //Estamos en un lazo hasta que no salimos con 999
ENDDO
return(NIL)
// This function will create the menu and return the newly created
// MenuObject as its return value. This menu consists of three "Main Menu choices" ..
// File, Edit, and Options. Additionally, the Edit TopBarMenu item has a secondary menu for
// one of its menu items.
function MENU_Create()
local oTopBar, oPopUp, oPopUp1, oItem
// TopBar() creates the menu at the very top of the screen
oTopBar := TopBar( 0, 0, 79)
oTopBar:ColorSpec :="b/w,gr+/rb,r/w,g/rb,n+/w,w+/b"
// Create a new popup menu named FILE and add it to the TopBar object
oPopUp := PopUp()
oPopUp :ColorSpec:= "b/w,gr+/rb,r/w,g/rb,n+/w,w+/b"
oTopBar:AddItem( MenuItem ( "&File",oPopUp) )
// Add some menu items to the newly created File popup
oItem :=MenuItem( "&New" ,{|| MyCreateFile() }, K_CTRL_N,"Create a new file", 101)
oPopUp:AddItem( oItem)
oItem :=MenuItem( "&Open..." ,{|| MyOpenFile() }, K_CTRL_O,"Open a file")
oPopUp:AddItem( oItem)
oItem :=MenuItem( "&Save" ,{|| MySaveFile() }, K_CTRL_S,"Save a file")
// Disable this menu item
oItem:Enabled := .f.
oPopUp:AddItem( oItem)
// Add a separator
oPopUp:AddItem( MenuItem( MENU_SEPARATOR ) )
oItem :=MenuItem( "&Print..." ,{|| MyPrintFile() }, K_CTRL_P,"Print a file",HASH_Print)
// Disable this menu item
oItem:Enabled := .f.
oPopUp:AddItem( oItem)
// Another separator
oPopUp:AddItem( MenuItem( MENU_SEPARATOR ) )
oItem :=MenuItem( "E&xit" ,{|| .t. }, K_ALT_F4,"End of application", 999)
oPopUp:AddItem( oItem)
// Create a second popup menu named EDIT and attach it to oTopBar
oPopUp := PopUp()
oPopUp :ColorSpec:= "b/w,gr+/rb,r/w,g/rb,n+/w,w+/b"
oTopBar:AddItem( MenuItem ( "&Edit",oPopUp) )
// Add some menu items to this EDIT popup menu
oItem :=MenuItem( "&Undo" ,{|| MyUndo() }, K_CTRL_Z,"Reverse changes made to this file")
oPopUp:AddItem( oItem)
oItem :=MenuItem( "Cu&t" ,{|| MyCut() }, K_CTRL_X,"Cut to clipboard")
oPopUp:AddItem( oItem)
oItem :=MenuItem( "&Copy" ,{|| MyCopy() }, K_CTRL_C,"Copy to clipboard")
oPopUp:AddItem( oItem)
oItem :=MenuItem( "&Paste" ,{|| MyPaste() }, K_CTRL_V,"Paste from clipboard")
oPopUp:AddItem( oItem)
// Another separator
oPopUp:AddItem( MenuItem( MENU_SEPARATOR ) )
// Create a new popup menu on the EDIT popup menu named Go. (This is also
// known as a "Cascading" menu.)
oPopUp1 := PopUp()
oPopUp1 :ColorSpec:= "b/w,gr+/rb,r/w,g/rb,n+/w,w+/b"
oItem :=MenuItem( "&Go",oPopUp1 )
oPopup:AddItem( oItem )
// Add some items to the Go cascading menu
oItem :=MenuItem( "&Go To..." ,{|| MyGoToLine() }, K_F5,"Go to a specific line number")
oPopUp1:AddItem( oItem)
oItem :=MenuItem( "G&o To Top" ,{|| MyGoTop() }, K_CTRL_HOME,"Go to top of file")
oPopUp1:AddItem( oItem)
oItem :=MenuItem( "Go To &Bottom" ,{|| MyGoBottom() }, K_CTRL_END,"Go to bottom of file")
oPopUp1:AddItem( oItem)
// Add yet another separator
oPopUp:AddItem( MenuItem( MENU_SEPARATOR ) )
// Add one final menu item to the EDIT popup menu
oItem :=MenuItem( "&Find..." ,{|| MySearch() }, K_ALT_F3,"Search for text")
oPopUp:AddItem( oItem)
//Create a 3rd PopUpMenu called OPTIONS and attatch it to oTopBar
oPopUp := PopUp()
oPopUp :ColorSpec:= "b/w,gr+/rb,r/w,g/rb,n+/w,w+/b"
oTopBar:AddItem( MenuItem ( "&Options",oPopUp) )
oItem :=MenuItem( "AutoSave" ,{|| MyAutoSave() },,"Toggle auto save preference")
oItem:Checked := .t.
oPopUp:AddItem( oItem)
oItem :=MenuItem( "Tab Stops..." ,{|| MyTabStops() },,"Set number of spaces for tab stops")
oPopUp:AddItem( oItem)
// Return our TopBar object back to MENU_Test()
return ( oTopBar)
// The following are dummy functions which do not do anything
// These functions are here to avoid receiving Unresolved External
// Errors
FUNCTION MyCreateFile()
alert("create file")
RETURN NIL
FUNCTION MyOpenFile()
RETURN NIL
FUNCTION MySaveFile()
RETURN NIL
FUNCTION MyPrintFile()
RETURN NIL
FUNCTION MyUndo()
RETURN NIL
FUNCTION MyCut()
RETURN NIL
FUNCTION MyCopy()
RETURN NIL
FUNCTION MyPaste()
RETURN NIL
FUNCTION MyGoToLine()
RETURN NIL
FUNCTION MyGoTop()
RETURN NIL
FUNCTION MyGoBottom()
RETURN NIL
FUNCTION MySearch()
RETURN NIL
FUNCTION MyAutoSave()
RETURN NIL
FUNCTION MyTabStops()
RETURN NIL
Compilamos:
$ export LANG=es_ES ; clip -e menu.prg
ejecutamos:
$ rxvt -e ./menu
Observaciones y cuidados con la localización
Cuando se compila Clip, se crea una serie de archivos para administrar correctamente los distintos tipos de idiomas, terminales y conjunto de caracteres. No olvidemos que el Clip es de origen ruso, de modo que ellos sufren en carne propia los errores y omisiones en tal sentido. El manual de Clip tiene un extenso capítulo para explicar como crear las distintas mapeos de terminal y de teclado. Sin embargo tenemos la fortuna de que para el castellano dicho mapeo está disponible y funciona correctamente.
Para ello es indispensable darle a Clip el entorno adecuado al momento de compilar, eso se hace simplemente con la variable de entorno LANG:
$ export LANG=es_ES
Esto se debe hacer antes de compilar el Clip en sí mismo y cualquier programa modo caracter.
Si consulto la variable de entorno de un Ubuntu Dapper, por ejemplo:
$ echo LANG $ es_AR.UTF-8
El cual no concuerda con ninguno de los conjuntos de caracteres que tiene Clip preconfigurados.
Cuando configuramos correctamente el Clip, nos muestra los mensajes de error en castellano y muestra correctamente el conjunto de caracteres. Sin embargo no funciona correctamente en la gnome-terminal, ya que esta le dice al Clip que es una "xterm", la cual no tiene las características que necesita Clip para adminstrar la pantalla. Es por eso que es necesiario usar alguna otra terminal simple, la que mejor funciona por su facilidad de configuración es "rxvt".
Para invocar un programa modo caracter desde una terminal tal como la gnome-terminal, hacemos:
$ rxvt -e ./programa
Actividad 2: Ejecutar los programas anteriormente compilados en una gnome-terminal y en rxvt
Actividad 3: Hacer un acceso directo al escritorio
Migrar un programa CA-Clipper a Clip
Para migrrar un sistema Clipper a Clip es necesario seguir los siguientes pasos:
- Agregar al programa principal la sentencia "function main()"
- Renombrar el primer programa para que sea el primero en un "ls"
- Pasar todos los nombres de los archivos a minúsculas
- Fijarse en las incompatibilidades que nombra el manual de Clip: http://www.itk.ru/clip-doc.en/prb.html
- dar la siguiente orden para que tome bien el set de caracteres de MS-DOS:
$ export LANG=en_EN.CP437
Compilar con:
$ clip -e *.prg
Los fuentes, bases de datos y una mejor explicación lo pueden en AplicacionApadir
Actividad 4: Pasar a Clip una biblioteca (las superfunctions) y la aplicación "apadir"
Usando Clip con MySQL como ejemplo de base de datos relacional
Primera Parte Clip/MySQL - Nos conectamos a MySQL
Para ello escribimos las siguientes funciones que nos ocultan la complejidad y los detalles, esta serie de funciones las ponemos en un archivo llamado gcfunc.prg:
* --------------------------------------- * Archivo: gcfunc.prg * Función que se conecta a la base de datos MySQL * Parámetros: usuario y clave * Devuelve: el objeto de conexión * --------------------------------------- function conectargc( cUsuario, cPass ) local oConn local host := ip_mysql() // IP o nombre del host local port := NIL // uso el puerto por omisión local db := db_mysql() // me conecto a la base de datos local socket := NIL // uso el socket por omisión local flag := NIL // no se usa en MySQL // Conexión a la base de datos oConn := ConnectNew( "MS" , host , port , cUsuario , cPass , db , socket , flag ) return oConn
* ------------------- function ip_mysql() return "127.0.0.1"
* -------------------------------- function db_mysql() return "gc"
- Fin de gcfunc.prg
El objeto de las funciones ip_mysql() y db_mysql() es mantener cierta parametrización la posibilidad de generalización de "conectargc" y cambiar con facilidad el nombre del host y el nombre de la base de datos.
Algunos datos como para probar.
Suponemos que el usuario de la base de datos MySQL es "gc" con clave "clip". Invocamos el monitor mysql:
mysql -h 127.0.0.1 -u root -p
Creamos la base de datos:
create gc; use gc;
Utilizaremos la siguiente secuencia de comandos para crear y poblar nuestra base de datos:
CREATE TABLE `provincia` ( id_provincia` int(3) unsigned NOT NULL default '0', pcia_nombre` varchar(20) NOT NULL default , pcia_abreviatura` char(3) NOT NULL default , pcia_codigoib` int(3) unsigned NOT NULL default '0', pcia_letra` char(1) NOT NULL default , KEY `prov_nombre` (`pcia_nombre`,`pcia_codigoib`) ) TYPE=InnoDB COMMENT='Tabla de provincias unidas del río de la plata';
INSERT INTO `provincia` VALUES (901,'CAPITAL FEDERAL','CAP',901,'C'); INSERT INTO `provincia` VALUES (902,'BUENOS AIRES','BAI',902,'B'); INSERT INTO `provincia` VALUES (903,'CATAMARCA','CAT',903,'K'); INSERT INTO `provincia` VALUES (904,'CORDOBA','CBA',904,'X'); INSERT INTO `provincia` VALUES (905,'CORRIENTES','COR',905,'W'); INSERT INTO `provincia` VALUES (906,'CHACO','CHA',906,'H'); INSERT INTO `provincia` VALUES (907,'CHUBUT','CHU',907,'U'); INSERT INTO `provincia` VALUES (908,'ENTRE RIOS','ERI',908,'E'); INSERT INTO `provincia` VALUES (909,'FORMOSA','FOR',909,'P'); INSERT INTO `provincia` VALUES (910,'JUJUY','JUJ',910,'Y'); INSERT INTO `provincia` VALUES (911,'LA PAMPA','LAP',911,'L'); INSERT INTO `provincia` VALUES (912,'LA RIOJA','LAR',912,'F'); INSERT INTO `provincia` VALUES (913,'MENDOZA','MZA',913,'M'); INSERT INTO `provincia` VALUES (914,'MISIONES','MIS',914,'N'); INSERT INTO `provincia` VALUES (915,'NEUQUEN','NEU',915,'Q'); INSERT INTO `provincia` VALUES (916,'RIO NEGRO','RNE',916,'R'); INSERT INTO `provincia` VALUES (917,'SALTA','SAL',917,'A'); INSERT INTO `provincia` VALUES (918,'SAN JUAN','SJU',918,'J'); INSERT INTO `provincia` VALUES (919,'SAN LUIS','SLU',919,'D'); INSERT INTO `provincia` VALUES (920,'SANTA CRUZ','SCR',920,'Z'); INSERT INTO `provincia` VALUES (921,'SANTA FE','SFE',921,'S'); INSERT INTO `provincia` VALUES (922,'SANTIAGO DEL ESTERO','STG',922,'G'); INSERT INTO `provincia` VALUES (923,'TIERRA DEL FUEGO','TIE',923,'V'); INSERT INTO `provincia` VALUES (924,'TUCUMAN','TUC',924,'T');
Ahora que tenemos todo preparado vamos hacer nuestro primer programa Clip/MySQL:
* ----------------------------------------------------- * Programa para conectarse y mostrar la tabla provincia * Nombre del programa: browselocalidad.prg * ----------------------------------------------------- function main() oConn := conectargc( "gc" , "clip" ) oRs := oConn:CreateRowset( "select * from localidad" ) oRs:browse() return NIL
Compilamos ahora nuestra pequeña aplicación:
clip -e browselocalidad.prg gcfunc.prg -lclip-mysql
Probamos la aplicación:
./browselocalidad
¡Listo! Vemos la tabla en la pantalla y nos podemos mover con las flechas de movimiento del cursor. Mediante este browse sólo podemos ver los datos y no modificarlos.
Con la tecla Esc podemos salir del browse y retornar al shell.
Segunda Parte Clip/MySQL - Búsqueda usando MySQL y mostrar los resultados en un rowset
Ahora que tenemos nuestro andamiaje básico vamos a complicarlo un poco agregando una búsqueda.
El armado de rowset permite el pasaje de parámetros mediante un vector o arreglo de dos dimensiones.
Los elementos de este vector son entonces otros vectores
Supongamos que deseamos filtrar por las provincias que comiencen con cierto texto, por ejemplo SANTA FE de allí en adelante hasta digamos 20 registros, para ello usamos el siguiente comando SQL:
select * from provincia where pcia_nombre >= 'SANTA FE' limit 20
Pero queremos pasarle, en lugar de 'SANTA FE' una cadena ingresada por el usuario, supongamos que se llame cBusqueda esa cadena variable.
La forma de pasarle este parámetro es entonces:
* --
aParam := {} //Creamos un vector vacío
/* Ahora agregamos un elemento a aParam que es a su vez
un vector con dos elementos: una cadena cPar que luego
veremos para que se usa y lo que queremos buscar
*/
aadd( aParam , { "cPar" , cBusqueda } )
/* Generamos nuestro SQL
Observemos que el parámetro se pasa mediante
cPar y con dos puntos adelante.
*/
cSQL := "select * from provincia where pcia_nombre >= :cPar limit 20"
Pongamos ahora todo junto a ver que sucede:
* -----------------------------------------------------
* Programa para conectarse y mostrar la tabla provincia
* Nombre del programa: browsepcia.prg
* -----------------------------------------------------
#include "inkey.ch"
function main()
local oConn
local cBusqueda
oConn := conectargc( "gc" , "clip" )
while .t.
clear
cBusqueda := space(15)
@ 1,1 say "Ingrese provincia a Buscar <Esc-Sale>" get cBusqueda
read
if lastkey() = K_ESC
clear
exit
endif
cBusqueda := alltrim( cBusqueda )
aParam := {} //Creamos un vector vacío
/* Ahora agregamos un elemento a aParam que es a su vez
un vector con dos elementos: una cadena cPar que luego
veremos para que se usa y lo que queremos buscar
*/
aadd( aParam , { "cPar" , cBusqueda } )
/* Generamos nuestro SQL
Observemos que el parámetro se pasa mediante
cPar y con dos puntos adelante.
*/
cSQL := "select * from provincia where pcia_nombre >= :cPar limit 20"
oRs := oConn:CreateRowset( cSQL , aParam )
oRs:browse( 3 , 1 , 24 , 78 ) // Usamos el browse para ver los datos.
oRs:destroy() // Destruimos el rowset para liberar los recursos
enddo
return NIL
- Fin BrowsePcia.prg
Ahora vamos a compilar nuestra aplicación:
$ clip -e browsepcia.prg gcfunc.prg -lclip-prg $ ./browsepcia
¡Listo! ya tenemos una simple consulta funcionando.
Por supuesto podemos cambiar el comando SQL (ordenarlo de manera distinto por ejemplo)
Ahora que podemos mostrar los datos mediante una búsqueda, vamos a agregar registros, modificar registros y borrar registros de una tabla.
Clip nos ofrece dos métodos para las operaciones descriptas.
Metodo 1:
Mediante el mismo objeto *Rowset*, se le pasan como parámetros los comandos SQL, los cuales usando variables "implícitas" permite la modificación de la tabla.
Metodo 2:
Invocando el método *command* a la conexión. Se puede invocar este método siempre que no el comando SQL no sea un "select", es decir que tenga que devolver un resultado de la consulta.
El Método 2 es más rápido siempre. El Método 1 se vuelve lento a medida que crece el rowset en cantidad de registros (no importa la cantidad de registros que tenga la tabla, sino cuántos trae el rowset)
Usaremos un método simple para ingresar y modificar registros: cargar los datos en variables de memoria y luego aplicarles el comando SQL adecuado.
Siguiendo con nuestro ejemplo usaremos un "browse" para mostrar los datos y luego con las teclas <Insert>, <Intro> y <Supr>, agregar, modificar o borrar los registros.
Usaremos el método 2 por claridad en primer lugar.
Necesitamos entonces usar un TBrowse para controlar el ingreso de las teclas del usuario.
* --------------------------------------------------------
* Programa para conectarse y modificar la tabla provincia
* Metodo 2
* Nombre del programa: ambpcia.prg
* --------------------------------------------------------
#include "inkey.ch"
#include "pgch.ch"
function main()
local oConn
local cBusqueda
local k
oConn := conectargc( "gc" , "clip" )
while .t.
clear
cBusqueda := space(15)
@ 1,1 say "Ingrese provincia a Buscar <Esc-Sale>" get cBusqueda
read
if lastkey() = K_ESC
clear
exit
endif
cBusqueda := alltrim( cBusqueda )
aParam := {} //Creamos un vector vacío
/* Ahora agregamos un elemento a aParam que es a su vez
un vector con dos elementos: una cadena cPar que luego
veremos para que se usa y lo que queremos buscar
*/
aadd( aParam , { "cPar" , cBusqueda } )
/* Generamos nuestro SQL
Observemos que el parámetro se pasa mediante
cPar y con dos puntos adelante.
*/
cSQL := "select * from provincia where pcia_nombre >= :cPar limit 20"
oRs := oConn:CreateRowset( cSQL , aParam )
/* Un lindo marco :-) y texto para el usuario*/
@ 3,1 to 20,78
@ 21,1 say " INTRO-Modifica, INS-Agrega, SUPR-Borra"
/* Creamos el TBrowse */
oTb := tbrowsenew( 4 , 2 , 19 , 77 )
oTb:colSep := chr( PGCH_VLINE ) //Modificaciones
oTb:headSep := chr( PGCH_TTEE ) + chr( PGCH_HLINE ) //para que luzca mejor
/* Armamos las columnas del tbrowse */
for k = 1 to oRs:NFields() // Cantidad de campos
/* Usamos los métodos del rowset para escribir menos :-) */
oTb:addcolumn( tbcolumnnew( oRs:Fieldname( k) , oRs:Fieldblock(k) ) )
next k
/* Declaramos los métodos para el movimiento en el rowset */
oTB:goTopBlock := { || oRs:GoTop() }
oTB:goBottomBlock := { || oRs:GoBottom() }
oTB:skipBlock := { |n| SkipRecs( oRs , n ) } // La funcion SkipRecs se define en gcfunc
/* El conocido lazo del TBrowse */
while .t.
while nextkey() == 0
if oTB:stabilize()
exit
endif
enddo
oTB:refreshCurrent()
oTB:ForceStable()
nKey := inkey(0)
do case
case nKey == K_UP; oTB:up()
case nKey == K_DOWN; oTB:down()
case nKey == K_CTRL_PGUP; oTB:gotop()
case nKey == K_CTRL_PGDN; oTB:gobottom()
case nKey == K_LEFT; oTB:left()
case nKey == K_RIGHT; oTB:right()
case nKey == K_PGUP; oTB:pageup()
case nKey == K_PGDN; oTB:pagedown()
case nKey == K_INS // Agregamos un registro
modpvcia( .t., oConn, oRs)
// Refrescamos el rowset y el tbrowse
oRs:refreshall()
oTb:refreshCurrent()
case nKey == K_ENTER // Modificamos un registro
modpvcia( .f. ,oConn, oRs)
// Refrescamos el rowset y el tbrowse
oRs:refreshall()
oTb:refreshCurrent()
case nKey == K_DEL // Borramos un registro
borrapvcia(oConn, oRs)
// Refrescamos el rowset y el tbrowse
oRs:refreshall()
oTb:refreshCurrent()
case nKey == K_ESC; exit
endcase
enddo
oRs:destroy() // Destruimos el rowset para liberar los recursos
enddo
return NIL
/* Las funciones que necesitamos para modificar la tabla */
- -----------------------------------------------
static function modpvcia( lAlta , oConn , oRs )
- -----------------------------------------------
- Parametro: lAlta es TRUE (.T.) si es un alta
- FALSE (.F.) si es una edicion
- oConn: Objeto conexión donde voy a ejecutar el
- comando SQL
- oRs: Rowset de donde buscar la informacion
- Observación importante:
- Los datos tipo caracter que traigo desde MySQL vienen del largo
- de lo que está guardado y NO de la longitud del campo definida
- en la estructura de la tabla
- Si es un alta llenamos los campos con los valores en blanco,
- sino guardamos los valores que están en la tabla
- obteniendo el valor con el metodo GetValue
local cSQL local aParam
if lAlta nId_provincia := 0 cPcia_nombre := space(20) cPcia_abreviatura := space(3) nPcia_codigoib := 0 cPcia_letra := space(1)
- El comando SQL para agregar el registro
cSQL := "insert into provincia values " + ; "( :id_provincia, " + ; ":pcia_nombre, " +; ":pcia_abreviatura, "+; ":pcia_codigoib, "+; ":pcia_letra )" else
- Agrego espacios a la derecha cuando lo necesito
nId_provincia := oRs:GetValue("id_provincia") cPcia_nombre := padr( oRs:GetValue( "pcia_nombre") , 20 ) cPcia_abreviatura := padr( oRs:GetValue( "pcia_abreviatura" ) , 3 ) nPcia_codigoib := oRs:GetValue( "pcia_codigoib" ) cPcia_letra := padr( oRs:GetValue("pcia_letra") , 1 )
- El comando SQL para actualizar el registro
cSQL := "update provincia set " + ; "pcia_nombre = :pcia_nombre, " +; "pcia_abreviatura = :pcia_abreviatura, "+; "pcia_codigoib = :pcia_codigoib, "+; "pcia_letra = :pcia_letra " +; "where id_provincia = :id_provincia "
endif
- Ingresamos los datos
cPantalla := savescreen( 6, 4 , 12 , 75 ) @ 6,4 clear to 12, 75 @ 6,4 to 12, 75
@ 7,6 say "Id " get nId_provincia when lAlta @ 8,6 say "Nombre " get cPcia_nombre @ 9,6 say "Abreviatura" get cPcia_abreviatura @ 10,6 say "Codigo IB " get nPcia_codigoib @ 11,6 say "Letra " get cPcia_letra read
restscreen(6,4,12,75, cPantalla )
- Armamos el vector de parámetros que necesitamos para el SQL
- Los parámetros a pasar tienen el mismo nombre que los campos
- de la tabla por cuestiones de orden, pueden tener cualquier
- nombre
aParam := {}
aadd( aParam , { "id_provincia" , nId_Provincia } )
aadd( aParam , { "pcia_nombre" , cPcia_nombre } )
aadd( aParam , { "pcia_abreviatura" , cPcia_abreviatura } )
aadd( aParam , { "pcia_codigoib" , nPcia_codigoib } )
aadd( aParam , { "pcia_letra" , cPcia_letra } )
- Ejecutamos el comando
oConn:command( cSQL , aParam )
return NIL
static function borrapvcia( oConn, oRs ) local cSioNo := space(2) local aParam local nPar
- Preguntamos si está seguro de hacer la operación
@ 24,2 say "Escriba SI si está seguro de borrar el registro" get cSioNO pict "!!" read if cSioNo == "SI" nIdProvincia := oRs:GetValue( "id_provincia" ) //Obtenemos el valor del ID
aParam := {} aadd( aParam , {"nPar" , nIdProvincia } ) //Usamos otro nombre por didactica oConn:command("delete from provincia where id_provincia = :nPar" , aParam ) endif @ 24,2 // Borramos la línea return NIL
- Fin ModPcia.prg
Usando el método 1
Es necesario crear el rowset con los SQL adecuados para cada una de las operaciones. Es decir que al rowset se le pasan como parámetros los rowset y luego mediante métodos de ese rowset se los invoca.
Para ello debemos cambiar la creación del rowset:
aadd( aParam , { "cPar" , cBusqueda } )
cSQL := "select * from provincia where pcia_nombre >= :cPar limit 20"
cInserta := "insert into provincia values " + ; "( :id_provincia, " + ; ":pcia_nombre, " +; ":pcia_abreviatura, "+; ":pcia_codigoib, "+; ":pcia_letra )"
cActualiza := "update provincia set " + ; "pcia_nombre = :pcia_nombre, " +; "pcia_abreviatura = :pcia_abreviatura, "+; "pcia_codigoib = :pcia_codigoib, "+; "pcia_letra = :pcia_letra " +; "where id_provincia = :id_provincia "
cBorra := "delete from provincia where id_provincia = :id_provincia"
/* Pasamos esos comandos SQL al crear el rowset */
oRs := oConn:CreateRowset( cSQL , aParam , cInserta , cBorra , cActualiza )
Las funciones que modifican el rowset quedan entonces:
- -----------------------------------------------
static function modpvcia( lAlta , oRs )
- -----------------------------------------------
- Parametro: lAlta es TRUE (.T.) si es un alta
- FALSE (.F.) si es una edicion
- oRs: Rowset de donde buscar la informacion y donde ejecutar
- los SQL
- Observación importante:
- Los datos tipo caracter que traigo desde MySQL vienen del largo
- de lo que está guardado y NO de la longitud del campo definida
- en la estructura de la tabla
- Si es un alta llenamos los campos con los valores en blanco,
- sino guardamos los valores que están en la tabla
- obteniendo el valor con el metodo GetValue
local oMap
if lAlta nId_provincia := 0 cPcia_nombre := space(20) cPcia_abreviatura := space(3) nPcia_codigoib := 0 cPcia_letra := space(1)
else
- Agrego espacios a la derecha cuando lo necesito
nId_provincia := oRs:GetValue("id_provincia") cPcia_nombre := padr( oRs:GetValue( "pcia_nombre") , 20 ) cPcia_abreviatura := padr( oRs:GetValue( "pcia_abreviatura" ) , 3 ) nPcia_codigoib := oRs:GetValue( "pcia_codigoib" ) cPcia_letra := padr( oRs:GetValue("pcia_letra") , 1 )
endif
- Ingresamos los datos
cPantalla := savescreen( 6, 4 , 12 , 75 ) @ 6,4 clear to 12, 75 @ 6,4 to 12, 75
@ 7,6 say "Id " get nId_provincia when lAlta @ 8,6 say "Nombre " get cPcia_nombre @ 9,6 say "Abreviatura" get cPcia_abreviatura @ 10,6 say "Codigo IB " get nPcia_codigoib @ 11,6 say "Letra " get cPcia_letra read
restscreen(6,4,12,75, cPantalla )
- Cargamos un vector asociativo con los valores a pasarle al comando SQL
oMap := map() //Creamos el vector asociativo vacío oMap:id_provincia := nId_Provincia oMap:pcia_nombre := cPcia_nombre oMap:pcia_abreviatura := cPcia_abreviatura oMap:pcia_codigoib := nPcia_codigoib oMap:pcia_letra := cPcia_letra
if lAlta
- Agregamos un registro
oRs:append( oMap ) else oRs:write( oMap ) endif
return NIL
static function borrapvcia( oRs ) local cSioNo := space(2) local aParam local nPar
- Preguntamos si está seguro de hacer la operación
@ 24,2 say "Escriba SI si está seguro de borrar el registro" get cSioNO pict "!!" read if cSioNo == "SI" nIdProvincia := oRs:GetValue( "id_provincia" ) //Obtenemos el valor del ID
oMap := map() oMap:id_provincia := nIdProvincia oRs:delete(oMap) endif @ 24,2 // Borramos la línea return NIL
Nos queda entonces nuestro ABM con el segundo método de la siguiente manera:
- --------------------------------------------------------
- Programa para conectarse y modificar la tabla provincia
- Metodo 1
- Nombre del programa: ambpcia.prg
- --------------------------------------------------------
- include "inkey.ch"
- include "pgch.ch"
function main()
local oConn local cBusqueda local k
oConn := conectargc( "gc" , "clip" )
while .t.
clear
cBusqueda := space(15)
@ 1,1 say "Ingrese provincia a Buscar <Esc-Sale>" get cBusqueda
read
if lastkey() = K_ESC
clear
exit
endif
cBusqueda := alltrim( cBusqueda )
aParam := {} //Creamos un vector vacío
/* Ahora agregamos un elemento a aParam que es a su vez un vector con dos elementos: una cadena cPar que luego veremos para que se usa y lo que queremos buscar */
aadd( aParam , { "cPar" , cBusqueda } )
/* Generamos nuestro SQL Observemos que el parámetro se pasa mediante cPar y con dos puntos adelante. */
cSQL := "select * from provincia where pcia_nombre >= :cPar limit 20"
cInserta := "insert into provincia values " + ;
"( :id_provincia, " + ;
":pcia_nombre, " +;
":pcia_abreviatura, "+;
":pcia_codigoib, "+;
":pcia_letra )"
cActualiza := "update provincia set " + ;
"pcia_nombre = :pcia_nombre, " +;
"pcia_abreviatura = :pcia_abreviatura, "+;
"pcia_codigoib = :pcia_codigoib, "+;
"pcia_letra = :pcia_letra " +;
"where id_provincia = :id_provincia "
cBorra := "delete from provincia where id_provincia = :id_provincia"
/* Pasamos esos comandos SQL al crear el rowset */
oRs := oConn:CreateRowset( cSQL , aParam , cInserta , cBorra , cActualiza )
/* Un lindo marco */
@ 3,1 to 20,78 @ 21,1 say "<Intro>Modifica Agrega <Supr>Borra"
/* Creamos el TBrowse */ oTb := tbrowsenew( 4 , 2 , 19 , 77 ) oTb:colSep := chr( PGCH_VLINE ) //Modificaciones oTb:headSep := chr( PGCH_TTEE ) + chr( PGCH_HLINE ) //para que luzca mejor
/* Armamos las columnas del tbrowse */ for k = 1 to oRs:NFields() // Cantidad de campos
/* Usamos los métodos del rowset para escribir menos :-) */ oTb:addcolumn( tbcolumnnew( oRs:Fieldname( k) , oRs:Fieldblock(k) ) )
next k
/* Declaramos los métodos para el movimiento en el rowset */
oTB:goTopBlock := { || oRs:GoTop() }
oTB:goBottomBlock := { || oRs:GoBottom() }
oTB:skipBlock := { |n| SkipRecs( oRs , n ) } // La funcion SkipRecs se
// define en gcfunc
/* El conocido lazo del TBrowse */
while .t.
while nextkey() == 0
if oTB:stabilize()
exit
endif
enddo
oTB:refreshCurrent()
oTB:ForceStable()
nKey := inkey(0) do case case nKey == K_UP; oTB:up() case nKey == K_DOWN; oTB:down() case nKey == K_CTRL_PGUP; oTB:gotop() case nKey == K_CTRL_PGDN; oTB:gobottom() case nKey == K_LEFT; oTB:left() case nKey == K_RIGHT; oTB:right() case nKey == K_PGUP; oTB:pageup() case nKey == K_PGDN; oTB:pagedown() case nKey == K_INS // Agregamos un registro modpvcia( .t., oRs) * Refrescamos el rowset y el tbrowse oRs:refreshall() oTb:refreshCurrent() case nKey == K_ENTER // Modificamos un registro modpvcia( .f. , oRs) * Refrescamos el rowset y el tbrowse oRs:refreshall() oTb:refreshCurrent()
case nKey == K_DEL // Borramos un registro borrapvcia(oRs) * Refrescamos el rowset y el tbrowse oRs:refreshall() oTb:refreshCurrent()
case nKey == K_ESC
exit
endcase
enddo
oRs:destroy() // Destruimos el rowset para liberar los recursos enddo return NIL
/* Las funciones que necesitamos para modificar la tabla */
- -----------------------------------------------
static function modpvcia( lAlta , oRs )
- -----------------------------------------------
- Parametro: lAlta es TRUE (.T.) si es un alta
- FALSE (.F.) si es una edicion
- oRs: Rowset de donde buscar la informacion y donde ejecutar
- los SQL
- Observación importante:
- Los datos tipo caracter que traigo desde MySQL vienen del largo
- de lo que está guardado y NO de la longitud del campo definida
- en la estructura de la tabla
- Si es un alta llenamos los campos con los valores en blanco,
- sino guardamos los valores que están en la tabla
- obteniendo el valor con el metodo GetValue
local oMap
if lAlta
nId_provincia := 0 cPcia_nombre := space(20) cPcia_abreviatura := space(3) nPcia_codigoib := 0 cPcia_letra := space(1)
else
- Agrego espacios a la derecha cuando lo necesito
nId_provincia := oRs:GetValue("id_provincia")
cPcia_nombre := padr( oRs:GetValue( "pcia_nombre") , 20 )
cPcia_abreviatura := padr( oRs:GetValue( "pcia_abreviatura" ) , 3 )
nPcia_codigoib := oRs:GetValue( "pcia_codigoib" )
cPcia_letra := padr( oRs:GetValue("pcia_letra") , 1 )
endif
- Ingresamos los datos
cPantalla := savescreen( 6, 4 , 12 , 75 ) @ 6,4 clear to 12, 75 @ 6,4 to 12, 75
@ 7,6 say "Id " get nId_provincia when lAlta @ 8,6 say "Nombre " get cPcia_nombre @ 9,6 say "Abreviatura" get cPcia_abreviatura @ 10,6 say "Codigo IB " get nPcia_codigoib @ 11,6 say "Letra " get cPcia_letra read
restscreen(6,4,12,75, cPantalla )
- Cargamos un vector asociativo con los valores a pasarle al comando SQL
oMap := map() //Creamos el vector asociativo vacío oMap:id_provincia := nId_Provincia oMap:pcia_nombre := cPcia_nombre oMap:pcia_abreviatura := cPcia_abreviatura oMap:pcia_codigoib := nPcia_codigoib oMap:pcia_letra := cPcia_letra
if lAlta
- Agregamos un registro
oRs:append( oMap )
else
oRs:write( oMap )
endif
return NIL
static function borrapvcia( oRs ) local cSioNo := space(2) local aParam local nPar
- Preguntamos si está seguro de hacer la operación
@ 24,2 say "Escriba SI si está seguro de borrar el registro" get cSioNO pict "!!" read if cSioNo == "SI"
nIdProvincia := oRs:GetValue( "id_provincia" ) //Obtenemos el valor del ID
oMap := map() oMap:id_provincia := nIdProvincia oRs:delete(oMap)
endif @ 24,2 // Borramos la línea return NIL
A gcfunc.prg necesitamos agregarle la siguiente función:
- -------------------------------
function SkipRecs( oRs , nRecs )
- -------------------------------
- Función que controla el salto
- de los registros dentro del
- rowset
- Parametros:
- nRecs: cantidad de registros a saltar
local nSkipped := 0 do case case nRecs == 0
oRs:Skip( 0 )
case nRecs < 0 while nSkipped > nRecs
oRs:Skip( -1 )
if oRs:bof()
exit
endif
nSkipped--
enddo case nRecs > 0
while nSkipped < nRecs
oRs:Skip( 1 )
if oRs:eof()
exit
endif
nSkipped++
enddo
endcase return nSkipped
- EOF
Actividad 5: Probar los ejemplos anteriores
Usar Clip para el desarrollo de aplicaciones gráficas con Glade y libglade.
Problemas que se suscitan ante el desafío de la programación gráfica Clip es un lenguaje extremadamente poderoso y flexible, que tiene las siguientes formas para hacer interfaces gráficas:
* GTK * GTK2 * Fivewin implementada con GTK * Clip-UI * Clip-glade2
Nos concentraremos en la forma de hacer una agenda muy simple mediante clip-glade2 por las siguientes razones:
1. Es sencillo crear las interfaces gráficas mediante Glade2 2. Es lo único que entiendo por ahora de la interfaz gráfica en GNU/Linux :-) 3. No conozco Fivewin 4. Clip-UI carece de generador de interfaces gráficas 5. Programar directamente usando las funciones de GTK y GTK2 se ve como tedioso
*
¿Porqué GTK2?
GTK se considera obsoleta y se detuvo su desarrollo.
GTK2 es multiplataforma
Hay abundante documentación
Se puede usar Glade2 *
¿Porqué Glade?
Glade permite diseñar con facilidad las interfases
Es ubicua a cualquier distribución y funciona aun en MS-Windows
Con libglade es lo que usaremos para crear la interfaz gráfica. *
¿Es multiplataforma?
En teoría es multiplataforma: Clip funciona con cygwin y Glade2 y GTK2 están portadas a MS-Windows.
*
Distintas alternativas a GTK2 para Clip
Como mencionamos antes, está la biblioteca que emula la famosa FiveWin para Clipper. Está basada en GTK. Clip-UI permite generar interfaces gráficas también con documentos XML, pero carece de un generador de tales documentos, tal como sería Glade2. Si se quiere ver un desarrollo hecho con Clip-UI, ver la siguiente página:
http://eas.lrn.ru/index.php?id=screenshots
*
Primeros pasos con Glade2
Abrimos Glade2 y aparecen dos paneles:
Para el manejo de Glade y sus conceptos básicos ver la siguiente página:
http://eddy.writelinux.com/spanish/
Programando una simple agenda paso por paso.
Creo una ventana nueva con el icono correspondiente a la que llamamos 'window1' Le cambiamos el título a la ventana: “Agenda 0.2” Si no se ven las propiedades de los widgets, es porque no está activada en el menú “Ver” de Glade.
Ahora voy a crear el contenedor para la barra de tareas, los campos y las etiquetas de los campos.
El contenedor deberá tener entonces dos renglones y al renglón de abajo lo deberé dividir a su vez en otro contenedor de tres renglones con dos columnas (los campos a mostrar serán: nombre, irección y teléfono)
Como resultado tendremos lo siguiente:
Ahora agrego los botones que serán las acciones sobre la agenda:
Agregar, Editar, Borrar, Buscar, Siguiente y Anterior.
Vemos como Glade acomoda automaticamente las dos secciones.
Para ello agregamos un widget “barra de herramientas” de 5 botones. Luego vamos agregando los botones y editando sus propiedades Ahora agregamos los botones a la barra de herramientas. Recordar que para que muestre el icono con el texto incluido hay que seleccionar "Botón de inventario".
A cada botón le agregamos la señal cuando “clicked”:
Lo que obtenemos al final, rellenando los distintos contenedores con etiquetas y campos de edición es lo siguiente:
Grabamos el proyecto como 'agenda.glade'
Ahora intentaremos hacer que con Clip nos muestre esta interfaz, sólo eso.
Siguiendo el ejemplo de Sergio Zayas en:
clip-prg-1.2-0-0/cliplibs/clip-glade2/examples
cargaremos la interfaz con el siguiente código:
/*
agenda.prg
Primera prueba con clip-libglade2
- /
static _xmlfic
function xmlfic() return ( _xmlfic )
function main( glade_file)
local xml, widget _xmlfic := iif( empty( glade_file) , "agenda.glade" , glade_file )
if (!file( _xmlfic ) ) ? "No se encontró el archivo de interfaz: " + _xmlfic ? quit endif gtk_init() xml_w1 := glade_xml_new( _xmlfic , "window1" ) if (xml_w1 == nil ) ? "** Error de parser, archivo: " + _xmlfic return( 1 ) endif
widget := glade_xml_get_widget( xml_w1 , "window1" ) glade_xml_signal_autoconnect( xml_w1 ) //glade autoconecta las señales
gtk_widgetShow( widget ) //lo mostramos si no está visible gtk_main() // esto arranca todos los eventos
return( 0 )
Ahora compilamos:
$ clip -e agenda.prg -lclip-gtk2 -lclip-glade2
Ejecutamos nuestra agenda:
./agenda
Vemos lo siguiente:
Hasta aquí llegamos con la primera parte de nuestra pequeña investigación.
Vemos una "agenda" mas funcional:
/*
agenda.prg Primera prueba con clip-libglade2
- /
static _xmlfic
function xmlfic() return ( _xmlfic )
function main( glade_file)
local xml, widget _xmlfic := iif( empty( glade_file) , "agenda.glade" , glade_file )
if (!file( _xmlfic ) )
? "No se encontró el archivo de interfaz: " + _xmlfic ? quit
endif set delete on
- ---------------------------------------------------
if !file( "agenda.dbf" )
* Creamos la agenda con datos básicos de prueba
aDbf := {}
aadd( aDbf , { "NOMBRE" , "C" , 30 , 0 } )
aadd( aDbf , { "DIRECCION" , "C" , 45 , 0 } )
aadd( aDbf , { "TELEFONO" , "C" , 20 , 0 } )
DBCREATE( "agenda" , aDbf )
/* Cargamos un dato para mostrar luego */
use agenda
append blank
replace nombre with "Sylvia Saint"
replace direccion with "Alguna casa en California"
replace telefono with "Si lo supiera!"
use
endif
- ---------------------------------------------------
gtk_init() xml_w1 := glade_xml_new( _xmlfic , "window1" ) if (xml_w1 == nil )
? "** Error de parser, archivo: " + _xmlfic return( 1 )
endif
widget := glade_xml_get_widget( xml_w1 , "window1" ) glade_xml_signal_autoconnect( xml_w1 ) //glade autoconecta las señales
// Esta no la autoconecta
gtk_signalConnect( widget , "delete-event",;
{ |w,e| on_window1_delete_event(w,e) } )
// Esta tampoco se autoconecta. Debe ser que no hago bien las cosas // Es para agregar un registro
glade_xml_signal_connect( xml_w1 , "on_toolbutton1_clicked", ;
{ |w| on_new_activate( w ) } )
// Esto es para editar el registro glade_xml_signal_connect( xml_w1 , "on_toolbutton2_clicked", ;
{ |w| on_edit_activate( w ) } )
// Esto es para borrar un registro glade_xml_signal_connect( xml_w1 , "on_toolbutton3_clicked" , ;

