Впечатлениями по FireMonkey поделились, каждый кто хотел высказал свою точку зрения, а теперь снова вернемся к работе над программой «Я математик». В прошлый раз мы остановились на том, что создали свой простенький стиль для TTabItem — добавили на вкладку две кнопки, клик по которым должен (и будет) запускать одну из стадий игры — «тренировку» или «экзамен». Сейчас наши импровизированные кнопки реагируют на курсор мыши (срабатывает эффект TShadowEffet), но событие OnClick этих кнопок никак не обрабатывается. Вот сегодня этим и займемся — допишем свой класс для TTabItem, а также создадим слой (TLayout) для вывода вопросов игроку. Вообще, по поводу создания компонентов для FireMonkey на сайте Embarcadero есть небольшая справка — там рассказывается о том как надо сохранять стиль в ресурсах, правильно его загружать и т.д. Можно было бы воспользоваться и этой справкой, но сегодня мы обойдемся без res-файлов.
Продолжаем работу с TTabItem
Итак, в прошлый раз мы остановились на том, что создали вот такой стиль для TTabItem и даже протестировали его на TTabControl’e:
Теперь нам надо сделать обработчики OnClick для наших кнопок, а также возможность делать кнопки активными и неактивными. Создадим наш собственный класс, который будет наследником TTabItem и назовем его TMathTabItem:
type TMathTabItem = class(TTabItem) private {элементы кнопок} FLeftButton: TControl; FRightButton: TControl; {события} FOnRightClick: TNotifyEvent; FOnLeftClick: TNotifyEvent; {активность кнопок} FRightEnabled: boolean; FLeftEnabled: boolean; procedure DoClick(Sender: TObject); procedure SetLeftEnabled(const Value: boolean); procedure SetRightEnabled(const Value: boolean); procedure SetEnabledBtn(Value: boolean; ShadowEffectName: string; BtnControl: TControl); protected procedure ApplyStyle; override; public constructor Create(AOwner: TComponent); {свойства кнопок} property RightEnabled: boolean read FRightEnabled write SetRightEnabled; property LeftEnabled: boolean read FLeftEnabled write SetLeftEnabled; {события} property OnRightClick: TNotifyEvent read FOnRightClick write FOnRightClick; property OnLeftClick: TNotifyEvent read FOnLeftClick write FOnLeftClick; end;
Сейчас мой стиль для TTabItem носит название MathTabItemStyle. Вначале чуть подробнее рассмотрим сам стиль, а потом приступим к рассмотрению методов класса. Во-первых, чтобы любой контрол FireMonkey мог обрабатывать события OnClick у этого контрола свойство HitTest должно ровняться True. Поэтому и наши кнопки, которые по сути являются простыми TImage содержат HitTest=True.
Второй момент, на который вы могли обратить внимание при работе с любыми контролами FMX — это свойство BindingName, т.е. имя компонента, используемое для связи данных, например, через тот же LiveBindings. В моем стиле BindingName кнопок содержит туже строку, что и свойство StyleName. То есть в Object Inspector’e свойства кнопки выглядят вот так:
Теперь вернемся к нашему классу. Вначале пишем конструктор. Он у нас будет простой, как табуретка:
constructor TMathTabItem.Create(AOwner: TComponent); begin inherited Create(AOwner); StyleLookup := cStyleName; end;
Здесь константа cStyleName — это название моего стиля, т.е. MathTabItemStyle. Несмотря на то, что конструктор получился простенький, при присвоении значения свойству StyleLookup в глубинах FireMonkey происходит довольно много вещей — вызываются методы ApplyStyleLookup, GetStyledObject, ApplyStyle и т.д. В общем, происходит полная перерисовка компонента согласно заданному стилю.
Теперь посмотрим, что происходит при применении стиля. В нашем классе есть перекрытый метод ApplyStyle, который выглядит следующим образом:
procedure TMathTabItem.ApplyStyle; var btnLeft, btnRight: TFmxObject; begin inherited; {левая кнопка - btnTraning} btnLeft:=FindStyleResource(cLeftBtnStyleName);//ищем в стиле левую кнопку if Assigned(btnLeft)and(btnLeft is TControl) then //нашли и это TControl begin FLeftButton:=TControl(btnLeft);//запомнили FLeftButton.OnClick:=DoClick;//назначили обработчик FLeftEnabled:=True;//сделали активной end; {правая кнопка - btnExam} btnRight:=FindStyleResource(cRightBtnStyleName); if Assigned(btnRight)and(btnRight is TControl) then begin FRightButton := TControl(btnRight); FRightButton.OnClick := DoClick; FRightEnabled := True; end; end;
То есть в момент применения стиля мы делаем активными кнопки и назначаем им обработчик DoClick, который выглядит следующим образом:
procedure TMathTabItem.DoClick(Sender: TObject); begin {кликнули по левой кнопке} if SameText(TFmxObject(Sender).BindingName, cLeftBtnStyleName) then begin if Assigned(FOnLeftClick) and FLeftEnabled then FOnLeftClick(self) end else {кликнули по правой кнопке} if SameText(TFmxObject(Sender).BindingName, cRightBtnStyleName) then if Assigned(FOnRightClick) and FRightEnabled then FOnRightClick(self) end;
Теперь, если мы делаем клик по кнопке, которая является активной, то вызывается её обработчик.
- Воспользоваться методом FindBinding(BindingName: string):TfmxObject;
- Воспользоваться свойством Binding[index:string]: Variant
Теперь посмотрим, что происходит при присвоении нового значения свойствам LeftEnabled и RightEnabled. Сеттеры этих свойств выглядят следующим образом:
procedure TMathTabItem.SetLeftEnabled(const Value: boolean); begin if Assigned(FLeftButton)and(Value<>FLeftEnabled) then begin FLeftEnabled := Value; SetEnabledBtn(Value, cLeftBtnEffect, FLeftButton); end; end; procedure TMathTabItem.SetRightEnabled(const Value: boolean); begin if Assigned(FRightButton)and(Value<>FRightEnabled) then begin FRightEnabled := Value; SetEnabledBtn(Value, cRightBtnEffect, FRightButton); end; end; procedure TMathTabItem.SetEnabledBtn(Value: boolean; ShadowEffectName: string; BtnControl: TControl); var Shadow: TFmxObject; begin Shadow := FindStyleResource(ShadowEffectName); if Value then begin if Assigned(Shadow) then TEffect(Shadow).Trigger := cShadowTrigger; BtnControl.Opacity := cEnabledBtnOpacity; end else begin if Assigned(Shadow) then TEffect(Shadow).Trigger := EmptyTrigger; BtnControl.Opacity := cNotEnabledBtnOpacity; end; end;
В процедуре SetEnabledBtn мы:
- Ищем объект эффекта тени и если находим, то, в зависимости от параметра Value либо убираем, либо устанавливаем триггер «IsMouseOver=True»
- Если кнопка должна стать неактивной, то мы устанавливаем её новой значение Opacity = 0.4
Вот так выглядит наш класс на текущий момент. Осталось проверить его работоспособность. Для этого в основной программе можно, например, написать вот такой код:
procedure Tfmain.CreateTabs; var I: Integer; ti: TMathTabItem; begin for I := 0 to FTestOptions.SectionCount-1 do begin ti:=TMathTabItem.Create(tcTest); ti.Parent:=tcTest; case FTestOptions.Section[i] of tsMultiplyBy5: ti.Text:=rsMultiplyBy5; tsMultiplyBy9: ti.Text:=rsMultiplyBy9; tsMultiplyBy11: ti.Text:=rsMultiplyBy11; tsSquaringNumbers: ti.Text:=rsSquaringNumbers; tsRootDoubleDegree: ti.Text:=rsRootDoubleDegree; end; ti.OnRightClick:=RClick; ti.OnLeftClick:=LClick; ti.RightEnabled:=False; end; end;
Если Вы качали исходники «Я математик» со страницы софта блога, то должны понять, что в приведенной выше процедуре создаются только те табы, для которых в опциях программы были выбраны чекбоксы. То есть, при таких настройках программы:
TabControl станет вот таким:
При этом правая кнопка на всех табах будет неактивна. Кстати, обработчики событий OnClick кнопок сделаны пока только для проверки и выглядят очень просто:
procedure Tfmain.RClick(Sender: TObject); begin ShowMessage(TMathTabItem(Sender).Text + ' Rigth btn clicked'); end; procedure Tfmain.LClick(Sender: TObject); begin ShowMessage(TMathTabItem(Sender).Text + ' Left btn clicked'); end;
Теперь, когда у нас есть TabControl с необходимым нам видом табов, прикинем как будут выводиться задания для тестов. В первой версии программы все элементарно — есть отдельный TLayout на котором расположены элементы TText для вывода текста задания и подсказки и TEdit для ввода ответа учеником. Собственно все меня в том TLayout устраивает за исключением вывода текста задания для задачи «Вычисление квадратного корня» — очень уж просто выводится задание — «Корень из Х = «. С одной стороны и сойдет, а с другой…как-то не по-математически выглядит. Можно было бы показывать специально для таких задач рядом с текстом (или где-то ещё) картинку с изображением того самого корня квадратного, но я решил воспользоваться для этого случае компонентом TPath и отрисовать корень руками (надо же где-то этот компонент использовать :)). В итоге пришлось немного модернизировать и TLayout о котором мы сейчас и поговорим.
TLayout для вывода заданий
Вначале определимся с макетом слоя для вывода заданий. Я пока остановился на таком:
Определившись с макетом, посмотрим что находится внутри TLayout для элементов задания (на рисунке он выделен синим цветом. Структура элементов внутри этого TLayout такая:
- Внутри компонента TPath (RootPath) находится QuestionText: TText в который будет выводиться текст задания.
- AnswerEdit: TEdit — поле для ввода ответа игроком имеет свойство Align = alRight
- equallyText: TText — служит для вывода знака «=» и также выровнен по правому краю (находится слева от AnswerEdit)
То есть в дизайнере все содержимое TLayout выглядит вот таким образом:
Теперь приступим к рисованию в TPath. Про рисование в TPath я уже рассказывал на примере простых примитивов. Сегодня мы тоже нарисуем примитивную фигурку, но уже чуть по-сложнее, чем ромбик или дуга. Все в курсе как выглядит корень квадратный..Например так:
Попробуем нарисовать то, что изображено на черном фоне. Чтобы было легче думаться — можно, например, взять листок бумаги и расчертить на нем небольшую сетку с узловыми точками:
Теперь, используя узлы сетки в качестве ориентиров можно открыть редактор свойства PathData у TPath и нарисовать значок корня. У меня получлся вот такой ряд данных:
M 0,2.7 L 0.2,2.6 L 0.8,3.2 L 1.5,1.0 L 18.0,1.0
И в итоге мой TLayout с TPath принял вот такой вид:
По-моему вполне сойдет для первого раза. Определившись с формой нашей фигуры можно отредактировать TPath так, чтобы все точки имели целые координаты, т.е. просто умножим координаты каждой точки на 10 и получим вот такой путь:
M 0,27 L 2,26 L 8,32 L 15,10 L 180,10
Осталось дело за малым — сохраняем содержимое PathData в какую-нибудь константу, например:
const cRootPathData = 'M 0,27 L 2,26 L 8,32 L 15,10 L 180,10';
И, когда надо отрисовать знак корня делаем так:
RootPath.Data.Data:=cRootPathData;
а когда надо наоборот — убрать значок — вот так:
RootPath.Data.Clear;
Вот и весь секрет рисования в TPath, а точнее, никакого секрета. Теперь осталось дело за «малым» — переписать внутренности программы и сделать поэтапный вывод заданий игроку, подсчёт баллов, возможности перехода на предыдущие этапы игры, отслеживание отвеченных вопросов и т.д. и т.п. Но всё это уже мало связано с FireMonkey. Так что, скорее всего к теме FireMonkey в Delphi вернемся теперь уже после выхода Delphi XE3 — там будет на что посмотреть, что изучить, а заодно и попытаемся пересобрать этот проект.
На сегодня все. Исходников пока никаких не выкладываю, т.к. их толком ещё и нет — выложу, когда программа будет полностью собрана.
Привет.
Делал что-то подобное в XE3, но с компонентом ListBoxItem.
Нет смысла переопределять конструктор, т.к. если название стиля и название класса объекта совпадают, то стиль применится автоматически.
Если же переопределить конструктор, то у меня не создавался CheckBox, который был встроен по умолчанию (именно для моего класса), при этом, при переименовании стиля в обычное (listboxitemstyle), показывались и мои изменения и CheckBox.