Используем макросы в IDE-редакторе Delphi

Занимаясь отладкой процедур, связанных с обработкой сообщений, постоянно приходится делать сопоставление между кодом сообщения и его строковым наименованием. Ну, к примеру, «прилетает» код 6 — это WM_ACTIVATE. Или сложнее: код 274 (0x0112) = WM_SYSCOMMAND.

Мне это порядком надоело — решил сделать Helper для TMessage (попутно и для TMsg). Ну и в планах попробовать сделать Debugger Visualizer.
Однако о самом хелпере я постараюсь написать в следующий раз. В этой заметке хочу описать, как можно использовать кнопочки Record Macro и Playback Macro, которые находятся в левом нижнем углу строки состояния редактора кода.

(Кстати, мне впервые в жизни пришло в голову попробовать их использовать.)

Итак, я хочу получить код вида:
  case Msg of
    WM_NULL: Result := 'WM_NULL';
    WM_CREATE: Result := 'WM_CREATE';
    ...
    WM_APP: Result := 'WM_APP';
  end;

Идём в модуль Messages и копируем оттуда код:
  {$EXTERNALSYM WM_NULL}
  WM_NULL             = $0000;
  {$EXTERNALSYM WM_CREATE}
  WM_CREATE           = $0001;
  ...
  {$EXTERNALSYM WM_APP}
  WM_APP = $8000;

Вставляем в новый модуль и начинаем макрос. По шагам:
  1. Нажимаю Ctrl+F (Панель поиска), указываю пробел, снимаю все флажки. Enter — чтобы запомнилось.
  2. Устанавливаю курсор на первой строке, нажимаю «Record Macro»:.
  3. Ctrl+Y — удаляем строку
  4. Ctrl+Вправо — курсор к началу идентификатора
  5. F3 — поиск до пробела
  6. Влево — курсор к концу идентификатора
  7. Ctrl+Shift+Влево — выделили идентификатор
  8. Ctrl+C — скопировали выделенное в буфер обмена
  9. Повторяем 5. и 6. — курсор к концу идентификатора
  10. Shift+End — выделение до конца строки
  11. Delete — удаляем выделенное
  12. Набираем на клавиатуре
    : Result := ‘
  13. Ctrl+V — вставили скопированное
  14. Набираем на клавиатуре
    ‘;
  15. Home — переход к началу строки
  16. Вниз — переход к следующей строке
  17. Нажимаю «Stop Recording Macro»:.
Макрос готов, теперь просто жамкаем в «Playback Macro»
пока не достигнем нужного результата.

Читать на сайте автора.

Простой текстовый итератор. Пример использования record в Delphi

Задача: осуществить перебор строковых значений в исходной строке, разделённых символом-разделителем, с учётом символа-кавычки.
Решение:

unit dnStringIterator;

interface

