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).
Related
I created an application that goes out and scans every computer and populates a TreeView with Hardware, Software and updates/hotfixes information:
The problem I’m having is with printing, how do you automatically expand the treeview and sends the results of the selected computer to the printer? The method I am currently using involves sending the contents to a canvas (BMP) and then send it to the printer but that does not capture the whole treeview only whatever is being displayed on the screen. Any advice? Thank you so much.
The problem with printing the TTreeView is that the part that isn't visible has nothing to be drawn. (Windows draws only the visible portion of the control, so when you use PrintTo or the API PrintWindow function, it only has the visible nodes available to print - the non-displayed content hasn't yet been drawn and therefore can't be printed.)
If a tabular layout works (no lines, just indented levels), the easiest way is to create text and put it in a hidden TRichEdit, and then let the TRichEdit.Print handle the output. Here's an example:
// File->New->VCL Forms Application, then
// Drop a TTreeView and a TButton on the form.
// Add the following for the FormCreate (to create the treeview content)
// and button click handlers, and the following procedure to create
// the text content:
procedure TreeToText(const Tree: TTreeView; const RichEdit: TRichEdit);
var
Node: TTreeNode;
Indent: Integer;
Padding: string;
const
LevelIndent = 4;
begin
RichEdit.Clear;
Node := Tree.Items.GetFirstNode;
while Node <> nil do
begin
Padding := StringOfChar(#32, Node.Level * LevelIndent);
RichEdit.Lines.Add(Padding + Node.Text);
Node := Node.GetNext;
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
HideForm: TForm;
HideEdit: TRichEdit;
begin
HideForm := TForm.Create(nil);
try
HideEdit := TRichEdit.Create(HideForm);
HideEdit.Parent := HideForm;
TreeToText(TreeView1, HideEdit);
HideEdit.Print('Printed TreeView Text');
finally
HideForm.Free;
end;
end;
procedure TForm3.FormCreate(Sender: TObject);
var
i, j: Integer;
RootNode, ChildNode: TTreeNode;
begin
RootNode := TreeView1.Items.AddChild(nil, 'Root');
for i := 1 to 6 do
begin
ChildNode := TreeView1.Items.AddChild(RootNode, Format('Root node %d', [i]));
for j := 1 to 4 do
TreeView1.Items.AddChild(ChildNode, Format('Child node %d', [j]));
end;
end;
I'm trying to implement a 'toggle' function when clicking on a tlistview item. But when testing I notice that the click event get's fired 2 times with a single click / tap with IDENTICAL parameters..
I'm testing this for now on windows.
Is this 'works as designed' ?
I only added a listview to an empty form and implemented the 'ItemClickEx' event.
I could not find a workaround way to my toggle..except implementing a timer that would keep track of the clicks and ignore a second click to soon...
( it seems the FMX framework also works with delayed events when looking at the stack trace )
I tested also the onitemclick event and this one DOES fire only once. .. so I could probably use it to implement simple workaround. But not nice, I need the 'ex' version as well as eventually, I need to add/delete items from my list which is recommended only from the 'ex' version, according to the documentation .
Regards
Dirk
Update Please see the Update section below which is based on the OP's observations in his own answer.
The minimal project below does not exhibit the behaviour your describe.
If I click any item in ListView1, the ItemClickedCount variable only increments by one,
as confirmed by the display on the form's caption (if I double-click the form, ItemClickedCount
increments by 2, as expected).
To implement your toggle, simply toggling a boolean should suffice or you could simply
derive the toggle state from whether the ItemClickedCount is odd or even.
So, I think the behaviour you describe must be coming from soe part
of your code not mentioned in your q. Obviously, the way to identify the cause
is to iteratively simplify your form and its code. Good luck!
procedure TForm1.BuildList;
var
LItem : TListViewItem;
ListItemText : TListItemText;
Index : Integer;
begin
ListView1.BeginUpdate;
try
ListView1.Items.Clear;
ListView1.ItemAppearanceObjects.ItemEditObjects.Text.TextVertAlign := TTextAlign.Leading;
for Index := 0 to 19 do begin
LItem := ListView1.Items.Add;
LItem.ButtonText := 'Hello';
LItem.Text := 'Row: ' + IntToStr(Index);
LItem.Height := 25;
ListItemText := TListItemText.Create(LItem);
ListItemText.PlaceOffset.X := 100;
ListItemText.PlaceOffset.Y := 25 * (Index - 1);
ListItemText.Name := 'Name' + IntToStr(Index);
end;
finally
ListView1.EndUpdate;
end;
end;
procedure TForm1.ListView1ItemClickEx(const Sender: TObject; ItemIndex: Integer;
const LocalClickPos: TPointF; const ItemObject: TListItemDrawable);
begin
Inc(ItemClickedCount);
Caption := IntToStr(ItemClickedCount);
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
BuildList;
end;
Update Replying to your comment + answer, yes, I do see ListView1ItemClickEx being called twice for each mouse click. I've
looked into why this happens, and it is seemingly deliberate though why it should be that way isn't obvious to me.
Looking at the source of FMX.ListView (I'm doing this in Seattle so your line numbers may vary),
ListView1ItemClickEx is calledby line 2003 (Case Entry.Incident of ... TDelayedIncident.ClickEvent:)
in procedure TListViewBase.ProcessIncident(const Entry: TDelayedIncidentEntry);
Obviously, to be called twice, there must be two such Incidents per click, so I then looked at how these Incidents get added to whatever list/queue is being processed. So I then looked at procedure TListViewBase.StartIncident(const Incident: TDelayedIncident; const Triggered: Boolean;
const TimeToWait: Single; const CustomData: NativeInt);
at line 1949.
Following each mouse click, this is called twice:
The first time, looking at the call stack, the call originates in procedure TListViewBase.SetNewItemIndex(const NewIndex: Integer) at line 4083.
The second time, it is from inside procedure TListViewBase.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Single).
It's not obvious to me how to avoid this by property settings of the TListView, but it might be. However, there is still a simple work-around that can be included in the ListView1ItemClickEx handler:
procedure TForm1.ListView1ItemClickEx(const Sender: TObject; ItemIndex: Integer;
const LocalClickPos: TPointF; const ItemObject: TListItemDrawable);
var
LItem : TListViewITem ;
begin
Inc(ItemClickedCount);
if not Odd(ItemClickedCount) then
Exit;
Caption := IntToStr(ItemClickedCount);
LITem := ListView1.Items[ItemIndex];
LItem.Tag := LItem.Tag + 1 ;
LItem.Text := LItem.Text + ' ' + LItem.Tag.ToString + 'clicks';
end;
I'm wondering about wiring this de-duplication handling into an interposer class that you could include in your source unit. If I can think of a clean way, I'll maybe add it here later on.
Thx for your input.
You are correct, if you only add a TListview and fill it with TListviewitems in code and leave item appearance default as ListItem, the event fires only once also in my setup.
However, as soon as I change the ItemAppearance to 'custom' and, additionally, only make the Glyphbutton visible, it fires 2 times. It seems this is independant whether the actual itemobjects ( Text , Glyph , Accessory ) overlap or not ( so it's not that e.g. the glyphbutton click event is also passed once more into an itemclick event )
It's also not important where I click exactly, it always gets fired 2 times
Do you see the same behaviour ?
type
TForm1 = class(TForm)
Panel1: TPanel;
Button1: TButton;
ListView1: TListView;
procedure Button1Click(Sender: TObject);
procedure ListView1ItemClickEx(const Sender: TObject; ItemIndex: Integer;
const LocalClickPos: TPointF; const ItemObject: TListItemDrawable);
private
{ Private declarations }
public
{ Public declarations }
end;
procedure TForm1.Button1Click(Sender: TObject);
var
i : integer ;
LItem : TListViewItem ;
lListView : TListView ;
begin
// Here we should create the overal listview structure
// The visual appearance of items need to be done in updateobjects
// Only this one is always (re)triggered !!
lListView := ListView1;
lListView.BeginUpdate;
try
for i := 0 to 10 - 1 do begin
LItem := lListView.Items.Insert(i);
LItem.Text := 'Test' + I.ToString;
end;
finally
lListView.EndUpdate;
end;
end;
procedure TForm1.ListView1ItemClickEx(const Sender: TObject; ItemIndex: Integer;
const LocalClickPos: TPointF; const ItemObject: TListItemDrawable);
var
LItem : TListViewITem ;
begin
LITem := ListView1.Items[ItemIndex];
LItem.Tag := LItem.Tag + 1 ;
LItem.Text := LItem.Text + ' ' + LItem.Tag.ToString + 'clicks';
end;
...
object ListView1: TListView
ItemAppearanceClassName = 'TCustomizeItemObjects'
ItemEditAppearanceClassName = 'TCustomizeItemObjects'
HeaderAppearanceClassName = 'TListHeaderObjects'
FooterAppearanceClassName = 'TListHeaderObjects'
Align = Client
Size.Width = 640.000000000000000000
Size.Height = 439.000000000000000000
Size.PlatformDefault = False
TabOrder = 1
ItemAppearanceObjects.ItemObjects.Text.Width = 100.000000000000000000
ItemAppearanceObjects.ItemObjects.Text.Height = 44.000000000000000000
ItemAppearanceObjects.ItemObjects.Text.PlaceOffset.X = 168.000000000000000000
ItemAppearanceObjects.ItemObjects.Detail.Width = 230.000000000000000000
ItemAppearanceObjects.ItemObjects.Detail.Height = 44.000000000000000000
ItemAppearanceObjects.ItemObjects.Detail.PlaceOffset.X = 230.000000000000000000
ItemAppearanceObjects.ItemObjects.Accessory.Visible = True
ItemAppearanceObjects.ItemObjects.GlyphButton.Width = 31.000000000000000000
ItemAppearanceObjects.ItemObjects.GlyphButton.Height = 30.000000000000000000
ItemAppearanceObjects.ItemObjects.GlyphButton.Visible = True
ItemAppearanceObjects.ItemObjects.GlyphButton.PlaceOffset.X = 24.000000000000000000
OnItemClickEx = ListView1ItemClickEx
end
regards
Dirk
Its a dumb question maybe but I have a popup menu which is linked with many TTreeViewItems. The problem is that the TTreeView.Selected property never gets set on right click. The GetMousePos is prone to returning the next or the previous TTreeViewItem's coordinates. How can I get the Item which actually triggered the popup?
You can use OnPopup event of TPopupMenu like this:
procedure TForm7.PopupMenu1Popup(Sender: TObject);
var
aNode: TTreeNode;
p: TPoint;
begin
p := TreeView1.ScreenToClient(PopupMenu1.PopupPoint);
aNode := TreeView1.GetNodeAt(p.X, p.Y);
if aNode <> Nil then
caption := aNode.Text;
end;
seems TPopupMenu.PopupPoint returns (0,0) point when you click item in PopupMenu (In Delphi XE2, docwiki says that it is used internally to set position of menu, and seems it is set to 0 when menu dissapears).
so in this situation, seems to me, the easiest way is to handle TreeView.OnMouseDown where you can save reference to selected item, and then use it in Popup item event handler;
so, in the exmaple code below i've added FClickedItem : TTreeViewItem into the form class;
procedure TSampleForm.SampleTreeViewMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Single);
begin
if button = TMouseButton.mbRight then
FClickedItem := SampleTreeView.ItemByPoint(x,y)
else FClickedItem := nil;
end;
procedure TSampleForm.TestMenuItemClick(Sender: TObject);
begin
if Assigned(FClickedItem) then
ShowMessage(Format('Item `%s (%s)` was selected!', [FClickedItem.Text, FClickedItem.Name]))
else ShowMessage('there is nothing to show');
end;
UPDATE: i've just browsed the source code, private variable TPopupMenu.FPopupPoint (readonly property) is not used in implementation code, thats why it is always = (0,0)
I'm adding support for mouse wheel movement to a TScrollBox (using the FormMouseWheel procedure) and I need to determine if the mouse is inside the component.
Basically I need to determine if the mouse is inside the TScrollBox so that I then handle the scrolling code accordingly.
Any idea on how to do this?
EDIT: Here's the code (including the answer to this question) as it might help others:
procedure TForm1.FormMouseWheel(Sender: TObject;
Shift: TShiftState; WheelDelta: Integer; MousePos: TPoint;
var Handled: Boolean);
var
Msg: Cardinal;
Code: Cardinal;
I, ScrollLines: Integer;
ScrollBoxCursosPos: TPoint;
begin
//position of the mouse cursor related to TScrollBox
ScrollBoxCursosPos := ScrollBox1.ScreenToClient(Mouse.CursorPos);
if (PtInRect(ScrollBox1.ClientRect, ScrollBoxCursosPos)) then
begin
Handled := True;
If ssShift In Shift Then
msg := WM_HSCROLL
Else
msg := WM_VSCROLL;
If WheelDelta < 0 Then
code := SB_LINEDOWN
Else
code := SB_LINEUP;
ScrollLines:= Mouse.WheelScrollLines * 3;
for I:= 1 to ScrollLines do
ScrollBox1.Perform(Msg, Code, 0);
ScrollBox1.Perform(Msg, SB_ENDSCROLL, 0);
end;
end;
Mouse.CursorPos returns the mouse position in screen coordinates. You can convert this to "client" coordinates, ie coordinates relative to the control, by calling the control's ScreenToClient method.
So you'll have code something like this:
var
MyPoint : TPoint;
begin
MyPoint := ScrollBox1.ScreenToClient(Mouse.CursorPos);
if PtInRect(ScrollBox1.ClientRect, MyPoint) then
begin
// Mouse is inside the control, do something here
end;
end;
That will let you know if it's inside the control.
From the look of it you're implementing scrolling with the mousewheel? If so don't forget to call SystemParametersInfo with SPI_GETWHEELSCROLLLINES or possibly, if it's in your version of Delphi, Mouse.WheelScrollLines to find out how many lines to scroll per mousewheel increment. What that means to your app probably depends on what you've got in the scrollbox.
If you're planning to also implement middle-click-and-drag scrolling (I'm speculating here, this is well past what you asked about) you might want to get mouse events after the mouse has left the control or form until the user lets go the button, for example. If so, have a look at SetCapture and ReleaseCapture and this article. (That article uses those to see if the mouse is over a control (there, a form) although I think the code I wrote above is a better solution to that specific problem - point is they're handy for getting mouse information even when the mouse is not over your form or control.)
(Edit: I just noticed that Delphi 2010's TMouse has properties that wrap these API calls, WheelScrollLines and Capture. I'm not sure how recently they were added - I might just not have noticed them before - but on the assumption they're new-ish and because you don't say what version of Delphi you're using I'm leaving the above text and WinAPI references. If you're using a recent version have a look at the TMouse documentation.)
I am using the same method to scroll my scrollboxes using the mouse.
This is the event handler for the MouseWheel event of the form. It will scroll horizontally if you press shift key while scrolling:
procedure TForm1.FormMouseWheel(Sender: TObject; Shift: TShiftState; WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean);
var
Msg: Cardinal;
Code: Cardinal;
I, ScrollLines: Integer;
begin
if IsCoordinateOverControl(MousePos, ScrollBox1) then
begin
Handled := True;
If ssShift In Shift Then
Msg := WM_HSCROLL
Else
Msg := WM_VSCROLL;
If WheelDelta < 0 Then
Code := SB_LINEDOWN
Else
Code := SB_LINEUP;
ScrollLines := Mouse.WheelScrollLines * 3;
for I := 1 to ScrollLines do
ScrollBox1.Perform(Msg, Code, 0);
ScrollBox1.Perform(Msg, SB_ENDSCROLL, 0);
end;
end;
You can use this function to check if the screen coordinate of the mouse is over your control:
function IsCoordinateOverControl(screenCoordinate: TPoint; control: TControl): Boolean;
var
p: TPoint;
r: TRect;
begin
Result := False;
p := control.ScreenToClient(screenCoordinate);
r := Rect(0, 0, control.Width, control.Height);
if PtInRect(r, p) then
Result := True;
end;
My Delphi knowledge is a bit rusty, but shouldn't there be MouseEnter, MouseLeave events? A quick google showed this. Does that help you?
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