Dual ComboBox selection dependency - delphi

I would like to have a 2 drop down combobox with the following items:
Combo1:
Pets
Fruits.
Combo2:
If Pets is picked then combobox2items.Add:
Dog, Cat, Chicken
If Fruits is picked then combobox2items.Add:
Melon, Orange, Apple
So I try to do this:
procedure TForm1.FormCreate(Sender: TObject);
begin
ComboBox1.Items.Add('Pets');
ComboBox1.Items.Add('Fruits');
end;
procedure TForm1.ComboBox2Change(Sender: TObject);
begin
if ComboBox1.ItemIndex = 1) then
ComboBox2.Items.Add('Dog');
ComboBox2.Items.Add('Cat');
ComboBox2.Items.Add('Chicken');
if ComboBox1.ItemIndex = 2) then
ComboBox2.Items.Add('Melon');
ComboBox2.Items.Add('Orange');
ComboBox2.Items.Add('Apple');
end;
My code is not working. How to figure this out in simple way?

You need to use begin..end like this:
if ComboBox1.ItemIndex = 1 then
begin
ComboBox2.Items.Add ('Dog');
ComboBox2.Items.Add ('Cat');
ComboBox2.Items.Add ('Chicken');
end;
if ComboBox1.ItemIndex = 2 then
begin
ComboBox2.Items.Add ('Melon');
ComboBox2.Items.Add ('Orange');
ComboBox2.Items.Add ('Apple');
end;
Also you need to clear the combobox before adding in the new items;

When it comes to Combo Box dependencies, I like to build a Dictionary that represents these dependencies. Basically, you have your Dictionary keep the items of ComboBox1 as keys. When ComboBox1 changes, you re-assign ComboBox2's Items property to the StringList behind the selected Key. This saves you the trouble of removing/adding individual strings every time ComboBox1's index is changed.
procedure TForm1.FormCreate(Sender: TObject);
begin
FComboBoxDependencies := TDictionary<string,TStringList>.Create;
FComboBoxDependencies.Add('Pets',TStringList.Create);
FComboBoxDependencies['Pets'].Add('Dog');
FComboBoxDependencies['Pets'].Add('Cat');
FComboBoxDependencies['Pets'].Add('Chicken');
FComboBoxDependencies.Add('Fruit',TStringList.Create);
FComboBoxDependencies['Fruits'].Add('Orange');
FComboBoxDependencies['Fruits'].Add('Apple');
FComboBoxDependencies['Fruits'].Add('Melon');
//Trigger Change Event at start to display the selected Key
ComboBox1Change(self);
end;
procedure TForm1.ComboBox1Change(Sender: TObject);
begin
ComboBox2.Items := FComboBoxDependencies[ComboBox1.Text]; //Grab Items to be displayed from dictionary
ComboBox2.ItemIndex := 0; //Set Itemindex to 0 to show first item
end;
Of course, this can be refined and tweaked to be more reliable, but the core of this works pretty good.

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;

Update corresponding label depending on which combobox fired the event

I have a program with n ComboBoxes and n Labels and I want to update the corresponding Label depending on the selection from the adjacent ComboBox i.e ComboBox2 would update Label2.
I am using the same event handler for every ComboBox and currently checking if Combobox1 or Combobox2 has fired the event handler. Is there a way to use the ItemIndex of the ComboBox passed to the procedure, such as Sender.ItemIndex? This is not currently an option and gives the error 'TObject' does not contain a member named 'ItemIndex'.
procedure TForm2.ComboBoxChange(Sender: TObject);
begin
if Sender = ComboBox1 then
Label1.Caption := ComboBox1.Items.Strings[ComboBox1.ItemIndex]
else
Label2.Caption := ComboBox2.Items.Strings[ComboBox2.ItemIndex];
end;
This code has the desired behavior but is obviously not scale-able.
Every component has a Tag property inherited from TComponent, where the Tag is a pointer-sized integer. As such, you can store each TLabel pointer directly in the corresponding TComboBox.Tag, eg:
procedure TForm2.FormCreate(Sender: TObject);
begin
ComboBox1.Tag := NativeInt(Label1);
ComboBox2.Tag := NativeInt(Label2);
end;
This way, ComboBoxChange() can then directly access the TLabel of the changed TComboBox, eg:
procedure TForm2.ComboBoxChange(Sender: TObject);
var
CB: TComboBox;
begin
CB := TComboBox(Sender);
if CB.Tag <> 0 then
TLabel(CB.Tag).Caption := CB.Items.Strings[CB.ItemIndex];
end;
Option 1
This is the most robust one.
Let your form have private members
private
FControlPairs: TArray<TPair<TComboBox, TLabel>>;
procedure InitControlPairs;
and call InitControlPairs when the form is created (either in its constructor, or in its OnCreate handler):
procedure TForm1.InitControlPairs;
begin
FControlPairs :=
[
TPair<TComboBox, TLabel>.Create(ComboBox1, Label1),
TPair<TComboBox, TLabel>.Create(ComboBox2, Label2),
TPair<TComboBox, TLabel>.Create(ComboBox3, Label3)
]
end;
You need to add the controls to this array manually. That's the downside of this approach. But you only need to do this once, right here. Then everything else can be done automagically.
Now, this is where it gets really nice: Let all your comboboxes share this OnChange handler:
procedure TForm1.ComboBoxChanged(Sender: TObject);
var
i: Integer;
begin
for i := 0 to High(FControlPairs) do
if FControlPairs[i].Key = Sender then
FControlPairs[i].Value.Caption := FControlPairs[i].Key.Text;
end;
Option 2
Forget about any private fields. Now instead make sure that each pair has a unique Tag. So the first combo box and label both have Tag = 1, the second pair has Tag = 2, and so on. Then you can do simply
procedure TForm1.ComboBoxChanged(Sender: TObject);
var
TargetTag: Integer;
CB: TComboBox;
i: Integer;
begin
if Sender is TComboBox then
begin
CB := TComboBox(Sender);
TargetTag := CB.Tag;
for i := 0 to ControlCount - 1 do
if (Controls[i].Tag = TargetTag) and (Controls[i] is TLabel) then
begin
TLabel(Controls[i]).Caption := CB.Text;
Break;
end;
end;
end;
as the shared combo-box event handler. The downside here is that you must be sure that you control the Tag properties of all your controls on the form (at least with the same parent as your labels). Also, they must all have the same parent control.

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!)

