суббота, 17 января 2015 г.

Как я стал недоверять try-except

Я уже имел возможность убедиться, что try-except - жутко тормозная штука. Но я всё ещё думал, что это штука надёжная. По крайней мере, она никогда не подводила меня в мире Win32. И вот я пошёл с этими представлениями в мир мобильных приложений. И пришёл к печальному выводу - try-except работает ещё и отнюдь не всегда. Увы, "кто умножает познания, умножает скорбь"/широко известный источник/

Вот пример, где я пытался замерить скорость работы try-except при попытке доступа к неассигнованным объектным переменным:

procedure TForm5.Button1Click(Sender: TObject);
var
  dt: TDateTime;
  I: Integer;
  A: array[ 0..9 ] of TStringList;
 
  function V: Integer;
  begin
    try
      Result := A[ I mod 10 ].Count;
    except
      Result := 0;
    end;
  end;
  function V2: Integer;
  var
    Index: Integer;
  begin
    Index := I mod 10;
    if Assigned( A[ Index ] ) then
      Result := A[ Index ].Count
    else
      Result := 0;
  end;
var
  Summ: Integer;
begin
  FillChar( A, SizeOf( A ), 0 );
  for I := Low(A) to High(A) do
  if Odd( I ) then
    A[I] := TStringList.Create;
 
  dt := Now;
  try
 
    Summ := 0;
    for I := 0 to 1000000 do
    Summ := Summ + V2;
 
    ShowMessage( IntToStr( Summ ) + ': ' + FormatDateTime( 'hh:nn:ss:zzz', Now-dt ) );
 
  finally
    for I := Low(A) to High(A) do
    if Odd( I ) then
      FreeAndNil( A[I] );
  end;
 
end;

