I am trying to build my projects with a VirtualStringTree rather than a Listview, because of the vast speed difference. The thing is, even after looking thru the demo's, I just can't figure out exactly how I would use it as a ListView. Like, adding, deleting, and basically just working with ListView items is so easy, but when I look at the VT, it gets almost too complicated.
All I am looking for, is a VT that looks like a ListView, with subitems etc.
Here are some routines using the ListView, that I would like to use with VT (This is just a pseudo example:
procedure Add;
begin
with ListView.Items.Add do
Begin
Caption := EditCaption.Text;
SubItems.Add(EditSubItem.Text):
End;
end;
Procedure ReadItem(I : Integer);
begin
ShowMessage(ListView.Items[I].Caption);
ShowMessage(ListView.Items[I].SubItems[0]);
end;
Of course, also the Delete function, but since thats like 1 line, I didnt bother :P
Could anyone maybe translate the above examples into using a ListView style VT?
Thanks!
Why don't you use a list view in virtual mode? That will look and feel right and perform great.
The Delphi TListView control is a wrapper around the Windows list view component. In its default mode of operation copies of the list data are transferred from your app to the Windows control and this is slow.
The alternative to this is known as a virtual list view in Windows terminology. Your app doesn't pass the data to the Windows control. Instead, when the control needs to display data it asks your app for just the data that is needed.
The Delphi TListView control exposes virtual list views by use of the OwnerData property. You'll have to re-write your list view code somewhat but it's not too hard.
I also offer a link to another question here that covered similar ground. Rather oddly, the accepted answer for that question talked about list boxes even though the question was about list view controls.
with VirtualStringTree it's a bit more complex than the simple TListView, however here's a very simple tutorial that I've created a little while back on how to use VirtualStringTree http://www.youtube.com/watch?v=o6FpUJhEeoY I hope it helps, cheers!
Just use your normal TListView, but use it in virtual mode.
It's really simple:
Set the OwnerData property to true
Implement the OnData event handler.
Sample implementation that shows a simple list of 3 rows:
Type TMyItem=record
Item:String;
SubItem:String;
end;
var Items:Array of TMyItem;
// set up some in-memory dataset.. choose your own layout
SetLength(Items,3);
Items[0].Item := 'foo1';
Items[0].SubItem := 'bar1';
Items[1].Item := 'foo2';
Items[1].SubItem := 'bar2';
Items[2].Item := 'foo3';
Items[2].SubItem := 'bar3';
// tell ListView1 how many items there are
ListView1.Items.Count := Length(Items);
procedure TfrmMain.ListView1Data(Sender: TObject; Item: TListItem);
begin
Item.Caption := IntToStr(Item.Index);
Item.SubItems.Add( MyArray[Item.Index] );
Item.SubItems.Add( UpperCase(MyArray[Item.Index]) );
end;
// Updating a value:
Items[1].Item := 'bzzz';
ListView1.Update;
That's all!
Some things to keep in mind:
You don't call ListView1.Items.Add() anymore.
You need to keep your own list of data somewhere in memory, or come up with the data in real-time, so you cannot 'store' data in the listview any longer.
You need to set the items.count property, or you won't see anything.
Call ListView1.Update() if something changes.
procedure Add;
Var
Data: PLogData;
XNode: PVirtualNode;
begin
with vst do
Begin
XNode := AddChild(nil);
ValidateNode(XNode, False);
Data := GetNodeData(Xnode);
Data^.Name:= EditCaption.Text;
Data^.Msg := EditSubItem.Text;
End;
end;
Procedure ReadItem(I : Integer);
var
Data: PLogData;
begin
if not Assigned(vst.FocusedNode) then Exit;
Data := vst.GetNodeData(vst.FocusedNode);
ShowMessage(Data^.Name);
ShowMessage(Data^.Msg);
end;
Basically that is what you need to do, but the VirtualStringTree has/needs alot of other things working together to fully understand it. And once you "get it" the VST is easy and powerful. The following webpage will help you: http://wiki.freepascal.org/VirtualTreeview_Example_for_Lazarus
and below I will add more code I use for a simple VST Log display. I keep all the code in datamodule, just use the procedure Log to display information and change your FormMain.vstLog to yours...
unit udmVstLog;
interface
uses
SysUtils, Windows, Forms, Classes, Graphics,
VirtualTrees, ActnList, Dialogs, ExtDlgs;
type
PLogData = ^TLogData;
TLogData = record
IsErr : Boolean;
Name: String;
Msg : String;
end;
type
TdmVstLog = class(TDataModule)
actlst1: TActionList;
actClear: TAction;
actSave: TAction;
actCopyLine2Mem: TAction;
sdlgLog: TSaveTextFileDialog;
procedure DataModuleCreate(Sender: TObject);
procedure actClearExecute(Sender: TObject);
procedure actSaveExecute(Sender: TObject);
procedure actCopyLine2MemExecute(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
procedure VSTFreeNode(Sender: TBaseVirtualTree; Node: PVirtualNode);
procedure VSTGetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; TextType: TVSTTextType; var CellText: String);
procedure VSTPaintText(Sender: TBaseVirtualTree;
const TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex;
TextType: TVSTTextType);
end;
procedure Log(aIsErr: Boolean; AName, AMsg: string); overload;
procedure Log(AName, AMsg: string); overload;
procedure Log(AMsg: string); overload;
var
dmVstLog: TdmVstLog;
implementation
uses uFormMain, ClipBrd;
{$R *.dfm}
procedure Log(aIsErr: Boolean; AName, AMsg: string);
Var
Data: PLogData;
XNode: PVirtualNode;
begin
XNode:=FormMain.vstLog.AddChild(nil);
FormMain.vstLog.ValidateNode(XNode, False);
Data := FormMain.vstLog.GetNodeData(Xnode);
Data^.IsErr := aIsErr;
if aIsErr then
Data^.Name:= DateTimeToStr(Now) + ' ERROR ' + AName
else
Data^.Name:= DateTimeToStr(Now) + ' INFO ' + AName;
Data^.Msg:= AMsg;
end;
procedure Log(AName, AMsg: string);
begin
Log(False,AName,AMsg);
end;
procedure Log(AMsg: string);
begin
Log(False,'',AMsg);
end;
// VirtualStringTree Events defined here
procedure TdmVstLog.VSTFreeNode(Sender: TBaseVirtualTree; Node: PVirtualNode);
var
Data: PLogData;
begin
Data:=Sender.GetNodeData(Node);
Finalize(Data^);
end;
procedure TdmVstLog.VSTGetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; TextType: TVSTTextType; var CellText: String);
var
Data: PLogData;
begin
Data := Sender.GetNodeData(Node);
case Column of
0: CellText := Data^.Name + ' - '+ Data^.Msg;
end;
end;
procedure TdmVstLog.VSTPaintText(Sender: TBaseVirtualTree;
const TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex;
TextType: TVSTTextType);
Var
Data: PLogData;
begin
Data := Sender.GetNodeData(Node);
if Data^.IsErr then
TargetCanvas.Font.Color:=clRed;
end;
//PopUpMenu Actions defined here!
procedure TdmVstLog.actClearExecute(Sender: TObject);
begin
FormMain.vstLog.Clear;
end;
procedure TdmVstLog.actCopyLine2MemExecute(Sender: TObject);
var
Data: PLogData;
begin
if not Assigned(FormMain.vstLog.FocusedNode) then Exit;
Data := FormMain.vstLog.GetNodeData(FormMain.vstLog.FocusedNode);
ClipBoard.AsText := Data^.Name + ' - ' + Data^.Msg;
end;
procedure TdmVstLog.actSaveExecute(Sender: TObject);
Var
XNode: PVirtualNode;
Data: PLogData;
ts: TStringList;
begin
If FormMain.vstLog.GetFirst = nil then Exit;
XNode:=nil;
if sdlgLog.Execute then begin
ts:= TStringList.create;
try
Repeat
if XNode = nil then XNode:=FormMain.vstLog.GetFirst Else XNode:=FormMain.vstLog.GetNext(XNode);
Data:=FormMain.vstLog.GetNodeData(XNode);
ts.Add(Data^.Name + ' - '+ Data^.Msg);
Until XNode = FormMain.vstLog.GetLast();
ts.SaveToFile(sdlgLog.FileName);
finally
ts.Free;
end;
end;
end;
// Datamodule Events defined here
procedure TdmVstLog.DataModuleCreate(Sender: TObject);
begin
with FormMain.vstLog do begin
NodeDataSize := SizeOf(TLogData);
OnFreeNode := VSTFreeNode;
OnGetText := VSTGetText;
OnPaintText := VSTPaintText;
end;
end;
end.
...
procedure RemoveSelectedNodes(vst:TVirtualStringTree);
begin
if vst.SelectedCount = 0 then Exit;
vst.BeginUpdate;
vst.DeleteSelectedNodes;
vst.EndUpdate;
end;
procedure RemoveAllNodes(vst:TVirtualStringTree);
begin
vst.BeginUpdate;
vst.Clear;
vst.EndUpdate;
end;
Get the VT Contributions pack and check out some of the descendants of virtual string tree. That are in there. I haven't used them in projects, but they seem to make Virtual String Tree easier to use.
Here's my getting started primer nonetheless:
I've found after using Virtual String Tree quite a bit that the only way you can make the most of it is by implementing the init node/child functions and setting the root node count, much the same as you would a list view with ownerdraw := true.
It's pretty easy to do stuff with VirtualStringTree, you just need to implement the get text function and the node size functions (set it equal to the size of whatever record you'd like to use as the data behind your tree)
I've found it's almost always easier to do
TVirtualTreeNodeRecordData = record
Data : TVirtualTreeNodeData;
end
and create the data object on the init functions. It creates the pointers for you, but you need to free the objects (again, use another delete node callback).
Related
My Question is similar to the idea here: Replacing a component class in delphi.
But I need to change a specific component(s) class on demand.
Here is some pseudo demo code:
unit Unit1;
TForm1 = class(TForm)
ImageList1: TImageList;
ImageList2: TImageList;
private
ImageList3: TImageList;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
ImageList3 := TImageList.Create(Self);
// all instances of TImageList run as usual
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
Unit2.MakeSuperImageList(ImageList2);
Unit2.MakeSuperImageList(ImageList3);
// from now on ONLY ImageList2 and ImageList3 are TSuperImageList
// ImageList1 is unchanged
end;
unit Unit2;
type
TSuperImageList = class(Controls.TImageList)
protected
procedure DoDraw(Index: Integer; Canvas: TCanvas; X, Y: Integer;
Style: Cardinal; Enabled: Boolean = True); override;
end;
procedure TSuperImageList.DoDraw(Index: Integer; Canvas: TCanvas; X, Y: Integer;
Style: Cardinal; Enabled: Boolean = True);
var
Icon: TIcon;
begin
Icon := TIcon.Create;
try
Self.GetIcon(Index, Icon);
Canvas.Draw(X, Y, Icon);
finally
Icon.Free;
end;
end;
procedure MakeSuperImageList(ImageList: TImageList);
begin
// TImageList -> TSuperImageList
end;
Note: Just to be clear, I want to change some instances, but not all, so interposer class will not do.
This is easier as thought (thanks to Hallvard's Blog - Hack#14: Changing the class of an object at run-time):
procedure PatchInstanceClass(Instance: TObject; NewClass: TClass);
type
PClass = ^TClass;
begin
if Assigned(Instance) and Assigned(NewClass)
and NewClass.InheritsFrom(Instance.ClassType)
and (NewClass.InstanceSize = Instance.InstanceSize) then
begin
PClass(Instance)^ := NewClass;
end;
end;
type
TMyButton = class(TButton)
public
procedure Click; override;
end;
procedure TMyButton.Click;
begin
ShowMessage('Click!');
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
PatchInstanceClass(Button1, TMyButton);
end;
Executive summary: Use an interposer class with runtime switching of behaviour.
Although #kobik is using Delphi 5 and cannot do what I describe below, this answers fleshes out the supported way to change the VMT of an instance using TVirtualMethodInterceptor. Mason's comments inspired me to write this.
procedure MakeSuperImageList(ImageList: TImageList);
var
vmi: TVirtualMethodInterceptor;
begin
vmi := TVirtualMethodInterceptor.Create(ImageList.ClassType);
try
vmi.OnBefore := procedure(Instance: TObject; Method: TRttiMethod;
const Args: TArray<TValue>; out DoInvoke: Boolean; out Result: TValue)
var
Icon: TIcon;
Canvas: TCanvas;
Index: Integer;
X, Y: Integer;
begin
if Method.Name<>'DoDraw' then
exit;
DoInvoke := False;//don't call TImageList.DoDraw
Index := Args[0].AsInteger;
Canvas := Args[1].AsType<TCanvas>;
X := Args[2].AsInteger;
Y := Args[3].AsInteger;
Icon := TIcon.Create;
try
ImageList.GetIcon(Index, Icon);
Canvas.Draw(X, Y, Icon);
finally
Icon.Free;
end;
end;
vmi.Proxify(ImageList);
finally
vmi.Free;
end;
end;
I've only compiled this in my head so it will no doubt need debugging. Something tells me that capturing ImageList might not work, in which case you would need to write Instance as TImageList.
Unless you use a VMT modifying based solution, you will have to create new instances (as per Mason's suggestion). And this means that you will also have to modify all references to the image list instances at the same time that you create the new instances. In my view that rules out any proposed solution based on instantiating replacement objects.
So, my conclusion is that to implement your proposed solution in full generality, you need runtime VMT modification. And if you don't have modern Delphi that provides such facilities in a supported way, you will need to hack the VMT.
Now, modifying the VMT, even with virtual method interceptors, is rather distasteful, in my view. I think you are probably going about this the wrong way. I suggest that you use an interposer class (or some other sub-classing technique) and switch behaviour at runtime with a property of the sub-class.
type
TImageList = class(ImgList.TImageList)
private
FIsSuper: Boolean;
protected
procedure DoDraw(Index: Integer; Canvas: TCanvas; X, Y: Integer;
Style: Cardinal; Enabled: Boolean = True); override;
public
property IsSuper: Boolean read FIsSuper write FIsSuper;
end;
TImageList.DoDraw(Index: Integer; Canvas: TCanvas; X, Y: Integer;
Style: Cardinal; Enabled: Boolean = True);
var
Icon: TIcon;
begin
if IsSuper then
begin
Icon := TIcon.Create;
try
Self.GetIcon(Index, Icon);
Canvas.Draw(X, Y, Icon);
finally
Icon.Free;
end;
end
else
inherited;
end;
....
procedure TForm1.Button1Click(Sender: TObject);
begin
ImageList2.IsSuper := True;
ImageList3.IsSuper := True;
end;
There's no automatic way to do that, but you could try something like this:
procedure MakeSuperImageList(var ImageList: TImageList);
var
new: TImageList;
begin
if ImageList is TSuperImageList then
Exit;
new := TSuperImageList.Create(ImageList.Owner);
new.Assign(ImageList);
ImageList.Free;
ImageList := new;
end;
Depending on how Assign is implemented, it may not quite work as expected, but you can override Assign or AssignTo on TSuperImageList to get the desired behavior.
I wrote such module to store there last changes of picture in my paint application " in Delphi
unit HistoryQueue;
interface
uses
Graphics;
type
myHistory = class
constructor Create(Size:Integer);
public
procedure Push(Bmp:TBitmap);
function Pop():TBitmap;
procedure Clean();
procedure Offset();
function isEmpty():boolean;
function isFull():boolean;
function getLast():TBitmap;
protected
historyQueueArray: array of TBitmap;
historyIndex, hSize:Integer;
end;
implementation
procedure myHistory.Push(Bmp:TBitmap);
var tbmp:TBitmap;
begin
if(not isFull) then begin
Inc(historyIndex);
historyQueueArray[historyIndex]:=TBitmap.Create;
historyQueueArray[historyIndex].Assign(bmp);
end else begin
Offset();
historyQueueArray[historyIndex]:=TBitmap.Create;
historyQueueArray[historyIndex].Assign(bmp);
end;
end;
procedure myHistory.Clean;
var i:Integer;
begin
{ for i:=0 to hSize do begin
historyQueueArray[i].Free;
historyQueueArray[i].Destroy;
end; }
end;
constructor myHistory.Create(Size:Integer);
begin
hSize:=Size;
SetLength(historyQueueArray, hSize);
historyIndex:=-1;
end;
function myHistory.isEmpty: boolean;
begin
Result:=(historyIndex = -1);
end;
function myHistory.isFull: boolean;
begin
Result:=(historyIndex = hSize);
end;
procedure myHistory.Offset; {to handle overflow}
var i:integer;
begin
//historyQueueArray[0]:=nil;
for i:=0 to hSize-1 do begin
historyQueueArray[i]:=TBitmap.Create;
historyQueueArray[i].Assign(historyQueueArray[i+1]);
end;
end;
function myHistory.Pop: TBitmap;
var
popBmp:TBitmap;
begin
popBmp:= TBitmap.Create;
popBmp.Assign(historyQueueArray[historyIndex]);
Dec(historyIndex);
Result:=popBmp;
end;
function myHistory.getLast: TBitmap; {this function I use when I need refresh the cnvas when I draw ellipse or rect, to get rid of traces and safe previous changes of the picture}
var
tBmp:TBitmap;
begin
tBmp:= TBitmap.Create;
tBmp.Assign(historyQueueArray[historyIndex]);
Result:=tBmp;
end;
end.
And thats how I use it
procedure TMainForm.FormCreate(Sender: TObject);
var
cleanBmp:TBitmap;
begin
{...}
doneRedo:=false;
redomode:=false; undomode:=false;
//init arrays
picHistory:=myHistory.Create(10); //FOR UNDO
tempHistory:=myHistory.Create(10); //FOR REDO
cleanbmp:=TBitmap.Create;
cleanbmp.Assign(imgMain.Picture.Bitmap);
picHistory.Push(cleanbmp);
cleanbmp.Free;
{...}
end;
procedure TMainForm.btnUndoClick(Sender: TObject);
var redBmp:TBitmap;
begin
undoMode:=true;
//if there were some changes
if(not picHistory.isEmpty) then begin
redBmp:=TBitmap.Create;
redBmp.Assign(picHistory.getLast);
//clean canvas
imgMain.Picture.Bitmap:=nil;
//get what was there before
imgMain.Canvas.Draw(0,0, picHistory.Pop);
//and in case if we will not make any changes after UNDO(clicked one or more times)
//and call REDO then
tempHistory.Push(redBmp);//we save what were on canvas before UNDOand push it to redo history
redBmp.Free;
end;
end;
procedure TMainForm.btnRedoClick(Sender: TObject);
var undBmp:TBitmap;
begin
redoMode:=true;
if(not tempHistory.isEmpty) then begin
doneRedo:=True;
undBmp:=TBitmap.Create;
undBmp.Assign(tempHistory.getLast);
imgMain.Picture.Bitmap:=nil;
MainForm.imgMain.Canvas.Draw(0,0, tempHistory.Pop);
//same history (like with UNDO implementation) here but reverse
picHistory.Push(undBmp);
undBmp.Free;
end;
end;
{...}
procedure TMainForm.imgMainMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
var bmp:TBitmap;
begin
//if mouse were down and then it's up this means we drew something
//and must save changes into history to be able to make UNDO
{...}
bmp:=TBitmap.Create;
try
bmp.Assign(imgMain.Picture.Bitmap);
picHistory.Push(bmp);
//if there are some changes added after redo then we clean redo history
if (doneRedo) then begin
tempHistory.Clean;
doneRedo:=false;
end;
finally
bmp.Free;
//sor of refresh
imgMain.Canvas.Draw(0,0, picHistory.getLast);
end;
{...}
But the problem is it works not way I expected. an example:
If I push undo button once - nothing happens. On twice - it does what it should at once.
And if I drew an ellipse, then click undo once and start draw new one - last drawn ellipse just dissaperas!
Here's the elipse draw method in case if it could be helpful to find out the problem
procedure TMainForm.ellipseDraw(X, Y: Integer);
begin
imgMain.Canvas.Pen.Color:=useColor;
imgMain.Canvas.Brush.Color:=scndColor;
imgMain.Canvas.Pen.Width:=size;
if(mouseIsDown) then begin
imgMain.Canvas.Draw(0,0, picHistory.getLast); //there gonna be no bizzare traces from figures
imgMain.Canvas.Ellipse(dX, dY, X,Y);
end;
end;
Answer
If I push undo button once - nothing happens. On twice - it does what it should at once.
That is indeed exactly what your code does:
In imgMainMouseUp you add the current picture to the undo list, and
In btnUndoClick you retrieve the last bitmap from the undo list, which is the same as currently seen on the Image.
The solution - to this specific question - is to add the previous bitmap to the undo list instead of the current one.
Bonus
And to address David's comment concerning the leaking, your implementation leaks Bitmaps because:
The routines Pop and getLast return a newly local created Bitmap. This places the responsibility for its destruction on the caller ot the routines. Your MainForm code does not destroy those Bitmaps, thus they are memory leaks. The solution is to simply return the item in the array, instead of creating a new Bitmap.
In the Offset routine, you again create new Bitmaps and leak all previous ones. Just assign Queue[I] to Queue[I + 1].
In the Push method, you forget to free the last item.
The class does not have a destructor, which again places the responsibility for the destruction of all Bitmaps on the user of the object with the need to call Clean, which it does not. The solution is to add a destructor to your object which calls Clean.
Besides these leaks, there are more problems with your code. Here some fixes and tips:
Since dynamic arrays are zero-based, your isFull routine does not return True when it should. It should be implemented as Result := historyIndex = hSize - 1;
Your array is not a queue (FIFO), but a stack (LIFO).
Your Pop routine does not check for an empty list.
Altogether, your history class could better look like:
uses
SysUtils, Graphics;
type
TBitmapHistory = class(TObject)
private
FIndex: Integer;
FStack: array of TBitmap;
procedure Offset;
public
procedure Clear;
function Count: Integer;
constructor Create(ACount: Integer);
destructor Destroy; override;
function Empty: Boolean;
function Full: Boolean;
function Last: TBitmap;
function Pop: TBitmap;
procedure Push(ABitmap: TBitmap);
end;
implementation
{ TBitmapHistory }
procedure TBitmapHistory.Clear;
var
I: Integer;
begin
for I := 0 to Count - 1 do
FreeAndNil(FStack[I]);
FIndex := -1;
end;
function TBitmapHistory.Count: Integer;
begin
Result := Length(FStack);
end;
constructor TBitmapHistory.Create(ACount: Integer);
begin
inherited Create;
SetLength(FStack, ACount);
FIndex := -1;
end;
destructor TBitmapHistory.Destroy;
begin
Clear;
inherited Destroy;
end;
function TBitmapHistory.Empty: Boolean;
begin
Result := FIndex = -1;
end;
function TBitmapHistory.Full: Boolean;
begin
Result := FIndex = Count - 1;
end;
function TBitmapHistory.Last: TBitmap;
begin
if Empty then
Result := nil
else
Result := FStack[FIndex];
end;
procedure TBitmapHistory.Offset;
begin
FStack[0].Free;
Move(FStack[1], FStack[0], (Count - 1) * SizeOf(TBitmap));
end;
function TBitmapHistory.Pop: TBitmap;
begin
if not Empty then
begin
Result := Last;
Dec(FIndex);
end;
end;
procedure TBitmapHistory.Push(ABitmap: TBitmap);
begin
if Full then
Offset
else
Inc(FIndex);
FStack[Findex].Free;
FStack[FIndex] := TBitmap.Create;
FStack[Findex].Assign(ABitmap);
end;
Remarks:
There also exists a specialized class TObjectStack for this in the Contnrs unit which you could override/exploit.
There are also concerns with your MainForm code, but I politely leave that up to you to fix.
My Question is similar to the idea here: Replacing a component class in delphi.
But I need to change a specific component(s) class on demand.
Here is some pseudo demo code:
unit Unit1;
TForm1 = class(TForm)
ImageList1: TImageList;
ImageList2: TImageList;
private
ImageList3: TImageList;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
ImageList3 := TImageList.Create(Self);
// all instances of TImageList run as usual
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
Unit2.MakeSuperImageList(ImageList2);
Unit2.MakeSuperImageList(ImageList3);
// from now on ONLY ImageList2 and ImageList3 are TSuperImageList
// ImageList1 is unchanged
end;
unit Unit2;
type
TSuperImageList = class(Controls.TImageList)
protected
procedure DoDraw(Index: Integer; Canvas: TCanvas; X, Y: Integer;
Style: Cardinal; Enabled: Boolean = True); override;
end;
procedure TSuperImageList.DoDraw(Index: Integer; Canvas: TCanvas; X, Y: Integer;
Style: Cardinal; Enabled: Boolean = True);
var
Icon: TIcon;
begin
Icon := TIcon.Create;
try
Self.GetIcon(Index, Icon);
Canvas.Draw(X, Y, Icon);
finally
Icon.Free;
end;
end;
procedure MakeSuperImageList(ImageList: TImageList);
begin
// TImageList -> TSuperImageList
end;
Note: Just to be clear, I want to change some instances, but not all, so interposer class will not do.
This is easier as thought (thanks to Hallvard's Blog - Hack#14: Changing the class of an object at run-time):
procedure PatchInstanceClass(Instance: TObject; NewClass: TClass);
type
PClass = ^TClass;
begin
if Assigned(Instance) and Assigned(NewClass)
and NewClass.InheritsFrom(Instance.ClassType)
and (NewClass.InstanceSize = Instance.InstanceSize) then
begin
PClass(Instance)^ := NewClass;
end;
end;
type
TMyButton = class(TButton)
public
procedure Click; override;
end;
procedure TMyButton.Click;
begin
ShowMessage('Click!');
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
PatchInstanceClass(Button1, TMyButton);
end;
Executive summary: Use an interposer class with runtime switching of behaviour.
Although #kobik is using Delphi 5 and cannot do what I describe below, this answers fleshes out the supported way to change the VMT of an instance using TVirtualMethodInterceptor. Mason's comments inspired me to write this.
procedure MakeSuperImageList(ImageList: TImageList);
var
vmi: TVirtualMethodInterceptor;
begin
vmi := TVirtualMethodInterceptor.Create(ImageList.ClassType);
try
vmi.OnBefore := procedure(Instance: TObject; Method: TRttiMethod;
const Args: TArray<TValue>; out DoInvoke: Boolean; out Result: TValue)
var
Icon: TIcon;
Canvas: TCanvas;
Index: Integer;
X, Y: Integer;
begin
if Method.Name<>'DoDraw' then
exit;
DoInvoke := False;//don't call TImageList.DoDraw
Index := Args[0].AsInteger;
Canvas := Args[1].AsType<TCanvas>;
X := Args[2].AsInteger;
Y := Args[3].AsInteger;
Icon := TIcon.Create;
try
ImageList.GetIcon(Index, Icon);
Canvas.Draw(X, Y, Icon);
finally
Icon.Free;
end;
end;
vmi.Proxify(ImageList);
finally
vmi.Free;
end;
end;
I've only compiled this in my head so it will no doubt need debugging. Something tells me that capturing ImageList might not work, in which case you would need to write Instance as TImageList.
Unless you use a VMT modifying based solution, you will have to create new instances (as per Mason's suggestion). And this means that you will also have to modify all references to the image list instances at the same time that you create the new instances. In my view that rules out any proposed solution based on instantiating replacement objects.
So, my conclusion is that to implement your proposed solution in full generality, you need runtime VMT modification. And if you don't have modern Delphi that provides such facilities in a supported way, you will need to hack the VMT.
Now, modifying the VMT, even with virtual method interceptors, is rather distasteful, in my view. I think you are probably going about this the wrong way. I suggest that you use an interposer class (or some other sub-classing technique) and switch behaviour at runtime with a property of the sub-class.
type
TImageList = class(ImgList.TImageList)
private
FIsSuper: Boolean;
protected
procedure DoDraw(Index: Integer; Canvas: TCanvas; X, Y: Integer;
Style: Cardinal; Enabled: Boolean = True); override;
public
property IsSuper: Boolean read FIsSuper write FIsSuper;
end;
TImageList.DoDraw(Index: Integer; Canvas: TCanvas; X, Y: Integer;
Style: Cardinal; Enabled: Boolean = True);
var
Icon: TIcon;
begin
if IsSuper then
begin
Icon := TIcon.Create;
try
Self.GetIcon(Index, Icon);
Canvas.Draw(X, Y, Icon);
finally
Icon.Free;
end;
end
else
inherited;
end;
....
procedure TForm1.Button1Click(Sender: TObject);
begin
ImageList2.IsSuper := True;
ImageList3.IsSuper := True;
end;
There's no automatic way to do that, but you could try something like this:
procedure MakeSuperImageList(var ImageList: TImageList);
var
new: TImageList;
begin
if ImageList is TSuperImageList then
Exit;
new := TSuperImageList.Create(ImageList.Owner);
new.Assign(ImageList);
ImageList.Free;
ImageList := new;
end;
Depending on how Assign is implemented, it may not quite work as expected, but you can override Assign or AssignTo on TSuperImageList to get the desired behavior.
I know i have posted a similar question before but i am not able to get it working I have this simple code :
procedure TfrmMain.srvrConnect(AContext: TIdContext); //idhttpserver on connect event
var
S,C : String;
begin
repeat
s := s + AContext.Connection.Socket.ReadChar;
until AContext.Connection.Socket.InputBufferIsEmpty = True;
frmMain.caption := S;
Memo1.Lines.Add(S);
end;
The strings displays ok in the memo but the caption doesn't get updated
TIdHTTPServer is a multi-threaded component. TIdContext runs in its own worker thread. You cannot safely update the Form's Caption (or do anything else with the UI) from outside of the main thread. You need to synchronize with the main thread, such as with the TIdSync or TIdNotify class.
On a side note, calling ReadChar() in a loop is very inefficient, not to mention error-prone if you are using Delphi 2009+ since it cannot return data for surrogate pairs.
Use something more like this instead;
type
TDataNotify = class(TIdNotify)
protected
Data: String;
procedure DoNotify; override;
public
constructor Create(const S: String);
class procedure DataAvailable(const S: String);
end;
constructor TDataNotify.Create(const S: String);
begin
inherited Create;
Data := S;
end;
procedure TDataNotify.DoNotify;
begin
frmMain.Caption := Data;
frmMain.Memo1.Lines.Add(Data);
end;
class procedure TDataNotify.DataAvailable(const S: String);
begin
Create(S).Notify;
end;
procedure TfrmMain.srvrConnect(AContext: TIdContext); //idhttpserver on connect event
var
S: String;
begin
AContext.Connection.IOHandler.CheckForDataOnSource(IdTimeoutDefault);
if not AContext.Connection.IOHandler.InputBufferIsEmpty then
begin
S := AContext.Connection.IOHandler.InputBufferAsString;
TDataNotify.DataAvailable(S);
end;
end;
First, make sure you are writing to the right variable. Are you sure that frmMain is the form you want the caption do change?
Also, you could try:
procedure TfrmMain.srvrConnect(AContext: TIdContext); //idhttpserver on connect event
var
S,C : String;
begin
repeat
s := s + AContext.Connection.Socket.ReadChar;
until AContext.Connection.Socket.InputBufferIsEmpty = True;
oCaption := S;
TThread.Synchronize(nil, Self.ChangeCaption);
end;
procedure TfrmMain.ChangeCaption;
begin
Self.Caption := oCaption;
Memo1.Lines.Add(oCaption);
end;
And finally, make sure that the first line on S is not a blank line, because the form's caption will not show strings that contains a line feed.
I have a heirarchy of data that is displayed in a VirtualStringTree. I use this heirarchy multiple times in my application with slight modifications to the way the tree is drawn/displayed. My method currently utilizes the AddChild() procedure for adding nodes an d as such i have multiple copies of the data when the application is run.
I would now like to consolidate these trees and have a 'master' tree which points to the actual data, but then have the 'slave' trees point to the SAME data.
I am a little unsure of if/how this can be acheived. I would think i could simply load the master tree and populate its NodeData with pointers to where the data i being held, and then for all the slave trees, simply store the same pointer in their nodedata.
However im not having much luck.
My current code looks like:
//Initialize the NodeDataSize
procedure TForm1.FormCreate(Sender: TObject);
begin
TreeMasterComponents.NodeDataSize := SizeOf(rMasterComponent);
VST.NodeDataSize := SizeOf(Pointer);
end;
Procedure to copy the master tree to slave trees
procedure TForm1.LoadSlaveTree(ATree: TVirtualStringTree);
var Node : PVirtualNode;
procedure RecursiveCopy(SrcPNode,SrcTNode : PVirtualNode; ATree : TVirtualStringTree);
var SrcNode, TargetNode : PVirtualNode;
SrcData : PMasterComponent;
begin
SrcNode := TreeMasterComponents.GetFirstChild(SrcPNode);
while Assigned(SrcNode) do
begin
SrcData := TreeMasterComponents.GetNodeData(SrcNode);
TargetNode := ATree.AddChild(SrcTNode,SrcData);
RecursiveCopy(SrcNode,TargetNode,ATree);
SrcNode := SrcNode.NextSibling;
end;
end;
begin
ATree.BeginUpdate;
ATree.Clear;
Node := TreeMasterComponents.GetFirst(true);
while Assigned(Node) do
begin
RecursiveCopy(Node,nil,ATree);
Node := Node.NextSibling;
end;
ATree.EndUpdate;
end;
Procedure for slave tree to getCellText
procedure TForm1.SlaveGetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; TextType: TVSTTextType; var CellText: WideString);
var Data : PMasterComponent;
begin
Data := Sender.GetNodeData(Node);
Case column of
0:CellText := Data^.ComponentCode;
1:CellText := Data^.FullLocation;
end;
end;
At the moment, the nodes are added in the correct heirarchy, however there is no text appearing for the slave trees. Any help would be appreciated.
I don't know why no text appears in your slave trees but I'd like to advise the following.
Probably easier would be to create a frame with the tree and code, and reuse the frame on your forms. Each instance of the tree would simply load the same data (no copying of nodes necessary).
The slight modifications can be achieved by visual form inheritance by writing new event handlers for the specific instance of the frame/tree. You can also inherit from the frame, creating a new class, if needed.
Ok so i beleive i have found a solution:
The trick was to create a new record to store the Pointer to the original data:
type PRefMasterComponent = ^RefMasterComponent;
RefMasterComponent = packed record
PMCData : PMasterComponent;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
TreeMasterComponents.NodeDataSize := SizeOf(rMasterComponent);
VST.NodeDataSize := SizeOf(RefMasterComponent);
end;
Now the Copy code looks like:
procedure TForm1.LoadTree(ATree: TVirtualStringTree);
var Node,TargetNode : PVirtualNode;
SrcData : PMasterComponent;
Data : PRefMasterComponent;
procedure RecursiveCopy(SrcPNode, TargetPNode : PVirtualNode; ATree : TVirtualStringTree);
var Node, TargetNode : PVirtualNode;
SrcData : PMasterComponent;
Data : PRefMasterComponent;
begin
Node := TreeMasterComponents.GetFirstChild(SrcPNode);
while Assigned(Node) do
begin
SrcData := TreeMasterComponents.GetNodeData(Node);
TargetNode := ATree.AddChild(TargetPNode);
Data := ATree.GetNodeData(TargetNode);
Data.PMCData := SrcData;
RecursiveCopy(Node,TargetNode,ATree);
Node := Node.NextSibling;
end;
end;
begin
ATree.BeginUpdate;
ATree.Clear;
Node := TreeMasterComponents.GetFirst(true);
while Assigned(Node) do
begin
SrcData := TreeMasterComponents.GetNodeData(Node);
TargetNode := ATree.AddChild(nil);
Data := ATree.GetNodeData(TargetNode);
Data.PMCData := SrcData;
RecursiveCopy(Node,TargetNode,ATree);
Node := Node.NextSibling;
end;
ATree.EndUpdate;
end;
and the OnGetText:
procedure TForm1.vstGetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
Column: TColumnIndex; TextType: TVSTTextType; var CellText: WideString);
var Data : PRefMasterComponent;
RefData : PMasterComponent;
begin
Data := Sender.GetNodeData(Node);
RefData := Data.PMCData;
Case column of
0:CellText := RefData.ComponentCode;
1:CellText := RefData.FullLocation;
end;
end;
Now if i change data in one tree all i have to do is call VST.Invalidate and the changes are reflected in the other tree.