how to expand only to the second level of the treeview - delphi

Ex.:
- link 1
-- link 1.1
--- link 1.1.1(only)
---- link 1.1.1.1 (not expand)
-- link 1.2
--- link 1.2.1 (only)
---- link 1.2.1.1 (not expand)
I can expand only link 1.1, link 1.2... how?

There is no build-in functionality for expanding multiple items or items on a specific level, so there is no other way then traversing through the items. Call the Expand method on all second level items. As well on all first level items, otherwise the second level items will not be shown. Its Recurse parameter should be False in order not to expand a possibly third or deeper level.
There are two ways to traverse through a TreeView's items: by Item index and by Node. Normally, operations on a TreeView's items are preferably done by Node, because the Items property's getter traverses through all Items to find a single Item with a specific index. However, TTreeNodes caches the last retrieved item, thus by incrementing the loop Index with 1, harm will be minimized.
The easy solution then becomes:
procedure ExpandTreeNodes(Nodes: TTreeNodes; Level: Integer);
var
I: Integer;
begin
Nodes.BeginUpdate;
try
for I := 0 to Nodes.Count - 1 do
if Nodes[I].Level < Level then
Nodes[I].Expand(False);
finally
Nodes.EndUpdate;
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
ExpandTreeNodes(TreeView1.Items, 2);
end;
Note that the Items property's getter is still called twice. Despite the caching mechanism, I think this still should be avoided:
procedure ExpandTreeNodes(Nodes: TTreeNodes; Level: Integer);
var
I: Integer;
Node: TTreeNode;
begin
Nodes.BeginUpdate;
try
for I := 0 to Nodes.Count - 1 do
begin
Node := Nodes[I];
if Node.Level < Level then
Node.Expand(False);
end;
finally
Nodes.EndUpdate;
end;
end;
But then you might as well use:
procedure ExpandTreeNodes(Nodes: TTreeNodes; Level: Integer);
var
Node: TTreeNode;
begin
Nodes.BeginUpdate;
try
Node := Nodes.GetFirstNode;
while Node <> nil do
begin
if Node.Level < Level then
Node.Expand(False);
Node := Node.GetNext;
end;
finally
Nodes.EndUpdate;
end;
end;
This still traverses áll Items. Ok, just once. But if you have a really big and/or deep tree, and you want maximum efficiency, and you do not care about readability, or you just want to experiment with a TreeView's Nodes for fun, then use:
procedure ExpandTreeNodes(Nodes: TTreeNodes; Level: Integer);
var
Node: TTreeNode;
Next: TTreeNode;
begin
if Level < 1 then
Exit;
Nodes.BeginUpdate;
try
Node := Nodes.GetFirstNode;
while Node <> nil do
begin
Node.Expand(False);
if (Node.Level < Level - 1) and Node.HasChildren then
Node := Node.GetFirstChild
else
begin
Next := Node.GetNextSibling;
if Next <> nil then
Node := Next
else
if Node.Level > 0 then
Node := Node.Parent.GetNextSibling
else
Node := Node.GetNextSibling;
end;
end;
finally
Nodes.EndUpdate;
end;
end;

Related

How to get Id values for the adjacent records in TcxGrid (TcxGridDBTableView)

I have DevExpress cxGrid and I would like to get the Ids of the adjacent records. I can use them in the following use case: user deletes focused record and I position the (grid view) cursor on one of the adjacent records (as determined but the current sorting and grouping order of the grid view). Otherwise the position of the grid cursor is poorly determined after removal of the record and after refreshing the grid view.
I have made the following attempts but they are not working - the Id values are junk:
procedure GetAdjacentRecordIds(AView: TcxGridDBTableView; ACdFieldName: string; var APrevId, AId, ANextId: Integer);
var Item: TcxCustomGridTableItem;
RecIdx: Integer;
i, RecCount: Integer;
begin
APrevId:=-1;
AId:=-1;
ANextId:=-1;
if Trim(AIdFieldName)='' then Exit;
if not Assigned(AView) then Exit;
if not Assigned(AView.DataController) then Exit;
Item:=AView.DataController.GetItemByFieldName(UpperCase(AIdFieldName));
if not Assigned(Item) then Exit;
{//First attempt, didn't work, AId was the right one, but APrevId and ANextId were junk
RecIdx:=AView.DataController.FocusedRecordIndex;
AId:=AView.DataController.Values[RecIdx, Item.Index];
APrevId:=AView.DataController.Values[RecIdx-1, Item.Index];
ANextId:=AView.DataController.Values[RecIdx+1, Item.Index];}
//Second attempt, doesn't work, all three Ids are junk
RecIdx:=-1;
RecCount:=AView.ViewData.RecordCount;
for i:=0 to RecCount-1 do begin
if AView.ViewData.Records[i].Focused then begin
RecIdx:=1;
Break;
end;
end;
if RecIdx<0 then Exit;
AId:=AView.ViewData.Records[RecIdx].Values[Item.Index];
if RecIdx>0 then
APrevId:=AView.ViewData.Records[RecIdx-1].Values[Item.Index];
if RecIdx<RecCount then
ANextId:=AView.ViewData.Records[RecIdx+1].Values[Item.Index];
end;
How can I correct this code to get the field values for adjacent records. Or maybe I should use Grid navigator and do prev/next on it, but I would like to find the values in invisible. And DataSet.DisableControls may stop the Grid navigator?
I used the DevExpress knowledgebase to find this answer. You are referencing the internal storage of the grid incorrectly. You can also use dataset.previous and dataset.next to position the record pointer in a bound grid, and simply use dataset.fieldbyname(AidFieldName).AsInteger to retrieve key values.
get-cell-value-by-column-name
procedure TForm1.Button1Click(Sender: TObject);
var
AColumn: TcxGridDBColumn;
I: Integer;
AView: TcxGridDBTableView;
v: Variant;
begin
AView := cxGrid1DBTableView1;
AColumn := TcxGridDBColumn( AView.FindItemByName('cxGrid1DBTableView1Capital'));
if Assigned(AColumn) then
for I := 0 to AView.ViewData.RowCount - 1 do
v := AView.DataController.Values[AView.ViewData.Rows[0].RecordIndex, AColumn.Index];
end;
Use of the navigation functions of DataSet is not the solution, because CxGrid can have completely different sorting order. But DataController navigation is good solution and there is no visual flickering as well. So, the solution is:
RecIdx:=AView.DataController.FocusedRecordIndex;
AId:=AView.DataController.Values[RecIdx, Item.Index];
if not AView.DataController.IsBOF then begin
AView.DataController.GotoPrev;
RecIdx:=AView.DataController.FocusedRecordIndex;
APrevId:=AView.DataController.Values[RecIdx, Item.Index];
AView.DataController.GotoNext;
end;
if not AView.DataController.IsEOF then begin
AView.DataController.GotoNext;
RecIdx:=AView.DataController.FocusedRecordIndex;
ANextId:=AView.DataController.Values[RecIdx, Item.Index];
AView.DataController.GotoPrev;
end;

What is the most elegant way to reposition after filtering a TClientDataset

I'm working on a TClientDataset that the user can filter at any time based on some criterias. My problem is, we'd like the dataset's cursor to remain positionned "mostly" at the same place after filtering. ("Mostly" in double quote since, of course, it can't stay at the same place if the record is filtered out).
After doing some research, the best I could come up with is the following :
procedure RefreshFilter;
var
I : Integer;
sFilter : string;
vIndexValue: array of TVarRec;
vIndexValueAsVar : Array of Variant;
begin
sFilter := GenerateNewFilterExpression;
if sFilter <> MyDataset.Filter then
begin
if MyDataset.IndexFieldCount > 0 then
begin
SetLength(vIndexValueAsVar, MyDataset.IndexFieldCount);
SetLength(vIndexValue, MyDataset.IndexFieldCount);
for I := 0 to MyDataset.IndexFieldCount - 1 do
begin
vIndexValueAsVar[I] := MyDataset.IndexFields[I].AsVariant;
vIndexValue[I].VType := vtVariant;
vIndexValue[I].VVariant := #vIndexValueAsVar[I];
end;
end;
MyDataset.Filtered := sFilter <> '';
Mydataset.Filter := sFilter;
if MyDataset.IndexFieldCount > 0 then
begin
MyDataset.FindNearest(vIndexValue);
end;
end;
end;
Even though it works pretty well, I find the solution a bit "bulky". I was wondering if there was a some built-in function or a different approach that might be more elegant and less "heavy".
And please, don't mention bookmarks... Bookmarks don't work properly after changing the active filter, and not at all if your record gets filtered out.

