I am working on Delphi 7 + SQLserver
in BDE all database related related erros can be handled using EDatabaseError
try
//all database related operations
Except
on EDatabaseError do
begin
showmessage(e.message)
end;
End;
but in ADO i tried different examples and i am getting different errors like EOleError/EDatabaseError/...
I Tried below 2 points to raise errors in ADO and i got different errors
1) In sql server stored procedure i am raising error on first line. when i execute that procedure using TADOStoredProc in delphi i am getting EOleError.
2) In TADOQuery i have written a wrong sql statement so when i open TADOQUery i am getting EDatabaseError.
so now i am confused how to handle ADO errors. i don't want to check for all the errors(EOleError,EDatabaseError,EAdoError...) in each and every form so i have written 2 examples,
Please suggest me which one is good. if both are wrong please give me a good example.
Example 1:
Here i am showing only one form in example so Delphi Procedure HandleErrors and function GetErrorDescription may look stupid. i don't want to write same piece of
code in all the forms. When it comes to real scenario i am gonna keep Delphi Procedure HandleErrors and function GetErrorDescription on different unit and use that unit all over the application.
type
TForm1 = class(TForm)
ADOConnection1: TADOConnection;
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
Procedure HandleErrors(e: Exception );
function GetErrorDescription : WideString;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
function TForm1.GetErrorDescription : WideString;
var
LastErrorIndex : Integer;
begin
LastErrorIndex :=ADOConnection1.Errors.Count-1;
Result:=ADOConnection1.Errors.Item[LastErrorIndex].Description;
//Code :=ADOConnection1.Errors.Item[LastErrorIndex].NativeError;
end;
procedure TForm1.HandleErrors(e: Exception);
var
Code: Integer;
ErrorDescription: WideString ;
begin
if e is EOleError then
ShowMessage(GetErrorDescription)
else
if e is EDatabaseError then
ShowMessage(GetErrorDescription)
else
if e is EADOError then
ShowMessage(GetErrorDescription)
else
ShowMessage(GetErrorDescription)
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
try
// any database related operations
except
on E : Exception do
begin
HandleErrors(E);
end;
end;
end;
end.
Example 2:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs,ADODB, StdCtrls, DB,COMOBJ;
type
TForm1 = class(TForm)
ADOConnection1: TADOConnection;
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
var
LastErrorIndex : Integer;
begin
try
// any database related operations
except
on E : Exception do
begin
LastErrorIndex :=ADOConnection1.Errors.Count-1;
ShowMessage(ADOConnection1.Errors.Item[LastErrorIndex].Description);
end;
end;
end;
end.
which example is better. Can you please suggest a good one
As I said in a comment, you shouldn't scatter AdoConnections and datasets across multiple forms/ Instead you should put them in a datamodule then Use the datamodule's unit in your forms' units. That way, you will be able to connect TDataSources and db-aware components on the form to the datamodule's datasets.
The other thing you can do is to install an application-wide exception handler to centralize your exception handling, if that's what you want to do. Delphi's Application object has an OnException event which you can assign to your own exception handler using code like that shown below.
A downside of an application-wide exception handler is that it can be difficult in the OnException handler to identify which of your objects is actually responsible for causing the exception.
type
TMainForm = class(TForm)
[...]
procedure FormCreate(Sender: TObject);
private
procedure ApplicationException(Sender: TObject; E: Exception);
public
end;
procedure TMainForm.FormCreate(Sender: TObject);
begin
Application.OnException := ApplicationException;
end;
procedure TMainForm.ApplicationException(Sender: TObject; E: Exception);
var
AErrors : Errors;
AError : Error;
i : Integer;
S : String;
begin
Caption := 'Exception';
if E is EDatabaseError then begin
AErrors := DataModule1.AdoQuery1.Connection.Errors;
for i := 0 to AErrors.Count - 1 do begin
AError := AErrors.Item[i];
S := Format('Number: %d, NativeError: %d, source: %s, description: %s',
[AError.Number, AError.NativeError, AError.Source, AError.Description]);
Memo1.Lines.Add(S);
end;
end;
end;
Related
I wrote a simple component that monitors a folder and triggers an event when it detects changes. It works well... apparently. But I'm not sure of one thing. From time to time, the main thread may need to update the monitored path and I'm not sure if I've done this right. It is about the SetNewPath procedure. This is executed from the main thread and it changes the UpdatePath variable from the other thread. It is possible to create an conflict when the main thread writes to UpdatePath and the component thread tries to read its value in the Execute cycle ?
FolderMonitor.pas
unit FolderMonitor;
interface
uses
SysUtils, Windows, Classes, ExtCtrls;
type
TOnFolderChange = procedure(Sender: TObject) of object;
TFolderMonitor = class(TThread)
private
MainWait: THandle;
UpdatePath: Boolean;
TimeOut: Cardinal;
FPath: String;
FOnFolderChange: TOnFolderChange;
procedure DoOnFolderChange;
procedure SetNewPath(Path:String);
protected
procedure Execute; override;
public
constructor Create(const FolderPath: String; OnFolderChangeHandler: TOnFolderChange);
destructor Destroy; override;
procedure Unblock;
property Path: String read FPath write SetNewPath;
property OnFolderChange: TOnFolderChange read FOnFolderChange write FOnFolderChange;
end;
implementation
constructor TFolderMonitor.Create(const FolderPath: String; OnFolderChangeHandler: TOnFolderChange);
begin
inherited Create(True);
FOnFolderChange:=OnFolderChangeHandler;
FPath:=FolderPath;
UpdatePath:=false;
FreeOnTerminate:=false;
MainWait:=CreateEvent(nil,true,false,nil);
Resume;
end;
destructor TFolderMonitor.Destroy;
begin
CloseHandle(MainWait);
inherited;
end;
procedure TFolderMonitor.DoOnFolderChange;
begin
if Assigned(FOnFolderChange) then
Synchronize(procedure
begin
FOnFolderChange(Self);
end);
end;
procedure TFolderMonitor.Unblock;
begin
PulseEvent(MainWait);
end;
procedure TFolderMonitor.SetNewPath(Path:String);
begin
FPath:=Path;
UpdatePath:=true;
PulseEvent(MainWait);
end;
procedure TFolderMonitor.Execute;
var Filter,WaitResult: Cardinal;
WaitHandles: array[0..1] of THandle;
begin
Filter:=FILE_NOTIFY_CHANGE_DIR_NAME + FILE_NOTIFY_CHANGE_FILE_NAME + FILE_NOTIFY_CHANGE_SIZE;
WaitHandles[0]:=MainWait;
WaitHandles[1]:=FindFirstChangeNotification(PWideChar(FPath),false,Filter);
TimeOut:=INFINITE;
while not Terminated do begin
if UpdatePath then begin
if WaitHandles[1]<>INVALID_HANDLE_VALUE then FindCloseChangeNotification(WaitHandles[1]);
WaitHandles[1]:=FindFirstChangeNotification(PWideChar(FPath),false,Filter);
TimeOut:=INFINITE;
UpdatePath:=false;
end;
if WaitHandles[1] = INVALID_HANDLE_VALUE
then WaitResult:=WaitForSingleObject(WaitHandles[0],INFINITE)
else WaitResult:=WaitForMultipleObjects(2,#WaitHandles,false,TimeOut);
case WaitResult of
WAIT_OBJECT_0: Continue;
WAIT_OBJECT_0+1: TimeOut:=200;
WAIT_TIMEOUT: begin DoOnFolderChange; TimeOut:=INFINITE; end;
end;
if WaitHandles[1] <> INVALID_HANDLE_VALUE then
FindNextChangeNotification(WaitHandles[1]);
end;
if WaitHandles[1] <> INVALID_HANDLE_VALUE then
FindCloseChangeNotification(WaitHandles[1]);
end;
end.
UnitMain.pas
unit UnitMain;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, FolderMonitor;
type
TForm1 = class(TForm)
Memo1: TMemo;
Edit1: TEdit;
Button1: TButton;
procedure FormCreate(Sender: TObject);
procedure OnFolderChange(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure Button1Click(Sender: TObject);
end;
var
Form1: TForm1;
Mon: TFolderMonitor;
X: integer;
implementation
{$R *.dfm}
procedure TForm1.FormCreate(Sender: TObject);
begin
X:=0;
Mon:=TFolderMonitor.Create('D:\Test',OnFolderChange);
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
Mon.Terminate;
Mon.Unblock;
Mon.WaitFor;
Mon.Free;
end;
procedure TForm1.OnFolderChange(Sender: TObject);
begin
inc(x);
Memo1.Lines.Add('changed! '+IntToStr(x));
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
Mon.Path:=Edit1.Text;
end;
end.
You have a variable shared between multiple threads, with one thread modifying the variable. This scenario is known as a data race.
Some races may be benign. This one is not. If one thread modifies the variable whilst another thread reads it, errors may occur. Because the data type is complex (pointer to heap allocated array of characters) it is quite possible for the reading thread to attempt to read from deallocated memory.
For a complex type like this you need to use a mutual exclusion lock whenever you access the value. All reads and writes must be serialised by the lock. Use a critical section or a monitor.
To ensure that you don't ever perform unprotected access it is wise to enforce this rule in code. For example, my TThreadSafe<T> described here: Generic Threadsafe Property
I try to make file manager in Delphi and there is I need to be able create new folders.
So, i got my Main Form and when I press button Create New Folder other form appears where I can type new folder name and confrim or cancel creation.
So I created new form for folder creation and make it invisible.
I made it like this - here I got procedure in Main Form
procedure TfolderFrame.CreateFolder;
begin
newFolderDialog.Visible:=true;
end;
And here's new folder form
unit FolderDialog;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs,fileOperations, StdCtrls;
type
TnewFolderDialog = class(TForm)
edtName: TEdit;
lblName: TLabel;
btnOK: TButton;
btnCancel: TButton;
procedure btnOKClick(Sender: TObject);
procedure btnCancelClick(Sender: TObject);
procedure FormActivate(Sender: TObject);
private
{ Private declarations }
public
FolderName:String;
kindOfAction:char;
hasUpdated:Boolean;
end;
var
newFolderDialog: TnewFolderDialog;
implementation
{$R *.dfm}
procedure TnewFolderDialog.btnOKClick(Sender: TObject);
begin
FolderName:=edtName.Text;
if CreateDir(FolderName)
then begin
ShowMessage('New folder created!');
end
else begin
ShowMessage('Creation failed. Error : '+ IntToStr(GetLastError));
end;
newFolderDialog.edtName.Clear;
newFolderDialog.Close;
hasUpdated:=True;
end;
procedure TnewFolderDialog.btnCancelClick(Sender: TObject);
begin
newFolderDialog.edtName.Clear;
newFolderDialog.Close;
end;
procedure TnewFolderDialog.FormActivate(Sender: TObject);
begin
hasUpdated:=false;
end;
end.
The problem is - when TfolderFrame.CreateFolder; called it just make new folder form visible and then procedure ends. But I need to made some other thigs after folder will be created, something like Refresh or stuff.
I've been trying to do it like this:
procedure TfolderFrame.CreateFolder;
begin
newFolderDialog.Visible:=true;
while not (newFolderDialog.hasUpdated) do begin
if(newFolderDialog.hasUpdated) then
RefreshAllStuff;
end;
end;
But programm just stuck because of it.
How could I call Refresh procedure in Form1 only after confirming of folder creation in Form2?
Redesign your code to use TForm.ShowModal() instead, eg:
procedure TfolderFrame.CreateFolder;
begin
if newFolderDialog.ShowModal = mrOk then
RefreshAllStuff;
end;
procedure TnewFolderDialog.btnOKClick(Sender: TObject);
begin
FolderName := edtName.Text;
if CreateDir(FolderName) then
begin
ShowMessage('New folder created!');
ModalResult := mrOk;
end
else
ShowMessage('Creation failed. Error : '+ IntToStr(GetLastError));
end;
procedure TnewFolderDialog.btnCancelClick(Sender: TObject);
begin
ModalResult =: mrCancel;
end;
procedure TnewFolderDialog.FormShow(Sender: TObject);
begin
edtName.Clear;
end;
I would like to know how to get keyboard input in my delphi application while its not focussed.
The application i am programming is going to be taking a screenshot while i am in game.
I have wrote the screen capture code but i am missing this last piece any advice would be appreciated.
You can register a hotkey (using the RegisterHotKey and UnregisterHotKey functions) and use the WM_HOTKEY message to intercept when the key is pressed.
Try this sample
type
TForm3 = class(TForm)
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
{ Private declarations }
public
procedure WMHotKey(var Message: TMessage); message WM_HOTKEY;
end;
var
Form3: TForm3;
implementation
{$R *.dfm}
{ TForm3 }
const
SaveScreeenHK=666;
procedure TForm3.FormCreate(Sender: TObject);
begin
RegisterHotKey(Handle, SaveScreeenHK , MOD_CONTROL, VK_F10);
end;
procedure TForm3.FormDestroy(Sender: TObject);
begin
UnregisterHotKey(Handle, SaveScreeenHK);
end;
procedure TForm3.WMHotKey(var Message: TMessage);
begin
//call your method here
end;
I have since been busy and created a library for getting keystrokes in delphi.
You can find it here : https://github.com/Kobusvdwalt/DelphiKeylogger
It still needs documentation but basicly you just call the olgetletter function.
What is the best solution to show that the application is doing something?
I tried showing a progress indicator, but it did not work.
UPDATE: -------------
A progress bar works fine, but isn't what I want.
I want to show a throbber, like what Web browsers use, so as long as something is being updated it keeps turning.
Cursor can also be in crHourGlass mode.
Try this:
AnimateUnit
unit AnimateUnit;
interface
uses
Windows, Classes;
type
TFrameProc = procedure(const theFrame: ShortInt) of object;
TFrameThread = class(TThread)
private
{ Private declarations }
FFrameProc: TFrameProc;
FFrameValue: ShortInt;
procedure SynchedFrame();
protected
{ Protected declarations }
procedure Frame(const theFrame: ShortInt); virtual;
public
{ Public declarations }
constructor Create(theFrameProc: TFrameProc; CreateSuspended: Boolean = False); reintroduce; virtual;
end;
TAnimateThread = class(TFrameThread)
private
{ Private declarations }
protected
{ Protected declarations }
procedure Execute(); override;
public
{ Public declarations }
end;
var
AnimateThread: TAnimateThread;
implementation
{ TFrameThread }
constructor TFrameThread.Create(theFrameProc: TFrameProc; CreateSuspended: Boolean = False);
begin
inherited Create(CreateSuspended);
FreeOnTerminate := True;
FFrameProc := theFrameProc;
end;
procedure TFrameThread.SynchedFrame();
begin
if Assigned(FFrameProc) then FFrameProc(FFrameValue);
end;
procedure TFrameThread.Frame(const theFrame: ShortInt);
begin
FFrameValue := theFrame;
try
Sleep(0);
finally
Synchronize(SynchedFrame);
end;
end;
{ TAnimateThread }
procedure TAnimateThread.Execute();
var
I: ShortInt;
begin
while (not Self.Terminated) do
begin
Frame(0);
for I := 1 to 8 do
begin
if (not Self.Terminated) then
begin
Sleep(120);
Frame(I);
end;
end;
Frame(0);
end;
end;
end.
Unit1
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, ExtCtrls, ImgList;
type
TForm1 = class(TForm)
ImageList1: TImageList;
Image1: TImage;
Button1: TButton;
Button2: TButton;
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
procedure UpdateFrame(const theFrame: ShortInt);
end;
var
Form1: TForm1;
implementation
uses
AnimateUnit;
{$R *.DFM}
procedure TForm1.UpdateFrame(const theFrame: ShortInt);
begin
Image1.Picture.Bitmap.Handle := 0;
try
ImageList1.GetBitmap(theFrame, Image1.Picture.Bitmap);
finally
Image1.Update();
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
AnimateThread := TAnimateThread.Create(UpdateFrame);
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
AnimateThread.Terminate();
end;
end.
The Images
You are probably running your time consuming task in the main thread.
One option is to move it to a background thread which will allow your message queue to be serviced. You need it to be serviced in order for your progress bar, and indeed any UI, to work.
Answer to the updated question:
generate an animated gif e.g. here
add a GIF library to your environment (JEDI JVCL+JCL)
insert a TImage and load the generated gif
make it visible if you need it
A indicator is OK. You have to call Application.ProcessMessages after changing it.
"What is the best solution to show that that application is doing something?" - set mouse cursor to crHourGlass? or to create another form/frame/etc which attentions the user that the application is 'doing' something, and he needs to wait.
From your lengthy task, you can occasionally update a visual indicator, like a progress bar or anything else. However, you need to redraw the changes immediately by calling Update on the control that provides the feedback.
Don't use Application.ProcessMessages as this will introduce possible reentrancy issues.
I am currently developing a delphi application that will need a browse history and am trying to work out how exactly to implement this.
The application has 2 modes. Browse and Details. Both designed as Frames.
After a search an appropriate number of Browse Frames are created in Panel 1 and populated.
From a Browse Frame we can either open the Detail Frame, replacing the contents of Panel 1 with the contents of the Detail Frame. Alternatively a new search can be spawned, replacing the current set of results with a new set.
From the Detail Frame we can either edit details, or spawn new searches. Certain searches are only available from the Detail Frame. Others from either the Browse Frames or the Detail Frame.
Each time a user displays the Detail Frame, or spawns a new search I want to record that action and be able to repeat it. Other actions like edits or "more details" won't be recorded. (Obviously if a user goes back a few steps then heads down a different search path this will start the history fresh from this point)
In my mind I want to record the procedure calls that were made in a list e.g.
SearchByName(Search.Text);
SearchByName(ArchName.Text);
DisplayDetails(JobID);
SearchByName(EngineerName.Text);
DisplayDetails(JobID);
Then I can just (somehow) call each item in order as I go bak and forward...
In response to Dan Kelly's request to store the function:
However what I still can't see is how I call the stored function -
What you are referring to is storing a method handler. The code below demonstrates this. But, as you indicated your self, you could do a big if..then or case statement.
This all will works. But an even more "eloquent" way of doing all this is to store object pointers. For example, if a search opens another search, you pass a pointer of the first to the 2nd. Then in the 2nd if you want to refer back to it, you have a pointer to it (first check that it is not nil/free). This is a much more object oriented approach and would lend itself better to situations where someone might close one of the frames out of sequence.
unit searchit;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TSearchObject = class
FSearchValue: String;
FOnEventClick: TNotifyEvent;
constructor Create(mSearchValue: string; mOnEventClick: TNotifyEvent);
procedure FireItsEvent;
end;
type
TForm1 = class(TForm)
SearchByName: TButton;
GoBack: TButton;
DisplayDetails: TButton;
searchfield: TEdit;
jobid: TEdit;
procedure FormCreate(Sender: TObject);
procedure SearchByNameClick(Sender: TObject);
procedure GoBackClick(Sender: TObject);
procedure DisplayDetailsClick(Sender: TObject);
private
{ Private declarations }
SearchObjectsList: TStringList;
procedure DisplayDetailFunction(Sender: TObject);
procedure SearchByNameFunction(Sender: TObject);
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
constructor TSearchObject.Create(mSearchValue: string;mOnEventClick: TNotifyEvent);
begin
FOnEventClick := mOnEventClick;
FSearchValue := mSearchValue;
end;
{$R *.dfm}
procedure TSearchObject.FireItsEvent;
begin
if Assigned(FOnEventClick) then
FOnEventClick(self);
end;
procedure TForm1.SearchByNameClick(Sender: TObject);
var
mSearchObject: TSearchObject;
begin
mSearchObject := TSearchObject.Create(SearchField.Text,SearchByNameFunction);
SearchObjectsList.AddObject(SearchField.Text,mSearchObject);
end;
procedure TForm1.DisplayDetailFunction(Sender: TObject);
var
mSearchObject: TSearchObject;
begin
mSearchObject := TSearchObject(Sender);
ShowMessage('This is the Display Detail Event. The value of the JobID is '+mSearchObject.FSearchValue);
end;
procedure TForm1.SearchByNameFunction(Sender: TObject);
var
mSearchObject: TSearchObject;
begin
mSearchObject := TSearchObject(Sender);
ShowMessage('This is the SearchByName Event. The value of the Search Field is '+mSearchObject.FSearchValue);
end;
procedure TForm1.DisplayDetailsClick(Sender: TObject);
var
mSearchObject: TSearchObject;
begin
mSearchObject := TSearchObject.Create(jobid.text,DisplayDetailFunction);
SearchObjectsList.AddObject(jobid.text,mSearchObject);
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
SearchObjectsList := TStringList.Create;
end;
procedure TForm1.GoBackClick(Sender: TObject);
var
mSearchObject: TSearchObject;
begin
if SearchObjectsList.count=0 then
showmessage('Cannot go Back!')
else begin
mSearchObject := TSearchObject(SearchObjectsList.Objects[SearchObjectsList.count-1]);
mSearchObject.FireItsEvent;
SearchObjectsList.Delete(SearchObjectsList.count-1);
end;
end;
end.
Keep track of everything in a TStringList; when they go "Back" you delete from the string list. This is a sort of prototype:
type
TSearchObject = class
FSearchFunction,FSearchValue: String;
constructor Create(mSearchFunction,mSearchValue: string);
end;
type
TForm1 = class(TForm)
SearchByName: TButton;
GoBack: TButton;
DisplayDetails: TButton;
searchfield: TEdit;
procedure FormCreate(Sender: TObject);
procedure SearchByNameClick(Sender: TObject);
procedure GoBackClick(Sender: TObject);
procedure DisplayDetailsClick(Sender: TObject);
private
{ Private declarations }
SearchObjectsList: TStringList;
jobid: String; //not sure how you get this
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
constructor TSearchObject.Create(mSearchFunction,mSearchValue: string);
begin
FSearchFunction := mSearchFunction;
FSearchValue := mSearchValue;
end;
{$R *.dfm}
procedure TForm1.SearchByNameClick(Sender: TObject);
var
mSearchObject: TSearchObject;
begin
mSearchObject := TSearchObject.Create('SearchByName',SearchField.Text);
SearchObjectsList.AddObject(SearchField.Text,mSearchObject);
end;
procedure TForm1.DisplayDetailsClick(Sender: TObject);
var
mSearchObject: TSearchObject;
begin
mSearchObject := TSearchObject.Create('DisplayDetails',JobID);
SearchObjectsList.AddObject(JobId,mSearchObject);
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
SearchObjectsList := TStringList.Create;
end;
procedure TForm1.GoBackClick(Sender: TObject);
var
mSearchObject: TSearchObject;
begin
if SearchObjectsList.count=0 then
showmessage('Cannot go Back!')
else begin
mSearchObject := TSearchObject(SearchObjectsList.Objects[SearchObjectsList.count-1]);
if mSearchObject.FSearchFunction ='SearchByName' then
ShowMessage('Value of Search Field:'+mSearchObject.FSearchValue)
else
ShowMessage('Value of JobID:'+mSearchObject.FSearchValue);
SearchObjectsList.Delete(SearchObjectsList.count-1);
end;
end;
Another option would be to use my wizard framework, which does this with TForms but can easily also be adjusted to use frames. The concept is that each summary form knows how to create its appropriate details. In your case the framework is more of an example of how to do it, rather than a plug and play solution.
Complementing MSchenkel answer.
To persist the list between program runs, use an ini file.
Here is the idea. You have to adapt it. Specially, you have to figure out the way to convert object to string and string to object, sketched here as ObjectToString(), StringToStringID and StringToObject().
At OnClose event, write the list out to the ini file.
const
IniFileName = 'MYPROG.INI';
MaxPersistedObjects = 10;
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
var
ini: TIniFile;
i: integer;
cnt: integer;
begin
ini:=TIniFile.Create(iniFileName);
cnt:=SearchObjectsList.Count;
if cnt>MaxPersistedObjects then
cnt:=MaxPersistedObjects;
for i:=1 to MaxPersistedObjects do
if i>cnt then
ini.WriteString('SearchObjects','SearchObject'+intToStr(i),'');
else
ini.WriteString('SearchObjects','SearchObject'+intToStr(i),
ObjectToString(SearchObjectsList[i-1],SearchObjectsList.Objects[i-1]) );
ini.Free;
end;
and read it back at OnCreate event.
procedure TForm1.FormCreate(Sender: TObject);
var
ini: TIniFile;
i: integer;
begin
SearchObjectsList := TStringList.Create;
ini:=TIniFile.Create(IniFileName);
for i:=1 to MaxPersistedObjects do
begin
s:=ini.ReadString('SearchObjects','SearchObject'+intToStr(i),'');
if s<>'' then
SearchObjectsList.AddObject(StringToID(s),StringToObject(s));
end;
ini.Free;
end;