2009. október 20., kedd

A Simple Property Editor


Problem/Question/Abstract:

How can I create a simple property editor?

Answer:

This is an introductory level article about creating a simple property editor that I hope will get you started. I'll provide enough information about the DSGNINTF.PAS (Design interface that holds the TPropertyEditor class) file so you can finish the article with a sense of that you are able to create a property editor.

TPropertyEditor

The following is a cut and paste of the TPropertyEditor class declaration found in DSGNINTF.PAS:

  TPropertyEditor = class
private
  FDesigner: TFormDesigner;
  FPropList: PInstPropList;
  FPropCount: Integer;
  constructor Create(ADesigner: TFormDesigner; APropCount: Integer);
  function GetPrivateDirectory: string;
  procedure SetPropEntry(Index: Integer; AInstance: TComponent;
    APropInfo: PPropInfo);
protected
  function GetPropInfo: PPropInfo;
  function GetFloatValue: Extended;
  function GetFloatValueAt(Index: Integer): Extended;
  function GetMethodValue: TMethod;
  function GetMethodValueAt(Index: Integer): TMethod;
  function GetOrdValue: Longint;
  function GetOrdValueAt(Index: Integer): Longint;
  function GetStrValue: string;
  function GetStrValueAt(Index: Integer): string;
  function GetVarValue: Variant;
  function GetVarValueAt(Index: Integer): Variant;
  procedure Modified;
  procedure SetFloatValue(Value: Extended);
  procedure SetMethodValue(const Value: TMethod);
  procedure SetOrdValue(Value: Longint);
  procedure SetStrValue(const Value: string);
  procedure SetVarValue(const Value: Variant);
public
  destructor Destroy; override;
  procedure Activate; virtual;
  function AllEqual: Boolean; virtual;
  procedure Edit; virtual;
  function GetAttributes: TPropertyAttributes; virtual;
  function GetComponent(Index: Integer): TComponent;
  function GetEditLimit: Integer; virtual;
  function GetName: string; virtual;
  procedure GetProperties(Proc: TGetPropEditProc); virtual;
  function GetPropType: PTypeInfo;
  function GetValue: string; virtual;
  procedure GetValues(Proc: TGetStrProc); virtual;
  procedure Initialize; virtual;
  procedure Revert;
  procedure SetValue(const Value: string); virtual;
  function ValueAvailable: Boolean;
  property Designer: TFormDesigner read FDesigner;
  property PrivateDirectory: string read GetPrivateDirectory;
  property PropCount: Integer read FPropCount;
  property Value: string read GetValue write SetValue;
end;

Whew! that's a lot of stuff, isn't it? Add to the fact that the Tools API is poorly documented, and you've got a lot confusion to deal with. There's a little relief in the Delphi 2.0 help file, but the way it's organized can leave you with the sinking feeling that you're in way over your head. After you've done a few property editors, it's really not that hard.

The Trick to Writing Property Editors

One of the biggest problems with technical documentation is that it's technical. Not much conceptual material is ever covered in tech specs or tech manuals. This leaves it up to the programmer to extrapolate the underlying concepts. I'm of the opinion that if something has the possibility of being a widely used feature, you should cover not only the technical specifications, but the conceptual points as well. Gaining conceptual understanding is the real trick to creating property.

The trick to writing property editors is understanding the virtual methods and what they do. Writing your own custom property editors is all about overriding the proper methods of the TPropertyEditor class to get the functionality out of property editor that you require. Granted, there are a lot of very complex property editors out there. But whether simple or complex, they're all built in a similar fashion: they override default functionality of TPropertyEditor.

Once you let this concept sink in, and as you gain more experience in building components, writing a property editor merely becomes the task of overriding the appropriate methods to get your job done.

Furthermore, the DSGNINFT.PAS is thoroughly commented. When constructing components that will have property editors, make sure that this file is open in the editor so you can refer to the documentation covering the virtual methods you will be overriding. As an aside, if you do BDE programming, having the BDE.INT (Delphi 2.0) or DBIPROCS.INT, DBITYPES.INT, DBIERRS.INT (Delphi 1.0) is essential to successful BDE programming

The Value List: the Simplest Type of Property Editor

Properties that display value lists are common to components. In fact, you see them all the time. For instance, a value list property that everyone has used is the Align property.

Value lists are simple enumerated types, which are merely a collection of sequentially ordered elements in a list. The first item has an ordinal value of 0, the second 1, and so forth. Enumerated types are useful in communicating with the user using a set of named choices rather than ordinal or numeric choices. For instance, the Align element alBottom is much easier to understand than '0,' which is its ordinal value in the list. In this case, the ordinal value has no clear conceptual context.

To create a property editor that presents a value list to a user in the object inspector is very simple and requires only a few steps. Here is a brief synopsis of what you have to do before we go into detail:

First, define and declare your enumerated type under a new type section.
Under the enumerated type declaration, declare your class, including the functions you will be overriding in your code.
Write your code in the implementation section of the unit.

