Конференция "Компоненты" » как в TTreeView правильно изменить класс нодов по умлчанию? [D7, Win95/98, WinXP]
 
  • umbra © (07.04.08 12:20) [0]
    Пишу наследника
    TTreeView

    и хочу сделать так, чтобы при добавлении нодов создавался экземпляр наследника
    TTreeNode

    .

    Сам дошел до того, что надо переписать метод
    TTreeView.CreateNode

    , т.е. скрыть его.

    Класс нода, в принципе можно задавать в
    OnCreateNodeClass

    , но как-то мне кажется нехорошо   иметь в компоненте обработчик события.

    Вопрос в следующем: нет ли другого способа дать понять
    TTreeView

    , ноды какого класса ему создавать, кроме двух вышеоуказанных?

    И еще - что еще надо учесть, если я все-таки скрою
    TTreeView.CreateNode

    . Т.е. какие еще методы надо при этом перекрыть/скрыть?
  • Семеныч (07.04.08 13:09) [1]
    > т.е. скрыть его
    Не скрыть, а перекрыть. Метод виртуальный. Специально для сабжа и предназначен.

    > нехорошо иметь в компоненте обработчик события.
    Не просто нехорошо, а вообще недопустимо. Пользователь компонента имеет полное право назначить свой обработчик, и тогда компонент работать перестанет.

    > нет ли другого способа
    Зачем? Разве недостаточно того, что уже есть?

    > какие еще методы надо при этом перекрыть/скрыть?
    Никакие.

    function TMyTreeView.CreateNode: TTreeNode; // override
    var
     LClass: TTreeNodeClass;
    begin
     LClass := TMyTreeNode; // Это и будет Ваш класс по умолчанию.
     if Assigned(OnCreateNodeClass) then
       OnCreateNodeClass(Self, LClass);
     Result := LClass.Create(Items);
    end;

  • umbra © (07.04.08 13:22) [2]

    > function TMyTreeView.CreateNode: TTreeNode; // override
    > var
    >  LClass: TTreeNodeClass;
    > begin
    >  LClass := TMyTreeNode; // Это и будет Ваш класс по умолчанию.
    >
    >  if Assigned(OnCreateNodeClass) then
    >    OnCreateNodeClass(Self, LClass);
    >  Result := LClass.Create(Items);
    > end;
    >

    да, это я и имел в виду. это никак не отличается от скрытия, поскольку не вызывается метод предка.

    Спасибо за информацию.
  • umbra © (07.04.08 13:35) [3]
    Оказалось все не так просто :(

    Нужен еще наследник
    TTreeNodes

    , который бы знал, какой класс имеют ноды. Иначе теряется смысл всей этой кутерьмы :)
    Т.е. сейчас я перекрыл
    CreateNode

    , но при попытке сделать что-нибудь вроде

    MyTreeView.Items[1].MyNewProperty := x;



    Получаю сообщение о том, что
    MyNewProperty

    - неизвестный идентификатор. Что, в общем то, естественно, т.к. у
    TTreeNode

    такого свойства нет.
  • umbra © (07.04.08 13:38) [4]
    придется, похоже, еще и геттер
    TTreeNodes.Item

    скопировать
  • Семеныч (07.04.08 15:00) [5]
    > umbra

    > это никак не отличается от скрытия
    От скрытия это отличается как раз самым коренным образом. И вызов метода предка (или НЕ вызов) тут абсолютно ни при чем.

    TMyTreeNode(MyTreeView.Items[1]).MyNewProperty := x;


    И все будет работать.

    Насчет геттера - если он виртуальный или динамический, то все будет ОК. Но если он статический, то ПЕРЕкрыть его не удастся, а удастся именно ЗАкрыть. А этого делать нельзя, потому что у пользователя компонента запросто могут возникать баги.
  • Kolan © (07.04.08 15:08) [6]

    > А этого делать нельзя, потому что у пользователя компонента
    > запросто могут возникать баги.

    Да ну.

    Вот
    TComponentList

    закрывает методы
    TObjectList

    и не жужжит. :)
  • umbra © (07.04.08 15:08) [7]

    > TMyTreeNode(MyTreeView.Items[1]).MyNewProperty := x;
    > И все будет работать.

    это-то понятно


    > От скрытия это отличается как раз самым коренным образом.
    >  И вызов метода предка (или НЕ вызов) тут абсолютно ни при
    > чем.

    а чем в данном конкретном случае это отличается от объявления

    function CreateNode: TTreeNode; reintroduce; virtual;



    ?
  • Kolan © (07.04.08 15:12) [8]
    > function CreateNode: TTreeNode; reintroduce; virtual;

    Ты тут переопределил вирт. метод предка и снова объявил его виртуальным. Похоже на бред. Я вообще не могу представить зачем такое делать.
  • Kolan © (07.04.08 15:18) [9]
    Все что тебе надо сделать, «чтобы при добавлении нодов создавался экземпляр наследника TTreeNode» — перекрыть CreateNode.

    Вот так:
    function CreateNode: TTreeNode; override;



    function T???TreeView.CreateNode: TTreeNode;
    var
     LClass: TTreeNodeClass;
    begin
     LClass := TTreeNode;
     if Assigned(FOnCreateNodeClass) then
       FOnCreateNodeClass(Self, LClass);
     Result := LClass.Create(Items);
    end;



    И надо еще будет соответственно сделать аналог
    TTVCreateNodeClassEvent

    , который принимает классы не младше твоего.
  • Kolan © (07.04.08 15:18) [10]
    Забыл исправить.

    function T???TreeView.CreateNode: TTreeNode;
    var
    LClass: TTreeNodeClass;
    begin
    LClass := T???TreeNode;
    if Assigned(FOnCreateNodeClass) then
      FOnCreateNodeClass(Self, LClass);
    Result := LClass.Create(Items);
    end;

  • umbra © (07.04.08 15:21) [11]

    > Ты тут переопределил вирт. метод предка и снова объявил
    > его виртуальным. Похоже на бред.


    а так я практически скопировал код метода предка и вставил в свой метод. Тоже не верх элегантности. Зато хоть понятно, что это как бы совершенно новый метод, не опирающийся, типа, на старый :)
  • Kolan © (07.04.08 15:30) [12]
    > Тоже не верх элегантности. Зато хоть понятно, что это как
    > бы совершенно новый метод, не опирающийся, типа, на старый
    > :)

    Это, как бы сказать то…, эти директивы — это не комментарии, они кое-что значат.


    > а так я практически скопировал код метода предка и вставил
    > в свой метод.

    Ну и нет в этом ничего страшного. Это нормально в данном случае. Это правильное решение которое и имел ввиду разработчик TCustomTreeView делая метод
    CreateNode

    виртуальным.

    Я бы на твоем сместе от TCustomTreeView отнаследовался бы, чтобы с событием TTVCreateNodeClassEvent правильно расправится.
  • umbra © (07.04.08 17:18) [13]
    пришел к выводу, что без полного копирования класса
    TCustomTreeView

    в свой модуль невозможно создать его полноценного наследника, использующего наследника
    TTreeNode

    . Потому как в проекте, ипользующем контрол-наследник, все равно придется явно приводить тип нодов к нужному.
  • Kolan © (07.04.08 17:25) [14]
    Ты че. Тебе надо написать наследников TCustomTreeView, TTreeNodes, TTreeNode.

    В них тебе надо перекрыть 1(один) CreateNode и 1(один) CreateNodes и свойства Items.

    Вот и все.
  • Kolan © (07.04.08 17:29) [15]
    Тебе для того и вирт методы сделали, чтобы перекрывать их, а ты весь модулькопировать собрался, а потом говоришь, что 4 строки из [10] — это некрасиво…
  • Семеныч (07.04.08 18:35) [16]
    Kolan ©   (07.04.08 15:08) [6]

    1. Данукать не надо. Не в пивной.

    2. Вот это посмотри:
    http://pda.delphimaster.net/?id=1207063175&n=12

    > Kolan ©  (07.04.08 15:18) [9], [10] etc.
    Чукча не читатель?
  • Kolan © (07.04.08 18:42) [17]
    > 2. Вот это посмотри:

    Ты хочешь сказать, что я должен еще и о тех, кто хакает думать?
    Или ты имеешь ввиду, что Borland, закрывшый в методы в
    TComponentList

    и аналогах идиот?

    Чукча не читатель?
    Что непононятно, поясни?
  • Семеныч (07.04.08 19:02) [18]
    > Kolan ©   (07.04.08 18:42) [17]

    > Ты хочешь сказать, что...

    Я уже сказал (в [5]) и повторяю: попытка "перекрытия" статических методов предка чревата неоднозначностью поведения и соответствующими багами. Примеры см. в ветке по ссылке и хаки тут ни при чем.

    > Что непононятно, поясни?

    Поясняю: ты эту ветку не читал.
  • Kolan © (08.04.08 09:08) [19]
    Так предложи решение сабжа.
    Или ты считаешь, что скопировать к себе все TCustomTreeVeiw лучше чем закрыть геттеры?
  • Kolan © (08.04.08 09:28) [20]
    К тому же у той ветки, которую я таки прочел, есть отличия от этой.

    Какие интересно баги я получу если закрою геттеры вот так:
     TMyObject = class
     end;

     TMyList = class(TObjectList)
     protected
       function GetItem(Index: Integer): TMyObject;
       procedure SetItem(Index: Integer; const Value: TMyObject);
     public
       property Items[Index: Integer]: TMyObject read GetItem write SetItem;
     end;

    function TMyList.GetItem(Index: Integer): TMyObject;
    begin
     Result := inherited Items[Index] as TMyObject;
    end;

    procedure TMyList.SetItem(Index: Integer; const Value: TMyObject);
    begin
     inherited Items[Index] := Value;
    end;



    Я так понял имеется ввиду, что если теперь сделать так:

    procedure TForm1.BitBtn1Click(Sender: TObject);
    var
     ObjList: TMyList;
    begin
     ObjList := TMyList.Create;
     try
       ObjList.Add(TMyObject.Create);
       ObjList.Items[0];             {1}
       TObjectList(ObjList).Items[0];{2}
     finally
       ObjList.Free;
     end;
    end;



    То в строке 2 мы обратимся не к закрытому методу, что естественно, а к методу предка. Ну и что страшного? В реализации
    GetItem

    и
    SetItem

    мы это и сделали, только код из [5] теперь в одном месте, чего и хотел (и правильно хотел) автор.
  • Kolan © (08.04.08 09:43) [21]
    Можно еще посмотреть на TCollection сама суть которой в работе с наследниками (
    Each TCollection holds a group of TCollectionItem descendants.

    ).

    Смотрим справку:
    The following table lists some typical descendants …

    Берем, например,
    TCookieCollection

    .
    И там:
    TCookieCollection = class(TCollection)
     private
    &#133
     protected
       function GetCookie(Index: Integer): TCookie;
       procedure SetCookie(Index: Integer; Cookie: TCookie);
     public
    &#133
       property Items[Index: Integer]: TCookie read GetCookie write SetCookie; default;
     end;

    function TCookieCollection.GetCookie(Index: Integer): TCookie;
    begin
     Result := TCookie(inherited Items[Index]);
    end;

    procedure TCookieCollection.SetCookie(Index: Integer; Cookie: TCookie);
    begin
     Items[Index].Assign(Cookie);
    end;



    Тут даже старые геттеры не закрыты.
    Делая:
    var
     CookieCollection: TCookieCollection;
    begin
     TCollection(CookieCollection).Items[0]
    end;



    Попадаем в GetItem пердка, ну и что страшного? Этого мы и добивались ©. :)
  • Игорь Шевченко © (08.04.08 10:25) [22]
    Kolan ©   (08.04.08 09:43) [21]

    Поищи в гугле термин Wannabee
  • Kolan © (08.04.08 10:38) [23]
    Удалено модератором
    Примечание: Выражения выбираем, не в пивной
  • Игорь Шевченко © (08.04.08 11:12) [24]
    Kolan ©   (08.04.08 10:38) [23]

    <offtopic>
    Изучи пожалуйста правила этого ресурса http://www.delphimaster.ru/forums.shtml#rule
    Уважай труд модератора - у него трафик не резиновый
    </offtopic>
  • Kolan © (08.04.08 11:33) [25]
    Удалено модератором
    Примечание: Offtopic
  • Семеныч (08.04.08 12:31) [26]
    > Kolan ©   (08.04.08 09:08) [19]

    > Так предложи решение сабжа. Или ты считаешь...

    Это хорошо, что ты таки прочел ТУ ветку. А если бы прочел еще и ЭТУ, то совсем хорошо стало бы.

    > Какие интересно баги я получу если закрою геттеры

    Про TList со всеми его явными и неявными производными можешь мне не рассказывать, я их знаю не хуже тебя. И что в некоторых контейнерах переписаны статические методы - тоже знаю, и сам не раз так делал (приняв соответствующие меры безопасности - см. ниже).

    Однако же, код коду рознь и класс классу - тоже. Если ЗАкрытый потомком статический метод ТОЛЬКО вызывает метод предка и приводит класс, а вызванный из него метод предка НЕ использует других тоже ЗАкрытых потомком статических методов, то все нормально. Но если это не так - возможны баги.

    Возьмем тот же TComponentList. Например, я хочу, чтобы при вставке компонента в список или удалении из него происходило уведомление об этом какого-то моего объекта, причем стандартное решение (перекрытие метода Notify) меня по каким-то причинам не устраивает. Казалось бы, задача проще простого - пишем потомок TComponentList, в нем ЗАкрываем методы вставки и удаления (поскольу они статические), в которых и делаем нужное уведомление.

    Но не тут-то было. Юзер (прикладной программер) вдруг захотел (и имеет на это полное право) использовать мой список одновременно со стандартным. Для этого он пишет что-то вроде:

    procedure АddComponentToList(Component: TComponent; List: TComponentList);


    и совершенно справедливо полагает, что параметром List он будет передавать сюда нужный ему список (моего или стандартного класса) - и все обязано нормально работать.

    И действительно обязано. Но не будет. Не будет работать то самое уведомление, ради которого я и писал свой список и на которое юзер имеет полное право рассчитывать. И если он не имет моих исходников, то отловить этот баг и понять его причину ему будет очень непросто (если не сказать - вообще невозможно). Да даже если исходники и есть, то ловля багов такого рода - тоже совсем не сахар.

    Вот об этом обо всем я человека и предупредил. Дальнейшее решение (делать так или не делать) должен принять уже он сам (правда, после reintroduce; virtual; как-то грустно стало).

    А решение было дано еще в самом первом постинге. Сравни с тем, что ты написал спустя почти 10 постов и найди 2 отличия.

    Спрашивается - если ты ветку читал, то за каким лешим ты писал то же самое? А если не читал, то за каким лешим вообще писать?
  • Kolan © (08.04.08 12:50) [27]
    > Вот об этом обо всем я человека и предупредил.

    С примером согласен, это почти тоже самое что и в ветке про SetColor.

    Но (!), автору то не надо ничего добавлять, ему просто надо, чтобы Item у TTreeNodes уже были приведены к нужному классу, чтобы не делать это приведение повсеместно (кстати врядли кто-то еще, кроме автора, догодается, что объекты в дереве не TTreeNode если Items возвращает TTreeNode). Я не вижу ситуации где может возникнуть проблема если он просто закроет геттер для того, чтобы сделать приведение, подобно тому, как это делается в
    TObjectList

    и т.д…
    А раз, как я думаю, проблем не будет, то это и есть самое правильное решение.
    Вот что я пытаюсь сказать.


    > А решение было дано еще в самом первом постинге. Сравни
    > с тем, что ты написал спустя почти 10 постов и найди 2 отличия.

    Во-прервых ты там забыл про событие, в котором можно подсунуть TTreeNode, а не потомка, и будет плохо.
    А во-вторых, я видел что ты написал, но раз человек пытается делать
    reintroduce; virtual;

    , то я подумал, что повторить не лишне.
  • Семеныч (08.04.08 13:01) [28]
    > Kolan ©   (08.04.08 12:50) [27]

    > ты там забыл про событие

    Не забыл. Читай код.

    > в котором можно подсунуть TTreeNode, а не потомка

    Можно. И TTreeNode, и потомка, и любого другого потомка - тоже можно. Что юзер хочет - то он туда и подсовывает, имеет полное право. Это событие специально для того и сделано, чтобы юзер мог использовать свой собственный потомок TTreeNode, не переписывая больше ничего.

    > и будет плохо.

    С какой стати? Будут создаваться узлы того класса, который хотел юзер. Ничего плохого в этом нет.
  • Kolan © (08.04.08 13:20) [29]
    > С какой стати?

    С той стати, что автор компонента создает его для того, чтобы работать с
    TMyTreeNode

    или младше.
    И он будет считать, что Элементы у него не как не младше
    TMyTreeNode

    . И плохо будет когда автор внутри напишет что-то вроде
    TMyTreeNode(Items).MyProp := 10;

    , а пользователь компонента, в OnCreateNodeClass, передаст TTreeNode.

    Так что в OnCreateNodeClass может быть любой класс, но только TMyTreeNode или его потомок.


    > Не забыл. Читай код.

    Имелось ввиду см. выше.
  • Семеныч (08.04.08 13:51) [30]
    > Kolan ©   (08.04.08 13:20) [29]

    > И он будет считать, что Элементы у него не как не младше TMyTreeNode.

    Это подход прикладника, а не разработчика компонентов. Нормальный же разработчик компонентов либо должен закрыть это событие совсем (и документировать это), либо не имеет права так считать (и должен понимать это).

    Дружище, скажи честно - сколько компонентов "не для себя" написал?
  • Kolan © (08.04.08 15:02) [31]
    > закрыть это событие совсем

    Я же это и предлагаю. Либо закрыть совсем и сделать вообще с другим именем, но тогда те, кто привык к обьычному TTreeView будут материться.
    Либо сделать точно такое же, только:

    TTVCreateNodeClassEvent = procedure(Sender: TCustomTreeView;
       var NodeClass: TMyTreeNodeClass) of object;




    > Дружище, скажи честно — сколько компонентов «не для себя»
    > написал?

    В принципе, один, да и то… Так что можешь смело считать что ни одного.


    > (и должен понимать это).

    Хорошо, как же быть? Все что нужно сделано CreateNode перекрыт и внутри все работает, как же теперь объяснить прикладнику, что теперь элементы дерева обладают доп. функциональностью?

    Как тогда видел порождение потомков разработчик TTreeView? Он же сделал вирт. методы не просто так…
  • Семеныч (08.04.08 15:25) [32]
    > Kolan ©   (08.04.08 15:02) [31]

    > как же теперь объяснить прикладнику, что теперь элементы дерева
    > обладают доп. функциональностью?

    Дык... в документации, естественно. Можно в справке. А где ж еще-то? Больше и негде, и не было никогда.

    > Как тогда видел порождение потомков разработчик TTreeView?

    Это лучше у него и спросить. Мне тоже в VCL не все нравится. Скажем, в TListView есть аналогичный механизм для создания Item'ов юзерского класса, но почему-то нет такого же механизма для юзерского класса колонок.

    Но в целом написано все равно классно, ничего не скажешь.
  • Kolan © (08.04.08 15:29) [33]
    > Дык… в документации, естественно.

    Незнаю, если бы мне дали компоненет и казали «Вызывай Items так:
    TMyTreeNode(Tree.Items).SomeNewProp

    », я бы послал бы с таким компонентом куда подальше, и документацию тудаже.

    Я как не разработчик компонентов хучу писать:
    Tree.Items.SomeNewProp

    . И, имхо, это естественно.
  • umbra © (10.04.08 17:42) [34]

    > правда, после reintroduce; virtual; как-то грустно стало

    я таки боюсь, что меня заплюют, но объясните мне что в этом такого плохого? без отсылок куда либо, а простыми русским словами
  • Семеныч (10.04.08 18:07) [35]
    > umbra ©   (10.04.08 17:42) [34]

    Атрибуты reintroduce; virtual; разрывают цепочку наследования виртуального метода и начинают новую. Зачем?
  • Семеныч (10.04.08 18:12) [36]
    > Семеныч   (10.04.08 18:07) [35]

    Проще говоря, если юзер напишет TTreeView(YourComponent).CreateNode (на что он имеет полное право), то буден вызван старый метод CreateNode, а не Ваш. Соответственно, будет создан узел класса TTreeNode, а не Вашего.
  • umbra © (10.04.08 18:13) [37]

    > Зачем?

    затем же, зачем и копировать код метода полностью из аналогичного метода предка. Причем этот предок - первый в иерархии.

    По-моему в данном конкретном случае не имеет значения, прерывать цепочку или нет.
  • umbra © (10.04.08 18:16) [38]

    > если юзер напишет TTreeView(YourComponent).CreateNode

    честно говоря, не могу придумать, зачем такая конструкция может понадобиться. :)
  • Семеныч (10.04.08 20:21) [39]
    > umbra ©   (10.04.08 18:16) [38]

    Пример. У юзера на форме лежать два TreeView - Ваше и стандартное. Он хочет динамически заполнять их и чтобы не писать 2 отдельных метода, пишет один общий. Этот общий метод имеет параметр - дерево, которое надо заполнить. Юзер совершенно правильно пишет метод так:

    procedure TForm1.FillTreeView(TreeView: TreeView);
    begin
     // Где-то здесь многократно вызывается CreateNode.
    end;


    и совершенно справедливо полагает, что все должно правильно работать. Но нарывается на Ваш баг.
  • umbra © (10.04.08 20:30) [40]

    > procedure TForm1.FillTreeView(TreeView: TreeView);
    > begin
    >  // Где-то здесь многократно вызывается CreateNode.
    > end;

    резонно. идея понятна. спасибо.
    :)
 
Конференция "Компоненты" » как в TTreeView правильно изменить класс нодов по умлчанию? [D7, Win95/98, WinXP]
Есть новые Нет новых   [134464   +62][b:0][p:0.004]