30 de junho de 2009

Trabalhando com BLOBs em Delphi e C++ Builder

Um BLOB - ou Binary Large OBject - é um tipo de dado existente em gerenciadores de Bancos de Dados. Apesar do nome "grandes objetos", ele é capaz de armazenar qualquer tipo de informação, independentemente do tamanho em bytes dessa informação. Um outra característica marcante é que o gerenciador de banco de dados não faz qualquer inferência sobre o conteúdo de campos deste tipo, cabendo à aplicação tratar o tipo corretamente. Do ponto de vista do banco de dados, um campo BLOB é apenas uma sequência de bytes.

O MS SQL Server fornece 2 tipos de BLOBs : binary e varbinary. A diferença entre ambos é que no binary o tamanho é fixo; o tamanho estipulado para o campo é o mesmo em todos os registros da tabela, o que o torna apropriado para armazenar estruturas cujo tamanho é conhecido e pré fixado. Já o varbinary aceita dados de tamanho variável, isto é, o campo pode ter tamanhos diferentes em cada registro na tabela. É útil para quando não se sabe de antemão qual o tamanho da informação a ser armazenada, como por exemplo, uma imagem, um vídeo, um áudio ou outro arquivo qualquer. Observação: de acordo com o MSDN, o tipo IMAGE do SQL Server será descontinuado. Veja aqui a informação a esse respeito.

O Oracle oferece o tipo BLOB, equivalente ao varbinary. Há uma tabela aqui com os tipos de dados do Oracle e o tamanho de cada um em várias versões deste banco de dados.

Suponha que você tem um texto formatado do tipo RTF (Rich Text Format) representando um contrato qualquer. Um usuário pode editar o conteúdo através de um componente TRichEdit inserido numa tela feita em Delphi ou C++ Builder e armazenar as alterações numa tabela no banco de dados. Para mostrar aqui como fazer isso, vou criar a seguinte tabela.
-- Em SQL Server
create table INFO
(
ID int not null,
DESCRICAO varchar(255) null,
OBJETOS varbinary(max) null
)

Não inclui chave primária por questão de simplicidade mas você deveria fazer isso num projeto real. A expressão max na declaração do BLOB indica que eu não sei qual é o tamanho máximo do conteúdo e que o Banco deve estar preparado para receber mais que os 8K normais.

Se você já tem um RTF pronto em um arquivo externo e quer carregar o contéudo no TRichEdit, pode usar o comando LoadFromFile, deixando o usuário livre para modificar o conteúdo na tela. Vou usar exemplos em C++ mas para Delphi a sequência de comandos é a mesma, bastando ajustar a sintaxe para essa linguagem.
RichEdit1->Lines->LoadFromFile (_FileName);

Inclui no Form também um componente TADOConnection configurado para se conectar ao meu banco de dados e um componente TADOCommand com o comando de inserção na tabela:
INSERT INTO INFO (ID, DESCRICAO, OBJETOS)
VALUES (:ID, :DESCRICAO, :OBJETOS)

A sintaxe :ID sinaliza ao Delphi (ou C++ Builder) que crie um parâmetro com o nome ID para facilitar o acesso. Ao solicitar a gravação, então, pode ser usada a sequência de comandos abaixo:
TMemoryStream *stream = new TMemoryStream ();
// Põe em stream com o conteúdo do RichEdit,
// isto é, o texto do contrato
RichEdit1->Lines->SaveToStream (stream);

// Monta os parâmetros do comando
TParameters *params;
params = ADOCommand1->Parameters;
params->ParamByName("ID")->Value = 1;
params->ParamByName("DESCRICAO")->Value = "Descrição";
params->ParamByName("OBJETOS")->LoadFromStream (stream, ftVarBytes);

// Executa o comando de inserção
ADOCommand1->Execute ();

delete stream;

Embora não mostrado, esse trecho de código deve ser inserido numa transação do Banco de Dados. O conteúdo para o BLOB é conseguido através de streams, primeiro salvando o texto formatado do RichEdit para dentro de um stream e depois carregando esse stream para o parâmetro do tipo BLOB do meu comando de inserção. Os demais valores eu deixei fixo apenas para simplificar; numa aplicação real, estas informações também deveriam ser variáveis.

Ficou faltando apenas mostrar como ler o conteúdo a partir do banco. Montei um TADOQuery que faz um SELECT para recuperar o registro da tabela INFO que tem o ID igual a 1. Então, uso esse SELECT para obter o BLOB:
ADOQuery1->Active = true;
if (ADOQuery1->Eof == false)
{
TBlobField *field;
TMemoryStream *stream = new TMemoryStream ();

// Salva o blob no stream
field = (TBlobField *)ADOQuery1->FieldByName ("OBJETOS");
field->SaveToStream (stream);

stream->Position = 0;
RichEdit1->Lines->LoadFromStream (stream);
delete stream;
}

Ou seja, leio o valor do campo BLOB num stream e alimento de volta o RichEdit, tomando o cuidado de reposicionar o stream no começo já que o comando LoadFromStream carrega a partir da posição atual.

Embora o exemplo tenha sido com um texto, a sequência de comandos seria a mesma para trabalhar com utro tipo qualquer de conteúdo - uma imagem, vídeo, XML, etc.

27 de junho de 2009

Novo antivírus da Microsoft

A Microsoft lançou recentemente um beta de seu novo programa antivírus, chamado Security Essencials. Ao contrário das tentativas anteriores de concorrer com Symantec, McAfee e outros grandes fabricantes de antivirus, essa versão será um serviço gratuito, como atesta o site do Security Essencials. Parece que essa é uma tentativa de entrar pra valer no nicho de segurança.

Os produtos anteriores da Microsoft para esse fim eram o OneCare - que é um serviço pago - e o Windows Defender. Esse último, embora gratuito, só protegia contra sypware. Já o novo produto protege o computador também contra outras ameaças, tais como malwares, rootkits, trojans e outros.

O Security Essencials está disponível para Windows XP (Service Pack 2 ou 3), Windows Vista e também para a próxima versão do sistema operacional da Microsoft, o Windows 7, que ainda não foi oficialmente lançado.

De acordo com o review publicado pelo site Baixaqui, o funcionamento não difere muito das outras aplicações antivírus existentes já que também permite aqendar verificações, padronizar ações e alertas, configurar a proteção em tempo real (isto é, verificar arquivos e outros recursos sempre forem acessados), excluir da varredura certos arquivos e/ou pastas específicas, etc.. Além disso, a atualização do programa e da lista de ameaças a verificar pode ser feita tanto pelo próprio Security Essencials quanto de forma integrada ao Windows Update. Acho que a Microsoft já está se resguardando de futuros processos já que foi multada na Europa por embutir o Internet Explorer no Windows e não dar oportunidade de escolha ao usuários.

