Graphical Hints – Image in Virtual Treeview Node Hint (Extending TVirtualTreeHintWindow)

delphi-vtw-pdf-hint-graphics-previewIn a Delphi application, the THintWindow class implements the small pop-up window that appears over a control at run time, when the control has its ShowHint property set to True (and has a value assigned to the Hint property).
The implementation of the THintWindow (at least what gets displayed by it, not how) is rather simple: it will display whatever string value is assigned for the Hint property of a control. If you want more control over what gets displayed by the hint window, and how, and when the hint window will popup – you can create your own version by extending the THintWindow class.
To use your own hint you would assign your own class to the global HintWindowClass variable at application start-up time, so that the new hint window type is used for hints. You can even fine tune the display of the hint window just before it pops-up using the OnHint and OnShowHint events of the TApplicationEvents.

That’s all nice and clear. But, what if the control you are using is Virtual Treeview which has its own version of the hint window implemented in TVirtualTreeHintWindow? What if you want to extend this by including some graphics along with the text that the hint window displays? Further, what if you need to have different hint values for every node displayed by the tree?
Of course, you are not working on a new application – all logic is already there, lots of methods and events already implemented.

The answers: here’s how to simply extend both the TVirtualTreeHintWindow and the TVirtualStringTree to add some node specific graphics to the hint window displaying node specific hints.

Here’s the actual real world feature I wanted to quickly have implemented: my application uses Virtual Treeview to present a folder structure containing PDF documents to the user – each node presents either a folder or a PDF file. Hints are node specific and for a file-type node some PDF values get displayed to the user (file name, PDF Title, PDF version, and alike). I wanted also to include a simple 1st page preview (as image) of the PDF file in the hint window.

Download Sample Project Source Code.

Hints in Virtual Treeview (TVirtualStringTree)

For the Virtual Treeview to display hints (node or control specific) the Hint property must be set to true. Next, there’s the HintMode property determining what the hint actually displays (single hint for control or node specific hint value and some more values). Also, there’s the HintAnimation property which determines the kind of animation when a hint is activated. Or, as those are implemented in the source code:

  TVTHintMode = (
    // show the hint of the control
    hmDefault,            
    // show node specific hint string returned by the application
    hmHint,  
    // same as hmHint but show the control's hint if no node is concerned
    hmHintAndDefault,    
    // show the text of the node if it isn't already fully shown
    hmTooltip);

  // Determines the kind of animation when a hint is activated.
  THintAnimationType = (
    // no animation at all, just display hint/tooltip
    hatNone,                
    // fade in the hint/tooltip, like in Windows 2000
    hatFade,                
    // slide in the hint/tooltip, like in Windows 98     
    hatSlide,                
    // use what the system is using (slide for Win9x, slide/fade for Win2K+, depends on settings)
    hatSystemDefault);

Before providing more details, here’s the record type each node stores internally (note: you need some basic knowledge on Virtual Treeview to follow what follows):

  TTreeNodeData = record
  private
    fPreview : TJpegImage;
    function GetPreview: TJpegImage;
  public
    Name: string;
    RelativeName : string;
    IsFolder : boolean;
    property Preview : TJpegImage read GetPreview;
  end;
  PTreeNodeData = ^TTreeNodeData;

As stated, each node in my tree either presents a folder or a (PDF) file. Hence the record field names: Name is the full file/folder name; RelativeName is the path relative to the root directory selected; IsFolder stores if the node presents a folder or a file. Finally the Preview property will lazily get the graphics of the first page of the PDF file (more on that later below).

And here’s what is set in the form’s OnCreate:

  tree.NodeDataSize := SizeOf(TTreeNodeData);

  tree.HintMode := hmHint;
  tree.ShowHint := true;

To have node specific hints you need to implement the OnGetHint event:

