Наши проекты:
Журнал · Discuz!ML · Wiki · DRKB · Помощь проекту |
||
ПРАВИЛА | FAQ | Помощь | Поиск | Участники | Календарь | Избранное | RSS |
[3.145.93.221] |
|
Сообщ.
#1
,
|
|
|
Скрытый текст Все исходники, приведённые в этой статье, распространяются по 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, а можно и руками. Вот какие изменения надо внести: Скрытый текст //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", а в модуле основной формы тестового приложения: Скрытый текст //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) |
Сообщ.
#2
,
|
|
|
Краткое отступление о комплекте поставки.
Если не ставить задачу сокрытия исходников компонента, то комплект поставки - в прилагаемом архиве (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. Для этого добавим в объявление класса нашего компонента всего одну функцию и одну директиву в соответствующий заголовочный файл: Скрытый текст //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 //--------------------------------------------------------------------------- __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; } ... зеленая галочка, а второй - красный крестик, сохраняем их под именами "enable.bmp" и "disable.bmp" соответственно. Добавляем две строки в файл "MyLabel.rc": Скрытый текст //MyLabel.rc ... ENABLEBMP BITMAP "enable.bmp" DISABLEBMP BITMAP "disable.bmp" опциями линкера. Измененный исходник: Прикреплённый файлPackage2.zip (11.29 Кбайт, скачиваний: 501) |
Сообщ.
#3
,
|
|
|
Ну а теперь займемся еще более интересными вещами. В литературе часто опускается
внесение в компонент обработчика какого-либо события, а также создание собственного редактора свойств(а) компонента. Добавим к нашему компоненту событие - например, возникающее при срабатывании нашего таймера анимации. Пусть пользователь компонента имеет возможность назначить обработчик и в нем узнать "текущую позицию" анимации, и если захочет, поменять ее. Добавим к компоненту еще и какое-нибудь "хитрое" свойство и свой редактор этого свойства - диалоговое окно, появляющееся в Design-time при клике на кнопку с тремя точками (ellipsis button) в правой части строки свойства в Object Inspector. За это отвечает так называемый "Редактор свойств" (Property Editor) Пусть еще во всплывающем меню при клике правой клавишей мыши на нашем компоненте в Design-Time добавляется наш пункт меню с вызовом этого же диалога - ответственный за это действие - "Редактор компонентов" (Component Editor; по умолчанию, т.е. без определения собственного, за это отвечает класс TDefaultEditor) У нашего TMyLabel есть недостаток, присутствующий и у TLabel - отсутствие возможности задания рамки по периметру. Давайте будем рисовать её, а для управления этим рисованием создадим новый класс, который будет управлять этим рисованием; пусть это будет наследник от класса TPersistent. Вставим его определение в файл "MyLabel.h" перед определением класса нашего компонента - TMyLabel: Скрытый текст //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 ... добавим реализацию методов класса TLabelBorder: Скрытый текст //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 { ... Скрытый текст //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 ------------------------------------------------------- ... свойства LabelBorder (за отсутствием такового, да и ellipsis button не видать). Хотя редактировать поля этого свойства можно. Приступим к же созданию редактора свойств. Создаем новую форму (File->New Form), сохраняем новый unit (пусть "BorderEditor"), кидаем на форму пару кнопок (ОК и Cancel), checkbox (не знаю, как по-русски ) - для изменения свойства TLabelBorder::Enabled, и TrackBar (с вкладки Win32).- для редактирования свойства TLabelBorder::Pen->Width Больше я поленился, для демонстрации хватит и этого. Назначаем форме обработчик OnShow, а кнопке OK - OnClick. Вносим некоторые изменения в свойства контролов и формы, так что dfm-файл приобретает такой вид (BorderEditor.dfm): Скрытый текст 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 ... //--------------------------------------------------------------------------- #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 потому (как я это понял), что они позволяют вывесить диалог редактирования свойств(а) компонента при двойном клике по компоненту. Вносим изменения в файл BorderEditor.cpp: Скрытый текст //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, а именно регистрацию редакторов свойства и компонента: Скрытый текст //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)); } } так ведь надо же ее туда вставить: //MyLabel.rc ... MYLABELICO ICON "mylabel.ico" И не забыть нарисовать эту иконку (16х16) и сохранить в файле с именем mylabel.ico! Рисовать можно все в том же Tools->Image Editor. Ну вот теперь действительно - всё: компонент готов, собираем библиотеку, и ... правильно - в тестовый проект, тестировать, тестировать и еще раз тестировать. Прикреплённый файлPackage3.zip (23.03 Кбайт, скачиваний: 404) |
Сообщ.
#4
,
|
|
|
И так, прошел месяц. Судя по числу сообщений, статья очень заинтересовала почтенейшуюю публику.
trainer, оформлять статью или ну её? |
Сообщ.
#5
,
|
|
|
Небольшое дополнение к статье.
Заметил небольшой изъян - если вызывать редактор свойства через редактор компонента (двойным щелчком на компоненте или через созданный пункт popup-меню), то значения свойства обновляются в Object Inspector'е не сразу при выходе из диалога редактирования, а только после выделения строки со значением свойства в этом самом Object Inspector'е - не очень симпатично получается. Для того, что бы избавиться от этого эффекта, можно воспользоваться свойством TComponentEditor::Designer (тип _di_IFormDesigner) - указатель на интерфейс дизайнера IDE Билдера. Внесем небольшое изменение в код метода TLabelBorderProperty::Edit() (файл BorderEditor.cpp): Скрытый текст 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; } в диалоге редактирования, это изменение сразу отображалось на компоненте. Добавим в класс диалога TMyLabelMenuDlg член _di_IFormDesigner IDEDesigner (в public-секцию), и так же надо создать обработчик события в диалоге, например, для ширины рамки - TTrackBar::OnChange, а для влючения/отключения рисования рамки - TCheckBox::OnClick, и воспользоваться указателем на объект свойства TMyLabelMenuDlg::LabelBorder, вытащенный нами в методе TLabelBorderProperty::Edit(): Скрытый текст 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(); } передать в форму диалога ссылку на дизайнер IDE, да и чтобы восстановить значения свойства, если пользователь компонента отменит изменения: Скрытый текст 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; } |
Сообщ.
#6
,
|
|
|
Спосибо, статья то шо надо! Час на разбор полётов - и можно уже свою задачу решать!
|