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.