type
  TStringValuesIterator = record
  type
    TCallBack = reference to procedure (const AValue: string);
    TOptions = set of (ioTrimValues, ioDequoteValues);
  private
    FSourceString: string;
    FDelimiter, FQuoteChar: Char;
    FCheckQuotes: Boolean;
    FOptions: TOptions;
    FOffset, FLength: Integer;
    FCurrent: string;
  public
    constructor Init(const ASourceString: string; ADelimiter: Char; AQuoteChar: Char = #0; AOptions: TOptions = []);
    function GetEnumerator: TStringValuesIterator;
    function MoveNext: Boolean;
    procedure Run(ACallBack: TCallBack); inline;
    property Current: string read FCurrent;
  end;

implementation

uses
  SysUtils;

{ TStringValuesIterator }

constructor TStringValuesIterator.Init(const ASourceString: string; ADelimiter, AQuoteChar: Char; AOptions: TOptions);
begin
  FSourceString := ASourceString;
  FDelimiter := ADelimiter;
  FQuoteChar := AQuoteChar;
  FOffset := 1;
  FLength := Length(FSourceString);
  FOptions := AOptions;
  FCurrent := '';

  FCheckQuotes := FQuoteChar <> #0;

  // нельзя, чтобы символ разделитель совпадал с символом-кавычкой:
  Assert(not FCheckQuotes or (FDelimiter <> FQuoteChar));
  // нельзя использовать ioDeqouteValues, если не указан QuoteChar
  Assert(FCheckQuotes or not (ioDequoteValues in FOptions));
end;

function TStringValuesIterator.GetEnumerator: TStringValuesIterator;
begin
  Result := Self;
end;

function TStringValuesIterator.MoveNext: Boolean;
var
  IsInQuote: Boolean;
  CurPos: Integer;
  Ch: Char;
begin
  Result := (FLength > 0) and (FOffset 

Примеры использования

С явным объявлением дополнительной переменной:
var
  svi: TStringValuesIterator;
begin
  svi.Init(TestString, ',');
  while svi.MoveNext do
    Memo1.Lines.Add(svi.Current);
end;

Без явного объявления дополнительной переменной, используя with:

begin
  with TStringValuesIterator.Init(TestString, ',') do
    while MoveNext do
      Memo1.Lines.Add(Current);
end;

Используя анонимную процедуру:

begin
  TStringValuesIterator.Init(TestString, ',').Run(
    procedure (const AValue: string)
    begin
      Memo1.Lines.Add(AValue);
    end
  );
end;

Используя for-in синтаксис:

var
  Tmp: string;
begin
  for Tmp in TStringValuesIterator.Init(TestString, ',') do
    Memo1.Lines.Add(Tmp);
end;

Читать на сайте автора.

VCL: Show vs Visible

У котнролов в VCL есть свойство Visible, которое как называется, за то и отвечает. Если Visible = False, то контрол скрыт и пользователь его не увидит. Если Visible = True, то есть вероятность, что пользователь его увидит.

А есть ещё два метода – Hide и Show, которые взаимодействуют со свойством Visible.

А теперь вопрос: как же правильно скрывать/отображать контрол?

На самом деле вопрос с небольшим подвохом, на него ответить однозначно нельзя, но… давайте по порядку

В большинстве случаев ответ напрашивается сам собой: используем Visible, т.к. это способствует единообразию. Например, потому что есть свойство Enabled, для которого нету методов типа Enable/Disable. У экшенов (TAction) есть Visible, но нет Show/Hide. И ещё такой момент, сравните:

LabelSomeInfo.Visible := SomeCondition;

и

if SomeCondition then
  LabelSomeInfo.Show
else
  LabelSomeInfo.Hide;

Первый вариант и пишется быстрее, и проще сопровождается. (Правда всегда найдутся люди, которые скажут, что второй вариант выразительнее первого…)

Ещё можно порассуждать так (на интуитивном уровне, ещё не заглядывая в исходники или справку). Visible – это свойство, которое задаёт (и показывает) состояние контрола на текущий момент времени. Show/Hide – это методы, они призваны изменить состояние контрола, но не факт, что это произойдёт одномоментно, и не факт, что это действие не приведёт к какому-то побочному эффекту. Поэтому (как бы) безопаснее использовать Visible.

 

Теперь самое время заглянуть в исходники VCL. Hide просто скрывает контрол (устанавливая Visible в False). А вот Show не только меняет Visible, но делает кое-что ещё. А именно – вызывает виртуальный метод ShowControl у родительского контрола (если такой определён). А это и есть тот самый побочный эффект, о котором можно догадаться на интуитивном уровне. На практике, в самой VCL, я нашёл только одно место, где это можно использовать – компонент TPageControl. Когда мы вызываем Show для контрола, расположенного на одной из вкладок PageControl’а, происходит активация той вкладки (TabSheet), на которой этот контрол расположен. Более того, если вложить несколько PageControl’ов друг-в-вдруга, то активация вкладок произойдёт по цепочке от контрола вверх до самой формы.

Т.е. вывод делаем такой: если просто нужно задать состояние для контрола (в большинстве случаев именно это и нужно), то используем Visible. Если нужно акцентировать внимание пользователя на контрол, то используем Show (и ещё, после этого, можно вызвать SetFocus для полей ввода). Зачем нужен Hide – не знаю, видимо он заводился на “автомате”, в противовес Show. Ещё странно, что эти методы не виртуальные…

В общем, это надо знать и уметь применять.

 

А за примерами, где это может быть применимо, далеко ходить не надо. В самой IDE Delphi: нажимаем F6, набираем “editor font”, жамкаем Enter – открывается окно с настройками IDE, осуществляется переход к вкладке Editor Options Display и устанавливается фокус ввода на комбобокс для выбора шрифта. (Уверен, что это сделано парой вызов Show + SetFocus для самого комбобокса.)

Ещё пример (уже из моей практики). На форме есть обязательное для ввода поле. Пользователь его не заполнил и переключился на другую вкладку, затем нажимает OK. Я вижу, что поле не заполнено, и просто вызываю пару методов Show + SetFocus (не задумываясь, надо ли активировать вкладку, на какой вкладке это поле и т.п.). После этого можно показать сообщение пользователю (мол заполни поле) или даже не показывать, а сделать Beep – фокус уже будет где надо, и пользователь поймёт, что от него требуется.

 

Полезный метод Show. Но его нельзя применять, когда надо просто отобразить какой-то контрол по условию (например при отображении формы), т.к. это может привести к переключению вкладок у PageControl’а.

Читать на сайте автора.

Вопросик про Show, Hide и Visible

Бывает, знаете ли, полезно скрывать часть информации от пользователя, если она ему в данный момент не нужна. Для этого у контролов в VCL есть свойство Visible, и есть ещё два метода – Show и Hide. Эта троица призвана управлять видимостью контролов на форме.
А вопрос такой. Как лучше писать:

LabelSomeInfo.Visible := True;

или

LabelSomeInfo.Show;

?
Как пишете Вы? Почему?

 

Ответ в следующей заметке.

Читать на сайте автора.

Tap-Tap on Google Play

Ура! Конкурс завершён, поздравляю победителей с победой!

Обещанный нехус 7 в оригинальной футболке приехал прямо ко мне домой курьерской доставкой. Достойный приз требует достойного завершения, а именно – размещения конкурсной программы в Google Play.

Ниже я опишу, что для этого мне потребовалось сделать.

Итак, прежде чем разместить приложение в Google Play, его необходимо специальным образом подготовить. Это самое неочевидное (для меня) действие, хотя делается всё очень просто.

Открываем проект в Delphi, затем выбираем Project Options –> Provisioning. Далее выбираем цель Target: Release configuration — Android platform и тип сборки Build Type: Android Store. Далее жамкаем кнопку New Keystore… и следуем указаниям мастера (придумываем имя для файла, пароли и алиасы). Мастер сгенерит цифровую подпись, без которой Google Play не примет apk-файл, ругнувшись на то, что “Архив APK-файла не оптимизирован”. У меня выглядит это так:

image

Затем необходимо выбрать (т.е. активировать в Project Manager’е) необходимую конфигурацию сборки. Это делается в три двойных клика по выделенным на картинке ниже строкам:

image

Если не выбрать Application Store, то Google Play ругнётся, мол необходимо удалить отладочную информацию из APK-файла.

Теперь делаем Project Build XXX и сразу Project Deploy libXXX.so (где XXX – это имя проекта). После этого, APK-файл готов для размещения в Google Play и находится в каталоге …AndroidReleaseXXXbin.

 

Следующий шаг – это регистрация аккаунта разработчика в Google Play. Для этого надо перейти в Google Play Developer Console. И теперь самое неприятное – заплатить 25 USD. Честно говоря, я немного поколебался, мол “а надо ли оно мне?”, но потом подумал, что это не такая уж и большая сумма, может быть она себя и не окупит, но по бюджету не ударит.

Ну и далее всё просто, я особо описывать не будут. Потребуют: подготовленный APK-файл, иконку приложения размером 512х512 пикселей и 2 скриншота с реального устройства.

После всех этих процедур мне пообещали, что для скачивания приложение будет доступно через несколько часов. В общем, как только – так сразу, а пока – ждём-с.

 

Ссылка для скачивания (в маркете искать по словам “nikolay zverev”).

Читать на сайте автора.

Delphi плюс Android? Есть идея! Tap#5

Эта заметка является продолжением предыдущих: Tap#1, Tap#2, Tap#3, Tap#4. И в ней я хочу завершить.

Итак, игра на логическое мышление.  Я решил сделать аналог игры Flip-Flop компании Gamos. На всякий случай, оговорюсь – я не ставлю перед собой цель создать коммерческий продукт. Цель другая –  попробовать, покрутить, поучаствовать.

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

В предыдущих заметках я рассказал о базовой фрейме TNPC. Так получилось, что эта фрейма у меня стала базовой для всех фрейм в проекте. А фрейм получилось несколько – я решил раздробить части игры именно по фреймам и в главной форме уже наладить взаимодействие между фреймами.

У меня получились:

  uAni,                          // моя попытка сделать работу с анимацией более удобной
  frMain {frmMain},              // главная форма приложения
  uNPC {NPC: TFrame},            // базовый фрейм
  fraDroid {frameDroid: TFrame}, // фрейм с персонажем-роботом
  fraHello {frameHello: TFrame}, // фрейм с интро (оно же получилось титрами)
  fraDesk {frameDesk: TFrame},   // фрейм с игровым полем
  fraMenu {frameMenu: TFrame},   // фрейм с меню
  fraHelp {frameHelp: TFrame},   // фрейм с описанием как играть
  fraOptions {frameOptions: TFrame}, // фрейм с настройками
  fraScore {frameScore: TFrame};     // фрейм с рекордом и очками игрока

Всё вместе это выглядит так:

Tap-Tap!

Взаимодействие между фреймами я сделал очень простым, оно происходит через главную форму. Т.е. на экземпляр главной формы ссылается глобальная переменная. При этом главная форма содержит методы:

  public
    procedure ShowIntro;
    procedure StartNewGame;
    procedure StopGame;
    procedure ShowOptions;
    procedure ShowHelp;
    procedure ShowCredits;

    procedure RegClick;
    procedure RegWin;

Эти методы, теоретически, может “дёрнуть” любая фрейма – что не есть хорошо. Но для такой простой программы – вполне приемлемо.

 

О настройках и рекордах

Всё это хранится в обычном ini-файле, делается так:

uses
  .. System.IniFiles ..
  ..
  private
    FOptions: TMemIniFile;
  ..
const
  sIniFileName = 'TapTap.ini';
  ..
  FOptions := TMemIniFile.Create(TPath.GetHomePath + TPath.DirectorySeparatorChar + sIniFileName);
  ..
  // save options to file:
  FOptions.UpdateFile;

 

И ещё несколько слов об анимации

Мне показалось, что настройка анимации через компоненты типа TFloatAnimation в Design-Time (конкретно для задачи анимирования персонажа) не очень удобна. Потому что один TFloatAnimation-объект модифицирует всего одно свойство. (А ещё, при большом количестве объектов анимации может произойти рассинхронизация.) Поэтому, я пошёл немного другим путём.

Идея такая: анимацию сложного (составного) объекта помещаем в одну процедуру с входным параметром AProgress: Single, значения которого варьируются от 0 до 1. Задача процедуры – установить положение объекта на момент времени AProgress. При этом, в теле одной процедуры могут меняться и координаты и размеры разных составляющих объекта. Пример такой процедуры был в предыдущей заметке – см. метод ProcessOpenCloseAnimation).

Забота за вызов такой процедуры перекладывается на другой объект, который, по сути, состоит из TFloatAnimation. Если же надо вызвать последовательно несколько процедур, то их можно поместить в список. Так у меня “родился” модуль uAni.pas – ещё черновой, но уже рабочий.

Такой подход дал мне возможность всего одним TFloatAnimation-объектом анимировать смену состояний сразу у нескольких персонажей.

 

Вместо заключения

На текущий момент, игра оставляет впечатление недоделанности. Тут, конечно, есть что доводить до ума. У меня было ещё несколько идей и с уровнями сложности, и с анимацией, и с выбором персонажей, и с озвучкой (этого, кстати, вообще нет), и с рекордами (в плане внешнего оформления – тут я откровенно схалтурил). Но, надо понимать, что всё это требует времени. (Кстати, на написание заметок в блог порой уходит больше времени, чем на программирование.)

С другой стороны, игра получилась вполне “играбельной”. В общем, я вполне удовлетворён результатом и надеюсь, что и вы не останетесь равнодушными.

 

Скачать: исходники (zip-архив, 93.4 КБ), Win32-exe (zip-архив, 2.47 МБ), Android-apk (zip-архив, 6.33 МБ).

Читать на сайте автора.

Delphi плюс Android? Есть идея! Tap#4

Эта заметка является продолжением предыдущих: Tap#1, Tap#2, Tap#3. И в ней я поговорю об анимации.

В моей будущей игре у персонажа будет два основных состояния. Назовём их “Открытый” и “Закрытый”. В первом состоянии внешний вид персонажа соответствует тому, который разработан в дизайнере и сохранён в переменной DesignPositions. Во втором, вид персонажа будет другой. Для робота я решил так: меняем цвет на серый, чуть-чуть уменьшаем общий размер, втягиваем ноги, руки и антенны, закрываем глаза.

Соответственно переход из состояния в состояние будет анимированным.

Делаю я следующим образом: сразу после создания фреймы, в методе SaveDesignPositions, произвожу необходимые трансформации и сохраняю положения и размеры объектов в переменную FClosedPositions. Вот так:

type
  TframeRobot = class(TNPC)
  private
    FClosedPositions: TDesignPositions;
  public
    procedure SaveDesignPositions; override;
//...
procedure TframeRobot.SaveDesignPositions;
  procedure SwitchToClose;
  const
    kBody = 9/10;
    kEye = 1/4;
    kAnt = 1/4;
    kHand = 5/6;
    kHand2 = 1/2;
    kLeg = 1/3;
  begin
    // HINT: мы смотрим роботу в лицо, поэтому левая рука - справа, правая - слева

    // уменьшаем Body
    Body.Position.X := Body.Position.X + Body.Width / 2 * (1 - kBody);
    Body.Width := Body.Width * kBody;
    Body.Position.Y := Body.Position.Y + Body.Height / 2 * (1 - kBody);
    Body.Height := Body.Height * kBody;
    // смещаем соединительные линии между телом и ногами
    LegLeftCon.Position.Y := LegLeftCon.Position.Y - LegLeftCon.Height * kBody;
    LegRightCon.Position.Y := LegRightCon.Position.Y - LegRightCon.Height * kBody;

    // уменьшаем Head
    Head.Position.X := Head.Position.X + Head.Width / 2 * (1 - kBody);
    Head.Width := Head.Width * kBody;
    Head.Position.Y := Head.Position.Y + Head.Height / 2 * (1 - kBody);
    Head.Height := Head.Height * kBody;

    // приближаем Head к Body
    HeadCenter.Position.Y := HeadCenter.Position.Y + Head.Height * (1 - kBody);

    // закрываем глазки
    EyeLeft.Position.Y := EyeLeft.Position.Y * kBody + EyeLeft.Height * kEye;
    EyeLeft.Height := EyeLeft.Height * kEye;
    EyeRight.Position.Y := EyeRight.Position.Y * kBody + EyeRight.Height * kEye;
    EyeRight.Height := EyeRight.Height * kEye;

    // уменьшаем и прячем антенны
    AntenaRight.Position.Y := AntenaRight.Position.Y * kBody + AntenaRight.Height * kBody;
    AntenaRight.Height := AntenaRight.Height * kBody * kAnt;
    AntenaRight.Position.Y := AntenaRight.Position.Y - AntenaRight.Height;

    AntenaLeft.Position.Y := AntenaLeft.Position.Y * kBody + AntenaLeft.Height * kBody;
    AntenaLeft.Height := AntenaLeft.Height * kBody * kAnt;
    AntenaLeft.Position.Y := AntenaLeft.Position.Y - AntenaLeft.Height;

    // прячем руки
    HandRightCenter.RotationAngle := 90;
    HandRightCenter.Position.X := HandRightCenter.Position.X + HandRight.Height * kHand;
    HandRightCenter.Position.Y := HandRightCenter.Position.Y + HandRight.Width * kHand2;

    HandLeftCenter.RotationAngle := -90;
    HandLeftCenter.Position.X := HandLeftCenter.Position.X - HandLeft.Height * kHand;
    HandLeftCenter.Position.Y := HandLeftCenter.Position.Y + HandLeft.Width * kHand2;

    // прячем ноги
    LegRightCenter.Position.Y := LegRightCenter.Position.Y - LegRight.Height * kLeg;
    LegLeftCenter.Position.Y := LegLeftCenter.Position.Y - LegLeft.Height * kLeg;
  end;
begin
  inherited;

  SwitchToClose;
  FClosedPositions := TDesignPositions.Create;
  SavePositionsTo(FClosedPositions);
end;

Здесь TframeRobot – это наш персонаж, про TNPC и SaveDesignPositions я писал в предыдущей заметке. Вот так выглядит наш робот до и после трансформации:

DesignPositions соответствуют картинке слева, ClosedPositions – картинке справа. Это начальное и конечное состояние в анимации. Сама анимация протекает во времени. Если взять начало времени за ноль, а конец – за единицу, то положение объекта легко рассчитать по формуле:  f(t) = a + (b – a) * t, где t – время от 0 до 1, а – начальная координата, b – конечная координата, f – искомая координата. В библиотеке FMX такая функция уже реализована и называется InterpolateSingle.

Вот так у меня выглядит процедура для изменения размеров и положений объектов робота:

procedure TframeRobot.ProcessOpenCloseAnimation(const AProgress: Single);
var
  BodyColor: TAlphaColor;
  i: Integer;
  CP, DP: TDesignPosition;
  AControl: TControl;
begin
  BodyColor := InterpolateColor($FF99CC00, TAlphaColors.Gray, AProgress);

  // цикл от 1, т.к. саму фрейму масштабировать не надо
  for i := 1 to DesignPositions.Count - 1 do
  begin
    AControl := Controls[i];
    CP := ClosedPositions.Items[i];
    DP := DesignPositions.Items[i];

    // цвет
    if (AControl is TShape) and (AControl <> EyeLeft) and (AControl <> EyeRight) then
      if TShape(AControl).Fill.Color <> TAlphaColorRec.Null then
        TShape(AControl).Fill.Color := BodyColor;

    // положение
    AControl.Position.X := InterpolateSingle(DP.Left, CP.Left, AProgress) * ScaleFactor;
    AControl.Position.Y := InterpolateSingle(DP.Top, CP.Top, AProgress) * ScaleFactor;

    // размер применяем ко всем, кроме "опорных" точек
    if Pos('Center', AControl.Name) = 0 then
    begin
      AControl.Width := InterpolateSingle(DP.Width, CP.Width, AProgress) * ScaleFactor;
      AControl.Height := InterpolateSingle(DP.Height, CP.Height, AProgress) * ScaleFactor;
    end;

    // поворот
    AControl.RotationAngle := InterpolateSingle(DP.RotationAngle, CP.RotationAngle, AProgress);
  end;
end;

(Про ScaleFactor я писал в предыдущей заметке.)

Как видите, ничего тут сложного нет.

Теперь создаём TFloatAnimation-объект, назовём его faOpenClose:

  object faOpenClose: TFloatAnimation
    Duration = 0.300000000000000000
    OnProcess = faOpenCloseProcess
    PropertyName = 'OpenCloseValue'
  end

Далее у фреймы:

type
  TframeRobot = class(TNPC)
  ..
    procedure faOpenCloseProcess(Sender: TObject);
  private
    FOpenCloseValue: Single;
    FClosed: Boolean;
  ..
  public
    property OpenCloseValue: Single read FOpenCloseValue write FOpenCloseValue;
  ..
procedure TframeRobot.faOpenCloseProcess(Sender: TObject);
begin
  ProcessOpenCloseAnimation(FOpenCloseValue);
end;

И осталось запустить анимацию в прямом:

procedure TframeRobot.CloseWithAnimation;
begin
  FClosed := True;
  faOpenClose.Stop;
  faOpenClose.StartValue := 0;
  faOpenClose.StopValue := 1;
  faOpenClose.Start;
end;

, либо обратном направлении:

procedure TframeRobot.OpenWithAnimation;
begin
  FClosed := False;
  faOpenClose.Stop;
  faOpenClose.StartValue := 1;
  faOpenClose.StopValue := 0;
  faOpenClose.Start;
end;

 

Ну и теперь, мы можем реализовать метод ScaleControls, который в прошлой заметке остался не реализованным. Выглядит он у меня так:

procedure TframeRobot.ScaleControls;
  procedure ScalePositions;
  begin
    // устанавливаем расположение контролов в зависимости от текущего состояния
    // (процедура анимации учтёт ScaleFactor)
    if FClosed then
      ProcessOpenCloseAnimation(1)
    else
      ProcessOpenCloseAnimation(0)
  end;

  procedure ScaleStrokeThickness;
  var
    i: Integer;
  begin
    for i := 1 to Length(Controls) - 1 do
      if Controls[i] is TShape then
        if TShape(Controls[i]).Stroke.Kind = TBrushKind.bkSolid then
          TShape(Controls[i]).Stroke.Thickness := 12 * ScaleFactor;
  end;

  procedure RecalcBodyRadius;
  begin
    Body.XRadius := (LegLeft.LocalRect.Width - LegLeft.Stroke.Thickness) / 2;
    Body.YRadius := Body.XRadius;
  end;

  procedure RecalcAntenaRadius;
  begin
    AntenaLeft.XRadius := (AntenaLeft.Width - AntenaLeft.Stroke.Thickness) / 2;
    AntenaLeft.YRadius := AntenaLeft.XRadius;
    AntenaRight.XRadius := AntenaLeft.XRadius;
    AntenaRight.YRadius := AntenaLeft.XRadius;
  end;

begin
  ScalePositions;
  ScaleStrokeThickness;
  RecalcBodyRadius;
  RecalcAntenaRadius;
end;

 

Заметка получилась больше, чем я предполагал, поэтому закругляюсь. Напоследок скажу, что кроме анимации по смене состояния, у меня будет ещё как минимум одна анимация. Анимация-приветствие. Всё вместе выглядит так:

TapTap

Читать на сайте автора.

Delphi плюс Android? Есть идея! Tap#3

Эта заметка является продолжением предыдущих. Tap#1, Tap#2.

И прежде чем приступить к анимации, стоит определиться с масштабированием персонажа. То есть необходимо настроить фрейм таким образом, чтобы, задавая его размеры “сверху”, все объекты персонажа автоматически подгоняли свой размер, и персонаж (по своим внешним размерам) оказался вписанным во фрейм.

Сделать это можно очень просто. Для самой фреймы устанавливаем свойство Align = alFit (чтобы сохранить пропорцию у соотношения сторон фреймы), а всем контролам на фрейме –  Align = alScale.  Тогда, при изменении размеров фреймы, контролы будут масштабироваться автоматически. Оговорюсь, что размеры и положения контролов в FM задаются вещественными числами, поэтому масштабирование будет плавным.

Однако есть такой нюанс: платформа FM масштабирует контролы относительно их текущих размеров. Что это значит? А это значит, что при задании маленьких размеров фреймы, наш персонаж может оказаться очень маленьким. Настолько маленьким, что при последующем увеличении фреймы персонаж перестанет масштабироваться.

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

  • один раз при запуске приложения;
  • при повороте экрана.

Однако я хочу, чтобы приложение можно было так же запускать и в окне в Windows. А в оконном режиме существует ненулевая вероятность того, что пользователь захочет “поиграться” с размерами окна и уменьшит его до очень маленького размера…

Поэтому масштабирование всех контролов я буду делать вручную, в обработчике OnResize. При этом, я хочу ориентироваться на исходный размер контролов, т.е. после загрузки фреймы из fmx-ресурса, необходимо сохранить позиции и размеры контролов.

Для этого создаём простую запись:

type
  TDesignPosition = record
    Left, Top, Width, Height: Single;
    RotationAngle: Single;
    FontSize: Single;
  end;

И класс-наследник от TList<T>:

type
  TDesignPositions = class(TList<TDesignPosition>)
  private
    function IndexByControl(AControl: TControl): Integer;
    function GetItemByControl(AControl: TControl): TDesignPosition;
  public
    procedure AddByControl(AControl: TControl);
    property ItemByControl[AControl: TControl]: TDesignPosition read GetItemByControl; default;
  end;

{ TDesignPositionsList }

function TDesignPositions.IndexByControl(AControl: TControl): Integer;
begin
  Result := AControl.Tag;
end;

function TDesignPositions.GetItemByControl(AControl: TControl): TDesignPosition;
begin
  Result := Items[IndexByCOntrol(AControl)];
end;

procedure TDesignPositions.AddByControl(AControl: TControl);
var
  DP: TDesignPosition;
begin
  DP.Left := AControl.Position.X;
  DP.Top := AControl.Position.Y;
  DP.Width := AControl.Width;
  DP.Height := AControl.Height;
  DP.RotationAngle := AControl.RotationAngle;
  if AControl is TTextControl then
    DP.FontSize := TTextControl(AControl).Font.Size;
  Add(DP);
end;

Тут TDesignPositions – это простой список записей типа TDesignPosition. Так как я не планирую в Run-Time добавлять или удалять контролы, то (для простоты) соответствие между элементом в списке и контролом задаётся свойством TControl.Tag.

Заполняется список так:

procedure TNPC.SavePositionsTo(AList: TDesignPositions);
var
  i: Integer;
begin
  AList.Clear;
  i := 0;
  ForEach(
    procedure (AControl: TControl)
    begin
      AControl.Tag := i;
      Inc(i);
      AList.AddByControl(AControl);
    end
  );
end;

Здесь TNPC – это базовый фрейм для персонажа, от него будут наследоваться все персонажи в будущем. ForEach – это специальный метод, который вызывает переданную анонимную функцию и перебирает все контролы фреймы. Метод я объявил в специальном хелпере, т.к. планирую его использовать ещё не один раз. (Идею с хелпером я подсмотрел у Bioan Mitov.)

type
  TControlHelper = class helper for TControl
  public
    procedure ForEach(AProc: TProc<TControl>);
  end;

{ TControlHelper }

procedure TControlHelper.ForEach(AProc: TProc<TControl>);
var
  i: Integer;
begin
  AProc(Self);
  for i := 0 to ControlsCount - 1 do
    Controls[i].ForEach(AProc);
end;

Т.к. мы контролы вкладывали друг в друга, то здесь используется рекурсия. Метод ForEach сначала вызывает указанную процедуру, а потом перебирает дочерние контролы. Таким образом, в список TDesignPositions сначала добавляются размеры самой фреймы, а потом уже размеры её контролов. Я этим воспользуюсь, чтобы определить коэффициент масштабирования:

procedure TNPC.RecalcScaleFactor;
var
  dw, dh: Single;
begin
  dw := Width / FDesignPositions.Items[0].Width;
  dh := Height / FDesignPositions.Items[0].Height;
  if dw < dh then
    FScaleFactor := dw
  else
    FScaleFactor := dh;
end;

На текущий момент базовый фрейм для персонажа у меня выглядит примерно так:

  TNPC = class(TFrame)
  private
    FControls: TControls;
    FDesignPositions: TDesignPositions;
    FScaleFactor: Single;
    FIsLoaded: Boolean;
  protected
    procedure Loaded; override;
    procedure Resize; override;
    procedure SaveDesignPositions; virtual;
    procedure SaveControls;
    procedure ScaleControls; virtual;
  public
    procedure SavePositionsTo(AList: TDesignPositions);
    property IsLoaded: Boolean read FIsLoaded;
    property ScaleFactor: Single read FScaleFactor;
    // Controls - простой массив для быстрого доступа ко всем контролам на фрейме
    //  нулейвой элемент соответствует самой фрейме
    //  у каждого контрола свойство Tag соответствует индексу в этом массиве
    property Controls: TControls read FControls;
    // DesignPositions - координаты и размеры всех контролов, заданных в дизайнере
    //  нулейвой элемент соответствует самой фрейме
    property DesignPositions: TDesignPositions read FDesignPositions;
  end;
..
procedure TNPC.Loaded;
begin
  inherited;
  SaveDesignPositions;
  SaveControls;
  FIsLoaded := True;
end;

procedure TNPC.SaveDesignPositions;
begin
  FDesignPositions := TDesignPositions.Create;
  SavePositionsTo(FDesignPositions);
end;

procedure TNPC.SaveControls;
var
  i: Integer;
begin
  SetLength(FControls, DesignPositions.Count);
  i := 0;
  ForEach(
    procedure (AControl: TControl)
    begin
      FControls[i] := AControl;
    end
  );
end;

procedure TNPC.Resize;
begin
  inherited;

  if IsLoaded then
  begin
    BeginUpdate;
    try
      RecalcScaleFactor;
      ScaleControls;
    finally
      EndUpdate;
    end;
  end;
end;

procedure TNPC.ScaleControls;
begin
  // do nothing
end;

Самый интересный метод – ScaleControls, он отвечает за масштабирование. Как раз здесь можно использовать DesignPositions и ScaleFactor. Но на уровне базовой фреймы я решил, что он будет пустым, т.к. у персонажа будет ещё анимация и будут внутренние состояния. И именно от текущего состояния будут рассчитываться положения объектов. Другими словами, свойствами DesignPositions и ScaleFactor мы будем активно пользоваться в наследниках.

А в следующий раз уже поговорим об анимации.

UPD. Для удобства я сделал ещё такую вещь: все контролы с фреймы сохранил в линейный массив Controls.

Читать на сайте автора.

Delphi плюс Android? Есть идея! Tap#2

Эта заметка является продолжением предыдущей. Сегодня я расскажу, как я рисовал персонаж для будущей игры.

На самом деле, я перепробовал несколько вариантов. Сначала даже пробовал в 3D – я подумал, что можно сделать что-то красивое-объёмное, потом поиграться с тенями и освещением… но это занимает очень много времени, а в результате, при большом количестве объектов, на Android-устройстве всё это подтормаживает.

В 2D я тоже пробовал по-разному… От растровых картинок отказался сразу. Потому что векторную графику можно и отмасштабировать без потери качества, и анимировать отдельные объекты труда не составит. От своего собственного объекта-наследника от TShape, в котором закодировать (хардкорно) всю прорисовку – тоже отказался, т.к. это довольно трудоёмко. В конце-концов решил, что пусть это будет набор из готовых Shape-компонентов, размещённых в отдельной фрейме. Это и проще, и нагляднее. (Спасибо разработчикам FM, что вспомнили про TFrame, т.к. в Delphi XE2 в FireMonkey фрейм не было.)

Дальше я нашёл в интернете картинку с зелёным роботом, и положил её на фрейму в обычный TImage. Картинку выбрал с высоким разрешением, чтобы легче было накладывать на неё объекты. Затем, через меню Tools Options… –> Form Designer, выключил выравнивание по сетке в дизайнере, сняв флажок Snap to grid.

Теперь начинаем формировать персонаж. Тут тоже всё просто. Кидаем поверх картинки TRectangle, подгоняем размер до пикселя. Создаём следующий объект, кидаем поверх картинки и т.д. Чтобы легче было подогнать, делаем объект полупрозрачным, задав в его свойствах Opacity = 0,5.

Т.к. я планирую объекты анимировать, т.е. двигать (меняя Position), крутить (меняя RotationAngle), сжимать-растягивать (меняя Width и Height), то я применил такую хитрость: объекты кидаются не на фрейму, а в промежуточный TRectangle (размером в одну точку). Это нужно чисто для удобства, чтобы координаты объекта рассчитывались не относительно фреймы, а относительно некоторой точки (в 3D для этих целей есть TDummy, аналога для 2D я не нашёл). На картинке из предыдущей заметки Вы можете увидеть объекты с именами RoboCenter, LegRightCenter и т.п. – это как раз и есть “опорные” для анимации точки.

Когда робот готов – фоновая картинка больше не нужна. (Но я удалять её не стал, а просто создал отдельную фрейму, на которую скопировал получившегося персонажа.) С этой новой фреймой и буду работать дальше.

Теперь мне нужно получившийся персонаж научить каким-нибудь анимациям. Об этом – в следующий раз.

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

Читать на сайте автора.

Delphi плюс Android? Есть идея! Tap#1

В рамках обозначенного конкурса решил я что-нибудь смастерить на FM под Андроид.

Забегая чуточку вперёд обозначу, что буду делать незаурядное приложение – игру на логическое мышление, обязательно с анимацией. В игре будет персонаж (или несколько персонажей?), в зависимости от своего состояния персонаж будет выглядеть по-разному. Соответственно переходы из состояния в состояние будут анимированными. Приложение будет в 2D, т.к. игровое поле само по себе плоское. А пока, буду описывать сам процесс…

Итак, начинаем мастерить. Для начала надо дать какое-то черновое название проекту. Я решил, что пусть это будет “Тап-Тап!” (от английского “Tap” – тап-тап в экран пальцем). Создаём проект в Delphi: File New FireMonkey Mobile Application – Delphi, и далее выбираем Blank Application. Сохраняем проект: для формы задаём имя frmMain, для проекта – TapTap. Теперь сделаем несколько телодвижений, чтобы облегчить себе работу.

Во-первых, понятно, что я приложение буду запускать очень много раз, каждый раз делать это на Android-устройстве (или эмуляторе) – очень накладно по времени. Поэтому сразу выбираем в Project Manager’е Target Platform –> 32 bit Windows. То есть, хоть мы и указали, что приложение будет мобильным, но сначала компилировать и отлаживать мы будет прям в ОС, в которой установлена Delphi. В будущем, достаточно будет переключить Target Platform обратно на Android и выбрать нужное Android-устройство. По-моему, это очень удобно. (Вот она, сила RAD!)

Во-вторых, в главной форме приложения я отключу эту рамочку вокруг формы со скином девайса, под который я якобы разрабатываю интерфейс. Отключу по двум причинам: а) мне банально эта рамочка не нравится, б) эта рамочка фиксирует размер формы. В конце-концов, я хочу, чтобы приложение было универсальным и не зависело от форм-фактора конечного устройства. По крайней мере, пока у нас черновик, я вполне могу захотеть изменять размер формы как угодно и размещать на ней временные объекты.

Отключить довольно просто: правой кнопкой мыши на форму, выбираем View as Text и меняем свойство DesignerMobile с True на False. Далее жамкаем Alt+F12 – возвращаемся обратно к дизайнеру.

В-третьих, конструируя игрового персонажа и настраивая его анимацию, у нас будет много объектов. Я буду постоянно экспериментировать со свойствами объектов, мне надо будет постоянно между ними переключаться. Для удобства я расположу панели Structure и Object Inspecotr рядом друг с другом, слева-направо. Переключаться же между Project Manager’ом и Tool Palette я буду гораздо реже, поэтому их расположим друг-в-друге, вкладками. Всё это у меня выглядит так:

image

 

Как видите на картинке, персонаж у нас вполне себе предсказуемый для Android-приложения :). Чуть попозже я расскажу, как я его рисовал.

А пока сделаем ещё одно действие – настроим иконку для приложения. Можно, конечно, отложить это на потом; иконка – это творческий элемент приложения, когда надоело писать код, а сделать что-то надо, самое-то отвлечься на иконку.

Для создания иконок я использую приложение AWicons Pro. Приложение платное, но оно того стоит. Разобраться в нём труда не составит, поэтому процесс создания иконок останется за кадром – на всё про всё у меня ушло не более трёх минут. Иконки, для удобства, сохраняем в отдельный каталог, для Windows-сборки нам нужен файл с расширением .ico (причём один файл будет содержать внутри себя несколько иконок разных размеров), а для Android’а нам нужно несколько png-файлов с разным разрешением.

HINT: Кстати сказать, если для Android платформы не указать иконки, то приложение под Android собрать не получится.

Далее связываем иконки с приложением через меню Project Options –> Application. Выбираем Target и прописываем связи, у меня это выглядит так:

image

На заметку: диск F у меня ссылается на каталог с проектами Delphi. Очень удобно.

Пожалуй, на этом я закончу. Продолжение следует.

Читать на сайте автора.