Otra de las nuevas funcionalidades que trae la versión de XE2 es el soporte para ficheros ZIP. No es que sea especialmente transcendental, pero en mi caso la he utilizado bastante en mis desarrollos y hasta ahora lo había tenido que hacer mediante componentes externos (he probado varios paquetes).
Para ello se ha añadido a la unit System.ZIP, todo lo necesario para el trabajo con este tipo de ficheros.
En sistema de trabajo es bastante sencillo. Para trabajar con un fichero de este tipo tenemos que crear un objeto de la clase TZIPFile y abrirlo (utilizando la función Open). Tenemos diferentes modos de apertura dependiendo de las operaciones a realizar con el fichero (read,write o readwrite).
Embarcadero MVP.
Analista y Programador de Sistemas Informáticos.
Estudios de Informática (Ingeniería Técnica Superior) en la UPC (Universidad Politécnica de Barcelona).
Llevo utilizando Delphi desde su versión 3. Especialista en diseño de componentes, Bases de Datos, Frameworks de Persistencia, Integración Continua, Desarrollo móvil,…
Otra de las cosas de las que ya he hablado y que llaman la atención en esta nueva versión de Delphi, son los estilos. Es una primera implementación y es mejorable en muchos aspectos, pero es una paso más y un avance más para poder personalizar nuestras aplicaciones.
Hay que decir que el funcionamiento general es muy sencillo.
Estaría bien, por ejemplo, mejorar el generador de estilos para que se pudieran importar estilos desde otros archivos similares. En mi caso el generador se ha mostrado “poco estable” cuando he intentado hacer algo más complejo (cambio de imágenes) que intentar modificar los colores del estilo cargado.
Por internet podéis descargar varios videos sobre el uso del «VCL Style Designer», que así es como se llama la herramienta para crear y modificar los estilos. Aunque es bastante sencilla e intuitiva. Una vez cargado el estilo, podemos modificar los diferentes elementos. Se puede exportar la imagen del estilo completo, modificarla desde un editor externo y luego volverla a cargar, para poder así asignar elementos de esa nueva imagen a los diferentes componentes. Se pueden cargar varias imágenes y combinar elementos de varias de ellas; Pero como he dicho en estas operaciones se ha mostrado «inestable»
Los componentes que derivan de los estándar cogen de forma correcta los estilos. Lo que no he podido probar (y no parece que exista ahora solución -digo esto sin habérmelo mirado mucho-) es qué posibilidades hay para componentes «externos» que no deriven de uno estándar.
Una creado o modificado un estilo podemos cambiar los campos que identifican al creador, desde la misma herramienta de creación (autor, versión, página web y correo electrónico). Mientras que la previsualización desde el «Style Designer» funciona bastante bien (salvo cuando hay algun error que no muestra nada -tampoco el error-), desde el IDE de Delphi la previsualización es defectuosa (podrían haber utilizado el mismo método que desde el Designer) o «floja» por decirlo así.
En resumen, aunque el sistema tiene algunos detalles todavía faltos de pulir, en general me parece un acierto el poder dotar de esta posibilidad a las aplicaciones que se realicen. El sistema es muy sencillo de utilizar y aunque tiene algunas limitaciones, funciona bastante bien.
Actualmente al cargar instalar Delphi XE2 contamos contamos con 5 estilos que podemos utilizar.
Desde la web de Rodrigo podeís descargar algunos que él ya ha creado y es de esperar que los miembros de la comunidad vayan «colgando» los suyos propios a medida que los vayan creando (para eso supongo que se ha distribuido). Desde ahí mismo podéis consultar la explicación detallada de cómo crear nuevos estilos a partir de los existentes.
El código para cargar el estilo es bastante simple a partir de su nombre:
Var
Estilo:TStyleManager;
i:integer;begin
Estilo := TStyleManager.Create;try
i :=Random(6);
Estilo.SetStyle(lEstilos[i]);finally
Estilo.Free;end;</p>
Var
Estilo:TStyleManager;
i:integer;
begin
Estilo := TStyleManager.Create;
try
i := Random(6);
Estilo.SetStyle(lEstilos[i]);
finally
Estilo.Free;
end;</p>
En mi caso cargo los estilos de forma aleatoria a partir de un array donde los tengo definidos.
Hasta aquí esta primera visión. En general me ha parecido positivo. Algunos aspectos por pulir, pero creo que es un detalle más, que hace subir puntos a esta versión.
Embarcadero MVP.
Analista y Programador de Sistemas Informáticos.
Estudios de Informática (Ingeniería Técnica Superior) en la UPC (Universidad Politécnica de Barcelona).
Llevo utilizando Delphi desde su versión 3. Especialista en diseño de componentes, Bases de Datos, Frameworks de Persistencia, Integración Continua, Desarrollo móvil,…
Pues ya tengo Delphi XE2 en marcha. Que ya lo tengo instalado ¡Vamos!
No se a vosotros, pero a mi me pasa (y no sólo con esta versión) que una vez que la tengo instalada, lo primero que se me ocurre es probarla. Probar las nuevas características y ver qué tal funcionan.
¡¡Eso es lo que hace todo el mundo!! Diréis.
Bueno, sí y no. Me refiero a «probarlas en frío». Sin saber nada más. Sin leer nada más.
Se trata de abrir un proyecto en blanco y empezar a probar cosas. Cosas totalmente nuevas. Cosas que no existían hasta ahora. Cosas de las que he oído hablar, pero no he leído nada.
Eso me da una idea de cómo son de fáciles a la hora de asimilar. Difíciles, complicadas, rebuscadas,… Busco documentación y veo cómo se llega a ella, si la hay, cómo es de clara,…
Todo el mundo habla estos días de FireMonkey. ;-D ¡¡Pues allá que voy yo!!
He ido al menú de fichero y he creado una nueva aplicación de este tipo (Fire Monkey HD Application).
A priori todo parece normal; el formulario se ve algo diferente, pero todo lo demás tiene «la misma pinta».
Pongo algunos componentes, unos botones, un checkbox, una statusBar y una imagen. Mi primera sorpresa viene cuando voy a buscar la propiedad Caption del botón y no está. 8-|
¡No puede ser! Vuelvo a buscar y efectívamente no está.
En su lugar veo la propiedad Text. Pruebo y efectívamente pertenece al caption del TButton.
No parece que tenga mayor importancia…
Pongo un checkbox y al ir a buscar la propiedad Checked… ¿??¿?¿ ¡¡No está!! 8-||
Vuelvo a revisar las propiedades y… encuentro una propiedad IsChecked que pertenece a lo que normalmente se hace con la propiedad Checked.
Dejando de lado esto (cuestión de la adaptación de los nuevos componentes adquiridos por Embarcadero-me imagino-) el resto de cosas que he probado son bastante intuitivas y algunas bastante impresionantes. Aplicar un efecto (para lo que antes necesitábamos bastante código o componentes especializados) o realizar una rotación de una imagen es algo «trivial» (por su sencillez).
La velocidad es muy buena y la primera impresión muy positiva.
Os dejo este primer ejemplo; Bastante «tonto» por su funcionalidad, pero también por la complejidad que me ha conllevado (bastante poca).
Aquí los fuentes y el ejecutable, aunque sinceramente no creo que tengan mucho interés, desde en punto de vista de la codificación.
NOTA: Sigo buscando cómo asignarle un texto a la StatusBar que he puesto en el formulario… ;-D
Embarcadero MVP.
Analista y Programador de Sistemas Informáticos.
Estudios de Informática (Ingeniería Técnica Superior) en la UPC (Universidad Politécnica de Barcelona).
Llevo utilizando Delphi desde su versión 3. Especialista en diseño de componentes, Bases de Datos, Frameworks de Persistencia, Integración Continua, Desarrollo móvil,…
Esta entrada se podría considerar como una «ampliación» de la última publicada relacionada con los ficheros KML, Generar ficheros KML de rutas; Tracks en Google Maps. A partir de un tema surgido en los foros del Clubdelphi de cómo visualizar estos ficheros directamente sobre Google Maps dentro de una aplicación Delphi, he modificado el programa anterior (que incluía la conversión desde GPX) para poder visualizarlos directamente.
En un ejemplo más simplificado, y enfocado a los ficheros KML. Como ya dije anteriormente, este tipo de ficheros pueden almacenar otros tipos de información y en este ejemplo se leen los datos directamente desde este tipo de ficheros (en este caso sólo la ruta).
Además el mapa está optimizado para visualizarse a tamaño completo, y no según el tamaño de la ventana como estaba anteriormente (esto implica la optimización de que no se recarga la página al cambiar el tamaño).
También se ha modificado el recurso de la página Web, para que automáticamente se calcule en «centrado» y el «zoom» del mapa a partir del Track que se va a visualizar. Para ello se utiliza el siguiente código:
var polylineBounds = polyline.getBounds();
var ZoomLevel = map.getBoundsZoomLevel(polylineBounds) - 1;
map.setCenter(polylineBounds.getCenter(), ZoomLevel);
var polylineBounds = polyline.getBounds();
var ZoomLevel = map.getBoundsZoomLevel(polylineBounds) - 1;
map.setCenter(polylineBounds.getCenter(), ZoomLevel);
Se puede descargar el ejemplo desde aquí (incluye los fuentes).
Debido a un cambio en la definición de la función EnumResourceModules, que pasa de usar LongInt a NativeInt, hay que realizar un cambio en el procedimiento que se le pasa como parámetro, modificando este dato.
La definición del procedimiento, ahora queda así:
function _MyEnumResourceModules (
Instance:{$IFDEF VER230}NativeInt{$ELSE}Pointer{$ENDIF};
Data:Pointer):Boolean;
function _MyEnumResourceModules (
Instance:{$IFDEF VER230}NativeInt{$ELSE}Pointer{$ENDIF};
Data:Pointer):Boolean;
Embarcadero MVP.
Analista y Programador de Sistemas Informáticos.
Estudios de Informática (Ingeniería Técnica Superior) en la UPC (Universidad Politécnica de Barcelona).
Llevo utilizando Delphi desde su versión 3. Especialista en diseño de componentes, Bases de Datos, Frameworks de Persistencia, Integración Continua, Desarrollo móvil,…
En esta entrada mostraré el código nesario para generar ficheros KML (Keyhole Markup Language) a partir de datos que provienen de un DataSet o de un fichero GPX (GPX eXchage Format).
Los ficheros KML se utilizan para representar datos geográficos en 3 dimensiones (además de la longitud y la Latitud incluyen también la altura) y fue desarrollado para ser utilizado por un programa que a posteriori se convertiría en Google Earth.
En una entrada anterior ya vimos como importar un fichero GPX, con posiciones geográficas a un Dataset para posteriormente poder tratar y manejar estas posiciones (Cargar fichero GPX (XML) y acceder a los datos). Partiendo de lo visto en esa entrada como base, debe ser sencillo generar un fichero KML con los puntos que forma la ruta.
Hay que decir que los ficheros KML permiten muchas más cosas de las vistas en esta entrada, y para ver todas las posibilidades podemos revisar los ejemplos que se muestran aquí (Tutorial KML en Google); En este entrada me voy a centrar en la creación de rutas (lista de puntos) aunque las posibilidades van mucho más allá.
La estructura de nuestro fichero se compone por una cabecera, donde se almacenan datos genéricos de la ruta, y el bloque central donde añadimos los diferentes puntos geográficos que la componen.
Un ejemplo sencillo de fichero KML podría ser este:
Como vemos en la parte superior añadimos datos de la ruta y algunas opciones de visualización. En este caso un nombre y una descripción y porteriormente se definen opciones de estilo para el track (LineStyle); Definimos que usaremos una línea para representar esos puntos con color Rojo y un a anchura de 3.
Este archivo podemos abrirlo directamente con Google Earth o importarlo a «Mis Mapas» desde Google Maps y la visualización ser algo así;
En Google Maps al importar la ruta tendríamos esto:
Y al Abrir el fichero con Google Earth obtendríamos esto:
Podemos ver que como la ruta está definida como <LineString>, esta se pinta como una línea siguiendo los parámetros definidos en el fichero (color rojo y ancho 3).
Para generar el fichero con una ruta, partiremos de que los datos han sido imprtados a un Dataset y que ya disponemos de ellos en ese formato.
Primero generaremos la cabecera del fichero con los datos genéricos de la ruta; Nombre, Descripción, y las características de estilo para la línea:
const
CAB_XML ='<!--?xml version="1.0" encoding="UTF-8"?-->';
CAB_KLM ='';
CAB_PLACEMARK ='';
CAB_NAME ='%s';
STR_STYLE ='<!--
<LineStyle>
<color>%s</color>'+'<width>%d</width></LineStyle>
-->';
CAB_LINE ='';begin// Limpiar
TSTrack.Clear;// Añadir cabeceras
TSTrack.Add(CAB_XML);
TSTrack.Add(CAB_KLM);
TSTrack.Add(CAB_PLACEMARK);// Datos de la ruta
TSTrack.Add(Format(CAB_NAME,[AtrackName]));// color{$IFDEF VER220}// transparencia + color
Str :='ff'+ RGBToWebColorStr(ATrackColor);{$ELSE}
Str :='ff'+ uProcedures.RGBToWebColorStr(ATrackColor);{$ENDIF}
Str := AnsiReplaceText(Str,'#','');// TrackColor en HEXA
TSTrack.Add(Format(STR_STYLE,[Str, ATrackWidth]));
TSTrack.Add(CAB_LINE);
Con este código generamos la cabecera del fichero. En mi caso he usado una directiva condicional, para la función RGBToWebColorStr, que no está definida en versiones antiguas de la VCL.
Lo siguiente es realizar el recorrido por los puntos, añadiendo información de cada uno de ellos.
dsPoints.First;// recorridowhilenot(dsPoints.Eof)dobegin// Info. de punto
StrLine :=Copy(dsPoints.FieldByName(lonFName).AsString,1,10)+',';
StrLine := StrLine +Copy(dsPoints.FieldByName(latFName).AsString,1,10)+',';
StrLine := StrLine +
dsPoints.FieldByName(eleFName).AsString+' ';// Añadir la linea
TSTrack.Add(StrLine);// Next
dsPoints.Next;end;
dsPoints.First;
// recorrido
while not (dsPoints.Eof) do begin
// Info. de punto
StrLine :=
Copy(dsPoints.FieldByName(lonFName).AsString,1,10) + ',';
StrLine := StrLine +
Copy(dsPoints.FieldByName(latFName).AsString, 1,10) + ',';
StrLine := StrLine +
dsPoints.FieldByName(eleFName).AsString + ' ';
// Añadir la linea
TSTrack.Add(StrLine);
// Next
dsPoints.Next;
end;
Sólo queda finalmente completar el fichero añadiendo los TAG’s de las diferentes secciones que hemos ido creando.
Para facilitar el trabajo he encapsulado la conversión en el procedimiento siguiente:
procedure ConvertTrackToKLM(
ATrackName:string;// Nombre
ATrackDesc:string;//descripción
dsPoints:TDataSet;// Dataset que contiene los datos de los puntos{Nombres de los campos en el Dataset que contienen
Latitud, Longitud, Elevación y Time}
latFieldName, lonFieldName, eleFieldName, timeFieldName:string;var TSTrack:TStrings;// Estructura de salida con el track
ATrackColor:TColor=clBlue;// Color para la linea del track
ATrackWidth:integer=2// Anchura de la línea);
procedure ConvertTrackToKLM(
ATrackName:string; // Nombre
ATrackDesc:string; //descripción
dsPoints:TDataSet; // Dataset que contiene los datos de los puntos
{Nombres de los campos en el Dataset que contienen
Latitud, Longitud, Elevación y Time}
latFieldName, lonFieldName, eleFieldName, timeFieldName:string;
var TSTrack:TStrings; // Estructura de salida con el track
ATrackColor:TColor=clBlue; // Color para la linea del track
ATrackWidth:integer=2 // Anchura de la línea);
Y una llamada de ejemplo a este procedimiento podrías ser la siguiente:
TS := TStringList.Create();try// convertir a KML
ConvertTrackToKLM(
FormData.cdsTrack.FieldByName('name').AsString,// trackName'Descripción del track',// descripción
FormData.cdsPoints,// Dataset con los puntos'lat','lon','ele','time',// Nombre de los campos
TS);// Fichero de salida
newName :=ChangeFileExt(Self.TrackPath,'.KML');
TS.SaveToFile(newName);finallyFreeAndNil(TS);end;
TS := TStringList.Create();
try
// convertir a KML
ConvertTrackToKLM(
FormData.cdsTrack.FieldByName('name').AsString, // trackName
'Descripción del track', // descripción
FormData.cdsPoints, // Dataset con los puntos
'lat', 'lon','ele','time', // Nombre de los campos
TS);
// Fichero de salida
newName := ChangeFileExt(Self.TrackPath, '.KML');
TS.SaveToFile(newName);
finally
FreeAndNil(TS);
end;
Partiendo de la aplicación que sirvió de ejemplo en la entrada <Google Maps en Delphi>, he generado otro ejemplo que nos permitirá integrar la carga de un fichero GPX (utilizando el XML Mapper de Delphi), la conversión del fichero GPX a formato KML y la visualización de la ruta creada utilizando Google Maps integrado en nuestra aplicación, como prometí en la última entrada sobre el tema.
He probado a generar un video del ejemplo para ver el funcionamiento.
[youtube DLoX_KVSJ7c nolink]
Para las pruebas podeís descargar las rutas de ejemplo de la última entrada o buscar por internet en la infinidad de páginas que hay que las publican
Embarcadero MVP.
Analista y Programador de Sistemas Informáticos.
Estudios de Informática (Ingeniería Técnica Superior) en la UPC (Universidad Politécnica de Barcelona).
Llevo utilizando Delphi desde su versión 3. Especialista en diseño de componentes, Bases de Datos, Frameworks de Persistencia, Integración Continua, Desarrollo móvil,…
Haciendo pruebas con el componente TidHTTP me ha surgido este ejemplo y me parece interesante mostrar el proceso completo que he seguido para descargar contenido desde una web. En este caso he accedido a las imágenes en formato PNG, pero es extensible a cualquier otros contenidos.
En un primer paso se trata de obtener el contenido (source) desde una página web y a partir de ahí, extraer los links (en mi caso correspondientes a imágenes en formato PNG) y descargar los ficheros para almacenarlos en disco. Para ello utilizaremos el control TidHTTP de la Indy.
Para obtener el código fuente de la página podemos utilizar un código como este:
// URL de la página
StrURL :=...;// Crear componente para acceder
IdH := TIdHTTP.Create(nil);// proteccion para liberartry// proteccion por errortry// Obtenemos la pagina
Resp := IdH.Get(Trim(StrURL));// recuperamos la respuesta
TSContent.Text:= Resp;excepton E:Exception dobegin
MessageDlg('Error al acceder a la página. '+#13+#10+'Mensaje: '+
E.Message, mtError,[mbOK],0);end;end;// tryfinally
IdH.Free;end;
// URL de la página
StrURL := ... ;
// Crear componente para acceder
IdH := TIdHTTP.Create(nil);
// proteccion para liberar
try
// proteccion por error
try
// Obtenemos la pagina
Resp := IdH.Get(Trim(StrURL));
// recuperamos la respuesta
TSContent.Text := Resp;
except
on E:Exception do begin
MessageDlg('Error al acceder a la página. '+#13+#10+
'Mensaje: ' +
E.Message, mtError, [mbOK], 0);
end;
end; // try
finally
IdH.Free;
end;
Obtendremos como resultado del Get el siguiente texto, que corresponde al código fuente de la página:
Lo siguiente que vamos a hacer es un simple «parseo» buscando los enlaces que nos interesen. En nuestro ejemplo queremos links a imágenes en formato png. Utilizando funciones de la clase SysUtils podemos obtener sin mayor problemas los links contenidos en el código; Si probáis con la página de ejemplo, deberíais obtener un único link:
Por último, nos queda ver el código necesario para descargar la imagen y almacenarla en disco. Para ello podemos utilizar de nuevo el componente TidHTTP. En mi caso, y en previsión de que pueda haber varias imágenes a descargar, he creado una clase derivada de TThread para ello:
{: Clase para descargar una imagen y almacenarla en disco.}
TDownImageThread =class(TThread)private
FURLImage:string;
FPathImage:string;
FFileNameImage:string;// Internas
ImageName:string;
PathURL:string;// Componente
idH:TidHTTP;public// redefinir métodosconstructor Create(AURL:string; AOutPathImages:string);destructor Destroy;override;procedure Execute;override;{: URL de la imagen a descargar. }property URLImage:stringread FURLImage write FURLImage;{: Path de disco local donde voy a almacenar la imagen.}property PathImage:stringread FPathImage;{: Nombre completa (path+Nombre) de la imagen almacenada en disco local}property FileNameImage:stringread FFileNameImage;end;
{: Clase para descargar una imagen y almacenarla en disco.}
TDownImageThread = class(TThread)
private
FURLImage: string;
FPathImage: string;
FFileNameImage: string;
// Internas
ImageName: string;
PathURL: string;
// Componente
idH:TidHTTP;
public
// redefinir métodos
constructor Create(AURL:string; AOutPathImages:string);
destructor Destroy; override;
procedure Execute; override;
{: URL de la imagen a descargar. }
property URLImage:string read FURLImage write FURLImage;
{: Path de disco local donde voy a almacenar la imagen.}
property PathImage:string read FPathImage;
{: Nombre completa (path+Nombre) de la imagen almacenada en disco local}
property FileNameImage:string read FFileNameImage;
end;
Al crear el Thread (método Create) ya pasamos como parámetros, la URL de la imagen a descargar y el Directorio inicial donde se van a guardar las imágenes encontradas.
El método Execute creamos un componente TidHTTP (igual a como lo hemos hecho antes), pero en este caso utilizamos un TFileStream para recoger el fichero que descargamos y almacenarlo en disco.
Utilizamos el Path de la URL para generar diferentes directorios para las diferentes imágenes:
//: recupara la imagen y la guarda en discoprocedure TDownImageThread.Execute();var
Stream:TFileStream;
IdH:TidHTTP;
path:string;
dir:string;begin// Directorio de salida
dir := AnsiReplaceText(PathURL,'/', STR_EMPTY);// Nombre vacíoif(ImageName = STR_EMPTY)thenbegin
Exit;end;// Path de salida
path :=IncludeTrailingBackslash(IncludeTrailingBackslash(PathImage)+ dir)+ ImageName;// Crearlo por si no existeForceDirectories(ExtractFilePath(path));try// Stream para la imagfen
Stream := TFileStream.Create(path, fmCreate);try//recuperar la imagen
IdH := TidHTTP.Create(nil);
IdH.AllowCookies:=True;// protecciontry
IdH.Get(Trim( FURLImage), Stream);except// Error al descargar la imagen//.. Volcarlo al logend;finally// Liberar
idH.Free;
Stream.Free;end;// Path de salida
FFileNameImage := path;except// error al crear el fichero//... Logend;end;
//: recupara la imagen y la guarda en disco
procedure TDownImageThread.Execute();
var
Stream:TFileStream;
IdH:TidHTTP;
path:string;
dir:string;
begin
// Directorio de salida
dir := AnsiReplaceText(PathURL, '/', STR_EMPTY);
// Nombre vacío
if (ImageName = STR_EMPTY) then begin
Exit;
end;
// Path de salida
path := IncludeTrailingBackslash(IncludeTrailingBackslash(PathImage)
+ dir) + ImageName;
// Crearlo por si no existe
ForceDirectories(ExtractFilePath(path));
try
// Stream para la imagfen
Stream := TFileStream.Create(path, fmCreate);
try
//recuperar la imagen
IdH := TidHTTP.Create(nil);
IdH.AllowCookies := True;
// proteccion
try
IdH.Get(Trim( FURLImage), Stream);
except
// Error al descargar la imagen
//.. Volcarlo al log
end;
finally
// Liberar
idH.Free;
Stream.Free;
end;
// Path de salida
FFileNameImage := path;
except
// error al crear el fichero
//... Log
end;
end;
El resultado se puede ver en este ejemplo.
Una vez descargadas las imágenes se muestran en un componente en la misma aplicación, y desde ahí podemos acceder a la información de la imagen y realizar algunas acciones sobre ellas.
Para realizar pruebas podéis introducir, por ejemplo direcciones como:
Por lo tanto para poder descargar tanto la página inicial (de donde se extraen los enlaces), como las imágenes en si, hay que modificar ligeramente el programa.
Hay que utilizar el componente TIdSSLIOHandlerSocket para poder utilizar SSL y añadir al proyecto as librerías de acceso a SSL.
No hay muchas modificaciones en el código, pero si queréis verlas, dejaré el código antiguo y el nuevo (compilado en Delphi 7) para que podáis compararlos.
NOTA: Para generar las imágenes en disco PNG he utilizado el componente “Portable Network Graphics Delphi” de Gustavo Huffenbacher Daud, que podéis encontrar y descargar de forma gratuita de Internet.
Embarcadero MVP.
Analista y Programador de Sistemas Informáticos.
Estudios de Informática (Ingeniería Técnica Superior) en la UPC (Universidad Politécnica de Barcelona).
Llevo utilizando Delphi desde su versión 3. Especialista en diseño de componentes, Bases de Datos, Frameworks de Persistencia, Integración Continua, Desarrollo móvil,…
Desde hace un tiempo estoy trabajando con ficheros de rutas o «Tracks» que provienen de dispositivos GPS (Global Positioning System). Un track o una ruta, no es más que una sucesión de coordenadas de Latitud y Longitud. Adicionalmente esos puntos pueden contener más información como Elevación, Fecha y hora,…
Existen infinidad de formatos para almacenar «Tracks» muchos de ellos provenientes de marcas concretas de receptores GPS (Garmin, DeLorme, Holux, Magellan, Tomtom…) y otros provenientes de programas de software conocidos como OziExplorer, CompeGPS o los relacionados con Google Maps.
Además existe también un formato genérico y bastante aceptado como estandard, que almacena puntos con estructura XML. Es el formato GPX (GPS eXchange Format).
La idea de esta entrada es generar código para trabajar desde delphi con un track en formato genérico GPX. Poder abrirlo y cargar los valores sin problemas y posteriormente exportarlo a KML. Aunque una vez recuperados los datos desde Delphi, la exportación a otros formatos será muy similar.
Leer un archivo GPX
Lo primero que vamos a hacer para poder leer un archivo de tipo GPX es generar una pieza intermedia que nos permita fácilmente trasladar los datos desde un XML (con una estructura determinada) a un Dataset (en nuestro caso un TClientDataset).
NOTA: Para utilizar como ejempos, en la web podéis encontrar infinidad de archivos de este tipo; Yo para las pruebas utilizaré un par de archivos de rutas distintos que podeís descargar aquí.
Para ello utilizaremos la herramienta XML Mapper de Delphi, accesible desde el menú de Tools (o también desde el Menu de programa, en el mismo grupo donde está instalado Delphi).
PASO1: Abrir fichero GPX; Aunque la extensión no es XML, sí lo es la estructura del fichero, así que la herramienta reconocerá los campos y en la parte izquierda del programa podremos ver la estructura del fichero con los diferentes campos disponibles. En este caso vemos que el fichero contiene una serie de campos correspondientes al track, como creator, versión,… y luego un «Nested Dataset»; Un Dataset «anidado», que contiene todos los puntos del track con los campos Lat (latitud), Lon (longitud), Ele (altura o elevación) y time (fecha/hora).
PASO2: Generar un Datapacket; Una vez que la estructura se ha leído correctamente podemos generar lo que XML Mapper llama un DataPacket; Que viene a ser algo así como los datos que tenemos en el XML, pero en formato de Dataset y además nos permitirá guardar el archivo de transformación (que es realmente lo importante).
Este archivo de transformación nos permitirá posteriormente leer cualquier fichero XML con la misma estructura que este y realizar la traducción a «formato dataset» de forma automática.
Para ello Desde el menú contextual seleccionamos todos los campos (Select All) y en el menú de Create escogemos Datapacket from XML (Ctrl+D). Con ello habremos generado nuestro fichero de transformación y el Dataset final.
En la parte derecha de la pantalla podemos ver la misma estructura que teníamos en el XML pero en «formato datapacket». Podemos comprobar cómo se ha realizado la transformación y que los datos se leen correctamente utilizando un botón que hay en la parte inferior de la pantalla llamado «Create and Test Transformation«.
Esto nos abre un Grid y nos muestra los datos provenientes del Dataset que se ha generado utilizando el fichero de transformación. Esto es lo que posteriormente nosotros queremos repetir en nuestro programa. Además como la estructura XML posee un «DataSet anidado», si pulsamos doble click sobre el campo trkpt, se abre un segundo Dataset que contiene todos los registros de los puntos que hay en la ruta.
En este ejemplo hemos realizado una traducción directa, pero el programa (antes de generar la transformación) nos permite modificar los campos que queremos «mapear» entre el origen (el XML) y el destino (Datapacket/Dataset), de forma que podemos aplicar determinados cambios, variaciones o filtros a los datos de entrada.
PASO3: Guardar el fichero de transformación; Una vez realizado esto sólo nos queda (lo más importante) guardar el fichero de transformación, que nos asegura que a partir de este momento podremos repetir esta misma transformación (con cualquier otro fichero de entrada que sea del mismo formato que este). Desde el Menú de File seleccionamos Save Transformation. Asignaremos anuestro fichero el nombre de GPX_To_DP.xtr (GPX to DataPacket).
Utilizar un fichero de transformación
Una vez que hemos generado nuestro fichero de tranformación GPX_To_DP.xtr, vamos a ver cómo nos puede ser útil en un programa delphi para tratar ficheros de rutas guardaos en formato GPX. Para ello utilizaremos 2 TClientDataset, no para la ruta y otro para los puntos de la ruta (a modo de Master-Detail).
Creamos un un nuevo proyecto y un formulario con los siguientes componentes:
Un DBGrid, un TDatasource y un TClientDataset para los puntos
Un DBGrid, un TDatasource, un TClientDataset y un TXMLTransformProvider para los datos del track.
Cada uno de los DBGrid va ligado al TDataset y cada uno de estos al TClientDataset.
En el XMLTransformProvider deberemos colorcar en la propiedad TransformRead, el fichero de transformación que hemos generador mediante la herramienta XML Mapper.
Como hemos visto, el campo trkpt del TClientDataset asociado al track es un campo de tipo TDataset (en concreto un TDatasetField). Para mostrarlo utilizaremos el segundo TClientDataset que hemos colocado en el formulario (cdsPoints).
En la propiedad DatasetField del TClientDataset cdsPoints asignamos el campos del primer Dataset (en este caso trkpt). Con esto visualizaremos en el segundo DBGrid los puntos que contiene el track.
Una vez hecho esto, bastará con las siguentes líneas de código para conseguir leer el contenido de cualquier fichero con formato GPX. Únicamente hay que asignar el fichero de transformación y el fichero fuente XML.
var
b:Boolean;
path:string;begin// Abrir el diálogo
b := OpenDialog1.Execute;if(b)thenbegin// Asignar fichero de transformación
path :=ExtractFilePath(Application.ExeName)+'GPX_To_DP.xtr';
XMLTransformProvider.TransformRead.TransformationFile:= path;// Fichero XML (GPX) de ruta
XMLTransformProvider.XMLDataFile:= OpenDialog1.FileName;// Abrir el Dataset
cdsTrack.Open;end;end;
var
b:Boolean;
path:string;
begin
// Abrir el diálogo
b := OpenDialog1.Execute;
if (b) then begin
// Asignar fichero de transformación
path := ExtractFilePath(Application.ExeName) + 'GPX_To_DP.xtr';
XMLTransformProvider.TransformRead.TransformationFile := path;
// Fichero XML (GPX) de ruta
XMLTransformProvider.XMLDataFile := OpenDialog1.FileName;
// Abrir el Dataset
cdsTrack.Open;
end;
end;
El resultado son los 2 DBGrid con los datos del Track:
Podéis descargar los ficheros de ejemplo de las rutas y el código fuente y binarios en los siguientes enlaces:
Una vez que tenemos los datos en formato accesible (fácil) para nosotros, lo siguiente que vamos a proponer es exportarlos a un formato KML (Google Maps) y visualizar la ruta/track sobre un Mapa de Google Maps.
Algo como lo que se ve a continuación, aunque eso lo dejo para la próxima entrada:
Embarcadero MVP.
Analista y Programador de Sistemas Informáticos.
Estudios de Informática (Ingeniería Técnica Superior) en la UPC (Universidad Politécnica de Barcelona).
Llevo utilizando Delphi desde su versión 3. Especialista en diseño de componentes, Bases de Datos, Frameworks de Persistencia, Integración Continua, Desarrollo móvil,…
Hace tiempo me topé con un tema similar a este, aunque con un enfoque diferente; En aquel caso se trataba de que el usuario pudiera crear su propio menú personalizado dentro de una aplicación. Es decir, que además de las opciones propias de la aplicación, el usuario pudiera configurarse un menú con las opciones que más deseara utilizar o tener más «a mano». En este caso, y a partir de este hilo en los foros del Clubdelphi, se ha planteado la posibilidad de que un usuario pueda crearse sus propios «accesos directos» a opciones del menú.
La solución en aquel momento pasó por «volcar» el contenido del menú a otro componente (en ese caso un TreeView, de forma similar a cómo se ve en este ejemplo) y desde ese, generar la estructura del nuevo punto de menú arrastrando elementos.
Para el problema de generar accesos directos, se me antoja que se pueda usar un sistema similar.
(1) «Volcar» el contenido del menú hasta otro componente que nos permita trabajar con los elementos del menú (ya que ni el menú ni los ítems poseen opciones para arrastrar -Drag & Drop-). Este esta caso vamos a utilizar un componente (TListBox) donde almacenaremos los elementos y los apuntadores a los ítems del menú (propiedad Objects).
procedure TFormMain.Button1Click(Sender:TObject);var
i:integer;
str:string;// Recursiva para obtener los subItemsprocedure GetItems(mi:TMenuItem);var
i:Integer;beginfor i :=0to(mi.Count-1)dobegin
Str := mi.Items[i].Caption;
ListBox1.Items.AddObject(Str, mi.Items[i]);// SubItems de este
GetItems(mi.Items[i]);end;end;begin// Recorerr menu principalfor i :=0to(MainMenu1.Items.Count-1)dobegin
Str := MainMenu1.Items[i].Caption;
ListBox1.Items.AddObject(Str, MainMenu1.Items[i]);// SubItems de este
GetItems(MainMenu1.Items[i]);end;end;
procedure TFormMain.Button1Click(Sender: TObject);
var
i:integer;
str:string;
// Recursiva para obtener los subItems
procedure GetItems(mi:TMenuItem);
var
i:Integer;
begin
for i := 0 to (mi.Count - 1) do begin
Str := mi.Items[i].Caption;
ListBox1.Items.AddObject(Str, mi.Items[i]);
// SubItems de este
GetItems(mi.Items[i]);
end;
end;
begin
// Recorerr menu principal
for i := 0 to (MainMenu1.Items.Count - 1) do begin
Str := MainMenu1.Items[i].Caption;
ListBox1.Items.AddObject(Str, MainMenu1.Items[i]);
// SubItems de este
GetItems(MainMenu1.Items[i]);
end;
end;
Con este código poblamos el ListBox con los Caption(Text) de los elementos del menú, y lo que es más importante, los apuntadores a cada elementos que se guardar al utilizar AddObject.
(2) ¿Cómo crear un acceso directo que permita ejecutar una opción de menú? Para ello podemos utilizar un TImage que sobre el cual programaremos el evento OnDblClick/OnClick.
Crear el componente es sencillo, y se puede ver código de ejemplo de cómo hacerlo en estas entradas:
La idea es que cada «acceso directo» posea un apuntador al elemento de menú correspondiente para poder ejecutar el código programado en el OnClick o en la TAction asociada a ese elemento del menú. Lo lógico sería utilizar una propiedad del propio componente (Data, Object,….) que nos permitiera enlazar directamente. No es el caso del TImage, así que en el ejemplo utilizaremos el propio ListBox como el «contenedor» de los apuntadores (como una lista intermedia), aunque como he dicho, la solución ideal, sería que cada «acceso directo» tuviera un puntero «directo» al TMenItem asociado.
El código paras crear el componente y gestionar esa asociación podría ser similar a este:
procedure TFormMain.Button2Click(Sender:TObject);var
img:TImage;
mi:TMenuItem;beginIf ListBox1.ItemIndex=-1thenbegin
MessageDlg('Selecciona un elemento de la llista', mtWarning,[mbOK],0);
Exit;end;// Item del menu
mi := TMenuItem(ListBox1.Items.Objects[ListBox1.ItemIndex]);// Tiene asignado el OnClick?ifAssigned(mi.OnClick)thenbegin// Nadaendelsebegin// Tiene asignada la action?ifAssigned(mi.Action)thenbegin//signado OnExecuteifAssigned(mi.Action.OnExecute)thenbegin// Nadaendelsebegin
MessageDlg('Ese elemento no tiene nada que hacer asignado',
mtWarning,[mbOK],0);
Exit;end;endelsebegin
MessageDlg('Ese elemento no tiene nada que hacer asignado',
mtWarning,[mbOK],0);
Exit;end;end;
Randomize;// Elemento seleccionado
img := TImage.Create(nil);
img.Parent:= Panel1;
img.Height:=32;
img.Width:=32;
img.Left:=Random(panel1.Width- img.Width);
img.Top:=Random(panel1.Height- img.Height);
img.Stretch:=True;
img.Transparent:=True;// El TAG es la posicion en la lista
img.Tag:= ListBox1.ItemIndex;// Item del menu
mi := TMenuItem(ListBox1.Items.Objects[ListBox1.ItemIndex]);// Asignar la imagen
ImageList1.GetBitmap(mi.ImageIndex, img.Picture.Bitmap);// Asignar el evento
img.OnClick:= MyImgClick;end;
procedure TFormMain.Button2Click(Sender: TObject);
var
img:TImage;
mi:TMenuItem;
begin
If ListBox1.ItemIndex = -1 then begin
MessageDlg('Selecciona un elemento de la llista', mtWarning, [mbOK], 0);
Exit;
end;
// Item del menu
mi := TMenuItem(ListBox1.Items.Objects[ListBox1.ItemIndex]);
// Tiene asignado el OnClick?
if Assigned(mi.OnClick) then begin
// Nada
end
else begin
// Tiene asignada la action?
if Assigned(mi.Action) then begin
//signado OnExecute
if Assigned(mi.Action.OnExecute) then begin
// Nada
end
else begin
MessageDlg('Ese elemento no tiene nada que hacer asignado',
mtWarning, [mbOK], 0);
Exit;
end;
end
else begin
MessageDlg('Ese elemento no tiene nada que hacer asignado',
mtWarning, [mbOK], 0);
Exit;
end;
end;
Randomize;
// Elemento seleccionado
img := TImage.Create(nil);
img.Parent := Panel1;
img.Height := 32;
img.Width := 32;
img.Left := Random(panel1.Width - img.Width);
img.Top := Random(panel1.Height - img.Height);
img.Stretch := True;
img.Transparent := True;
// El TAG es la posicion en la lista
img.Tag := ListBox1.ItemIndex;
// Item del menu
mi := TMenuItem(ListBox1.Items.Objects[ListBox1.ItemIndex]);
// Asignar la imagen
ImageList1.GetBitmap(mi.ImageIndex, img.Picture.Bitmap);
// Asignar el evento
img.OnClick := MyImgClick;
end;
Primero se realizan unas comprobaciones para detectar si posee alguna acción asignada (sea OnClick o TAction) y posteriormente se crea el TImage, se configura y se asigna como TAG el ItemIndex del ListBox (que es este caso estamos utilizando como estructura intermedia para guardar el apuntador al TMenuItem).
Finalmente sólo quedar crear el procedimiento MyImgClick, que ejecutará el código asignado al elemento del menú cuando se presione sobre la imagen asociada. Se incluyen comprobaciones similares a las anteriores, por si el elemento no tiene nada asignado y se tiene en cuenta también que haya código en el OnClick del TMenuItem o exista una TAction asociada.
var
i:integer;
str:string;
mi:TMenuItem;begin// Test del senderifnot(sender is TImage)thenbegin
Exit;endelsebegin
i := TImage(Sender).Tag;
Str := ListBox1.Items[i];end;// Acceder a la opción de menú
mi := TMenuItem(ListBox1.Items.Objects[i]);// Asignado código?ifAssigned(mi.OnClick)thenbegin
mi.OnClick(nil);
Exit;endelsebegin// Tiene asignada la action?ifAssigned(mi.Action)thenbegin// Asignado OnExecuteifAssigned(mi.Action.OnExecute)thenbegin
mi.Action.OnExecute(nil);
Exit;end;endend;
MessageDlg('No hay nada asignado a esa opción...', mtInformation,[mbOK],0);end;
var
i:integer;
str:string;
mi:TMenuItem;
begin
// Test del sender
if not (sender is TImage) then begin
Exit;
end
else begin
i := TImage(Sender).Tag;
Str := ListBox1.Items[i];
end;
// Acceder a la opción de menú
mi := TMenuItem(ListBox1.Items.Objects[i]);
// Asignado código?
if Assigned(mi.OnClick) then begin
mi.OnClick(nil);
Exit;
end
else begin
// Tiene asignada la action?
if Assigned(mi.Action) then begin
// Asignado OnExecute
if Assigned(mi.Action.OnExecute) then begin
mi.Action.OnExecute(nil);
Exit;
end;
end
end;
MessageDlg('No hay nada asignado a esa opción...', mtInformation, [mbOK], 0);
end;
Se puede mejorar y «refinar» bastante más, pero creo que la idea queda clara. A partir de aquí cada uno que «añada» lo que quiera. Cualquier sugerencia será bien recibida.
Embarcadero MVP.
Analista y Programador de Sistemas Informáticos.
Estudios de Informática (Ingeniería Técnica Superior) en la UPC (Universidad Politécnica de Barcelona).
Llevo utilizando Delphi desde su versión 3. Especialista en diseño de componentes, Bases de Datos, Frameworks de Persistencia, Integración Continua, Desarrollo móvil,…
Se trata de un ejemplo sencillo que puede servir a quien esté comenzando con la creación de componentes en runtime y asignación de eventos. En este caso se trata de un visualizador de imágenes, que crea componentes TImage dentro de un TScrollBox, a modo de miniaturas, para luego visualizar esas imágenes en un control TImage a tamaño mayor.
Se utiliza también una TObjectList para almacenbar los objetos (componentes TImage y TPanel) que se van creando en ejecución para posteriormente poder acceder a ellos. Para crear los componentes se utiliza un código com este:
var
im:TImage;
i:Integer;
pnl:TPanel;begin// Para cada imagenfor i :=0to(imList.Count-1)dobegin// Crear el control imagen
im := TImage.Create(nil);// Asignar propiedades
im.Parent:= ScrollBox1;
im.Height:= ScrollBox1.Height;
im.Width:= im.Height;
im.Top:=0;
im.Left:=((im.Width+15{panel}+4)* i);
im.Transparent:=true;
im.Align:= alLeft;
im.Stretch:=True;
im.Proportional:=True;// Cargar
im.Picture.LoadFromFile(imList[i]);// vebto OnClick
im.OnClick:= ClickImage;// Añadirla a la lista de objetos
oList.Add(im);// Separados
pnl := TPanel.Create(nil);
pnl.Parent:= ScrollBox1;
pnl.Left:= im.Left+5;
pnl.Width:=15;
pnl.Height:= im.Height;
pnl.Align:= alLeft;
pnl.Color:= clBlack;// Aadirlo a la lista de objetos
oList.Add(pnl);end;
var
im:TImage;
i:Integer;
pnl:TPanel;
begin
// Para cada imagen
for i := 0 to (imList.Count - 1) do begin
// Crear el control imagen
im := TImage.Create(nil);
// Asignar propiedades
im.Parent := ScrollBox1;
im.Height := ScrollBox1.Height;
im.Width := im.Height;
im.Top := 0;
im.Left := ((im.Width + 15{panel} + 4) * i);
im.Transparent := true;
im.Align := alLeft;
im.Stretch := True;
im.Proportional := True;
// Cargar
im.Picture.LoadFromFile(imList[i]);
// vebto OnClick
im.OnClick := ClickImage;
// Añadirla a la lista de objetos
oList.Add(im);
// Separados
pnl := TPanel.Create(nil);
pnl.Parent := ScrollBox1;
pnl.Left := im.Left + 5;
pnl.Width := 15;
pnl.Height := im.Height;
pnl.Align := alLeft;
pnl.Color := clBlack;
// Aadirlo a la lista de objetos
oList.Add(pnl);
end;
Embarcadero MVP.
Analista y Programador de Sistemas Informáticos.
Estudios de Informática (Ingeniería Técnica Superior) en la UPC (Universidad Politécnica de Barcelona).
Llevo utilizando Delphi desde su versión 3. Especialista en diseño de componentes, Bases de Datos, Frameworks de Persistencia, Integración Continua, Desarrollo móvil,…
Puede ser algo trivial para los que llevamos un tiempo en esto, pero no tanto para los que empiezan; Reconozco que yo al principio también me liaba con los Canvas, los Rect, los ClientRect y no sabía cual copiar en cada cual. ;-)
El código para unir cuatro imágenes en una sóla formando un cuadro, es muy sencillo y sería algo así:
var
ARect:TRect;
path:string;begin// Añadir los 4 canvas a la nueva imagen
ARect.Left:=0; ARect.Top:=0; ARect.Right:=48; ARect.Bottom:=48;
imgDestino.Canvas.CopyRect(ARect, img1.Canvas, img1.ClientRect);
ARect.Left:=48; ARect.Top:=0; ARect.Right:=92; ARect.Bottom:=48;
imgDestino.Canvas.CopyRect(ARect, img2.Canvas, img2.ClientRect);
ARect.Left:=0; ARect.Top:=48; ARect.Right:=48; ARect.Bottom:=92;
imgDestino.Canvas.CopyRect(ARect, img3.Canvas, img3.ClientRect);
ARect.Left:=48; ARect.Top:=48; ARect.Right:=92; ARect.Bottom:=92;
imgDestino.Canvas.CopyRect(ARect, img4.Canvas, img4.ClientRect);// Path para grabarla a disco
path :=ChangeFileExt(Application.ExeName,'.bmp');
imgDestino.Picture.SaveToFile(path);
MessageDlg('Se ha guardado la imagen con el nombre: '+
path, mtInformation,[mbOK],0);
var
ARect:TRect;
path:string;
begin
// Añadir los 4 canvas a la nueva imagen
ARect.Left := 0; ARect.Top := 0; ARect.Right := 48; ARect.Bottom := 48;
imgDestino.Canvas.CopyRect(ARect, img1.Canvas, img1.ClientRect);
ARect.Left := 48; ARect.Top := 0; ARect.Right := 92; ARect.Bottom := 48;
imgDestino.Canvas.CopyRect(ARect, img2.Canvas, img2.ClientRect);
ARect.Left := 0; ARect.Top := 48; ARect.Right := 48; ARect.Bottom := 92;
imgDestino.Canvas.CopyRect(ARect, img3.Canvas, img3.ClientRect);
ARect.Left := 48; ARect.Top := 48; ARect.Right := 92; ARect.Bottom := 92;
imgDestino.Canvas.CopyRect(ARect, img4.Canvas, img4.ClientRect);
// Path para grabarla a disco
path := ChangeFileExt(Application.ExeName, '.bmp');
imgDestino.Picture.SaveToFile(path);
MessageDlg('Se ha guardado la imagen con el nombre: ' +
path, mtInformation, [mbOK], 0);
Si en lugar de unirlas formando un cuadro, se quieren unir de otra forma (las 4 en línea, por ejemplo), basta con cambiar las coordenadas de destino.
Si hubiera que cambiar el tamaño final, por ejemplo para reducirlo hasta el de las imágenes originales, se podrían aplicar técnicas de antialiasing al redimensionar.
Embarcadero MVP.
Analista y Programador de Sistemas Informáticos.
Estudios de Informática (Ingeniería Técnica Superior) en la UPC (Universidad Politécnica de Barcelona).
Llevo utilizando Delphi desde su versión 3. Especialista en diseño de componentes, Bases de Datos, Frameworks de Persistencia, Integración Continua, Desarrollo móvil,…