Using TTreeview as a menu - delphi

Im using delphi's ttreeview as an 'options' menu. how would i go upon selecting the next node at runtime like a previous and next button? i tried the getprev and getnext methods but no luck.

Here you have the 'Next' behavior. For 'Previous' I leave as exercise for you: :-)
procedure TForm8.btn1Click(Sender: TObject);
var
crt: TTreeNode;
begin
with tv1 do //this is our tree
begin
if Selected=nil then
crt:=Items[0] //the first one
else
crt:=Selected.GetNext; //for previous you'll have 'GetPrev'
if crt<>nil then //can be 'nil' if we reached to the end
Selected:=crt;
end;
end;
HTH

Maybe there is some space in tree item to store pointer to you correct page.
But - if you have some time - try to explore Virtual Treeview - it's Delphi's best treeview component.

here is another way to do this:
type TfrmMain = class(TForm)
...
public
DLLHandle : THandle;
function GetNodePath(node: TTreeNode; delimiter: string = '\') : String;
...
function TfrmMain.GetNodePath(node: TTreeNode; delimiter: string = '\') : String;
begin
Result:='';
while Assigned(node) do
begin
Result:=delimiter+node.Text+Result;
node:=node.Parent;
end;
if Result <> '' then
Delete(Result, 1, 1);
end;
...
here is how to use it: on your treeview's click or doubleclick event do this
...
var
path : String;
begin
path:=GetNodePath(yourTreeView.Selected);
ShowMessage(path);
...
if you have a 'Item 1' and a subitem called 'Item 1' and click on Item 2 than the message should be 'Item 1\Item 2'. By doing this you can have a better control...
hope this gives you another idea to enhance your code

Related

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

Delphi - how to address a tool in after running the program

I want to know if there is a way to address a tool which I put in my form after the program is executed? For example:
Suppose there are 100 label components in a form and you put an edit box in your form and ask the user to enter a number in the edit. When the number is written in the edit, the label with the same number will change the font colour.
But you cannot code it before running the program and need sth like this:
Label[strtoint(edit1.text)].color:=clblue;
But as you know this code does not work. What should I write to do what I want?
Yes, you can do something like you demonstrate, you just need to store the form’s controls into some type of array or list.
Sorry, I currently do not have access to my Delphi IDE, but I think I can give you an overview to what you need to do. I will also provide a link that better demonstrate the concept.
Here are the steps:
First ensure your controls have a consistent naming format that includes an index number in the name.
Example: Label1, Label2, . . . .
Next you need to store the controls into some type of an array or TList.
Example:
Var
ControlList : TList
. . . .
ControlList := TList.Create;
. . . .
{ Load the controls into the list after they been created }
ControlList.Add (Label1)
ControlList.Add (Label2)
ControlList.Add (Label3)
Here an alternatives to adding the Labels to the list manually.
for I := 1 to 3 do
begin
ControlList.Add(TLabel(FindComponent('Label'+IntToStr(I)));
end;
Now designate some event handler where you will put the code to update the label. This handler routine will first convert the user inputted value to an integer. Them use that value as an index to the control array. Once you have the label designated to be updated, set whatever properties you like.
idx := StrToInt(InputBox.Text);
lbl := TLabel( ControlList[idx])
. . . .
lbl.Color := clBlue;
Check out this link Control Arrays in Delphi for a more detailed description.
-- Update --
Although my previous answer would work, Remy Lebeau comment give me an idea to a better approach. You do not need to store the controls in an array or list, just use the Findcomponent() command to locate the control. Below are two examples demonstrating this concept.
Example using an Edit box OnKeyPress event:
procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
var
LabelControl : TLabel;
begin
if ord(Key) = VK_RETURN then
begin
LabelControl := TLabel(FindComponent('Label'+Edit1.Text));
if (LabelControl <> nil) then
LabelControl.Color := clblue;
Key := #0; // prevent continue processing of the WM_CHAR message
end;
end;
Another example using a Button's OnClick event:
procedure TForm1.Button1Click(Sender: TObject);
var
LabelControl : TLabel;
begin
LabelControl := TLabel(FindComponent('Label'+Edit1.Text));
if (LabelControl <> nil) then
begin
LabelControl.Color := clBlue;
end;
end;
Things to note about the code:
In the first example, for the label to be updated, the user must press the enter key after inputting the desired label number.
In the second example, the user must press a button after entering
the number of the label to be updated.
In In both examples, invalid responses are ignored.
As I understand you right, all the Labels already contain a number in their caption.
Then, you could use the Controls array, that already exists in TForm, which contains all controls that belong to the form:
type
TForm1 = class(TForm)
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
// ...
Edit1: TEdit;
procedure Edit1Change(Sender: TObject);
private
public
end;
// ...
{ uses
System.RegularExpressions;
}
// ...
procedure TForm1.Edit1Change(Sender: TObject);
{
// if aLabel.Name instead of aLabel.Caption
// will work for Label1, Label2, Label3, Label4 ...
function TryNameToInt(AName: string; var ANumber: Integer): boolean;
var
aRegEx: TRegEx;
aMatch: TMatch;
aStr: string;
begin
aStr := '';
ANumber := -1;
aRegEx:= TRegEx.Create('[A-Za-z_]+([0-9]+)');
aMatch:= aRegEx.Match(AName);
if aMatch.Success then begin
aStr := aMatch.Groups.Item[1].Value;
end;
Result := TryStrToInt(aStr, ANumber);
end;}
var
aControl: TControl;
aLabel: TLabel;
aNumberEdit: Integer;
aNumberLabel: Integer;
aIdx: Integer;
begin
if TryStrToInt(Edit1.Text, aNumberEdit) then begin
for aIdx := 0 to ControlCount - 1 do begin // Controls is the list of all Controls in the form, ControlCount is the length of this list
aControl := Controls[aIdx];
if aControl is TLabel then begin // this gets only TLabel controls
aLabel := TLabel(aControl);
if TryStrToInt(aLabel.Caption, aNumberLabel)
{or TryNameToInt(aLabel.Name, aNumberLabel)} then begin
if aNumberLabel = aNumberEdit then begin
aLabel.Font.Color := clBlue;
end
else begin
aLabel.Font.Color := clWindowText; // if needed
end;
end;
end;
end;
end;
end;
You can use FindComponent function to do that:
Here I dropped a TButton and TEdit on form, you type the Label number you want to change the font color in Edit and then press the Button. Write this code in OnClick event for the Button:
Var
mColor: TColor;
mLabel: Tlabel;
begin
mColor := clGreen;
mLabel := FindComponent('Label' + Edit1.Text) as TLabel;
if mLabel <> nil then
mLabel.Font.Color := mColor;
end;
or if you don't want to press the Button and want it as you type in Edit, you have to write the code in OnChange event for Edit.

Change a record property with a variable/dynamic name

I have a simple Delphi record:
type
TCustomer = record
name : string[30];
age : byte;
end;
And I know I can set this record's field by hard coding the field name in code:
var
customer : TCustomer;
begin
// Set up our customer record
customer.name := 'Fred Bloggs';
customer.age := 23;
end;
But I have a single TEdit, a single TComboBox next to it, and a single TButton. The combobox is fixed and has two items, "Name" and "Age". It will first be set to "Name". User types their name value into the edit box. The is a Save type of button that would have an OnClick event like:
procedure TMainForm.SaveButtonClick(Sender: TObject);
begin
if(MyComboBox.Text = 'Name') then
begin
customer.name := MyEditBox.Text;
end
else
begin
customer.age := MyEditBox.Text;
end;
end;
The record was initialized someplace else. What I'm getting at here is in my cases there are 101 possible combobox items. Should I make a massive case statement to handle this or can I consolidate the code by matching the reorder's field name to a dynamic piece of information being set by another control (in this case a combobox)?
If you have a Delphi Version that has enhanced RTTI (Delphi 2010 and higher) you can do it.
However there are a few traps:
1.Short strings have to defined as type for the compiler to create typeinfo for these fields (as I did with the String30)
2.TValue which is the type to carry things around in the enhanced RTTI does not automatic type conversion (like the string from the edit into the Integer for the Age field). That is why I took the way into a Variant and converted that into the correct type for the field (just for ShortString and Integer, the rest is left as an exercise to the reader).
3.TValue does not like conversions from different ShortString types (String30 is not the same as ShortString) that is why I used TValue.Make. What is missing there is a check if the provided value matches the type (like it it exceeds 30 chars). Also it of course is not unicode compatible.
uses
Rtti,
TypInfo;
type
String30 = string[30];
TCustomer = record
name: String30;
age: byte;
end;
var
c: TCustomer;
function CastFromVariant(ATypeInfo: PTypeInfo; const AValue: Variant): TValue;
var
asShort: ShortString;
begin
case ATypeInfo.Kind of
tkInteger: Result := TValue.From<Integer>(AValue);
tkString:
begin
asShort := AValue;
TValue.Make(#asShort, ATypeInfo, Result);
end;
end;
end;
procedure TForm5.Button1Click(Sender: TObject);
var
ctx: TRttiContext;
t: TRttiType;
f: TRttiField;
v: TValue;
begin
t := ctx.GetType(TypeInfo(TCustomer));
f := t.GetField(ComboBox1.Text);
v := CastFromVariant(f.FieldType.Handle, Edit1.Text);
f.SetValue(#c, v);
end;
procedure TForm5.ComboBox1Change(Sender: TObject);
var
ctx: TRttiContext;
t: TRttiType;
f: TRttiField;
v: TValue;
begin
t := ctx.GetType(TypeInfo(TCustomer));
f := t.GetField(ComboBox1.Text);
v := f.GetValue(#c);
Edit1.Text := v.ToString;
end;
The on-screen components are their own variables. They exist independently of your records.
You need to copy from one to the other as needed.
(Use my code as a guide -- it may not be exactly correct syntactically.)
// UI component declarations within the form
TForm1 = class(TForm)
. . .
cbo : TComboBox;
edt : TEdit;
. . .
end;
. . .
var
Form1 : TForm1;
. . .
// to copy values from customer to UI components:
cbo.ItemIndex := customer.age; // assuming this is what the combobox is used for,
// and it starts at zero
edt := customer.name;
// to copy from UI components into customer, you'll need to do it inside of one
// of the event handlers.
TForm1.cboCloseUp( sender : TObject ); // the onCloseUp handler, when the combo drop-down closes
begin
customer.age := cbo.ItemIndex; // or StrToInt(cbo.Text)
end;
TForm1.edtChange( sender : TObject ); // the onChange handler whenever the edit changes
begin
customer.name := edt.Text;
end;
That's the gist of it. If the ages in the combobox don't start at zero, then you'll want to adjust the offset accordingly when you copy bewteen the customer record and the combobox. You can either set ItemIndex+offset if the values are linear, or simply write a value into cbo.Text.

cxgrid popup menu paste value of popupmenuitem

IN TMS string grid I used to use this to paste the caption of the popup menu into the grid's cell :
var
s:string;
begin
s:=(Sender as TmenuItem).Caption;
s:=stringReplace(s,'&','',[rfReplaceAll]);
with AdvStringGrid1 do
Cells[Col,Row]:=s;
I never used this before in a cxGrid so I am totally new to this. I have linked cxGridpopUpMenu1 to my grid,added a classic PopUpMenu so it gets used by the cxGridpopUpMenu1,added some items in the popup menu and thats it. popup menu fires on right click in the grid ok, but how do you paste the value of the menuitem into the cell??
+ Is there a way to assign popopmenu to a particular column ?
I'd do it like this:
procedure TForm1.MenuItem1Click(Sender: TObject);
var
s: string;
begin
Assert(Sender is TMenuItem);
s := StripHotKey(TMenuItem(Sender).Caption);
cxGrid1TableView1.DataController.Edit;
cxGrid1TableView1.Controller.FocusedColumn.EditValue := s;
end;
This can be done combining two event handlers:
The OnPopUp handler of your TcxGridPopupMenu.
An OnClick handler for all your popup menu items.
The idea is to use the OnPopup to store a reference to the item (column) and record clicked, while the OnClick would apply the value to the cell.
Code is as following:
//in private section of your form
fItem: TcxCustomGridTableItem;
fRec: TcxCustomGridRecord;
procedure TForm1.cxGridPopupMenu1Popup(ASenderMenu: TComponent;
AHitTest: TcxCustomGridHitTest; X, Y: Integer; var AllowPopup: Boolean);
begin
if AHitTest is TcxGridRecordCellHitTest then
begin
fItem := TcxGridRecordCellHitTest(AHitTest).Item;
fRec := TcxGridRecordCellHitTest(AHitTest).GridRecord;
end;
end;
procedure TForm1.MenuItem1Click(Sender: TObject);
var
s : string;
begin
s := (sender as tmenuItem).Caption;
gridView.DataController.Values[frec.Index, fitem.Index] := StripHotKey(s);
end;
As #DavidHeffernan suggested, notice the use of StripHotKey that removes the accelerator character mark from the menu caption.

Delphi: Custom hints for Tree View

Is there a fast way to create 5 custom hints for 5 SubItems of Item of Tree View?
I have TreeView, 1 Item and 5 SubItems. I need a special hint for each SubItem (for first one - "F1", second one -"F2" and so on).
I can not apply this to my purpose: http://delphi.about.com/od/vclusing/a/treenode_hint.htm?
It sounds like you just want the OnHint event:
procedure TMyForm.TreeView1Hint(Sender: TObject; const Node: TTreeNode; var Hint: string);
begin
Hint := Node.Text;
end;
Sometimes this method can be a bit crude and offer up a Node that you aren't obviously hovering over. If you want more control you can use GetNodeAt and GetHitTestInfoAt:
procedure TMyForm.TreeView1Hint(Sender: TObject; const Node: TTreeNode; var Hint: string);
var
P: TPoint;
MyNode: TTreeNode;
HitTestInfo: THitTests;
begin
P := TreeView1.ScreenToClient(Mouse.CursorPos);
MyNode := TreeView1.GetNodeAt(P.X, P.Y);
HitTestInfo := TreeView1.GetHitTestInfoAt(P.X, P.Y) ;
if htOnItem in HitTestInfo then begin
Hint := MyNode.Text;
end else begin
Hint := '';
end;
end;
The definition of THitTests is as follows:
type
THitTest = (htAbove, htBelow, htNowhere, htOnItem, htOnButton, htOnIcon,
htOnIndent, htOnLabel, htOnRight, htOnStateIcon, htToLeft, htToRight);
THitTests = set of THitTest;
As you can see this gives you a lot of fine grained control over when and what you show as a hint.
I would set the hint of the component in response to OnMouseMove (or that other event that gives you mouse coordinates, from which you can get the item the mouse is over - I might have mistaken the name and at the moment I have no Delphi with me).

Resources