четверг, 19 февраля 2015 г.

Как обоблачиться с YandexDisk

Эту статью я написал потому, что сам очень хотел прочитать такую - о сервисах Яндекса, авторизации OAuth, о том, как держать бубен, чтобы коробочный Indy выполнял запросы по HTTPS. Это всё не сложно на самом деле, но прочти я такую статью раньше - глядишь, и если бы не одним седым волосом в моей бороде было бы меньше, то уж наверняка одним бы волосом на моей макушке - больше. 

Часть I. Теория

2.1 Подключение ЯД к Windows
2.2 Создание папки приложения
2.3 Indy и HTTPS
2.4 Скрипач как прокси
2.5 Протокольные компоненты, часть 1. TidHTTP и JSON
2.5 Протокольные компоненты, часть 2. TREST~ и TDataSet

YandexDisk (далее - ЯД, в хорошем смысле) может служить не только в качестве личного облачного хранилища, но и выступать как источник данных для вашего приложения.  Если вам нужно просто иметь доступное отовсюду место, куда можно заглянуть на предмет наличия каких-либо новостей, то бесплатное облако Яндекса может вполне удовлетворить ваши потребности.


В качестве примера давайте сделаем по следам Праздника Влюблённых (или в преддверии Международного Женского Дня) приложение для украшения фоток. Возьмём фотографию с камеры или из галереи, а потом налепим на неё каких-нибудь прикольных штучек. Например, пусть это будут сердечки, цветочки, звёздочки, очки, усы, рамки, буквы... Вот, видите - ассортимент может быть самый разнообразный. Не обновлять же всякий раз приложение, когда мы творчески разродившись добавим в нашу коллекцию приколов новую фишку? Значит, надо дать приложению возможность скачивать эти картинки по мере их создания.

