уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.

Впечатлениями по 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 свойства кнопки выглядят вот так:

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

Теперь вернемся к нашему классу. Вначале пишем конструктор. Он у нас будет простой, как табуретка:

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;

Теперь, если мы делаем клик по кнопке, которая является активной, то вызывается её обработчик.

В процедуре ApplyStyle для поиска необходимого нам элемента стиля мы воспользовались методом FindStyleResource. Так как в нашем стиле у элементов кнопок определены свойства BindingName мы могли воспользоваться ещё двумя вариантами поиска элементов:
  1. Воспользоваться методом FindBinding(BindingName: string):TfmxObject;
  2. Воспользоваться свойством 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 мы:

  1. Ищем объект эффекта тени и если находим, то, в зависимости от параметра Value либо убираем, либо устанавливаем триггер «IsMouseOver=True»
  2. Если кнопка должна стать неактивной, то мы устанавливаем её новой значение 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 на форму, устанавливаем ему нужные значения свойств Align, Margins и Padding и все — макет готов. потом внутри 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 — там будет на что посмотреть, что изучить, а заодно и попытаемся пересобрать этот проект.
На сегодня все. Исходников пока никаких не выкладываю, т.к. их толком ещё и нет — выложу, когда программа будет полностью собрана.

уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
1 Комментарий
Межтекстовые Отзывы
Посмотреть все комментарии
Сергей JSinx
25/03/2013 00:50

Привет.
Делал что-то подобное в XE3, но с компонентом ListBoxItem.
Нет смысла переопределять конструктор, т.к. если название стиля и название класса объекта совпадают, то стиль применится автоматически.

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