Finalizando, a info Online noticiou que a AV-Test, companhia especializada em testes de segurança de TI, confrontou a versão beta desse novo antivírus com uma bateria de testes de cerca de 3.200 vírus, cavalos de troia e vermes. Segundo a notícia, todos os arquivos infectados foram corretamente identificados e limpos.

Infelizmente, a Microsoft limitou a quantidade de downloads do Beta. Quem acessa a página do Security Essencials recebe a mensagem : "Agradecemos o seu interesse em ingressar no Microsoft® Security Essentials Beta. No momento, não estamos aceitando novos participantes. Volte novamente em uma data posterior para verificar se há disponibilidade para mais participantes."

A expectativa é que o lançamento da versão definitiva se dê antes do lançamento do Windows 7, em outubro. Agora, resta esperar para saber se a Microsoft manterá mesmo gratuita essa versão...

25 de junho de 2009

Comunicação entre processos com C++ Builder: File Mapping

Hoje em dia, é muito difícil que um programa seja autosuficiente, isto é, que suas funcionalidades sejam completamente isoladas de outros programas. Por exemplo, se um programa usa banco de dados, certamente ele precisará de um gerenciador para esse banco (SQL Server, Oracle, etc.) ou no mínimo um outro pedaço de software (OleDB, ODBC) para poder se comunicar com o banco.

Há uma porção de formas de fazer dois programas se comunicarem, desde a simples troca de mensagens nativas do sistema operacional (ex: SendMessage, no Windows), passando por componentes (tecnologia COM, como no caso do OleDB) e chegando mais recentemente nos WebServices.

Escolher qual das tecnologias melhor se aplica depende do cenário que se tem. Se ambos os programas se encontram em execução no mesmo computador ou não, se você tem acesso ao código fonte de ambos, etc..

Na ABC71 temos a seguinte situação: o programa principal do ERP apenas carrega e exibe o painel de navegação apropriado para cada usuário. Quando o usuário seleciona uma atividade, um novo programa entra em execução e fica ativo até que o usuário encerre a atividade. O programa principal, então, fica desimpedido para que o usuário possa ativar outra tarefa simultaneamente.

Neste cenário, como estamos no mesmo computador e temos os códigos fontes de ambos os programas, a melhor solução que encontramos foi o File Mapping. File Mapping é um recurso que permite mapear estruturas em memória e compatilhar o acesso a essas estruturas entre processos.

Para trabalhar com File Mapping, o primeiro passo é criar um handle para esse recurso. O código do exemplo abaixo em C++ foi extraído do nosso programa principal, que controla o mapeamento. O mesmo conceito pode ser usado em Delphi :
HANDLE FFileMappingHandle;
FFileMappingHandle = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, sizeof(TWMyStruct), "Omega@");
if ( FFileMappingHandle && (GetLastError() == ERROR_ALREADY_EXISTS) )
{
CloseHandle(FFileMappingHandle);
FFileMappingHandle = NULL;
}
Na chamada à função do Windows CreateFileMapping, o valor INVALID_HANDLE_VALUE no primeiro parâmetro faz com que seja usado o arquivo de paginação de memória do Windows para criar o mapeamento. O valor PAGE_READWRITE dá acesso a leitura e gravação na memória compartilhada. O tamanho da memória a ser usada é estipulado pelo sizeof(TWMyStruct), que é o tamanho da minha estrutura. Veja na documentação de CreateFileMapping que o tamanho da memória é determinado pela combinação dos valores de 2 parâmetros e o código mostrado aqui só é valido porque o tamanho de minha estrutura cabe num DWORD. O último parâmetro é o nome que dei ao mapeamento; é através desse nome que o programa auxiliar consegue enxergar a memória compartilhada.

O próximo passo é criar uma "visão" dessa memória. Parece complicado mas na verdade, é um conceito bastante simples. Como a memória para minha estrutura foi alocada pelo próprio CreateFileMapping, criar uma visão é apenas obter o endereço dessa memória e atribuir a uma variável que representa a estrutura. Para isso, usamos a função MapViewOfFile do Windows:
TWMyStruct *FMyStrc;
FMyStrc = (TWMyStruct *)MapViewOfFile(FOmegaMapHandle, FILE_MAP_ALL_ACCESS, 0, 0, 0);
Desse ponto em diante, todo conteúdo que for gravado em FMyStrc será compartilhado por qualquer programa que abrir o FileMapping chamado "Omega@".

Quando não for mais usar essa memória, encerre a "visão" e o mapeamento:
if ( FMyStrc )
UnmapViewOfFile(FMyStrc);
if ( FOmegaMapHandle )
CloseHandle(FOmegaMapHandle);

Até aqui, esses procedimentos dizem respeito ao programa principal, que cria o mapeamento. E quanto ao(s) outro(s) programa(s) que também fará(ão) uso dessa memória? Praticamente a mesma coisa ... A única diferença é que o File Mapping já está criado, então basta abrí-lo através do OpenFileMapping ao invés de usar CreateFileMapping.
FOmegaMapHandle = OpenFileMapping(FILE_MAP_ALL_ACCESS, true, "Omega@");

Com isso, consigo fazer com que o programa principal envie ao programa auxiliar as informações necessárias para iniciar a tarefa solicitada pelo usuário. Quando o programa auxiliar se encerra, ele também consegue sinalizar isso ao programa principal e retornar a ele dados relativos à tarefa executada.

23 de junho de 2009

Obtendo informações da CPU em Delphi

Publiquei em Maio um post a respeito da obtenção do MAC Address em Delphi para gerar um código que identificasse um computador de forma única. Naquela ocasião, sugeri que se incluisse informações sobre a CPU para melhorar o código obtido. Então, vou mostrar aqui como obter essas informações.

A primeira coisa a saber é que as informações sobre a CPU são obtidas através de uma instrução assembly chamada CPUID. Prepare a escova de bits!

A instrução CPUID usa o registrador EAX para determinar que tipo de informação deve ser retornado. Por exemplo, quando o EAX está com valor 0 (zero), CPUID retorna um texto de 12 caracteres com a identificação do fabricante da CPU. No exemplo que segue, uso o comando ASM do Delphi para submeter as instruções assembly.
var VendorID : array[0..12] of char;
begin
Asm
// Isso faz com que EAX tenha valor zero
XOR EAX, EAX

// Chama a instrução CPUID
CPUID

// Retorna 4 caracteres em cada um
// dos registradores EBX, EDX e ECX,
// totalizando 12 caracteres do ID
// do fabricante.
MOV dword ptr [VendorID], EBX
MOV dword ptr [VendorID+4], EDX
MOV dword ptr [VendorID+8], ECX
end;
// Como no C++, terminar com zero
VendorID[12] := Char(0);

Para obter o tipo, o modelo, a família e o stepping do processador, devemos usar EAX com valor 1. O stepping identifica melhorias do mesmo processador, sendo a primeira sempre A0 e nas releases seguintes há acréscimos no número e/ou na letra. O tipo mostra se o processador é Original, OverDrive ou Dual. Modelo e família são numerações dadas pelos fabricantes a cada versão de seus chips. O retorno desses valores se dá no próprio EAX.

