quinta-feira, 15 de janeiro de 2009

Delphi e MySQL de outra forma

Tutorial: libmysql.dll através do mysql.pas no Delphi / MySQL 5


Autor: João Pinto Neto
E-mail: joaopintoneto (arroba) gmail (ponto) com


Introdução

Existem várias formas para acessar os dados de uma Base de Dados MySQL no Delphi, o mais comum é através do driver ODBC adequado à versão do MySQL que se deseja utilizar. Neste tutorial eu ensinarei como obter acesso através da biblioteca libmysql.dll, uma alternativa muito interessante para trabalhar em um nível de comunicação quase que direto, passando as funções da DLL diretamente para o Banco de Dados. A biblioteca libmysql.dll é uma DLL que vem incluída na instalação padrão do MySQL, ela possui todas as funções necessárias para comunicação direta. E para utilizar essas funções é preciso criar um link de cada uma no projeto em que se deseja acessar o Banco de Dados, tarefa essa que esta implementada em C e que originalmente também vem com a instalação padrão, foi traduzida e adaptada por Matthias Fichtner para Delphi (4 ou superior), com o nome de mysql.pas, é nessa Unit que se encontram as funções. Ela pode ser encontrada para download no site: http://www.fichtner.net/delphi/mysql/. Porém, o autor anunciou em seu site a descontinuação de atualizações para essa Unit e está com o projeto parado dês de o MySQL 3.23.49. Mas esta Unit funciona com a libmysql.dll do MySQL 5. Este tutorial considera que você já esteja com o MySQL 5 instalado, vá ao site http://www.mysql.com/ para fazer o download do instalador ou do pacote .zip que contem os binários para windows e siga os passos para instalar e iniciar o Banco de Dados, caso você ainda não tenha feito.

Criando a Base de Dados

Abra um prompt de comando do windows e entre na pasta dos arquivos binários do MySQL, conecte-se como root para criar a nova Base de Dados, a tabela e o usuário do Banco de Dados que utilizaremos no decorrer de todo o tutorial, para isto, execute os seguintes passos:

1- Criar a Base de Dados e o usuário:

: Entre na pasta dos binários do MySQL
: Conecte como root, se tiver senha use o parâmetro -p

C:\> cd \mysql\bin
C:\mysql\bin> mysql -u root

-- Crie a Base de Dados
mysql> create database agenda;
Query OK, 1 row affected (0.08 sec)

-- Mude a Base de Dados atual
mysql> use agenda
Database changed

-- Crie um novo usuário
mysql> grant all privileges on agenda.* to agenda1 identified by "agenda123";
Query OK, 0 rows affected (0.03 sec)

-- Vamos testar o usuário que acabamos de criar, desconecte como root
mysql> quit
Bye

2- Criar as tabelas necessárias:

: Conecte com o usuário agenda1 na Base de Dados agenda
C:\mysql\bin>mysql -u agenda1 -p agenda
Enter password: *********

-- Tabela dos contatos
mysql> create table contatos(
-> con_codigo smallint(6) not null auto_increment,
-> con_nome varchar(40) default NULL,
-> con_endereco varchar(60) default NULL,
-> con_bairro varchar(20) default NULL,
-> con_cep char(8) default NULL,
-> con_cidade varchar(40) default NULL,
-> con_telefone varchar(16) default NULL,
-> con_celular varchar(16) default NULL,
-> con_email varchar(255) default NULL,
-> primary key(con_codigo)
-> );
Query OK, 0 rows affected (0.08 sec)

3- Cadastrar alguns contatos:

mysql> insert into contatos (con_nome) values
-> ('JOAO PINTO NETO'),
-> ('JOSE DA SILVA'),
-> ('BILLIE JOE ARMTRONG'),
-> ('CURT COBAIN'),
-> ('AXEL ROSE'),
-> ('ROBERT DE NIRO');
Query OK, 6 rows affected (0.05 sec)
Registros: 6 - Duplicados: 0 - Avisos: 0

Preparando a interface

Primeiramente teremos que criar uma pasta para o novo projeto, crie o diretório C:\AGENDA. Agora coloque a biblioteca que vem junto com o MySQL C:\mysql\lib\opt\libmysql.dll e também a Unit mysql.pas no diretório do projeto. Faça o download do projeto completo, com a DLL libmysql.dll, a Unit e o codigo-fonte completo aqui.

