Artículo técnico

Protección de hojas XLSX en Delphi: 15 opciones permitidas

Le entregas a un compañero un libro ya terminado y le pides que lo filtre, no que lo reescriba. Así que proteges la hoja. En compilaciones antiguas de HotXLS ese gesto escribía una sola cosa en el archivo: <sheetProtection sheet="1" objects="1" scenarios="1"/>, con valores fijos, siempre. La hoja quedaba bloqueada, el hash de la contraseña se adjuntaba y el usuario no podía hacer nada, ni siquiera ordenar y filtrar, justo lo que querías dejar abierto. El propio cuadro de diálogo "Proteger hoja" de Excel tiene quince casillas por este motivo, y el motor no podía expresar ninguna de ellas. Esa laguna es la que cierra el modelo de protección de v2.91.0

HotXLS es un componente VCL nativo para Delphi y C++Builder que lee y escribe XLS y XLSX sin tener Excel instalado. Este artículo trata la parte XLSX de la protección de hojas de cálculo: la nueva TXLSXSheetProtectionOption, la propiedad AllowOption que activa cada permiso y la regla de codificación OOXML que hace tropezar a cualquiera que escriba a mano un elemento <sheetProtection>

Qué protege realmente la protección de hojas de cálculo

Primero, el límite, porque de él depende cuánto debes confiar en todo esto. La protección de hojas de cálculo en el formato de hojas de cálculo OOXML (ECMA-376) es una política de interacción, no cifrado. Indica a una aplicación compatible qué ediciones debe rechazar mientras la hoja está protegida. Los valores de las celdas siguen dentro de xl/worksheets/sheetN.xml en texto plano; descomprime el .xlsx y ahí están. La contraseña opcional se almacena como un hash heredado corto, no como una clave que altere nada. Quien renombre el archivo, abra la parte y elimine la línea <sheetProtection> lee y edita todo

Así que la protección responde a "impedir que un compañero pise por accidente una fórmula", no a "mantener estos datos secretos frente a alguien con intención". Son problemas distintos y requieren herramientas distintas. Si necesitas confidencialidad, quieres el cifrado a nivel de libro que cubre salida XLSX protegida con AES, que sí cifra el paquete. La protección de hoja y el cifrado del libro se combinan bien, pero solo el segundo es un candado. Ten clara esa línea y el resto de esta página es pura fontanería

Las quince opciones y la propiedad AllowOption

Cada hoja de cálculo lleva ahora un conjunto de valores TXLSXSheetProtectionOption que describen qué puede seguir haciendo el usuario mientras la hoja está protegida. Los miembros se corresponden uno a uno con los atributos OOXML y con las casillas del cuadro de diálogo de Excel:

  • xlsxSpoEditObjects, xlsxSpoEditScenarios - editar objetos de dibujo y escenarios de hipótesis
  • xlsxSpoFormatCells, xlsxSpoFormatColumns, xlsxSpoFormatRows - volver a dar formato a celdas, columnas y filas
  • xlsxSpoInsertColumns, xlsxSpoInsertRows, xlsxSpoInsertHyperlinks - insertar columnas, filas y vínculos
  • xlsxSpoDeleteColumns, xlsxSpoDeleteRows - eliminar columnas y filas
  • xlsxSpoSelectLockedCells, xlsxSpoSelectUnlockedCells - mover la selección a celdas bloqueadas o desbloqueadas
  • xlsxSpoSort, xlsxSpoAutoFilter, xlsxSpoPivotTables - ordenar rangos, usar los desplegables de AutoFilter y trabajar con tablas dinámicas

Lees y escribes los bits individuales a través de la propiedad indexada AllowOption de TXLSXWorksheet. AllowOption[Opt] = True significa que la acción está permitida; ponerla en False la prohíbe. El conjunto completo también se puede leer o escribir de una vez mediante SheetProtectionOptions, un TXLSXSheetProtectionOptions (un simple set of de Pascal), así que puedes guardarlo, restaurarlo o reemplazarlo por completo