Quando chamamos CPUID com o valor de EAX igual a 1, outras informações são retornadas em outros registradores. Em EDX e ECX são retornadas indicações da presença de recursos da CPU. Informações sobre recursos extras são colocados em EBX. O trecho abaixo mostra como recuperar a assinatura da CPU (tipo + modelo + familia + stepping) além de reportar a presença de FPU (Floating Point Unit, que é o coprocessador matemático), se o processador possui tecnologia MMX (melhora processamentos envolvendo multimedia) e, no caso do processador ser Intel, se ele é HTT (Hyper Thread Technology, isto é, se há emulação de mais processadores).
var retStr, dType : String;
REGEAX, REGEBX, REGEDX : LongWord;
dLogicalProcessorCount : integer;
dFamily, dModel, dStepping, dFamilyEx, dModelEx : integer;
hasFpu, hasMmx, hasHtt : boolean;
begin
asm
MOV EAX, 1
CPUID
MOV [REGEAX], EAX
MOV [REGEBX], EBX
MOV [REGEDX], EDX
end;

dFamily := ((REGEAX shr 8) and $F);
dModel := ((REGEAX shr 4) and $F);
dStepping := (REGEAX and $F);

dFamilyEx := ((REGEAX shr 20) and $FF);
dModelEx := ((REGEAX shr 16) and $F);

case (((REGEAX shr 12) and $7)) of
0: dType := 'Original';
1: dType := 'OverDrive';
2: dType := 'Dual';
end;

hasFpu := ((REGEDX and $00000001) <> 0);
hasMmx := ((REGEDX and $00800000) <> 0);
hasHtt := ((REGEDX and $10000000) <> 0);

if ((StrComp(VendorID, 'GenuineIntel') = 0) And HasHTT) then
dLogicalProcessorCount := ((REGEBX shr 16) and $FF)
else
dLogicalProcessorCount := -1;

retStr := dType +
IntToStr (dFamily) + IntToStr (dModel) +
IntToHex (dStepping, 2) +
IntToStr (dFamilyEx) + IntToStr (dModelEx);

Tenho uma lista maior com mapeando os valores retornados em EDX com os recursos possívels. Caso julguem interessante, coloco em outro post esta lista.

22 de junho de 2009

Comparando e mesclando arquivos fonte

Para quem desenvolve software é muito comum a situação de ter que comparar duas versões de um mesmo fonte para saber quais as alterações que foram introduzidas. Quando se trabalha em equipe, então, a situação pode ser ainda pior já que mais de um desenvolvedor pode estar mexendo num mesmo fonte ao mesmo tempo. Depois que ambos terminam suas alterações, é necessário comparar as diferenças e quase sempre fazer um "merge" para que não se perca nenhuma das modificações.

Utilizamos na ABC71 o Visual Source Safe (VSS) da Microsoft para controle das versões dos fontes e o VSS possui recursos para comparar e mesclar o conteúdo de versões. Mas, como não é a função principal da ferramenta, acaba sendo trabalhoso realizar essas tarefas. Além disso, o VSS é bastante integrado a seu próprio banco de dados, tornando ainda mais trabalhoso comparar arquivos externos.

Há algum tempo venho usando uma ferramenta chamada WinMerge, mais apropriada para essas tarefas. Trata-se de um projeto Open Source desenvolvido em C++ cujo objetivo primário é visualizar as diferenças entre duas versões de um mesmo arquivo e, se desejar, mesclar o conteúdo de ambos, gerando uma terceira versão que contenha todas as alterações desejadas.

O WinMerge mostra cada versão do fonte num painel próprio, cada um dos fontes sincronizado no mesmo ponto, dando destaque às diferenças com cores diferentes. É possível navegar rapidamente entre cada alteração através de botões na barra de ferramentas ou usando teclas de atalho. É permitido jogar uma alteração específica de um fonte ao outro, sem qualquer restrição, isto é, nenhum dos fontes é privilegiado: posso lançar uma alteração da versão A de um fonte para sua versão B e a alteração seguinte lançar da B para a A ou optar por não fazer o lançamento. Obviamente, há uma opção para lançar todas as diferenças de uma vez de um fonte para outro. Entretanto, se você quiser garantir que uma das versões não sofra alterações por engano durante o processo de merge, pode usar o recurso de manter um dos fontes como read-only.

A imagem abaixo mostra a tela principal do WinMerge:
Veja na reprodução que há um painel à esquerda mostrando a distribuição das diferenças ao longo dos fontes e que uma seta aponta no painel o ponto exato que está sendo exibido dos fontes.

A imagem mostra também um outro recurso: os fontes são exibidos com syntax highlighting. Como o WinMerge também é um editor de texto, permitindo introduzir novas alterações junto com o merge, acaba sendo bem prática a edição. A imagem não mostra mas também há um painel na parte de baixo da tela que exibe as linhas do ponto atual que está diferente nos dois fontes.

Mais: o WinMerge pode gerar um lista com os arquivos que estão diferentes numa comparação entre duas pastas, incluindo as subpastas, se quiser. Um duplo clique em um arquivo e a tela exibindo visualmente as diferenças é apresentada.

Para terminar, o WinMerge interage com o Windows Explorer de forma que você pode disparar uma comparação simplesmente selecionando um ou dois arquivos (ou pastas) a partir do próprio Explorer.

Mais Informações
Winmerge.org

18 de junho de 2009

Criando queries com uma coluna RecordNumber (ranking)

A ABC71 disponibiliza para seus Clientes uma ferramenta para construção de relatórios baseada no Report Builder, onde se monta relatórios costurando queries prontas a um layout de apresentação. Outro dia precisei montar uma query para um relatório desses e era imperativo que uma das colunas trouxesse valores sequenciais (um ranking) de modo que no primeiro registro teria valor 1, no 2o teria valor 2 e assim por diante.

Pesquisei no help do SQL Server 2005 e me deparei com uma solução talhada para resolver esse tipo de problema já que a Microsoft incluiu funções para ranking nessa versão! Para o que eu precisava, a função ROW_NUMBER bastava. O uso dela é na cláusula SELECT como no exemplo:
SELECT EMP_FIL, FORNEC, ROW_NUMBER() OVER (ORDER BY FORNEC) AS SEQUENCIA
FROM CFOR
ORDER BY EMP_FIL

A cláusula OVER que vem depois do nome da função é obrigatória e indica qual é o campo (ou campos) que deve(m) ser usado(s) para sequenciar os registros. A grosso modo, a query é primeiro ordenada segundo as instruções dessa cláusula, um número sequencial é atribuído a cada registro e só então o ORDER BY geral da query é aplicado antes dos registros serem retornados.

Como não é obrigatório que o ORDER BY estipulado no OVER seja o mesmo da query, pode ser que a coluna com o ranking não venha na ordem.

