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!
|