I have a frame with an TLMDDockPanel component as the parent, on the frame there is a TTreeView component:
unit devices;
...
Tmaster = class(TObject)
...
devTreeNode : ttreenode;
...
end;
...
end.
unit deviceTree;
...
TfrmDevTree = class(TFrame)
JvTreeView1: TTreeView;
...
end;
procedure TfrmDevTree.GetSlavesOnSelectedClick(Sender: TObject);
var
Node: TTreeNode;
begin
...
Node := self.JvTreeView1.Selected;
...
end;
...
end.
unit mainForm;
...
TfrmMain = class(TForm)
...
LMDDockSite1: TLMDDockSite;
LMDDockPanel_DevTree: TLMDDockPanel;
...
var
frmDevTree : TfrmDevTree;
...
procedure TfrmMain.FormCreate(Sender: TObject);
begin
...
frmDevTree := TfrmDevTree.Create(self);
frmDevTree.Parent := LMDDockPanel_DevTree;
...
end;
...
end.
At application start, i fill the 'Data' fields for all the nodes of JvTreeView1:
master := Tmaster.create;
Node.Data := master;
master.devtreenode := node; //I also save the treenode that is representing the master in JvTreeView1 into a master field.
The LMDDockPanel_DevTree dock panel is docked at the left of the docksite by default and there is no any problem while the dock panel sits there, but after undocking it, the obj. references for the treenodes are changing so the references stored in the masters (master.devtreenode) are no longer valid.
Can someone please explain why are the treenode references changing? How to avoid this? Should i refresh all the references stored in the masters every time i dock/undock the dock panel?
Thank You.
The reason it happens is because docking/undocking destroys and recreates the TreeView's HWND, which in turn destroys and recreates its node objects. A TreeView is designed to cache and restore the TTreeNode.Data values automatically during this recreation process, but it knows nothing about TMaster.DevTreeNode. As such, you need to detect when the nodes have been recreated so you can manually update their DevTreeNode values with the new TTreeNode pointers.
A TreeView has OnAddition and OnDeletion events that one would think would be ideal for this task. However, they are inconveniently NOT triggered during HWND recreation!
So you have two choices:
subclass the TreeView's WindowProc property to catch the recreation messages.
private
{ Private declarations }
DefTreeViewWndProc: TWndMethod;
procedure TreeViewWndProc(var Message: TMessage);
procedure TfrmDevTree.FormCreate(Sender: TObject);
begin
DefTreeViewWndProc := JvTreeView1.WindowProc;
JvTreeView1.WindowProc := TreeViewWndProc;
end;
procedure UpdateMasterDevNode(Node: TTreeNode; Destroying: Boolean);
var
Child: TTreeNode;
begin
if Node.Data <> nil then
begin
if Destroying then
TMaster(Node.Data).DevTreeNode := nil
else
TMaster(Node.Data).DevTreeNode := Node;
end;
Child := Node.getFirstChild;
while Child <> nil do
begin
UpdateMasterDevNode(Child, Destroying);
Child := Child.getNextSibling;
end;
end;
procedure UpdateMasterDevNodes(Nodes: TTreeNodes; Destroying: Boolean);
var
Node: TTreeNode;
begin
Node := Nodes.GetFirstNode;
while Node <> nil do
begin
UpdateMasterDevNode(Node, Destroying);
Node := Node.getNextSibling;
end;
end;
procedure TfrmDevTree.TreeViewWndProc(var Message: TMessage);
const
WM_UPDATEMASTERDEVNODES = WM_APP + 1;
begin
if Message.Msg = CM_RECREATEWND then
UpdateMasterDevNodes(JvTreeView1.Items, True);
DefTreeViewWndProc(Message);
if Message.Msg = WM_CREATE then
begin
// the cached nodes have not been recreated yet, so delay the DevTreeNode updates
PostMessage(TreeView1.Handle, WM_UPDATEMASTERDEVNODES, 0, 0)
end
else if Message.Msg = WM_UPDATEMASTERDEVNODES then
UpdateMasterDevNodes(JvTreeView1.Items, False);
end;
use an interceptor class to override the virtual CreateWnd() and DestroyWnd() methods.
type
TJvTreeView = class(JVCL.ListsAndTrees.Trees.TJvTreeView)
protected
procedure CreateWnd; override;
procedure DestroyWnd; override;
end;
TfrmDevTree = class(TForm)
JvTreeView1: TJvTreeView;
...
end;
procedure UpdateMasterDevNode(Node: TTreeNode; Destroying: Boolean);
var
Child: TTreeNode;
begin
if Node.Data <> nil then
begin
if Destroying then
TMaster(Node.Data).DevTreeNode := nil
else
TMaster(Node.Data).DevTreeNode := Node;
end;
Child := Node.getFirstChild;
while Child <> nil do
begin
UpdateMasterDevNode(Child, Destroying);
Child := Child.getNextSibling;
end;
end;
procedure UpdateMasterDevNodes(Nodes: TTreeNodes; Destroying: Boolean);
var
Node: TTreeNode;
begin
Node := Nodes.GetFirstNode;
while Node <> nil do
begin
UpdateMasterDevNode(Node, Destroying);
Node := Node.getNextSibling;
end;
end;
procedure TJvTreeView.CreateWnd;
begin
inherited;
UpdateMasterDevNodes(Items, False);
end;
procedure TTreeView.DestroyWnd;
begin
if csRecreating in ControlState then
UpdateMasterDevNodes(Items, True);
inherited;
end;
Either way, be sure that any code which uses TMaster.DevTreeNode checks for nil first before using the TTreeNode.
Related
Using some answers in StackOverflow I've created a searcheable TComboBox in Delphi. It works fine when you add it directly to a Form, but breaks as soon as you add it to a TPanel and I can't seem to figure out why.
Directly on the form:
After typing t:
Inside a panel:
After typing t:
Here is the component's code:
unit uSmartCombo;
interface
uses
Vcl.StdCtrls, Classes, Winapi.Messages, Controls;
type
TSmartComboBox = class(TComboBox)
private
FStoredItems: TStringList;
procedure FilterItems;
procedure CNCommand(var AMessage: TWMCommand); message CN_COMMAND;
procedure RedefineCombo;
procedure SetStoredItems(const Value: TStringList);
procedure StoredItemsChange(Sender: TObject);
protected
procedure KeyPress(var Key: Char); override;
procedure CloseUp; override;
procedure Loaded; override;
procedure DoExit; override;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
property StoredItems: TStringList read FStoredItems write SetStoredItems;
end;
procedure Register;
implementation
uses
SysUtils, Winapi.Windows, Vcl.Forms;
procedure Register;
begin
RegisterComponents('Standard', [TSmartComboBox]);
end;
constructor TSmartComboBox.Create(AOwner: TComponent);
begin
inherited;
FStoredItems := TStringList.Create;
FStoredItems.OnChange := StoredItemsChange;
end;
destructor TSmartComboBox.Destroy;
begin
FStoredItems.Free;
inherited;
end;
procedure TSmartComboBox.DoExit;
begin
inherited;
RedefineCombo;
end;
procedure TSmartComboBox.Loaded;
var LParent: TWinControl;
LPoint: TPoint;
begin
inherited;
if Items.Count > 0 then
FStoredItems.Assign(Items);
AutoComplete := False;
Style := csDropDownList;
// The ComboBox doesn't behave properly if the parent is not the form.
// Workaround to pull it from any parenting
//if not (Parent is TForm) then
//begin
// LParent := Parent;
// while (not (LParent is TForm)) and Assigned(LParent) do
// LParent := LParent.Parent;
// LPoint := ClientToParent(Point(0,0), LParent);
// Parent := LParent;
// Left := LPoint.X;
// Top := LPoint.Y;
// BringToFront;
//end;
end;
procedure TSmartComboBox.RedefineCombo;
var S: String;
begin
if Style = csDropDown then
begin
if ItemIndex <> -1 then
S := Items[ItemIndex];
Style := csDropDownList;
Items.Assign(FStoredItems);
if S <> '' then
ItemIndex := Items.IndexOf(S);
end;
end;
procedure TSmartComboBox.SetStoredItems(const Value: TStringList);
begin
if Assigned(FStoredItems) then
FStoredItems.Assign(Value)
else
FStoredItems := Value;
end;
procedure TSmartComboBox.StoredItemsChange(Sender: TObject);
begin
if Assigned(FStoredItems) then
begin
RedefineCombo;
Items.Assign(FStoredItems);
end;
end;
procedure TSmartComboBox.KeyPress(var Key: Char);
begin
if CharInSet(Key, ['a'..'z']) and not (Style = csDropDown) then
begin
DroppedDown := False;
Style := csDropDown;
end;
inherited;
if not (Ord(Key) in [13,27]) then
DroppedDown := True;
end;
procedure TSmartComboBox.CloseUp;
begin
if Style = csDropDown then
RedefineCombo;
inherited;
end;
procedure TSmartComboBox.CNCommand(var AMessage: TWMCommand);
begin
inherited;
if (AMessage.Ctl = Handle) and (AMessage.NotifyCode = CBN_EDITUPDATE) then
FilterItems;
end;
procedure TSmartComboBox.FilterItems;
var I: Integer;
Selection: TSelection;
begin
SendMessage(Handle, CB_GETEDITSEL, WPARAM(#Selection.StartPos), LPARAM(#Selection.EndPos));
Items.BeginUpdate;
Try
if Text <> '' then
begin
Items.Clear;
for I := 0 to FStoredItems.Count - 1 do
if (Pos(Uppercase(Text), Uppercase(FStoredItems[I])) > 0) then
Items.Add(FStoredItems[I]);
end
else
Items.Assign(FStoredItems);
Finally
Items.EndUpdate;
End;
SendMessage(Handle, CB_SETEDITSEL, 0, MakeLParam(Selection.StartPos, Selection.EndPos));
end;
end.
Any help in how I can proceed to figure out why this is happening would be greatly appreciated!
Edit 1:
After doing some extra debugging, I've noticed the messages being sent to the ComboBox differ from the ones inside the panel.
A CBN_EDITUPDATE is never sent, like #Sherlock70 mentioned in the comments, which makes the FilterItems procedure never trigger.
I've also noticed the form behaves strangely after using the ComboBox inside the panel, sometimes freezing and even not responding, like it gets stuck in a loop.
This unpredictable behavior has made me move away from this approach, and I'm probably going to take an alternate route to create a "searchable ComboBox".
Going to leave the question open if someone wants to figure it out and maybe even use the component.
I hope this will help someone in future even after 7 months of the question. Setting the style of a Combobox will destroy the window handle of that Combobox and create a new one. This means windows will free your control's Window Handle and create a new one.
You are setting your Combobx style while searching and this is wrong. Try removing Style := from your code and test it again you will get the same results for Combobox on a form and Combobox on a panel or other TWinControl. As you can see in the following code, setting Style will call RecreateWnd.
procedure TCustomComboBox.SetStyle(Value: TComboBoxStyle);
begin
if FStyle <> Value then
begin
FStyle := Value;
if Value = csSimple then
ControlStyle := ControlStyle - [csFixedHeight] else
ControlStyle := ControlStyle + [csFixedHeight];
RecreateWnd;
end;
end;
RecreateWnd will call DestroyHandle()
procedure TWinControl.CMRecreateWnd(var Message: TMessage);
var
WasFocused: Boolean;
begin
WasFocused := Focused;
DestroyHandle;
UpdateControlState;
if WasFocused and (FHandle <> 0) then Windows.SetFocus(FHandle);
end;
Then DestroyHandle will call DestroyWnd() which will call DestroyWindowHandle().
I want information from TADOQuery to be loaded into a TTreeView. For example, I want it to be loaded as Field1->Add in Table1 and as Field2->AddChild with buttonClick. But when I run the code, I am getting an error:
Access violation at adress 0043616B in module "TRV2.exe"
I'm making a mistake or something is missing. Can you guide me?
procedure TForm1.AddButtonClick(Sender: TObject);
var
t: Integer;
MyNode, Node : TTreeNode;
begin
MyNode := Node;
t := Node.AbsoluteIndex;
TreeView1.Items.Add(MyNode, ADOQuery1.FieldByName('CODE_NAME').AsString);
end;
procedure TForm1.AddChildButtonClick(Sender: TObject);
var
t: Integer;
MyNode, Node: TTreeNode;
begin
MyNode := Node;
t := Node.AbsoluteIndex;
TreeView1.Items.Add(MyNode, ADOQuery1.FieldByName('CODE_CHILD').AsString);
end;
procedure TForm1.FormCreate(Sender: TObject);
var
t: Integer;
MyNode, Node: TTreeNode;
begin
MyNode := Node;
t := Node.AbsoluteIndex;
ADOQuery1.Open;
end;
UPDATE: I want to get the whole table and update the TTreeView when I add new Add and Child to the database. With these codes (AddButtonClick and AddChildButtonClick) I can only import the first values into the TTreeView. I wonder if a loop is needed?
MyNode and Node are both local variables that you are not initializing to anything. Your AV is because you are trying to access an object that doesn't exist.
Try using a class member instead, where you initialize it with one button click, and then use it with the other button click, eg:
private
MyNode: TTreeNode;
...
procedure TForm1.AddButtonClick(Sender: TObject);
begin
MyNode := TreeView1.Items.Add(nil, ADOQuery1.FieldByName('CODE_NAME').AsString);
end;
procedure TForm1.AddChildButtonClick(Sender: TObject);
begin
if MyNode <> nil then
TreeView1.Items.AddChild(MyNode, ADOQuery1.FieldByName('CODE_CHILD').AsString);
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
ADOQuery1.Open;
end;
UPDATE: to iterate through multiple records in the query result, you need to call TADOQuery.Next() in a loop until TADOQuery.Eof is true.
The database was taken into treview with the following codes. There is something missing. Because Field1=Add and Field2=Child. Same Fields repeating.
procedure TForm1.AddButtonClick(Sender: TObject);
var
CurrentDeptID, RecordDeptID: Integer; RootNode, DeptNode: TTreeNode;
begin
CurrentDeptID := 0;
TreeView1.Items.Clear;
RootNode := TreeView1.Items.Add(DeptNode, 'CODE_NAME');
DeptNode := nil;
ADOQuery1.SQL.Text := 'Select * from Tablo1 where CODE_NAME= CODE_NAME';
ADOQuery1.Open;
try
ADOQuery1.First;
while not ADOQuery1.Eof do
begin
RecordDeptID := ADOQuery1.FieldByName('ID').AsInteger;
if ( DeptNode = nil) or (RecordDeptID <> CurrentDeptID) then
begin
DeptNode := TreeView1.Items.AddChild(RootNode,
ADOQuery1.FieldByName('CODE_NAME').AsString); //
CurrentDeptID := RecordDeptID;
end;
TreeView1.Items.AddChild(DeptNode,
ADOQuery1.FieldByName('CODE_CHILD').AsString);
ADOQuery1.Next;
end;
finally
ADOQuery1.close;
end;
end;
[https://i.stack.imgur.com/kNojV.jpg]
Blockquote
I use a custom listview component and I need it to have a popupmenu item "copy data to clipboard". If there is no assigned popup, I create one and add the menuitem, if there is already a menu assigned, add the item to the current popup. Tried to put the code in the constructor, but then I realized, that popupmenu is still not created or associated to my listview. So any idea when to create my default item?
constructor TMyListView.Create(AOwner: TComponent);
var
FpopupMenu: TPopupMenu;
begin
inherited;
.....
FPopUpMenuItem := TMenuItem.Create(self);
FPopUpMenuItem.Caption := 'Copy data to clipboard';
FPopUpMenuItem.OnClick := PopupMenuItemClick;
if assigned(PopupMenu) then begin
popupMenu.Items.Add(FPopUpMenuItem);
end
else begin
FpopupMenu := TPopupMenu.Create(self);
FpopupMenu.Items.Add(FPopUpMenuItem);
PopupMenu := FpopupMenu;
end;
...
end;
Override the virtual TControl.DoContextPopup() method, eg:
type
TMyListView = class(TListView)
protected
...
procedure DoContextPopup(MousePos: TPoint; var Handled: Boolean); override;
...
end;
procedure TMyListView.DoContextPopup(MousePos: TPoint; var Handled: Boolean);
var
LPopupMenu: TPopupMenu;
LItem: TMenuItem;
function IsSameEvent(const E1, E2: TNotifyEvent): Boolean;
begin
Result := (TMethod(E1).Code = TMethod(E2).Code) and
(TMethod(E1).Data = TMethod(E2).Data);
end;
begin
inherited DoContextPopup(MousePos, Handled);
if Handled then Exit;
LPopupMenu := PopupMenu;
if not Assigned(LPopupMenu) then
begin
LPopupMenu := TPopupMenu.Create(Self);
PopupMenu := LPopupMenu;
end;
for I := 0 to LPopupMenu.Items.Count-1 do
begin
LItem := LPopupMenu.Items[I];
if IsSameEvent(LItem.OnClick, PopupMenuItemClick) then
Exit;
end;
LItem := TMenuItem.Create(Self);
LItem.Caption := 'Copy data to clipboard';
LItem.OnClick := PopupMenuItemClick;
LPopupMenu.Items.Add(LItem);
end;
The accepted answer indeed works perfectly - unless you add keyboard shortcuts to your menu item. If you do, these won't work before the popup menu has been accessed in some other way, because the items will not have been created.
If you need shortcuts, it may therefore be preferable to move the code from DoContextPopup to Loaded. Most simply,
procedure Loaded; override;
...
procedure Loaded;
var
MI: TMenuItem;
ItemCovered: boolean;
i: integer;
begin
inherited;
if not Assigned(PopupMenu) then
PopupMenu:=TPopupMenu.Create(self);
ItemCovered:=false;
for i := 0 to PopupMenu.Items.Count-1 do
if IsSameEvent(PopupMenu.Items[I].OnClick, CopyDataToClipboardClick) then begin
ItemCovered:=true;
break;
end;
if not ItemCovered then begin
MI:=TMenuItem.Create(PopupMenu);
MI.Caption:='Copy data to clipboard';
MI.OnClick:=CopyDataToClipboardClick;
MI.ShortCut:=ShortCut(Ord('C'),[ssShift,ssCtrl]);
PopupMenu.Items.Add(MI);
end;
end;
This won't check for popup menus added on runtime, but probably serve most cases better.
this is the implementation for the treeview to have a check boxes in every nodes.
procedure TTreeView.CreateParams(var Params: TCreateParams);
begin
inherited;
Params.Style := Params.Style or TVS_CHECKBOXES;
end;
now, i want to get all the text of all checked nodes in treeview and append it in memo
Sending a TVM_GETITEM message to the handle of a TreeItem will retrieve the needed state information in a TTVItem record defined in CommCtrl, if is called with the correct mask and ItemID.
The procedure TreeView_GetItem doing this can be found (and copied) in CommCtrl.
So you just need to iterate over your treeview items an check if the state is checked.
{type
TTreeView = Class(ComCtrls.TTreeView)
procedure CreateParams(var Params: TCreateParams); override;
End;}
uses CommCtrl;
Function TreeNodeChecked(n:TTreenode):Boolean;
Const
TVIS_CHECKED = $2000;
var
Item: TTVItem;
begin
Item.mask := TVIF_STATE or TVIF_HANDLE;
Item.hItem := n.ItemId;
if Bool(SendMessage(n.Handle, TVM_GETITEM, 0, lParam(#Item))) then
Result := (Item.State and TVIS_CHECKED) = TVIS_CHECKED
else
Result := false;
end;
procedure TForm4.Button1Click(Sender: TObject);
var
i: Integer;
begin
ListBox1.Items.Clear;
for i := 0 to TV.Items.Count - 1 do
begin
if TreenodeChecked(TV.Items[i]) then
ListBox1.Items.Add(TV.Items[i].Text);
end;
end;
{ TTreeView }
procedure TTreeView.CreateParams(var Params: TCreateParams);
begin
inherited;
Params.Style := Params.Style or TVS_CHECKBOXES;
end;
As the title says, I'd like a component (say, a label) to be notified when it's parent (say, a panel) receives and loses focus. I wandered a bit in Delphi source, in hope of using TControl.Notify, but it's only used to notify child controls of some property changes like font and color. Any suggestions?
Whenever the active control in an application changes, a CM_FOCUSCHANGED message is broadcast to all controls. Simply intercept it, and act accordingly.
Also, I assumed that by when it's parent (say, a panel) receives and loses focus you mean whenever a (nested) child control on that parent/panel receives or loses focus.
type
TLabel = class(StdCtrls.TLabel)
private
function HasCommonParent(AControl: TWinControl): Boolean;
procedure CMFocusChanged(var Message: TCMFocusChanged);
message CM_FOCUSCHANGED;
end;
procedure TLabel.CMFocusChanged(var Message: TCMFocusChanged);
const
FontStyles: array[Boolean] of TFontStyles = ([], [fsBold]);
begin
inherited;
Font.Style := FontStyles[HasCommonParent(Message.Sender)];
end;
function TLabel.HasCommonParent(AControl: TWinControl): Boolean;
begin
Result := False;
while AControl <> nil do
begin
if AControl = Parent then
begin
Result := True;
Break;
end;
AControl := AControl.Parent;
end;
end;
If you don't like to subclass TJvGradientHeader, then it is possible to design this generically by the use of Screen.OnActiveControlChange:
TForm1 = class(TForm)
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
FHeaders: TList;
procedure ActiveControlChanged(Sender: TObject);
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
FHeaders := TList.Create;
FHeaders.Add(Label1);
FHeaders.Add(Label2);
Screen.OnActiveControlChange := ActiveControlChanged;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
FHeaders.Free;
end;
function HasCommonParent(AControl: TWinControl; AMatch: TControl): Boolean;
begin
Result := False;
while AControl <> nil do
begin
if AControl = AMatch.Parent then
begin
Result := True;
Break;
end;
AControl := AControl.Parent;
end;
end;
procedure TForm1.ActiveControlChanged(Sender: TObject);
const
FontStyles: array[Boolean] of TFontStyles = ([], [fsBold]);
var
I: Integer;
begin
for I := 0 to FHeaders.Count - 1 do
TLabel(FHeaders[I]).Font.Style :=
FontStyles[HasCommonParent(Screen.ActiveControl, TLabel(FHeaders[I]))];
end;
Note that I chose TLabel to demonstrate this works also for TControl derivatives.