Essa solução seria perfeita se ABC71 não tivesse Clientes que usassem outros tipos de banco de dados, como ORACLE, PostgreSQL e versões mais antigas do SQL Server. E como esses comandos não são do padrão SQL ....

Pesquisando na Internet, encontrei outras sugestões para solucionar esse problema numa gama maior de versões e fabricantes de bancos de dados. A que me pareceu mais consistente sugere criar um campo IDENTITY na tabela que deve ser rankeada, sendo o campo programado para iniciar com valor 1 e para ir incrementando de 1 em 1 a cada novo registro inserido. O trecho de script que segue é um exemplo em SQL Server para criar uma tabela cuja coluna ID tem essas características:
CREATE TABLE [dbo].[CFOR](
ID identity(1, 1),
[EMP_FIL] [int] NOT NULL,
[FORNEC] [numeric](14, 0) NOT NULL,
[RAZ_SOCIAL] [varchar](35) NULL,
[NOME_FANTAS] [varchar](25) NULL
)

Para quem já tem muitos Clientes usando uma aplicação com a tabela, no entanto, essa abordagem ainda pode ser inconveniente pois será necessário reorganizar a tabela para incluir o novo campo.

Uma solução que deve ser aplicável a todos os bancos relacionais envolve criar um OUTER JOIN já que esse é um recurso padrão SQL. A ideia é montar uma query agrupando (GROUP BY) todas as colunas desejadas e ligando essa query a uma outra que envolva a mesma tabela. A ligação é através de LEFT OUTER JOIN, usada para trazer todos os registros da primeira query e usando a segunda para contar quantos registros são maiores ou iguais ao registro atual. O comando reproduzido abaixo traz o mesmo resultado da primeira query deste post. Tal comando está usando a sintaxe do SQL Server.
SELECT F1.EMP_FIL, F1.FORNEC, COUNT(*) AS SEQUENCIA
FROM CFOR F1 LEFT OUTER JOIN
CFOR F2 ON (F1.FORNEC > F2.FORNEC)
OR (F1.FORNEC = F2.FORNEC AND F1.EMP_FIL >= F2.EMP_FIL)
GROUP BY F1.FORNEC, F1.EMP_FIL
ORDER BY F1.FORNEC, F1.EMP_FIL

Já que nem tudo pode ser perfeito, esta query resolve o problema para diversos bancos mas tem um tempo de execução muito maior que a primeira, usando ROW_NUMBER. Com ROW_NUMBER, executou praticamente instantâneo para 23.000 registros enquanto a que usa LEFT OUTER JOIN levou quase 2 minutos no mesmo servidor.

16 de junho de 2009

Fórmulas no Excel através de programação

Certamente o recurso mais utilizado do Excel são as fórmulas, isto é, cálculos envolvendo uma ou mais células. As fórmulas podem ser desde a simples referência ao conteúdo de uma única célula até a inclusão de funções avançadas, como tratamentos estatísticos.

Para falar desse assunto, vou assumir o cenário descrito nos posts sobre criação e formatação de planilhas Excel através de scripts.

Por exemplo, a primeira linha do código abaixo configura a célula B32 (linha 32 da coluna B) para que ela simplesmente exiba o conteúdo da célula B2 (linha 2 da coluna B). Depois, configuro a célula C32 (linha 32 da coluna C) para mostrar a somatória das linhas 2 e 3 da coluna C:
folha.Cells (32,"B").Formula = "=$B$2"
folha.Cells (32,"C").Formula = "=$C$2+$C$3"
folha.Cells (33,"C").Formula = "=$C$2+$C$3+Sheet2!A1"

Veja que o valor da fórmula sempre começa com o sinal de = e que não há espaços separando as referências envolvidas. O sinal $ indica que a referência é fixa, de forma que se esta fórmula for copiada para outra célula, a referência permanece a mesma. Portanto, o sinal $ não é obrigatório e, se for omitido, a referência é modificada automaticamente para refletir a nova localização sempre que a fórmula for copiada.

Também é permitido referenciar células de outras folhas de trabalho, bastando incluir o nome dessa folha seguido do sinal !, como na última linha do trecho acima.

A propriedade Formula usada nos exemplos vem do objeto Range mas até agora só a usamos para acessar a fórmula de células individuais. O que acontece se atribuirmos uma fórmula para um Range que englobe mais de uma célula, como no exemplo que segue?
folha.Range("B32:C32").Formula = "=SUM(B2:B31)"
folha.Range("B33:C33").Formula = "=SUM($B$2:$B$31)"

Se as referências contidas na fórmula possuirem o sinal $ (como na 2a linha do exemplo), todas as células do Range terão exatamente a mesma fórmula e, portanto, o mesmo valor. Se não tiverem esse sinal, o Excel modificará a fórmula para cada célula, substituindo a referência original por outra de acordo com a nova linha/coluna, do mesmo modo que acontece quando copiamos e colamos fórmulas pela interface gráfica do Excel.

Uma última coisa a observar é que a Microsoft tem versões traduzidas do Office para outras linguagens. Se seu Excel está em português, quando você monta fórmulas através da interface gráfica usa os nomes das funções traduzidos para português. Para montar o último exemplo pela interface, ao invés de SUM você usaria SOMA, por exemplo.

Então este script só funcionaria para o Excel em inglês? A resposta é não. A Microsoft mantem mais de uma propriedade com fórmulas. A propriedade Formula contem sempre funções com o nome original, em inglês. Se quiser configurar fórmulas com nome traduzido, use a propriedade FormulaLocal pois nela se usa funções cujos nomes estão na linguagem local. As linhas abaixo são equivalentes para quem tem o Excel em português:
folha.Range ("B32:C32").Formula = "=SUM(B2:B31)"
folha.Range("B33:C33").FormulaLocal = "=SOMA(B2:B31)"

Tenha em mente, no entanto, que atribuir uma fórmula usando a propriedade FormulaLocal limita o script a ser usado exclusivamente em ambientes onde o Excel usa a mesma lingua que você.

Mais Informações
Modelo de objetos do Excel, Leitura e Gravação de planilhas Excel através de scripts, Download do Exemplo.

15 de junho de 2009

Formatando planilhas Excel através de programação

Já descrevi em posts anteriores como automatizar a leitura e a criação de planilhas Excel, bem como fiz um overview dos principais objetos que compõem a estrutura dessa ferramenta. Agora, vou mostrar objetos auxiliares para formatar o conteúdo das planilhas.

Há diversas partes de uma célula ou de um grupo de células (Range) que podem ser formatadas. Essa formatação inclui o tipo de letra (fontes), formato de apresentação para números e datas, bordas, cor de fundo, etc.

