Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Parallel

Visually Constructing Delphi Components


DEC95: Visually Constructing Delphi Components

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.
Initially, the ReRead method may seem superfluous. Why not read the file each time the Filename property changes? The problem is that the filename is not the only key to the data. You also need the section and item strings. If you made any of these properties refresh the control, users would need to set the properties in a specific order.

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.
A simple editor (like the one for Char properties) may only supply GetValue and SetValue. If you want to supply GetValues, AllEqual, or Edit, you'll need to override GetAttributes and return the proper values (see
Table 1). These values are elements of a set. Include the ones that apply to your editor (see Listing Two for an example).

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.

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.


Copyright © 1995, Dr. Dobb's Journal


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.