procedure TMainForm.treeGetHint(Sender: TBaseVirtualTree;
  Node: PVirtualNode; Column: TColumnIndex;
  var LineBreakStyle: TVTTooltipLineBreakStyle; var HintText: string);
var
  data: PTreeNodeData;
begin
  data := Sender.GetNodeData(Node);

  HintText := data.Name; //just display full name / file path
end; 

All set, now the question is how do I get some graphics also displayed by the hint window.

Fake Extending (i.e. Intercepting) the TVirtualTreeHintWindow, TVirtualStringTree (Plus Overcoming Some Issues)

First step is to have my own version of the TVirtualTreeHintWindow class, of course by extending what’s already provided. If you’ve ever done some custom THintWindow implementation you know you need to override at least two methods: CalcHintRect and Paint. The CalcHintRect gets the dimensions of the hint window rectangle required to display the hint. Paint does the actual painting (of the hint text).

The second step (actually first) would be to tell to the Tree to use my own version of the TVirtualTreeHintWindow class.

And I hit the wall first time! There’s no public property or method I can call on Virtual Treeview to have it use my own version of TVirtualTreeHintWindow. There IS a method GetHintWindowClass and a comment next to it saying: “Returns the default hint window class used for the tree. Descendants can override it to use their own classes.

Ah! I have to override the entire TVirtualStringTree (i.e. extend / make my own version) to have this. Me no like. I have it (the tree) in many forms in my application and I do not want to change all this. But, Delphi has a solution called “intercepting classes“.
So, I’ve intercepted the TVirtualStringTree, and have it as:

  TVirtualStringTree = class(VirtualTrees.TVirtualStringTree)
  private
    fHintModeImage: boolean;
    procedure SetHintModeImage(const Value: boolean);
  public
    constructor Create(AOwner: TComponent); override;
  public
    function GetHintWindowClass: THintWindowClass; override;
    property HintModeImage : boolean read fHintModeImage write SetHintModeImage;
  end;

The newly added property HintModeImage is used to decide if the node hint should also display the image – will the tree use its own TVirtualTreeHintWindow class or my extended version.

Ok, back to extending the TVirtualTreeHintWindow class. Looking into the implementation (long live open source) I see that the protected Paint method (which I can override) is used only when HintAnimation property is “hatNone”, otherwise the class does all the rendering inside the private (cannot override) AnimationCallback method (which calls InternalPaint I also cannot override). Second wall, this time a harder one!

If I want to go cheap I must have no animation on hints – well, ok, I can leave with that (and the users of the application will have to).

Ok, so here’s the simpler part, implementation of my intercepted TVirtualStringTree:

constructor TVirtualStringTree.Create(AOwner: TComponent);
begin
  fHintModeImage := false;
  inherited;
end;

function TVirtualStringTree.GetHintWindowClass: THintWindowClass;
begin
  if HintModeImage then
    result := TVirtualTreeHintWindowEx
  else
    result := inherited; //use what tree uses
end;

procedure TVirtualStringTree.SetHintModeImage(const Value: boolean);
begin
  fHintModeImage := Value;

  if fHintModeImage then
    HintAnimation := hatNone //so Paint procedure is called and can be overridden
  else
    HintAnimation := hatSystemDefault; //default
end;

Finally, here’s how the implementation of my intercepted version of TVirtualTreeHintWindow looks:

function TVirtualTreeHintWindowEx.CalcHintRect(MaxWidth: Integer;  const AHint: string; AData: TCustomData): TRect;
var
  r : TRect;
  hd : TVTHintData;
  jpg : TJpegImage;
  nodeData : PTreeNodeData;
begin
  r := inherited;

  hd := PVTHintData(AData)^;

  if Assigned(hd.Tree) AND Assigned(hd.Node) AND (HintData.Tree is TVirtualStringTree) AND (TVirtualStringTree(HintData.Tree).HintModeImage) then
  begin
    nodeData := HintData.Tree.GetNodeData(hd.Node);

    jpg := nodeData.Preview;

    if Assigned(jpg) AND (NOT jpg.Empty) then
    begin
      r.Height := r.Height + jpg.Height + 4;
      if r.Width < jpg.Width + 4 then r.Width := jpg.Width + 4;
    end;
  end;

  result :=  r;