Agora abra o Delphi e crie uma nova aplicação, mande salvar tudo (Save All...), quando pedir nome do arquivo para Unit1, salve como main.pas e o projeto, salve como agenda. Insira 2 componentes Panels, selecione o Panel1 e inclua 10 SpeedButtons, selecione o Panel2 e inclua 9 componentes Label e 9 componentes Edit. Agora altere as propriedades no Object Inspector dos componentes:

Form1
Caption = Agenda
BorderStyle = bsSingle
BorderIcons = biMaximize = False

Panel1
Caption = deixe nulo
BevelOuter = bvNone
Align = alTop
Height = 48

Panel2
Caption = deixe nulo
BevelOuter = bvNone
Align = alClient

Altere a propriedade Name dos SpeedButtons para facilitar a programação do código de cada um:

SpeedButton1 = btnPrimeiro
SpeedButton2 = btnAnterior
SpeedButton3 = btnProximo
SpeedButton4 = btnUltimo
SpeedButton5 = btnNovo
SpeedButton6 = btnEditar
SpeedButton7 = btnSalvar
SpeedButton8 = btnCancelar
SpeedButton9 = btnDeletar
SpeedButton10 = btnFechar

Altere as propriedades dos componentes para que o formulário fique semelhante à figura abaixo:
Form1

Codificação

Antes de começar a explicar os procedimentos do código-fonte do exemplo, darei uma explicação básica de como fazer uma conexão com o Banco de Dados MySQL. Precisaremos definir 3 variáveis, essas variáveis na verdade serão ponteiros apontando uma para conexão (myData), outra para os resultados da query SQL (myRes) e outra que aponta as linhas do resultado.

Linha 01: inicia o ponteiro myData; Linha 02: estabelece a conexão com o banco; Linha 03: seleciona a Base de Dados; Linha 04: executa consulta SQL; Linha 05: pega os resultados da consulta SQL em myRes; Linha 06: Pega a linha 0 do resultado de myRes em myRow; Linha 07: exibe mensagem A; Linha 08: Pega a linha 1 do resultado de myRes em myRow; Linha 09: exibe mesagem B; Linha 10: Fecha a conexão; Linha 11: myData aponta para nil (nada); myRow[X] é a coluna, onde X é o número da coluna e a coluna é o campo da tabela.

{ Linha 01 } myData:= mysql_init(myData);
{ Linha 02 } mysql_real_connect(myData,PChar('localhost'),PChar('USUARIO'),PChar('SENHA'),nil,3306,nil,0);
{ Linha 03 } mysql_select_db(myData,PChar('DATABASE'));
{ Linha 04 } mysql_query(myData,PChar('SELECT NOW()'));
{ Linha 05 } myRes:= mysql_store_result(myData);
{ Linha 06 } myRow:= mysql_fetch_row(myRes); // Aqui o valor é A
{ Linha 07 } ShowMessage(myRow[0]); // Exibe A
{ Linha 08 } myRow:= mysql_fetch_row(myRes); // Aqui o valor é B
{ Linha 09 } ShowMessage(myRow[0]); // Exibe B
{ Linha 10 } mysql_close(myData);
{ Linha 11 } myData:= nil;

A biblioteca libmysql.dll torna a codificação parecida com PHP, que recupera os dados através de uma consulta SQL e fecha a conexão. Para manipular os registros, criaremos um índice na chave primária. O índice será um TStringList (variável Registros) que armazenará todos os códigos da tabela contatos de uma consulta SQL na tabela contatos, cada código terá uma posição no índice, tornando possível a navegação entre os registros.
Para identificar o estado das tabelas, criaremos o tipo TMySQLState (variável Estado) com 3 estados: msEdit, msInsert e msBrowse. Assim, será possível saber se o que está acontecendo com a tabela. Será necessário armazenar o código atual (variável Codigo) para poder edita-lo ou exclui-lo e a posição dele no índice TStringList, aramazenando o índice (variável Indice) para identificar se o índice está no início (BOF) ou no fim (EOF) da tabela. Criaremos o tipo TMySQLPos (variável Posicao) com 3 estados: BOF, NORMAL e EOF para armazenar esta posição do índice.

