Reordenação de Items em ListBox

Eu tenho uma tabela que contém itens ordenados. A coluna “posicao” indica a posição relativa de cada item (de 1 a n) e seus valores não podem ser repetidos. Eu precisava de uma forma prática de redefinir a ordenação dos itens. A dificuldade é que a cada vez que um item muda de posição todos os itens posteriores também devem mudar.

A solução foi levar todos os nomes de itens para um TListBox respeitando a ordenação atual e deixar o usuário livremente posicionar os nomes sem se preocupar com o dataset. Quando o usuário estiver satisfeito com a ordem dos itens, clica em OK e o programa atribui de uma só vez todos os valores de “posicao” ao dataset conforme os índices dos nomes no ListBox.

ListBox Reorder DemoA idéia foi rapidamente implementada pelo Fábio Costa, que trabalha comigo. A solução inicial usava apenas teclas de atalho e botões para mover o item selecionado para cima e para baixo, para a primeira ou última posição. O problema é que ele começou a tomar gosto pela coisa e danou a ter idéias. Por que não arrastar e soltar um item com o mouse para mudar sua posição? Por que não fazer o mouse dar scroll sozinho no ListBox quando o usuário estiver arrastando para além da primeira ou da última posição do controle?

Assim, a coisa foi evoluindo para uma solução mais e mais sofisticada. Não é que ficou bom?

Várias técnicas foram utilizadas para chegar ao resultado final: TActionList com teclas de atalhos (Ctrl+Home, Ctrl+Up, Ctrl+Down, Ctrl+End) e habilitação condicional, manipulação dos eventos OnStartDrag, OnDragOver, OnDragDrop e OnDragEnd, execução de mensagens do Windows pelo método Perform, scroll de TListBox por código, identificação do item do ListBox dada uma posição (X, Y) de tela, timers com intervalos dinâmicos (1s para começar, 0.2s para continuar a rolagem).

Veja o código de exemplo comentado abaixo, sem a parte de banco de dados. Você também pode fazer o download do código fonte do projeto de demonstração.

unit ListBoxReorder_Form;
 
interface
 
uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ActnList, StdCtrls, ExtCtrls;
 
type
  TScrollZone = (szNone, szTop, szBottom);
 
  TMainForm = class(TForm)
    ActionList: TActionList;
    ListBox: TListBox;
    ScrollTimer: TTimer;
    DownAction: TAction;
    DownButton: TButton;
    FirstAction: TAction;
    FirstButton: TButton;
    LastAction: TAction;
    LastButton: TButton;
    UpAction: TAction;
    UpButton: TButton;
    procedure FormCreate(Sender: TObject);
    procedure FirstActionExecute(Sender: TObject);
    procedure FirstActionUpdate(Sender: TObject);
    procedure UpActionExecute(Sender: TObject);
    procedure UpActionUpdate(Sender: TObject);
    procedure DownActionExecute(Sender: TObject);
    procedure DownActionUpdate(Sender: TObject);
    procedure LastActionExecute(Sender: TObject);
    procedure LastActionUpdate(Sender: TObject);
    procedure ListBoxStartDrag(Sender: TObject; var DragObject: TDragObject);
    procedure ListBoxDragOver(Sender, Source: TObject; X, Y: Integer;
      State: TDragState; var Accept: Boolean);
    procedure ListBoxDragDrop(Sender, Source: TObject; X, Y: Integer);
    procedure ListBoxEndDrag(Sender, Target: TObject; X, Y: Integer);
    procedure ScrollTimerExecute(Sender: TObject);
  private
    FScrollZone: TScrollZone;
    procedure InitListBox;
    function IsSelected: Boolean;
    procedure MoveItem(NewIndex: Integer);
    function GetScrollZone(X, Y: Integer): TScrollZone;
  end;
 
var
  MainForm: TMainForm;
 
implementation
 
{$R *.dfm}
 
{ Inicializa ListBox.Items para demonstração }
procedure TMainForm.FormCreate(Sender: TObject);
begin
  InitListBox;
  ListBox.DragMode := dmAutomatic;
end;
 
{ Inicializa ListBox.Items com nomes de arquivos da pasta System32 }
procedure TMainForm.InitListBox;
var
  F: TSearchRec;
begin
  ListBox.Items.BeginUpdate;
  if FindFirst('C:\Windows\System32\*.*', faArchive, F) = 0 then
    repeat
      ListBox.Items.Add(F.Name);
    until FindNext(F) <> 0;
  FindClose(F);
  ListBox.Items.EndUpdate;
  ListBox.ItemIndex := 0;
end;
 
{ Indica se algum item do ListBox está selecionado }
function TMainForm.IsSelected: Boolean;
begin
  Result := ListBox.ItemIndex <> -1;
end;
 
{ Move um item do ListBox para outra posição,
  seleciona a nova posição no ListBox e
  atribui o foco ao ListBox. }
procedure TMainForm.MoveItem(NewIndex: Integer);
begin
  ListBox.Items.Move(ListBox.ItemIndex, NewIndex);
  ListBox.ItemIndex := NewIndex;
  ListBox.SetFocus;
