It may seem a little newbie, but I really have got problem with it. I have a form (not the main one)for getting many different data from the user and I want to pass it to a manager class for creating an object with these. The problem is that I can't have this class to use the other unit (getting circle uses) and also it doesn't have access to the manager class instance (which is in main form).
So, what shall I do? I've already considered using public variable but I have a bad feeling about it (regarding OOD patterns).
My suggestion is to decouple data from the GUI because this is causing your problem.
If you have a form which gathers data from the user then you should distinguish the data from the form(TForm).
For example, let's assume that you have some instance of TForm and a form, which is built from three fields: username, age and location. You want the user to enter those three things, but when the user closes the form, you should pass this inserted data onto some object. Form closes, it is freed, but the object persist. Then you pass this object to your manager object.
Simple example:
This is your record which will hold the data
type
TGatheredData = record
Name: String[40];
Age: Byte;
Location: String[40];
end;
Your TForm1 might have an aditional constructor:
constructor TForm1.Create(AOwner: TComponent; var GatheredData: TGatheredData );
begin
inherited Create(AOwner);
FGatheredData := GatheredData;
//you may want to deserialize GatheredData here and show the data in your form controls
end;
You call it, pass GatheredData and then your are showing your form.
Next, when closing form, you pick upd the data from the form controls.
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
if Self.ModalResult = mrOk then
begin
//serialize your object
FGatheredData.Name := '';//name taken from control f.e. TEdit
FGatheredData.Age := '';//name taken from control f.e. TSpinButton
FGatheredData.Location := '';//name taken from control f.e. TEdit
end;
end;
Having this record of data, you may now pass it in the same manner to your Manager object.
You decoupled data from GUI in this way, and you may easly plugin in your record to a number of different forms.
Just remember to declare your record type in external unit and use that unit in your manager unit and forms unit.
Hope this helps a little.
The "manager class" shouldn't be in either form's unit, but in its own. By separating GUI code from bussiness logic you avoid problems such like this.
[Edit: I originally put this answer in a comment, but decided to move it out into full answer. TDatamodules are too important and too common in Delphi not to emphasize them and they provide built-in easy-to-use means of seperating gui from logic and data.]
Other people have given good comments about decoupling gui from the logic and data. Surprisingly, I don't think anybody has mentioned that in Delphi TDatamodules are one main means of doing this. You put your data and logic on the Datamodule, then have both forms "use" the Datamodule to get access to its data and methods. Here is brief intro: http://delphi.about.com/od/database/l/aa101601a.htm
Both of your forms (and other forms) can access datasets or other data/datastructures that are located on/in a Datamodule unit. There should be an easy to find sample project out there illustrating the setup, since it is (or at least was) the standard way to construct Delphi apps.
If I understand your question properly then you want the manager to manage the forms (not the forms to access the manger). Right? You can't close the Main Form as if you do you close the application but you CAN Hide it. (unless you create a console app). But it poses a nice little problem :)
So... Splash form (Main Form) is:
.
.
.
uses
ManagerU;
type
TFormSplash = class(TForm)
procedure FormPaint(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
Manager: TManager;
end;
var
FormSplash: TFormSplash;
implementation
{$R *.dfm}
procedure TFormSplash.FormCreate(Sender: TObject);
begin
Manager := TManager.Create;
end;
procedure TFormSplash.FormDestroy(Sender: TObject);
begin
Manager.Free;
end;
procedure TFormSplash.FormPaint(Sender: TObject);
begin
if Visible then
begin
Manager.GetData(Self);
Hide;
Manager.DoDataStuff;
Close;
end;
end;
end.
DaaObject is:
unit DataObjectU;
interface
uses classes;
type TDataObject = class(TObject)
Data: string;
end;
implementation
end.
Manager is:
interface
uses DataObjectU;
type
TManager = Class(Tobject)
MyData: TDataObject;
constructor Create; virtual;
destructor Destroy; override;
procedure GetData(OwnerForm: TForm);
procedure DoDataStuff;
end;
implementation
uses DataFormU;
{ TManager }
constructor TManager.Create;
begin
inherited Create;
MyData := TDataObject.Create;
end;
destructor TManager.Destroy;
begin
MyData.Free;
inherited;
end;
procedure TManager.DoDataStuff;
begin
// do stuff with data here
end;
procedure TManager.GetData(OwnerForm: TForm);
var
MyDataForm: TDataForm;
begin
MyDataForm := TDataForm.Create(OwnerForm);
try
if MyDataForm.ShowModal = mrOK then
begin
MyData.Data := MyDataForm.Data;
end;
finally
MyDataForm.Free;
end;
end;
end.
The Dataform is:
type
TDataForm = class(TForm)
btnOK: TButton;
procedure btnOKClick(Sender: TObject);
private
function GetData: String;
{ Private declarations }
public
{ Public declarations }
MyData: TDataObject;
property Data: String read GetData;
end;
var
DataForm: TDataForm;
implementation
{$R *.dfm}
procedure TDataForm.btnOKClick(Sender: TObject);
begin
MyData := TDataObject.Create;
ModalResult := mrOk;
end;
function TDataForm.GetData: String;
begin
Result := MyData.Data;
end;
You will need to fill in the rest of the unit code and free some stuff but essentially this:
Start Program (Creates Splash)
Splash Creates the manager calls it to get data from the dataform then hides itself
calls manager to manage the data
when manager is done it then closes.
There is no other way to shut it down now except through task manager!)
Tim
To solve circular refrence error, you use that unit in implementation section.
implementation
{$R *.DFM}
Uses <Your Unit>;
end.
Having this 3 units:
FormMain
FormEdit
UnitMyClass
You create your object in FormMain and pass it to the FormEdit in a function like:
class function FormEdit.EditMyObject(AObject: TMyClass): boolean;
this function will showModal the form. The form will do the changes to the object, and finally return True if user pressed OK.
As you can see in UnitMyClass there is no reference to FormMain or FormEdit
FWIW, I did a whole presentation on this topic in a CodeRage 9 video. It can be seen here:
https://youtu.be/qqKx8fQTTfI
Related
We have many forms in our application, and I need a global event handler to detect when one of the forms is being destroyed (and then take some action).
p.s: I want to avoid adding code to each form that will need to send a message to the main form when it's about to destroy. also most of the forms are created and destroyed dynamicaly at run-time.
I was thinking about maybe use a global TApplicationEvents.
What is the best approach for this?
Contrary to David's answer, there is a suitable framework. It's built in higher up in the class hierarchy at TComponent. Sir Rufo is on the right track, but you don't need to force your forms to be owned by this object.
You're welcome to write any number of classes that can take specialised action when a form (or any other component for that matter) is destroyed. E.g.
TDestroyedFormLogger = class(TComponent)
protected
{ Write to log file when forms are destroyed. }
procedure Notification(AComponent: TComponent; Operation: TOperation); override;
end;
TMenuManager = class(TComponent)
protected
{ Remove/hide a menu item corresponding to the form that has been destroyed. }
procedure Notification(AComponent: TComponent; Operation: TOperation); override;
end;
Now whenever you create a form, simply set a notification as follows (assuming you have given yourself access to suitable instances of the above objects):
LForm := TMyForm.Create(Application);
LForm.FreeNotification(DestroyedFormLogger);
LForm.FreeNotification(MenuManager);
This approach is better than using the OnDestroy event because that permits only 1 observer, whereas FreeNotification permits any number of observers.
NOTE: As with any useful technique, don't force-fit a problem to the technique. There may be a more appropriate technique to your specific problem. E.g. The MenuManager idea might be better solved by using the global Screen object to iterate forms OnPopup.
EDIT: Explanation of Observer Pattern
The TComponent notification mechanism is a built-in implementation of the Observer Pattern for when a component is destroyed. FreeNotification (perhaps not ideally named) is the equivalent of registerObserver and RemoveNotification the equivalent of unregisterObserver.
The whole point of the observer pattern is that the subject being observed (sometimes called publisher) has no type-specific knowledge of the objects that are observing it (sometimes called subscribers). Publishers only know that they are able to call a generic notification method on each registered subscriber (observer). This allows objects to be loosely coupled from those that are watching it. In fact the publisher doesn't even need to be observed at all. Obviously the registration method needs to be called either from the subscribers themselves or from a third-party - otherwise the decoupling objective is defeated.
Observers can be implemented at varying degrees of complexity. The simplest being an event or callback. The most complex being a dispatcher that manages registrations in-between and independent of both publishers and subscribers. The dispatcher might even implement thread switching so that publishers don't even get impacted by performance side-effects of slow subscribers.
TComponent's observer implementation has a limitation that both the publisher and subscriber must inherit from TComponent. Basically any component can register with another component to be notified of its destruction.
Perhaps the most common use of this feature in Delphi is: When component A has a reference to component B; If component B is destroyed, component A is notified so that it can set its reference to nil.
What you are wanting is for the framework to fire an event when a form is destroyed. When a form is destroyed, its destructor is run. So, in order for the framework to fire such an event, it would need to be implemented from within the form's destructor. If you take a look inside TCustomForm.Destroy, you will find that there is not such event.
From this we can conclude that there can be no application wide event fired whenever a form is destroyed. This means that you will have to implement a solution yourself. One obvious way to make this happen is to introduce a common base class for all your forms. Ensure that every form in your program derives ultimately from this common base class. Then arrange for the base class to surface an event that is fired whenever an instance is destroyed.
There seems to be some mis-understanding about what I am saying above. Craig demonstrates how to subscribe to notification of a single form's destruction. The ability to do that does not contradict what I am saying. My point is that there is no mechanism in place that allows you to subscribe to receive notification when any form is destroyed.
A constraint on modifying code in existing forms, or creation of forms, as can be seen from other answers and comments, leaves hacks and hooks. A local CBT hook, f.i., would be a little work but probably work fine. Below is one of the simpler hacky solutions.
Screen global object holds a list of forms at all times via a regular TList. TList has a virtual Notify procedure which is called every time an item is added/removed. The idea is to employ a TList derivative that overrides this method and use it in the Screen object.
type
TNotifyList = class(TList)
protected
procedure Notify(Ptr: Pointer; Action: TListNotification); override;
end;
procedure TNotifyList.Notify(Ptr: Pointer; Action: TListNotification);
begin
inherited;
if (Action = lnDeleted) and (csDestroying in TForm(Ptr).ComponentState) and
(TForm(Ptr) <> Application.MainForm) then
// do not use ShowMessage or any 'TForm' based dialog here
MessageBox(0,
PChar(Format('%s [%s]', [TForm(Ptr).ClassName, TForm(Ptr).Name])), '', 0);
end;
Testing for csDestroying is required because the Screen adds/removes forms to its list not only when forms are created/destroyed but also when they are activated etc..
Then make the Screen use this list. This requires an "accessing private fields" hack, as the FForms list is private. You can read about this hack on Hallvard Vassbotn's blog. It also requires "changing the class of an object at run time" hack. You can read about this hack on Hallvard Vassbotn's blog.
type
THackScreenFForms = class
{$IF CompilerVersion = 15}
Filler: array [1..72] of Byte;
{$ELSE}
{$MESSAGE ERROR 'verify/modify field position before compiling'}
{$IFEND}
Forms: TList;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
PPointer(THackScreenFForms(Screen).Forms)^ := TNotifyList;
end;
Note that the notification will be called for every form destruction. This also includes forms created through MessageDlg, ShowMessage etc..
This is not the best practice (have a look at David's answer) but a way to go.
Since every form can have an owner (type TComponent) and this owner gets notified, if a child component is destroyed, just create a global form owner and pass this as the owner of every created form you want to get notified on destroy.
You have to override the TComponent.Notification method and do what you have to (e.g. fire an event)
unit GlobalViewHolder;
interface
uses
Forms,
Classes;
type
TComponentNotificationEvent = procedure( Sender : TObject; AComponent : TComponent; Operation : TOperation ) of object;
TGlobalViewHolder = class( TComponent )
private
FOnNotification : TComponentNotificationEvent;
protected
procedure Notification( AComponent : TComponent; Operation : TOperation ); override;
public
property OnNotification : TComponentNotificationEvent read FOnNotification write FOnNotification;
end;
// small and simple singleton :o)
function ViewHolder : TGlobalViewHolder;
implementation
var
_ViewHolder : TGlobalViewHolder;
function ViewHolder : TGlobalViewHolder;
begin
if not Assigned( _ViewHolder )
then
_ViewHolder := TGlobalViewHolder.Create( Application );
Result := _ViewHolder;
end;
{ TGlobalViewHolder }
procedure TGlobalViewHolder.Notification( AComponent : TComponent; Operation : TOperation );
begin
inherited;
if Assigned( OnNotification )
then
OnNotification( Self, AComponent, Operation );
end;
end.
The main form owner is always Application but there is no need to track this.
Personally I'd prefer David Heffernan's solution since all my forms are allways based on a template and it would be the cleanest and easiest to implement way.
But coming from you demand
p.s: I want to avoid adding code to each form that will need to send a message to the main form when it's about to destroy. also most of the forms are created and destroyed dynamicaly at run-time.
you would be able to patch Destroy to an own method.
I'd take the latest called destructor in the chain and patch TObject.Destroy to TMyClass.Destroy. The place to implement should be the project.
The code for patching is taken from David Heffernan 's answer on Patch routine call in delphi and only included to keep the answer complete, credits regarding this code go there.
program AInformOnCloseForms;
uses
Forms,
Classes,
Windows,
Dialogs,
Unit3 in 'Unit3.pas' {Mainform},
Unit4 in 'Unit4.pas' {Form2};
{$R *.res}
// PatchCode and RedirectProcedure are taken from David Heffernans answer
// https://stackoverflow.com/a/8978266/1699210
// on "Patch routine call in delphi" , credits regarding this code go there
procedure PatchCode(Address: Pointer; const NewCode; Size: Integer);
var
OldProtect: DWORD;
begin
if VirtualProtect(Address, Size, PAGE_EXECUTE_READWRITE, OldProtect) then
begin
Move(NewCode, Address^, Size);
FlushInstructionCache(GetCurrentProcess, Address, Size);
VirtualProtect(Address, Size, OldProtect, #OldProtect);
end;
end;
type
PInstruction = ^TInstruction;
TInstruction = packed record
Opcode: Byte;
Offset: Integer;
end;
procedure RedirectProcedure(OldAddress, NewAddress: Pointer);
var
NewCode: TInstruction;
begin
NewCode.Opcode := $E9;//jump relative
NewCode.Offset := NativeInt(NewAddress)-NativeInt(OldAddress)-SizeOf(NewCode);
PatchCode(OldAddress, NewCode, SizeOf(NewCode));
end;
type
TMyClass=Class(TObject) // Dummy to handle "events"
public
Destructor Destroy;override;
End;
destructor TMyClass.Destroy;
begin
// pervent recursion from call to Showmessage
if (Self.InheritsFrom(TCustomForm)) and (Self.ClassName<>'TTaskMessageDialog') then
Showmessage(Self.ClassName);
end;
begin
RedirectProcedure(#TObject.Destroy,#TMyClass.Destroy);
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TMainform, Mainform);
Application.CreateForm(TForm2, Form2);
Application.Run;
end.
As per Vlad's request this expands on my original answer by explaining how to register all forms owned by Application without any changes to the construction of each form. I.e. forms created using TMyForm.Create(Application); and by implication also Application.CreateForm(TMyForm, MyForm);.
The original answer doesn't specify any particular means of registering for FreeNotification because the options vary according to how forms are created. Since the question answered did not put any constraints on how the forms are created, the original answer is more appropriate in the general case.
If we could ensure that Application referred to a custom subclass of TApplication, the problem would be fairly easy to solve by overriding TApplication.Notification;. That's not possible, so this special case leverages the fact that the component ownership framework notifies all owned components when another component is added or removed. So basically all we need is a component tracker also owned by Application and we can react on its "sibling" notifications.
The following test case will demonstrate that new notifications work.
procedure TComponentTrackerTests.TestNewNotifications;
var
LComponentTracker: TComponentTracker;
LInitialFormCount: Integer;
LForm: TObject;
begin
LComponentTracker := TComponentTracker.Create(Application);
try
LComponentTracker.OnComponentNotification := CountOwnedForms;
LInitialFormCount := FOwnedFormCount;
LForm := TForm.Create(Application);
CheckEquals(LInitialFormCount + 1, FOwnedFormCount, 'Form added');
LForm.Free;
CheckEquals(LInitialFormCount, FOwnedFormCount, 'Form removed');
finally
LComponentTracker.Free;
end;
end;
procedure TComponentTrackerTests.CountOwnedForms(AComponent: TComponent; AOperation: TOperation);
begin
if (AComponent is TCustomForm) then
begin
case AOperation of
opInsert: Inc(FOwnedFormCount);
opRemove: Dec(FOwnedFormCount);
end;
end;
end;
TComponentTracker is implemented as follows:
TComponentNotificationEvent = procedure (AComponent: TComponent; AOperation: TOperation) of object;
TComponentTracker = class(TComponent)
private
FOnComponentNotification: TComponentNotificationEvent;
procedure SetOnComponentNotification(const Value: TComponentNotificationEvent);
procedure DoComponentNotification(AComponent: TComponent; AOperation: TOperation);
protected
procedure Notification(AComponent: TComponent; AOperation: TOperation); override;
public
property OnComponentNotification: TComponentNotificationEvent read FOnComponentNotification write SetOnComponentNotification;
end;
procedure TComponentTracker.DoComponentNotification(AComponent: TComponent; AOperation: TOperation);
begin
if Assigned(FOnComponentNotification) then
begin
FOnComponentNotification(AComponent, AOperation);
end;
end;
procedure TComponentTracker.Notification(AComponent: TComponent; AOperation: TOperation);
begin
inherited Notification(AComponent, AOperation);
DoComponentNotification(AComponent, AOperation);
end;
procedure TComponentTracker.SetOnComponentNotification(const Value: TComponentNotificationEvent);
var
LComponent: TComponent;
begin
FOnComponentNotification := Value;
if Assigned(Value) then
begin
{ Report all currently owned components }
for LComponent in Owner do
begin
DoComponentNotification(LComponent, opInsert);
end;
end;
end;
WARNING
You could implement anything you choose in the OnComponentNotification event handler. This would include logging that the form is "destroyed". However, such a simplistic approach would actually be flawed because TComponent.InsertComponent allows a component's owner to be changed without destroying it.
Therefore to accurately report destruction, you would have to combine this with using FreeNotification as in my first answer.
This is quite easily done by setting LComponentTracker.OnComponentNotification := FDestructionLogger.RegisterFreeNotification; where RegisterFreeNotification is implemented as follows:
procedure TDestructionLogger.RegisterFreeNotification(AComponent: TComponent; AOperation: TOperation);
begin
if (AComponent is TCustomForm) then
begin
case AOperation of
opInsert: AComponent.FreeNotification(Self);
end;
end;
end;
A very simple approach could be keeping track of the Form count. When it lowers, then there is a Form destroyed. Check in Application.OnIdle:
procedure TMainForm.ApplicationEvents1Idle(Sender: TObject; var Done: Boolean);
begin
if Screen.CustomFormCount < FFormCount then
FormDestroyed;
if FFormCount <> Screen.CustomFormCount then
FFormCount := Screen.CustomFormCount;
end;
Depending on what action should be taken, you can loop through Screen.CustomForms to determine which Form was destroyed.
For example, I have a couple of functions written for my form. Now, I need the exact same functions in another form. So, how can I share them between the two forms? Please, provide a simple example if possible.
Don't put them in your form. Separate them and put them in a common unit, and add that unit to the uses clause where you need access to them.
Here's a quick example, but you can see many of the Delphi RTL units (for instance, SysUtils) that do this. (You should learn to use the VCL/RTL source and the demo apps that are included in Delphi; they could answer many of the questions you've posted more quickly than waiting for an answer here.)
SharedFunctions.pas:
unit
SharedFunctions;
interface
uses
SysUtils; // Add other units as needed
function DoSomething: string;
implementation
function DoSomething: string;
begin
Result := 'Something done';
end;
end.
UnitA.pas
unit
YourMainForm;
uses
SysUtils;
interface
type
TMainForm = class(TForm)
procedure FormShow(Sender: TObject);
// other stuff
end;
implementation
uses
SharedFunctions;
procedure TMainForm.FormShow(Sender: TObject);
begin
ShowMessage(DoSomething());
end;
end.
In more recent versions of Delphi than Delphi 7, you can create the functions/methods in a record instead:
unit
SharedFunctions;
interface
uses
SysUtils;
type
TSharedFunctions = record
public
class function DoSomething: string;
end;
implementation
function TSharedFunctions.DoSomething: string;
begin
Result := 'Something done';
end;
end;
UnitB.pas
unit
YourMainForm;
uses
SysUtils;
interface
type
TMainForm = class(TForm)
procedure FormShow(Sender: TObject);
// other stuff
end;
implementation
uses
SharedFunctions;
procedure TMainForm.FormShow(Sender: TObject);
begin
ShowMessage(TSharedFunctions.DoSomething());
end;
end.
If you need forms. You could use inherited forms. Creating a form that inherit the functions of a parent form.
The most interesting. Any changes in the parent form is reflected a change in inherited forms. You even can inherit form controls (tbutton, tlabel, etc...).
In GUI Delphi7. Option "new form", option "inherited from a existing form".
Example:
//MainForm.pas
type
TMainForm = class(TForm)
procedure MiFunction();
.
.
end;
//ChilForm.pas
type
TChildForm = class(TMainForm)
.
.
end;
i can't figure out the formatting rules here .. too many lines of code in my example to add 4 spaces to each line, so here is the link to the code i need help with
http://nitemsg.blogspot.com/2011/01/heres-unit-written-in-delphi-7-that-you.html
The problem I have is that I don't know enough about delphi to use this code with a form.
I am a drag and drop programmer only.
An example with a showmessage('friendly name =' + ... ) when a USB device is detected is what I need.
cheers,
If you are only familiar with drag-and-drop programming, and don't know much about objects or other units, then you need to get yourself familiarized with using objects other than auto-created forms and the components you drop in them.
The code at this link is an entire unit. You need to create a new Unit in your project (File > New > Unit). It will look something like this:
unit Unit1;
interface
implementation
end.
Now when you save the unit, the name of the unit will automatically change to the filename (without the extension) like this:
unit MahUSB;
interface
implementation
end.
In this example, you should use the same unit name as that source you're trying to use. Save the unit as 'MahUSB.pas', and should be in the same folder as the rest of your project (or elsewhere, just a suggestion). Copy/Paste all the code from that website and replace everything in this unit now.
Now in order to actually use this, you need to create an instance of this object. ONLY ONE INSTANCE (I say that just because by the looks of this, there's no need for more than one).
Very important: Seeing as you are not familiar with objects, let me quickly explain something. Objects need to be created in order to work. At the same time, anything that's created also needs to be free'd when you're done with it. In this case, we will create this object when your application starts, and free the object when your application closes.
Now on your MAIN FORM (not any other forms) you need to put an event handler for both OnCreate and OnDestroy. You also need to declare a variable to represent this object. In the declaration of your main form, add a variable 'USB' with the type of this object. Make sure that goes under the 'private' or 'public' section, either one is ok. Also make sure you declare the "MahUSB" unit at the top of your main unit in the uses clause.
Declaring the object in your main form:
type
TForm1 = class(TForm)
private
USB: TUsbClass;
public
end;
Creating/freeing object when your app starts/closes:
procedure TForm1.FormCreate(Sender: TObject);
begin
USB:= TUsbClass.Create;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
if assigned(USB) then USB.Free;
end;
Now we're not done yet. Now we need to add the event handlers. Notice at the top of this unit you got, there are two types called TOnDevVolumeEvent and TOnUsbChangeEvent. These are event types. The parameters in the event handlers must be identical to the parameters declared in these types. So now in your main form, declare these event handler procedures...
type
TForm1 = class(TForm)
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
USB: TUsbClass;
procedure VolumeEvent(const bInserted : boolean; const sDrive : string);
procedure ChangeEvent(const bInserted : boolean;
const ADevType,ADriverName, AFriendlyName : string);
public
end;
Now just one more thing we have to do before this will work. The USB object needs to know what event handlers to use, therefore, we need to assign these procedures to the events. Upon your form's creation, we need to assign these events...
procedure TForm1.FormCreate(Sender: TObject);
begin
USB:= TUsbClass.Create;
USB.OnUsbChange:= Self.ChangeEvent;
USB.OnDevVolume:= Self.VolumeEvent;
end;
When all is said and done, your main form unit should look something like this:
unit uUSBTest;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, MahUSB;
type
TForm1 = class(TForm)
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
USB: TUsbClass;
procedure VolumeEvent(const bInserted : boolean; const sDrive : string);
procedure ChangeEvent(const bInserted : boolean;
const ADevType,ADriverName, AFriendlyName : string);
public
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.ChangeEvent(const bInserted: boolean; const ADevType,
ADriverName, AFriendlyName: string);
begin
ShowMessage('Change event for "'+AFriendlyName+'"');
end;
procedure TForm1.VolumeEvent(const bInserted: boolean;
const sDrive: string);
begin
ShowMessage('Volume event for "'+sDrive+'\"');
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
USB:= TUsbClass.Create;
USB.OnUsbChange:= Self.ChangeEvent;
USB.OnDevVolume:= Self.VolumeEvent;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
if assigned(USB) then USB.Free;
end;
end.
And there you are! You will have these two event handler procedures where you can further handle either of those two events.
I'm in the middle of a project with a number of child forms. Many of the forms may be open at once. I'd like to know if there's already something I can use to manage and keep track of these forms, much like the windows taskbar and/or task manager. If not, then what would be the best approach? I don't want to have to reinvent the wheel if this is already done.
Description
As mentioned above, this project has many forms which may be opened at once. I will also be implementing some visual list control (much like the taskbar or task manager) for user control of these forms (or in the user's case, the forms are called windows). The most ideal way to manage these would be to first capture each of these forms as they're created and keep record of them somewhere. Some forms need this behavior, and some forms do not. For example, modal forms will never need this handling.
I will be giving the user access to show, minimize, or close these forms, as well as some other future un-thought handling, like maybe a custom popup menu associated with one of these forms (but that's another subject). The point is, I need to build something to capture these forms and keep them in order.
This will also include some other user interaction with all the forms at once, as well as simple access to each one of them, similar to how Screen.Forms already works. For example, a command to minimize all forms (FormManager.MinimizeAll), to maximize the currently active form (FormManager.ActiveForm.Maximize), or with a particular form (FormManager[3].Maximize).
Possible Options
I understand there are a few far different approaches to accomplish similar results, and haven't started coding it yet because each of those approaches has a different starting point. The options are...
Wrap Screen.Forms and other associated functionality from the Screen (which wouldn't allow too much of my desired flexibility)
Every time I create a form, register it with this form manager (which is very flexible, but I have to make sure I always register each created form)
Build a master form to register its self with the form manager and inherit everything from it (which is also very flexible, but in different ways, and much more complex)
The second option is sounding the most promising so far. But again, I don't want to start building it if there is already a solution for this. I'm pretty confident that I'm not the first person to do this. I don't know how to search for such a thing, I get nothing related to what I want on Google.
The global variable Screen (in Forms unit) does some "tracking", ie
Screen.Forms list all currently open forms;
Screen.ActiveForm form which has input focus (see also FocusedForm);
Screen.OnActiveFormChange event;
You could add each form to a TObjectList. I wrote a component called FormStack, which allows you to add forms (even forms with the same name), retrieve, remove, etc. To get a Task Manager like behavior, I think you'd just need to iterate the list to obtain form names . Hopefully you can use something here to shed some light on your idea..
Here's the code for FormStack.
unit uFormstack;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, Contnrs;
type
TFormstack = class(TComponent)
private
{ Private declarations }
FormList: TObjectList;
protected
{ Protected declarations }
public
{ Public declarations }
Constructor Create(AOwner: TComponent); Override;
Destructor Destroy; Override;
Procedure Add(InstanceClass: TComponentClass; Var Reference);
Procedure RemoveLast;
Procedure RemoveAll;
Function FindForm(AComponentClass: TComponentClass): Boolean;
Function GetForm(AComponentClass: TComponentClass): TObject;
Function GetByIndex(AIndex: Integer): TObject;
Procedure RemoveByIndex(AIndex: Integer);
published
{ Published declarations }
end;
procedure Register;
implementation
//{$R *.res}
procedure Register;
begin
RegisterComponents('FormStack', [TFormstack]);
end;
{-----------------------------------------------------------------------------
TFormStack
-----------------------------------------------------------------------------}
Constructor TFormStack.Create(AOwner: TComponent);
Begin
Inherited Create(AOwner);
FormList := TObjectList.Create;
FormList.OwnsObjects := True;
End;
Destructor TFormStack.Destroy;
Begin
FormList.Free;
Inherited Destroy;
End;
Procedure TFormStack.Add(InstanceClass: TComponentClass; Var Reference);
Var
Instance: TComponent;
Begin
Instance := TComponent(InstanceClass.NewInstance);
TComponent(Reference) := Instance;
Instance.Create(Self); // Owner is FormList <<-- blows up if datamodule in D2010
FormList.Add(Instance);
Instance.Tag := FormList.Count-1;
End;
Procedure TFormStack.RemoveAll;
Var
I: Integer;
Begin
For I := FormList.Count -1 downto 0 do // last in first out
begin
Self.RemoveLast;
End;
End;
// This removes the last form on the stack
Procedure TFormStack.RemoveLast;
Begin
if FormList.Count > 0 then
FormList.Remove(FormList.Items[FormList.Count-1]);
End;
Function TFormStack.FindForm(AComponentClass: TComponentClass): Boolean;
Var
I: Integer;
Begin
Result := False;
For I := FormList.Count-1 downto 0 do
If Formlist.Items[I].ClassType = AComponentClass then
Result := True;
End;
Function TFormStack.GetForm(AComponentClass: TComponentClass): TObject;
Var
I: Integer;
begin
Result := Nil;
For I := FormList.Count-1 downto 0 do
If Formlist.Items[I].ClassType = AComponentClass then
Result := FormList.Items[I];
end;
Function TFormStack.GetByIndex(AIndex: Integer): TObject;
begin
Result := Nil;
If FormList.Count-1 >= AIndex then
Result := FormList.Items[AIndex];
end;
Procedure TFormStack.RemoveByIndex(AIndex: Integer);
begin
If FormList.Count-1 >= AIndex then
FormList.Remove(FormList.Items[AIndex]);
end;
end.
If I understand you correctly, you want to track this in code while the app is running?
Maybe you can do something with Screen.Forms?
I'm trying to call the Enabled property of a Timer from a procedure defined like this: procedure Slide(Form: TForm; Show: Boolean); and not with a fixed form name (like: Form2.Timer...)
After putting the form's unit in the uses list, this works: Form2.Timer1.Enabled := True;
but the following is not working: Form.Timer1.Enabled := True; (where Form is the form passed as parameter to the procedure.
How to get access to the Timer component on my form?
Thanks in advance.
If every form you're going to pass into your function will have a published field named "Timer1," then you can use the FindComponent method to get a reference to it:
procedure Slide(Form: TForm; Show: Boolean);
var
TimerObj: TComponent;
Timer: TTimer;
begin
TimerObj := Form.FindComponent('Timer1');
Assert(Assigned(TimerObj), 'Form has no Timer1');
Assert(TimerObj is TTimer, 'Form.Timer1 is not a TTimer');
Timer := TTimer(TimerObj);
// Continue using Form and Timer
end;
That's a rather weak interface to program against, though. If you've accidentally neglected to place a timer on your form, or if you've given it the wrong name, or if you've given it a different visibility, you won't discover your mistake until run time (when the assertions fail). And even if the form does meet the required interface, there's no guarantee that it was intentional. There may be lots of forms that have published TTimer fields named Timer1, but they're not all meant to be used with this Slide function. They might already be using their timers for other purposes, so calling Slide on them will break other parts of your program, possibly in difficult-to-debug ways.
It would be a slightly stronger interface if you gave the timer a more descriptive name, such as SlideTimer. Timer1 merely says it was the first TTimer you created on that form, and once you leave the Form Designer, that ceases to be a meaningful designation. You are not required to use the IDE's naming choices.
Another, better, option is to make all your slidable forms have a common base class, and put the timer in that base class. Then, change the parameter type in Slide to take that form class instead of just TForm.
type
TSlidableForm = class(TForm)
Timer1: TTimer;
end;
procedure Slide(Form: TSlidableForm; Show: Boolean);
You seemed concerned that you would have to include a reference to Form2's unit in your Slide unit, and that's generally a good concern to have. You don't want your code to be too tightly coupled since this Slide function should be able to work with more than just one form in one project. But if it's too loose, then you run into the problems I described above. This TSlidableForm class is a compromise; your Slide function isn't bound directly to TForm2 or its unit. Change TForm2 to descend from TSlidableForm.
The_Fox's second suggestion is a variation on my first, but there's still another variation. Instead of passing the name of the timer component, you can pass a reference to the component itself:
procedure Slide(Form: TForm; Timer: TTimer; Show: Boolean);
Now you don't need to use FindComponent to search for the timer to use; you've provided a direct reference to it. The component's name doesn't even matter, so different forms can use different names. You can call it like this:
Slide(Form2, Form2.Timer1, True);
Slide(AnotherForm, AnotherForm.SlideTimer, False);
Once you've come this far, you may go beyond your original question and realize that the timer doesn't even need to belong to the form anymore. You could create it specially for the call to Slide, or the timer could belong to something else entirely (like a data module). But if you're going to create the timer just for the Slide call, then you could use David's suggestion of creating the timer within the routine itself and not have the caller or the form deal with it at all:
procedure Slide(Form: TForm; Show: Boolean);
var
Timer: TTimer;
begin
Timer := TTimer.Create(nil);
try
Timer.OnTimer := ...;
Timer.Interval := 500;
Timer.Enabled := True;
// Put your normal Slide stuff here
finally
Timer.Free;
end;
end;
You cannot access the Timer from your procedure because your parameter is a TForm, and TForm does not have a Timer1 member. You have to adjust your procedure like this:
uses
Unit2; //unit with your form
procedure Slide(Form: TForm2; Show: Boolean); //change TForm2 to the classname you use
begin
Form.Timer1.Enabled := True;
end;
Edit:
If you want to pass any form, you can try this:
procedure Slide(Form: TForm; const aTimerName: string; Show: Boolean);
var
lComponent: TComponent;
begin
lComponent := Form.FindComponent(aTimerName);
if Assigned(lComponent) and (lComponent is TTimer) then
TTimer(lComponent).Enabled := True;
end;
Call like this from a button on your form:
procedure TForm2.Button1Click(Sender: TObject);
begin
Slide(Self, 'Timer1', False);
end;
Or you let your Form inherit an interface with methods to turn on the timer. It's a little bit more complicated though.
Add the unit to the uses list.
...
implementation
uses Unit2;
{$R *.dfm}
...
Use Form2's unit name in uses list of current unit.
For your Edit:
By using TForm you cant access TTimer because TForm has no fields or properties as TTimer.
So you need to use TForm1 as the parameter.
If you are having a form say Form1, for which you are creating multiple instances and showing to the user, then change your procedure syntax to procedure Slide(Form: TForm1; Show: Boolean);
But if you are having multiple forms with different components, it will become difficult. You need to overload procedures with different parameters. Below code shows the approach.
Form1 Unit
var
Form1: TForm1;
implementation
{$R *.dfm}
uses Unit3;
procedure TForm1.Button1Click(Sender: TObject);
begin
Slide(Form1, True)
end;
Form2 Unit
var
Form2: TForm2;
implementation
{$R *.dfm}
uses Unit3;
procedure TForm2.Button1Click(Sender: TObject);
begin
Slide(Form2, True)
end;
Unit in which your procedures lies
unit Unit3;
interface
uses Forms, ExtCtrls, Unit1, Unit2;
procedure Slide(Form: TForm1; Show: Boolean) overload;
procedure Slide(Form: TForm2; Show: Boolean) overload;
implementation
procedure Slide(Form: TForm2; Show: Boolean);
begin
Form.Timer1.Enabled := True;
end;
procedure Slide(Form: TForm1; Show: Boolean);
begin
Form.Timer1.Enabled := True;
end;
end.
Just to add some more information.
In a Delphi project, code is organised into units. Each unit is a file with a name and a .pas extention.
The unit has the following form:
unit Name;
interface
uses
// The definition of these units can be used both in the
// interface as in the implementation section.
unit1, unit2;
// Public interface, visible to other units that use this unit.
implementation
uses
// The definition of these units can be used only in the
// implementation section.
unit3, unit4;
// Private implementation, not visible outside.
initialization
// code to initialize the unit, run only once
finalization
// code to cleanup the unit, run only once
end.
A unit can use anything defined in the unit as long as it is defined before it is beïng used.
Sometimes this leads to confusing situations if names are identical:
unit1;
interface
type
TTest = Integer;
// ...
unit2;
interface
type
TTest = Boolean;
// ...
unit3;
interface
uses
unit1, unit2;
var
a: TTest; // Which TTest?
// ...
You can solve this by either knowing the evaluation order, or use a unit prefix:
unit3;
interface
uses
unit1, unit2;
var
a: unit1.TTest; // TTest from unit1
b: unit2.TTest; // TTest from unit2
// ...
In your case, you need to use the unit in which Form2 is defined.
If the timer is needed by both forms, you can also use a datamodule (just like a form, you can drag nonvisible components to it but they won't be visible). Both forms then can use the datamodule.
Edit
You try to use:
procedure Slide(Form: TForm; Show: Boolean);
And TForm has no Timer1.
You can do the following:
procedure Slide(Form: TForm2; Show: Boolean);
If TForm2 is the form containing the Timer.
The easiest way is to find it on the form:
procedure Slide(Form: TForm; Show: Boolean);
var
Timer: TTimer;
begin
Timer := Form.FindComponent('Timer1');
if Assigned(Timer) then
Timer.Enabled := True;
end;
To appease David ;-), here's a more "type-safe" alternative:
procedure Slide(Form: TForm; Show: Boolean);
var
i: Integer;
begin
// Could use a TComponent and for..in instead. This works in
// all Delphi versions, though.
for i := 0 to Form.ComponentCount - 1 do
if Form.Components[i] is TTimer then
TTimer(Form.Components[i]).Enabled := True;
end;♦♠
A simple (and OOP) way to implement this is to use interfaces.
Declare an interface ISlideable which defines a SlideTimer property (with getter and setter methods) and then write the Slide method like
Slide(const Target: ISlideable; Show: Boolean);
And every form which should be passed says that it is Slideable
MyFormN = class(TForm, ISlideable)
...