Manipulando arquivos no Delphi
Matheus Degiovani

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:

createFile: Cria/Abre um arquivo para leitura ou escrita (ou ambos).
writeFile: Escreve o conteúdo de uma variável para o arquivo.
readFile: Lê um determinado número de bytes para uma variável.
closeHandle: Fecha um arquivo aberto anteriormente.

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:

AssignFile: Associa um nome de arquivo à uma variável de arquivo.
Rewrite: Prepara um arquivo associado anteriormente para escrita.
Reset: Prepara um arquivo associado anteriormente para leitura.
Write: Escreve uma variável para um arquivo.
Read: Lê uma variável de um arquivo.
Writeln: Escreve uma linha para um arquivo tipo texto.
ReadLn: Lê uma linha de um arquivo do tipo texto.
CloseFile: Fecha um arquivo associado anteriormente.

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.


Download...
Clique no link para fazer o download dos arquivos. Se sua assinatura do club TILT está para vencer, clique aqui e saiba como renová-la.

Fontes completos do exemplo da matéria
 
online