Mostrando postagens com marcador Script. Mostrar todas as postagens
Mostrando postagens com marcador Script. Mostrar todas as postagens

19 de dezembro de 2012

Personalizando a apresentação de mapas com o Google Maps

Na maioria das vezes, as informações apresentadas num mapa produzido através da API do Google Maps são suficientes para que o usuário rapidamente consiga ver um endereço e seu entorno ou a rota a ser seguida para ir de um ponto a outro. Em certas situações, no entanto, pode ser necessário adicionar ao mapa outras informações para melhor transmitir a ideia correta que se deseja.

Esse tipo de necessidade foi exemplificado quando falei sobre o uso de marcações num mapa para apontar locais de interesse nas proximidades de um determinado endereço. O exemplo pode ser encontrado neste link.

A API do Google Maps nos permite acrescentar informações extras aos mapas através do conceito de camadas que são sobrepostas ao mapa, chamadas de overlays. Para isso, a API disponibiliza um conjunto de classes javascript. Além da já citada Marker, há classes para traçar linhas, delimitar áreas circulares ou retangulares e até mesmo para projetar uma imagem personalizada sobre uma área do mapa.

O overlay mais simples é o Polyline, usado para traçar linhas por sobre o mapa. O quadro abaixo traz código javascript que une com uma linha reta 2 pontos do mapa: a sede da ABC71 e a praça da Sé, em São Paulo:
var mapOptions = {zoom: 10,mapTypeId: google.maps.MapTypeId.ROADMAP};
map = new google.maps.Map(document.getElementById('map_canvas'), mapOptions);
geocoder = new google.maps.Geocoder();

var endDe = new google.maps.LatLng(-23.566146,-46.652579);
var endAte = new google.maps.LatLng(-23.550563,-46.633101);
var request = { location: endDe, region: 'BR'};

geocoder.geocode (request, function(response, status) {
if (status == google.maps.GeocoderStatus.OK) {
map.fitBounds (new google.maps.LatLngBounds(endDe, endAte));
/* ... */
var pontos = new google.maps.MVCArray ();
pontos.push (endDe);
pontos.push (endAte);

var rect = new google.maps.Polyline ({
path: pontos,
map:map,
strokeWeight: 5,
strokeColor: 'blue',
visible: true
});
}
});
Após as inicializações de praxe para o ambiente do Maps, o código solicita a geocodificação do endereço da ABC71 (na verdade, sua latitude/longitude).
Em resposta à conclusão da geocodificação, isto é, assim que o mapa com o endereço solicitado está pronto, eu crio uma lista MVCArray e adiciono a ela os dois pontos que definem a reta desejada.

Em seguida, instancio a classe Polyline propriamente dita, fornecendo-lhe a lista de pontos. Também indico o mapa onde a linha será traçada (map), a espessura da linha em pixels (strokeWeight) e a cor dela (strokeColor). As cores podem ser informadas tanto pelo nome HTML quanto pela notação RGB hexadecimal.

Posso, por exemplo, demarcar um bairro inteiro com esse recurso? Sim, mas você terá que montar a lista de pontos o mais completa possível para que o desenho resultante tenha uma precisão adequada. O Polyline produz linhas abertas; para traçar linhas fechadas com uma quantidade arbitrária de pontos use a classe Polygon.

A função fitBounds, usada no início da resposta, apenas aplica um nível de zoom apropriado para garantir que ambos os endereços sejam exibidos num único quadro, sem que o usuário precise interagir com ele.

O código analisado produz o mapa a seguir:
Imagine agora o caso de uma pizzaria que queira divulgar num mapa o raio de ação onde ela faz entregas. O overlay produzido pela classe Circle permite dar destaque a uma área circular, dado um centro e um raio em metros. Veja o exemplo:
var endDe = new google.maps.LatLng(-23.566146,-46.652579);
var request = { location: endDe, region: 'BR'};

geocoder.geocode (request, function(response, status) {
if (status == google.maps.GeocoderStatus.OK) {
map.setCenter(endDe);
/* ... */
var circleOpts = ({
map: map,
center: endDe,
editable: false,
fillColor: 'yellow',
fillOpacity: 0.4,
radius: 3500,
strokeColor: '#0099ff',
strokeWeight: 4,
visible: true });
var circle = new google.maps.Circle (circleOpts);
}
});
Como no caso da linha, para o círculo também valem as propriedades que indicam o mapa, a cor do traço e sua espessura. Mas, ao invés de uma lista de pontos, informamos apenas o centro; no código anterior, usei o próprio endereço da ABC71 para isso. A definição do círculo fica completa com a propriedade radius - o raio, fornecido em metros; no exemplo, o raio terá 3500 metros (ou 3,5 Km).

Outra configuração disponível diz respeito ao preenchimento da área delimitada pelo círculo. A propriedade fillColor determina a cor do preenchimento enquanto a fillOpacity controla o grau de transparência, sendo que o valor 1 significa totalmente opaco (o mapa sob o círculo não é visto) e o valor 0 suspende o preenchimento.

A propriedade editable, quando ativa, permite ao usuário redimensionar o círculo. Eu a desliguei pois ela não faz sentido no contexto do exemplo. O resultado é o mapa exibido no quadro abaixo:

Os mesmos conceitos valem para a classe Rectangle. Além disso, é permitido trabalhar com vários overlays num mesmo mapa, se for necessário. Os exemplos publicados nesse post podem ser acessados respectivamente nos seguintes links : Overlay com Linha e Overlay com Círculo.

2 de outubro de 2012

Trabalhando com marcações em mapas com a versão 3 da API do Google Maps

Além dos tradicionais recursos de exibir o mapa para um determinado endereço ou traçar rotas entre endereços fornecidos, a API do Google Maps disponibliza um outro recurso bastante interessante que permite incluir num mapa marcações customizadas. Com isso, você pode marcar pontos de interesse de acordo com as necessidades de sua aplicação. Um exemplo de aplicabilidade desse conceito seria indicar num mapa a localização de estabelecimentos comerciais nas proximidades de um endereço, tais como Caixas 24 Horas, Supermercados, escolas de inglês ou outro qualquer. Obviamente, isso demandará saber de antemão a posição de cada um dos locais passíveis de serem marcados no mapa.

O processo de marcação em si é bastante simples. Após termos ajustado a parametrização inicial do ambiente do Maps e requisitado a exibição de um endereço (veja post Usando a versão 3 da API do Google Maps), basta criar uma instância da classe google.maps.Marker, como no quadro abaixo:
function mostraEnderecoABC71 () {
var lRequest = { address: 'Alameda Santos, 1000, São Paulo - SP, 01418-902', region: 'BR'};
geocoder.geocode( lRequest, trataLocais);
}

function trataLocais (results, status){
if (status == google.maps.GeocoderStatus.OK)
{
map.setCenter(results[0].geometry.location);
var marker = new google.maps.Marker(
{map: map,
position: results[0].geometry.location,
animation: google.maps.Animation.DROP,
icon: 'http://maps.google.com/mapfiles/kml/pal3/icon18.png',
shadow: 'http://maps.google.com/mapfiles/kml/pal3/icon18s.png'
});
/* ... */
}
}

A função trataLocais reproduzida acima é o callback passado para o Geocoder junto com a requisição do endereço a ser apresentado no mapa. No exemplo, a marcação é inserida no próprio endereço requisitado, razão pela qual foi usado o retorno da API results[0].geometry.location como local para fixar a marcação.