Incremental search of a dbgrid using an edit box

It has been awhile since I have done any programming in Delphi and I was looking around for some examples on how to incrementally search a dbgrid by typing a search term into a edit box and I found the following code which seems to do the trick for the most part, but it checks for the filter condition on every column in the grid and I would like to limit the filter condition so it only checks one column in the grid (Column 1 for instance), how would I do that using the code provided ?
procedure TForm1.Edit1Change(Sender: TObject);
begin
FDTable1.Filtered := false;
FDTable1.Filtered := Edit1.Text <> '';
end;
procedure TForm1.FDTable1FilterRecord(DataSet: TDataSet;
var Accept: Boolean);
var
i: integer;
begin
for i := 0 to DataSet.FieldCount - 1 do begin
Accept := Pos(UpperCase(Edit1.Text),
UpperCase(DataSet.Fields[i].AsString)) = 1;
if Accept then exit;
end;
end;

Remove duplicates from combobox

Say i have a combobox with
apples
apples
pears
oranges
oranges
i would like to have it show
apples
pears
oranges
how can i do this?
for iter := combobox.Items.Count - 1 downto 0 do
begin
index := combobox.Items.IndexOf(combobox.Items[iter]);
if index < iter then
combobox.Items.Delete(iter);
end;
I suggest that you simply refill the combo box each time. That makes the logic simpler:
ComboBox.Items.BeginUpdate;
try
ComboBox.Clear;
for Str in Values do
begin
if ComboBox.Items.IndexOf (Str) = -1 then
ComboBox.Items.Add (Str);
end;
finally
ComboBox.Items.EndUpdate;
end;
Just to put methods against eachother: one keeps the order but is increasingly slow with larger number of items. The other stays relatively faster but doesn't keep order:
procedure SortStringlist;
var
i,index,itimer: integer;
sl : TStringlist;
const
numberofitems = 10000;
begin
sl := TStringlist.Create;
for i := 0 to numberofitems-1 do begin
sl.Add(IntToStr(random(2000)));
end;
Showmessage(IntToStr(sl.Count));
itimer := GetTickCount;
sl.Sort;
for I := sl.Count-1 downto 1 do begin
if sl[i]=sl[i-1] then sl.Delete(i);
end;
Showmessage(IntToStr(sl.Count)+' Time taken in ms: '+IntToStr(GetTickCount-itimer));
sl.free;
sl := TStringlist.Create;
for i := 0 to numberofitems-1 do begin
sl.Add(IntToStr(random(2000)));
end;
Showmessage(IntToStr(sl.Count));
itimer := GetTickCount;
for i := sl.Count - 1 downto 0 do
begin
index := sl.IndexOf(sl[i]);
if index < i then
sl.Delete(i);
end;
Showmessage(IntToStr(sl.Count)+' Time taken in ms: '+IntToStr(GetTickCount-itimer));
end;
If you don't care if the items get reordered (or they're sorted already), TStrings can do the work for you - it eliminates all of the looping, deletion, and other work. (Of course, it requires the creation/destruction of a temporary TStringList, so if that's an issue for you it won't work.)
var
SL: TStringList;
begin
ComboBox1.Items.BeginUpdate;
try
SL := TStringList.Create;
try
SL.Sorted := True; // Required for Duplicates to work
SL.Duplicates := dupIgnore;
SL.AddStrings(ComboBox1.Items);
ComboBox1.Items.Assign(SL);
finally
SL.Free;
end;
finally
ComboBox1.Items.EndUpdate;
end;
end;
To properly compare with Igor's answer (which includes no BeginUpdate/EndUpdate), remove those things:
var
SL: TStringList;
begin
SL := TStringList.Create;
try
SL.Sorted := True; // Required for Duplicates to work
SL.Duplicates := dupIgnore;
SL.AddStrings(ComboBox1.Items);
ComboBox1.Items.Assign(SL);
finally
SL.Free;
end;
end;
You have to remove duplicates from the source data.
In most scenarios, a ComboBox is filled with data in run-time, which means, data is coming from some source. There are basically 2 scenarios here: a dataset from database and a collection of strings from any other source. In both cases you filter out duplicates before inserting anything into the ComboBox.
If source is a dataset from database, simply use the SQL DISTINCT keyword.
If source is any collection of strings, use a peace of code provided in the answer by #Smasher.
I faced this problem several times before, and i used all the previous approaches and I'm still using them, but do you know : i think the best approach , though not mentioned here, is to subclass TComboBox, creating a new method (say AddUnique ) that add the string to the combo ONLY if it does not exist previously , otherwise it will drop it.
This solution may cost some extra time in the beginning , but it will solve the problem once and for all.

