4

I am trying to implement MoveItemUp and MoveItemDown methods that move a selected row up or down one index within a TCollection.

The following code added to my subclass of TCollection does not work:

procedure TMyCollection.MoveRowDown(index: Integer);
var
 item:TCollectionItem;
begin
  if index>=Count-1 then exit;
  item := Self.Items[index];
  Self.Delete(index); // whoops this destroys the item above.
  Self.Insert(index+1);
  Self.SetItem(index+1,item); // this actually does an assign from a destroyed object.
end;

I am fairly sure this must be possible at runtime, as its done in designtime by the Delphi IDE itself which provides a way to reorder Collection items in a list. I am hoping to do this by simply reordering existing objects, without creating, destroying, or Assigning any objects. Is this possible from a subclass of Classes.pas TCollection? (If not, I may have to make my own TCollection from a source clone)

menjaraz
  • 7,551
  • 4
  • 41
  • 81
Warren P
  • 65,725
  • 40
  • 181
  • 316
  • 10
    Setting the `Index` property of the collection item should do `Item.Index:=Item.Index+1` (this calls the `Move` of the items list of the collection). If any special handling is necessary the `SetIndex` method is to be overriden. – Sertac Akyuz Nov 28 '11 at 18:29

3 Answers3

10

According to the VCL source, you don't need to manually do that. Simply set the Index property like @Sertac suggested and it should work just fine. If you have the source, check out the code of TCollectionItem.SetIndex.

Pateman
  • 2,727
  • 3
  • 28
  • 43
4

You can use something like this - declare a dummy class type for a collection, and use it to gain access to the internal FItems of that collection, which is a TList. You can then use the TList.Exchange method to handle the actual move (or any other functionality of the TList, of course).

type
  {$HINTS OFF}
  TCollectionHack = class(TPersistent)
  private
    FItemClass: TCollectionItemClass;
    FItems: TList;
  end;
  {$HINTS ON}

// In a method of your collection itself (eg., MoveItem or SwapItems or whatever)
var
  TempList: TList;
begin
  TempList := TCollectionHack(Self).FItems;
  TempList.Exchange(Index1, Index2);
end;
Ken White
  • 123,280
  • 14
  • 225
  • 444
  • Oh this isn't so much Subclassing as cracking the private member using my own hopefully correctly ordered class? – Warren P Nov 28 '11 at 18:40
  • Yes. That's why I mentioned it as something of a hack. I had trouble using `Index` when trying to sort a collection's items, and exchange isn't implemented. – Ken White Nov 28 '11 at 18:55
  • Moving an item up/down one position (which I'm trying to do) seems to work fine by assignment of TcollectionItem.Index. – Warren P Nov 28 '11 at 18:59
  • 1
    Right, but it doesn't if you're wanting to do any more than that, and earlier versions of Delphi had issues with changing the Index many times (IIRC, D2007 did, but I'd have to check - it may have been earlier even). No matter, though. It's an alternative in case you need to do more than a single item move. – Ken White Nov 28 '11 at 19:04
0

Here is a class helper solution that sorts by DisplayName: You can improve the sort if you like, I used a TStringList to do my sorting for me. The Class helper is available anywhere you reference the unit containing the class helper, so if you have a utility unit put it there.

interface

  TCollectionHelper = class helper for TCollection    
  public    
    procedure SortByDisplayName;    
  end;

Implementation

procedure TCollectionHelper.SortByDisplayName;    
var i, Limit : integer;    
    SL: TStringList;    
begin    
  SL:= TStringList.Create;    
  try    
    for i := self.Count-1 downto 0 do    
      SL.AddObject(Items[i].DisplayName, Pointer(Items[i].ID));    
    SL.Sort;    
    Limit := SL.Count-1;    
    for i := 0 to Limit do    
      self.FindItemID(Integer(SL.Objects[i])).Index := i;    
  finally    
    SL.Free;    
  end;    
end;

Then to use the method simply pretend it is a method of the TCollection class. This works on any subclass of TCollection as well.

MyCollection.SortByDisplayName or MyCollectionItem.Collection.SortByDisplayName.

LU RD
  • 34,438
  • 5
  • 88
  • 296
  • Did Embarcadero finally fix the problem where only one class helper can be in scope ? If not, then this only works if there is no existing helper in scope for TCollection and will break in future if some other helper is introduced into scope. – Deltics Oct 02 '14 at 22:36
  • @Deltics, the scope of class helpers has not changed. There is a possibility to subclass the helpers, but then you have to be in control of the whole chain. Other possibilities are hacking like Ken shows, and making a global procedure that performs this on a collection or gather more "helper" rutines for a collection inside a class/record. – LU RD Oct 03 '14 at 11:44
  • **Why not just**: `for i:=0 to Self.Count -1 do SL.AddObject(Items[i].DisplayName), Items[i]); SL.Sort; for i:=0 to SL.Count -1 do TCollectionItem(SL.Objects[i]).Index := i;` – Vassilis Jun 12 '15 at 16:35