Recent

Author Topic: Creating a Component 101  (Read 532 times)

Davo

  • Full Member
  • ***
  • Posts: 131
Creating a Component 101
« on: July 18, 2020, 12:15:09 pm »
Embarking on creating my first component. I'm uncertain about one aspect and based on past practice I'd normally just go ahead and create the packaged component using test code to see if it works. But I read that it's very important to test the component code extensively first before packaging it into a component and that it's definitely not a good idea to package code that contains a flaw as the conseqences can be dire for your copy of Lazarus itself. So I'd like to resolve the uncertainty before going on to the packaging stage.

The uncertainty is how the component's OnClick event will behave after a program developer selects it for use in his application using his object inspector. I have two questions.

By way of background, the component displays two images that alternate with each other. One image is of a switch in the up position and the other is of the switch in the down position. I can create the component in a couple of different ways :

1. As a TCustomControl object that contains two separate TImages – one each for the two different switch positions. In the object's constructor each image gets its own switch position by using its picture.LoadFromFile function, or

2. As a TImage object that dynamically changes its picture property at run time to show one or other of the two different switch positions using the LoadFromFile function.

The first question concerns case 1. In that case in the object constructor both of the two TImages' OnClick events are pointed to a single method called SwitchClick that alternates the display of the images. Of course the TCustomControl object also has its own OnClick event but that is not triggered when the user clicks on one or other of the two images. In effect, the images hide the object's OnClick event. But the object's OnClick event can be triggered if the code line “Click” is included in SwitchClick. The following code extract shows this using a dummy procedure ObjectClick to illustrate the point.

Code: Pascal  [Select][+][-]
  1.   TSwitch = class(TCustomControl)
  2.   private
  3.     fImageUp: TImage;
  4.     fImageDown: TImage;
  5.     procedure SwitchClick(Sender:TObject);
  6.     procedure ObjectClick(Sender:TObject);     (*****)
  7.     procedure flip;
  8.   public
  9.     constructor Create(aOwner: TComponent);
  10.   published
  11.   end;
  12.  
  13. constructor TSwitch.Create(aOwner: TComponent);
  14. begin
  15.   inherited;
  16.   fImageUp := TImage.create (aOwner as TForm);
  17.   fImageDown := TImage.create (aOwner as TForm);
  18.   fImageUp.parent := aOwner as TForm;
  19.   fImageUp.picture.LoadFromFile('Brown_switch_up.jpg');
  20.   fImageUp.onClick := @SwitchClick;    
  21.   fImageDown.parent := aOwner as TForm;
  22.   fImageDown.picture.LoadFromFile('Brown_switch_down.jpg');
  23.   fImageDown.onClick := @SwitchClick;    
  24.   onClick := @ObjectClick;     (*****)
  25. end;
  26.  
  27. procedure TSwitch.SwitchClick(Sender:TObject);
  28. begin
  29.   flip;
  30.   Click;
  31. end;
  32.  
  33. procedure TSwitch.ObjectClick(Sender:TObject);   (*****)
  34. begin                                            (*****)
  35.   showmessage('Component was clicked');          (*****)
  36. end;                                             (*****)
  37.  
  38. procedure TSwitch.Flip;
  39. begin
  40.  fUp := not fUp;
  41.   if fUp then
  42.     begin
  43.       fImageUp.show;
  44.       fImageDown.hide;
  45.       .....
  46.     end
  47.   else
  48.     begin
  49.       fImageUp.hide;
  50.       fImageDown.show;
  51.       .....
  52.     end;
  53. end;

The first question is : assuming that the code lines marked (*****) are removed before packaging the component and the program developer creates an OnClick event for the component in his application that includes the code line “ShowMessage('Component was clicked')”, what will be the outcome after an end user clicks one of the component's images? Ideally the component's SwitchClick method should first alternate the display of the two images by calling flip and then the OnClick event in the application should show the message. But will that in fact be the case?

The second question concerns case 2. Since in that case the object itself is derived from TImage it's OnClick event is immediately available to user action. In the object's constructor that OnClick event is pointed to the SwitchClick method which alternates the display of the switch position by using the LoadFromFile function to get the appropriate switch-up or switch-down image. The following code extract shows this :

Code: Pascal  [Select][+][-]
  1. TSwitch = class(TImage)
  2.   private
  3.     procedure SwitchClick(Sender:TObject);
  4.     procedure flip;
  5.   public
  6.     constructor Create(aOwner: TComponent);
  7.   published
  8.   end;
  9.  
  10. constructor TSwitch.Create(aOwner: TComponent);
  11. begin
  12.   inherited;
  13.   picture.LoadFromFile('Brown_switch_up.jpg');
  14.   parent := aOwner as TForm;
  15.   onClick := @SwitchClick;
  16. end;
  17.  
  18. procedure TSwitch.SwitchClick(Sender:TObject);
  19. begin
  20.   flip;
  21. end;
  22.  
  23. procedure TSwitch.Flip;
  24. begin
  25.  fUp := not fUp;
  26.   if fUp then
  27.     begin
  28.       picture.LoadFromFile('Brown_switch_up.jpg');
  29.      .....;
  30.     end
  31.   else
  32.     begin
  33.       picture.LoadFromFile('Brown_switch_down.jpg');
  34.      .....
  35. end;

