PDA

Просмотр полной версии : Архитектура интерфейса со стилями...


JackFromChaos
31.12.2010, 06:25
Как лучше подходить к вопросу?
Ну к примеру у нас есть некий сингелтон с указателем на текущий стиль(ну там цвета всякие, шрифты и т.д.).
А еще есть какой нибудь компонент, например Label(аля контейнер для TextField). Вопрос, как правильно сообщить всем экземплярам Label, что стиль изменился?
Первое что приходит голову, это подписаться в Label(ну или базовом компоненте) на какое нибудь глобальное событие типа ChangeStyle. Но смущает, что при таком подходе если принудительно нигде не отписаться от события, экземпляр Label никогда не удалится, даже если он уже не нужен.

Вариант решения – useWeakReference. Это нормальное решение? Есть ли альтернативы?

alatar
31.12.2010, 12:44
Посмотрите как сделан Flash Camouflage (http://code.google.com/p/flash-camouflage/).

JackFromChaos
31.12.2010, 16:05
Посмотрите как сделан Flash Camouflage (http://code.google.com/p/flash-camouflage/).
Спасибо за ссылку. Правда ответ на свой вопрос еще не нашел, но втыкнул... Много любопытного в сырцах... Буду дальше втыкать... :)

Wolsh
31.12.2010, 16:08
Я делал у всех контролов сеттер style, принимающий объект стиля. Стили передавал по цепочке, т.е. каждый экземпляр получал объект стиля от родителя и в свою очередь передавал нужный детям. Стили были вложенными (загружались через самодельный CSS), т.е. скажем стиль для окна приложения содержит стиль для Главного меню, этот стиль содержит в себе стили для панели и для кнопок меню, те - содержат стили для панелек-стейтов и стили для лейблов. В результате при смене стиля вызывается сеттер style у окна приложения, а там уже по цепочке все перерисовывается.
Посмотреть можно здесь (http://wolsh.narod2.ru/experience/css_for_components/), но без исходников.

alatar
31.12.2010, 16:31
Правда ответ на свой вопрос еще не нашел, но втыкнул...
Обычно как у Wolsh, плюс добавляются setStyle / getStyle для смены конкретного "свойства" стиля.

JackFromChaos
31.12.2010, 17:12
Данный подход будет работать, если у нас есть информация о всех элементах интерфейса, например все окна будут находится внутри некоторого родительского элемента и мы можем обойти рекурсивно их все.
А я часто применяют концепцию окон которые держат статичные ссылки на себя самих и имеют статичную функцию Show. Это гарантирует существования любого уникального окна не более чем одном экземпляре. Создаются по необходимости, существуют до конца завершения приложения. Но при закрытии детачаться от основной сцены.

Пока писал, пришло в голову, что можно во первых обрабатывать все рекурсивно от основной сцены, и запускать аналогичную обработку в функции Show. В принципе вполне выход...

Wolsh
31.12.2010, 17:26
Мне самому эта система показалась громоздкой (хотя пришел я к ней через несколько поистине чудовищных концепций))), конкретно та часть, где надо каждому компоненту/контролу иметь стиль по-умолчанию плюс реализацию того, о чем говорит alatar, плюс фактически разборщик объекта стиля, заменяющий только те свойства, что пришли в объекте и оставляющий нетронутыми дефолтные.. довольно много кода добавлялось в каждый компонент, а обобщить и наследовать этот разбор как-то не очень получалось, свойства то у всех разные)) Так что этот проект пока лежит на полочке и ждет просветления в мозгах. Но это касается только способов реализации - сама концепция меня вполне устраивает. От системы, основанной на событиях, я отказался сразу (и оставил текст о ней в той демо-флэшке как напоминание об альтернативном варианте))). Смутил такой момент - допустим есть Лейбл в кнопке, которую Вы разместили в меню, и есть лейбл скажем в одном из виджетов СтатусБара. Получив событие "Меняем Стиль", они должны запросить стили для себя у менеджера. И тот и другой - экземпляры Лейбл, но в разных компонентах, и должны запрашивать разные объекты стилей. Не просто getStyle("Label"), а что-то вроде getStyle("mainWindow.MainMenu.MenuButton.Label"). Конечно, в принципе передать Лейблу всю эту цепочку не трудно. Просто такая логика показалась мне.. упадочной. Не логичней ли передать по этой цепочке сам стиль? Не логичней ли выглядит сама стилизация, когда Меню получает объект стиля, содержащий стили для всех элементов этого Меню, а кнопка - для всех элементов кнопки? Когда в расположение роты отправляется грузовик тушенки и там на месте распределяется, а не каждый солдат каждой роты лично ломится на склад за своей банкой тушенки.

