Get position of object in a list in Delphi? - delphi

I was wondering how you get a position of a certain object in a list that is created.
Lets say it is like a graphical list where you can click on objects.
Lets say you right click on a object and click "Refresh", how do I get the position of that object so that after the whole list is refreshed (refreshes with a clearlist for some reason) i go back to the same position in the list? This is if the list is say 1000 objects long which makes it bothersome to try and scroll down to the same position after the refresh.
The code uses Tobject but can i do something like
position:=integer(TObject."pointerinfo???");
And after that when the program refreshes
like set the position of the view to the pointer
like currentview(pointer) or something like that?
Actually it doesn't have to be the same object, but the same "view" of the list would be even better.
Thanks in advance

If refreshing is going to give you the same list again, and in the same order, then don't bother discovering anything about the object. Just store the list control's ItemIndex property, which indicates the currently selected item.
If refreshing might give you a different list, then the object you want might not be at the same position afterward, so just remembering ItemIndex won't be enough. In that case, you'll need to find the new position of the object. How to do that depends on the capabilities of the list control and how it exposes the objects associated with each position. If you have a TListBox, for example, then the Items property is a TStrings object, and you can inspect each value of the Objects array until you find the object you want. Store the value of the object reference, and then search for that. Something like this:
procedure Refresh;
var
CurrentSelection: TObject;
ObjectPosition: Integer;
i: Integer;
begin
if List.ItemIndex >= 0 then
CurrentSelection := List.Strings.Objects[List.ItemIndex]
else
CurrentSelection = nil;
RefreshList;
ObjectPosition := -1;
if Assigned(CurrentSelection) then begin
for i := 0 to Pred(List.Count) do begin
if List.Strings.Objects[i] = CurrentSelection then begin
ObjectPosition := i;
break;
end;
end;
end;
if ObjectPosition = -1 then
// Object isn't in refreshed list
else
// It is.
end;
A final possibility is that refreshing doesn't actually keep the same objects at all. All the previous objects are destroyed, and a new list of objects is generated. In that case, you'll have to remember certain identifying characteristics of the original object so you can find the new object that represents the same thing. Something like this:
var
CurrentObject, Person: TPerson;
CurrentName: string;
i, ObjectPosition: Integer;
begin
if List.ItemIndex >= 0 then begin
CurrentObject := List.Strings.Objects[List.ItemIndex] as TPerson;
CurrentName := CurrentObject.Name;
end else
CurrentName = '';
RefreshList;
ObjectPosition := -1;
if CurrentName <> '' then begin
for i := 0 to Pred(List.Count) do begin
Person := List.Strings.Objects[i] as TPerson;
if Person.Name = CurrentName then begin
ObjectPosition := i;
break;
end;
end;
end;
if ObjectPosition = -1 then
// Object isn't in refreshed list
else
// It is.
end;
In all these cases, you should realize that the object's position in the list is not actually a property of the object. Rather, it's a property of the list, which is why the list plays such a bigger role than the object in all that code. There isn't really any "pointerinfo" to get from the object because the object, in general, has no idea it's even in a list, so it certainly won't know its position relative to all the other items in the list.
Once you've determined the position of the object in the list, you need to scroll it into view. For TListBox, a simple way is to set its TopIndex property:
List.TopIndex := ObjectPosition;
And finally, if you don't actually care about the current object at all, but just want to restore the current scroll position, then that's even easier:
procedure Refresh;
var
CurrentPosition: Integer;
begin
CurrentPosition := List.TopIndex;
RefreshList;
List.TopIndex := CurrentPosition;
end;

So I think with the help I got I answered my own question.
What I did was write something that took the x and y position of the listview and later after I did the refresh with a clearlist, I used the scroll function to get back to the same function. My program looks something like this.
procedure Refresh(Sender: TObject);
var
horzpos:integer;
vertpos:integer;
begin
horzpos:=ListView1.ViewOrigin.X;
vertpos:=ListView1.ViewOrigin.Y;
RefreshListView(ListView1); //A function that refreshes the list and clears it
ListView1.Scroll(horzpos, vertpos);
end;
Maybe I should've stated earlier that it was a listview type and that I wanted to get back to the same position again after the "clearlist".
Thanks for all the help, this community rocks!