Usando como exemplo a planilha do post sobre criação de planilhas, o código abaixo mostra uma parte da formatação de um cabeçalho:
folha.Cells (1,"B") = "Mês 1"
folha.Cells (1,"C") = "Mês 2"
folha.Range ("B1:C1").ColumnWidth = 12
folha.Range ("B1:C1").Interior.Color = RGB(210, 220, 255)
folha.Range ("B1:C1").Interior.Pattern = 1 'xlPatternSolid

A propriedade Interior, que é acessível através através de um Range, representa a parte interna de cada célula contida no Range, permitindo configurar a cor de fundo e o padrão de preenchimento dessas células. Usei também a propriedade ColumnWidth do Range para modificar o comprimento das 2 colunas utilizadas.

Outro objeto importante para a formatação é o Font, que permite modificar a aparência das letras de cada célula. No trecho abaixo, instruo o Excel para que use o tipo de Fonte Arial Bold com tamanho 14 na faixa de células que engloba as colunas B e C da primeira linha.
folha.Range ("B1:C1").Font.Name = "Arial"
folha.Range ("B1:C1").Font.Bold = True
folha.Range ("B1:C1").Font.Size = 14

Use o objeto Borders para modificar simultaneamente a aparência das 4 bordas de todas as céulas de um Range. Para configurar bordas de forma independente, use a propriedade Items usando como índice um dos valores do enumerado XlBordersIndex.
folha.Range ("B1:C31").Borders.LineStyle = 1 'xlContinuous
folha.Range ("B1:C31").Borders.Weight = 2 'xlThin
folha.Range ("B1:C31").Borders.Color = RGB(0,0,0)
' Linha embaixo do cabeçalho mais grossa
' (xlEdgeBottom = 9)
folha.Range ("B1:C1").Borders(9).Weight = 4

Na primeira parte, indico que quero bordas em todas as células pertencentes à àrea que vai da linha 1 à 31, colunas B e C. Essa é a região que foi alimentada no outro post. Na última linha desse trecho, indico que a borda inferior da área que é meu cabeçalho deve ter uma linha mais grossa. Como Items é a propriedade padrão de Borders, não é preciso explicitá-la no script, embora outras linguagens possam exigir isso.

Há no Range ainda uma propriedade para configurar o formato de apresentação do conteúdo das células. Trata-se de NumberFormat, que aceita como valor um texto representando a formatação desejada.
folha.Range ("B1:C1").NumberFormat = "General"
folha.Range ("B2:C31").NumberFormat = "#,##0.00_);[Red](#,##0.00)"
folha.Range ("B1:C31").HorizontalAlignment = 4 ' xlRight

Neste exemplo, "General" indica que o contéudo é um texto simples. Já a sequência #,##0.00_) significa que o Excel deve formatar as células indicadas no Range com 2 casas decimais, que não deve mostrar zeros à esquerda e que deve usar o separador de milhar para números maiores que 999. O que está depois do ponto-e-vírgula é a formatação a ser usada caso o valor da célula seja negativo.

Mais uma vez, esse exemplo foi construído com a versão 2007 do Office e pode ser necessário modificar uma ou outra linha do código para trabalhar com versões anteriores.

Volto depois com o uso de fórmulas e outros detalhes. Clique aqui para fazer o download do exemplo.

Mais Informações
Modelo de objetos do Excel, Leitura e Gravação de planilhas Excel através de scripts, objeto Interior, objeto Font, objeto Borders.

13 de junho de 2009

Anunciada primeira release do Mono para Visual Studio

Já publiquei aqui um post sobre a ferramenta Mono, que permite que se execute programas .Net em outros sistemas operacionais, além do próprio Windows.

Depois disso, vi no site da Embarcadero que eles têm uma versão do Delphi chamada Delphi Prism, que é vendida como uma solução de desenvolvimento para .Net e Mono baseado no ambiente do Visual Studio mas que suporta programação em linguagem Pascal (agora chamada de Delphi). Ou seja, você pode aproveitar os conhecimentos adquiridos no desenvolvimento Pascal para criar aplicações .Net que rodam também em Linux e Mac.

Na verdade, retomei esse assunto porque vi essa semana uma outra notícia a respeito do Mono: o lançamento do suporte ao Visual Studio. Reproduzo abaixo esta notícia conforme está no site da InfoWorld.

" Os desenvolvedores do projeto de código aberto Mono, que permite que aplicações construídas com o Microsoft .Net possam ser executados em Linux, Unix e Macintosh, liberaram uma release do Mono Tools for Visual Studio para um número limitado de desenvolvedores.

Num post publicado segunda-feira, a equipe do Mono descreveu a release como um "preview fechado". Esta primeira release agrega quatro funcionalidades ao Visual Studio :
* Mono Migration Analzyer (MoMA), que analisa os projetos .Net verificando trechos incompatíveis e auxilia os desenvolvedores a resolverem estas incompatibilidades.
* Executação no Mono para Windows, ajudando a isolar problemas decorrentes das diferenças entre Mono e .Net.
* Execução no Mono para Linux, permitindo trabalhar as diferenças entre os sistemas operacionais Windows e Linux.
* Depuração no Mono para Linux, para permitir depuração remota de aplicações Mono executando no Linux.

[ Leia o que Neil McAllister, da InfoWorld, tem a dizer sobre "The case for supporting and using Mono (em inglês)." ]

O Projeto Mono vem sendo patrocinado pela Novell como um esforço para desenvolver um versão de código aberto para Unix da plataforma .Net. "O objetivo é permitir que desenvolvedores Unix construam e entreguem aplicações .Net que sejam muti-plataforma. O projeto implementa várias tecnologias desenvolvidas pela Microsoft que foram submetidas ao ECMA para padronização," conforme afirma o site do Projeto Mono.
"


10 de junho de 2009

Criando uma planilha Excel através de script

Neste post, pretendo mostrar na prática os objetos do Excel que descrevi no post anterior e montar dinamicamente uma planilha usando VBScript. O ideal seria trabalhar com dados vindos de um banco de dados como o SQL Server, por exemplo. Mas, como também estou preparando posts para mostrar o uso de ADO e banco de dados, o exemplo que usarei aqui vai trabalhar com informações geradas localmente. Após os posts sobre ADO volto à carga para melhorar o exemplo.

Da mesma forma que já havia mostrado no post sobre leitura de uma planilha Excel, devemos começar o script criando uma instância do objeto Application pois é a partir dele que conseguiremos acesso aos demais recursos do Excel:
Dim objExcel
Dim objWorkBook
Dim folha, i
Set objExcel = CreateObject("EXCEL.APPLICATION")

O próximo passo é disponibilizar uma planilha para podermos trabalhar. No exemplo do outro post, disponibilizei uma planilha através do comando Open da lista de Workbooks mas aqui eu quero criar um nova planilha, vazia. A instrução abaixo comanda essa criação, como se tivéssemos entrado no Excel e selecionado New no menu File e pedido para criar uma nova planilha em branco.

Set objWorkBook = objExcel.Workbooks.Add

