Dois documentos abertos ao mesmo tempo, no mesmo número de página, cada um no seu painel com deslocamento: é esse o cerne de um visualizador de comparação. O PDFium Component entrega isto através de um modelo de objetos direto em que TPdf é dono do ficheiro e TPdfView é dono da apresentação. Um documento, um TPdf, um TPdfView. Se quer três painéis, tem três pares. As partes difíceis não são as chamadas à API; são a aritmética da disposição quando a janela é redimensionada e a lógica de sincronização de páginas quando decide que vista deve seguir qual
Disposição do formulário
O formulário VCL contém três contentores TScrollBox lado a lado, cada um com um TPdfView lá dentro alinhado a alClient para que preencha a caixa. Dois componentes TSplitter ficam entre as caixas para que o utilizador possa ajustar as larguras das colunas em tempo de execução. Uma barra de ferramentas por cima dos painéis leva os botões de abertura, os controlos de ampliação e o alternador entre duas e três vistas
O modo de três vistas é um booleano que o formulário guarda internamente. Quando muda, recalcula as larguras e mostra ou esconde a terceira coluna. A abordagem mais simples é limpar todas as propriedades Align, esconder os separadores, e depois definir posições absolutas:
procedure TFormMain.UpdateLayout;
var
TotalWidth: Integer;
begin
TotalWidth := ClientWidth;
if ThreeViewMode then
begin
ScrollBox3.Visible := True;
ScrollBox1.Left := 0;
ScrollBox1.Width := TotalWidth div 3;
ScrollBox2.Left := ScrollBox1.Width;
ScrollBox2.Width := TotalWidth div 3;
ScrollBox3.Left := ScrollBox2.Left + ScrollBox2.Width;
ScrollBox3.Width := TotalWidth - ScrollBox3.Left;
// Aplicar o mesmo (ClientHeight - altura da barra) aos três valores de Height
end
else
begin
ScrollBox3.Visible := False;
ScrollBox1.Left := 0;
ScrollBox1.Width := TotalWidth div 2;
ScrollBox2.Left := ScrollBox1.Width;
ScrollBox2.Width := TotalWidth - ScrollBox2.Left;
end;
end;
Definir Align := alNone nas três caixas antes da aritmética inteira evita que o motor de restrições da VCL lute contra as suas atribuições. Reponha a visibilidade dos separadores depois de posicionar, se quiser redimensionamento por arrasto no modo de duas vistas
A altura de cada caixa de deslocamento é a área de cliente menos a altura do painel da barra de ferramentas. Como a barra está ancorada ao topo com alTop, ClientHeight - PanelButtons.Height dá-lhe o espaço vertical utilizável. Atribua isto às três caixas dentro da mesma chamada a UpdateLayout, para que nunca haja um fotograma em que uma caixa fique mais alta do que as outras e provoque uma tremura na disposição
Abrir um documento
Cada par de painéis precisa do seu próprio procedimento de abertura. O padrão é curto: desativar o componente, definir o nome do ficheiro, ativar, e depois verificar Active; se tiver ficado a False, pedir uma palavra-passe e tentar de novo. Repare que TPdfView.Active é o que controla a renderização, mas TPdf.Active é o que abre efetivamente o ficheiro; são independentes. Pôr PdfView.Active := True quando o TPdf ligado ainda não está ativo é inócuo mas não mostra nada
procedure TFormMain.OpenPdfFile(PdfComponent: TPdf;
PdfViewComponent: TPdfView);
var
Password: string;
begin
if not OpenDialog.Execute then
Exit;
PdfComponent.Active := False;
PdfComponent.FileName := OpenDialog.FileName;
PdfComponent.Password := '';
PdfComponent.Active := True;
// As falhas de carregamento são silenciosas: Active fica False sem exceção.
if not PdfComponent.Active then
begin
// Provavelmente um ficheiro protegido; dê ao utilizador uma tentativa.
if InputQuery('Password', 'Enter document password:', Password) then
begin
PdfComponent.Password := Password;
PdfComponent.Active := True;
end;
end;
if not PdfComponent.Active then
begin
ShowMessage('Could not open ' + OpenDialog.FileName +
' (damaged file or wrong password)');
Exit;
end;
PdfViewComponent.PageNumber := 1;
SetActivePdfView(PdfViewComponent);
end;
Verifique sempre PdfComponent.Active depois da atribuição; um ficheiro danificado ou uma palavra-passe errada fazem o carregamento falhar em silêncio, sem levantar exceção no caminho por omissão. Definir explicitamente PdfViewComponent.PageNumber := 1 depois de uma abertura bem-sucedida evita arrastar um número de página desatualizado do documento anterior
A caixa de mensagem no fim é intencional: quer que os ficheiros corrompidos ou não suportados apareçam de imediato em vez de serem engolidos como um painel branco e silencioso. Um utilizador que não vê nada não faz ideia se o ficheiro carregou e está simplesmente vazio, ou se o componente o rejeitou. Reportar a falha mantém o erro visível
Seguir o painel ativo
Quando o utilizador clica dentro de um painel, esse painel passa a ativo. O formulário guarda um campo privado FActivePdfView: TPdfView. O retorno visual é uma mudança de cor de contorno na TScrollBox que o contém: ponha-a a clHighlight na ativa e a clWindow nas outras. Ligue isto ao TPdfView.OnClick de cada painel e também ao procedimento de abertura, para que o foco acompanhe o documento que acabou de abrir
Algumas operações aplicam-se a todos os painéis visíveis e não apenas ao ativo. Um booleano FAllViewsMode no formulário comanda esse ramo. Quando está a verdadeiro, as mudanças de ampliação e a navegação de páginas abrem-se em leque para todos os painéis que tenham um documento ativo:
procedure TFormMain.ApplyZoomToAll(NewZoom: Double);
begin
if PdfView1.Active then PdfView1.Zoom := NewZoom;
if PdfView2.Active then PdfView2.Zoom := NewZoom;
if ThreeViewMode and PdfView3.Active then PdfView3.Zoom := NewZoom;
end;
Navegação de páginas sincronizada
A navegação sincronizada é opcional mas útil em fluxos de revisão de documentos em que ambos os ficheiros cobrem o mesmo intervalo de páginas. A lógica pertence a um tratador de evento que dispara depois de o utilizador navegar numa das vistas. Quando uma vista de origem muda o seu PageNumber, o tratador propaga esse número às outras vistas, sujeito a uma guarda: a vista de destino tem de ter pelo menos esse número de páginas, caso contrário salta
O PageNumber de TPdfView e o de TPdf são independentes. TPdf.PageNumber acompanha a página que o componente de documento considera atual; TPdfView.PageNumber acompanha o que está apresentado no ecrã. Para efeitos de navegação quer a propriedade da vista, não a do documento
Uma caixa de verificação com um rótulo do género "Sincronizar páginas" dá o controlo ao utilizador. Quando está desmarcada, cada painel navega de forma independente e o tratador sai de imediato. Essa independência é importante para casos em que os dois documentos têm contagens de páginas diferentes, ou em que o utilizador quer encontrar a passagem equivalente numa tradução que começa noutra página. Forçar a sincronização sempre tornaria a ferramenta mais difícil de usar do que uma simples disposição de duas janelas no ambiente de trabalho
Uma coisa a vigiar: definir PdfView.PageNumber por código dentro do tratador de sincronização vai, por si só, disparar o evento de mudança nessa vista. Proteja-se contra a recursão infinita com uma flag booleana que ativa antes da atribuição e limpa logo a seguir. A flag é por formulário, não por vista, porque as três vistas partilham o mesmo tratador
Ampliação por painel
Cada TPdfView transporta a sua própria propriedade Zoom, um Double em percentagem em que Zoom := 100 significa tamanho real (100%). Defini-la sobrepõe-se a qualquer FitMode ativo. Para um botão de ajustar à largura no painel ativo, leia a ampliação de ajuste em PdfView.PageWidthZoom[PdfView.PageNumber] e atribua-a. Para ajustar à página, use PageZoom[PageNumber]. Ambas são propriedades de vetor indexadas por número de página a começar em 1, pelo que deve proteger-se contra um número de página igual a zero antes de lhes aceder
Quando exporta a página atual para uma imagem, leia a rotação a partir da vista mas chame RenderPage no componente TPdf, não na vista. A forma de bitmap de TPdf.RenderPage recebe dimensões explícitas em pixels mais um valor TRotation e um conjunto TRenderOptions. A variante de função devolve um TBitmap cuja propriedade é de quem chama e que liberta por si depois de gravar:
procedure TFormMain.SaveActiveViewAsImage;
var
Pdf: TPdf;
Bmp: TBitmap;
Jpeg: TJpegImage;
begin
if not Assigned(FActivePdfView) or not FActivePdfView.Active then
Exit;
Pdf := FActivePdfView.Pdf;
Pdf.PageNumber := FActivePdfView.PageNumber;
Bmp := Pdf.RenderPage(
0, 0,
Round(Pdf.PageWidth * 2),
Round(Pdf.PageHeight * 2),
FActivePdfView.Rotation, [], clWhite);
try
if SavePictureDialog.Execute then
begin
Jpeg := TJpegImage.Create;
try
Jpeg.Assign(Bmp);
Jpeg.CompressionQuality := 90;
Jpeg.SaveToFile(SavePictureDialog.FileName);
finally
Jpeg.Free;
end;
end;
finally
Bmp.Free;
end;
end;
O multiplicador de 2x na largura e na altura dá saída mais nítida em documentos com texto miúdo. O try/finally à volta da libertação do bitmap não é opcional; um cancelamento em TSaveDialog continua a passar pelo bloco finally, e quer o bitmap libertado independentemente do que o utilizador tenha feito
Requisitos de DLL
O PDFium Component envolve a biblioteca nativa pdfium. Um processo anfitrião de 32 bits precisa de pdfium32.dll; um de 64 bits precisa de pdfium64.dll. As variantes com o motor de JavaScript V8 acrescentam o sufixo v8 e pesam aproximadamente 23-27 MB, contra os 5-6 MB das compilações normais. Para um visualizador de comparação que desativa o preenchimento de formulários (Pdf.FormFill := False), a compilação normal sem V8 chega e mantém a distribuição mais pequena
Coloque a DLL no mesmo diretório do executável, ou em qualquer diretório do PATH do sistema. O componente carrega-a a pedido quando o primeiro TPdf é ativado, pelo que uma DLL em falta só se manifesta nesse ponto e não no arranque da aplicação. Se distribuir um instalador, a abordagem mais fiável é copiar a DLL para a pasta da aplicação durante a instalação em vez de contar com um diretório de sistema que um administrador possa mais tarde limpar
As compilações com V8 são úteis sobretudo quando precisa de interagir com ações de JavaScript do PDF, por exemplo para acionar campos de cálculo ou tratadores de submissão. Um visualizador de comparação passivo não tem razão nenhuma para correr JavaScript; pôr Pdf.FormFill := False antes de Active := True salta por completo o ambiente de preenchimento de formulários, o que também significa que nenhum motor de JS é inicializado mesmo que use a compilação normal. Esse é o comportamento por omissão correto para um visualizador só de leitura, seja qual for a variante de DLL que distribua
Para mais detalhes sobre o PDFium Component e a sua API completa, visite a página de produto do Delphi PDFium Component