Dos parâmetros para criar o marcador, apenas 2 são obrigatórios : o map (representando a instância de Map que gerencia a apresentação do mapa na página) e o position (um par latitude/longitude onde o marcador deverá ser fixado). Os demais parâmetros são auxiliares, úteis para melhorar a apresentação. No exemplo, forneço outros 3 parâmetros : animation (uma animação dinâmica usada para cravar o marcador no mapa; o padrão é o marcador simplesmente aparecer), icon (uma URL apontando a imagem que será usada como marcador; o padrão é ) e shadow (URL com a imagem que representa uma sombra do marcador). Há uma lista com imagens padronizadas e suas respectivas sombras neste link mas pode-se trabalhar com qualquer URL personalisada que se queira.

O mapa abaixo usa esses conceitos pra incluir algumas marcações nas proximidades da sede da ABC71 em São Paulo:

A API prevê ainda a possibilidade de se abrir uma janela para apresentar outros detalhes a respeito de um ponto no mapa. A flexibilidade desse tipo de recurso é grande já que é permitido incluir código HTML para formatar as informações que são apresentadas - embora isso não seja obrigatório.

Do ponto de vista da programação, criar uma janela com informações adicionais sobre um ponto também é simples. Consiste em instanciar a classe google.maps.InfoWindow, providenciando um conteúdo para essa janela. Uma vez instanciada, podemos solicitar que a janela seja aberta num ponto específico do mapa. Complementando a função trataLocais :
function trataLocais (results, status){
if (status == google.maps.GeocoderStatus.OK)
{
/* ... */
google.maps.event.addListener(marker, 'click',
function () {
var infowindow = new google.maps.InfoWindow(
{
content: '<div style="text-align:justify;"><b>Ponto de Interesse</b><br/><br/>ABC71 Soluções em Informática.</div>'
});
infowindow.open(map, marker);
});
}
}
No código javascript do quadro anterior, a abertura da janela de informação é vinculada a um evento de "clique" no marcador. Isto é, ela só aparece se o usuário clicar no marcador em questão. Veja que o conteúdo (content) da janela foi formatado com tags HTML, possibilitando adequar a apresentação ao propósito de sua aplicação. A função open - que exibe a janela - aceita como parâmetros o mapa que estamos usando e o marcador que servirá de referência para a exibição da janela.

No mapa que aparece nesse post, os marcadores podem ser clicados, ação que cria uma janela simples com outras informações sobre o ponto marcado.

O arquivo HTML encontrado neste endereço traz o exemplo completo mostrado aqui no post. Ele simula a recepção em formato XML de informações sobre locais que devem ser marcados no mapa. Para isso, o exemplo utiliza também recursos da biblioteca JQuery.

31 de janeiro de 2012

Informações sobre rotas com a versão 3 da API do Google Maps

No ano passado eu publiquei dois posts mostrando como traçar rotas usando a versão 3 da API do Google Maps. O primeiro tratava de um cenário básico onde consideramos um endereço de origem e outro de destino; o segundo avançava um passo ao permitir a inclusão de um ou mais pontos intermediários ao trajeto. Por razões distintas, várias pessoas me perguntaram como é possível obter a distância total entre a origem e o destino em uma rota, independendo se vamos efetivamente exibir a rota traçada.

Pra refrescar a memória, o quadro a seguir ilustra os comandos e estruturas básicos necessários pra trabalharmos com uma rota.
var directionsService, directionsRenderer;
var enderDe, enderAte;

directionsService = new google.maps.DirectionsService();
directionsRenderer = new google.maps.DirectionsRenderer();
directionsRenderer.setMap(map);

enderDe = 'ALAMEDA SANTOS, 1000, SÃO PAULO - SP, 01418-9028';
enderAte =
'AVENIDA NAÇÕES UNIDAS, 17-17, BAURU - SP, 17013-035';

var request = {
origin:endDe,
destination:endPara,
travelMode: google.maps.DirectionsTravelMode.DRIVING
};

directionsService.route(request, function(response, status) {
if (status == google.maps.DirectionsStatus.OK) {
directionsRenderer.setDirections(response);
}
});)

A linha directionsRenderer.setDirections (response) usa o objeto response para traçar automaticamente a rota no mapa. Esse objeto é do tipo DirectionsResult e é montado pela função route antes de ser repassado como response à nossa função de callback. No entanto, você pode usar manualmente as informações contidas em response sem que a rota precise ser exibida.

De acordo com a documentação, o DirectionsResult possui como única propriedade um Array chamado routes contendo a lista de rotas encontradas pelo serviço para atender a requisição feita. Cada elemento dessa lista traz detalhes de uma rota específica, descrita na forma de um objeto DirectionsRoute, o que inclui detalhes de cada trecho do trajeto, como a distância de cada um (em metros) e o tempo estimado usando o tipo de locomoção escolhido.

Com isso, podemos facilmente obter a distância a ser percorrida para chegar ao destino usando a rota em questão:
directionsService.route(request, function(response, status) {
if (status == google.maps.DirectionsStatus.OK) {
var distancia;
var rota = response.routes[0]; /* Primeira rota */
var etapa = rota.legs[0]; /* única etapa dessa rota */

distancia = etapa.distance.value;

alert ('Distância Total => ' + distancia.toString() + ' metros.');
}
});)

A propriedade legs destrincha cada etapa do trajeto usando objetos do tipo DirectionsLeg para descrevê-las. Por etapa entende-se o deslocamento necessário entre dois endereços. Assim, se você estipulou apenas a origem e o destino, haverá uma única etapa e, por conseguinte, uma única posição em legs. Por outro lado, se você adicionou pontos de referência usando o waypoints na requisição (veja esse post para mais detalhes), haverá uma nova etapa descrevendo como chegar a cada ponto. Nesse caso, será preciso percorrer todas as posições, somando a distância de cada etapa para se obter a distância total da rota:
directionsService.route(request, function(response, status) {
if (status == google.maps.DirectionsStatus.OK) {
var distancia = 0;
var rota = response.routes[0]; /* Primeira rota */
var etapa, i;

for (i = 0; i < rota.legs.length; i++)
{
/* Obtem cada etapa */
etapa = rota.legs[i];
distancia += etapa.distance.value;
}

alert ('Distância Total => ' + distancia.toString() + ' metros.');
}
});)

O detalhamento do caminho a ser seguido dentro de cada etapa do trajeto pode ser acessado através da propriedade steps. Cada passo também possui sua distância mas a soma delas já está consolidada na própria etapa (leg). Tenha em mente que, para certos trajetos, pode acontecer de as informações de distância e duração não estarem disponíveis. Por questão de simplicidade, os scripts acima não levam isso em conta. Um exemplo completo dos recursos mostrados até agora pode ser visto na página abaixo, cujo código fonte está neste link:

Um outro ponto importante a ser lembrado é que a função route é assíncrona. Isto significa que a função de callback que passamos a ela poderá ser executada depois da linha de código imediatamente seguinte ao route. Assim, se tiver que comparar as distâncias de várias rotas, terá que montar uma forma de sincronizar os cálculos para que a comparação só ocorra depois da execução de todos os callbacks envolvidos. Esse efeito normalmente é conseguido através da função javascript setinterval, verificando a cada segundo, por exemplo, se as rotas já estão prontas.

Mais Informações
Traçando rotas com a versão 3 da API do Google Maps - Parte I e Parte II, Google Maps JavaScript API V3

28 de fevereiro de 2011

Traçando rotas com a versão 3 da API do Google Maps - Parte II

Traçar rotas usando a API do Google Maps é uma tarefa relativamente simples. Para o processo básico, basta criar instâncias de dois serviços existentes na API - um que calcula as rotas (DirectionsService) e outro que as exibe (DirectionsRenderer). Além, é claro, de fornecermos o endereço de origem e o de destino (ou pares latitude/longitude que correspondam aos locais desejados). Esse processo básico foi assunto de um outro post aqui no blog, onde foi apresentado um passo a passo para esse uso da API.

Rotas onde apenas dois pontos são especificados atendem uma gama grande de situações, mas nem sempre isso é suficiente. É o que ocorre quando há uma logística mais complexa envolvida no traçado da rota, como por exemplo, quando temos um ponto de origem e outro de destino mas queremos também estabelecer locais intermediários que devem constar da rota calculada.

Na realidade, o trabalho feito no post básico sobre rotas nos deixa bem próximos de atender também a necessidade de passar por outros pontos preestabelecidos. O que vai abaixo é parte do código explicado naquele outro post, justamente o trecho que traça a rota entre dois locais dados:
var directionsService, directionsRenderer;
var enderDe, enderAte;

directionsService = new google.maps.DirectionsService();
directionsRenderer = new google.maps.DirectionsRenderer();
directionsRenderer.setMap(map);

enderDe = 'ALAMEDA SANTOS, 1000, SÃO PAULO - SP, 01418-9028';
enderAte =
'AVENIDA NAÇÕES UNIDAS, 17-17, BAURU - SP, 17013-035';

var request = {
origin:endDe,
destination:endPara,
travelMode: google.maps.DirectionsTravelMode.DRIVING
};

directionsService.route(request, function(response, status) {
if (status == google.maps.DirectionsStatus.OK) {
directionsRenderer.setDirections(response);
}
});)

A variável request nesse código é do tipo DirectionsRequest. De acordo com a documentação dessa classe, além das propriedades utilizadas no exemplo básico acima, ela possui uma outra chamada waypoints. A propriedade waypoints é um array de objetos do tipo DirectionsWaypoint, cada um contendo informações sobre um endereço intermediário que deve constar do trajeto final. Cada endereço pode ser informado como um texto - exatamente como fazemos para definir os pontos de origem e destino - ou ainda o par latidude/longitude que representa o local por onde a rota deve passar.

Com isso, podemos montar um novo exemplo para traçar a rota entre dois locais, passando obrigatoriamente por uma lista de outros locais, cujos endereços são fornecidos. É preciso apenas passar uma lista desses endereços usando a propriedade waypoints do objeto request:
var directionsService, directionsRenderer;
var enderDe, enderAte;

directionsService = new google.maps.DirectionsService();
directionsRenderer = new google.maps.DirectionsRenderer();
directionsRenderer.setMap(map);

/* Endereço de origem e o de destino. */
enderDe = 'ALAMEDA SANTOS, 1000, SÃO PAULO - SP, 01418-9028';
enderAte =
'AVENIDA NAÇÕES UNIDAS, 17-17, BAURU - SP, 17013-035';

/* Locais intermediários por onde a rota deve passar. */
var local1, local, local3;
local1 = {location:'AV. 11 DE JUNHO, 52, SÃO PAULO - SP'};
local2 = {location:'RUA CEL. MURSA, 10, SÃO PAULO - SP'};
local3 = {location:'RUA CAMPOS SALLES, 214, BOTUCATU - SP'};

var request = {
origin:endDe,
destination:endPara,
travelMode: google.maps.DirectionsTravelMode.DRIVING,
waypoints: new Array (local1, local2, local3)
};

Acrescentando esse valor do waypoints ao exemplo básico publicado no outro post, temos o mapa e a rota abaixo:

O HTML com este exemplo completo pode ser acessado neste link.

Veja que os endereços da lista não precisam estar na mesma cidade. Outro aspecto importante a se destacar é que a rota é traçada a partir do local de origem, passando pelos pontos extras na ordem exata em que eles foram incluídos na lista. Isto é, o serviço da API que traça as rotas não faz qualquer inferência a respeito da melhor ordem a seguir para obter a rota mais curta. Portanto, quando essa questão for um problema, é preciso prestar atenção ao construir o array waypoints para evitar que a rota resultante seja irracional.

Por fim, como a propriedade waypoints é do tipo Array, todos os métodos e propriedades dessa classe são aplicáveis, inclusive push, que permite acrescentar novos itens ao array depois que ele foi instanciado:
request.waypoints.push (
{location: 'AV. RODRIGUES ALVES, 14-10, BAURU - SP' }
);

A documentação para a classe Array está disponível neste link.

Mais Informações
Básico sobre rotas com a versão 3 da API do Google Maps, Documentação da classe DirectionsRequest, Documentação da classe DirectionsWaypoint

16 de agosto de 2010

Traçando rotas com a versão 3 da API do Google Maps

Usar a versão 3 da API do Google Maps para traçar a rota entre dois endereços envolve basicamente os mesmos procedimentos descritos no post "Usando a versão 3 da API do Google Maps" aos quais se acrescenta o uso de um objeto específico da API para traçar a rota - o DirectionsService.

Mas, vamos por partes. Como já disse naquele post, a API é disponibilizada como um conjunto de classes e outros códigos JavaScript. Então, o local deste script precisa ser indicado no HTML:

<script type="text/javascript" src="http://maps.google.com/maps/api/js?v=3.1&sensor=false&language=pt-BR"></script>

Também precisaremos de instâncias dos dois objetos principais da API - o que representa o mapa em si e o Geocoder, responsável pela conversão entre endereços reais e suas respectivas coordenadas (latitude/longitude).
var map, geocoder;
function initialize() {
var lOptions = {zoom: 15,
mapTypeId: google.maps.MapTypeId.ROADMAP};
var lMapNode;
lMapNode = document.getElementById('map_canvas');
map = new google.maps.Map(lMapNode, lOptions);
geocoder = new google.maps.Geocoder();
}

function trataLocais (results, status) {
if (status == google.maps.GeocoderStatus.OK) {
map.setCenter(results[0].geometry.location);
var marker = new google.maps.Marker({map: map, position: results[0].geometry.location });
}
}

Apenas para lembrar, o nome 'map_canvas' identifica uma tag HTML do tipo DIV. Com isso, mostrar um endereço no mapa se restringe ao código javascript abaixo.
var lEndereco;
var lRequest;

lEndereco = 'ALAMEDA SANTOS, 1000, SÃO PAULO - SP, 01418-9028';
lRequest = { address: lEndereco, region: 'BR'};

geocoder.geocode( lRequest, trataLocais);

Até aqui, o processo foi o mesmo descrito no outro post. No entanto, traçar a rota não exige muito mais que isso. Para esse recurso funcionar, temos que instanciar o serviço de cálculo de rotas do Google Maps e o serviço que exibe uma rota calculada. Depois, basta solicitar ao primeiro que trace a rota entre dois endereços fornecidos:
var directionsService;
var directionsRenderer;

directionsService = new google.maps.DirectionsService();

directionsRenderer = new google.maps.DirectionsRenderer();
directionsRenderer.setMap(map);

var enderDe = 'ALAMEDA SANTOS, 1000, SÃO PAULO - SP, 01418-9028';
var enderAte = 'AVENIDA NAÇÕES UNIDAS, 17-17, BAURU - SP, 17013-035';

var request = {
origin:endDe,
destination:endPara,
travelMode: google.maps.DirectionsTravelMode.DRIVING
};

directionsService.route(request, function(response, status) {
if (status == google.maps.DirectionsStatus.OK) {
directionsRenderer.setDirections(response);
}
});)

Veja que é o serviço de exibição quem é conectado ao mapa. Como o serviço que traça a rota apenas realiza os cálculos, este não precisa conhecer o mapa que está instanciado. Veja também que foi necessário construir uma requisição com as informações sobre a rota que se deseja traçar. Esta requisição é um objeto do tipo DirectionsRequest, que oferece opções para refinar a rota. Além do endereço de origem para a rota e o seu destino, é exigido da requisição que se estipule o meio de transporte a ser utilizado para ir de um endereço a outro. Nesta versão da API, as possibilidades são ir de carro (como no exemplo), ir de bicicleta ou andando. Outros ajustes disponíveis na requisição permitem configurar a rota para evitar estradas pedagiadas e fornecer rotas alternativas.

Outro ponto a destacar é o segundo parâmetro da função route, responsável pelo cálculo da rota. Este parâmetro é uma função que devemos usar para receber os dados da rota calculada e um status desse cálculo. Caso o status seja OK, basta passar os dados calculados ao serviço de exibição, fechando o ciclo.

Esse procedimento resulta no seguinte mapa:


O HTML com este exemplo pode ser acessado neste link.

Um outro recurso útil a ser adicionado é o que descreve como seguir a rota, da mesma maneira que um aparelho de GPS narra as direções a tomar e distâncias a percorrer. Para isso, crie uma nova tag DIV no seu HTML - digamos que você a identifique como panel. Basta, então, configurar o serviço de exibição, indicando-lhe qual a tag onde descrever a rota. Apenas uma linha, como segue:
directionsRenderer.setPanel(document.getElementById("panel"));


3 de agosto de 2010

Usando a versão 3 da API do Google Maps

A API do Google Maps é uma coleção de classes, estruturas e outros tipos de dados construídos em JavaScript para facilitar o trabalho de manipulação de mapas e rotas em uma página HTML, tanto para exibição nos navegadores tradicionais disponíveis em desktops (Internet Explorer, Firefox, etc.) quanto para dispositivos móveis, cujos recursos são mais limitados.

De acordo com o site do Goggle Maps, a versão 3 foi projetada para ser mais rápida que a anterior, além de dar um suporte melhor à criação de sites com mapas para uso específico em dispositivos móveis (celulares, smartphones, tablets, etc).

Recentemente, a ABC71 passou a usar esse serviço gratuito para exibir no cadastro de Clientes e Fornecedores de seu ERP Omega um mapa com a localização do Cliente ou Fornecedor. A tela também permite que se trace a rota entre a empresa usuária do ERP e seu Cliente ou Fornecedor. Isso foi possível em parte devido à facilidade com que a API permite trabalhar diretamente com endereços reais - a alternativa é o sistema onde temos que fornecer a latitude e longitude do ponto a ser exibido.

O que o Omega faz é preparar um HTML em tempo de execução usando as informações de endereço do Cliente e apresentar esse HTML num navegador embutido na aplicação, segundo a técnica descrita no post Usando "streams" para navegação no TWebBrowser. No presente post eu mostro como construir esse HTML que exibe um endereço.

Todo o conteúdo da API está disponível num único local que precisa ser indicado no HTML de modo que você possa usar as classes e outros símbolos aí definidos. Para isso, basta incluir uma tag script simples na área de cabeçalho do HTML:

<script type="text/javascript" src="http://maps.google.com/maps/api/js?v=3.1&sensor=false&language=pt-BR"></script>

Repare que há alguns parâmetros na referência ao endereço do script, permitindo um ajuste mais fino do uso da API. No exemplo, instruo os servidores do Google a me fornecer a versão 3.1 do script e a usar textos traduzidos para o português brasileiro - veja outras configurações possíveis aqui.

Há dois objetos principais na API que devem ser criados para mostrar um mapa. O primeiro é o mapa em si e o segundo chama-se Geocoder, responsável por converter de forma transparente um endereço real em suas respectivas coordenadas (latitude/longitude), usadas internamente pelo mapa.

var map, geocoder;
function initialize() {
var lOptions = {zoom: 15,mapTypeId: google.maps.MapTypeId.ROADMAP};
var lMapNode;
lMapNode = document.getElementById('map_canvas');
map = new google.maps.Map(lMapNode, lOptions);

geocoder = new google.maps.Geocoder();

mostraEndereco ('AVENIDA NAÇÕES UNIDAS, 17-17, BAURU - SP, 17013-035');
}

function mostraEndereco (pEndereco) {
var lRequest = { address: pEndereco, region: 'BR'};
geocoder.geocode( lRequest, trataLocais);
}

O construtor da classe de mapa aceita 2 parâmetros: o nó HTML onde o mapa será desenhado (no exemplo, uma tag DIV cujo nome é map_canvas) e uma estrutura com a configuração inicial do mapa. No HTML acima eu estabeleço explicitamente as opções de nível de zoom e o tipo de visão desejado. O zoom pode variar de 0 (mais afastado) até 20 (maior proximidade), mas nem todos os níveis de zoom estão disponíveis para todos os endereços. O tipo de visão configurada é o mapa de ruas tradicional - outras opções incluem o uso de imagens de satélite ou uma mistura dos dois tipos, por exemplo. A documentação das opções disponíveis pode ser encontrada aqui.

O construtor do Geocoder em si não possui parâmetros mas sua principal função - geocode - sim. Também são 2: o primeiro é uma estrutura do tipo GeocoderRequest, onde passamos informações sobre o local a ser tratado no mapa. Repare na forma com que passei o endereço, separando logradouro, número, cidade, estado e CEP. O segundo parâmetro é uma função que será chamada automaticamente pelo script com a lista dos endereços encontrados pela função geocode. Isso significa que se os dados do GeocoderRequest forem insuficientes para determinar um lugar único - endereço errado ou não mapeado, por exemplo - pode ser retornado mais de um endereço aproximado. Para esses casos, pode-se optar por fornecer o par latitude/longitude diretamente, garantindo a exatidão e unicidade do local que desejamos apresentar. No entanto, você precisará ter essa informação de antemão para poder repassá-la ao mapa - talvez até mesmo cadastrando-a num banco de dados.

De qualquer forma, a função pode simplesmente exibir o primeiro endereço retornado. Veja um exemplo, onde o primeiro local encontrado é centralizado no mapa e identificado por um marcador:

function trataLocais (results, status) {
if (status == google.maps.GeocoderStatus.OK) {
map.setCenter(results[0].geometry.location);
var marker = new google.maps.Marker({map: map, position: results[0].geometry.location });
}
}

A partir deste momento, temos criado o vinculo entre o mapa e o Geocoder, resultando no mapa interativo abaixo:

O HTML com o exemplo completo pode ser acessado neste link; ele inclui código para tratar a situação em que mais de um endereço atende os parâmetros informados. Para um exemplo traçando rotas, veja o post Traçando Rotas com a API do Google Maps

30 de junho de 2010

Automatizando tarefas que envolvem arquivos através de scripts

O tempo passa, as tecnologias evoluem, novos paradigmas são introduzidos e passam a ser dominantes. Mas ainda não inventaram nada que seja tão efetivo e rápido para integrar sistemas do que a boa e velha troca de arquivos. Mesmo com a popularização dos Web Services e outros mecanismos, a troca de informações entre sistemas de fabricantes diferentes ou que exijam acesso a um sistema legado ainda têm no uso de arquivos texto uma solução prática.

Embora obviamente não se restrinjam ao cenário descrito acima, a Microsoft disponibiliza em seus sistemas operacionais ferramentas que permitem aos desenvolvedores ter acesso a toda a estrutura de pastas e arquivos e realizar operações básicas nessa estrutura.

O ponto de entrada para ter acesso a esta estrutura é o objeto FileSystemObject, disponível como uma interface COM. Com uma instância desse objeto você pode listar as subpastas e arquivos existentes numa pasta específica. Pode também copiar, mover e criar tanto pastas quanto arquivos, além de listar os drives conectados no computador e obter informações a respeito deles - como o espaço livre remanescente.