Related

TStringGrid hide column

procedure TFormMain.CaseListMyShares(uri: String);
var
i: Integer;
begin
myShares := obAkasShareApiAdapter.UserShares(uri);
MySharesGrid.RowCount:= Length(myShares) +1;
MySharesGrid.AddCheckBoxColumn(0, false);
MySharesGrid.AutoFitColumns;
for i := 0 to Length(myShares) -1 do
begin
MySharesGrid.Cells[1, i+1]:= myShares[i].patientCase;
MySharesGrid.Cells[2, i+1]:= myShares[i].sharedUser;
MySharesGrid.Cells[3, i+1]:= myShares[i].creationDate;
MySharesGrid.Cells[4, i+1]:= statusText[StrToInt(myShares[i].situation) -1];
end;
end;
I want to create an invisible column to myself. I have to know row's identifier to make some operations with REST API. But end-user does not need to see this identifier on table. How can i create an invisible column ?
myShares[i].identifier which i want to hide on every row. Is that possible ? Using TAG or anything ?
To assign the identifier:
MySharesGrid.Objects[0, i+1] := TObject(myShares[i].identifier)
To access the identifier (e.g. OnClick):
Integer(MySharesGrid.Objects[0, MySharesGrid.Row])
To answer the question, assign its ColWidths to -1, thusly:
StringGrid1.ColWidths[4] := -1;
You can show it again by setting that back to a positive value.
This is not about using a StringGrid to store data, but for the User Interface to show/hide columns - like a Collapse speedbutton in the title, with a corresponding Show All button to restore it.

delete all elements from objectlist except one selected element

What is best coding practise in deleting all elements from a list except one element which I would like to keep inside the list?
TMyCLass = Class()
....
end;
MyObjectList = TObjectList <TMyClass>;
var MySaveClass : TMyCLass;
begin
MySaveClass = MyObjectList.items[saveindex];
for i = 1... MyObjectList.count-1 do
if i <> saveindex then
MyObjectList.delete (i); ?? // not working :-)
end;
As TLama said in a comment, Extract the item, Clear the list (if OwnsObjects is false, loop through and free each item first), and then Add the item back in.
var
SavedItem: TMyClass;
...
SavedItem := MyObjectList.Extract(MyObjectList.Items[i]);
// Loop here to free if needed because list doesn't own objects
MyObjectList.Clear;
MyObjectList.Add(SavedItem);
If the Objectlist has OwnsObjects := True then you can just delete the elements.
But care to make it backwards otherwise you might get an error.
for i := MyObjectlist.count -1 downto 0 do
if i = Saveindex then
continue
else
MyObjectList.Delete(i);

Remove in a simple linked list node in pascal

