Artigo Técnico

Validação de lista do HotXLS: limite de 255 caracteres e x14

O Excel se recusa a abrir uma pasta de trabalho XLSX cuja validação de dados de lista guarda uma lista inline com mais de 255 caracteres, e isso vale até para um arquivo que o próprio Excel salvou. O HotXLS, o componente Excel nativo para Delphi e C++Builder, carrega essas listas e modela regras de lista entre planilhas como extensões x14, mas a correção duradoura é a mesma em todo lugar: mantenha listas longas num range de planilha ou num defined name

O relatório de bug costuma chegar de um cliente, não dos seus testes. Um template ganha uma coluna Region ou Product cuja lista suspensa começou com oito entradas e agora tem sessenta. Tudo continua funcionando na cópia XLSB que alguém guarda num drive compartilhado. Aí um job de exportação grava a mesma regra num XLSX, e o Excel responde com um prompt de reparo ou uma recusa seca. Nada no seu código mudou exceto o comprimento de uma string, que é exatamente por que vale entender essa fronteira antes de ela te encontrar

Onde o limite de 255 caracteres de lista realmente se aplica?

O limite se aplica à forma inline de uma validação de lista no XLSX, a string entre aspas separada por vírgulas armazenada no formula1, e o Excel o impõe ao abrir o arquivo, não só quando você digita a lista no diálogo Data Validation. Testar contra o Excel direto deu um resultado limpo: uma lista de 253 caracteres abriu, enquanto listas de 260 e de 421 caracteres foram recusadas. O caso de 421 caracteres é o instrutivo, porque veio de uma pasta de trabalho que o próprio Excel escreveu. O Excel grava um arquivo que o Excel não vai reler

O Excel recusa listas inline XLSX além de 255 caracteres enquanto tokens PtgStr do XLSB carregam listas longas, mostrado como matriz de resultado por contêiner no HotXLS Delphi
a parede é uma restrição de aplicação por contêiner: o Excel recusa a forma inline XLSX que ele mesmo escreve, enquanto tokens XLSB e listas apoiadas em range carregam os mesmos itens

O schema não te salva aqui. O ECMA-376 Part 1, §18.3.1.32, define o elemento dataValidation e as fórmulas dele sem declarar um teto de 255 caracteres, então um arquivo pode ser válido pelo schema e ainda assim ser recusado. Essa é uma restrição de aplicação, e a única referência confiável para ela é o comportamento do próprio Excel

O HotXLS não impõe esse teto por você. O TXLSXWorksheet.AddListValidation envolve os itens em aspas e os armazena como dados; ele nem trunca nem recusa uma lista longa, então uma lista de 400 caracteres entra no XLSX salvo exatamente como pedida e o Excel depois recusa o resultado. Essa é uma escolha deliberada, já que aparar uma lista em silêncio corromperia dados de um jeito que ninguém nota, mas põe a checagem de comprimento do seu lado. Um helper pequeno mantém tudo honesto:

const
  // O Excel abriu uma lista de 253 caracteres e recusou 260 e 421.
  // Mantenha uma margem em vez de mirar na borda exata.
  MaxInlineListChars = 250;

function InlineListLength(const AItems: array of WideString): Integer;
var
  I: Integer;
begin
  Result := 0;
  for I := 0 to High(AItems) do
    Inc(Result, Length(AItems[I]));
  if Length(AItems) > 1 then
    Inc(Result, High(AItems));  // vírgulas separadoras
end;

Trate aspas duplas embutidas como custando dois caracteres, porque o AddListValidation as duplica para manter a string de fórmula válida

Por que o XLSB pode carregar listas que o XLSX não pode

O XLSB armazena uma lista inline como um único token de string PtgStr cujo comprimento é um campo de 16 bits, então o formato binário não tem parede de 255 caracteres. Uma amostra nativa com 60 itens e 419 caracteres vive num único token e faz ida e volta no Excel sem reclamar. A mesma lista no XLSX é o caso de 421 caracteres acima

Um loader que aplicasse o limite de 255 caracteres do diálogo ao XLSB recusaria esse arquivo. A checagem de token de validação XLSB do HotXLS limita um token de string a 32.767 caracteres em vez disso, o mesmo teto de uma string de célula do Excel. Duas qualificações valem ser ditas com clareza. O loader também limita uma fórmula de validação completa a 16.384 bytes, então o teto prático para uma lista inline é menor do que a checagem de token sozinha sugere. E listas separadas por caracteres NUL dentro de um PtgStr ficam fora do perfil suportado, então são reportadas como não suportadas em vez de chutadas