El valor predeterminado importa y es deliberado: una hoja recién creada empieza con todas las opciones permitidas. El constructor inicializa SheetProtectionOptions con el rango completo, [Low(TXLSXSheetProtectionOption)..High(TXLSXSheetProtectionOption)]. A partir de ahí reduces el conjunto excluyendo las acciones que quieres prohibir, en lugar de construir un permiso desde cero. Esa elección es lo que hace que la regla de codificación del escritor, más abajo, encaje con el comportamiento de Excel

Proteger una hoja y dejar libres la ordenación y el filtro

Este es el caso habitual de principio a fin: proteger un informe ya terminado para que el diseño no pueda reorganizarse, pero permitir que el lector lo ordene y lo filtre. Ten en cuenta que Protect y las opciones son independientes. Protect cambia la hoja al estado protegido y guarda el hash opcional de la contraseña; no toca el conjunto de opciones. Ajustas AllowOption por separado y los cambios surten efecto una vez que la hoja está protegida y guardada

var
  wb: TXLSXWorkbook;
  sh: TXLSXWorksheet;
begin
  wb := TXLSXWorkbook.Create;
  try
    sh := wb.Sheets.Add('Protected');
    sh.Cells[1, 1].Value := 'Region'; sh.Cells[1, 2].Value := 'Units';
    sh.Cells[2, 1].Value := 'North';  sh.Cells[2, 2].Value := 120;
    sh.Cells[3, 1].Value := 'South';  sh.Cells[3, 2].Value := 98;

    // Protect with a password. This only sets the protected state + hash;
    // the option set is left at its all-permitted default.
    sh.Protect('HotXLS-2026');

    // Narrow: keep sort + AutoFilter, forbid reshaping and reformatting.
    sh.AllowOption[xlsxSpoSort]          := True;
    sh.AllowOption[xlsxSpoAutoFilter]    := True;
    sh.AllowOption[xlsxSpoFormatCells]   := False;
    sh.AllowOption[xlsxSpoFormatColumns] := False;
    sh.AllowOption[xlsxSpoFormatRows]    := False;
    sh.AllowOption[xlsxSpoInsertRows]    := False;
    sh.AllowOption[xlsxSpoDeleteRows]    := False;

    if wb.SaveAs('protection.xlsx') <> 1 then
      Writeln('SaveAs failed');
  finally
    wb.Free;
  end;
end;

Hay dos cosas que sacar de ese fragmento. Las líneas Sort y AutoFilter se escriben explícitamente aunque ambas tengan como valor predeterminado True; eso es documentación para quien mantenga el código después, no una necesidad funcional. Y como los valores predeterminados son permisivos, las únicas líneas que cambian el archivo de salida son las que ponen una opción en False. Eso no es una casualidad de esta API, sino la propia representación OOXML asomando, que es lo que explica la siguiente sección

La regla de codificación: omitir equivale a permitir, attr=0 equivale a prohibir

Este es el único dato contraintuitivo de toda la característica, y es donde suele fallar el <sheetProtection> escrito a mano. En OOXML, cada atributo por acción es una marca de prohibición, y su ausencia significa permiso. Un atributo ausente significa que la acción está permitida. Un atributo escrito como "0" significa que la acción queda prohibida mientras la hoja está protegida. No existe un formatCells="1" en un archivo bien formado que signifique "se permite dar formato"; simplemente dejas el atributo fuera. (El valor predeterminado de un atributo ausente es el booleano por defecto de OOXML, true, y estos atributos están nombrados de modo que "true" significa que la edición correspondiente está permitida)

El escritor de HotXLS lo refleja exactamente. Emite sheet="1" para activar la protección y luego recorre el conjunto de opciones, escribiendo attr="0" solo para las opciones que pongas en False. Las acciones permitidas no aportan nada al resultado. Así que el libro del apartado anterior se serializa en algo parecido a esto, con solo las acciones prohibidas y el hash de la contraseña:

// Conceptual output for the snippet above (attributes elided for brevity):
// <sheetProtection sheet="1"
//   formatCells="0" formatColumns="0" formatRows="0"
//   insertRows="0" deleteRows="0"
//   password="...4-hex..."/>
// Note what is NOT there: no sort, no autoFilter, no selectLockedCells.
// Their absence is exactly what tells Excel those actions stay allowed.

