Главная » C++, C++ Builder » Компонент AngleText (повернутый текст) в CBuilder

0

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

Для этого первого компонента мы разберем весь процесс от начала до конца, чтобы у вас появилось четкое представление о том, как это все происходит в среде CBuilder. В разговорах о последующих компонентах я буду опускать детали того или иного шага разработки, рекомендуя вам обратиться к этой части, если вам потребуются более подробные разъяснения. Как вы увидите в дальнейшем, процесс написания компонента остается одним и тем же, вне зависимости от типа компонента.

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

должен делать и как. То есть сформулировать проблему. Так зачем же мы создаем компонент? В случае компонента AngleText проблема, которую мы попытаемся разрешить, состоит в отображении вертикальной строки текста для подписи под осью ординат (Y). Для разрешения этой проблемы мы должны повернуть строку на 90 градусов от горизонтали. Это и будет частным решением.

Итак, частное решение проблемы состоит в том, чтобы поворачивать текст на некий угол при отображении. Это не трудно сделать (собственно, мы это уже делали, когда говорили об использовании модулей Delphi в наших приложениях), манипулируя объектом шрифт (font), присвоенным полю Canvas. В данном случае мы создадим у компонента собственное свойство Canvas, чтобы можно было работать с ним, а не с полем формы, на которой он расположен.

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

Как и стадия проработки проблемы, стадия проектирования имеет лишь одно решение, которое в данном случае является  частным.  Это решение ставит перед нами две большие  проблемы.  Во- первых, мы должны определить, какими свойствами будет обладать наш компонент.  В зависимости от этого нам надо будет принять второе решение — от какого  класса VCL будет наследовать наш компонент.

Выбор свойств

Компоненты обладают двумя видами свойств. Первый — это свойства, унаследованные от класса базового компонента и представленные в наследнике. Эти свойства предоставляют базовые возможности, основанные на базовом классе компонента VCL; вы сможете их использовать без труда, выбрав подходящий базовый класс. Для компонента AngleText нам бы тоже хотелось получить легкий доступ к некоторым свойствам для использования их в компоненте. Второй вид свойств — это новые свойства, которые мы определили специально для этого компонента. Эти свойства уникальны для компонента и не зависят от выбора базового класса.

Когда вы создаете компонент, вы наследуете его от базового класса VCL. Этот выбор базового класса влияет на свойства, доступные вам при написании компонента. Все компоненты должны по крайней мере наследовать от TComponent, базового класса всех компонентов в VCL, который не предоставляет почти что никаких возможностей. В качестве альтернативы вы можете выбрать базовый класс компонента, который будет делать большую часть работы за вас. От выбора класса зависит, какую часть работы будете делать вы и какая часть будет сделана за вас. Как правило, вы будете выбирать класс самого высокого уровня, который удовлетворяет критериям вашего компонента. То есть вы не будете наследовать от класса комбинированного списка, если хотите написать новую кнопку. Вы, скорее всего, выберите класс TButton.

Итак, все, о чем мы говорили, сводится к следующему — для того, чтобы определить свойства для класса, надо сначала выбрать базовый класс компонента. Но для того, чтобы выбрать базовый класс компонента, надо сначала понять, какими же собственно свойствами должен обладать наш компонент. Похоже на замкнутый круг, не правда ли? Не волнуйтесь — все не так плохо, как кажется. Выбрать класс, от которого наш компонент будет наследовать, куда проще, чем кажется на первый взгляд. Если вы хотите создать визуальный компонент, который должен будет сам себя отрисовывать, вам надо наследовать от одного из классов TCustomxxx. Если вы работаете над компонентом, который представляет из себя кнопку, но с некоторыми изменениями (например, кнопку, у которой событие  нажатия повторяется,  если кнопка мыши остается  нажатой на этой кнопке в течение определенного промежутка  времени — то есть как на клавиатуре), вам стоит

наследовать от TCustomButton.

В случае класса TAngleText нас интересуют базовые возможности управляющих элементов, которые включают в себя наличие свойств для текста, поля, шрифта, цвета и т. п. Следовательно, базовым для класса TAngleText станет класс TCustomControl. Этот базовый класс предоставит нам все базовые свойства управляющих элементов, которые нам потребуются.

