1

I am using Delphi XE3 and Virtual TreeView.

I want to use Virtual TreeView to implement a tree, when clicking "Start" button, the program will search all files and folders under a drive recursively, then add them one by one to the tree, just like Windows Explorer. Moreover, there should be a number indicating number of files and subfolders under a folder, using static text like this:

VirtualTreeView - different color of text in the same node

In my impelemention, I find sometimes the number is not updated correctly.

Therefore, I think the following way to refresh the node, whenever the number of files/subfolders are changed:

  1. Call tvItems.Change(PNode) to update the node.

  2. Call tvItems.InvalidateNode(PNode).

  3. Call tvItems.RepaintNode(PNode).

  4. Call tvItems.UpdateAction.

However, 1 is protected method that cannot be invoked. 2 and 3 are both OK but don't know which is better for updating. 4 is not documented and don't know how to call it.

alancc
  • 487
  • 2
  • 24
  • 68

1 Answers1

7

The fundamental idea is that if something behind the scenes changes, the tree need to repainted. This means that the next time the tree draws itself, it will use the new underlying values.

If your tree is sitting there on screen:

enter image description here

You can simply call:

tvItems.Invalidate;

This tells Windows that the entire tree is now "invalid" and needs to be redrawn. I can represent this "invalid" region that will be updated the next time the tree paints itself:

enter image description here

And that's fine, correct, and will work perfectly adequately.

Performance improvements

A lot of times it's perfectly reasonable to force the entire tree to repaint all of itself.

But it's also possible to begin to optimize things. If you know that only a certain region of a Windows control is "invalid", then you could just invalidate that portion.

The Windows function to do that is InvalidateRect:

InvalidateRect(tvItems.Handle, Rect(13, 18, 30, 38), True); 

That will invalidate a 30x38 square at 13,18:

enter image description here

In fact all TWinControl.Invalidate is doing is turning around and calling the Windows InvalidateRect function:

//true means to also trigger an erase of the background
InvalidateRect(Self.Handle, Self.BoundsRect, True); 

There may not be much use for such a strange rectangle to be invalidated. But you can probably imagine other rectangles you'd like to be invalidated.

Invalidating nodes

Windows doesn't realize it, but your control represents a tree, and the tree and nodes. And there might be times when you want to invalidate the rectangle of a "node":

enter image description here

Rather than you having to figure out the coordinates and size of a node, the TVirtualTree already provides you a handy method to invalidate a node:

function InvalidateNode(Node: PVirtualNode): TRect; virtual;
// Initiates repaint of the given node and returns the just invalidated rectangle.

so:

tvItems.InvalidateNode(someNode);

It also provides a method to invalidate a node and all its children:

procedure TBaseVirtualTree.InvalidateChildren(Node: PVirtualNode; Recursive: Boolean);
// Invalidates Node and its immediate children.
// If Recursive is True then all grandchildren are invalidated as well.
// The node itself is initialized if necessary and its child nodes are created (and initialized too if
// Recursive is True).

This is useful when your tree has children:

enter image description here

You can invalidate the parent node and all the children that now need to be updated with the new numbers:

tvItems.InvalidateChildren(someNode, True);

And other helper methods

The Virtual Tree has other helpful methods that:

  • get a certain interesting rectangle to invalidate
  • call Windows.InvalidateRect

That is:

  • InvalidateToBottom(Node: PVirtualNode); Initiates repaint of client area starting at given node. If this node is not visible or not yet initialized then nothing happens.
  • TBaseVirtualTree.InvalidateColumn(Column: TColumnIndex); Invalidates the client area part of a column.

Invalidating vs Repaint

Your other question is in regards to the confusion over the difference between:

  • invalidating
  • repainting

When you invalidate a rectangle (e.g. an entire form, an entire control, or some smaller rectangle such as a node, node and its children, or a column) you are telling Windows that it needs to ask the control to paint self. That is:

the pixels on screen are now invalid and must be repainted

This will happen the next time the tree is asked to paint itself. Windows is message-based. And as your application runs, it processes messages, including a WM_PAINT message. When the VirtualTree gets a WM_PAINT message, it paints the portions it was asked to repaint.

This means that for any and all painting to happen, you have to be processing messages - i.e. you have to let your program go "idle".

If you sit there is a busy loop, never letting your code exit:

procedure TForm1.Button1Click(Sender: TObject);
begin
   while (true) do 
   begin
      tvItems.Invalidate;
      Sleep(100);
   end;
end;

The loop never ends, and the tree is never given a chance to actually paint itself.

Delphi's horrible Repaint hack

Delphi has a horrible hack that forces a control to be painted.

  • it pretends that it's Windows asking a control to paint itself
  • and then jumps directly to the control's paint routine

This means that the control will paint itself, even though it's not received a WM_PAINT message from Windows - it just does the scribbling.

procedure TForm1.Button1Click(Sender: TObject);
begin
   while (true) do 
   begin
      tvItems.Repaint; //force the control to repaint itself
      Sleep(100);
   end;
end;

It's an ugly hack because:

  • in the first case our code wasn't processing Windows messages
  • in the revised case we're still not doing the right thing, and trying to use a hammer to screw in a screw

The correct solution in these cases is to have a background thread. And have the background thread signal the main application that it needs to invalidate the tree. The main program will then receive a WM_PAINT message and draw itself as normal.

Ian Boyd
  • 246,734
  • 253
  • 869
  • 1,219
  • Thank you very much for your great and detailed answer! It is really helpful! – alancc Dec 05 '19 at 09:51
  • If I understand correctly, 1. you recommend to use Invalidate instead of Repaint. 2. tvItems.InvalidateNode and tvItems.RepaintNode work similary as tvItems.Invalidate and tvItems.Repaint, except that they are for specific node. tvItems.RepaintNode will force the node to repaint instead of posting a WM_PAINT message. – alancc Dec 05 '19 at 09:58
  • @lanBoyd, Thank you very much – alancc Dec 11 '19 at 23:21