A armadilha fica na conversão. Uma lista longa que está perfeitamente saudável no XLSB vira um arquivo recusado no momento em que qualquer coisa, HotXLS incluído, salva essa pasta de trabalho como XLSX. Se um pipeline converte arquivos XLSB de clientes, rode a checagem de comprimento em toda regra de lista antes do save para XLSX, não depois de um cliente reclamar

O que acontece com listas que apontam para outra planilha?

O Excel armazena uma lista cuja fonte fica numa planilha diferente na extensão x14:dataValidations, não no elemento comum dataValidations. A regra vive dentro do extLst da planilha sob o URI de extensão {CCE6A557-97BC-4b89-ADB6-D9C93CAAB3DF}, definido no [MS-XLSX], e a referência de fonte dela é um elemento xm:f como Lists!$A$2:$A$61. O XLSB mantém a mesma regra nos próprios records de extensão dele

O HotXLS parseia essas regras de extensão XLSX na coleção tipada DataValidations e marca cada uma com TXLSXDataValidation.IsExtension. Uma lista entre planilhas então aparece ao lado das regras comuns com o range, o tipo e a fórmula legíveis, e ela sobrevive a edições, saves e reaberturas. No save, o writer mantém regras de extensão fora do elemento comum e reconstrói a extensão x14 na posição de schema dela, na cauda da planilha. As duas fórmulas são mantidas, então regras de número inteiro, decimal, comprimento de texto, data e hora usando between ou notBetween mantêm os dois limites

Anatomia da extensão x14 para validações de lista entre planilhas no Delphi com HotXLS: regras ficam no extLst da planilha com uma fonte xm:f, parseadas em coleções tipadas
regras de lista entre planilhas vivem na extensão x14, e o HotXLS as mantém tipadas, marcadas com IsExtension e reconstruídas na cauda da planilha no save

O perfil tipado de extensão cobre a regra inteira. Além de type, operator, allowBlank, as duas flags de show, o range e as duas fórmulas, uma regra de extensão mantém o título e o texto do prompt de entrada, o título e o texto do alerta de erro, o estilo do alerta, o modo IME e a configuração hide-dropdown através de uma carga e resave, com as mesmas regras de escaping de uma validação comum

uses
  SysUtils, lxHandleX, lxDiagnostics;

procedure AuditListRules(const FileName: WideString);
var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Dv: TXLSXDataValidation;
  S, I: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    if Book.Open(FileName) <> 1 then
      raise Exception.Create('Cannot open ' + FileName);
    for S := 1 to Book.Sheets.Count do        // Sheets[] tem base 1
    begin
      Sheet := Book.Sheets[S];
      for I := 0 to Sheet.DataValidations.Count - 1 do
      begin
        Dv := Sheet.DataValidations[I];
        if Dv.ValidationType <> xlsxDvList then
          Continue;
        if Dv.IsExtension then
          Writeln(Sheet.Name, ' ', Dv.Range, ': x14 list from ', Dv.Formula1)
        else if (Dv.Formula1 <> '') and (Dv.Formula1[1] = '"') and
                (Length(Dv.Formula1) - 2 > MaxInlineListChars) then
          Writeln(Sheet.Name, ' ', Dv.Range, ': inline list of ',
            Length(Dv.Formula1) - 2, ' chars, refused by Excel as XLSX');
      end;
    end;
  finally
    Book.Free;
  end;
end;

Para ler os itens por trás de uma lista suspensa, selecionar entradas e conferir a entrada digitada contra essas regras, veja ler itens de listas suspensas e validar entrada com o HotXLS; este artigo fica nos formatos de armazenamento

Como o Excel e o HotXLS tratam tipos de validação desconhecidos

O Excel abre um dataValidation cujo atributo type guarda um token não reconhecido como uma regra de tipo none, mantém a fórmula dele, e no save o reescreve como um elemento dataValidation puro sem atributo type, descartando o atributo desconhecido. Tais tokens vêm de XML editado à mão, geradores de terceiros e recursos futuros do Office, e a resposta do Excel é tolerância, não rejeição

O HotXLS segue a mesma tolerância. Em vez de descartar a regra, o que a perderia no resave sem rastro nenhum, o reader a mantém como xlsxDvNone com as fórmulas e o range dela, e registra um diagnóstico de aviso por regra normalizada com código 1307, exposto como a constante xlsDiagnosticValidationTypeNormalized no lxDiagnostics. No save, uma regra none é gravada como o elemento puro, a mesma forma que o Excel produz. Uma regra none não valida nada, então o diagnóstico é o seu sinal de que uma regra que o autor acreditava ativa não restringe mais a entrada

