На главную Наши проекты:
Журнал   ·   Discuz!ML   ·   Wiki   ·   DRKB   ·   Помощь проекту
ПРАВИЛА FAQ Помощь Участники Календарь Избранное RSS
msm.ru
! Правила раздела FAQ в группе разделов С++.
1. Раздел FAQ предназначен для публикации готовых статей.
2. Здесь нельзя задавать вопросы, для этого существуют соответствующие разделы:
Чистый С++
Visual C++ / MFC / WTL / WinApi
Borland C++ Builder
COM / DCOM / ActiveX / ATL
Сопутствующие вопросы
3. Внимание, все темы и сообщения в разделе премодерируются. Любое сообщение или тема будут видны остальным участникам только после одобрения модератора.
Модераторы: B.V., Qraizer
  
> Создание VCL компонентов в BCB , Основы, ресурсы, редактор свойств...
    Скрытый текст
    Все исходники, приведённые в этой статье, распространяются по BSD-лиценизии.
    Говоря по-простому - делайте с ними, что хотите, ну а я умываю руки.


    В этой статье на простом примере демонстрируется, как можно создавать
    собственные VCL-компоненты в среде Borland C++ Builder. Т.к. это довольно
    избитая тема, то, не пропуская основные моменты создания компонента, здесь
    приводятся часто опускаемые в литературе (как "для облегчения понимания" или,
    наоборот, как "само собой разумеющееся") вопросы: что должно входить в комплект
    поставки компонента, использование собственных ресурсов в компоненте, создание
    собственного редактора свойств компонента.
    Давайте попробуем на примере создания простейшего компонента рассмотреть эти
    вопросы. Я предлагаю сделать компонент, аналогичный стандартному и хорошо Вам
    известному компоненту TLabel, слегка модифицировав его свойства (Вы,
    конечно, в праве отказаться от этого предложения, но, боюсь, что уже поздно).
    Заранее прошу извинить мой консерватизм, но всё действо будет относиться к
    5ой версии BCB, какие изменения надо сделать для 6ой (и надо ли их делать) - я
    не знаю. Так же заранее я прошу Вас воздержаться от вопросов типа "А почему
    "это" было сделано "так", а не "этак"; ведь "этак" будет красивее/удобнее?",
    потому что понятия красоты и удобства, вещи, мягко говоря, довольно
    субъективные; можете считать, что я ответил: "А лично мне "так" больше
    нравится". В свою очередь обещаю с должным вниманием отнестись к замечаниям
    типа ""Это" сделано (сказано) не правильно (не верно), потому что ***" с
    убедительными ***, а такие замечания наверняка возникнут.
    Ну, и так, раз мы создаем аналог TLabel, то над выбором базового класса
    для нашего компонента особенно долго думать не будем (хотя, стоит заметить, в
    общем случае к этому вопросу надо подходить более обстоятельно) - пусть это
    будет TCustomLabel, который является базовым и для TLabel. Пусть,
    для начала, наш компонент, назовем его, скажем, TMyLabel имеет такое отличие от
    TLabel - строка заголовка (Caption) будет "анимированной" - т.е.
    ее символы будут появляться по одному через некоторый интервал времени, а после
    появления всей строки - опять повторяем все по кругу. Для управления этой
    "анимацией" пусть у TLabel будут два свойства: булевое (bool) Animate,
    при установке которого в true анимация разрешена, и целочисленное (int)
    AnimateInterval, задающее период появления символов в миллисекундах.
    Создадим сначала отдельную библиотеку (BPL, package) для нашего компонента.
    Выбираем File->New..., вкладка New, находим иконку Package и нажимаем ОК. IDE
    создаст каркас нашей библиотеки, сохраняем все где-нибудь в укромном месте.
    Теперь создадим компонент. Воспользуемся преимуществами IDE (хотя весь код
    каркаса компонента можно набить и вручную) - выбираем Component->New... и, в
    появившемся диалоге создания нового компонента, в поле Ancestor Type (тип
    родителя) - введем или выберем TCustomLabel, в поле ClassName - TMyLabel,
    в поле PalletePage - My Page (это имя той вкладки на палитре компонентов, на
    которой будет размещена иконка нашего компонента после установки библиотеки),
    остальные поля можно не трогать, а их назначение, думаю, можно понять из их
    названия. Нажимаем ОК и IDE создает каркас компонента. Сохраняем cpp-файл под
    именем MyLabel, лучше там же, где и библиотека. Теперь нам надо просто добавить
    необходимые члены-данные, члены-методы и свойства. Это можно сделать,
    используя ClassExplorer, а можно и руками. Вот какие изменения надо внести:
    Скрытый текст
    ExpandedWrap disabled
      //MyLabel.h
      ...
      #include <StdCtrls.hpp>
      #include <Menus.hpp>
      #include <Controls.hpp>
       
      class PACKAGE TMyLabel : public TCustomLabel
      {
        typedef TCustomLabel inherit;//обычная практика, просто для удобства
      private:
        TTimer *timer;//это таймер, задающий интервал
        int CurPos;//счетчик текущей позиции для вывода неполного текста
      protected:
        void __fastcall TimerEvent(TObject* Sender); //здесь мы будем изменять значение CurPos
       
        //а вот эту виртуальную функцию мы как раз и переопределим
        //обычная GetLabelText (а именно TCustomLabel::GetLabelText), возвращает
        //то, что задано в св-ве Caption, мы же переопределим ее так, чтобы
        //она возвращала текст в зависимости от значения нашего счетчика CurPos;
        virtual AnsiString __fastcall GetLabelText();//эта функция вызывается при выводе текста внутри метода
                                                //класса TCustomLabel
       
      public:
        __fastcall TMyLabel(TComponent* Owner);//конструктор компонента
        virtual __fastcall ~TMyLabel();//деструктор компонента
       
      protected:
        //члены методы, используемые при определении новых свойств
        void __fastcall SetAnimInterval(int Value);
        int __fastcall GetAnimInterval();
        void __fastcall SetAnimate(bool Value);
        bool __fastcall GetAnimate();
        
      __published:
        //свойства, объявленные в этой секции будут видны в Object Inspector
        //при условии, что они - не "только для чтения"
        //будем использовать для "хранения" значения свойства Animate
        //свойство таймера TTimer::Enabled
        __property bool Animate = {read = GetAnimate, write = SetAnimate};
        __property int AnimateInterval = {read = GetAnimInterval, write = SetAnimInterval};
       
      __published:
      //а это свойства, наследуемые от TCustomLabel и его предков
      //т.к. в этих предках они не объявлены в секции published, то, если их не написать
      //здесь, они и не будут видны в Object Inspector
        __property Align ;
        __property Alignment ;
        __property Anchors ;
        __property AutoSize ;
        __property BiDiMode ;
        __property Caption ;
        __property Color ;
        __property Constraints ;
        __property DragCursor ;
        __property DragKind ;
        __property DragMode ;
        __property Enabled ;
        __property FocusControl ;
        __property Font ;
        __property ParentBiDiMode ;
        __property ParentColor ;
        __property ParentFont ;
        __property ParentShowHint ;
        __property PopupMenu;
        __property ShowAccelChar ;
        __property ShowHint ;
        __property Transparent ;
        __property Layout ;
        __property Visible ;
        __property WordWrap ;
        __property OnClick ;
        __property OnContextPopup ;
        __property OnDblClick ;
        __property OnDragDrop ;
        __property OnDragOver ;
        __property OnEndDock ;
        __property OnEndDrag ;
        __property OnMouseDown ;
        __property OnMouseMove ;
        __property OnMouseUp ;
        __property OnStartDock ;
        __property OnStartDrag ;
      };
      ...
      //--------------------------------  
      //MyLabel.cpp
      ...
      //---------------------------------------------------------------------------
      // ValidCtrCheck is used to assure that the components created do not have
      // any pure virtual functions.
      // - это означает, что эта функция предназначена только для того,
      // чтобы разработчик компонента не забыл переопределить чистые виртуальные
      // функции базового класса, если они есть, т.е. если базовый класс -
      // абстрактный, т.к. реальный компонент не может быть абстрактным
      static inline void ValidCtrCheck(TMyLabel *)
      {
        new TMyLabel(NULL);
      }
      //---------------------------------------------------------------------------
      __fastcall TMyLabel::TMyLabel(TComponent* Owner)
      : TCustomLabel(Owner)
      {
        //инициализируем наш таймер
        timer=new TTimer(this); //передавая this в его конструктор, мы будем уверены
        //что при удалении нашего компонента удалиться и этот таймер
        timer->Interval=500;//по умолчанию текст будет анимирован с частотой в 0.5 сек
        timer->OnTimer=TimerEvent;//задаем нашу функцию обработки события таймера
        CurPos=0;//инициализируем счетчик позиции
      }
      //---------------------------------------------------------------------------
      __fastcall TMyLabel::~TMyLabel()
      {
      }
      //---------------------------------------------------------------------------
      AnsiString __fastcall TMyLabel::GetLabelText()
      {
        if(Animate)
        {
          AnsiString ret=Caption;
          ret.SetLength(CurPos);
          if(AutoSize)
          {//насильно увеличиваем ширину компонента
            int wt=Canvas->TextWidth(ret);
            if(Width<wt)
              Width=wt;
          }
          return ret;
        }
        return Caption;
      }
      //---------------------------------------------------------------------------
      void __fastcall TMyLabel::TimerEvent(TObject* Sender)
      {
        //на всякий случай проверим, что компонент не находится в стадии уничтожения
        if(!ComponentState.Contains(csDestroying))
        {
          //каждый раз увеличиваем счетчик, пока он не станет больше длины текста
          if(++CurPos>Caption.Length())
            CurPos=0;//если больше длины текста, то устанавливаем его в 0
          Invalidate();
        }
      }
      //---------------------------------------------------------------------------
      void __fastcall TMyLabel::SetAnimInterval(int Value)
      {
        if(timer->Interval!=(Cardinal)Value)
          timer->Interval=(Cardinal)Value;
      }
      //---------------------------------------------------------------------------
      int  __fastcall TMyLabel::GetAnimInterval()
      {
        return (int)timer->Interval;
      }
      //---------------------------------------------------------------------------
      void __fastcall TMyLabel::SetAnimate(bool Value)
      {
        if(timer->Enabled!=Value)
        {
          timer->Enabled=Value;
          CurPos=0;
          Invalidate();
        }
      }
      //---------------------------------------------------------------------------
      bool __fastcall TMyLabel::GetAnimate()
      {
        return timer->Enabled;
      }
      //---------------------------------------------------------------------------
      namespace Mylabel
      {
        void __fastcall PACKAGE Register()
        {
          TComponentClass classes[1] = {__classid(TMyLabel)};
          //пусть компонент будет расположен на вкладке с именем "My Page"
          //эта функция регистрирует наш компонент
          RegisterComponents("My Page", classes, 0);
          //если бы мы хотели зарегистрировать не один, а больше компонентов (пусть N)
          //то массив classes надо было объявить как  classes[N], а значение последнего
          //параметра ф-ции RegisterComponents должно было быть N-1  (индекс последнего элемента
          //массива classes[N])
        }
      }
    Ну вот, компонент практически готов. Давайте теперь создадим для него
    иконку, иначе на палитре компонентов для него будет назначена иконка
    родительского компонента.
    Проще всего сделать так - скопировать из $(BCB)\Examples\Controls\Source
    какой-нибудь рез-файл компонента, например "trayicon.res" в папку с нашей
    библиотекой и компонентом, переименовать его в "MyLabel.res" и открыть его в
    Image Editor (меню Tools), переименовать лежащий в нем bitmap в TMYLABEL,
    и перерисовать этот битмап как душе угодно, оставив, правда, его разрешение и
    размер нетронутыми. Не забудем сохранить. Теперь можно добавить этот файл в нашу
    библиотеку - я делаю это добавлением строки USERES("MyLabel.res"); в файл
    Package1.cpp после строки USEUNIT("MyLabel.cpp"); (хотя можно и
    обычным - Project->Add to Project... - результат одинаков).
    Но так как в будущем мы планируем переделать компонент для демонстрации работы
    с ресурсами, лучше сделать следующим образом. Создадим любым редактором растров
    bmp-файл размером 24х24 точки и разрешением 16 цветов. Сохраним этот файл в
    директории нашей библиотеки, неважно, под каким именем, пусть "mylabel.bmp".
    Создадим в той же директории текстовый (ASCII) файл с таким содержимым:
    TMYLABEL BITMAP "mylabel.bmp"
    Важно, что бы имя ресурса совпадало с именем класса нашего компонента. Сохраним
    этот текстовый файл под именем "mylabel.rc"; важно, что бы имя этого файла
    совпадало с именем срр-файла с кодом для нашего компонента. Добавим этот файл к
    проекту библиотеки (можно вставить строку USERС("MyLabel.rс");); теперь
    при сборке библиотеки этот файл будет компилироваться в "MyLabel.res".
    Теперь компонент готов для пробной установки. Собираем библиотеку, вылавливая
    опечатки, и устанавливаем. Создаем тестовый ехе-проект, кидаем на его главную
    форму наш компонент, и любуемся им (в смысле - тестируем) в Design- и Run-time.
    Наш компонент довольно примитивен, для более сложных компонентов я рекомендовал
    бы сначала потестировать его, создавая динамически. Для этого в проект тестового
    приложения надо включить модуль "MyLabel.cpp", а в модуле основной формы
    тестового приложения:
    Скрытый текст
    ExpandedWrap disabled
      //unit1.cpp - Test Application:
      ...
      #include "MyLabel.h"
      ...
      TMyLabel* mlb; //это тестовое приложение, так что
      //не будем привередливыми, используем глобальную переменную
      void __fastcall TForm1::Button1Click(TObject*)
      {  //создаем компонент динамически:
        mlb=new TMyLabel(this);
        mlb->Parent=this;
        mlb->Top=10;
        mlb->Left=10;
        mlb->Caption="DynaLabel";
      }
       
      void __fastcall TForm1::Button2Click(TObject*)
      {  
        if(mlb)
          mlb->Activate=!mlb->Activate;//тестируем свойство
      }


    Исходник этого компонента:
    Прикреплённый файлПрикреплённый файлPackage1.zip (7.94 Кбайт, скачиваний: 616)
      Краткое отступление о комплекте поставки.
      Если не ставить задачу сокрытия исходников компонента, то комплект поставки - в
      прилагаемом архиве (Package1.bpr, Package1.cpp, MyLabel.cpp,
      MyLabel.h, MyLabel.rc, MyLabel.bmp).
      Если исходники передавать не хочется (от жадности, например), то в комплект
      поставки надо внести файлы Package1.bpl, Package1.bpi, Package1.lib и файл
      MyLabel.h. Файл bpl - это динамическая библиотека, аналог dll, содержащая код
      и ресурсы нашего компонента, bpi - это файл для статической линковки bpl и lib -
      это статическая библиотека с кодом для внесения его в ехе-файл при использовании
      библиотеки без bpl (т.е. отключенной опцией проекта "Build with runtime
      packages"
      ).
      Если в компоненте использовались сторонние библиотеки, то в комплект
      поставки надо включать и их.

      Теперь давайте усовершенствуем компонент, при чем сделаем это таким образом,
      чтобы он использовал ресурсы. Подвох здесь в том, что lib-файл библиотеки не
      может содержать ресурсы (в отличие от bpl), и для того, чтобы проект,
      использующий наш компонент, мог быть собран с выключенной опцией "Build with
      runtime packages"
      , мы должны, во-первых, включить ресурс в комплект поставки
      отдельно (не только в bpl), например, в виде файла скомпилированного
      ресурса (*.res), а во-вторых, как-то сообщить этому проекту, что ресурсы
      из этого файла надо "сунуть" в выходной ехе-файл - иначе наш компонент их не
      найдет.
      Пусть наш компонент "автоматически" создает всплывающее меню (PopupMenu),
      которое будет состоять из одного пункта, запрещающего или позволяющего "анимацию"
      в зависимости от текущего значения свойства Animate. Для этого добавим
      в объявление класса нашего компонента всего одну функцию и одну директиву в
      соответствующий заголовочный файл:
      Скрытый текст
      ExpandedWrap disabled
        //MyLabel.h
        ...
        protected:
          ...
          //эта функция будет обработчиком выбора пункта меню
          void __fastcall OnPMClick(TObject* Sender);
        public:
          __fastcall TMyLabel(TComponent* Owner);
        ...
        };
        //---------------------------------------------------------------------------
        //а вот это собственно, то что заставит линковщик подключать ресурс
        //с нашими картинками.
        //Дело в том, что если проект, использующий наш компонент, собирается
        //с отключенным "Build with runtime packages", то код компонента "вставляется" в
        //ехе-шник из статической библиотеки *.lib нашего пакаджа. А в этой библиотеке,
        //в отличие от динамической *.bpl, ресурсы храниться не могут. Поэтому ресурсы,
        //необходимые для работы нашего компонента при статической линковке, мы должны
        //поставлять отдельно, лучше в уже скомпилированном виде. Еще мы должны каким-то
        //образом сказать линковщику, собирающему проект с нашим компонентом, что надо
        //эти ресурсы вставить в выходной ехе-файл. Лучшего способа, чем добавление
        //этой строки в наш заголовочный файл, я не придумал.
        #pragma resource "MyLabel.res"
        //не забыть включить MyLabel.res в комплект поставки!
        #endif
      а в файл MyLabel.cpp внесем следующие изменения:
      Скрытый текст
      ExpandedWrap disabled
        //MyLabel.cpp
         
        //---------------------------------------------------------------------------
        __fastcall TMyLabel::TMyLabel(TComponent* Owner)
        : TCustomLabel(Owner)
        {
          ...
          //создаем всплывающее меню, воспользуемся свойством TControl::PopupMenu
          PopupMenu = new TPopupMenu(this);//назначаем владельцем сам компонент
          //и пункт меню для него
          TMenuItem* mi = new TMenuItem(PopupMenu);
          //назначаем обработчик его выбора (ту функцию, что мы добавили в h-файле)
          mi->OnClick = OnPMClick;
          //и добавляем в меню
          PopupMenu->Items->Add(mi);
         
          //задаем начальные свойства
          //так как по умолчанию меню анимировано, то пункт должен останавливать
          //"анимацию"
          mi->Caption = "Disable animate";
          //загружаем соответствующую картинку из ресурсов
          mi->Bitmap->Handle = LoadImage(HInstance, "DISABLEBMP", IMAGE_BITMAP, 0, 0, LR_LOADMAP3DCOLORS);
          
          //Т.к. программист-пользователь компонента может захотеть
          //назначить нашему компоненту своё всплывающее меню,
          //то, мы должны как-то определить, что используется
          //именно наше меню. Для этого давайте использовать
          //св-во Tag нашей менюшки.
          PopupMenu->Tag = (int)this;
        }
        ...
        //---------------------------------------------------------------------------
        void __fastcall TMyLabel::SetAnimate(bool Value)
        {
          if(timer->Enabled!=Value)
          {
            timer->Enabled=Value;
            CurPos=0;
            Invalidate();
            //проверим, наше ли меню используется?
            if(PopupMenu && PopupMenu->Items->Count>0 && PopupMenu->Tag==(int)this)
            {//да наша. Надо переделать пункт:
              TMenuItem *mi = PopupMenu->Items->Items[0];
              if(Value)
              {
                mi->Caption = "Disable animate";
                //насколько я понимаю, память из-под предыдущего битмапа
                //освобождается при смене Handle, поэтому беспокоиться об этом не нужно
                mi->Bitmap->Handle = LoadImage(HInstance, "DISABLEBMP", IMAGE_BITMAP, 0, 0, LR_LOADMAP3DCOLORS);
              }
              else
              {
                mi->Caption = "Enable animate";
                mi->Bitmap->Handle = LoadImage(HInstance, "ENABLEBMP", IMAGE_BITMAP, 0, 0, LR_LOADMAP3DCOLORS);
              }
            }
          }
        }
        //---------------------------------------------------------------------------
        //функция обработки выбора пункта меню
        void __fastcall TMyLabel::OnPMClick(TObject* Sender)
        {//просто делаем наоборот
          Animate=!Animate;
        }
        ...
      Теперь создаем два файла-картинки, размером 16х16 точек, первая, например -
      зеленая галочка, а второй - красный крестик, сохраняем их под именами
      "enable.bmp" и "disable.bmp" соответственно. Добавляем две строки
      в файл "MyLabel.rc":
      Скрытый текст
      ExpandedWrap disabled
        //MyLabel.rc
        ...
        ENABLEBMP BITMAP "enable.bmp"
        DISABLEBMP BITMAP "disable.bmp"
      Вот и все. Пересобираем библиотеку и идем мучать тестовый проект с разными
      опциями линкера. Измененный исходник:
      Прикреплённый файлПрикреплённый файлPackage2.zip (11.29 Кбайт, скачиваний: 501)
        Ну а теперь займемся еще более интересными вещами. В литературе часто опускается
        внесение в компонент обработчика какого-либо события, а также создание собственного
        редактора свойств(а) компонента. Добавим к нашему компоненту событие - например,
        возникающее при срабатывании нашего таймера анимации. Пусть пользователь компонента
        имеет возможность назначить обработчик и в нем узнать "текущую позицию" анимации, и
        если захочет, поменять ее. Добавим к компоненту еще и какое-нибудь "хитрое" свойство
        и свой редактор этого свойства - диалоговое окно, появляющееся в Design-time при клике
        на кнопку с тремя точками (ellipsis button) в правой части строки свойства в Object
        Inspector. За это отвечает так называемый "Редактор свойств" (Property Editor)
        Пусть еще во всплывающем меню при клике правой клавишей мыши на нашем компоненте в
        Design-Time добавляется наш пункт меню с вызовом этого же диалога - ответственный за это
        действие - "Редактор компонентов" (Component Editor; по умолчанию, т.е. без
        определения собственного, за это отвечает класс TDefaultEditor)

        У нашего TMyLabel есть недостаток, присутствующий и у TLabel - отсутствие
        возможности задания рамки по периметру. Давайте будем рисовать её, а для
        управления этим рисованием создадим новый класс, который будет управлять этим рисованием;
        пусть это будет наследник от класса TPersistent. Вставим его определение в файл
        "MyLabel.h" перед определением класса нашего компонента - TMyLabel:
        Скрытый текст

        ExpandedWrap disabled
          //MyLabel.h
          ...
          //объявляем класс, определяющий свойства рамки
          class  PACKAGE TLabelBorder : public TPersistent
          {
          private:
            //рисовать или нет
            bool FEnabled;
            //Перо рамки
            Graphics::TPen *FPen;
           
            //для того, чтобы сигнализировать нашему компоненту об изменении
            //свойств TLabelBorder введем указатель на функцию и будем
            //вызывать эту функцию при изменении свойств
            TNotifyEvent FOnChange;
          protected:
           
            //методы-члены для записи свойств
            void __fastcall SetEnabled(bool Value);
            void __fastcall SetPen(Graphics::TPen *Value);
            void __fastcall Changed(TObject* Sender);
            
          public:
            __fastcall TLabelBorder();       //конструктор
            __fastcall virtual ~TLabelBorder(); //деструктор
           
            //метод присвоения значений свойств нашего объекта от другого объекта
              void __fastcall Assign(TPersistent* Value);
              
            __property TNotifyEvent OnChange = {read=FOnChange, write=FOnChange};
              
          __published:
            //эти свойства будут доступны в Object Inspector
            __property bool Enabled= {read=FEnabled, write=SetEnabled, nodefault};
            __property Graphics::TPen *Pen = {read=FPen, write=SetPen, nodefault};
          };
           
          //здесь определим тип обработчика - указатель на метод-обработчик события:
          typedef void __fastcall (__closure *TAnimTimerEvent)(TObject* Sender, int& CurPos);
          //модификатор __closure указывает на то, что это тип указателя на метод какого-либо класса
          //чтобы пользователь компонента мог поменять значение CurPos, объявляем этот параметр ссылкой
           
          class PACKAGE TMyLabel : public TCustomLabel
          ...
        В файл "MyLabel.cpp", например, перед строкой namespace Mylabel
        добавим реализацию методов класса TLabelBorder:
        Скрытый текст
        ExpandedWrap disabled
          //MyLabel.h
          ...
          //----  TLabelBorder  -------------------------------------------------------
          __fastcall TLabelBorder::TLabelBorder() : TPersistent()
          {
            FPen=new TPen;//создаем перо
            //чтобы LabelBorder, и значит MyLabel, реагировали
            //на изменение свойств пера, воспользуемся свойством TPen::OnChage
            FPen->OnChange=Changed;
           
            //значения по умолчанию:
            FOnChange=NULL;
            FEnabled=false;
          }
          //---------------------------------------------------------------------------
          __fastcall TLabelBorder::~TLabelBorder()
          {
            delete FPen; //не забудем удалить перо
          }
          //---------------------------------------------------------------------------
          void __fastcall TLabelBorder::Assign(TPersistent* Value)
          {
            TLabelBorder* lb=dynamic_cast<TLabelBorder*>(Value);
            if(lb)
            { //забираем значения свойств у другого LabelBorder
              FPen->Assign(lb->FPen);
              FEnabled=lb->FEnabled;
              Changed(this);//изменилось, вызываем
            }
            else//если Value не является указателем на TLabelBorder, то выбрасываем исключение
              throw Exception(AnsiString("Can't assign ")+Value->ClassName()+" to "+ClassName());
          }
          //---------------------------------------------------------------------------
          void __fastcall TLabelBorder::SetEnabled(bool Value)
          {
            if(FEnabled!=Value)
            {
              FEnabled=Value;
              Changed(this);//изменилось, вызываем
            }
          }
          //---------------------------------------------------------------------------
          void __fastcall TLabelBorder::SetPen(Graphics::TPen *Value)
          {
            if(Value->Width<=0 || Value->Width>5)
              Value->Width=FPen->Width;
            FPen->Assign(Value);
            Changed(this);//изменилось, вызываем
          }
          //---------------------------------------------------------------------------
          void __fastcall TLabelBorder::Changed(TObject* Sender)
          {
            if(FOnChange)//Если обработчик задан,
              FOnChange(Sender);//то вызываем его
          }
          //---------------------------------------------------------------------------
           
          namespace Mylabel
          {
          ...
        Добавим изменения в класс TMyLabel:
        Скрытый текст
        ExpandedWrap disabled
          //MyLabel.h
          ...
          class PACKAGE TMyLabel : public TCustomLabel
          {
            typedef TCustomLabel inherit;
          private:
            ...
            //указатель на объект, отвечающий за свойства рамки
            TLabelBorder *FLabelBorder;
            //указатель на метод-обработчик
            TAnimTimerEvent FOnAnimTimer;
          protected:
            ...
            //для отрисовки рамки переопределяем функцию Paint
            virtual void __fastcall Paint();
            //метод записи свойства TLabelBorder *LabelBorder
            void __fastcall SetLabelBorder(TLabelBorder *Value);
            //а в этой функции будем вызывать перерисовку TMyLabel
            void __fastcall Changed(TObject* Sender);
            //указатель же на нее будем передавать в TLabelBorder::OnChange
            //да и просто вызывать, если поменялись еще какие-нибудь свойства
            //(вместо Invalidate(), как раньше, раз уж появилась такая ф-ция)
          public:
            ...
          __published:
            ...
            //объявим наше свойство в секции __published
            __property TLabelBorder *LabelBorder = {read=FLabelBorder, write=SetLabelBorder, nodefault};
           
            //и не забудем свойство-обработчик; какое имя здесь зададим, такое имя
            //обработчика и будет видно на вкладке Events в Object Inspector'e
            __property TAnimTimerEvent OnAnimTimer = {read=FOnAnimTimer, write=FOnAnimTimer};
           
          __published:
            ...
           
            
          //MyLabel.cpp
          //---------------------------------------------------------------------------
          __fastcall TMyLabel::TMyLabel(TComponent* Owner)
          : TCustomLabel(Owner)
          {
            ...
            PopupMenu->Tag=(int)this;
           
            //теперь создаем объект свойств рамки
            FLabelBorder=new TLabelBorder;
            //и задаем обработчик его OnChange
            FLabelBorder->OnChange=Changed;
           
            //обнуляем указатель на метод-обработчик:
            FOnAnimTimer=NULL;
          }
          //---------------------------------------------------------------------------
          __fastcall TMyLabel::~TMyLabel()
          {
            delete FLabelBorder;
          }
          ...
          //---------------------------------------------------------------------------
          void __fastcall TMyLabel::TimerEvent(TObject* Sender)
          {
            if(!ComponentState.Contains(csDestroying))
            {
              //каждый раз увеличиваем счетчик, пока он не станет больше длины текста
              //но теперь еще и вызываем обработчик события, если он задан:
              ++CurPos;
              if(FOnAnimTimer)
                 FOnAnimTimer(this,CurPos);
              if(CurPos>Caption.Length() || CurPos<0)//проверим по-тщательней, а то мало ли что
                CurPos=0;//если больше длины текста, то устанавливаем его в 0
              Changed(this);//вместо Invalidate();
            }
          }
          //---------------------------------------------------------------------------
          void __fastcall TMyLabel::SetAnimInterval(int Value)
          {
            if(timer->Interval!=(Cardinal)Value)
            {
              timer->Interval=(Cardinal)Value;
              if(timer->Enabled)
                Changed(this);//вместо Invalidate();
            }
          }
          ...
          //---------------------------------------------------------------------------
          void __fastcall TMyLabel::SetAnimate(bool Value)
          {
            if(timer->Enabled!=Value)
            {
              timer->Enabled=Value;
              CurPos=0;
              
              //проверям, наша ли менюшка используется?
              if(PopupMenu && PopupMenu->Items->Count>0 && PopupMenu->Tag==(int)this)
              {//да наша. Надо переделать пункт:
                TMenuItem *mi=PopupMenu->Items->Items[0];
                if(Value)
                {
                  mi->Caption ="Disable animate";
                  //насколько я понимаю, память из-под предыдущего битмапа
                  //освобождается при смене Handle, поэтому беспокоиться об этом не нужно
                  mi->Bitmap->Handle=LoadImage(HInstance,"DISABLEBMP",IMAGE_BITMAP,0,0,LR_LOADMAP3DCOLORS);
                }
                else
                {
                  mi->Caption ="Enable animate";
                  mi->Bitmap->Handle=LoadImage(HInstance, "ENABLEBMP",IMAGE_BITMAP,0,0,LR_LOADMAP3DCOLORS);
                }
              }
              Changed(this);//вместо Invalidate();
            }
          }
          ...
          //---------------------------------------------------------------------------
          void __fastcall TMyLabel::SetLabelBorder(TLabelBorder *Value)
          {
            //"копируем" значения
            FLabelBorder->Assign(Value);
            Changed(this);
          }
          //---------------------------------------------------------------------------
          void __fastcall TMyLabel::Changed(TObject* Sender)
          {
            //на все изменения - перерисовываем компонент, точнее посылаем WM_PAINT
            Invalidate();
          }
          //---------------------------------------------------------------------------
          //а вот собственно перерисовка
          void __fastcall TMyLabel::Paint()
          {
            //сначала вызываем Paint базового класса - не рисовать же самим!
            inherit::Paint();
            //а теперь отрисовываем рамку, если нужно
            if(FLabelBorder && FLabelBorder->Enabled)
            {
              Canvas->Pen->Assign(FLabelBorder->Pen);
              Canvas->MoveTo(0,0);
              Canvas->LineTo(ClientRect.Right-Canvas->Pen->Width,0);
              Canvas->LineTo(ClientRect.Right-Canvas->Pen->Width,ClientRect.Bottom-Canvas->Pen->Width);
              Canvas->LineTo(0,ClientRect.Bottom-Canvas->Pen->Width);
              Canvas->LineTo(0,0);
            }
          }
          //---------------------------------------------------------------------------
           
          //----  TLabelBorder  -------------------------------------------------------
          ...
        В принципе TMyLabel с рамкой сделан. Только мы не можем вызвать редактор
        свойства LabelBorder (за отсутствием такового, да и ellipsis button не
        видать). Хотя редактировать поля этого свойства можно.

        Приступим к же созданию редактора свойств. Создаем новую форму
        (File->New Form), сохраняем новый unit (пусть "BorderEditor"),
        кидаем на форму пару кнопок (ОК и Cancel), checkbox (не знаю, как по-русски :( ) -
        для изменения свойства TLabelBorder::Enabled,
        и TrackBar (с вкладки Win32).- для редактирования свойства
        TLabelBorder::Pen->Width Больше я поленился, для демонстрации хватит и этого.
        Назначаем форме обработчик OnShow, а кнопке OK - OnClick.
        Вносим некоторые изменения в свойства контролов и формы,
        так что dfm-файл приобретает такой вид (BorderEditor.dfm):
        Скрытый текст
        ExpandedWrap disabled
          object MyLabelMenuDlg: TMyLabelMenuDlg
            Left = 498
            Top = 88
            BorderIcons = [biSystemMenu]
            BorderStyle = bsDialog
            Caption = 'Редактор границы'
            ClientHeight = 139
            ClientWidth = 262
            Color = clBtnFace
            Font.Charset = DEFAULT_CHARSET
            Font.Color = clWindowText
            Font.Height = -11
            Font.Name = 'MS Sans Serif'
            Font.Style = []
            OldCreateOrder = False
            Position = poOwnerFormCenter
            OnShow = FormShow
            PixelsPerInch = 96
            TextHeight = 13
            object Label1: TLabel
              Left = 184
              Top = 56
              Width = 47
              Height = 13
              Caption = 'Pen width'
            end
            object Bevel1: TBevel
              Left = 12
              Top = 95
              Width = 239
              Height = 2
              Anchors = [akLeft, akRight, akBottom]
            end
            object OkBtn: TButton
              Left = 87
              Top = 108
              Width = 75
              Height = 25
              Anchors = [akRight, akBottom]
              Caption = 'OK'
              ModalResult = 1
              TabOrder = 0
              OnClick = OkBtnClick
            end
            object CancelBtn: TButton
              Left = 175
              Top = 108
              Width = 75
              Height = 25
              Anchors = [akRight, akBottom]
              Cancel = True
              Caption = 'Cancel'
              ModalResult = 2
              TabOrder = 1
            end
            object EnableCB: TCheckBox
              Left = 16
              Top = 16
              Width = 225
              Height = 17
              Caption = '       Enable border'
              TabOrder = 2
            end
            object PenWidthTB: TTrackBar
              Left = 16
              Top = 56
              Width = 150
              Height = 30
              LineSize = 2
              Max = 5
              Min = 1
              Orientation = trHorizontal
              Frequency = 1
              Position = 1
              SelEnd = 0
              SelStart = 0
              TabOrder = 3
              ThumbLength = 12
              TickMarks = tmBottomRight
              TickStyle = tsAuto
            end
          end
        Вносим изменения в файл BorderEditor.h:
        Скрытый текст
        ExpandedWrap disabled
          //BorderEditor.h
          ...
          //---------------------------------------------------------------------------
          #include <DsgnIntf.hpp>
          #include "MyLabel.h"
          //---------------------------------------------------------------------------
          class TMyLabelMenuDlg : public TForm
          {
          ... //оставляем как есть
          };
          //удалили extern TMyLabelMenuDlg *MyLabelMenuDlg; - оно нам не понадобится
           
          //---------------------------------------------------------------------------
          //объявление класса редактора нашего свойства
          //TClassProperty - определённый в DSGNINTF.HPP наследник TPropertyEditor.
          //Он показывает название класса свойства в ObjectInspector'е и позволяет
          //раскрыть свойства класса, щелкнув на плюсик слева от имени свойства
          class PACKAGE TLabelBorderProperty : public TClassProperty
          {
          public:
          //эта функция вызывается из редактора компонента
            virtual void __fastcall Edit();
          //эта функция вызывается IDE, мы должны вернуть необходимые нам
          //атрибуты свойства из редактора компонента, а именно paDialog -
          //тогда Object Inspector создаст кнопку с тремя точками напротив имени свойства
          //и paSubProperties - тогда Object Inspector сообразит, что у нашего свойства есть
          //еще свои published-свойства  и поставить плюсик у имени свойства
            virtual TPropertyAttributes __fastcall GetAttributes(void);
           
          //конструктор-деструктор  
            __fastcall virtual ~TLabelBorderProperty(void);
            __fastcall TLabelBorderProperty(const _di_IFormDesigner ADesigner, int APropCount);
          };
           
          //переопределяем класс редактора компонента
          class PACKAGE TMyLabelEditor : public TDefaultEditor
          {
          protected:
          //эта функция вызывается из IDE при необходимости редактировать значения свойства
          //в нашем случае - при нажатии кнопки [...] у свойства в Object Inspector
            virtual void __fastcall EditProperty(TPropertyEditor* PropertyEditor,
                                                   bool& Continue, bool& FreeEditor);
          public:
            //Следующие три функции используются для добавления своих пунктов в сплывающее меню
            //при клике правой клавишей мышки на компоненте
           
            //мы должны возвратить из этой функции количество добавляемых пунктов
            //пусть у нас будет всего 1 пункт - редактирование нашего свойства
            virtual int __fastcall GetVerbCount(void);
            
            //мы должны возвратить из этой функции заголовок пункт с номером Index
            virtual AnsiString __fastcall GetVerb(int Index);
            
            //вызывается при выборе пункта с номером Index
            virtual void __fastcall ExecuteVerb(int Index);
           
            //ну и переопределим еще одну функцию, для красоты,
            //которая "подготавливает" пункт с номером Index перед показом в меню
            //Мы добавим картинку к нашему пункту - ту же самую, что использует
            //наш компонент
            virtual void __fastcall PrepareItem(int Index, const TMenuItem *AItem);
            
            __fastcall virtual TMyLabelEditor(TComponent* AComponent, _di_IFormDesigner ADesigner);
            __fastcall virtual ~TMyLabelEditor(void);
          };
          #endif
        "Редактором компонента" наследники от TDefaultEditor называются только
        потому (как я это понял), что они позволяют вывесить диалог редактирования
        свойств(а) компонента при двойном клике по компоненту.
        Вносим изменения в файл BorderEditor.cpp:
        Скрытый текст
        ExpandedWrap disabled
          //BorderEditor.cpp
          ...
          //-------  TMyLabelMenuDlg  -------------------------------------------------
          //пустой конструктор диалога
          __fastcall TMyLabelMenuDlg::TMyLabelMenuDlg(TComponent* Owner) : TForm(Owner){}
          //---------------------------------------------------------------------------
          //Обработчик OnShow диалога
          void __fastcall TMyLabelMenuDlg::FormShow(TObject *Sender)
          {
            //задаем диалогу нашу иконку
            SendMessage(Handle,WM_SETICON,ICON_SMALL,(LPARAM)LoadIcon(HInstance,"MYLABELICO"));
            //устанавливаем значения для CheckBox'a и TrackBar'a
            EnableCB->Checked=LabelBorder->Enabled;
            PenWidthTB->Position=LabelBorder->Pen->Width;
          }
          //---------------------------------------------------------------------------
          void __fastcall TMyLabelMenuDlg::OkBtnClick(TObject *Sender)
          {//если нажата кнопка OK, то присваиваем новые значения
            LabelBorder->Enabled=EnableCB->Checked;
            LabelBorder->Pen->Width=PenWidthTB->Position;
          }
          //---------------------------------------------------------------------------
          //------------- TLabelBorderProperty  ---------------------------------------
          __fastcall TLabelBorderProperty::TLabelBorderProperty(const _di_IFormDesigner ADesigner,
                  int APropCount) : TClassProperty(ADesigner, APropCount) { }
          __fastcall TLabelBorderProperty::~TLabelBorderProperty(void) { }
          //---------------------------------------------------------------------------
          void __fastcall TLabelBorderProperty::Edit()
          {
            TMyLabelMenuDlg *prop_dlg=new TMyLabelMenuDlg(Application);
            try
            {
              //вытаскиваем указатель на адрес объекта с нашим свойством для компонета
              prop_dlg->LabelBorder = (TLabelBorder*)GetOrdValue();
              //если редактируемых свойств больше одного, то надо использовать
              //метод GetOrdValueAt(int Index);
              prop_dlg->ShowModal();
            }catch(Exception& ex)
            {
              ShowMessage(ex.Message);
            }
            delete prop_dlg;
          }
          //---------------------------------------------------------------------------
          TPropertyAttributes __fastcall TLabelBorderProperty::GetAttributes()
          {
            return (TPropertyAttributes() << paDialog << paSubProperties);
          }
          //---------------------------------------------------------------------------
          //-------------  TMyLabelEditor  --------------------------------------------
          __fastcall TMyLabelEditor::TMyLabelEditor(TComponent* AComponent, _di_IFormDesigner ADesigner)
                                : TDefaultEditor(AComponent, ADesigner) { }
          __fastcall TMyLabelEditor::~TMyLabelEditor(void) { }
           
          void __fastcall TMyLabelEditor::EditProperty(TPropertyEditor* PropertyEditor,
                                           bool& Continue,   bool& /*FreeEditor*/ )
          {
            //проверяем, что вызывается на ковёр именно наше свойство
            if (PropertyEditor->GetName()=="LabelBorder")
            {//"LabelBorder" - соответствует свойству нашего компонента TMyLabel::LabelBorder
              PropertyEditor->Edit();//редактируем
           
              //при попытке редактировать какое-либо свойство, эта функция вызывается
              //методом  TDefaultEditor::Edit() в поиске "подходящего" редактора
              //если он найден, как сейчас, то надо сделать:
              Continue = false;
            }//Если бы мы создавали редакторы для нескольких свойств
              //то это подходящее место, чтобы вызвать остальные (по соответствию имени)
          }
          //---------------------------------------------------------------------------
          int __fastcall TMyLabelEditor::GetVerbCount()
          {
            return 1; //всего один пункт меню
          }
          //---------------------------------------------------------------------------
          AnsiString __fastcall TMyLabelEditor::GetVerb(Integer Index)
          {
            if (Index == 0)
              return "Редактировать границу";//заголовок нашего меню
            return EmptyStr;
          }
          //---------------------------------------------------------------------------
          void __fastcall TMyLabelEditor::ExecuteVerb(Integer Index)
          {
            if (Index == 0) Edit();//запускаем поиск подходящего редактора свойств
          }
          //---------------------------------------------------------------------------
          void __fastcall TMyLabelEditor::PrepareItem(int Index, const TMenuItem *AItem)
          {
            //а здесь загрузим нашу картинку
            if(Index==0)
            {
          #pragma warn -8037 //на время отключаем предупреждение о применении неconst функции к const объекту
              if(!AItem->Bitmap)
                AItem->Bitmap=new Graphics::TBitmap;
              AItem->Bitmap->LoadFromResourceName((DWORD)HInstance,"TMYLABEL");
          #pragma warn +8037
            }
          }
          //---------------------------------------------------------------------------
        Ну вот и всё. Почти...
        Добавим кое-что в функцию Register() из файла MyLabel.cpp, а
        именно регистрацию редакторов свойства и компонента:
        Скрытый текст
        ExpandedWrap disabled
          //MyLabel.cpp
           
          #include "BorderEditor.h" //не забудем подключить
          ...
          namespace Mylabel
          {
           
            void __fastcall PACKAGE Register()
            {
              TComponentClass classes[1] = {__classid(TMyLabel)};
              //пусть компонент будет расположен на вкладке с именем "My Page"
              //регистрируем наш компонент
              RegisterComponents("My Page", classes, 0);
              //если бы мы хотели зарегистрировать не один, а больше компонентов (пусть N)
              //то массив classes надо было объявить как  classes[N], а значение последнего
              //параметра ф-ции RegisterComponents должно было быть N-1  (индекс последнего элемента
              //массива classes[N])
           
              
              //регистрируем наш редактор объекта
              RegisterComponentEditor(__classid(TMyLabel), __classid(TMyLabelEditor));
              //регистрируем наш редактор свойства
              RegisterPropertyEditor(__typeinfo(TLabelBorder),
                                   NULL,
                                   "",
                                   __classid(TLabelBorderProperty));
            }
          }
        Да, последний штрих - мы же используем в диалоге иконку из ресурса,
        так ведь надо же ее туда вставить:
        ExpandedWrap disabled
          //MyLabel.rc
          ...
          MYLABELICO ICON "mylabel.ico"

        И не забыть нарисовать эту иконку (16х16) и сохранить в файле с именем mylabel.ico!
        Рисовать можно все в том же Tools->Image Editor.

        Ну вот теперь действительно - всё: компонент готов, собираем библиотеку, и ...
        правильно - в тестовый проект, тестировать, тестировать и еще раз тестировать.
        Прикреплённый файлПрикреплённый файлPackage3.zip (23.03 Кбайт, скачиваний: 404)
          И так, прошел месяц. Судя по числу сообщений, статья очень заинтересовала почтенейшуюю публику. <_<
          trainer, оформлять статью или ну её?
            Небольшое дополнение к статье.
            Заметил небольшой изъян - если вызывать редактор свойства через редактор компонента
            (двойным щелчком на компоненте или через созданный пункт popup-меню), то значения
            свойства обновляются в Object Inspector'е не сразу при выходе из диалога редактирования,
            а только после выделения строки со значением свойства в этом самом Object Inspector'е - не
            очень симпатично получается. Для того, что бы избавиться от этого эффекта, можно
            воспользоваться свойством TComponentEditor::Designer (тип _di_IFormDesigner) -
            указатель на интерфейс дизайнера IDE Билдера. Внесем небольшое изменение в код метода
            TLabelBorderProperty::Edit() (файл BorderEditor.cpp):
            Скрытый текст
            ExpandedWrap disabled
              void __fastcall TLabelBorderProperty::Edit()
              {
                TMyLabelMenuDlg *prop_dlg=new TMyLabelMenuDlg(Application);
                try
                {
                  //вытаскиваем указатель на адрес объекта с нашим своством для компонета
                  prop_dlg->LabelBorder = (TLabelBorder*)GetOrdValue();
                  //если редактируемых свойств больше одного, то надо использовать
                  //метод GetOrdValueAt(int Index);
                  if(prop_dlg->ShowModal()==mrOk && Designer)
                    Designer->Modified();//метод _di_IFormDesigner::Modified() как раз и "извещает" Дизайнер об изменении значений
                }catch(Exception& ex)
                {
                  ShowMessage(ex.Message);
                }
                delete prop_dlg;
              }
            Также иногда хочется устроить нечто вроде "Preview" - чтобы при изменении значения
            в диалоге редактирования, это изменение сразу отображалось на компоненте. Добавим в класс диалога
            TMyLabelMenuDlg член _di_IFormDesigner IDEDesigner (в public-секцию), и так же надо создать обработчик события в диалоге,
            например, для ширины рамки - TTrackBar::OnChange, а для влючения/отключения рисования рамки - TCheckBox::OnClick,
            и воспользоваться указателем на объект свойства TMyLabelMenuDlg::LabelBorder, вытащенный
            нами в методе TLabelBorderProperty::Edit():
            Скрытый текст
            ExpandedWrap disabled
              void __fastcall TMyLabelMenuDlg::PenWidthTBChange(TObject *Sender)
              {
                LabelBorder->Pen->Width=PenWidthTB->Position;
                //Для "Preview" этого достаточно, но,
                //чтобы изменение отобразилось и в Object Inspector'e:
                if(IDEDesigner)//IDEDesigner мы должны проинициализировать в методе TLabelBorderProperty::Edit
                  IDEDesigner->Modified();
              }
              //---------------------------------------------------------------------------
               
              void __fastcall TMyLabelMenuDlg::EnableCBClick(TObject *Sender)
              {
                LabelBorder->Enabled=EnableCB->Checked;
                //Для "Preview" этого достаточно, но,
                //чтобы изменение отобразилось и в Object Inspector'e:
                if(IDEDesigner)//IDEDesigner мы должны проинициализировать в методе TLabelBorderProperty::Edit
                  IDEDesigner->Modified();
              }
            Подправим код метода TLabelBorderProperty::Edit() (файл BorderEditor.cpp), чтобы
            передать в форму диалога ссылку на дизайнер IDE, да и чтобы восстановить значения свойства,
            если пользователь компонента отменит изменения:
            Скрытый текст
            ExpandedWrap disabled
              void __fastcall TLabelBorderProperty::Edit()
              {
                TMyLabelMenuDlg *prop_dlg=new TMyLabelMenuDlg(Application);
                //создаем временный объект для хранения значений свойства
                TLabelBorder *tmpBorder=new TLabelBorder;
                try
                {
                  //вытаскиваем указатель на адрес объекта с нашим своством для компонета
                  prop_dlg->LabelBorder = (TLabelBorder*)GetOrdValue();
                  //если редактируемых свойств больше одного, то надо использовать
                  //метод GetOrdValueAt(int Index);
                  prop_dlg->IDEDesigner=Designer;//"передадим" диалогу указатель на интерфейс дизайнера IDE
                  tmpBorder->Assign(prop_dlg->LabelBorder);//"запомним" значения свойства
                  if(prop_dlg->ShowModal()!=mrOk)
                    prop_dlg->LabelBorder->Assign(tmpBorder);//восстановим значения свойства, если не была нажата кнопка "ОК"
                  if(Designer)
                    Designer->Modified();
                }catch(Exception& ex)
                {
                  ShowMessage(ex.Message);
                }
                delete tmpBorder;//не забудем освободить память
                delete prop_dlg;
              }
              Спосибо, статья то шо надо! Час на разбор полётов - и можно уже свою задачу решать! :D
              0 пользователей читают эту тему (0 гостей и 0 скрытых пользователей)
              0 пользователей:


              Рейтинг@Mail.ru
              [ Script execution time: 0,0609 ]   [ 15 queries used ]   [ Generated: 1.05.24, 23:01 GMT ]