Записи с пометкой ·

Delphi

·...

SourceOddity => FixInsight

Я уже писал о своем проекте SourceOddity. Он у меня, к сожалению, немного забуксовал. Но наконец закончилось лето, прошли отпуска и я вернулся к работе. И в связи с этим у меня есть три новости. Надеюсь, все три — хорошие.

1) Я переименовал SourceOddity в FixInsight. Считаю, что новое название лучше передает суть;
2) Я выложил часть проекта в open source;
3) Я наконец довел проект до состояния, когда его вроде бы не стыдно показать публике.

И так, встречайте FixInsight 2014.10beta. Первая публичная версия :)

Я благодарю людей, которые все это время помогали, советовали, тестировали анализатор на разных примерах кода. Эта помощь неоценима. Без вас я бы не справился.

Первые шаги SourceOddity

Хочу рассказать о том, на что я потратил почти все свободное время в начале этой весны. Это статический анализатор кода для Delphi. Назвал я его скромно — SourceOddity. Очень краткий анонс и чуть больше скриншотов доступны по ссылке.

SourceOddity Screenshot

Мотивация достаточно проста.

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

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

Иногда, вовремя обнаружив подобную ситуацию, можно сэкономить много сил и нервов:

SourceOddity Screenshot

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

Статический анализ безусловно полезен.

Я осознаю, что статические анализаторы изобрел не я и SourceOddity не первая ласточка, но я думаю, что у меня есть некоторые идеи и опыт, которые позволят сделать более пригодный к повседневному использованию инструмент.

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

DelphiSpec неделю спустя

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

Давайте я вам вкратце расскажу о том, что теперь умеет DelphiSpec. Базовые вещи озвучены в предыдущем посте, обсудим, что появилось нового. А нового довольно много.

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

В первом посте я показывал вам такой синтаксис:

    // пример сценария: "Given user ROOT exists"    
    [Given_('user (.*) exists')]
    procedure MyProcName(const Name: string);

Он отлично работает и сейчас, но теперь можно сделать еще проще — без атрибутов:

 
    // пример сценария: "Given user ROOT exists"
    procedure Given_user_name_exists(const Name: string);

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

Также теперь поддерживаются не только строковые параметры. Такой код будет работать:

 
    // пример сценария: "Given I have 3 apples"
    procedure Given_I_have_N_apples(N: Integer);

Можно передать даже массив:

 
    // пример сценария: "Given I have a list 1,2,3,4"
    procedure Given_I_have_a_list_Ns(Ns: TArray<Integer>);

И даже массив записей:

 
type   
  TUserInfo = record
    Name: string;
    Password: string;
    Id: Integer;
  end;

  // пример сценария
  // Given users exist:
  //  | id | name  | password |
  //  | 1  | Roman | pass1    |  
  //  | 2  | Other | pass2    |
  procedure Given_users_exist(Table: TArray<TUserInfo>);

Последний пример демонстрирует интересный тип данных Gherkin — data table. Обратите внимание, что такая таблица в метод-обработчик передается как массив обычных типизированных записей.

Всю работу с типами берет на себя DelphiSpec, вам об этом думать не придется. Библиотека может это делать благодаря магии RTTI. Не верьте, когда вам говорят, что волшебства не бывает :)

Еще один тип данных, который поддерживает Gherkin и теперь реализован в DelphiSpec — это «многострочные строки» в стиле Python. Такая строка обрамляется троекратными двойными кавычками сверху и снизу, а в метод-обработчик передается как обычный string:

 Given I have a blacklist:
   """
   m@mail.com
   123@mail.com
   """

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

Добавлена поддержка блока background. Этот блок позволяет описать контекст выполнения сценариев — он выполняется перед каждым сценарием.

Feature: Accounts

Background:
  Given users exist:
    | id | name  | password |
    | 1  | Roman | pass1    |  
    | 2  | Other | pass2    |

Scenario: Correct Login
  Given my name is "Roman"
    And my password is "pass1"
  When I login
  Then I have access to private messages

Scenario: Incorrect Login
  Given my name is "Roman"
    And my password is "pass2"
  When I login
  Then access denied

Также теперь можно описывать не только отдельные сценарии, но и более общие структуры сценариев:

Scenario Outline: Add two numbers
  Given I have entered <num1> in calculator
    And I have entered <num2> in calculator
  When I press Add
  Then the result should be <sum> on the screen
  
  Examples:
    | num1 | num2 | sum |
    |  1   |  2   |  3  | 
    |  4   |  5   |  9  |
    |  3   |  1   |  4  |

Такая структура будет развернута в несколько сценариев — по одному на каждую строку таблицы. В дереве DUnit подобная ситуация будет выглядеть вот так:

То есть создан не один тест «Add two numbers», а три. По одному для каждого случая.

Полный код примеров лежит вместе с библиотекой на github. Как я и обещал, DelphiSpec я не забрасываю и развиваю. Впереди мне предстоит тестирование и написание хотя бы краткой документации. А там уже и 1.0 не за горами :)

WordLines — работа на конкурс

Update: Теперь игра есть и в Google Play.

Много лет назад я написал небольшую игрушку — WordLines. Старожилы возможно помнят.

В связи с тем, что с недавнего времени Delphi поддерживает Android, а также в связи с подвернувшимся под руку конкурсом, стало интересно сделать то же самое для планшета. Хорошо получилось или не очень — судите сами. Главное помнить, что это не коммерческий продукт, а результат эксперимента.

На удивление все прошло довольно просто. Это непередаваемое ощущение — компилировать и запускать все то же самое, но на новом устройстве. Без шероховатостей конечно не обошлось и именно с процессом работы над WordLines связаны некоторые мои недавние посты, но в целом я остался доволен. Основные сложности были связаны не с Delphi, а с отсутствием у меня опыта разработки мобильных приложений — из-за разных форм-факторов заставить одну и ту же форму выглядеть одинаково на разных устройствах не так уж и просто (возможно, это требует привычки).

Картинка мелкая, да, но вот такое уж у меня на планшете разрешение, извините :)

Также поигрался с рекламируемым TRESTClient. О нем сказать особенно нечего. Он работает, все как обещали.

С помощью REST организованно взаимодействие с небольшим веб-сервисом. Этот функционал позволяет игроку отправить свои результаты на сервер и получить от него информацию о рекордном результате среди всех игроков. Так же есть онлайн-таблица рекордов. Уделять внимание дизайну мне было некогда, так что не пугайтесь :)

Бэкенд сделан с помощью эмбаркадеровского HTML5 Builder (люблю экстрим). Впечатления неоднозначные. Возможно, когда-нибудь о них расскажу.

Плюс ко всему бесплатно я получил версию WordLines для Mac и iOS. Это круто.

То есть обещанная 100% переносимость кода между платформами реально работает, нас не обманули. У меня во всем проекте буквально полтора IFDEF’а. Убедитесь сами.

Напоследок в двух словах расскажу о самой игре, если вдруг не все очевидно.

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

В WordLines на поле выпадают буквы и перемещая эти буквы нужно составлять слова, которые затем исчезают. Но исчезают они не автоматически, а после нажатия кнопки «Убрать слова» (в этой версии — зеленая стрелка под списком найденных слов). За убранные с поля слова начисляются очки. Если убирать слова автоматически, то играть не так интересно, как мне кажется — буквы пропадают внезапно и становится труднее составлять длинные слова.

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

Ну и конечно же ссылки: .exe (win32), .apk и исходники.

Анонс DelphiSpec

В блоге я уже несколько раз писал о предметно-ориентированных языках (DSL). Эта моя заметка будет куда более приближена к реальной жизни, так что не закрывайте пока страницу.

Не буду повторять сам себя, процитирую Фаулера:

That said, I do think that the greatest potential benefit of DSLs comes when business people participate directly in the writing of the DSL code. The sweet spot, however is in making DSLs business-readable rather than business-writeable. If business people are able to look at the DSL code and understand it, then we can build a deep and rich communication channel between software development and the underlying domain.

Наибольший потенциал DSL он видит в том, что текст на предметно-ориентированном языке могут писать и — главное — читать эксперты предметной области, а вовсе не программисты. Благодаря этому можно обеспечить совершенно другой уровень коммуникации между разработкой ПО и предметной областью.

В этом посте я хотел бы анонсировать библиотеку DelphiSpec. Не буду скрывать, что вдохновил меня проект Cucumber. То есть фактически библиотека является Delphi-реализацией языка Gherkin (переводится как «корнишон»). Реализация не полная (считайте альфа-версией). Некоторых возможностей пока нет, но проект будет развиваться. Мне он интересен.

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

Feature: Calculator

Scenario: Add two numbers
  Given I have entered 50 in calculator
    And I have entered 70 in calculator
  When I press Add
  Then the result should be 120 on the screen

Scenario: Add two numbers (fails)
  Given I have entered 50 in calculator
    And I have entered 50 in calculator
  When I press Add
  Then the result should be 120 on the screen

Scenario: Multiply three numbers
  Given I have entered 5 in calculator
    And I have entered 5 in calculator
    And I have entered 4 in calculator
  WHEN I press mul
  Then the result should be 100 on the screen

Подчеркну, это не тесты, это сценарии. То есть настоящий Behavior-Driven Development (BDD) в дополнение к известному Test-Driven Development (TDD). Такие сценарии могут использоваться как часть постановки задачи, как часть документации к проекту и конечно как тесты. С помощью сценариев не принято описывать внутренние особенности системы, сценарии описывают как происходящее выглядит для пользователя.

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

Вернемся к примеру с калькулятором. Результат выполнения сценариев в DUnit будет выглядеть так:
dunit-delphispec

Как это работает? Сам язык Gherkin очень мал. По сути единственная его функция — это описание связок Given-When-Then (по-русски: дано-если-то). Есть несколько ключевых слов, все остальное кастомизируется.

В примере мы тестируем очень простой класс-калькулятор. Здесь я его код выкладывать не буду, смотрите его по ссылке. Интересно другое: как программе понять и выполнить команды вроде «Given I have entered 50 in calculator»? Вот тут в дело вступает вспомогательный класс описывающий эти шаги (так называемые «step definitions»). Выглядит класс так:

unit SampleCalculatorStepDefs;

interface

uses
  SampleCalculator, DelphiSpec.Attributes, DelphiSpec.StepDefinitions;

type
  [_Feature('calculator')]
  TSampleCalculatorSteps = class(TStepDefinitions)
  private
    FCalc: TCalculator;
  public
    procedure SetUp; override;
    procedure TearDown; override;

    [_Given('I have entered (.*) in calculator')]
    procedure EnterInt(Value: string);

    [_When('I press Add')]
    procedure AddInt;

    [_When('I press Mul')]
    procedure MulInt;

    [_Then('the result should be (.*) on the screen')]
    procedure TestResult(Value: string);
  end;

implementation

uses
  System.SysUtils, TestFramework, DelphiSpec.Core;

{ TSampleCalculatorSteps }

procedure TSampleCalculatorSteps.AddInt;
begin
  FCalc.Add;
end;

procedure TSampleCalculatorSteps.EnterInt(Value: string);
begin
  FCalc.Push(Value.ToInteger);
end;

procedure TSampleCalculatorSteps.MulInt;
begin
  FCalc.Mul;
end;

procedure TSampleCalculatorSteps.SetUp;
begin
  FCalc := TCalculator.Create;
end;

procedure TSampleCalculatorSteps.TearDown;
begin
  FCalc.Free;
end;

procedure TSampleCalculatorSteps.TestResult(Value: string);
begin
  if FCalc.Value <> Value.ToInteger then
    raise ETestFailure.Create('Incorrect result on calculator screen');
end;

initialization
  RegisterStepDefinitionsClass(TSampleCalculatorSteps);

end.

То есть по сути у каждого шага есть тип (Given/When/Then) и регулярное выражение, описывающее остальную часть шага. Регулярные выражения помогают выделить из шагов параметры, которые будут переданы в методы класса для выполнения соответствующей части сценария.

Чтобы вся эта система заработала, остается только добавить в dpr-файл вызов процедуры, получающей на вход путь к файлам с описанием сценариев и превращающей эти сценарии в настоящие тесты для DUnit.

begin
  PrepareDelphiSpecs(TPath.Combine(ExtractFilePath(Application.ExeName), 'features'), True, 'EN');
  DUnitTestRunner.RunRegisteredTests;
end.

Последний параметр («EN») позволяет указать буквенный код языка, на котором написаны сценарии. Это еще одна интересная возможность Gherkin. Так как в общем-то нет разницы текст на каком языке обрабатывать с помощью регулярных выражений, то грех было не перевести на разные языки и несколько ключевых слов, для чего служит соответствующий XML-файл.

То есть ничто не мешает записывать сценарии даже вот так:

Функционал: Калькулятор

Сценарий: Сложить два числа
  Допустим я ввожу 50 на калькуляторе
         И я ввожу 70 на калькуляторе
  Когда я нажимаю СЛОЖИТЬ
  Тогда результат на экране должен быть 120

Это пока первые шаги, но постепенно я планирую реализовать все возможности Gherkin.

Кроссплатформенность и модальные формы

Пару месяцев назад Марко Канту порадовал нас информацией о том, что Android не поддерживает модальные формы и поэтому теперь у нас в Delphi XE5 будет два разных метода ShowModal:

    function ShowModal: TModalResult; overload;
    procedure ShowModal(const ResultProc: TProc<TModalResult>); overload;

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

var
  dlg: TForm1;
begin
  dlg := TForm1.Create(nil);
  try
    // select current value, if avaialble in the list
    dlg.ListBox1.ItemIndex := dlg.ListBox1.Items.IndexOf(edit1.Text);
    if dlg.ShowModal = mrOK then
      // if OK was pressed and an item is selected, pick it
      if dlg.ListBox1.ItemIndex >= 0 then
        edit1.Text := dlg.ListBox1.Items [dlg.ListBox1.ItemIndex];
  finally
    dlg.Free;
  end;
end;

нужно писать такой:

var
  dlg: TForm1;
begin
  dlg := TForm1.Create(nil);
  // select current value, if avaialble in the list
  dlg.ListBox1.ItemIndex := dlg.ListBox1.Items.IndexOf(Edit1.Text);
  dlg.ShowModal(
    procedure(ModalResult: TModalResult)
    begin
      if ModalResult = mrOK then
      // if OK was pressed and an item is selected, pick it
        if dlg.ListBox1.ItemIndex >= 0 then
          edit1.Text := dlg.ListBox1.Items [dlg.ListBox1.ItemIndex];
      dlg.DisposeOf;
    end);
end;

Идея ясна и мотивация вполне понятна. Код, который должен выполниться после закрытия формы, перемещается в анонимную процедуру, которая и будет вызвана в нужный момент.

Этот код будет работать на всех платформах. Спасибо, Марко!

Но есть нюанс. Давайте заглянем внутрь новой версии ShowModal:

procedure TCommonCustomForm.ShowModal(const ResultProc: TProc<TModalResult>);
begin
  FResultProc := ResultProc;
  Show;
end;

Такая форма будет одинаково немодальна на всех платформах! То есть, если мы хотим писать кроссплатформенных код, нам предлагают либо усыпать код IFDEF-ами, либо отказаться от использования модальных форм совсем. Как-то это нехорошо.

Не знаю как у вас, но у меня в коде часто встречается что-то такое:

with TForm1.Create(nil) do
try
  ShowModal;
finally
  Free;
end;

С учетом андроида этот код можно переписать следующим образом:

var
  Frm: TForm1;
begin
  Frm := TForm1.Create(nil);
  ShowModal(procedure (Res: TModalResult) begin Frm.DisposeOf; end);
end;

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

var
  Frm: TForm1;
begin
  Frm := TForm1.Create(nil);
  {$IFDEF ANDROID}
    Frm.ShowModal(procedure (Res: TModalResult) begin end);
  {$ELSE}
    try
      Frm.ShowModal;
    finally
      Frm.Free;
    end;
  {$ENDIF}
end;

Этот код обеспечит модальность формы в обычных случаях и «всевдомодальность» на андроиде. Но как-то это не очень красиво. Не люблю применять IFDEF-ы по пустякам. Таких мест в коде может быть много.

Подведу итог. В связи с вышеизложенным у меня есть два риторических вопроса.

1) Почему нельзя было новый ShowModal реализовать хотя бы так:

procedure TCommonCustomForm.ShowModal(const ResultProc: TProc<TModalResult>);
begin
  {$IFDEF ANDROID}
    FResultProc := ResultProc;
    Show;
  {$ELSE}
    ResultProc(ShowModal);
  {$ENDIF}
end;

?

2) Если такой подход все-таки чем-то не устраивает, то зачем было называть новый метод ShowModal, ведь он с модальными формами никак не связан?

Awaitable-значения в Delphi

В последнее время я исследую возможности выполнения параллельных и асинхронных операций в Delphi. В качестве одного из результатов родился класс TAwaitable. Код нарочито простой. С одной стороны хочется продемонстрировать идею, хотя она в целом очевидна, с другой стороны — получить свою порцию конструктивной критики. Я довольно мало сталкивался с многопоточностью в Delphi, так что будет здорово, если кто-то укажет на недочеты в реализации, если они есть.

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

procedure TForm1.Button1Click(Sender: TObject);
var
  WaitFor2, WaitFor3: IAwaitable<Integer>;
  Log: string;
begin
  Memo1.Lines.Add(DateTimeToStr(Now) + ' Start testing...');

  WaitFor2 := TAwaitable<Integer>.Create(
    function : Integer begin
      Sleep(2000);
      Result := 2;
    end);

  WaitFor3 := TAwaitable<Integer>.Create(
    function : Integer begin
      Sleep(3000);
      Result := 3;
    end);

  Memo1.Lines.Add(DateTimeToStr(Now) + ' One more action');
  Log := IntToStr(WaitFor2.Value + WaitFor3.Value);
  Memo1.Lines.Add(DateTimeToStr(Now) + ' And finally the result is... ' + Log);
  Log := IntToStr(WaitFor2.Value + WaitFor3.Value);
  Memo1.Lines.Add(DateTimeToStr(Now) + ' Cached result... ' + Log);
end;

Здесь запускаются в параллельное выполнение две функции. Одна выполняется 2 секунды, другая — 3. И в одном случае результат вычислений — 2, а в другом соответственно — 3. В качестве итога нам нужна сумма двух этих значений. Последовательное вычисление заняло бы, очевидно, 5 секунд. Но данный код выполняется за 3 секунды, благодаря тому, что он выполняется параллельно.

19.11.2013 23:33:04 Start testing...
19.11.2013 23:33:04 One more action
19.11.2013 23:33:07 And finally the result is... 5
19.11.2013 23:33:07 Cached result... 5

Повторное обращение к значению TAwaitable.Value выполняется быстрее, так как вычисления уже завершены.

Код самого класса довольно прост.

unit Awaitable;

interface

uses
  System.Classes;

type
  TAwaitableProc<T> = reference to function : T;

  IAwaitable<T> = interface
    function Value: T;
  end;

  TAwaitable<T> = class(TInterfacedObject, IAwaitable<T>)
  private
    FThread: TThread;
    FValue: T;
  public
    constructor Create(AwaitableProc: TAwaitableProc<T>); reintroduce;
    destructor Destroy; override;

    function Value: T;
  end;

implementation

{ TAwaitable<T> }

constructor TAwaitable<T>.Create(AwaitableProc: TAwaitableProc<T>);
begin
  inherited Create;

  FThread := TThread.CreateAnonymousThread(
    procedure begin
      FValue := AwaitableProc;
    end);

  FThread.FreeOnTerminate := False;
  FThread.Start;
end;

destructor TAwaitable<T>.Destroy;
begin
  FThread.Free;
  inherited;
end;

function TAwaitable<T>.Value: T;
begin
  FThread.WaitFor;
  Result := FValue;
end;

end.

Ну вот как-то так. Этот класс есть куда функционально развивать, но пока не хочется заходить слишком далеко.

update
День не прошел зря, я понял свой недочет. Вместо критических секций лучше использовать уже имеющийся в классе TThread метод WaitFor. Поэтому я немного изменил код.

Работа с TCanvas в Delphi под Android

Рисование на TCanvas в Delphi XE5 под Android как оказалось имеет некоторые особенности, которые по-началу ввели меня в легкое замешательство и которыми хотелось бы поделиться.

Нарисуем несколько параллельных линий.

Здесь хотелось бы отвлечься и заметить, что под Windows значение Stroke.Kind по умолчанию равно bkSolid, а под Android — bkNone. То есть, если явно не установить значение Stroke.Kind, то линии будут видны под Windows, но не под Android. Не знаю, чем такой подход может быть вызван.

procedure TForm2.FormPaint(Sender: TObject; Canvas: TCanvas;
  const ARect: TRectF);
var
  I: Integer;
begin
  if Canvas.BeginScene then
    try
      Canvas.Stroke.Thickness := 1.5;
      Canvas.Stroke.Kind := TBrushKind.bkSolid;
      Canvas.Fill.Color := TAlphaColorRec.Black;
      Canvas.Fill.Kind := TBrushKind.bkSolid;

      for I := 1 to 9 do
        Canvas.DrawLine(PointF(50 + I * 25, 0), PointF(50 + I * 25, ClientHeight), 1);
    finally
      Canvas.EndScene;
    end;
end;

Вот, что у меня получилось:

Очевидно, некоторые линии получились толще других. Тот же код под Windows работает идеально.

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

Если все же нужно нарисовать одинаковые линии, то можно их сдвинуть на половину толщины, чтобы гарантировать их попадание в физические пиксели.

Именно так решает проблему компонент TLine и его предок TShape:

function TShape.GetShapeRect: TRectF;
begin
  Result := LocalRect;
  if FStroke.Kind <> TBrushKind.bkNone then
    InflateRect(Result, -(FStroke.Thickness / 2), -(FStroke.Thickness / 2));
end;

procedure TLine.Paint;
begin
  case FLineType of
    TLineType.ltTop:
      Canvas.DrawLine(GetShapeRect.TopLeft, PointF(GetShapeRect.Right, GetShapeRect.Top),
        AbsoluteOpacity, FStroke);
    TLineType.ltLeft:
      Canvas.DrawLine(GetShapeRect.TopLeft, PointF(GetShapeRect.Left, GetShapeRect.Bottom),
        AbsoluteOpacity, FStroke);
  else
    Canvas.DrawLine(GetShapeRect.TopLeft, GetShapeRect.BottomRight, AbsoluteOpacity, FStroke);
  end;
end;

Изменив код соответствующим образом, можно добиться одинаковости линий:

procedure TForm2.FormPaint(Sender: TObject; Canvas: TCanvas;
  const ARect: TRectF);
var
  I: Integer;
begin
  if Canvas.BeginScene then
    try
      Canvas.Stroke.Thickness := 1.5;
      Canvas.Stroke.Kind := TBrushKind.bkSolid;
      Canvas.Fill.Color := TAlphaColorRec.Black;
      Canvas.Fill.Kind := TBrushKind.bkSolid;

      for I := 1 to 9 do
      begin
        Canvas.DrawLine(PointF(50 + I * 25 - (Canvas.Stroke.Thickness / 2), 0),
          PointF(50 + I * 25 - (Canvas.Stroke.Thickness / 2), ClientHeight), 1);
      end;
    finally
      Canvas.EndScene;
    end;
end;

Результат:

Намного симпатичнее :)

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

update

Alysson Cunha предложил еще один подход к решению проблемы:

function TForm2.RoundLogicPointsToMatchPixel(const LogicPoints: Single;
  const AtLeastOnePixel: Boolean = False): Single;
var
  ws: IFMXWindowService;
  ScreenScale, Pixels: Single;
begin
  ws := TPlatformServices.Current.GetPlatformService(IFMXWindowService) as IFMXWindowService;
  ScreenScale := ws.GetWindowScale(Self);

  // Maybe you will want to use Ceil or Trunc instead of Round
  Pixels := Round(LogicPoints * ScreenScale);

  if (Pixels < 1) and (AtLeastOnePixel) then
    Pixels := 1.0;

  Result := Pixels / ScreenScale;
end;

procedure TForm2.FormPaint(Sender: TObject; Canvas: TCanvas;
  const ARect: TRectF);
var
  I: Integer;
begin
  if Canvas.BeginScene then
    try
      Canvas.Stroke.Thickness := RoundLogicPointsToMatchPixel(1.0, True);
      Canvas.Stroke.Kind := TBrushKind.bkSolid;
      Canvas.Fill.Color := TAlphaColorRec.Black;
      Canvas.Fill.Kind := TBrushKind.bkSolid;

      for I := 1 to 9 do
        Canvas.DrawLine(PointF(RoundLogicPointsToMatchPixel(50 + I * 25), 0),
          PointF(RoundLogicPointsToMatchPixel(50 + I * 25), ClientHeight), 1);
    finally
      Canvas.EndScene;
    end;
end;

Дженерики: ограничение «constructor»

Наш японский коллега Jun Hosokawa заметил интересную особенность ограничения «constructor» в дженериках Delphi.

type
  TConstructorConstraint<T: constructor> = class
  end;

В качестве вступления цитата из справки:

This means that the actual argument type must be a class that defines a default constructor (a public parameterless constructor), so that methods within the generic type may construct instances of the argument type using the argument type’s default constructor, without knowing anything about the argument type itself (no minimum base type requirements).

То есть, согласно документации это ограничение позволяет убедиться, что аргумент дженерика — это класс, который имеет публичный конструктор без параметров, благодаря чему дженерик сможет создать экземпляр соответствующего типа, ничего о нем не зная. Здравый смысл может подсказывать нам, что речь только о классах, но на практике все оказывается не так просто. И встретив на просторах интернета высказывания о том, что это при использовании этого ограничения в качестве аргумента все-таки можно передавать не только классы, Jun решил проверить как такое возможно.

unit Unit1;

interface

type
  // нет конструктора без параметров
  TBar = class
  public
    constructor Create(const iDummy: Integer); reintroduce;
  end;

  procedure Test;

implementation

uses
  System.Rtti;

type
  TConstructorConstraint<T: constructor> = class
  private
    FValue: T;
  public
    constructor Create; reintroduce;
    function ToString: string; override; 
  end; 

  // конструктор без параметров
  TFoo = class
  public
    constructor Create; reintroduce;
  end;

  //динамический массив
  TStringDynArray = array of string; 

  // перечислимый тип
  TFactor = (Windows, MacOSX, Android, iOS, WindowsPhone);

  // множество
  TSet = set of TFactor;


{ TConstructorConstraint<T> }

constructor TConstructorConstraint<T>.Create;
begin
  inherited Create;

  // вызываем конструктор, не зная ничего о типе T !!!
  FValue := T.Create;
end;

// вывод информации о типе T
function TConstructorConstraint<T>.ToString: string;
var
  Rtti: TRttiContext;
  Field: TRttiField;
  FieldType: TRttiType;
begin
  Result := ''; 

  Rtti := TRttiContext.Create;
  try
    Field := Rtti.GetType(ClassInfo).GetField('FValue');
    FieldType := Field.FieldType;

    Result := Field.Name + ': ' + FieldType.ToString + ';'; 

    if (FieldType.IsPublicType) then
      Result := Result + ' Public;';

    if (FieldType.IsManaged) then
      Result := Result + ' Managed;';

    if (FieldType.IsInstance) then
      Result := Result + ' Instance;';

    if (FieldType.IsOrdinal) then
      Result := Result + ' Ordinal;';

    // record не может использоваться с ограничением "constructor" и здесь не появится
    if (FieldType.IsRecord) then
      Result := Result + ' Record;';

    if (FieldType.IsSet) then
      Result := Result + ' Set;';
  finally
    Rtti.Free;
  end;
end;

{ TFoo }

constructor TFoo.Create;
begin
  inherited Create;

  Writeln('TFoo Created !');
end;

{ TBar }

constructor TBar.Create(const iDummy: Integer);
begin
  inherited Create;

  // у этого конструктора есть параметры, он не сработает
  Writeln('TBar Created !');
end;

procedure Test;
var
  Foo: TConstructorConstraint<TFoo>;
  Bar: TConstructorConstraint<TBar>;
  Str: TConstructorConstraint<string>;
  Int: TConstructorConstraint<Integer>; 
  Ary: TConstructorConstraint<TStringDynArray>;
  Sets: TConstructorConstraint<TSet>;
  Factor: TConstructorConstraint<TFactor>;
begin
  Foo := nil;
  Bar := nil;
  Str := nil;
  Int := nil;
  Ary := nil;
  Sets := nil;
  Factor := nil;
  try
    Foo := TConstructorConstraint<TFoo>.Create;
    Bar := TConstructorConstraint<TBar>.Create;
    Str := TConstructorConstraint<string>.Create;
    Int := TConstructorConstraint<Integer>.Create;
    Ary := TConstructorConstraint<TStringDynArray>.Create;
    Sets := TConstructorConstraint<TSet>.Create;
    Factor := TConstructorConstraint<TFactor>.Create;

    WriteLn(Foo.ToString);
    WriteLn(Bar.ToString);
    WriteLn(Str.ToString);
    WriteLn(Int.ToString);
    WriteLn(Ary.ToString);
    WriteLn(Sets.ToString);
    WriteLn(Factor.ToString);
    
    Readln;
  finally
    Factor.Free;
    Sets.Free;
    Ary.Free;
    Int.Free;
    Str.Free;
    Bar.Free;
    Foo.Free;
  end;
end;

end.

Тип TFactor был объявлен, но автор не добавил его в тесты (видимо, забыл). Я это поправил.

Программа очень простая и всего-лишь выводит информацию о типе аргумента (спасибо, RTTI). Ниже результат её работы:

TFoo Created !
FValue: TFoo; Instance;
FValue: TBar; Public; Instance;
FValue: string; Public; Manged;
FValue: Integer; Public; Ordinal;
FValue: TStringDynArray; Managed;
FValue: TSet; Set;
FValue: TFactor; Ordinal;

И это удивительно, но это работает. Мы видим информацию о типах. Интересно, что TBar, в отличии от TFoo — это public type. Разница вызвана тем, что они объявлены в разных секциях модуля (interface и implementation). А перечислимый тип превратился в ordinal, что в принципе логически верно.

Но самое интересное то, что все это вообще работает, хотя у многих из этих типов совсем нет конструктора!

Если посмотреть на ассемблерный код, то видно, что здесь имеет место compiler magic.

// TFoo
Project1.dpr.28: FValue := T.Create;
00407D7A B201             mov dl,$01
00407D7C A158724000       mov eax,[$00407258]
00407D81 E85AF6FFFF       call TFoo.Create      // код инициализации
00407D86 8B55FC           mov edx,[ebp-$04]
00407D89 894204           mov [edx+$04],eax

// TBar
Project1.dpr.28: FValue := T.Create;
00407DCE B201             mov dl,$01
00407DD0 A114734000       mov eax,[$00407314]
00407DD5 E8BACDFFFF       call TObject.Create   // код инициализации
00407DDA 8B55FC           mov edx,[ebp-$04]
00407DDD 894204           mov [edx+$04],eax

// string
Project1.dpr.28: FValue := T.Create;
00407E22 8B45FC           mov eax,[ebp-$04]
00407E25 83C004           add eax,$04
00407E28 E843DDFFFF       call @UStrClr         // код инициализации

// Integer
Project1.dpr.28: FValue := T.Create;
00407E6E 8B45FC           mov eax,[ebp-$04]
00407E71 33D2             xor edx,edx           // код инициализации
00407E73 895004           mov [eax+$04],edx

// динамический массив
Unit1.pas.50: FValue := T.Create;
004D50F6 8B45FC           mov eax,[ebp-$04]
004D50F9 83C004           add eax,$04
004D50FC 8B1500394D00     mov edx,[$004d3900]
004D5102 E87D68F3FF       call @DynArrayClear   // код инициализации

// множество
Unit1.pas.50: FValue := T.Create;
004D540E 8B45FC           mov eax,[ebp-$04]
004D5411 8A153C544D00     mov dl,[$004d543c]    // код инициализации?
004D5417 885004           mov [eax+$04],dl

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

Можно прийти к выводу, что нет особого смысла в использовании ограничения «constructor» отдельно от других ограничений, очень уж широко оно трактуется компилятором.

Чтобы ограничить аргументы дженерика именно классами, следует использовать следующий код:

type
  TConstructorConstraint<T: class, constructor> = class
  end;

И интересно, что если убрать из этого кода слово «constructor», то дженерик потеряет возможность создать экземпляр класса, так как именно слово «constructor» позволяет гарантировать, что аргумент будет не просто классом, но так же он будет иметь конструктор по умолчанию. Но ведь в Delphi все классы наследуются от TObject и тем самым всегда имеют публичный конструктор по умолчанию.

В чем же здесь смысл?

P.S. Благодарю сестру за помощью с переводом с японского.

Разработка «стандартных» TAction

Почти все мы сталкиваемся с разработкой довольно сложных пользовательских интерфейсов. Приходится работать с нагромождениями кнопок, тулбаров, меню и всего, что только может придти в голову. К счастью, в Delphi есть компонент TActionList, немного упрощающий жизнь. И он ведь действительно помогает бороться с растущей сложностью интерфейсов.

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

Как говорится, любую проблему можно решить добавлением еще одного уровня абстракции. И такая возможность есть.

Как правило всю огромную кучу действий можно логически разделить на несколько категорий с более-менее похожим поведением. И грех этим не воспользоваться, благо Delphi позволяет создавать собственные типы «стандартных» действий.

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

На примере будет понятно. Представим, что часть действий необходимы для работы с данными и их свойство Enabled жестко привязано к тому, открыт соответствующий датасет или нет. Здесь можно использовать обычные TAction и писать каждый раз SomeAction.Enabled := DataSet.Active в событии OnUpdate, можно придумать что-то более изощренное, а можно просто написать соответствующий «стандартный» наследник TAction.

unit uDataAction;

interface

uses
  System.SysUtils, System.Classes, System.Actions, Vcl.ActnList, Data.DB;

type
  TDbAction = class(TAction)
  private
    FDataSet: TDataSet;
  public
    function Update: Boolean; override;
  published
    property DataSet: TDataSet read FDataSet write FDataSet;
  end;

procedure Register;

implementation

{ TDbAction }

function TDbAction.Update: Boolean;
begin
  if not (csDesigning in ComponentState) then
    self.Enabled := Assigned(FDataSet) and FDataSet.Active;

  Result := inherited Update;
end;

procedure Register;
begin
  RegisterActions('Custom Actions', [TDbAction], nil);
end;

end.

Все до очевидного просто. Этот класс унаследован от обычного TAction, в него добавлено поле DataSet и переопределена функция Update, где значение свойства Enabled по умолчанию привязывается к значению свойства DataSet.Active. Ключевой момент — процедура Register, которая позволит увидеть TDbAction в IDE. Этот модуль нужно добавить в пакет и установить его. И тогда, выбрав в диалоге редактирования TActionList пункт «New Standard Action…», можно увидеть новый TDbAction.

Далее с этим TDbAction можно работать точно так же, как и с привычными действиями, добавляя весь необходимый код в события OnUpdate и OnExecute. С той лишь разницей, что с состоянием датасета оно уже умеет работать.

Пара слов в заключение.

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