Com isso, temos agora uma planilha com 3 folhas (Worksheets). Se for necessário, podemos adicionar outras folhas ou remover aquelas que não formos usar.

A seguir, vou usar as rotinas do VBScript que tratam números aleatórios para gerar duas colunas e 30 linhas com dados:
i = 2
Randomize
Set folha = objWorkBook.WorkSheets (1)

while (i <= 31)
folha.Cells (i,2) = Rnd * 100
folha.Cells (i,"C") = Rnd * 100
i = i + 1
wend

Algumas explicações sobre o que está acontecendo acima:
  • Randomize inicia o gerador interno de números aleatórios (randômicos) do vbScript, evitando que se gere sempre a mesma sequência.
  • objWorkBook.WorkSheets (1) referencia a primeira folha de trabalho (tabela ou Worksheet) do Workbook que criamos. Essa referência é salva numa variável chamada folha por questão de comodidade e clareza no código, mas não é obrigatório que se acesse dessa forma.
  • A propriedade Cells dá acesso ao conteúdo e formatação de células individuais numa folha. Posso referenciar uma célula ou grupo de células informando o número da linha e da coluna. Também posso fazer essa referência através da(s) letra(s) que identifica(m) cada coluna. Os dois métodos são equivalentes e aparecem no exemplo acima para alimentar as linhas da coluna 2 (ou B) e C (ou 3), respectivamente.
  • A função Rnd gera um número aleatório entre 0 e 1. Multiplicando por 100, obtenho números entre 0 e 100.
Agora que os dados da planilha estão lançados, podemos salvar o conteúdo e encerrar a automação do Excel.
objWorkBook.SaveAs "c:\temp\planilha.xlsx", 51
objWorkBook.Close
Set objWorkBook = Nothing
Set objExcel = Nothing
Passei dois parâmetros para a função que salva a planilha: o nome do arquivo para onde quero que a planilha seja salva e um código que indica o formato com que o arquivo será criado - pode ser, por exemplo, uma versão anterior do Excel, HTML, modelo de planilha (template), XML, etc. O valor 51 indica que desejo utilizar o formato padrão de planilhas. Outros códigos aceitos podem ser encontrados aqui. Caso já exista um arquivo com o nome dado, o Excel vai mostrar uma caixa de diálogo solicitando confirmação antes de sobrepor o arquivo existente. Usei o Excel 2007 para montar esse exemplo mas creio que as propriedades e métodos usados aqui estão disponíveis também em versões anteriores. Em posts futuros, mostro como incluir fórmulas, formatações, gráficos e outros recursos que forem oportunos.

8 de junho de 2009

Classes para automatização do Excel

Escrevi há alguns dias um post mostrando como automatizar a leitura de uma planilha Excel mas não cheguei a falar muito sobre os objetos que são expostos pelo Excel para uso em automação. Como pretendo colocar outros posts sobre o assunto - incluindo a criação de planilhas - achei melhor dar um overview desses objetos antes.

Segundo a documentação da Microsoft no MSDN, tudo que você pode fazer através da interface gráfica do usuário também pode fazer via automação. Aliás, usando automação há coisas que você pode fazer que não estão disponíveis na interface.

Quando falo automação, me refiro a qualquer forma de acessar por programação o Excel, seja através do VBA embutido na aplicação, VBScripts externos ou através de programas Delphi ou C#. Os objetos que vou descrever aqui são os mesmos para todos esses cenários. Lembro, entretanto, que em todos eles é preciso ter o Excel instalado para que os objetos estejam acessíveis.

Application
Este objeto é a porta de entrada para automatizar o Excel. A grosso modo, equivale a carregar o programa Excel. É através dele que se controla todas as funções e configurações, permitindo acesso às planilhas propriamente ditas. Realiza, ainda, a carga e gravação de arquivos.
Se você vai automatizar usando VBA dentro do Excel, uma instância de Application está sempre disponível, automaticamente. Qualquer das outras formas, será preciso antes criar (ou obter) a instância antes de sair usando.

Workbook
Um Workbook corresponde a um arquivo XLS (ou XLSX, no Excel 2007), isto é, é a representação computacional de uma planilha Excel. Um objeto desse tipo pode ser acessado através da propriedade ActiveWorkbook do Application, que representa a planilha atualmente ativa dentro do Excel. Application tem ainda uma lista chamada Workbooks que dá acesso a cada uma das planilhas que estiverem carregadas no Excel num determinado momento.

Worksheet
Cada Workbook é composto por uma ou mais tabelas, com diversas linhas e colunas. A classe que manipula essas tabelas é chamada Worksheet e, por padrão, um Workbook é criado com três Worksheets (ou folhas de trabalho), nomeadas como Sheet1, Sheet2 e Sheet3. Esses nomes e outras propriedades de cada folha podem ser alterados por automação e estão disponíveis na propriedade Worksheets, que é uma lista com todas as folhas que compõe o Workbook com o qual se está trabalhando.

Range
Dentre os conceitos de objetos do Excel, o de Range é provavelmente o mais utilizado pois é a forma mais básica de organização e acesso das informações. É através de Ranges que se acessa o valor individual de uma célula, sua formatação, a fórmula que eventualmente esteja associada a ela, etc..
Quando montamos fórmulas de cálculo ou gráficos, teremos que nos referir a um conjunto de células agrupadas em um ou mais Ranges. A formatação de células também é feita através de Range, quer ele represente uma única célula dentro de uma folha (Worksheet), um região englobando diversas células, uma linha ou uma coluna inteiras ou até mesmo um arranjo tridimensional de células espalhas por múltiplas folhas.

Mostro em outros posts como usar esses conceitos para criar uma planilha.

5 de junho de 2009

Resolvendo problema no Windows Explorer ao acessar certos arquivos

Já se deparou com uma demora excessiva quando seleciona um arquivo no Windows Explorer? Ou demora para aparecer o menu suspenso quando clica com a direita sobre determinado arquivo ? Ou, pior ainda, sempre que seleciona uma pasta ou arquivo, o Explorer é encerrado com a mensagem de arrepiar a espinha "Windows Explorer encontrou um problema e precisa ser encerrado. Desculpe-nos o inconveniente.".

Todos esses problemas podem ter a mesma causa: erro no Context Menu Handler ou Manipulador de Menu de Contexto. O Windows permite que se estenda suas funcionalidades através da criação de componentes COM que são chamados de Shell Extensions. Há diversos tipos de extensões, sendo muito comuns o próprio Context Menu Handler - para incluir uma opção no menu de contexto do Windows Explorer - e também os Browser Helper Object - para estender as funções do Internet Explorer. Uma lista dos tipos possíveis pode ser encontrada aqui (há uma tabela no meio da página).