Declarando constantes, variáveis, funções e procedimentos:

unit main;
interface
uses
// Inclua a Unit mysql
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls,
Forms, Dialogs, Buttons, ExtCtrls, Grids, StdCtrls, mysql;

type
// Declaração de tipos adicionais
TMySQLState = (msEdit, msInsert, msBrowse);
TMySQLPos = (BOF, NORMAL, EOF);

type
TForm1 = class(TForm)
{ . . . }
// Declaração de procedimentos adicionais
procedure LimpaEdits;
procedure AtualizaBarra;
procedure AtualizaRegistros(TUDO: Boolean);
procedure AbreContato(Cod: ShortString);
{ . . . }
private
{ Private declarations }
myData: PMYSQL; // Ponteiro de conexão do MySQL
myRes: PMYSQL_RES; // Ponteiro de resultado da query
myRow: PMYSQL_ROW; // Ponteiro de linha do resultado
public
{ Public declarations }
SQL: PAnsiChar; // SQL
Estado: TMySQLState; // Estado dos registros na tabela
Posicao: TMySQLPos; // Posição dos registros na tabela
Codigo: ShortString; // Código atual
Indice: integer; // Índice do código atual
Registros: TStringList; // Todos os códigos da tabela
end;

var
Form1: TForm1;

const
// Constantes de conexão
myDB: PAnsiChar = 'agenda';
myUser: PAnsiChar = 'agenda1';
myPass: PAnsiChar = 'agenda123';
myLocalHost: PAnsiChar = 'localhost';

Procedimento LimpaEdits:

O procedimento LimpaEdits, serve para limpar o valor Text de todos os componentes Edits do formulário de uma vez.

procedure TForm1.LimpaEdits;
var x: byte;
begin
for x:= 0 to ComponentCount - 1 do
if Components[x].ClassType = TEdit
then (Components[x] as TEdit).Clear;
end;

Procedimento AtualizaBarra:

O procedimento AtualizaBarra, serve para atualizar a barra de botões, habilitando e desabilitando cada um de acordo com o Estado (msBrowse = Navegação, msInsert = Novo registro e msEdit = Edição do registro), Posicao (BOF = Primeiro registro, NORMAL = Nem no primeiro e nem no último e EOF = Último registro) e a quantidade dos registros.

procedure TForm1.AtualizaBarra;
begin
btnPrimeiro.Enabled:= (Estado = msBrowse) and (Posicao in [EOF, NORMAL]) and (Registros.Count > 0);
btnAnterior.Enabled:= (Estado = msBrowse) and (Posicao <> BOF) and (Registros.Count > 0);
btnProximo.Enabled:= (Estado = msBrowse) and (Posicao <> EOF) and (Registros.Count > 1);
btnUltimo.Enabled:= (Estado = msBrowse) and (Posicao in [BOF, NORMAL]) and (Registros.Count > 1);
btnNovo.Enabled:= (Estado = msBrowse);
btnEditar.Enabled:= (Estado = msBrowse) and (Registros.Count > 0);
btnSalvar.Enabled:= (Estado in [msInsert, msEdit]);
btnCancelar.Enabled:= (Estado in [msInsert, msEdit]);
btnDeletar.Enabled:= (Estado in [msBrowse, msEdit]) and (Registros.Count > 0);
Panel2.Enabled:= (Estado in [msInsert, msEdit]);
end;

Procedimento AtualizaRegistros:

O procedimento AtualizaRegistros, serve para atualizar o índice (Registro) de códigos, a opção TUDO, é para, se caso True, atualizar todos os códigos dentro do índice, ou no caso de False, atualizar apenas a posição (Posicao) e o índice (Indice).