How should I implement GetLastNode for TTreeNodes?

When I need to find the first node in a TTreeView, I call TTreeNodes.GetFirstNode. However, I sometimes need to locate the last node in a tree and there is no corresponding TTreeNodes.GetLastNode function.
I don't want to use Items[Count-1] since that results in the entire tree being walked with Result := Result.GetNext. Naturally this only matters if the tree views have a lot of nodes. I fully appreciate the virtues of virtual container controls but I am not going to switch to Virtual TreeView just yet.
So far I have come up with the following:
function TTreeNodes.GetLastNode: TTreeNode;
var
Node: TTreeNode;
begin
Result := GetFirstNode;
if not Assigned(Result) then begin
exit;
end;
while True do begin
Node := Result.GetNextSibling;
if not Assigned(Node) then begin
Node := Result.GetFirstChild;
if not Assigned(Node) then begin
exit;
end;
end;
Result := Node;
end;
end;
Can anyone:
Find a flaw in my logic?
Suggest improvements?
Edit 1
I'm reluctant to keep my own cache of the nodes. I have been doing just that until recently but have discovered some hard to track very intermittent AVs which I believe must be due to my cache getting out of synch. Clearly one solution would be to get my cache synchronisation code to work correctly but I have an aversion to caches because of the hard to track bugs that arise when you get it wrong.
Although I am not a non-Exit purist, I think that when it is doable without Exit while keeping readability intact, one might prefer that option.
So here is exactly the same code, for I don't think you can get any other way (faster) to the end node, but without Exit and slightly more compact:
function TTreeNodes.GetLastNode: TTreeNode;
var
Node: TTreeNode;
begin
Node := GetFirstNode;
Result := Node;
if Result <> nil then
repeat
Result := Node;
if Node <> nil then
Node := Result.GetNextSibling;
if Node = nil then
Node := Result.GetFirstChild;
until Node = nil;
end;
The method I've used before is the maintain a TList using List.Add on the OnAddition event and List.Remove on the OnDeletion event (OnRemove?). You've access to List.Count-1 (or whatever you need) pretty much instantly then.
Post edit - I have to say that although this worked fine, I then grew up and moved to Virtual Tree View :-)
If I was to implement it, this would probably be my first draft.
function TTreeNodes.GetLastNode: TTreeNode;
var
Node: TTreeNode;
function GetLastSibling(aNode : TTreeNode) : TTreeNode;
begin
if not Assigned(aNode) then
EXIT(nil);
repeat
Result := aNode;
aNode := Result.GetNextSibling;
until not Assigned(aNode) ;
end;
begin
Node := GetFirstNode;
if not Assigned(Node) then begin
exit;
end;
repeat
Result := GetLastSibling(Node);
Node := Result.GetFirstChild;
until not Assigned(Node);
end;
I find this slightly more readable. It might be slightly slower though.
I'm unsure about whether or not this approach would be faster than items[Count-1], in some cases, it could be slower, as the TTreeNodes actually caches the last node accessed through the items property.

Resources