The second question is what will be the outcome of an end user clicking the component if the program developer, as before, creates an OnClick event in the application that shows the same message? My guess is that, as written, only the actions in SwitchClick will be implemented and the OnClick event in the application will not be triggered. But is that correct? If that's not correct and the message does in fact get displayed – all well and good. But if the analysis is correct then adding the code line “Click” to SwitchClick to try to solve the problem would seem to be problematic as that would be a recursive call to itself and that might create havoc. So what then is the solution?

Any response would be much appreciated. Thanks.

jamie

  • Hero Member
  • *****
  • Posts: 3664
Re: Creating a Component 101
« Reply #1 on: July 18, 2020, 12:25:15 pm »
  1. do not use file operations to load your images especially being loaded in the IDE, use resource methods instead where by they get linked and keep your images small or some sort of compressed option.

   2. You have the control and component states to look at during component loading and initialization periods. Use the flags in there to indicate what is taking place...

    (a)   Is it loading ? This would be when it's reading the lfm data from resource

    (b)   is it in design mode, this would be the csDesiging flag which tells you to limit your use of the control to that to only what is needed on the IDE UI.

   3. Try to use existing property editors unless absolutely needed you need a custom one.

that should get you started.

     
 
The only true wisdom is knowing you know nothing

wp

  • Hero Member
  • *****
  • Posts: 7636
Re: Creating a Component 101
« Reply #2 on: July 18, 2020, 12:34:00 pm »
I did not try too much to understand your issues because I think displaying the switch images in TImage components makes the entire component much more complicated than needed. You noticed yourself that the clicked image must propagate its events to the component (you are talking of OnClick, but maybe a user needs the OnMouseMove or OnEnter or whatever, and you have the same problem again and again).

Why don't you just use two TBitmaps for the on/off states and just paint them on the TSwitch canvas, similar to TBitBtn? Instead of loading the images from the file system you should provide them in the program resource. Because your component will not work correctly any more when the image files were deleted, forgotten to copy, ...

If you insist on using a TImage do not create it with the form as owner. The simple rule is: The one who created something must take care of cleaning it up. So, the Owner of the images must be the TSwitch itself.

As for your second question: When you derive a class from TImage do not use its events for internal purposes because they are intended for the user of the component and you will have lots of efforts to re-establish them. Every event is called from an internal virtual method which you must overide. In case of the OnClick there is a virtual Click() method. So when you override this method you should first call your stuff and then call "inherited" - the latter cass will fire the event handler. Or depending on your requirements you might have to exchange the order and fire the event first and afterwards call your stuff.

A reason NOT to use TImage as ancestor could be that TImage inherits from TGraphicControl and thus does not receive keyboard events. This means that you user will not be able to change the switch by a key press.

Another more general tip: Do not name the component simply TSwitch because that there is some chance that another library will contain a similar component with the same name; having two components with the same name is not allowed, and your user will have to decide whether he uninstalls the other existing package just to test your component. Guess what he will do... Add your initials to make the name more unique, or if you plan to add more components to a package "SuperComponents" then add the initials of the packae, i.e. TSCSwitch.
« Last Edit: July 18, 2020, 12:49:52 pm by wp »
Mainly Lazarus trunk / fpc 3.2.0 / all 32-bit on Win-10, but many more...

Davo

  • Full Member
  • ***
  • Posts: 131
Re: Creating a Component 101
« Reply #3 on: July 20, 2020, 12:01:17 am »
Thank you Jamie and wp for your replies. As always, they are really appreciated.

Jamie

I had assumed that when the time came to package the component I could include the two images in a suitable resource file. But, as you say, the LoadFromFile function is probably not suitable for use in that context. Since this is my first attempt at creating a component and it's probably not sensible to try to solve too many problems all at once, I can side-step this particular issue for the moment by using a couple of rectangular shapes to represent the switch instead of the two images - see comparison image attached. That way the component doesn't have to get anything visual from outside its own unit. In due course, once I know that I can create a component successfully, I can get back to using images as they are not as clunky looking as a couple of shapes.

As to the rest of your advice, I'm sure it's good but I'll need some time to study the matters that you raise.

wp

Regarding the use of images – please see above. And your advice regarding the owner is noted.

As to the second question, if I understand you properly I should allow he underlying control's OnClick method to be overridden by declaring it “virtual” and then in that override first include the code instructions that I want to run and end it with a call to inherited, That makes sense to me. I see that in the code example for the declaration of a class on page 523 of “Lazarus The Complete Guide” there is a line : “procedure Clear; virtual;”. So, taking that as a cue I created the following code :

Code: Pascal  [Select][+][-]
  1. procedure OnClick; virtual;
  2.  
  3. procedure TSwitch.OnClick;
  4. begin
  5.   Play;
  6.   flip;
  7.   inherited;
  8. end;

But it does not compile. The code line “inherited;” produces an error message “Wrong number of parameters specified for call to”<Procedure Variable>””. I noted that OnClick is of type TNotifyEvent which has the parameter “(Sender : TObject):” so I tried adding that parameter to the procedure declaration and header. But that does not change the outcome. I then tried using “override” instead of “virtual” but that gave the compiler error message “There is no method in an ancestor class to be overridden: “OnClick””. Obviously I have got something wrong.

As regards planning for keyboard events and naming the control, that's good advice that I won't forget to address when the time comes.

wp

  • Hero Member
  • *****
  • Posts: 7636
Re: Creating a Component 101
« Reply #4 on: July 20, 2020, 12:58:15 am »
When writing a component you should not think in "events", but in the methods provided by the ancestor.

Let me assume that the ancestor of your TSwitch is TImage (but see note below). TImage inherits a method named Click from TControl. The method is virtual, so only must write your own Click method (no OnClick!) and TSwitch will use the new method instead of the inherited one. Taking your erroenous OnClick code, here is what should work:

Code: Pascal  [Select][+][-]
  1. type
  2.   TSwitch = class(TImage)
  3.   ...
  4.   protected
  5.      procedure Click; override;
  6.   ....
  7.   end;
  8.  
  9. procedure TSwitch.Click;
  10. begin
  11.   Play;
  12.   flip;
  13.   inherited;
  14. end;

As already noted, the immediate ancestor of TSwitch is TImage here. This means that the switch inherits all events and properties from TImage. While this is comfortable to you as component writer it has the big disadvantage that the user may see properties in the Object Inspector which are out of context, may not work or may even interfere with the inner workings of your component. Example: property "Picture"- this is problematic because your component uses its own pictures and the user should not be able to insert his own image which will not be used - very confusing.

To avoid this you should inherit the component from TCustomImage. This class has exactly the same functionality as TImage with the important difference that is does not publish any properties. This way your TSwitch is able to publish only those properties which are needed:

Code: Pascal  [Select][+][-]
  1. type
  2.   TSwitch = class(TCustomImage)
  3.   .... // your methods
  4.   published     // publish the properties inherited from TCustomImage, but only those which are needed
  5.     property Cursor;
  6.     property Enabled;
  7.     property Visible;
  8.     // etc....
  9.     // inherited events
  10.     property OnClick;
  11.     property OnMouseDown;
  12.     property OnMouseMove;
  13.     property OnMouseUp;
  14.     property OnPictureChanged;
  15.     // etc...
  16.   end;
Mainly Lazarus trunk / fpc 3.2.0 / all 32-bit on Win-10, but many more...

Davo

  • Full Member
  • ***
  • Posts: 131
Re: Creating a Component 101
« Reply #5 on: July 20, 2020, 01:18:13 pm »
Thanks wp, your revised code suggestion works just fine.

I may have to abandon the idea of temporarily using TShapes instead of Timages to display the up-down switch positions because when I use TGrapicControl as the projected component's ancestor and incorporate your Click override, etc. code I can't find a way to link the two shapes' own Click method to the component's Click method. And without that link the shapes simply block the user from clicking the invisible object behind them with the result that at run time clicking either of the shapes does precisely nothing. (To prove the point, if I make the invisible object's size larger than the size of the larger shape then clicking on the part not hidden by the shape does trigger the object's OnClick event.) At least when using a projected component derived from TCustomImage and swapping its picture value between different up and down images, the object itself is visible and available to be clicked by the user.

So now it seems that I have to create a resource file containing the two image files and then probably use the LoadFromLazarusResource procedure to copy them to the component's picture value as needed at run time. The on-line documentation regarding creating a resource file, linking it into the component unit and incorporating it into the component package is somewhat scattered and hard to follow. I'll persevere and hope to understand it shortly. But I wish that there was a simple step by step guide aimed at beginners that I could follow. Anyway, thanks again for your support.

[Edit] I now find that the two shapes can be prevented from blocking the component's own Click method if the instruction "SendToBack" is included for each of the two shapes in the component's creator. But I still need to use a .WAV file to make a click sound in procedure Play so even if I temporarily use TShapes I will still need a resource file to hold the .WAV file.
« Last Edit: July 20, 2020, 01:38:20 pm by Davo »

 

TinyPortal © 2005-2018