unit DrawTreeDemo; {$MODE Delphi} // Virtual Treeview sample form demonstrating following features: // - General use of TVirtualDrawTree. // - Use of vertical node image alignment. // - Effective use of node initialization on demand to load images. // Written by Mike Lischke. // // Note: define the symbol "GraphicEx" if you have my GraphicEx library // available (see http://www.delphi-gems.com) which allows to load // more image formats into the application. // Otherwise disable the conditional symbol to compile this demo. interface uses {$ifdef Windows} Windows, {$endif} LCLIntf, delphicompat, LCLType, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, VirtualTrees, StdCtrls, ComCtrls, shlobjext, LResources; type TDrawTreeForm = class(TForm) VDT1: TVirtualDrawTree; Label7: TLabel; SystemImages: TImageList; Label1: TLabel; TrackBar1: TTrackBar; Label3: TLabel; procedure FormCreate(Sender: TObject); procedure VDT1CompareNodes(Sender: TBaseVirtualTree; Node1, Node2: PVirtualNode; Column: TColumnIndex; var Result: Integer); procedure VDT1DrawHint(Sender: TBaseVirtualTree; Canvas: TCanvas; Node: PVirtualNode; R: TRect; Column: TColumnIndex); procedure VDT1DrawNode(Sender: TBaseVirtualTree; const PaintInfo: TVTPaintInfo); procedure VDT1FreeNode(Sender: TBaseVirtualTree; Node: PVirtualNode); procedure VDT1GetHintSize(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; var R: TRect); procedure VDT1GetImageIndex(Sender: TBaseVirtualTree; Node: PVirtualNode; Kind: TVTImageKind; Column: TColumnIndex; var Ghosted: Boolean; var Index: Integer); procedure VDT1GetNodeWidth(Sender: TBaseVirtualTree; Canvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex; var NodeWidth: Integer); procedure VDT1HeaderClick(Sender: TVTHeader; Column: TColumnIndex; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); procedure VDT1InitChildren(Sender: TBaseVirtualTree; Node: PVirtualNode; var ChildCount: Cardinal); procedure VDT1InitNode(Sender: TBaseVirtualTree; ParentNode, Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates); procedure TrackBar1Change(Sender: TObject); procedure VDT1StateChange(Sender: TBaseVirtualTree; Enter, Leave: TVirtualTreeStates); private FThumbSize: Integer; FExtensionsInitialized: Boolean; FExtensionList: TStringList; FDriveStrings: string; function CanDisplay(const Name: String): Boolean; function GetDriveString(Index: Integer): string; function ReadAttributes(const Name: String): Cardinal; procedure RescaleImage(Source, Target: TBitmap); end; var DrawTreeForm: TDrawTreeForm; //---------------------------------------------------------------------------------------------------------------------- implementation uses States; //---------------------------------------------------------------------------------------------------------------------- type // This data record contains all necessary information about a particular file system object. // This can either be a folder (virtual or real) or an image file. PShellObjectData = ^TShellObjectData; TShellObjectData = record FullPath, Display: String; Attributes: Cardinal; OpenIndex, CloseIndex: Integer; // image indices into the system image list Image: TBitmap; Properties: String; // some image properties, preformatted end; //---------------------------------------------------------------------------------------------------------------------- function HasChildren(const Folder: string): Boolean; // Determines whether folder contains other file objects. var SR: TSearchRec; begin Result := FindFirst(IncludeTrailingPathDelimiter(Folder) + {$ifdef Windows}'*.*'{$else}'*'{$endif}, faAnyFile, SR) = 0; if Result then FindClose(SR); end; //---------------------------------------------------------------------------------------------------------------------- function GetIconIndex(Name: string; Flags: Cardinal): Integer; // Returns the index of the system icon for the given file object. { var SFI: TSHFileInfo; } begin Result := -1; //todo { if SHGetFileInfo(PChar(Name), 0, SFI, SizeOf(TSHFileInfo), Flags) = 0 then Result := -1 else Result := SFI.iIcon; } end; //---------------------------------------------------------------------------------------------------------------------- procedure GetOpenAndClosedIcons(Name: string; var Open, Closed: Integer); begin //todo Closed := 0; Open := 0; { Closed := GetIconIndex(Name, SHGFI_SYSICONINDEX or SHGFI_SMALLICON); Open := GetIconIndex(Name, SHGFI_SYSICONINDEX or SHGFI_SMALLICON or SHGFI_OPENICON); } end; //----------------- TDrawTreeForm -------------------------------------------------------------------------------------- procedure GetLogicalDrivesInfo(var DriveStrings: String; var DriveCount: Integer); var I, BufferSize, DriveMap, Mask: Cardinal; begin {$ifdef Windows} DriveCount := 0; DriveMap := GetLogicalDrives; Mask := 1; for I := 0 to 25 do begin if (DriveMap and Mask) <> 0 then Inc(DriveCount); Mask := Mask shl 1; end; BufferSize := GetLogicalDriveStrings(0, nil); SetLength(DriveStrings, BufferSize); GetLogicalDriveStrings(BufferSize, PChar(DriveStrings)); {$else} DriveCount := 1; DriveStrings := '/'; {$endif} end; procedure TDrawTreeForm.FormCreate(Sender: TObject); var //SFI: TSHFileInfo; I, Count: Integer; begin VDT1.NodeDataSize := SizeOf(TShellObjectData); GetLogicalDrivesInfo(FDriveStrings,Count); VDT1.RootNodeCount := Count; //todo { SystemImages.Handle := SHGetFileInfo('', 0, SFI, SizeOf(SFI), SHGFI_SYSICONINDEX or SHGFI_SMALLICON); SystemImages.ShareImages := True; } FThumbSize := 200; end; //---------------------------------------------------------------------------------------------------------------------- function TDrawTreeForm.CanDisplay(const Name: string): Boolean; // Determines whether the given file is one we can display in the image tree. var Ext: string; I: Integer; begin if not FExtensionsInitialized then begin FExtensionsInitialized := True; FExtensionList := TStringList.Create; {$ifdef GraphicEx} FileFormatList.GetExtensionList(FExtensionList); for I := 0 to FExtensionList.Count - 1 do FExtensionList[I] := '.' + FExtensionList[I]; {$else} // GraphicEx is not used so add some default extensions with FExtensionList do begin Add('.bmp'); Add('.ico'); Add('.jpg'); Add('.jpeg'); //Add('.wmf'); //Add('.emf'); end; {$endif} FExtensionList.Sort; end; Ext := ExtractFileExt(Name); Result := FExtensionList.Find(Ext, I); end; //---------------------------------------------------------------------------------------------------------------------- function TDrawTreeForm.GetDriveString(Index: Integer): string; // Helper method to extract a sub string (given by Index) from FDriveStrings. var Head, Tail: PChar; begin Head := PChar(FDriveStrings); Result := ''; repeat Tail := Head; while Tail^ <> #0 do Inc(Tail); if Index = 0 then begin SetString(Result, Head, Tail - Head); Break; end; Dec(Index); Head := Tail + 1; until Head^ = #0; end; //---------------------------------------------------------------------------------------------------------------------- function TDrawTreeForm.ReadAttributes(const Name: String): Cardinal; // Determines the attributes of the given shell object (file, folder). const SFGAO_CONTENTSMASK = $F0000000; // This value is wrongly defined in ShlObj. //var //Desktop: IShellFolder; { Eaten: Cardinal; PIDL: PItemIDList; Malloc: IMalloc; } begin Result := 0; //todo { // Get the root folder of the shell name space. SHGetDesktopFolder(Desktop); // While parsing the name also the shell object's attributes are determined. // These is what we are really interested in. Result := SFGAO_DISPLAYATTRMASK or SFGAO_CONTENTSMASK or SFGAO_COMPRESSED; Desktop.ParseDisplayName(0, nil, PWideChar(Name), Eaten, PIDL, Result); // Don't forget to free the returned PIDL. The shell folder is released automatically. SHGetMalloc(Malloc); Malloc.Free(PIDL); } end; //---------------------------------------------------------------------------------------------------------------------- procedure TDrawTreeForm.RescaleImage(Source, Target: TBitmap); // if source is in at least one dimension larger than the thumb size then // rescale source but keep aspect ratio var NewWidth, NewHeight: Integer; begin if (Source.Width > FThumbSize) or (Source.Height > FThumbSize) then begin if Source.Width > Source.Height then begin NewWidth := FThumbSize; NewHeight := Round(FThumbSize * Source.Height / Source.Width); end else begin NewHeight := FThumbSize; NewWidth := Round(FThumbSize * Source.Width / Source.Height); end; Target.Width := NewWidth; Target.Height := NewHeight; SetStretchBltMode(Target.Canvas.Handle, HALFTONE); StretchBlt(Target.Canvas.Handle, 0, 0, NewWidth, NewHeight, Source.Canvas.Handle, 0, 0, Source.Width, Source.Height, SRCCOPY); end else Target.Assign(Source); end; //---------------------------------------------------------------------------------------------------------------------- procedure TDrawTreeForm.VDT1InitNode(Sender: TBaseVirtualTree; ParentNode, Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates); var Data: PShellObjectData; Picture: TPicture; begin Data := Sender.GetNodeData(Node); if ParentNode = nil then begin // top level node, initialize first enumeration Data.FullPath := GetDriveString(Node.Index); Data.Display := Data.FullPath; GetOpenAndClosedIcons(Data.FullPath, Data.OpenIndex, Data.CloseIndex); end else begin Picture := TPicture.Create; Data.Display := ExtractFileName(ExcludeTrailingPathDelimiter(Data.FullPath)); if (Data.Attributes and SFGAO_FOLDER) = 0 then try try Data.Image := TBitmap.Create; Picture.LoadFromFile(Data.FullPath); if not (Picture.Graphic is TBitmap) then begin // Some extra steps needed to keep non TBitmap descentants alive when // scaling. This is needed because when accessing Picture.Bitmap all // non-TBitmap content will simply be erased (definitly the wrong // action, but we can't do anything to prevent this). Hence we // must explicitly draw the graphic to a bitmap. with Data.Image do begin Width := Picture.Width; Height := Picture.Height; Canvas.Draw(0, 0, Picture.Graphic); end; Picture.Bitmap.Assign(Data.Image); end; RescaleImage(Picture.Bitmap, Data.Image); // Collect some additional image properties. Data.Properties := Data.Properties + Format('%d x %d pixels', [Picture.Width, Picture.Height]); case Picture.Bitmap.PixelFormat of pf1bit: Data.Properties := Data.Properties + ', 2 colors'; pf4bit: Data.Properties := Data.Properties + ', 16 colors'; pf8bit: Data.Properties := Data.Properties + ', 256 colors'; pf15bit: Data.Properties := Data.Properties + ', 32K colors'; pf16bit: Data.Properties := Data.Properties + ', 64K colors'; pf24bit: Data.Properties := Data.Properties + ', 16M colors'; pf32bit: Data.Properties := Data.Properties + ', 16M+ colors'; end; if Cardinal(Data.Image.Height) + 4 > TVirtualDrawTree(Sender).DefaultNodeHeight then Sender.NodeHeight[Node] := Data.Image.Height + 4; except Data.Image.Free; Data.Image := nil; end; finally Picture.Free; end; end; //todo //Data.Attributes := ReadAttributes(Data.FullPath); //if ((Data.Attributes and SFGAO_HASSUBFOLDER) <> 0) or // (((Data.Attributes and SFGAO_FOLDER) <> 0) and HasChildren(Data.FullPath)) then if HasChildren(Data.FullPath) then Include(InitialStates, ivsHasChildren); end; //---------------------------------------------------------------------------------------------------------------------- procedure TDrawTreeForm.VDT1FreeNode(Sender: TBaseVirtualTree; Node: PVirtualNode); var Data: PShellObjectData; begin Data := Sender.GetNodeData(Node); Data.Image.Free; Finalize(Data^); // Clear string data. end; //---------------------------------------------------------------------------------------------------------------------- procedure TDrawTreeForm.VDT1DrawNode(Sender: TBaseVirtualTree; const PaintInfo: TVTPaintInfo); // This is the main paint routine for a node in a draw tree. There is nothing special here. Demonstrating the // specific features of a draw tree (compared to the string tree) is a bit difficult, since the only difference is // that the draw tree does not handle node content (captions in the case of the string tree). var Data: PShellObjectData; X: Integer; S: String; R: TRect; begin with Sender as TVirtualDrawTree, PaintInfo do begin Data := Sender.GetNodeData(Node); if (Column = FocusedColumn) and (Node = FocusedNode) then Canvas.Font.Color := clHighlightText else if (Data.Attributes and SFGAO_COMPRESSED) <> 0 then Canvas.Font.Color := clBlue else Canvas.Font.Color := clWindowText; SetBKMode(Canvas.Handle, TRANSPARENT); R := ContentRect; InflateRect(R, -TextMargin, 0); Dec(R.Right); Dec(R.Bottom); S := ''; case Column of 0, 2: begin if Column = 2 then begin if Assigned(Data.Image) then S:= Data.Properties; end else S := Data.Display; if Length(S) > 0 then begin with R do begin if (NodeWidth - 2 * Margin) > (Right - Left) then S := ShortenString(Canvas.Handle, S, Right - Left); end; DrawText(Canvas.Handle, PChar(S), Length(S), R, DT_TOP or DT_LEFT or DT_VCENTER or DT_SINGLELINE); end; end; 1: begin if Assigned(Data.Image) then begin X := ContentRect.Left + (VDT1.Header.Columns[1].Width - Data.Image.Width - Margin) div 2; BitBlt(Canvas.Handle, X, ContentRect.Top + 2, Data.Image.Width, Data.Image.Height, Data.Image.Canvas.Handle, 0, 0, SRCCOPY); end; end; end; end; end; //---------------------------------------------------------------------------------------------------------------------- procedure TDrawTreeForm.VDT1GetNodeWidth(Sender: TBaseVirtualTree; Canvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex; var NodeWidth: Integer); // Since the draw tree does not know what is in a cell, we have to return the width of the content (not the entire // cell width, this could be determined by the column width). var Data: PShellObjectData; AMargin: Integer; begin with Sender as TVirtualDrawTree do AMargin := TextMargin; begin Data := Sender.GetNodeData(Node); case Column of 0: begin if Node.Parent = Sender.RootNode then NodeWidth := Canvas.TextWidth(Data.FullPath) + 2 * AMargin else NodeWidth := Canvas.TextWidth(ExtractFileName(Data.FullPath)) + 2 * AMargin; end; 1: begin if Assigned(Data.Image) then NodeWidth := Data.Image.Width; end; 2: NodeWidth := Canvas.TextWidth(Data.Properties) + 2 * AMargin; end; end; end; //---------------------------------------------------------------------------------------------------------------------- procedure TDrawTreeForm.VDT1InitChildren(Sender: TBaseVirtualTree; Node: PVirtualNode; var ChildCount: Cardinal); // Called just before a node with children (only folder nodes can have children) is expanded. var Data, ChildData: PShellObjectData; SR: TSearchRec; ChildNode: PVirtualNode; NewName: String; begin Data := Sender.GetNodeData(Node); if FindFirst(IncludeTrailingPathDelimiter(Data.FullPath) + {$ifdef Windows}'*.*'{$else}'*'{$endif}, faAnyFile, SR) = 0 then begin Screen.Cursor := crHourGlass; try repeat if (SR.Name <> '.') and (SR.Name <> '..') then begin NewName := IncludeTrailingPathDelimiter(Data.FullPath) + SR.Name; if (SR.Attr and faDirectory <> 0) or CanDisplay(NewName) then begin ChildNode := Sender.AddChild(Node); ChildData := Sender.GetNodeData(ChildNode); ChildData.FullPath := NewName; ChildData.Attributes := 0; //ReadAttributes(NewName); //if (ChildData.Attributes and SFGAO_FOLDER) = 0 then if (SR.Attr and faDirectory = 0) then ChildData.Properties := Format('%n KB, ', [SR.Size / 1024]) else ChildData.Attributes := SFGAO_FOLDER; GetOpenAndClosedIcons(ChildData.FullPath, ChildData.OpenIndex, ChildData.CloseIndex); Sender.ValidateNode(Node, False); end; end; until FindNext(SR) <> 0; ChildCount := Sender.ChildCount[Node]; // finally sort node if ChildCount > 0 then Sender.Sort(Node, 0, TVirtualStringTree(Sender).Header.SortDirection, False); finally FindClose(SR); Screen.Cursor := crDefault; end; end; end; //---------------------------------------------------------------------------------------------------------------------- procedure TDrawTreeForm.VDT1GetImageIndex(Sender: TBaseVirtualTree; Node: PVirtualNode; Kind: TVTImageKind; Column: TColumnIndex; var Ghosted: Boolean; var Index: Integer); // Returns the proper node image which has been determine on initialization time. Also overlay images are // used properly for shared folders. var Data: PShellObjectData; begin if Column = 0 then begin Data := Sender.GetNodeData(Node); case Kind of ikNormal, ikSelected: begin if Sender.Expanded[Node] then Index := Data.OpenIndex else Index := Data.CloseIndex; end; ikOverlay: if (Data.Attributes and SFGAO_SHARE) <> 0 then Index := 0 else if (Data.Attributes and SFGAO_LINK) <> 0 then Index := 1; end; end; end; //---------------------------------------------------------------------------------------------------------------------- procedure TDrawTreeForm.VDT1GetHintSize(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; var R: TRect); // Draw trees must manage parts of the hints themselves. Here we return the size of the hint window we want to show // or an empty rectangle in the case we don't want a hint at all. var Data: PShellObjectData; begin Data := Sender.GetNodeData(Node); if Assigned(Data) and Assigned(Data.Image) and (Column = 1) then R := Rect(0, 0, 2 * Data.Image.Width, 2 * Data.Image.Height) else R := Rect(0, 0, 0, 0); end; //---------------------------------------------------------------------------------------------------------------------- procedure TDrawTreeForm.VDT1DrawHint(Sender: TBaseVirtualTree; Canvas: TCanvas; Node: PVirtualNode; R: TRect; Column: TColumnIndex); // Here we actually paint the hint. It is the image in a larger size. var Data: PShellObjectData; begin Data := Sender.GetNodeData(Node); if Assigned(Data) and Assigned(Data.Image) and (Column = 1) then begin SetStretchBltMode(Canvas.Handle, HALFTONE); StretchBlt(Canvas.Handle, 0, 0, 2 * Data.Image.Width, 2 * Data.Image.Height, Data.Image.Canvas.Handle, 0, 0, Data.Image.Width, Data.Image.Height, SRCCOPY); end; end; //---------------------------------------------------------------------------------------------------------------------- procedure TDrawTreeForm.VDT1CompareNodes(Sender: TBaseVirtualTree; Node1, Node2: PVirtualNode; Column: TColumnIndex; var Result: Integer); // The node comparison routine is the heart of the tree sort. Here we have to tell the caller which node we consider // being "larger" or "smaller". var Data1, Data2: PShellObjectData; begin Data1 := Sender.GetNodeData(Node1); Data2 := Sender.GetNodeData(Node2); // Folder are always before files. Check if *both* are folders or *both* are non-folders, but not different. if ((Data1.Attributes xor Data2.Attributes) and SFGAO_FOLDER) <> 0 then begin // One of both is a folder the other is a file. if (Data1.Attributes and SFGAO_FOLDER) <> 0 then Result := -1 else Result := 1; end else // Both are of same type (folder or file). Just compare captions. // Note that we use ANSI comparison, while the strings are Unicode. Since this will implicitely convert the captions // to ANSI for comparation it might happen that the sort order is wrong for names which contain text in a language // other than the current OS language. A full blown Unicode comparison is beyond the scope of this demo. Result := CompareText(Data1.FullPath, Data2.FullPath); end; //---------------------------------------------------------------------------------------------------------------------- procedure TDrawTreeForm.VDT1HeaderClick(Sender: TVTHeader; Column: TColumnIndex; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); // Click handler to switch the column on which will be sorted. Since we cannot sort image data sorting is actually // limited to the main column. begin if Button = mbLeft then begin with Sender do begin if Column <> MainColumn then SortColumn := NoColumn else begin if SortColumn = NoColumn then begin SortColumn := Column; SortDirection := sdAscending; end else if SortDirection = sdAscending then SortDirection := sdDescending else SortDirection := sdAscending; Treeview.SortTree(SortColumn, SortDirection, False); end; end; end; end; //---------------------------------------------------------------------------------------------------------------------- procedure TDrawTreeForm.TrackBar1Change(Sender: TObject); // This part has nothing to do with the tree content and is only to show the effect of vertical image alignment for nodes // (since this does not justify an own demo). // Btw: look how fast this stuff is. Even with several thousands of nodes you still can adjust the position interactively. var Run: PVirtualNode; begin Label3.Caption := Format('%d%%', [Trackbar1.Position]); with VDT1, Trackbar1 do begin BeginUpdate; try Run := GetFirst; while Assigned(Run) do begin VerticalAlignment[Run] := Position; Run := GetNextVisible(Run); end; finally EndUpdate; end; end; end; //---------------------------------------------------------------------------------------------------------------------- procedure TDrawTreeForm.VDT1StateChange(Sender: TBaseVirtualTree; Enter, Leave: TVirtualTreeStates); begin if not (csDestroying in ComponentState) then UpdateStateDisplay(Sender.TreeStates, Enter, Leave); end; //---------------------------------------------------------------------------------------------------------------------- initialization {$I DrawTreeDemo.lrs} end.