Добавлено через 30 минут
Ах да, еще такой момент -
Меню получило свой новый стиль, в котором, скажем, указано расстояние между кнопками, и перераспределило все кнопки с учетом их высоты. В то же время каждая кнопка получила свой стиль, и изменила свою высоту. Выровняв при этом Лейбл с учетом его высоты) А Лейбл получил свой стиль и поменял наконец размер шрифта на 2 пункта)). Кажется, надо заводить еще и систему, управляющую последовательностью стилизации? Если Меню не знает новый размер кнопок, которые понятия не имеют, каким будет размер их Лейблов после изменения стиля. Ниспадающий вызов перерисовки (сеттера style) гарантирует нам, что Меню не будет перерисовано, пока Лейбл не изменит шрифт. А в случае с событиями нам, похоже, придется делать обратный поток, в котором каждый компонент должен будет дождаться от своих элементов события подтверждения окончания перерисовки, и только после этого будет перерисовываться сам и отправлять событие окончания "наверх". Не знаю, как вам, а мне такая система совсем не нравится.

alatar
31.12.2010, 18:06
Пока писал, пришло в голову, что можно во первых обрабатывать все рекурсивно от основной сцены, и запускать аналогичную обработку в функции Show. В принципе вполне выход...
Да ничего не надо рекурсивно обрабатывать. Элементы поддерживающие стилизацию реализуют интерфейс, допустим IStyle. При добавлении объекта в контейнер, контейнер проверяет на наличие интерфейса и если интерфейс есть сливает элементу менеджер стилей. Объект (при инициализации) запрашивает у менеджера нужные ему стили и подписывается у него на события изменения стиля (по событию вызывается инвалидация и стили снова будут опрошены). getStyle будет запрашивать у менеджера, setStyle действует только на компонет и к менеджеру не обращается.
Ниспадающий вызов перерисовки (сеттера style) гарантирует нам, что Меню не будет перерисовано, пока Лейбл не изменит шрифт.
Инвалидация свойств тоже решит эти проблемы.

Wolsh
31.12.2010, 18:59
Инвалидация свойств тоже решит эти проблемы.А можно чуть разжевать про механизм такой инвалидации? Ниспадающий вызов гарантируется синхронностью, не требует ни строчки кода и по своей сути не способен рожать баги. Не представляю, что можно противопоставить этому)).

alatar
31.12.2010, 19:13
Ниспадающий вызов гарантируется синхронностью
Смысл как раз уйти от синхронности. По-сути получается эдакий микрофлекс. Т.е. свойство применяется не сразу, а (например) в следующем кадре.
В итоге делается перерисовка по ниспадающей, а не применение свойств. На момент перерисовки все свойства уже известны и проверены.

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

etc
31.12.2010, 20:29
Нафига вам в реалтайме перерисовывать интерфейс?