O código VBScript abaixo pesquisa os arquivos e subpastas contidos na pasta Processo, detecta os que são arquivos compactados (ZIP) e os move para a pasta Historico. Usei o VBScript porque ele pode ser agendado como se fosse um executável, completando a automatização das tarefas.
Dim fso
Dim gArqLog, caminho
Dim gPasta, gArqs, arquivo

Set fso = CreateObject("Scripting.FileSystemObject")
Set gArqLog = fso.CreateTextFile ("carga.log")

' Lê o conteúdo da pasta Processo
caminho = "c:\Processo\"
Set gPasta = fso.GetFolder (caminho)
Set gArqs = gPasta.Files

' Processa os arquivos que são do tipo ZIP
for each arquivo in gArqs
if fso.GetExtensionName (arquivo) = "zip" then
' Armazena no log o nome do arquivo processado
gArqLog.WriteLine (arquivo.Name)

' incluir aqui algum processamento com o arquivo
fso.MoveFile caminho + arquivo.Name, "c:\Historico\" + arquivo.Name
end if
Next

gArqLog.Close ()
Set fso = Nothing
O exemplo apenas ilustra como funciona uma pesquisa básica numa pasta - para a tarefa proposta, poderíamos simplesmente usar o MoveFile com uma máscara *.zip de uma pasta para a outra.

Algumas observações sobre o que acontece no código acima. Para começar, a função CreateObject cria o objeto principal - uma instância do Scripting.FileSystemObject. A princípio, uso esse objeto para criar um arquivo texto onde são gravados os nomes dos arquivos que forem processados - uma espécie de log.

Em seguida, a função GetFolder recupera um objeto que representa a pasta com a qual queremos trabalhar; dessa pasta, obtemos uma "coleção" com os arquivos contidos nela, coleção esta representada pela proprieda Files. Tal coleção implementa a interface IEnumerator, o que nos permite navegar pela lista usando o laço For Each, mais direto que o For convencional nesses casos.

Para terminar, o comando MoveFile move o arquivo processado da pasta original para outro local, de modo que se, o script for executado de novo, serão processados somente arquivos novos colocados na pasta de origem.

Note que eu a função GetExtensionName para determinar se o arquivo deve ou não ser considerado. Poderia ter usado a propriedade Type do arquivo. No entanto, o valor retornado por Type é afetado pela língua do sistema operacional (português, inglês, etc.), sendo um problema quando se precisa ter tal script em outras máquinas.

O script poderia ainda incluir outras tarefas com o ZIP, incluindo a chamada de rotinas para descompactá-lo e processar o conteúdo. Neste caso, um outro objeto seria necessário para executar um programa externo (como o unzip): o WshShellObject.

22 de setembro de 2009

Acrescentando suporte a Scripts em aplicações Delphi - parte 5

Prosseguindo o assunto iniciado no post anterior, vou mostrar aqui o código para fazer funcionar a manupulação de variáveis do meu programa dentro dos scripts.

A primeira providência é criar uma estrutura com as informações que poderão ser manipuladas, isto é, o saldo e a cor do Form. O Form então manterá uma instância dessa estrutura e poderá passá-la para a interface que criei no outro post usando para isso a função InitData:
{ A estrutura ...}
DoublePtr = ^Double;
TWDados = record
Saldo : DoublePtr;
ParentForm : TForm;
end;
TWDadosPtr = ^TWDados;

Veja que criei o Saldo como um ponteiro para Double. A intenção é que esta estrutura sirva de ponte para variáveis reais do meu programa. Assim, quando o script referenciar o Saldo estará referenciando exatamente a mesma variável que existe internamente no programa.

Agora, o Form pode criar uma instância de classe que implementa a nossa interface e transferir para ela a estrutura alimentada. O IDE do Delphi monta automaticamente uma classe (a CoClass) que cria tal instância, ligando a interface a uma classe concreta, que no nosso caso é a TExemplo, cujo esqueleto também foi montado pelo IDE do Delphi.
{ A classe TExemplo }
TExemplo = class(TAutoObject, IExemplo)
private
_Dados : TWDadosPtr;
{ ...}
end;

{ No construtor do Form }
_Saldo := 0;
_Dados.Saldo := @_Saldo;
_Dados.ParentForm := Self;

_VarExemplo := CoExemplo.Create;
_VarExemplo.InitData(@_Dados);
{ ...}

Observe a declaração da classe TExemplo : ela é uma herança do TAutoObject (um objeto COM para automação, definindo o programa como um COM Server) e também implementa a interface IExemplo, com as propriedades e métodos que planejamos permitir interação através de scripts. Como ela é a classe concreta para a interação, inclui nela um membro do tipo TWDadosPtr para receber a estrutura que é mantida pelo Form.

Ao preencher os esqueletos de métodos gerados pelo Delphi para a classe concreta TExemplo, usamos a estrutura informada pelo InitData. Por exemplo:
procedure TExemplo.InitData(AInfo: PChar);
begin
_Dados := TWDadosPtr (AInfo);
end;

function TExemplo.Get_CorForm: Integer;
begin
Result := integer (_Dados.ParentForm.Color);
end;

function TExemplo.Get_Saldo: Double;
begin
Result := _Dados.Saldo^;
end;

O que a função CoExemplo.Create retorna é um ponteiro para a interface e não para a classe concreta. Por isso, _VarExemplo deve ser declarada no Form como sendo do tipo interface.
private
_VarExemplo: IExemplo;

Só falta incluir a variável do programa no engine do Script Control para permitir que ela seja acessada por scripts. Para isso, basta usar o método AddObject.
_ScriptControl.AddObject('Vars', _VarExemplo, true);

O nome Vars passa a representar a instância de nossa interface nos scripts. Então, o código abaixo é reconhecido como válido para um script executado por nosso programa:
Vars.Saldo = Vars.Saldo + 1
Vars.CorForm = RGB (0, 255, 0)

O programa completo com os exemplos incluídos nos posts sobre Script com Delphi (incluindo o executável, os fontes e a type library) pode ser baixado neste link.

21 de setembro de 2009

Acrescentando suporte a Scripts em aplicações Delphi - parte 4

Comecei uma série de posts em Agosto para mostrar como fazer com que uma aplicação Delphi permita interação com o usuário através de scripts usando o componente COM da Microsoft chamado Script Control. Ficou faltando falar sobre a publicação de variáveis do programa no engine no Script Control, o que na prática dará ao usuário acesso a essa variável. Num primeiro momento, pode parecer um risco permitir tal acesso mas pense na liberdade para o usuário customizar fórmulas dentro da aplicação, por exemplo. Dependendo do objetivo, é possível planejar uma estrutura em que o usuário poderá até mesmo customizar o funcionamento e a aparência de funções inteiras da aplicação.

Na verdade, a publicação tem que ser feita através de interfaces o que significa que você publicará o objeto inteiro que implementar tal interface. No entanto, pelas características do funcionamento das interfaces, o script enxergará apenas o que você desejar - os métodos e propriedades declarados na tal interface.

O primeiro passo, portanto, é planejar o quê o usuário poderá manipular. Vou estender o exemplo que usei nos outros posts e acrescentar uma interface que permita ao usuário manipular um saldo e a cor do form. Como as duas informações não têm qualquer relação entre si, o ideal seria colocá-las em interfaces distintas. Mas como se trata de um exemplo ...

Para criar a interface da forma que o COM precisa teremos que descrevê-la em uma Type Library. Uma Type Library define tipos de dados usando uma sintaxe que independe de linguagens de programação. Um IDE como o Delphi é capaz de ler essas informações e gerar automaticamente classes para lidar com o componente COM que a Type Library descreve. Uma aplicação ou biblioteca que implemente a interface nesses termos é classificada como um Servidor COM (COM Server) pois ela é capaz de suprir as chamadas dirigidas à interface.

