t e m p o r a l 
 d o o r w a y 

Writing Your Own Property Editor

 

Introduction

Property editors shouldn't be hard to write. And they aren't. But writing them is poorly documented, and you need all the help you can get. That's the purpose of this article.

There are some books which can help. Unfortunately, most of them are Delphi books. When you get to the point where you want to write a property editor, you should also be at a point where you are comfortable reading books on Delphi, and reading Delphi code.

For property editors, I recommend

Basic Property Editor Concepts

Property editors and component editors are two of the most important ways the developer can customize the C++ Builder IDE. Basically, they allow you to provide yourself or another developer with a simple and integrated way to change the values of key properties in your component.

Property editors typically are designed to operate within the object inspector, though they can also produce dialog windows which appear separately from the object inspector.

Property editors inherit at least from TPropertyEditor, but can also inherit from TStringProperty or a variety of other specialized property editors discussed in the Component Developer's Guide and in the on-line help.

Property editors need to know both about the property they are editing and the component in which that property exists. Calls exist for the property editor to access the value of the property. They include

  • GetOrdValue() which is used to get the pointer to a class property.
  • GetIntValue() which is used to get the value of an integer property.
  • GetStrValue() which is used to get the value of a string property.

Getting information about the component, on the other hand. requires the property editor know the actual class or an ancestor class of the component. It gets a pointer to the component through GetComponent(); that pointer must then be cast to the type of the component in which the property editor expects the property to exist.

The Example Property Editor

This property editor is to be used on a TDataSet based component. It will retrieve the names of the fields from that component and allow the user to select one, which will then be stored in the property. You will also find how to define an AnsiString property editor, and you will find here the special trick that allows you to register a property editor for AnsiString (which is not normally allowed in C++ Builder).

The Example Property Type

The property type is defined in the article on Streaming Classes. It wraps a simple AnsiString.

The Property Editor Code

//---------------------------------------------------------------------------
#ifndef aDataSetFieldNamePropertyEditorH
#define aDataSetFieldNamePropertyEditorH

#include <SysUtils.hpp>
#include <Controls.hpp>
#include <Classes.hpp>
#include <Forms.hpp>
#include <DB.hpp>
#include <aStringObject.h>
//---------------------------------------------------------------------------
class PACKAGE aDataSetFieldNamePropertyEditor: public TStringProperty
{
   public:

      __fastcall aDataSetFieldNamePropertyEditor(void){}; // BCB 3 only
      __fastcall ~aDataSetFieldNamePropertyEditor(void){};

      TPropertyAttributes __fastcall GetAttributes(void)
      {
         return TPropertyAttributes() << paValueList;
      };

      void __fastcall GetValues(Classes::TGetStrProc AddValueToList)
      {
         TDataSet *HostComponent = dynamic_cast<TDataSet *>(GetComponent(0));

         if (HostComponent != NULL)
         {
            TStringList *FieldName = new TStringList;
            FieldName->Sorted = true;

            HostComponent->GetFieldNames(FieldName);

            for (int Index = 0; Index < FieldName->Count; Index++)
            {
               AddValueToList(FieldName->Strings[Index]);
            };

            delete FieldName;

         }
         else
         {
            AddValueToList("n/a");
         };
      };

      AnsiString __fastcall GetValue(void)
      {
         aStringObject *StringObject = dynamic_cast<aStringObject *>((TObject *) GetOrdValue());
         return StringObject != NULL ? (String) *StringObject : AnsiString("");
      };

      void __fastcall SetValue(AnsiString theValue)
      {
         aStringObject *StringObject = dynamic_cast<aStringObject *>((TObject *) GetOrdValue());
         if (StringObject != NULL) *StringObject = theValue;
         Modified();
      };
};

#endif

  • Note that even though I am editing a class property, my property editor is derived from TStringProperty. This is OK, and lets me automagically get the drop down that goes with a TStringProperty.

  • GetAttributes is an overridden function from TPropertyEditor - in this case, it tells the IDE that my property is a string property and that it will produce multiple values from which the user can pick one.

  • GetValues is an overridden function from TPropertyEditor - it is called because my GetAttributes function tells the IDE that this property editor provides multiple values for possible selection. However, its operation is strange. You do not return the values through the function, for instance as a TStringList. Instead, you must call the provided TGetStrProc type function provided by the IDE, once for each string that will appear in the combo box in the Object Inspector. In this case, the editor gets the data source from the component (GetComponent(0) is a function in TPropertyEditor which returns a pointer to a TComponent - that is the component currently appearing in the Object Inspector), and then gets the list of field names from the data set of that data source, and iterates through that list of field names, calling the IDE's function once for each.

  • GetValue is an overridden function from TPropertyEditor that returns the current string value for the property - in this case, the field name from the aStringObject property. Note how it casts the GetOrdValue() return value, which is the pointer to the property, to provide access to the property as its required type. Note also how it returns an empty string if the dynamic cast fails.

  • SetValue is an overriden function from TPropertyEditor that sets the property based on the user selected string value. Note that this function uses the same cast as GetValue, and if the cast fails, it does nothing. Also note that it calls Modified(), a TPropertyEditor function which notifies the IDE that the property has been changed, which effectively means that the component has changed and that its unit (and perhaps the project) need to be saved. A better approach is to check the incoming value against the existing value, and only call Modified() when the incoming value is different.