for I := 0 to Book.Diagnostics.Count - 1 do
  if Book.Diagnostics[I].Code = xlsDiagnosticValidationTypeNormalized then
    Writeln('Untyped rule kept on sheet ', Book.Diagnostics[I].SheetName,
      ': ', Book.Diagnostics[I].Message);

Por que o Excel pode recusar uma pasta de trabalho após uma ida e volta?

O Excel recusa uma pasta de trabalho resalva quando um fragmento de extensão re-executado usa um prefixo de namespace que o novo arquivo nunca declara, porque o resultado não é XML bem formado. Fragmentos de extensão costumam carregar atributos com prefixo como xr:uid, cuja declaração xmlns:xr fica na raiz da planilha original, não dentro do fragmento. Copie o fragmento verbatim para uma raiz de planilha nova que não tem essa declaração e as regras de namespace XML quebram antes de o Excel olhar uma única célula

O HotXLS lida com isso na etapa de captura. Ao ler, ele varre cada fragmento re-executado procurando prefixos de elemento e atributo que o fragmento não declara por si, resolve cada prefixo para o URI dele pelo reader, e grava essas declarações na raiz da planilha na hora do save. O caminho de save streaming constrói a raiz pela mesma rotina, então os dois modos de save produzem saída bem formada, e um arquivo resalvo reabre no Excel com toda regra de validação presente, incluindo uma lista x14 entre planilhas re-executada. O quadro mais amplo de como o HotXLS carrega conteúdo extLst que não modela está coberto em idas e voltas XLSX lossless para themes, extLst e calcChain, e as regras gerais por trás do prompt de reparo do Excel estão em por que o Excel mostra um prompt de reparo

Listas longas pertencem a um range ou a um defined name

O design robusto para qualquer lista que possa crescer é armazenar os itens em células de planilha e apontar a validação para elas através de um defined name. Uma fonte de range não tem limite de 255 caracteres em nenhum dos formatos, os itens podem ser editados sem tocar a regra, e um defined name mantém a regra no elemento comum dataValidation mesmo quando as células ficam numa planilha separada, que todo consumidor do arquivo consegue ler

Decisão de fonte de validação de lista no Delphi com HotXLS: listas curtas ficam inline, listas longas migram para um range referenciado através de um defined name
rode a checagem de comprimento antes de a regra ser criada, e o = inicial é o que faz um defined name virar referência em vez de lista de texto de um item
// Lists é uma planilha dedicada, ex.: Book.Sheets.Add('Lists')
function AddListRule(Book: TXLSXWorkbook; Target, Lists: TXLSXWorksheet;
  ACol: Integer; const ARange, AName: WideString;
  const AItems: array of WideString): Integer;
var
  I: Integer;
  ColRef: WideString;
begin
  if InlineListLength(AItems) <= MaxInlineListChars then
  begin
    Result := Target.AddListValidation(ARange, AItems);
    Exit;
  end;
  for I := 0 to High(AItems) do
    Lists.WriteAutoValue(I + 1, ACol, AItems[I]);   // linha e coluna com base 1
  ColRef := Chr(Ord('A') + ACol - 1);               // colunas A..Z
  Book.DefinedNames.Add(AName, Lists.Name + '!$' + ColRef + '$1:$' +
    ColRef + '$' + IntToStr(Length(AItems)));
  // O '=' inicial marca uma referência; sem ele o nome é citado como texto
  Result := Target.AddListValidation(ARange, '=' + AName);
end;

Esse sinal de igual inicial importa. O AddListValidation trata um argumento começando com = como referência e remove o sinal, deixa um argumento contendo ! sem aspas como range, e cita todo o resto como lista inline. Passe 'StatusList' sem o sinal e você recebe uma lista suspensa de um item que literalmente lê StatusList. Dois pontos menores: o WriteAutoValue converte texto do jeito que a entrada de célula do Excel faz, então um item como TRUE vira Boolean, e um nome de planilha contendo espaços precisa de aspas simples na referência

Para uma regra que precisa apontar direto para outra planilha sem nome, defina IsExtension := True na regra no índice que o AddListValidation devolveu, e o HotXLS a grava como a extensão x14, a mesma forma que o próprio Excel usa. A rota de defined name é mais simples e continua funcionando para ferramentas que nunca parseiam extensões, por isso é a recomendação padrão aqui

Todo o comportamento descrito aqui vem no HotXLS Delphi Excel component para Delphi, C++Builder e Lazarus, onde a página do produto lista os tipos de validação suportados, os formatos de arquivo e a referência completa de diagnósticos