Wolsh
01.01.2011, 01:14
alatar, во-первых инвалидация у меня применена во всей своей красе прямо в том примере - она то как раз синхронности ничем не противоречит - самый базовый элемент всех контролов Pane имеет около 15 сеттеров, и пересчитывать отрисовку при задании каждого свойства никто не собирается, естественно они ждут рендера. Но порядок отрисовки элементов при этом жестко соблюдается;
во-вторых, Вы не ответили на мой вопрос - каким образом инвалидация поможет при асинхронности, т.е. когда некий контрол понятия не имеет, обновились уже его внутренние элементы РАНЬШЕ него, обновятся ли ПОЗЖЕ, будут ли вообще обновляться? Как Вы сделаете это без события "я готов"? Как Вы сделаете это С СОБЫТИЕМ "я готов", если тот кто его получает - сам еще не готов? Как навести порядок во всей этой каше поможет ожидание рендера? Его событие "щас я вас перерисую" гарантирует, что все элементы получили и обработали свои стили? А разве инвалидация здесь не играет на врага, говоря - "Получили - может быть, но уж точно не обработали, ибо ждали рендера!" Ну и в каком порядке теперь рендер будет вызывать перерисовку? В том же, в котором ему добавлялись слушатели? Ну так и что у нас изменилось? Меню все так же может быть перерисовано ДО того, как лейбл пересчитает свою высоту с новым шрифтом, а кнопка перерисует свои панельки. Мы просто отодвинули этот кавардак до последнего момента. Пожалуйста, если я что-то не понимаю, объясните мне, для меня этот вопрос совсем не абстрактный
etc, лично мне сама возможность смены в рантайме была не более чем забавной фичей (используемой зачем-то во многих настольных приложениях). Я стремился сделать возможной настройку стиля скомпилированного флэш-приложения веб-дизайнером, знакомым только с CSS. Поскольку я рисую контролы в рантайме графиксом, возможности стилизации через текстовый ини-файл на CSS или XML колоссальны, и было грех этим не воспользоваться. А возможность смены в рантайме это не цель и не путь, это "цветы вдоль дороги", она появляется сама.

JackFromChaos
01.01.2011, 19:55
Тут выходит даже более глубокий вопрос, чем просто обновление стилей. Он заключается в том, как заставить обновится контейнер, если изменился размер дочерних элементов.
Мне пока в голову ничего не приходит, по крайней мере при классической схеме отложенного invalidate...
Вообще, отложенный invalidate имеет свои минусы, по крайней мере, если он основан на событии enter_frame(MinimalComponent, стандартные flash компоненты), потому что по сути внешний вид компонента настраивается не перед первым кадром, а после – и это заметно.

Добавлено через 12 минут
Ну как вариант механизм рекурсивного brjadcast-а, вызывющего draw снизу верх, аля:

public function broadcastDraw(container:DisplayObjectContainer):void
{
var drawable:iDraweble;
var count:int = container.numChildren;
for (var i:int = 0; i < count; i++)
{
var obj:DisplayObject = container.getChildAt(i);
if (obj is DisplayObjectContainer)
broadcastDraw(obj as DisplayObjectContainer);
else if (obj is iDraweble) //Эту и следующую строки можно убрать, что бы обновлялись только контейнеры...
(obj as iDraweble).draw();

}
if (container is iDraweble)
(container as iDraweble).draw();
}

Но этот подход будет работать нормально относительно некоторого рута, не важно, главная сцена это, или, например окно. Причем draw будет вызыаться для тех объектов, для которых некоторых ситуациях может быть и не очень нужен.


В общем не очень мне нравится этот подход...

P.S. Опять же, draw() придется делать публичным...

alatar
02.01.2011, 11:15
Вы не ответили на мой вопрос...
Флаги и события.
Событие "я готов" бессмыссленно, компонент либо готов на момент отрисовки контейнера (известны его размеры), либо не готов и отрисовывать нечего (или размеры будут заданы контейнером). Из событий используется только "у меня поменялись размеры", все остальное контейнеру безразлично (исключение только контейнер без лайаута, тогда добавится событие "я переместился").

Таким образом контейнер перерисуется только ПОСЛЕ изменения размеров лейбла. Т.е. по событию ребенка "у меня поменялись размеры" будет вызвана инвалидация перерисовки контейнера. Отрисован ли ребенок на момент отрисовки контейнера, опять же, не важно.

Т.е. перерисовка стимулируется снизу-вверх, а не сверху-вниз.

etc
02.01.2011, 15:10
Ресайз слушают от родителя к детям.

alatar
02.01.2011, 15:27
Ресайз слушают от родителя к детям.
Я где-то написал обратное?

etc
02.01.2011, 15:31
Я где-то написал обратное?

Нет :)

