I'm trying to find all Properties with a TStrings descendant type on an Object.
Here is what I've tried sofar:
Place a Memo and a Chart on a form and then this code.
procedure TForm1.FormCreate(Sender: TObject);
var
aObject: TObject;
begin
for aObject in GetItemsObjects(Chart1) do
Memo1.Lines.Add(aObject.ClassName);
end;
class function TForm1.GetItemsObjects(aObject: TObject): TArray<TObject>;
var
RttiProperty: TRttiProperty;
RttiType: TRttiType;
ResultList: TList<TObject>;
PropertyValue: TValue;
PropertyObject: TObject;
s: String;
begin
ResultList := TList<TObject>.Create;
try
RttiType := RttiContext.GetType(aObject.ClassType);
for RttiProperty in RttiType.GetProperties do
begin
PropertyValue := RttiProperty.GetValue(aObject);
if (not PropertyValue.IsObject) or (PropertyValue.IsEmpty) then
continue;
try
PropertyObject := PropertyValue.AsObject;
s := PropertyObject.ClassName;
if (PropertyObject is TStrings) then
ResultList.Add(PropertyObject)
else
ResultList.AddRange(GetItemsObjects(PropertyObject));
except
end;
end;
finally
Result := ResultList.ToArray;
ResultList.Free;
end;
end;
The problem is that I get an stack overflow. Even though I've tried to stop the recursion with this code:
if (PropertyObject is TStrings) then
ResultList.Add(PropertyObject)
else
ResultList.AddRange(GetItemsObjects(PropertyObject));
Your code is causing a stack overflow because you are recursively calling GetItemsObjects which also scans properties like Parent from TControl.
If you really want to recursively do a deep scan of TStrings properties then you need to make sure to not revisit the same objects again - keep track of them with a list or something.
Related
I have a class that has a few event properties, and another class that contains the event handlers. At compile-time I don't know the structure of either class, at run-time I only get to know the match between event property and event handler using their names. Using RTTI, I'd like to assign the event handlers to the respective event properties, how can I do so?
I currently have something like this:
type
TMyEvent = reference to procedure(const AInput: TArray<string>; out AOutput: TArray<string>);
TMyBeforeEvent = reference to procedure(const AInput: TArray<string>; out AOutput: TArray<string>; out ACanContinue: boolean);
TMyClass = class
private
FOnBeforeEvent: TMyBeforeEvent;
FOnEvent: TMyEvent;
public
property OnBeforeEvent: TMyBeforeEvent read FOnBeforeEvent write FOnBeforeEvent;
property OnEvent: TMyEvent read FOnEvent write FOnEvent;
end;
TMyEventHandler = class
public
procedure DoBeforeEvent(const AInput: TArray<string>; out AOutput: TArray<string>; out ACanContinue: boolean);
procedure DoEvent(const AInput: TArray<string>; out AOutput: TArray<string>);
end;
procedure AssignEvent;
implementation
uses
Vcl.Dialogs, System.RTTI;
{ TMyEventHandler }
procedure TMyEventHandler.DoBeforeEvent(const AInput: TArray<string>;
out AOutput: TArray<string>; out ACanContinue: boolean);
begin
// do something...
end;
procedure TMyEventHandler.DoEvent(const AInput: TArray<string>;
out AOutput: TArray<string>);
begin
// do something...
end;
procedure AssignEvent;
var
LObj: TMyClass;
LEventHandlerObj: TMyEventHandler;
LContextObj, LContextHandler: TRttiContext;
LTypeObj, LTypeHandler: TRttiType;
LEventProp: TRttiProperty;
LMethod: TRttiMethod;
LNewEvent: TValue;
begin
LObj := TMyClass.Create;
LEventHandlerObj := TMyEventHandler.Create;
LContextObj := TRttiContext.Create;
LTypeObj := LContextObj.GetType(LObj.ClassType);
LEventProp := LTypeObj.GetProperty('OnBeforeEvent');
LContextHandler := TRttiContext.Create;
LTypeHandler := LContextHandler.GetType(LEventHandlerObj.ClassType);
LMethod := LTypeHandler.GetMethod('DoBeforeEvent');
LEventProp.SetValue(LObj, LNewEvent {--> what value should LNewEvent have?});
end;
A reference to procedure(...) is an anonymous method type. Under the hood, it is implemented as an interfaced object (ie, a class that implements the IInterface interface) with an Invoke() method that matches the parameters of the procedure.
So, can't use TMyEventHandler.DoBeforeEvent() directly with TMyClass.OnBeforeEvent, for instance, since TMyEventHandler does not match that criteria. But, you can wrap the call to DoBeforeEvent() inside of an actual anonymous procedure, eg:
procedure AssignEvent;
var
LObj: TMyClass;
LEventHandlerObj: TMyEventHandler;
LEventHandler: TMyBeforeEvent;
LContextObj: TRttiContext;
LMethod: TRttiMethod;
LEventProp: TRttiProperty;
begin
LObj := TMyClass.Create;
LEventHandlerObj := TMyEventHandler.Create;
LContextObj := TRttiContext.Create;
LMethod := LContextObj.GetType(LEventHandlerObj.ClassType).GetMethod('DoBeforeEvent');
LEventHandler := procedure(const AInput: TArray<string>; out AOutput: TArray<string>; out ACanContinue: boolean);
begin
// Note: I don't know if/how TRttiMethod.Invoke() can handle
// 'out' parameters, so this MAY require further tweaking...
LMethod.Invoke(LEventHandlerObj, [AInput, AOutput, ACanContinue]);
end;
LEventProp := LContextObj.GetType(LObj.ClassType).GetProperty('OnBeforeEvent');
LEventProp.SetValue(LObj, TValue.From(LEventHandler));
end;
Same with using TMyEventHandler.DoEvent() with TMyClass.OnEvent.
Alternatively, if you change the reference to procedure(...) into procedure(...) of object instead, you can then use the TMethod record to assign TMyEventHandler.DoBeforeEvent() directly to TMyClass.OnBeforeEvent, eg:
procedure AssignEvent;
var
LObj: TMyClass;
LEventHandlerObj: TMyEventHandler;
LEventHandler: TMyBeforeEvent;
LContextObj: TRttiContext;
LEventProp: TRttiProperty;
LMethod: TRttiMethod;
begin
LObj := TMyClass.Create;
LEventHandlerObj := TMyEventHandler.Create;
LContextObj := TRttiContext.Create;
LEventProp := LContextObj.GetType(LObj.ClassType).GetProperty('OnBeforeEvent');
LMethod := LContextObj.GetType(LEventHandlerObj.ClassType).GetMethod('DoBeforeEvent');
with TMethod(LEventHandler) do
begin
Code := LMethod.CodeAddress;
Data := LEventHandlerObj;
end;
LEventProp.SetValue(LObj, TValue.From(LEventHandler));
end;
Same with using TMyEventHandler.DoEvent() with TMyClass.OnEvent.
Good question!
I believe I have found a sound approach.
To illustrate it, create a new VCL application with a form (Form1) and a single button (Button1) on it. Give the form an OnClick handler:
procedure TForm1.FormClick(Sender: TObject);
begin
ShowMessage(Self.Caption);
end;
We will attempt to assign this handler to the button's OnClick property using only RTTI and property and method names (as strings).
The key thing to remember is that a method pointer is a pair (code pointer, object pointer), specifically, a TMethod record:
procedure TForm1.FormCreate(Sender: TObject);
var
C: TRttiContext;
b, f: TRttiType;
m: TRttiMethod;
bp: TRttiProperty;
handler: TMethod;
begin
C := TRttiContext.Create;
f := C.GetType(TForm1);
m := f.GetMethod('FormClick');
b := C.GetType(TButton);
bp := b.GetProperty('OnClick');
handler.Code := m.CodeAddress;
handler.Data := Form1;
bp.SetValue(Button1, TValue.From<TNotifyEvent>(TNotifyEvent(handler)));
end;
In Delphi 6, I could change the Mouse Cursor for all forms using Screen.Cursor:
procedure TForm1.Button1Click(Sender: TObject);
begin
Screen.Cursor := crHourglass;
end;
I am searching the equivalent in Firemonkey.
Following function does not work:
procedure SetCursor(ACursor: TCursor);
var
CS: IFMXCursorService;
begin
if TPlatformServices.Current.SupportsPlatformService(IFMXCursorService) then
begin
CS := TPlatformServices.Current.GetPlatformService(IFMXCursorService) as IFMXCursorService;
end;
if Assigned(CS) then
begin
CS.SetCursor(ACursor);
end;
end;
When I insert a Sleep(2000); at the end of the procedure, I can see the cursor for 2 seconds. But the Interface probably gets freed and therefore, the cursor gets automatically resetted at the end of the procedure. I also tried to define CS as a global variable, and add CS._AddRef at the end of the procedure to prevent the Interface to be freed. But it did not help either.
Following code does work, but will only work for the main form:
procedure TForm1.Button1Click(Sender: TObject);
begin
Application.MainForm.Cursor := crHourGlass;
end;
Since I want to change the cursor for all forms, I would need to iterate through all forms, but then the rollback to the previous cursors is tricky, as I need to know the previous cursor for every form.
My intention:
procedure TForm1.Button1Click(Sender: TObject);
var
prevCursor: TCursor;
begin
prevCursor := GetCursor;
SetCursor(crHourglass); // for all forms
try
Work;
finally
SetCursor(prevCursor);
end;
end;
You'd have to implement your own cursor service that makes it possible to enforce a certain cursor.
unit Unit2;
interface
uses
FMX.Platform, FMX.Types, System.UITypes;
type
TWinCursorService = class(TInterfacedObject, IFMXCursorService)
private
class var FPreviousPlatformService: IFMXCursorService;
class var FWinCursorService: TWinCursorService;
class var FCursorOverride: TCursor;
class procedure SetCursorOverride(const Value: TCursor); static;
public
class property CursorOverride: TCursor read FCursorOverride write SetCursorOverride;
class constructor Create;
procedure SetCursor(const ACursor: TCursor);
function GetCursor: TCursor;
end;
implementation
{ TWinCursorService }
class constructor TWinCursorService.Create;
begin
FWinCursorService := TWinCursorService.Create;
FPreviousPlatformService := TPlatformServices.Current.GetPlatformservice(IFMXCursorService) as IFMXCursorService; // TODO: if not assigned
TPlatformServices.Current.RemovePlatformService(IFMXCursorService);
TPlatformServices.Current.AddPlatformService(IFMXCursorService, FWinCursorService);
end;
function TWinCursorService.GetCursor: TCursor;
begin
result := FPreviousPlatformService.GetCursor;
end;
procedure TWinCursorService.SetCursor(const ACursor: TCursor);
begin
if FCursorOverride = crDefault then
begin
FPreviousPlatformService.SetCursor(ACursor);
end
else
begin
FPreviousPlatformService.SetCursor(FCursorOverride);
end;
end;
class procedure TWinCursorService.SetCursorOverride(const Value: TCursor);
begin
FCursorOverride := Value;
TWinCursorService.FPreviousPlatformService.SetCursor(FCursorOverride);
end;
end.
MainUnit:
procedure TForm1.Button1Click(Sender: TObject);
var
i: integer;
begin
TWinCursorService.CursorOverride := crHourGlass;
try
Sleep(2000);
finally
TWinCursorService.CursorOverride := crDefault;
end;
end;
The IFMXCursorService is how the FMX framework manages cursors. It is not intended for your use. The mechanism that you are meant to use is the form's Cursor property.
This means that you will need to remember the cursor for each form in order to restore it. I suggest that you use a dictionary to do that. Wrap the functionality up into a small class and then at least the pain is localized to the implementation of that class. You can make the code at the call site reasonable.
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.
I do not understand where are the objects below and how to clear them?
for example:
public
Alist: TStringlist;
..
procedure TForm1.FormCreate(Sender: TObject);
begin
Alist:=Tstringlist.Create;
end;
procedure TForm1. addinstringlist;
var
i: integer;
begin
for i:=0 to 100000 do
begin
Alist.add(inttostr(i), pointer(i));
end;
end;
procedure TForm1.clearlist;
begin
Alist.clear;
// inttostr(i) are cleared, right?
// Where are pointer(i)? Are they also cleared ?
// if they are not cleared, how to clear ?
end;
procedure TForm1. repeat; //newly added
var
i: integer;
begin
For i:=0 to 10000 do
begin
addinstringlist;
clearlist;
end;
end; // No problem?
I use Delphi 7. In delphi 7.0 help file, it says:
AddObject method (TStringList)
Description
Call AddObject to add a string and its associated object to the list.
AddObject returns the index of the new string and object.
Note:
The TStringList object does not own the objects you add this way.
Objects added to the TStringList object still exist
even if the TStringList instance is destroyed.
They must be explicitly destroyed by the application.
In my procedure Alist.add(inttostr(i), pointer(i)), I did not CREATE any object. Were there objects or not ?
how can I clear both inttostr(i) and pointer(i).
Thank you in advance
There is no need to clear Pointer(I) because the pointer does not reference any object. It is an Integer stored as Pointer.
Advice: if you are not sure does your code leak or not write a simple test and use
ReportMemoryLeaksOnShutDown:= True;
If your code leaks you will get a report on closing the test application.
No the code you added does not leak. If your want to check it write a test like this:
program Project2;
{$APPTYPE CONSOLE}
uses
SysUtils, Classes;
var
List: TStringlist;
procedure addinstringlist;
var
i: integer;
begin
for i:=0 to 100 do
begin
List.addObject(inttostr(i), pointer(i));
end;
end;
procedure clearlist;
begin
List.clear;
end;
procedure repeatlist;
var
i: integer;
begin
For i:=0 to 100 do
begin
addinstringlist;
clearlist;
end;
end;
begin
ReportMemoryLeaksOnShutDown:= True;
try
List:=TStringList.Create;
repeatlist;
List.Free;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.
Try to comment List.Free line to create a memory leak and see what happens.
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.