Вдогонку к предыдущему посту еще немного поговорим о FireMonkey. Очень неприятная для меня вещь — ощущение, что при разработке стандартных компонентов никто и не задумывался, что от них кто-то будет наследоваться, пытаться расширять и изменять их поведение. Если сравнивать с VCL, то доступных компонентов FireMonkey сейчас очень и очень мало. И мне кажется, что судьба фреймворка сегодня во многом зависит именно от удобства разработки новых компонентов.
Разберу конкретный пример.
Продолжаю работу с TTabControl. На этот раз я хочу получить возможность размещать компоненты в неклиентской области. Признаюсь, хочу повторить функционал браузеров — разместить кнопку для открытия новых вкладок справа от них.
Проблемы быть не должно, так как в FireMonkey все стилизованные контролы могут быть контейнерами для других контролов. Особенность данной конкретной ситуации в том, что в этом месте просто напросто нет ничего, на чем можно было бы размещать компоненты.
Реализация очевидна. Нужен контрол с простейшим стилем (скажем, только TLayout), его расположим на TTabControl’е и будем позиционировать по аналогии с вкладками. Напомню, что вкладка здесь — это только сама кнопка, мне же хочется всего-лишь оказаться правее кнопок.
Смотрим на метод TTabControl.Realign. Метод длинный, но из песни слов не выкинешь. Можно перемотать, его суть в двух словах я перескажу чуть позже.
procedure TTabControl.Realign; var Idx, i: Integer; CurX, CurY: Single; AutoWidth, MaxHeight: Single; B: TFmxObject; begin if FDisableAlign then Exit; FDisableAlign := True; try { move all non TabItem to end of list } if FChildren <> nil then for i := 0 to FChildren.Count - 1 do if not(TFmxObject(FChildren[i]) is TTabItem) then TFmxObject(FChildren[i]).Index := FChildren.Count - 1; { calc max height } MaxHeight := 0; Idx := 0; if FChildren <> nil then for i := 0 to FChildren.Count - 1 do if TFmxObject(FChildren[i]) is TTabItem then with TTabItem(FChildren[i]) do begin if not Visible then Continue; FIndex := Idx; if Height + Padding.top + Padding.bottom > MaxHeight then MaxHeight := Height + Padding.top + Padding.bottom; Idx := Idx + 1; end; if Idx = 0 then MaxHeight := 0 else if FTabHeight > 0 then MaxHeight := FTabHeight; { background } if FResourceLink <> nil then begin B := FResourceLink; if (B <> nil) and (B is TControl) then begin TControl(B).Align := TAlignLayout.alNone; TControl(B).SetBounds(TControl(B).Padding.left, MaxHeight + TControl(B).Padding.top, Width - TControl(B).Padding.left - TControl(B).Padding.top, Height - MaxHeight - TControl(B).Padding.top - TControl(B).Padding.bottom); TControl(B).BringToFront; end; end; { align } CurX := 0; CurY := 0; AutoWidth:= Width; if FBackground <> nil then AutoWidth := Width - FBackground.Margins.left - FBackground.Margins.right; if FFullSize and (Idx > 0) then AutoWidth := AutoWidth / Idx else AutoWidth := AutoWidth; if FChildren <> nil then for i := 0 to FChildren.Count - 1 do if TFmxObject(FChildren[i]) is TTabItem then with TTabItem(FChildren[i]) do begin if not Visible then Continue; Align := TAlignLayout.alNone; FContent.Align := TAlignLayout.alNone; FContent.Visible := Index = TabIndex; FContent.DesignVisible := (Index = TabIndex); FContent.ClipChildren := True; if FContent.Visible then FContent.BringToFront; if FFullSize then SetBounds(CurX + Padding.left, CurY + Padding.top, AutoWidth, MaxHeight - Padding.top - Padding.bottom) else SetBounds(CurX + Padding.left, CurY + Padding.top, Width, MaxHeight - Padding.top - Padding.bottom); CurX := CurX + Padding.left + Width + Padding.right; end; finally FDisableAlign := False; end; inherited; end;
Здесь в общем-то никакой магии нет. Метод по большому счету делает только три важные вещи:
- Пробегает по списку вкладок, определяя максимальную высоту вкладки (MaxHeight);
- Располагает «тело» TTabControl’а на доступном ему пространстве, сдвигая его верхнюю часть вниз на эти самые MaxHeight пикселей;
- Таким образом сверху остается полоска свободного пространства и там спокойно можно расположить кнопки вкладок.
К этому всему теперь требуется добавить установку позиции моей новой неклиентской панели. Что для этого требуется? Значение MaxHeight, чтобы знать высоту данного пространства, и координаты крайней правой кнопки. К тому же нужно внести небольшие изменения в логику размещения кнопок, запретив им занимать всю ширину TTabControl’а — у вкладок не должно быть возможности сделать ширину неклиентской панели меньше какого-то установленного значения.
И, честно говоря, я не вижу способа сделать это красиво. Перегрузка Realign не поможет. В итоге придется продублировать довольно приличные по объему куски кода. Например, MaxHeight будет вычисляться дважды. Это конечно мелочь, но быстродействия не прибавит, а вот багов прибавить может.
Не буду предлагать ничего глобального, покажу только к чему мог бы привести маленький и очень простой рефакторинг. Давайте признаемся сами себе, что сейчас Realign если и не нарушает «single responsibility principle», то явно балансирует на грани.
Нужно разбить метод Realign на более мелкие части. Тем более, разработчик компонента уже сделал половину работы, разделив его на 4 части комментариями.
{ move all non TabItem to end of list } { calc max height } { background } { align }
Таким образом, получилось бы что-то вроде:
procedure TTabControl.Realign; var MaxHeight: Single; begin SortItems(...); MaxHeight := CalcMaxHeight(...); AlignBackground(MaxHeight, ...); AlignTabs(MaxHeight, ...); inherited; end;
Итого 4 новых метода со старым кодом. Каждый из которых должен быть виртуальным. Тогда вся моя работа свелась бы к перегрузке метода AlignTabs. В нем бы и значение MaxHeight было доступно, и логику размещения вкладок я бы смог изменить, не затрагивая всего остального, ну и конечно же добавить новую панель.
К сожалению, описанная проблема — это не частный случай. Покажу еще один пример.
TMemo в FireMonkey реализован довольно интересно. Текст рисуется простым FillText’ом на канве, а для позиционирования каретки используется соответственно вычисление высоты строк и ширины фрагмента текущей строки. Возможности кастомизации на первый взгляд безграничны.
Мне показалось интересным попробовать реализовать простенькую подсветку синтаксиса. Рисование устроено просто: обработчику OnPaint канвы присваивается метод DoContentPaintWithCache, он когда нужно вызывает DoContentPaint для рисования, а когда можно просто рисует старую картинку из кэша.
Проблемы начинаются с того, что обе функции не виртуальны. То есть я не могу просто перегрузить DoContentPaint, я должен сначала скопировать в своем наследнике код DoContentPaintWithCache, заменив вызов DoContentPaint на свой новый метод рисования. А затем эту копию установить обработчиком OnPaint. Только что произошло дублирование ~50 строк кода. Одно лишь ключевое слово «virtual» в объявлении метода TMemo.DoContentPaint избавило бы от всего этого.
Но это еще не вся проблема. DoContentPaint — большой метод (~120 строк), а мне всего-лишь нужно заменить в нем простой FillText на что-то чуть более сложное. То есть снова необходимо скопировать весь код, заменив в нем пару вызовов.
Если идти дальше и изменять не только цвет отдельных слов, но и делать их полужирными (фактически изменять ширину), придется немного доработать логику позиционирования каретки. Метод TextWidth, который используется при вычислении позиции каретки в пикселях, в текущей реализации конечно же этих нюансов не учитывает. Но и этот метод не виртуален. К слову, он даже приватный. Непонятно почему.
Disclaimer: Не хочу быть неправильно понят, на самом деле затея с FireMonkey мне нравится. Но вот головой о стены я побился уже изрядно. Желаю Embarcadero терпения и удачи в доводке этого фреймворка до ума.
1. Андрей
19 Июл 2012 11:24 дп
По моим наблюдениям многие крупные производители (например, таких продуктов как 1С, Галактика, SyteLine и др.) при постоянных разговорах о юзабилити не особо парятся как по части внешнего вида, так и по части функционала. Я к тому, что цена реализации проекта может быть слишком высокой в случае определенного количества работ по доработке применяемого инструментария. Поэтому не всегда целесообразно копировать какой-то функционал, если для его реализации нет соответствующих средств. Добавим к описанному тот факт, что разработчики средств разработки не заинтересованы в реализации частных решений, они делают более общий функционал, чтобы максимально увеличить продажи. В итоге, описанный в статье конфликт интересов неизбежен для любого ЯП.
Проблема выбора средств разработки усугубляется необходимостью реализации кросплатформенных решений (на данный момент, на мой взгляд, есть потребность в реализации версий одного проекта для десктопа, веба и мобильных устройств). Значимость описанных проблем и способы их решения каждый определяет сам. Для себя решил, что программирование как таковое не должно рассматриваться обособленно, а только как часть экономических взаимоотношений. После этого меняются многие приоритеты. Кроме этого считаю, что не стоит сильно привязываться (т.е. становится зависимым) к какому-то одному ЯП, т.к. очень часто мы слышим или о появлении какого-то нового чуда, которое интенсивно начинают пиарить (например, HTML 5) или о прекращении поддержки какой-то технологии, которую раньше пиарили с неменьшим размахом (например, QT).
Может изъясняюсь немного сумбурно, но большинство проблем с ЯП к ЯП отношения не имеют (поэтому и решить их средствами ЯП невозможно), а являются следствием современной модели экономических взаимоотношений.
2. Алексей
19 Июл 2012 1:20 пп
Я к FireMonkey отношения не имею(у меня нет XE2), но процесс его создания мне не понравился: куча багфиксов, в т.ч. ~5 Help Update’ов, я думаю, не только у меня сложилось впечатление,что продукт был выпущен немножко «сырым». И, как пример, такие страшные проблемы с созданием новых контролов.
p.s. Против самой идеи ничего не имею: пусть люди поразвлекаются)), а я остался верен VCL.
3. Роман Янковский
19 Июл 2012 6:19 пп
Андрей,
Это утверждение для меня не очевидно. А в остальном согласен.
Алексей, поторопились они с релизом, явно потороплись.
4. Андрей
19 Июл 2012 11:06 пп
>>Это утверждение для меня не очевидно. А в остальном согласен.
Тут в двух словах не объяснишь. Нужно изучать основы реальной (не той, о которой рассказывают в учебных заведениях) экономики. Обыватели наивно полагают, что основная задача фирмы — получение прибыли. На самом деле основная задача фирмы — постоянное увеличение прибыли. Если немного поразмыслить на эту тему, то все должно быть очевидно.
Что мы имеем по факту: например, 1С написана на С++, т.е. на С++ нет готового фреймворка для реализации учетных систем, иначе все бы использовали для автоматизации бух. учета С++, а не 1С. 1С — не средство разработки приложений в общем, т.к. ее функционал не позволяет реализовать многие задачи, например, аудиоплеер…. Вы не найдете ЯП, в котором разработчик среды предоставил бы какой-нибудь компонент для реализации какой-то конкретной коммерческой задачи целиком, обязательно нужно что-то докупить (при реализации учетных систем в Delphi это скорее всего компоненты доступа к БД, грид и генератор отчетов) и еще немало покодировать…
5. Наиль
21 Июл 2012 7:12 дп
Не соглашусь с Андреем.
Нынешние болячки Firemonkey — это явления эволюционного характера, а не экономического. FastReport планирует выпустить FireMonkey генератора отчётов. Очевидно, что без генератора отчётов постоянное увеличение прибыли не реально. Кому нужен инструмент, с помощью которого нельзя создать продукт корпоративного уровня? Поэтому разработчикам FireMonkey нет смысла ограничивать расширение числа сторонних компонент.
Другое дело, что нельзя сразу определить, что понадобиться при наследовании, а что нет. Частично это решается написанием парных компонент (TCustomForm -> TForm). Но учесть потребности сразу всех возможных наследников, это вряд ли поможет. Иногда и в VCL можно наткнуться на методы, которые хотелось бы иметь в protected секции, а не в private.
Убедиться в моей правоте или в правоте Андрея мы сможем тогда, когда выйдет вторая версия Firemonkey. Тогда будет видно, насколько разработчикам сторонних компонент станет легче жить.
Не согласен я и с мнением, что релиз FireMonkey состоялся рано.
Сырые продукты, вроде Delphi 2006 — это действительно было рано.
А FireMonkey продукт не сырой. Просто он не учитывает всех потребностей.
Это абсолютно новая технология. И чтобы она получила правильное развитие, нужно чтобы народ понял что это такое, и тогда будет ясно чего не хватает.
Windows 95, Windows 2000, Windows Vista — это новые технологии в том виде, как их видели разработчики. Windows 98, Windows XP, Windows 7 — это те же технологии, но которые учитывают потребности потребителей.
Без первой версии, не может быть второй. Главное, чтобы первая версия не разваливалась в руках. А про FireMonkey я такого ещё не слышал. Значит будем ждать вторую версию. И получать удовольствие от тех возможностей, которые предоставляет версия первая.
Ссылки в тему
http://www.fast-report.com/ru/download/fast-report-firemonky.html
6. Alex W. Lulin
31 Мар 2013 10:27 пп
Disclaimer: Не хочу быть неправильно понят, на самом деле затея с FireMonkey мне нравится. Но вот головой о стены я побился уже изрядно. Желаю Embarcadero терпения и удачи в доводке этого фреймворка до ума.
Полностью согласен.
7. Alex W. Lulin
31 Мар 2013 10:30 пп
Нужно разбить метод Realign на более мелкие части. Тем более, разработчик компонента уже сделал половину работы, разделив его на 4 части комментариями.
(y)(y)(y)(y) :-) :-) :-)
8. Alex W. Lulin
31 Мар 2013 10:32 пп
«TMemo в FireMonkey реализован довольно интересно»
я бы с ним не связывался бы. По-моему проще свой написать. Я думаю об этом.