O Windows instala por padrão uma série de extensões - algumas importantes para o correto funcionamento do sistema - mas diversos fornecedores de software também o fazem. Por exemplo, o antivírus costuma disponibilizar no menu de contexto de pastas e arquivos uma opção para procurar vírus nessa(s) pasta(s) ou arquivo(s). O mesmo ocorre com softwares de compactação (como o Winzip e o 7Z), tocadores de áudio e vídeo, etc. Um outro exemplo é o Acrobat Reader que tem uma extensão para permitir a leitura de arquivos PDF no Internet Explorer.

Voltando ao assunto do começo do post, o que fazer quando esses comportamentos estranhos começam a pipocar? Uma boa solução é a ferramenta Shell Extension Manager para Windows. Ela mostra as extensões de todos os tipos que estiverem instaladas em seu computador, trazendo informações sobre cada uma, incluindo o fabricante, número da versão e o caminho do programa responsável por executar a extensão. O ShellExView não deixa de fora nem mesmo as extensões da própria Microsoft.

Esta ferramenta permite desativar e reativar facilmente as extensões. Portanto, se ocorrer algum dos erros descritos no primeiro parágrafo, basta desativar a extensão que se suspeita estar causando o problema. Há colunas com o nome e a descrição fornecida por cada fabricante para a sua extensão para facilitar a identificação das suspeitas. Há ainda uma coluna com os tipos de arquivo sobre os quais a extensão pode trabalhar.

Normalmente, são as extensões de terceiros as causadoras de problema. Tanto que o ShellExView emite um aviso quando se tenta desativar uma extensão da própria Microsoft, indicando que aquilo é parte do sistema operacional e que desativá-la pode causar problemas no Windows.

Depois de identificado quem é o real culpado pelo problema, pode-se optar por mantê-lo desativado ou desinstalá-lo. Ainda, se o programa é de uma fonte confiável, pode-se tentar a reinstalação.

Problemas com o Internet Explorer também podem ser resolvidos assim, inibindo o Browser Helper Object que possa impedí-lo de funcionar quando se navega em uma página específica.

4 de junho de 2009

Automatizando a DI API com scripts

A DI API - Data Interface API - é o mecanismo disponibilizado pela SAP para acesso aos objetos de negócio do Business One, pemitindo a criação de extensões ao ERP que agem nos dados exatamente como o próprio Business One.

Como esta API foi desenvolvida na plataforma COM da Microsoft, é possível automatizar tarefas relativas ao SAP Business One usando scripts que, por sua vez, podem ser agendados no Windows ou utilizados numa aplicação HTML.

O ponto de entrada para o Business One é o objeto Company. É com ele que se faz login no sistema e é ele que dá acesso aos demais objetos de negócio do ERP. Então, o script deve ser iniciado com a obtenção desse objeto. Veja um exemplo em VBScript:
Dim Company
Set Company = CreateObject ("SAPbobsCOM.Company")

Para fazer a conexão, informe o endereço do servidor de licenças, o Company DB, e o nome e senha do usuário. No exemplo abaixo, estou considerando que o servidor é o próprio computador que está executando o script. Os demais dados são os padrões para a base de demonstração que vem com o ERP.
Company.CompanyDB = "SBODemo_Brazil"
Company.Password = "manager"
Company.UserName = "manager"
Company.Server = "(local)"

ret = Company.Connect
if (ret <> 0) then
MsgBox Company.GetLastErrorDescription(), 0, "Erro"
end if

Como segurança sempre é uma questão importante, é uma boa ideia arrumar uma forma de passar para o script informações sensíveis como a senha do usuário. Montar uma aplicação HTML pode ser uma opção mas, se quiser agendar a execução do script, provavelmente terá que criar outro mecanismo - um arquivo encriptado, por exemplo.

Agora que o Company está conectado, podemos obter qualquer objeto publicado pelo Business One. No trecho de código que segue, obtenho um RecordSet e navego por todos os registros existentes:
const BoRecordset = 300
Dim oRs
Set oRs = Company.GetBusinessObject (BoRecordset)

oRs.DoQuery "SELECT * FROM OITM"
oRs.MoveFirst
while (oRs.EoF = False)
' Incluir aqui tratamento p/ cada item
' Exemplo: exportar p/ uma planilha
oRs.MoveNext
wend

Set oRs = nothing
Company.Disconnect
Set oCompany = nothing

Atribuir nothing ao RecordSet se não formos mais usuá-lo é imprescindível para liberar os recursos de máquina que ele consome. Só depois disso é que podemos desconectar o Company sem problemas.

Outro tipo de objeto que pode ser usado são aqueles relacionados às regras de negócio propriamente ditas. Por exemplo, o trecho de script abaixo recupera o código de item trazido no RecordSet do exemplo anterior. Com esse código, posiciona o objeto de negócio Items do Business One, alimenta a descrição em outra língua com a própria descrição do item, sinaliza que o item pode ser movimentado e manda gravar essas alterações no banco de dados, segundo as próprias regras do ERP.
const oItems = 4
Dim oItem, cProd
Set oItem = Company.GetBusinessObject (oItems)
c_prod = oRs.Fields.Item("ItemCode").Value

if ( oItem.GetByKey (c_prod) ) Then
oItem.ForeignName = oItem.ItemName
oItem.Frozen = 0

if (oItem.Update <> 0) then
MsgBox Company.GetLastErrorDescription(), 0, "Erro"
end if
End If

' Avisa que não vai mais usar
Set oItem = nothing

O objeto Company disponibiliza também funções para controlar transações no banco de dados - StartTransaction e EndTransaction - caso seja necessário. Apenas tenha em mente que o Business One aborta sozinho as transações caso aconteça algum erro. Use o método InTransaction para saber se ainda há uma transação ativa.

Observação: Para executar esses exemplos é preciso ter a DI API instalada. Consulte a documentação do Business One para saber como fazer isso.

3 de junho de 2009

Automatizando o Excel com scripts - Leitura

Há uma máxima que corre entre os fabricantes de ERP que diz que o maior concorrente dos ERPs é o Excel. Por sua versatilidade em fazer cálculos, sumarizar informações e apresentar resultados em gráficos, as planilhas Excel são uma opção difícil de bater. Mesmo quando o próprio ERP disponibiliza de forma nativa os mesmos recursos, sempre há os que se sentem mais confortáveis manipulando planilhas.

Com a ABC71 isso não é diferente. E como em casa de ferreiro o espeto é de pau, nós também temos nossas planilhas. Vou usar como exemplo um VBScript que usamos para carregar no banco de dados uma planilha de previsão de tempos de migração de fontes para Web, mostrando como ler o conteúdo. Um VBScript é um arquivo texto comum - a única diferença é que ao invés de TXT ele tem extensão VBS, podendo ser executado com um duplo-clique pelo Windows Explorer.

O primeiro passo é criar um objeto que representa a aplicação Excel.
Set objExcel = CreateObject("EXCEL.APPLICATION")

