TComboBox and TListBox item Removal? - delphi

i am trying to add items to TListBox and TComboBox from text given in TEdit
My code is working fine while adding item in TListBox TComboBox but when i try to remove the selected item in TListBox from itself and from TComobBox it shows Access Violation.
Below is procedure from my code:-
procedure TMainForm.Button1Click(Sender: TObject);
begin
ListBox1.Items.Add(Edit1.Text);
ComboBox1.Items.Add(Edit1.Text);
end;
procedure TMainForm.Button2Click(Sender: TObject);
begin
ListBox1.Items.Delete(ListBox1.Selected.Index);
ComboBox1.Items.Delete(ComboBox1.Items.IndexOf(ListBox1.Selected.Text));
end;
Solved: Did a Kiddish Mistake now Solved. Here it is working code:
procedure TMainForm.Button2Click(Sender: TObject);
begin
ComboBox1.Items.Delete(ComboBox1.Items.IndexOf(ListBox1.Selected.Text));
ListBox1.Items.Delete(ListBox1.Selected.Index);
end;

A safe(r) way to do the deletions is
procedure TForm1.DeleteItems(const TextToFind : String);
var
i1,
i2 : Integer;
begin
i1 := ListBox1.Items.IndexOf(TextToFind);
i2 := ComboBox1.Items.IndexOf(TextToFind);
if i1 >=0 then
ListBox1.Items.Delete(i1);
if i2 >=0 then
ComboBox1.Items.Delete(i2);
end;
Usage:
DeleteItems(Edit1.Text);
because this does not make assumptions about which items are selected in the two lists.
I leave you to find out using the debugger why you are getting the AV. It will be more instructive for you to find out than me to tell you.

This line removes the item from the listbox
ListBox1.Items.Delete(ListBox1.Selected.Index);
This line is trying to remove the item from the combobox
ComboBox1.Items.Delete(ComboBox1.Items.IndexOf(ListBox1.Selected.Text));
But in it you refer to ListBox1.Selected.Text.
This is referring the item you just removed in the first delete.
Swapping the order of execution around should work:
begin
ComboBox1.Items.Delete(ComboBox1.Items.IndexOf(ListBox1.Selected.Text));
ListBox1.Items.Delete(ListBox1.Selected.Index);
end

procedure TMainForm.Button2Click(Sender: TObject);
begin
if ListBox1.Selected.Index > -1 then ListBox1.Items.Delete(ListBox1.Selected.Index);
if ComboBox1.ItemIndex > - 1 then ComboBox1.Items.Delete(ComboBox1.ItemIndex);
end;

Related

Delphi 11 - TTreeView - How set Checkbox value for items in a node

I would like to set all Checkboxes in a TTreeNode.
Is there a simpler way to do this, or is there a "Best Practices" way to do it?
For example, TreeNode.SetAllCheckboxes:=true;
In my example below, I can set all checkboxes to true.
procedure TForm1.TreeView1DblClick(Sender: TObject);
begin
TreeViewCheckAllNodes(TreeView1.Selected);
end;
procedure TForm1.TreeViewCheckAllNodes(treeNode:TTreeNode);
var
loop:integer;
check:boolean;
begin
//Check or Uncheck
check:=true;
if treeNode.Checked=True then check:=false;
treeNode.Checked:=check;
for loop:=0 to treeNode.Count-1 do
begin
treeNode[loop].Checked:=check;
end;
treeNode.Expand(true);
end;
you can try to use a recursive call in your procedure CheckAllNodes.
This will check or uncheck all the way down regardless if you have more than one level below the selected node or different number of levels to the nodes below the selected.
procedure TForm1.CheckAllNodes(treeNode:TTreeNode; Check: boolean);
begin
while (treeNode<> nil) do
begin
treeNode.Checked := Check;
CheckAllNodes(treeNode.getFirstChild, Check);
if treeNode<> Form1.TreeView1.Selected then
treeNode:= treeNode.getNextSibling
else
treeNode:= nil;
end;
end;
procedure TForm1.TreeView1DblClick(Sender: TObject);
begin
CheckAllNodes(TreeView1.Selected, not TreeView1.Selected.Checked);
end;

Hiding items in TListBox while filtering by String

