пятница, 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;
...



понедельник, 31 марта 2014 г.

Как жить с таким TNumberBox?

Очень мне хотелось бы поговорить о TNumberBox. С одной стороны - чудо-контрол, который нам должен давать в установленных пределах готовое число, а с другой стороны - что-то он работает не слишком предсказуемо. 


Сначала обсудим, что в нём вкусного. Во-первых, он нацелен на цифровую виртуальную клавиатуру. Это хорошо. А то ужас как неудобно устанавливать KeyboardType в vktNumberPad. Шучу, конечно. Но ведь в самом деле, вряд ли только из-за этого нужен дополнительный контрол. Поэтому смотрим, что там во-вторых.

Во вторых, он обрабатывает вертикальный и горизонтальный жесты, позволяя менять численное значение без ввода цифр. Да и мышь в Windows (и, наверно, на Mac ) тоже прекрасно работает. Причём мы можем отдельно регулировать скорость приращения по вертикали и по горизонтали! Допустим, вдоль длинной горизонтали меняется целая часть числа, а вдоль короткой вертикали - десятые доли. Такого поведения в VCL не было!

В третьих, ЧисловойКоробок способен форматировать ввод и обеспечивать валидный (от франц. valide - законный, действующий) результат ввода в виде численного значения Value. Для форматирования и валидации у нас есть количество точек после запятой и диапазон значений Min и Max. Вот это уже кажется настоящим делом для нового контрола.


А теперь рассмотрим некоторые сложности. Обратите внимание - Min и Max = 0..100. Вас устроит такое ограничение? А если вообще ограничения не нужны, если нужен диапазон "по-максимуму"? Установка 0..0 по моей логике должна бы отменить всякие ограничения, но - нет. Стоит отвернуться, отвлечься и нажать даже не другой контрол, а всего лишь какую-нибудь "не такую" клавишу, как значение в поле редактирования сбрасывается в 0. Вот беда! Да ладно, не беда! - скажем мы, посмотрим в справкепосмотрим в справке ещё и найдём диапазон Single = 1.5e-45..3.4e+38 и ... Увы, Object Inspector не понимает таких значений. Я лично оставляю -1E38..1E38. Если переборщить, то мы даже можем увидеть нехорошее слово из трёх букв - INF (я думаю, что это Infinity - бесконечность). Нехороши эти буквы потому, что вместо обещания Infinity они не только не дадут нам диапазон -∞↔+∞, но и не дадут запустить программу, и, что особенно неприятно, не дадут загрузить  в следующий раз саму форму, если нечаянно записать её с этими буквами вместо цифр. Придётся открывать <unit>.FMX в текстовом редакторе и менять буквы на на 0. При этом не помешает соблюдать осторожность, чтобы не споткнуться о какой-нибудь [Inf]o или Ma[inF]orm.

Теперь жесты. Казалось бы - вот здорово! А оказалось - а вот и не очень. Если поле ввода перекрывается всплывающей клавиатурой, то что надо сделать с полем? Да, мы его двигаем вверх, да ещё и, как real programmers, с анимацией. А пока мы двигаем поле, NumberBox отслеживает точку нажатия относительно своих границ. Т.е. он думает, что это не он перемещается, а что это мы двигаем пальцем! Он меняет значение, хотя палец просто ткнулся и стоит там, где только что было поле ввода. Мне кажется, что когда программа вдруг меняет значение, которое должен менять лично сам пользователь, то это доверия пользователя к программе совсем не добавляет. Это какая-то ерунда. Но тогда, может, ну их - жесты эти - к лешему? Мы как-то и без них раньше обходились. Верно? К чему тут виски напрягать да копья ломать? Возьмём просто и отключим их! Это же кажется легче лёгкого - достаточно установить HorizontalIncrement и/или VerticalIncrement в 0. Но увы и снова увы - после запуска (!) приложения мы обнаруживаем, в VerticalIncrement что 0 заменяется дефолтным 5! Когда я столкнулся с этим мне почему-то вспомнилось классическое "Братья и сёстры! Это ли не чудо?!" Я пробовал организовать чудоборство запоминанием в TagString текста контрола перед каждым FloatAnimation1.Start и, соответственно, восстановлением на OnFinish анимации. Но - увы, ничего хорошего не получилось. Кстати, что мешало сделать TAnimation.OnStart? Если кто знает как уведомить EMBT о целесообразности этого события, напишите, пожалуйста.