I have made a piece of code that works in simple linked list node in pascal. Now I need add a procedure that can delete or remove an element in my simple linked list. I have made many procedures that do test and adding but I can't figure out how to add a procedure that can remove a item. Here is my code:
type
node = ^MyRec;
MyRec = record
value: Integer;
index: integer;
next: node
end;
var
head, tail: node;
i, ix: Integer;
const
el = 14;
procedure Insert;
var
tmp: node;
ix: Integer;
begin
Randomize;
for i := 1 to el do
begin
new (tmp);
inc(ix);
tmp^.value := Random(88);
tmp^.index := ix;
tmp^.next:= nil;
if head = nil
then head := tmp
else
tail^.next := tmp;
tail:= tmp;
end;
end;
//displays the list
procedure Display;
var
tmp: node;
begin
tmp := head;
while tmp <> nil do
begin
WriteLn('Val=', tmp^.value, ' ID=', tmp^.index);
tmp := tmp^.next
end;
end;
begin
head := nil;
ReadLn;
end.
I'm going to give you some information and pseudocode so you will be able to fix this problem yourself.
You can see a linked list as a chain of items:
Head -> Item1 -> Item2 -> Item3 -> Item4 <- Tail
If you need to delete Item3, you need to change the next pointer of Item2 so it points to Item4 (The item following Item3). Then you can release Item3.
But there are some special cases:
If you want to delete Item1 (the first item), you need to change the Head pointer, so it points to Item2.
If you want to delete Item4 (the last item), you need to change the Tail pointer, so it points to Item3.
If the list contains only one item and you want to delete that, you need to change both the Head and the Tail to nil.
If the list is empty, you don't need to delete anything, but your code should not crash because of that.
So in order to fix all these, you can use 2 pointers to walk the list, (current and previous).
Current start at the first item (Head) and previous start at nil.
As long as current is not nil and it does not point to the item to be deleted, set previous to the value of current and set current to the value of the next item.
If current is nil, then the item is not found and there is nothing to delete.
If current points to the correct item, and previous is nil, you need to change the head pointer.
If current points to the correct item and previous is not nil, set the next pointer of previous to the next pointer of current.
If the next pointer of current is nil (you are at the tail), you also set the tail pointer to previous.
If all pointers are changed, you can dispose of the current item.
A side note on your code. You have an insert procedure that inserts several random elements. A better idea is to have a separate procedure to insert a single element and a separate procedure to add several items like:
procedure Insert(index, value: Integer);
var
tmp: node;
begin
new (tmp);
tmp^.value := value;
tmp^.index := index;
tmp^.next:= nil;
if head = nil
then head := tmp
else
tail^.next := tmp;
tail:= tmp;
end;
procedure FillList;
var
ix: Integer;
begin
Randomize;
for i := 1 to el do
begin
inc(ix);
Insert(ix, Random(88));
end;
end;

Dynamic TForm creation Idiosyncrasies(bugs?) in Delphi XE5