Nosso objetivo, então, é transformar a aplicação num servidor COM e a forma mais prática de fazer isso em Delphi (e C++ Builder) é criando um Automation Object. Com o projeto da aplicação aberto, escolha o menu File, opção New e Other. No diálogo que aparece, localize a opção Active X e, finalmente, Automation Object. O IDE se encarregará de criar uma Type Library e uma classe capaz de criar uma instância da classe concreta (chamada de CoClass) que implementa nossa interface. Também criará código para ligar isso tudo. Dei o nome de Exemplo quando o IDE pediu :
Criação do Automation Object

No editor de Type Library, acrescentei à interface IExemplo as propriedades relativas aos valores que escolhemos anteriormente para publicação : o saldo e a cor do Form. Veja na reprodução abaixo que há também uma função com o nome de InitData.
Type Library Exemplo

Quando trabalho com interfaces COM eu tenho problemas em obter o ponteiro para a classe original e fazê-la interagir com outras classes do programa - é o mesmo tipo de problema enfrentado quando se trabalha com interfaces no Delphi. Por essa razão, introduzi a função InitData com um parâmetro PChar, que é um ponteiro genérico. Desse jeito, mesmo que o programa só consiga enxergar as propriedades e métodos definidos na interface, eu posso passar à classe que implementa essa interface um ponteiro para uma estrutura interna minha, com os valores que eu quero permitir interação. Essa estrutura pode até ser uma classe minha ou todo o Form Delphi, se for necessário.

Mas tudo que está numa interface não é público ? Desse jeito, alguém com más intenções não pode passar um lixo qualquer nessa função através do script e derrubar a aplicação ? Pode sim. Para evitar isso, marquei na guia Flags do editor de Type Library as caixas Hidden e Restricted. Com isso, apenas o meu programa consegue enxergar a função InitData, restringindo o acesso quando a chamada parte de um Client COM qualquer.

Agora que tenho uma interface COM, falta preencher o código por trás dela (a CoClass) e permitir que o Script Engine embutido na aplicação enxergue uma variável que a implemente. Faço isso no próximo post e publico o programa de exemplo.

Mais Informações
Acrescentando suporte a Scripts em aplicações Delphi - parte 1, parte 2 e parte 3, Interfaces com Delphi.

26 de agosto de 2009

Acrescentando suporte a Scripts em aplicações Delphi - parte 3

No último post, montei uma aplicação Delphi mostrando como usar o Script Control para avaliar uma expressão VBScript em tempo de execução. Lá, usei algumas funções básicas do componente e uma técnica simples para interagir com o usuário. Agora, vou avançar um pouco, modificando o programa do post anterior para dar a ele suporte à criação de rotinas completas que poderão também ser usadas para avaliar expressões. Ou seja, o usuário poderá avaliar expressões mais complexas, tendo também a liberdade de criar funções que encapsulam seus cálculos mais comuns.

Acrescentei ao programa uma nova guia com título "Métodos e Funções" na qual inclui um TRichEdit (onde o usuário poderá criar suas funções), um TListBox (apenas para listar as funções disponíveis após a criação) e um botão. A resposta ao clique do botão adiciona ao engine do VBScript as funções criadas pelo usuário:
_ScriptControl.Reset;
InitScriptControl;
try
_ScriptControl.AddCode(reFuncao.Text);
except
end;
A primeira instrução, Reset, faz com que a instância do componente Script Control descarte todo a configuração feita nele anteriormente, incluindo funções e métodos que já tenham sido adicionados. Isto evita que se tente criar uma função com um nome que já existe - todas elas serão recriadas e poderão até mesmo executar cálculos diferentes da versão que havia antes. Como toda a configuração foi descartada, a segunda linha é uma função que agrupa os comandos de configuração que haviam no OnCreate do Form, mostrado no outro post.

O que está dentro do Try/Except adiciona ao engine do VBScript no componente as funções digitadas pelo usuário no Rich Text. Erros de sintaxe embutidos no código das funções só serão reportados quando for avaliada uma expressão que use a função problemática.

Após termos adicionado as funções, podemos recuperar no programa informações a respeito delas na propriedade Procedures do componente. O código abaixo põe no ListBox o nome das funções criadas com o AddCode:
lbFuncoes.Clear;
for i := 1 to _ScriptControl.Procedures.Count do
lbFuncoes.AddItem(_ScriptControl.Procedures.Item[i].Name, Nil);
A propriedade Procedures é uma lista com as funções e métodos que adicionamos ao engine e cada elemento recuperado em Item traz informações sobre uma dessas funções/métodos, dizendo se aceitam parâmetros e em qual quantidade, se têm valor de retorno, etc.

Algo importante a observar aqui é que todo código solto que seja adicionado através do método AddCode é executado imediatamente. Por código solto quero dizer as linhas de script que não façam parte da declaração de uma função ou método. Esta técnica permite executar um script inteiro de uma vez, bastando adicionar todo o código numa única chamada ao AddCode. Isto também dá abertura a uma solução rudimentar para o problema de fazer o script enxergar variáveis do programa. Crie no script uma variável - digamos que se chame y - e a adicione ao componente com AddCode:
Dim y
No programa Delphi, antes de avaliar uma expressão que vá precisar do valor de y, adicione uma atribuição como a seguinte:
_ScriptControl.AddCode('y = ' + IntToStr (Y));
Ao fazer isso, o valor da variável y do seu programa Delphi é imediatamente transferido para a variável y que existe no script. A partir desse momento, qualquer avaliação de expressão que use y enxergará o mesmo valor do programa Delphi.

Há uma forma mais elegante de se fazer isso e a mostrarei em outro post.Para baixar o exemplo completo - incluindo código e executável - clique aqui. O programa foi feito com Delphi 2005 mas deve funcionar com pouca ou nenhuma alteração em outras versões.


Mais Informações
Acrescentando suporte a Scripts em aplicações Delphi - parte 1 e parte 2, Documentação do ScriptControl no MSDN, Programando com VBScript

25 de agosto de 2009

Acrescentando suporte a Scripts em aplicações Delphi - parte 2

Dando continuidade ao post anterior sobre o MS Script Control, vou mostrar aqui um exemplo básico do uso do componente, montando uma aplicação onde o usuário poderá informar uma expressão qualquer em VBScript numa caixa de texto. Pressionar um botão na tela fará com que o programa avalie essa expressão, lançando numa outra caixa de texto o resultado obtido. Estou considerando que a expressão é numérica mas essa é uma questão de desenho da aplicação; se for necessário, pode-se usar qualquer dos outros tipos de dado da linguagem de script - no caso, VBScript.

Considerando que você já tem o fonte com as interfaces do Script Control, o primeiro passo é criar e configurar uma instância do componente. Coloquei esse código no evento OnCreate do Form:
_ScriptControl := TScriptControl.Create (Self);

_ScriptControl.SitehWnd := Handle;
_ScriptControl.Language := 'VBScript';
_ScriptControl.Timeout := -1;
_ScriptControl.AllowUI := true;
_ScriptControl.UseSafeSubset := false;
_ScriptControl.OnError := OnScriptError;
De acordo com o post anterior, este código configura o componente para que ele use 'VBScript'. Também permite interação via interface gráfica usando o próprio Handle do meu Form. O Timeout foi ajustado com valor -1, significando que o componente não gerará eventos relativos à expiraração de tempo enquanto executa um script demorado. Não falei da propriedade UseSafeSubset: ela determina se o componente deve restringir ou não os comandos aceitos de forma que somente os considerados seguros possam ser executados. Ajustei-a aqui para false, garantindo que todo o conjunto de funções do VBScript estará disponível. Ajuste o valor dela para true se segurança for uma preocupação pois assim o componente não permitirá a execução de comandos que possam burlar as permissões de acesso.

