Ordenação Dinâmica de TBufDataSet com Suporte a Múltiplas Colunas no Lazarus

Ao trabalhar com componentes de grade de dados (DBGrid) conectados a um TBufDataSet no Lazarus, é comum desejar que o usuário possa ordenar os registros clicando nos títulos das colunas. A seguir, apresenta-se uma abordagem para implementar ordenação alternada (ascendente/descendente) e ordenação por múltiplas colunas utilizando a tecla Shift.

Ordenação Simples por Coluna

A função abaixo verifica se já existe um índice aplicado para o campo solicitado. Caso exista um índice ascendente, alterna para descendente, e vice-versa. Se nenhum índice existir, cria um novo. Campos do tipo TBlobField, TVariantField e TBinaryField são ignorados por não serem ordenáveis de forma convencional.

uses
  BufDataset, typinfo;

function AplicarOrdenacao(AConjuntoDados: TBufDataSet; const ANomeCampo: String): Boolean;
var
  Contador: Integer;
  DefinicoesIdx: TIndexDefs;
  NomeIndice: String;
  OpcoesIndice: TIndexOptions;
  CampoAlvo: TField;
begin
  Result := False;
  CampoAlvo := AConjuntoDados.Fields.FindField(ANomeCampo);
  if CampoAlvo = nil then Exit;

  if (CampoAlvo is TBlobField) or (CampoAlvo is TVariantField)
     or (CampoAlvo is TBinaryField) then Exit;

  if IsPublishedProp(AConjuntoDados, 'IndexDefs') then
    DefinicoesIdx := GetObjectProp(AConjuntoDados, 'IndexDefs') as TIndexDefs
  else
    Exit;

  if not IsPublishedProp(AConjuntoDados, 'IndexName') then
    Exit;

  DefinicoesIdx.Updated := False;
  DefinicoesIdx.Update;

  NomeIndice := GetStrProp(AConjuntoDados, 'IndexName');

  if NomeIndice = ANomeCampo + '_Asc' then
  begin
    NomeIndice := ANomeCampo + '_Desc';
    OpcoesIndice := [ixDescending];
  end
  else
  begin
    NomeIndice := ANomeCampo + '_Asc';
    OpcoesIndice := [];
  end;

  for Contador := 0 to Pred(DefinicoesIdx.Count) do
  begin
    if DefinicoesIdx[Contador].Name = NomeIndice then
    begin
      Result := True;
      Break;
    end;
  end;

  if not Result then
  begin
    if NomeIndice = ANomeCampo + '_Desc' then
      AConjuntoDados.AddIndex(NomeIndice, ANomeCampo, OpcoesIndice, ANomeCampo)
    else
      AConjuntoDados.AddIndex(NomeIndice, ANomeCampo, OpcoesIndice);
    Result := True;
  end;

  SetStrProp(AConjuntoDados, 'IndexName', NomeIndice);
end;

Para integrar com um TDBGrid, basta chamar a função no evento OnTitleClick:

procedure TFormPrincipal.DBGridProdutosTitleClick(Coluna: TColumn);
begin
  AplicarOrdenacao(FDsProdutos, Coluna.FieldName);
end;

Ordenação por Múltiplas Colunas com Helper

Quando se deseja ordenar por mais de uma coluna simultaneamente (pressionando Shift), um class helper para TDBGrid pode encapsularr toda a lógica de gerenciamento de índices compostos e ícones de direção nos títulos. O helper abaixo mantém internamente um TStringList delimitado para representar a combinação atual de campos e direções.

type
  TDBGridOrdenacao = class helper for TDBGrid
  public const
    MAX_CAMPOS_ORDENACAO = 3;
  private
    procedure MontarListasCampos(AEntrada: TStrings; out ACampos, ADescendentes: String);
    procedure AtualizarIconesColunas(AEntrada: TStrings; AIdxAsc, AIdxDesc: Integer);
    function IndiceJaExiste(ADefs: TIndexDefs; const ANome: String): Boolean;
  public
    procedure OrdenarPor(const ANomeCampo: String; AIdxAsc: Integer = -1; AIdxDesc: Integer = -1);
    procedure LimparOrdenacao;
  end;

procedure TDBGridOrdenacao.MontarListasCampos(AEntrada: TStrings;
  out ACampos, ADescendentes: String);
var
  ListaCampos: TStringList;
  ListaDesc: TStringList;
  NomeCampo, Direcao: String;
  k: Integer;
begin
  ACampos := '';
  ADescendentes := '';
  if AEntrada.Count = 0 then Exit;

  ListaCampos := TStringList.Create;
  ListaDesc := TStringList.Create;
  try
    ListaCampos.Delimiter := ';';
    ListaDesc.Delimiter := ';';

    for k := 0 to AEntrada.Count - 1 do
    begin
      AEntrada.GetNameValue(k, NomeCampo, Direcao);
      ListaCampos.Add(NomeCampo);
      if Direcao = 'D' then
        ListaDesc.Add(NomeCampo);
    end;

    ACampos := ListaCampos.DelimitedText;
    ADescendentes := ListaDesc.DelimitedText;
  finally
    ListaCampos.Free;
    ListaDesc.Free;
  end;
