Artículo técnico

Una cuadrícula de hoja de cálculo personalizada en Delphi con HotXLS

HotXLS distribuye TXLSWorkbookViewer, un control VCL nativo que renderiza libros XLS, XLSX, XLSM y ODS como una cuadrícula de hoja de cálculo interactiva dentro de un formulario Delphi o C++Builder, sin instalar Excel ni gobernarlo mediante automatización OLE. Construir bien ese tipo de control significa resolver tres problemas concretos: asociar un clic de ratón que cae dentro de una celda combinada con la celda lógica correcta, mantener coherentes la posición de desplazamiento, las bandas de encabezado y la selección de celda mientras un usuario recorre una hoja mucho más grande que la ventana visible, y decidir qué debería hacer realmente un clic sobre un marcador de comentario o sobre una celda de hipervínculo

La mayoría de las empresas Delphi recurren a un visor de hojas de cálculo por razones que no tienen nada que ver con la edición: una estación de auditoría que previsualiza libros subidos antes de que entren en un pipeline, un quiosco o visor de informes donde Microsoft Office no forma parte de la imagen de despliegue, o una herramienta de control de calidad que necesita mostrar el contenido de un libro sin la imprevisibilidad de automatizar un proceso Excel real mediante COM. Una simple cuadrícula de cadenas os da texto en celdas rápidamente, pero un archivo de hoja de cálculo no es una simple cuadrícula: las celdas se combinan en bloques que solo existen una vez en el modelo subyacente, las hojas llevan bandas de encabezado fijas y posiciones de desplazamiento horizontal y vertical independientes, y las celdas individuales llevan comentarios e hipervínculos que necesitan su propio modelo de interacción. TXLSWorkbookViewer es la respuesta de HotXLS a esa brecha, y su diseño interno es un plano razonable para cualquiera que construya un control similar desde cero

¿Cómo evita un visor de libros depender de Excel?

TXLSWorkbookViewer evita Excel por completo leyendo a través del propio modelo de objetos analizado de HotXLS en lugar de abrir un documento a través de Excel y manejarlo como una marioneta. La propiedad Workbook enlaza un TXLSWorkbook existente para archivos XLS clásicos, y XlsxWorkbook enlaza un TXLSXWorkbook para variantes XLSX, XLSM y de plantilla; cualquiera de los dos puede estar ya abierto en otro lugar de la aplicación, y el visor solo lee de él. Cuando el control debe ser propietario del propio archivo, LoadFromFile inspecciona la extensión, encamina XLSX, XLSM, XLTX, XLTM y ODS a través del motor moderno y todo lo demás a través del clásico, y libera cualquiera que sea el libro que creó en cuanto el control se limpia o se destruye

var
  Viewer: TXLSWorkbookViewer;
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  if Book.Open('quarterly-report.xlsx') <> 1 then
    raise Exception.Create('Could not open workbook');

  Viewer := TXLSWorkbookViewer.Create(Self);
  Viewer.Parent := Self;
  Viewer.Align := alClient;
  Viewer.XlsxWorkbook := Book;        // the viewer does not take ownership
  Viewer.GoToCell(1, 1);

  Caption := Viewer.WorksheetName + ': ' + Viewer.SelectedCellText;
end;

Localizar la celda correcta dentro de un rango combinado

Resolver un clic hasta la celda correcta en TXLSWorkbookViewer es una búsqueda en dos etapas, y la división importa porque la geometría de píxeles y la semántica de hoja de cálculo son problemas genuinamente distintos. La primera etapa es geometría pura: un método privado CellAtPoint recorre anchos de columna y altos de fila desde la posición de desplazamiento actual hasta encontrar la banda que contiene la coordenada X e Y donde se hizo clic, sin ninguna conciencia de las celdas combinadas. La segunda etapa es semántica: cada vía que cambia la selección, un clic de ratón, una tecla de flecha, Tab, o una llamada directa a GoToCell, se canaliza a través de una única rutina interna ChangeSelection, que normaliza la fila y columna en bruto contra cualquier combinación y las ajusta a la celda ancla de la combinación antes de que la selección realmente cambie

