Dos documentos abiertos a la vez, mismo número de página, cada uno en su propio panel desplazable: ese es el núcleo de un visor de comparación. PDFium VCL lo proporciona a través de un modelo de objetos sencillo donde TPdf posee el fichero y TPdfView posee la visualización. Un documento, un TPdf, un TPdfView. Si se quieren tres paneles, se tienen tres pares. Las partes difíciles no son las llamadas al API; son la aritmética de maquetación cuando la ventana cambia de tamaño y la lógica de sincronización de páginas cuando se decide qué vista debe seguir a cuál.
Maquetación del formulario
El formulario VCL contiene tres contenedores TScrollBox en paralelo, cada uno con un TPdfView dentro alineado a alClient para que rellene el cuadro. Dos componentes TSplitter se sitúan entre los cuadros para que el usuario pueda ajustar el ancho de las columnas en tiempo de ejecución. Una barra de herramientas encima de los paneles lleva los botones de apertura, los controles de zoom y el conmutador de dos/tres vistas.
El modo de tres vistas es un valor booleano que el formulario lleva internamente. Cuando cambia, se recalculan los anchos y se muestra u oculta la tercera columna. El enfoque más sencillo es borrar todas las propiedades Align, ocultar los separadores y luego establecer posiciones 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;
// Apply the same (ClientHeight - toolbar height) to all three Height values
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;
Establecer Align := alNone en los tres cuadros antes de la aritmética de enteros evita que el motor de restricciones de VCL interfiera con las asignaciones. Hay que restaurar la visibilidad de los separadores tras el posicionamiento si se quiere el redimensionado por arrastre en modo de dos vistas.
La altura de cada cuadro de desplazamiento es el área de cliente menos la altura del panel de la barra de herramientas. Como la barra de herramientas está acoplada arriba con alTop, ClientHeight - PanelButtons.Height da el espacio vertical utilizable. Hay que asignarlo a los tres cuadros dentro de la misma llamada a UpdateLayout para que nunca haya un fotograma donde un cuadro sea más alto que los demás y cause un parpadeo de maquetación.
Apertura de un documento
Cada par de paneles necesita su propio procedimiento de apertura. El patrón es corto: desactivar el componente, establecer el nombre de fichero, intentar activar y capturar EPdfError si el fichero requiere contraseña. Hay que tener en cuenta que TPdfView.Active es lo que controla el renderizado, pero TPdf.Active es lo que realmente abre el fichero; son independientes. Establecer PdfView.Active := True cuando su TPdf vinculado aún no está activo es inofensivo pero no muestra 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 := '';
try
PdfComponent.Active := True;
except
on E: EPdfError do
begin
if InputQuery('Password', 'Enter document password:', Password) then
begin
PdfComponent.Password := Password;
PdfComponent.Active := True;
end
else
raise;
end;
end;
if PdfComponent.Active then
begin
PdfViewComponent.PageNumber := 1;
SetActivePdfView(PdfViewComponent);
end;
end;
Siempre hay que comprobar PdfComponent.Active tras la asignación; un fichero dañado o una contraseña incorrecta hace que la carga falle silenciosamente sin lanzar una excepción en la ruta por defecto. Establecer PdfViewComponent.PageNumber := 1 explícitamente tras una apertura exitosa evita un número de página obsoleto del documento anterior.
El código de gestión de contraseñas anterior lanza en cualquier error que no sea el mensaje de contraseña conocido. Eso es intencionado: se quiere que los ficheros corruptos o no soportados aparezcan inmediatamente en lugar de absorberse como un panel en blanco silencioso. Un usuario que no ve nada no tiene forma de saber si el fichero se cargó y está simplemente vacío, o si el componente lo rechazó. Lanzar la excepción mantiene el error visible.
Seguimiento del panel activo
Cuando el usuario hace clic dentro de un panel, ese panel se activa. El formulario lleva un campo privado FActivePdfView: TPdfView. La retroalimentación visual es un cambio de color del borde del TScrollBox contenedor: se establece en clHighlight para el activo y en clWindow para los demás. Hay que conectar esto a cada TPdfView.OnClick y al procedimiento de apertura para que el foco siga al documento que se acaba de abrir.
Algunas operaciones se aplican a todos los paneles visibles en lugar de solo al activo. Un booleano FAllViewsMode en el formulario controla esa ramificación. Cuando es verdadero, los cambios de zoom y la navegación de páginas se propagan a todos los paneles que tienen un documento activo:
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;
Navegación de páginas sincronizada
La navegación sincronizada es opcional pero útil para flujos de trabajo de revisión de documentos donde ambos ficheros cubren el mismo rango de páginas. La lógica pertenece a un manejador de eventos que se dispara después de que el usuario navegue en una vista. Cuando una vista de origen cambia su PageNumber, el manejador propaga ese número a las otras vistas, con una comprobación: la vista de destino debe tener al menos ese número de páginas, de lo contrario se omite.
El PageNumber en TPdfView y en TPdf son independientes. TPdf.PageNumber lleva la cuenta de qué página considera actual el componente de documento; TPdfView.PageNumber lleva la cuenta de lo que se muestra en pantalla. Para la navegación conviene usar la propiedad de la vista, no la del documento.
Una casilla de verificación etiquetada algo como "Sincronizar páginas" da control al usuario. Cuando no está marcada, cada panel navega de forma independiente y el manejador sale inmediatamente. Esa independencia es importante para casos de uso donde los dos documentos tienen recuentos de páginas diferentes, o donde el usuario quiere encontrar el pasaje equivalente en una traducción que empieza en una página distinta. Forzar siempre la sincronización haría la herramienta más difícil de usar que una simple disposición de dos ventanas en el escritorio.
Hay que vigilar un aspecto: establecer PdfView.PageNumber mediante programación dentro del manejador de sincronización disparará a su vez el evento de cambio en esa vista. Hay que protegerse contra la recursión infinita con un indicador booleano que se establece antes de la asignación y se borra inmediatamente después. El indicador es por formulario, no por vista, porque las tres vistas comparten el mismo manejador.
Zoom por panel
Cada TPdfView lleva su propia propiedad Zoom, un Double en porcentaje donde 1,0 es el 100%. Establecerla anula cualquier FitMode activo. Para un botón de ajuste al ancho en el panel activo, hay que leer el zoom de ajuste desde PdfView.PageWidthZoom[PdfView.PageNumber] y asignarlo. Para ajuste a la página, se usa PageZoom[PageNumber]. Ambas son propiedades de array indexadas por número de página base 1, por lo que hay que protegerse contra un número de página cero antes de acceder a ellas.
Al exportar la página actual a una imagen, hay que leer la rotación desde la vista pero llamar a RenderPage en el componente TPdf, no en la vista. La forma de mapa de bits de TPdf.RenderPage toma dimensiones de píxel explícitas más un valor TRotation y un conjunto TRenderOptions. La variante de función devuelve un TBitmap cuya gestión corresponde al llamante y que se libera después de guardar:
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;
El multiplicador de 2x en el ancho y el alto da una salida más nítida para documentos con texto fino. El bloque try/finally alrededor de la liberación del mapa de bits no es opcional; una cancelación de TSaveDialog sigue llegando al bloque finally, y se quiere que el mapa de bits se libere independientemente de lo que haya hecho el usuario.
Requisitos de la DLL
PDFium VCL envuelve la biblioteca nativa pdfium. Un proceso anfitrión de 32 bits necesita pdfium32.dll; uno de 64 bits necesita pdfium64.dll. Las variantes con el motor JavaScript V8 añaden el sufijo v8 y pesan aproximadamente 23-27 MB frente a los 5-6 MB de las versiones estándar. Para un visor de comparación que deshabilita el relleno de formularios (Pdf.FormFill := False), la versión estándar sin V8 es suficiente y mantiene la distribución más pequeña.
Hay que colocar la DLL en el mismo directorio que el ejecutable, o en cualquier directorio del PATH del sistema. El componente la carga bajo demanda cuando se activa el primer TPdf, por lo que una DLL ausente se detecta en ese momento y no al iniciar la aplicación. Si se distribuye con un instalador, el enfoque más fiable es copiar la DLL en la carpeta de la aplicación durante la instalación en lugar de depender de un directorio del sistema que un administrador podría limpiar después.
Las versiones V8 son principalmente útiles cuando se necesita interactuar con acciones JavaScript de PDF, por ejemplo para activar campos de cálculo o manejadores de envío. Un visor de comparación pasivo no tiene motivo para ejecutar JavaScript; establecer Pdf.FormFill := False antes de Active := True omite completamente el entorno de relleno de formularios, lo que también significa que no se inicializa ningún motor JS aunque se use la versión estándar. Ese es el valor por defecto correcto para un visor de solo lectura independientemente de qué variante de DLL se distribuya.
Para más detalles sobre el componente PDFium VCL y su API completa, visita la página de producto del componente PDFium VCL para Delphi.