Сначала я пробую цикл с if Assigned(... - результат мгновенный: 0: 00:00:00:006. Теперь меняю Summ + V2 на Summ + V, и...


!!! Что же это за кака-бяка такая!!! И как дальше жить? Теперь, выходит, кроме создаваемых в коде исключений, отлавливать ничего не получится? Тоже не верно - перехватчик самого приложения всё-таки отработал, выдал сообщение, корректно завершил метод и даёт таким образом нам возможность нажимать на кнопку сколько угодно раз и сколько угодно раз любоваться этим "Access violation at address ..., accessing address 00000000." Но где это работает, а где нет? Где играть, где не играть, где рыбу заворачивать? Ы-ы-ы-ы-ы...


ЗЫ: Спасибо Владимиру Натановичу Винокуру за певца Григория Долголоба, служащего в Чугуевской филармонии.

Как лучше брать поля объектных полей? Или Assigned() VS try-except.

Как лучше брать поля объектных полей? Что предпочесть - ясный и простой код с исключением в стиле Delphi, или проверки с повторным чтением объекта или использованием  промежуточных значений в стиле Pascal? Вот пример:

procedure TForm5.Button1Click(Sender: TObject);
var
  dt: TDateTime;
  I: Integer;
  A: array[ 0..9 ] of TStringList;
 
  function V: Integer;
  begin
    try
      Result := A[ I mod 10 ].Count;
    except
      Result := 0;
    end;
  end;
  function V2: Integer;
  var
    Index: Integer;
  begin
    Index := I mod 10;
    if Assigned( A[ Index ] ) then
      Result := A[ Index ].Count
    else
      Result := 0;
  end;
var
  Summ: Integer;
begin
  FillChar( A, SizeOf( A ), 0 );
  try
for I := Low(A) to High(A) do if Odd( I ) then A[I] := TStringList.Create;   dt := Now;   Summ := 0; for I := 0 to 100000 do Summ := Summ + V;   ShowMessage( IntToStr( Summ ) + ': ' + FormatDateTime( 'hh:nn:ss:zzz', Now-dt ) );   finally for I := Low(A) to High(A) do if Odd( I ) then FreeAndNil( A[I] ); end;   end;

Это событие нажатия на кнопку,  в результате работы которого у меня на экране компьютера появляется сообщение с такими цифрами: 0: 00:04:41:248(*). Т.е. около пяти минут на 100 тыс. записей. Много ли это? Скажу только, что изначально я ставил 1 миллион, и просто замучился ждать.

И вот, вместо Summ + V я ставлю Summ + V2, запускаю, жму кнопку... Оп! И 0: 00:00:00:002!


!!! Так вот как должен работать процессор с восемью ядрами на частоте 4 ГГц! И миллион операций ведёт к увеличению времени до 0.021 секунды. И 10 миллионов - лишь малая доля секунды...

Красивый, простой как 2 копейки код с try-exсept против жуть какого старого, извилистого, но всё ещё очень и очень доброго if... Если кто-то опять мне будет говорить, что новее - значит лучше, то скорее всего, я снова ему не поверю.

Кстати, надо погонять это дело на телефоне...

Примечание *: Такое ужасно время получено из-за запуска по F9 - с отладкой. Для такого рода замеров следует запускать Shift-Ctrl-F9, т.е. без отладки. Без отладки время ф-ии V будет уже 0,677 секунды, что ни в какое сравнение не идёт с 5-ю минутами. Однако, это всё же более, чем в 300 раз дольше варианта с V2. И к тому же совершенно очевидно, что отладка программы, где происходит исполнение такого рода кода, может стать весьма утомительным занятием. Спасибо Alexey Kazantsev за замечание по поводу отладчика. 

пятница, 26 декабря 2014 г.

Как нарисовать крестик в середине TImage


Суть даже не в крестике, а как рисовать в нужном месте. Координаты? Да, слыхал. И рисовал как бы даже... Но стоило с MSWindows перейти на Android - и "Шеф, всё пропало, все пропало!"(© Козодоев Г.П.)

Вот пример, замечательно работающий в Винде, но не работающий на моём Nexus 4:
const
  Ident = 10;
... 
      DrawLine( //Horizontal
        TPointF.Create( Ident, ( FRawBitmap.Height / 2 ) ),
        TPointF.Create( FRawBitmap.Width - Ident, FRawBitmap.Height / 2 ), 0.5 );
      DrawLine(  //Vertical
        TPointF.Create( FRawBitmap.Width / 2, Ident ),
        TPointF.Create( FRawBitmap.Width / 2, FRawBitmap.Height - Ident ), 0.5 );

Вот на компе рисует, а в телефоне - нет!

Отладчик говорит, что всё нормально - методы проходят. А сдвиг координат подсказал, что действительно - всё рисуется, но где-то за пределами картинки. Гром и молния! Как может что-то рисоваться за пределами, если мы рисуем в точке с координатами меньше пределов?!

Посмотрев в FMX.Objects как рисует TImage, я среди прочего заметил странную такую штуку - FScreenScale. Не "паблик", не свойство, а чисто "для себя". И берётся оно из свойства TControl.Scene:IScene. А Scene - это, как можно легко заметить, интерфейс, у которого есть даже не свойство SceneScale, а именно метод GetSceneScale. Мало того, интерфейс этот ещё может и отсутствовать, и тогда FScreenScale = 1.

Ещё есть у IScene функции расчёта координат экран-локаль, но они тут не работают, нужен именно  "Scale". Этот Скэйл у меня на ПК - 1, а на Нексусе - 2. Сама же Сцена есть и там, и там. Т.е. вроде бы получается, что проверять её наличие не обязательно (но я себе проверку всё же скопипастил), а нужно взять коэффициент и разделить координаты на него.

Поскольку я много делить не люблю, я разделил один раз, а потом умножаю:
var
  FScreenScale : Single;
... 
  if Assigned(Scene) then
    FScreenScale := 1 / Scene.GetSceneScale
  else
    FScreenScale := 1.0;
... 
      DrawLine( //Horizontal
        TPointF.Create( FScreenScale * Ident, FScreenScale * ( FRawBitmap.Height / 2 ) ),
        TPointF.Create( FScreenScale * ( FRawBitmap.Width - Ident ), FScreenScale * FRawBitmap.Height / 2 ), 0.5 );
      DrawLine(  //Vertical
        TPointF.Create( FScreenScale * FRawBitmap.Width / 2, FScreenScale * Ident ),
        TPointF.Create( FScreenScale * FRawBitmap.Width / 2, FScreenScale * ( FRawBitmap.Height - Ident ) ), 0.5 );

Ну вот, теперь мой крестик ровно в середине рисунка.

- Погоди-ка, а если там всё в два раза больше, то попиксельно ничего не нарисуешь????
- Да нарисуешь, координаты-то не целые!

Только зачем такие странности? Чтобы понять это, как сказал Ярослав Бровин "В общем ничего кроме стандартных знаний линейной алгебры здесь не требуется". И мне остаётся стыдливо воспринимать эти обстоятельства как данность, данную нам свыше.

четверг, 18 декабря 2014 г.

Как сканировать штрих-код


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

Если с печатью штрих кодов понятно - там какой-то шрифт или ещё как что нарисовать по правилам можно, то считать, распознать - у-у-у - это сложно. Это же математика какая-то, алгоритмы сложные - короче, на коленке не сделаешь.

А что же делать, если вдруг нужно? Конечно, готовые компоненты искать. И желательно - бесплатные :)