El ancla es la celda superior izquierda del rango combinado, y es la única celda de ese bloque que realmente contiene un valor, un formato, un comentario o un hipervínculo en el modelo de libro subyacente; cualquier otra celda que la combinación cubra visualmente está vacía en los propios datos. Para libros XLS clásicos, el ancla procede de Cell.MergeArea, un IXLSRange cuyos Row y Column apuntan a la celda propietaria; para libros XLSX y ODS, MergedCells.FindAt devuelve un TXLSXMergedRange que expone la misma ancla como Row1 y Col1. El pintado resuelve un problema equivalente de forma independiente, expandiendo el rectángulo de una celda combinada a todo su alcance de fila y columna y saltándose las celdas dentro de ese alcance, así que el contorno de selección envuelve todo el bloque combinado y no solo su esquina ancla, y escribir disposiciones combinadas en lugar de solo volver a leerlas es un problema relacionado pero distinto cubierto en el artículo complementario sobre la disposición de celdas combinadas para plantillas de informe

var
  Sheet: TXLSXWorksheet;
begin
  Sheet := Book.Sheets.Add('Summary');
  Sheet.MergeCells(2, 2, 3, 4);       // B2:D3
  Sheet.Cells[2, 2].Value := 'Region totals';

  Viewer.XlsxWorkbook := Book;
  Viewer.GoToCell(3, 4);              // targets the bottom-right corner of the merge
  // SelectedRow is now 2 and SelectedCol is now 2: normalized to the anchor cell
end;

¿Qué mantiene sincronizados el desplazamiento, los encabezados y la selección?

TXLSWorkbookViewer mantiene coherentes tres piezas de estado independientes: la posición de desplazamiento lógica alojada en TopRow y LeftCol, las barras de desplazamiento nativas de Windows que el control solicita mediante WS_HSCROLL y WS_VSCROLL en CreateParams, y la selección actual en SelectedRow y SelectedCol. Arrastrar una barra de desplazamiento o girar la rueda del ratón dispara WM_HSCROLL, WM_VSCROLL o WM_MOUSEWHEEL, que actualizan TopRow o LeftCol y repintan; la selección no se mueve, lo que coincide con cómo el propio Excel separa el desplazamiento de la selección. Después de cualquiera de esas actualizaciones, UpdateScrollBars devuelve la nueva posición a la barra de desplazamiento nativa mediante SetScrollInfo, así que el pulgar de la barra nunca se desvía de lo que la cuadrícula realmente muestra

La navegación con teclado ejecuta la misma sincronización en la dirección opuesta: mover la selección más allá del borde de la cuadrícula visible llama a EnsureSelectionVisible, que ajusta TopRow o LeftCol acumulando anchos de columna y altos de fila reales en lugar de simplemente incrementar en uno, ya que las filas y columnas pueden llevar tamaños personalizados, y a continuación llama a UpdateScrollBars para que el pulgar refleje adondequiera que el teclado acabe de llevar la vista. Las bandas de encabezado de número de fila y letra de columna, dimensionadas mediante RowHeaderWidth y ColumnHeaderHeight, son la parte de este control que permanece fija en pantalla mientras TopRow y LeftCol desplazan los datos por debajo, y ese es todo el alcance de la inmovilización que este control hace por sí mismo: no es la función Inmovilizar paneles de Excel, y no hay ninguna forma integrada de fijar una fila o columna de datos arbitraria mientras el resto de la hoja se desplaza más allá de ella. Un límite que merece la pena probar antes de publicar un visor sobre archivos que no controláis del todo es que TopRow y LeftCol no están acotados contra el rango realmente usado de la hoja de cálculo, así que un pulgar arrastrado hasta su límite estructural puede aterrizar en la fila 1.048.576 o en la columna 16.384 y mostrar una cuadrícula en blanco en lugar de la última fila o columna que realmente contiene datos; los libros lo bastante grandes como para que esto sea perceptible suelen ser también lo bastante grandes como para necesitar la atención del lado de carga cubierta en el artículo sobre rendimiento de libros grandes

Conectar comentarios e hipervínculos a eventos de ratón y selección