Isso carrega o Excel, permitindo que o script acesse seus comandos. Como quero trabalhar com um planilha que já existe, o passo seguinte é instruir o Excel a carregá-la. O Excel mantem uma lista das planilhas abertas em sua propriedade Workbooks; ela também é responsável pela abertura de planilhas:
Set objWorkBook = objExcel.Workbooks.Open("c:\Web\tempos.xls")

O layout da minha planilha incluir uma folha (Sheet) para cada um dos módulos do ERP da ABC71. Vou, então, montar um laço que me permita percorrer as informações contidas em cada uma das folhas:
For I = 1 To objWorkBook.Sheets.Count
Set folha = objWorkBook.Sheets.Item(I)
' Incluir aqui o processamento
' dos dados da folha atual
Next

Nesse trecho, usei a propriedade Sheets do Workbook (planilha) aberto. Ela lista todas as folhas da planilha atual. Count é a quantidade de folhas existentes na lista e Item permite acessar cada uma delas de forma independente, através da posição (índice) que ocupam na lista. Esse índice vai de 1 até a quantidade de folhas na lista.

Dentro de cada folha, tenho colunas com as previsões para a migração dos fontes pertencentes ao módulo. Há um cabeçalho na primeira e na segunda linha; portanto, vou começar a leitura na linha 3. Como não sei de antemão quantas linhas têm que ser processadas, vou percorrê-las até que não possuam mais dados.
lin = 3
while Trim (folha.Cells (lin, 1)) <> ""
on error resume Next

' Tratar dados da linha aqui

lin = lin + 1
wend

Uso a sintaxe Cells(lin,col) para obter o valor inserido numa célula específica - aquela que está onde a linha lin cruza com a coluna col. É importante observar que o valor retornado por Cells(lin,col) deve ser tratado com o tipo de dado apropriado. Isto é, as datas são do tipo Date, números são de um dos tipos numéricos (inteiros ou com decimais) e textos são do tipo string. Dependendo do contexto, será preciso realizar conversões entre os tipos de dados. Por exemplo, um comando SQL a ser submetido ao banco de dados é necessariamente um texto e dados que são numéricos - como Tempo em minha planilha - devem ser convertidos através da função CStr antes que possam ser concatenados ao comando:
tempoStr = CStr (folha.Cells (lin, 5))

Para finalizar, é preciso fechar o Workbook que foi aberto e descarregar o Excel quando ambos não forem mais necessários ao script:
objWorkBook.Close True
Set objWorkBook = Nothing
objExcel.Quit
Set objExcel = Nothing

Como isso é um script, também pode ser agendado como uma tarefa no Windows ou ainda ser incluído numa aplicação HTML.

Componente Delphi para embutir arquivos numa aplicação

Recentemente, um amigo me perguntou se havia jeito de inserir um arquivo inteiro num formulário do Delphi/C++ Builder. A ideia dele era construir uma aplicação que, entre outras coisas, tocasse um MP3 mas não queria ter que distribuir o tal MP3 junto com a aplicação.

Um excelente recurso das linguagens de programação visual (Delphi, VB, Visual C#) é a facilidade de componentização, isto é, criar um pedaço de código funcional que pode ser usado em diversos projetos. Assim, ajudei esse amigo a montar um componente no Delphi que é capaz de embutir um arquivo qualquer num Form ou Data Module. Da mesma forma que o arquivo externo, esse conteúdo embutido é acessível através de um Stream.

Para construir o componente, criei um novo projeto de pacote Win32 no Delphi mas, se você possuir um pacote, pode incluir o novo componente nele mesmo. Criei um novo componente Win32, escolhi TComponent como ancestral (classe base) e dei-lhe o nome de TWRecurso. Depois, adicionei-o ao projeto do pacote.

A interação desse componente no ambiente de programação é restrita a uma única propriedade exibida no Object Inspector: o nome do arquivo a ser embutido. Ao modificar esse nome, o componente inserirá no Form o conteúdo do arquivo indicado. Na classe TWRecurso, crio a propriedade FileName e um MemoryStream para guardar o conteúdo do arquivo:
TWRecurso = class(TComponent)
private
FFileName: String;
FDados: TMemoryStream;
protected
procedure SetFileName (AFileName: String);
public
{ ... }
property Dados: TMemoryStream read FDados;
published
property FileName: String read FFileName write SetFileName;
end;

FileName está na área published pois será visível no Object Inspector. Já Dados (o TMemoryStream) está na área public, onde poderá ser acessado via programa mas não pelo Inspector. Como, então, Dados será embutido no Form?

A mágica está no mecanismo de persistência dos componentes no Delphi. A função DefineProperties, que é parte desse mecanismo, nos dá a oportunidade de acrescentar ao Form o que for necessário. Reproduzo abaixo a sobrecarga da função para esse exemplo:
procedure TWRecurso.DefineProperties(Filer: TFiler);
function DeveGravar: Boolean;
begin
Result := (FDados.Size > 0);
end;
begin
inherited;
Filer.DefineBinaryProperty('Dados', LoadDadosProperty, StoreDadosProperty, DeveGravar);
end;

O código mostra que estamos definindo uma nova propriedade Dados, indicando as funções que devem ser usadas para gravação e leitura e informa que esta propriedade só deve ser gravada quando nosso MemoryStream interno tiver algum dado.

As funções para gravação e leitura recebem um Stream como parâmetro, bastando gravar nele nosso conteúdo - ou ler dele, na função de leitura. Um detalhe é que não há como saber de antemão a quantidade de bytes que têm que ser lidos; então, esta informação também tem que ser incluída.
procedure TWRecurso.LoadDadosProperty(Reader: TStream);
var lSize : Int64;
begin
FDados.Clear;
Reader.Read(lSize, sizeof (lSize));
FDados.CopyFrom (Reader, lSize);
end;

procedure TWRecurso.StoreDadosProperty(Writer: TStream);
var lSize : Int64;
begin
FDados.Position := 0;
lSize := FDados.Size;
Writer.Write(lSize, sizeof (lSize));
Writer.CopyFrom(FDados, FDados.Size);
end;

Se mudar o nome do arquivo, o nosso Memory Stream deve carregar o contéudo do novo arquivo:
procedure TWRecurso.SetFileName (AFileName: String);
begin
if FileName <> AFileNAme then begin
FFileName := AFileName;
FDados.Clear;
if (FileExists (AFileName) ) then
FDados.LoadFromFile(AFileName);
end;
end;

Não mostrei, mas o construtor de TWRecurso deve instanciar FDados e iniciar FFileName com brancos, enquanto o destrutor faz a limpeza necessária.

Montei esse exemplo com Delphi2005 mas não deve haver diferenças profundas para outras versões. Lembro, ainda, que o C++ Builder é capaz de compilar e instalar componentes feitos em pascal. Basta criar um pacote no C++ Builder e incluir o fonte WRecurso.pas para ter o componente também nesse ambiente.

Com esse componente, é possível embutir qualquer tipo de arquivo num Form: XML, MP3, TXT, etc.