Но пойдите на Torry - что там? Там компонентов для распознавания кодов - раз, и обчёлся.

Например, для FMX есть компонент от WINSOFT. Даже с возможностью перед покупкой закомпилить примерчик. Только он у меня не пошёл - на телефоне что-то с синтаксическим нераспознаванием пакета, а на планшете запускается, но умирает - какой-то  libzbar хочет или что-то в этом духе. Я бы подробнее почитал, но не сошёлся же свет на них клином - должно же быть такого добра  (тем более, что и не за бесплатно) - море! Но что-то море это мелкое какое-то...

Я даже и дот-нетов смотрел, и, поскольку мне для Андроида, смотрел, что джависты предлагают - на всё кидался, там может и мне что найдётся. Вот, например, Java4less предлагает за сотню убитых енотов с хвостиком на каждый тип кода (а их не два и не три) отдельный Delphi-компонент. И ещё полторы сотни просит за редистрибуцию! А вот демку скомпилить - не даёт!

Но, долго ли, коротко ли, вариант подходящий отыскался. Вы не поверите! - БЕСПЛАТНАЯ библиотека на сайте - (барабанная дробь) - кто бы мог подумать, кому это в голову только могло прийти?! - EMBARCADERO!!!! (та-дам!!!)

http://cc.embarcadero.com/Item/29811 Там штрих-коды и для Андроида, и для йОса. И плюс ещё там какие-то другие нативные штуки типа всплывающих сообщений Андроида и проверки наличия сетевого подключения. Сборничек небольшой. Небольшой, но в общем интересный.

Да, я стокамногабуков написал, чтобы только ссылку дать. Но я просто рад очень, чес-слово!

Правда, оказалось, что проект был сделан для XE5. Для XE6 пришлось пробежаться по uses и расставить для Android новый юнит Androidapi.Helpers, а старую енумерацию aeBecameActive надо было заменить на TApplicationEvent.BecameActive. И вот - у меня на телефоне (некоторые утверждают, что правильно говорить "в телефоне") уже новое приложение!

Однако, счастье всё ещё никак не давалось в руки так просто. Как только я нажимал на кнопку сканирования - телефон уходил в задумчивость и предлагал приложение остановить. Тоже и планшет. И я тогда пошёл под отладчиком. Странно, но под отладчиком на телефоне у меня даже появилось такое зелёное окошко с видом через видеокамеру телефона. И там даже хелп был, и всё говорило, что сейчас штрих-код считается. Но не считывалось ничего. А казалось, что счастье уже было так близко, так возможно!

И вот - О, Моя Врождённая Чудо-Наблюдательность! - я обнаружил, что требуется пакет "com.google.zxing.client.android.SCAN". Ага! По этому поводу интернет делится на 2 лагеря: одни рассказывают, как пришить этот пакет в приложение, а другие, коих, как мне показалось, большинство - говорят, что не надо ни в коем случае пришивать, т.к. пакет обновляется часто, и лучше всегда отдельно скачивать свежачок - он же бесплатный!

