A
utilização de arquivos acontece de
modo natural no desenvolvimento de jogos em Delphi:
carregar uma imagem para um componente TImage
ou salvar um TMemo
para um arquivo são operações
que tomam exatamente uma linha de código.
Porém, muitas vezes não é óbvio
para o programador casual como criar funções
mais complexas para ler ou escrever arquivos de
um tipo proprietário. Visando contornar essa
deficiência, este artigo irá descrever
os principais métodos para manipulação
de arquivos do tipo texto e binário em Delphi,
mostrando seu uso em um pequeno projeto de testes.
Aplicação:
O código
para a manipulação de arquivos irá
se basear em um hipotético jogo 2d. O jogo
contém diversos personagens espalhados pela
tela, cada um com uma determinada posição,
nome e energia. A estrutura que representa um personagem
é a seguinte:
TPersonagem = record posicao: TPoint; nome: string[20]; energia: integer; end; |
Note
que é possível ter sub-estruturas
(a variável "posicao"
que é do tipo TPoint)
porém não é possível
ter strings longas (strings que têm tamanho
variável), arrays dinâmicos ou ponteiros
nas estruturas que serão usadas para ler
o arquivo. Isso porque as funções
que lêem bytes do sistema de armazenagem não
são capazes de redimensionar esses tipos
de variáveis automaticamente, gerando problemas
quando elas são usadas.
A última
coisa que deve ser declarada é uma variável
global chamada "pers"
como um array dinâmico (pers: array of TPersonagem)
que irá armazenar os personagens na memória
do computador.
O código
completo para os métodos discutidos pode
ser conferido no programa de demonstração
que se encontra para download no final desta página.
Tipos
de arquivos:
Normalmente,
os formatos para descrição de arquivos
começam com a definição do
tipo de arquivo que será utilizado: tipo
texto, onde os bytes são interpretados como
caracteres ASCII e entendidos como sequências
de texto ou do tipo binário, que entendem
o conteúdo de um arquivo como estruturas
de dados definidas pelo programador (ou como conjuntos
de bytes sem qualquer estrutura).
A vantagem
principal dos arquivos do tipo texto é a
sua facilidade de ser interpretado até mesmo
pelas pessoas (sem ter necessidade de nenhum tipo
de processamento). Essa característica é
especialmente importante nas fases de desenvolvimento
de um programa, principalmente durante a construção
das rotinas de leitura e escrita de arquivos, já
que eles podem ser modificados manualmente em qualquer
editor de texto simples.
Já
arquivos binários têm as vantagens
de não necessitar de passos adicionais para
sua interpretação (todas as informações
de arquivos texto precisam ser convertidas para
seu formato padrão), geralmente ocupar menos
espaço de armazenagem (um número inteiro
do Delphi sempre ocupa 4 bytes de memória
quando salvo em um arquivo binário, enquanto
qualquer número com mais de 4 dígitos
ocupará mais memória quando salvo
em um arquivo de texto) e ser (acredite se quiser)
mais fáceis de se manipular em uma linguagem
de programação.
A decisão
sobre utilizar um ou outro tipo de arquivo deve
se basear principalmente no uso que ele terá.
Arquivos pequenos (alguns KBytes) que se destinam
a guardar informações que um usuário
do programa possivelmente desejaria alterar ou informações
que são naturalmente textos (especialmente
textos longos) são melhor armazenados como
arquivos tipo texto. Já aqueles que devem
ser processados rapidamente ou que guardam outros
tipos de variáveis (números, estruturas
de dados complexas) são melhor expressos
como arquivos binários.
Este
artigo irá demostrar como trabalhar com ambos
os tipos, usando diferentes técnicas presentes
no Delphi.
API
do Windows:
Utilizar
a API do Windows é a maneira mais complexa,
porém (possivelmente) a mais eficiente para
trabalhar com arquivos. Isso porque todos os métodos
seguintes são, em última instância,
convertidos para chamadas à API, que por
sua vez cuida das tarefas de interfaceamento com
o hardware do computador.
As
chamadas da API, no entanto, não fazem distinção
entre arquivos de texto e binários, portanto
só o segundo tipo será mostrado. Explicações
mais detalhadas sobre cada função
podem ser encontradas na documentação
da Microsoft ou nos arquivos de ajuda instalados
com o Delphi.
As
principais funções usadas nesse método
são:
Tanto
ler quanto escrever são processos bem similares,
portanto eles serão explicados juntos.
Em
primeiro lugar, o arquivo precisa ser aberto e um
handle do windows adquirido para as futuras operações,
o que é feito através da função
createFile.
A diferença entre abertura para escrita ou
leitura é feita pelos argumentos passados
para essa função: para escrita utilizam-se
os parâmetros GENERIC_WRITE
(escrita genérica) e CREATE_ALWAYS
(criar ou sobrescrever o arquivo), enquanto para
leitura é usado GENERIC_READ
(leitura genérica) e OPEN_EXISTING
(abrir arquivo existente).
Essa
função retorna uma referência
ao arquivo aberto pelo windows na forma de handle
(um número inteiro positivo) ou, caso ela
falhe (como por exemplo, se o arquivo a ser lido
não existe) o valor da constante INVALID_HANDLE_VALUE.
Essa característica pode ser usada para checar
se houve erro e tratá-lo conforme o necessário
(os métodos posteriores não necessitarão
de codificação para tratamento de
erros porque o Delphi automaticamente lança
exceções quando um erro é detectado,
o que não ocorre quando se utiliza a API
do Windows).
Com
o arquivo aberto, o próximo passo é
ler ou escrever nele. Tanto leitura quanto escrita
são feitos em blocos de memória referenciados
por variáveis. Portanto é possível
ler/escrever variáveis de registro (records)
que contenham uma complexa estrutura com apenas
um comando. Na versão que utiliza a API usaremos
uma estrutura como cabeçalho (chamada de
TCabecalho)
que contenha um inteiro com o número de personagens
do arquivo. Portanto, após abrir o arquivo
é preciso ler (função readFile
passando como argumentos o handle do arquivo aberto,
a variável que contém os dados a serem
lidos e o tamanho dessa estrutura) ou escrever (função
writeFile
com os mesmos parâmetros) o cabeçalho.
Com
a informação de quantos personagens
existem nesse arquivo e sabendo como ler ou escrever
uma estrutura, é fácil deduzir o que
fazer em seguida: ler ou escrever cada um dos elementos
do array "pers" definido
no inicio do artigo. A única diferença
entre esse processo e o executado no parágrafo
anterior é que ao invés de passar
uma variável do tipo TCabecalho,
será passada uma variável do tipo
TPersonagem
para as funções readFile
e writeFile.
Após
concluído essa tarefa, a única coisa
que resta fazer é fechar o arquivo e devolver
a sua referência ao Windows. Para isso, basta
chamar a função closeHandle
enviando como parâmetro a variável
que armazenou o handle.
O processo
visto nesse método para a manipulação
de arquivos é semelhante para os próximos
itens que serão vistos: abrir um arquivo,
ler/escrever seu conteúdo e fechar o arquivo.
A utilização de registros facilita
a vida do programador, diminui a chamada aos métodos
de leitura/gravação e portanto aumenta
a performance geral do sistema.
Pascal:
Utilizar
a API do Windows, mesmo que seja eficiente, exige
um esforço maior de programação
do que com outros métodos nativos do Delphi.
A forma mais antiga (em termos de compiladores Pascal
da Borland) de acesso à arquivos é
o uso de funções padrão. Essas
funções simplificam os passos necessários
para a abrir, ler, escrever e fechar tanto arquivos
binários quanto do tipo texto, que podem
ser lidos linha à linha de maneira bem direta.
Elas são:
Seja
qual for o tipo de arquivo que será lido,
a estrutura básica de programação
será a mesma: Chama-se o método AssignFile
passando como argumentos uma variável que
armazenará o handle do arquivo e o nome (e
caminho) para o arquivo desejado. Em seguida chama-se
a função Rewrite
(se essa for uma operação de escrita)
ou Reset
(caso deseje-se executar uma operação
de leitura). Após todas as operações
(read,
write,
readLn
e writeLn)
fecha-se o arquivo com a função CloseFile.
A utilização
dessas funções requer a definição
de uma variável de arquivo, que tem o mesmo
propósito do handle quando a API do Windows
foi utilizada: armazenar uma referência ao
arquivo aberto. Porém, em Pascal, é
possível utilizar três diferentes tipos
de arquivo (ou melhor, de referência): texto,
não tipificados e tipificados.
Arquivos
do tipo texto têm seu handle definido como
sendo do tipo "TextFile"
e aceitam as funções readLn
e writeLn
para leitura e escrita de linhas. Arquivos não
tipificados são definidos apenas pela palavra
"file" (var arq: file;)
e não recebem qualquer formatação
das funções de leitura/escrita. Arquivos
tipificados (o tipo utilizado no projeto de demonstração
nas funções bin-Pascal) são
entendidos como coleções de um determinado
tipo de registro (record), ou seja,
todas as informações são salvas
como registros em forma de lista. Essa característica
facilita a sua leitura, já que não
é preciso conhecer como exatamente as informações
são salvas, apenas o quê é salvo.
A definição desse tipo de arquivo
é feita através da palavra chave "file
of <tipo>".
No
caso da versão binária, um arquivo
tipificado é ideal para o propósito
da aplicação, já que os personagens
serão armazenados sequencialmente. Portanto,
a variável do arquivo é definida como
"file of TPersonagem"
e os métodos read
e write
lêem e escrevem itens para o array de personagens.
A versão
texto é mais complicada: em primeiro lugar
é preciso ler uma linha do arquivo para uma
variável auxiliar. Em seguida, "quebra-se"
essa linha de acordo com a posição
de um caracter de controle (os dois pontos neste
caso). Finalmente, cada item da linha é convertido
para o seu valor nativo conforme necessário.
Na função para escrita faz-se o inverso:
converte-se cada item do personagem para sua representação
texto concatenando-os e colocando um caractere ":"
entre cada um deles.
Como
pode ser visto, a versão Pascal para manipulação
de arquivos é consideravelmente mais simples
que a versão que usa a API do Windows. Se
existe uma única crítica que pode
ser feita à ela é a de que não
permite uma maneira genérica de carregamento/armazenagem
de dados - é possível utilizar apenas
o sistema de arquivos para esse fim. Um último
método alternativo que corrige esse problema
será estudado em seguida.
Streams:
O último
método para manipulação de
arquivos é a utilização de
streams para leitura e escrita de dados. Streams
(literalmente: fluxos, correntes) são abstrações
para estruturas de transporte de dados sequenciais.
Um Stream pode ser entendido como uma linha de bytes
que pode ser manipulada de alguma maneira.
Existem
versões de streams que trabalham com memória,
strings e até dados de arquivos. Utilizá-los
da maneira correta pode significar um aumento nas
possibilidades de processamento de dados enorme.
Ao invés de limitar que os dados de um programa
sejam carregados apenas através de arquivos
pode-se usar streams para possibilitar que as informações
sejam carregadas de outros meios (como por exemplo
de dados vindos da rede ou gerados pelo próprio
programa).
A versão
texto dos streams são os descendentes da
classe TStrings,
que implementam métodos para manipulação
de texto. Todos os componentes do Delphi que trabalham
com texto de múltiplas linhas (incluíndo
o TMemo
e TListBox)
contém um objeto desse tipo que representa
o seu conteúdo.
A programação
com streams é simples: basta criar um objeto
descendente da classe TStream
(neste caso da classe TFileStream),
ler ou escrever os dados necessários e finalizar
o stream. O construtor da classe TFileStream
recebe como parâmetros o nome do arquivo que
será aberto, a operação e o
modo de acesso ao arquivo. A operação
diz respeito ao que se quer fazer com o arquivo
(ler, escrever, etc) e o modo de acesso indica ao
Windows como outros programas reagem à tentativa
de abrir o mesmo arquivo (a opção
fmShareExclusive
indica que apenas um processo pode abrir o arquivo
por vez).
Ler
ou escrever dados se traduz em uma operação
sobre o objeto criado: "<objeto>.read"
lê um dado e "<objeto>.write"
escreve. Ao contrário do método anterior,
os streams são naturalmente não tipificados,
o que significa que o programador deve conhecer
a estrutura interna do arquivo e informar quantos
bytes devem ser lidos e armazenados na variável
passada para os métodos read
e write.
A função "sizeof"
faz exatamento isso: retorna o número de
bytes ocupados por uma estrutura qualquer.
Já
utilizar o modo texto implica em criar um objeto
descendente de TStrings
(TStringList),
carregar o arquivo que contenha os dados à
serem lidos (ou salvá-lo no final do procedimento
caso seja uma operação de escrita)
e ler ou escrever os dados de uma maneira semelhante
daquela feita no método anterior: "quebrar"
cada linha de texto em seus respectivos itens e
convertê-los para seu formato nativo ou converter
do formato nativo para uma linha de texto e adicioná-la
ao arquivo.
Após
concluído o serviço, libera-se a memória
que não será mais usada finalizando
o objeto.
A programação
genérica que os streams podem fornecer é
conseguida separando-se a parte de carregamento/salvamento
do arquivo da leitura/escrita em si: uma função
cria e finaliza o stream (ou seja, usa um tipo específico
como o TFileStream
ou TMemoryStream)
e outra função faz a realiza as operações
com os dados (recebendo como parâmetro uma
variável do tipo TStream).
De maneira geral, o código para uma operação
de leitura ficaria:
procedure lerDoStream(stream: TStream); begin //lê os dados end;
procedure lerDoArquivo(nomeDoArquivo: string); var str: TFileStream; begin str:= TFileStream.create(nomeDoArquivo,
fmOpenRead or fmShareExclusive); lerDoStream(str); str.free; end;
procedure lerDaMemoria(mem: Pointer; tamanho: integer); var str: TMemoryStream; begin str:= TMemoryStream.create; str.write(mem^, tamanho); lerDoStream(str); str.free; end; |
A leitura
dos dados é feita no método lerDoStream,
que pode ser chamado tanto para ler de arquivos
quanto da própria memória do computador
(através dos outros dois métodos).
Agora, a complexidade de carregamento foi concentrada
em apenas um ponto, o que a torna muito mais simples
de controlar.
Arquivos
complexos (3ds):
Uma
última nota sobre manipulação
de arquivos: eles podem ser bem complexos. Arquivos
do formato 3ds (um dos formatos
mais populares para codificação de
cenas 3d) têm esquemas de codificação
que especificam algumas centenas de elementos de
dados diferentes.
Por
esse motivo, um sistema muito inteligente para armazenar
essas informações foi desenvolvido:
a estrutura de arquivos baseada em "chunks"
(literalmente: pedaços). Um chunk
é uma unidade de dados do arquivo (algo como
um registro porém com tamanho variável).
Todo chunk têm uma estrutura
bem definida: ele tem um header (cabeçalho)
com dados como número identificador (ou seja,
qual o tipo de chunk), tamanho
total e número de sub-chunks,
e uma área de dados que contém as
informações do chunk
em si.
Esse
tipo de estrutura é bem maleável,
já que novos dados podem ser adicionados
como novos chunks e ainda assim
preservar compatibilidade com programas antigos,
permite uma organização lógica
em forma de hierarquia (um chunk
de material pode conter sub-chunks
de cor, textura, etc) e além de tudo isso
simplifica o processo de leitura (ele não
se torna mais uma grande função que
lê byte a byte o arquivo, mas apenas que identifica
as fronteiras de chunk). Por exemplo, um (pseudo-)
código para leitura de arquivos 3ds (à
partir de um stream) poderia ser:
procedure ler3ds(str: TStream); var wd: word; size: integer; begin str.read(wd, 2); //id do chunk principal str.read(size, 4); //tamanho do chunk principal //enquanto houver dados a serem lidos while str.position < str.size do begin str.read(wd, 2); //id desse chunk str.read(size, 4); //tamanho desse chunk if wd = $0010 then lerChunkRGB(str, size) else if wd = $1100 then lerChunkBackgroundImg(str,size) else str.position:= str.position + size; end; end; |
Esse
pequeno trecho de código seria capaz de analisar
o arquivo 3ds e carregar os chunks
que identificam uma cor RGB no formato ponto flutuante
e a imagem de fundo do viewport. Caso qualquer outro
tipo seja encontrado, ele é simplesmente
ignorado (pulando um número de bytes igual
ao seu tamanho).
Entretanto,
na maioria das vezes é possível encontrar
pela internet bibliotecas para carregamento de arquivos
para o Delphi já prontas. Entre os vários
tipos de arquivos, pode-se encontrar o próprio
3ds, imagens gif e até mesmo png.
E se
essa biblioteca ainda não existir, nós
da TILT
temos certeza de que agora você mesmo será
capaz de construí-la.
Portanto,
mãos à obra!
Referência online da Microsoft sobre as funções
de leitura de arquivos da API do Windows.
Biblioteca de importação de arquivos
3ds de Mike Lischke.