end;
 
procedure TMainForm.DownActionExecute(Sender: TObject);
begin
  MoveItem(ListBox.ItemIndex + 1);
end;
 
procedure TMainForm.DownActionUpdate(Sender: TObject);
begin
  DownAction.Enabled := IsSelected and
    (ListBox.ItemIndex < ListBox.Items.Count - 1);
end;
 
procedure TMainForm.FirstActionExecute(Sender: TObject);
begin
  MoveItem(0);
end;
 
procedure TMainForm.FirstActionUpdate(Sender: TObject);
begin
  FirstAction.Enabled := IsSelected and (ListBox.ItemIndex > 0);
end;
 
procedure TMainForm.LastActionExecute(Sender: TObject);
begin
  MoveItem(ListBox.Items.Count - 1);
end;
 
procedure TMainForm.LastActionUpdate(Sender: TObject);
begin
  LastAction.Enabled := IsSelected and
    (ListBox.ItemIndex < ListBox.Items.Count - 1);
end;
 
procedure TMainForm.UpActionExecute(Sender: TObject);
begin
  MoveItem(ListBox.ItemIndex - 1);
end;
 
procedure TMainForm.UpActionUpdate(Sender: TObject);
begin
  UpAction.Enabled := IsSelected and (ListBox.ItemIndex > 0);
end;
 
{ Inicializa FScrollZone com szNone }
procedure TMainForm.ListBoxStartDrag(Sender: TObject;
  var DragObject: TDragObject);
begin
  FScrollZone := szNone;
end;
 
{ Define se a posição do mouse aponta para um item válido
  do ListBox que permita soltar o item arrastado (Accept).
  Inicia ou cancela o timer de scroll automático. }
procedure TMainForm.ListBoxDragOver(Sender, Source: TObject; X, Y: Integer;
  State: TDragState; var Accept: Boolean);
var
  NewZone: TScrollZone;
begin
  Accept := (Source = ListBox) and
    (ListBox.ItemAtPos(Point(X, Y), True) <> -1);
  NewZone := GetScrollZone(X, Y);
  if NewZone <> FScrollZone then
  begin
    FScrollZone := NewZone;
    if (FScrollZone <> szNone) then
    begin
      ScrollTimer.Interval := 1000;
      ScrollTimer.Enabled := True;
    end
    else
      ScrollTimer.Enabled := False;
  end;
end;
 
{ Move item selecionado para nova posição }
procedure TMainForm.ListBoxDragDrop(Sender, Source: TObject; X, Y: Integer);
var
  NewIndex: Integer;
begin
  NewIndex := ListBox.ItemAtPos(Point(X, Y), True);
  if NewIndex <> -1 then
    MoveItem(NewIndex);
end;
 
{ Desliga timer de scroll no fim do drag and drop }
procedure TMainForm.ListBoxEndDrag(Sender, Target: TObject; X, Y: Integer);
begin
  ScrollTimer.Enabled := False;
end;
 
{ Retorna a szTop se mouse está na área da primeira linha visível do
  ListBox, szBottom se mouse está na área da última linha visível do
  ListBox ou szNone caso contrátrio. }
function TMainForm.GetScrollZone(X, Y: Integer): TScrollZone;
begin
  if (Y < ListBox.ItemHeight) then
    Result := szTop
  else
    if (Y >= ListBox.ClientHeight - ListBox.ItemHeight) then
      Result := szBottom
    else
      Result := szNone;
end;
 
{ Faz scroll de uma linha para cima ou para baixo conforme a zona
  de scroll definida pela posição do mouse. Redefine intervalo
  do timer para 0.2s após o primeiro scroll. }
procedure TMainForm.ScrollTimerExecute(Sender: TObject);
begin
  ScrollTimer.Interval := 200;
  if (FScrollZone = szTop) then
  begin
    ListBox.Perform(WM_VSCROLL, SB_LINEUP, 0);
    ListBox.Perform(WM_VSCROLL, SB_ENDSCROLL, 0);
  end
  else
    if (FScrollZone = szBottom) then
    begin
      ListBox.Perform(WM_VSCROLL, SB_LINEDOWN, 0);
      ListBox.Perform(WM_VSCROLL, SB_ENDSCROLL, 0);
    end;
end;
 
end.

Comments

One Response to “Reordenação de Items em ListBox”

  1. Mauro Otoni on November 21st, 2008 9:41 am

    Maravilha de artigo, sensacional. Gostaria de aproveitar este espaço e pedir sua ajuda mais uma vez. Estou migrando meu projeto do Delphi 7 para o Delphi 2009, tenho uma rotina no delphi 7 que lista o conteudo de um Memo usando o “assignPrn” no delphi 7 funciona perfeitamente porem quando implemento do Delphi 2009 no relatório é imprimido sugeira “(@##– ªªº]…)” será q/ tem algum detalhe q/ eu deva mudar no delphi2009. Se Vc puder me ajudar. Grande abraço - Mauro

Deixe uma Resposta




XHTML: Você pode usar essas tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="">