Давайте вернемся к определению того, какие же свойства нам надо предоставить компоненту TAngleText. Прежде всего, нам нужно свойство для текста (Text), который мы собираемся поворачивать. Свойство Text — одно из базовых свойств, принадлежащих классу TCustomControl, так что здесь никакой работы для нас нет. Все, что нам надо, это представить это свойство (позже мы рассмотрим, как это сделать за пару секунд), и оно будет работать, как обещано. Следующее очевидное свойство, которое нам надо определить, это угол (Angle), на который будет повернут текст. Это свойство мы назовем Angle.

Разобравшись с двумя простейшими свойствами, давайте посмотрим, какие еще свойства нам надо определить для нашего компонента. Все компоненты VCL автоматически имеют свойства для обозначения позиции и родителя. Это свойства Left (левый отступ), Top (верхний отступ), Heigh (высота), Width (ширина) и Parent (родитель). Ни одно из этих свойств вам, разработчику компонентов, определять уже не надо.

Первое, о чем следует задуматься — то, каким образом рисуется строка под углом. Для этого нам нужны три вещи. Во-первых, отображаемая строка. Это свойство Text  нашего компонента. Во- вторых, нужен угол, на который строка будет повернута. Это уже определенное нами свойство Angle. И, наконец, нам нужна позиция, в которой будет отображаться строка, то есть базовая точка нашей строки. Мы могли бы определить эту точку автоматически, но это не самое лучшее решение. Некоторые пользователи могут пожелать, чтобы строка появлялась отцентрованной по вертикали, другие захотят отобразить ее вверху компонента, третьи — внизу. Вместо того, чтобы самим решать это за всех, мы предоставим конечному пользователю (то есть программисту, который будет использовать наш компонент) определение базовой точки. С точкой как таковой работать тяжело, поэтому мы предоставим два свойства — X-координату и Y-координату базовой точки.

Следующим после базовой точки аспектом, который нам надо рассмотреть, станет собственно отрисовка компонента. Для того, чтобы изменять отображение компонента, нам нужен шрифт для отображения. Мы могли бы просто использовать родительский шрифт, но это было бы неоправданным ограничением свободы пользователя. Поэтому мы представим свойство Font класса TCustomControl для конечного пользователя. Точно так же мы должны предоставить пользователю возможность изменять цвет фона компонента, чтобы компонент мог, если в  этом есть необходимость, выделяться на форме, в которой расположен. Для этого мы представим свойство Color. Итак, на данный момент у нас есть шесть свойств, выбранных для компонента. Три из них предоставляет базовый класс TCustomControl (Font, Text и Color), а три должны быть воплощены нашим компонентом (Angle, XPos и YPos). Настало время перейти к собственно кодированию.

Воплощение нового компонента

CBuilder — не такой уж великий помощник в создании компонентов. Мастер компонентов (Component Wizard) может быть использован для создания самого простейшего скелета компонента, но после этого ничем помочь уже не может. Мы сделаем кое-что подобное самостоятельно чуть позже, а пока собственно создание компонента принесет  вам  кое-какую пользу и покажет, для чего и как служат отдельные части.

Мастер компонентов CBuilder служит для создания скелета  компонента в CBuilder. Вы можете предположить, что подобный инструмент должен находиться в меню Tools (инструменты), но это не так. Выберите Component|New в главном меню CBuilder, и увидите окно Мастера компонентов, показанное на рис. 14.1. Это простейшее окно позволит вам определить имя компонента, базовый класс компонента и страницу палитры, в которой вы будете отображать компонент. Вот и все, что вы можете определить в Мастере компонентов. В нем нет полей ввода ни для свойств, которые вы хотите добавить в компонент, ни для добавляемых методов, ни для событий, которые должны обрабатываться. Все это вам придется определять самим, но  не пугайтесь — мы  шаг за шагом проделаем весь процесс в этой главе.

Рис. 14.1. Мастер компонентов CBuilder

Для нашего примера введите имя компонента TAngleText. Выберите компонент TCustomControl в качестве базового класса компонента и оставьте предлагаемую по умолчанию страницу Samples в поле выбора страницы палитры. Нажмите кнопку OK, и компонент будет автоматически сгенерирован и добавлен в ваш проект. Это весьма полезно, так как дает нам возможность протестировать компонент прямо в проекте до того, как он будет сынсталлирован в системе. А отлаживать и тестировать компонент в проекте гораздо проще, чем делать это после того, как он сынсталлирован.