Si venías de la cadena fija antigua y esperabas ver todos los atributos escritos, esto parece escaso, casi incorrecto. No lo es. Un archivo que enumerara sort="1" y autoFilter="1" significaría lo mismo para un lector conforme, pero Excel escribe por sí mismo la forma mínima basada solo en prohibiciones, y copiar ese patrón mantiene pequeños los diffs y tranquilos los reencuadres. Los atributos objects y scenarios siguen la misma regla: por defecto están permitidos, así que solo aparecen como "0" cuando los prohíbes, que es justo lo contrario de los viejos objects="1" scenarios="1" que se emitían sin condición

Leer la protección de vuelta: fidelidad en ida y vuelta

Un modelo de permisos que puedes escribir pero no leer es una calle de sentido único, y el síntoma habitual es un ciclo cargar-editar-guardar que amplía silenciosamente los permisos. HotXLS corrige eso. Cuando ParseWorksheetXml encuentra un elemento <sheetProtection>, marca la hoja como protegida, captura el hash de la contraseña si existe y después decodifica cada atributo por acción de vuelta a AllowOption usando la misma convención en sentido inverso: un atributo presente y igual a "0" prohíbe la acción; un atributo ausente deja la opción en su valor permitido predeterminado

var
  wb: TXLSXWorkbook;
  sh: TXLSXWorksheet;
begin
  wb := TXLSXWorkbook.Create;
  try
    wb.LoadFromFile('protection.xlsx');
    sh := wb.Sheets[1];                  // XLSX sheets are 1-based
    if sh.IsProtected then
    begin
      Writeln('Protected; password hash present: ',
        sh.SheetProtectHash <> '');
      Writeln('Sort allowed:       ', sh.AllowOption[xlsxSpoSort]);
      Writeln('AutoFilter allowed: ', sh.AllowOption[xlsxSpoAutoFilter]);
      Writeln('FormatCells allowed:', sh.AllowOption[xlsxSpoFormatCells]);
    end;
  finally
    wb.Free;
  end;
end;

Cargas el archivo que escribió el escritor y recuperas Sort y AutoFilter como True, FormatCells como False, el conjunto que guardaste, intacto. Esa simetría es todo el objetivo: editar una celda en una hoja protegida con permisos parciales y volver a guardar, y que los catorce permisos que no tocaste sobrevivan en lugar de volver al viejo valor predeterminado de todo o nada

Notas prácticas y límites

Conviene saber algunas cosas antes de integrar esto en una cadena de generación de informes:

  • La contraseña es débil por diseño. La protección de hojas XLSX almacena un hash heredado de 16 bits, el mismo que Excel lleva usando durante décadas, y se mantiene aquí por compatibilidad. Sirve para evitar ediciones accidentales; no resiste a un atacante. No la trates como un guardián de secretos. Para una protección real, cifra el libro
  • Establecer opciones antes de proteger está bien. AllowOption se puede asignar tanto si la hoja está protegida como si no; los conmutadores solo describen lo que la protección permitirá una vez que Protect esté en efecto. UnProtect limpia el estado protegido y el hash, pero deja tu conjunto de opciones listo para la próxima vez
  • La semántica de las celdas bloqueadas sigue aplicándose. La protección solo bloquea la edición de las celdas cuyo atributo Locked está activado, que es el valor predeterminado del libro. Dejar editable una zona de entrada es tarea del estilo de celda, no de una opción de protección; ambas capas se combinan igual que en Excel
  • Este es el motor XLSX. El modelo de opciones refleja las propiedades Allow* más antiguas del motor XLS, pero aquí los nombres de la enumeración y la propiedad (xlsxSpo*, AllowOption) pertenecen a TXLSXWorksheet en lxHandleX. Si además controlas el diseño de impresión en las mismas hojas, la guía de protección y configuración de página explica cómo estas opciones conviven con las áreas de impresión y los encabezados, y validación de datos, AutoFilter y tablas encaja de forma natural con dejar xlsxSpoAutoFilter abierto en un informe bloqueado

El modelo de protección detallada y el resto del motor de lectura y escritura XLSX se entregan en HotXLS Component para Delphi y C++Builder; la página del producto contiene la API completa de hojas de cálculo, incluida la referencia completa de las opciones de protección