Dos documentos abiertos a la vez, el 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 Component lo resuelve mediante un modelo de objetos directo en el que TPdf es el dueño del archivo y TPdfView es el dueño de la presentación. Un documento, un TPdf, un TPdfView. Si quiere tres paneles, tendrá tres parejas. Las partes difíciles no son las llamadas a la API; son la aritmética de la maqueta cuando la ventana cambia de tamaño y la lógica de sincronización de páginas cuando decide qué vista debe seguir a cuál
Maqueta del formulario
El formulario VCL alberga tres contenedores TScrollBox uno al lado del otro, cada uno con un TPdfView dentro alineado a alClient para que llene la caja. Dos componentes TSplitter se sitúan entre las cajas para que el usuario pueda ajustar el ancho de las columnas en tiempo de ejecución. Una barra de herramientas sobre los paneles lleva los botones de apertura, los controles de zoom y el conmutador de dos vistas o tres vistas
El modo de tres vistas es un booleano que el formulario mantiene internamente. Cuando cambia, recalcula los anchos y muestra u oculta la tercera columna. El enfoque más sencillo es limpiar todas las propiedades Align, ocultar los divisores y luego fijar 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;
// Aplicar el mismo (ClientHeight - alto de la barra) a los tres 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;
Poner Align := alNone en las tres cajas antes de la aritmética entera evita que el motor de restricciones de la VCL pelee con sus asignaciones. Restaure la visibilidad de los divisores después de posicionar si quiere redimensionado por arrastre en el modo de dos vistas
El alto de cada caja de desplazamiento es el área de cliente menos el alto del panel de la barra de herramientas. Como la barra está acoplada arriba con alTop, ClientHeight - PanelButtons.Height le da el espacio vertical utilizable. Asigne esto a las tres cajas dentro de la misma llamada a UpdateLayout, de modo que nunca haya un fotograma en el que una caja sea más alta que las otras y provoque un parpadeo de la maqueta
Abrir un documento
Cada pareja de paneles necesita su propio procedimiento de apertura. El patrón es corto: desactivar el componente, fijar el nombre de archivo, activar y luego comprobar Active; si se quedó en False, pedir una contraseña y reintentar. Fíjese en que TPdfView.Active es lo que controla el renderizado, pero TPdf.Active es lo que realmente abre el archivo; son independientes. Poner PdfView.Active := True cuando su TPdf enlazado 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 := '';
PdfComponent.Active := True;
// Los fallos de carga son silenciosos: Active se queda en False, no lanza.
if not PdfComponent.Active then
begin
// Lo más probable es un archivo con contraseña; dé un reintento al usuario.
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;
Compruebe siempre PdfComponent.Active después de la asignación; un archivo dañado o una contraseña equivocada hacen que la carga falle en silencio, sin lanzar una excepción en la ruta por defecto. Fijar PdfViewComponent.PageNumber := 1 de forma explícita tras una apertura correcta evita arrastrar un número de página obsoleto del documento anterior
El diálogo de mensaje del final es intencionado: quiere que los archivos corruptos o no soportados salgan a la luz de inmediato en lugar de quedar tapados como un panel en blanco silencioso. Un usuario que no ve nada no tiene forma de saber si el archivo se cargó y simplemente está vacío, o si el componente lo rechazó. Informar del fallo mantiene el error visible
Seguimiento del panel activo
Cuando el usuario hace clic dentro de un panel, ese panel pasa a ser el activo. El formulario mantiene un campo privado FActivePdfView: TPdfView. La respuesta visual es un cambio de color del borde en el TScrollBox contenedor: póngalo a clHighlight para el activo y a clWindow para los demás. Conecte esto a cada TPdfView.OnClick y al procedimiento de apertura, de forma que el foco siga al documento que acaba de abrir
Algunas operaciones se aplican a todos los paneles visibles y no solo al activo. Un booleano FAllViewsMode en el formulario gobierna esa rama. Cuando está a true, los cambios de zoom y la navegación de páginas se reparten a todos los paneles que tengan 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 en flujos de trabajo de revisión de documentos donde ambos archivos cubren el mismo rango de páginas. La lógica corresponde a un manejador de evento 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 demás vistas, sujeto a una guarda: la vista de destino debe tener al menos esas páginas, y si no, se omite
El PageNumber de TPdfView y el de TPdf son independientes. TPdf.PageNumber registra qué página considera actual el componente de documento; TPdfView.PageNumber registra lo que se muestra en pantalla. Para navegar le interesa la propiedad de la vista, no la del documento
Una casilla de verificación con una etiqueta del estilo "Sincronizar páginas" le da control al usuario. Cuando está desmarcada, cada panel navega de forma independiente y el manejador sale de inmediato. Esa independencia es importante para casos de uso en los que los dos documentos tienen recuentos de páginas distintos, o en los que el usuario quiere encontrar el pasaje equivalente en una traducción que empieza en otra página. Forzar siempre la sincronización haría la herramienta más incómoda que una simple disposición de dos ventanas en el escritorio
Una cosa a vigilar: fijar PdfView.PageNumber por programa dentro del manejador de sincronización disparará a su vez el evento de cambio en esa vista. Protéjase de la recursión infinita con una bandera booleana que active antes de la asignación y limpie justo después. La bandera 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 Zoom := 100 significa tamaño real (100%). Fijarla anula cualquier FitMode activo. Para un botón de ajustar al ancho en el panel activo, lea el zoom de ajuste desde PdfView.PageWidthZoom[PdfView.PageNumber] y asígnelo. Para ajustar a la página, use PageZoom[PageNumber]. Ambas son propiedades de array indexadas por número de página en base 1, así que protéjase de un número de página cero antes de acceder a ellas
Cuando exporte la página actual a una imagen, lea la rotación de la vista pero llame a RenderPage sobre el componente TPdf, no sobre la vista. La forma de mapa de bits de TPdf.RenderPage toma dimensiones explícitas en píxeles más un valor TRotation y un conjunto TRenderOptions. La variante de función devuelve un TBitmap cuya propiedad pasa a quien llama y que usted mismo 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 2x sobre el ancho y el alto da una salida más nítida en documentos con texto fino. El try/finally alrededor de la liberación del mapa de bits no es opcional; una cancelación de TSaveDialog sigue pasando por el bloque finally, y usted quiere el mapa de bits liberado hiciera lo que hiciera el usuario
Requisitos de la DLL
PDFium Component 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 compilaciones estándar. Para un visor de comparación que desactiva el relleno de formularios (Pdf.FormFill := False), la compilación estándar sin V8 basta y mantiene la distribución más ligera
Coloque 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, de modo que una DLL ausente se manifiesta en ese momento y no al arrancar la aplicación. Si distribuye un instalador, lo 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 puede limpiar más adelante
Las compilaciones con V8 sirven sobre todo cuando necesita interactuar con acciones JavaScript de un PDF, por ejemplo para disparar campos de cálculo o manejadores de envío. Un visor de comparación pasivo no tiene motivo para ejecutar JavaScript; poner Pdf.FormFill := False antes de Active := True se salta por completo el entorno de relleno de formularios, lo que además significa que no se inicializa ningún motor de JS ni siquiera con la compilación estándar. Ese es el valor por defecto correcto para un visor de solo lectura, sea cual sea la variante de DLL que distribuya
Para más detalles sobre PDFium Component y su API completa, visite la página de producto de Delphi PDFium Component