end;

procedure TDBGridOrdenacao.AtualizarIconesColunas(AEntrada: TStrings;
  AIdxAsc, AIdxDesc: Integer);
var
  m: Integer;
  ValorDir: String;
begin
  for m := 0 to Self.Columns.Count - 1 do
  begin
    ValorDir := AEntrada.Values[Self.Columns[m].Field.FieldName];

    if ValorDir = 'A' then
      Self.Columns[m].Title.ImageIndex := AIdxAsc
    else if ValorDir = 'D' then
      Self.Columns[m].Title.ImageIndex := AIdxDesc
    else
      Self.Columns[m].Title.ImageIndex := -1;
  end;
end;

function TDBGridOrdenacao.IndiceJaExiste(ADefs: TIndexDefs;
  const ANome: String): Boolean;
var
  n: Integer;
begin
  for n := 0 to ADefs.Count - 1 do
    if ADefs[n].Name = ANome then
      Exit(True);
  Result := False;
end;

procedure TDBGridOrdenacao.OrdenarPor(const ANomeCampo: String;
  AIdxAsc, AIdxDesc: Integer);
var
  CampoVerificado: TField;
  DS: TBufDataset;
  Defs: TIndexDefs;
  NomeAtual, Sentido, ListaCamposResult, ListaDescResult: String;
  MapaCampos: TStringList;
begin
  if not Assigned(DataSource.DataSet) or
     not DataSource.DataSet.Active or
     not (DataSource.DataSet is TBufDataset) then
    Exit;

  DS := DataSource.DataSet as TBufDataset;
  CampoVerificado := DS.FindField(ANomeCampo);
  if (CampoVerificado is TBlobField) or
     (CampoVerificado is TVariantField) or
     (CampoVerificado is TBinaryField) then
    Exit;

  Defs := DS.IndexDefs;
  NomeAtual := DS.IndexName;

  if not Defs.Updated then
    Defs.Update;

  MapaCampos := TStringList.Create;
  try
    MapaCampos.DelimitedText := NomeAtual;
    Sentido := MapaCampos.Values[ANomeCampo];

    case AnsiIndexStr(Sentido, ['A', 'D']) of
      0: Sentido := 'D';
      1: Sentido := 'A';
    else
      Sentido := 'A';
    end;

    if ssShift in GetKeyShiftState then
    begin
      MapaCampos.Values[ANomeCampo] := Sentido;
      if MapaCampos.Count > MAX_CAMPOS_ORDENACAO then
        Exit;
    end
    else
    begin
      MapaCampos.Clear;
      MapaCampos.Values[ANomeCampo] := Sentido;
    end;

    NomeAtual := MapaCampos.DelimitedText;

    if not IndiceJaExiste(Defs, NomeAtual) then
    begin
      MontarListasCampos(MapaCampos, ListaCamposResult, ListaDescResult);
      DS.AddIndex(NomeAtual, ListaCamposResult, [], ListaDescResult, '');
    end;

    DS.IndexName := NomeAtual;
    AtualizarIconesColunas(MapaCampos, AIdxAsc, AIdxDesc);
  finally
    MapaCampos.Free;
  end;
end;

procedure TDBGridOrdenacao.LimparOrdenacao;
var
  DS: TBufDataset;
  Vazios: TStringList;
begin
  if not Assigned(DataSource.DataSet) or
     not DataSource.DataSet.Active or
     not (DataSource.DataSet is TBufDataset) then
    Exit;

  DS := DataSource.DataSet as TBufDataset;
  DS.IndexName := '';

  Vazios := TStringList.Create;
  try
    AtualizarIconesColunas(Vazios, -1, -1);
  finally
    Vazios.Free;
  end;
end;

Para ativar a ordenação, conecte os eventos de clique no título e de clique na célula. O evento OnTitleClick aplica ordenação ascendente ou descendente (com Shift para colunas adicionais), enquanto um duplo clique na primeira célula limpa a ordenação:

procedure TForm1.GridPaisesCellClick(Coluna: TColumn);
begin
  if not Assigned(Coluna) then
    GridPaises.LimparOrdenacao;
end;

procedure TForm1.GridPaisesTitleClick(Coluna: TColumn);
begin
  GridPaises.OrdenarPor(Coluna.Field.FieldName, 0, 1);
end;

Ao atribuir uma lista de imagens ao TitleImageList do grid, os índices passados como parâmetro em OrdenarPor controlam qual ícone representa a ordenação ascendente e qual representa a descendente.

Tags: Lazarus Free Pascal TBufDataSet TDBGrid Ordenação de Dados

Publicado em 6-7 00:22 por Thomas