Я всё понял! Я пошёл в АппСторр... Ой! Я зашёл в Гугл-Плэй, задал в поиске zxing и установил его. У меня появилось приложение, которое чего-то сканирует. Так вот, мой (ну, я же тоже его дописывал) MobileWrapers сразу же повеселел и стал ловко пикать совсем уже не зелёным, а полупрозрачным окошком с красивой красной полоской для прицеливания. Ура!

Теперь проблема сканирования штрих-кодов для Андроид-приложений - уже не проблема. И я верю, что для йОса тоже уже всё готово. Но я пока не компилил - может, там тоже что-то с юнитами поменялось со времён XE5.

Для желающих подкрученные мной исходники здесь - https://drive.google.com/file/d/0B8ZLqV359_X5R1ZKS3ktdF9FLXc/view?usp=sharing

Спасибо

 Fernando Rizzato!

воскресенье, 4 мая 2014 г.

Как ради WebTest я отказался от Sorted и ActiveControl

Я чрезвычайно доволен своим компаратором WebTest, который даже загрузился на http://store.yandex.ru (поищите WebTest или WarThunder), и даже пытается, несмотря на объявленную цену в $0,95( 29 руб.), обновиться совершенно бесплатно. К сожалению, пока обновление не проходит - "ошибка загрузки". Но я не теряю надежды.

А что, собственно, обновилось? Есть и фичи, есть и баги. Причём, баги, увы, не все мои. Это, так сказать, "системные фичи", которыми на текущий момент (XE5 Update5) "радует" нас Fire Monkey. Подробнее об этом потом, а сначала немного о том, что поменялось именно под влиянием эксплуатационного опыта.

Фичи:


1. Имена вновь созданных юнитов теперь не вычисляются в виде "Новый", "Новый(2)" и т.д. Я скопировал поведение MSWindows для имени новой папки, и на предварительных тестах оно мне нравилось. Но, когда пришёл массовый ввод, я запарился удалять эти буквы - они оказались только помехой. Теперь имя пустое, лишь с текстом-подсказкой. Если оставить имя пустым и ничего не вводить в числитель-знаменатель, то по Назад-кнопке юнит будет тихо удалён. В принципе, такое поведение и для старых юнитов вполне логично - если удалил имя и данные, то удалил всю запись. Но такое поведение не вполне очевидно, и кнопку "Удалить" я оставил.

2. Формат дробных чисел теперь - с тремя знаками после запятой. Я понадеялся на форматы по-умолчанию, которые идут с двумя знаками. Вначале я не особо напрягался, что данные статистики идут в простых единицах или в кило (К), т.е. с разницей в три порядка. Но потом кол-во два знака вместо трёх стало напрягать всё больше и больше, и я, ценой титанических усилий, изменил формат 2-х контролов и 2-3 места форматирования рейтинга. И стало счастье.

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

А теперь о багах.


Во-первых, я заметил, что при изменении статуса юнита не меняется звёздочка-индикатор в списке, пока не поменяется порядок сортировки. Мой грех. Исправил. Не стоило бы и говорить об этом, т.к. здесь ничего интересного. Но пусть будет для отчётности. А интересное - дальше.

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

Итак, я заполнял список процедурой Populate( const SelectItem: String = '' ), где заполнял отсортированный список (Sorted = True, OnCompare = ListBox1Compare). Параметры фильтра и сортировки я брал из текущих значений контролов, а для установления текущей строки в списке передавал в процедуру параметр.   

...
    ListBox1.BeginUpdate;
    try
      ListBox1.Clear;
...
      for ...
...
        with TListBoxItem.Create( ListBox1 ) do begin
          Parent            := ListBox1;
          ...
          TagString         := ...;
          Text              := ...
...
        end;  //with
      end; //for
...
    finally ListBox1.EndUpdate;
    end;
...
  if not SelectItem.IsEmpty then
    for I := 0 to ListBox1.Count-1 do
      if SameStr( ListBox1.ItemByIndex( I ).TagString, SelectItem ) then begin
         ListBox1.ItemIndex := I;
         Break;
      end;
 