procedure TForm1.AtualizaRegistros(TUDO: Boolean);
var x: integer;
begin
if TUDO then
begin
SQL:= PChar('select con_codigo from contatos order by con_nome');
Registros.Clear;
myData:= mysql_init(myData);
if (myData = nil)
or (mysql_real_connect(myData,myLocalHost,myUser,myPass,nil,3306,nil,0) = nil) then
begin
MessageDlg('Conexão perdida, o Banco de Dados parece estar off-line.',mtError,[mbOK],0);
mysql_close(myData);
myData:= nil;
end else
begin
mysql_select_db(myData,myDB);
mysql_query(myData,SQL);
myRes:= mysql_store_result(myData);

for x:= 0 to mysql_num_rows(myRes) - 1 do
begin
myRow:= mysql_fetch_row(myRes);
Registros.Add(myRow[0]);
end;

mysql_free_result(myRes);
mysql_close(myData);
myData:= nil;
myRes:= nil;
myRow:= nil;
end;
end;

if Registros.IndexOf(Codigo) <= 0 then Posicao:= BOF
else if Registros.IndexOf(Codigo) >= Registros.Count - 1
then Posicao:= EOF
else Posicao:= NORMAL;
Indice:= Registros.IndexOf(Codigo);
end;

Procedimento AbreContato:

O procedimento AbreContato, serve para recuperar todos os dados relativos a um determinado código (Cod) nos Edits do formulário e no fim do procedimento, aciona o procedimento AtualizaRegistros(False) para atualizar a posição do índice (Registro). Devemos prestar atenção quando o parâmetro Cod é informado, não é o seu índice que é passado e sim o código real do contato, aquele que está cadastrado na tabela no campo con_codigo.

procedure TForm1.AbreContato(Cod: ShortString);
begin
SQL:= PChar('select * from contatos where con_codigo = '+Cod+' limit 1');
myData:= mysql_init(myData);
if (myData = nil)
or (mysql_real_connect(myData,myLocalHost,myUser,myPass,nil,3306,nil,0) = nil) then
begin
MessageDlg('Conexão perdida, o Banco de Dados parece estar off-line.',mtError,[mbOK],0);
mysql_close(myData);
myData:= nil;
end else
begin
mysql_select_db(myData,myDB);
mysql_query(myData,SQL);
myRes:= mysql_store_result(myData);
myRow:= mysql_fetch_row(myRes);
Codigo:= myRow[0];
Edit1.Text:= myRow[0];
Edit2.Text:= myRow[1];
Edit3.Text:= myRow[2];
Edit4.Text:= myRow[3];
Edit5.Text:= myRow[4];
Edit6.Text:= myRow[5];
Edit7.Text:= myRow[6];
Edit8.Text:= myRow[7];
Edit9.Text:= myRow[8];
mysql_free_result(myRes);
mysql_close(myData);
myRow:= nil;
myRes:= nil;
myData:= nil;
end;
AtualizaRegistros(False);
end;

Procedimento FormCreate:

Abra o procedumento FormCreate e inclua o código abaixo. Quando o Form for criado, o índice Registros é criado, Estado inicia em modo navegação (msBrowse), o índice dos códigos é preenchido em AtualizaRegistros(True), se a quantidade de Registros for maior que 0, então abre o primeiro registro de Registros. Strings[0] quer dizer para passar como parâmetro o código (con_codigo) que esta na posição 0 das strings do Registros. E por fim, AtualizaBarra, que habilita ou desabilita os botões de controle do formulário.

procedure TForm1.FormCreate(Sender: TObject);
begin
Registros:= TStringList.Create;
Estado:= msBrowse;
AtualizaRegistros(True);
if (Registros.Count > 0) then AbreContato(Registros.Strings[0]);
AtualizaBarra;
end;

Procedimento FormDestroy:

Se criamos o índice de Registros, temos que liberar ele no FormDestroy.

procedure TForm1.FormDestroy(Sender: TObject);
begin
Registros.Free;
end;

Procedimento btnPrimeiroClick:

Dê duplo clique no botão btnPrimeiro e inclua o o código abaixo. Registros.Strings[0] é o primeiro código, como já foi dito anteriormente e se quisermos ir para o segundo código informamos Registros.Strings[1], assim por diante.

procedure TForm1.btnPrimeiroClick(Sender: TObject);
begin
AbreContato(Registros.Strings[0]);
AtualizaBarra;
end;

Procedimento btnAnteriorClick:

