13 min read

(For more resources related to this topic, see here.)

Creating a new component package

We are going to create a custom-logging component, and add it to the Misc tab of the component palette. To do this, we first need to create a new package and add out component to that package along with any other required resources, such as an icon for the component. To create a new package, do the following:

  1. Select package from the main menu.

  2. Select New Package…. from the submenu.

  3. Select a directory that appears in the Save dialog and create a new directory called MyComponents. Select the MyComponents directory.

  4. Enter MyComponents as the filename and press the Save button.

Now, you have a new package that is ready to have components added to it. Follow these steps:

  1. On the Package dialog window, click on the add (+) button.

  2. Select the New Component tab.

  3. Select TComponent as Ancestor Type.

  4. Set New class name to TMessageLog.

  5. Set Palette Page to Misc.

  6. Leave all the other settings as they are.

You should now have something similar to the following screenshot. If so, click on the Create New Component button:

You should see messagelog.pas listed under the Files node in the Package dialog window. Let’s open this file and see what the auto-generated code contains. Double-click on the file or choose Open file from More menu in the Package dialog.

Do not name your component the same as the package. This will cause you problems when you compile the package later. If you were to do this, the .pas file would be over written, because the compile procedure creates a .pas file for the package automatically.

The code in the Source Editor window is given as follows:

unit TMessageLog;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, LResources, Forms, Controls, Graphics, Dialogs,
StdCtrls;
type
TMessageLog = class(TComponent)
private
{ Private declarations }
protected
{ Protected declarations }
public
{ Public declarations }
published
{ Published declarations }
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Misc',[TMessageLog]);
end;
end.

What should stand out in the auto-generated code is the global procedure RegisterComponents. RegisterComponents is contained in the Classes unit. The procedure registers the component (or components if you create more than one in the unit) to the component page that is passed to it as the first parameter of the procedure.

Since everything is in order, we can now compile the package and install the component.

Click the Compile button on the toolbar.

Once the compile procedure has been completed, select Install, which is located in the menu under the Use button. You will be presented with a dialog telling you that Lazarus needs to be rebuilt. Click on the Yes button, as shown in the following screenshot:

The Lazarus rebuilding process will take some time. When it is complete, it will need to be restarted. If this does not happen automatically, then restart Lazarus yourself.

On restarting Lazarus, select the Misc tab on the component palette. You should see the new component as the last component on the tab, as shown in the following screenshot:

You have now successfully created and installed a new component. You can now create a new application and add this component to a Lazarus form. The component in its current state does not perform any action. Let us now look at adding properties and events to the component that will be accessible in the Object Inspector window at design time.

Adding properties

Properties of a component that you would like to have visible in the Object Inspector window must be declared as published. Properties are attributes that determine an object’s status and behavior. A property is a name that is mapped to read and write methods or access data directly. This means, when you read or write a property, you are accessing a field or calling a method of the object. For example, let us add a FileName property to TMessageLog, which is the name of the file that messages will be written to. The actual field of the object that will store this data will be named fFileName.

To the TMessageLog private declaration section, add:

fFileName: String;

To the TMessagLog published declaration section, add:

property FileName: String read fFileName write fFileName;

With these changes, when the packages are compiled and installed, the property FileName will be visible in the Object Inspector window when the TMessageLog declaration is added to a form in a project. You can do this now if you would like to verify this.

Adding events

Any interaction that a user has with a component, such as clicking it, generates an event. Events are also generated by the system in response to a method call or a change in a component’s property, or if different component’s property changes, such as the focus being set on one component causes the current component in focus to lose it, which triggers an event call. Event handlers are methods of the form containing the component; this technique is referred to as delegation. You will notice that when you double-click on a component’s event in the object inspector it creates a new procedure of the form.

Events are properties, and such methods are assigned to event properties, as we just saw with normal properties. Because events are the properties and use of delegation, multiple events can share the same event handler.

The simplest way to create an event is to define a method of the type TNotifyEvent. For example, if we want to add an OnChange event to TMessageLog, we could add the following code:

...
private
FonChange : TNotifyEvent;
...
public
property OnChange: TNotifyEvent read FOnChange write FOnChange;

end;

When you double-click on the OnChange event in Object Inspector, the following method stub would be created in the form containing the TMessageLog component:

procedure TForm.MessageLogChange(Sender: TObject);
begin
end;

Some properties, such as OnChange or OnFocus, are sometimes called on the change of value of a component’s property or the firing of another event. Traditionally, in this case, a method with the prefix of Do and with the suffix of the On event are called. So, in the case of our OnChange event, it would be called from the DoChange method (as called by some other method). Let us assume that, when a filename is set for the TMessageLog component, the procedure SetFileName is called, and that calls DoChange. The code would look as follows:

procedure SetFileName(name : string);
begin
FFileName = name;
//fire the event
DoChange;
end;
procedure DoChange;
begin
if Assigned(FOnChange) then
FOnChange(Self);
end;

The DoChange procedure checks to see if anything has been assigned to the FOnChange field. If it is assigned, then it executes what is assigned to it. What this means is that if you double-click on the OnChange event in Object Inspector, it assigns the method name you enter to FOnChange, and this is the method that is called by DoChange.

Events with more parameters

You probably noticed that the OnChange event only had one parameter, which was Sender and is of the type Object. Most of the time, this is adequate, but there may be times when we want to send other parameters into an event. In those cases, TNotifyEvent is not an adequate type, and we will need to define a new type. The new type will need to be a method pointer type, which is similar to a procedural type but has the keyword of object at the end of the declaration. In the case of TMessageLog, we may need to perform some action before or after a message is written to the file. To do this, we will need to declare two method pointers, TBeforeWriteMsgEvent and TAfterWriteMsgEvent, both of which will be triggered in another method named WriteMessage. The modification of our code will look as follows:

type
TBeforeWriteMsgEvent = procedure(var Msg: String; var OKToWrite:
Boolean) of Object;
TAfterWriteMsgEvent = procedure(Msg: String) of Object;
TmessageLog = class(TComponent)

public
function WriteMessage(Msg: String): Boolean;
...
published
property OnBeforeWriteMsg: TBeforeWriteMsgEvent read fBeforeWriteMsg
write fBeforeWriteMsg;
property OnAfterWriteMsg: TAfterWriteMsgEvent read fAfterWriteMsg
write fAfterWriteMsg;
end;
implementation
function TMessageLog.WriteMessage(Msg: String): Boolean;
var
OKToWrite: Boolean;
begin
Result := FALSE;
OKToWrite := TRUE;
if Assigned(fBeforeWriteMsg) then
fBeforeWriteMsg(Msg, OKToWrite);
if OKToWrite then
begin
try
AssignFile(fLogFile, fFileName);
if FileExists(fFileName) then
Append(fLogFile)
else
ReWrite(fLogFile);
WriteLn(fLogFile, DateTimeToStr(Now()) + ' - ' + Msg);
if Assigned(fAfterWriteMsg) then
fAfterWriteMsg(Msg);
Result := TRUE;
CloseFile(fLogFile);
except
MessageDlg('Cannot write to log file, ' + fFileName + '!',
mtError, [mbOK], 0);
CloseFile(fLogFile);
end; // try...except
end; // if
end; // WriteMessage

While examining the function WriteMessage, we see that, before the Msg parameter is written to the file, the FBeforeWriteMsg field is checked to see if anything is assigned to it, and, if so, the write method of that field is called with the parameters Msg and OKToWrite. The method pointer TBeforeWriteMsgEvent declares both of these parameters as var types. So if any changes are made to the method, the changes will be returned to WriteMessage function. If the Msg parameter is successfully written to the file, the FAfterWriteMsg parameter is checked for assigned and executed parameter (if it is). The file is then closed and the function’s result is set to True. If the Msg parameter value is not able to be written to the file, then an error dialog is shown, the file is closed, and the function’s result is set to False.

With the changes that we have made to the TMessageLog unit, we now have a functional component. You can now save the changes, recompile, reinstall the package, and try out the new component by creating a small application using the TMessageLog component.

Property editors

Property editors are custom dialogs for editing special properties of a component. The standard property types, such as strings, images, or enumerated types, have default property editors, but special property types may require you to write custom property editors.

Custom property editors must extend from the class TPropertyEditor or one of its descendant classes. Property editors must be registered in the Register procedure using the function RegisterPropertyEditor from the unit PropEdits. An example of property editor class declaration is given as follows:

TPropertyEditor = class
public
function AutoFill: Boolean; Virtual;
procedure Edit; Virtual; // double-clicking the property value to
activate
procedure ShowValue; Virtual; //control-clicking the property
value to activate
function GetAttributes: TPropertyAttributes; Virtual;
function GetEditLimit: Integer; Virtual;
function GetName: ShortString; Virtual;
function GetHint(HintType: TPropEditHint; x, y: integer): String;
Virtual;
function GetDefaultValue: AnsiString; Virtual;
function SubPropertiesNeedsUpdate: Boolean; Virtual;
function IsDefaultValue: Boolean; Virtual;
function IsNotDefaultValue: Boolean; Virtual;
procedure GetProperties(Proc: TGetPropEditProc); Virtual;
procedure GetValues(Proc: TGetStrProc); Virtual;
procedure SetValue(const NewValue: AnsiString); Virtual;
procedure UpdateSubProperties; Virtual;
end;

Having a class as a property of a component is a good example of a property that would need a custom property editor. Because a class has many fields with different formats, it is not possible for Lazarus to have the object inspector make these fields available for editing without a property editor created for a class property, as with standard type properties. For such properties, Lazarus shows the property name in parentheses followed by a button with an ellipsis (…) that activates the property editor. This functionality is handled by the standard property editor called TClassPropertyEditor, which can then be inherited to create a custom property editor, as given in the following code:

TClassPropertyEditor = class(TPropertyEditor)
public
constructor Create(Hook: TPropertyEditorHook; APropCount: Integer);
Override;
function GetAttributes: TPropertyAttributes; Override;
procedure GetProperties(Proc: TGetPropEditProc); Override;
function GetValue: AnsiString; Override;
property SubPropsTypeFilter: TTypeKinds Read FSubPropsTypeFilter
Write SetSubPropsTypeFilter
Default tkAny;
end;

Using the preceding class as a base class, all you need to do to complete a property editor is add a dialog in the Edit method as follows:

TMyPropertyEditor = class(TClassPropertyEditor)
public
procedure Edit; Override;
function GetAttributes: TPropertyAttributes; Override;
end;
procedure TMyPropertyEditor.Edit;
var
MyDialog: TCommonDialog;
begin
MyDialog := TCommonDialog.Create(NIL);
try

//Here you can set attributes of the dialog
MyDialog.Options := MyDialog.Options + [fdShowHelp];
...
finally
MyDialog.Free;
end;
end;

Component editors

Component editors control the behavior of a component when double-clicked or right-clicked in the form designer. Classes that define a component editor must descend from TComponentEditor or one of its descendent classes. The class should be registered in the Register procedure using the function RegisterComponentEditor. Most of the methods of TComponentEditor are inherited from it’s ancestor TBaseComponentEditor, and, if you are going to write a component editor, you need to be aware of this class and its methods. Declaration of TBaseComponentEditor is as follows:

TBaseComponentEditor = class
protected
public
constructor Create(AComponent: TComponent;
ADesigner: TComponentEditorDesigner); Virtual;
procedure Edit; Virtual; Abstract;
procedure ExecuteVerb(Index: Integer); Virtual; Abstract;
function GetVerb(Index: Integer): String; Virtual; Abstract;
function GetVerbCount: Integer; Virtual; Abstract;
procedure PrepareItem(Index: Integer; const AnItem: TMenuItem);
Virtual; Abstract;
procedure Copy; Virtual; Abstract;
function IsInInlined: Boolean; Virtual; Abstract;
function GetComponent: TComponent; Virtual; Abstract;
function GetDesigner: TComponentEditorDesigner; Virtual;
Abstract;
function GetHook(out Hook: TPropertyEditorHook): Boolean;
Virtual; Abstract;
procedure Modified; Virtual; Abstract;
end;

Let us look at some of the more important methods of the class.

The Edit method is called on the double-clicking of a component in the form designer.

GetVerbCount and GetVerb are called to build the context menu that is invoked by right-clicking on the component. A verb is a menu item. GetVerb returns the name of the menu item. GetVerbCount gets the total number of items on the context menu. The PrepareItem method is called for each menu item after the menu is created, and it allows the menu item to be customized, such as adding a submenu or hiding the item by setting its visibility to False. ExecuteVerb executes the menu item.

The Copy method is called when the component is copied to the clipboard.

A good example of a component editor is the TCheckListBox component editor. It is a descendant from TComponentEditor so all the methods of the TBaseComponentEditor do not need to be implemented. TComponentEditor provides empty implementation for most methods and sets defaults for others. Using this, methods that are needed for the TCheckListBoxComponentEditor component are overwritten. An example of the TCheckListBoxComponentEditor code is given as follows:

TCheckListBoxComponentEditor = class(TComponentEditor)
protected
procedure DoShowEditor;
public
procedure ExecuteVerb(Index: Integer); override;
function GetVerb(Index: Integer): String; override;
function GetVerbCount: Integer; override;
end;
procedure TCheckGroupComponentEditor.DoShowEditor;
var
Dlg: TCheckGroupEditorDlg;
begin
Dlg := TCheckGroupEditorDlg.Create(NIL);
try
// .. shortened
Dlg.ShowModal;
// .. shortened
finally
Dlg.Free;
end;
end;
procedure TCheckGroupComponentEditor.ExecuteVerb(Index: Integer);
begin
case Index of
0: DoShowEditor;
end;
end;
function TCheckGroupComponentEditor.GetVerb(Index: Integer): String;
begin
Result := 'CheckBox Editor...';
end;
function TCheckGroupComponentEditor.GetVerbCount: Integer;
begin
Result := 1;
end;

Summary

In this article, we learned how to create a new Lazarus package and add a new component to that using the New Package dialog window to create our own custom component, TMessageLog. We also learned about compiling and installing a new component into the IDE, which requires Lazarus to rebuild itself in order to do so. Moreover, we discussed component properties. Then, we became acquainted with the events, which are triggered by any interaction that a user has with a component, such as clicking it, or by a system response, which could be caused by the change in any component of a form that affects another component. We studied that Events are properties, and they are handled through a technique called delegation. We discovered the simplest way to create an event is to create a descendant of TNotifyEvent—if you needed to send more parameters to an event and a single parameter provided by TNotifyEvent, then you need to declare a method pointer.

We learned that property editors are custom dialogs for editing special properties of a component that aren’t of a standard type, such as string or integer, and that they must extend from TPropertyEditor. Then, we discussed the component editors, which control the behavior of a component when it is right-clicked or double- clicked in the form designer, and that a component editor must descend from TComponentEditor or a descendant class of it. Finally, we looked at an example of a component editor for the TCheckListBox.

Resources for Article :


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here