Sounds pretty simple, right? It is. So let's go and create one now, then we'll discuss it in detail below.

...other code

interface

type
  TEnumMonth = (emJan, emFeb, emMar,
    emApr, emMay, emJun,
    emJul, emAug, emSep,
    emOct, emNov, emDec);

  TEnumMonths = class(TEnumProperty)
  public
    function GetAttributes: TPropertyAttributes; override;
    function AllEqual: Boolean; override;
  end;

implementation

...other code

function TEnumMonths.AllEqual: Boolean;
begin
  Result := True;
end;

function TEnumMonths.GetAttributes: TPropertyAttributes;
begin
  Result := [paMultiSelect, paValueList];
end;

procedure Register;
begin
  RegisterPropertyEditor(TypeInfo(TEnumMonth), TPSIBaseExt, 'RangeBegin',
    TEnumMonths);
  RegisterPropertyEditor(TypeInfo(TEnumMonth), TPSIBaseExt, 'RangeEnd', TEnumMonths);
end;

The property editor listed above was created to serve a singular purpose: Allow the user to select a specific month from a list of months, rather than typing in a month value code himself (which is more work than the user needs and is also prone to spelling errors).

This component modernizes a cumbersome style of interaction in existing applications. In these applications users were required to enter month ranges as a six-digit string beginning with current two-digit year plus the four digit month/day combination (eg., YYMMDD). Past experience said that runtime errors or empty result sets from queries that used the range values were usually the result of mistyping. So the property editor was created to let the user pick a month for both the starting month and ending month of the range of values they wanted to extract. This explains why in the code above I registered the property editor for both the RangeBegin and RangeEnd properties.

Elsewhere in the component, I have created an array type of type String and created two arrays representing the starting month and ending month code values, respectively.

Here's the array type declaration:

type
  TMonthRng = array[0..11] of string;
  ....

Here are the declarations and initializations of the arrays themselves:

var
  stmonArr,
    enmonArr: TMonthRng;
begin
  stmonArr[0] := '0101';
  stmonArr[1] := '0201';
  stmonArr[2] := '0301';
  stmonArr[3] := '0401';
  stmonArr[4] := '0501';
  stmonArr[5] := '0601';
  stmonArr[6] := '0701';
  stmonArr[7] := '0801';
  stmonArr[8] := '0901';
  stmonArr[9] := '1001';
  stmonArr[10] := '1101';
  stmonArr[11] := '1201';
  enmonArr[0] := '0131';
  enmonArr[1] := '0229';
  enmonArr[2] := '0331';
  enmonArr[3] := '0430';
  enmonArr[4] := '0531';
  enmonArr[5] := '0630';
  enmonArr[6] := '0731';
  enmonArr[7] := '0831';
  enmonArr[8] := '0930';
  enmonArr[9] := '1031';
  enmonArr[10] := '1130';
  enmonArr[11] := '1231';

  ...

By doing things in this manner, one can easily get the appropriate value needed by passing the ordinal value of the appropriate enumerated type as an index of an element in the array. For example, let's say the user chose emApr as his/her starting month. The ordinal value of emApr is 3. Referencing that value in the stmonArr array would produce the string '0401.' What I've essentially done here is eliminate the need for the user to do anything more than choose an appropriate month to start with. The proper code is handled by the program. Here's some sample code that demonstrates how it's done:

procedure ReturnMonthCode(Index: Integer; StartMonth: Boolean): string;
var
  stmonArr,
    enmonArr: TMonthRng;
begin
  stmonArr[0] := '0101';
  stmonArr[1] := '0201';
  stmonArr[2] := '0301';
  stmonArr[3] := '0401';
  stmonArr[4] := '0501';
  stmonArr[5] := '0601';
  stmonArr[6] := '0701';
  stmonArr[7] := '0801';
  stmonArr[8] := '0901';
  stmonArr[9] := '1001';
  stmonArr[10] := '1101';
  stmonArr[11] := '1201';

  enmonArr[0] := '0131';
  enmonArr[1] := '0229';
  enmonArr[2] := '0331';
  enmonArr[3] := '0430';
  enmonArr[4] := '0531';
  enmonArr[5] := '0630';
  enmonArr[6] := '0731';
  enmonArr[7] := '0831';
  enmonArr[8] := '0930';
  enmonArr[9] := '1031';
  enmonArr[10] := '1130';
  enmonArr[11] := '1231';

  if StartMonth then
    Result := stMonArr[Index]
  else
    Result := enMonArr[Index];
end;

To actually use ReturnMonthCode all we do is the following:

var
  S: string;
begin

  S := ReturnMonthCode(Ord(RangeBegin), True);

Remember, RangeBegin is a property of type TEnumArray. Therefore, to access its ordinal value, all we need do is apply the Ord function to it.

Based on the information above, you should be able to create at the very least a simple property editor like the example above. For more complex property editors, you will have to override more of the methods; but remember, don't be daunted by the code. The trick is overriding the default methods with your own.

Nincsenek megjegyzések:

Megjegyzés küldése