TComboBox and TListBox item Removal?

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;

Devexpress grid with unbound column

I have a DevExpress grid where I would like to add an unbound checkbox to be able to select some of the items.
After the selection is made I press a button and I must loop the grid to get all the selected items.
It has to be a checkbox. I have tried with a multiselectable grid, but the users can't work with that.
I have tried all the samples that I have been able to find on the supportsites, but no luck.
I need the unbound approach since it is a multiuser setup and users have been selecting and deselecting for each other.
My question: does anyone have a working sample that shows how this can be done?
I've done this and it was (is!) pretty ugly! Create the grid view with bound columns and add an unbound checkbox column with a field type of boolean.
Basically I handle the OnCellClick of the grid view. I check if the item clicked is the checkbox column - by finding the first unbound column in the view with a checkbox type. Then I toggle its state.
I've set AutoEdit on the dataset to true but Deleting/Editing/Inserting to false and ImmediateEditor is false. Not exactly sure which of those are important.
I think the hardest thing was trying to fathom out the complex hierarchy of grid and view level objects and working out which levels contained which of the needed bits. I'm sure there's a better way of doing it but what we've got now works and I'm not going to touch it again!
This is lifted from my code but modified slightly and not tested as it stands - it also needs a bit more error checking:
procedure TMyForm.ViewCellClick(Sender: TcxCustomGridTableView;
ACellViewInfo: TcxGridTableDataCellViewInfo; AButton: TMouseButton;
AShift: TShiftState; var AHandled: Boolean);
var
col: TcxGridColumn;
begin
// Manually handle the clicking of the checkbox cell - otherwise it seems
// virtually impossible to get the checked count correct.
col := GetViewCheckColumn(Sender);
if (Sender.Controller.FocusedItem = col) then
begin
ToggleRowSelection(TcxCustomGridTableView(TcxGridSite(Sender).GridView), col);
end;
end;
procedure TMyForm.ToggleRowSelection(AView: TcxCustomGridTableView; ACol: TcxGridColumn);
var
rec: TcxCustomGridRecord;
begin
rec := AView.Controller.FocusedRecord;
if (rec = nil) then exit;
if (rec.Values[ACol.Index] = TcxCheckBoxProperties(ACol.Properties).ValueChecked) then
begin
rec.Values[ACol.Index] := TcxCheckBoxProperties(ACol.Properties).ValueUnchecked;
end
else
begin
rec.Values[ACol.Index] := TcxCheckBoxProperties(ACol.Properties).ValueChecked;
end;
end;
function TMyForm.GetViewCheckColumn(AView: TcxCustomGridView): TcxGridColumn;
var
index: integer;
vw: TcxCustomGridTableView;
item: TcxCustomGridTableItem;
begin
// We're looking for an unbound check box column - we'll return the first
// one found.
Assert(AView <> nil);
result := nil;
if (AView is TcxCustomGridTableView) then
begin
vw := TcxCustomGridTableView(AView);
for index := 0 to vw.ItemCount - 1 do
begin
item := vw.Items[index];
if (item.Properties is TcxCustomCheckBoxProperties) then
begin
if (item is TcxGridDBColumn) then
begin
if (TcxGridDBColumn(item).DataBinding.FieldName = '') then
begin
result := TcxGridColumn(item);
break;
end;
end;
end;
end;
end;
end;
I then extended it by checking for a SPACE bar press in the OnKeyUp of the grid and calling ToggleRowSelection and also similar for a double click on a row.
When iterating through the rows you can test if a row is checked using something like the following:
function TMyForm.GetViewIsRowChecked(AView: TcxCustomGridView; ARecord: TcxCustomGridRecord): boolean;
var
col: TcxGridColumn;
begin
result := False;
col := GetViewCheckColumn(AView);
if ((col <> nil) and (ARecord <> nil)) then
begin
result := (ARecord.Values[col.Index] = TcxCheckBoxProperties(col.Properties).ValueChecked);
end;
end;
I think that's it. I've dug it out of a large grid/view helper unit we've built up over a while. Oh, and it's currently working with Delphi 2010 with DXVCL v2011 vol 1.10.
Hope it helps.

Resources