O evento OnError é configurado para executar a função OnScriptError sempre que houver um erro durante a execução de scripts - mesmo erros de sintaxe são reportados por este método:
procedure TfTesteScript.OnScriptError(Sender : TObject);
var lMsg : String;
lControl : TScriptControl;
begin
lControl := Sender As TScriptControl;
lMsg := lControl.Error.Source + #13#10;
lMsg := lMsg + 'Erro ' + IntToStr (lControl.Error.Number) + #13#10;
lMsg := lMsg + lControl.Error.Description + #13#10;
lMsg := lMsg + 'Linha ' + IntToStr (lControl.Error.Line);
lMsg := lMsg + ' ,Coluna ' + IntToStr (lControl.Error.Column) + #13#10;
MostraErro (lMsg);
end;
O código reproduzido acima mostra que há uma propriedade do Script Control chamado Error que descreve toda a condição do erro que ocorreu, incluindo a linha e coluna dentro do código, um número de erro e uma descrição do erro. A documentação do objeto Error pode ser encontrada aqui.

Para concluir o exemplo, faltou mostrar como é feita a avaliação da expressão. O código é a resposta ao evento de clique no botão de avaliação:
procedure TfTesteScript.btAvaliarClick(Sender: TObject);
var res : Variant;
begin
try
res := _ScriptControl.Eval(edExpressao.Text);
if VarType (res) <> varEmpty then
edResult.Text := FloatToStrF(res, ffFixed, 15, 4);
except
edResult.Text := '## erro ##';
end;
end;
Veja que o processo é bastante simples: basta chamar o método Eval do Script Control passando como parâmetro a expressão digitada pelo usuário. O valor retornado é um Variant mas como o valor que estou esperando na expressão é de ponto flutuante, posso fazer a conversão de float para string sem problema. Em todo caso, todo o trecho é protegido por um par Try/Except.

Neste exemplo, as expressões que podem ser avaliadas são fixas, isto é, não consigo informar na expressão uma variável que eu tenho criado e alimentado no meu programa. Apenas constantes, funções e operações matemáticas são válidas. Há uma lista aqui com as funções definidas no VBScript e que podem ser utilizadas na caixa com o texto para avaliação de expressões.

No próximo post, mostro como incluir métodos e funções para executar scripts mais complexos. Em outra oportunidade, mostro também como permitir que o script interaja mais efetivamente com o programa através da adição de objetos do programa ao engine de script.

Para baixar o programa de exemplo - incluindo código e executável - clique aqui. O programa foi feito com Delphi 2005 mas deve funcionar com pouca ou nenhuma alteração em outras versões.

24 de agosto de 2009

Acrescentando suporte a Scripts em aplicações Delphi - parte 1

Quando se desenvolve aplicações destinadas a empresas com perfis operacionais e ramos de negócio muito diferentes entre si é inevitável que surjam situações em que um Cliente precise que determinado cálculo seja feito de um jeito enquanto para Clientes de outros ramos esse cálculo tem que ser de outra forma. Há situações que envolvem a criação de fórmulas e cada Cliente pode querer aplicar uma diferente. Qual a melhor abordagem para tratar essas discrepâncias ? Criar telas diferentes resolvem parcialmente o problema já que isso implica em que as opções de cálculo têm que estar disponíveis dentro do programa para serem utilizadas.

Essas situações apareceram em diversos pontos no ERP da ABC71 e, para solucioná-los, sugeri o uso de um componente COM distribuído pela Microsoft chamado MS Script Control. O objetivo desse componente é permitir que programas de terceiros tirem proveito da infraestrutura de Scripts do Windows de uma forma direta e simples, sem ter que se preocupar em conhecer os pormenores dessa infraestrutura nem ter que implementar as interfaces relacionadas a ela. Basta entrar com expressões que se queira avaliar ou incluir um código script para execução e o componente se encarregará do resto.

Listo abaixo as propriedades e métodos disponibilizados pelo Script Control.
Propriedades
Language determina qual a linguagem de script será usada pelo componente. Atualmente, apenas "VBScript" e "JScript" são aceitos por padrão.
Timeout é o tempo (em milisegundos) para execução do script. Se esse tempo decorrer mas o script ainda não terminou, o evento OnTimeout é disparado. Use o valor (-1) para que o timeout seja desconsiderado.
AllowUI deve ser ajustado para true se o código do script puder interagir com o usuário através da interface gráfica, como por exemplo usando a função MsgBox.
SitehWnd é o handle para a janela que exibirá interações com a interface gráfica.

Métodos
AddCode é usado para adicionar código script ao componente. É permitido incluir funções, métodos (sub rotinas), criação de variáveis, etc. Código que estiver fora de uma definição de funções ou métodos será executado imediatamente.
Eval avalia a expressão passada como parâmetro, retornando o valor obtido dessa avaliação. A expressão terá que ser escrita na linguagem estipulada na propriedade Language, estando sujeita à sintaxe e operadorades dessa linguagem. É permitido chamar funções ou métodos adicionados com o AddCode.
Run permite executar uma função ou método adicionado anteriormente ao componente, independendo se eles possuem parâmetros ou não. No caso das funções, o valor de retorno pode ser recuperado.
AddObject mapeia um objeto do seu programa de modo que o script pode utilizá-lo como se fosse nativo da linguagem em que o script foi escrito.
Reset põe o componente em seu estado inicial, isto é, sem qualquer código ou objeto adicionado. As propriedades são ajustadas para o valor padrão e devem ser modificadas conforme a necessidade antes de poder usar de novo o componente.

Eventos
OnError ocorre sempre que um erro é encontrado no código do script.
OnTimeout é disparado quando o tempo para execução terminou mas o script ainda está em execução. Esse tempo é configurado pelo valor da propriedade Timeout.

É recomendável que a execução de scripts com esse componente seja feita na Thread principal da aplicação pois certos comandos não são thread-safe, abrindo a possibilidade de pipocar erros estranhos em pontos aleatórios do código.

O Script Control é distribuído como uma biblioteca COM e, portanto, o primeiro passo é pedir ao Delphi que crie um fonte pascal baseado nessa biblioteca para que possamos utilizá-lo. Há um post aqui mostrando como fazer isso - basta substituir a biblioteca indicada por Microsoft Script Control.

No próximo post, coloco um exemplo prático do uso dos Script Control interagindo com um programa Delphi.

4 de agosto de 2009

Atualizando Pivot Tables do Excel com scripts

Na semana passada me colocaram uma questão prática envolvendo Excel. Foram incluídas numa planilha diversas Pivot Tables conectadas a tabelas no banco de dados e essas Pivot Tables deveriam ser atualizadas automaticamente num determinado dia/hora, uma vez por semana.

As Pivot Tables são um recurso encontrado no Excel (e em outros programas de manipulação de dados, como o Lotus 1-2-3 e o Calc do Open Office) que permitem a criação de visões multidimensionais dos dados através de operações do tipo arrasta-e-solta nas colunas de valores. O resultado é uma poderosa ferramenta gerencial de análise que permite fazer o cruzamento dos dados das diversas colunas, sumarizando-os. O conceito é mais ou menos como os cubos de informações dos data-minings. Há uma matéria sobre o uso de Pivot Tables neste link.

Como escrevi uma série de posts mostrando como automatizar operações no Excel através de scripts VB, me pareceu que a solução estava bem próxima. Os scripts podem ser considerados como programas, de modo que é possível agendá-los sem problemas no Windows, resolvendo a questão do dia/hora especifico para atualização. Restava, então, desvendar a atualização dos dados em si.

