Блог → О построении пользовательского интерфейса. Часть 2

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

Граф перехода

Если строить интерфейс способом, традиционным для построения диалоговых систем, то возникают понятия состояния (как точки диалога) и графа перехода из одного состояния в другое. Разными состояниями при этом, по определению, считаются те, в которых хотя бы для одного входного события переходы различны.

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

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

Дерево состояний

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

Этот метод, типичный для идеологии Windows, при которой в основном реализуется контекстно-свободное описание интерфейса, в то же время использует положительные стороны объектно-ориентированного описания - возможность иерархической классификации объектов, поскольку реальное дерево состояний отображает их классификацию.

Дополнительный контекст

Общая схема построена, как потактовый интерпретатор. При его функционировании имеется стек состояний, где доступно только последнее, и очередь событий, из которых доступно только первое. На каждом такте по текущему состоянию и событию исполняется функция, записанная в соответствующем элементе матрицы, и происходит изменение стека состояния и/или вычеркивается из очереди первое событие.

Перед циклом событие "транслируется", т.е. определяется его рабочий код, например, при операциях с мышью формируется событие мыши либо в текущем окне, либо в каком-то другом. Иногда трансляция сводится к уточнению события или замене его кода, как, скажем, при замене "щелчка" мыши кодом соответствующей клавиши. Далее последовательно проверяется, есть ли текущее событие в каждом из перечисленных ниже списков:
- в списке суперглобальных ключей (обычно это вызов контекстной помощи), и если есть, то исполняется соответствующая функция и событие вычеркивается из очереди; цикл схемы считается выполненным;
- в списке внутренних событий активного состояния, и если есть, то выполняется соответствующая функция и событие вычеркивается из очереди; цикл схемы считается выполненным;
- в списке внешних событий активного состояния, и если да, то выполняется соответствующая функция, по ее коду возврата преобразовывается стек состояний и событие вычеркивается из очереди; цикл схемы считается выполненным;
- в списке глобальных ключей, и если есть, то активное состояние вычеркивается из стека, а текущее событие не вычеркивается из очереди; цикл схемы считается выполненным. Таким образом, на следующем цикле то же событие будет проходить проверку по спискам внутренних и внешних событий нового активного состояния.

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

Упрощение контекста

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

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

Взаимодействие состояний

Одним из самых больших недостатков программ, организованных в контексте интерфейса, является то, что программный контекст фактически реализуется набором глобальных переменных со всеми вытекающими отсюда последствиями. Хотя в приведенной схеме эту проблему не удалось решить до конца, использование механизма внутренних событий для передачи информации между состояниями ее существенно упростило.

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

Наоборот, в функции окончательного выхода из состояния оно может оставить "завещание" в очереди в виде события со специальным кодом. Новое текущее состояние по этой информации может распознать, чем закончилась работа предыдущего. В случае вызова дочернего состояния такое событие передает результат, подобно возвращаемому функцией значению.

На этом механизме, в частности, удобно проектируются и реализуются "библиотечные" состояния, например ввод текстовой строки или имени файла. Параметром обращения к таким состояниям служит событие, содержащее начальное значение вводимой строки или имени (принятое по умолчанию), а результатом - событие, содержащее полученное от пользователя значение. Если пользователь нажал клавишу ESC, то результирующее событие в очередь не ставится и значение строки в вызывающем состоянии не меняется. При этом, разумеется, состояние ввода имени файла обеспечивает навигацию по файловой системе и все необходимые средства редактирования.

Следует обратить внимание на то, что все глобальные ключи программы остаются в силе во время работы "библиотечного" состояния. В других известных системах дело обстоит иначе. В частности, в Windows либо выход в Dialog-box блокирует все ключи программы, либо программисту самому надо писать отработчик сообщений в этом Dialog-box'е, и тогда он не может быть стандартным.

Как программировать?

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

Заключение

Предложенная схема может служить основой структурной организации больших программ любого назначения. Механизм слежения за состояниями способен взять на себя функции того, что в языковых средах называется "поддержкой времени исполнения" (Run-time support). При этом возникает возможность нового уровня работы с ресурсами - памятью, файлами, неразделяемыми внешними устройствами. Все запросы на такие ресурсы могут проходить через ту же среду, т.е. реализовываться специальными для этого способами организации программ средствами. В итоге все ресурсы приписываются какому-либо состоянию и при уничтожении состояния либо передаются другим состояниям согласно указаниям программиста (при их запросе или в программе выхода из состояния), либо освобождаются. Таким образом, состояния становятся хранилищами ресурсов и, вообще говоря, любых других объектов, а интерпретатор состояний - способом макроструктурирования программы.