Professional Documents
Culture Documents
A seguir vemos uma referência rápida para ajudá-lo a localizar alguns dos tópicos
mais importantes no livro.
Tradutor
Daniel Vieira
Capa
Adaptação da edição americana por Editora Campus
Editoração Eletrônica
RioTexto
Revisão Gráfica
Iv\one Teixeira
Roberto Mauro Facce
Projeto Gráfico
Editora Campus Ltda.
A Qualidade da Informação.
Rua Sete de Setembro, 111 – 16º andar
20050-002 Rio de Janeiro RJ Brasil
Telefone: (21) 509-5340 FAX (21) 507-1991
E-mail: info@campus.com.br
ISBN 85-352-0578-0
(Edição original: 0-672-31781-8)
CIP-Brasil. Catalogação-na-fonte.
Sindicato Nacional dos Editores de Livros, RJ
T269d
Teixeira, Steve
Delphi 5, guia do desenvolvedor / Steve Teixeira, Xavier Pacheco ;
tradução de Daniel Vieira. – Rio de Janeiro : Campus, 2000
: + CD-ROM
Todos os esforços foram feitos para assegurar a precisão absoluta das informações apresentadas nesta
publicação. A editora responsável pela publicação original, a Editora Campus e o(s) autor(es) deste livro
se isentam de qualquer tipo de garantia (explícita ou não), incluindo, sem limitação, garantias implícitas
de comercialização e de adequação a determinadas finalidades, com relação ao código-fonte e/ou às
técnicas descritos neste livro, bem como ao CD que o acompanha.
Dedicatórias
Dedicatória de Xavier
Para Anne
Dedicatória de Steve
Para Helen e Cooper
Agradecimentos
Gostaríamos de agradecer a todos aqueles cuja ajuda foi essencial para que este livro pudesse ser escrito.
Além do nosso agradecimento, também queremos indicar que quaisquer erros ou omissões que você en-
contrar no livro, apesar dos esforços de todos, são de responsabilidade nossa.
Gostaríamos de agradecer aos nossos revisores técnicos e bons amigos, Lance Bullock, Chris Hesik
e Ellie Peters. O revisor técnico ideal é brilhante e detalhista, e tivemos a sorte de contar com três indiví-
duos que atendem exatamente a essas qualificações! Esse pessoal realizou um ótimo trabalho com um
prazo bastante apertado, e somos imensamente gratos por seus esforços.
Em seguida, um enorme agradecimento aos nossos autores colaboradores, que emprestaram suas
habilidades superiores de desenvolvimento e escrita de software para tornar o Delphi 5 – Guia do Desen-
volvedor melhor do que teria sido de outra forma. O guru do MIDAS, Dan Miser, entrou escrevendo o
excelente Capítulo 32. Lance Bullock, a quem oferecemos o dobro da dose normal de gratidão, conse-
guiu compactar o Capítulo 27, entre suas tarefas como revisor técnico. Finalmente, o mago da Web Nick
Hodges (inventor do TSmiley) está de volta nesta edição do livro no Capítulo 31.
Agradecemos a David Intersimone, que encontrou tempo para escrever o prefácio deste livro, ape-
sar de sua agenda tão ocupada.
Enquanto escrevíamos o Delphi 5 - Guia do Desenvolvedor, recebemos conselhos ou dicas de inú-
meros amigos e colegas de trabalho. Entre essas pessoas estão Alain “Lino” Trados, Roland Bouchereau,
Charlie Calvert, Josh Dahlby, David Sampson, Jason Sprenger, Scott Frolich, Jeff Peters, Greg de Vries,
Mark Duncan, Anders Ohlsson, David Streever, Rich Jones e outros – tantos que não conseguiríamos
mencionar.
Finalmente, agradecemos ao pessoal da Macmillan: Shelley Johnston, Gus Miklos, Dan Scherf e
tantos outros que trabalham em tarefas de suporte, os quais nunca vimos mas, sem sua ajuda, este livro
não seria uma realidade.
Os Autores
Steve Teixeira é vice-presidente de Desenvolvimento de Software na DeVries Data Systems, uma empre-
sa de consultoria sediada no Vale do Silício, especializada em soluções da Borland/Inprise. Anteriormen-
te, era engenheiro de software de pesquisa e desenvolvimento na Inprise Corporation, onde ajudou a
projetar e desenvolver o Delphi e o C++Builder, ambos da Borland. Steve também é colunista da The
Delphi Magazine, consultor e treinador profissional, e palestrante conhecido internacionalmente. Steve
mora em Saratoga, Califórnia, com sua esposa e seu filho.
Xavier Pacheco é o presidente e consultor-chefe da Xapware Technologies, Inc., uma empresa de con-
sultoria/treinamento com sede em Colorado Springs. Xavier constantemente realiza palestras em confe-
rências do setor e é autor colaborador de periódicos sobre o Delphi. É consultor e treinador sobre o
Delphi, conhecido internacionalmente, e membro do seleto grupo de voluntários de suporte do Delphi –
o TeamB. Xavier gosta de passar tempo com sua esposa, Anne, e sua filha, Amanda. Xavier e Anne mo-
ram no Colorado com seus pastores-alemães, Rocky e Shasta.
VIII
Prefácio
Comecei a trabalhar na Borland em meados de 1985, com o intuito de fazer parte da nova geração de fer-
ramentas de programação (o UCSC Pascal System e as ferramentas da linha de comandos simplesmente
não eram suficientes), para ajudar a aperfeiçoar o processo de programação (talvez para deixar um pou-
co mais de tempo para nossas famílias e amigos) e, finalmente, para ajudar a enriquecer a vida dos pro-
gramadores (incluindo eu mesmo). O Turbo Pascal 1.0 mudou a cara das ferramentas de programação de
uma vez por todas. Ele definiu o padrão em 1983.
O Delphi também mudou a cara da programação mais uma vez. O Delphi 1.0 visava facilitar a pro-
gramação orientada a objeto, a programação do Windows e a programação de bancos de dados. Outras
versões do Delphi tentaram aliviar a dor da escrita de aplicações para Internet e aplicações distribuídas.
Embora tenhamos incluído inúmeros recursos aos nossos produtos com o passar dos anos, escrevendo
muitas páginas de documentação e megabytes de ajuda on-line, ainda há mais informações, conhecimen-
to e conselhos necessários para os programadores completarem seus projetos com sucesso.
A manchete poderia ser: “Delphi 5 – Dezesseis Anos em Desenvolvimento”. Não este livro, mas o
produto. Dezesseis anos? – você poderia questionar. Foram aproximadamente 16 anos desde que a pri-
meira versão do Turbo Pascal apareceu em novembro de 1983. Pelos padrões da Internet, esse tempo
todo facilmente estouraria uma Int64. O Delphi 5 é a próxima grande versão que está chegando.
Na realidade, ela é a 13a versão do nosso compilador. Não acredita? Basta executar DCC32.EXE na li-
nha de comandos (costumamos chamá-la de “prompt do DOS”) e você verá o número de versão do com-
pilador e o texto de ajuda para os parâmetros da linha de comandos. Foram necessários muitos engenhei-
ros, testadores, documentadores, autores, fãs, amigos e parentes para a produção de um produto. É ne-
cessária uma classe especial de escritores para poder escrever um livro sobre o Delphi.
O que é preciso para escrever um guia do programador? A resposta simples é “muita coisa”. Como
eu poderia definir isso? Não posso – é impossível definir. Em vez de uma definição, só posso oferecer al-
gumas informações para ajudá-lo a formar a definição, uma “receita”, se preferir:
Variações:
Substitua sua escolha favorita de líquido – água, suco, café etc.
Para citar um comediante famoso, “deixemos toda a seriedade de lado”. Conheci Steve Teixeira (al-
guns o chamam de T-Rex) e Xavier Pacheco (alguns o chamam apenas de X) há anos como amigos, cole-
gas de trabalho, palestrantes em nossa conferência anual de programadores e como membros da comuni-
dade da Borland.
As edições anteriores foram recebidas entusiasticamente pelos programadores Delphi do mundo in-
teiro. Agora, a versão mais recente está pronta para todos aproveitarem.
Divirta-se e aprenda muito. Esperamos que todos os seus projetos em Delphi sejam agradáveis, bem-
sucedidos e recompensadores.
X
Sumário
XV
PARTE V DESENVOLVIMENTO RÁPIDO DE APLICAÇÕES
DE BANCO DE DADOS . . . . . . . . . . . . . . . . . . . . . . . . . 1077
CAPÍTULO 33 GERENCIADOR DE ESTOQUE: DESENVOLVIMENTO
CLIENTE/SERVIDOR . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1079
Projeto do back-end . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1080
Acesso centralizado ao banco de dados: as regras comerciais . . . . . . . . . . . . . . . . 1087
Projeto da interface do usuário . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1101
Resumo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1122
ÍNDICE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1195
Você acredita que já se passaram quase cinco anos desde que começamos a trabalhar na primeira edição
do Delphi? Naquela época, éramos apenas alguns programadores trabalhando no departamento de su-
porte a linguagens da Borland, procurando algum novo desafio no software. Tínhamos uma idéia para
um livro que pudesse evitar coisas que você poderia aprender na documentação do produto em favor de
mostrar práticas de codificação apropriadas e algumas técnicas interessantes. Também achávamos que
nossa experiência com suporte ao programador nos permitiria responder às dúvidas do programador an-
tes mesmo que elas fossem feitas. Levamos a idéia para a Sams e eles se entusiasmaram muito. Depois ini-
ciamos os muitos e extenuantes meses de desenvolvimento do manuscrito, programação, altas horas da
noite, mais programação e talvez alguns prazos perdidos (porque estávamos muito ocupados progra-
mando). Finalmente, o livro foi terminado.
Nossas expectativas eram modestas. A princípio, estávamos apenas esperando sair sem ganhar ou
perder. No entanto, após vários meses de muitas vendas, descobrimos que nosso conceito de um guia do
programador essencial era simplesmente o que o médico (ou, neste caso, o programador) solicitara. Nos-
sos sentimentos se solidificaram quando você, o leitor, votou no Guia do Programador Delphi para o prê-
mio Delphi Informant Reader’s Choice, como melhor livro sobre Delphi.
Creio que nosso editor nos introduziu de mansinho, pois não pudemos mais parar de escrever. Lan-
çamos o Delphi 2 no ano seguinte, completamos um manuscrito para o Delphi 3 (que infelizmente nunca
foi publicado) no ano seguinte e publicamos o Delphi 4 no próximo ano, para o qual formos novamente
honrados com o prêmio Delphi Informant Reader’s Choice como melhor livro sobre Delphi. O que você
tem em suas mãos é o nosso trabalho mais recente, o Delphi 5, e acreditamos que ele será um recurso ain-
da mais valioso do que qualquer edição anterior.
Atualmente, Steve é vice-presidente de Desenvolvimento de Software na DeVries Data Systems,
uma empresa de consultoria sediada no Vale do Silício, especializada em soluções da Borland, e Xavier
possui sua própria forma de consultoria e treinamento em Delphi, a XAPWARE Technologies Inc. Acre-
ditamos que nossa combinação exclusiva de experiência “nas trincheiras” dos departamentos de suporte
ao programador e pesquisa e desenvolvimento da Borland, combinada com nossa experiência do mundo
real como programadores e conhecedores do interior do produto Delphi, constituem a base para um li-
vro muito bom sobre o Delphi.
Simplificando, se você quiser desenvolver apresentações em Delphi, este é o livro perfeito para
você. Nosso objetivo é não apenas mostrar como desenvolver aplicações usando o Delphi, mas desen-
volver aplicações da maneira correta. Delphi é uma ferramenta inigualável, que permite reduzir drasti-
camente o tempo necessário para desenvolver aplicações, oferecendo ainda um nível de desempenho
que atende ou excede o da maioria dos compiladores C++ no mercado. Este livro mostra como obter
o máximo desses dois mundos, demonstrando o uso eficaz do ambiente de projeto do Delphi, técnicas
apropriadas para reutilizar o código e mostrando-lhe como escrever um código bom, limpo e eficiente.
Este livro está dividido em cinco partes. A Parte I oferece uma base forte sobre os aspectos impor-
tantes da programação com Delphi e Win32. A Parte II utiliza essa base para ajudá-lo a montar aplicações
e utilitários pequenos, porém úteis, que o ajudam a expandir seu conhecimento dos tópicos de progra-
mação mais complexos. A Parte III discute o desenvolvimento de componentes da VCL e o desenvolvi-
mento usando COM. A Parte IV o acompanha pelas etapas de desenvolvimento de banco de dados no
Delphi, desde tabelas locais até bancos de dados SQL e soluções em várias camadas. A Parte V reúne XVII
grande parte do que você aprendeu nas partes anteriores para montar aplicações do mundo real em esca-
la maior.
Capítulos no CD
Não há dúvida de que você já viu o sumário, e pode ter notado que existem vários capítulos que apare-
cem apenas no CD e não estão no livro impresso. O motivo para isso é simples: escrevemos mais material
do que poderia ser incluído em um único livro. Devido a esse problema, tivemos várias escolhas. Pode-
ríamos dividir o Guia do Programador Delphi 5 em dois livros, mas decidimos não fazer isso principal-
mente porque os leitores teriam que pagar mais para obter o material. Outra opção foi omitir alguns ca-
pítulos inteiramente, mas achamos de isso criaria alguns buracos óbvios na cobertura do livro. A escolha
que fizemos, naturalmente, foi colocar alguns capítulos no CD. Isso nos permitiu equilibrar os pesos en-
tre cobertura, conveniência e custo. É importante lembrar que os capítulos no CD não são “extras”, mas
uma parte integral do livro. Eles foram escritos, revisados e editados com o mesmo cuidado e atenção aos
detalhes que todo o restante do livro.
Começando
As pessoas costumam nos perguntar o que nos leva a continuar escrevendo livros sobre o Delphi. É difícil
explicar, mas sempre que encontramos outros programadores e vemos sua cópia obviamente bem utili-
zada, cheia de marcadores e um tanto surrada do Delphi – Guia do Desenvolvedor, de alguma forma isso
nos recompensa.
Agora é hora de relaxar e divertir-se programando com Delphi. Começaremos devagar, mas passa-
remos para tópicos mais avançados em um ritmo rápido, porém satisfatório. Antes que você perceba,
terá o conhecimento e as técnicas necessárias para ser verdadeiramente chamado de guru do Delphi.
XIX
Fundamentos para PARTE
Desenvolvimento
Rápido
I
NE STA PART E
1 Programação do Windows no Delphi 5 3
3 A API do Win32 95
Windows no Delphi 5
1
NE STE C AP ÍT UL O
l A família de produtos Delphi 4
l Delphi: o que é e por quê 6
l Uma pequena história 9
l A IDE do Delphi 12
l Uma excursão pela fonte do seu projeto 15
l Viagem por uma pequena aplicação 17
l O que há de tão interessante nos eventos? 18
l Criação avançada de “protótipos” 19
l Ambiente e componentes extensíveis 20
l Os 10 recursos mais importantes da IDE que você
precisa conhecer e amar 20
l Resumo 23
Este capítulo apresenta uma visão geral de alto nível do Delphi, incluindo história, conjunto de recursos,
como o Delphi se adapta ao mundo do desenvolvimento no Windows e um apanhado geral das informa-
ções de que você precisa para se tornar um programador em Delphi. Para deixá-lo com água na boca com
as potencialidades abertas por essa linguagem, este capítulo também discute os recursos indispensáveis
da IDE do Delphi, dando ênfase particularmente em alguns recursos tão raros que até mesmo os progra-
madores experientes em Delphi podem não ter ouvido falar da existência deles. Este capítulo não tem a
finalidade de ensinar os fundamentos do processo de desenvolvimento de software no ambiente Delphi.
Acreditamos que você tenha gasto o seu rico dinheirinho com este livro para aprender coisas novas e in-
teressantes – e não para ler um pastiche de um conteúdo ao qual você pode ter acesso na documentação
da Borland. Na verdade, nossa missão é demonstrar as vantagens: mostrar-lhe os poderosos recursos
desse produto e, em última análise, como empregar esses recursos para construir softwares de qualidade
comercial. Esperamos que nosso conhecimento e experiência com a ferramenta nos possibilite lhe forne-
cer alguns “insights” interessantes e úteis ao longo do caminho. Acreditamos que tanto os programado-
res que já conhecem a linguagem Delphi como aqueles que somente agora estão entrando nesse universo
possam tirar proveito deste capítulo (e deste livro!), desde que os neófitos entendam que esta obra não é
o marco zero de sua caminhada para se tornar um programador em Delphi. Inicie com a documentação
da Borland e os exemplos simples. Uma vez que você tenha se familiarizado com o funcionamento da
IDE e o be-a-bá do desenvolvimento de aplicações, seja bem-vindo a bordo e faça uma boa viagem!
IDE visual
a
Co
utur
mp
Estr
ilad
or
s
ado
d
Lin
de
gu
ag
o
nc
em
Ba
Delphi 1
Nos primórdios do DOS, programadores se viam diante do seguinte dilema: por um lado, tinham o pro-
dutivo porém lento BASIC e, do outro, a eficiente porém complexa linguagem Assembly. O Turbo Pas-
cal, que oferecia a simplicidade de uma linguagem estruturada e o desempenho de um compilador real,
supria essa deficiência. Programadores em Windows 3.1 se viram diante de uma encruzilhada semelhan-
te – por um lado, tinham uma linguagem poderosa porém pesada como o C++ e, de outro, uma lingua- 9
gem fácil de usar porém limitada como o Visual Basic. O Delphi 1 resolveu esse dilema oferecendo uma
abordagem radicalmente diferente para o desenvolvimento do Windows: desenvolvimento visual, exe-
cutáveis compilados, DLLs, bancos de dados, enfim um ambiente visual sem limites. O Delphi 1 foi a pri-
meira ferramenta de desenvolvimento do Windows que combinou um ambiente de desenvolvimento vi-
sual, um compilador de código nativo otimizado e um mecanismo de acesso a um banco de dados redi-
mensionável. Remonta a essa época o surgimento do conceito RAD, ou seja, de desenvolvimento rápido
de aplicação.
A combinação de compilador, ferramenta RAD e acesso rápido ao banco de dados mostrou-se mui-
to atraente para as fileiras de programadores em VB, e o Delphi conquistou assim muitos adeptos. Mui-
tos programadores em Turbo Pascal também reinventaram suas carreiras migrando para esta nova e astu-
ta ferramenta. Começou a correr a idéia de que a Object Pascal não era a mesma linguagem que nos fize-
ram usar na faculdade, dando-nos a sensação de que estávamos programando com uma mão presa às cos-
tas. Moral da história: uma nova leva de programadores debandou para o Delphi para tirar proveito dos
robustos padrões de projeto encorajados pela linguagem e pela ferramenta. A equipe do Visual Basic da
Microsoft, que até o surgimento do Delphi não tinha um concorrente sério, foi pega totalmente de sur-
presa. Lento, pesado e burro, o Visual Basic 3 não era um adversário à altura do Delphi 1.
Estávamos no ano de 1995. A Borland levou um duro golpe na Justiça, que a obrigou a pagar uma
pesada indenização à Lotus, que entrou com um processo devido à semelhança entre as interface do
1-2-3 e a do Quattro. A Borland também sofreu alguns reveses da Microsoft, ao tentar disputar um es-
paço no mercado de softwares com a Microsoft. A Borland saiu do mercado de aplicativos vendendo o
Quattro para a Novell e direcionando o dBASE e o Paradox para programadores de bancos de dados,
deixando de lado os usuários casuais. Enquanto a Borland disputava o mercado de aplicativos, a Mi-
crosoft alavancara silenciosamente o setor de plataforma e, assim, surrupiou da Borland uma vasta fa-
tia do mercado de ferramentas para programadores do Windows. Voltando a se concentrar no que ti-
nha de melhor, as ferramentas para programador, a Borland voltou a causar um novo estrago no mer-
cado com o Delphi e uma nova versão do Borland C++.
Delphi 2
Um ano depois, o Delphi 2 fornecia os mesmos benefícios para os sistemas operacionais de 32 bits da Mi-
crosoft, o Windows 95 e o Windows NT. Além disso, o Delphi 2 estendeu a produtividade com recursos
e funcionalidade adicionais não encontrados na versão 1, como um compilador de 32 bits capaz de pro-
duzir aplicações mais rápidas, uma biblioteca de objetos melhorada e estendida, suporte a banco de da-
dos reforçado, tratamento de strings aperfeiçoado, suporte a OLE, Visual Form Inheritance e compatibi-
lidade com projetos Delphi de 16 bits. O Delphi 2 tornou-se o padrão com base no qual todas as outras
ferramentas RAD passaram a ser medidas.
Estávamos agora em 1996 e a mais importante versão de plataforma do Windows desde a 3.0 – o
Windows 95 de 32 bits – chegara ao mercado no segundo semestre do ano anterior. A Borland estava an-
siosa para tornar o Delphi 2 a grande ferramenta de desenvolvimento dessa plataforma. Uma nota histó-
rica interessante é que o Delphi 2 originalmente ia se chamar Delphi 32, dando ênfase ao fato de que fora
projetado para o Windows de 32 bits. Entretanto, o nome do produto foi mudado para Delphi 2 antes do
lançamento, a fim de ilustrar que o Delphi era um produto maduro e evitar o trauma da primeira versão,
que é conhecida no setor de software como “blues do 1.0”.
A Microsoft tentou contra-atacar com o Visual Basic 4, mas esse produto caiu no campo de batalha,
vitimado por um desempenho fraco, ausência de portabilidade de 16 para 32 bits e falhas fundamentais
no projeto. Entretanto, um impressionante número de programadores continuou a usar o Visual Basic
por qualquer que fosse a razão. A Borland também desejava ver o Delphi penetrar no sofisticado merca-
do cliente/servidor, dominado por ferramentas como o PowerBuilder, mas essa versão ainda não tinha a
musculatura necessária para desbancar esses produtos das grandes empresas.
A estratégia da empresa nessa época era, sem sombra de dúvidas, atacar os clientes corporativos.
Com toda a certeza, a decisão para trilhar esse novo caminho teve como principal estímulo a perda de
10 mercado do dBASE e do Paradox, bem como a queda nas receitas do mercado de C++. Pensando em ga-
nhar solidez para atacar o mercado corporativo, a Borland cometeu o erro de assumir o controle da
Open Environment Corporation, empresa essa que basicamente contava com dois produtos: um obsole-
to middleware baseado no DCE, que você poderia chamar de ancestral do CORBA, e uma tecnologia
proprietária para OLE distribuído, prestes a ser sucateada devido ao surgimento da DCOM.
Delphi 3
Durante o desenvolvimento do Delphi 1, a equipe de desenvolvimento do Delphi só estava preocupada
com a criação e o lançamento de uma ferramenta de desenvolvimento revolucionária. Para o Delphi 2, a
equipe de desenvolvimento tinha como principal objetivo migrar para o ambiente de 32 bits (mantendo
uma compatibilidade quase total) e adicionar novos recursos de banco de dados e cliente/servidor, usa-
dos pela tecnologia de informações das grandes corporações. Durante a criação do Delphi, a equipe de
desenvolvimento teve a oportunidade de expandir o conjunto de ferramentas para fornecer um extraor-
dinário nível de amplitude e profundidade para soluções de alguns dos problemas enfrentados pelos pro-
gramadores do Windows. Em particular, o Delphi 3 facilitou o uso de tecnologias notoriamente compli-
cadas do COM e ActiveX, o desenvolvimento de aplicações para Word Wide Web, aplicações “cliente
magro” e várias arquiteturas de banco de dados de múltiplas camadas. O Code Insight do Delphi 3 aju-
dou a tornar mais fácil o processo de escrita em código propriamente dito, embora em grande parte a
metodologia básica para escrever aplicações do Delphi fosse igual à do Delphi 1.
Estávamos em 1997 e a competição estava fazendo algumas coisas interessantes. No nível de entra-
da, a Microsoft finalmente começou a obter algum êxito com o Visual Basic 5, que incluía um compila-
dor capaz de resolver problemas que de há muito vinham comprometendo o desempenho, bom suporte
para COM/ActiveX e alguns recursos fundamentais para a nova plataforma. No topo de linha, o Delphi
agora estava conseguindo desbancar produtos como PowerBuilder e Forte das grandes corporações.
O Delphi perdeu um membro-chave da equipe durante o ciclo de desenvolvimento do Delphi 3
quando Anders Hejlsberg, o arquiteto-chefe, resolveu ir trabalhar na Microsoft Corporation. Entre-
tanto, o grupo não sentiu muito essa perda, pois Chuck Jazdzewski, que há muito tempo era co-
arquiteto, assumiu o comando a contento. Nessa mesma época, a empresa também perdeu Paul
Gross, também para a Microsoft, embora essa perda tenha causado muito mais impacto no campo
das relações públicas do que no dia-a-dia do setor de desenvolvimento de software.
Delphi 4
A prioridade do Delphi 4 foi facilitar o desenvolvimento no Delphi. O Module Explorer foi introduzido
no Delphi, permitindo ao usuário procurar e editar unidades a partir de uma prática interface gráfica.
Novos recursos de navegação de código e preenchimento de classe permitiram que se voltasse para a es-
sência das suas aplicações com um mínimo de trabalho. A IDE foi reprojetada com barras de ferramentas
e janelas encaixáveis, de modo a tornar seu desenvolvimento mais confortável, e o depurador sofreu
grandes melhorias. O Delphi 4 estendeu o alcance do produto às grandes empresas com um notável su-
porte a camadas múltiplas usando tecnologias como MIDAS, DCOM, MTS e CORBA.
Em 1998, a posição do Delphi estava mais do que consolidada em relação à concorrência. As linhas
de frente tinham se estabilizado, embora pouco a pouco o Delphi continuasse ganhando mercado. O
CORBA provocou um verdadeiro alvoroço no mercado e apenas o Delphi detinha essa tecnologia. Havia
também uma pequena desvantagem para o Delphi 4: depois de vários anos gozando o status de ser a fer-
ramenta de desenvolvimento mais estável do mercado, o Delphi 4 ganhou fama, entre os usuários de lon-
ga data do Delphi, de não manter o altíssimo padrão de engenharia e estabilidade de que a empresa que o
produzia desfrutava.
O lançamento do Delphi 4 seguiu a aquisição da Visigenic, um dos líderes da indústria CORBA. A
Borland, agora chamada de Inprise, depois de tomar a questionável decisão de mudar o nome da compa-
nhia para facilitar a sua entrada no mercado corporativo, estava em condições de levar a indústria para
um novo patamar, integrando suas ferramentas com a tecnologia CORBA. Para vencer de verdade, o
CORBA precisava se tornar tão fácil quanto o desenvolvimento da Internet ou COM tinha se tornado 11
nas versões anteriores das ferramentas da Borland. Entretanto, por várias razões, a integração não foi tão
completa quanto deveria ter sido e a integração da ferramenta de desenvolvimento CORBA estava fada-
da a desempenhar um papel secundário no quadro geral de desenvolvimento de software.
Delphi 5
O Delphi 5 adianta algumas peças no tabuleiro: primeiro, o Delphi 5 continua o que o Delphi 4 ini-
ciou, adicionando muito mais recursos para facilitar a execução de tarefas que tradicionalmente são
muito demoradas, felizmente permitindo que você se concentre mais no que deseja escrever e menos
em como escrevê-lo. Esses novos recursos de produtividade incluem novas melhorias na IDE e no de-
purador, o software de desenvolvimento em equipe TeamSource e ferramentas de tradução. Segundo,
o Delphi 5 contém uma série de novos recursos que de fato facilitam o desenvolvimento para a Inter-
net. Esses novos recursos de Internet incluem o Active Server Object Wizard para criação de ASP, os
componentes do InternetExpress para suporte a XML e os novos recursos de MIDAS, o que fez dele
uma plataforma de dados extremamente versátil para a Internet. Finalmente, a Borland incluiu tempo
na agenda para chegar ao mais importante recurso do Delphi 5: estabilidade. Como um bom vinho,
você não pode ter pressa para ter um bom software, e a Borland esperou até o Delphi 5 ficar pronto
para lançá-lo no mercado.
O Delphi 5 foi lançado no segundo semestre de 1999. O Delphi continua a conquistar espaço no
mercado de grandes corporações e a competir em igualdade de condições com o Visual Basic em nível
de entrada. Entretanto, a batalha continua acirrada. A Inprise teve o bom-senso de retomar o nome
Borland, medida essa que foi bastante apreciada pelos clientes mais antigos. Os executivos enfrenta-
ram alguma turbulência depois que a empresa foi dividida entre ferramentas e middleware, da abrupta
saída do diretor-executivo Del Yocam e da contratação de Dale Fuller, um especialista em Internet,
para comandá-la. Fuller redirecionou a empresa para os programadores de software, e seus produtos
parecem tão bons quanto nos velhos tempos. Acreditamos que a Inprise finalmente tenha reencontra-
do o caminho certo.
O futuro?
Embora o histórico do produto seja importante, talvez ainda mais importante seja o que o futuro reserva
para o Delphi. Usando a história como um guia, podemos prever, com razoável margem de acerto, que o
Delphi continuará a ser uma grande alternativa para se desenvolver aplicações para Windows por um
longo tempo. Para mim, a grande questão é se nós algum dia veremos versões do Delphi destinadas a uma
plataforma que não a Win 32. Baseado em informações provenientes da Borland, parece que essa preo-
cupação já faz parte do cotidiano da empresa. Na Borland Conference em 1998, o arquiteto-chefe do
Delphi, Chuck Jazdzewski, apresentou uma versão do compilador Delphi que gerava código de bytes em
Java, que teoricamente poderia se destinar a qualquer computador equipado com uma Java Virtual Ma-
chine. Embora existam obstáculos técnicos óbvios a esse tipo de tecnologia, e ainda esteja longe o dia em
que a tecnologia Delphi para Java venha a se tornar um produto, ela confirma a hipótese de fazer parte
da estratégia da Borland migrar o Delphi para outras plataformas. Mais recentemente, na Borland Con-
ference de 1999, o diretor-executivo Dale Fuller deixou escapar, no discurso de abertura dos trabalhos,
que existem planos para produzir uma versão do Delphi destinada à plataforma Linux.
A IDE do Delphi
Para garantir que todos nós estejamos na mesma página no tocante à terminologia, a Figura 1.2 mostra a
IDE do Delphi e chama atenção para seus principais itens: a janela principal, a Component Palette, as
barras de ferramentas, o Form Designer, o Code Editor, o Object Inspector e o Code Explorer.
12
Barras de ferramentas Janela principal Component Palette
Object
Inspector
A janela principal
Imagine a janela principal como o centro de controle da IDE do Delphi. A janela principal tem toda a
funcionalidade padrão da janela principal de qualquer outro programa para Windows. Ela consiste em
três partes: o menu principal, as barras de ferramentas e a Component Palette.
O menu principal
Como em qualquer programa Windows, você vai para o menu principal quando precisa abrir e salvar ar-
quivos, chamar assistentes, exibir outras janelas e modificar opções, entre outras coisas. Cada item no
menu principal pode também ser chamado através de um botão na barra de ferramentas.
A Component Palette
A Component Palette é uma barra de ferramentas com altura dupla que contém um controle de página
com todos os componentes da VCL e controles ActiveX instalados na IDE. A ordem e a aparência das
páginas e componentes na Component Palette podem ser configuradas com um clique do botão direito
do mouse ou selecionando Component, Configure Palette (configurar palheta) no menu principal.
O Form Designer
O Form Designer inicia com uma janela vazia, pronta para ser transformada em uma aplicação do Win-
dows. Considere o Form Designer como a tela na qual você pode criar aplicações do Windows; é aqui
que você determina como suas aplicações serão representadas visualmente para seus usuários. Você in-
terage com o Form Designer selecionando componentes a partir da Component Palette e soltando-os
no formulário. Depois de ter incluído um componente qualquer no formulário, você pode usar o mou-
se para ajustar a posição ou o tamanho desse componente. Você pode controlar a aparência e o com-
portamento desses componentes usando o Object Inspector e o Code Editor.
O Object Inspector
Com o Object Inspector, você pode modificar as propriedades do formulário ou do componente, ou per-
mitir que seu formulário ou componente responda a diferentes eventos. Propriedades são dados como altu-
ra, cor e fonte, os quais determinam como um objeto aparece na tela. Eventos são trechos de código execu-
tados em resposta a determinadas ocorrências dentro da sua aplicação. Uma mensagem de clique do mouse
e uma mensagem para que uma janela seja redesenhada são dois exemplos de eventos. A janela Object
Inspector usa a metáfora de guias de caderno padrão do Windows, que utiliza guias para alternar entre pro-
priedades de componente ou eventos; basta selecionar a página desejada a partir da guia no alto da janela.
As propriedades e eventos exibidos no Object Inspector refletem o formulário ou componente atualmente
selecionado no Form Designer.
Uma das novidades do Delphi 5 é a sua habilidade ao organizar o conteúdo do Object Inspector por
categoria ou nome em ordem alfabética. Você pode fazer isso dando um clique com o botão direito do
14 mouse em qualquer lugar no Object Inspector e selecionando Arrange (organizar) a partir do menu local.
A Figura 1.5 mostra dois Object Inspectors lado a lado. O que se encontra à esquerda é organizado por
categoria e o que se encontra à direita é organizado por nome. Você também pode especificar as catego-
rias que gostaria de exibir selecionando View (exibir) a partir do menu local.
Uma das informações que você, como um programador em Delphi, realmente precisa saber é que o
sistema de ajuda está altamente integrado ao Object Inspector. Se você sempre empacar em uma determi-
nada propriedade ou evento, basta pressionar a tecla F1 para ser salvo pelo WinHelp.
O Code Editor
O Code Editor é o local no qual você digita o código que dita como seu programa se comporta e onde o
Delphi insere o código que ele gera baseado nos componentes em sua aplicação. A parte superior da jane-
la do Code Editor contém uma série de guias e cada uma delas corresponde a um arquivo ou módulo di-
ferente do código-fonte. Cada vez que você adiciona um novo formulário à sua aplicação, uma nova uni-
dade é criada e adicionada ao conjunto de guias na parte superior do Code Editor. O menu local no Code
Editor dá uma ampla gama de opções durante o processo de edição, como fechar arquivos, definir mar-
cadores e navegar para símbolos.
DICA
Você pode exibir diversas janelas do Code Editor simultaneamente selecionando View, New Edit Window
(exibir, nova janela de edição) a partir do menu principal.
O Code Explorer
O Code Explorer fornece um modo de exibição da unidade mostrada no Code Editor em estilo de árvo-
re. O Code Explorer facilita a navegação entre as unidades e a inclusão de novos elementos ou a mudan-
ça de nome dos elementos existentes em uma unidade. É importante lembrar que existe um relaciona-
mento de um-para-um entre as janelas do Code Explorer e as janelas do Code Editor. Dê um clique com
o botão do mouse em um nó no Code Explorer para exibir as opções disponíveis para esse nó. Você tam-
bém pode controlar comportamentos como classificação e filtro no Code Explorer, modificando as op-
ções encontradas na guia Explorer da caixa de diálogo Environment Options (opções de ambiente).
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs;
type
TForm1 = class(TForm)
private
{ Declarações privadas }
public
{ Declarações públicas }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
end.
Isso indica que o objeto de formulário, ele mesmo, é um objeto derivado do TForm e o espaço no
qual você pode inserir suas próprias variáveis públicas e privadas é claramente identificado. Não se preo-
cupe agora com o que significa objeto, público ou privado. O Capítulo 2 discute o Object Pascal de modo
mais detalhado.
A linha a seguir é muito importante:
{$R *.DFM}
A diretiva $R em Pascal é usada para carregar um arquivo de recurso externo. Essa linha vincula o
arquivo .DFM (que é a sigla de Delphi form) ao executável. O arquivo .DFM contém uma representação
binária do formulário que você criou no Form Designer. O símbolo * nesse caso não tem a finalidade de
representar um curinga; ele representa o arquivo que tem o mesmo nome que a unidade atual. Por exem-
plo, se essa mesma linha estivesse em um arquivo chamado Unit1.pas, o *.DFM poderia representar um ar-
16 quivo com o nome Unit1.dfm.
NOTA
Um novo recurso do Delphi 5 é a capacidade da IDE de salvar novos arquivos DFM no formato de texto, ao
invés do formato binário. Essa opção é permitida por padrão, mas você pode modificá-la usando a caixa
de seleção New forms as text (novos formulários como texto) da página Preferences da caixa de diálogo
Environment Options. Embora salvar formulários no formato de texto seja um pouco menos eficiente em
termos de tamanho, essa é uma boa prática por duas razões: primeiro, é muito fácil fazer pequenas altera-
ções, em qualquer editor de textos, no arquivo DFM de texto. Segundo, se o arquivo for danificado, será
muito mais fácil reparar um arquivo de texto danificado do que um arquivo binário danificado. Lembre-se
também de que as versões anteriores do Delphi esperam arquivos DFM binários e, portanto, você terá que
desativar essa opção se desejar criar projetos que serão usados por outras versões do Delphi.
Basta dar uma olhada no arquivo de projeto da aplicação para saber o valor dele. O nome de arqui-
vo de um projeto termina com .DPR (significando Delphi project) e na verdade não passa de um arqui-
vo-fonte do Pascal com uma extensão de arquivo diferente. É no arquivo de projeto que se encontra a
parte principal do seu programa (do ponto de vista do Pascal). Ao contrário das outras versões do Pascal
com as quais você deve estar familiarizado, a maioria do “trabalho” do seu programa é feita em unidades,
e não no módulo principal. Você pode carregar o arquivo-fonte do seu projeto no Code Editor selecio-
nando Project, View Source (exibir fonte) a partir do menu principal.
Veja a seguir o arquivo de projeto da aplicação de exemplo:
program Project1;
uses
Forms,
Unit1 in ‘Unit1.pas’ {Form1};
{$R *.RES}
begin
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
À medida que você adiciona mais formulários e unidades para a aplicação, eles aparecem na cláusu-
la uses do arquivo de projeto. Observe, também, que depois do nome de uma unidade na cláusula uses, o
nome do formulário relatado aparece nos comentários. Se você estiver confuso a respeito da relação en-
tre unidades e formulários, poderá esclarecer tudo selecionando View, Project Manager (gerenciador de
projeto) para abrir a janela Project Manager.
NOTA
Cada formulário tem exatamente uma unidade associada a ele e, além dele, você pode ter outras unidades
“apenas de código”, que não estão associadas a qualquer formulário. No Delphi, você trabalha principal-
mente dentro das unidades do programa, e raramente precisa editar o arquivo .DPR do seu projeto.
type
TForm1 = class(TForm)
Button1: TButton;
private 17
{ Declarações privadas }
public
{ Declarações públicas }
end;
Agora, como você pode ver, o botão é uma variável de instância da classe TForm1. Mais tarde, quando
você fizer referência ao botão fora do contexto TForm1 no seu código-fonte, terá que se lembrar de endere-
çá-lo como parte do escopo do TForm1 através da instrução Form1.Button1. O escopo é explicado com maio-
res detalhes no Capítulo 2.
Quando esse botão é selecionado no Form Designer, você pode alterar seu comportamento atra-
vés do Object Inspector. Suponha que, durante o projeto, você queira alterar a largura do botão para
100 pixels e, em runtime, você queira fazer com que o botão responda a um toque dobrando sua pró-
pria altura. Para alterar a largura do botão, vá para a janela Object Browser, procure a propriedade
Width e altere o valor associado à largura para 100. Observe que a alteração não é efetivada no Form De-
signer até você pressionar Enter ou sair da propriedade Width. Para fazer o botão responder a um clique
do mouse, selecione a página Events na janela Object Inspector para expor sua lista de eventos ao qual
o botão pode responder. Dê um clique duplo na coluna próxima ao evento Onclick para que o Delphi
gere um esquema de projeto para uma resposta a um clique do mouse e o remeta para o lugar apropria-
do no código-fonte – nesse caso, um procedimento chamado TForm1.Button1Click( ). Tudo o que você
precisa fazer é inserir o código para dobrar a largura do botão entre o começo e o fim do método de
resposta ao evento:
Button1.Height := Button1.Height * 2;
Para verificar se a “aplicação” é compilada e executada com sucesso, pressione a tecla F9 no seu te-
clado e veja o que acontece!
NOTA
O Delphi mantém uma referência entre procedimentos gerados e os controles aos quais eles correspon-
dem. Quando você compila ou salva um módulo do código-fonte, o Delphi varre o código-fonte e remove
todas as estruturas de procedimento para as quais você não tenha digitado algum código entre o início e o
fim. Isso significa que, se você não escrevesse nenhum código entre o begin e o end do procedimento
TForm1.Button1Click( ), por exemplo, o Delphi teria removido o procedimento do código-fonte. Moral da
história: não exclua procedimentos de manipulador de evento que o Delphi tenha criado; basta excluir o
código e deixar o Delphi remover os procedimentos para você.
Depois de se divertir tornando o botão realmente grande no formulário, encerre o programa e volte
para a IDE do Delphi. Agora é uma boa hora para lembrar que você poderia ter gerado uma resposta a
um clique de mouse para seu botão dando um clique duplo no controle depois de dobrar seu tamanho no
formulário. Um clique duplo em um componente faz surgir automaticamente o editor de componentes
associado a ele. Para a maioria dos componentes, essa resposta gera um manipulador para o primeiro
evento do componente listado no Object Inspector.
Todas as informações de que você precisa sobre a tecla estão ao alcance dos seus dedos. Se você não
é um programador experiente do Windows, gostará do fato de que não há parâmetros LParam ou WParam,
manipuladores herdados, traduções ou despachos para se preocupar. Isso é muito além de “desvendar
mensagens”, pois um evento do Delphi pode representar diferentes mensagens do Windows, como é o
caso de OnMouseDown (que manipula uma série de mensagens de mouse). Além disso, cada um dos parâme-
tros de mensagem é passado como parâmetros fáceis de entender. O Capítulo 5 dará mais detalhes sobre
o funcionamento do sistema de troca de mensagens interno do Delphi.
1. Preenchimento de classe
Nada desperdiça mais o tempo de um programador do que precisar digitar todo esse maldito código!
Com que freqüência você sabe exatamente o que deseja escrever, mas está limitado pela velocidade com
que seus dedos podem se mover sobre as teclas? Como todos os tipos de documentação já vêm preenchi-
dos para livrá-lo completamente de toda essa digitação, o Delphi tem um recurso chamado preenchimen-
to de classe, que elimina grande parte do trabalho pesado.
Possivelmente, o recurso mais importante do preenchimento de classe é aquele projetado para fun-
cionar de modo invisível. Basta digitar parte de uma declaração, pressionar a mágica combinação de te-
clas Crtl+Shift+C para que o preenchimento de classe tente adivinhar o que você está tentando fazer e
gerar o código certo. Por exemplo, se você colocar a declaração de um procedimento chamado Foo na sua
classe e ativar o preenchimento de classe, ele automaticamente criará a definição desse método na parte
20
da implementação da unidade. Declare uma nova propriedade que leia um campo e escreva um método e
ative o preenchimento de classe. O código do campo será automaticamente gerado e o método será de-
clarado e implementado.
Se você ainda não entrou em contato com o preenchimento de classe, faça uma experiência. Logo
você vai se sentir perdido sem esse recurso.
4. Encaixe!
A IDE permite organizar as janelas na tela encaixando várias janelas como painéis em uma única janela.
Se você definiu o arraste completo de janelas na área de trabalho, pode identificar facilmente as janelas
que estão encaixadas porque elas desenham uma caixa pontilhada quando são arrastadas pela tela. O
Code Editor oferece três compartimentos de encaixe, um à esquerda, outro à direita e um terceiro no
centro, nos quais você pode encaixar janelas. As janelas podem ser encaixadas lado a lado arrastando-se
uma janela para uma borda de outra, ou encaixadas com guias arrastando-se uma janela para o meio de
outra. Uma vez tendo conseguido uma arrumação adequada, certifique-se de salvá-la usando a barra de
ferramentas Desktops. Quer impedir que uma janela seja encaixada? Mantenha pressionada a tecla Crtl
enquanto a arrasta ou dê um clique com o botão direito do mouse na janela e desative a opção Dockable
(encaixável) no menu local.
DICA
Eis um precioso recurso oculto: dê um clique com o botão direito do mouse nas guias das janelas encaixa-
das e você poderá mover as guias para a parte superior, inferior, esquerda ou direita da janela.
5. Um navegador de verdade
O tímido navegador de objeto do Delphi 1 ao 4 sofreu pouquíssimas alterações. Se você não sabia que ele
existia, não pense que só você é vítima dessa catástrofe; muitas pessoas nunca o usaram porque ele não ti-
nha quase nada a oferecer. Finalmente, o Delphi 5 veio equipado com um navegador de objeto de verda-
de! Mostrado na Figura 1.6, o novo navegador é acessível selecionando-se View, Browser no menu prin-
cipal. Essa ferramenta apresenta uma visão de árvore que lhe permite navegar pelas globais, classes e uni-
dades e aprofundar-se no escopo, na herança e nas referências dos símbolos.
21
FIGURA 1.6 O novo navegador de objeto.
8. To Do...
Use a To Do List para gerenciar o trabalho em andamento em seus arquivos-fonte. Você pode exibir a To
Do List (lista de coisas a fazer) selecionando View, To Do List no menu principal. Essa lista é automatica-
mente preenchida com todos os comentários em seu código-fonte que comecem com o código TODO. Você
pode usar a janela To Do Items (itens a fazer) para definir o proprietário, a prioridade e a categoria de
qualquer item To Do. Essa janela é mostrada na Figura 1.7, onde aparece fixada na parte inferior do
Code Editor.
Resumo
Agora você deve compreender melhor a linha de produtos do Delphi 5 e a IDE, bem como o modo como
essa linguagem se ajusta ao quadro de desenvolvimento do Windows em geral. Este capítulo teve como
objetivo familiarizá-lo com o Delphi e com os conceitos usados ao longo de todo o livro. Agora o palco
está pronto para o grande espetáculo, que mal começou. Antes de você ousar mergulhos mais profundos
neste livro, certifique-se de usar e navegar à vontade pela IDE, além de saber como trabalhar com peque-
nos projetos.
23
A linguagem CAPÍTULO
Object Pascal
2
NE STE C AP ÍT UL O
l Comentários 25
l Novos recursos de procedimento e função 25
l Variáveis 27
l Constantes 28
l Operadores 30
l Tipos do Object Pascal 33
l Tipos definidos pelo usuário 53
l Typecast e conversão de tipo 62
l Recursos de string 63
l Testando condições 64
l Loops 65
l Procedimentos e funções 67
l Escopo 71
l Unidades 72
l Pacotes 74
l Programação orientada a objeto 75
l Como usar objetos do Delphi 77
l Tratamento estruturado de exceções 87
l Runtime Type Information 93
l Resumo 94
Além de definir os elementos visuais do Delphi, este capítulo contém uma visão geral da linguagem básica
do Delphi – Object Pascal. Para começar, você será apresentado aos fundamentos da linguagem Object Pas-
cal, como regras e construção da linguagem. Depois, aprenderá sobre alguns dos mais avançados aspectos
do Object Pascal, como classes e tratamento de exceções. Como este não é um livro para iniciantes, deduzi-
mos que você já tem alguma experiência com outras linguagens de alto nível para computador, como C,
C++ ou Visual Basic, e compara a estrutura da linguagem Object Pascal com essas outras linguagens. Ao
terminar este capítulo, você entenderá como conceitos de programação, como variáveis, tipos, operadores,
loops, cases, exceções e objetos funcionam no Pascal em relação ao C++ e ao Visual Basic.
Mesmo que você tenha alguma experiência recente com Pascal, irá achar este capítulo útil, pois este
é o único ponto no livro em que você aprende os grandes macetes e dicas sobre a sintaxe e a semântica do
Pascal.
Comentários
Como ponto de partida, você deve saber como fazer comentários no código Pascal. O Object Pascal su-
porta três tipos de comentários: comentários entre chaves, comentários entre parêntese/asterisco e co-
mentários de barra dupla. Veja a seguir um exemplo de cada um desses tipos de comentário:
O dois tipos de comentários do Pascal têm um comportamento praticamente idêntico. Para o com-
pilador, comentário é tudo que se encontra entre os delimitadores de abertura e fechamento de comentá-
rio. Para o comentário no estilo do C++, tudo que vem depois da barra dupla até o fim da linha é consi-
derado um comentário:
NOTA
Você não pode aninhar comentários do mesmo tipo. Embora, do ponto de vista da sintaxe, seja legal ani-
nhar comentários Pascal de diferentes tipos um dentro do outro, não recomendamos essa prática. Veja os
exemplos a seguir:
{ (* Isto é legal *) }
(* { Isto é legal } *)
(* (* Isto é ilegal *) *)
{ { Isto é ilegal } }
Parênteses
Embora não seja novo para o Delphi 5, um dos recursos menos conhecidos do Object Pascal é que parên-
teses são opcionais quando chamamos um procedimento ou função que não utiliza parâmetros. Por esse
motivo, ambos os exemplos de sintaxe a seguir são válidos:
Form1.Show;
Form1.Show( );
25
Esse recurso não é o que se pode chamar de uma maravilha do outro mundo, mas é particularmente
bom para aqueles que dividem seu tempo entre Delphi e linguagens como C++ ou Java, onde os parên-
teses são obrigatórios. Se você não trabalha apenas no Delphi, esse recurso significa que você não precisa
se lembrar de usar uma sintaxe de chamada de função diferente para linguagens diferentes.
Overloading
O Delphi 4 introduziu o conceito de overloading (sobrecarga) de função (ou seja, a capacidade de ter vá-
rios procedimentos ou funções de mesmo nome com diferentes listas de parâmetros). Todo método de
overload tem que ser declarado com a diretiva de overload, como mostrado aqui:
procedure Hello(I: Integer); overload;
procedure Hello(S: string); overload;
procedure Hello(D: Double); overload;
Observe que as regras para métodos de overload de uma classe são ligeiramente diferentes e estão
explicados na seção “Método de overload”. Embora esse seja um dos recursos mais solicitados pelos pro-
gramadores desde o Delphi 1, a frase que aparece na mente é esta: “Cuidado com o que deseja.” O fato
de ter várias funções e procedimentos com o mesmo nome (além da capacidade tradicional de ter fun-
ções e procedimentos de mesmo nome em diferentes unidades) pode dificultar a previsão do fluxo de
controle e a depuração da sua aplicação. Por isso, o overloading é um recurso que você deve empregar
com prudência. Não digo que você deva evitá-lo; apenas não abuse dele.
É possível chamar o procedimento HasDefVal( ) de duas formas. Na primeira, você pode especificar
ambos os parâmetros:
HasDefVal(‘hello’, 26);
Na segunda, você pode especificar apenas o parâmetro S e usar o valor default para I:
HasDefVal(‘hello’); // valor default usado para I
26
function AddInts(I1, I2: Integer): Integer;
begin
Result := I1 + I2;
end;
Para se manter competitivo, você sente que deve atualizar essa função de modo que ela tenha a ca-
pacidade para somar três números. Entretanto, você odeia fazer isso porque adicionar um parâmetro im-
pedirá que o código existente, que chama essa função, seja compilado. Graças aos parâmetros default,
você pode aperfeiçoar a funcionalidade de AddInts( ) sem comprometer a compatibilidade. Veja o exem-
plo a seguir:
function AddInts(I1, I2: Integer; I3: Integer = 0);
begin
Result := I1 + I2 + I3;
end;
Variáveis
Você deve estar acostumado a declarar variáveis aonde for preciso: “Eu preciso de outro inteiro e, por-
tanto, vou declarar um bem aqui no meio desse bloco de código.” Se essa tem sido sua prática, você terá
que se reciclar um pouco para usar variáveis em Object Pascal. O Object Pascal exige que você declare to-
das variáveis em uma seção exclusiva para elas antes de iniciar um procedimento, função ou programa.
Talvez você esteja acostumado a escrever código desta forma:
void foo(vazio)
{
int x = 1;
x++;
int y = 2;
float f;
//... etc ...
}
No Object Pascal, esse tipo de código deve ser amarrado e estruturado, como no exemplo a seguir:
Procedure Foo;
var
x, y: Integer;
f: Double;
begin
x := 1;
inc(x);
y := 2;
//... etc ...
end;
Você deve estar se perguntando o que é toda essa história de estrutura e para que ela serve. Você
descobrirá, entretanto, que o estilo estruturado do Object Pascal torna o código mais legível, facilita a
sua manutenção e tem uma incidência de bugs menor do que o estilo do C++ ou Visual Basic, que pode
causar alguma confusão.
Observe como o Object Pascal permite que você agrupe mais de uma variável de mesmo tipo na
mesma linha com a seguinte sintaxe:
NomeDaVariável1, NomeDaVariável2: AlgumTipo;
27
NOTA
O Object Pascal – como o Visual Basic, mas ao contrário do C e do C++ – não é uma linguagem que
faça distinção entre letras maiúsculas e minúsculas. As letras maiúsculas e minúsculas são usadas
apenas por uma questão de legibilidade; portanto, seja criterioso e use um estilo como o deste li-
vro. Se o nome do identificador for uma junção de várias palavras, lembre-se de colocar a inicial de
cada uma delas em maiúscula, para torná-lo mais legível. Por exemplo, o nome a seguir é confuso e
difícil de ler:
procedure onomedesteprocedimentonãofazsentido
procedure OnomeDesteProcedimentoÉMaisClaro
Para obter uma referência completa sobre o estilo de código usado neste livro, consulte o Capítulo 6 no CD
que acompanha esta edição.
Lembre-se de que, quando você está declarando uma variável no Object Pascal, o nome da variável
vem antes do tipo e um sinal de dois-pontos separa as variáveis e os tipos. Observe que a inicialização da
variável é sempre separada da declaração da variável.
Um recurso de linguagem introduzido no Delphi 2 permite que você inicialize variáveis globais den-
tro de um bloco var. Aqui estão alguns exemplos que mostram a sintaxe fazendo isso:
var
i: Integer = 10;
S: string = ‘Hello world’;
D: Double = 3.141579;
NOTA
A inicialização prévia de variáveis só é permitida para variáveis globais, não para variáveis que são locais a
um procedimento ou função.
DICA
Para o compilador Delphi, todo dado global é automaticamente inicializado como zero. Quando sua apli-
cação é iniciada, todos os tipos inteiros armazenarão um 0, tipos ponto flutuante armazenarão 0.0, pontei-
ros serão nil, as strings serão vazias e assim por diante. Portanto, não há a menor necessidade de dados
globais inicializados como zero no seu código-fonte.
Constantes
Constantes em Pascal são definidas em uma cláusula const, cujo comportamento é semelhante ao da pala-
vra-chave const do C. Veja a seguir um exemplo de três declarações de constante em C:
const float ADecimalNumber = 3.14;
const int i = 10;
const char * ErrorString = “Danger, Danger, Danger!”;
A maior diferença entre as constantes do C e as constantes do Object Pascal é que o Object Pascal,
como o Visual Basic, não exige que você declare o tipo da constante juntamente com o valor na declara-
28 ção. O compilador Delphi aloca automaticamente o espaço apropriado para a constante com base no seu
valor, ou, no caso de constante escalar como Integer, o compilador monitora os valores enquanto funcio-
na e o espaço nunca é alocado. Aqui está um exemplo:
const
ADecimalNumber = 3.14;
i = 10;
ErrorString = ‘Danger, Danger, Danger!’;
NOTA
O espaço é alocado para constantes da seguinte maneira: valores Integer são “ajustados” ao menor tipo
aceitável (10 em um ShortInt, 32.000 em um SmallInt etc.). Valores alfanuméricos são ajustados em Char
ou o tipo string atualmente definido (por $H). Valores de ponto flutuante são mapeados para o tipo de
dado estendido, a não ser que o valor contenha quatro ou menos espaços decimais explicitamente; nesse
caso, ele é mapeado para um tipo Comp. Conjuntos de Integer e Char são, é claro, armazenados como eles
mesmos.
Opcionalmente, você também pode especificar um tipo de constante na declaração. Isso lhe dá con-
trole total sobre o modo como o compilador trata suas constantes:
const
ADecimalNumber: Double = 3.14;
I: Integer = 10;
ErrorString: string = ‘Danger, Danger, Danger!’;
O Object Pascal permite o uso das funções em tempo de compilação nas declarações const e var.
Essas rotinas incluem Ord( ), Chr( ), Trunc( ), Round( ), High( ), Low( ) e SizeOf( ). Todos os códigos a se-
guir, por exemplo, são válidos:
type
A = array[1..2] of Integer;
const
w: Word = SizeOf(Byte);
var
i: Integer = 8;
j: SmallInt = Ord(‘a’);
L: Longint = Trunc(3.14159);
x: ShortInt = Round(2.71828);
B1: Byte = High(A);
B2: Byte = Low(A);
C: char = Chr(46);
ATENÇÃO
O comportamento das constantes de tipo especificado do Delphi de 32 bits é diferente do Delphi 1 de 16
bits. No Delphi 1, o identificador declarado não era tratado como uma constante, mas como uma variável
pré-inicializada chamada constante tipificada. Entretanto, no Delphi 2 e nas versões mais recentes, cons-
tantes de tipo especificado têm a capacidade de ser uma constante no sentido estrito da palavra. O Delphi
fornece uma chave de compatibilidade na página Compiler (compilador) da caixa de diálogo Project,
Options (projeto, opções), mas você também pode usar a diretiva do compilador $J. Por default, essa cha-
ve é permitida por compatibilidade com o código em Delphi 1, mas é melhor você não se fiar nessa capaci-
dade, pois os implementadores da linguagem Object Pascal estão tentando se livrar da noção de constan-
tes atribuíveis. 29
Se você tentar alterar o valor de qualquer uma dessas constantes, o compilador Delphi emitirá uma
mensagem de erro informando que é proibido alterar o valor de uma constante. Como as constantes são
somente para leitura, o Object Pascal otimiza seu espaço de dados armazenando as constantes dignas de
armazenamento nas páginas de código da aplicação. Se o conceito de código e páginas de dados não está
claro para você, consulte o Capítulo 3.
NOTA
O Object Pascal não tem um pré-processador, como o C e C++. O conceito de uma macro não existe no
Object Pascal e, portanto, o Object Pascal não tem um equivalente para #define do C para uma declara-
ção de constante. Embora possa usar diretiva de compilador $define do Object para compilações seme-
lhantes à de #define do C, você não pode usá-la para definir constantes. Use const em Object Pascal onde
usaria #define para declarar uma constante em C ou C++.
Operadores
Operadores são os símbolos em seu código que permitem manipular todos os tipos de dados. Por exem-
plo, há operadores para adição, subtração, multiplicação e divisão de dados numéricos. Há também ope-
radores para tratar de um elemento particular de um array. Esta seção explica alguns dos operadores do
Pascal e descreve algumas diferenças entre seus correspondentes no C e no Visual Basic.
Operadores de atribuição
Se você é iniciante em Pascal, o operador de atribuição do Delphi será uma das coisas mais difíceis de ser
usada. Para atribuir um valor a uma variável, use o operador := do mesmo modo como usaria o operador
= no C ou no Visual Basic. Programadores em Pascal constantemente chamam isso de operador de obten-
ção ou atribuição, e a expressão
Number1 := 5;
Operadores de comparação
Se você já programou no Visual Basic, se sentirá muito à vontade com os operadores de comparação do
Delphi, pois eles são praticamente idênticos. Como esses operadores são quase padrão em todas as lin-
guagens de programação, vamos falar deles apenas de passagem nesta seção.
O Object Pascal usa o operador = para executar comparações lógicas entre duas expressões ou valo-
res. Como o operador = do Object Pascal é análogo ao operador == do C, uma expressão que em C seria
escrita desta forma
if (x == y)
NOTA
Lembre-se de que, no Object Pascal, o operador := é usado para atribuir um valor a uma variável e o ope-
rador = compara os valores de dois operandos.
30
O operador “não igual a” do Delphi é < >, cuja finalidade é idêntica à do operador != do C. Para de-
terminar se duas expressões não são iguais, use este código:
if x < > y then FazAlgumaCoisa
Operadores lógicos
O Pascal usa as palavras and e or como os operadores lógicos “e” e “ou”, enquanto o C usa os símbolos &&
e ¦¦, respectivamente, para esses operadores. O uso mais comum de operadores and e or é como parte de
uma instrução if ou loop, como demonstrado nos dois exemplos a seguir:
if (Condição 1) and (Condição 2) then
FazAlgumaCoisa;
while (Condição 1) or (Condição 2) do
FazAlgumaCoisa;
O operador lógico “não” do Pascal é not, que é usado para inverter uma expressão booleana. Ele é
análogo ao operador ! do C. Ele também é usado em instruções if, como mostrado aqui:
if not (condição) then (faz alguma coisa); // se condição falsa, então...
Atribuição := = =
Comparação = == = ou Is*
Não igual a < > != < >
Menor que < < <
Maior que > > >
Menor que ou igual a <= <= <=
Maior que ou igual a >= >= >=
E lógico and && And
Ou lógico or ¦¦ Or
Não lógico not ! Not
*O operador de comparação Is é usado para objetos, enquanto o operador de comparação = é usado para outros tipos.
Operadores aritméticos
Você já deve estar familiarizado com a maioria dos operadores aritméticos do Object Pascal, pois em ge-
ral são semelhantes aos que são usados em C, C++ e Visual Basic. A Tabela 2.2 ilustra todos os operado-
res aritméticos do Pascal e seus equivalentes em C/C++ e Visual Basic.
Você pode perceber que o Pascal e o Visual Basic fornecem operadores de divisão diferentes para
ponto flutuante e inteiro, o que, no entanto, não acontece com o C/C++. O operador div trunca auto-
maticamente qualquer resto quando você está dividindo duas expressões inteiras.
31
Tabela 2.2 Operadores aritméticos
Adição + + +
Subtração - - -
Multiplicação * * *
Divisão de ponto flutuante / / /
Divisão de inteiro div / \
Módulo mod % Mod
Expoente Nenhum Nenhum ^
NOTA
Lembre-se de usar o operador de divisão correto para os tipos de expressão com os quais esteja trabalhan-
do. O compilador Object Pascal emite uma mensagem de erro se você tentar dividir dois números de ponto
flutuante com o operador div de inteiro ou dois inteiros com o operador / de ponto flutuante, como ilustra o
código a seguir:
var
i: Integer;
r: Real;
begin
i := 4 / 3; // Essa linha vai provocar um erro de compilação
f := 3.4 div 2.3; // Essa linha também vai provocar um erro
end;
Muitas outras linguagens de programação não distinguem divisão de inteiro de ponto flutuante. Em vez dis-
so, elas sempre executam divisão de ponto flutuante e em seguida convertem o resultado em um inteiro,
quando necessário. Isso pode comprometer sobremaneira o desempenho. O operador div do Pascal é
mais rápido e mais específico.
Operadores de bit
Operadores de bit são operadores que permitem modificar bits individuais de uma determinada variável.
Os operadores de bit comuns permitem que você desloque os bytes para a esquerda ou direita ou que
execute operações de bit “and”, “not”, “or” e “exclusive or” (xor) com dois números. Os operadores
Shift+Left e Shift+Right são shl e shr, respectivamente, e são muito parecidos com os operadores << e >>
do C. Os demais operadores de bit do Pascal são tão fáceis que podem ser decorados: and, not, or e xor. A
Tabela 2.3 lista os operadores de bit.
Inc(variável);
Dec(variável);
Compare as duas linhas a seguir, que incrementam ou decrementam a variável por 3 usando as ins-
truções add e sub do Assembly:
Inc(variável, 3);
Dec(variável, 3);
NOTA
Com a otimização do compilador ativada, os procedimentos Inc( ) e Dec( ) normalmente produzem o
mesmo código de máquina, no qual a sintaxe é variável := variável + 1; portanto, você pode usar a op-
ção com a qual se sinta mais à vontade para incrementar e decrementar variáveis.
NOTA
Se você estiver transportando o código de 16 bits do Delphi 1.0, certifique-se de que o tamanho de ambos
tipos Integer e Cardinal tenham aumentado de 16 para 32 bits. Na verdade, esse incremento não prima
pela exatidão: no Delphi 2 e 3, o tipo Cardinal era tratado como um inteiro de 31 bits não-sinalizado para
preservar a precisão aritmética (porque o Delphi 2 e 3 carecem de um verdadeiro inteiro de 32 bits ao qual
os resultados de operações de inteiro pudessem ser promovidos). Do Delphi 4 em diante, Cardinal é um in-
teiro de 32 bits não-sinalizado de verdade.
ATENÇÃO
No Delphi 1, 2 e 3, o identificador de tipo Real especificava um número de ponteiro flutuante de 6 bytes,
que é um tipo exclusivo do Pascal e geralmente incompatível com outras linguagens. No Delphi 4, Real é
um nome alternativo para o tipo Double. O antigo número de ponteiro flutuante de 6 bytes ainda está lá,
mas agora é identificado por Real48. Você também pode forçar o identificador Real a fazer referência ao
número de ponto flutuante de 6 bytes usando a diretiva {$REALCOMPATIBILITY ON}.
34
Caracteres
O Delphi fornece três tipos de caracteres:
l AnsiChar. Este é o caracter ANSI padrão de um byte que os programadores aprenderam a respei-
tar e amar.
l WideChar. Este caracter tem dois bytes e representa um caracter Unicode.
l Char. Atualmente, esse caracter é idêntico ao AnsiChar, mas a Borland alerta que a definição pode
alterar em uma versão posterior do Delphi para um WideChar.
Lembre-se de que, como o tamanho de um caracter nem sempre é de um byte, você não deve definir
manualmente o tamanho em suas aplicações. Em vez disso, use a função SizeOf( ) onde for apropriado.
NOTA
O procedimento-padrão SizeOf( ) retorna o tamanho, em bytes, de um tipo ou instância.
Você também pode fazer com que as variáveis sejam declaradas como tipo string, e não como tipo
ShortString, usando a diretiva do compilador $H. Quando o valor da diretiva do compilador $H é negativo,
as variáveis string são do tipo ShortString, e quando o valor da diretiva é positivo (o default), as variáveis
string são do tipo AnsiString. O código a seguir demonstra esse comportamento:
var
{$H-}
S1: string; // S1 é uma ShortString
{$H+}
S2: string; // S2 é uma AnsiString
A exceção para a regra $H é que uma string declarada com um tamanho explícito (limitado a um má-
ximo de 255 caracteres) é sempre uma ShortString:
var
S: string[63]; // Uma ShortString com até 63 caracteres
35
O tipo AnsiString
O tipo AnsiString (ou string longa) foi introduzido na linguagem no Delphi 2. Ele é fruto das reivindica-
ções dos clientes do Delphi 1, que desejavam um tipo de string fácil de usar, sem a limitação de 255 ca-
racteres. Mas a AnsiString é mais do que isso.
Embora tipos AnsiString mantenham uma interface quase idêntica à de seus antecessores, eles são di-
namicamente alocados e jogados no lixo. Por essa razão, AnsiString é muitas vezes chamado de um tipo
gerenciado permanentemente. O Object Pascal também gerencia automaticamente a alocação de strings
temporárias conforme a necessidade e, portanto, você não precisa se preocupar em alocar buffers para
resultados intermediários, como aconteceria no C/C++. Além disso, os tipos AnsiString são sempre ter-
minados em null e dessa forma são sempre compatíveis com as strings terminadas em null usadas pela
API do Win 32. Na verdade, o tipo AnsiString é implementado como um ponteiro para uma estrutura de
string na memória do heap. A Figura 2.1 mostra como uma AnsiString é organizada na memória.
AnsiString
ATENÇÃO
O formato interno completo do tipo string longo não foi documentado pela Borland, que se reserva o direi-
to de alterar o formato interno das strings longas nas futuras versões do Delphi. A informação dada aqui
tem como objetivo ajudá-lo a entender como trabalhar com tipos AnsiString, e você deve evitar ser depen-
dente da estrutura de uma AnsiString em seu código.
Os programadores que evitaram a implementação de detalhes da string mudando do Delphi 1 para o
Delphi 2 puderam migrar seus códigos sem problemas. Aqueles que escreveram código que dependia do
formato interno (como o elemento zero na string sendo seu tamanho) tiveram de modificar seus códigos
para o Delphi 2.
Como ilustra a Figura 2.1, tipos AnsiString possuem contagem de referência, o que significa que vá-
rias strings podem apontar para a mesma memória física. Portanto, é muito rápido o processo de cópia
de string, pois ele está restrito à cópia de um ponteiro, não precisando que todo o conteúdo da string seja
copiado. Quando dois ou mais tipos AnsiString compartilham uma referência para a mesma string física,
o gerenciador de memória do Delphi usa uma técnica de copiar ao escrever, que permite que ele aguarde
até uma string ser modificada para liberar uma referência e alocar uma nova string física. O exemplo a se-
guir ilustra esses conceitos:
var
S1, S2: string;
begin
// armazena string em S1, contagem de referência de S1 é 1
S1 := ‘E agora para alguma coisa... ‘;
S2 := S1; // S2 agora faz referência a S1. Contagem ref. de S1 é 2.
// S2 é alterada e é copiada em seu próprio espaço de memória,
// fazendo com que a contagem de referência de S1 seja decrementada
S2 := S2 + ‘completamente diferente!’;
36
Tipos gerenciados permanentemente
Além da AnsiString, o Delphi fornece vários outros tipos que são permanentemente gerenciados. Esses
tipos incluem WideString, Variant, OleVariant, interface, dispinterface e arrays dinâmicos. Ainda neste
capítulo, você aprenderá mais sobre cada um desses tipos. Por enquanto, vamos nos concentrar no
que são exatamente tipos permanentemente gerenciados e como eles funcionam.
Os tipos permanentemente gerenciados, algumas vezes chamados tipos apanhados do lixo, são
tipos que potencialmente consomem algum recurso em particular ao serem usados e liberam automa-
ticamente o recurso quando saem do escopo. Naturalmente, a variedade de recursos usados depende
do tipo envolvido. Por exemplo, uma AnsiString consome memória para a string de caracteres usada e
a memória ocupada pela string de caracteres é liberada quando ela sai do escopo.
Para variáveis globais, esse processo se dá de um modo extremamente objetivo: como uma parte
do código de finalização gerado para sua aplicação, o compilador insere código para certificar-se de
que cada variável global permanentemente gerenciada seja limpada. Como todo dado global é iniciali-
zado em zero quando sua aplicação é carregada, cada variável global gerenciada permanentemente
irá inicialmente sempre conter um zero, um vazio ou algum outro valor indicando que a variável “não
está sendo usada”. Dessa forma, o código de finalização não tentará liberar recursos a não ser que de
fato sejam usados em sua aplicação.
Quando você declara uma variável local permanentemente gerenciada, o processo é ligeira-
mente mais complexo. Primeiro, o compilador insere código para assegurar que a variável é inicializa-
da como zero quando a função ou o procedimento é digitado. Depois, o compilador gera um bloco de
tratamento de exceção try..finally, que envolve todo o corpo da função. Finalmente, o compilador
insere código no bloco finally para limpar a variável permanentemente gerenciada (o tratamento de
exceção é explicado de modo mais detalhado na seção “Tratamento estruturado de exceções”). Com
isso em mente, considere o seguinte procedimento:
procedure Foo;
var
S: string;
begin
// corpo do procedimento
// use S aqui
end;
Embora esse procedimento pareça simples, se você levar em conta o código gerado pelo compi-
lador nos bastidores, ele na verdade deveria ter a seguinte aparência:
procedure Foo;
var
S: string;
begin
S := ‘’;
try
// corpo do procedimento
// use S aqui
finally
// limpe S aqui
end;
end;
Operações de string
Você pode concatenar duas strings usando o operador + ou a função Concat( ). O método preferido de
concatenação de string é o operador +, pois a função Concat( ) na verdade existe para manter a compatibi-
lidade com versões anteriores. O exemplo a seguir demonstra o uso de + e Concat( ):
37
{ usando + }
var
S, S2: string
begin
S:= ‘Cookie ‘:
S2 := ‘Monster’;
S := S + S2; { Cookie Monster }
end.
{ usando Concat( ) }
var
S, S2: string;
begin
S:= ‘Cookie ‘;
S2 := ‘Monster’;
S := Concat(S, S2); { Cookie Monster }
end.
NOTA
Use sempre um apóstrofo (‘Uma String’) quando trabalhar com strings literais no Object Pascal.
DICA
Concat( ) é uma das muitas funções e procedimentos do “compilador mágico” (como ReadLn( ) e Wri-
teLn( ), por exemplo) que não têm uma definição no Object Pascal. Como essas funções e procedimentos
têm como finalidade aceitar um número indeterminado de parâmetros ou parâmetros opcionais, não po-
dem ser definidas em termos da linguagem no Object Pascal. Por isso, o compilador fornece um caso espe-
cial para cada uma dessas funções e gera uma chamada para uma das funções auxiliadoras do “compila-
dor mágico” definidas na unidade System. Essa funções auxiliadoras são geralmente implementadas na lin-
guagem Assembly para driblar as regras da linguagem Pascal.
Além das funções e procedimentos de suporte a string do “compilador mágico”, há uma série de fun-
ções e procedimentos na unidade SysUtils cuja finalidade é facilitar o trabalho com strings. Procure
“String-handling routines (Pascal-style)” (rotinas de manipulação de string em estilo Pascal) no sistema de
ajuda on-line do Delphi.
Além disso, você encontrará algumas funções e procedimentos utilitários de string personalizados e
muito úteis na unidade SysUtils, no diretório \Source\Utils do CD-ROM que acompanha este livro.
Tamanho e alocação
Ao ser declarada pela primeira vez, uma AnsiString não tem tamanho e, portanto, não tem espaço alo-
cado para os caracteres na string. Para fazer com que espaço seja alocado para a string, você pode
atribuir a string a uma literal de string ou a outra string, ou usar o procedimento SetLength( ) mostra-
do a seguir:
var
S: string; // inicialmente a string não tem tamanho
begin
S := ‘Doh!’; // aloca pelo menos o espaço necessário a uma literal de string
{ ou }
S := OtherString // aumenta a contagem de referência da OtherString
// (presume que OtherString já aponte para uma string válida)
{ ou }
SetLength(S, 4); // aloca espaço suficiente para pelo menos 4 caracteres
end;
Você pode indexar os caracteres de uma AnsiString como um array, mas cuidado para não indexar
38 além do comprimento da string. Por exemplo, o trecho de código a seguir causaria um erro:
var
S: string;
begin
S[1] := ‘a’; // Não funcionará porque S não foi alocado!
end;
Compatibilidade Win32
Como já dissemos, os tipos AnsiString são sempre terminados em null e, portanto, são compatíveis com as
strings terminadas em null. Isso facilita a chamada de funções da API do Win32 ou outras funções que
exigem strings tipo PChar. É exigido apenas que você execute um typecast da string, tornando-a um PChar
(typecast é explicado de modo mais detalhado na seção “Typecast e conversão de tipo”). O código a se-
guir mostra como se chama a função GetWindowsDirectory( ) do Win32, que aceita um PChar e tamanho de
buffer como parâmetros:
var
S: string;
begin
SetLength(S, 256); // importante! Primeiro obtenha espaço para a string
// chama função, S agora contém string de diretório
GetWindowsDirectory(PChar(S), 256);
end;
Depois de usar uma AnsiString onde uma função ou procedimento espera um PChar, você deve defi-
nir manualmente o tamanho da variável de string com seu tamanho terminado em null. A função Realize-
Length( ), que também provém da unidade STRUTILS, executa essa tarefa:
ATENÇÃO
Tome cuidado quando tentar fazer um typecast em uma string, tornando-a uma variável PChar. Como as
strings são apagadas quando saem do escopo, você deve prestar atenção quando fizer atribuições como P
:= PChar(Str), onde o escopo (ou a vida útil) de P é maior do que Str.
39
Questões relacionadas ao transporte
Quando você está transportando aplicações do Delphi 1 de 16 bits, precisa ter em mente uma série de
questões durante a migração de tipos AnsiString:
l Nos lugares nos quais você usou o tipo PString (ponteiro para uma ShortString), deve usar o tipo
string. Lembre-se de que uma AnsiString já é um ponteiro para uma string.
l Você não pode mais acessar o elemento zero de uma string para obter ou definir o tamanho. Em
vez disso, use a função Length( ) para obter o tamanho da string e o procedimento SetLength( )
para definir o tamanho.
l Não há mais nenhuma necessidade de se usar StrPas( ) e StrPCopy( ) para fazer conversões entre
strings e tipos Pchar. Como mostramos anteriormente, você pode fazer typecast de uma
AnsiString para um Pchar. Quando desejar copiar o conteúdo de um PChar em uma AnsiString, você
pode usar uma atribuição direta:
StringVar := PCharVar;
ATENÇÃO
Lembre-se de que você deve usar o procedimento SetLength( ) para definir o tamanho de uma string lon-
ga, enquanto a prática passada era acessar diretamente o elemento zero de uma string curta para definir o
tamanho. Você vai se deparar com esse problema quando tentar transportar código do Delphi 1.0 de 16
bits para 32 bits.
O tipo ShortString
Se você trabalha há bastante tempo com o Delphi, vai reconhecer o tipo ShortString como o tipo string do
Delphi 1.0. Algumas vezes, os tipos ShortString são chamados de strings do Pascal ou strings de byte. Para
reiterar, lembre-se de que o valor da diretiva $H determina se as variáveis declaradas como string são tra-
tadas pelo compilador como AnsiString ou ShortString.
Na memória, a string se parece com um array de caracteres onde o caracter zero na string contém o
tamanho da string, e a string propriamente dita está contida nos caracteres seguintes. O tamanho de ar-
mazenamento de uma ShortString default é de no máximo 256 bytes. Isso significa que você nunca pode
ter mais do que 255 caracteres em uma ShortString (255 caracteres + 1 byte de comprimento = 256).
Assim como acontece com AnsiString, é simples trabalhar com ShortString, pois o compilador aloca
strings temporários conforme a necessidade; portanto, você não tem que se preocupar em alocar buffers
para os resultados intermediários ou dispor deles, como é feito com C.
A Figura 2.2 ilustra como uma string do Pascal é organizada na memória.
#3 D D G
Opcionalmente, você pode alocar menos do que 256 bytes para uma ShortString usando apenas o
identificador de tipo e um especificar de comprimento, como no exemplo a seguir:
40
var
S: string[45]; { uma ShortString de 45 caracteres }
begin
S := ‘This string must be 45 or fewer characters.’;
end.
O código anterior faz com que ShortString seja criada independentemente da definição atual da di-
retiva $H. O comprimento máximo que você pode especificar é de 255 caracteres.
Nunca armazene mais caracteres em uma ShortString que excedam o espaço que você alocou para
ela na memória. Se você declarasse uma variável como uma string[8], por exemplo, e tentasse atribuir
‘uma_string_longa_demais’ para essa variável, a string seria truncada para apenas oito caracteres e você per-
deria dados.
Ao usar um subscrito de array para endereçar um determinado caracter em uma ShortString, você
obterá resultados estranhos ou corromperá a memória se tentar usar um índice de subscrito maior do que
o tamanho declarado da ShortString. Por exemplo, suponha que você declare uma variável da seguinte
maneira:
var
Str: string[8];
Se em seguida você tentar escrever no décimo elemento da string como se vê no exemplo a seguir,
provavelmente corromperá a memória usada por outras variáveis:
var
Str: string[8];
i: Integer;
begin
i := 10;
Str[i] := ‘s’; // a memória será corrompida
Se você selecionar a opção Range Checking (verificação de intervalo) da caixa de diálogo Options,
Project (opções, projeto) fará com que o compilador capture esses tipos de erro em runtime.
DICA
Embora a inclusão da lógica de verificação de intervalo em seu programa o ajude a encontrar erros de
string, ela compromete ligeiramente o desempenho de sua aplicação. É comum a prática de usar a verifica-
ção de intervalo durante as fases de desenvolvimento e depuração do seu programa, mas você deve desati-
var esse recurso depois que tiver certeza de que seu programa é estável.
Ao contrário dos tipos AnsiString, os tipos ShortString não são inerentemente compatíveis com
strings de terminação nula. Por isso, é preciso um pouco de trabalho para poder passar uma ShortString
para uma função da API do Win32. A função a seguir, ShortStringAsPChar( ), pertence à unidade
STRUTILS.PAS, mencionada anteriormente:
41
ATENÇÃO
As funções e procedimentos na API do Win32 exigem strings de terminação nula. Não tente passar um tipo
ShortString para uma função da API, pois o seu programa não será compilado. Sua vida será bem mais fá-
cil se você usar strings longas quando trabalhar com a API.
O tipo WideString
O tipo WideString é um tipo gerenciado permanentemente, semelhante ao AnsiString; ambos são dinami-
camente alocados, excluídos quando saem do escopo e inclusive compatíveis um com um outro em ter-
mos de atribuição. Entretanto, WideString difere do AnsiString em três aspectos básicos:
l Tipos WideString consistem em caracteres WideChar, não em caracteres AnsiChar, o que os torna
compatíveis com strings Unicode.
l Tipos WideString são alocados usando a função SysAllocStrLen( ), o que os torna compatíveis com
strings BSTR do OLE.
l Tipos WideString não têm contagem de referência e, portanto, a atribuição de uma WideString para
outra exige que toda a string seja copiada de uma localização na memória para outra. Isso torna
os tipos WideString menos eficientes do que os tipos AnsiString em termos de velocidade e uso de
memória.
Como já foi dito, o compilador automaticamente sabe como converter entre variáveis dos tipos
e WideString, como se pode ver a seguir:
AnsiString
var
W: WideString;
S: string;
begin
W := ‘Margaritaville’;
S := W; // Wide convertida para Ansi
S := ‘Come Monday’;
W := S; // Ansi convertida para Wide
end;
Para fazer o trabalho com tipos WideString parecer natural, o Object Pascal faz o overload das roti-
nas Concat( ), Copy( ), Insert( ), Length( ), Pos( ) e SetLength( ) e os operadores +, = e < > para serem usa-
dos com os tipos WideString. Portanto, o código a seguir é sintaticamente correto:
var
W1, W2: WideString;
P: Integer;
begin
W1 := ‘Enfield’;
W2 := ‘field’;
if W1 < > W2 then
P := Pos(W1, W2);
end;
Como acontece com os tipos AnsiString e ShortString, você pode usar colchetes de array para fazer
referência a caracteres individuais de uma WideString:
var
W: WideString;
C: WideChar;
begin
42
W := ‘Ebony and Ivory living in perfect harmony’;
C := W[Length(W)]; // C armazena o último caracter em W
end;
DICA
Como o tipo AnsiString do Object Pascal pode ser usado como um PChar na maioria das situações, você
deve usar esse tipo no lugar do tipo PChar sempre que possível. Como o gerenciamento de memória para
strings ocorre automaticamente, você reduz significativamente a possibilidade de introduzir bugs que cor-
rompam a memória em suas aplicações se, quando possível, evitar tipos PChar e a alocação de memória
manual associada a eles.
D D G #0
PChar
Como já dissemos, as variáveis PChar exigem que você aloque e libere manualmente os buffers de
memória que contenham essas strings. Normalmente, você aloca memória para um buffer PChar usando a
função StrAlloc( ), mas várias outras funções podem ser usadas para alocar memória para tipos PChar, in-
cluindo AllocMem( ), GetMem( ), StrNew( ) e até mesmo a função da API VirtualAlloc( ). Também existem
funções correspondentes para muitas dessas funções, que devem ser usadas para desalocar a memória. A
Tabela 2.6 lista várias funções de alocação e as funções de desalocação correspondentes.
AllocMem( ) FreeMem( )
GlobalAlloc( ) GlobalFree( )
GetMem( ) FreeMem( )
New( ) Dispose( )
StrAlloc( ) StrDispose( )
StrNew( ) StrDispose( )
VirtualAlloc( ) VirtualFree( )
43
O exemplo a seguir demonstra técnicas de alocação da memória enquanto se trabalha com tipos
PChar e string:
var
P1, P2: PChar;
S1, S2: string;
begin
P1 := StrAlloc(64 * SizeOf(Char)); // P1 aponta para alocação de 63 caracteres
StrPCopy(P1, ‘Delphi 5 ‘); // Copia string literal em P1
S1 := ‘Developer’’s Guide’; // Coloca algum texto na string S1
P2 := StrNew(PChar(S1)); // P1 aponta para uma cópia de S1
StrCat(P1, P2); // concatena P1 e P2
S2 := P1; // S2 agora armazena ‘Delphi 5 Developer’s Guide’
StrDispose(P1); // apaga os buffers P1 e P2
StrDispose(P2);
end.
Observe, antes de mais nada, o uso do SizeOf(Char) com StrAlloc( ) durante a alocação de memória
para P1. Lembre-se de que o tamanho de um Char pode alterar de um byte para dois em futuras versões do
Delphi; portanto, você não pode partir do princípio de que o valor de Char será sempre de um byte. Si-
zeOf( ) assegura que a alocação vai funcionar bem, independentemente do número de bytes que um ca-
racter ocupe.
StrCat( ) é usado para concatenar duas strings PChar. Observe aqui que você não pode usar o opera-
dor + para concatenação, ao contrário do que acontece com os tipos de string longa e ShortString.
A função StrNew( ) é usada para copiar o valor contido pela string S1 para P2 (um PChar). Tome cuida-
do ao usar essa função. É comum a ocorrência de erros de memória sobrescrita durante o uso de
StrNew( ), pois ele só aloca a memória necessária para armazenar a string. Considere o seguinte exemplo:
var
P1, P2: Pchar;
begin
P1 := StrNew(‘Hello ‘); // Aloca apenas memória suficiente para P1 e P2
P2 := StrNew(‘World’);
StrCat(P1, P2); // Cuidado: memória corrompida!
.
.
.
end;
DICA
Como acontece com os outros tipos de strings, o Object Pascal fornece uma razoável biblioteca de funções
e procedimentos para operar com tipos PChar. Procure a seção “String-handling routines (null-terminated)”
(rotinas de manipulação de string de terminação nula) no sistema de ajuda on-line do Delphi.
Você também encontrará algumas interessantes funções e procedimentos de terminação nula na unidade
StrUtils, no diretório \Source\Utils do CD-ROM que acompanha este livro.
Tipos Variant
O Delphi 2.0 introduziu um poderoso tipo de dado chamado Variant. Variantes foram criadas basica-
mente para dar suporte para OLE Automation, que utiliza bastante o tipo Variant. De fato, o tipo de dado
Variant do Delphi encapsula a variante usada com OLE. A implementação de variantes do Delphi tam-
bém vem se mostrando útil em outras áreas de programação do Delphi, como você logo aprenderá. O
Object Pascal é a única linguagem compilada que integra completamente variantes como um tipo de
dado dinâmico em runtime e como um tipo estático em tempo de compilação no qual o compilador sem-
44 pre sabe que se trata de uma variante.
O Delphi 3 introduziu um novo tipo chamado OleVariant, que é idêntico a Variant, exceto pelo fato
de só poder armazenar tipos compatíveis com Automation. Nesta seção, inicialmente vamos nos concen-
trar no tipo Variant e em seguida discutiremos OleVariant e faremos uma comparação entre os dois.
As variantes podem suportar todos os tipos de dados simples, como inteiros, valores de ponto flutu-
ante, strings, booleanos, data e hora, moeda e também objetos de OLE Automation. Observe que as vari-
antes não podem fazer referência a objetos do Object Pascal. Além disso, as variantes podem fazer refe-
rência a um array não-homogêneo, que pode variar em tamanho e cujos elementos de dados podem fazer
referência a qualquer um dos tipos de dados citados (inclusive outro array de variante).
A estrutura de Variant
A estrutura de dados que define o tipo Variant é definida na unidade System e também pode ser vista no có-
digo a seguir:
type
PVarData = ^TVarData;
TVarData = packed record
VType: Word;
Reserved1, Reserved2, Reserved3: Word;
case Integer of
varSmallint: (VSmallint: Smallint);
varInteger: (VInteger: Integer);
varSingle: (VSingle: Single);
varDouble: (VDouble: Double);
varCurrency: (VCurrency: Currency);
varDate: (VDate: Double);
varOleStr: (VOleStr: PWideChar);
varDispatch: (VDispatch: Pointer);
varError: (VError: LongWord);
varBoolean: (VBoolean: WordBool);
varUnknown: (VUnknown: Pointer);
varByte: (VByte: Byte);
varString: (VString: Pointer);
varAny: (VAny: Pointer);
varArray: (VArray: PVarArray);
varByRef: (VPointer: Pointer);
end;
A estrutura TVarData consome 16 bytes de memória. Os primeiro dois bytes da estrutura TVarData
contêm um valor de palavra que representa o tipo de dado ao qual a variante faz referência. O código a 45
seguir mostra os diversos valores que podem aparecer no campo VType do registro TVarData. Os próximos
seis bytes não são usados. Os outros oito bytes contêm os dados propriamente ditos ou um ponteiro para
os dados representados pela variante. Novamente, essa estrutura é mapeada diretamente para a imple-
mentação OLE do tipo variante. Veja o código a seguir:
{ Códigos de tipo de Variant }
const
varEmpty = $0000;
varNull = $0001;
varSmallint = $0002;
varInteger = $0003;
varSingle = $0004;
varDouble = $0005;
varCurrency = $0006;
varDate = $0007;
varOleStr = $0008;
varDispatch = $0009;
varError = $000A;
varBoolean = $000B;
varVariant = $000C;
varUnknown = $000D;
varByte = $0011;
varStrArg = $0048;
varString = $0100;
varAny = $0101;
varTypeMask = $0FFF;
varArray = $2000;
varByRef = $4000;
NOTA
Como nos códigos de tipo na lista anterior, uma Variant não pode conter uma referência para um tipo
Pointer ou class.
Você perceberá na listagem de TVarData que o registro TVarData na verdade não passa de um registro
de variante. Não confunda isso com o tipo Variant. Embora o registro de variante e o tipo Variant tenham
nomes semelhantes, eles representam duas construções totalmente diferentes. Registros de variante per-
mitem que vários campos de dados se sobreponham na mesma área de memória (como uma união do
C/C++). Isso é discutido com mais detalhes na seção “Registros”, posteriormente neste capítulo. A ins-
trução case no registro de variante TVarData indica o tipo de dado ao qual a variante faz referência. Por
exemplo, se o campo VType contém o valor varInteger, somente quatro dos oito bytes de dados na parte de
variante do registro são usados para armazenar um valor inteiro. Da mesma forma, se VType tem o valor
varByte, somente um dos oito bytes é usado para armazenar um valor byte.
Você perceberá que, se VType armazenar o valor varString, os oito bytes de dados não armazenarão a
string. Isso é um ponto importante porque você pode acessar campos de uma variante diretamente, como
mostramos aqui:
var
V: Variant;
begin
TVarData(V).VType := varInteger;
TVarData(V).VInteger := 2;
end;
46
Você tem que entender que em alguns casos essa é uma prática perigosa, pois se pode perder uma
referência a uma string ou a uma outra entidade permanentemente gerenciada, que resultará em seu apli-
cativo perdendo memória ou outro recurso. Você verá que o que queremos dizer com o termo apanhar o
lixo na próxima seção.
Como já dissemos neste capítulo, na nota explicativa dedicada a tipos permanentemente gerencia-
dos, várias coisas que estão ocorrendo aqui podem não ser aparentes. O Delphi primeiro inicializa a
variante como um valor não-atribuído. Durante a atribuição, ele define o campo VType como varString e
copia o ponteiro de string para o campo VString. Em seguida, ele aumenta a contagem de referência da
string S. Quando a variante sai do escopo (isto é, o procedimento termina e retorna para o código que o
chamou), ela é apagada e a contagem de referência da string S é decrementada. O Delphi faz isso inserin-
do implicitamente um bloco try..finally no procedimento, como podemos ver aqui:
procedure ShowVariant(S: string);
var
V: Variant
begin
V := Unassigned; // inicializa a variante como “vazia”
try
V := S;
ShowMessage(V);
finally
// Agora limpa os recursos associados à variante
end;
end;
Essa mesma liberação implícita de recursos ocorre quando você atribui um tipo de dado diferente a
uma variante. Por exemplo, examine o código a seguir:
procedure ChangeVariant(S: string);
var
V: Variant
begin
V := S;
V := 34;
end;
Se você entende o que aconteceu no exemplo anterior, verá por que não é recomendado que você
manipule campos do registro TVarData diretamente, como mostramos aqui:
procedure ChangeVariant(S: string);
var
V: Variant
begin
V := S;
TVarData(V).VType := varInteger;
TVarData(V).VInteger := 32;
V := 34;
end;
Embora isso possa parecer seguro, não o é porque gera a impossibilidade de decrementar a conta-
gem de referência da string S, o que provavelmente resultará em um vazamento de memória. Via de re-
gra, não acesse campos TVarData diretamente ou, se o fizer, certifique-se de que sabe exatamente o que
está fazendo.
Typecast de Variants
Você pode fazer explicitamente um typecast de expressões para o tipo Variant. Por exemplo, a expressão
Variant(X)
resulta em um tipo Variant cujo código de tipo corresponde ao resultado da expressão X, que deve ser um
tipo integer, real, currency, string, character ou Boolean.
Você também pode fazer um typecast de uma variante de modo a torná-la um tipo de dados sim-
ples. Por exemplo, dada a atribuição
V := 1.6;
onde V é uma variável de tipo Variant, as seguintes expressões terão os resultados mostrados:
S := string(V); // S conterá a string ‘1.6’;
// I está arredondado para o valor Integer mais próximo, que nesse caso é 2.
I := Integer(V);
B := Boolean(V); // B contém False se V contém 0; se não, B é True
D := Double(V); // D contém o valor 1.6
Esses resultados são determinados por certas regras de conversão de tipo aplicáveis a tipos Variant.
Essas regras são definidas em detalhes no Object Pascal Language Guide (guia da linguagem Object Pas-
cal) do Delphi.
A propósito, no exemplo anterior, não é necessário fazer um typecast com a variante de modo a tor-
ná-la um tipo de dado capaz de fazer a atribuição. O código a seguir funcionaria muito bem:
V := 1.6;
S := V;
I := V;
B := V;
D := V;
48
O que acontece aqui é que as conversões para os tipos de dados de destino são feitas através de um
typecast implícito. Entretanto, como essas conversões são feitas em runtime, há muito mais código lógico
anexado a esse método. Se você tem certeza do tipo que uma variante contém, é melhor fazer o typecast
para esse tipo, a fim de acelerar a operação. Isso é especialmente verdadeiro se a variante é usada em uma
expressão, o que discutiremos a seguir.
Variantes em expressões
Você pode usar variantes em expressões com os seguintes operadores: +, =, *, /, div, mod, shl, shr, and, or,
xor, not, :=, < >, <, >, <= e >=.
Quando usamos variantes em expressões, o Delphi sabe como executar as operações baseado no
conteúdo da variante. Por exemplo, se duas variantes, V1 e V2, contêm inteiros, a expressão V1 + V2 resulta
na adição de dois inteiros. Entretanto, se V1 e V2 contêm strings, o resultado é uma concatenação das duas
strings. O que acontece se V1 e V2 contêm dois tipos de dados diferentes? O Delphi usa certas regras de
promoção para executar a operação. Por exemplo, se V1 contém a string ‘4.5’ e V2 contém um número de
ponto flutuante, V1 será convertido para um ponto flutuante e em seguida somado a V2. O código a seguir
ilustra isso:
var
V1, V2, V3: Variant;
begin
V1 := ‘100’; // Um tipo string
V2 := ‘50’; // Um tipo string
V3 := 200; // Um tipo Integer
V1 := V1 + V2 + V3;
end;
Baseado no que acabamos de falar sobre regras de promoção, a primeira impressão que teríamos
com o código anterior é que ele resultaria no valor 350 como um inteiro. Entretanto, se você prestar um
pouco mais de atenção, verá que não é bem assim. Como a ordem de precedência é da esquerda para a di-
reita, a primeira equação executada é V1 + V2. Como essas duas variantes fazem referência a strings, uma
concatenação de string é executada, resultando na string ‘10050’. Esse resultado é em seguida adicionado
ao valor de inteiro armazenado pela variante V3. Como V3 é um inteiro, o resultado ‘10050’ é convertido
para um inteiro e adicionado a V3 e dessa forma nosso resultado final é 10250.
O Delphi promove as variantes para o tipo mais alto na equação de modo a executar o cálculo com
sucesso. Entretanto, quando uma operação é executada em duas variantes que o Delphi não é capaz de
compreender, uma exceção do tipo “conversão de tipo de variante inválida” é criada. O código a seguir
ilustra isso:
var
V1, V2: Variant;
begin
V1 := 77;
V2 := ‘hello’;
V1 := V1 / V2; // Produz uma exceção.
end;
Como já dissemos, algumas vezes é uma boa idéia fazer explicitamente um typecast de uma variante
para um tipo de dado específico, caso você saiba de que tipo ele é e se ele é usado em uma expressão.
Considere a linha de código a seguir:
V4 := V1 * V2 / V3;
Antes de um resultado poder ser gerado para essa equação, cada operação é manipulada por uma
função em runtime que dá vários giros para determinar a compatibilidade dos tipos que as variantes re-
presentam. Em seguida, as conversões são feitas para os tipos de dados apropriados. Isso resulta em uma
grande quantidade de código e overhead. Uma solução melhor é obviamente não usar variantes. Entre- 49
tanto, quando necessário, você também pode fazer explicitamente o typecast das variantes de modo que
os tipos de dados sejam resolvidos durante a compilação:
V4 := Integer(V1) * Double(V2) / Integer(V3);
Não se esqueça de que isso pressupõe que você sabe que tipos de dados as variantes representam.
Empty e Null
Dois valores de VType especiais para variantes merecem uma rápida análise. O primeiro é varEmpty, que sig-
nifica que a variante ainda não foi atribuída a um valor. Esse é o valor inicial da variante, definida pelo
compilador quando ela entra no escopo. A outra é varNull, que é diferente de varEmpty, que na verdade re-
presenta o valor Null, não uma ausência de valor. Essa diferença entre ausência de valor e valor Null é es-
pecialmente importante quando aplicada aos valores de campo de uma tabela de banco de dados. No Ca-
pítulo 28, você aprenderá como as variantes são usadas no contexto das aplicações de banco de dados.
Outra diferença é que a tentativa de executar qualquer equação com uma variante varEmpty conten-
do um valor VType resultará em uma exceção “operação de variante inválida”. No entanto, o mesmo não
acontece com variantes contendo um valor varNull. Quando uma variante envolvida em uma equação
contém um valor Null, esse valor se propagará para o resultado. Portanto, o resultado de qualquer equa-
ção contendo um Null é sempre Null.
Se você deseja atribuir ou comparar uma variante a um desses dois valores especiais, a unidade
System define duas variantes, Unassigned e Null, que possuem os valores VType de varEmpty e varNull, respecti-
vamente.
ATENÇÃO
Pode parecer tentador o uso de variantes no lugar dos tipos de dados convencionais, pois eles parecem
oferecer muita flexibilidade. Contudo, isso aumentará o tamanho de seu código e suas aplicações serão
executadas mais lentamente. Além disso, a manutenção do seu código se tornará mais difícil. As variantes
são úteis em muitas situações. De fato, a própria VCL usa variantes em vários lugares, mais notadamente
no ActiveX e em áreas de banco de dados, em virtude da flexibilidade de tipo de dado que elas oferecem.
Entretanto, falando de um modo geral, você deve usar tipos de dados convencionais em vez de variantes.
Você só deve recorrer ao uso de variantes em situações em que flexibilidade da variante tem mais valor do
que o desempenho do método convencional. Tipos de dados ambíguos produzem bugs ambíguos.
Arrays de variantes
Já dissemos aqui que uma variante pode fazer referência a um array não-homogêneo. Nesse caso, a sinta-
xe a seguir é válida:
var
V: Variant;
I, J: Integer;
begin
I := V[J];
end;
Não se esqueça de que, embora o código precedente seja compilado, você vai obter uma exceção
em runtime porque V ainda não contém um array de variantes. O Object Pascal fornece várias funções de
suporte a array de variantes com as quais você pode criar um array de variantes. VarArrayCreate( ) e
VarArrayOf( ) são duas dessas funções.
VarArrayCreate( )
50 VarArrayCreate( ) é definida na unidade System da seguinte maneira:
function VarArrayCreate(const Bounds: array de Integer;
VarType: Integer): Variant;
Para usar VarArrayCreate( ), você passa os limites do array que deseja criar e um código de tipo de va-
riante para o tipo dos elementos do array (o primeiro parâmetro é um array aberto, que é discutido na se-
ção “Passando parâmetros” neste capítulo). Por exemplo, o código a seguir retorna um array de variantes
de inteiros e atribui valores aos itens do array:
var
V: Variant;
begin
V := VarArrayCreate([1, 4], varInteger); // Cria um array de 4 elementos
V[1] := 1;
V[2] := 2;
V[3] := 3;
V[4] := 4;
end;
Se arrays de variante de um único tipo já não lhe parecerem suficientemente confusos, você pode
passar varVariant como o código de tipo para criar um array de variantes de variantes! Dessa forma, cada
elemento no array tem a capacidade de conter um tipo diferente de dado. Você também pode criar um
array multidimensional passando os limites adicionais necessários. Por exemplo, o código a seguir cria
um array com limites [1..4, 1..5]:
V := VarArrayCreate([1, 4, 1, 5], varInteger);
VarArrayOf( )
A função VarArrayOf( ) é definida na unidade System da seguinte maneira:
function VarArrayOf(const Values: array de Variant): Variant;
Essa função retorna um array unidimensional cujos elementos são dados no parâmetro Values. O
exemplo a seguir cria um array de variantes de três elementos com um inteiro, uma string e um valor de
ponto flutuante:
V := VarArrayOf([1, ‘Delphi’, 2.2]);
A função VarArrayRedim( ) permite que você redimensione o limite superior da dimensão mais à direi-
ta de um array de variantes. A função VarArrayDimCount( ) retorna o número de dimensões em um array de
variantes. VarArrayLowBound( ) e VarArrayHighBound( ) retornam os limites inferior e superior de um array,
respectivamente. VarArrayLock( ) e VarArrayUnlock( ) são duas funções especiais, que são descritas em deta-
lhes na próxima seção.
51
VarArrayRef( ) tem a finalidade de resolver um problema que existe durante a passagem de arrays de
variantes para servidores OLE Automation. O problema ocorre quando você passa uma variante conten-
do um array de variantes para um método de automação, como este:
Server.PassVariantArray(VA);
O array é passado não como um array de variantes, mas como uma variante contendo um array de
variantes – uma diferença significativa. Se o servidor esperar um array de variantes e não uma referência
a um, o servidor provavelmente encontrará uma condição de erro quando você chamar o método com a
sintaxe anterior. VarArrayRef( ) resolve essa situação transformando a variante no tipo e no valor espera-
dos pelo servidor. Esta é a sintaxe para se usar VarArrayRef( ):
Server.PassVariantArray(VarArrayRef(VA));
VarIsArray( ) é uma simples verificação booleana, que retorna True se o parâmetro de variante passa-
do para ele for um array de variantes ou False, caso contrário.
Essa linha cria um array de variantes de 10.000 bytes. Suponha que você tenha outro array (não-
variante) declarado do mesmo tamanho e que você deseja copiar o conteúdo desse array não-variante
para o array de variantes. Normalmente, você só pode fazer isso percorrendo os elementos e atribuin-
do-os aos elementos do array de variantes, como se pode ver a seguir:
begin
V := VarArrayCreate([1, 10000], VarByte);
for i := 1 to 10000 do
V[i] := A[i];
end;
O problema com esse código é que ele é comprometido pelo significativo overhead necessário para
inicializar os elementos do array de variantes. Isso se deve às atribuições dos elementos do array que têm
que percorrer a lógica em runtime para determinar a compatibilidade de tipos, a localização de cada ele-
mento e assim por diante. Para evitar essas verificações em runtime, você pode usar a função VarArray-
Lock( ) e o procedimento VarArrayUnlock( ).
VarArrayLock( ) bloqueia o array na memória de modo que ele não possa ser movido ou redimensio-
nado enquanto estiver bloqueado e retorna um ponteiro para os dados do array. VarArrayUnlock( ) desblo-
queia um array bloqueado com VarArrayLock( ) e mais uma vez permite que o array de variantes seja redi-
mensionado e movido na memória. Depois que o array é bloqueado, você pode empregar um método
mais eficiente para inicializar o dado usando, por exemplo, o procedimento Move( ) com o ponteiro para
os dados do array. O código a seguir executa a inicialização do array de variantes mostrado anteriormen-
te, mas de uma maneira muito mais eficiente:
begin
V := VarArrayCreate([1, 10000], VarByte);
P := VarArrayLock(V);
try
Move(A, P^, 10000);
finally
VarArrayUnlock(V);
end;
52 end;
Suporte para funções
Há várias outras funções de suporte para variantes que você pode usar. Essas funções são declaradas na
unidade System e também listadas aqui:
procedure VarClear(var V: Variant);
procedure VarCopy(var Dest: Variant; const Source: Variant);
procedure VarCast(var Dest: Variant; const Source: Variant; VarType: Integer);
function VarType(const V: Variant): Integer;
function VarAsType(const V: Variant; VarType: Integer): Variant;
function VarIsEmpty(const V: Variant): Boolean;
function VarIsNull(const V: Variant): Boolean;
function VarToStr(const V: Variant): string;
function VarFromDateTime(DateTime: TDateTime): Variant;
function VarToDateTime(const V: Variant): TDateTime;
O procedimento VarClear( ) atualiza uma variante e define o campo VType como varEmpty. VarCopy( )
copia a variante Source na variante Dest. O procedimento VarCast( ) converte uma variante para um tipo
especificado e armazena esse resultado em outra variante. VarType( ) retorna um dos códigos tipo varXXX
para uma variante especificada. VarAsType( ) tem a mesma funcionalidade que VarCast( ). VarIsEmpty( ) re-
torna True se o código do tipo em uma variante específica for varEmpty. VarIsNull( ) indica se uma variante
contém um valor Null. VarToStr( ) converte uma variante para representação em string (uma string vazia
no caso de uma variante Null ou vazia). VarFromDateTime( ) retorna uma variante que contém um valor TDa-
teTime dado. Finalmente, VarToDateTime( ) retorna o valor TDateTime contido em uma variante.
OleVariant
O tipo OleVariant é quase idêntico ao tipo Variant descrito totalmente nesta seção deste capítulo. A única
diferença entre OleVariant e Variant é que OleVariant somente suporta tipos compatíveis com o Automati-
on. Atualmente, o único VType suportado que não é compatível com o Automation é varString, o código
para AnsiString. Quando uma tentativa é feita para atribuir uma AnsiString a um OleVariant, a AnsiString
será automaticamente convertida em BSTR OLE e armazenada na variante como uma varOleStr.
Currency
O Delphi 2.0 introduziu um novo tipo chamado Currency, que é ideal para cálculos financeiros. Ao con-
trário dos números de ponto flutuante, que permitem que a casa decimal “flutue” dentro de um número,
Currency é um tipo decimal de ponto fixo que pode ter uma precisão de 15 dígitos antes da casa decimal e
de quatro dígitos depois da casa decimal. Por essa razão, ele não é suscetível a erros de arredondamento,
como acontece com os tipos de ponto flutuante. Ao transportar projetos do Delphi 1.0, é uma boa idéia
usar esse tipo em lugar de Single, Real, Double e Extended quando o assunto é dinheiro.
Arrays
O Object Pascal permite criar arrays de qualquer tipo de variável (exceto arquivos). Por exemplo, uma
variável declarada como um array de oito inteiros tem a seguinte aparência:
53
var
A: Array[0..7] of Integer;
Os arrays do Object Pascal têm uma propriedade especial que os diferencia de outras linguagens:
eles não têm que começar em determinado número. Portanto, você pode declarar um array de três ele-
mentos que inicia no 28, como no seguinte exemplo:
var
A: Array[28..30] of Integer;
Como o array do Object Pascal nem sempre começa em 0 ou em 1, você deve ter alguns cuidados
quando interagir com os elementos do array em um loop for. O compilador fornece funções embutidas
chamadas High( ) e Low( ), que retornam os limites inferior e superior de um tipo ou variável de array,
respectivamente. Seu código será menos propenso a erro e mais fácil de se manter se você usar essas fun-
ções para controlar o loop for, como se pode ver a seguir:
var
A: array[28..30] of Integer;
i: Integer;
begin
for i := Low(A) to High(A) do // não use números fixos no loop for!
A[i] := i;
end;
DICA
Sempre comece arrays de caracteres em 0. Os arrays de caracteres baseados em zero podem ser passados
para funções que exigem variáveis do tipo PChar. Essa é uma concessão especial que o compilador oferece.
Para especificar várias dimensões, use uma lista de limites delimitada por vírgulas:
var
// Array bidimensional de Integer:
A: array[1..2, 1..2] of Integer;
Para acessar um array multidimensional, use vírgulas para separar cada dimensão dentro de um
conjunto de colchetes:
I := A[1, 2];
Arrays dinâmicos
Arrays dinâmicos são arrays dinamicamente alocados, nos quais as dimensões não são conhecidas duran-
te a compilação. Para declarar um array dinâmico, basta declarar um array sem incluir as dimensões,
como no exemplo a seguir:
var
// array dinâmico de string:
SA: array of string;
54
Antes de poder usar um array dinâmico, você deve usar o procedimento SetLength( ) para alocar
memória para o array:
begin
// espaço alocado para 33 elementos:
SetLength(SA, 33);
Uma vez que a memória tenha sido alocada, você deve acessar elementos do array dinâmico como
um array normal:
SA[0] := ‘Pooh likes hunny’;
OtherString := SA[0];
NOTA
Arrays dinâmicos são sempre baseados em zero.
Arrays dinâmicos são permanentemente gerenciados e portanto não é preciso liberá-los quando a-
cabar de usá-los, pois serão automaticamente abandonados quando saírem do escopo. Entretanto, pode
surgir o momento em que você deseje remover o array dinâmico da memória antes que ele saia do escopo
(se ele usa muita memória, por exemplo). Para fazer isso, você só precisa atribuir o array dinâmico a nil:
SA := nil; // libera SA
Arrays dinâmicos são manipulados usando uma semântica de referência semelhante à dos tipos
AnsiString, não à
semântica do valor, como ocorre em um array normal. Um teste rápido: qual é o valor
de A1[0] no final
do seguinte fragmento de código?
var
A1, A2: array of Integer;
begin
SetLength(A1, 4);
A2 := A1;
A1[0] := 1;
A2[0] := 26;
A resposta correta é 26. O motivo é que a atribuição A2 := A1 não cria um novo array mas, em vez
disso, fornece A2 com uma referência para o mesmo array de A1. Além disso, qualquer modificação em A2
poderá afetar A1. Se na verdade você deseja fazer uma cópia completa de A1 em A2, use o procedimento
Copy( ) padrão:
A2 := Copy(A1);
Depois que essa linha de código é executada, A2 e A1 serão dois arrays separados, inicialmente con-
tendo os mesmos dados. As mudanças feitas em um deles não afetará o outro. Opcionalmente, você pode
especificar o elemento inicial e número de elementos a serem copiados como parâmetros para Copy( ),
como mostrado aqui:
// copia 2 elementos, iniciando no elemento um:
A2 := Copy(A1, 1, 2);
Arrays dinâmicos também podem ser multidimensionais. Para especificar várias dimensões, acres-
cente um array of adicional para a declaração de cada dimensão:
var
// array dinâmico bidimensional de Integer:
IA: array of array of Integer;
55
Para alocar memória para um array dinâmico multidimensional, passe os tamanhos das outras di-
mensões como parâmetros adicionais em SetLength( ):
begin
// IA será um array de Integer 5 x 5
SetLength(IA, 5, 5);
Você acessa arrays dinâmicos multidimensionais da mesma forma que arrays multidimensionais
normais; cada elemento é separado por uma vírgula com um único conjunto de colchetes:
IA[0,3] := 28;
Records
Uma estrutura definida pelo usuário é chamada de record no Object Pascal, sendo o equivalente da struct
do C ou ao Type do Visual Basic. Como exemplo, aqui está uma definição de registro em Pascal e as defi-
nições equivalentes a ele no C e no Visual Basic:
{ Pascal }
Type
MyRec = record
i: Integer;
d: Double;
end;
/* C */
typedef struct {
int i;
double d;
} MyRec;
‘Visual Basic
Type MyRec
i As Integer
d As Double
End Type
Ao trabalhar com um registro, use o símbolo de ponto para acessar seus campos. Aqui está um
exemplo:
var
N: MyRec;
begin
N.i := 23;
N.d := 3.4;
end;
O Object Pascal também trabalha com registros de variantes, que permite que diferentes partes de
dados ocupem a mesma parte da memória no registro. Não confunda isso com o tipo de dados Variant; os
registros de variante permitem que cada sobreposição de campo de dados seja acessada independente-
mente. Se você tem formação em C/C++, perceberá as semelhanças entre o conceito de registro de vari-
ante e o de uma union dentro da struct do C. O código a seguir mostra um registro de variante no qual um
Double, um Integer e um char ocupam o mesmo espaço de memória:
type
TVariantRecord = record
NullStrField: PChar;
IntField: Integer;
case Integer of
56 0: (D: Double);
1: (I: Integer);
2: (C: char);
end;
NOTA
As regras do Object Pascal determinam que a parte variante de um registro não pode ser de nenhum tipo
permanentemente gerenciado.
Sets
Sets (conjuntos) são um tipo exclusivo do Pascal, que não têm um equivalente no Visual Basic, C ou no
C++ (embora o Borland C++Builder implemente uma classe de modelo chamada Set, que simula o
comportamento de um conjunto do Pascal). Os conjuntos fornecem um método muito eficiente de re-
presentação de uma coleção de valores enumerados, ordinais e de caracteres. Você pode declarar um
novo tipo de conjunto usando as palavras-chave set of seguida por um tipo ordinal ou subfaixas de possí-
veis valores do conjunto. Veja o exemplo a seguir:
type
TCharSet = set of char; // membros possíveis: #0 - #255
TEnum = (Monday, Tuesday, Wednesday, Thursday, Friday);
TEnumSet = set of TEnum; // pode conter qualquer combinação de membros TEnum
TSubrangeSet = set of 1..10; // membros possíveis: 1 - 10
TAlphaSet = set of ‘A’..’z’; // membros possíveis: ‘A’ - ‘z’
Observe que um conjunto só pode conter até 256 elementos. Além disso, apenas tipos ordinais po-
dem seguir as palavras-chave set of. Portanto, as seguintes declarações são ilegais:
type
TIntSet = set of Integer; // Inválida: excesso de elementos
TStrSet = set of string; // Inválida: não é um tipo ordinal
Os conjuntos armazenam seus elementos internamente como bytes individuais. Isso os torna muito
eficientes em termos de velocidade e uso de memória. Conjuntos com menos de 32 elementos no tipo bá-
sico podem ser armazenados e operados à medida que a CPU os registra, o que aumenta ainda mais a sua
eficácia. Conjuntos com 32 ou mais elementos (como um conjunto de 255 elementos char) são armazena-
dos na memória. Para obter todo o benefício que os conjuntos podem proporcionar em termos de de-
sempenho, mantenha o número de elementos no tipo básico do conjunto inferior a 32.
57
Usando conjuntos
Use colchetes para fazer referência aos elementos do conjunto. O código a seguir demonstra como decla-
rar variáveis tipo set e atribuir valores a elas:
type
TCharSet = set of char; // membros possíveis: #0 - #255
TEnum = (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday);
TEnumSet = set of TEnum; // pode conter qualquer combinação de membros TEnum
var
CharSet: TCharSet;
EnumSet: TEnumSet;
SubrangeSet: set of 1..10; // membros possíveis: 1 - 10
AlphaSet: set of ‘A’..’z’; // membros possíveis: ‘A’ - ‘z’
begin
CharSet := [‘A’..’J’, ‘a’, ‘m’];
EnumSet := [Saturday, Sunday];
SubrangeSet := [1, 2, 4..6];
AlphaSet := [ ]; // Vazio; sem elementos
end;
Conjunto de operadores
O Object Pascal fornece vários operadores para usar na manipulação de conjuntos. Você pode usar esses
operadores para determinar a filiação, união, diferença e interseção do conjunto.
Filiação
Use o operador para determinar se um elemento dado está contido em um conjunto qualquer. Por exem-
plo, o código a seguir poderia ser usado para determinar se o conjunto CharSet mencionado anteriormen-
te contém a letra ‘S’:
if ‘S’ in CharSet then
// faz alguma coisa;
União e diferença
Use os operadores + e - ou os procedimentos Include( ) e Exclude( ) para adicionar e remover elementos
de uma variável de conjunto:
Include(CharSet, ‘a’); // adiciona ‘a’ ao conjunto
CharSet := CharSet + [‘b’]; // adiciona ‘b’ ao conjunto
Exclude(CharSet, ‘x’); // remove ‘z’ do conjunto
CharSet := CharSet - [‘y’, ‘z’]; // remove ‘y’ e ‘z’ do conjunto
DICA
Quando for possível, use Include( ) e Exclude( ) para adicionar e remover um único elemento de um con-
junto, em vez dos operadores + e -. Tanto Include( ) quanto Exclude( ) constituem apenas uma instrução
de máquina, enquanto os operadores + e - exigem 13 + 6n instruções (onde n é o tamanho em bytes de um
conjunto).
58
Interseção
Use o operador * para calcular a interseção de dois conjuntos. O resultado da expressão Set1 * Set2 é um
conjunto que contém todos os membros que Set1 e Set2 têm em comum. Por exemplo, o código a seguir
poderia ser usado como um método eficiente para determinar se um determinado conjunto contém vários
elementos:
if [‘a’, ‘b’, ‘c’] * CharSet = [‘a’, ‘b’, ‘c’] then
// faz alguma coisa
Objetos
Pense em objetos como registros que também contêm funções e procedimentos. O modelo de objeto do
Delphi é discutido com maiores detalhes na seção “Como usar objetos do Delphi” deste capítulo; por
essa razão, esta seção vai se ater apenas à sintaxe básica dos objetos do Object Pascal. Um objeto é defini-
do da seguinte maneira:
Type
TChildObject = class(TParentObject);
SomeVar: Integer;
procedure SomeProc;
end;
Embora os objetos do Delphi não sejam idênticos aos objetos do C++, essa declaração pode ser
considerada um equivalente à seguinte declaração no C++:
class TChildObject : public TparentObject
{
int SomeVar;
void SomeProc( );
};
Os métodos são definidos do mesmo modo que os procedimentos e as funções normais (discutidos
na seção “Procedimentos e funções”), com o acréscimo do nome do objeto e o operador de símbolo de
ponto:
procedure TChildObject.SomeProc;
begin
{ código de procedimento entra aqui }
end;
NOTA
Os objetos do Object Pascal não são organizados na memória do mesmo modo que os objetos do C++ e,
por essa razão, não é possível usar objetos do C++ diretamente no Delphi (e vice-versa). Entretanto, o Ca-
pítulo 13 mostra uma técnica para compartilhar objetos entre C++ e Delphi.
Uma exceção é a capacidade do C++Builder da Borland de criar classes que são mapeadas diretamente
em classes do Object Pascal usando a diretiva registrada __declspec(delphiclass). Esses objetos são
igualmente incompatíveis com os objetos normais do C++.
59
Pointers
Um pointer (ponteiro) é uma variável que contém uma localização na memória. Você já viu um exemplo
de um ponteiro no tipo PChar neste capítulo. Um tipo de ponteiro genérico do Pascal é denominado, logi-
camente, Pointer. Algumas vezes, um Pointer é chamado de ponteiro não-tipificado, pois contém apenas
um endereço de memória e o compilador não mantém qualquer informação sobre os dados para os quais
aponta. Essa noção, entretanto, vai de encontro à natureza de proteção de tipos do Pascal; portanto, os
ponteiros em seu código normalmente serão ponteiros tipificados.
NOTA
O uso de ponteiros é um tópico relativamente avançado e com toda a certeza você não precisa dominá-lo
para escrever uma aplicação em Delphi. Quando tiver mais experiência, os ponteiros se tornarão outra fer-
ramenta valiosa para sua caixa de ferramentas de programador.
Ponteiros tipificados são declarados usando o operador ^ (ou ponteiro) na seção Type do seu progra-
ma. Ponteiros tipificados ajudam o compilador a monitorar com precisão o tipo para o qual um determi-
nado ponteiro aponta, permitindo assim que o compilador monitore o que você está fazendo (e pode fa-
zer) com uma variável de ponteiro. Aqui estão algumas declarações típicas para ponteiros:
Type
PInt = ^Integer; // PInt é agora um ponteiro para um Integer
Foo = record // Um tipo de registro
GobbledyGook: string;
Snarf: Real;
end;
PFoo = ^Foo; // PFoo é um ponteiro para um tipo Foo
var
P: Pointer; // Ponteiro não-tipificado
P2: PFoo; // Exemplo de PFoo
NOTA
Os programadores em C observarão a semelhança entre o operador ^ do Object Pascal e o operador * do
C. O tipo Pointer do Pascal corresponde ao tipo void * do C.
Lembre-se de que uma variável de ponteiro armazena apenas um endereço de memória. Cabe a
você, como programador, alocar espaço para o local que o ponteiro aponta, qualquer que seja ele. Você
pode alocar espaço para um ponteiro usando uma das rotinas de alocação de memória discutidas ante-
riormente e mostradas na Tabela 2.6.
NOTA
Quando um ponteiro não aponta para nada (seu valor é zero), diz-se que seu valor é Nil e geralmente ele é
chamado de ponteiro nil ou null.
Se você deseja acessar os dados para os quais um ponteiro aponta, coloque o ponteiro ^ depois do
nome de variável do ponteiro. Esse método é conhecido como desreferenciamento do ponteiro. O códi-
go a seguir ilustra o trabalho com ponteiros:
60
Program PtrTest;
Type
MyRec = record
I: Integer;
S: string;
R: Real;
end;
PMyRec = ^MyRec;
var
Rec : PMyRec;
begin
New(Rec); // memória alocada para Rec
Rec^.I := 10; // Coloca dados no Rec. Observe os desreferenciamento
Rec^.S := ‘And now for something completely different.’;
Rec^.R := 6.384;
{ Rec agora está cheio}
Dispose(Rec); // Não se esqueça de liberar a memória!
end.
Um aspecto do Object Pascal que pode dar alguma dor de cabeça aos programadores C é a rígida ve-
rificação de tipo executada nos tipos de ponteiro. Por exemplo, os tipos das variáveis a e b no exemplo a
seguir não são compatíveis:
var
a: ^Integer;
b: ^Integer;
Por outro lado, os tipos das variáveis a e b na declaração equivalente no C são compatíveis:
int *a;
int *b
Como o Object Pascal só cria um tipo para cada declaração “ponteiro-para-tipo”, você deve criar
um tipo nomeado caso deseje atribuir valores de a para b, como mostrado aqui:
type
PtrInteger = ^Integer; // dá nome ao tipo
var
a, b: PtrInteger; // agora a e b são compatíveis
61
Aliases de tipo
O Object Pascal tem a capacidade de criar nomes novos, ou aliases (nomes alternativos), para tipos já de-
finidos. Por exemplo, se você deseja criar um nome novo para um Integer chamado MyReallyNiftyInteger,
poderia fazê-lo usando o código a seguir:
type
MyReallyNiftyInteger = Integer;
O alias de tipo recém-definido é totalmente compatível com o tipo do qual ele é um alias. Nesse
caso, isso significa que você poderia usar MyReallyNiftyInteger em qualquer lugar em que pudesse usar
Integer.
É possível, entretanto, definir aliases solidamente tipificados, que são considerados tipos novos e
exclusivos pelo compilador. Para fazer isso, use a palavra reservada type da seguinte forma:
type
MyOtherNeatInteger = type Integer;
Usando essa sintaxe, o tipo MyOtherNeatInteger será convertido para um Integer quando houver neces-
sidade de se fazer uma atribuição, mas MyOtherNeatInteger não será compatível com Integer quando usado
em parâmetros var e out. Portanto, o código a seguir é sintaticamente correto:
var
MONI: MyOtherNeatInteger;
I: Integer;
begin
I := 1;
MONI := I;
var
M: MyOtherNeatInteger;
begin
M := 29;
Goon(M); // Erro: M não é uma var compatível com Integer
Além dessa questão de compatibilidade de tipo imposta pelo compilador, o compilador gera RTTI
para aliases solidamente tipificados. Isso permite que você crie editores de propriedade exclusivos para
tipos simples, como irá aprender no Capítulo 22.
Na sintaxe a seguir, um typecast é exigido para converter c em um byte. Na prática, um typecast diz
ao compilador que você realmente sabe o que está fazendo e que deseja converter um tipo para outro:
var
c: char;
b: byte;
begin
c := ‘s’;
b := byte(c); // o compilador fica feliz da vida nesta linha
end.
NOTA
Você só pode fazer um typecast de uma variável de um tipo para outro tipo se o tamanho dos dados das
duas variáveis for igual. Por exemplo, você não pode fazer um typecast de um Double para um Integer. Para
converter um tipo de ponto flutuante para um integer, use as funções Trunc( ) ou Round( ). Para converter
um inteiro em um valor de ponto flutuante, use o operador de atribuição: FloatVar := IntVar.
O Object Pascal também aceita uma variedade especial de typecast entre objetos usando o operador
as, que é descrito posteriormente na seção “Runtime Type Information” deste capítulo.
Recursos de string
O Delphi 3 introduziu a capacidade de colocar recursos de string diretamente no código-fonte do Object
Pascal usando a cláusula resourcestring. Os recursos de string são strings literais (geralmente exibidas
para o usuário), que estão fisicamente localizadas em um recurso anexado à aplicação ou à biblioteca, em
vez de estarem embutidos no código-fonte. Seu código-fonte faz referência a recursos de string, não a
strings literais. Separando as strings do código-fonte, sua aplicação pode ser mais facilmente traduzida
pelos recursos de string adicionados para um idioma diferente. Recursos de string são declarados no for-
mato identificador = string literal, na cláusula resourcestring, como se vê a seguir:
resourcestring
ResString1 = ‘Resource string 1’;
ResString2 = ‘Resource string 2’;
ResString3 = ‘Resource string 3’;
Sintaticamente, essas strings podem ser usadas no seu código-fonte de um modo idêntico às cons-
tantes de string:
resourcestring
ResString1 = ‘hello’;
ResString2 = ‘world’;
var
String1: string;
begin
String1 := ResString1 + ‘ ‘ + ResString2;
.
.
.
end;
63
Testando condições
Esta seção compara construções if e case no Pascal a construções semelhantes no C e no Visual Basic.
Pressupomos que você já esteja acostumado com esses tipos de construções de programa, e por isso não
vamos perder tempo ensinando o que você já sabe.
A instrução if
Uma instrução if permite que você determine se certas condições são atendidas antes de executar um de-
terminado bloco de código. Como exemplo, aqui está uma instrução if em Pascal, seguida pelas defini-
ções equivalentes no C e no Visual Basic:
{ Pascal }
if x = 4 then y := x;
/* C */
if (x == 4) y = x;
‘Visual Basic
If x = 4 Then y = x
NOTA
Se você tem uma instrução if que faz várias comparações, certifique-se de fechar cada conjunto de com-
paração entre parênteses a fim de não comprometer a legibilidade do código. Faça isto:
if (x = 7) and (y = 8) then
Entretanto, não faça isto (para não deixar o compilador de mau humor):
if x = 7 and y = 8 then
Use as palavras-chave begin e end em Pascal praticamente do mesmo modo que você usaria { e } em C
e C++. Por exemplo, use a seguinte construção se você deseja executar várias linhas de texto quando
uma dada condição é verdadeira:
if x = 6 then begin
DoSomething;
DoSomethingElse;
DoAnotherThing;
end;
NOTA
O tipo seletor de uma instrução case deve ser um tipo ordinal. É ilegal usar tipos não-ordinais (strings, por
exemplo) como seletores de case.
Loops
Um loop é uma construção que permite executar repetidamente algum tipo de ação. As construções de
loop do Pascal são muito semelhantes às que você já viu na sua experiência com outras linguagens, e por
essa razão este capítulo não irá desperdiçar o seu precioso tempo com aulas sobre loops. Esta seção des-
creve as várias construções de loop que você pode usar em Pascal.
O loop for
Um loop for é ideal quando você precisa repetir uma ação por um determinado número de vezes. Aqui está
um exemplo, embora não muito útil, de um loop for que soma o índice do loop a uma variável dez vezes:
var
I, X: Integer;
begin
X := 0;
for I := 1 to 10 do
inc(X, I);
end.
X = 0
For I = 1 to 10
X = X + I
Next I
ATENÇÃO
Um aviso para aqueles que estão familiarizados com o Delphi 1: atribuições para a variável de controle do
loop não são mais permitidas devido ao modo como o loop é otimizado e gerenciado pelo compilador de
32 bits.
O loop while
Use uma construção de loop while quando desejar que alguma parte do seu código se repita enquanto al-
guma condição for verdadeira. As condições do loop while são testadas antes que o loop seja executado.
Um exemplo clássico para o uso de um loop while é executar repetidamente alguma ação em um arquivo
enquanto o fim do arquivo não for encontrado. Aqui está um exemplo que demonstra um loop que lê
uma linha de cada vez de um arquivo e a escreve na tela:
Program FileIt;
{$APPTYPE CONSOLE}
var
f: TextFile; // um arquivo de texto
s: string;
begin
AssignFile(f, ‘foo.txt’);
Reset(f);
while not EOF(f) do begin
readln(f, S);
writeln(S);
end;
CloseFile(f);
end.
O loop while do Pascal funciona basicamente da mesma maneira que o loop while do C ou do Visual
Basic.
repeat... untill
O loop repeat..until trata do mesmo tipo de problema de um loop while, porém por um ângulo diferente.
Ele repete um determinado bloco de código até uma certa condição tornar-se verdadeira (True). Ao con-
trário de um loop while, o código do loop sempre é executado ao menos uma vez, pois a condição é testa-
da no final do loop. A construção repeat..until do Pascal é, grosso modo, equivalente ao loop do..while do
C.
Por exemplo, o fragmento de código a seguir repete uma instrução que incrementa um contador até
o valor do contador se tornar maior do que 100:
66
var
x: Integer;
begin
X := 1;
repeat
inc(x);
until x > 100;
end.
O procedimento Break( )
Chamar Break( ) de dentro de um loop while, for ou repeat faz com que o fluxo do seu programa salte ime-
diatamente para o fim do loop atualmente executado. Esse método é útil quando você precisa deixar o
loop imediatamente devido a alguma circunstância que tenha surgido dentro do loop. O procedimento
Break( ) do Pascal é análogo às instruções Break do C e Exit do Visual Basic. O loop a seguir usa Break( )
para terminar o loop após cinco iterações:
var
i: Integer;
begin
for i := 1 to 1000000 do
begin
MessageBeep(0); // faz o computador emitir um aviso sonoro
if i = 5 then Break;
end;
end;
O procedimento Continue( )
Chame Continue( ) dentro de um loop quando desejar ignorar uma parte do código e o fluxo de controle
para continuar com a próxima iteração do loop. Observe no exemplo a seguir que o código depois de
Continue( ) não é executado na primeira iteração do loop:
var
i: Integer;
begin
for i := 1 to 3 do
begin
writeln(i, ‘. Before continue’);
if i = 1 then Continue;
writeln(i, ‘. After continue’);
end;
end;
Procedimentos e funções
Como um programador, você já deve estar familiarizado com os fundamentos de procedimentos e fun-
ções. Um procedimento é uma parte distinta do programa que executa uma determinada tarefa quando é
chamado e em seguida retorna para a parte do código que o chamou. Uma função funciona da mesma
maneira, exceto que retorna um valor depois de sair para a parte do programa que a chamou.
Se você está familiarizado com C ou C++, considere que um procedimento do Pascal é equivalente
a uma função C ou C++ que retorna void, enquanto uma função corresponde a uma função C ou C++
que possui um valor de retorno.
A Listagem 2.1 demonstra um pequeno programa em Pascal com um procedimento e uma função.
67
Listagem 2.1 Um exemplo de funções e procedimentos
Program FuncProc;
{$APPTYPE CONSOLE}
var
Num: Integer;
begin
Num := 23;
BiggerThanTen(Num);
if IsPositive(Num) then
writeln(Num, ‘Is positive.’)
else
writeln(Num, ‘Is negative.’);
end.
NOTA
A variável local Result na função IsPositive( ) merece atenção especial. Todas as funções do Object Pas-
cal têm uma variável local implícita chamada Result, que contém o valor de retorno da função. Observe
que, diferentemente de C e C++, a função não termina tão logo um valor seja atribuído a Result.
Você também pode retornar um valor de uma função atribuindo o nome de uma função para um valor
dentro do código da função. Essa é a sintaxe-padrão do Pascal e um remanescente de versões do Borland
Pascal. Se você escolher usar o nome de função dentro do corpo, observe cuidadosamente que existe uma
enorme diferença entre usar o nome de função no lado esquerdo de um operador de atribuição e usá-lo
em qualquer outro lugar no seu código. Se você o usa à esquerda, está atribuindo o valor de retorno da fun-
ção. Se você o usa em qualquer lugar no seu código, está chamando a função recursivamente!
Observe que a variável Result implícita não é permitida quando a opção Extended Syntax (sintaxe es-
tendida) do compilador está desativada na caixa de diálogo Project, Options, Compiler (projeto, opções,
compilador) ou quando você está usando a diretiva {$X-}.
Passando parâmetros
O Pascal permite que você passe parâmetros por valor ou por referência para funções e procedimentos.
Os parâmetros que você passa podem ser de qualquer base ou um tipo definido pelo usuário ou um array
aberto (arrays abertos são discutidos posteriormente neste capítulo). Os parâmetros também podem ser
constantes, se seus valores não mudarem no procedimento ou função.
68
Parâmetros de valor
Os parâmetros de valor são o modo-padrão de passar parâmetros. Quando um parâmetro é passado por
valor, significa que uma cópia local dessa variável é criada e a função ou o procedimento opera sobre a
cópia. Considere o seguinte exemplo:
procedure Foo(s: string);
Quando você chama um procedimento dessa forma, uma cópia da string s é criada e Foo( ) opera so-
bre a cópia local de s. Isso significa que você pode escolher o valor de s sem ter nenhum efeito na variável
passada a Foo( ).
Parâmetros de referência
O Pascal também permite passar variáveis para funções e procedimentos por referência; os parâmetros
passados por referência são também chamados de parâmetros de variável. Passar por referência significa
que a função ou procedimento que recebe a variável pode modificar o valor dessa variável. Para passar
uma variável por referência, use a palavra-chave var na lista de parâmetros de procedimento ou função:
procedure ChangeMe(var x: longint);
begin
x := 2; { x é agora alterado no procedimento de chamada }
end;
Em vez de fazer uma cópia de x, a palavra-chave var faz com que o endereço do parâmetro seja co-
piado, de modo que seu valor possa ser modificado diretamente.
O uso de parâmetros var é equivalente a passar variáveis por referência no C++ usando o operador
&. Assim como o operador & do C++, a palavra-chave var faz com que o endereço da variável seja passa-
do para a função ou procedimento, e não o valor da variável.
Parâmetros de constante
Se você não deseja que o valor de um parâmetro passado em uma função seja mudado, pode declará-lo com
a palavra-chave const. A palavra-chave const não apenas o impede de modificar o valor dos parâmetros,
como também gera mais código adequado para strings e registros passados no procedimento ou função.
Aqui está um exemplo de uma declaração de procedimento que recebe um parâmetro de string constante:
procedure Goon(const s: string);
Você pode passar variáveis, constantes ou expressões de constantes para funções e procedimentos
de array aberto. O código a seguir demonstra isso chamando AddEmUp( ) e passando uma variedade de ele-
mentos diferentes:
var
i, Rez: Integer;
const
j = 23;
begin
i := 8;
Rez := AddEmUp([i, 50, j, 89]);
Para funcionar com um array aberto dentro da função ou procedimento, você pode usar as funções
High( ), Low( ) e SizeOf( ) para obter informações sobre o array. Para ilustrar isso, o código a seguir mos-
tra uma implementação da função AddEmUp( ) que retorna a soma de todos os números passados em A: 69
function AddEmUp(A: array of Integer): Integer;
var
i: Integer;
begin
Result := 0;
for i := Low(A) to High(A) do
inc(Result, A[i]);
end;
O Object Pascal também aceita um array of const, que permite passar tipos de dados heterogêneos
em um array para uma função ou procedimento. A sintaxe para definir uma função ou procedimento que
aceita um array of const é a seguinte:
procedure WhatHaveIGot(A: array of const);
O compilador converte implicitamente todos os parâmetros para o tipo TVarRec quando eles são pas-
sados para a função ou procedimento aceitando o array of const. TVarRec é definido na unidade System da
seguinte maneira:
type
PVarRec = ^TVarRec;
TVarRec = record
case Byte of
vtInteger: (VInteger: Integer; VType: Byte);
vtBoolean: (VBoolean: Boolean);
vtChar: (VChar: Char);
vtExtended: (VExtended: PExtended);
vtString: (VString: PShortString);
vtPointer: (VPointer: Pointer);
vtPChar: (VPChar: PChar);
vtObject: (VObject: TObject);
vtClass: (VClass: TClass);
vtWideChar: (VWideChar: WideChar);
vtPWideChar: (VPWideChar: PWideChar);
vtAnsiString: (VAnsiString: Pointer);
vtCurrency: (VCurrency: PCurrency);
vtVariant: (VVariant: PVariant);
vtInterface: (VInterface: Pointer);
vtWideString: (VWideString: Pointer);
vtInt64: (VInt64: PInt64);
end;
O campo VType indica o tipo de dados que o TVarRec contém. Esse campo pode ter qualquer um dos
seguintes valores:
const
{ TVarRec.VType values }
vtInteger = 0;
vtBoolean = 1;
vtChar = 2;
vtExtended = 3;
vtString = 4;
vtPointer = 5;
vtPChar = 6;
70
vtObject = 7;
vtClass = 8;
vtWideChar = 9;
vtPWideChar = 10;
vtAnsiString = 11;
vtCurrency = 12;
vtVariant = 13;
vtInterface = 14;
vtWideString = 15;
vtInt64 = 16;
Como você pode imaginar, visto que array of const no código permite passar parâmetros indepen-
dentemente de seu tipo, pode ser difícil de trabalhar com eles no lado do receptor. Como um exemplo
de como trabalhar com um array of const, a implementação de WhatHaveIGot( ) a seguir percorre o array
e mostra uma mensagem para o usuário indicando o tipo de dados que foi passado em determinado ín-
dice:
procedure WhatHaveIGot(A: array of const);
var
i: Integer;
TypeStr: string;
begin
for i := Low(A) to High(A) do
begin
case A[i].VType of
vtInteger : TypeStr := ‘Integer’;
vtBoolean : TypeStr := ‘Boolean’;
vtChar : TypeStr := ‘Char’;
vtExtended : TypeStr := ‘Extended’;
vtString : TypeStr := ‘String’;
vtPointer : TypeStr := ‘Pointer’;
vtPChar : TypeStr := ‘PChar’;
vtObject : TypeStr := ‘Object’;
vtClass : TypeStr := ‘Class’;
vtWideChar : TypeStr := ‘WideChar’;
vtPWideChar : TypeStr := ‘PWideChar’;
vtAnsiString : TypeStr := ‘AnsiString’;
vtCurrency : TypeStr := ‘Currency’;
vtVariant : TypeStr := ‘Variant’;
vtInterface : TypeStr := ‘Interface’;
vtWideString : TypeStr := ‘WideString’;
vtInt64 : TypeStr := ‘Int64’;
end;
ShowMessage(Format(‘Array item %d is a %s’, [i, TypeStr]));
end;
end;
Escopo
Escopo faz referência a alguma parte do seu programa na qual uma determinada função ou variável é
conhecida pelo compilador. Por exemplo, uma constante global está no escopo de todos os pontos do
seu programa, enquanto uma variável local só tem escopo dentro desse procedimento. Considere a Lis-
tagem 2.2.
71
Listagem 2.2 Uma ilustração de escopo
program Foo;
{$APPTYPE CONSOLE}
const
SomeConstant = 100;
var
SomeGlobal: Integer;
R: Real;
begin
LocalReal := 10.0;
R := R - LocalReal;
end;
begin
SomeGlobal := SomeConstant;
R := 4.593;
SomeProc(R);
end.
SomeConstant, SomeGlobal e R possuem escopo global – seus valores são conhecidos pelo compilador
em todos os pontos dentro do programa. O procedimento SomeProc( ) possui duas variáveis nas quais o
escopo é local a esse procedimento: R e LocalReal. Se você tentar acessar LocalReal fora de SomeProc( ), o
compilador exibe um erro de identificador desconhecido. Se você acessar R dentro de SomeProc( ), estará
se referindo à versão local, mas, se acessar R fora desse procedimento, estará se referindo à versão global.
Unidades
Unidades são módulos de código-fonte individuais que compõem um programa em Pascal. Uma unidade
é um lugar para você agrupar funções e procedimentos que podem ser chamados a partir do seu progra-
ma principal. Para ser uma unidade, um módulo-fonte deve consistir em pelo menos três partes:
l Uma instrução unit. Todas as unidades devem ter como sua primeira linha uma instrução dizendo
que é uma unidade e identificando o nome da unidade. O nome da unidade sempre deve ser igual
ao nome do arquivo. Por exemplo, se você tem um arquivo chamado FooBar, a instrução deve ser
unit FooBar;
l A parte interface. Depois da instrução unit, a linha de código funcional a seguir deve ser a instru-
ção interface. Tudo o que vem depois dessa instrução, até a instrução implementation, é informa-
ção que pode ser compartilhada com o seu programa e com outras unidades. A parte interface de
uma unidade é onde você declara os tipos, constantes, variáveis, procedimentos e funções que
deseja tornar disponíveis ao seu programa principal e a outras unidades. Somente declarações –
nunca o corpo de um procedimento – podem aparecer na interface. A instrução interface deverá
ser uma palavra em uma linha:
interface
l A parte implementation. Isso vem depois da parte interface da unidade. Embora a parte implementa-
tion da unidade contenha principalmente procedimentos e funções, também é nela que você de-
clara os tipos, constantes e variáveis que não deseja tornar disponíveis fora dessa unidade. A
72
parte implementation é onde você define as funções ou procedimentos que declarou na parte in-
terface. A instrução implementation deverá ser uma palavra em uma linha:
implementation
Opcionalmente, uma unidade também pode incluir duas outras partes:
l Uma parte initialization. Essa parte da unidade, que está localizada próximo ao fim do arquivo,
contém qualquer código de inicialização para a unidade. Esse código será executado antes de o
programa principal iniciar sua execução e é executado apenas uma vez.
l Uma parte finalization. Essa parte da unidade, que está localizada entre initialization e end da
unidade, contém qualquer código de limpeza executado quando o programa termina. A seção
finalization foi introduzida à linguagem no Delphi 2.0. No Delphi 1.0, a finalização da unidade
era realizada com a adição de um novo procedimento de saída por meio da função AddExit-
Proc( ). Se você está transportando uma aplicação do Delphi 1.0, deve mover os procedimentos
de saída para a parte finalization de suas unidades.
NOTA
Quando várias unidades possuem código initialization/finalization, a execução de cada seção segue
na ordem na qual as unidades são encontradas pelo compilador (a primeira unidade na cláusula uses do
programa, depois a primeira unidade na cláusula uses dessa unidade etc.). Também é uma péssima idéia
escrever código de inicialização e finalização que dependa dessa seqüência, pois uma pequena mudança
na cláusula uses pode gerar alguns bugs difíceis de serem localizados!
A cláusula uses
A cláusula uses é onde você lista as unidades que deseja incluir em um programa ou unidade em
particular. Por exemplo, se você tem um programa chamado FooProg, que utiliza funções e tipos em
duas unidades, UnitA e UnitB, a declaração uses apropriada é feita da seguinte maneira:
Program FooProg;
As unidades podem ter duas cláusulas uses: uma na seção interface e outra na seção implementation.
Veja a seguir o exemplo de um código para uma unidade:
Unit FooBar;
interface
uses BarFoo;
implementation
uses BarFly;
initialization
{ inicialização da unidade aqui }
finalization
{ término da unidade aqui }
end.
73
Referências circulares entre unidades
Ocasionalmente, você se verá em uma situação onde UnitA usa UnitB e UnitB usa UnitA. Essa é a chamada refe-
rência circular entre unidades. A ocorrência de uma referência circular é muitas vezes uma indicação de
uma falha de projeto na sua aplicação; evite estruturar seu programa com uma referência circular. Muitas
vezes, a melhor solução é mover uma parte dos dados que tanto a UnitA quanto a UnitB precisam utilizar de
modo a criar uma terceira unidade. Entretanto, como acontece com muitas coisas, algumas vezes você não
pode evitar a referência circular entre as unidades. Nesse caso, mova uma das cláusulas uses para a parte im-
plementation de sua unidade e deixe a outra na parte interface. Isso normalmente resolve o problema.
Pacotes
Os pacotes (packages) do Delphi permitem que você coloque partes de sua aplicação em módulos separa-
dos, que podem ser compartilhados por diversas aplicações. Se você já tem algum conhecimento do có-
digo do Delphi 1 ou 2, apreciará poder tirar vantagem de pacotes sem qualquer alteração no seu códi-
go-fonte existente.
Pense em um pacote como uma coleção de unidades armazenadas em um módulo semelhante à
DLL separada (uma Borland Package Library ou um arquivo BPL). Em seguida, sua aplicação pode ser
vinculada a essas unidades de “pacote” em runtime, não durante a compilação/linkedição. Como o códi-
go dessas unidades reside no arquivo BPL e não no EXE ou no DLL, o tamanho do EXE ou do DLL pode
se tornar muito pequeno. Quatro tipos de pacotes estão disponíveis para você criar e usar:
l Pacote de runtime. Esse tipo de pacote contém unidades exigidas em runtime pela sua aplicação.
Quando compilada de modo a depender de um pacote de runtime em particular, sua aplicação
não será executada na ausência desse pacote. O arquivo VCL50.BPL do Delphi é um exemplo desse
tipo de pacote.
l Pacote de projeto. Esse tipo de pacote contém elementos necessários ao projeto da aplicação,
como componentes, propriedades e editores de componentes, bem como assistentes. Pode ser
instalado na biblioteca de componentes do Delphi usando o item de menu Component, Install
Package (componente, instalar pacote). Os pacotes DCL*.BPL do Delphi são exemplos desse tipo
de pacote. Esse tipo de pacote é descrito com maiores detalhes no Capítulo 21.
l Pacote de runtime e projeto. Esse pacote serve para ambos os objetivos listados nos dois primei-
ros itens. Criar esse tipo de pacote torna o desenvolvimento e a distribuição de aplicações muito
mais simples, mas esse tipo de pacote é menos eficiente porque deve transportar a bagagem de
suporte ao projeto até mesmo em suas aplicações já distribuídas.
l Pacote nem runtime nem projeto. Esse tipo raro de pacote só é usado por outros pacotes e uma
aplicação não deve fazer referência a ele, que também não deve ser usado no ambiente de projeto.
Sintaxe do pacote
Pacotes normalmente são criados por meio do Package Editor, que você chama selecionando o item de
menu File, New, Package (arquivo, novo, pacote). Esse editor gera um arquivo Delphi Package Source
(DPK), que será compilado em um pacote. A sintaxe para esse arquivo DPK é bem simples e usa o seguin-
74 te formato:
package PackageName
contains
Unit1 in ‘Unit1.pas’,
Unit2, in ‘Unit2.pas’,
...;
end.
Os pacotes listados na cláusula requires são necessários para que esse pacote seja carregado. Geral-
mente, os pacotes que contêm unidades usadas pelas unidades listadas na cláusula contains são listados
aqui. As unidades listadas na cláusula contains serão compiladas nesse pacote. Observe que as unidades
listadas aqui não devem ser listadas na cláusula contains de qualquer um dos pacotes listados na cláusula
requires. Observe também que qualquer unidade usada pelas unidades na cláusula contains será implicita-
mente inserida nesse pacote (a não ser que estejam contidas no pacote exigido).
75
Fruta
Argentina Brasileira
Você deve compreender os três termos a seguir antes de continuar a explorar o conceito de objetos:
l Campo. Também chamado definições de campo ou variáveis de instância, campos são variáveis
de dados contidas nos objetos. Um campo em um objeto é como um campo em um registro do
Pascal. Em C++, alguma vezes os campos são chamados de dados-membro.
l Método. O nome para procedimentos e funções pertencentes a um objeto. Métodos são chama-
dos funções-membro no C++.
l Propriedade. Uma entidade que atua como um acesso para os dados e o código contidos em um
objeto. Propriedades preservam o usuário final dos detalhes de implementação de um objeto.
NOTA
Geralmente é considerado mau estilo de OOP acessar um campo de um objeto diretamente. Isso se deve
ao fato de os detalhes de implementação do objeto poderem mudar. Em vez disso, use propriedades de
acesso, que concedem uma interface de objeto default sem que haja necessidade de muitos conhecimen-
tos sobre o modo como os objetos são implementados. As propriedades são explicadas na seção “Proprie-
dades”, mais adiante neste capítulo.
Declaração e instanciação
É claro que, antes de usar um objeto, você deve ter declarado um objeto usando a palavra-chave class.
Como já dissemos neste capítulo, os objetos são declarados na seção type de uma unidade ou programa:
type
TFooObject = class;
Além de um tipo de objeto, você normalmente terá uma variável desse tipo de classe, ou instância,
declarada na seção var:
var
FooObject: TFooObject;
Você cria uma instância de um objeto em Object Pascal chamando um dos seus construtores. Um
construtor é responsável pela criação de uma instância de seu objeto e pela alocação de qualquer memó-
ria ou pela inicialização dos campos necessários, de modo que o objeto esteja em um estado utilizável
quando o construtor for fechado. Os objetos do Object Pascal sempre têm no mínimo um construtor
chamado Create( ) – embora seja possível que um objeto tenha mais de um construtor. Dependendo do
tipo de objeto, Create( ) pode utilizar diferentes quantidades de parâmetros. Este capítulo discute um
caso simples, no qual Create( ) não utiliza parâmetros.
Ao contrário do que acontece com o C++, os objetos construtores no Object Pascal não são chama-
dos automaticamente, cabendo ao programador chamar o construtor do objeto. Veja a seguir a sintaxe
para se chamar um construtor:
FooObject := TFooObject.Create;
Observe que a sintaxe para uma chamada de construtor é um pouco singular. Você está se referindo
ao método Create( ) do objeto pelo tipo, e não pela instância, como faria com outros métodos. Isso pode
parecer estranho a princípio, mas tem sentido. FooObject, uma variável, é indefinida na hora de chamar,
mas o código para TFooObject, um tipo, está estático na memória. Por esse motivo, uma chamada estática
para o método Create( ) é totalmente válida.
O ato de chamar um construtor para criar uma instância de um objeto normalmente é chamado de
instanciação.
NOTA
Quando uma instância de objeto é criada usando o construtor, o compilador garante que todos os campos
do objeto serão inicializados. Você pode presumir com segurança que todos os números serão inicializa-
dos como 0, todos os ponteiros como Nil e todas as strings estarão vazias.
77
Destruição
Quando você termina de usar um objeto, deve desalocar a instância chamando seu método Free( ). O
método Free( ) primeiro verifica se a instância do objeto não é Nil e em seguida chama o método destrui-
dor do objeto, Destroy( ). O destruidor, é claro, é o contrário do construtor; ele desaloca qualquer me-
mória alocada e executa todo o trabalho de manutenção necessário para que o objeto seja devidamente
removido da memória. A sintaxe é simples:
FooObject.Free;
Em vez de chamar Create( ), a instância do objeto é usada para chamar o método Free( ). Lembre-se
de jamais chamar Destroy( ) diretamente, mas, em vez disso, chamar o método Free( ), que é mais seguro.
ATENÇÃO
No C++, o destruidor de um objeto declarado estaticamente é chamado automaticamente quando seu
objeto sai do escopo, mas você deve chamar o destruidor para qualquer objeto alocado dinamicamente. A
regra é a mesma no Object Pascal, mas, como todos os objetos são implicitamente dinâmicos no Object
Pascal, você deve seguir a regra geral segundo a qual tudo o que é criado deve ser liberado. Entretanto,
existem algumas exceções importantes a essa regra. A primeira é que quando seu objeto é possuído por ou-
tros objetos (como descrito no Capítulo 20), ele será libertado para você. A segunda são objetos com con-
tagem de referência (como os que descendem de TInterfacedObject ou TComObject), que são destruídos
quando a última referência é liberada.
Você deve estar se perguntando como todos esses métodos cabem no seu pequeno objeto. Certa-
mente você não os declarou, certo? Os métodos discutidos na verdade vêm do objeto básico do Object
Pascal, TObject. No Object Pascal, todos os objetos sempre são descendentes de TObject, independente-
mente de serem declarados como tal. Portanto, a declaração
Type TFoo = Class;
é equivalente à declaração
Métodos
Métodos são procedimentos e funções pertencentes a um dado objeto. Os métodos determinam o com-
portamento do objeto. Dois métodos importantes dos objetos que você cria são os métodos construtor e
destruidor, que acabamos de discutir. Você também pode criar métodos personalizados em seus objetos
para executar uma série de tarefas.
A criação de um método é um processo que se dá em duas etapas. Primeiro você deve declarar o mé-
todo na declaração de tipo do objeto e em seguida deve definir o método no código. O código a seguir
demonstra o processo de declaração e definição de um método:
type
TBoogieNights = class
Dance: Boolean;
procedure DoTheHustle;
end;
procedure TBoogieNights.DoTheHustle;
begin
Dance := True;
end;
78
Observe que, ao definir o corpo do método, você tem que usar o nome plenamente qualificado,
como quando definiu o método DoTheHustle. Também é importante observar que o campo Dance do objeto
pode ser acessado diretamente de dentro do método.
Tipos de métodos
Os métodos de objeto podem ser declarados como static, virtual, dynamic ou message. Considere o seguin-
te exemplo de objeto:
TFoo = class
procedure IAmAStatic;
procedure IAmAVirtual; virtual;
procedure IAmADynamic; dynamic;
procedure IAmAMessage(var M: TMessage); message wm_SomeMessage;
end;
Métodos estáticos
IAmAStatic é um método estático. O método estático é o tipo de método default e funciona de forma se-
melhante à chamada de procedimento ou função normal. O compilador conhece o endereço desses mé-
todos e, portanto, quando você chama um método estático, ele é capaz de vincular essa informação no
executável estaticamente. Os métodos estáticos são executados com mais rapidez; entretanto, eles não
têm a capacidade de serem modificados de modo a fornecer polimorfismo.
Métodos virtuais
IAmAVirtual é um método virtual. Os métodos virtuais são chamados da mesma forma que os métodos es-
táticos, mas, como os métodos virtuais podem ser modificados, o compilador não sabe o endereço de
uma função virtual em particular quando você a chama em seu código. O compilador, por esse motivo,
constrói uma Virtual Method Table (VMT), que fornece um meio para pesquisar endereços de função em
runtime. Todos os métodos virtuais chamados são disparados em runtime através da VMT. A VMT de
um objeto contém todos os métodos virtuais dos seus ancestrais, bem como os que declara; por essa ra-
zão, os métodos virtuais usam mais memória do que os métodos dinâmicos, embora sejam executados
com mais rapidez.
Métodos dinâmicos
IAmADynamic é um método dinâmico. Os métodos dinâmicos são basicamente métodos virtuais com um sis-
tema de despacho diferente. O compilador atribui um número exclusivo a cada método dinâmico e usa
esses números, juntamente com os endereços do método, para construir uma Dynamic Method Table
(DMT). Ao contrário da VMT, a DMT de um objeto contém apenas os métodos dinâmicos que declara, e
esse método depende da DMT de seu ancestral para o restante de seus métodos dinâmicos. Por isso, os
métodos dinâmicos fazem uso menos intensivo da memória do que os métodos virtuais, mas eles são
mais demorados para se chamar, pois você pode ter que propagar através de várias DMTs ancestrais an-
tes de encontrar o endereço de um método dinâmico em particular.
Métodos de mensagem
IAmAMessage é um método de manipulação de mensagem. O valor depois da palavra-chave message determi-
na a mensagem à qual o método responderá. Os métodos de mensagem são usados para criar uma respos-
ta automática para as mensagens do Windows e geralmente você não as pode chamar diretamente. A ma-
nipulação de mensagem é discutida em detalhes no Capítulo 5.
79
Modificando métodos
A modificação (overriding) de um método é a implementação do Object Pascal do conceito de polimor-
fismo da OOP. Ela permite que você altere o comportamento de um método de descendente para des-
cendente. Os métodos do Object Pascal podem ser modificados somente se primeiro forem declarados
como virtual ou dynamic. Para modificar um método, use a diretiva override em vez de virtual ou dynamic
no tipo do seu objeto descendente. Por exemplo, você pode modificar os métodos IAmAVirtual e IAmADyna-
mic da seguinte maneira:
TFooChild = class(TFoo)
procedure IAmAVirtual; override;
procedure IAmADynamic; override;
procedure IAmAMessage(var M: TMessage); message wm_SomeMessage;
end;
A diretiva override substitui a entrada do método original na VMT pelo novo método. Se você rede-
clarasse IAmAVirtual e IAmADynamic com a palavra-chave virtual ou dynamic, e não override, teria criado novos
métodos em vez de modificar os métodos ancestrais. Além disso, se você tentar modificar um método
estático em um tipo descendente, o método estático no novo objeto substituirá completamente o método
no tipo ancestral.
Overloading de métodos
Como os procedimentos e as funções normais, os métodos podem ter overloading de modo que uma
classe possa conter vários métodos de mesmo nome com diferentes listas de parâmetros. Os métodos de
overloading devem ser marcados com a diretiva overload, embora, em uma hierarquia de classe, seja opcio-
nal o uso da diretiva na primeira instância do nome de um método. O exemplo de código a seguir mostra
uma classe contendo três métodos de overloading:
type
TSomeClass = class
procedure AMethod(I: Integer); overload;
procedure AMethod(S: string); overload;
procedure AMethod(D: Double); overload;
end;
TSomeClass = class
procedure Cooper; reintroduce;
end;
80
Self
Uma variável implícita chamada Self está disponível dentro de todos os métodos de objeto. Self é um
ponteiro para a instância de classe que foi usada para chamar o método. Self é passado pelo compilador
como um parâmetro oculto para todos os métodos.
Propriedades
Talvez ajude pensar nas propriedades como campos de acesso especiais que permitem que você modifi-
que dados e execute o código contido na sua classe. Para os componentes, propriedades são as coisas que
aparecem na janela Object Inspector quando publicadas. O exemplo a seguir ilustra um Object simplifica-
do com uma propriedade:
TMyObject = class
private
SomeValue: Integer;
procedure SetSomeValue(AValue: Integer);
public
property Value: Integer read SomeValue write SetSomeValue;
end;
procedure TMyObject.SetSomeValue(AValue: Integer);
begin
if SomeValue < > AValue then
SomeValue := AValue;
end;
TMyObject é um objeto que contém o seguinte: um campo (um inteiro chamado SomeValue), um méto-
do (um procedimento chamado SetSomeValue) e uma propriedade chamada Value. O propósito do procedi-
mento SetSomeValue é definir o valor do campo SomeValue. A propriedade Value na verdade não contém
dado algum. Value é um acesso para o campo SomeValue; quando você pergunta a Value qual o número que
ele contém, é lido o valor de SomeValue. Quando você tenta definir o valor da propriedade Value, Value cha-
ma SetSomeValue para modificar o valor de SomeValue. Isso é útil por duas razões: primeiro permite que você
apresente aos usuários da classe uma variável simples sem que eles tenham que se preocupar com os deta-
lhes da implementação da classe. Segundo, você pode permitir que os usuários modifiquem os métodos
de acesso em classes descendentes por um comportamento polimórfico.
Especificadores de visibilidade
O Object Pascal oferece mais controle sobre o comportamento de seus objetos, permitindo que você de-
clare os campos e os métodos com diretivas como protected, private, public, published e automated. A sintaxe
para usar essas palavras-chave é a seguinte:
TSomeObject = class
private
APrivateVariable: Integer;
AnotherPrivateVariable: Boolean;
protected
procedure AProtectedProcedure;
function ProtectMe: Byte;
public
constructor APublicContructor;
destructor APublicKiller;
published
property AProperty read APrivateVariable write APrivateVariable;
end;
81
Você pode colocar tantos campos ou métodos quantos desejar abaixo de cada diretiva. O estilo de-
termina que você deve recuar o especificador da mesma maneira que o faz com o nome da classe. Essas
diretivas têm o seguinte significado:
l private.Essas partes de seu objeto são acessíveis apenas para o código na mesma unidade que a
implementação do seu objeto. Use esta diretiva para ocultar detalhes de implementação de seus
objetos dos usuários e para impedi-los de modificar membros que possam afetar seu objeto.
l protected. Os membros protected do seu objeto podem ser acessados por descendentes do seu ob-
jeto. Essa capacidade permite que você oculte os detalhes de implementação do seu objeto dos
usuários ao mesmo tempo que fornece flexibilidade máxima para descendentes do objeto.
l public. Esses campos e métodos são acessíveis de qualquer lugar do seu programa. Construtores e
destruidores de objeto devem ser sempre public.
l published.Runtime Type Information (RTTI) a ser gerada para a parte publicada de seus objetos
permite que outras partes de sua aplicação obtenham informações sobre as partes publicadas do
seu objeto. O Object Inspector usa a RTTI para construir sua lista de propriedades.
l automated.
O especificador automated é obsoleto mas permanece para manter a compatibilidade
com o Delphi 2. O Capítulo 23 tem mais detalhes sobre isso.
O código a seguir se destina à classe TMyObject que foi introduzida anteriormente, com a inclusão de
diretivas para melhorar a integridade do objeto:
TMyObject = class
private
SomeValue: Integer;
procedure SetSomeValue(AValue: Integer);
published
property Value: Integer read SomeValue write SetSomeValue;
end;
Agora, os usuários de seu objeto não poderão modificar o valor de SomeValue diretamente e terão que
percorrer a interface fornecida pela propriedade Value para modificar os dados do objeto.
Classes “amigas”
A linguagem C++ possui um conceito de classes amigas (ou seja, classes que têm permissão de acessar os
dados privados e as funções em outras classes). Isso é obtido no C++ usando a palavra-chave friend.
Embora, estritamente falando, o Object Pascal não tenha uma palavra-chave semelhante, ele oferece uma
funcionalidade semelhante. Todos os objetos declarados dentro da mesma unidade são considerados
“amigos” e têm acesso a informações privadas localizadas nos outros objetos dessa unidade.
Objetos internos
Todas as instâncias de classe no Object Pascal são na verdade armazenadas como ponteiros de 32 bits
para os dados da instância de classe localizados na memória heap. Quando você acessa campos, métodos
ou propriedades dentro de uma classe, o compilador automaticamente executa um pequeno truque que
gera o código para desreferenciar esse ponteiro para você. Portanto, para o olho desacostumado, uma
classe aparece como uma variável estática. Entretanto, isso significa que, ao contrário do C++, o Object
Pascal não oferece outro meio razoável para alocar uma classe de um segmento de dados da aplicação
82 que não seja o heap.
TObject: a mãe de todos os objetos
Como tudo descende de TObject, todas as classes possuem alguns métodos herdados de TObject e você
pode fazer algumas deduções especiais sobre as capacidades de um objeto. Todas as classes têm a capaci-
dade de, por exemplo, dizer-lhe seu nome, tipo ou se é herdada de uma classe em particular. O melhor
disso é que você, como um programador de aplicações, não tem que se preocupar com a mágica por meio
da qual o compilador faz com que isso aconteça. Você pode se dar o luxo de usar e abusar da funcionali-
dade que ele oferece!
TObject é um objeto especial porque sua definição vem da unidade System, e o compilador do Object
Pascal está “ciente” do TObject. O código a seguir ilustra a definição da classe TObject:
type
TObject = class
constructor Create;
procedure Free;
class function InitInstance(Instance: Pointer): TObject;
procedure CleanupInstance;
function ClassType: TClass;
class function ClassName: ShortString;
class function ClassNameIs(const Name: string): Boolean;
class function ClassParent: TClass;
class function ClassInfo: Pointer;
class function InstanceSize: Longint;
class function InheritsFrom(AClass: TClass): Boolean;
class function MethodAddress(const Name: ShortString): Pointer;
class function MethodName(Address: Pointer): ShortString;
function FieldAddress(const Name: ShortString): Pointer;
function GetInterface(const IID: TGUID; out Obj): Boolean;
class function GetInterfaceEntry(const IID: TGUID): PInterfaceEntry;
class function GetInterfaceTable: PInterfaceTable;
function SafeCallException(ExceptObject: TObject;
ExceptAddr: Pointer): HResult; virtual;
procedure AfterConstruction; virtual;
procedure BeforeDestruction; virtual;
procedure Dispatch(var Message); virtual;
procedure DefaultHandler(var Message); virtual;
class function NewInstance: TObject; virtual;
procedure FreeInstance; virtual;
destructor Destroy; virtual;
end;
Você encontrará cada um desses métodos documentados no sistema de ajuda on-line do Delphi.
Em particular, observe os métodos que são precedidos pela palavra-chave class. A inclusão da pala-
vra-chave class a um método permite que ele seja chamado como um procedimento ou função normal
sem de fato ter uma instância da classe da qual o método é um membro. Essa é uma excelente funcionali-
dade que foi emprestada das funções static do C++. Porem, tenha cuidado para não fazer uma classe
depender de qualquer informação da instância; caso contrário, você receberá um erro do compilador.
Interfaces
Talvez o acréscimo mais significativo da linguagem Object Pascal no passado recente tenha sido o supor-
te nativo para interfaces, que foi introduzido no Delphi 3. Trocando em miúdos, uma interface define
um conjunto de funções e procedimentos que pode ser usado para interagir com um objeto. A definição
de uma dada interface é conhecida tanto pelo implementador quanto pelo cliente da interface – agindo
como uma espécie de contrato por meio do qual uma interface será definida e usada. Uma classe pode 83
implementar várias interfaces, fornecendo várias “caras” conhecidas, por meio das quais um cliente pode
controlar um objeto.
Como o nome sugere, uma interface define apenas, bem, uma interface pela qual o objeto e os clien-
tes se comunicam. Esse é um conceito semelhante ao da classe PURE VIRTUAL do C++. Cabe a uma classe
que suporta uma interface implementar cada uma das funções e procedimentos da interface.
Neste capítulo você aprenderá sobre os elementos de linguagem de interfaces. Para obter informa-
ções sobre o uso de interfaces dentro de suas aplicações, consulte o Capítulo 23.
Definindo Interfaces
Como todas as classes do Delphi descendem implicitamente de TObject, todas as interfaces são implici-
tamente derivadas de uma interface chamada IUnknown. IUnknown é definida na unidade System da seguinte
maneira:
type
IUnknown = interface
[‘{00000000-0000-0000-C000-000000000046}’]
function QueryInterface(const IID: TGUID; out Obj): Integer; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
end;
Como você pode ver, a sintaxe para definir uma interface é muito parecida com a de uma classe. A
principal diferença é que uma interface pode opcionalmente ser associada a um GUID (globally unique
identifier, ou identificador globalmente exclusivo), exclusivo da interface. A definição de IUnknown vem da
especificação do Component Object Model (COM) fornecido pela Microsoft. Isso também é descrito
com mais detalhes no Capítulo 23.
A definição de uma interface personalizada é um processo objetivo para quem compreende como se
criam as classes do Delphi. O código a seguir define uma nova interface chamada IFoo, que implementa
um método chamado F1( ):
type
IFoo = interface
[‘{2137BF60-AA33-11D0-A9BF-9A4537A42701}’]
function F1: Integer;
end;
DICA
O IDE do Delphi produzirá novos GUIDs para suas interfaces quando você usar a combinação de teclas
Ctrl+Shift+G.
O código a seguir define uma nova interface, IBar, que descende de IFoo:
type
IBar = interface(IFoo)
[‘{2137BF61-AA33-11D0-A9BF-9A4537A42701}’]
function F2: Integer;
end;
Implementando Interfaces
O pequeno código a seguir demonstra como implementar IFoo e IBar na classe chamada TFooBar:
84
type
TFooBar = class(TInterfacedObject, IFoo, IBar)
function F1: Integer;
function F2: Integer;
end;
function TFooBar.F1: Integer;
begin
Result := 0;
end;
Observe que várias interfaces podem ser listadas depois da classe ancestral na primeira linha da
declaração de classe para implementar várias interfaces. A união de uma função de interface a uma de-
terminada função na classe acontece quando o compilador combina uma assinatura de método na in-
terface com uma assinatura correspondente na classe. Um erro do compilador ocorrerá se uma classe
declarar que implementa uma interface, mas a classe não conseguir implementar um ou mais métodos
da interface.
Se uma classe implementa várias interfaces cujos métodos têm a mesma assinatura, você deve atri-
buir um alias para os métodos que têm o mesmo nome, como mostra o pequeno exemplo a seguir:
type
IFoo = interface
[‘{2137BF60-AA33-11D0-A9BF-9A4537A42701}’]
function F1: Integer;
end;
IBar = interface
[‘{2137BF61-AA33-11D0-A9BF-9A4537A42701}’]
function F1: Integer;
end;
TFooBar = class(TInterfacedObject, IFoo, IBar)
// métodos com aliases
function IFoo.F1 = FooF1;
function IBar.F1 = BarF1;
// métodos da interface
function FooF1: Integer;
function BarF1: Integer;
end;
function TFooBar.FooF1: Integer;
begin
Result := 0;
end;
function TFooBar.BarF1: Integer;
begin
Result := 0;
end;
A diretiva implements
O Delphi 4 introduziu a diretiva implements, que lhe permite delegar a implementação dos métodos
de interface para outra classe ou interface. Essa técnica é muitas vezes chamada de implementação por
delegação. Implements é usada como a última diretiva em uma propriedade de classe ou tipo de interface,
como se pode ver no exemplo a seguir: 85
type
TSomeClass = class(TInterfacedObject, IFoo)
// dados
function GetFoo: TFoo;
property Foo: TFoo read GetFoo implements IFoo;
// dados
end;
Usando interfaces
Algumas regras de linguagem importantes se aplicam quando você está usando variáveis de tipos de in-
terface em suas aplicações. A principal regra a ser lembrada é que uma interface é um tipo permanente-
mente gerenciado. Isso significa que ela é sempre inicializada como nil, tem contagem de referência, uma
referência é automaticamente adicionada quando você obtém uma interface e ela é automaticamente li-
berada quando sai do escopo ou recebe o valor nil. O exemplo de código a seguir ilustra o gerenciamento
permanente de uma variável de interface:
var
I: ISomeInterface;
begin
// I é iniciallizado como nil
I := FunctionReturningAnInterface; // cont. ref. I é incrementado
I.SomeFunc;
// contador de ref. é incrementado. Se 0, I é automaticamente liberado
end;
Outra regra exclusiva de variáveis de interface é que uma interface é uma atribuição compatível
com classes que implementam a interface. Por exemplo, o código a seguir é válido usando a classe TFooBar
definida anteriormente:
procedure Test(FB: TFooBar)
var
F: IFoo;
begin
F := FB; // válido porque FB aceita IFoo
.
.
.
86
Finalmente, o operador de typecast as pode ser usado para que uma determinada variável de inter-
face faça uma QueryInterface com outra interface (isso é explicado com maiores detalhes no Capítulo 23).
Isso é ilustrado aqui:
var
FB: TFooBar;
F: IFoo;
B: IBar;
begin
FB := TFooBar.Create
F := FB; // válido porque FB aceita IFoo
B := F as IBar; // QueryInterface F para IBar
.
.
.
Program FileIO;
uses Classes, Dialogs;
{$APPTYPE CONSOLE}
var
F: TextFile;
S: string;
begin
AssignFile(F, ‘FOO.TXT’);
try
Reset(F);
try
ReadLn(F, S);
finally
CloseFile(F);
end;
except
on EInOutError do
ShowMessage(‘Error Accessing File!’);
end;
end.
87
Na Listagem 2.3, o bloco try..finally é usado para garantir que o arquivo seja fechado independen-
temente de qualquer exceção. Esse bloco poderia ser traduzido da seguinte maneira para os mortais: “Ei,
programa, tente executar as instruções entre o try e o finally. Quando terminar, ou no caso de tropeçar
em alguma exceção, execute as instruções entre o finally e o end. Se ocorrer uma exceção, vá para o pró-
ximo bloco de tratamento de exceção.” Isso significa que o arquivo será fechado e o erro poderá ser devi-
damente manipulado, independentemente do erro que ocorrer.
NOTA
As instruções depois do bloco try..finally são executadas independentemente da ocorrência de uma ex-
ceção. Certifique-se de que o código em seu bloco finally não presume que uma exceção tenha ocorrido.
Além disso, como a instrução finally não interrompe a migração de uma exceção, o fluxo da execução do
seu programa se deslocará para o próximo manipulador de exceção.
O bloco externo try..except é usado para manipular as exceções à medida que elas ocorram no pro-
grama. Depois que o arquivo é fechado no bloco finally, o bloco except produz uma mensagem infor-
mando ao usuário que ocorreu um erro de I/O.
Uma das grandes vantagens que o tratamento de exceção fornece sobre o método tradicional de tra-
tamento de erros é a capacidade de separar com nitidez o código de detecção de erro do código de corre-
ção de erro. Isso é bom principalmente porque torna seu código mais fácil de se ler e manter, permitindo
que você se concentre em um determinado aspecto do código de cada vez.
É fundamental o fato de você não poder interceptar qualquer exceção usando o bloco try..finally.
Quando você usa um bloco try..finally no código, isso significa que você não precisa se preocupar com
as exceções que possam ocorrer. Você só quer executar algumas tarefas quando elas ocorrerem para sair
da situação de forma ordenada. O bloco finally é um lugar ideal para liberar recursos que você tenha alo-
cado (como arquivos ou recursos do Windows), pois eles sempre serão executados no caso de um erro.
Em muitos casos, entretanto, você precisa de algum tipo de tratamento de erro que seja capaz de respon-
der diferentemente dependendo do tipo de erro que ocorre. Você pode interceptar exceções específicas
usando um bloco try..except, que mais uma vez é ilustrado na Listagem 2.4.
Program HandleIt;
{$APPTYPE CONSOLE}
var
R1, R2: Double;
begin
while True do begin
try
Write(‘Enter a real number: ‘);
ReadLn(R1);
Write(‘Enter another real number: ‘);
ReadLn(R2);
Writeln(‘I will now divide the first number by the second...’);
Writeln(‘The answer is: ‘, (R1 / R2):5:2);
except
On EZeroDivide do
Writeln(‘You cannot divide by zero!’);
On EInOutError do
Writeln(‘That is not a valid number!’);
end;
end;
end.
88
Embora você possa interceptar exceções específicas com o bloco try..except, você também pode
capturar outras exceções adicionando a cláusula else a essa construção. Veja a seguir a sintaxe da cons-
trução try..except:
try
Instruções
except
On ESomeException do Something;
else
{ realiza algum tratamento de exceção default }
end;
ATENÇÃO
Ao usar a construção try..except..else, você deve estar consciente de que a parte else vai capturar todas
as exceções – inclusive as exceções inesperadas, como falta de memória ou outras exceções da biblioteca
de runtime. Tenha cuidado ao usar a cláusula else e só o faça com cautela. Você sempre deve reproduzir
uma exceção quando interceptar manipuladores de exceção não-qualificados. Isso é explicado na seção
“Recriando uma exceção”.
Você pode obter o mesmo efeito de uma construção try..except..else não especificando a classe de
exceção em um bloco try..except, como mostramos neste exemplo:
try
Instruções
except
HandleException // quase igual à instrução else
end;
Classes de exceção
Exceções não passam de instâncias de objetos especiais. Esses objetos são instanciados quando uma exce-
ção ocorre e são destruídos quando uma exceção é manipulada. O objeto básico da exceção é denomina-
do Exception, que é definido da seguinte maneira:
type
Exception = class(TObject)
private
FMessage: string;
FHelpContext: Integer;
public
constructor Create(const Msg: string);
constructor CreateFmt(const Msg: string; const Args: array of const);
constructor CreateRes(Ident: Integer); overload;
constructor CreateRes(ResStringRec: PResStringRec); overload;
constructor CreateResFmt(Ident: Integer; const Args: array of const); overload;
constructor CreateResFmt(ResStringRec: PResStringRec; const Args: array of const); overload;
constructor CreateHelp(const Msg: string; AHelpContext: Integer);
constructor CreateFmtHelp(const Msg: string; const Args: array of const;
AHelpContext: Integer);
constructor CreateResHelp(Ident: Integer; AHelpContext: Integer); overload;
constructor CreateResHelp(ResStringRec: PResStringRec; AHelpContext: Integer); overload;
constructor CreateResFmtHelp(ResStringRec: PResStringRec; const Args: array of const;
AHelpContext: Integer); overload;
89
constructor CreateResFmtHelp(Ident: Integer; const Args: array of const;
AHelpContext: Integer); overload;
property HelpContext: Integer read FHelpContext write FHelpContext;
property Message: string read FMessage write FMessage;
end;
O elemento importante do objeto Exception é a propriedade Message, uma string. Message fornece
mais informações ou explicações sobre a exceção. As informações fornecidas por Message dependem do
tipo de exceção produzida.
ATENÇÃO
Se você define seu próprio objeto de exceção, certifique-se de que vai derivá-lo de um objeto de exceção
conhecido, como Exception, ou de um de seus descendentes. A razão para isso é que os manipuladores de
exceção genéricos serão capazes de interceptar sua exceção.
Quando você manipula um tipo específico de exceção em um bloco except, esse manipulador tam-
bém capturará qualquer exceção que seja descendente da exceção especificada. Por exemplo, EMathError é
o objeto ancestral de uma série de exceções relacionadas a cálculos, como EZeroDivide e EOverflow. Você
pode capturar qualquer uma dessas exceções configurando um manipulador para EMathError, como mos-
tramos a seguir:
try
Instruções
except
on EMathError do // capturará EMathError ou qualquer descendente
HandleException
end;
Qualquer exceção que você não manipule explicitamente em seu programa mais cedo ou mais tarde
fluirá e será manipulada pelo manipulador default, localizado dentro da biblioteca de runtime do Delphi.
O manipulador default exibirá uma caixa de diálogo de mensagem informando ao usuário que ocorreu
uma exceção. A propósito, o Capítulo 4 mostrará um exemplo de como se modifica o tratamento de ex-
ceção default.
Durante o tratamento de uma exceção, algumas vezes você precisa acessar a instância do objeto de
exceção para recuperar mais informações sobre a exceção, como a que foi fornecida pela propriedade
Message. Há duas formas de se fazer isso: usar um identificador opcional com a construção ESomeException
ou usar a função ExceptObject( ).
Você pode inserir um identificador opcional na parte ESomeException de um bloco except e fazer o
identificador ser mapeado para uma instância da exceção atualmente produzida. A sintaxe para isso é co-
locar um identificador e dois-pontos antes do tipo de exceção, como no exemplo a seguir:
try
Alguma coisa
except
on E:ESomeException do
ShowMessage(E.Message);
end;
Nesse caso, o identificador (no caso, E) se torna a instância da exceção atualmente produzida. Esse
identificador é sempre do mesmo tipo que a exceção que ele precede.
Você também pode usar a função ExceptObject( ), que retorna uma instância da exceção atualmente
produzida. O inconveniente de ExceptObject( ), entretanto, é que ela retorna um TObject no qual em se-
guida você fará um typecast para o objeto de exceção à sua escolha. O exemplo a seguir mostra o uso des-
90 sa função:
try
Alguma coisa
except
on ESomeException do
ShowMessage(ESomeException(ExceptObject).Message);
end;
Fluxo de execução
Depois que uma exceção é produzida, o fluxo de execução do seu programa se propaga até o próximo
manipulador de exceção, onde a instância de exceção é finalmente manipulada e destruída. Esse proces-
so é determinado pela pilha de chamadas e, portanto, abrange todo o programa (não se limitando a um
procedimento ou unidade). A Listagem 2.5 ilustra o fluxo de execução de um programa quando uma ex-
ceção é produzida. Essa listagem é a unidade principal de uma aplicação em Delphi que consiste em um
formulário com um botão incluído. Quando damos um clique no botão, o método Button1Click( ) chama
Proc1( ), que chama Proc2( ), que por sua vez chama Proc3( ). Uma exceção é produzida em Proc3( ) e
você pode presenciar o fluxo da execução se propagando através de cada bloco try..finally até a exceção
ser finalmente manipulada dentro de Button1Click( ).
DICA
Quando você executa esse programa a partir do IDE do Delphi, pode ver melhor o fluxo de execução desa-
tivar o tratamento de exceções do depurador integrado, desmarcando Stop on Delphi Exceptions (parar nas
exceções do Delphi) a partir de Tools, Debugger Options, Language Exceptions (ferramentas, opções do
depurador, exceções da linguagem).
unit Main;
interface
uses
SysUtils, Windows, Messages, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Declarações privadas }
public
{ Declarações públicas }
end;
var
Form1: TForm1; 91
Listagem 2.5 Continuação
implementation
{$R *.DFM}
type
EBadStuff = class(Exception);
procedure Proc3;
begin
try
raise EBadStuff.Create(‘Up the stack we go!’);
finally
ShowMessage(‘Exception raised. Proc3 sees the exception’);
end;
end;
procedure Proc2;
begin
try
Proc3;
finally
ShowMessage(‘Proc2 sees the exception’);
end;
end;
procedure Proc1;
begin
try
Proc2;
finally
ShowMessage(‘Proc1 sees the exception’);
end;
end;
end.
92
Recriando uma exceção
Quando você precisa realizar algum tratamento especial para uma instrução dentro de um bloco try..except
existente e também permitir que a exceção flua para o manipulador default fora do bloco, pode usar uma téc-
nica chamada recriação da exceção. A Listagem 2.6 demonstra um exemplo de recriação de uma exceção.
O Object Pascal fornece dois operadores, is e as, que permitem comparações e typecast de objetos
via RTTI.
93
A palavra-chave as é uma nova forma de typecast seguro. Isso permite que você difunda um objeto
de baixo nível para um descendente e produza uma exceção caso o typecast seja inválido. Suponha que
você tenha um procedimento para o qual deseja ser capaz de passar qualquer tipo de objeto. Essa defini-
ção de função poderia ser feita da seguinte forma:
Procedure Foo(AnObject: TObject);
Se você deseja fazer alguma coisa útil com AnObject posteriormente nesse procedimento, provavel-
mente terá que difundi-lo para um objeto descendente. Suponha que você deseje partir do princípio de
que AnObject é um descendente de TEdit e deseja alterar o texto que ele contém (um TEdit é um controle de
edição da VCL do Delphi). Você pode usar o seguinte código:
(Foo as TEdit).Text := ‘Hello World.’;
Você pode usar o operador de comparação booleana is para verificar se os tipos de dois objetos são
compatíveis. Use o operador is para comparar um objeto desconhecido com um tipo ou instância para
determinar as propriedades e o comportamento que você pode presumir sobre o objeto desconhecido.
Por exemplo, você pode verificar se AnObject é compatível em termos de ponteiro com TEdit antes de ten-
tar fazer um typecast com ele:
If (Foo is TEdit) then
TEdit(Foo).Text := ‘Hello World.’;
Observe que você não usou o operador as para executar typecast nesse exemplo. Isso é porque uma
certa quantidade de overhead é envolvida no uso da RTTI e, como a primeira linha já determinou que Foo
é um TEdit, você pode otimizar executando um typecast de ponteiro na segunda linha.
Resumo
Este capítulo discutiu uma série de aspectos da linguagem Object Pascal. Você aprendeu os fundamentos
da sintaxe e da semântica da linguagem, incluindo variáveis, operadores, funções, procedimentos, tipos,
construções e estilo. Você também pôde entender melhor sobre OOP, objetos, campos, propriedades,
métodos, TObject, interfaces, tratamento de exceção e RTTI.
Agora, com uma compreensão geral de como funciona a linguagem orientada a objetos do Object
Pascal do Delphi, você está pronto para participar de discussões mais avançadas, como a API do Win32 e
a Visual Component Library.
94
A API do Win32 CAPÍTULO
3
NE STE C AP ÍT UL O
l Objetos – antes e agora 96
l Multitarefa e multithreading 99
l Gerenciamento de memória no Win32 100
l Tratamento de erros no Win32 102
l Resumo 103
Este capítulo fornece uma introdução à API do Win32 e ao sistema Win32 em geral. O capítulo discute
as capacidades do sistema Win32 e ainda destaca algumas diferenças básicas em relação a vários aspec-
tos da implementação de 16 bits. O propósito deste capítulo não é documentar a totalidade do sistema,
mas apenas oferecer uma idéia básica de como ele opera. Tendo uma compreensão básica da operação
do Win32, você será capaz de usar aspectos avançados oferecidos pelo sistema Win32, sempre que for
preciso.
Objetos do kernel
Os objetos do kernel são nativos do sistema Win32 e incluem eventos, mapeamentos de arquivo, arqui-
vos, mailslots, mutexes, pipes, processos, semáforos e threads. A API do Win32 inclui várias funções es-
pecíficas a cada objeto do kernel. Antes de discutirmos sobre os objetos do kernel em geral, queremos
discutir sobre os processos que são essenciais para se entender como são gerenciados os objetos no ambi-
ente Win32.
Processos e threads
Um processo pode ser considerado como uma aplicação em execução ou uma instância de aplicação. Por-
tanto, vários processos podem estar ativos ao mesmo tempo no ambiente Win32. Cada processo recebe
seu próprio espaço de endereços de 4GB para seu código e dados. Dentro desse espaço de endereços de
4GB, existem quaisquer alocações de memória, threads, mapeamentos de arquivo e outros. Além disso,
quaisquer bibliotecas de vínculo dinâmico (DLLs) carregadas por um processo são carregadas no espaço
de endereços do processo. Falaremos mais sobre o gerenciamento de memória do sistema Win32 mais
adiante neste capítulo, na seção “Gerenciamento de memória no Win32”.
Processos são inertes. Em outras palavras, eles não executam coisa alguma. Pelo contrário, cada
processo toma um thread primário que executa o código dentro do contexto do processo que contém
este thread. Um processo pode conter diversos threads. Entretanto, possui apenas um thread principal
ou primário.
NOTA
Um thread é um objeto do sistema operacional que representa um caminho de execução de código dentro
de um determinado processo. Toda aplicação do Win32 tem pelo menos um thread – sempre chamado de
thread primário ou thread default – porém, as aplicações estão livres para criar outros threads para realizar
outras tarefas. Threads são tratados com mais detalhes no Capítulo 11.
Quando se cria um processo, o sistema cria o thread principal para ele. Esse thread pode então criar
threads adicionais, se necessário. O sistema Win32 aloca tempo de CPU, chamado fatias de tempo, para
os threads do processo.
A Tabela 3.1 mostra algumas funções de processo comuns da API do Win32.
96
Tabela 3.1 Funções de processo
Função Finalidade
CreateProcess( ) Cria um novo processo e seu thread primário. Essa função substitui a função
WinExec( ) usada no Windows 3.11.
ExitProcess( ) Sai do processo corrente, terminando o processo e todos os threads
relacionados àquele processo.
GetCurrentProcess( ) Retorna uma pseudo-alça do processo atual. Uma pseudo-alça é uma alça
especial que pode ser interpretada como a alça do processo corrente. Uma alça
real pode ser obtida por meio da função DuplicateHandle( ).
DuplicateHandle( ) Duplica a alça de um objeto do kernel.
GetCurrentProcessID( ) Restaura o código de ID do processo atual, que identifica exclusivamente o
processo através do sistema até que o processo tenha terminado.
GetExitCodeProcess( ) Restaura o status de saída de um processo específico.
GetPriorityClass( ) Restaura a categoria de um processo específico. Esse valor e os valores de cada
prioridade de thread no processo determinam o nível de prioridade básico para
cada thread.
GetStartupInfo( ) Restaura os conteúdos da estrutura TStartupInfo iniciada quando o processo
foi criado.
OpenProcess( ) Retorna uma alça de um processo existente, conforme especificada por um ID
de processo.
SetPriorityClass( ) Define a categoria de prioridade de um processo.
TerminateProcess( ) Termina um processo e encerra todos os threads associados a esse processo.
WaitForInputIdle( ) Espera até que o processo esteja esperando pela entrada do usuário.
Algumas funções da API do Win32 exigem uma alça de instância da aplicação, enquanto outras re-
querem uma alça de módulo. No Windows de 16 bits, havia uma distinção entre esses dois valores. Isso
não é verdade em relação ao Win32. Todo processo recebe sua própria alça de instância. Suas aplicações
do Delphi 5 podem se referir a essa alça de instância, acessando a variável global HInstance. Como HInstan-
ce e a alça de módulo da aplicação são os mesmos, você pode passar HInstance para as funções da API do
Win32 chamando por uma alça de módulo, tal como a função GetModuleFileName( ), que retorna um nome
de arquivo de um módulo específico. Veja o aviso a seguir, sobre quando a HInstance não se refere à alça
de módulo da aplicação atual.
ATENÇÃO
HInstance não será a alça de módulo da aplicação para o código que está sendo compilado em pacotes.
Use MainInstance para se referir sempre ao módulo host da aplicação e HInstance para se referir ao módu-
lo no qual reside o seu código.
Outra diferença entre o Win32 e o Windows de 16 bits tem a ver com a variável global HPrevInst. No
Windows de 16 bits, essa variável mantém a alça de uma instância previamente em execução na mesma
aplicação. Você poderia usar o valor para impedir a execução de instâncias múltiplas de sua aplicação.
Isso nunca funciona em Win32. Cada processo é executado dentro de seu próprio espaço de endereços
de 4GB e não pode reconhecer qualquer outro processo. Portanto, HPrevInst está sempre apontado para o
valor 0. Você deve usar outras técnicas para impedir a execução das instâncias múltiplas da sua aplicação,
como mostradas no Capítulo 13. 97
Tipos de objetos do kernel
Há diversos tipos de objetos do kernel. Quando um objeto do kernel é criado, ele existe no espaço de
endereços do processo, e esse processo pega uma alça para esse objeto. Essa alça não pode ser passada
para outro processo nem reutilizada pelo próximo processo para acessar o mesmo objeto do kernel.
No entanto, um segundo processo pode obter sua própria alça para um objeto do kernel já existente,
usando a função apropriada da API do Win32. Por exemplo, a função CreateMutex( ) da API do Win32
cria um objeto mutex, nomeado ou não, e retorna sua alça. A função OpenMutex( ) da API retorna a alça
para um objeto mutex nomeado já existente. OpenMutex( ) passa o nome do mutex cuja alça está sendo
solicitada.
NOTA
Objetos nomeados do kernel opcionalmente recebem um nome de string terminado em nulo quando cria-
dos com suas respectivas funções CreateXXXX( ). Esse nome está registrado no sistema Win32. Outros pro-
cessos podem acessar o mesmo objeto do kernel ao abri-lo, usando a função OpenXXXX( ) e passando o
nome do objeto especificado. Uma demonstração dessa técnica é usada no Capítulo 13, no qual explica-
mos como é possível impedir a execução de múltiplas instâncias.
Se você deseja compartilhar um mutex entre processos, pode fazer o primeiro processo criar o mu-
tex usando a função CreateMutex( ). Esse processo deve passar um nome que será associado a esse novo
mutex. Outros processos deverão usar a função OpenMutex( ), para a qual passam o mesmo nome do mu-
tex usado pelo primeiro processo. OpenMutex( ) retornará uma alça ao objeto mutex como nome indicado.
Diversas restrições de segurança podem ser impostas a outros processos, acessando objetos do kernel já
existentes. Tais restrições de segurança estão especificadas quando o mutex é inicialmente criado com
CreateMutex( ). Procure essas restrições na ajuda on-line, conforme se apliquem a cada objeto do kernel.
Como os processos múltiplos podem acessar objetos do kernel, os objetos do kernel são mantidos
por um contador de uso. Enquanto uma segunda aplicação acessa o objeto, o contador de uso é incre-
mentado. Quando terminar de usar o objeto, a aplicação chamará a função CloseHandle( ), que decremen-
ta o contador de uso do objeto.
ATENÇÃO
Embora seja possível que um processo possa chamar SelectObject( ) em uma alça de outro processo e
usar essa alça com sucesso, isso seria uma total coincidência. Objetos GDI possuem significados diferentes
em diferentes processos. Assim, você não deve praticar esse método.
O gerenciamento de alças da GDI acontece no subsistema GDI do Win32, que inclui a validação
dos objetos da GDI e a reciclagem de alças.
Os objetos User operam de modo semelhante aos objetos GDI, e são gerenciados pelo subsistema
User do Win32. No entanto, todas as tabelas de alças também são mantidas pelo User – não no espaço de
endereços do processo, como nas tabelas de alças da GDI. Portanto, objetos tais como janelas, classes de
janelas, átomos, e assim por diante, são compartilháveis entre processos.
Multitarefa e multithreading
Multitarefa é um termo usado para descrever a capacidade de um sistema operacional de executar simul-
taneamente múltiplas aplicações. O sistema faz isso emitindo “fatias” de tempo a cada aplicação. Nesse
sentido, multitarefa não é multitarefa a rigor, mas sim comutação de tarefa. Em outras palavras, o siste-
ma operacional não está realmente executando várias aplicações ao mesmo tempo. Pelo contrário, está
executando uma aplicação por um certo espaço de tempo e então alternando para outra aplicação e exe-
cutando-a por um certo espaço de tempo. Ela faz isso para cada aplicação. Para o usuário, parece como se
todas as aplicações estivessem sendo executadas simultaneamente, pois as fatias de tempo são muito pe-
quenas.
Esse conceito de multitarefa não é realmente um recurso novo no Windows, e já existia em versões
anteriores. A diferença básica entre a implementação de multitarefa do Win32 e a das versões anteriores
do Windows é que o Win32 usa a multitarefa preemptiva, enquanto as versões prévias usam a multitare-
fa não-preemptiva (o que significa que o sistema Windows não programa o tempo reservado para as apli-
cações com base no timer do sistema). As aplicações têm que dizer ao Windows que acabaram de proces-
sar o código antes que o Windows possa conceder tempo a outras aplicações. Isso é um problema, por-
que uma única aplicação pode travar o sistema com um processo demorado. Portanto, a menos que os
programadores da aplicação garantam que a aplicação abrirá mão do tempo para outras aplicações, po-
dem surgir problemas para o usuário.
No Win32, o sistema concede tempo de CPU para os threads de cada processo. O sistema Win32
gerencia o tempo alocado a cada thread com base nas prioridades dos threads. Esse conceito é discutido
com maiores detalhes no Capítulo 11.
99
NOTA
A implementação Windows NT/2000 do Win32 oferece a capacidade para realizar verdadeira multitarefa
em máquinas com múltiplos processadores. Sob essas condições, cada aplicação pode receber tempo no
seu próprio processador. Na verdade, cada thread individual pode receber tempo de CPU em qualquer
CPU disponível em máquinas de multiprocessadores.
Multithreading é a capacidade de uma aplicação realizar multitarefa dentro de si mesma. Isso signi-
fica que sua aplicação pode realizar simultaneamente diferentes tipos de processamentos. Um processo
pode ter diversos threads, e cada thread contém seu próprio código distinto para executar. Os threads
podem ter dependências um do outro e, portanto, devem ser sincronizados. Por exemplo, seria uma boa
idéia supor que um thread em particular terminará de processar seu código quando seu resultado tiver
que ser usado por outro thread. Técnicas de sincronismo de thread são usadas para coordenar a execução
de múltiplos threads. Os threads são discutidos com maiores detalhes no Capítulo 11.
As próximas seções discutem sobre o modelo de memória do Win32 e como o sistema Win32 lhe
permite manipular a memória.
Memória virtual
O Win32 lhe oferece um conjunto de funções de baixo nível que o capacita a manipular a memória vir-
tual de um processo. Essa memória existe em um dos seguintes estados:
l Livre. Memória disponível para ser reservada e/ou comprometida.
l Reservada. Memória dentro de um intervalo de endereços que está reservado para uso futuro. A
memória dentro desse endereço está protegida de outros pedidos de alocação. Entretanto, essa
memória não pode ser acessada pelo processo porque nenhuma memória física está associada a
ela até que esteja comprometida. A função VirtualAlloc( ) é utilizada para reservar a memória.
l Comprometida. Memória que foi alocada e associada com a memória física. A memória compro-
metida pode ser acessada pelo processo. A função VirtualAlloc( ) é usada para comprometer a
memória virtual.
Como já dissemos, o Win32 provê diversas funções VirtualXXXX( ) para manipular a memória virtual,
como foi mostrado na Tabela 3.2. Essas funções estão também documentadas com detalhes na ajuda
on-line.
Função Finalidade
101
NOTA
As rotinas xxxEx( ) listadas nesta tabela só podem ser usadas por um processo que tenha privilégios de de-
puração sobre o outro processo. A utilização dessas rotinas é complicada e raramente será feita por algo
que não seja um depurador.
Heaps
Heaps são blocos consecutivos de memória nos quais os blocos menores podem ser alocados. Os heaps
gerenciam de modo eficaz a alocação e a manipulação da memória dinâmica. A memória heap é manipu-
lada por meio de diversas funções HeapXXXX( ) da API do Win32. Essas funções estão listadas na Tabela
3.3 e se acham também documentadas com detalhes na ajuda on-line do Delphi.
Função Finalidade
NOTA
É importante notar que existem várias diferenças na implementação Win32 entre o Windows NT/2000 e o
Windows 95/98. Geralmente, essas diferenças têm a ver com segurança e velocidade. O gerenciador de
memória do Windows 95/98, por exemplo, é mais fraco que o do Windows NT/2000 (o NT mantém mais
informações internas de acompanhamento sobre os blocos de heap). No entanto, o gerenciador de memó-
ria virtual do NT é geralmente considerado tão rápido quanto o do Windows 95/98.
Esteja atento a tais diferenças quando usar as várias funções associadas a esses objetos do Windows. A aju-
da on-line destacará as variações específicas da plataforma para o uso de tal função. Não se esqueça de
consultar a ajuda sempre que usar essas funções.
Esse código de erro é mantido para cada thread, de modo que GetLastError( ) deve ser chamado no
contexto do thread que causa o erro. A seguir vemos um exemplo de uso dessa função:
if not CreateProcess(CommandLine, nil, nil, nil, False,
NORMAL_PRIORITY_CLASS, nil, nil, StartupInfo, ProcessInfo) then
raise Exception.Create(‘Error creating process: ‘+
IntToStr(GetLastError));
DICA
A unidade SysUtils.pas do Delphi 5 possui uma classe de exceção padrão e função utilitária para conver-
ter os erros do sistema em exceções. Essas funções são Win32Check( ) e RaiseLastWin32Error( ), que ge-
ram uma exceção EWin32Error. Use essas rotinas auxiliadoras ao invés de escrever suas próprias verifica-
ções de resultado.
Esse código tenta criar um processo especificado pela string terminada em nulo CommandLine. Deixa-
remos a discussão sobre o método CreateProcess( ) para um capítulo posterior, uma vez que estamos fo-
calizando a função GetLastError( ). Se o CreateProcess( ) falhar, uma exceção será gerada. Tal exceção
exibe o último código de erro que resultou da chamada da função, obtido a partir da função Get-
LastError( ). Você pode utilizar um método parecido em sua aplicação.
DICA
Os códigos de erros retornados por GetLastError( ) são normalmente documentados na ajuda on-line
sob as funções em que o erro ocorre. Portanto, o código de erro para CreateMutex( ) seria documentado
sob CreateMutex( ) na ajuda on-line do Win32.
Resumo
Este capítulo é uma introdução à API do Win32. Você deverá ter agora uma idéia quanto aos novos obje-
tos do kernel disponíveis, bem como de que modo o Win32 gerencia a memória. Você também já deverá
estar familiarizado com os recursos de gerenciamento de memória à sua disposição. Como programador
Delphi, não é necessário conhecer todos os detalhes específicos do sistema Win32. Entretanto, você pre-
cisa ter uma compreensão básica do sistema Win32, suas funções, e como pode usar essas funções para
aprimorar seu trabalho de desenvolvimento. Este capítulo oferece um ponto de partida.
103
Estruturas e conceitos CAPÍTULO
de projeto de aplicações
4
NE STE C AP ÍT UL O
l O ambiente e a arquitetura de projetos do
Delphi 105
l Arquivos que compõem um projeto do
Delphi 5 105
l Dicas de gerenciamento de projeto 109
l As classes de estruturas em um projeto do
Delphi 5 112
l Definição de uma arquitetura comum: o Object
Repository 124
l Rotinas variadas para gerenciamento de
projeto 136
l Resumo 147
Este capítulo trata do gerenciamento e da arquitetura de projetos em Delphi. Ele explica como usar cor-
retamente formulários em suas aplicações, além de como manipular suas características comporta-
mentais e visuais. As técnicas discutidas neste capítulo incluem procedimentos de partida/inicialização de
aplicações, reutilização/herança de código e melhoria da interface com o usuário. O texto também discu-
te as classes de estruturas que compõem as aplicações do Delphi 5: TApplication, TForm, TFrame e TScreen.
Depois, mostraremos por que a arquitetura apropriada das aplicações do Delphi depende desses concei-
tos fundamentais.
DICA
O sistema de ajuda do Delphi 5 é, sem dúvida alguma, a mais valiosa e rápida referência que você tem à sua
disposição. Seria muito proveitoso aprender a usá-lo para explorar as milhares de telas de ajuda disponíveis.
O Delphi 5 contém ajuda sobre tudo, desde como usar o ambiente do Delphi 5 até detalhes sobre a API
do Win32 e estruturas complexas do Win32. Você pode obter ajuda imediata sobre um tópico digitando o tó-
pico no editor e, com o cursor ainda na palavra que você digitou, pressionando Ctrl+F1. A tela de ajuda apa-
rece imediatamente. A ajuda também está disponível a partir das caixas de diálogo do Delphi 5, selecionan-
do-se o botão Help ou pressionando-se F1 quando um determinado componente tiver o foco. Você também
pode navegar pela ajuda simplesmente selecionando Help a partir do menu Help do Delphi 5.
O arquivo de projeto
O arquivo de projeto é criado durante o projeto e possui a extensão .dpr. Esse arquivo é o código-fonte do
programa principal. O arquivo de projeto é onde são instanciados o formulário principal e quaisquer for- 105
mulários criados automaticamente. Você raramente terá que editar esse arquivo, exceto ao realizar roti-
nas de inicialização do programa, exibir uma tela de abertura ou realizar várias outras rotinas que devam
acontecer imediatamente quando o programa for iniciado. O código a seguir mostra um arquivo de pro-
jeto típico:
program Project1;
uses
Forms,
Unit1 in ‘Unit1.pas’ {Form1};
{$R *.RES}
begin
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
Essa linha diz ao compilador para vincular o arquivo de recursos que possui o mesmo nome do ar-
quivo de projeto e uma extensão .RES a este projeto. O arquivo de recursos do projeto contém o ícone de
programa e informações sobre a versão.
Finalmente, é no bloco begin..end que o código principal da aplicação é executado. Neste exemplo
bem simples, é criado um formulário principal, Form1. Quando Application.Run( ) é executado, Form1 apa-
rece como o formulário principal. Você pode incluir código nesse bloco, como veremos mais adiante
neste capítulo.
Arquivos de formulário
Um arquivo de formulário contém uma representação binária de um formulário. Sempre que você criar
um novo formulário, o Delphi 5 criará um arquivo de formulário (com a extensão .dfm) e uma unidade
do Pascal (com a extensão .pas) para o seu novo formulário. Se você olhar para o arquivo de unidade de
um formulário, você verá a seguinte linha:
106 {$R *.DFM}
Essa linha diz ao compilador para vincular ao projeto o arquivo de formulário correspondente (o
arquivo de formulário que possui o mesmo nome do arquivo de unidade e uma extensão DFM).
Normalmente, você não edita o próprio arquivo de formulário (embora seja possível fazer isso).
Você pode carregar o arquivo do formulário no editor do Delphi 5 para que possa ver ou editar a repre-
sentação de texto desse arquivo. Selecione File, Open e depois selecione a opção para abrir apenas arqui-
vos de formulário (.dfm). Você também pode fazer isso simplesmente dando um clique com o botão direi-
to no Form Designer e selecionando View as Text (exibir como texto) no menu pop-up. Quando você
abrir o arquivo, verá a representação do formulário como texto.
A exibição da representação textual do formulário é prática porque você pode ver as configurações
de propriedade não-default para o formulário e quaisquer componentes que existam no formulário.
Uma maneira de editar o arquivo de formulário é alterar um tipo de componente. Por exemplo, suponha
que o arquivo de formulário contenha esta definição para um componente TButton:
object Button1: Tbutton
Left = 8
Top = 8
Width = 75
Height = 25
Caption = ‘Button1’
TabOrder = 0
end
Se você mudar a linha object Button1: TButton para object Button1: TLabel, mudará o tipo de compo-
nente para um componente TLabel. Quando o formulário aparecer, você verá um label no interior desse
formulário, e não um botão.
NOTA
A mudança dos tipos de componentes no arquivo de formulário poderá resultar em um erro de leitura de
propriedade. Por exemplo, ao trocar um componente TButton (que possui uma propriedade TabOrder) para
um componente TLabel (que não possui essa mesma propriedade), surgirá um erro. No entanto, não é pre-
ciso se preocupar com isso, pois o Delphi corrigirá a referência à propriedade da próxima vez que o formu-
lário for salvo.
ATENÇÃO
Você precisa ter extremo cuidado ao editar o arquivo de formulário. É possível danificá-lo, o que impedirá
que o Delphi 5 abra o formulário mais tarde.
NOTA
A capacidade de salvar formulários em formato de arquivo de texto é nova no Delphi 5. Isso se tornou pos-
sível para permitir a edição com outras ferramentas comuns, como Notepad.exe. Basta dar um clique com o
botão direito no formulário para fazer surgir o menu de contexto e selecionar Text DFM.
Arquivos de recursos
Arquivos de recursos contêm dados binários, também chamados recursos, que são vinculados ao arquivo
executável da aplicação. O arquivo RES criado automaticamente pelo Delphi 5 contém o ícone de aplica-
ção do projeto, as informações de versão da aplicação e outras informações. Você pode incluir recursos à
sua aplicação criando um arquivo de recurso separado e vinculando-o ao seu projeto. Você poderá criar
esse arquivo de recurso com um editor de recursos, como o Image Editor fornecido com o Delphi 5 ou
com o Resource Workshop. 107
ATENÇÃO
Não edite o arquivo de recursos que o Delphi cria automaticamente no momento da compilação. Isso fará
com que quaisquer mudanças sejam perdidas na próxima compilação. Se você quiser incluir recursos na
sua aplicação, crie um arquivo de recursos separado, com um nome diferente daquele usado para o seu
arquivo de projeto. Depois vincule o novo arquivo ao seu projeto usando a diretiva $R, como vemos na li-
nha de código a seguir:
{$R MYRESFIL.RES}
DICA
Um arquivo DSK ou DOF danificado pode gerar resultados inesperados, como uma GPF (falha geral de
proteção) durante a compilação. Se isso acontecer, apague os arquivos DOF e DSK. Eles serão criados no-
vamente quando você salvar seu projeto e quando sair do Delphi 5; a IDE e o projeto retornarão às configu-
rações default.
Arquivos de backup
O Delphi 5 cria arquivos de backup para o arquivo de projeto DPR e para quaisquer unidades PAS no se-
gundo e próximos salvamentos. Os arquivos de backup contêm a última cópia do arquivo antes que o sal-
vamento fosse realizado. O arquivo de backup do projeto possui a extensão .~dp. Os arquivos de backup
da unidade possuem a extensão .~pa.
Um backup binário do arquivo de formulário DRM também é criado depois que você o salvar pela
segunda vez em diante. Esse backup de arquivo de formulário possui uma extensão ~df.
Não haverá prejuízo algum se você apagar qualquer um desses arquivos – desde que observe que
está apagando seu último backup. Além disso, se você preferir não criar qualquer um desses arquivos,
pode impedir que o Delphi os crie retirando a seleção de Create Backup File (criar arquivo de backup) na
página Display (exibir) da caixa de diálogo Editor Properties (propriedades do editor).
Arquivos de pacote
Pacotes são simplesmente DLLs contendo código que pode ser compartilhado entre muitas aplicações.
No entanto, os pacotes são específicos do Delphi, no sentido de que permitem compartilhar componen-
tes, classes, dados e código entre os módulos. Isso significa que você pode agora reduzir drasticamente o
tamanho total da sua aplicação usando componentes que residem em pacotes, em vez de vinculá-los dire-
tamente nas suas aplicações. Outros capítulos falam mais a respeito de pacotes. Os arquivos-fonte de pa-
cote usam uma extensão .dpk (abreviação de Delphi package). Quando compilado, um arquivo BPL é cria-
do (um arquivo .BPL não é uma DLL). Esse BPL pode ser composto de várias unidades ou arquivos DCU
(Delphi Compiled Units), que podem ser de qualquer um dos tipos de unidade já mencionados. A ima-
108 gem binária de um arquivo DPK contendo todas as unidades incluídas e o cabeçalho do pacote possui a
extensão .dcp (Delphi Compiled Package). Não se preocupe se isso parecer confuso no momento; dare-
mos mais detalhes sobre os pacotes em outra oportunidade.
Um projeto, um diretório
É sempre bom controlar seus projetos de modo que os arquivos de um projeto fiquem separados dos ar-
quivos de outro projeto. Isso impede que um projeto interfira nos dados dos arquivos de outro projeto.
Observe que cada projeto no CD-ROM que acompanha este livro está no seu próprio diretório.
Você deverá acompanhar essa técnica e manter cada um de seus projetos no seu diretório próprio.
DICA
Usando o Project Manager, você pode incluir uma unidade de outro diretório em um projeto existente, o
que automaticamente cuida da inclusão do caminho de procura de arquivo.
Para explicar como usar as unidades utilitárias, a Listagem 4.1 mostra uma pequena unidade,
StrUtils.pas, que contém uma única função utilitária de string. Na realidade, tais unidades provavelmen-
te teriam muito mais rotinas, mas isso é suficiente para este exemplo. Os comentários explicam a finali-
dade da função.
unit strutils;
interface
function ShortStringAsPChar(var S: ShortString): PChar;
implementation
function ShortStringAsPChar(var S: ShortString): PChar; 109
Listagem 4.1 Continuação
{ Esta função termina com nulo uma string curta, para que possa ser
passada a funções que exigem tipos PChar. Se a string for maior que
254 chars, então será truncada para 254.
}
begin
if Length(S) = High(S) then Dec(S[0]); { Trunca S se for muito grande }
S[Ord(Length(S)) + 1] := #0; { Inclui nulo ao final da string }
Result := @S[1]; { Retorna string “PChar’d” }
end;
end.
Suponha que você tenha uma unidade, SomeUnit.Pas, que exija o uso dessa função. Basta incluir
StrUtilsna cláusula uses da unidade que a necessita, como vemos aqui:
unit SomeUnit;
interface
...
implementation
uses
strutils;
...
end.
Além disso, você precisa garantir que o Delphi 5 poderá encontrar a unidade StrUtils.pas, incluin-
do-a no caminho de procura a partir do menu Project, Options.
Quando você fizer isso, poderá usar a função ShortStringAsPChar( ) de qualquer lugar da seção de imple-
mentação de SomeUnit.pas. Você precisa colocar StrUtils na cláusula uses de todas as unidades que precisam
acessar a função ShortStringAsPChar( ). Não é suficiente incluir StrUtils apenas em uma unidade do projeto, ou
ainda no arquivo de projeto (DPR) da aplicação, para que a rotina fique à disposição da aplicação inteira.
DICA
Visto que ShortStringAsPChar( ) é uma função bastante útil, vale a pena incluí-la em uma unidade utilitária
onde possa ser reutilizada por qualquer aplicação, para que você não tenha que se lembrar como ou onde
a usou pela última vez.
NOTA
O vínculo de formulário perguntará se você deseja incluir Unit2 na cláusula uses de Unit1 quando você
compilar o projeto, caso você se refira ao formulário de Unit2 (chamá-lo de Form2); basta referenciar Form2
em algum lugar de Unit1.
Com essa estrutura, você possui dois diretórios separados para cada projeto do Delphi: BugReport-
Project e BugAdminTool. No entanto, esses dois projetos podem usar formulários e unidades comuns. Você
colocaria esses arquivos no diretório CommonFiles.
A organização é fundamental nos seus esforços de desenvolvimento, especialmente em um ambien-
te de desenvolvimento em equipe. É altamente recomendado que você estabeleça um padrão antes que
sua equipe se aprofunde na criação de diversos arquivos que serão difíceis de se gerenciar. Você pode
usar o Project Manager do Delphi para ajudá-lo a entender sua estrutura de gerenciamento de projeto.
111
As classes de estruturas em um projeto do Delphi 5
A maioria das aplicações do Delphi 5 possui pelo menos uma instância de um TForm. Além do mais, as
aplicações da VCL do Delphi 5 terão apenas uma instância de uma classe TApplication e de uma classe
TScreen. Essas três classes desempenham funções importantes ao se gerenciar o comportamento de um
projeto do Delphi 5. As próximas seções o familiarizam com os papéis desempenhados por essas classes,
para que, quando for preciso, você tenha o conhecimento suficiente para modificar seus comportamen-
tos default.
A classe TForm
A classe TForm é o ponto de enfoque para aplicações do Delphi 5. Na maioria das vezes, a aplicação inteira
gira em torno do formulário principal. A partir dele, você pode ativar outros formulários, normalmente
como resultado de um evento de menu ou de clique de um botão. Você pode querer que o Delphi 5 crie
seus formulários automaticamente, quando você não terá que se preocupar em criá-los e destruí-los.
Você também pode decidir criar os formulários dinamicamente, durante a execução.
NOTA
O Delphi pode criar aplicações que não usam formulários (por exemplo, aplicações de console, serviços e
servidores COM). Portanto, a classe TForm nem sempre é o ponto de enfoque das suas aplicações.
Você pode exibir o formulário para o usuário final usando um destes dois métodos: modal ou
não-modal. O método que você escolhe depende de como você pretende que o usuário interaja com o
formulário e com outros formulários simultaneamente.
Esse código mostra como você criaria dinamicamente uma instância de TModalForm e lhe atribuiria à va-
riável ModalForm. É importante observar que, se você criar um formulário dinamicamente, terá que removê-
lo da lista de formulários disponíveis a partir da caixa de listagem Auto-Create na caixa de diálogo Pro-
ject Options (opções do projeto). Essa caixa de diálogo é ativada pela seleção de Project, Options a partir
do menu. Entretanto, se a instância do formulário já estiver criada, você poderá exibi-la como um formu-
lário modal simplesmente chamando o método ShowModal( ). Todo o código ao redor pode ser removido:
112
begin
if ModalForm.ShowModal = mrOk then // ModalForm já foi criado
{ faz alguma coisa }
end;
O método ShowModal( ) retorna o valor atribuído à propriedade ModalResult de ModalForm. Por default,
ModalResult é zero, que é o valor da constante predefinida mrNone. Quando você atribui qualquer valor di-
ferente de zero a ModalResult, o formulário é fechado e a atribuição feita para ModalResult é passada de vol-
ta à rotina que chamou por meio do método ShowModal( ).
Os botões possuem uma propriedade ModalResult. Você pode atribuir um valor a essa propriedade,
que será passado para a propriedade ModalResult do formulário quando o botão for pressionado. Se esse
valor for algo diferente de mrNone, o formulário será fechado e o valor passado de volta pelo método Show-
Modal( ) refletirá o que foi atribuído a ModalResult.
Você também pode atribuir um valor à propriedade ModalResult do formulário durante a execução:
begin
ModalForm.ModalResult := 100; // Atribuindo um valor para ModalResult
// fechando o formulário.
end;
Constante Valor
mrNone 0
mrOk idOk
mrCancel idCancel
mrAbort idAbort
mrRetry idRetry
mrIgnore idIgnore
mrYes idYes
mrNo idNo
mrAll mrNo+1
A versão anterior desse código resolve o problema do formulário não sendo liberado. Mas há um
outro aspecto. Você pode ter notado que esta linha foi usada no primeiro trecho de código referente aos
formulários não-modais:
if not Assigned(Modeless) then begin
A linha verifica uma instância de TModeless referenciada pela variável Modeless. Na realidade, isso ve-
rifica se Modeless não é nil. Embora Modeless seja nil na primeira vez em que você entrar na rotina, não será
nil quando você entrar na rotina pela segunda vez depois de ter destruído o formulário. O motivo é que a
VCL não define a variável Modeless como nil quando ela é destruída. Portanto, isso é algo que você mes-
mo precisa fazer.
Ao contrário de um formulário modal, você não pode determinar no código quando o formulário
não-modal será destruído. Portanto, você não pode destruir o formulário dentro da rotina que o cria. O
usuário pode fechar o formulário a qualquer momento enquanto executa a aplicação. Portanto, a defini-
ção de Modeless como nil precisa ser um processo da própria classe TModeless. O melhor local para se fazer
isso é no manipulador de evento OnDestroy de TModeless:
procedure TModeless.FormDestroy(Sender: Tobject);
begin
Modeless := nil; // Define variável Modeless como nil quando destruída
end;
Isso garante que a variável Modeless será definida como nil toda vez que for destruída, evitando a fa-
lha do método Assigned( ). Lembre-se de que é por sua conta garantir que somente uma instância de TMo-
deless seja criada ao mesmo tempo, como vimos nessa rotina.
O projeto ModState.dpr no CD-ROM que acompanha este livro ilustra o uso de formulários modais e
não-modais.
114
ATENÇÃO
Evite a armadilha a seguir ao trabalhar com formulários não-modais:
begin
Form1 := TForm1.Create(Application);
Form1.Show;
end;
Esse código resulta em uma memória sendo consumida desnecessariamente, pois toda vez que você cria
uma instância de formulário, substitui a instância anterior referenciada por Form1. Embora você possa refe-
renciar cada instância do formulário criado através da lista Screen.Forms, a prática mostrada no código an-
terior não é recomendada. Passar nil para o construtor Create( ) resultará na impossibilidade de se referir
ao ponteiro de instância do formulário depois que a variável de instância Form1 for substituída.
NOTA
As mudanças nas propriedades BorderIcon e BorderStyle não são refletidas durante o projeto. Essas mu-
danças acontecem apenas durante a execução. Isso também acontece com outras propriedades, princi-
palmente as encontradas em TForm. O motivo para esse comportamento é que não faz sentido alterar a
aparência de certas propriedades durante o projeto. Por exemplo, considere a propriedade Visible. É difí-
cil selecionar um controle de um formulário quando sua propriedade Visible está definida como False,
pois o controle ficaria invisível.
115
Títulos que não somem!
Você pode ter notado que nenhuma das opções mencionadas permite criar formulários redi-
mensionáveis e sem título. Embora isso não seja impossível, requer um pouco de truque, ainda não
explicado. Você precisa modificar o método CreateParams( ) do formulário e definir os estilos necessá-
rios para esse estilo de janela. O trecho de código a seguir faz exatamente isso:
unit Nocapu;
interface
uses
SysUtils, WinTypes, WinProcs, Messages, Classes,
Graphics, Controls, Forms, Dialogs;
type
TForm1 = class(TForm)
public
{ substitui método CreateParams }
procedure CreateParams(var Params: TCreateParams); override;
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
Dê uma olhada no projeto BrdrIcon.dpr no CD-ROM. Esse projeto ilustra como você pode alterar as
propriedades BorderIcon e BorderStyle durante a execução, para que veja o efeito visual. A Listagem 4.2
mostra o formulário principal para esse projeto, que contém o código relevante.
unit MainFrm;
interface
uses
SysUtils, Windows, Messages, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls, ExtCtrls;
type
TMainForm = class(TForm)
116
Listagem 4.2 Continuação
gbBorderIcons: TGroupBox;
cbSystemMenu: TCheckBox;
cbMinimize: TCheckBox;
cbMaximize: TCheckBox;
rgBorderStyle: TRadioGroup;
cbHelp: TCheckBox;
procedure cbMinimizeClick(Sender: TObject);
procedure rgBorderStyleClick(Sender: TObject);
end;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
end.
NOTA
Algumas propriedades no Object Inspector afetam a aparência do seu formulário; outras definem aspectos
de comportamento para o formulário. Experimente cada propriedade com que não esteja acostumado. Se
você precisar saber mais sobre uma propriedade, use o sistema de ajuda do Delphi 5 para descobrir outras
informações.
O Object Repository
O Delphi 5 possui um recurso de gerenciamento de projeto que permite aos programadores
compartilharem formulários, caixas de diálogo, módulos de dados e modelos de projeto. Esse recur-
so é chamado Object Repository. Usando o Object Repository, os programadores podem comparti-
lhar os vários objetos listados com os programadores desenvolvendo outros projetos. Além do
mais, o Object Repository permite que os programadores aprimorem a reutilização de código que existe
no Object Repository. O Capítulo 4 do Delphi 5 User’s Guide explica sobre o Object Repository. É sem-
pre bom familiarizar-se com esse poderoso recurso.
DICA
Em um ambiente de rede, você poderá compartilhar modelos de formulário com outros programadores.
Isso é possível criando-se um repositório compartilhado. Na caixa de diálogo Environment Options (op-
ções de ambiente, obtida pelas opções de menu Tools, Environment Options), você pode especificar o lo-
cal de um repositório compartilhado. Cada programador deve mapear a mesma unidade que aponta para
o local desse diretório. Depois, sempre que File, New for selecionado, o Delphi analisará esse diretório e
procurará itens compartilhados no repositório.
A herança de um formulário a partir de outro formulário é simples porque está completamente em-
butida no ambiente do Delphi 5. Para criar um formulário descendente de outra definição de formulário,
basta selecionar File, New no menu principal do Delphi, fazendo surgir a caixa de diálogo New Items
(novos itens). Essa caixa de diálogo na realidade lhe oferece uma visão dos objetos que existem no Object
Repository (ver a nota “O Object Repository”), Depois você seleciona a página Forms, que lista os for-
mulários que foram incluídos no Object Repository.
NOTA
Você não precisa passar pelo Object Repository para obter herança do formulário. Você pode herdar de
formulários que estão no seu projeto. Selecione File, New e depois selecione a página Project. A partir daí,
você pode selecionar um formulário existente no seu projeto. Os formulários mostrados na página Project
não estão no Object Repository.
Os vários formulários listados são aqueles que foram incluídos anteriormente no Object Repo-
sitory. Você notará que existem três opções para inclusão do formulário no seu projeto: Copy, Inhe-
rit e Use.
A escolha de Copy inclui uma duplicata exata do formulário no seu projeto. Se o formulário manti-
do no Object Repository for modificado, isso não afetará seu formulário copiado.
A escolha de Inherit faz com que uma nova classe de formulário derivada do formulário que você
selecionou seja incluída no seu projeto. Esse recurso poderoso permite herdar a partir da classe no Object
Repository, para que as mudanças feitas no formulário do Object Repository também sejam refletidas
pelo formulário no seu projeto. Essa é a opção que a maioria dos programadores deve selecionar.
A escolha de Use faz com que o formulário seja incluído no seu projeto como se você o tivesse cria-
do como parte do projeto. As mudanças feitas no item durante o projeto aparecerão em todos os projetos
118 que também usam o formulário e em quaisquer projetos que herdam a partir do formulário.
A classe TApplication
Cada formulário baseado no programa Delphi 5 contém uma variável global, Application, do tipo TAppli-
cation. TApplication encapsula seu programa e realiza muitas funções nos bastidores, permitindo que sua
aplicação funcione corretamente dentro do ambiente Windows. Essas funções incluem a criação da
sua definição de classe de janela, a criação da janela principal para a sua aplicação, a ativação da sua apli-
cação, o processamento de mensagens, a inclusão da ajuda sensível ao contexto, o processamento de te-
clas aceleradoras do menu e o tratamento de exceções da VCL.
NOTA
Somente aplicações do Delphi baseadas em formulário contêm o objeto global Application. Aplicações
como as de console não contêm um objeto Application da VCL.
Normalmente você não terá se preocupar com as tarefas de segundo plano que TApplication realiza.
No entanto, algumas situações podem exigir que você se aprofunde no funcionamento interno de TAppli-
cation.
Visto que TApplication não aparece no Object Inspector, você não pode modificar suas propriedades
por lá. Entretanto, você pode escolher Project, Options e seleciona a página Application, da qual poderá
definir algumas das propriedades para TApplication. Fundamentalmente, você trabalha com a instância de
TApplication, Application, em runtime – ou seja, você define seus valores de propriedade e atribui manipu-
ladores de evento para Application quando o programa está sendo executado.
Propriedades de TApplication
TApplication possui várias propriedades que você pode acessar em runtime. As próximas seções discutem
algumas das propriedades específicas de TApplication e como você pode usá-las para alterar o comporta-
mento default de Application para aprimorar seu projeto. As propriedades de TApplication também são
bem documentadas na ajuda on-line do Delphi 5.
A propriedade TApplication.ExeName
A propriedade ExeName de Application contém o caminho completo e o nome de arquivo do projeto. Como
esta é uma propriedade de runtime, apenas para leitura, você não poderá modificá-la. No entanto, você
poderá lê-la – ou ainda permitir que seus usuários saibam de onde executaram a aplicação. Por exemplo,
a linha de código a seguir muda o título do formulário principal para o conteúdo de ExeName.
Application.MainForm.Caption := Application.ExeName;
DICA
Use a função ExtractFileName( ) para apanhar apenas o nome de arquivo de uma string contendo o cami-
nho completo de um arquivo:
ShowMessage(ExtractFileName(Application.ExeName));
Use ExtractFilePath( ) para apanhar apenas o caminho de uma string de caminho completa:
ShowMessage(ExtractFilePath(Application.ExeName));
ShowMessage(ExtractFileExt(Application.ExeName));
119
A propriedade TApplication.MainForm
Na seção anterior, você viu como acessar a propriedade MainForm para alterar seu Caption e refletir o ExeNa-
me da aplicação. MainForm aponta para um TForm, de modo que você pode acessar qualquer propriedade de
TForm através de MainForm. Você também pode acessar propriedades incluídas nos seus formulários descen-
dentes, desde que digite o tipo de MainForm corretamente:
(MainForm as TForm1).SongTitle := ‘The Flood’;
MainForm é uma propriedade apenas de leitura. Durante o projeto, você pode especificar qual for-
mulário da sua aplicação é o formulário principal, usando a página Forms da caixa de diálogo Project
Options.
A propriedade TApplication.Handle
A propriedade Handle é um HWND (uma alça de janela, em termos da API do Win32). A alça de janela é o
proprietário de todas as janelas de alto nível da sua aplicação. Handle é o que torna as caixas de diálogo
modais por todas as janelas da sua aplicação. Você não precisa acessar Handle com tanta freqüência, a me-
nos que queira controlar o comportamento default da aplicação de tal forma que não seja oferecida pelo
Delphi. Você também pode referenciar a propriedade Handle ao usar funções da API do Win32 que exi-
gem a alça de janela da aplicação. Discutiremos sobre Handle mais adiante neste capítulo.
Outras propriedades
A propriedade Active é uma propriedade booleana apenas para leitura, que indica se a aplicação possui o
foco e se está ativa.
A propriedade ComponentCount indica o número de componentes que Application contém. Esses com-
ponentes são, principalmente, formulários e uma instância de THintWindow se a propriedade Applicati-
on.ShowHint for True. ComponentIndex é sempre -1 para qualquer componente que não tenha um proprietá-
rio. Portanto, Tapplication.ComponentIndex é sempre -1. Essa propriedade aplica-se principalmente a for-
mulários e componentes nos formulários.
A propriedade Components é um array de componentes que pertencem a Application. Haverá TApplica-
tion.ComponentCount itens no array Components. O código a seguir mostra como você incluiria os nomes de
classe de todos os componentes referenciados por ComponentCount a um componente TListBox:
var
i: integer;
begin
for i := 0 to Application.ComponentCount - 1 do
ListBox1.Items.Add(Application.Components[i].ClassName);
end;
A propriedade HelpFile contém o nome de arquivo de ajuda do Windows, que permite incluir ajuda
on-line à sua aplicação. Ele é usado por TApplication.HelpContext e outros métodos de chamada de ajuda.
120
A propriedade TApplication.Owner é sempre nil, pois TApplication não pode ser possuído por qualquer
outro componente.
A propriedade ShowHint ativa ou desativa a exibição de sugestões para a aplicação inteira. A proprie-
dade Application.ShowHint substitui os valores da propriedade ShowHint de qualquer outro componente.
Portanto, se Application.ShowHint for False, as sugestões não aparecem para componente algum.
A propriedade Terminated é True sempre que a aplicação for terminada pelo fechamento do formulá-
rio principal ou pela chamada do método TApplication.Terminate( ).
Métodos de TApplication
TApplication possui vários métodos com os quais você precisa se acostumar. As próximas seções discutem
alguns dos métodos específicos a TApplication.
O método TApplication.CreateForm( )
O método TApplication.CreateForm( ) é definido da seguinte maneira:
procedure CreateForm(InstanceClass: TComponentClass; var Reference)
Esse método cria uma instância de um formulário com o tipo especificado por InstanceClass, e atri-
bui essa instância à variável Reference. Você já viu anteriormente como esse método foi chamado no ar-
quivo DPR do projeto. O código tinha a seguinte linha, que cria a instância de Form1 do tipo TForm1:
Application.CreateForm(TForm1, Form1);
A linha teria sido criada automaticamente pelo Delphi 5 se Form1 aparecesse na lista Auto-Create do
projeto. No entanto, você pode chamar esse método de qualquer outro lugar do seu código se estiver cri-
ando um formulário que não aparece na lista Auto-Create (quando a instância do formulário teria sido
criada automaticamente). Essa técnica não difere muito da chamada do próprio método Create( ) do for-
mulário, exceto que TApplication.CreateForm( ) verifica se a propriedade TApplication.MainForm é nil; se for,
CreateForm( ) atribui o formulário recém-criado a Application.MainForm. As chamadas seguintes para Create-
Form( ) não afetam essa atribuição. Normalmente, você não chama CreateForm( ), mas em vez disso utiliza
o método Create( ) de um formulário.
O método TApplication.HandleException( )
O método HandleException( ) é o local onde a instância TApplication apresenta informações sobre exceções
que ocorrem no seu projeto. Essas informações são apresentadas com uma caixa de mensagem de exce-
ção padrão, definida pela VCL. Você pode redefinir essa caixa de mensagem conectando um manipula-
dor de evento ao evento Application.OnException, como veremos na seção “Substituindo o tratamento de
exceção da aplicação”, mais adiante neste capítulo.
O método TApplication.ProcessMessages( )
ProcessMessages( )faz com que sua aplicação receba ativamente quaisquer mensagens que estejam espe-
rando por ela e depois as processe. Isso é útil quando você tiver que realizar um processo dentro de um 121
loop apertado e não queira que seu código o impeça de executar outro código (como o processamento de
um botão de abortar). Ao contrário, TApplication.HandleMessages( ) coloca a aplicação em um estado ocio-
so se não houver mensagens, enquanto ProcessMessages( ) não a coloca em um estado ocioso. O método
ProcessMessages( ) é usado no Capítulo 10.
O método TApplication.Run( )
O Delphi 5 coloca automaticamente o método Run( ) dentro do bloco principal do arquivo de projeto.
Você nunca precisa chamar esse método diretamente, mas precisa saber onde ele entra e o que ele faz
caso você tenha que modificar o arquivo de projeto. Basicamente, TApplication.Run( ) primeiro estabelece
um procedimento de saída para o projeto, o que garante que todos os componentes sejam liberados
quando o projeto terminar. Depois ele entra em um loop que chama os métodos para processar mensa-
gens para o projeto até que a aplicação seja terminada.
O método TApplication.ShowException( )
O método ShowException( ) simplesmente apanha uma classe de exceção como um parâmetro e mostra
uma caixa de mensagem com informações sobre essa exceção. Esse método é prático se você estiver subs-
tituindo o método de tratamento de exceção de Application, como mostramos mais adiante na seção
“Substituindo o tratamento de exceção da aplicação”.
Outros métodos
TApplication.Create( ) cria a instância de TApplication. Esse método é chamado internamente pelo Delphi
5; você nunca terá que chamá-lo.
TApplication.Destroy( ) destrói a instância de TApplication. Esse método é chamado internamente
pelo Delphi 5; você nunca terá que chamá-lo.
TApplication.MessageBox( ) permite que você apresente uma caixa de mensagem do Windows. No en-
tanto, o método não exige que você lhe passe uma alça de janela, como na função MessageBox( ) do Win-
dows.
TApplication.Minimize( ) coloca a sua aplicação em um estado minimizado.
TApplication.Restore( ) restaura a sua aplicação ao seu tamanho anterior a partir de um estado mini-
mizado ou maximizado.
TApplication.Terminate( ) termina a execução da sua aplicação. Terminate é uma chamada indireta a
PostQuitMessage, resultando em um encerramento natural da aplicação (ao contrário de Halt( )).
NOTA
Use o método TApplication.Terminate( ) para interromper uma aplicação. Terminate( ) chama a função
PostQuitMessage( ) da API do Windows, que posta uma mensagem na fila de mensagens da sua aplica-
ção. A VCL responde liberando corretamente os objetos que foram criados na aplicação. O método Termi-
nate( ) é um modo limpo de encerrar o processo da sua aplicação. É importante observar que sua aplica-
ção não termina na chamada a Terminate( ). Em vez disso, ela continua a rodar até que a aplicação retor-
ne à sua fila de mensagens e recupere a mensagem WM_QUIT. Halt( );, por outro lado, força o término da
aplicação sem liberar quaisquer objetos, sem encerrar naturalmente. Após a chamada a Halt( ), a execu-
ção não retorna.
Eventos de TApplication
TApplicationpossui diversos eventos aos quais você pode incluir manipuladores (ou handlers) de evento.
Nas versões passadas do Delphi, esses eventos não eram acessíveis por meio do Object Inspector (por
exemplo, os eventos para o formulário ou componentes da Component Palette). Você tinha que incluir
122 um manipulador de evento na variável Application, primeiro definindo o manipulador como um método
e, em seguida, atribuindo esse método ao manipulador em runtime. O Delphi 5 inclui um novo compo-
nente à página Additional da Component Palette – TApplicationEvents. Esse componente permite atribuir,
durante o projeto, manipuladores de evento à instância global Application. A Tabela 4.2 relaciona os
eventos associados a TApplication.
Evento Descrição
OnActivate Ocorre quando a aplicação se torna ativa; OnDeactivate ocorre quando a aplicação
deixa de estar ativa (por exemplo, quando você passa para outra aplicação).
OnException Ocorre quando tiver havido uma exceção não-tratada; você pode incluir um
processamento default para as exceções não-tratadas. OnException ocorre se a exceção
conseguir chegar até o objeto da aplicação. Normalmente, você deve permitir que as
exceções sejam tratadas pelo manipulador de exceção default, e não interceptadas por
Application.OnException ou algum código inferior. Se você tiver de interceptar uma
exceção, gere-a novamente e certifique-se de que a instância da exceção transporte
uma descrição completa da situação, para que o manipulador de exceção default
possa apresentar informações úteis.
OnHelp Ocorre para qualquer chamada do sistema de ajuda, como quando F1 é pressionado
ou quando os métodos a seguir são chamados: HelpCommand( ), HelpContext( ) e
HelpJump( ).
OnMessage Permite que você processe mensagens antes que elas sejam despachadas para seus
controles intencionados. OnMessage consegue apanhar todas as mensagens postadas
para todos os controles da aplicação. Tenha cuidado ao usar OnMessage, pois poderia
resultar em um engarrafamento.
OnHint Permite que você apresente sugestões associadas aos controles quando o mouse
estiver posicionado sobre o controle. Um exemplo disso é uma sugestão na linha de
status.
OnIdle Ocorre quando a aplicação é passada para um estado ocioso. OnIdle não é chamado
continuamente. Estando no estado ocioso, uma aplicação não sairá dele até que
receba uma mensagem.
Você trabalhará com TApplication mais adiante neste capítulo, e também em outros projetos de ou-
tros capítulos.
NOTA
O evento TApplication.OnIdle oferece um modo prático de realizar certo processamento quando não esti-
ver havendo interação com o usuário. Um uso comum para o manipulador de evento OnIdle é atualizar
menus e speedbuttons com base no status da aplicação.
A classe TScreen
A classe TScreen simplesmente encapsula o estado da tela em que as suas aplicações são executadas. TScreen
não é um componente que você inclui nos seus formulários do Delphi 5, e você também não o cria dina-
micamente em runtime. O Delphi 5 cria automaticamente uma variável global de TScreen, chamada Scre-
en, que você pode acessar de dentro da sua aplicação. A classe TScreen contém várias propriedades que
você achará úteis. Essas propriedades são relacionadas na Tabela 4.3. 123
Tabela 4.3 Propriedades de TScreen
Propriedade Significado
ActiveControl Uma propriedade apenas de leitura, que indica qual controle na tela possui o foco
atualmente. Quando o foco passa de um controle para outro, ActiveControl recebe o
controle recém-focalizado antes do término do evento OnExit do controle que está
perdendo o foco.
ActiveForm Indica o formulário que possui o foco. Essa propriedade é definida quando outro
formulário recebe o foco ou quando a aplicação do Delphi 5 recebe o foco a partir de
outra aplicação.
Cursor A forma do cursor global à aplicação. Por default, esta é definida como crDefault.
Cada componente em janela possui sua propriedade Cursor independente, que pode
ser modificada. No entanto, quando o cursor é definido para algo diferente de
crDefault, todos os outros controles refletem essa mudança até que Screen.Cursor seja
definido de volta para crDefault. Outra maneira de se ver isso é através de
Screen.Cursor = crDefault, que significa “pergunte ao controle sob o mouse que
cursor deve ser apresentado”. Screen.Cursor < > crDefault significa “não pergunte”.
Cursors Uma lista de todos os cursores disponíveis para o dispositivo de tela.
DataModules Uma lista de todos os módulos de dados pertencentes à aplicação.
DataModuleCount O número de módulos de dados pertencentes à aplicação.
FormCount O número de formulários disponíveis na aplicação.
Forms Uma lista dos formulários disponíveis para a aplicação.
Fonts Uma lista dos nomes de fonte disponíveis ao dispositivo de tela.
Height A altura do dispositivo de tela em pixels.
PixelsPerInch Indica a escala relativa da fonte do sistema.
Width A largura do dispositivo de tela em pixels.
124
l A arquitetura aceita reutilização de código?
l O sistema é organizado de modo que os módulos, objetos e outros possam ser localizados?
l As mudanças podem ser feitas mais facilmente na arquitetura?
l A interface com o usuário e o back-end estão localizados de modo que ambos possam ser substi-
tuídos?
l A arquitetura aceita um esforço de desenvolvimento em equipe? Em outras palavras, os mem-
bros da equipe podem trabalhar facilmente em módulos separados sem sobreposição?
Estas são apenas algumas das coisas a considerar durante o desenvolvimento.
Muitos volumes têm sido escritos apenas sobre esse tópico, e por isso não tentaremos competir com
essa informação. No entanto, esperamos ter aumentado seu interesse o suficiente para que você estude
mais a respeito disso, se ainda não for um guru em arquitetura de aplicações. As próximas seções ilustram
um método simples para a arquitetura de uma interface com o usuário comum para aplicações de banco
de dados, e como o Delphi pode ajudá-lo a fazer isso.
Um exemplo de arquitetura
Para demonstrar o poder da herança de formulário e também o uso do Object Repository, vamos definir
uma arquitetura de aplicação comum. As questões que estaremos focalizando são reutilização de código,
flexibilidade para mudanças, coerência e facilidade para desenvolvimento em equipe.
Uma hierarquia de classe de formulário, ou estrutura, consiste em formulários a serem usados espe-
cificamente para aplicações de banco de dados. Esses formulários são típicos da maioria das aplicações de
banco de dados. Os formulários devem conhecer o estado da operação do banco de dados (edição, inser-
ção ou navegação). Eles também devem conter os controles comuns usados para realizar essas operações
sobre uma tabela de banco de dados, como uma barra de ferramentas e barra de status cujos mostradores
e controles mudam de acordo com o estado do formulário. Além disso, eles devem oferecer um evento
que possa ser chamado sempre que o modo do formulário mudar.
Essa estrutura também deverá permitir que uma equipe trabalhe em partes isoladas da aplicação
sem exigir o código-fonte da aplicação inteira. Caso contrário, existe a probabilidade de que diferentes
programadores modifiquem os mesmos arquivos.
Por enquanto, essa hierarquia estrutural terá três níveis. Isso será expandido mais adiante no livro.
A Tabela 4.4 descreve a finalidade de cada formulário da estrutura.
125
Tabela 4.4 Estrutura do formulário de banco de dados
TChildForm = class(TForm) Oferece a capacidade de ser inserido como um filho de outra janela.
TDBModeForm = class(TChildForm) Conhece o estado de um banco de dados (navegação, inserção, edição) e
contém um evento para ser chamado se houver mudança de estado.
TDBNavStatForm = class(TDBBaseForm) Formulário típico de entrada de banco de dados, que conhece o estado e
contém a barra de navegação padrão e a barra de status a ser usada por
todas as aplicações de banco de dados.
unit ChildFrm;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls, ExtCtrls, Menus;
type
TChildForm = class(TForm)
private
FAsChild: Boolean;
FTempParent: TWinControl;
protected
procedure CreateParams(var Params: TCreateParams); override;
procedure Loaded; override;
public
constructor Create(AOwner: TComponent); overload; override;
constructor Create(AOwner: TComponent;
AParent: TWinControl); reintroduce; overload;
implementation
{$R *.DFM}
constructor TChildForm.Create(AOwner: TComponent);
begin
FAsChild := False;
inherited Create(AOwner);
end;
procedure TChildForm.Loaded;
begin
inherited;
if FAsChild then
begin
align := alClient;
BorderStyle := bsNone;
BorderIcons := [ ];
Parent := FTempParent;
Position := poDefault;
end;
end;
end.
Essa listagem demonstra algumas técnicas. Primeiro, ela mostra como usar as extensões de overload
da linguagem Object Pascal, e segundo, ela mostra como tornar um formulário um filho de outra janela.
NOTA
O Delphi 5 introduz os frames na VCL. Os frames funcionam de modo que possam ser incorporados dentro
de um formulário. Como os frames servem como recipientes (containers) para componentes, eles funcio-
nam de modo semelhante ao formulário filho mostrado anteriormente. Um pouco mais adiante, você verá
uma discussão mais detalhada sobre frames.
unit DBModeFrm;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
CHILDFRM;
type
TDBModeForm = class(TChildForm)
private
FFormMode : TFormMode;
FOnSetFormMode : TNotifyEvent;
protected
procedure SetFormMode(AValue: TFormMode); virtual;
function GetFormMode: TFormMode; virtual;
public
property FormMode: TFormMode read GetFormMode write SetFormMode;
published
property OnSetFormMode: TNotifyEvent read FOnSetFormMode
write FOnSetFormMode;
end;
var
DBModeForm: TDBModeForm;
implementation
{$R *.DFM}
end.
A implementação de TDBModeForm é muito simples. Embora estejamos usando algumas técnicas a res-
peito das quais ainda não discutimos, você deverá poder acompanhar o que acontece aqui. Primeiro, sim-
plesmente definimos o tipo enumerado, TFormMode, para representar o estado do formulário. Depois ofe-
recemos a propriedade FormMode e seus métodos de leitura e escrita. A técnica para a criação da proprieda-
de e dos métodos de leitura/escrita é discutida mais adiante, no Capítulo 21.
Uma demonstração usando TDBModeForm está no projeto FormModeTest.DPR, encontrado no diretório
\Form Framework do CD-ROM.
unit DBNavStatFrm;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
DBMODEFRM, ComCtrls, ToolWin, Menus, ExtCtrls, ImgList;
type
TDBNavStatForm = class(TDBModeForm)
{ components not included in listing. }
procedure sbAcceptClick(Sender: TObject);
procedure sbInsertClick(Sender: TObject);
procedure sbEditClick(Sender: TObject);
private
{ Declarações privadas }
protected
procedure Setbuttons; virtual;
procedure SetStatusBar; virtual;
procedure SetFormMode(AValue: TFormMode); override;
public
constructor Create(AOwner: TComponent); overload; override;
constructor Create(AOwner: TComponent; AParent: TWinControl); overload;
procedure SetToolBarParent(AParent: TWinControl);
procedure SetStatusBarParent(AParent: TWinControl);
end;
var
DBNavStatForm: TDBNavStatForm;
implementation
{$R *.DFM}
{ TDBModeForm3 }
procedure TDBNavStatForm.Setbuttons;
procedure SetBrowseButtons;
130
Listagem 4.5 Continuação
begin
sbAccept.Enabled := False;
sbCancel.Enabled := False;
sbInsert.Enabled := True;
sbDelete.Enabled := True;
sbEdit.Enabled := True;
sbFind.Enabled := True;
sbBrowse.Enabled := True;
sbFirst.Enabled := True ;
sbPrev.Enabled := True ;
sbNext.Enabled := True ;
sbLast.Enabled := True ;
end;
procedure SetInsertButtons;
begin
sbAccept.Enabled := True;
sbCancel.Enabled := True;
sbInsert.Enabled := False;
sbDelete.Enabled := False;
sbEdit.Enabled := False;
sbFind.Enabled := False;
sbBrowse.Enabled := False;
sbFirst.Enabled := False;
sbPrev.Enabled := False;
sbNext.Enabled := False;
sbLast.Enabled := False;
end;
procedure SetEditButtons;
begin
sbAccept.Enabled := True;
sbCancel.Enabled := True;
sbInsert.Enabled := False;
sbDelete.Enabled := False;
sbEdit.Enabled := False;
sbFind.Enabled := False;
sbBrowse.Enabled := True;
sbFirst.Enabled := False;
sbPrev.Enabled := False;
sbNext.Enabled := False;
sbLast.Enabled := False;
end;
131
Listagem 4.5 Continuação
begin
case FormMode of
fmBrowse: SetBrowseButtons;
fmInsert: SetInsertButtons;
fmEdit: SetEditButtons;
end; { case }
end;
procedure TDBNavStatForm.SetStatusBar;
begin
case FormMode of
fmBrowse: stbStatusBar.Panels[1].Text := ‘Browsing’;
fmInsert: stbStatusBar.Panels[1].Text := ‘Inserting’;
fmEdit: stbStatusBar.Panels[1].Text := ‘Edit’;
end;
mmiInsert.Enabled := sbInsert.Enabled;
mmiEdit.Enabled := sbEdit.Enabled;
mmiDelete.Enabled := sbDelete.Enabled;
mmiCancel.Enabled := sbCancel.Enabled;
mmiFind.Enabled := sbFind.Enabled;
mmiNext.Enabled := sbNext.Enabled;
mmiPrevious.Enabled := sbPrev.Enabled;
mmiFirst.Enabled := sbFirst.Enabled;
mmiLast.Enabled := sbLast.Enabled;
end;
end.
O código a seguir mostra como chamar o formulário como uma janela filha:
Dê uma olhada na demonstração contida no CD-ROM. A Figura 4.1 mostra esse projeto com ins-
tâncias TDBNavStatForm de formulário filho e independentes sendo criadas. Observe que colocamos um
componente TImage no formulário para exibir melhor o formulário como um filho. A Figura 4.1 mostra
como usamos o mesmo formulário filho (aquele com a figura) como uma janela incorporada e como um
formulário separado.
Mais adiante, usaremos e expandiremos essa mesma estrutura para criar uma aplicação de banco de
dados totalmente funcional.
134
Listagem 4.6 Demonstação de frames
unit MainFrm;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, ExtCtrls;
type
TMainForm = class(TForm)
spltrMain: TSplitter;
pnlParent: TPanel;
pnlMain: TPanel;
btnFrame1: TButton;
btnFrame2: TButton;
procedure btnFrame1Click(Sender: TObject);
procedure btnFrame2Click(Sender: TObject);
private
{ Declarações privadas }
FFrame: TFrame;
public
{ Declarações públicas }
end;
var
MainForm: TMainForm;
implementation
uses Frame1Fram, Frame2Fram;
{$R *.DFM}
end.
135
Na Listagem 4.6, mostramos um formulário principal que contém dois painéis compostos de dois
painéis separados. O painel da direita servirá para conter nosso frame. Definimos dois frames separados.
O campo privado, FFrame, é uma referência a uma classe TFrame. Como nossos dois frames descendem di-
retamente de TFrame, FFrame pode se referir aos nossos dois descendentes de TFrame. Os dois botões no for-
mulário principal criam um TFrame diferente cada, e o atribuem a FFrame. O efeito é o mesmo obtido por
TChildForm. A demonstração FrameDemo.dpr está localizada no CD-ROM que acompanha este livro.
Essa instrução pode ser colocada diretamente sob a instrução a seguir, que vincula ao seu projeto o
arquivo de recursos com o mesmo nome do arquivo de projeto:
{$R *.RES}
Se você fizer isso corretamente, então poderá carregar recursos do arquivo RES usando o método
TBitmap.LoadFromResourceName( ) ou TBitmap.LoadFromResourceID( ). A Listagem 4.7 mostra a técnica usada
para carregar um mapa de bits, ícone e cursor a partir de um arquivo de recursos (RES). Você poderá en-
contrar esse projeto, Resource.dpr, no CD-ROM que acompanha este livro. Observe que as funções da
API usadas aqui – LoadIcon( ) e LoadCursor( ) – são totalmente documentadas na ajuda da API do Win-
dows.
NOTA
A API do Windows oferece uma função chamada LoadBitmap( ), que carrega um mapa de bits (como seu
nome indica). No entanto, essa função não retorna uma palheta de cores, e portanto não funciona para
carregar mapas de bits de 256 cores. Use TBitmap.LoadFromResouceName( )ou TBitmap.LoadFromResou-
ceID( ) no lugar dela.
unit MainFrm;
interface
uses
Windows, Forms, Controls, Classes, StdCtrls, ExtCtrls;
const
crXHair = 1; // Declara uma constante para o novo cursor. Esse valor
type // precisa ser um número positivo ou menor que -20.
TMainForm = class(TForm)
136 imgBitmap: TImage;
Listagem 4.7 Continuação
btnChemicals: TButton;
btnClear: TButton;
btnChangeIcon: TButton;
btnNewCursor: TButton;
btnOldCursor: TButton;
btnOldIcon: TButton;
btnAthena: TButton;
procedure btnChemicalsClick(Sender: TObject);
procedure btnClearClick(Sender: TObject);
procedure btnChangeIconClick(Sender: TObject);
procedure btnNewCursorClick(Sender: TObject);
procedure btnOldCursorClick(Sender: TObject);
procedure btnOldIconClick(Sender: TObject);
procedure btnAthenaClick(Sender: TObject);
end;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
end;
end.
crHourGlass é uma constante predefinida, indexada pelo array Cursors. Existem outras constantes de
cursor, como crBeam e crSize. Os valores de cursor existentes variam de 0 a -20 (crDefault a crHelp). Procure,
na ajuda on-line, a propriedade Cursors – você encontrará uma lista de todos os cursores disponíveis.
Você pode atribuir esses valores a Screen.Cursor quando for preciso.
Você também pode criar seus próprios cursores e incluí-los na propriedade Cursors. Para fazer isso, é
preciso primeiro definir uma constante com um valor que não entre em conflito com os cursores já dis-
poníveis. Os valores de cursor predefinidos variam de -20 a 0. Os cursores da aplicação devem usar ape-
nas números de código positivos. Todos os números de código de cursor negativos são reservados pela
Borland. Veja um exemplo:
crCrossHair := 1;
Você pode usar qualquer editor de recursos (como o Image Editor, que vem com o Delphi 5) para
criar seu cursor personalizado. É preciso salvar o cursor em um arquivo de recursos (RES). Um ponto im-
portante: você precisa dar ao seu arquivo RES um nome que seja diferente do nome do seu projeto. Lem-
bre-se de que, sempre que seu projeto é compilado, o Delphi 5 cria um arquivo RES com o mesmo nome
desse projeto. Você não vai querer que o Delphi 5 grave sobre o cursor que você criou. Ao compilar seu
projeto, verifique se o arquivo RES está no mesmo diretório dos seus arquivos-fonte, para que o Delphi 5
vincule o recurso do cursor à sua aplicação. Você diz ao Delphi 5 para vincular o arquivo RES incluindo
uma instrução como esta no arquivo DPR da aplicação:
{$R CrossHairRes.RES}
Finalmente, você precisa incluir as linhas de código a seguir para carregar o cursor, incluí-lo na pro-
priedade Cursors e depois mudar para esse cursor:
procedure TMainForm.FormCreate(Sender: Tobject);
begin
138
Screen.Cursors[crCrossHair] := LoadCursor (hInstance, ‘CROSSHAIR’);
Screen.Cursor := crCrossHair;
end;
Aqui, você está usando a função LoadCursor( ) da API do Win32 para carregar o cursor. LoadCursor( )
utiliza dois parâmetros: uma alça de instância para o módulo do qual você deseja obter o cursor e o nome
do cursor, conforme especificado no arquivo RES. Certifique-se de escrever o nome do cursor no arqui-
vo em MAIÚSCULAS!
hInstance refere-se à aplicação atualmente em execução. Em seguida, atribua o valor retornado de
LoadCursor( ) à propriedade Cursors no local especificado por crCrossHair, que foi definido anteriormente.
Por fim, atribua o cursor atual a Screen.Cursor.
Para ver um exemplo, localize o projeto CrossHair.dpr no CD-ROM que acompanha este livro. Esse
projeto carrega e altera o cursor em forma de cruz criado aqui e colocado no arquivo CrossHairRes.res.
Você também pode querer chamar o Image Editor selecionando Tools, Image Editor e abrindo o ar-
quivo CrossHairRes.res para ver como o cursor foi criado.
Nesse código, é preciso atribuir nil à variável SomeForm depois que ela tiver sido destruída. Caso con-
trário, o método Assigned( ) não funcionará corretamente, e o método falhará. No entanto, isso não fun-
cionaria para um formulário não-modal. Com formulários não-modais, você não pode determinar no
código quando o formulário será destruído. Portanto, você precisa criar a atribuição de nil dentro do
manipulador de evento OnDestroy do formulário sendo destruído. Esse método foi descrito anteriormente
neste capítulo.
139
Listagem 4.8 O arquivo Initialize.dpr, mostrando a inicialização do projeto
program Initialize;
uses
Forms,
Dialogs,
Controls,
MainFrm in ‘MainFrm.pas’ {MainForm};
{$R *.RES}
var
Password: String;
begin
if InputQuery(‘Password’, ‘Enter your password’, PassWord) then
if Password = ‘D5DG’ then
begin
// Outras rotinas de inicialização podem entrar aqui.
Application.CreateForm(TMainForm, MainForm);
Application.Run;
end
else
MessageDlg(‘Incorrect Password, terminating program’, mtError, [mbok], 0);
end.
unit MainFrm;
interface
uses
SysUtils, Windows, Messages, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls, AppEvnts, Buttons;
140
Listagem 4.9 Continuação
type
ENotSoBadError = class(Exception);
EBadError = class(Exception);
ERealBadError = class(Exception);
TMainForm = class(TForm)
btnNotSoBad: TButton;
btnBad: TButton;
btnRealBad: TButton;
appevnMain: TApplicationEvents;
procedure btnNotSoBadClick(Sender: TObject);
procedure btnBadClick(Sender: TObject);
procedure btnRealBadClick(Sender: TObject);
procedure appevnMainException(Sender: TObject; E: Exception);
public
end;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
end.
DICA
Se a caixa de seleção Stop on Delphi Exceptions (interromper nas exceções do Delphi) estiver selecionada
na página Language Exceptions (exceções da linguagem) da caixa de diálogo Debugger Options (opções
do depurador) – acessada por meio de Tools, Debugger Options no menu –, então o depurador do IDE do
Delphi 5 informará a exceção na sua própria caixa de diálogo, antes que sua aplicação tenha a chance de
interceptá-la. Embora seja útil para depuração, essa caixa de seleção selecionada poderá ser incômoda
quando você quiser ver como o seu projeto cuida das exceções. Desative a opção para fazer com que seu
projeto seja executado normalmente.
1. Depois de criar o formulário principal da sua aplicação, crie outro formulário para representar a tela
de abertura. Chame esse formulário de SplashForm.
2. Use o menu Project, Options para garantir que SplashForm não esteja na lista de Auto-Create.
3. Atribua bsNone à propriedade BorderStyle de SplashForm e [ ] à sua propriedade BorderIcons.
4. Coloque um componente TImage em SplashForm e atribua alClient à propriedade Align da imagem.
5. Carregue um mapa de bits no componente TImage selecionando sua propriedade Picture.
Agora que você criou a tela de abertura, só precisa editar o arquivo DPR do projeto para exibi-la. A
Listagem 4.10 mostra o arquivo de projeto (DPR) para o qual a tela de abertura é exibida. Você encon-
trará esse projeto, Splash.dpr, no CD-ROM que acompanha este livro.
142
Listagem 4.10 Um arquivo DPR com uma tela de abertura
program splash;
uses
Forms,
MainFrm in ‘MainFrm.pas’ {MainForm},
SplashFrm in ‘SplashFrm.pas’ {SplashForm};
{$R *.RES}
begin
Application.Initialize;
{ Cria a tela de abertura }
SplashForm := TSplashForm.Create(Application);
SplashForm.Show; // Apresenta a tela de abertura
SplashForm.Update; // Atualiza a tela de abertura para garantir que
// ela será desenhada
Application.CreateForm(TMainForm, MainForm);
SplashForm.Hide; // Oculta a tela de abertura
SplashForm.Free; // Libera a tela de abertura
Application.Run;
end.
Esse é simplesmente um modo de simular um processo longo. Um componente TTimer foi colocado
em SplashForm, e sua propriedade Interval foi definida como 3000. Quando ocorre o evento OnTimer do
componente TTimer, após cerca de três segundos, ele executa a seguinte linha:
tmMainTimer.Enabled := False;
Isso fará com que a condição do loop while seja False, fazendo com que a execução saia do loop.
unit BlueBackFrm;
interface
143
Listagem 4.11 Continuação
uses
SysUtils, Windows, Messages, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, Buttons, ExtCtrls;
type
TBlueBackForm = class(TForm)
pnlMain: TPanel;
bbtnOK: TBitBtn;
bbtnCancel: TBitBtn;
procedure FormResize(Sender: TObject);
private
Procedure CenterPanel;
{ cria um manipulador para a mensagem WM_WINDOWPOSCHANGING }
procedure WMWindowPosChanging(var Msg: TWMWindowPosChanging);
message WM_WINDOWPOSCHANGING;
end;
var
BlueBackForm: TBlueBackForm;
implementation
uses Math;
{$R *.DFM}
procedure TBlueBackForm.CenterPanel;
{ Este procedimento centraliza o painel principal horizontalmente e
verticalmente dentro da área do cliente do formulário
}
begin
{ Centraliza horizontalmente }
if pnlMain.Width < ClientWidth then
pnlMain.Left := (ClientWidth - pnlMain.Width) div 2
else
pnlMain.Left := 0;
{ Centraliza verticalmente }
if pnlMain.Height < ClientHeight then
pnlMain.Top := (ClientHeight - pnlMain.Height) div 2
else
pnlMain.Top := 0;
end;
144
Listagem 4.11 Continuação
inherited;
end;
end.
Na verdade, você pode ainda remover a cláusula uses e as chamadas para Application.Initialize e
Application.Run:
program Project1;
begin
end.
Esse é um projeto sem muita utilidade, mas lembre-se de que você pode incluir o que quiser no blo-
co begin..end, o que seria o ponto de partida de uma aplicação de console para Win32.
Saindo do Windows
Um motivo para você querer sair do Windows a partir de uma aplicação é porque a sua aplicação fez algu-
mas mudanças de configuração no sistema que não entrarão em vigor até que o usuário reinicialize o Win- 145
dows. Em vez de pedir que o usuário faça isso pelo Windows, sua aplicação poderá perguntar se o usuário
deseja sair do Windows; ela mesma poderá então cuidar de todo esse trabalho sujo. No entanto, lembre-se
de que exigir a reinicialização do sistema é considerado um mau procedimento, e deve ser evitado.
Para sair do Windows, você precisa usar uma destas duas funções da API do Windows: ExitWin-
dows( ) ou ExitWindowsEx( ).
A função ExitWindows( ) vem dos tempos do Windows de 16 bits. Nessa versão anterior do Win-
dows, você podia especificar várias opções que permitiam reinicializar o Windows após a saída. No en-
tanto, no Win32, essa função apenas registra o usuário ativo do Windows e permite que outro usuário se
conecte à próxima sessão do Windows.
ExitWindows( ) foi substituído pela nova função ExitWindowsEx( ). Com essa função, você pode se des-
conectar, encerrar o Windows ou encerrar o Windows e reiniciar o sistema (dar novo boot). A Listagem
4.12 mostra o uso das duas funções.
unit MainFrm;
interface
uses
SysUtils, Windows, Messages, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, ExtCtrls;
type
TMainForm = class(TForm)
btnExit: TButton;
rgExitOptions: TRadioGroup;
procedure btnExitClick(Sender: TObject);
end;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
end.
A Listagem 4.12 usa o valor de um botão de opção para determinar qual opção de saída do Win-
dows será usada. A primeira opção usa ExitWindows( ) para desconectar o usuário e reinicializar o Windows,
146 perguntando se o usuário deseja se conectar novamente.
As outras opções usam a função ExitWindowsEx( ). A segunda opção encerra o Windows e reinicializa
o sistema. A terceira opção sai do Windows e encerra o sistema, para que o usuário possa desligar o com-
putador. A quarta opção realiza a mesma tarefa da primeira, mas utiliza a função ExitWindowsEx( ).
Tanto ExitWindows( ) quanto ExitWindowsEx( ) retornam True se tiver sucesso e False em caso contrá-
rio. Você pode usar a função Win32Check( ) de SysUtils.pas, que chama a função GetLastError( ) da API do
Win32 e apresenta o texto do erro, caso tenha havido algum erro.
NOTA
Se você estiver executando o Windows NT, a função ExitWindowsEx( ) não encerrará o sistema; isso exige
um privilégio especial. Você deve usar a função AdjustTokenPrivleges( ) da API do Win32 para ativar o
privilégio SE_SHUTDOWN_NAME. Outras informações sobre esse assunto poderão ser encontradas na ajuda
on-line do Win32.
Você encontrará um exemplo desse código no projeto ExitWin.dpr, no CD-ROM que acompanha
este livro.
Definindo CanClose como False, você diz ao Windows para não encerrar o sistema. Outra opção é
definir CanClose como True apenas depois de lhe pedir para salvar um arquivo, se for preciso. Você verá
uma demonstração disso no projeto NoClose.dpr, que se encontra no CD-ROM deste livro.
NOTA
Se você estiver rodando um projeto sem formulário, terá que subclassificar o procedimento de janela dessa
aplicação e capturar a mensagem WM_QUERYENDSESSION que é enviada para cada aplicação sendo executa-
da sempre que ExitWindows( ) ou ExitWindowsEx( ) for chamado por qualquer aplicação. Se a aplicação
retornar um valor diferente de zero vindo dessa mensagem, a aplicação poderá ser encerrada com suces-
so. A aplicação deverá retornar zero para impedir que o Windows seja encerrado. Você aprenderá mais so-
bre o processamento de mensagens do Windows no Capítulo 5.
Resumo
Este capítulo focaliza as técnicas de gerenciamento e os aspectos de arquitetura do projeto. Ele discute os
principais componentes que compõem a maioria dos projetos em Delphi 5: TForm, TApplication e TScreen.
Demonstramos como você pode iniciar o projeto de suas aplicações desenvolvendo primeiro uma arqui-
tetura comum. O capítulo também mostra várias rotinas úteis para a sua aplicação.
147
As mensagens do CAPÍTULO
Windows
5
NE STE C AP ÍT UL O
l O que é uma mensagem? 149
l Tipos de mensagens 150
l Como funciona o sistema de mensagens do
Windows 150
l O sistema de mensagens do Delphi 151
l Tratamento de mensagens 152
l Como enviar suas próprias mensagens 156
l Mensagens fora do padrão 157
l Anatomia de um sistema de mensagens:
a VCL 161
l Relacionamento entre mensagens e eventos 167
l Resumo 167
Embora os componentes da Visual Component Library (VCL) exponham muitas mensagens do Win32
por meio de eventos do Object Pascal, torna-se ainda essencial que você, o programador Win32, com-
preenda como funciona o sistema de mensagens do Windows.
Como um programador de aplicações do Delphi, você descobrirá que os eventos providos pela
VCL vão se ajustar à maioria de suas necessidades; apenas ocasionalmente você precisará mergulhar no
mundo do tratamento de mensagens do Win32. Já como um programador de componentes do Delphi,
entretanto, você e as mensagens se tornarão grandes amigos porque você terá de manipular diretamente
várias mensagens do Windows e chamar eventos correspondentes àquelas mensagens.
Tipos de mensagens
A API do Win32 define previamente uma constante para cada mensagem do Windows. Essas constantes
são os valores guardados no campo de mensagem do registro TMsg. Todas essas constantes são definidas
na unidade Messages do Delphi; a maioria está também descrita no ajuda on-line. Observe que cada uma
dessas constantes inicia com as letras WM, que significam Windows Message (mensagem do Windows). A
Tabela 5.1 lista algumas mensagens comuns do Windows, juntamente com seus significados e valores.
NOTA
Uma função de callback é uma função no seu programa que é chamada pelo Windows ou por algum outro
módulo externo.
150
Apanhar uma mensagem no ponto A (algum evento ocorre, criando uma mensagem) e levando-a
até o ponto B (uma janela na sua aplicação responde à mensagem) é um processo de cinco passos:
1. Algum evento ocorre no sistema.
2. O Windows traduz esse evento em uma mensagem e a coloca na fila de mensagens da sua aplicação.
3. Sua aplicação recupera a mensagem da fila e a coloca em um registro TMsg.
4. Sua aplicação encaminha a mensagem para o procedimento de janela da janela apropriada na aplicação.
5. O procedimento de janela realiza alguma ação em resposta à mensagem.
As etapas 3 e 4 constituem o loop de mensagens da aplicação. O loop de mensagens normalmente é
considerado como o coração de um programa do Windows, por ser a facilidade que capacita um progra-
ma a responder a eventos externos. O loop de mensagens passa sua vida inteira trazendo mensagens da
fila da aplicação e as enviando às janelas apropriadas na sua aplicação. Se não houver nenhuma mensa-
gem na fila da sua aplicação, o Windows permitirá então que outras aplicações processem suas mensa-
gens. A Figura 5.1 mostra essas etapas.
Loop de Procedimento
Alguma coisa
mensagens de janela
151
Observe que o registro TMessage possui um pouco menos informações que um TMsg. Isso acontece
porque o Delphi internaliza os outros campos TMsg; TMessage contém apenas as informações essenciais de
que você precisa para manipular uma mensagem.
É importante notar que o registro TMsg também contém um campo Result. Como já foi mencionado
anteriormente, algumas mensagens exigem que o procedimento de janela retorne algum valor após pro-
cessar uma mensagem. Com o Delphi, você executa esse processo de um modo direto colocando o valor
de retorno no campo Result de TMessage. Esse processo é explicado com detalhes na seção intitulada “De-
signando valores de resultados de mensagens”, mais adiante.
Todos os tipos de registro para mensagens específicas do mouse (WM_LBUTTONDOWN e WM_RBUTTONUP, por
exemplo) estão simplesmente definidas como iguais a TWMMouse, como no exemplo a seguir:
TWMRButtonUp = TWMMouse;
TWMLButtonDown = TWMMouse;
NOTA
Um registro de mensagem é definido para quase toda mensagem-padrão do Windows. A convenção de
nomes estabelece que o nome do registro deva ser o mesmo nome da mensagem antecedido de um T, utili-
zando maiúsculas alternadas e sem o sublinhado. Por exemplo, o nome do tipo de registro de mensagem
para uma mensagem WM_SETFONT é TWMSetFont.
A propósito, TMessage funciona com todas as mensagens em todas as situações, mas não é tão conve-
niente quanto os registros específicos da mensagem.
Tratamento de mensagens
Manipular ou processar uma mensagem significa que sua aplicação responde de alguma maneira à mensa-
gem do Windows. Numa aplicação-padrão do Windows, o tratamento de mensagem é executado em
cada procedimento de janela. Internalizando o procedimento de janela, no entanto, o Delphi faz com
que se torne bem mais fácil manipular mensagens individuais; em vez de se ter um procedimento que ma-
nipule todas as mensagens, cada mensagem possui seu próprio procedimento. Três requisitos são neces-
sários para que um procedimento seja um procedimento de tratamento de mensagem:
152
l O procedimento deve ser um método de um objeto.
l O procedimento deve tomar um único parâmetro var de TMessage ou outro tipo de registro espe-
cífico da mensagem.
l O procedimento deve utilizar a diretiva TMessage seguida pelo valor constante da mensagem que
você queira processar.
Aqui está um exemplo de um procedimento que manipula mensagens WM_PAINT:
procedure WMPaint(var Msg: TWMPaint); message WM_PAINT;
NOTA
Quando da nomeação dos procedimentos de tratamento de mensagens, a regra é dar a eles o mesmo
nome da mensagem em si, usando maiúsculas alternadas e sem o sublinhado.
Agora acrescente a definição da função na parte implementation dessa unidade. Lembre-se de usar o
operador ponto para definir o escopo desse procedimento como um método de TForm1. Não utilize a dire-
tiva message como parte da implementação da função:
procedure TForm1.WMPaint(var Msg: TWMPaint);
begin
Beep;
inherited;
end;
Observe o uso da palavra-chave inherited aqui. Chame inherited quando você quiser passar a mensa-
gem para o manipulador de objetos ancestrais. Chamando inherited nesse exemplo, você encaminha a
mensagem para o manipulador WM_PAINT de TForm.
NOTA
Ao contrário das chamadas normais para métodos herdados, aqui você não precisa dar o nome do método
herdado. Isso acontece porque o método não é importante quando é despachado. O Delphi sabe qual méto-
do deve chamar baseado no valor da mensagem utilizado com a diretiva message na interface da classe.
A unidade principal na Listagem 5.1 fornece um exemplo simples de um formulário que processa a
mensagem WM_PAINT. A criação desse projeto é fácil: simplesmente crie um projeto novo e acrescente um
código do procedimento WMPaint para o objeto TForm.
unit GMMain;
interface
uses 153
Listagem 5.1 Continuação
type
TForm1 = class(TForm)
private
procedure WMPaint(var Msg: TWMPaint); message WM_PAINT;
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
end.
Sempre que uma mensagem WM_PAINT aparecer, ela é passada para o procedimento WMPaint. O proce-
dimento WMPaint simplesmente informa quanto à mensagem WM_PAINT fazendo algum ruído com o procedi-
mento MessageBeep( ) e depois passando a mensagem para o manipulador herdado.
154
Tratamento de mensagens: não sem acordo
Ao contrário de responder a eventos do Delphi, manipular mensagens do Windows não é “sem acordo”.
Geralmente, quando você decide manipular uma mensagem sozinho, o Windows espera que você execu-
te alguma ação ao processar tal mensagem. Na maioria das vezes, a VCL possui boa parte desse processa-
mento básico de mensagem embutido – tudo que você precisa fazer é chamar inherited para acessá-lo.
Pense dessa forma: você elabora um manipulador de mensagens de forma que sua aplicação faça aquilo
que você espera, e chama inherited para que sua aplicação faça as coisas adicionais que o Windows espera
que ele faça.
NOTA
A natureza contratual do tratamento de mensagens pode ser mais do que apenas chamar o manipulador her-
dado. Nos manipuladores de mensagens, você às vezes se vê restrito quanto ao que pode fazer. Por exemplo,
numa mensagem WM_KILLFOCUS não dá para se definir o foco em outro controle sem causar uma pane.
Para fazer uma demonstração dos elementos inherited, tente executar o programa da Listagem 5.1
sem chamar inherited no método WMPaint( ). Apenas remova a linha que chama inherited de forma que o
procedimento se pareça assim:
procedure TForm1.WMPaint(var Msg: TWMPaint);
begin
MessageBeep(0);
end;
Como você nunca dá ao Windows uma chance de realizar tratamentos básicos da mensagem
WM_PAINT,o formulário nunca será desenhado por conta própria.
Às vezes poderá haver circunstâncias em que você não vai querer chamar o manipulador de mensa-
gens herdadas. Um exemplo é manipular as mensagens WM_SYSCOMMAND para impedir que uma janela seja mi-
nimizada ou maximizada.
155
O evento OnMessage do tipo TApplication
Outra técnica para se manipular mensagens é utilizar o evento OnMessage do TApplication. Quando você de-
signa um procedimento para OnMessage, esse procedimento é chamado sempre que uma mensagem é reti-
rada da fila e estiver a ponto de ser processada. Esse manipulador de evento é chamado antes mesmo de o
próprio Windows ter uma chance de processar a mensagem. O manipulador de evento Application.OnMes-
sage é do tipo TMessageEvent e deve ser definido com uma lista de parâmetros, como mostra o exemplo a
seguir:
procedure AlgumObjeto.AppMessageHandler(var Msg: TMsg;
var Handled: Boolean);
Todos os parâmetros de mensagens são passados para o manipulador do evento OnMessage no parâ-
metro Msg. (Observe que esse parâmetro pertence ao tipo de registro TMsg do Windows, descrito anterior-
mente neste capítulo.) O campo Handled exige que você designe um valor booleano indicando se já mani-
pulou a mensagem.
O primeiro passo para se criar um manipulador de evento OnMessage é criar um método que aceite a
mesma lista de parâmetros que um TMessageEvent. Por exemplo, aqui temos um método que fornece uma
contagem atual de quantas mensagens sua aplicação recebe:
var
NumMessages: Integer;
Uma limitação de OnMessage é ser executada apenas para mensagens retiradas da fila e não para men-
sagens enviadas diretamente para os procedimentos de janela das janelas da sua aplicação. O Capítulo 13
aponta algumas técnicas para se contornar essa limitação através de um maior aprofundamento no pro-
cedimento de janela da aplicação.
DICA
OnMessage observa todas as mensagens endereçadas a todas as alças de janela na sua aplicação. Esse é o
evento mais ocupado da sua aplicação (milhares de mensagens por segundo); então, não faça nada num
manipulador OnMessage que leve muito tempo ou você poderá retardar toda a sua aplicação. Na verdade,
esse é um lugar no qual um ponto de interrupção seria uma péssima idéia.
156
O método Perform( )
A VCL oferece o método Perform( ) para todos os descendentes de Tcontrol; Perform( ) permite enviar
uma mensagem para qualquer formulário ou objeto de controle que tenha sido solicitado. O método Per-
form( ) toma três parâmetros – uma mensagem e seu Iparam e wParam correspondentes – e é definida da se-
guinte maneira:
function TControl.Perform(Msg: Cardinal; WParam, LParam: Longint):
Longint;
Para enviar uma mensagem para um formulário ou controle, utilize a seguinte sintaxe:
RetVal := ControlName.Perform(MessageID, wParam, lParam);
Depois que você chamar Perform( ), ele não retorna até que a mensagem tenha sido manipulada. O
método Perform( ) empacota seus parâmetros num registro TMessage e em seguida chama o método Dis-
patch( ) do objeto para enviar a mensagem – criando um atalho para o sistema de mensagens da API do
Windows. O método Dispatch( ) é descrito mais tarde neste capítulo.
NOTA
Embora SendMessage( ) e PostMessage( ) sejam usadas semelhantemente, seus respectivos valores de
retorno são diferentes. SendMessage( ) retorna o valor do resultado da mensagem sendo processada,
mas a PostMessage( ) retorna apenas um BOOL que indica se a mensagem foi posicionada na fila da jane-
la de destino.
Notificação Significado
Notificação de botão
BN_CLICKED O usuário deu um clique num botão.
BN_DISABLE Um botão foi desativado.
BN_DOUBLECLICKED O usuário deu um clique duplo em um botão.
BN_HILITE O usuário destacou um botão.
BN_PAINT O botão deve ser pintado.
BN_UNHILITE O destaque deve ser removido.
Notificação da caixa de combinação
CBN_CLOSEUP A caixa de listagem de uma caixa de combinação se fechou.
CBN_DBLCLK O usuário deu um clique duplo numa string.
CBN_DROPDOWN A caixa de listagem de uma caixa de combinação está descendo.
CBN_EDITCHANGE O usuário mudou o texto no controle de edição.
CBN_EDITUPDATE O texto alterado está a ponto de ser exibido.
CBN_ERRSPACE A caixa de combinação está sem memória.
CBN_KILLFOCUS A caixa de combinação está perdendo o foco de entrada.
CBN_SELCHANGE Uma nova listagem da caixa de combinação é selecionada.
CBN_SELENDCANCEL A seleção do usuário deve ser cancelada.
CBN_SELENDOK A seleção do usuário é válida.
CBN_SETFOCUS A caixa de combinação está recebendo o foco da entrada.
Notificação de edição
EN_CHANGE O monitor é atualizado após mudanças no texto.
EN_ERRSPACE O controle de edição está fora de memória.
EN_HSCROLL O usuário deu um clique duplo na barra de rolagem horizontal.
EN_KILLFOCUS O controle de edição está perdendo o foco da entrada.
EN_MAXTEXT A inserção está truncada.
EN_SETFOCUS O controle de edição está recebendo o foco da entrada.
EN_UPDATE O controle de edição está a ponto de exibir texto alternado.
EN_VSCROLL O usuário deu um clique na barra de rolagem vertical.
158
Tabela 5.2 Continuação
Notificação Significado
begin
SomeForm.Perform(SX_MYMESSAGE, 0, 0);
{ ou }
SendMessage(SomeForm.Handle, SX_MYMESSAGE, 0, 0);
{ ou }
PostMessage(SomeForm.Handle, SX_MYMESSAGE, 0, 0);
. 159
.
.
end;
Como você pode ver, há pouca diferença entre usar uma mensagem definida pelo usuário na sua
aplicação e manipular qualquer mensagem-padrão do Windows. O ponto-chave aqui é começar em
WM_USER + 100 para mensagens interaplicação e dar a cada mensagem um nome que tenha algo a ver com
sua finalidade.
ATENÇÃO
Nunca envie mensagens com valores de WM_USER a $7FFF a menos que você esteja certo de que o recipiente
pretendido esteja preparado para manipular a mensagem. Como cada janela pode definir esses valores
independentemente, é grande a possibilidade de que coisas indesejadas aconteçam, a não ser que você
tome bastante cuidado no que diz respeito a quais recipientes você envia mensagens de WM_USER a $7FFF.
NOTA
O número retornado por RegisterWindowMessage( ) varia entre as sessões do Windows e não pode ser de-
terminado até o momento da execução.
160
Difundindo mensagens
Os descendentes do TWinControl podem difundir um registro de mensagem para cada um de seus próprios
controles – graças ao método Broadcast( ). Tal técnica é útil quando você precisa enviar a mesma mensa-
gem para um grupo de componentes. Por exemplo, para mandar uma mensagem definida pelo usuário,
chamada um_Foo, para todos os controles próprios de Panel1, utilize o seguinte código:
var
M: TMessage;
begin
with M do
begin
Message := UM_FOO;
wParam := 0;
lParam := 0;
Result := 0;
end;
Panel1.Broadcast(M);
end;
161
NOTA
Você deve sempre chamar inherited quando estiver manipulando mensagens, a não ser que esteja abso-
lutamente certo de que queira impedir o processamento normal da mensagem.
DICA
Como todas as mensagens não-manipuladas fluem para o DefaultHandler( ), esse é geralmente o melhor
lugar para se manipular mensagens entre aplicações nas quais os valores foram obtidos por meio do pro-
cedimento RegisterWindowMessage( ).
Para melhor entender o sistema de mensagens da VCL, crie um pequeno programa que possa mani-
pular uma mensagem em um estágio de Application.OnMessage, WndProc( ) ou DefaultHandler( ). Esse projeto
é chamado CatchIt; seu formulário principal está ilustrado na Figura 5.3.
Os manipuladores de evento OnClick para PostMessButton e SendMessButton são mostrados no próximo
trecho de código. O primeiro utiliza PostMessage( ) para postar uma mensagem definida pelo usuário para
um formulário; o segundo utiliza SendMessage( ) para enviar uma mensagem definida pelo usuário a um
formulário. Para diferenciar entre postar e enviar, observe que o valor 1 é passado no wParam de PostMessa-
ge( ) e que o valor 0 (zero) é passado para SendMessage( ). Eis aqui o código:
WndProc de
Mensagem
AlgumaClasse
Dispatch de
AlgumaClasse
Manipulador default
de AlgumaClasse
unit CIMain;
interface
uses
SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls, ExtCtrls, Menus;
const
SX_MYMESSAGE = WM_USER; // Valor de mensagem definido por usuário
MessString = ‘%s message now in %s.’; // String para alertar usuário
type
TMainForm = class(TForm)
GroupBox1: TGroupBox;
PostMessButton: TButton;
WndProcCB: TCheckBox;
MessProcCB: TCheckBox;
DefHandCB: TCheckBox;
SendMessButton: TButton;
AppMsgCB: TCheckBox;
EatMsgCB: TCheckBox;
EatMsgGB: TGroupBox;
OnMsgRB: TRadioButton;
WndProcRB: TRadioButton;
MsgProcRB: TRadioButton;
DefHandlerRB: TRadioButton;
procedure PostMessButtonClick(Sender: TObject);
procedure SendMessButtonClick(Sender: TObject);
procedure EatMsgCBClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure AppMsgCBClick(Sender: TObject);
private
{ Trata da mensagem em nível de aplicação }
procedure OnAppMessage(var Msg: TMsg; var Handled: Boolean);
{ Trata da mensagem em nível de WndProc }
procedure WndProc(var Msg: TMessage); override;
{ Trata da mensagem após despacho }
procedure SXMyMessage(var Msg: TMessage); message SX_MYMESSAGE;
{ Manipulador de mensagens default }
procedure DefaultHandler(var Msg); override;
end; 163
Listagem 5.2 Continuação
var
MainForm: TMainForm;
implementation
{$R *.DFM}
const
// strings que vão indicar se uma mensagem é enviada ou postada
SendPostStrings: array[0..1] of String = (‘Sent’, ‘Posted’);
begin
if WndProcCB.Checked then // se caixa de seleção WndProcCB estiver marcada...
begin
// Informa ao usuário sobre a mensagem.
ShowMessage(Format(MessString, [SendPostStrings[Msg.WParam],
‘WndProc’]));
// Chama o herdado apenas se não formos digerir a mensagem.
CallInherited := not WndProcRB.Checked;
end;
end;
if CallInherited then inherited WndProc(Msg);
end;
end.
ATENÇÃO
Embora não haja problema em utilizar apenas a palavra-chave inherited para mandar a mensagem para
um manipulador herdado em procedimentos do manipulador de mensagens, essa técnica não funciona
com WndProc( ) ou DefaultHandler( ). Com esses procedimentos, você deve também fornecer o nome da
função ou procedimento herdado, como nesse exemplo:
inherited WndProc(Msg);
166
Você deve ter notado que o procedimento DefaultHandler( ) é um tanto quanto incomum na medida
em que ele toma um parâmetro var sem tipo. Isso acontece porque o DefaultHandler( ) presume que a pri-
meira palavra no parâmetro seja o número da mensagem; ele não está preocupado com o restante da in-
formação que está sendo passada. Por causa disso, você coage o parâmetro como um TMessage de forma
que os parâmetros da mensagem possam ser acessados.
OnActivate WM_ACTIVATE
OnClick WM_XBUTTONDOWN
OnCreate WM_CREATE
OnDblClick WM_XBUTTONDBLCLICK
OnKeyDown WM_KEYDOWN
OnKeyPress WM_CHAR
OnKeyUp WM_KEYUP
OnPaint WM_PAINT
OnResize WM_SIZE
OnTimer WM_TIMER
A Tabela 5.3 é uma boa referência de regra prática quando você estiver procurando eventos que
correspondam diretamente a mensagens.
DICA
Nunca escreva um manipulador de mensagens quando você puder utilizar um evento predefinido para fa-
zer a mesma coisa. Devido à natureza sem necessidade de acordo dos eventos, você encontrará menos
problemas manipulando eventos do que manipulando mensagens.
Resumo
Neste ponto, você já deve ter um entendimento bastante claro de como funciona o sistema de mensa-
gens do Win32 e de como a VCL encapsula esse sistema de mensagens. Embora o sistema de eventos do
Delphi seja ótimo, é essencial que todo programador Win32 sério saiba como funcionam as mensagens.
Se você estiver ansioso para aprender mais sobre o tratamento de mensagens do Windows, examine
o Capítulo 21. Nesse capítulo, você encontra uma aplicação prática do conhecimento que adquiriu neste
capítulo. No próximo capítulo, você aprenderá a elaborar seu código do Delphi em um conjunto de pa-
drões, de modo a facilitar as práticas de codificação lógicas e compartilhar o código-fonte.
167
Documento de padrões CAPÍTULO
de codificação
6
NE STE C AP ÍT UL O
l Introdução
l Regras gerais de formatação sobre o código-fonte
l Object Pascal
l Arquivos
l Formulários e módulos de dados
l Pacotes
l Componentes
169
Controles ActiveX CAPÍTULO
com Delphi
7
NE STE C AP ÍT UL O
l O que é um controle ActiveX?
l Quando deve ser utilizado um controle ActiveX
l Inclusão de um controle ActiveX na Component
Palette
l O wrapper de componentes do Delphi
l Usando controles ActiveX em suas aplicações
l Distribuindo aplicações equipadas com controle
ActiveX
l Registro do controle ActiveX
l BlackJack: um exemplo de aplicação OCX
l Resumo
NOTA
O Delphi 1 foi a última versão do Delphi a dar suporte para controles VBX (Visual Basic Extension). Se você
tiver um projeto do Delphi 1 que se baseie em um ou mais controles VBX, verifique com os fornecedores de
VBX para saber se eles fornecem uma solução ActiveX compatível para usar em suas aplicações Delphi de
32 bits.
171
Técnicas PARTE
Avançadas
II
NE STA PART E
8 Programação gráfica com GDI e fontes 175
176
Bibliotecas de vínculo CAPÍTULO
dinâmico (DLLs)
9
NE STE C AP ÍT UL O
l O que é exatamente uma DLL? 178
l Vínculo estático comparado ao vínculo
dinâmico 180
l Por que usar DLLs? 181
l Criação e uso de DLLs 182
l Exibição de formulários sem modo a partir
de DLLs 186
l Uso de DLLs nas aplicações em Delphi 188
l Carregamento explícito de DLLs 189
l Função de entrada/saída da biblioteca de vínculo
dinâmico 192
l Exceções em DLLs 196
l Funções de callback 197
l Chamada das funções de callback a partir de
suas DLLs 200
l Compartilhamento de dados da DLL por diferentes
processos 203
l Exportação de objetos a partir de DLLs 209
l Resumo 213
Este capítulo explica as bibliotecas de vínculo dinâmico do Win32, também conhecidas como DLLs. As
DLLs correspondem a um componente-chave para a gravação de quaisquer aplicações do Windows. Este
capítulo abrange os vários aspectos do uso e criação de DLLs. Fornece uma visão geral de como as DLLs
funcionam e aborda como criar e usar DLLs. Você aprenderá métodos diferentes de carregamento de
DLLs e vínculo com os procedimentos e funções por elas exportados. Este capítulo também abrange o
uso das funções de callback e ilustra como compartilhar os dados da DLL entre diferentes processos de
chamada.
NOTA
O Delphi introduz uma DLL com objetivo especial conhecido como um pacote, que é utilizado em ambien-
tes Delphi e C++Builder. Apresentaremos mais detalhadamente os pacotes no Capítulo 21.
As DLLs compartilham seu código com outras aplicações por meio de um processo denominado
vínculo dinâmico, o qual será explicado mais adiante neste capítulo. Em geral, quando uma aplicação
usar uma DLL, o sistema Win32 garantirá que apenas uma cópia da DLL irá residir na memória. Faz isso
utilizando os arquivos mapeados na memória. A DLL é primeiramente carregada no heap global do siste-
ma Win32. Em seguida, é mapeada no espaço de endereços do processo de chamada. No sistema Win32,
cada processo recebe seu próprio espaço de endereço linear de 32 bits. Quando a DLL é carregada por
múltiplos processos, cada processo recebe sua própria imagem da DLL. Portanto, os processos não com-
partilham o mesmo código físico, dados ou recursos, como no caso do Windows de 16 bits. No Win32, a
DLL aparece como se fosse realmente pertencente por código ao processo de chamada. Para obter mais
informações sobre as construções do Win32, consulte o Capítulo 3.
Isso não significa que quando múltiplos processos carregam uma DLL, a memória física é consumida
em cada utilização da DLL. A imagem da DLL é colocada no espaço de endereços de cada processo ao mape-
ar sua imagem a partir do heap global do sistema ao espaço de endereços de cada processo que usar a DLL,
178 pelo menos no caso ideal (consulte a barra lateral “Definição de um endereço de base preferencial da DLL”).
Definição de um endereço de base preferencial da DLL
O código da DLL somente será compartilhado entre processos se a DLL puder ser carregada no espaço
de endereços do processo de todos os clientes interessados no endereço de base preferencial da DLL.
Se o endereço de base preferencial e o intervalo de DLL forem sobrepostos por algo já alocado em um
processo, o carregador do Win32 terá que realocar toda a imagem da DLL para algum outro endereço
de base. Quando isso acontecer, nenhuma imagem da DLL realocada será compartilhada por qual-
quer outro processo no sistema – cada instância da DLL realocada consome seu próprio bloco de me-
mória física e espaço de arquivo de permuta.
É importante definir o endereço de base de cada DLL criada a um valor que não entre em conflito
e nem seja sobreposto por outros intervalos de endereço usados por sua aplicação com a diretiva
$IMAGEBASE.
Se sua DLL tiver que ser usada por múltiplas aplicações, escolha um endereço de base exclusivo
que provavelmente não irá colidir com os endereços da aplicação na extremidade inferior do intervalo
de endereços virtuais do processo ou com as DLLs comuns (como pacotes da VCL) na extremidade su-
perior do intervalo de endereços. O endereço de base padrão de todos os arquivos executáveis (EXEs e
DLLs) é $400000, ou seja, sempre colidirá com o endereço de base de seu host EXE, a menos que você
mude o endereço de base de sua DLL e, portanto, nunca será compartilhada entre os processos.
Há um outro benefício do carregamento de endereços de base. Já que a DLL não requer realoca-
ção ou correções (o que geralmente ocorre) e por ser armazenada em uma unidade de disco local, as
páginas de memória da DLL serão mapeadas diretamente para o arquivo da DLL no disco. O código
da DLL não consome qualquer espaço no arquivo de paginação do sistema (também chamado arqui-
vo de troca de página). Por isso, o total das estatísticas de tamanho e contagem de páginas compro-
metidas do sistema pode ser bem maior do que o arquivo de permuta do sistema mais a RAM.
Você encontrará informações detalhadas sobre como utilizar a diretiva $IMAGEBASE consultando
“Image Base Address” (Endereço de base da imagem) na ajuda on-line do Delphi 5.
A seguir, vemos alguns termos que você precisará conhecer relacionados às DLLs:
l Aplicação. Um programa do Windows localizado em um arquivo .exe.
l Executável. Um arquivo contendo o código executável. Arquivos executáveis incluem .dll e
.exe.
179
Vínculo estático comparado ao vínculo dinâmico
Vínculo estático refere-se ao método pelo qual o compilador do Delphi soluciona uma chamada de fun-
ção ou procedimento para seu código executável. O código da função pode existir no arquivo .dpr da
aplicação ou em uma unidade. Ao vincular suas aplicações, essas funções e procedimentos tornam-se par-
te do arquivo executável final. Em outras palavras, no disco, cada função irá residir num local específico
do arquivo .exe do programa.
O local de uma função também é predeterminado para um local relativo ao local onde o programa
está carregado na memória. Quaisquer chamadas para tal função fazem com que a execução do progra-
ma pule para onde a função reside, execute a função e, em seguida, retorne para o local no qual foi cha-
mada. O endereço relativo da função é determinado durante o processo de vínculo.
Essa é uma descrição vaga de um processo mais complexo que o compilador do Delphi usa para
executar o vínculo estático. Entretanto, para o propósito deste livro, você não precisa compreender as
operações fundamentais que o compilador executa para usar as DLLs de forma eficaz em suas aplicações.
NOTA
O Delphi implementa um linkeditor inteligente que remove automaticamente as funções, procedimentos,
variáveis e constantes digitadas que nunca são referenciadas no projeto final. Portanto, as funções residen-
tes em unidades de grande porte que nunca são usadas, não se tornam parte de seu arquivo EXE.
Suponha que você tenha duas aplicações que usam a mesma função residente em uma unidade. É
claro que ambas as aplicações teriam que incluir a unidade em suas instruções uses. Se as duas aplica-
ções fossem executadas simultaneamente no Windows, a função existiria duas vezes na memória. Se
houvesse uma terceira aplicação, existiria uma terceira instância da função na memória e você estaria
usando até três vezes seu espaço de memória. Esse pequeno exemplo ilustra uma das principais razões
do vínculo dinâmico. Com o vínculo dinâmico, essa função reside em uma DLL. Sendo assim, quando
uma aplicação carregar a função na memória, todas as outras aplicações que precisarem referenciá-la
poderão compartilhar seu código pelo mapeamento da imagem da DLL para seu próprio espaço de
memória do processo. O resultado final é que a função da DLL existiria apenas uma vez na memória –
pelo menos, teoricamente.
Com o vínculo dinâmico, o vínculo entre a chamada de uma função e seu código executável é deter-
minado em tempo de execução (runtime) pelo uso de uma referência externa à função da DLL. Essas re-
ferências podem ser declaradas na aplicação, mas geralmente são colocadas em uma unidade import sepa-
rada. A unidade import declara as funções e procedimentos importados e define os vários tipos exigidos
pelas funções da DLL.
Por exemplo, suponha que você tenha uma DLL denominada MaxLib.dll que contenha uma função:
function Max(i1, I2: integer): integer;
Essa função retorna o maior de dois inteiros passados para ela. Uma unidade import típica se parece-
ria com:
unit MaxUnit;
interface
function Max(I1, I2: integer): integer;
implementation
function Max; external ‘MAXLIB’;
end.
Você notará que, embora se pareça com uma unidade típica, ela não define a função Max( ). A pala-
vra-chave external simplesmente informa que a função reside na DLL do nome que a segue. Para usar essa
unidade, uma aplicação simplesmente colocaria MaxUnit em sua instrução uses. Quando a aplicação for
executada, a DLL será automaticamente carregada na memória e quaisquer chamadas para Max( ) serão
180 vinculadas à função Max( ) na DLL.
Isso ilustra um de dois modos de carregar uma DLL, denominado carregamento implícito, que faz
com que o Windows carregue automaticamente a DLL quando a aplicação for carregada. Um outro mé-
todo é o carregamento explícito da DLL, que será discutido mais adiante neste capítulo.
Ocultando a implementação
Em alguns casos, você pode querer ocultar os detalhes das rotinas por você disponibilizadas a partir de
uma DLL. Independente do motivo para a decisão de ocultar a implementação de seu código, uma DLL
fornece um modo para que você disponibilize suas funções ao público e, com isso, não se desfaça de seu
código-fonte. Tudo o que você precisa fazer é fornecer uma unidade de interface para permitir que ou-
tros acessem sua DLL. Se você acha que isso já é possível com as unidades compiladas do Delphi (DCUs),
considere que as DCUs se aplicam apenas a outras aplicações em Delphi, criadas com a mesma versão do
Delphi. As DLLs são independentes de linguagem, sendo assim, é possível criar uma DLL que possa ser
utilizada pelo C++, VB ou qualquer outra linguagem que ofereça suporte a DLLs.
A unidade Windows é a unidade de interface para as DLLs do Win32. Os arquivos-fonte da unidade
API do Win32 estão incluídos no Delphi 5. Um dos arquivos obtidos é o Windows.pas, a fonte para a unida-
de do Windows. Em Windows.pas, você encontra definições da função como a seguinte na seção interface:
function ClientToScreen(Hwnd: HWND; var lpPoint: TPoint): BOOL; stdcall;
Basicamente, isso informa que o procedimento ClientToScreen( ) existe na biblioteca de vínculo di-
nâmico User32.dll e seu nome é ClientToScreen. 181
Controles personalizados
Em geral, os controles personalizados são colocados nas DLLs. Esses controles não são iguais aos compo-
nentes personalizados do Delphi. Os controles personalizados estão registrados no Windows e podem
ser usados por qualquer ambiente de desenvolvimento do Windows. Esses tipos de controles personali-
zados são colocados em DLLs para economizar a memória, tendo apenas uma cópia do código do con-
trole na memória quando várias cópias do controle estiverem sendo usadas.
NOTA
O antigo mecanismo da DDL de controle personalizado é extremamente primitivo e inflexível, sendo o mo-
tivo pelo qual a Microsoft agora usa os controles OLE e ActiveX. Esses antigos formatos de controles perso-
nalizados são raros.
Listagem 9.1 PenniesLib.dpr, uma DLL para converter centavos para outras moedas
library PenniesLib;
{$DEFINE PENNIESLIB}
uses
SysUtils,
Classes,
PenniesInt;
Observe que esta biblioteca usa a unidade PenniesInt. Trataremos disso com mais detalhes a qual-
quer momento.
A cláusula exports especifica quais funções ou procedimentos na DLL são exportados e disponibili-
zados para as aplicações de chamada.
unit PenniesInt;
{ Rotina da interface para PENNIES.DLL }
interface
type
{$IFNDEF PENNIESLIB}
{ Declara a função com a palavra-chave export }
implementation
{$IFNDEF PENNIESLIB}
{ Define a função importada }
function PenniesToCoins; external ‘PENNIESLIB.DLL’ name ‘PenniesToCoins’;
{$ENDIF}
end.
183
Na seção type deste projeto, você declara o registro TCoinsRec, como também um indicador para esse
registro. Esse registro manterá as denominações que irão converter a quantia em centavos passada pela
função PenniesToCoins( ). A função obtém dois parâmetros – a quantia total do dinheiro em centavos e um
indicador para uma variável TCoinsRec. O resultado da função é a quantia em centavos passada.
PenniesInt.pas declara a função que PenniesLib.dll exporta em sua seção interface. A definição da
função PenniesToCoins( ) é colocada na seção implementation. Essa definição especifica que a função é uma
função externa existente no arquivo da DLL PenniesLib.dll. Ela é vinculada à função da DLL pelo seu
nome. Observe que você utilizou uma diretiva do compilador PENNIESLIB para compilar condicionalmente
a declaração da função PenniesToCoins( ). Isso é feito porque não é necessário vincular essa declaração ao
compilar a unidade de interface para a biblioteca. Isso lhe permite compartilhar as definições do tipo de
unidade interface com a biblioteca e quaisquer aplicações que pretendam utilizar a biblioteca. Qualquer
mudança nas estruturas utilizadas por ambas somente deve ser feita na unidade de interface.
DICA
Para definir uma diretiva condicional de toda a aplicação, especifique a condicional na página Directo-
ries/Conditionals (diretórios/condicionais) da caixa de diálogo Project, Options. Observe que você deverá
reconstruir seu projeto para que as alterações nas definições condicionais tenham efeito, pois a lógica Make
não reavalia as definições condicionais.
NOTA
A definição a seguir mostra uma de duas maneiras de importar uma função da DLL:
Esse método é denominado importação por ordinal. O outro método com o qual você pode importar as
funções da DLL é o método por nome:
O método por nome utiliza o nome especificado após a palavra-chave name para determinar qual função
será vinculada à DLL.
O método por ordinal reduz o tempo de carregamento da DLL, pois não é preciso analisar o nome da
função na tabela de nomes da DLL. Entretanto, este método não é o preferencial no Win32. A importação
por nome é a técnica preferencial, de modo que as aplicações não fiquem hipersensíveis à realocação dos
pontos de entrada da DLL, à medida que as DLLs forem atualizadas com o passar do tempo. Quando im-
portar por ordinal, você estará criando um vínculo a um local na DLL. Quando importar por nome, você es-
tará criando um vínculo ao nome da função, independente do local onde ela será incluída na DLL.
Se essa fosse uma DLL real planejada para distribuição, você forneceria PenniesLib.dll e PenniesInt.pas
a seus usuários. Isso permitiria que eles usassem a DLL ao definir os tipos e funções em PenniesInt.pas exi-
gidos por PenniesLib.dll. Além disso, os programadores usando diferentes linguagens, como C++, pode-
riam converter PenniesInt.pas para tais linguagens, permitindo assim o uso da DLL em seus ambientes de
desenvolvimento. Você encontrará um exemplo de projeto que usa PenniesLib.dll no CD que acompanha
este livro.
unit DLLFrm;
interface
uses
SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,
Forms, Dialogs, Grids, Calendar;
type
TDLLForm = class(TForm)
calDllCalendar: TCalendar;
procedure calDllCalendarDblClick(Sender: TObject);
end;
implementation
{$R *.DFM}
end.
185
O formulário principal nessa DLL é incorporado na função exportada. Observe que a declaração
DLLForm foi removida da seção interface e, ao invés disso, foi declarada dentro da função.
A primeira coisa que a função da DLL faz é atribuir o parâmetro AHandle à propriedade Applicati-
on.Handle. Pelo Capítulo 4 lembre-se de que os projetos do Delphi, incluindo os projetos da biblioteca,
contêm um objeto Application global. Em uma DLL, esse objeto está separado do objeto Application que
existe na aplicação de chamada. Para que o formulário na DLL atue verdadeiramente como um formulá-
rio modal para a aplicação de chamada, você deverá atribuir o identificador da aplicação de chamada à
propriedade Application.Handle da DLL, como foi ilustrado. Se isso não for feito, o resultado será um
comportamento irregular, especialmente quando você começar a minimizar o formulário da DLL. Além
disso, como vimos, você deverá certificar-se de não passar nil como o proprietário do formulário da
DLL.
Depois que o formulário for criado, você terá que atribuir a string ACaption para Caption do formulá-
rio da DLL. Será então exibido de forma modal. Quando o formulário for fechado, a data selecionada
pelo usuário no componente TCalendar será retornada para a função de chamada. O formulário será fe-
chado depois que o usuário clicar duas vezes no componente TCalendar.
ATENÇÃO
ShareMem deverá ser a primeira unidade na cláusula uses de sua biblioteca e na cláusula uses de seu projeto
(selecione View, Project Source), se sua DLL exportar quaisquer procedimentos ou funções que passarem
strings ou arrays dinâmicos como resultado da função ou parâmetros. Isso se aplica a todas as strings pas-
sadas e retornadas da sua DLL – mesmo as aninhadas em registros e classes. ShareMem é a unidade de in-
terface para o gerenciador de memória compartilhada Borlndmm.dll, que deve ser distribuída juntamente
com sua DLL. Para evitar o uso de Borlndmm.dll, passe as informações da string usando os parâmetros de
PChar ou ShortString.
ShareMem será apenas exigida quando as strings alocadas pelo heap ou os arrays dinâmicos forem pas-
sados entre módulos e tais transferências também passarem a propriedade da memória dessa string. O
typecast de uma string interna para um PChar e sua passagem para outro módulo como um PChar não irá
transferir a propriedade da memória da string para o módulo de chamada, de modo que ShareMem não será
necessária.
Observe que essa questão de ShareMem se aplica apenas às DLLs DelphiC++Builder que passam
strings ou arrays dinâmicos para outras DLLs do Delphi/BCB ou EXEs. As strings ou os arrays dinâmicos do
Delphi nunca devem ser expostos (como parâmetros ou resultados de função das funções exportadas pela
DLL) a DLLs que não sejam do Delphi ou aplicações host. Elas não saberiam como dispor os itens do Delphi
corretamente.
Além disso, a ShareMem nunca será exigida entre os módulos construídos com pacotes. O alocador de
memória é implicitamente compartilhado entre módulos em pacotes.
Isso é só o que é necessário ao encapsular um formulário modal em uma DLL. Na próxima seção,
discutiremos a exibição de um formulário sem modo em uma DLL.
186
Listagem 9.4 Um formulário sem modo em uma DLL
unit DLLFrm;
interface
uses
SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,
Forms, Dialogs, Grids, Calendar;
type
TDLLForm = class(TForm)
calDllCalendar: TCalendar;
end;
implementation
{$R *.DFM}
end.
unit MainFrm;
interface
uses
SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls, Mask;
type
TMainForm = class(TForm)
lblTotal: TLabel;
lblQlbl: TLabel;
lblDlbl: TLabel;
lblNlbl: TLabel;
lblPlbl: TLabel;
lblQuarters: TLabel;
lblDimes: TLabel;
lblNickels: TLabel;
lblPennies: TLabel;
btnMakeChange: TButton;
meTotalPennies: TMaskEdit;
procedure btnMakeChangeClick(Sender: TObject);
end;
var
MainForm: TMainForm;
implementation
uses PenniesInt; // Usa uma unidade de interface
188
Listagem 9.5 Continuação
{$R *.DFM}
end.
Observe que MainFrm.pas usa a unidade PenniesInt. Lembre-se de que PenniesInt.pas inclui as declara-
ções externas nas funções existentes em PenniesLib.dpr. Quando essa aplicação for executada, o sistema
Win32 irá carregar automaticamente PenniesLib.dll e mapeá-la no espaço de endereços do processo para
a aplicação de chamada.
O uso de uma unidade import é opcional. Você pode remover PenniesInt da instrução uses e colocar a
declaração external em PenniesToCoins( ) na seção implementation de MainFrm.pas, como no código a seguir:
implementation
Você também teria que definir PChangeRec e TChangeRec novamente em MainFrm.pas, ou então pode
compilar sua aplicação usando a diretiva do compilador PENNIESLIB. Essa técnica será satisfatória quando
você precisar apenas acessar algumas rotinas a partir de uma DLL. Em muitos casos, você perceberá que
não irá precisar apenas das declarações externas para as rotinas da DLL, mas também do acesso aos tipos
definidos na unidade interface.
NOTA
Muitas vezes, ao usar a DLL de um outro fornecedor, você poderá não ter uma unidade interface em Pas-
cal; em vez disso, você terá uma biblioteca de importação em C/C++. Nesse caso, você terá que conver-
ter a biblioteca para uma unidade interface equivalente em Pascal.
unit MainFfm;
interface
uses
SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls;
type
{ Primeiro, define um tipo de dado de procedimento; ele deverá refletir
o procedimento exportado da DLL. }
TShowCalendar = function (AHandle: THandle; ACaption: String):
TDateTime; StdCall;
{ Cria nova classe de exceção para refletir uma falha no carregamento da DLL }
EDLLLoadError = class(Exception);
TMainForm = class(TForm)
lblDate: TLabel;
btnGetCalendar: TButton;
procedure btnGetCalendarClick(Sender: TObject);
end;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
end.
Em primeiro lugar, essa unidade define um tipo de dado de procedimento, TshowCalendar, o qual re-
fletirá a definição da função a ser usada a partir de CalendarLib.dll. Ela define então uma exceção especial,
que será gerada quando existir um problema no carregamento da DLL. No manipulador do evento
btnGetCalendarClick( ), você notará o uso de três funções da API do Win32: LoadLibrary( ), FreeLibrary( ) e
GetProcAddress( ).
LoadLibrary( ) é definida da seguinte forma:
Essa função carrega o módulo da DLL especificado por lpLibFileName e faz seu mapeamento no espa-
ço de endereços do processo de chamada. Se essa função for bem-sucedida, ela retornará um identifica-
dor para o módulo. Se falhar, retornará o valor 0 e uma exceção será gerada. Você pode pesquisar LoadLi-
brary( ), na ajuda on-line, para obter informações detalhadas sobre sua funcionalidade e possíveis valo-
res de retorno de erro.
FreeLibrary( ) é definida da seguinte forma:
FreeLibrary( ) decrementa a contagem de instâncias da biblioteca especificada por LibModule. Ela re-
moverá a biblioteca da memória quando a contagem de instâncias da biblioteca for zero. A contagem de
instâncias controla o número de tarefas que usam a DLL.
A seguir, veja como é definida a função GetProcAddress( ):
function GetProcAddress(hModule: HMODULE; lpProcName: LPCSTR):
FARPROC; stdcall
GetProcAddress( ) retorna o endereço de uma função dentro do módulo especificado em seu pri-
meiro parâmetro, hModule . hModule é o THandle retornado de uma chamada para LoadLibrary( ).
Se GetProcAddress( ) falhar, nil será retornado. Você deve chamar GetLastError( ) para obter informações
estendidas sobre o erro.
No manipulador do evento OnClick de Button1, LoadLibrary( ) é chamada para carregar CALDLL. Se
ocorrer uma falha no carregamento, uma exceção será gerada. Se a chamada for bem-sucedida, será feita
uma chamada para GetProcAddress( ) da janela a fim de obter o endereço da função ShowCalendar( ). Ane-
xando a variável do tipo de dado de procedimento ShowCalendar ao caracter de endereço do operador (@),
você impedirá que o compilador emita um erro de correspondência de tipos devido à sua verificação res-
trita de tipo. Após obter o endereço de ShowCalendar( ), será possível usá-lo conforme definido por Tshow- 191
Calendar.Finalmente, FreeLibrary( ) é chamada dentro do bloco finally para garantir que a memória da
biblioteca seja liberada quando não for mais exigida.
Você poderá ver se a biblioteca está carregada e liberada sempre que essa função for chamada. Se
essa função tiver sido chamada apenas uma vez durante a execução de uma aplicação, ficará aparente o
quanto pode economizar o carregamento explícito em se tratando dos recursos mais necessários e limita-
dos de memória. Por outro lado, se essa função tivesse sido freqüentemente chamada, o carregamento e
o descarregamento da DLL adicionaria muito trabalho extra.
Evento Objetivo
ATENÇÃO
Threads terminados de forma incorreta – pela chamada de TerminateThread( ) – não são garantidos para
chamada de DLL_THREAD_DETACH.
192
Exemplo de entrada/saída da DLL
A Listagem 9.7 ilustra como você instalaria um procedimento de entrada/saída para a variável DLLProc da
DLL.
library DllEntry;
uses
SysUtils,
Windows,
Dialogs,
Classes;
procedure DLLEntryPoint(dwReason: DWord);
begin
case dwReason of
DLL_PROCESS_ATTACH: ShowMessage(‘Attaching to process’);
DLL_PROCESS_DETACH: ShowMessage(‘Detaching from process’);
DLL_THREAD_ATTACH: MessageBeep(0);
DLL_THREAD_DETACH: MessageBeep(0);
end;
end;
begin
{ Primeiro, atribui o procedimento à variável DLLProc }
DllProc := @DLLEntryPoint;
{ Agora, chama o procedimento para refletir se a DLL está anexada ao
processo }
DLLEntryPoint(DLL_PROCESS_ATTACH);
end.
unit MainFrm;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls, ComCtrls, Gauges;
type
TTestThread = class(TThread)
procedure Execute; override;
procedure SetCaptionData;
end;
TMainForm = class(TForm)
btnLoadLib: TButton;
btnFreeLib: TButton;
btnCreateThread: TButton;
btnFreeThread: TButton;
lblCount: TLabel;
procedure btnLoadLibClick(Sender: TObject);
procedure btnFreeLibClick(Sender: TObject);
procedure btnCreateThreadClick(Sender: TObject);
procedure btnFreeThreadClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
LibHandle : THandle;
TestThread : TTestThread;
Counter : Integer;
GoThread : Boolean;
end;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
procedure TTestThread.Execute;
begin
while MainForm.GoThread do
begin
Synchronize(SetCaptionData);
Inc(MainForm.Counter);
end;
end;
procedure TTestThread.SetCaptionData;
begin
MainForm.lblCount.Caption := IntToStr(MainForm.Counter);
end;
else
MessageDlg(‘Library already loaded’, mtWarning, [mbok], 0);
end;
end;
end.
Esse projeto consiste em um formulário principal com quatro componentes TButton. BtnLoadLib car-
rega a DLL DllEntryLib.dll. BtnFreeLib libera a biblioteca do processo. BtnCreateThread cria um objeto des-
cendente de TThread, que por sua vez cria um thread. BtnFreeThread destrói o objeto TThread. lblCount é usa-
da apenas para mostrar a execução do thread.
O manipulador do evento btnLoadLibClick( ) chama LoadLibrary( ) para carregar DllEntryLib.dll. Isso
faz com que a DLL seja carregada e mapeada ao espaço de endereços do processo. Além disso, o código 195
de início na DLL é executado. Novamente, esse é o código que aparece no bloco begin..end da DLL, o
qual executa o seguinte para definir um procedimento de entrada/saída para a DLL:
begin
{ Primeiro, atribui o procedimento à variável DLLProc }
DllProc := @DLLEntryPoint;
{ Agora, solicita que o procedimento descubra se a DLL está anexada ao
processo }
DLLEntryPoint(DLL_PROCESS_ATTACH);
end.
Essa seção de inicialização será chamada apenas uma vez por processo. Se um outro processo carre-
gar essa DLL, a seção será chamada novamente, exceto no contexto do processo separado – os processos
não compartilham as instâncias da DLL.
O manipulador do evento btnFreeLibClick( ) descarrega a DLL ao chamar FreeLibrary( ). Quando
isso acontecer, o procedimento ao qual DLLProc aponta, DLLEntryProc( ), será chamado com o valor de
DLL_PROCESS_DETACH passado como parâmetro.
O manipulador do evento btnCreateThreadClick( ) cria o objeto descendente de TThread. Isso faz com
que a DLLEntryProc( ) seja chamada e o valor DLL_THREAD_ATTACH seja passado como parâmetro. O manipula-
dor do evento btnFreeThreadClick( ) chama DLLEntryProc novamente, mas passa DLL_THREAD_DETACH como va-
lor para o procedimento.
Embora seja solicitada apenas uma caixa de mensagem, quando da ocorrência do evento, você usará
esses eventos para realizar a inicialização ou limpeza de qualquer processo ou thread que possa ser neces-
sário para sua aplicação. Mais adiante, você verá um exemplo do uso dessa técnica para definir dados
globais compartilháveis da DLL. Você pode pesquisar a demonstração dessa DLL em DLLEntryTest.dpr do
projeto no CD.
Exceções em DLLs
Esta seção aborda os tópicos relacionados às exceções das DLLs e do Win32.
Isso não ocorre mais com o Delphi 2. As exceções do Delphi 5 fazem o mapeamento delas mesmas
com as exceções do Win32. As exceções geradas nas DLLs não são mais um recurso do compilador/lin-
guagem do Delphi; em vez disso, são um recurso do sistema Win32.
Entretanto, para que isso funcione, será necessário certificar-se de que SysUtils esteja incluída
na cláusula uses da DLL. A não-inclusão de SysUtils desativará o suporte da exceção do Delphi dentro
da DLL.
196
ATENÇÃO
A maioria das aplicações no Win32 não é projetada para manipular as exceções; sendo assim, embora as
exceções da linguagem do Delphi sejam convertidas para as exceções do Win32, as que você permite que
escapem de uma DLL para a aplicação host irão provavelmente fechar a aplicação.
Se a aplicação host estivesse incorporada no Delphi ou no C++Builder, isso não seria mais um pro-
blema, mas há ainda muitos códigos primitivos em C e C++ que não aceitam as exceções.
Portanto, para tornar suas DLLs confiáveis, você ainda deverá considerar o uso de um método de 16
bits de proteção dos pontos de entrada da DLL com blocos try..except, a fim de capturar as exceções gera-
das em suas DLLs.
NOTA
Quando uma aplicação não-Delphi usar uma DLL escrita em Delphi, ela não será capaz de utilizar as
classes de exceção específicas da linguagem do Delphi. Entretanto, poderá ser manipulada como uma
exceção do sistema Win32 com o código de exceção $0EEDFACE. O endereço da exceção será a primeira
entrada no array ExceptionInformation do sistema Win32, EXCEPTION_RECORD. A segunda entrada irá con-
ter uma referência ao objeto da exceção do Delphi. Para obter informações adicionais, procure por
EXCEPTION_RECORD na ajuda on-line do Delphi.
Em seguida, o compilador insere um bloco try..except implícito, que envolve todo o conteúdo da
função e alcança qualquer exceção gerada. O bloco except solicita uma chamada para SafecallException-
Handler( ) a fim de converter a exceção em um HResult. Isso é mais ou menos semelhante ao método de 16
bits de captura de exceções e retorno de valores de erro.
Funções de callback
Uma função de callback é uma função em sua aplicação chamada por DLLs do Win32 ou por outras
DLLs. Basicamente, o Windows tem várias funções de API que exigem uma função de callback. Ao cha-
mar essas funções, você passa um endereço de uma função definida por sua aplicação, que pode ser cha-
mada pelo Windows. Se você estiver pensando como tudo isso está relacionado às DLLs, lembre-se de
que a API do Win32 corresponde, na verdade, a várias rotinas exportadas de DLLs do sistema. Essencial-
mente, quando você passa uma função de callback para uma função do Win32, estará passando esta fun-
ção para uma DLL.
Uma função desse tipo é a função da API EnumWindows( ), a qual é enumerada por todas as janelas em ní-
vel superior. Essa função passa o identificador de cada janela na enumeração para a função de callback defi-
nida por sua aplicação. Você precisa definir e passar o endereço da função de callback para a função Enum-
Windows( ). A função de callback que deve ser fornecida para EnumWindows( ) é definida da seguinte forma:
Ilustraremos o uso da função EnumWindows( ) no projeto CallBack.dpr no CD que acompanha este livro
e na Listagem 9.9. 197
Listagem 9.9 MainForm.pas, código-fonte para o exemplo de callback
unit MainFrm;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls, ComCtrls;
type
TMainForm = class(TForm)
lbWinInfo: TListBox;
btnGetWinInfo: TButton;
hdWinInfo: THeaderControl;
procedure btnGetWinInfoClick(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure lbWinInfoDrawItem(Control: TWinControl; Index: Integer;
Rect: TRect; State: TOwnerDrawState);
procedure hdWinInfoSectionResize(HeaderControl: THeaderControl;
Section: THeaderSection);
end;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
function EnumWindowsProc(Hw: HWnd; AMainForm: TMainForm):
Boolean; stdcall;
{ Este procedimento é chamado pela biblioteca User32.DLL à medida que
for enumerado pelas janelas ativas no sistema. }
var
WinName, CName: array[0..144] of char;
WindowInfo: TWindowInfo;
begin
{ Retorna verdadeiro por default, o que indica não parar a enumeração
pelas janelas }
Result := True;
GetWindowText(Hw, WinName, 144); // Obtém o texto da janela atual
GetClassName(Hw, CName, 144); // Obtém o nome da classe da janela
{ Cria uma instância TWindowInfo e define seus campos com os valores do
nome da janela e do nome da classe da janela. Em seguida, adiciona este
objeto no array Objects de ListBox1. Esses valores serão exibidos mais
adiante pela caixa de listagem }
WindowInfo := TWindowInfo.Create;
198
Listagem 9.9 Continuação
with WindowInfo do
begin
SetLength(WindowName, strlen(WinName));
SetLength(WindowClass, StrLen(CName));
WindowName := StrPas(WinName);
WindowClass := StrPas(CName);
end;
// Adiciona ao array Objects
MainForm.lbWinInfo.Items.AddObject(‘’, WindowInfo); end;
procedure TMainForm.hdWinInfoSectionResize(HeaderControl:
THeaderControl; Section: THeaderSection);
begin
lbWinInfo.Invalidate; // Força ListBox1 para se auto-redesenhar.
end;
end.
199
Essa aplicação usa a função EnumWindows( ) para extrair o nome da janela e o nome da classe de to-
das as janelas em nível superior e os adiciona na caixa de listagem de desenho do proprietário no for-
mulário principal. O formulário principal usa uma caixa de listagem de desenho do proprietário para
que o nome da janela e o nome da classe da janela apareçam em forma de colunas. Primeiro, explicare-
mos o uso da função de callback. Em seguida, explicaremos como criamos a caixa de listagem em for-
ma de colunas.
200
Listagem 9.10 Chamando uma demonstração de callback: código-fonte para StrSrchLib.dll
library StrSrchLib;
uses
Wintypes,
WinProcs,
SysUtils,
Dialogs;
type
{ declara o tipo da função de callback }
TFoundStrProc = procedure(StrPos: PChar); StdCall;
exports
SearchStr;
begin
end.
A DLL também define um tipo de procedimento, TfoundStrProc, para a função de callback, o qual
será utilizado para o typecast da função de callback quando chamada.
O procedimento exportado SearchStr( ) é o local em que a função de callback será chamada. O co-
mentário na listagem explica o que faz esse procedimento.
Um exemplo da utilização dessa DLL será fornecido no projeto CallBackDemo.dpr, no diretório
\DLLCallBack do CD. O código-fonte para o formulário principal dessa demonstração é apresentado na
Listagem 9.11.
unit MainFrm;
interface
201
Listagem 9.11 Continuação
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls;
type
TMainForm = class(TForm)
btnCallDLLFunc: TButton;
edtSearchStr: TEdit;
lblSrchWrd: TLabel;
memStr: TMemo;
procedure btnCallDLLFuncClick(Sender: TObject);
end;
var
MainForm: TMainForm;
Count: Integer;
implementation
{$R *.DFM}
end.
202
Essa aplicação contém um controle TMemo. EdtSearchStr.Text contém uma string, na qual o conteúdo
de memStr será buscado. O conteúdo de memStr é passado como a string de origem para a função da DLL,
SearchStr( ), e edtSearchStr.Text é passada como a string de busca.
A função StrPosProc( ) é a função de callback real. Essa função incrementa o valor da variável global
Count, a qual será usada para reter o número de vezes que a string de busca ocorre no texto de memStr.
Listagem 9.12 ShareLib: Uma DLL que ilustra o compartilhamento dos dados globais
library ShareLib;
uses
ShareMem,
Windows,
SysUtils,
Classes;
const
{$I DLLDATA.INC}
var
GlobalData : PGlobalDLLData;
203
Listagem 9.11 Continuação
MapHandle : THandle;
procedure OpenSharedData;
var
Size: Integer;
begin
{ Obtém o tamanho dos dados a serem mapeados. }
Size := SizeOf(TGlobalDLLData);
if MapHandle = 0 then
RaiseLastWin32Error;
{ Agora, faz o mapeamento dos dados ao espaço de endereços do processo de
chamada e obtém um indicador para o início desse endereço }
GlobalData := MapViewOfFile(MapHandle, FILE_MAP_ALL_ACCESS, 0, 0, Size);
{ Inicializa esses dados }
GlobalData^.S := ‘ShareLib’;
GlobalData^.I := 1;
if GlobalData = nil then
begin
CloseHandle(MapHandle);
RaiseLastWin32Error;
end;
end;
procedure CloseSharedData;
{ Este procedimento desfaz o mapeamento do arquivo mapeado na memória e libera
o identificador do arquivo mapeado na memória }
begin
UnmapViewOfFile(GlobalData);
CloseHandle(MapHandle);
end;
end;
end;
exports
GetDLLData;
begin
{ Primeiro, atribui o procedimento à variável DLLProc }
DllProc := @DLLEntryPoint;
{ Agora, solicita para que o procedimento descubra se a DLL está anexada
ao processo }
DLLEntryPoint(DLL_PROCESS_ATTACH);
end.
PGlobalDLLData = ^TGlobalDLLData;
TGlobalDLLData = record
S: String[50];
I: Integer;
end;
Nessa DLL, você usa o mesmo processo discutido anteriormente neste capítulo para adicionar o có-
digo de entrada e saída para a DLL no formato de um procedimento de entrada/saída. Esse procedimen-
to é denominado DLLEntryPoint( ), conforme mostrado na listagem. Quando um processo carrega a DLL,
o método OpenSharedData( ) é chamado. Quando um processo é desanexado da DLL, o método CloseSha-
redData( ) é chamado.
Não nos aprofundaremos aqui sobre a utilização do arquivo mapeado na memória, pois abordare-
mos o tópico com mais detalhes no Capítulo 12. Entretanto, explicaremos os princípios básicos para que
você compreenda o objetivo dessa DLL.
Os arquivos mapeados na memória fornecem um meio de reservar uma região do espaço de endere-
ços no sistema Win32 com a qual se comprometerá a memória física. Isso é semelhante à alocação de me-
mória e referência à memória com um indicador. Entretanto, com arquivos mapeados na memória, você
pode fazer o mapeamento de um arquivo de disco a esse espaço de endereço e referir-se ao espaço dentro
do arquivo como se estivesse se referindo a uma área da memória com um indicador.
Com arquivos mapeados na memória, você deve primeiro obter um identificador para um arquivo
existente no disco ao qual um objeto mapeado na memória será mapeado. Depois, você fará o mapea-
mento do objeto mapeado na memória ao arquivo. No início deste capítulo, explicamos como o sistema
compartilha as DLLs com múltiplas aplicações ao carregar primeiro a DLL na memória e, então, ao for-
necer a cada aplicação sua própria imagem da DLL, de modo que pareça que cada aplicação tenha carre-
gado uma instância separada da DLL. Entretanto, na realidade, a DLL existe na memória apenas uma
vez. Isso é feito utilizando os arquivos mapeados na memória. Você pode usar o mesmo processo para
dar acesso aos arquivos de dados. Você só precisa fazer chamadas da API do Win32 necessárias para lidar
com a criação dos arquivos mapeados na memória e o acesso aos mesmos.
Agora, considere este exemplo: suponha que uma aplicação, à qual damos o nome de App1, crie um
arquivo mapeado na memória, que é mapeado a um arquivo no disco, MyFile.dat. App1 poderá agora ler e
gravar dados no arquivo. Se, durante a execução de App1, App2 também for mapeada para o mesmo arqui-
vo, as alterações feitas no arquivo por App1 serão vistas por App2. Na verdade, isso é um pouco mais com-
plexo; certos indicadores devem ser definidos, para que alterações no arquivo sejam imediatamente defi- 205
nidas e assim por diante. Para esta discussão, basta dizer que as alterações serão observadas por ambas as
aplicações, já que isso é possível.
Um dos modos em que os arquivos mapeados na memória podem ser usados é criando um mapea-
mento de arquivo a partir do arquivo de paginação do Win32 em vez de um arquivo existente. Isso signi-
fica que em vez do mapeamento para um arquivo existente no disco, é possível reservar uma área da me-
mória à qual você pode referir-se como se fosse um arquivo do disco. Isso evita que você tenha de criar e
destruir um arquivo temporário, se tudo o que você quer é criar um espaço de endereços que possa ser
acessado por múltiplos processos. O sistema Win32 gerencia seu arquivo de paginação de modo que,
quando a memória do arquivo de paginação não for mais necessária, ela será liberada.
Nos parágrafos anteriores, apresentamos um exemplo que ilustrava como duas aplicações podiam
acessar o mesmo arquivo de dados usando um arquivo mapeado na memória. O mesmo pode ser feito
entre uma aplicação e uma DLL. Na verdade, se a DLL criar o arquivo mapeado na memória quando car-
regada por uma aplicação, ela usará o mesmo arquivo mapeado na memória quando carregada por uma
outra aplicação. Existirão duas imagens da DLL, uma para cada aplicação de chamada, e as duas usarão a
mesma instância do arquivo mapeado na memória. A DLL pode criar uma referência aos dados pelo ma-
peamento de arquivo disponível para sua aplicação de chamada. Quando uma aplicação fizer alterações
nesses dados, a segunda aplicação verá essas alterações, pois estão se referindo aos mesmos dados, mapea-
dos por duas instâncias diferentes de objeto mapeado na memória. Utilizamos essa técnica no exemplo.
Na Listagem 9.12, OpenSharedData( ) é responsável pela criação do arquivo mapeado na memória.
Ela usa a função CreateFileMapping( ) para primeiro criar o objeto de mapeamento de arquivo e, em segui-
da, passar para a função MapViewOfFile( ). A função MapViewOfFile( ) faz o mapeamento de uma visão do
arquivo no espaço de endereços do processo de chamada. O valor de retorno dessa função é o início do
espaço de endereços. Agora lembre-se de que esse é o espaço de endereços do processo de chamada. Para
duas aplicações diferentes usando essa DLL, o local do endereço pode ser diferente, embora os dados aos
quais se referem sejam os mesmos.
NOTA
O primeiro parâmetro para CreateFileMapping( ) é um identificador para um arquivo ao qual é mapeado
o arquivo mapeado na memória. Entretanto, se você estiver fazendo o mapeamento para um espaço de
endereços do arquivo de paginação do sistema, passe o valor $FFFFFFFF (igual a DWord(-1)) como o valor
desse parâmetro. Você deve também fornecer um nome para o objeto de mapeamento de arquivo como
último parâmetro para CreateFileMapping( ). Esse será o nome que o sistema usará para se referir a esse
mapeamento de arquivo. Se múltiplos processos criarem um arquivo mapeado na memória usando o mes-
mo nome, os objetos de mapeamento irão se referir à mesma memória do sistema.
Após a chamada para MapViewOfFile( ), a variável GlobalData irá se referir ao espaço de endereços para
o arquivo mapeado na memória. A função exportada GetDLLData( ) atribui a memória à qual GlobalData se
refere ao parâmetro AglobalData. AGlobalData é passado a partir da aplicação de chamada; portanto, a apli-
cação de chamada tem acesso de leitura/gravação a esses dados.
O procedimento CloseSharedData( ) é responsável por desmapear a visão do arquivo a partir do pro-
cesso de chamada e liberar o objeto de mapeamento de arquivo. Isso não afeta outros objetos de mapea-
mento de arquivo ou mapeamentos de arquivo de outras aplicações.
unit MainFrmA1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls, ExtCtrls, Mask;
{$I DLLDATA.INC}
type
TMainForm = class(TForm)
edtGlobDataStr: TEdit;
btnGetDllData: TButton;
meGlobDataInt: TMaskEdit;
procedure btnGetDllDataClick(Sender: TObject);
procedure edtGlobDataStrChange(Sender: TObject);
procedure meGlobDataIntChange(Sender: TObject);
procedure FormCreate(Sender: TObject);
public
GlobalData: PGlobalDLLData;
end;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
end.
Essa aplicação também vincula o arquivo de inclusão DllData.inc, o qual define o tipo de dados TGlo-
balDLLData e seu indicador. O manipulador do evento btnGetDllDataClick( ) obtém um indicador para os
dados da DLL, que são acessados por um arquivo mapeado na memória na DLL. Ele faz isso ao chamar a
função GetDLLData( ) da DLL. Em seguida, atualiza seus controles com o valor desse indicador, GlobalData.
Os manipuladores do evento OnChange para os controles de edição alteram os valores de GlobalData. Já que
GlobalData se refere aos dados da DLL, ela modifica os dados referidos pelo arquivo mapeado na memória
da DLL.
A Listagem 9.14 mostra o código-fonte do formulário principal de App2.dpr.
unit MainFrmA2;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
ExtCtrls, StdCtrls;
{$I DLLDATA.INC}
type
TMainForm = class(TForm)
lblGlobDataStr: TLabel;
tmTimer: TTimer;
lblGlobDataInt: TLabel;
procedure tmTimerTimer(Sender: TObject);
public
GlobalData: PGlobalDLLData;
end;
var
MainForm: TMainForm;
208
Listagem 9.14 Continuação
implementation
{$R *.DFM}
end.
Esse formulário contém dois componentes TLabel, os quais são atualizados durante o evento OnTimer
de tmTimer. Quando o usuário alterar os valores dos dados da DLL a partir de App1, App2 irá refletir essas al-
terações.
Você pode executar ambas as aplicações para experimentar. Você as encontrará no CD que acom-
panha este livro.
type
TStringConvert = class(TObject)
{$IFDEF STRINGCONVERTLIB}
private
209
Listagem 9.15 Continuação
FPrepend: String;
FAppend : String;
{$ENDIF}
public
function ConvertString(AConvertType: TConvertType; AString: String): String;
virtual; stdcall; {$IFNDEF STRINGCONVERTLIB} abstract; {$ENDIF}
{$IFDEF STRINGCONVERTLIB}
constructor Create(APrepend, AAppend: String);
destructor Destroy; override;
{$ENDIF}
end;
TStringConvert = class(TObject)
public
function ConvertString(AConvertType: TConvertType; AString: String): String;
virtual; stdcall; abstract;
end;
}
A Listagem 9.15 é, na verdade, um arquivo de inclusão denominado StrConvert.inc. A razão por que
colocamos esse objeto em um arquivo de inclusão é para atender ao terceiro requisito da lista anterior –
ou seja, o objeto deve ser igualmente definido na DLL e na aplicação de chamada. Ao colocar o objeto em
um arquivo de inclusão, tanto a aplicação de chamada como a DLL poderão incluir esse arquivo. Se fo-
rem feitas alterações no objeto, você terá apenas que compilar os projetos em vez de digitar as alterações
duas vezes – uma vez na aplicação de chamada e uma vez na DLL –, o que pode causar erros.
Observe a seguinte definição do método ConvertSring( ):
function ConvertString(AConvertType: TConvertType; AString: String):
➥String; virtual; stdcall;
A razão para declarar esse método como virtual não é para poder criar um objeto descendente que
possa anular o método ConvertString( ). Em vez disso, ele é declarado como virtual, de modo que uma en-
trada no método ConvertString( ) seja feita na VMT (Virtual Method Table, ou tabela de métodos vir-
tuais). Não entraremos em detalhes sobre a VMT aqui; ela será discutida no Capítulo 13. Por enquanto,
pense na VMT como um bloco de memória que retém indicadores para métodos virtuais de um objeto.
Devido à VMT, a aplicação de chamada pode obter um indicador para o método do objeto. Sem declarar
o método como virtual, a VMT não teria uma entrada para o método e a aplicação de chamada não teria
como obter o indicador para o método. Então, na verdade, o que você tem na aplicação de chamada é um
indicador para a função. Devido a você ter baseado esse indicador em um tipo de método definido em
um objeto, o Delphi automaticamente identificará quaisquer correções, como passar o parâmetro self
implícito ao método.
Observe a definição condicional STRINGCONVERTLIB. Quando você estiver exportando o objeto, os úni-
cos métodos que precisarão da redefinição na aplicação de chamada serão os métodos a serem acessados
externamente a partir da DLL. Além disso, esses métodos podem ser definidos como métodos abstratos a
fim de impedir a geração de um erro durante a compilação. Isso é válido porque, durante a execução, es-
ses métodos serão implementados no código da DLL. O comentário mostra como o objeto TStringConvert
aparece na aplicação.
A Listagem 9.16 mostra a implementação do objeto TStringConvert.
210
Listagem 9.16 Implementação do objeto TStringConvert
unit StringConvertImp;
{$DEFINE STRINGCONVERTLIB}
interface
uses SysUtils;
{$I StrConvert.inc}
implementation
destructor TStringConvert.Destroy;
begin
inherited Destroy;
end;
function TStringConvert.ConvertString(AConvertType:
TConvertType; AString: String): String;
begin
case AConvertType of
ctUpper: Result := Format(‘%s%s%s’, [FPrepend, UpperCase(AString),
FAppend]);
ctLower: Result := Format(‘%s%s%s’, [FPrepend, LowerCase(AString),
FAppend]);
end;
end;
end.
Conforme estabelecido nas condições, o objeto deve ser criado na DLL. Isso é feito em uma função
exportada da DLL padrão InitStrConvert( ), a qual obtém dois parâmetros que são passados ao constru-
tor. Adicionamos isso com o intuito de ilustrar o modo como você passaria as informações para o cons-
trutor de um objeto por intermédio de uma função de interface.
Além disso, observe que nessa unidade você declara as diretivas condicionais STRINGCONVERTLIB. O
restante da unidade é auto-explicativo. A Listagem 9.17 mostra o arquivo de projeto da DLL.
211
Listagem 9.17 Arquivo de projeto para StringConvertLib.dll
library StringConvertLib;
uses
ShareMem,
SysUtils,
Classes,
StringConvertImp in ‘StringConvertImp.pas’;
exports
InitStrConvert;
end.
Em geral, essa biblioteca não contém nada que ainda não explicamos. Observe, entretanto, que
você usou a unidade ShareMem. Essa unidade deve ser a primeira unidade declarada no arquivo de projeto
da biblioteca, como também no arquivo de projeto da aplicação de chamada. Essa é uma questão extre-
mamente importante para ser lembrada.
A Listagem 9.18 mostra um exemplo de como usar o objeto exportado para converter uma string
para maiúsculas e minúsculas. Você encontrará o projeto dessa demonstração no CD, como StrConvert-
Test.dpr.
unit MainFrm;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls;
{$I strconvert.inc}
type
TMainForm = class(TForm)
btnUpper: TButton;
edtConvertStr: TEdit;
btnLower: TButton;
procedure btnUpperClick(Sender: TObject);
procedure btnLowerClick(Sender: TObject);
private
public
end;
var
MainForm: TMainForm;
implementation
212
Listagem 9.18 Continuação
{$R *.DFM}
end.
Resumo
As DLLs são uma parte essencial da criação de aplicações no Windows ao enfocar a reutilização do códi-
go. Este capítulo abordou as razões para a criação ou utilização de DLLs. Ilustrou como criar e usar DLLs
nas aplicações do Delphi e mostrou diferentes métodos de carregamento de DLLs. Discutiu sobre algu-
mas das principais considerações que devem ser estudadas ao usar DLLs com o Delphi e mostrou como
tornar os dados da DLL compartilháveis com diferentes aplicações.
Com esse conhecimento, você será capaz de criar DLLs com o Delphi e usar as mesmas com facili-
dade nas aplicações do Delphi. Você aprenderá mais sobre as DLLs em outros capítulos.
213
Impressão em CAPÍTULO
Delphi 5
10
NE STE C AP ÍT UL O
l O objeto TPrinter
l TPrinter.Canvas
l Impressão simples
l Impressão de um formulário
l Impressão avançada
l Tarefas de impressão diversas
l Como obter informações da impressora
l Resumo
NOTA
Você encontrará um conjunto de componentes de relatório da QuSoft na página QReport da Component
Palette. A documentação para essa ferramenta está localizada no arquivo de ajuda QuickRpt.hlp.
As ferramentas da QuSoft são apropriadas para aplicações que geram relatórios complexos. No entanto,
elas o limitam a sua utilização dos detalhes da impressão em nível de código-fonte, onde terá mais controle
sobre o que é impresso. Este capítulo não aborda o QuickReports; em vez disso, ele aborda a criação dos
seus próprios relatórios no Delphi.
O objeto TPrinter do Delphi, que encapsula o mecanismo de impressão do Windows, realiza um óti-
mo trabalho para você, que de outra forma teria que ser feito por você mesmo.
Este capítulo lhe ensina a realizar diversas operações de impressão usando TPrinter. Você aprenderá
sobre tarefas simples que o Delphi tornou muito mais fáceis para a criação de tarefas de impressão. Tam-
bém aprenderá sobre as técnicas de criação de rotinas avançadas para impressão, que lhe dará partida
para se tornar um guru da impressão.
215
Aplicações em CAPÍTULO
multithreading
11
NE STE C AP ÍT UL O
l Explicação sobre os threads 217
l O objeto TThread 218
l Gerenciamento de múltiplos threads 230
l Exemplo de uma aplicação de multithreading 244
l Acesso ao banco de dados em multithreading 256
l Gráficos de multithreading 260
l Resumo 264
O sistema operacional Win32 permite que você tenha múltiplos threads (ou caminhos) de execução em
suas aplicações. Indiscutivelmente, a vantagem mais importante e exclusiva que o Win32 tem em relação
ao Windows de 16 bits, este recurso permite que sejam realizados diferentes tipos de processamento si-
multâneo em sua aplicação. Esse é um dos principais motivos para que você faça uma atualização para
uma versão Delphi 32 bits, e este capítulo fornece todos os detalhes sobre como obter o máximo provei-
to dos threads em suas aplicações.
DICA
Os threads não são e nunca serão usados no Windows de 16 bits. Isso significa que nenhum código do
Delphi de 32 bits escrito com o uso dos threads será compatível com o Delphi versão 1.0. Lembre-se disso
ao desenvolver aplicações para ambas as plataformas.
VCL fora da UI
Existem na verdade poucas áreas da VCL com garantia da proteção contra thread. Talvez a área mais
eminente entre essas áreas protegidas contra thread seja o mecanismo de streaming de propriedade da
VCL, que garante que os fluxos de componentes possam ser lidos e gravados de forma eficiente por múl-
tiplos threads. Lembre-se de que até mesmo as classes básicas na VCL, como a TList, por exemplo, não
destinam-se a serem manipuladas a partir de múltiplos threads simultâneos. Em alguns casos, a VCL ofe-
rece alternativas de proteção contra thread que podem ser utilizadas quando você precisar. Por exemplo,
use TThreadList no lugar de TList, quando a lista estiver sujeita a ser manipulada por múltiplos threads.
VCL da UI
A VCL requer que todo o controle da interface do usuário (UI – User Interface) seja realizado dentro do
contexto do thread principal de uma aplicação (uma exceção é o thread protegido TCanvas, que será expli-
cado mais adiante neste capítulo). É claro que existem técnicas disponíveis para atualização da interface
do usuário a partir de um thread secundário (a ser discutido mais adiante), mas esta limitação o forçará
necessariamente a utilizar threads de forma um pouco mais sensata. Os exemplos deste capítulo mostram
algumas utilizações perfeitas para múltiplos threads em aplicações Delphi.
O objeto TThread
O Delphi faz o encapsulamento do objeto de thread da API para um objeto do Object Pascal denominado
TThread. Apesar de TThread encapsular quase todas as funções da API em um objeto discreto, há alguns
pontos – especialmente os relacionados ao sincronismo de thread – em que você deve usar a API. Nesta
seção, você aprende como funciona o objeto TThread e como utilizá-lo em suas aplicações.
Como você pode observar a partir da declaração, TThread é um descendente direto de TObject e, por-
tanto, não é um componente. Você também deve ter observado que o método TThread.Execute( ) é abstra-
to. Isso significa que a própria classe TThread é abstrata, o que quer dizer que você nunca criará uma ins-
tância do próprio TThread. Você criará apenas instâncias de descendentes de TThread. Por falar nisso, a for-
ma mais correta de criar um descendente de TThread é selecionando Thread Object (objeto de thread) na
caixa de diálogo New Items (novos itens), apresentada na opção de menu File, New. A caixa de diálogo
New Items aparece na Figura 11.1.
Após selecionar Thread Object na caixa de diálogo New Items, aparecerá uma caixa de diálogo soli-
citando que você digite um nome para o novo objeto. Você poderia digitar TTestThread, por exemplo. O
Delphi criará então uma nova unidade que contém seu objeto. Seu objeto será inicialmente definido
como a seguir:
type
TTestThread = class(TThread)
private
{ Declarações privadas }
protected
procedure Execute; override;
end; 219
FIGURE 11.1 Item Thread Object na caixa de diálogo New Items
Como você pode ver, o único método que você precisa substituir para criar um descendente funcio-
nal de TThread é o método Execute( ). Suponha, por exemplo, que você queira realizar um cálculo comple-
xo dentro de TTestThread. Nesse caso, você poderia definir seu método Execute( ) como a seguir:
procedure TTestThread.Execute;
var
i: integer;
begin
for i := 1 to 2000000 do
inc(Answer, Round(Abs(Sin(Sqrt(i)))));
end;
Na realidade, a equação é inventada, mas ainda ilustra o ponto nesse caso, porque o único objetivo
desta equação é levar um tempo relativamente longo para ser executada.
Agora você pode executar este exemplo de thread chamando seu construtor Create( ). Por enquan-
to, você pode fazer isso clicando no formulário principal, conforme demonstrado no código a seguir
(lembre-se de incluir a unidade que contém TTestThread na cláusula uses da unidade que contém TForm1
para evitar um erro de compilação):
procedure TForm1.Button1Click(Sender: Tobject);
var
NewThread: TTestThread;
begin
NewThread := TTestThread.Create(False);
end;
Se você executar a aplicação e der um clique no botão, perceberá que continuará podendo manipu-
lar o formulário movendo-o ou redimensionando-o enquanto o cálculo prossegue em segundo plano.
NOTA
O único parâmetro Boolean passado para o construtor Create( ) de TThread é denominado CreateSuspended,
e indica para iniciar o thread em um estado de suspensão. Se esse parâmetro for False, o método Execute( )
do objeto será automaticamente chamado depois de Create( ). Se esse parâmetro for True, você deverá
chamar o método Resume( ) de TThread em algum ponto para realmente iniciar a execução do thread. Isso
fará com que o método Execute( ) seja chamado a qualquer momento. Você deve configurar CreateSuspen-
ded para True se precisar configurar propriedades adicionais em seu objeto de thread antes que ele seja exe-
cutado. Configurar as propriedades depois que o thread estiver em execução poderá causar problemas.
Para ir um pouco mais a fundo, o construtor de Create( ) chama a função da biblioteca em tempo de
compilação (RTL) do Delphi, BeginThread( ), que, por sua vez, chama a função CreateThread( ) da API
para criar o novo thread. O valor do parâmetro CreateSuspended indica se o flag CREATE_SUSPENDED deve ser
passado para CreateThread( ).
220
Instâncias de thread
Retornando ao método Execute( ) para o objeto TTestThread, observe que ele contém uma variável local
denominada i. Imagine o que aconteceria a i se você criasse duas instâncias de TTestThread. O valor para
um thread substituiria o valor do outro? O primeiro thread teria prioridade? Ele explodiria? As respostas
são não, não e não. O Win32 mantém uma pilha separada para cada thread em execução no sistema. Isso
significa que, conforme você cria múltiplas instâncias do objeto TtestThread, cada uma mantém sua pró-
pria cópia de i em sua própria pilha. Portanto, todos os threads operarão de forma independente um do
outro nesse sentido.
No entanto, é importante salientar que essa noção da mesma variável operando de forma indepen-
dente em cada thread não se aplica a todas as variáveis. Esse assunto é explorado em detalhes nas seções
“Armazenamento local de thread” e “Sincronismo de thread”, mais adiante nesse capítulo.
Término do thread
Um TThread é considerado terminado quando o método Execute( ) tiver terminado de ser executado. Nes-
se ponto, é chamado o procedimento padrão do Delphi EndThread( ) que, por sua vez, chama o procedi-
mento da API ExitThread( ). ExitThread( ) dispõe adequadamente a pilha do thread e remove a alocação
do objeto de thread da API. Isso conclui o thread no que diz respeito à API.
Você precisa certificar-se também de que o objeto do Object Pascal será destruído quando terminar
de usar um objeto TThread. Isso garantirá que toda a memória ocupada por esse objeto tenha sido adequa-
damente alocada. Apesar disso acontecer automaticamente com o término do seu processo, pode ser que
você queira alocar seu objeto antes para que sua aplicação não perca memória durante a execução. A ma-
neira mais fácil de garantir que o objeto TThread esteja alocado é configurar sua propriedade FreeOnTermina-
te como True. Isso pode ser feito a qualquer momento antes do término da execução do método Execu-
te( ). Por exemplo, você pode fazer isso para o objeto TTestThread configurando a propriedade no méto-
do Execute( ), como a seguir:
procedure TTestThread.Execute;
var
i: integer;
begin
FreeOnTerminate := True;
for i := 1 to 2000000 do
inc(Answer, Round(Abs(Sin(Sqrt(i)))));
end;
O objeto TThread também possui um evento OnTerminate que é chamado mediante o término do
thread. Ele também é aceitável para liberar o objeto TThread de um manipulador para esse evento.
DICA
O evento OnTerminate de TThread é chamado a partir do contexto do thread principal da sua aplicação. Isso
significa que você pode acessar propriedades e métodos da VCL a partir de qualquer manipulador para
esse evento, sem utilizar o método Synchronize( ), conforme descrito na próxima seção.
Também é importante observar que o método Execute( ) do seu thread é responsável pela verifica-
ção de status da propriedade Terminated para determinar a necessidade de uma saída antecipada. Apesar
disso representar mais um detalhe para você se preocupar ao trabalhar com threads, o lado bom é que
esse tipo de arquitetura garante que ninguém vai puxar seu tapete e que você será capaz de realizar qual-
quer limpeza necessária no término do thread. É muito simples acrescentar esse código ao método Execu-
te( ) do TTestThread, conforme demonstrado abaixo:
221
procedure TTestThread.Execute;
var
i: integer;
begin
FreeOnTerminate := True;
for i := 1 to 2000000 do begin
if Terminated then Break;
inc(Answer, Round(Abs(Sin(Sqrt(i)))));
end;
end;
ATENÇÃO
Em caso de emergência, você também pode usar a função TerminateThread( ) da API do Win32 para ter-
minar a execução de um thread. Isso só deve ser feito na falta de outra opção, como, por exemplo, quando
um thread fica preso em um loop contínuo e deixa de responder. Essa função é definida como a seguir:
A propriedade Handle de TThread oferece a alça de thread da API, de forma que você pode chamar essa
função com sintaxe semelhante à demonstrada a seguir:
TerminateThread(MyHosedThread.Handle, 0);
Se você decidir utilizar essa função, deverá ser cauteloso quanto aos efeitos negativos que ela causará. Pri-
meiro, essa função tem um comportamento diferente no Windows NT/2000 e no Windows 95/98. No
Windows 95/98, TerminateThread( ) aloca a pilha associada ao thread; no Windows NT/2000, a pilha
fica fixa até o término do processo. Segundo, em todos os sistemas operacionais Win32, TerminateThre-
ad( ) simplesmente interrompe a execução, onde quer que seja, e não permite tentativas. Por último, blo-
queia a limpeza de recursos. Isso significa que os arquivos abertos pelo thread não podem ser fechados, a
memória alocada pelo thread não pode ser liberada e assim por diante. Além disso, as DLLs carregadas
pelo seu processo não serão notificadas quando um thread destruído com TerminateThread( ) sumir e isso
poderá ocasionar problemas quando a DLL fechar. Consulte o Capítulo 9, para obter mais informações so-
bre notificações de thread nas DLLs.
Método Synchronize( )
TThread oferece um método denominado Synchronize( ), que permite que alguns de seus próprios métodos
sejam executados a partir do thread principal da aplicação. Synchronize( ) é definido da seguinte forma:
procedure Synchronize(Method: TThreadMethod);
Seu parâmetro Method é do tipo TThreadMethod (que representa um método de procedimento que não
utiliza parâmetro), definido da seguinte forma:
type
TThreadMethod = procedure of object;
O método que você passa como o parâmetro Method é o que é executado a partir do thread principal
da aplicação. Voltando ao exemplo de TTestThread, suponha que você queira exibir o resultado em um
controle de edição no formulário principal. Você poderia fazer isso introduzindo em TTestThread um mé-
todo que fizesse a alteração necessária à propriedade Text do controle de edição e chamando esse método
através de Synchronize( ).
Nesse caso, suponha que esse método seja denominado GiveAnswer( ). O código-fonte para essa uni-
dade, chamado ThrdU, que inclui o código para atualizar o controle de edição no formulário principal,
aparece na Listagem 11.1.
unit ThrdU;
interface
uses
Classes;
type
TTestThread = class(TThread)
private
Answer: integer;
protected
procedure GiveAnswer;
procedure Execute; override;
end;
implementation
{ TTestThread }
procedure TTestThread.GiveAnswer;
begin
MainForm.Edit1.Text := InttoStr(Answer);
end;
223
Listagem 11.1 Continuação
procedure TTestThread.Execute;
var
I: Integer;
begin
FreeOnTerminate := True;
for I := 1 to 2000000 do
begin
if Terminated then Break;
Inc(Answer, Round(Abs(Sin(Sqrt(I)))));
Synchronize(GiveAnswer);
end;
end;
end.
Você já sabe que o método Synchronize( ) permite que você execute métodos a partir do contexto do
thread principal, mas até esse ponto você considerou Synchronize( ) como uma espécie de caixa preta mis-
teriosa. Você não sabe como ele funciona – você sabe apenas que ele funciona. Se você quiser desvendar o
mistério, continue lendo.
Na primeira vez que você cria um thread secundário em sua aplicação, a VCL cria e mantém uma ja-
nela de thread oculta a partir do contexto de seu thread principal. O único objetivo dessa janela é seriali-
zar as chamadas de procedimento feitas através do método Synchronize( ).
O método Synchronize( ) armazena o método especificado no seu parâmetro Method em um campo pri-
vado denominado FMethod e envia uma mensagem CM_EXECPROC definida pela VCL à janela, passando Self
(Self inicia o objeto TThread nesse caso) como lParam da mensagem. Quando o procedimento da janela rece-
be essa mensagem CM_EXECPROC, ele chama o método especificado em FMethod através da instância do objeto
TThread passada em lParam. Lembre-se de que, como a janela de thread foi criada a partir do contexto do
thread principal, o procedimento de janela para a janela de thread também é executado pelo thread princi-
pal. Sendo assim, o método especificado no campo FMethod também é executado pelo thread principal.
Veja a Figura 11.2 para obter uma melhor ilustração do que acontece dentro de Synchronize( ).
A mensagem é processada
Configura FMethod CM_EXECPROC pelo procedimento de
para Foo. Envia a janela da janela thread.
mensagem IParam torna-se TThread,
CM_EXECPROC para e a chamada é feita para
a janela de thread, FMethod.
passando Self como
IParam.
unit Main;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, ThrdU;
type
TMainForm = class(TForm)
Edit1: TEdit;
Button1: TButton;
Memo1: TMemo;
Label1: TLabel;
Label2: TLabel;
procedure Button1Click(Sender: TObject);
private
{ Declarações privadas }
public
{ Declarações públicas }
end;
var
MainForm: TMainForm;
implementation
225
Listagem 11.2 Continuação
{$R *.DFM}
end.
Observe que, depois de dar um clique no botão para chamar o thread secundário, você ainda conse-
gue digitar no controle de memorando como se o thread secundário não existisse. Quando o cálculo ter-
mina, o resultado aparece no controle de edição.
Prioridades e scheduling
Conforme já mencionado, o sistema operacional é responsável pelo scheduling, a fim de programar para
a execução de cada thread alguns ciclos da CPU, nos quais ele possa ser executado. O tempo programado
para um determinado thread depende da prioridade atribuída a ele. A prioridade total de um thread indi-
vidual é determinada por uma combinação da prioridade do processo que criou o thread – denominada
classe de prioridade – e a prioridade do próprio thread – denominada prioridade relativa.
NOTA
A modificação da classe de prioridade de um processo requer privilégios especiais do processo no Win-
dows NT/2000. A configuração padrão permite que os processos definam suas classes de prioridade, mas
elas podem ser desativadas pelos administradores do sistema, especialmente em servidores Windows
NT/2000 com uma carga alta.
A Tabela 11.1 mostra cada classe de prioridade e seu flag e valor numérico correspondentes.
Para obter e definir dinamicamente a classe de prioridade de um determinado processo, o Win32
oferece as funções GetPriorityClass( ) e SetPriorityClass( ), respectivamente. Essas funções são definidas
da seguinte forma:
function GetPriorityClass(hProcess: THandle): DWORD; stdcall;
*Disponível apenas no Windows 2000 e a constante de flag não está presente na versão Delphi 5 do Windows.pas.
O valor de retorno destas funções é um pseudomanipulador para o processo atual. Dizemos pseudo
porque a função não cria um novo manipulador e o valor de retorno não tem que ser fechado com Close-
Handle( ). Ela simplesmente oferece um manipulador que pode ser utilizado como referência para um
manipulador existente.
Para definir a classe de prioridade da sua aplicação como Alta, use código semelhante ao seguinte:
if not SetPriorityClass(GetCurrentProcess, HIGH_PRIORITY_CLASS) then
ShowMessage(‘Error setting priority class.’);
ATENÇÃO
Em quase todos os casos, você deve evitar definir a classe de prioridade de qualquer processo como Tempo
Real. Como a maioria dos threads do sistema operacional é executada em uma classe de prioridade infe-
rior a Tempo Real, seu thread receberá mais tempo de CPU do que o próprio OS, e isso poderá ocasionar
alguns problemas inesperados.
Mesmo a definição da classe de prioridade do processo para Alta pode ocasionar problemas se os
threads do processo não gastarem a maior parte do tempo ocioso ou à espera de eventos externos (como,
por exemplo, I/O de arquivo). É provável que um thread de alta prioridade esgote todo o tempo da CPU de
threads e processos de baixa prioridade até que seja bloqueado em um evento, fique ocioso ou processe
mensagens. A multitarefa preemptiva pode facilmente ser anulada pelas prioridades excessivas do scheduler.
Prioridade relativa
Outro fator determinante da prioridade total de um thread é a prioridade relativa. É importante salientar
que a classe de prioridade está associada a um processo e a prioridade relativa está associada aos threads
individuais dentro de um processo. Um thread pode ter uma entre sete prioridades relativas possíveis:
Ociosa, Mínima, Abaixo de Normal, Normal, Acima de Normal, Alta ou Crítica.
TThread expõe uma propriedade Priority de uma TthreadPriority de tipo numerado. Há uma numera-
ção para cada prioridade relativa:
type
TThreadPriority = (tpIdle, tpLowest, tpLower, tpNormal, tpHigher,
tpHighest, tpTimeCritical);
227
Você pode obter e definir a prioridade de qualquer objeto TThread simplesmente lendo ou escreven-
do em sua propriedade Priority. O código a seguir define a prioridade de uma instância descendente de
TThread denominada MyThread para Alta:
MyThread.Priority := tpHighest.
Assim como as classes de prioridade, cada prioridade relativa está associada a um valor numérico. A
diferença é que a prioridade relativa é um valor sinalizado que, quando somado à classe de prioridade de
um processo, é utilizado para determinar a prioridade total de um thread dentro do sistema. Por esse mo-
tivo, a prioridade relativa às vezes é denominada prioridade delta. A prioridade total de um thread pode
ser qualquer valor de 1 a 31 (1 é o menor). As constantes são definidas na unidade Windows que representa o
valor sinalizado para cada prioridade. A Tabela 11.2 mostra como cada numeração em TThreadPriority re-
presenta uma constante da API.
A razão pela qual os valores para as prioridades tpIdle e tpTimeCritical estão assinalados com asteris-
cos é que, ao contrário dos outros, esses valores de prioridade relativa não são somados à classe de priori-
dade para determinar a prioridade total do thread. Qualquer thread que possua a prioridade relativa
tpIdle, independente de sua classe de prioridade, tem uma prioridade total de 1. A prioridade Realtime é
uma exceção a essa regra porque, quando combinada com a prioridade relativa tpIdle, tem um valor total
de 16. Qualquer thread que tenha uma prioridade tpTimeCritical, independente de sua classe de priorida-
de, tem uma prioridade total de 15. A classe de prioridade Realtime é uma exceção a essa regra porque,
quando combinada com a prioridade relativa tpTimeCritical, tem um valor total de 31.
Temporização de um thread
Retornando à época dos 16 bits, quando programávamos em Windows 3.x, era bastante comum ajustar
uma parte do código com chamadas para GetTickCount( ) ou timeGetTime( ) para determinar quanto tempo
pode levar um determinado cálculo (mais ou menos como o exemplo a seguir):
228
var
StartTime, Total: Longint;
begin
StartTime := GetTickCount;
{ Faz algum cálculo aqui }
Total := GetTickCount - StartTime;
É muito mais difícil fazer isso em um ambiente de multithreading, porque sua aplicação pode tor-
nar-se preemptiva pelo sistema operacional no meio do cálculo para oferecer à CPU ciclos para outros
processos. Sendo assim, nenhuma temporização feita que dependa do tempo do sistema é capaz de ofere-
cer uma avaliação real de quanto tempo será necessário para devorar o cálculo em seu thread.
Para evitar tal problema, o Win32 no Windows NT/2000 oferece uma função denominada GetThre-
adTimes( ), que oferece informações completas sobre temporização de thread. Essa função é definida da
seguinte forma:
function GetThreadTimes(hThread: THandle; var lpCreationTime, lpExitTime,
lpKernelTime, lpUserTime: TFileTime): BOOL; stdcall;
O parâmetro hThread é o manipulador para o qual você quer obter as informações de temporização.
Os outros parâmetros para essa função são passados por referência e são preenchidos pela função. Aqui
está uma explicação de cada um:
l lpCreationTime. A hora da criação do thread.
l lpExitTime. A hora do término da execução do thread. Se o thread ainda estiver em execução, esse
valor será indefinido.
l lpKernelTime. Tempo que o thread gastou executando o código do sistema operacional.
l lpUserTime. Tempo que o thread gastou executando o código da aplicação.
Os quatro últimos parâmetros são do tipo TFileTime, que é definido na unidade Windows da seguinte
forma:
type
TFileTime = record
dwLowDateTime: DWORD;
dwHighDateTime: DWORD;
end;
Esse tipo de definição é um pouco incomum, mas faz parte da API do Win32, sendo assim: dwLowDa-
teTime e dwHighDateTime são combinados em um valor com palavra quádrupla (64 bits) que representa o nú-
mero de intervalos de 100 nanossegundos passados desde 1o de janeiro de 1601. Isso significa, obvia-
mente, que se você quisesse gravar uma simulação dos movimentos da frota inglesa enquanto derrota-
vam a Armada Espanhola em 1588, o tipo TFileTime seria uma maneira totalmente inadequada de manter
o controle do tempo... mas estamos só divagando.
DICA
Como o tamanho do tipo TFileTime é 64 bits, você pode fazer o typecasting e converter um TFileTime para
um tipo Int64 por uma questão de aritmética nos valores de TFileTime. O código a seguir demonstra como
saber rapidamente se um TFileTime é maior do que o outro:
Para ajudá-lo a trabalhar com os valores de TFileTime de um modo mais comum ao Delphi, as seguin-
tes funções permitem que você faça conversões entre os tipos TFileTime e TDateTime:
229
function FileTimeToDateTime(FileTime: TFileTime): TDateTime;
var
SysTime: TSystemTime;
begin
if not FileTimeToSystemTime(FileTime, SysTime) then
raise EConvertError.CreateFmt(‘FileTimeToSystemTime failed. ‘ +
‘Error code %d’, [GetLastError]);
with SysTime do
Result := EncodeDate(wYear, wMonth, wDay) +
EncodeTime(wHour, wMinute, wSecond, wMilliseconds)
end;
ATENÇÃO
Lembre-se de que a função GetThreadTimes( ) é implementada apenas no Windows NT/2000. A função
sempre retorna False quando é chamada no Windows 95 ou 98. Infelizmente, o Windows 95/98 não ofe-
rece qualquer mecanismo para recuperar informações sobre temporização de thread.
DICA
Acessar um campo de um objeto chega a ser dez vezes mais rápido do que acessar uma variável threadvar;
portanto você deve armazenar seus dados específicos do thread no seu descendente de TThread, se possí-
vel. Os dados que não precisarem continuar existindo após a duração de um determinado procedimento
ou função devem ser armazenados em variáveis locais, pois elas são mais rápidas até mesmo do que os
campos de um objeto TThread.
Se esse procedimento for chamado dentro do contexto de apenas um thread, não haverá qualquer
problema. Você pode chamar o procedimento uma vez para definir o valor de GlobalStr e chamá-lo de
novo para exibir o valor. No entanto, pense no que poderá acontecer se dois ou mais threads chamarem
esse procedimento em um determinado momento. Nesse caso, é possível que um thread chame o proce-
dimento para definir a string e, em seguida, outro thread que também pode chamar a função para definir
a string, torne-o preemptivo. Até o momento em que o sistema operacional der tempo de CPU de volta
ao primeiro thread, o valor de GlobalStr para esse thread estará irremediavelmente perdido.
Para situações como essa, o Win32 oferece uma facilidade conhecida como armazenamento local
de thread, que permite que sejam criadas cópias separadas das variáveis globais para cada thread em exe-
cução. O Delphi faz o encapsulamento de forma satisfatória dessa funcionalidade com a cláusula thread-
var. Simplesmente declare qualquer variável global que você queira que exista separadamente para cada 231
thread dentro de uma cláusula threadvar (ao contrário de var) e o trabalho estará feito. Uma nova declara-
ção da variável GlobalStr é tão simples quanto o exemplo abaixo:
threadvar
GlobalStr: String;
A unidade que aparece na Listagem 11.3 ilustra exatamente esse problema. Ela representa a unida-
de principal para uma aplicação do Delphi que contém apenas um botão em um formulário. Quando o
botão é acionado, o procedimento é chamado para definir e, em seguida, para exibir GlobalStr. Em segui-
da, outro thread é criado e o valor interno do thread é definido e aparece de novo. Após a criação do
thread, o thread principal novamente chama SetShowStr para exibir GlobalStr.
Tente executar essa aplicação com GlobalStr declarado como uma var e, em seguida, como uma thre-
advar. Você notará a diferença no resultado.
sunit Main;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TMainForm = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Declarações privadas }
public
{ Declarações públicas }
end;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
type
TTLSThread = class(TThread)
private
FNewStr: String;
protected
procedure Execute; override;
public
constructor Create(const ANewStr: String);
end;
232
Listagem 11.3 Continuação
procedure TTLSThread.Execute;
begin
FreeOnTerminate := True;
SetShowStr(FNewStr);
SetShowStr(‘’);
end;
end.
NOTA
O programa de demonstração chama o procedimento Sleep( ) da API do Win32 depois de criar o thread.
Sleep( ) é declarado como vemos a seguir:
O procedimento Sleep( ) avisa ao sistema operacional que o thread atual não precisa de mais nenhum ci-
clo da CPU por outros dwMilliseconds milissegundos. A inclusão dessa chamada no código tem o efeito de
simular condições do sistema onde mais multitarefa está ocorrendo e introduzir um pouco mais de “aleato-
riedade” nas aplicações quanto ao momento de execução de cada thread.
Geralmente, é aceitável passar zero no parâmetro dwMilliseconds. Apesar de não evitar que o thread atual
seja executado por algum tempo especificado, isso faz com que o sistema operacional dê ciclos de CPU
para qualquer thread à espera com prioridade igual ou superior.
Seja cauteloso ao usar Sleep( ) para contornar problemas de temporização desconhecidos. Sleep( )
pode funcionar para um determinado problema em sua máquina, mas os problemas de temporização que
não forem resolvidos definitivamente, aparecerão de novo na máquina de mais alguém, especialmente
quando a máquina for significativamente mais rápida ou mais lenta ou tiver um número de processadores
diferente da sua máquina.
233
Sincronismo de thread
Ao trabalhar com múltiplos threads, em geral você precisa sincronizar o acesso dos threads a algum re-
curso ou parte específica dos dados. Por exemplo, suponha que você tenha uma aplicação que utilize um
thread para ler um arquivo na memória e outro thread para contar o número de caracteres no arquivo. É
desnecessário dizer que você não consegue contar todos os caracteres no arquivo até que todo o arquivo
tenha sido carregado na memória. Porém, como cada operação ocorre em seu próprio thread, o sistema
operacional gostaria de tratá-las como duas tarefas completamente distintas. Para solucionar esse proble-
ma, você deve sincronizar os dois threads de forma que o thread contador não seja executado antes que o
thread carregador termine.
Esses são os tipos de problemas que o sincronismo de thread resolve e o Win32 oferece várias manei-
ras de sincronizar os threads. Nesta seção, você verá exemplos de técnicas de sincronismo de thread
usando seções críticas, mutexes, semáforos e eventos.
Para examinar essas técnicas, primeiro veja um problema que envolve threads que precisam ser sin-
cronizados. Como exemplo, suponha que você tenha um array de inteiros que precisa ser inicializado
com valores crescentes. Você quer primeiro definir os valores de 1 a 128 no array e, depois, reinicializar o
array com valores de 128 a 255. O thread final aparecerá então em uma caixa de listagem. Isso pode ser fei-
to inicializando-se dois threads separados. Considere o código na Listagem 11.4 para uma unidade que
tenta realizar essa tarefa.
unit Main;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TMainForm = class(TForm)
Button1: TButton;
ListBox1: TListBox;
procedure Button1Click(Sender: TObject);
private
procedure ThreadsDone(Sender: TObject);
end;
TFooThread = class(TThread)
protected
procedure Execute; override;
end;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
const
MaxSize = 128;
234
Listagem 11.4 Continuação
var
NextNumber: Integer = 0;
DoneFlags: Integer = 0;
GlobalArray: array[1..MaxSize] of Integer;
procedure TFooThread.Execute;
var
i: Integer;
begin
OnTerminate := MainForm.ThreadsDone;
for i := 1 to MaxSize do
begin
GlobalArray[i] := GetNextNumber; // define elemento do array
Sleep(5); // permite entrelaçamento do thread
end;
end;
end.
Como ambos os threads são executados simultaneamente, o que ocorre é que o conteúdo do array é
corrompido assim que ele é inicializado. Como exemplo, veja o resultado desse código, que aparece na
Figura 11.4.
A solução para esse problema é sincronizar os dois threads assim que eles acessam o array global, de
forma que eles não sejam inicializados ao mesmo tempo. Você pode escolher qualquer uma de uma série
de soluções válidas para esse problema.
235
FIGURE 11.4 Resultado da inicialização de array não-sincronizada.
Seções críticas
As seções críticas oferecem uma das formas mais simples de sincronizar os threads. Uma seção crítica é
uma seção de código que permite que apenas um thread seja executado de cada vez. Se você quiser confi-
gurar o código usado para inicializar o array em uma seção crítica, não será permitido que outros threads
entrem na seção de código até que o primeiro termine.
Antes de utilizar uma seção crítica, você deve inicializá-la utilizando o procedimento da API Initia-
lizeCriticalSection( ), declarado da seguinte forma:
NOTA
A Microsoft oculta deliberadamente a estrutura do registro TRTLCriticalSection porque o conteúdo varia
de uma plataforma de hardware para outra e porque é bem provável que mexer com o conteúdo dessa es-
trutura possa ocasionar danos em seu processo. Em sistemas Intel, a estrutura da seção crítica contém um
contador, um campo que contém o manipulador de thread atual e (provavelmente) um manipulador de um
evento do sistema. No hardware Alpha, o contador é substituído por uma estrutura de dados da CPU Alpha
denominada spinlock, que é muito mais eficiente do que a solução da Intel.
Quando o registro estiver preenchido, você poderá criar uma seção crítica em sua aplicação confi-
gurando algum bloco de código com chamadas para EnterCriticalSection( ) e LeaveCriticalSection( ).
Esses procedimentos são declarados da seguinte forma:
procedure EnterCriticalSection(var lpCriticalSection:
TRTLCriticalSection); stdcall;
procedure LeaveCriticalSection(var lpCriticalSection:
TRTLCriticalSection); stdcall;
Como você pode imaginar, o parâmetro lpCriticalSection passado é o mesmo que é preenchido pelo
procedimento InitializeCriticalSection( ).
Quando o registro TRTLCriticalSection estiver terminado, você deverá limpar chamando o procedi-
mento DeleteCriticalSection( ), que é declarado da seguinte forma:
procedure DeleteCriticalSection(var lpCriticalSection:
TRTLCriticalSection); stdcall;
A Listagem 11.5 demonstra a técnica de sincronismo de threads de inicialização em array com se-
236 ções críticas.
Listagem 11.5 Utilizando seções críticas
unit Main;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TMainForm = class(TForm)
Button1: TButton;
ListBox1: TListBox;
procedure Button1Click(Sender: TObject);
private
procedure ThreadsDone(Sender: TObject);
end;
TFooThread = class(TThread)
protected
procedure Execute; override;
end;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
const
MaxSize = 128;
var
NextNumber: Integer = 0;
DoneFlags: Integer = 0;
GlobalArray: array[1..MaxSize] of Integer;
CS: TRTLCriticalSection;
procedure TFooThread.Execute;
var
i: Integer;
begin
OnTerminate := MainForm.ThreadsDone;
EnterCriticalSection(CS); // seção crítica começa aqui
for i := 1 to MaxSize do
begin
GlobalArray[i] := GetNextNumber; // define o elemento do array
237
Listagem 11.5 Continuação
end.
Depois que o primeiro thread passa a chamada para EnterCriticalSection( ), todos os outros threads
são impedidos de entrar nesse bloco de código. O próximo thread que vier para essa linha de código será
colocado em descanso até que o primeiro thread chame LeaveCriticalSection( ). Nesse ponto, o segundo
thread será despertado e poderá tomar o controle da seção crítica. O resultado dessa aplicação quando os
threads são sincronizados aparece na Figura 11.5.
Mutexes
Os mutexes funcionam de forma bem parecida com as seções críticas, exceto por duas diferenças-chave.
Primeiro, os mutexes podem ser usados para sincronizar threads através dos limites do processo. Segun-
do, os mutexes podem receber um nome de string e podem ser criados manipuladores extras para os ob-
jetos mutex existentes através de referência a esse nome.
238
DICA
Semântica à parte, o desempenho é a maior diferença entre as seções críticas e os objetos de evento como
mutexes. As seções críticas são bem leves – apenas 10-15 ciclos de clock para entrar ou sair da seção críti-
ca quando não há colisão de thread. Quando houver uma colisão de thread para essa seção crítica, o sis-
tema criará um objeto de evento (provavelmente um mutex). O custo de usar objetos de evento tais como
mutexes é que isso requer uma viagem de ida e volta ao kernel, que exige uma troca de contexto do proces-
so e uma mudança de níveis de anel, gastando de 400 a 600 ciclos de clock em cada sentido. Todo esse
gasto ocorre mesmo que sua aplicação não tenha múltiplos threads ou que nenhum outro thread esteja
competindo pelo recurso que você está protegendo.
A função usada para criar um mutex é adequadamente denominada CreateMutex( ). Essa função é
declarada da seguinte forma:
function CreateMutex(lpMutexAttributes: PSecurityAttributes;
bInitialOwner: BOOL; lpName: PChar): THandle; stdcall;
lpMutexAttributes é um indicador para um registro TSecurityAttributes. É comum passar nil nesse pa-
râmetro, caso em que os atributos de segurança default serão utilizados.
bInitialOwner indica se o thread que está criando o mutex deve ser considerado o proprietário do
mutex quando for criado. Se esse parâmetro for False, o mutex não terá propriedade.
lpName é o nome do mutex. Esse parâmetro pode ser nil se você não quiser nomear o mutex. Se esse
parâmetro for diferente de nil, a função procurará no sistema um mutex existente com o mesmo nome.
Se for encontrado um mutex existente, será retornado um manipulador para o mutex existente. Caso
contrário, será retornado um manipulador para um novo mutex.
Quando terminar de usar um mutex, você deverá fechá-lo usando a função da API CloseHandle( ).
A Listagem 11.6 demonstra novamente a técnica de sincronismo dos threads para inicialização do
array, mas dessa vez utilizando mutexes.
unit Main;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TMainForm = class(TForm)
Button1: TButton;
ListBox1: TListBox;
procedure Button1Click(Sender: TObject);
private
procedure ThreadsDone(Sender: TObject);
end;
TFooThread = class(TThread)
protected
procedure Execute; override;
end;
var 239
Listagem 11.6 Continuação
MainForm: TMainForm;
implementation
{$R *.DFM}
const
MaxSize = 128;
var
NextNumber: Integer = 0;
DoneFlags: Integer = 0;
GlobalArray: array[1..MaxSize] of Integer;
hMutex: THandle = 0;
procedure TFooThread.Execute;
var
i: Integer;
begin
FreeOnTerminate := True;
OnTerminate := MainForm.ThreadsDone;
if WaitForSingleObject(hMutex, INFINITE) = WAIT_OBJECT_0 then
begin
for i := 1 to MaxSize do
begin
GlobalArray[i] := GetNextNumber; // define elemento do array
Sleep(5); // permite entrelaçamento do thread
end;
end;
ReleaseMutex(hMutex);
end;
begin
hMutex := CreateMutex(nil, False, nil);
TFooThread.Create(False); // cria threads
TFooThread.Create(False);
end;
end.
Você perceberá que nesse caso a função WaitForSingleObject( ) é utilizada para controlar a entrada
do thread no bloco de código sincronizado. Essa função é declarada da seguinte forma:
function WaitForSingleObject(hHandle: THandle; dwMilliseconds: DWORD):
DWORD; stdcall;
O objetivo dessa função é colocar o thread atual para descansar durante dwMilliseconds até que o ob-
jeto da API especificado no parâmetro hHandle torne-se sinalizado. Sinalizado tem diferentes significados
para diferentes objetos. Um mutex torna-se sinalizado quando pertence a um thread, enquanto que um
processo, por exemplo, torna-se sinalizado quando termina. Além de um período de tempo real, o parâme-
tro dwMilliseconds também pode ter o valor 0, o que significa que o status do objeto deve ser verificado e re-
tornado imediatamente, ou INFINITE, que significa que deve-se esperar para sempre que o objeto fique sina-
lizado. O valor de retorno dessa função pode ser qualquer um dos valores que aparecem na Tabela 11.3.
Valor Significado
WAIT_ABANDONED O objeto especificado é um objeto mutex e o thread que possui o mutex foi
terminado antes que ele liberasse o mutex. Essa circunstância é referenciada como um
mutex abandonado; nesse caso, a propriedade do objeto mutex é conferida ao thread
de chamada e o mutex é configurado como não-sinalizado.
WAIT_OBJECT_0 O estado do objeto especificado é sinalizado.
WAIT_TIMEOUT Intervalo de tempo limite decorrido, e o estado do objeto é não-sinalizado.
Novamente, quando um mutex não é de propriedade de um thread, ele está no estado sinalizado. O
primeiro thread a chamar WaitForSingleObject( ) nesse mutex passa a ser o proprietário do mutex e o esta-
do do objeto mutex é configurado para não-sinalizado. A propriedade do mutex em relação ao thread é
interrompida quando o thread chama a função ReleaseMutex( ) passando o manipulador do mutex como
parâmetro. Nesse ponto, o estado do mutex novamente torna-se sinalizado.
NOTA
Além de WaitForSingleObject( ), a API do Win32 também contém funções denominadas WaitForMulti-
pleObjects( ) e MsgWaitForMultipleObjects( ), que permitem que você espere que o estado de um ou
mais objetos fique sinalizado. Essas funções estão documentadas na ajuda on-line da API do Win32.
Semáforos
Outras técnica de sincronismo de thread envolve o uso de objetos de semáforo da API. Os semáforos
constroem a funcionalidade dos mutexes enquanto acrescentam um importante recurso: oferecem a ca-
pacidade de contagem de recursos, de forma que um número predeterminado de threads possa entrar em 241
partes sincronizadas de código de uma só vez. A função usada para criar um semáforo é CreateSemapho-
re( ),e é declarada da seguinte forma:
function CreateSemaphore(lpSemaphoreAttributes: PSecurityAttributes;
lInitialCount, lMaximumCount: Longint; lpName: PChar): THandle;stdcall;
unit Main;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TMainForm = class(TForm)
Button1: TButton;
ListBox1: TListBox;
procedure Button1Click(Sender: TObject);
private
procedure ThreadsDone(Sender: TObject);
end;
TFooThread = class(TThread)
protected
procedure Execute; override;
end;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
const
MaxSize = 128;
var
242 NextNumber: Integer = 0;
Listagem 11.7 Continuação
DoneFlags: Integer = 0;
GlobalArray: array[1..MaxSize] of Integer;
hSem: THandle = 0;
procedure TFooThread.Execute;
var
i: Integer;
WaitReturn: DWORD;
begin
OnTerminate := MainForm.ThreadsDone;
WaitReturn := WaitForSingleObject(hSem, INFINITE);
if WaitReturn = WAIT_OBJECT_0 then
begin
for i := 1 to MaxSize do
begin
GlobalArray[i] := GetNextNumber; // define elemento do array
Sleep(5); // permite entrelaçamento do thread
end;
end;
ReleaseSemaphore(hSem, 1, nil);
end;
end.
243
Como você permite que apenas um thread entre na parte de código sincronizada, a contagem máxi-
ma para o semáforo, nesse caso, é 1.
A função ReleaseSemaphore( ) é usada para aumentar a contagem para o semáforo. Observe que essa
função é um pouco mais complicada do que ReleaseMutex( ). A declaração para ReleaseSemaphore( ) é a se-
guinte:
function ReleaseSemaphore(hSemaphore: THandle; lReleaseCount: Longint;
lpPreviousCount: Pointer): BOOL; stdcall;
O parâmetro lReleaseCount permite que você especifique o número a ser aumentado na contagem do
semáforo. A contagem anterior será armazenada no longint indicado pelo parâmetro lpPreviousCount se
seu valor não for Nil. Um comprometimento sutil desse recurso é que um semáforo nunca é realmente
possuído por um thread específico. Por exemplo, suponha que a contagem máxima de um semáforo seja
10 e que 10 threads chamem WaitForSingleObject( ) para definir a contagem do thread para 0 e para colo-
cá-lo em um estado não-sinalizado. Tudo o que é necessário é que um desses threads chame lRelease-
Semaphore( ) com 10 como o parâmetro lReleaseCount, não apenas para tornar o parâmetro novamente si-
nalizado, mas também para aumentar a contagem novamente para 10. Esse poderoso recurso pode ocasio-
nar alguns bugs difíceis de serem rastreados em suas aplicações, e por isso deve ser utilizado com cautela.
Certifique-se de usar a função CloseHandle( ) para liberar o manipulador de semáforo alocado com
CreateSemaphore( ).
unit SrchU;
interface
type
TSearchThread = class(TThread)
private
LB: TListbox;
CaseSens: Boolean;
FileNames: Boolean;
Recurse: Boolean;
SearchStr: string;
SearchPath: string;
FileSpec: string;
AddStr: string;
FSearchFile: string;
procedure AddToList;
procedure DoSearch(const Path: string);
procedure FindAllFiles(const Path: string);
procedure FixControls;
procedure ScanForStr(const FName: string; var FileStr: string);
procedure SearchFile(const FName: string);
procedure SetSearchFile;
protected
procedure Execute; override;
public
constructor Create(CaseS, FName, Rec: Boolean; const Str, SPath,
FSpec: string);
destructor Destroy; override;
end;
implementation
FileNames := FName;
Recurse := Rec;
SearchStr := Str;
SearchPath := AddBackSlash(SPath);
FileSpec := FSpec;
inherited Create(False);
end;
destructor TSearchThread.Destroy;
begin
FSearchFile := ‘’;
Synchronize(SetSearchFile);
Synchronize(FixControls);
inherited Destroy;
end;
procedure TSearchThread.Execute;
begin
FreeOnTerminate := True; // define todos os campos
LB := MainForm.lbFiles;
Priority := TThreadPriority(MainForm.SearchPri);
if not CaseSens then SearchStr := UpperCase(SearchStr);
FindAllFiles(SearchPath); // processa o diretório atual
if Recurse then // se subdiretório, então...
DoSearch(SearchPath); // faz a recursão, caso contrário...
end;
procedure TSearchThread.FixControls;
{ Ativa os controles no formulário principal. Deve ser chamado
através de Synchronize }
begin
MainForm.EnableSearchControls(True);
end;
procedure TSearchThread.SetSearchFile;
{ Atualiza a barra de status com nome do arquivo. Deve ser chamado
através de Synchronize }
begin
MainForm.StatusBar.Panels[1].Text := FSearchFile;
end;
procedure TSearchThread.AddToList;
{ Acrescenta string à caixa de listagem principal. Deve ser chamado
através de Synchronize }
begin
LB.Items.Add(AddStr);
end;
FindPos: integer;
begin
FindPos := Pos(SearchStr, FileStr);
FoundOnce := False;
while (FindPos < > 0) and not Terminated do
begin
if not FoundOnce then
begin
{ usa “:” apenas se o usuário não selecionar “apenas nome
do arquivo” }
if FileNames then
Marker := ‘’
else
Marker := ‘:’;
{ acrescenta arquivo à caixa de listagem }
AddStr := Format(‘File %s%s’, [FName, Marker]);
Synchronize(AddToList);
FoundOnce := True;
end;
{ não procura a mesma string no mesmo arquivo em caso
de apenas nome do arquivo }
if FileNames then Exit;
ScanForStr(FName, SearchString);
except
on Exception do
begin
AddStr := Format(‘Error reading file: %s’, [FName]);
Synchronize(AddToList);
end;
end;
end;
end.
248
Muitas coisas acontecem nessa unidade, e merecem alguma explicação. Primeiro, você observará o
procedimento PrintStrings( ) que é utilizado para enviar o conteúdo das TStrings para a impressora. Para
fazer isso, o procedimento utiliza as vantagens do procedimento-padrão AssignPrn( ) do Delphi, que atri-
bui uma variável TextFile à impressora. Dessa forma, qualquer texto gravado em TextFile é automatica-
mente escrito na impressora. Quando você terminar de imprimir na impressora, certifique-se de utilizar
o procedimento CloseFile( ) para fechar a conexão com a impressora.
Também é importante o uso do procedimento da API ShellExecute( ) do Win32 para executar um
visualizador para um arquivo que aparecerá na caixa de listagem. ShellExecute( ) não apenas permite que
você chame programas executáveis como também permite que chame associações para extensões de ar-
quivo registradas. Por exemplo, se você tentar chamar um arquivo com uma extensão pas usando Shell-
Execute( ), o Delphi será automaticamente carregado para visualizar o arquivo.
DICA
Se ShellExecute( ) retornar um valor indicando um erro, a aplicação chamará RaiseLastWin32Error( ).
Esse procedimento, localizado na unidade SysUtils, chama a função da API GetLastError( ) e a SysError-
Message( ) do Delphi para obter informações mais detalhadas sobre o erro e para formatar tais informa-
ções em uma string. Você pode usar RaiseLastWin32Error( ) dessa maneira em suas próprias aplicações
se quiser que seus usuários obtenham mensagens de erro detalhadas sobre as falhas da API.
O thread de pesquisa
O mecanismo de pesquisa está presente dentro de uma unidade denominada SrchU.pas, que aparece na
Listagem 11.9. Essa unidade faz uma série de coisas interessantes, inclusive copiar um arquivo inteiro em
uma string, fazer a recursão de subdiretórios e passar informações de volta ao formulário principal.
unit SrchU;
interface
type
TSearchThread = class(TThread)
private
LB: TListbox;
CaseSens: Boolean;
FileNames: Boolean;
Recurse: Boolean;
SearchStr: string;
SearchPath: string;
FileSpec: string;
AddStr: string;
FSearchFile: string;
procedure AddToList;
procedure DoSearch(const Path: string);
procedure FindAllFiles(const Path: string);
procedure FixControls;
procedure ScanForStr(const FName: string; var FileStr: string);
procedure SearchFile(const FName: string);
249
Listagem 11.9 Continuação
procedure SetSearchFile;
protected
procedure Execute; override;
public
constructor Create(CaseS, FName, Rec: Boolean; const Str, SPath,
FSpec: string);
destructor Destroy; override;
end;
implementation
destructor TSearchThread.Destroy;
begin
FSearchFile := ‘’;
Synchronize(SetSearchFile);
Synchronize(FixControls);
inherited Destroy;
end;
procedure TSearchThread.Execute;
begin
FreeOnTerminate := True; // define todos os campos
LB := MainForm.lbFiles;
Priority := TThreadPriority(MainForm.SearchPri);
if not CaseSens then SearchStr := UpperCase(SearchStr);
FindAllFiles(SearchPath); // processa o diretório atual
if Recurse then // se subdirs, então...
DoSearch(SearchPath); // faz a recursão, caso contrário...
end;
procedure TSearchThread.FixControls;
{ Ativa os controles no formulário principal. Deve ser chamado
através de Synchronize }
begin
MainForm.EnableSearchControls(True);
end;
procedure TSearchThread.SetSearchFile;
{ Atualiza o status da barra com nome de arquivo. Deve ser chamado
250
Listagem 11.9 Continuação
através de Synchronize }
begin
MainForm.StatusBar.Panels[1].Text := FSearchFile;
end;
procedure TSearchThread.AddToList;
{ Acrescenta a string à caixa de listagem principal. Deve ser
chamado através de Synchronize }
begin
LB.Items.Add(AddStr);
end;
DataFile: THandle;
FileSize: Integer;
SearchString: string;
begin
FSearchFile := FName;
Synchronize(SetSearchFile);
try
DataFile := FileOpen(FName, fmOpenRead or fmShareDenyWrite);
if DataFile = 0 then raise Exception.Create(‘’);
try
{ define o comprimento da string de pesquisa }
FileSize := GetFileSize(DataFile, nil);
SetLength(SearchString, FileSize);
{ Copia os dados do arquivo para a string }
FileRead(DataFile, Pointer(SearchString)^, FileSize);
finally
CloseHandle(DataFile);
end;
if not CaseSens then SearchString := UpperCase(SearchString);
ScanForStr(FName, SearchString);
except
on Exception do
begin
AddStr := Format(‘Error reading file: %s’, [FName]);
Synchronize(AddToList);
end;
end;
end;
end.
Quando criado, esse thread chama primeiro seu método FindAllFiles( ). Esse método usa Find-
First( ) e FindNext( ) para pesquisar todos os arquivos no diretório atual correspondentes à especificação
de arquivo indicada pelo usuário. Se o usuário tiver optado pela recursão de subdiretórios, então será
chamado o método DoSearch( ) para examinar a árvore de um diretório. Esse método novamente utiliza
FindFirst( ) e FindNext( ) para localizar diretórios, mas o detalhe é que ele chama a si próprio repetida-
mente para examinar a árvore. Assim que cada diretório é localizado, FindAllFiles( ) é chamado para
processar todos os arquivos correspondentes no diretório.
DICA
O algoritmo de recursão usado pelo método DoSearch( ) é uma técnica-padrão para examinar a árvore de
diretórios. Como é obviamente difícil depurar algoritmos recursivos, o programador que for esperto utiliza-
rá os que já são conhecidos. É uma boa idéia guardar esse método para que você possa utilizá-lo futura-
mente com outras aplicações.
Para processar cada arquivo, você perceberá que o algoritmo de pesquisa por um token dentro de
um arquivo envolve a utilização do objeto TMemMapFile, que faz o encapsulamento de um arquivo mapeado
na memória do Win32. Esse objeto é discutido em detalhes no Capítulo 12, mas por enquanto você deve
considerar apenas que isso oferece uma maneira fácil de mapear o conteúdo de uma arquivo na memória.
O algoritmo inteiro funciona da seguinte forma:
1. Quando um arquivo correspondente à especificação de arquivo é localizado pelo método FindAllFi-
les( ), o método SearchFile( ) é chamado e o conteúdo é copiado em uma string.
2. O método ScanForStr( ) é chamado para cada string de arquivo. ScanForStr( ) pesquisa ocorrências do
token da pesquisa dentro de cada string.
3. Quando é localizada uma ocorrência, o nome do arquivo e/ou a linha de texto é acrescentada à caixa
de listagem. A linha de texto é acrescentada apenas quando a caixa de seleção File Names Only (ape-
nas nomes de arquivo) não estiver marcada pelo usuário.
Observe que todos os métodos no objeto TSearchThread verificam periodicamente o status do flag
StopIt (que é disparado com a solicitação de parada do thread) e o flag Terminated (que é disparado com o
término do objeto TThread).
253
ATENÇÃO
Lembre-se de que qualquer método dentro de um objeto TThread que modifique a interface do usuário da
aplicação de qualquer forma deve ser chamado através do método Synchronize( ) ou a interface do usuá-
rio deve ser modificada pelo envio de mensagens.
Definindo a prioridade
Apenas para acrescentar mais um recurso, DelSrch permite que o usuário defina dinamicamente a priori-
dade do thread de pesquisa. O formulário usado para esse objetivo aparece na Figura 11.7 e a unidade
para esse formulário, PRIU.PAS, aparece na Listagem 11.10.
unit PriU;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, ComCtrls, Buttons, ExtCtrls;
type
TThreadPriWin = class(TForm)
tbrPriTrackBar: TTrackBar;
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
btnOK: TBitBtn;
btnRevert: TBitBtn;
Panel1: TPanel;
procedure tbrPriTrackBarChange(Sender: TObject);
procedure btnRevertClick(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure FormShow(Sender: TObject);
procedure btnOKClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
{ Declarações privadas }
OldPriVal: Integer;
public
{ Declarações públicas}
end;
var
ThreadPriWin: TThreadPriWin;
254
Listagem 11.10 Continuação
implementation
{$R *.DFM}
end.
O código para essa unidade é bem simples. Tudo o que ele faz é definir o valor da variável
SearchPri no formulário principal para corresponder ao da posição no controle de barra deslizante. Se o
thread estiver em execução, ele também definirá a prioridade do thread. Como TThreadPriority é um
tipo numerado, um typecast direto mapeia os valores de 1 a 5 no controle deslizante para enumerações
em TThreadPriority.
255
Acesso ao banco de dados em multithreading
Apesar de a programação de banco de dados não ser realmente discutida antes do Capítulo 28, esta seção
destina-se a dar algumas dicas sobre como usar múltiplos threads no contexto de desenvolvimento do
banco de dados. Se você não estiver familiarizado com a programação do banco de dados no Delphi,
deve consultar o Capítulo 28 antes de continuar lendo esta seção.
A exigência mais comum para os programadores de aplicações de bancos de dados no Win32 é a ca-
pacidade de realizar procedimentos armazenados ou consultas complexas em um thread em segundo
plano. Felizmente, esse tipo de procedimento é aceito pelo Borland Database Engine (BDE) de 32 bits e é
fácil de ser feito no Delphi.
Na verdade, existem apenas duas exigências para executar uma consulta em segundo plano através
de, por exemplo, um componente TQuery:
l Cada consulta encadeada deve residir dentro de sua própria seção. Você pode oferecer a um
TQuery sua própria sessão colocando um componente TSession em seu formulário e atribuindo seu
nome à propriedade SessionName de TQuery. Isso também implica que, se seu TQuery usar um com-
ponente TDatabaset, você terá que usar um TDatabase exclusivo para cada sessão.
l O TQuery não deve ser anexado a nenhum componente TDataSource no momento em que a consul-
ta é aberta a partir do thread secundário. Quando a consulta é anexada a um TDataSource, isso
deve ser feito através do contexto do thread principal. TDataSource é usado apenas para conectar
datasets aos controles da interface do usuário e a manipulação da interface do usuário deve ser
realizada no thread principal.
Para ilustrar as técnicas de consultas em segundo plano, a Figura 11.8 mostra o formulário principal
para um projeto de demonstração denominado BDEThrd. Esse formulário permite que você especifique um
alias do BDE, um nome de usuário e uma senha para um determinado banco de dados e insira consulta
em relação ao banco de dados. Ao dar um clique no botão Go!, um thread secundário é gerado para pro-
cessar a consulta e os resultados aparecem em um formulário filho.
O formulário filho TQueryForm aparece na Figura 11.9. Observe que esse formulário contém um com-
ponente TQuery, TDatabase, TSession, TDataSource e TDBGrid. Sendo assim, cada instância de TQueryForm possui
suas próprias instâncias desses componentes.
unit Main;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs, Grids, StdCtrls, ExtCtrls;
type
TMainForm = class(TForm)
pnlBottom: TPanel;
pnlButtons: TPanel;
GoButton: TButton;
Button1: TButton;
memQuery: TMemo;
pnlTop: TPanel;
Label1: TLabel;
AliasCombo: TComboBox;
Label3: TLabel;
UserNameEd: TEdit;
Label4: TLabel;
PasswordEd: TEdit;
Label2: TLabel;
procedure Button1Click(Sender: TObject);
procedure GoButtonClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
{ Declarações privadas }
public
{ Declarações públicas }
end;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
var
FQueryNum: Integer = 0;
end.
Como você pode ver, não há muita coisa nova nessa unidade. A caixa de combinação AliasCombo é
preenchida com aliases do BDE no manipulador OnCreate para o formulário principal usando o método
GetAliasNames( ) de TSession. O manipulador para o evento OnClick do botão Go! é responsável pela cha-
mada de uma nova consulta, chamando o procedimento NewQuery( ) que fica em uma unidade secundária,
QryU.pas. Observe que ele passa um novo número exclusivo, FQueryNum, para o procedimento NewQuery( ) a
cada vez que o botão é acionado. Esse número é usado para criar um nome do banco de dados e uma ses-
são exclusiva para cada thread de consulta.
O código para a unidade QryU aparece na Listagem 11.12.
unit QryU;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Grids,
DBGrids, DB, DBTables, StdCtrls;
type
TQueryForm = class(TForm)
Query: TQuery;
DataSource: TDataSource;
Session: TSession;
Database: TDatabase;
dbgQueryGrid: TDBGrid;
memSQL: TMemo;
procedure FormClose(Sender: TObject; var Action: TCloseAction);
private
{ Declarações privadas }
public
{ Declarações públicas }
end;
implementation
type
TDBQueryThread = class(TThread)
private
FQuery: TQuery;
FDataSource: TDataSource;
FQueryException: Exception;
procedure HookUpUI;
procedure QueryError;
protected
procedure Execute; override;
public
constructor Create(Q: TQuery; D: TDataSource); virtual;
end;
procedure TDBQueryThread.Execute;
begin
try
FQuery.Open; // abre a consulta
Synchronize(HookUpUI); // atualiza o thread do formulário principal da UI
except
FQueryException := ExceptObject as Exception;
Synchronize(QueryError); // mostra exceção a partir do thread principal
end;
end;
procedure TDBQueryThread.HookUpUI;
begin
FDataSource.DataSet := FQuery;
end;
procedure TDBQueryThread.QueryError;
begin
Application.ShowException(FQueryException);
end;
with Database do
begin
{ define um nome exclusivo para o banco de dados }
DatabaseName := Format(‘DB%d’, [QryNum]);
{ define parâmetro alias }
AliasName := Alias;
{ relaciona o banco de dados à sessão }
SessionName := Session.SessionName;
{ senha e nome de usuário definidos pelo usuário }
Params.Values[‘USER NAME’] := UserName;
Params.Values[‘PASSWORD’] := Password;
end;
with Query do
begin
{ relaciona a consulta ao banco de dados e à sessão }
DatabaseName := Database.DatabaseName;
SessionName := Session.SessionName;
{ define as strings da consulta }
SQL.Assign(Qry);
end;
{ mostra as strings da consulta em SQL Memo }
memSQL.Lines.Assign(Qry);
{ mostra o formulário da consulta }
Show;
{ abre a a consulta em seu próprio thread }
TDBQueryThread.Create(Query, DataSource);
end;
end;
end.
O procedimento NewQuery( ) cria uma nova instância do formulário filho TQueryForm, define as pro-
priedades para cada um dos seus componentes de acesso aos dados e cria nomes exclusivos para seus
componentes TDatabase e TSession. A propriedade SQL da consulta é preenchida a partir das TStrings passa-
das no parâmetro Qry e o thread de consulta é então gerado.
O código dentro do próprio TDBQueryThread é um tanto quanto disperso. O programador simples-
mente define algumas variáveis de instância e o método Execute( ) abre a consulta e chama o método
HookupUI( ) através de Synchronize( ) para anexar a consulta à origem dos dados. Você deve observar tam-
bém o bloco try..except dentro do procedimento Execute( ), que usa Synchronize( ) para mostrar as men-
sagens de exceção a partir do contexto do thread principal.
Gráficos de multithreading
Mencionamos anteriormente que a VCL não se destina a ser manipulada simultaneamente por múltiplos
threads, mas essa afirmação não está totalmente correta. A VCL permite que múltiplos threads manipu-
lem objetos gráficos individuais. Graças aos novos métodos Lock( ) e Unlock( ) introduzidos em TCanvas,
toda a unidade Graphics tornou-se protegida contra threads. Isso inclui as classes TCanvas, TPen, TBrush,
260 TFont, TBitmap, TMetafile, TPicture e TIcon.
O código para esses métodos Lock( ) são semelhantes aos que usam uma seção crítica e à função
EnterCriticalSection( ) da API (descrita anteriormente neste capítulo) para manter o acesso à tela de dese-
nho (canvas) ou objeto gráfico. Depois que um determinado thread chama um método Lock( ), esse thre-
ad está liberado para manipular exclusivamente o objeto gráfico ou tela de desenho. Outros threads es-
perando para entrar na parte do código após a chamada para Lock( ) serão colocados para descansar até
que o thread proprietário da seção crítica chame Unlock( ), que, por sua vez, chama LeaveCriticalSecti-
on( ) para liberar a seção crítica e deixar o próximo thread à espera (se houver algum) na parte de código
protegida. O trecho de código a seguir mostra como esses métodos podem ser usados para controlar o
acesso a um objeto de tela de desenho:
Form.Canvas.Lock;
// o código que manipula a tela de desenho entra aqui
Form.Canvas.Unlock;
Para ilustrar melhor esse ponto, a Listagem 11.13 mostra a unidade Main do projeto MTGraph – uma
aplicação que demonstra múltiplos threads acessando a tela de desenho de um formulário.
unit Main;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Menus;
type
TMainForm = class(TForm)
MainMenu1: TMainMenu;
Options1: TMenuItem;
AddThread: TMenuItem;
RemoveThread: TMenuItem;
ColorDialog1: TColorDialog;
Add10: TMenuItem;
RemoveAll: TMenuItem;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure AddThreadClick(Sender: TObject);
procedure RemoveThreadClick(Sender: TObject);
procedure Add10Click(Sender: TObject);
procedure RemoveAllClick(Sender: TObject);
private
ThreadList: TList;
public
{ Declarações públicas }
end;
TDrawThread = class(TThread)
private
FColor: TColor;
FForm: TForm;
public
constructor Create(AForm: TForm; AColor: TColor);
procedure Execute; override;
end; 261
Listagem 11.13 Continuação
var
MainForm: TMainForm;
implementation
{$R *.DFM}
{ TDrawThread }
procedure TDrawThread.Execute;
var
P1, P2: TPoint;
procedure GetRandCoords;
var
MaxX, MaxY: Integer;
begin
{ inicializa P1 e P2 para pontos aleatórios dentro dos
limites do Formulário }
MaxX := FForm.ClientWidth;
MaxY := FForm.ClientHeight;
P1.x := Random(MaxX);
P2.x := Random(MaxX);
P1.y := Random(MaxY);
P2.y := Random(MaxY);
end;
begin
FreeOnTerminate := True;
// thread é executado até que ele ou a aplicação termine
while not (Terminated or Application.Terminated) do
begin
GetRandCoords; // inicializa P1 e P2
with FForm.Canvas do
begin
Lock; // bloqueia tela de desenho
// apenas um thread por vez pode executar o código a seguir:
Pen.Color := FColor; // define cor da caneta
MoveTo(P1.X, P1.Y); // move para a posição 1 da tela
LineTo(P2.X, P2.Y); // desenha uma linha para a posição P2
// após a execução da próxima linha, outro thread terá
// a entrada permitida no bloco de código acima
Unlock; // desbloqueia a tela de desenho
end;
end;
end;
{ TMainForm }
262
Listagem 11.13 Continuação
initialization
Randomize; // nova semente do gerador de números aleatórios
end.
263
Essa aplicação possui um menu principal que tem quatro itens, conforme aparece na Figura 11.10.
O primeiro item, Add thread (acrescentar thread), cria uma nova instância de TDrawThread, que pinta li-
nhas aleatórias no formulário principal. Essa opção pode ser selecionada repetidamente para jogar mais e
mais threads na mistura de threads acessando o formulário principal. O próximo item, Remove thread
(remover thread), remove o último thread acrescentado. O terceiro item, Add 10 (acrescentar 10), cria
10 novas instâncias de TDrawThread. Por último, o quarto item, Remove all (remover tudo), termina e des-
trói todas as instâncias de TDrawThread. A Figura 11.10 também mostra os resultados de 10 threads dese-
nhando simultaneamente na tela do formulário.
As regras de bloqueio da tela de desenho determinam que, como cada usuário de uma tela a bloque-
ia antes de desenhar e a desbloqueia depois, múltiplos threads que utilizam essa tela não podem interferir
um com o outro. Observe que todos os eventos OnPaint e as chamadas ao método Paint( ) iniciadas pela
VCL automaticamente bloqueiam e desbloqueiam a tela para você; portanto, o código normal e existen-
te do Delphi pode coexistir com novas operações gráficas de thread em segundo plano.
Utilizando essa aplicação como exemplo, avalie as conseqüências ou os sintomas de colisões de
threads se você não realizar adequadamente o bloqueio da tela. Se o thread um definir uma cor vermelha
para a caneta da tela e, em seguida, desenhar uma linha e o thread dois definir uma cor azul e desenhar
um círculo, e se esses threads não bloquearem a tela antes de iniciarem essa operação, o seguinte cenário
de colisão de thread será possível: o thread um define a cor da caneta como vermelha. O scheduler do sis-
tema passa a execução para o thread dois. O thread dois define a cor da caneta para azul e desenha um
círculo. A execução muda para o thread um. O thread um desenha uma linha. Porém, a linha não é ver-
melha, é azul porque o thread dois teve a oportunidade de intervir nas operações do thread um.
Observe também que apenas um thread incorreto causa problema. Se o thread um bloquear a tela e
o thread dois não, o cenário descrito permanecerá o mesmo. Os dois threads devem bloquear a tela em
todas as suas operações de tela para evitar tal cenário de colisão de thread.
Resumo
Até agora você teve uma apresentação completa sobre os threads e como utilizá-los de forma adequada
no ambiente Delphi. Você aprendeu diversas técnicas de sincronismo de múltiplos threads e também
como fazer a comunicação entre threads secundários e o thread principal de uma aplicação Delphi. Além
disso, você viu exemplos de utilização de threads dentro do contexto da aplicação de pesquisa de um ar-
quivo real, obteve informações sobre como aproveitar os threads nas aplicações de bancos de dados e
aprendeu como desenhar em uma TCanvas com múltiplos threads. No próximo capítulo, você aprenderá
diversas técnicas para trabalhar com diferentes tipos de arquivos no Delphi.
264
Trabalho com CAPÍTULO
arquivos
12
NE STE C AP ÍT UL O
l Tratamento do I/O de arquivo 266
l As estruturas de registro TTextRec e TFileRec 284
l Trabalho com arquivos mapeados na
memória 285
l Diretórios e unidades de disco 300
l Uso da função SHFileOperation( ) 319
l Resumo 322
Trabalhar com arquivos, diretórios e unidades de disco é uma tarefa de programação comum que, sem
dúvida, algum dia você terá de realizar. Este capítulo ilustra como trabalhar com diferentes tipos de ar-
quivo: arquivos de texto, arquivos tipificados e arquivos não-tipificados. O capítulo abrange como utili-
zar um TFileStream para encapsular o I/O de arquivo e como se beneficiar a partir de um dos melhores re-
cursos do Win32: arquivos mapeados na memória. Você criará uma classe, TMemoryMappedFile, que pode
ser utilizada e que faz o encapsulamento de algumas das funcionalidades mapeadas na memória, e apren-
derá como utilizar essa classe para executar buscas de texto em arquivos de texto. Este capítulo também
demonstra algumas rotinas úteis para determinar as unidades de disco disponíveis, analisar árvores de di-
retório para localizar arquivos e obter informações sobre versão dos arquivos. Ao concluir este capítulo,
você será capaz de trabalhar com arquivos, diretórios e unidades de disco.
Agora, você pode utilizar essa variável para se referir a um arquivo de texto.
Você precisa conhecer dois procedimentos para abrir o arquivo. O primeiro procedimento é Assign-
File( ). AssignFile( ) associa um nome de arquivo à variável do arquivo:
AssignFile(MyTextFile, ‘MyTextFile.txt’);
Depois de associar a variável de arquivo a um nome de arquivo, você poderá abrir o arquivo. Você
poderá abrir um arquivo de texto de três maneiras. Primeiro, pode criar e abrir um arquivo utilizando o
procedimento Rewrite( ). Se você utilizar Rewrite( ) em um arquivo existente, ele será gravado por cima e
um novo será criado com o mesmo nome. Você também pode abrir um arquivo com acesso apenas de lei-
tura utilizando o procedimento Reset( ). Você pode anexar a um arquivo existente utilizando o procedi-
mento Append( ).
NOTA
Reset( ) abre os arquivos tipificados e não-tipificados com acesso apenas de leitura.
Para fechar um arquivo após abri-lo, você utiliza o procedimento CloseFile( ). Observe os exemplos
a seguir, os quais ilustram cada procedimento.
Para abrir com acesso apenas de leitura, utilize este procedimento:
var
MyTextFile: TextFile;
begin
266 AssignFile(MyTextFile, ‘MyTextFile.txt’);
Reset(MyTextFile);
try
{ manipula o arquivo }
finally
CloseFile(MyTextFile);
end;
end;
A Listagem 12.1 mostra como você utilizaria Rewrite( ) para criar um arquivo e nele adicionar cinco
linhas de texto.
var
MyTextFile: TextFile;
S: String;
i: integer;
begin
AssignFile(MyTextFile, ‘MyTextFile.txt’);
Rewrite(MyTextFile);
try
for i := 1 to 5 do
begin
S := ‘This is line # ‘;
Writeln(MyTextFile, S, i);
end;
finally
CloseFile(MyTextFile);
end;
end;
267
Esse arquivo agora iria conter o seguinte texto:
This is line # 1
This is line # 2
This is line # 3
This is line # 4
This is line # 5
A Listagem 12.2 ilustra como você adicionaria mais cinco linhas ao mesmo arquivo.
var
MyTextFile: TextFile;
S: String;
i: integer;
begin
AssignFile(MyTextFile, ‘MyTextFile.txt’);
Append(MyTextFile);
try
for i := 6 to 10 do
begin
S := ‘This is line # ‘;
Writeln(MyTextFile, S, i);
end;
finally
CloseFile(MyTextFile);
end;
end;
Observe que em ambas as listagens você foi capaz de gravar uma string e um número inteiro no ar-
quivo. O mesmo acontece para todos os tipos numéricos em Object Pascal. Para ler a partir desse mesmo
arquivo de texto, você faria como pode ser visto na Listagem 12.3.
var
MyTextFile: TextFile;
S: String[15];
i: integer;
j: integer;
268 begin
Listagem 12.3 Continuação
AssignFile(MyTextFile, ‘MyTextFile.txt’);
Reset(MyTextFile);
try
while not Eof(MyTextFile) do
begin
Readln(MyTextFile, S, j);
Memo1.Lines.Add(S+IntToStr(j));
end;
finally
CloseFile(MyTextFile);
end;
end;
Na Listagem 12.3, você notará que a variável de string S é declarada como String[15]. Isso é necessá-
rio para impedir a leitura da linha interia do arquivo na variável, S. Não fazer isso teria causado um erro
ao se tentar ler um valor na variável inteira J. Isso ilustra outro recurso importante do I/O de arquivo de
texto: você pode escrever colunas em arquivos de texto. Essas colunas podem então ser lidas em strings
de um tamanho específico. É importante que cada coluna seja definida para um tamanho específico, em-
bora as strings reais armazenadas lá possam ser de um tamanho diferente. Além disso, observe o uso da
função Eof( ). Essa função realiza um teste para determinar se o ponteiro do arquivo está no final do ar-
quivo. Se estiver, você terá de sair do loop, pois não há mais texto para ser lido.
Para ilustrar a leitura de um arquivo de texto formatado em colunas, criamos um arquivo de texto
chamado USCaps.txt, que contém uma lista das capitais dos EUA em uma arrumação por colunas. Uma
parte desse arquivo aparece aqui:
Alabama Montgomery
Alaska Juneau
Arizona Phoenix
Arkansas Little Rock
California Sacramento
Colorado Denver
Connecticut Hartford
Delaware Dover
A coluna do nome do estado possui exatamente 20 caracteres. Desse modo, as capitais são alinha-
das verticalmente. Criamos um projeto que lê esse arquivo e armazena os estados em uma tabela do Para-
dox. Você encontrará esse projeto no CD como Capitals.dpr. Seu código-fonte aparece na Listagem 12.4.
NOTA
Antes que você possa executar essa demonstração, terá de criar o alias do BDE, DDGData. Caso contrário,
o programa falhará. Se você instalou o software a partir do CD deste livro, esse alias já foi criado para você.
unit MainFrm;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, Grids, DBGrids, DB, DBTables;
269
Listagem 12.4 Continuação
type
TMainForm = class(TForm)
btnReadCapitals: TButton;
tblCapitals: TTable;
dsCapitals: TDataSource;
dbgCapitals: TDBGrid;
procedure btnReadCapitalsClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
end;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
end.
270
Embora este livro ainda não tenha abordado a programação de banco de dados no Delphi, o código
anterior é muito simples. O mais importante aqui é explicar que, de um modo geral, o processamento de
arquivos de texto pode ter alguma finalidade muito útil. Esse arquivo de texto pode muito bem ter sido um
arquivo contendo informações de conta bancária retiradas de um serviço bancário on-line, por exemplo.
NOTA
Registros que contêm strings ANSI, variantes, instâncias de classe, interfaces ou arrays dinâmicos não po-
dem ser gravados em um arquivo.
Agora suponha que você queira armazenar um ou mais desses registros em um arquivo. Na seção
anterior, você já viu que é possível fazer isso usando um arquivo de texto. No entanto, isso também pode
ser feito por meio de um arquivo de registro, definido da seguinte forma:
DataFile: File of TPersonRec;
Observe o uso do procedimento Seek( ) para mover a posição do arquivo para o final do arquivo an-
tes de gravar o registro. O uso dessa função é bastante documentado na ajuda on-line do Delphi, de
modo que não entraremos em detalhes sobre isso agora.
Para ilustrar o uso dos arquivos tipificados, criamos uma pequena aplicação que armazena informa-
ções sobre pessoas em um formato do Object Pascal. Essa aplicação permite procurar, incluir e editar es-
ses registros. Também ilustramos o uso de um descendente de TFileStream, que usamos para encapsular o
I/O do arquivo para tais registros.
NOTA
O streaming é um tópico que abordamos com mais profundidade no Capítulo 22.
unit persrec;
interface
uses Classes, dialogs, sysutils;
type
TRecordStream = class(TFileStream)
private
function GetNumRecs: Longint;
function GetCurRec: Longint;
procedure SetCurRec(RecNo: Longint);
protected
272 function GetRecSize: Longint; virtual;
Listagem 12.5 Continuação
public
function SeekRec(RecNo: Longint; Origin: Word): Longint;
function WriteRec(const Rec): Longint;
function AppendRec(const Rec): Longint;
function ReadRec(var Rec): Longint;
procedure First;
procedure Last;
procedure NextRec;
procedure PreviousRec;
// NumRecs mostra o número de registros no stream
property NumRecs: Longint read GetNumRecs;
// CurRec reflete o registro atual no stream
property CurRec: Longint read GetCurRec write SetCurRec;
end;
implementation
function TRecordStream.GetRecSize:Longint;
begin
{ Esta função retorna o tamanho do registro a respeito do qual este stream
conhece (TPersonRec) }
Result := SizeOf(TPersonRec);
end;
procedure TRecordStream.First;
begin
{ Esta função posiciona o ponteiro de arquivo no início do stream }
Seek(0, 0);
end;
procedure TRecordStream.Last;
begin
// Este procedimento posiciona o ponteiro de arquivo no final do stream
Seek(0, 2);
Seek(-GetRecSize, 1);
end;
procedure TRecordStream.NextRec;
begin
{ Este procedimento posiciona o ponteiro de arquivo no próximo
local de registro. }
procedure TRecordStream.PreviousRec;
274
Listagem 12.5 Continuação
begin
{ Este procedimento posiciona o ponteiro de arquivo no registro anterior
do stream. }
{ Chama essa função, desde que não estendamos para além do início
do arquivo }
if (Position - GetRecSize >= 0) then
Seek(-GetRecSize, 1)
else
Raise Exception.Create(‘Cannot read beyond beginning of the file.’);
end;
end.
Nesta unidade, primeiro você declara o registro que deseja armazenar, TPersonRec. TRecordStream é o
descendente de TFileStream que você usa para realizar o I/O de arquivo para TPersonRec. TRecordStream pos-
sui duas propriedades: NumRecs, que indica o número de registros no sistema, e CurRec, que indica o regis-
tro atual que o stream está visualizando.
O método GetNumRecs( ), que é o método de acesso para a propriedade NumRecs, determina quantos
registros existem no stream. Ele faz isso dividindo o tamanho total do stream em bytes, conforme deter-
minado na propriedade TStream.Size, pelo tamanho do registro TPersonRec. Portanto, dado que o registro
TPersonRec possui 56 bytes, se a propriedade Size tiver o valor 162, haveria quatro registros no stream.
Observe, no entanto, que você só pode ter certeza de que o registro possui 56 bytes se ele estiver compac-
tado (com packed). O motivo por trás disso é que os tipos estruturados, como registros e arrays, são alinha-
dos pelos limites de palavra ou de dupla palavra para permitir o acesso mais rápido. Isso pode significar que
o registro consome mais espaço do que realmente precisa. Usando a palavra reservada packed antes da de-
claração do registro, você pode garantir um armazenamento de dados compactado e preciso. Se não for
usada a palavra-chave packed, você pode obter resultados pouco precisos com o método GetNumRecs( ).
O método GetCurRec( ) determina o registro atual. Você faz isso dividindo a propriedade TStream.Po-
sition pelo tamanho da propriedade TPersonRec e somando 1 ao valor. O método SetCurRec( ) coloca o
ponteiro de arquivo na posição do fluxo que é o início do registro especificado pela propriedade RecNo.
O método SeekRec( ) permite que o procedimento que chama coloque o ponteiro de arquivo em
uma posição determinada pelos parâmetros RecNo e Origin. Esse método move o ponteiro do arquivo para
frente ou para trás no fluxo, a partir da posição inicial, final ou atual do ponteiro de arquivo, conforme
especificado pelo valor da propriedade Origin. Isso é feito usando-se o método Seek( ) do objeto TStream.
O uso do método TStream.Seek( ) é explicado no arquivo de ajuda on-line “Component Writers Guide”
(guia para criadores de componentes).
O método WriteRec( ) grava o conteúdo do parâmetro TPersonRec no arquivo, na posição atual, que
será a posição de um registro existente, de modo que gravará sobre esse registro.
O método AppendRec( ) inclui um novo registro ao final do arquivo.
O método ReadRec( ) lê os dados do stream no parâmetro TPersonRec. Depois ele reposiciona o pon-
teiro de arquivo no início do registro, usando o método Seek( ). O motivo para isso é que, para usar o ob-
jeto TRecordStream em um padrão de banco de dados, o ponteiro de arquivo sempre precisa estar no início
do registro atual (ou seja, no registro sendo visto).
Os métodos First( ) e Last( ) colocam o ponteiro do arquivo no início e no final do arquivo, respec-
tivamente.
O método NextRec( ) coloca o ponteiro do arquivo no início do próximo registro, desde que o pon-
teiro de arquivo já não esteja no último registro do arquivo.
O método PreviousRec( ) coloca o ponteiro do arquivo no início do registro anterior, desde que o
ponteiro de arquivo já não esteja no primeiro registro do arquivo.
275
Usando um descendente de TFileStream para o I/O de arquivo
A Listagem 12.6 é o código-fonte para o formulário principal de uma aplicação que utiliza o objeto TRe-
cordStream.
Esse projeto é FileOfRec.dpr no CD.
unit MainFrm;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls, Mask, Persrec, ComCtrls;
const
// Declara o nome do arquivo como uma constante
FName = ‘PERSONS.DAT’;
type
TMainForm = class(TForm)
edtFirstName: TEdit;
edtLastName: TEdit;
edtMI: TEdit;
meAge: TMaskEdit;
lblFirstName: TLabel;
lblLastName: TLabel;
lblMI: TLabel;
lblBirthDate: TLabel;
lblAge: TLabel;
btnFirst: TButton;
btnNext: TButton;
btnPrev: TButton;
btnLast: TButton;
btnAppend: TButton;
btnUpdate: TButton;
btnClear: TButton;
lblRecNoCap: TLabel;
lblRecNo: TLabel;
lblNumRecsCap: TLabel;
lblNoRecs: TLabel;
dtpBirthDay: TDateTimePicker;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure FormShow(Sender: TObject);
procedure btnAppendClick(Sender: TObject);
procedure btnUpdateClick(Sender: TObject);
procedure btnFirstClick(Sender: TObject);
procedure btnNextClick(Sender: TObject);
procedure btnLastClick(Sender: TObject);
procedure btnPrevClick(Sender: TObject);
procedure btnClearClick(Sender: TObject);
public
276 PersonRec: TPersonRec;
Listagem 12.6 Continuação
RecordStream: TRecordStream;
procedure ShowCurrentRecord;
end;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
procedure TMainForm.ShowCurrentRecord;
begin
// Lê o registro atual.
RecordStream.ReadRec(PersonRec);
// Copia os dados de PersonRec para os controles no formulário
with PersonRec do
begin
edtFirstName.Text := FirstName;
edtLastName.Text := LastName;
edtMI.Text := MI;
dtpBirthDay.Date := BirthDay;
meAge.Text := IntToStr(Age);
end;
// Mostra número do registro e total de registros no formulário principal.
lblRecNo.Caption := IntToStr(RecordStream.CurRec);
lblNoRecs.Caption := IntToStr(RecordStream.NumRecs);
end;
begin
// Copia o conteúdo dos controles do formulário para registro PersonRec
with PersonRec do
begin
FirstName := edtFirstName.Text;
LastName := edtLastName.Text;
MI := edtMI.Text;
BirthDay := dtpBirthDay.Date;
Age := StrToInt(meAge.Text);
end;
// Grava o novo registro no stream
RecordStream.AppendRec(PersonRec);
// Exibe o registro atual.
ShowCurrentRecord;
end;
begin
{ Vai para o último registro do stream, desde que haja registros
no stream }
if RecordStream.NumRecs < > 0 then
begin
RecordStream.Last;
ShowCurrentRecord;
end;
end;
end.
O método ShowCurrentRecord( ) é usado para extrair o registro atual do stream, chamando o método
RecordStream.ReadRec( ). Lembre-se de que o método RecordStream.ReadRec( ) primeiro lê o registro, o que
posiciona o ponteiro de arquivo para o final do registro depois de ser lido. Depois ele reposiciona o pon-
teiro do arquivo no início do registro.
279
A maior parte da funcionalidade dessa aplicação é discutida no comentário do arquivo-fonte. Dis-
cutiremos rapidamente apenas os pontos mais importantes.
O método btnAppendClick( ) insere um novo registro no arquivo.
O método btnUpdateClick( ) grava o conteúdo dos controles do formulário na posição do registro
ativo, modificando assim o conteúdo nessa posição.
Os métodos restantes reposicionam o ponteiro do arquivo nos registros seguinte, anterior, primeiro
e último no arquivo, permitindo assim que você navegue pelos registros no arquivo.
Esse exemplo ilustra como você pode usar arquivos tipificados para realizar operações simples no
banco de dados usando I/O de arquivo-padrão. Ele também ilustra como utilizar o objeto TFileStream
para obter a funcionalidade de I/O dos registros no arquivo.
Isso declara um arquivo consistindo em uma seqüência de blocos, cada um tendo 128 bytes de dados.
Para ler dados de um arquivo não-tipificado, você usaria o procedimento BlockRead( ). Para gravar
dados em um arquivo não-tipificado, você usa o procedimento BlockWrite( ). Esses procedimentos são
declarados da seguinte forma:
procedure BlockRead(var F: File; var Buf;
➥Count: Integer [; var Result: Integer]);
Tanto BlockRead( ) quanto BlockWrite( ) utilizam três parâmetros. O primeiro parâmetro é uma va-
riável de arquivo não-tipificado, F. O segundo parâmetro é um buffer de variável, Buf, que contém os da-
dos lidos ou gravados no arquivo. O parâmetro Count contém o número de registros a serem lidos do ar-
quivo. O parâmetro opcional Result contém o número de registros lidos do arquivo em uma operação de
leitura. Em uma operação de gravação, Result contém o número de registros completos gravados. Se esse
valor não for igual a Count, é possível que o disco esteja sem espaço.
Explicaremos o que estamos querendo dizer quando falamos que esses procedimentos lêem e gra-
vam registros Count. Quando você declara um arquivo não-tipificado da seguinte forma, por default, isso
define um arquivo cujos registros consistem em 128 bytes de dados:
UntypedFile: File;
Isso não tem nada a ver com qualquer estrutura de registro em particular. Simplesmente especifica o
tamanho do bloco de dados que é lido para um único registro. A Listagem 12.7 ilustra como ler um regis-
tro de 128 bytes de um arquivo:
280
Listagem 12.7 Lendo de um arquivo não-tipificado
var
UnTypedFile: File;
Buffer: array[0..128] of byte;
NumRecsRead: Integer;
begin
AssignFile(UnTypedFile, ‘SOMEFILE.DAT’);
Reset(UnTypedFile);
try
BlockRead(UnTypedFile, Buffer, 1, NumRecsRead);
finally
CloseFile(UnTypedFile);
end;
end;
Aqui, você abre o arquivo SOMEFILE.DAT e lê 128 bytes de dados (um registro ou bloco) no buffer
apropriadamente chamado Buffer. Para gravar 128 bytes de dados em um arquivo, dê uma olhada na Lis-
tagem 12.8.
var
UnTypedFile: File;
Buffer: array[0..128] of byte;
NumRecsWritten: Integer;
begin
AssignFile(UnTypedFile, ‘SOMEFILE.DAT’);
// Se o arquivo não existir, ele é criado. Caso contrário,
// basta abri-lo para acesso de leitura/escrita
if FileExists(‘SOMEFILE.DAT’) then
Reset(UnTypedFile)
else
Rewrite(UnTypedFile);
try
// Posiciona o ponteiro de arquivo para o final do arquivo
Seek(UnTypedFile, FileSize(UnTypedFile));
FillChar(Buffer, SizeOf(Buffer), ‘Y’);
BlockWrite(UnTypedFile, Buffer, 1, NumRecsWritten);
finally
CloseFile(UnTypedFile);
end;
end;
Um problema com o uso do tamanho de bloco default de 128 bytes ao se ler de um arquivo é que
seu tamanho deve ser um múltiplo de 128 para evitar a leitura além do final do arquivo. Você pode con-
tornar essa situação especificando um tamanho de registro de um byte com o procedimento Reset( ). Se
você passar um tamanho de registro de um byte, a leitura de blocos de qualquer tamanho sempre será um
múltiplo de um byte. Como exemplo, a Listagem 12.9, que utiliza os procedimentos Blockread( ) e Block-
Write( ), ilustra uma rotina simples de cópia de arquivo.
281
Listagem 12.9 Demonstração de cópia de arquivo
unit MainFrm;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls, ComCtrls, Gauges;
type
TMainForm = class(TForm)
prbCopy: TProgressBar;
btnCopy: TButton;
procedure btnCopyClick(Sender: TObject);
end;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
begin
BlockWrite(DestFile, Buffer, BytesRead, BytesWritten);
if BytesRead < > BytesWritten then
raise Exception.Create(‘Error copying file’)
else begin
TotalRead := TotalRead + BytesRead;
prbCopy.Position := Trunc(TotalRead / Fsize) * 100;
prbCopy.Update;
end;
end
until BytesRead = 0;
except
{ Havendo uma exceção, apaga o arquivo de destino por poder estar
danificado. Depois gera a exceção novamente. }
Erase(DestFile);
raise;
end;
finally
CloseFile(DestFile); // Fecha o arquivo de destino.
end;
finally
CloseFile(SrcFile); // Fecha o arquivo de origem.
end;
end;
end.
NOTA
Uma das demonstrações que vem com o Delphi 5 contém diversas funções úteis para tratamento de arqui-
vos, incluindo uma função para copiar um arquivo. Essa demonstração está no diretório \DEMOS\
DOC\FILMANEX\. Aqui estão as funções contidas no arquivo FmxUtils.PAS:
Além disso, mais adiante neste capítulo, vamos mostrar como copiar arquivos e diretórios inteiros usando a
função ShFileOperation( ).
Primeiramente, a demonstração abre um arquivo de origem para entrada e cria um arquivo de des-
tino no qual os dados do arquivo de origem serão copiados. As variáveis TotalRead e FSize são usadas na
atualização de um componente TProgressBar para indicar o status da operação de cópia. Na verdade, a
operação de cópia é realizada dentro do loop repeat. Primeiro, SizeOf(Buffer) bytes são lidos do arquivo
de origem. A variável BytesRead determina o número real de bytes lidos. Depois, tenta-se copiar BytesRead
para o arquivo de destino. O número de bytes reais gravados é armazenado na variável BytesWritten. Nes-
se ponto, se não tiver havido erros, BytesRead e BytesWritten terão os mesmos valores. Esse processo conti- 283
nua até que todos os bytes do arquivo tenham sido copiados. Se houver um erro, uma exceção será gera-
da e o arquivo de destino será apagado do disco.
Uma aplicação de exemplo ilustrando o código anterior encontra-se no CD com o nome File-
Copy.dpr.
TFileRec = record
Handle: Integer; // Alça do arquivo
Mode: Integer; // Modo do arquivo
RecSize: Cardinal; // Tamanho de cada registro no arquivo
Private: array[1..28] of Byte; // Usado internamente pelo Object Pascal
UserData: array[1..32] of Byte; // Não usado.
Name: array[0..259] of Char; // Caminho completo até o arquivo
end;
284
Trabalho com arquivos mapeados na memória
Provavelmente, uma das características mais convenientes do ambiente Win32 é a capacidade de acessar
arquivos no disco como se você estivesse acessando o conteúdo do arquivo na memória. Essa capacidade
é oferecida por meio de arquivos mapeados na memória.
Arquivos mapeados na memória permitem evitar a realização de todas as operações de I/O no ar-
quivo. Ao invés disso, você reserva um intervalo do espaço de endereços virtual e entrega o armazena-
mento físico do arquivo em disco ao endereço desse espaço reservado na memória. Depois você pode re-
ferenciar o conteúdo do arquivo através de um ponteiro para essa região reservada. Em breve, vamos
mostrar como você pode usar esse recurso para criar um utilitário importante de procura de texto para
arquivos de texto, simplificado com o uso de arquivos mapeados na memória.
Essa função cria um novo arquivo com o nome especificado por seu parâmetro de string FileName. Se
a função tiver sucesso, uma alça de arquivo válida será retornada. Caso contrário, será retornado o valor
definido pela constante INVALID_HANDLE_VALUE.
FileOpen( ) abre um arquivo existente usando um modo de acesso especificado. Essa função, quan-
do tiver sucesso, retornará uma alça de arquivo válida. Caso contrário, ela retornará o valor definido
pela constante INVALID_HANDLE_VALUE. FileOpen( ) é definida na unidade SysUtils.pas da seguinte maneira:
function FileOpen(const FileName: string; Mode: Word): Integer;
O primeiro parâmetro é o nome completo do caminho até o arquivo onde o mapeamento deve ser
aplicado. O segundo parâmetro é um dos modos de acesso ao arquivo, conforme descritos na Tabe-
la 12.1.
Se você especificar um valor 0 como o parâmetro Mode, não poderá ler ou gravar no arquivo especifi-
cado. Você poderia usar isso quando quiser apenas obter os vários atributos do arquivo. Você pode espe-
cificar como um arquivo pode ser compartilhado com diferentes aplicações aplicando a operação de bit
or, usando os modos de acesso especificados na Tabela 12.1 com um dos modos de fmShareXXXX. Os modos
de fmShareXXXX estão relacionados na Tabela 12.2.
Depois que uma alça de arquivo válida for obtida, é possível obter um objeto de arquivo mapeado.
PAGE_READONLY Você pode ler o conteúdo do arquivo. O arquivo precisa ter sido criado com a
função FileCreate( ) ou aberto com FileOpen( ) e um modo de acesso
fmOpenRead.
PAGE_READWRITE Você pode ler e gravar no arquivo. O arquivo precisa ter sido aberto com o
modo de acesso fmOpenReadWrite.
PAGE_WRITECOPY Você pode ler e gravar no arquivo. No entanto, quando você grava no arquivo,
é criada uma cópia privada da página modificada. O significado disso é que os
arquivos mapeados na memória compartilhados entre processos não
consomem o dobro dos recursos em memória do sistema ou uso de arquivo de
swap (paginação). Só é duplicada a memória necessária para as páginas
diferentes. O arquivo precisa ter sido aberto com o acesso fmOpenWrite ou
fmOpenReadWrite.
Você também pode aplicar atributos de seção a flProtect usando o operador de bit or. A Tabela 12.4
explica o significado desses atributos.
287
O parâmetro dwMaximumSizeHigh especifica os 32 bits de alta ordem do tamanho máximo do objeto de
arquivo mapeado. A não ser que você esteja acessando arquivos maiores do que 4GB, esse valor sempre
será zero.
O parâmetro dwMinimumSizeLow especifica os 32 bits de baixa ordem do tamanho máximo do objeto
de arquivo mapeado. Um valor zero para esse parâmetro indicaria um tamanho máximo para o objeto de
arquivo mapeado igual ao tamanho do arquivo sendo mapeado.
O parâmetro lpName especifica o nome do objeto de arquivo mapeado. Esse valor poderá conter
qualquer caracter exceto uma contrabarra (\). Se esse parâmetro combinar com o nome de um objeto de
arquivo mapeado já existente, a função solicita acesso a esse mesmo objeto de arquivo mapeado usando
os atributos especificados pelo parâmetro flProtect. É válido passar nil nesse parâmetro, o que cria um
objeto de arquivo mapeado sem nome.
Se CreateFileMapping( ) tiver sucesso, ele retornará uma alça válida para um objeto de arquivo ma-
peado. Se esse objeto de arquivo mapeado se referir a um objeto de arquivo mapeado já existente, o valor
ERROR_ALREADY_EXISTS será retornado da função GetLastError( ). Se CreateFileMapping( ) falhar, ela retornará
um valor nil. Você precisa chamar a função GetLastError( ) para determinar o motivo da falha.
ATENÇÃO
Sob o Windows 95/98, não use funções de I/O de arquivo sobre alças de arquivo que tenham sido usadas
para criar mapeamentos de arquivo. Os dados nesses arquivos podem não ser coerentes. Portanto, reco-
menda-se que você abra o arquivo com acesso exclusivo. Ver seção “Coerência de arquivo mapeado na
memória”.
Depois de ter obtido um objeto de arquivo mapeado válido, você poderá mapear os dados do arqui-
vo no espaço de endereços do processo.
hFileMappingObject é uma alça para um objeto de arquivo mapeado aberto, que foi aberto com uma
chamada à função CreateFileMapping( ) ou OpenFileMapping( ).
O parâmetro dwDesiredAccess indica como os dados do arquivo devem ser acessados, e pode ser um
dos valores especificados na Tabela 12.5.
O parâmetro dwFileOffsetHigh especifica os 32 bits de alta ordem do deslocamento do arquivo onde
o mapeamento de arquivo se inicia.
O parâmetro dwFileOffsetLow especifica os 32 bits de baixa ordem do deslocamento do arquivo onde
o mapeamento se inicia.
O parâmetro dwNumberOfBytesToMap indica quantos bytes do arquivo devem ser mapeados. O valor
zero indica o arquivo inteiro.
MapViewOfFile( ) retorna o endereço inicial da visão mapeada. Se não tiver sucesso, nil é retornado e
você precisa chamar a função GetLastError( ) para determinar a causa do erro.
288
Tabela 12.5 Acesso desejado à visão do arquivo
O único parâmetro da função, lpBaseAddress, precisa apontar para o endereço de base da região ma-
peada. Esse é o mesmo valor retornado da função MapViewOfFile( ).
Você precisa chamar UnmapViewOfFile( ) quando tiver acabado de trabalhar com o arquivo; caso con-
trário, a região mapeada da memória não será liberada pelo sistema até que o processo termine.
Se a chamada a CloseHandle( ) tiver sucesso, ela retornará True. Caso contrário, retornará False e
você terá que examinar o resultado de GetLastError( ) para determinar a causa do erro.
unit MainFrm;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls;
const 289
Listagem 12.10 Continuação
FName = ‘test.txt’;
type
TMainForm = class(TForm)
btnUpperCase: TButton;
memTextContents: TMemo;
lblContents: TLabel;
btnLowerCase: TButton;
procedure btnUpperCaseClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure btnLowerCaseClick(Sender: TObject);
public
UCase: Boolean;
procedure ChangeFileCase;
end;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
procedure TMainForm.ChangeFileCase;
var
FFileHandle: THandle; // Alça para o arquivo aberto.
FMapHandle: THandle; // Alça para um objeto de arquivo mapeado
FFileSize: Integer; // Variável para conter tamanho do arquivo.
FData: PByte; // Ponteiro para dados do arquivo quando mapeado.
PData: PChar; // Ponteiro usado para referenciar dados do arquivo.
begin
{ Primeiro apanha uma alça para o arquivo a ser aberto. Este código
290
Listagem 12.10 Continuação
try
{ Agora apanha o tamanho do arquivo, que passaremos às outras funções
de mapeamento de arquivo. Tornaremos esse tamanho um byte maior,
pois temos que anexar um caracter de terminação nula ao final
dos dados do arquivo mapeado. }
FFileSize := GetFileSize(FFileHandle, Nil);
if FMapHandle = 0 then
raise Exception.Create(‘Failed to create file mapping’);
finally
// Libera a alça do arquivo
CloseHandle(FFileHandle);
end;
try
{ Mapeia o objeto de arquivo mapeado para uma visão. Isso fará
retornar um ponteiro para os dados do arquivo. Se a função não tiver
sucesso, então gera uma exceção. }
FData := MapViewOfFile(FMapHandle, FILE_MAP_ALL_ACCESS, 0, 0, FFileSize);
finally
// Libera a alça do objeto de arquivo mapeado
CloseHandle(FMapHandle);
end;
try
{ !!! Aqui é onde você colocaria as funções para trabalhar com
os dados do arquivo mapeado. Por exemplo, a linha a seguir força
todos os caracteres do arquivo para maiúsculas }
PData := PChar(FData);
// Posiciona o ponteiro para o final dos dados do arquivo
inc(PData, FFileSize);
291
Listagem 12.10 Continuação
finally
// Libera o mapeamento de arquivo.
UnmapViewOfFile(FData);
end;
memTextContents.Lines.Clear;
memTextContents.Lines.LoadFromFile(FName);
end;
end.
Você verá, pela Listagem 12.10, que o primeiro passo é obter uma alça para o arquivo a ser mapea-
do na região da memória do processo. Isso é feito chamando-se a função FileOpen( ). Você passa o modo
de acesso do arquivo fmOpenReadWrite para a função a fim de receber acesso de leitura/gravação ao conteú-
do do arquivo.
Em seguida, você apanha o tamanho do arquivo e muda o último caracter para uma terminação
nula. Esse deverá realmente ser o marcador de fim de arquivo, que é o mesmo valor de byte da termina-
ção nula. Isso é feito aqui por questão de clareza. O motivo é que, como você está manipulando os dados
do arquivo como uma string de terminação nula, terá que garantir que haverá um valor nulo presente no
final.
A etapa seguinte apanha o objeto do arquivo de mapeamento da memória, chamando CreateFileMap-
ping( ). Se essa função falhar, você gerará uma exceção. Caso contrário, seguirá adiante para a próxima
etapa, mapeando o objeto de arquivo mapeado em uma visão. Mais uma vez, você gera uma exceção se
essa função fracassar.
Depois você muda o texto nos dados do arquivo. Se você visse o arquivo em um editor de textos de-
pois de executar essa rotina, veria que os caracteres do arquivo foram todos convertidos para o tipo de
letra selecionado. Por fim, você desmapeia a visão do arquivo, chamando a função UnMapViewOfFile( ).
Você pode ter notado que, neste código, tanto a alça do arquivo quanto a alça do objeto de arquivo
mapeado são liberadas antes que você sequer manipule os dados do arquivo, após terem sido mapeados
para uma visão. Isso é possível porque o sistema mantém uma contagem de uso da alça do arquivo e do
objeto de arquivo mapeado quando é fetia a chamada a MapViewOfFile( ). Portanto, você pode fechar o
objeto logo no início chamando CloseHandle( ), reduzindo assim as chances de causar uma brecha nos re-
cursos. Mais adiante, você verá um uso mais elaborado para os arquivos mapeados na memória, enquan-
to cria uma classe TMemoryMapFile e a utiliza para realizar buscas em arquivos de texto.
Esse projeto também ilustra como encapsular a funcionalidade dos arquivos mapeados na memória
em um objeto. Para mostrar isso, criamos a classe TMemMapFile.
A classe TMemMapFile
A unidade contendo a classe TMemMapFile aparece na Listagem 12.11.
Listagem 12.11 O código-fonte para MemMap.pas, a unidade que define a classe TMemMapFile
unit MemMap;
interface
type
EMMFError = class(Exception);
TMemMapFile = class
private
FFileName: String; // Nome do arquivo mapeado.
FSize: Longint; // Tamanho da visão mapeada
FFileSize: Longint; // Tamanho real do arquivo
FFileMode: Integer; // Modo de acesso ao arquivo
FFileHandle: Integer; // Alça do arquivo
293
Listagem 12.11 Continuação
implementation
try
AllocFileMapping; // Apanha a alça do objeto de map. de arquivo.
except
on EMMFError do
begin
CloseHandle(FFileHandle); // Fecha alça do arquivo se houver erro
294
Listagem 12.11 Continuação
destructor TMemMapFile.Destroy;
begin
procedure TMemMapFile.FreeMapping;
{ Este método desmapeia a visão do arquivo a partir do espaço de
endereços desse processo. }
begin
if FData < > Nil then
begin
UnmapViewOfFile(FData);
FData := Nil;
end;
end;
procedure TMemMapFile.AllocFileHandle;
{ Cria ou abre arquivo de disco antes de criar arquivo mapeado na memória }
begin
if FFileMode = fmCreate then
FFileHandle := FileCreate(FFileName)
else
FFileHandle := FileOpen(FFileName, FFileMode);
295
Listagem 12.11 Continuação
procedure TMemMapFile.AllocFileMapping;
var
ProtAttr: DWORD;
begin
if FFileMode = fmOpenRead then // Apanha atributos de proteção corretos
ProtAttr := Page_ReadOnly
else
ProtAttr := Page_ReadWrite;
{ Tenta criar mapeamento do arquivo em disco.
Raise exception on error. }
FMapHandle := CreateFileMapping(FFileHandle, Nil, ProtAttr,
0, FSize, Nil);
if FMapHandle = 0 then
raise EMMFError.Create(‘Failed to create file mapping’);
end;
procedure TMemMapFile.AllocFileView;
var
Access: Longint;
begin
if FFileMode = fmOpenRead then // Apanha modo de arquivo correto
Access := File_Map_Read
else
Access := File_Map_All_Access;
FData := MapViewOfFile(FMapHandle, Access, 0, 0, FSize);
if FData = Nil then
raise EMMFError.Create(‘Failed to map view of file’);
end;
end.
unit MainFrm;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
296 Dialogs, StdCtrls, Buttons, FileCtrl;
Listagem 12.12 Continuação
type
TMainForm = class(TForm)
btnSearch: TButton;
lbFilesFound: TListBox;
edtSearchString: TEdit;
lblSearchString: TLabel;
lblFilesFound: TLabel;
memFileText: TMemo;
btnFindNext: TButton;
FindDialog: TFindDialog;
dcbDrives: TDriveComboBox;
dlbDirectories: TDirectoryListBox;
procedure btnSearchClick(Sender: TObject);
procedure lbFilesFoundClick(Sender: TObject);
procedure btnFindNextClick(Sender: TObject);
procedure FindDialogFind(Sender: TObject);
procedure edtSearchStringChange(Sender: TObject);
procedure memFileTextChange(Sender: TObject);
public
end;
var
MainForm: TMainForm;
implementation
uses MemMap, Search;
{$R *.DFM}
begin
FindDialog.FindText := edtSearchString.Text;
FindDialog.Execute;
FindDialog.Top := Top+Height;
FindDialogFind(FindDialog);
end;
end.
Este projeto realiza uma pesquisa diferenciando maiúsculas de minúsculas sobre os arquivos de tex-
to do diretório ativo.
btnSearchClick( ) contém o código que realiza a pesquisa real, determina o número de vezes em que
a string especificada aparece em cada arquivo e inclui os arquivos que contêm a string na caixa de lista-
gem lbFilesFound.
Primeiro, é usada a seqüência de chamadas a FindFirst( )/FindNext( ) para localizar cada arquivo
com uma extensão .txt no diretório ativo. Essas duas funções são discutidas mais adiante neste capítulo.
O método utiliza, então, uma classe TMemMapFile no arquivo temporário para ter acesso aos dados do ar-
quivo. Esse arquivo é aberto com acesso apenas de leitura, pois você não o modificará. As linhas de códi-
go a seguir realizam a lógica necessária para obter uma contagem do número de vezes que a string apare-
ce no arquivo:
if FoundStr < > nil then
begin
repeat
inc(WordCount);
inc(FoundStr, length(FoundStr));
FoundStr := StrPos(PChar(FoundStr), PChar(FindString))
until FoundStr = nil;
Tanto o nome do arquivo quanto o número de ocorrências da string no arquivo são acrescentados
na caixa de listagem lbFilesFound.
Quando o usuário dá um clique duplo em um item de TListBox, o arquivo é carregado no controle
TMemo, onde o usuário poderá localizar cada ocorrência da string dando um clique no botão Find Next (lo-
calizar próxima).
299
O manipulador de evento btnFindNext( ) inicializa a propriedade FindDialog.FindText como a string
em edtSearchString. Depois ele chama FindDialog.
Quando o usuário dá um clique no botão Find Next de FindDialog, seu manipulador de evento OnFind
é chamado. Esse manipulador de evento é FindDialogFind( ). FindDialogFind( ) utiliza a função SearchMe-
mo( ), que é definida na unidade Search.pas.
SearchMemo( ) percorre o texto de qualquer descendente de TCustomEdit e seleciona esse texto, o que o
faz aparecer.
NOTA
A unidade Search.pas é um arquivo que vem no Borland Delphi 1.0 como um de seus arquivos de demons-
tração. Obtivemos permissão da Borland para incluir esse arquivo no CD-ROM que acompanha este livro.
Essa unidade não utiliza os diversos recursos de tratamento de string, pois foi projetada para o Delphi 1.0.
No entanto, fizemos uma pequena mudança para permitir que um controle TMemo mostrasse o cursor de
edição caret, o que era feito automaticamente no Windows 3.1. No Win32, você precisa passar uma men-
sagem EM_SCROLLCARET para o controle TMemo após definir sua propriedade SelStart. Leia os comentários
em Search.pas para obter mais informações.
C: String;
DType: Integer;
DriveString: String;
begin
{ Loop de A..Z para determinar as unidades disponíveis }
for i := 65 to 90 do
begin
C := chr(i)+’:\’; // Formata uma string representando o diretório-raiz.
{ Chama a função GetDriveType( ), que retorna um valor inteiro
representando um dos tipos que aparecem na instrução case
em seguida }
DType := GetDriveType(PChar(C));
{ Baseado no tipo de unidade retornado, formata uma string
para incluir
a caixa de listagem exibindo os diversos tipos de unidade. }
case DType of
0: DriveString := C+’ The drive type cannot be determined.’;
1: DriveString := C+’ The root directory does not exist.’;
DRIVE_REMOVABLE: DriveString :=
C+’ The drive can be removed from the drive.’;
DRIVE_FIXED: DriveString :=
C+’ The disk cannot be removed from the drive.’;
DRIVE_REMOTE: DriveString :=
C+’ The drive is a remote (network) drive.’;
DRIVE_CDROM: DriveString := C+’ The drive is a CD-ROM drive.’;
DRIVE_RAMDISK: DriveString := C+’ The drive is a RAM disk.’;
end;
{ Só inclui tipos de unidade que possam ser determinados. }
if not ((DType = 0) or (DType = 1)) then
lbDrives.Items.AddObject(DriveString, Pointer(i));
end;
end;
A Listagem 12.13 é uma rotina simples que percorre todos os caracteres no alfabeto e os passa para
a função GetDriveType( ) como diretórios-raiz para determinar se são tipos de unidade válidos. Se forem,
GetDriveType( ) retornará o tipo de unidade, que é determinado pela instrução case. Uma string descritiva
é criada e incluída em uma caixa de listagem junto com o número representando a letra da unidade no ar-
ray Objects da caixa de listagem. Somente as unidades que são válidas são incluídas na caixa de listagem. A
propósito, o Delphi 5 vem com um componente TDriveComboBox que permite selecionar uma unidade.
Você encontrará isso na página Win 3.1 da Component Palette.
begin
with lbDrives do
begin
{ Converte o valor ASCII da letra de unidade para um número de
unidade válido:
1 = A, 2 = B, etc. subtraindo 64 do valor ASCII. }
DriveByte := Integer(Items.Objects[ItemIndex])-64;
{ Primeiro cria a string do caminho até o diretório-raiz }
RootPath := chr(Integer(Items.Objects[ItemIndex]))+’:\’;
{ Chama GetDiskFreeSpace para obter as informações de unidade }
if GetDiskFreeSpace(PChar(RootPath), SectorsPerCluster,
BytesPerSector, NumFreeClusters, TotalClusters) then
begin
{ Se essa função tiver sucesso, então atualiza os labels para
exibir as informações do disco. }
lblSectPerCluster.Caption := Format(‘%.0n’, [SectorsPerCluster*1.0]);
lblBytesPerSector.Caption := Format(‘%.0n’, [BytesPerSector*1.0]);
lblNumFreeClust.Caption := Format(‘%.0n’, [NumFreeClusters*1.0]);
lblTotalClusters.Caption := Format(‘%.0n’, [TotalClusters*1.0]);
// Apanha o espaço disponível no disco
FreeSpace := DiskFree(DriveByte);
TotalSpace := DiskSize(DriveByte);
lblFreeSpace.Caption := Format(‘%.0n’, [FreeSpace*1.0]);
{ Calcula o espaço total no disco }
lblTotalDiskSpace.Caption := Format(‘%.0n’, [TotalSpace*1.0]);
end
else begin
{ Define labels para não exibir nada }
lblSectPerCluster.Caption := ‘X’;
lblBytesPerSector.Caption := ‘X’;
lblNumFreeClust.Caption := ‘X’;
lblTotalClusters.Caption := ‘X’;
lblFreeSpace.Caption := ‘X’;
302
Listagem 12.14 Continuação
lblTotalDiskSpace.Caption := ‘X’;
ShowMessage(‘Cannot get disk info’);
end;
end;
end;
A Listagem 12.14 é o manipulador de evento OnClick de uma caixa de listagem. Na verdade, existe
um exemplo de um projeto ilustrando as funções GetDriveType( ) e GetDiskFreeSpace( ) no CD, com o
nome DrvInfo.dpr.
Na Listagem 12.14, quando o usuário dá um clique em um dos itens disponíveis em lbDrives, uma
representação de string do diretório-raiz para essa unidade é criada e passada para a função GetDiskFree-
Space( ). Se a função tiver sucesso ao determinar as informações da unidade, vários labels no formulário
são atualizados para refletir essa informação. Um exemplo do formulário para o projeto de exemplo que
acabamos de mencionar aparece na Figura 12.3.
Observe que você não usa os valores retornados de GetDiskFreeSpace( ) para determinar o tamanho
da unidade ou seu espaço livre. Em vez disso, você usa as funções DiskFree( ) e DiskSize( ) que são defini-
das em SysUtils.pas. O motivo para isso é que GetDiskFreeSpace( ) possui uma falha no Windows 95, e não
informa tamanhos de unidade superiores a 2GB, além de informar tamanhos de setor alterados para uni-
dades maiores do que 1GB. As funções DiskSize( ) e DiskFree( ) usam uma nova API do Win32 para obter
as informações, se estiverem disponíveis no sistema operacional.
FIGURA 12.3 O formulário principal mostrando informações de unidade para as unidades disponíveis.
O primeiro parâmetro é um buffer de string de terminação nula que manterá o local do diretório do
Windows. O segundo parâmetro indica o tamanho do buffer. O fragmento de código a seguir explica
como você usaria essa função:
var
WDir: String;
begin
SetLength(WDir, 144);
if GetWindowsDirectory(PChar(WDir), 144) < > 0 then
begin
SetLength(WDir, StrLen(PChar(WDir)));
303
ShowMessage(WDir);
end
else
RaiseLastWin32Error;
end;
Observe que, como usamos uma variável de string longa, pudemos usar o typecast para convertê-la
para o tipo PChar. A função GetWindowsDirectory( ) retorna um valor inteiro representando a extensão do
caminho do diretório. Caso contrário, ela retorna zero, indicando que houve um erro, quando você terá
que chamar RaiseLastWin32Error para determinar a causa.
NOTA
Você notará no código anterior que incluímos a seguinte linha após a chamada a GetWindowsDirec-
tory( ):
SetLength(WDir, StrLen(PChar(WDir)));
Sempre que você passar uma string longa para uma função primeiro convertendo-a para um PChar, o
Delphi não saberá que a string foi manipulada, e portanto não poderá atualizar suas informações de tama-
nho. Você precisa fazer isso explicitamente usando a técnica indicada, que usa StrLen( ) para procurar a
terminação nula e determinar o tamanho da string. Depois a string é redimensionada por meio de Set-
Length( ).
NOTA
O Delphi oferece as funções CurDir( ) e ChDir( ) na unidade System, além das funções GetCurrentDir( ) e
SetCurrentDir( ) em SysUtils.pas.
O Delphi vem com seu próprio conjunto de rotinas para obter informações de diretório sobre um de-
terminado arquivo. Por exemplo, a propriedade TApplication.ExeName contém o caminho completo e o
nome de arquivo do processo em execução. Considerando que esse parâmetro contém o valor “C:\Delphi\
Bin\Project.exe”, a Tabela 12.7 mostra os valores retornados pelas várias funções do Delphi ao passar a
propriedade TApplication.ExeName.
ExtractFileDir( ) C:\Delphi\Bin
ExtractFileDrive( ) C:
ExtractFileExt( ) .exe
ExtractFileName( ) Project1.exe
ExtractFilePath( ) C:\Delphi\Bin\
NOTA
Você pode usar a função SearchPath( ) da API do Win32 para procurar em um diretório especificado,
nos diretórios do sistema, nos diretórios da variável de ambiente PATH ou em uma lista de diretórios se-
parados com ponto-e-vírgulas. Entretanto, essa função não procura em subdiretórios de um determi-
nado diretório.
305
Listagem 12.15 Exemplo de pesquisa entre os diretórios para realizar uma busca de arquivo
unit MainFrm;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls, FileCtrl, Grids, Outline, DirOutln;
type
TMainForm = class(TForm)
dcbDrives: TDriveComboBox;
edtFileMask: TEdit;
lblFileMask: TLabel;
btnSearchForFiles: TButton;
lbFiles: TListBox;
dolDirectories: TDirectoryOutline;
procedure btnSearchForFilesClick(Sender: TObject);
procedure dcbDrivesChange(Sender: TObject);
private
FFileName: String;
function GetDirectoryName(Dir: String): String;
procedure FindFiles(APath: String);
end;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
begin
APath := GetDirectoryName(APath); // Obter um nome válido de diretório
{ Localiza a primeira ocorrência do nome de arquivo especificado }
FindResult := FindFirst(APath+FFileName,faAnyFile+faHidden+
faSysFile+faReadOnly,FSearchRec);
try
{ Continua a procurar os arquivos de acordo com a máscara indicada.
inclui os arquivos e seus caminhos na caixa de listagem.}
while FindResult = 0 do
begin
lbFiles.Items.Add(LowerCase(APath+FSearchRec.Name));
FindResult := FindNext(FSearchRec);
end;
while FindResult = 0 do
begin
if ((DSearchRec.Attr and faDirectory) = faDirectory) and not
IsDirNotation(DSearchRec.Name) then
FindFiles(APath+DSearchRec.Name); // A recursão está aqui!
FindResult := FindNext(DSearchRec);
end;
finally
FindClose(FSearchRec);
end;
end;
end.
307
No método FindFiles( ), a primeira construção while..do procura arquivos no diretório ativo especi-
ficado pelo parâmetro APath e depois acrescenta os arquivos e seus caminhos em lbFiles. A segunda cons-
trução while..do localiza os subdiretórios do diretório ativo e os anexa à variável APath. O método FindFi-
les( ) passa então o parâmetro APath, agora com um nome de subdiretório, para si mesmo, resultando em
uma chamada recursiva. Esse processo continua até que todos os subdiretórios tenham sido pesquisados.
A Figura 12.4 mostra os resultados de uma busca por todos os arquivos PAS no diretório Code do
Delphi 5.
Duas estruturas do Object Pascal e duas funções merecem ser mencionadas aqui. Primeiro, vamos
falar um pouco sobre a estrutura TSearchRec e as funções FindFirst( ) e FindNext( ). Depois, discutiremos
sobre a estrutura TWin32FindData.
Discutiremos sobre SHFileOperation( ) com mais detalhes em outro ponto deste capítulo.
O registro TSearchRec
O registro TSearchRec define dados retornados pelas funções FindFirst( ) e FindNext( ). O Object Pascal
define esse registro da seguinte forma:
TSearchRec = record
Time: Integer;
Size: Integer;
Attr: Integer;
Name: TFileName;
ExcludeAttr: Integer;
FindHandle: Thandle;
FindData: TWin32FindData;
end;
Os campos de TSearchRec são modificados pelas funções acima mencionadas quando o arquivo é lo-
calizado.
O campo Time contém a hora de criação ou modificação do arquivo. O campo Size contém o tama-
nho do arquivo em bytes. O campo Name contém o nome do arquivo. O campo Attr contém um ou mais
dos atributos de arquivo apresentados na Tabela 12.8.
309
Os campos FindHandle e ExcludeAttr são usados internamente por FindFirst( ) e FindNext( ). Você não
precisa se preocupar com esses campos.
Tanto FindFirst( ) quanto FindNext( ) utilizam um caminho como parâmetro, o qual poderá conter
curingas – por exemplo, C:\DELPHI 5\BIN\*.EXE significa todos os arquivos com a extensão .EXE no diretório
C:\DELPHI 5\BIN\. O parâmetro Attr especifica os atributos de arquivo para a pesquisa. Suponha que você
queira procurar apenas os arquivos do sistema; nesse caso, chamaria FindFirst( ) e/ou FindNext( ) como
neste código:
FindFirst(Path, faSysFile, SearchRec);
O registro TWin32FindData
O registro TWin32FindData contém informações sobre o arquivo ou subdiretório localizado. Esse registro é
definido da seguinte forma:
TWin32FindData = record
dwFileAttributes: DWORD;
ftCreationTime: TFileTime;
ftLastAccessTime: TFileTime;
ftLastWriteTime: TFileTime;
nFileSizeHigh: DWORD;
nFileSizeLow: DWORD;
dwReserved0: DWORD;
dwReserved1: DWORD;
cFileName: array[0..MAX_PATH - 1] of AnsiChar;
cAlternateFileName: array[0..13] of AnsiChar;
end;
Campo Significado
unit VerInfo;
interface
type
{ define a generic exception class for version info, and an exception
to indicate that no version info is available. }
EVerInfoError = class(Exception);
ENoVerInfoError = class(Exception);
eNoFixeVerInfo = class(Exception);
const
‘FileDescription’,
‘FileVersion’,
‘InternalName’,
‘LegalCopyright’,
‘LegalTrademarks’,
‘OriginalFilename’,
‘ProductName’,
‘ProductVersion’,
‘Comments’);
type
implementation
uses Windows;
const
// strings que devem ser incluídas na função VerQueryValue( )
SFInfo = ‘\StringFileInfo\’;
VerTranslation: PChar = ‘\VarFileInfo\Translation’;
FormatStr = ‘%s%.4x%.4x\%s%s’;
FFileName := aFileName;
FFileFlags := TStringList.Create;
// Apanha a informação de versão do arquivo
FillFileVersionInfo;
// Apanha a informação de arquivo fixo
FillFixedFileInfoBuf;
// Apanha os valores de máscara de arquivo
FillFileMaskInfo;
end;
destructor TVerInfoRes.Destroy;
begin
FFileFlags.Free;
end;
procedure TVerInfoRes.FillFileVersionInfo;
var
SBSize: UInt;
begin
// Determina o tamanho da informação de versão
Size := GetFileVersionInfoSize(PChar(FFileName), Handle);
if Size <= 0 then { raise exception if size <= 0 }
raise ENoVerInfoError.Create(‘No Version Info Available.’);
procedure TVerInfoRes.FillFixedFileInfoBuf;
var
Size: Longint;
begin
if VerQueryValue(PChar(RezBuffer), ‘\’, pointer(FixedFileInfoBuf),
åSize) then begin
if Size < SizeOf(TVSFixedFileInfo) then
raise eNoFixeVerInfo.Create(‘No fixed file info’);
end
else
raise eNoFixeVerInfo.Create(‘No fixed file info’)
end;
procedure TVerInfoRes.FillFileMaskInfo;
begin
313
Listagem 12.16 Continuação
314
Listagem 12.16 Continuação
VOS__WINDOWS16:
Result := ‘Designed for 16-bit Windows’;
VOS__PM16:
Result := ‘Designed for 16-bit PM’;
VOS__PM32:
Result := ‘Designed for 32-bit PM’;
VOS__WINDOWS32:
Result := ‘Designed for 32-bit Windows’;
VOS_DOS_WINDOWS16:
Result := ‘Designed for 16-bit Windows, running on MS-DOS’;
VOS_DOS_WINDOWS32:
Result := ‘Designed for Win32 API, running on MS-DOS’;
VOS_OS216_PM16:
Result := ‘Designed for 16-bit PM, running on 16-bit OS/2’;
VOS_OS232_PM32:
Result := ‘Designed for 32-bit PM, running on 32-bit OS/2’;
VOS_NT_WINDOWS32:
Result := ‘Designed for Win32 API, running on Windows/NT’;
else
Result := ‘Unknown’;
end;
end;
end.
TVerInfoRes contém os campos necessários e encapsula as rotinas apropriadas da API do Win32 para
obter informações de versão de qualquer arquivo. O arquivo do qual as informações devem ser obtidas é
especificado pela passagem do nome do arquivo como AFileName ao construtor TVerInfoRes.Create( ). Esse
nome de arquivo é atribuído ao campo FFileName, que é usado em outra rotina para realmente extrair as
informações de versão. O construtor chama então três métodos, FillFileVersionInfo( ), FillFixedFileInfo-
Buf( ) e FillFileMaskInfo( ). 315
O método FillFileVersionInfo( )
O método FillFileVersionInfo( ) realiza o trabalho inicial de carregar as informações de versão antes que
você possa começar a examinar seus detalhes. O método primeiro determina se o arquivo possui infor-
mações de versão e, se houver, seu tamanho. O tamanho é necessário para determinar quanta memória
deve ser alocada para conter essa informação, quando for recebida. A função GetFileVersionInfoSize( ) da
API do Win32 determina o tamanho das informações de versão contidas em um arquivo. Essa função é
declarada da seguinte forma:
function GetFileVersionInfoSize(lptstrFilename: Pchar;
var lpdwHandle: DWORD): DWORD; stdcall;
O parâmetro lptstrFileName refere-se ao arquivo do qual as informações de versão devem ser obti-
das. O parâmetro lpdwHandle é uma variável DWORD definida em zero quando a função é chamada. Pelo que
pudemos notar, essa variável não tem qualquer outra finalidade.
FillFileVersionInfo( ) passa FFileName a GetFileVersionInfoSize( ); se o valor de retorno, armazenado
na variável Size, for maior do que zero, um buffer (RezBuffer) será alocado para armazenar Size bytes.
Depois que a memória para RezBuffer tiver sido alocada, ela será passada à função GetFileVersion-
Info( ), que realmente preenche RezBuffer com as informações de versão. GetFileVersionInfo( ) é declara-
do da seguinte forma:
function GetFileVersionInfo(lptstrFilename: PChar; dwHandle,
dwLen: DWORD; lpData: Pointer): BOOL; stdcall;
O parâmetro lptstrFileName apanha o nome do arquivo, FFileName. DwHandle é ignorado. DwLen é o va-
lor de retorno de GetFileVersionInfoSize( ), que foi armazenado na variável Size. LpData é um ponteiro
para o buffer que contém as informações de versão. Se GetFileVersionInfo( ) não tiver sucesso para recu-
perar a informação de versão, ela retorna False; caso contrário, é retornado um valor True.
Finalmente, o método FillFileVersionInfo( ) chama a função VerQueryValue( ) da API, que é usada
para retornar informações de versão selecionadas a partir do recurso de informações de versão. Nesse
caso, VerQueryValue( ) é chamada para apanhar um ponteiro para o array identificador de idioma (lingua-
gem) e conjunto de caracteres. Esse array é usado em chamadas subseqüentes a VerQueryValue( ) para aces-
sar informações de versão na StringTable específica do idioma no recurso de informações de versão.
VerQueryValue( ) é declarada da seguinte forma:
O parâmetro pBlock refere-se ao parâmetro lpData, que foi passado para GetFileVersionInfo( ). LpSubB-
lock é uma string de terminação nula que especifica qual valor de informação de versão deve ser apanha-
do. Você pode dar uma olhada na ajuda on-line e procurar VerQueryValue( ), que descreve as várias strings
que podem ser passadas a VerQueryValue( ). No caso do exemplo anterior, a string “\VarFileInfo\Translati-
on” é passada como parâmetro lpSubBlock para recuperar as informações de tradução de idioma e conjun-
to de caracteres. O parâmetro lplpBuffer aponta para o buffer que contém o valor das informações de
versão. O parâmetro puLen contém o tamanho dos dados apanhados.
O método FillFixedFileInfoBuf( )
O método FillFixedFileInfoBuf( ) ilustra como usar VerQueryValue( ) para obter um ponteiro para a estru-
tura VS_FIXEDFILEINFO, que contém a informação de versão sobre o arquivo. Isso é feito passando-se a
string “\” como parâmetro lpSubBlock para a função VerQueryValue( ). O ponteiro é armazenado no campo
TVerInfoRes.FixedFileInfoBuf.
O método FillFileMaskInfo( )
O método FillFileMaskInfo( ) ilustra como obter atributos de módulo. Isso é tratado pela realização da
316 operação de máscara de bit apropriada sobre os campos dwFileFlagsMask e dwFileFlags de FixedFileInfoBuf,
além do flag específico que está sendo avaliado. Não entraremos nos detalhes do significado desses flags.
Se estiver interessado, a ajuda on-line para a página Version Info (informação de versão) da caixa de diá-
logo Project Options (opções de projeto) explica isso com detalhes.
unit MainFrm;
interface
317
Listagem 12.17 Continuação
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs, FileCtrl, StdCtrls, verinfo, Grids, Outline, DirOutln,
ComCtrls;
type
TMainForm = class(TForm)
lvVersionInfo: TListView;
btnClose: TButton;
procedure FormDestroy(Sender: TObject);
procedure FormShow(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure btnCloseClick(Sender: TObject);
private
VerInfoRes: TVerInfoRes;
end;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
begin
318
Listagem 12.17 Continuação
end;
end.
Copiando um diretório
A Listagem 12.18 é um procedimento que escrevemos para copiar uma árvore de diretórios de um local
para outro.
GetMem(FromDir, Length(AFromDirectory)+2);
try
GetMem(ToDir, Length(AToDirectory)+2);
try
StrCopy(FromDir, PChar(AFromDirectory));
StrCopy(ToDir, PChar(AToDirectory));
with SHFileOpStruct do
begin
Wnd := AHandle; // Atribui a alça da janela
wFunc := FO_COPY; // Especifica uma cópia de arquivo
pFrom := FromDir;
pTo := ToDir;
fFlags := FOF_NOCONFIRMATION or FOF_RENAMEONCOLLISION;
fAnyOperationsAborted := False;
hNameMappings := nil;
lpszProgressTitle := nil;
if SHFileOperation(SHFileOpStruct) < > 0 then
RaiseLastWin32Error;
end;
finally
FreeMem(ToDir, Length(AToDirectory)+2);
end;
finally
FreeMem(FromDir, Length(AFromDirectory)+2);
end;
end;
320
O procedimento CopyDirectoryTree( ) utiliza três parâmetros. O primeiro, AHandle, é a alça de um
proprietário de caixa de diálogo que mostraria informações de status sobre a operação do arquivo. Os
dois parâmetros restantes são os locais de diretório de origem e destino. Como a API do Windows traba-
lha com PChars, simplesmente copiamos esses dois locais para variáveis PChar depois de alocarmos memó-
ria para os PChars. Depois, atribuímos esses valores aos membros pFrom e pTo da estrutura SHFileOpStruct.
Observe a atribuição ao membro wFunc como FO_COPY. Isso é o que instrui SHFileOperation quanto ao tipo de
operação a ser realizada. Os outros membros são explicados na ajuda on-line. Na chamada a SHFileOpera-
tion( ), o diretório de origem seria movido para o destino especificado pelo parâmetro AToDirectory.
with SHFileOpStruct do
begin
Wnd := AHandle;
wFunc := FO_DELETE;
pFrom := DirName;
pTo := nil;
fFlags := FOF_ALLOWUNDO;
fAnyOperationsAborted := False;
hNameMappings := nil;
lpszProgressTitle := nil;
end;
Você notará que não há muita diferença entre este procedimento e o anterior, exceto que o número
wFunc recebe FO_DELETE e o membro pTo é definido para nil. O membro pTo é ignorado pela função SHFile-
Operation( ) em uma operação de exclusão. Além disso, como o flag FOF_ALLOWUNDO é incluído no membro
fFlags, a função moverá o arquivo para a Lixeira, permitindo que a operação seja desfeita.
321
Alguns exemplos dessas operações estão incluídos no CD que acompanha este livro, no projeto
SHFileOp.dpr.
Resumo
Este capítulo ofereceu informações substanciais sobre o trabalho com arquivos, diretórios e unidades de
disco. Você aprendeu a manipular diferentes tipos de arquivo. O capítulo ilustrou a técnica de descen-
dência da classe TFileStream do Delphi para encapsular o I/O de registro e arquivo. Ele também mostrou
como usar os arquivos mapeados na memória do Win32. Você criou uma classe TMemMapFile para encapsu-
lar a funcionalidade do mapeamento de memória. Mais adiante, o arquivo demonstrou como apanhar
informações de versão de um arquivo que possua tais informações. Por fim, você viu como é fácil realizar
operações de cópia, mudança, renomeação ou exclusão em arquivos e diretórios, incluindo a passagem
de arquivos para a Lixeira do Windows.
322
Técnicas mais CAPÍTULO
complexas
13
NE STE C AP ÍT UL O
l Tratamento avançado de mensagens da
aplicação 324
l Evitando múltiplas instâncias da aplicação 330
l Uso do BASM com o Delphi 334
l Uso de ganchos do Windows 338
l Uso de arquivos OBJ do C/C++ 352
l Uso de classes do C++ 360
l Thunking 364
l Obtenção de informações do pacote 380
l Resumo 384
Existe um momento em que você precisa sair do caminho batido para realizar um objetivo em particular.
Este capítulo ensina algumas técnicas avançadas que você pode usar nas aplicações em Delphi. Você che-
gará muito mais perto da API do Win32 neste capítulo do que na maioria dos outros capítulos, e explora-
rá algumas coisas que não são óbvias ou não são fornecidas sob a Visual Component Library (VCL). Você
aprenderá a respeito de conceitos como procedimentos de janela, múltiplas instâncias de programa, gan-
chos do Windows e compartilhamento entre o código do Delphi e do C++.
Subclassificação
Para saber quando uma mensagem foi enviada para a sua aplicação, você precisa substituir o procedi-
mento da janela Application pelo seu próprio procedimento. No seu procedimento de janela, você precisa
fazer qualquer processamento ou tratamento de mensagem que seja necessário antes de passar a mensa-
gem para o procedimento da janela original. Esse processo é conhecido como subclassificar uma janela.
Você pode usar a função SetWindowLong( ) da API do Win32 com a constante GWL_WNDPROC para definir
uma nova função de procedimento de janela para uma janela. A própria função de procedimento de jane-
la pode ter um ou dois formatos: ela pode seguir a definição da API de um procedimento de janela ou
pode tirar proveito de algumas funções auxiliadoras do Delphi e tornar seu próprio procedimento de ja-
nela um método especial referenciado como um método de janela.
ATENÇÃO
Um problema que pode surgir quando você subclassifica um procedimento de janela de uma janela da
VCL é que a alça da janela pode ser recriada abaixo de você, causando assim a falha da aplicação. Previ-
na-se usando essa técnica se houver uma chance de que a alça da janela que você está subclassificando
seja recriada. Uma técnica mais segura é usar Application.HookMainWindow( ), que aparece mais adiante
neste capítulo.
324
O parâmetro Handle identifica a janela de destino, o parâmetro Msg é a mensagem da janela e os parâ-
metros wParam e lParam contêm informações adicionais específicas da mensagem. Essa função retorna um
valor que depende da mensagem recebida. Observe cuidadosamente que essa função precisa usar a con-
venção de chamada stdcall.
Você pode usar a função SetWindowLong( ) para definir o procedimento da janela Application, confor-
me vemos a seguir:
var
WProc: Pointer;
begin
WProc := Pointer(SetWindowLong(Application.Handle, GWL_WNDPROC,
Integer(@NewWndProc)));
Depois dessa chamada, WProc terá um ponteiro para o procedimento de janela antigo. É preciso sal-
var esse valor, pois você precisa passar quaisquer mensagens que não sejam tratadas por você mesmo
para o procedimento de janela antigo, usando a função da API CallWindowProc( ). O código a seguir dá
uma idéia da implementação do procedimento de janela:
function NewWndProc(Handle: hWnd; Msg, wParam, lParam: Longint):
Longint; stdcall;
begin
{ Verifica valor de Msg e realiza qualquer tipo de ação que você }
{ quiser, dependendo do valor da mensagem. Para mensagens que você }
{ não trata explicitamente, passe adiante a informaçao da mensagem }
{ para o procedimento de janela original, como vemos a seguir: }
Result := CallWindowProc(WProc, Application.Handle, Msg, wParam,
lParam);
end;
A Listagem 13.1 mostra a unidade ScWndPrc.pas, que subclassifica o procedimento de janela de Appli-
cation para tratar de uma mensagem definida pelo usuário, chamada DDGM_FOOMSG.
unit ScWndPrc;
interface
const
DDGM_FOOMSG = WM_USER;
implementation
var
WProc: Pointer;
initialization
{ Define procedimento de janela da janela Application. }
WProc := Pointer(SetWindowLong(Application.Handle, gwl_WndProc,
Integer(@NewWndProc)));
end.
ATENÇÃO
Certifique-se de salvar o procedimento de janela antigo retornado por GetWindowLong( ). Se você não cha-
mar o procedimento de janela antigo dentro do seu procedimento de janela subclassificado para mensa-
gens que você não deseja tratar, provavelmente causará o término da sua aplicação, e poderá ainda tran-
car o sistema operacional.
A Listagem 13.2 mostra o código-fonte para Main.pas, a unidade principal para o projeto WinProc.
unit Main;
interface
326
Listagem 13.2 Continuação
uses
SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls;
type
TMainForm = class(TForm)
SendBtn: TButton;
PostBtn: TButton;
procedure SendBtnClick(Sender: TObject);
procedure PostBtnClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
OldWndProc: Pointer;
WndProcPtr: Pointer;
procedure WndMethod(var Msg: TMessage);
procedure HandleAppMessage(var Msg: TMsg; var Handled: Boolean);
end;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
uses ScWndPrc;
end.
Quando SendBtn é acionado, a função da API SendMessage( ) é usada para enviar a mensagem
DDGM_FOOMSG para a alça da janela de Application. Quando PostBtn é acionado, a mesma mensagem é postada
para Application usando a função da API PostMessage( ).
A HandleAppMessage( ) é atribuída para tratar do evento Application.OnMessage. Esse procedimento sim-
plesmente usa ShowMessage( ) para chamar uma caixa de diálogo indicando que ele vê uma mensagem. O
evento OnMessage é atribuído no manipulador do evento OnCreate para o formulário principal.
Observe que o manipulador OnDestroy para o formulário principal retorna o procedimento de janela
de Application para o valor original (OldWndProc) antes de chamar FreeObjectInstance( ) para liberar o pro-
cedimento criado com MakeProcInstance( ). Se o procedimento de janela antigo não for outra vez instan-
ciado em primeiro lugar, o efeito seria o de “desconectar” o procedimento de janela de uma janela ativa,
efetivamente removendo a capacidade da janela de tratar das mensagens. Isso não é nada bom, pois po-
tencialmente poderia destruir a execução da aplicação ou do sistema operacional.
Só por segurança, a unidade ScWndPrc, mostrada anteriormente neste capítulo, está incluída em Main.
Isso significa que a janela Application será subclassificada duas vezes: uma por ScWndPrc usando a técnica da
API e outra por Main usando a técnica do método de janela. Não existe absolutamente perigo algum em
fazer isso, desde que você se lembre de usar CallWindowProc( ) no procedimento e método da janela para
passar as mensagens para os procedimentos de janela antigos.
Quando você executar essa aplicação, poderá ver que a caixa de diálogo ShowMessage( ) aparece pelo
procedimento e método da janela, independente do botão que é pressionado. Além do mais, você verá
que Application.OnMessage vê apenas as mensagens postadas para a janela.
HookMainWindow( )
Outra técnica para interceptar mensagens visadas para a janela Application, talvez mais típica da VCL, é o
328 método HookMainWindow( ) de TApplication. Esse método permite inserir seu próprio manipulador de
mensagem no início do método WndProc( ) de TApplication para realizar um processamento de mensa-
gem especial ou impedir que TApplication processe certas mensagens. HookMainWindow( ) é definido da se-
guinte forma:
procedure HookMainWindow(Hook: TWindowHook);
O parâmetro para esse método é do tipo TWindowHook, que é definido da seguinte forma:
type
TWindowHook = function (var Message: TMessage): Boolean of object;
Não é preciso fazer muita coisa para usar esse método; basta chamar HookMainWindow( ), passando seu
próprio método no parâmetro Hook. Isso acrescenta seu método em uma lista de métodos de gancho de ja-
nela que serão chamados antes do processamento normal da mensagem, que ocorre em TApplicati-
on.WndProc( ). Se um método de gancho de janela retornar True, a mensagem será considerada como trata-
da, e o método WndProc( ) terminará imediatamente.
Quando você terminar de processar as mensagens, chame o método UnhookMainWindow( ) para remo-
ver seu método da lista de métodos de gancho de janela. Esse método é igualmente definido da seguinte
forma:
procedure UnhookMainWindow(Hook: TWindowHook);
Empregando essa técnica, a Listagem 13.3 mostra o formulário principal para um projeto da VCL
simples de um formulário, e a Figura 13.2 mostra essa aplicação em ação.
unit HookMain;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, ExtCtrls;
type
THookForm = class(TForm)
SendBtn: TButton;
GroupBox1: TGroupBox;
LogList: TListBox;
DoLog: TCheckBox;
ExitBtn: TButton;
procedure SendBtnClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject); 329
Listagem 13.3 Continuação
var
HookForm: THookForm;
implementation
{$R *.DFM}
end.
unit MultInst;
interface
const
MI_QUERYWINDOWHANDLE = 1;
MI_RESPONDWINDOWHANDLE = 2;
MI_ERROR_NONE = 0;
MI_ERROR_FAILSUBCLASS = 1;
331
Listagem 13.4 Continuação
MI_ERROR_CREATINGMUTEX = 2;
implementation
const
UniqueAppStr = ‘DDG.I_am_the_Eggman!’;
var
MessageId: Integer;
WProc: TFNWndProc;
MutHandle: THandle;
MIError: Integer;
end
// Caso contrário, passa mensagem para o procedimento da janela antiga
else
Result := CallWindowProc(WProc, Handle, Msg, wParam, lParam);
end;
procedure SubClassApplication;
begin
// Subclassificamos o procedimento da janela Application para que
// Application.OnMessage permaneça disponível para o usuário.
WProc := TFNWndProc(SetWindowLong(Application.Handle, GWL_WNDPROC,
Longint(@NewWndProc)));
// Define flag de erro apropriado se tiver ocorrido condição de erro
if WProc = nil then
MIError := MIError or MI_ERROR_FAILSUBCLASS;
end;
procedure DoFirstInstance;
// Isso é chamado apenas para a primeira instância da aplicação
begin
// Cria o mutex com o string exclusivo (esperamos assim)
MutHandle := CreateMutex(nil, False, UniqueAppStr);
if MutHandle = 0 then
MIError := MIError or MI_ERROR_CREATINGMUTEX;
end;
procedure BroadcastFocusMessage;
// Isso é chamado quando já existe uma instância em execução.
var
BSMRecipients: DWORD;
begin
// Impede que o formulário principal pisque
Application.ShowMainForm := False;
// Posta mensagem e tenta estabelecer diálogo com instância anterior
BSMRecipients := BSM_APPLICATIONS;
BroadCastSystemMessage(BSF_IGNORECURRENTTASK or BSF_POSTMESSAGE,
@BSMRecipients, MessageID, MI_QUERYWINDOWHANDLE,
Application.Handle);
end;
procedure InitInstance;
begin
SubClassApplication; // hook application message loop
MutHandle := OpenMutex(MUTEX_ALL_ACCESS, False, UniqueAppStr);
if MutHandle = 0 then
// Objeto mutex ainda não foi criado, o que significa que nenhuma
// instância anterior foi criada.
DoFirstInstance
else
BroadcastFocusMessage;
end;
initialization
333
Listagem 13.4 Continuação
MessageID := RegisterWindowMessage(UniqueAppStr);
InitInstance;
finalization
// Restaura procedimento da janela de aplicação antiga
if WProc < > Nil then
SetWindowLong(Application.Handle, GWL_WNDPROC, LongInt(WProc));
if MutHandle < > 0 then CloseHandle(MutHandle); // Free mutex
end.
unit OIMain;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TMainForm = class(TForm)
Label1: TLabel;
CloseBtn: TButton;
procedure CloseBtnClick(Sender: TObject);
private
{ Declarações privadas }
public
{ Declarações públicas }
end;
var
MainForm: TMainForm;
implementation
uses MultInst;
{$R *.DFM}
end.
NOTA
Esta seção não ensina programação em assembler, mas mostra a facilidade do Delphi em usar o assembler
se você já estiver familiarizado com a linguagem.
Além do mais, se você já programou em BASM com o Delphi 1, lembre-se de que, no Delphi de 32 bits, o
BASM é algo totalmente novo. Como agora você precisa escrever linguagem Assembly de 32 bits, quase
todo o seu código do BASM para 16 bits terá que ser reescrito para a nova plataforma. O fato de que o có-
digo do BASM pode exigir tanto cuidado para se manter é outro motivo para reduzir o seu uso do BASM nas
aplicações.
A capacidade de acessar parâmetros por nome é importante, pois você não precisa referenciar variá-
veis passadas a um procedimento através de um registrador de ponteiro de base da pilha (ebp), como faria
em um programa normal em Assembly. Em um procedimento normal da linguagem Assembly, você teria
que referenciar a variável I como [ebp+4] (seu deslocamento a partir do ponteiro de base da pilha).
NOTA
Quando você usar o BASM para referenciar parâmetros passados para um procedimento, lembre-se de
que você pode acessar esses parâmetros por nome, e não precisa acessá-los por seu deslocamento a partir
do registrador ebp. O acesso pelo deslocamento a partir de ebp torna o seu código mais difícil de se manter.
Parâmetros var
Lembre-se de que, quando um parâmetro é declarado como var na lista de parâmetros de uma função ou
procedimento, é passado um ponteiro para essa variável, e não o seu valor. Isso significa que, quando
você referenciar parâmetros var dentro de um bloco BASM, precisa levar em consideração que o parâme-
tro é um ponteiro de 32 bits para uma variável, e não uma instância da variável. Para expandir o trecho
de exemplo anterior, o exemplo a seguir mostra como você incrementaria a variável I se ela fosse passada
como um parâmetro var:
procedure Foo(var I: integer);
begin
{ algum código }
asm
mov eax, I
inc dword ptr [eax]
end;
{ I foi incrementado em 1 }
{ mais algum código }
end;
336
Convenção de chamada de registrador
Lembre-se de que a convenção de chamada default para as funções e procedimentos do Object Pascal é
register. Tirar proveito desse método de passagem de parâmetros poderá ajudá-lo a otimizar seu código.
A convenção de chamada de registrador especifica que os três primeiro parâmetros de 32 bits são passa-
dos nos registradores eax, edx e ecx. Isso significa que, para a declaração de função
function BlahBlah(I1, I2, I3: Integer): Integer;
você pode contar com o fato de que o valor de I1 está armazenado em eax, I2 em edx e I3 em ecx. Conside-
re o método a seguir como outro exemplo:
procedure TSomeObject.SomeProc(S1, S2: PChar);
Aqui, o valor de S1 será passado em ecx, S2 em edx e o parâmetro Self implícito será passado em eax.
NOTA
Se você estiver estudando um código em 16 bits, deverá saber que não é mais necessário usar a diretiva as-
sembler dos tempos do Delphi 1. Essa diretiva é simplesmente ignorada pelo compilador Delphi de 32 bits.
O procedimento anterior aceita uma variável inteira I e a incrementa. Como o valor da variável é
colocado no registrador eax, esse é o valor retornado pela função. A Tabela 13.1 mostra como diferentes
tipos de dados são retornados de uma função no Delphi.
NOTA
Um tipo ShortString é retornado como um ponteiro para uma instância temporária de uma string na pilha.
337
Registros
O BASM oferece um atalho elegante para acessar os campos de um registro. Você pode acessar os cam-
pos de qualquer registro em um bloco BASM usando a sintaxe Registro.Tipo.Campo. Por exemplo, conside-
re um registro definido da seguinte forma:
type
TDumbRec = record
i: integer;
c: char;
end;
Além disso, considere uma função que aceite um TDumbRec como parâmetro de referência, como
mostramos aqui:
procedure ManipulateRec(var DR: TDumbRec);
asm
mov [eax].TDumbRec.i, 24
mov [eax].TDumbRec.c, ‘s’
end;
Observe a sintaxe do atalho para acessar os campos de um registro. A alternativa seria calcular ma-
nualmente o deslocamento correto dentro do registro para se obter ou definir o valor apropriado. Use
essa técnica sempre que você utilizar registros no BASM para tornar o seu BASM mais flexível com rela-
ção a mudanças em potencial nos tipos de dados.
Definindo o gancho
Um gancho do Windows é definido usando-se a função da API SetWindowsHookEx( ):
function SetWindowsHookEx(idHook: Integer; lpfn: TFNHookProc; hmod: HINST;
dwThreadID: DWORD): HHOOK; stdcall;
ATENÇÃO
Use apenas a função SetWindowsHookEx( ) – não a função SetWindowsHook( ) – nas suas aplicações. Set-
WindowsHook( ), que existia no Windows 3.x, não está implementada na API do Win32.
O parâmetro idHook descreve o tipo de gancho a ser instalado. Esse pode ser qualquer uma das cons-
tantes de gancho predefinidas, que aparecem na Tabela 13.2.
O parâmetro lpfn é o endereço da função de callback para atuar como função de gancho do Win-
dows. Essa função é do tipo TFNHookProc, que é definida da seguinte maneira:
TFNHookProc = function (code: Integer; wparam: WPARAM; lparam: LPARAM):
LRESULT stdcall;
O conteúdo de cada um dos parâmetros da função de gancho varia de acordo com o tipo de gancho
instalado; os parâmetros são documentados na ajuda da API do Win32.
O parâmetro hMod deverá ser o valor de hInstance no EXE ou DLL contendo o callback do gancho.
O parâmetro dwThreadID identifica o thread com o qual o gancho deve ser associado. Se esse parâme-
tro for zero, o gancho será associado a todos os threads.
O valor de retorno é a alça do gancho que você precisa salvar em uma variável global para uso pos-
terior.
O Windows pode ter vários ganchos instalados de uma só vez, e pode ainda ter o mesmo tipo de
gancho instalado várias vezes.
Observe também que alguns ganchos operam com a restrição de que precisam ser implementados a
partir de uma DLL. Verifique a documentação da API do Win32 para ver os detalhes sobre cada gancho
específico. 339
ATENÇÃO
Uma séria limitação para os ganchos do sistema é que novas instâncias da DLL do gancho são carregadas
separadamente no espaço de endereços de cada processo. Por causa disso, a DLL do gancho não pode se
comunicar diretamente com a aplicação host que definiu o gancho. Você precisa percorrer as mensagens
ou as áreas de memória compartilhada (como os arquivos mapeados na memória, descritos no Capítulo
12) para se comunicar com a aplicação host.
ATENÇÃO
Ao chamar o próximo gancho na cadeia, não chame DefHookProc( ). Essa é outra função do Windows 3.x
não-implementada.
O tipo de retorno de TSendKeyError é um tipo enumerado que indica a condição de erro. Ele pode ser
340 qualquer um dos valores que aparecem na Tabela 13.3.
Tabela 13.3 Códigos de erro de SendKey
Valor Significado
S pode incluir qualquer caracter alfanumérico ou @ para a tecla Alt, ^ para a tecla Ctrl ou ~ para a te-
cla Shift. SendKeys( ) também permite especificar teclas especiais do teclado entre chaves, conforme re-
presentado na unidade KeyDefs.pas da Listagem 13.6.
unit KeyDefs;
interface
uses Windows;
const
MaxKeys = 24;
ControlKey = ‘^’;
AltKey = ‘@’;
ShiftKey = ‘~’;
KeyGroupOpen = ‘{‘;
KeyGroupClose = ‘}’;
type
TKeyString = String[7];
TKeyDef = record
Key: TKeyString;
vkCode: Byte;
end;
const
KeyDefArray : array[1..MaxKeys] of TKeyDef = (
(Key: ‘F1’; vkCode: vk_F1),
(Key: ‘F2’; vkCode: vk_F2),
(Key: ‘F3’; vkCode: vk_F3),
(Key: ‘F4’; vkCode: vk_F4),
(Key: ‘F5’; vkCode: vk_F5),
(Key: ‘F6’; vkCode: vk_F6),
(Key: ‘F7’; vkCode: vk_F7),
(Key: ‘F8’; vkCode: vk_F8),
(Key: ‘F9’; vkCode: vk_F9),
(Key: ‘F10’; vkCode: vk_F10),
(Key: ‘F11’; vkCode: vk_F11),
(Key: ‘F12’; vkCode: vk_F12),
341
Listagem 13.6 Continuação
implementation
uses SysUtils;
end.
Depois de receber a string, SendKeys( ) desmembra e analisa os toques de tecla individuais a partir da
string e inclui cada um deles em uma lista na forma de registros de mensagem, contendo mensagens
wm_KeyUp e wm_KeyDown. Essas mensagens são então reproduzidas no Windows através de um gancho
wh_JournalPlayback.
Campo Valor
Como a tabela na unidade KeyDefs mapeia apenas o código de tecla virtual, você precisa encontrar
um meio de determinar o código de varredura da tecla dado o código de tecla virtual. Felizmente, a API
do Windows oferece uma função chamada MapVirtualKey( ), que faz exatamente isso. O código a seguir
mostra o fonte para o procedimento MakeMessage( ):
procedure MakeMessage(vKey: byte; M: Cardinal);
{ O procedimento monta um registro TEventMsg que simula um toque de }
{ tecla e o inclui na lista de mensagens. }
var
E: PEventMsg;
begin
New(E); // aloca um registro de mensagem
with E^ do begin
message := M; // define campo de mensagem
paramL := vKey; // código da vk em ParamL
paramH := MapVirtualKey(vKey, 0); // código de varredura em ParamH
time := GetTickCount; // define hora
hwnd := 0; // ignorado
end;
MessageList.Add(E);
end;
Depois que a lista de mensagens inteira estiver criada, o gancho poderá ser definido para reproduzir
a seqüência de teclas. Você faz isso por meio de um procedimento chamado StartPlayback( ). StartPlay-
back prepara a bomba colocando a primeira mensagem da lista em um buffer global. Ele também iniciali-
za um buffer global que registra quantas mensagens foram reproduzidas e os flags que indicam o estado
343
das teclas Ctrl, Alt e Shift. Em seguida, esse procedimento define o gancho. StartPlayBack( ) aparece no
código a seguir:
procedure StartPlayback;
{ Inicializa globais e define o gancho }
begin
{ apanha primeira mensagem da lista e coloca no buffer caso }
{ apanhemos um hc_GetNext antes de um hc_Skip }
MessageBuffer := TEventMsg(MessageList.Items[0]^);
{ inicializa contador de mensagem e indicador de reprodução }
MsgCount := 0;
{ inicializa flags de tecla Alt, Control e Shift }
AltPressed := False;
ControlPressed := False;
ShiftPressed := False;
{ define o gancho! }
HookHandle := SetWindowsHookEx(wh_JournalPlayback, Play, hInstance, 0);
if HookHandle = 0 then
raise ESKSetHookError.Create(‘Couldn’’t set hook’)
else
Playing := True;
end;
Como você pode observar pela chamada de SetWindowsHookEx( ), Play é o nome da função de gancho.
A declaração para Play é a seguinte:
function Play(Code: integer; wParam, lParam: Longint): Longint; stdcall;
Valor Significado
Code Um valor de hc_GetNext indica que você precisa preparar a próxima mensagem na lista para
processamento. Você faz isso copiando a próxima mensagem da lista no seu buffer global.
Um valor de hc_Skip significa que um ponteiro para a próxima mensagem deverá ser
colocado no parâmetro lParam para processamento. Qualquer outro valor significa que você
precisa chamar CallNextHookEx( ) e passar os parâmetros para o próximo gancho na cadeia.
wParam Não usado.
lParam Se Code for hc_Skip, você deverá colocar um ponteiro para o próximo registro TEventMsg no
parâmetro lParam.
Valor de retorno Retorna zero se Code for hc_GetNext. Se Code for hc_Skip, retorna o tempo total (em tiques)
antes que essa mensagem seja processada. Se for retornado zero, a mensagem será
processada. Caso contrário, o valor de retorno deverá ser o valor de retorno de
CallNextHookEx( ).
unit SendKey;
interface
344
Listagem 13.7 Continuação
uses
SysUtils, Windows, Messages, Classes, KeyDefs;
type
{ Códigos de erro }
TSendKeyError = (sk_None, sk_FailSetHook, sk_InvalidToken,
sk_UnknownError, sk_AlreadyPlaying);
{ primeiro código de tecla virtual ao útlimo código }
TvkKeySet = set of vk_LButton..vk_Scroll;
{ exceções }
ESendKeyError = class(Exception);
ESKSetHookError = class(ESendKeyError);
ESKInvalidToken = class(ESendKeyError);
ESKAlreadyPlaying = class(ESendKeyError);
var
Playing: Boolean;
implementation
uses Forms;
type
{ um descendente de TList que sabe como determinar seu conteúdo }
TMessageList = class(TList)
public
destructor Destroy; override;
end;
const
{ teclas “sys” válidas }
vkKeySet: TvkKeySet = [Ord(‘A’)..Ord(‘Z’), vk_Menu, vk_F1..vk_F12];
destructor TMessageList.Destroy;
var
i: longint;
begin
{ desaloca todos os registros de mensagem antes de descartar a lista }
for i := 0 to Count - 1 do
Dispose(PEventMsg(Items[i]));
inherited Destroy;
end;
var
{ variáveis globais à DLL }
MsgCount: word = 0;
MessageBuffer: TEventMsg;
HookHandle: hHook = 0;
345
Listagem 13.7 Continuação
procedure StopPlayback;
{ Desconecta o gancho e prepara para encerrar }
begin
{ Se o ganho estiver ativo atualmente, então o desconecta }
if Playing then
UnhookWindowsHookEx(HookHandle);
MessageList.Free;
Playing := False;
end;
HC_GETNEXT:
{ HC_GETNEXT significa preencher wParam e lParam com os valores }
{ apropriados para que a mensagem possa ser reproduzida. Não }
{ desconecte o gancho aqui. O valor de retorno indica quanto }
{ tempo até que o Windows deva reproduzir a mensagem. Retornamos }
{ 0 para que seja processado imediatamente. }
begin
{ move mensagem no buffer para a fila de mensagens }
PEventMsg(lParam)^ := MessageBuffer;
Result := 0 { processa imediatamente }
end
else
{ se Code não é HC_SKIP ou HC_GETNEXT, chama próximo gancho na cadeia }
Result := CallNextHookEx(HookHandle, Code, wParam, lParam);
end;
end;
procedure StartPlayback;
{ Inicializa globais e define o gancho }
begin
346
Listagem 13.7 Continuação
MakeMessage(vKey, wm_KeyUp);
end;
begin
Token := ‘’;
inc(index);
while S[index] < > KeyGroupClose do begin
{ inclui no Token até que seja encontrado o símbolo de final de código }
Token := Token + S[index];
inc(index);
{ verifica se o código não é muito longo }
if (Length(Token) = 7) and (S[index] < > KeyGroupClose) then
raise ESKInvalidToken.Create(‘No closing brace’);
end;
{ procura código no array, parâmetro Key terá }
{ código de tecla virtual, se tiver sucesso }
if not FindKeyInArray(Token, Key) then
raise ESKInvalidToken.Create(‘Invalid token’);
{ simula seqüência de toque de tecla }
SimKeyPresses(MakeWord(Key, 0));
end;
AltKey: AltPressed := True; // define flag Alt
ControlKey: ControlPressed := True; // define flag Control
ShiftKey: ShiftPressed := True; // define flag Shift
else begin
{ Um caracter normal foi pressionado }
{ converte em uma palavra onde o byte alto contém }
{ o estado de Shift e o byte baixo contém o código da vk }
KeyCode := vkKeyScan(S[index]);
{ simula seqüência de toque de tecla }
SimKeyPresses(KeyCode);
end;
end;
Inc(index);
until index > Length(S);
end;
procedure WaitForHook;
begin
repeat Application.ProcessMessages until not Playing;
end;
begin
MessageList.Free;
if E is ESKSetHookError then
Result := sk_FailSetHook
else if E is ESKInvalidToken then
Result := sk_InvalidToken
else if E is ESKAlreadyPlaying then
Result := sk_AlreadyPlaying;
end
else
Result := sk_UnknownError; // Tratamento de exceção genérico
end;
end;
end.
Usando SendKeys( )
Nesta seção, você criará um pequeno projeto que demonstra a função SendKeys( ). Comece com um for-
mulário que contém dois componentes TEdit e vários componentes TButton, como mostra a Figura 13.4.
Esse projeto se chama TestSend.dpr.
A Listagem 13.8 mostra o código-fonte para a unidade principal de TestSend, Main.pas. Essa unida-
de inclui manipuladores para os eventos de clique de botão.
unit Main;
interface
uses
SysUtils, Windows, Messages, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls, Menus;
type
TForm1 = class(TForm)
Edit1: TEdit;
Edit2: TEdit;
Button1: TButton;
Button2: TButton;
350
Listagem 13.8 Continuação
MainMenu1: TMainMenu;
File1: TMenuItem;
Open1: TMenuItem;
Exit1: TMenuItem;
Button4: TButton;
Button3: TButton;
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
procedure Open1Click(Sender: TObject);
procedure Exit1Click(Sender: TObject);
procedure Button4Click(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure Button3Click(Sender: TObject);
private
{ Declarações privadas }
public
{ Declarações públicas }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
end
else
MessageDlg(Format(‘Failed to invoke Notepad. Error code %d’,
[GetLastError]), mtError, [mbOk], 0);
end;
end.
Depois que você der um clique em Button1, SendKeys( ) é chamado e os toques de tecla a seguir são
enviados: Shift+Del apaga o conteúdo de Edit1; “I love...” é então digitado em Edit1; um caracter de ta-
bulação é enviado, o qual passará o foco para Edit2, para onde será enviado Shift+D, “elphi ”, Shift+D,
“evelopers ”, Shift+G, “uide!”.
O manipulador OnClick para Button2 também é interessante. Esse método usa a função da API Crea-
teProcess( ) para executar uma instância do Bloco de notas. Depois ele usa a função da API WaitFor-
InputIdle( ) para esperar até que o processo do Bloco de notas esteja pronto para a entrada. Finalmente,
ele digita uma mensagem na janela do Bloco de notas.
Depois que o arquivo-objeto estiver vinculado, você terá que definir cada função que deseja chamar
a partir do arquivo-objeto no seu código em Object Pascal. Use a diretiva external para indicar que o com-
pilador Pascal deverá esperar até o momento da linkedição para tentar decidir o nome da função. Por
352
exemplo, a linha de código a seguir define uma função externa, chamada Foo, que não utiliza e nem retor-
na parâmetro algum:
procedure Foo; external;
Embora a princípio essa capacidade possa parecer poderoso, ela possui diversas limitações que tor-
nam esse recurso difícil de se implementar em muitos casos:
lO Object Pascal só pode acessar diretamente apenas código, e não dados contidos em arqui-
vos-objeto (embora exista um truque para se obter dados em um OBJ, que você verá mais adian-
te). No entanto, os dados do Pascal podem ser acessados a partir dos arquivos-objeto.
lO Object Pascal não pode linkeditar com arquivos LIB (biblioteca estática).
lOs arquivos-objeto contendo classes do C++ não serão linkeditados devido às referências im-
plícitas à RTL do C++. Embora seja possível resolver essas referências separando a RTL do
C++ em OBJs, geralmente o resultado não compensa o trabalho envolvido.
lOs arquivos-objeto precisam estar no formato OMF da Intel. Esse é o formato de saída dos com-
piladores C++ da Borland, mas não dos compiladores C++ da Microsoft, que produzem ar-
quivos OBJ no formato COFF.
NOTA
Uma limitação já extinta, que recentemente tem sido focalizada pelo compilador do Delphi é a capacidade
de resolver referências OBJ-para-OBJ. Nas versões anteriores do Delphi, os arquivos-objeto não podiam
conter referências ao código ou dados armazenados em outros arquivos-objeto.
Para chamar essa função por uma aplicação em Delphi, você precisa primeiro vincular o arqui-
vo-objeto ao EXE usando a diretiva $L ou $LINK:
{$L ccode.obj}
Depois disso, você precisa criar uma definição em Object Pascal para a função, como vemos aqui:
function SayHello(Text: PChar): integer; external;
ATENÇÃO
Observe o uso da diretiva __fastcall em C++, que serve para garantir que as convenções de chamada
usadas no código em C++ e Object Pascal são iguais. Os temíveis erros fatais podem ocorrer se você não
combinar corretamente as convenções de chamada entre o protótipo do C++ e a declaração do Object
Pascal, e problemas de convenção de chamada são o obstáculo mais comum para os programadores que
tentam compartilhar código entre as duas linguagens. Para ajudar a esclarecer as coisas, a tabela a seguir
mostra a correspondência entre as diretivas de convenção de chamada do Object Pascal e do C++.
register* __fastcall
pascal __pascal
cdecl __cdecl*
stdcall __stdcall
353
Mutilação de nome
Por default, o compilador do C++ mutilará os nomes das funções não declaradas explicitamente usando
o modificador extern “C”. O compilador do Object Pascal, é claro, não mutila os nomes das funções. Por
exemplo, o utilitário TDUMP do Delphi revela o nome do símbolo exportado da função SAYHELLO mostrada
anteriormente em ccode.obj como @SAYHELLO$qqrpc, enquanto o nome da função importada de acordo com
o Object Pascal é SAYHELLO (o Object Pascal força os símbolos para maiúsculas).
Na superfície, isso pode parecer um problema: como o linkeditor do Delphi pode solucionar a roti-
na externa se o nome da função nem sequer é o mesmo? A resposta é que o linkeditor do Delphi simples-
mente ignora a parte mutilada do símbolo (o @ e tudo após o $), mas isso pode ter alguns efeitos colaterais
bastante desagradáveis.
O motivo geral para o C++ mutilar os nomes é para permitir o overload de funções (funções tendo
os mesmos nomes e diferentes listas de parâmetros). Se você possuir uma função com várias definições de
overload e o Delphi ignorar a parte mutilada do símbolo, nunca saberá com certeza se o Delphi está cha-
mando a função de overload que você deseja chamar. Devido a essas complexidades, recomendamos que
você não tente chamar funções de overload por meio de arquivos-objeto.
NOTA
As funções em um arquivo-fonte do C++ (.CPP) sempre serão mutiladas, a menos que os protótipos sejam
combinados com o modificador extern “C” ou se a chave da linha de comandos apropriada for utilizada no
compilador do C++ para suprimir a mutilação de nomes.
Compartilhando dados
Como já dissemos, é possível acessar dados do Delphi a partir do arquivo-objeto. O primeiro passo é de-
clarar uma variável global no seu código-fonte em Object Pascal semelhante à variável mostrada a seguir
(observe o sublinhado):
var
_GLOBALVAR: PChar = ‘This is a Delphi String’;
ATENÇÃO
O comportamento default do compilador Borland C++ é iniciar as variáveis externas com um sublinhado
ao gerar o símbolo externo (ou seja, GLOBALVAR torna-se _GLOBALVAR). Você pode contornar isso de duas ma-
neiras:
• Use a chave da linha de comandos para desativar o acréscimo do sublinhado (-u- nos compiladores Bor-
land C++).
Embora não seja possível compartilhar diretamente dados declarados em um arquivo OBJ com o
código em Object Pascal, é possível enganar o Object Pascal para que acesse dados baseados no OBJ. O
primeiro passo é declarar os dados que você deseja exportar no seu código em C++ usando a diretiva
__export. Por exemplo, você tornaria um array char disponível para exportação da seguinte forma:
Isso permitirá que o linkeditor solucione as referências a _C_VAR no seu código em Pascal. Finalmente
(e aqui está a segunda parte do truque), você pode usar _C_VAR no seu código em Pascal como um ponteiro
para os dados. Por exemplo, o código a seguir pode ser usado para se obter o valor do array:
type
PCharArray = ^TCharArray;
TCharArray = array[0..127] of char;
fará com que o linkeditor do Object Pascal procure uma função própria, chamada MessageBox, quando
montar o executável. Naturalmente, existe uma função com esse nome definida na unidade do Win-
dows. Agora, sua aplicação será compilada e linkeditada facilmente, sem qualquer empecilho.
A Listagem 13.9 mostra um exemplo completo de tudo o que falamos a respeito até o momento. Ela
contém um módulo em C muito simples, chamado ccode.c.
355
Listagem 13.9 Um módulo simples do C++: ccode.c
#include “PasStng.h”
// globais
extern char * GLOBALVAR;
// dados exportados
char __export C_VAR[128];
#ifdef __cplusplus
extern “C” {
#endif
//externos
extern int __stdcall MessageBox(long, char *, char *, long);
//funções
int __export __cdecl SAYHELLO(char * hellostr)
{
char a[64];
memset(a, 64, 0);
strcat(a, hellostr);
strcat(a, “ from Borland C++Builder”);
MessageBox(0, a, GLOBALVAR, 0);
return 0;
}
#ifdef __cplusplus
} // final do “C” externo
#endif
Além de MessageBox( ), observe as chamadas que esse módulo faz às funções da RTL do C++ mem-
set( ) e strcat( ). Essas funções são tratadas de modo semelhante no arquivo de cabeçalho (header)
PasStng.h, que contém algumas das funções mais comuns do cabeçalho string.h. Esse arquivo aparece na
Listagem 13.10.
// PasStng.h
// Este módulo externa uma parte do cabeçalho string.h da RTL do C++
// para que a RTL do Object Pascal possa lidar com as chamadas.
#ifndef PASSTNG_H
#define PASSTNG_H
#ifndef _SIZE_T
#define _SIZE_T
typedef unsigned size_t;
#endif
#ifdef __cplusplus
extern “C” {
356 #endif
Listagem 13.10 Continuação
#ifdef __cplusplus
} // fim do “C” externo
#endif
#endif // PASSTNG_H
Visto que essas funções não existem na RTL do Object Pascal, podemos contornar o problema cri-
ando uma unidade do Object Pascal para incluir no nosso projeto, que mapeia essas funções para seus
correspondentes em Object Pascal. Essa unidade, PasStrng.pas, aparece na Listagem 13.11.
unit PasStrng;
interface
uses Windows;
implementation
uses SysUtils;
begin
Result := StrLIComp(P1, P2, MaxLen);
end;
end;
end.
DICA
Usando a técnica que acabamos de mostrar, você poderia externar mais da RTL do C++ e da API do
Win32 em arquivos de cabeçalho mapeados em unidades do Object Pascal.
Listagem 13.12 cdll.cpp: um módulo do C++ que contém uma definição de classe
#include <windows.h>
// objetos
class TFoo
{
virtual int function1(char *);
virtual int function2(int);
};
// funções-membro
int TFoo::function1(char * str1)
{
MessageBox(NULL, str1, “Hello from C++ DLL”, MB_OK);
return 0;
}
int TFoo::function2(int i)
{
return i * i;
360 }
Listagem 13.12 Continuação
#ifdef __cplusplus
extern “C” {
#endif
// protótipos
TFoo * __declspec(dllexport) ClassFactory(void);
void __declspec(dllexport) ClassKill(TFoo *);
#ifdef __cplusplus
}
#endif
Para usar esse objeto a partir de uma aplicação em Delphi, você precisa fazer duas coisas. Primeiro,
precisa importar as funções que criam e destroem instâncias da classe. Segundo, precisa estabelecer uma
definição de classe abstrata virtual do Object Pascal, que envolva a classe do C++. Veja como fazer isso:
type
TFoo = class
function Function1(Str1: PChar): integer; virtual; cdecl; abstract;
function Function2(i: integer): integer; virtual; cdecl; abstract;
end;
NOTA
Ao definir o wrapper do Object Pascal para uma classe do C++, você não precisa se preocupar com os
nomes das funções, pois eles não são importantes para determinar como a função é chamada internamen-
te. Como todas as chamadas serão emitidas através da Virtual Method Table (tabela de método virtual), a
ordem em que as funções são declaradas é de importância fundamental. Não se esqueça de que a ordem
das funções é a mesma nas definições do C++ e do Object Pascal.
A Listagem 13.13 mostra Main.pas, uma unidade principal para o projeto CallC.dpr, que demonstra
todas as técnicas do C++ mostradas até aqui neste capítulo. O formulário principal para esse projeto
aparece na Figura 13.5. 361
FIGURA 13.5 O formulário principal para o projeto CallC.
unit Main;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, ExtCtrls;
type
TMainForm = class(TForm)
Button1: TButton;
Button2: TButton;
FooData: TEdit;
Button3: TButton;
Button4: TButton;
SetCVarData: TEdit;
GetCVarData: TEdit;
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
procedure Button3Click(Sender: TObject);
procedure Button4Click(Sender: TObject);
private
{ Declarações privadas }
public
{ Declarações públicas }
end;
var
MainForm: TMainForm;
_GlobalVar: PChar = ‘This is a Delphi String’;
implementation
uses PasStrng;
{$R *.DFM}
{$L ccode.obj}
type
TFoo = class
function Function1(Str1: PChar): integer; virtual; cdecl; abstract;
function Function2(i: integer): integer; virtual; cdecl; abstract;
end;
362
Listagem 13.13 Continuação
PCharArray = ^TCharArray;
TCharArray = array[0..127] of char;
end.
363
DICA
Embora a técnica demonstrada aqui permita um meio limitado de comunicação com as classes do C++ a
partir do Object Pascal, se você quiser fazer esse tipo de coisa em grande escala, recomendamos que use
objetos COM para a comunicação entre as linguagens, conforme descrito no Capítulo 23.
Thunking
Em algum ponto no seu desenvolvimento de aplicações para Windows e Win32, você precisará chamar
um código de 16 bits a partir de uma aplicação de 32 bits, ou ainda um código de 32 bits a partir de uma
aplicação de 16 bits. Esse processo é conhecido como thunking. Embora as diferentes variedades de
Win32 ofereçam várias facilidades para tornar isso possível, resta uma das tarefas mais difíceis de se rea-
lizar quando se desenvolve aplicações do Windows.
DICA
Além do thunking, você precisa saber que a Automation (descrita no Capítulo 23) oferece uma alternativa
razoável para a travessia dos limites de 16/32 bits. Essa capacidade está embutida na interface IDispatch
da Automation.
O Win32 oferece três tipos diferentes de thunking: universal, genérico e plano. Cada uma dessas
técnicas possui vantagens e desvantagens:
l O thunking universal está disponível apenas sob a plataforma Win32s (Win32s é o subconjunto
da API do Win32 disponível sob o Windows de 16 bits). Ele permite que as aplicações de 16 bits
carreguem e chamem DLLs do Win32. Como essa variedade de thunking é aceita apenas para
Win32s, uma plataforma não aceita oficialmente pelo Delphi, não discutiremos mais sobre esse
assunto.
l O thunking genérico permite que aplicações de 16 bits do Windows chamem DLLs do Win32
sob o Windows 95, 98, NT e 2000. Esse é o tipo mais flexível de thunking, pois está disponível
em todas as principais plataformas Win32 e é baseado na API. Discutiremos essa opção com de-
talhes mais adiante.
l O thunking plano permite que aplicações do Win32 chamem DLLs de 16 bits e que aplicações de
16 bits chamem DLLs do Win32. Infelizmente, esse tipo de thunking só está disponível sob o
Windows 95/98; ele também exige o uso do compilador thunk para criar os arquivos-objeto,
que precisam ser vinculados nos lados de 32 e 16 bits. Devido à falta de portabilidade e ao requi-
sito de ferramentas adicionais, não explicaremos aqui o thunking plano.
Além disso, existe um meio de compartilhar dados entre processos de 32 bits e 16 bits, usando a
mensagem WM_COPYDATA do Windows. Em particular, WM_COPYDATA oferece um meio direto de acessar código
de 16 bits a partir do Windows NT/2000 (onde o thunking pode ser uma dor de cabeça), e portanto tam-
bém explicamos isso nesta seção.
Thunking genérico
O thunking genérico é facilitado por meio de um conjunto de APIs que reside nos lados de 16 bits e de 32
bits. Essas APIs são conhecidas como WOW16 e WOW32, respectivamente. No campo dos 16 bits, WOW16 oferece
funções que permitem carregar a DLL do Win32, apanhar o endereço de funções na DLL e chamar essas
funções. O código-fonte para a unidade WOW16.pas aparece na Listagem 13.14.
364
Listagem 13.14 WOW16.pas, funções para carregar uma DLL de 32 bits a partir de uma
aplicação de 16 bits
unit WOW16;
// Unidade que oferece uma interface para o Windows de 16 bits na API
// do Win32 (WOW) a partir de uma aplicação de 16 bits rodando no Win32.
// Essas funções permitem que aplicações de 16 bits chamem DLLs de 32 bits.
// Copyright (c) 1996, 1999 Steve Teixeira e Xavier Pacheco
interface
uses WinTypes;
type
THandle32 = Longint;
DWORD = Longint;
{ Converte alça de janela de 16 bits para 32 bits para uso no Windows NT. }
function HWnd16To32(Handle: hWnd): THandle32;
implementation
uses WinProcs;
end.
Todas as funções nesta unidade são simplesmente exports do kernel de 16 bits, exceto para a função
Call32BitProc( ), que emprega algum código em Assembly para permitir que o usuário passe um número
variável de parâmetros em um array de Longint.
As funções WOW32 compõem a unidade WOW32.pas, que aparece na Listagem 13.15.
Listagem 13.15 WOW32.pas, interface para WOW32.dll, que oferece acesso ao código de 16 bits a partir
de aplicações Win32
unit WOW32;
// Importação de WOW32.DLL, que fornece utilitários para acessar
// código de 16 bits a partir do Win32.
// Copyright (c) 1996, 1999 Steve Teixeira e Xavier Pacheco
interface
uses Windows;
//
// Tradução de ponteiro 16:16 -> 0:32.
//
// WOWGetVDMPointer converterá o endereço de 16 bits passado
// no ponteiro plano equivalente de 32 bits. Se fProtectedMode
// for TRUE, a função trata os 16 bits superiores como um seletor
// na tabela de descritor local. Se fProtectedMode for FALSE,
// os 16 bits superiores são tratados como um valor de segmento em
367
Listagem 13.15 Continuação
//
// As duas funções a seguir estão aqui por compatibilidade com o
// Windows 95. No Win95, a heap global pode ser reorganizada,
// invalidando os ponteiros planos retornados por WOWGetVDMPointer,
// enquanto um thunk está em execução. No Windows NT, a VDM de 16 bits
// é completamente interrompida enquanto um thunk é executado, de modo
// que a única maneira de a heap ser reorganizada é fazer um
// callback para o código Win16.
//
// As versões Win95 dessas funções chamam GlobalFix para bloquear o
// endereço plano de um segmento, e GlobalUnfix para liberar o
// segmento.
//
// As implementações do NT dessas funções “não” chamam
// GlobalFix/GlobalUnfix no segmento, pois não haverá qualquer
// movimento da heap a menos que ocorra um callback. Se o seu
// thunk fizer callback para o lado de 16 bits, certifique-se de
// descartar os ponteiros planos e chamar WOWGetVDMPointer novamente
// para ter certeza de que o endereço plano está correto.
//
function WOWGetVDMPointerFix(vp, dwBytes: DWORD; fProtectedMode: BOOL):
Pointer; stdcall;
procedure WOWGetVDMPointerUnfix(vp: DWORD); stdcall;
//
// Gerenciamento de memória do Win16.
//
// Estas funções podem ser usadas para gerenciar a memória na heap
// do Win16. As quatro funções a seguir são idênticas ao seu
// correspondente Win16, exceto que são chamadas a partir do
// código do Win32.
//
function WOWGlobalAlloc16(wFlags: word; cb: DWORD): word; stdcall;
function WOWGlobalFree16(hMem: word): word; stdcall;
function WOWGlobalLock16(hMem: word): DWORD; stdcall;
function WOWGlobalUnlock16(hMem: word): BOOL; stdcall;
//
// As três funções a seguir combinam duas operações comuns em uma
// passagem para o modo de 16 bits.
368 //
Listagem 13.15 Continuação
//
// Gerando o escalonador não-preemptivo do Win16
//
// As duas funções a seguir são fornecidas para o código do Win32
// chamado por meio de Generic Thunks, que precisa gerar o escalonador
// do Win16 para que as tarefas nessa VDM possam ser executadas
// enquanto o thunk espera por algo para completar. Essas duas
// funções são funcionalmente idênticas a chamar de volta para o
// código de 16 bits, que chama Yield ou DirectedYield.
//
procedure WOWYield16;
procedure WOWDirectedYield16(htask16: word);
//
// Callbacks genéricos.
//
// WOWCallback16 pode ser usado no código do Win32 chamado a partir de
// 16 bits (como ao usar Generic Thunks) para chamar de volta para o
// lado de 16 bits. A função chamada deve ser declarada de modo
// semelhante a este:
//
// function CallbackRoutine(dwParam: Longint): Longint; export;
//
// Se você estiver passando um ponteiro, declare o parâmetro como:
//
// function CallbackRoutine(vp: Pointer): Longint; export;
//
// NOTA: Se estiver passando um ponteiro, você terá que obter o
// ponteiro usando WOWGlobalAlloc16 ou WOWGlobalAllocLock16
//
// Se a função chamada retornar uma palavra ao invés de um Longint,
// os 16 bits superiores do valor de retorno são indefinidos. De modo
// semelhante, se a função chamada não tiver valor de retorno, o
// valor de retorno inteiro será indefinido.
//
// WOWCallback16Ex permite qualquer combinação de argumentos até o
// total de bytes de WCB16_MAX_CBARGS ser passado para a rotina de 16
// bits. cbArgs é usado para limpar corretamente a pilha de 16 bits
// depois de chamar a rotina. Independente do valor de cbArgs,
// WCB16_MAX_CBARGS bytes sempre serão copiados de pArgs para a pilha
// de 16 bits. Se pArgs for menor do que WCB16_MAX_CBARGS bytes a
// partir do final de uma página, e a página seguinte for inacessível,
// WOWCallback16Ex incorrerá em uma violação de acesso.
//
// Se cbArgs for maior do que o WCB16_MAX_ARGS que o sistema em
// execução aceita, a função retorna FALSE e GetLastError retorna
// ERROR_INVALID_PARAMETER. Caso contrário, a função retorna TRUE e
// a DWORD apontada pelo pdwRetCode contém o código de retorno da
369
Listagem 13.15 Continuação
const
WCB16_MAX_CBARGS = 16;
WCB16_PASCAL = $0;
WCB16_CDECL = $1;
//
// Funções de mapeamento de alça 16 <–> 32.
//
type
TWOWHandleType = (
WOW_TYPE_HWND,
WOW_TYPE_HMENU,
WOW_TYPE_HDWP,
WOW_TYPE_HDROP,
WOW_TYPE_HDC,
WOW_TYPE_HFONT,
WOW_TYPE_HMETAFILE,
WOW_TYPE_HRGN,
WOW_TYPE_HBITMAP,
WOW_TYPE_HBRUSH,
WOW_TYPE_HPALETTE,
370
Listagem 13.15 Continuação
WOW_TYPE_HPEN,
WOW_TYPE_HACCEL,
WOW_TYPE_HTASK,
WOW_TYPE_FULLHWND);
implementation
const
WOW32DLL = ‘WOW32.DLL’;
function WOWCallback16;
external WOW32DLL name ‘WOWCallback16’;
function WOWCallback16Ex;
external WOW32DLL name ‘WOWCallback16Ex’;
function WOWGetVDMPointer;
external WOW32DLL name ‘WOWGetVDMPointer’;
function WOWGetVDMPointerFix;
external WOW32DLL name ‘WOWGetVDMPointerFix’;
procedure WOWGetVDMPointerUnfix;
external WOW32DLL name ‘WOWGetVDMPointerUnfix’
function WOWGlobalAlloc16;
external WOW32DLL name ‘WOWGlobalAlloc16’
function WOWGlobalAllocLock16;
external WOW32DLL name ‘WOWGlobalAllocLock16’;
function WOWGlobalFree16;
external WOW32DLL name ‘WOWGlobalFree16’;
function WOWGlobalLock16;
external WOW32DLL name ‘WOWGlobalLock16’;
function WOWGlobalLockSize16;
external WOW32DLL name ‘WOWGlobalLockSize16’;
function WOWGlobalUnlock16;
external WOW32DLL name ‘WOWGlobalUnlock16’;
function WOWGlobalUnlockFree16;
external WOW32DLL name ‘WOWGlobalUnlockFree16’;
function WOWHandle16;
external WOW32DLL name ‘WOWHandle16’;
function WOWHandle32;
external WOW32DLL name ‘WOWHandle32’;
procedure WOWYield16;
external WOW32DLL name ‘WOWYield16’;
procedure WOWDirectedYield16;
external WOW32DLL name ‘WOWDirectedYield16’;
end.
371
Para ilustrar o thunking genérico, criaremos uma pequena DLL de 32 bits que será chamada a partir
de um executável de 16 bits. O projeto de DLL de 32 bits, TestDLL.dpr, aparece na Listagem 13.16.
library TestDLL;
uses
SysUtils, Dialogs, Windows, WOW32;
const
DLLStr = ‘I am in the 32-bit DLL. The string you sent is: “%s”’;
exports
DLLFunc32 name ‘DLLFunc32’ resident;
begin
end.
Essa DLL exporta uma função que apanha um PChar e uma função de callback como parâmetros.
O PChar é apresentado imediatamente em uma caixa de ShowMessage( ). A função de callback permite que
a função chame de volta no processo de 16 bits, passando alguma memória de 16 bits alocada especial-
mente.
O código para a aplicação de 16 bits, Call32.dpr, aparece na Listagem 13.17. O formulário principal
pode ser visto na Figura 13.6.
372
FIGURA 13.6 O formulário principal de Call32.
Listagem 13.17 Main.pas, a unidade principal para a parte de 16 bits da aplicação de teste do thunking
genérico
unit Main;
{$C FIXED DEMANDLOAD PERMANENT}
interface
uses
SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls;
type
TMainForm = class(TForm)
CallBtn: TButton;
Edit1: TEdit;
Label1: TLabel;
procedure CallBtnClick(Sender: TObject);
private
{ Declarações privadas }
public
{ Declarações públicas }
end;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
uses WOW16;
const
ExeStr = ‘The 32-bit DLL has called back into the 16-bit EXE. ‘ +
‘The string to the EXE is: “%s”’;
begin
{ carrega DLL de 32 bits }
H := LoadLibraryEx32W(‘TestDLL.dll’, 0, 0);
AStr := StrNew(‘I love DDG.’);
try
if H > 0 then
begin
{ Recupera endereço do proc a partir da DLL de 32 bits }
TFarProc(P) := GetProcAddress32W(H, ‘DLLFunc32’);
if P > 0 then
begin
{ Chama proc na DLL de 32 bits }
R := Call32BitProc(P, [Longint(AStr), Longint(@CallBackFunc)],
1);
Edit1.Text := IntToStr(R);
end;
end;
finally
StrDispose(AStr);
if H > 0 then FreeLibrary32W(H);
end;
end;
end.
Essa aplicação passa um PChar de 16 bits e o endereço da função para a DLL de 32 bits. CallBack-
Func( ) por fim é chamada pela DLL de 32 bits. Na verdade, se você olhar atentamente, o valor de retor-
no de DLLFunc32( ) é o valor retornado por CallBackFunc( ).
WM_COPYDATA
O Windows 95/98 possui suporte para thunks planos chamando DLLs de 16 bits a partir de aplicações
Win32. O Windows NT/2000 não oferece um meio de chamar diretamente o código de 16 bits a partir
de uma aplicação Win32. Devido a essa limitação, a pergunta seguinte é: qual a melhor maneira de co-
municar dados entre processos de 32 bits e 16 bits no NT? Mais ainda, isso nos leva a outra pergunta:
existe alguma maneira fácil de compartilhar dados de tal maneira que possa ser executada sob todas as
principais plataformas Win32 (Windows 95, 98, NT e 2000)?
A resposta para as duas perguntas é WM_COPYDATA. A mensagem WM_COPYDATA do Windows oferece um
meio de transferir dados binários entre processos, sejam eles de 32 ou de 16 bits. Quando uma mensagem
WM_COPYDATA é enviada para uma janela, o wParam dessa mensagem identifica a janela que passa os dados, e o
lParam contém um ponteiro para um registro TCopyDataStruct. Esse registro é definido da seguinte maneira:
type
PCopyDataStruct = ^TCopyDataStruct;
TCopyDataStruct = packed record
dwData: DWORD;
cbData: DWORD;
lpData: Pointer;
end;
O campo dwData contém 32 bits de informações definidas pelo usuário. cbData contém o tamanho do
buffer apontado por lpData. lpData é um ponteiro para um buffer de informações que você deseja passar
entre as aplicações. Se você enviar essa mensagem entre aplicações de 32 e de 16 bits, o Windows conver-
374
terá automaticamente o ponteiro lpData de um ponteiro 0:32 para um ponteiro 16:16, ou vice-versa.
Além do mais, o Windows garantirá que os dados apontados por lpData sejam mapeados no espaço de en-
dereços do processo receptor.
NOTA
WM_COPYDATA funciona muito bem para quantidades de informações relativamente pequenas, mas se você
tiver muitas informações que devam ser comunicadas entre os limites de 16/32 bits, o melhor será usar a
Automation, que possui capacidade interna para se guiar entre os limites de processo. A Automation é des-
crita no Capítulo 23.
DICA
Deve ser claro que, embora o NT não aceite o uso direto de DLLs de 16 bits a partir de aplicações Win32,
você pode criar um executável de 16 bits que encapsule a DLL e pode se comunicar com esse executável
usando WM_COPYDATA.
Para lhe mostrar como funciona WM_COPYDATA, vamos criar dois projetos, o primeiro sendo uma apli-
cação de 32 bits. Essa aplicação terá um controle memo, no qual você poderá digitar algum texto. Além
disso, essa aplicação oferecerá um meio de comunicação com o segundo projeto, uma aplicação de 16
bits, para transferir texto do memo. Para fornecer um meio pelo qual as duas aplicações possam iniciar a
comunicação, use as seguintes etapas:
1. Registre uma mensagem de janela para obter um identificador (ID) de mensagem exclusivo para a co-
municação entre as aplicações.
2. Transmita a mensagem por todo o sistema a partir da aplicação Win32. No wParam dessa mensagem,
armazene a alça para a janela principal da aplicação Win32.
3. Quando a aplicação de 16 bits receber a mensagem transmitida, ela responderá enviando a mensagem
registrada de volta à aplicação emissora, passando a alça de janela do seu próprio formulário principal
como wParam.
4. Depois de receber a resposta, a aplicação de 32 bits agora terá a alça para o formulário principal da apli-
cação de 16 bits. A aplicação de 32 bits pode agora enviar uma mensagem WM_COPYDATA para a aplicação
de 16 bits, para que o compartilhamento possa ser iniciado.
O código para a unidade RegMsg.pas, que é compartilhada pelos dois projetos, aparece na Listagem 13.18.
unit RegMsg;
interface
var
DDGM_HandshakeMessage: Cardinal;
implementation
uses WinProcs;
const
HandshakeMessageStr: PChar = ‘DDG.CopyData.Handshake’;
initialization
DDGM_HandshakeMessage := RegisterWindowMessage(HandshakeMessageStr);
end.
375
O código-fonte para CopyMain.pas, a unidade principal do projeto CopyData.dpr de 32 bits, aparece na
Listagem 13.19. Essa é a unidade que estabelece a conversação e envia os dados.
unit CopyMain;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, ExtCtrls, Menus;
type
TMainForm = class(TForm)
DataMemo: TMemo;
BottomPnl: TPanel;
BtnPnl: TPanel;
CloseBtn: TButton;
CopyBtn: TButton;
MainMenu1: TMainMenu;
File1: TMenuItem;
CopyData1: TMenuItem;
N1: TMenuItem;
Exit1: TMenuItem;
Help1: TMenuItem;
About1: TMenuItem;
procedure CloseBtnClick(Sender: TObject);
procedure FormResize(Sender: TObject);
procedure About1Click(Sender: TObject);
procedure CopyBtnClick(Sender: TObject);
private
{ Declarações privadas }
protected
procedure WndProc(var Message: TMessage); override;
public
{ Declarações públicas }
end;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
var
Recipients: DWORD = BSM_APPLICATIONS;
end.
377
O código-fonte para ReadMain.pas, a unidade principal para o projeto ReadData.dpr de 16 bits, apare-
ce na Listagem 13.20. Essa é a unidade que se comunica com o projeto CopyData e recebe o buffer de
dados.
unit Readmain;
interface
uses
SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,
Forms, Dialogs, Menus, StdCtrls;
type
TMainForm = class(TForm)
ReadMemo: TMemo;
MainMenu1: TMainMenu;
File1: TMenuItem;
Exit1: TMenuItem;
Help1: TMenuItem;
About1: TMenuItem;
procedure Exit1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure About1Click(Sender: TObject);
private
procedure OnAppMessage(var M: TMsg; var Handled: Boolean);
procedure WMCopyData(var M: TMessage); message WM_COPYDATA;
end;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
type
{ O tipo de registro TCopyDataStruct não é definido na unidade }
{ WinTypes, embora esteja disponível na API de 16 bits do Windows }
{ quando executado sob o Windows 95 e NT. O lParam da mensagem }
{ WM_COPYDATA aponta para um destes. }
PCopyDataStruct = ^TCopyDataStruct;
TCopyDataStruct = record
378
Listagem 13.20 Continuação
dwData: DWORD;
cbData: DWORD;
lpData: Pointer;
end;
end.
379
FIGURA 13.7 Comunicando com WM_COPYDATA.
Module é a alça de módulo da API do Win32 do arquivo de pacote, como a alça retornada pela função
LoadLibrary( ) da API.
Param são dados definidos pelo usuário, que serão passados para o procedimento especificado pelo
parâmetro InfoProc.
Ao retornar, o parâmetro Flags terá informações sobre o pacote. Isso se tornará uma combinação
dos flags mostrados na Tabela 13.6.
O parâmetro InfoProc identifica um método de callback que será chamado uma vez para cada pacote
que esse pacote necessita e para cada unidade contida nesse pacote. Esse parâmetro é do tipo TPackageIn-
foProc, que é definido da seguinte maneira:
type
TNameType = (ntContainsUnit, ntRequiresPackage);
TPackageInfoProc = procedure (const Name: string; NameType: TNameType;
Flags: Byte; Param: Pointer);
Nesse tipo de método, Name identifica o nome do pacote ou unidade, NameType indica se esse arquivo é
um pacote ou uma unidade, Flags oferece algumas informações adicionais para o arquivo e Param contém
os dados definidos pelo usuário, passados originalmente para GetPackageInfo( ).
Para demonstrar o procedimento GetPackageInfo( ), a seguir vemos uma aplicação de exemplo, usa-
da para obter informações para qualquer pacote. Esse projeto é denominado PackInfo, e o arquivo de
projeto aparece na Listagem 13.21.
380
Tabela 13.6 Flags de GetPackageInfo( )
program PkgInfo;
uses
Forms,
Dialogs,
SysUtils,
PkgMain in ‘PkgMain.pas’ {PackInfoForm};
{$R *.RES}
var
OpenDialog: TOpenDialog;
begin
if (ParamCount > 0) and FileExists(ParamStr(1)) then
PkgName := ParamStr(1)
else begin
OpenDialog := TOpenDialog.Create(Application);
OpenDialog.DefaultExt := ‘*.bpl’;
OpenDialog.Filter := ‘Packages (*.bpl)|*.bpl|Delphi 3 Packages ‘ +
‘(*.dpl)|*.dpl’;
if OpenDialog.Execute then PkgName := OpenDialog.FileName;
end;
if PkgName < > ‘’ then
begin
Application.Initialize;
Application.CreateForm(TPackInfoForm, PackInfoForm);
Application.Run;
end;
end.
381
Se não forem passados parâmetros da linha de comandos para essa aplicação, ela imediatamente
apresenta ao usuário uma caixa de diálogo File Open, onde o usuário pode selecionar um arquivo de pa-
cote. Se um arquivo de pacote for passado na linha de comandos ou se um arquivo for selecionado na cai-
xa de diálogo, esse nome de arquivo será atribuído a PkgName e a aplicação poderá ser executada normal-
mente.
A unidade principal para essa aplicação aparece na Listagem 13.22. Essa é a unidade que realiza a
chamada para GetPackageInfo( ).
unit PkgMain;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, ExtCtrls;
type
TPackInfoForm = class(TForm)
GroupBox1: TGroupBox;
DsgnPkg: TCheckBox;
RunPkg: TCheckBox;
BuildCtl: TRadioGroup;
GroupBox2: TGroupBox;
GroupBox3: TGroupBox;
Button1: TButton;
Label1: TLabel;
DescEd: TEdit;
memContains: TMemo;
memRequires: TMemo;
procedure FormCreate(Sender: TObject);
procedure Button1Click(Sender: TObject);
end;
var
PackInfoForm: TPackInfoForm;
PkgName: string; // Isso é atribuído no arquivo de projeto
implementation
{$R *.DFM}
else
Memo := nil;
end;
if Memo < > nil then
begin
if Memo.Text < > ‘’ then AddName := ‘, ‘ + AddName;
Memo.Text := Memo.Text + AddName;
end;
end;
end.
Parece que existe uma quantidade de código desproporcionalmente pequena para essa unidade,
considerando as informações de baixo nível que ela obtém. Quando o formulário é criado, o pacote é
carregado, GetPackageInfo( ) é chamado e alguma interface com o usuário é atualizada. O método Packa-
geInfoCallback( ) é passado no parâmetro InfoProc de GetPackageInfo( ). PackageInfoCallback( ) inclui o
nome do pacote ou da unidade no controle TMemo apropriado. A Figura 13.8 mostra a aplicação PackInfo
exibindo informações para um dos pacotes do Delphi.
383
FIGURA 13.8 Exibindo informações do pacote com PackInfo.
Resumo
Ufa! Esse foi um capítulo profundo! Reflita por um momento e veja tudo o que você aprendeu: subclassi-
ficar procedimentos de janela, evitar instâncias múltiplas, ganhos de janelas, programação em BASM,
uso de arquivos-objeto do C++, uso de classes do C++, thunking, WM_COPYDATA e apanhar informações
para pacotes compilados. Não sei quanto a você, mas vimos tanta coisa neste capítulo que estou com uma
baita fome – que tal pizza e Coca-Cola? Como estamos trabalhando com programação de baixo nível, o
próximo capítulo explica como abrir as entranhas do sistema operacional para obter informações sobre
processos, threads e módulos.
384
Análise de informações CAPÍTULO
do sistema
14
NE STE C AP ÍT UL O
l InfoForm: obtendo informações gerais 386
l Projeto independente da plataforma 398
l Windows 95/98: usando ToolHelp32 399
l Windows NT/2000: PSAPI 420
l Resumo 431
Neste capítulo, você aprenderá a criar um utilitário completo, chamado SysInfo, elaborado para pesqui-
sar os parâmetros vitais do seu sistema. Durante o desenvolvimento dessa aplicação, você aprenderá a
empregar APIs menos conhecidas para ter acesso a informações de baixo nível, de todo o sistema, refe-
rentes a processos, threads, módulos, heaps, drivers e páginas. Este capítulo também explica como o
Windows 95/98 e o Windows NT obtêm essas informações de modo diferente. Além do mais, SysInfo
oferece as técnicas para obter informações sobre recursos de memória livres, informação sobre a versão
do Windows, configurações de variáveis de ambiente e uma lista de módulos carregados. Você não ape-
nas aprenderá a usar essas funções práticas da API, mas também descobrirá como integrar essas informa-
ções em uma interface com o usuário funcional e esteticamente agradável. Além do mais, você descobrirá
quais as funções da API do Windows 3.x foram substituídas pelas funções do Win32 deste capítulo.
Existem vários motivos para você querer obter tais informações do Windows. Naturalmente, o hacker
em cada um de nós argumentaria que a melhor recompensa é poder bisbilhotar o interior do sistema opera-
cional, como algum tipo de cyber-voyeur. Talvez você esteja escrevendo um programa que precise acessar va-
riáveis de ambiente para poder encontrar certos arquivos. Talvez você precise determinar quais módulos es-
tão carregados a fim de remover manualmente os módulos da memória. Possivelmente, você precisa criar um
capítulo sensacional para um livro que esteja escrevendo. Pois é, há muitos motivos válidos!
O conteúdo do formulário é bastante simples. Ele contém um THeaderListbox (um componente per-
sonalizado explicado no Capítulo 21) e um TButton. Para refrescar sua memória, o controle THeaderListbox
é uma combinação de um controle THeader e um controle TListBox. Quando as seções do cabeçalho são di-
mensionadas, o conteúdo da caixa de listagem também será dimensionado de acordo. O controle Thea-
derListbox, chamado InfoLB, apresenta as informações mencionadas anteriormente. O botão faz o formu-
lário fechar.
Formatando as strings
Essa aplicação faz bastante uso da função Format( ) para formatar strings predefinidas com dados recupera-
dos do SO em runtime. As strings que serão usadas são definidas em uma seção const na unidade principal:
const
{ Strings de status da memória }
SMemUse = ‘Memory in useq%d%%’;
STotMem = ‘Total physical memoryq$%.8x bytes’;
386 SFreeMem = ‘Free physical memoryq$%.8x bytes’;
STotPage = ‘Total page file memoryq$%.8x bytes’;
SFreePage = ‘Free page file memoryq$%.8x bytes’;
STotVirt = ‘Total virtual memoryq$%.8x bytes’;
SFreeVirt = ‘Free virtual memoryq$%.8x bytes’;
{ Strings de informação da versão do OS }
SOSVer = ‘OS Versionq%d.%d’;
SBuildNo = ‘Build Numberq%d’;
SOSPlat = ‘Platformq%s’;
SOSWin32s = ‘Windows 3.1x running Win32s’;
SOSWin95 = ‘Windows 95/98’;
SOSWinNT = ‘Windows NT/2000’;
{ Strings de informações do sistema }
SProc = ‘Processor Arhitectureq%s’;
SPIntel = ‘Intel’;
SPageSize = ‘Page Sizeq$%.8x bytes’;
SMinAddr = ‘Minimum Application Addressq$%p’;
SMaxAddr = ‘Maximum Application Addressq$%p’;
SNumProcs = ‘Number of Processorsq%d’;
SAllocGra = ‘Allocation Granularityq$%.8x bytes’;
SProcLevl = ‘Processor Levelq%s’;
SIntel3 = ‘80386’;
SIntel4 = ‘80486’;
SIntel5 = ‘Pentium’;
SIntel6 = ‘Pentium Pro’;
SProcRev = ‘Processor Revisionq%.4x’;
{ Strings de diretório }
SWinDir = ‘Windows directoryq%s’;
SSysDir = ‘Windows system directoryq%s’;
SCurDir = ‘Current directoryq%s’;
Você provavelmente está perguntando por que aparece um “q” no meio de cada uma das strings. Ao
exibir essas strings, a propriedade DelimChar de InfoLB é definida como q, o que significa que o componente
InfoLB assume que o caracter q define o delimitador entre cada coluna na caixa de listagem.
Existem três motivos principais para se usar Format( ) com strings predefinidas, em vez de formatar
literais de string individualmente:
l Já que Format( ) aceita vários tipos de parâmetros, não é preciso obscurecer seu código com um
punhado de chamadas variadas para funções (como IntToStr( ) e IntToHex( )), que formatam di-
ferentes tipos de parâmetros para exibição.
l Format( ) trata com facilidade de vários tipos de dados. Nesse caso, usamos strings de formato %s
e %s para formatar dados de string e dados numéricos, e por isso o método é mais flexível.
l Ao manter as strings em um local separado, fica mais fácil localizar, inserir e alterar strings, se for
preciso. A manutenção também é facilitada.
NOTA
Use um sinal de porcentagem duplo (%%) para apresentar um símbolo de porcentagem em uma string for-
matada.
l O primeiro campo desse registro, dwLength, descreve o tamanho do registro TMemoryStatus. Você
deve inicializar esse valor como SizeOf(TMemoryStatus) antes de chamar GlobalMemoryStatus( ). Isso
permite que o Windows mude o tamanho desse registro em versões futuras, pois ele poderá dife-
renciar as versões com base no valor do primeiro campo.
l dwMemoryLoad fornece um número de 0 até 100, que dará uma idéia geral do uso da memória. 0 sig-
nifica que nenhuma memória está sendo usada, e 100 significa que toda a memória está em uso.
l dwTotalPhys indica o número total de bytes de memória física (a quantidade de RAM instalada no
computador), e dwAvailPhys indica o quanto desse total está atualmente sem uso.
l dwTotalPageFile indica o número total de bytes que podem ser armazenados em arquivo(s) de pa-
ginação do disco rígido. Esse número não é o mesmo que o tamanho do arquivo de paginação no
disco. dwAvailPageFile indica o quanto desse total está disponível.
l dwTotalVirtual indica o número total de bytes de memória virtual utilizável no processo de cha-
mada. dwAvailVirtual indica o quanto dessa memória está disponível para o processo de chamada.
O código a seguir obtém o status da memória e preenche a caixa de listagem com informações de
status:
procedure TInfoForm.ShowMemStatus;
var
MS: TMemoryStatus;
begin
InfoLB.DelimChar := ‘q’;
MS.dwLength := SizeOf(MS);
GlobalMemoryStatus(MS);
with InfoLB.Items, MS do
begin
Clear;
Add(Format(SMemUse, [dwMemoryLoad]));
Add(Format(STotMem, [dwTotalPhys]));
Add(Format(SFreeMem, [dwAvailPhys]));
Add(Format(STotPage, [dwTotalPageFile]));
Add(Format(SFreePage, [dwAvailPageFile]));
Add(Format(STotVirt, [dwTotalVirtual]));
Add(Format(SFreeVirt, [dwAvailVirtual]));
end;
InfoLB.Sections[0].Text := ‘Resource’;
InfoLB.Sections[1].Text := ‘Amount’;
Caption:= ‘Memory Status’;
end;
388
ATENÇÃO
Não se esqueça de inicializar o campo dwLength da estrutura TMemoryStatus antes de chamar GlobalMemory-
Status( ).
type
TOSVersionInfo = record
dwOSVersionInfoSize: DWORD;
dwMajorVersion: DWORD;
dwMinorVersion: DWORD;
dwBuildNumber: DWORD;
dwPlatformId: DWORD;
szCSDVersion: array[0..126] of AnsiChar; {String de manutenção para uso do PSS}
end;
l O campo dwOSVersionInfoSize deve ser inicializado como SizeOf(TOSVersionInfo) antes de chamar
GetVersionEx( ).
l indica o número de versão principal do OS. Em outras palavras, se o número de
dwMajorVersion
versão do OS for 4.0, o valor desse campo será 4.
l dwMinorVersion indica o número de versão secundário do OS. Em outras palavras, se o número de
versão do OS for 4.0, o valor desse campo será 0.
l dwBuildNumber contém o número de montagem do OS em sua palavra de baixa ordem.
l dwPlatformId descreve a plataforma Win32 atual. Esse parâmetro pode ter qualquer um dos valo-
res da tabela a seguir:
Valor Plataforma
l szCSDVersion contém informações adicionais arbitrárias sobre o OS. Esse valor normalmente é
uma string vazia.
389
O procedimento a seguir preenche InfoLB com as informações de versão do OS:
procedure TInfoForm.GetOSVerInfo;
var
VI: TOSVersionInfo;
begin
VI.dwOSVersionInfoSize := SizeOf(VI);
GetVersionEx(VI);
with InfoLB.Items, VI do
begin
Clear;
Add(Format(SOSVer, [dwMajorVersion, dwMinorVersion]));
Add(Format(SBuildNo, [LoWord(dwBuildNumber)]));
case dwPlatformID of
VER_PLATFORM_WIN32S: Add(Format(SOSPlat, [SOSWin32s]));
VER_PLATFORM_WIN32_WINDOWS: Add(Format(SOSPlat, [SOSWin95]));
VER_PLATFORM_WIN32_NT: Add(Format(SOSPlat, [SOSWinNT]));
end;
end;
end;
NOTA
No Windows 3.x, a função GetVersion( ) obtinha informações de versão semelhantes. Como agora você
está no mundo do Win32, precisa usar a função GetVersionEx( ); ela oferece informações mais detalha-
das do que GetVersion( ).
Naturalmente, sob o Windows 95/98, somente os valores PROCESSOR_INTEL_x são possíveis, en-
quanto todos são válidos sob o Windows NT.
l dwAllocationGranularity retorna a granularidade de alocação na qual a memória será alocada. Nas
implementações anteriores do Win32, esse valor era fixado como 64KB. No entanto, é possível
que outras arquiteturas de hardware exijam valores diferentes.
l O campo wProcessorLevel especifica o nível de dependência do processador na arquitetura do sis-
tema. Esse campo pode conter diversos valores para diferentes processadores. Para os processa-
dores da Intel, esse parâmetro pode ter qualquer um dos valores da tabela a seguir:
Valor Significado
3 Processador é um 80386
4 Processador é um 80486
5 Processador é um Pentium
6 Processador é um Pentium Pro ou superior
NOTA
A função GetSystemInfo( ) efetivamente substitui a função GetWinFlags( ) da API do Windows 3.x.
A Figura 14.3 mostra InfoForm exibindo, em runtime, as informações do sistema, incluindo a versão
do sistema operacional e as informações de diretório.
Verificando o ambiente
A obtenção da lista de variáveis de ambiente – coisas como conjuntos, caminho e prompt – para o proces-
so atual é uma tarefa fácil, graças à função das API GetEnvironmentStrings( ). Essa função não usa parâme-
tros e retorna uma lista de strings de ambiente separada por nulo. O formato dessa lista é uma string, se-
guida por um nulo, seguido por uma string, seguida por um nulo e assim por diante, até que a string intei-
ra seja terminada com um nulo duplo (#0#0). A função a seguir é usada na aplicação SysInfo para apanhar a
saída da função GetEnvironmentStrings( ) e colocá-la em InfoLB:
procedure TInfoForm.ShowEnvironment;
var
EnvPtr, SavePtr: PChar;
begin
InfoLB.DelimChar := ‘=’;
EnvPtr := GetEnvironmentStrings;
SavePtr := EnvPtr;
InfoLB.Items.Clear;
repeat
InfoLB.Items.Add(StrPas(EnvPtr));
inc(EnvPtr, StrLen(EnvPtr) + 1);
until EnvPtr^ = #0;
FreeEnvironmentStrings(SavePtr);
InfoLB.Sections[0].Text := ‘Environment Variable’;
InfoLB.Sections[1].Text := ‘Value’;
Caption:= ‘Current Environment’;
end; 393
NOTA
O método ShowEnvironment( ) aproveita a capacidade do Object Pascal de realizar aritmética de ponteiro
sobre strings do tipo PChar. Observe como são necessárias poucas linhas de código para atravessar a lista
de strings de ambiente.
Podemos agora fazer alguns comentários sobre esse método. Primeiro, observe que a propriedade
DelimChar de InfoLB é inicialmente definida como ‘=’. Como cada um dos pares de variável de valor de am-
biente já está separado por esse caracter, é muito fácil exibi-los corretamente em InfoLB. Além disso,
quando você acabar de usar as strings de ambiente, deve chamar a função FreeEnvironmentStrings( ) para
liberar o bloco alocado.
DICA
Você não pode obter ou definir variáveis de ambiente individuais com a função GetEnvironmentStrings( ).
Para obter e definir variáveis de ambiente individuais, consulte as funções GetEnvironmentVariable( ) e
SetEnvironmentVariable( ) no sistema de ajuda da API do Win32.
unit InfoU;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
HeadList, StdCtrls, ExtCtrls, SysMain;
type
TInfoVariety = (ivMemory, ivSystem, ivEnvironment);
TInfoForm = class(TForm)
InfoLB: THeaderListbox;
Panel1: TPanel;
OkBtn: TButton;
private
394
Listagem 14.1 Continuação
procedure GetSysInfo;
procedure GetDirInfo;
public
procedure ShowMemStatus;
procedure ShowSysInfo;
procedure ShowEnvironment;
end;
implementation
{$R *.DFM}
const
{ Strings de status da memória }
SMemUse = ‘Memory in useq%d%%’;
STotMem = ‘Total physical memoryq$%.8x bytes’;
SFreeMem = ‘Free physical memoryq$%.8x bytes’;
STotPage = ‘Total page file memoryq$%.8x bytes’;
SFreePage = ‘Free page file memoryq$%.8x bytes’;
STotVirt = ‘Total virtual memoryq$%.8x bytes’;
SFreeVirt = ‘Free virtual memoryq$%.8x bytes’;
{ Strings de diretório }
SWinDir = ‘Windows directoryq%s’;
SSysDir = ‘Windows system directoryq%s’;
SCurDir = ‘Current directoryq%s’;
procedure TInfoForm.ShowMemStatus;
var
MS: TMemoryStatus;
begin
InfoLB.DelimChar := ‘q’;
MS.dwLength := SizeOf(MS);
GlobalMemoryStatus(MS);
with InfoLB.Items, MS do
begin
Clear;
Add(Format(SMemUse, [dwMemoryLoad]));
Add(Format(STotMem, [dwTotalPhys]));
Add(Format(SFreeMem, [dwAvailPhys]));
Add(Format(STotPage, [dwTotalPageFile]));
Add(Format(SFreePage, [dwAvailPageFile]));
Add(Format(STotVirt, [dwTotalVirtual]));
Add(Format(SFreeVirt, [dwAvailVirtual]));
end;
InfoLB.Sections[0].Text := ‘Resource’;
InfoLB.Sections[1].Text := ‘Amount’;
Caption:= ‘Memory Status’;
end;
procedure TInfoForm.GetOSVerInfo;
var
VI: TOSVersionInfo;
begin
VI.dwOSVersionInfoSize := SizeOf(VI);
GetVersionEx(VI);
with InfoLB.Items, VI do
begin
Clear;
Add(Format(SOSVer, [dwMajorVersion, dwMinorVersion]));
Add(Format(SBuildNo, [LoWord(dwBuildNumber)]));
case dwPlatformID of
VER_PLATFORM_WIN32S: Add(Format(SOSPlat, [SOSWin32s]));
VER_PLATFORM_WIN32_WINDOWS: Add(Format(SOSPlat, [SOSWin95]));
VER_PLATFORM_WIN32_NT: Add(Format(SOSPlat, [SOSWinNT]));
396
Listagem 14.1 Continuação
end;
end;
end;
procedure TInfoForm.GetSysInfo;
var
SI: TSystemInfo;
begin
GetSystemInfo(SI);
with InfoLB.Items, SI do
begin
Add(Format(SProc, [SPIntel]));
Add(Format(SPageSize, [dwPageSize]));
Add(Format(SMinAddr, [lpMinimumApplicationAddress]));
Add(Format(SMaxAddr, [lpMaximumApplicationAddress]));
Add(Format(SNumProcs, [dwNumberOfProcessors]));
Add(Format(SAllocGra, [dwAllocationGranularity]));
case wProcessorLevel of
3: Add(Format(SProcLevl, [SIntel3]));
4: Add(Format(SProcLevl, [SIntel4]));
5: Add(Format(SProcLevl, [SIntel5]));
6: Add(Format(SProcLevl, [SIntel6]));
else Add(Format(SProcLevl, [IntToStr(wProcessorLevel)]));
end;
end;
end;
procedure TInfoForm.GetDirInfo;
var
S: array[0..MAX_PATH] of char;
begin
{ Apanha diretório do Windows }
GetWindowsDirectory(S, SizeOf(S));
InfoLB.Items.Add(Format(SWinDir, [S]));
{ Apanha diretório do sistema do Windows }
GetSystemDirectory(S, SizeOf(S));
InfoLB.Items.Add(Format(SSysDir, [S]));
{ Apanha diretório atual para o processo ativo }
GetCurrentDirectory(SizeOf(S), S);
InfoLB.Items.Add(Format(SCurDir, [S]));
end;
procedure TInfoForm.ShowSysInfo;
begin
InfoLB.DelimChar := ‘q’;
GetOSVerInfo;
GetSysInfo;
GetDirInfo;
InfoLB.Sections[0].Text := ‘Item’;
InfoLB.Sections[1].Text := ‘Value’;
Caption:= ‘System Information’;
end;
397
Listagem 14.1 Continuação
procedure TInfoForm.ShowEnvironment;
var
EnvPtr, SavePtr: PChar;
begin
InfoLB.DelimChar := ‘=’;
EnvPtr := GetEnvironmentStrings;
SavePtr := EnvPtr;
InfoLB.Items.Clear;
repeat
InfoLB.Items.Add(StrPas(EnvPtr));
inc(EnvPtr, StrLen(EnvPtr) + 1);
until EnvPtr^ = #0;
FreeEnvironmentStrings(SavePtr);
InfoLB.Sections[0].Text := ‘Environment Variable’;
InfoLB.Sections[1].Text := ‘Value’;
Caption:= ‘Current Environment’;
end;
end.
type
IWin32Info = interface
procedure FillProcessInfoList(ListView: TListView; ImageList: TImageList);
procedure ShowProcessProperties(Cookie: Pointer);
end;
398
Windows 95/98: usando ToolHelp32
ToolHelp32 é uma coleção de funções e procedimentos, parte da API do Win32, que permite ver o status
de parte das operações de baixo nível do sistema operacional. Em particular, as funções permitem obter
informações sobre todos os processos atualmente em execução no sistema e os threads, módulos e heaps
que pertencem a cada um dos processos. Como você poderia imaginar, a maior parte das informações
obtidas pelo ToolHelp32 é usada principalmente por aplicações que precisam olhar para “dentro” do
OS, como depuradores, embora o exame dessas funções dê até mesmo ao programador mediano uma
idéia melhor de como o Win32 é formado.
NOTA
A API ToolHelp32 está disponível apenas na implementação Windows 95/98 do Win32. Esse tipo de funcio-
nalidade violaria os poderosos recursos de proteção e segurança de processos do NT. Portanto, as aplica-
ções que usam as funções de ToolHelp32 só funcionarão sob o Windows 95/98, e não sob o Windows NT.
Dissemos ToolHelp32 para diferenciá-la da versão de 16 bits do ToolHelp que foi incluída no Win-
dows 3.1x. A maior parte das funções na versão anterior do ToolHelp não se aplica mais ao Win32, e
portanto não são mais aceitas. Além disso, sob o Windows 3.1x, as funções de ToolHelp eram localiza-
das fisicamente em uma DLL chamada TOOLHELP.DLL, enquanto as funções de ToolHelp32 residem no ker-
nel sob o Win32.
As definições de tipos e funções do ToolHelp32 estão localizadas na unidade TlHelp32, e por isso ela
deve ser incluída na sua cláusula uses quando estiver trabalhando com essas funções. Para garantir que
você terá uma visão geral sólida, a aplicação que você montará neste capítulo utiliza cada função definida
na unidade TlHelp32.
A Figura 14.5 mostra o formulário principal para SysInfo. A interface com o usuário consiste prin-
cipalmente em TheaderListbox, um controle personalizado explicado com detalhes no Capítulo 11. A lista
contém informações importantes para um determinado processo. Com um clique duplo em um processo
na lista, você pode obter informações mais detalhadas sobre ele. Esses detalhes são mostrados em um
formulário filho, semelhante ao formulário principal.
Snapshots
Devido à natureza de multitarefa do ambiente Win32, objetos como processos, threads, módulos e heaps
estão sendo constantemente criados, destruídos e modificados. Como o status da máquina está constan-
temente em um estado de fluxo, as informações do sistema que poderiam ser significativas agora podem
não ter significado daqui a um segundo. Por exemplo, suponha que você queira escrever um programa
para enumerar todos os módulos carregados em nível de sistema. Como o sistema operacional poderia
pedir a execução de threads do seu programa a qualquer momento para fornecer tempo para outros thre-
ads no sistema, os módulos teoricamente podem ser criados e destruídos até mesmo enquanto você os
enumera. 399
Nesse ambiente dinâmico, faria mais sentido se você pudesse congelar o sistema no tempo por um
instante a fim de obter tais informações do sistema. Embora ToolHelp32 não ofereça um meio de conge-
lar o sistema no tempo, ele oferece uma função que permite apanhar um snapshot (ou instantâneo) do
sistema em um determinado momento. Essa função é CreateToolhelp32Snapshot( ), e ela é declarada da se-
guinte maneira:
function CreateToolhelp32Snapshot(dwFlags, th32ProcessID: DWORD): THandle;
stdcall;
l O parâmetro dwFlags indica que tipo de informação deve ser incluído no snapshot. Esse parâme-
tro pode ter qualquer um dos valores mostrados na tabela a seguir:
Valor Significado
l O parâmetro th32ProcessID identifica o processo para o qual você deseja obter informações. Passe
0 nesse parâmetro para indicar o processo atual. Esse parâmetro afeta apenas as listas de módulo
e heap, pois são específicas do processo. As listas de processo e thread fornecidas por Tool-
Help32 referem-se a todo o sistema.
l A função CreateToolhelp32Snapshot( ) retorna a alça para um snapshot ou -1 no caso de um erro. A
alça retornada funciona como outras alças do Win32 com relação aos processos e threads para
os quais são válidas.
O código a seguir cria uma alça de snapshot que contém informações sobre todos os processos atu-
almente carregados em nível de sistema (EToolHelpError é uma exceção definida pelo programador):
var
Snap: THandle;
begin
Snap := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if Snap = -1 then
raise EToolHelpError.Create(‘CreateToolHelp32Snapshot failed’);
end;
NOTA
Quando você terminar de usar a alça, use a função CloseHandle( ) da API do Win32 para liberar os recur-
sos associados a uma alça criada por CreateToolHelp32Snapshot( ).
Percorrendo os processos
Dada uma alça de snapshot que inclua informações sobre processos, ToolHelp32 define duas funções
que lhe oferecem a capacidade de enumerar pelos (percorrer) processos. As funções, Process32First( ) e
400 Process32Next( ), são declaradas da seguinte forma:
function Process32First(hSnapshot: THandle;
var lppe: TProcessEntry32): BOOL; stdcall;
function Process32Next(hSnapshot: THandle;
var lppe: TProcessEntry32): BOOL; stdcall;
O primeiro parâmetro dessas funções, hSnapshot, é a alça do snapshot retornada por CreateTool-
Help32Snapshot( ).
O segundo parâmetro, lppe, é um registro TProcessEntry32 que é passado por referência. À medida
que você percorre a enumeração, as funções preencherão esse registro com informações sobre o processo
seguinte. O registro TProcessEntry32 é definido da seguinte maneira:
type
TProcessEntry32 = record
dwSize: DWORD;
cntUsage: DWORD;
th32ProcessID: DWORD;
th32DefaultHeapID: DWORD;
th32ModuleID: DWORD;
cntThreads: DWORD;
th32ParentProcessID: DWORD;
pcPriClassBase: Longint;
dwFlags: DWORD;
szExeFile: array[0..MAX_PATH - 1] of Char;
end;
l O campo dwSize contém o tamanho do registro TProcessEntry32. Esse deve ser inicializado como
SizeOf(TProcessEntry32) antes de usar o registro.
A Figura 14.6 mostra esse código em ação, exibindo informações sobre processos em uma máquina
Windows 98.
O código que obtém um ícone para cada processo não pode ser ignorado. A exibição do ícone junto
com o nome da aplicação dá à aplicação um toque mais profissional e uma aparência mais semelhante à do
Windows. A função da API ExtractIcon( ) da unidade ShellAPI tenta extrair o ícone do arquivo da aplicação.
Se ExtractIcon( ) falhar, HWinIcon será usado em seu lugar. HWinIcon é o ícone default do Windows, e foi
pré-carregado no manipulador de evento OnCreate desse formulário usando a função LoadImage( ) da API:
FWinIcon := LoadImage(0, IDI_WINLOGO, IMAGE_ICON, LR_DEFAULTSIZE,
LR_DEFAULTSIZE, LR_DEFAULTSIZE or LR_DEFAULTCOLOR or LR_SHARED);
Quando o usuário dá um clique duplo em um dos processos no formulário principal (consulte a Fi-
gura 14.6), o método ShowProcessProperties( ) de IWin32Info é chamado, e a implementação desse método
passa o parâmetro adiante para um método incluído na unidade Detail9x, chamado ShowProcessDetails( ):
procedure TWin95Info.ShowProcessProperties(Cookie: Pointer);
begin
ShowProcessDetails(PProcessEntry32(Cookie));
end;
O objeto TDetailForm apresenta apenas uma lista de cada vez. Um tipo enumerado registra o signifi-
cado de cada lista:
type
TListType = (ltThread, ltModule, ltHeap); 403
TDetailForm também mantém três componentes TStringList separados para cada um dos threads, mó-
dulos e heaps. Essas listas são definidas como parte de um array chamado DetailLists:
DetailLists: array[TListType] of TStringList;
Percorrendo os threads
Para percorrer a lista de threads de um processo, ToolHelp32 oferece duas funções semelhantes às utili-
zadas para percorrer os processos: Thread32First( ) e Thread32Next( ). Essas funções são declaradas da se-
guinte maneira:
function Thread32First(hSnapshot: THandle;
var lpte: TThreadEntry32): BOOL; stdcall;
Além do parâmetro hSnapshot normal, essas funções também aceitam um parâmetro por referência
do tipo TThreadEntry32. Quanto às funções do processo, a função de chamada preenche esse registro. O re-
gistro TThreadEntry32 é definido da seguinte maneira:
type
TThreadEntry32 = record
dwSize: DWORD;
cntUsage: DWORD;
th32ThreadID: DWORD;
th32OwnerProcessID: DWORD;
tpBasePri: Longint;
tpDeltaPri: Longint;
dwFlags: DWORD;
end;
l dwSize é o tamanho do registro, e deve ser inicializado como SizeOf(TThreadEntry32) antes que o re-
gistro seja usado.
l cntUsage é o contador de referência do thread. Quando esse valor atinge zero, o thread é descar-
regado pelo sistema operacional.
l th32ThreadIDé o número de identificação do thread. Esse valor possui significado apenas dentro
das funções de ToolHelp32.
l é o identificador do processo que possui esse thread. Esse ID pode ser usado
th32OwnerProcessID
com outras funções do Win32.
l tpBasePri é a classe de prioridade básica do thread. Esse valor é igual para todos os threads de um
determinado processo. Os valores possíveis para esse campo normalmente estão na faixa de 4 a
24. A tabela a seguir relaciona o significado de cada valor:
Valor Significado
4 Ociosa
8 Normal
13 Alta
24 Tempo real
l tpDeltaPrié a prioridade delta (mudança na prioridade) de tpBasePri. Ele é um número positivo
ou negativo que, quando combinado com a classe de prioridade básica, revela a prioridade geral
404 do thread. A tabela a seguir mostra as constantes definidas para cada valor possível:
Constante Valor
THREAD_PRIORITY_IDLE -15
THREAD_PRIORITY_LOWEST -2
THREAD_PRIORITY_BELOW_NORMAL -1
THREAD_PRIORITY_NORMAL 0
THREAD_PRIORITY_ABOVE_NORMAL 1
THREAD_PRIORITY_HIGHEST 2
THREAD_PRIORITY_TIME_CRITICAL 15
NOTA
A linha de código a seguir, do método WalkThreads( ), é importante porque as listas de threads de Tool-
Help32 não são específicas ao processo:
Portanto, você precisa fazer uma comparação manual ao analisar os threads para determinar quais thre-
ads estão associados ao processo em questão.
405
FIGURA 14.7 Exibindo threads no formulário de detalhe sob o Windows 98.
Percorrendo os módulos
O trabalho de percorrer os módulos é muito semelhante ao que já vimos para processos e threads. Tool-
Help32 oferece duas funções que realizam esse trabalho: Module32First( ) e Module32Next( ). Essas funções
são declaradas da seguinte maneira:
function Module32First(hSnapshot: THandle;
var lpme: TModuleEntry32): BOOL; stdcall;
Novamente, a alça do snapshot é o primeiro parâmetro das funções. O segundo parâmetro var, lpme,
é um registro TModuleEntry32. Esse registro é definido da seguinte forma:
type
TModuleEntry32 = record
dwSize: DWORD;
th32ModuleID: DWORD;
th32ProcessID: DWORD;
GlblcntUsage: DWORD;
ProccntUsage: DWORD;
modBaseAddr: PBYTE;
modBaseSize: DWORD;
hModule: HMODULE;
szModule: array[0..MAX_MODULE_NAME32 + 1] of Char;
szExePath: array[0..MAX_PATH - 1] of Char;
end;
l dwSize é o tamanho do registro, e deve ser inicializado como SizeOf(TModuleEntry32) antes que o re-
gistro seja usado.
l é o identificador do módulo. Esse valor possui significado apenas com funções de
th32ModuleID
ToolHelp32.
l th32ProcessID é o identificador do processo sendo examinado. Esse valor pode ser usado com ou-
tras funções do Win32.
l GlblcntUsage é o contador de referência global do módulo.
l ProccntUsage é o contador de referência do módulo dentro do contexto do processo que o possui.
l modBaseAddr é o endereço básico do módulo na memória. Esse valor só é válido dentro do contex-
to de th32ProcessID.
406
l modBaseSize é o tamanho do módulo na memória em bytes.
l hModule é a alça do módulo. Esse valor é válido apenas dentro do contexto de th32ProcessID.
l szModule é uma string terminada em nulo, contendo o nome do módulo.
l szExepath é uma string terminada em nulo, contendo o nome do caminho completo do módulo.
O método WalkModules( ) de TDetailForm é muito semelhante ao seu método WalkThreads( ). Como ve-
mos no código a seguir, esse método atravessa a lista de módulos e o inclui na parte da lista de módulos
do array DetailLists:
procedure TWin95DetailForm.WalkModules;
{ Usa funções de ToolHelp32 para percorrer lista de módulos }
var
M: TModuleEntry32;
begin
DetailLists[ltModule].Clear;
M.dwSize := SizeOf(M);
if Module32First(FCurSnap, M) then
repeat
DetailLists[ltModule].Add(Format(SModuleStr, [M.szModule, M.ModBaseAddr,
M.ModBaseSize, M.ProcCntUsage]));
until not Module32Next(FCurSnap, M);
end;
Percorrendo os heaps
Percorrer os heaps é ligeiramente mais complicado do que os outros tipos de enumeração que você
aprendeu neste capítulo. O ToolHelp32 oferece quatro funções que permitem percorrer os heaps. As
duas primeiras funções, Heap32ListFirst( ) e Heap32ListNext( ), permitem repetir por cada um dos heaps
de um processo. As duas outras funções, Heap32First( ) e Heap32Next( ), permitem que você obtenha infor-
mações mais detalhadas sobre todos os blocos dentro de um heap individual.
Heap32ListFirst( ) e Heap32ListNext( ) são definidos da seguinte forma:
l dwSize é o tamanho do registro, e deve ser inicializado como SizeOf(THeapList32) antes que o regis-
tro seja usado.
l th32ProcessID é o identificador do processo possuidor do heap.
l th32HeapID é o identificador do heap. Esse valor possui significado apenas para o processo especi-
ficado e dentro de ToolHelp32.
l dwFlags contém um flag que determina o tipo de heap. O valor desse campo pode ser HF32_DEFAULT,
que significa que o heap atual é o heap default do processo, ou HF32_SHARED, que significa que o
heap atual é um heap compartilhado normal.
As funções Heap32First( ) e Heap32Next( ) são definidas da seguinte forma:
function Heap32First(var lphe: THeapEntry32; th32ProcessID,
th32HeapID: DWORD): BOOL; stdcall;
Observe que as listas de parâmetros dessas funções são diferentes das funções de enumeração de lis-
ta de processos, threads, módulos e heaps a respeito das quais você aprendeu neste capítulo. Essas fun-
ções foram criadas para enumerar os blocos de um determinado heap em um determinado processo, ao
em vez enumerar algumas propriedades de apenas um processo. Ao chamar Heap32First( ), os parâmetros
th32ProcessID e th32HeapID devem ser configurados para os valores do campo do mesmo nome do registro
THeapList32 preenchido por Heap32ListFirst( ) ou Heap32ListNext( ). O parâmetro lphe var de Heap32First( )
e Heap32Next( ) é do tipo THeapEntry32. Esse registro contém informações descritivas pertencentes ao bloco
do heap e é definido da seguinte forma:
type
THeapEntry32 = record
dwSize: DWORD;
hHandle: THandle; // Alça do bloco de heap
dwAddress: DWORD; // Endereço linear do início do bloco
dwBlockSize: DWORD; // Tamanho do bloco em bytes
dwFlags: DWORD;
dwLockCount: DWORD;
dwResvd: DWORD;
th32ProcessID: DWORD; // processo possuidor
th32HeapID: DWORD; // bloco de heap está em
end;
l dwSize é o tamanho do registro, e deve ser inicializado como SizeOf(THeapEntry32) antes que o re-
gistro seja usado.
l hHandle é a alça do bloco de heap.
l dwAddress é o endereço linear do início do bloco de heap.
l dwBlockSize é o tamanho, em bytes, desse bloco de heap.
l dwFlagsdescreve o tipo de bloco de heap. Esse campo pode ter qualquer um dos valores mostra-
dos na tabela a seguir:
408
Valor Significado
A Figura 14.9 mostra o formulário de detalhes com a lista de blocos de heap visível.
409
FIGURA 14.9 Exibindo blocos de heap das janelas no formulário de detalhes sob o Windows 98.
Exibição de heaps
Até este ponto, você aprendeu sobre cada função da API ToolHelp32, exceto uma: ToolHelp32ReadProcess-
Memory( ).Para garantir que você terminará este capítulo com um sentimento de totalidade, também ex-
plicaremos sobre essa função.
ToolHelp32ReadProcessMemory( ) é declarada desta maneira:
function Toolhelp32ReadProcessMemory(th32ProcessID: DWORD;
Esse método primeiro verifica se a página de guia atual é a página de lista de heap. Se for, aloca um
buffer temporário e o passa para a função ToolHelp32ReadProcessMemory( ) para ser preenchido. Quando
o buffer for preenchido, ele é apresentado no controle HeapView de TMemView, e HeapViewForm aparece modal-
mente. Quando o formulário retornar da chamada a ShowModal( ), o buffer é liberado. A Figura 14.10
mostra uma exibição de heap em ação.
O fonte
As Listagens 14.2 e 14.3 mostram o código-fonte completo para as unidades W9xInfo.pas e Detail9x.pas,
respectivamente.
411
Listagem 14.2 W9xInfo.pas, obtendo informações sobre o processo no Windows 95/98
unit W9xInfo;
interface
type
TWin9xInfo = class(TInterfacedObject, IWin32Info)
private
FProcList: TList;
FWinIcon: HICON;
FSnap: THandle;
procedure Refresh;
public
constructor Create;
destructor Destroy; override;
procedure FillProcessInfoList(ListView: TListView; ImageList: TImageList);
procedure ShowProcessProperties(Cookie: Pointer);
end;
implementation
const
ProcessInfoCaptions: array[0..3] of string = (
‘ProcessName’, ‘Threads’, ‘ID’, ‘ParentID’);
{ TProcList }
type
TProcList = class(TList)
procedure Clear; override;
end;
procedure TProcList.Clear;
var
I: Integer;
begin
for I := 0 to Count - 1 do Dispose(PProcessEntry32(Items[I]));
inherited Clear;
end;
{ TWin95Info }
constructor TWin9xInfo.Create;
begin
FProcList := TProcList.Create;
FWinIcon := LoadImage(0, IDI_WINLOGO, IMAGE_ICON, LR_DEFAULTSIZE,
LR_DEFAULTSIZE, LR_DEFAULTSIZE or LR_DEFAULTCOLOR or LR_SHARED);
end;
destructor TWin9xInfo.Destroy;
412
Listagem 14.2 Continuação
begin
DestroyIcon(FWinIcon);
if FSnap > 0 then CloseHandle(FSnap);
FProcList.Free;
inherited Destroy;
end;
procedure TWin9xInfo.Refresh;
var
413
Listagem 14.2 Continuação
PE: TProcessEntry32;
PPE: PProcessEntry32;
begin
FProcList.Clear;
if FSnap > 0 then CloseHandle(FSnap);
FSnap := CreateToolHelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if FSnap = INVALID_HANDLE_VALUE then
raise Exception.Create(‘CreateToolHelp32Snapshot failed’);
PE.dwSize := SizeOf(PE);
if Process32First(FSnap, PE) then // apanha processo
repeat
New(PPE); // cria novo PPE
PPE^ := PE; // preenche
FProcList.Add(PPE); // inclui na lista
until not Process32Next(FSnap, PE); // apanha processo seguinte
end;
end.
unit Detail9x;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, ComCtrls, HeadList, TlHelp32, Menus, SysMain, DetBase;
type
TListType = (ltThread, ltModule, ltHeap);
TWin9xDetailForm = class(TBaseDetailForm)
procedure DetailTabsChange(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure DetailLBDblClick(Sender: TObject);
private
FCurSnap: THandle;
FCurProc: TProcessEntry32;
DetailLists: array[TListType] of TStringList;
ProcMem: PByte;
HeapListAlloc: Boolean;
procedure FreeHeapList;
procedure ShowList(ListType: TListType);
procedure WalkThreads;
procedure WalkHeaps;
414 procedure WalkModules;
Listagem 14.3 Continuação
public
procedure NewProcess(P: PProcessEntry32);
end;
implementation
{$R *.DFM}
uses ProcMem;
const
{ Array de strings que entra no cabeçalho de cada lista respectiva. }
HeaderStrs: array[TListType] of TDetailStrings = (
(‘Thread ID’, ‘Base Priority’, ‘Delta Priority’, ‘Usage Count’),
(‘Module’, ‘Base Addr’, ‘Size’, ‘Usage Count’),
(‘Heap ID’, ‘Base Addr’, ‘Size’, ‘Flags’));
procedure TWin9xDetailForm.WalkThreads;
{ Usa funções de ToolHelp32 para percorrer lista de threads }
var
T: TThreadEntry32;
begin
DetailLists[ltThread].Clear;
T.dwSize := SizeOf(T);
if Thread32First(FCurSnap, T) then
repeat
{ Certifica que o thread é do processo atual }
if T.th32OwnerProcessID = FCurProc.th32ProcessID then
DetailLists[ltThread].Add(Format(SThreadStr, [T.th32ThreadID,
GetClassPriorityString(T.tpBasePri),
GetThreadPriorityString(T.tpDeltaPri), T.cntUsage]));
until not Thread32Next(FCurSnap, T);
end;
procedure TWin9xDetailForm.WalkModules;
{ Usa funções de ToolHelp32 para percorrer lista de módulos }
var
M: TModuleEntry32;
begin
DetailLists[ltModule].Clear;
M.dwSize := SizeOf(M);
if Module32First(FCurSnap, M) then
repeat
DetailLists[ltModule].Add(Format(SModuleStr, [M.szModule, M.ModBaseAddr,
M.ModBaseSize, M.ProcCntUsage]));
until not Module32Next(FCurSnap, M);
end;
procedure TWin9xDetailForm.WalkHeaps;
{ Usa funções de ToolHelp32 para percorrer lista de heaps }
var
HL: THeapList32;
HE: THeapEntry32;
PHE: PHeapEntry32;
begin
DetailLists[ltHeap].Clear;
HL.dwSize := SizeOf(HL);
HE.dwSize := SizeOf(HE);
if Heap32ListFirst(FCurSnap, HL) then
repeat
if Heap32First(HE, HL.th32ProcessID, HL.th32HeapID) then
417
Listagem 14.3 Continuação
repeat
New(PHE); // precisa fazer cópia de registro de THeapList32
PHE^ := HE; // para ter info suficiente para exibir heap mais tarde
DetailLists[ltHeap].AddObject(Format(SHeapStr, [HL.th32HeapID,
Pointer(HE.dwAddress), HE.dwBlockSize,
GetHeapFlagString(HE.dwFlags)]), TObject(PHE));
until not Heap32Next(HE);
until not Heap32ListNext(FCurSnap, HL);
HeapListAlloc := True;
end;
procedure TWin9xDetailForm.FreeHeapList;
{ Como alocações especiais de objetos de PHeapList32 são incluídas }
{ na lista, estas devem ser liberadas. }
var
i: integer;
begin
for i := 0 to DetailLists[ltHeap].Count - 1 do
Dispose(PHeapEntry32(DetailLists[ltHeap].Objects[i]));
end;
end.
419
Windows NT/2000: PSAPI
Como já dissemos, a API ToolHelp32 não existe sob o Windows NT/2000. No entanto, o Windows Plat-
form SDK (kit de desenvolvimento de sistemas para a plataforma Windows) oferece uma DLL chamada
PSAPI.DLL, da qual você pode obter os mesmos tipos de informações de ToolHelp32 sob o Windows
NT/2000, incluindo
l Processos em execução
l Módulos carregados por processo
l Drivers de dispositivo carregados
l Informações de memória do processo
l Arquivos mapeados na memória por processo
Outras versões posteriores do NT e todas as versões do Windows 2000 incluem PSAPI.DLL, embora
você possa redistribuir esse arquivo, se quiser oferecê-lo para usuários de suas aplicações. O Delphi ofe-
rece uma unidade de interface para essa DLL chamada PSAPI.pas, que carrega todas as suas funções dina-
micamente. Portanto, as aplicações que utilizam essa unidade rodarão em máquinas com ou sem
PSAPI.DLL (naturalmente, as funções não funcionarão sem o PSAPI.DLL instalado, mas a aplicação ainda será
executada).
A primeira etapa para obter informações do processo usando PSAPIé chamar EnumProcesses( ), que é
definida da seguinte maneira:
function EnumProcesses(lpidProcess: LPDWORD; cb: DWORD;
var cbNeeded: DWORD): BOOL;
l lpidProcess é um ponteiro para um array de DWORDS, que será preenchido com IDs de proces-
so pela função.
l cb contém o número de DWORDS no array passado em lpidProcess.
l No retorno da função, cbNeeded terá o número de bytes copiados para lpidProcess. A expressão
cbNeeded div SizeOf(DWORD) fornecerá o número de elementos copiados para o array e, portanto, o
número de processos em execução.
Depois de chamar essa função, o array passado em lpidProcess terá um punhado de IDs de processo.
Os IDs de processo não são muito úteis por si só, mas você pode passar o ID de um processo para a fun-
ção da API OpenProcess( ) a fim de obter uma alça de processo. Quando tiver uma alça de processo, você
poderá chamar outras funções da PSAPI ou ainda outras funções da API do Win32 que exijam alças de
processo.
PSAPI oferece uma função semelhante para obter informações sobre os drivers de dispositivo carre-
gados, chamada – vamos lhe dar uma chance para adivinhar - EnumDeviceDrivers( ). Esse método é defini-
do da seguinte maneira:
function EnumDeviceDrivers(lpImageBase: PPointer; cb: DWORD;
var lpcbNeeded: DWORD): BOOL;
l lpImageBase é um ponteiro para um array de Pointers que será preenchido com o endereço de base
de cada driver de dispositivo.
l cb contém o número de Pointers no array passado em lpImageBase.
l No retorno da função, lpcbNeeded terá o número de bytes copiados para lpImageBase.
No projeto SysInfo, ID é uma unidade chamada WNTInfo.pas, que contém uma classe chamada
que implementa IWin32Info. Essa classe contém um método privado, chamado Refresh( ), que
TWinNTInfo
obtém informações sobre processos e drivers de dispositivos:
420
procedure TWinNTInfo.Refresh;
var
Count: DWORD;
BigArray: array[0..$3FFF - 1] of DWORD;
begin
// Apanha array de IDs de processos
if not EnumProcesses(@BigArray, SizeOf(BigArray), Count) then
raise Exception.Create(SFailMessage);
SetLength(FProcList, Count div SizeOf(DWORD));
Move(BigArray, FProcList[0], Count);
// Apanha array de endereços de driver
if not EnumDeviceDrivers(@BigArray, SizeOf(BigArray), Count) then
raise Exception.Create(SFailMessage);
SetLength(FDrvList, Count div SizeOf(DWORD));
Move(BigArray, FDrvList[0], Count);
end;
Esse método inicialmente passa um local chamado BigArray para EnumProcesses( ) e EnumDeviceDri-
vers( ), e depois move os dados de BigArray para arrays dinâmicos chamados FProcList e FDrvList. O moti-
vo para essa implementação desajeitada dessas funções é que nem EnumProcesses( ) nem EnumDeviceDri-
vers( ) oferecem um meio para determinar quantos elementos serão retornados antes de alocar um ar-
ray. Portanto, somos obrigados a passar um array grande (que esperamos ser grande o suficiente) para os
métodos e copiar o resultado para um array dinâmico com tamanho apropriado.
O método FillProcessInfoList( ) para TWinNTInfo exige dois métodos auxiliadores – FillProcesses( ) e
FillDrivers( ) – para preencher o conteúdo de TListView no formulário principal. FillProcesses( ) é mos-
trado a seguir:
procedure TWinNTInfo.FillProcesses(ListView: TListView;
ImageList: TImageList);
var
I: Integer;
Count: DWORD;
ProcHand: THandle;
ModHand: HMODULE;
HAppIcon: HICON;
ModName: array[0..MAX_PATH] of char;
begin
for I := Low(FProcList) to High(FProcList) do
begin
ProcHand := OpenProcess(PROCESS_QUERY_INFORMATION or PROCESS_VM_READ,
False, FProcList[I]);
if ProcHand > 0 then
try
EnumProcessModules(Prochand, @ModHand, 1, Count);
if GetModuleFileNameEx(Prochand, ModHand, ModName,
SizeOf(ModName)) > 0 then
begin
HAppIcon := ExtractIcon(HInstance, ModName, 0);
try
if HAppIcon = 0 then HAppIcon := FWinIcon;
with ListView.Items.Add, SubItems do
begin
Caption := ModName; // nome do arquivo
Data := Pointer(FProcList[I]); // salva ID
Add(SProcName); // “processo”
421
Add(IntToStr(FProcList[I])); // ID do processo
Add(‘$’ + IntToHex(ProcHand, 8)); // alça do processo
// classe de prioridade
Add(GetPriorityClassString(GetPriorityClass(ProcHand)));
// ícone
if ImageList < > nil then
ImageIndex := ImageList_AddIcon(ImageList.Handle,
HAppIcon);
end;
finally
if HAppIcon < > FWinIcon then DestroyIcon(HAppIcon);
end;
end;
finally
CloseHandle(ProcHand);
end;
end;
end;
Esse método usa OpenProcess( ) para converter cada ID de processo em uma alça de processo. Vários
flags podem ser passados para esse método no primeiro parâmetro, mas para fins de consulta de informa-
ções com PSAPI, PROCESS_QUERY_INFORMATION e PROCESS_VM_READ juntos funcionam melhor. Dada uma alça de
processo, o código em seguida chama EnumProcessModules( ) para obter o nome do arquivo para o proces-
so. Esse método é definido da seguinte forma:
function EnumProcessModules(hProcess: THandle; lphModule: LPDWORD;
cb: DWORD; var lpcbNeeded: DWORD): BOOL;
Esse método funciona de uma maneira semelhante às outras funções da PSAPI: hProcess é uma alça
de processo, lphModule é um ponteiro para um array de alças de módulo, cb indica o número de elementos
do array e o parâmetro final retorna o número de bytes copiados para lphModule.
Como só estamos interessados no módulo primário para esse processo no momento, só passamos
um array de um elemento. O primeiro módulo retornado por EnumProcessModules( ) é o módulo primário
para o processo. Todas as informações de processo são então incluídas no componente TListView de uma
maneira semelhante à que aparece em TWin9xInfo.
FillDrivers( ) funciona de modo semelhante, exceto por usar o método GetDeviceDriverFileName( ),
mostrado a seguir:
function GetDeviceDriverFileName(ImageBase: Pointer; lpFileName: PChar;
nSize: DWORD): DWORD;
Esse método apanha a imagem básica do driver de dispositivo como primeiro parâmetro, um pon-
teiro para um buffer de string como segundo parâmetro e o tamanho do buffer no último parâmetro. Ao
retornar com sucesso, lpFileName terá o nome de arquivo do driver de dispositivo. Nosso uso desse méto-
do aparece no código a seguir:
procedure TWinNTInfo.FillDrivers(ListView: TListView;
ImageList: TImageList);
var
I: Integer;
DrvName: array[0..MAX_PATH] of char;
begin
for I := Low(FDrvList) to High(FDrvList) do
if GetDeviceDriverFileName(FDrvList[I], DrvName, SizeOf(DrvName)) > 0 then
with ListView.Items.Add do
begin
Caption := DrvName;
422
SubItems.Add(SDrvName);
SubItems.Add(‘$’ + IntToHex(Integer(FDrvList[I]), 8));
end;
end;
A Figura 14.11 mostra a aplicação SysInfo rodando em uma máquina com Windows NT 4.0.
Como você pode ver, esse método faz chamadas para OpenProcess( ) e EnumProcessModules( ), sobre as
quais você já aprendeu. No entanto, esse método também chama uma função da PSAPI chamada Query-
WorkingSet( ), para obter informações de memória para um processo. Essa função é definida da seguinte
forma:
function QueryWorkingSet(hProcess: THandle; pv: Pointer; cb: DWORD): BOOL;
unit WNTInfo;
interface
type
TWinNTInfo = class(TInterfacedObject, IWin32Info)
private
FProcList: array of DWORD;
FDrvlist: array of Pointer;
FWinIcon: HICON;
procedure FillProcesses(ListView: TListView; ImageList: TImageList);
procedure FillDrivers(ListView: TListView; ImageList: TImageList);
procedure Refresh;
public
constructor Create;
destructor Destroy; override;
procedure FillProcessInfoList(ListView: TListView;
ImageList: TImageList);
procedure ShowProcessProperties(Cookie: Pointer);
end;
implementation
const
SFailMessage = ‘Failed to enumerate processes or drivers. Make sure ‘+
‘PSAPI.DLL is installed on your system.’;
SDrvName = ‘driver’;
SProcname = ‘process’;
ProcessInfoCaptions: array[0..4] of string = (
‘Name’, ‘Type’, ‘ID’, ‘Handle’, ‘Priority’);
{ TWinNTInfo }
constructor TWinNTInfo.Create;
begin
FWinIcon := LoadImage(0, IDI_WINLOGO, IMAGE_ICON, LR_DEFAULTSIZE,
LR_DEFAULTSIZE, LR_DEFAULTSIZE or LR_DEFAULTCOLOR or LR_SHARED);
end;
425
Listagem 14.4 Continuação
destructor TWinNTInfo.Destroy;
begin
DestroyIcon(FWinIcon);
inherited Destroy;
end;
// classe de prioridade
Add(GetPriorityClassString(GetPriorityClass(ProcHand)));
// ícone
if ImageList < > nil then
ImageIndex := ImageList_AddIcon(ImageList.Handle,
HAppIcon);
end;
finally
if HAppIcon < > FWinIcon then DestroyIcon(HAppIcon);
end;
end;
finally
CloseHandle(ProcHand);
end;
end;
end;
procedure TWinNTInfo.Refresh;
var
Count: DWORD;
BigArray: array[0..$3FFF - 1] of DWORD;
begin
// Apanha array de IDs de processo
if not EnumProcesses(@BigArray, SizeOf(BigArray), Count) then
raise Exception.Create(SFailMessage);
SetLength(FProcList, Count div SizeOf(DWORD));
Move(BigArray, FProcList[0], Count);
// Apanha array de endereços de drivers
if not EnumDeviceDrivers(@BigArray, SizeOf(BigArray), Count) then
raise Exception.Create(SFailMessage);
SetLength(FDrvList, Count div SizeOf(DWORD));
Move(BigArray, FDrvList[0], Count);
end;
427
Listagem 14.4 Continuação
end.
unit DetailNT;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
DetBase, ComCtrls, HeadList;
type
TListType = (ltModules, ltMemory);
TWinNTDetailForm = class(TBaseDetailForm)
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure DetailTabsChange(Sender: TObject);
private
FProcHand: THandle;
DetailLists: array[TListType] of TStringList;
procedure ShowList(ListType: TListType);
public
procedure NewProcess(ProcessID: DWORD);
end;
implementation
uses PSAPI;
{$R *.DFM}
const
TabStrs: array[0..1] of string[7] = (‘Modules’, ‘Memory’);
DetailLists[LT].Free;
end;
end;
DetailSB.Panels[0].Text := Format(ACountStrs[ListType],
[DetailLists[ListType].Count]);
finally
Screen.Cursor := crDefault;
end;
end;
end.
Resumo
Este capítulo demonstrou técnicas para acessar informações do sistema de dentro dos seus programas em
Delphi. Ele focalizou o uso apropriado das funções da ToolHelp32 oferecidas pelo Windows 95/98 e as
funções da PSAPI encontradas no Windows NT. Você aprendeu a usar algumas funções da API do
Win32 para obter outros tipos de informações do sistema, incluindo informações da memória, variáveis
de ambiente e informações de versão. Além disso, você aprendeu a incorporar os componentes persona-
lizados TListView, TImageList, THeaderListbox e TMemView nas suas aplicações. O capítulo seguinte discute a
migração de suas aplicações a partir das versões anteriores do Delphi.
431
Transporte CAPÍTULO
para Delphi 5
15
NE STE C AP ÍT UL O
l Novo no Delphi 5
l Migração do Delphi 4
l Migração do Delphi 3
l Migração do Delphi 2
l Migração do Delphi 1
l Resumo
433
Aplicações MDI CAPÍTULO
16
NE STE C AP ÍT UL O
l Criação de uma aplicação MDI
l Trabalho com menus
l Técnicas variadas de MDI
l Resumo
ATENÇÃO
A Microsoft reconheceu que a implementação da MDI do Windows possui falhas. Ela aconselhou aos pro-
gramadores a não continuarem a criar aplicações no modelo MDI. Desde então, a Microsoft voltou a criar
aplicações no modelo da MDI, mas sem usar a implementação da MDI do Windows. Você ainda poderá
usar a MDI, mas fique sabendo que a implementação da MDI do Windows ainda possui falhas, e a Micro-
soft não possui planos para reparar esses problemas. O que apresentamos neste capítulo é uma implemen-
tação segura do modelo MDI.
O tratamento simultâneo de eventos entre vários formulários pode parecer difícil. Na programação
tradicional do Windows, você precisava conhecer bem a classe MDICLIENT do Windows, estruturas de da-
dos da MDI e as funções e mensagens adicionais específicas da MDI. Com o Delphi 5, a criação de aplica-
ções MDI simplificou bastante. Quando você terminar este capítulo, terá uma base sólida para montar
aplicações MDI, que poderão ser facilmente expandidas para incluir outras técnicas mais avançadas.
435
Compartilhamento de CAPÍTULO
informações com o
Clipboard
17
NE STE C AP ÍT UL O
l No princípio, havia o Clipboard 437
l Criação do seu próprio formato de Clipboard 439
l Resumo 446
No princípio, a humanidade lutava por sua sobrevivência. As pessoas viviam em cavernas escuras, caça-
vam por comida com lanças e pedras, e se comunicavam através de sons guturais e gestos. Elas trabalha-
ram o fogo, porque isso lhes deu a luz sob a qual trabalharam em seus lentos computadores. Naquela
época, os computadores podiam rodar apenas uma aplicação por vez, visto as limitações de software e
hardware. A única maneira de compartilhar informações era salvá-las em um disco e distribuí-lo, para
que outros pudessem copiar para suas máquinas.
Hoje, pelo menos software e hardware evoluíram. Com sistemas operacionais como o Windows
95/98 e Windows NT/2000, múltiplos aplicativos podem rodar simultaneamente, o que faz a vida muito
mais fácil e produtiva para os usuários de computadores. Uma das vantagens obtidas do Windows é o
compartilhamento de informações entre aplicativos na mesma máquina. Duas das mais novas tecnologi-
as para compartilhamento de informações são o Clipboard do Win32 e o Dynamic Data Exchange
(DDE). Você pode permitir que seus usuários copiem informações de um aplicativo para outro, com o
mínimo de trabalho, usando essas duas ferramentas.
Este capítulo mostra como usar o encapsulamento do Clipboard do Win32 em Delphi. As edições
anteriores desse livro explicavam o DDE. Mas com as poderosas tecnologias de interprocessamento de
comunicação como COM, não podemos, com consciência limpa, nos ater a uma tecnologia ultrapassa-
da. Mais adiante, no Capítulo 23, iremos discutir sobre o COM muito mais profundamente. Para simples
implementações de compartilhamento de informações entre aplicativos, o Clipboard ainda é uma solu-
ção muito sólida.
Formatos do Clipboard
O Win32 aceita 25 formatos predefinidos sobre os quais os aplicativos podem copiar ou colar no Clip-
board. Os formatos mais comuns são:
CF_BITMAP Especifica dados de Bitmap.
CF_DIB Especifica dados de Bitmap completos, com a informação da palheta de Bitmap.
CF_PALETE Especifica uma palheta de cores.
CF_TEXT Especifica um vetor (array) onde cada linha termina com um Enter (carriage re-
turn/linefeed). Esse é o formato mais comum.
Você pode ir à ajuda on-line da API do Win32, no tópico “SeTClipboardData” se estiver curioso so-
bre os formatos menos comuns. Além disso, o Win32 permite que você defina os seus próprios forma-
tos para o Clipboard, como veremos mais adiante neste capítulo.
437
Antes do Delphi, era necessário chamar diretamente várias funções do Clipboard e ser responsável
por assegurar que a aplicação não fizesse algo que fosse proibido com o conteúdo do Clipboard. Com o
Delphi, faz-se uso apenas da variável global Clipboard. Clipboard é uma classe que encapsula o Clipboard
do Win32.
procedure TMdiEditForm.mmiCutClick(Sender:Tobject);
begin
inherited;
memMainMemo.CutToClipboard;
end;
procedure TmdiEditForm.mmiCopyClick(Sender:Tobject);
begin
inherited;
memMainMemo.CopyToClipboard;
end;
procedure TmdiEditForm.mmiPasteClick(Sender:Tobject);
begin
inherited;
memMainMemo.PasteFormClipBoard;
end;
Conforme ilustrado na Listagem 17.1, você precisa chamar os métodos do TMemo para executar as
funções. Você também pode colocar o texto no Clipboard manualmente, usando a propriedade Clipbo-
ard.AsText. No sistema de 16 bits, a propriedade AsText era limitada até o máximo de 255 caracteres e
você tinha que usar os métodos SetTextBuf( ) e GetTextBuf( ) para copiar strings largas para o Clipboard.
Isso não é mais assim no Delphi de 32 bits, porque o tipo de string da propriedade AsText agora significa
strings longas. Você irá notar que SetTextBuf( ) e GetTextBuf( ) ainda são aceitos.
NOTA
As funções do Clipboard, GetTextBuf( ) e SetTextBuf( ), usam os tipos Pchar do Pascal como buffers para
passar e retirar dados do Clipboard. Quando se usam tais métodos, pode-se fazer um typecast de strings
longas para Pchar, para que você não precise fazer qualquer conversão de tipos String para Pchar.
438
Usando o Clipboard com imagens
O Clipboard também pode copiar e colar imagens. Você viu como isso pode ser feito no mesmo exemplo
de programa MDI. Os manipuladores de evento que executaram as operações do Clipboard aparecem na
Listagem 17.2.
DICA
Para acessar a variável global Clipboard, você deve incluir ClipBrd na cláusula uses na unidade que estará
fazendo uso do Clipboard.
NOTA
CF_PICTURE não é um formato default do Clipboard do Win32. Ao contrário, ele é um formato privado usa-
do pelas aplicações do Delphi para determinar quando os dados do Clipboard estão num formato compa-
tível com TPicture, como bitmaps e metafiles. Se você quisesse registrar o seu próprio formato gráfico,
TPicture suportaria normalmente esse formato. Procure por TPicture na ajuda on-line do Delphi, para ob-
ter mais informações sobre os formatos compatíveis com TPicture.
Listagem 17.3 Uma unidade que define dados personalizados para o Clipboard
unit cbdata;
interface
uses
SysUtils, Windows, clipbrd;
const
var
CF_DDGDATA: word; // Recebe valor de retorno de RegisterClipboardFormat( )
implementation
procedure TData.CopyToClipBoard;
{ Esta função copia o conteúdo do campo TDataRec, Rec, para o
clipboard como dados binários e como texto. Os dois formatos estarão
disponívies pelo clipboard }
const
CRLF = #13#10;
var
Data: THandle;
DataPtr: Pointer;
TempStr: String[50];
begin
// Aloca SizeOf(TDataRec) bytes do heap
Data := GlobalAlloc(GMEM_MOVEABLE, SizeOf(TDataRec));
try
// Obtém um ponteiro para o primeiro byte da memória alocada
DataPtr := GlobalLock(Data);
try
// Move os dados no Rec para o bloco de memória
Move(Rec, DataPtr^, SizeOf(TDataRec));
{ Clipboard.Open deve ser chamado se vários formatos de clipboard estão
sendo copiados para lá ao mesmo tempo. Se somente um formato estiver
sendo copiado, a chamada não é necessária. }
ClipBoard.Open;
// Primeiro copia os dados como seu formto personalizado
ClipBoard.SetAsHandle(CF_DDGDATA, Data);
// Agora copia os dados como formato de texto
with Rec do
TempStr := FName+CRLF+LName+CRLF+MI+CRLF+IntToStr(Age)+CRLF+
DateTimeToStr(BirthDate);
ClipBoard.AsText := TempStr;
{ Se for feita uma chamada a Clipboard.Open, você deverá casá-la
com uma chamada a Clipboard.Close }
Clipboard.Close
finally
// Desbloqueia a memória alocada globalmente
GlobalUnlock(Data);
end;
except
{ Uma chamada a GlobalFree só é necessária se ocorrer uma exceção.
Caso contrário, o clipboard assume o gerenciamento de qualquer
memória alocada a ele. }
GlobalFree(Data);
raise;
end;
end;
procedure TData.GetFromClipBoard;
{ Este método cola a memória salva no clipboard se ela estiver no
441
Listagem 17.3 Continuação
initialization
// Registra o formato de clipboard personalizado
CF_DDGDATA := RegisterClipBoardFormat(DDGData);
end.
Essa unidade executa várias funções. Primeiro, registra o novo formato no Clipboard do Win32
chamando a função RegisterClipboardFormat( ). Essa função retorna um valor que identifica esse novo for-
mato. Qualquer aplicação que registre esse mesmo formato, como foi especificado pelo parâmetro
string, obterá o mesmo valor quando chamar essa função. O novo formato estará disponível na lista de
formatos de ClipBoard, a qual pode ser acessada pela propriedade Clipboard.Formats.
A unidade também define o registro que contém os dados a serem colocados no Clipboard e o obje-
to que encapsula esse registro. O registro, TDataRec, possui campos de string para armazenar o nome da
pessoa, um campo inteiro para a idade e um campo TDateTime para a data de nascimento dessa pessoa.
O objeto que encapsula TDataRec, TData, define os métodos CopyToClipboard( ) e GetFromClipboard( ).
TData.CopyToClipboard( ) coloca o conteúdo do campo TData.Rec no Clipboard em dois formatos:
CF_DDGDATA e CF_TEXT. CF_TEXT, como você sabe, é um formato de Clipboard predefinido. A versão texto do
conteúdo de TData.Rec é colocada no Clipboard através da concatenação dos seus campos como strings
separadas por caracteres CR/LF (carriage return/line feed). Os campos não-string são convertidos para
strings antes da formulação da string final, que será salva no Clipboard. Clipboard.SetAsHandle( ) primeiro
aloca uma determinada alça no Clipboard, usando o formato especificado pelo seu parâmetro. Nesse
caso, o parâmetro é o formato recém-definido para o Clipboard, CF_DDGDATA.
Antes de chamar Clipboard.SetAsHandle( ), como sempre, o método prepara um THandle válido, que
deverá passar para SetAsHandle( ). Essa alça representa o bloco de memória que contém os dados sendo
enviados ao Clipboard. Veja a nota intitulada “Trabalhando com THandles”. A linha a seguir diz ao sis-
tema Win32 para alocar SizeOf(TDataRec) bytes de memória, que talvez sejam movidos, se necessário, e
para retornar uma alça dessa memória à variável Data:
442
Data := GlobalAlloc(GMEM_MOVEABLE, SizeOf(TDataRec));
Os dados são, então, movidos para o bloco de memória com a função Move( ). Nas linhas restantes
do código, o método Clipboard.Open( ) abre o Clipboard para impedir que outras aplicações o utilizem
enquanto está recebendo dados:
ClipBoard.Open;
try
ClipBoard.SetAsHandle(CF_DDGDATA, Data);
with Rec do
TempStr := FName+CRLF+LName+CRLF+MI+CRLF+IntToStr(Age)+CRLF+
DateTimeToStr(BirthDate);
ClipBoard.AsText := TempStr;
finally
Clipboard.Close
end;
Normalmente, não é necessário chamar Open( ), a menos que você esteja enviando formatos múlti-
plos ao Clipboard, como estamos fazendo aqui. Isso é porque cada atribuição do Clipboard usando um
de seus métodos (como Clipboard.SetTextBuf( )) ou propriedades (como Clipboard.AsText) faz com que o
Clipboard apague seu conteúdo anterior, porque eles também chamam Open( ) e Close( ) internamente.
Se chamarmos primeiro Clipboard.Open( ), impedimos que isso aconteça e por isso podemos enviar for-
matos múltiplos simultaneamente. Se você não tivesse chamado o método Open( ), só o formato CF_TEXT
estaria disponível no Clipboard depois da execução desse método. As linhas depois da chamada de
Open( ) simplesmente atribuem os dados ao Clipboard e então chamam o método Clipboard.Close( )
como for preciso.
Nesse ponto, o sistema Win32 é responsável pelo gerenciamento da memória alocada para o Clip-
board com a função GlobalAlloc( ). Uma chamada a GloblaFree( ) só seria necessária se uma exceção ocor-
resse durante o processo de cópia. Não chame GlobalFree( ) de outro modo, pois o Win32 passou o ge-
renciamento dessa memória para o Clipboard.
Com ambos os formatos CF-DDGDATA e CF_TEXT disponíveis no Clipboard, você pode colar os dados de
volta neste programa de exemplo ou em qualquer outra aplicação, como ilustraremos logo adiante.
TData.GetFromClipboard( ) faz justamente o contrário – ele retira dados do Clipboard no formato
CF_DDGDATA e coloca os dados no campo TData.Rec. O comentário na listagem explica como esse método
funciona. O exemplo de aplicação que iremos mostrar em seguida ilustra como usar essa unidade. Obser-
ve que esse objeto Clipboard pode ser modificado facilmente para armazenar qualquer tipo de registro
que você possa definir.
NOTA
Não libere a alça retornada de GetAsHandle( ); ela não pertence à sua aplicação – pertence ao Clipboard.
Logo, os dados aos quais a alça faz referência deverão ser copiados.
Como você pode ver, esse formulário contém os controles exibidos para se preencher o campo TDa-
taRec do objeto TData. A Listagem 17.4 mostra o código-fonte para esse formulário. O projeto está no CD
como Ddgcbp.dpr.
unit MainFrm;
interface
uses
SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls, clipbrd, Mask, ComCtrls;
type
TMainForm = class(TForm)
edtFirstName: TEdit;
edtLastName: TEdit;
edtMI: TEdit;
btnCopy: TButton;
btnPaste: TButton;
meAge: TMaskEdit;
btnClear: TButton;
lblFirstName: TLabel;
lblLastName: TLabel;
lblMI: TLabel;
lblAge: TLabel;
lblBirthDate: TLabel;
memAsText: TMemo;
lblCustom: TLabel;
444
Listagem 17.4 Continuação
lblText: TLabel;
dtpBirthDate: TDateTimePicker;
procedure btnCopyClick(Sender: TObject);
procedure btnPasteClick(Sender: TObject);
procedure btnClearClick(Sender: TObject);
end;
var
MainForm: TMainForm;
implementation
uses cbdata;
{$R *.DFM}
dtpBirthDate.Date := BirthDate;
end;
finally
DataObj.Free;
end;
// Agora copia versão texto dos dados para componente TMemo do formulário.
if ClipBoard.HasFormat(CF_TEXT) then
memAsText.PasteFromClipBoard;
end;
end.
Quando o usuário dá um clique no botão Copy, ele copia os dados contidos nos controles TEdit, TDa-
teTime e TMaskEdit para o campo TDataRec de um objeto TData. Isso faz chamar o método TData.CopyToClipbo-
ard( ), que coloca os dados no Clipboard.
Quando o botão Paste é acionado, ocorre o processo contrário. Primeiro, se os dados no Clipboard
são do tipo CF_DDGDATA, eles são copiados do Clipboard e colocados nos controles de edição do formulá-
rio. A representação textual dos dados também é copiada e colocada no componente TMemo do formulário
principal. O resultado de uma operação de colagem é mostrado na Figura 17.2. Você também pode colar
o texto representando os dados em outro aplicativo do Windows, como o Bloco de Notas.
Resumo
Compartilhar dados com outras aplicações é uma técnica extremamente útil. Permitindo que suas aplica-
ções compartilhem dados com outras aplicações, você os torna mais úteis e os seus usuários mais produti-
vos. Este capítulo mostra como usar as funções embutidas do Clipboard em conjunto com os controles
do Delphi. Ele também demonstra como criar seus próprios formatos para o Clipboard. Outro método
de comunicação interprocesso, ainda mais poderoso, é o COM, que será explicado com detalhes em ou-
tros capítulos.
446
Programação de CAPÍTULO
448
Teste e depuração CAPÍTULO
19
NE STE C AP ÍT UL O
l Bugs comuns do programa
l Uso do depurador integrado
l Resumo
450
Desenvolvimento PARTE
com base em
componentes
III
NE STA PART E
20 Elementos-chave da VCL e RTTI 454
da VCL e RTTI
20
NE STE C AP ÍT UL O
l O que é um componente? 454
l Tipos de componentes 455
l A estrutura do componente 456
l A hierarquia do componente visual 461
l RTTI (Runtime Type Information) 469
l Resumo 489
Quando a Borland lançou a OWL (Object Windows Library) com o Turbo Pascal for Windows, introdu-
ziu uma significativa simplificação em relação à programação tradicional no ambiente Windows. Os ob-
jetos da OWL automatizaram muitas tarefas cansativas que, não fossem eles, exigiriam a introdução de
código da parte do usuário. A partir de então, você não precisava mais escrever uma série de instruções
case para capturar mensagens ou uma grande massa de código para gerenciar as classes do Windows; a
OWL fazia isso para você. Por outro lado, você tinha que aprender uma nova metodologia de programa-
ção – a programação orientada a objeto.
A VCL (Visual Component Library), lançada no Delphi 1, sucedeu a OWL. Teoricamente, ela se
baseava em um modelo de objeto semelhante ao da OWL, porém a sua implementação era radicalmente
diferente. A VCL no Delphi 5 é igual à que é encontrada no Delphi 1, 2, 3 e 4, com algumas novidades e
diversas melhorias.
A VCL foi projetada com a finalidade específica de trabalhar dentro do ambiente visual do Delphi.
Em vez de criar uma janela ou caixa de diálogo e adicionar seu comportamento no código, você modifica
as características comportamentais e visuais dos componentes à medida que elabora o programa visual-
mente.
O nível de conhecimento necessário sobre a VCL realmente depende do modo como você a utiliza.
Primeiro, você deve perceber que há dois tipos de programadores em Delphi: programadores de aplica-
ções e criadores de componentes visuais. Os programadores de aplicações criam aplicações interagindo
com o ambiente visual do Delphi (um conceito inexistente em muitas outras estruturas). Essas pessoas
usam a VCL para criar a interface do usuário e outros elementos de sua aplicação, como, por exemplo, a
conectividade do banco de dados. Criadores de componentes, por outro lado, expandem a VCL existente
desenvolvendo novos componentes. Esses componentes chegam ao mercado através de empresas inde-
pendentes.
Se você planeja criar aplicações com o Delphi ou criar componentes do Delphi, a compreensão da Vi-
sual Component Library é de fundamental importância. Um programador de aplicações precisa saber quais
são as propriedades, eventos e métodos disponíveis para cada componente. Além disso, vale a pena com-
preender todo o modelo de objeto inerente a uma aplicação fornecida pela VCL. Um problema comum
que vemos com os programadores em Delphi é que eles tendem a lutar com a ferramenta – o que mostra que
ela não foi compreendida em sua totalidade. Os criadores de componentes aprofundam um pouco mais
esse conhecimento para determinar se pretendem escrever um componente novo ou ampliar um já existen-
te, como por exemplo as mensagens de janela de alças da VCL, notificações internas, propriedade do com-
ponente, questões de propriedade/paternidade e editores de propriedade, entre outras questões.
Este capítulo apresenta a VCL (Visual Component Library). Ele discute a hierarquia do componen-
te e explica o objetivo dos principais níveis dentro da hierarquia. Discute ainda o objetivo das proprieda-
des, dos métodos e dos eventos que aparecem nos diferentes níveis de componentes. Finalmente, com-
pletamos este capítulo discutindo sobre a RTTI (Runtime Type Information).
O que é um componente?
Componentes são os blocos de montagem que os programadores usam para projetar a interface do usuá-
rio e proporcionar alguns recursos não-visuais para as suas aplicações. Para os programadores, um com-
ponente é algo a que eles têm acesso através da Component Palette e colocam em seus formulários. A
partir daí, eles podem manipular as várias propriedades e adicionar manipuladores para dar ao compo-
nente uma aparência ou comportamento específico. Da perspectiva de um criador de componente, os
componentes são objetos em um código Object Pascal. Esses objetos podem encapsular o comportamen-
to de elementos fornecidos pelo sistema (como, por exemplo, os controles-padrão do Windows 95/98).
Outros objetos podem introduzir elementos visuais e não-visuais inteiramente novos, quando o código
de um componente pode assumir todo o comportamento do componente.
A complexidade dos componentes varia de modo significativo. Alguns componentes são simples;
outros encapsulam elaboradas tarefas. Não há limites para o que um componente pode fazer ou o que
pode ser feito a partir dele. Você pode ter um componente simples como um TLabel ou ter um compo-
454 nente muito mais complexo, que encapsule toda a funcionalidade de uma planilha.
O segredo para a compreensão da VCL é saber os tipos de componentes que existem. Você precisa
compreender os elementos comuns dos componentes. Também é preciso entender a hierarquia de com-
ponentes e o objetivo de cada nível dentro da hierarquia. As seções a seguir contêm essas informações.
Tipos de componentes
Há quatro tipos de componentes básicos que você usa e/ou cria no Delphi: controles-padrão, controles
personalizados, controles gráficos e componentes não-visuais.
NOTA
Você verá com freqüência os termos componente e controle usados de modo indistinto, embora nem sem-
pre sejam a mesma coisa. Um controle é um elemento visual da interface do usuário. No Delphi, os contro-
les são sempre componentes, pois descendem da classe TComponent. Componentes são os objetos cujo
comportamento básico permite que eles apareçam na Component Palette e sejam manipulados no Form
Designer. Componentes são do tipo TComponent e nem sempre são controles – ou seja, nem sempre são ele-
mentos visuais da interface do usuário.
Componentes-padrão
O Delphi fornece componentes-padrão, que encapsulam o comportamento dos controles do Windows
95/98, como, por exemplo, TRichEdit, TTrackBar e TListView (para citar apenas alguns). Esses componentes
existem na página Win95 da Component Palette. Na verdade, esses componentes são wrappers que en-
volvem os controles comuns do Windows 95/98. Se você for um proprietário do código-fonte da VCL,
pode exibir o método da Borland que envolve esses controles no arquivo ComCtrls.pas.
DICA
Ter o código-fonte para a VCL é fundamental para compreender a VCL, especialmente se você planeja es-
crever componentes. Provavelmente, não existe uma forma melhor de se aprender a escrever componentes
do que ver como a Borland os produziu. Se você não tiver a biblioteca RTL (Runtime Library), insistimos para
que a encomende junto à Borland.
Componentes personalizados
Componentes personalizados é um termo geral para componentes que não fazem parte da biblioteca de
componentes do Delphi. Em outras palavras, são componentes que você ou outros programadores escre-
vem e adicionam ao conjunto de componentes existentes. Voltaremos a falar sobre a criação de compo-
nentes personalizados ainda neste capítulo.
Componentes gráficos
Componentes gráficos permitem que você tenha ou crie controles visuais que não recebem o foco de en-
trada do usuário. Esses componentes são úteis quando você deseja exibir algo para o usuário, mas não de-
seja que eles sobrecarreguem os recursos do Windows, como acontece com os componentes-padrão e
personalizados. Os componentes gráficos não usam os recursos do Windows, pois não precisam de alça
de janela, que é também a razão para que não tenham o foco. Exemplos de componentes gráficos são TLa-
bel e TShape. Esses componentes também não podem servir como componentes contêiner; ou seja, não
podem possuir outros componentes colocados sobre deles. Outros exemplos de componentes gráficos
são TImage, TBevel e TPaintBox.
455
Alças
Alças (ou handles) são números de 32 bits produzidos pelo Win32 que fazem referência a determi-
nadas instâncias de objeto. O termo objeto aqui diz respeito a objetos do Win32, não a objetos do
Delphi. Há diferentes tipos de objetos no Win32: objetos de kernel, objetos de usuário e objetos de
GDI. Objetos de kernel se aplicam a itens como eventos, objetos de mapeamento de arquivo e pro-
cessos. Objetos de usuário dizem respeito a objetos de janela, como, por exemplo, controles de edi-
ção, caixas de listagem e botões. Objetos de GDI referem-se a bitmaps, pincéis e fontes, entre ou-
tras coisas.
No ambiente Win32, todas as janelas têm uma alça exclusiva. Muitas funções da API do Win-
dows requerem uma alça de modo que elas saibam em que janela devem executar a operação. O
Delphi encapsula grande parte da API do Win32 e executa o gerenciamento de alça. Se você deseja
usar uma função da API do Windows que faça uso de uma alça de janela, deve usar descendentes de
TWinControl e TCustomControl, que têm uma propriedade Handle.
Componentes não-visuais
Como o próprio nome indica, componentes não-visuais não têm uma característica visual. Esses compo-
nentes dão a possibilidade de encapsular a funcionalidade de uma entidade dentro de um objeto e permi-
tem que você modifique certas características desse componente através do Object Inspector durante o
projeto, modificando suas propriedades e fornecendo manipuladores de evento para seus eventos.
Alguns exemplos desses componentes são TOpenDialog, TTable e TTimer.
A estrutura do componente
Como dissemos, componentes são classes do Object Pascal que encapsulam a funcionalidade e o com-
portamento de elementos que os programadores usam para adicionar características visuais e comporta-
mentais aos seus programas. Todos os componentes têm uma certa estrutura. As seções a seguir discutem
o processo de composição dos componentes do Delphi.
NOTA
Entenda a diferença entre um componente e uma classe. Um componente é uma classe que pode ser mani-
pulada dentro do ambiente Delphi. Uma classe é uma estrutura do Object Pascal, como dissemos no Capí-
tulo 2.
Propriedades
O Capítulo 2 fez uma apresentação das propriedades. As propriedades dão ao usuário uma interface para
os campos de armazenamento interno de um componente. Usando propriedades, o usuário do compo-
nente pode modificar ou ler valores de campo de armazenamento. Geralmente, o usuário não tem acesso
direto aos campos de armazenamento do componente, pois eles são declarados na seção private da defi-
nição de classe de um componente.
Este método primeiro verifica se o usuário do componente não está tentando atribuir o mesmo va-
lor que a propriedade já armazena. Se não, ele faz a atribuição ao campo de armazenamento interno FMax-
Length e em seguida chama a função SendMessage( ) para passar a mensagem EM_LIMITTEXT do Windows à ja-
nela que TCustomEdit encapsula. Essa mensagem limita a quantidade de texto que um usuário pode inserir
em um controle de edição. A chamada de SendMessage( ) no método de acesso de escrita da propriedade é
conhecida como um efeito colateral durante a atribuição dos valores de propriedade.
Efeitos colaterais são as ações afetadas pela atribuição de um valor a uma propriedade. Na atribuição
de um valor à propriedade MaxLength de TCustomEdit, o efeito colateral é que o controle de edição encapsula-
do recebe um limite de entrada. Os efeitos colaterais podem ser muito mais sofisticados do que isso.
Uma grande vantagem de fornecer acesso aos campos de armazenamento internos de um compo-
nente através de propriedades é que o criador do componente pode alterar a implementação do acesso
ao campo sem afetar o comportamento para o usuário do componente.
Um método de acesso de leitura, por exemplo, pode alterar o tipo do valor retornado para alguma
coisa diferente do tipo do campo de armazenamento a que a propriedade faz referência.
457
Outra razão fundamental para o uso de propriedades é tornar as modificações disponíveis para elas
durante o tempo de projeto. Quando uma propriedade aparece na seção published da declaração de um
componente, ela também aparece no Object Inspector, de modo que o usuário do componente possa fa-
zer modificações nessa propriedade.
Você aprenderá muito mais sobre as propriedades e como criá-las e a seus métodos de acesso no Ca-
pítulo 21.
Tipos de propriedades
As regras-padrão que se aplicam aos tipos de dados do Object Pascal também se aplicam às propriedades.
O ponto mais importante sobre as propriedades é que seus tipos também determinam como elas são edi-
tadas no Object Inspector. As propriedades podem ser dos tipos mostrados na Tabela 20.1. Para obter in-
formações mais detalhadas, consulte “properties” (propriedades) na ajuda on-line.
Métodos
Como os componentes são objetos, eles podem ter métodos. Você já viu informações sobre os métodos
de objeto no Capítulo 2 (essas informações não serão repetidas aqui). A seção “A hierarquia do compo-
nente visual” descreve alguns dos principais métodos dos diferentes níveis de componente na hierarquia
de componentes.
Eventos
Eventos são ocorrências de uma ação, geralmente uma ação de sistema como um clique em um controle
de botão ou a ativação de uma tecla em um teclado. Os componentes contêm propriedades especiais cha-
madas eventos; os usuários do componente podem conectar um código de evento que será executado
quando ocorrer o evento.
458
Conectando um código aos eventos durante o projeto
Se você olhar para a página de eventos de um componente TEdit, verá eventos como OnChange, OnClick e
OnDblClick. Para criadores de componentes, eventos são ponteiros para métodos. Quando os usuários de
um componente atribuem código a um evento, criam um manipulador de evento. Por exemplo, quando
você dá um clique duplo em um evento na página de eventos do Object Inspector de um componente, o
Delphi gera um método ao qual você adiciona o seu código, como o código a seguir para o evento OnClick
de um componente TButton:
TForm1 = class(TForm)
Button1: Tbutton;
procedure Button1Click(Sender: TObject);
end;
...
procedure TForm1.Button1Click(Sender: TObject);
begin
{ O código do evento é incluído aqui }
end;
O exemplo anterior mostra um método definido pelo usuário chamado MyOnClickEvent( ) que serve
como o manipulador de evento para Button1.OnClick. A linha a seguir mostra como você atribui esse méto-
do ao evento Button1.OnClick em código, o que em geral é feito no manipulador de evento OnCreate do for-
mulário:
procedure TForm1.FormCreate(Sender: TObject);
begin
Button1.OnClick := MyOnClickEvent;
end;
Essa técnica pode ser usada para adicionar diferentes manipuladores de evento a eventos, com base
em diversas condições no seu código. Além disso, você pode desativar um manipulador de um evento
atribuindo nil ao evento, como se vê a seguir:
Button1.OnClick := nil;
459
A atribuição de manipuladores de evento em runtime é, basicamente, o que acontece quando você
cria um manipulador de evento através do Object Inspector do Delphi – exceto que o Delphi gera a de-
claração do método. Você não pode atribuir qualquer método a um manipulador de evento em particu-
lar. Como as propriedades de evento são ponteiros de método, elas têm assinaturas de método específi-
cas, dependendo do tipo de evento. Por exemplo, um método OnMouseDown é do tipo TMouseEvent, uma defi-
nição de procedimento mostrada a seguir:
TMouseEvent = procedure (Sender: TObject; Button: TMouseButton; Shift:
TShiftState; X, Y: Integer) of object;
Portanto, os métodos que se tornam manipuladores de evento para certos eventos devem seguir a
mesma assinatura que os tipos de evento. Eles devem conter o mesmo tipo, número e ordem de parâ-
metros.
Já dissemos que os eventos são propriedades. Como propriedades de dados, os eventos dizem res-
peito a campos de dados privados de um componente. Esse campo de dados é do tipo de procedimento,
como TMouseEvent. Examine este código:
TControl = class(TComponent)
private
FOnMouseDown: TMouseEvent;
protected
property OnMouseDown: TMouseEvent read FOnMouseDown write FOnMouseDown;
public
end;
Lembre-se da discussão de propriedades e como elas fazem referência aos campos de dados priva-
dos de um componente. Você pode ver como os eventos, sendo propriedades, fazem referência aos cam-
pos de ponteiro de método privados de um componente.
Para obter mais informações sobre a criação de eventos e manipuladores de evento, consulte o Ca-
pítulo 21.
Fluxo
Uma característica dos componentes é que eles devem ter a possibilidade de ter fluxo. Fluxo é uma forma
de armazenar um componente e as informações relacionadas aos valores de suas propriedades em um ar-
quivo. Os recursos de fluxo do Delphi cuidam de todos esses detalhes para você. Na verdade, o arquivo
DFM, criado pelo Delphi, não passa de um arquivo de recursos contendo as informações de fluxo no for-
mulário e seus componentes como um recurso RCDATA. Como um criador de componente, no entanto,
algumas vezes você deve ir além do que o Delphi pode fazer automaticamente. O mecanismo de fluxo do
Delphi é explicado de modo bem mais profundo no Capítulo 22.
Posse
Os componentes têm a capacidade de possuir outros componentes. O proprietário de um componente é
especificado por sua propriedade Owner. Quando um componente possui outros componentes, é respon-
sável pela liberação dos componentes que possui quando for destruído. Geralmente, o formulário possui
todos os componentes que aparecem nele. Quando você posiciona um componente em um formulário
no Form Designer, o formulário automaticamente se torna o proprietário do componente. Quando você
cria um componente em runtime, deve passar a posse do componente para o construtor Create do com-
ponente; ela é atribuída à propriedade Owner do novo componente. A linha a seguir mostra como passar a
variável Self implícita do formulário para um construtor TButton.Create( ), tornando assim o formulário
o proprietário do componente recém-criado:
MyButton := TButton.Create(self);
460
Quando o formulário é destruído, a instância TButton à qual MyButton faz referência também é des-
truída. Isso é manipulado internamente na VCL. Basicamente, o formulário interage com os componen-
tes referenciados por meio de sua propriedade de array Components (explicada de modo mais detalhado
logo a seguir) e os destrói.
É possível criar um componente sem um proprietário passando nil para o método Create( ) do com-
ponente. No entanto, quando isso é feito, cabe a você destruir o componente por meio de programação.
O código a seguir mostra essa técnica:
MyTable := TTable.Create(nil)
try
{ MyTable é processada }
finally
MyTable.Free;
end;
Ao usar essa técnica, você deve usar um bloco try..finally para garantir a liberação dos recursos alo-
cados caso ocorra uma exceção. Você só deve usar essa técnica em última instância, quando for impossí-
vel passar um proprietário para o componente.
Outra propriedade associada à posse é a propriedade Components. A propriedade Components é uma
propriedade de array que mantém uma lista de todos os componentes pertencentes a um componente.
Por exemplo, para fazer um loop por todos os componentes em um formulário para mostrar os seus no-
mes de classe, execute o código a seguir:
var
i: integer;
begin
for i := 0 to ComponentCount - 1 do
ShowMessage(Components[i].ClassName);
end;
Obviamente, você por certo vai executar uma operação mais importante nesses componentes. O
código anterior apenas ilustra a técnica.
Paternidade
O que não deve ser confundido com propriedade é um conceito chamado paternidade. Componentes
podem ser pais de outros componentes. Apenas os componentes dentro de janela, como os descendentes
de TWinControl, podem servir como pais de outros componentes. Os componentes pai são responsáveis
pela chamada dos métodos do componente filho para forçá-los a serem desenhados. Os componentes pai
são responsáveis pela pintura apropriada dos componentes filhos. O pai de um componente é especifica-
do através da propriedade Parent.
O pai de um componente não tem necessariamente de ser seu proprietário. É perfeitamente possí-
vel que um componente tenha pai e proprietário diferentes.
461
TObject
TApplication
TGraphicControl TWinControl
TCustomComboBox TButtonControl
TCustomControl TScrollBar
TCustomEdit TScrollingWinControl
TCustomListBox
TForm
Como um criador de componente, você não faz seus componentes descenderem diretamente de
TObject. A VCL já tem descendentes da classe TObject, a partir dos quais os novos componentes podem ser
derivados. Essas classes existentes fornecem grande parte da funcionalidade de que você precisa para
seus próprios componentes. Você só deve descender diretamente de TObject apenas quando cria classes
de não-componentes.
Os métodos Create( ) e Destroy( ) de TObject são responsáveis pela alocação e desalocação de memó-
ria para uma instância de objeto. Na verdade, o construtor TObject.Create( ) retorna uma referência para
o objeto que está sendo criado. TObject contém diversas funções que retornam informações úteis sobre
um determinado objeto.
A VCL usa a maioria dos métodos de TObject internamente. Você pode obter informações úteis so-
bre uma instância de um TObject ou de um descendente de TObject como, por exemplo, o tipo de classe, o
nome da classe e as classes ancestrais da instância.
ATENÇÃO
Use TObject.Free no lugar de TObject.Destroy. O método free chama destroy para você, mas primeiro ve-
rifica se o objeto é nil antes de chamar destroy. Esse método garante que você não vai gerar uma exceção
tentando destruir um objeto inválido.
A classe TPersistent
A classe TPersistent descende diretamente de TObject. A característica especial de TPersistent é que os obje-
tos descendentes dele podem ler e escrever suas propriedades de e para um fluxo depois que forem cria-
dos. Como todos os componentes são descendentes de TPersistent, todos eles podem ser colocados no
fluxo. TPersistent não define propriedades ou eventos especiais, embora defina alguns métodos que são
úteis tanto para o usuário como para o criador do componente.
Métodos de TPersistent
A Tabela 20.2 relaciona alguns métodos interessantes definidos pela classe TPersistent.
462
Tabela 20.2 Métodos da classe TPersistent
Método Objetivo
Assign( ) Esse método público permite que um componente atribua a si mesmo os dados
associados a outro componente.
AssignTo( ) Esse método protegido é onde os descendentes de TPersistent devem implementar a
definição da VCL para AssignTo( ). TPersistent, por si só, produz uma exceção
quando esse método é chamado. AssignTo( ) é onde um componente pode atribuir
seus valores de dados para outra classe ou instância – o oposto de Assign( ).
DefineProperties( ) Esse método protegido permite que os criadores de componentes definam o modo
como o componente armazena propriedades extras ou não-publicadas. Esse método
geralmente é usado para fornecer um meio para que um componente armazene dados
que não sejam um tipo de dados simples, como, por exemplo, dados binários.
A capacidade de fluxo de componentes é descrita de modo mais detalhado no Capítulo 12. Por en-
quanto, basta-nos saber que os componentes podem ser armazenados e recuperados de um arquivo de
disco através do streaming.
A classe TComponent
A classe TComponent descende diretamente de TPersistent. As características especiais de TComponent são que
suas propriedades podem ser manipuladas durante o projeto através do Object Inspector e que pode pos-
suir outros componentes.
Os componentes não-visuais também descendem de TComponent e, portanto, herdam a capacidade de
serem manipulados durante o projeto. Um bom exemplo de um descendente de TComponent não-visual é o
componente TTimer. Os componentes TTimer são controles não visuais, mas mesmo assim estão disponí-
veis na Component Palette.
TComponent define diversas propriedades e métodos interessantes, conforme descrito nas seções a seguir.
Propriedades de TComponent
As propriedades definidas por TComponent e seus objetivos são mostrados na Tabela 20.3.
Métodos de TComponent
TComponent define diversos métodos relacionados à sua capacidade de possuir outros componentes e ser
manipulada no Form Designer.
TComponent define o construtor Create( ) do componente, que já foi discutido neste capítulo. Esse
construtor é responsável pela criação de uma instância do componente e por dar-lhe um proprietário
com base no parâmetro passado para ele. Ao contrário de TObject.Create( ), TComponent.Create( ) é virtual.
Os descendentes de TComponent que implementam um construtor devem declarar o construtor Create( )
com a diretiva override. Embora você possa declarar outros construtores em uma classe de componente,
TComponent.Create( ) é o único construtor que a VCL usará para criar uma instância da classe durante o
projeto e em runtime, ao carregar o componente de um fluxo.
O destruidor TComponent.Destroy( ) é responsável pela liberação do componente e dos recursos alo-
cados pelo componente.
O método TComponent.Destroying( ) é responsável pela definição de um componente e dos compo-
nentes que ele possui como um estado que indique que estão sendo destruídos; o método TComponent.Des-
troyComponents( ) é responsável pela destruição dos componentes. Provavelmente você não vai ter de lidar
com esses métodos.
O método TComponent.FindComponent( ) é prático quando você deseja fazer referência a um compo-
nente do qual você conhece apenas o nome. Suponha que você sabe que o formulário principal tem um
componente TEdit chamado Edit1. Quando você não tem uma referência para esse componente, pode re-
cuperar um ponteiro para sua instância executando o seguinte código:
EditInstance := FindComponent.(‘Edit1’);
Nesse exemplo, EditInstance é um tipo TEdit. FindComponent( ) retornará nil se o nome não existir.
O método TComponent.GetParentComponent( ) recupera uma instância para o componente pai do com-
ponente. Esse método pode retornar nil se não houver um pai para um componente.
O método TComponent.HasParent( ) retorna um valor booleano indicando se o componente tem um com-
ponente pai. Observe que esse método não faz referência ao fato de esse componente ter um proprietário.
O método TComponent.InsertComponent( ) adiciona um componente a fim de que ele seja possuído
pelo componente que o chama; TComponent.RemoveComponent( ) remove um componente possuído do com-
ponente que o chama. Normalmente, você não usaria esses métodos, pois eles são chamados automatica-
mente pelo construtor Create( ) e pelo destruidor Destroy( ) do componente.
A classe TControl
A classe TControl define muitas propriedades, métodos e eventos comumente usados por componentes vi-
suais. Por exemplo, TControl introduz a capacidade de um controle exibir a si mesmo. A classe TControl in-
clui propriedades de posição, como, por exemplo, Top e Left, bem como propriedades de tamanho,
como, por exemplo, Width e Height, que armazenam os tamanhos horizontal e vertical. Outras proprieda-
des incluem ClientRect, ClientWidth e ClientHeight.
464
TControl também introduz propriedades relacionadas à exibição e ao nível de acesso, como, por
exemplo, Visible, Enabled e Color. Você também pode especificar uma fonte para o texto de uma TControl
através de sua propriedade Font. Esse texto é fornecido através das propriedades Text e Caption de TControl.
TControl introduz também eventos-padrão, como os eventos de mouse OnClick, OnDblClick, OnMouse-
Down, OnMouseMove e OnMouseUp. Introduz também eventos de arrastar, como, por exemplo, OnDragOver,
OnDragDrop e OnEndDrag.
TControl não é muito útil em nível de TControl. Você nunca criará descendentes de TControl.
Outro conceito introduzido por TControl é que ele pode ter um componente pai. Embora TControl
possa ter um pai, seu pai deve ser um TWinControl (os controles pais devem ser controles em janela). A
TControl introduz a propriedade Parent.
A maioria dos controles do Delphi deriva dos descendentes de TControl: TWinControl e TGraphicControl.
A classe TWinControl
Os controles-padrão do Windows descendem da classe TWinControl. Os controles-padrão são os objetos
de interface de usuário que você vê na maioria das aplicações do Windows. Itens como controles de edi-
ção, caixas de listagem, caixas de combinação e botões são exemplos desses controles. Como o Delphi
encapsula o comportamento de controles-padrão no lugar de usar as funções da API do Windows para
manipulá-los, você usa as propriedades fornecidas por cada um dos vários componentes de controle.
As três características básicas dos objetos de TWinControl são que têm uma alça do Windows, podem
receber foco de entrada e podem ser pais de outros controles. Você descobrirá que as propriedades per-
tencentes a TWinControl suportam a mudança de foco, os eventos de teclado, o desenho de controles e ou-
tras funções obrigatórias de TWinControl.
Um programador de aplicações usa principalmente descendentes de TWinControl. Um criador de
componentes deve entender o descendente TCustomControl de TWinControl.
Propriedades de TWinControl
TWinControl define diversas propriedades aplicáveis para mudar o foco e a aparência do controle.
A propriedade TWinControl.Brush é usada para desenhar os padrões e as formas do controle. Discuti-
mos essa propriedade no Capítulo 8.
A propriedade TWinControl.Controls é uma propriedade de array que mantém uma lista de todos os
controles para os quais o TWinControl que chama é pai.
A propriedade TWinControl.ControlCount armazena um contador dos controles para os quais ela é pai.
TWinControl.Ctl3D é uma propriedade que especifica se o controle deve ser desenhado usando uma
aparência tridimensional.
A propriedade TWinControl.Handle corresponde à alça do objeto do Windows que a TWinControl encap-
sula. Essa é a alça que você passaria para as funções da API do Win32 que fazem uso de um parâmetro de
alça de janela.
TWinControl.HelpContext armazena um número de contexto de ajuda que corresponde a uma tela de
ajuda em um arquivo de ajuda. Isso é usado para fornecer ajuda contextual para controles individuais.
TWinControl.Showing indica se um controle é visível.
A propriedade TWinControl.TabStop armazena um valor booleano para determinar se um usuário
pode tabular para o controle em questão. A propriedade TWinControl.TabOrder especifica onde, na lista de
controles tabulados do pai, o controle se encontra localizado.
Métodos de TWinControl
O componente TWinControl também oferece diversos métodos que têm a ver com a criação de janela, o
controle de foco, o disparo de evento e o posicionamento. Há muitos métodos para serem discutidos
neste capítulo; no entanto, todos eles estão documentados na ajuda on-line do Delphi. Nos próximos pa-
rágrafos, iremos listar apenas os métodos de interesse particular.
465
Métodos relacionados à criação, recriação e destruição de janela se aplicam principalmente a cria-
dores de componentes, e são discutidos no Capítulo 21. Esses métodos são CreateParams( ), CreateWnd( ),
CreateWindowHandle( ), DestroyWnd( ), DestroyWindowHandle( ) e RecreateWnd( ).
Os métodos que têm a ver com foco, posicionamento e alinhamento de janela são CanFocus( ), Focu-
sed( ), AlignControls( ), EnableAlign( ), DisableAlign( ) e ReAlign( ).
Eventos de TWinControl
TWinControl introduz eventos para mudança de foco e utilização do teclado. Os eventos de teclado são
OnKeyDown, OnKeyPress e OnKeyUp. Eventos de mudança de foco são OnEnter e OnExit. Todos esses eventos estão
documentados na ajuda on-line do Delphi.
A classe TGraphicControl
TGraphicControls, ao contrário de TWinControls, não tem uma alça de janela e por essa razão não pode rece-
ber o foco de entrada. Ela também não pode ser pai de outros controles. TGraphicControls são usados
quando você deseja exibir algo para o usuário no formulário, mas não deseja que esse controle funcione
como um controle normal de entrada do usuário. A vantagem de TGraphicControls é que eles não solicitam
uma alça do Windows que consuma recursos do sistema. Além disso, não ter uma alça de janela significa
que TGraphicControls não tem percorrer o atribulado processo de pintura do Windows. Isso torna o dese-
nho com TGraphicControls muito mais rápido do que usar os equivalentes de TWinControl.
TGraphicControls podem responder a eventos do mouse. Na verdade, o pai de TGraphicControl proces-
sa a mensagem do mouse e a envia para os controles filhos.
TGraphicControl permite que você pinte o controle e, portanto, fornece a propriedade Canvas, que é
do tipo TCanvas. TGraphicControl também fornece um método Paint( ) que seus descendentes devem modi-
ficar.
A classe TCustomControl
Você deve ter percebido que os nomes de alguns descendentes de TWinControl começam com TCustom,
como, por exemplo, TCustomComboBox, TCustomControl, TCustomEdit e TCustomListBox.
Os controles personalizados têm a mesma funcionalidade que outros descendentes de TWinControl,
exceto que, com características visuais e interativas especializadas, os controles personalizados oferecem
uma base a partir da qual pode derivar e criar seus próprios componentes personalizados. Você fornece a
funcionalidade para o controle personalizado desenhar a si mesmo, caso você seja um criador de compo-
nentes.
Outras classes
Diversas classes não são componentes, mas servem como classes de suporte para o componente existen-
te. Essas classes são em geral propriedades de outros componentes e descendem diretamente de TPersis-
tent. Algumas dessas classes são do tipo TStrings, TCanvas e TCollection.
TCustomListBox = class(TWinControl)
private
FItems: TStrings;
NOTA
Embora o tipo de classe mostrado no código acima seja TCustomListBox, TListBox descende diretamente
de TCustomListBox na mesma unidade e por essa razão tem acesso a seus campos privados.
A unidade StdCtrls.pas, que é parte da VCL do Delphi, define uma classe descendente de TListBox-
Strings,que é descendente de TStrings. A Listagem 20.1 mostra sua definição.
TListBoxStrings = class(TStrings)
private
ListBox: TCustomListBox;
protected
procedure Put(Index: Integer; const S: string); override;
function Get(Index: Integer): string; override;
function GetCount: Integer; override;
function GetObject(Index: Integer): TObject; override;
procedure PutObject(Index: Integer; AObject: TObject); override;
procedure SetUpdatessate(Updating: Boolean); override;
public
function Add(const S: string): Integer; override;
procedure Clear; override;
procedure Delete(Index: Integer); override;
procedure Exchange(Index1, Index2: Integer); override;
function IndexOf(const S: string): Integer; override;
procedure Insert(Index: Integer; const S: string); override;
procedure Move(CurIndex, NewIndex: Integer); override;
end;
Queremos deixar claro que, embora a classe TStrings defina seus métodos, ela não implementa esses
métodos para manipular strings. A classe descendente de TStrings faz a implementação desses métodos.
Isso é importante se você é um criador de componentes, pois tem que saber como executar essa técnica
do mesmo modo que os componentes do Delphi o fazem. É sempre bom fazer referência ao código-fonte
da VCL para ver como a Borland executa essas técnicas, quando não estiver convicto.
Se você não é um criador de componentes, mas quiser manipular uma lista de strings, pode usar a
classe TStringList, outro descendente de TStrings, com a qual você pode instanciar uma classe completa-
mente independente. TStringList mantém uma lista de strings externas para componentes. A melhor par-
te é que TStringList é totalmente compatível com TStrings. Isso significa que você pode atribuir direta-
mente uma instância de TStringList a uma propriedade TStrings do controle. O código a seguir mostra
como você pode criar uma instância de TStringList:
var
MyStringList: TStringList;
begin
MyStringList := TStringList.Create;
Se você quiser adicionar essas mesmas strings a um componente TMemo e a um componente TListBox,
tudo o que você tem que fazer é tirar proveito da compatibilidade entre as propriedades TStrings de dife-
rentes componentes e fazer cada uma das atribuições em uma linha de código:
Memo1.Lines.Assign(MyStringList);
ListBox1.Items.Assign(MyStringList);
Você usa o método Assign( ) para copiar instâncias de TStrings em vez de fazer uma atribuição dire-
ta, como, por exemplo, Memo1.Lines := MyStringList.
A Tabela 20.4 mostra alguns métodos comuns de classes Tstrings.
Add(const S: String): Integer Adiciona a string S à lista de strings e retorna a posição da string na lista.
AddObject(const S: string; Anexa uma string e um objeto a uma string ou a objeto da lista de
AObject: TObject): Integer strings.
AddStrings(Strings: TStrings) Copia strings de uma TStrings no final da lista de strings existente.
Assign(Source: TPersistent) Substitui as strings existentes pela que é especificada no parâmetro
Source.
Clear Remove todas as strings da lista.
Delete(Index: Integer) Remove a string na localização especificada por Index.
Exchange(Index1, Index2: Integer) Alterna a localização das duas strings especificadas pelos dois valores de
índice.
IndexOf(const S: String): Integer Retorna a posição da string S na lista.
468
Tabela 20.4 Continuação
A classe TCanvas
A propriedade Canvas, do tipo TCanvas, é fornecida por controles em janela e representa a superfície dese-
nhada do controle. TCanvas encapsula o que chamado de contexto de dispositivo de uma janela. Ela forne-
ce muitas das funções e dos objetos necessários ao desenho da superfície da janela. Para obter mais infor-
mações sobre a classe TCanvas, consulte o Capítulo 8.
Um uso típico para o operador as seria executar uma ação no parâmetro Sender de um manipulador
de evento, onde o manipulador está anexado a diversos componentes. Presumindo que você sabe que to-
dos os componentes são derivados de um ancestral comum cuja propriedade você deseja acessar, o mani-
pulador de evento pode usar o operador as para fazer com segurança um typecast de Sender como o des-
cendente desejado, revelando assim a propriedade desejada. Veja o exemplo a seguir:
procedure TForm1.ControlOnClickEvent(Sender: TObject);
var
i: integer;
begin
(Sender as TControl).Enabled := False;
end; 469
Esses exemplos de programação de tipo seguro ilustram as melhorias na linguagem Object Pascal que
utilizam indiretamente a RTTI. Agora vamos analisar um problema que pode advir do uso direto da RTTI.
Suponha que você tenha um formulário contendo componentes que sejam cientes dos dados (data
aware) e componentes que não sejam cientes dos dados. No entanto, você só precisa executar alguma
ação nos componentes cientes dos dados. Certamente, você poderia fazer um loop pelo array Components
do formulário e testar cada um dos tipos de componente ciente dos dados. No entanto, isso poderia ser
difícil de manter, pois você teria que fazer o teste em todos os tipos de componentes cientes de dados.
Além disso, você não tem uma classe básica para testar apenas o que é comum aos componentes cientes
dos dados. Por exemplo, alguma coisa como TDataAwareControl seria perfeito, desde que existisse.
Uma forma prática de determinar se um componente é ciente de dados é testar a existência de uma
propriedade DataSource. Para fazer isso, no entanto, você precisa usar a RTTI diretamente.
As próximas seções discutem a RTTI de um modo mais profundo, dando-lhe a base teórica necessá-
ria para resolver problemas como o que mencionamos anteriormente.
Por enquanto, queremos nos deter na função ClassInfo( ), que é definida da seguinte maneira:
class function ClassInfo: Pointer;
Essa função retorna um ponteiro para a RTTI da classe que chama. A estrutura à qual esse ponteiro faz
referência é do tipo PTypeInfo. Esse tipo é definido na unidade TypInfo.pas como um ponteiro para uma estru-
tura TTypeInfo. Ambas as definições são dadas no código a seguir do modo como aparecem em TypInfo.pas:
PPTypeInfo = ^PTypeInfo;
PTypeInfo = ^TTypeInfo;
TTypeInfo = record
Kind: TTypeKind;
Name: ShortString;
{TypeData: TTypeData}
end;
O campo comentado, TypeData, representa a referência real para a informação de tipo da classe
dada. O tipo ao qual ela realmente faz referência depende do valor do campo Kind. Kind pode ser qualquer
um dos valores enumerados definidos em TTypeKind:
TTypeKind = (tkUnknown, tkInteger, tkChar, tkEnumeration, tkFloat,
tkString, tkSet, tkClass, tkMethod, tkWChar, tkLString, tkWString,
470 tkVariant, tkArray, tkRecord, tkInterface);
Dê uma olhada na unidade TypInfo.pas nesse momento para examinar o subtipo de alguns dos valo-
res enumerados anteriores, a fim de familiarizar-se com eles. Por exemplo, o valor tkFloat pode ser des-
membrado da seguinte maneira:
TFloatType = (ftSingle, ftDouble, ftExtended, ftComp, ftCurr);
Agora você sabe que Kind determina o tipo a que TypeData faz referência. A estrutura TTypeData é defi-
nida em TypInfo.pas, como mostra a Listagem 20.2.
PTypeData = ^TTypeData;
TTypeData = packed record
case TTypeKind of
tkUnknown, tkLString, tkWString, tkVariant: ( );
tkInteger, tkChar, tkEnumeration, tkSet, tkWChar: (
OrdType: TOrdType;
case TTypeKind of
tkInteger, tkChar, tkEnumeration, tkWChar: (
MinValue: Longint;
MaxValue: Longint;
case TTypeKind of
tkInteger, tkChar, tkWChar: ( );
tkEnumeration: (
BaseType: PPTypeInfo;
NameList: ShortStringBase));
tkSet: (
CompType: PPTypeInfo));
tkFloat: (FloatType: TFloatType);
tkString: (MaxLength: Byte);
tkClass: (
ClassType: TClass;
ParentInfo: PPTypeInfo;
PropCount: SmallInt;
UnitName: ShortStringBase;
{PropData: TPropData});
tkMethod: (
MethodKind: TMethodKind;
ParamCount: Byte;
ParamList: array[0..1023] of Char
{ParamList: array[1..ParamCount] of
record
Flags: TParamFlags;
ParamName: ShortString;
TypeName: ShortString;
end;
ResultType: ShortString});
tkInterface: (
IntfParent : PPTypeInfo; { ancestral }
IntfFlags : TIntfFlagsBase;
Guid : TGUID;
IntfUnit : ShortStringBase;
{PropData: TPropData});
tkInt64: (
MinInt64Value, MaxInt64Value: Int64);
end;
471
Como você pode ver, a estrutura TTypeData é apenas um grande registro de variante. Se você está
acostumado a trabalhar com ponteiros e registros de variante, verá que é realmente simples lidar com a
RTTI. Ela só parece complexa, pois é um recurso que ainda não foi devidamente documentado.
NOTA
Freqüentemente, a Borland não documenta um recurso pelo fato de ele poder mudar de uma versão para
outra. Ao usar recursos como a RTTI não-documentada, observe que o seu código pode não ser plenamen-
te compatível de uma versão para outra do Delphi.
Nesse ponto, já estamos prontos para demonstrar como usar essas estruturas de RTTI para obter in-
formações de tipo.
unit MainFrm;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, ExtCtrls, DBClient, MidasCon, MConnect;
type
TMainForm = class(TForm)
pnlTop: TPanel;
pnlLeft: TPanel;
lbBaseClassInfo: TListBox;
spSplit: TSplitter;
lblBaseClassInfo: TLabel;
pnlRight: TPanel;
lblClassProperties: TLabel;
lbPropList: TListBox;
lbSampClasses: TListBox;
procedure FormCreate(Sender: TObject);
procedure lbSampClassesClick(Sender: TObject);
private
{ Declarações privadas }
public
{ Declarações públicas }
end;
var
MainForm: TMainForm;
implementation
472 uses TypInfo;
Listagem 20.3 Continuação
{$R *.DFM}
ClassTypeInfo := AClass.ClassInfo;
ClassTypeData := GetTypeData(ClassTypeInfo);
finally
FreeMem(PropList, SizeOf(PPropInfo) * ClassTypeData.PropCount);
end;
end;
end;
lbSampClasses.Items.Add(‘TListBox’);
lbSampClasses.Items.Add(‘TPaintBox’);
lbSampClasses.Items.Add(‘TMidasConnection’);
lbSampClasses.Items.Add(‘TFindDialog’);
lbSampClasses.Items.Add(‘TOpenDialog’);
lbSampClasses.Items.Add(‘TTimer’);
lbSampClasses.Items.Add(‘TComponent’);
lbSampClasses.Items.Add(‘TGraphicControl’);
end;
initialization
begin
RegisterClasses([TApplication, TButton, TForm, TListBox, TPaintBox,
TMidasConnection, TFindDialog, TOpenDialog, TTimer, TComponent,
TGraphicControl]);
end;
end.
Esse formulário principal contém três caixas de listagem. lbSampClasses contém nomes de classe de
alguns objetos de exemplo cujas informações de tipo vamos recuperar. Quando um objeto é selecionado
em lbSampClasses, lbBaseClassInfo é preenchido com informações básicas sobre o objeto selecionado,
como, por exemplo, seu tamanho e seu ancestral. lbPropList exibirá as propriedades pertencentes ao ob-
jeto selecionado de lbSampClasses.
Três procedimentos auxiliadores são usados para obter informações de classe:
l preenche uma lista de strings com informações básicas sobre um objeto,
GetBaseClassInfo( )
como, por exemplo, seu tipo, seu tamanho, sua unidade de definição e seu número de proprie-
dades.
l GetClassAncestry( ) preenche uma lista de strings com os nomes de objetos do ancestral de um
dado objeto.
l GetClassProperties( ) preenche uma lista de strings com as propriedades e seus tipos de uma de-
terminada classe.
Cada procedimento utiliza uma instância de objeto e uma lista de strings como parâmetros.
Quando o usuário seleciona uma das classes de lbSampClasses, seu evento OnClick, lbSampClasses- 475
Click( ),chama uma função auxiliadora, CreateAClass( ), que cria uma instância de uma classe dado o
nome do tipo de classe. Em seguida, passa a instância do objeto e a propriedade TListBox.Items apropriada
a ser preenchida.
DICA
A função CreateAClass( ) pode ser usada para criar qualquer classe pelo seu nome. No entanto, conforme
demonstrado, você deve se certificar de que qualquer classe passada para ela tenha sido registrada cha-
mando o procedimento RegisterClasses( ).
DICA
Use a função GetTypeData( ) definida em TypInfo.pas para retornar um ponteiro para a estrutura TTypeIn-
fo de uma classe dada. Você deve passar o resultado de TObject.ClassInfo( ) para GetTypeData( ).
DICA
Você pode usar a função GetEnumName( ) para obter o nome de um valor de enumeração como uma string.
GetEnumValue( ) retorna o valor de enumeração, dado o seu nome.
Isso é usado para filtrar as propriedades que são eventos (ponteiros de método). Preenchemos essas
últimas propriedades no fim, o que nos permite demonstrar um método alternativo para recuperar a
RTTI da propriedade. Na parte final do método GetClassProperties( ), usamos a função GetPropList( )
para retornar a TPropList das propriedades de um tipo específico. Nesse caso, só queremos propriedades
do tipo tkMethod. GetPropList( ) também é definida em TypInfo.pas. Para obter mais informações, consulte
o comentário-fonte.
DICA
Use GetPropInfos( ) quando quiser recuperar um ponteiro para a RTTI da propriedade de todas as pro-
priedades de um objeto dado. Use GetPropList( ) se quiser recuperar as mesmas informações, exceto as
propriedades de um tipo específico.
A Figura 20.2 mostra a saída do formulário principal com a RTTI de uma classe selecionada.
Aqui, estamos usando a função GetPropInfo( ) para retornar o ponteiro TPropInfo em um determina-
da propriedade. Essa função retorna nil se a propriedade não existir. Só por garantia, certificamo-nos de
que a propriedade DataSource é descendente de TDataSource.
Também poderíamos ter escrito essa função de modo mais genérico para verificar a existência de
uma propriedade qualquer pelo seu nome, como no exemplo a seguir:
function HasProperty(AComponent: TComponent; APropertyName: String): Boolean;
var
PropInfo: PPropInfo;
begin
PropInfo := GetPropInfo(AComponent.ClassInfo, APropertyName);
Result := PropInfo < > nil;
end;
Observe, no entanto, que isso só funciona em propriedades que são publicadas. A RTTI não existe
para propriedades não-publicadas.
unit MainFrm;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, ExtCtrls, DBClient, MidasCon, MConnect;
type
478
Listagem 20.4 Continuação
TMainForm = class(TForm)
lbSampMethods: TListBox;
lbMethodInfo: TMemo;
lblBasicMethodInfo: TLabel;
procedure FormCreate(Sender: TObject);
procedure lbSampMethodsClick(Sender: TObject);
private
{ Declarações privadas }
public
{ Declarações públicas }
end;
var
MainForm: TMainForm;
implementation
uses TypInfo, DBTables, Provider;
{$R *.DFM}
type
// É preciso redefinir esse registro por ter sido passado para
// comentário em typinfo.pas.
PParamRecord = ^TParamRecord;
TParamRecord = record
Flags: TParamFlags;
ParamName: ShortString;
TypeName: ShortString;
end;
MethodTypeData: PTypeData;
MethodDefine: String;
ParamRecord: PParamRecord;
TypeStr: ^ShortString;
ReturnStr: ^ShortString;
i: integer;
begin
MethodTypeData := GetTypeData(ATypeInfo);
480
Listagem 20.4 Continuação
with lbSampMethods do
begin
GetBaseMethodInfo(PTypeInfo(Items.Objects[ItemIndex]), lbMethodInfo.Lines);
GetMethodDefinition(PTypeInfo(Items.Objects[ItemIndex]),
lbMethodInfo.Lines);
end;
end;
end.
Na Listagem 20.4, preenchemos uma caixa de listagem, lbSampMethods, com alguns nomes de método
de exemplo. Também armazenamos as referências aos dados da RTTI desses métodos na array Objects da
caixa de listagem. Fazemos isso usando a função TypeInfo( ), que é uma função especial que pode recupe-
rar um ponteiro para a RTTI de um identificador de tipo de dados. Quando o usuário seleciona um des-
ses métodos, usamos esses dados da RTTI da array Objects para recuperar e reconstruir a definição de mé-
todo das informações que temos sobre o método e seus parâmetros nos dados da RTTI. Para obter mais
informações, consulte o comentário da listagem. A Figura 20.3 mostra a saída desse formulário quando
um método é selecionado.
DICA
Use a função TypeInfo( ) para recuperar um ponteiro para a RTTI gerada pelo compilador de um identifi-
cador de tipo indicado. Por exemplo, a linha a seguir recupera um ponteiro para a RTTI para o tipo Tbutton:
TypeInfoPointer := TypeInfo(TButton);
TypeNameStr: String;
TypeKindStr: String;
MinVal, MaxVal: Integer;
begin
memInfo.Lines.Clear;
with lbSamps do
begin
Aqui, usamos a função TypeInfo( ) para obter um ponteiro para a estrutura TTypeInfo do tipo de da-
dos Integer. Em seguida, passamos essa referência para a função GetTypeData( ) para obter um ponteiro
para a estrutura TTypeData. Usamos essas estruturas para preencher uma caixa de listagem com a RTTI do
integer. Para obter mais informações, consulte a demonstração chamada IntegerRTTI.dpr no diretório re-
ferente a esse capítulo no CD-ROM que acompanha este livro.
TypeNameStr: String;
TypeKindStr: String;
MinVal, MaxVal: Integer;
i: integer;
begin
memInfo.Lines.Clear;
with lbSamps do
begin
end;
end;
end;
Você encontrará uma demonstração mais detalhada, cujo nome é EnumRTTI.dpr, no CD-ROM que
acompanha essa edição, no diretório referente a este capítulo.
484
Informações de tipo para tipos de conjunto
A obtenção da RTI de tipos de conjunto (set) é apenas um pouco mais complexa do que as duas técnicas
anteriores. A Listagem 20.7 é o formulário principal do projeto SetRTTI.dpr, que você encontrará no
CD-ROM que acompanha este livro, no diretório referente a este capítulo.
unit MainFrm;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, Grids;
type
TMainForm = class(TForm)
lbSamps: TListBox;
memInfo: TMemo;
procedure FormCreate(Sender: TObject);
procedure lbSampsClick(Sender: TObject);
private
{ Declarações privadas }
public
{ Declarações públicas }
end;
var
MainForm: TMainForm;
implementation
uses TypInfo, Buttons;
{$R *.DFM}
TypeNameStr: String;
TypeKindStr: String;
MinVal, MaxVal: Integer; 485
Listagem 20.7 Continuação
i: integer;
begin
end;
Esse procedimento utiliza três parâmetros. O primeiro, AComp, é o componente cuja propriedade
você deseja modificar. O segundo parâmetro, APropName, é o nome da propriedade à qual você desejaria
atribuir o valor do terceiro parâmetro, AValue. Esse procedimento usa a função GetPropInfo( ) para recu-
perar o ponteiro TPropInfo na propriedade especificada. GetPropInfo( ) retornará nil se a propriedade não
existir. Se a propriedade existir, a segunda cláusula if determina se a propriedade é do tipo correto. O
tipo de propriedade tkInteger é definido na unidade TypInfo.pas juntamente com outros possíveis tipos de
propriedade, como se vê a seguir:
Mais uma vez, há muitas outras rotinas auxiliadoras que você achará úteis em TypInfo.pas.
O código a seguir mostra como atribuir uma propriedade de objeto:
procedure SetObjectPropertyIfExists(AComponent: TComponent; APropName: String;
AValue: TObject);
var
PropInfo: PPropInfo;
begin
PropInfo := GetPropInfo(AComponent.ClassInfo, APropName);
if PropInfo < > nil then
begin
if PropInfo^.PropType^.Kind = tkClass then
SetObjectProp(AComponent, PropInfo, AValue);
end;
end;
Esse método requer o uso do tipo TMethod, que é definido na unidade SysUtils.pas. Para chamar esse
método para atribuir um manipulador de evento de um componente para outro, você pode usar GetMethod-
Prop para recuperar o valor de TMethod do componente de origem, conforme o código a seguir.
SetMethodPropertyIfExists(Button5, ‘OnClick’,
GetMethodProp(Panel1, ‘OnClick’));
O CD-ROM que acompanha este livro inclui um projeto, SetProperties.dpr, que demonstra essas ro-
tinas.
Resumo
Este capítulo apresentou a VCL (Visual Component Library). Discutimos a hierarquia da VCL e as carac-
terísticas especiais de componentes em diferentes níveis na hierarquia. Também analisamos a RTTI em
profundidade. Este capítulo serve como uma preparação para os próximos capítulos, nos quais analisare-
mos a criação de componente.
489
Escrita de componentes CAPÍTULO
personalizados do Delphi
21
NE STE C AP ÍT UL O
l Fundamentos da criação de componentes 491
l Componentes de exemplo 513
l TddgButtonEdit – componentes contêiner 528
l Pacotes de componentes 536
l Pacotes de add-ins 545
l Resumo 551
A capacidade de criar facilmente componentes personalizados no Delphi 5 é a principal vantagem no
campo da produtividade que você pode obter em relação aos outros programadores. Na maioria dos ou-
tros ambientes, as pessoas se vêem usando os controles-padrão disponíveis através do Windows ou, por
outro lado, têm que usar um conjunto de controles, diferentes e complexos, desenvolvido por outra
pessoa. Ser capaz de incorporar seus componentes personalizados nas aplicações do Delphi significa
que você tem o total controle sobre a interface do usuário da aplicação. Os controles personalizados dão
a você a última palavra em relação à aparência e ao comportamento de sua aplicação.
Se você tem um fraco por projeto de componente, vai gostar das informações que este capítulo tem
a oferecer. Você vai aprender todos os aspectos de projeto de componente, do conceito à integração no
ambiente Delphi. Você também vai aprender sobre as armadilhas de projeto de componente, bem como
dicas e macetes para desenvolver componentes altamente funcionais e passíveis de expansão.
Mesmo que seu principal interesse seja desenvolvimento de aplicação e não projeto de componen-
te, você vai tirar bastante proveito desde capítulo. A incorporação de um ou dois componentes personali-
zados em seus programas é uma forma ideal de tornar mais agradável e de melhorar a produtividade de
suas aplicações. Invariavelmente, você vai se ver diante de uma situação em que, de todos os componen-
tes de que dispuser para criar uma aplicação, nenhum deles será suficientemente satisfatório para uma ta-
refa em particular. É aí que entra em cena o projeto de componente. Você será capaz de ajustar um com-
ponente às suas reais necessidades e, com a graça do bom Deus, de projetá-lo com a necessária inteligên-
cia para que ele possa ser usado nas próximas aplicações.
Neste capítulo, vamos discutir as cinco primeiras etapas; no entanto, está fora do escopo deste capí-
tulo a discussão sobre os arquivos de ajuda. No entanto, isso não significa que essa etapa seja menos im-
portante que as demais. Recomendamos que você analise algumas das ferramentas de terceiros disponí-
veis, que simplificam a criação de arquivos de ajuda. Além disso, a Borland fornece informações sobre
como fazer isso em sua ajuda on-line. Para obter mais informações, consulte “Providing Help for Your
Component” (oferecendo ajuda para o seu componente), na ajuda on-line.
492
Tabela 21.1 Classes da VCL como classes baseadas em componente
TObject Embora as classes que descedem diretamente de TObject não sejam componentes no
sentido estrito da palavra, elas são dignas de menção. Você usará TObject como uma
classe básica para muitas coisas com as quais você não precisa trabalhar durante o
projeto. Um bom exemplo é o objeto TIniFile.
TComponent Um ponto de partida para muitos componentes não-visuais. Seu forte é que ela
oferece capacidade de streaming interna para carregar e salvar a si mesma no IDE
durante o projeto.
TGraphicControl Use esta classe quando quiser criar um componente personalizado que não tenha alça
de janela. Os descendentes de TGraphicControl são desenhados na superfície do cliente
dos seus pais e, portanto, não sobrecarregam a máquina.
TWinControl Essa é a classe básica para todos os componentes que usam uma alça de janela. Ela
fornece propriedades e eventos comuns, específicos aos controles usados em
interfaces gráficas.
TCustomControl Essa classe descende de TWinControl. Ela introduz os conceitos de uma tela de desenho
(canvas) e um método Paint( ) para dar mais controle sobre a aparência do
componente. Use essa classe para a maioria dos componentes personalizados com alça
de janela.
TCustomClassName A VCL contém diversas classes que não publicam todas as suas propriedades; elas
delegam tal tarefa para as classes descendentes. Isso permite que os criadores de
componentes criem componentes “personalizados” da mesma classe básica e
publiquem apenas as propriedades predefinidas exigidas pela classe personalizada.
TComponentName Uma classe existente, como TEdit, TPanel ou TScrollBox. Use um componente já
estabelecido como uma classe básica para a sua classe (como por exemplo TEdit) e
componentes personalizados quando você quiser estender o comportamento de um
controle, em vez de criar um novo a partir do nada. Muitos dos seus componentes
personalizados se enquadrarão nessa categoria.
É extremamente importante que você entenda essas várias classes e também as capacidades dos
componentes existentes. Na maioria das vezes, você perceberá que um componente existente já forne-
ce a maioria da funcionalidade de que você precisa para o seu novo componente. Só se souber as capa-
cidades de componentes existentes é que você poderá definir de qual componente derivará o novo
componente. Não podemos injetar esse conhecimento no seu cérebro a partir deste livro. O que pode-
mos fazer é dizer que você deve fazer o máximo de esforço para aprender sobre cada componente e
classe dentro da VCL do Delphi, e a única forma de fazer isso é usá-la, mesmo que seja apenas de modo
experimental.
unit Worthless;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs;
type
TddgWorthless = class(TCustomControl)
private
{ Declarações privadas }
protected
{ Declarações protegidas }
public
{ Declarações públicas }
published
{ Declarações publicadas }
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents(‘DDG’, [TddgWorthless]);
end;
end.
Você pode ver que, neste ponto, TddgWorthless não passa do esqueleto de um componente. Nas pró-
ximas seções, você adicionará propriedades, métodos e eventos a TddgWorthless.
Criando propriedades
O Capítulo 20 discute o uso e as vantagens do uso de propriedades com os seus componentes. Esta seção
mostra como adicionar os diversos tipos de propriedades a seus componentes.
494
Tipos de propriedades
A Tabela 20.1 do Capítulo 20 lista os diversos tipos de propriedades. Vamos adicionar propriedades de
cada um desses tipos ao componente TddgWorthless para ilustrar as diferenças entre cada tipo. Cada tipo
diferente de propriedade é editado de um modo ligeiramente diferente no Object Inspector. Você vai
examinar cada um desses tipos e como eles são editados.
TddgWorthless = class(TCustomControl)
private
// Armazenamento de dados internos
FIntegerProp: Integer;
FStringProp: String;
FCharProp: Char;
published
// Tipos de propriedades simples
property IntegerProp: Integer read FIntegerProp write FIntegerProp;
property StringProp: String read FStringProp write FStringProp;
property CharProp: Char read FCharProp write FCharProp;
end;
Você já deve estar familiarizado com a sintaxe usada aqui, pois ela foi discutida no Capítulo 20.
Aqui, você tem seu armazenamento de dados internos para o componente declarado na seção private. As
propriedades que fazem referência a esses campos de armazenamento são declaradas na seção published,
o que significa que, quando você instala o componente no Delphi, pode editar as propriedades no Object
Inspector.
NOTA
Durante a criação de componentes, a convenção é fazer os nomes de campo privado começarem com a
letra F. Para componentes e tipos em geral, dê ao objeto ou tipo um nome que comece com a letra T. O có-
digo será muito mais claro se você seguir essas convenções simples.
TddgWorthless = class(TCustomControl)
private
// Tipos de dados enumerados
FEnumProp: TEnumProp;
FBooleanProp: Boolean;
published
property EnumProp: TEnumProp read FEnumProp write FEnumProp;
property BooleanProp: Boolean read FBooleanProp write FBooleanProp;
end;
Excluímos as outras propriedades para facilitar a compreensão do nosso exemplo. Se você fosse ins-
talar esse componente, suas propriedades enumeradas apareceriam no Object Inspector, como mostra a
Figura 21.2.
Aqui, você primeiro define uma faixa para o conjunto, definindo um tipo enumerado TSetPropOpti-
on.Em seguida, você define o conjunto TSetPropOptions.
Agora você pode adicionar uma propriedade de TSetPropOptions para o componente TddgWorthless da
seguinte maneira:
TddgWorthless = class(TCustomControl)
private
FOptions: TSetPropOptions;
published
property Options: TSetPropOptions read FOptions write FOptions;
end;
A Figura 21.3 mostra a aparência que essa propriedade tem quando expandida no Object Inspector.
496
FIGURA 21.3 A propriedade de conjunto no Object Inspector.
TSomeObject = class(TPersistent)
private
FProp1: Integer;
FProp2: String;
public
procedure Assign(Source: TPersistent)
published
property Prop1: Integer read FProp1 write FProp1;
property Prop2: String read FProp2 write FProp2;
end;
A classe TSomeObject descende diretamente de TPersistent, embora tal situação não seja obrigatória.
Como o objeto do qual a nova classe descende é um descendente de TPersistent, ele pode ser usado como
outra propriedade do objeto.
Demos a essa classe duas propriedades retiradas dela mesma: Prop1 e Prop2, que são tipos de proprie-
dade simples. Também adicionamos um procedimento, Assign( ), a TSomeObject, que discutiremos logo a
seguir.
Agora você pode adicionar um campo do tipo TSomeObject ao componente TddgWorthless. No entanto,
como essa propriedade é um objeto, ela deve ser criada. Caso contrário, quando o usuário colocar um
componente TddgWorthless no formulário, não haverá uma instância de TSomeObject que o usuário possa
editar. Portanto, é preciso modificar o construtor Create( ) para TddgWorthless criar uma instância de TSo-
meObject. A Listagem 21.5 mostra a declaração de TddgWorthless com sua nova propriedade de objeto.
TddgWorthless = class(TCustomControl)
private
FSomeObject: TSomeObject;
procedure SetSomeObject(Value: TSomeObject); 497
Listagem 21.5 Continuação
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
published
property SomeObject: TSomeObject read FSomeObject write SetSomeObject;
end;
Observe que incluímos o construtor Create( ) e o destruidor Destroy( ) modificados. Além disso, ob-
serve que declaramos um método de acesso de escrita, SetSomeObject( ), para a propriedade SomeObject.
Um método de acesso de escrita normalmente é chamado de método definidor. Os métodos de acesso de
leitura são chamados de métodos captadores. Se você se lembra do Capítulo 20, os métodos de escrita de-
vem ter um parâmetro do mesmo tipo que a propriedade à qual pertencem. Por convenção, o nome do
método de escrita geralmente começa com Set.
Definimos o construtor TddgWorthless.Create( ) da seguinte maneira:
constructor TddgWorthless.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FSomeObject := TSomeObject.Create;
end;
Agora que mostramos como criar a instância de TSomeObject, considere o que aconteceria se, em run-
time, o usuário executasse o código a seguir:
var
MySomeObject: TSomeObject;
begin
MySomeObject := TSomeObject.Create;
ddgWorthless.SomeObjectj := MySomeObject;
end;
Se você se lembra do Capítulo 2, as instâncias de objeto são na verdade referências de ponteiro para
o objeto propriamente dito. Quando você faz uma atribuição, como mostramos no exemplo anterior,
está se referindo ao ponteiro para outra instância de objeto, enquanto a instância de objeto anterior se
mantém no lugar. Enquanto projeta componentes, você não pretende impor condições para os usuários
que forem acessar as propriedades. Para evitar essa armadilha, você simplifica ao máximo o componente,
498 criando métodos de acesso para propriedades que são objetos. Esses métodos de acesso podem garantir
que os recursos não sejam perdidos quando o usuário atribui novos valores a essas propriedades. Essa é a
função do método de acesso para SomeObject, como se pode ver a seguir:
procedure TddgWorthLess.SetSomeObject(Value: TSomeObject);
begin
if Assigned(Value) then
FSomeObject.Assign(Value);
end;
Em TSomeObject.Assign( ), você primeiro garante que o usuário passou uma instância válida de TSo-
meObject. Nesse caso, você em seguida copia os valores de propriedade de Source de modo adequado. Isso
ilustra outra técnica que você verá em toda a VCL para atribuir objetos para outros objetos. Se você tiver
o código-fonte da VCL, deve dar uma olhada nos diversos métodos Assign( ), como, por exemplo, TBrush
e TShape, para ver como eles são implementados. Isso lhe daria algumas idéias sobre como distribuí-los em
seus componentes.
ATENÇÃO
Nunca faça uma atribuição a uma propriedade no método de escrita da propriedade. Por exemplo, exami-
ne a declaração de propriedade a seguir:
Por estar acessando a própria propriedade (não o campo de armazenamento interno), você fará com que o
método SetSomeProp( ) seja chamado de novo, o que resulta em um loop infinito. Em algum momento, o
programa dará pau, com um estouro de pilha. Sempre acesse o campo de armazenamento interno no mé-
todo de escrita de uma propriedade.
ShowMessage(ddgPlanets.PlanetName[8]);
Compare a diferença quando a sentença From the sun, Neptune is planet number: 8 é gerada a partir da
seguinte instrução:
ShowMessage(‘From the sun, Neptune is planet number: ‘+
IntToStr(ddgPlanets.PlanetPosition[‘Neptune’]));
unit planets;
interface
uses
Classes, SysUtils;
type
TddgPlanets = class(TComponent)
private
// Métodos de acesso da propriedade de array
function GetPlanetName(const AIndex: Integer): String;
function GetPlanetPosition(const APlanetName: String): Integer;
public
{ Propriedade Array indexada por um valor inteiro. Essa será a propriedade array padrão. }
property PlanetName[const AIndex: Integer]: String
read GetPlanetName; default;
// Índice da propriedade de array por um valor de string
property PlanetPosition[const APlanetName: String]: Integer
500
Listagem 21.6 Continuação
read GetPlanetPosition;
end;
implementation
const
// Declara um array constante contendo nomes de planetas
PlanetNames: array[1..9] of String[7] =
(‘Mercury’, ‘Venus’, ‘Earth’, ‘Mars’, ‘Jupiter’, ‘Saturn’,
‘Uranus’, ‘Neptune’, ‘Pluto’);
end.
Esse componente dá uma idéia de como você cria uma propriedade array, com um inteiro e uma
string sendo usados como um índice. Observe como o valor retornado da leitura do valor da propriedade
é baseado no valor de retorno da função, e não no valor de um campo de armazenamento, como é o caso
com as outras propriedades. Você pode consultar os comentários do código para obter explicação adi-
cional sobre esse componente.
Valores-padrão
Você pode dar um valor-padrão a uma propriedade atribuindo um valor à propriedade no construtor do
componente. Portanto, se adicionarmos a instrução a seguir ao construtor do componente TddgWorthless,
sua propriedade FIntegerProp seria 100 como padrão quando o componente fosse colocado pela primeira
vez no formulário: 501
FIntegerProp := 100;
Esse é provavelmente o melhor lugar para mencionar as diretivas Default e NoDefault para declara-
ções de propriedade. Se você tivesse olhado o código-fonte da VCL do Delphi, provavelmente teria per-
cebido que algumas declarações de propriedade contêm a diretiva Default, como é o caso com a proprie-
dade TComponent.FTag:
property Tag: Longint read FTag write FTag default 0;
Não confunda essa instrução com o valor-padrão especificado no construtor do componente, que
de fato define o valor da propriedade. Por exemplo, altere a declaração da propriedade IntegerProp para o
componente TddgWorthless para que fique da seguinte maneira:
property IntegerProp: Integer read FIntegerProp write FIntegerProp default 100;
Essa instrução não define o valor da propriedade como 100. Isso afeta apenas se o valor da proprie-
dade será salvo ou não quando você salvar um formulário contendo o componente TddgWorthless. Se o va-
lor de IntegerProp não for 100, o valor será salvo com o arquivo DFM. Caso contrário, não será salvo (pois
100 é o valor da propriedade em um objeto recém-construído antes de ler suas propriedades no stream).
É recomendado que você use a diretiva Default sempre que possível, pois ela pode agilizar o tempo de car-
ga de seus formulários. É importante que a diretiva Default não defina o valor da propriedade. Você deve
fazer isso no construtor do componente, como mostramos anteriormente.
A diretiva NoDefault é usada para declarar novamente uma propriedade que especifique um valor-
padrão, de modo que ela sempre será escrita no stream, independentemente do seu valor. Por exemplo,
você pode redeclarar seu componente para não especificar um valor-padrão para a propriedade Tag:
TSample = class(TComponent)
published
property Tag NoDefault;
Observe que você nunca deve declarar alguma coisa NoDefault, a não ser que tenha uma razão especí-
fica para fazer isso. Um exemplo desse tipo de propriedade é TForm.PixelsPerInch, que sempre deve ser ar-
mazenada de modo que o dimensionamento venha a funcionar de modo adequado em runtime. Além
disso, as propriedades de tipo string, ponto flutuante e int64 não podem declarar valores-padrão.
Para mudar o valor-padrão de uma propriedade, você a redeclara usando o novo valor-padrão (mas
não métodos de leitura ou escrita).
Somente uma propriedade de array pode ser declarada para um objeto, e ela não pode ser modifica-
da nos descendentes.
Criando eventos
No Capítulo 20, apresentamos eventos e dissemos que os eventos eram propriedades especiais vincula-
das a código que são executados todas as vezes que ocorre uma determinada ação. Nesta seção, vamos
discutir eventos de modo mais detalhado. Vamos mostrar como os eventos são gerados e como você
502 pode definir suas próprias propriedades de evento para seus componentes personalizados.
De onde vêm os eventos?
A definição geral de um evento é basicamente qualquer tipo de ocorrência que pode resultar da interação
do usuário, do sistema ou de um código lógico. O evento é vinculado a algum código que responde a essa
ocorrência. A vinculação do evento ao código é chamado de propriedade de evento e é fornecida na for-
ma de um ponteiro de método. O método ao qual uma propriedade de evento aponta é chamado de ma-
nipulador de evento.
Por exemplo, quando o usuário dá um clique no botão do mouse, uma mensagem WM_MOUSEDOWN é en-
viada para o sistema Win32. O Win32 passa essa mensagem ao controle para o qual essa mensagem se
destina. Esse controle pode responder a essa mensagem em seguida. O controle pode responder a esse
evento primeiro verificando se há um código a ser executado. Ele faz isso verificando se a propriedade
de evento aponta para algum código. Sendo assim, ele executa esse código, ou melhor, o manipulador de
evento.
O evento OnClick é apenas uma das propriedades de evento padrão definidas pelo Delphi. OnClick e
outras propriedades de evento têm, cada uma, um método de disparo de evento. Esse método é geralmen-
te um método protegido do componente ao qual pertence. Esse método executa a lógica para determinar
se a propriedade de evento faz referência a qualquer código fornecido pelo usuário do componente. No
caso da propriedade OnClick, isso seria o método Click( ). Tanto a propriedade OnClick como o método
Click( ) são definidos por TControl da seguinte maneira:
TControl = class(TComponent)
private
FOnClick: TNotifyEvent;
protected
procedure Click; dynamic;
property OnClick: TNotifyEvent read FOnClick write FOnClick;
end;
Uma informação essencial que você deve entender é que as propriedades de evento são, na verdade,
ponteiros de método. Observe que a propriedade FOnClick é definida para ser TNotifyEvent. TNotifyEvent é
definido da seguinte maneira:
TNotifyEvent = procedure(Sender: TObject) of object;
Isso diz que TNotifyEvent é um procedimento que utiliza um parâmetro, Sender, que é do tipo TObject.
A diretiva, of object, é o que faz esse procedimento se tornar um método. Isso significa que um parâmetro
implícito adicional que você não vê na lista de parâmetros também é passado para esse procedimento.
Esse é o parâmetro Self que faz referência ao objeto ao qual esse método pertence. Quando o método
Click( ) de um componente é chamado, ele verifica se FOnClick de fato aponta para um método e, nesse
caso, chama esse método.
Como um criador de componente, você escreve todo o código que define seu evento, sua proprie-
dade de evento e seus métodos de disparo. O usuário do componente fornecerá o manipulador de evento
quando ele usar seu componente. Seu método de disparo de evento verifica se o usuário atribuiu qual-
quer código a sua propriedade de evento e em seguida o executa, quando existe código.
No Capítulo 20, discutimos como os manipuladores de evento são atribuídos às propriedades de
evento em runtime ou durante o projeto. Na próxima seção, mostramos como criar seus próprios even-
tos, propriedades de eventos e métodos de disparo.
503
Definindo propriedades de evento
Antes de você definir uma propriedade de evento, é preciso determinar se ela precisa de um tipo de even-
to especial. É importante familiarizar-se com as propriedades de evento comuns que existem na VCL do
Delphi. Na maioria das vezes, você será capaz de fazer com que o componente descenda de um dos com-
ponentes existentes e use suas propriedades de evento ou de poder trazer à tona uma propriedade de
evento protegida. Se você perceber que nenhum dos eventos existentes atende às suas necessidades, pode
definir o seu próprio evento.
Como um exemplo, considere o seguinte cenário. Suponha que você deseje um componente que
contém um evento chamado a cada meio minuto, com base no clock do sistema. Ou seja, ele é invocado
em cima do minuto e do minuto e meio. Bem, você certamente pode usar um componente TTimer para ve-
rificar a hora do sistema e em seguida executar alguma ação sempre que a hora estiver em cima do minu-
to ou do minuto e meio. No entanto, você pode querer incorporar esse código em seu próprio compo-
nente e, em seguida, tornar esse componente disponível para os seus usuários. Assim, tudo o que eles têm
a fazer é adicionar código ao seu evento OnHalfMinute.
O componente TddgHalfMinute, mostrado na Listagem 21.7, demonstra como você projetaria um
componente desse tipo. Mais importante, ela mostra como você deve agir para criar seu próprio tipo de
evento.
unit halfmin;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs, ExtCtrls;
type
{ Define um procedimento para o manipulador de evento. A propriedade do
evento será desse tipo de procedimento. Esse tipo pegará dois parâmetros,
o objeto que invocou o evento e um valor TDateTime para representar a
hora em que o evento ocorreu. Para o nosso componente, isso será a cada
meio minuto. }
TTimeEvent = procedure(Sender: TObject; TheTime: TDateTime) of object;
TddgHalfMinute = class(TComponent)
private
FTimer: TTimer;
{ Define um campo de armazenamento para apontar para o manipulador de
evento do usuário. O manipulador de evento do usuário deve ser do tipo
de procedimento TTimeEvent. }
FOnHalfMinute: TTimeEvent;
FOldSecond, FSecond: Word; // Variáveis usadas no código
{ Define um procedimento, FTimerTimer, que será atribuído a
FTimer.OnClick. Esse procedimento deve ser do tipo TNotifyEvent
que é o tipo de TTimer.OnClick. }
procedure FTimerTimer(Sender: TObject);
protected
{ Define o método de despacho para o evento OnHalfMinute. }
procedure DoHalfMinute(TheTime: TDateTime); dynamic;
public
504 constructor Create(AOwner: TComponent); override;
Listagem 21.7 Continuação
implementation
destructor TddgHalfMinute.Destroy;
begin
FTimer.Free;
inherited Destroy;
end;
DoOnHalfMinute. }
if FSecond < > FOldSecond then
if ((FSecond = 30) or (FSecond = 0)) then
DoHalfMinute(DT)
end;
end.
Durante a criação de seus próprios eventos, você deve determinar quais as informações que deseja
fornecer para os usuários do seu componente como um parâmetro no manipulador de evento. Por exem-
plo, quando você cria um manipulador de evento para o evento TEdit.OnKeyPress, seu manipulador de
evento pode se parecer com o código a seguir:
procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
begin
end;
Não apenas você obtém uma referência ao objeto que causou o evento, mas também um parâmetro
Char especificando a tecla que foi pressionada. No contexto da VCL do Delphi, esse evento ocorreu como
um resultado de uma mensagem WM_CHAR do Win32, que traz consigo algumas informações adicionais re-
lacionadas à tecla pressionada. O Delphi cuida da extração dos dados necessários e de disponibilizar os
mesmos para os usuários do componente como parâmetros do manipulador de evento. Uma das coisas
interessantes sobre o esquema como um todo é que ele permite que os criadores de componentes peguem
informações que podem ser de difícil compreensão e as torne disponíveis para os usuários de componen-
te em um formato muito mais compreensível e fácil de usar.
Observe o parâmetro var no método Edit1KeyPress( ), apresentado acima. Você pode se perguntar
por que esse método não foi declarado como uma função que retorna um tipo Char em vez de um proce-
dimento. Embora os tipos de método possam ser funções, você não deve declarar eventos como funções
porque isso traria consigo a ambigüidade; quando faz referência a um ponteiro de método que é uma
função, você não pode saber se está fazendo referência ao resultado das funções ou ao valor do ponteiro
da função propriamente dito. A propósito, há um evento de função na VCL que passou pelos programa-
dores na época do Delphi 1 e que agora não tem mais como ser descartado. Esse é o evento TApplicati-
on.OnHelp.
Observando a Listagem 21.7, você verá que definimos o tipo de procedimento TOnHalfMinute da se-
guinte maneira:
TTimeEvent = procedure(Sender: TObject; TheTime: TDateTime) of object;
Esse tipo de procedimento define o tipo do manipulador de evento OnHalfMinute. Aqui, decidimos
que o usuário tem uma referência ao objeto fazendo o evento ocorrer e o valor TDateTime de quando o
evento ocorreu.
O campo de armazenamento FOnHalfMinute é a referência ao manipulador de evento do usuário e é
trazido para o Object Inspector durante o projeto através da propriedade OnHalfMinute.
A funcionalidade básica do componente usa um objeto TTimer para verificar o valor de segundos a
506 cada meio segundo. Se o valor de segundos for 0 ou 30, ele chama o método DoHalfMinute( ), que é res-
ponsável pela verificação da existência de um manipulador de evento e em seguida pela chamada do mes-
mo. Isso, em parte, é explicado nos comentários do código, nos quais você deve dar pelo menos uma
olhadinha rápida.
Depois da instalação desse componente na Component Palette do Delphi, você pode colocar o
componente no formulário e adicionar o seguinte manipulador de evento ao evento OnHalfMinute:
procedure TForm1.ddgHalfMinuteHalfMinute(Sender: TObject; TheTime: TDateTime);
begin
ShowMessage(‘The Time is ‘+TimeToStr(TheTime));
end;
Isso deve ilustrar como o tipo de evento recém-definido se torna um manipulador de evento.
Criando métodos
A inclusão de métodos aos componentes é um processo semelhante ao da adição de métodos a outros ob-
jetos. No entanto, há algumas orientações que você sempre deve levar em consideração durante o proje-
to de componentes.
Sem interdependências!
Uma das metas-chave por trás da criação de componentes é simplificar o uso do componente para o
usuário final. Por essa razão, você vai querer evitar ao máximo possível interdependências de método.
Por exemplo, você nunca vai querer forçar o usuário a ter que chamar um método particular para usar o
componente, e os métodos nunca têm que ser chamados em uma determinada ordem. Além disso, os mé-
todos chamados pelo usuário não devem colocar o componente em um estado que torne outros eventos
ou métodos inválidos. Finalmente, você vai querer dar a seus métodos nomes compreensíveis de modo
que o usuário não tenha que tentar adivinhar o que um método faz.
Exposição do método
Parte da criação de um projeto é saber os métodos que devem ser privados, públicos ou protegidos. Você
deve levar em consideração não apenas os usuários do seu componente, mas também aqueles que devem
usar seu componente como um ancestral de um outro componente personalizado. A Tabela 21.2 ajudará
você a decidir o status de cada um desses métodos no seu componente personalizado.
Private Variáveis e métodos de instância que você não deseja que o tipo descendente seja capaz de acessar
ou modificar. Geralmente, você dará acesso a algumas variáveis de instância privada através de
propriedades que têm diretivas read e write definidas de modo a impedir os usuários de darem um
tiro no próprio pé. Portanto, você deve evitar dar acesso a qualquer método que seja um método
de implementação de propriedade.
Protected Variáveis, métodos e propriedades de instância que você quer que as classes descendentes sejam
capazes de acessar e modificar – mas não os usuários de sua classe. É uma prática comum colocar
propriedades na seção protegida de uma classe básica para as classes descendentes publicarem de
acordo com a própria vontade.
Public Métodos e propriedades que você quer tornar acessíveis para qualquer usuário de sua classe. Se
você tiver propriedades que deseja tornar acessíveis em runtime, mas não durante o projeto, esse é
o lugar para colocá-las.
Published Propriedades que você quer que sejam colocadas no Object Inspector durante o projeto. A RTTI
(Runtime Type Information) é gerada para todas as propriedades nesta seção.
507
Construtores e destruidores
Durante a criação de um novo componente, você tem a opção de modificar o construtor do componente
ancestral e defini-lo você mesmo. É preciso tomar algumas precauções durante esse processo.
Modificando construtores
Certifique-se sempre de incluir a diretiva override durante a declaração de um construtor em uma classe
descendente de TComponent. Veja o exemplo a seguir:
TSomeComponent = class(TComponent)
private
{ Declarações privadas }
protected
{ Declarações protegidas }
public
constructor Create(AOwner: TComponent); override;
published
{ Declarações publicadas }
end;
NOTA
O construtor Create( ) é tornado virtual no nível de TComponent. Classes sem componentes têm construto-
res estáticos que são invocados a partir das classes TComponent do construtor. Portanto, se você estiver cri-
ando uma classe descendente sem componente, como na linha de código mostrada a seguir, o construtor
não pode ser modificado porque ele não é virtual:
TMyObject = class(TPersistent)
Embora não adicionar a diretiva de redefinição seja sintaticamente correto, isso pode causar proble-
mas durante o uso do componente. Isso é porque, quando você usa o componente (durante o projeto e
em runtime), o construtor não-virtual não será chamado pelo código que cria o componente através de
uma referência de classe (como um sistema de streaming).
Além disso, certifique-se de que você chama o construtor herdado dentro do código do seu construtor:
constructor TSomeComponent.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
// Inclua seu código aqui.
end;
Na grande maioria das vezes, você usará o estado csDesigning para determinar se seu componente
está no modo de projeto. Você pode fazer isso com a seguinte instrução:
inherited Create(AOwner);
if csDesigning in ComponentState then
{ Faz o que for necessário }
Você deve notar que o estado csDesigning é incerto até depois de o construtor herdado ter sido cha-
mado e o componente estiver sendo criado com um proprietário. Isso quase sempre acontece no criador
de formulário do IDE.
Modificando destruidores
A orientação geral a ser seguida durante a modificação de destruidores é se certificar de chamar o des-
truidor herdado só depois de liberar os recursos alocados pelo seu componente, e não antes. O código a
seguir demonstra isso:
destructor TMyComponent.Destroy;
begin
FTimer.Free;
MyStrings.Free;
inherited Destroy;
end;
DICA
Via de regra, quando você modifica construtores, em geral chama o construtor herdado primeiro, e quan-
do modifica destruidores, geralmente chama o destruidor herdado depois. Isso é uma garantia de que a
classe foi configurada antes de você modificá-la e de que todos os recursos dependentes foram excluídos
antes de você dispor de uma classe.
Há exceções a essa regra, mas geralmente você só não deve segui-las se houver uma razão muito forte
para isso.
509
Registrando seu componente
O registro do componente faz com que o Delphi saiba o componente que deve ser colocado na Compo-
nent Palette. Se você usou o Component Expert para projetar seu componente, não tem que fazer nada
aqui, pois o Delphi já gerou o código para você. No entanto, se você estiver criando seu componente ma-
nualmente, precisará adicionar o procedimento Register( ) à unidade do componente.
Tudo o que você tem que fazer é adicionar o procedimento Register( ) à seção de interface da uni-
dade do componente.
O procedimento Register simplesmente chama o procedimento RegisterComponents( ) para todos os
componentes que você estiver registrando no Delphi. O procedimento RegisterComponents( ) utiliza dois
parâmetros: o nome da página na qual irá colocar os componentes e um array de tipos de componentes.
A Listagem 21.8 mostra como fazer isso.
Unit MyComp;
interface
type
TMyComp = class(TComponent)
...
end;
TOtherComp = class(TComponent)
...
end;
procedure Register;
implementation
{ Métodos TMyComp }
{ Métodos TotherComp }
procedure Register;
begin
RegisterComponents(‘DDG’, [TMyComp, TOtherComp]);
end;
end.
Testando o componente
Embora seja muito excitante quando você finalmente escreve um componente e está no estágio de testes,
não se atreva a adicionar o seu componente à Component Palette antes que ele tenha sido suficientemen-
te depurado. Você deve fazer todos os testes preliminares com o componente criando um projeto que
crie e use uma instância dinâmica do componente. A razão para isso é que seu componente reside dentro
do IDE quando é usado durante o projeto. Se o componente contém um bug que danifica a memória, por
exemplo, ele também pode dar pau no IDE. A Listagem 21.9 descreve uma unidade para testar o compo-
nente TddgExtendedMemo, que será criado mais tarde neste capítulo. Esse projeto pode ser encontrado no
CD, com o nome TestEMem.dpr.
510
A Component Palette
No Delphi 1 e 2, o Delphi tinha um arquivo de biblioteca de componentes que armazenava todos os
componentes, ícones e editores para serem usados durante o projeto. Embora algumas vezes fosse
prático ter tudo relacionado ao projeto em um arquivo, ele podia facilmente se tornar incontrolável
quando muitos componentes eram colocados na biblioteca de componentes. Além disso, quanto mais
componentes você adicionava à palheta, mais tempo a biblioteca de componentes levava para ser re-
construída quando novos componentes eram adicionados.
Graças aos pacotes, introduzidos no Delphi 3, você pode dividir seus componentes em diversos
pacotes de projeto. Embora seja ligeiramente mais complexo lidar com diversos arquivos, essa solu-
ção é significativamente mais configurável, e o tempo necessário para reconstruir um pacote depois
da adição de um componente é bem menor do que o necessário para a reconstrução da biblioteca de
componentes.
Por default, novos componentes são adicionados a um pacote chamado DCLUSR50, mas você pode
criar e instalar novos pacotes de projeto usando o item de menu File, New, Package. O CD que acom-
panha este livro contém um pacote de projeto pré-construído chamado DdgDsgn50.dpk, contendo os
componentes deste livro. O pacote de runtime se chama DdgStd50.dpk.
Se o seu suporte em tempo de projeto envolve algo mais do que uma chamada para RegisterCom-
ponents( ) (como editores de propriedade ou editores de componente ou registros de especialistas),
você deve mover o procedimento Register( ) e o conteúdo que ele registra em uma unidade separada
do seu componente. A razão para isso é que, se você compila sua unidade tudo-em-um em um pacote
de runtime e o procedimento Register da unidade tudo-em-um faz referências a classes ou procedi-
mentos que existem apenas no IDE em tempo de projeto, seu pacote de runtime não possui utilidade.
O suporte em tempo de projeto deve ser empacotado separadamente do material de runtime.
unit MainFrm;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls, exmemo, ExtCtrls;
type
TMainForm = class(TForm)
btnCreateMemo: TButton;
btnGetRowCol: TButton;
btnSetRowCol: TButton;
edtColumn: TEdit;
edtRow: TEdit;
Panel1: TPanel;
procedure btnCreateMemoClick(Sender: TObject);
procedure btnGetRowColClick(Sender: TObject);
procedure btnSetRowColClick(Sender: TObject);
public
EMemo: TddgExtendedMemo; // Declara o componente.
procedure OnScroll(Sender: TObject);
end;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
511
Listagem 21.9 Continuação
end.
512
Não se esqueça de que, mesmo testando o componente durante o projeto, isso não significa que
seus componentes sejam perfeitos. Um dos comportamentos em tempo de projeto ainda pode provocar
erro com o IDE do Delphi, como por exemplo, não chamar o construtor Create( ) herdado.
NOTA
Você não pode partir do princípio de que o componente foi criado e configurado pelo ambiente em tempo
de projeto. O componente tem que estar em plenas condições de uso tão logo o construtor Create( ) tenha
sido executado. Por essa razão, você não deve tratar o método Loaded( ) como parte do processo de cons-
trução de componente. O método Loaded( ) só é chamado quando o componente é carregado de um stre-
am – como quando é colocado em um formulário construído durante o projeto. Loaded( ) marca o final do
processo de streaming. Se o seu componente foi apenas criado (não entrou no stream), Loaded( ) não é
chamado.
DICA
Mesmo que você tenha um driver de 256 ou mais cores, salve o ícone na Component Palette como um bit-
map de 16 cores caso pretenda comercializar o componente. A qualidade dos bitmaps de 256 cores ficará
bastante comprometida em máquina que execute drivers de 16 cores.
Depois que você criar o bitmap no arquivo DCR, dê ao bitmap um nome igual ao nome de classe do
componente – em LETRAS MAIÚSCULAS. Salve o arquivo de recurso com o nome igual ao da unidade
do componente, com uma extensão .dcr. Portanto, se o seu componente tiver o nome TXYZComponent, o
nome do bitmap deve ser TXYZCOMPONENT. Se o nome da unidade do componente for XYZCOMP.PAS, atribua ao
arquivo de recurso o nome XYZCOMP.DCR. Coloque esse arquivo no mesmo diretório que a unidade e, quan-
do você recompilar a unidade, o bitmap será automaticamente vinculado à biblioteca de componentes.
Componentes de exemplo
As próximas seções deste capítulo contêm exemplos reais de criação de componente. Os componentes
criados aqui têm dois objetivos. Primeiro, eles ilustram as técnicas explicadas na primeira parte deste ca-
pítulo. Segundo, você pode usar esses componentes em suas aplicações. Também é possível estender a
funcionalidade deles de modo a atender as suas necessidades.
513
TddgExtendedMemo: estendendo o componente TMemo
Embora o componente TMemo seja bastante robusto, há alguns recursos que ele não torna disponível, mas
que seriam de grande utilidade. Para os iniciantes, ele não é capaz de fornecer a posição do circunflexo
(^) em termos da linha e da coluna na qual se situa. Vamos estender o componente TMemo para fornecer es-
sas propriedades como públicas.
Além disso, algumas vezes é conveniente executar alguma ação sempre que o usuário tocar nas bar-
ras de rolagem de TMemo. Você vai criar eventos aos quais o usuário pode anexar código sempre que ocor-
rer esses eventos de rolagem.
O código-fonte do componente TddgExtendedMemo é mostrado na Listagem 21.10.
unit ExMemo;
interface
uses
Windows, Messages, Classes, StdCtrls;
type
TddgExtendedMemo = class(TMemo)
private
FRow: Longint;
FColumn: Longint;
FOnHScroll: TNotifyEvent;
FOnVScroll: TNotifyEvent;
procedure WMHScroll(var Msg: TWMHScroll); message WM_HSCROLL;
procedure WMVScroll(var Msg: TWMVScroll); message WM_VSCROLL;
procedure SetRow(Value: Longint);
procedure SetColumn(Value: Longint);
function GetRow: Longint;
function GetColumn: Longint;
protected
// Métodos de despacho de evento
procedure HScroll; dynamic;
procedure VScroll; dynamic;
public
property Row: Longint read GetRow write SetRow;
property Column: Longint read GetColumn write SetColumn;
published
property OnHScroll: TNotifyEvent read FOnHScroll write FOnHScroll;
property OnVScroll: TNotifyEvent read FOnVScroll write FOnVScroll;
end;
implementation
514
Listagem 21.10 Continuação
procedure TddgExtendedMemo.HScroll;
{ Este é o método de despacho do evento OnHScroll. Ele verifica se
OnHScroll aponta para um manipulador de evento e, caso afirmativo,
o chama. }
begin
if Assigned(FOnHScroll) then
FOnHScroll(self);
end;
procedure TddgExtendedMemo.VScroll;
{ Este é o método de despacho do evento OnVScroll. Ele verifica se
OnVScroll aponta para um manipulador de evento e, caso afirmativo,
o chama. }
begin
if Assigned(FOnVScroll) then
FOnVScroll(self);
end;
end.
Primeiro, vamos discutir a adição da capacidade de fornecer informações sobre a linha e a coluna
para TddgExtendedMemo. Observe que adicionamos dois campos privados ao componente, FRow e FColumn.
Esses campos não conterão a linha e a coluna da posição do circunflexo do TddgExtendedMemo. Observe que
também fornecemos as propriedades públicas Row e Column. Essas propriedades são tornadas públicas por-
que não há um uso real para elas durante o projeto. As propriedades Row e Column possuem métodos de
acesso de escrita e de leitura. Para a propriedade Row, esses métodos de acesso são GetRow( ) e SetRow( ). Os
métodos de acesso de Column são GetColumn( ) e SetColumn( ). Do ponto de vista prático, você poderia man-
dar para o espaço os campos de armazenamento FRow e FColumn, pois os valores de Row e Column são forneci-
dos através de métodos de acesso. No entanto, nós os mantivemos aqui porque oferecem a oportunidade
de estender esse componente.
Os quatro métodos de acesso fazem uso de várias mensagens EM_XXXX. Os comentários do código ex-
plicam o que está acontecendo em cada método e como essas mensagens são usadas para fornecer infor-
mações de Row e Column para o componente.
O componente TddgExtendedMemo também fornece dois novos eventos: OnHScroll e OnVScroll. O evento
OnHScroll ocorre sempre que o usuário dá um clique na barra de rolagem horizontal do controle. Da mes-
ma forma, OnVScroll ocorre quando o usuário dá um clique na barra de rolagem vertical. Para trazer à
tona esses eventos, você tem que capturar as mensagens do Win32 WM_HSCROLL e WM_VSCROLL, que são passa-
das para o controle sempre que o usuário dá um clique em uma das barras de rolagem. Dessa forma, você
criou os dois manipuladores de mensagem: WMHScroll( ) e WMVScroll( ). Esses dois manipuladores de men-
sagem chamam os métodos de disparo de evento HScroll( ) e VScroll( ). Cabe a esses métodos a responsa-
bilidade de verificar se o usuário do componente forneceu manipuladores de evento para os eventos
OnHScroll e OnVScroll e em seguida chamar esses manipuladores de evento. Se você estiver se perguntando
o motivo para não executarmos essa verificação nos métodos do manipulador de mensagem, não o faze-
mos porque normalmente você vai desejar ser capaz de chamar um manipulador de evento em conse-
qüência de uma ação diferente, como por exemplo quando o usuário muda a posição do circunflexo.
Você pode instalar e usar TddgExtendedMemo com as suas aplicações. Você também pode considerar a
extensão desse componente; por exemplo, sempre que o usuário muda a posição do circunflexo, uma
mensagem WM_COMMAND é enviada para o proprietário do controle. HiWord(wParam) transporta um código de
notificação indicando a ação que ocorreu. Esse código teria o valor de EN_CHANGE, que denota uma mudan-
ça de mensagem de notificação de edição. É possível tornar o componente uma subclasse do seu pai e
capturar essa mensagem no procedimento de janela do pai. Posteriormente, é possível atualizar automa-
ticamente os campos FRow e FColumn. A criação de subclasse é um tópico diferente e avançado, que discuti-
remos posteriormente.
A idéia
A idéia para esse componente, como a maioria delas, surgiu da necessidade. Era preciso uma caixa de lis-
tagem com a capacidade de usar paradas de tabulação (com suporte na API do Win32, mas não em TList-
box) e, juntamente com ela, uma barra de rolagem horizontal para exibir as strings maiores que a largura
da caixa de listagem (também com suporte pela API, mas não em TListbox). Esse componente será chama-
do de TddgTabListbox.
O plano para o componente TddgTabListbox não chega a ser terrivelmente complexo; fizemos isso
criando um componente descendente de TListbox contendo as propriedades de campo corretas, métodos
modificados e novos métodos para obter o comportamento desejado.
O código
O primeiro passo na criação de uma caixa de listagem rolável com paradas de tabulação é incluir estilos
de janela no estilo da TddgTabListbox quando a janela listbox é criada. Os estilos de janela necessários são
lbs_UseTabStops para tabulações e ws_HScroll para permitir uma barra de rolagem horizontal. Sempre que
você adicionar estilos de janela a um descendente de TWinControl, faça isso modificando o método Create-
Params( ), como mostrado no código a seguir:
CreateParams( )
Sempre que você precisa modificar um dos parâmetros – como a classe da janela ou estilo – passados
para a função CreateWindowEx( ) da API, deve fazê-lo no método CreateParams( ). CreateWindowEx( ) é a
função usada para criar a alça de janela associada a um descendente de TWinControl. Modificando
CreateParams( ), você pode controlar a criação de uma janela no nível da API.
CreateParams aceita um parâmetro do tipo TCreateParams, mostrado a seguir:
TCreateParams = record
Caption: Pchar;
Style: Longint;
ExStyle: Longint;
X, Y: Integer;
Width, Height: Integer;
WndParent: Hwnd;
Param: Pointer;
WindowClass: TWndClass;
WinClassName: array[0..63] of Char;
end;
Como um criador de componente, você vai modificar CreateParams( ) com freqüência – sempre
que precisar controlar a criação de um componente no nível da API. Certifique-se de chamar o primei-
ro CreateParams( ) herdado para preencher o registro Params para você.
Para definir as paradas de tabulação, a TddgTabListbox executa uma mensagem lb_SetTabStops, passan-
do o número de paradas de tabulação e um ponteiro para um array de tabulações como o wParam e lParam
(essas duas variáveis serão armazenadas na classe como FNumTabStops e FTabStops). O único inconveniente é
517
que as paradas de tabulação de listbox são manipuladas em uma unidade de medida chamada unidades de
caixa de diálogo. Como as unidades de caixa de diálogo não fazem sentido para o programador em Delphi,
você só medirá as tabulações em pixels. Com a ajuda da unidade PixDlg.pas mostrada na Listagem 21.11,
você pode fazer conversões entre unidades de caixa de diálogo e pixels de tela nos eixos X e Y.
unit Pixdlg;
interface
implementation
uses WinProcs;
end.
Quando você conhece as paradas de tabulação, pode calcular a extensão da barra de rolagem hori-
zontal. A barra de rolagem deve se estender pelo menos até a string mais longa da caixa de listagem. Fe-
lizmente, a API do Win32 fornece uma função chamada GetTabbedTextExtent( ), que recupera apenas as
informações de que você precisa. Quando você conhece o comprimento da string mais longa, pode defi-
nir a extensão da barra de rolagem executando a mensagem lb_SetHorizontalExtent, passando a extensão
desejada como a wParam.
Você também precisa escrever manipuladores de mensagem para algumas mensagens especiais do
Win32. Em particular, você precisa manipular as mensagens que controlam a inserção e a exclusão, pois
precisa ser capaz de medir o comprimento de qualquer string nova ou saber quando uma string longa foi
excluída. As mensagens com as quais você tem que se preocupar são lb_AddString, lb_InsertString e lb_De-
leteString. A Listagem 21.12 contém o código-fonte da unidade LbTab.pas, que contém o componente
518 TddgTabListbox.
Listagem 21.12 LbTab.pas, a TddgTabListBox
unit Lbtab;
interface
uses
SysUtils, Windows, Messages, Classes, Controls, StdCtrls;
type
EddgTabListboxError = class(Exception);
TddgTabListBox = class(TListBox)
private
FLongestString: Word;
FNumTabStops: Word;
FTabStops: PWord;
FSizeAfterDel: Boolean;
function GetLBStringLength(S: String): word;
procedure FindLongestString;
procedure SetScrollLength(S: String);
procedure LBAddString(var Msg: TMessage); message lb_AddString;
procedure LBInsertString(var Msg: TMessage); message lb_InsertString;
procedure LBDeleteString(var Msg: TMessage); message lb_DeleteString;
protected
procedure CreateParams(var Params: TCreateParams); override;
public
constructor Create(AOwner: TComponent); override;
procedure SetTabStops(A: array of word);
published
property SizeAfterDel: Boolean read FSizeAfterDel write FSizeAfterDel default True;
end;
implementation
uses PixDlg;
i: word;
TempTab: word;
TempBuf: PWord;
begin
{ Armazena novos valores em temps caso ocorra exceção na definição de
tabulações }
TempTab := High(A) + 1; // Descobre número de paradas de tab
GetMem(TempBuf, SizeOf(A)); // Reserva novas paradas de tabulação
Move(A, TempBuf^, SizeOf(A));// Copia novas paradas de tabulação }
{ Converte de pixels em unidades de diálogo e... }
for i := 0 to TempTab - 1 do
A[i] := PixelsToDialogUnitsX(A[i]);
{ Envia novas paradas de tabulação para a caixa de listagem. Observe que
devemos usar unidades de caixa de diálogo. }
if Perform(lb_SetTabStops, TempTab, Longint(@A)) = 0 then
begin
{ se zero, não foi possível definir novas paradas de tabulação, libera
o buffer temporário de paradas de tabulação e produz uma exceção }
FreeMem(TempBuf, SizeOf(Word) * TempTab);
raise EddgTabListboxError.Create(‘Failed to set tabs.’)
end
else begin
{ se diferente de zero, novas paradas de tabulação foram definidas com
êxito e libera paradas de tabulação anteriores }
FreeMem(FTabStops, SizeOf(Word) * FNumTabStops);
{ copia valores de temps... }
FNumTabStops := TempTab; // define no. de paradas de tabulação
FTabStops := TempBuf; // define buffer de parada de tabulação
FindLongestString; // reinicializa barra de rolagem
Invalidate; // pinta novamente
end;
end;
procedure TddgTabListBox.FindLongestString;
var
i: word;
Strg: String;
begin
FLongestString := 0;
521
Listagem 21.12 Continuação
end.
Um ponto de particular interesse nesse componente é o método SetTabStops( ), que aceita um array
de abertura de word como um parâmetro. Isso permite que os usuários passem o número de paradas de ta-
bulação que desejarem. Veja o exemplo a seguir:
ddgTabListboxInstance.SetTabStops([50, 75, 150, 300]);
Se o texto na caixa de listagem ultrapassar a área de exibição da janela, a barra de rolagem aparecerá
automaticamente.
{
Copyright © 1999 by Delphi 5 Developer’s Guide - Xavier Pacheco and Steve Teixeira
}
unit RunBtn;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls, Buttons;
type
TddgRunButton = class(TSpeedButton)
private
FCommandLine: TCommandLine;
// Ocultando propriedades do Object Inspector
FCaption: TCaption;
FAllowAllUp: Boolean;
FFont: TFont;
FGroupIndex: Integer;
FLayOut: TButtonLayout;
procedure SetCommandLine(Value: TCommandLine);
public
constructor Create(AOwner: TComponent); override;
procedure Click; override;
published
property CommandLine: TCommandLine read FCommandLine write SetCommandLine;
// Propriedades de leitura são escondidas
property Caption: TCaption read FCaption;
property AllowAllUp: Boolean read FAllowAllUp;
property Font: TFont read FFont;
property GroupIndex: Integer read FGroupIndex;
property LayOut: TButtonLayOut read FLayOut;
end;
523
Listagem 21.13 Continuação
implementation
uses ShellAPI;
const
EXEExtension = ‘.EXE’;
begin
{ Define as propriedades de TddgRunButton de modo que o ícone contido
em Icon possa ser copiado nele. }
{ Primeiro, apaga a tela. Isso é preciso no caso de outro ícone ter
sido desenhado anteriormente na tela }
Canvas.Brush.Style := bsSolid;
Canvas.FillRect(Canvas.ClipRect);
{ Define a largura e a altura de Icon }
Width := Icon.Width;
Height := Icon.Height;
Canvas.Draw(0, 0, Icon); // Desenha o ícone na tela de TddgRunButton
end;
finally
Icon.Free; // Libera a instância TIcon.
end;
end;
procedure TddgRunButton.Click;
var
WERetVal: Word;
begin
inherited Click; // Invoca o método Click herdado
{ Executa o método ProcessExecute e verifica seu valor de retorno.
Se o valor de retorno for < > 0, uma exceção será produzida, já que
teria ocorrido um erro. O código de erro é mostrado na exceção }
WERetVal := ProcessExecute(FCommandLine, sw_ShowNormal);
if WERetVal < > 0 then begin
raise Exception.Create(‘Error executing program. Error Code:; ‘+
IntToStr(WERetVal));
end;
end;
end.
tem uma propriedade, CommandLine, que é definida para ser do tipo String. O campo de
TddgRunButton
armazenamento privado para CommandLine é FCommandLine.
DICA
Vale a pena discutir a definição especial de TCommandLine. Veja a seguir a sintaxe usada:
Definindo TCommandLine dessa forma, você manda o compilador tratar TCommandLine como um tipo exclusi-
vo e porém compatível com outros tipos de string. O novo tipo obterá suas próprias informações de tipo de
runtime e por essa razão você pode ter seu próprio editor de propriedades. Essa mesma técnica também
pode ser usada com outros tipos. Veja o exemplo a seguir:
Vamos mostrar como usamos isso para criar um editor de propriedades para a propriedade CommandLine no
próximo capítulo. Não mostramos essa técnica neste capítulo porque a criação de editores de proprieda-
des é um tópico avançado, sobre o qual queremos falar com mais profundidade.
526
O método de acesso de escrita para CommandLine é SetCommandLine( ). Fornecemos duas funções auxilia-
doras: IsExecutableFile( ) e ProcessExecute( ).
IsExecutableFile( ) é uma função que determina se um nome de arquivo passado para ele é um ar-
quivo executável, baseado na extensão do arquivo.
ProcessExecute( ) é uma prática função utilitária que você pode querer manter em uma unidade se-
parada, que possa ser compartilhada por outras aplicações.
Métodos de TddgRunButton
O construtor TddgRunButton.Create( ) simplesmente define um tamanho-padrão para si depois de chamar
o construtor herdado.
O método SetCommandLine( ), que é o método de acesso de escrita do parâmetro CommandLine, executa
diversas tarefas. Primeiro, ele determina se o valor que está sendo atribuído a CommandLine é um nome de
arquivo executável válido. Caso contrário, ele produz uma exceção.
Se a entrada for válida, ela será atribuída ao campo FCommandLine. SetCommandLine( ), em seguida, ex-
trai o ícone do arquivo da aplicação e o desenha na tela do TddgRunButton. A função ExtractIcon( ) da API
do Win32 é usada para fazer isso. A técnica usada é explicada nos comentários.
TddgRunButton.Click( ) é o método de despacho de evento para o evento TSpeedButton.OnClick. É ne-
cessário chamar o método Click( ) herdado que invocará o manipulador de evento OnClick, caso tenha 527
sido atribuído. Depois da chamada do Click( ) herdado, você chama ProcessExecute( ) e examina o valor
resultante para determinar se a chamada foi bem-sucedida. Caso contrário, ele produz uma exceção.
Decisões de projeto
Considerando que o Object Pascal é baseado em um modelo de objeto de herança única, TddgButtonEdit
precisará ser um componente em seu próprio direito, que deve conter um TEditl e um TSpeedButton. Além
disso, como é necessário que esse componente contenha controles de janela, terá ele mesmo um controle
de janela. Por essas razões, escolhemos descender TddgButtonEdit de TWinControl. Criamos TEdit e TSpeedBut-
ton no construtor do TddgButtonEdit usando o seguinte código:
FSpeedButton := TSpeedButton.Create(Self);
FSpeedButton.Left := FEdit.Width;
FSpeedButton.Height := 19; // dois a menos que a altura de TEdit
FSpeedButton.Width := 19;
FSpeedButton.Caption := ‘...’;
FSpeedButton.Parent := Self;
Width := FEdit.Width+FSpeedButton.Width;
Height := FEdit.Height;
end;
O desafio durante a criação de um componente que contém outros componentes é trazer à tona as
propriedades dos componentes “interiores” do componente container. Por exemplo, TddgButtonEdit pre-
cisará de uma propriedade Text. Você também vai querer ser capaz de alterar a fonte do texto no contro-
le; por essa razão, uma propriedade Font se faz necessária. Finalmente, existe a necessidade de um evento
OnClick para o botão no controle. Você não iria querer tentar implementar isso sozinho no componente
contêiner quando ele já estiver disponível a partir dos componentes interiores. O objetivo é, portanto,
trazer à tona as propriedades apropriadas dos controles interiores sem reescrever as interfaces desses
controles.
unit ButtonEdit;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, Buttons;
type
TddgButtonEdit = class(TWinControl)
private
FSpeedButton: TSpeedButton;
FEdit: TEdit;
protected
procedure WMSize(var Message: TWMSize); message WM_SIZE;
procedure SetText(Value: String);
function GetText: String;
function GetFont: TFont;
procedure SetFont(Value: TFont); 529
Listagem 21.14 Continuação
implementation
FSpeedButton := TSpeedButton.Create(Self);
FSpeedButton.Left := FEdit.Width;
FSpeedButton.Height := 19; // dois a menos que a altura de TEdit
FSpeedButton.Width := 19;
FSpeedButton.Caption := ‘...’;
FSpeedButton.Parent := Self;
Width := FEdit.Width+FSpeedButton.Width;
Height := FEdit.Height;
end;
destructor TddgButtonEdit.Destroy;
begin
FSpeedButton.Free;
FEdit.Free;
inherited Destroy;
end;
FEdit.Text := Value;
end;
end.
{
Copyright © 1999 Guia do programador em Delphi 5 - Xavier Pacheco e Steve Teixeira
}
{$OBJEXPORTALL ON}
{$ENDIF}
unit DDGClock;
interface
uses
Windows, Messages, Controls, Forms, SysUtils, Classes, ExtCtrls;
type
TddgDigitalClock = class(TPanel)
private
{ Campos de dados }
FHour,
FMinute,
FSecond: Word;
FDateTime: TDateTime;
FOldMinute,
FOldSecond: Word;
FTimer: TTimer;
{ Manipuladores de evento }
FOnHour: TTimeEvent; // Ocorre a cada hora
FOnHalfPast: TTimeEvent; // Ocorre a cada meia hora
FOnMinute: TTimeEvent; // Ocorre a cada minuto
FOnSecond: TTimeEvent; // Ocorre a cada segundo
FOnHalfMinute: TTimeEvent; // Ocorre a cada 30 segundos
{ Define o manipulador de evento OnTimer para o Ttimer interno, FTimer }
procedure TimerProc(Sender: TObject);
protected
{ Redefine os métodos de Paint }
procedure Paint; override;
public
{ Redefine o construtor Create e o destruidor Destroy }
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
published
{ Define propriedades de evento }
property OnHour: TTimeEvent read FOnHour write FOnHour;
property OnHalfPast: TTimeEvent read FOnHalfPast write FOnHalfPast;
532
Listagem 21.15 Continuação
implementation
destructor TddgDigitalClock.Destroy;
begin
FTimer.Free; // Libera a instância TTimer.
inherited Destroy; // Chama método Destroy herdado
end;
procedure TddgDigitalClock.Paint;
begin
inherited Paint; // Chama método Paint herdado
{ Agora define a propriedade Caption herdada como a hora atual. }
inherited Caption := TimeToStr(FDateTime);
end;
DoHour(FDateTime);
if FMinute = 30 then
DoHalfPast(FDateTime);
if (FMinute < > FOldMinute) then
DoMinute(FDateTime);
if FSecond < > FOldSecond then
if ((FSecond = 30) or (FSecond = 0)) then
DoHalfMinute(FDateTime)
else
DoSecond(FDateTime);
end;
end.
A lógica por trás desse componente é explicada no comentário do código-fonte. Os métodos usados
não são diferentes dos que foram explicados anteriormente quando discutimos a criação de eventos.
TddgDigitalClock adiciona apenas mais eventos e contém lógica para determinar quando cada evento é
chamado.
534
Adicionando formulários à Component Palette
A adição de formulários ao Object Repository é uma forma conveniente de dar um ponto de partida aos
formulários. Mas, e se você desenvolver um formulário o qual reutilize com freqüência, que não precise
ser herdado e que não exija o acréscimo de funcionalidade? O Delphi 5 fornece uma maneira de você po-
der reutilizar seus formulários como componentes na Component Palette. Na verdade, os componentes
TFontDialog e TOpenDialog são exemplos de formulários aos quais se pode ter acesso a partir da Component
Palette. Na verdade, essas caixas de diálogo não são formulários do Delphi; são caixas de diálogo forne-
cidas pela CommDlg.dll. No entanto, o conceito é o mesmo.
Para adicionar formulários à Component Palette, você deve envolver o formulário com um compo-
nente para torná-lo um componente instalável à parte. O processo descrito a seguir usa uma simples cai-
xa de diálogo para senha, cuja funcionalidade verificará a senha automaticamente. Embora esse seja um
projeto muito simples, o objetivo dessa discussão não é mostrar como instalar uma caixa de diálogo com-
plexa como um complemento, mas mostrar o método geral de adição de caixas de diálogo à Component
Palette. O mesmo método se aplica a caixas de diálogo de qualquer complexidade.
Primeiro, você deve criar o formulário que vai ser envolvido pelo componente. O formulário que
usamos é definido no arquivo PwDlg.pas. Essa unidade também mostra um wrapper de componente para
esse formulário.
A Listagem 21.16 mostra a unidade definindo o formulário TPasswordDlg e o componente que lhe
serve de wrapper, TddgPasswordDialog.
unit PwDlg;
interface
type
TPasswordDlg = class(TForm)
Label1: TLabel;
Password: TEdit;
OKBtn: TButton;
CancelBtn: TButton;
end;
implementation
{$R *.DFM}
begin
{ Cria uma instância de TPasswordDlg }
PasswordDlg := TPasswordDlg.Create(Application);
try
Result := False; // Inicializa o resultado como falso
{ Mostra a caixa de diálogo e retorna verdadeiro se a senha estiver
correta. }
if PasswordDlg.ShowModal = mrOk then
Result := PasswordDlg.Password.Text = FPassword;
finally
PasswordDlg.Free; // Libera instância de PasswordDlg
end;
end;
end.
Pacotes de componentes
O Delphi 3 introduziu pacotes, que permitem que você coloque trechos de sua aplicação em módulos se-
536 parados, que podem ser compartilhados por várias aplicações. Pacotes são semelhantes a bibliotecas de
vínculo dinâmico (DLLs), porém com um uso diferente. A principal finalidade dos pacotes é armazenar
coleções de componentes em um módulo compartilhável separado (uma Borland Package Library, ou ar-
quivo.bpl). À medida que você ou outros programadores criam aplicações em Delphi, os pacotes que
você cria podem ser usados pela aplicação em runtime em vez de serem diretamente vinculados durante a
compilação/linkedição. Como o código para essas unidades reside no arquivo.bpl, e não no seu.exe ou
.dll, o tamanho do seu.exe ou .dll pode se tornar muito pequeno.
Os pacotes diferem das DLLs por serem específicos da VCL do Delphi; ou seja, as aplicações escri-
tas em outras linguagens não podem usar pacotes criados pelo Delphi (com exceção do CBuilder). Uma
das razões para o uso dos pacotes é contornar uma limitação do Delphi 1 e 2. Nessas versões anteriores
do Delphi, a VCL adicionava um mínimo de 150KB a 200KB de código para todos os executáveis. Por
essa razão, mesmo que você fosse separar um pedaço da sua aplicação em uma DLL, tanto a DLL como a
aplicação teriam código redundante. Isso é um problema especialmente se você estiver fornecendo um
conjunto de aplicações em uma máquina. Os pacotes permitem que você reduza os vestígios das suas
aplicações e fornecem um meio conveniente para você distribuir suas coleções de componente.
Redução de código
A principal razão por trás do uso de pacotes é reduzir o tamanho das suas aplicações e DLLs. O Delphi já
vem com diversos pacotes predefinidos, que dividem a VCL em agrupamentos lógicos. Na verdade, você
pode escolher compilar sua aplicação de modo que ela presuma a existência de muitos desses pacotes do
Delphi.
Contenção de componentes
Provavelmente, uma das razões mais comuns para se usar pacotes é a distribuição de componentes de ter-
ceiros. Se você for um fornecedor de componentes, deve saber como criar pacotes. A razão para isso é
que certos elementos durante a execução do projeto – como, por exemplo, editores de componentes e
propriedades, assistentes e especialistas – são fornecidos por pacotes.
Tipos de pacotes
Há quatro tipos de pacotes disponíveis para você criar e usar:
l Pacote de runtime. Os pacotes de runtime contêm código, componentes e outros elementos que
uma aplicação necessita em runtime. Se você escrever uma aplicação que dependa de um pacote
de runtime em particular, a aplicação não será executada na ausência desse pacote.
l Pacote de projeto. Os pacotes de projeto contêm componentes, editores de propriedade/compo-
nente, especialistas e outros elementos necessários ao projeto de aplicação no IDE do Delphi.
Esse tipo de pacote só é usado pelo Delphi, e nunca é distribuído com as suas aplicações.
l Pacote de runtime e projeto. Um pacote ativado tanto no projeto como em runtime é, em geral,
usado quando não há elementos específicos de projeto, como, por exemplo, editores de proprie-
dades/componentes e especialistas. Você pode criar esse tipo de pacote para simplificar o desen-
volvimento e a implantação de aplicações. No entanto, se esse pacote contiver elementos de
projeto, o uso do runtime sobrecarregará o suporte ao projeto nas suas aplicações distribuídas.
Recomendamos a criação de um pacote de projeto e runtime para separar elementos específicos
do projeto, quando eles estiverem presentes.
l Nem pacote de runtime nem de projeto. Essa espécie de pacote tem como finalidade ser usada
apenas por outros pacotes e não deve ser referenciada por uma aplicação ou usada no ambiente
de projeto. Isso implica que os pacotes podem usar ou incluir outros pacotes.
Arquivos de pacotes
A Tabela 21.5 lista e descreve os arquivos específicos de pacote com base nas extensões de arquivo.
.dpk Arquivo-fonte do Esse arquivo é criado quando você chama o Package Editor. Você
pacote pode pensar nele como pensa no arquivo .dpr para um projeto do
Delphi.
.dcp Arquivo de símbolo Essa é a versão compilada do pacote que contém as informações
do pacote de runtime/ de símbolos para o pacote e suas unidades. Além disso, há
projeto informações de cabeçalho usadas pelo IDE do Delphi.
.dcu Unidade compilada Uma versão compilada de uma unidade contida em um pacote.
Um arquivo .dcu será criado para cada unidade contida no
pacote.
.bpl Biblioteca de pacotes Esse é o pacote de runtime ou de projeto, equivalente a uma DLL
de runtime/projeto do Windows. Se for um pacote de runtime, você distribuirá o
arquivo juntamente com as aplicações (se estiverem ativados para
pacotes de runtime). Se esse arquivo representa um pacote de
projeto, você o distribuirá juntamente com seu parceiro de
runtime para programadores que o usarão para escrever
programas. Observe que, se você não estiver distribuindo o
código-fonte, deverá distribuir os arquivos.dcp correspondentes.
538
Ativação de pacotes nas aplicações do Delphi 5
É fácil ativar pacotes nas aplicações do Delphi. Basta marcar a caixa de seleção Build with Runtime
Packages (montar com pacotes de runtime), encontrada na caixa de diálogo Project, Options da pági-
na Packages. Da próxima vez em que você criar uma aplicação depois que essa opção for selecionada, a
aplicação será vinculada dinamicamente a pacotes de runtime, em vez de ter unidades vinculadas esta-
ticamente ao.exe ou .dll. O resultado será uma aplicação muito mais elegante (embora você precise ter
em mente que terá que distribuir os pacotes necessários à sua aplicação).
Para instalar um pacote, você só precisa ativar a página Packages da caixa de diálogo Project Opti-
ons selecionando Component, Install Packages no sistema de menus do Delphi 5.
Selecionando o botão Add, você pode selecionar o arquivo .bpl específico. Ao fazer isso, esse arqui-
vo se tornará o arquivo selecionado na página Project. Quando você der um clique em OK, o novo paco-
te será instalado no IDE do Delphi. Se esse pacote possui componentes, você verá a nova página Compo-
nent na Component Palette, juntamente com os componentes recém-instalados.
A pasta Contains
Na pasta Contains, especifique as unidades que precisam ser compiladas no novo pacote. Há algumas re-
gras a serem levadas em consideração durante a colocação de unidades na página Contains de um pacote:
l O pacote não deve ser listado na cláusula contains de outro pacote ou na cláusula uses de uma
unidade dentro de outro pacote.
l As unidades listadas na cláusula contains de um pacote, seja direta ou indiretamente (elas existem
nas cláusulas uses de unidades listadas na cláusula contains do pacote), não podem ser listadas na
cláusula requires do pacote. É por isso que essas unidades já estão vinculadas ao pacote quando
ele é compilado.
l Você não pode listar uma unidade em uma cláusula contains do pacote caso ela já esteja listada na
cláusula contains de outro pacote usado pelo mesma aplicação.
A página Requires
Na página Requires, você especifica outros pacotes que são exigidos pelo novo pacote. Isso é semelhante à
cláusula uses de uma unidade do Delphi. Na maioria dos casos, qualquer pacote que você crie terá VCL50 – o
pacote que contém os componentes-padrão da VCL do Delphi – na sua cláusula requires. O arranjo típico
aqui, por exemplo, é que você coloque todos os seus componentes em um pacote de runtime. Em seguida,
você cria um pacote de projeto que inclui o pacote de runtime em sua cláusula requires. Há algumas regras
a serem levadas em consideração na colocação de pacotes na página Requires de outro pacote:
l Evite referências circulares: Package1 não pode ter Package1 em sua cláusula requires, nem pode
conter outro pacote que tenha Package1 em sua cláusula requires.
l A cadeia de referências não deve fazer referência a um pacote ao qual já se tenha feito referência
na cadeia.
O Package Editor tem uma barra de ferramentas e menus de contexto. Para saber o que fazem esses
botões, consulte o tópico “Package Editor” da ajuda on-line do Delphi 5. Não reproduziremos essas in-
formações aqui.
540
Dado este cenário, você cria um pacote de projeto e um pacote de runtime. A Figura 21.4 descreve
esse arranjo. Como a figura ilustra, o pacote de projeto (DDGDsgn50.dpk) abrange os recursos de projeto
(editores de propriedades e componentes) e o pacote de runtime (DDGStd50.dpk). O pacote de runtime
(DDGStd50.dpk) inclui apenas seus componentes. Esse arranjo é realizado pela listagem do pacote de runti-
me na seção requires do pacote de projeto, como mostrado na Figura 21.4.
DDGDsgn50.dpk
DdgReg.pas
Editores de componentes
Editores de propriedades
DDGStd50.dpk
TddgButtonEdit
TddgDigitalClock
TddgLaunchPad
TddgRunButton
Você também deve aplicar as opções de uso apropriadas para cada pacote antes de compilar esse pa-
cote. Isso é feito a partir da caixa de diálogo Package Options (opções de pacote). (Você acessa a caixa de
diálogo Package Options dando um clique com o botão direito do mouse dentro do Package Editor, para
chamar o menu local. Selecione Options para ter acesso à caixa de diálogo.) Para o pacote de runtime,
DDGStd50.dpk, a opção de uso deve ser definida como Runtime Only (apenas runtime). Isso garante que o
pacote não pode ser instalado no IDE como um pacote de projeto (veja o quadro “Segurança do compo-
nente” posteriormente neste capítulo). Para o pacote de projeto, DDGDsgn50.dpk, a opção de uso Design
Time Only (apenas durante o projeto) deve ser selecionada. Isso permite que os usuários instalem o paco-
te no IDE do Delphi e os impede de usar o pacote como um pacote de runtime.
A adição do pacote de runtime ao pacote de projeto não torna os pacotes contidos no pacote de
runtime disponíveis para o IDE do Delphi. Você ainda deve registrar seus componentes com o IDE.
Como você já sabe, sempre que criar um componente, o Delphi insere automaticamente um procedi-
mento Register( ) na unidade do componente, que por sua vez chama o procedimento RegisterCompo-
nents( ). RegisterComponents( ) é o procedimento que de fato registra o componente no IDE do Delphi
quando você o instala. Durante o trabalho com pacotes, a abordagem recomendada é mover o procedi-
mento Register( ) da unidade do componente para uma unidade de registro separada. Essa unidade de
registro registra todos os seus componentes chamando RegisterComponents( ). Isso não apenas facilita o
gerenciamento do registro dos seus componentes, como também impede qualquer pessoa de ser capaz de
instalar e usar o pacote de runtime ilegalmente, pois os componentes não estarão disponíveis para o IDE
do Delphi.
Como um exemplo, os componentes usados neste livro podem ser encontrados no pacote de runti-
me DDGStd50.dpk. Os editores de propriedades, editores de componentes e a unidade de registro
(DdgReg.pas) para nossos componentes existem no pacote de projeto DDGDsgn50.dpk. DDGDsgn50.dpk também
inclui DDGStd50.dpk em sua cláusula requires. A Listagem 21.17 mostra o conteúdo da nossa unidade de re-
gistro.
541
Listagem 21.17 Unidade de registro para os componentes deste livro
unit DDGReg;
interface
procedure Register;
implementation
procedure Register;
begin
// Registra os componentes.
RegisterComponents(‘DDG’,
[ TddgTrayNotifyIcon, TddgDigitalClock, TddgHalfMinute, tddgButtonEdit,
TddgExtendedMemo, TddgTabListbox, TddgRunButton, TddgLaunchPad,
TddgMemView, TddgMarquee, TddgWaveFile, TddgCard, TddgPasswordDialog,
TddgPlanet, TddgPlanets, TddgWorthLess, TddgDrawPanel,
TComponentEditorSample, TDefinePropTest]);
end;
end.
Segurança do componente
É possível que alguém registre seus componentes, muito embora ele só tenha seu pacote de run-
time. Ele faria isso criando sua própria unidade de registro, na qual registraria seus componentes. Em
seguida, ele incluiria essa unidade a um pacote separado, que também teria seu pacote de runtime na
cláusula requires. Depois de instalar esse novo pacote no IDE do Delphi, os componentes aparecerão
na Component Palette. No entanto, ainda não é possível compilar as aplicações usando os seus com-
ponentes, pois estarão faltando os arquivos *.dcu exigidos para suas unidades de componente.
542
Distribuição de pacote
Ao distribuir os pacotes para criadores de componente sem o código-fonte, você deve distribuir ambos os
pacotes compilados, DDGDsgn50.bpl e DDGStd50.bpl, ambos os arquivos *.dcp e as unidades compiladas
(*.dcu) necessárias para compilar seus componentes. Os programadores usando seus componentes que
desejam os pacotes de runtime das suas aplicações ativadas devem distribuir o pacote DDGStd50.bpl junta-
mente com as suas aplicações e qualquer outro pacote de runtime que elas possam estar usando.
Distribuição de pacote
Ao distribuir o pacote para criadores de componentes sem o código-fonte, você deve distribuir o pacote
compilado, DDGDsgn50.bpl, o arquivo DDGDsgn50.dcp e as unidades compiladas (*.dcu) necessárias à compila-
ção dos seus componentes. Os programadores que usam seus componentes devem compilar os compo-
nentes nas suas aplicações. Eles não estarão distribuindo nenhum dos seus componentes como pacotes de
runtime.
Versão do pacote
A versão do pacote é um tópico que não é bem entendido. Você pode pensar na versão do pacote da mes-
ma maneira que pensa na versão da unidade. Ou seja, qualquer pacote que você forneça para a sua aplica-
ção deve ser compilado usando a mesma versão do Delphi usada para compilar a aplicação. Portanto,
543
você não pode fornecer um pacote escrito em Delphi 5 para ser usado por uma aplicação escrita em
Delphi 4. Os programadores da Inprise se referem à versão de um pacote como uma base de código. Por-
tanto, um pacote escrito em Delphi 5 tem uma base de código 5.0. Esse conceito deve influenciar a con-
venção de nomeação que você usa para os arquivos de pacote.
Tabela 21.7 Diretivas de compilador para unidades que estão sendo empacotadas
Diretiva Significado
{$G} ou {IMPORTEDDATA OFF} Use isso quando quiser impedir que a unidade seja empacotada – quando você
quiser que ela seja diretamente vinculada à aplicação. Compare isso com a
diretiva {$WEAKPACKAGEUNIT}, que permite que uma unidade seja incluída em um
pacote cujo código, porém, se mantenha estaticamente vinculado à aplicação.
{$DENYPACKAGEUNIT} Igual a {$G}.
{$WEAKPACKAGEUNIT} Veja a seção “Mais sobre a diretiva {$WEAKPACKAGEUNIT}”.
Diretiva Significado
Pacotes de add-ins
Os pacotes de add-in permitem que você particione suas aplicações em peças ou módulos e distribua os
módulos separadamente da aplicação principal. Esse esquema é especialmente atraente, pois permite que
você estenda a funcionalidade da sua aplicação sem ter que recompilar/reprojetar a aplicação inteira. No
entanto, isso requer um cuidadoso planejamento do projeto arquitetônico. Não faz parte do escopo des-
te livro discutir as questões relacionadas ao projeto. Para ter acesso a uma discussão mais detalhada sobre
os pacotes de add-in e como eles se relacionam com as estruturas da aplicação e padrões de projeto, você
encontrará artigos em http://www.xapware.com.
Nosso exemplo é uma ilustração simples dessa técnica. Vamos mostrar como se adiciona um formu-
lário a uma aplicação sem ter que reescrever a aplicação inteiramente. Você pode obter um exemplo mais
elaborado na URL mencionada no parágrafo anterior.
NOTA
Se você instalou os formulários usados na demonstração de estrutura de aplicação do Capítulo 4 no
Object Repository, terá que removê-los do Repository antes de carregar o projeto desta aplicação.
545
A aplicação é particionada em três peças lógicas: a aplicação principal (ChildTest.exe), o pacote
TChildForm (AIChildForm50.bpl)e as classes concretas descendentes de TChildForm, que residem em seu pró-
prio pacote.
A aplicação principal é basicamente igual à que foi apresentada no Capítulo 4, com alguma modifi-
cação. O pacote AIChildForm50.bpl contém a classe abstrata de TChildForm. Os outros pacotes contêm clas-
ses descendentes de TChildForm ou TChildForms concretas. Vamos nos referir a esses pacotes como o pacote
abstrato e os pacotes concretos, respectivamente.
A aplicação principal usa o pacote abstrato (AIChildForm50.bpl). Cada pacote concreto usa também o
pacote abstrato. Para que isso funcione de modo adequado, a aplicação principal deve ser compilada
com pacotes de runtime, incluindo o pacote AIChildForm50.dcp. Da mesma forma, cada pacote concreto
deve requerer o pacote AIChildForm50.dcp. Não listaremos o código-fonte de TChildForm ou os descendentes
concretos de TChildForm, pois eles não são muito diferentes dos que são mostrados no Capítulo 4. A única
diferença é que cada unidade descendente de TChildForm deve incluir os blocos initialization e finalizati-
on, que é mais ou menos assim:
initialization
RegisterClass(TCF2Form);
finalization
UnRegisterClass(TCF2Form);
A chamada para RegisterClass( ) é necessária para tornar a classe descendente de TChildForm disponí-
vel para o sistema de streaming da aplicação principal quando esta carrega seu pacote. Isso é semelhante
ao modo como RegisterComponents( ) torna os componentes disponíveis para o IDE do Delphi. Quando o
pacote é descarregado, a chamada para UnRegisterClass( ) é exigida para remover a classe registrada. No
entanto, observe que apenas RegisterClass( ) torna a classe disponível para a aplicação principal. A apli-
cação principal ainda sabe o nome da classe. Sendo assim, como a aplicação principal cria uma instância
de uma classe cujo nome de classe é desconhecido? Não é o objetivo deste exercício tornar esses formulá-
rios disponíveis para a aplicação principal, sem ter que programar os seus nomes de classe no códi-
go-fonte da aplicação principal? A Listagem 21.18 mostra o código-fonte para o formulário principal da
aplicação principal, onde destacamos como executamos formulários de add-in com pacotes de add-in.
unit MainFrm;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, ExtCtrls, ChildFrm, Menus;
const
{ Localização do registro do formulário filho no Registro do Windows. }
cCFRegLocation = ‘Software\Delphi 5 Developer’’s Guide’;
cCFRegSection = ‘ChildForms’; // Seção de dados de inicialização do módulo
type
TMainForm = class(TForm)
546
Listagem 21.18 Continuação
pnlMain: TPanel;
Splitter1: TSplitter;
pnlParent: TPanel;
mmMain: TMainMenu;
mmiFile: TMenuItem;
mmiExit: TMenuItem;
mmiHelp: TMenuItem;
mmiForms: TMenuItem;
procedure mmiExitClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
// Referência ao formulário filho.
FChildForm: TChildForm;
// Uma lista de formulários filho disponíveis para montar um menu.
FChildFormList: TStringList;
// Índice para o menu Close Form que muda de posição.
FCloseFormIndex: Integer;
// Alça para o pacote atualmente carregado.
FCurrentModuleHandle: HModule;
// Método para tornar disponíveis os menus para formulários filhos.
procedure CreateChildFormMenus;
// Manipulador para carregar um formulário filho e seu pacote.
procedure LoadChildFormOnClick(Sender: TObject);
// Manipulador para descarregar um formulário filho e seu pacote.
procedure CloseFormOnClick(Sender: TObject);
// Método para recuperar o nome da classe para um descendente de TChildForm
function GetChildFormClassName(const AModuleName: String): String;
public
{ Declarações públicas }
end;
var
MainForm: TMainForm;
implementation
uses Registry;
{$R *.DFM}
procedure TMainForm.CreateChildFormMenus;
{ Todos os formulários filhos disponíveis são registrados no Registro do
Windows. Aqui, usamos essa informação para criar itens de menu para carregar
cada um dos formulários filhos. }
var
IniFile: TRegIniFile;
MenuItem: TMenuItem;
i: integer;
begin
inherited;
for i := 0 to FChildFormList.Count - 1 do
begin
MenuItem := TMenuItem.Create(mmMain);
MenuItem.Caption := FChildFormList.Names[i];
MenuItem.OnClick := LoadChildFormOnClick;
mmiForms.Add(MenuItem);
end;
// Cria separador
MenuItem := TMenuItem.Create(mmMain);
MenuItem.Caption := ‘-’;
mmiForms.Add(MenuItem);
MenuItem.OnClick := CloseFormOnClick;
MenuItem.Enabled := False;
mmiForms.Add(MenuItem);
try
// Carrega o pacote especificado
FCurrentModuleHandle := LoadPackage(ChildFormPackage);
mmiForms[FCloseFormIndex].Enabled := True;
except
on E: Exception do
begin
CloseFormOnClick(nil);
raise;
end;
end;
end;
FCurrentModuleHandle := 0;
mmiForms[FCloseFormIndex].Enabled := False;
Caption := FMainCaption;
end;
end;
end.
Na verdade, a lógica da aplicação é bastante simples. Ela usa o registro do sistema para determinar
os pacotes que estão disponíveis, as teclas de atalho do menu a serem usadas durante a construção de me-
nus para carga em cada pacote e o nome de classe do formulário contido em cada pacote.
NOTA
Incluímos um arquivo chamado D5DG.Reg no qual você pode dar um clique duplo no Windows Explorer.
Este importa as definições do Registro de modo que a demonstração do pacote de add-in seja executada
de modo apropriado.
A variável ChildFormClass é uma referência de classe pré-declarada para TChildForm, e pode fazer uma
referência de classe para uma descendente de TChildForm.
O manipulador de evento CloseFormOnClick( ) simplesmente fecha o formulário filho e descarrega
seu pacote. O restante do código basicamente é configurado para criar menus de pacotes e ler as informa-
ções do registro do sistema.
Um estudo mais profundo sobre essa técnica permitirá que você crie estruturas de aplicação muito
flexíveis e pouco acopladas.
Resumo
É fundamental, para a compreensão do Delphi, saber como os componentes funcionam. Você traba-
lhará com muitos componentes mais personalizados em outras partes deste livro. Agora que você pôde
ver o que acontece nos bastidores, os componentes deixarão de ser um mistério. O próximo capítulo
vai além da criação de componentes, mostrando técnicas muito mais avançadas para a construção de
componentes.
551
Técnicas avançadas CAPÍTULO
com componentes
22
NE STE C AP ÍT UL O
l Componentes pseudovisuais 553
l Componentes animados 556
l Escrita de editores de propriedades 569
l Editores de componentes 578
l Streaming de dados não-publicados do
componente 583
l Categorias de propriedades 592
l Listas de componentes: TCollection e
TCollectionItem 596
l Resumo 615
O capítulo anterior explicou sobre a criação de componentes personalizados do Delphi e proporcionou
uma sólida introdução aos fundamentos. Neste capítulo, você vai aprender a levar a escrita de compo-
nentes para o próximo nível, incorporando técnicas avançadas de projeto nos componentes personaliza-
dos do Delphi. Este capítulo fornece exemplos de técnicas avançadas, como por exemplo, componentes
pseudovisuais, editores de propriedades detalhadas, editores de componentes e coleções.
Componentes pseudovisuais
Você já aprendeu a trabalhar com componentes visuais, como TButton e TEdi, além de componentes
não-visuais, como TTable e TTimer. Nesta seção, você também aprenderá a trabalhar com um tipo de com-
ponente que se encontra a meio caminho entre os componentes visuais e não-visuais – vamos chamar es-
ses componentes de componentes pseudovisuais.
Estendendo dicas
Especificamente, o componente não-visual mostrado nesta seção é uma extensão de uma janela de dica
que o Delphi abre automaticamente. Chamamos esse componente de pseudovisual porque ele não é um
componente usado visualmente a partir da Component Palette durante o projeto, mas se representa visu-
almente em runtime no corpo de dicas pop-up.
Para substituir uma janela de dica no estilo padrão em uma aplicação do Delphi, você precisa execu-
tar as quatro etapas a seguir:
unit RndHint;
interface
type
TDDGHintWindow = class(THintWindow)
private
FRegion: THandle;
procedure FreeCurrentRegion;
public
destructor Destroy; override;
procedure ActivateHint(Rect: TRect; const AHint: string); override;
procedure Paint; override;
553
Listagem 22.1 Continuação
implementation
destructor TDDGHintWindow.Destroy;
begin
FreeCurrentRegion;
inherited Destroy;
end;
procedure TDDGHintWindow.FreeCurrentRegion;
{ Regiões, como outros objetos da API, devem ser liberadas quando você
terminar de usá-las. Observe, no entanto, que você não pode excluir uma
região que é inteiramente definida em uma janela; portanto, esse método
define a região da janela como 0 antes de excluir o objeto da região. }
begin
if FRegion < > 0 then begin // se Region estiver vivo...
SetWindowRgn(Handle, 0, True); // define região win como 0
DeleteObject(FRegion); // encerra região
FRegion := 0; // zera campo
end;
end;
procedure TDDGHintWindow.Paint;
{ Este método chama o manipulador WM_PAINT. Ele é responsável por pintar
a janela de dica. }
var
R: TRect;
554
Listagem 22.1 Continuação
begin
R := ClientRect; // apanha retângulo
Inc(R.Left, 1); // move ligeiramente o lado esquerdo
Canvas.Font.Color := clInfoText; // define cor apropriada
{ string de pintura no centro do retângulo arredondado }
DrawText(Canvas.Handle, PChar(Caption), Length(Caption), R,
DT_NOPREFIX or DT_WORDBREAK or DT_CENTER or DT_VCENTER);
end;
initialization
Application.ShowHint := False; // destrói antiga janela de dica
HintWindowClass := TDDGHintWindow; // atribui nova janela de dica
Application.ShowHint := True; // cria nova janela de dica
end.
l CreateEllipticRgnIndirect( )
l CreatePolygonRgn( )
l CreatePolyPolygonRgn( )
l CreateRectRgn( )
l CreateRectRgnIndirect( )
l CreateRoundRectRgn( )
l ExtCreateRegion( )
Além disso, a função CombineRgn( ) pode ser usada para combinar diversas regiões em uma região
complexa. Todas essas funções são descritas em detalhe na ajuda on-line da API do Win32.
SetWindowRgn( ) é chamada em seguida, passando a alça da região recém-criada como um parâmetro.
Essa função faz com que o sistema operacional pegue a propriedade da região e todos os desenhos subse-
qüentes na janela especificada ocorrerão somente dentro da região. Portanto, se a região definida for um
retângulo arredondado, a pintura ocorrerá apenas dentro dessa região retangular arredondada.
555
ATENÇÃO
Você precisa ter consciência de dois efeitos colaterais ao usar SetWindowRgn( ). Primeiro, como apenas a
porção da janela dentro da região é pintada, a janela provavelmente não terá um quadro ou uma barra de
título. Você deve estar preparado para fornecer ao usuário uma alternativa para mover, dimensionar e fe-
char a janela sem a ajuda de um quadro ou uma barra de título. Segundo, como o sistema operacional as-
sume a propriedade da região especificada em SetWindowRgn( ), você deve tomar cuidado para não mani-
pular ou excluir a região enquanto ela estiver em uso. O componente TDDGHintWindow manipula isso cha-
mando seu método FreeCurrentRegion( ) antes de a janela ser destruída ou uma nova janela ser criada.
Distribuindo TDDGHintWindow
A distribuição desse componente pseudovisual é diferente de componentes visuais e não-visuais normais.
Como todo o trabalho de instanciação do componente é executado na parte initialization de sua unida-
de, a unidade não deve ser adicionada a um pacote de projeto a ser usado na paleta de componentes, mas
tão-somente adicionada à cláusula uses de um dos arquivos-fonte do seu projeto.
Componentes animados
Durante a criação de uma aplicação do Delphi, é possível que nos vejamos diante da seguinte questão:
“Esta é uma aplicação bacana, mas nossa caixa de diálogo About é um tédio só. Temos que dar um jeito
nisso.” De repente, pode dar um estalo e surgir a idéia de um novo componente: criamos uma janela de
letreiro para incorporar em nossas caixas de diálogo About.
O componente de letreiro
Vamos reservar um tempo para ver como o componente de letreiro funciona. O controle de letreiro é ca-
paz de pegar algumas strings e passá-las pelo componente no comando, como um letreiro na vida real.
Você usará TCustomPanel como a classe básica desse componente TddgMarquee, pois ele já tem a funcionalida-
de básica interna de que você precisa, inclusive uma bela borda chanfrada em 3D.
TddgMarquee pinta algumas strings de texto em um bitmap que reside na memória e em seguida copia
trechos do bitmap na memória em sua própria tela a fim de criar o efeito de texto passando. Ele faz isso
usando a função BitBlt( ) da API para copiar uma porção do tamanho do componente da tela da memó-
ria no componente, começando na parte superior desse último. Em seguida, ele se desloca alguns pixels
abaixo na tela da memória e copia essa imagem no controle. Ele se move para baixo novamente, copia
556
novamente e repete o processo até todo o conteúdo da memória ter percorrido todo o componente.
Agora está na hora de identificar as classes adicionais de que você pode precisar para integrar ao
componente TddgMarquee a fim de lhe dar vida. Na verdade, existem apenas duas classes. Primeiro, você
precisa da classe TStringList para armazenar todas as strings que deseja rolar. Segundo, você deve ter um
bitmap de memória no qual possa produzir todas as strings de texto. O componente Tbitmap da própria
VCL desempenha esse papel a contento.
Criando o componente
Assim como os componentes anteriores deste capítulo, o código de TddgMarquee deve ser abordado com
um plano de ataque lógico. Nesse caso, dividimos o trabalho do código em partes razoáveis. O compo-
nente TddgMarquee deve ser dividido em cinco grandes partes:
l O mecanismo que produz o texto na tela da memória
l O mecanismo que copia o texto da tela da memória na janela de letreiro
l O timer que monitora quando e como rola a janela para executar a animação
l O construtor e o destruidor de classe, bem como os métodos associados
l Os toques de acabamento, como diversas propriedades e métodos auxiliadores
NOTA
A função GetTextMetrics( ) da API modifica um registro TTextMetric que contenha um grande volume de
informações sobre a fonte atualmente selecionada para o dispositivo. Essa função dá informações não so-
mente sobre a altura e a largura da fonte, mas também se a fonte está em negrito, itálico, tachada ou mes-
mo o nome do conjunto de caracteres.
O método TextHeight( ) de TCanvas não funcionará aqui. Esse método determina apenas a altura de uma
linha de texto específica, não o espaçamento da fonte em geral.
A altura de uma célula de caractere na fonte atual da tela é dada pelo campo tmHeight do registro Me-
trics.Se você adicionar a esse valor o campo tmInternalLeading – para permitir algum espaço entre as li-
nhas – você obtém a altura de cada linha de texto a ser desenhado na tela da memória:
LineHi := Metrics.tmHeight + Metrics.tmInternalLeading;
Em seguida, a altura necessária para a tela da memória pode ser determinada pela multiplicação de
LineHi pelo número de linhas de texto e pela adição desse valor a duas vezes a altura do controle TddgMar-
quee (para criar o espaço em branco no início e no fim do letreiro). Suponha que a TStringList na qual se
encontram todas as strings seja chamada de FItems; agora coloque as dimensões da tela de memória em
uma estrutura TRect: 557
var
VRect: TRect;
begin
{ Retângulo VRect representa todo o bitmap na memória }
VRect := Rect(0, 0, Width, LineHi * FItems.Count + Height * 2);
end;
DICA
Quando você produz o texto em TCanvas, o segundo plano do texto é preenchido pela cor atual de TCan-
vas.Brush. Para fazer com que o segundo plano do texto seja invisível, defina TCanvas.Brush.Style como
bsClear.
Como o grosso do trabalho preliminar já foi realizado, chegou a hora de produzir o texto no bitmap
na memória. Como dissemos no Capítulo 8, há duas formas de produzir texto em uma tela. A mais obje-
tiva é usar o método TextOut( ) de TCanvas; no entanto, você tem um controle muito mais complexo sobre
a formatação do texto quando usa a função DrawText( ) da API, que é muito mais complexa. Como ela re-
quer controle sobre a justificação, TddgMarquee usa a função DrawText( ). Um tipo enumerado é ideal para
representar a justificação do texto:
type
TJustification = (tjCenter, tjLeft, tjRight);
O código a seguir mostra o método PaintLine( ) de TddgMarquee, que faz uso de DrawText( ) para pro-
duzir texto no bitmap de memória. Neste método, FJust representa uma variável de instância do tipo
TJustification. Veja o código a seguir:
Pintando o componente
Agora que você sabe como criar o bitmap de memória e pintar texto nele, a próxima etapa é aprender a
copiar esse texto na tela TddgMarquee.
O método Paint( ) de um componente é chamado em resposta a uma mensagem WM_PAINT do Win-
dows. O método Paint( ) é o que dá vida a seu componente; você pode usar o método Paint( ) para pin-
tar, desenhar e preencher e, assim, determinar a aparência gráfica dos componentes.
A tarefa de TddgMarquee.Paint( ) é copiar as strings da tela de memória na tela TddgMarquee. Essa proeza
558 é executada pela função BitBlt( ) da API, que copia os bits de um dispositivo de contexto em outro.
Para determinar se TddgMarquee está sendo atualmente executado, o componente manterá uma variá-
vel de instância booleana chamada FActive que revela se a capacidade de rolar do letreiro foi ativada. Por-
tanto, o método Paint( ) pinta de um modo diferente, caso o componente esteja ativo:
procedure TddgMarquee.Paint;
{ Este método virtual é chamado em resposta a uma mensagem de pintura
do Windows }
begin
if FActive then
{ Copia o conteúdo do bitmap na memória na tela }
BitBlt(Canvas.Handle, 0, 0, InsideRect.Right, InsideRect.Bottom,
MemBitmap.Canvas.Handle, 0, CurrLine, srcCopy)
else
inherited Paint;
end;
Se o letreiro estiver ativo, o componente usa a função BitBlt( ) para pintar uma porção da tela na
memória na tela TddgMarquee. Observe a variável CurrLine, que é passada como o parâmetro próximo ao úl-
timo para BitBlt( ). O valor desse parâmetro determina a porção da tela na memória a ser transferida
para a tela. Usando continuamente o recurso de incrementar e decrementar o valor CurrLine, você pode
criar a sensação de que o texto em TddgMarquee está subindo ou descendo.
Animando o letreiro
Os aspectos visuais do componente TddgMarquee já foram definidos. Para que o componente comece a fun-
cionar, faltam apenas alguns detalhes. Neste ponto, TddgMarquee requer algum mecanismo para ficar o
tempo todo mudando o valor de CurrLine e, assim, repintando o componente. É extremamente fácil exe-
cutar esse truque usando o componente TTimer do Delphi.
Antes de poder usar TTimer, é claro, você deve criar e inicializar a instância da classe. TddgMarquee terá
uma instância de TTimer chamada FTimer e você a inicializará em um procedimento chamado DoTimer:
procedure DoTimer;
{ Procedimento configura o timer de TddgMarquee }
begin
FTimer := TTimer.Create(Self);
with FTimer do
begin
Enabled := False;
Interval := TimerInterval;
OnTimer := DoTimerOnTimer;
end;
end;
NOTA
Ao atribuir valores a eventos em seu código, você precisa seguir duas regras:
l O procedimento que você atribui ao evento deve ser um método de alguma instância de objeto. Ele
não pode ser um procedimento ou função independente.
l O método que você atribui ao evento deve aceitar a mesma lista de parâmetros que o tipo do evento.
Por exemplo, o evento OnTimer de TTimer é do tipo TNotifyEvent. Como TNotifyEvent aceita um parâ-
metro, Sender, do tipo TObject, qualquer método que você atribua a OnTimer deve pegar um parâmetro
do tipo TObject.
559
O método DoTimerOnTimer( ) é definido da seguinte maneira:
procedure TddgMarquee.DoTimerOnTimer(Sender: TObject);
{ Este método é executado em resposta a um evento de timer }
begin
IncLine;
{ repinta apenas dentro das bordas }
InvalidateRect(Handle, @InsideRect, False);
end;
Na verdade, o construtor TddgMarquee é bastante simples. Ele chama o método Create( ) herdado, cria
uma instância de TStringList, configura FTimer e em seguida define todos os valores-padrão das variáveis
da instância. Mais uma vez, você deve se lembrar de chamar o método Create( ) herdado nos seus compo-
nentes. Se ocorrer uma falha nesse processo, os componentes deixarão de executar uma funcionalidade
importante e útil, como por exemplo a criação de uma alça e uma tela, streaming e resposta a uma mensa-
gem do Windows. O código a seguir mostra o construtor de TddgMarquee, Create( ):
constructor TddgMarquee.Create(AOwner: TComponent);
{ construtor da classe TddgMarquee }
procedure DoTimer;
{ procedimento configura o timer de TddgMarquee }
begin
FTimer := TTimer.Create(Self);
with FTimer do
begin
560
Enabled := False;
Interval := TimerInterval;
OnTimer := DoTimerOnTimer;
end;
end;
begin
inherited Create(AOwner);
FItems := TStringList.Create; { instancia lista de string }
DoTimer; { configura timer }
{ define valores-padrão da variável de instância }
Width := 100;
Height := 75;
FActive := False;
FScrollDown := False;
FJust := tjCenter;
BevelWidth := 3;
end;
O destruidor TddgMarquee é ainda mais simples: o método desativa o componente passando False
como método de SetActive( ), libera o timer e a lista de strings e em seguida chama o método Destroy( )
herdado:
destructor TddgMarquee.Destroy;
{ destruidor da classe TddgMarquee }
begin
SetActive(False);
FTimer.Free; // libera objetos alocados
FItems.Free;
inherited Destroy;
end;
DICA
Via de regra, quando você modifica construtores, geralmente chama primeiro inherited e quando modifi-
ca destruidores, você geralmente chama o inherited no fim. Isso garante que a classe foi configurada an-
tes de ser modificada e que todos os recursos dependentes foram excluídos antes de você dispor da classe.
Há algumas exceções a essa regra; no entanto, você só não deve segui-las se tiver uma razão muito forte
para isso.
O método SetActive( ), que é chamado pelo método IncLine( ) e pelo destruidor (além de servir
como o criador da propriedade Active), serve como um veículo que começa e termina a rolagem do letrei-
ro pela tela:
procedure TddgMarquee.SetActive(Value: Boolean);
{ chamado para ativar/desativar o letreiro }
begin
if Value and (not FActive) and (FItems.Count > 0) then
begin
FActive := True; // define flag de ativo
MemBitmap := TBitmap.Create;
FillBitmap; // pinta imagem no bitmap
FTimer.Enabled := True; // inicia timer
end
561
else if (not Value) and FActive then
begin
FTimer.Enabled := False; // desativa timer,
if Assigned(FOnDone) // dispara evento OnDone,
then FOnDone(Self);
FActive := False; // define FActive como False
MemBitmap.Free; // libera bitmap da memória
Invalidate; // apaga controle de janela
end;
end;
Um importante recurso de TddgMarquee até agora ausente é um evento que diz ao usuário quando o
letreiro acabou de passar. Não tema – esse recurso é extremamente simples de se adicionar através de
um evento: FonDone. O primeiro passo para adicionar um evento é declarar uma variável de instância
de algum tipo de evento na porção private da definição de classe. Você usará o tipo TNotifyEvent do
evento FOnDone:
FOnDone: TNotifyEvent;
O evento deve ser declarado em seguida, na parte published da classe, como uma propriedade:
property OnDone: TNotifyEvent read FOnDone write FOnDone;
Lembre-se de que as diretivas read e write especificam de qual função ou variável uma dada proprie-
dade deve obter ou definir seu valor.
Esses dois pequenos passos farão com que uma entrada para OnDone seja exibida na página Events do
Object Inspector durante o projeto. A única coisa que precisa ser feita é chamar o manipulador de OnDone
do usuário (se um método tiver sido atribuído a OnDone), como demonstrado por TddgMarquee com a linha
de código a seguir no método Deactivate( ):
if Assigned(FOnDone) then FOnDone(Self); // dispara evento OnDone
Basicamente, essa linha tem o seguinte significado: “Se o usuário do componente tiver atribuído um
método ao evento OnDone, chame esse método e passe a instância da classe TddgMarquee (Self) como um pa-
râmetro.”
A Listagem 22.2 mostra o código-fonte da unidade Marquee. Observe que, como o componente des-
cende de uma classe de TCustomXXX, você precisa publicar muitas das propriedades fornecidas por TCustom-
Panel.
unit Marquee;
interface
uses
SysUtils, Windows, Classes, Forms, Controls, Graphics,
Messages, ExtCtrls, Dialogs;
const
ScrollPixels = 3; // número de pixels para cada rolagem
TimerInterval = 50; // tempo entre rolagens em ms
type
TJustification = (tjCenter, tjLeft, tjRight);
EMarqueeError = class(Exception);
562
Listagem 22.2 Continuação
TddgMarquee = class(TCustomPanel)
private
MemBitmap: TBitmap;
InsideRect: TRect;
FItems: TStringList;
FJust: TJustification;
FScrollDown: Boolean;
LineHi : Integer;
CurrLine : Integer;
VRect: TRect;
FTimer: TTimer;
FActive: Boolean;
FOnDone: TNotifyEvent;
procedure SetItems(Value: TStringList);
procedure DoTimerOnTimer(Sender: TObject);
procedure PaintLine(R: TRect; LineNum: Integer);
procedure SetLineHeight;
procedure SetStartLine;
procedure IncLine;
procedure SetActive(Value: Boolean);
protected
procedure Paint; override;
procedure FillBitmap; virtual;
public
property Active: Boolean read FActive write SetActive;
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
published
property ScrollDown: Boolean read FScrollDown write FScrollDown;
property Justify: TJustification read FJust write FJust default tjCenter;
property Items: TStringList read FItems write SetItems;
property OnDone: TNotifyEvent read FOnDone write FOnDone;
{ Publica propriedades herdadas: }
property Align;
property Alignment;
property BevelInner;
property BevelOuter;
property BevelWidth;
property BorderWidth;
property BorderStyle;
property Color;
property Ctl3D;
property Font;
property Locked;
property ParentColor;
property ParentCtl3D;
property ParentFont;
property Visible;
property OnClick;
property OnDblClick;
property OnMouseDown;
property OnMouseMove;
property OnMouseUp;
563
Listagem 22.2 Continuação
property OnResize;
end;
implementation
procedure DoTimer;
{ procedimento configura timer de TddgMarquee }
begin
FTimer := TTimer.Create(Self);
with FTimer do
begin
Enabled := False;
Interval := TimerInterval;
OnTimer := DoTimerOnTimer;
end;
end;
begin
inherited Create(AOwner);
FItems := TStringList.Create; { instancia lista de strings }
DoTimer; { configura timer }
{ define valores-padrão da variável da instância }
Width := 100;
Height := 75;
FActive := False;
FScrollDown := False;
FJust := tjCenter;
BevelWidth := 3;
end;
destructor TddgMarquee.Destroy;
{ destruidor da classe TddgMarquee }
begin
SetActive(False);
FTimer.Free; // dispara objetos alocados
FItems.Free;
inherited Destroy;
end;
procedure TddgMarquee.IncLine;
{ Este método é chamado para incrementar uma linha }
begin
564
Listagem 22.2 Continuação
procedure TddgMarquee.SetLineHeight;
{ este método virtual define a variável da instância LineHi }
var
Metrics : TTextMetric;
begin
{ obtém informações métricas para a fonte }
GetTextMetrics(Canvas.Handle, Metrics);
{ ajusta altura da linha }
LineHi := Metrics.tmHeight + Metrics.tmInternalLeading;
end;
procedure TddgMarquee.SetStartLine;
{ este método virtual inicializa a variável da instância CurrLine }
begin
// inicializa linha atual para o topo se rolando para cima, ou...
if not FScrollDown then CurrLine := 0
// para baixo se rolando para baixo
else CurrLine := VRect.Bottom - Height;
end;
procedure TddgMarquee.FillBitmap;
var
y, i : Integer;
R: TRect;
begin
SetLineHeight; // define altura de cada linha
{ Retângulo VRect representa todo o bitmap na memória }
VRect := Rect(0, 0, Width, LineHi * FItems.Count + Height * 2);
{ Retângulo InsideRect representa o interior de borda chanfrada }
InsideRect := Rect(BevelWidth, BevelWidth, Width - (2 * BevelWidth),
Height - (2 * BevelWidth));
R := Rect(InsideRect.Left, 0, InsideRect.Right, VRect.Bottom);
SetStartLine;
MemBitmap.Width := Width; // inicializa bitmap na memória
with MemBitmap do
begin
Height := VRect.Bottom;
with Canvas do
begin
Font := Self.Font;
Brush.Color := Color;
FillRect(VRect);
Brush.Style := bsClear;
end;
end;
y := Height;
i := 0;
repeat
R.Top := y;
PaintLine(R, i);
{ incrementa y pela altura (em pixels) de uma linha }
inc(y, LineHi);
inc(i);
until i >= FItems.Count; // repete para todas as linhas
end;
procedure TddgMarquee.Paint;
{ este método virtual é chamado em resposta a uma mensagem de pintura
do Windows }
begin
if FActive then
{ Copia do bitmap na memória na tela }
BitBlt(Canvas.Handle, 0, 0, InsideRect.Right, InsideRect.Bottom,
MemBitmap.Canvas.Handle, 0, CurrLine, srcCopy)
else
inherited Paint;
end;
566
Listagem 22.2 Continuação
end.
DICA
Observe a diretiva default e o valor usado com a propriedade Justify de TddgMarquee. Esse uso de default
otimiza o streaming do componente, que, por sua vez, melhora o tempo de projeto do componente. Você
pode dar valores-padrão a propriedades de qualquer tipo ordinal (Integer, Word, Longint, bem como tipos
enumerados, por exemplo), mas você não pode dar a eles tipos de propriedade não-ordinais, como por
exemplo strings, números de ponto flutuante, arrays, registros e classes.
Você também precisa inicializar os valores-padrão das propriedades no seu construtor. Se você não o fizer,
terá problemas no streaming.
Testando TddgMarquee
Embora seja muito excitante finalmente ter esse componente escrito e poder testar os estágios, não se ar-
vore a tentar adicioná-lo à Component Palette. Primeiro ele tem que ser depurado. Você deve fazer to-
dos os testes preliminares com o componente criando um projeto que crie e use uma instância dinâmica
do componente. A Listagem 22.3 descreve a unidade principal de um projeto chamado TestMarq, que é
usada para testar o componente TddgMarquee. Este simples projeto consiste em um formulário que contém
dois botões.
unit Testu;
interface
uses
SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls, 567
Listagem 22.3 Continuação
type
TForm1 = class(TForm)
Button1: TButton;
Button2: TButton;
procedure FormCreate(Sender: TObject);
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
private
Marquee1: TddgMarquee;
procedure MDone(Sender: TObject);
public
{ Declarações públicas }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
begin
Marquee1.Active := True;
end;
end.
DICA
Crie sempre um projeto de teste para os novos componentes. Nunca tente fazer um teste inicial em um com-
ponente adicionando-o à Component Palette. Ao tentar depurar um componente que resida na paleta,
não apenas você desperdiçará tempo com a reconstrução de um monte de pacotes desnecessários, mas é
possível que dê pau na IDE em decorrência de um bug no componente.
Depois de se livrar de todos os bugs que você encontrar neste programa, chegou a hora de adicio-
ná-lo à Component Palette. Como você deve se lembrar, isso é fácil: basta escolher Component, Install
Component no menu principal e em seguida preencher o nome de pacote e o nome de arquivo da unida-
de na caixa de diálogo Install Component. Escolha OK e o Delphi reconstruirá o pacote no qual o com-
ponente foi adicionado e atualizará a Component Palette. É claro que o componente precisará expor um
procedimento Register( ) para ser inserido na Component Palette. O componente TddgMarquee é registra-
do na unidade DDGReg.pas do pacote DDGDsgn no CD-ROM que acompanha esta edição.
570
O editor de propriedades do qual seu editor de propriedades deve descender depende do modo
como a propriedade vai se comportar quando for editada. Em alguns casos, por exemplo, sua proprieda-
de pode exigir a mesma funcionalidade que TIntegerProperty, mas também deve exigir lógica adicional no
processo de edição. Portanto, seria lógico que sua propriedade descendesse de TIntegerProperty.
DICA
Lembre-se de que há casos em que você não precisa criar um editor de propriedades que depende de seu
tipo de propriedade. Por exemplo, tipos de subfaixa são verificados automaticamente (por exemplo, 1..10 é
verificado por TIntegerProperty), tipos enumerados obtêm listas drop-down automaticamente e assim por
diante. Você deve tentar usar definições de tipo em vez de editores de propriedades personalizados, pois
são forçados pela linguagem no tempo de compilação bem como pelos editores de propriedades padrão.
TIntegerProperty = class(TOrdinalProperty)
public
function GetValue: string; override;
procedure SetValue(const Value: string); override;
end;
Veja a seguir que os métodos GetValue( ) e SetValue( ) foram modificados. A implementação de Get-
Value( ) é feita da seguinte forma:
571
GetValue( ) retorna a representação de string de uma propriedade integer. O Object Inspector usa
este valor para exibir o valor da propriedade. GetOrdValue( )é um método definido de TPropertyEditor e é
usado para recuperar o valor da propriedade a que o editor de propriedades faz referência.
SetValue( ) pega o valor de string inserido pelo usuário e o atribui à propriedade no formato corre-
to. SetValue( ) também executa alguma verificação de erro para garantir que o valor esteja dentro de uma
faixa de valores especificada. Isso ilustra o modo como você pode executar a verificação de erro com os
editores de propriedades do descendente. O método SetOrdValue( ) atribui o valor à propriedade a que o
editor de propriedades faz referência.
TPropertyEditor define diversos métodos semelhantes a GetOrdValue( ) para obter a representação de
string de diversos tipos. Além disso, TPropertyEditor contém os métodos “set” equivalentes para definir os
valores em seu respectivo formato. Os descendentes de TPropertyEditor herdam esses métodos. Esses mé-
todos são usados para obter e definir os valores das propriedades a que o editor de propriedades faz refe-
rência. A Tabela 22.2 mostra esses métodos.
Para ilustrar a criação de um novo editor de propriedades, vamos nos divertir um pouco mais com o
exemplo do sistema solar apresentado no último capítulo. Dessa vez, criamos apenas um componente,
TPlanet, para representar um planeta. TPlanet contém a propriedade PlanetName. O armazenamento inter-
no de PlanetName vai ser feito em um tipo integer e armazenará a posição do planeta no sistema solar. No
entanto, ela será exibida no Object Inspector como o nome do planeta.
Até agora isso parece fácil, mas veja esta armadilha: queremos permitir que o usuário digite dois va-
lores para representar o planeta. O usuário deve ser capaz de digitar o nome do planeta como uma string,
como por exemplo Venus, VENUS ou VeNuS. Ele também deve ser capaz de digitar a posição do planeta no sis-
tema solar. Portanto, para o planeta Vênus, o usuário digitaria o valor numérico 2.
Veja a seguir como seria o componente Tplanet:
type
TPlanetName = type Integer;
TPlanet = class(TComponent)
private
FPlanetName: TPlanetName;
published
property PlanetName: TPlanetName read FPlanetName write FPlanetName;
end;
Como você pode ver, não há muito o que fazer nesse componente. Ele só tem uma propriedade:
PlanetName do tipo TPlanetName. Aqui, a definição especial de TPlanetName é usada de modo que a ela seja
dada sua própria RTTI, ainda que ela seja tratada como um tipo integer.
Essa funcionalidade não provém do componente TPlanet; na verdade, ela provém do editor de pro-
priedades do tipo de propriedade TPlanetName. Este editor de propriedades é mostrado na Listagem 22.4.
572
Listagem 22.4 PlanetPE.PAS – o código-fonte de TPlanetNameProperty
unit PlanetPE;
interface
uses
Windows, SysUtils, DsgnIntF;
type
TPlanetNameProperty = class(TIntegerProperty)
public
function GetValue: string; override;
procedure SetValue(const Value: string); override;
end;
implementation
const
{ Declara um array de constante contendo nomes de planeta }
PlanetNames: array[1..9] of String[7] =
(‘Mercury’, ‘Venus’, ‘Earth’, ‘Mars’, ‘Jupiter’, ‘Saturn’,
‘Uranus’, ‘Neptune’, ‘Pluto’);
[Value]));
if (i <= 0) or (i >= 10) then
raise Exception.Create(‘Sorry, that planet is not in OUR solar system.’);
SetOrdValue(i);
end;
end;
end.
DICA
Embora, para fins de ilustração, esse editor de propriedades em particular seja registrado para ser usado
apenas com o componente TPlanet e o nome de propriedade ‘PlanetName’, você pode escolher ser menos
restritivo ao registrar seus próprios editores de propriedades personalizados. Definindo o parâmetro Compo-
nentClass como nil e o parâmetro PropertyName como ‘’, seu editor de propriedades funcionará para
qualquer tipo de propriedade TPlanetName do componente.
574
Você pode registrar o editor de propriedades juntamente com o registro do componente na unida-
de do componente, mostrada na Listagem 22.5.
unit Planet;
interface
uses
Classes, SysUtils;
type
TPlanetName = type Integer;
TddgPlanet = class(TComponent)
private
FPlanetName: TPlanetName;
published
property PlanetName: TPlanetName read FPlanetName write FPlanetName;
end;
implementation
end.
DICA
A inserção do registro do editor de propriedades no procedimento Register( ) da unidade do componente
fará com que todo o código do editor de propriedades seja vinculado ao componente quando ele for colo-
cado em um pacote. Para componentes complexos, as ferramentas de tempo de projeto ocupam mais es-
paço em código do que os próprios componentes. Embora o tamanho do código não seja uma questão
pertinente para um pequeno componente como este, não se esqueça de que tudo que está listado na seção
interface da unidade do componente (como o procedimento Register( )), bem como tudo que ele toca
(como o tipo de classe do editor de propriedade), acompanhará o componente quando ele for compilado
em um pacote. Por essa razão, pode ser que você deseje executar o registro do editor de propriedades em
uma unidade separada. Além disso, alguns criadores de componente escolhem criar pacotes de tempo de
projeto e de runtime para seus componentes, pois os editores de propriedades e outras ferramentas de tem-
po de projeto residem apenas no pacote de tempo de projeto. Você perceberá que os pacotes contendo o
código do livro fazem isso usando o pacote de runtime DdgStd5 e o pacote de projeto DdgDsgn5.
Mais uma vez, essa é uma declaração especial que anexa uma RTTI (Runtime Type Information)
exclusiva desse tipo. Isso permite que você defina um editor de propriedades específico para o tipo TCom-
mandLine. Além disso, como TCommandLine é tratado como uma string, o editor de propriedades para editar
as propriedades da string também se aplica ao tipo TCommandLine.
Além disso, como ilustramos o editor de propriedades para o tipo TCommandLine, não se esqueça de
que TddgRunButton já incluiu a verificação de erro necessária das atribuições de propriedade no método
de acesso das propriedades. Portanto, não é necessário repetir essa verificação de erro na lógica do editor
de propriedades.
A Listagem 22.6 mostra a definição do editor de propriedades TCommandLineProperty.
unit runbtnpe;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls, Buttons, DsgnIntF, TypInfo;
type
implementation
procedure TCommandLineProperty.Edit;
{ O método Edit exibe uma TOpenDialog, a partir da qual o usuário obtém
o nome do arquivo de um executável que é atribuído à propriedade }
var
OpenDialog: TOpenDialog;
begin
{ Cria a TOpenDialog }
576 OpenDialog := TOpenDialog.Create(Application);
Listagem 22.6 Continuação
try
{ Mostra apenas os arquivos executáveis }
OpenDialog.Filter := ‘Executable Files|*.EXE’;
{ Se o usuário selecionar um arquivo, atribui-o à propriedade. }
if OpenDialog.Execute then
SetStrValue(OpenDialog.FileName);
finally
OpenDialog.Free // Libera a instância TOpenDialog.
end;
end;
end.
Uma análise de TCommandLineProperty mostra que o editor de propriedades, em si, é muito simples.
Primeiro, observe que ele descende de TStringProperty e, portanto, as capacidades de edição de string são
mantidas. Portanto, no Object Inspector não é necessário chamar a caixa de diálogo. O usuário só pode
digitar a linha de comando diretamente. Além disso, não vamos modificar os métodos SetValue( ) e GetVa-
lue( ), pois TStringProperty já manipula isso corretamente. No entanto, era necessário modificar o méto-
do GetAttributes( ) para que o Object Inspector saiba que essa propriedade é capaz de ser editada com
uma caixa de diálogo. GetAttributes( ) merece um pouco mais de discussão.
paValueList Retorna uma lista de valores enumerados para a propriedade. O método GetValues( )
preenche a lista. Um botão de seta drop-down aparece à direita do valor de propriedade.
Isso se aplica a propriedades enumeradas como TForm.BorderStyle e a grupos de const de
integer como TColor and TCharSet.
paSubProperties Subpropriedades são exibidas recuadas abaixo da propriedade atual no formato de tópicos.
paValueList também deve ser definido. Isso se aplica a propriedades set e a propriedades de
classe como TOpenDialog.Options e TForm.Font.
paDialog Um botão de elipse é exibido à direita da propriedade no Object Inspector, que, ao ser
pressionado, faz com que o método Edit( ) do editor de propriedades chame uma caixa de
diálogo. Isso se aplica a propriedades como TForm.Font
paMultiSelect Propriedades exibidas quando mais de um componente é selecionado no Form Designer,
permitindo que o usuário mude os valores de propriedade para múltiplos componentes de
uma só vez. Algumas propriedades não são apropriadas para essa capacidade, como a
propriedade Name.
577
Tabela 22.3 Continuação
paAutoUpdate SetValue( ) é chamado em cada mudança feita na propriedade. Se esse flag não for
definido, SetValue( ) é chamado quando o usuário pressiona Enter ou move a propriedade
para fora do Object Inspector. Isso se aplica a propriedades como TForm.Caption
paFullWidthName Diz ao Object Inspector que o valor não precisa ser produzido e, portanto, o nome deve
ocupar toda a largura do inspetor.
paSortList O Object Inspector classifica a lista retornada por GetValues( )
paReadOnly O valor da propriedade não pode ser mudado.
paRevertable A propriedade pode ser revertida para seu valor original. Algumas propriedades, como as
propriedades aninhadas, não devem ser revertidas. TFont é um exemplo disso.
NOTA
Você deve dar uma olhada em DsgnIntf.pas e observar que os flags de TPropertyAttribute são definidos
para diversos editores de propriedades.
Registrando TCommandLineProperty
A última etapa necessária à implementação do editor de propriedades TCommandLineProperty é registrá-lo
usando o procedimento RegisterPropertyEditor( ), já discutido neste capítulo. Esse procedimento foi adi-
cionado ao procedimento Register( ) em DDGReg.pas no pacote DDGDsgn:
RegisterComponents(‘DDG’, [TddgRunButton]);
RegisterPropertyEditor(TypeInfo(TCommandLine), TddgRunButton,
‘’, TCommandLineProperty);
Além disso, observe que as unidades DsgnIntf e RunBtnPE tinham que ser adicionadas à cláusula uses.
Editores de componentes
Os editores de componentes estendem o comportamento do tempo de projeto de seus componentes per-
mitindo-lhe adicionar itens ao menu local associado a um determinado componente e permitindo que
você altere a ação-padrão quando um componente recebe um duplo clique no Form Designer. Você está
familiarizado com os editores de componente mesmo sem saber, pois já usou o editor de campos forneci-
do com os componentes TTable, TQuery e TStoredProc.
TComponentEditor
Você pode não ter consciência disso, mas um editor de componentes diferente é criado para cada com-
ponente que é selecionado no Form Designer. O tipo de editor de componentes criado depende do tipo
do componente, muito embora todos os editores de componente descendam de TComponentEditor. Essa
578 classe é definida em DsgnIntf da seguinte forma:
type
TComponentEditor = class(TInterfacedObject, IComponentEditor)
private
FComponent: TComponent;
FDesigner: IFormDesigner;
public
constructor Create(AComponent: TComponent; ADesigner: IFormDesigner);
virtual;
procedure Edit; virtual;
procedure ExecuteVerb(Index: Integer); virtual;
function GetIComponent: IComponent;
function GetDesigner: IFormDesigner;
function GetVerb(Index: Integer): string; virtual;
function GetVerbCount: Integer; virtual;
procedure Copy; virtual;
property Component: TComponent read FComponent;
property Designer: IFormDesigner read GetDesigner;
end;
Propriedades
A propriedade Component de TComponentEditor é a instância do componente que você está editando. Como
essa propriedade é do tipo TComponent genérico, você deve coagir o tipo da propriedade a fim de acessar os
campos introduzidos pelas classes descendentes.
A propriedade Designer é a instância de TFormDesigner que atualmente está hospedando a aplicação
durante o projeto. Você vai achar a definição para essa classe na unidade DsgnIntf.pas.
Métodos
O método Edit( ) é chamado quando o usuário dá um clique duplo no componente durante o projeto.
Com freqüência, esse método chamará algum tipo de caixa de diálogo de projeto. O comportamento-
padrão para esse método é chamar ExecuteVerb(0) se GetVerbCount( ) retornar um valor de 1 ou maior.
Você deve chamar Designer.Modified( ) se você modificar o componente desse (ou qualquer) método.
O método GetVerbCount( ) é chamado para recuperar o número de itens que são adicionados ao
menu local.
GetVerb( ) aceita um integer, Index, e retorna uma string contendo o texto que deve aparecer no
menu local na posição correspondente a Index.
Quando um item é escolhido no menu local, o método ExecuteVerb( ) é chamado. Esse método rece-
be o índice baseado em zero do item selecionado no menu local no parâmetro Index. Você deve respon-
der executando qualquer que seja a ação necessária com base no verbo que o usuário selecionou no menu
local.
O método Paste( ) é chamado sempre que o componente é colado no Clipboard. O Delphi insere a
imagem do stream arquivado do componente no Clipboard, mas você pode usar esse método para colar
dados no Clipboard em um tipo de formato diferente.
TDefaultEditor
Se um editor de componentes personalizado não for registrado para um determinado componente, esse
componente usará o editor de componentes padrão, TDefaultEditor. TDefaultEditor modifica o comporta-
mento do método Edit( ) de modo que ele procure as propriedades do componente e gere (ou navegue
para) o evento OnCreate, OnChanged ou OnClick (o que for localizado primeiro).
579
Um componente simples
Considere o componente personalizado simples a seguir:
type
TComponentEditorSample = class(TComponent)
protected
procedure SayHello; virtual;
procedure SayGoodbye; virtual;
end;
procedure TComponentEditorSample.SayHello;
begin
MessageDlg(‘Hello, there!’, mtInformation, [mbOk], 0);
end;
procedure TComponentEditorSample.SayGoodbye;
begin
MessageDlg(‘See ya!’, mtInformation, [mbOk], 0);
end;
Como você pode ver, esse sujeito não faz muita coisa: é um componente não-visual que descende
diretamente de TComponent e contém dois métodos, SayHello( ) e SayGoodbye( ), que simplesmente exibem
caixas de diálogo de mensagem.
O método GetVerbCount( ) retorna 2, indicando que há dois diferentes verbos que o editor de compo-
nentes está preparado para executar. GetVerb( ) retorna uma string para cada um desses verbos aparecer
no menu local. O método ExecuteVerb( ) chama o método apropriado dentro do componente, baseado no
índice de verbos que ele recebe como um parâmetro.
O primeiro parâmetro para essa função é o tipo de componente para o qual você deseja registrar um
editor de componentes e o segundo parâmetro é o editor de componentes propriamente dito.
A Listagem 22.7 mostra a unidade CompEdit.pas, que inclui as chamadas componente, editor de com-
ponentes e registro. A Figura 22.3 mostra o menu local associado ao componente TComponentEditorSample e
a Figura 22.4 exibe o resultado da seleção de um dos verbos do menu local.
unit CompEdit;
interface
uses
SysUtils, Windows, Messages, Classes, Graphics, Controls, Forms, Dialogs,
DsgnIntf;
type
TComponentEditorSample = class(TComponent)
protected
procedure SayHello; virtual;
procedure SayGoodbye; virtual;
end;
TSampleEditor = class(TComponentEditor)
private
procedure ExecuteVerb(Index: Integer); override;
function GetVerb(Index: Integer): string; override;
function GetVerbCount: Integer; override;
end;
implementation
{ TComponentEditorSample }
procedure TComponentEditorSample.SayHello;
begin
581
Listagem 22.7 Continuação
procedure TComponentEditorSample.SayGoodbye;
begin
MessageDlg(‘See ya!’, mtInformation, [mbOk], 0);
end;
{ TSampleEditor }
const
vHello = ‘Hello’;
vGoodbye = ‘Goodbye’;
end.
582
FIGURA 22.4 O resultado da seleção de um verbo.
Definindo propriedades
A primeira etapa na definição de “propriedades” não-publicadas persistentes é modificar o método Defi-
neProperties( ) do componente. Esse método é herdado de TPersistent e é definido da seguinte maneira:
Como padrão, esse método manipula propriedades publicadas de escrita e leitura para/do arquivo
DFM. Você pode modificar esse método e, depois de chamar inherited, pode chamar os métodos Define-
Property( ) ou DefineBinaryProperty( ) de TFiler, um para cada peça de dados que você deseja tornar parte
do arquivo DFM. Esses métodos são definidos, respectivamente, da seguinte maneira:
procedure DefineProperty(const Name: string; ReadData: TReaderProc;
WriteData: TWriterProc; HasData: Boolean); virtual;
DefineProperty( ) é usado para todos os tipos de dados-padrão persistentes, como os tipos strings, in-
tegers, Booleans, chars, floats e enumerated. DefineBinaryProperty( ) é usado para fornecer acesso a dados
binários brutos, como dados gráficos ou de som, escritos no arquivo DFM.
Para ambas as funções, o parâmetro Name identifica o nome da propriedade que deve ser escrita no
arquivo DFM. Isso não tem o mesmo nome interno do campo de dados que você está acessando. Os pa-
râmetros ReadData e WriteData diferem em tipo entre DefineProperty( ) e DefineBinaryProperty( ), mas ser-
vem ao mesmo propósito: esses métodos são chamados para escrever ou ler dados para/do arquivo
DFM. (Vamos aprofundar essa discussão dentro em breve.) O parâmetro HasData indica se a “proprieda-
de” tem dados que precisa armazenar.
Os parâmetros ReadData e WriteData de DefineProperty( ) são do tipo TReaderProc e TWriterProc, respecti-
vamente. Esses tipos são definidos da seguinte maneira:
type
TReaderProc = procedure(Reader: TReader) of object;
TWriterProc = procedure(Writer: TWriter) of object;
583
TReader e TWriter são descendentes especializados de TFiler que têm métodos adicionais para ler e es-
crever tipos nativos. Os métodos desses tipos fornecem um canal entre os dados publicados do compo-
nente e o arquivo DFM.
Os parâmetros ReadData e WriteData de DefineBinaryProperty( ) são do tipo TStreamProc, que é definido
da seguinte maneira:
type
TStreamProc = procedure(Stream: TStream) of object;
Como os métodos do tipo TStreamProc recebem apenas TStream como um parâmetro, isso permite que
você leia e escreva dados binários muito facilmente para/do stream. Como os outros tipos de método des-
critos anteriormente, os métodos desse tipo fornecem o elo entre dados não-padrão e o arquivo DFM.
Um exemplo de DefineProperty( )
Para reunir todas as informações técnicas, a Listagem 22.8 mostra a unidade DefProp.pas. Essa unidade
ilustra o uso de DefineProperty( ) fornecendo armazenamento para dois campos de dados privados: uma
string e um integer.
unit DefProp;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs;
type
TDefinePropTest = class(TComponent)
private
FString: String;
FInteger: Integer;
procedure ReadStrData(Reader: TReader);
procedure WriteStrData(Writer: TWriter);
procedure ReadIntData(Reader: TReader);
procedure WriteIntData(Writer: TWriter);
protected
procedure DefineProperties(Filer: TFiler); override;
public
constructor Create(AOwner: TComponent); override;
end;
implementation
inherited DefineProperties(Filer);
{ Define novas propriedades e métodos de leitura/escrita }
Filer.DefineProperty(‘StringProp’, ReadStrData, WriteStrData,
FString < > ‘’);
Filer.DefineProperty(‘IntProp’, ReadIntData, WriteIntData, True);
end;
end.
ATENÇÃO
Use sempre os métodos ReadString( ) e WriteString( ) de TReader e TWriter para ler e escrever dados de
string. Nunca use os métodos ReadStr( ) e WriteStr( ) semelhantes, pois danificarão o seu arquivo DFM.
Para demonstrar que a prova está no ponto, a Figura 22.5 mostra um formulário contendo um com-
ponente TDefinePropTest, como texto, no Code Editor do Delphi. Observe que as novas propriedades fo-
ram escritas no arquivo.
FIGURA 22.5 Exibindo um formulário como texto para ver as propriedades. 585
TddgWaveFile: um exemplo de DefineBinaryProperty( )
Já dissemos que uma boa hora para usar DefineBinaryProperty( ) é quando você precisa armazenar infor-
mações gráficas ou de som juntamente com um componente. Na verdade, a VCL usa esta técnica para ar-
mazenar imagens associadas a componentes – o Glyph de um TBitBtn, por exemplo, ou o Icon de um TForm.
Nesta seção, você vai aprender a usar esta técnica durante o armazenamento do som associado ao com-
ponente TddgWaveFile.
NOTA
TddgWaveFile é um componente repleto de recursos, que contém uma propriedade personalizada, um edi-
tor de propriedades e um editor de componentes para permitir que você produza sons durante o projeto.
Ainda neste capítulo, você vai aprender a capturar tudo isso através do código, mas por enquanto vamos
focar a discussão no mecanismo de armazenamento da propriedade binária.
begin
inherited DefineProperties(Filer);
Filer.DefineBinaryProperty(‘Data’, ReadData, WriteData, DoWrite);
end;
Este método define uma propriedade binária chamada Data, que é lida e escrita usando os métodos
ReadData( ) e WriteData( ) do componente. Além disso, os dados só são escritos se o valor de retorno de
DoWrite( ) for True. (Você vai aprender mais sobre DoWrite( ) dentro de alguns instantes.)
Os métodos ReadData( ) e WriteData( ) são definidos da seguinte maneira:
procedure TddgWaveFile.ReadData(Stream: TStream);
{ Lê dados WAV do stream DFM. }
begin
LoadFromStream(Stream);
end;
Como você pode ver, não há muito o que se dizer sobre esse métodos; eles simplesmente chamam
os métodos LoadFromStream( ) e SaveToStream( ), que também são definidos pelo componente TddgWaveFile.
Veja a seguir o método LoadFromStream( ):
procedure TddgWaveFile.LoadFromStream(S: TStream);
{ Carrega dados WAV do stream S. Esse procedimento liberará qualquer
586 memória anteriormente alocada para FData. }
begin
if not Empty then
FreeMem(FData, FDataSize);
FDataSize := 0;
FData := AllocMem(S.Size);
FDataSize := S.Size;
S.Read(FData^, FDataSize);
end;
Esse método primeiro verifica se a memória foi alocada testando o valor do campo FDataSize. Se ele
for maior do que zero, a memória apontada pelo campo FData foi liberada. Nesse ponto, um novo bloco
de memória é alocada para FData e FDataSize é definido como o tamanho do stream de dados de entrada.
O conteúdo do stream é lido no ponteiro FData.
O método SaveToStream( ) é muito mais simples; ele é definido da seguinte maneira:
procedure TddgWaveFile.SaveToStream(S: TStream);
{ Salva dados WAV no stream S. }
begin
if FDataSize > 0 then
S.Write(FData^, FDataSize);
end;
Esse método escreve os dados apontados pelo ponteiro FData para TStream S.
A função DoWrite( ) local dentro do método DefineProperties( ) determina se Data precisa ser incluí-
do no stream. É claro que, se FData estiver vazio, não há necessidade de se criar um stream de dados. Além
disso, você deve tomar medidas extras para garantir que o seu componente funcione corretamente com
herança de formulário: você deve verificar se a propriedade Ancestor de Filer não é nil. Se for e apontar
para uma versão ancestral do componente atual, você deve verificar se os dados que vai escrever são dife-
rentes do ancestral. Se você não executar esses testes adicionais, uma cópia dos dados (o arquivo wave,
nesse caso) será escrita em cada um dos formulários descendentes e as mudanças no arquivo wave do
Ancestor não serão copiadas nos formulários descendentes.
ATENÇÃO
Pelas razões já explicadas, DefineProperties( ) é uma área na qual você encontrará uma sensível diferen-
ça entre o Delphi de 16 e 32 bits. Em geral, a Borland tentou tornar a herança do formulário transparente
para o criador de componente. Esse é o lugar em que ele não poderia ser oculto. Embora os componentes
do Delphi 1.0 funcionem no Delphi do 32 bits, eles não serão capazes de propagar atualizações na heran-
ça do formulário sem modificação.
A Figura 22.6 mostra uma tela do Code Editor exibindo, como texto, um formulário contendo
TddgWaveFile.
unit Wavez;
interface
uses
SysUtils, Classes;
type
{ “Descendente” especial da string usada para criar editor de propriedades. }
TWaveFileString = type string;
EWaveError = class(Exception);
TddgWaveFile = class(TComponent)
private
FData: Pointer;
FDataSize: Integer;
FWaveName: TWaveFileString;
FWavePause: TWavePause;
FWaveLoop: TWaveLoop;
FOnPlay: TNotifyEvent;
FOnStop: TNotifyEvent;
procedure SetWaveName(const Value: TWaveFileString);
procedure WriteData(Stream: TStream);
procedure ReadData(Stream: TStream);
protected
procedure DefineProperties(Filer: TFiler); override;
public
destructor Destroy; override;
function Empty: Boolean;
function Equal(Wav: TddgWaveFile): Boolean;
procedure LoadFromFile(const FileName: String);
procedure LoadFromStream(S: TStream);
procedure Play;
procedure SaveToFile(const FileName: String);
procedure SaveToStream(S: TStream);
procedure Stop;
published
property WaveLoop: TWaveLoop read FWaveLoop write FWaveLoop;
property WaveName: TWaveFileString read FWaveName write SetWaveName;
property WavePause: TWavePause read FWavePause write FWavePause;
property OnPlay: TNotifyEvent read FOnPlay write FOnPlay;
property OnStop: TNotifyEvent read FOnStop write FOnStop;
end;
implementation
588
Listagem 22.9 Continuação
{ TddgWaveFile }
destructor TddgWaveFile.Destroy;
{ Garante que qualquer memória alocada seja liberada }
begin
if not Empty then
FreeMem(FData, FDataSize);
inherited Destroy;
end;
begin
inherited DefineProperties(Filer);
Filer.DefineBinaryProperty(‘Data’, ReadData, WriteData, DoWrite);
end;
SaveToStream(MyImage);
WavImage := TMemoryStream.Create;
try
Wav.SaveToStream(WavImage);
Result := StreamsEqual(MyImage, WavImage);
finally
WavImage.Free;
end;
finally
MyImage.Free;
end;
end;
end;
procedure TddgWaveFile.Play;
{ Reproduz o som WAV em FData usando os parâmetros encontrados em
FWaveLoop e FWavePause. }
const
LoopArray: array[TWaveLoop] of DWORD = (0, SND_LOOP);
PauseArray: array[TWavePause] of DWORD = (SND_ASYNC, SND_SYNC);
begin
{ Confere se o componente contém dados }
if Empty then
raise EWaveError.Create(‘No wave data’);
if Assigned(FOnPlay) then FOnPlay(Self); // dispara evento
{ Tenta reproduzir o som wave }
if not PlaySound(FData, 0, SND_MEMORY or PauseArray[FWavePause] or
590
Listagem 22.9 Continuação
LoopArray[FWaveLoop]) then
raise EWaveError.Create(‘Error playing sound’);
end;
procedure TddgWaveFile.Stop;
{ Pára o som WAV que está sendo reproduzido } 591
Listagem 22.9 Continuação
begin
if Assigned(FOnStop) then FOnStop(Self); // dispara evento
PlaySound(Nil, 0, SND_PURGE);
end;
end.
Categorias de propriedades
Como você aprendeu no Capítulo 1, um novo recurso do Delphi 5 são as categorias de propriedades. Esse
recurso fornece um meio para que as propriedades dos componentes da VCL sejam especificadas como
pertencentes a categorias em particular e para que o Object Inspector seja classificado por essas catego-
rias. As propriedades podem ser registradas como pertencentes a uma categoria em particular usando Re-
gisterPropertyInCategory( ) e RegisterPropertiesInCategory( ) declaradas na unidade DsgnIntf. O formulário
permite que você registre uma propriedade para uma categoria, enquanto o último permite que você re-
gistre múltiplas propriedades como uma chamada.
RegisterPropertyInCategory( ) é sobrecarregado para fornecer quatro diferentes versões dessa função
e dessa forma atender às suas reais necessidades. Todas as versões dessa função pegam uma TPropertyCate-
goryClass como o primeiro parâmetro, descrevendo a categoria. A partir daí, cada uma dessas versões
pega uma diferente combinação de nome de propriedade, tipo de propriedade e classe de componente
para permitir que você escolha o melhor método para registrar suas propriedades. As várias versões de
RegisterPropertyInCategory( ) são mostradas a seguir:
function RegisterPropertyInCategory(ACategoryClass: TPropertyCategoryClass;
const APropertyName: string): TPropertyFilter; overload;
function RegisterPropertyInCategory(ACategoryClass: TPropertyCategoryClass;
AComponentClass: TClass; const APropertyName: string): TPropertyFilter
overload;
function RegisterPropertyInCategory(ACategoryClass: TPropertyCategoryClass;
APropertyType: PTypeInfo; const APropertyName: string): TPropertyFilter;
overload;
function RegisterPropertyInCategory(ACategoryClass: TPropertyCategoryClass;
APropertyType: PTypeInfo): TPropertyFilter; overload;
Essas funções são suficientemente inteligentes para entender curingas e, portanto, você pode, por
exemplo, adicionar todas as propriedades que combinem com ‘Data*’ com uma categoria em particular.
Consulte a ajuda on-line da classe TMask para obter uma lista completa dos curingas suportados e o com-
portamento deles.
RegisterPropertiesInCategory( ) vem em três variações sobrecarregadas:
function RegisterPropertiesInCategory(ACategoryClass: TPropertyCategoryClass;
const AFilters: array of const): TPropertyCategory; overload;
function RegisterPropertiesInCategory(ACategoryClass: TPropertyCategoryClass;
AComponentClass: TClass; const AFilters: array of string): TPropertyCategory;
overload;
function RegisterPropertiesInCategory(ACategoryClass: TPropertyCategoryClass;
APropertyType: PTypeInfo; const AFilters: array of string): TPropertyCategory;
592 overload;
Classes de categorias
O tipo TPropertyCategoryClass é uma referência de classe para TPropertyCategory. TPropertyCategory é a classe
básica para todas as categorias de propriedades-padrão na VCL. Há 12 categorias de propriedades-
padrão e essas classes são descritas na Tabela 22.4.
Para exemplificar, vamos dizer que você escreveu um componente chamado TNeato com uma pro-
priedade chamada Keen e deseja registrar a propriedade Keen como um membro da categoria Action repre-
sentada por TActionCategory. Você poderia fazer isso adicionando uma chamada para RegisterPropertyInCa-
tegory( ) para o procedimento Register( ) para o seu controle, como mostrado a seguir:
Categorias personalizadas
Como você já aprendeu, uma categoria de propriedade é representada em código como uma classe que
descende de TPropertyCategory. Qual a dificuldade para criar suas próprias categorias de propriedades nes- 593
se caso? Na verdade, isso é bastante fácil. Na maioria dos casos, tudo o que você precisa é modificar as
funções da classe virtual Name( ) e Description( ) de TPropertyCategory para retornar informações específi-
cas para sua categoria.
Como uma ilustração, criaremos uma nova categoria Sound que será usada para classificar algumas
das propriedades do componente TddgWaveFile, que você aprendeu um pouco antes neste capítulo. Essa
nova categoria, chamada TSoundCategory, é mostrada na Listagem 22.10. Essa listagem contém WavezEd.pas,
que é um arquivo que contém a categoria, o editor de propriedades e o editor de componentes do com-
ponente.
Listagem 22.10 WavezEd.pas – ilustra um editor de propriedades para o componente do arquivo Wave
unit WavezEd;
interface
uses DsgnIntf;
type
{ Categoria de algumas das propriedades de TddgWaveFile }
TSoundCategory = class(TPropertyCategory)
public
class function Name: string; override;
class function Description: string; override;
end;
implementation
{ TSoundCategory }
{ TWaveFileStringProperty }
procedure TWaveFileStringProperty.Edit;
{ Executado quando o usuário dá um clique no botão de elipse na propriedade
WavName no Object Inspector. Este método permite que o usuário pegue um
arquivo de OpenDialog e defina o valor de propriedade. }
begin
with TOpenDialog.Create(nil) do
try
{ Configura as propriedades da caixa de diálogo }
Filter := ‘Wav files|*.wav|All files|*.*’;
DefaultExt := ‘*.wav’;
{ Coloca valor atual na propriedade FileName da caixa de diálogo }
FileName := GetStrValue;
{ Executa a caixa de diálogo e define o valor da propriedade se
a caixa de diálogo estiver OK }
if Execute then
SetStrValue(FileName);
finally
Free;
end;
end;
{ TWaveEditor }
const
VerbCount = 2;
VerbArray: array[0..VerbCount - 1] of string[7] = (‘Play’, ‘Stop’);
procedure TWaveEditor.Edit;
{ Chamado quando o usuário dá um duplo clique no componente durante projeto. }
{ Este método chama o método GetComponentProperties para chamar o método
Edit do editor de propriedades WaveName. }
var
Components: TDesignerSelectionList;
begin
Components := TDesignerSelectionList.Create;
try
Components.Add(Component);
GetComponentProperties(Components, tkAny, Designer, EditProp);
finally
Components.Free;
595
Listagem 22.10 Continuação
end;
end;
end.
Com a classe de categoria definida, tudo o que precisa ser feito é registrar as propriedades para a ca-
tegoria usando uma das funções de registro. Isso é feito no procedimento Register( ) de TddgWaveFile
usando a seguinte linha de código:
RegisterPropertiesInCategory(TSoundCategory, TddgWaveFile,
[‘WaveLoop’, ‘WaveName’, ‘WavePause’]);
E se você desejasse salvar uma lista de itens como componentes ou objetos que já não estejam encap-
sulados por uma classe existente, como TStrings? Bem, você poderia criar uma classe que execute o strea-
ming dos itens listados e em seguida torne essa propriedade do componente proprietário. Além disso,
você poderia modificar o mecanismo de streaming padrão do componente proprietário de modo que ele
saiba como distribuir o stream de sua lista de itens. No entanto, uma solução melhor seria tirar vantagem
das classes TCollection e TCollectionItem.
A classe TCollection é um objeto usado parra armazenar uma lista de objetos TCollectionItem. Tcollec-
tion, por si só, não é um componente mas um descendente de TPersistent. Geralmente, TCollection está as-
sociado a um componente existente.
Para usar TCollection para armazenar uma lista de itens, você derivaria uma classe descendente de
TCollection, que você poderia chamar de TNewCollection. TNewCollection servirá como um tipo de proprie-
dade de um componente. Posteriormente, você deve derivar uma classe da classe TCollectionItem, que
você poderia chamar de TnewCollectionItem. TNewCollection manterá uma lista de objetos TNewCollectionItem.
A beleza disso é que os dados pertencentes a TNewCollectionItem que precisam ser distribuídos no stream só
precisam ser publicados por TNewCollectionItem. O Delphi já sabe como distribuir o stream das proprieda-
des publicadas.
Um exemplo de onde TCollection é usado é com o componente TStatusBar. TStatusBar é um descen-
dente de TWinControl. Uma de suas propriedades é Panels. TStatusBar.Panels é do tipo TStatusPanels, que é
um descendente de TCollection e definido da seguinte maneira:
type
TStatusPanels = class(TCollection)
private
FStatusBar: TStatusBar;
function GetItem(Index: Integer): TStatusPanel;
procedure SetItem(Index: Integer; Value: TStatusPanel);
protected
procedure Update(Item: TCollectionItem); override;
public
constructor Create(StatusBar: TStatusBar);
function Add: TStatusPanel;
property Items[Index: Integer]: TStatusPanel read GetItem write SetItem;
default;
end;
type
TRunBtnItem = class(TCollectionItem)
private
FCommandLine: String; // Armazena a linha de comandos
FLeft: Integer; // Armazena propriedades de posição para
FTop: Integer; // TddgRunButton.
FRunButton: TddgRunButton; // Referencia um TddgRunButton
·
public
598
constructor Create(Collection: TCollection); override;
published
{ As propriedades publicadas serão distribuídas para o stream }
property CommandLine: String read FCommandLine write SetCommandLine;
property Left: Integer read FLeft write SetLeft;
property Top: Integer read FTop write SetTop;
end;
Observe que TRunBtnItem mantém uma referência para um componente de TddgRunButton, ainda que
apenas distribua o stream das propriedades necessárias para construir TddgRunButton. Inicialmente, você
pode pensar que, como TRunBtnItem se associa a TddgRunButton, poderia apenas publicar o componente e
permitir que o mecanismo de streaming fizesse o resto. Bem, isso implica alguns problemas com o meca-
nismo de streaming e o modo como ele manipula o streaming das classes de TComponent diferentemente
das classes de TPersistent. A regra fundamental aqui é que o sistema de streaming é responsável pela cria-
ção de novas instâncias de nome de classe derivadas de TComponent que localize em um stream, uma vez
que presume que as instâncias de TPersistent que já existem não tentem instanciar as novas. Seguindo esta
regra, distribuímos o stream das informações necessárias de TddgRunButton e em seguida criamos TddgRun-
Button no construtor TRunBtnItem, que ilustraremos dentro em breve.
TRunButtons se associa a um componente de TddgLaunchPad, que mostraremos logo a seguir. Ele faz isso
em seu construtor Create( ), que, como você pode ver, pega um componente de TddgLaunchPad como seu
parâmetro. Observe as diversas propriedades e métodos que foram adicionadas para permitir que o usuá-
rio manipule as classes individuais de TRunBtnItem. Em particular, a propriedade Items é uma array para a
lista TRunBtnItem list.
O uso das classes de TRunBtnItem e TRunButtons se tornará mais claro à medida que discutirmos a imple-
mentação do componente TddgLaunchPad.
599
Listagem 22.11 LnchPad.pas – ilustra a implementação de TddgLaunchPad
unit LnchPad;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs, RunBtn, ExtCtrls;
type
TddgLaunchPad = class;
TRunBtnItem = class(TCollectionItem)
private
FCommandLine: string; // Armazena a linha de comandos
FLeft: Integer; // Armazena propriedades de posição para
FTop: Integer; // TddgRunButton.
FRunButton: TddgRunButton; // Referencia um TddgRunButton
FWidth: Integer; // Registra a largura e a altura
FHeight: Integer;
procedure SetCommandLine(const Value: string);
procedure SetLeft(Value: Integer);
procedure SetTop(Value: Integer);
public
constructor Create(Collection: TCollection); override;
destructor Destroy; override;
procedure Assign(Source: TPersistent); override;
property Width: Integer read FWidth;
property Height: Integer read FHeight;
published
{ As propriedades publicadas serão distribuídas para o stream }
property CommandLine: String read FCommandLine
write SetCommandLine;
property Left: Integer read FLeft write SetLeft;
property Top: Integer read FTop write SetTop;
end;
TRunButtons = class(TCollection)
private
FLaunchPad: TddgLaunchPad; // Mantém uma referência a TddgLaunchPad
function GetItem(Index: Integer): TRunBtnItem;
procedure SetItem(Index: Integer; Value: TRunBtnItem);
protected
procedure Update(Item: TCollectionItem); override;
public
constructor Create(LaunchPad: TddgLaunchPad);
function Add: TRunBtnItem;
procedure UpdateRunButtons;
property Items[Index: Integer]: TRunBtnItem read
GetItem write SetItem; default;
end;
TddgLaunchPad = class(TScrollBox)
private
600
Listagem 22.11 Continuação
FRunButtons: TRunButtons;
TopAlign: Integer;
LeftAlign: Integer;
procedure SeTRunButtons(Value: TRunButtons);
procedure UpdateRunButton(Index: Integer);
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure GetChildren(Proc: TGetChildProc; Root: TComponent); override;
published
property RunButtons: TRunButtons read FRunButtons write SeTRunButtons;
end;
implementation
{ TRunBtnItem }
destructor TRunBtnItem.Destroy;
begin
FRunButton.Free; // Destrói a instância TddgRunButton.
inherited Destroy; // Chama o destruidor herdado Destroy.
end;
end;
{ TRunButtons }
end;
procedure TRunButtons.UpdateRunButtons;
{ UpdateRunButtons é um procedimento público que tornamos disponível de
modo que os usuários de TRunButtons possam forçar todos os botões de
execução a se redesenharem. Esse método chama TddgLaunchPad.UpdateRunButton
para cada instância de TRunBtnItem. }
var
i: integer;
begin
for i := 0 to Count - 1 do
FLaunchPad.UpdateRunButton(i);
end;
{ TddgLaunchPad }
destructor TddgLaunchPad.Destroy;
603
Listagem 22.11 Continuação
begin
FRunButtons.Free; // Libera a instância TRunButtons.
inherited Destroy; // Chama o método herdado destroy.
end;
end.
604
Implementando TRunBtnItem
O construtor TRunBtnItem.Create( ) cria uma instância de TddgRunButton. Cada TRunBtnItem na coleção man-
terá sua própria instância de TddgRunButton. As duas linhas a seguir em TRunBtnItem.Create( ) requerem um
pouco mais de explicação:
FRunButton := TddgRunButton.Create(TRunButtons(Collection).FLaunchPad);
FRunButton.Parent := TRunButtons(Collection).FLaunchPad;
Implementando TRunButtons
TRunButtons.Create( )simplesmente aponta FLaunchPad para o parâmetro TddgLaunchPad passado para ele de
modo que LaunchPad possa ser referido posteriormente.
TRunButtons.Update( ) é um método chamado sempre que uma mudança tenha sido feita a qualquer
uma das instâncias de TRunBtnItem. Esse método contém lógica que deve ocorrer devido a essa mudança.
Nós a usamos para chamar o método de TddgLaunchPad que redesenha as instâncias de TRunBtnItem. Tam-
bém adicionamos um método público, UpdateRunButtons( ), para permitir que o usuário force um redese-
nho.
Os demais métodos de TRunButtons são métodos de acesso de propriedade, que são explicados nos
comentários do código na Listagem 22.11.
Implementando TddgLaunchPad
O construtor e o destruidor de TddgLaunchPad são simples. TddgLaunchPad.Create( ) cria uma instância do ob-
jeto TRunButtons e se passa como um parâmetro. TddgLaunchPad.Destroy( ) libera a instância de TRunButtons.
A modificação do método TddgLaunchPad.GetChildren( ) é importante de se observar aqui. É aqui o lu-
gar no qual impedimos que as instâncias de TddgRunButton armazenadas pela coleção sejam distribuídas
para o stream, já que possuíam componentes de TddgLaunchPad. Lembre-se de que isso é necessário, pois
elas não devem ser criadas no contexto do objeto TddgLaunchPad, mas no contexto das instâncias de
TRunBtnItem. Como não há componentes de TddgRunButton passados para o procedimento Proc, não serão
distribuídos para o stream ou lido de um stream. 605
O método TddgLaunchPad.UpdateRunButton( ) é onde as instâncias de TddgRunButton mantidas pela cole-
ção são desenhadas. A lógica nesse código garante que nunca ultrapassem a largura de TddgLaunchPad.
Como TddgLaunchPad é um descendente de TScrollBox, a rolagem ocorrerá verticalmente.
Os outros métodos são simplesmente métodos de acesso da propriedade e são comentados no códi-
go da Listagem 22.11.
Finalmente, regitramos o editor de propriedades para a classe da coleção TRunButtons no procedi-
mento Register( ) dessa unidade. A próxima seção discute esse editor de propriedades e ilustra como edi-
tar uma lista de componentes de um editor de propriedades de caixa de diálogo.
Essa caixa de diálogo manipula diretamente os componentes de TRunBtnItem mantidos pela coleção
RunButtons de TddgLaunchPad. As diversas strings CommandLine de cada TddgRunButton envolvida em TRunBtnItem
são exibidas em PathListBox. Um componente de TddgRunButton reflete o item atualmente selecionado na
caixa de listagem para permitir que o usuário teste a seleção. A caixa de diálogo contém botões para per-
mitir que o usuário adicione ou remova um item, aceite as mudanças e cancele a operação. Como o usuá-
rio faz mudanças na caixa de diálogo, as mudanças são refletidas em TddgLaunchPad.
DICA
Uma convenção para os editores de propriedades é incluir um botão Apply para chamar as mudanças no
formulário. Não mostramos isso aqui, mas você deve considerar a adição desse tipo de botão no editor de
propriedades RunButtons como um exercício. Para ver como um botão Apply funciona, dê uma olhada no
editor de propriedades da propriedade Panels do componente TStatusBar da página Win32 da Compo-
nent Palette.
A Figura 22.9 ilustra o editor de propriedades TddgLaunchPad - RunButtons com alguns itens. Ele tam-
bém mostra o componente TddgLaunchPad do formulário, com os componentes de TddgRunButton listados no
editor de propriedades.
606
FIGURA 22.9 O editor de propriedades TddgLaunchPad – RunButtons com componentes de TrunBtnItem.
unit LPadPE;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs, Buttons, RunBtn, StdCtrls, LnchPad, DsgnIntF, TypInfo, ExtCtrls;
type
implementation
{$R *.DFM}
{ TLaunchPadEditor }
TestRunBtn.Width := pnlRBtn.Width;
TestRunBtn.Height := pnlRBtn.Height;
end;
608
Listagem 22.12 Continuação
procedure TLaunchPadEditor.UpdatePathListBox;
{ Reinicializa PathListBox de modo que reflita a lista de TRunBtnItems. }
var
i: integer;
begin
PathListBox.Clear; // Primeiro apaga a caixa de listagem.
for i := 0 to FRunButtons.Count - 1 do
PathListBox.Items.Add(FRunButtons[i].CommandLine);
end;
i: integer;
begin
i := PathListBox.ItemIndex;
if i >= 0 then
begin
PathListBox.Items.Delete(i); // Remove o item da caixa de listagem
FRunButtons[i].Free; // Remove o item da coleção
TestRunBtn.CommandLine := ‘’; // Apaga o botão de teste de execução
Modified := True;
end;
end;
{ TRunButtonsProperty }
procedure TRunButtonsProperty.Edit;
{ Chama o método EditRunButton( ) e passa a referênca para a instância
de TRunButton que está sendo editada. Essa referência pode ser obtida pelo
método GetOrdValue. Em seguida, redesenha LaunchDialog chamando o
método TRunButtons.UpdateRunButtons. }
begin
if EditRunButtons(TRunButtons(GetOrdValue)) then
Modified;
TRunButtons(GetOrdValue).UpdateRunButtons;
end;
end.
TddgLaunchPadEditor = class(TForm)
PathListBox: TListBox;
610
Listagem 22.12 Continuação
AddBtn: TButton;
RemoveBtn: TButton;
TestRunBtn: TddgRunButton;
CancelBtn: TButton;
OkBtn: TButton;
Label1: TLabel;
procedure PathListBoxClick(Sender: TObject);
procedure AddBtnClick(Sender: TObject);
procedure RemoveBtnClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure CancelBtnClick(Sender: TObject);
private
FLaunchPad: TddgLaunchPad; // Para ser usado como backup
FRunButtons: TRunButtons; // Referirá aos TRunButtons reais
Modified: Boolean;
procedure UpdatePathListBox;
end;
implementation
{$R *.DFM}
611
Listagem 22.12 Continuação
{ TddgLaunchPadEditor }
TestRunBtn.Width := pnlRBtn.Width;
TestRunBtn.Height := pnlRBtn.Height;
end;
procedure TddgLaunchPadEditor.UpdatePathListBox;
{ Reinicializa a PathListBox de modo que reflita a lista de
TRunBtnItems }
var
i: integer;
begin
PathListBox.Clear; // Primeiro apaga a caixa de listagem.
for i := 0 to FRunButtons.Count - 1 do
PathListBox.Items.Add(FRunButtons[i].CommandLine);
end;
PathListBox.Items.Add(OpenDialog.FileName);
FRunButtons.Add; // Cria nova instância de TRunBtnItem.
{ Define o foco para o novo item em PathListBox }
PathListBox.ItemIndex := FRunButtons.Count - 1;
{ Define a linha de comando para o novo TRunBtnItem como o
nome de arquivo especificado por PathListBox.ItemIndex }
FRunButtons[PathListBox.ItemIndex].CommandLine :=
PathListBox.Items[PathListBox.ItemIndex];
{ Chama o manipulador de evento PathListBoxClick de modo que o
TddgRunButton de teste reflita o item recém-adicionado. }
PathListBoxClick(nil);
Modified := True;
end;
finally
OpenDialog.Free
end;
end;
{ TRunButtonsProperty }
procedure TRunButtonsProperty.Edit;
{ Chama o método EdiTddgRunButton( ) e passa a referência para a instância
de TddgRunButton que está sendo editada. Essa referência pode ser obtida
usando o método GetOrdValue. Em seguida, redesenha LaunchDialog chamando
o método TRunButtons.UpdateRunButtons. }
begin
if EdiTRunButtons(TRunButtons(GetOrdValue)) then
Modified;
TRunButtons(GetOrdValue).UpdateRunButtons;
end;
end.
Resumo
Este capítulo mostra a você alguns dos mais avançados truques e técnicas de projeto de componente do
Delphi. Entre outras coisas, você aprendeu a estender componentes de animação e dicas, bem como edi-
tores de componente, editores de propriedades e coleções de componente. Munido com essas informa-
ções, bem como com as informações mais convencionais que você aprendeu no capítulo anterior, você
deve ser capaz de resolver a contento quase todas as suas necessidades de programação. No próximo ca-
pítulo, vamos nos aprofundar ainda mais no mundo do desenvolvimento baseado em componentes.
615
Tecnologias CAPÍTULO
Fundamentos do COM
Primeiro, as primeiras coisas. Antes de mergulharmos de cabeça no tópico que vamos discutir, é impor-
tante que você entenda os conceitos básicos e a terminologia associada a essa tecnologia. Esta seção apre-
senta as idéias e termos básicos por trás da tecnologia COM e Activex.
Terminologia
Como as tecnologias COM trazem consigo uma quantidade significativa de termos novos, vamos apre-
sentar alguns deles aqui antes de mergulharmos de cabeça no universo do ActiveX e OLE.
Embora uma instância de um objeto COM em geral seja chamado de um objeto, o tipo que identifi-
ca esse objeto normalmente é chamado de classe de componente ou co-classe. Portanto, para criar uma
instância de um objeto COM, você deve passar a CLSID para a classe COM que deseja criar.
O conjunto de dados que é compartilhado entre aplicações é chamado como um objeto OLE. As
aplicações que têm a capacidade para conter objetos OLE são chamadas de containers OLE. As aplica-
ções que têm a capacidade para ter seus dados armazenados em um container OLE são chamadas servido-
res OLE.
Um documento que contém um ou mais objetos OLE costuma ser chamado de documento compos-
to. Embora os objetos OLE possam ser armazenados em um determinado documento, as aplicações que
podem ser hospedadas no contexto de outro documento são conhecidas como documentos ActiveX.
Como o nome dá a entender, um objeto OLE pode ser vinculado ou incorporado em um documento
composto. Os objetos vinculados são armazenados em um arquivo em disco. Com a vinculação do obje-
to, vários contêineres – ou mesmo a aplicação servidora – podem ser vinculados ao mesmo objeto OLE
no disco. Quando uma aplicação modifica o objeto vinculado, a modificação é refletida em todas as ou-
tras aplicações que mantêm um vínculo com esse objeto. Os objetos incorporados são armazenados pela
aplicação container OLE. Somente a aplicação container é capaz de editar o objeto OLE. A incorporação
impede que outras aplicações acessem (e portanto os modifiquem ou danifiquem) seus dados, mas limita
a possibilidade de gerenciamento deles ao container.
Outra faceta do ActiveX que será discutida de modo mais profundo neste capítulo é o Automation,
que é um meio de permitir que as aplicações (chamadas controladores Automation ) manipulem objetos
associados a outras aplicações ou bibliotecas (chamadas servidores Automation ). O Automation permite
a você manipular objetos em outra aplicação e expor os elementos de sua aplicação para outros progra-
madores.
Armazenamento estruturado
O OLE 2 define uma sistema para armazenar informações no disco conhecido como armazenamento estru-
turado. Basicamente, esse sistema faz no nível do arquivo o mesmo que o DOS faz no nível de disco. Um
objeto de armazenamento é um arquivo físico em um disco, mas, como um diretório do DOS, é composto
de vários armazenamentos e streams. Um armazenamento equivale a um subdiretório e o stream, a um ar-
quivo DOS. Freqüentemente você vai ouvir essa implementação sendo chamada de arquivos compostos.
Modelos de threading
Todo objeto COM opera em um determinado modelo de threading, que por sua vez determina como um
objeto pode ser manipulado em um ambiente multithreaded. Quando um servidor COM é registrado,
cada um dos objetos COM contidos nesse servidor deve registrar o modelo de threading que aceita. Para
objetos COM escritos em Delphi, o modelo de threading escolhido no Automation, controle ActiveX ou
assistentes de objeto COM determina o modo como um controle é registrado. Veja a seguir os modelos
de threading COM:
l Simples. Todo o servidor COM é executado em apenas um thread.
l Apartamento. Também conhecido como STA (Single-Threaded Apartment). Cada objeto COM é
executado dentro do contexto de seu próprio thread, e várias instâncias do mesmo tipo de obje-
to COM podem ser executadas dentro de threads separados. Por essa razão, os dados comparti-
lhados entre instâncias de objeto (como variáveis globais) devem ser protegidos pelos objetos de
sincronização de thread quando apropriado. 619
l Livre. Também conhecido como MTA (Multithreaded Apartment). Um cliente pode chamar um
método de um objeto em qualquer thread a qualquer momento. Isso significa que o objeto COM
deve impedir o acesso simultâneo por múltiplos threads até mesmo dos dados da sua instância.
l Ambos. Tanto o modelo de threading apartamento como o livre são aceitos.
Não se esqueça de que a mera seleção do modelo de threading desejado no assistente não garante
que seu objeto COM será salvo como esse modelo de threading. Você deve escrever o código para asse-
gurar que seus servidores COM funcionam normalmente no modelo de threading que deseja dar supor-
te. Na maioria das vezes, isso inclui o uso de objetos de sincronização de threading para proteger o acesso
a dados globais ou de instância em seus objetos COM. Para obter maiores informações sobre o ambiente
multithreaded no Delphi, consulte o Capítulo 11.
COM+
No Windows 2000, a Microsoft forneceu a mais significativa atualização para COM dos últimos tempos
com o lançamento de uma nova iteração chamada COM+. A meta do COM+ é a simplificação do pro-
cesso de desenvolvimento do COM através da integração de várias tecnologias satélites, particularmente
o MTS (descrito posteriormente neste capítulo) e o MSMQ (Microsoft Message Queue). A integração
dessas tecnologias no runtime-padrão do COM+ significa que todos os programadores em COM+ se-
rão capazes de tirar proveito de recursos como controle de transação, segurança, administração, compo-
nentes enfileirados e serviços de evento, assinatura e publicação. Como o COM+ é em grande parte
composto de partes disponíveis no mercado, isso significa total compatibilidade retroativa e dessa forma
todas as aplicações COM e MTS automaticamente se tornam aplicações COM+.
Interfaces
O COM define um mapa-padrão para definir o modo como funções do objeto são organizadas na me-
mória. As funções são organizadas em tabelas virtuais (chamadas vtables) – tabelas de endereço de função
idênticas às VMTs (Virtual Method Tables) de classe do Delphi. A descrição de linguagem de programa-
ção de cada vtable é chamada de uma interface.
Pense em uma interface como uma faceta de uma determinada classe. Cada faceta representa um
conjunto específico de funções ou procedimentos que você pode usar para manipular a classe. Por exem-
plo, um objeto COM que representa uma imagem de bitmap pode dar suporte a duas interfaces: uma
contendo métodos que permitem que o bitmap seja representado na tela ou na impressora e outra inter-
face para gerenciar o armazenamento e a recuperação do bitmap a partir de um arquivo do disco.
Na verdade, uma interface tem duas partes: a primeira parte é a definição de interface, que consiste
em uma coleção de uma ou mais declarações de função em uma ordem específica. A definição de interfa-
ce é compartilhada entre o objeto e o usuário do objeto. A segunda parte é a implementação de interface,
que é a implementação propriamente dita das funções descritas na declaração de interface. A definição
de interface é uma espécie de contrato entre o objeto COM e um cliente desse objeto – uma garantia para
o cliente de que o objeto vai implementar métodos específicos em uma ordem específica.
Introduzida no Delphi 3, a palavra-chave interface do Object Pascal permite a você definir facil-
mente interfaces COM. Uma declaração de interface é semanticamente semelhante a uma declaração de
classe, com algumas exceções. As interfaces podem consistir somente em propriedades e métodos – sem
dados. Como as interfaces não podem conter dados, suas propriedades devem escrever e ler para/de mé-
todos. Mais importante, as interfaces não têm implementação porque elas apenas definem um contrato.
620
IUnknown
Da mesma forma como todas as classes Object Pascal descendem implicitamente de TObject, todas as in-
terfaces COM (e, portanto, todas as interfaces Object Pascal) derivam implicitamente de IUnknown, que é
definido na unidade System da seguinte forma:
type
IUnknown = interface
[‘{00000000-0000-0000-C000-000000000046}’]
function QueryInterface(const IID: TGUID; out Obj): Integer; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
end;
Além do uso da palavra-chave interface, outra diferença óbvia entre uma interface e uma declaração
de classe que você perceberá no código acima é a presença de um GUID (globally unique identifier).
Em COM, todas as interfaces ou classes têm um GUID que define exclusivamente essa interface.
Nesse sentido, duas interfaces ou classes têm o mesmo nome definido por duas pessoas diferentes
nunca entrarão em conflito porque seus respectivos GUIDs serão diferentes. Quando usado para re-
presentar uma interface, um GUID normalmente é chamado de IID (interface ID). Quando usado para
representar uma classe, um GUID é chamado de CLSID (class ID).
621
DICA
Você pode gerar um GUID novo na IDE do Delphi usando a tecla de atalho Ctrl+Shift+G no Code Editor.
Além do IID, IUnknown declara três métodos: QueryInterface( ), _AddRef( ) e _Release( ). Como
IUnknown é a interface básica de COM, todas as interfaces devem implementar IUnknown e seus métodos. O
método _AddRef( ) pode ser chamado quando um cliente obtém e deseja usar um ponteiro em dada inter-
face, e uma chamada para _AddRef( ) deve ser seguida de uma chamada para _Release( ) quando o cliente
tiver terminado de usar a interface. Nesse sentido, o objeto que implementa as interfaces podem manter
uma contagem de clientes que estão mantendo uma referência para o objeto, ou uma contagem de refe-
rência. Quando a contagem de referência chega a zero, o objeto deve ser liberado da memória. A função
QueryInterface( ) é usada para consultar se um objeto aceita uma determinada interface e, caso o faça, re-
tornar um ponteiro para essa interface. Por exemplo, suponha que o objeto O suporte duas interfaces, I1 e
I2, e que você tem um ponteiro para interface I1 de O. Para obter um ponteiro para interface I2 de O, você
deveria chamar I1.de QueryInterface( ).
NOTA
Se você é um programador COM experiente, deve ter percebido que o sublinhado antes dos métodos
_AddRef( ) e _Release( ) não é consistente com outras linguagens de programação COM ou mesmo com
a documentação COM da Microsoft. Como a Object Pascal é ciente de “IUnknown”, normalmente você
não vai chamar esses métodos diretamente (voltaremos a falar sobre isso daqui a pouco) e portanto o subli-
nhado tem como principal finalidade fazê-lo pensar antes de chamar esses métodos.
Como toda interface no Delphi descende implicitamente de IUnknown, todas as classes de Delphi que
implementam interfaces também devem implementar os três métodos IUnknown. Você pode fazer isso
manualmente ou deixar a VCL fazer o trabalho pesado para você descendendo sua classe de TInterfaced-
Object, que implementa IUnknown para você.
Usando Interfaces
O Capítulo 2 e a própria documentação do Delphi, “Guia da linguagem Object Pascal”, analisam a se-
mântica usada nas instâncias de interface; portanto, não vamos perder tempo com esse material aqui.
Preferimos discutir o modo como IUnknown é totalmente integrado às regras do Object Pascal.
Quando um valor é atribuído a uma variável de interface, o compilador gera automaticamente uma
chamada para o método _AddRef( ) da interface a fim de que a contagem de referência do objeto seja incre-
mentada. Quando uma variável de interface sai do escopo ou um valor nil é atribuído, o compilador gera au-
tomaticamente uma chamada para o método _Release( ) da interface. Considere a peça de código a seguir:
var
I: ISomeInteface;
begin
I := FunctionThatReturnsAnInterface;
I.SomeMethod;
end;
Agora observe o fragmento de código a seguir, que mostra o código que você deveria digitar (em ne-
grito) e uma versão Pascal aproximada do código que o compilador gera (na fonte normal):
var
I: ISomeInterface;
begin
622 // a interface é automaticamente inicializada como nil
I := nil;
try
// seu código entra aqui
I := FunctionThatReturnsAnInterface;
// _AddRef( ) é chamado implicitamente quando I é atribuído
I._AddRef;
I.SomeMethod;
finally
// o bloco finally implícito assegura que a referência para a
// interface é liberada
if I < > nil I._Release;
end;
end;
O compilador Delphi também é suficientemente inteligente para saber quando chamar _AddRef( ) e
quando interfaces são reatribuídas a outras instâncias de interface ou atribuídas ao valor nil.
_Release( )
Por exemplo, considere o bloco de código a seguir:
var
I: ISomeInteface;
begin
// reatribui I
I := FunctionThatReturnsAnInterface;
I.SomeMethod;
// reatribui I
I := OtherFunctionThatReturnsAnInterface;
I.OtherMethod;
// define I com nil
I := nil;
end;
Veja a seguir uma mistura do código escrito pelo usuário (negrito) e o código, aproximado, gerado
pelo compilador (normal):
var
I: ISomeInterface;
begin
// interface é automaticamente inicializada como nil
I := nil;
try
// seu código entra aqui
// atribui I
I := FunctionThatReturnsAnInterface;
// _AddRef( ) é chamado implicitamente quando I é atribuído
I._AddRef;
I.SomeMethod;
// reatribui I
I._Release;
I := OtherFunctionThatReturnsAnInterface;
I._AddRef;
I.OtherMethod;
// define I como nil
I._Release;
I := nil;
623
finally
// o bloco finally implícito assegura que a referência para a
// interface é liberada
if I < > nil I._Release;
end;
end;
Este exemplo de código também ajuda a ilustrar a razão pela qual o Delphi coloca um sublinhado
antes dos métodos _AddRef( ) e _Release( ). Esquecer de incrementar ou decrementar a referência de
uma interface foi um dos clássicos bugs da programação em COM antes da interface. O suporte à interfa-
ce do Delphi é projetado de modo a reduzir esses problemas por meio da manipulação das questões ine-
rentes à manutenção para você; portanto, só muito raramente você terá uma razão para chamar esses
métodos diretamente.
Como o compilador sabe como gerar chamadas para _AddRef( ) e _Release( ), não faria sentido se o
compilador tivesse algum conhecimento inerente do terceiro método IUnknown, QueryInterface( )? Não só
faria sentido como ele o tem. Dado um ponteiro de interface para um objeto, você pode usar o operador
as para fazer “um typecast” da interface para outra interface aceita pelo objeto COM. Nós dissemos type-
cast porque essa aplicação do operador as não é um typecast no sentido estrito da palavra, mas uma cha-
mada interna para o método QueryInterface( ). O exemplo de código a seguir demonstra isso:
var
I1: ISomeInterface;
I2: ISomeOtherInterface;
begin
// atribui como I1
I1 := FunctionThatReturnsAnInterface;
// QueryInterface I1 atrás de uma interface I2
I2 := I1 as ISomeOtherInterface;
end;
No exemplo anterior, se o objeto referenciado por I1 não dar suporte à interface ISomeOtherInterfa-
ce, uma exceção será produzida pelo operador as.
Uma regra adicional de linguagem referente a interfaces é que uma variável de interface seja uma
atribuição compatível com uma classe do Object Pascal que implemente essa interface. Por exemplo,
considere a interface e as declarações de classe a seguir:
type
IFoo = interface
// definição de IFoo
end;
IBar = interface(IFoo)
// definição para IBar
end;
Embora esse recurso pareça violar regras tradicionais de compatibilidade de atribuição do Pascal,
ele torna interfaces mais naturais e fáceis de trabalhar.
Uma conseqüência importante porém não tão óbvia dessa regra é que as interfaces só têm compatibi-
lidade de atribuição com classes que suportem explicitamente a interface. Por exemplo, a classe TBarClass
definida anteriormente declara suporte explícito para a interface IBar. Como IBar descende de IFoo, tudo
leva a crer que TBarClass também aceita diretamente IFoo. Porém, esse não é o caso, como o exemplo de
código a seguir ilustra:
var
IF: IFoo;
TB: TBarClass;
begin
TB := TBarClass.Create;
try
// erro de compilador produzido na próxima linha pelo fato de TbarClass
// não dar suporte explicitamente a IFoo.
IF := TB;
// usa TB e IF
finally
IF := nil; // requisita explicitamente IF
TB.Free;
end;
end;
Interfaces e IIDs
Como o ID da interface é declarado como uma parte de uma declaração de interface, o compilador do
Object Pascal sabe como obter o IID de uma interface. Portanto, você pode passar um tipo de interface
para um procedimento ou função que exija um TIID ou TGUID como um parâmetro. Por exemplo, suponha
que você tenha uma função como esta:
procedure TakesIID(const IID: TIID);
Essa capacidade elimina a necessidade de constantes IID_InterfaceType definidas para cada tipo de
interface, com as quais um programador de COM em C++ está acostumado.
Aliasing de método
Um problema que ocasionalmente aparece quando você implementa múltiplas interfaces em uma única
classe é que pode haver uma colisão de nomes de método em duas ou mais interfaces. Por exemplo, con-
sidere as interfaces a seguir:
type
IIntf1 = interface
procedure AProc;
end;
IIntf2 = interface
procedure AProc;
end; 625
Levando-se em conta que cada interface contém um método chamado AProc( ), como você pode de-
clarar que uma classe que implemente ambas as interfaces? A resposta é o método aliasing. O método ali-
asing permite a você mapear um método de interface particular para um método de nome diferente em
uma classe. O código a seguir mostra como declarar uma classe que implementa IIntf1 e IIntf2:
type
TNewClass = class(TInterfacedObject, IIntf1, IIntf2)
protected
procedure IIntf2.AProc = AProc2;
procedure AProc; // une para IIntf1.AProc
procedure AProc2; // une para IIntf2.AProc
end;
Nessa declaração, o método AProc( ) de IIntf2 é mapeado para um método com o nome AProc( ). A
criação de aliases dessa forma permite a você implementar qualquer interface em qualquer classe sem
medo de colisões de nome de método.
if Failed(FunctionThatReturnsHResult) then
\\ código de condição de erro
É claro que a verificação do valor de retorno de cada chamada de função pode se tornar tediosa.
Além disso, os métodos de manipulação de exceção do Delphi para detectar e recuperar erros têm o seu
desempenho comprometido quando há erros retornados por funções. Por essas razões, a unidade ComObj
define um procedimento chamado OleCheck( ) que converte erros de HResult em exceções. A sintaxe para
chamar esse método é esta:
OleCheck(FunctionThatReturnsHResult);
Esse procedimento pode ser bem prático e deixará o seu código ActiveX bastante leve.
TComObject e TComObjectFactory
O Delphi fornece duas classes que encapsulam objetos COM e factories de classe: TComObject e TComObject-
Factory, respectivamente. TComObject contém a infra-estrutura necessária para dar suporte a IUnknown e cria-
ção via TComObjectFactory. Da mesma forma, TComObjectFactory aceita IClassFactory e tem a capacidade de
criar objetos TComObject. Você pode gerar facilmente um objeto COM usando o COM Object Wizard (as-
sistente de objeto COM) encontrado nas páginas do ActiveX da caixa de diálogo New Items (itens no-
vos). A Listagem 23.1 mostra um pseudocódigo para a unidade gerada por esse assistente, que ilustra o
relacionamento entre essas classes.
interface
uses ComObj;
type
TSomeComObject = class(TComObject, interfaces supported)
class and interface methods declared here
end;
implementation
uses ComServ;
initialization
TComObjectFactory.Create(ComServer, TSomeComObject,
CLSID_TSomeComObject, ‘ClassName’, ‘Description’);
end;
O descendente TComServer é declarado e implementado como a maioria das classes VCL. O que o
vincula ao objeto TComObjectFactory correspondente é o parâmetro passado para construtor Create( )
de TComObjectFactory. O primeiro parâmetro do construtor é um objeto TComServer. Você sempre passará o
objeto ComServer global declarado na unidade ComServ nesse parâmetro. O segundo parâmetro é a classe
TComObject que você deseja vincular à factory de classe. O terceiro parâmetro é o CLSID da classe COM
de TComObject. O quarto e o quinto parâmetros são as strings nomes de classe e descrição usadas para des-
crever a classe COM no Registro do Sistema.
A instância TComObjectFactory é criada na inicialização da unidade para assegurar que a factory de
classe esteja disponível para criar instâncias de objeto COM tão logo o servidor COM seja carregado. O
modo como o servidor COM é carregado depende de o servidor COM ser um servidor em processo
(uma DLL) ou um servidor fora do processo (um aplicativo).
627
Servidores COM em processo
Os servidores COM em processo (ou in-proc, para abreviar) são DLLs que podem criar os objetos COM
a serem usados pela aplicação host. Esse tipo de servidor COM é chamado em processo porque, como
uma DLL, reside no mesmo processo que a aplicação de chamada. Um servidor in-proc deve exportar
quatro funções de ponto de entrada padrão:
function DllRegisterServer: HResult; stdcall;
function DllUnregisterServer: HResult; stdcall;
function DllGetClassObject (const CLSID, IID: TGUID; var Obj): HResult;
stdcall;
function DllCanUnloadNow: HResult; stdcall;
Como cada uma dessas funções já é implementada pela unidade ComServ, o único trabalho a ser feito
pelos servidores COM do Delphi COM é assegurar que essas funções sejam adicionadas a uma cláusula
exports em seu projeto.
NOTA
Um bom exemplo de uma aplicação de servidores COM em processo pode ser encontrado no Capítulo 24,
que demonstra como criar extensões do shell.
DllRegisterServer( )
A função DllRegisterServer( ) é chamada para registrar uma DLL do servidor COM com o Registro do Sis-
tema. Se você simplesmente exportar esse método da sua aplicação Delphi, como descrito anteriormente, a
VCL percorrerá todos os objetos COM em sua aplicação e registrá-los no Registro do Sistema. Quando um
servidor COM for registrado, criará uma entrada de chave no Registro do Sistema embaixo de
HKEY_CLASSES_ROOT\CLSID\{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx}
para cada classe COM, onde o X indica o CLSID da classe COM. Para os servidores em processo, uma
entrada adicional é criada como uma subchave da chamada anterior chamada InProcServer32. O va-
lor-padrão dessa chave é o caminho para a DLL do servidor em processo. A Figura 23.1 mostra um servi-
dor COM registrado com o Registro do Sistema.
DllUnregisterServer( )
A tarefa da função DllUnregisterServer( ) é simplesmente desfazer o que é feito pela função DllRegister-
Server( ). Quando chamada, ela deve remover todas as entradas no Registro do Sistema feitas pelo DllRe-
gisterServer( ).
628
DllGetClassObject( )
DllGetClassObject( ) é chamado pelo mecanismo COM para retornar uma factory de classe para uma de-
terminada classe COM. O parâmetro CLSID desse método é o CLSID do tipo de classe COM que você de-
seja criar. O parâmetro IID armazena o IID do ponteiro de instância da interface que você deseja obter
para o objeto de factory de classe (geralmente, o ID de interface da IclassFactory é passado aqui). Se essa
operação for bem-sucedida, o parâmetro Obj conterá um ponteiro para a interface de factory de classe in-
dicada por IID que é capaz de criar objetos COM do tipo de classe representada pelo CLSID.
DllCanUnloadNow( )
DllCanUnloadNow( ) é chamado pelo mecanismo COM para determinar se a DLL do servidor COM é capaz
de ser descarregado da memória. Se há referências a qualquer objeto COM dentro da DLL, essa função
deverá retornar S_FALSE, indicando que a DLL não deve ser descarregada. Se nenhum dos objetos COM
da DLL for usado, esse método poderia retornar S_TRUE.
DICA
Mesmo depois de todas referências aos objetos COM de um servidor em processo terem sido liberadas,
nem sempre o COM chama DllCanUnloadNow( ) para começar o processo de liberação da DLL do servidor
em processo da memória. Se você deseja assegurar que todas as DLLs ociosas do servidor COM sejam li-
beradas da memória, chame a função CoFreeUnusedLibraries( ) API, que é definida nas unidades ActiveX
da seguinte maneira:
O parâmetro ClassID armazena o CLSID, que identifica o tipo de objeto COM que você deseja criar.
O valor de retorno dessa função é a interface IUnknown do objeto COM solicitado ou, se o objeto COM
não puder ser criado, a função produzirá uma exceção.
CreateComObject( ) é um wrapper em torno da função CoCreateInstance( ) API do COM. Internamen-
te, CoCreateInstance( ) chama a função CoGetClassObject( ) API para obter uma IClassFactory para o objeto
COM especificado. CoCreateInstance( ) faz isso procurando no Registro a entrada InProcServer32 da clas-
se COM a fim de localizar o caminho para a DLL do servidor em processo, chamando LoadLibrary( ) na
DLL do servidor em processo e, em seguida, chamando a função DllGetClassObject( ) da DDL. Depois de
obter o ponteiro de interface IClassFactory, CoCreateInstance( ) chama IClassFactory.CreateInstance( ) para
criar uma instância da classe COM especificada.
DICA
CreateComObject( ) pode não ser eficiente para quem precisa criar vários objetos de uma factory de classe
porque ele dispõe do ponteiro de interface IClassFactory obtido por CoGetClassObject( ) depois de criar o
objeto COM solicitado. Em casos em que você precisa criar várias instâncias do mesmo objeto COM, você
deve chamar CoGetClassObject( ) diretamente e usar IClassFactory.CreateInstance( ) para criar várias
instâncias do objeto COM.
629
NOTA
Antes de poder usar qualquer função COM ou OLE API, você deve inicializar a biblioteca COM usando a
função CoInitialize( ). O único parâmetro para essa função deve ser nil. Para fechar de modo adequa-
do a biblioteca COM, você deve chamar a função CoUninitialize( ) como a última chamada para a biblio-
teca OLE. Como as chamadas são cumulativas, cada chamada para CoInitialize( ) em sua aplicação
deve ter uma chamada correspondente para CoUninitialize( ).
Não é necessário chamar essas funções a partir das bibliotecas em processo, pois suas aplicações-cliente
são necessárias para executar a inicialização e “desinicialização” do processo.
Registro
Como seus primos em processo, os servidores fora do processo também devem ser registrados no Regis-
tro do Sistema. Os servidores fora do processo devem fazer a entrada embaixo de
HKEY_CLASSES_ROOT\CLSID\{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx}
chamada LocalServer32, que identifica o nome do caminho do executável do servidor fora do processo.
Os servidores COM das aplicações Delphi são registrados no método Application.Initialize( ), que
normalmente é a primeira linha do código em um arquivo de projeto da aplicação. Se o parâmetro da li-
nha de comando /regserver for passado para sua aplicação, Application.Initialize( ) apagará o registro
das classes COM no Registro do Sistema e imediatamente encerrará a aplicação. Do mesmo modo, se o
parâmetro da linha de comando /unregserver for passado, Application.Initialize( ) apagará o registro das
classes COM no Registro do Sistema e imediatamente encerrará a aplicação. Se nenhum desses parâme-
tros for passado, Application.Initialize( )registrará as classes COM no Registro do Sistema e continuará
a executar a aplicação normalmente.
Agregação
Você sabe agora que interfaces são os blocos de construção básicos do COM assim como essa herança é
possível com interfaces, mas interfaces são entidades sem implementação. O que acontece, então, quan-
630 do você deseja reciclar a implementação de um objeto COM dentro de outro? A resposta COM para essa
pergunta é um conceito chamado agregação. Agregação significa que o objeto externo cria o objeto inter-
no como parte de seu processo de criação e as interfaces do objeto interno são expostas pelo externo. Um
objeto tem que se permitir operar como um agregado fornecendo um meio para encaminhar todas as
chamadas para seus métodos IUnknown para o objeto externo. Para ver um exemplo de agregação dentro
do contexto de objetos COM da VCL, você deve dar uma olhada na classe TAggregatedObject na unidade
AxCtrls.
Distributed COM
Introduzido com Windows NT 4, Distributed COM (ou DCOM) fornece um meio para acessar objetos
COM localizados em outras máquinas em uma rede. Além da criação remota de objeto, o DCOM forne-
ce facilidades de segurança que permitem aos servidores especificar quais clientes têm direito criar ins-
tâncias de tais servidores e que operações eles podem executar. O Windows NT 4 e o Windows 98 têm a
capacidade DCOM embutida, mas o Windows 95 precisa de um add-on, disponível no site da Web da
Microsoft (http://www.microsoft.com), para servir como um cliente DCOM.
Você pode criar objetos COM remotos usando a função CreateRemoteComObject( ), que é declarada na
unidade ComObj da seguinte forma:
function CreateRemoteComObject(const MachineName: WideString;
const ClassID: TGUID): IUnknown;
O primeiro parâmetro, MachineName, para essa função é uma string representando o nome da rede da
máquina que contém a classe COM. O parâmetro ClassID especifica o CLSID da classe COM a ser criada.
O valor de retorno dessa função é o ponteiro de interface IUnknown para o objeto COM especificado no
CLSID. Uma exceção será produzida se o objeto não puder ser criado.
CreateRemoteComObject( ) é um wrapper em torno da função CoCreateInstanceEx( ) da API do COM,
que é uma versão estendida de CoCreateInstance( ) que sabe como criar objetos remotamente.
Automation
O Automation (anteriormente chamado Automation OLE ) fornece um meio para que as aplicações ou
DLLs exponham objetos programáveis a serem usados por outras aplicações. As aplicações ou DLLs que
expõem objetos programáveis são chamados de servidores Automation. As aplicações que acessam e ma-
nipulam os objetos programáveis contidos nos servidores Automation são conhecidas como controlado-
res de Automation. Controladores de Automation são capazes de programar o servidor Automation
usando uma linguagem como macro exposta pelo servidor.
Entre as principais vantagens de usar Automation em suas aplicações, está sua natureza indepen-
dente de linguagem. Um controlador Automation é capaz de manipular um servidor independentemente
da linguagem de programação usada para desenvolver um componente. Além disso, como o Automation
é aceito no nível do sistema operacional, teoricamente você será capaz de aproveitar futuros avanços nes-
sa tecnologia usando Automation hoje. Se essas coisas soam bem para você, continue a leitura. As infor-
mações apresentadas a seguir dizerem respeito à criação de servidores e controladores Automation no
Delphi.
ATENÇÃO
Se você tem um projeto Automation do Delphi 2 que deseja migrar para a versão atual do Delphi, você
deve levar em consideração que as técnicas para Automation mudaram drasticamente a partir do Delphi 3.
Em geral, você não deve misturar a unidade de Automation do Delphi 2, OleAuto, com as mais recentes
unidades ComObj ou ComServ. Se você deseja compilar um projeto Automation do Delphi 2 no Delphi 5,
a unidade OleAuto permanece no subdiretório \Delphi5\lib\Delphi2 para compatibilidade retroativa.
631
IDispatch
Os objetos Automation são essencialmente objetos COM que implementam a interface IDispatch. IDis-
patché definida na unidade System da seguinte maneira:
type
IDispatch = interface(IUnknown)
[‘{00020400-0000-0000-C000-000000000046}’]
function GetTypeInfoCount(out Count: Integer): Integer; stdcall;
function GetTypeInfo(Index, LocaleID: Integer; out TypeInfo):
Integer; stdcall;
function GetIDsOfNames(const IID: TGUID; Names: Pointer;
NameCount, LocaleID: Integer; DispIDs: Pointer): Integer; stdcall;
function Invoke(DispID: Integer; const IID: TGUID; LocaleID: Integer;
Flags: Word; var Params; VarResult, ExcepInfo, ArgErr: Pointer): Integer;
end;
A primeira coisa que você deveria saber é que não tem que entender a interface IDispatch pelo avesso
para tirar proveito do Automation no Delphi; portanto, não se deixe impressionar por essa complicada
interface. Geralmente, você não tem que interagir com essa interface diretamente porque o Delphi for-
nece um elegante encapsulamento do Automation, mas a descrição do IDispatch nesta seção deve forne-
cer a você uma boa base para entender o Automation.
Como o cerne da função IDispatch é o método Invoke( ), é por ela que vamos começar. Quando um
cliente obtém um ponteiro IDispatch para um servidor Automation, pode chamar o método Invoke( ) para
executar um método particular no servidor. O parâmetro DispID desse método armazena um número,
chamado ID de disparo, que indica o método no servidor que deve ser chamado. O parâmetro IID não é
usado. O parâmetro LocaleID contém informações de linguagem. O parâmetro Flags descreve o tipo de
método a ser usado e se é um método normal ou um método de colocação ou obtenção de uma proprie-
dade. A propriedade Params contém um ponteiro para um array de TDispParams, que armazena os parâme-
tros passados para o método. O parâmetro VarResult é um ponteiro para uma OleVariant, que irá armaze-
nar o valor de retorno do método que é chamado. ExcepInfo é um ponteiro para um registro TExcepInfo
que vai conter informação de erro se Invoke( ) retorna DISP_E_EXCEPTION. Finalmente, se Invoke( ) retorna
DISP_E_TYPEMISMATCH ou DISP_E_PARAMNOTFOUND, o parâmetro ArgError é um ponteiro para um integer que vai
conter o índice do parâmetro responsável pelo erro no array Params.
O método GetIDsOfName( ) do IDispatch é chamado para obter o ID de disparo de um ou mais nomes
de método quando determinadas strings identificam esses métodos. O parâmetro IID desse método não é
usado. O parâmetro Names aponta para um array de nomes de métodos PWideChar. O parâmetro NameCount
armazena o número de strings no array Names. LocaleID contém informação de linguagem. O último parâ-
metro, DispIDs, é um ponteiro para um array de integers NameCount, que GetIDsOfName( ) preencherá com os
IDs de disparo para os métodos listados no parâmetro Names.
GetTypeInfo( ) recupera a informação de tipo (informação de tipo é descrita daqui a pouco) para o
objeto Automation. O parâmetro Index representa o tipo de informação a ser obtido e normalmente deve
ser 0. O parâmetro LCID contém informação de linguagem. Se essa operação for bem-sucedida, o parâme-
tro TypeInfo armazenará um ponteiro ITypeInfo para a informação de tipo do objeto Automation.
O método GetTypeInfoCount( ) recupera o número das interfaces de informação de tipo aceitas pelo
objeto Automation no parâmetro Count. Atualmente, Count só pode conter dois possíveis valores: 0, signi-
ficando que o objeto Automation não aceita informação de tipo, e 1, significando que o objeto Automati-
on aceita informação de tipo.
Informação de tipo
Depois que investir um tempo enorme na configuração de um servidor Automation, seria uma humilha-
ção se possíveis usuários de seu servidor não pudessem explorar todo o seu potencial devido à falta de
documentação nos métodos e propriedades fornecidos. Felizmente, o Automation fornece um meio para
632 ajudá-lo a evitar esse problema permitindo aos programadores associar informação de tipo com objetos
Automation. Essa informação de tipo é armazenada em alguma coisa chamada biblioteca de tipo, e uma
biblioteca de tipo do servidor Automation pode ser vinculada à aplicação ou biblioteca do servidor como
um recurso ou armazenada em um arquivo externo. As bibliotecas de tipo contêm informação sobre clas-
ses, interfaces, tipos e outras entidades em um servidor. Essa informação fornece clientes do servidor Au-
tomation com a informação necessária para criar instâncias de cada classe e chamar métodos de modo
apropriado em cada interface.
O Delphi gera bibliotecas de tipo quando você adiciona objetos Automation para aplicações e biblio-
tecas. Além disso, o Delphi sabe como traduzir a informação de biblioteca de tipos no Object Pascal de
modo que você possa facilmente controlar servidores Automation a partir de suas aplicações Delphi.
Registro
Todos os objetos Automation devem fazer as mesmas entradas de Registro que os objetos COM regula-
res, mas os servidores Automation geralmente também fazem uma entrada adicional em
HKEY_CLASSES_ROOT\CLSID\{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx}
chamada ProgID, que fornece um identificador de string da classe Automation. Também é feita a entrada
HKEY_CLASSES_ROOT\(ProgID string), que contém a CLSID da classe de Automation para fazer referência cru-
zada com a primeira entrada de Registro em CLSID.
Agora adicione um objeto Automation ao projeto selecionando File, New no menu principal e esco-
lha Automation Object na página ActiveX da caixa de diálogo New Items, como mostra a Figura 23.3.
Isso chama o Automation Object Wizard mostrado na Figura 23.4.
No campo Class Name (nome de classe) da caixa de diálogo Automation Object Wizard (assistente
de objeto Automation), você deve digitar o nome da classe COM que você der a esse objeto Automation.
Automaticamente, o assistente anexará um T na frente do nome de classe quando criar a classe Object
Pascal do objeto Automation e um I na frente do nome de classe quando criar a interface principal do ob-
jeto Automation. A caixa de combinação Instancing no assistente pode conter qualquer um desses três
634 valores:
Valor Descrição
Interno Esse objeto OLE será usado tão-somente dentro da aplicação, não sendo registrado
no Registro do Sistema. Os processos externos não podem acessar servidores
Automation com esse tipo de instância.
Instância única Cada instância do servidor só pode exportar uma instância do objeto OLE. Se uma
aplicação controladora solicitar outra instância do objeto OLE, o Windows iniciará
uma nova instância da aplicação servidora.
Instância múltiplas Cada instância de servidor pode criar e exportar várias instâncias do objeto OLE.
Servidores em processo são sempre de múltiplas instâncias.
Quando você preencher a caixa de diálogo do assistente, o Delphi criará uma nova biblioteca de ti-
pos para seu projeto (se ainda não existe uma) e adicionará uma interface e uma co-classe à biblioteca de
tipos. Além disso, o assistente gerará uma nova unidade em seu projeto, contendo a implementação da
interface Automation adicionada à biblioteca de tipos. A Figura 23.5 mostra o editor de biblioteca de ti-
pos apresentado imediatamente após a caixa de diálogo do assistente e a Listagem 23.2 mostra a unidade
de implementação do objeto Automation.
FIGURA 23.5 Um projeto Automation novo como mostrado no editor de biblioteca de tipos.
unit TestImpl;
interface
uses
ComObj, ActiveX, Srv_TLB;
type
TAutoTest = class(TAutoObject, IAutoTest)
protected
{ Declarações protegidas}
end;
implementation
uses ComServ;
initialization
TAutoObjectFactory.Create(ComServer, TAutoTest, Class_AutoTest,
ciMultiInstance, tmApartment);
end.
635
O objeto Automation, TAutoTest, é uma classe que descende do TAutoObject. TAutoObject é a classe bási-
ca para todos os servidores Automation. À medida que você adiciona métodos à sua interface usando o
editor de biblioteca de tipos, novas estruturas de método serão geradas nessa unidade que você imple-
mentará, assim formando as entranhas do seu objeto Automation.
ATENÇÃO
Mais uma vez, tome cuidado para não confundir o TAutoObject do Delphi 2 (da unidade OleAuto) com o
TAutoObject do Delphi 5 (da unidade ComObj unit). Os dois não são compatíveis.
NOTA
À medida que você adiciona propriedades e métodos a objetos Automation na biblioteca de tipos, lem-
bre-se de que os parâmetros e os valores de retorno usados para essas propriedades e métodos devem ser
de tipos compatíveis com o Automation. Tipos compatíveis com Automation incluem Byte, SmallInt, Inte-
ger, Single, Double, Currency, TDateTime, WideString, WordBool, PSafeArray, TDecimal, OleVariant, Iunknown
e IDispatch.
Depois de preencher a biblioteca de tipos, tudo o que nos resta a fazer é preencher a implementação
de cada uma das estruturas de método criadas pelo editor de biblioteca de tipos. Essa unidade é mostrada
na Listagem 23.3.
636
Listagem 23.3 A unidade de implementação preenchida
unit TestImpl;
interface
uses
ComObj, ActiveX, Srv_TLB;
type
TAutoTest = class(TAutoObject, IAutoTest)
protected
function Get_EditText: WideString; safecall;
function Get_ShapeColor: OLE_COLOR; safecall;
procedure Set_EditText(const Value: WideString); safecall;
procedure Set_ShapeColor(Value: OLE_COLOR); safecall;
function Get_ShapeType: TxShapeType; safecall;
procedure Set_ShapeType(Value: TxShapeType); safecall;
procedure ShowInfo; safecall;
end;
implementation
uses ComServ, SrvMain, TypInfo, ExtCtrls, Dialogs, SysUtils, Graphics;
function TAutoTest.Get_EditText: WideString;
begin
Result := FrmAutoTest.Edit.Text;
end;
function TAutoTest.Get_ShapeColor: OLE_COLOR;
begin
Result := ColorToRGB(FrmAutoTest.Shape.Brush.Color);
end;
procedure TAutoTest.Set_EditText(const Value: WideString);
begin
FrmAutoTest.Edit.Text := Value;
end;
procedure TAutoTest.Set_ShapeColor(Value: OLE_COLOR);
begin
FrmAutoTest.Shape.Brush.Color := Value;
end;
function TAutoTest.Get_ShapeType: TxShapeType;
begin
Result := TxShapeType(FrmAutoTest.Shape.Shape);
end;
procedure TAutoTest.Set_ShapeType(Value: TxShapeType);
begin
FrmAutoTest.Shape.Shape := TShapeType(Value);
end;
procedure TAutoTest.ShowInfo;
const
SInfoStr = ‘The Shape’s color is %s, and it’s shape is %s.’#13#10 +
‘The Edit’s text is “%s.”’;
begin
with FrmAutoTest do
ShowMessage(Format(SInfoStr, [ColorToString(Shape.Brush.Color),
GetEnumName(TypeInfo(TShapeType), Ord(Shape.Shape)), Edit.Text]));
end;
initialization
TAutoObjectFactory.Create(ComServer, TAutoTest, Class_AutoTest,
ciMultiInstance, tmApartment);
end.
637
A cláusula uses dessa unidade contém uma unidade chamada Srv_TLB. Essa unidade é a tradução do
Object Pascal da biblioteca de tipos do projeto e é mostrada na Listagem 23.4.
unit Srv_TLB;
// ******************************************************************** //
// ATENÇÃO
// -----
// Os tipos declarados nesse arquivo foram gerados a partir dos dados lidos em uma
// biblioteca de tipos. Se essa biblioteca de tipos for explícita ou indiretamente
// (através de outra biblioteca de tipos que faça referência a essa biblioteca de
// tipos) reimportada ou o comando ‘Refresh’ (atualização) do Type Library Editor
// for ativado durante a edição da biblioteca de tipos, o conteúdo desse arquivo
// será regenerado e todas as modificações manuais serão perdidas.
// ******************************************************************** //
// PASTLWTR : $Revision: 1.88 $
// Arquivo gerado em 28/10/99 às 13:55:17 da biblioteca de tipos descrita a seguir
// ******************************************************************** //
// NOTA:
// Itens guardados pelo $IFDEF_LIVE_SERVER_AT_DESIGN_TIME são usados pelas
// propriedades que retornam objetos que podem precisar ser explicitamente criados
// através de uma chamada de função antes de qualquer acesso através da propriedade.
// Esses itens foram desativados para impedir usos acidentais de dentro do
// inspetor de objeto. Você deve ativá-los definindo
// LIVE_SERVER_AT_DESIGN_TIME ou removendo-os seletivamente dos
// blocos $IFDEF. No entanto, esses itens ainda têm que ser criados, via programação,
// por um métod//o da CoClass apropriada antes de poderem ser usados
// ******************************************************************** //
// Type Lib: C:\work\d5dg\code\Ch23\Automate\Srv.tlb (1)
// IID\LCID: {B43DD7DB-21F8-4244-A494-C4793366691B}\0
// Helpfile:
// DepndLst:
// (1) v2.0 stdole, (C:\WINDOWS\SYSTEM\STDOLE2.TLB)
// (2) v4.0 StdVCL, (C:\WINDOWS\SYSTEM\STDVCL40.DLL)
// ******************************************************************** //
{$TYPEDADDRESS OFF} Unidade deve ser compilada sem a interface de ponteiros
// de verificação de tipo
// *********************************************************************//
// GUIDS declarados na TypeLibrary. Os prefixos a seguir são usados:
// Bibliotecas de tipos : LIBID_xxxx
// CoClasses : CLASS_xxxx
// DISPInterfaces : DIID_xxxx
// Interfaces não-DISP: IID_xxxx
// *********************************************************************//
const
// Versão principal e secundárias de TypeLibrary
SrvMajorVersion = 1;
SrvMinorVersion = 0;
LIBID_Srv: TGUID = ‘{B43DD7DB-21F8-4244-A494-C4793366691B}’;
// *********************************************************************//
// Declaração de Enumerations definida na biblioteca de tipos
// *********************************************************************//
// Constantes para enum TxShapeType
type
TxShapeType = TOleEnum;
const
stRectangle = $00000000;
stSquare = $00000001;
stRoundRect = $00000002;
stRoundSquare = $00000003;
stEllipse = $00000004;
stCircle = $00000005;
type
// *********************************************************************//
// Encaminha declaração de tipos definidos em TypeLibrary
// *********************************************************************//
IAutoTest = interface;
IAutoTestDisp = dispinterface;
// *********************************************************************//
// Declaração de CoClasses definidas na biblioteca de tipos
// (NOTA: Aqui mapeamos cada CoClass para sua interface-padrão)
// *********************************************************************//
AutoTest = IAutoTest;
// *********************************************************************//
// Interface: IAutoTest
// Flags: (4416) OleAutomation dual disparável
// GUID: {C16B6A4C-842C-417F-8BF2-2F306F6C6B59}
// *********************************************************************//
IAutoTest = interface(IDispatch)
[‘{C16B6A4C-842C-417F-8BF2-2F306F6C6B59}’]
function Get_EditText: WideString; safecall;
procedure Set_EditText(const Value: WideString); safecall;
function Get_ShapeColor: OLE_COLOR; safecall;
procedure Set_ShapeColor(Value: OLE_COLOR); safecall;
function Get_ShapeType: TxShapeType; safecall;
procedure Set_ShapeType(Value: TxShapeType); safecall;
procedure ShowInfo; safecall;
property EditText: WideString read Get_EditText write Set_EditText;
property ShapeColor: OLE_COLOR read Get_ShapeColor write
Set_ShapeColor;
property ShapeType: TxShapeType read Get_ShapeType write
Set_ShapeType;
end;
// *********************************************************************//
// DispIntf: IAutoTestDisp
// Flags: (4416) OleAutomation dual disparável
// GUID: {C16B6A4C-842C-417F-8BF2-2F306F6C6B59}
// *********************************************************************//
IAutoTestDisp = dispinterface
[‘{C16B6A4C-842C-417F-8BF2-2F306F6C6B59}’]
property EditText: WideString dispid 1; 639
Listagem 23.4 Continuação
// *********************************************************************//
// A classe CoAutoTest fornece um método Create e CreateRemote para
// criar instâncias da interface-padrão IAutoTest exposta pelo
// CoClass AutoTest. As funções foram criadas para serem usadas pelos
// clientes que desejam automatizar os objetos CoClass expostos pelo
// servidor dessa biblioteca de tipos.
// *********************************************************************//
CoAutoTest = class
class function Create: IAutoTest;
class function CreateRemote(const MachineName: string): IAutoTest;
end;
// *********************************************************************//
// declaração da classe OLE Server Proxy
// Server Object : TAutoTest
// Help String : AutoTest Object
// Default Interface: IAutoTest
// Def. Intf. DISP? : Não
// Event Interface:
// TypeFlags : (2) CanCreate
// *********************************************************************//
{$IFDEF LIVE_SERVER_AT_DESIGN_TIME}
TAutoTestProperties= class;
{$ENDIF}
TAutoTest = class(TOleServer)
private
FIntf: IAutoTest;
{$IFDEF LIVE_SERVER_AT_DESIGN_TIME}
FProps: TAutoTestProperties;
function GetServerProperties: TAutoTestProperties;
{$ENDIF}
function GetDefaultInterface: IAutoTest;
protected
procedure InitServerData; override;
function Get_EditText: WideString;
procedure Set_EditText(const Value: WideString);
function Get_ShapeColor: OLE_COLOR;
procedure Set_ShapeColor(Value: OLE_COLOR);
function Get_ShapeType: TxShapeType;
procedure Set_ShapeType(Value: TxShapeType);
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure Connect; override;
procedure ConnectTo(svrIntf: IAutoTest);
procedure Disconnect; override;
procedure ShowInfo;
property DefaultInterface: IAutoTest read GetDefaultInterface;
640 property EditText: WideString read Get_EditText write Set_EditText;
Listagem 23.4 Continuação
{$IFDEF LIVE_SERVER_AT_DESIGN_TIME}
// *********************************************************************//
// Classe Proxy das propriedades do servidor OLE
// Objeto servidor : TAutoTest
// (Esse objeto é usado pelo Property Inspector da IDE para permitir a edição
// das propriedades desse servidor)
// *********************************************************************//
TAutoTestProperties = class(TPersistent)
private
FServer: TAutoTest;
function GetDefaultInterface: IAutoTest;
constructor Create(AServer: TAutoTest);
protected
function Get_EditText: WideString;
procedure Set_EditText(const Value: WideString);
function Get_ShapeColor: OLE_COLOR;
procedure Set_ShapeColor(Value: OLE_COLOR);
function Get_ShapeType: TxShapeType;
procedure Set_ShapeType(Value: TxShapeType);
public
property DefaultInterface: IAutoTest read GetDefaultInterface;
published
property EditText: WideString read Get_EditText write Set_EditText;
property ShapeColor: OLE_COLOR read Get_ShapeColor write
Set_ShapeColor;
property ShapeType: TxShapeType read Get_ShapeType write
Set_ShapeType;
end;
{$ENDIF}
procedure Register;
implementation
uses ComObj;
class function CoAutoTest.Create: IAutoTest;
begin
Result := CreateComObject(CLASS_AutoTest) as IAutoTest;
end;
class function CoAutoTest.CreateRemote(const MachineName: string):
IAutoTest;
begin
Result := CreateRemoteComObject(MachineName, CLASS_AutoTest) as IAutoTest;
end;
procedure TAutoTest.InitServerData;
const
CServerData: TServerData = ( 641
Listagem 23.4 Continuação
ClassID: ‘{64C576F0-C9A7-420A-9EAB-0BE98264BC9D}’;
IntfIID: ‘{C16B6A4C-842C-417F-8BF2-2F306F6C6B59}’;
EventIID: ‘’;
LicenseKey: nil;
Version: 500);
begin
ServerData := @CServerData;
end;
procedure TAutoTest.Connect;
var
punk: IUnknown;
begin
if FIntf = nil then
begin
punk := GetServer;
Fintf:= punk as IAutoTest;
end;
end;
procedure TAutoTest.ConnectTo(svrIntf: IAutoTest);
begin
Disconnect;
FIntf := svrIntf;
end;
procedure TAutoTest.DisConnect;
begin
if Fintf < > nil then
begin
FIntf := nil;
end;
end;
function TAutoTest.GetDefaultInterface: IAutoTest;
const
ErrStr = ‘DefaultInterface is NULL. Component is not connected to ‘ +
‘Server. Você must call ‘’Connect’’ or ‘’ConnectTo’’ before this ‘ +
‘operation’;
begin
if FIntf = nil then
Connect;
Assert(FIntf < > nil, ErrStr);
Result := FIntf;
end;
constructor TAutoTest.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
{$IFDEF LIVE_SERVER_AT_DESIGN_TIME}
FProps := TAutoTestProperties.Create(Self);
{$ENDIF}
end;
destructor TAutoTest.Destroy;
begin
{$IFDEF LIVE_SERVER_AT_DESIGN_TIME}
FProps.Free;
642 {$ENDIF}
Listagem 23.4 Continuação
inherited Destroy;
end;
{$IFDEF LIVE_SERVER_AT_DESIGN_TIME}
function TAutoTest.GetServerProperties: TAutoTestProperties;
begin
Result := FProps;
end;
{$ENDIF}
function TAutoTest.Get_EditText: WideString;
begin
Result := DefaultInterface.Get_EditText;
end;
procedure TAutoTest.Set_EditText(const Value: WideString);
begin
DefaultInterface.Set_EditText(Value);
end;
function TAutoTest.Get_ShapeColor: OLE_COLOR;
begin
Result := DefaultInterface.Get_ShapeColor;
end;
procedure TAutoTest.Set_ShapeColor(Value: OLE_COLOR);
begin
DefaultInterface.Set_ShapeColor(Value);
end;
function TAutoTest.Get_ShapeType: TxShapeType;
begin
Result := DefaultInterface.Get_ShapeType;
end;
procedure TAutoTest.Set_ShapeType(Value: TxShapeType);
begin
DefaultInterface.Set_ShapeType(Value);
end;
procedure TAutoTest.ShowInfo;
begin
DefaultInterface.ShowInfo;
end;
{$IFDEF LIVE_SERVER_AT_DESIGN_TIME}
constructor TAutoTestProperties.Create(AServer: TAutoTest);
begin
inherited Create;
FServer := AServer;
end;
function TAutoTestProperties.GetDefaultInterface: IAutoTest;
begin
Result := FServer.DefaultInterface;
end;
function TAutoTestProperties.Get_EditText: WideString;
begin
Result := DefaultInterface.Get_EditText;
end;
procedure TAutoTestProperties.Set_EditText(const Value: WideString);
begin
DefaultInterface.Set_EditText(Value); 643
Listagem 23.4 Continuação
end;
function TAutoTestProperties.Get_ShapeColor: OLE_COLOR;
begin
Result := DefaultInterface.Get_ShapeColor;
end;
procedure TAutoTestProperties.Set_ShapeColor(Value: OLE_COLOR);
begin
DefaultInterface.Set_ShapeColor(Value);
end;
function TAutoTestProperties.Get_ShapeType: TxShapeType;
begin
Result := DefaultInterface.Get_ShapeType;
end;
procedure TAutoTestProperties.Set_ShapeType(Value: TxShapeType);
begin
DefaultInterface.Set_ShapeType(Value);
end;
{$ENDIF}
procedure Register;
begin
RegisterComponents(‘Servers’,[TAutoTest]);
end;
end.
Analisando essa unidade de cima para baixo, você perceberá que a versão da biblioteca de tipos é es-
pecificada primeiro e só então o GUID da biblioteca de tipos, LIBID_Srv, é declarado. Esse GUID será usa-
do quando a biblioteca de tipos for registrada no Registro do Sistema. Posteriormente, os valores para a
enumeração TxShapeType são listados. Vale frisar que os valores da enumeração são declarados como cons-
tantes, não como um tipo enumerado do Object Pascal. Isso se deve ao fato de as enums da biblioteca de
tipos serem como as enums da C/C++ e, ao contrário do que ocorre no Object Pascal, terem que come-
çar no valor ordinal zero ou ter um valor seqüencial.
Em seguida, já na unidade Srv_TLB, a interface IAutoTest é declarada. Nessa declaração de interface,
você verá as propriedades e métodos que criou no editor da biblioteca de tipo. Além disso, verá os méto-
dos Get_XXX e Set_XXX gerados como métodos read e write para cada uma das propriedades.
Safecall
Safecall é a convenção de chamada padrão dos métodos inseridos no editor de biblioteca de tipos,
como você pôde ver na declaração IAutoTest. Na verdade, Safecall é mais que uma convenção de cha-
mada, pois implica duas coisas: primeiro, significa que o método será chamado usando a convenção
de chamada safecall. Segundo, significa que o método será encapsulado de modo a retornar um va-
lor HResult para quem faz a chamada. Por exemplo, suponha que você tem um método com a seguinte
aparência no Object Pascal:
function Foo(W: WideString): Integer; safecall;
Na verdade, esse método é compilado em um código que tem a seguinte aparência:
function Foo(W: WideString; out RetVal: Integer): HResult; stdcall;
A vantagem de safecall é que ele captura todas as exceções antes de elas serem remetidas para
o responsável pela chamada. Quando uma exceção não-manipulada é produzida em um método sa-
fecall, ela é manipulada pelo wrapper implícito e convertida em HResult, que é retornado para o res-
ponsável pela chamada.
644
Posteriormente, vê-se em Srv_TLB a declaração de dispinterface para o objeto Automation: IAutoTest-
Disp. Uma dispinterface sinaliza para o responsável pela chamada que métodos Automation podem ser
executados pelo Invoke( ), mas não implica uma interface personalizada através das quais os métodos po-
dem ser executados. Embora a interface IAutoTest possa ser usada pelas ferramentas de desenvolvimento
que aceitam Automation de vinculação inicial, a dispinterface de IautoTestDisp pode ser usada pelas ferra-
mentas que aceitam vinculação inicial.
A unidade Srv_TLB declara em seguida uma classe chamada CoAutoTest, que torna a criação de objetos
Automation fácil; basta chamar CoAutoTest.Create( ) para criar uma instância do objeto Automation.
Finalmente, Srv_TLB cria uma classe chamada TAutoTest que envolve o servidor em um componente
que pode ser posicionado na palheta. Esse recurso, introduzido no Delphi 5, é destinado mais para servi-
dores Automation que você importa do que para os servidores Automation novos que você cria.
Como dissemos, você deve executar essa aplicação uma vez para registrá-la no Registro do Sistema.
Posteriormente neste capítulo, você vai aprender sobre a aplicação controladora usada para manipular
esse servidor.
NOTA
Se você não está familiarizado com DLLs, elas são discutidos em profundidade no Capítulo 9. Este capítulo
parte da premissa de você tem algum conhecimento de programação em DLL.
Como dissemos, para servir como um servidor Automation em processo, uma DLL deve exportar
quatro funções definidas na unidade ComServ: DllGetClassObject( ), DllCanUnloadNow( ), DllRegisterServer( )
e DllUnregisterServer( ). Faça isso adicionando essas funções à cláusula exports no seu arquivo de projeto,
como mostrado no arquivo de projeto IPS.dpr na Listagem 23.5.
library IPS;
uses
ComServ;
exports
DllRegisterServer,
DllUnregisterServer,
DllGetClassObject,
DllCanUnloadNow;
begin
end.
O objeto Automation é adicionado ao projeto da DLL da mesma maneira que um projeto executá-
vel: através do Automation Object Wizard. Para esse projeto, você só vai adicionar uma propriedade e
um método, como mostrado no editor de biblioteca de tipos da Figura 23.7. A versão da biblioteca de ti-
pos do Object Pascal, IPS_TLB, é mostrada na Listagem 23.6. 645
FIGURA 23.7 O projeto IPS no editor de biblioteca de tipos.
unit IPS_TLB;
// ************************************************************************ //
// ATENÇÃO
// -----
// Os tipos declarados neste arquivo foram gerados a partir de dados lidos de uma
// biblioteca de tipos. Se essa biblioteca de tipo for explícita ou indiretamente
// (através de outra biblioteca de tipos que faça referência a essa biblioteca de
// tipos) reimportada ou o comando ‘Refresh’ (atualizar) do Type Library Editor
// for ativado durante a edição da biblioteca de tipos, o conteúdo desse arquivo
// será regenerado e todas as modificações manuais serão perdidas.
// ************************************************************************ //
// PASTLWTR : $Revision: 1.79 $
// Arquivo gerado em 14/8/99, às 23:37:16 a partir da biblioteca de tipos descrita
// abaixo.
// ************************************************************************ //
// Biblioteca de tipos: C:\work\d5dg\code\Ch23\Automate\IPS.tlb (1)
// IID\LCID: {17A05B88-0094-11D1-A9BF-F15F8BE883D4}\0
// Helpfile:
// DepndLst:
// (1) v1.0 stdole, (C:\WINDOWS\SYSTEM\stdole32.tlb)
// (2) v2.0 StdType, (c:\WINDOWS\SYSTEM\OLEPRO32.DLL)
// (3) v1.0 StdVCL, (C:\WINDOWS\SYSTEM\STDVCL32.DLL)
// ************************************************************************ //
interface
uses Windows, ActiveX, Classes, Graphics, OleServer, OleCtrls, StdVCL;
// *********************************************************************//
// GUIDS declarados em TypeLibrary. Os prefixos a seguir são usados:
// Bibliotecas de tipo : LIBID_xxxx
// CoClasses : CLASS_xxxx
// DISPInterfaces : DIID_xxxx
// Interfaces não-DISP: IID_xxxx
// *********************************************************************//
const
// Versões principal e secundária de TypeLibrary
IPSMajorVersion = 1;
IPSMinorVersion = 0;
end.
É claro que esse é um servidor Automation bastante simples, mas ele serve para ilustrar o tópico que
estamos discutindo: a propriedade MessageStr pode ser definida como um valor e em seguida ser mostrada
com a função ShowMessageStr( ). A implementação da interface IIPTest reside na unidade IPSMain.pas, que é
mostrada na Listagem 23.7.
unit IPSMain;
interface
uses
ComObj, IPS_TLB;
type
TIPTest = class(TAutoObject, IIPTest)
private
MessageStr: string;
protected
function Get_MessageStr: WideString; safecall;
procedure Set_MessageStr(const Value: WideString); safecall;
function ShowMessageStr: Integer; safecall;
end;
implementation
uses Windows, ComServ;
function TIPTest.Get_MessageStr: WideString;
begin
Result := MessageStr;
end;
function TIPTest.ShowMessageStr: Integer;
begin
MessageBox(0, PChar(MessageStr), ‘Sua string is...’, MB_OK);
Result := Length(MessageStr);
end;
procedure TIPTest.Set_MessageStr(const Value: WideString);
begin
MessageStr := Value;
end;
initialization
TAutoObjectFactory.Create(ComServer, TIPTest, Class_IPTest, ciMultiInstance,
tmApartment);
end.
Como você já viu neste capítulo, os servidores em processo são registrados de modo diferente dos
servidores fora de processo; uma função DllRegisterServer( ) do servidor em processo é chamada para re-
gistrá-lo no Registro do Sistema. A IDE do Delphi torna isso muito fácil: Selecione Run, Register Acti-
648 veX Server (executar, servidor Register ActiveX) no menu principal.
Criando controladores de Automation
O Delphi facilita sobremaneira o controle dos servidores Automation nas suas aplicações. O Delphi tam-
bém dá a você uma grande flexibilidade no que tange ao modo como você deseja controlar os servidores
Automation, com opções para vinculação inicial usando interfaces ou vinculação tardia usando dispin-
terfaces ou variantes.
Quando você dá um clique no botão Connect, a aplicação Control se conecta ao servidor de várias
formas diferentes com o código a seguir:
FIntf := CoAutoTest.Create;
FDispintf := CreateComObject(Class_AutoTest) as IAutoTestDisp;
FVar := CreateOleObject(‘Srv.AutoTest’);
Esse código mostra as variáveis interface, dispinterface e OleVariant, que criam uma instância do ser-
vidor Automation de diferentes formas. O que essas diferentes técnicas têm de interessante é que são
quase totalmente intercambiáveis. Por exemplo, o código a seguir também é correto:
FIntf := CreateComObject(Class_AutoTest) as IAutoTest;
FDispintf := CreateOleObject(‘Srv.AutoTest’) as IAutoTestDisp;
FVar := CoAutoTest.Create;
A Listagem 23.8 mostra a unidade Ctrl, que contém o restante do código-fonte do controlador Au-
tomation. Observe que a aplicação permite a você manipular o servidor usando cada interface, dispinter-
face ou OleVariant.
Listagem 23.8 Ctrl.pas – a unidade principal para o projeto de controlador para um projeto de servidor
fora do processo
unit Ctrl;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, ColorGrd, ExtCtrls, Srv_TLB, Buttons;
type 649
Listagem 23.8 Continuação
TControlForm = class(TForm)
CallViaRG: TRadioGroup;
ShapeTypeRG: TRadioGroup;
GroupBox1: TGroupBox;
GroupBox2: TGroupBox;
Edit: TEdit;
GroupBox3: TGroupBox;
ConBtn: TButton;
DisBtn: TButton;
InfoBtn: TButton;
ColorBtn: TButton;
ColorDialog: TColorDialog;
ColorShape: TShape;
ExitBtn: TButton;
TextBtn: TButton;
procedure ConBtnClick(Sender: TObject);
procedure DisBtnClick(Sender: TObject);
procedure ColorBtnClick(Sender: TObject);
procedure ExitBtnClick(Sender: TObject);
procedure TextBtnClick(Sender: TObject);
procedure InfoBtnClick(Sender: TObject);
procedure ShapeTypeRGClick(Sender: TObject);
private
{ Declarações privadas }
FIntf: IAutoTest;
FDispintf: IAutoTestDisp;
FVar: OleVariant;
procedure SetControls;
procedure EnableControls(DoEnable: Boolean);
public
{ Declarações públicas }
end;
var
ControlForm: TControlForm;
implementation
{$R *.DFM}
uses ComObj;
procedure TControlForm.SetControls;
// Inicializa o controle como os valores do servidor atual
begin
case CallViaRG.ItemIndex of
0:
begin
ColorShape.Brush.Color := FIntf.ShapeColor;
ShapeTypeRG.ItemIndex := FIntf.ShapeType;
Edit.Text := FIntf.EditText;
end;
1:
begin
ColorShape.Brush.Color := FDispintf.ShapeColor;
ShapeTypeRG.ItemIndex := FDispintf.ShapeType;
Edit.Text := FDispintf.EditText;
650 end;
Listagem 23.8 Continuação
2:
begin
ColorShape.Brush.Color := FVar.ShapeColor;
ShapeTypeRG.ItemIndex := FVar.ShapeType;
Edit.Text := FVar.EditText;
end;
end;
end;
procedure TControlForm.EnableControls(DoEnable: Boolean);
begin
DisBtn.Enabled := DoEnable;
InfoBtn.Enabled := DoEnable;
ColorBtn.Enabled := DoEnable;
ShapeTypeRG.Enabled := DoEnable;
Edit.Enabled := DoEnable;
TextBtn.Enabled := DoEnable;
end;
procedure TControlForm.ConBtnClick(Sender: TObject);
begin
FIntf := CoAutoTest.Create;
FDispintf := CreateComObject(Class_AutoTest) as IAutoTestDisp;
FVar := CreateOleObject(‘Srv.AutoTest’);
EnableControls(True);
SetControls;
end;
procedure TControlForm.DisBtnClick(Sender: TObject);
begin
FIntf := nil;
FDispintf := nil;
FVar := Unassigned;
EnableControls(False);
end;
procedure TControlForm.ColorBtnClick(Sender: TObject);
var
NewColor: TColor;
begin
if ColorDialog.Execute then
begin
NewColor := ColorDialog.Color;
case CallViaRG.ItemIndex of
0: FIntf.ShapeColor := NewColor;
1: FDispintf.ShapeColor := NewColor;
2: FVar.ShapeColor := NewColor;
end;
ColorShape.Brush.Color := NewColor;
end;
end;
case CallViaRG.ItemIndex of
0: FIntf.EditText := Edit.Text;
1: FDispintf.EditText := Edit.Text;
2: FVar.EditText := Edit.Text;
end;
end;
end.
Outra coisa interessante que esse código ilustra é o modo como é fácil encerrar uma conexão de
um servidor Automation: as interfaces e dispinterfaces podem ser definidas como nil, e as variantes
podem ser definidas como Unassigned. É claro que o servidor Automation também será liberado quando
a aplicação Control for fechada, como uma parte da finalização normal desses tipos permanentemente
gerenciados.
DICA
Como na grande maioria das vezes as interfaces são mais bem executadas do que as dispinterfaces e varian-
tes, você deve usar interfaces para controlar servidores Automation sempre que elas estiverem disponíveis.
Das três possibilidades, são as variantes que apresentam o pior desempenho, pois, no runtime, uma
chamada de Automation através de uma variante deve chamar GetIDsOfNames( ) para converter um nome
de método em um ID de disparo antes de poder executar o método com uma chamada para Invoke( ).
O desempenho de dispinterfaces está entre o de uma interface e o de uma variante. Você pode se
perguntar, no entanto, por que o desempenho é diferente se tanto variantes quanto as dispinterfaces
usam vinculação tardia. A razão para isso é que as dispinterfaces tiram vantagem de uma otimização
chamada vinculação de ID, o que significa que os IDs de disparo dos métodos são conhecidos no tempo
de compilação e, portanto, o compilador não precisa gerar uma chamada de runtime para GetIDsOfNa-
me( ) anterior de chamar Invoke( ). Outra vantagem, talvez mais óbvia, das dispinterfaces sobre as vari-
antes é que as primeiras permitem o uso de CodeInsight para facilitar a codificação, o que não é possível
usando variantes.
652
FIGURA 23.9 Controlador e servidor Automation.
Listagem 23.9 IPCMain.pas – a unidade principal para o projeto de controlador para o projeto de
servidor em processo
unit IPCMain;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, ExtCtrls, IPS_TLB;
type
TIPCForm = class(TForm)
ExitBtn: TButton;
Panel1: TPanel;
ConBtn: TButton;
DisBtn: TButton;
Edit: TEdit;
SetBtn: TButton;
ShowBtn: TButton;
procedure ConBtnClick(Sender: TObject);
procedure DisBtnClick(Sender: TObject);
procedure SetBtnClick(Sender: TObject); 653
Listagem 23.9 Continuação
end.
Lembre-se de certificar-se de que o servidor foi registrado antes de tentar executar IPCtrl. Você
pode fazer isso de diversas formas: usando Run, Register ActiveX Server a partir do menu principal
enquanto o projeto IPS é carregado, usando o utilitário RegSvr32.exe do Windows e usando a ferra-
menta TRegSvr.exe que vem com o Delphi. A Figura 23.11 mostra esse projeto em ação controlando o
654 servidor IPS.
FIGURA 23.11 IPCtrl controlando o servidor IPS.
Eventos de Automation
Nós, programadores em Delphi, sempre subestimamos os eventos. Você pressiona um botão, dá um du-
plo clique em OnClick no Object Inspector e escreve algum código. Nada demais. Mesmo do ponto de vis-
ta do escritor do controle, eventos têm um quê de simplório. Você cria um tipo de método novo, adicio-
na um campo e uma propriedade publicada a seu controle e vai cuidar da vida. Para os programadores
em COM do Delphi, no entanto, os eventos podem ser assustadores. Muitos programadores em COM
do Delphi evitam eventos pela simples razão de que “não têm tempo para dominar esse hipopótamo”. Se
você faz parte desse grupo, ficará feliz em saber que o trabalho com eventos na verdade nada mais tem de
difícil, graças a alguns fascinantes suportes internos fornecidos pelo Delphi. Embora todos os termos no-
vos associados a eventos Automation tenham uma aparência pomposa, nesta seção eu espero desmistifi-
car eventos a um ponto tal que alguém diga assim: era essa minhoca que eu achava que era um bicho-de-
sete-cabeças?
Eventos no Delphi
O Delphi é um fiel adepto da metodologia MASSI (mantenha a simplicidade, seu idiota!) quando está
lidando com eventos. Os eventos são implementados como ponteiros de método – esses ponteiros po- 655
dem ser atribuídos a algum método na aplicação e são executados quando um método é chamado atra-
vés do ponteiro de método. Só para ilustrar, considere a banal situação de desenvolvimento de aplica-
ção de uma aplicação que precisa manipular um evento em um componente. Teoricamente, o “servi-
dor” nesse caso seria um componente, que define e aciona o evento. O “cliente” é a aplicação que em-
prega o componente, pois se conecta ao evento atribuindo algum nome de método específico ao pontei-
ro de método de evento.
Embora esse simples modelo de evento seja uma das coisas que torna o Delphi elegante e fácil de
usar, com certeza essa praticidade sacrifica parte do seu poder. Por exemplo, não há um recurso interno
que permita que vários clientes escutem o mesmo evento (isso é chamado de multidifusão). Também não
existe um meio de se obter dinamicamente uma descrição de tipo para um evento sem escrever um códi-
go RTTI (que provavelmente você não deveria estar usando em uma aplicação de qualquer forma, devi-
do a sua natureza específica à versão).
Eventos em Automation
Enquanto o modelo de evento do Delphi é simples e limitado, o modelo de evento do Automation é po-
deroso porém mais complexo. Como um programador em COM, você já deve ter percebido que os
eventos são implementados no Automation usando interfaces. Em vez de haver um para cada método,
os eventos existem apenas como parte de uma interface. Essa interface costuma ser chamada de interface
de eventos ou interface de saída. Ela é chamada de saída porque não é implementada pelo servidor como
as outras interfaces, mas em vez disso é implementada pelos clientes do servidor, e os métodos da interfa-
ce serão chamados de dentro para fora, ou seja, do servidor para o cliente. Como todas as interfaces, as
interfaces de evento são associadas a elas por meio de identificações de interface (IIDs), que as identifica
com exclusividade. Além disso, a descrição das interfaces de eventos é encontrada na biblioteca de tipo
de um objeto Automation, vinculada à co-classe do objeto Automation, como outras interfaces.
Os servidores que precisam expor as interfaces de eventos para os clientes devem implementar a in-
terface IConnectionPointContainer. Essa interface é definida na unidade ActiveX da seguinte forma:
type
IConnectionPointContainer = interface
[‘{B196B284-BAB4-101A-B69C-00AA00341D07}’]
function EnumConnectionPoints(out Enum: IEnumConnectionPoints):
HResult; stdcall;
function FindConnectionPoint(const iid: TIID;
out cp: IConnectionPoint): HResult; stdcall;
end;
Para o COM, um ponto de conexão descreve a entidade que fornece acesso programático a uma in-
terface de saída. Se um cliente precisa determinar se um servidor aceita eventos, tudo que ele tem de fazer
é QueryInterface atrás da interface IConnectionPointContainer. Se essa interface estiver presente, o servidor
será capaz de expor os eventos. O método EnumConnectionPoints( ) de IConnectionPointContainer permite
que os clientes percorram todas as interfaces de saída aceitas pelo servidor. Os clientes podem usar o mé-
todo FindConnectionPoint( ) para obter uma interface de saída específica.
Você vai perceber que FindConnectionPoint( ) fornece um IConnectionPoint que representa uma inter-
face externa. Além disso, IConnectionPoint é definida na unidade ActiveX da seguinte maneira:
type
IConnectionPoint = interface
[‘{B196B286-BAB4-101A-B69C-00AA00341D07}’]
function GetConnectionInterface(out iid: TIID): HResult; stdcall;
function GetConnectionPointContainer(
out cpc: IConnectionPointContainer): HResult; stdcall;
function Advise(const unkSink: IUnknown; out dwCookie: Longint):
HResult; stdcall;
656 function Unadvise(dwCookie: Longint): HResult; stdcall;
function EnumConnections(out Enum: IEnumConnections): HResult;
stdcall;
end;
O servidor
A primeira etapa para criar o servidor é criar uma aplicação nova. Só para ilustrar, vamos criar uma nova
aplicação contendo um formulário com um TMemo alinhado pelo cliente, como mostra a Figura 23.12.
Em seguida, vamos adicionar um objeto Automation a essa aplicação selecionando File, New, Acti-
veX, Automation Object no menu principal. Isso chama o Automation Object Wizard (assistente de obje-
to de automação) (ver a Figura 23.4).
Observe a opção Generate Event Support Code (gerar código de suporte a evento) no Automation
Object Wizard. Essa caixa de diálogo deve ser selecionada porque vai gerar o código necessário para ex-
por uma interface de saída no objeto Automation. Isso também vai criar a interface de saída na biblioteca
de tipos. Depois de selecionar OK nessa caixa de diálogo, somos apresentados à janela Type Library Edi- 657
tor. Tanto a interface de Automation quanto a interface de saída já estão presentes na biblioteca de tipos
(chamadas IServerWithEvents e IServerWithEventsEvents, respectivamente). Os métodos AddText( ) e Clear( )
foram adicionados à interface IServerWithEvents e os métodos OnTextChanged( ) e OnClear( ) foram adiciona-
dos à interface IServerWithEventsEvents.
Como você deve estar imaginando, Clear( ) apagará o conteúdo da memória e AddText( )adicionará
outra linha de texto à memória. O evento OnTextChanged( ) será acionado quando o conteúdo da memória
mudar e o evento OnClear( ) será acionado quando a memória for apagada. Observe também que tanto
AddText( ) como OnTextChanged( ) têm um parâmetro do tipo WideString.
A primeira coisa a fazer é implementar os métodos AddText( ) e Clear( ). A implementação desses
métodos é mostrada aqui:
procedure TServerWithEvents.AddText(const NewText: WideString);
begin
MainForm.Memo.Lines.Add(NewText);
end;
procedure TServerWithEvents.Clear;
begin
MainForm.Memo.Lines.Clear;
if FEvents < > nil then FEvents.OnClear;
end;
Você deve estar familiarizado com todos esses códigos, exceto, talvez, a ultima linha de Clear( ).
Esse código assegura que há um depósito de cliente sinalizado no evento verificando para nil; em segui-
da, ele primeiro aciona o evento simplesmente chamando OnClear( ).
Para configurar o evento OnTextChanged( ), primeiro temos que manipular o evento OnChange da me-
mória. Faremos fazer isso inserindo uma linha de código no método Initialized( ) do TServerWithEvents
que aponta o evento para o método em TServerWithEvents:
MainForm.Memo.OnChange := MemoChange;
Esse código também verifica se o cliente está ouvindo; em seguida, aciona o evento, passando o tex-
to da memória como o parâmetro.
Acredite ou não, o servidor já está devidamente implementado! Agora vamos atacar o cliente.
O cliente
O cliente é uma aplicação com um formulário que contém TEdit, Tmemo e três componentes TButton, como
mostra a Figura 23.13.
658
Na unidade principal da aplicação-cliente, a unidade Server_TLB foi adicionada à cláusula uses a fim
de que tenhamos acesso aos tipos e aos métodos contidos nessa unidade. O objeto do formulário princi-
pal, TMainForm, da aplicação cliente vai conter um campo que faz referência ao servidor chamado FServer
do tipo IServerWithEvents. Nós vamos criar uma instância do servidor no construtor do TmainForm usando a
classe de ajuda encontrada em Server_TLB, como esta:
FServer := CoServerWithEvents.Create;
A próxima etapa é implementar a classe de depósito de evento. Como essa classe será chamada pelo
servidor via Automation, deve implementar IDispatch (e por extensão IUnknown). A declaração de tipo para
essa classe é mostrada aqui:
type
TEventSink = class(TObject, IUnknown, IDispatch)
private
FController: TMainForm;
{ IUnknown }
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
{ IDispatch }
function GetTypeInfoCount(out Count: Integer): HResult; stdcall;
function GetTypeInfo(Index, LocaleID: Integer; out TypeInfo):
HResult; stdcall;
function GetIDsOfNames(const IID: TGUID; Names: Pointer;
NameCount, LocaleID: Integer; DispIDs: Pointer): HResult; stdcall;
function Invoke(DispID: Integer; const IID: TGUID; LocaleID: Integer;
Flags: Word; var Params; VarResult, ExcepInfo, ArgErr: Pointer):
HResult; stdcall;
public
constructor Create(Controller: TMainForm);
end;
A maioria dos métodos de IUnknown e IDispatch não é implementada, com as exceções notável de
IUnknown.QueryInterface( ) e IDispatch.Invoke( ). Vamos discutir um de cada vez.
O método QueryInterface( ) para TEventSink é implementado da seguinte maneira:
function TEventSink.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
// Primeiro procura minha própria implementação de uma interface
// (Eu implemento IUnknown e IDispatch).
if GetInterface(IID, Obj) then
Result := S_OK
// Em seguida, se estiverem procurando uma interface de saída, faça com que retorne
// nosso ponteiro IDispatch.
else if IsEqualIID(IID, IServerWithEventsEvents) then
Result := QueryInterface(IDispatch, Obj)
// Para tudo mais, retorne um erro.
else
Result := E_NOINTERFACE;
end;
No frigir dos ovos, esse método só retorna apenas uma instância quando a interface fornecida é
IUnknown, IDispatch
ou IServerWithEventsEvents.
Aqui está o método Invoke para TEventSink:
function TEventSink.Invoke(DispID: Integer; const IID: TGUID;
LocaleID: Integer; Flags: Word; var Params; VarResult, ExcepInfo,
ArgErr: Pointer): HResult;
659
var
V: OleVariant;
begin
Result := S_OK;
case DispID of
1:
begin
// O primeiro parâmetro é a string nova
V := OleVariant(TDispParams(Params).rgvarg^[0]);
FController.OnServerMemoChanged(V);
end;
2: FController.OnClear;
end;
end;
TEventSink.Invoke( ) é programado para métodos que têm DispID 1 ou DispID 2, que no caso vem a
ser o DispIDs escolhido para OnTextChanged( ) e OnClear( ), respectivamente, na aplicação servidora. OnCle-
ar( ) tem a implementação mais objetiva: simplesmente chama o método OnClear( ) do formulário prin-
cipal do cliente em resposta ao evento. O evento OnTextChanged( ) é um pouco mais complicado: esse códi-
go retira o parâmetro do array Params.rgvarg, que é passado como um parâmetro para esse método, e o
passa para o método OnServerMemoChanged( ) do formulário principal do cliente. Observe que, como o nú-
mero e tipo dos parâmetros são conhecidos, podemos executar esse recurso por meio de simples dedu-
ções a partir do que vemos no código-fonte. Se você for safo, conseguirá implementar Invoke( ) de uma
maneira genérica de modo que ele descubra o número e os tipos de parâmetros e os direcione para a pilha
ou para os registros antes de chamar a função apropriada. Um exemplo dessa situação pode ser encontra-
da no método TOleControl.InvokeEvent( ) na unidade OleCtrls. Esse método representa a lógica de depósito
de eventos para o container de controle ActiveX.
A implementação OnClear( ) e OnServerMemoChanged( ) para manipular o conteúdo da memória do cli-
ente é mostrada a seguir:
procedure TMainForm.OnServerMemoChanged(const NewText: string);
begin
Memo.Text := NewText;
end;
procedure TMainForm.OnClear;
begin
Memo.Clear;
end;
A peça final do quebra-cabeça é conectar o depósito de evento para a interface fonte do servidor.
Isso é facilmente realizado por meio da função InterfaceConnect( ) encontrada na unidade ComObj, que ire-
mos chamar a partir do construtor do formulário principal da seguinte maneira:
InterfaceConnect(FServer, IServerWithEventsEvents, FEventSink, FCookie);
O primeiro parâmetro para essa função é uma referência ao objeto-fonte. O segundo parâmetro é o
IID da interface de saída. O terceiro parâmetro armazena a interface de depósito de evento. O quarto e
último parâmetro é o cookie, que é um parâmetro de referência que será preenchido pelo responsável
pela chamada.
Para justificar a fama de bom cidadão, você também precisa fazer uma limpeza decente, chamando
InterfaceDisconnect( ) quando terminar de brincar com eventos. Isso é feito no destruidor do formulário
principal:
InterfaceDisconnect(FEventSink, IServerWithEventsEvents, FCookie);
660
O ‘demo’
Agora que o cliente e o servidor estão escritos, podemos vê-los em ação. Certifique-se de executar e fe-
char o servidor (ou executá-lo com o parâmetro /regserver) para assegurar que ele seja registrado antes de
tentar executar o cliente. A Figura 23.14 mostra as interações entre o cliente, o servidor, o fonte e o de-
pósito.
Antes de as conexões poderem ser enumeradas, precisamos obter uma referência IConnectionPoint-
Container. Em IConnectionPointContainer, podemos obter o IConnectionPoint que representa a interface de
saída e usando o método IConnectionPoint.EnumConnections( ) podemos obter uma interface IEnumConnecti-
ons que pode ser usada para enumerar as conexões. Toda essa lógica está encapsulada no método mostra-
do a seguir:
function TServerWithEvents.GetConnectionEnumerator: IEnumConnections;
var
Container: IConnectionPointContainer;
CP: IConnectionPoint;
begin
Result := nil;
OleCheck(QueryInterface(IConnectionPointContainer, Container));
OleCheck(Container.FindConnectionPoint(AutoFactory.EventIID, CP));
CP.EnumConnections(Result);
end;
Depois de obter a interface de enumeração, chamar o depósito para cada cliente torna-se uma ques-
tão de percorrer cada conexão. Essa lógica é demonstrada no código a seguir, que aciona o evento OnText-
Changed( ):
661
procedure TServerWithEvents.MemoChange(Sender: TObject);
var
EC: IEnumConnections;
ConnectData: TConnectData;
Fetched: Cardinal;
begin
EC := GetConnectionEnumerator;
if EC < > nil then
begin
while EC.Next(1, ConnectData, @Fetched) = S_OK do
if ConnectData.pUnk < > nil then
(ConnectData.pUnk as IServerWithEventsEvents).OnTextChanged(
➥(Sender as TMemo).Text);
end;
end;
Finalmente, para permitir que os clientes se conectem apenas a uma instância ativa do objeto Auto-
mation, devemos chamar a função RegisterActiveObject( ).API do COM. Essa função aceita como parâ-
metros um IUnknown para o objeto, o CLSID do objeto, um flag indicando se o registro é forte (o servidor
deve ter AddRef) ou fraco (não é um servidor AddRef), e uma alça que é retornada por referência:
RegisterActiveObject(Self as IUnknown, Class_ServerWithEvents,
ACTIVEOBJECT_WEAK, FObjRegHandle);
A Listagem 23.10 mostra todo o código-fonte da unidade ServAuto, que mantém toda essa parafer-
nália junta.
unit ServAuto;
interface
uses
ComObj, ActiveX, AxCtrls, Server_TLB;
type
TServerWithEvents = class(TAutoObject, IConnectionPointContainer,
IServerWithEvents)
private
{ Declarações privadas}
FConnectionPoints: TConnectionPoints;
FObjRegHandle: Integer;
procedure MemoChange(Sender: TObject);
protected
{ Declarações protegidas }
procedure AddText(const NewText: WideString); safecall;
procedure Clear; safecall;
function GetConnectionEnumerator: IEnumConnections;
property ConnectionPoints: TConnectionPoints read FconnectionPoints
implements IConnectionPointContainer;
public
destructor Destroy; override;
procedure Initialize; override;
end;
implementation
uses Windows, ComServ, ServMain, SysUtils, StdCtrls;
destructor TServerWithEvents.Destroy;
662 begin
Listagem 23.10 Continuação
inherited Destroy;
RevokeActiveObject(FObjRegHandle, nil); // Certifique-se de que fui removido do ROT
end;
procedure TServerWithEvents.Initialize;
begin
inherited Initialize;
FConnectionPoints := TConnectionPoints.Create(Self);
if AutoFactory.EventTypeInfo < > nil then
FConnectionPoints.CreateConnectionPoint(AutoFactory.EventIID, ckMulti,
EventConnect);
// Encaminha o evento OnChange do formulário principal para o método MemoChange:
MainForm.Memo.OnChange := MemoChange;
// Registra esse objeto com a ROT (Running Object Table) do COM de modo que outros
// clientes possam se conectar a essa instância.
RegisterActiveObject(Self as IUnknown, Class_ServerWithEvents,
ACTIVEOBJECT_WEAK, FObjRegHandle);
end;
procedure TServerWithEvents.Clear;
var
EC: IEnumConnections;
ConnectData: TConnectData;
Fetched: Cardinal;
begin
MainForm.Memo.Lines.Clear;
EC := GetConnectionEnumerator;
if EC < > nil then
begin
while EC.Next(1, ConnectData, @Fetched) = S_OK do
if ConnectData.pUnk < > nil then
(ConnectData.pUnk as IServerWithEventsEvents).OnClear;
end;
end;
procedure TServerWithEvents.AddText(const NewText: WideString);
begin
MainForm.Memo.Lines.Add(NewText);
end;
procedure TServerWithEvents.MemoChange(Sender: TObject);
var
EC: IEnumConnections;
ConnectData: TConnectData;
Fetched: Cardinal;
begin
EC := GetConnectionEnumerator;
if EC < > nil then
begin
while EC.Next(1, ConnectData, @Fetched) = S_OK do
if ConnectData.pUnk < > nil then
(ConnectData.pUnk as IServerWithEventsEvents).OnTextChanged(
(➥(Sender as TMemo).Text);
end;
end;
function TServerWithEvents.GetConnectionEnumerator: IEnumConnections;
var
663
Listagem 23.10 Continuação
Container: IConnectionPointContainer;
CP: IConnectionPoint;
begin
Result := nil;
OleCheck(QueryInterface(IConnectionPointContainer, Container));
OleCheck(Container.FindConnectionPoint(AutoFactory.EventIID, CP));
CP.EnumConnections(Result);
end;
initialization
TAutoObjectFactory.Create(ComServer, TServerWithEvents,
Class_ServerWithEvents, ciMultiInstance, tmApartment);
end.
No lado do cliente, um pequeno ajuste precisa ser feito para permitir aos clientes se conectarem a
uma instância ativa, caso ela já esteja sendo executada. Use para tal a função GetActiveObject da API COM,
mostrada a seguir:
procedure TMainForm.FormCreate(Sender: TObject);
var
ActiveObj: IUnknown;
begin
// Obtém objeto ativo, caso ele esteja disponível, ou cria um novo, caso ele não esteja
GetActiveObject(Class_ServerWithEvents, nil, ActiveObj);
if ActiveObj < > nil then FServer := ActiveObj as IserverWithEvents
else FServer := CoServerWithEvents.Create;
FEventSink := TEventSink.Create(Self);
InterfaceConnect(FServer, IServerWithEventsEvents, FEventSink, FCookie);
end;
Coleções de Automation
Temos de admitir: nós, programadores, temos verdadeira obsessão por códigos que possam servir como
containers para outros códigos. Pense nisso – seja um array, uma TList, uma TCollection, uma classe con-
664 têiner de modelo para quem é da tribo C++ ou um vetor Java, parece que estamos sempre à procura da
melhor ratoeira para objetos de software que armazenem outros objetos de software. Se você levar em
consideração o tempo investido ao longo dos anos na procura da classe container perfeita, é claro que,
para os programadores, essa é uma questão fundamental. E por que não? Essa separação lógica entre en-
tidades contêiner e contidas nos ajuda a organizar melhor nossos algoritmos, do mesmo modo como
acontece na vida real (um cesto pode conter ovos, um bolso pode conter moedas, um estacionamento
pode conter automóveis etc.). Quando você aprende uma linguagem nova ou um modelo de desenvolvi-
mento, tem que aprender o modo como ele gerencia grupos de entidades. E aqui voltamos para o nosso
ponto de partida: como qualquer outro modelo de desenvolvimento de software, o COM também ge-
rencia esses tipos de grupos de entidades a seu modo e, para sermos programadores eficientes em COM,
precisamos aprender a controlar essas coisas.
Quando trabalhamos com a interface IDispatch, o COM especifica dois métodos principais pelos
quais representamos a noção de container: arrays e coleções. Se você já fez algum trabalho de controle de
Automation ou ActiveX no Delphi, provavelmente já está sabe o que são arrays. Você pode criar facil-
mente arrays de automação no Delphi adicionando uma propriedade de array à interface descendente de
IDispatch ou à dispinterface, como mostra o exemplo a seguir:
type
IMyDisp = interface(IDispatch)
function GetProp(Index: Integer): Integer; safecall;
procedure SetProp(Index, Value: Integer); safecall;
property Prop[Index: Integer]: Integer read GetProp write SetProp;
end;
Os arrays são úteis em muitas circunstâncias, mas têm lá suas limitações. Por exemplo, os arrays fa-
zem sentido quando você tem dados que podem ser acessados de uma forma lógica, por meio de um índi-
ce fixo, como as strings em um IStrings. Entretanto, se a natureza dos dados for daquela em que os itens
individuais são freqüentemente excluídos, adicionados ou movidos, não vale a pena usar um array como
container. O exemplo clássico é um grupo de janelas ativas. Como janelas estão constantemente sendo
criadas, destruídas e tendo a ordem z alterada, não existe um critério sólido para determinar a ordem na
qual as janelas podem aparecer no array.
As coleções podem resolver esse problema, pois permitem que você manipule uma série de elemen-
tos de um modo que não implique qualquer ordem ou número de itens em particular. As coleções são ra-
ras pelo fato de não haver um objeto ou interface coleção no sentido estrito da palavra, porém uma cole-
ção é representada como uma IDispatch personalizada que leva em consideração uma série de regras e di-
retrizes. As regras a seguir devem ser respeitadas para que uma IDispatch se qualifique como uma coleção:
l As coleções devem conter uma propriedade _NewEnum que retorne o IUnknown para um objeto que
aceita a interface IEnumVARIANT, que será usada para enumerar os itens da coleção. Observe que o
nome dessa propriedade deve ser precedido de um sublinhado, e essa propriedade deve ser mar-
cada como restrita na biblioteca de tipo. O DispID da propriedade _NewEnum deve ser DISPID_
NEWENUM (-4) e será definido da seguinte maneira no editor de biblioteca de tipos do Delphi:
l Linguagens como o Visual Basic, que aceitam a construção For Each, usarão esse método para ob-
ter a interface IEnumVARIANT necessária para enumerar os itens da coleção. Voltaremos a falar so-
bre isso daqui a pouco.
l As coleções devem conter um método Item( ) que retorna um elemento da coleção com base no
índice. O DispID desse método deve ser 0, que pode ser marcado com o flag elemento de coleção-
padrão. Se fôssemos implementar uma coleção dos ponteiros de interface IFoo, a definição para
esse método no editor de biblioteca de tipo poderia ter a seguinte aparência:
function Item(Index: Integer): IFoo [propget, dispid $00000000,
defaultcollelem]; safecall;
665
Observe que o parâmetro Index também pode ser aceito como uma OleVariant de modo que um
Integer, WideString ou algum outro tipo de valor possa indexar o item em questão.
l As coleções devem conter uma propriedade Count que retorna o número de itens na coleção. Ge-
ralmente, este método seria definido no editor de biblioteca de tipo da seguinte forma:
function Count: Integer [propget, dispid $00000001]; safecall;
Além das regras acima mencionadas, você deve seguir essas diretrizes durante a criação de seus obje-
tos de coleção:
l A propriedade ou método que retorna uma coleção pode ser nomeada com o plural do nome dos
itens na coleção. Por exemplo, se você tivesse uma propriedade que retornasse uma coleção de
itens listview, o nome de propriedade provavelmente seria Items, enquanto o nome do item na
coleção seria Item. Da mesma maneira, um item chamado Foot seria contido em uma propriedade
de coleção chamada Feet. Nos raros casos em que o plural e singular de uma palavra são iguais
(uma de coleção de peixes ou veados, por exemplo), o nome da propriedade de coleção deve ser
o nome do item seguido da palavra “Collections” (FishCollection ou DeerCollection).
l As coleções que aceitam adição de itens devem fazê-lo usando um método chamado Add( ). Os
parâmetros desse método variam conforme a implementação, mas você pode desejar passar pa-
râmetros que indicam a posição inicial do item novo dentro da coleção. O método Add( ) nor-
malmente retorna uma referência para o item adicionado à coleção.
l As coleções que aceitam exclusão de itens devem fazê-lo usando um método chamado Remove( ).
Esse método deve pegar um parâmetro que identifica o índice do item que está sendo excluído e
o índice deve apresentar o mesmo comportamento semântico que o método Item( ).
unit LVItem;
interface
uses
ComObj, ActiveX, ComCtrls, LVCtrl_TLB, StdVcl, AxCtrls;
type
TListItem = class(TAutoIntfObject, IListItem)
private
FListItem: ComCtrls.TListItem;
protected
function Get_Caption: WideString; safecall;
function Get_Index: Integer; safecall;
function Get_SubItems: IStrings; safecall;
procedure Set_Caption(const Value: WideString); safecall;
procedure Set_SubItems(const Value: IStrings); safecall;
function Get_Checked: WordBool; safecall;
procedure Set_Checked(Value: WordBool); safecall;
public
constructor Create(AOwner: ComCtrls.TListItem);
end;
implementation
uses ComServ;
constructor TListItem.Create(AOwner: ComCtrls.TListItem);
begin
inherited Create(ComServer.TypeLib, IListItem);
FListItem := AOwner;
end;
function TListItem.Get_Caption: WideString;
begin
Result := FListItem.Caption;
end;
function TListItem.Get_Index: Integer;
begin
Result := FListItem.Index;
end;
function TListItem.Get_SubItems: IStrings;
begin 667
Listagem 23.11 Continuação
GetOleStrings(FListItem.SubItems, Result);
end;
procedure TListItem.Set_Caption(const Value: WideString);
begin
FListItem.Caption := Value;
end;
procedure TListItem.Set_SubItems(const Value: IStrings);
begin
SetOleStrings(FListItem.SubItems, Value);
end;
function TListItem.Get_Checked: WordBool;
begin
Result := FListItem.Checked;
end;
procedure TListItem.Set_Checked(Value: WordBool);
begin
FListItem.Checked := Value;
end;
end.
Observe que ComCtrls.TListItem( ) está sendo passado no construtor para servir como o item listview
a ser manipulado por esse objeto Automation.
A implementação para o objeto de coleção ListItems é apenas um pouco mais complexa. Primeiro,
porque o objeto deve ser capaz de fornecer um objeto aceitando IEnumVARIANT para implementar a proprie-
dade _NewEnum, IEnumVARIANT é aceita diretamente nesse objeto. Portanto, a classe TListItems aceita tanto
IListItems quanto IEnumVARIANT. IEnumVARIANT contém quatro métodos, que são descritos na Tabela 23.1.
Método Objetivo
unit LVItems;
interface
uses
ComObj, Windows, ActiveX, ComCtrls, LVCtrl_TLB;
type
TListItems = class(TAutoIntfObject, IListItems, IEnumVARIANT)
private
FListItems: ComCtrls.TListItems;
668 FEnumPos: Integer;
Listagem 23.12 Continuação
protected
{ Métodos IListItems }
function Add: IListItem; safecall;
function Get_Count: Integer; safecall;
function Get_Item(Index: Integer): IListItem; safecall;
procedure Remove(Index: Integer); safecall;
function Get__NewEnum: IUnknown; safecall;
{ Métodos IEnumVariant }
function Next(celt: Longint; out elt; pceltFetched: PLongint): HResult;
stdcall;
function Skip(celt: Longint): HResult; stdcall;
function Reset: HResult; stdcall;
function Clone(out Enum: IEnumVariant): HResult; stdcall;
public
constructor Create(AOwner: ComCtrls.TListItems);
end;
implementation
uses ComServ, LVItem;
{ TListItems }
constructor TListItems.Create(AOwner: ComCtrls.TListItems);
begin
inherited Create(ComServer.TypeLib, IListItems);
FListItems := AOwner;
end;
{ TListItems.IListItems }
function TListItems.Add: IListItem;
begin
Result := LVItem.TListItem.Create(FListItems.Add);
end;
function TListItems.Get__NewEnum: IUnknown;
begin
Result := Self;
end;
function TListItems.Get_Count: Integer;
begin
Result := FListItems.Count;
end;
function TListItems.Get_Item(Index: Integer): IListItem;
begin
Result := LVItem.TListItem.Create(FListItems[Index]);
end;
procedure TListItems.Remove(Index: Integer);
begin
FListItems.Delete(Index);
end;
{ TListItems.IEnumVariant }
function TListItems.Clone(out Enum: IEnumVariant): HResult;
begin
Enum := nil;
Result := S_OK;
try
Enum := TListItems.Create(FListItems);
except 669
Listagem 23.12 Continuação
Result := E_OUTOFMEMORY;
end;
end;
function TListItems.Next(celt: Integer; out elt; pceltFetched: PLongint):
HResult;
var
V: OleVariant;
I: Integer;
begin
Result := S_FALSE;
try
if pceltFetched < > nil then pceltFetched^ := 0;
for I := 0 to celt - 1 do
begin
if FEnumPos >= FListItems.Count then Exit;
V := Get_Item(FEnumPos);
TVariantArgList(elt)[I] := TVariantArg(V);
// truque pra impedir que a variante tenha o seu conteúdo excluído,
// já que precisa permanecer ativa pelo fato de pertencer ao array elt
TVarData(V).VType := varEmpty;
TVarData(V).VInteger := 0;
Inc(FEnumPos);
if pceltFetched < > nil then Inc(pceltFetched^);
end;
except
end;
if (pceltFetched = nil) or ((pceltFetched < > nil) and
(pceltFetched^ = celt)) then
Result := S_OK;
end;
function TListItems.Reset: HResult;
begin
FEnumPos := 0;
Result := S_OK;
end;
function TListItems.Skip(celt: Integer): HResult;
begin
Inc(FEnumPos, celt);
Result := S_OK;
end;
end.
O único método nessa unidade cuja implementação não é trivial é o método Next( ). O parâmetro
celt do método Next( ) indica agora quantos itens poderiam ser recuperados. O parâmetro elt contém
um array de TVarArgs com pelo menos elt elementos. No retorno, pceltFetched (se não for nil) pode con-
ter o verdadeiro número de itens extraídos. Esse método retorna S_OK quando o número de itens retor-
nado é igual ao número solicitado; caso contrário, ele retorna S_FALSE. A lógica desse método percorre
o array no elt e atribui uma TVarArg representando um item de coleção a um elemento do array. Obser-
ve o pequeno truque que executamos para limpar a OleVariant depois de atribuí-la ao array. Isso assegu-
ra que o array não vai ter o seu conteúdo excluído. Se não tomássemos essa precaução, possivelmente o
conteúdo de elt poderia ser danificado se os objetos referidos por V forem liberados quando a OleVari-
670 ant for finalizada.
Do mesmo modo que acontece com TListItem, o construtor de TListItems pega ComCtrls.TListItems
como um parâmetro e manipula esse objeto na implementação desses métodos.
Finalmente, completamos a implementação do controle ActiveX adicionando a lógica para gerenciar
a propriedade Items. Primeiro, devemos adicionar um campo para que o objeto armazene a coleção:
type
TListViewX = class(TActiveXControl, IListViewX)
private
...
FItems: IListItems;
...
end;
Finalmente, o método Get_Items( ) pode ser implementado de modo a retornar apenas FItems:
function TListViewX.Get_Items: IListItems;
begin
Result := FItems;
end;
O teste real para ver se essa coleção funciona é carregar o controle no Visual Basic 6 e tentar usar o cons-
trutor For Each com a coleção. A Figura 23.16 mostra uma aplicação Visual Basic testando nossa coleção.
FIGURA 23.16 Uma aplicação Visual Basic para testar nossa coleção.
Dos dois botões de comando que você vê na Figura 23.16, o Command1 adiciona itens à listview, en-
quanto o Command2 percorre todos os itens na listview usando For Each e adiciona pontos de exclamação a
cada legenda. O código desses métodos é mostrado aqui:
Private Sub Command1_Click( )
ListViewX1.Items.Add.Caption = “Delphi”
End Sub
Apesar do preconceito que alguns fiéis seguidores do Delphi têm em relação ao VB, devemos lem-
brar que o VB é o principal consumidor de controles ActiveX e que é muito importante assegurar que
controles funcionem a contento nesse ambiente.
As coleções fornecem poderosa funcionalidade que pode permitir que seus controles e servidores
Automation funcionem de um modo mais harmonioso no mundo do COM. Como é extremamente difí- 671
cil implementar coleções, vale a pena adquirir o hábito de usá-las quando apropriado. Infelizmente, é
bastante possível que, mal você comece a se sentir à vontade com as coleções, alguém surja no pedaço
com um objeto container ainda mais novo e melhor para o COM.
Você também deve ter consciência do fato de que a interface para a qual difundiu o IUnknown deve ser
uma interface que o COM saiba como conduzir. Isso significa que ela deve ser definida em uma bibliote-
ca de tipos em algum lugar, deve ser um tipo compatível com o condutor Automation padrão ou o servi-
dor COM em questão deve fornecer uma DDL proxy/estrutura capaz de conduzir a interface.
unit ServObj;
interface
uses
ComObj, ActiveX, Server_TLB;
tipo
TBinaryData = class(TAutoObject, IBinaryData)
protected
function Get_Data: OleVariant; safecall;
procedure Set_Data(Value: OleVariant); safecall;
end;
implementation
initialization
TAutoObjectFactory.Create(ComServer, TBinaryData, Class_BinaryData,
ciSingleInstance, tmApartment);
end.
673
Listagem 23.14 A unidade do cliente
unit CliMain;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, ExtCtrls, Server_TLB;
tipo
TMainForm = class(TForm)
Memo: TMemo;
Panel1: TPanel;
SetButton: TButton;
GetButton: TButton;
OpenButton: TButton;
OpenDialog: TOpenDialog;
procedure OpenButtonClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure SetButtonClick(Sender: TObject);
procedure GetButtonClick(Sender: TObject);
private
FServer: IBinaryData;
end;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
procedure TMainForm.FormCreate(Sender: TObject);
begin
FServer := CoBinaryData.Create;
end;
procedure TMainForm.OpenButtonClick(Sender: TObject);
begin
if OpenDialog.Execute then
Memo.Lines.LoadFromFile(OpenDialog.FileName);
end;
procedure TMainForm.SetButtonClick(Sender: TObject);
var
P: Pointer;
L: Integer;
V: OleVariant;
begin
// Envia dados da memória para servidor
L := Length(Memo.Text);
V := VarArrayCreate([0, L - 1], varByte);
P := VarArrayLock(V);
try
Move(Memo.Text[1], P^, L);
finally
VarArrayUnlock(V);
end;
FServer.Data := V;
end;
procedure TMainForm.GetButtonClick(Sender: TObject);
var
P: Pointer;
674
Listagem 23.14 Continuação
L: Integer;
S: string;
V: OleVariant;
begin
// Obtém os dados da memória do servidor
V := FServer.Data;
L := VarArrayHighBound(V, 1) - VarArrayLowBound(V, 1) + 1;
SetLength(S, L);
P := VarArrayLock(V);
try
Move(P^, S[1], L);
finally
VarArrayUnlock(V);
end;
Memo.Text := S;
end;
end.
Variantes
As variantes são as mais antigas formas de suporte COM no Delphi, que remontam ao velho Delphi 2.
Como você provavelmente já sabe, uma Variant não passa de um grande registro que é usado para passar
alguns dados que podem ser de uma série de tipos. Se você estiver interessado na aparência desse regis-
tro, ele é definido na unidade System como TVarData:
type
PVarData = ^TVarData;
TVarData = record 675
VType: Word;
Reserved1, Reserved2, Reserved3: Word;
case Integer of
varSmallint: (VSmallint: Smallint);
varInteger: (VInteger: Integer);
varSingle: (VSingle: Single);
varDouble: (VDouble: Double);
varCurrency: (VCurrency: Currency);
varDate: (VDate: Double);
varOleStr: (VOleStr: PWideChar);
varDispatch: (VDispatch: Pointer);
varError: (VError: LongWord);
varBoolean: (VBoolean: WordBool);
varUnknown: (VUnknown: Pointer);
varByte: (VByte: Byte);
varString: (VString: Pointer);
varAny: (VAny: Pointer);
varArray: (VArray: PVarArray);
varByRef: (VPointer: Pointer);
end;
O valor do campo VType desse registro indica o tipo de dado contido na Variant e ele pode ser qual-
quer um dos códigos de tipo encontrados na parte superior da unidade System e listados na porção de va-
riant desse registro (dentro da instrução case). A única diferença entre Variant e OleVariant é que Variant
aceita todos os tipos de código, enquanto OleVariant só aceita os tipos compatíveis com o Automation.
Por exemplo, uma tentativa para atribuir uma string (varString) do Pascal a uma Variant é uma prática
aceitável, mas atribuir a mesma string a uma OleVariant fará com que ela seja convertida em uma Wide-
String (varOleStr) compatível com o Automation.
Quando você trabalha com os tipos Variant e OleVariant, na verdade o que o compilador está mani-
pulando e passando são instâncias do registro TVarData. De fato, você pode fazer um typecast seguro de
uma Variant ou OleVariant para uma TVarData se por alguma razão precisar manipular o conteúdo do regis-
tro (embora só recomendemos essa prática para quem realmente saiba o que está fazendo).
No inóspito mundo da programação de COM em C e C++ (sem uma estrutura de classe), as vari-
antes são representadas com a estrutura VARIANT definida em oaidl.h. Ao trabalhar com variantes nesse
ambiente, você tem que inicializá-las manualmente e gerenciá-las usando as funções VariantXXX( ) API en-
contradas em oleaut32.dll, como VariantInit( ), VariantCopy( ), VariantClear( ) etc. Isso torna o trabalho
com variantes diretamente no ambiente do C e do C++ uma tarefa que implica muita manutenção.
Com suporte para variantes dentro do Object Pascal, o compilador gera as chamadas necessárias
para as rotinas de suporte a variante da API automaticamente quando você usa instâncias dos tipos Vari-
ant e OleVariant. Esse grau de precisão na linguagem obriga-o a ampliar o seu leque de conhecimentos, no
entanto. Se você inspecionar a tabela de importação de um EXE “sem função” do Delphi usando uma
ferramenta como TDUMP.EXE da Borland ou DUMPBIN.EXE da Microsoft, verá algumas importações um tanto
suspeitas de oleaut32.dll: VariantChangeTipoEx( ), VariantCopyInd( ) e VariantClear( ). Isso significa que mes-
mo em uma aplicação na qual você não empregue explicitamente os tipos Variant ou OleVariant, o EXE do
Delphi ainda depende dessas funções API do COM em oleaut32.dll.
Arrays de variante
Os arrays de variantes no Delphi se destinam a encapsular safearrays COM, que são um tipo de registro
usado para encapsular um array de dados no Automation. Eles são chamados seguros porque são auto-
descritivos; além dos dados do array, o registro contém informações sobre o número de dimensões, o ta-
manho de um elemento e o número de elementos no array. Os arrays de variantes são criados e gerencia-
dos no Delphi usando as funções e procedimentos VarArrayXXX( ) encontrados na unidade System e são do-
676 cumentados na ajuda on-line. Na verdade, essas funções e procedimentos são wrappers em volta das fun-
ções SafeArrayXXX( ) API. Como uma Variant contém um array de variantes, a sintaxe-padrão do array é
usada para acessar elementos do array. Novamente, comparando isso com safearrays codificadas manu-
almente que você faria no C e C++, a encapsulação de linguagem do Object Pascal é limpa e muito me-
nos pesada e propensa a erros.
WideString
O tipo de dado WideString foi adicionado no Delphi 3 para servir a um duplo propósito de fornecer um
byte duplo nativo, uma string de caracteres Unicode e uma string de caracteres compatível com a string
BSTR do COM. O tipo WideString difere de seu primo AnsiString em alguns aspectos-chave:
Interfaces
Talvez o mais importante recurso COM na linguagem do Object Pascal seja o suporte nativo a interfaces.
Um tanto ironicamente, embora recursos notadamente menores, como Variants e WideStrings, recorram a
funções da API do COM para serem implementadas, a implementação de interfaces do Object Pascal não
fornece nenhum tipo de COM. Ou seja, o Object Pascal fornece uma implementação de interfaces total-
mente independente que adere à especificação COM, que, no entanto, não precisa de nenhuma das fun-
ções API do COM.
Como uma parte da aderência às especificações do COM, todas as interfaces no Delphi descendem
implicitamente de IUnknown. Como você deve saber, IUnknown fornece a identidade e o suporte a contagem
de referência fundamentais para o COM. Isso significa que o conhecimento de IUnknown é construído no
compilador e que IUnknown é definido na unidade System. Tornando IUnknown um cidadão de primeira classe
na linguagem, o Delphi é capaz de fornecer contagem de referência automática ao fazer o compilador
gerar as chamadas para IUnknown.AddRef( ) e IUnknown.Release( ) nos momentos apropriados. Além disso, o
operador as pode ser usado como um atalho para identidade de interface normalmente obtida via
QueryInterface( ). O suporte a IUnknown, entretanto, é quase incidental quando você considera o suporte
de baixo nível que a linguagem e o compilador fornecem para interfaces em geral.
A Figura 23.17 mostra um diagrama simplificado de como as classes aceitam interfaces internamen-
te. Na verdade, um objeto Delphi é uma referência que aponta para a instância física. Os primeiros qua-
tro bytes de uma instância de objeto são um ponteiro para a tabela VMT (Virtual Method Table). Um
offset positivo da VMT contém todos os métodos virtuais do objeto. Um offset negativo contém os pon-
teiros para métodos e dados que são importantes para a função interna do objeto. Em particular, o offset
–72 da VMT contém um ponteiro para a tabela de interface do objeto. A tabela de interface é uma lista de
registros PInterfaceEntry (definidos na unidade System ), que essencialmente contém o IID e a informação
sobre onde encontrar o ponteiro vtable desse IID.
Virtual
Method Tabela de
Table interface
GUID VTable
–72 Tabela Intf
GUID VTable
Métodos GUID VTable
e dados
Instância internos GUID VTable
do objeto Apanha
0 GUID VTable VTable do VTable
VMT
método
Objeto ou
Dados Métodos Via
de implements campo
virtuais
instância
Depois de parar um pouco para pensar sobre o diagrama da Figura 23.17 e entender como as coisas
são integradas, veremos que os detalhes inerentes à implementação das interfaces não passam de um que-
bra-cabeças. Por exemplo, QueryInterface( ) é normalmente implementado nos objetos do Object Pascal
chamando TObject.GetInterface( ). GetInterface( ) percorre a tabela de interface à procura do IID em
questão e retorna o ponteiro de vtable dessa interface. Isso também ilustra por que tipos de interface no-
678 vos devem ser definidos com um GUID; de outra maneira, GetInterface( ) não poderia percorrer a tabela
de interface e, portanto, não haveria sido identificado via QueryInterface( ). Como o typecast de interfa-
ces usando o operador as simplesmente gera uma chamada para QueryInterface( ), as mesmas regras se
aplicam lá.
A última entrada na tabela de interface na Figura 23.17 ilustra como uma interface é implementada
internamente usando a diretiva implements. Em vez de fornecer um ponteiro direto para a vtable, a entra-
da de tabela de interface fornece o endereço de uma pequena função captadora gerada pelo compilador
que obtém a vtable da interface da propriedade na qual a diretiva implements foi usada.
Dispinterfaces
Uma dispinterface fornece uma encapsulação de uma IDispatch não-dual. Ou seja, um IDispatch no qual os
métodos podem ser chamados via Invoke( ) mas não via vtable. Nesse aspecto, uma dispinterface é seme-
lhante a Automation com variantes. Entretanto, dispinterfaces são ligeiramente mais eficientes do que
variantes, pois as declarações de dispinterface contêm o DispID de cada uma das propriedades ou méto-
dos aceitos. Isso significa que IDispatch.Invoke( ) pode ser chamado diretamente sem chamar primeiro
IDispatch.GetIDsOfNames( ), como deve ser feito com uma variante. O mecanismo por trás das dispinterfa-
ces é semelhante ao das variantes: quando você chama um método via uma dispinterface, o compilador
gera uma chamada para _IntfDispCall na unidade System. Esse método salta através do ponteiro Disp-
CallByIDProc, que como padrão só retorna um erro. Entretanto, quando a unidade ComObj é incluída, Disp-
CallByIDProc é direcionado para o procedimento DispCallByID( ), que é declarado em ComObj da seguinte
maneira:
procedure DispCallByID(Result: Pointer; const Dispatch: IDispatch;
DispDesc: PDispDesc; Params: Pointer); cdecl;
O que é MTS?
MTS é um modelo de programação em COM e ActiveX e uma coleção de serviços runtime para de-
senvolvimento de aplicações COM e ActiveX escaláveis ou transacionais. Parte do modelo de programa-
ção do MTS não é muito diferente do que você, como um programador em COM, já está acostumado.
Há alguns macetes que daqui a pouco você vai dominar, mas em geral qualquer objeto COM em proces-
so (DLL) com uma biblioteca de tipos pode ser um objeto MTS. No entanto, não é recomendado que
você execute componentes COM não-cientes do MTS dentro do MTS. Os serviços de runtime do MTS
significam que o MTS assume a responsabilidade pelos seus componentes COM. O MTS pode hospe-
dá-los, fazer o gerenciamento permanente deles, fornecer-lhes segurança etc. Isso significa que, em vez
de serem executados dentro do contexto de sua aplicação, os objetos COM do MTS são executados den-
tro do contexto do runtime do MTS. Tudo isso introduz uma série de novos recursos dos quais você
pode tirar partido com pouca ou mesmo nenhuma mudança no código do objeto COM ou do cliente.
É interessante observar que, como os objetos MTS não são executados diretamente dentro do con-
texto de um cliente como outros objetos COM, os clientes nunca obtêm realmente ponteiros de interface
diretamente para uma instância de objeto. Em vez disso, o MTS insere um proxy entre o cliente e o obje-
to MTS, já que esse proxy é idêntico para o objeto do ponto de vista do cliente. Entretanto, como o MTS
tem completo controle sobre o proxy, ele pode controlar o acesso para métodos de interface do objeto
para objetivos como gerenciamento permanente e segurança, como você aprenderá logo.
Como pode imaginar, você poderia usar essa interface mais ou menos assim:
var
CB: ICheckbook;
begin
CB := SomehowGetInstance;
CB.SetAccount(‘12345ABCDE’); // consulta a conta
CB.AddActivity(-100); // faz uma retirada de $100
...
end;
O problema com esse estilo é que o objeto não é sem estado entre as chamadas de método, pois as
informações de estado referentes ao número de conta devem ser mantidas ao longo de toda a chamada.
Uma abordagem melhor a ser usada no MTS seria passar todas as informações necessárias para o método
AddActivity( ) de modo que o objeto pudesse se comportar como se estivesse sem estado, como se pode
ver no exemplo a seguir:
procedure AddActivity(AccountNum: WideString; Amount: Integer); safecall;
O estado particular de um objeto ativo também é chamado de contexto. O MTS mantém um contexto
para cada objeto ativo que monitora detalhes como informações de transação e segurança para o objeto. A
qualquer momento, um objeto pode chamar GetObjectContext( ) para obter um ponteiro de interface
IObjectContext para o contexto do objeto. IObjectContext é definido na unidade Mtx da seguinte maneira:
IObjectContext = interface(IUnknown)
[‘{51372AE0-CAE7-11CF-BE81-00AA00A2FA25}’]
function CreateOcorrência(const cid, rid: TGUID; out pv): HResult; stdcall;
procedure SetComplete; safecall;
procedure SetAbort; safecall;
procedure EnableCommit; safecall;
procedure DisableCommit; safecall;
function IsInTransaction: Bool; stdcall;
function IsSecurityEnabled: Bool; stdcall;
function IsCallerInRole(const bstrRole: WideString): Bool; safecall;
end;
Os dois métodos mais importantes nessa interface são SetComplete( ) e SetAbort( ). Se um desses mé-
todos for chamado, o objeto está informando ao MTS que não tem mais nenhum estado para manter. O
MTS vai portanto destruir o objeto (sem o cliente saber, é claro), liberando assim recursos para outras
instâncias. Se o objeto estiver participando em uma transação, SetComplete( ) e SetAbort( ) também têm o
efeito de um commit e redução de preço para a transação, respectivamente. 681
Gerenciamento permanente
Na época em que estávamos engatinhando na programação em COM, ensinaram-nos que só devíamos
armazenar ponteiros de interface em última instância e mesmo assim se fosse para liberá-los tão logo eles
se tornassem desnecessárias. No COM tradicional, isso faz sentido porque não desejamos ocupar o siste-
ma mantendo recursos que não estejam sendo usados. Entretanto, como o MTS libera automaticamente
os objetos sem estado depois de eles chamarem SetComplete( ) ou SetAbort( ), não há nenhuma conse-
qüência associada ao armazenamento de uma referência para um desses objetos indefinidamente. Além
disso, como o cliente nunca sabe que a instância de objeto pode ter sido excluída por baixo dos panos, os
clientes não precisam ser reescritos para tirar proveito desse recurso.
Pacotes
A palavra pacote está mais do que saturada – pacotes Delphi, pacotes C++Builder e pacotes Oracle são
apenas alguns exemplos do excesso de uso dessa palavra. O MTS também tem um conceito de pacotes
que sem dúvida é diferente dessas outras acepções da palavra. Um pacote MTS é mais lógico do que físi-
co, pois representa uma coleção de objetos definidos pelo programador de objetos MTS como atributos
de transação, ativação e segurança. A parte física de um pacote é um arquivo que contém referências às
DLLs do servidor COM e objetos MTS dentro dos servidores que compõem um pacote. O arquivo de pa-
cote também contém informações sobre os atributos dentro dos objetos MTS.
O MTS executará todos os componentes dentro de um pacote no mesmo processo. Isso permite a
você configurar seu pacote de modo que eles sejam isolados dos possíveis problemas que poderiam ser
causados por falhas ou erros em outro pacote. Também é interessante observar que a localização física
dos componentes não é um critério para que um pacote seja incluído: um único servidor COM pode con-
ter vários objetos COM, cada um em um pacote separado.
Os pacotes são criados e manipulados usando o menu Run, Install MTS Objects do Delphi ou o
Transaction Server Explorer, que é instalado com o MTS e mostrado na Figura 23.18.
Segurança
O MTS fornece um sistema de segurança baseado em hierarquia que é muito mais flexível do que a segu-
rança do Windows NT padrão normalmente usada com o DCOM. Uma hierarquia é uma categoria de
usuário (em um sistema bancário, por exemplo, uma hierarquia típica seria caixa, supervisor e gerente).
O MTS permite a você especificar o grau no qual qualquer hierarquia pode manipular um objeto em
cada interface. Por exemplo, você pode especificar que a hierarquia tem acesso à interface ICreateHomeLo-
an, ao contrário do caixa. Se você precisa obter mais granularidade do que acesso a interfaces inteiras,
pode determinar a hierarquia do usuário no contexto atual chamando o método IsCallerInRole( ) de
IObjectContext. Usando isso, por exemplo, você poderia impor uma regra comercial que estipule que os
caixas podem aprovar fechamento de uma conta normal, mas somente os supervisores podem aprovar o
fechamento de conta quando o saldo dessa última é superior a $100.000. A hierarquia da segurança pode
682 ser configurada no Transaction Server Explorer.
Oh, ele também faz transações
Como o nome sugere, o MTS também faz transações. Você deve estar pensando consigo mesmo: “Grande
idéia, meu servidor de banco de dados já aceita transações. Por que diacho vou que querer que meus com-
ponentes também as suportem?” Essa é uma pergunta bastante razoável, para a qual existe uma resposta
igualmente boa. O suporte a transação no MTS pode permitir a você executar transações em múltiplos
bancos de dados ou fazer com que uma ação atômica de alguns conjuntos de operações não tenham nada a
ver com os bancos de dados. Para dar suporte a transações nos seus objetos MTS, você deve definir o flag
de transação correto na co-classe do objeto na biblioteca de tipos durante o desenvolvimento (tarefa essa
que fica a cargo do Delphi MTS Wizard) ou depois da distribuição, agora no Transaction Server Explorer.
Quando você deve usar transações em seus objetos? Essa é fácil: você deve usar transações quando
tem um processo envolvendo várias etapas que deseja executar em apenas uma transação atômica. Dessa
forma, todo o processo pode ser submetido ou cancelado, mas você jamais vai deixar sua lógica ou dado
em um estado incorreto ou indeterminado em algum lugar intermediário. Por exemplo, se você estiver
escrevendo um software para um banco e deseja manipular a condição em que um cliente passa um che-
que sem fundos, provavelmente haveria várias etapas envolvidas na manipulação disso, como registrar a
quantia do cheque, registrar a tarifa de devolução de cheque e enviar uma carta para o cliente.
Para que o cheque sem fundos seja devidamente processado, cada uma dessas coisas deve acontecer.
Portanto, o envolvimento de todas elas em apenas uma transação seria uma forma de garantir que tudo
isso vai acontecer (se nenhum erro for encontrado) ou, caso ocorra um erro, tudo voltaria para o estado
anterior à transação.
Recursos
Com objetos sendo criados e destruídos o tempo todo e transações acontecendo em todos os lugares, é
importante para o MTS fornecer um meio para compartilhar recursos seguros finitos ou dispendiosos
(como conexões de banco de dados) através de múltiplos objetos. O MTS faz isso usando gerenciadores
de recursos e processadores de recursos. Um gerenciador de recursos é um serviço que gerencia algum
tipo de dado durável, como uma conta bancária ou um estoque. A Microsoft fornece um processador de
recursos no MS SQL Server. Um processador de recursos gerencia recursos não-duráveis, como conexões
de banco de dados. A Microsoft fornece um processador de recursos para conexões de banco de dados
ODBC, e a Borland fornece um processador de recursos para conexões de banco de dados BDE.
Quando uma transação faz uso de um mesmo tipo de recurso, ela convoca o recurso para se tornar
parte da transação de modo que toda alteração feita no recurso durante a transação seja inserida na ope-
ração commit ou rollback da transação.
MTS no Delphi
Agora que você tem o “o quê” e o “por quê”, chegou a hora de falarmos sobre o “como”. Em particular,
vamos nos concentrar sobre o suporte de MTS do Delphi e como construir soluções MTS no Delphi.
Antes de mergulharmos de cabeça, no entanto, você deve primeiro saber que só existe o suporte MTS na
versão Enterprise do Delphi. Embora tecnicamente seja possível criar componentes MTS usando as faci-
lidades disponíveis nas versões Standard e Professional, você teria muito mais coisas produtivas a fazer
com o tempo. Portanto, esta seção vai ajudar você a aproveitar os recursos do Delphi Enterprise.
Assistentes do MTS
O Delphi fornece dois assistentes para construir componentes MTS, ambos igualmente encontrados na
guia Multitier da caixa de diálogo New Items: o MTS Remote Data Module Wizard e o MTS Object Wi-
zard. O MTS Remote Data Module Wizard permite a você construir servidores MIDAS que operam no
ambiente MTS. O MTS Object Wizard servirá como o ponto de partida para seus objetos MTS e será o
foco dessa discussão. Chamando esse assistente, você é apresentado à caixa de diálogo mostrada na Figu-
ra 23.19.
683
FIGURA 23.19 O Object Wizard MTS novo.
A caixa de diálogo na Figura 23.19 é semelhante ao Automation Object Wizard discutido anterior-
mente neste capítulo. A diferença óbvia é a facilidade fornecida por esse assistente para selecionar o mode-
lo de transação aceito pelo seu componente MTS. Os modelos de transação disponíveis são os seguintes:
l Requer uma transação. O componente sempre será criado dentro do contexto de uma transação.
Herdará a transação de seu criador, caso exista; caso contrário, criará uma nova.
l Requer uma transação nova. Uma transação nova sempre será criada para que o componente seja
executado dentro.
l Tem suporte para transações. O componente herdará a transação do seu criador, caso exista;
caso contrário, ele será executado sem uma transação.
l Não tem suporte para transações. O componente nunca será criado dentro de uma transação.
A informação de modelo de transação é armazenada como um atributo juntamente com a co-classe
do componente na biblioteca de tipos.
Depois de você dar um clique em OK para fechar a caixa de diálogo, o assistente gerará uma defini-
ção vazia para uma classe que descende de TMtsAutoObject e o deixará fora do Type Library Editor para de-
finir os componentes MTS adicionando propriedades, métodos, interfaces etc. Isso deve ser um ter-
ritório familiar porque o stream de trabalho é idêntico ao desenvolvimento de objetos Automation no
Delphi. É interessante observar que, embora os objetos MTS criados pelo assistente do Delphi sejam ob-
jetos Automation (ou seja, objetos COM que implementam IDispatch), o MTS tecnicamente não exige
isso. Entretanto, como o COM inerentemente sabe como conduzir interfaces IDispatch acompanhadas
por bibliotecas de tipo, empregar esse tipo de objeto no MTS permite a você se concentrar mais na fun-
cionalidade dos seus componentes e menos em como eles integram com o MTS. Você também deve estar
ciente de que os componentes MTS devem residir nos servidores COM em processo (DLLs); os compo-
nentes MTS não são aceitos nos servidores fora de processo (EXEs).
Estrutura do MTS
A classe TMtsAutoObject acima mencionada, que é a classe básica de todos os objetos MTS criados pelo as-
sistente do Delphi, é definida na unidade MtsObj. TMtsAutoObject é uma classe relativamente simples, que é
definida da seguinte forma:
type
TMtsAutoObject = class(TAutoObject, IObjectControl)
private
FObjectContext: IObjectContext;
protected
{ IObjectControl }
procedure Activate; safecall;
procedure Deactivate; stdcall;
function CanBePooled: Bool; stdcall;
procedure OnActivate; virtual;
procedure OnDeactivate; virtual;
684
property ObjectContext: IObjectContext read FObjectContext;
public
procedure SetComplete;
procedure SetAbort;
procedure EnableCommit;
procedure DisableCommit;
function IsInTransaction: Bool;
function IsSecurityEnabled: Bool;
function IsCallerInRole(const Role: WideString): Bool;
end;
CreateInstance Cria uma instância de outro objeto MTS. Você pode pensar nesse método para
executar a mesma tarefa para objetos MTS que IClassFactory.CreateInstance
executam com ojetos COM normais.
SetComplete Avisa ao MTS que o componente completou qualquer que seja o trabalho que
precisa fazer e não tem mais nenhum estado interno para manter. Se o
componente for transacional, também indica que as transações atuais podem
ser alocadas. Depois que a chamada dessa função retorna, o MTS pode
desativar o objeto, liberando os recursos de modo a ampliar a escalabilidade.
SetAbort Semelhante a SetComplete( ), este método sinaliza para o MTS que o
componente completou o trabalho e que não tem mais informação de estado
para manter. Entretanto, chamar esse método também significa que o
componente está em um estado de erro ou indeterminado e que qualquer
transação pendente deve ser abortada.
685
Nome do método Descrição
EnableCommit Indica que o componente está em um estado “alocável”, já que essas transações
podem ser alocadas quando o componente chamar SetComplete. Esse é o
estado-padrão de um componente.
DisableCommit Indica que o componente está em um estado inconsistente e novas chamadas
de método são necessárias antes de o componente ser preparado para alocar
transações.
IsInTransaction Permite que um componente determine se está sendo executado dentro do
contexto de uma transação.
IsSecurityEnabled Permite que um componente determine se a segurança do MTS é ativa. Esse
método sempre retorna True a não ser que o componente esteja sendo
executado no espaço de processo do cliente.
IsCallerInRole Fornece um meio pelo qual um componente pode determinar se o usuário
servindo como cliente para o componente é um membro de uma hierarquia
específica do MTS. Esse método é fundamental para o sistema de segurança do
MTS, baseado em hierarquia e fácil de usar. (Voltamos a falar sobre hierarquia
ainda neste capítulo.)
A unidade Mtx contém os principais elementos do suporte a MTS. É a tradução do Pascal do arquivo
de cabeçalho mtx.h e contém os tipos (como IObjectControl e IObjectContext) e funções que constituem a
API do MTS.
A lógica por trás dos três métodos dessa interface é simples, e esses métodos constituem os requisi-
686 tos para dar suporte a um jogo entre um humano e um tic-tac-toe computadorizado. NewGame( ) inicializa
um jogo novo para o cliente. ComputerMove( ) analisa os movimentos disponíveis e faz um movimento para
o computador. PlayerMove( ) permite que o cliente deixe o computador saber como ele escolheu o movi-
mento. Já dissemos que o desenvolvimento de componente MTS exige uma nova abordagem de desen-
volvimento de componentes, diferente da que dá origem ao COM padrão. Esse componente oferece
uma boa oportunidade para ilustrar esse fato.
Se esse fosse um componente COM com o qual você trabalhasse normalmente, devia abordar o
projeto do objeto inicializando algumas estruturas de dado para manter o estado do jogo no método New-
Game( ). A estrutura de dados provavelmente seria um campo de instância do objeto, que os outros méto-
dos acessariam e manipulariam ao longo de toda a vida do objeto.
Qual é o problema com essa aproximação para um componente MTS? Uma palavra: estado. Como
você aprendeu antes, os objetos sem estado devem tirar total proveito do MTS. No entanto, uma arquite-
tura de componente que depende dos dados da instância para ser mantida ao longo da chamada do méto-
do está longe de ser sem estado. Um projeto melhor para o MTS seria retornar uma “alça” identificando
um jogo do método NewGame( ) e usando essa alça para manter estruturas de dados de jogo para jogo em
algum tipo de facilidade de recurso compartilhado. Esse facilidade de recurso compartilhada precisaria
ser mantida fora do contexto de uma instância de objeto específica, pois o MTS pode ativar e desativar
instâncias de objeto com cada chamada de método. Cada um dos outros métodos do componente pode-
ria aceitar essa alça como um parâmetro, permitindo que ele recuperasse os dados de jogo da facilidade
de recurso compartilhado. Esse é um projeto sem estado porque não exige que o objeto permaneça ativa-
do entre chamadas de método, pois cada método é uma operação independente que obtém todos os da-
dos de que precisa dos parâmetros e de uma facilidade de dados compartilhados.
Essa facilidade de dados compartilhados sobre a qual estamos falando abstratamente é conhecida
como um processador de recursos no MTS. Especificamente, o Shared Property Manager é o processador
de recursos do MTS que é usado para manter dados compartilhados definidos pelo componente ao lon-
go de todo o processo. The Shared Property Manager é representado pela interface ISharedPropertyGroup-
Manager. O Shared Property Manager é o nível superior de um sistema de armazenamento hierárquico,
mantendo qualquer número de grupos de propriedade compartilhados, que são representados pela in-
terface ISharedPropertyGroup. Por sua vez, cada grupo de propriedade compartilhada pode conter qualquer
número de propriedades compartilhadas, representadas pela interface ISharedProperty. As propriedades
compartilhadas são convenientes porque existem dentro do MTS, fora do contexto de qualquer instân-
cia de objeto específica e o acesso a elas é controlado pelos bloqueadores e semáforos gerenciados pelo
Shared Property Manager.
Com tudo isso em mente, a implementação do método NewGame( ) é mostrada no código a seguir:
procedure TGameServer.NewGame(out GameID: Integer);
var
SPG: ISharedPropertyGroup;
SProp: ISharedProperty;
Exists: WordBool;
GameData: OleVariant;
begin
// Use hierarquia do responsável pela chamada para validar segurança
CheckCallerSecurity;
// Obtém grupo de propriedades compartilhadas para esse objeto
SPG := GetSharedPropertyGroup;
// Cria ou recupera propriedade compartilhada NextGameID
SProp := SPG.CreateProperty(‘NextGameID’, Exists);
if Exists then GameID := SProp.Value
else GameID := 0;
// Incrementa e armazena propriedade compartilhada NextGameID
SProp.Value := GameID + 1;
// Cria array de dados do jogo
GameData := VarArrayCreate([1, 3, 1, 3], varByte);
SProp := SPG.CreateProperty(Format(GameDataStr, [GameID]), Exists); 687
SProp.Value := GameData;
SetComplete;
end;
Esse método primeiro verifica se o responsável pela chamada tem a hierarquia necessária para cha-
mar esse método (voltamos a esse assunto daqui a pouco). Em seguida, usa uma propriedade comparti-
lhada para obter um número de ID para o próximo jogo. Depois, esse método cria um array variante na
qual os dados do jogo são armazenados e salva esses dados como uma propriedade compartilhada. Final-
mente, esse método chama SetComplete( ) de modo que o MTS saiba que pode desativar essa instância
quando o método retornar.
Isso conduz para a regra número um de desenvolvimento de MTS: chamar SetComplete( ) ou Set-
Abort( ) tão freqüentemente quanto possível. Teoricamente, você vai chamar SetComplete( ) ou SetA-
bort( ) em todos os métodos de modo que o MTS possa reivindicar recursos anteriormente consumidos
por sua instância de componente depois que o método retornar. Um corolário para essa regra é que a ati-
vação e desativação do objeto não deve ser muito dispendiosa, pois esse código costuma ser chamado
com freqüência.
A implementação do método CheckCallerSecurity( ) ilustra como é fácil tirar vantagem da segurança
baseada em hierarquia no MTS:
procedure TGameServer.CheckCallerSecurity;
begin
// Só de brincadeira, só permite que a hierarquia “TTT” participe deste jogo.
if IsSecurityEnabled and not IsCallerInRole(‘TTT’) then
raise Exception.Create(‘Only those in the TTT role can play tic-tac-toe’);
end;
Esse código chega ao “x” da questão: “Como a gente estabelece a hierarquia TTT e determina os
usuários que pertencem a ela?” Embora seja possível definir as hierarquias programaticamente, o mais
objetivo para adicionar e configurar hierarquias é usar o Transaction Server Explorer do Windows NT.
Depois de o componente ser instalado (você vai aprender como instalar o componente daqui a pouco),
você pode configurar hierarquias usando o nó “Roles” encontrado em cada nó de pacote no Explorer. É
importante observar que a segurança baseada em hierarquias só é aceita para componentes executados
no Windows NT. Para componentes executados no Windows 9x, IsCallerInRole( ) sempre retornará
True.
Esses métodos são semelhantes já que ambos obtêm dados do jogo com base na propriedade com-
partilhada no parâmetro GameID, manipulam os dados de modo a refletir o movimento atual, salvam os
dados fora do array e verificam se o jogo acabou. O método ComputerMove( ) também chama CalcComputer-
Move( ) para analisar o jogo e faz um movimento. Se você está interessado em ver esse e outros compo-
nentes lógicos do MTS, observe a Listagem 23.15, que contém todo o código-fonte da unidade ServMain.
689
Listagem 23.15 LServMain.pas: contendo TGameServer
unit ServMain;
interface
uses
ActiveX, MtsObj, Mtx, ComObj, TTTServer_TLB;
tipo
PGameData = ^TGameData;
TGameData = array[1..3, 1..3] of Byte;
TGameServer = class(TMtsAutoObject, IGameServer)
private
procedure CalcComputerMove(GameData: PGameData; Skill: SkillLevels;
var X, Y: Integer);
function CalcGameStatus(GameData: PGameData): GameResults;
function GetSharedPropertyGroup: ISharedPropertyGroup;
procedure CheckCallerSecurity;
protected
procedure NewGame(out GameID: Integer); safecall;
procedure ComputerMove(GameID: Integer; SkillLevel: SkillLevels; out X,
Y: Integer; out GameRez: GameResults); safecall;
procedure PlayerMove(GameID, X, Y: Integer; out GameRez: GameResults);
safecall;
end;
implementation
uses ComServ, Windows, SysUtils;
const
GameDataStr = ‘TTTGameData%d’;
EmptySpot = 0;
PlayerSpot = $1;
ComputerSpot = $2;
function TGameServer.GetSharedPropertyGroup: ISharedPropertyGroup;
var
SPGMgr: ISharedPropertyGroupManager;
LockMode, RelMode: Integer;
Exists: WordBool;
begin
if ObjectContext = nil then
raise Exception.Create(‘Failed to obtain Object context’);
// Cria grupo de propriedade compartilhada para esse objeto
OleCheck(ObjectContext.CreateOcorrência(CLASS_SharedPropertyGroupManager,
ISharedPropertyGroupManager, SPGMgr));
LockMode := LockSetGet;
RelMode := Process;
Result := SPGMgr.CreatePropertyGroup(‘DelphiTTT’, LockMode, RelMode, Exists);
if Result = nil then
raise Exception.Create(‘Failed to obtain property group’);
end;
procedure TGameServer.NewGame(out GameID: Integer);
var
SPG: ISharedPropertyGroup;
SProp: ISharedProperty;
Exists: WordBool;
GameData: OleVariant;
begin
// Usa hierarquia do responsável pela chamada para validar segurança
690
Listagem 23.15 Continuação
CheckCallerSecurity;
// Obtém grupo de propriedades compartilhadas desse objeto
SPG := GetSharedPropertyGroup;
// Cria ou recupera propriedade compartilhada NextGameID
SProp := SPG.CreateProperty(‘NextGameID’, Exists);
if Exists then GameID := SProp.Value
else GameID := 0;
// Incrementa e armazena propriedade NextGameID compartilhada
SProp.Value := GameID + 1;
// Cria array de dados do jogo
GameData := VarArrayCreate([1, 3, 1, 3], varByte);
SProp := SPG.CreateProperty(Format(GameDataStr, [GameID]), Exists);
SProp.Value := GameData;
SetComplete;
end;
Exit;
end;
end;
if GameData[2, 2] < > EmptySpot then
begin
// Procura um vencedor na linha do meio, na coluna do meio e traça uma diagonal
// do canto inferior esquerdo até o canto inferior direito
if ((GameData[2, 2] = GameData[2, 1]) and (GameData[2, 2] =
GameData[2, 3])) or
((GameData[2, 2] = GameData[1, 2]) and (GameData[2, 2] =
GameData[3, 2])) or
((GameData[2, 2] = GameData[3, 1]) and (GameData[2, 2] =
GameData[1, 3])) then
begin
Result := GameData[2, 2] + 1; // Game result is spot ID + 1
Exit;
end;
end;
// Finalmente, verifica se o jogo ainda está em progresso
for I := 1 to 3 do
for J := 1 to 3 do
if GameData[I, J] = 0 then
begin
Result := grInProgress;
Exit;
end;
// Se estamos aqui é porque ainda estamos no jogo
Result := grTie;
end;
end
else if CalcTipo = ctColumn then
begin
I := 1;
J := Position;
end
else begin
I := 1;
case Position of
1: J := 1; // procurando do canto superior esquerdo ao canto inferior direito
2: J := 3; // procurando do canto superior direito ao canto inferior esquerdo
else
Exit; // abandona, somente 2 buscas diagonais
end;
end;
// Mascara bit Player ou Computer, dependendo se estamos pensando
// ofensiva ou defensivamente. Checktotal determina se essa é uma
// linha em que preciamos entrar.
case CurrentMoveTipo of
mtWin:
begin
Mask := PlayerSpot;
CheckTotal := 4;
end;
mtNew:
begin
Mask := PlayerSpot;
CheckTotal := 0;
end;
mtBlock:
begin
Mask := ComputerSpot;
CheckTotal := 2;
end;
else
begin
Mask := 0;
CheckTotal := 2;
end;
end;
// percorre todas as linhas no CalcTipo atual
repeat
// Obtém status de local atual (X, O ou vazio)
PosVal := GameData[I, J];
// Salva o último local vazio no caso de decidirmos mover aqui
if PosVal = 0 then
begin
X := I;
Y := J;
end
else
// Se a casa não estiver vazia, adicione valor mascarado a RowData
694
Listagem 23.15 Continuação
var
A, B, C: Integer;
begin
if Skill = slAwake then
begin
// Primeiro tenta ganhar o jogo, dpois tenta impedir uma vitória
for A := Ord(mtWin) to Ord(mtBlock) do
begin
CurrentMoveTipo := TMoveTipo(A);
for B := Ord(ctRow) to Ord(ctDiagonal) do
for C := 1 to 3 do
if DoCalcMove(TCalcTipo(B), C) then Exit;
end;
// Depois tenta tomar o centro do tabuleiro
if GameData[2, 2] = 0 then
begin
GameData[2, 2] := ComputerSpot;
X := 2;
Y := 2;
Exit;
end;
// Depois procura as posições mais vantajosas em uma linha
for A := Ord(mtOne) to Ord(mtNew) do
begin
CurrentMoveTipo := TMoveTipo(A);
for B := Ord(ctRow) to Ord(ctDiagonal) do
for C := 1 to 3 do
if DoCalcMove(TCalcTipo(B), C) then Exit;
end;
end;
// Finalmente (ou se o jogador não tiver nenhuma estratégia), basta ocupar
// a primeira casa livre
for A := 1 to 3 do 695
Listagem 23.15 Continuação
for B := 1 to 3 do
if GameData[A, B] = 0 then
begin
GameData[A, B] := ComputerSpot;
X := A;
Y := B;
Exit;
end;
end;
procedure TGameServer.CheckCallerSecurity;
begin
// Só de brincadeira, deixa participar do jogo os usuários que têm
// a hierarquia “TTT”.
if IsSecurityEnabled and not IsCallerInRole(‘TTT’) then
raise Exception.Create(‘Only those in the TTT role can play tic-tac-toe’);
end;
initialization
TAutoObjectFactory.Create(ComServer, TGameServer, Class_GameServer,
ciMultiOcorrência, tmApartment);
end.
Instalando o servidor
Uma vez que o servidor tenha sido escrito e você já esteja pronto para instalá-lo no MTS, o Delphi torna
sua vida muito fácil. Basta selecionar Run, Install MTS Objects (instalar objetos MTS) no menu principal
para chamar a caixa de diálogo Install MTS Objects. Essa caixa de diálogo permite instalar seu(s) obje-
to(s) em um pacote novo ou existente, como mostra a Figura 23.21.
A aplicação cliente
A Listagem 23.16 mostra o código-fonte da aplicação cliente desse componente MTS. Seu objetivo é ma-
pear o mecanismo fornecido pelo componente MTS para uma interface do usuário semelhante à do
696 tic-tac-toe.
Listagem 23.16 UiMain.pas: a unidade principal da aplicação cliente
unit UiMain;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
Buttons, ExtCtrls, Menus, TTTServer_TLB, ComCtrls;
type
TRecord = record
Wins, Loses, Ties: Integer;
end;
TFrmMain = class(TForm)
SbTL: TSpeedButton;
SbTM: TSpeedButton;
SbTR: TSpeedButton;
SbMM: TSpeedButton;
SbBL: TSpeedButton;
SbBR: TSpeedButton;
SbMR: TSpeedButton;
SbBM: TSpeedButton;
SbML: TSpeedButton;
Bevel1: TBevel;
Bevel2: TBevel;
Bevel3: TBevel;
Bevel4: TBevel;
MainMenu1: TMainMenu;
FileItem: TMenuItem;
HelpItem: TMenuItem;
ExitItem: TMenuItem;
AboutItem: TMenuItem;
SkillItem: TMenuItem;
UnconItem: TMenuItem;
AwakeItem: TMenuItem;
NewGameItem: TMenuItem;
N1: TMenuItem;
StatusBar: TStatusBar;
procedure FormCreate(Sender: TObject);
procedure ExitItemClick(Sender: TObject);
procedure SkillItemClick(Sender: TObject);
procedure AboutItemClick(Sender: TObject);
procedure SBClick(Sender: TObject);
procedure NewGameItemClick(Sender: TObject);
private
FXImage: TBitmap;
FOImage: TBitmap;
FCurrentSkill: Integer;
FGameID: Integer;
FGameServer: IGameServer;
FRec: TRecord;
procedure TagToCoord(ATag: Integer; var Coords: TPoint);
function CoordToCtl(const Coords: TPoint): TSpeedButton;
procedure DoGameResult(GameRez: GameResults);
end;
var
FrmMain: TFrmMain;
implementation 697
Listagem 23.16 Continuação
implementation
uses UiAbout;
{$R *.DFM}
{$R xo.res}
const
RecStr = ‘Wins: %d, Loses: %d, Ties: %d’;
procedure TFrmMain.FormCreate(Sender: TObject);
begin
// carrega imagens “X” e “O” a partir de recursos no TBitmaps
FXImage := TBitmap.Create;
FXImage.LoadFromResourceName(MainOcorrência, ‘x_img’);
FOImage := TBitmap.Create;
FOImage.LoadFromResourceName(MainOcorrência, ‘o_img’);
// define habilidade padrão
FCurrentSkill := slAwake;
// inicializa UI de registro
with FRec do
StatusBar.SimpleText := Format(RecStr, [Wins, Loses, Ties]);
// Obtém instância do servidor
FGameServer := CoGameServer.Create;
// Inicia um jogo novo
FGameServer.NewGame(FGameID);
end;
procedure TFrmMain.ExitItemClick(Sender: TObject);
begin
Close;
end;
procedure TFrmMain.SkillItemClick(Sender: TObject);
begin
with Sender as TMenuItem do
begin
Checked := True;
FCurrentSkill := Tag;
end;
end;
procedure TFrmMain.AboutItemClick(Sender: TObject);
begin
// Mostra caixa de diálogo About
with TFrmAbout.Create(Application) do
try
ShowModal;
finally
Free;
end;
end;
procedure TFrmMain.TagToCoord(ATag: Integer; var Coords: TPoint);
begin
case ATag of
0: Coords := Point(1, 1);
1: Coords := Point(1, 2);
2: Coords := Point(1, 3);
3: Coords := Point(2, 1);
4: Coords := Point(2, 2);
698 5: Coords := Point(2, 3);
Listagem 23.16 Continuação
end;
DoGameResult(GameRez);
end;
end;
end;
procedure TFrmMain.NewGameItemClick(Sender: TObject);
var
I: Integer;
begin
FGameServer.NewGame(FGameID);
for I := 0 to ControlCount - 1 do
if Controls[I] is TSpeedButton then
TSpeedButton(Controls[I]).Glyph := nil;
end;
procedure TFrmMain.DoGameResult(GameRez: GameResults);
const
EndMsg: array[grTie..grComputerWin] of string = (
‘Tie game’, ‘You win’, ‘Computer wins’);
begin
if GameRez < > grInProgress then
begin
case GameRez of
grComputerWin: Inc(FRec.Loses);
grPlayerWin: Inc(FRec.Wins);
grTie: Inc(FRec.Ties);
end;
with FRec do
StatusBar.SimpleText := Format(RecStr, [Wins, Loses, Ties]);
if MessageDlg(Format(‘%s! Play again?’, [EndMsg[GameRez]]), mtConfirmation,
[mbYes, mbNo], 0) = mrYes then
NewGameItemClick(nil);
end;
end;
end.
FIGURA 23.23 Usando a caixa de diálogo Run Parameters para configurar uma sessão de depuração do MTS.
O MTS é uma poderosa adição à família de tecnologias COM. Adicionando serviços como geren-
ciamento permanente, suporte a transação, segurança e transações a objetos COM sem exigir mudanças
significativas no código-fonte existente, a Microsoft aproveitou o COM em uma tecnologia mais escalá-
vel, que pode ser usada no desenvolvimento distribuído em grande escala. Esta seção mostra os funda-
mento do MTS e os detalhes específicos do suporte a MTS do Delphi, bem como o processo de criação
de aplicações MTS no Delphi. Você também vai aprender algumas dicas e macetes para desenvolver
componentes MTS otimizados e bem-comportados. MTS se destaca fornecendo serviços como geren-
ciamento permanente, suporte a transação e a segurança, tudo em uma estrutura familiar. O MTS e o
Delphi, juntos, fornecem um meio através do qual você pode alavancar sua experiência com o COM e
utilizá-la para criar aplicações multicamadas escaláveis. Mas não se esqueça as pequenas diferenças de
projeto entre os componentes COM e MTS normais!
TOleContainer
Agora que você tem uma base de ActiveX OLE, dê uma olhada na classe TOleContainer do Delphi. TOleCon-
tainer está localizado na unidade OleCntrs e encapsula as complexidades de um container OLE Document
e ActiveX Document em um componente VCL facilmente digerível.
NOTA
Se você se sentisse familiar com o uso do componente TOleContainer do Delphi 1.0, poderia atirar esse co-
nhecimento na janela. Como a versão de 32 bits desse componente foi totalmente reprojetada (como di-
zem nos comerciais de carro), qualquer conhecimento que você tenha da versão de 16 bits desse compo-
nente pode não ser aplicável à versão de 32 bits. Não se deixe assustar, no entanto; a versão de 32 bits des-
se componente é um projeto muito mais limpo e você perceberá que o código que deve escrever para dar
suporte ao objeto talvez seja um terço do trabalho com o qual está acostumado.
NOTA
Um objeto OLE não será ativado no local no tempo de projeto. Você só será capaz de tirar partido da capa-
cidade de ativação no local do TOleContainer no runtime.
Se você quiser chamar a caixa de diálogo Insert Object no runtime, poderá chamar o método
InsertObjectDialog( ) de TOleContainer, que é definido da seguinte maneira:
function InsertObjectDialog: Boolean;
Essa função retorna True se um novo tipo de objeto OLE tiver sido escolhido com êxito a partir da
caixa de diálogo.
O formulário MDI filho é mostrado na Figura 23.28. Na verdade, ele é um formulário no estilo
fsMDIChild com um componente TOleContainer alinhado a um alClient.
A Listagem 23.17 mostra o ChildWin.pas, a unidade do código-fonte do formulário MDI filho. Obser-
ve que essa unidade é bastante padrão, exceto pela adição da propriedade OLEFileName e o método associado
e a variável da instância private. Essa propriedade armazena o caminho e o nome de arquivo do arquivo
OLE e o método de acesso à propriedade define a legenda do formulário filho como o nome do arquivo.
unit Childwin;
interface
uses WinTipos, WinProcs, Classes, Graphics, Forms, Controls, OleCtnrs;
type
TMDIChild = class(TForm)
OleContainer: TOleContainer;
procedure FormClose(Sender: TObject; var Action: TCloseAction);
private
FOLEFilename: String;
procedure SetOLEFileName(const Value: String);
public
property OLEFileName: String read FOLEFileName write SetOLEFileName;
end;
implementation
{$R *.DFM}
uses Main, SysUtils;
procedure TMDIChild.SetOLEFileName(const Value: String);
begin
if Value < > FOLEFileName then begin
FOLEFileName := Value;
Caption := ExtractFileName(FOLEFileName);
end;
end;
procedure TMDIChild.FormClose(Sender: TObject; var Action: TCloseAction);
begin
Action := caFree;
end;
end.
O código a seguir manipula itens de menu File, Save As e File, Save. Observe que o método FileSa-
veItemClick( )chama FileSaveAsItemClick( ) quando o filho MDI ativo não tem um nome especificado.
Aqui está o código:
procedure TMainForm.FileSaveAsItemClick(Sender: TObject);
begin
if (ActiveMDIChild < > Nil) and (SaveDialog.Execute) then
with TMDIChild(ActiveMDIChild) do
begin
OleFileName := SaveDialog.FileName;
OleContainer.SaveToFile(OleFileName);
end;
end;
procedure TMainForm.FileSaveItemClick(Sender: TObject);
begin
if ActiveMDIChild < > Nil then
{ se não houver um nome atribuído, use “salvar como” }
if TMDIChild(ActiveMDIChild).OLEFileName = ‘’ then
FileSaveAsItemClick(Sender)
else
{ caso contrário, salve com o nome atual}
with TMDIChild(ActiveMDIChild) do
OleContainer.SaveToFile(OLEFileName);
end;
Quando estiver certo de ter um objeto OLE no Clipboard, basta uma etapa adicional para lê-lo em
um componente TOleContainer. Antes de tentar colar o conteúdo do Clipboard em um TOleContainer, você
deve primeiro verificar o valor da propriedade CanPaste para se assegurar de que os dados no Clipboard
sejam compatíveis com um objeto OLE. Depois disso, você deve chamar a caixa de diálogo Paste Special
(colar especial) para colar o objeto no TOleContainer chamando seu método PasteSpecialDialog( ), como
mostra o código a seguir (a caixa de diálogo Paste Special aparece na Figura 23.29):
procedure TMainForm.PasteItemClick(Sender: TObject);
begin
if ActiveMDIChild < > nil then
with TMDIChild(ActiveMDIChild).OleContainer do
{ Antes de chamar a caixa de diálogo, certifique-se de que há}
{ objetos OLE válidos no clipboard. }
if CanPaste then PasteSpecialDialog;
end;
Quando a aplicação é executada, o servidor controlando o objeto OLE no filho MDI ativo mistu-
ra-se com ou assume o controle do menu e barra de ferramentas da aplicação. As Figuras 23.30 e 23.31
mostram um recurso de ativação no local da OLE – a aplicação OLE MDI é controlada por dois servido-
res OLE diferentes.
706
FIGURA 23.31 Editando um gráfico incorporado do Paint.
A listagem completa de Main.pas, a unidade principal da aplicação MDI OLE, é mostrada na Lista-
gem 23.18.
unit Main;
interface
type
TMainForm = class(TForm)
MainMenu1: TMainMenu;
File1: TMenuItem;
FileNewItem: TMenuItem;
FileOpenItem: TMenuItem;
FileCloseItem: TMenuItem;
Window1: TMenuItem;
Help1: TMenuItem;
N1: TMenuItem;
FileExitItem: TMenuItem;
WindowCascadeItem: TMenuItem;
WindowTileItem: TMenuItem;
WindowArrangeItem: TMenuItem;
HelpAboutItem: TMenuItem;
OpenDialog: TOpenDialog;
FileSaveItem: TMenuItem;
FileSaveAsItem: TMenuItem;
Edit1: TMenuItem;
PasteItem: TMenuItem;
WindowMinimizeItem: TMenuItem;
SaveDialog: TSaveDialog;
CopyItem: TMenuItem;
CloseAll1: TMenuItem;
StatusBar: TStatusBar;
CoolBar1: TCoolBar;
ToolBar1: TToolBar; 707
Listagem 23.18 Continuação
OpenBtn: TToolButton;
SaveBtn: TToolButton;
ToolButton3: TToolButton;
CopyBtn: TToolButton;
PasteBtn: TToolButton;
ToolButton6: TToolButton;
ExitBtn: TToolButton;
ImageList1: TImageList;
procedure FormCreate(Sender: TObject);
procedure FileNewItemClick(Sender: TObject);
procedure WindowCascadeItemClick(Sender: TObject);
procedure UpdateMenuItems(Sender: TObject);
procedure WindowTileItemClick(Sender: TObject);
procedure WindowArrangeItemClick(Sender: TObject);
procedure FileCloseItemClick(Sender: TObject);
procedure FileOpenItemClick(Sender: TObject);
procedure FileExitItemClick(Sender: TObject);
procedure FileSaveItemClick(Sender: TObject);
procedure FileSaveAsItemClick(Sender: TObject);
procedure PasteItemClick(Sender: TObject);
procedure WindowMinimizeItemClick(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure HelpAboutItemClick(Sender: TObject);
procedure CopyItemClick(Sender: TObject);
procedure CloseAll1Click(Sender: TObject);
private
procedure ShowHint(Sender: TObject);
end;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
uses About;
var
NumChildren: Cardinal = 0;
procedure TMainForm.FormCreate(Sender: TObject);
begin
Application.OnHint := ShowHint;
Screen.OnActiveFormChange := UpdateMenuItems;
end;
procedure TMainForm.ShowHint(Sender: TObject);
begin
{ Mostra dicas na barra de status }
StatusBar.Panels[0].Text := Application.Hint;
end;
procedure TMainForm.FileNewItemClick(Sender: TObject);
begin
inc(NumChildren);
{ Cria uma nova janela filho MDI }
with TMDIChild.Create(Application) do
begin
Caption := ‘Untitled’ + IntToStr(NumChildren);
708 { Introduz caixa de diálogo inserir objeto OLE e insere na janela filho}
Listagem 23.18 Continuação
OleContainer.InsertObjectDialog;
end;
end;
procedure TMainForm.FileOpenItemClick(Sender: TObject);
begin
if OpenDialog.Execute then
with TMDIChild.Create(Application) do
begin
try
OleFileName := OpenDialog.FileName;
OleContainer.LoadFromFile(OleFileName);
Show;
except
Release; // libera formulário no erro
raise; // reproduz exceção
end;
end;
end;
procedure TMainForm.FileCloseItemClick(Sender: TObject);
begin
if ActiveMDIChild < > nil then
ActiveMDIChild.Close;
end;
procedure TMainForm.FileSaveAsItemClick(Sender: TObject);
begin
if (ActiveMDIChild < > nil) and (SaveDialog.Execute) then
with TMDIChild(ActiveMDIChild) do
begin
OleFileName := SaveDialog.FileName;
OleContainer.SaveToFile(OleFileName);
end;
end;
procedure TMainForm.FileSaveItemClick(Sender: TObject);
begin
if ActiveMDIChild < > nil then
{ se não houver algum nome atribuído, use “salvar como” }
if TMDIChild(ActiveMDIChild).OLEFileName = ‘’ then
FileSaveAsItemClick(Sender)
else
{ caso contrário, salve com o nome atual }
with TMDIChild(ActiveMDIChild) do
OleContainer.SaveToFile(OLEFileName);
end;
procedure TMainForm.FileExitItemClick(Sender: TObject);
begin
Close;
end;
procedure TMainForm.PasteItemClick(Sender: TObject);
begin
if ActiveMDIChild < > nil then
with TMDIChild(ActiveMDIChild).OleContainer do
{ Antes de chamar a caixa de diálogo, certifique-se de haver }
{ objetos OLE válidos no clipboard. } 709
Listagem 23.18 Continuação
ShowModal;
Free;
end;
end;
end.
Resumo
Chegamos ao fim do capítulo sobre COM, OLE e ActiveX. Este capítulo discutiu uma grande massa de
informações! Primeiro, você recebeu uma base sólida em tecnologias COM e ActiveX, que deve ajudar
você a entender o que acontece nos bastidores. Depois, você foi apresentado a alguns truques e macetes
sobre vários tipos de clientes e servidores COM. Em seguida, conheceu várias técnicas avançadas de Au-
tomation no Delphi. Por fim, este capítulo discutiu a teoria e a prática do MTS. Além de conhecer pro-
fundamente o COM, o Automation e o MTS, você deve estar familiarizado com o funcionamento do
componente de TOleContainer da VCL.
Se você quiser saber mais sobre COM, encontrará mais informação sobre a tecnologia COM e
ActiveX em outras áreas deste livro. O Capítulo 24 mostra exemplos da vida real de criação de servi-
dor COM e o Capítulo 25 discute a criação de controle ActiveX no Delphi.
711
Extensão do shell CAPÍTULO
do Windows
24
NE STE C AP ÍT UL O
l Um componente de ícone de notificação da
bandeja 713
l Barras de ferramentas de desktop da
aplicação 726
l Vínculos do shell 738
l Extensões do shell 754
l Resumo 776
Lançado no Windows 95, o shell do Windows também tem suporte no Windows NT 3.51 (e superior),
Windows 98 e Windows 2000. Parente distante do Program Manager (gerenciador de programas), o
shell do Windows inclui alguns recursos para estender o shell de modo a atender às suas necessidades. O
problema é que muitos desses poderosos recursos extensíveis são temas de desenvolvimento do Win32
maldocumentados. Este capítulo tem como objetivo prestar informações e oferecer exemplos de que
você precisa para ter acesso aos recursos do shell, por exemplo, os ícones de notificação da bandeja, bar-
ras de ferramentas de desktop da aplicação, vínculos do shell e extensões do shell.
A API
Acredite se quiser, mas somente uma chamada da API está envolvida na criação, modificação e remoção
dos ícones de notificação da bandeja. A função se chama Shell_NotifyIcon( ). Essa e outras funções rela-
cionadas ao shell do Windows estão contidas na unidade ShellAPI. Shell_NotifyIcon( ) é definida da se-
guinte maneira:
function Shell_NotifyIcon(dwMessage: DWORD; lpData:
PNotifyIconData): BOOL; stdcall;
O parâmetro dwMessage descreve a ação a ser executada pelo ícone, que pode ser qualquer um dos va-
lores mostrados na Tabela 24.1.
O parâmetro lpData é um ponteiro para um registro TNotifyIconData. Esse registro é definido da se-
guinte maneira:
type
TNotifyIconData = record
cbSize: DWORD;
713
Wnd: HWND;
uID: UINT;
uFlags: UINT;
uCallbackMessage: UINT;
hIcon: HICON;
szTip: array [0..63] of AnsiChar;
end;
O campo cbSize armazena o tamanho do registro e deve ser inicializado como SizeOf(TNotifyIconData).
Wnd é a alça da janela à qual as mensagens de “callback” de notificação da bandeja devem ser envia-
das (callback está entre aspas, pois não se trata de uma callback no sentido estrito da palavra; no entanto,
a documentação do Win32 usa essa terminologia para mensagens enviadas para uma janela em favor de
um ícone de notificação da bandeja).
uID é um número de ID exclusivo definido pelo programador. Se você tiver uma aplicação com di-
versos ícones, precisará identificar cada um deles colocando um número diferente nesse campo.
uFlags descreve qual dos campos do registro TNotifyIconData deve ser considerado ativo pela função
Shell_NotifyIcon( ) e, por essa razão, qual das propriedades do ícone será afetada pela ação especificada
pelo parâmetro dwMessage. Esse parâmetro pode ser qualquer combinação de flags mostrada na Tabela 24.2.
uCallbackMessage contém o valor da mensagem do Windows a ser enviada para a janela identificada
pelo campo Wnd. Geralmente, o valor desse campo é obtido chamando RegisterWindowMessage( ) ou usando
um offset de WM_USER. O lParam dessa mensagem terá o mesmo valor que o campo uID e wParam armazenará a
mensagem de mouse gerada pelo ícone de notificação.
hIcon identifica a alça para o ícone que será colocado na bandeja de notificação.
szTip armazena uma string terminada em null que aparecerá na janela de dica exibida quando o pon-
teiro do mouse for colocado sobre o ícone de notificação.
O componente TTrayNotifyIcon encapsula Shell_NotifyIcon( ) em um método chamado SendTrayMessa-
ge( ), que é mostrado a seguir:
initialization
{ Obtém uma ID de mensagem exclusiva do Windows para a callback da bandeja }
DDGM_TRAYICON := RegisterWindowMessage(TrayMsgStr);
hIcon apanha o valor de retorno fornecido pelo método ActiveIconHandle( ). Esse método retorna a
alça do ícone atualmente selecionado na propriedade Icon.
Manipulando mensagens
Já dissemos que todas as mensagens da bandeja de notificação são enviadas para uma janela mantida pelo
objeto IconMgr global. Esse objeto é construído e liberado nas seções initialization e finalization da unida-
de do componente, conforme mostrado a seguir:
initialization
{ Obtém a ID de mensagem exclusiva do Windows para a callback da bandeja }
DDGM_TRAYICON := RegisterWindowMessage(TrayMsgStr);
IconMgr := TIconManager.Create;
finalization
IconMgr.Free;
A janela para a qual as mensagens da bandeja de notificação serão enviadas é criada no construtor
desse objeto, usando a função AllocateHWnd( ):
constructor TIconManager.Create;
begin
FHWindow := AllocateHWnd(TrayWndProc);
end;
Cliques do mouse
Uma das partes mais desafiadoras desse componente é garantir que os cliques do mouse sejam manipula-
dos de modo apropriado. Você pode ter percebido que muitos ícones de notificação da bandeja execu-
tam três ações diferentes devido a cliques do mouse:
l Abre uma janela com um clique único.
l Abre uma janela diferente (geralmente uma folha de propriedades) com um clique duplo.
l Chama um menu local com um clique no botão direito.
O desafio está na criação de um evento que represente o clique duplo sem acionar também o evento
de clique único.
Em termos de mensagem do Windows, quando o usuário dá um clique duplo no botão esquerdo do
mouse, a janela selecionada recebe tanto a mensagem WM_LBUTTONDOWN quanto a mensagem WM_LBUTTONDBLCLK.
Para permitir que uma mensagem de clique duplo seja processada independentemente de um clique úni-
co, é preciso um mecanismo para retardar a manipulação da mensagem de clique único pelo tempo ne-
cessário para garantir que uma mensagem de clique duplo não esteja a caminho.
O intervalo de tempo a esperar antes de você poder ter certeza de que uma mensagem
WM_LBUTTONDBLCLK não esteja vindo depois de uma mensagem M_LBUTTONDOWN é bastante fácil de determinar. A
função GetDoubleClickTime( ) da API, que não utiliza parâmetros, retorna o maior intervalo de tempo pos-
sível (em milissegundos) que o Control Panel (painel de controle) permitirá entre os dois cliques de um
clique duplo. O componente TTimer é escolha óbvia para um mecanismo permitir que você aguarde o nú-
716
mero de milissegundos especificado por GetDoubleClickTime( ) para garantir que um clique duplo não vem
depois de um clique único. Por essa razão, um componente TTimer é criado e inicializado no construtor do
componente TTrayNotifyIcon com o seguinte código:
FTimer := TTimer.Create(Self);
with FTimer do
begin
Enabled := False;
Interval := GetDoubleClickTime;
OnTimer := OnButtonTimer;
end;
é um método que será chamado quando o intervalo do timer expirar. Vamos mos-
OnButtonTimer( )
trar esse método daqui a pouco.
Em outra oportunidade, já dissemos que as mensagens da bandeja de notificação são filtradas atra-
vés do método TrayWndProc( ) de IconMgr. Agora chegou a hora de você conhecer esse método:
procedure TIconManager.TrayWndProc(var Message: TMessage);
{ Isso nos permite manipular as mensagens de callback da bandeja }
{ de dentro do contexto do componente. }
var
Pt: TPoint;
TheIcon: TTrayNotifyIcon;
begin
with Message do
begin
{ caso seja a mensagem de callbacck da bandeja }
if (Msg = DDGM_TRAYICON) then
begin
TheIcon := TTrayNotifyIcon(WParam);
case lParam of
{ ativa o timer no primeiro pressionamento do mouse. }
{ OnClick será acionado pelo método OnTimer, desde que }
{ o clique duplo não tenha ocorrido. }
WM_LBUTTONDOWN: TheIcon.FTimer.Enabled := True;
{ Não define um flag de clique no clique duplo. Isso eliminará o }
{ clique único. }
WM_LBUTTONDBLCLK:
begin
TheIcon.FNoShowClick := True;
if Assigned(TheIcon.FOnDblClick) then TheIcon.FOnDblClick(Self);
end;
WM_RBUTTONDOWN:
begin
if Assigned(TheIcon.FPopupMenu) then
begin
{ Chamada para SetForegroundWindow é exigida pela API }
SetForegroundWindow(IconMgr.HWindow);
{ Abre o menu local na posição do cursor. }
GetCursorPos(Pt);
TheIcon.FPopupMenu.Popup(Pt.X, Pt.Y);
{ Postagem da mensagem exigida pela API p/forçar troca de tarefa }
PostMessage(IconMgr.HWindow, WM_USER, 0, 0);
end;
end;
end;
717
end
else
{ Se não for uma mensagem de callback da bandeja, chama DefWindowProc }
Result := DefWindowProc(FHWindow, Msg, wParam, lParam);
end;
end;
O que faz isso tudo funcionar é que a mensagem de clique único se limita a ativar o timer, enquanto
a mensagem de clique duplo define um flag para indicar que o clique duplo ocorreu antes do acionamen-
to do seu evento OnDblClick. A propósito, o clique com o botão direito chama o menu instantâneo dado
pela propriedade PopupMenu. Agora dê uma olhada no método OnButtonTimer( ):
procedure TTrayNotifyIcon.OnButtonTimer(Sender: TObject);
begin
{ Desativa o timer, pois só queremos que ele seja acionado uma vez. }
FTimer.Enabled := False;
{ Se um clique duplo não tiver ocorrido, o clique único é acionado. }
if (not FNoShowClick) and Assigned(FOnClick) then
FOnClick(Self);
FNoShowClick := False; // reseta o flag
end;
Esse método primeiro desativa o timer para garantir que o evento só seja acionado uma vez a cada
clique no mouse. Posteriormente, o método verifica o status do flag FNoShowClick. Lembre-se de que esse
flag será definido pela mensagem de clique duplo no método OwnerWndProc( ). Por essa razão, o evento
OnClick só será acionado quando OnDblClk não o for.
Ocultando a aplicação
Outro aspecto das aplicações da bandeja de notificação é que elas não aparecem como botões na barra de
tarefas do sistema. Para fornecer essa funcionalidade, o componente TTrayNotifyIcon expõe uma proprie-
dade HideTask que permite que o usuário decida se a aplicação deve ser visível na barra de tarefas. O méto-
do write para essa propriedade é mostrado no código a seguir. A linha de código que realiza o trabalho é a
chamada para o procedimento ShowWindow( ) da API, que passa a propriedade Handle de Application e uma
constante para indicar se a aplicação tem que ser mostrada normalmente ou oculta. Veja o código a se-
guir:
procedure TTrayNotifyIcon.SetHideTask(Value: Boolean);
{ Escreve método da propriedade HideTask }
const
{ Apresenta flag para mostrar a aplicação normalmente ou ocultá-la }
ShowArray: array[Boolean] of integer = (sw_ShowNormal, sw_Hide);
begin
if FHideTask < > Value then begin
FHideTask := Value;
{ Não faz nada no modo de projeto }
if not (csDesigning in ComponentState) then
ShowWindow(Application.Handle, ShowArray[FHideTask]);
end;
end;
A Listagem 24.1 mostra a unidade TrayIcon.pas, que contém o código-fonte completo do compo-
nente TTrayNotifyIcon.
718
Listagem 24.1 TrayIcon.pas: código-fonte do componente TrayIcon
unit TrayIcon;
interface
type
ENotifyIconError = class(Exception);
TTrayNotifyIcon = class(TComponent)
private
FDefaultIcon: THandle;
FIcon: TIcon;
FHideTask: Boolean;
FHint: string;
FIconVisible: Boolean;
FPopupMenu: TPopupMenu;
FOnClick: TNotifyEvent;
FOnDblClick: TNotifyEvent;
FNoShowClick: Boolean;
FTimer: TTimer;
Tnd: TNotifyIconData;
procedure SetIcon(Value: TIcon);
procedure SetHideTask(Value: Boolean);
procedure SetHint(Value: string);
procedure SetIconVisible(Value: Boolean);
procedure SetPopupMenu(Value: TPopupMenu);
procedure SendTrayMessage(Msg: DWORD; Flags: UINT);
function ActiveIconHandle: THandle;
procedure OnButtonTimer(Sender: TObject);
protected
procedure Loaded; override;
procedure LoadDefaultIcon; virtual;
procedure Notification(AComponent: TComponent;
Operation: TOperation); override;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
published
property Icon: TIcon read FIcon write SetIcon;
property HideTask: Boolean read FHideTask write SetHideTask default False;
property Hint: String read FHint write SetHint;
property IconVisible: Boolean read FIconVisible write SetIconVisible
default False;
property PopupMenu: TPopupMenu read FPopupMenu write SetPopupMenu;
property OnClick: TNotifyEvent read FOnClick write FOnClick;
property OnDblClick: TNotifyEvent read FOnDblClick write FOnDblClick;
end;
implementation
{ TIconManager }
719
Listagem 24.1 Continuação
var
IconMgr: TIconManager;
DDGM_TRAYICON: Integer;
constructor TIconManager.Create;
begin
FHWindow := AllocateHWnd(TrayWndProc);
end;
destructor TIconManager.Destroy;
begin
if FHWindow < > 0 then DeallocateHWnd(FHWindow);
inherited Destroy;
end;
WM_RBUTTONDOWN:
begin
if Assigned(TheIcon.FPopupMenu) then
begin
{ Chamada para SetForegroundWindow é exibida pela API }
SetForegroundWindow(IconMgr.HWindow);
{ Abre menu local na posição do cursor. }
GetCursorPos(Pt);
TheIcon.FPopupMenu.Popup(Pt.X, Pt.Y);
{ Postagem da mensagem exigida pela API para forçar troca de tarefa }
PostMessage(IconMgr.HWindow, WM_USER, 0, 0);
end;
end;
end;
end
else
{ Se não for uma mensagem de callback da bandeja, chama DefWindowProc }
Result := DefWindowProc(FHWindow, Msg, wParam, lParam);
end;
end;
{ TTrayNotifyIcon }
destructor TTrayNotifyIcon.Destroy;
begin
if FIconVisible then SetIconVisible(False); // ícone de destruição
FIcon.Free; // libera objetos
FTimer.Free;
inherited Destroy;
end;
Result := FDefaultIcon;
end;
procedure TTrayNotifyIcon.LoadDefaultIcon;
{ Carrega o ícone-padrão da janela para mantê-lo à mão. }
{ Isso permitirá que o componente use o logo das janelas }
{ como o default quando nenhum ícone estiver selecionado na }
{ propriedade Icon. }
begin
FDefaultIcon := LoadIcon(0, IDI_WINLOGO);
end;
procedure TTrayNotifyIcon.Loaded;
{ Chamado depois que o componente é carregado a partir do stream }
begin
inherited Loaded;
{ Se o ícone deve ser visível, crie-o. }
if FIconVisible then
SendTrayMessage(NIM_ADD, NIF_MESSAGE or NIF_ICON or NIF_TIP);
end;
uID := UINT(Self);
Wnd := IconMgr.HWindow;
uCallbackMessage := DDGM_TRAYICON;
hIcon := ActiveIconHandle;
end;
Shell_NotifyIcon(Msg, @Tnd);
end;
const
{ String para identificar mensagem registrada do Windows }
TrayMsgStr = ‘DDG.TrayNotifyIconMsg’;
initialization
{ Obtém ID exclusiva da mensagem do Windows para a callback da bandeja }
DDGM_TRAYICON := RegisterWindowMessage(TrayMsgStr);
IconMgr := TIconManager.Create;
finalization
IconMgr.Free;
end.
724
Listagem 24.2 Main.pas: a unidade principal da aplicação de um ícone de notificação de exemplo
unit main;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, ShellAPI, TrayIcon, Menus, ComCtrls;
type
TMainForm = class(TForm)
pmiPopup: TPopupMenu;
pgclPageCtl: TPageControl;
TabSheet1: TTabSheet;
btnClose: TButton;
btnTerm: TButton;
Terminate1: TMenuItem;
Label1: TLabel;
N1: TMenuItem;
Propeties1: TMenuItem;
TrayNotifyIcon1: TTrayNotifyIcon;
procedure NotifyIcon1Click(Sender: TObject);
procedure NotifyIcon1DblClick(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure btnTermClick(Sender: TObject);
procedure btnCloseClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
end;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
Application.Terminate;
end;
end.
Além de ser encaixada nas bordas da tela, AppBars pode empregar recursos iguais aos da barra de
tarefas, como por exemplo, a funcionalidade de ocultar automaticamente e arrastar e soltar. Você pode
se surpreender, no entanto, é com o tamanho da API, que é mínima (tem apenas uma função). Como se
pode deduzir pelo seu pequeno tamanho, a API não tem muita coisa a oferecer. A função da API é mais
consultiva do que funcional. Ou seja, em vez de controlar a AppBar com comandos do tipo “faça isso,
faça aquilo”, você interroga a AppBar com comandos do tipo “posso fazer isso, posso fazer aquilo?”
A API
Como os ícones de notificação da bandeja, AppBars só tem uma função da API com a qual você vai traba-
lhar – SHAppBarMessage( ), nesse caso. Veja a seguir como SHAppBarMessage( ) é definida na unidade ShellAPI:
function SHAppBarMessage(dwMessage: DWORD; var pData: TAppBarData): UINT
stdcall;
O primeiro parâmetro dessa função, dwMessage, pode conter um dos valores descritos na Tabela 24.3.
ABM_NEW $0 Registra uma nova AppBar e especifica uma nova mensagem de callback.
ABM_REMOVE $1 Retira o registro de uma AppBar existente.
726 ABM_QUERYPOS $2 Solicita uma posição e um tamanho novos de uma AppBar.
Tabela 24.3 Continuação
Nesse registro, o campo cbSize armazena o tamanho do registro, o campo hWnd armazena a alça da ja-
nela da AppBar especificada, uCallbackMessage armazena o valor da mensagem que será enviada para a janela
AppBar juntamente com as mensagens de notificação, rc armazena o retângulo que envolve a AppBar em
questão e lParam armazena algumas informações adicionais específicas da mensagem.
DICA
Para obter maiores informações sobre a função SHAppBarMessage( ) da API e sobre o tipo TAppBarData, con-
sulte a ajuda on-line do Win32.
Se a AppBar for criada com êxito, o método SetAppBarEdge( ) será chamado para definir a AppBar
como sua posição inicial. Esse método, por sua vez, chama o método SetAppBarPos( ), passando o flag
apropriado, definido pela API, que indica a borda da margem solicitada. Como era de se imaginar, os
flags ABE_TOP, ABE_BOTTOM, ABE_LEFT e ABE_RIGHT representam cada uma das bordas da tela. Isso é mostrado no
código a seguir:
procedure TAppBar.SetAppBarPos(Edge: UINT);
begin
if csDesigning in ComponentState then Exit;
FABD.uEdge := Edge; // define borda
with FABD.rc do
begin
// define coordenada como tela inteira
Top := 0;
Left := 0;
Right := Screen.Width;
Bottom := Screen.Height;
// Envia ABM_QUERYPOS para obter retângulo apropriado na margem
SendAppBarMsg(ABM_QUERYPOS);
// reajusta retângulo com base no que foi modificado por ABM_QUERYPOS
case Edge of
ABE_LEFT: Right := Left + FDockedWidth;
ABE_RIGHT: Left := Right - FDockedWidth;
ABE_TOP: Bottom := Top + FDockedHeight;
ABE_BOTTOM: Top := Bottom - FDockedHeight;
end;
// Define a posição da barra da aplicação.
SendAppBarMsg(ABM_SETPOS);
end;
// Define a propriedade BoundsRect de modo que ela se adapte
// ao retângulo passado para o sistema.
BoundsRect := FABD.rc;
end;
Esse método primeiro define o campo uEdge de FABD como o valor passado através do parâmetro Edge.
Em seguida, ele envia o campo rc para as coordenadas da tela inteira e envia a mensagem ABM_QUERYPOS.
Essa mensagem redefine o campo rc de modo que contenha o retângulo apropriado para a borda indica-
da por uEdge. Uma vez o retângulo apropriado tenha sido obtido, rc é ajustado novamente de modo a ter
altura e largura razoáveis. Nesse ponto, rc armazena o retângulo final para a AppBar. A mensagem
ABM_SETPOS, em seguida, é enviada para informar ao shell quanto ao novo retângulo; o retângulo é defini-
do usando a propriedade BoundsRect do controle.
Já dissemos que as mensagens de notificação da AppBar serão enviadas para a janela indicada por
FABD.hWnd usando o identificador de mensagem armazenado em FABD.uCallbackMessage. Essas mensagens de
notificação são manipuladas no método WndProc( ) mostrado a seguir:
procedure TAppBar.WndProc(var M: TMessage);
var
State: UINT;
WndPos: HWnd;
begin
if M.Msg = AppBarMsg then
728
begin
case M.WParam of
// Enviada quando sempre visível ou ocultar automaticamente é alterado.
ABN_STATECHANGE:
begin
// Verifica se a barra de acesso ainda é ABS_ALWAYSONTOP.
State := SendAppBarMsg(ABM_GETSTATE);
if ABS_ALWAYSONTOP and State = 0 then
SetTopMost(False)
else
SetTopMost(True);
end;
// Uma aplicação de tela cheia foi iniciada ou a última
// aplicação de tela cheia foi fechada.
ABN_FULLSCREENAPP:
begin
// Define a ordem z da barra de acesso de modo aproprieado.
State := SendAppBarMsg(ABM_GETSTATE);
if M.lParam < > 0 then begin
if ABS_ALWAYSONTOP and State = 0 then
SetTopMost(False)
else
SetTopMost(True);
end
else
if State and ABS_ALWAYSONTOP < > 0 then
SetTopMost(True);
end;
// Enviado quando houver algo que afete a posição da AppBar.
ABN_POSCHANGED:
begin
// A barra de tarefas ou outra barra de acesso
// mudou seu tamanho ou sua posição.
SetAppBarPos(FABD.uEdge);
end;
end;
end
else
inherited WndProc(M);
end;
Esse método manipula algumas mensagens de notificação que permitem que a AppBar responda às
mudanças que podem ocorrer no shell enquanto a aplicação estiver sendo executada. O restante do códi-
go do componente AppBar é mostrado na Listagem 24.3.
Listagem 24.3 AppBars.pas, a unidade que contém a classe básica que oferece suporte a AppBar
unit AppBars;
interface
type
TAppBarEdge = (abeTop, abeBottom, abeLeft, abeRight); 729
Listagem 24.3 Continuação
EAppBarError = class(Exception);
TAppBar = class(TCustomForm)
private
FABD: TAppBarData;
FDockedHeight: Integer;
FDockedWidth: Integer;
FEdge: TAppBarEdge;
FOnEdgeChanged: TNotifyEvent;
FTopMost: Boolean;
procedure WMActivate(var M: TMessage); message WM_ACTIVATE;
procedure WMWindowPosChanged(var M: TMessage); message WM_WINDOWPOSCHANGED;
function SendAppBarMsg(Msg: DWORD): UINT;
procedure SetAppBarEdge(Value: TAppBarEdge);
procedure SetAppBarPos(Edge: UINT);
procedure SetTopMost(Value: Boolean);
procedure SetDockedHeight(const Value: Integer);
procedure SetDockedWidth(const Value: Integer);
protected
procedure CreateParams(var Params: TCreateParams); override;
procedure CreateWnd; override;
procedure DestroyWnd; override;
procedure WndProc(var M: TMessage); override;
public
constructor CreateNew(AOwner: TComponent; Dummy: Integer = 0); override;
property DockManager;
published
property Action;
property ActiveControl;
property AutoScroll;
property AutoSize;
property BiDiMode;
property BorderWidth;
property Color;
property Ctl3D;
property DockedHeight: Integer read FDockedHeight write SetDockedHeight
default 35;
property DockedWidth: Integer read FDockedWidth write SetDockedWidth
default 40;
property UseDockManager;
property DockSite;
property DragKind;
property DragMode;
property Edge: TAppBarEdge read FEdge write SetAppBarEdge default abeTop;
property Enabled;
property ParentFont default False;
property Font;
property HelpFile;
property HorzScrollBar;
property Icon;
property KeyPreview;
property ObjectMenuItem;
property ParentBiDiMode;
730
Listagem 24.3 Continuação
property PixelsPerInch;
property PopupMenu;
property PrintScale;
property Scaled;
property ShowHint;
property TopMost: Boolean read FTopMost write SetTopMost default False;
property VertScrollBar;
property Visible;
property OnActivate;
property OnCanResize;
property OnClick;
property OnClose;
property OnCloseQuery;
property OnConstrainedResize;
property OnCreate;
property OnDblClick;
property OnDestroy;
property OnDeactivate;
property OnDockDrop;
property OnDockOver;
property OnDragDrop;
property OnDragOver;
property OnEdgeChanged: TNotifyEvent read FOnEdgeChanged
write FOnEdgeChanged;
property OnEndDock;
property OnGetSiteInfo;
property OnHide;
property OnHelp;
property OnKeyDown;
property OnKeyPress;
property OnKeyUp;
property OnMouseDown;
property OnMouseMove;
property OnMouseUp;
property OnMouseWheel;
property OnMouseWheelDown;
property OnMouseWheelUp;
property OnPaint;
property OnResize;
property OnShortCut;
property OnShow;
property OnStartDock;
property OnUnDock;
end;
implementation
var
AppBarMsg: UINT;
FDockedWidth := 40;
inherited CreateNew(AOwner, Dummy);
ClientHeight := 35;
Width := 100;
BorderStyle := bsNone;
BorderIcons := [ ];
// configura o registro TAppBarData
FABD.cbSize := SizeOf(FABD);
FABD.uCallbackMessage := AppBarMsg;
end;
end
else
if State and ABS_ALWAYSONTOP < > 0 then
SetTopMost(True);
end;
// Enviado quando houver algo que possa afetar a posição de AppBar.
ABN_POSCHANGED:
// A barra de tarefas ou outra barra de acesso
// teve seu tamanho ou sua posição alterada.
SetAppBarPos(FABD.uEdge);
end;
end
else
inherited WndProc(M);
end;
procedure TAppBar.CreateWnd;
begin
inherited CreateWnd;
FABD.hWnd := Handle;
if not (csDesigning in ComponentState) then
begin
if SendAppBarMsg(ABM_NEW) = 0 then
raise EAppBarError.Create(‘Failed to create AppBar’);
// Inicializa a posição
SetAppBarEdge(FEdge);
end;
end;
procedure TAppBar.DestroyWnd;
begin
// Deve informar ao shel que a AppBar está sendo fechada
SendAppBarMsg(ABM_REMOVE);
inherited DestroyWnd;
end;
initialization
AppBarMsg := RegisterWindowMessage(‘DDG AppBar Message’);
end.
Usando TAppBar
Se você instalou o software encontrado no CD-ROM que acompanha este livro, o uso de TAppBar será mui-
to fácil: basta selecionar a opção AppBar da página DDG da caixa de diálogo File, New. Isso chama um
assistente que gerará uma unidade contendo um componente TAppBar.
NOTA
O Capítulo 26 ensina a criar um assistente que gera automaticamente um TAppBar. No entanto, neste capí-
tulo você pode ignorar a implementação do assistente. Basta entender que algum trabalho está sendo feito
nos bastidores para gerar a unidade e o formulário da AppBar para você.
Nessa pequena aplicação de exemplo, TAppBar é usada para criar uma barra de ferramentas de aplica-
ção que contenha botões para diversos comandos de edição: Open, Save, Cut, Copy e Paste. Os botões
manipularão um componente TMemo encontrado no formulário principal. O código-fonte dessa unidade é
mostrado na Listagem 24.4 e a Figura 24.5 mostra a aplicação em ação com o controle da AppBar encai-
xado na parte superior da tela.
unit ApBarFrm;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
AppBars, Menus, Buttons;
type
TAppBarForm = class(TAppBar)
735
Listagem 24.4 Continuação
sbOpen: TSpeedButton;
sbSave: TSpeedButton;
sbCut: TSpeedButton;
sbCopy: TSpeedButton;
sbPaste: TSpeedButton;
OpenDialog: TOpenDialog;
pmPopup: TPopupMenu;
Top1: TMenuItem;
Bottom1: TMenuItem;
Left1: TMenuItem;
Right1: TMenuItem;
N1: TMenuItem;
Exit1: TMenuItem;
procedure Right1Click(Sender: TObject);
procedure sbOpenClick(Sender: TObject);
procedure sbSaveClick(Sender: TObject);
procedure sbCutClick(Sender: TObject);
procedure sbCopyClick(Sender: TObject);
procedure sbPasteClick(Sender: TObject);
procedure Exit1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormEdgeChanged(Sender: TObject);
private
FLastChecked: TMenuItem;
procedure MoveButtons;
end;
var
AppBarForm: TAppBarForm;
implementation
uses Main;
{$R *.DFM}
{ TAppBarForm }
procedure TAppBarForm.MoveButtons;
// Esse método é aparentemente complicado, mas na prática ele apenas organiza
// os botões de modo apropriado, dependendo do lado em que a AppBar foi encaixada.
var
DeltaCenter, NewPos: Integer;
begin
if Edge in [abeTop, abeBottom] then
begin
DeltaCenter := (ClientHeight - sbOpen.Height) div 2;
sbOpen.SetBounds(10, DeltaCenter, sbOpen.Width, sbOpen.Height);
NewPos := sbOpen.Width + 20;
sbSave.SetBounds(NewPos, DeltaCenter, sbOpen.Width, sbOpen.Height);
NewPos := NewPos + sbOpen.Width + 10;
sbCut.SetBounds(NewPos, DeltaCenter, sbOpen.Width, sbOpen.Height);
NewPos := NewPos + sbOpen.Width + 10;
736
Listagem 24.4 Continuação
737
Listagem 24.4 Continuação
end.
F I G U R A 2 4 . 5 TAppBar em ação.
Vínculos do shell
O shell do Windows expõe uma série de interfaces que podem ser utilizadas para manipular diferentes
aspectos do shell. Essas interfaces são definidas na unidade ShlObj. Seria preciso um novo livro para discu-
tir em profundidade todos os objetos nessa unidade e, portanto, vamos concentrar nossos esforços em
uma das interfaces mais úteis (e mais usadas): IShellLink.
IShellLink é uma interface que permite a criação e manipulação dos vínculos (ou atalhos) do shell
nas suas aplicações. Caso esteja em dúvida, a maioria dos ícones do seu desktop provavelmente é com-
posta de vínculos do shell. Além disso, cada item no menu Send To (enviar para) local do shell ou no
menu Documents (documentos ao qual você tem acesso pelo menu Start) é um vínculo do shell. A inter-
face IShellLink é definida da seguinte maneira:
const
type
IShellLink = interface(IUnknown)
[‘{000214EE-0000-0000-C000-000000000046}’]
function GetPath(pszFile: PAnsiChar; cchMaxPath: Integer;
738 var pfd: TWin32FindData; fFlags: DWORD): HResult; stdcall;
function GetIDList(var ppidl: PItemIDList): HResult; stdcall;
function SetIDList(pidl: PItemIDList): HResult; stdcall;
function GetDescription(pszName: PAnsiChar; cchMaxName: Integer): HResult;
stdcall;
function SetDescription(pszName: PAnsiChar): HResult; stdcall;
function GetWorkingDirectory(pszDir: PAnsiChar; cchMaxPath: Integer):
HResult;
stdcall;
function SetWorkingDirectory(pszDir: PAnsiChar): HResult; stdcall;
function GetArguments(pszArgs: PAnsiChar; cchMaxPath: Integer): HResult;
stdcall;
function SetArguments(pszArgs: PAnsiChar): HResult; stdcall;
function GetHotkey(var pwHotkey: Word): HResult; stdcall;
function SetHotkey(wHotkey: Word): HResult; stdcall;
function GetShowCmd(out piShowCmd: Integer): HResult; stdcall;
function SetShowCmd(iShowCmd: Integer): HResult; stdcall;
function GetIconLocation(pszIconPath: PAnsiChar; cchIconPath: Integer;
out piIcon: Integer): HResult; stdcall;
function SetIconLocation(pszIconPath: PAnsiChar; iIcon: Integer): HResult;
stdcall;
function SetRelativePath(pszPathRel: PAnsiChar; dwReserved: DWORD):
HResult;
stdcall;
function Resolve(Wnd: HWND; fFlags: DWORD): HResult; stdcall;
function SetPath(pszFile: PAnsiChar): HResult; stdcall;
end;
NOTA
Como IshellLink e seus métodos são descritos em detalhe na ajuda online do Win32, não vamos discu-
ti-los aqui.
NOTA
Não se esqueça de que, antes de poder usar qualquer uma das funções do OLE, você deve inicializar a bi-
blioteca do COM usando a função CoInitialize( ). Quando você tiver acabado de usar o COM, deve ex-
cluí-lo chamando CoUninitialize( ). Essas funções serão chamadas pelo Delphi em uma aplicação que
use ComObj e contenha uma chamada para Application.Initialize( ). Caso contrário, você mesmo terá
de chamar essas funções.
739
Usando IShellLink
Os vínculos do shell parecem ter algum tipo de mágica: você dá um clique com o botão direito do mouse
no desktop, cria um novo atalho e alguma coisa acontece que faz com que um ícone apareça no desktop.
Na verdade, essa coisa é uma coisinha à toa, quando você sabe o que está acontecendo. Um vínculo de
shell não passa de um arquivo com uma extensão LNK que reside em algum diretório. Quando o Win-
dows é inicializado, ele procura arquivos LNK em determinados diretórios, que representam vínculos
que residem em diferentes pastas do shell. Essas pastas do shell, ou pastas especiais, incluem itens como
Network Neighborhood (ambiente de rede), Send To (enviar para), Startup (iniciar) e o desktop (área de
trabalho), entre outras coisas. O shell armazena a correspondência vínculo/pasta no Registro do Sistema
– encontradas em sua maioria abaixo da seguinte chave, caso você queira saber:
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer
➥\Shell Folders
A criação de um vínculo do shell em uma pasta especial, portanto, é apenas uma questão de colocar
um arquivo de vínculo em um determinado diretório. Em vez de escarafunchar o Registro, você pode
usar SHGetSpecialFolderPath( ) para obter o caminho de diretório para as diversas pastas especiais. Esse
método é definido da seguinte maneira:
function SHGetSpecialFolderPath(hwndOwner: HWND; lpszPath: PChar;
nFolder: Integer; fCreate: BOOL): BOOL; stdcall;
hwndOwner contém a alça de uma janela que servirá como o proprietário para qualquer caixa de diálo-
go que a função possa chamar.
lpszPath é um ponteiro de um buffer para receber o caminho. Esse buffer deve ter pelo menos o nú-
mero de caracteres registrado em MAX_PATH.
nFolder identifica a pasta especial cujo caminho você deseja obter. A Tabela 24.4 mostra os possíveis
valores para esse parâmetro e uma descrição de cada um deles.
fCreate indica se uma pasta deve ser criada, caso não exista.
Flag Descrição
Flag Descrição
741
Criando um vínculo de shell
A interface IShellLink é um encapsulamento de um objeto de vínculo com o shell, que no entanto não tem
idéia de como ler ou gravar um arquivo no disco. No entanto, os implementadores da interface IShellLink
também têm a obrigação de oferecer suporte à interface IPersistFile para fornecer o acesso ao arquivo.
IPersistFile é uma interface que fornece métodos de leitura e gravação de/para o disco e é definida da se-
guinte maneira:
type
IPersistFile = interface(IPersist)
[‘{0000010B-0000-0000-C000-000000000046}’]
function IsDirty: HResult; stdcall;
function Load(pszFileName: POleStr; dwMode: Longint): HResult;
stdcall;
function Save(pszFileName: POleStr; fRemember: BOOL): HResult;
stdcall;
function SaveCompleted(pszFileName: POleStr): HResult;
stdcall;
function GetCurFile(out pszFileName: POleStr): HResult;
stdcall;
end;
NOTA
Para obter uma descrição completa de IPersistFile e seus métodos, consulte a ajuda on-line do Win32.
Como a classe que implementa IShellLink também é obrigatória para implementar IPersistFile,
você pode usar a interface QueryInterface para consultar a instância de IshellLink de uma instância de IPer-
sistFile usando o operador as, como mostrado a seguir:
var
SL: IShellLink;
PF: IPersistFile;
begin
OleCheck(CoCreateInstance(CLSID_ShellLink, nil, CLSCTX_INPROC_SERVER,
IShellLink, SL));
PF := SL as IPersistFile;
// use PF e SL
end;
Como já dissemos, o uso de objetos da interface COM corresponde a usar os objetos normais do
Object Pascal. O código mostrado a seguir, por exemplo, cria um vínculo com o shell do desktop com a
aplicação Notepad:
procedure MakeNotepad;
const
// NOTA: Posição presumida do Notepad (Bloco de notas):
AppName = ‘c:\windows\notepad.exe’;
var
SL: IShellLink;
PF: IPersistFile;
LnkName: WideString;
begin
OleCheck(CoCreateInstance(CLSID_ShellLink, nil, CLSCTX_INPROC_SERVER,
IShellLink, SL));
742
{ Os implementadores de IShellLink têm que implementar IPersistFile }
PF := SL as IPersistFile;
{ define caminho do vínculo para o arquivo apropriado }
OleCheck(SL.SetPath(PChar(AppName)));
{ cria uma localização de caminho e um nome para o arquivo de vínculo }
LnkName := GetFolderLocation(‘Desktop’) + ‘\’ +
ChangeFileExt(ExtractFileName(AppName), ‘.lnk’);
PF.Save(PWideChar(LnkName), True); // salva arquivo de vínculo
end;
Nesse procedimento, o método SetPath( ) de IShellLink é usado para apontar o vínculo para um do-
cumento ou arquivo executável (nesse caso, o Notepad). Posteriormente, caminho e nome de arquivo
para o vínculo são criados usando o caminho retornado por GetFolderLocation(‘Desktop’) (já descrita nessa
seção) e pelo uso da função ChangeFileExt( ) para alterar a extensão de Notepad de EXE para LNK. Esse novo
nome de arquivo é armazenado em LnkName. Depois disso, o método Save( ) salva o vínculo como um ar-
quivo em disco. Como você já aprendeu, quando o procedimento termina e as instâncias de interface SL e
PF saem do escopo, suas respectivas referências são liberadas.
Dado esse registro, você pode criar funções que recuperam as definições de um determinado víncu-
lo do shell com o registro ou que definem os valores do vínculo com base nos que são indicados pelo con-
teúdo do registro. Essas funções são mostradas na Listagem 24.5; WinShell.pas é uma unidade que contém
o código completo dessas funções.
Listagem 24.5 WinShell.pas, a unidade que contém as funções que operam nos vínculos do shell
unit WinShell;
interface
type
EShellOleError = class(Exception);
TShellLinkInfo = record
PathName: string;
743
Listagem 24.5 Continuação
Arguments: string;
Description: string;
WorkingDirectory: string;
IconLocation: string;
IconIndex: integer;
ShowCmd: integer;
HotKey: word;
end;
TSpecialFolderInfo = record
Name: string;
ID: Integer;
end;
const
SpecialFolders: array[0..29] of TSpecialFolderInfo = (
(Name: ‘Alt Startup’; ID: CSIDL_ALTSTARTUP),
(Name: ‘Application Data’; ID: CSIDL_APPDATA),
(Name: ‘Recycle Bin’; ID: CSIDL_BITBUCKET),
(Name: ‘Common Alt Startup’; ID: CSIDL_COMMON_ALTSTARTUP),
(Name: ‘Common Desktop’; ID: CSIDL_COMMON_DESKTOPDIRECTORY),
(Name: ‘Common Favorites’; ID: CSIDL_COMMON_FAVORITES),
(Name: ‘Common Programs’; ID: CSIDL_COMMON_PROGRAMS),
(Name: ‘Common Start Menu’; ID: CSIDL_COMMON_STARTMENU),
(Name: ‘Common Startup’; ID: CSIDL_COMMON_STARTUP),
(Name: ‘Controls’; ID: CSIDL_CONTROLS),
(Name: ‘Cookies’; ID: CSIDL_COOKIES),
(Name: ‘Desktop’; ID: CSIDL_DESKTOP),
(Name: ‘Desktop Directory’; ID: CSIDL_DESKTOPDIRECTORY),
(Name: ‘Drives’; ID: CSIDL_DRIVES),
(Name: ‘Favorites’; ID: CSIDL_FAVORITES),
(Name: ‘Fonts’; ID: CSIDL_FONTS),
(Name: ‘Hissory’; ID: CSIDL_HISTORY),
(Name: ‘Internet’; ID: CSIDL_INTERNET),
(Name: ‘Internet Cache’; ID: CSIDL_INTERNET_CACHE),
(Name: ‘Network Neighborhood’; ID: CSIDL_NETHOOD),
(Name: ‘Network Top’; ID: CSIDL_NETWORK),
(Name: ‘Personal’; ID: CSIDL_PERSONAL),
(Name: ‘Printers’; ID: CSIDL_PRINTERS),
(Name: ‘Printer Links’; ID: CSIDL_PRINTHOOD),
(Name: ‘Programs’; ID: CSIDL_PROGRAMS),
(Name: ‘Recent Documents’; ID: CSIDL_RECENT),
(Name: ‘Send To’; ID: CSIDL_SENDTO),
(Name: ‘Start Menu’; ID: CSIDL_STARTMENU),
(Name: ‘Startup’; ID: CSIDL_STARTUP),
(Name: ‘Templates’; ID: CSIDL_TEMPLATES));
implementation
uses ComObj;
end.
746
Um método de IShellLink que ainda precisa ser explicado é o método Resolve( ). Resolve( ) deve ser
chamado depois que a interface IPersistFile de IshellLink é usada para carregar um arquivo de vínculo.
Isso pesquisa o arquivo de vínculo consultado e preenche o objeto IShellLink com os valores especifica-
dos no arquivo.
DICA
Na função GetShellLinkInfo( ), mostrada na Listagem 24.5, observe o uso da array local AStr, na qual os
valores são recuperados. Essa técnica é usada no lugar de SetLength( ) para alocar espaço para as strings
– o uso de SetLength( ) em tantas strings acarretaria fragmentação do heap da aplicação. O uso de Astr
como um intermediário impede que isso aconteça. Além disso, como o tamanho das strings só precisa ser
definido uma vez, o uso de AStr acaba sendo ligeiramente mais rápido.
FIGURA 24.6 O formulário principal de Shell Link, mostrando um dos vínculos com o desktop.
unit Main;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, ComCtrls, ExtCtrls, Spin, WinShell, Menus;
type
TMainForm = class(TForm)
Panel1: TPanel;
btnOpen: TButton;
edLink: TEdit;
btnNew: TButton;
btnSave: TButton;
Label3: TLabel;
747
Listagem 24.6 Continuação
Panel2: TPanel;
Label1: TLabel;
Label2: TLabel;
Label4: TLabel;
Label5: TLabel;
Label6: TLabel;
Label7: TLabel;
Label8: TLabel;
Label9: TLabel;
edIcon: TEdit;
edDesc: TEdit;
edWorkDir: TEdit;
edArg: TEdit;
cbShowCmd: TComboBox;
hkHotKey: THotKey;
speIcnIdx: TSpinEdit;
pnlIconPanel: TPanel;
imgIconImage: TImage;
btnExit: TButton;
MainMenu1: TMainMenu;
File1: TMenuItem;
Open1: TMenuItem;
Save1: TMenuItem;
NewLInk1: TMenuItem;
N1: TMenuItem;
Exit1: TMenuItem;
Help1: TMenuItem;
About1: TMenuItem;
edPath: TEdit;
procedure btnOpenClick(Sender: TObject);
procedure btnNewClick(Sender: TObject);
procedure edIconChange(Sender: TObject);
procedure btnSaveClick(Sender: TObject);
procedure btnExitClick(Sender: TObject);
procedure About1Click(Sender: TObject);
private
procedure GetControls(var SLI: TShellLinkInfo);
procedure SetControls(const SLI: TShellLinkInfo);
procedure ShowIcon;
procedure OpenLinkFile(const LinkFileName: String);
end;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
type
THotKeyRec = record
748
Listagem 24.6 Continuação
procedure TMainForm.ShowIcon;
{ Recupera ícone do arquivo apropriado e o mostra em IconImage }
var
HI: THandle;
IcnFile: string;
IconIndex: word;
begin
{ Obtém nome do arquivo de ícone }
IcnFile := edIcon.Text;
{ Se existiver em branco, usa o nome do exe }
if IcnFile = ‘’ then
IcnFile := edPath.Text;
{ Confere se o arquivo existe }
if FileExists(IcnFile) then
begin
IconIndex := speIcnIdx.Value;
{ Extrai ícone do arquivo }
HI := ExtractAssociatedIcon(hInstance, PChar(IcnFile), IconIndex);
{ Atribui a alça do ícone a IconImage }
imgIconImage.Picture.Icon.Handle := HI;
end;
end;
end.
751
Listagem 24.7 NewLinkU.pas, a unidade com formulário que ajuda a criar um novo vínculo
unit NewLinkU;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
Buttons, StdCtrls;
type
TNewLinkForm = class(TForm)
Label1: TLabel;
Label2: TLabel;
edLinkTo: TEdit;
btnOk: TButton;
btnCancel: TButton;
cbLocation: TComboBox;
sbOpen: TSpeedButton;
OpenDialog: TOpenDialog;
procedure sbOpenClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
end;
implementation
uses WinShell;
{$R *.DFM}
end.
Listagem 24.8 PickU.pas, a unidade com formulário que permite que o usuário escolha o local do
vínculo
unit PickU;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, FileCtrl;
type
TLinkForm = class(TForm)
lbLinkFiles: TFileListBox;
btnOk: TButton;
btnCancel: TButton;
cbLocation: TComboBox;
Label1: TLabel;
procedure lbLinkFilesDblClick(Sender: TObject);
procedure cbLocationChange(Sender: TObject);
procedure FormCreate(Sender: TObject);
end;
implementation
{$R *.DFM}
end.
Extensões do shell
Última palavra em termos de extensibilidade, o shell do Windows fornece um meio para você desenvol-
ver um código capaz de ser executado dentro do próprio namespace e do processo do shell. As extensões
do shell são implementadas como servidores COM em processo, que são criados e usados pelo shell.
NOTA
Como as extensões do shell no fundo não passam de servidores COM, você só conseguirá entender o que
são se tiver um mínimo de compreensão do que é COM. Se você não tem a menor idéia do que seja COM,
o Capítulo 23 oferece os fundamentos.
Diversos tipos de extensões do shell estão disponíveis para lidar com uma série de aspectos do shell.
Também conhecida como um manipulador, uma extensão do shell deve implementar uma ou mais inter-
faces COM. O shell oferece suporte aos seguintes tipos de extensões do shell:
754
l Manipuladores de hook de cópia implementam a interface ICopyHook. Essas extensões do shell per-
mitem que você receba notificações sempre que uma pasta é copiada, excluída, movida ou reno-
meada e para opcionalmente impedir que a operação ocorra.
l Manipuladores de menu de contexto implementam as interfaces IContextMenu e IShellExtInit.
Essas extensões do shell permitem que você adicione itens ao menu de contexto de um determi-
nado objeto de arquivo no shell.
l Manipuladores de arrastar e soltar também implementam as interfaces IContextMenu e
IShellExtInit. A implementação dessas extensões do shell é quase idêntica à dos manipuladores
de menu de contexto, exceto pelo fato de essas serem chamadas quando um usuário arrasta um
objeto para uma nova localização.
l Manipuladores de ícones implementam as interfaces IExtractIcon e IPersistFile. Os manipulado-
res de ícones permitem que você forneça diferentes ícones para múltiplas instâncias do mesmo
tipo de objeto de arquivo.
l Manipuladores de folha de propriedades implementam as interfaces IShellPropSheetExt e
IShellExtInit e permitem que você adicione páginas à caixa de diálogo associada a um tipo de ar-
quivo.
l Manipuladores de alvo de soltar implementam as interfaces DropTarget e IPersistFile. Essas exten-
sões do shell permitem que você controle o que acontece quando arrasta um objeto de shell so-
bre outro.
l Manipuladores de objeto de dados implementam as interfaces IDataObject e IPersistFile e
fornecem o objeto de dados quando arquivos estão sendo arrastados e soltos ou copiados e
colados.
1. Torne explorer.exe a aplicação host para a extensão do shell na caixa de diálogo Run, Parameters
(executar, parâmetros). Certifique-se de incluir o caminho completo (ou seja, c:\windows\explo-
rer.exe).
2. No menu Start (iniciar) do shell, selecione Shut Down (desligar). Isso chamará a caixa de diálogo
Shut Down Windows (desligar Windows).
3. Na caixa de diálogo Shut Down Windows, mantenha pressionadas as teclas Ctrl+Alt+Shift e dê um
clique no botão No (não). Isso fechará o shell sem fechar o Windows.
4. Usando Alt+Tab, volte para o Delphi e execute a extensão do shell. Isso chamará uma nova cópia
do shell executada no depurador do Delphi. Agora você pode definir pontos de interrupção no seu
código e depurar como sempre.
5. Quando você estiver pronto para fechar o Windows, poderá fazê-lo de modo apropriado sem o uso
do shell: use Ctrl+Esc para chamar a janela Tasks (tarefas) e em seguida selecione Windows, Shut-
down Windows para fechar o Windows.
755
O restante deste capítulo é dedicado a mostrar uma visão das extensões do shell que acabamos de
descrever. Você vai aprender sobre manipuladores de hook de cópia, manipuladores de menu de contex-
to e manipuladores de ícones.
O método CopyCallback( )
Como você pode ver, ICopyHook é uma interface bastante simples e implementa apenas uma função: Copy-
Callback( ). Essa função será chamada sempre que uma pasta do shell for manipulada. Os próximos pará-
grafos descrevem os parâmetros dessa função.
Wnd é a alça da janela que o manipulador de hook de cópia deve usar como o pai de qualquer janela
que ele apresente. wFunc indica a operação que está sendo executada. Isso pode ser qualquer um dos valo-
res mostrados na Tabela 24.5.
wFlags armazena os flags que controlam a operação. Esse parâmetro pode ser uma combinação dos
valores mostrados na Tabela 24.6.
757
Tabela 24.7 Os valores de wFlags de CopyCallback
Implementação de TCopyHook
Sendo um objeto que implementa uma interface com um método, não há muita coisa em TCopyHook:
type
TCopyHook = class(TComObject, ICopyHook)
protected
function CopyCallback(Wnd: HWND; wFunc, wFlags: UINT;
pszSrcFile: PAnsiChar;
dwSrcAttribs: DWORD; pszDestFile: PAnsiChar; dwDessattribs: DWORD): UINT;
stdcall;
end;
DICA
Você pode estar se perguntando por que a função MessageBox( ) da API é usada para exibir uma mensa-
gem em vez de se usar uma função do Delphi, como MessageDlg( ) ou ShowMessage( ). A razão é simples:
tamanho e eficiência. A chamada de qualquer função fora da unidade Dialogs ou Forms faria com que uma
grande parte da VCL fosse vinculada à DLL. Mantendo essas unidades fora da cláusula uses, a DLL da ex-
tensão do shell ocupa apenas 70KB.
Acredite se quiser, mas isso é tudo o que há para se falar sobre o objeto TCopyHook propriamente dito.
No entanto, ainda há um importante trabalho a ser feito antes que ele possa ser chamado algum dia: a ex-
tensão do shell deve ser registrada com o Registro do Sistema antes que possa funcionar.
Registro
Além do registro normal exigido de qualquer servidor COM, um manipulador de hook de cópia deve ter
758 uma entrada de Registro adicional em:
HKEY_CLASSES_ROOT\directory\shellex\CopyHookHandlers
Além disso, o Windows NT exige que todas as extensões do shell sejam registradas conforme as ex-
tensões do shell aprovadas em:
HKEY_LOCAL_MACHINE\ SOFTWARE\Microsoft\Windows\CurrentVersion
➡\Shell Extensions\Approved
Você pode registrar as extensões do shell de várias maneiras: elas podem ser registradas através de
um arquivo REG ou através de um programa de instalação. A DLL da extensão do shell propriamente
dita pode ser auto-registrável. Embora isso implique um pouco de trabalho extra, a melhor solução é tor-
nar cada DLL de extensão do shell auto-registrável. Isso é mais legível, pois torna sua extensão do shell
um pacote de único arquivo, auto-suficiente.
Como você aprendeu no capítulo anterior, os objetos COM são sempre criados a partir de factories
de classe. Dentro da estrutura da VCL, os objetos factory de classe também são responsáveis pelo registro
do objeto COM que criarem. Se um objeto COM requer entradas de Registro personalizadas (como é o
caso com uma extensão do shell), a definição dessas entradas é só uma questão de modificar o método
UpdateRegistry( ) da factory de classe. A Listagem 24.9 mostra a unidade CopyMain completa, que inclui um
factory de classe especializado para executar um registro personalizado.
unit CopyMain;
interface
type
TCopyHook = class(TComObject, ICopyHook)
protected
function CopyCallback(Wnd: HWND; wFunc, wFlags: UINT;
pszSrcFile: PAnsiChar; dwSrcAttribs: DWORD;
pszDestFile: PAnsiChar; dwDessattribs: DWORD): UINT; stdcall;
end;
TCopyHookFactory = class(TComObjectFactory)
protected
function GetProgID: string; override;
procedure ApproveShellExtension(Register: Boolean; const ClsID: string);
virtual;
public
procedure UpdateRegistry(Register: Boolean); override;
end;
implementation
{ TCopyHook }
MyMessage: string = ‘Are you sure you want to mess with “%s”?’;
begin
// confirma operação
Result := MessageBox(Wnd, PChar(Format(MyMessage, [pszSrcFile])),
‘D4DG Shell Extension’, MB_YESNO);
end;
{ TCopyHookFactory }
const
CLSID_CopyHook: TGUID = ‘{66CD5F60-A044-11D0-A9BF-00A024E3867F}’;
initialization
760
Listagem 24.9 Continuação
O que faz a factory da classe TcopyHookFactory funcionar é o fato de uma instância dela, não o TCom-
ObjectFactorynormal, estar sendo criada na parte initialization da unidade. A Figura 24.7 mostra o que
acontece quando você tenta renomear uma pasta no shell depois que a DLL da extensão do shell do hook
de cópia é instalada.
IShellExtInit
A interface IShellExtInit é usada para inicializar uma extensão do shell. Essa interface é definida na uni-
dade ShlObj da seguinte maneira:
type
IShellExtInit = interface(IUnknown)
[‘{000214E8-0000-0000-C000-000000000046}’]
function Initialize(pidlFolder: PItemIDList; lpdobj: IDataObject;
hKeyProgID: HKEY): HResult; stdcall;
end; 761
Initialize( ), sendo o único método dessa interface, é chamado para inicializar o manipulador do
menu de contexto. Os próximos parágrafos descrevem os parâmetros desse método.
pidlFolder é um ponteiro para uma estrutura PItemIDList (lista de identificadores de item) para a pas-
ta que contém o item cujo menu de contexto está sendo exibido. lpdobj armazena o objeto de interface
IDataObject usado para recuperar o objeto sobre o qual a ação está sendo executada. hkeyProgID contém a
chave de Registro do objeto de arquivo ou do tipo de pasta.
A implementação desse método é mostrada no código a seguir. À primeira vista, o código pode pa-
recer complexo, mas na realidade ele se reduz a três coisas: uma chamada para lpobj.GetData( ) para ob-
ter dados de IDataObject e duas chamadas para DragQueryFile( ) (uma chamada para obter o número de ar-
quivos e outra para obter o nome do arquivo). O nome do arquivo é armazenado no campo FFileName do
objeto. Veja o código a seguir:
function TContextMenu.Initialize(pidlFolder: PItemIDList; lpdobj: IDataObject;
hKeyProgID: HKEY): HResult;
var
Medium: TStgMedium;
FE: TFormatEtc;
begin
try
// Aborta a chamada se lpdobj for nil.
if lpdobj = nil then
begin
Result := E_FAIL;
Exit;
end;
with FE do
begin
cfFormat := CF_HDROP;
ptd := nil;
dwAspect := DVASPECT_CONTENT;
lindex := -1;
tymed := TYMED_HGLOBAL;
end;
// Produz os dados referecenciados pelo ponteiro IDataObject para um meio de
// armazenamento HGLOBAL no formato CF_HDROP.
Result := lpdobj.GetData(FE, Medium);
if Failed(Result) then Exit;
try
// Se apenas um arquivo está selecionado, recupera o nome do arquivo
// e o armazena em szFile. Caso contrário, aborta a chamada.
if DragQueryFile(Medium.hGlobal, $FFFFFFFF, nil, 0) = 1 then
begin
DragQueryFile(Medium.hGlobal, 0, FFileName, SizeOf(FFileName));
Result := NOERROR;
end
else
Result := E_FAIL;
finally
ReleaseStgMedium(medium);
end;
except
Result := E_UNEXPECTED;
end;
end;
762
IContextMenu
A interface IContextMenu é usada para manipular o menu pop-up associado a um arquivo no shell. Essa in-
terface é definida na unidade ShlObj da seguinte maneira:
type
IContextMenu = interface(IUnknown)
[‘{000214E4-0000-0000-C000-000000000046}’]
function QueryContextMenu(Menu: HMENU;
indexMenu, idCmdFirst, idCmdLast, uFlags: UINT): HResult; stdcall;
function InvokeCommand(var lpici: TCMInvokeCommandInfo): HResult; stdcall;
function GetCommandString(idCmd, uType: UINT; pwReserved: PUINT;
pszName: LPSTR; cchMax: UINT): HResult; stdcall;
end;
Depois que o manipulador for inicializado através da interface IShellExtInit, o próximo método a
ser chamado é IContextMenu.QueryContextMenu( ). Os parâmetros passados para esse método incluem uma
alça de menu, o índice no qual o primeiro item de menu será inserido, os valores mínimo e máximo das
IDs de item de menu e flags que indicam os atributos de menu. A implementação de TContextMenu desse
método, mostrada a seguir, adiciona um item de menu com o texto “Package Informação...” à alça de
menu passada no parâmetro Menu (observe que o valor de retorno de QueryContextMenu( ) é o índice do últi-
mo item de menu inserido mais um):
function TContextMenu.QueryContextMenu(Menu: HMENU; indexMenu, idCmdFirst,
idCmdLast, uFlags: UINT): HResult;
begin
FMenuIdx := indexMenu;
// Adiciona um item de menu ao menu de contexto
InsertMenu (Menu, FMenuIdx, MF_STRING or MF_BYPOSITION, idCmdFirst,
‘Package Info...’);
// Retorna índice do último item inserido + 1
Result := FMenuIdx + 1;
end;
O próximo método chamado pelo shell é GetCommandString( ). Esse método tem o objetivo de recu-
perar a string de comando ou de ajuda independente de linguagem de um determinado item de menu. Os
parâmetros desse método incluem o offset do item de menu, flags indicando o tipo de informação a ser
recebida, um parâmetro reservado, um buffer de string e um tamanho de buffer. A implementação de
TContextMenu desse método, mostrada a seguir, só precisa lidar com o fornecimento da string de ajuda para
o item de menu:
function TContextMenu.GetCommandString(idCmd, uType: UINT; pwReserved: PUINT;
pszName: LPSTR; cchMax: UINT): HRESULT;
begin
Result := S_OK;
try
// certifica-se de que o índice de menu está correto e de que o shell está
// solicitando a string de ajuda
if (idCmd = FMenuIdx) and ((uType and GCS_HELPTEXT) < > 0) then
// retorna string de ajuda para o item de menu
StrLCopy(pszName, ‘Get information for the selected package.’, cchMax)
else
Result := E_INVALIDARG;
except
Result := E_UNEXPECTED;
end;
end;
763
Quando você dá um clique no novo item no menu de contexto, o shell chama o método InvokeCom-
mand( ).
O método aceita um registro TCMInvokeCommandInfo como parâmetro. Esse registro é definido na
unidade ShlObj da seguinte maneira:
type
PCMInvokeCommandInfo = ^TCMInvokeCommandInfo;
TCMInvokeCommandInfo = packed record
cbSize: DWORD; { deve ser SizeOf(TCMInvokeCommandInfo) }
fMask: DWORD; { qualquer combinação de CMIC_MASK_* }
hwnd: HWND; { pode ser NULL (janela de proprietário ausente) }
lpVerb: LPCSTR; { uma string de MAKEINTRESOURCE(idOffset) }
lpParameters: LPCSTR; { pode ser NULL (indicando ausência de parâmetro) }
lpDirectory: LPCSTR; { pode ser NULL (indicando ausência de diretório específico) }
nShow: Integer; { um dos valores SW_ da API ShowWindow( ) }
dwHotKey: DWORD;
hIcon: THandle;
end;
A palavra baixa ou o campo lpVerb conterá o índice do item de menu selecionado. Veja a seguir a im-
plementação desse método:
function TContextMenu.InvokeCommand(var lpici: TCMInvokeCommandInfo): HResult;
begin
Result := S_OK;
try
// Certifica-se de que não estamos sendo chamados por uma aplicação
if HiWord(Integer(lpici.lpVerb)) < > 0 then
begin
Result := E_FAIL;
Exit;
end;
// Executa o comando especificado por lpici.lpVerb.
// Retorna E_INVALIDARG se recebermos um número de argumento inválido.
if LoWord(lpici.lpVerb) = FMenuIdx then
ExecutePackInfoApp(FFileName, lpici.hwnd)
else
Result := E_INVALIDARG;
except
MessageBox(lpici.hwnd, ‘Error obtaining package information.’, ‘Error’,
MB_OK or MB_ICONERROR);
Result := E_FAIL;
end;
end;
Se tudo correr bem, a função ExecutePackInfoApp( ) é chamada para iniciar a aplicação PackInfo.exe,
que exibe diversas informações sobre um pacote. No entanto, não vamos entrar nas particularidades des-
sa aplicação agora; ela é discutida em detalhes no Capítulo 13.
Registro
Os manipuladores de menu de contexto devem ser registrados em
HKEY_CLASSES_ROOT\<file type>\shellex\ContextMenuHandlers
no Registro do Sistema. Seguindo o modelo da extensão de hook de cópia, a capacidade de registro é adi-
cionada à DLL criando um descendente de TComObject especializado. O objeto é mostrado na Listagem
24.10, juntamente com todo o código-fonte da unidade que contém TContextMenu. A Figura 24.9 mostra o
menu local do arquivo BPL com o novo item, e a Figura 24.10 mostra a janela PackInfo.exe do modo
764 como é chamada pelo manipulador do menu de contexto.
FIGURA 24.9 O manipulador de menu de contexto em ação.
unit ContMain;
interface
type
TContextMenu = class(TComObject, IContextMenu, IShellExtInit)
private
FFileName: array[0..MAX_PATH] of char;
FMenuIdx: UINT;
protected
// Métodos de IContextMenu
function QueryContextMenu(Menu: HMENU; indexMenu, idCmdFirst, idCmdLast,
uFlags: UINT): HResult; stdcall;
function InvokeCommand(var lpici: TCMInvokeCommandInfo): HResult; stdcall;
function GetCommandString(idCmd, uType: UINT; pwReserved: PUINT;
pszName: LPSTR; cchMax: UINT): HResult; stdcall;
// Método de IShellExtInit 765
Listagem 24.10 Continuação
TContextMenuFactory = class(TComObjectFactory)
protected
function GetProgID: string; override;
procedure ApproveShellExtension(Register: Boolean; const ClsID: string);
virtual;
public
procedure UpdateRegistry(Register: Boolean); override;
end;
implementation
{ TContextMenu }
{ TContextMenu.IContextMenu }
{ TContextMenu.IShellExtInit }
Exit;
end;
with FE do
begin
cfFormat := CF_HDROP;
ptd := nil;
dwAspect := DVASPECT_CONTENT;
lindex := -1;
tymed := TYMED_HGLOBAL;
end;
// Produz os dados referenciados pelo ponteiro IDataObject para um meio
// de armazenamento HGLOBAL no formato CF_HDROP.
Result := lpdobj.GetData(FE, Medium);
if Failed(Result) then Exit;
try
// Se apenas um arquivo for selecionado, recupera o nome do arquivo
// e o armazena em szFile. Caso contrário, aborta a chamada.
if DragQueryFile(Medium.hGlobal, $FFFFFFFF, nil, 0) = 1 then
begin
DragQueryFile(Medium.hGlobal, 0, FFileName, SizeOf(FFileName));
Result := NOERROR;
end
else
Result := E_FAIL;
finally
ReleaseStgMedium(medium);
end;
except
Result := E_UNEXPECTED;
end;
end;
{ TContextMenuFactory }
CreateRegKey(‘BorlandPackageLibrary\shellex\ContextMenuHandlers\’ +
ClassName, ‘’, ClsID);
end
else begin
DeleteRegKey(‘.bpl’);
DeleteRegKey(‘BorlandPackageLibrary\shellex\ContextMenuHandlers\’ +
ClassName);
end;
end;
const
CLSID_CopyHook: TGUID = ‘{7C5E74A0-D5E0-11D0-A9BF-E886A83B9BE5}’;
initialization
TContextMenuFactory.Create(ComServer, TContextMenu, CLSID_CopyHook,
‘D4DG_ContextMenu’, ‘D4DG Context Menu Shell Extension Example’,
ciMultiInstance, tmApartment);
end.
Manipuladores de ícones
Os manipuladores de ícones permitem que diferentes ícones sejam usados em múltiplas instâncias do
mesmo tipo de arquivo. Nesse exemplo, o objeto manipulador de ícones TIconHandler fornece diferentes
ícones para diferentes tipos de arquivos Borland Package (BPL). Dependendo de um pacote ser runtime,
em tempo de projeto, ambos ou nenhum deles, um ícone diferente será exibido em uma pasta do shell.
Flags de pacote
Antes de obter implementações das interfaces necessárias para essa extensão do shell, separe um momen-
to para examinar o método que determina o tipo de um determinado arquivo de pacote. O método re-
torna TPackType, que é definida da seguinte maneira:
TPackType = (ptDesign, ptDesignRun, ptNone, ptRun);
769
Veja o método a seguir:
function TIconHandler.GetPackageType: TPackType;
var
PackMod: HMODULE;
PackFlags: Integer;
begin
// Como só precisamos obter os recursos do pacote,
// LoadLibraryEx com LOAD_LIBRARY_AS_DATAFILE fornece um meio veloz
// e eficiente para carregar o pacote.
PackMod := LoadLibraryEx(PChar(FFileName), 0, LOAD_LIBRARY_AS_DATAFILE);
if PackMod = 0 then
begin
Result := ptNone;
Exit;
end;
try
GetPackageInfo(PackMod, nil, PackFlags, PackInfoProc);
finally
FreeLibrary(PackMod);
end;
// remove todas as máscaras, exceto os flags de execução e projeto, e
// retorna o resultado
case PackFlags and (pfDesignOnly or pfRunOnly) of
pfDesignOnly: Result := ptDesign;
pfRunOnly: Result := ptRun;
pfDesignOnly or pfRunOnly: Result := ptDesignRun;
else
Result := ptNone;
end;
end;
Esse método funciona chamando o método GetPackageInfo( ) da unidade SysUtils para obter os flags
de pacote. Um ponto interessante a ser observado a respeito da otimização do desempenho é que a fun-
ção LoadLibraryEx( ) da API é chamada, e não o procedimento LoadPackage( ) do Delphi, para carregar a
biblioteca de pacotes. Internamente, o procedimento LoadPackage( ) chama a API LoadLibrary( ) para car-
regar a BPL e, em seguida, chama InitializePackage( ) para executar o código de inicialização de cada
uma das unidades no pacote. Como tudo o que queremos é obter os flags de pacote e, como os flags resi-
dem em um recurso vinculado à BPL, podemos carregar com segurança o pacote com LoadLibraryEx( )
usando o flag LOAD_LIBRARY_AS_DATAFILE.
IPersistFile = interface(IPersist)
770 [‘{0000010B-0000-0000-C000-000000000046}’]
function IsDirty: HResult; stdcall;
function Load(pszFileName: POleStr; dwMode: Longint): HResult; stdcall;
function Save(pszFileName: POleStr; fRemember: BOOL): HResult; stdcall;
function SaveCompleted(pszFileName: POleStr): HResult; stdcall;
function GetCurFile(out pszFileName: POleStr): HResult; stdcall;
end;
Embora aparentemente isso pareça dar muito trabalho, as aparências enganam; na verdade, apenas
dois desses métodos têm que ser implementados. O primeiro arquivo que deve ser implementado é IPer-
sistFile.Load( ). Esse é o método que é chamado para inicializar as extensões do shell e nele você deve
salvar o nome do arquivo passado pelo parâmetro pszFileName. Veja a seguir a implementação de TExtract-
Icon desse método:
Flag Significado
GIL_DONTCACHE Os bits da imagem física desse ícone não devem ser colocados em cache pelo
responsável pela chamada. Essa diferença deve ser levada em consideração, pois um
flag GIL_DONTCACHELOCATION pode ser introduzido em futuras versões do shell.
GIL_NOTFILENAME O local não é um par nome de arquivo/índice. Os responsáveis pela chamada que
decidem extrair o ícone do local devem chamar o método IExtractIcon.Extract( )
desse objeto para obter as imagens de ícone desejadas.
GIL_PERCLASS Todos os objetos dessa classe têm o mesmo ícone. Esse flag é usado internamente
pelo shell. Geralmente, as implementações de IextractIcon não exigem esse flag, pois
ele implica que um manipulador de ícone não é necessário para resolver o ícone para
cada objeto. O método recomendado para implementação de ícones por classe é
registrar um ícone-padrão para a classe.
GIL_PERINSTANCE Cada objeto dessa classe tem seu próprio ícone. Esse flag é usado internamente pelo
shell para manipular casos como setup.exe, onde mais de um objeto com nomes
idênticos podem ser conhecidos do shell e usam diferentes ícones. Implementações
típicas de IExtractIcon não exigem esse flag.
GIL_SIMULATEDOC O responsável pela chamada deve criar um ícone de documento usando o ícone
especificado.
771
A implementação de TIconHandler para GetIconLocation( ) é mostrada a seguir:
function TIconHandler.GetIconLocation(uFlags: UINT; szIconFile: PAnsiChar;
cchMax: UINT; out piIndex: Integer; out pwFlags: UINT): HResult;
begin
Result := S_OK;
try
// retorna essa DLL para o nome de módulo localizar ícone
GetModuleFileName(HInstance, szIconFile, cchMax);
// instrui o shell a não fazer cache dos bits de imagem, caso o ícone mude
// e que cada instância pode ter seu próprio ícone
pwFlags := GIL_DONTCACHE or GIL_PERINSTANCE;
// índice do ícone coincide com TPackType
piIndex := Ord(GetPackageType);
except
// se houve um erro, usa o ícone de pacote padrão
piIndex := Ord(ptNone);
end;
end;
Os ícones são vinculados à DLL da extensão do shell como um arquivo de recurso e, portanto, o
nome do arquivo atual, retornado por GetModuleFileName( ), é escrito no buffer szIconFile. Além disso, os
ícones são organizados de um modo que o índice de um tipo de pacote corresponda ao índice do tipo de
pacote na enumeração TPackType e, portanto, o valor de retorno de GetPackageType( ) seja atribuído a piIn-
dex.
Registro
Os manipuladores de ícone devem ser registrados na chave do Registro
HKEY_CLASSES_ROOT\<file type>\shellex\IconHandler.
Mais uma vez, um descendente de TComObjectFactory é criado para lidar com o registro dessa exten-
são do shell. Isso é mostrado na Listagem 24.11, juntamente com o restante do código-fonte do manipu-
lador de ícones.
A Figura 24.11 mostra uma pasta do shell contendo pacotes de diferentes tipos. Observe os diferen-
tes ícones de pacotes.
772
Listagem 24.11 IconMain.pas, a unidade principal de implementação de manipulador de ícone
unit IconMain;
interface
type
TPackType = (ptDesign, ptDesignRun, ptNone, ptRun);
TIconHandlerFactory = class(TComObjectFactory)
protected
function GetProgID: string; override;
procedure ApproveShellExtension(Register: Boolean; const ClsID: string);
virtual;
public
procedure UpdateRegistry(Register: Boolean); override;
end;
implementation
{ TIconHandler }
PackMod: HMODULE;
PackFlags: Integer;
begin
// Como só precisamos ter acesso aos recursos do pacote, LoadLibraryEx
// com LOAD_LIBRARY_AS_DATAFILE fornecem um meio de acesso rápido
// e eficiente para carregar o pacote.
PackMod := LoadLibraryEx(PChar(FFileName), 0, LOAD_LIBRARY_AS_DATAFILE);
if PackMod = 0 then
begin
Result := ptNone;
Exit;
end;
try
GetPackageInfo(PackMod, nil, PackFlags, PackInfoProc);
finally
FreeLibrary(PackMod);
end;
// elimina a máscara de tudo, exceto os flags de execução e projeto, e
// retorna o resultado
case PackFlags and (pfDesignOnly or pfRunOnly) of
pfDesignOnly: Result := ptDesign;
pfRunOnly: Result := ptRun;
pfDesignOnly or pfRunOnly: Result := ptDesignRun;
else
Result := ptNone;
end;
end;
{ TIconHandler.IExtractIcon }
{ TIconHandler.IPersist }
{ TIconHandler.IPersistFile }
{ TIconHandlerFactory }
var
ClsID: string;
begin
ClsID := GUIDToString(ClassID);
inherited UpdateRegistry(Register);
ApproveShellExtension(Register, ClsID);
if Register then
begin
// deve registar .bpl como um tipo de arquivo
CreateRegKey(‘.bpl’, ‘’, ‘BorlandPackageLibrary’);
// registra essa DLL como um manipulador de ícones para arquivos .bpl
CreateRegKey(‘BorlandPackageLibrary\shellex\IconHandler’, ‘’, ClsID);
end
else begin
DeleteRegKey(‘.bpl’);
DeleteRegKey(‘BorlandPackageLibrary\shellex\IconHandler’);
end;
end;
const
CLSID_IconHandler: TGUID = ‘{ED6D2F60-DA7C-11D0-A9BF-90D146FC32B3}’;
initialization
TIconHandlerFactory.Create(ComServer, TIconHandler, CLSID_IconHandler,
‘D4DG_IconHandler’, ‘D4DG Icon Handler Shell Extension Example’,
ciMultiInstance, tmApartment);
end.
Resumo
Este capítulo fornece todos os diferentes aspectos da extensão do shell do Windows: ícones de notificação
da bandeja, AppBars, vínculos do shell e uma série de extensões do shell. Ele é uma continuação do conhe-
cimento que você obteve no capítulo anterior, quando trabalhou com COM e ActiveX. No próximo capí-
tulo, você aumentará ainda mais seus conhecimentos ao aprender a desenvolver controles ActiveX.
776
Criação de controles CAPÍTULO
ActiveX
25
NE STE C AP ÍT UL O
l Por que criar controles ActiveX? 778
l Criação de um controle ActiveX 778
l ActiveForms 817
l Adicionando propriedades aos ActiveForms 818
l ActiveX na Web 825
l Resumo 836
Para muitos programadores, a capacidade de criar facilmente controles ActiveX é um dos mais podero-
sos recursos que o Delphi traz à mesa. O ActiveX é um padrão para controles independentes da lingua-
gem de programação, que podem funcionar em uma série de ambientes, como Delphi, C++Builder, Vi-
sual Basic e Internet Explorer. Esses controles podem ser tão simples como um controle de texto estático
ou tão complexos como uma planilha ou um processador de textos totalmente funcional. Tradicional-
mente, os controles ActiveX são bastante complicados e difíceis de escrever, mas o Delphi traz a criação
de controles ActiveX para as massas, permitindo que você converta um formulário ou componente VCL
relativamente fácil de criar em um controle ActiveX.
Este capítulo não tem a pretensão de esgotar a discussão sobre controles ActiveX – tema esse que
justificaria a produção de um livro. O que este capítulo demonstrará é como funciona a criação de con-
troles ActiveX no Delphi e como usar as estruturas e os assistentes do Delphi para fazer com que os con-
troles ActiveX criados pelo Delphi funcionem para você.
NOTA
A capacidade de criar controles ActiveX só é fornecida pelas edições Professional e Enterprise do Delphi.
Esse assistente permite que você escolha uma classe de controle da VCL para encapsular como um
controle ActiveX. Além disso, ele permite que você especifique o nome da classe de controle ActiveX, o
nome do arquivo que conterá a implementação do novo controle ActiveX e o nome do projeto no qual o
controle ActiveX residirá.
NOTA
Embora o assistente de ActiveX não permita que você gere automaticamente um controle ActiveX a partir de
um controle não-TWinControl, é possível escrever esse tipo de controle manualmente usando a estrutura
DAX (Delphi ActiveX).
779
Controles VCL no ActiveX Control Wizard
Se você examinar a lista de controles VCL na caixa de combinação drop-down no ActiveX Control Wi-
zard, perceberá que nem todos os componentes VCL são encontrados na lista. Um controle VCL deve
atender a três critérios para ser listado no assistente:
l O controle VCL deve residir em um pacote de projeto atualmente instalado (ou seja, deve estar na
Component Palette – paleta de componentes).
l O controle VCL deve descender de TWinControl. Atualmente, os controles sem janela não podem
ser encapsulados como controles ActiveX.
l O controle VCL não deve ter sido excluído da lista com o procedimento RegisterNonActiveX( ). Re-
gisterNonActiveX( ) é descrito com detalhes na ajuda on-line do Delphi.
Muitos componentes padrão da VCL são excluídos da lista pelo fato de não fazerem sentido
como controles ActiveX ou por exigirem um trabalho fora do escopo do assistente para que funcionem
como controles ActiveX. TDBGrid é um bom exemplo de um controle VCL que não faz sentido como um
controle ActiveX; ele requer outra VCL (TDataSource) como uma propriedade para funcionar, o que não
é possível quando se usa um ActiveX. TTreeView é um exemplo de um controle que exigiria um trabalho
que vai muito além do assistente para ser encapsulado como um controle ActiveX, pois seria difícil de
representar os nós de TTreeView em ActiveX.
NOTA
A descrição da etapa 3 suscita a seguinte questão: o que constitui uma propriedade, um método ou um
evento qualificado para inclusão na biblioteca de tipos? Para se qualificarem para a inclusão na biblioteca
de tipos, as propriedades devem ser de um tipo compatível com Automation e os parâmetros e valores de
retorno dos métodos e eventos também devem ser de um tipo compatível com Automation. Você viu no Ca-
pítulo 23 que os tipos compatíveis com Automation são Byte, SmallInt, Integer, Single, Double, Currency,
TDateTime, WideString, WordBool, PSafeArray, TDecimal OleVariant, IUnknown e Idispatch.
No entanto, há exceções a essa regra. Além dos tipos compatíveis com Automation, os parâmetros do tipo
TStrings, TPicture e TFont também são permitidas. Para esses tipos, o assistente empregará objetos adaptado-
res especiais que permitem ser envolvidos com um IDispatch ou uma dispinterface compatível com ActiveX.
4. Uma vez que todas as propriedades, métodos e eventos qualificados tenham sido adicionados, o
editor da biblioteca de tipos gera um arquivo que é uma conversão Object Pascal do conteúdo da bi-
blioteca de tipos.
5. Posteriormente, o assistente gera o arquivo de implementação para o controle ActiveX. Esse arqui-
vo de implementação contém um objeto TActiveXControl que implementa a interface descrita na bi-
blioteca de tipos. O assistente escreve automaticamente encaminhadores para as propriedades e mé-
todos de interface. Esses métodos encaminhadores encaminham chamadas de método do wrapper
do controle ActiveX no controle e encaminham eventos do controle VCL para o controle ActiveX.
Para ajudar a ilustrar o que estamos descrevendo aqui, fornecemos as listagens a seguir. Elas perten-
cem a um projeto de controle ActiveX criado a partir de um controle VCL TMemo. Esse projeto foi salvo
como Memo.dpr. A Listagem 25.1 mostra o arquivo de projeto, a Listagem 25.2 mostra o arquivo de biblio-
teca de tipos e a Listagem 25.3 mostra o arquivo de implementação gerado para o controle. Além disso, a
Figura 25.3 mostra o conteúdo do editor de biblioteca de tipos.
library Memo;
uses
ComServ,
Memo_TLB in ‘Memo_TLB.pas’,
MemoImpl in ‘MemoImpl.pas’ {MemoX: CoClass},
About in ‘About.pas’ {MemoXAbout};
{$E ocx}
exports
DllGetClassObject,
DllCanUnloadNow,
DllRegisterServer,
DllUnregisterServer;
{$R *.TLB}
{$R *.RES}
begin
end.
781
F I G U R A 2 5 . 3 Memo, como aparece no editor de biblioteca de tipos.
unit Memo_TLB;
// ************************************************************************ //
// ATENÇÃO
// -----
// Os tipos declarados neste arquivo foram gerados da leitura de dados de uma
// Type Library. Se essa biblioteca de tipos for reimportada explícita ou
// indiretamente (via outra biblioteca de tipos que faça referência a essa
// biblioteca de tipos), ou o comando ‘Refresh’ do Type Library Editor for
// ativado durante a edição da Type Library, o conteúdo deste arquivo será
// gerado novamente e todas as modificações manuais serão perdidas.
// ************************************************************************ //
// *************************************************************************//
// NOTA:
// Itens guardados por $IFDEF_LIVE_SERVER_AT_DESIGN_TIME são usados por
// propriedades que retornam objetos que podem precisar ser explicitamente
// criados através de uma chamada de função anterior a qualquer acesso
// através da propriedade. Esses item foram desativados para impedir
// o uso acidental a partir do inspector de objeto.
// Você pode ativá-los definindo LIVE_SERVER_AT_DESIGN_TIME ou removendo-os
// seletivamente dos blocos $IFDEF. No entanto, esses itens devem ser criados
// programaticamente através de um método apropriado da CoClass antes que
// possam ser usados.
// ************************************************************************ //
// Type Lib: X:\work\d5dg\code\Ch25\Memo\Memo.tlb (1)
// IID\LCID: {0DB4686F-09C5-11D2-AE5C-00A024E3867F}\0
// Helpfile:
// DepndLst:
// (1) v2.0 stdole, (C:\WINDOWS\SYSTEM\STDOLE2.TLB)
// (2) v4.0 StdVCL, (C:\WINDOWS\SYSTEM\STDVCL40.DLL)
// ************************************************************************ //
{$TYPEDADDRESS OFF} // Compila a unidade sem ponteiros de tipo verificado.
interface
782
Listagem 25.2 Continuação
// *********************************************************************//
// GUIDS declaradas na TypeLibrary. Os seguintes prefixos são usados:
// Type Libraries : LIBID_xxxx
// CoClasses : CLASS_xxxx
// DISPInterfaces : DIID_xxxx
// Interfaces não DISP: IID_xxxx
// *********************************************************************//
const
// Versões principal e secundária de TypeLibrary
MemoMajorVersion = 1;
MemoMinorVersion = 0;
// *********************************************************************//
// Declaração de enumerações definidas na Type Library
// *********************************************************************//
// Constantes para enum TxAlignment
type
TxAlignment = TOleEnum;
const
taLeftJustify = $00000000;
taRightJustify = $00000001;
taCenter = $00000002;
type
// *********************************************************************//
// Encaminha declaração de tipos definidos em TypeLibrary
// *********************************************************************//
IMemoX = interface;
IMemoXDisp = dispinterface;
IMemoXEvents = dispinterface;
// *********************************************************************//
// Declaração de CoClasses definidas em Type Library
// (NOTA: Aqui, mapeamos cada CoClass para sua interface padrão)
// *********************************************************************//
MemoX = IMemoX;
// *********************************************************************//
// Interface: IMemoX
// Flags: (4416) Dual OleAutomation Dispatchable
784
Listagem 25.2 Continuação
// GUID: {0DB46870-09C5-11D2-AE5C-00A024E3867F}
// *********************************************************************//
IMemoX = interface(IDispatch)
[‘{0DB46870-09C5-11D2-AE5C-00A024E3867F}’]
function Get_Alignment: TxAlignment; safecall;
procedure Set_Alignment(Value: TxAlignment); safecall;
function Get_BiDiMode: TxBiDiMode; safecall;
procedure Set_BiDiMode(Value: TxBiDiMode); safecall;
function Get_BorderStyle: TxBorderStyle; safecall;
procedure Set_BorderStyle(Value: TxBorderStyle); safecall;
function Get_Color: OLE_COLOR; safecall;
procedure Set_Color(Value: OLE_COLOR); safecall;
function Get_Ctl3D: WordBool; safecall;
procedure Set_Ctl3D(Value: WordBool); safecall;
function Get_DragCursor: Smallint; safecall;
procedure Set_DragCursor(Value: Smallint); safecall;
function Get_DragMode: TxDragMode; safecall;
procedure Set_DragMode(Value: TxDragMode); safecall;
function Get_Enabled: WordBool; safecall;
procedure Set_Enabled(Value: WordBool); safecall;
function Get_Font: IFontDisp; safecall;
procedure _Set_Font(const Value: IFontDisp); safecall;
procedure Set_Font(var Value: IFontDisp); safecall;
function Get_HideSelection: WordBool; safecall;
procedure Set_HideSelection(Value: WordBool); safecall;
function Get_ImeMode: TxImeMode; safecall;
procedure Set_ImeMode(Value: TxImeMode); safecall;
function Get_ImeName: WideString; safecall;
procedure Set_ImeName(const Value: WideString); safecall;
function Get_MaxLength: Integer; safecall;
procedure Set_MaxLength(Value: Integer); safecall;
function Get_OEMConvert: WordBool; safecall;
procedure Set_OEMConvert(Value: WordBool); safecall;
function Get_ParentColor: WordBool; safecall;
procedure Set_ParentColor(Value: WordBool); safecall;
function Get_ParentCtl3D: WordBool; safecall;
procedure Set_ParentCtl3D(Value: WordBool); safecall;
function Get_ParentFont: WordBool; safecall;
procedure Set_ParentFont(Value: WordBool); safecall;
function Get_ReadOnly: WordBool; safecall;
procedure Set_ReadOnly(Value: WordBool); safecall;
function Get_ScrollBars: TxScrollStyle; safecall;
procedure Set_ScrollBars(Value: TxScrollStyle); safecall;
function Get_Visible: WordBool; safecall;
procedure Set_Visible(Value: WordBool); safecall;
function Get_WantReturns: WordBool; safecall;
procedure Set_WantReturns(Value: WordBool); safecall;
function Get_WantTabs: WordBool; safecall;
procedure Set_WantTabs(Value: WordBool); safecall;
function Get_WordWrap: WordBool; safecall;
procedure Set_WordWrap(Value: WordBool); safecall;
function GetControlsAlignment: TxAlignment; safecall;
procedure Clear; safecall;
785
Listagem 25.2 Continuação
// *********************************************************************//
// DispIntf: IMemoXDisp
// Flags: (4416) Dual OleAutomation Dispatchable
// GUID: {0DB46870-09C5-11D2-AE5C-00A024E3867F}
// *********************************************************************//
IMemoXDisp = dispinterface
[‘{0DB46870-09C5-11D2-AE5C-00A024E3867F}’]
property Alignment: TxAlignment dispid 1;
property BiDiMode: TxBiDiMode dispid 2;
property BorderStyle: TxBorderStyle dispid 3;
property Color: OLE_COLOR dispid -501;
property Ctl3D: WordBool dispid 4;
property DragCursor: Smallint dispid 5;
property DragMode: TxDragMode dispid 6;
property Enabled: WordBool dispid -514;
property Font: IFontDisp dispid -512;
property HideSelection: WordBool dispid 7;
property ImeMode: TxImeMode dispid 8;
property ImeName: WideString dispid 9;
property MaxLength: Integer dispid 10;
property OEMConvert: WordBool dispid 11;
property ParentColor: WordBool dispid 12;
property ParentCtl3D: WordBool dispid 13;
property ParentFont: WordBool dispid 14;
property ReadOnly: WordBool dispid 15;
property ScrollBars: TxScrollStyle dispid 16;
property Visible: WordBool dispid 17;
property WantReturns: WordBool dispid 18;
property WantTabs: WordBool dispid 19;
property WordWrap: WordBool dispid 20;
function GetControlsAlignment: TxAlignment; dispid 21;
procedure Clear; dispid 22;
procedure ClearSelection; dispid 23;
procedure CopyToClipboard; dispid 24;
procedure CutToClipboard; dispid 25;
procedure PasteFromClipboard; dispid 27;
procedure Undo; dispid 28;
procedure ClearUndo; dispid 29;
787
Listagem 25.2 Continuação
// *********************************************************************//
// DispIntf: IMemoXEvents
// Flags: (4096) Dispatchable
// GUID: {0DB46872-09C5-11D2-AE5C-00A024E3867F}
// *********************************************************************//
IMemoXEvents = dispinterface
[‘{0DB46872-09C5-11D2-AE5C-00A024E3867F}’]
procedure OnChange; dispid 1;
procedure OnClick; dispid 2;
procedure OnDblClick; dispid 3;
procedure OnKeyPress(var Key: Smallint); dispid 9;
end;
// *********************************************************************//
// Declaração da classe OLE Control Proxy
// Nome do controle : TMemoX
// String de ajuda : MemoX Control
// Interface padrão : IMemoX
// Def. Intf. DISP? : Não
// Interface de evento : IMemoXEvents
// TypeFlags : (34) pode criar controle
// *********************************************************************//
TMemoXOnKeyPress = procedure(Sender: TObject; var Key: Smallint) of object;
TMemoX = class(TOleControl)
private
FOnChange: TNotifyEvent;
FOnClick: TNotifyEvent;
FOnDblClick: TNotifyEvent;
FOnKeyPress: TMemoXOnKeyPress;
FIntf: IMemoX;
788
Listagem 25.2 Continuação
procedure Register;
implementation
uses ComObj;
procedure TMemoX.InitControlData;
const
CEventDispIDs: array [0..3] of DWORD = (
$00000001, $00000002, $00000003, $00000009);
CTFontIDs: array [0..0] of DWORD = (
$FFFFFE00);
790
Listagem 25.2 Continuação
CControlData: TControlData2 = (
ClassID: ‘{0DB46874-09C5-11D2-AE5C-00A024E3867F}’;
EventIID: ‘{0DB46872-09C5-11D2-AE5C-00A024E3867F}’;
EventCount: 4;
EventDispIDs: @CEventDispIDs;
LicenseKey: nil (*HR:$80040154*);
Flags: $0000002D;
Version: 401;
FontCount: 1;
FontIDs: @CTFontIDs);
begin
ControlData := @CControlData;
TControlData2(CControlData).FirstEventOfs := Cardinal(@@FOnChange) –
Cardinal(Self);
end;
procedure TMemoX.CreateControl;
procedure DoCreate;
begin
FIntf := IUnknown(OleObject) as IMemoX;
end;
begin
if FIntf = nil then DoCreate;
end;
procedure TMemoX.Clear;
begin
DefaultInterface.Clear;
end;
procedure TMemoX.ClearSelection;
begin
DefaultInterface.ClearSelection;
end;
procedure TMemoX.CopyToClipboard;
begin
DefaultInterface.CopyToClipboard;
end;
791
Listagem 25.2 Continuação
procedure TMemoX.CutToClipboard;
begin
DefaultInterface.CutToClipboard;
end;
procedure TMemoX.PasteFromClipboard;
begin
DefaultInterface.PasteFromClipboard;
end;
procedure TMemoX.Undo;
begin
DefaultInterface.Undo;
end;
procedure TMemoX.ClearUndo;
begin
DefaultInterface.ClearUndo;
end;
procedure TMemoX.SelectAll;
begin
DefaultInterface.SelectAll;
end;
procedure TMemoX.InitiateAction;
begin
DefaultInterface.InitiateAction;
end;
procedure TMemoX.AboutBox;
begin
DefaultInterface.AboutBox;
end;
procedure Register;
begin
RegisterComponents(‘ActiveX’,[TMemoX]);
end;
end.
NOTA
Se você examinar cuidadosamente o código da Listagem 25.2, perceberá que, além das informações da
biblioteca de tipos, Memo_TLB.pas contém uma classe chamada TMemoX, que é o wrapper TOleControl do
controle ActiveX. Isso permite que você adicione um controle ActiveX criado pelo Delphi à paleta simples-
mente adicionando a unidade xxx_TLB gerada para um pacote de projeto.
unit MemoImpl;
interface
uses
Windows, ActiveX, Classes, Controls, Graphics, Menus, Forms, StdCtrls,
ComServ, StdVCL, AXCtrls, Memo_TLB;
type
TMemoX = class(TActiveXControl, IMemoX)
private
{ Declarações privadas }
FDelphiControl: TMemo;
FEvents: IMemoXEvents;
procedure ChangeEvent(Sender: TObject);
procedure ClickEvent(Sender: TObject); 793
Listagem 25.3 Continuação
implementation
{ TMemoX }
procedure TMemoX.InitializeControl;
begin
FDelphiControl := Control as TMemo;
FDelphiControl.OnChange := ChangeEvent;
FDelphiControl.OnClick := ClickEvent;
FDelphiControl.OnDblClick := DblClickEvent;
FDelphiControl.OnKeyPress := KeyPressEvent;
end;
Result := FDelphiControl.Ctl3D;
end;
797
Listagem 25.3 Continuação
procedure TMemoX.AboutBox;
begin
ShowMemoXAbout;
end; 799
Listagem 25.3 Continuação
procedure TMemoX.Clear;
begin
FDelphiControl.Clear;
end;
procedure TMemoX.ClearSelection;
begin
FDelphiControl.ClearSelection;
end;
procedure TMemoX.ClearUndo;
begin
FDelphiControl.ClearUndo;
end;
procedure TMemoX.CopyToClipboard;
begin
FDelphiControl.CopyToClipboard;
end;
procedure TMemoX.CutToClipboard;
begin
FDelphiControl.CutToClipboard;
end;
procedure TMemoX.InitiateAction;
begin
FDelphiControl.InitiateAction;
end;
procedure TMemoX.PasteFromClipboard;
begin
FDelphiControl.PasteFromClipboard;
end;
procedure TMemoX.SelectAll;
begin
FDelphiControl.SelectAll;
end;
procedure TMemoX.Undo;
begin
FDelphiControl.Undo;
end;
begin
TempKey := Smallint(Key);
if FEvents < > nil then FEvents.OnKeyPress(TempKey);
Key := Char(TempKey);
end;
initialization
TActiveXControlFactory.Create(ComServer, TMemoX, TMemo, Class_MemoX, 1, ‘’,
0, tmApartment);
end.
Não há dúvida de que as Listagens 25.1 a 25.3 contêm um bocado de código. Às vezes, o volume de
código pode fazer com que alguma coisa pareça difícil e cansativa; no entanto, se você olhar atentamen-
te, verá que esses arquivos não têm nada de especial. O que há de interessante é que agora você tem um
controle ActiveX plenamente funcional (incluindo uma interface, uma biblioteca de tipos e eventos) ba-
seado em um controle memo sem precisar ter escrito uma linha de código sequer!
Observe as funções auxiliadoras que são usadas para converter as propriedades de IStrings e IFont
para os tipos TStrings e TFont nativos do Delphi. Cada uma dessas rotinas opera de um modo semelhante:
fornecem uma ponte entre uma classe do Object Pascal e uma interface de despacho compatível com Auto-
mation. A Tabela 25.1 mostra uma lista de classes VCL e as interfaces equivalentes a ela em Automation.
TFont IFont
TPicture IPicture
TStrings Istrings
NOTA
ActiveX define as interfaces IFont e IPicture. No entanto, o tipo IStrings é definido na VCL. O Delphi for-
nece um arquivo redistribuível chamado StdVcl40.dll que contém a biblioteca de tipos que define essa in-
terface. Essa biblioteca deve ser instalada e registrada em máquinas clientes para aplicações usando um
controle ActiveX com propriedades IStrings para funcionar de modo apropriado.
A estrutura do ActiveX
A estrutura DAX (de Delphi ActiveX) reside na unidade AxCtrls. Um controle ActiveX poderia ser descri-
to como um objeto Automation com esteróides, pois deve implementar a interface IDispatch (além de
muitas outras). Por causa disso, a estrutura DAX é semelhante à dos objetos Automation, sobre a qual
você aprendeu no Capítulo 23. TActiveXControl é um descendente de TAutoObject que implementa as inter-
faces necessárias de um controle ActiveX. A estrutura DAX funciona como uma estrutura de objetos
dual, onde a parte do controle ActiveX contida em TActiveXControl se comunica com uma classe TWinCon-
trol separada, que contém o controle VCL.
Como todos os objetos COM, os controles ActiveX são criados de factories. TActiveXControlFactory
da DAX serve como o factory do objeto TActiveXControl. Uma instância de uma dessas factories é criada na
seção initialization de cada arquivo de implementação de controle. O construtor dessa classe é definido
804 da seguinte maneira:
constructor TActiveXControlFactory.Create(ComServer: TComServerObject;
ActiveXControlClass: TActiveXControlClass;
WinControlClass: TWinControlClass; const ClassID: TGUID;
ToolboxBitmapID: Integer; const LicStr: string; MiscStatus: Integer;
ThreadingModel: TThreadingModel = tmSingle);
ComServer armazena uma instância de TComServer. Geralmente, a global ComServer declarada na unida-
de ComServ é passada nesse parâmetro.
ActiveXControlClass contém o nome do descendente de TActiveXControl que é declarado no arquivo de
implementação.
WinControlClass contém o nome do descendente de TWinControl da VCL que você deseja encapsular
como um controle ActiveX.
ClassID armazena a CLSID da coclass de controle, conforme listada no editor da biblioteca de tipos.
ToolboxBitmapID contém o identificador de recurso de bitmap que deve ser usado como representação
do controle na Component Palette (paleta de componentes).
LicStr armazena a string que deve ser usada como a string-chave de licença do controle. Se ela esti-
ver vazia, trata-se de uma indicação de que o controle não é licenciado.
MiscStatus armazena os flags de status OLEMISC_XXX do controle. Esses flags são definidos na unidade
ActiveX. Esses flags OLEMISC são inseridos no Registro do Sistema quando um controle ActiveX é registra-
do. Os flags OLEMISC fornecem contêineres de controle ActiveX com informações concernentes a vários
atributos do controle ActiveX. Por exemplo, há flags OLEMISC que indicam como um controle é pintado e
se um controle pode conter outros controles. Esses flags são abundantemente documentados em Net-
work, do Microsoft Developer, no tópico “OLEMISC”.
Finalmente, ThreadingModel identifica o modelo de threading ao qual esse controle será registrado para
oferecer suporte. É importante notar que a definição desse parâmetro como algum modelo de threading
em particular não garante que seu controle é mais seguro para esse modelo em particular; ela apenas afeta o
modo como o controle é registrado. Cabe a você, como programador, elaborar a segurança do thread. Para
obter mais informações sobre cada um dos modelos de threading, consulte o Capítulo 23.
A janela refletora
Alguns controles VCL exigem mensagens de notificação para que funcionem adequadamente. Por essa ra-
zão, a DAX criará uma janela refletora cujo trabalho é receber mensagens e encaminhá-las para o controle
VCL. Os controles VCL padrão que exigem uma janela refletora terão o membro csReflector incluído no
conjunto ControlStyle. Se você tiver um TWinControl personalizado que opere usando mensagens de notifica-
ção, deve se certificar de adicionar esse membro ao conjunto ControlStyle no construtor do controle.
Licenciamento de controle
Já dissemos neste capítulo que o esquema DAX padrão para licenciamento envolve um arquivo LIC que
deve acompanhar o arquivo OCX do controle ActiveX nas máquinas de desenvolvimento. Como você já
viu, a string de licença é um dos parâmetros para o construtor de factory de classe do controle ActiveX.
Quando a caixa de seleção Make Control Licensed (criar controle licenciado) está selecionada no assis-
tente, essa opção gera uma string de GUID que será inserida na chamada do construtor e no arquivo LIC
(você está livre para modificar a string posteriormente, se assim desejar). Quando o controle é usado du-
rante o projeto em uma ferramenta de desenvolvimento, a DAX tentará combinar a string de licença na
factory da classe com uma string no arquivo LIC. Se houver uma combinação, a instância do controle
será criada. Quando uma aplicação que inclui o controle ActiveX licenciado é compilada, a string de li-
cença é incorporada na aplicação e o arquivo LIC deixa de ser obrigatório para a execução da aplicação.
O esquema do arquivo LIC para licenciamento não é o único que existe na face da terra. Por exem-
plo, alguns programadores acham incômodo o uso de um arquivo adicional e preferem armazenar uma
chave de licença no Registro. Felizmente, a DAX facilita sobremaneira a implementação de um esquema
de licenciamento alternativo como esse. A verificação de licença ocorre em um método TActiveXControl-
Factory chamado HasMachineLicense( ). Como padrão, esse método tenta localizar a string de licenciamen-
to no arquivo LIC, mas você pode fazer esse método executar qualquer tipo de verificação para determi-
nar o licenciamento. Por exemplo, a Listagem 25.4 mostra um descendente de TActiveXControlFactory que
procura a chave de licença no Registro.
{ TRegLicAxControlFactory }
type
TRegLicActiveXControlFactory = class(TActiveXControlFactory)
protected
function HasMachineLicense: Boolean; override;
end;
Um arquivo de Registro (REG) pode ser usado para colocar a chave de licença no Registro de uma
máquina licenciada. Isso é mostrado na Listagem 25.5.
REGEDIT4
[HKEY_CLASSES_ROOT\Licenses\{C06EFEA0-06B2-11D1-A9BF-B18A9F703311}]
@= “Licensing info for DDG demo ActiveX control”
Páginas de propriedades
As páginas de propriedades fornecem um meio para modificar as propriedades de um controle ActiveX
através de uma caixa de diálogo personalizada. As páginas de propriedade do controle são adicionadas
como páginas em uma caixa de diálogo com guias que é criada pelo ActiveX. As caixas de diálogo da pá-
gina de propriedades costumam ser chamadas de um menu local, ao qual você tem acesso dando um cli-
que com o botão direito do mouse no contêiner host do controle.
Páginas de propriedades-padrão
A DAX fornece páginas de propriedades-padrão para propriedades do tipo IStrings, IPicture, TColor e
IFont. As CLSIDs dessas páginas de propriedades são encontradas na unidade AxCtrls. Elas são declaradas
da seguinte maneira:
const
{ Delphi property page CLSIDs }
Class_DColorPropPage: TGUID = ‘{5CFF5D59-5946-11D0-BDEF-00A024D1875C}’;
Class_DFontPropPage: TGUID = ‘{5CFF5D5B-5946-11D0-BDEF-00A024D1875C}’;
Class_DPicturePropPage: TGUID = ‘{5CFF5D5A-5946-11D0-BDEF-00A024D1875C}’;
Class_DStringPropPage: TGUID = ‘{F42D677E-754B-11D0-BDFB-00A024D1875C}’;
O uso de qualquer uma dessas páginas de propriedade no seu controle não tem nada de muito com-
plexo: basta passar uma dessas CLSIDs para o parâmetro do procedimento DefinePropertyPage( ) no mé-
todo DefinePropertyPages( ) de seu controle ActiveX, como vemos a seguir:
procedure TMemoX.DefinePropertyPages(DefinePropertyPage: TDefinePropertyPage);
begin
DefinePropertyPage(Class_DColorPropPage);
DefinePropertyPage(Class_DFontPropPage);
DefinePropertyPage(Class_DStringPropPage);
end;
As Figuras 25.4 a 25.7 mostram cada uma das páginas de propriedades DAX padrão.
807
FIGURA 25.4 Página de propriedades DAX Colors (cores DAX).
Cada uma dessas páginas de propriedades funciona de modo semelhante. A caixa de combinação
contém os nomes de cada uma das propriedades do tipo especificado. Você só precisa selecionar o nome da
propriedade, definir o valor na caixa de diálogo e em seguida dar um clique em OK para modificar a pro-
priedade selecionada.
NOTA
Se você quiser usar as páginas de propriedades-padrão DAX, deve distribuir StdVcl40.dll juntamente com
seu arquivo OCX. Como já dissemos neste capítulo, esse arquivo contém a definição de IStrings, bem
como as interfaces IProvider e IDataBroker. Além disso, StdVcl40.dll contém a implementação de cada
uma das páginas de propriedades DAX. Você também deve garantir que tanto o arquivo OCX como
StdVcl40.dll foram registrados na máquina de destino.
unit CardImpl;
interface
uses
Windows, ActiveX, Classes, Controls, Graphics, Menus, Forms, StdCtrls,
ComServ, StdVCL, AXCtrls, AxCard_TLB, Cards;
type
TCardX = class(TActiveXControl, ICardX)
private
{ Declarações privadas }
FDelphiControl: TCard;
FEvents: ICardXEvents;
procedure ClickEvent(Sender: TObject);
procedure DblClickEvent(Sender: TObject);
procedure KeyPressEvent(Sender: TObject; var Key: Char);
protected
{ Declarações protegidas }
procedure DefinePropertyPages(DefinePropertyPage: TDefinePropertyPage);
override;
procedure EventSinkChanged(const EventSink: IUnknown); override;
procedure InitializeControl; override;
function ClassNameIs(const Name: WideString): WordBool; safecall;
function DrawTextBiDiModeFlags(Flags: Integer): Integer; safecall;
function DrawTextBiDiModeFlagsReadingOnly: Integer; safecall;
function Get_BackColor: OLE_COLOR; safecall;
function Get_BiDiMode: TxBiDiMode; safecall;
function Get_Color: OLE_COLOR; safecall;
function Get_Cursor: Smallint; safecall;
function Get_DoubleBuffered: WordBool; safecall;
function Get_DragCursor: Smallint; safecall;
function Get_DragMode: TxDragMode; safecall;
function Get_Enabled: WordBool; safecall;
function Get_FaceUp: WordBool; safecall;
function Get_ParentColor: WordBool; safecall;
function Get_Suit: TxCardSuit; safecall;
function Get_Value: TxCardValue; safecall;
function Get_Visible: WordBool; safecall;
function GetControlsAlignment: TxAlignment; safecall;
function IsRightToLeft: WordBool; safecall;
function UseRightToLeftAlignment: WordBool; safecall;
function UseRightToLeftReading: WordBool; safecall;
function UseRightToLeftScrollBar: WordBool; safecall;
procedure FlipChildren(AllLevels: WordBool); safecall;
procedure InitiateAction; safecall;
procedure Set_BackColor(Value: OLE_COLOR); safecall;
procedure Set_BiDiMode(Value: TxBiDiMode); safecall;
procedure Set_Color(Value: OLE_COLOR); safecall;
procedure Set_Cursor(Value: Smallint); safecall;
procedure Set_DoubleBuffered(Value: WordBool); safecall;
procedure Set_DragCursor(Value: Smallint); safecall;
procedure Set_DragMode(Value: TxDragMode); safecall;
809
Listagem 25.6 Continuação
implementation
{ TCardX }
procedure TCardX.InitializeControl;
begin
FDelphiControl := Control as TCard;
FDelphiControl.OnClick := ClickEvent;
FDelphiControl.OnDblClick := DblClickEvent;
FDelphiControl.OnKeyPress := KeyPressEvent;
end;
procedure TCardX.InitiateAction;
begin
FDelphiControl.InitiateAction;
end;
procedure TCardX.AboutBox;
begin
ShowCardXAbout;
end;
initialization
TActiveXControlFactory.Create(ComServer, TCardX, TCard, Class_CardX,
1, ‘’, 0, tmApartment);
end.
Basicamente, essa unidade contém os elementos gerados pelo assistente, exceto as duas linhas de
código mostradas no método DefinePropertyPages( ). Nesse método, você pode ver que empregamos a pá-
gina de propriedades VCL Color (cor VCL) padrão, além de uma página de propriedade personalizada
cuja CLSID é definida como Class_CardPropPage. Essa página de propriedades foi criada através da seleção
do item Property Page (página de propriedades) da página ActiveX da caixa de diálogo New Items (itens
novos). A Figura 25.8 mostra essa página de propriedades no Form Designer e a Listagem 25.7 mostra o
código-fonte dessa página de propriedades.
unit CardPP;
interface
type
814
Listagem 25.7 Continuação
TCardPropPage = class(TPropertyPage)
Card1: TCard;
ValueGroup: TGroupBox;
SpeedButton1: TSpeedButton;
SpeedButton2: TSpeedButton;
SpeedButton3: TSpeedButton;
SpeedButton4: TSpeedButton;
SpeedButton5: TSpeedButton;
SpeedButton6: TSpeedButton;
SpeedButton7: TSpeedButton;
SpeedButton8: TSpeedButton;
SpeedButton9: TSpeedButton;
SpeedButton10: TSpeedButton;
SpeedButton11: TSpeedButton;
SpeedButton12: TSpeedButton;
SuitGroup: TGroupBox;
SpeedButton13: TSpeedButton;
SpeedButton14: TSpeedButton;
SpeedButton15: TSpeedButton;
SpeedButton16: TSpeedButton;
SpeedButton17: TSpeedButton;
procedure FormCreate(Sender: TObject);
procedure SpeedButton1Click(Sender: TObject);
protected
procedure UpdatePropertyPage; override;
procedure UpdateObject; override;
end;
const
Class_CardPropPage: TGUID = ‘{C06EFEA1-06B2-11D1-A9BF-B18A9F703311}’;
implementation
{$R *.DFM}
procedure TCardPropPage.UpdatePropertyPage;
var
i: Integer;
AValue, ASuit: Integer;
begin
// obtém naipe e valor
AValue := OleObject.Value;
ASuit := OleObject.Suit;
// define carta corretamente
Card1.Value := TCardValue(AValue);
Card1.Suit := TCardSuit(ASuit);
// define speedbutton do valor correto
with ValueGroup do
for i := 0 to ControlCount - 1 do
if (Controls[i] is TSpeedButton) and
(TSpeedButton(Controls[i]).Tag = AValue) then
TSpeedButton(Controls[i]).Down := True;
// define speedbuttons do naipe correto
815
Listagem 25.7 Continuação
with SuitGroup do
for i := 0 to ControlCount - 1 do
if (Controls[i] is TSpeedButton) and
(TSpeedButton(Controls[i]).Tag = ASuit) then
TSpeedButton(Controls[i]).Down := True;
end;
procedure TCardPropPage.UpdateObject;
var
i: Integer;
begin
// define speedbutton do valor correto
with ValueGroup do
for i := 0 to ControlCount - 1 do
if (Controls[i] is TSpeedButton) and TSpeedButton(Controls[i]).Down then
begin
OleObject.Value := TSpeedButton(Controls[i]).Tag;
Break;
end;
// define speedbutton do naipe correto
with SuitGroup do
for i := 0 to ControlCount - 1 do
if (Controls[i] is TSpeedButton) and TSpeedButton(Controls[i]).Down then
begin
OleObject.Suit := TSpeedButton(Controls[i]).Tag;
Break;
end;
end;
Card1.Suit := TCardSuit(Tag);
end;
Modified;
end;
end;
initialization
TActiveXPropertyPageFactory.Create(
ComServer,
TCardPropPage,
Class_CardPropPage);
end.
Você deve se comunicar com o controle ActiveX da página de propriedades usando seu campo
OleObject. OleObject é uma variante que armazena uma referência para a interface IDispatch do controle.
Os métodos UpdatePropertyPage( ) e UpdateObject( ) são gerados pelo assistente. UpdatePropertyPage( ) é
chamado quando a página de propriedades é chamada. Nesse método, você deve definir o conteúdo da
página para combinar com os valores atuais do controle ActiveX, conforme indicado na propriedade
OleObject. UpdateObject( ) será chamado quando o usuário dá um clique no botão OK ou Apply (aplicar)
na caixa de diálogo Property Page. Nesse método, você deve usar a propriedade OleObject para definir as
propriedades do controle ActiveX conforme indicado pela página de propriedades.
Nesse exemplo, a página de propriedades permite que você edite o naipe ou o valor do controle
ActiveX TCardX. À medida que você modifica o conjunto ou o valor usando os speedbuttons na caixa de
diálogo, um controle VCL TCard que reside na página de propriedades muda de modo a refletir o naipe ou
o valor atual. Observe também que, quando se dá um clique em um speedbutton, o procedimento Modifi-
ed( ) da página de propriedades é chamado para definir o flag modificado da caixa de diálogo Property
Page. Isso ativa o botão Apply na caixa de diálogo.
Essa página de propriedades é mostrada em ação na Figura Figure 25.9.
ActiveForms
Funcionalmente, o ActiveForms é praticamente igual aos controles ActiveX sobre os quais falamos ao
longo deste capítulo. A principal diferença é que o controle VCL no qual você baseia o controle ActiveX
não muda depois que você executa o assistente, enquanto um ActiveForm se caracteriza por mudar cons-
tantemente à medida que é manipulado no designer. Como o assistente e a estrutura do ActiveForm são
basicamente iguais aos dos controles ActiveX, não vamos repetir tudo isso. Em vez disso, vamos nos con-
centrar em algumas coisas interessantes que você pode fazer com ActiveForms.
817
Adicionando propriedades aos ActiveForms
Um problema com os ActiveForms é que sua representação na biblioteca de tipos consiste em interfaces
“flat”, e não em componentes aninhados, com os quais você está acostumado a trabalhar na VCL. Isso
significa que, se você tem um formulário com diversos botões, eles não podem ser facilmente endereça-
dos ao modo da VCL, ActiveForm.Botão.PropriedadeBotão, como um ActiveForm. Em vez disso, a for-
ma mais fácil de fazer isso é expor as propriedades do botão em questão como propriedades do próprio
ActiveForm. A estrutura DAX simplifica bastante a adição de propriedades aos ActiveForms; tudo o que
você precisa fazer é executar duas etapas. Veja a seguir o que é preciso para publicar a propriedade Cap-
tion de um botão em ActiveForm:
unit AFImpl;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
ActiveX, AxCtrls, AFrm_TLB, StdCtrls;
type
TActiveFormX = class(TActiveForm, IActiveFormX)
Button1: TButton;
private
{ Declarações privadas }
FEvents: IActiveFormXEvents;
procedure ActivateEvent(Sender: TObject);
procedure ClickEvent(Sender: TObject);
procedure CreateEvent(Sender: TObject);
procedure DblClickEvent(Sender: TObject);
procedure DeactivateEvent(Sender: TObject);
procedure DestroyEvent(Sender: TObject);
procedure KeyPressEvent(Sender: TObject; var Key: Char);
procedure PaintEvent(Sender: TObject);
function GetButtonCaption: string;
procedure SetButtonCaption(const Value: string);
protected
{ Declarações protegidas }
procedure DefinePropertyPages(DefinePropertyPage: TDefinePropertyPage);
override;
procedure EventSinkChanged(const EventSink: IUnknown); override;
function Get_Active: WordBool; safecall;
function Get_AutoScroll: WordBool; safecall;
function Get_AutoSize: WordBool; safecall;
function Get_AxBorderStyle: TxActiveFormBorderStyle; safecall;
818
Listagem 25.8 Continuação
implementation
{$R *.DFM}
{ TActiveFormX }
procedure TActiveFormX.DefinePropertyPages(DefinePropertyPage:
TDefinePropertyPage);
begin 819
Listagem 25.8 Continuação
procedure TActiveFormX.Initialize;
begin
inherited Initialize;
OnActivate := ActivateEvent;
OnClick := ClickEvent;
OnCreate := CreateEvent;
OnDblClick := DblClickEvent;
OnDeactivate := DeactivateEvent;
OnDestroy := DestroyEvent;
OnKeyPress := KeyPressEvent;
OnPaint := PaintEvent;
end;
820
Listagem 25.8 Continuação
procedure TActiveFormX.AboutBox;
begin
ShowActiveFormXAbout;
end;
initialization
TActiveFormFactory.Create(ComServer, TActiveFormControl, TActiveFormX,
Class_ActiveFormX, 1, ‘’, OLEMISC_SIMPLEFRAME or OLEMISC_ACTSLIKELABEL,
tmApartment);
end.
ActiveX na Web
Um uso ideal para ActiveForms é como um veículo para transportar pequenas aplicações pela World
Wide Web. Os controles ActiveX menores também são úteis para melhorar a aparência e a utilidade de
páginas da Web. No entanto, para tirar o melhor proveito possível de controles ActiveX escritos em
Delphi na Web, você precisa saber algumas coisas sobre streaming de controle, segurança e comunicação
com o browser.
O parâmetro pUnk de cada uma dessas funções é a interface IUnknown para o controle ActiveX. No
caso de controles ActiveX, você pode passar Control como IUnknown neste parâmetro. No caso de Active-
Forms, você deve passar IUnknown(VclComObject) nesse parâmetro. O parâmetro szTarget de HlinkNavigate-
String( ) representa o URL que você deseja usar.
Uma tarefa mais ambiciosa seria usar a função URLDownloadToFile( ) para transferir um arquivo do
servidor para a máquina local. Esse método é definido em UrlMon da seguinte maneira:
function URLDownloadToFile(p1: IUnknown; p2: PChar; p3: PChar; p4: DWORD;
p5: IBindStatusCallback): HResult; stdcall;
Nomes de parâmetro muito úteis, não? p1 representa a interface do controle ActiveX, semelhante
ao parâmetro pUnk das funções HlinkXXX( ). p2 armazena o URL do arquivo a ser transferido. p3 é o nome
do arquivo local que será preenchido com os dados do arquivo especificado por p2. p4 deve ser definido
como 0. p5 armazena um ponteiro de interface IBindStatusCallback opcional. Essa interface pode ser usada
para obter informações incrementais no arquivo à medida que ele é transferido.
A Listagem 25.9 mostra o arquivo de implementação de um ActiveForm que implementa esses
métodos. Ela também demonstra um simples exemplo de implementação da interface IBindStatus-
Callback.
825
Listagem 25.9 Um ActiveFome que usa funções UrlMon
unit UrlTestMain;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
ActiveX, AxCtrls, UrlTest_TLB, UrlMon, StdCtrls, MPlayer, ExtCtrls,
ComCtrls;
type
TUrlTestForm = class(TActiveForm, IUrlTestForm, IBindStatusCallback)
GroupBox1: TGroupBox;
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
MediaPlayer1: TMediaPlayer;
Panel1: TPanel;
Button1: TButton;
StatusPanel: TPanel;
ProgressBar1: TProgressBar;
ServerName: TEdit;
StaticText1: TStaticText;
procedure Label1Click(Sender: TObject);
procedure Label2Click(Sender: TObject);
procedure Label3Click(Sender: TObject);
procedure Button1Click(Sender: TObject);
private
{ Declarações privadas }
FEvents: IUrlTestFormEvents;
procedure ActivateEvent(Sender: TObject);
procedure ClickEvent(Sender: TObject);
procedure CreateEvent(Sender: TObject);
procedure DblClickEvent(Sender: TObject);
procedure DeactivateEvent(Sender: TObject);
procedure DestroyEvent(Sender: TObject);
procedure KeyPressEvent(Sender: TObject; var Key: Char);
procedure PaintEvent(Sender: TObject);
protected
{ IBindStatusCallback }
function OnStartBinding(dwReserved: DWORD; pib: IBinding): HResult;
stdcall;
function GetPriority(out nPriority): HResult; stdcall;
function OnLowResource(reserved: DWORD): HResult; stdcall;
function OnProgress(ulProgress, ulProgressMax, ulStatusCode: ULONG;
szStatusText: LPCWSTR): HResult; stdcall;
function OnStopBinding( hRes: HResult; szError: PWideChar ): HResult;
stdcall;
function GetBindInfo(out grfBINDF: DWORD; var bindinfo: TBindInfo):
HResult;
stdcall;
function OnDataAvailable(grfBSCF: DWORD; dwSize: DWORD;
formatetc: PFormatEtc; stgmed: PStgMedium): HResult; stdcall;
function OnObjectAvailable(const iid: TGUID; punk: IUnknown): HResult;
826
Listagem 25.9 Continuação
stdcall;
{ UrlTestForm }
procedure EventSinkChanged(const EventSink: IUnknown); override;
procedure Initialize; override;
function Get_Active: WordBool; safecall;
function Get_AutoScroll: WordBool; safecall;
function Get_AxBorderStyle: TxActiveFormBorderStyle; safecall;
function Get_Caption: WideString; safecall;
function Get_Color: OLE_COLOR; safecall;
function Get_Cursor: Smallint; safecall;
function Get_DropTarget: WordBool; safecall;
function Get_Enabled: WordBool; safecall;
function Get_Font: IFontDisp; safecall;
function Get_HelpFile: WideString; safecall;
function Get_KeyPreview: WordBool; safecall;
function Get_PixelsPerInch: Integer; safecall;
function Get_PrintScale: TxPrintScale; safecall;
function Get_Scaled: WordBool; safecall;
function Get_Visible: WordBool; safecall;
function Get_WindowState: TxWindowState; safecall;
procedure Set_AutoScroll(Value: WordBool); safecall;
procedure Set_AxBorderStyle(Value: TxActiveFormBorderStyle); safecall;
procedure Set_Caption(const Value: WideString); safecall;
procedure Set_Color(Color: OLE_COLOR); safecall;
procedure Set_Cursor(Value: Smallint); safecall;
procedure Set_DropTarget(Value: WordBool); safecall;
procedure Set_Enabled(Value: WordBool); safecall;
procedure Set_Font(const Font: IFontDisp); safecall;
procedure Set_HelpFile(const Value: WideString); safecall;
procedure Set_KeyPreview(Value: WordBool); safecall;
procedure Set_PixelsPerInch(Value: Integer); safecall;
procedure Set_PrintScale(Value: TxPrintScale); safecall;
procedure Set_Scaled(Value: WordBool); safecall;
procedure Set_Visible(Value: WordBool); safecall;
procedure Set_WindowState(Value: TxWindowState); safecall;
public
{ Declarações públicas }
end;
implementation
{$R *.DFM}
{ TUrlTestForm.IBindStatusCallback }
827
Listagem 25.9 Continuação
{ TUrlTestForm }
828
Listagem 25.9 Continuação
procedure TUrlTestForm.Initialize;
begin
OnActivate := ActivateEvent;
OnClick := ClickEvent;
OnCreate := CreateEvent;
OnDblClick := DblClickEvent;
OnDeactivate := DeactivateEvent;
OnDestroy := DestroyEvent;
OnKeyPress := KeyPressEvent;
OnPaint := PaintEvent;
end;
begin
Result := Enabled;
end;
initialization
TActiveFormFactory.Create(ComServer, TActiveFormControl, TUrlTestForm,
Class_UrlTestForm, 1, ‘’, OLEMISC_SIMPLEFRAME or OLEMISC_ACTSLIKELABEL,
tmApartment);
end.
A página Project
Nessa página, Target Dir (diretório de destino) representa o caminho para o qual você deseja distribuir o
projeto ActiveX. Observe que isso parte do princípio de que você é capaz de mapear uma unidade de dis-
co para seu servidor da Web – o conteúdo do controle de edição deve ser um caminho normal ou UNC.
Observe também que você não deve digitar um nome de arquivo, mas apenas o caminho.
O URL de destino é o URL que faz referência ao mesmo diretório especificado em Target Dir. Esse
deverá ser um URL válido, que usa um prefixo de URL padrão (http://, file://, ftp:// e assim por dian-
te). Mais uma vez, não inclua um nome de arquivo aqui, apenas o caminho do URL.
HTML Dir (diretório de HTML) é outro caminho que determina onde o arquivo HTML gerado
será copiado. Geralmente, ele é igual ao de Target Dir.
Essa caixa de diálogo também permite que você escolha diversas opções de distribuição de projeto:
l Use CAB file compression (usar compactação de arquivo CAB). A seleção dessas opções fará com
que seu arquivo OCX seja compactado usando o formato Microsoft Cabinet (CAB). Isso é reco-
mendado para controles que você planeja distribuir para clientes que usam links na Web com
pouca largura de banda.
l Include file version number (incluir número da versão do arquivo). Essa opção indica se deve ser
incluído um número de versão no arquivo INF ou HTML gerado. Isso é recomendado, pois for-
nece um meio pelo qual os usuários podem evitar o download do controle, caso já tenham uma
versão mais recente.
l Autoincrement release number (incrementar automaticamente o número da versão). Quando
selecionada, essa opção faz com que a parte referente ao número da versão de seus recursos Ver-
sionInfo sejam automaticamente incrementados após a distribuição.
NOTA
Você precisará do Internet Explorer 3.02 ou mais recente e o Authenticode 2.0, além de um certificado de
um provedor como VeriSign, para assinar um código nos arquivos.
834
l Deploy required packages (distribuir pacotes obrigatórios). Se o seu projeto for construído com
pacotes, basta selecionar essa caixa para incluir automaticamente os pacotes usados pelo seu
projeto no conjunto de distribuição de arquivo.
l Deploy additional files (distribuir arquivos adicionais). A seleção dessa caixa permite que você
adicione os arquivos mostrados na página Additional Files (arquivos adicionais) ao conjunto de
distribuição de arquivos.
Quando você escolhe usar compactação CAB na página Project, o grupo CAB Options (opções de
CAB) das páginas Packages e Additional Files permitem que você selecione se deseja o arquivo compacta-
do com o OCX ou em um arquivo CAB separado. Geralmente, é mais eficiente compactar cada arquivo
em seu próprio CAB, pois o usuário não terá de transferir arquivos que possivelmente já tenham sido ins-
talados em suas máquinas. Veja a seguir algumas outras opções com as quais você deve se familiarizar:
l Se a opção Use File VersionInfo estiver selecionada, o mecanismo de distribuição determinará se
o arquivo selecionado possui VersionInfo e, nesse caso, exibirá o número de versão contido em
VersionInfo no arquivo INF.
l A caixa de edição Target URL terá, como padrão, o mesmo local que o URL de destino da página
Project. Esse é o URL do qual o arquivo pode ser transferido por download. Se você estiver pre- 835
sumindo que o cliente de seu controle ActiveX já tenha esse arquivo instalado, deixe esse valor
em branco.
l A caixa de edição Target Directory permite que você especifique o diretório no qual o arquivo
indicado deve ser copiado. Deixe essa caixa em branco se o arquivo já existe no servidor e não
precisa ser copiado novamente para o servidor.
Code Signing
A página Code Signing (assinatura de código), mostrada na Figura 25.14, permite que você especifique a
localização do arquivo de certificado e o arquivo de chave privada associado ao seu certificado. Além dis-
so, você pode especificar um título para sua aplicação, um URL para sua aplicação ou empresa, o tipo de
criptografia que deseja usar e se o certificado deve ter uma marca de hora. Recomenda-se que você esco-
lha atribuir uma marca de hora à assinatura do código, pois assim a assinatura permanecerá válida mes-
mo depois que expirar a validade do seu certificado.
Dicas gerais
Se você cometer um erro na página Project page, o controle aparecerá na página da Web como uma caixa
com um X vermelho no canto superior direito. Se isso acontecer, você deve verificar erros nos arquivos
HTM e INF gerados (caso esteja distribuindo múltiplos arquivos). O problema mais comum é um URL
incorreto especificado para o controle.
Resumo
Isso é praticamente tudo o que você precisa saber sobre a criação de controles ActiveX e ActiveForms no
Delphi. Este capítulo revelou muitos segredos dos assistentes de ActiveX para ajudá-lo a trabalhar na es-
trutura DAX e estendê-la para o seu próprio benefício. Este capítulo é uma evolução natural das infor-
mações que você obteve sobre COM e ActiveX nos dois capítulos anteriores – você está cada vez mais
próximo de se tornar um exímio programador em ActiveX. Agora chegou a hora de mudar de ares. O
próximo capítulo mostra como se usa a API Open Tools do Delphi para entrar no IDE.
836
Uso da API Open CAPÍTULO
Tools do Delphi
26
NE STE C AP ÍT UL O
l Interfaces da Open Tools 838
l Uso da API Open Tools 839
l Assistentes de formulário 862
l Resumo 869
Você já se colocou diante da seguinte questão: “o Delphi é realmente bom, mas por que o IDE não execu-
ta esta pequena tarefa que eu gostaria que ele fizesse?” Se esse é o seu caso, não tema. A API Open Tools
está aí para atender às suas necessidades. A API Open Tools do Delphi oferece a capacidade de criar fer-
ramentas que trabalham em conjunto com o IDE. Neste capítulo, você vai aprender as diferentes interfa-
ces que compõem a API Open Tools, como usar as interfaces e também como aproveitar sua especialida-
de recém-adquirida para escrever um assistente repleto de recursos.
NOTA
A API Open Tools completa só está disponível com o Delphi Professional e o Client/Server Suite. O Delphi
Standard tem a capacidade de usar add-ons criados com a API Open Tools, mas não pode criar add-ons
porque só contém as unidades para criar editores de propriedades e componentes. Você pode achar o có-
digo-fonte para as interfaces da Open Tools no subdiretório \Delphi 5\Source\ToolsAPI.
A Tabela 26.1 mostra as unidades que compõem a API Open Tools e as interfaces que elas forne-
cem. O termo interface é usado livremente aqui, pois não diz respeito aos tipos interface nativos do
Delphi. Como a API Open Tools é anterior ao suporte da interface nativa do Delphi, ela utiliza as classes
regulares do Delphi com métodos abstratos virtuais como substitutos para as verdadeiras interfaces. O
uso de interfaces verdadeiras comprometia a API Open Tools nas últimas versões do Delphi e a versão
atual da API Open Tools é basicamente baseada em interface.
ToolsAPI Contém os elementos da API Open Tool baseada na interface mais recente. O
conteúdo dessa unidade basicamente aposenta a API Open Tools pré-Delphi, que usa
classes abstratas para manipular menus, notificações, o sistema de arquivos, o editor e
add-ins de assistente. Ela também contém as novas interfaces para manipular o
depurador, os principais mapeamentos do IDE, projetos, grupos de projetos, pacotes
e a lista To Do.
VirtIntf* Define a classe TInterface, básica da qual as outras interfaces são derivadas. Essa
unidade também define a classe TIStream, que é um wrapper em torno de uma TStream
da VCL.
838
Tabela 26.1 Continuação
NOTA
Você pode estar se perguntando onde todo esse material referente a assistentes é documentado no Delphi.
Garantimos que ele está documentado, mas não é nada fácil de se achar. Cada uma dessas unidades con-
tém documentação completa para a interface, classes, métodos e procedimentos declarados que ela pos-
sui. Como não vamos repassar as mesmas informações, convidamos você a dar uma olhada nas unidades
para ter acesso à documentação completa.
O assistente Dumb
Para começar, você criará um assistente muito simples que não à toa é chamado de assistente dumb (bur-
ro). O requisito mínimo para a criação de um assistente é criar uma classe que implemente a interface
IOTAWizard. Caso não saiba, IOTAWizard é definida na unidade ToolsAPI da seguinte maneira:
type
IOTAWizard = interface(IOTANotifier)
[‘{B75C0CE0-EEA6-11D1-9504-00608CCBF153}’]
{ Strings UI especialistas }
function GetIDString: string;
function GetName: string;
function GetState: TWizardState;
{ Carrega o AddIn }
procedure Execute;
end;
Essa interface consiste principalmente em algumas funções GetXXX( ) que são projetadas para serem
modificadas pelas classes descendentes de modo a fornecer informações específicas para cada assistente.
O método Execute( ) é o lado comercial de IOTAWizard. Execute( ) é chamado pelo IDE quando o usuário
seleciona seu assistente no menu principal ou no menu New Items (novos itens) e é nesse método que o
assistente deve ser criado e chamado.
Se você tiver um olho astuto, deve ter percebido que IOTAWizard descende de outra interface, chama-
da IOTANotifier. IOTANotifier é uma interface definida na unidade ToolsAPI que contém métodos que po-
dem ser chamados pelo IDE para notificar um assistente quanto a várias ocorrências. Essa interface é de-
finida da seguinte maneira:
type
IOTANotifier = interface(IUnknown)
[‘{F17A7BCF-E07D-11D1-AB0B-00C04FB16FB3}’]
{ Este procedimento é chamado imediatamente depois que o item é salvo com
åsucesso. Ele não é o responsável pela chamada de IOTAWizards }
procedure AfterSave;
{ Esta função é chamada imediatamente antes de o item ser salvo. Ela é
chamada para IOTAWizard }
procedure BeforeSave;
{ O item associado está sendo destruído de modo que todas as referências
ådevem ser liberadas. As execeções são ignoradas. }
procedure Destroyed;
{ Este item associado foi modificado de alguma forma. Ele não é chamado
para IOTAWizards }
procedure Modified;
end;
Como indicam os comentários no código-fonte, a maioria desses métodos não é chamada para as-
sistentes IOTAWizard simples. Por causa disso, a ToolsAPI fornece uma classe chamada TNotifierObject que
fornece implementações vazias para os métodos IOTANotifier. Você pode escolher descender seus assis-
tentes dessa classe para tirar proveito da conveniência de ter os métodos IOTANotifier implementados
para você.
Os assistentes não são muito úteis sem um meio para chamá-los, e uma das formas mais simples de
se fazer isso é através de um menu. Se você quiser colocar seu assistente no menu principal do Delphi, só
precisa implementar a interface IOTAMenuWizard, que é definida em toda a sua complexidade em ToolsAPI da
840 seguinte maneira:
type
IOTAMenuWizard = interface(IOTAWizard)
[‘{B75C0CE2-EEA6-11D1-9504-00608CCBF153}’]
function GetMenuText: string;
end;
Como você pode ver, essa interface descende de IOTAWizard e só adiciona um método adicional para
retornar a string de texto do menu.
Para reunir e mostrar para que servem as informações a que você teve acesso até agora, a Listagem
26.1 mostra a unidade DumbWiz.pas, que contém o código-fonte de TDumbWizard.
unit DumbWiz;
interface
uses
ShareMem, SysUtils, Windows, ToolsAPI;
type
TDumbWizard = class(TNotifierObject, IOTAWizard, IOTAMenuWizard)
// Métodos de IOTAWizard
function GetIDString: string;
function GetName: string;
function GetState: TWizardState;
procedure Execute;
// Método de IOTAMenuWizard
function GetMenuText: string;
end;
procedure Register;
implementation
uses Dialogs;
procedure TDumbWizard.Execute;
begin
MessageDlg(‘This is a dumb wizard.’, mtInformation, [mbOk], 0); 841
Listagem 26.1 Continuação
end;
procedure Register;
begin
RegisterPackageWizard(TDumbWizard.Create);
end;
end.
IOTAWizard.Execute( ) chama o assistente. Como mostra a Listagem 26.1, o método Execute( ) para
TDumbWizard não faz muita coisa. No entanto, ainda neste capítulo você verá alguns assistentes que real-
mente realizam algumas tarefas.
retorna o texto que deve aparecer no menu principal. Essa função é
IOTAMenuWizard.GetMenuText( )
chamada todas as vezes em que o usuário abre o menu Help e, portanto, é possível mudar dinamicamente
o valor do texto do menu à medida que o assistente é executado.
Dê uma olhada na chamada para RegisterPackageWizard( ) dentro do procedimento Register( ). Você
vai perceber que isso é muito parecido com a sintaxe usada para registrar componentes, editores de com-
ponentes e editores de propriedades para inclusão na biblioteca de componentes, como descrito nos Ca-
pítulos 21 e 22. A razão para essa semelhança é que esse tipo de assistente é armazenado em um pacote
que faz parte da biblioteca de componentes, juntamente com os componentes e tudo o mais. Você tam-
bém pode armazenar assistentes em uma DLL independente, como verá no próximo exemplo.
Esse assistente é instalado como qualquer componente: selecione os componentes, ative a opção
Install Component (instalar componente) no menu principal e adicione a unidade a um pacote novo ou
existente. Uma vez instalada, a opção de menu para chamar o assistente aparece no menu Help, como
mostra a Figura 26.1. Você pode ver o resultado fantástico desse assistente na Figura 26.2.
O assistente Wizard
É preciso um pouco mais de trabalho para criar um assistente baseado em DLL (que é o oposto de um as-
sistente baseado em uma biblioteca de componentes). Além de demonstrar a criação de um assistente ba-
seado em DLL, o assistente Wizard é usado aqui como exemplo por duas razões: ilustrar como os assis-
tentes DLL se relacionam com o Registro e como manter a base de um código-fonte que se destina tanto
a um assistente EXE como a um assistente DLL.
NOTA
As DLLs são discutidas com mais profundidade no Capítulo 9.
DICA
Não há uma regra infalível que determine se um assistente deve residir em um pacote na biblioteca de com-
ponentes ou em uma DLL. Do ponto de vista de um usuário, a principal diferença entre as duas é que os as-
sistentes de biblioteca de componentes só precisa da instalação de um pacote para ser reconstruída, en-
quanto os assistentes de DLL exigem uma entrada no Registro e o Delphi deve ser fechado e reiniciado para
que as mudanças façam efeito. No entanto, como um programador, os assistentes de pacote são um pou-
co mais fáceis de lidar por uma série de razões. Falando um português mais claro, as exceções se propa-
gam automaticamente entre o assistente e o IDE, você não precisa usar sharemem.dll para gerenciamento
de memória, não precisa fazer nada de especial para inicializar a variável de aplicação da DLL e as mensa-
gens de entrar/sair do mouse e dicas pop-up funcionarão a contento.
Com isso em mente, você deve considerar o uso de um assistente de DLL quando quiser que o assistente
seja instalado com um mínimo de trabalho por parte do usuário final.
Para que o Delphi reconheça um assistente de DLL, ele deve ter uma entrada no Registro do sistema
sob a seguinte chave:
HKEY_CURRENT_USER\Software\Borland\Delphi\5.0\Experts
unit InitWiz;
interface
type
TWizardWizard = class(TNotifierObject, IOTAWizard, IOTAMenuWizard)
// Métodos de IOTAWizard
function GetIDString: string;
function GetName: string;
function GetState: TWizardState;
procedure Execute;
// Método de IOTAMenuWizard
function GetMenuText: string;
end;
var
{ Chave de Registro na qual os assistentes do Delphi 5 são mantidos.
A versão EXE usa o default, enquanto a versão DLL obtém a chave de
ToolServices.GetBaseRegistryKey }
SDelphiKey: string = ‘\Software\Borland\Delphi\5.0\Experts’;
implementation
end;
procedure TWizardWizard.Execute;
{ Chamado quando o especialista é escolhido no menu principal.
Este procedimento cria, mostra e libera o formulário principal. }
begin
MainForm := TMainForm.Create(Application);
try
MainForm.ShowModal;
finally
MainForm.Free;
end;
end;
end.
Você deve perceber algumas poucas diferenças entre essa unidade e a que é usada para criar o assis-
tente Dumb. Mais importante, uma função de inicialização do tipo TWizardInitProc é exigida como um
ponto de entrada para o IDE na DLL do assistente. Nesse caso, essa função é chamada InitWizard( ). Essa
função executa uma série de tarefas de inicialização de assistente, listadas a seguir:
lObtenção da interface IOTAServices do parâmetro BorlandIDEServices.
lSalvamento do ponteiro de interface BorlandIDEServices para uso posterior.
lDefinição da alça da variável Application da DLL como o valor retornado por IOTAServices.GetPa-
rentHandle( ). GetParentHandle( ) retorna a alça de janela da janela que deve servir como pai para
todas as janelas de nível superior criadas pelo assistente.
lPassagem da instância recém-criada do assistente para o procedimento RegisterProc( ) para regis-
trar o assistente com o IDE. RegisterProc( ) será chamada uma vez para cada instância de assis-
tente que a DLL registra com o IDE. 845
l Opcionalmente, InitWizard( ) também pode atribuir um procedimento do tipo TWizardTerminate-
Proc ao parâmetro Terminate para servir como procedimento de saída para o assistente. Esse pro-
cedimento será chamado imediatamente antes de o assistente ser descarregado pelo IDE, e nele
você pode executar qualquer finalização necessária. Inicialmente, esse parâmetro é nil e, portan-
to, se você não precisar de alguma finalização especial, deixe seu valor como nil.
ATENÇÃO
O método de inicialização do assistente deve usar a convenção de chamada stdcall.
ATENÇÃO
Os assistentes de DLL que chamam funções da API Open Tools que possuem parâmetros de string devem
ter a mesma unidade ShareMem na cláusula uses; caso contrário, o Delphi emitirá uma violação de acesso
quando a instância do assistente for liberada.
unit Main;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, ExtCtrls, Registry, AddModU, ComCtrls, Menus;
type
TMainForm = class(TForm)
TopPanel: TPanel;
Label1: TLabel;
BottomPanel: TPanel;
WizList: TListView;
PopupMenu1: TPopupMenu;
Add1: TMenuItem;
846
Listagem 26.3 Continuação
Remove1: TMenuItem;
Modify1: TMenuItem;
AddBtn: TButton;
RemoveBtn: TButton;
ModifyBtn: TButton;
CloseBtn: TButton;
procedure RemoveBtnClick(Sender: TObject);
procedure CloseBtnClick(Sender: TObject);
procedure AddBtnClick(Sender: TObject);
procedure ModifyBtnClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
procedure DoAddMod(Action: TAddModAction);
procedure RefreshReg;
end;
var
MainForm: TMainForm;
implementation
uses InitWiz;
{$R *.DFM}
var
DelReg: TRegistry;
Item := WizList.Selected;
if Item = nil then Exit; // o item deverá estar selecionado
ExpName := Item.Caption; // variáveis de inicialização
if Item.SubItems.Count > 0 then
ExpPath := Item.SubItems[0];
OrigName := ExpName; // salva nome original
end;
{ Chama diálogo que permite que o usuário adicione ou modifique entrada }
if AddModWiz(Action, ExpName, ExpPath) then
begin
{ se ação for Modify, e o nome foi alterado, manipula-o }
if (Action = amaModify) and (OrigName < > ExpName) then
DelReg.RenameValue(OrigName, ExpName);
DelReg.WriteString(ExpName, ExpPath); // escreve novo valor
end;
RefreshReg; // atualiza caixa de listagem
end;
procedure TMainForm.RefreshReg;
{ Atualiza caixa de listagem com conteúdo do Registro }
var
i: integer;
TempList: TStringList;
Item: TListItem;
begin
WizList.Items.Clear;
TempList := TStringList.Create;
try
{ Obtém nomes do assistente do Registro }
DelReg.GetValueNames(TempList);
{ Obtém strings de caminho de cada nome de especialista }
for i := 0 to TempList.Count - 1 do
begin
Item := WizList.Items.Add;
Item.Caption := TempList[i];
Item.SubItems.Add(DelReg.ReadString(TempList[i]));
end;
finally
TempList.Free;
end;
end;
begin
RefreshReg;
end;
initialization
DelReg := TRegistry.Create; // cria objeto do Registro
DelReg.RootKey := HKEY_CURRENT_USER; // define chave do Registro
DelReg.OpenKey(SDelphiKey, True); // abre/cria chave do assistente do Delphi
finalization
Delreg.Free; // libera objeto do Registro
end.
Essa é a unidade responsável pelo fornecimento da interface com o usuário para adicionar, remover
e modificar entradas do assistente de DLL no Registro. Na seção initialization dessa unidade, um objeto
TRegistry chamado DelReg é criado. A propriedade RootKey de DelReg é definida como HKEY_CURRENT_USER e
abre a chave \Software\Borland\Delphi\5.0\Experts – a chave usada para monitorar assistentes de DLL –
usando seu método OpenKey( ).
Quando o assistente é acionado, um componente TListView chamado ExptList é preenchido com os
itens e os valores da chave de Registro mencionada anteriormente. Isso é feito pela chamada de
DelReg.GetValueNames( ) para recuperar os nomes dos itens em TStringList. Um componente TListItem é adi-
cionado a ExptList para cada elemento na lista de strings e o método DelReg.ReadString( ) é usado para ler
o valor de cada item, que é colocado na lista SubItems de TListItem.
O trabalho do Registro é feito nos métodos RemoveBtnClick( ) e DoAddMod( ). RemoveBtnClick( ) é res-
ponsável pela remoção do item de assistente atualmente selecionado do Registro. Ele primeiro verifica se
existe um item destacado; em seguida, ele abre uma caixa de diálogo de confirmação. Finalmente, ele faz
o trabalho chamando o método DelReg.DeleteValue( ) e passando CurrentItem como parâmetro.
DoAddMod( ) aceita um parâmetro do tipo TAddModAction. Esse tipo é definido da seguinte maneira:
type
TAddModAction = (amaAdd, amaModify);
Como se pode deduzir pelos valores do tipo, essa variável indica se um novo item será adicionado ou
um item existente será modificado. Essa função primeiro verifica se há um item atualmente selecionado ou,
se não houver, se o parâmetro Action armazena o valor amaAdd. Depois disso, se Action for amaModify, o item de
assistente existente e o valor são copiados nas variáveis locais ExpName e ExpPath. Posteriormente, esses valo-
res são passados para uma função chamada AddModExpert( ), que é definida na unidade AddModU, mostrada na
Listagem 26.4. Essa função chama uma caixa de diálogo na qual o usuário pode inserir um nome novo ou
modificado ou informações de caminho para um assistente (veja a Figura 26.5). Ela retorna True quando o
usuário fecha a caixa de diálogo com o botão OK. Nesse ponto, um item existente é modificado usando
DelReg.RenameValue( ) e um valor novo ou modificado é escrito com DelReg.WriteString( ).
849
Listagem 26.4 AddModU.pas, a unidade que adiciona e modifica entradas de assistente no Registro
unit AddModU;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, ExtCtrls;
type
TAddModAction = (amaAdd, amaModify);
TAddModForm = class(TForm)
OkBtn: TButton;
CancelBtn: TButton;
OpenDialog: TOpenDialog;
Panel1: TPanel;
Label1: TLabel;
Label2: TLabel;
PathEd: TEdit;
NameEd: TEdit;
BrowseBtn: TButton;
procedure BrowseBtnClick(Sender: TObject);
private
{ Declarações privadas }
public
{ Declarações públicas }
end;
implementation
{$R *.DFM}
end;
Free;
end;
end;
end.
{$ifdef BUILD_EXE}
program WizWiz; // Cria como EXE
{$else}
library WizWiz; // Cria como DLL
{$endif}
uses
{$ifndef BUILD_EXE}
ShareMem, // ShareMem exigida pela DLL
InitWiz in ‘InitWiz.pas’, // Material do assistente
{$endif}
ToolsAPI,
Forms,
Main in ‘Main.pas’ {MainForm},
AddModU in ‘AddModU.pas’ {AddModForm};
{$ifdef BUILD_EXE}
{$R *.RES} // obrigatório para EXE
{$else}
exports // obrigatório para DLL
InitWizard name WizardEntryPoint; // ponto de entrada obrigatório
{$endif}
begin
{$ifdef BUILD_EXE} // obrigatório para EXE...
Application.Initialize;
Application.CreateForm(TMainForm, MainForm);
Application.Run;
{$endif}
end.
851
Como o código mostra, esse projeto construirá um executável se a condicional BUILD_EXE foi defini-
da. Caso contrário, ele construirá um assistente baseado em DLL. Você pode definir uma condicional em
Conditional Defines (definições condicionais) na página Directories/Conditionals (diretórios/condicio-
nais) da caixa de diálogo Project, Options (projeto, opções), que é mostrada na Figura 26.6.
Uma última observação sobre esse projeto: observe que a função InitWizard( ) da unidade InitWiz
está sendo exportada na cláusula exports do arquivo de projeto. Você deve exportar essa função com o
nome WizardEntryPoint, que é definido na unidade ToolsAPI.
ATENÇÃO
A Borland não fornece um arquivo ToolsAPI.dcu, o que significa que EXEs ou DLLs contendo uma referência
a ToolsAPI em uma cláusula uses só pode ser construída com pacotes. Atualmente, não é possível construir
assistentes sem pacotes.
DDG Search
Lembra-se do pequeno, porém interessante, programa que você desenvolveu no Capítulo 11? Nesta se-
ção, você vai aprender como pode tornar essa útil aplicação em um assistente do Delphi ainda mais útil,
adicionando-lhe apenas um pouco de código. Esse assistente é chamado de DDG Search.
Primeiro, a unidade que faz a interface de DDG Search com o IDE, InitWiz.pas, é mostrada na
Listagem 26.6. Você vai perceber que essa unidade é muito semelhante à unidade homônima no
exemplo anterior. Isso foi proposital. Essa unidade é apenas uma cópia da anterior, com algumas
mudanças necessárias envolvendo o nome do assistente e o método Execute( ). Copiar e colar é o que
chamamos de “herança à moda antiga”. Afinal de contas, por que digitar mais do que o estritamente
necessário?
852
Listagem 26.6 InitWiz.pas, a unidade que contém a lógica do assistente DDG Search
unit InitWiz;
interface
uses
Windows, ToolsAPI;
type
TSearchWizard = class(TNotifierObject, IOTAWizard, IOTAMenuWizard)
// Métodos de IOTAWizard
function GetIDString: string;
function GetName: string;
function GetState: TWizardState;
procedure Execute;
// Método de IOTAMenuWizard
function GetMenuText: string;
end;
var
ActionSvc: IOTAActionServices;
implementation
procedure TSearchWizard.Execute;
{ Chamado quando o nome do assistente for selecionado no menu Help do IDE.
Esta função chama o assistente }
begin
// caso não tenha sido criado, cria e mostra
if MainForm = nil then
853
Listagem 26.6 Continuação
begin
MainForm := TMainForm.Create(Application);
ThreadPriWin := TThreadPriWin.Create(Application);
MainForm.Show;
end
else
// se criou, restaura a janela e mostra
with MainForm do
begin
if not Visible then Show;
if WindowState = wsMinimized then WindowState := wsNormal;
SetFocus;
end;
end;
end.
A função Execute( ) desse assistente mostra uma coisa um pouco diferente do que você viu até ago-
ra: o formulário principal do assistente, MainForm, não está sendo em módulos. É claro que isso requer um
trabalho extra, pois você tem de saber quando um formulário é criado e quando a variável do formulário
é inválida. Isso pode ser feito certificando-se de que a variável MainForm é definida como nil quando o as-
sistente está inativo. Falaremos um pouco mais sobre isso daqui a pouco.
Um outro aspecto desse projeto, que foi significativamente alterado desde o Capítulo 11, é que o ar-
quivo de projeto é denominado DDGSrch.dpr. Esse arquivo é mostrado na Listagem 26.7.
library DDGSrch;
uses
ShareMem,
ToolsAPI,
Main in ‘MAIN.PAS’ {MainForm},
SrchIni in ‘SrchIni.pas’,
SrchU in ‘SrchU.pas’,
PriU in ‘PriU.pas’ {ThreadPriWin},
854
Listagem 26.7 Continuação
InitWiz in ‘InitWiz.pas’,
MemMap in ‘..\..\Utils\MemMap.pas’,
StrUtils in ‘..\..\Utils\StrUtils.pas’;
{$R *.RES}
exports
{ Ponto de entrada que é chamado pelo IDE do Delphi }
InitWizard name WizardEntryPoint;
begin
end.
Como você pode ver, trata-se de um arquivo mínimo. Os dois pontos importantes é que ele usa o ca-
beçalho library para indicar que é uma DLL e exporta a função InitWiz( ) para ser inicializada pelo IDE.
Apenas algumas mudanças foram feitas na unidade Main nesse projeto. Como já dissemos, a variável
MainForm deve ser definida como nil quando o assistente não está ativo. Como você aprendeu no Capítulo
2, a variável de instância MainForm automaticamente terá o valor nil na inicialização da aplicação. Além
disso, no manipulador de evento OnClose do formulário, a instância do formulário é liberada e a global
MainForm é redefinida como nil. Veja o método a seguir:
O toque final desse assistente é abrir os arquivos no Code Editor do IDE quando o usuário der um
clique duplo na caixa de listagem do formulário principal. Essa lógica é manipulada por um novo méto-
do FileLBDblClick( ), mostrado a seguir:
procedure TMainForm.FileLBDblClick(Sender: TObject);
{ Chamado quando o usuário dá um clique duplo na caixa de listagem. Carrega
o arquivo no IDE }
var
FileName: string;
Len: Integer;
begin
FileName := FileLB.Items[FileLB.ItemIndex];
{ Certifica-se de que o usuário deu um clique em um arquivo... }
if (FileName < > ‘’) and (Pos(‘File ‘, FileName) = 1) then
begin
{ Elimina “File “ e “:” da string }
FileName := Copy(FileName, 6, Length(FileName));
Len := Length(FileName);
if FileName[Len] = ‘:’ then SetLength(FileName, Len - 1);
{ Abre o projeto ou o arquivo }
if CompareText(ExtractFileExt(FileName), ‘.DPR’) = 0 then
ActionSvc.OpenProject(FileName, True)
else
ActionSvc.OpenFile(FileName);
end;
end;
855
Esse método emprega os métodos OpenFile( ) e OpenProject( ) de IOTAActionServices para abrir um de-
terminado arquivo.
A Listagem 26.8 mostra o código-fonte completo da unidade Main no projeto DDGSrch, e a Figura 26.7
mostra o assistente DDG Search fazendo seu trabalho dentro do IDE.
unit Main;
interface
uses
SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls, Buttons, ExtCtrls, Menus, SrchIni,
SrchU, ComCtrls, InitWiz;
type
TMainForm = class(TForm)
FileLB: TListBox;
PopupMenu1: TPopupMenu;
Font1: TMenuItem;
N1: TMenuItem;
Exit1: TMenuItem;
FontDialog1: TFontDialog;
StatusBar: TStatusBar;
AlignPanel: TPanel;
ControlPanel: TPanel;
ParamsGB: TGroupBox;
LFileSpec: TLabel;
LToken: TLabel;
lPathName: TLabel;
EFileSpec: TEdit;
EToken: TEdit;
PathButton: TButton;
OptionsGB: TGroupBox;
cbCaseSensitive: TCheckBox;
cbFileNamesOnly: TCheckBox;
cbRecurse: TCheckBox;
SearchButton: TBitBtn;
CloseButton: TBitBtn;
PrintButton: TBitBtn;
PriorityButton: TBitBtn;
View1: TMenuItem;
EPathName: TEdit;
procedure SearchButtonClick(Sender: TObject);
procedure PathButtonClick(Sender: TObject);
procedure FileLBDrawItem(Control: TWinControl; Index: Integer;
Rect: TRect; State: TOwnerDrawState);
procedure Font1Click(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure PrintButtonClick(Sender: TObject);
procedure CloseButtonClick(Sender: TObject);
procedure FileLBDblClick(Sender: TObject);
856
Listagem 26.8 Continuação
var
MainForm: TMainForm;
implementation
{$R *.DFM}
end;
end;
procedure TMainForm.ReadIni;
{ Lê os valores default do Registro }
begin
with SrchIniFile do
begin
EPathName.Text := ReadString(‘Defaults’, ‘LastPath’, ‘C:\’);
EFileSpec.Text := ReadString(‘Defaults’, ‘LastFileSpec’, ‘*.*’);
EToken.Text := ReadString(‘Defaults’, ‘LastToken’, ‘’);
cbFileNamesOnly.Checked := ReadBool(‘Defaults’, ‘FNamesOnly’, False);
cbCaseSensitive.Checked := ReadBool(‘Defaults’, ‘CaseSens’, False);
cbRecurse.Checked := ReadBool(‘Defaults’, ‘Recurse’, False);
Left := ReadInteger(‘Position’, ‘Left’, 100);
Top := ReadInteger(‘Position’, ‘Top’, 50);
Width := ReadInteger(‘Position’, ‘Width’, 510);
Height := ReadInteger(‘Position’, ‘Height’, 370);
end;
end;
procedure TMainForm.WriteIni;
{ Escreve as definições atuais no Registro }
begin
with SrchIniFile do
begin
WriteString(‘Defaults’, ‘LastPath’, EPathName.Text);
WriteString(‘Defaults’, ‘LastFileSpec’, EFileSpec.Text);
WriteString(‘Defaults’, ‘LastToken’, EToken.Text);
WriteBool(‘Defaults’, ‘CaseSens’, cbCaseSensitive.Checked);
WriteBool(‘Defaults’, ‘FNamesOnly’, cbFileNamesOnly.Checked);
WriteBool(‘Defaults’, ‘Recurse’, cbRecurse.Checked);
WriteInteger(‘Position’, ‘Left’, Left);
WriteInteger(‘Position’, ‘Top’, Top);
WriteInteger(‘Position’, ‘Width’, Width);
WriteInteger(‘Position’, ‘Height’, Height);
end;
end;
end.
861
FIGURA 26.7 O assistente DDG Search em ação.
Assistentes de formulário
Outro tipo de assistente que possui suporte da API Open Tools é o assistente de formulário. Uma vez
instalados, os assistentes de formulário são acessados a partir da caixa de diálogo New Items; eles ge-
ram novos formulários e unidades para o usuário. O Capítulo 24 empregou esse tipo de assistente
para gerar novos formulários AppBar; no entanto, você não conseguiu ver o código que fez o assistente
funcionar.
É extremamente simples criar um assistente de formulário, embora você deva implementar uma
boa quantidade de métodos de interface. A criação de um assistente de formulário pode ser dividida em
cinco etapas básicas:
1. Crie uma classe descendente de TCustomForm, TDataModule ou qualquer TWinControl, que será usada
como a classe básica do formulário. Geralmente, essa classe residirá em uma unidade separada do
assistente. Nesse caso, TAppBar servirá como a classe básica.
2. Crie um descendente de TNotifierObject que implemente as seguintes interfaces: IOTAWizard, IOTARepo-
sitoryWizard, IOTAFormWizard, IOTACreator e IOTAModuleCreator.
3. No seu método IOTAWizard.Execute( ), você normalmente chamará IOTAModuleServices.GetNewModu-
leAndClassName( ) para obter uma nova unidade e nome de classe para seu assistente e IOTAModuleSer-
vices.CreateModule( ) para instruir o IDE a começar a criação do novo módulo.
4. Muitas das implementações de método das interfaces mencionadas acima possuem apenas uma li-
nha. Os não triviais são os métodos NewFormFile( ) e NewImplFile( ) da IOTAModuleCreator, que retor-
nam o código para o formulário e a unidade, respectivamente. O método IOTACreator.GetOwner( )
pode ser um pouco estranho, mas o exemplo a seguir lhe dá uma boa técnica para adicionar a unida-
de ao projeto atual (se houver).
5. Complete o procedimento Register( ) do assistente registrando um manipulador para a nova classe
de formulário, usando o procedimento RegisterCustomModule( ) na unidade DsgnIntf, e criando o as-
sistente, chamando o procedimento RegisterPackageWizard( ) na unidade ToolsAPI.
A Listagem 26.9 mostra o código-fonte de ABWizard.pas, que é o assistente AppBar.
862
Listagem 26.9 ABWizard.pas, a unidade que contém a implementação do assistente AppBar
unit ABWizard;
interface
type
TAppBarWizard = class(TNotifierObject, IOTAWizard, IOTARepositoryWizard,
IOTAFormWizard, IOTACreator, IOTAModuleCreator)
private
FUnitIdent: string;
FClassName: string;
FFileName: string;
protected
// Métodos de IOTAWizard
function GetIDString: string;
function GetName: string;
function GetState: TWizardState;
procedure Execute;
// Métodos de IOTARepositoryWizard / IOTAFormWizard
function GetAuthor: string;
function GetComment: string;
function GetPage: string;
function GetGlyph: HICON;
// Métodos de IOTACreator
function GetCreatorType: string;
function GetExisting: Boolean;
function GetFileSystem: string;
function GetOwner: IOTAModule;
function GetUnnamed: Boolean;
// Métodos de IOTAModuleCreator
function GetAncestorName: string;
function GetImplFileName: string;
function GetIntfFileName: string;
function GetFormName: string;
function GetMainForm: Boolean;
function GetShowForm: Boolean;
function GetShowSource: Boolean;
function NewFormFile(const FormIdent, AncestorIdent: string): IOTAFile;
function NewImplSource(const ModuleIdent, FormIdent,
AncestorIdent: string): IOTAFile;
function NewIntfSource(const ModuleIdent, FormIdent,
AncestorIdent: string): IOTAFile;
procedure FormCreated(const FormEditor: IOTAFormEditor);
end;
implementation
{$R CodeGen.res}
type
863
Listagem 26.9 Continuação
TBaseFile = class(TInterfacedObject)
private
FModuleName: string;
FFormName: string;
FAncestorName: string;
public
constructor Create(const ModuleName, FormName, AncestorName: string);
end;
{ TBaseFile }
{ TUnitFile }
{ TFormFile }
864
Listagem 26.9 Continuação
{ TAppBarWizard }
{ TAppBarWizard.IOTAWizard }
procedure TAppBarWizard.Execute;
begin
(BorlandIDEServices as IOTAModuleServices).GetNewModuleAndClassName(
‘AppBar’, FUnitIdent, FClassName, FFileName);
(BorlandIDEServices as IOTAModuleServices).CreateModule(Self);
end;
{ TAppBarWizard.IOTARepositoryWizard / TAppBarWizard.IOTAFormWizard }
{ TAppBarWizard.IOTACreator }
866
Listagem 26.9 Continuação
{ TAppBarWizard.IOTAModuleCreator }
867
Listagem 26.9 Continuação
end.
unit %0:s;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs, AppBars;
type
T%1:s = class(%2:s)
private
{ Declarações privadas }
public
{ Declarações públicas }
end;
var
%1:s: T%1:s;
implementation
{$R *.DFM}
end.
868
O registro do assistente e do módulo personalizado ocorre dentro de um procedimento Register( )
no pacote de projeto que contém o assistente, usando as duas linhas a seguir:
RegisterCustomModule(TAppBar, TCustomModule);
RegisterPackageWizard(TAppBarWizard.Create);
Resumo
Depois da leitura deste capítulo, você deverá ter uma compreensão maior das diversas unidades e interfa-
ces envolvidas na API Open Tools do Delphi. Em particular, você deverá saber e entender as questões en-
volvidas na criação de assistentes que são plugados ao IDE. O próximo capítulo completa esta parte do li-
vro com uma discussão completa da tecnologia CORBA e sua implementação no Delphi.
869
Desenvolvimento CAPÍTULO
ORB
O impulsionador da arquitetura CORBA é o ORB (Object Request Broker). O ORB fornece a implemen-
tação da especificação CORBA e é a cola (ou middleware) que une a solução inteira. Se você conhece a
tecnologia COM/DCOM da Microsoft, perceberá que o ORB fornece camadas de transporte, runtime e
segurança semelhantes às que são encontradas na biblioteca do COM/DCOM. Toda a comunicação en-
tre cliente e servidor passa pelo ORB de modo que os parâmetros e as chamadas de método possam ser
resolvidos no espaço de endereço do responsável pela chamada ou ou quem foi chamado (marshaling). O
ORB também fornece muitas rotinas auxiliadoras que podem ser chamadas diretamente de um cliente ou
servidor, semelhantes à funcionalidade que oleaut32.dll fornece para o COM/DCOM. Como dissemos, a
especificação CORBA não fornece a implementação de uma biblioteca ORB. Como a construção de um
ORB não é uma tarefa das mais simples, os programadores em CORBA são dependentes de terceiros
para fornecer implementações ORB compatíveis com CORBA. As boas-novas são que muitos fornecedo-
res estão disponibilizando implementações ORB, hoje encontradas tanto para as principais plataformas
(como Windows e UNIX) como para os sistemas operacionais mais obscuros. Atualmente, as duas imple-
mentações CORBA mais reconhecidas são o Inprise VisiBroker ORB e o IONA Orbix ORB.
Interfaces
Uma solução CORBA pode ser composta de vários objetos, desenvolvidos em uma mistura heterogênea
de linguagens de desenvolvimento e executados em uma série de plataformas diferentes. Por essa razão,
existe a necessidade de alternativas-padrão para que os objetos se façam representar para outros objetos,
clientes e o ORB. Essa representação é feita por meio de uma interface. Uma interface define uma lista de
métodos disponíveis e seus parâmetros, mas não serve para implementar alguma funcionalidade dessas
rotinas. Quando um objeto CORBA implementa uma interface, está garantindo que implementa todos
os métodos definidos pela interface. Em seu nível mais baixo, uma interface não passa de uma tabela de
funções ou uma lista de pontos de entrada nos métodos específicos. Como essa construção pode ser re-
presentada em qualquer plataforma de hardware e por qualquer ferramenta de desenvolvimento séria, as
interfaces se tornam a língua universal do mundo CORBA. Como a sintaxe pode variar de modo signifi-
cativo de uma linguagem de desenvolvimento para outra, o OMG definiu a IDL (Interface Definition
Language), que é usada para definir interfaces CORBA. A IDL é a linguagem-padrão para definir interfa-
ces CORBA e muitas ferramentas de desenvolvimento são capazes de traduzir IDL para sua sintaxe nati-
va a fim de permitir que os programadores construam facilmente interfaces compatíveis com CORBA.
Com o Delphi, não precisaremos escrever a IDL manualmente; em vez disso, o editor de biblioteca de ti-
pos nos permitirá definir visualmente nossas interfaces e opcionalmente exportar o código IDL corres-
pondente.
Stubs e estruturas
O mecanismo CORBA trabalha usando proxies. Atualmente, o uso de proxies é o principal padrão de
projeto para resolver os complexos problemas associados à passagem de dados entre objetos distribuí-
dos. Um proxy pertence tanto ao cliente quanto ao servidor e aparece tanto para o cliente quanto para o
servidor que está se comunicando com um processo local. Em seguida, o ORB manipula todos os deta-
871
lhes confusos que precisam ocorrer entre os proxies (por exemplo, o marshaling e a comunicação de
rede, entre outras coisas). Essa arquitetura, mostrada na Figura 27.1, livra os programadores de um cli-
ente ou servidor CORBA dos detalhes de transporte de baixo nível e permite que se concentrem na im-
plementação das soluções específicas a seu negócio. Em termos de CORBA, o proxy que representa o
servidor com o qual um cliente se comunica é chamado de stub e um proxy que representa um cliente no
lado do servidor é chamado de estrutura. Quando você está criando um objeto servidor CORBA usando
o assistente do Delphi, uma unidade contendo definições de interface para o stub e a estrutura será auto-
maticamente gerada.
Implementação Servidor
do objeto
Interface
Estrutura
ORB ORB
Stub
Interface
Cliente
O VisiBroker ORB
Como dissemos, CORBA é um padrão que precisa de terceiros para implementar os serviços ORB. O
suporte a CORBA oferecido nos Delphi 4 e 5 usa o VisiBroker ORB da Inprise para implementar a es-
pecificação CORBA. O produto da VisiBroker fornece pleno suporte à especificação CORBA, bem
como a muitas extensões do VisiBroker como, por exemplo, serviços de evento e atribuição de
nome. Como este livro não se propõe a fazer uma análise completa do VisiBroker, vamos nos con-
centrar nas partes do produto mais pertinentes à implementação do CORBA no Delphi. Mais infor-
mações sobre o VisiBroker, incluindo a documentação do produto, podem ser encontradas em
www.borland.com/visibroker.
O IREP
O IREP (Interface Repository) é um banco de dados on-line de informações de tipo de objeto. Esse repo-
sitório é necessário para clientes que desejem vincular dinamicamente interfaces CORBA. O ORB pode
usar a informação de tipo no repositório de interface para apresentar corretamente chamadas de método
de vinculação dinâmica. Para que a vinculação dinâmica seja usada, o Interface Repository deve estar
sendo executado em algum local na rede acessível aos clientes, e a interface a ser usada deve ser registra-
da com o repositório.
Ferramenta Finalidade
TObject
TCorbaStub TCorbaInterfaceIDManager
TCorbaDispatchStub TCorbaStubManager
TCorbaSkeleton TCorbaSkeletonManager
TCorbaFactory TCorbaFactoryManager
TCorbaObjectFactory
Agora dê um clique em CORBA Object a fim de ver o CORBA Object Wizard (assistente de objeto
CORBA), mostrado na Figura 27.4.
Preencha o nome da classe com o nome desejado para seu objeto e interface CORBA. Observe que
provavelmente você não deverá usar a convenção padrão do Delphi de começar o nome da classe com
um T, pois ele será automaticamente adicionado para você. Por exemplo, se você digitar MeuObjeto,
uma classe do Delphi chamada TMeuObjeto será gerada para implementar a interface IMeuObjeto.
A opção Instancing (instanciando) determina o modo como as instâncias de objeto são manipuladas
para clientes. Uma das duas opções a seguir pode ser escolhida:
l Shared Instance (instância compartilhada). Esse modelo normalmente é usado para desenvolvi-
mento CORBA. Cada cliente usa uma instância compartilhada da implementação do objeto. Os
servidores CORBA que usam esse modelo devem ser construídos como servidores “sem estado”.
Como muitos clientes podem estar compartilhando uma instância, nenhum cliente tem a garan-
876 tia de localizar o servidor no mesmo estado que ele estava depois da última chamada.
lInstance-per-client (instância por cliente). O modelo de instância por cliente constrói uma ins-
tância exclusiva de um objeto para cada cliente que solicite um serviço do objeto. Esse modelo
permite a construção de objetos de “estado” que mantêm um estado coerente para todas as cha-
madas de cliente. No entanto, esse modelo pode fazer uso mais intensivo de recursos, pois obriga
os servidores a monitorarem o estado de clientes conectados de modo que os objetos possam ser
liberados quando os clientes são terminados com eles.
A opção Threading Model (modelo de encaminhamento) especifica o modo como os objetos
CORBA serão chamados. Veja a seguir as duas opções disponíveis:
lSingle-threaded (único thread). Cada instância do objeto será chamada de um thread único; por
essa razão, o objeto não precisa ser colocado no modo de thread seguro. Observe que a aplicação
do servidor CORBA pode conter múltiplos objetos ou instâncias; por essa razão, os dados glo-
bais ou compartilhados devem ser mantidos no modo de thread seguro.
lMultithreaded. Embora cada conexão de cliente venha a fazer chamadas em um thread de cliente
dedicado, os objetos podem receber chamadas concorrentes de múltiplos threads. Nesse cená-
rio, os dados globais e de objeto devem ser colocados no modo de thread seguro. O cenário mais
difícil de implementar (independente de preocupações com threading) é quando você está usan-
do uma instância de objeto compartilhado com o modelo multithread. O mais simples seria re-
correr ao modelo de uma instância por cliente com único thread ativado.
Não se esqueça de que a simples seleção de uma opção de thread não serve para implementar seus
servidores ou objetos em um modo de thread seguro. Essas opções só servem para especificar o modelo
de thread que o seu objeto suporta. Continua sendo sua responsabilidade implementar seus servidores
CORBA em um modo de thread seguro, baseado no modelo de thread desejado.
Depois de preencher o assistente CORBA com êxito, duas unidades de código Pascal serão geradas.
Uma unidade stub/estrutura será gerada seguindo o padrão de nomeação SeuProjeto_TLB.pas. Esse ar-
quivo conterá a definição da interface principal do seu objeto, uma classe de stub e estrutura, uma classe
factory da classe CORBA e código para registrar o stub, a estrutura e a interface com os mecanismos
apropriados do Delphi. A Listagem 27.1 mostra o código gerado para uma classe nomeada “My-
FirstCORBAServer”.
unit FirstCorbaServer_TLB;
// ************************************************************************ //
// ATENÇÃO
// -------
// Os tipos declarados neste arquivo foram gerados da leitura de dados de uma
// Type Library. Se essa biblioteca de tipos for explícita ou indiretamente
// (através de outra biblioteca de tipos que faça referência a essa biblioteca
// de tipos) reimportada, ou o comando ‘Refresh’ do Type Library Editor ativado
// durante a edição da Type Library, o conteúdo deste arquivo será gerado
// novamente, e todas as modificações manuais serão perdidas.
// ************************************************************************ //
// ************************************************************************ //
// Biblioteca de tipos: C:\ICON99\FirstCORBAServer\FirstCorbaServer.tlb (1)
// IID\LCID: {CE8DB340-913A-11D3-9706-0000861F6726}\0
// Helpfile:
// DepndLst:
// (1) v2.0 stdole, (C:\WINDOWS\SYSTEM\STDOLE2.TLB)
// (2) v4.0 StdVCL, (C:\WINDOWS\SYSTEM\STDVCL40.DLL) 877
Listagem 27.1 Continuação
// ************************************************************************ //
{$TYPEDADDRESS OFF} // Unidade será compilada sem ponteiros com tipo verificado.
interface
// *********************************************************************//
// GUIDS declareadas na TypeLibrary. Os seguintes prefixos são usados:
// Bibliotecas de tipo: LIBID_xxxx
// CoClasses : CLASS_xxxx
// DISPInterfaces : DIID_xxxx
// Interfaces não-DISP: IID_xxxx
// *********************************************************************//
const
// Versões principal e secundária de TypeLibrary
FirstCorbaServerMajorVersion = 1;
FirstCorbaServerMinorVersion = 0;
// *********************************************************************//
// Encaminha declaração de tipos definidos na TypeLibrary
// *********************************************************************//
IMyFirstCorbaServer = interface;
IMyFirstCorbaServerDisp = dispinterface;
// *********************************************************************//
// Declaração de CoClasses definidas na Type Library
// (NOTA: Aqui mapeamos cada CoClass para sua interface-padrão)
// *********************************************************************//
MyFirstCorbaServer = IMyFirstCorbaServer;
// *********************************************************************//
// Interface: IMyFirstCorbaServer
// Flags: (4416) Dual OleAutomation Dispatchable
// GUID: {CE8DB341-913A-11D3-9706-0000861F6726}
// *********************************************************************//
IMyFirstCorbaServer = interface(IDispatch)
[‘{CE8DB341-913A-11D3-9706-0000861F6726}’]
procedure SayHelloWorld; safecall;
end;
// *********************************************************************//
// DispIntf: IMyFirstCorbaServerDisp
// Flags: (4416) Dual OleAutomation Dispatchable
// GUID: {CE8DB341-913A-11D3-9706-0000861F6726}
// *********************************************************************//
878
Listagem 27.1 Continuação
IMyFirstCorbaServerDisp = dispinterface
[‘{CE8DB341-913A-11D3-9706-0000861F6726}’]
procedure SayHelloWorld; dispid 1;
end;
TMyFirstCorbaServerSkeleton = class(TCorbaSkeleton)
private
FIntf: IMyFirstCorbaServer;
public
constructor Create(const InstanceName: string; const Impl: IUnknown);
override;
procedure GetImplementation(out Impl: IUnknown); override; stdcall;
published
procedure SayHelloWorld(const InBuf: IMarshalInBuffer; Cookie: Pointer);
end;
// *********************************************************************//
// A classe CoMyFirstCorbaServer fornece um método Create e CreateRemote para
// criar instância da interface-padrão IMyFirstCorbaServer exposta pela
// CoClass MyFirstCorbaServer. As funções devem ser usadas por clientes que
// automatizarem os objetos expostos pelo servidor dessa TypeLibrary.
// *********************************************************************//
CoMyFirstCorbaServer = class
class function Create: IMyFirstCorbaServer;
class function CreateRemote(const MachineName: string): IMyFirstCorbaServer;
end;
TMyFirstCorbaServerCorbaFactory = class
class function CreateInstance(const InstanceName: string):
IMyFirstCorbaServer;
end;
implementation
uses ComObj;
{ TMyFirstCorbaServerStub }
procedure TMyFirstCorbaServerStub.SayHelloWorld;
var
OutBuf: IMarshalOutBuffer;
InBuf: IMarshalInBuffer;
begin
FStub.CreateRequest(‘SayHelloWorld’, True, OutBuf);
FStub.Invoke(OutBuf, InBuf);
end;
{ TMyFirstCorbaServerSkeleton }
879
Listagem 27.1 Continuação
procedure TMyFirstCorbaServerSkeleton.SayHelloWorld(
const InBuf: IMarshalInBuffer; Cookie: Pointer);
var
OutBuf: IMarshalOutBuffer;
begin
FIntf.SayHelloWorld;
FSkeleton.GetReplyBuffer(Cookie, OutBuf);
end;
initialization
CorbaStubManager.RegisterStub(IMyFirstCorbaServer, TMyFirstCorbaServerStub);
CorbaInterfaceIDManager.RegisterInterface(IMyFirstCorbaServer,
‘IDL:FirstCorbaServer/IMyFirstCorbaServer:1.0’);
CorbaSkeletonManager.RegisterSkeleton(IMyFirstCorbaServer,
TMyFirstCorbaServerSkeleton);
end.
880
Se examinarmos essa unidade de stub e estrutura, vale observar que a classe da estrutura não imple-
menta a interface IMyFirstCorbaServer. A estrutura terá os mesmos métodos que a interface de suporte, mas
você perceberá que os parâmetros são diferentes. Os métodos da estrutura receberão informações brutas e
ordenadas, que em seguida devem desordenar os parâmetros e passá-los para a interface apropriada. Por
essa razão, a estrutura não implementa a interface diretamente. Em vez disso, a estrutura armazenará uma
referência interna para a interface de suporte e delegará suas chamadas para essa referência interna.
A segunda unidade gerada conterá o stub para implementar seu objeto. Uma classe do Delphi que
descende de TCorbaImplementation e implementa a interface principal será gerada. Essa unidade também
criará uma instância da factory responsável pela criação do objeto CORBA. Uma unidade típica de im-
plementação de objeto CORBA seria semelhante ao código mostrado na Listagem 27.2.
unit uMyFirstCorbaServer;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, ComObj, StdVcl,
CorbaObj, FirstCorbaServer_TLB;
type
implementation
uses CorbInit;
procedure TMyFirstCorbaServer.SayHelloWorld;
begin
// Implemente o método aqui.
end;
initialization
TCorbaObjectFactory.Create(‘MyFirstCorbaServerFactory’, ‘MyFirstCorbaServer’,
‘IDL:FirstCorbaServer/MyFirstCorbaServerFactory:1.0’, IMyFirstCorbaServer,
TMyFirstCorbaServer, iMultiInstance, tmSingleThread);
end.
Essa unidade por fim conterá o código que implementa todos os métodos da interface
IMyFirstCORBAServer, bem como qualquer funcionalidade interna da classe TMyFirstCORBAServer. Usando a
herança de implementação clássica, que descende de TCorbaImplementation, a implementação automatica-
mente será capaz de se tornar um objeto CORBA. Suportando a interface IMyFirstCorbaServer, o objeto ga-
rante que obedecerá ao contrato dessa interface. Em vez de declarar manualmente a interface e a imple-
mentação do objeto, vamos nos voltar para o Visual Type Library Editor do Delphi. 881
Type Library Editor do Delphi
Para implementar na íntegra esse objeto CORBA personalizado, o código deve ser adicionado à unidade
de stub e estrutura e à unidade de implementação do objeto listada anteriormente. Embora em princípio
isso dê a impressão de ser uma tarefa cansativa, o Type Library Editor do Delphi está disponível para aju-
dá-lo nesse processo. Abra o menu principal do Delphi e selecione View, Type Library (exibir, biblioteca
de tipos). Você verá a janela mostrada na Figura 27.5, que representa visualmente as interfaces e outras
entidades definidas na unidade de stub e estrutura.
Neste ponto, você pode selecionar a interface IMyFirstCorbaServer no editor e dar um clique em speedbutton
para adicionar um novo método. Uma vez que o método tenha sido adicionado, você pode usar a interfa-
ce visual do editor para definir parâmetros e retornar tipos, entre outras coisas. Observe que todos os ti-
pos de dados mostrados como possíveis tipos de parâmetro no Type Library Editor são válidos para obje-
tos CORBA. Como atualmente o Type Library Editor é uma ferramenta tanto para COM como para
CORBA, muitos dos tipos de dados são válidos apenas para objetos COM/Automation. Os arquivos de a-
juda do Delphi fornecem gigantescas listas de tipos de dados CORBA (IDL) válidos. Quando você tiver
usado o Type Library Editor para adicionar os métodos de sua interface, um clique em Refresh speedbut-
ton (atualizar speedbuttom) recriará o código no seu projeto. A unidade de stub e estrutura será atualiza-
da e os métodos de implementação vazios serão adicionados à sua unidade de implementação. Só lhe res-
ta preencher o código e implementar os métodos vazios que o Type Library Editor gera.
NOTA
O Delphi 5 contém um novo recurso que gerará invólucros de componente de CoClasses contidos em uma bi-
blioteca de tipos. Infelizmente, os invólucros são gerados se você estiver importando uma biblioteca de tipos
existente ou se estiver criando uma manualmente. Esses invólucros de componente não são apropriados para
um objeto CORBA e, portanto, você deve executar as etapas a seguir para impedir a criação desse código adi-
cional. No menu do Delphi, selecione Project, Import Type Library (projeto, importar biblioteca de tipos). Quan-
do aparecer a caixa de diálogo, desative a caixa de seleção “Generate Component Wrapper” (gerar wrapper
de componente) e feche a caixa de diálogo clicando em Close no canto superior direito. Finalmente, dê um cli-
que em Refresh speedbutton no Type Library Editor. O código estranho será eliminado de sua aplicação.
no arquivo de projeto da aplicação. No exemplo em questão, gostaríamos de verificar que o servidor está
sendo executado e portanto vamos deixar o formulário visível e fornecer TLabel para nos informar que
nosso servidor CORBA está ativo. Esse formulário é mostrado na Figura 27.6.
Não se esqueça de que esse formulário deve ser considerado dados globais. Muito embora tenha-
mos criado o objeto CORBA com um modelo single-threaded, a aplicação do servidor CORBA poderia
conter outros objetos que estejam atendendo a chamadas em outros threads. Portanto, o acesso a esse
formulário a partir do código do objeto não seria considerado um thread seguro.
884
Listagem 27.3 A unidade de stub e estrutura de IQueryServer
unit SimpleCorbaServer_TLB;
// ************************************************************************ //
// ATENÇÃO
// -----
// Os tipos declarados neste arquivo foram gerados da leitura de dados de uma
// Type Library. Se essa biblioteca de tipos for explícita ou indiretamente (via
// outra biblioteca de tipos que faça referência a essa biblioteca de tipos)
// reimportada, ou o comando ‘Refresh’ do Type Library Editor ativado durante
// a edição da Type Library, o conteúdo desse arquivo será recriado e todas as
// modificações manuais serão perdidas.
// ************************************************************************ //
// ************************************************************************ //
// Biblioteca de tipos: C:\ICON99\CORBA Server\SimpleCorbaServer.tlb (1)
// IID\LCID: {B7D4ED80-27C2-111D3-9703-0000861F6726}\0
// Helpfile:
// DepndLst:
// (1) v2.0 stdole, (C:\WINDOWS\SYSTEM\STDOLE2.TLB)
// (2) v4.0 StdVCL, (C:\WINDOWS\SYSTEM\STDVCL40.DLL)
// ************************************************************************ //
{$TYPEDADDRESS OFF} // Unidade será compilada sem ponteiros de tipo verificados.
interface
// *********************************************************************//
// GUIDS declaradas na TypeLibrary. Os seguintes prefixos são usados:
// Bibliotecas de tipo: LIBID_xxxx
// CoClasses : CLASS_xxxx
// DISPInterfaces : DIID_xxxx
// Interfaces não-DISP: IID_xxxx
// *********************************************************************//
const
// Versões principal e secundária de TypeLibrary
SimpleCorbaServerMajorVersion = 1;
SimpleCorbaServerMinorVersion = 0;
// *********************************************************************//
// Encaminha declaração de tipos definidos em TypeLibrary
// *********************************************************************//
IQueryServer = interface;
IQueryServerDisp = dispinterface;
885
Listagem 27.3 Continuação
// *********************************************************************//
// Declaração de CoClasses definidas em Type Library
// (NOTA: Aqui mapeamos cada CoClass para sua interface-padrão)
// *********************************************************************//
QueryServer = IQueryServer;
// *********************************************************************//
// Interface: IQueryServer
// Flags: (4416) Dual OleAutomation Dispatchable
// GUID: {B7D4ED81-27C2-11D3-9703-0000861F6726}
// *********************************************************************//
IQueryServer = interface(IDispatch)
[‘{B7D4ED81-27C2-11D3-9703-0000861F6726}’]
function Login(const Db: WideString; const User: WideString;
const Password: WideString): WordBool; safecall;
function Get_SQL: WideString; safecall;
procedure Set_SQL(const Value: WideString); safecall;
procedure Next; safecall;
procedure Prev; safecall;
procedure First; safecall;
procedure Last; safecall;
function Get_FieldCount: Integer; safecall;
function Data: OleVariant; safecall;
function Get_EOF: WordBool; safecall;
function Get_BOF: WordBool; safecall;
function Execute: WordBool; safecall;
property SQL: WideString read Get_SQL write Set_SQL;
property FieldCount: Integer read Get_FieldCount;
property EOF: WordBool read Get_EOF;
property BOF: WordBool read Get_BOF;
end;
// *********************************************************************//
// DispIntf: IQueryServerDisp
// Flags: (4416) Dual OleAutomation Dispatchable
// GUID: {B7D4ED81-27C2-11D3-9703-0000861F6726}
// *********************************************************************//
IQueryServerDisp = dispinterface
[‘{B7D4ED81-27C2-11D3-9703-0000861F6726}’]
function Login(const Db: WideString; const User: WideString;
const Password: WideString): WordBool; dispid 1;
property SQL: WideString dispid 2;
procedure Next; dispid 3;
procedure Prev; dispid 4;
procedure First; dispid 5;
procedure Last; dispid 6;
property FieldCount: Integer readonly dispid 7;
function Data: OleVariant; dispid 8;
property EOF: WordBool readonly dispid 9;
property BOF: WordBool readonly dispid 11;
function Execute: WordBool; dispid 12;
end;
886
Listagem 27.3 Continuação
TQueryServerSkeleton = class(TCorbaSkeleton)
private
FIntf: IQueryServer;
public
constructor Create(const InstanceName: string; const Impl: IUnknown);
override;
procedure GetImplementation(out Impl: IUnknown); override; stdcall;
published
procedure Login(const InBuf: IMarshalInBuffer; Cookie: Pointer);
procedure Get_SQL(const InBuf: IMarshalInBuffer; Cookie: Pointer);
procedure Set_SQL(const InBuf: IMarshalInBuffer; Cookie: Pointer);
procedure Next(const InBuf: IMarshalInBuffer; Cookie: Pointer);
procedure Prev(const InBuf: IMarshalInBuffer; Cookie: Pointer);
procedure First(const InBuf: IMarshalInBuffer; Cookie: Pointer);
procedure Last(const InBuf: IMarshalInBuffer; Cookie: Pointer);
procedure Get_FieldCount(const InBuf: IMarshalInBuffer; Cookie: Pointer);
procedure Data(const InBuf: IMarshalInBuffer; Cookie: Pointer);
procedure Get_EOF(const InBuf: IMarshalInBuffer; Cookie: Pointer);
procedure Get_BOF(const InBuf: IMarshalInBuffer; Cookie: Pointer);
procedure Execute(const InBuf: IMarshalInBuffer; Cookie: Pointer);
end;
// *********************************************************************//
// A Classe CoQueryServer fornece um método Create e CreateRemote para
// criar instâncias da interface-padrão IQueryServer exposta pela CoClass
// QueryServer. As funções devem ser usadas por clientes que queiram
// automatizar os objetos CoClass expostos pelo servidor desta TypeLibrary.
// *********************************************************************//
CoQueryServer = class
class function Create: IQueryServer;
class function CreateRemote(const MachineName: string): IQueryServer;
end;
TQueryServerCorbaFactory = class
class function CreateInstance(const InstanceName: string): IQueryServer;
end;
887
Listagem 27.3 Continuação
implementation
uses ComObj;
{ TQueryServerStub }
procedure TQueryServerStub.Next;
var
OutBuf: IMarshalOutBuffer;
InBuf: IMarshalInBuffer;
begin
FStub.CreateRequest(‘Next’, True, OutBuf);
FStub.Invoke(OutBuf, InBuf);
end;
procedure TQueryServerStub.Prev;
var
OutBuf: IMarshalOutBuffer;
InBuf: IMarshalInBuffer;
888
Listagem 27.3 Continuação
begin
FStub.CreateRequest(‘Prev’, True, OutBuf);
FStub.Invoke(OutBuf, InBuf);
end;
procedure TQueryServerStub.First;
var
OutBuf: IMarshalOutBuffer;
InBuf: IMarshalInBuffer;
begin
FStub.CreateRequest(‘First’, True, OutBuf);
FStub.Invoke(OutBuf, InBuf);
end;
procedure TQueryServerStub.Last;
var
OutBuf: IMarshalOutBuffer;
InBuf: IMarshalInBuffer;
begin
FStub.CreateRequest(‘Last’, True, OutBuf);
FStub.Invoke(OutBuf, InBuf);
end;
889
Listagem 27.3 Continuação
{ TQueryServerSkeleton }
890
Listagem 27.3 Continuação
891
Listagem 27.3 Continuação
initialization
CorbaStubManager.RegisterStub(IQueryServer, TQueryServerStub);
CorbaInterfaceIDManager.RegisterInterface(IQueryServer,
‘IDL:SimpleCorbaServer/IQueryServer:1.0’);
CorbaSkeletonManager.RegisterSkeleton(IQueryServer, TQueryServerSkeleton);
end.
Observe que o Type Library Editor, juntamente com os assistentes do Delphi, geraram todo o códi-
go necessário para ordenar corretamente os parâmetros. Os parâmetros são ordenados do stub para o
ORB e são desordenados da estrutura para a implementação de objeto propriamente dita.
O único código que teremos de escrever é mostrado na Listagem 27.4. Você pode ver que só temos
de lidar corretamente com a implementação do comportamento do nosso objeto; não temos de nos preo-
cupar com os detalhes confusos da CORBA e do processo de ordenação de parâmetro.
unit uQueryServer;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, ComObj, StdVcl,
CorbaObj, db, dbtables, orbpas, SimpleCorbaServer_TLB, frmqueryserver;
type
implementation
uses CorbInit;
procedure TQueryServer.First;
begin
FQuery.First;
end;
procedure TQueryServer.Last;
begin
FQuery.Last;
end; 893
Listagem 27.4 Continuação
procedure TQueryServer.Next;
begin
FQuery.Next;
end;
procedure TQueryServer.Prev;
begin
FQuery.Prior;
end;
destructor TQueryServer.Destroy;
begin
FQuery.Free;
FDatabase.Free;
inherited Destroy;
end;
initialization
TCorbaObjectFactory.Create(‘QueryServerFactory’, ‘QueryServer’,
‘IDL:SimpleCorbaServer/QueryServerFactory:1.0’, IQueryServer,
TQueryServer, iMultiInstance, tmSingleThread);
end.
Um detalhe da VCL que você deve observar no código da Listagem 27.4 é a manipulação correta do
objeto TDatabase. O espaço de nomes do BDE só aceita um banco de dados com um nome exclusivo den-
tro da mesma sessão. Como podemos ter múltiplos objetos TQueryServer dentro desse servidor CORBA
que estejam compartilhando um único objeto TSession, devemos definir a propriedade HandleShared de
TDatabase como True. Se não fizermos isso, o próximo cliente que crie um novo TQueryServer não consegui-
rá estabelecer uma conexão.
No Type Library Editor, você pode exibir a IDL que representa nossa interface. Dê um clique na
894
seta drop-down em Export to IDL speedbutton (exportar para speedbbutton IDL) no Type Library Edi-
tor e selecione Export to CORBA IDL (exportar para IDL CORBA) (observe que isso é semelhante, po-
rém diferente, da Microsoft IDL, ou MIDL). Você verá o código IDL no editor do Delphi, como mostra-
mos na Listagem 27.5.
module SimpleCorbaServer
{
interface IQueryServer;
interface IQueryServer
{
boolean Login(in wstring Db, in wstring User, in wstring Password);
wstring Get_SQL( );
wstring Set_SQL(in wstring Value);
void Next( );
void Prev( );
void First( );
void Last( );
long Get_FieldCount( );
any Data( );
boolean Get_EOF( );
boolean Get_BOF( );
boolean Execute( );
};
interface QueryServerFactory
{
IQueryServer CreateInstance(in string InstanceName);
};
};
Observe que os tipos de dados COM que selecionamos no Type Library Editor foram devidamente
convertidos para os seus equivalentes IDL. Essa IDL pode ser importada para qualquer outra ferramenta
que aceite CORBA. As ferramentas de desenvolvimento como CBuilder e JBuilder gerarão classes de
wrapper de modo que os clientes escritos nessas linguagens possam facilmente usar a funcionalidade do
nosso objeto CORBA do Delphi.
NOTA
A IDL gerada pelo Delphi, mostrada na Listagem 27.5, está ligeiramente incorreta. A função Set_SQL não
deverá estar retornando um valor. Embora o Delphi seja capaz de manipular isso corretamente, o proble-
ma deriva do fato de que adicionamos uma propriedade (SQL) no Type Library Editor. As propriedades são
reconhecidas pelo COM, mas não são uma construção normalmente encontradas em CORBA. O Delphi
criou os métodos de leitura e escrita da propriedade, mas não exportou corretamente o método de escrita
para a IDL. Esse problema pode ser evitado tão-somente com a declaração de métodos nas suas interfaces
CORBA ou pela edição manual da IDL gerada para corrigir a declaração da seguinte maneira:
895
Executando o servidor CORBA
A construção do nosso servidor de consulta finalmente está completa. Agora chegou a hora de executar a
aplicação do servidor CORBA e deixar o VisiBroker ORB saber que nosso objeto está disponível para os
clientes. Para que os clientes localizem e estabeleçam uma conexão com a implementação do objeto
CORBA usando o VisiBroker ORB, o VisiBroker Smart Agent deve estar sendo executado em algum
ponto da rede local. O agente não tem de estar sendo executado no mesmo computador que o cliente ou
o servidor. O Smart Agent pode ser inicializado a partir da linha de comandos (no Windows NT, o Smart
Agent pode ser executado como um serviço) digitando
OSAGENT [-opções]
Uma vez que o Smart Agent esteja sendo executado na rede, você pode executar o projeto que aca-
bamos de construir para que seja registrado com o Smart Agent e se torne disponível para as conexões do
cliente. Observe que, nesse ponto, você realmente deve executar a aplicação servidora; não há recurso
interno para carregar um servidor (como em DCOM) a não ser que você use o OAD.
896
Criando o cliente CORBA
Primeiro criaremos uma simples aplicação GUI do Delphi que servirá para exibir os resultados que obti-
vemos da interface IQueryServer, mostrado na Figura 27.8.
Tendo feito isso, vamos adicionar a unidade de stub e estrutura do exemplo do servidor à cláusula
uses da unidade do nosso formulário (SimpleCorbaServer_TLB.pas).
unit ufrmCorbaClient;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, SimpleCorbaServer_TLB, corbaObj, Grids;
type
TForm1 = class(TForm)
GroupBox1: TGroupBox;
Label2: TLabel;
edtDatabase: TEdit;
Label3: TLabel;
edtUserName: TEdit;
897
Listagem 27.6 Continuação
Label4: TLabel;
edtPassword: TEdit;
Button5: TButton;
GroupBox2: TGroupBox;
memoSQL: TMemo;
GroupBox3: TGroupBox;
Button6: TButton;
grdCorbaData: TStringGrid;
procedure ConnectClick(Sender: TObject);
procedure ExecuteClick(Sender: TObject);
private
{ Declarações privadas }
FQueryServer: IQueryServer;
public
{ Declarações públicas }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
grdCorbaData.ColCount := FQueryServer.FieldCount;
grdCorbaData.RowCount := 0;
j := 0;
while not(FQueryServer.EOF) do
begin
inc(j);
grdCorbaData.RowCount := j;
CorbaData := (FQueryServer.Data);
for i := 0 to FQueryServer.FieldCount - 1 do
begin
grdCorbaData.Cells[i + 1,j-1] := CorbaData[i];
end;
FQueryServer.Next;
end;
end;
end.
898
Desde que você tenha inicializado o Smart Agent e o servidor esteja sendo executado onde o Smart
Agent possa vê-lo, é possível executar essa aplicação e recuperar dados do nosso servidor CORBA!
unit ufrmCorbaClientLate;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, corbaObj, Grids;
type
TForm1 = class(TForm)
GroupBox1: TGroupBox;
Label2: TLabel;
edtDatabase: TEdit;
Label3: TLabel;
edtUserName: TEdit;
Label4: TLabel;
edtPassword: TEdit;
Button5: TButton;
GroupBox2: TGroupBox;
memoSQL: TMemo;
GroupBox3: TGroupBox;
Button6: TButton;
grdCorbaData: TStringGrid;
procedure ConnectClick(Sender: TObject);
procedure ExecuteClick(Sender: TObject);
private
{ Declarações privadas }
FQueryServer: TAny;
public
899
Listagem 27.7 Continuação
{ Declarações públicas }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
grdCorbaData.ColCount := FQueryServer.Get_FieldCount;
grdCorbaData.RowCount := 0;
j := 0;
while not(FQueryServer.Get_EOF) do
begin
inc(j);
grdCorbaData.RowCount := j;
CorbaData := FQueryServer.Data;
for i := 0 to FQueryServer.Get_FieldCount - 1 do
begin
grdCorbaData.Cells[i + 1,j-1] := CorbaData[i];
end;
FQueryServer.Next;
end;
end;
end.
O único argumento obrigatório aqui é IRname. Como múltiplas instâncias do Interface Repository
podem estar sendo executadas, esta precisa ser identificada de alguma forma. O argumento –console es-
pecifica se o Interface Repository é executado no modo console (o padrão é o modo GUI) e o argumento
arquivo.idl pode especificar um arquivo IDL inicial a ser carregado quando o repositório é iniciado.
Arquivos IDL adicionais podem ser carregados usando a opção de menu (se a execução for GUI) ou exe-
cutando o utilitário idl2ir.
module CorbaServer {
interface SimpleText {
string setText(in string txt);
};
};
Desde que o servidor CORBA tenha sido registrado com o Interface Repository, o Delphi poderá
facilmente acessar o servidor usando DII (esse código é mostrado na Listagem 27.9, no método btnDel-
phiTextEarly).
Para fazer uma vinculação inicial sem um compilador Idl2Pas, devemos escrever manualmente nos-
sa própria classe de stub para executar a ordenação do código. Embora não tenha nada de especial, essa
tarefa pode ser bastante cansativa e induzir a erro com facilidade, já que possui um grande número de
métodos. Também devemos registrar a classe do stub e a interface da classe do stub com os mecanismos
apropriados do Delphi. A Listagem 27.9 contém o código inteiro.
901
Listagem 27.9 O código para acessar um servidor Java do cliente Delphi (vinculado inicial e
tardiamente)
unit uDelphiClient;
interface
uses
Windows, Messages, SysUtils, CorbInit, CorbaObj, orbpas, Classes,
Graphics, Controls, Forms, Dialogs, StdCtrls;
type
ISimpleText = interface
[‘{49F25940-3C3C-11D3-9703-0000861F6726}’]
function SetText(const txt: String): String;
end;
TForm1 = class(TForm)
edtDelphiText: TEdit;
btnDelphiTextLate: TButton;
btnDelphiTextEarlyClick: TButton;
edtResult: TEdit;
procedure btnDelphiTextLateClick(Sender: TObject);
procedure btnDelphiTextEarlyClickClick(Sender: TObject);
private
{ Declarações privadas }
public
{ Declarações públicas }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
{ TSimpleTextStub }
InBuf: IMarshalInBuffer;
OutBuf: IMarshalOutBuffer;
begin
FStub.CreateRequest(‘setText’,True,OutBuf);
OutBuf.PutText(pchar(txt));
FStub.Invoke(OutBuf, InBuf);
right begin
JavaServer := CorbaBind(ISimpleText) as ISimpleText;
edtResult.Text := JavaServer.SetText(edtDelphiText.text);
end;
initialization
CorbaStubManager.RegisterStub(ISimpleText, TSimpleTextStub);
CorbaInterfaceIDManager.RegisterInterface(ISimpleText,
‘IDL:CorbaServer/SimpleText:1.0’);
end.
Você perceberá que o código acima é muito parecido com o código gerado pelo Type Library Edi-
tor quando criamos um objeto CORBA dentro do Delphi. Adicionamos nosso próprio descendente de
TCorbaStub, que servirá para fornecer o marshaling do lado do cliente. Observe que não é necessário des-
cender de TCorbaDispatchStub, pois o Type Library Editor não está envolvido aqui. Depois implementamos
nosso stub personalizado para ordenar os parâmetros para/de interfaces de marshaling de buffer da
CORBA: IMarshalInBuffer e IMarshalOutBuffer. Essas interfaces contêm métodos convenientes para leitura
e escrita de diversos tipos de dados para os buffers. Consulte a ajuda on-line do Delphi 5 para obter mais
informações sobre o uso desses métodos. Finalmente, precisamos registrar nosso stub personalizado e
nossa interface com o stub CORBA do Delphi. Esse código é mostrado na parte initialization de nossa
unidade.
O compilador Idl2Pas gerará dois arquivos no diretório especificado, baseados no nome do módulo
incluído no arquivo idl. No nosso exemplo, CorbaServer_i.pas conterá as declarações do Pascal das inter-
faces idl e aparece na Listagem 27.10.
903
Listagem 27.10 Definições geradas de IDL2PAS
unit CorbaServer_i;
// Este arquivo foi gerado no dia 4 Nov 1999, às 17:58:12 GMT, por versão
// 01.09.00.A2.032c do compilador Inprise VisiBroker idl2pas CORBA IDL.
// Esta unidade contém o código interface Pascal para CorbaServer do módulo IDL.
interface
uses
CORBA;
type
// Encaminha referências que tenham sido fornecidas para resolver dependências
// entre as interfaces a seguir.
SimpleText = interface;
// Essas definições de interface foram geradas a partir da IDL da qual esta
// unidade se originou.
implementation
initialization
end.
unit CorbaServer_c;
// c:\icon99\MultiLanguage\MyProjects\CorbaServer\SimpleText.idl.
interface
uses
CORBA,
CorbaServer_i;
type
// Encaminha referências que tenham sido fornecidas para resolver dependências
// entre as interfaces a seguir.
TSimpleTextHelper = class;
TSimpleTextStub = class;
// Estas interfaces auxiliadoras e de stub foram geradas da IDL da qual
// esta unidade se originou.
TSimpleTextHelper = class
class procedure Insert(const A: CORBA.Any;
const Value: CorbaServer_i.SimpleText);
class function Extract(const A: CORBA.Any): CorbaServer_i.SimpleText;
class function TypeCode: CORBA.TypeCode;
class function RepositoryId: string;
class function Read(const Input: CORBA.InputStream):
CorbaServer_i.SimpleText;
class procedure Write(const Output: CORBA.OutputStream;
const Value: CorbaServer_i.SimpleText);
class function Narrow(const Obj: CORBA.CORBAObject; IsA: Boolean = False):
CorbaServer_i.SimpleText;
class function Bind(const InstanceName: string = ‘’;
HostName : string = ‘’): CorbaServer_i.SimpleText; overload;
class function Bind(Options: BindOptions;
const InstanceName: string = ‘’; HostName: string = ‘’):
CorbaServer_i.SimpleText; overload;
end;
end;
implementation
// Essas implentações auxiliadoras e de stub foram geradas a partir da
// IDL da qual a unidade se originou.
906
Listagem 27.11 Continuação
initialization
CORBA.InterfaceIDManager.RegisterInterface(CorbaServer_i.SimpleText,
CorbaServer_c.TSimpleTextHelper.RepositoryId);
Você pode notar que o código de marshaling contido no método setText do código gerado difere
um pouco do código que escrevemos para ordenar essa mesma interface à mão. Isso porque a ferramenta
Idl2Pas usa uma DLL diferente para fornecer acesso ORB/Pascal (OrbPas33.dll) e fornece duas novas uni-
dades Pascal que suplementam a estrutura CORBA do Delphi (Corba.pas, OrbPas30.pas). Esses novos acrés-
cimos coexistirão pacificamente e não substituirão as unidades e bibliotecas atualmente encontradas no
Delphi 5.
O lançamento do compilador Inprise Idl2Pas o ajudará a simplificar algumas das tarefas mais difí-
ceis da CORBA, como chamar servidores escritos em outras linguagens, ordenar tipos de dados não sim-
ples e manipular exceções de usuário personalizadas.
Resumo
Neste capítulo, examinamos os fundamentos do desenvolvimento CORBA com o Delphi 5. Criamos cli-
entes e servidores CORBA, além de termos feito experiências com vinculação inicial e tardia. Também
analisamos o que é necessário para fazer uma vinculação inicial com um servidor CORBA escrito em ou-
tra linguagem. Finalmente, demos uma rápida passada pelo compilador Idl2Pas da Inprise e mostramos
como o lançamento dessa ferramenta ajudará a simplificar o desenvolvimento CORBA com o Delphi.
909
Desenvolvimento de PARTE
banco de dados
IV
NE STA PART E
28 Escrita de aplicações de banco de dados
de desktop 913
29 Desenvolvimento de aplicações
cliente/servidor 967
banco de dados de
desktop
28
NE STE C AP ÍT UL O
l Trabalho com datasets 914
l Uso de TTable 937
l Módulos de dados 943
l O exemplo Search, Range e Filter 943
l TQuery e TStoredProc: os outros datasets 953
l Tabelas de arquivos de texto 1953
l Conexão com ODBC 957
l ActiveX Data Objects (ADO) 961
l Resumo 966
Neste capítulo, você aprenderá a arte e a ciência do acesso a arquivos de banco de dados externos a partir
das suas aplicações em Delphi. Se você é novo em programação de banco de dados, consideramos um pe-
queno conhecimento de banco de dados, mas este capítulo fará com que você dê início à criação de apli-
cações de banco de dados de alta qualidade. Se as aplicações de banco de dados são coisas “corriqueiras”
para você, então você será beneficiado com a demonstração deste capítulo sobre a agilidade do Delphi
com programação de banco de dados. Neste capítulo, primeiro você aprenderá sobre datasets e as técni-
cas para manipulá-los, e depois aprenderá a trabalhar com tabelas e consultas especificamente. Enquanto
isso, o capítulo esboça os pontos importantes que você precisa saber para se tornar um programador pro-
dutivo com bancos de dados no Delphi.
O Delphi 5 vem com a versão 5.0 do mecanismo de banco de dados da Borland (BDE – Borland Da-
tabase Engine), que oferece a capacidade de se comunicar com Paradox, dBASE, Access, FoxPro, ODBC,
texto ASCII e servidores de banco de dados SQL, tudo praticamente da mesma maneira. Ao contrário
das versões anteriores, a edição Standard do Delphi 5 não contém conectividade de banco de dados. A
edição Professional oferece conexões com formatos Paradox, dBASE, Access, FoxPro e texto ASCII ba-
seados em arquivo, além da conectividade com as origens de dados Local InterBase e ODBC. O Delphi
Enterprise baseia-se no Delphi Professional, acrescentando conexões de links SQL do BDE com alto de-
sempenho para InterBase, Microsoft SQL Server, Oracle, Informix Dynamic Server, Sybase Adaptive
Server e DB2. Além do mais, o Delphi Enterprise também oferece componentes ADOExpress para o
acesso nativo às origens de dados do Microsoft ActiveX Data Objects (ADO). Os tópicos discutidos refe-
rem-se principalmente ao uso do Delphi com dados baseados em arquivo, como tabelas do Paradox e
dBASE, embora o capítulo também focalize o acesso aos dados por meio de ODBC e ADO. Este capítulo
também serve como uma introdução para o próximo capítulo.
Conforme seus nomes indicam, TTable é um componente que representa a estrutura e os dados con-
tidos dentro de uma tabela do banco de dados, TQuery é um componente representando o dataset retorna-
do por uma operação de consulta SQL e TStoredProc encapsula um procedimento armazenado em um ser-
vidor SQL. Neste capítulo, por questão de simplicidade, usamos o componente TTable ao discutir sobre
datasets. Mais adiante, o componente TQuery será explicado com detalhes.
915
Abrindo um dataset
Antes que você possa fazer qualquer manipulação inteligente com o seu dataset, é preciso primeiro
abri-lo. Para abrir um dataset, basta chamar seu método Open( ), como vemos neste exemplo:
Table1.Open;
Há um pouco menos de trabalho extra nesse segundo método, pois o método Open( ) acaba definin-
do a propriedade Active como True. No entanto, o trabalho extra é tão pequeno nem precisa ser levado
em consideração.
Quando o dataset tiver sido aberto, você estará livre para manipulá-lo, como veremos em breve.
Quando você acabar de usar o dataset, deverá fechá-lo chamando seu método Close( ), da seguinte for-
ma:
Table1.Close;
Como alternativa, você poderia fechá-lo definindo sua propriedade Active como False, desta forma:
Table1.Active := False;
DICA
Quando você estiver se comunicando com servidores SQL, uma conexão com o banco de dados precisa
ter sido estabelecida quando você abrir um dataset nesse banco de dados. Quando você fechar o último
dataset em um banco de dados, sua conexão será terminada. Abrir e fechar essas conexões envolve um
certo trabalho extra. Portanto, se você descobrir que está abrindo e fechando a conexão com o banco de
dados com freqüência, use um componente TDatabase em vez disso, a fim de manter uma conexão com o
banco de dados de um servidor SQL durante muitas operações de abertura e fechamento. O componente
TDatabase é explicado com mais detalhes no próximo capítulo.
Evite usar um loop repeat..until para realizar ações sobre um dataset. O código a seguir pode pare-
cer correto na superfície, mas coisas ruins poderão acontecer se você tentar usá-lo em um dataset vazio,
pois o procedimento DoSomeStuff( ) sempre será executado pelo menos uma vez, não importa se o dataset
contém registros ou não:
repeat
DoSomeStuff;
Table1.Next;
until Table1.EOF;
Visto que o loop while-not-EOF realiza a verificação logo no início, você não encontrará tal problema
com essa construção.
Marcadores
Marcadores (ou bookmarks) permitem que você salve o seu local em um dataset para que possa retornar
ao mesmo ponto posteriormente. Os marcadores são muito fáceis de se usar no Delphi, pois você só pre-
cisa se lembrar de uma propriedade.
O Delphi representa um marcador como o tipo TBookmarkStr. TTable possui uma propriedade desse
tipo chamada Bookmark. Quando você lê essa propriedade, obtém um marcador, e quando escreve nessa
propriedade, vai para um marcador. Quando você encontrar um local particularmente interessante no
dataset, e que pretenda retornar a ele com facilidade, veja a sintaxe a ser utilizada:
var
BM: TBookmarkStr;
begin
BM := Table1.Bookmark;
Quando quiser retornar ao local marcado no dataset, basta fazer o inverso – definir a propriedade
Bookmark para o valor que você obteve anteriormente lendo a propriedade Bookmark.
Table1.Bookmark := BM;
Observe que TBookmarkStr é um AnsiString por conveniência de armazenamento. Você precisa consi-
derá-lo como um tipo de dados opaco e não depender da implementação, pois os dados do marcador são
completamente determinados pelo BDE e pelas camadas de dados básicas.
NOTA
Embora o Delphi de 32 bits ainda aceite GetBookmark( ), GotoBookmark( ) e FreeBookmark( ) do Delphi
1.0, como a técnica do Delphi de 32 bits é um pouco mais limpa e menos passível de erros, você deverá
usar essa técnica mais nova, a menos que tenha que manter a compatibilidade com os projetos de 16 bits.
917
Exemplo de navegação
Agora você criará um pequeno projeto que incorpora os métodos e propriedades de navegação de TData-
Setque você acabou de aprender. Esse projeto será chamado Navig8, e o formulário principal para esse
projeto aparece na Figura 28.2.
Para exibir os dados contidos em um objeto TTable, esse projeto utilizará o componente TDBGrid. O
processo de “ligar” um controle ciente dos dados, como o componente TDBGrid, a um dataset requer vá-
rias etapas. A lista a seguir aborda as etapas necessárias para exibir os dados de Table1 em DBGrid1.
1. Defina a propriedade DatabaseName de Table1 como um nome alias ou diretório existente. Use o alias
DBDEMOS se você tiver instalado os programas de exemplo do Delphi.
2. Escolha uma tabela a partir da lista apresentada na propriedade TableName de Table1.
3. Coloque um componente TDataSource no formulário e ligue-o a TTable definindo a propriedade de
dataset de DataSource1 a Table1. TDataSource serve como um canal entre as origens de dados e os con-
troles; ele é explicado com mais detalhes anteriormente neste capítulo.
4. Ligue o componente TDBGrid ao componente TDataSource definindo a propriedade DataSource de
DBGrid1 a DataSource1.
5. Abra a tabela definindo a propriedade Active de Table1 em True.
6. Ufa! Você agora possui dados no controle de grade.
DICA
Um atalho para escolher componentes da lista drop-down fornecida para as propriedades DataSet e Data-
Source é dar um clique duplo na área à direita do nome da propriedade no Object Inspector. Isso define o
valor da propriedade para o primeiro item da lista drop-down.
O código-fonte para a unidade principal de Navig8, chamado Nav.pas, aparece na Listagem 28.1.
unit Nav;
interface
uses
SysUtils, Windows, Messages, Classes, Controls, Forms, StdCtrls,
Grids, DBGrids, DB, DBTables, ExtCtrls;
918
Listagem 28.1 Continuação
type
TForm1 = class(TForm)
Table1: TTable;
DataSource1: TDataSource;
DBGrid1: TDBGrid;
GroupBox1: TGroupBox;
GetButton: TButton;
GotoButton: TButton;
ClearButton: TButton;
GroupBox2: TGroupBox;
FirstButton: TButton;
LastButton: TButton;
NextButton: TButton;
PriorButton: TButton;
MoveByButton: TButton;
Edit1: TEdit;
Panel1: TPanel;
PosLbl: TLabel;
Label1: TLabel;
procedure FirstButtonClick(Sender: TObject);
procedure LastButtonClick(Sender: TObject);
procedure NextButtonClick(Sender: TObject);
procedure PriorButtonClick(Sender: TObject);
procedure MoveByButtonClick(Sender: TObject);
procedure DataSource1DataChange(Sender: TObject; Field: TField);
procedure GetButtonClick(Sender: TObject);
procedure GotoButtonClick(Sender: TObject);
procedure ClearButtonClick(Sender: TObject);
private
BM: TBookmarkStr;
public
{ Declarações públicas }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
end.
Esse exemplo ilustra muito bem o fato de que você pode usar as classes de banco de dados do
Delphi para realizar muita manipulação de banco de dados nos seus programas com muito pouco có-
digo.
Observe que você também deve definir inicialmente as propriedades Enabled de GotoButton e FreeBut-
ton como False, pois não pode usá-los até que um marcador seja alocado. Os métodos FreeButtonClick( ) e
GetButtonClick( ) garantem que os botões apropriados estejam ativados, dependendo se um marcador foi
definido ou não.
920
A maioria dos outros procedimentos desse exemplo utiliza apenas uma linha, embora haja um mé-
todo que mereça mais alguma explicação: TForm1.DataSource1DataChange( ). Esse método está acoplado ao
evento OnDataChange de DataSource1, que é disparado toda vez que o valor de um campo muda (por exem-
plo, quando você passa de um registro para outro). Esse evento verifica se você está no início, no meio ou
no final de um dataset; depois ele muda o rótulo do label de acordo. Você aprenderá mais sobre os even-
tos TTable e TDataSource um pouco mais adiante neste capítulo.
BOF e EOF
Você poderá notar que, quando roda o projeto Navig8, o rótulo de PosLbl indica que você está no iní-
cio do dataset, o que faz sentido. No entanto, se você passar para o próximo registro e voltar nova-
mente, o rótulo de PosLbl não saberá que você está no primeiro registro. No entanto, observe que
PosLbl.Caption indica BOF se você der um clique no botão Prior mais uma vez. Observe que o mesmo
acontece para EOF se você experimentar isso no final do dataset. Por quê?
A explicação é que o BDE não pode mais garantir que você está no início ou no final do dataset,
pois outro usuário da tabela (se for uma tabela em rede) ou até mesmo outro processo dentro do seu
programa poderia ter incluído um registro no início ou no final da tabela no momento em que você
moveu do primeiro para o segundo registro e depois voltou.
Com isso em mente, BOF só pode ser True sob uma das seguintes circunstâncias:
l Você acabou de abrir o dataset.
l Você acabou de chamar o método First( ) do dataset.
l Uma chamada para TDataSet.Prior( ) falhou, indicando que não há registros anteriores.
Da mesma forma, EOF só pode ser True sob uma destas circunstâncias:
l Você abriu um dataset vazio.
l Você acabou de chamar o método Last( ) do dataset.
l Uma chamada para TDataSet.Next( ) falhou, indicando que não há mais registros.
Uma informação sutil, porém importante, que você pode obter a partir dessa lista é que você
sabe quando um dataset está vazio quando tanto BOF quanto EOF são True.
TDataSource
Um componente TDataSource foi usado nesse último exemplo, e por isso vamos divagar por um momento
para discutir esse objeto tão importante. TDataSource é o canal que permite aos componentes de acesso a
dados, como os componentes TTable conectarem-se a controles de dados como TDBEdit e TDBLookupCombo.
Além de ser a interface entre datasets e os controles cientes dos dados, TDataSource contém algumas pro-
priedades e eventos práticos que facilitam sua vida na manipulação de dados.
A propriedade State de TDataSource revela o estado atual do dataset básico. O valor de State informa
se o dataset está atualmente inativo ou no modo Insert, Edit, SetKey ou CalcFields, por exemplo. A pro-
priedade State de TDataSet é explicada com detalhes mais adiante neste capítulo. O evento OnStateChange é
disparado sempre que o valor dessa propriedade mudar.
O evento OnDataChange de TDataset é executado sempre que o dataset se torna ativo ou um controle ci-
ente dos dados informa ao dataset que algo foi alterado.
O evento OnUpdateData ocorre sempre que um registro é postado ou atualizado. Esse é o evento que
faz com que controles cientes dos dados mudem seu valor com base no conteúdo da tabela. Você pode
responder ao evento por si mesmo para acompanhar tais alterações dentro da sua aplicação.
Valores de campo
É muito fácil acessar os valores de campo a partir do Delphi. TDataSet oferece uma propriedade de array padrão,
chamada FieldValues[ ], que retorna o valor de um determinado campo como uma Variant. Como FieldVa-
lues[ ] é a propriedade padrão do array, você não precisa especificar o nome da propriedade para acessar o ar-
ray. Por exemplo, o fragmento de código a seguir atribui o valor do campo CustName de Table1 à string S:
S := Table1[‘CustName’];
Você também poderia facilmente armazenar o valor de um campo inteiro chamado CustNo em uma
variável inteira chamada I:
I := Table1[‘CustNo’];
Os programadores do Delphi 1 notarão que a técnica de FieldValues[ ] é muito mais fácil do que a
técnica anterior para acesso aos valores de campo. Essa técnica (que ainda funciona no Delphi de 32 bits
por questão de compatibilidade) envolve o uso da propriedade de array Fields[ ] de TDataset ou da fun-
ção FieldsByName( ) para acessar objetos TField individuais associados ao dataset. O componente TField
oferece informações sobre um campo específico.
Fields[ ] é um array baseado em zero com objetos TField, de modo que Fields[0] retorna um TField
representando o primeiro campo lógico do registro. FieldsByName( ) aceita um parâmetro de string que
corresponde a um determinado nome de campo na tabela; portanto, FieldsByName(‘OrderNo’) retornaria
um componente TField representando o campo OrderNo no registro atual do dataset.
Dado um objeto TField, você pode apanhar ou atribuir o valor do campo usando uma das proprie-
dades de TField mostradas na Tabela 28.1.
AsBoolean Boolean
AsFloat Double
AsInteger Longint
AsString String
AsDateTime TDateTime
Value Variant
922
Se o primeiro campo do dataset atual for um string, você poderá armazenar seu valor na variável de
String S,da seguinte forma:
S := Table1.Fields[0].AsString;
O código a seguir define a variável inteira I para conter o valor do campo ‘OrderNo’ no registro atual
da tabela:
I := Table1.FieldsByName(‘OrderNo’).AsInteger;
Existem descendentes de TField projetados para trabalhar especificamente com muitos dos tipos de
dados anteriores. Estes são explicados um pouco mais adiante neste capítulo.
De modo semelhante, você pode obter o número de um campo do qual conhece apenas pelo nome
usando a propriedade FieldNo. O código a seguir armazena o número do campo OrderNo na variável Integer I:
var
I: integer;
begin
I := Table1.FieldsByName(‘OrderNo’).FieldNo;
end;
NOTA
Para determinar quantos campos um dataset contém, use a propriedade FieldList de TDataset. FieldList
representa uma visão achatada de todos os campos aninhados em uma tabela contendo campos que são
tipos de dados abstratos (ADTs).
Por compatibilidade, a propriedade FieldCount ainda funciona, mas pulará quaisquer campos de
ADT.
923
Manipulando os dados do campo
Aqui está um processo em três etapas para editar um ou mais campos no registro atual:
1. Chame o método Edit( ) do dataset para colocar o dataset no modo Edit.
2. Atribua novos valores aos campos à sua escolha.
3. Poste as mudanças no dataset chamando o método Post( ) ou passando para um novo registro, o
que automaticamente postará a edição.
Por exemplo, uma edição típica de registro pode se parecer com isto:
Table1.Edit;
Table1[‘Age’] := 23;
Table1.Post;
DICA
Às vezes, você trabalha com datasets que contêm dados apenas de leitura. Alguns exemplos disso incluem
uma tabela localizada em uma unidade de CD-ROM ou uma consulta com um conjunto de resultados não
ao vivo. Antes de tentar editar os dados, você pode determinar se o dataset contém dados apenas de leitura
antes de tentar modificá-los verificando o valor da propriedade CanModify. Se CanModify for True, você terá
luz verde para editar o dataset.
Com as mesmas orientações da edição de dados, você pode inserir ou acrescentar registros em um
dataset de um modo semelhante:
1. Chame o método Insert( ) ou Append( ) do dataset para colocar o dataset no modo Insert ou
Append.
2. Atribua valores aos campos do dataset.
3. Poste o novo registro no dataset chamando Post( ) ou passando para um novo registro, o que força-
rá uma postagem.
NOTA
Quando você estiver no modo Edit, Insert ou Append, lembre-se de que suas alterações sempre serão pos-
tadas quando você sair do registro atual. Portanto, cuidado ao usar os métodos Next( ), Prior( ),
First( ), Last( ) e MoveBy( ) enquanto edita os registros.
Se, em algum ponto, antes que seus acréscimos ou modificações no dataset sejam postados, você
quiser abandonar as alterações, poderá fazer isso chamando o método Cancel( ). Por exemplo, o código a
seguir cancela a edição antes que as alterações sejam postadas na tabela:
Table1.Edit;
Table1[‘Age’] := 23;
Table1.Cancel;
Cancel( ) desfaz as alterações no dataset, retira o dataset do modo Edit, Append ou Insert e o coloca
de volta ao modo Browse.
Para fechar o conjunto de métodos de manipulação de registro de TDataSet, o método Delete( ) re-
move o registro atual do dataset. Por exemplo, o código a seguir remove o último registro da tabela:
Table1.Last;
Table1.Delete;
924
O Fields Editor
O Delphi oferece um ótimo grau de controle e flexibilidade ao trabalhar com campos do dataset, por meio
do Fields Editor (editor de campos). Você pode exibir o Fields Editor para um dataset em particular no
Form Designer, seja dando um clique na TTable, TQuery ou TStoredProc, ou selecionando Fields Editor a partir
do menu local do dataset. A janela Fields Editor permite determinar com quais dos campos de um dataset
você deseja trabalhar e criar novos campos calculados e de pesquisa. Você pode usar um menu local para
realizar essas tarefas. A janela Fields Editor, incluindo seu menu local, aparece na Figura 28.3.
Para demonstrar o uso do Fields Editor, abra um novo projeto e coloque um componente TTable no
formulário principal. Defina a propriedade DatabaseName de Table1 para DBDEMOS (esse é o alias que aponta
para as tabelas de exemplo do Delphi) e defina a propriedade TableName para ORDERS.DB. Para fornecer al-
gum retorno visual, inclua também um componente TDataSource e TDBGrid no formulário. Conecte Data-
Source1 a Table1 e depois conecte DBGrid1 a DataSource1. Agora defina a propriedade Active de Table1 como
True, e os dados de Table1 aparecerão na grade.
Incluindo campos
Chame o Fields Editor dando um clique duplo em Table1 e você verá a janela Fields Editor, conforme
mostra a Figura 28.3. Digamos que você queira limitar sua exibição da tabela a apenas alguns campos.
Selecione Add Fields (adicionar campos) no menu local do Fields Editor. Isso chamará a caixa de diálogo
Add Fields. Destaque os campos OrderNo, CustNo e ItemsTotal nessa caixa de diálogo e dê um clique em OK.
Os três campos selecionados agora estarão visíveis no Fields Editor e na grade.
O Delphi cria objetos descendentes de TField, que são mapeados nos campos do dataset que você
selecionou no Fields Editor. Por exemplo, para os três campos mencionados no parágrafo anterior, o
Delphi inclui as seguintes declarações de descendentes de TField no código-fonte para o seu formulário:
Table1OrderNo: TFloatField;
Table1CustNo: TFloatField;
Table1ItemsTotal: TCurrencyField;
Observe que o nome do objeto de campo é a concatenação do nome TTable com o nome do campo.
Como esses campos são criados no código, você também pode acessar propriedades e métodos descen-
dentes de TField no seu código, e não apenas durante o projeto.
Descendentes de Tfield
Vamos divagar por um momento sobre o tópico de TFields. Existem um ou mais objetos descendentes de
TField diferentes para cada tipo de campo (tipos de campo são descritos na seção “Tipos de dados de 925
campo”, anteriormente neste capítulo). Muitos desses tipos de campo também são mapeados para tipos
de dados do Object Pascal. A Tabela 28.2 mostra as várias classes na hierarquia TField, suas classes ances-
trais, seus tipos de campo e os tipos do Object Pascal aos quais elas correspondem.
926
Como mostra a Tabela 28.2, tipos de campo BLOB e Object são especiais porque não são mapeados
diretamente em tipos nativos do Object Pascal. Os campos BLOB são discutidos com detalhes mais adi-
ante neste capítulo.
Passe para a página Events do Object Inspector e você verá que também existem eventos associados
aos objetos de campo. Os eventos OnChange, OnGetText, OnSetText e OnValidate são todos bem-documentados
na ajuda on-line. Basta dar um clique à esquerda do evento no Object Inspector e pressionar F1. Destes,
OnChange provavelmente é o mais usado. Ele permite realizar alguma ação sempre que o conteúdo do cam-
po for alterado (movendo para outro registro ou inserindo um registro, por exemplo).
Campos calculados
Usando o Fields Editor, você também pode incluir campos calculados em um dataset. Digamos, por
exemplo, que você queira incluir um campo para descobrir o total por atacado para cada item da tabela
ORDERS, e o total por atacado é 68 por cento a menos que o total normal. Selecione New Field (campo
novo) no menu local do Fields Editor e você poderá ver a caixa de diálogo New Field, como mostra a Fi-
gura 28.5. Digite o nome do novo campo, WholesaleTotal, no controle de edição Name. O tipo desse cam-
po é Currency; portanto, informe isso no controle de edição Type. Certifique-se de que o botão de op-
ção Calculated (calculado) esteja marcado no grupo Field Type; depois pressione OK. Agora, o novo
campo aparecerá na grade, mas ele não terá dado algum.
FIGURA 28.5 Incluindo um campo calculado com a caixa de diálogo New Field. 927
Para que o novo campo seja preenchido com dados, você precisa atribuir um método ao evento
OnCalcFieldsde Table1. O código para esse evento simplesmente atribui o valor do campo WholesaleTotal
para ser 68 por cento do valor do campo SalesTotal existente. Esse método, que trata de Table1.OnCalcFi-
elds, aparece a seguir:
A Figura 28.6 mostra que o campo WholesaleTotal na grade agora contém os dados corretos.
Campos de pesquisa
Os campos de pesquisa permitem criar campos em um dataset que na realidade pesquisam seus valores
em outro dataset. Para ilustrar isso, você incluirá um campo de pesquisa no projeto atual. O campo CustNo
da tabela ORDERS não significa nada para alguém que não tenha todos os números de cliente decorados.
Você pode incluir um campo de pesquisa em Table1 que verifique a tabela CUSTOMER e, com base no número
do cliente, apanhe o nome do cliente atual.
Primeiro, você precisa incluir um segundo objeto TTable, definindo sua propriedade DatabaseName
para DBDEMOS e sua propriedade TableName para CUSTOMER. Esta é a Table2. Depois você seleciona novamente
New Field no menu local do Fields Editor para chamar a caixa de diálogo New Field. Dessa vez, você
chamará o campo CustName e o tipo de campo será um String. O tamanho do string é de 15 caracteres. Não
se esqueça de selecionar o botão Lookup no grupo de botões Field Type. O controle Dataset nessa caixa
de diálogo deverá ser definido como Table2 – o dataset que você deseja verificar. Os controles Key Fields
(campos de chave) e Lookup Keys (campos de pesquisa) deverão ser definidos como CustNo – esse é o
campo comum sobre o qual a pesquisa será realizada. Finalmente, o campo Result deverá ser definido
como Contact – esse é o campo que você deseja apresentar. A Figura 28.7 mostra a caixa de diálogo New
Field para o novo campo de pesquisa. O novo campo agora mostrará os dados corretos, como vemos no
projeto completo da Figura 28.8.
FIGURA 28.7 Incluindo um campo de pesquisa com a caixa de diálogo New Field.
928
FIGURA 28.8 Exibindo a tabela que contém um campo de pesquisa.
Todos esses tipos de campo e o tipo de dado associado a esses tipos de campo são listados na Ta-
bela 28.3. 929
Tabela 28.3 Tipos de campo de TBlobField
Você verá que a maioria do trabalho que precisa ser feito para a entrada e saída de dados de compo-
nentes TBlobField pode ser feita carregando-se ou salvando-se o BLOB em um arquivo ou usando um
TBlobStream. TBlobStream é um descendente especializado de TStream que utiliza o campo BLOB dentro da
tabela física como local de fluxo. Para demonstrar essas técnicas para interagir com componentes TBlob-
Field, você criará uma aplicação de exemplo.
NOTA
Se você rodou o programa Setup no CD-ROM que acompanha este livro, ele deverá ter configurado um
alias do BDE que aponta para o subdiretório \Data do diretório em que você instalou o software. Nesse di-
retório, você poderá encontrar as tabelas usadas nas aplicações deste livro. Vários dos exemplos do
CD-ROM esperam encontrar o alias DDGData.
WaveTitle Character 25
FileName Character 25
Wave BLOB
930
FIGURA 28.10 Formulário principal para Wavez, o exemplo de campo BLOB.
O botão Add é usado para carregar um arquivo WAV do disco e incluí-lo na tabela. O método atri-
buído ao evento OnClick do botão Add aparece a seguir:
procedure TMainForm.sbAddClick(Sender: TObject);
begin
if OpenDialog.Execute then
begin
tblSounds.Append;
tblSounds[‘FileName’] := ExtractFileName(OpenDialog.FileName);
tblSoundsWave.LoadFromFile(OpenDialog.FileName);
edTitle.SetFocus;
end;
end;
O código tenta primeiro executar OpenDialog. Se tiver sucesso, tblSounds será colocado no modo
Append, o campo FileName receberá um valor e o campo BLOB Wave será carregado a partir do arquivo es-
pecificado por OpenDialog. Observe que o método LoadFromFile de TBlobField é muito prático aqui, e o códi-
go para carregar um arquivo em um campo BLOB é bastante claro.
De modo semelhante, o botão Save salva o som WAV atual encontrado no campo Wave em um arqui-
vo externo. O código para esse botão é o seguinte:
procedure TMainForm.sbSaveClick(Sender: TObject);
begin
with SaveDialog do
begin
FileName := tblSounds[‘FileName’]; // inicializa nome do arquivo
if Execute then // executa diálogo
tblSoundsWave.SaveToFile(FileName); // salva blob em arquivo
end;
end;
Existe ainda menos código aqui. SaveDialog é inicializado com o valor do campo FileName. Se a execu-
ção de SaveDialog tiver sucesso, o método SaveToFile de tblSoundsWave será chamado para salvar o conteúdo
do campo BLOB no arquivo.
O manipulador para o botão Play realiza o trabalho de ler os dados WAV do campo BLOB e pas-
sá-los para a função da API PlaySound( ) para serem tocados. O código desse handler, mostrado a seguir, é
um pouco mais complexo do que o código que apareceu até aqui:
procedure TMainForm.sbPlayClick(Sender: TObject);
var
B: TBlobStream;
M: TMemoryStream;
begin
B := TBlobStream.Create(tblSoundsWave, bmRead); // cria fluxo blob
Screen.Cursor := crHourGlass; // mostra ampulheta
try
M := TMemoryStream.Create; // cria fluxo na memória
try
M.CopyFrom(B, B.Size); // copia do blob para o fluxo na memória
931
// Tenta tocar o som. Levanta uma exceção se algo sair errado
Win32Check(PlaySound(M.Memory, 0, SND_SYNC or SND_MEMORY));
finally
M.Free;
end;
finally
Screen.Cursor := crDefault;
B.Free; // libera
end;
end;
A primeira coisa que esse método faz é criar uma instância de TBlobStream, B, usando o campo BLOB
tblSoundsWave. O primeiro parâmetro passado a TBlobStream.Create( ) é o objeto do campo BLOB, e o se-
gundo parâmetro indica como você deseja abrir o fluxo. Normalmente, você usará bmRead para o acesso
apenas de leitura ao fluxo BLOB ou bmReadWrite para o acesso de leitura/escrita.
DICA
O dataset precisa estar no modo Edit, Insert ou Append para abrir um TBlobStream com privilégio bmReadWrite.
Uma instância de TMemoryStream, M, é criada. Nesse ponto, a forma do cursor passa para uma ampulheta
para que o usuário saiba que a operação poderá levar alguns segundos. O fluxo B é então copiado para o
fluxo M. A função usada para tocar o som WAV, PlaySound( ), exige um nome de arquivo ou um ponteiro de
memória como seu primeiro parâmetro. TBlobStream não oferece acesso por ponteiro aos dados do fluxo,
mas TMemoryStream sim, através de sua propriedade Memory. Com isso, você pode chamar PlaySound( ) com su-
cesso para tocar os dados apontados por M.Memory. Quando a função é chamada, ela libera os fluxos e restau-
ra o cursor. O código completo da unidade principal desse projeto aparece na Listagem 28.2.
unit Main;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
ExtCtrls, DBCtrls, DB, DBTables, StdCtrls, Mask, Buttons, ComCtrls;
type
TMainForm = class(TForm)
tblSounds: TTable;
dsSounds: TDataSource;
tblSoundsWaveTitle: TStringField;
tblSoundsWave: TBlobField;
edTitle: TDBEdit;
edFileName: TDBEdit;
Label1: TLabel;
Label2: TLabel;
OpenDialog: TOpenDialog;
tblSoundsFileName: TStringField;
SaveDialog: TSaveDialog;
pnlToobar: TPanel;
sbPlay: TSpeedButton;
sbAdd: TSpeedButton;
932
Listagem 28.2 Continuação
sbSave: TSpeedButton;
sbExit: TSpeedButton;
Bevel1: TBevel;
dbnNavigator: TDBNavigator;
stbStatus: TStatusBar;
procedure sbPlayClick(Sender: TObject);
procedure sbAddClick(Sender: TObject);
procedure sbSaveClick(Sender: TObject);
procedure sbExitClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
procedure OnAppHint(Sender: TObject);
end;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
uses MMSystem;
933
Listagem 28.2 Continuação
end.
Atualizando o dataset
Se existe algo com que você possa contar ao criar aplicações de banco de dados é que os dados contidos
em um dataset estão em um constante estado de fluxo. Os registros serão constantemente adicionados,
removidos e modificados no seu dataset, principalmente em um ambiente de rede. Por causa disso, você
poderá ocasionalmente precisar reler as informações do dataset a partir do disco ou da memória para
atualizar o conteúdo do seu dataset.
Você pode atualizar seu dataset usando o método Refresh( ) de TDataset. Funcionalmente, ele faz a
mesma coisa que usar Close( ) e depois Open( ) sobre o dataset, mas Refresh( ) é um pouco mais rápido. O
método Refresh( ) funciona com todas as tabelas locais; no entanto, algumas restrições se aplicam para o
uso de Refresh( ) com um banco de dados a partir de um servidor de banco de dados SQL.
Componentes TTable conectados a bancos de dados SQL precisam ter um índice exclusivo antes que
o BDE tente uma operação Refresh( ). Isso porque Refresh( ) tenta preservar o registro atual, se possível.
Isso significa que o BDE precisa usar Seek( ) para ir até o registro atual em algum ponto, o que é prático
apenas em um dataset SQL se um índice exclusivo estiver disponível. Refresh( ) não funciona para com-
ponentes TQuery conectados a bancos de dados SQL.
ATENÇÃO
Quando Refresh( ) é chamado, ele pode criar alguns efeitos colaterais inesperados para os usuários do
seu programa. Por exemplo, se o usuário 1 estiver vendo um registro em uma tabela em rede e esse registro
tiver sido excluído pelo usuário 2, uma chamada a Refresh( ) fará com que o usuário 1 veja o registro de-
saparecer sem qualquer motivo aparente. O fato de que os dados podem ser alterados debaixo do usuário
é algo que você precisa ter em mente quando chamar essa função.
934
Estados alterados
Em algum ponto, você pode ter que saber se uma tabela está no modo Edit ou no modo Append, ou ainda
se ela está ativa. Você poderá obter essas informações inspecionando a propriedade State de TDataset. A
propriedade State é do tipo TDataSetState, e ela pode ter qualquer um dos valores listados na tabela 28.4.
Valor Significado
Filtros
Os filtros permitem realizar consultas ou filtragens simples no dataset usando apenas o código em Object
Pascal. A principal vantagem do uso de filtros é que eles não exigem um índice ou qualquer outra prepa-
ração nos datasets com os quais são usados. Em muitos casos, os filtros podem ser um pouco mais lentos
do que a pesquisa baseada em índice (explicada mais adiante neste capítulo), mas eles ainda são muito
úteis em quase todo tipo de aplicação.
Filtrando um dataset
Um dos usos mais comuns do mecanismo de filtragem do Delphi é limitar a visão de um dataset a apenas
alguns registros específicos. Esse é um simples processo em duas etapas:
1. Atribua um procedimento ao evento OnFilterRecord do dataset. Dentro desse procedimento, você
deverá escrever um código que aceite registros com base nos valores de um ou mais campos.
2. Defina a propriedade Filtered do dataset como True.
935
Como exemplo, a Figura 28.11 mostra um formulário contendo TDBGrid, que apresenta uma visão
não filtrada da tabela CUSTOMER do Delphi.
Na etapa 1, você escreve um manipulador para o evento OnFilterRecord da tabela. Nesse caso, aceita-
remos apenas registros cujo campo Company comece com a letra S. O código para esse procedimento apa-
rece aqui:
procedure TForm1.Table1FilterRecord(DataSet: TDataSet;
var Accept: Boolean);
var
FieldVal: String;
begin
FieldVal := DataSet[‘Company’]; // Apanha o valor do campo Company
Accept := FieldVal[1] = ‘S’; // Aceita registro se o campo começa com ‘S’
end;
Depois de acompanhar a etapa 2 e definir a propriedade Filtered da tabela como True, você poderá
ver na Figura 28.12 que a grade apresenta apenas os registros que atendem aos critérios de filtro.
NOTA
O evento OnFilterRecord só deverá ser usado em casos em que o filtro não pode ser expresso na proprieda-
de Filter. O motivo para isso é que ele pode fornecer benefícios significativos ao desempenho. Em bancos
de dados SQL, por exemplo, o componente TTable passará o conteúdo da propriedade FILTER em uma
cláusula WHERE para o banco de dados, o que geralmente é muito mais rápido do que a consulta regis-
tro-por-registro realizada em OnFilterRecord.
FindFirst/FindNext
TDataSettambém oferece métodos chamados FindFirst( ), FindNext( ), FindPrior( ) e FindLast( ), que em-
pregam filtros para localizar registros correspondentes a um determinado critério de consulta. Todas es-
sas funções trabalham sobre datasets não-filtrados, chamando o manipulador de evento OnFilterRecord
936
desse dataset. Baseado no critério de consulta do manipulador de evento, essas funções encontrarão a
primeira, próxima, anterior ou última combinação, respectivamente. Cada uma dessas funções não acei-
ta parâmetros e retorna um valor Booleano, que indica se uma combinação foi encontrada.
Localizando um registro
Os filtros são úteis não apenas para se definir uma visão de subconjunto de um dataset em particular, mas
eles também podem ser usados para procurar registros dentro de um dataset com base no valor de um ou
mais campos. Para essa finalidade, TDataSet oferece um método chamado Locate( ). Mais uma vez, como
Locate( ) emprega filtros para realizar a consulta, ele funcionará independente de qualquer índice aplica-
do ao dataset. O método Locate( ) é definido da seguinte forma:
function Locate(const KeyFields: string; const KeyValues: Variant;
Options: TLocateOptions): Boolean;
O primeiro parâmetro, KeyFields, contém o nome do(s) campos(s) que você deseja procurar. O se-
gundo parâmetro, KeyValues, contém o(s) valor(es) de campo que você deseja localizar. O terceiro e últi-
mo parâmetro, Options, permite personalizar o tipo de consulta que você deseja realizar. Esse parâmetro é
do tipo TLocateOptions, que é um tipo de conjunto definido na unidade DB da seguinte forma:
type
TLocateOption = (loCaseInsensitive, loPartialKey);
TLocateOptions = set of TLocateOption;
Se o conjunto incluir o membro loCaseInsensitive, será realizada uma busca pelos dados sem dife-
renciação de maiúsculas e minúsculas. Se o conjunto incluir o membro loPartialKey, os valores contidos
em KeyValues combinarão mesmo que sejam uma parte do valor do campo.
Locate( ) retornará True se encontrar uma combinação. Por exemplo, para procurar a primeira ocor-
rência do valor 1356 no campo CustNo de Table1, use a seguinte sintaxe:
Table1.Locate(‘CustNo’, 1356, [ ]);
DICA
Você deverá usar Locate( ) sempre que possível para procurar registros, pois ele sempre tentará usar o mé-
todo mais rápido possível para localizar o item, trocando de índices temporariamente, se for necessário.
Isso torna o seu código independente dos índices. Além disso, se você determinar que não precisa mais de
um índice em um campo qualquer, ou se a inclusão de um índice tornar seu programa mais rápido, você
poderá fazer essa mudança nos dados sem ter que recodificar a aplicação.
Uso de TTable
Esta seção descreve as propriedades e métodos comuns do componente TTable e como usá-los. Em parti-
cular, você aprenderá a procurar registros, filtrar registros usando intervalos e criar tabelas. Esta seção
também contém uma discussão dos eventos TTable.
Procurando registros
Quando você tiver que procurar registros em uma tabela, a VCL oferecerá vários métodos para ajudá-lo.
Ao trabalhar com tabelas do dBASE e do Paradox, o Delphi considera que os campos sobre os quais você
consulta são indexados. Para tabelas SQL, o desempenho da sua consulta sofrerá se você pesquisar por
campos não indexados.
Digamos, por exemplo, que você tenha uma tabela com uma chave no campo 1, que é numérico, e
no campo 2, que é alfanumérico. Você terá duas maneiras de procurar um registro específico baseado
nesses dois critérios: usando a técnica FindKey( ) ou a técnica SetKey( )..GotoKey( ). 937
FindKey( )
O método FindKey( ) de TTable permite procurar um registro combinando com um ou mais campos de
chave em uma única chamada de função. FindKey( ) aceita um array of const (o critério de consulta) como
parâmetro e retorna True quando for bem sucedido. Por exemplo, o código a seguir faz com que o dataset
passe para o registro em que o primeiro campo do índice possui o valor 123 e o segundo campo do índice
contém a string Hello:
if not Table1.FindKey([123, ‘Hello’]) then MessageBeep(0);
SetKey( )..GotoKey( )
Ao chamar o método SetKey( ) de TTable, você coloca a tabela em um modo que prepara seus campos para
serem carregados com valores representando critérios de consulta. Quando o critério de consulta tiver
sido estabelecido, use o método GotoKey( ) para realizar uma busca top-down (de cima para baixo) pelo
registro correspondente. O exemplo anterior pode ser reescrito com SetKey( )..GotoKey( ), da seguinte
forma:
with Table1 do begin
SetKey;
Fields[0].AsInteger := 123;
Fields[1].AsString := ‘Hello’;
if not GotoKey then MessageBeep(0);
end;
Mais uma vez, FindNearest( ) aceita um array of const como parâmetro que contém os valores de
campo pelos quais você deseja procurar.
Para procurar usando a técnica mais extensa fornecida por SetKey( )..GotoNearest( ), você pode usar
este código:
with Table1 do begin
SetKey;
Fields[0].AsInteger := 123;
GotoNearest;
end;
Se a busca for bem-sucedida e a propriedade KeyExclusive da tabela estiver definida como False, o
ponteiro de registro estará no primeiro registro que tiver correspondência. Se KeyExclusive for True, o re-
gistro atual será aquele imediatamente após a correspondência.
DICA
Se você quiser procurar em campos indexados de uma tabela, use FindKey( ) e FindNearest( ) – em vez de
SetKey( )..GotoX( ) – sempre que possível, pois você digita menos código e oferece menos margem para
erro humano.
938
Qual índice?
Todos esses métodos de consulta consideram que você está procurando sob o índice primário da tabela.
Se você quiser procurar usando um índice secundário, precisa definir o parâmetro IndexName da tabela
para o índice desejado. Por exemplo, se a sua tabela tivesse um índice secundário sobre o campo Company
chamado ByCompany, o código a seguir lhe permitiria procurar a empresa “Unisco”:
with Table1 do begin
IndexName := ‘ByCompany’;
SetKey;
FieldValues[‘Company’] := ‘Unisco’;
GotoKey;
end;
NOTA
Lembre-se de que algum trabalho extra é realizado na troca de índices enquanto uma tabela está aberta.
Você deverá esperar um atraso de um segundo ou mais quando definir a propriedade IndexName para um
novo valor.
Os intervalos (ou ranges) permitem filtrar uma tabela de modo que contenha apenas registros com
valores de campo que estejam dentro de um certo escopo, definido por você. Os intervalos funcionam de
modo semelhante às consultas de chave, e assim como as consultas, existem várias maneiras de aplicar
um intervalo a uma determinada tabela – seja usando o método SetRange( ) ou com os métodos manuais
SetRangeStart( ), SetRangeEnd( ) e ApplyRange( ).
ATENÇÃO
Se você estiver trabalhando com tabelas do dBASE ou do Paradox, os intervalos só funcionarão com cam-
pos indexados. Se você estiver trabalhando com dados SQL, o desempenho sofrerá bastante se você não ti-
ver um índice sobre o campo do intervalo.
SetRange( )
Assim como FindKey( ) e FindNearest( ), SetRange( ) permite realizar uma ação bastante complexa sobre
uma tabela com uma única chamada de função. SetRange( ) aceita duas variáveis array of const como parâ-
metros: a primeira representa os valores de campo para o início do intervalo e a segunda representa os
valores de campo para o final do intervalo. Como exemplo, o código a seguir filtra apenas os registros
nos quais o valor do primeiro campo é maior ou igual a 10, porém menor ou igual a 15:
Table1.SetRange([10], [15]);
ApplyRange( )
Para usar o método ApplyRange( ) para se definir um intervalo, siga estas etapas:
1. Chame o método SetRangeStart( ) e depois modifique a propriedade de array Fields[ ] da tabela
para estabelecer o valor inicial do(s) campo(s) de chave.
2. Chame o método SetRangeEnd( ) e modifique a propriedade de array Fields[ ] mais uma vez para es-
tabelecer o valor final do(s) campo(s) de chave.
3. Chame ApplyRange( ) para estabelecer o novo filtro do intervalo.
O exemplo de intervalo anterior poderia ser reescrito usando esta técnica:
939
with Table1 do begin
SetRangeStart;
Fields[0].AsInteger := 10; // intervalo começa em 10
SetRangeEnd;
Fields[0].AsInteger := 15; // intervalo termina em 15
ApplyRange;
end;
DICA
Use SetRange( ) sempre que possível para filtrar registros – seu código será menos passível de erros se você
fizer isso.
Para remover um filtro de intervalo de uma tabela e restaurar a tabela ao estado em que se encon-
trava antes que você chamasse ApplyRange( ) ou SetRange( ), basta chamar o método CancelRange( ) de
TTable.
Table1.CancelRange;
Tabelas mestre/detalhe
Freqüentemente, ao programar bancos de dados, você encontrará situações nas quais os dados a serem
gerenciados podem ser desmembrados em várias tabelas relacionadas umas às outras. O exemplo clássico
é uma tabela de clientes com um registro por informação do cliente e uma tabela de pedidos com um re-
gistro por pedido. Como cada pedido teria de ser feito por um dos clientes, forma-se um relacionamento
natural entre as duas coleções de dados. Isso é chamado relacionamento um-para-muitos, pois um clique
pode ter muitos pedidos (a tabela de clientes sendo a mestre e a tabela de pedidos sendo a detalhe).
O Delphi facilita a criação desses tipos de relacionamentos entre tabelas. Na verdade, tudo isso é li-
dado durante o projeto por meio do Object Inspector; portanto, nem sequer é necessário que você escre-
va algum código. Comece com um projeto vazio e inclua dois de cada componente TTable, TDataSource e
TDBGrid. DBGrid1 será conectado a Table1 por meio de DataSource1, e DBGrid2 será conectado a Table2 por meio
de DataSource2. Usando o alias DBDEMOS como DatabaseName, Table1 conecta-se à tabela CUSTOMER.DB e Table2 co-
necta-se à tabela ORDERS.DB. Seu formulário deverá se parecer com o da Figura 28.13.
Agora você possui duas tabelas não-relacionadas compartilhando o mesmo formulário. Quando
você tiver chegado a esse ponto, a única coisa que restará a fazer é criar o relacionamento entre as tabelas
usando as propriedades MasterSource e MasterFields da tabela de detalhe. A propriedade MasterSource de
Table2 deverá ser definida para DataSource1. Quando você tentar editar a propriedade MasterFields, será
apresentado a um editor de propriedades chamado Field Link Designer (criador de vínculo entre cam-
pos). Este aparece na Figura 28.14.
940
FIGURA 28.14 O Field Link Designer.
Nessa caixa de diálogo, você especifica quais campos comuns relacionam as duas tabelas uma à outra.
O campo que as duas tabelas têm em comum é CustNo – um identificador numérico que representa um clien-
te. Como o campo CustNo não faz parte do índice primário da tabela ORDERS, você terá de passar para um ín-
dice secundário que inclua o campo CustNo. Você pode fazer isso usando a lista drop-down Available Inde-
xes (índices disponíveis) no Field Link Designer. Quando você tiver passado para o índice CustNo, poderá
então selecionar o campo CustNo das caixa de listagem Detail Fields e Master Fields e dar um clique no bo-
tão Add para criar um vínculo entre as tabelas. Dê um clique em OK para remover o Field Link Designer.
Agora você notará que, ao movimentar-se pelos registros em Table1, a visão de Table2 será limitada a
apenas os registros que compartilham o mesmo valor no campo CustNo de Table1. O comportamento pode
ser visto na aplicação finalizada, na Figura 28.15.
Eventos de TTable
TTable oferece eventos que ocorrem antes e depois que um registro da tabela é excluído, editado ou inse-
rido, sempre que uma modificação é postada ou cancelada, e sempre que a tabela é aberta ou fechada.
Isso é para que você tenha todo o controle da sua aplicação de banco de dados. A nomenclatura para es-
ses eventos é BeforeXXX e AfterXXX, onde XXX pode ser Delete, Edit, Insert, Open e assim por diante. Esses even-
tos são bastante auto-explicativos, e você os usará nas aplicações de banco de dados das Partes II e III.
O evento OnNewRecord de TTable é disparado toda vez que um novo registro é postado na tabela. Ele é
ideal para realizar várias tarefas de manutenção em um manipulador para esse evento. Um exemplo disso
seria manter um total acumulado dos registros incluídos em uma tabela.
O evento OnCalcFields ocorre sempre que o cursor da tabela é retirado do registro atual ou quando o
registro atual é alterado. A inclusão de um manipulador para o evento OnCalcFields permite manter atuali-
zado um campo calculado sempre que a tabela é modificada.
NOTA
Como já dissemos, TTable.CreateTable( ) funciona apenas com tabelas locais. Para tabelas SQL, você
precisa usar uma técnica que emprega TQuery (esta é apresentada no próximo capítulo).
942
Módulos de dados
Os módulos de dados permitem manter todas as suas regras e relacionamentos do banco de dados em um
local central, para serem compartilhados entre projetos, grupos ou empresas. Os módulos de dados são
encapsulados pelo componente TDataModule da VCL. Pense em TDataModule como um formulário invisível
no qual você pode incluir componentes de acesso a dados que serão usados por um projeto inteiro. A
criação de uma instância de TDataModule é simples: selecione File, New no menu principal e depois selecio-
ne Data Module a partir do Object Repository.
A justificativa simples para o uso de TDataModule em vez de simplesmente incluir componentes de
acesso a dados em um formulário é que, dessa forma, é mais fácil compartilhar os mesmos dados entre
vários formulários e unidades no seu projeto. Em uma situação mais complexa, você teria uma organiza-
ção de vários componentes TTable, TQuery e/ou TStoredProc. Você poderia ter relacionamentos definidos
entre os componentes e talvez regras impostas no nível de campo, como valores mínimo/máximo ou for-
matos de exibição. Talvez essa variedade de componentes de acesso a dados modele as regras comerciais
da sua empresa. Depois de realizar tanto trabalho para montar algo tão impressionante, você não gosta-
ria de fazer tudo novamente para outra aplicação, gostaria? Certamente que não. Nesses casos, você de-
sejaria salvar seu módulo de dados no Object Repository para uso posterior. Se você trabalha em um am-
biente de equipe, pode ainda querer manter o Object Repository em uma unidade compartilhada da
rede, para que seja usado por todos os programadores na sua equipe.
No exemplo a seguir, você criará uma instância simples de um módulo de dados, de modo que
muitos formulários tenham acesso aos mesmos dados. Nas aplicações de banco de dados mostradas
em vários dos próximos capítulos, você montará relacionamentos mais complexos em módulos de
dados.
O módulo de dados
Embora estejamos começando um pouco fora de ordem, o módulo de dados para esse projeto será expli-
cado em primeiro lugar. Esse módulo de dados, chamado DM, contém apenas uma TTable e um componen-
te TDataSource. A TTable, chamada Table1, está acoplada à tabela CUSTOMERS.DB no alias DBDEMOS. O componen-
te TDataSource, DataSource1, está ligado a Table1. Todos os controles cientes dos dados nesse projeto usarão
DataSource1 como seu DataSource. DM está contido em uma unidade chamada DataMod, e aparece na Figura
28.16 em seu estado durante o projeto.
O formulário principal
O formulário principal para o SRF, apropriadamente chamado MainForm, aparece na Figura 28.17. Esse
formulário está contido em uma unidade chamada Main. Como você pode ver, ele contém um controle
TDBGrid, DBGrid1, para navegar por uma tabela, e contém um botão de opção que permite alternar entre di-
ferentes índices na tabela. DBGrid1, conforme explicado anteriormente, está acoplado a DM.DataSource1
como sua origem de dados.
943
F I G U R A 2 8 . 1 6 DM, o módulo de dados.
NOTA
Para que DBGrid1 possa se conectar a DM.DataSource1 durante o rpojeto, a unidade DataMod precisa estar na
cláusula uses da unidade Main. O modo mais fácil de fazer isso é trazer a unidade Main no Code Editor e se-
lecionar File, Use Unit (usar unidade) no menu principal. Você verá então uma lista de unidades no seu pro-
jeto, da qual poderá selecionar DataMod. Você precisa fazer isso para cada uma das unidades das quais de-
seja acessar os dados contidos dentro de DM.
O grupo de botões de opção, chamado RGKeyField, é usado para determinar qual dos dois índices da
tabela está ativo atualmente. O código conectado ao evento OnClick para RGKeyField aparece a seguir:
procedure TMainForm.RGKeyFieldClick(Sender: TObject);
begin
case RGKeyField.ItemIndex of
0: DM.Table1.IndexName := ‘’; // índice primário
1: DM.Table1.IndexName := ‘ByCompany’; // secundário, por empresa
end;
end;
MainForm também contém um componente TMainMenu, MainMenu1, que permite abrir e fechar cada
um dos outros formulários. Os itens desse menu são Key Search, Range, Filter e Exit. A unidade Main, em
sua totalidade, aparece na Listagem 28.3.
944
Listagem 28.3 O código-fonte para MAIN.PAS
unit Main;
interface
uses
SysUtils, Windows, Messages, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls, ExtCtrls, Grids, DBGrids, DB, DBTables,
Buttons, Mask, DBCtrls, Menus;
type
TMainForm = class(TForm)
DBGrid1: TDBGrid;
RGKeyField: TRadioGroup;
MainMenu1: TMainMenu;
Forms1: TMenuItem;
KeySearch1: TMenuItem;
Range1: TMenuItem;
Filter1: TMenuItem;
N1: TMenuItem;
Exit1: TMenuItem;
procedure RGKeyFieldClick(Sender: TObject);
procedure KeySearch1Click(Sender: TObject);
procedure Range1Click(Sender: TObject);
procedure Filter1Click(Sender: TObject);
procedure Exit1Click(Sender: TObject);
private
{ Declarações privadas }
public
{ Declarações públicas }
end;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
end.
O formulário Range
RangeForm aparece na Figura 28.18. RangeForm está localizado em uma unidade chamada Rng. Esse formulá-
rio permite definir um intervalo para os dados apresentados em MainForm, limitando a exibição da tabela.
Dependendo do índice ativo, os itens que você especifica nos controles de edição Range Start (início do
intervalo) e Range End (final do intervalo) podem ser numéricos (o índice primário) ou texto (o índice
secundário). A Listagem 28.4 mostra o código-fonte para RNG.PAS.
unit Rng;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, ExtCtrls;
type
TRangeForm = class(TForm)
Panel1: TPanel;
Label2: TLabel;
StartEdit: TEdit;
Label1: TLabel;
EndEdit: TEdit;
Label7: TLabel;
ApplyButton: TButton;
946
Listagem 28.4 Continuação
CancelButton: TButton;
procedure ApplyButtonClick(Sender: TObject);
procedure CancelButtonClick(Sender: TObject);
private
{ Declarações privadas }
procedure ToggleRangeButtons;
public
{ Declarações públicas }
end;
var
RangeForm: TRangeForm;
implementation
uses DataMod;
{$R *.DFM}
procedure TRangeForm.ToggleRangeButtons;
begin
{ Inverte a propriedade ativada dos botões de intervalo }
ApplyButton.Enabled := not ApplyButton.Enabled;
CancelButton.Enabled := not CancelButton.Enabled;
end;
end.
NOTA
Preste bastante atenção na seguinte linha de código da unidade Rng:
DM.Table1.SetRange([StartEdit.Text], [EndEdit.Text]);
Você pode achar estranho que, embora o campo de chave possa ser de um tipo Numeric ou de um tipo
Text, sempre estará passando strings para o método SetRange( ). O Delphi permite isso porque SetRan-
ge( ), FindKey( ) e FindNearest( ) realizarão a conversão de String para Integer, e vice-versa, automati-
camente.
O que isso significa para você é que não é preciso se incomodar em chamar IntToStr( ) ou
StrToInt( ) nessas situações – as providências já terão sido tomadas para você.
947
O formulário básico de consulta
KeySearchForm, contido na unidade KeySrch, oferece um meio para o usuário da aplicação procurar um va-
lor de chave em particular na tabela. O formulário permite que o usuário procure um valor de duas ma-
neiras. Primeiro, quando o botão de opção Normal estiver selecionado, o usuário pode pesquisar digi-
tando texto no controle de edição Search For (procurar) e pressionando o botão Exact (exato) ou Nea-
rest (mais próximo) para encontrar uma correspondência exata ou mais próxima na tabela. Segundo,
quando o botão de opção Incremental for selecionado, o usuário poderá realizar uma consulta incremen-
tal sobre a tabela toda vez que ele ou ela mudar o texto no controle de edição Search For. O formulário
aparece na Figura 28.19. O código da unidade KeySrch aparece na Listagem 28.5.
unit KeySrch;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, ExtCtrls;
type
TKeySearchForm = class(TForm)
Panel1: TPanel;
Label3: TLabel;
SearchEdit: TEdit;
RBNormal: TRadioButton;
Incremental: TRadioButton;
Label6: TLabel;
ExactButton: TButton;
NearestButton: TButton;
procedure ExactButtonClick(Sender: TObject);
procedure NearestButtonClick(Sender: TObject);
procedure RBNormalClick(Sender: TObject);
procedure IncrementalClick(Sender: TObject);
private
procedure NewSearch(Sender: TObject);
end;
var
KeySearchForm: TKeySearchForm;
implementation
uses DataMod;
948
Listagem 28.5 Continuação
{$R *.DFM}
end.
O código para a unidade KeySrch deverá ser bastante claro para você. Você poderá notar que, mais
uma vez, podemos seguramente passar strings de texto para os métodos FindKey( ) e FindNearest( ), sa-
bendo que eles farão a coisa certa com relação à conversão de tipo. Você também poderá apreciar o pe-
queno truque empregado para alternar entre a consulta incremental durante a execução. Isso é feito atri-
buindo-se um método ou atribuindo-se Nil ao evento OnChange do controle de edição SearchEdit. Quando
atribuído a um método manipulador, o evento OnChange será disparado sempre que o texto no controle
for modificado. Chamando FindNearest( ) dentro desse manipulador, uma consulta incremental poderá
ser realizada enquanto o usuário digita. 949
O formulário Filter
A finalidade de FilterForm, encontrado na unidade Fltr, é dupla. Primeiro, ele permite que o usuário filtre a
visão da tabela para um conjunto no qual o valor do campo State combina com o do registro atual. Segun-
do, esse formulário permite que o usuário procure um registro no qual o valor de qualquer campo da tabela
seja igual a algum valor que ele ou ela tenha especificado. Esse formulário aparece na Figura 28.20.
A funcionalidade da filtragem de registros na realidade envolve muito pouco código. Primeiro, o es-
tado da caixa de seleção intitulada Filter on this State (chamada cbFiltered) determina a definição da pro-
priedade Filtered de DM.Table1. Isso é realizado com a seguinte linha de código conectada a cbFilte-
red.OnClick:
DM.Table1.Filtered := cbFiltered.Checked;
Quando DM.Table1.Filtered é True, Table1 filtra registros usando o seguinte método OnFilterRecord,
que na realidade está localizado na unidade DataMod:
procedure TDM.Table1FilterRecord(DataSet: TDataSet;
var Accept: Boolean);
begin
{ Aceita registro como parte do filtro se o valor do campo State }
{ for o mesmo que DBEdit1.Text. }
Accept := Table1State.Value = FilterForm.DBEdit1.Text;
end;
O nome do campo é tomado de uma caixa de combinação chamada CBField. O conteúdo dessa caixa
de combinação é gerado no evento OnCreate desse formulário usando o seguinte código para repetir pelos
campos de Table1:
procedure TFilterForm.FormCreate(Sender: TObject);
var
i: integer;
begin
with DM.Table1 do begin
for i := 0 to FieldCount - 1 do
CBField.Items.Add(Fields[i].FieldName);
end;
end;
950
DICA
O código anterior só funcionará quando DM for criado antes desse formulário. Caso contrário, quaisquer
tentativas de acessar DM antes que ele seja criado provavelmente resultarão em um erro de Access Violation
(violação de acesso). Para certificar-se de que o módulo de dados DM, será criado antes de quaisquer for-
mulários filhos, ajustamos manualmente a ordem de criação dos formulário na lista Autocreate Forms da
página Forms da caixa de diálogo Project Options (encontrada sob Options, project no menu principal).
Naturalmente, o formulário principal deve ser primeiro a ser criado, mas além disso, esse pequeno tru-
que garante que o módulo de dados seja criado antes de qualquer outro formulário na aplicação.
unit Fltr;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, Buttons, Mask, DBCtrls, ExtCtrls;
type
TFilterForm = class(TForm)
Panel1: TPanel;
Label4: TLabel;
DBEdit1: TDBEdit;
cbFiltered: TCheckBox;
Label5: TLabel;
SpeedButton1: TSpeedButton;
SpeedButton2: TSpeedButton;
SpeedButton3: TSpeedButton;
SpeedButton4: TSpeedButton;
Panel2: TPanel;
EValue: TEdit;
LocateBtn: TButton;
Label1: TLabel;
Label2: TLabel;
CBField: TComboBox;
MatchGB: TGroupBox;
RBExact: TRadioButton;
RBClosest: TRadioButton;
CBCaseSens: TCheckBox;
procedure cbFilteredClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure LocateBtnClick(Sender: TObject);
procedure SpeedButton1Click(Sender: TObject);
procedure SpeedButton2Click(Sender: TObject);
procedure SpeedButton3Click(Sender: TObject);
procedure SpeedButton4Click(Sender: TObject);
end;
951
Listagem 28.6 Continuaçào
var
FilterForm: TFilterForm;
implementation
{$R *.DFM}
end.
952
TQuery e TStoredProc: os outros datasets
Embora esses componentes não sejam discutidos com detalhes antes do próximo capítulo, esta seção irá
apresentar os componentes TQuery e TStoredProc como descendentes de TDataSet e irmãos de TTable.
TQuery
O componente TQuery permite usar a SQL para obter datasets específicos de uma ou mais tabelas. O
Delphi permite usar o componente TQuery com dados de servidor orientado para arquivo (ou seja, Para-
dox e dBASE) e dados de servidor SQL. Depois de atribuir a propriedade DatabaseName de TQuery a um
alias ou diretório, você poderá entrar na propriedade SQL as linhas de código SQL que você deseja
executar contra o banco de dados indicado. Por exemplo, se Query1 estivesse ligado ao alias DBDEMOS, o
código a seguir recuperaria todos os registros da tabela BIOLIFE onde o campo Length (cm) fosse maior
do que 100:
select * from BIOLIFE where BIOLIFE.”Length (cm)” > 100
Assim como outros datasets, a consulta será executada quando sua propriedade Active estiver defi-
nida como True ou quando seu método Open( ) for chamado. Se você quiser realizar uma consulta que não
retorne um conjunto de resultados (uma consulta insert into, por exemplo), terá de usar ExecSQL( ) em
vez de Open( ) para chamar a consulta.
Outra propriedade importante de TQuery é RequestLive. A propriedade RequestLive indica se o conjun-
to de resultados retornado é editável. Defina essa propriedade como True quando quiser editar os dados
retornados por uma consulta.
NOTA
A simples definição da propriedade RequestLive não garante um conjunto de resultados vivo. Dependendo
da estrutura da sua consulta, o BDE pode não conseguir obter um conjunto de resultados vivo. Por exem-
plo, as consultas contendo uma cláusula HAVING, usando a função TO_DATE ou contendo campos de tipo de
dados abstrato (ADT) não são editáveis (veja na documentação do BDE uma lista completa das restrições).
Para determinar se uma consulta está viva, verifique o valor da propriedade CanModify depois de abrir a
consulta.
No próximo capítulo, você aprenderá mais sobre os recursos de TQuery, como consultas parametri-
zadas e otimização da SQL.
TStoredProc
O componente TStoredProc oferece um meio de executar procedimentos armazenados em um servidor
SQL. Como esse é um recurso específico do servidor – e certamente não para os iniciantes em banco de
dados –, deixaremos a explicação desse componente para o próximo capítulo.
953
O arquivo de esquema
O formato de um arquivo de esquema é semelhante ao de um arquivo INI do Windows. O nome da seção
é o mesmo que o da tabela (sem a extensão). A Tabela 28.5 mostra os itens e possíveis valores de itens
para um arquivo de esquema.
Usando as informações mostradas na Tabela 28.5, o arquivo de esquema precisa ter um item para
cada campo da tabela. Cada item estará no seguinte formato:
FieldX = Nome do campo, Tipo de campo, Tamanho, Casas decimais, Deslocamento
Tipo Significado
Veja um arquivo de esquema para uma versão VARYING de uma tabela semelhante, chamada OPTeam2:
[OPTEAM2]
FILETYPE = VARYING
CHARSET = ascii
DELIMITER = “
SEPARATOR = ,
Field1 = EmpNo,LONGINT,04,00,00
Field2 = Name,CHAR,16,00,00
Field3 = OfficeNo,CHAR,05,00,00
Field4 = PhoneExt,LONGINT,04,00,00
Field5 = Height,FLOAT,05,02,00
ATENÇÃO
O BDE é muito exigente em relação ao formato de um arquivo de esquema. Se você tiver errado um carac-
tere ou uma palavra, o BDE pode não conseguir reconhecer dado algum do arquivo. Se estiver com proble-
mas para obter seus dados, analise bem o seu arquivo de esquema.
O arquivo de dados
O arquivo de dados deverá ser um arquivo de tamanho fixo (FIXED) ou um arquivo delimitado (VARYING)
que contenha um registro por linha. Um exemplo de arquivo de dados para OPTeam pode ser mostrado da
seguinte forma:
2093 Steve Teixeira C2121 1234 6.5
3265 Xavier Pacheco C0001 3456 5.6
2610 Lino Tadros E2126 5678 5.11
2900 Lance Bullock C2221 9012 6.5
0007 Greg de Vries F3169 7890 5.10
1001 Tillman Dickson C3456 0987 5.9
2611 Rory Bannon E2127 6543 6.0
6908 Karl Santos A1098 5893 5.6
0909 Mr. T B0087 1234 5.9
Limitações
A Borland nunca desejou que os arquivos de texto fossem usados no lugar dos formatos de banco de da-
dos apropriados. Devido às limitações inerentes aos arquivos de texto, nós (os autores) aconselhamos se-
riamente que você não utilize tabelas de arquivo de texto para qualquer outra coisa além de importar da-
dos e exportar dados de formatos de banco de dados reais. Veja uma lista das limitações que você deve
ter em mente ao trabalhar com tabelas de texto:
l Os índices não são aceitos, e por isso você não pode usar qualquer método TTable que exija um
índice.
l Você não pode usar um componente TQuery com uma tabela de texto.
l A exclusão de registros não é aceita.
l A inserção de registros não é aceita. As tentativas de inserir um registro farão com que um novo
registro seja anexado ao final da tabela.
l A integridade referencial não é aceita.
l Tipos de dados BLOB não são aceitos.
l A edição não é aceita sobre tabelas VARYING.
l As tabelas de texto sempre são abertas com acesso exclusivo. Portanto, você precisa abrir suas ta-
belas de texto no código, e não durante o projeto.
ATENÇÃO
Nem todos os drivers ODBC são criados da mesma forma! Muitos drivers ODBC são “obrigados” a traba-
lhar apenas com um pacote de software em particular ou a ter sua funcionalidade limitada de alguma outra
maneira. Alguns exemplos desses tipos de drivers são aqueles que vêm com as versões anteriores dos pro-
dutos do Microsoft Office (que foram idealizados para trabalharem apenas com o MS Office). Certifique-se
de que o driver ODBC que você adquire seja certificado para desenvolvimento de aplicações, e não ape-
nas para trabalhar com algum pacote existente.
FIGURA 28.22 O Painel de Controle do Windows contendo o ícone Fontes de dados ODBC (32 bits).
5. Feche todas as aplicações que usam o BDE. Execute a ferramenta BDE Administrator que vem com
o Delphi e passe para a página Configuration no painel da esquerda. Expanda o ramo Drivers da vi-
são de árvore, dê um clique com o botão direito em ODBC e selecione New no menu local. Isso ati-
vará a caixa de diálogo New ODBC Driver (novo driver ODBC). Driver Name (nome do driver)
pode ser qualquer coisa que você desejar. Para este exemplo, usaremos ODBC_Access. ODBC Driver
Name será “Driver do Microsoft Access (*.mdb)” (o mesmo nome de driver da etapa 2). Default
Data Source Name deverá surgir automaticamente como AccessDB (o mesmo nome da etapa 3). A cai-
xa de diálogo completada aparece na Figura 28.26. Selecione OK e você retornará à janela principal
do BDE Administrator.
6. Passe para a página Database no painel esquerdo do BDE Administrator e selecione Object, New
no menu principal. Isso chamará a caixa de diálogo New Database Alias. Nessa caixa de diálogo,
selecione ODBC_Access (da etapa 5) como nome do driver de banco de dados e dê um clique em OK.
Você poderá então dar ao alias qualquer nome que desejar – usaremos Access neste caso. O alias
completado aparece na Figura 28.27. Selecione OK para fechar a caixa de diálogo e depois sele-
cione Object, Apply na janela principal do BDE Administrator. O alias agora já foi criado, e você
pode fechar a ferramenta BDE Administrator. A etapa seguinte é criar um tabela para o banco de
dados. 959
7. Você usará a aplicação Database Desktop que vem com o Delphi para criar tabelas para o seu banco
de dados do Access. Selecione File, New, Table no menu principal e você verá a caixa de diálogo
Create Table (criar tabela). Escolha ODBC_Access (o mesmo que nas etapas 5 e 6) como tipo de tabela e
surgirá a caixa de diálogo Create ODBC_Access Table.
8. Supondo que você esteja acostumado com a criação de tabelas no Database Desktop (se não estiver,
consulte a documentação do Delphi), a caixa de diálogo Create ODBC_Access Table funciona da
mesma forma que as caixa de diálogo “criar tabela” para outros tipos de banco de dados. Para fins
de demonstração, inclua um campo do tipo CHAR e um do tipo INTEGER. A Figura 28.29 mostra a caixa
de diálogo completada.
9. Dê um clique no botão Save As e você verá surgir a caixa de diálogo Save Table As. Nessa caixa de
diálogo, primeiro defina Alias como Access (pela etapa 6). Nesse ponto, você receberá uma caixa
de diálogo de login do banco de dados – basta dar um clique em OK para fechar a caixa, pois o
nome do usuário ou a senha foram especificados. Agora dê um nome para a tabela (não use uma ex-
tensão) no controle de edição File Name (nome do arquivo). Usaremos TestTable nesse caso. Dê um
clique em OK, e a tabela será armazenada no banco de dados. Agora estamos prontos para acessar
esse banco de dados com o Delphi.
NOTA
As tabelas do MS Access que compreendem um banco de dados são armazenadas em um arquivo MDB.
Embora isso seja contrário ao Paradox e ao dBASE, que armazena cada tabela como um arquivo separa-
do, é semelhante aos bancos de dados de servidor SQL.
960
10. Crie um novo projeto no Delphi. O formulário principal deverá conter cada um dos componentes
TTable, TDataSource e TDBGrid. DBGrid1 conecta-se a Table1 por meio de DataSource1. Selecione Access
(pelas etapas 6 e 9) na propriedade DatabaseName de Table1. Dê um clique na propriedade TableName de
Table1 e você será apresentado a uma caixa de diálogo de login. Basta dar um clique no botão OK
(nenhuma senha foi configurada) e você poderá escolher uma tabela disponível no banco de dados
do Access. Como TestTable é a única tabela que você criou, escolha essa tabela. Agora defina a pro-
priedade Active de Table1 como True e verá os nomes de campo aparecerem em DBGrid1. Execute a
aplicação e você poderá editar a tabela. A Figura 28.29 mostra a aplicação completa.
Componentes do ADOExpress
Seis componentes compõem o ADOExpress. Aqui, vamos categorizá-los em três grupos: conectividade,
acesso ao ADO e compatibilidade.
Componentes de conectividade
O componente TADOConnection é usado para estabelecer uma conexão com um armazenamento de dados
ADO. Você pode conectar vários componentes de dataset e comando do ADO a um único componente
TADOConnection a fim de compartilhar a conexão para fins de executar comandos, recuperar dados e operar
sobre metadados. Esse componente é semelhante ao componente TDataBase para aplicações baseadas no
BDE, e não é necessário para aplicações simples.
O componente TRDSConnection encapsula uma conexão RDS remota, expondo a funcionalidade do
objeto DataSpace do RDS. TRDSConnection é usado especificando-se o nome da máquina servidora de RDS
no parâmetro ComputerName e o ProgID do servidor RDS na propriedade ServerName.
Componentes de compatibilidade
Consideramos TADOTable, TADOQuery e TADOStoredProc como sendo componentes de compatibilidade porque
oferecem aos programadores os componentes separados de tabela, consulta e procedimento armazena-
do, com os quais já podem estar acostumados. Os programadores têm a liberdade de usar estes ou os
componentes de acesso ao ADO, descritos anteriormente, embora o uso destes componentes possa facili-
tar um pouco o transporte de aplicações baseadas no BDE para o ADO. Assim como TADODataSet e TADOCom-
mand, os componentes de compatibilidade têm a capacidade de se conectarem diretamente a um armaze-
namento de dados ou de se conectarem através de um componente TADOConnection.
Como você já deve ter imaginado, TADOTable é usado para recuperar e operar sobre um dataset produzido
por uma única tabela. TADOQuery pode ser usado para recuperar e operar sobre um dataset produzido por uma
instrução SQL ou executar instruções SQL da Data Definition Language (DDL), como CREATE TABLE. TADOStored-
Proc é usado para executar procedimentos armazenados, retornando ou não conjuntos de resultados.
Nessa caixa de diálogo, você tem a opção de escolher um arquivo de vínculo de dados ou uma string
de conexão para o valor da propriedade. Um arquivo de vínculo de dados é um arquivo no disco, nor-
malmente com a extensão UDL, em que uma string de conexão é armazenada. Supondo que você queira
montar uma nova string de conexão em vez de usar um arquivo UDL, é preciso selecionar o botão de op-
ção Connection String (string de conexão) e dar um clique no botão Build (criar). Isso chamará a janela
Data Link Properties (propriedades do vínculo de dados), mostrada na Figura 28.31.
Depois de selecionar o provedor, você pode dar um clique no botão Next ou na guia Connection
(conexão) para ser levado à página Connection, mostrada na Figura 28.32. Nessa página, você configu-
rará o driver para se conectar a um banco de dados em particular. Para este exemplo, queremos nos co-
nectar a uma tabela do dBASE; portanto, selecione a fonte de dados ODBC do dBASE a partir da lista
drop-down Use Data Source Name (usar nome da fonte de dados) na parte 1 da página. Você poderá sal-
tar a parte 2 da página, pois a tabela do dBASE não é protegida por senha. Na parte 3 da caixa de diálogo,
precisamos definir o nome de catálogo inicial para o diretório contendo as tabelas do dBASE. Para fins de
teste, definimos para o diretório contendo os dados de exemplo da Borland.
Para garantir que a conexão seja válida, dê um clique no botão Test Connection (testar conexão), e
você receberá uma confirmação de uma conexão válida ou um erro, se o diretório que você incluiu foi in-
válido.
As páginas Advanced (avançado) e All (tudo) da janela Data Link Properties permitem definir várias
propriedades na conexão, como Connect Timeout (tempo limite para conexão), Access Permissions
(permissões de acesso), Locale ID (código de local) e assim por diante. Para nossas finalidades, não preci-
samos editar essas páginas, e podemos usar os valores padrão. Um clique em OK nessa janela e depois no-
vamente na caixa de diálogo do editor de propriedade fará com que a string de conexão seja criada e co-
locada no Object Inspector, como mostra a Figura 28.33.
Quando você tiver definido a propriedade CommandText, poderá definir a propriedade Active do
TADODataSetcomo True. O componente está agora exibindo os dados ativamente. Para poder vê-los, você
pode inserir um componente TDataSource, que você conectará ao TADODataSet, e um componente TDBGrid,
que você coenctará a TDataSource, como aprendemos anteriormente neste capítulo. O resultado aparece
na Figura 28.35.
965
Distribuição do ADO
Para poder distribuir soluções baseadas em ADO no Windows 95, 98 e NT, lembre-se de que o MDAC
precisa estar instalado nos sistemas de destino. Você encontrará os arquivos para serem redistribuídos no
diretório \MDAC do CD-ROM do Delphi 5. O Windows 2000 inclui o MDAC, de modo que a redistribui-
ção do MDAC não é necessária se a sua aplicação estiver rodando em uma máquina Windows 2000.
Resumo
Depois de ler este capítulo, você deverá estar pronto para qualquer tipo de programação de banco de da-
dos não SQL com o Delphi. Você aprender os detalhes sobre o componente TDataSet do Delphi, que é o
ancestral dos componentes TTable, TQuery e TStoredProc. Também aprendeu as técnicas para manipular ob-
jetos TTable, como gerenciar campos e como trabalhar com tabelas de texto. Junto com toda essa infor-
mação prática, você também aprendeu sobre as várias estratégias de acesso a dados, incluindo BDE,
ODBC e ADO. Como vimos, a VCL oferece uma amarração orientada a objeto bem apertada em torno
do BDE de procedimentos, além de uma estrutura extensível que pode acomodar outros mecanismos,
como o ADO.
No próximo capítulo, focalizaremos um pouco mais sobre a tecnologia cliente/servidor e o uso das
classes relacionadas na VCL, como os componentes TQuery e TStoredProc.
966
Desenvolvimento de CAPÍTULO
aplicações cliente/
servidor
29
NE STE C AP ÍT UL O
l Por que utilizar cliente/servidor? 968
l Arquitetura cliente/servidor 969
l Modelos cliente/servidor 972
l Desenvolver em cliente/servidor ou em banco de
dados para desktop? 974
l SQL: seu papel no desenvolvimento
cliente/servidor 976
l Desenvolvimento em cliente/servidor no
Delphi 977
l O servidor: projeto do back-end 977
l O cliente: projeto do front-end 988
l Resumo 1008
Temos vivido uma grande euforia em torno dos conceitos de “cliente/servidor”. Atualmente, todos utili-
zamos ou desenvolvemos algum tipo de sistema cliente/servidor. Não é muito fácil compreender o que
significa, precisamente, cliente/servidor, bem como compreender as vantagens desta tecnologia em rela-
ção às demais, a não ser que você já tenha estudado bastante os fundamentos a ela relacionados.
Se você é um programador em Delphi, é natural que já esteja satisfeito com seus conhecimentos re-
lacionados ao conceito de cliente/servidor. O Delphi 5 é, antes de mais nada, um ambiente de desenvolvi-
mento cliente/servidor. Entretanto, isto não significa que tudo o que é desenvolvido no Delphi obedece à
tecnologia cliente/servidor. Também não se pode afirmar que uma aplicação seja cliente/servidor, apenas
pelo fato de ela acessar dados em um banco de dados cliente/servidor, como Oracle, Microsoft SQL ou
InterBase.
Este capítulo apresenta os elementos que compõem um sistema cliente/servidor, comparando o de-
senvolvimento cliente/servidor com o desenvolvimento tradicional, em desktops e bancos de dados de
mainframe. Também apresenta os motivos para utilizar uma solução cliente/servidor e as soluções ofere-
cidas pelo Delphi 5, no que se refere ao desenvolvimento de aplicações de três camadas de cliente/servi-
dor. Este capítulo apresenta, finalmente, algumas armadilhas com as quais os programadores de bancos
de dados de desktop devem tomar cuidado, quando migrarem para a arquitetura cliente/servidor.
Arquitetura cliente/servidor
A arquitetura cliente/servidor é aquela na qual o front-end (ou usuário final – o cliente) acessa e processa
dados em uma máquina remota (o servidor). Não existe uma definição única para cliente/servidor. Con-
sidere, por enquanto, a seguinte definição: um servidor fornece um serviço e o cliente solicita um serviço
do servidor. Pode haver muitos clientes solicitando esses serviços de um mesmo servidor. Cabe ao servi-
dor decidir como processar tais solicitações. Além disso, o sistema cliente/servidor acarreta maior com-
plexidade do que a simples junção de um cliente e um servidor. Isso será discutido com maiores detalhes
na seção que apresenta os sistemas de três camadas.
Em um ambiente cliente/servidor, o servidor manipula muito mais do que a simples distribuição
dos dados. De fato, o servidor desempenha, na maioria das vezes, as tarefas relacionadas à lógica co-
mercial, além de comandar a forma como os dados são acessados e manipulados pelo cliente. As aplica-
ções do cliente se destinam, apenas, a apresentar os dados e a obtê-los do usuário final. As subseções se-
guintes explicarão com maiores detalhes os papéis desempenhados pelo cliente e pelo servidor. Além
disso, falaremos sobre regras comerciais, que descrevem a maneira como o cliente acessa os dados no
servidor.
O cliente
O cliente tanto pode corresponder a uma aplicação GUI como não-GUI. O Delphi 5 permite desenvolver
o cliente e quaisquer servidores de aplicação da camada intermediária, no modelos de três camadas. O
servidor de banco de dados é, na maioria das vezes, desenvolvido usando um SGBDR, como o Oracle ou
o InterBase.
Aplicações escaláveis
Você já deve ter ouvido falar na expressão escalabilidade, pertencente ao jargão do desenvolvi-
mento em Delphi, em cliente/servidor. O que significa, exatamente, escalabilidade? Algumas pessoas
gostam de defini-la como a capacidade de acessar, facilmente, bancos de dados no servidor, usando
os poderosos recursos de bancos da dados do Delphi. Também pode ser definida como a capacidade
de aumentar, rapidamente, o número de usuários, bem como a demanda de utilização em um siste-
ma, sem que haja prejuízo no desempenho, ou que a perda de desempenho seja mínima. Outros ain-
da preferem defini-la como a capacidade de transformar, em um passe de mágica, uma aplicação de
desktop em uma aplicação de cliente, através da simples alteração de um alias na aplicação. Infeliz-
mente, esta última definição não é muito precisa. Certamente, você pode alterar uma propriedade
Alias para acessar, repentinamente, os dados em um banco de dados no servidor. Entretanto, esta
medida não é suficiente para transformar sua aplicação em uma aplicação de cliente, nem tampouco
para escalar sua aplicação. Uma vantagem fundamental, em cliente/servidor, é que você tem, à sua
disposição, todas as vantagens dos poderosos recursos oferecidos pelo servidor, vantagens estas que
ficariam indisponíveis, se suas aplicações fossem projetadas utilizando métodos de bancos de dados
de desktop.
969
As aplicações do cliente oferecem a interface para que os usuários possam manipular dados no ex-
tremo do servidor. Os serviços são solicitados, no servidor, através do cliente. Por exemplo, um serviço
típico consiste em adicionar um cliente, um pedido ou imprimir um relatório. Nesse tipo de serviço, o
cliente simplesmente faz a solicitação, fornecendo os dados necessários, cabendo ao servidor o processa-
mento da solicitação, o que não significa que o cliente não possa desempenhar algumas tarefas relaciona-
das à lógica da operação. Em algumas situações, é possível que a aplicação do cliente execute a maioria
da lógica comercial, ou mesmo toda ela. Nestas situações podemos chamar o cliente de cliente gordo.
O servidor
O servidor oferece os serviços para o cliente. Basicamente, o servidor aguarda uma solicitação do cliente
e, então, a processa. Um servidor deve ser capaz de processar várias solicitações para vários clientes, além
de estabelecer suas prioridades. Na maioria das vezes, o servidor poderá operar ininterruptamente, a fim
de permitir um acesso constante a seus serviços.
NOTA
Um cliente não tem de residir, obrigatoriamente, em uma máquina diferente do servidor. Geralmente, as
tarefas de segundo plano que utilizam os dados podem residir na mesma máquina do servidor.
Regras comerciais
As regras comerciais correspondem, em poucas palavras, aos procedimentos que estabelecem como os
clientes acessam os dados no servidor. Essas regras são implementadas, através de código de programa-
ção, no cliente, no servidor, ou em ambos. No Delphi 5, as regras comerciais são implementadas na for-
ma de código do Object Pascal. No lado do servidor, as regras comerciais são implementadas na forma de
procedimentos armazenados do SQL, triggers e outros objetos de bancos de dados nativos nos bancos de
dados do servidor. Em modelos de três camadas, as regras comerciais podem ser implementadas na ca-
mada intermediária. Esses objetos serão discutidos posteriormente, neste capítulo.
As regras comerciais definem o comportamento de todo o sistema. Sem as regras comerciais, não
haveria nada mais do que dados, residentes em uma máquina, e uma aplicação GUI, em outra, sem a ne-
cessidade de qualquer método destinado a conectá-las.
Em algum momento da fase de projeto, no desenvolvimento do seu sistema, deve-se decidir quais
processos devem compô-lo. Tome, como exemplo, um sistema de estoque, onde os processos típicos
correspondem a tarefas como cadastrar um pedido, imprimir uma fatura, adicionar um cliente e outros.
Conforme descrito anteriormente, essas regras são implementadas em código Object Pascal no cliente,
ou em uma camada intermediária, podendo também ser escritas em código SQL no servidor ou, final-
mente, utilizando uma combinação dos três tipos. Quando a maior parte das regras é implementada no
servidor, este recebe a denominação servidor gordo. Quando a maioria das regras se encontra no cliente,
este se denomina cliente gordo. Quando as regras estão localizadas na camada intermediária, também po-
demos nos referir a isso como servidor gordo. A quantidade, bem como o tipo dos controles necessários
para manipular os dados, determina o local em que serão colocadas as regras comerciais.
NOTA
Geralmente, o termo três camadas também pode receber as denominações n-camadas ou camadas múlti-
plas, embora esses últimos termos sejam, ambos, inadequados. Em um modelo de três camadas, geral-
mente existem um ou mais clientes, a lógica comercial e o servidor de banco de dados. A lógica comercial
pode, perfeitamente, ser distribuída em muitas partes, em várias máquinas diferentes, ou mesmo em servi-
dores de aplicações. A complexidade aumenta quando o sistema é composto de 10, 15, ou até mesmo 25
camadas. A camada da lógica comercial, ou a intermediária, deve ser vista como uma camada única, in-
dependentemente de quantas caixas e servidores de aplicações sejam necessários.
970
Cliente gordo, servidor gordo ou camada intermediária: onde entram as
regras comerciais?
A decisão sobre o local em que se situarão as regras comerciais, ou a maneira como distribuir as regras co-
merciais entre o servidor e os clientes, depende de diversos fatores, incluindo a segurança dos dados, a
integridade dos dados, os controles centralizados e uma distribuição adequada do trabalho.
Distribuição do trabalho
Seja posicionando as regras no servidor, seja separando-as em várias camadas intermediárias, o MIS
pode, com facilidade, realizar as tarefas de dividir as responsabilidades por departamentos específicos,
além de manter a integridade e a segurança dos dados no servidor, o que permite que os departamentos
compartilhem os mesmos dados, ainda que somente manipulem os dados necessários para atender os
próprios objetivos. Essa distribuição de trabalho é executada atribuindo-se a permissão de acesso apenas
para os procedimentos armazenados, bem como para os outros objetos de banco de dados necessários
para um determinado departamento.
Como exemplo, usaremos, novamente, o sistema de estoque. Suponhamos um sistema de estoque
de uma loja de peças automotivas, onde muitas pessoas precisam acessar os mesmos dados, embora com
propósitos diferentes. O encarregado do caixa precisa estar habilitado para processar faturas, adicionar e
remover clientes e alterar informações de clientes. Os funcionários do estoque devem estar capacitados a
adicionar novas peças ao banco de dados, bem como fazer o pedido de novas peças. O pessoal da conta-
bilidade deve ter acesso, também, à parte do sistema necessária ao seu trabalho. É pouco provável que o
pessoal do estoque tenha de executar um relatório mensal de estoque. Também é improvável que o pes-
soal da contabilidade tenha que alterar informações de endereços de cliente. Quando as regras comer-
ciais são criadas no servidor, é possível permitir o acesso, com base nas necessidades de uma pessoa ou de
um departamento. Neste exemplo, o pessoal encarregado da caixa tem acesso às regras do cliente e das
faturas. O pessoal do estoque tem acesso às regras comerciais necessárias às suas necessidades, enquanto
o pessoal da contabilidade tem acesso aos dados relacionados à contabilidade.
A distribuição do trabalho se refere, não apenas, à sua divisão entre vários clientes, mas também à
determinação de qual trabalho pode ser melhor executado no cliente, em vez de no servidor, ou nas ca-
madas intermediárias. Como um programador, você deve avaliar várias estratégias que permitam atri-
buir operações pesadas, em termos de utilização de CPU, para as máquinas de cliente mais rápidas, libe-
rando o servidor para que execute menos operações de uso intensivo. Deve-se considerar, durante o pro-
cesso de decisão sobre qual estratégia empregar, quais regras comerciais podem ser violadas, bem como
os riscos, com relação à segurança.
Modelos cliente/servidor
Você já ouviu falar, inúmeras vezes, de sistemas cliente/servidor que estão sujeitos a um ou dois modelos.
Estes correspondem a modelos de duas ou três camadas, conforme aparecem nas Figuras 29.1 e 29.2,
respectivamente.
972
Dados
Regras
comerciais
Dados
Regras
comerciais
Segurança de dados
Os bancos de dados baseados em SQL tratam a segurança de maneira diferente dos bancos de dados de
desktop. Embora ofereçam as mesmas medidas de segurança, através de senhas, que os outros bancos de
dados, também oferecem um mecanismo capaz de restringir o acesso de usuários a objetos de bancos de
dados específicos, como views, tabelas, procedimentos armazenados e outros. Esses objetos serão discu-
tidos, com maiores detalhes, ainda neste capítulo. Em virtude das características acima descritas, o acesso
dos usuários pode ser definido no servidor, baseado nas necessidades de visualização dos dados, pelo
usuário.
Os bancos de dados SQL permitem, Normalmente, conceder ou revogar privilégios a um usuário
ou grupo de usuários. Assim sendo, é possível definir um grupo de usuários em um banco de dados SQL.
Os privilégios podem se referir a qualquer um dos objetos de banco de dados já mencionados.
974
Métodos de bloqueio de registro
O bloqueio é um mecanismo utilizado para permitir transações SQL simultâneas, para vários usuários,
em um mesmo banco de dados. Existem diversos níveis de bloqueio, sendo que, aos servidores, podem
corresponder diferentes níveis.
O bloqueio em nível de tabela evita que sejam feitas modificações na tabela, que possam refletir em
transações futuras. Embora esse método permita um processamento paralelo, a velocidade desse proces-
samento é lenta, pois os usuários compartilham, muitas vezes, as mesmas tabelas.
O bloqueio em nível de página é uma técnica mais avançada, onde o servidor bloqueia certos blocos
de dados no disco, denominados páginas. Enquanto uma transação realiza uma operação em uma deter-
minada página, outras transações são impedidas de terem os dados atualizados, na mesma página. Nor-
malmente, os dados se estendem por centenas de páginas, de maneira que não é muito comum que várias
transações ocorram na mesma página.
Alguns servidores oferecem o bloqueio em nível de registro, que impõe um bloqueio em uma linha
específica de uma tabela de banco de dados. Entretanto, esse bloqueio resulta em um grande decréscimo
na eficiência da manutenção das informações de bloqueio.
Bancos de dados de desktop utilizam o bloqueio pessimista ou determinístico, o que significa que
você não poderá fazer alterações em registros de tabela que estejam sendo modificados, no momento,
por outro usuário. Se você tentar acessar esses registros, será emitida uma mensagem de erro, indicando
que esses registros não poderão ser acessados, até que o usuário anterior os libere.
Os bancos de dados SQL operam segundo um conceito conhecido como bloqueio otimista. Nessa
técnica, você não está proibido de acessar um registro que tenha sido acessado, previamente, por outro
usuário. Você pode fazer edições e solicitar que o servidor salve o registro. Porém, antes que um registro
seja salvo, ele é comparado com a cópia do servidor, que pode ter sido atualizada por outro usuário, no
mesmo instante em que você estava visualizando ou fazendo edições no extremo do cliente, o que resulta
em uma mensagem de erro indicando que o registro foi modificado desde que você o obteve inicialmen-
te. Como programador, você deve levar isso em conta quando projetar sua aplicação de cliente. Aplica-
ções cliente/servidor são mais sensíveis a esse tipo de ocorrência, o que não ocorre com seus equivalentes
para desktops.
Integridade de dados
Os bancos de dados SQL viabilizam a aplicação de restrições mais sólidas aos dados do servidor. Embora,
nos bancos de dados de desktop, as restrições relacionadas à integridade de dados sejam construídas no
banco de dados, você deve definir quaisquer regras comerciais no contexto do código da aplicação. Os
bancos de dados SQL, ao contrário, permitem definir essas regras no servidor final, o que é positivo, pois
não apenas admitem que todas as aplicações de cliente utilizem o mesmo conjunto de regras comerciais,
como também a manutenção dessas regras é centralizada.
As restrições de integridade são definidas quando as tabelas são criadas no servidor. Alguns exem-
plos serão apresentados posteriormente neste capítulo, na seção “Criando uma tabela”. Dentre as restri-
ções, estão incluídas as restrições de validação, as de exclusividade e as de referência.
Conforme mencionamos anteriormente, as restrições de integridade também podem ser definidas
no contexto dos procedimentos armazenados da SQL. Você pode, por exemplo, verificar se o cliente tem
o limite de crédito compatível com o pedido, antes de processá-lo. Observe como essas regras forçam a
integridade dos dados.
Orientação da transação
Bancos de dados SQL são orientados à transação, o que significa que as alterações nos dados não são fei-
tas diretamente nas tabelas, da mesma maneira que nos bancos de dados de desktop. As alterações em
aplicações de cliente devem ser feitas no servidor, e este deve implementar esse lote de operações através
de uma única transação.
975
Para que as alterações nos dados sejam concluídas, a transação deve ser submetida, como um con-
junto. Se qualquer uma das alterações dentro da transação falhar, a transação inteira será cancelada (em
outras palavras, abortada).
As transações preservam a consistência dos dados no servidor. Voltemos para o exemplo do esto-
que. Quando um pedido é feito, uma tabela ORDER deve ser atualizada, a fim de refleti-lo. Adicionalmente,
a tabela PARTS deve refletir o número reduzido de itens do pedido. Se, por algum motivo, o sistema “cair”
em um instante situado entre a atualização da tabela ORDERS e a atualização da tabela PARTS, os dados pode-
rão não refletir, com precisão, o número real de itens em estoque. Se essa operação inteira for encapsula-
da dentro de uma transação, nenhuma das tabelas afetadas dentro da transação poderá ser atualizada, até
que a transação inteira seja submetida.
As transações tanto podem ser controladas no nível do servidor quanto no nível do cliente, dentro
da sua aplicação do Delphi 5. Isso será ilustrado posteriormente no capítulo, na seção “Controle de tran-
sação”.
NOTA
Alguns bancos de dados de desktop, como o Paradox 9, trabalham com transações.
NOTA
Neste livro, propomos a cobrir a implementação SQL do InterBase, bem como os aspectos do InterBase
relacionados a esse tema. Estamos usando o InterBase para facilitar a discussão do desenvolvimento de
aplicações cliente/servidor, o que se justifica, pois a versão local do InterBase é distribuída com o Delphi 5.
Muito do que discutiremos se aplica a outras implementações de SQL, em outros bancos de dados de servi-
dor, exceto quando se relacionam a recursos específicos de um servidor.
Definindo tabelas
Com relação à estrutura e à funcionalidade, as tabelas do InterBase são bastante parecidas com as tabelas
descritas no Capítulo 28. Ou seja, elas contêm um conjunto desordenado de linhas, cada qual contendo
um certo número de colunas.
Tipos de dados
As colunas podem corresponder a qualquer um dos tipos de dados disponíveis, conforme descrito na Ta-
bela 29.1.
Os tipos de campos também podem ser definidos com domínios no InterBase. Isso será discutido
brevemente na seção “Usando domínios”.
Criando a tabela
Utilize a instrução CREATE TABLE para criar a tabela e suas colunas, bem como quaisquer restrições de inte-
gridade que desejar aplicar a cada coluna. A Listagem 29.1 mostra como pode ser criada uma tabela
InterBase.
978
Listagem 29.1 Criação de tabela no InterBase
/* Definições de domínio */
CREATE DOMAIN FIRSTNAME AS VARCHAR(15);
CREATE DOMAIN LASTNAME AS VARCHAR(20);
CREATE DOMAIN DEPTNO AS CHAR(3)
CHECK (VALUE = ‘000’ OR (VALUE > ‘0’ AND VALUE <= ‘999’)
OR VALUE IS NULL);
CREATE DOMAIN JOBCODE AS VARCHAR(5)
CHECK (VALUE > ‘99999’);
CREATE DOMAIN JOBGRADE AS SMALLINT
CHECK (VALUE BETWEEN 0 AND 6);
CREATE DOMAIN SALARY AS NUMERIC(15, 2)
DEFAULT 0 CHECK (VALUE > 0);
/* Tabela: EMPLOYEE, Proprietário: SYSDBA */
CREATE TABLE EMPLOYEE (
EMP_NO EMPNO NOT NULL,
FIRST_NAME FIRSTNAME NOT NULL,
LAST_NAME LASTNAME NOT NULL,
PHONE_EXT VARCHAR(4),
HIRE_DATE DATE DEFAULT ‘NOW’ NOT NULL,
DEPT_NO DEPTNO NOT NULL,
JOB_CODE JOBCODE NOT NULL,
JOB_GRADE JOBGRADE NOT NULL,
JOB_COUNTRY COUNTRYNAME NOT NULL,
SALARY SALARY NOT NULL,
FULL_NAME COMPUTED BY (last_name || ‘, ‘ || first_name),
PRIMARY KEY (EMP_NO));
A primeira seção da Listagem 29.1 mostra uma série de instruções CREATE DOMAIN, que serão explica-
das brevemente, enquanto a segunda seção da Listagem 29.1 cria uma tabela denominada EMPLOYEE, com
as linhas especificadas. Cada definição de linha é seguida pelo tipo de linha e, possivelmente, pela cláusu-
la NOT NULL. A cláusula NOT NULL indica que um valor é requerido para aquela linha. Você também pode ob-
servar que especificamos uma chave primária no campo EMP_NO, usando a cláusula PRIMARY KEY. A especifi-
cação de uma chave primária não apenas assegura a exclusividade do campo, como também cria um índi-
ce para aquele campo. Os índices aceleram a recuperação dos dados.
Índices
Os índices também podem ser criados explicitamente, usando a instrução CREATE INDEX. Índices são basea-
dos em uma ou mais coluna de uma tabela. Por exemplo, a seguinte instrução SQL pode criar um índice a
ser aplicado no sobrenome de um funcionário, além de no seu nome:
CREATE INDEX IDX_EMPNAME ON EMPLOYEE (LAST_NAME, FIRST_NAME);
Colunas calculadas
O campo FULL_NAME corresponde a um campo calculado. Colunas calculadas se baseiam em uma expressão
fornecida na cláusula COMPUTED BY. O exemplo na Listagem 29.1 utiliza a cláusula COMPUTED BY, com o sobre-
nome e o primeiro nome separados por uma vírgula. Você pode criar diversas variações de colunas calcu-
ladas, de acordo com suas necessidades. Você pode consultar a documentação do seu servidor, para ver
quais recursos estão disponíveis, no que se refere às colunas calculadas.
979
Chaves externas
Você também pode especificar uma restrição para uma chave externa, em certos campos. Por exemplo, o
campo DEPT_NO é definido como:
DEPT_NO DEPTNO NOT NULL
O tipo DEPT NO é definido de acordo com o seu domínio. Não se preocupe, se esse conceito parecer
um pouco avançado. Considere apenas que foi atribuída uma definição válida para o campo, como
CHAR(3). Para garantir que esse campo fará referência a outro campo em outra tabela, adicione a cláusula
FOREIGN KEY à definição da tabela, conforme mostrado a seguir, onde alguns dos campos foram excluídos:
Nesse caso, a cláusula FOREIGN KEY garante que o valor do campo DEPT_NO da tabela EMPLOYEE é equiva-
lente ao valor da coluna DEPT_NO da tabela DEPARTMENT. Chaves externas também resultam na criação de um
índice para uma coluna.
Valores default
Você pode usar a cláusula DEFAULT para especificar um valor default para um certo campo. Por exemplo,
observe a definição para HIRE_DATE, que utiliza a cláusula DEFAULT, para especificar um valor default para
esse campo:
HIRE_DATE DATE DEFAULT ‘NOW’ NOT NULL,
Aqui, o valor padrão a ser atribuído a esse campo é proveniente do resultado da função NOW, uma
função do InterBase que retorna a data atual.
Usando domínios
Observe a lista de definições de domínio que aparecem antes da instrução CREATE TABLE. Domínios corres-
pondem a definições de colunas personalizadas, e através de sua utilização, você pode definir colunas de
tabela com características complexas para serem usadas no mesmo banco de dados. A Listagem 29.1
mostra a definição de domínio para FIRSTNAME como
CREATE DOMAIN FIRSTNAME VARCHAR(15);
Qualquer outra tabela que utilize FIRSTNAME como uma das definições de campo herdará o mesmo
tipo de dados, VARCHAR(15). Posteriormente, se houver necessidade de redefinir FIRSTNAME, qualquer tabela
em que seja criado um campo com esse tipo herdará a nova definição.
Você pode adicionar restrições a definições de domínio, da mesma maneira que as adiciona a defini-
ções de coluna. Veja, por exemplo, a definição de domínio para JOBCODE, que verifica se o seu valor é mai-
or do que 99999:
CREATE DOMAIN JOBCODE AS VARCHAR(5)
CHECK (VALUE > ‘99999’);
Veja, também, que o domínio de JOBGRADE testa se o valor está compreendido entre 0 e 6:
CREATE DOMAIN JOBGRADE AS SMALLINT
CHECK (VALUE BETWEEN 0 AND 6);
980
Os exemplos aqui fornecidos são sucintos, e representam apenas os tipos de restrições de integrida-
de que podem ser atribuídos a definições de tabela. Esses tipos também variam, de acordo com o tipo de
servidor a ser utilizado. É conveniente estar familiarizado com os vários recursos oferecidos pelo seu ser-
vidor.
Definindo views
Uma view corresponde a um importante objeto de banco de dados, que permite criar um conjunto de re-
sultados personalizado. Ela consiste em agrupamentos de colunas, em uma ou mais tabelas de um banco
de dados. Nessa “tabela virtual”, podem ser realizadas operações, como em uma tabela real, o que permi-
te definir o subconjunto de dados que um usuário em particular ou um grupo de usuários pode acessar.
Dessa maneira, pode-se ampliar a restrição do acesso aos demais dados.
Para criar uma view, você pode usar a instrução CREATE VIEW. No InterBase, existem basicamente três
maneiras de se construir uma view:
l Um subconjunto horizontal de linhas em uma única tabela. A view a seguir, por exemplo, exibe
todos os campos da tabela EMPLOYEE, com a exceção da coluna SALARY, podendo ser aplicada, ape-
nas, ao pessoal da gerência:
CREATE VIEW EMPLOYEE_LIST AS
SELECT EMP_NO, FIRST_NAME, LAST_NAME, PHONE_EXT, FULL_NAME
FROM EMPLOYEE;
l Um subconjunto de linhas e colunas em uma única tabela. O exemplo a seguir mostra uma view
de funcionários, que sejam executivos, e que tenham um salário superior a U$ 100.000:
CREATE VIEW EXECUTIVE_LIST AS
SELECT EMP_NO, FIRST_NAME, LAST_NAME, PHONE_EXT, FULL_NAME
FROM EMPLOYEE WHERE SALARY >= 100,000;
l Um subconjunto de linhas e colunas em mais de uma tabela. A view a seguir mostra um subcon-
junto da tabela EMPLOYEE, acrescido de duas colunas da tabela JOB. Desde que as aplicações do cli-
ente sejam afetadas, as linhas e colunas retornadas pertencem a uma única tabela:
CREATE VIEW ENTRY_LEVEL_EMPL AS
SELECT JOB_CODE, JOB_TITLE, FIRST_NAME, LAST_NAME.
FROM JOB, EMPLOYEE
WHERE JOB.JOB_CODE = EMPLOYEE.JOB_CODE AND SALARY < 15000;
Muitas operações podem ser aplicadas a views. Algumas views são apenas de leitura, enquanto outras
podem ser atualizadas, o que depende de certos critérios específicos do servidor que você está usando.
Esse procedimento obtém uma string iCOUNTRY como parâmetro, retornando as linhas da tabela
CUSTOMER em que o país coincide com o parâmetro iCOUNTRY. O código usa uma instrução FOR SELECT..DO que
recupera várias linhas. Essa instrução funciona exatamente como uma instrução SELECT comum, que recu-
pera uma linha de cada vez, posicionando os valores da coluna especificada nas variáveis especificadas
pela instrução INTO. Para executar essa instrução a partir do Windows ISQL, você deve inserir a seguinte
instrução:
SELECT * FROM CUSTOMER_SELECT(“USA”);
982
Mais tarde, mostraremos como executar esse procedimento armazenado a partir de uma aplicação
em Delphi 5.
Esse procedimento adiciona um novo registro à tabela COUNTRY, através de uma instrução INSERT,
onde os dados são passados pelo procedimento, através de parâmetros. Esse procedimento não retorna
um conjunto de resultados, e pode ser executado a partir da utilização da instrução EXECUTE PROCEDURE no
Windows ISQL, conforme mostrado adiante:
EXECUTE PROCEDURE ADD_COUNTRY(“Mexico”, “Peso”);
UPDATE customer
SET on_hold = “*”
WHERE cust_no = :cust_no;
SUSPEND;
END
SUSPEND;
END
^
984
O procedimento exibido na Listagem 29.4 mostra outro recurso da DDL do InterBase – exceções.
As exceções, no InterBase, são bastante parecidas com as exceções no Delphi 5. Correspondem a mensa-
gens de erro, facilmente identificáveis, obtidas dentro do procedimento armazenado quando ocorre um
erro. Quando se alcança uma exceção, ela retorna a mensagem de erro para a aplicação que fez a chama-
da, terminando a execução do procedimento armazenado. É possível, porém, manipular a exceção de
dentro do procedimento armazenado, o que permite que o procedimento continue a ser processado.
As exceções são criadas através da instrução CREATE EXCEPTION, conforme mostrado na Listagem 29.4.
Para gerar uma exceção de dentro do procedimento armazenado, você pode usar a sintaxe mostrada no
exemplo, bem como esta:
EXCEPTION NomeDaExceção;
Na Listagem 29.4, definimos três exceções obtidas no procedimento armazenado, sob várias cir-
cunstâncias. Os comentários, no procedimento, explicam o que ocorre. O principal aspecto a considerar
é que essas verificações são executadas dentro do procedimento armazenado. Conseqüentemente, qual-
quer aplicação de cliente que execute esse procedimento pode ter as mesmas restrições de integridade ga-
rantidas.
Definindo triggers
Triggers são, basicamente, procedimentos armazenados, com a diferença de que dependem de certos
eventos, não sendo chamados diretamente da aplicação do cliente ou de um outro procedimento armaze-
nado. Um evento de trigger ocorre durante uma operação de atualização, inserção ou exclusão de uma ta-
bela.
Da mesma maneira que os procedimentos armazenados, os triggers podem usar exceções, o que
permite executar várias verificações de integridade de dados durante qualquer uma das operações men-
cionadas anteriormente, em uma determinada tabela. Os triggers oferecem os seguintes benefícios:
l Imposição de integridade referencial. Apenas dados válidos podem ser inseridos em uma tabela.
l Manutenção otimizada. Quaisquer alterações feitas no trigger são refletidas em todas as aplica-
ções que utilizam a tabela na qual o trigger se aplica.
l Rastreamento automático de modificações de tabela. O trigger pode registrar vários eventos que
ocorrem nas tabelas.
l Notificação automática de alterações na tabela, através de alertas de evento.
Os triggers consistem em um cabeçalho e um corpo, exatamente como os procedimentos armazena-
dos. O cabeçalho do trigger contém o nome do trigger, o nome da tabela na qual se aplicará o trigger e
uma instrução indicando quando um trigger é disparado. O corpo do trigger contém uma lista opcional
de variáveis locais e o bloco das instruções SQL que executam a lógica efetiva inserida em um bloco
BEGIN..END, exatamente como um procedimento armazenado.
Os triggers são criados através da instrução CREATE TRIGGER. A Listagem 29.5 ilustra um trigger no
InterBase que armazena um histórico de alterações salariais para funcionários.
OLD_SALARY,
PERCENT_CHANGE)
VALUES
old.EMP_NO,
“now”,
USER,
old.SALARY,
(new.SALARY - old.SALARY) * 100 / old.SALARY);
END
Vamos examinar esse exemplo mais detalhadamente. O cabeçalho contém a seguinte instrução:
CREATE TRIGGER SALARY_CHANGE_HISTORY FOR EMPLOYEE
AFTER UPDATE AS
Primeiramente, a instrução CREATE TRIGGER cria um trigger, com o nome SALARY_CHANGE_HISTORY. A se-
guir, a instrução FOR EMPLOYEE especifica em qual tabela o trigger será aplicado; neste caso, a tabela é
EMPLOYEE. A instrução AFTER UPDATE informa que o trigger deve ser disparado depois das atualizações na ta-
bela EMPLOYEE. Essa instrução pode ser substituída por BEFORE UPDATE, que especifica que o trigger deve ser
disparado antes que as alterações sejam feitas na tabela.
Os triggers não servem apenas para a atualização de tabelas. As seguintes partes de cabeçalho de
trigger podem ser usadas, na definição dos triggers:
l AFTER UPDATE. Dispara o trigger depois de um registro ter sido atualizado na tabela.
l AFTER INSERT. Dispara o trigger depois de um registro ter sido inserido na tabela.
l AFTER DELETE. Dispara o trigger depois de um registro ter sido excluído da tabela.
l BEFORE UPDATE. Dispara o trigger antes de um registro ter sido atualizado na tabela.
l BEFORE INSERT. Dispara o trigger antes da inserção de um novo registro na tabela.
l BEFORE DELETE. Dispara o trigger antes da exclusão de um registro na tabela.
Seguindo a cláusula AS na definição do trigger, encontramos o corpo do trigger, que consiste em ins-
truções SQL formando a lógica do trigger. No exemplo da Listagem 29.5, é feita uma comparação entre
o salário antigo e o novo. Se houver uma diferença, um registro será adicionado à tabela SALARY_HISTORY,
indicando a alteração.
O exemplo faz referência aos identificadores Old e New. Essas variáveis de contexto se referem aos va-
lores atual e anterior da linha que está sendo atualizada. Old não é usado durante uma inserção de regis-
tro, bem como New não é usado durante uma exclusão de registro.
Você verá os triggers usados mais freqüentemente no Capítulo 33, que cobre uma aplicação clien-
te/servidor do InterBase.
986
Tabela 29.2 Privilégios de acesso da SQL
Privilégio Acesso
ALL O usuário pode selecionar, inserir, atualizar e excluir dados. Veja outros direitos de acesso.
ALL também concede direitos de execução em procedimentos armazenados.
SELECT O usuário pode ler os dados.
DELETE O usuário pode excluir os dados.
INSERT O usuário pode gravar novos dados.
UPDATE O usuário pode editar dados.
EXECUTE O usuário pode executar ou chamar um procedimento armazenado.
A próxima instrução concede acesso de leitura e edição na tabela EMPLOYEE para os usuários JOHN e
JANE:
Também é possível conceder acesso a uma lista de usuários. Se você deseja conceder todos os privi-
légios a um usuário, use o privilégio ALL em sua instrução GRANT:
GRANT ALL ON EMPLOYEE TO JANE;
Através da instrução anterior, o usuário JANE terá os acessos SELECT, UPDATE e DELETE na tabela EMPLOYEE.
Também é possível conceder privilégios a colunas específicas em uma tabela, conforme mostrado a
seguir:
GRANT SELECT, UPDATE (CONTACT, PHONE) ON CUSTOMERS TO PUBLIC;
Essa instrução concede o acesso de leitura e de edição para os campos CONTACT e PHONE, na tabela
CUSTOMERS, para todos os usuários, usando PUBLIC, que representa todos os usuários.
Você também deve conceder privilégios para procedimentos armazenados que requeiram acesso
para certas tabelas. Por exemplo, o exemplo a seguir concede acesso de leitura e atualização à tabela de
cliente no procedimento armazenado UPDATE_CUSTOMER:
GRANT SELECT, UPDATE ON CUSTOMERS TO PROCEDURE UPDATE_CUSTOMER;
987
Concedendo acesso a views
Na maioria das vezes, usar GRANT em uma view equivale, em SQL, a utilizar GRANT em uma tabela. Entretan-
to, você deve se certificar de que o usuário para o qual está concedendo privilégios UPDATE, INSERT e/ou
DELETE também tem o mesmo privilégio que para as tabelas básicas, às quais a view faz referência. Usar
uma instrução WITH CHECK OPTION ao criar uma view assegura que os campos a serem editados podem ser
vistos antes que a operação seja efetivada. Recomenda-se usar essa opção para criar views modificáveis.
Aqui, os usuários MIKE, KIM e SALLY, bem como o procedimento armazenado ADD_CUSTOMER, podem
executar o procedimento armazenado EDIT_CUSTOMER.
988
Tabela 29.3 Propriedades de TDatabase
Propriedade Propósito
AliasName Um alias do BDE definido pelo utilitário BDE Configuration. Essa propriedade não pode ser
usada em conjunto com a propriedade DriverName.
Connected Uma propriedade booleana que determina se o componente TDatabase está vinculado a um
banco de dados.
DatabaseName Define um alias específico para a aplicação. Outros componentes TDataset (TTable, Tquery e
TStoredProc) usam o valor dessa propriedade para suas propriedades AliasName.
DatasetCount O número de componentes TDataset vinculados ao componente TDatabase.
Datasets Uma matriz referindo-se a todos os componentes TDataset vinculados ao componente
TDatabase.
Directory Diretório de trabalho para um banco de dados do Paradox ou do dBase.
DriverName Nome de um driver BDE como o Oracle, dBASE, InterBase e assim por diante. Essa
propriedade não pode ser usada em conjunto com a propriedade AliasName.
Exclusive Concede a uma aplicação acesso exclusivo ao banco de dados.
Handle Utilizado para fazer chamadas diretas para a API do Borland Database Engine (BDE).
InTransaction Especifica se uma transação está em andamento.
IsSQLBased Propriedade booleana que determina se o banco de dados conectado é baseado em SQL.
Esse valor é False se a propriedade Driver equivale a STANDARD.
KeepConnection Propriedade booleana que determina se o TDatabase mantém uma conexão com o banco de
dados, quando nenhum TDatasets está aberto. Essa propriedade é usada por motivos de
eficiência, pois a conexão com alguns servidores SQL pode demorar alguns instantes.
Locale Identifica o driver da linguagem usado com o componente TDatabase. Ele é usado
principalmente para chamadas diretas ao BDE.
LoginPrompt Determina como o componente TDatabase manipula logins de usuário. Se essa propriedade é
definida como True, uma caixa de diálogo de login padrão é exibida. Se essa propriedade é
definida como False, os parâmetros login devem ser fornecidos no código no evento
TDataBase.OnLogin.
Name O nome do componente, conforme a referência em outros componentes.
Owner O proprietário do componente TDatabase.
Params Passa os parâmetros requeridos para conectar ao banco de dados do servidor. Os parâmetros
default são definidos através do utilitário de configuração BDE, mas podem ser
personalizados aqui.
Session Aponta para o componente da sessão com o qual esse componente de banco de dados está
associado.
SessionAlias Especifica se um componente de banco de dados está usando um alias de sessão.
Tag Propriedade longint usada para armazenar algum valor inteiro.
Temporary Propriedade booleana, indicando se o componente TDatabase foi criado como resultado de
nenhum componente TDatabase ter sido apresentado, durante a abertura de uma TTable,
TQuery ou TStoredProc.
TraceFlags Especifica as operações de banco de dados para fazer rastreamentos com o SQL Monitor,
durante o processo de execução.
TransIsolation Determina o nível de isolamento da transação para o servidor.
989
A Tabela 29.4 lista métodos de TDataBase.
Método Propósito
ApplyUpdates Envia atualizações, por cache, para datasets especificados no servidor do banco de dados.
Close Fecha a conexão TDatabase e todos os componentes TDataset vinculados.
CloseDatasets Fecha todos os componentes TDataset vinculados ao componente TDatabase, o que não
fecha, necessariamente, a conexão TDatabase.
Commit Emite todas as alterações no banco de dados dentro de uma transação. A transação deve ter
sido estabelecida através de uma chamada para StartTransaction.
Create Reserva memória e cria instâncias de um componente TDatabase.
Destroy Libera memória, destruindo a instância TDatabase.
Execute Executa uma instrução SQL sem o overhead de um componente TQuery.
FlushSchemaCache Esvazia as informações de esquema em cache para uma tabela.
Free O mesmo que Destroy, com a exceção de que ele primeiro determina se o componente
TDatabase é definido como nil antes da chamada para destruir.
Open Conecta o componente TDatabase ao banco de dados do servidor. Definindo a propriedade
Connected como True, esse método é chamado automaticamente.
RollBack Aborta ou cancela uma transação, para cancelar então quaisquer alterações feitas no servidor
desde a última chamada para StartTransaction.
StartTransaction Inicia uma transação com o nível de isolamento especificado pela propriedade
TransIsolation. As modificações feitas no servidor não são emitidas até que seja feita uma
chamada para o método Commit. Para cancelar alterações, faça uma chamada para o método
RollBack.
ValidateName Gera uma exceção, se um banco de dados especificado já estiver aberto na sessão ativa.
Controle de segurança
O componente TDatabase permite controlar o acesso do usuário aos dados do servidor, manipulando o
processo de login, durante o qual um usuário deve fornecer um nome de usuário e senha válidos, a fim de
obter acesso a dados vitais. Por default, uma caixa de diálogo de login padrão é chamada quando o usuá-
rio é conectado a um banco de dados do servidor.
Existem várias maneiras de manipular logins. Em uma delas, você pode modificar todos os logins,
990 permitindo que os usuários obtenham acesso aos dados sem ter de efetuar o login. Em outra, você pode
fornecer uma caixa de diálogo de login diferente, de maneira que possa realizar suas próprias verifica-
ções de validação antes de passar o nome e a senha do usuário para o servidor, para as verificações nor-
mais. Finalmente, você pode permitir que os usuários efetuem o logoff e o login novamente, sem encer-
rar a aplicação. As próximas seções ilustram essas três técnicas.
Propriedade Descrição
AliasName Define um alias BDE existente que foi definido com o BDE Administrator. Corresponde,
normalmente, ao mesmo valor usado como valor da propriedade Alias, para os
componentes TTable e TQuery.
DatabaseName Define um alias em nível de aplicação que será visto pelos componentes descendentes de
TDataset (TTable, TQuery e TStoredProc) dentro da aplicação atual. Esses componentes usarão
esses valores como valores da propriedade Alias.
LoginPrompt Defina como False, para que o componente TDatabase se pareça com a sua propriedade
Params, a fim de descobrir o nome e a senha do usuário.
Params Especifica o nome e a senha do usuário. Providencie a chamada do String List Editor, para
essa propriedade, a fim de definir seus valores.
Depois de definir as propriedades de TDatabase adequadamente, você deverá vincular todos os com-
ponentes de TTable, TQuery e TStoredProc a TDatabase, substituindo o valor da propriedade TDatabase.Databa-
seName pelo valor da sua propriedade Alias. Esse valor aparecerá na lista drop-down de aliases, quando
você selecionar a lista drop down no Object Inspector.
Agora, quando você definir a propriedade TDatabase.Connected como True, sua aplicação será conecta-
da ao servidor, sem que seja emitido um pedido para o usuário, solicitando um nome de usuário e uma
senha. Serão usados os valores definidos na propriedade Params. O mesmo ocorrerá quando a aplicação
for executada.
Você encontrará um exemplo sucinto, denominado NoLogin.dpr, no CD-ROM que acompanha este
livro.
unit MainFrm;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls, Grids, DBGrids, BDE, DB, DBTables;
type
TMainForm = class(TForm)
dbMain: TDatabase;
tblEmployee: TTable;
dsEmployee: TDataSource;
dgbEmployee: TDBGrid;
btnLogon: TButton;
btnLogOff: TButton;
procedure btnLogonClick(Sender: TObject);
procedure dbMainLogin(Database: TDatabase; LoginParams: TStrings);
procedure btnLogOffClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
992
Listagem 29.6 Continuação
var
MainForm: TMainForm;
implementation
uses LoginFrm;
{$R *.DFM}
993
Listagem 29.6 Continuação
A Listagem 29.6 apresenta um formulário principal com dois campos: TempLoginParams e LoginSuccess.
O campo TempLoginParams obtém o nome e a senha do usuário. O método btnLogonClick( ) corresponde à
lógica do processo de login, enquanto o manipulador de evento btnLogOffClick( ) corresponde à lógica do
processo de logoff. O método dbMainLogin( ) corresponde ao manipulador de evento OnLogin para dbMain.
A lógica do código está explicada no comentário do código. Observe que esse projeto utiliza o mesmo
TLoginForm que foi usado no exemplo anterior. Você encontrará esse exemplo no projeto LogOnOff.dpr no
CD-ROM que acompanha este livro.
Controle de transação
Anteriormente, neste capítulo, falamos sobre transações e mencionamos como elas permitem que muitas
alterações sejam feitas no banco de dados, como um todo, para assegurar a consistência do banco de da-
dos.
Pode-se manipular um processamento de transação a partir de aplicações de cliente do Delphi 5,
usando as propriedades e métodos específicos para transações do TDatabase. A próxima seção explica
como executar processamentos de transações de dentro da sua aplicação em Delphi 5.
Manipulando transações
Na Tabela 29.4, você viu três métodos de TDatabase que lidam, especificamente, com as transações: Start-
Transaction( ), Commit( ) e RollBack( ).
StartTransaction( ) inicia uma transação usando o nível de isolamento especificado pela proprie-
dade TDatabase.TransIsolation. Quaisquer alterações feitas no servidor depois da chamada de StartTrans-
action( ) serão refletidas na transação atual.
Se todas as alterações no servidor forem bem-sucedidas, será feita uma chamada para TDatabase.Com-
mit( ), para que todas as alterações sejam finalizadas de uma só vez. Se tiver ocorrido um erro, por outro
lado, TDatabase.RollBack( ) será chamado para cancelar quaisquer alterações que tenham sido feitas.
994
O exemplo típico da aplicação do processamento das transações é o exemplo do estoque. Dada uma
tabela ORDER e uma tabela INVENTORY, sempre que um pedido for feito, um novo registro deverá ser adicio-
nado à tabela ORDER. Igualmente, a tabela INVENTORY deverá ser atualizada para refletir a nova contagem de
item em estoque para a peça ou as peças solicitadas. Agora, suponha que um usuário insira um pedido em
um sistema no qual as transações não foram apresentadas. A tabela ORDER obtém o novo registro, mas an-
tes da tabela INVENTORY ser atualizada, ocorre falta de energia. O banco de dados pode apresentar incoe-
rências, pois a tabela INVENTORY pode não refletir, com precisão, os itens em estoque. O processamento da
transação pode contornar esse problema, assegurando que ambas as modificações na tabela sejam bem-
sucedidas, antes de finalizar quaisquer alterações no banco de dados. A Listagem 29.7 ilustra como isso
ocorre, no código do Delphi 5.
dbMain.StartTransaction;
try
spAddOrder.ParamByName(‘ORDER_NO’).AsInteger := OrderNo;
Esse código contém um exemplo bastante simples, descrevendo como usar o processamento de
transações, para assegurar a coerência do banco de dados. Ele utiliza dois procedimentos armazenados –
um para adicionar o novo registro do pedido e outro para atualizar a tabela INVENTORY com os novos da-
dos. Esse é apenas um trecho de código ilustrando um processamento de transação com o Delphi. Essa
lógica pode ser melhor manipulada, provavelmente, no lado do servidor.
Em alguns casos, o tipo de processamento de transação ideal depende dos recursos específicos do
servidor. Nessa situação, você pode usar um componente TQuery, a fim de passar o código SQL específico
para o servidor, o que requer que você defina, convenientemente, o modo de passagem SQL.
SHARED AUTOCOMMIT As transações são manipuladas linha a linha. Esse método é bastante parecido com o
de bancos de dados de desktop. No ambiente cliente/servidor, isso causa uma
elevação no tráfego da rede, não sendo a abordagem recomendada. Entretanto, essa é
a definição default para aplicações do Delphi 5.
SHARED NOAUTOCOMMIT As aplicações do Delphi 5 devem, implicitamente, iniciar, emitir e cancelar transações
usando os métodos Tdatabase.StartTransaction( ), Commit( ) e RollBack( ).
NOT SHARED Os componentes do BDE e TQuery, relacionados a instruções de passagem SQL, não
compartilham as mesmas conexões, o que significa que o código SQL não se restringe
a recursos do BDE, podendo utilizar recursos específicos do servidor.
Se você não está usando SQL de passagem, mas deseja ter um controle maior sobre o processamento
da transação, defina o modo de passagem como SHARED NOAUTOCOMMIT e manipule, você mesmo, o processa-
mento da transação. Na maioria dos casos, isso deve atender às suas necessidades. Considere que, em am-
bientes multiusuários, se as mesmas linhas forem atualizadas repetidas vezes, poderão ocorrer conflitos.
Níveis de isolamento
Os níveis de isolamento determinam a forma como as transações vêem os dados que estão sendo acessa-
dos por outras transações. A propriedade TDatabase.TransIsolation determina o nível de isolamento que
será usado por uma determinada transação. Existem três níveis de isolamento, aos quais pode-se atribuir
a propriedade TransIsolation:
tiDirtyRead O nível de isolamento mais baixo. As transações que usam esse nível de isolamento
podem ler as alterações feitas em outras transações, que não tenham sido emitidas.
tiReadCommitted O nível de isolamento padrão. As transações que usam esse nível de isolamento
podem ler apenas alterações emitidas por outras transações.
tiRepeatableRead Esse é o nível de isolamento mais elevado. As transações que usam esse nível de
isolamento não podem ler alterações em dados lidos anteriormente, feitos por outras
transações.
O suporte para os níveis de isolamento aqui listados pode variar para diferentes servidores. Se um
nível de isolamento específico não for suportado, o Delphi 5 usará o nível de isolamento imediatamente
superior.
TTable ou TQuery
Um erro comum é a idéia de que desenvolver aplicações de cliente front-end equivale ou é semelhante a
desenvolver aplicações para bancos de dados de desktop.
Você encontrará esse tipo de pensamento ao decidir como ou quando se deve usar os componentes
TTable ou TQuery para o acesso ao banco de dados. Nos parágrafos seguintes, discutiremos algumas quali-
dades e defeitos de um componente TTable, bem como quando ele pode ser usado e quando deve ser evi-
tado. Você verá o motivo de normalmente ser melhor usar um componente TQuery.
É emitida, então, uma instrução SELECT, visando construir um conjunto de resultados contendo to-
das as colunas e linhas da tabela. O período de tempo necessário para abrir uma tabela é proporcional ao
tamanho da tabela SQL (ao número de linhas). Embora apenas a quantidade de linhas necessária para
preencher os componentes ligados aos dados retorne para o cliente, será construído um conjunto inteiro
de resultados, em resposta à consulta. Esse processo ocorre sempre que TTable é aberto. Em tabelas extre-
mamente grandes, freqüentes em bancos de dados cliente/servidor, apenas essa operação pode levar até
cerca de 20 segundos. É importante ressaltar que alguns servidores SQL, como o Sybase e o Microsoft
SQL, não permitem que o cliente aborte a recuperação de um conjunto de resultados, e essa característi-
ca, além do tamanho da tabela, que afeta a duração da seleção, devem ser consideradas. No Oracle, no
InterBase e no Informix, é possível abortar um conjunto de resultados, o que permite superar uma even-
tual demora desnecessária.
Apesar das desvantagens da utilização de TTables em aplicações de cliente, geralmente elas são bas-
tante adequadas para acessar pequenas tabelas no servidor. É preciso testar suas aplicações, para determi-
nar se o desempenho é aceitável.
NOTA
O MIDAS manipula o retorno de pacotes de dados de maneira ligeiramente diferente. Leia mais sobre esse
assunto no Capítulo 34.
Esse é um SELECT aninhado. Em uma instrução SELECT aninhada, a instrução interna retorna seu con-
junto de resultados para a instrução SELECT externa. Essa, por sua vez, usa esse conjunto de resultados
para processar suas instruções. A consulta interna desse exemplo usou a função de agregação SQL MIN( )
para retornar o valor mínimo na coluna NAME da tabela EMPLOYEES. O conjunto de resultados, composto de
uma única linha e uma única coluna é, então, usado na consulta externa, a fim de recuperar as linhas res-
tantes.
O resultado disso é que você obtém muito mais flexibilidade e eficiência maximizando os recursos
da SQL, em vez de usar o componente TQuery. Quando usa TTable, você limita o que pode ser feito nos da-
dos do servidor.
SQL dinâmica
Através da SQL dinâmica, você pode modificar as instruções SQL em runtme, com base em várias condi-
ções. Quando você chama o String List Editor para a propriedade TQuery.SQL e insere uma instrução como
a seguinte, está inserindo uma instrução SQL estática:
SELECT * FROM EMPLOYEE WHERE COUNTRY = “USA”
Essa instrução não sofrerá variações em runtime, a menos que você a substitua completamente.
Para tornar essa instrução dinâmica, você deve inserir a seguinte propriedade SQL:
SELECT * FROM CUSTOMER WHERE COUNTRY = :iCOUNTRY;
Nessa instrução, em vez de construir uma codificação complicada para informar o valor a ser pro-
curado, fornecemos uma espécie de variável, ou seja, um parâmetro cujos valores poderão ser especifica-
dos mais tarde. Essa variável é denominada iCountry, e aparece depois dos dois pontos na instrução
SELECT. Seu nome foi escolhido aleatoriamente. Agora, você pode fazer pesquisas para qualquer país, for-
necendo sua string.
Existem várias maneiras de fornecer valores para uma consulta parametrizada. Uma delas consiste
em usar o editor de propriedades para a propriedade TQuery.Params. Outra maneira consiste em fornecer
esse valor em runtime. Você também pode fornecer o valor a partir de outro dataset, através de um com-
ponente TDataSource.
998
Fornecendo parâmetros TQuery através do Params Property Editor
Quando você chama o editor de propriedade para TQuery.Params, a lista Parameter Name (nome do parâ-
metro) exibe os parâmetros para uma determinada consulta. Para cada parâmetro listado, você deve sele-
cionar um tipo na caixa de combinação drop down Data Type (tipo de dado). Pode-se especificar um va-
lor inicial para o parâmetro no campo de valor, se for o caso. Você pode selecionar a caixa de seleção
NULL, para definir o valor do parâmetro como nulo. Quando você seleciona OK, a consulta prepara
seus parâmetros, acoplando-os a seus tipos (veja a nota explicativa intitulada “Preparando consultas”).
Quando você chamar TQuery.Open( ), um conjunto de resultados será retornado para a TQuery.
Preparando consultas
Quando uma instrução SQL é enviada para o servidor, este deve desmembrar, validar, compilar e
executar as instruções. Esses passos sempre ocorrem quando são enviadas instruções SQL para o ser-
vidor. Pode-se melhorar o desempenho, se permitirmos que o servidor execute, antecipadamente, os
passos de desmembramento, validação e compilação, a fim de “preparar” as instruções SQL, antes da
sua execução pelo servidor. Isso pode ser bastante vantajoso quando usamos uma consulta repetidas
vezes em um loop. Devemos chamar TQuery.Prepare( ) antes de inserir o loop, conforme mostrado no
código a seguir:
Query1.Prepare; // Prepara primeiramente a consulta.
try
{ Insere um loop para executar uma consulta várias vezes }
for i := 1 to 100 do begin
{ fornece os parâmetros para a consulta }
Query1.ParamByName(‘SomeParam’).AsInteger := I;
Query1.ParamByName(‘SomeOtherParam’).AsString := SomeString;
Query1.Open; // Abre a consulta.
try
{ Usa, aqui, o conjunto de resultados de Query1. }
finally
Query1.Close; // Fecha a consulta.
end;
end;
finally
Query1.Unprepare; // Chama Unprepare para liberar recursos
end;
Prepare( ) deve ser chamado uma única vez, antes de sua utilização repetidas vezes. Você tam-
bém pode alterar os valores dos parâmetros da consulta, depois da primeira chamada de Prepare( ),
sem que haja a necessidade de chamar Prepare( ) novamente. Entretanto, se você mesmo alterar a
instrução SQL, deverá chamar Prepare( ) novamente, antes de reutilizá-la. Cada chamada para Prepa-
re( ) deve corresponder uma chamada para TQuery.UnPrepare( ), a fim de liberar os recursos alocados
por Prepare( ).
As consultas estão preparadas quando se seleciona o botão OK no editor da propriedade Params,
ou quando você chama o método TQuery.Prepare( ), conforme mostrado no código anterior. Também
é recomendado chamar Prepare( ) uma única vez no manipulador de evento OnCreate do formulário,
bem como UnPrepare( ) no manipulador de evento OnDestroy do formulário, para as consultas cujas ins-
truções SQL não se alterarão. Não é necessário preparar suas consultas SQL, embora essa seja uma
boa idéia.
Para usar a propriedade Params, a fim de fornecer valores para os parâmetros :Name, :CAPITAL e
:POPULATION,
você deve emitir a seguinte instrução:
with Query1 do begin
Params[0].AsString := ‘Peru’;
Params[1].AsString := ‘Lima”
Params[2].AsInteger := 22,000,000;
end;
Os valores fornecidos podem ser acoplados aos parâmetros da instrução SQL. É importante saber
que a ordem dos parâmetros na instrução SQL informa suas posições na propriedade Params.
Esse código é um pouco mais claro do que o código contendo parâmetros aos quais estão sendo atri-
buídos valores.
Nesse caso, você precisa de um valor para o parâmetro denominado EMP_NO. Primeiro, faça a atri-
buição para TDataSource que faz referência ao componente mestre TTable para a propriedade DataSource de
TQuery. O Delphi procurará, então, por um campo denominado EMP_NO na tabela à qual TTable faz referên-
cia, e acoplará o valor daquela coluna ao parâmetro de Tquery, na linha atual. Isso está ilustrado no exem-
1000 plo encontrado no projeto LnkQuery.dpr, no CD-ROM que acompanha este livro.
Usando a função Format para projetar instruções SQL dinâmicas
Tendo em vista o que já vimos com relação à utilização de consultas parametrizadas, não teremos dificul-
dade em reconhecer como válidas as seguintes instruções SQL:
SELECT * FROM PART ORDER BY :ORDERVAL;
SELECT * FROM :TABLENAME
Infelizmente, não é possível substituir certas palavras em uma instrução SQL, como, por exemplo,
nomes de colunas e nomes de tabelas. Os servidores SQL não aceitam esse recurso. Felizmente, é possível
superar essa limitação, incorporando uma certa flexibilidade a suas instruções dinâmicas SQL, se cons-
truirmos instruções SQL em runtime usando a função Format( ).
Se você tem alguma experiência com programação em C ou C++, verá que a função Format( ) se as-
semelha à função printf( ) do C. Veja a nota explicativa da função Format( ).
Especificador Descrição
Por exemplo, na string “Meu nome é %s e eu tenho %d anos de idade.”, pode-se observar dois especifi-
cadores de formato. O especificador %s indica que uma string deve ser inserida nesse local, enquanto
o especificador %d indica que um inteiro deve ser inserido nesse local. Para construir a string, use a fun-
ção Format( ):
S := Format(‘Meu nome é %s e eu tenho %d anos de idade.”, [‘Xavier’, 32]);
A função Format( ) substitui os especificadores de formato por uma string de origem e uma ma-
triz aberta de argumentos, o que retorna a string resultante. Veja mais detalhes sobre a função For-
mat( ) na ajuda on-line do Delphi 5.
Assim, para construir instruções SQL com flexibilidade suficiente para modificar nomes de campos
ou de tabelas, use a função Format( ), conforme ilustrado nos exemplos de código seguintes.
A Listagem 29.8 mostra como usar a função Format( ), para que o usuário obtenha os campos so-
bre os quais será aplicada a classificação do conjunto de resultados de uma consulta. A lista de campos
se localiza em uma caixa de listagem, e o código corresponde precisamente ao evento OnClick daquela
caixa de listagem. Você verá essa demonstração no projeto OrderBy.dpr, no CD-ROM que acompanha
este livro.
1001
Listagem 29.8 Usando Format( ) para especificar a classificação de colunas
A fim de preencher a caixa de listagem na Listagem 29.8 com os nomes de campo na tabela de itens,
utilizamos o seguinte código no manipulador de evento OnCreate do formulário:
tblParts.Open;
try
tblParts.GetFieldNames(lbFields.Items);
finally
tblParts.Close;
end;
É usado, a seguir, o manipulador de evento lbTables.OnClick, para selecionar a tabela na qual será
executada uma consulta SELECT, conforme mostra a Listagem 29.9.
Listagem 29.9 Usando Format( ) para especificar uma tabela a ser selecionada
Essa demonstração é fornecida no projeto SelTable.dpr, no CD-ROM que acompanha este livro.
Você pode recuperar os valores das colunas, conforme mostra a Listagem 29.10, que mostra o códi-
go para o projeto ResltSet.dpr no CD-ROM que acompanha este livro.
Use o método de datasets padrão, FieldValues( ), para acessar os valores dos campos de qryCustomer
do código acima. FieldValues( ) é o método de dataset padrão e, portanto, é desnecessário especificar o
nome do método explicitamente, como mostramos a seguir:
Add(VarToStr(qryCustomer.FieldValues[‘Contact’]));
ATENÇÃO
A função FieldValues( ) retorna um tipo de campo variant. Se o campo contiver um valor nulo, uma tenta-
tiva de obter o valor do campo, com FieldValue( ), poderá resultar em uma exceção EVariantError. Por
esse motivo, o Delphi fornece a função VarToStr( ), que converte valores de string nulos em uma string va-
zia. Funções equivalentes para outros tipos de dados não são fornecidas. Entretanto, você pode construir
as suas próprias funções, conforme mostrado a seguir, para tipos inteiros: 1003
function VarToInt(const V: Variant): Integer;
begin
if TVarData(V).VType < > varNull then
Result := V
else
Result := 0;
end;
Tenha cuidado, porém, quando salvar os dados novamente. Um valor nulo é um valor válido em um
banco de dados SQL. Se você tentar substituir aquele valor por uma string vazia, diferente de nulo, estará
comprometendo a integridade dos dados. Você terá de construir uma solução em runtime, como um teste
para valores NULL, armazenando algumas strings predefinidas, a fim de representar o valor nulo.
Também é possível recuperar os valores dos campos de uma TQuery usando a propriedade TQuery.
Fields.
A propriedade Fields é usada da mesma maneira que a propriedade TQuery.Params, com a exceção
de que ela se refere às colunas do conjunto de resultados. TQuery também tem o método FieldByName( ),
que apresenta o mesmo comportamento que o método ParamByName( ).
A propriedade UniDirectional
O componente TQuery tem a propriedade UniDirectional, que visa otimizar o acesso a um banco de dados.
Ela se aplica a bancos de dados que trabalham com cursores bidimensionais. Esse recurso possibilita mo-
ver para a frente e para trás, através do conjunto de resultados da consulta. Por default, essa propriedade
é False. Por esse motivo, quando você tem componentes como o TDBGrid vinculado a um banco de dados
que não aceita movimento bidirecional, o Delphi simula esse movimento, através do armazenamento dos
registros em buffers no lado do cliente. Isso pode comprometer rapidamente muitos recursos no lado do
cliente. Portanto, se você quiser mover apenas para a frente em um conjunto de resultados, ou se quiser
avançar ao longo do conjunto de resultados uma única vez, defina UniDirectional como True.
Propriedade Descrição
with spAddCountry do
begin
ParamByName(‘iCOUNTRY’).AsString := edtCountry.Text;
ParamByName(‘iCURRENCY’).AsString := edtCurrency.Text;
ExecProc;
edtCountry.Text := ‘’;
edtCurrency.Text := ‘’;
tblCountries.Refresh;
end;
Nesse caso, você primeiramente atribui os valores de dois TEdits aos parâmetros TStoredProc, através
do método ParamByName( ). A seguir, faça a chamada para a função TStoredProc.ExecProc( ), que executa o
procedimento armazenado. Você encontrará um exemplo ilustrando esse código no projeto AddCntry.dpr.
NOTA
Para executar o projeto AddCntry.dpr, você deve usar o utilitário BDEADMIN.EXE para definir um novo alias
denominado “DB”, que deve, por sua vez, apontar para o arquivo \CODE\DATA\DDGIB.GDB, encontrado no
CD-ROM que acompanha este livro. Para obter mais informações, consulte a documentação do utilitário
BDE Administrator.
Para executar esse procedimento armazenado dentro do Delphi 5, você precisa usar um componen-
te TQuery com a seguinte propriedade SQL:
SELECT * FROM GET_EMPLOYEES_IN_DEPT(
:iDEPT_NO)
Observe que essa instrução utiliza a instrução SELECT, mesmo que o procedimento seja uma tabela. A
diferença, como você pode ver, é que também é necessário fornecer o parâmetro de entrada iDEPT_NO.
Criamos um projeto de exemplo, Emp_Dept.dpr, para ilustrar a execução do procedimento armazena-
do anterior.
qryGetEmployees é o componente de TQuery que executa o procedimento armazenado mostrado na
Listagem 29.13. Ele obtém seus parâmetros de qryDepartment, que executa uma instrução SELECT simples na
tabela DEPARTMENT do banco de dados. qryGetEmployees está vinculado a dbgEmployees, que mostra uma lista
rolável de departamentos. Quando o usuário rola através de dbgDepartment, o manipulador de evento OnDa-
taChange de dsDepartment é chamado. Podemos dizer que dsDepartment está vinculado a qryDepartment. Esse
manipulador de evento executa o código mostrado na Listagem 29.14, que define o parâmetro para
qryGetEmployees e apanha a saída do conjunto de resultados.
Close;
ParamByName(‘iDEPT_NO’).AsString := qryDepartment[‘DEPT_NO’];
Open;
end;
end;
Resumo
Este capítulo ofereceu informações resumidas sobre o desenvolvimento cliente/servidor. Discutimos,
primeiramente, os elementos que compõem um sistema cliente/servidor. Comparamos o desenvolvi-
mento cliente/servidor com metodologias de desenvolvimento de bancos de dados de desktop tradicio-
nais. Apresentamos, ainda, várias técnicas, usando o Delphi 5 e o InterBase, para ampliar seus conheci-
mentos com relação ao desenvolvimento de projetos cliente/servidor.
1008
Extensão da VCL de CAPÍTULO
banco de dados
30
NE STE C AP ÍT UL O
l Utilização do BDE
l Tabelas do dBASE
l Tabelas do Paradox
l Extensão de TDataSet
l Resumo
1010
WebBroker: usando a CAPÍTULO
Internet em suas
aplicações
POR NICK HODGES
31
NE STE C AP ÍT UL O
l Extensões de servidor da Web ISAPI, NSAPI
e CGI 1013
l Criação de aplicações da Web com o Delphi 1014
l Páginas HTML dinâmicas com criadores de
conteúdo HTML 1020
l Manutenção de estado com cookies 1028
l Redirecionamento para outro site da Web 1031
l Recuperação de informações de formulários
HTML 1032
l Streaming de dados 1034
l Resumo 1037
A popularidade da Internet sem dúvida vem crescendo muito, e sua utilização por proprietários de com-
putadores é uma realidade. A tecnologia que viabilizou o funcionamento da Internet é incrivelmente sim-
ples, o que resulta na utilização dessa tecnologia por muitas empresas comerciais para criar intranets –
pequenas redes de Web acessadas apenas por pessoas dentro de uma determinada organização. As Intra-
nets correspondem, certamente, a uma forma barata e extremamente eficiente de impulsionar sistemas
de informações de uma organização. Com o advento de novas tecnologias, algumas intranets podem ser
expandidas para extranets – redes que permitem um acesso limitado, embora sua utilização não seja res-
trita aos limites da organização.
Todos esses conceitos, obviamente, tornam a programação específica para a Internet, ou para intra-
nets, um recurso a mais para o programador. Como seria de se esperar, a programação voltada para a
Internet ou intranet, em Delphi, é uma tarefa bastante objetiva. É possível transportar todo o poder do
Delphi para a Web, das seguintes maneiras:
l Encapsulando o Hypertext Transfer Protocol (HTTP) em objetos que podem ser facilmente
acessados
l Fornecendo uma estrutura de aplicação para as interfaces de programação de aplicações (APIs)
dos servidores da Web mais populares e poderosos
l Fornecendo uma abordagem Rapid Application Development (RAD) para construir extensões de
servidores da Web
Através do Delphi e de seus componentes WebBroker, você pode construir, facilmente, extensões
de servidores da Web capazes de fornecer páginas de Hypertext Markup Language (HTML) dinâmicas e
personalizadas, que incluem o acesso a dados de praticamente qualquer fonte.
DICA
Os componentes WebBroker são fornecidos como parte do Delphi Enterprise. Se você é um usuário do
Delphi Professional, pode adquirir os componentes WebBroker como um complemento separado. Visite o
site da Web da Borland (http://www.borland.com) para obter mais informações.
A tecnologia básica que viabiliza a Web é bastante simples. Os dois agentes do processo – o cliente da
Web, ou cliente, e o servidor da Web – devem estabelecer um link de comunicação e passar informações um
para o outro. O cliente solicita informações e o servidor as fornece. É claro que o cliente e o servidor têm de
estar sintonizados com relação à maneira como as informações são transmitidas. Basta um fluxo do tamanho
de um byte em formato ASCII, ao longo da Web, para fazê-lo. O cliente envia uma solicitação de texto e ob-
tém um texto de resposta, e pouco sabe sobre o que está ocorrendo no servidor. Esse processo simples permi-
te que ocorram comunicações entre plataformas diferentes, normalmente através do protocolo TCP/IP.
O método padrão de comunicação usado na Web é o Hypertext Transfer Protocol (HTTP). Um
protocolo corresponde, em poucas palavras, a uma convenção sobre a maneira de conduzir uma negocia-
ção. O HTTP é a um protocolo projetado para passar informações do cliente para o servidor, na forma
de uma solicitação, e do servidor para o cliente, na forma de uma resposta. Isso é feito através de infor-
mações de formatação, correspondentes a um fluxo de bytes de caracteres ASCII e do envio dessa infor-
mação entre os dois agentes. O protocolo HTTP, propriamente dito, é flexível e também bastante pode-
roso. Quando usado em sintonia com a Hypertext Markup Language (HTML), ele fornece, rapidamente
e sem muita dificuldade, páginas de Web para um browser.
Uma solicitação HTTP pode ter a seguinte aparência:
GET /mysite/webapp.dll/dataquery?name=CharlieTuna&company=Borland HTTP/1.0
Connection: Keep-Alive
User-Agent: Mozilla/3.0b4Gold (WinNT; I)
Host:
1012 Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*
A HTTP é independente de estado, o que significa que o servidor não conhece o estado do cliente, e
que a comunicação entre o servidor e o cliente será finalizada quando a solicitação tiver sido satisfeita.
Isso dificulta a criação de aplicações de bancos de dados que usam HTTP, pois muitas aplicações de ban-
cos de dados se baseiam na idéia do acesso, pelo cliente, a um conjunto de dados ativo. As informações de
estado podem ser armazenadas através da utilização de cookies – pedaços de informações armazenados
no cliente, resultantes da resposta do HTTP. O conceito de cookies será discutido mais adiante, ainda
neste capítulo.
ISAPI e NSAPI
Os maiores fornecedores de servidores da Web, a Microsoft e a Netscape, reconhecem a fragilidade ca-
racterística da programação de CGIs, mas reconhecem também as vantagens da criação de Webs dinâmi-
cas. Por esse motivo, em vez de usar um processo separado para cada solicitação, cada empresa escreveu
APIs específicas para seus servidores de Web, que permitem executar extensões de servidores da Web
como dynamic link libraries (DLLs). As DLLs podem ser carregadas de uma única vez, podendo respon-
der, então, a qualquer quantidade de solicitações. Elas são executadas como parte do processo do servi-
dor da Web, sendo seus códigos executados no mesmo espaço de memória usado pelo próprio servidor
da Web. Em vez de passar informações na forma de arquivos, em um sentido ou em outro, as extensões
de servidores da Web podem, simplesmente, passar as informações em ambos os sentidos, utilizando o
mesmo espaço de memória, o que possibilita que as aplicações da Web sejam mais rápidas, eficientes e
menos exigentes, quanto à demanda por recursos.
A Microsoft oferece a simples e objetiva Internet Server Application Programming Interface (ISAPI),
com o seu Internet Information Server (IIS), enquanto a Netscape oferece o Netscape Application Pro-
gramming Interface (NSAPI), um pouco mais complexo, com a sua família de servidores da Web. 1013
O Delphi oferece acesso às duas APIs através das unidades NSAPI.PAS e ISAPI.PAS. Para executar as
aplicações deste capítulo, você deve utilizar um servidor IIS, um servidor Netscape ou um dos numerosos
servidores de shareware ou freeware que atendam à especificação ISAPI.
DICA
Se você ainda não tem um servidor da Web instalado, deve fazer o download do Microsoft Personal Web
Server, no site da Web da Microsoft (http://www.microsoft.com). Ele é freeware e compatível com ISAPI, e
executará todos os exemplos deste capítulo.
TWebModule e TwebDispatcher
Se você selecionar File, New no menu do Delphi, aparecerá a caixa de diálogo New Items (novos itens).
Selecione o ícone Web Server Application (aplicação de servidor da Web), para abrir o assistente que
permite selecionar o tipo de extensão do servidor da Web. As três opções se referem a aplicações
ISAPI/NSAPI, CGI e WinCGI. Este capítulo apresenta o tipo de aplicação ISAPI/NSAPI. A construção
das extensões de servidores de CGI é feita, quase sempre, de uma mesma maneira; porém, as aplicações
ISAPI são as mais fáceis, seja no que se refere à construção ou à execução.
NOTA
O Delphi também inclui um projeto, ISAPITER.DPR, que permite executar módulos ISAPI em um servidor da
Web baseado em NSAPI. A ajuda on-line contém informações sobre como definir um servidor da Web da
Netscape, para executar as DLLs de ISAPI criadas neste capítulo.
1014
Quando você selecionar o tipo de aplicação, o Delphi criará um projeto, baseado em um TWebModule.
O projeto principal, propriamente dito, corresponde a uma DLL, e a unidade principal contém o TWebMo-
dule. TWebModule é um descendente de TDataModule, e contém toda a lógica necessária para receber a solicita-
ção de HTTP, bem como para responder a ela. Um módulo TWebModule pode aceitar apenas controles não-
visuais, exatamente como seu antecedente. Você pode usar todos esses controles de banco de dados, bem
como os controles geradores de HTML, na página da Internet do Component Palette para gerar o con-
teúdo em um TWebModule, o que permite adicionar regras comerciais à sua aplicação baseada na Web, da
mesma maneira que com o TDataModule, em aplicações normais.
O TWebModule tem a propriedade Actions, que contém uma coleção de objetos TWebActionItem que per-
mite, por sua vez, executar um código baseado em uma determinada solicitação. Cada TWebActionItem tem
seu próprio nome; quando um cliente faz a solicitação, com base nesse nome, o código é executado e a
resposta apropriada é dada.
NOTA
Você pode criar uma aplicação de servidor da Web com um dos seus módulos de dados existentes. O TWeb-
Module tem, como um de seus campos, a classe TWebDispatcher, incluída na Component Palette como o
componente TWebDispatcher. Se você substituir o TWebModule padrão em sua aplicação de servidor da Web
por um módulo de dados existente, através da utilização do Project Manager, poderá colocar nele um com-
ponente TWebDispatcher, para que se torne uma aplicação de servidor da Web. O componente TWebDis-
patcher na página da Internet da Component Palette adiciona toda a funcionalidade encapsulada no TWeb-
Module. Assim, se você tiver todas as suas regras comerciais ocultas em um TDataModule existente, faça com
que elas se tornem disponíveis para suas aplicações da Web, o que é muito simples de ser feito por meio do
mouse. Um TDataModule com um componente TWebDispatcher equivale, em termos de funcionamento, a
um TWebModule. A única diferença é que você acessa as ações do HTTP através do componente TWebDispat-
cher, e não do TDataModule.
Selecione o TWebModule, para que suas propriedades sejam exibidas no Object Inspector. Selecione a
propriedade Actions e dê, sobre ela, um clique duplo, ou selecione o editor de propriedades, através do
pequeno botão de reticências (…). Será aberta a caixa de diálogo Actions de WebModule. Dê um clique so-
bre o botão New e selecione o WebActionItem resultante, no editor de propriedades que aparece a seguir.
As propriedades do item Action serão, então, exibidas no Object Inspector. Vá para a propriedade
PathInfo e insira /test. Vá, então, para a página Events no Object Inspector e dê um clique duplo sobre o
evento OnAction para criar um novo manipulador de evento. A aparência será como a seguir:
procedure TWebModule1.WebModule1WebActionItem1Action(Sender: TObject;
Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
begin
end;
Esse manipulador de evento contém todas as informações sobre a solicitação que gerou a ação, bem
como a maneira de responder a ela. A informação da solicitação do cliente está contida no parâmetro Re-
quest, do tipo TWebRequest. O parâmetro Response é do tipo TWebResponse, sendo usado para enviar as infor-
mações necessárias de volta para o cliente. Dentro desse manipulador de evento, você pode gravar qual-
quer código que seja necessário para responder à solicitação, o que inclui a manipulação do arquivo, as
ações sobre o banco de dados, e tudo o que seja necessário para enviar uma página HTML de volta para o
cliente.
Antes de vermos os detalhes de TWebModule, usaremos um exemplo simples para demonstrar os fun-
damentos do funcionamento de uma aplicação de servidor da Web. A maneira mais simples de criar uma
página HTML que responda à solicitação do cliente consiste em construir rapidamente a HTML, o que
pode ser feito com facilidade usando uma TStringList. Depois de posicionar a HTML em TStringList, ela
1015
poderá ser atribuída à propriedade Content do parâmetro Response. Content é uma string sendo usada para
obter a HTML a ser retornada para o cliente. É a única propriedade de Response que deve ser preenchida,
pois contém os dados a serem exibidos. Se for deixado em branco, o browser do cliente informará que o
documento solicitado está vazio. A Listagem 31.1 mostra o código que deve ser adicionado ao manipula-
dor de evento do item de ação /test.
Salve o projeto como SAMPLE1.DLL, compile-o e posicione o arquivo resultante no diretório padrão,
no seu servidor da Web, compatível com ISAPI ou NSAPI. A seguir, posicione seu browser no seguinte
local:
<endereço do servidor da Web>/sample1.dll/test
NOTA
Se você utilizar o código da Listagem 31.1 no CD-ROM que acompanha este livro e colocá-lo em seu com-
putador, mantendo a mesma estrutura de diretórios do CD-ROM, poderá facilmente definir seu servidor da
Web para que acesse a HTML e as DLLs, o que possibilita executar todas as aplicações de exemplo deste
capítulo. Crie, simplesmente, um diretório virtual de servidor da Web, no diretório-raiz, em um diretório
compatível com ISAPI, que aponte para o diretório \bin. A seguir, abra no diretório-raiz o arquivo
INDEX.HTM, que oferece acesso a todo o código do exemplo. Observe que, se você copiar os arquivos do
CD-ROM, eles terão o flag definido como somente-leitura. Será preciso remover este flag no Explorer, caso
seja necessário editar os arquivos copiados do CD-ROM.
1016
FIGURA 31.1 Um exemplo de página da Web.
Observe que o resultado da compilação do projeto corresponde a uma DLL, que obedece à especifi-
cação do ISAPI. O código-fonte do projeto é o seguinte:
library Sample1;
uses
WebBroker,
ISAPIApp,
Unit1 in ‘Unit1.pas’ {WebModule1: TWebModule};
{$R *.RES}
exports
GetExtensionVersion,
HttpExtensionProc,
TerminateExtension;
begin
Application.Initialize;
Application.CreateForm(TWebModule1, WebModule1);
Application.Run;
end.
Observe as três rotinas exportadas. Essas três rotinas – GetExtensionVersion, HttpExtensionProc e Termi-
– são os três únicos procedimentos exigidos pela especificação ISAPI.
nateExtension
ATENÇÃO
Como qualquer outra aplicação típica, sua aplicação ISAPI utiliza um objeto Application global.
Entretanto, diferentemente de uma aplicação regular, esse projeto não utiliza a unidade Forms. Em vez
disso, a unidade WebBroker contém uma variável Application declarada com o tipo TWebApplication,
que manipula todas as chamadas especiais, necessárias para poder se ligar a um servidor da Web
compatível com ISAPI ou NSAPI. Dessa forma, nunca tente adicionar a unidade Forms a uma extensão
de servidor da Web baseado em ISAPI, pois o compilador poderá usar, equivocadamente, a variável
Application errada.
1017
Esse projeto simples ilustra a construção de uma aplicação de servidor da Web, que fornece uma
resposta a uma solicitação do cliente, através do Delphi. Esse exemplo trata da criação dinâmica de
HTML através de código, o que é relativamente simples. Porém, como será visto posteriormente, o
Delphi fornece ferramentas necessárias para soluções muito mais complexas e interessantes. Antes, po-
rém, vamos investigar com um pouco mais de detalhes o funcionamento da aplicação WebBroker, da
próxima seção.
TWebRequest e TWebResponse
TWebRequest e TWebResponse são duas classes abstratas, que encapsulam o protocolo HTTP. TWebRequest ofe-
rece acesso a todas as informações passadas para o servidor, pelo cliente, enquanto TWebResponse contém
propriedades e métodos que permitem que você responda de uma maneira, dentre as várias permitidas,
através do protocolo HTTP. Ambas as classes são declaradas na unidade HTTPAPP.pas, usada pela unidade
WebBroker.pas. As aplicações da Web compatíveis com ISAPI usam, de fato, TISAPIResponse e TISAPIRequest,
que são descendentes das classes abstratas, sendo declaradas em ISAPIAPP.PAS. Devido às poderosas carac-
terísticas do polimorfismo, o Delphi pode passar as classes TISAPIxxx para os parâmetros TWebxxx do mani-
pulador de evento OnAction do TWebModule.
TISAPIRequest contém todas as informações passadas pelo cliente, ao fazer uma solicitação por uma
página da Web. É possível reunir informações sobre o cliente a partir da solicitação. Muitas das proprie-
dades podem estar vazias, pois nem todos os campos são preenchidos, para cada solicitação do HTTP. As
propriedades RemoteHost e RemoteAddr contêm o endereço IP da máquina responsável pela solicitação. A
propriedade UserAgent contém informações sobre o browser usado pelo cliente. A propriedade Accept in-
clui uma listagem dos tipos de gráficos que podem ser exibidos no browser do usuário. A propriedade Re-
ferer contém a URL para a página que o usuário clicou, o que possibilita criar a solicitação. Se for forne-
cida alguma informação relativa a cookies (os cookies serão discutidos mais adiante, ainda neste capítu-
lo), esta fará parte da propriedade Cookie. Se existirem vários cookies, estes poderão ser mais facilmente
acessados através do array CookieFields. Se forem passados parâmetros pela solicitação, todos estarão
contidos em uma única string, dentro da propriedade Query. Poderão, também, ser desmembrados em um
array, na propriedade QueryFields.
NOTA
Os parâmetros passados para uma URL seguem, geralmente, um ponto de interrogação (?) depois do
nome da URL. Se existirem vários parâmetros, serão separados por sinais &, e se tiverem espaços, um sinal
de adição (+) os substituirá. Um conjunto de parâmetros válido pode ter a seguinte aparência, em uma pá-
gina HTML:
<A HREF=”http://www.someplace.com/ISAPIApp?Param1=This+
åParameter&Param2=That+Parameter”>Alguns Links</A>
A maior parte das informações de um TISAPIRequest se refere às propriedades. Muitas das funções
usadas para preencher essas propriedades são tornadas públicas pela classe, o que permite acessar os da-
dos diretamente, se houver necessidade. TISAPIRequest contém outras propriedades, além das que estão
sendo aqui apresentadas, mas estas últimas são as que deverão interessá-lo. Todas essas propriedades po-
dem ser usadas no manipulador de evento OnAction, para determinar o tipo de resposta que será oferecida
pela aplicação do servidor da Web. Se quiser incluir informações sobre o endereço do IP do usuário, ou
alterar a resposta, de acordo com o tipo de browser usado pelo cliente, você poderá fazê-lo em seu mani-
pulador de evento OnAction.
Será possível ver a aparência de um TISAPIRequest se executarmos o projeto a seguir em seu servidor
da Web. Crie uma nova aplicação de servidor da Web, carregue o editor de propriedades Actions, dando
um clique duplo sobre a propriedade Actions no Object Inspector, e crie um novo TWebActionItem, definin-
1018
do PathInfo como http. Vá para a página Internet, na Component Palette, e coloque um TPageProducer (dis-
cutido mais adiante neste capítulo) no WebModule. Em seguida, adicione o código mostrado na Listagem
31.2 ao manipulador de evento OnAction para /http.
Add(‘</BODY>’);
Add(‘</HTML>’);
end;
PageProducer1.HTMLDoc := Page;
Response.Content := PageProducer1.Content;
finally
Page.Free;
end;
Handled := True;
end;
DICA
A maioria dos browsers reage de sua própria maneira, de acordo com determinados códigos de status.
Você pode verificar os códigos de status específicos da especificação HTTP no site da Web
http://www.w3.org.
O poder de TISAPIResponse deve-se aos seus métodos. Uma vez que você tenha formatado adequada-
mente sua resposta, use o método SendResponse para forçar sua aplicação da Web a enviar as informações
de TWebResponse de volta para o cliente. Você poderá enviar qualquer tipo de dado de volta para o cliente,
se usar o método SendStream. Além disso, se sua aplicação precisar enviar para o cliente mais do que ape-
nas a resposta fornecida pela própria aplicação, poderá ser usado o método SendRedirect, que será discuti-
do mais adiante, neste capítulo.
TPageProducer
Para manipular um código HTML de maneira objetiva, utilizamos TPageProducer, que utiliza tags HTML
personalizadas, substituindo-as pelo próprio conteúdo. Você cria, durante o projeto ou em runtime, um
modelo HTML contendo tags ignoradas pelo HTML padrão. TPageProducer é capaz de localizar essas
tags, substituindo-as pela informação apropriada. As tags podem conter parâmetros relativos à passagem
de informações. Você pode, até mesmo, substituir uma tag personalizada por um texto contendo, por
sua vez, outras tags personalizadas. Isso permite vincular criadores de página, causando um efeito em ca-
deia, que permite definir uma página da Web dinâmica baseada em diferentes entradas.
Essas tags dinâmicas têm a aparência de tags HTML normais, mas não seguem o padrão HTML e,
portanto, são ignoradas pelo browser do cliente. Essa tag pode ter a seguinte aparência:
<#CustomTag Param1=SomeValue “Param2=Some Value with Spaces”>
A tag deve ser delimitada por um sinal de “menor-que” (<) e outro de “maior-que” (>), e o seu nome
deve se iniciar com um sinal de libra (#). O nome da tag deve ser um identificador válido do Pascal. Os pa-
râmetros com espaços devem estar inteiramente rodeados por aspas. Essas tags personalizadas podem ser
colocadas em qualquer posição, dentro do documento HTML, e até mesmo dentro de outras tags de
HTML.
O Delphi oferece uma certa quantidade de nomes de tags predefinidos. Nenhum desses valores tem
qualquer ação especial associada; em vez disso, são definidos apenas por motivo de conveniência e clare-
za de código. Por exemplo, você não é obrigado a usar a tag tgLink personalizada para um link, embora
isso faça sentido (o que fica claro em seus modelos HTML). Observe que você pode definir todas as suas
tags personalizadas, da maneira que quiser, e todas elas irão se tornar valores tgCustom válidos. A Tabela
31.1 mostra os valores de tag predefinidos.
Custom TgCustom Uma tag definida pelo usuário ou não identificada. Ela pode ser
convertida para qualquer valor definido pelo usuário.
Link TgLink Essa tag pode ser convertida para um valor de âncora, que normalmente
corresponde a um link de hipertexto ou um valor de bookmark
(<A>..</A>).
Image TgImage Essa tag pode ser convertida para uma tag de imagem (<IMG SRC=...>).
Table TgTable Essa tag pode ser substituída por uma tabela HTML (<TABLE>..</TABLE>).
ImageMap TgImageMap Essa tag pode ser substituída por um mapa de imagem. Um mapa de
imagem define links baseados em zonas de ativação (hot zones) dentro
de uma imagem (<MAP>...</MAP>).
Object TgObject Essa tag pode ser substituída por um código que chame um controle
ActiveX.
Embed TgEmbed Essa tag pode ser convertida para uma tag que se refira a um
complemento compatível com o Netscape.
1021
Usar o componente TPageProducer é mais objetivo. É possível atribuir um código HTML ao compo-
nente, seja através da propriedade HTMLDoc ou da HTMLFile. Sempre que a propriedade Content for atribuída
a outra variável (geralmente, a propriedade TISAPIResponse.Content), ela explorará a HTML fornecida, e
chamará o evento OnHTMLTag, sempre que uma tag personalizada for encontrada na HTML. O manipula-
dor de evento OnHTMLTag tem o seguinte formato:
procedure TWebModule1.PageProducer1HTMLTag(Sender: TObject; Tag: TTag;
const TagString: String; TagParams: TStrings; var ReplaceText: String);
begin
end;
O parâmetro Tag contém o tipo de tag encontrado (veja a Tabela 31.1). O parâmetro tagString ob-
tém o valor integral da tag propriamente dita. O parâmetro tagParams está contido em uma lista indexada
de cada parâmetro, incluindo o nome do parâmetro, o sinal de igualdade (=) e o valor propriamente dito.
O parâmetro ReplaceText corresponde a uma variável de string que será preenchida com o novo valor que
substituirá a tag. A tag, que inclui os sinais de menor e maior (< e >), é substituída no código HTML pelo
valor que tenha sido passado nesse parâmetro.
Você pode atribuir um modelo HTML para o TPageProducer de duas maneiras diferentes. Você pode
criar a HTML em runtime como uma string, passando-a para a propriedade HTMLDoc, ou pode atribuir um
arquivo HTML existente à propriedade HTMLFile, o que permite que você construa a HTML rapidamen-
te, ou que use modelos que tenham sido preparados antecipadamente.
Suponha, por exemplo, que você tenha um arquivo HTML chamado MYPAGE.HTM com o seguinte có-
digo HTML embutido:
<HTML>
<HEAD>
<TITLE>Minha Homepage Legal</TITLE>
</HEAD>
<BODY>
Howdy <#Name>! Obrigado por visitar meu site na Web!
</BODY>
</HTML>
1022
Suponha que você tenha usado esse código com o evento OnAction em um WebModule, como a seguir:
procedure TWebModule1.WebModule1WebActionItem1Action(Sender: TObject;
Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
begin
PageProducer1.HTMLFile := ‘MYPAGE.HTM’;
Response.Content := PageProducer1.Content;
end;
A página assim criada poderia ser enviada de volta para o cliente, quando solicitada. Quando a pro-
priedade PageProducer.Content é chamada, ela faz a substituição necessária do texto, para cada tag encon-
trada, chamando o manipulador de evento OnHTMLTag para cada uma delas. Pode haver páginas mais com-
plexas, com numerosas entradas na instrução case, substituindo diferentes tags personalizadas por gran-
des blocos de HTML, links para outras páginas, gráficos, tabelas e outros recursos.
Os objetos de TCustomPageProducer também podem ser vinculados, formando uma cadeia. Você pode
usar dois deles para produzir uma única página. Você pode ter, por exemplo, um modelo básico HTML
que mantenha o código do cabeçalho e rodapé padrão, juntamente com tags personalizadas que definem
alguns valores gerais para a página e o local do corpo principal da página. Passe-os através de um criador
de página, substituindo tags gerais de dados por informações baseadas na identidade do usuário. Substi-
tua a tag do corpo principal por um código personalizado, ou por mais tags, com base nas informações
solicitadas por aquele usuário. O resultado pode ser passado, então, para outro TPageProducer, que pode
substituir aqueles valores de tags específicas pelas informações apropriadas.
TDataSetTableProducer e TQueryTableProducer
Além de documentos HTML normais, o Delphi fornece o TDataSetTableProducer, que permite criar, de
modo fácil e eficiente, tabelas HTML baseadas em um determinado conjunto de dados. TDataSetTablePro-
ducer permite personalizar todas as características da tabela, dentro dos limites definidos pela HTML.
Essa classe pode funcionar, em grande parte, como um TDBGrid, pois você pode formatar células, linhas e
colunas individuais. Você pode acessar dados a partir de quaisquer datasets disponíveis no seu sistema,
sejam locais ou remotos, o que permite construir sites da Web em nível de empresa, que acessam dados a
partir de praticamente qualquer fonte.
TDataSetTableProducer possui um comportamento ligeiramente diferente dos outros controles de
banco de dados, onde os dados são acessados diretamente de um descendente de TDataSet, em vez de atra-
vés de um TDataSource. Ele tem a propriedade DataSet que pode ser definida em tempo de execução para
qualquer descendente de TDataSet encontrado no mesmo TWebModule, ou em tempo de execução, para
qualquer valor criado dinamicamente. Depois da definição da propriedade DataSet, você pode acessar e
configurar o TDataSetTableProducer, para que apresente quaisquer colunas para o determinado conjunto de
dados, conforme a necessidade. A propriedade TableAttributes permite definir as características gerais da
tabela, novamente dentro da especificação HTML.
As propriedades Header e Footer são do tipo TStrings, e permitem adicionar código HTML antes e
depois da tabela propriamente dita. Use essas propriedades em conjunto com sua própria HTML cria-
da dinamicamente, ou com a HTML de um TPageProducer. Por exemplo, se o recurso principal de uma
página é uma tabela, você deve usar as propriedades Header e Footer, para preencher a estrutura básica
da página HTML. Se a tabela não corresponde ao foco principal da página, você deve optar por utilizar
um TTag personalizado em um TPageProducer, a fim de posicionar a tabela no local apropriado. De qual-
quer maneira, você pode usar o TDataSetTableProducer para criar páginas da Web que sejam baseadas em
dados.
A personalização da tabela que está sendo elaborada é feita nas propriedades Columns, RowAttributes e
TableAttributes. A propriedade Columns oculta um editor de componente muito poderoso, que pode ser
usado para definir a maioria dos atributos do componente.
1023
DICA
Dê um clique duplo no próprio componente, ou na propriedade Columns no Object Inspector para chamar
o editor da propriedade Columns.
As propriedades Caption e CaptionAlign determinam a maneira como o rótulo da tabela será mostra-
do. O rótulo é o texto exibido acima ou abaixo da tabela, servindo para explicar o conteúdo da mesma. A
propriedade DataSet (Query no TQueryTableProducer) determina os dados a serem usados na tabela.
TDataSetTableProducer e TQueryTableProducer funcionam de maneira idêntica, embora acessem os dados
de diferentes maneiras. Também têm as mesmas propriedades e são configurados da mesma maneira.
Crie uma tabela que seja o resultado de uma associação simples e use TQueryTableProducer em um exemplo,
para ver como ambos funcionam.
Inicie uma nova aplicação da Web, colocando um TQueryTableProducer na página da Internet do
Component Palette e um TQuery e uma TSession na página Data Access Palette de TWebModule. Defina a pro-
priedade QueryTableProducer1.Query como Query1 e a propriedade Query1.DatabaseName como DBDEMOS. Salve o
projeto como TABLEEX.DPR. Defina, então, a propriedade Query1.SQL como se segue:
SELECT CUSTNO, ORDERNO, COMPANY, AMOUNTPAID, ITEMSTOTAL FROM CUSTOMER,
➥ORDERS WHERE
CUSTOMER.CUSTNO = ORDERS.CUSTNO
AND
ORDERS.AMOUNTPAID < > ORDERS.ITEMSTOTAL
Será produzida uma pequena tabela associada, que obtém todos os clientes da tabela CUSTOMER.DB no
alias padrão DBDemos, que ainda não efetuaram o pagamento referente a todos os seus pedidos. Você pode,
então, construir uma tabela que mostra esses dados e realça os valores devidos. Defina Query1.Active
como True para que os dados sejam exibidos no editor de Columns.
NOTA
Todas as aplicações de servidor da Web que manipulam dados e usam componentes de dados do Delphi
precisam ter uma TSession incluída no WebModule. Aplicações de servidor da Web podem ser acessadas
muitas vezes simultaneamente, e o Delphi executará cada aplicação de servidor do ISAPI ou NSAPI em um
thread separado para cada solicitação. Como resultado, sua aplicação precisará ter sua única sessão
quando estiver se comunicando com o BDE. Na sua aplicação, ter uma TSession com a propriedade Auto-
SessionName definida como True assegura que cada thread possui sua própria sessão, e que não entra em
conflito com outros threads que tentam acessar os mesmos dados. É preciso apenas ter certeza de que exis-
te uma TSession presente em seu projeto – o Delphi cuida do resto.
DICA
Quando você constrói aplicações de extensões da Web, a propriedade TWebApplication.CacheConnections
pode agilizar sua aplicação. Todas as vezes em que um cliente faz uma solicitação de sua aplicação ISAPI
ou NSAPI, um novo thread é gerado, a fim de manipular a solicitação, no processo que cria uma nova ins-
tância do seu TWebModule. Cada thread é executado, geralmente, para uma única conexão, sendo que
TWebModule é destruído quando a conexão é fechada. Se CacheConnections é definido como True, cada thre-
ad é preservado e reutilizado, conforme a necessidade. Novos threads são criados apenas quando um
thread em cache não está disponível, o que melhora o desempenho. O tempo de execução se reduzirá, se
sempre criarmos uma solicitação TWebModule. Entretanto, você tem de tomar muito cuidado, pois TWebModu-
le.OnCreate é chamado uma única vez para cada thread em cache. Quando um thread em cache é finali-
zado, ele permanece no estado em que se encontrava, quando foi completado, o que pode ocasionar pro-
blemas na próxima vez em que o thread for usado, dependendo do que acontecer no evento OnCreate. Se
1024 você depender de OnCreate para inicializar variáveis ou para executar outras ações de inicialização, não
será necessário usar conexões em cache. Em vez disso, use um método adicional que inicialize os dados na
sua aplicação da Web e, então, chame-o no manipulador de evento BeforeDispatch. Dessa maneira, todas
as vezes em que uma solicitação for feita, os dados do seu módulo da Web serão inicializados.
Você pode verificar o número atual de conexões não usadas, em cache, através da propriedade TWebAppli-
cation.InactiveCount. TWebApplication.ActiveCount informa o número de conexões ativas. Essas duas
propriedades podem ajudá-lo a determinar um valor apropriado para TWebApplication.MaxConnections, li-
mitando o número total de conexões que podem ser manipuladas, de cada vez, por TWebModule. Sempre
que ActiveCount exceder MaxConnections, ocorrerá uma exceção.
Dê um clique duplo sobre QueryTableProducer1 para chamar o editor do componente Columns. No can-
to superior esquerdo do editor do componente, você pode definir as propriedades gerais para a tabela,
como um todo. A metade inferior do editor contém um controle HTML que exibe a tabela, de acordo
com a sua configuração atual. O canto superior direito contém uma coleção de itens THTMLTableColumn que
podem ser configurados para determinar os campos do banco de dados que serão incluídos na tabela,
bem como a maneira como esses campos serão exibidos. O Delphi importará os campos de TQuery auto-
maticamente e os adicionará ao editor de campos. Essa aplicação não exibirá o último campo. Portanto,
selecione o campo ItemsTotal e exclua-o. Além disso, selecione o campo AMOUNTPAID e defina a propriedade
BgColor como Lime.
DICA
Pode ser uma boa idéia redimensionar o editor de propriedade Columns, para que sua tabela se ajuste, es-
pecialmente se ela contiver algumas colunas.
No canto superior esquerdo da janela, defina o valor de Border como 1, para que seja possível ver a
borda da tabela no editor de componentes, da forma como ele está construído. Defina o valor CellPadding
como 2, para que exista um pequeno espaço entre a borda e o texto. Se você quiser adicionar uma cor suave
à tabela, defina a propriedade BgColor como Aqua, para que a cor de segundo plano padrão da tabela seja
definida como aqua. Observe que essa é a cor padrão – definir a cor de segundo plano para uma linha ou
coluna fará com que esse valor seja modificado. Além disso, as definições de cor color têm preferência so-
bre as definições de cor Row.
Quando o Delphi cria as colunas de campo para a tabela, atribui, aos cabeçalhos de colunas de
HTML, os nomes dos campos. Entretanto, os nomes de campos de bancos de dados nem sempre oferecem
cabeçalhos de colunas de tabelas atraentes. Você pode, por esse motivo, alterar os valores padrão usando a
propriedade Title, que é uma propriedade composta, sendo Caption uma de suas subpropriedades. Defina
as propriedades Title.Caption das quatro colunas como Cust #, Order #, Company e Amount Owed, respectivamen-
te. Amount Owed não corresponde, precisamente, à quarta coluna representada atualmente, mas você deverá
personalizar a saída dessa coluna, um pouco mais tarde. A propriedade Title também permite personalizar
o alinhamento vertical e horizontal, bem como a cor da célula do cabeçalho da coluna.
NOTA
TTHMLTableColumn, como outras classes relacionadas a tabelas, tem uma propriedade Custom. Essa propriedade
permite inserir um valor de string para um determinado item na tabela. Esse valor será inserido diretamente na
tag da HTML que define um determinado elemento da tabela. Os itens Custom podem incluir modificadores de
célula, linha ou coluna HTML não incluídos nas propriedades da classe ou extensões da HTML registradas. O
Microsoft Internet Explorer inclui um certo número de extensões de formatação de tabela que permitem perso-
nalizar as molduras da tabela. Se você quiser adicionar esses recursos, faça com que a entrada na propriedade
Custom tenha a forma nomeparam=valor. Você pode adicionar vários parâmetros, separados por espaços.
1025
Vimos as propriedades básicas para a tabela, que podem ser definidas durante o projeto. Agora, dis-
cutiremos os eventos associados com TQueryTableProducer que permitem que você personalize a tabela em
runtime. OnCreateContent ocorre antes da geração de qualquer HTML. Ele contém o parâmetro Continue,
um valor booleano que você pode definir. Se sua aplicação puder determinar que, por algum motivo, a
tabela não venha a ser gerada, defina esse parâmetro como False, para que nenhum processamento seja
feito; uma chamada para a propriedade Content retorna uma string vazia. Isso poderia ser usado para, por
exemplo, preparar a consulta, definir a propriedade TQueryTableProducer.MaxRows, ou para qualquer outro
processamento que precise ser feito antes da exibição da tabela.
No exemplo atual, a aplicação deverá avançar registro a registro, em Query, à medida em que a tabe-
la for sendo construída. Para assegurar que, enquanto a tabela estiver sendo construída, a consulta esteja
apontando para o registro apropriado, a aplicação incrementará manualmente o cursor na consulta, to-
das as vezes em que uma nova linha for iniciada. Para fazer isso, a consulta deve se iniciar logo no início,
como é feito por TQueryTableProducer. Por esse motivo, uma chamada para Query1.First no manipulador de
evento OnCreateContent assegura que a consulta e a tabela HTML estejam sincronizadas uma com a outra.
Assim, adicione o seguinte código ao manipulador de evento para QueryTableProducer1.OnCreateContent:
procedure TWebModule1.QueryTableProducer1CreateContent(Sender: TObject;
var Continue: Boolean);
begin
QueryTableProducer1.MaxRows := Query1.RecordCount;
Query1.First;
Continue := True;
end;
O evento OnGetTableCaption permite formatar o rótulo da tabela, se for necessário. Um clique duplo
sobre o evento no Object Inspector produz este manipulador de evento:
procedure TWebModule1.QueryTableProducer1GetTableCaption(Sender: TObject;
var Caption: String; var Alignment: THTMLCaptionAlignment);
begin
end;
O parâmetro Caption é um parâmetro de variável que terá o resultado final de seu rótulo. Você pode
manipular esse parâmetro como desejar, o que inclui adicionar tags HTML relativas ao tamanho, à cor e
à fonte do rótulo da tabela. Use o parâmetro Alignment para determinar se o rótulo está alinhado pela par-
te superior ou inferior da tabela.
Crie um OnGetTableCaption para o exemplo no qual você está trabalhando, dando um clique duplo no
Object Inspector. Insira o seguinte código para formatar o Caption da tabela, para que se destaque um
pouco mais na página (essa alteração não se refletirá na tabela HTML mostrada no editor da propriedade
Columns):
O evento OnFormatCell pode ser usado para alterar a aparência de uma célula individual. Neste exem-
plo, você pode adicionar um código para realçar a célula Amount Owed de qualquer empresa que ainda não
tenha efetuado o pagamento de suas faturas, o que é um pouco mais elaborado do que para as grades nor-
mais, pois TQueryTableProducer oferece apenas valores de string. Entretanto, conforme mencionado anteri-
ormente, você pode usar os parâmetros CellRow e CellColumn para mover o cursor de TQuery à medida em
que a tabela é construída, obtendo os dados apropriados e fazendo cálculos, à medida em que as linhas
são processadas.
1026
O manipulador de evento OnFormatCell passa, para você, as informações a respeito da célula que está
sendo formatada atualmente nos parâmetros CellRow e CellColumn. Ambos são baseados em zero. Os de-
mais parâmetros são parâmetros variáveis, para os quais você pode atribuir valores, dependendo da lógi-
ca da sua aplicação. Você pode ajustar o alinhamento horizontal e vertical dos dados, na célula, utilizan-
do os parâmetros Align e VAlign, podendo também passar parâmetros adicionais Custom para a célula no
parâmetro CustomAttrs. Você pode alterar, é claro, o texto vigente na célula, com o parâmetro CellData.
O parâmetro CellData é do tipo string, o que limita a possibilidade de processá-lo em seu formato
nativo. Se os dados fossem realmente armazenados no banco de dados como um inteiro, você deveria
chamar StrtoInt, a fim de convertê-los de volta para um número útil. O código a seguir ilustra como ob-
ter os valores TField reais para a célula indicada. Talvez versões futuras do Delphi venham a passar o va-
lor de TField pelo manipulador de evento OnFormatCell, em vez de pelo valor da string, ou como um adicio-
nal a esse valor. Adicione o código da Listagem 31.3 ao manipulador de evento OnFormatCell para TQuery-
TableProducer.
Esse código apanha os dados das contas que não foram pagas, subtrai o valor devido do valor pago e
depois realça as contas devedoras. O código também mostra como usar o cursor atual do componente
TQuery para acessar os dados que estão sendo exibidos na tabela HTML.
Adicione, então, as seguintes strings à propriedade TQueryTableProducer.Header:
<HTML>
<HEAD>
<TITLE>Contas inadimplentes</TITLE>
</HEAD>
<BODY>
<CENTER><H2>Big Shot Widgets</H2></CENTER>
<P>
As contas realçadas em vermelho estão com pagamento atrasado:
<P> 1027
Agora, adicione isto à propriedade TQueryTableProducer.Footer:
<P>
<I>As informações aqui apresentadas são sigilosas</I><P>
<B><I>Copyright 1999 by BigShotWidgets</I></B><P>
</BODY>
</HTML>
A tabela será posicionada entre essas duas definições de código HTML, e levará à criação de uma
página completa, quando a propriedade Content de TQueryTableProducer for chamada, no código a seguir.
Finalmente, volte para o TWebModule principal de sua aplicação e adicione uma única Action, definin-
do sua PathInfo como /TestTable. No seu manipulador de evento OnAction, adicione o seguinte código:
procedure TWebModule1.WebModule1WebActionItem1Action(Sender: TObject;
Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
begin
Response.Content := QueryTableProducer1.Content;
end;
Depois, compile o projeto e certifique-se de que a DLL resultante poderá ser acessada pelo seu ser-
vidor da Web. Se você chamar a URL http://<seu servidor>/tableex.dll/TestTable, verá a tabela com o tex-
to de cabeçalho e rodapé, bem como os valores devidos, realçados em vermelho, conforme mostrado na
Figura 31.2.
O parâmetro Values é um descendente de TStrings (você provavelmente usará uma TStringList) que
obtém os valores atuais da string dos cookies. Você pode passar vários cookies no parâmetro Values.
O parâmetro ADomain permite definir quais os domínios em que os cookies são relevantes. Se não for
passado um valor de domínio, o cookie será passado para qualquer servidor em que tenha sido feita uma
solicitação pelo cliente. Geralmente, uma aplicação da Web define, nesse ponto, seu próprio domínio, de
maneira que apenas os cookies pertinentes sejam retornados. O cliente examinará os valores dos cookies
existentes, retornando os cookies que satisfazem aos critérios indicados.
Se você passar, por exemplo, widgets.com no parâmetro ADomain, todas as futuras solicitações para
widgets.com no servidor também passarão o valor de cookie definido com aquele valor de domínio. O va-
lor do cookie não será passado para outros domínios. Se o cliente solicitar big.widgets.com ou small.wid-
gets.com, o cookie será passado. Apenas hosts dentro do domínio podem definir valores de cookie para
aquele domínio, o que evita qualquer possibilidade de danos.
O parâmetro APath permite que você defina um subconjunto de URLs dentro do domínio no qual o
cookie será válido. O parâmetro APath é um subconjunto do parâmetro ADomain. Se o domínio do servidor
satisfaz o parâmetro ADomain, o parâmetro APath é verificado, segundo a informação de caminho atual do
domínio solicitado. Se o parâmetro APath satisfaz à informação do nome do caminho na solicitação do cli-
ente, o cookie é considerado válido.
Seguindo o exemplo anterior, por exemplo, se APath contiver o valor /nuts, o cookie será válido para
uma solicitação para widgets.com/nuts. O mesmo é válido para quaisquer caminhos adicionais, como wid-
gets.com/nuts/andbolts.
O parâmetro AExpires determina por quanto tempo um cookie permanece válido. Você pode passar
qualquer valor TDateTime nesse parâmetro. O cliente pode estar situado em qualquer lugar no mundo e,
portanto, esse valor deve se basear no horário de Greenwich. Se você quiser que um cookie permaneça
válido por 10 dias, passe Now + 10 como valor.
Se você quiser excluir um cookie, passe como valor uma data passada (ou seja, um valor negativo) o
que invalidará o cookie. Observe que um cookie pode tornar-se inválido, e não será passado, o que não
significa necessariamente que o cookie será realmente removido da máquina do cliente.
O parâmetro final, ASecure, é um valor Booleano que determina se o cookie pode ser transmitido ao
longo de canais sem tratamento de segurança. Um valor True significa que o cookie só pode ser passado
pelo protocolo de segurança do HTTP ou uma rede Secure Sockets Layer. Para utilização normal, esse
parâmetro pode ser definido como False.
1029
Sua aplicação de servidor da Web recebe cookies enviados pelo cliente na propriedade TWebRequest.
CookieFields.Esse parâmetro corresponde a um TStrings descendente que obtém os valores em uma array
indexada. As strings correspondem ao valor completo do cookie na forma parâmetro=valor, podendo ser
acessadas como qualquer outro valor de TStrings. Os cookies também são passados como uma única string
na propriedade TWebRequest.Cookie, embora você possa não querer manipulá-los aqui. É possível atribuir os
cookies diretamente a um objeto TStrings existente, através do método TWebRequest.ExtractCookieFields.
Um exemplo simples ilustra a facilidade com que o Delphi lida com cookies. Primeiramente, crie
uma nova Web Application, adicionando a unidade WebUtils à cláusula uses. A unidade WebUtils está in-
cluída no CD-ROM que acompanha este livro. Crie, então, uma nova aplicação de servidor da Web,
dando-lhe duas ações – uma denominada SetCookie e a outra GetCookie. Defina o código no evento OnAction
de SetCookie da seguinte forma:
procedure TWebModule1.WebModule1WebActionItem1Action(Sender: TObject;
Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
var
List: TStringList;
begin
List := TStringList.Create;
try
List.Add(‘LastVisit=’ + FormatDateTime(‘mm/dd/yyyy hh:mm:ss’, Now));
Response.SetCookieField(List, ‘’, ‘’, Now + 10, False);
Response.Content := ‘Cookie set – ‘ + Response.Cookies[0].Name;
finally
List.Free;
end;
Handled := True;
end;
O código de OnAction para GetCookie deverá ser o seguinte:
procedure TWebModule1.WebModule1WebActionItem2Action(Sender: TObject;
Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
var
Params: TParamsList;
begin
Params := TParamsList.Create;
try
Params.AddParameters(Request.CookieFields);
Response.Content := ‘You last set the cookie on ‘ + Params[‘LastVisit’];
finally
Params.Free;
end;
end;
Defina uma página da Web que chame as duas URLs a seguir:
http://<seu servidor>/project1.dll/SetCookie
http://<seu servidor>/project1.dll/GetCookie
NOTA
A classe TParamsList faz parte da unidade WebUtils, incluída no CD-ROM. É uma classe que analisa auto-
maticamente os parâmetros de um TStrings descendente e permite que você os indexe através do nome do
parâmetro. Por exemplo, TWebResponse obtém todos os cookies passados em uma resposta HTTP, posicio-
nando-os na propriedade CookieFields, descendente de TStrings. Os cookies obedecem a forma Cookie-
Name=CookieValue.TParamsList obtém esses valores, analisa-os, e os indexa através do nome do parâmetro.
Assim, o parâmetro anterior pode ser acessado com MyParams[‘CookieName’], que retorna CookieValue.
Você pode usar essa classe ou a propriedade Values que se encontra na classe TStrings, incluída no VCL.
1030
Defina o cookie chamando a primeira URL de uma página da Web, no mesmo diretório que a DLL,
para definir um cookie na máquina do cliente que dure por 10 dias e contenha a data e hora em que a so-
licitação foi feita, em um cookie denominado LastVisit. Se o seu browser da web estiver definido para
aceitar cookies, ele poderá solicitar uma confirmação da gravação do cookie. Chame, então, a ação Get-
Cookie para ler o cookie, para ver a data e a hora em que a ação SetCookie foi chamada pela última vez.
Os cookies podem conter qualquer informação que possa ser armazenada em uma string. Os coo-
kies podem ter um tamanho de até, por exemplo, 4KB, e um cliente pode armazenar até cerca de 300
cookies. Qualquer servidor individual, ou domínio, limita-se a 20 cookies. Os cookies são poderosos,
mas evite abusar deles. Evidentemente, eles não podem ser usados para armazenar grandes quantidades
de dados na máquina de um cliente.
Muitas vezes, você pode querer armazenar mais informações sobre um usuário do que o possível,
em um cookie. Algumas vezes, você desejará monitorar as preferências do usuário, como endereço, in-
formações pessoais ou itens de evento em um carrinho de compras, que deverão ser adquiridos do seu
site de e-commerce. Essas informações podem ficar bastante volumosas. Em vez de tentar armazenar
essas informações no próprio cookie, de preferência codifique as informações do usuário em um coo-
kie, em vez de armazenar as informações conforme se apresentam. Por exemplo, se quiser armazenar
um conjunto de preferências de usuários que correspondem a valores Booleanos, você deve armaze-
ná-lo em um formato binário dentro do cookie. Por este motivo, um valor ‘1001’ pode significar que o
usuário não deseja atualizações de e-mail adicionais, nem que seu endereço de e-mail seja dado a ou-
tros usuários, ou que seja adicionado ao seu servidor de lista, mas deseja se associar a seus grupos de
discussão on-line. Pode-se usar caracteres de números em um cookie para codificar até mesmo dados
relacionados a um usuário.
Também é possível armazenar um valor de identificação de usuário em um cookie que identifique
um usuário exclusivamente. É possível recuperar o valor do cookie, bem como usá-lo para pesquisar os
dados do usuário em um banco de dados. Dessa maneira, é possível minimizar a quantidade de dados ar-
mazenados no computador do usuário, otimizando seu controle sobre as informações que você pretende
manter a respeito de um usuário.
Os cookies oferecem uma forma poderosa e fácil de fazer a manutenção dos dados para os usuários
entre sessões individuais de HTTP.
Dada essa informação, um evento OnAction em uma aplicação de servidor da Web denominada
/transfer tem a aparência do seguinte fragmento de código:
Através dessa técnica, você pode criar uma aplicação de transferência genérica que processe todos
os anúncios em um site. É claro que pode haver outros motivos, além do anúncios, para chamar SendRedi-
rect. Você pode usar SendRedirect sempre que quiser monitorar solicitações específicas de URL e quais-
quer dados que possam estar associados com um hyperlink em particular. Obtenha simplesmente os da-
dos da propriedade QueryFields e depois chame SendRedirect, se houver necessidade.
Esse código leva o formulário a “postar” seus dados, quando solicitado a fazê-lo, e chama o evento
OnAction da DLL especificada. O formulário permite que o usuário insira seu nome, o endereço de e-mail,
a cidade e os comentários. Quando o usuário dá um clique no botão Submit, essa informação é obtida e
passada para a aplicação da Web.
A ação com o nome /form recebe então os seus dados, em Request.ContentFields, na forma de parâme-
tros HTTP padrão. ContentFields é um descendente de TStrings que extrai seu conteúdo do formulário
submetido. A aplicação contém uma TTable denominada GBTable à qual é feita uma referência pelo alias
GBDATA. É preciso criar esse alias, posicionando-o no diretório /GBDATA em que residem as tabelas do Para-
dox, a fim de rodar a aplicação do livro de convidados. A Listagem 31.4 mostra o código que recebe o
conteúdo do formulário e o insere no banco de dados.
1032
Listagem 31.4 Código para a recuperação do conteúdo de um formulário
var
MyPage: TStringList;
ParamsList: TParamsList;
begin
begin
ParamsList := TParamsList.Create;
try try
ParamsList.AddParameters(Request.ContentFields);
GBTable.Open;
GBTable.Append;
GBTable.FieldByName(‘Name’).Value := ParamsList[‘fullnameText’];
GBTable.FieldByName(‘EMail’).Value := ParamsList[‘emailText’];
GBTable.FieldByName(‘WhereFrom’).Value :=
åParamsList[‘wherefromText’];
GBTable.FieldByName(‘Comments’).Value := ParamsList[‘commentsTextArea’];
GBTable.FieldByName(‘FirstTime’).Value :=
å(CompareStr(ParamsList[‘firstVisitCheck’], ‘on’) = 0);
GBTable.FieldByName(‘DateTime’).Value := Now;
GBTable.Post;
except
Response.Content := ‘An Error occurred in processing your data.’;
Handled := True;
end;
finally
ParamsList.Free;
GBTable.Close;
end;
end;
Esse código, em primeiro lugar, insere a propriedade ContentFields em uma TParamsList. Ele abre a
GBTablee insere os dados do formulário nos campos apropriados. O código na Listagem 31.4 é bastante
objetivo.
A parte seguinte do código, mostrada no Listagem 31.5, cria uma resposta HTML de agradecimen-
to pela entrada feita pelo usuário. Ela usa alguns dados do formulário para endereçar o usuário, pelo
nome, além de confirmar o endereço de e-mail do usuário.
MyPage := TStringList.Create;
ParamsList := TParamsList.Create;
try
with MyPage do
begin
Add(‘<HTML>’);
Add(‘<HEAD><TITLE>Página de demonstração de livro de convidado</TITLE></HEAD>’);
Add(‘<BODY>’);
Add(‘<H2>Demonstração de livro de convidado do Delphi</H2><HR>’);
ParamsList.AddParameters(Request.ContentFields);
Add(‘<H3>Olá <FONT COLOR=”RED”>’+ ParamsList[‘fullnameText’]
å+’</FONT> de ‘+ParamsList[‘wherefromText’]+’!</H3><P>’);
Add(‘Obrigado por visitar minha homepage e fazer
åuma entrada em meu livro de convidados.<P>’);
1033
Listagem 31.5 Continuação
Add(‘Se precisarmos mandar um e-mail para você, nós usaremos este endereço – <B>’
å+ParamsList[‘emailText’]+’</B>’);
Add(‘<HR></BODY>’);
Add(‘</HTML>’);
end;
PageProducer1.HtmlDoc := MyPage;
finally
MyPage.Free;
ParamsList.Free;
end;
Response.Content := PageProducer1.Content;
Handled := True;
Streaming de dados
A maioria dos dados que você oferece para o cliente através de solicitações de HTTP consiste, provavel-
mente, em páginas baseadas em HTML. Porém, haverá uma hora em que será preciso enviar outros tipos
de dados em resposta a uma solicitação do usuário. Algumas vezes, você pode querer que diferentes grá-
ficos ou sons sejam baseados em uma entrada do usuário. É possível ter um formato de dados especial,
que possa enviar o pipe para um usuário, para que seja manipulado pelo browser do cliente. O Netscape,
por exemplo, oferece uma arquitetura de plug-in que permite que os programadores gravem extensões
para o browser Navigator, para manipular quaisquer tipos de dados. O RealAudio, o Shockwave e outros
tipos de streamings de dados são exemplos de plug-ins do Netscape, que podem ampliar o poder do
browser do cliente.
Qualquer que seja o tipo dos dados a serem transmitidos, o Delphi facilita o envio de um streaming
de dados de volta para um cliente. O método TWebResponse.SendStream, em conjunto com a propriedade
TWebResponse.ContentStream, permite enviar qualquer tipo de dados de volta para o cliente, ao carregá-los
em uma classe de streaming do Delphi. Você deve, é claro, permitir que o browser do cliente saiba quais
tipos de dados estão sendo enviados. Portanto, você também deve definir a propriedade TWebResponse.Con-
tentType. Defina esse valor de string com um tipo MIME adequado, para que o browser manipule apro-
priadamente os dados que estão chegando. Por exemplo, se você quiser fazer um streaming para um ar-
quivo WAV do Windows, defina a propriedade ContentType como ‘audio/wav’.
NOTA
O termo MIME é a forma abreviada para Multipurpose Internet Mail Extensions. As extensões MIME foram
desenvolvidas para permitir que os clientes e servidores passem dados, por e-mail, mais sofisticados do que
o texto padrão geralmente passado pela maioria dos e-mails. Os browsers, bem como o protocolo HTTP,
contêm extensões MIME adaptadas, permitindo a passagem de praticamente qualquer tipo de dados de
um servidor da Web para um browser da Web. Seu browser da Web contém uma farta lista dos tipos MIME,
e associa uma aplicação em particular, ou plug-in, com cada tipo MIME. Quando o browser obtém o tipo,
faz uma pesquisa para descobrir quais aplicações podem ser usadas para manipular aquele tipo em parti-
cular de MIME, passando então os seus dados.
Os streamings permitem passar qualquer tipo de dados, de praticamente qualquer origem, na má-
quina do servidor da Web. Você pode passar dados usando arquivos que residem em seu servidor ou em
1034
qualquer local de sua rede, a partir de recursos do Windows incluídos na DLL ISAPI, ou outras DLLs
disponíveis em sua DLL ISAPI, ou até mesmo construir os dados com rapidez, enviando-os para o clien-
te. Não existe limite quanto à maneira e à quantidade do que pode ser enviado, uma vez que o browser
do cliente saiba como tratar dos dados.
Agora, vamos construir uma aplicação simples da Web, ilustrando o que pode ser feito. Você defini-
rá uma página da Web exibindo imagens de várias origens. A aplicação processará os dados da imagem,
se for preciso, retornando-os para o cliente, caso seja solicitado. Isso é surpreendentemente fácil, pois o
Delphi oferece inúmeras classes de streaming, o que facilita bastante a obtenção de dados em um strea-
ming. As classes da extensão ISAPI também agilizam o envio daqueles dados.
Para construir o exemplo de streaming de dados, selecione File, New no menu principal e escolha
Web Server Application na caixa de diálogo resultante. Isto lhe dará um TWebModule. Vá para o módulo da
Web, selecione-o e, em seguida, vá para o Object Inspector. Dê um clique duplo na propriedade Actions e
crie três ações denominadas /file, /bitmap e /resource.
Selecione a ação /file, vá para o Object Inspector e selecione a página Events. Crie um evento
OnAction e, em seguida, adicione o seguinte código ao manipulador de evento:
O código acima é bastante objetivo. Se você preparar o código descrito anteriormente no seu compu-
tador, tomando como base o CD-ROM, deverá haver um arquivo JPEG, denominado TESTIMG.JPG, no dire-
tório \bin. O manipulador de evento OnAction cria um TFileStream que carrega aquele arquivo. Ele define, en-
tão, o tipo MIME adequado, para informar ao browser do cliente que um arquivo JPEG está a caminho,
atribuindo o TFileStream para a propriedade Response.ContentStream. Depois, os dados são retornados para o
cliente, através da chamada do método Response.SendResponse. Como resultado, no arquivo HTML que o
acompanha, deverá haver uma figura representando uma rosa, na página HTML fornecida.
NOTA
No HTML que exibe esse arquivo JPEG no seu browser, você pode simplesmente colocar a referência à pro-
priedade Action da aplicação da Web diretamente na tag IMG, desta forma:
Os exemplos de streaming podem ser exibidos por meio da página INDEX.HTM no diretório \STREAMS
A aplicação poderá agora encontrar o arquivo JPEG, pois quando ele foi criado, definiu a variável
JPEGFilenamedesta maneira:
procedure TWebModule1.WebModule1Create(Sender: TObject);
var 1035
Path: array[0..MAX_PATH - 1] of Char;
PathStr: string;
begin
SetString(PathStr, Path, GetModuleFileName(HInstance, Path, SizeOf(Path)));
JPEGFilename := ExtractFilePath(PathStr) + ‘TESTIMG.JPG’;
end;
A ação /bitmap carregará uma imagem diferente, embora de uma maneira totalmente diferente. O
código para essa ação é um pouco mais complicado, e tem a seguinte aparência:
procedure TWebModule1.WebModule1WebActionItem3Action(Sender: TObject;
Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
var
BM: TBitmap;
JPEGImage: TJPEGImage;
begin
BM := TBitmap.Create;
JPEGImage := TJPEGImage.Create;
try
BM.Handle := LoadBitmap(hInstance, ‘ATHENA’);
JPEGImage.Assign(BM);
Response.ContentStream := TMemoryStream.Create;
JPEGImage.SaveToStream(Response.ContentStream);
Response.ContentStream.Position := 0;
Response.SendResponse;
Handled := True;
finally
BM.Free;
JPEGImage.Free;
end;
end;
É um pouco mais trabalhoso obter um mapa de bits convertido para um JPEG e que tenha sido en-
viado para o cliente em um streaming. Um TBitmap é usado para obter o mapa de bits, independentemente
do arquivo de recursos. É criada uma TJPEGImage, na unidade JPEG, e o mapa de bits é convertido para um
arquivo JPEG.
A classe TBitmap é criada e, em seguida, a API do Windows chama LoadBitmap, usado para obter o
mapa de bits a partir do recurso denominado ‘ATHENA’. LoadBitmap retorna o manipulador do mapa de bits,
atribuído à propriedade Handle. O mapa de bits, propriamente dito, é atribuído imediatamente a TJPEGIma-
ge. O método Assign recebe overload, e é responsável pela conversão do bitmap para um JPEG.
A seguir vemos um bom exemplo de polimorfismo. Response.ContentStream é declarado como TStream,
uma classe abstrata. Devido às características do polimorfismo, você pode criá-la com qualquer tipo des-
cendente de TStream que desejar. Neste caso, ela é criada como um TMemoryStream, sendo usada para obter o
JPEG através do método TJPEGImage.SaveToStream. Agora, o JPEG já está em um streaming, podendo ser
enviado. Um passo importante, embora fácil de esquecer, consiste em retornar a posição do streaming
para zero, depois de salvar o JPEG. Se essa medida não for tomada, o streaming será posicionado no fi-
nal, e não haverá um streaming de dados para o cliente. Depois disso tudo, o método Response.SendRespon-
se será chamado para enviar os dados armazenados no streaming. O resultado, neste caso, é a rajada de
Athena da caixa de diálogo About do Delphi.
Outra maneira de carregar um JPEG consiste em usar uma entrada de recurso. Você pode carregar
um JPEG em um arquivo RES usando o código a seguir em um arquivo RC e, depois, compilando-o com
BRCC32.EXE. Se você carregá-lo como RCDATA, poderá usar a classe TResourceStream para carregá-lo facilmente e
enviá-lo para o browser do cliente. TResourceStream é uma classe muito poderosa, que carregará um recurso
do próprio arquivo EXE ou um recurso localizado em um arquivo DLL externo. A ação /resource ilustra
como fazê-lo, carregando o JPEG a partir do recurso denominado ‘JPEG’ que é compilado para o EXE:
1036
procedure TWebModule1.WebModule1WebActionItem4Action(Sender: TObject;
Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
begin
Response.ContentStream := TResourceStream.Create(hInstance,
å’JPEG’, RT_RCDATA);
Response.ContentType := ‘image/jpeg’;
Response.SendResponse;
Handled := True;
end;
Esse código envia os dados para o cliente de uma maneira um pouco diferente. Ele é muito mais ob-
jetivo e corresponde, mais uma vez, a um bom exemplo de polimorfismo. Um TResourceStream é criado e
atribuído à propriedade ContentStream. Como o construtor do TResourceStream carrega o recurso no stream,
nenhuma outra ação será necessária, e uma simples chamada a Response.SendResponse enviará os dados flu-
xo abaixo.
O exemplo final realiza o streaming de um arquivo WAV, armazenado como um recurso RCDATA.
Este exemplo utiliza o método Response.SendStream para enviar um stream criado com este método. Isso
mostra outra maneira de enviar dados de stream. Você pode criar um stream, manipulá-lo e modificá-lo,
conforme a necessidade, enviando-o diretamente de volta para o cliente por meio do método SendStream.
Essa ação fará com que o seu browser execute um arquivo WAV contendo o som de um cão latindo. Veja
o código:
procedure TWebModule1.WebModule1WebActionItem1Action(Sender: TObject;
Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
var
RS: TResourceStream;
begin
RS := TResourceStream.Create(hInstance, ‘BARK’, RT_RCDATA);
try
Response.ContentType := ‘audio/wav’;
Response.SendStream(RS);
Handled := True;
finally
RS.Free;
end;
end;
Resumo
Este capítulo mostrou como construir extensões de servidores da Web através das extensões ISAPI ou
NSAPI. Essas informações são facilmente transportadas para as aplicações CGI produzidas pelo Delphi.
Discutimos sobre o protocolo HTTP e a maneira como o Delphi o encapsula em suas classes TWebRequest e
TWebResponse. Mostramos, ainda, como construir aplicações usando o TWebModule e seus eventos OnAction,
através da HTML dinâmica. Ilustramos, ainda, documentos HTML personalizados com os descendentes
de TContentPageProducer, bem como o acesso aos dados e a construção de tabelas HTML usando o TQueryTa-
bleProducer. Mostramos também como manipular cookies e o conteúdo dos formulários HTML. Por fim,
mostramos como enviar um stream personalizado para o cliente. No próximo capítulo, voltaremos a
uma abordagem voltada para os bancos de dados, quando estudarmos a tecnologia MIDAS em multica-
madas.
1037
Desenvolvimento CAPÍTULO
MIDAS
POR DAN MISER
32
NE STE C AP ÍT UL O
l Mecânica da criação de uma aplicação em
multicamadas 1039
l Benefícios da arquitetura em multicamadas 1040
l Arquitetura MIDAS típica 1041
l Uso do MIDAS para criar uma aplicação 1045
l Outras opções para fortalecer sua aplicação 1051
l Exemplos do mundo real 1055
l Mais recursos de dataset do cliente 1064
l Distribuição de aplicações MIDAS 1072
l Resumo 1122
Atualmente, fala-se em aplicações em multicamadas como qualquer outro tópico em programação de
computador. Isso está acontecendo por um bom motivo. As aplicações em multicamadas possuem muitas
vantagens em relação às aplicações cliente/servidor mais tradicionais. O Multitier Distributed Applica-
tion Services Suite (MIDAS) da Borland é uma forma de ajudá-lo a criar e oferecer aplicações em multica-
madas usando o Delphi, baseando-se nas técnicas e habilidades que você já vem adquirindo com o uso do
Delphi. Este capítulo dará algumas informações gerais sobre o projeto de aplicações em multicamadas e
mostrará como aplicar esses princípios para criar aplicações MIDAS sólidas.
SGBD
Cliente
Nas aplicações em multicamadas, a arquitetura se parece mais com a da Figura 32.2. Usando esta ar-
quitetura, você encontrará muitos benefícios em relação à aplicação cliente/servidor equivalente.
BDE, ADO e outros
SGBD
Cliente Servidor
IAppServer IAppServer
MIDAS.DLL MIDAS.DLL
Tolerância a falhas
Se a máquina do seu servidor não estiver disponível devido a circunstâncias imprevistas, seria bom passar
dinamicamente para um servidor de reserva sem ter que recompilar suas aplicações cliente ou servidor.
Originalmente, o Delphi oferece funcionalidade para isso.
1040
Equilíbrio de carga
Ao distribuir sua aplicação cliente para mais pessoas, você inevitavelmente começará a saturar a largura
de banda do seu servidor. Há duas maneiras de tentar equilibrar o tráfego na rede: equilíbrio de carga es-
tático e dinâmico. Para o equilíbrio de carga estático, você incluiria outra máquina no servidor e faria
com que metade de seus clientes usassem o servidor A e a outra metade acessasse o servidor B. No entan-
to, e se os clientes que usam o servidor A exigissem muito mais do seu servidor do que os que usam o ser-
vidor B? Usando o equilíbrio de carga dinâmico, você poderia resolver esse problema, dizendo a cada
aplicação cliente qual servidor ela deverá acessar. Existem muitos algoritmos diferentes de equilíbrio de
carga dinâmico, como aleatório, seqüencial, menor número de usuários na rede e menor tráfego na rede.
O Delphi 4 em diante resolve isso fornecendo um componente para implementar o equilíbrio de carga
seqüencial.
Erros clássicos
O erro mais comum na criação de uma aplicação em multicamadas é introduzir conhecimento desneces-
sário da camada de dados na camada de apresentação. Algumas validações são mais adequadas na cama-
da de apresentação, mas é o modo como essa validação é realizada que determina sua utilidade em uma
aplicação em multicamadas.
Por exemplo, se você estiver passando instruções SQL dinâmicas do cliente para o servidor, isso
gera uma dependência de que a aplicação cliente sempre esteja sincronizada com a camada de dados. Ao
fazer as coisas dessa forma, você gera mais partes em movimento, que precisam estar coordenadas na
aplicação em multicamadas de modo geral. Se você mudar a estrutura de uma das tabelas na camada de
dados, terá de atualizar todas as aplicações cliente que enviam SQL dinâmica, para que possam agora en-
viar a instrução SQL correta. Isso certamente limita o benefício acarretado por uma aplicação de cliente
magro desenvolvida corretamente.
Outro exemplo é quando a aplicação cliente tenta controlar o tempo de vida da transação, em vez
de permitir que a camada comercial cuide disso em favor do cliente. Na maior parte do tempo, isso é im-
plementado expondo-se três métodos da instância TDataBase no servidor = BeginTransaction( ), Commit( ) e
Rollback( ) – e chamando-se esses métodos a partir do cliente. Isso torna o código do cliente muito mais
complicado de se manter e infringe o princípio de que a camada de apresentação deve ser a única camada
responsável pela comunicação com a camada de dados. A camada de apresentação nunca deve se basear
em tal método. Em vez disso, você precisa enviar suas atualizações para a camada comercial e deixar que
essa camada lide com a atualização dos dados em uma transação.
Servidor
Agora que você já viu como é montada uma aplicação MIDAS típica, vejamos como fazer isso acontecer
no Delphi. Vamos começar verificando algumas das opções disponíveis na configuração do servidor.
1041
Formulário/Módulo de dados Remote Data Module (RDM)
TClientDataset
TDatasetProvider TDataset
TDispatchConnection
Cliente Servidor
Opções de instanciação
A especificação de uma opção de instanciação afeta o número de cópias do processo servidor que serão
iniciadas. A Figura 32.5 mostra como as opções feitas aqui controlam o comportamento do seu servidor.
Cliente 1 Thread 1
Cliente 3 Thread 3
Threading de apartamentos
Opções de threading
O suporte para threading no Delphi 5 mudou drasticamente para melhor. No Delphi 5, a seleção do mo-
delo de threading para um servidor EXE não tinha significado. O flag simplesmente marcava o Registro
para dizer ao COM que a DLL era capaz de ser executada sob o modelo de threading selecionado. Com o
Delphi 5, a opção de modelo de threading agora se aplica a servidores EXE, permitindo que o COM co-
loque as conexões em threads sem usar qualquer código externo. A seguir vemos um resumo das opções
de threading disponíveis para um RDM:
l Single. A seleção de Single significa que o servidor só pode tratar de um pedido de cada vez. Quan-
do estiver usando Single, você não precisa se preocupar com aspectos de threading, pois o servi-
dor roda em um thread e o COM trata dos detalhes do sincronismo das mensagens para você.
No entanto, esta é a pior seleção que você pode fazer se pretende ter um sistema multiusuário,
pois o cliente B teria de esperar até o cliente A terminar seu processamento antes que possa co-
meçar a trabalhar. Essa, obviamente, não é uma boa situação, pois o cliente A poderia estar reali-
zando um relatório de resumo do fim do dia ou alguma outra operação demorada.
l Apartment. A seleção do modelo de threading Apartment oferece o melhor de todos os mun-
dos possíveis quando combinada com a instanciação ciMultiInstance. Nesse cenário, todos os
cliente compartilham um processo servidor, por causa de ciMultiInstance, mas o trabalho feito
no servidor a partir de um clique não impede que outro cliente realize o trabalho, devido à op-
ção de threading Apartment. Ao usar o threading Apartment, você tem garantias de que os da-
dos da instância do seu RDM estão seguros, mas precisa proteger o acesso às variáveis globias
usando alguma técnica de sincronismo de thread, como PostMessage( ), seções críticas, mute-
xes, semáforos ou a classe wrapper TMultiReadExclusiveWriteSynchronizer do Delphi. Esse é o mo-
delo de threading preferido para datastes do BDE. Observe que, se você usar esse modelo de
threading com datasets do BDE, terá de colocar um componente TSession no seu RDM e definir
a propriedade AutoSessionName como True para ajudar o BDE a se adequar aos requisitos internos
para o threading.
l Free. Esse modelo oferece ainda mais flexibilidade no processamento do servidor, permitindo
que várias chamadas sejam feitas a partir do cliente para o servidor simultaneamente. No entan-
to, junto com esse pode vem a responsabilidade. Você precisa cuidar da proteção de todos os da-
dos contra conflitos de thread – tanto dados de instância quanto variáveis globais. Esse é modelo
de threading preferido quando se usa Microsoft Active Data Objects (ADO).
l Both. Essa opção é efetivamente a mesma da opção Free, com uma exceção – os callback são au-
tomaticamente colocados em série.
Cliente
Depois de montar o servidor, precisamos criar um cliente para usar os serviços fornecidos pelo servidor.
Vejamos algumas das opções disponíveis na montagem do seu cliente MIDAS.
Opções de conexão
A arquitetura do Delphi para a conexão do cliente ao servidor começa com TDispatchConnection. Esse obje-
to de base é o pai de todos os tipos de conexão listados mais adiante. Quando o tipo de conexão é irrele-
vante para a seção específica, TDispatchConnection é usado para indicar esse fato.
TDCOMConnection oferece a segurança e a autenticação básicas usando a implementação padrão do
Windows para esses serviços. Esse tipo de conexão é útil especialmente se você estiver usando esta apli-
cação em um esquema de intranet/extranet (ou seja, onde as pessoas que usam sua aplicação são “conhe-
cidas” do ponto de vista do domínio). Você pode usar a vinculação inicial (early binding) ao usar
DCOM, e pode usar callbacks e ConnectionPoints com facilidade (você também pode usar callbacks quan-
do usar soquetes, mas está limitado a usar a vinculação inicial para fazer isso). As desvantagens do uso
dessa conexão são:
l Configuração difícil em muitos casos
l Não é um tipo de conexão que facilita o uso de firewall
l Exige a instalação do DCOM95 para máquinas Windows 95
TSocketConnection é a conexão mais fácil de se configurar. Além disso, ela só usa uma parta para o trá-
fego do MIDAS, de modo que seus administradores de firewall ficarão mais felizes do que se tivessem de
fazer o trabalho do DCOM através do firewall. Você precisa estar rodando o ScktSrvr (encontrado no di-
retório <DELPHI>\BIN) para que essa configuração funcione, de modo que existe um arquivo extra a ser dis-
tribuído e executado no servidor. O Delphi 4 também exigia que você instalasse o WinSock2, o que sig-
nificava outra instalação para os clientes Windows 9x. No entanto, se você estiver usando o Delphi 5 e
não estiver usando callbacks, poderá considerar a definição de TSocketConnection.SupportCallbacks como
False. Isso permite ficar com o WinSock 1 nas máquinas cliente.
TOLEnterpriseConnection oferece suporte interno para tolerância a falhas e equilíbrio de carga. Ele
também facilita o uso de uma máquina Windows 9x como servidor. O Delphi 4 introduziu um compo-
nente que permite a tolerância a falhas e equilíbrio de carga simples (TSimpleObjectBroker), e agora sabe
como usar o Windows 9x como servidor. Além do mais, o trabalho de instalação é muito grande.
A partir do Delphi 4, você também pode usar TCORBAConnection. Ele é o equivalente padrão de abertu-
ra do DCOM. Você acabará usando CORBA ao migrar suas aplicações MIDAS para permitir as cone-
xões entre plataformas. Por exemplo, o cliente Java para MIDAS (disponível separadamente na Borland)
permite que um cliente JBuilder fale com um servidor MIDAS – mesmo que tenha sido criado com o
Delphi.
1044
O componente TWebConnection é novo no Delphi 5. Esse componente de conexão permite que o trá-
fego do MIDAS seja transportado por http ou HTTPS. Mas existem algumas limitações para o uso desse
tipo de conexão:
l Callbacks de qualquer tipo não são aceitos.
l O cliente precisa ter instalado a WININET.DLL.
l A máquina do servidor precisa estar rodando o MS Internet Information Server (IIS) 4.0 ou o
Netscape 3.6 ou mais recente.
No entanto, essas limitações parecem valer a penas quando você tiver de oferecer uma aplicação
pela Internet ou por um firewall que não esteja sob o seu controle.
Observe que todos esses transportes consideram uma instalação válida do TCP/IP. A única exceção
a isso é se você estiver usando duas máquinas Windows NT para se comunicar via DCOM. Nesse caso,
você pode especificar qual protocolo o DCOM usará rodando DCOMCNFG e passando o protocolo de-
sejado para o topo da lista na guia Default Protocols (protocolos default). O DCOM para Windows 9x só
trabalha com TCP/IP.
Montando o servidor
Primeiro vamos focalizar nossa atenção na mecânica de montagem do servidor de aplicação. Depois de
criarmos o servidor, vamos explicar como montar o cliente.
Esse método é chamado sempre que o servidor é registrado ou perde o registro. Além dos itens do
Registro específicos do COM que são criados na chamada ao UpdateRegistry herdado, você pode chamar
os métodos EnableXXXTransport e DisableXXXTransport para marcar esse objeto como protegido.
NOTA
A versão Delphi 5 do componente TSocketConnection só mostrará objetos registrados e protegidos na pro-
priedade ServerName. Se você não quiser impor segurança alguma, desmarque a opção de menu Connec-
tions, Registered Objects Only (apenas objetos registrados) no SCKTSRVR.
Provedores
Visto que o servidor de aplicação será responsável por fornecer dados ao cliente, você terá de encontrar
um modo de fornecer dados do servidor em um formato adequado ao cliente. Felizmente, o MIDAS ofe-
rece um componente TDatasetProvider para facilitar essa etapa.
Comece incluindo um TQuery no RDM. Se você estiver usando um SGBDR, inevitavelmente tam-
bém terá que configurar um TDatabase. Por enquanto, ligaremos o TQuery ao TDatabase e especificaremos
uma consulta simples na propriedade SQL, como select * from customer. Por fim, inclua um componente
TDatasetProvider no RDM e vincule-o ao TQuery por meio da propriedade Dataset. A propriedade Exported
no DatasetProvider determina se esse provedor será visível aos clientes. Essa propriedade tem a capacidade
1046 de controlar com facilidade quais provedores também estarão visíveis em runtime.
NOTA
Embora a discussão desta seção gire em torno do uso do TDBDataset baseado no BDE, os mesmos princípi-
os se aplica se você quiser usar qualquer outro descendente de TDataset para o acesso aos seus dados.
Duas dessas possibilidades já estão prontas: ADO e InterBase Express.
Registrando o servidor
Quando o servidor de aplicação estiver montado, ele precisará ser registrado com o COM para torná-lo
disponível para as aplicações do cliente que serão conectada a ele. As entradas do Registro discutidas no
Capítulo 23 também são usadas para servidores MIDAS. Você só precisa rodar a aplicação servidora e a
configuração do Registro será incluída. No entanto, antes de registrar o servidor, não se esqueça de sal-
var o projeto primeiro. Isso garante que o ProgID estará correto desse ponto em diante.
Se você preferir não rodar a aplicação servidora, poderá passar o parâmetro /regserver na linha de
comandos ao rodar a aplicação. Isso simplesmente realizará o processo de registro e terminará imediata-
mente a aplicação. Para remover as entradas do Registro associadas a essa aplicação, você poderá usar o
parâmetro /unregserver.
Criando o cliente
Agora que temos um servidor de aplicação funcionando, vejamos como realizar algumas tarefas bási-
cas com o cliente. Veremos como apanhar os dados, como editá-los, como atualizar o banco de dados
com mudanças feitas no cliente e como tratar de erros durante o processo de atualização do banco de
dados.
Apanhando dados
Durante a execução de uma aplicação de banco de dados, é preciso trazer dados do servidor para o clien-
te a fim de editar esses dados. Trazendo os dados para um cache local, você pode reduzir o tráfego da
rede e minimizar os tempos de transação. Nas versões anteriores do Delphi, você usaria atualizações em
cache para realizar essa tarefa. No entanto, as mesmas etapas gerais ainda se aplicam a aplicações
MIDAS.
O cliente fala com o servidor por meio de um componente TDispatchConnection. Dando ao TDispat-
chConnection o nome do computador onde o servidor de aplicação se encontra, essa tarefa é realizada com
facilidade. Se você usar TDCOMConnection, poderá especificar o nome de domínio totalmente qualificado
(por exemplo, nt.dmiser.com), o endereço IP numérico do computador (por exemplo, 192.168.0.2) ou o
nome do NetBIOS do computador (por exemplo, nt). Entretanto, devido a um bug no DCOM, você não
pode usar o nome localhost de modo confiável em todos os casos. Se você usar TSocketConnection, especifi-
que os endereços IP numéricos na propriedade Address ou o FQDN na propriedade Host. Veremos as op-
ções para TWebConnection um pouco mais adiante.
Quando você especificar onde o servidor de aplicação reside, você terá que dar ao TDispatchConnecti-
on um meio de identificar esse servidor de aplicação. Isso é feito por meio da propriedade ServerName. Ao
atribuir a propriedade ServerName, a propriedade ServerGUID já será preenchida. A propriedade ServerGUID é
a parte mais importante. Na verdade, se você quiser distribuir sua aplicação cliente da forma mais genéri-
ca possível, deverá excluir a propriedade ServerName e só usar ServerGUID.
NOTA
Se você usar TDCOMConnection, a lista de ServerName só mostrará a lista de servidores que estão registrados
na máquina atual. No entanto, TSocketConnection é inteligente o suficiente para mostrar a lista de servido-
res de aplicação registrados na máquina remota.
1047
Neste ponto, a definição de TDispatchConnection.Connected como True o conectará ao servidor de apli-
cação.
Agora que o cliente está falando com o servidor, você precisa de uma maneira de usar o provedor
criado no servidor. Faça isso usando o componente TClientDataset. Um TClientDataSet é usado para o vín-
culo com o provedor (e assim a TQuery que está vinculada ao provedor) no servidor.
Primeiro, você precisa unir TClientDataSet a TDispatchConnection atribuindo a propriedade RemoteSer-
ver de TClientDataSet. Quando tiver feito isso, você poderá apanhar uma lista dos provedores disponíveis
nesse servidor verificando a lista da propriedade ProviderName.
Neste ponto, tudo está preparado corretamente para abrir um ClientDataset.
Visto que TClientDataSet é um descendente virtual de TDataset, você pode utilizar muitas das técnicas
que já aprendeu usando os componentes TDBDataset nas aplicações cliente/servidor. Por exemplo, a defi-
nição de Active como True abre o TClientDataSet e apresenta os dados. A diferença entre isso e definir TTa-
ble.Active como True é que o TClientDataSet, na realidade, está apanhando seus dados do servidor de apli-
cação.
NOTA
TClientDataset provou ser útil de várias maneiras que não foram intencionadas inicialmente. Ele também
serve como um excelente método para armazenar em tabelas da memória, o que não tem nada a ver com
MIDAS especificamente. Além disso, devido ao modo como expõe dados através das propriedades Data e
Delphi, provou ser útil em diversas implementações do padrão OOP. Este capítulo não tem como objetivo
discutir essas técnicas. No entanto, você encontrará alguns trabalhos sobre esses tópicos em
http://www.xapware.com ou http://www.xapware.com/ddg.
Desfazendo alterações
A maioria dos usuários já usou algum aplicação de processamento de textos que permite a operação
“Desfazer”. Essa operação apanha sua ação anterior e a retorna ao estado imediatamente antes de tê-la
1048 iniciado. Usando TClientDataset, você pode chamar cdsCustomer.UndoLastChange( ) para simular esse com-
portamento. A pilha de desfazimento (undo) não tem limites, permitindo que o usuário continue recuan-
do até o início da sessão de edição, se desejar. O parâmetro que você passa para esse método especifica se
o cursor está posicionado no registro sendo afetado.
Se o usuário quiser se livrar de todas as suas atualizações de uma só vez, há um modo mais fácil do
que chamar UndoLastChange( ) repetidamente. Você pode simplesmente chamar cdsCustomer.CancelUpda-
tes( ) para cancelar todas as mudanças que foram feitas em uma única sessão de edição.
ATENÇÃO
Um aviso sobre SavePoint está em ordem. Você pode invalidar um SavePoint chamando UndoLastChange( )
além do ponto em que está salvo atualmente. Por exemplo, considere que o usuário edita dois registros e
emite um SavePoint. Nesse momento, o usuário editar outro registro. No entanto, ele usa UndoLastChan-
ge( ) para reverter as mudanças duas vezes em seqüência. Como o TClientDataset está agora em um es-
tado anterior ao SavePoint, o SavePoint fica em um estado indefinido.
Reconciliando dados
Depois que você tiver acabado de fazer as mudanças na cópia local dos dados em TClientDataset, terá de
sinalizar sua intenção de aplicar essas mudanças de volta ao banco de dados. Isso é feito chamando-se
cdsCustomer.ApplyUpdates( ). Nesse ponto, o MIDAS apanhará o Delta de cdsCustomer e o passará ao servi-
dor de aplicação, onde aplicará essas mudanças ao servidor de banco de dados usando o mecanismo de
reconciliação que você escolheu para esse dataset. Todas as atualizações são realizadas dentro do contex-
to de uma transação. Em breve, veremos como os erros são tratados durante esse processo.
O parâmetro que você passa para ApplyUpdates( ) especifica o número de erros que o processo de
atualização permitirá antes de considerar a atualização como ruim e subseqüentemente desfazer todas as
mudanças que foram feitas. A palavra erros aqui refere-se a erros de violação de chave, erros de integrida-
de referencial ou quaisquer outros erros de banco de dados. Se você especificar zero para esse parâmetro,
estará dizendo ao MIDAS que não tolerará quaisquer erros. Portanto, se ocorrer um erro, todas as mu-
danças feitas não serão submetidas ao banco de dados. Essa é a configuração que você usará com mais
freqüência, pois se ajusta melhor às sólidas orientações e princípios de uso dos bancos de dados.
Entretanto, se você desejar, poderá especificar que um certo número de erros pode ocorrer, en-
quanto ainda submete todos os registros que tiveram sucesso. A extensão máxima desse conceito é passar
–1 como parâmetro para ApplyUpdates( ). Isso diz ao MIDAS que ele deve submeter cada registro que pu-
der, independente do número de erros encontrados no caminho. Em outras palavras, a transação sempre
será submetida quando esse parâmetro for usado.
1049
Se você quiser ter o máximo de controle sobre o processo de atualização – incluindo alterar a SQL
que será executada para uma operação de inserção, atualização ou exclusão –, poderá fazê-lo no evento
TDatasetProvider.BeforeUpdateRecord( ). Por exemplo, quando um usuário deseja deletar um registro, você
pode não querer realmente realizar uma operação de exclusão no banco de dados. Em vez disso, um flag
será definido para dizer às aplicações que esse registro não está mais disponível. Mais adiante, um admi-
nistrador poderá revisar essas exclusões e submeter a operação física de exclusão. O exemplo a seguir
mostra como fazer isso:
procedure TDataModule1.Provider1BeforeUpdateRecord(Sender: TObject;
SourceDS: TDataset; DeltaDS: TClientDataset; UpdateKind: TUpdateKind;
var Applied: Boolean);
begin
if UpdateKind=ukDelete then
begin
Query1.SQL.Text:=’update CUSTOMER set STATUS=”DEL” where ID=:ID’;
Query1.Params[0].Value:=SourceDS.FieldByName(‘ID’).Value;
Query1.ExecSQL;
Applied:=true;
end;
end;
Você pode criar quantas consultas quiser, controlando o fluxo e o conteúdo do processo de atuali-
zação com base em diferentes fatores, como UpdateKind e valores no Dataset. Ao inspecionar ou modificar
registros do DeltaDS, não se esqueça de usar as propriedades OldValue e NewValue do TField apropriado. O
uso de TField.Value ou TField.AsXXX gerará resultados imprevisíveis.
Além disso, você pode impor regras comerciais aqui para evitar completamente a postagem de um
registro no banco de dados. Qualquer exceção que você gere aqui acabará no mecanismo de tratamento
de erros do MIDAS, que veremos em seguida.
Quando a transação terminar, você terá uma oportunidade para tratar dos erros. O erro pára em
eventos no servidor e no cliente, dando-lhe uma chance de tomar alguma ação corretiva, registrar o erro
ou fazer algo mais que você queira com ele.
A primeira parada para o erro é o evento DatasetProvider.OnUpdateError. Esse é um excelente lugar
para tratar de erros que você esteja esperando ou que possam ser resolvidos sem qualquer intervenção do
cliente.
O destino final para o erro é de volta ao cliente, onde você poderá lidar com o erro, permitindo que
o usuário ajude a determinar o que fazer com o registro. Você pode fazer isso atribuindo um manipula-
dor para o evento TClientDataset.OnReconcileError.
Isso é útil especialmente porque o MIDAS é baseado em uma estratégia de bloqueio de registro oti-
mista. Essa estratégia permite que vários usuários trabalhem com o mesmo registro ao mesmo tempo. Em
geral, isso causará conflitos quando o MIDAS tentar reconciliar os dados no banco de dados porque o re-
gistro foi modificado desde que foi apanhado. Trataremos de algumas alternativas para esse processo de
identificação default mais adiante.
1050
FIGURA 32.6 A caixa de diálogo Reconcile Error em ação.
A principal funcionalidade dessa unidade está contida na função HandleReconcileError( ). Existe uma
relação forte entre o evento OnReconcileError e a função HandleReconcileError. De fato, a ação típica no
evento OnReconcileError é chamar a função HandleReconcileError. Fazendo isso, a aplicação permite que o
usuário final na máquina do cliente interaja com o processo de reconciliação de erro na máquina servido-
ra e especifique como esses erros devem ser tratados. Aqui está o código:
procedure TMyForm.CDSReconcileError(Dataset: TClientDataset;
E: EReconcileError; UpdateKind: TUpdateKind;
var Action: TReconcileAction);
begin
Action:=HandleReconcileError(Dataset, UpdateKind, E);
end;
O valor do parâmetro Action determina o que o MIDAS fará com esse registro. Um pouco mais adi-
ante, focalizaremos alguns outros fatores que afetam quais seções são válidas nesse ponto. A lista a seguir
mostra as ações válidas:
l raSkip. Não atualize esse registro específico do banco de dados. Deixe o registro alterado no ca-
che do cliente.
l raMerge.Mescle os campos desse registro com o registro do banco de dados. Esse registro não se
aplicará aos registros que foram inseridos.
l raCorrect. Atualize o registro do banco de dados com os valores especificados. Ao selecionar essa
ação na caixa de diálogo Reconcile Error, você poderá editar os valores na grade. Esse método
não pode ser usado se outro usuário tiver alterado o registro do banco de dados.
l raCancel. Não atualize o registro do banco de dados. Remova o registro do cache do cliente.
l raRefresh. Atualize o registro no cache do cliente com o registro atual no banco de dados.
l raAbort. Aborte a operação de atualização inteira.
Nem todas essas opções fazem sentido (e portanto não serão apresentadas) em todos os casos. Um
requisito para que as ações raMerge e raRefresh estejam disponíveis é que o MIDAS identifique o registro
por meio da chave primária do banco de dados. Isso é feito automaticamente com o InterBase, mas ou-
tros SGBDR exigirão que você defina manualmente a propriedade TField.ProviderFlags.pfInKey como True
no componente TDataset para todos os campos que estão na sua chave primária.
Client:
procedure TForm1.ClientDataSet1BeforeGetRecords(Sender: TObject;
var OwnerData: OleVariant);
begin
// KeyValue é uma variável OleVariant privada
if not (Sender as TClientDataSet).Active then
KeyValue := Unassigned;
OwnerData := KeyValue;
end;
1052
procedure TForm1.ClientDataSet1AfterGetRecords(Sender: TObject;
var OwnerData: OleVariant);
begin
KeyValue := OwnerData;
end;
Um último ponto sobre o uso da busca automática é que a execução de TClientDataSet.Last( ) apa-
nha o restante dos registros que foram deixados no conjunto de resultados. Isso pode ser feito inocente-
mente pressionando-se Ctrl+End na TDBGrid. Para contornar o problema, você precisa definir TClientDa-
taSet.FetchOnDemand como False. Essa propriedade controla se um pacote de dados será apanhado automa-
ticamente quando o usuário tiver lido todos os registros existentes no cliente. Para simular esse compor-
tamento no código, você pode usar o método GetNextPacket( ), que retornará o próximo pacote de dados
para você.
DICA
Se você especificar um TClientDataset.Filename que possua a extensão XML , o pacote de dados será arma-
zenado no formato XML, permitindo que você utilize qualquer quantidade de ferramentas XML disponíveis
para o trabalho no porta-arquivos.
l upWhereChanged.Essa opção permite que os dois usuários realmente editem o mesmo registro ao
mesmo tempo; desde que ambos editem diferentes campos do mesmo registro, não haverá qual-
quer detecção de colisão. Por exemplo, se o usuário A modifica o campo Endereço e atualiza o re-
gistro, o usuário B ainda poderá modificar o campo DataNascimento e atualizar o registro com
sucesso.
l upWhereKeyOnly.Essa opção é a mais aberta de todas. Desde que o registro exista no banco de da-
dos, todo usuário terá sua mudança aceita. Isso sempre modificará o registro existente no banco
de dados, de modo que pode ser visto como um meio de fornecer a funcionalidade do tipo “o úl-
timo a entrar ganha”.
ATENÇÃO
A opção poAutoRefresh não funcionava na versão inicial do Delphi 5. poAutoRefresh só funcionará com
uma versão posterior do Delphi 5, que inclua o reparo desse bug. A alternativa nesse meio tempo é chamar
Refresh( ) para os seus TClientDatasets ou tomar você mesmo o controle total do processo de aplicação de
atualizações.
A discussão inteira do processo de reconciliação até aqui girou em torno da reconciliação padrão
baseada em SQL. Isso significa que todos os eventos no TDataset dinâmico não serão usados durante o
processo de reconciliação. A propriedade TDatasetProvider.ResolveToDataset foi criada para usar esses
eventos durante a reconciliação. Por exemplo, se TDatasetProvider.ResolveToDataset for verdadeiro, a maior
parte dos eventos do TDataset será disparada. Saiba que os eventos usados são chamados apenas quando
1054 se aplica atualizações no servidor. Em outras palavras, se você tiver um evento TQuery.BeforeInsert defini-
do no servidor, ele só será disparado no servidor quando você chamar TClientDataSet.ApplyUpdates. Os
eventos não se integram aos eventos correspondentes do TClientDataSet.
Datasets aninhados
O Delphi 4 introduziu datasets aninhados. Os datasets aninhados permitem que uma tabela mestre re-
almente contenha datasets de detalhe. Além de atualizar registros mestre e detalhe em uma transação,
eles permitem o armazenamento de todos os registros mestre e detalhe em um porta-arquivos, e você
poderá usar as melhorias em DBGrid para fazer surgir datasets de detalhe em suas próprias janelas. Um
aviso, caso você queira usar datasets aninhados: todos os registros de detalhe serão apanhados e trazi-
dos para o cliente quando umr egistro mestre é selecionado. Isso causará uma possível perda de desem-
penho se você aninhar muitos níveis de datasets de detalhe. Por exemplo, se você apanhar apenas um
registro mestre que tenha 10 registros de detalhe, e cada registro de detalhe tiver três registros de deta-
lhe vinculados ao detalhe de primeiro nível, você apanharia 41 registros inicialmente. Quando estiver
usando o vínculo no lado do cliente, só apanharia 14 registros inicialmente, obtendo os outros regis-
tros netos enquanto você rola o detalhe de TClientDataSet. Veremos os datasets aninhados com mais de-
talhes em outra oportunidade.
Associações
A escrita de uma aplicação de banco de dados relacional depende bastante dos relacionamentos entre as
tabelas. Normalmente, você verá que é conveniente representar seus dados (bastante normalizados) em
uma visão que seja mais achatada do que a estrutura de dados básica. Entretanto, a atualização dos dados
a partir dessas associações exige cuidados extras da sua parte.
Fazendo isso, você está dizendo ao ClientDataset para cuidar do nome da tabela para você. Agora,
quando você chamar ClientDataset1.ApplyUpdates( ), o MIDAS saberá usar por default o nome da tabela
que você especificou, em vez de deixar que ele próprio tente descobrir qual é o nome da tabela.
Um modo alternativo seria usar um componente TUpdateSQL que só atualiza a tabela do seu interesse.
Esse novo recurso do Delphi 5 permite que o TQuery.UpdateObject seja usado durante o processo de recon-
ciliação e combine mais de perto com o processo usado nas aplicações cliente/servidor tradicionais.
Você encontrará um exemplo no CD-ROM que acompanha este livro, no diretório deste capí-
tulo, abaixo de \Join1.
usqlEmp.DatabaseName := JoinQuery.DatabaseName;
usqlEmp.Dataset := Dataset;
usqlEmp.Apply(UpdateKind);
usqlFTEmp.SessionName := JoinQuery.SessionName;
usqlFTEmp.DatabaseName := JoinQuery.DatabaseName;
usqlFTEmp.Dataset := Dataset;
usqlFTEmp.Apply(UpdateKind);
UpdateAction := uaApplied;
end;
Visto que estamos seguindo as regras de atualização de dados dentro da arquitetura MIDAS, o pro-
cesso inteiro de atualização é disparado de modo transparente como sempre no MIDAS, com uma cha-
mada para ClientDataset1.ApplyUpdates(0);.
NOTA
Agora que o Delphi 5 aceita a propriedade UpdateObject durante a reconciliação, é totalmente razoável
considerar que o mesmo método de aplicação de atualizações em associações de múltiplas tabelas que
existe para atualizações em cache estará disponível para o MIDAS. No entanto, no momento em que este li-
vro foi escrito, essa funcionalidade não estava disponível.
Você verá um exemplo no CD-ROM que acompanha este livro, no diretório deste capítulo, abaixo
de \Join2.
MIDAS na Web
O Delphi está ligado à plataforma Windows; portanto, quaisquer clientes que você escreva deverão ro-
dar em uma máquina Windows. Mas nem sempre se deseja isso. Por exemplo, você pode querer fornecer
acesso fácil para os dados que existem no seu banco de dados a qualquer um que tenha uma conexão com
a Internet. Como você já escreveu um servidor de aplicação que atua como agente para os seus dados –
além de abrigar regras comerciais para esses dados –, é preferível reutilizar o servidor de aplicação ao in-
vés de reescrever as camadas inteiras de acesso aos dados e regra comercial em outro ambiente.
HTML direto
Esta seção explica como aproveitar seu servidor de aplicação enquanto se oferece uma nova camada de
apresentação que usará HTML direto. Esta seção considera que você esteja acostumado com o material
coberto no Capítulo 31. Usando esse método, você está introduzindo outra camada na sua arquitetura. O
WebBroker atua como cliente para o servidor de aplicação e reempacota esses dados na HTML que será
exibida no navegador. Você também perderá alguns dos benefícios do trabalho com o IDE do Delphi,
como a falta de controles ligados aos dados. No entanto, esta é uma opção bastante viável para conceder
acesso aos seus dados em um formato HTML simples.
Depois de criar um WebModule, você simplesmente inclui uma TDispatchConnection e um TClientDataset
no WebModule. Quando as propriedades estiverem preenchidas, você poderá usar diversos métodos dife-
rentes para traduzir esses dados em HTML, que será eventualmente vista pelo cliente.
Uma técnica válida seria incluir um TDatasetTableProducer vinculado ao TClientDataset do seu interesse. A
partir daí, o usuário poderá dar um clique em um link e ir para uma página de edição, onde poderá editar os
dados e aplicar as atualizações. Veja nas Listagens 32.3 e 32.4 uma implementação simples dessa técnica.
1057
Listagem 32.3 HTML para editar e aplicar atualizações
unit WebMain;
interface
uses
Windows, Messages, SysUtils, Classes, HTTPApp, DBWeb, Db, DBClient,
MConnect, DSProd;
type
TWebModule1 = class(TWebModule)
dcJoin: TDCOMConnection;
cdsJoin: TClientDataSet;
dstpJoin: TDataSetTableProducer;
dsppJoin: TDataSetPageProducer;
ppSuccess: TPageProducer;
ppError: TPageProducer;
procedure WebModuleBeforeDispatch(Sender: TObject;
Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
procedure WebModule1waListAction(Sender: TObject; Request: TWebRequest;
Response: TWebResponse; var Handled: Boolean);
procedure dstpJoinFormatCell(Sender: TObject; CellRow,
1058 CellColumn: Integer; var BgColor: THTMLBgColor;
Listagem 32.4 Continuação
var
WebModule1: TWebModule1;
implementation
{$R *.DFM}
cdsJoin.Open;
if cdsJoin.Locate(‘EMPNO’, EmpNo, [ ]) then
begin
cdsJoin.Edit;
cdsJoin.FieldByName(‘LastName’).AsString:=LastName;
cdsJoin.FieldByName(‘FirstName’).AsString:=FirstName;
cdsJoin.FieldByName(‘HireDate’).AsString:=HireDate;
cdsJoin.FieldByName(‘Salary’).AsString:=Salary;
cdsJoin.FieldByName(‘Vacation’).AsString:=Vacation;
if cdsJoin.ApplyUpdates(0)=0 then
Response.Content:=ppSuccess.Content else
Response.Content:=pPError.Content;
end;
end;
end.
Observe que este método requer muito código personalizado seja escrito, e o conjunto de recursos
completo do MIDAS não está implementado neste exemplo – especialmente a reconciliação de erro.
1060
Você pode continuar a melhorar este exemplo para que seja mais poderoso, caso você use essa técnica
extensivamente.
ATENÇÃO
É imperativo que você considere o conceito de estado quando escrever seu WebModule e servidor de aplica-
ção. Como o HTTP é um protocolo sem estado, você não pode se basear no fato de que os valores de pro-
priedades serão iguais aos que você os deixou depois que a chamada terminar.
Você encontrará um exemplo no CD-ROM deste livro, no diretório deste capítulo, abaixo de
\WebBrok.
InternetExpress
Com o InternetExpress, você pode melhorar a funcionalidade de uma técnica direta de WebModule para
permitir uma experiência mais rica para o cliente. Isso é possível devido ao uso de padrões abertos,
como XML e JavaScript, no InternetExpress. Usando o InternetExpress, você pode criar um front-end
apenas de browser para o seu servidor de aplicações MIDAS. Nenhum controle ActiveX é baixado,
com zero requisitos de instalação e configuração no lado do cliente; nada além de um browser atingin-
do um servidor da Web.
Para usar o InternetExpress, você deverá ter algum código rodando em um servidor da Web. Neste
exemplo, usaremos uma aplicação ISAPI, mas você também poderia usar CGI ou ASP. A finalidade do
agente da Web é apanhar pedidos do navegador e passar esses pedidos para o servidor de aplicação. A in-
clusão de componentes InternetExpress na aplicação de agente da Web facilita essa tarefa.
Este exemplo usará um servidor de aplicação MIDAS padrão, que possui Customers (clientes),
Orders (pedidos) e Employees (funcionários). Customers e Orders estão vinculados em um relaciona-
mento de dataset aninhado (para obter mais informações sobre datasets aninhados, consulte a próxima
seção), enquanto o dataset Employees servirá como tabela de pesquisa. Veja a definição do servidor de
aplicação no código-fonte do CD que acompanha este livro. Depois que o servidor de aplicação tiver
sido montado e registrado, podemos focalizar a montagem da aplicação do agente da Web, que se comu-
nicará com o servidor de aplicação.
Crie uma nova aplicação ISAPI selecionando File, New, Web Server Application a partir do Object
Repository. Coloque um componente TDCOMConnection no WebModule. Isso atuará como um vínculo para o
servidor de aplicação; portanto, preencha a propriedade ServerName com o ProgId do servidor de aplicação.
Em seguida, colocaremos um componente TXMLBroker da página InternetExpress da palheta de compo-
nentes no WebModule e definiremos as propriedades RemoteServer e ProviderName para o CustomerProvider.
O componente TXMLBroker atua de modo semelhante ao TClientDataset. Ele é responsável por apanhar paco-
tes de dados do servidor de aplicação e passar esses pacotes de dados ao browser. A principal diferença en-
tre o pacote de dados em um TXMLBroker e um TClientDataset é que o TXMLBroker traduz os pacotes de dados do
MIDAS para XML. Também incluiremos um TClientDataset ao WebModule e o ligaremos ao provedor Emplo-
yees no servidor de aplicação. Mais adiante, usaremos isso como origem de dados de pesquisa.
O componente TXMLBroker é responsável pela comunicação com o servidor de aplicação e também
pela navegação das páginas HTML. Existem muitas propriedades disponíveis para personalizar o modo
como sua aplicação InternetExpress se comportará. Por exemplo, você pode limitar o número de regis-
tros que serão transmitidos para o cliente, ou pode especificar o número de erros permitidos durante
uma atualização.
Agora precisamos de uma maneira de mover esses dados para o navegador. Usando o componente
TMidasPageProducer, podemos usar a tecnologia WebBroker no Delphi para enviar uma página HTML
para o navegador. No entanto, o TMidasPageProducer também permite a criação visual da página da Web
por meio do Web Page Editor. 1061
Dê um clique duplo no TMidasPageProducer para trazer o Web Page Editor. Esse editor visual ajuda a
personalizar os elementos presentes em uma determinada página da Web. Uma das coisas mais interes-
santes sobre o InternetExpress é que ele é completamente extensível. Você poderá criar seus próprios
componentes, que podem ser usados no Web Page Editor seguindo algumas regras bem definidas. Para
obter exemplos de componentes personalizados de ItnernetExpress, consulte o diretório <DELPHI>\DEMOS\
MIDAS\INTERNETEXPRESS\INETXCUSTOM.
ATENÇÃO
TMidasPageProducer possui uma propriedade chamada IncludePathURL. É essencial definir essa proprie-
dade corretamente, ou sua aplicação InternetExpress não funcionará. Defina o valor para o diretório virtual
que contém os arquivos JavaScript do IntenetExpress. Por exemplo, se você incluir os arquivos em c:\inet-
pub\wwwroot\jscript, o valor dessa propriedade será /jscript/.
Com o Web Page Editor ativo, selecione o botão da ferramenta Insert para exibir a caixa de diálo-
go Add Web Component. Essa caixa de diálogo contém uma lista dos componentes da Web que podem
ser incluídos na página HTML. A lista é baseada no componente pai (a seção no canto superior esquer-
do) atualmente selecionado. Por exemplo, inclua um componente DataForm Web na raiz para permi-
tir que os usuários finais apresentem e editem informações do banco de dados em um layout tipo for-
mulário.
FIGURA 32.7 Incluindo a caixa de diálogo Web Component no Web Page Editor.
Se você selecionar então o nó DataDorm no Web Page Editor, poderá selecionar o botão da ferra-
menta “Insert” novamente. Observe que a lista de componentes disponíveis neste ponto é diferente da
lista apresentada pela etapa anterior. Depois de selecionar o componente FieldGroup, você verá um avi-
so no painel de visualização, informando que a propriedade TXMLBroker para o FieldGroup não está atri-
buída. Atribuindo o XMLBroker no Object Inspector, você notará imediatamente o layout do HTML no
painel de prévia do Web Page Editor. Ao continuar modificando as propriedades ou incluir componen-
tes, o estado da página HTML será constantemente atualizado.
O nível de personalização disponível com os componentes padrão da Web é praticamente ilimita-
do. As propriedades facilitam a mudança de legendas de campo, alinhamento, cores; incluir código
HTML personalizado; e até mesmo usar folhas de estilo. Além do mais, se o componente não estiver exa-
tamente de acordo com suas necessidades, você sempre poderá criar um componente descendente e
usá-lo no seu lugar. A estrutura é verdadeiramente tão extensível quanto a sua imaginação permitir.
Para chamar a DLL ISAPI, você precisa colocá-la em um diretório virtual capaz de executar um
script. Também é preciso mover os arquivos em JavaScript encontrados em <DELPHI>\SOURCE\WEBMIDAS
para um local valido no seu servidor da Web e modificar a propriedade TMidasPageProducer.Include-
PathURL para que aponte para o URL dos arquivos em JavaScript. Depois disso, a página estará pronta
para ser vista.
1062
Para acessar a página, tudo o que você precisa é de um browser capaz de ler JavaScript. Basta apon-
tar o browser para http://localhost/inetx/inetxisapi.dll e os dados aparecerão no browser.
Por último, você pode detectar erros de reconciliação durante o processo ApplyUpdates, como já
está acostumado a fazer em uma aplicação MIDAS independente. Essa capacidade é permitida quando
você atribui a propriedade TXMLBroker.ReconcileProducer a um TPageProducer. Sempre que ocorrer um erro,
o Content do TPageProducer atribuído a essa propriedade será retornado ao usuário final.
FIGURA 32.8 O Web Page Editor depois de designar uma página HTML.
1063
Você encontrará um exemplo no CD-ROM que acompanha este livro, no diretório deste capí-
tulo, abaixo de \InetX.
Datasets aninhados
Já vimos os datasets aninhados a partir de um nível mais alto. Agora, vamos analisá-los com mais detalhes.
Para se configurar um relacionamento de dataset aninhado, você precisa definir o relacionamento
mestre/detalhe no servidor de aplicação. Isso é feito usando-se a mesma técnica que você usou nas aplica-
ções cliente/servidor – a saber, definindo a instrução SQL para o detalhe TQuery, incluindo o parâmetro de
link. Veja um exemplo:
“select * orders where custno=:custno”
Depois você atribui o TQuery.Datasource para o detalhe TQuery apontar para um componente TData-
source ligado ao mestre TDataset. Quando esse relacionamento estiver preparado, você só terá de exportar
o TDatasetProvider que está ligado ao dataset mestre. O MIDAS é inteligente o bastante para entender que
o dataset mestre possui datasets de detalhe vinculados a ele, e portanto enviará os datasets de detalhe
para o cliente como um TDatasetField.
No cliente, você atribui a propriedade TClientDataset.ProviderName do mestre para o provedor mestre.
Em seguida, inclua campos persistentes para o TClientDataset. Observe o último campo no Fields Editor.
Ele contém um campo com o mesmo nome do dataset de detalhe no servidor, e é declarado como um tipo
TDatasetField. Nesse ponto, você tem informações suficientes para usar o dataset aninhado no código.
Entretanto, para facilitar realmente as coisas, você pode incluir um TClientDataset de detalhe e atribuir sua
propriedade DatasetField ao TDatasetField apropriado a partir do mestre. É importante observar aqui que
você não definiu quaisquer outras propriedades no detalhe TClientDataset, como RemoteServer, ProviderName,
MasterSource, MasterFields ou PacketRecords. A única propriedade que você definiu foi a propriedade Dataset-
Field. Nesse ponto, você também pode vincular os controles ligados aos dados ao detalhe TClientDataset.
1064
Depois de ter acabado de trabalhar com os dados no dataset aninhado, você precisará aplicar as atua-
lizações no banco de dados. Isso é feito chamando-se o método ApplyUpdates do mestre TClientDataset. O
MIDAS aplicará todas as mudanças no mestre TClientDataset, que inclui os datasets de detalhe, de volta
ao servidor dentro do contexto de uma transação.
Você encontrará um exemplo no CD-ROM do livro, no diretório para este capítulo, sob
\NestCDS.
ATENÇÃO
A versão inicial do Delphi 5 tinha um bug que impedia a aplicação de vários deltas ao servidor dentro do
contexto da transação. Substitua o método a seguir em DBTABLES.PAS pelo código aqui listado, se quiser
aproveitar essa técnica.
1065
Você encontrará um exemplo no CD-ROM deste livro, no diretório deste capítulo, abaixo de \MDCDS.
procedure TClientDM.ApplyUpdates;
var
MasterVar, DetailVar: OleVariant;
begin
Master.CheckBrowseMode;
Detail_Proj.CheckBrowseMode;
if Master.ChangeCount > 0 then
MasterVar := Master.Delta else
MasterVar := NULL;
if Detail.ChangeCount > 0 then
DetailVar := Detail.Delta else
DetailVar := NULL;
RemoteServer.AppServer.ApplyUpdates(DetailVar, MasterVar);
{ Reconcilia os pacotes de dados de erro. Como permitimos 0 erro, somente
um pacote de erro poderá conter erros. Se nenhum pacote tiver erros,
então atualizamos os dados. }
if not VarIsNull(DetailVar) then
Detail.Reconcile(DetailVar) else
if not VarIsNull(MasterVar) then
Master.Reconcile(MasterVar) else
begin
Detail.Reconcile(DetailVar);
Master.Reconcile(MasterVar);
Detail.Refresh;
Master.Refresh;
end;
end;
unit CDSUtil;
interface
uses
DbClient, DbTables;
implementation
uses
SysUtils, Provider,
{$IFDEF VER130}Midas{$ELSE}StdVcl{$ENDIF};
type
PArrayData = ^TArrayData;
TArrayData = array[0..1000] of Olevariant;
{$ENDIF}
if ErrCount > 0 then
SysUtils.Abort; // Isso causará um Rollback no proc. de chamada
end;
end;
{Server call}
procedure CDSApplyUpdates(ADatabase : TDatabase; var vDeltaArray: OleVariant;
const vProviderArray: OleVariant);
var
i : integer;
LowArr, HighArr: integer;
P: PArrayData;
begin
{ Envolve as atualizações em uma transação. Se qualquer etapa resultar em
um erro, gera uma exceção, que causará o Rollback da transação. }
ADatabase.Connected:=true;
ADatabase.StartTransaction;
try
LowArr:=VarArrayLowBound(vDeltaArray,1);
HighArr:=VarArrayHighBound(vDeltaArray,1);
P:=VarArrayLock(vDeltaArray);
try
for i:=LowArr to HighArr do
ApplyDelta(vProviderArray[i], P^[i]);
finally
VarArrayUnlock(vDeltaArray);
end;
ADatabase.Commit;
except
ADatabase.Rollback;
end;
end;
end;
end;
end.
1069
A Listagem 32.8 mostra uma mudança do exemplo anterior usando a unidade CDSUtil.
vDelta:=RetrieveDeltas(arrCDS); // Etapa 1
vProvider:=RetrieveProviders(arrCDS); // Etapa 2
DCOMConnection1.ApplyUpdates(vDelta, vProvider); // Etapa 3
ReconcileDeltas(arrCDS, vDelta); // Etapa 4
end;
Você pode usar essa unidade em aplicações de duas ou três camadas. Para passar de um enfoque de
duas para três camadas, você exportaria uma função no servidor que chama CDSApplyUpdates, ao invés de
chamar CDSApplyUpdates no cliente. Tudo o mais no cliente permanece igual.
1070
Listagem 32.9 Código para atribuir dados diretamente de um TDataSet
Esse método utiliza mais código e esforço do que as versões anteriores do Delphi, onde você sim-
plesmente atribuiria a propriedade Table1.Provider.Data à propriedade ClientDataset1.Data. No entanto,
essa função ajudará a tornar o código adicional menos observável.
Você também pode usar o componente TClientDataset para apanhar os dados de um TDataset durante
o projeto, selecionando o comando Assign Local Data (atribuir dados locais) no menu de contexto do
componente TClientDataset. Depois, especifique o componente TDataset que contém os dados que você
deseja e os dados serão trazidos para o TClientDataset e armazenados na propriedade Data.
ATENÇÃO
Se você tivesse que salvar o arquivo nesse estado e comparar o tamanho do arquivo DFM com o tamanho
antes de executar esse comando, notaria um aumento no tamanho do DFM. Isso acontece porque o Delphi
armazenou todos os metadados e registros associados ao TDataset no DFM. O Delphi só encaminhará es-
ses dados para o DFM se o TClientDataset estiver Active. Você também pode remover esse espaço extra
executando o comando Clear Data (limpar dados) no menu de contexto de TClientDataset.
Se você quiser obter toda a flexibilidade que uma atribuição de provedor permite, terá de atribuir a
propriedade AppServer. Em runtime, você pode atribuir a propriedade AppServer no código. Isso pode ser
tão simples quanto a instrução a seguir, encontrada em FormCreate:
ClientDataset1.AppServer:=TLocalAppServer.Create(Table1);
ClientDataset1.Open;
Por último, você pode atribuir a propriedade AppServer durante o projeto. Se você deixar a proprie-
dade RemoteServer em branco em um TClientDataset, poderá atribuir um componente TDatasetProvider à
propriedade TClientDataset.ProviderName.
A principal diferença entre o uso de componentes TDataset e ClientDataset é que, quando você está
usando ClientDataset,. está usando a interface IAppServer para intermediar seus pedidos de dados ao com-
ponente TDataset básico. Isso significa que você estará manipulando as propriedades, métodos, eventos e
campos do componente TClientDataset, e não o componente TDataset. Pense no componente TDataset
como se ele estivesse em uma aplicação separada e, portanto, não pode ser manipulado diretamente por
você no código. Coloque todos os seus componentes do “servidor” em um DataModule separado. A coloca-
ção dos componentes TDatabase, TDataset e TCDSProvider em um DataModule separado, com efeito, prepara
sua aplicação para uma transição mais fácil para uma distribuição em multicamadas mais à frente. Outro
benefício de se fazer isso é que pode ajudá-lo a pensar no DataModule como algo que o cliente não pode to- 1071
car com facilidade. Novamente, essa não é uma boa preparação para a sua aplicação, e para o seu raciocí-
nio, quando chegar a hora de transportar essa aplicação para uma distribuição em multicamadas.
NOTA
A propriedade TClientDataset.ProviderName não pode ser atribuída a provedores que residem em outro
formulário ou DataModule durante o projeto. Portanto, você precisa definir a propriedade TClientData-
set.AppServer em runtime no código.
Questões de licenciamento
O licenciamento tem sido um assunto confuso para muitas pessoas desde que o MIDAS foi introduzido
inicialmente no Delphi 3. As inúmeras opções de distribuição dessa tecnologia contribuíram para essa
confusão. Esta seção detalhará os requisitos gerais de quando você precisa adquirir uma licença para o
MIDAS. Entretanto, o único documento legal para o licenciamento está em DEPLOY.TXT, localizado no di-
retório do Delphi 5. Finalmente, para chegar à autoridade máxima para responder a essa pergunta em
uma situação específica, você terá que contatar seu escritório de vendas local da Borland. Outras orienta-
ções e exemplos estão disponíveis em
http://www.borland.com/midas/papers/licensing/
As informações desse documento foram preparadas para responder a alguns dos cenários mais co-
muns em que o MIDAS é utilizado. As informações e opções de preço também estão incluídas no docu-
mento.
O critério chave para se determinar a necessidade de uma licença do MIDAS para a sua aplicação é
verificar se o pacote de dados do MIDAS atravessa ou não um limite de máquina. Se atravessar, então
você precisa adquirir uma licença. Se não (como nos exemplos de uma e duas camadas apresentados an-
teriormente), você estará usando a tecnologia MIDAS, mas não haverá necessidade de adquirir uma li-
cença para usar o MIDAS dessa maneira.
Configuração do DCOM
A configuração do DCOM parece ser tanto uma arte quanto uma ciência. Existem muitos aspectos para
uma configuração completa e segura do DCOM, mas esta seção o ajudará a entender alguns dos funda-
mentos dessa arte oculta.
Depois de registrar seu servidor de aplicação, o objeto servidor agora está disponível para persona-
lização no utilitário DCOMCNFG da Microsoft. Esse utilitário é incluído automaticamente em sistemas
NT, mas é um download separado para máquinas Win9x. Como um parêntese, existem muitos bugs no
DCOMCNFG, o principal deles é que DCOMCNFG só pode ser executado em máquinas Win9x que te-
nham ativado o compartilhamento em nível de usuário. Isso, é claro, requer um domínio, o que nem
sempre é possível ou desejável em uma rede não-hierárquica (peer-to-peer), como em duas máquinas
Windows 9x. Isso levou muitas pessoas a considerarem incorretamente que era preciso usar uma máqui-
na NT para rodar o DCOM.
1072
Se você puder rodar o DCOMCNFG, poderá selecionar o servidor de aplicação registrado e dar um
clique no botão Properties para revelar informações personalizadas sobre o seu servidor. A página Iden-
tity é um bom local para iniciar nossa breve excursão pelo DCOMCNFG. A opção default para um obje-
to servidor registrado é Launching User (usuário acionador). A Microsoft não poderia ter feito uma deci-
são pior com relação ao default.
Quando o DCOM cria o servidor, ele usa o contexto de segurança do usuário especificado na pági-
na Identity. O “usuário acionador” gerará um novo processo do objeto servidor para o login de todo e
qualquer usuário distinto. Muitas pessoas se admiram do fato de terem selecionado o modo de instancia-
ção ciMultiple e diversas cópias do seu servidor estarem sendo criadas. Por exemplo, se o usuário A se co-
necta ao servidor e depois o usuário B se conecta, o DCOM gerará um processo inteiramente novo para o
usuário B. Além disso, você não verá a parte GUI do servidor para os usuários que se conectam sob uma
conta diferente da que está atualmente em uso na máquina servidora. Isso é devido ao conceito do NT
conhecido como estações Windows. A única estação Windows capaz de escrever na GUI é a do Interacti-
ve User (usuário interativo). Esse é o usuário que está conectado atualmente à máquina servidora. Resu-
mindo, nunca use a opção Lauching User como sua identidade para o seu servidor.
A próxima opção interessante nessa página é Interactive User. Isso significa que cada cliente que cria um
servidor fará isso no contexto do usuário que está logado ao servidor nesse ponto do tempo. Isso também lhe
permitirá ter uma interação visual com o seu servidor de aplicação. Infelizmente, a maioria dos administrado-
res de sistema não permite que um login aberto fique lá ocioso em uma máquina NT. Além disso, se o usuário
conectado decidir se desconectar, o servidor de aplicação não funcionará mais conforme desejado.
Para esta discussão, isso só nos deixa a última opção ativada na página Identity: This User (este usuá-
rio). Usando essa opção, todos os clientes criarão um servidor de aplicação e usarão as credenciais do login
e o contexto do usuário especificado na página Identity. Isso também significa que a máquina NT não exige
que um usuário esteja conectado para usar o servidor de aplicação. A única desvantagem desse método é
que não haverá exibição GUI do servidor quando se estiver usando essa opção. No entanto, essa é a melhor
de todas as opções disponíveis para que seu servidor de aplicação se comporte como deveria.
Quando o objeto servidor estiver configurado de modo apropriado com a identidade correta, você
terá que voltar sua atenção para a guia Security (segurança). Certifique-se de que o usuário que estará ro-
dando esse objeto recebeu os privilégios corretos. Além disso, certifique-se de conceder ao usuário
SYSTEM o acesso ao servidor; caso contrário, você encontrará erros no caminho.
Existem muitas nuances sutis espalhadas pelo processo de configuração do DCOM. Para ver o que
há de mais recente em problemas de configuração do DCOM, especialmente em relação ao Windows 9x,
Delphi e MIDAS, visite a página do DCOM no nosso site da Web, em
http://www.DistribuCon.com/dcom95.htm
Outro benefício do uso de HTTP para o seu transporte é que um OS como o NT Enterprise permite
agrupar servidores. Isso oferece verdadeiro equilíbrio de carga e tolerância a falhas para o seu servidor de
aplicação. Para obter mais informações sobre o agrupamento de servidores, visite:
http://www.microsoft.com/ntserver/ntserverenterprise/exec/overview/clustering
As limitações do uso de TWebConnection são triviais e compensam qualquer concessão a fim de ter
mais clientes capazes de alcançar seu servidor de aplicação. As limitações são que você precisa instalar wi-
ninet.dll no cliente, e nenhum callback está disponível quando se usa TWebConnection. Além do mais, você
precisa registrar o servidor de aplicação com a função utilitária EnableWebTransport em um método Update-
Registry redefinido.
Resumo
Este capítulo ofereceu muitas informações sobre MIDAS. Ainda assim, ele apenas arranhou a superfície
do que pode ser feito com essa tecnologia – algo muito além do escopo de um único capítulo. Mesmo de-
pois de ter explorado todos os detalhes do MIDAS, ainda poderá aumentar seu conhecimento e recursos
usando o MIDAS com o C++Builder e JBuilder. Usando o JBuilder, você poderá atingir o céu do acesso
a um servidor de aplicação entre plataformas diferentes, enquanto utiliza a mesma tecnologia e conceitos
que aprendeu aqui.
MIDAS é uma tecnologia em rápida evolução, que traz a promessa de aplicações em multicamadas
para cada programador. Quando você experimentar o verdadeiro poder da criação de aplicações com
MIDAS, talvez nunca retorne ao desenvolvimento de aplicações de banco de dados conforme o conhece
atualmente.
1075
Desenvolvimento PARTE
rápido de aplicações
de banco de dados
V
NE STA PART E
33 Gerenciador de estoque: desenvolvimento
cliente/servidor
desenvolvimento
cliente/servidor
33
NE STE C AP ÍT UL O
l Projeto do back-end 1080
l Acesso centralizado ao banco de dados:
as regras comerciais 1087
l Projeto da interface do usuário 1101
l Resumo 1122
Este capítulo explica como projetar uma aplicação de banco de dados usando os conceitos discutidos no
Capítulo 29. Aqui, ilustramos as técnicas para o desenvolvimento de uma aplicação cliente/servidor em
duas camadas. Nessa aplicação, dividimos a lógica da aplicação, ou regras comerciais, entre o cliente e o
servidor. Também ilustramos como centralizar o acesso aos dados em um módulo de dados, permitin-
do-nos assim separar completamente a interface do usuário da lógica de banco de dados.
Ainda no Capítulo 4, apresentamos uma estrutura para os formulários que poderiam ser criados in-
dependentemente ou como janelas filhas de outro controle. Neste capítulo, usamos essa estrutura para
nossa interface do usuário.
O back-end de banco de dados utilizado é o Local InterBase. A aplicação foi elaborada em torno de
um modelo comercial típico para peças automotivas. Esse modelo comercial requer que a aplicação re-
gistre três conjuntos de dados principais.
l Estoque de produtos. Isso inclui as quantidades de cada item no estoque e o preço de cada item.
l Vendas. Esse conjunto contém informações sobre itens vendidos e para quais clientes esses itens
foram vendidos.
l Clientes. Esse conjunto contém informações como nome e endereço dos clientes.
Isso, de forma alguma, constitui uma aplicação de gerenciamento de estoque completa. A finalidade
deste capítulo é focalizar as técnicas de desenvolvimento cliente/servidor. Oferecemos uma aplicação
funcional completa para ilustrar esse foco.
O capítulo é dividido em três partes. A primeira parte, “Projeto do back-end”, discute o projeto do
back-end. Isso inclui os objetos do banco de dados, que você aprendeu a respeito no Capítulo 29. A se-
gunda parte, “Acesso centralizado ao banco de dados: as regras comerciais”, discute como usar o TDataMo-
dule do Delphi para centralizar o acesso ao banco de dados. Finalmente, a terceira parte, “Projeto da in-
terface do usuário”, discute o projeto da interface de usuário real para a aplicação de estoque.
Projeto do back-end
Usamos o Local InterBase Server, da InterBase Software Corporation, como back-end para a aplicação
Inventory Manager. Isso nos dá a capacidade de projetar o banco de dados inteiramente através da SQL.
Também oferece a flexibilidade de poder mover parte do processamento de dados para o lado do servi-
dor da equação, através do uso de triggers, geradores e procedimentos armazenados – o que também aju-
da a garantir melhor integridade de dados. Outro benefício mais tangível do back-end SQL é que ele
pode ser dimensionado para um verdadeiro ambiente cliente/servidor.
NOTA
Alguns dos tópicos deste capítulo são específicos do InterBase, e podem não se aplicar a outros SGBDRs
SQL, como Oracle e Microsoft SQL. No entanto, os conceitos discutidos ainda se aplicam e podem muito
bem ser implementados de forma diferente.
Como discutimos no Capítulo 29, usaremos SQL para criar os diversos objetos de banco de dados
exigidos para a aplicação Inventory Manager. Isso incluirá objetos como domínios, tabelas, geradores,
triggers, procedimentos armazenados e permissões.
Existem várias maneiras de criar o back-end usando diversas ferramentas de modelagem de dados.
Ferramentas de modelagem de dados como xCase, RoboCase, Erwin e SQL-Designer são apenas algu-
mas das ferramentas que simplificam bastante o processo de modelagem de dados. Todas basicamente
permitem modelar visualmente seus dados sem ter que digitar o código SQL. Depois que você tiver pro-
jetado seu modelo de dados básico, poderá fazer as mudanças que forem necessárias.
A Figura 33.1 representa o modelo de dados para a nossa aplicação de vendas.
1080
Part
Part_number:vc(10)
Description:vc(18)
Items
Quantity:si(4,0) RRRI
List_price:f Sale_number:i(9,0)
Retail_price:f Item_no:i(9,0)
Dealer_price:f Part_number:vc(10)
Jobber_price:f Quantity:i(9,0)
Customer
RRRI Customer_id:(9,0)
Frame:c(20)
Lname:c(20)
Sales Credit_line:si(4,0)
Sale_number:i(9,0) RRRI Work_address:vc(50)
Alt_address:vc(50)
Costumer_id:(9,0) City:vc(20)
Sale_date:dt State:vc(20)
Total_price:f Zip:vc(20)
Work_phone:vc(20)
Alt_phone:vc(20)
Comments:b
Company:vc(40)
Definindo domínios
Antes de definir quaisquer tabelas, triggers etc., você definirá domínios que usará pelo restante do código
SQL que compõe os metadados.
NOTA
Metadados são todos os objetos (tabelas, índices etc.) contidos como parte de uma definição do banco de
dados.
Pense em um domínio como uma entidade semelhante a um tipo definido pelo usuário em Object
Pascal. Os domínios permitem definir tipos de dados especiais com mais estrutura do que os tipos de da-
dos embutidos.
Os domínios ajudam a simplificar declarações de dados e restrição, permitindo a criação de nomes
abreviados para os tipos comuns no seu banco de dados. Observe que você não pode alterar um domínio
depois que as colunas da tabela o tiverem utilizado.
A seguir vemos alguns dos domínios usados nos metadados de vendas:
l CREATE DOMAIN DCUSTOMERID AS INTEGER;
Este é um domínio simples. Ele define um novo domínio chamado DCUSTOMERID como um tipo
idêntico ao de um inteiro padrão, bastante comum.
l CREATE DOMAIN DCREDITLINE AS SMALLINT default 0 CHECK (VALUE BETWEEN 0 AND 3000);
Isso define um novo domínio tipo smallint, mas aplica a restrição adicional de que o valor deve
estar entre 0 e 3000.
l CREATE DOMAIN DNAME AS CHAR(20);
Isso define um domínio chamado DNAME, que é uma string de tamanho fixo com exatamente 20 ca-
racteres. 1081
l CREATE DOMAIN DADDRESS AS VARCHAR(50);
CREATE DOMAIN DCITY AS VARCHAR(20);
CREATE DOMAIN DSTATE AS VARCHAR(20);
CREATE DOMAIN DZIP AS VARCHAR(10);
CREATE DOMAIN DPHONE AS VARCHAR(20);
Isso define vários domínios como strings de tamanho variável, de até 50, 20, 20, 10 e 20 caracteres,
respectivamente.
l CREATE DOMAIN DPRICE AS NUMERIC(15, 2) default 0.00;
Isso cria um domínio representando um número decimal. O primeiro número, 15, especifica os
dígitos de precisão a armazenar. O segundo número, 2, especifica o número de casas decimais a
armazenar. O valor default para as colunas desse domínio é 0.00.
NOTA
O tipo de dados CHAR(n) sempre armazena n caracteres no banco de dados. Se a string contida em um
campo em particular for menor do que n caracteres, os caracteres não utilizados serão preenchidos com
espaços.
O tipo de dados VARCHAR(n) armazena o tamanho exato da string, até um máximo de n. Sua vantagem
em relação a CHAR é que economiza mais espaço, mas as operações com VARCHAR costumam ser ligeira-
mente mais lentas.
Para obter mais informações sobre domínios, você poderá ler o “InterBase Language Reference
Guide” da InterBase Corp. ou o arquivo de ajuda IB32.Hlp.
Definindo as tabelas
Usando os domínios definidos, você pode criar tabelas. Cada tabela é criada por meio da instrução SQL
CREATE TABLE, seguida pela enumeração dos campos da tabela e tipos de dados ou domínios.
A tabela CUSTOMER
A tabela CUSTOMER representa o objeto de dados do cliente, e é definida da seguinte maneira:
/* Tabela: CUSTOMER, Proprietário: SYSDBA */
CREATE TABLE CUSTOMER (CUSTOMER_ID INTEGER NOT NULL,
FNAME DNAME NOT NULL,
LNAME DNAME NOT NULL,
CREDIT_LINE DCREDITLINE NOT NULL,
WORK_ADDRESS DADDRESS,
ALT_ADDRESS DADDRESS,
CITY DCITY,
STATE DSTATE,
ZIP DZIP,
WORK_PHONE DPHONE,
ALT_PHONE DPHONE,
COMMENTS BLOB SUB_TYPE TEXT SEGMENT SIZE 80,
COMPANY VARCHAR(40),
CONSTRAINT PCUSTOMER_ID PRIMARY KEY (CUSTOMER_ID));
Os campos definidos com o especificador NOT NULL indicam que o usuário precisa incluir um valor
para esses campos antes que um registro possa ser postado na tabela. Em outras palavras, esses campos
1082 não podem ser deixados em branco.
O campo COMMENTS requer alguma explicação. Esse campo é do tipo BLOB (Binary Large Object), o que
significa que qualquer tipo de dado em forma livre pode ser armazenado no campo. O SUB TYPE de TEXT, no
entanto, significa que os dados contidos no BLOB são texto ASCII e, portanto, compatíveis com o compo-
nente TDBMemo do Delphi.
A instrução CONSTRAINT cria uma chave primária no campo CUSTOMER_ID, que garante que o valor de
cada registro para esse campo será exclusivo. Esse também é o primeiro passo para garantir a integridade
referencial de todo o banco de dados; o campo PRIMARY KEY atua como campo de pesquisa par o campo
FOREIGN KEY de outra tabela, como veremos mais adiante.
A tabela PART
A tabela PART é o estoque da loja. A definição dessa tabela é muito simples:
/* Tabela: PART, Proprietário: SYSDBA */
CREATE TABLE PART (PART_NUMBER VARCHAR(10) NOT NULL,
DESCRIPTION VARCHAR(18),
QUANTITY SMALLINT NOT NULL,
LIST_PRICE DPRICE NOT NULL,
RETAIL_PRICE DPRICE NOT NULL,
DEALER_PRICE DPRICE NOT NULL,
JOBBER_PRICE DPRICE NOT NULL,
CONSTRAINT PPART_NUMBER PRIMARY KEY (PART_NUMBER));
Cada registro representa o item de uma peça exclusiva, contendo informações de descrição, quanti-
dade e preço. Observe que essa tabela também possui uma chave primária – dessa vez, no campo
PART_NUMBER.
A tabela SALES
A tabela SALES é a tabela que contém registros para cada venda para um cliente. Essa tabela é definida da
seguinte forma:
/* Tabela: SALES, Proprietário: SYSDBA */
CREATE TABLE SALES (SALE_NUMBER INTEGER,
CUSTOMER_ID INTEGER,
SALE_DATE DATE,
TOTAL_PRICE DOUBLE PRECISION);
Observe a instrução ALTER TABLE, que inclui uma chave externa para a tabela SALES. Uma chave exter-
na é uma coluna ou conjunto de colunas de uma tabela que corresponde na ordem exata a uma coluna ou
conjunto de colunas definidas como chave primária de outra tabela. As chaves externas completam a in-
tegridade referencial com a tabela SALES, garantindo que nenhuma entrada seja feita para o campo
CUSTOMER_ID, a menos que exista uma entrada com o mesmo código de cliente na tabela CUSTOMER.
A tabela ITEMS
A tabela ITEMS contém os itens, ou peças, para uma determinada venda. A tabela SALES possui um relacio-
namento um-para-muitos com a tabela ITEMS, e está vinculada pelos campos SALE_NUMBER e SALE_NO em cada
tabela. A tabela ITEMS é definida da seguinte forma:
/* Tabela: ITEMS, Proprietário: SYSDBA */
CREATE TABLE ITEMS (SALE_NUMBER INTEGER,
ITEM_NO INTEGER,
PART_NO VARCHAR(10), 1083
QTY SMALLINT);
Assim como a tabela SALES, a tabela ITEMS possui uma chave externa que garante que nenhum registro
será inserido onde o número da peça não existe na tabela PART.
Definindo geradores
Você pode pensar em um gerador como um mecanismo que gera automaticamente números seqüenciais
a serem inseridos em uma tabela. Os geradores normalmente são usados para criar números exclusivos a
serem inseridos no campo de chave de uma tabela. O banco de dados SALES usará geradores para gerar au-
tomaticamente novos códigos para as tabelas CUSTOMER, SALES e ITEMS. Esses geradores são definidos da se-
guinte forma:
CREATE GENERATOR GEN_CUSTID;
CREATE GENERATOR GEN_ITEMNO;
CREATE GENERATOR GEN_SALENO;
NOTA
Depois de incluir um gerador para um banco de dados, ele não pode ser removido com facilidade. A técni-
ca mais simples é remover ou modificar o trigger ou procedimento armazenado para que GEN ID( ) não
seja chamado. Você também pode remover seu gerador da tabela do sistema RDB$GENERATORS.
Definindo triggers
Um trigger é uma rotina que realiza alguma ação automaticamente sempre que um registro de uma tabela
é inserido, atualizado ou deletado. Os triggers permitem deixar que o banco de dados realize tarefas re-
petitivas à medida que os registros são submetidos às tabelas, evitando assim que as aplicações usadas
para acessar e modificar os dados façam isso.
NOTA
Os triggers e geradores são recursos específicos do InterBase. Embora a maioria dos principais fornecedo-
res de SQL também ofereça essas facilidades, é possível que outros fornecedores usem uma sintaxe ou se-
mântica diferente em suas implementações. Embora sendo recursos muito bons, você precisa se lembrar de
que o uso de geradores e triggers pode ser um problema na migração da aplicação para um servidor SQL
que não seja da InterBase.
Para os iniciantes, você precisa de triggers que incluem novos números de clientes e de vendas em
suas respectivas tabelas, através dos geradores criados anteriormente. O trigger para inserir um novo có-
digo de cliente exclusivo poderia ser o seguinte:
CREATE TRIGGER TCUSTOMER_ID FOR CUSTOMER
ACTIVE BEFORE INSERT POSITION 0
as begin
new.customer_id = gen_id(gen_custid, 1);
end
NOTA
Existem vários outros triggers nesse banco de dados, a fim de converter uma abreviação de estado de duas
letras em um nome de estado completo. Você encontrará esses triggers em Sales.ddl no CD-ROM que
acompanha este livro, abaixo do diretório referente a este capítulo.
TOTAL_PRICE)
VALUES(
:rSALE_NUMBER,
:iCUSTOMER_ID,
:iSALE_DATE,
:iTOTAL_PRICE);
END
Esse procedimento armazenado executa algum código SQL muito básico. Primeiro ele apanha um
novo código para o registro de vendas no gerador GEN_SALENO. Depois ele realiza uma instrução SQL INSERT
INTO simples para inserir os dados passados a ele através dos parâmetros.
O segundo procedimento armazenado usado pela aplicação é ligeiramente mais complexo. Esse se-
gundo procedimento armazenado é denominado INSERT_SALE_ITEM, sendo usado para inserir itens indivi-
duais de uma venda na tabela ITEMS. Provavelmente, esse procedimento armazenado será chamado várias
vezes para uma única venda. Portanto, o cliente primeiro chamará o procedimento armazenado
INSERT_SALE para inserir um registro de venda. Ele também teria apanhado um código de venda por meio
da chamada a INSERT_SALE. Depois, o cliente chamaria INSERT_SALE_ITEM para cada item sendo vendido. Para
cada chamada, ele precisa passar a informação do item específico e o código de venda obtido anterior-
mente.
INSERT_SALE_ITEM utiliza três parâmetros: o código da venda, o número da peça e a quantidade do
item específico sendo vendido. Esse procedimento armazenado realiza algumas operações de integridade
de dados. Primeiro, ele se certifica de que exista pelo menos o número de itens solicitados na tabela PART.
Se não, uma exceção será gerada. Se a quantidade de peças existir, o valor do parâmetro Qty será subtraí-
do da quantidade na tabela PART para a peça especificada. Finalmente, o item é incluído na tabela ITEMS.
INSERT_SALE_ITEM aparece na Listagem 33.2.
PART_NO,
QTY)
VALUES(
:iSALE_NUMBER,
:iPART_NO,
:iQTY);
END
END
NOTA
Se você não estiver usando a ferramenta ISQL para incluir os metadados do banco de dados, terá mudar o
caracter de término. Como todas as instruções SQL dentro de um procedimento precisam terminar com um
sinal de ponto-e-vírgula (;) – que também é o caractere de término da SQL –, é preciso definir o caractere
de término da SQL para algum outro símbolo, para evitar conflitos. Faça isso usando o comando SET TERM.
Em SALES, você usará o símbolo de circunflexo como caractere de término. Esta linha de código SQL cha-
mará a mudança a seguir:
SET TERM ^ ;
Concedendo permissões
A última etapa na definição de um banco de dados é conceder permissão para as tabelas e procedimentos
armazenados a usuários em particular. Por simplicidade, você pode conceder a todos os usuários os direi-
tos SELECT e UPDATE sobre a tabela CUSTOMER com a seguinte instrução:
GRANT SELECT, UPDATE ON CUSTOMER TO PUBLIC WITH GRANT OPTION;
Como alternativa, você pode conceder todos os direitos à tabela SALES com esta instrução:
GRANT ALL ON SALE TO PUBLIC WITH GRANT OPTION;
A cláusula GRANT OPTION significa que aqueles que recebem acesso às tabelas também podem conceder
acesso para outros. As instruções GRANT usadas nas tabelas e procedimentos armazenados do Inventory
Manager são as seguintes:
/* Permissões concedidas para este banco de dados */
GRANT SELECT, UPDATE ON CUSTOMER TO PUBLIC WITH GRANT OPTION;
GRANT ALL ON SALES TO PUBLIC WITH GRANT OPTION;
GRANT ALL ON PART TO PUBLIC WITH GRANT OPTION;
GRANT ALL ON ITEMS TO PUBLIC WITH GRANT OPTION;
GRANT EXECUTE ON PROCEDURE INSERT_SALE TO PUBLIC;
GRANT EXECUTE ON PROCEDURE INSERT_SALE_ITEM TO PUBLIC;
unit SalesDM;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
DBTables, Db;
type
TDDGSalesDataModule = class(TDataModule)
qryCustomer: TQuery;
dbSales: TDatabase;
usqlCustomer: TUpdateSQL;
qryCustomerCUSTOMER_ID: TIntegerField;
qryCustomerFNAME: TStringField;
qryCustomerLNAME: TStringField;
qryCustomerCREDIT_LINE: TSmallintField;
qryCustomerWORK_ADDRESS: TStringField;
qryCustomerALT_ADDRESS: TStringField;
qryCustomerCITY: TStringField;
qryCustomerSTATE: TStringField;
qryCustomerZIP: TStringField;
qryCustomerWORK_PHONE: TStringField;
qryCustomerALT_PHONE: TStringField;
qryCustomerCOMMENTS: TMemoField;
qryCustomerCOMPANY: TStringField;
qryParts: TQuery;
usqlParts: TUpdateSQL;
qryPartsPART_NUMBER: TStringField;
qryPartsDESCRIPTION: TStringField;
qryPartsQUANTITY: TSmallintField;
qryPartsLIST_PRICE: TFloatField;
qryPartsRETAIL_PRICE: TFloatField;
qryPartsDEALER_PRICE: TFloatField;
qryPartsJOBBER_PRICE: TFloatField;
spInsertSaleItem: TStoredProc;
spInsertSale: TStoredProc;
qryTotalPrice: TQuery;
tblTempItems: TTable;
tblTempItemsPART_NUMBER: TStringField;
tblTempItemsDESCRIPTION: TStringField;
1088 tblTempItemsQUANTITY: TSmallintField;
Listagem 33.3 Continuação
tblTempItemsRETAIL_PRICE: TFloatField;
tblTempItemsTOTAL_PRICE: TFloatField;
qryTotalPriceSUMOFTOTAL_PRICE: TFloatField;
qrySale: TQuery;
dsCustomer: TDataSource;
qryItems: TQuery;
dsSale: TDataSource;
qrySaleSALE_NUMBER: TIntegerField;
qrySaleSALE_DATE: TDateTimeField;
qrySaleTOTAL_PRICE: TFloatField;
qryItemsDESCRIPTION: TStringField;
qryItemsQTY: TSmallintField;
qryCustomerSearch: TQuery;
procedure tblTempItemsBeforePost(DataSet: TDataSet);
procedure dbSalesLogin(Database: TDatabase; LoginParams: TStrings);
protected
procedure SetAfterTempItemsChange(Value: TDataSetNotifyEvent);
function GetAfterTempItemsChange: TDataSetNotifyEvent;
public
// Métodos de conexão
procedure Logout;
function Login: Boolean;
function Connect: Boolean;
procedure Disconnect;
// Métodos do cliente
procedure FirstCustomer;
procedure LastCustomer;
procedure NextCustomer;
procedure PrevCustomer;
procedure EditCustomer;
procedure NewCustomer;
procedure AcceptCustomer;
procedure CancelCustomer;
procedure DeleteCustomer;
function IsFirstCustomer: Boolean;
function IsLastCustomer: Boolean;
function GetCustomerName: String;
function SearchForCustomer: Boolean;
// Métodos de peças
procedure FirstPart;
procedure LastPart;
procedure NextPart;
procedure PrevPart;
procedure EditPart;
procedure NewPart;
procedure AcceptPart;
procedure CancelPart;
procedure DeletePart;
function IsFirstPart: Boolean;
1089
Listagem 33.3 Continuação
// Métodos de vendas
procedure AddItemToSale;
procedure SaveSale;
procedure CancelSale;
function SaleItemsTotalPrice: double;
procedure OpenTempItems;
procedure CloseTempItems;
end;
var
DDGSalesDataModule: TDDGSalesDataModule;
implementation
{$R *.DFM}
procedure TDDGSalesDataModule.SetAfterTempItemsChange(Value:
TDataSetNotifyEvent);
begin
{ Este método escrito inclui o parâmetro Value nos eventos AfterPost e
AfterDelete da tabela temporária de itens. Isso garante que, quando os
dados mudarem, o manipulador de evento será chamado. }
tblTempItems.AfterPost := Value;
tblTempItems.AfterDelete := Value;
end;
// Métodos de login.
procedure TDDGSalesDataModule.Logout;
begin
Disconnect;
end;
procedure TDDGSalesDataModule.Disconnect;
begin
// Desconecta do banco de dados.
dbSales.Connected := False;
end;
// Métodos do cliente
procedure TDDGSalesDataModule.AcceptCustomer;
begin
dbSales.ApplyUpdates([qryCustomer]);
end;
procedure TDDGSalesDataModule.CancelCustomer;
begin
qryCustomer.CancelUpdates;
end;
procedure TDDGSalesDataModule.DeleteCustomer;
begin
qryCustomer.Delete;
end;
1091
Listagem 33.3 Continuação
procedure TDDGSalesDataModule.EditCustomer;
begin
qryCustomer.Edit;
end;
procedure TDDGSalesDataModule.FirstCustomer;
begin
qryCustomer.First;
end;
procedure TDDGSalesDataModule.LastCustomer;
begin
qryCustomer.Last;
end;
procedure TDDGSalesDataModule.NewCustomer;
begin
qryCustomer.Insert;
end;
procedure TDDGSalesDataModule.NextCustomer;
begin
qryCustomer.Next;
end;
procedure TDDGSalesDataModule.PrevCustomer;
begin
qryCustomer.Prior;
end;
CustID: Integer;
SearchQry: String;
begin
// Considera falha.
Result := False;
{ Chama a função SearchCustomer que está definida em CustomerSrchFrm.pas.
Essa função retorna a string de consutla que é incluída no componente
qryCustomerSearch de TQuery. }
SearchQry := SearchCustomer;
if SearchQry < > EmptyStr then
begin
Screen.Cursor := crSQLWait;
try
qryCustomerSearch.Close;
qryCustomerSearch.SQL.Clear;
qryCustomerSearch.SQL.Add(SearchQry);
qryCustomerSearch.Open;
try
// Métodos de peças
procedure TDDGSalesDataModule.AcceptPart;
begin
dbSales.ApplyUpdates([qryParts]);
end;
procedure TDDGSalesDataModule.CancelPart;
begin
qryParts.CancelUpdates;
end;
procedure TDDGSalesDataModule.DeletePart;
begin
qryParts.Delete;
end;
procedure TDDGSalesDataModule.EditPart;
begin
qryParts.Edit;
end;
procedure TDDGSalesDataModule.FirstPart;
begin
qryParts.First;
end;
procedure TDDGSalesDataModule.LastPart;
begin
qryParts.Last;
end;
procedure TDDGSalesDataModule.NewPart;
begin
qryParts.Insert;
end;
procedure TDDGSalesDataModule.NextPart;
begin
qryParts.Next;
end;
procedure TDDGSalesDataModule.PrevPart;
begin
qryParts.Prior;
end;
var
PartNumber: string;
begin
Result := False;
PartNumber := ‘’;
if InputQuery(‘Part Search’, ‘Enter a Part Number’, PartNumber) then
if not qryParts.Locate(‘PART_NUMBER’, PartNumber, [ ]) then
Exit
else
Result := True;
end;
// Métodos de vendas
procedure TDDGSalesDataModule.AddItemToSale;
begin
{ A tabela tblTempItems é uma tabela temporária usada para conter os itens
que estão sendo incluídos em uma venda. Se o usuário salvar a venda,
esses registros serão usados nas chamadas do procedimento armazenado
que realmente armazenam a venda no banco de dados. }
if not tblTempItems.Locate(‘PART_NUMBER’,
qryParts.FieldByName(‘PART_NUMBER’).AsString, [ ]) then
begin
tblTempItems.Insert;
try
tblTempItems[‘PART_NUMBER’] := qryParts[‘PART_NUMBER’];
tblTempItems[‘DESCRIPTION’] := qryParts[‘DESCRIPTION’];
tblTempItems[‘QUANTITY’] := 1;
tblTempItems[‘RETAIL_PRICE’] := qryParts[‘RETAIL_PRICE’];
tblTempItems.Post;
except
tblTempItems.Cancel;
end;
end
else
MessageDlg(‘Item already in list’, mtWarning, [mbok], 0);
end;
procedure TDDGSalesDataModule.CancelSale;
begin
{ Se o usuário cancelar a venda, os itens que foram incluíds na tabela
tblTempItems terão de ser apagados. }
tblTempItems.Close;
tblTempItems.EmptyTable;
tblTempItems.Open;
end;
procedure TDDGSalesDataModule.SaveSale;
var
SaleNo: Integer;
begin
{ Se o usuário salvar a venda, primeiro cria um registro da venda, que
retornará uma chave da venda para SaleNo. Isso é usado como vínculo para
1095
Listagem 33.3 Continuação
dbSales.Commit;
tblTempItems.Close;
tblTempItems.EmptyTable;
tblTempItems.Open;
except
dbSales.Rollback;
end;
end;
procedure TDDGSalesDataModule.OpenTempItems;
begin
tblTempItems.Close;
tblTempItems.EmptyTable;
tblTempItems.Open;
end;
procedure TDDGSalesDataModule.CloseTempItems;
begin
tblTempItems.Active := False;
end;
end.
Métodos de conexão Métodos que permitem ao usuário se conectar e desconectar com a aplicação.
Métodos de cliente Métodos que manipulam dados especificamente de cliente.
Métodos de peças Métodos que manipulam dados especificamente de peças.
Métodos de vendas Métodos que criam e gerenciam vendas.
1097
Consulte os comentários na listagem para ver uma explicação sobre os diversos métodos. Em parti-
cular, examine o método SaveSale( ), que é o método que utiliza o componente TStoredProc para criar
uma nova venda e inclui itens de vendas a essa venda. Os procedimentos armazenados nesse método são
ligados aos procedimentos armazenados que aparecem nas Listagens 33.1 e 33.2.
Métodos Login/Logout
Os métodos para conectar e desconectar são apropriadamente chamados Login( ) e Logout( ). Login( )
chama o método Connect( ), que estabelece uma conexão com o banco de dados através de dbSales. Ele faz
isso definindo a propriedade dbSales.Connected como True. Quando isso acontece, o manipulador de even-
to dbSales.OnLogin é chamado, se existir. O manipulador de evento dbSalesLogin( ) chama o método GetLo-
ginParams( ) deifnido em LoginFrm.pas, que preenche as informações de login do usuário, exibindo uma cai-
xa de diálogo de login personalizada. Esse método aparece na Listagem 33.4.
interface
type
TLoginForm = class(TForm)
lblEnterPassword: TLabel;
lblEnterName: TLabel;
edtName: TEdit;
edtPassword: TEdit;
btnOK: TButton;
btnCancel: TButton;
public
end;
implementation
{$R *.DFM}
end.
1098
O método Logout( ) simplesmente fecha dbSales, que por sua vez fecha todas as conexões de TQu-
ery/TTable.
Métodos de vendas
Os métodos de vendas são o local no qual as coisas ficam um pouco mais interessantes. Esses métodos re-
presentam mais o que você estaria fazendo para realizar diversas operações contra um banco de dados
cliente/servidor – em particular, o método SaveSale( ).
AddItemToSale( ) permite que o usuário especifique os itens a incluir em uma nova venda (ver Figura 33.2).
Esse valor é usado mais tarde para cada registro incluído na tabela ITEMS, através do componente de
TStoredProc, spInsertSaleItems. É assim que você vincula os itens sendo vendidos com uma venda.
Essa instrução retorna a soma dos valores numéricos para a coluna especificada – nesse caso, a colu-
na RETAIL_PRICE.
O método tblTempItemsBeforePost( ) é o manipulador de evento para o evento tblTempParts.Before-
Post. Esse manipulador de evento garante que o registro sendo postado reflete o preço correto com base
na quantidade de itens sendo vendidos. Isso é possível porque o evento BeforePost ocorre antes que o re-
gistro seja realmente postado na tabela.
TMainForm
1 1
1 1
1 1 1 1
1101
Listagem 33.5 O formulário principal da aplicação de estoque TMainForm
unit MainFrm;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
Menus, StdCtrls, ComCtrls, ExtCtrls, ChildFrm;
type
TMainForm = class(TForm)
mmSales: TMainMenu;
mmiScreen: TMenuItem;
mmiCustomer: TMenuItem;
mmiParts: TMenuItem;
mmiNewSale: TMenuItem;
mmiSales: TMenuItem;
mmiFile: TMenuItem;
mmiExit: TMenuItem;
mmiHelp: TMenuItem;
tcMain: TTabControl;
mmiUser: TMenuItem;
mmiLogon: TMenuItem;
mmiLogoff: TMenuItem;
imgCar: TImage;
procedure ScreenClick(Sender: TObject);
procedure mmiExitClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure tcMainChange(Sender: TObject);
procedure tcMainChanging(Sender: TObject; var AllowChange: Boolean);
procedure mmiLogonClick(Sender: TObject);
procedure mmiLogoffClick(Sender: TObject);
private
// ActiveScreenType armazena o tipo de formulário que está ativo.
ActiveScreenType: TActiveScreenType;
// ActiveScreen é uma referência ao formulário filho ativo.
ActiveScreen: TChildForm;
procedure SetActiveScreen;
public
{ Declarações públicas }
end;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
TMenuItem(Sender).Checked := True;
if Sender = mmiCustomer then
ActiveScreenType := acCustomer
else if Sender = mmiParts then
ActiveScreenType := acParts
else if Sender = mmiSales then
ActiveScreenType := acSales
else if Sender = mmiNewSale then
ActiveScreenType := acNewSales;
// Ensure the TTabControl is in-sync with the clicked item on the menu.
tcMain.TabIndex := ord(ActiveScreenType);
SetActiveScreen;
end
end;
end;
end;
begin
case tcMain.TabIndex of
0: mmiCustomer.Checked := True;
1: mmiParts.Checked := True;
2: mmiSales.Checked := True;
3: mmiNewSale.Checked := True;
end;
ActiveScreenType := TActiveScreenType(tcMain.TabIndex);
SetActiveScreen;
end;
end;
procedure TMainForm.SetActiveScreen;
{ Este método muda a tela ativa para um dos quatro formulários filhos. Cada
formulário filho se torna um filho do tcMain TtabControl. }
var
TempScreen: TChildForm;
begin
{ Determina se já temos um formulário filho instanciado. Se tivermos,
separa seu menu e libera o formulário filho. }
TempScreen := ActiveScreen;
end;
{ Determina qual tela ativa (formulário filho) será criada e define sua
barra de ferramentas para ter o formulário principal como pai, se for
apropriado.}
case ActiveScreenType of
acCustomer:
begin
ActiveScreen := TCustomerForm.Create(Application, tcMain);
TCustomerForm(ActiveScreen).SetToolBarParent(self);
end;
acParts:
begin
ActiveScreen := TPartsForm.Create(Application, tcMain);
TPartsForm(ActiveScreen).SetToolBarParent(self);
end;
acSales:
ActiveScreen := TSalesForm.Create(Application, tcMain);
acNewSales:
begin
ActiveScreen := TNewSalesForm.Create(Application, tcMain);
TPartsForm(ActiveScreen).SetToolBarParent(self);
end;
1104 end;
Listagem 33.5 Continuação
end;
if Assigned(TempScreen) then
TempScreen.Free;
end;
tcMain.Visible := False;
DDGSalesDataModule.Logout;
mmiScreen.Enabled := False;
mmiLogon.Enabled := True;
mmiLogoff.Enabled := False;
end;
end.
1105
Consulte os comentários dentro da listagem do formulário principal (ver Listagem 33.5) para obter
os detalhes sobre cada método. O núcleo do código dessa aplicação está em DDGSalesDataModule (já discuti-
do). Os formulários filhos contêm a maior parte da lógica com relação à interface do usuário. veremos
isso em seguida.
unit CustomerFrm;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
DBNAVSTATFRM, StdCtrls, DBCtrls, Mask, Menus, ImgList, ComCtrls, ToolWin,
Db, DBModeFrm;
type
TCustomerForm = class(TDBNavStatForm)
lblFirstName: TLabel;
dbeFirstName: TDBEdit;
lblLastName: TLabel;
dbeLastName: TDBEdit;
lblCreditLine: TLabel;
dbeCreditLine: TDBEdit;
lblWorkAddress: TLabel;
dbeWorkAddress: TDBEdit;
lblHomeAddress: TLabel;
dbeHomeAddress: TDBEdit;
lblCity: TLabel;
dbeCity: TDBEdit;
1106 lblState: TLabel;
Listagem 33.6 Continuação
dbeState: TDBEdit;
lblZipCode: TLabel;
dbeZip: TDBEdit;
lblWorkPhone: TLabel;
dbeWorkPhone: TDBEdit;
lblHomePhone: TLabel;
dbeHomePhone: TDBEdit;
lblComments: TLabel;
dbmmComments: TDBMemo;
lblCompany: TLabel;
dbeCompany: TDBEdit;
dsCustomer: TDataSource;
EXit1: TMenuItem;
procedure sbFirstClick(Sender: TObject);
procedure sbPrevClick(Sender: TObject);
procedure sbNextClick(Sender: TObject);
procedure sbLastClick(Sender: TObject);
procedure sbInsertClick(Sender: TObject);
procedure sbEditClick(Sender: TObject);
procedure sbDeleteClick(Sender: TObject);
procedure sbCancelClick(Sender: TObject);
procedure sbAcceptClick(Sender: TObject);
procedure FormShow(Sender: TObject);
procedure sbFindClick(Sender: TObject);
procedure sbBrowseClick(Sender: TObject);
private
procedure SetNavButtons;
public
function GetFormMenu: TMainMenu; override;
function CanChange: Boolean; override;
end;
var
CustomerForm: TCustomerForm;
implementation
uses SalesDM;
{$R *.DFM}
procedure TCustomerForm.SetNavButtons;
begin
// Os botões de navegação são definidos de acordo com o modo do formulário
sbFirst.Enabled := not DDGSalesDataModule.IsFirstCustomer;
sbLast.Enabled := not DDGSalesDataModule.IsLastCustomer;
sbPrev.Enabled := not DDGSalesDataModule.IsFirstCustomer;
sbNext.Enabled := not DDGSalesDataModule.IsLastCustomer;
1107
Listagem 33.6 Continuação
mmiPrevious.Enabled := sbPrev.Enabled;
mmiNext.Enabled := sbNext.Enabled;
end;
inherited;
DDGSalesDataModule.DeleteCustomer;
end;
end.
1109
Veja mais explicações no comentário da listagem dos métodos específicos. A pequena quantidade
de código necessária para esse formulário é possível porque a maior parte da lógica de banco de dados
existe em TDDGSalesDataModule, sem falar no que é tratado para você pela VCL. Os outros formulários tam-
bém são muito elegantes.
unit PartsFrm;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
DBNAVSTATFRM, Menus, ImgList, ComCtrls, ToolWin, Grids, DBGrids, Db,
StdCtrls, Mask, DBCtrls, DBModeFrm;
type
TPartsForm = class(TDBNavStatForm)
lblPartNo: TLabel;
dbePartNo: TDBEdit;
dsParts: TDataSource;
lblDescription: TLabel;
dbeDescription: TDBEdit;
lblQuantity: TLabel;
dbeQuantity: TDBEdit;
lblListPrice: TLabel;
dbeListPrice: TDBEdit;
lblRetailPrice: TLabel;
dbeRetailPrice: TDBEdit;
lblDealerPrice: TLabel;
dbeDealerPrice: TDBEdit;
lblJobberPrice: TLabel;
dbeJobberPrice: TDBEdit;
1110 dbgParts: TDBGrid;
Listagem 33.7 Continuação
var
PartsForm: TPartsForm;
implementation
uses SalesDM;
{$R *.DFM}
procedure TPartsForm.SetNavButtons;
begin
// Os botões de navegação são definidos de acordo com o modo do formulário.
sbFirst.Enabled := not DDGSalesDataModule.IsFirstPart;
sbLast.Enabled := not DDGSalesDataModule.IsLastPart;
sbPrev.Enabled := not DDGSalesDataModule.IsFirstPart;
sbNext.Enabled := not DDGSalesDataModule.IsLastPart;
end;
inherited;
DDGSalesDataModule.LastPart;
SetNavButtons;
end;
end.
Você verá, pela listagem, que isso é quase idêntico a TCustomerForm. Esse tipo de coerência é um atri-
buto desejado e que torna o código mais fácil de se entender.
unit NewSalesFrm;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
CHILDFRM, Grids, DBGrids, Buttons, StdCtrls, Db, Menus, ToolWin, ComCtrls,
1114 ImgList;
Listagem 33.8 Continuação
type
TNewSalesForm = class(TChildForm)
dsParts: TDataSource;
dsTempItems: TDataSource;
lblCustomer1: TLabel;
lblCustomerName: TLabel;
lblTotCost: TLabel;
lblTotalCost: TLabel;
lblSelectParts: TLabel;
sbAddPart: TSpeedButton;
sbRemovePart: TSpeedButton;
lblSaleItems: TLabel;
dbgParts: TDBGrid;
dbgSaleItems: TDBGrid;
mmFormMenu: TMainMenu;
mmiSales: TMenuItem;
mmiNew: TMenuItem;
mmiCancel: TMenuItem;
mmiSave: TMenuItem;
tbSales: TToolBar;
sbAccept: TToolButton;
sbCancel: TToolButton;
tb1: TToolButton;
sbInsert: TToolButton;
ilNavigationBar: TImageList;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure FormShow(Sender: TObject);
procedure sbAddPartClick(Sender: TObject);
procedure mmiNewClick(Sender: TObject);
procedure mmiCancelClick(Sender: TObject);
procedure mmiSaveClick(Sender: TObject);
private
AddingSale: Boolean;
procedure SetSaleMenus;
procedure TempItemsAfterChange(DataSet: TDataSet);
public
function CanChange: Boolean; override;
function GetFormMenu: TMainMenu; override;
procedure SetToolBarParent(AParent: TWinControl);
end;
var
NewSalesForm: TNewSalesForm;
implementation
uses SalesDM;
{$R *.DFM}
begin
Result := not AddingSale;
end;
inherited;
AddingSale := True;
SetSaleMenus;
end;
procedure TNewSalesForm.SetSaleMenus;
begin
// Define itens de menu e speedbuttons para refletir o modo do formulário.
mmiNew.Enabled := not AddingSale;
mmiCancel.Enabled := AddingSale;
mmiSave.Enabled := AddingSale;
sbAddPart.Enabled := AddingSale;
sbRemovePart.Enabled := AddingSale;
sbAccept.Enabled := mmiSave.Enabled;
sbCancel.Enabled := mmiCancel.Enabled;
sbInsert.Enabled := mmiNew.Enabled;
end;
end.
unit CustomerSrchFrm;
interface
type
TCustomerSearchForm = class(TForm)
lblIDNumber: TLabel;
edtIDNumber: TEdit;
lblFirstName: TLabel;
lblLastName: TLabel;
lblAltPhone: TLabel;
lblWorkPhone: TLabel;
lblWorkAddress: TLabel;
lblAltAddress: TLabel;
lblCompany: TLabel;
edtFirstName: TEdit;
1118
Listagem 33.9 Continuação
edtLastName: TEdit;
edtWorkPhone: TEdit;
edtAltPhone: TEdit;
edtWorkAddress: TEdit;
edtAltAddress: TEdit;
edtCompany: TEdit;
btnCancel: TButton;
btnFind: TButton;
lblInstruction: TLabel;
procedure FormCreate(Sender: TObject);
procedure FindCustBtnClick(Sender: TObject);
procedure CancelBtnClick(Sender: TObject);
procedure lblIDNumberClick(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
private
FindPressed: Boolean;
procedure ClearEditFields;
function BuildSQLStatement: string;
public
QueryString: String;
end;
implementation
{$R *.DFM}
uses Dialogs;
end;
var
Sep: String[3]; // Used as a seperator.
begin
Sep := ‘’;
Result := ‘’;
end;
procedure TCustomerSearchForm.ClearEditFields;
{ Este método apaga todos os campos de edição e define seus labels para
a cor clNavy. }
var
i: word;
begin
for i := 0 to ComponentCount - 1 do
begin
if Components[i] is TEdit then
TEdit(Components[i]).Text := ‘’;
end;
end.
O método principal para examinarmos aqui é a função BuildSQLStatement( ), que retorna uma string
representando a instrução de consulta SQL. Esse método verifica cada um dos labels e, se sua cor for
clRed, usa seu controle de edição correspondente para montar uma instrução de consulta usando uma sé-
rie de instruções Format( ).
ClearEditFields( ) é um método simples, usado para definir todos os labels como clNavy e apagar o
conteúdo dos controles de edição. Esse método é usado quando o formulário é criado, no manipulador
de evento FormCreate( ).
O manipulador de evento FormClose( ) garante que o usuário tenha especificado campos para reali-
zar a pesquisa, verificando se QueryString não está vazia. Somente se um campo foi selecionado, Query-
String terá uma instrução SQL válida. Além disso, o método permite que o formulário seja fechado, não
importa os campos especificados pelo usuário, se este der um clique no botão Cancel. Isso é determinado
pelo valor da variável Booleana FindPressed, que é definida como True quando o botão Find é acionado.
Se Find for acionado, a instrução SQL será retornada para o formulário que chamou.
Resumo
Isso conclui a aplicação Inventory Manager. Este capítulo ilustra como você projetaria uma aplicação cli-
ente/servidor em duas camadas. O modelo de duas camadas provavelmente compõe a maioria dos siste-
mas cliente/servidor. Apesar disso, com o advento da Internet e das tecnologias relacionadas, o modelo
de três camadas está se tornando mais popular, e é o tópicos dos próximos capítulos.
1122
Desenvolvimento CAPÍTULO
MIDAS para
rastreamento de clientes
34
NE STE C AP ÍT UL O
l Projeto da aplicação servidora 1124
l Projeto da aplicação cliente 1126
l Resumo 1142
No capítulo anterior, discutimos sobre as técnicas para o desenvolvimento de aplicações em duas cama-
das. Neste capítulo, criaremos uma aplicação em três camadas, usando a tecnologia MIDAS, apresentada
no Capítulo 32. O foco deste capítulo é ilustrar a simplicidade de uso dos componentes MIDAS, além do
modelo de porta-arquivos (briefcase) para o trabalho fora do site.
A aplicação que iremos desenvolver serve bem para o modelo de porta-arquivos. A aplicação é um
rastreador ou gerenciador de clientes. Normalmente, os representantes de vendas realizam grande parte
do seu trabalho nos clientes, possivelmente viajando para vários locais diferentes. A lista de clientes com
que esses representantes trabalham pode ser crítica para eles mesmos e para a empresa que representam.
Portanto, essas informações de clientes provavelmente deverão residir no site da empresa. Assim, como
os representantes de vendas podem utilizar esses dados sem que precisem utilizar uma conexão de rede?
Além disso, como eles podem atualizar os dados da empresa com informações mais recentes, que prova-
velmente apanharão no escritório do cliente?
TClientDataSet possibilita a criação de aplicações de porta-arquivos com sua implementação do ca-
ching interno, para que os representantes de vendas façam o download dos dados, ou ainda de um sub-
conjunto dos dados, para poderem trabalhar com eles externamente. Mais adiante, quando eles retorna-
rem ao escritório da sede, poderão fazer o upload de quaisquer mudanças feitas no banco de dados. Nes-
te capítulo, montaremos uma ferramenta simples de gerenciamento de clientes, que ilustra esse método
de montagem de aplicações de porta-arquivos.
unit CustRDM;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
ComServ, ComObj, VCLCom, StdVcl, DataBkr, DBClient, CustServ_TLB,
Db, DBTables, Provider;
type
TCustomerRemoteDataModule = class(TRemoteDataModule,
ICustomerRemoteDataModule)
ssnCust: TSession;
dbCust: TDatabase;
qryCust: TQuery;
prvCust: TDataSetProvider;
1124
Listagem 34.1 Continuação
private
FFilterStr: String;
FFilterType: TFilterType;
public
{ Declarações públicas }
protected
procedure FilterByCity(const ACity: WideString; out Data: OleVariant);
safecall;
procedure FilterByState(const AStateStr: WideString; out Data: OleVariant);
safecall;
procedure NoFilter(out Data: OleVariant); safecall;
end;
var
CustomerRemoteDataModule: TCustomerRemoteDataModule;
implementation
{$R *.DFM}
procedure TCustomerRemoteDataModule.FilterByState(
const AStateStr: WideString; out Data: OleVariant);
begin
FFilterType := ftState;
FFilterStr := AStateStr;
qryCust.Close;
qryCust.SQL.Clear;
qryCust.SQL.Add(Format(‘select * from CUSTOMER where STATE = “%s”’,
[AStateStr]));
qryCust.Open;
Data := prvCust.Data;
end;
end;
initialization
TComponentFactory.Create(ComServer, TCustomerRemoteDataModule,
Class_CustomerRemoteDataModule, ciMultiInstance, tmSingle);
end.
A Listagem 34.1 mostra três métodos que foram incluídos em TCustomerDataModule. Esses métodos fo-
ram realmente incluídos na interface ICustomerRemoteDateModule através do Type Library Editor (editor da
biblioteca de tipos), que por sua vez criou os métodos de implementação para a classe TCustomerRemoteDa-
taModule (ver Figura 34.1). No Type Library Editor, definimos os métodos e seus parâmetros, e depois in-
cluímos o código em cada método de implementação criado pelo Delphi. Você pode examinar o códi-
go-fonte para a biblioteca de tipos no arquivo CustServ_TLB.pas.
NOTA
Esta demonstração foi convertida de outra, escrita para o Delphi 4. Para transportar servidores MIDAS do
Delphi 4 para o Delphi 5, você precisa realizar algumas etapas. Essas etapas são detalhadas na ajuda
on-line, sob o título “Converting MIDAS Applications” (conversão de aplicações MIDAS).
Os métodos FilterByCity( ) e FilterByState( ) são usados para permitir que o cliente faça o down-
load de um subconjunto da tabela inteira. Isso faz sentido, pois pode não ser necessário fazer o download
da lista de clientes inteira quando o representante de vendas está viajando para um único local. Esses dois
métodos apanham um parâmetro de string que é usado para especificar o valor do filtro. NoFilter( ) re-
move qualquer filtro aplicado à tabela.
Esses métodos fazem a filtragem no lado do servidor, pois oferecem um meio de limitar os registros
retornados ao cliente. Como alternativa, o usuário pode querer realizar a filtragem no lado do cliente.
Ou seja, o representante de vendas pode querer acessar o conjunto de resultados inteiro, mas poder fil-
trar os registros desejados conforme a necessidade. Ilustraremos as duas técnicas.
unit CustDM;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
DBClient, MConnect, Db;
const
cFileName = ‘CustData.cds’;
type
TCustomerDataModule = class(TDataModule)
cdsCust: TClientDataSet;
dcomCust: TDCOMConnection;
cdsCustCUSTOMER_ID: TIntegerField;
cdsCustFNAME: TStringField;
cdsCustLNAME: TStringField;
cdsCustCREDIT_LINE: TSmallintField;
cdsCustWORK_ADDRESS: TStringField;
cdsCustALT_ADDRESS: TStringField;
cdsCustCITY: TStringField;
cdsCustSTATE: TStringField;
cdsCustZIP: TStringField;
cdsCustWORK_PHONE: TStringField;
cdsCustALT_PHONE: TStringField;
cdsCustCOMMENTS: TMemoField;
cdsCustCOMPANY: TStringField;
procedure CustomerDataModuleCreate(Sender: TObject);
procedure cdsCustReconcileError(DataSet: TClientDataSet;
E: EReconcileError; UpdateKind: TUpdateKind;
1127
Listagem 34.2 Continuação
public
procedure EditClient;
procedure AddClient;
procedure SaveClient;
procedure CancelClient;
procedure DeleteClient;
procedure ApplyUpdates;
procedure CancelUpdates;
procedure First;
procedure Previous;
procedure Next;
procedure Last;
function IsBOF: Boolean;
function IsEOF: Boolean;
procedure FilterByState;
procedure FilterByCity;
procedure NoFilter;
end;
var
CustomerDataModule: TCustomerDataModule;
implementation
uses MainCustFrm, Registry;
{$R *.DFM}
procedure TCustomerDataModule.AddClient;
begin
cdsCust.Insert;
1128 end;
Listagem 34.2 Continuação
procedure TCustomerDataModule.ApplyUpdates;
begin
cdsCust.ApplyUpdates(-1);
end;
procedure TCustomerDataModule.CancelClient;
begin
cdsCust.Cancel;
end;
procedure TCustomerDataModule.CancelUpdates;
begin
cdsCust.CancelUpdates;
end;
procedure TCustomerDataModule.DeleteClient;
begin
if MessageDlg(‘Are you sure you want to delete the current record?’,
mtConfirmation, [mbYes, mbNo], 0) = mrYes then
cdsCust.Delete;
end;
procedure TCustomerDataModule.EditClient;
begin
cdsCust.Edit;
end;
procedure TCustomerDataModule.First;
begin
cdsCust.First;
end;
procedure TCustomerDataModule.Last;
begin
cdsCust.Last;
end;
procedure TCustomerDataModule.Next;
begin
cdsCust.Next;
end;
procedure TCustomerDataModule.Previous;
1129
Listagem 34.2 Continuação
begin
cdsCust.Prior;
end;
procedure TCustomerDataModule.SaveClient;
begin
cdsCust.Post;
end;
procedure TCustomerDataModule.cdsCustReconcileError(
DataSet: TClientDataSet; E: EReconcileError; UpdateKind: TUpdateKind;
var Action: TReconcileAction);
{ Se houver um erro, atualiza a TListview apropriada no formulário principal
com os dados de erro. }
var
CurStr, NewStr, OldStr: String;
i: integer;
V: Variant;
if VarIsNull(V) then
S := EmptyStr
else
S := String(V);
end;
begin
for i := 0 to DataSet.FieldCount - 1 do
begin
V := DataSet.Fields[i].NewValue;
SetString(V, NewStr);
V := DataSet.Fields[i].CurValue;
SetString(V, CurStr);
V := DataSet.Fields[i].OldValue;
SetString(V, OldStr);
1130
Listagem 34.2 Continuação
end
else begin
cdsCust.FileName := cFileName;
dcomCust.Connected := False;
end;
end;
if IsOnline then
begin
dcomCust.Connected := True;
cdsCust.Open;
end
else begin
cdsCust.FileName := cFileName;
cdsCust.Open;
end;
end; 1131
Listagem 34.2 Continuação
procedure TCustomerDataModule.FilterByCity;
{ Se estivermos on-line, deixa que o servidor aplique o filtro, para só
recebermos os registros desejados. Caso contrário, aplica o filtro ao
conjunto de resultados de cdsCust na memória. }
var
CityStr: String;
Data: OleVariant;
begin
InputQuery(‘Filter on City’, ‘Enter City: ‘, CityStr);
FFilterStr := CityStr;
if Online then
begin
dcomCust.AppServer.FilterByCity(CityStr, Data);
cdsCust.Refresh;
end
else begin
FFilterType := ftByCity;
cdsCust.Filtered := True;
cdsCust.Refresh;
end;
end;
procedure TCustomerDataModule.FilterByState;
{ Se estivermos on-line, deixa o servidor aplicar o filtro, para apanhar
somente os registros que queremos. Caso contrário, aplica o filtro ao
conjunto de resultados de cdsCust na memória. }
var
StateStr: String;
Data: OleVariant;
begin
InputQuery(‘Filter on State’, ‘Enter State: ‘, StateStr);
FFilterStr := StateStr;
if Online then
begin
dcomCust.AppServer.FilterByState(StateStr, Data);
cdsCust.Refresh;
1132
Listagem 34.2 Continuação
end
else begin
FFilterType := ftByState;
cdsCust.Filtered := True;
cdsCust.Refresh;
end;
end;
procedure TCustomerDataModule.NoFilter;
{ Se estivermos on-line, deixa o servidor aplicar o filtro, para apanhar
somente os registros que queremos. Caso contrário, aplica o filtro ao
conjunto de resultados de cdsCust na memória. }
var
Data: OleVariant;
begin
if Online then
begin
dcomCust.AppServer.NoFilter(Data);
cdsCust.Refresh;
end
else begin
FFilterType := ftNone;
cdsCust.Filtered := False;
cdsCust.Refresh;
end;
end;
end.
Ligação inicial
A maior parte dos métodos que você encontra na Listagem 34.2 são métodos simples, que realizam a na-
vegação e a manipulação do dataset do cliente, cdsCust. Observe que incluímos um método no módulo de
dados para expor uma operação sobre cdsCust, em vez de permitir que quaisquer formulários o acessem
diretamente. Aqui, estamos apenas aderindo às metodologias estritas da OOP. Embora isso não seja ne-
cessário em Delphi, fazemos isso por coerência e para impor a centralização da lógica do banco de dados.
CustomerDataModule contém um objeto TDCOMConnection, dcomCust, e o objeto TClientDataSet, cdsCust. Dcom-
Cust está conectado à aplicação servidora por meio de sua propriedade ServerName, que é definida para
CustServ.CustomerRemoteDataModule.
CdsCust está vinculado a qryCust no módulo de dados remoto em sua propriedade ProviderName. Isso
cuida da ligação necessária para fazer o servidor e o cliente de uma aplicação MIDAS funcionarem. No
entanto, para obter o máximo dessa tecnologia, algum código terá de ser escrito. 1133
Reconciliação de erro
Depois que a aplicação cliente passar as mudanças de volta ao servidor, poderão ocorrer erros (especi-
almente no modelo de porta-arquivos, onde é possível que outro usuário tenha modificado um deter-
minado registro). O erro pode ser tratado no servidor ou no cliente. Se for tratado no cliente, o servi-
dor passará informações de erro de volta ao cliente através do manipulador OnReconcileError de TClient-
DataSet. Nesse manipulador de evento, várias opções estão disponíveis, e falaremos sobre elas em bre-
ve. A propriedade OnReconcileError refere-se a um método TReconcileErrorEvent, que é definido da se-
guinte forma:
TReconcileErrorEvent = procedure(DataSet: TClientDataSet; E:
åEReconcileError; UpdateKind: TUpdateKind; var Action: TReconcileAction)
åof object;
O parâmetro DataSet refere-se ao conjunto de dados (dataset) no qual o erro ocorreu. EReconcileError
é uma classe de exceção para erros de dataset do cliente. Você pode usar essa classe conforme usaria qual-
quer classe de exceção. UpdateKind pode ser qualquer um dos valores especificados na Tabela 34.1. Essa
informação vem da ajuda on-line do Delphi.
O parâmetro Action é do tipo TReconcileAction. Definindo o parâmetro Action como raRefresh, a apli-
cação do cliente cancela quaisquer mudanças feitas pelo usuário e atualiza sua cópia do resultado, para
que seja igual à cópia do servidor. Isso é o que é feito no exemplo. Outras opções para a propriedade
Action podem ser definidas para os valores mostrados na Tabela 34.2, que vem da ajuda on-line do Del-
phi (onde você também poderá procurar outras informações sobre reconciliação de erros).
1134
Dentro do manipulador OnReconcileError, você pode se referir às propriedades OldValue, NewValue e
CurValue para cada campo do dataset do cliente. Estas são discutidas no Capítulo 32.
O manipulador de evento OnReconcileError para cdsCust, cdsCustReconcileError( ), cuida de apanhar
os valores novo, antigo e atual de qualquer campo para o qual tenha sido retornado um erro ao cliente no
momento de uma atualização. Depois ele chama o método referenciado por FOnAddErrorToClient.
FOnAddErrorToClient é um ponteiro de método do tipo TAddErrorToClientEvent, que é definido no topo da
Listagem 34.2. Você verá, na nossa discussão sobre o formulário principal da aplicação, MainCustForm,
como implementamos um método TAddErrorToClientEvent e o atribuímos a FOnAddErrorToClient. Novamen-
te, este é outro exemplo de como tentamos manter o módulo de dados independente dos elementos da
interface do usuário.
NOTA
TClientDataSet.FileName é específico do Delphi 4 e 5. Se você estiver rodando o Delphi 3, poderá conse-
guir a mesma coisa chamando os métodos SaveToFile( ) e LoadFromFile( ) de TClientDataSet.
Filtrando registros
CustomerDataModule permite que o usuário filtre certos registros com base na cidade ou estado do cli-
ente. A filtragem no lado do cliente ocorre quando o status da aplicação é off-line. Quando o cliente está
on-line, o servidor pode realizar a filtragem. Uma coisa a observar é que, quando o cliente emite um filtro
estando on-line, somente os registros que faziam parte do filtro serão salvos localmente na máquina do
cliente, quando ele passar para off-line.
Os métodos FilterByCity( ) e FilterByState( ) chamam os métodos FilterbyCity( ) e FilterbyState( )
do servidor da aplicação, discutido anteriormente. Esses métodos são chamados apenas se o usuário esti-
ver on-line. Se o usuário estiver off-line, a filtragem será feita por meio da propriedade Filter e do mani-
pulador de evento OnFilterRecord de TClientDataSet.
1135
Listagem 34.3 MainCustFrm.pas – TMainCustForm
unit MainCustFrm;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
DBNAVSTATFRM, Db, StdCtrls, DBCtrls, Mask, ComCtrls, Menus, ImgList,
ToolWin, DBMODEFRM, Grids, DBGrids;
type
TMainCustForm = class(TDBNavStatForm)
pcClients: TPageControl;
dsClientDetail: TTabSheet;
lblFirstName: TLabel;
lblLastName: TLabel;
lblCreditLine: TLabel;
lblWorkAddress: TLabel;
lblAltAddress: TLabel;
lblCity: TLabel;
lblState: TLabel;
lblZipCode: TLabel;
lblWorkPhone: TLabel;
lblAltPhone: TLabel;
lblCompany: TLabel;
dbeFirstName: TDBEdit;
dbeLastName: TDBEdit;
dbeCreditLine: TDBEdit;
dbeWorkAddress: TDBEdit;
dbeAltAddress: TDBEdit;
dbeCity: TDBEdit;
dbeState: TDBEdit;
dbeZipCode: TDBEdit;
dbeWorkPhone: TDBEdit;
dbeAltPhone: TDBEdit;
dbeCompany: TDBEdit;
tsComments: TTabSheet;
dbmComments: TDBMemo;
dsClients: TDataSource;
SaveDialog1: TSaveDialog;
OpenDialog1: TOpenDialog;
mmiSave: TMenuItem;
N3: TMenuItem;
mmiApplyUpdates: TMenuItem;
mmiCancelUpdates: TMenuItem;
mmiMode: TMenuItem;
mmiOffline: TMenuItem;
mmiOnline: TMenuItem;
tsErrors: TTabSheet;
lvClient: TListView;
mmiExit: TMenuItem;
mmiFilter: TMenuItem;
mmiByState: TMenuItem;
1136
Listagem 34.3 Continuação
mmiByCity: TMenuItem;
mmiNoFilter: TMenuItem;
tsClientList: TTabSheet;
DBGrid1: TDBGrid;
procedure sbAcceptClick(Sender: TObject);
procedure sbCancelClick(Sender: TObject);
procedure sbInsertClick(Sender: TObject);
procedure sbEditClick(Sender: TObject);
procedure sbDeleteClick(Sender: TObject);
procedure sbFirstClick(Sender: TObject);
procedure sbPrevClick(Sender: TObject);
procedure sbNextClick(Sender: TObject);
procedure sbLastClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure mmiOnlineClick(Sender: TObject);
procedure mmiApplyUpdatesClick(Sender: TObject);
procedure mmiCancelUpdatesClick(Sender: TObject);
procedure dsClientsDataChange(Sender: TObject; Field: TField);
procedure Exit1Click(Sender: TObject);
procedure mmiExitClick(Sender: TObject);
procedure mmiByStateClick(Sender: TObject);
procedure mmiByCityClick(Sender: TObject);
procedure mmiNoFilterClick(Sender: TObject);
private
procedure SetControls;
procedure GoToOnlineMode;
procedure GoToOfflineMode;
public
procedure AddErrorToClient(const aFieldName, aOldValue, aNewValue,
aCurValue, aErrorStr: String);
end;
var
MainCustForm: TMainCustForm;
implementation
uses CustDM;
{$R *.DFM}
NewItem.SubItems.Add(aOldValue);
NewItem.SubItems.Add(aNewValue);
NewItem.SubItems.Add(aCurValue);
NewItem.SubItems.Add(aErrorStr);
end;
procedure TMainCustForm.SetControls;
begin
// Garante que as funções de navegação são definidas de acordo com o modo
do formulário. }
sbFirst.Enabled := not CustomerDataModule.IsBof;
sbLast.Enabled := not CustomerDataModule.IsEof;
sbPrev.Enabled := not CustomerDataModule.IsBof;
sbNext.Enabled := not CustomerDataModule.IsEof;
mmiOnline.Checked := CustomerDataModule.Online;
mmiOffline.Checked := not mmiOnline.Checked;
if CustomerDataModule.Online then
stbStatusBar.Panels[2].Text := ‘Working Online’
else
stbStatusBar.Panels[2].Text := ‘Working Offline’
end;
1138
Listagem 34.3 Continuação
inherited;
CustomerDataModule.CancelClient;
SetControls;
end;
procedure TMainCustForm.GoToOnlineMode;
begin
CustomerDataModule.Online := True;
SetControls;
end;
procedure TMainCustForm.GoToOfflineMode;
begin
CustomerDataModule.Online := False;
SetControls;
end;
if mi = mmiOnline then
begin
if mi.Checked then
GoToOnlineMode
else
GoToOffLineMode
end
else begin
if mi.Checked then
GoToOfflineMode
else
GoToOnlineMode
end;
end;
end;
1140
Listagem 34.3 Continuação
end.
1141
A maior parte dos métodos para TMainCustForm chama os métodos de CustomerDataModule.
Observe bem o método AddErrorToClient( ). Esse método serve como propriedade OnAddErrorToClient
de CustomerDataModule. O manipulador de evento OnCreate de TMainCustForm atribui esse método à proprieda-
de do módulo de dados. AddErrorToClient( ) acrescenta quaisquer eventos ao controle TListView no formu-
lário principal, para exame do usuário. Esse controle TListView apresenta o nome do campo, o valor anti-
go, o valor novo e os valores atuais para o erro. Ele também apresenta a string de erro.
O simples método SetControls( ) trata da configuração dos vários controles no formulário. Ele ga-
rante que os controles serão ativados ou desativados quando for apropriado. Os outros métodos são dis-
cutidos nos comentários do código-fonte.
Resumo
Embora o Client Tracker seja uma aplicação simples, a maior parte da ligação necessária para se criar
aplicações de três camadas aparece neste exemplo. Você também poderá descobrir que precisa imple-
mentar alguns outros detalhes, como callbacks ou pooling de conexão, conforme discutimos no Capítulo
32. A conclusão é esta: desenvolver três aplicações usando MIDAS não é mais difícil do que desenvolver
aplicações de banco de dados de duas camadas ou até mesmo de desktop.
1142
Ferramenta DDG para CAPÍTULO
relatório de bugs –
desenvolvimento de
aplicação de desktop
35
NE STE C AP ÍT UL O
l Requisitos gerais da aplicação 1144
l O modelo de dados 1145
l Desenvolvimento do módulo de dados 1145
l Desenvolvimento da interface do usuário 1159
l Como capacitar a aplicação para a Web 1166
l Resumo 1166
Este capítulo discute técnicas para desenvolver aplicações de banco de dados de desktop. A aplicação de
relatório de bugs DDG demonstra vários métodos para se levar em consideração, em particular, como
projetar uma aplicação que possa ser distribuída na Internet. Nessa demonstração, também ilustramos
várias técnicas e truques para contornar alguns problemas que aparecem quando se separa a interface do
usuário das rotinas de manipulação de dados.
Devido à facilidade de uso do Delphi, o desenvolvimento de aplicações de banco de dados é muito
simples. No entanto, também é fácil se esquecer aspectos que acabarão fazendo falta mais tarde, quando
quiser estender a funcionalidade básica da aplicação. Neste capítulo, vamos mostrar como levar isso em
consideração para criar suas aplicações de banco de dados.
Ações de bug
Os usuários podem incluir ações (notas) a um relatório de bugs existentes. Eles também podem nave-
gar pelas ações inseridas previamente por eles mesmos ou por outros usuários. Esse é um modo prático
de acompanhar o progresso da correção do bug e para que as partes interessadas façam anotações so-
bre um bug.
1144
Outra funcionalidade da IU
A aplicação precisa utilizar as técnicas necessárias para tornar a interface do usuário fácil de entender e
usar. Recursos como campos de pesquisa (lookup) e labels de exibição “amigável” serão usados onde for
necessário.
O modelo de dados
O modelo de dados para a aplicação de relatório de bugs aparece na Figura 35.1. Veja agora em que con-
sistem as tabelas do modelo:
l IDs.A tabela IDs serve como tabela de geração de chave e registra a próxima chave disponível
para as tabelas Users, Bugs e Actions.
l Users. A tabela Users armazena usuários que estão incluídos no sistema de relatório de bugs.
l Bugs. A tabela Bugs armazena as informações gerais sobre os bugs.
l Actions. A tabela Actions armazena notas sobre bugs. Cada bug pode ter várias notas.
l Status. A tabela Status é uma tabela de pesquisa para atribuir um status específico a cada bug.
Users
IDs
User ID LongPK
BugsID Long
ActionsID Long User Name Alpha(30) DK1
UsedID Long UserFirstName Alpha(30)
UserLastName Alpha(30)
???
Actions BUGS
has
Status
StatusID Long PK
StatusTitle Alpha(20)
unit DDGBugsDM;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
Db, DBTables, HTTPApp, DBWeb;
type
EUnableToObtainID = class(Exception);
TDDGBugsDataModule = class(TDataModule)
dbDDGBugs: TDatabase;
tblBugs: TTable;
tblUsers: TTable;
tblStatus: TTable;
tblActions: TTable;
tblBugsBugID: TIntegerField;
tblBugsWhenReported: TDateField;
tblBugsSummaryDescription: TStringField;
tblBugsDetails: TStringField;
tblBugsAffectedSource: TStringField;
tblBugsUserID: TIntegerField;
tblBugsStatusID: TIntegerField;
dsUsers: TDataSource;
dsStatus: TDataSource;
tblIDs: TTable;
tblBugsUserNameLookup: TStringField;
tblBugsAssignedToLookup: TStringField;
tblUsersUserID: TIntegerField;
tblUsersUserName: TStringField;
tblUsersUserFirstName: TStringField;
tblUsersUserLastName: TStringField;
tblBugsAssignedToUserID: TIntegerField;
dsBugs: TDataSource;
wbdpBugs: TWebDispatcher;
dstpBugs: TDataSetTableProducer;
procedure DDGBugsDataModuleCreate(Sender: TObject);
procedure tblBugsBeforePost(DataSet: TDataSet);
procedure tblBugsFilterRecord(DataSet: TDataSet; var Accept: Boolean);
1146 procedure tblUsersBeforePost(DataSet: TDataSet);
Listagem 35.1 Continuação
// Métodos de bugs
procedure FirstBug;
procedure LastBug;
procedure NextBug;
procedure PreviousBug;
function IsLastBug: Boolean;
function IsFirstBug: Boolean;
function IsBugsTblEmpty: Boolean;
procedure InsertBug;
procedure DeleteBug;
procedure EditBug;
procedure SaveBug;
procedure CancelBug;
procedure SearchForBug;
// Funções do usuário
{$IFNDEF DDGWEBBUGS}
procedure AddUser;
{$ENDIF}
// Métodos de ação
{$IFNDEF DDGWEBBUGS}
procedure AddAction;
{$ENDIF}
// Função de login
function Login: Boolean;
// Propriedades expostas
var
DDGBugsDataModule: TDDGBugsDataModule;
implementation
{$IFNDEF DDGWEBBUGS}
uses UserFrm, ActionFrm;
{$ENDIF}
{$R *.DFM}
// Funções auxiliadoras.
Stream := TMemoryStream.Create;
try
AMemoField.SaveToStream(Stream);
Stream.Seek(0, soFromBeginning);
AStrings.LoadFromStream(Stream);
finally
Stream.Free;
end;
end;
// Métodos internos
// Métodos de identificação
// Métodos de inicialização/login.
// Métodos de bug.
procedure TDDGBugsDataModule.FirstBug;
begin
tblBugs.First;
end;
procedure TDDGBugsDataModule.LastBug;
begin
tblBugs.Last;
end;
procedure TDDGBugsDataModule.NextBug;
begin
tblBugs.Next;
end;
procedure TDDGBugsDataModule.PreviousBug;
begin
tblBugs.Prior;
end;
procedure TDDGBugsDataModule.InsertBug;
begin
tblBugs.Insert;
end;
procedure TDDGBugsDataModule.DeleteBug;
var
Qry: TQuery;
BugID: Integer;
begin
if MessageDlg(‘Delete Action?’, mtConfirmation,
[mbYes, mbNo], 0) = mrYes then
begin
BugID := tblBugs.FieldByName(‘BugID’).AsInteger;
{ Usa um componente TQuery criado dinamicamente para realizar essas
1151
Listagem 35.1 Continuação
operações. }
Qry := TQuery.Create(self);
try
dbDDGBugs.StartTransaction;
try
// Primeiro exclui qualquer ação pertencente a este bug.
Qry.DatabaseName := dbDDGBugs.DataBaseName;
Qry.SQL.Add(Format(‘DELETE FROM ACTIONS WHERE BugID = %d’, [BugID]));
Qry.ExecSQL;
tblBugs.Refresh;
tblActions.Refresh;
dbDDGBugs.Commit;
except
dbDDGBugs.Rollback;
raise;
end;
finally
Qry.Free;
end;
end;
end;
procedure TDDGBugsDataModule.EditBug;
begin
tblBugs.Edit;
end;
procedure TDDGBugsDataModule.SaveBug;
begin
tblBugs.Post;
end;
procedure TDDGBugsDataModule.CancelBug;
begin
tblBugs.Cancel;
end;
procedure TDDGBugsDataModule.SearchForBug;
var
BugStr: String;
begin
InputQuery(‘Search for bug’, ‘Enter bug ID: ‘, BugStr);
if IsInteger(BugStr) then
if not tblBugs.Locate(‘BugID’, StrToInt(BugStr), [ ]) then
MessageDlg(‘Bug not found.’, mtInformation, [mbOK], 0);
end;
1152
Listagem 35.1 Continuação
// Métodos do usuário.
{$IFNDEF DDGWEBBUGS}
procedure TDDGBugsDataModule.AddUser;
begin
tblUsers.Insert;
try
if NewUserForm(PostUser) = mrCancel then
tblUsers.Cancel;
except
{ Houve um erro. Coloca a tabela no modo de navegação e gera a
exceção novamente. }
tblUsers.Cancel;
raise;
end;
end;
{$ENDIF}
{$IFNDEF DDGWEBBUGS}
procedure TDDGBugsDataModule.AddAction;
begin
NewActionForm(PostAction);
end;
{$ENDIF}
begin
Action := TStringList.Create;
try
with tblActions do
begin
tblActions.First;
1153
Listagem 35.1 Continuação
// Manipuladores de evento
end.
program DDGBugs;
uses
Forms,
Dialogs,
ChildFrm in ‘..\ObjRepos\CHILDFRM.pas’ {ChildForm},
DBModeFrm in ‘..\ObjRepos\DBMODEFRM.pas’ {DBModeForm},
DBNavStatFrm in ‘..\ObjRepos\DBNAVSTATFRM.pas’ {DBNavStatForm},
MainFrm in ‘MainFrm.pas’ {MainForm},
UserFrm in ‘UserFrm.pas’ {UserForm},
ActionFrm in ‘ActionFrm.pas’ {ActionForm},
DDGBugsDM in ‘..\Shared\DDGBugsDM.pas’ {DDGBugsDataModule: TDataModule};
{$R *.RES}
begin
Application.Initialize;
Application.CreateForm(TDDGBugsDataModule, DDGBugsDataModule);
if DDGBugsDataModule.Login then
begin
Application.CreateForm(TMainForm, MainForm);
Application.Run;
end
else
MessageDlg(‘Invalid Login’, mtError, [mbOk], 0);
end.
1155
Gerando chaves do Paradox
Visto que a aplicação de relatório de bugs utiliza o banco de dados do Paradox como back-end, adquiri-
mos uma ligeira anomalia que precisa ser resolvida. Essa anomalia tem a ver com os campos de auto-in-
cremento do Paradox. Embora os campos de auto-incremento do Paradox supostamente possam lhe per-
mitir usá-los como campos-de-chave, eles não são muito confiáveis. Nossa experiência tem sido de que
eles podem facilmente ficar fora de sincronismo com chaves externas. Optamos por evitar seu uso e criar
nossas próprias chaves com base nos valores contidos na tabela IDs.db.
A tabela IDs.db armazena o próximo valor inteiro disponível para as chaves Bugs, Users e Action. O
método TDDGBugsDataModule.GetDataSetID( ) garante que somente um usuário seja capaz de colocar o cam-
po-de-chave específico da tabela tblIDs no modo Edit. Isso garantirá que dois usuários não receberão va-
lores-de-chave idênticos quando estiverem inserindo registros. GetDataSetID( ) torna-se genérico para os
três tipos de chaves, passando o nome de campo para o valor-de-chave desejado. Portanto, esse método
pode ser usado para obter chaves para bugs, usuários e ações. Na verdade, esse método é chamado pelos
métodos GetNewActionID( ), GetNewBugID( ) e GetNewUserID( ). Esses três métodos podem ser chamados sem-
pre que se posta um registro em uma dessas tabelas. Você faz isso nos manipuladores de evento BeforePost
para as tabelas tblBugs e tblUsers e no método PostAction( ) para a tabela tblActions.
Navegação/filtragem de bugs
O usuário é capaz de navegar por todos os bugs no banco de dados ou simplesmente aqueles bugs perten-
centes a ele. Isso é possível por meio do uso da propriedade Filtered do componente tblBugs. Quando
tblBugs.Filtered é True, a propriedade tblBugs.OnFilterRecord é chamada para cada registro. Aqui, você só
exibe um registro se o seu campo UserID for o do usuário conectado, conforme identificado pelo campo
global FLoginUserID. Observe como você traz à tona a propriedade Filtered da tabela tblBugs para a interfa-
ce do usuário. Em vez de permitir que a interface do usuário acesse diretamente a propriedade
tblBugs.Filtered, você traz essa propriedade à tona através da propriedade TDDGBugsDataModule.Fil-
terOnUser. O método escritor dessa propriedade, SetFilterOnUser( ), realiza a atribuição para a proprieda-
de tblBugs.Filtered. Agora, você não pode realmente impor a regra de que os formulários não podem
acessar diretamente as propriedades dos componentes que residem em um TDatamodule, pois a VCL não
está usando regras de visibilidade estritas da OOP (programação orientada a objeto).
Incluindo usuários
A inclusão de usuários é feita por meio dos métodos TDDGBugsDataModule.AddUser( ) e TDDGBugsDataModu-
le.PostUser( ). O método AddUser( ) chama uma caixa de diálogo simples, com a qual você inclui os dados
do usuário. Observe como o método PostUser( ) é passado para a função NewUserForm( ), que chama o for-
mulário do usuário. Isso demonstra como você pode evitar ter que fazer com que um formulário, chama-
do por um módulo de dados, se refira a esse módulo de dados. O motivo pelo qual esse problema se apre-
sentou é que estamos protegendo os componentes do módulo de dados do acesso externo. Provavelmen-
te, existem diversas maneiras de realizar isso – essa é simplesmente aquela que escolhemos. NewUserForm( )
1156 chama o formulário definido na unidade UserFrm.pas que aparece na Listagem 35.3.
Listagem 35.3 UserFrm.pas: o formulário do usuário
unit UserFrm;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, Mask, DBCtrls;
type
TUserForm = class(TForm)
lblUserName: TLabel;
dbeUserName: TDBEdit;
lblFirstName: TLabel;
dbeFirstName: TDBEdit;
lblLastName: TLabel;
dbeLastName: TDBEdit;
btnOK: TButton;
btnCancel: TButton;
procedure btnOKClick(Sender: TObject);
private
FPostUser: TNotifyEvent;
public
{ Public declarations }
end;
implementation
uses dbTables;
{$R *.DFM}
begin
MessageDlg(‘User name already exists.’, mtWarning, [mbOK], 0);
dbeUserName.SetFocus;
ModalResult := mrNone;
end;
end;
end;
end;
end.
Como vimos na Listagem 35.3, NewUserForm( ) cria o TUserForm e o apresenta. Observe que você atribui o
parâmetro APostUser ao campo FPostUser, que é do tipo TNotifyEvent. Declarando FPostUser como um ponteiro
de método (TNotifyEvent), você pode atribuir o método PostUser( ) de TDDGBugDataModule a FPostUser, pois Post-
User( ) combina com a definição de TNotifyEvent. Esse conceito foi explicado nos Capítulos 20 e 21.
Quando o usuário aciona o botão OK, btnOkClick( ) é chamado. Se um nome de usuário foi infor-
mado, o método TDDGBugDataModule.PostUser( ) (referenciado por FPostUser) será chamado, o qual deverá
salvar o registro do usuário (ver PostUser( ) na Listagem 35.1). Se houver um erro em PostUser( ), o nome
do usuário já existirá no banco de dados. Isso ilustra outra vantagem de passar o método PostUser( ) para
o TUserForm. O TUserForm pode tratar de um erro gerado no módulo de dados. Esse conceito não é diferente
do desenvolvimento de componentes. Você desenvolve o módulo de dados de modo que esteja comple-
tamente autocontido. Você também permite que os usuários do módulo de dados tratem de quaisquer
erros gerados dentro do módulo de dados.
Incluindo ações
Ações são basicamente notas que você opcionalmente conectou a cada bug. Qualquer um pode incluir
uma ação em um bug. O método TDDGBugsDataModule.AddAction( ) chama NewActionForm( ), que obtém os da-
dos da ação do usuário e os inclui no banco de dados. NewActionForm( ) é definido em ActionFrm.pas, que
aparece na Listagem 35.4.
unit ActionFrm;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls;
type
public
{ Declarações públicas }
end;
implementation
{$R *.DFM}
end.
O formulário principal
A interface do usuário basicamente refere-se aos métodos do módulo de dados. Temos uma única inter-
face do usuário, consistindo em três páginas. A primeira página permite que o usuário inclua, edite e veja
as informações de bug. A segunda página é para ações de navegação. A terceira página permite que o
usuário veja uma grade contendo todos os bugs ou apenas os bugs do usuário conectado. As Figuras 35.2,
35.3 e 35.4 mostram as três páginas do formulário principal.
1159
FIGURA 35.2 A página Bug Information.
unit MainFrm;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
DBNAVSTATFRM, Menus, ImgList, ComCtrls, ToolWin, StdCtrls, DBCtrls, Db,
Mask, dbModeFrm, ActnList, Grids, DBGrids, ExtCtrls;
type
TMainForm = class(TDBNavStatForm)
pcMain: TPageControl;
tsBugInformation: TTabSheet;
tsActions: TTabSheet;
lblBugID: TLabel;
dbeBugID: TDBEdit;
dsBugs: TDataSource;
lblDateReported: TLabel;
lblSummary: TLabel;
lblDetails: TLabel;
lblAffectedSource: TLabel;
lblReportedBy: TLabel;
lblAssignedTo: TLabel;
lblStatus: TLabel;
dbmSummary: TDBMemo;
dbmDetails: TDBMemo;
dbmAffectedSource: TDBMemo;
tsBrowseBugs: TTabSheet;
rgWhoseBugs: TRadioGroup;
dbgBugs: TDBGrid;
dbmSummary2: TDBMemo;
memAction: TMemo;
dblcAssignedTo: TDBLookupComboBox;
dblcStatus: TDBLookupComboBox;
dbeDateReported: TDBEdit;
mmiFile: TMenuItem;
mmiExit: TMenuItem;
mmiUsers: TMenuItem;
mmiAddUser: TMenuItem;
mmiActions: TMenuItem;
mmiAddActionToBug: TMenuItem;
dblcReportedBy: TDBLookupComboBox;
procedure FormCreate(Sender: TObject);
procedure sbFirstClick(Sender: TObject);
procedure sbPreviousClick(Sender: TObject);
procedure sbNextClick(Sender: TObject);
procedure sbLastClick(Sender: TObject);
procedure sbSearchClick(Sender: TObject);
procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
procedure sbAcceptClick(Sender: TObject);
procedure sbCancelClick(Sender: TObject);
procedure sbInsertClick(Sender: TObject);
1161
Listagem 35.5 Continuação
protected
public
{ Declarações públicas }
end;
var
MainForm: TMainForm;
implementation
uses DDGBugsDM;
{$R *.DFM}
{ TMainForm }
procedure TMainForm.SetActionStatus;
begin
mmiFirst.Enabled := not DDGBugsDataModule.IsFirstBug;
mmiLast.Enabled := not DDGBugsDataModule.IsLastBug;
mmiNext.Enabled := not DDGBugsDataModule.IsLastBug;
mmiPrevious.Enabled := not DDGBugsDataModule.IsFirstBug;
mmiDelete.Enabled := not DDGBugsDataModule.IsBugsTblEmpty;
sbFirst.Enabled := mmiFirst.Enabled;
sbLast.Enabled := mmiLast.Enabled;
sbNext.Enabled := mmiNext.Enabled;
sbPrev.Enabled := mmiPrevious.Enabled;
sbDelete.Enabled := mmiDelete.Enabled;
end;
1162
Listagem 35.5 Continuação
CanClose := True;
end;
mrNo:
begin
DDGBugsDataModule.CancelBug;
FormMode := fmBrowse;
CanClose := True;
end;
mrCancel:
CanClose := False;
end;
end;
end;
SetActionStatus;
end;
end.
TMainForm descende de TDBNavStatForm, que foi apresentado no Capítulo 4. Ele deverá existir no
seu Object Repository. TDBNavStatForm contém a funcionalidade para atualizar os speedbuttons e a barra
de status, com base no modo do formulário (Add, Edit ou Browse). A maioria dos métodos simplesmente
chama os métodos do módulo de dados correspondente.
Observe que você define a propriedade dsBugs.AutoEdit como False, evitando assim que o usuário
inadvertidamente coloque a tabela no modo Edit. Você deseja fazer com que o usuário defina explicita-
mente a tabela Bugs para o modo Edit ou Insert, dando um clique nos botões ou nos itens de menu apro-
priados.
Esse formulário é descomplicado. SetActionStatus( ) simplesmente ativa/desativa os botões e menus,
com base em diversas condições. FormCloseQuery( ) garante que o usuário salvará ou cancelará qualquer
edição ou inserção pendente.
1165
Outros aspectos da interface do usuário
A partir do módulo de dados, controlamos como os labels de campo são exibidos, incluindo os campos
no objeto TTable e especificando um label mais amigável no Object Inspector. A mesma coisa pode ser fei-
ta para objetos TDBGrid, modificando a propriedade Title da propriedade TDBGrid.Columns. Usamos os dois
métodos para controlar os labels apresentados ao usuário.
{$IFNDEF DDGWEBBUGS}
procedure AddUser;
{$ENDIF}
A condição {$IFNDEF} garante que o método AddUser( ) só será compilado na aplicação se a diretiva
condicional DDBWEBBUGS não estiver definida, o que acontece nessa aplicação.
Resumo
Neste capítulo, discutimos sobre técnicas para desenvolver uma aplicação de banco de dados de desktop.
Também enfatizamos a separação da interface do usuário das rotinas de manipulação de dados. Isso faci-
litará a conversão da aplicação para uma versão preparada para a Web. O próximo capítulo demonstra
como fazer exatamente isso.
1166
Ferramenta DDG para CAPÍTULO
Página de
introdução
Página de Página de
tabela detalhes
de bugs de bugs
Você pode ver, pelo layout das páginas, que essa aplicação é realmente um subconjunto da funcio-
nalidade apresentada no Capítulo 35. Como exercício, fique à vontade para expandir as técnicas de-
monstradas neste capítulo para fornecer toda a funcionalidade apresentada no capítulo anterior.
As próximas seções explicam o código usado para desenvolver as páginas. Você notará neste exem-
plo que todas as páginas são criadas em runtime – ou seja, nenhum documento HTML predefinido é car-
regado. Não há qualquer motivo importante para termos escolhido esse método em vez de escrever al-
guns documentos HTML carregados pelos componentes do WebBroker. Você certamente poderá usar
esse outro método para as suas aplicações.
TactionItem Finalidade
Nas próximas seções, mostraremos a listagem individual para cada método incluído na unidade
DDBBugsDM.pas,
em vez de mostrar a listagem inteira.
Rotinas auxiliadoras
O procedimento AddHeader( ), mostrado na Listagem 36.1, é usado para incluir um cabeçalho padrão nas
páginas de bugs da Web, contendo o título da página e o cabeçalho. Além disso, a imagem do fundo a ser
utilizada é especificada aqui. Observe que o local dessa imagem de fundo depende do servidor da Web.
Você provavelmente terá de modificar essa instrução, dependendo do seu sistema, para poder encontrar
a imagem. AddFooter( ), mostrado na Listagem 36.2, é usado para incluir a informação de cabeçalho pa-
drão, incluindo a nota de direito autoral.
1170
Listagem 36.1 TDDGBugsDataModule.AddHeader( ) é usado para incluir a informação padrão do cabeçalho
A página de introdução
A página de introdução aparece na Figura 36.4. Ela é criada pelo manipulador de evento waIntro.OnAction,
wbdpBugswaIntroAction( ), que aparece na Listagem 36.3.
Você notará que, em cada instância na qual uma página da Web é gerada, passamos WebPage para os pro-
cedimentos AddHeader( ) e AddFooter( ). A página de introdução é bastante simples. Ela simplesmente contém
um link com a TWebAction, waUserName. Para obter mais informações sobre TWebAction, consulte o Capítulo 31.
begin
Add(‘<BODY>’);
Add(‘<H1>Enter your user name</H1>’);
Add(‘<FORM action=”../DDGWebBugs.dll/VerifyUserName” method=”GET”>’);
Add(‘<p>UserName: <INPUT type=”text” name=”UserName” maxlength=”30”
åsize=”50”></P>’);
Add(‘<p><INPUT type=”SUBMIT”><INPUT type=”RESET”></p>’);
Add(‘</FORM>’);
AddFooter(WebPage);
Response.Content := WebPage.Text;
Handled := True;
end;
finally
WebPage.Free;
end;
end;
procedure TDDGBugsDataModule.wbdpBugswaVerifyUserNameAction(
Sender: TObject; Request: TWebRequest; Response: TWebResponse;
var Handled: Boolean);
{ Esta página apanha o nome inserido pelo usuário. As informações são salvas
e passadas de volta ao cliente como um cookie. Outras informações também
são passadas de volta como um cookie, que será usado mais adiante para
incluir bugs a partir da Web. }
var
WebPage: TStringList;
CookieList: TStringList;
UserName: String;
UserFName,
1173
Listagem 36.5 Continuação
UserLName: String;
UserID: Integer;
ValidLogin: Boolean;
procedure BuildValidLoginPage;
begin
AddHeader(WebPage);
with WebPage do
begin
Add(‘<BODY>’);
Add(Format(‘<H1>User name, %s verified. User ID is: %d</H1>’,
[Request.QueryFields.Values[‘UserName’], UserID]));
Add(‘<BR><BR><A href=”../DDGWebBugs.dll/BrowseBugs”>Browse Bug List</A>’);
Add(‘<BR><A href=”../DDGWebBugs.dll/GetBugInfo”>Add a New Bug</A>’);
AddFooter(WebPage);
end;
end;
procedure BuildInValidLoginPage;
begin
AddHeader(WebPage);
with WebPage do
begin
Add(‘<BODY>’);
Add(Format(‘<H1>User name, %s is not a valid user.</H1>’,
[Request.QueryFields.Values[‘UserName’]]));
AddFooter(WebPage);
end;
end;
begin
UserName := Request.QueryFields.Values[‘UserName’];
WebPage := TStringList.Create;
try
if ValidLogin then
begin
CookieList := TSTringList.Create;
try
CookieList.Add(‘UserName=’+UserName);
CookieList.Add(‘UserFirstName=’+UserFName);
CookieList.Add(‘UserLastName=’+UserLName);
Response.Content := WebPage.Text;
Handled := True;
finally
WebPage.Free;
end;
end;
try
AddHeader(WebPage);
with WebPage do
begin
Add(‘<BODY>’);
Add(‘<H1>Browse Option</H1>’);
Add(‘<BR><BR><A href=”../DDGWebBugs.dll/BrowseAllBugs”>
➥Browse All Bugs</A>’);
Add(‘<BR><A href=”../DDGWebBugs.dll/BrowseYourBugs”>
➥Browse Your Bugs</A>’);
AddFooter(WebPage);
Response.Content := WebPage.Text;
Handled := True;
end;
finally
WebPage.Free;
end;
end;
pprdBugs.HTMLDoc.Clear;
pprdBugs.HTMLDoc.AddStrings(WebPage);
Handled := True;
finally
WebPage.Free;
end;
end;
1176
Este manipulador de evento utiliza o componente de TPageProducer, pprdBugs. A funcionalidade ne-
cessária a partir desse componente é a sua capacidade de usar tags dentro do conteúdo HTML. Em parti-
cular, queremos usar a tag #TABLE. Incluímos o cabeçalho e rodapé padrão à página da Web. No entanto,
em vez de atribuir WebPage a Response.Content, atribuímos WebPage à propriedade pprdBugs.HTMLDoc. Depois,
atribuímos pprdBugs.Content a Response.Content. Isso faz com que o evento pprdBugs.OnHTMLTag seja chamado.
Esse evento, TDDGBugsDataModule.pprdBugsHTMLTag( ), aparece na Listagem 36.8.
Este manipulador de evento simples atribui a propriedade dstpBugs.Content, que se refere à tabela, à
propriedade pprdBugs.ReplaceText, a qual substituirá a tag #TABLE pelo conteúdo da tabela. A página resul-
tante aparece na Figura 36.6. Ela mostra os bugs inseridos por todos os usuários.
FIGURA 36.6 Uma lista dos bugs inseridos por todos os usuários.
1177
Listagem 36.9 TDDGBugsDataModule.wbdpBugswaBrowseYourBugsAction( ) apresenta apenas os bugs do
usuário
procedure TDDGBugsDataModule.wbdpBugswaBrowseYourBugsAction(
Sender: TObject; Request: TWebRequest; Response: TWebResponse;
var Handled: Boolean);
{ Esta página prepara o componente TpageProducer para navegar por bugs que
pertencem ao usuário. O cabeçalho e rodapé padrão é aplicado a esta página,
mas uma tag é usada para incluir a tabela na página. }
var
WebPage: TStringList;
UserID: Integer;
UserFName,
UserLName: String;
begin
WebPage := TStringList.Create;
try
AddHeader(WebPage);
WebPage.Add(‘<BODY>’);
AddFooter(WebPage);
Response.Content := pprdBugs.Content;
Handled := True;
finally
WebPage.Free;
end;
end;
Assim como acontecia com o manipulador de evento para navegar por todos os bugs, o cabeçalho e
rodapé padrão precisam ser incluídos nessa página. Além disso, os cookies UserID, UserFirstName e User-
LastName são apanhados da propriedade Request.CookieFields. UserFirstName e UserLastName são usados para
exibir o nome do usuário na página da Web. UserID recebe FLoginUserID. Depois a propriedade FilterOnUser
é definida para True. Se você se lembra do capítulo anterior, definindo a propriedade FilterOnUser como
True, seu método escritor SetFilterOnUser( ) é chamado, o que por sua vez define tblBugs.Filtered como
True. Isso faz com que o manipulador de evento OnFilterRecord para tblBugs, tblBugsFilterRecord( ), seja
1178 chamado para cada registro no dataset. Esse evento executa a seguinte linha de código:
Accept := tblBugs.FieldByName(‘UserID’).AsInteger = FLoginUserID;
Você pode ver que o filtro aplicado depende do valor contido no campo FLoginUserID. Isso explica
por que o valor de UserID a partir do campo de cookie precisa ser atribuído a FLoginUserID.
Finalmente, a propriedade pprdBugs.Content é atribuída a Response.Content. Novamente, isso fará com
que o evento pprdBugs.OnHTMLTag seja chamado.
procedure GetBug;
begin
if tblBugs.Locate(‘BugID’, BugID, [ ]) then
with tblBugs do
begin
WebPage.Add(Format(‘Bug ID: %d’, [BugID]));
WebPage.Add(Format(‘<BR>Reported By: %s’,
[FieldByName(‘UserNameLookup’).AsString]));
WebPage.Add(FormatDateTime(‘“<BR>Reported On:” mmm dd, yyyy’,
FieldByName(‘WhenReported’).AsDateTime));
WebPage.Add(Format(‘<BR>Assigned To: %s’,
[FieldByName(‘AssignedToLookup’).AsString]));
WebPage.Add(Format(‘<BR>Status: %s’,
[FieldByName(‘StatusTitle’).AsString]));
WebPage.Add(Format(‘<BR>Summary: %s’,
[FieldByName(‘SummaryDescription’).AsString]));
WebPage.Add(Format(‘<BR>Details: %s’,
[FieldByName(‘Details’).AsString]));
1179
Listagem 36.10 Continuação
WebPage.Add(‘<BR>’);
WebPage.Add(‘<BR>’);
GetActions(WebPage);
end;
end;
begin
BugID := StrToInt(Request.QueryFields.Values[‘BugID’]);
WebPage := TStringList.Create;
try
AddHeader(WebPage);
with WebPage do
begin
Add(‘<BODY>’);
Add(‘<H1>Bug Detail</H1>’);
GetBug;
AddFooter(WebPage);
Response.Content := WebPage.Text;
Handled := True;
end;
finally
WebPage.Free;
end;
end;
procedure AddAssignToNames;
1180
Listagem 36.11 Continuação
WebPage.Add(‘<BR>Assign To:’);
WebPage.Add(‘<BR><SELECT name=”AssignTo”><BR>’);
with tblUsers do
begin
First;
while not Eof do
begin
WebPage.Add(Format(‘<OPTION>%s %s - %s’,
[FieldByName(‘UserFirstName’).AsString,
FieldByName(‘UserLastName’).AsString,
FieldByName(‘UserName’).AsString]));
tblUsers.Next;
end;
WebPage.Add(‘</SELECT>’);
end;
end;
procedure AddStatusTitles;
{ Inclui uma lista suspensa à página HTML de itens de status do bug. }
begin
WebPage.Add(‘<BR>Status:’);
WebPage.Add(‘<BR><SELECT name=”Status”><BR>’);
with tblStatus do
begin
First;
while not Eof do
begin
WebPage.Add(Format(‘<OPTION>%s’, [FieldByName(‘StatusTitle’).AsString]));
tblStatus.Next;
end;
WebPage.Add(‘</SELECT>’);
end;
end;
begin
WebPage := TStringList.Create;
try
AddHeader(WebPage);
with WebPage do
begin
Add(‘<BODY>’);
Add(‘<H1>Add New Bug</H1>’);
Add(‘<FORM action=”../DDGWebBugs.dll/AddBug”
åmethod=”GET”>’);
Add(‘<BR>Summary Description:<BR><INPUT type=”text”
åname=”Summary” maxlength=”100” size=”50”>’);
Add(‘<BR>Details:<BR><TEXTAREA name=”Details”
årows=5 cols=50> </TEXTAREA>’);
1181
Listagem 36.11 Continuação
AddAssignToNames;
AddStatusTitles;
var
SummaryStr,
DetailsStr,
AssignToStr,
StatusStr: String;
WebPage: TStringList;
UserID: Integer;
UserName: String;
UserFName,
UserLName: String;
AssignedToUserName: String;
PostSucceeded: boolean;
procedure DoPostSuccessPage;
begin
with WebPage do
begin
Add(Format(‘<H1>Thank you %s %s, your bug has been added.</H1>’,
[UserFName, UserLName]));
Add(FormatDateTime(‘“<BR><BR>Bug Entered on:” mmm dd, yyyy’, Date));
Add(Format(‘<BR>Bug Assigned to: %s’, [AssignedToUserName]));
Add(Format(‘<BR>Details: %s’, [DetailsStr]));
Add(Format(‘<BR>Status: %s’, [StatusStr]));
end;
end;
procedure DoPostFailPage;
begin
WebPage.Add(‘<BR>Bug Entry failed.’);
end;
begin
InsertBug;
try
tblBugs.FieldByName(‘SummaryDescription’).AsString := SummaryStr;
tblBugs.FieldByName(‘WhenReported’).AsDateTime := Date;
tblBugs.FieldByName(‘Details’).AsString := DetailsStr;
tblBugs.FieldByName(‘AssignedToUserID’).AsInteger := GetAssignedToID;
tblBugs.FieldByName(‘StatusID’).AsInteger := GetStatusID;
tblBugs.Post;
PostSucceeded := True;
except
tblBugs.Cancel;
PostSucceeded := False;
end;
WebPage := TStringList.Create;
try
AddHeader(WebPage);
with WebPage do
begin
Add(‘<BODY>’);
if PostSucceeded then
DoPostSuccessPage
else
DoPostFailPage;
AddFooter(WebPage);
Response.Content := WebPage.Text;
Handled := True;
end;
finally
WebPage.Free;
end;
end;
Esse manipulador de evento primeiro apanha todos os valores inseridos pelo usuário da página de
entrada de bug mostrada na Figura 36.7. Ele também apanha os campos de cookie inseridos anterior-
mente. Estas linhas de código
// Necessário para o manipulador de evento AfterInsert.
FLoginUserID := UserID;
1184 FLoginUserName := UserName;
são necessárias para o manipulador de evento AfterInsert de tblBugs, que realiza o seguinte:
tblBugs.FieldByName(‘UserID’).AsInteger := FLoginUserID;
tblBugs.FieldByName(‘UserNameLookup’).AsString := FLoginUserName;
Finalmente, o novo bug é inserido em tblBugs. Se a inserção tiver sucesso, a página da Web será
construída pela chamada a DoPostSuccessPage( ); caso contrário, DoPostFailPage( ) será chamado. DoPost-
SuccessPage( ) simplesmente apresenta os dados do bug para o usuário, enquanto DoPostFailPage( ) apre-
senta uma notificação de falha.
Lembre-se de que os controles de pesquisa ligados aos dados não são usados para obter entradas
para os campos AssignToUserID e StatusID de tblBugs. Nossa página de entrada de bug oferece ao usuário as
strings que representam esses itens nas caixas de combinação suspensas. Para incluir os valores de índice
de pesquisa apropriados em tblBugs, uma consulta é realizada sobre as strings selecionadas pelo usuário
contra tblUsers e tblStatus. Observe que um pouco de manipulação de string é necessária para o campo
AssignToUserID. a fim de extrair a string apropriada com a qual será realizada a consulta (veja o método
GetAssignToID( )).
Resumo
Este capítulo explicou a distribuição de aplicações de banco de dados na Web. Nele, demonstramos
como, se corretamente projetada, uma aplicação pode ser distribuída para a Web com algumas modifica-
ções no código existente (com a exceção de incluir código específico à Web). Na realidade, a maior parte
do que apresentamos aqui tem mais a ver com a construção de documentos HTML do que com a mani-
pulação de bancos de dados. Você poderá modificar essa demonstração para estender sua funcionalida-
de, além de mudar o código de construção da HTML para arquivos HTML reais.
1185
Apêndices PARTE
VI
NE STA PART E
Apêndice A Mensagens de erro e exceções 1189
e exceções
A
NE STE APÊN D ICE
l Camadas de manipuladores, camadas
de rigor
l Erros de runtime
1190
Códigos de APÊNDICE
erro do BDE
B
NE STE APÊN D ICE
O texto completo deste apêndice aparece no CD
que acompanha este livro.
Ao trabalhar com o Borland Database Engine, ocasionalmente você receberá uma caixa de diálogo de
erro indicando que ocorreu algum erro no mecanismo. Normalmente, isso acontece quando os clientes
instalam o seu software em suas máquinas, mas a máquina possui alguns problemas de configuração e
você está tentando resolver para eles. Normalmente, essa caixa de diálogo de erro oferece um código de
erro hexadecimal como descrição do erro. A questão é como transformar esse número em uma mensa-
gem de erro significativa. Para ajudá-lo nessa tarefa, oferecemos a tabela a seguir. A Tabela B.1 relaciona
todos os códigos de erro possíveis do BDE, além das strings de erro do BDE associadas a esses códigos de
erro.
1192
Leitura sugerida APÊNDICE
C
NE STE APÊN D ICE
l Programação em Delphi 1194
l Projeto de componentes 1194
l Programação em Windows 1194
l Programação orientada a objeto 1194
l Gerenciamento de projeto de software e projeto
de interface com o usuário 1194
l COM/ActiveX/OLE 1194
Programação em Delphi
l The Tomes of Delphi 3: Win32 Graphical API, de John Ayres, David Bowden, Larry Diehl, Phil
Dorcas, Kenneth Harrison, Rod Mathes, Ovias Reza e Mike Tobin (Wordware Publishing, Inc.,
1998).
l The Tomes of Delphi 3: Win32 Core API, de John Ayres, David Bowden, Larry Diehl, Phil Dor-
cas, Kenneth Harrison, Rod Mathes, Ovias Reza e Mike Tobin (Wordware Publishing, Inc.,
1997).
l Charlie Calvert’s Delphi 4 Unleashed, de Charlie Calvert (Sams Publishing, 1998).
l Mastering Delphi 5, de Marco Cantu (Sybex, 1999).
l Delphi Developer’s Handbook, de Marco Cantu, Tim Gooch e John F. Lam (Sybex,1997).
l Hidden Paths of Delphi 3, de Ray Lischner (Informant Communications Group, 1997).
l Secrets of Delphi 2, de Ray Lischner (Waite Group Press, 1996).
Projeto de componentes
Os dois livros a seguir estão listados como esgotados. No entanto, ainda pode ser possível conse-
gui-los pelo Amazon.com ou outro distribuidor.
l Developing Custom Delphi 3 Components, de Ray Konopka (Coriolis Group Books, 1997).
l Delphi Component Design, de Danny Thorpe (Addison-Wesley, 1997).
Programação em Windows
l Advanced Windows, 3a. ed., de Jeffrey Richter (Microsoft Press, 1997).
COM/ActiveX/OLE
l Essential COM, de Don Box (Addison-Wesley, 1998).
l Inside OLE, 2a ed., de Kraig Brockschmidt (Microsoft Press, 1995).
1194
Índice
1219
O QUE HÁ NO CD
O CD-ROM que acompanha o livro contém 11 capítulos do livro em formatoAdobe Acrobat, todos os
códigos-fonte do autor e amostras do livro, além de produtos de software de terceiros.
NOTA
Se o Windows 95, 98 ou NT 4 estiver instalado em seu computador, e você tiver o recurso AutoPlay ativado,
o programa START.EXE inicia automaticamente assim que inserir o disco em sua unidade de CD-ROM.
1220
Abrindo este pacote, você concorda com os seguintes termos de acordo:
Não é permitido copiar ou redistribuir o CD-ROM inteiro, como um todo. A cópia e a distribuição de
programas de software individuais no CD-ROM é governada pelos termos definidos pelos mantenedo-
res individuais do direito autoral.
Os direitos autorais do programa instalador e do código do autor pertencem à editora e ao autor. Os pro-
gramas individuais e outros itens no CD-ROM possuem direitos autorais e estão sob licença GNU de
seus vários autores e outros mantenedores de direito autoral.
O software no CD-ROM é vendido como se encontra, sem qualquer tipo de garantia, seja expressa ou
implícita, incluindo (mas não limitado a) garantias implícitas de comercialização e ajuste a uma determi-
nada finalidade. Nem a editora nem seus revendedores ou distribuidores assumem qualquer responsabi-
lidade por quaisquer danos, alegados ou reais, que surjam em decorrência do uso do software.
NOTA: Este CD-ROM utiliza nomes de arquivo extensos e também diferencia letras maiúsculas de mi-
núsculas; isso exige o uso de um driver de CD-ROM no modo protegido.
1221
Os produtos de software contidos neste CD-ROM são, em muitos casos, protegidos por copyright, e
todos os direitos relativos aos produtos em questão pertencem aos seus desenvolvedores e/ou
profissionais responsáveis por sua publicação. Você deve se orientar pelos acordos de licenciamento
específicos relacionados a cada produto de software gravado neste CD-ROM. Os produtos de software
deste CD-ROM estão sendo publicados gratuitamente, no estado em que se encontram e sem qualquer
tipo de garantia, de natureza explícita ou não, incluindo as garantias implícitas de comercialização e
adequação a propósitos específicos, sem no entanto se limitar a elas. A Editora Campus não assume
qualquer responsabilidade por danos alegados ou comprovados decorrentes da utilização dos produtos
de software e do código-fonte contidos neste CD-ROM ou de problemas ligados à funcionalidade do
código-fonte para datas a partir de 01/01/2000.
1222