Ну, что тут неправильного?  Список отсортирован, и на EndUpdate вызывается ListBox1Compare. Осталось, вроде бы, отыскать после сортировки нужный ListBoxItem, и всё. Но в результате я постоянно получал по две(!) выделенных строки.


Решив, что дело в не вполне корректной отрисовке, я после некоторых экспериментов добавил   перед ListBox1.ItemIndex := I строчку  ListBox1.SetFocus; и, увидев стабильное одинарное выделение, успокоился. А зря.

Поработав некоторое время с программой, я обнаружил, что выделяется вовсе не та строка, которую я имел в виду. Т.е. когда я меняю рейтинг, то Ок. Но когда, скажем, программа стартует и пытается выйти на ту строку, которая была активна до предыдущего завершения, то получается нечто весьма забавное: в цикле поиска индекс искомого элемента равен его месту в неотсортированном списке, т.е. полученному при загрузке! И это после того, как был вызван алгоритм сортировки! А в момент появления на экране строки расположены уже в отсортированном порядке, и найденный ранее индекс соответствует уже другой строке!

Оказывается, сортируемые элементы списка в первый раз ещё не связаны с FMX-объектами, которыми они будут представлены на экране. Контекст контрола-списка создаётся в момент отрисовки. И при этом проводится повторная сортировка. И вот уже теперь элементы данных соответствуют объектам представления. Бред какой-то. Я не стал разбираться, кто виноват и как исправить FMX. Меня больше интересует вопрос "Что делать?".

Итак, результатом моих изысканий стало следующее решение: отказаться от TListBox.Sorted = True, и использовать ListBox1.Sort.

Этот метод требует делегата, который нужно определить вне класса. И работает он с парой TFmxObject'ов. А старое OnCompare - выкинуть? А оно даже не всю сортировку делало, а работало только для определённого вида сортировки. Стоит ещё сказать, что моё решение было экспериментальным, и переписывать всё подряд я не хотел. Поэтому я использовал protected функцию CompareItems, которая и раньше делала всю работу по предварительной сортировке и вызову события OnCompare.


procedure TForm2.ListBox1Compare(Item1, Item2: TListBoxItem;
  var Result: Integer);
begin
...
end;
 
type
  MyLB = class ( TListBox )
//    function CompareItems(const Item1, Item2: TListBoxItem): Integer; virtual;
 end;
 
function ListBox1Compare(Left, Right: TFmxObject): Integer;
var
  Item1: TListBoxItem absolute Left;
  Item2: TListBoxItem absolute Right;
begin
  Result := MyLB( Form2.ListBox1 ).CompareItems( Item1, Item2 );
end;
...
      ListBox1.Sort( uMain.ListBox1Compare );
 
Обращу внимание на совпадение имён делегата и события, которое по-вкусу мне и может быть не по-вкусу вам. Но более важно здесь то, что мне пришлось использовать переменную Form2. Вот это действительно не очень изящно. Мне кажется, это уже повод для размышлений по поводу языка-компилятора - почему мы не можем в качестве делегата использовать нечто более инкапсулированное. Но, тем не менее, решение такое, и оно работает. Сама Fire Monkey для доступа к экземпляру списка берёт парента у Left, что я, наверно, и буду использовать в дальнейшем, но это тоже не выглядит супер-пупер.

И вот уже наконец я добрался до "в третьих". Здесь всё просто. Точнее - не просто, но быстро. Быстро нашёлся способ обхода проблемы. В конце концов, проблемы программиста не в том, что его средство программирования как-то неправильно работает, а в том, что неправильно работает его программа. Кто-то не согласен?



Вот у меня Числитель и Знаменатель - числа. Это значит, что у меня на экране устройства(скриншот здесь из Win32, но все понимают) цифровая клавиатура с цифрами, точкой и кнопками "Забой" и "Ввод". Я хочу использовать vkReturn(т.е. Ввод, <ВК>, Ентер, Пуск), чтобы перемещаться между числами. Более того, такое перемещение вызывает всякие события валидации и изменения контрола, что приводит к приятному вычислению показателя. Не сложно вроде бы:

procedure TForm2.ItemDenominatorKeyDn(Sender: TObject; var Key: Word;
  var KeyChar: Char; Shift: TShiftState);
begin
  if Key = vkReturn then begin
    if Sender = ItemNumerator then
      ActiveControl := ItemDenominator
    else
      ActiveControl := ItemNumerator;
  end;
end

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

Я стал разбираться. Даже пробовал вместо KeyDn повесить событие на KeyUp. Иногда событие приходило сначала на один контрол, а потом сразу на второй, и в результате двойной смены хозяина фокус оставался на месте. Я стал обнулять Key. Но выяснилось, что ActiveControl иногда просто не соответствует реальному положению вещей! Т.е. Sender не только вовсе не равен ActiveControl но и уже равен тому контролу, в который я собираюсь передать фокус, т.е. внешне неактивному!

Что делать? QualityCentral? Я не останавливаю желающих улучшить FMX. Я даже призываю: Эй, смелые и грамотные люди, сделайте заявку! Но я пока нашёл работающую альтернативу:

procedure TForm2.ItemDenominatorKeyDn(Sender: TObject; var Key: Word;
  var KeyChar: Char; Shift: TShiftState);
begin
  if Key = vkReturn then begin
    if Sender = ItemNumerator then
      ItemDenominator.SetFocus // ActiveControl := ItemDenominator
    else
      ItemNumerator.SetFocus; // ActiveControl := ItemNumerator;
    Key := 0;
  end;
end

Итак, всем удачи. Надеюсь, чтение сего поста было для вас хоть как-то полезно.

пятница, 2 мая 2014 г.

Как продать программу через Яндекс

Случайно узнал о магазине приложений Яндекса. Решил посмотреть и попробовать выложить свой WarTest. Вы знаете, что ГуглПлэй требует не только денег в количестве $25, но и подписания приложения и соответствующей специальной компиляции для Application Store.

Так вот - в Яндексе этого ничего не надо! Я просто зашёл по своему обычному аккаунту, дописал свои координаты, и тут же залил прямо из WarTest\Android\Debug\WarTest\bin отладочный APK. И всё!


Да, ещё не всё, но ничего перекомпилировать или модерировать не нужно. Я даже подумал, нельзя ли продавать за рубль свою поделку. Оказалось - нельзя, можно только минимум за 29 рублей или отдавать бесплатно. Это надо сразу сказать и потом не менять. Теперь я понимаю, зачем к названию прибавляют слово Free - это уже другая программа получается.

На снимке выше можно заметить, что программу я загружал 2 раза. Для второго раза я изменил свойства проекта, чтобы посмотреть, как что получится.


Иначе повторная загрузка не пойдёт:


Ну вот, ещё пара скриншотов требуется (я взял из предыдущего поста), плюс плакатик обязательного размера 1024х500 (тоже подрезал из прошлого поста) и некая промо-иконка. А я всё гадал - зачем люди к своим приложениям 512х512 иконки делают? Я тоже такую делал, но не знал зачем. Теперь пригодилось. Правда, где она светится в магазине, я пока не увидел.


Ещё Яндекс дал мне какой-то публичный ключ. Зачем - не знаю, но на всякий случай я скопировал его себе в проектный каталог. Кто знает, может быть сгодится. Вероятно, эти цифры участвуют при внутренних покупках и адд-онах с рекламой.

А вот так теперь моё приложение находится в магазине:


Здесь я обнаружил, что засветилось моё имя и e-mail, чего я не очень стремился делать. Я думал, что это, как и "физический" почтовый адрес, чисто для внутренних дел Яндекса, а не для сторонних зрителей. Советую иметь это в виду.

И ещё такой интересный момент. Приложение я сделал платным (дал банковские реквизиты работодателя) и при первом нахождении в магазине светилось "29 руб". Так вот, после запуска приложения из Delphi по шнурку Яндекс его тут же отследил и сообщил, что покупать приложение уже не нужно, а можно просто взять и открыть! И таки да, открывает.

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

среда, 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;
...