Search code examples
listviewdelphi

Delphi ListView toggle item selected


Is there a way to implement a toggle-like selection mode for a TListView?

Clicking on an item, make it selected, click on another item make it selected as well. Clicking on the previous item, make it unselected and keeping the second one selected.

This does not seem to work:

procedure TForm1.ListView1Changing(Sender: TObject; Item: TListItem; Change: TItemChange; var AllowChange: Boolean);
begin
    ListView1.OnChanging := nil;
    try
        if Change = ctState then begin
            AllowChange := False;
            if NOT Item.Selected then begin
                Item.Selected := True;
            end else begin
                Item.Selected := False;
            end;
        end;
    finally
        ListView1.OnChanging := ListView1Changing;
    end;
end;

Solution

  • I did not give up that easily, the following code seems to work:

    • Toggleing works
    • SHIFT + click adds to selection
    • Editing item caption works
    • Cursor keys work (up/down), the code preserves previous selected state of the items

    Set MultiSelect to True, tested with ViewStyle = vsReport.

    var
      LastItem: TListItem;
      HotItem: TListItem;
      PreviousItemWasSelected: Boolean;
      ShiftDown: Boolean;
      CtrlDown: Boolean;
    
    procedure TForm1.ListView1Changing(Sender: TObject; Item: TListItem; Change: TItemChange; var AllowChange: Boolean);
    var
        LVChangingEvent: TLVChangingEvent;
    begin
        if CtrlDown then begin
            Exit;
        end;
        LVChangingEvent := (Sender as TListView).OnChanging;
        (Sender as TListView).OnChanging := nil;
        try
            if Change = ctState then begin
                if ShiftDown then begin
                    if Item.Selected then begin
                        AllowChange := False;
                        Exit;
                    end;
                end;
                if LastItem = Item then begin
                    LastItem := nil;
                    Exit;
                end;
                if Item.Focused then begin
                    Item.Selected := NOT Item.Selected;
                    LastItem := Item;
                    Exit;
                end;
                AllowChange := False;
                if NOT ShiftDown then begin
                    if HotItem <> Item then begin
                        Exit;
                    end;
                end;
                if NOT Item.Selected then begin
                    Item.Selected := True;
                    Item.Focused := True;
                    PreviousItemWasSelected := True;
                end else if Item.Selected then begin
                    Item.Selected := False;
                    PreviousItemWasSelected := False;
                end;
                LastItem := Item;
            end;
        finally
            (Sender as TListView).OnChanging := LVChangingEvent;
        end;
    end;
    
    procedure TForm1.ListView1KeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
    var
        ListView: TListView;
        LVChangingEvent: TLVChangingEvent;
        CurrentNextSelected: Boolean;
    begin
        if ssShift in Shift then begin
            ShiftDown := True;
        end;
        if ssCtrl in Shift then begin
            CtrlDown := True;
        end;
        ListView := Sender as TListView;
        if ListView.IsEditing then begin
            Exit;
        end;
        if Key = VK_DOWN then begin
            if NOT Assigned(ListView.ItemFocused) then begin
                Exit;
            end;
            if ListView.ItemFocused.Index = ListView.Items.Count - 1 then begin
                Exit;
            end;
            LVChangingEvent := ListView.OnChanging;
            ListView.OnChanging := nil;
            try
                CurrentNextSelected := ListView.Items[ListView.ItemFocused.Index + 1].Selected;
                ListView.ItemFocused := ListView.Items[ListView.ItemFocused.Index + 1];
                ListView.ItemFocused.Selected := True;
                ListView.Items[ListView.ItemFocused.Index - 1].Selected := PreviousItemWasSelected;
                PreviousItemWasSelected := CurrentNextSelected;
                Key := 0;
            finally
                ListView.OnChanging := LVChangingEvent;
            end;
        end;
        if Key = VK_UP then begin
            if NOT Assigned(ListView.ItemFocused) then begin
                Exit;
            end;
            if ListView.ItemFocused.Index = 0 then begin
                Exit;
            end;
            LVChangingEvent := ListView.OnChanging;
            ListView.OnChanging := nil;
            try
                CurrentNextSelected := ListView.Items[ListView.ItemFocused.Index - 1].Selected;
                ListView.ItemFocused := ListView.Items[ListView.ItemFocused.Index - 1];
                ListView.ItemFocused.Selected := True;
                ListView.Items[ListView.ItemFocused.Index + 1].Selected := PreviousItemWasSelected;
                PreviousItemWasSelected := CurrentNextSelected;
                Key := 0;
            finally
                ListView.OnChanging := LVChangingEvent;
            end;
        end;
    end;
    
    procedure TForm1.ListView1KeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
    begin
        ShiftDown := ssShift in Shift;
        CtrlDown := ssCtrl in Shift;
    end;
    
    procedure TForm1.ListView1MouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
    begin
        HotItem := (Sender as TListView).GetItemAt(X, Y);
    end;
    

    But it seems there is a little issue that there is some kind of delay, maybe for the edit caption functionality, that quickly toggleing the same item is not possible. To toggle a specific item, a couple of 100 ms is needed to wait before the toggle.

    If somebody has a nicer solution please share it! Thank you!