Добавление  родительских свойств

Добавление свойств в компонент это первый и очень важный шаг в его воплощении. Существует два способа добавления свойств в компонент, каждый относится к одному из двух типов свойств, которые в нем присутствуют. Во-первых, вы можете добавлять свои собственные свойства, что мы и проделаем для свойств Angle, XPos и YPos. Когда вы определяете свои собственные свойства, вы несете ответственность за определение их типа, а также возможностям чтения и записи в них. Со вторым видом свойств, родительскими (или предопределенные — predefined) свойствами, работать куда проще. Давайте и начнем с добавления более простых, родительских, свойств.

В заголовочном файле вашего компонента вы найдете строку __published. Все пункты, находящиеся в этой секции, будут отражены в Object Inspector среды CBuilder, когда компонент будет выделен на форме в окне редактора форм. Если вы добавляете описание вне этой секции, свойство будет доступно программисту во время исполнения (конечно, если оно будет находиться в секции public заголовочного файла), но не будет отображаться во время проектирования. Свойства, располагающиеся вне секции published, известны как свойства, доступные только во время исполнения (runtime-only).

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

случая  надо  добавить  те  три  родительских  свойства,  о  которых  мы  говорили,  в  заголовочный файл:

__published:

__property Text;

__property Font;

__property Color;

В данном случае мы не изменяем никаких частей свойства, но это допускается — вы можете изменять в родительском свойстве все, что угодно, кроме его типа. Изменения могут затронуть функцию read (чтение),  write (запись)  и значение, задаваемое по умолчанию.  Мы позже кратко рассмотрим, как это делается.

Добавление новых свойств

После того, как мы добавили родительские свойства в компонент, следующим шагом станет добавление специфических свойств нашего компонента. Давайте сначала  разберемся  с изменениями, которые надо внести в заголовочный файл, а потом займемся воплощением (implementation). Следующие строки добавьте  в  секцию  published  заголовочного  файла  для класса компонента TAngleText:

__published:

__property double Angle={read=FAngle, write=FAngle, default=0};

__property int         XPos={read=FXPos, write=SetXPos};

__property int         YPos={read=FYPos, write=SetYPos};

В данном случае приведенные выше описания свойств определяют новые свойства  для класса компонента, которые будут доступны во время проектирования. Свойство Angle определено как имеющее тип double. В общем виде формат выражения __property имеет следующий вид:

__property <тип> <ИмяСвойства>={[read=

<ФункцияЧтенияИлиЗначение>] [,write=<ФункцияЗаписиИлиЗначение>][,default=<значение>]};

где <тип> — допустимый тип C++ для этого свойства. Обычно типом свойства является один из базовых типов C++, такой, как short, long, int и т. п.

<ИмяСвойства> — имя свойства; под этим именем свойство появится и в Object Inspector.

<ФункцияЧтенияИлиЗначение> — это либо функция, которая будет использоваться для чтения значения свойства, или само значение. Мы остановимся на этом чуть позже.

<ФункцияЗаписиИлиЗначение> — то же самое, что и функция чтения, но относится к

изменению значения свойства.

<значение> — значения компонента по умолчанию, отображаемое в Object Inspector. Отметьте, что значения по умолчанию НЕ устанавливают собственно свойство компонента, они только отображаются в Object Inspector во время проектирования.

Так что же, самом деле, представляют из себя функции чтения (Read) и записи (Write)? Здесь мы подошли к самому критическому различию между свойством и переменной-членом класса (member variable). Несмотря на то, что в вашем коде, использующем компонент, свойства проявляют себя как простые переменные-члены класса, они, на самом деле, представляют из себя нечто большее. Переменные-члены класса просто позволяют пользователю присваивать значения свойствам компонента и модифицировать их. Вы можете не разрешить им напрямую присваивать

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

myComponent->SetArrayElement(3,12);

если мне куда больше нравится такая запись: myComponent->Array[3] = 12;

Первое значение (3) — это номер элемента массива или его значение? Вы не сможете сказать этого наверняка, не сверившись с документацией. Для случая с двумерным массивом дела обстоят и того хуже:

myComponent->Set2Darray(1,2,12);

