Структура документа Lexi

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

Авторы часто рассматривают документы и в терминах их логической структуры: предложений, абзацев, разделов, подразделов и глав. Чтобы не слишком усложнять пример, мы не будем явно хранить во внутреннем представлении информацию о логической структуре. Но то проектное решение, которое мы опишем, вполне пригодно для представления и такой информации.

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

В частности, внутреннее представление должно поддерживать:
  • отслеживание физической структуры документа, то есть разбиение текста и графики на строки, колонки, таблицы и т.д.;
  • генерирование визуального представления документа;
  • отображение позиций экрана на элементы внутреннего представления. Это позволит определить, что имел в виду пользователь, когда указал на что-то в визуальном представлении.

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

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

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

Рекурсивная композиция

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

Рекурсивная композиция текста и графики

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

Структура объекта для рекурсивной композиции текста и графики

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

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

Глифы

Абстрактный класс Glyph (глиф) определяется для всех объектов, которые могут присутствовать в структуре документа.

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

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

Частичная иерархия класса Glyph

А в таблице 2.1 более подробно представлен базовый интерфейс этого класса в нотации C++.

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

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

Подклассы класса Glyph переопределяют операцию Draw, выполняющую перерисовку себя в окне. При вызове Draw ей передается ссылка на объект Window. В классе Window определены графические операции для прорисовки в окне на текста и основных геометрических фигур. Например, в подклассе Rectangle операция Draw могла определяться так:

void Rectangle::Draw(Window* w) {
 w->DrawRect(_x0,

где _x0, _y0, _x1 и _y1 - это данные-члены класса Rectangle, определяющие два противоположных угла прямоугольника, a DrawRect - операция из класса Window, рисующая на экране прямоугольник.

Глифу-родителю часто бывает нужно «знать», сколько места на экране занимает глиф-потомок, чтобы расположить его и остальные глифы в строке без перекрытий (как показано на первом рисунке). Операция Bounds возвращает прямоугольную область, занимаемую глифом, точнее, противоположные углы наименьшего прямоугольника, содержащего глиф. В подклассах класса Glyph эта операция переопределена в соответствии с природой конкретного элемента.

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

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

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

Операция Child возвращает потомка с заданным индексом (если таковой существует). Глифы типа Row, у которых действительно есть потомки, должны пользоваться операцией Child, а не обращаться к структуре данных потомка напрямую. В таком случае при изменении структуры данных, скажем, с массива на связанный список не придется модифицировать операции вроде Draw, которые обходят всех потомков. Аналогично операция Parent предоставляет стандартный интерфейс для доступа к родителю глифа, если таковой имеется. В Lexi глифы хранят ссылку на своего родителя, a Parent просто возвращает эту ссылку.

Паттерн компоновщик

Рекурсивная композиция пригодна не только для документов. Мы можем воспользоваться ей для представления любых потенциально сложных иерархических структур. Паттерн компоновщик инкапсулирует сущность рекурсивной композиции объектно-ориентированным способом. Сейчас самое время обратиться к разделу об этом паттерне и изучить его, имея в виду, только что рассмотренный сценарий.