Чтобы построить наше  фишкохранилище давайте заведём аккаунт на ЯД(https://disk.yandex.ru).

Думаю, ввести своё имя и телефон вы сможете без подсказок. Что касается предлагаемого Яндексом клиента для Windows, то тут советую подумать - нужно ли вам, чтобы каждый раз, когда вы сделаете копию экрана или воткнёте в телефон шнурок, выскакивало окошко с просьбой забрать у вас свеженький контент. Меня лично такая настойчивая забота несколько отпугивает. А подключить ЯД к проводнику Windows можно и другим, менее "дружественным" способом - через "Сервис->Подключить сетевой диск". Об этом позднее.

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


Ну, во-первых, давайте не будем рассматривать "внешние ссылки", т.к. они на самом-то деле ведут не к требуемому файлу, а на страницу скачивания, где искать уже ссылку на исходный файл - задача архисложная для слишком больших любителей. Мы пойдём другим путём(© Владимир Ильич Ульянов) - API Диска.

API Диска предлагает Виджеты Диска, REST API Диска, SDK Диска и WebDAV API Диска. Виджеты нам в приложении вряд ли помогут, SDK (основан на WebDAV) для Delphi пока не сделали, а выбор между REST и WebDAV - однозначно в пользу REST, т.к. о REST слышал даже самый ленивый менеджер проектов, а рассказывать на собеседовании, что вы освоили какой-то "Web-Dove" - ну, мало ли уже в компании всяких Web-разработчиков и Web-дизайнеров, так что не будем путать людей.

Естественно, это  всё шутки. А на самом деле я исхожу из того, что при определении разрешённых действий REST-ЯД даёт возможность ограничить доступ к диску рамками папки приложения. Если учесть, что к одному номеру телефона Яндекс разрешает привязывать только 3 аккаунта, то ограничение приложения своей папкой - весьма значительный фактор. Так что, выбор - очевиден.

Ну что ж, решив, в каком направлении будем двигаться, начинаем. Первым пунктом нам предлагают получить для своего приложения OAuth-токен. OAuth (Open Authorization, я так думаю ©Рубен Вартанович Хачикян) - это что такое? Это когда нам надо получить куда-то доступ, а пароль за нас вводит кто-то другой. Это весьма странная штука, найти аналогию которой в реальном мире довольно затруднительно.



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


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

А где же токен? Вернёмся на шаг назад (кнопка "Редактировать") и ещё раз глянем в низ анкеты - там ясно написано, что будет токен, время жизни которого "Не менее, чем 1 год"!


   
Вот, кажется, и пришла пора ознакомиться с документацией, в которой какие-то схемы взаимодействия, УРЛы обратного вызова и много-много букв. Ну-ну-ну, не будем отчаиваться! Давайте просто немного подумаем, прикинем, что должно происходить в процессе, и наверняка разберёмся без лишних страданий.

Итак, как уже было замечено, айди - это не просто номер приложения, а обозначение списка ресурсов. Причём сейчас стало совершенно очевидно, что ресурсы принадлежат не приложению, а пользователю. Т.е. чтобы добраться до ресурса, нужен логин и пароль пользователя. А логин и пароль пользователя - это доступ не к ресурсам из списка, а ко ВСЕМ ресурсам пользователя, это ПОЛНЫЙ доступ ко всем ресурсам, включая их администрирование. Поэтому, если чисто конкретно для своего аккаунта мы можем вписать в своё приложение логин и пароль, то ждать от стороннего пользователя, чьими, скажем, фотками (только фотками и ничем больше!) мы хотим воспользоваться, что он для этого станет вводить нам в диалог свои входные данные - это, мягко скажем, слишком большие ожидания.

И как же тогда быть? Или точнее - как же они это делают?

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

Да вы наверняка сами часто видели такие окошки, когда какая-нибудь "Забавная ферма" или "Джонни-попрыгунчик" просят вас поделиться своими успехами где-нибудь в FaceBook® или в Одноклассниках®. И, если вы заметили, часто даже и пароль вводить не нужно - обычно он уже давно лежит в куках.
{Здесь должна быть картинка из "Забавная ферма" или "Джонни-попрыгунчик" и авторизация кого-нибудь в чём-то из, но я не играю ни в то, ни в другое, и поэтому у меня нет такой картинки.}
У Яндекса это окошко вызывается УРЛом https://oauth.yandex.ru/authorize?response_type=token&client_id=<идентификатор приложения>[&display=popup][&state=<произвольная строка>]. Добавка display=popup - это чтобы без навигации Яндекса и прочего дополнительного контента. Такой минимализм изначально предназначался для небольших экранов, но на самом деле, я считаю, для диалога в приложении по-другому и быть не должно. Параметр state - это придумываемая автором приложения метка, которая вместе с выдаваемым токеном пойдет по тому УРЛу, что мы задали во время регистрации приложения. Для Web-приложений может быть очень удобно указать свою точку входа, общую для нескольких идентификаторов, и по параметру state судить о том, какое приложение сейчас получило токен. 


Поскольку у нас не Web-приложение, а нормальное настоящее, то нам надо показывать форму с компонентом TWebBrowser, у которого при старте следует вызвать Navigate(<тот самый УРЛ>). Когда нажимается "Разрешить" и уходит POST с формы, обратно возвращается 302 - Temporary moved с "новым" Location https://oauth.yandex.ru/verification_code#access_token=<желанный OAuth-токен>&token_type=bearer&expires_in=31536000. Разумеется, браузер тут же попытается открыть эту страничку, где будет - что? - правильно, наш токен, аккуратно написанный на милой страничке  красивым почерком. Конечно, страничку грузить не обязательно, а достаточно повеситься на событие OnBeforeNavigate2 и вырезать параметр access_token=. Всё!

А вы знаете, что форма с браузером уже есть в готовом виде? Она лежит  в  $(BDS)\Source\data\rest в виде двух модулей REST.Authenticator.OAuth.WebForm - .Win и .FMX. Как ею пользоваться - в примерах Object Pascal\Database\RESTDemo\RESTDemos.dpr. В форму добавлена нерусская кнопка Close, событие редиректа почему-то отлавливается с двух событий - Before и After, а сам обработчик поиска токена в УРЛе в комплектацию формы не входит. Так что, если надумаете пользоваться, советую скопировать модуль с формой в папку проекта и подшаманить, заглядывая в RESTDemos.

А как же компоненты с такими интересными названиями - TOAuth1Authenticator и TOAuth2Authenticator? Увы, господа, эти компоненты ничего не делают. Ну, то есть не то, чтобы совсем ничего, но, скажем так, ничего, что приводит к вызову той самой аутентификации в окошке. Это всего лишь пристёжка к TRESTClient, которая добавляет в параметры уже полученный ранее  OAuth-токен. Често говоря, мне кажется, что делать для этого компонент - как из пушки по воробьям, проще самому добавить параметр ручками. Но тут играют роль общеконцептуальные соображения, т.к. аутентификация может быть не только OAuth.

Ну, вот. Откуда токен - мы знаем: из УРЛа с идентификатором приложения. Что такое OAuth-токен - мы тоже уже понимаем: токен - это идентификатор связки прав доступа с ресурсом пользователя. Срок действия - 31536000 секунд - позволяет нам зашить этот токен в приложение, и уже безо всяких особых форм с браузерами просто работать с Яндексом через REST или другой API.

--------------------------------------
Чтобы найти своё приложение я захожу на ЯД, выбираю "Разработчикам", "REST API Диска", "OAuth-токен" и тычу в. Это почему-то кажется мне проще, чем просто набрать oauth.yandex.ru.



2.1 Подключение ЯД к Windows
2.2 Создание папки приложения
2.3 Indy и HTTPS
2.4 Скрипач как прокси
2.5 Протокольные компоненты, часть 1. TidHTTP и JSON
2.5 Протокольные компоненты, часть 2. TREST~ и TDataSet

пятница, 30 января 2015 г.

Как я боролся с iOS 8.1.3

Давно я не брал в руки йОС, но давеча мне говорят: "ну-ка, покажи нам на айпаде пару новых идей"! Я говорю: "сей момент"! И показываю. А потом, пока я ещё радостный и добрый, айпад тихо так говорит, что хочет чего-то новенького - обновиться желает. И я широким жестом разрешаю, а мне - оп-па! - и геморрой хидэйк на сутки - Unable to install package. (e800003a). и Unable to install package. (e8008016).

Ребята, кому есть до этого дело:
https://www.danielwolf.eu/blog/2015/1458-delphi-mobile-und-ios-8-1-3
http://blogs.embarcadero.com/ao/2013/05/24/39472

четверг, 22 января 2015 г.

Как хранить числа в объектах TStringList

Как хранить числа в объектах TStringList прекрасно известно: MyStrings.AddObject( MyString, TObject( MyNumber ) )...казалось бы. Однако, мне (да вот - дошла очередь и до меня) пришлось убедиться, что к словам "прекрасно известно" следует добавить слово "было" - прекрасно известно было. Увы, двадцать лет работавший в Win32 и даже в Win64 способ теперь уже не работает. Точнее, не работает на мобильных платформах, т.е. там, где действует незримая власть коварного AUTOREFCOUNT aka ARC.

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


Цвет идёт к справочнику в виде строки #rrggbb - тут StringToAlphaColor() сразу в руки. Получаем бинарное значение TAlphaColor, которое на самом деле = type Cardinal, т.е. то, что отличнейшим образом укладывается в AddObject() рядом со строчкой из справочника. Теперь, получая данные, я легко в справочнике найду соответствующий цвет: 'критично'-#ff0000, 'неважно'-#cccccc, и т.д. Один раз цвета распознал, а потом пользуйся сколько влезет. Только обязательно надо не забыть включить для цвета канал Alpha (например, Result := TAlphaColorRec.Alpha + AColor), а то нарисованное будет настолько прозрачным, что его станет совершенно невозможно разглядеть.

Связка справочников - тоже штука банальная. У меня в одном из двух справочников дано дефолтное значение из второго. Надо при заполнении TStringList просто бежать по первому списку и искать в предварительно заполненном втором IndexOf() и класть в объекты первого списка найденные индексы, а текущий счётчик класть, разумеется, в объекты второго списка с этими индексами. Тут, правда, есть нюанс: нулевое значение nil, которым инициализируются объекты списков, будет в виде целого числа равно нулю, что означает не пустое значение, а индекс первого элемента. Надо быть внимательнее - ставить -1 везде, где только можно, или при сохранении найденных индексов добавлять единицу, а потом об этом надо не забывать, и оперировать уже с отниманием единицы. Я лично предпочитаю второй способ - точно не нарвёшься на случайный ноль.


Не долго думая, я сделал прототип на Win32. И всё у меня прекрасно заработало: метки красились нужным цветом, словари дружно связывались. Ура! Осталось только скомпилить под Андроид, который требовался Заказчику, и ... Вот тут-то и обнаружилось, что связки не связываются, а цвета не цветут. Как же так?

Чтобы не рвать куски из проекта, возьмём надуманный пример. Создадим проект пустого мобильного приложения, добавим в дереве проекта платформу Win32 и вставим на событие OnCreate главной и единственной формы код, что дан ниже (имя формы только подкорректируйте под себя). Ах, да - не забудем ещё бросить на форму TMemo c Align = alClient ( все эти манипуляции читатель может делать мысленно ).

const
  Labels: array [1..5] of String = ('Lorem','ipsum','dolor','sit','amet');
 
procedure TForm6.FormCreate(Sender: TObject);
var
  I: Integer;
begin
  with TStringList.Create do
  try
    for I := Low(Labels) to High(Labels) do
      AddObject( Labels[ I ], TObject( I ) );
    Sort;
    for I := 0 to Count-1 do
      Strings[ I ] := Format( '%s=%d', [ Strings[ I ], Integer( Objects[ I ] ) ] );
    Memo1.Lines.Text := Text;
  finally Free;
  end;
end;

Ожидаемый результат получен:


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

А у меня в проекте этот фокус прошёл. Я поназаполнял словарей, развесил там индексы и цвета и пошёл дальше читать уже данные. И только когда стал доставать из закромов все свои богатства, то обнаружил сплошные nil. И сильно удивился - мыши съели?! - Так нет здесь мышей! Ну да говорят, что удивляться - это даже полезно.

Что делать? Проект - вон он уже дышит и играет мускулами! Не будешь же перетрахивать (© Лукашенко А.Г.) его из-за какой-то мелочи. Сейчас дадим таблеточку, и будет как здоровый!

type
  TData<T> = class
    FValue: T;
    constructor Create( const AValue: T );
  end;
 
const
  Labels: array [1..5] of String = ('Lorem','ipsum','dolor','sit','amet');
 
procedure TForm6.FormCreate(Sender: TObject);
var
  I: Integer;
begin
  with TStringList.Create( True ) do
  try
    for I := Low(Labels) to High(Labels) do
      AddObject( Labels[ I ], TData<Integer>.Create( I ) );
    Sort;
    for I := 0 to Count-1 do
      Strings[ I ] := Format( '%s=%d', [ Strings[ I ], TData<Integer>( Objects[ I ] ).FValue ] );
 
    Memo1.Lines.Text := Text;
  finally Free;
  end;
end;
 
{ TData<T> }
 
constructor TData<T>.Create(const AValue: T);
begin
  FValue := AValue;
end;

Просто? Да! Мне даже как-то понравилось - появилось ещё "пустое значение": nil=Null. Так что и игры с плюс-минус единицей для индексов можно выкинуть. А если, например, нужно Альфа-канал цвета устанавливать по месту, то уже ни за что не спутаешь чёрный цвет с прозрачным - 0 уже не равен nil! И кода лишнего - с гулькин нос. Главное - при создании TStringList включить  параметр OwnsObjects, чтобы объекты прибивались самим списком, и всё. Красота!


...............

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

вторник, 20 января 2015 г.

Как несовершенны константные аргументы

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

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

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

Полазил я в CPU на предмет совпадения адресов - но это так, чисто посмотреть с укором в подкапотное пространство. А дай, думаю, присвою той локальной переменной значение аргумента. Присваивается, идёт дальше, и вот в момент "волшебного" присвоения - Бах! - неверная операция с указателем! И только тогда, когда аргумент не пуст.

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

Это просто счастливое совпадение, что мёртвая ссылка точно легла на локальную переменную! 

Лечение прошло банально - передачей параметра по значению. Но каково?! Как компилятор не отследил, что область памяти занята процедурой?! 

К сожалению, я не смог воспроизвести ситуацию на простом примере. И если кто-нибудь понял что за оптимизация такая у компилятора происходит, то пусть попробует создать пример в пробирке для quality.embarcadero.com. А я свой рассказ заканчиваю. Спасибо за внимание!

Как побег лямбды из цикла удивляет компилятор

Это такая шутка - заставить компилятор выдать [dcc32 Fatal Error] InternalError.dpr(15): F2084 Internal Error: C22972.

Попробуйте написать в цикле лямбда-выражение, в котором есть оператор управления циклом - break или continue. Причём не важно, какой цикл - for, while, repeat. Компилятор от этого не умрёт, но и сказать ничего умного не сможет.

program InternalError;
{$APPTYPE CONSOLE}
begin
  while True
  (procedure begin
    break
  end)();
end.
 
Так ему! Пусть не думает, что умнее паровоза! Пусть выучит и повторяет: BREAK or CONTINUE outside of loop!

..........................

Не, ну что это я так на него в самом деле? Всё-таки он не совсем крэшанулся, не сбил Дэлфу, не открыл синий экран - тоже своего рода утешение. Ну, в конце-то концов он же понял, что тут что-то не так! И отреагировал, как уж смог.

Ну, ладно. Иди сюда мой хороший, давай я тебя обниму. Ну, что ты, что ты? Расстроился? Ну, ничего, не плачь, всё будет хорошо... А вот мы сейчас с тобой ещё одну программку покомпилим, хорошую программку... А та програмяка - бяка! Фу, брось её! Пусть там на quality.embarcadero.com с ней разбираются, а нам она не нужна, у нас и так программок много. Пойдём, моя радость!

суббота, 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 по шнурку Яндекс его тут же отследил и сообщил, что покупать приложение уже не нужно, а можно просто взять и открыть! И таки да, открывает.

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