Uma das características das Pivot Tables do Excel é que elas podem ser configuradas para reter as informações usadas para conectar com a fonte de dados, isto é, o nome do servidor e o usuário/senha para acessar o banco de dados. Você pode saber se isso já está configurado abrindo a planilha, selecionando a Pivot Table e pedindo para dar o Refresh nos dados manualmente. Se nenhuma informação sobre a conexão for solicitada pelo Excel, a configuração já está pronta para ser usada pelo script. Considerando que a planilha em questão tenha sido criada com as Pivot Tables associadas a uma fonte de dados externa (uma query num banco de dados, por exemplo), o problema fica restrito a criar um script VB que carregue a planilha, localize as Pivot Tables existentes, atualize os dados de cada uma delas e então salve a planilha com os novos valores.

Estudando o Modelo de Objetos do Excel, encontrei o método PivotTables do WorkSheet, que dá acesso à lista de todas as Pivot Tables existentes numa folha de trabalho. Também encontrei o método que faz a atualização dos dados (RefreshTable). A sintaxe para atualizar a primeira Pivot Table de uma folha de trabalho é como segue.
folha.PivotTables(1).RefreshTable

Como pode ser que não haja Pivot Tables numa determinada folha, é conveniente verificar a existência antes. PivotTables retorna uma lista e a propriedade folha.PivotTables.Count indica quantos elementos há nessa lista. A solução será mais efetiva se usar esse Count para dar Refresh em todas as PivotTables que encontrar.
For I = 1 To objWorkBook.Sheets.Count
Set folha = objWorkBook.Sheets.Item(I)
For j = 1 to folha.PivotTables.Count
folha.PivotTables(j).RefreshTable
Next
Next

objWorkBook.Save
objWorkBook.Close

No exemplo acima, todas as folhas da planilha aberta são percorridas e, para cada folha, todas as Pivot Tables são atualizadas. A instrução Save salva as alterações feitas de volta na mesma planilha que foi aberta. Se quiser salvar numa planilha diferente, use SaveAs - isso permitirá também salvar a planilha num formato diferente, preservando aquela que foi originalmente aberta.

Para ver como abrir uma planilha já existente e como obter a lista de folhas de trabalho dela, veja o post Automatizando a leitura de planilhas Excel.

Para fazer download do script de exemplo, clique aqui.

24 de julho de 2009

Exportando dados para Excel com ADO

Agora que postei um pouco da teoria básica sobre o modelo de classes do ADO, posso cumprir a promessa que fiz de mostrar interação entre o Excel e o ADO através de script, populando uma planilha com os dados trazidos por uma query num banco de dados. Para ver como criar a planilha em si, consulte o post Criando uma planilha através de script já que neste eu vou focar mais a parte do uso do ADO.

De início, vou criar uma conexão com o banco de dados SQL Server pois esse é o banco mais comum entre os Clientes da ABC71. Veja o código usando VBScript:
Dim Cnxn, strCnxn, rs, query
' Conectar ao banco SQL
SetCnxn = CreateObject("ADODB.Connection")
strCnxn = "Provider=SQLOLEDB.1;Data Source=ABCVMS;Initial Catalog=DBTP;Locale Identifier=1046;"
Cnxn.Open strCnxn, "gustavo", ""
Cnxn.Execute ("set language us_english ")
Cnxn.Execute ("set dateformat dmy ")

O comando CreateObject cria uma instância do objeto "ADODB.Connection", isto é, aloca a memória necessária para que possa usar o objeto de conexão. O comando Open que vem logo em seguida estabelece a conexão com o banco de dados. Passo a ele 3 valores: a string de conexão, o nome do usuário do banco de dados e a senha deste usuário. O exemplo traz uma string de conexão para o MS SQL Server; para saber como montar essa string para outros bancos, clique aqui. Os dois comandos Execute que encerram o trecho de código configuram a linguagem e o formato de datas retornados pelo SQL Server.

Já temos a conexão, podemos então submeter uma query no banco de dados:
query = "SELECT * FROM TPUF"
Set rs = Cnxn.Execute (query)
Set folha = objWorkBook.WorkSheets (1)
for j = 0 to rs.Fields.Count - 1
folha.Cells (1,j+2) = rs.Fields.Item(j).Name
next

' Configura o cabeçalho
Set lRng = folha.Range (folha.Cells (1,1), folha.Cells (1,j+1))
lRng.Interior.Color = RGB(240,240,127)
lRng.Interior.Pattern = 1 'xlPatternSolid
lRng.Font.Name = "Tahoma"
lRng.Font.Bold = True
lRng.Font.Size = 11

Neste trecho do código, o comando Execute da conexão submete um SELECT e obtem um Recordset com os registros e campos encontrados. As definições dos campos podem ser consultadas na propriedade Fields, que é uma lista. Através do laço for, esta lista de campos é percorrida e o nome de cada campo é colocado numa coluna da primeira linha da folha de trabalho do Excel. Após o laço for, a coluna com os nomes dos campos é formatada de modo a representar um cabeçalho para os valores a serem recuperados dos registros.

Falta, então, percorrer os registros encontrados e lançar os valores sob a coluna correta na folha da planilha:
i = 2
Do While Not rs.EOF
folha.Cells (i,1) = i - 1 'Número do registro
for j = 0 to rs.Fields.Count - 1
folha.Cells (i,j+2) = rs.Fields.Item(j).Value
next
i = i + 1
rs.MoveNext
Loop

O laço Do While percorre todos os registros retornados pela query, testando se o fim foi atingido através da propriedade EOF (fim de arquivo). O laço for interno percorre a lista de campos do registro atual, lançando o valor de cada um na coluna certa da planilha. Cada registro ocupa uma linha na folha da planilha, sendo que a variável i determina qual é o número da linha. Ela é iniciada com a segunda linha já que a primeira foi preenchida com o cabeçalho.

Como arremate, inclui mais um laço para formatar cada coluna da folha com base no tipo de dado especificado na definição do campo:
for j = 0 to rs.Fields.Count - 1
Set lRng = folha.Range (folha.Cells (1,j+2), folha.Cells (i,j+2))

Select Case rs.Fields.Item(j).Type
Case 4, 5, 6, 14, 131, 139 ' Números c/ decimais
lRng.ColumnWidth = rs.Fields.Item(j).DefinedSize
lRng.NumberFormat = "#,##0.00_);[Red](#,##0.00)"
lRng.HorizontalAlignment = -4152 ' xlRight'

Case 2, 3, 16, 17, 18, 19, 20, 21 ' Números Inteiros
lRng.ColumnWidth = rs.Fields.Item(j).DefinedSize * 4
lRng.NumberFormat = "#,##0_);[Red](#,##0)"
lRng.HorizontalAlignment = -4152 ' xlRight'

Case 7, 64, 133, 135 ' Datas
lRng.ColumnWidth = 12
lRng.NumberFormat = "m/d/yyyy"
lRng.HorizontalAlignment = -4108 ' xlCenter'

Case Else ' Textos e outros
lRng.ColumnWidth = rs.Fields.Item(j).DefinedSize + 3
lRng.NumberFormat = "General"
lRng.HorizontalAlignment = -4131 ' xlLeft'
End Select
next

Neste ponto, a variável i armazena o número da última linha que foi alimentada com valores dos registros. Então, percorro novamente a definição de todos os campos, obtenho a propriedade Type de cada um e determino a formatação mais conveniente. Os tipos de dados são definidos por um enumerado do ADO chamado DataTypeEnum mas nem todos foram incluídos no exemplo. Consulte a documentação desse enumerado para mais detalhes.