Dê duplo clique no botão btnAnterior e inclua o o código abaixo. A verificação de Posicao <> BOF, garante que se Posicao estiver no início da tabela, não volte, pois estaria fora do contexto selecionado em Registros e depois atualiza os botões de controle do formulário. Indice é a posição atual do índice então, Indice menos 1 será o registro anterior.

procedure TForm1.btnAnteriorClick(Sender: TObject);
begin
if Posicao <> BOF then AbreContato(Registros.Strings[Indice-1]);
AtualizaBarra;
end;

Procedimento btnProximoClick:

Dê duplo clique no botão btnProximo e inclua o código abaixo. Aqui a verificação é para o fim da tabela e o código será o íncice atual Indice mais 1.

procedure TForm1.btnProximoClick(Sender: TObject);
begin
if Posicao <> EOF then AbreContato(Registros.Strings[Indice+1]);
AtualizaBarra;
end;

Procedimento btnUltimoClick:

Dê duplo clique no botão btnUltimo e inclua o código abaixo. Registros.Count retorna o número de itens de Registros, assim é possível saber quantos registros a tabela têm e ir ao último, retira-se menos 1, por calsa da contagem de ítens começar em 1.

procedure TForm1.btnUltimoClick(Sender: TObject);
begin
AbreContato(Registros.Strings[Registros.Count-1]);
AtualizaBarra;
end;

Procedimento btnNovoClick:

Dê duplo clique no botão btnNovo e inclua o código abaixo. Estado muda para msInsert e habilita os Edits para cadastrar um novo registro. LimpaEdits limpa o Text de todos os Edits do formulário.

procedure TForm1.btnNovoClick(Sender: TObject);
begin
Estado:= msInsert;
AtualizaBarra;
LimpaEdits;
end;

Procedimento btnEditarClick:

Dê duplo clique no botão btnEditar e inclua o código abaixo. A diferença entre btnEditarClick e btnNovoClick é o Estado em msEdit e o Text dos Edits são mantidos.

procedure TForm1.btnEditarClick(Sender: TObject);
begin
Estado:= msEdit;
AtualizaBarra;
end;

Procedimento btnCancelarClick:

Dê duplo clique no botão btnCancelar e inclua o código abaixo. Se o Estado for msEdit ou msInsert, neste procedimento, ele volta para o modo de navegação entre registros msBrowse e abre o contato da posição Indice, que indica o código do último registro aberto, esta função é necessária porque o Estado em msEdit limpa os Edits, então, temos que carrega-los novamente.

procedure TForm1.btnCancelarClick(Sender: TObject);
begin
Estado:= msBrowse;
AbreContato(Registros.Strings[Indice]);
AtualizaBarra;
end;

Procedimento btnSalvarClick:

Dê duplo clique no botão btnSalvar e inclua o código abaixo. O comando SQL se altera para caso Estado = msInsert, então o SQL muda para inserção, ou Estado = msEdit, então SQL muda para edição. Na Linha 16 a variável myData é iniciada como um ponteiro do tipo PMYSQL, se o ponteiro não conseguir iniciar ele aponta para nil, que significa apontar "para o nada" e também se a conexão falhar ela aponta para nil, se estas 2 condições forem verdadeiras, então é preciso fechar a conexão e anular o ponteiro myData, setando o seu valor como nil, uma mensagem de erro aparece para o usuário dizendo que "o Banco de Dados parece estar off-line". Na Linha 25, seleciona a database myDB (que corresponde à agenda). Então se Estado = msInsert, verifica se já existe algum outro contato de nomes identicos (veja Linha 28) avisa o usuário e cansela, esta verificação é apenas para ilustrar essa possibilidade, neste caso não haveria necessidade, se não existir nenhum nome igual, executa comando SQL para inserir o novo registro e sai do procedimento (veja linha 52), caso contrário, se Estado = msEdit, executa o comando SQL para atualizar os dados (veja linha 54, ela só executa para Estado = msEdit). No fim, o Estado volta para msBrowse.