Wolsh
02.01.2011, 21:31
Из событий используется только "у меня поменялись размеры", все остальное контейнеру безразлично (исключение только контейнер без лайаута, тогда добавится событие "я переместился").Ага, собственно это я и называл "я готов". Итак, элемент получил событие "пришел вагон тушенки новый стиль", запросил его у менеджера и пересчитал свои размеры, после чего диспатчит сообщение "у меня поменялись размеры". Допустим, это был "конечный" элемент, листик дерева. Простой, не содержащий в себе других элементов. Посланное им событие получил подписанный на него сложный компонент, к примеру Кнопка получила событие от Лейбла. Но вопрос - а что должен делать сложный компонент Кнопка, получив событие "пришел новый стиль"? Вроде как ей надо пересчитаться и отправить событие "у меня поменялись размеры" наверх, экземпляру класса Меню. Но кнопка знает, что ей надо отцентрировать Лейбл, но понятия не имеет, НАДО ЛИ ждать от него событие, будет ли Лейбл перерисовываться/пересчитываться/менять стиль. Выходит конфуз - кнопка пересчиталась, но не известно, правильно ли. Поскольку она то точно изменилась, ей надо сообщить об этом контейнеру. Но ситуация с Лейблом до сих пор не ясна.
Допустим в качестве решения, что ВСЕ элементы ОБЯЗАНЫ в ответ на сообщение "пришел новый стиль" послать сообщение "у меня поменялись размеры" или "не буду я меняться", и все сложносоставные компоненты обязаны дождаться сообщений от всех своих элементов, прежде чем сделать собственный пересчет и послать такое же сообщение наверх. Но я не очень себе представляю, кто гарантирует, что компонент получит своё сообщение "пришел новый стиль" и начнет ждать сообщений раньше, чем его элементы, и ни одного не пропустит. Но даже если все это предусмотреть, если удастся жестко управлять последовательностью рассылки сообщений - когда я смотрю на всю эту.. архитектуру.. правильнее сказать - на все эти нагромождения, на всю эту бюрократию с отчетностью, я не понимаю одного - ЗАЧЕМ? Когда можно получить ту же самую функциональность, не делая абсолютно ничего из этого?
У меня Меню получает Объект стилей от, скажем, Окна. У Меню есть элементы двух типов - Кнопки и Панель. Получив стиль, Меню находит в нем Объекты для Кнопок и для Панели, и отдает в их сеттеры. Выполнение кода в сеттере Меню останавливается, пока не выполнится код в сеттере Кнопки. Кнопка проверяет, нет ли в полученном Объекте стиля для Лейбла, и если есть - вызывает сеттер стиля у Лейбла. Код в сеттере стиля Кнопки останавливается, пока Лейбл не завершит свой. Вот и всё, вся архитектура. Все стили будут отданы сверху вниз, а размеры и положения расчитаны снизу вверх, и только после этого произойдет отрисовка. Зачем нужен этот пинг-понг событиями, когда реальное событие только одно - файл стиля загрузился и распарсен. А дальше - рутинная работа по отображению, а не панические звонки по всем этажам офиса.

udaaff
02.01.2011, 23:56
Тут выходит даже более глубокий вопрос, чем просто обновление стилей. Он заключается в том, как заставить обновится контейнер, если изменился размер дочерних элементов.
Мне пока в голову ничего не приходит, по крайней мере при классической схеме отложенного invalidate...
Вообще, отложенный invalidate имеет свои минусы, по крайней мере, если он основан на событии enter_frame(MinimalComponent, стандартные flash компоненты), потому что по сути внешний вид компонента настраивается не перед первым кадром, а после – и это заметно.

Ничего особо страшного не вижу в запаздывании обновления свойств (за исключением реализации :D, т.к. попытки могут закончиться летальным исходом), отвечающих за отображение объекта, на один кадр. Если объекта нету на сцене, то обновлять, вообще, ничего не нужно до тех пор пока он туда не добавится. Без отложенного подсчета, на мой взгляд, тут никак не обойтись. Если сразу синхронно всё просчитывать, то может получиться очень много лишних расчетов. Если попытаться реализовать тот же функционал флекса по позицонированию/масштабированию элементов (т.е. автосайз контейнеров, определенные схемы позиционирования дочерних элементов на основе left, right и т.п.) в синхронном виде, то получится уйма лишних расчетов. Т.е. нужно будет пересчитывать размеры контейнера (которые могут повлечь за собой пересчеты прочих дисплей объектов, как его детей, так и предков) если: добавляем/убираем дитенка из контейнера, меняется ширина или высота контейнера, меняется позиция/размеры/(left, right и т.п.) дочернего элемента (если контейнеру вручную не заданы размеры, и он автосайзится). Это всё примерно. Тут синхронная реализация была бы конечно гораздо проще. В этом я вижу её плюс :)
Если откладывать пересчет некоторых свойств, то простым сверху вниз или снизу вверх не обойтись. В том же флексе, на сколько я знаю, этот процесс разбит на несколько фаз: пересчет размеров, обновление дисплей листа, перерисовка (могу ошибаться, но что-то типа этого) и это не из-за простого удобства или какой-то религии, видимо, так сделано. Если просто попытаться всё перерисовать и проапдейтить за одну фазу, то получается палка о двух концах: если элемент автосайзится, то расчет размеров должен идти снизу вверх (от потомков к предкам), если меняется размер контейнера, то, наоборот, пересчет должен идти сверху вниз (от предков к потомкам), т.к. позиция и размеры детей контейнера, могут зависеть от его размеров, если контейнер использует какую-то схему для позиционирования детей. Без такой фичи как автосайз, можно было бы расчитывать всё сверху вниз и не париться, опираясь только на явно заданные размеры =)



P.S. Опять же, draw() придется делать публичным...
Можно отдельный неймспейс сделать, project_internal какой-нибудь.

JackFromChaos
03.01.2011, 00:26
Использование события resize как то не радует вообще(меня, по крайне мере), так как это обязывает нас всю иерархию строить исключительно на основе базовых компонентов...

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

udaaff
03.01.2011, 00:34
Использование события resize как то не радует вообще(меня, по крайне мере), так как это обязывает нас всю иерархию строить исключительно на основе базовых компонентов...
Если какой-то объект не удовлетворяет определенному интерфейсу, то можно просто не принимать его в расчет.


На счет отложенных расчетов, а как вообще тут можно контролировать какой либо порядок? А так понимаю, стандартный диспечер сообщений не гарантирует никакого порядка в принципе... Так что при отложенном вызове гарантировать ни сверху вниз, ни снизу вверх не возможно? или я что-то не понимаю?
Использовать какой-нибудь LayoutManager, которому сообщать, о том, что тот или иной компонент нуждается в определенных расчетах. К примеру, в самом компоненте:
layoutManager.invalidateSize(this);
А LayoutManager уже бы в свою очередь следил за последовательностью во время апдейта. + При таком подходе можно было бы обновлять только те элементы, которые попросили этого, а не все которые попадут под раздачу.

alatar
03.01.2011, 00:36
В том же флексе, на сколько я знаю, этот процесс разбит на несколько фаз: пересчет размеров, обновление дисплей листа, перерисовка (могу ошибаться, но что-то типа этого) и это не из-за простого удобства или какой-то религии, видимо, так сделано.
Если точнее, то на 4-ре. createChildren (создание детишек), measure (расчет дефолтных размеров), commitProperties (расчет свойств), updateDisplayList (позиционирование и отрисовка).

Добавлено через 11 часов 21 минуту
Во флексовом LayoutManager используется такая последовательность для компонентов которые сообщили об изменениях, т.е. вызвали invalidateProperties() и/или invalidateSize() и/или invalidateDisplayList:
1. Сверху-вниз вызывается validateProperties (commitProperties) – компонент проверяет свои свойства и корректирует в случае необходимости.
2. Снизу-вверх вызывается validateSize (measure) – компонент рассчитывает минимальные размеры. (Эти размеры еще ничего не значат).
3. Сверху вниз вызывается validateDisplayList (updateDisplayList) – компонент отрисовывается и позиционирует своих детей. На этом этапе компоненту будут предложены либо размеры которые ему задаст его контейнер, либо те, которые он рассчитал.

Добавлено через 11 часов 26 минут
У меня Меню получает Объект стилей от, скажем, Окна. У Меню есть элементы двух типов - Кнопки и Панель. Получив стиль, Меню находит в нем Объекты для Кнопок и для Панели, и отдает в их сеттеры. Выполнение кода в сеттере Меню останавливается, пока не выполнится код в сеттере Кнопки. Кнопка проверяет, нет ли в полученном Объекте стиля для Лейбла, и если есть - вызывает сеттер стиля у Лейбла.
А как вы задаете глобальные стили? Например все леблы должны выглядеть одинаково. Стили будут дублироваться? Получается, что стили должны дублировать структуру вида?