end;

procedure TVirtualTreeHintWindowEx.Paint; //called only if HintAnimation = hatNone
var
  jpg : TJpegImage;
  nodeData : PTreeNodeData;
begin
  InternalPaint(0, 0);

  if Assigned(HintData.Tree) AND Assigned(HintData.Node) AND (HintData.Tree is TVirtualStringTree) AND (TVirtualStringTree(HintData.Tree).HintModeImage) then
  begin
    nodeData := HintData.Tree.GetNodeData(HintData.Node);

    jpg := nodeData.Preview;

   if Assigned(jpg) AND (NOT jpg.Empty) then Canvas.Draw(2, -2 + Height - jpg.Height, jpg);
  end;
end;

The CalcHintRect first calculates the size of the image (PDF first page preview if you remember), Paint than paints it to the hint window. A simple AND’d IF is used to ensure the tree is ok, the node is here, the data is here etc…

The PDF 1st Page Preview – Using Debenu PDF Library

I guess you are asking yourself from the beginning: how do you get the preview of a page of a PDF file. In short: I’m using Debenu Quick PDF Library.

Without too many details about the Debenu Quick PDF Library (at least for now), here’s how easy is to get the image of a page of a PDF:

function TTreeNodeData.GetPreview: TJpegImage;
var
  fh, pr : integer;
  ms : TMemoryStream;
  bmp : TBitmap;
  pdfFileName : string;
begin
  if fPreview = nil then fPreview := TJpegImage.Create;

  if (NOT IsFolder) AND fPreview.Empty then
  begin
    pdfFileName := self.Name;

    //<a href="http://www.debenu.com/products/development/debenu-pdf-library/" target="_blank">http://www.debenu.com/products/development/debenu-pdf-library/</a>
    with TQuickPDF.Create do
    try
      fh := DAOpenFileReadOnly(pdfFileName, '');
      if fh <> 0 then
      try
        pr := DAFindPage(fh, 1);
        if pr <> 0 then
        begin
          ms := TMemoryStream.Create;
          try
            DARenderPageToStream(fh, pr, 1, 24, ms);
            ms.Seek(0,0);

            fPreview.LoadFromStream(ms);
          finally
            ms.Free;
          end;
        end;
      finally
        DACloseFile(fh);
      end;
    finally
      Free;
    end;
  end;

  result := fPreview;
end;

Destroying/Freeing The Preview Object in Record (When No Destructors)

Note that my TTreeNodeData uses some advanced features like properties and methods in records. The Preview property is an instance of the TJpegImage. The Preview must be freed when no longer needed, and records cannot have destructors. So the question is when and where to free the image? Again, no problem, since the Tree handles record instances it exposes an event where you can free whatever object is assigned to a node when the node is destroyed, the OnFreeNode event:

procedure TMainForm.treeFreeNode(Sender: TBaseVirtualTree; Node: PVirtualNode);
var
  data: PTreeNodeData;
begin
  data := Sender.GetNodeData(Node);
  if Assigned(data.fPreview) then data.fPreview.Free;
  Finalize(data^);
end;

And that’s all folks. Some Delphi magic, some exploring of inner workings of controls you are using and the sky is the limit 🙂

Comments? More than welcome!

2 thoughts on “Graphical Hints – Image in Virtual Treeview Node Hint (Extending TVirtualTreeHintWindow)

  1. Roman

    Dear Zarko,

    did you check out the latest VST version? I’m asking because I added some changes from de Novo software to it. These changes allow you to write a custom OnDrawHint event handler so that it shouldn’t be necessary to use an intercepting class.


    Thanks,

    Roman

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *