среда, 30 апреля 2014 г.

WarTest - компаратор для WarThunder


После недавнего размышления про странности события Compare мой калькулятор для самолётиков назвали компаратором и попросили скриншотов. Мне очень понравилось слово "компаратор" и я решил и скриншотов накидать, и даже снять ролик.


Скачать текущую версию тут: https://www.dropbox.com/s/hzabel4nb6gsu0j/WarTest.apk



А вот и кино:




ЗЫ: В следующем сообщении я описал, как пробовал продать этот компаратор через Яндекс.

вторник, 29 апреля 2014 г.

Как я обустроил TNumberBox

Я уже как-то жаловался на тяжёлую жизнь с TNumberBox. Три основные проблемы это:
1.Значение Value и OnChange живут отдельно от происходящего в поле ввода.
2.При тыкании пальцем в контрол число может внезапно поменяться.
3.При вводе цифр в нулевое поле ноль так и остаётся в начале числа.

Но что делать? Отказаться от имеющегося, написать и регистрировать свой компонент? Я настолько глуп и ленив, что мне это всё трудно, и нужно что-то совершенно простое. Например:

unit MyNumberBox;
 
interface
 
uses System.Classes, FMX.Edit;
 
type
  TNumberBox = class( FMX.Edit.TNumberBox )
    procedure MouseMove(Shift: TShiftState; X, Y: Single); override;
    procedure DoChangeTracking; override;
  end;
 
implementation
uses
  System.SysUtils, System.Character;
 
{ TNumberBox }
 
procedure TNumberBox.DoChangeTracking;
var
  S : String;
  I : Integer;
  V : Single;
begin
  S := Text;
  if not S.IsEmpty then begin
    I := Low( S );
    while ( S[ I ] = '0' ) and S[ I + 1 ].IsDigit do
      Inc( I );
    if I <> Low( S ) then
      Text := S.Substring( I - Low( S ) );
  end;
  if TryTextToValue( Text, V, Value ) then
    if not SameStr( FloatToStr( V ), FloatToStr( Value ) ) then
      Value := V;
  inherited;
end;
 
procedure TNumberBox.MouseMove(Shift: TShiftState; X, Y: Single);
type
  PClass = ^TClass;
var
  ClassOld: TClass;
begin
  ClassOld := PClass(Self)^;
  PClass(Self)^ := TCustomEditBox;
  try
    MouseMove( Shift, X, Y );
  finally
    PClass(Self)^ := ClassOld;
  end;
end;
 
end.

Это такая подмена-заглушка, которая не требует никаких настроек среды разработки и никак не мешает в дизайн-тайме, а действует автоматически во время выполнения. DoChangeTracking выполняет обрезку лидирующего нуля (достаточно хорошо для меня) и проверяет изменения в Value. MouseMove перекрывает обработку скрола ( всего-то навсего можно было сохранять значение ноль в вертикальной прокрутке, но я глуп, и не знаю как это делать ) и на всякий случай (на всякий случай, да) вызывает "дедушкин" метод-обработчик. Вот и всё.

Теперь вставляем в интерфейсный uses наш перехватчик, чтобы он сидел в конце и переопределял тип TNumberBox.
unit uMain;
 
interface
 
uses
...
  FMX.ListBox, FMX.StdCtrls, FMX.Layouts, FMX.Edit, System.Actions, FMX.ActnList,
  MyNumberBox;
 
type
  TForm2 = class(TForm)
...
    ItemDenominator: TNumberBox;
...
 
Я использую XE5 и у меня всё работает замечательно. А как обстоят дела у вас? 

понедельник, 28 апреля 2014 г.

Как выбрать наихудший самолёт? Или про странности события Compare.


Когда есть дома свободных минут 10-20 я люблю запустить игру про самолётики и поучаствовать в сражении или двух или даже на всю ночь. Есть такой грех. Ну так вот, (извините за длинное предложение) в каждом сражении я выбираю одну из пяти стран, у каждой из которых несколько экипажей, каждый из которых использует один самолёт из кучи уникальных самолётов, полученных за накопление очков опыта и прочего вознаграждения.  И когда я получаю очередной новый самолёт, то возникает вопрос - какой из старых самолётов заменить на новую модель? Т.е. какой из действующих самолётов наименее эффективен?

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


Я установил TListBox.Sorted = True, которое вызовет событие OnCompare в момент  TListBox.EndUpdate. Причём при сортировке перед каждым вызовом OnCompare уже успевает отработать стандартное сравнение по TListBoxItem.Text, и результат этого сравнения даётся нам в Result. Если нам это и нужно, то делать уже ничего не надо. Если потребуется сравнение по отношению зарплата/гибель, то нужно изменить Result, воспользовавшись Item.TagFloat, где хранится вычисленное ранее значение. Ничего сложного? Вроде бы да, и я написал так:

  if tacSort.TabIndex > 0 then
    Result := 1 - 2*Byte( Item1.TagFloat > Item2.TagFloat );
 
И вот! Вот из-за этого всё и стало падать! Причём падает не здесь, а в FMX.ListBox.CompareListItem на Item1 is TListBoxItem. Странно, да? Т.е. когда у нас TabIndex = 0, то Result остаётся без изменений и всё хорошо, а когда переключаемся на "Рейтинг", то начинаются неприятности.



Я поясню, пожалуй, свою математику. Берём данные нам алгоритмом сортировки два предмета, сравниваем кто больше, получаем при приведении к байту 0 (False) или 1 (True), что даёт нам либо 1 = 1 - 2*0, либо -1 = 1 - 2*1.

Разумеется, догадаться, что основная причина сбоя программы - это "сам дурак", было не трудно. Дело в том, что алгоритм сортировки подсовывает между делом нам для сравнения в качестве Item1 и Item2 - один и тот же объект! И получается, что каждый элемент никогда не равен сам себе, и этого алгоритм выдержать не может. Он сам не знает и знать не желает, что сравнивается что-то с самим собой. И достаточно дополнить процедуру сравнения условием равенства Item1= Item2, как всё становится хорошо.

  if tacSort.TabIndex > 0 then
  if Item1 <> Item2 then
    Result := 1 - 2*Byte( Item1.TagFloat > Item2.TagFloat )
 else
   Result := 0;
 
Боже мой! Что это за глупость такая? Ясно же как день, что каждый предмет равен сам себе! Для чего же тогда вызывать лишний раз событие? Бред какой-то! Я негодовал. Но проверив VCL мне пришлось смириться с судьбой - там всё то же самое. Ужас.

Я несколько расстроен своим детским  открытием. И как это мне удавалось много лет счастливо жить, не зная таких элементарных вещей? Мне кажется, Quality Central будет надо мной громко смеяться, если я подниму этот вопрос. Мне грустно. Солнце перестало светить в моё окно. Надеюсь, читающие мимо друзья найдут пару слов мне в утешение.

ЗЫ: апк тут: https://www.dropbox.com/s/hzabel4nb6gsu0j/WarTest.apk
Cкриншоты и даже ролик тут: http://alhymov.blogspot.ru/2014/04/wartest-warthunder.html.

понедельник, 7 апреля 2014 г.

Как заставить TPath рисовать график

Задача простая - отобразить простой график. TPath - вот идеальный кандидат! Стоит ему только дать точек, и он сам всё прекрасно отмасштабирует! Ура, ура, ура!

Но оказалось, что ...

Вот простой контрол, который вы можете бросить себе на форму:

object Path1: TPath
  Align = alClient
  Data.Path = {
    0700000000000000000000009A99193E010000000000803FC3F5A83E01000000
    00000040AE47E13E0100000000004040000000000100000000008040CDCC4C3F
    010000000000A040000000C001000000000040410000A040}
  Fill.Kind = bkNone
  HitTest = False
  RotationAngle = 180.000000000000000000
  Stroke.Color = claRed
  Stroke.Thickness = 5.000000000000000000
  Stroke.Cap = scRound
  Stroke.Join = sjRound
end
 
Я задал в Инспекторе несколько точек, изобразив некий, как мне кажется, красивый график. Здорово? Великолепно! Но, не забудьте, что ордината ( ось Y ) направлена вниз. Поэтому координаты по Y следует задавать в отрицательном виде. Мне в моей программе  показалось удобнее задавать в минусах абсциссу. Поэтому я развернул контрол на 180 градусов и минусую координаты X. Вот как это происходит:

var
  PathMax   : Single;
  PathMin   : Single;
  I         : Integer;
  P         : TPointF;
begin
 ...
    Path1.Data.Clear;
    P :=  LI2PointF( 0 );
 ...
PathMax := P.Y; PathMin := PathMax;   with Path1.Data do begin MoveTo( P ); for I := 1 to List1.Count - 1 do begin P := LI2PointF( I ); if P.Y > PathMax then PathMax := P.Y else if P.Y < PathMin then PathMin := P.Y; LineTo( P ); end; end;
 ...
Здесь LI2PointF - ф-я, возвращающая очередную точку. Причём, поделюсь страшным секретом: ордината у меня - дата TDateTime, приведённая к -Double. Да, всё так просто. Не просто только оказалось увидеть график в "бегущем" приложении. Но почему?

А что нам советуют профессионалы? Если посмотреть WebDelphi или Всеволода Леонова, то похоже, что следует использовать TPath.Data(:TPathData).Data - строку SVG. Надо сформировать скрипт типа 'M 227 239 L 328 90 L 346 250' и присвоить. И знаете что? Оно работает!

А если посмотреть в недавно вышедшую книгу Дмитрия Осипова, то кажется, что надо использовать методы MoveTo и LineTo. А я так и делал. Но стоит посмотреть на представленный в книге код по отрисовке уже заданной траектории (так можно перевести "Path"), как желание пользоваться методами начинает пропадать.

А чем плох SVG? Да собственно, нельзя сказать, что он плох. Но не кажется ли вам, что использование SVG - это вырезание гланд через анальное отверстие? Собирать скрипт - это, как ни крути - отдельная мелодия, на которую нужны таки свои инструменты - костанты команд, форматирование чисел, хранение и конкатенация строк. А потом этот свежеиспечённый скрипт придётся нашему компоненту парсить, и применять все те методы отрисовки, которые и так нам даны напрямую!

Да что же не так с этими методами? Почему они не рисуют, как полагается? Я искал какой-нибудь Begin-End или Update и нашёл ClosePath. С ним вдруг всё стало рисоваться. И даже больше, чем всё - стало рисоваться ещё линия, замыкающая последнюю и первую точки. Мне не нужна эта линия, но, ведь, остальное всё рисуется же! Что же там такого, чего нет, скажем, в AddEllipse? А там, как и в том же SetPathString, который вызывается при Data := SVG, в конце метода скромно стоит   if Assigned(FOnChanged) then    FOnChanged(Self);

Вот оно что! Ну, осталось только посмотреть подробности и вытащить метод DoChanged из protected. Всё, можно рисовать:

type
  TMyPath = class ( FMX.Objects.TPath )
//  public
//    procedure DoChanged(Sender: TObject);
  end;
...
          LineTo( P );
        end;
      end;
      TMyPath( Path1 ).DoChanged( nil );
 
      laPathMax.Text := Format( '%.3f', [ PathMax ] );
      laPathMin.Text := Format( '%.3f', [ PathMin ] );
  end;
...