Rambler's Top100
"Knowledge itself is power"
F.Bacon
Поиск | Карта сайта | Помощь | О проекте | ТТХ  
 Подземелье Магов
  
 

Фильтр по датам

 
 К н и г и
 
Книжная полка
 
 
Библиотека
 
  
  
 


Поиск
 
Поиск по КС
Поиск в статьях
Яndex© + Google©
Поиск книг

 
  
Тематический каталог
Все манускрипты

 
  
Карта VCL
ОШИБКИ
Сообщения системы

 
Форумы
 
Круглый стол
Новые вопросы

 
  
Базарная площадь
Городская площадь

 
   
С Л С

 
Летопись
 
Королевские Хроники
Рыцарский Зал
Глас народа!

 
  
ТТХ
Конкурсы
Королевская клюква

 
Разделы
 
Hello, World!
Лицей

Квинтана

 
  
Сокровищница
Подземелье Магов
Подводные камни
Свитки

 
  
Школа ОБЕРОНА

 
  
Арсенальная башня
Фолианты
Полигон

 
  
Книга Песка
Дальние земли

 
  
АРХИВЫ

 
 

Сейчас на сайте присутствуют:
 
  
 
Во Флориде и в Королевстве сейчас  08:22[Войти] | [Зарегистрироваться]

Сапоги для сапожника

Инна Аринович
дата публикации 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
Имя свойства отображается на всю длину, не оставляя для значения даже пикселя. Глубинный смысл этого атрибута ускользает от моего понимания.
Новые атрибуты, появившиеся в версии 6.
paVolatileSubProperties
Любое изменение свойства вызывает изменение вложенных свойств, т.е. заново будет вызван метод GetProperties.
paVCL
Редактор использует VCL компонент. По логике вещей это должно означать, что редактор не используется под Linux. Не проверяла!
paNotNestable
Данный атрибут оказывает влияние только на раскрывающийся список свойства типа "компонент". Например, такой атрибут стоит у редактора TComponentNameProperty, в результате чего свойство Name не отображается в раскрывающемся списке.
Итак, приступим.

property Value: string read GetValue write SetValue;

Значение 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
// TStrings - это указатель, а значит целое.
// ему соответствует метод GetOrdValue
  items:=TStrings(GetOrdValue);
  case items.Count of
    0: result:='Empty';
    1: result:='1 item';
  else
    result:=IntToStr(items.Count)+' items';
  end;
end;



GetValues(Proc: TGetStrProc);
TGetStrProc = procedure(const S: string) of object;

Этот метод должен сформировать выпадающий список. В качестве параметра ему передается указатель процедурного типа. Необходимо вызвать 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;

{$IFDEF I_DO_NOT_UNDERSTAND}
{
Здесь используется информация о типах времени исполнения (RTTI).
Это основной поставщик информации для редакторов свойств.
Умение в совершенстве работать с RTTI для разработчика редакторов обязательно.
Необходимым и достаточным условием для этого является изучение
модуля TypInfo.pas.
Чтобы ускорить этот процесс, разберем предыдущий пример поподробнее
}
procedure TEnumProperty.GetValues(Proc: TGetStrProc);
var
  I: Integer;
  EnumType: PTypeInfo;
  pData: PTypeData;
  Data:TTypeData;
  cStr:strring;
begin
// получение указателя на RTTI с помощью метода TPropertyEditor GetPropType
  EnumType := GetPropType;
// используем его для получения указателя на TTypeData с помощью функции
// GetTypeData(TypeInfo: PTypeInfo): PTypeData. Она определена в TypInfo.pas
  pData:=GetTypeData(EnumType);
  Data:=pData^;
// TTypeData представляет собой обыкновенную вариантную запись
{ в данном случае нас интересует вариант:
        case TTypeKind of
          tkInteger, tkChar, tkEnumeration, tkWChar: (
            MinValue: Longint;
            MaxValue: Longint;
            case TTypeKind of
              tkInteger, tkChar, tkWChar: ();
              tkEnumeration: (
                BaseType: PPTypeInfo;
                NameList: ShortStringBase));}
  for i:=Data.MinValue to DataMaxValue do
  begin
{
функция GetEnumName тоже определена в TypInfo.pas.
По типу перечислимого свойства и индексу элемента она
определяет строковое представление этого элемента.
}
     cStr:=GetEnumName(EnumType,I);
     Proc(cStr);
  end;
end;
{$IFEND}

Примечание: для простоты были взяты редакторы версии 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.

Edit
Пример.
Для свойства 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
// убираем последние 2 символа конца строки
      Value:=Copy(Dlg.Lines.Text,1,Length(Dlg.Lines.Text)-2);
  finally
    Dlg.Free;
  end;
end;

Форма TStringsEditDlg - это стандартная форма, используемая редактором свойств TStrings.

Помни о ToolsAPI !

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);';
// добавляем вызов DefaultDrawDataCel
         writer.insert(PChar(cStr));
       finally
// REALESE, а не Free !!!
         writer.Release;
       end;
// курсор на пустую строчку
       ePos.Col:=ePos.Col+2;
       EV.CursorPos:=ePos;
     finally
       EV.Release;
     end;
   finally
     EI.Release;
   end;
 finally
   MI.Release;
 end;
end;

Delphi 6. Что новенького?

Вложенные свойства для свойства типа "компонент".

Наконец-то! Теперь у компонентов появился раскрывающийся список, содержащий 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) {and AllEqual} 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).

Проблемы переноса из младших версий в версии 6,7.

Шестая версия оказалась революционной для редакторов свойств, и без жертв не обошлось.

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 сообщений
  
Время на сайте: GMT минус 5 часов

Если вы заметили орфографическую ошибку на этой странице, просто выделите ошибку мышью и нажмите Ctrl+Enter.
Функция может не работать в некоторых версиях броузеров.

Web hosting for this web site provided by DotNetPark (ASP.NET, SharePoint, MS SQL hosting)  
Software for IIS, Hyper-V, MS SQL. Tools for Windows server administrators. Server migration utilities  

 
© При использовании любых материалов «Королевства Delphi» необходимо указывать источник информации. Перепечатка авторских статей возможна только при согласии всех авторов и администрации сайта.
Все используемые на сайте торговые марки являются собственностью их производителей.

Яндекс цитирования