procedure TForm1.btnSalvarClick(Sender: TObject);
var Cod: ShortString;
begin
Panel2.Enabled:= False;
// Insert
if Estado = msInsert then
SQL:= PChar('insert into contatos(con_nome,con_endereco,con_bairro,con_cep,con_cidade,con_telefone,con_celular,con_email) ' +
'values("' + Trim(Edit2.Text) + '","' + Trim(Edit3.Text) + '","' + Trim(Edit4.Text) + '","' + Trim(Edit5.Text) + '","' + Trim(Edit6.Text) + '","' +
Trim(Edit7.Text) + '","' + Trim(Edit8.Text) + '","' + Trim(Edit9.Text) + '")')
else // Edit
SQL:= PChar('update contatos set ' +
'con_nome = "' + Trim(Edit2.Text) + '", con_endereco = "' + Trim(Edit3.Text) + '", con_bairro = "' + Trim(Edit4.Text) + '", con_cep = "' +
Trim(Edit5.Text) + '", con_cidade = "' + Trim(Edit6.Text) + '", con_telefone = "' + Trim(Edit7.Text) + '", con_celular = "' + Trim(Edit8.Text) +
'", con_email = "' + Trim(Edit9.Text) + '" where con_codigo = ' + Codigo);

myData:= mysql_init(myData); { Linha 16 }
if (myData = nil)
or (mysql_real_connect(myData,myLocalHost,myUser,myPass,nil,3306,nil,0) = nil) then
begin
MessageDlg('Conexão perdida, o Banco de Dados parece estar off-line.',mtError,[mbOK],0);
mysql_close(myData);
myData:= nil;
end else
begin
mysql_select_db(myData,myDB); { Linha 25 }
if Estado = msInsert then
begin
mysql_query(myData,PChar('select con_codigo from contatos where con_nome like "'+Edit2.Text+'"')); { Linha 28 }
myRes:= mysql_store_result(myData);
// Se já existir usuario
if mysql_num_rows(myRes) > 0 then
begin
MessageDlg('"'+Edit2.Text+'" já está cadastrado.',mtInformation,[mbOK],0);
mysql_close(myData);
myData:= nil;
Panel2.Enabled:= True;
Exit;
end;
mysql_query(myData,SQL);
mysql_query(myData,PChar('select con_codigo from contatos where con_nome like "'+Trim(Edit2.Text)+'"'));
myRes:= mysql_store_result(myData);
myRow:= mysql_fetch_row(myRes);
Cod:= myRow[0];
mysql_close(myData);
myData:= nil;
myRes:= nil;
myRow:= nil;
Estado:= msBrowse;
AtualizaRegistros(True);
AbreContato(Cod);
AtualizaBarra;
Exit; { Linha 52 }
end;
mysql_query(myData,SQL); { Linha 54 }
mysql_close(myData);
myData:= nil;
end;

Estado:= msBrowse;
AbreContato(Registros.Strings[Indice]);
AtualizaBarra;
end;

Procedimento btnDeletarClick:

Dê duplo clique no botão btnDeletar e inclua o código abaixo. Se a resposta for não para deletar o registro, então sai do procedimento e não executa nada. Senão, deleta o registro de código Codigo, atualiza o índice Registros e abre outro contato, tentando abrir primeiro um anterior, se não tiver nenhum antes, tenta abrir um depois.

procedure TForm1.btnDeletarClick(Sender: TObject);
begin
if (MessageDlg('Deseja remover "'+Edit2.Text+'" do registro definitivamente?',mtConfirmation,[mbYes,mbNo],0) = mrNo)
then Exit;
myData:= mysql_init(myData);
if (myData = nil)
or (mysql_real_connect(myData,myLocalHost,myUser,myPass,nil,3306,nil,0) = nil) then
begin
MessageDlg('Conexão perdida, o Banco de Dados parece estar off-line.',mtError,[mbOK],0);
mysql_close(myData);
myData:= nil;
end else
begin
mysql_select_db(myData,myDB);
// Deleta o contato
SQL:= PChar('delete from contatos where con_codigo = '+Codigo);
mysql_query(myData,SQL);
mysql_close(myData);
myData:= nil;
end;
AtualizaRegistros(True);
if Registros.Count > 0 then
try
AbreContato(Registros.Strings[Indice-1]);
except
AbreContato(Registros.Strings[Indice+1]);
end;
AtualizaBarra;
end;

Nenhum comentário: