Visually Constructing Delphi Components
Creating visual tools
Al Williams
Al, a consultant specializing in software development, training, and documentation, can be contacted at [email protected].
Borland's Delphi visual-programming environment has a split personality. It lets you drag-and-drop Visual Component Library (VCL) components around the screen without deriving new classes. Yet building those VCL components is anything but visual. To create a component, you have to write naked Pascal code, understand class derivation, and more.
In this article, I'll present a form-based Delphi program called "CompBld" that helps you write components. It won't make the process as visual as application development, but it's far better than starting from scratch. Along the way, I'll examine how to write Delphi components (including a custom property editor) and how to use objects in applications.
Inside VCL
When you add a component to a form, Delphi automatically creates a variable to hold an instance of the component. You then customize the component by altering its properties; see Figure 1. Contrast this with environments such as OWL or MFC, where you subclass existing classes to customize them. When building applications with Delphi, you may never need to resort to class derivation.
As a component developer, however, you must subclass. Each time you create a new component, you'll derive it from an existing class (TComponent or one of its subclasses). You'll also have to be aware of some other Delphi issues:
- All Delphi objects are references to objects that Delphi dynamically creates on the heap. In TForm1.Button.Text:='Close';, for instance, the Button field is a pointer to a heap object. The compiler automatically dereferences the pointer, so you don't need to use the clumsy pointer notation. This is similar to a C++ reference variable.
- Components have properties (pseudo-variables), methods (function calls), and events (message-handling functions). As a component designer, you need to define these items and control their visibility to other portions of the program.
- Properties may have side effects. When a component user reads or modifies a property, Delphi can call a function. For example, setting a button's Enable property to True modifies the property and also calls code that makes a standard EnableWindow() call.
- Properties and methods may be private (visible only inside a unit), protected (visible inside a unit and to descendant classes), public (visible globally), or published (like public, but available in the Object Inspector at design time).
Anatomy of a Component
A component is simply a class derived from TComponent or a subclass of TComponent. Writing a component is a non-visual operation. You can use Delphi's Component Expert, but it writes little code for you. To run Component Expert, select New Component from the File menu. Example 1 is the code you can expect it to generate.
The class(TComboBox) syntax in Example 1 indicates that the component derives directly from TComboBox. From this skeleton, you'll need to fill in any private variables you want to use, and add properties, methods, and occasionally, events. Methods are just ordinary procedure or function definitions. Properties, however, are unlike anything you've used in other programming languages. They are variables, function calls, and more, all rolled into one. Events are simply a special form of properties.
More About Properties
Properties are part of what makes Delphi so powerful. A component can create properties that appear on the Object Inspector in the design environment. In the class definition in Example 2, the Delimiter property will appear in the Object Inspector window because of the published keyword preceding it. The default value is a comma. When a component user reads or writes this property, it will directly access the FDelimiter variable. If you want a read-only property, omit the write method. The default value is optional, and only works for ordinal and small-set types.
In this case, a property is little more than a variable. However, you can also define read and write procedures that execute at the proper time. In Example 3, DynamicProp defines functions to control access to the property. When the component user writes x:=TDDJComponent.DynamicProp;, you can think of it as x:=TDDJComponent.GetDynProp;. Of course, you couldn't really call GetDynProp directly, since it is private to the class, but the result is the same.
Methods and Events
To define a method, just write an ordinary function or procedure in the public or protected declaration sections. Because of properties, you won't use methods as often as you do in other languages. Consider a custom edit control where you want to encrypt the contents. You could write a method to do the encryption. But if you published an encrypt property instead, the write function would perform the encryption operation when the component user changed the property to True. Properties are powerful; you'll often use them instead of directly calling methods. This better encapsulates your implementation and makes the values available at design time.
As a component user, you assign methods to an event handler using the Object Inspector. This simply assigns a method pointer to a special event property. As a component builder, you must not do this. If you use the event handler, you'll conflict with any user attempts to intercept the event.
To solve this problem, you need to know how Delphi events work. Consider the OnKeyDown handler. Inside components that process WM_KEYDOWN (the underlying message handler), you'll find a KeyDown handler. This method uses the message keyword to indicate it handles a Windows message. KeyDown checks to see if the OnKeyDown property is set. If so, KeyDown calls the routine pointed to by the property.
To change default event processing for WM_KEYDOWN, you override KeyDown, not OnKeyDown. This allows users to continue to assign events as usual. Defining a custom event is somewhat more difficult.
To define a new event, you have to define a custom message, message structure, event type, and message-handling procedure. You'll also want to publish a property for user-defined handlers, as in the component that needs to handle a custom WM_USER message in Example 4.
Handling Default Values
You can specify a default value for any ordinal or small-set property using the default keyword. This may not work as you expect, however. While Object Inspector initializes the property's value to this default, Delphi never saves the default values when it writes your project code. You must set the default values in the object's constructor, and be sure to use the same value in both the default clause and the constructor. Example 5 is a typical constructor for a component with two default properties.
Automating the Process
Designing a reasonable user interface for entering method and event definitions can be difficult because you have to handle a varying number of arguments. Luckily, it is easy to write methods and override existing events; you essentially write self-contained functions. You rarely need to create new events. The most tedious (and most common) task is writing properties. You have to declare a variable to hold the results and possibly create read and write functions. You also need to write a constructor to handle the default values. Why not automate these steps?
Figure 2 shows CompBld in action. The left portion of the screen is similar to the Component Expert (except for the comments field). However, the right side gives you a complete tool for creating properties. Simply enter the property name and select the type. CompBld will automatically provide read and write variable names. If you want a read or write method, type another name in the appropriate slot. If you want a read-only property, delete the entry in the write field. Finally, supply any default value and select public or published. Press the Add button (or press Enter) and the property will appear in the list box. To alter a property (or delete it), select it in the list box and make any changes.
Listing One is the main CompBld program (SDIMAIN.PAS). The bulk of the program deals with UI issues. The real meat is in the SaveItemClick procedure. This ugly piece of code writes the skeleton component file. I wanted to prevent this code from knowing too much about the property's user interface. The SaveItemClick procedure deals with a TPropVals object. This is a good example of the power of a user-defined class in a Delphi application. This object knows how to transfer data from components on the form to a data structure and back (the SetValues and GetValues methods). The property list box stores a list of these objects in its Objects array (a standard part of the list box). When SaveItemClick needs to work with the properties you define, it simply walks the Objects array. The PAddClick handler copies the correct values into the Objects array. When you click on an entry in the list box, PBoxClick reverses the process. It copies the data from the Objects array to the controls on the form (using the TPropVals.SetValues method).
Improving CompBld with a Custom Component
The first version of CompBld had values hardcoded in the combo boxes. A more-general implementation would read values from an INI file. Since this is a useful variation on a combo box, it calls for a custom component. These are the design criteria for the TIniCombo component:
- The control should act just like an ordinary combo box.
- Each TIniCombo will have a Filename, Section, and Item property, as well as a Separator property (a character property for the item delimiter).
- When the TIniCombo initializes, it should read a string from the file using the appropriate section and item name. The combo box then fills with the values as delimited by the separator character. The ReRead method also performs this function.
- Calling the Write method places the current contents of the combo box back in the INI file in the same format.
- TIniCombo will provide a custom property editor for the Filename property, which will bring up the standard file-open dialog, but will only store the base filename. Programs should assume an INI file is in the Windows directory, which may differ between machines.
When I added TIniCombo to CompBld, I also extended the UI. When you enter a value in a combo box that doesn't appear in the list, the code adds it to the list (see ComboExit). If you press Ctrl-Del while in a combo box, it will remove the item from the list (this occurs in the ComboKey procedure). In either case, CompBld writes the combo-box data back to the INI file.
Inside TIniCombo
Listing Two is TIniCombo. Since a TIniCombo looks just like a TComboBox, it derives directly from this class. If you wanted to hide some parts of the normal combo box, you'd need to derive from TCustomComboBox, a special class that defines almost everything as protected. You could then change the attributes in a derived subclass. In a derived class, you can publish items from your base class, but you can't unpublish them. In this case, you don't want to change anything, so start with TComboBox.
Delphi has many objects that aren't visible. One of these (TIniFile) is exactly what TIniCombo needs to read and write to an INI file. The only problem with TIniFile is that it uses standard Pascal strings, which are limited to 255 characters. However, Delphi also supports NULL-terminated strings (using character arrays and type PChar). You can use these strings and the normal Windows API calls to break the 255-character limit, provided you also rewrite all the string-manipulation code in TIniCombo.
The TIniCombo design calls for the box to initialize itself on creation. The obvious method would be to set everything up in the constructor, but this won't work. Regardless of the settings in Object Inspector, the object is empty when the constructor runs, because Delphi hasn't loaded the property values yet. The correct place to initialize is during the Loaded procedure. Be sure to use inherited to call the base class in case it needs to initialize, too. The TIniCombo.Loaded routine checks the Filename property. If it is not empty, the code calls ReRead to set everything up.
Custom Property Editors
Delphi allows you to write custom editors for properties. One example of a property editor is that supplied by Delphi for the Font property. Pressing the button on this property opens a dialog allowing you to select a font. However, many property editors are not so visual. For example, a property of type Char uses an editor that you may never notice. Its sole purpose is to translate non-printable characters to hex notation (for example, Control-A is #01).
Writing a custom property editor isn't difficult. Simply derive a class from TPropertyEditor, override a few procedures, and call RegisterPropertyEditor. Your editor will translate between the property and a string representation that Object Inspector displays. There are six methods to override in your property editor:
- GetValue, which returns a string for Object Inspector to display.
- SetValue, which converts a string from Object Inspector into a property value.
- Edit, which displays a dialog box and returns a string.
- GetValues, which returns a list of values for enumerated properties.
- AllEqual, which determines if the user can set the property on multiple objects simultaneously.
- GetAttributes, which informs Object Inspector of the editor's capabilities.
TPropertyEditor also provides several methods to read and write common types (see Table 2). In many cases, GetValue and SetValue will simply call these built-in methods. If you want to do range checking or other validation, you can raise an EPropertyError to signal a problem.
TIniCombo defines a special type for the INI filename (TIniFilename). This allows it to supply a custom editor that affects only this type. The property editor brings up a file-open dialog. Since the user's PC may be set up differently than the programmer's PC, you don't want to store the INI file's directory. The TIniFileNameEditor object removes the directory name before setting the property value. The component registers the editor when it registers itself.
You can call RegisterPropertyEditor to achieve at least three different effects:
- Register your editor to handle all properties of a given type by specifying a type name.
- You can also specify a component name. Your editor will handle only properties of a given type that occur in the named component or components derived from it by specifying a component name.
- Limit the editor to properties with specific names.
Conclusion
Building components is an important way to realize Delphi's full potential. While Delphi gives app developers a first-class visual environment, it doesn't do much for component builders. CompBld can take some of the work out of creating new components.
Adding custom property editors to your components can make them even more intuitive for users. The easier a component is to use, the more likely other programmers are to incorporate it in their applications.
Component developers frequently create new classes. However, new classes can simplify application development, too. The TPropVals class in CompBld, for example, makes the code easier to understand and maintain.
Figure 1: Object Inspector.
Figure 2: CompBld in action.
Example 1: Code generated by Component Expert.
Copyright © 1995, Dr. Dobb's Journal
unit DDJ;
interface
uses
SysUtils, WinTypes, WinProcs,
Messages, Classes, Graphics,
Controls, Forms, Dialogs,
ExtCtrls;
type
TDDJComponent = class(TComboBox)
private
{ Private declarations }
protected
{ Protected declarations }
public
{ Public declarations }
published
{ Published declarations }
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Samples',
[TDDJComponent]);
end;
end.
Example 2: Class definition.
type
TDDJComponent = class(TComboBox)
private
FDelimiter : Char;
published
property Delimiter : Char read FDelimiter write FDelimiter
default ',' ;
end;
Example 3: The DynamicProp property calls functions.
type
TDDJComponent = class(TComboBox)
private
function GetDynProp : Integer;
procedure SetDynProp(const value :Integer);
published
property DynamicProp : Integer read GetDynProp write SetDynProp;
end;
Example 4: A component that handles WM_USER.
{ Define message }
const WM_MYMSG=WM_USER;
{ Event Structure ( parses arguments }
TMyMsg = record
Msg: Cardinal;
Unused : Word; { wParam }
Flag : LongInt;{ lParam }
Result : LongInt;
end;
{ Define event type }
TMyEvent = procedure(Sender : TObject;
Flag : LongInt) of object;
.
.
.
private:
{ Event handler pointer }
FOnMyEvent : TMyEvent;
{ Message handling procedure }
procedure WMMyMsg(
var Msg : TMyMsg); message WM_MYMSG;
{ Ordinary procedure receives parsed arguments
and calls the event handler if set }
procedure MyMsg(Sender : TObject; Flag : Boolean);
published:
{ Event handler shows up in Object Inspector }
property OnMyEvent : TMyEvent read FOnMyEvent
write FOnMyEvent;
.
.
.
procedure TXControl.WMMyMsg(var Msg:TMyMsg);
var
Boolean flg;
begin
flg:=True;
if Msg.Flag=0 then flg:=False;
MyMsg(self,flg);
end;
procedure TXControl.MyMsg(Sender : TObject;
Flag : Boolean);
begin
if Assigned(FOnMyEvent) then FOnMyEvent(self,Flag);
end;
Example 5: Typical constructor for a component with
two default properties.
.
.
.
published:
property HighMark read FHighMark
write FHighMark default 100;
property LowMark read FLowMark
write FLowMark default 33;
.
.
.
constructor TCompDDJ.Create(AOwner
: TComponent);
begin
inherited Create(AOwner);
HighMark:=100; { default value }
LowMark:=33; { default value }
end;
Table 1: Property-editor attributes.
<b>Attribute Definition</b>
<I>paValueList</I> Call <I>GetValues</I> to retrieve enumerated values.
<I>paDialog</I> Call <I>Edit</I> to open a property-specific editor dialog.
<I>paMultiSelect</I> Allow the property to apply to multiple selected components.
<I>paSubProperties</I> This property has subproperties.
Table 2: Built-in property methods.
<b
>Property Types Read Write</b>
Floating point <I>GetFloatValue SetFloatValue</I>
Event <I>GetMethodValue SetMethodValue</I>
Ordinal <I>GetOrdValue SetOrdValue</I>
String <I>GetStrValue SetStrValue</I>
Listing One
unit Sdimain;
{ DDJ Component Builder -- Al Williams }
interface
uses WinTypes, WinProcs, Classes, Graphics, Forms, Controls, Menus,
Dialogs, StdCtrls, Buttons, ExtCtrls,SysUtils,
IniCombo;
type
TMainForm = class(TForm)
MainMenu: TMainMenu;
FileMenu: TMenuItem;
SaveItem: TMenuItem;
ExitItem: TMenuItem;
N1: TMenuItem;
SaveDialog: TSaveDialog;
Help1: TMenuItem;
About1: TMenuItem;
StatusBar: TPanel;
SpeedPanel: TPanel;
SaveBtn: TSpeedButton;
ExitBtn: TSpeedButton;
CName: TEdit;
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
CBase: TIniCombo;
CGroup: TIniCombo;
GroupBox1: TGroupBox;
CProp: TEdit;
PRead: TEdit;
PType: TIniCombo;
PWrite: TEdit;
PDefault: TEdit;
PAdd: TButton;
PRemove: TButton;
PBox: TListBox;
Label5: TLabel;
Label6: TLabel;
Label7: TLabel;
Label8: TLabel;
Label9: TLabel;
CComment: TMemo;
BPublish: TRadioButton;
BPublic: TRadioButton;
New1: TMenuItem;
NewBtn: TSpeedButton;
procedure ShowHint(Sender: TObject);
procedure ExitItemClick(Sender: TObject);
procedure SaveItemClick(Sender: TObject);
procedure About1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure CNameChange(Sender: TObject);
procedure CPropChange(Sender: TObject);
procedure PAddClick(Sender: TObject);
procedure PBoxClick(Sender: TObject);
procedure PChange(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure PRemoveClick(Sender: TObject);
procedure New1Click(Sender: TObject);
procedure ComboExit(Sender: TObject);
procedure ComboKey(Sender: TObject; var Key: Word; Shift: TShiftState);
private
{ Private declarations }
procedure Init; { make new document }
public
{ No Public declarations }
end;
PType = (pPublic,pPublished); { Type of property }
{ This non-visual object represents one property associated with the component.
SetValues and GetValues methods transfer data from the form to/from object }
TPropVals = class(TObject)
PropName : String;
PropType : String;
ReadName : String;
WriteName : String;
Default : String;
PrType : PType;
public
procedure SetValues(nm : TEdit; typ : TComboBox; r,w,d :
TEdit; pubsh, publc : TRadioButton);
procedure GetValues(nm : TEdit; typ : TComboBox; r,w,d :
TEdit; pubsh, publc : TRadioButton);
end;
var
MainForm: TMainForm;
implementation
uses About;
{$R *.DFM}
procedure TMainForm.ShowHint(Sender: TObject);
begin
StatusBar.Caption := Application.Hint;
end;
procedure TMainForm.ExitItemClick(Sender: TObject);
begin
Close;
end;
{ The save is the bulk of the code we need to write the unit name (based on
the file name), the uses clause, the type and implementation sections with
stubs for all the functions.
}
procedure TMainForm.SaveItemClick(Sender: TObject);
const
cr = chr(10);
var
fout : TextFile;
i : Integer;
ct : Integer;
vals : TPropVals;
unitname : String;
begin
if SaveDialog.Execute then
begin
AssignFile(fout,SaveDialog.FileName);
Rewrite(fout);
unitname:=ExtractFileName(SaveDialog.Filename);
i:=Pos('.',unitname);
if i<>0 then
unitname:=Copy(unitname,1,i-1); { remove extension }
Writeln(fout,'Unit ' + unitname + ';'+cr);
if CComment.Text <> '' then
Writeln(fout,'{' + CComment.Text + '}'+cr);
Writeln(fout,'interface'+cr);
Writeln(fout,'uses SysUtils, WinTypes, WinProcs, Messages, Classes,');
Writeln(fout,' Graphics, Controls, Forms, Dialogs, StdCtrls;'+cr);
Writeln(fout,'type');
Writeln(fout,CName.Text + '= class('+ CBase.Text +')');
Writeln(fout,'private');
ct := PBox.Items.Count;
{ dump property variables here }
for i:=0 to ct-1 do
begin
vals:=TPropVals(PBox.Items.Objects[i]);
Writeln(fout,' F'+vals.PropName + ':' + vals.PropType+';');
end;
{ dump property functions here }
for i:= 0 to ct-1 do
begin
vals:=TPropVals(PBox.Items.Objects[i]);
if (vals.ReadName <> '') and (vals.ReadName <> 'F'+vals.PropName) then
Writeln(fout,' function '+vals.ReadName+' : '+vals.PropType+';');
if (vals.WriteName <>'') and (vals.WriteName <> 'F' + vals.PropName) then
Writeln(fout,' procedure '+vals.WriteName+'( const Value : '+
vals.PropType+');');
end;
{ Write constructor header }
Writeln(fout,cr+'public');
Writeln(fout,' constructor Create(AOwner : TComponent); override;');
{ Public properties }
for i:= 0 to ct-1 do
begin
vals:=TPropVals(PBox.Items.Objects[i]);
if vals.PrType=pPublic then
begin
Write(fout,' property ' + vals.PropName + ' : ' + vals.PropType);
if vals.ReadName<>'' then
Write(fout,' read '+vals.ReadName);
if vals.WriteName<>'' then
Write(fout,' write '+vals.WriteName);
if vals.Default<>'' then
Write(fout,' default '+vals.Default);
Writeln(fout,';');
end;
end;
{ Published properties }
Writeln(fout,cr+'published');
for i:= 0 to ct-1 do
begin
vals:=TPropVals(PBox.Items.Objects[i]);
if vals.PrType=pPublished then
begin
Write(fout,' property ' + vals.PropName + ' : ' + vals.PropType);
if vals.ReadName<>'' then
Write(fout,' read '+vals.ReadName);
if vals.WriteName<>'' then
Write(fout,' write '+vals.WriteName);
if vals.Default<>'' then
Write(fout,' default '+vals.Default);
Writeln(fout,';');
end;
end;
Writeln(fout,'end;');
{ Take care of Register function and implemenation }
Writeln(fout,cr+'procedure Register;'+cr);
Writeln(fout,'implementation');
Writeln(fout,cr+'procedure Register;');
Writeln(fout,'begin');
Writeln(fout,' RegisterComponents('''+CGroup.Text+''',['+CName.Text+']);');
Writeln(fout,'end;');
{ write constructor }
Writeln(fout,cr+'constructor '+CName.Text+'.Create(AOwner:TComponent);');
Writeln(fout,'begin');
Writeln(fout,' inherited Create(AOwner);');
{ Set up defaults here }
for i:= 0 to ct-1 do
begin
vals:=TPropVals(PBox.Items.Objects[i]);
if vals.Default<>'' then
Writeln(fout,' '+vals.PropName+':='+vals.Default+';');
end;
Writeln(fout,' { your code here }');
Writeln(fout,'end;');
{ Write get/set functions }
for i:= 0 to ct-1 do
begin
vals:=TPropVals(PBox.Items.Objects[i]);
if (vals.ReadName<>'') and (vals.ReadName <> 'F'+vals.PropName) then
begin
Writeln(fout,cr+'function '+Cname.Text+'.'+vals.ReadName+
' : '+vals.PropType+';');
Writeln(fout,'begin');
Writeln(fout,' result:=F'+vals.PropName+';');
Writeln(fout,'end;');
end;
if (vals.WriteName<>'') and (vals.WriteName <> 'F'+vals.PropName) then
begin
Writeln(fout,cr+'procedure '+Cname.Text+'.'+vals.WriteName+
'(const Value : '+vals.PropType+');');
Writeln(fout,'begin');
Writeln(fout,' F'+vals.PropName+':=Value;');
Writeln(fout,'end;');
end;
end;
Writeln(fout,cr+'end.');
CloseFile(fout);
end;
end;
procedure TMainForm.About1Click(Sender: TObject);
begin
AboutBox.ShowModal;
end;
procedure TMainForm.FormCreate(Sender: TObject);
begin
Application.OnHint := ShowHint;
Init; { Set up as new document }
end;
{ The TPropVal object contains information about a property }
{ Set up component values from a TPropVals object }
procedure TPropVals.SetValues(nm : TEdit; typ : TComboBox; r,w,d :
TEdit; pubsh, publc : TRadioButton);
begin
nm.Text:=PropName;
typ.Text:=PropType;
r.Text:=ReadName;
w.Text:=WriteName;
d.Text:=Default;
if PrType = pPublished then
pubsh.Checked:=True
else
publc.Checked:=True;
end;
{ Set up a TPropVal based on component values }
procedure TPropVals.GetValues(nm : TEdit; typ : TComboBox; r,w,d :
TEdit; pubsh, publc : TRadioButton);
begin
PropName:=nm.Text;
PropType:=typ.Text;
ReadName:=r.Text;
WriteName:=w.Text;
Default:=d.Text;
if (pubsh.Checked) then
PrType:=pPublished
else
PrType:=pPublic;
end;
{ Come here when name changes }
procedure TMainForm.CNameChange(Sender: TObject);
var en : Boolean;
begin
if CName.Text = '' then
en:=false
else
en:=true;
CProp.Enabled:=en;
PBox.Enabled:=en;
end;
{ Come here when property name changes }
procedure TMainForm.CPropChange(Sender: TObject);
var
en : Boolean;
begin
if CProp.Text='' then
en := false
else
en := true;
PType.Enabled:=en;
PRead.Enabled:=en;
PWrite.Enabled:=en;
PDefault.Enabled:=en;
BPublish.Enabled:=en;
BPublic.Enabled:=en;
PAdd.Enabled:=en;
PRead.Text:='F'+CProp.Text;
PWrite.Text:='F'+CProp.Text;
PDefault.Text:='';
end;
{ Click on Add button }
procedure TMainForm.PAddClick(Sender: TObject);
var
vals : TPropVals;
idx : Integer;
istring : String;
begin
vals:=TPropVals.Create;
vals.GetValues(CProp,PType,PRead,PWrite,PDefault,BPublish,BPublic);
istring:=Cprop.Text;
idx := PBox.Items.IndexOf(istring);
if idx = -1 then { new item }
idx:=PBox.Items.AddObject(istring,vals)
else
begin { replace item }
PBox.Items.Objects[idx].Free;
PBox.Items.Objects[idx]:=vals;
end;
PBox.ItemIndex:=idx;
PBoxClick(nil); { Update remove button, et. al. }
ActiveControl:=CProp; { reset to property field }
CProp.SelectAll;
end;
{ Click on list box }
procedure TMainForm.PBoxClick(Sender: TObject);
var
vals : TPropVals;
begin
if PBox.ItemIndex <> -1 then
begin
vals:=TPropVals(PBox.Items.Objects[PBox.ItemIndex]);
vals.SetValues(CProp,PType,PRead,PWrite,PDefault,BPublish,BPublic);
PRemove.Enabled:=True;
end
else
PRemove.Enabled:=False;
end;
{ Come here when any property field changes }
procedure TMainForm.PChange(Sender: TObject);
var
vals : TPropVals;
idx : Integer;
istring : String;
begin
vals:=TPropVals.Create;
vals.GetValues(CProp,PType,PRead,PWrite,PDefault,BPublish,BPublic);
istring:=CProp.Text;
idx := PBox.Items.IndexOf(istring);
if idx <> -1 then
begin
PBox.Items.Objects[idx].Free;
PBox.Items.Objects[idx]:=vals;
end;
end;
procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
if MessageDlg('Really Quit?',mtConfirmation,[mbOK,mbCancel],0)=mrOK then
Action:=caFree
else
Action:=caNone;
end;
{ On Remove Button }
procedure TMainForm.PRemoveClick(Sender: TObject);
begin
Pbox.Items.Delete(PBox.ItemIndex);
CProp.Text:='';
PRead.Text:='';
PWrite.Text:='';
PDefault.Text:='';
PBoxClick(nil);
end;
procedure TMainForm.New1Click(Sender: TObject);
begin
if MessageDlg('Start new component?',mtConfirmation,[mbYes,mbNo],0)=mrYes then
Init;
end;
procedure TMainForm.Init;
begin
{ empty form }
CName.Text:='';
CBase.Text:='TComponent';
CGroup.Text:='Samples';
CComment.Text:='';
PBox.Clear;
BPublish.Checked:=True;
CProp.Text:='';
PRead.Text:='';
PWrite.Text:='';
PDefault.Text:='';
end;
{ When combo box exits, add any new item to the list and write to INI file}
procedure TMainForm.ComboExit(Sender: TObject);
var
cb : TIniCombo;
temp : String;
begin
cb:=Sender as TIniCombo;
if cb.Items.IndexOf(cb.Text)=-1 then
begin
temp:=cb.Text;
cb.Items.Add(temp);
cb.Write;
cb.ReRead;
cb.ItemIndex:=cb.Items.IndexOf(temp);
end;
end;
{ Look for ^Del and remove item from INI file and list }
procedure TMainForm.ComboKey(Sender: TObject; var Key: Word;
Shift: TShiftState);
var
cb : TIniCombo;
begin
if (Key=VK_DELETE) and (Shift=[ssCtrl]) then
begin
cb:=Sender as TIniCombo;
cb.Items.Delete(cb.Items.IndexOf(cb.Text));
cb.Text:='';
cb.Write;
end;
end;
end.
Listing Two
unit Inicombo;
interface
uses
SysUtils,
WinTypes,
WinProcs,
Messages,
Classes,
Graphics,
Controls,
Forms,
Dialogs,
StdCtrls,
IniFiles,
DsgnIntf;
type
TIniFileName = String[255];
TIniCombo = class(TComboBox)
private
FFilename : TIniFileName;
FSection : String;
FItem : String;
FSeparator : Char;
protected
procedure Loaded; override;
public
procedure Reread;
procedure Write;
constructor Create(AOwner : TComponent); override;
published
property Filename : TIniFileName read FFilename write FFilename;
property Section : String read FSection write FSection;
property Item : String read FItem write FItem;
property Separator : Char read FSeparator write FSeparator default '|';
end;
TIniFileNameEditor=class(TPropertyEditor)
public
function GetValue : String; override;
procedure SetValue(const Value : String); override;
function GetAttributes : TPropertyAttributes; override;
procedure Edit; override;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterPropertyEditor(TypeInfo(TIniFileName),nil,'',TIniFileNameEditor);
RegisterComponents('Samples', [TIniCombo]);
end;
constructor TIniCombo.Create(AOwner : TComponent);
begin
inherited Create(AOwner);
Separator:='|'; { default value }
end;
{ Setup after everthing is ready }
procedure TIniCombo.Loaded;
begin
inherited Loaded;
if Filename <> '' then Reread;
end;
procedure TIniCombo.Reread;
var
Ini : TIniFile;
Work : String;
Entry : String;
n : Integer;
begin
Ini:=TIniFile.Create(FFilename);
Work := Ini.ReadString(FSection,FItem,'');
if Work <> '' then Clear;
n:=Pos(FSeparator,Work);
while n<>0 do
begin
Entry:=Copy(Work,1,n-1);
Items.Add(Entry);
Delete(Work,1,n);
n:=Pos(FSeparator,Work);
end;
{ Add last one }
if Length(Work)<>0 then Items.Add(Work);
end;
procedure TIniCombo.Write;
var
Ini : TIniFile;
Work : String;
Entry : String;
n : Integer;
begin;
Ini:=TIniFile.Create(FFilename);
{ build string }
Work:='';
for n:=0 to Items.Count do
begin
Work:=Work + Items[n];
if n<> Items.Count then Work:=Work + FSeparator;
end;
Ini.WriteString(FSection,FItem,Work);
end;
function TIniFileNameEditor.GetAttributes : TPropertyAttributes;
begin
result:=[paMultiSelect,paDialog];
end;
function TIniFileNameEditor.GetValue : String;
begin
result:=GetStrValue;
end;
procedure TIniFileNameEditor.SetValue(const Value : String);
begin
SetStrValue(Value);
end;
procedure TIniFileNameEditor.Edit;
var
dlg : TOpenDialog;
dbuf : array[0..255] of Char;
begin
dlg:=TOpenDialog.Create(Application);
try
dlg.Filename:=GetStrValue;
dlg.DefaultExt:='INI';
dlg.Filter:='Ini Files|*.INI|All Files|*.*';
GetWindowsDirectory(dbuf,256);
dlg.InitialDir:=StrPas(dbuf);
if dlg.Execute=TRUE then
SetStrValue(ExtractFileName(dlg.FileName));
finally
dlg.Free;
end;
end;
end.