Search code examples
delphitreeviewdockingobject-reference

Object reference to treeview items are changing after undock?


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.


Solution

  • 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:

    1. 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;
      
    2. 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.