TXLSWorkbookViewer trata los comentarios e hipervínculos como atributos de la celda que esté seleccionada en cada momento en lugar de como objetivos al pasar el ratón por encima, así que SelectedCellCommentText, SelectedCellCommentAuthor y SelectedCellHyperlink se actualizan cada vez que se dispara OnSelectionChange, ya sea que la selección se moviera por clic de ratón, tecla de flecha o una llamada a GoToCell. A una celda comentada se le pinta un pequeño triángulo rojo en su esquina superior derecha como pista visual, similar al propio indicador de comentario de Excel, pero ese marcador es puramente visual; no hay ningún tooltip integrado en el control que se dispare al pasar el ratón por encima, así que una aplicación que quiera un popup al pasar el ratón en lugar de al seleccionar tiene que construir esa capa por sí misma. La activación de hipervínculos funciona con el mismo enfoque de selección primero: hacer doble clic en una celda llama a ActivateSelectedCell, que lee SelectedCellHyperlink y, si no está vacía, dispara OnHyperlinkClick con la dirección de destino y un parámetro var Handled: Boolean para que el gestor lo establezca

Lo que OnHyperlinkClick no hace es igual de importante: TXLSWorkbookViewer nunca llama a ShellExecute ni abre un navegador por sí mismo, independientemente de si el gestor pone Handled a true o lo deja en false. La navegación, y cualquier decisión sobre qué cuenta como un destino seguro, es responsabilidad íntegra de la aplicación anfitriona, que es el comportamiento por defecto correcto para un componente que no tiene forma de saber si está incrustado en una herramienta interna de confianza o en un visor de archivos que un cliente acaba de subir

procedure TMainForm.ViewerSelectionChange(Sender: TObject; Row, Col: Integer);
begin
  if Viewer.SelectedCellCommentText <> '' then
    StatusBar.SimpleText := Viewer.SelectedCellCommentAuthor + ': ' +
      Viewer.SelectedCellCommentText
  else
    StatusBar.SimpleText := Viewer.SelectedCellHyperlink;
end;

procedure TMainForm.ViewerHyperlinkClick(Sender: TObject;
  const Target: WideString; var Handled: Boolean);
begin
  ShellExecute(0, 'open', PWideChar(Target), nil, nil, SW_SHOWNORMAL);
  Handled := True;
end;

Alcance de la selección y límites de la navegación con teclado

La selección en TXLSWorkbookViewer siempre es una única celda lógica, rastreada como SelectedRow y SelectedCol; no hay selección de rango rectangular multicelda en el control base, así que cualquier funcionalidad que necesite actuar sobre un bloque de celdas tiene que construirse por encima en lugar de leerse de un objeto de selección. La cobertura de teclado es deliberadamente básica: las teclas de flecha mueven una celda a la vez, Inicio vuelve al principio de la fila o, con Ctrl, a la celda A1, Re Pág y Av Pág saltan diez filas, y Tab y Mayús+Tab avanzan entre columnas; no hay ningún salto Ctrl+Flecha hasta el borde de una región de datos ni selección de rango extendida con Mayús, así que los usuarios que vengan directamente de Excel notarán la carencia en una hoja densa

Los límites de columna se aplican en el mismo punto de estrangulamiento ChangeSelection que gestiona la normalización de combinaciones, y difieren por motor a propósito: un visor enlazado a un TXLSWorkbook clásico se acota en la columna 256, el techo estructural del formato BIFF8, mientras que uno enlazado a TXLSXWorkbook respeta el límite moderno de 16.384 columnas que XLSX heredó de Excel 2007 en adelante. Las filas están limitadas a 1.048.576 en ambos casos, así que la diferencia práctica entre abrir un archivo XLS heredado y uno XLSX en el mismo visor tiene que ver enteramente con hasta dónde hacia la derecha está dispuesta a dejaros llegar la cuadrícula

Nada de esto es exótico una vez descompuesto en búsqueda de píxel, normalización de ancla y un puñado de gestores de mensajes, pero conseguir que los tres coincidan con archivos reales, con combinaciones, comentarios e hipervínculos reales, es la mayor parte del trabajo en un componente como este. TXLSWorkbookViewer se distribuye como parte del componente Excel HotXLS estándar para Delphi y C++Builder, junto con los modelos de objetos clásico y XLSX a partir de los cuales renderiza