Short Version: Is there any way to control or modify LisBox items individually? for example set their Visible property to False separately.
I found a TListBoxItem class in Fire Monkey when I was searching, but I don't want to use Fire Monkey and want it in VCL.
Detailed Version:
I tried to filter my ListBox using two TStringList and an Edit, one StringList is global to keep the original list (list_files_global) and another StringList to help filtering procedure (list_files_filter) and my primary list of files is my ListBox (list_files).
I created my global StringList on onCreate event while program is starting to store my original list:
procedure Tfrm_main.FormCreate(Sender: TObject);
Begin
list_files_global := TStringList.Create;
list_files_global.Assign(list_files.Items);
End;
and used Edit's onChange event for filtering:
procedure Tfrm_main.edit_files_filterChange(Sender: TObject);
Var
list_files_filter: TStringList;
i: Integer;
Begin
list_files_filter := TStringList.Create;
list_files_filter.Assign(list_files.Items);
list_files.Clear;
for i := 0 to list_files_filter.Count - 1 do
if pos(edit_files_filter.text, list_files_filter[i]) > 0 then
list_files.Items.Add(list_files_filter[i]);
End;
and for switching off the filter, just recover the list from my global list that I created at first:
list_files.Items := list_files_global;
here so far, everything works just fine, but problem is when I'm trying to edit/rename/delete items from filtered list, for example I change an item:
list_files.Items[i] := '-- Changed Item --';
list will be edited, but when I switch off the filter, the original list will be back and all changes are lost.
so I want to know is there any proper way to solve this problem? Something like hiding items individually or change items visibility, etc... so I can change the filtering algorithm and get rid of all this making extra lists.
I searched the internet and looked into Delphi's help file for a whole day and nothing useful came up.
The items of a VCL listbox, List Box in the API, does not have any visibility property. The only option for not showing an item is to delete it.
You can use the control in virtual mode however, where there are no items at all. You decide what data to keep, what to display. That's LBS_NODATA window style in the API. In VCL, set the style property to lbVirtual.
Extremely simplified example follows.
Let's keep an array of records, one record per virtual item.
type
TListItem = record
FileName: string;
Visible: Boolean;
end;
TListItems = array of TListItem;
You can extend the fields as per your requirements. Visibility is one of the main concerns in the question, I added that. You'd probably add something that represents the original name so that you know what name have been changed, etc..
Have one array per listbox. This example contains one listbox.
var
ListItems: TListItems;
Better make it a field though, this is for demonstration only.
Required units.
uses
ioutils, types;
Some initialization at form creation. Empty the filter edit. Set listbox style accordingly. Fill up some file names. All items will be visible at startup.
procedure TForm1.FormCreate(Sender: TObject);
var
ListFiles: TStringDynArray;
i: Integer;
begin
ListFiles := ioutils.TDirectory.GetFiles(TDirectory.GetCurrentDirectory);
SetLength(ListItems, Length(ListFiles));
for i := 0 to High(ListItems) do begin
ListItems[i].FileName := ListFiles[i];
ListItems[i].Visible := True;
end;
ListBox1.Style := lbVirtual;
ListBox1.Count := Length(ListFiles);
Edit1.Text := '';
end;
In virtual mode the listbox is only interested in the Count property. That will arrange how many items will show, accordingly the scrollable area.
Here's the filter part, this is case sensitive.
procedure TForm1.Edit1Change(Sender: TObject);
var
Text: string;
Cnt: Integer;
i: Integer;
begin
Text := Edit1.Text;
if Text = '' then begin
for i := 0 to High(ListItems) do
ListItems[i].Visible := True;
Cnt := Length(ListItems);
end else begin
Cnt := 0;
for i := 0 to High(ListItems) do begin
ListItems[i].Visible := Pos(Text, ListItems[i].FileName) > 0;
if ListItems[i].Visible then
Inc(Cnt);
end;
end;
ListBox1.Count := Cnt;
end;
The special case in the edit's OnChange is that when the text is empty. Then all items will show. Otherwise code is from the question. Here we also keep the total number of visible items, so that we can update the listbox accordingly.
Now the only interesting part, listbox demands data.
procedure TForm1.ListBox1Data(Control: TWinControl; Index: Integer;
var Data: string);
var
VisibleIndex: Integer;
i: Integer;
begin
VisibleIndex := -1;
for i := 0 to High(ListItems) do begin
if ListItems[i].Visible then
Inc(VisibleIndex);
if VisibleIndex = Index then begin
Data := ListItems[i].FileName;
Break;
end;
end;
end;
What happens here is that the listbox requires an item to show providing its index. We loop through the master list counting visible items to find out which one matches that index, and supply its text.
This is something I often do, but with list views instead of list boxes. The basic principles are the same, though.
I tend to store the individual items as objects, which are reference types in Delphi. And I keep them all in one main unfiltered list, which owns the objects, while I maintain a filtered list (which does not own the objects) for display purposes. Like #Sertac, I combine this with a virtual list view.
To see how this works in practice, create a new VCL application and drop a list view (lvDisplay) and an edit control (eFilter) on the main form:
Notice I have added three columns to the list view control: "Name", "Age", and "Colour". I also make it virtual (OwnerData = True).
Now define the class for the individual data items:
type
TDogInfo = class
Name: string;
Age: Integer;
Color: string;
constructor Create(const AName: string; AAge: Integer; const AColor: string);
function Matches(const AText: string): Boolean;
end;
where
{ TDogInfo }
constructor TDogInfo.Create(const AName: string; AAge: Integer;
const AColor: string);
begin
Name := AName;
Age := AAge;
Color := AColor;
end;
function TDogInfo.Matches(const AText: string): Boolean;
begin
Result := ContainsText(Name, AText) or ContainsText(Age.ToString, AText) or
ContainsText(Color, AText);
end;
And let us create the unfiltered list of dogs:
TForm1 = class(TForm)
eFilter: TEdit;
lvDisplay: TListView;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
FList, FFilteredList: TObjectList<TDogInfo>;
public
end;
where
function GetRandomDogName: string;
const
DogNames: array[0..5] of string = ('Buster', 'Fido', 'Pluto', 'Spot', 'Bill', 'Rover');
begin
Result := DogNames[Random(Length(DogNames))];
end;
function GetRandomDogColor: string;
const
DogColors: array[0..2] of string = ('Brown', 'Grey', 'Black');
begin
Result := DogColors[Random(Length(DogColors))];
end;
procedure TForm1.FormCreate(Sender: TObject);
var
i: Integer;
begin
FList := TObjectList<TDogInfo>.Create(True); // Owns the objects
// Populate with sample data
for i := 1 to 1000 do
FList.Add(
TDogInfo.Create(GetRandomDogName, Random(15), GetRandomDogColor)
);
FFilteredList := FList;
lvDisplay.Items.Count := FFilteredList.Count;
lvDisplay.Invalidate;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
if FFilteredList <> FList then
FreeAndNil(FFilteredList);
FreeAndNil(FList);
end;
The idea is that the list view control always displays the FFilteredList, which either points to the same object instance as FList, or points to a filtered (or sorted) version of it:
// The list view's OnData event handler
procedure TForm1.lvDisplayData(Sender: TObject; Item: TListItem);
begin
if FFilteredList = nil then
Exit;
if not InRange(Item.Index, 0, FFilteredList.Count - 1) then
Exit;
Item.Caption := FFilteredList[Item.Index].Name;
Item.SubItems.Add(FFilteredList[Item.Index].Age.ToString);
Item.SubItems.Add(FFilteredList[Item.Index].Color);
end;
// The edit control's OnChange handler
procedure TForm1.eFilterChange(Sender: TObject);
var
i: Integer;
begin
if string(eFilter.Text).IsEmpty then // no filter, display all items
begin
if FFilteredList <> FList then
begin
FreeAndNil(FFilteredList);
FFilteredList := FList;
end;
end
else
begin
if (FFilteredList = nil) or (FFilteredList = FList) then
FFilteredList := TObjectList<TDogInfo>.Create(False); // doesn't own the objects
FFilteredList.Clear;
for i := 0 to FList.Count - 1 do
if FList[i].Matches(eFilter.Text) then
FFilteredList.Add(FList[i]);
end;
lvDisplay.Items.Count := FFilteredList.Count;
lvDisplay.Invalidate;
end;
The result:
Notice that there always is only one in-memory object for each dog, so if you rename a dog, the changes will reflect in the list view, filtered or not. (But don't forget to invalidate it!)

Select component created at runtime

What I Want to do:
I create on the run a panel, 2 editfield, a radiobox.
When I Click on the RadioButton Edit1 I want to have the Edit1 field visible, and Edit2 filed Not visible
and reverse by the click on the RadioButton Edit2.
But it doesn't work I probably misunderstand something.
Do you have any idea?
procedure CreationPanel(L,T,H,W:integer;Nom,Titre:string
{MyPanel:TPanel});
begin
MyPanel:=TPanel.Create(Form1);
with MyPanel do
begin
Height:=H;
Left:=L;
Width:=W;
Top:=T;
BorderStyle:=bsSingle;
BevelWidth:=3;
BevelOuter:=bvRaised;
Parent:=Form1;
Visible:=True;
TabOrder:=-1;
TabStop:=False;
Tag:=100;
Name:=Nom;
Caption:=Nom;
end;
end;
procedure Creation_Edit(L,T,FontSize:integer;
NomComp,NomPanel:String;
Bool:Boolean
{MyPanel:Tpanel});
begin
MyEdit:=TEdit.Create(MyPanel);
with MyEdit do
begin
Parent:=MyPanel;
{Relation Table Dossier avec Table Bezeichnung}
Height :=21 ;
Left :=L ;
Top :=T ;
Width :=100;
Name:=NomComp;
Font.Name:='MS Sans Serif';
Font.Size:=FontSize;
Visible:=Bool;
end;
end;
procedure Creation_RadioGroup(L,T,H,W,FontSize:integer;
RGName,NomPanel:string
{MyPanelRG:TPanel});
begin
MyRadioGroup:=TRadioGroup.Create(MyPanel);
with MyRadioGroup do
begin
Parent:=MyPanel;
Height :=H ;
Left :=L ;
Top :=T ;
Width :=W;
Font.Name:='MS Sans Serif';
Name:=RGName;
Font.Size:=FontSize;
Items.Add('Edit1);
Items.Add('Edit2');
ItemIndex:=0;
OnClick:=Form1.RadioGroupClick;
end;
end;
procedure TForm1.RadioGroupClick(Sender: TObject);
var
E1,E2:TEdit;
begin {1}
E1:=TEdit(FindComponent('Edit1'));
E2:=TEdit(FindComponent('Edit2'));
E1:=TEdit.Create(MyPanel);
E2:=TEdit.Create(MyPanel);
if E1=nil then showmessage('E1=Nil');
if E2=nil then showmessage('E2=Nil');
If MyRadioGroup.Name='RD1' then {with TEDIT}
begin {If}
If Assigned(E1) and Assigned(E2) then
begin {2}
Case MyRadioGroup.ItemIndex of
0: begin
E1.Visible:=true;
E2.Visible:=False;
end;
1:begin
E1.Visible:=False;
E2.Visible:=true;
end;
end;{Case}
end;{2}
end;{If}
end;{1}
procedure TForm1.FormCreate(Sender: TObject);
begin
{Pannel Left,Top,Hieght,Width}
CreationPanel(80,100,300,180,'Panel1','Panel 1');
Creation_Edit(30,184,10,'Edit1','Panel1',true);
Creation_Edit(30,234,10,'Edit2','Panel1',false);
{RadioGroup Left,Top,Height,Width,FontSize,Tab}
Creation_RadioGroup(30,12,90,120,12,'RD1','Panel1');
end;
end.
OK your code is a complete mess, but I can tell you what your underlying problem is (i.e. the one that caused you to ask the question.)
Firstly Nasreddine is right about removing
E1:=TEdit.Create(MyPanel);
E2:=TEdit.Create(MyPanel);
That is a complete floundering (try anything) attempt because E1 and E2 return nil.
You should not be trying to stop them being nil. Instead you should ask yourself why they are nil.
And the answer to that is that Edit1 and Edit2 are not owned by Form1. They are owned by MyPanel.
MyEdit:=TEdit.Create(MyPanel);
So your code should read
procedure TForm1.RadioGroupClick(Sender: TObject);
var
E1,E2:TEdit;
begin {1}
E1:=TEdit( MyPanel.FindComponent('Edit1'));
E2:=TEdit(MyPanel.FindComponent('Edit2'));
if E1=nil then showmessage('E1=Nil');
if E2=nil then showmessage('E2=Nil');
//etc...
end;{1}

Delphi - How to delete all child components at runtime?

At design time, I create a TScrollBox which will be the parent of TLayouts created at runtime.
The Layouts will also contain Tlabels and Tedits like this:
var
Layout1: TLayout;
Label1: TLabel;
Edit1: TEdit;
begin
Layout1 := TLayout.Create(self);
Layout1.Parent := ScrollBox1;
Label1 := TLabel.Create(self);
Label1.Parent := Layout1;
Label1.Text := 'abc';
end;
Now I want to delete everything out like this procedure has never been called.
I have tried the following, but the program would just crash.
var
i : integer;
Item : TControl;
begin
for i := 0 to Scrollbox1.ControlCount - 1 do
begin
Item := Scrollbox1.controls[i];
Item.Free;
end;
end;
Can anyone please give me a hint?
When you remove a control, the index of the ones behind it in the control list shifts down. I.e, you end up trying to access positions that do not exist.
You need to iterate the list downwards:
var
i : integer;
Item : TControl;
begin
for i := (Scrollbox1.ControlCount - 1) downto 0 do
begin
Item := Scrollbox1.controls[i];
Item.Free;
end;
end;
Another way is to stay always at index 0, free its control and check that you still have controls to free:
var
i : integer;
Item : TControl;
begin
while Scrollbox1.ControlCount > 0 do
begin
Item := Scrollbox1.controls[0];
Item.Free;
end;
end;
UPDATE
As #DavidHeffernan pointed out, there is nested parentage here. This means you should free your components from bottom up. One way to do it is by recursion.
Basically you would need a procedure to encapsulate the freeing of child controls. The code would be similar to following (please note this is just a small test I did and extra code may be required):
procedure freeChildControls(myControl : TControl; freeThisControl: boolean);
var
i : integer;
Item : TControl;
begin
if Assigned(myControl) then
begin
for i := (myControl.ControlsCount - 1) downto 0 do
begin
Item := myControl.controls[i];
if assigned(item) then
freeChildControls(item, childShouldBeRemoved(item));
end;
if freeThisControl then
FreeAndNil(myControl);
end;
end;
function childShouldBeRemoved(child: TControl): boolean;
begin
//consider whatever conditions you need
//in my test I just checked for the child's name to be layout1 or label1
Result := ...;
end;
In order to free the scrollbox1 child controls (but not itself) you would call it like this:
freeChildControls(scrollbox1, false);
Please note that I had to add the childShouldBeRemoved function in order to avoid this recursive function to free child controls of the label and layout that you should leave for their destructors to free.
One possible solution to implement this function would be to use an object list where you would add your created components, and then inside the function check if the passed child component has to be freed.
If you create components at runtime - use parent control as parameter of the constructor. Like Label1 := TLabel.Create(Layout1); - so that the parent is also the owner. When you destroy Layout1 the Label1 also will be destroyed.

Delphi Dyncamically created Popup Menu Items - Invalid Pointer Operation/Access Violation

I have a problem with error message "Invalid Floating Point operation." The popup menu is a design time control and it is named NavPop. It has no menu items assigned. It is assigned as popupmenu for Panel1.
I then create the menu items dynamically from a listbox, and assign the caption and on click events. Everything works 100% in terms of what I am trying to accomplish. Ie it works.
Only when i close the program, do I get
Invalid floating point operation
or otherwise:
Access Violation Address 000007355. Read of Addrss 0000007355.
Please note that everything works perfectly, except that the error when I close the program. I would appreciate any help.
// I declare the Array of TMenuItems
private
{ Private declarations }
ItemArray : array of TMenuItem;
...
procedure TMainForm.Button1Click(Sender: TObject);
begin
CreateNavPop;
end;
// Create the menu items from listbox(Navlist) items and Link them
// to events on a navigation bar.
procedure TMainForm.CreateNavPop;
var
I: Integer;
NavIndex: Integer;
begin
SetLength(ItemArray, NavList.Items.Count);
NavIndex:=0;
For I:=0 to NavList.Items.Count-1 do
begin
NavIndex:=NavBar1.Items.ItemByCaption(NavList.Items.Strings[i]).Index;
ItemArray[i]:=TMenuItem.create(Nil);
ItemArray[i].Caption:=NavList.Items.Strings[i];
ItemArray[i].OnClick:=NavBar1.Items.Items[Navindex].OnClick;
NavPop.Items.Add(ItemArray[i]);
end;
end;
// Call the Items free on program close
procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
FreeItems(ItemArray);
end;
// Free Dynamically created Menu Items on Form Close
procedure TMainForm.FreeItems(MItems : array of TMenuItem);
var
cnt : integer;
begin
for cnt := High(MItems) downto Low(MItems) do
begin
MItems[cnt].Free;
MItems[cnt] := nil;
end;
end;
This happens because the TPopupMenu already free the items, and you are freeing it again.
This code causes an "Invalid pointer operation":
procedure TForm1.FormCreate(Sender: TObject);
var
I: Integer;
begin
for I := 0 to 3 do
PopupMenu1.Items.Add(TMenuItem.Create(nil));
end;
destructor TForm1.Destroy;
var
I: Integer;
begin
for I := 3 downto 0 do
PopupMenu1.Items.Free;
inherited;
end;
The Items property is a TMenuItem instance, and if you look at it's destructor, it already free all the items you added.
destructor TMenuItem.Destroy;
begin
...
while Count > 0 do Items[0].Free;
...
Keeping it short, you don't need to do it again in the FreeItems method.
I tested with ReportMemoryLeaksOnShutdown := True and no memory leaks occur.

Resources