Инна Аринович дата публикации 18-11-2004 16:54 Сапоги для сапожникаКак ни удобна среда разработки Delphi, рано или поздно приходит мысль "а еще бы...". Если такие мысли появляются периодически, значит, настало время отложить текущие проекты и написать эксперт, редактор свойств или компонента. Эксперт - это, пожалуй, сюда. Я хочу поделиться опытом создания редакторов свойств.
На самом деле создание редактора принципиально ничем не отличается от создания компонента. Разве что намного проще, но это компенсируется скудностью источников информации, в т.ч. Help. Чаще всего работа сводится к выбору наиболее подходящего предка и переопределению его методов для реализации нужного поведения. Базовым предком для всех редакторов является TPropertyEditor. Первое, что нужно сделать, это решить, какие характеристики нужны редактору, т.е. переопределить его метод GetAttibutes.
function GetAttributes: TPropertyAttributes;
TPropertyAttributes = set of TPropertyAttribute;
- paValueList
- Свойство имеет выпадающий список значений. Для формирования этого списка необходимо будет переопределить метод GetValues.
- paSortList
- Выпадающий список будет отсортирован.
- paDialog
- Свойство содержит кнопку с многоточием. По щелчку на ней активизируется метод Edit. Его надо будет переопределить. Обычно его реализация вызывает диалоговое окно, например, у свойства Font. Нет необходимости добавлять этот атрибут, если включен paValueList. В этом случае справа все равно будет кнопка выпадающего списка. Вызвать метод Edit можно двойным щелчком на самом свойстве. Это происходит при любых атрибутах.
- paSubProperties
- Содержит раскрывающийся список. Для формирования этого списка необходимо переопределить метод GetProperties. Скажу сразу, что это, пожалуй, самый сложный метод для реализации с нуля. В нем необходимо самому создать редакторы для каждого свойства из списка. Поэтому при выборе предка следует изучить редакторы, которые уже умеют это делать. Это TSetProperty для свойства типа "множество", TClassProperty для свойство типа "объект". Начиная с версии 6, TComponentProperty для свойств "компонент". Правда в последнем случае необходимо при создании свойства (речь идет о реализации самого компонента, а не редактора) указать SetSubComponent(true).
- paMultiSelect
- Свойство можно редактировать, если выбрано несколько компонент. Этот атрибут включен уже в TProportyEditor, т.к. большинство свойств поддерживают множественный выбор. Исключение составляет, например, свойство Name - оно должно быть уникальным.
- paReadOnly
- Не означает, что свойство "только для чтения". Нельзя непосредственно ввести значение в Object Inspector. Если включены paValueList, paDialog или paSubProperties соответствующие им методы будут вызываться. Например, свойство Anchors не является "только для чтения", но редактор свойства типа "множество" содержит этот атрибут. Поэтому изменить Anchors можно только с помощью вложенных свойств.
- paAutoUpdate
- Любое изменение в Object Inspector сразу вызывает изменение свойства, не дожидаясь нажатия клавиши или перехода на другое свойство. Например, этот атрибут добавлен для TCaptionProperty.
- paRevertable
- Возвращает старое значение для свойства при нажатии клавиши . Относится только к значениям, непосредственно набранным в Object Inspector до нажатия или перехода на другое свойство. Этот атрибут, как часто используемый, тоже включен в TPropertyEditor.
- paFullWidthName
- Имя свойства отображается на всю длину, не оставляя для значения даже пикселя. Глубинный смысл этого атрибута ускользает от моего понимания.
- paVolatileSubProperties
- Любое изменение свойства вызывает изменение вложенных свойств, т.е. заново будет вызван метод GetProperties.
- paVCL
- Редактор использует VCL компонент. По логике вещей это должно означать, что редактор не используется под Linux. Не проверяла!
- paNotNestable
- Данный атрибут оказывает влияние только на раскрывающийся список свойства типа "компонент". Например, такой атрибут стоит у редактора TComponentNameProperty, в результате чего свойство Name не отображается в раскрывающемся списке.
Итак, приступим.
Значение Value содержит строковое представление свойства, отображающееся в левой колонке Object Inspector. Метод SetValue базового редактора не делает ничего, GetValue возвращает '(Unknown)'. Зато там же определено множество методов GetXXXValue, SetXXXValue для получения/установки значения типа XXX. Часто переопределение этих методов сводится к преобразованию строкового представления к нужному типу и вызову соответствующих методов.
Например, редактор свойства типа TDate:
Function TDateProperty.GetValue: string;
var
DT: TDateTime;
begin
DT := GetFloatValue;
if DT = 0.0 then Result := '' else
Result := DateToStr(DT);
End;
procedure TDateProperty.SetValue(const Value: string);
var
DT: TDateTime;
begin
if Value = ' then DT := 0.0
else DT := StrToDate(Value);
SetFloatValue(DT);
end;
| |
Надо заметить, что все методы SetXXXValue после установки нового значения вызывают метод Modified. Он сообщает дизайнеру форм, что свойство изменилось. Не забудьте об этом, если будете изменять значения свойства, не используя SetXXXValue.
Пример.
Свойство Items компонента Combobox. Для него используется редактор TStringListProperty. Метод GetValue возвращает тип свойства, в данном случае - '(TStrings)'. Сделаем его более информативным.
TComboItemsProperty=class(TStringListproperty)
public
function GetValue:string; override;
end;
function TComboItemsProperty.GetValue: string;
var items:TStrings;
begin
items:=TStrings(GetOrdValue);
case items.Count of
0: result:='Empty';
1: result:='1 item';
else
result:=IntToStr(items.Count)+' items';
end;
end;
| |
Этот метод должен сформировать выпадающий список. В качестве параметра ему передается указатель процедурного типа. Необходимо вызвать Proc для каждого значения свойства.
Самая простая реализация этого метода у TBoolProperty:
procedure TBoolProperty.GetValues(Proc: TGetStrProc);
begin
Proc('False');
Proc('True');
end;
| |
У перечислимых свойств реализация посложнее:
procedure TEnumProperty.GetValues(Proc: TGetStrProc);
var
I: Integer;
EnumType: PTypeInfo;
begin
EnumType := GetPropType;
with GetTypeData(EnumType)^ do
for I := MinValue to MaxValue do Proc(GetEnumName(EnumType, I));
end;
| |
procedure TEnumProperty.GetValues(Proc: TGetStrProc);
var
I: Integer;
EnumType: PTypeInfo;
pData: PTypeData;
Data:TTypeData;
cStr:strring;
begin
EnumType := GetPropType;
pData:=GetTypeData(EnumType);
Data:=pData^;
for i:=Data.MinValue to DataMaxValue do
begin
cStr:=GetEnumName(EnumType,I);
Proc(cStr);
end;
end;
| |
Примечание: для простоты были взяты редакторы версии 5. В 6-й версии булевы свойства объединены с перечислимыми.
Пример.
Для свойства Text компонента TCombobox создадим выпадающий список, состоящий из строк Items.
TComboTextProperty=class(TStringProperty)
public
function GetAttributes: TPropertyAttributes; override;
procedure GetValues(Proc: TGetStrProc); override;
end;
function TComboTextProperty.GetAttributes: TPropertyAttributes;
begin
Result := inherited GetAttributes + [paValueList];
end;
procedure TComboTextProperty.GetValues(Proc: TGetStrProc);
var i:integer;
begin
if PropCount>1 then Exit;
with TComboBox(GetComponent(0)) do
for i:=0 to Items.Count-1 do
Proc(Items[i]);
end;
| |
PropCount - cвойство, определенное в TPropertyEditor. Оно показывает, сколько выбрано компонент. У разных компонент могут быть разные списки, поэтому не будем формировать выпадающий список в случае множественного выбора. GetComponent(Index: Integer) - метод TPropertyEditor. Возвращает, несмотря на название, TPersistent, т.к. для вложенных свойств, например, Charset, вернет Font. Поскольку PropCount мы уже проверили, Index может быть только 0.
Пример. Для свойства Hint добавим многострочность.
THintProperty = class(TStringProperty)
public
function GetAttributes: TPropertyAttributes; override;
procedure Edit; override;
end;
function THintProperty.GetAttributes: TPropertyAttributes;
begin
Result := inherited GetAttributes;
if GetName='Hint' then
Result := Result + [paDialog];
end;
| |
К сожалению, нельзя зарегистрировать именно свойство Hint. Чтобы остальные строковые свойства оставить без изменений, нужна дополнительная проверка имени.
procedure THintProperty.Edit;
var Dlg:TStringsEditDlg;
begin
Dlg:=TStringsEditDlg.Create(nil);
try
Dlg.Lines.Text:=Value;
if Dlg.ShowModal=mrOk then
Value:=Copy(Dlg.Lines.Text,1,Length(Dlg.Lines.Text)-2);
finally
Dlg.Free;
end;
end;
| |
Форма TStringsEditDlg - это стандартная форма, используемая редактором свойств TStrings.
ToolsApi является основным поставщиком информации для экспертов. Но почему-то в литературе темы эксперты и редакторы считаются абсолютно разными. Вы вряд ли найдете упоминание о ToolsAPI, читая о редакторах. И напрасно! Не вдаваясь в подробности, я хочу продемонстрировать, что знание ToolsAPI может придать "блеск" вашим редакторам.
Пример: событие OnDrawDataCell для компонента TDbGrid. Реализация этого события обычно заключается в задании нужного цвета шрифта или фона, затем вызывается метод DefaultDrawDataCell. Но раз этот метод вызывается всегда, поручим это редактору.
procedure TDBGidDrawProperty.Edit;
var FileName:string;
MI:TIModuleInterface;
EI:TIEditorInterface;
EV:TIEditView;
ePos:TEditPos;
cPos:TCharPos;
i:integer;
Writer: TIEditWriter;
cStr:string;
lNeed:boolean;
begin
if GetMethodValue.Code=nil then
lNeed:=true
else
lNeed:=false;
inherited Edit;
if not lNeed then Exit;
FileName:=ToolServices.GetCurrentFile;
MI:=ToolServices.GetModuleInterface(FileName);
try
EI:=Mi.GetEditorInterface;
try
EV:=EI.GetView(0);
try
ePos:=EV.CursorPos;
EV.ConvertPos(true,ePos,cPos);
i:=EV.CharPosToPos(cPos);
writer:=EI.CreateWriter;
try
writer.CopyTo(i);
writer.Insert(''+#13#10);
cStr:=' '+TDBGrid(GetComponent(0)).Name+
'.DefaultDrawDataCell(Rect, Field, State);';
writer.insert(PChar(cStr));
finally
writer.Release;
end;
ePos.Col:=ePos.Col+2;
EV.CursorPos:=ePos;
finally
EV.Release;
end;
finally
EI.Release;
end;
finally
MI.Release;
end;
end;
| |
Наконец-то! Теперь у компонентов появился раскрывающийся список, содержащий published свойства, как у наследников TPersistent. Раньше нужные свойства приходилось искусственно выводить на поверхность, создавая лишние свойства, или класс-посредник. И все-таки некоторые неудобства существуют. Оказалось, что у компонентов published свойств много, а Object Inspector не резиновый. Кроме того, для них запрещен множественный выбор.
Пример. EditLabel компонента TLabeledEdit. Уменьшим количество свойств в раскрывающемся списке и разрешим им множественный выбор.
TLabelEditProperty = class(TComponentProperty)
protected
function МуFilterFunc(const ATestEditor: IProperty): Boolean;
function GetSelections: IDesignerSelections; override;
public
function GetAttributes: TPropertyAttributes; override;
procedure GetProperties(Proc: TGetPropProc); override;
end;
| |
Стандартный способ фильтрации состоит в регистрации редакторов всех ненужных свойств c атрибутом paNotNestable. Но если нужных свойств гораздо меньше, чем ненужных, можно поступить по-другому. С придирчивостью Эллочки-людоедки выбираем свойства. У меня получилось:
const LabelEditProperties: array[0..1] of string=('Font','Caption');
function TLabelEditProperty.МуFilterFunc(
const ATestEditor: IProperty): Boolean;
var i:integer;
begin
result := true;
for i:=0 to Length(LabelEditProperties)-1 do
if CompareStr(ATestEditor.GetName,LabelEditProperties[i])=0 then Exit;
Result := false;
end;
| |
Затем замещаем метод GetProperties. Он полностью соответствует стандартному за исключением последнего параметра у процедуры GetComponentProperties. К сожалению, стандартный метод FilterFunc не виртуальный.
procedure TLabelEditProperty.GetProperties(Proc: TGetPropProc);
var
LComponents: IDesignerSelections;
LDesigner: IDesigner;
begin
LComponents := GetSelections;
if LComponents <> nil then
begin
if not Supports(FindRootDesigner(LComponents[0]), IDesigner, LDesigner) then
LDesigner := Designer;
GetComponentProperties(LComponents, tkAny, LDesigner, Proc, MyFilterFunc);
end;
end;
| |
Множественный выбор
function TLabelEditProperty.GetSelections: IDesignerSelections;
var
I: Integer;
begin
Result := nil;
if (GetComponentReference <> nil) then
begin
Result := TDesignerSelections.Create;
for I := 0 to PropCount - 1 do
Result.Add(TComponent(GetOrdValueAt(I)));
end;
end;
| |
Это тоже практически стандартная реализация, за исключением закомментированного куска {and AllEqual}. Значение AllEqual возвращает true, если свойство "одинаково" для всех выбранных компонент. В этом случае его значение отображается в левой колонке, в противном случае - пустая строка. Для свойства типа компонент "одинаково" означает одну и ту же ссылку, что в данном случае всегда не так. Но это совсем не повод, чтобы не включать свойство в список выделенных.
function TLabelEditProperty.GetAttributes: TPropertyAttributes;
begin
Result := [paMultiSelect, paReadOnly, paSubProperties];
end;
| |
Примечание: массив необязательно должен быть статическим. Работа с реестром, файлами в DesignTime ничем не отличается от RealTime. Для инициализации можно заместить методы Activate или Initialize. Activate вызывается каждый раз, когда свойство выбирается в Object Inspector. Initialize - после создания редактора, но перед его использованием. Вызывается один раз.
Теперь предоставляется возможность проявить свои дизайнерские способности, рисуя в Object Inspector.
Пример. Свойство BorderIcons. Вместо biSystemMenu,biMinimize… которые все равно не помещаются в левой колонке, нарисуем системные кнопки.
В объявлении класса указываем интерфейс ICustomPropertyDrawing и реализуем его процедуры.
TBorderIconProperty = class(TSetProperty,ICustomPropertyDrawing)
public
procedure PropDrawName(ACanvas: TCanvas; const ARect: TRect;
ASelected: Boolean);
procedure PropDrawValue(ACanvas: TCanvas; const ARect: TRect;
ASelected: Boolean);
end;
| |
Имя свойства оставляем без изменений
procedure TBorderIconProperty.PropDrawName(ACanvas: TCanvas;
const ARect: TRect; ASelected: Boolean);
begin
DefaultPropertyDrawName(Self, ACanvas, ARect);
end;
| |
Значение свойства
procedure TBorderIconProperty.PropDrawValue(ACanvas: TCanvas;
const ARect: TRect; ASelected: Boolean);
var
Right: Integer;
Width:integer;
S: TIntegerSet;
begin
Right:=ARect.Left;
Width:=ARect.Bottom - ARect.Top+2;
Integer(S):=GetOrdValue;
if Ord(biSystemMenu) in S then
begin
DrawFrameControl(ACanvas.Handle,Rect(Right, ARect.Top,
Right+Width, ARect.Bottom),DFC_CAPTION,DFCS_CAPTIONCLOSE);
Right :=Right + Width;
end;
if Ord(biMinimize) in S then
begin
DrawFrameControl(ACanvas.Handle,Rect(Right, ARect.Top,
Right+Width, ARect.Bottom),DFC_CAPTION,DFCS_CAPTIONMIN);
Right :=Right + Width;
end;
if Ord(biMaximize) in S then
begin
DrawFrameControl(ACanvas.Handle,Rect(Right, ARect.Top,
Right+Width, ARect.Bottom),DFC_CAPTION,DFCS_CAPTIONMAX);
Right :=Right + Width;
end;
if Ord(biHelp) in S then
begin
DrawFrameControl(ACanvas.Handle,Rect(Right, ARect.Top,
Right+Width, ARect.Bottom),DFC_CAPTION,DFCS_CAPTIONHELP);
Right :=Right + Width;
end;
DefaultPropertyListDrawValue('', ACanvas, Rect(Right, ARect.Top, ARect.Right,
ARect.Bottom), ASelected);
end;
| |
В модуле VCLEditors.pas описан еще один интерфейс: ICustomPropertyListDrawing для собственной прорисовки выпадающего списка. Там же объявлена глобальная переменная FontNamePropertyDisplayFontNames. Если ей присвоить значение true, выпадающий список свойства FontName будет использовать для каждой строки свой шрифт.
procedure RegisterPropertyEditor(PropertyType: PTypeInfo;
ComponentClass: TClass;
const PropertyName: string;
EditorClass: TPropertyEditorClass);
| |
PropertyType — это указатель на RTTI для свойства, к которому применяется редактор. Получить его можно с помощью функции TypeInfo. ComponentClass - тип компонента. Если он равен nil, редактор применяется ко всем свойствам первого параметра. PropertyName - имя свойства. Если это значение равно пустой строке, редактор применяется ко всем свойствам первого параметра. EditorClass - тип редактора.
К сожалению, если не указан тип компонента, PropertyName не оказывает никакого влияния на ограничение применения редактора. Например, регистрация THintProperty:
RegisterPropertyEditor(TypeInfo(string),nil,''Hint'',THintProperty);
В данном случае регистрируется редактор для всех свойств типа string. Поэтому пришлось делать дополнительную проверку в методе Edit. При разработке собственных компонент можно учесть это прискорбное обстоятельство, объявляя новый тип. Например,
type TCaption = type string;
Это позволило зарегистрировать редактор именно для TypeInfo(TCaption).
Шестая версия оказалась революционной для редакторов свойств, и без жертв не обошлось.
- File not found: DsgnIntf.dcu.
-
Теперь вместо одного файла DsgnIntf существуют DesignEditors и DesignIntf.
- File not found: Proxies.dcu
-
Его действительно нет. Он в откомпилированном виде входит в пакет designide. Пакет designide не является свободно распространяемым, вы имеете право использовать его только на машине, на которой установлена Delphi. Поэтому вынесите код редакторов и процедуру регистрации в отдельный пакет с опцией "Designtime only". И добавьте designide в секцию requires.
Благодарности.
Спасибо Сергею Осколкову, научившему меня рисовать системные кнопочки. И Елене Филипповой, протестировавшей редакторы в седьмой версии.
К материалу прилагаются файлы:
[Редакторы свойств]
Обсуждение материала [ 14-01-2011 09:00 ] 13 сообщений |