Опять-таки, что здесь номер строки? Что здесь номер столбца? Что является присваеваемым значением? Представьте, что вместо этого можно написать просто:

myComponent->Array[1][2] = 12;

Не кажется ли вам, что такая запись повлечет за собой куда меньше ошибок? Ну конечно это так. Используя функции Read и Write подобные вещи пишутся проще. Кроме того, если вы не хотите, чтобы пользователь мог изменять значение свойства компонента, вы просто опускаете функцию Write, и пользователь не сможет его изменить. Не кажется ли вам, что это гораздо более приятно, чем беспокоиться о функциях доступа, использующих Set (установить) и Get (получить)? В качестве примечания замечу, что вы на самом деле можете опустить функцию Read, оставив пользователю возможность изменять значение свойства посредством функции Write. Не зная уж, зачем вам может понадобиться предоставление пользователю этакого черного ящика, но возможность такая есть.

Замечание

Значение по умолчанию может быть определено при помощи предложения default= в выражении, определяющем свойство (то есть выражении __property). Но это не установит значение свойства в значение, указанное в этом предложении, а только поместит его в  Object Inspector, когда ваш компонент будет впервые создан. Если вы хотите инициализировать значение при помощи предложения default=, вам придется установить значение свойства в конструкторе класса.

Я надеюсь, вы поняли, почему следует использовать функции Read и Write в вашем компоненте, но теперь встает вопрос — как их использовать? Если вас не волнует то, какое значение пользователь установит для свойства (как это не волнует нас в случае свойства Angle), то вы можете просто присвоить самой переменной значение функции Read  или  Write.  Для  свойства Angle мы так и поступим. В случае же, если вы хотите каким-то образом фильтровать вводимые значения, вы присваиваете функцию-член класса нашей функции. Давайте пока добавим в класс компонента переменные для свойств, чтобы вам было проще воспринимать происходящее. В секцию приватных объявлений (private) заголовочного файла добавьте следующие строки:

private:

double FAngle; int FXPos;

int FYPos;

Эти переменные-члены класса — не более чем нормальные переменные C++, к которым вы, наверное, привыкли, работая с классами C++. У конечного пользователя (программиста) нет к ним

прямого доступа. Является общепринятым использование префикса F при работе с переменными, которые представляют свойства компонента. Это наследие оригиналов компонентов Delphi, но тем не менее весьма полезное для использования соглашение.

Эти переменные вы будете использовать в своем коде. С другой стороны, свойства будут напрямую использоваться конечным пользователем. Как это совмещается? В случае прямых свойств, таких, как Angle, функции Read и Write определяют, что когда пользователь изменяет значение свойства, написав следующую строку кода:

pAngleText->Angle = 90.0;

то этот код автоматически присваивает переменной-члену класса FAngle значение 90.0. Это происходит без вашего участия при посредстве базового класса и компилятора С++, встроенного в CBuilder. В то же время, когда программист пишет строку кода следующего содержания:

pAngleText->XPos = 100;

происходит нечто совершенно другое. В этом случае вызывается функция компонента Write. Если вы помните, свойство XPos использовало функцию, названную SetXPos для установки значений. Когда пользователь пытается записать значение в свойство, называемое XPos, значение преобразуется в вызов  функции. Вам надо дописать две строки, содержащие прототипы таких вызовов функций, в заголовочный файл. Итак, добавьте следующие две строки в секцию protected заголовочного файла:

virtual void     fastcall SetXPos(int XPos ); virtual void     fastcall SetYPos(int YPos );

Когда вы пишете функцию Set (или Write), в эту функцию должен передаваться один параметр. Из этого правила бывают исключения, например, для случая, когда свойство представляет собой массив, но эти варианты мы рассмотрим чуть позже в этой главе.

Обратите внимание на использование модификатора __fastcall для функций. Все функции свойств Read и Write должны использовать модификатор fastcall.  Если  вы  его  не  используете,  то  в лучшем случае будут происходить странные вещи, а в худшем — среда выдаст исключительную ситуацию. Итак, не забудьте __fastcall.

Воплощение этих функций до удивительного несложно. Все, что вам надо, это определить, какие данные вы разрешите передавать в переменные-члены класса, а какие нет. Это оставляется полностью на ваше усмотрение. Хоть вы и не можете возвратить ошибку из методов Read и Write, можно сгенерировать исключительную ситуацию в случае, если данные имеют недопустимые значения. Кроме тех случаев, когда некорректные данные будут в состоянии вызвать серьезный сбой программы, я бы не советовал так поступать в ваших компонентах.