This one is driving me up a wall. Most of the conversion from Delphi 6 to XE5 is proceeding smoothly, but I have various routines to dynamically build various TForm descendents (NO DFM), pop it up and generally return a value. I have a number of them that work fine in D6. Generally, I choose a place I want to pop something up (like over a panel), and what I want to popup (editbox, memo, listbox...). I create the form, set initial values and call showmodal and return some result.
The same code, compiled in XE5 has execution (glitches). One is that the created form accepts left,top and such, but does NOT display itself there. The values are correctly in the properties, but the form is in the wrong place. A second, probably related (glitch) is that when I create a TMemo or TListbox and store some text in it, "ShowModal" displays the data properly, but "Show" does not.
It has taken me several hours to digest the problem down to its simplest form, removing virtual all of my personal code. AS SHOWN HERE, IT WORKS PERFECTLY
If I comment out this line, it does not work - the form is displayed in the wrong place
XX.ClientToScreen(Point(0,0)); // EXTREMELY WEIRD PATCH
This line is a function call which OUGHT NOT affect anything else, and I don't use the returned value.
The commented out "Show" line demonstrates the other problem (data not being displayed).
I have tried Application.ProcessMessages in all sorts of places, but it never makes things better, and at times make things worse.
Color me "puzzled".
//-----------------------------------------------------------------------------------------
//-----------------------------------------------------------------------------------------
type TMemoForm = class(TForm)
private
public
XMemo : TMemo;
end;
Function PopUpMemoStr(txt : AnsiString; x : integer = 200; y : integer = 200; w : integer = 400 ; h : integer = 400 ) : AnsiString; // more or less a dummy for testing on XE5 2/28/14
var XX : TMemoForm;
begin
XX := TMemoForm.CreateNew(Application);
XX.ClientToScreen(Point(0,0)); // *** EXTREMELY WEIRD FIX ***
XX.Left := X; XX.Top := Y; XX.Width := w; XX.height := h;
XX.caption := 'Dummy PopUpMemo';
XX.XMemo := TMemo.create(XX);
XX.XMemo.parent := XX;
XX.XMemo.align := alClient;
XX.XMemo.text := txt;
//logit('PopUpMemoStr R='+TRectToStr(MyGetScreenRect(XX)));
XX.showmodal;
//XX.show; delay(3.00); // other "no data" problem
XX.free;
end;
//exercise code -- Panel2 is just a visible spot to see if positioning works correctly
var s : AnsiString;
var R : TRect;
begin
//R := MyGetScreenRect(Panel2);
R := Rect(414,514,678,642); // just a useful screen location for testing
s := 'One'+CRLF+'Two'+CRLF+'Three'+CRLF+'Four'; // "CRLF is #13#10
PopUpMemoStr(s,R.Left,R.Top,R.Right-R.Left,R.Bottom-R.Top);
//-----------------------------------------------------------------------------------------
//-----------------------------------------------------------------------------------------
To fix the form positioning problem, you need to set the form's Position to poDesigned.
For your second problem, you can't delay like that. You are not giving the Form a chance to process messages. Changing it to something like the code below displays the data correctly (although you really should not be doing this sort of thing either):
begin
XX := TMemoForm.CreateNew(nil);
try
XX.Position := poDesigned; // This line needs to be added for the positioning
XX.SetBounds(X, Y, w, h);
XX.Caption := 'Dummy PopUpMemo';
XX.XMemo := TMemo.Create(XX);
XX.XMemo.Parent := XX;
XX.XMemo.Align := alClient;
XX.XMemo.Text := txt;
//logit('PopUpMemoStr R='+TRectToStr(MyGetScreenRect(XX)));
// XX.ShowModal;
// This displays the data correctly but is not advisable
XX.Show;
for I := 1 to 6 do
begin
Sleep(500);
Application.ProcessMessages;
end;
finally
XX.Free;
end;
end;
If you want to use Show() for a Form like that, you should use the Form's OnClose event and set its Action parameter to caFree and just do the Show() in your code. Put a timer on the Form for x seconds and Close() it when the timer finishes. A bit like this:
type
TMemoForm = class(TForm)
public
XMemo : TMemo;
XTimer: TTimer;
procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure TimerElapsed(Sender: TObject);
end;
procedure TMemoForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
Action := caFree;
end;
procedure TMemoForm.TimerElapsed(Sender: TObject);
begin
Close;
end;
begin
XX := TMemoForm.CreateNew(nil);
try
XX.Position := poDesigned; // This line needs to be added for the positioning
XX.SetBounds(X, Y, w, h);
XX.Caption := 'Dummy PopUpMemo';
XX.OnClose := XX.FormClose;
XX.XMemo := TMemo.Create(XX);
XX.XMemo.Parent := XX;
XX.XMemo.Align := alClient;
XX.XMemo.Text := txt;
XX.XTimer := TTimer.Create(XX);
XX.XTimer.Interval := 3000;
XX.XTimer.OnTimer := XX.TimerElapsed;
XX.Active := True;
XX.Show; // Just show the form. The rest is in the Form itself.
except
XX.Free;
raise;
end;
end;
Your extremely weird patch, calling ClientToScreen on the newly created form, should fix the issue as it does, even if you don't use the point that's returned.
In the case when you don't use it, when you set your form's bounds, since the window of the form has not yet been created, the VCL keeps this information to be later passed to the API when the window is about to be shown. But this information will be discarded since VCL also tells the API to use default window position because of the poDefaultPosOnly setting of Position property.
In the case when you use it, to be able to determine the position of the form in the screen the VCL first creates the window of the form. Hence when you later set the bounds of the form, they are actually implemented through SetWindowPos.
As such, if you've used
XX.HandleNeeded;
instead of
XX.ClientToScreen(Point(0,0));
it would be a more direct workaround.
Of course the correct solution is in Graymatter's answer.
I cannot comment on Show not displaying data, the code you posted in the question should not exhibit that kind of behavior.

