Artículo técnico

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

Entregas un libro terminado a un colega y le pides que lo filtre, no que lo reescriba. Entonces proteges la hoja. En versiones antiguas de HotXLS, ese gesto escribía una sola cosa en el archivo: <sheetProtection sheet="1" objects="1" scenarios="1"/>, codificada de forma fija, siempre. La hoja se bloqueaba, se adjuntaba el hash de la contraseña y el usuario no podía hacer nada, ni siquiera ordenar y filtrar lo que realmente querías dejar abierto. El propio cuadro de diálogo "Proteger hoja" de Excel tiene quince casillas para eso mismo, y el motor no podía expresar ninguna. Esa brecha es la que cierra el modelo de protección de v2.91.0

HotXLS es un componente nativo VCL de hoja de cálculo 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: el nuevo TXLSXSheetProtectionOption, la propiedad AllowOption que activa o desactiva cada permiso, y la regla de codificación OOXML que suele confundir a cualquiera que escribe a mano un elemento <sheetProtection>

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

Primero, el límite, porque de él depende cuánto deberías confiar en todo esto. La protección de hojas en el formato de hoja de cálculo OOXML (ECMA-376) es una política de interacción, no un cifrado. Le dice a una aplicación conforme qué ediciones debe rechazar mientras la hoja está protegida. Los valores de celda siguen en xl/worksheets/sheetN.xml en texto plano; descomprime el .xlsx y ahí están. La contraseña opcional se guarda como un hash heredado corto, no como una clave que mezcle nada. Cualquiera que renombre el archivo, abra la parte y quite la línea <sheetProtection> podrá leer y editar todo

Así que la protección responde "evita que mi compañero pise una fórmula por accidente", no "mantén estos datos secretos frente a alguien decidido". Son problemas distintos con herramientas distintas. Si necesitas confidencialidad, te conviene el cifrado a nivel de libro cubierto en 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 diferencia y el resto de la página es solo cableado

Las quince opciones y la propiedad AllowOption

Cada hoja ahora lleva un conjunto de valores TXLSXSheetProtectionOption que describen qué puede seguir haciendo el usuario mientras la hoja está protegida. Los miembros se mapean 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 hipotéticos
  • xlsxSpoFormatCells, xlsxSpoFormatColumns, xlsxSpoFormatRows: 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 menús desplegables de AutoFilter y trabajar con tablas dinámicas

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

La configuración predeterminada importa y es deliberada: 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 vez de construir un conjunto de permisos desde cero. Esa decisión es la que hace que la regla de codificación del escritor, más abajo, coincida con el comportamiento de Excel

Proteger una hoja sin cerrar el ordenamiento ni el filtro

Aquí va el caso común de principio a fin: proteger un informe terminado para que no puedan cambiarle el diseño, pero dejar que el lector lo ordene y lo filtre. Nota que Protect y las opciones son independientes. Protect pone la hoja en 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;

Dos cosas se desprenden de ese fragmento. Las líneas de Sort y AutoFilter se escriben explícitamente aunque ambas tengan True por defecto; eso es documentación para quien venga después, no un requisito 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 un accidente de esta API, es el formato OOXML dejándose ver, y esa es la siguiente sección

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

Este es el único hecho contraintuitivo de toda la función, y es donde suele fallar un <sheetProtection> escrito a mano. En OOXML, cada atributo por acción es una marca de prohibición, y su ausencia significa permiso. Un atributo que falta quiere decir que la acción está permitida. Un atributo escrito como "0" quiere decir que la acción está prohibida mientras la hoja está protegida. No existe un formatCells="1" en un archivo bien formado que signifique "el formato está permitido"; simplemente se omite el atributo. (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 y escribe attr="0" solo para las opciones que pongas en False. Las acciones permitidas no contribuyen a la salida. 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 vieja cadena codificada a mano y esperabas ver cada atributo escrito, esto parece escaso, casi incorrecto. Pero es correcto. Un archivo que listara sort="1" y autoFilter="1" significaría lo mismo para un lector conforme, pero Excel mismo escribe la forma mínima, solo con prohibiciones, y copiar ese patrón mantiene los diffs pequeños y los reintentos de ida y vuelta aburridos. Los atributos objects y scenarios siguen la misma regla: están permitidos por defecto, así que solo aparecen como "0" cuando los prohíbes, que es justo lo contrario del viejo objects="1" scenarios="1" que se emitía siempre

Leer la protección de vuelta: fidelidad de 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 resuelve eso. Cuando ParseWorksheetXml encuentra un elemento <sheetProtection>, marca la hoja como protegida, captura el hash de la contraseña si existe y luego 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 por defecto

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;

Carga el archivo que produjo el escritor y recuperas Sort y AutoFilter como True, FormatCells como False, el conjunto que guardaste, intacto. Esa simetría es la idea central: editar una celda en una hoja protegida y con permisos parciales, guardarla de nuevo y conservar los catorce permisos que no tocaste en lugar de volver al viejo comportamiento de todo o nada

Notas prácticas y límites

A few things worth knowing before you wire this into a report pipeline:

  • The password is weak by design. XLSX worksheet protection stores a 16-bit legacy hash (the same one Excel has used for decades), kept here for interoperability. It deters accidental edits; it does not resist an attacker. Do not treat it as a secret-keeper. For real protection, encrypt the workbook.
  • Configurar opciones antes de proteger está bien. AllowOption puede asignarse tanto si la hoja está protegida como si no; los cambios 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 en su lugar para la próxima vez.
  • La semántica de celdas bloqueadas sigue vigente. La protección solo bloquea la edición de celdas cuyo atributo Locked está establecido (el valor predeterminado del libro). Dejar editable una zona de entrada es trabajo del estilo de celda, no 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 antiguas propiedades Allow* del motor XLS, pero aquí los nombres del enum y de la propiedad (xlsxSpo*, AllowOption) pertenecen a TXLSXWorksheet en lxHandleX. Si además controlas el diseño de impresión sobre las mismas hojas, la guía de protección y configuración de página explica cómo conviven estos ajustes 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 incluyen en HotXLS Component para Delphi y C++; la página del producto incluye la API completa de hojas de cálculo, incluida la referencia completa de opciones de protección