Using The Property Editor

I chose to be conservative in registering this property editor. I registered it individually for each field property of my component which used it, rather than registering it for the class of the field property, as you can see in the RegisterPropertyEditor call from the Register function in the .cpp of a component that uses it, simplified:

namespace Agridwiththeeditor
{
   void __fastcall PACKAGE Register()
   {
      TComponentClass classes[1] = {__classid(aGridWithTheEditor)};

        RegisterPropertyEditor
      (
         __typeinfo(aStringObject),
         __classid(aGridWithTheEditor),
         "IDFieldName",
         __classid(aDataSetFieldNamePropertyEditor)
      );
        RegisterPropertyEditor
      (
         __typeinfo(aStringObject),
         __classid(aGridWithTheEditor),
         "SequenceFieldName",
         __classid(aDataSetFieldNamePropertyEditor)
      );

      RegisterComponents("Test Page", classes, 0);
   }
}

Debugging A Property Editor

This is the hard part. Sure, theoretically you are supposed to be able to run the IDE in the debugger and step through a property editor in action. In reality, you probably aren't going to do that.

However, you can pop up Application->MessageBox from any point within your property editor, and, of course, you can use what little space you have for the string version of the property value to provide a message too.

But what's most important is that you write it with excessive care, lots of exception trapping and messaging, and very, very careful code inspections.

Where Does All This Stuff Go?

The property type object, the property editor, and the component which uses both should be in the same .bpk. But they can be in different directories.

What About An AnsiString Property Editor?

If you create a property editor for an AnsiString property, you will find you can't register it - because RegisterPropertyEditor requires a classid for the property class and AnsiString doesn't have one. But there is a workaround, thanks to Mark T. Van Ditta, which allows that problem to be overcome. You can find the original thread at http://www.deja.com in past posts by searching for "Slaying the AnsiString".

I use the following .h

#ifndef anAnsiStringTypeInfo_h
#define anAnsiStringTypeInfo_h

#include <vcl\SysUtils.hpp>
#include <vcl\Controls.hpp>
#include <vcl\Classes.hpp>
#include <vcl\Forms.hpp>

#define __typeinfoAnsiString anAnsiStringTypeInfo().Pointer

class PACKAGE anAnsiStringTypeInfo
{
   public:

   static PTypeInfo myTypeInfo;

   __fastcall anAnsiStringTypeInfo(void);
   __property PTypeInfo Pointer = {read=myTypeInfo};
};
#endif

and .cpp:

//---------------------------------------------------------------------------
#include <vcl.h>
#pragma hdrstop

#include "anAnsiStringTypeInfo.h"

//---------------------------------------------------------------------------
#pragma package(smart_init)

PTypeInfo anAnsiStringTypeInfo::myTypeInfo = NULL;

//---------------------------------------------------------------------------
__fastcall anAnsiStringTypeInfo::anAnsiStringTypeInfo(void)
{
   if (myTypeInfo == NULL) // Hasn't been initialized yet
   {
      myTypeInfo = new TTypeInfo;

      myTypeInfo->Name = "AnsiString";
      myTypeInfo->Kind = tkLString; // This is what tells the system what kind of class this is
   };
};

which works with RegisterPropertyEditor...

RegisterPropertyEditor(__typeinfoAnsiString /* This is a macro from the header file */,__classid(aComponentClass),PropertyName,__classid(SomeAnsiStringPropertyEditor));

Working with an AnsiString property instead of the class property described above only changes the following property editor functions:

AnsiString __fastcall GetValue(void)
{
   String Value = GetStrValue();
   return Value;
};

void __fastcall SetValue(AnsiString theValue)
{
   SetStrValue(theValue);
   Modified();
};

Conclusion

Writing a property editor can make your components safer and more convenient to use during development, and with some thought, can be as reusable as any other object. Have fun!

Copyright © 2004 by Mark Cashman (unless otherwise indicated), All Rights Reserved