How to free ObjectList without freeing contents

I have an ObjectList that is populated. Then details changed in the objects. Now I need to free the ObjectList but when I do it also frees the objects in the list. How can I free this list without freeing the objects themselfs?
Example code:
{Gets starting cards and put them into the correct rows}
//***************************************************************************
procedure TFGame.GetStartingCards;
//***************************************************************************
const
ManaTypes : array [0..3] of string = ('Lava','Water','Dark','Nature');
var
i: integer;
z:integer;
Cards: TObjectList<Tcard>;
begin
Cards := TObjectList<TCard>.Create;
z:=0;
{add all tcards (Desgin ) to this list in order Lava,water,dark,nature }
cards.Add(cardLava1);
cards.Add(cardlava2);
cards.Add(cardlava3);
cards.Add(cardlava4);
cards.Add(cardwater1);
cards.Add(cardwater2);
cards.Add(cardwater3);
cards.Add(cardwater4);
cards.Add(carddark1);
cards.Add(carddark2);
cards.Add(carddark3);
cards.Add(carddark4);
cards.Add(cardnature1);
cards.Add(cardnature2);
cards.Add(cardnature3);
cards.Add(cardnature4);
//get data from DB
for i := 0 to Length(ManaTypes) - 1 do
begin
with adoquery1 do
begin
close;
sql.Clear;
sql.Add('SELECT TOP 4 * FROM Cards WHERE Color = "'+ManaTypes[i]+'" ORDER BY Rnd(-(1000*ID)*Time())');
open;
end;
{return the result of everything for giving mana type.. }
if adoquery1.RecordCount = 0 then
Showmessage('Error no cards in db');
adoquery1.First;
while not adoquery1.Eof do
begin
cards[z].Cname := adoquery1.FieldByName('Name').AsString;
cards[z].Ccost := adoquery1.Fieldbyname('Cost').AsInteger;
cards[z].Ctext := adoquery1.FieldByName('Text').AsString;
cards[z].Ccolor := adoquery1.FieldByName('Color').AsString;
cards[z].Cinplay := false; //in the play area
if adoquery1.fieldbyname('Power').asstring <> '' then
cards[z].Cpower := adoquery1.FieldByName('Power').AsInteger;
if adoquery1.fieldbyname('Def').asstring <> '' then
cards[z].Cdef := adoquery1.FieldByName('Def').AsInteger;
if adoquery1.FieldByName('Type').AsString = 'Spell' then
cards[z].Cspell := true
else
cards[z].Cspell := false;
if adoquery1.FieldByName('Target').AsString = 'yes' then
cards[z].SetTargetTrue
else
cards[z].settargetfalse;
//based on color change background
cards[z].Background.LoadFromFile(format('%s\pics\%s.png',[maindir,cards[z].Ccolor]));
adoquery1.Next;
cards[z].repaint;
z:=z+1;
end;
end;
//cards.Free; if i free this it removes all the cards added to it..
end;
The constructor for TObjectList<T> receives a parameter named AOwnsObjects which specifies who is to be responsible for freeing the objects. If you pass True then the list has that responsibility. Otherwise, the responsibility remains with you.
constructor Create(AOwnsObjects: Boolean = True); overload;
The parameter has a default value of True and I presume that you are calling the constructor without specifying that parameter. Hence you get the default behaviour – the list owns its members.
The documentation says it like this:
The AOwnsObjects parameter is a boolean that indicates whether object entries are owned by the list. If the object is owned, when the entry is removed from the list, the object is freed. The OwnsObjects property is set from the value of this parameter. The default is true.
Now, if you want this particular list instance not to free its members, then there is little to be gained from using TObjectList<T>. You may as well use plain old TList<T>.

Resources