И форматирование ввода - тоже не слишком похоже на ожидаемое. Когда при значении 0 вводится какая-то цифра, то вводится следующая цифра! Что это за число такое "008,9" или "+00"? Что это за левые (во всех смыслах) нолики? Может, это мода такая новая? Мне  лично эта мода кажется ругательной, и я прошу её ко мне не применять! Поэтому я вешаюсь на ChangeTracking и режу первый символ, если он 0 и при этом второй - цифра (минус бы тоже учесть можно, но это уже для гурманов). Такую ерунду подвешивать к каждому полю - это разве дело? Но иначе я на поле ввода числа смотреть не могу. Чес-сло, я уже с теплом начинаю вспоминать TMaskEdit. И ещё вопрос у меня - а кому там нужен плюсик? Ведь любое ненулевое число без минуса - уже плюс! Просветите меня, если можете. Мне кажется, я какой-то малопрозрачный.

А как апофеоз моих исследований - свойство Value. Я-то думал, что стоит ValueType установить в vtFloat, так сразу буду получать введённое пользователем значение типа Single. Ну да, кто-то кое-где у нас порой этого не хочет, ему подавай Int64, а Value - только Single. Но это уже не так важно, как то, что не получится получить значение "сразу". Я имею в виду, что если по какой-то причине вы захотите получить значение Value сразу, как только пользователь нажмёт Enter (Ну и что же тут криминального, если в OnKeyDown установить if Key = vkReturn then AddValue?), то скорее всего в Value будет что-то такое, что напоминает число в свойстве Text очень и очень отдалённо. Вешаться же на OnChange для того, чтобы отследить нажатие Enter на виртуальной клавиатуре я вам не советую - оно срабатывает совсем не только на Enter и совсем не всегда на Enter. Слава Богу, что он вдохновил программиста (или его начальника) вынести в интерфейс модуля FMX.Edit процедуру TryTextToValue, которую я использую сам, и рекомендую вам. Она запускает старый (как время летит!) добрый TryStrToFloat, которому входная строка причёсывается по пробелам и десятичному сепаратору. В случае неудачи возвращается дефолтное значение. Вот спасибо!

Эпилог

TNumberBox, не смотря на далеко не первую версию платформы FM,  выглядит довольно сыро. Возможно, это связано с небольшим опытом его применения программистами. Возможно,  это связано с небольшим количеством обращений программистов в qc.embarcadero.com. Я лично (это моя стандартная отговорка) плохо формулирую свои мысли даже на русском. А то, что получилось сложить - слишком многословно, чтобы внятно перевести на английский. Если вы чувствуете в себе силы написать о проблемах этого контрола в EMBT, то прошу вас, напишите. Если вы можете что-то подсказать мне по применению этого контрола - тоже пишите.

Спасибо за внимание!
Всем всего доброго!

ЗЫ: Спустя некоторое время я стал использовать специальную заглушку-перехватчик, чтобы обойти основные проблемы контрола. http://alhymov.blogspot.ru/2014/04/tnumberbox.html

вторник, 11 февраля 2014 г.

Помоги зоопарку. Samsung GALAXY Note 10.1,Fly IQ440 Energie,МТС 970H,Samsung Galaxy S III GT-I9300

Широко известный в наших кругах Всеводод Леонов обратился к общественности с манифестом Спаси Delphi, помоги зоопарку! Я уже писал ранее про Nexus 4. В ответ на призыв я отправил ему несколько заметок, некоторые из которых он опубликовал. Также Всеволод мне написал, что ничего плохого не случится, если я помещу свои заметки в своём блоге. Так красиво оформлять заметки, как он, я ленюсь, но думаю, что больших неудобств Читателю это не доставит.  Итак:

Samsung GALAXY Note 10.1 (http://www.samsung.com/ru/consumer/mobile-devices/tablet-pc-slate-pc/samsung-galaxy-note/GT-N8000EAASER-gallery)


Android 4.1.2. Купил в позапрошлом году  планшет супруге на ДР. Обошёлся с картой памяти примерно в 25 килорубля + чехол. Колебался - iPad или Android? Выбрал по цифрам самый мощный Андроид. Яблоки мне казались снобизмом, пугали я-мелодии, отсутствие поддержки Flash, отсутствие SD-карточки, платность всего-привсего, странность поведения поклонников торговой марки. 

После приобретения был неприятно поражён стоимостью чехлов и непродуманностью их конструкций. Оказалось, что планшет не поддерживает Flash! К тому же ещё и кабель имеет проприетарный разъём. Кнопки по периметру расположены неудачно - легко случайно выключить, взяв устройство за угол. По сравнению с яблоком - тяжёл, экран проигрывает в качестве. Гориллное стекло в женской сумке без чехла быстро получило заметную царапину от ключей. Рекламируемый стилус терялся пару раз и в настоящее время сидит в своём гнезде и практически не используется. Детям купил уже яблочные дощечки.


Аппарат был подключен к Win7 и сразу оснащён ПО Kies от производителя ещё до установки Делфи. Когда настала пора - сразу стал в работу. Но! При переходе на другой комп(Win8) работать с отладчиком отказался. Кто виноват? - Делфи? Win8? USB3? Оказалось, что если поставить Kies, то и тут всё работает - кто бы мог подумать?!


Fly IQ440 Energie (http://www.fly-phone.com/devices/smartphones/iq440_energie/?sphrase_id=56721)


Андроид 4.0.4. Это мой первый смартфон. Я посмотрел, как люди постоянно держат на верёвке свои аппараты, и решил взять повышенной ёмкости. К тому же, для меня было важно иметь 2 симки. Удалось уложиться в 10 килорублей вместе с картой на 32 Гига.


Аппарат действительно может целый день активно использоваться. Неактивно может легко провести выходные. Весьма тяжёл. Скоростью не балует. Фронтальная камера так плоха, что в помещении трудно считать штрих- или QR-код. Разъём под провод сбоку мешает разместить аппарат в моём автомобильном держателе. Плюс (точнее - минус) разъём довольно скоро хрустнул, и теперь провод слегка люфтит, что несколько тревожит, но пока не привело к каким-то проблемам. Большая проблема - подобрать гарнитуру: многие "пробки" шумят, тикают и стучат при прослушивании, а прилагаемые в комплекте свои - без резинок, в ушах не держаться.

Сначала аппарат был не виден, и я не знал почему - это был мой первый опыт с телефоном. У меня тогда оказался ещё другой аппарат (планшет жены), для которого я стал компилить программу, а потом просто заливал приложение на Fly и запускал. С Делфи телефон заработал через пару недель ВНЕЗАПНО! По ощущениям :) дело в том, что приходили люди с HTC-аппаратом и проставили свой софт. Увы, ничего подробнее сказать не могу - теперь Муха прекрасно себя чувствует и на Win7, и на Win8.


МТС 970H (http://www.shop.mts.ru/smartfony/mts/smartfon-970-black-dlya-raboty-v-seti-mts.html)


Андроид 4.1.1. Куплен ребёнку задёшево - меньше 3 килорублей. Вполне себя оправдывает. Народ интересуется - чё так дёшево? Глючный? Тормозной? Ни разу глюков и тормозов. Очень лёгкая машинка как по весу, так и по отзывчивости. По-шустрее моей энергичной мухи iq440, да и легче раза в два.

Дешевизна сказывается на экране - инерционность и узкий угол обзора. Не слишком приятно листать списки и смотреть фотки, особенно после iPad Air. Но, скажем, в Cut the Rope играть вполне можно. А небольшой размер - даже плюс для людей с пальцами менее 5":)


Сначала был не виден, и я уже знал почему - нужен драйвер. Почти первая ссылка в поисковике отправила меня на известный форум, а оттуда я попал на сайт настоящих производителей аппарата - alcatel(http://www.alcatelonetouch.com/global-en/support/faq/usbdriver.html), где, как рекомендовали на форуме, скачал именно ВТОРОЙ драйвер, и все заработало. В общем - всё оказалось довольно легко и быстро. :)

Samsung Galaxy S III GT-I9300 (http://www.samsung.com/ru/consumer/mobile-devices/smart-phones/samsung-galaxy/GT-I9300RWDSER-gallery)


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

При первом подключении в Делфи был не виден, хотя в диспетчере задач висел как вполне хорошее и нормальное "переносное устройство" GT-I9300. После включения на аппарате режима отладки (да, это тоже камлание - настройки, о телефоне, 10 раз ткнуть в номерок версии, вернуться, зайти в таинственно появившееся программисткое меню, включить отладку) появилось неопределённое Composite Device( Вин8 выдала банер "Составное устройство"). 


Яндекс массово советовал поставить ПО от изготовителя - Kies. Но я уже знаком с этим обеспечением и не хотел его излишней заботы. Поэтому выбрал на (уж не знаю, реклама ли это) сайте http://www.sidenxab.ru USB Android Драйвер. Как только диспетчер устройств снова увидел GT-I9300, Делфи также смогла с ним работать.

Вывод.


Я хочу, чтобы начинающие Андроид-программисты поняли, что "проблема подключения Андроид к Делфи" или "Делфи к Андроид" - не проблема Делфи, а проблема драйверов Windows. Тот, кто впервые сталкивается с этим в Делфи, винит в первую очередь Делфи, также как, например, в своё время винил я. Но на самом деле, если посмотреть вокруг, видно, что эта проблема встречается У ВСЕХ систем разработки, а не конкретно у Делфи. Так что некоторые трудности с андроидными устройствами - это, как говорит Елена Малышева, НОРМА. И не надо расстраиваться, просто надо понимать ситуацию, и знать, куда копать. А копать-то на самом деле особо глубоко - и не нужно. Просто Диспетчер Устройств, просто Драйвер.

Всем удачи!!!