воскресенье, 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

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