FireMonkey e FireDAC: Construindo uma aplicação completa
FireMonkey e FireDAC: Construindo uma aplicação completa – Parte 1
Neste artigo veremos como desenvolver uma aplicação completa de Ordens de Serviço utilizando o FireMonkey para construir as interfaces gráficas, e o FireDAC para o acesso a dados.
Artigos Delphi FireMonkey e FireDAC: Construindo uma aplicação completa – Parte 1
Aplicação Completa com FireMonkey e FireDAC
Atualmente o Delphi dispõe de dois frameworks para o desenvolvimento visual de aplicações: o VCL (Visual Component Library) e o FireMonkey. O primeiro é muito conhecido pelos desenvolvedores desktop, pois acompanha o Delphi desde as primeiras versões, e é caracterizado por funcionar somente no Windows. Por outro lado, o FireMonkey é um framework mais recente, e sua característica principal é gerar aplicações nativas tanto para Windows quanto para OS X, Android e iOS. Dessa forma, é importante o conhecimento sobre como utilizar os componentes desse framework para construir aplicações também com características desktop, porém que possam ser executadas em um número maior de dispositivos.
O Delphi sempre foi considerado uma das melhores IDEs para o desenvolvimento de aplicações desktop, possuindo uma gama de recursos que agilizam muito o trabalho dos programadores e contribuem para o rápido desenvolvimento de softwares comerciais. Nesse contexto, o VCL é o conjunto de componentes mais utilizado para a programação de softwares nesse estilo, sendo compatível somente com o Windows. Porém, como a utilização de diferentes sistemas operacionais (como OS X, Android e iOS) cresceu muito nos últimos anos, o Delphi passou por uma evolução natural, e foi criado o framework FireMonkey, que é compatível com todos esses sistemas operacionais, inclusive com o Windows.
Curso: O que é FireDAC?
Portanto, uma vantagem do FireMonkey sobre o VCL é que ele permite que um software seja compilado com código nativo para múltiplos dispositivos sem a necessidade de alterações no código fonte, desde que esse seja criado com componentes compatíveis. Dessa forma, esse framework oferece muitas vantagens para os desenvolvedores, porém existem algumas diferenças com relação a alguns componentes, como a ausência do DBGrid, que é um dos componentes mais utilizados para a listagem de registros. Esse fato acaba gerando um problema no processo de adaptação que os desenvolvedores precisam passar para começarem a utilizar o FireMonkey ao invés do VCL (quando aplicações multidispositivos são necessárias).
Tendo essa necessidade como base, o objetivo deste artigo é apresentar uma visão prática de como construir uma aplicação completa passo a passo utilizando o framework FireMonkey, o Delphi 10.1 Berlin e a engine FireDAC de acesso a dados. Nesta primeira parte serão mostrados os processos para a criação da base de dados de um cenário de ordens de serviços de um auto center, a definição do menu principal com acesso às janelas do sistema e, por fim, o desenvolvimento de uma tela padrão de pesquisa, que será utilizada como janela base para as outras telas (utilizando herança). As próximas seções mostram o desenvolvimento completo do software.
Curso: O que é FireMonkey?
Entendendo a base de dados
A Figura 1 apresenta o modelo entidade relacionamento da base de dados, a qual é composta por seis tabelas. Na parte central do diagrama, encontra-se a tabela m_ordem_servico, a qual armazenará as principais informações sobre o cenário proposto. Para isso, ela se liga por meio de chaves estrangeiras às tabelas de clientes, formas de pagamento e veículos, ou seja, quando uma ordem de serviço for preenchida pelo usuário o mesmo terá que informar esses campos.
Considerando o cenário de auto center, quando um veículo é enviado para manutenção vários serviços podem ser realizados, como geometria, balanceamento, rodízio de pneus, lavação, entre vários outros. Dessa forma, uma ordem de serviço pode possuir vários serviços, enquanto que um mesmo tipo de serviço pode ser executado em várias ordens de serviço. Esse raciocínio nos leva a conclusão que esse relacionamento é do tipo muitos para muitos (N-N), o que requer que seja criada uma nova tabela, comumente chamada de entidade associativa, a qual fará a ligação entre as tabelas m_ordem_servico e c_servico. Esse relacionamento é demonstrado pela tabela m_ordem_servico_servico, que possui duas chaves estrangeiras: uma relacionando com a tabela m_ordem_servico e outra com a tabela c_servico. Em outras palavras, esse vínculo entre as tabelas caracteriza o recurso de mestre-detalhe, que é muito comum e de extrema importância para qualquer sistema comercial.
É importante enfatizar que a tabela c_servico armazenará o cadastro de todos os serviços ofertados pela loja, enquanto que a entidade associativa tem a função de registrar somente os serviços relacionados a uma ordem de serviço.
Figura 1. Modelo entidade relacionamento da base de dados.
Após o entendimento da estrutura da base de dados, o próximo passo é efetivamente criá-la no MySQL por meio da linguagem SQL. A Listagem 1 apresenta os scripts para a definição das tabelas de cadastro, as quais possuem o prefixo "c" no início do nome. Na linha 01 a base de dados é criada, com o nome ordem_servico, enquanto que o restante do código apresenta a definição das tabelas de formas de pagamento (linhas 03 até 08), serviços (linhas 09 até 14), veículos (linhas 15 até 21) e clientes (linhas 22 até 30).
É possível observar que todas as tabelas possuem uma chave primária do tipo autoincremento, ou seja, os identificadores serão gerados automaticamente pelo MySQL. A engine de acesso a dados que usaremos será o FireDAC, o qual já possui recursos nativos para identificar campos do tipo autoincremento automaticamente independente do sistema gerenciador de banco de dados (consulte a seção Links para mais detalhes).
Listagem 1. Criação das tabelas de cadastro.
01 create database ordem_servico;
02 use ordem_servico;
03 create table c_forma_pagamento (
04   idforma_pagamento int not null auto_increment,
05   nome varchar(20) not null,
06   observacao text,
07   constraint pk_fp_idforma_pagamento primary key (idforma_pagamento)
09 create table c_servico (
10   idservico int not null auto_increment,
11   nome varchar(30) not null,
12   observacao text,
13   constraint pk_sv_idservico primary key (idservico)
15 create table c_veiculo (
16   idveiculo int not null auto_increment,
17   placa char(8) not null,
18   modelo varchar(25) not null,
19   observacao text,
20   constraint pk_vc_idveiculo primary key (idveiculo)
22 create table c_cliente (
23   idcliente int not null auto_increment,
24   nome varchar(50) not null,
25   data_nascimento date,
26   cpf char(11),
27   rg char(10),
28   observacao text,
29   constraint pk_cl_idcliente primary key (idcliente)
A Listagem 2, por sua vez, mostra os scripts para a inserção de alguns registros nas tabelas de cadastro, que serão utilizados para teste posteriormente. Conforme pode ser observado na listagem, são incluídos três registros para cada uma das quatro tabelas.
Listagem 2. Inserção de dados nas tabelas de cadastro.
01 insert into c_forma_pagamento (nome) values ('À vista');
02 insert into c_forma_pagamento (nome) values ('Boleto');
03 insert into c_forma_pagamento (nome) values ('Cartão');
04 insert into c_servico (nome) values ('Balanceamento');
05 insert into c_servico (nome) values ('Geometria');
06 insert into c_servico (nome) values ('Ajuste no freio');
07 insert into c_veiculo (placa, modelo) values ('AAA-8945', 'Fusion');
08 insert into c_veiculo (placa, modelo) values ('BBB-7451', 'Passat');
09 insert into c_veiculo (placa, modelo) values ('CCC-1254', 'Jetta');
10 insert into c_cliente (nome, data_nascimento, cpf, rg) values ('Jones', '1983-07-15', '11111111111', '1111111');
11 insert into c_cliente (nome, data_nascimento, cpf, rg) values ('Adaiane', '1985-11-11', '22222222222', '222222');
12 insert into c_cliente (nome, data_nascimento, cpf, rg) values ('Ana', '1965-04-21', '33333333333', '3333333');
A Listagem 3 apresenta os scripts para a criação das duas tabelas de movimentação, ou seja, a das ordens de serviço (linhas 01 até 14) e a dos serviços relacionados (linhas 15 até 26). Na primeira tabela pode-se notar a existência de três chaves estrangeiras (linhas 11, 12 e 13), as quais estão vinculadas às tabelas de clientes, veículos e formas de pagamento. O mesmo acontece com a tabela de serviços da ordem de serviço, que possui uma chave estrangeira que a vincula com a ordem de serviço (relacionamento N-N mestre-detalhe — linha 24) e outra vinculando-a com a tabela de cadastro de serviços (linha 25).
Note que, diferentemente das tabelas de cadastro, os nomes das tabelas de movimentação recebem o prefixo "m". Essa diferenciação das tabelas por meio do prefixo pode ser considerada uma boa prática para diferenciação da função de cada uma delas, ou seja, enquanto as tabelas de cadastro armazenam os dados básicos do software, as tabelas de movimentação efetivamente apresentam os dados transacionais da empresa.
Listagem 3. Criação das tabelas de movimentação.
01 create table m_ordem_servico (
02   idordem_servico int not null auto_increment,
03   idcliente int not null,
04   idveiculo int not null,
05   idforma_pagamento int not null,
06   numero char(15) not null,
07   data_ordem_servico date not null,
08   valor_total float not null,
09   observacao text,
10   constraint pk_osr_idordem_servico primary key (idordem_servico),
11   constraint fk_osr_idcliente foreign key (idcliente) references c_cliente (idcliente),
12   constraint fk_osr_idveiculo foreign key (idveiculo) references c_veiculo (idveiculo),
13   constraint fk_osr_idforma_pagamento foreign key (idforma_pagamento) references c_forma_pagamento (idforma_pagamento)
15 create table m_ordem_servico_servico (
16   idordem_servico_servico int not null auto_increment,
17   idordem_servico int not null,
18   idservico int not null,
19   quantidade int not null,
20   valor_unitario decimal(10,2) not null,
21   valor_total float not null,
22   observacao text,
23   constraint pk_oss_idordem_servico_servico primary key (idordem_servico_servico),
24   constraint fk_oss_idordem_servico foreign key (idordem_servico) references m_ordem_servico (idordem_servico),
25   constraint fk_oss_idservico foreign key (idservico) references c_servico (idservico)
A Listagem 4 mostra os scripts para inserção de alguns registros para teste nas duas tabelas de movimentação. Entre as linhas 01 e 03 são inseridas três ordens de serviço, enquanto que no restante da listagem encontra-se o código para inserir os respectivos serviços. Na linha 04 um serviço é inserido para a primeira ordem (idordem_servico = 1), nas linhas 05 e 06 são adicionados dois serviços para a segunda (idordem_servico = 2) e, por fim, na linha 07, um serviço é incluído para a terceira ordem de serviço (idordem_servico = 3).
Listagem 4. Inserção de dados nas tabelas de movimentação.
01 insert into m_ordem_servico (idcliente, idveiculo, idforma_pagamento, numero, data_ordem_servico, valor_total) values (1, 1, 1, '1234', '2017-02-24', 1000);
02 insert into m_ordem_servico (idcliente, idveiculo, idforma_pagamento, numero, data_ordem_servico, valor_total) values (2, 2, 1, '4567', '2017-02-25', 1200);
03 insert into m_ordem_servico (idcliente, idveiculo, idforma_pagamento, numero, data_ordem_servico, valor_total) values (2, 3, 2, '3258', '2017-02-26', 500);
04 insert into m_ordem_servico_servico (idordem_servico, idservico, quantidade, valor_unitario, valor_total) values (1, 1, 2, 500, 1000);
05 insert into m_ordem_servico_servico (idordem_servico, idservico, quantidade, valor_unitario, valor_total) values (2, 1, 2, 500, 1000);
06 insert into m_ordem_servico_servico (idordem_servico, idservico, quantidade, valor_unitario, valor_total) values (2, 2, 1, 200, 200);
07 insert into m_ordem_servico_servico (idordem_servico, idservico, quantidade, valor_unitario, valor_total) values (3, 3, 2, 250, 500);
É importante salientar que nos scripts de inserção de dados das Listagens 2 e 4, os campos chave primária (idordem_servico e idordem_servico_servico) não foram preenchidos porque estão definidos como autoincremento — o servidor é que será o responsável pela geração dos identificadores.
Criando o Data Module para acesso aos dados
Após a base de dados estar concluída, o próximo passo é criar uma nova aplicação multidispositivo no Delphi (File > New > Multi-Device Application – Delphi). Depois, salve o formulário principal com o nome de arquivo PrincipalFrm e configure sua propriedade name como frmPrincipal. Esse tipo de aplicação utiliza o FireMonkey como framework visual e possibilita que as aplicações possam funcionar em Windows, Mac (OSX), iOS e Android.
Agora, crie um Data Module (File > New > Other > Delphi Files > Data Module) com o nome de arquivo ConexaoDtm e a propriedade name com o valor dtmConexao. Esse Data Module será utilizado para centralizar o componente de conexão com a base de dados, e nele devem ser adicionados dois componentes (vide Figura 2): TFDConnection (cnnConexao) e TFDPhysMySQLDriverLink (driver), sendo que o primeiro efetivamente realizará a conexão com a base de dados, enquanto que o segundo representa o driver de acesso ao MySQL.
Figura 2. dtmConexao com os componentes driver e cnnConexao.
Clique duas vezes sobre o componente de conexão para abrir a janela FireDAC Connection Editor (Figura 3), na qual as seguintes propriedades devem ser configuras:
Driver ID: MySQL
Database: ordem_servico
Essas propriedades equivalem ao nome da base de dados, o usuário do MySQL, a senha (de acordo com o que foi definido na instalação do SGBD), a localização do servidor e a porta na qual os dados irão trafegar. Adicionalmente, a propriedade LoginPrompt desse componente deve ser marcada como False para que a senha não seja solicitada a cada nova tentativa de conexão.
Com relação ao componente driver, a única propriedade que deve ser configurada é a VendorLib, e ela deve apontar para o arquivo libmysql.dll, que se encontra na pasta de instalação do MySQL.
Figura 3. Configuração da conexão com a base de dados.
O formulário principal (frmPrincipal) exibe os botões e o menu de acesso para todas as janelas. Para obter uma visualização igual a apresentada na esquerda da Figura 4, primeiramente um componente TToolBar deve ser adicionado (tlbBarraFerramentas), dentro do qual devem ser inseridos dois TButtons com a propriedade Align configurada para Left (btnClientes e btnOrdemServico). Também deve ser adicionado ao formulário um componente TMainMenu (menu), cujos itens estão apresentados na direita da Figura 4 (utilize os botões Add Item e Add Child Item para obter uma lista igual a apresentada na figura). Vale citar que não é possível visualizar em tempo de design o resultado dessa configuração, ou seja, para obter o efeito apresentado o sistema precisa ser executado.
Figura 4. Menu principal do sistema.
Quando o usuário clicar em algum botão ou opção de menu, a janela correspondente será aberta na parte central do formulário. Para isso, um componente TLayout (lytPrincipal) (BOX 1) deve ser adicionado no centro da janela com a propriedade Align configurada como Client, para que, desse modo, ele ocupe toda a janela do formulário principal. Além disso, a propriedade WindowState do formulário principal deve ser mudada para wsMaximized para a janela ser exibida maximizada quando o sistema for executado.
BOX 1. TLayout
O componente TLayout é um container para outros objetos gráficos e é utilizado quando há a necessidade de organizar diversos componentes gráficos sob o mesmo componente pai, sendo bastante similar às estratégias de layout utilizadas na linguagem Java. Com a maior popularização do desenvolvimento de aplicativos mobile no Delphi, esse componente tornou-se importante para a organização e agrupamento de janelas, e pode ser considerado uma alternativa para o componente TPanel, muito utilizado para a organização das janelas. Consulte a seção Links para mais informações sobre o TLayout.
Quando se desenvolve aplicações utilizando o FireMonkey, deve-se tomar cuidado com os componentes a serem utilizados, pois alguns podem não estar disponíveis para todos os sistemas operacionais desejados. Uma maneira fácil de identificar a compatibilidade de um componente é posicionar o mouse sobre o nome do mesmo na Tool Palette. Por exemplo, ao posicionar o ponteiro do mouse sobre o componente TMainMenu, um hint surge informando que o mesmo é compatível com 64-bit Windows, 32-bit Windows e OS X. Com isso, podemos chegar à conclusão que esse componente não funcionará em uma aplicação Android ou iOS, por isso é sempre importante, antes de iniciar o desenvolvimento de um projeto, questionar em quais plataformas o software funcionará. Consulte a seção Links para maiores informações sobre menus no FireMonkey.
Um último passo com relação a parte visual da aplicação é a configuração das ações para abertura das janelas, que serão centralizadas no componente TActionList (actAcoes). Esse componente é bastante útil para manter uma lista de ações, que podem ser utilizadas por botões, menus e outros controles, e tem a vantagem de evitar a repetição de código. Em nosso menu principal, por exemplo, temos um botão e um menu para a abertura da janela de clientes, sendo que o código correspondente para essa ação ficará centralizado nesse componente, tornando desnecessária a replicação de código.
Dê um duplo clique sobre o componente para que o editor de ações seja aberto e adicione as novas ações clicando na opção New Action (primeiro botão da janela, sinalizado na Figura 5) até que o resultado seja igual ao mostrado na Figura 5. Para que as ações fiquem separadas por categorias (Categories na figura) é preciso indicar manualmente o nome da categoria na propriedade Category de cada ação. A codificação para abertura das janelas será feita posteriormente quando as janelas forem construídas. Por fim, mude a propriedade Action tanto das opções de menu quanto dos botões, de modo que eles estejam ligados às suas ações correspondentes.
Figura 5. Ações do menu principal.
Conforme mencionado anteriormente, as outras janelas do sistema serão abertas dentro do componente TLayout (lytPrincipal) quando o usuário executar alguma ação correspondente. Para que isso seja possível, é necessário definir um método que gerenciará os formulários construídos e os exibirá dentro desse layout. A Listagem 5 apresenta esse código, na qual pode-se notar que foi definida uma variável global do tipo TForm (FFormularioAtivo) na linha 02 e o procedimento AbrirJanela na linha 03, ambos na seção private do formulário. Como o próprio nome indica, essa variável é responsável por armazenar o formulário ativo, que o usuário está manipulando no momento.
Na linha 04 é dado início ao método para abrir as janelas. Primeiramente, na linha 08, é verificado se FFormularioAtivo possui algum valor (se aponta para algum formulário). Caso sim, na linha 10 é feita uma verificação para saber se o tipo da classe do formulário (ClassType) atualmente ativo na memória é igual ao parâmetro AClasseFormulario, que possui o formulário que o usuário está tentando abrir. Se essa condição for verdadeira, é um sinal de que o usuário está tentando abrir o mesmo formulário que já está aberto na aplicação. Neste caso, na linha 11 é invocado o método Exit, que simplesmente aborta o procedimento para evitar carregar dois objetos iguais na memória.
Caso a condição da linha 10 seja falsa, o fluxo de execução do programa será redirecionado para as linhas 14 e 15, que simplesmente retiram o formulário atualmente ativo da memória. Esse procedimento é feito utilizando o comando DisposeOf (linha 14) e atribuindo o valor nil para a variável FFormularioAtivo (linha 15).
Na linha 14 da Listagem 5 foi utilizado o comando DisposeOf para liberar o objeto da memória, mas também é possível utilizar o comando Free. A diferença entre ambos é que o Free passa o controle da liberação do objeto para o Garbage Collector (coletor de lixo) do sistema operacional, enquanto que o DisposeOf já libera a memória no momento em que é chamado. Na maioria dos casos, em aplicações desktop a escolha de qual componente usar não é tão importante, já que nesse tipo de sistema não há tanta preocupação com limitações de hardware. Por outro lado, em aplicações móveis, nas quais existem algumas limitações de hardware, é importante utilizar o DisposeOf para não sobrecarregar o coletor de lixo do sistema operacional do aparelho.
Após as verificações terem sido feitas, na linha 18 da Listagem 5 o novo formulário é efetivamente criado dentro da aplicação, ou seja, o parâmetro AClasseFormulário (que contém o novo formulário a ser aberto) é associado à variável global FFormularioAtivo por meio do comando CreateForm.
Listagem 5. Método para abertura das janelas.
02   FFormularioAtivo: TForm;
03   procedure AbrirJanela(AClasseFormulario: TComponentClass);
04   procedure TfrmPrincipal.AbrirJanela(AClasseFormulario: TComponentClass);
06   LayoutBase: TComponent;
08   if Assigned(FFormularioAtivo) then
09   begin
10     if FFormularioAtivo.ClassType = AClasseFormulario then
11       Exit
14       FFormularioAtivo.DisposeOf;
15       FFormularioAtivo := nil;
18   Application.CreateForm(AClasseFormulario, FFormularioAtivo);
19   LayoutBase := FFormularioAtivo.FindComponent('lytBase');
20   if Assigned(LayoutBase) then
21     lytPrincipal.AddObject(TLayout(LayoutBase));
Na linha 19 é utilizada a variável LayoutBase do tipo TComponent (declarada na linha 06), sendo que ela recebe um layout que estará presente no novo formulário que será criado. Conforme explanado anteriormente, as novas janelas serão abertas dentro do componente lytPrincipal do formulário principal, porém o que efetivamente deve ser adicionado dentro de um layout é um outro layout, e não um formulário. Para entender melhor essa questão, a parte superior da Figura 6 apresenta a estrutura da janela principal, na qual existe um componente TLayout (lytPrincipal) dentro do formulário (TForm). Similarmente, na parte inferior dessa figura é possível observar que há um formulário de cadastro (TForm) e dentro dele existe outro TLayout (lytBase), dentro do qual ficarão armazenados todos os componentes para manipulação dos registros, bem como botões, edits e grids. Note também que a flecha indica que tudo o que estiver dentro do lytBase será copiado para o lytPrincipal, ou seja, se algum componente estiver fora do layout não será carregado.
A Figura 6 explicou a função da linha 19 da Listagem 5, que é: o método FindComponent fará uma pesquisa no formulário ativo e localizará um componente com o nome lytBase, copiando-o para a variável LayoutBase. Como o nome lytBase é constante neste exemplo, todos os demais formulários deverão possuir um layout exatamente com esse mesmo nome, caso contrário os componentes não serão copiados. Por fim, na linha 20 é verificado se LayoutBase possui algum valor e, na linha 21, o layout do novo formulário é adicionado ao layout do formulário principal, código que representa a flecha na Figura 6.
Figura 6. TForm e TLayout.
Neste artigo optou-se pelo desenvolvimento da janela principal utilizando a abertura de uma tela por vez, que tem sido o padrão utilizado em grande parte das aplicações mais modernas. Consulte a seção Links para mais informações sobre aplicações MDI (Multiple Document Interface) no Delphi utilizando a VCL (Visual Component Library).
Criando o formulário padrão para listagem de registros
Quando o usuário clicar em alguma das opções do menu, primeiramente será carregado no formulário principal uma listagem completa de todos os registros correspondentes à respectiva tabela. Para evitar repetição de componentes no projeto, criaremos uma janela padrão (vide Figura 7) com conceitos de herança que servirá de base para todas as outras, já que as funcionalidades serão as mesmas tanto para as tabelas de cadastro quanto para as de movimentação.
Seguindo a opção de menu File > New > Multi-Device Form – Delphi será criado um novo formulário FireMonkey, o qual deve ser salvo com o nome ListagemPadraoFrm e ter sua propriedade name mudada para frmListagemPadrao. Para obter uma visualização semelhante à Figura 7, inicialmente deve ser adicionado um componente TLayout com o nome lytBase, o qual necessita ser exibido na janeira inteira (Align = Client) e servir como contêiner para os demais. Depois, uma TToolBar (tblBarraFerramentas) necessita ser adicionada ao topo com três TButtons (btnIncluir, btnAlterar e btnExcluir) com a propriedade Align configurada com o valor Left. Esses botões serão utilizados para abrir uma nova janela para inclusão e alteração e também para excluir o registro que estiver selecionado.
Abaixo da barra de ferramentas encontra-se um componente TPanel (pnlPesquisa) com a propriedade Align configurada para Top, o qual servirá posteriormente para a adição de componentes específicos para a realização de consultas SQL personalizadas. Por fim, encontra-se um componente TGrid (grdDados), que apresentará os registros retornados da base de dados. A vantagem de utilizar uma janela padrão é que os componentes visuais e o código fonte podem ser reaproveitados em outras telas, o que facilita a manutenção do software bem como a adição de novas funcionalidades, que podem ser automaticamente replicadas para todas as outras janelas.
Figura 7. Tela padrão para listagem de registros.
No FireMonkey não existe o componente TDBGrid, que é um dos mais utilizados para listagem de dados em aplicações desktop utilizando a VCL (Visual Component Library). Como alternativa, pode-se utilizar o TGrid ou o TListView.
Criando a listagem de ordens de serviço
Agora que temos nosso formulário padrão para a listagem de registros, construiremos a listagem de todas as ordens de serviço cadastradas na base de dados. A estrutura do projeto para cada janela será composta por dois arquivos: um novo Data Module, que conterá a consulta SQL para o retorno dos registros, e uma nova janela, que herdará da listagem padrão.
A Figura 8 apresenta o novo Data Module (File > New > Other > Delphi Files > Data Module), que deve ser salvo com o nome OrdemServicoDtm, ter sua propriedade name alterada para dtmOrdemServico e importar o arquivo ConexaoDtm para que tenha acesso ao componente de conexão (File > Use Unit). Note que o único componente adicionado é um TFDQuery (qryOrdemServico), que será responsável por executar a instrução SQL. As seguintes propriedades devem ser configuradas para esse componente:
Connection: dtmConexao.cnnConexao
SQL: select ors.idordem_servico, cln.nome as cliente, vlc.placa as veiculo, fpg.nome as forma_pagamento, ors.numero, ors.data_ordem_servico from m_ordem_servico ors inner join c_cliente cln on ors.idcliente = cln.idcliente inner join c_veiculo vlc on ors.idveiculo = vlc.idveiculo inner join c_forma_pagamento fpg on ors.idforma_pagamento = fpg.idforma_pagamento
A primeira propriedade liga o componente à conexão com a base de dados, enquanto que a segunda apresenta um código SQL completo para buscar todas as ordens de serviço, juntamente com o nome do cliente, a placa do veículo e a forma de pagamento, informações essas que estão armazenados em suas respectivas tabelas. A seguir deve-se acessar o Fields Editor da query (botão direito no componente) e escolher a opção Add All Fields para que todos os campos sejam adicionados, conforme mostra a Figura 8.
Neste exemplo, somente os campos observação e valor_total não estão sendo retornados, porém, em uma aplicação comercial que recebe muitos acessos simultâneos, uma boa prática é adicionar na instrução SQL somente os campos principais e que efetivamente serão úteis para consulta, evitando, assim, possíveis problemas de tráfego na rede. Outra questão é que nesta consulta não estão sendo retornados os registros detalhe que pertencem à tabela m_ordem_servico_servico, pois isso também poderia acarretar em atrasos na rede para a busca dos dados. Em suma, os serviços da ordem de serviço serão carregados somente quando o usuário clicar no botão btnAlterar, sendo aberta uma nova janela para edição dos registros.
Figura 8. Data Module da ordem de serviço.
O próximo passo é criar o formulário para listagem dos registros, o qual será baseado na listagem padrão construída anteriormente. Para isso, deve-se seguir a opção de menu File > New > Other > Inheritable Items e escolher o item frmListagemPadrao, para que o conceito de herança seja utilizado e nosso novo formulário herde todas as características de seu ancestral. A Figura 9 mostra essa janela, na qual todos os formulários e/ou Data Modules já criados no projeto podem ser herdados.
Figura 9. Janela para herdar itens do projeto.
A nova janela deve ser salva com o nome OrdemServicoListagemFrm, e seu nome (propriedade name) deve ser alterado para frmOrdemServicoListagem. Note que a janela possui o mesmo visual da listagem padrão, e para que os registros sejam exibidos não será necessária nenhuma linha de código, basta que esse novo formulário tenha acesso ao dtmOrdemServico (File > Use Unit).
A vinculação do grid com os dados será feita por meio do recurso de LiveBindings (View > LiveBindings Designer). Clicando no botão do wizard dessa janela, deve-se escolher a opção Link a grid with a data source na primeira etapa do assistente, selecionar a grade na etapa 2 (esquerda da Figura 10) e selecionar a query na terceira etapa (direita da Figura 10). Na quarta e última etapa do assistente é possível adicionar um componente para navegação — Add data source navigator —, mas para esta aplicação essa opção não precisa ser adicionada. Após a conclusão do assistente são adicionados dois componentes: BindingsList1 e BindSourceDB1, que são os responsáveis pela operacionalização da vinculação entre os componentes. A partir deste momento, quando a query for ativada (propriedade Active = True) os dados já podem ser visualizados na grade.
Figura 10. Wizard do LiveBindings.
Para que a tela seja exibida em tempo de execução pelo menu principal, é necessário, primeiramente, que a janela principal (PrincipalFrm) tenha acesso à listagem das ordens de serviço, o que pode ser configurado pelo comando File > Use Unit. Depois, basta que o evento OnExecute da ação actOrdensServicos seja codificado de acordo com o que é mostrado na Listagem 6. O único comando é mostrado na linha 03, na qual é invocado o método para abertura das janelas definido anteriormente, passando como parâmetro a janela de listagem das ordens de serviço.
Listagem 6. Abertura da listagem das ordens de serviços.
01 procedure TfrmPrincipal.actOrdensServicosExecute(Sender: TObject);
03   AbrirJanela(TfrmOrdemServicoListagem);
A Figura 11 mostra um exemplo de execução, no qual se pode observar as três ordens de serviço que foram anteriormente adicionadas via SQL.
Figura 11. Execução da listagem de ordens de serviço.
É importante lembrar que o componente qryOrdemServico necessita estar ativo para que os dados sejam exibidos em tela, porém essa não é uma boa prática para ser mantida em sistemas comerciais. O recomendável é preservar as queries e o componente de conexão sempre fechados e abri-los somente quando uma requisição for necessária em tempo de execução. Além disso, não é também uma boa prática carregar todos os registros quando as janelas são abertas, pois em uma tabela de movimentação podem existir milhares de linhas e elas poderão demorar um tempo considerável para serem carregadas, fora o fato de que dificilmente o usuário precisará visualizar todos os registros de uma única vez.
Para resolver esse problema adicionaremos um TEdit (edtNumero) e um TButton (btnPesquisar) no painel localizado abaixo dos botões de inclusão, alteração e exclusão (Figura 12). O objetivo é que o usuário informe o código da ordem de serviço e a consulta SQL seja executada com um filtro where que busque somente o código informado.
A Listagem 7 apresenta a codificação, que deve ser inserida no evento OnClick do novo botão. Na linha 03 é definida uma variável do tipo string (ASql), que receberá, entre as linhas 06 e 10, o mesmo comando SQL que foi definido no componente qryOrdemServico. Na linha 11 é feita uma estrutura condicional que verifica se existe algum valor no edit. Caso afirmativo, na linha 12 é concatenado um comando where para fazer o filtro. Por fim, na linha 13 a query recebe a consulta completa e na linha 14 ela é aberta, momento no qual ela será executada. A Figura 12 apresenta um exemplo de execução com uma consulta sendo feita pelo código "1234".
Listagem 7. Abertura da listagem das ordens de serviços.
01 procedure TfrmOrdemServicoListagem.btnPesquisarClick(Sender: TObject);
03   ASql: String;
05   inherited;
06   ASql := 'select ors.idordem_servico, cln.nome as cliente, vlc.placa as veiculo, fpg.nome as forma_pagamento, ';
07   ASql := ASql + ' ors.numero, ors.data_ordem_servico from m_ordem_servico ors ';
08   ASql := ASql + ' inner join c_cliente cln on ors.idcliente = cln.idcliente ';
09   ASql := ASql + ' inner join c_veiculo vlc on ors.idveiculo = vlc.idveiculo ';
10   ASql := ASql + ' inner join c_forma_pagamento fpg on ors.idforma_pagamento = fpg.idforma_pagamento';
11   if Trim(edtNumero.Text) <> '' then
12     ASql := ASql + ' where ors.numero = ' + QuotedStr(edtNumero.Text);
13   dtmOrdemServico.qryOrdemServico.SQL.Text := ASql;
14   dtmOrdemServico.qryOrdemServico.Open();
O comando Trim na linha 11 da Listagem 7 é utilizado para retirar os espaços em branco do início e do final da string, enquanto que a função QuotedStr da linha 12 adiciona aspas simples no início e no final do texto para não serem gerados erros de SQL.
Figura 12. Execução da consulta com filtros.
Criando as telas de listagem dos cadastros
Os mesmos processos devem ser executados para a criação das outras listagens, ou seja: definição de um Data Module para cada cadastro, criação de janelas utilizando herança, vinculação via LiveBindings e programação das ações na janela principal. Serão mostrados na sequência somente alguns desses passos, visto que os procedimentos são exatamente os mesmos, com a única exceção de que para esses cadastros básicos não utilizaremos o recurso de pesquisa mostrado na Listagem 7. A Figura 13 mostra os quatro Data Modules (um para cada tabela) juntamente com os respectivos Fields Editors.
Figura 13. Data Modules para os cadastros.
A seguir serão mostrados os comandos SQL que devem ser inseridos na propriedade SQL de cada um dos componentes TFDQuery.
qryCliente: select idcliente, nome, data_nascimento, cpf, rg from c_pessoa
qryFormaPagamento: select idforma_pagamento, nome from c_forma_pagamento
qryServico: select idservico, nome from c_servico
qryVeiculo: select idveiculo, placa, modelo from c_veiculo
Para finalizar a criação das janelas, o próximo passo é a criação de quatro formulários (ClienteListagemFrm, FormaPagamentoListagemFrm, ServicoListagemFrm e VeiculoListagemFrm) herdando de ListagemPadraoFrm. Após isso, basta seguir o wizard para configuração dos LiveBindings para que os registros possam ser visualizados no componente TGrid (grdDados). Por fim, a Listagem 8 mostra o código que deve ser implementado no evento OnExecute de cada uma das ações, o qual simplesmente invoca o método AbrirJanela e passa como parâmetro a tela que deve ser aberta.
Listagem 8. Codificação das ações da janela principal.
01 procedure TfrmPrincipal.actClientesExecute(Sender: TObject);
03   AbrirJanela(TfrmClienteListagem);
05 procedure TfrmPrincipal.actFormasPagamentoExecute(Sender: TObject);
07   AbrirJanela(TfrmFormaPagamentoListagem);
09 procedure TfrmPrincipal.actServicosExecute(Sender: TObject);
11   AbrirJanela(TfrmServicoListagem);
13 procedure TfrmPrincipal.actVeiculosExecute(Sender: TObject);
15   AbrirJanela(TfrmVeiculoListagem);
É preciso enfatizar que até este ponto do software, com exceção da janela de ordens de serviço, todas as outras necessitam que a query esteja aberta em design time para que os dados sejam visualizados, visto que nenhum código adicional foi implementado. Para resolver essa questão o seguinte código pode ser adicionado ao evento OnCreate do formulário de clientes, por exemplo: dtmCliente.qryCliente.Open(). O mesmo código pode ser implementado para os outros cadastros, bastando mudar o nome do Data Module e da query.
Com isso, a primeira parte do software já está totalmente funcional no que diz respeito aos recursos implementados. Uma dúvida comum é se realmente são necessários todos esses Data Modules somente para manter as queries com os SQLs de consulta aos dados. Apesar de serem vários arquivos, uma boa prática é que a estrutura do projeto fique consistente e com os arquivos divididos exatamente com suas funções específicas. Por exemplo, caso fosse definido somente um Data Module para centralizar todos os componentes, um futuro crescimento do sistema ficaria mais difícil de ser implementado, visto que os componentes ficariam "acoplados" em um único arquivo. Neste contexto, haveria a necessidade de dividi-los em arquivos separados assim como o feito neste artigo. Outra desvantagem de uni-los em um único local é que a codificação ficaria misturada entre vários contextos diferentes, e dependendo do tamanho do software seria difícil realizar manutenções. Por fim, a divisão do software em módulos distintos que poderiam ser acoplados em outros softwares também ficaria prejudicada, pois, como já mencionado, as funções de vários contextos ficariam misturadas.