3

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.

The_Fox
  • 6,992
  • 2
  • 43
  • 69
emurad
  • 3,448
  • 10
  • 40
  • 47
  • 1
    This sounds like the same problem as your earlier question: http://stackoverflow.com/questions/4404206/why-does-the-compiler-say-undeclared-identifier-when-i-try-to-show-form-b-from – David Heffernan Dec 13 '10 at 12:30
  • +1 for David; and a 'close' request. – Jeroen Wiert Pluimers Dec 13 '10 at 13:12
  • 1
    No, as I posted below: The problem is I'm trying to call the Enabled property from a procedure defined like this: procedure Slide(Form: TForm; Show: Boolean); and not with a fixed form name. 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. – emurad Dec 13 '10 at 13:19
  • 1
    Why don't you just create your timer in your slide function? This saves you having to declare a TTimer in every form which you need to slide which sounds like a bad design choice. – David Heffernan Dec 13 '10 at 14:33
  • 1
    @emurdad: To elaborate on my earlier comment, because I suspect you may not quite follow what I mean, in your function called Slide declare a local variable of type TTimer, create it with TTimer.Create and use this timer to control your slide. Or alternatively, add a timer to your main form and make the Slide function a method of main form. – David Heffernan Dec 13 '10 at 14:43

7 Answers7

7

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;
Community
  • 1
  • 1
Rob Kennedy
  • 161,384
  • 21
  • 275
  • 467
  • -1 Abandoning type safety is not good practice and there's no compelling need for it. – David Heffernan Dec 13 '10 at 14:34
  • 2
    In what way has this abandoned type safety, @David? Both versions check that the argument they receive meets the required interface. These techniques allow *decoupling*, so the `Slide` function will work with any form, not just `Form2`. I'd prefer my second suggestion, with a common base class for all forms, but that's up to Emurad. – Rob Kennedy Dec 13 '10 at 14:42
  • 1
    @Rob you are right, I don't mean type safety, I should think harder before writing! I mean static typing rather than type safety. I still think it is a pretty revolting way to do this. Answering the question as asked isn't really going to help @emurad who appears to be quite new to programming. He needs help and encouragement to step back and look at his problem in a different way. – David Heffernan Dec 13 '10 at 14:45
  • @David Heffernan is this enough "if Assigned(Component) and (Component is TTimer) then"? BTW, I'm a web developer since almost 12 years now but Delphi documentation sucks :/ – emurad Dec 13 '10 at 14:57
  • @emurad I don't think your problems are down to Delphi documentation, which I grant isn't great. Have you read an introductory text on Delphi yet? I don't think you need to go outside the static typing system to get this done. – David Heffernan Dec 13 '10 at 15:00
  • @David - A correct answer is a correct answer. No one is required to formulate his answers according to specific circumstances a questioner might have. SO is not a class consisted of students and instructors. It is of one group: programmers. Well, at least IMHO. :) – Sertac Akyuz Dec 13 '10 at 15:36
  • As a matter of facts, I read Sams Teach Yourself Delphi in 21 Days back in 2004 but this is the first *real* program I write in Delphi. I doubt you would find an explanation of FindComponent with an example in an introductory text anyway. – emurad Dec 13 '10 at 15:39
  • 2
    @Sertac In my opinion, for what it is worth, a professional engineer should always be questioning whether or not the problem being solved is in fact the right problem. – David Heffernan Dec 13 '10 at 15:42
  • @David - I agree with that. I don't see the relevance immediately though, this is addressed to the question or the answer? – Sertac Akyuz Dec 13 '10 at 15:45
  • @Sertac I don't believe that @Rob would use such a solution in his own code. I think he's more skilled than that. – David Heffernan Dec 13 '10 at 15:54
  • @David - Oh come on.. Now you're saying you're down-voting depending on the skill of the answer provider, and not the content of the answer? That's not fair at best.. – Sertac Akyuz Dec 13 '10 at 15:59
  • @Sertac I'm not saying that. That's incidental. I wouldn't do it the way Rob suggests. – David Heffernan Dec 13 '10 at 16:08
  • In practice, we should indeed question whether we're solving the right problem, but Stack Overflow *answers* are not the place for it. If you have an *answer*, then post it in the answer section. If you're denying the premise of the question, then do so in a *comment*. (That's why Ken and I voted down [your answer](http://stackoverflow.com/questions/4426486#4426593), @David.) And I *have* used the `FindComponent` technique in my own code, such as when making modifications to the object returned by `CreateMessageDialog`. – Rob Kennedy Dec 13 '10 at 17:50
  • 1
    @Rob thanks for this, I'm relatively new on SO so this is helpful to me. I would say that your CreateMessageDialog example is a little disingenuous because you don't have control of the implementation of that routine whereas the OP does have control of all the forms in question. That would be what I would class as a *compelling* reason for leaving the static type system. – David Heffernan Dec 13 '10 at 18:44
4

Add the unit to the uses list.

...
implementation

uses Unit2;

{$R *.dfm}
...
4

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.

The_Fox
  • 6,992
  • 2
  • 43
  • 69
  • This is hardcoded. I can't do that - What's the benefit of passing the form as parameter if I can't pass another one!? – emurad Dec 13 '10 at 14:02
  • 2
    If you pass TForm, you can only access the properties and methods of TForm but not of TForm1 which is inherited form TForm and having additional members. – Bharat Dec 13 '10 at 14:07
2

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.
Bharat
  • 6,828
  • 5
  • 35
  • 56
2

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.

Toon Krijthe
  • 52,876
  • 38
  • 145
  • 202
  • The problem is I'm trying to call the Enabled property from a procedure defined like this: procedure Slide(Form: TForm; Show: Boolean); and not with a fixed form name. 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. – emurad Dec 13 '10 at 13:09
1

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;♦♠
Ken White
  • 123,280
  • 14
  • 225
  • 444
  • -1 Abandoning type safety is not good practice and there's no compelling need for it. – David Heffernan Dec 13 '10 at 14:34
  • It's not abandoning type safety, as presumably the OP is smart enough only to pass a form that has a timer on it. However, in the interest of preventing you from posting yet another erroneous comment about no type safety , I've edited to offer a more type-safe alternative. – Ken White Dec 13 '10 at 19:36
0

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)
...
mjn
  • 36,362
  • 28
  • 176
  • 378