Вот как выглядит воплощение этих двух методов: void __fastcall TAngleText::SetXPos(int XPos )

{

if ( XPos < 0 || XPos > Width ) return;

FXPos = XPos;

}

void __fastcall TAngleText::SetYPos(int YPos )

{

if ( YPos < 0 || YPos > Height ) return;

FYPos = YPos;

}

Как видно из приведенного кода, функция просто проверяет, что заданные вами значения лежат внутри границ компонента. Это весьма распространенный случай. Если значение является допустимым, переменной-члену класса присваивается новое значение. Если значение не является допустимым, метод просто возвращает управление. Что же происходит с  переменной-членом класса в этом случае? Ничего. Вы надежно защитили собственно свойство компонента от получения некорректных данных, причем гораздо лучше и проще, чем Set и Get.

Следующим делом будет инициализация переменных в классе.  Как и во всех классах C++, нам важно инициализировать переменные до того, как они будут использованы. Для компонентов это сможет еще и установить соответствие со значениями, которые мы выбрали для указания по умолчанию в описании свойства (если, конечно, таковые имеются). Добавьте следующий код в конструктор класса:

__fastcall TAngleText::TAngleText(TComponent* Owner)

: TCustomControl(Owner)

{

FXPos = -1;

FYPos = -1;

Angle = 0;

}

Обратите внимание, что мы не инициализировали, не получали, не устанавливали свойств Text, Font или Color. Эти свойства относятся к компонентам низкого уровня и инициализируются там. Правда, вы могли бы инициализировать их в своем собственном конструкторе, что заместило бы установки базового класса.

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

Отрисовка управляющего элемента

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

К сожалению, не существует никакого другого способа добавить новый метод компонента, кроме как применив грубую силу. В этой области есть над чем поработать создателям CBuilder, и почти наверняка в следующих версиях положение будет исправлено. А пока что вам придется сделать это своими руками.

Для начала надо определить,  какой метод компонента  вам надо  заместить.  Это можно  сделать,

изучив методы класса компонента, перечисленные в контекстной помощи по этому компоненту. Выберите один из них и скопируйте текст прототипа метода в буфер обмена (clipboard), потом откройте заголовочный файл класса и добавьте следующую строку в секцию protected (большинство замещаемых методов будут вступать в строй именно в этой секции заголовочного файла компонента):

virtual void     fastcall Paint(void);

Обратите внимание на использование ключевого слова virtual. Только виртуальные методы могут быть замещены наследующим классом. К счастью, большинство обработчиков в классах CBuilder воплощены в виде виртуальных методов.

После того, как прототип определен в заголовочном файле, пришло время добавить рабочую часть метода в исходный файл класса. Добавьте следующий метод в конец исходного файла компонента TAngleText:

void __fastcall TAngleText::Paint(void)

{

// Устанавливаем угол поворота текста

LOGFONT LogRec;

GetObject(Font->Handle,sizeof(LogRec),&LogRec);

// Примечание: угол в десятках градусов

LogRec.lfEscapement = Angle * 10.0;

// Проверяем, задана ли позиция по умолчанию

if (FXPos == -1 ) FXPos = Width / 2; if (FYPos == -1 ) FYPos = Height / 2;

Canvas->Font->Handle = CreateFontIndirest(&LogRec); Canvas->Brush->Color = Color;

Canvas->TextOut( FXPos, FYPos, Text );

}

Этот код достаточно прямолинеен в той части, где устанавливаются свойства Canvas для шрифта и цвета, а потом рисуется текст в заданной пользователем позиции. Изменение шрифта осуществляется изменением части lfEscapement структуры, которая определяет угол (в десятках градусов) поворота текста. Остальная часть кода это обычная магия Windows.

На этом созидательная часть разработки компонента TAngleText закончена. Следующий этап — это тестирование компонента, осуществляемая для того, чтобы удостовериться в том, что он работает правильно перед тем, как инсталлировать его.

Источник: Теллес М. – Borland C++ Builder. Библиотека программиста – 1998

По теме:

  • Комментарии