Docu/ru/Tut-6.odc

Часть III: Конструирование отображений
ENGLISH

В части I учебника по BlackBox дается введение в схемы проектирования, применяемые в BlackBox. Знание этих схем облегчает понимание и запоминание частных решений в различных модулях BlackBox.
Часть II учебника по BlackBox демонстрирует применение наиболее важных библиотек компонентов: элементы управления, формы, текстовые компоненты.
Часть III учебника по BlackBox демонстрирует разработку новых отображений на серии усложняющихся примеров от самых простых к более сложным.


6 Конструирование отображений

Отображения являются центральной абстракцией BlackBox Component Builder. Всё вращается вокруг них: большинство команд выполняются над отображениями, окна показывают отображения, отображения могут быть загружены и выгружены, с помощью отображений проводится взаимодействие с пользователем и отображения могут быть внедрены в другие отображения.

6.1 Введение

Учебник программирования отображений состоит из четырех разделов, каждый из которых представляет особый аспект программирования отображений. В разделе "обработка сообщений" ("message handling")  (главы с 6.2 по 6.5) разъясняется поведение отображений, определенное для обработки сообщений отображений, то есть какие могут быть ответы, сообщения элементов управления и свойства потока взаимодействия отображений с окружающей средой и как отображения реагируют на пользовательский ввод. Во втором разделе (6.7) объясняются аспекты разделения моделей и отображений, которые разрешают многооконное редактирование отображений. В следующем разделе (6.7) показывается, как могут быть использованы объекты операций для возможностей отката/повтора BlackBox Component Framework при изменении содержимого отображения. В последнем разделе (6.8) объясняется особая структура всех расширяемых модулей BlackBox Component Framework, а именно разделение интерфейса и реализации отображений. Фабричный объект (directory object) является ключевой схемой проектирования.

6.2 Обработка сообщений

В этом разделе представлена последовательность реализаций отображения, со все более возрастающими возможностями, которое показывает прямоугольную закрашенную область. В частности, отображение обрабатывает Preferences, Controller messages и Properties.
Каждое отображение является подтипом абстрактного типа Views.View. Конкретное расширение должно как минимум реализовывать абстрактную процедуру Views.Restore, которая отрисовывает содержимое отображения. Первый пример нашего отображения просто рисует красные прямоугольник. Эта версия представляет собой простейшую из возможных реализацию отображения. Главными компонентами являются определение расширения от Views.View, реализация процедуры Views.Restore, которая отрисовывает содержимое отображения и управляющая процедура для размещения и инициализации отображения:
MODULE ObxViews0;

	IMPORT Views, Ports;

	TYPE View = POINTER TO RECORD (Views.View) END;

	PROCEDURE (v: View)  Restore (f: Views.Frame; l, t, r, b: INTEGER);
	BEGIN
		f.DrawRect(l, t, r, b, Ports.fill, Ports.red)
	END Restore;

	PROCEDURE Deposit*;
		VAR v: View;
	BEGIN
		NEW(v); Views.Deposit(v)
	END Deposit;

END ObxViews0.
Для выполнения этой программы запустите следующую команду:

 "ObxViews0.Deposit; StdCmds.Open"

Результат этой команды представлен на рисунке 6-1.



Рисунок 6-1.  Результат "ObxViews0.Deposit; StdCmds.Open"

Эта простая программа уже полностью функциональна. Процедура ObxViews0.Deposit заносит вновь размещенное отображение в системную очередь. Команда StdCmds.Open, приведенная выше в командной строке, извлекает отображение из очереди и открывает его в новом окне. Следует отметить, что последовательность команд должна быть заключена в кавычки, в то время как для одиночной команды кавычки можно опустить.
Вместо того, чтобы открыть отображение в окне, его можно поместить в контейнер, обладающий фокусом ввода (то есть в текст или форму):

 "ObxViews0.Deposit; StdCmds.PasteView" ><перед выполнением команды поместите курсор сюда

Как и StdCmds.Open, команда StdCmds.PasteView извлекает отображение из очереди, но вставляет его в отображение, обладающее фокусом ввода. Вышеприведенная последовательность команд может быть использована, например, в меню.
Каждый документ, содержащий отображение ObxViews0 может быть сохранен в файл, который затем может быть открыт вновь с помощью стандартной команды меню Open... Простой красный прямоугольник должен быть отображен правильно во вновь открытом документе, что обеспечивается доступностью файла кода модуля ObxViews0.
Отображение, которое было открыто в собственном окне, так же могло быть сохранено в файле. При повторном открытии документ состоит только из этого единственного отображения.
Каждое отображение осуществляет операции вывода и обработки мыши/клавиатуры через объект Views.Frame. В приведенном выше примере ObxViews0.View.Restore использует собственную рамку как параметр f для отрисовки строки. Рамка может считаться объектом отображателя, в данном случае как ввода, так и вывода одновременно (здесь будет интересно уточнить о курьерах и носителях для этого отображателя, но оба они определены в модуле Ports). Рамка реализует в себе преобразование координат и возможности обрезки.
В случае, если отображение отрисовывается на экране, рамка является отображателем для порта экрана. Если отображение распечатывается, рамка является отображателем для порта принтера. С точки зрения отображения эта разница не существенна. Так, не требуется специального кода в случае необходимости поддержки печати.
Отображения могут быть скопированы и вставлены в контейнеры, такие как текстовые отображения или отображения в виде форм. Дополнительно может быть изменен размер отображения, если его выбрать и изменить размер. Вся эта функциональность поддерживается каркасом (BlackBox Component Framework) и не требует дополнительного кодирования. Если поведение по умолчанию неудобно, его можно изменить. В дальнейшем будет показано, как это сделать. 


6.3 Сообщения свойств по умолчанию 

Следует отметить, что размер вновь открытого отображения может быть различным. Тем не менее, перед открытием отображения оболочка посылает отображению сообщение с предполагаемыми размерами. Отображение может приспособить предлагаемые размеры для собственных нужд. Для этого сообщение типа Properties.SizePrep должно быть обработано в процедуре отображения HandlePropMsg. Перед первой отрисовкой отображения предполагаемые размер для ширины и высоты отображения задан в Views.undefined. В следующей версии примера отображения рисунок отрисовывается с шириной 2 и высотой 1 см. Внесенные в сравнении с предыдущей версией изменения отмечены жирным шрифтом.
MODULE ObxViews1;

	IMPORT Views, Ports, Properties;

	TYPE View = POINTER TO RECORD (Views.View) END;

	PROCEDURE (v: View)  Restore (f: Views.Frame; l, t, r, b: INTEGER);
	BEGIN
		f.DrawRect(l, t, r, b, Ports.fill, Ports.red)
	END Restore;

	PROCEDURE (v: View) HandlePropMsg (VAR msg: Properties.Message);
	BEGIN
		WITH msg: Properties.SizePref DO
			IF (msg.w = Views.undefined) OR (msg.h = Views.undefined) THEN
				 msg.w := 20 * Ports.mm; msg.h := 10 * Ports.mm
			END
		ELSE	(* игнорировать другие сообщения *)
		END
	END HandlePropMsg;

	PROCEDURE Deposit*;
		VAR v: View;
	BEGIN
		NEW(v); Views.Deposit(v)
	END Deposit;

END ObxViews1.

 "ObxViews1.Deposit; StdCmds.Open"

Теперь каждое вновь созданное отображение принимает желаемые предопределенные по умолчанию размеры. Это возможно, потому что отображение и контейнер взаимодействуют, в данном случае, посредством сообщения Properties.SizePref. Предполагается, что контейнер посылает это сообщение каждый раз, когда он хочет изменить размеры отображения, а затем принимает возвращенные данные. Тем не менее, для понимания каркаса BlackBox необходимо знать, что контейнер с правильным поведением (well-begaved) должен следовать этому протоколу, но данное следование не обязательно. Именно поэтому подобное сообщение называется "предпочтением". Оно описывает лишь пожелания отображения, но охватывающий контейнер может навязать следование специальным правилам для отображения. Контейнер всегда имеет право последнего слова в подобных переговорах. Например, контейнер не может позволить отображению быть больше собственных размеров. Стандартные контейнеры: документ, текст и формы - являются контейнерами с правильным поведением и поддерживают все предпочтения, определенные в модуле Properties. Специализированные контейнеры, такие как тексты, могут определить дополнительные предпочтения, специфические для типа содержимого. Как важный момент следует отметить, что отображения могут игнорировать все предпочтения, которые они не знают или о которых не беспокоятся.
Properties.SizePref позволяет ограничить возможные размеры по ширине и высоте отображения. Например, можно задать принудительно минимальный и максимальный размер или зафиксировать высоту и ширину отображения или определить ограничения на их ширину и высоту. Процедуры Properties.ProportionalConstraint и Properties.GridConstraint являются полезным стандартом реализации ограничений. Следующая версия реализации отображения определяет, что прямоугольник всегда имеет ширину в два раза больше его высоты. Дополнительно определяются минимальные и максимальные размеры для высоты (тем самым, и для ширины). Если размер отображения изменяется с помощью обработчика изменения размеров, ограничения не нарушаются. Это можно проверить.

MODULE ObxViews2;

	IMPORT Views, Ports, Properties;

	TYPE View = POINTER TO RECORD (Views.View) END;

	PROCEDURE (v: View)  Restore (f: Views.Frame; l, t, r, b: INTEGER);
	BEGIN
		f.DrawRect(l, t, r, b, Ports.fill, Ports.red)
	END Restore;

	PROCEDURE (v: View) HandlePropMsg (VAR msg: Properties.Message);
		CONST min = 5 * Ports.mm; max = 50 * Ports.mm;
	BEGIN
		WITH msg: Properties.SizePref DO
			IF (msg.w = Views.undefined) OR (msg.h = Views.undefined) THEN
				 msg.w := 20 * Ports.mm; msg.h := 10 * Ports.mm
			ELSE
				Properties.ProportionalConstraint(2, 1, msg.fixedW, msg.fixedH,
										msg.w, msg.h);
				IF msg.h < min THEN
					msg.h := min; msg.w := 2 * min
				ELSIF msg.h > max THEN
					msg.h := max; msg.w := 2 * max
				END
			END
		ELSE	(* игнорировать другие сообщения *)
		END
	END HandlePropMsg;

	PROCEDURE Deposit*;
		VAR v: View;
	BEGIN
		NEW(v); Views.Deposit(v)
	END Deposit;

END ObxViews2.
	
 "ObxViews2.Deposit; StdCmds.Open"
 В этом учебнике рассматривается еще два других предпочтения. Первое - Properties.ResizePref.

TYPE
		ResizePref = RECORD (Properties.Preference)
			fixed,
			horFitToPage, verFitToPage,
			horFitToWin, verFitToWin: BOOLEAN   (* OUT *)
		END;
		
Получатель этого сообщения может показать, что он не желает изменять размеры, выставив fixed в TRUE. Как следствие отображение не показывает метки для изменения размера при выделении, поэтому не может изменять размеры интерактивно. Тем не менее, сообщение Properties.SizePref все еще посылается отображению и начальный размер все еще необходимо определить. 
Если отображение является корневым, то есть самое дальнее от центра отображение в документе или окне (то есть открытое с помощью StdCmds.Open), размер окна, размер отображения или оба сразу могут быть изменены.
Тем не менее, иногда бывает удобно автоматически приводить размер отображения к изменению размера окна, то есть помочь текстам занимать как можно больше места на экране. Для других отображений было бы предпочтительней, если их размер определялся бы не окном, а содержимым или настройками страницы внедренного документа. Отображение может использовать эти возможности выставив значения horFitToWin, verFitToWin, horFitToPage и verFitToPage для сообщения Properties.SizePref. Эти значения не имеют силы, если отображение не является корневым, то есть является внедренным в документ.
Автоматическое приведение размеров отображения по текущему размеру окна может быть достигнуто путем выставления полей horFitToWin и verFitToWin. Размер отображения ограничен окном, то есть пользователь может изменить размер напрямую, изменяя размеры окна.
Следует отметить, что если размеры отображения ограничены размерами окна (а так же путем выставления hotFitToWin или verFitToWin), то сообщение Properties.SizePref для отображения не посылается, то есть ограничения, заданные в Properties.SizePref, больше не применяются. Дополнительно для отображения не показываются метки изменения размеров, вне зависимости от состяния fixed.
При установке полей horFitToPage или verFitToPage ширина или высота отображения может быть ограничена фактическим размером бумаги в принтере. Ширина текстового отображения обычно ограничена шириной листа бумаги и может быть изменена путем изменения установок размеров бумаги, которое поддерживается средствами операционной системы. Следующий пример принудительно выставляет размер корневого отображения по размерам окна:

MODULE ObxViews3;

	IMPORT Views, Ports, Properties;

	TYPE View = POINTER TO RECORD (Views.View) END;

	PROCEDURE (v: View)  Restore (f: Views.Frame; l, t, r, b: INTEGER);
	BEGIN
		f.DrawRect(l, t, r, b, Ports.fill, Ports.red)
	END Restore;

	PROCEDURE (v: View) HandlePropMsg (VAR msg: Properties.Message);
		CONST min = 5 * Ports.mm; max = 50 * Ports.mm;
	BEGIN
		WITH msg: Properties.SizePref DO
			IF (msg.w = Views.undefined) OR (msg.h = Views.undefined) THEN
				 msg.w := 20 * Ports.mm; msg.h := 10 * Ports.mm
			ELSE
				Properties.ProportionalConstraint(2, 1, msg.fixedW, msg.fixedH,
										msg.w, msg.h);
				IF msg.h < min THEN
					msg.h := min; msg.w := 2 * min
				ELSIF msg.h > max THEN
					msg.h := max; msg.w := 2 * max
				END
			END
		| msg: Properties.ResizePref DO
			msg.horFitToWin := TRUE; msg.verFitToWin := TRUE
		ELSE	
		END
	END HandlePropMsg;

	PROCEDURE Deposit*;
		VAR v: View;
	BEGIN
		NEW(v); Views.Deposit(v)
	END Deposit;

END ObxViews3.	 

 "ObxViews3.Deposit; StdCmds.Open"


Теперь рассмотрим Properties.FocusPref.

	  TYPE
		FocusPref = RECORD (Properties.Preference)
			atLocation: BOOLEAN;   (* IN *)
			x, y: INTEGER;   (* IN *)
			hotFocus, setFocus, selectOnFocus: BOOLEAN   (* OUT *)
		END;

При попытке активации внедренного отображения по щелчку на нем, отображению посылается сообщение Properties.FocusPref. Если на сообщение не было ответа, отображение выделяется целиком (так же называется singleton). Это поведение для отображения, реализованное в вышеприведенном примере. В подтверждение, разместите курсор на следующей строке и щелкните на коммандере, затем на вставленном отображении:

 "ObxViews3.Deposit; StdCmds.PasteView"	>< перед выполнением команды установите курсор здесь

Такое поведение применено для пассивоного отображения. Тем не менее, если содержимое отображение можно редактировать или имеются команды меню, выполняющие действия над отображением, пользователь должен иметь возможность получить фокус ввода на отображение. Фокус ввода - это место, куда осуществляется ввод данных с клавиатуры, где показывается текущее выделение и положение курсора (или какая-то другая подобная метка) и где выполняются некоторые команды меню. Фактически меню доступны, только в случае обладания отображением фокуса ввода, и меню становятся недоступными, когда отображение теряет фокус ввода (ниже об этом будет сказано подробнее). Корневое отображение всегда имеет фокус ввода, если фокус ввода имеет окно документа (то есть верхнее окно).
В случае, если внедренное отображение может получать фокус ввода, то оно должно отвечать на сообщение Properties.FocusPref. Отображение может определить, захватывается ли фокус ввода или фокус переводится только на время, в течение которого нажата кнопка мыши. Последняя ситуация называется "горячий фокус" (hot focus). Типичный пример горячего фокуса - кнопка. Для того, чтобы отображение приобрело свойство горячего фокуса, необходимо выставить флаг hotFocus. В этом случае фокус ввода переводится дальше сразу после отпускания кнопки мыши. Когда требуется захватить фокус ввода, необходимо выставить флаг setFocus. setFocus следует выставлять для всех редакторов, чтобы контекстнозависимые команды меню могли применяться к отображению. В дополнение к флагу setFocus для отображения может быть выставлен флаг selectOnFocus для того, чтобы при наведении с помощью клавиатуры содержимое отображения выделялось. Текстовые поля ввода являются первым примером подобного поведения: содержимое элемента, только что получившего фокус - выделено, если пользователь попал не него не с помощью мыши, а с применением клавиатуры.
Если пользователь щелкнет на отображении без фокуса ввода, то каркас выставляет флаг alLocation перед посылкой сообщения. Отображение-получатель может решить, каким образом сфокусироваться в зависимости от того, где щелкнул пользователь. Позиция мыши передается отображению в полях x и y предпочтения фокуса (focus preference). Текстовые линейки - примеры отображения которые фокусируются в зависимости от положения мыши. Если кликаются иконки в области ниже шкалы линейки, то линейка не получает фокус. В противном случае - получает. Это можно попробовать прямо сейчас на линейке. Показать/скрыть линейки можно с помощью команды меню Text->ShowMarks.
Отображение получают фокус не только после щелчка мыши. Например отображения могут быть выбраны с помощью клавиш управления курсора. В случае, если отображение фокусируется без участия мыши, каркас выставляет поле atLocation в FALSE.
В следующей версии примера будет обработано сообщение FocusPref и выставлен флаг setFocus. Это достигается добавлением в процедуру HandlePropMsg предыдущего примера следующего кода:

		| msg: Properties.FocusPref DO
			msg.setFocus := TRUE

Если внедренное отображение получает фокус, отрисовывается рамка отображения с соответствующими отметками, активируются специфические для отображения пункты меню, а остальные - дезактивируются.


6.4 Управляющие сообщения 

Другие сообщения, которые могут быть обработаны процедурой HandlerPropMsg будут рассмотрены позже. Дальше будет рассмотрено, как отображения реагируют на пользовательский ввод, то есть на щелчки мыши в отображении или команды, вызываемые из меню. Для простых отображений такое поведение реализуется процедурой Views.HandleCtrlMsg. Так же возможно определить отдельный объект, называемый контроллером, которые реализует поведение отображения при взаимодействии с пользователем. Программирование объектов контроллера здесь не рассматривается, так как применение контроллеров - только рекомендация для большинства сложных отображений.
Обработчки Views.HandleCtrlMsg отвечает на управляющие сообщения (controller messages), посылаемые к отображению. Каждое отображение в цепочки обработки фокуса самостоятельно решает завершить ли обработку сообщения, то есть обладает ли оно фокусом, или передать сообщение к другому внедренному отображению. Важно отметить, что все сообщения контроллеров, которые не подходят к типу отображения, игнорируются.
Для того, чтобы было возможно выполнять редактирование в отображении, обладающем фокусом ввода, каркас должен каким-то образом разрешить эти операции. Щелчки мыши и нажатия клавиш могут быть всегда. Тем не менее, внешний вид меню зависит от решения, принятого непосредственно отображением. Для этой цели отображению посылается сообщение Controllers.PollOpsMsg. В зависимости от текущего статуса (например, текущего выделения) и содержимого буфера обмена, отображение, обладающее фокусом ввода, может информировать каркас о том, какие операции редактирования сейчас возможны. Доступные операции содержатся в множестве элементов: {Controllers.cut, Controllers.copy, Controllers.paste, Controllers.pasteView}.

		TYPE
		PollOpsMsg = RECORD (Controllers.Message)
			type: Stores.TypeName;   (* OUT *)
			pasteType: Stores.TypeName;   (* IN *)
			singleton: Views.View;   (* OUT *)
			selectable: BOOLEAN;   (* OUT *)
			valid: SET   (* OUT *)
		END;

Множество доступных операций редактирования возвращается в поле valid, где valid среди {Controllers.cut, Controllers.copy, Controllers.paste}. В зависимости от набора доступных операций становятся доступными или недоступными соответствующие пункты меню Edit, то есть Cut, Copy, Paste и PasteObject...(Windows)/Paste as Part (MacOS). Поле pasteType содержит конкретный тип отображения, копия которого будет вставлена, если произойдет операция вставки. В зависимости от значения поля отображения может решить разрешить или запретить операцию вставки. PollOpsMsg посылается, когда пользователь щелкнул на меню. Единственное его назначение - разрешить или запретить элементы меню, то есть осуществление обратной связи с пользователем.
Если отображение поддерживает выделение содержимого, то selectable должно быть установлено в TRUE. Как следствие станет доступной команда меню Select All. Этот флаг следует установить вне зависимоси от того есть или нет текущее выделение. 
В поле type передается имя типа. Это имя является контекстом для отображения, обладающего фокусом ввода. Этот контекст применяется для определения соответствующего меню для отображения, обладающего фокусом ввода. По соглашению отображение задает в поле type свой базовый тип интерфейса, то есть "ObxViews4.View". Меню с отметкой активности для "ObxViews4.View" будет показыватся для отображения, обладающего фокусом. Имя типа применяется для облегчения поддержки уникальности и избежания конфликта имен. Оболочка никак не интерпретирует имена.
Поле singleton применяется для подчеркивания роли отображения-контейнера. Оно обозначает контейнер текущего выделенного отображения, если выделение состоит целиком из внедренного отображения.
Следующий пример отображения может получать фокус ввода, поддерживает операцию вставки и может информировать оболочку о возможности выделения содержимого. Вследствие этого пункты меню Paste и Select All из меню Edit доступны. Дополнительно определяется контекст "Obx.Tutorial". Поэтому следующее меню появляется всегда, когда отображение выделено; обеспечивается, если меню установлено.
Следует отметить, что имя контекста, в нашем случае "Obx.Tutorial", для удобства начинается с имени подсистемы или модуля с последующей точкой, в нашем случае "Obx.". Такое именование настоятельно рекомендуется в целях обеспечения глобальной уникальности имен. Часто это имя - наименование типа отображения, например: "TextViews.View".
Меню так же может быть объявлено в тексте глобального меню System/Rsrc/Menus или, более характерно, в собственном файле меню подсистемы, в данном случае Obx/Rsrc/Menus. Для того, чтобы меню Obx было видно в главном меню, в его текст необходимо добавить выражение INCLUDE "Obx".

		MENU "New" ("Obx.Tutorial")
			"Beep"	""	"Dialog.Beep"	""
		END
		
MODULE ObxViews4;

	IMPORT Views, Ports, Properties, Controllers;

	TYPE View = POINTER TO RECORD (Views.View) END;

	PROCEDURE (v: View)  Restore (f: Views.Frame; l, t, r, b: INTEGER);
	BEGIN
		f.DrawRect(l, t, r, b, Ports.fill, Ports.red)
	END Restore;

	PROCEDURE (v: View) HandlePropMsg (VAR msg: Properties.Message);
		CONST min = 5 * Ports.mm; max = 50 * Ports.mm;
	BEGIN
		WITH msg: Properties.SizePref DO
			IF (msg.w = Views.undefined) OR (msg.h = Views.undefined) THEN
				 msg.w := 20 * Ports.mm; msg.h := 10 * Ports.mm
			ELSE
				Properties.ProportionalConstraint(2, 1, msg.fixedW, msg.fixedH,
										msg.w, msg.h);
				IF msg.h < min THEN
					msg.h := min; msg.w := 2 * min
				ELSIF msg.h > max THEN
					msg.h := max; msg.w := 2 * max
				END
			END
		| msg: Properties.ResizePref DO
			msg.horFitToWin := TRUE; msg.verFitToWin := TRUE
		| msg: Properties.FocusPref DO
			msg.setFocus := TRUE
		ELSE	(* ignore other messages *)
		END
	END HandlePropMsg;

	PROCEDURE (v: View) HandleCtrlMsg (f: Views.Frame;
									VAR msg: Controllers.Message; VAR focus: Views.View);
	BEGIN
		WITH msg: Controllers.PollOpsMsg DO
			msg.valid := {Controllers.paste}; msg.selectable := TRUE;
			msg.type := "Obx.Tutorial"
		ELSE
		END
	END HandleCtrlMsg;
	
	stdTables

	PROCEDURE Deposit*;
		VAR v: View;
	BEGIN
		NEW(v); Views.Deposit(v)
	END Deposit;

END ObxViews4.
 "ObxViews4.Deposit; StdCmds.PasteView"	>< перед выполнением коменды установите курсор здесь
Далее будет рассмотрено, каким образом отображение может отреагировать на сообщение о редактировании. В частности имеются Controllers.EditMsg для операций редактирования, таких как вырезать, скопировать, вставить и Controllers.TrackMsg для щелчка мыши.
При любом нажатии клавиши в отображении или при вырезании, копировании или вставке Controllers.EditMsg посылается в отображение, обладающее фокусом ввода. Операции вырезания, копирования или вставки могут так же генерироваться средой окружения (через меню), если они были объявлены в Contollers.PollOpsMsg.

TYPE
		EditMsg = RECORD (Controllers.RequestMessage)
			op: INTEGER;   (* IN *)
			modifiers: SET;   (* IN *)
			char: CHAR;   (* IN *)
			view: Views.View;   (* IN для вставки, OUT для вырезания и копирования *)
			w, h: INTEGER;   (* IN для вставки, OUT для вырезания и копирования *)
			isSingle: BOOLEAN;   (* IN для вставки, OUT для вырезания и копирования *)
			clipboard: BOOLEAN   (* IN *)
		END;

Поле op определяет какого типа операция будет произведена. Если op = Controllers.cut, то для отображения, обладающего фокусом ввода, будет сгенерировано копирование. Содержимым нового отображения будет копия выделения в отображении, обладающем фокусом ввода. Новое отображение присваивается полю view. В дополнение, выделение удаляется из отображения, обладающего фокусом ввода. 
Есть один специальный случай: если выделение состоит только из одиночного отображения (синглетон - a singleton), то в поле view копируется сам синглетон, но не копия контейнера синглетона. В этом случае isSingle выставляется в TRUE.
За исключением операции удаления выделенного фрагмента, при значении op = Controllers.copy выполняются те же действия.
Если нажата клавиша на клавиатуре, поле op получает значение Controllers.pasteChar. Символ, который будет вставлен, хранится в поле char. Множество modifiers содержит нажатия вспомогательных клавиш. В последнем случае поле char должно интерпретироваться как управляющий символ.
Операция вставки (paste) является обратной к операции копирования (copy): копия содержимого отображения, находящаяся в поле view перемещается в состав отображения, обладающего фокусом ввода. Эта операция обозначается op = Controllers.paste. Отображение-приемник должно знать тип отображения, чье содержимое будет вставлено и должно решить каким образом содержимое вставляемого отображения будет размещено внутри себя. Например, текстовое отображение должно поддерживать копирование только из текстовых отображений. Если поле isSingle имеет значение TRUE или если содержимое отображения не может быть вставлено, потому что оно принадлежит неизвестному типу, то должна быть вставлена копия самого отображения (но не содержимого). Конечно, это возможно только для универсального контейнера, который позволяет внедрять отображения.
Когда вставляется отображение целиком, то предопределенные ширина и высота передаются в полях w и h. Эти значения могут использоваться как ориентир для отображения-приемника. Если они не подходят, могут использоваться другие. 
При любом нажатии кнопок мыши в отображении, обладающем фокусом ввода, отображению посылается сообщение Controllers.TrackMsg. Координаты находятся в полях x и y (базового типа Controllers.CursorMessage). Множество modifiers фиксирует нажатие модифицирующих клавиш и факт двойного щелчка. modifiers in {Controllers.doubleClick, Controllers.extend, Controllers.Modify}. Если отображению необходимо отразить обратную связь при перемещении мыши, необходимо регистрировать позицию мыши в цикле, вызывая процедуру Input рамки (frame), в котором перемещается мышь. Процедура Input так же возвращает информацию о том, где была отпущена мышь. Любая обратная связь должна быть напрямую отражена в окне, в котором была нажата кнопка мыши. 
После цикла обратной связи возможные изменения содержимого отображения должны быть распространены на все окна, показывающие это отображение. Необходимо помнить, что один документ, а, следовательно, все его внедренные отображения, могут показываться в нескольких окнах. Если внедренное отображение видно в трех окнах (windows), значит существует три рамки (frames), показывающие одно отображение. Процедура Views.Update приводит к обновлению отображения во всех видимых рамках (frame). 
В качестве примера расширим возможности тестового отображения маркером в виде креста с возможностью его перемещения. Следует отметить, что если отображение содержит экземпляры переменных (Instance variables), необходимо реализовать процедуры CopyFromSimpleView, Internalize и Externalize для корректной работы. В следующих разделах учебника будет рассказано назначение этих сообщений. В представленном примере отображения маркер может передвигаться мышью или клавишами перемещения курсора. При нажатии на такую клавишу отображение, имеющее фокус ввода, информируется об этом с помощью сообщения EditMsg с параметром op = pasteChar.
Периодически отображению, обладающему фокусом ввода, посылается сообщение Controllers.PollCursorMsg. Отображение может выставить форму курсора в зависимости от координат мыши. Полю cursor могут быть присвоены значения из набора: {Port.arrowCursor, Ports.textCursor, Ports.graphicsCursor, Ports.tableCursor, Ports.bitmapCursor}. Конкретная форма курсора зависит от особенностей операционной системы. В приведенном примере выставляется графический курсор.


MODULE ObxViews5;

	IMPORT Views, Ports, Properties, Controllers;

	TYPE
		View = POINTER TO RECORD (Views.View)
			x, y: INTEGER
		END;

	PROCEDURE (v: View)  Restore (f: Views.Frame; l, t, r, b: INTEGER);
	BEGIN
		f.DrawRect(l, t, r, b, Ports.fill, Ports.red);
		f.DrawLine(v.x, t, v.x, b, 0, Ports.white); f.DrawLine(l, v.y, r, v.y,
														0, Ports.white)
	END Restore;

	PROCEDURE (v: View) HandlePropMsg (VAR msg: Properties.Message);
		CONST min = 5 * Ports.mm; max = 50 * Ports.mm;
	BEGIN
		WITH msg: Properties.SizePref DO
			IF (msg.w = Views.undefined) OR (msg.h = Views.undefined) THEN
				 msg.w := 20 * Ports.mm; msg.h := 10 * Ports.mm
			ELSE
				Properties.ProportionalConstraint(2, 1, msg.fixedW, msg.fixedH,
										msg.w, msg.h);
				IF msg.h < min THEN msg.h := min; msg.w := 2 * min
				ELSIF msg.h > max THEN msg.h := max; msg.w := 2 * max
				END
			END
		| msg: Properties.ResizePref DO
			msg.horFitToWin := TRUE; msg.verFitToWin := TRUE
		| msg: Properties.FocusPref DO
			msg.setFocus := TRUE
		ELSE	
		END
	END HandlePropMsg;

	PROCEDURE (v: View) HandleCtrlMsg (f: Views.Frame;
										VAR msg: Controllers.Message; 
										VAR focus: Views.View);
		VAR x, y, w, h: INTEGER; m: SET; isDown: BOOLEAN;
	BEGIN
		WITH msg: Controllers.PollOpsMsg DO
			msg.valid := {Controllers.paste}; msg.selectable := TRUE;
			msg.type := "Obx.Tutorial"
		| msg: Controllers.EditMsg DO
			IF msg.op = Controllers.pasteChar THEN	(* клавиши курсора *)
				IF msg.char = 1DX THEN INC(v.x, Ports.mm)
				ELSIF msg.char = 1CX THEN DEC(v.x, Ports.mm)
				ELSIF msg.char  = 1EX THEN DEC(v.y, Ports.mm)
				ELSIF msg.char  = 1FX THEN INC(v.y, Ports.mm)
				END;
				Views.Update(v, Views.keepFrames)
			END
		| msg: Controllers.TrackMsg DO
			v.x := msg.x; v.y := msg.y;
			v.context.GetSize(w, h); v.Restore(f, 0, 0, w, h);
			REPEAT
				f.Input(x, y, m, isDown);
				IF (x # v.x) OR (y # v.y) THEN
					v.x := x; v.y := y; v.Restore(f, 0, 0, w, h)
				END
			UNTIL ~isDown;
			Views.Update(v, Views.keepFrames)
		| msg: Controllers.PollCursorMsg DO
			msg.cursor := Ports.graphicsCursor
		ELSE	
		END
	END HandleCtrlMsg;

	PROCEDURE Deposit*;
		VAR v: View;
	BEGIN
		NEW(v); v.x := 0; v.y := 0; Views.Deposit(v)
	END Deposit;

END ObxViews5.

 "ObxViews5.Deposit; StdCmds.Open"  

Существует много других управляющих сообщений, которые могут быть посланы отображению, но в данной книге ограничимся учебными целями. Имеются сообщения, реализующие общий механизм прокрутки BlackBox Component Framework (Controllers.PollSelectionMsg, Contollers.ScrollMsg, Controllers.PageMsg), сообщения, реализующие механизм drag&drop (Controllers.PollDropMsg, Controllers.DropMsg, Controllers.TransferMessage) и сообщения для контроля выделения (Controllers.SelectMsg). За справкой по этим и другим управляющим сообщениям следует обращаться к встроенной документации модуля Controllers.

6.5. Свойства

Этот раздел завершает расширение сквозного примера отображения: теперь цвет отображения можно изменить с помощью пункта меню Attr[ibutes]. Общий механизм получения или выставления свойств отображения через среду окружения реализован в виде свойств (properties). Отображение может обладать информацией об атрибутах, таких как: шрифт, цвет, размер и еще несколько других атрибутов. Свойства (properties) выставляются с помощью процедуры Properties.SetMsg и проверяются с помощью процедуры Properties.PollMsg. Свойства представляют собой сообщения, связанные в список. Когда отображение получает Properties.SetMsg, оно проходит список свойств и приводит в соответствие те из них, о которых у него есть информация. Properties.GetMsg посылается отображению, если необходимо затребовать значение свойства. Отображение возвращает все значения свойств, о которых есть информация. Свойство может быть вставлено в список свойств сообщения с помощью процедуры Properties.Insert. 
Каждое свойство описывает до 32 атрибутов. Множество known определяет, какие атрибуты известны отображению. Множество так же может определить, какие атрибуты являются атрибутами "только для чтения" с помощью множества readOnly. Множество valid указывает, какие из атрибутов имеют выставленное значение. Например, если в тексте выделено несколько символов разного размера, атрибут size будет поддерживаться для отображения, но его текущее значение не будет определено. Так как выделение не является однородным, то не может быть одного корректного значения.

	TYPE
		Property = POINTER TO ABSTRACT RECORD 
			next-: Properties.Property;
			known, readOnly, valid: SET;	(* valid and readOnly are subsets of known *)
			(p: Property) IntersectWith (q: Properties.Property; OUT equal: BOOLEAN), NEW, ABSTRACT
		END;

Имеется специальное свойство Properties.StdProp. Это свойство заключает в себе атрибуты шрифта так же как цвет и известно для среды окружения BlackBox. Поддерживаются атрибуты: Properties.color, Properties.typeface, Properties.size, Properties.style, Properties.weight. В этих полях записи содержатся соответствующие значения.

	TYPE
		StdProp = POINTER TO RECORD (Properties.Property)
			color: Dialog.Color;
			typeface: Fonts.Typeface;
			size: INTEGER;
			style: RECORD
				val, mask: SET
			END;
			weight: INTEGER
		END;


Последняя версия отображения поддерживает только атрибут цвета для стандартного свойства. При получении сообщения Properties.PollMsg возвращается объект типа Properties.StdProp, в котором установлено только поле для цвета и только атрибут цвета установлена как поддерживаемый и корректный. При Properties.SetMsg в списке свойств отыскивается стандартное свойство, у которого корректно поле цвета. После того, как цвет отображения изменен, его следует обновить во всех рамках. 

MODULE ObxViews6;

	IMPORT Views, Ports, Properties, Controllers;

	TYPE
		View = POINTER TO RECORD (Views.View)
			x, y: INTEGER;
			c: Ports.Color
		END;

	PROCEDURE (v: View)  Restore (f: Views.Frame; l, t, r, b: INTEGER);
	BEGIN
		f.DrawRect(l, t, r, b, Ports.fill, v.c);
		f.DrawLine(v.x, t, v.x, b, 0, Ports.white); f.DrawLine(l, v.y, r, v.y, 0, Ports.white)
	END Restore;
	
	PROCEDURE (v: View) HandlePropMsg (VAR msg: Properties.Message);
		CONST min = 5 * Ports.mm; max = 50 * Ports.mm;
		VAR stdProp: Properties.StdProp; prop: Properties.Property;
	BEGIN
		WITH msg: Properties.SizePref DO
			IF (msg.w = Views.undefined) OR (msg.h = Views.undefined) THEN
				 msg.w := 20 * Ports.mm; msg.h := 10 * Ports.mm
			ELSE
				Properties.ProportionalConstraint(2, 1, msg.fixedW, msg.fixedH,
										msg.w, msg.h);
				IF msg.h < min THEN msg.h := min; msg.w := 2 * min
				ELSIF msg.h > max THEN msg.h := max; msg.w := 2 * max
				END
			END
		| msg: Properties.ResizePref DO
			msg.horFitToWin := TRUE; msg.verFitToWin := TRUE
		| msg: Properties.FocusPref DO
			msg.setFocus := TRUE
		| msg: Properties.PollMsg DO
			NEW(stdProp);
			stdProp.color.val := v.c;
			stdProp.valid := {Properties.color};
			stdProp.known := {Properties.color};
			Properties.Insert(msg.prop, stdProp)
		| msg: Properties.SetMsg DO
			prop := msg.prop;
			WHILE prop # NIL DO
				WITH prop: Properties.StdProp DO
					IF Properties.color IN prop.valid THEN v.c := prop.color.val END
				ELSE
				END;
				prop := prop.next
			END;
			Views.Update(v, Views.keepFrames)
		ELSE	(* пропустить остальные сообщения *)
		END
	END HandlePropMsg;

	PROCEDURE (v: View) HandleCtrlMsg (f: Views.Frame; VAR msg: Controllers.Message;
										VAR focus: Views.View);
		VAR x, y, w, h: INTEGER; m: SET; isDown: BOOLEAN;
	BEGIN
		WITH msg: Controllers.PollOpsMsg DO
			msg.valid := {Controllers.paste}; msg.selectable := TRUE;
			msg.type := "Obx.Tutorial"
		| msg: Controllers.EditMsg DO
			IF msg.op = Controllers.pasteChar THEN
				IF msg.char = 1DX THEN INC(v.x, Ports.mm)
				ELSIF msg.char = 1CX THEN DEC(v.x, Ports.mm)
				ELSIF msg.char  = 1EX THEN DEC(v.y, Ports.mm)
				ELSIF msg.char  = 1FX THEN INC(v.y, Ports.mm)
				END;
				Views.Update(v, Views.keepFrames)
			END
		| msg: Controllers.TrackMsg DO
			v.x := msg.x; v.y := msg.y; v.context.GetSize(w, h); v.Restore(f, 0, 0, w, h);
			REPEAT
				f.Input(x, y, m, isDown);
				IF (x # v.x) OR (y # v.y) THEN
					v.x := x; v.y := y; v.Restore(f, 0, 0, w, h)
				END
			UNTIL ~isDown;
			Views.Update(v, Views.keepFrames)
		| msg: Controllers.PollCursorMsg DO
			msg.cursor := Ports.graphicsCursor
		ELSE	(*  пропустить остальные сообщения *)
		END
	END HandleCtrlMsg;

	PROCEDURE Deposit*;
		VAR v: View;
	BEGIN
		NEW(v); v.x := 0; v.y := 0; v.c := Ports.black; Views.Deposit(v)
	END Deposit;

END ObxViews6.
 "ObxViews6.Deposit; StdCmds.PasteView"	>< перед выполнением коменды установите курсор здесь

6.6 Разделение модели и отображения  

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

MODULE ObxViews10;

	IMPORT Fonts, Ports, Views, Controllers, Properties;

	CONST d = 20 * Ports.point;

	TYPE
		View = POINTER TO RECORD (Views.View)
			i: INTEGER;				(* позиция следующего свободного места в строке *)
			s: ARRAY 256 OF CHAR	(* строка *)
		END;

	PROCEDURE (v: View) Restore (f: Views.Frame; l, t, r, b: INTEGER);
	BEGIN
		f.DrawString(d, d, Ports.black, v.s, Fonts.dir.Default())
	END Restore;

	PROCEDURE (v: View) HandleCtrlMsg (f: Views.Frame;
										VAR msg: Controllers.Message; 
										VAR focus: Views.View);
	BEGIN
		WITH msg: Controllers.EditMsg DO
			IF msg.op = Controllers.pasteChar THEN	(* принять ввод *)
				v.s[v.i] := msg.char; INC(v.i); v.s[v.i] := 0X;	(* добавить символ к строке *)
				Views.Update(v, Views.keepFrames)	(* восстановить v в любой рамке, которая его показывает *)
			END
		ELSE						(* пропустить остальные сообщения *)
		END
	END HandleCtrlMsg;
	
	PROCEDURE (v:View) HandlePropMsg (VAR msg: Properties.Message);
	BEGIN
		WITH msg: Properties.SizePref DO
			IF (msg.w = Views.undefined) OR (msg.h = Views.undefined) THEN 
				msg.w := 10 * d; msg.h := 2 * d
			END
		| msg: Properties.FocusPref DO
			msg.setFocus := TRUE
		ELSE
		END
	END HandlePropMsg;

	PROCEDURE Deposit*;
		VAR v: View;
	BEGIN
		NEW(v); v.s := ""; v.i := 0;
		Views.Deposit(v)
	END Deposit;

END ObxViews10.

 "ObxViews10.Deposit; StdCmds.Open"
Как было указано в последнем разделе, напечатанный символ передается отображению в виде записи управляющего сообщения для последующей обработки процедурой отображения HandleCtrlMsg. В эту процедуру в качестве параметра передается Controllers.EditMsg с введенным символом, в качестве результата работы процедуры введенный символ вставляется в поле s. После этого необходимо обновить отображение, то есть везде, где это отображение видно, его необходимо перерисовать в новом статусе. При этом введенная строка становится на один символ длиннее.
В приведенном выше примере отображение содержит содержательную составляющую (строковое поле s отображения). Эта составляющая должна сохраняться, когда содержимое окна сохраняется на диске. С этой целью отображение поддерживает две процедуры с именами Internalize и Externalize, применение которых показано в очередном обновлении сквозного примера.

MODULE ObxViews11;
(* То же, что и ObxViews10, но строка отображения может быть сохранена и скопирована *)

	IMPORT Fonts, Ports, Stores, Views, Controllers, Properties;

	CONST d = 20 * Ports.point;

	TYPE
		View = POINTER TO RECORD (Views.View)
			i: INTEGER;					(* position of next free slot in string *)
			s: ARRAY 256 OF CHAR		(* string *)
		END;

	PROCEDURE (v: View) Internalize (VAR rd: Stores.Reader);
		VAR version: INTEGER;
	BEGIN
		rd.ReadVersion(0, 0, version);
		IF ~rd.cancelled THEN
			rd.ReadInt(v.i); rd.ReadString(v.s)
		END
	END Internalize;

	PROCEDURE (v: View) Externalize (VAR wr: Stores.Writer);
	BEGIN
		wr.WriteVersion(0);
		wr.WriteInt(v.i); wr.WriteString(v.s)
	END Externalize;

	PROCEDURE (v: View) CopyFromSimpleView (source: Views.View);
	BEGIN
		WITH source: View DO
			v.i := source.i; v.s := source.s
		END
	END CopyFromSimpleView;

	PROCEDURE (v: View) Restore (f: Views.Frame; l, t, r, b: INTEGER);
	BEGIN
		f.DrawString(d, d, Ports.black, v.s, Fonts.dir.Default())
	END Restore;

	PROCEDURE (v: View) HandleCtrlMsg (f: Views.Frame;
											VAR msg: Controllers.Message;
											VAR focus: Views.View);
	BEGIN
		WITH msg: Controllers.EditMsg DO
			IF msg.op = Controllers.pasteChar THEN	(* принять ввод *)
				v.s[v.i] := msg.char; INC(v.i); v.s[v.i] := 0X;	(* добавить символ к строке *)
				Views.SetDirty(v);	(* пометить документ отображения как испорченный *)
				Views.Update(v, Views.keepFrames)	(* восстановить v в любой рамке, которая его отображает *)
			END
		ELSE							(* пропустить остальные сообщения *)
		END
	END HandleCtrlMsg;

	PROCEDURE (v:View) HandlePropMsg (VAR msg: Properties.Message);
	BEGIN
		WITH msg: Properties.SizePref DO
			IF (msg.w = Views.undefined) OR (msg.h = Views.undefined) THEN
				msg.w := 10 * d; msg.h := 2 * d
			END
		| msg: Properties.FocusPref DO
			msg.setFocus := TRUE
		ELSE
		END
	END HandlePropMsg;

	PROCEDURE Deposit*;
		VAR v: View;
	BEGIN
		NEW(v); v.s := ""; v.i := 0;
		Views.Deposit(v)
	END Deposit;

END ObxViews11.

Несколько комментариев по поводу View.Internalize и View.Externalize:
Во-первых: обе процедуры получают в качестве параметра считыватель и записыватель соответственно. Отображатели файла (file mappers) создаются самостоятельно системой BlackBox, отображение просто использует их. 
Во-вторых: View.Internalize должна считать в точности те данные (общее количество), которые записаны View.Externalize.
В-третьих: интерфейс пользователя имеет некоторые отличия для документов, содержащих модифицированные отображения, то есть для "грязных" документов. Им может потребоваться дополнительный вопрос о сохранении содержимого при закрытии документа. Для обеспечения этой возможности отображение должно сообщить, когда его сообщение содержит целиком обновленные данные. Это достигается вызовами процедур Views.BeginModification/Views.EndModification.
В дополнение к View.Internalize и View.Externalize в приведенной выше реализации отображения еще появляется процедура CopyFromSimpleView. Такая процедура должна копировать в себя содержимое переданного отображения того же типа. CopyFromSimpleView обычно имеет тот же эффект, что и операция выброса (externalized) во временный файл с последующим затягиванием (internalized) в отображение-назначение. Кончено же могут быть и исключения. Например, копии текста, содержащие временные метки ошибок не выбрасывают (externalized) их в хранилище.
Для отображений, содержащих (изменившуюся) составляющую, следует реализовывать CopyFromSimpleView для возможности их печати. Это необходимо, потому что оболочка делает собственную копию, которая собственно и печатается, во избежание изменений в оригинальном отображении в процессе разметки страниц, прокруток и других изменений, возможных во время печати. 
В общем в ObxViews11 затронуто большинство возможностей класса простого отображения. Подобное простое отображение часто бывает достаточно, если легкость реализации имеет большую важность. Тем не менее, в некоторых случаях требуется более продвинутый дизайн отображения. В частности, если окно обычно показывает малую часть содержимого отображения, то необходима поддержка многооконного редактирования (multi-view editing).
Многооконное редактирование (multi-view editing) означает, что может быть несколько отображений, содержащих одни и те же данные. Типичным применением этой возможности является возможность показа различных частей одного документа, причем каждая часть показывается в своем окне. Так, если данные отображения изменяются, все задействованные отображения должны быть оповещены об этом, чтобы можно было обновить соответствующие изображения. 
Следующий пример программы, который отличается от предыдущего только поддержкой многооконного редактирования (multi-view editing), примерно в 2 раза длиннее. Это показывает, что дизайн и реализация подобных отображений требует бОльших усилий, чем простое отображение. Такое главное дизайнерское решение, несмотря на дополнительные сложности, оправдывается возрастающим удобством.

MODULE ObxViews12;
(* То же, что и ObxViews11, но использует отдельную модель для строк *)

	IMPORT Fonts, Ports, Stores, Models, Views, Controllers, Properties;

	CONST d = 20 * Ports.point;

	TYPE
		Model = POINTER TO RECORD (Models.Model)
			i: INTEGER;			(* позиция следующего свободного места в строке *)
			s: ARRAY 256 OF CHAR	(* строка *)
		END;

		View = POINTER TO RECORD (Views.View)
			model: Model
		END;


	(* Модель *)

	PROCEDURE (m: Model) Internalize (VAR rd: Stores.Reader);
		VAR version: INTEGER;
	BEGIN
		rd.ReadVersion(0, 0, version);
		IF ~rd.cancelled THEN
			rd.ReadInt(m.i); rd.ReadString(m.s)
		END
	END Internalize;

	PROCEDURE (m: Model) Externalize (VAR wr: Stores.Writer);
	BEGIN
		wr.WriteVersion(0);
		wr.WriteInt(m.i); wr.WriteString(m.s)
	END Externalize;

	PROCEDURE (m: Model) CopyFrom (source: Stores.Store);
	BEGIN
		WITH source: Model DO
			m.i := source.i; m.s := source.s
		END
	END CopyFrom;


	(*  Отображение *)

	PROCEDURE (v: View) Internalize (VAR rd: Stores.Reader);
		VAR version: INTEGER; st: Stores.Store;
	BEGIN
		rd.ReadVersion(0, 0, version);
		IF ~rd.cancelled THEN
			rd.ReadStore(st);
			v.model := st(Model)
		END
	END Internalize;

	PROCEDURE (v: View) Externalize (VAR wr: Stores.Writer);
	BEGIN
		wr.WriteVersion(0);
		wr.WriteStore(v.model)
	END Externalize;

	PROCEDURE (v: View) CopyFromModelView (source: Views.View;
										model: Models.Model);
	BEGIN
		v.model := model(Model)
	END CopyFromModelView;

	PROCEDURE (v: View) Restore (f: Views.Frame; l, t, r, b: INTEGER);
	BEGIN
		f.DrawString(d, d, Ports.black, v.model.s, Fonts.dir.Default())
	END Restore;

	PROCEDURE (v: View) ThisModel (): Models.Model;
	BEGIN
		RETURN v.model
	END ThisModel;

	PROCEDURE (v: View) HandleModelMsg (VAR msg: Models.Message);
	BEGIN
		Views.Update(v, Views.keepFrames)	(* восстановить v в любой рамке, которая его отображает *)
	END HandleModelMsg;

	PROCEDURE (v: View) HandleCtrlMsg (f: Views.Frame;
										VAR msg: Controllers.Message;
										VAR focus: Views.View);
		VAR m: Model; umsg: Models.UpdateMsg;
	BEGIN
		WITH msg: Controllers.EditMsg DO
			IF msg.op = Controllers.pasteChar THEN
				m := v.model;
				m.s[m.i] := msg.char; INC(m.i);
				m.s[m.i] := 0X;	(* добавить символ к строке *)
				Views.SetDirty(v);	(* пометить документ отображения как "испорченный" *)
				Models.Broadcast(m, umsg)	(* обновить все отображения этой модели*)
			END
		ELSE						(* пропустить остальные сообщения *)
		END
	END HandleCtrlMsg;

	PROCEDURE (v:View) HandlePropMsg (VAR msg: Properties.Message);
	BEGIN
		WITH msg: Properties.SizePref DO
			IF (msg.w = Views.undefined) OR (msg.h = Views.undefined) THEN
				msg.w := 10 * d; msg.h := 2 * d
			END
		| msg: Properties.FocusPref DO
			msg.setFocus := TRUE
		ELSE
		END
	END HandlePropMsg;

	PROCEDURE Deposit*;
		VAR v: View; m: Model;
	BEGIN
		NEW(m); m.i := 0; m.s := "";
		NEW(v); v.model := m; Stores.Join(v, m);
		Views.Deposit(v)
	END Deposit;

END ObxViews12.
	
 "ObxViews12.Deposit; StdCmds.Open"
Многооконное редактирование (multi-view editing) реализуется путем выделения данных отображения в отдельную структуру, называемую моделью (model). Эта модель используется совместно всеми отображениями, то есть каждое такое отображение содержит ссылку на модель.


Рисунок 6-2. Два отображения для одной модели.

Модель, как и отображение, является расширением Stores.Store, который, в свою очередь, является базовым типом для всех устойчивых (persistent) объектов. Для хранилищ (Stores) определены процедуры Internalize и Externalize, которые уже были рассмотрены для отображения. В ObxViews12 модель хранит строку и ее длину, в то время как отображение хранит модель целиком. С этой целью Stores.Writer поддерживает WriteStore и StoresReader поддерживает ReadStore процедуры.
Хранилища (Stores) могут состоять из нескольких графических образов. Например, одно или несколько отображений могут ссылаться на одну модель, а модели и отображения содержат расширения. Графический образ хранилища может быть сохранен в файле, называемом документ (document). Для возможности распознавания границ документа при выгрузке, хранилища ограничены доменом (domain). Все хранилища из документа относятся к одному и тому же доменному объекту (Stores.Domain). 
Домены так же используются в качестве контейнера для операций, то есть команд, выполняемых над хранилищем (store) с возможностью их отмены и повторения (см. следующую главу). История операций привязана к домену. Более того, домены определяют набор объектов, которые получают широковещательные сообщения. (Views.Broadcast, Views.Domaincast, Models.Broadcast).
За дальнейшей информацией о хранилищах (stores) и доменах обращайтесь к встроенной документации модуля Stores. 
Теперь, когда модель отделена от отображения, копирование выполняется только для модели, но не для отображения. 
Процедура View.ThisModel возвращает модель для отображения. Реализация по умолчанию возвращает NIL, то есть ее нужно перекрыть для отображений, содержащих модели. Эта процедура используется оболочкой для поиска всех отображений, показывающих данную модель с целью реализации широковещания для модели, как показано в следующем параграфе.
Отображения с моделями реализуют процедуру View.CopyFromModelView взамен View.CopyFromSimpleView. Подобно Internalize и Externalize, процедура CopyFromSimpleView и CopyFromModelView экспортируются как процедуры для реализации, то есть они только реализуются, но не вызываются. Они вызываются внутренними механизмами модуля Views в функциях CopyOf и CopyWithNewModel.
При изменении модели все отображения, показывающие модель, должны быть обновлены. Для этой цели генерируется широковещание (broadcast) для модели: модель вещает сообщения всем отображениям, содержащим конкретную модель, с информацией о случившихся изменениях в модели.
Это дает возможность отображению обновить содержимое экрана. Широковещание модели происходит в процедуре View.HandleCtrlMsg путем вызова Models.Broadcast. Сообщения получает каждое отображение, содержащее корректную модель, посредством процедуры View.HandleModelMsg.
Такая косвеная связь важна, если одну модель представляют разные типы отображений, например, табличное и графическое представление модели табличных данных, и, вместо обновления всего отображения (как сделано в предыдущих примерах), обновляется только необходимая часть отображения. Необходимая для обновления часть может быть совершенно разной для разных типов отображений.
Очень сложные модели могут содержать (или в них могут быть внедрены) несколько отображений в качестве составных частей. Например, текстовое отображение может изображать не только текстовую модель, содержащую только символы (натуральное содержимое), но и графические или другие отображения, обособленно следующие в текстовом потоке.
Комбинация многооконного (multi-view) редактирования и иерархического внедрения отображений может привести к ситуации, когда два текстовых отображения показывают одну текстовую модель, которая в свою очередь, содержит графическое отображение. Каждое текстовое отображение существует в собственном окне, поэтому имеет собственные рамки. Тем не менее, графическое отображение уникально тем, что внедрено в текстовую модель, которая, в свою очередь, разделяется обеими текстовыми отображениями. Тем не менее, графика может показываться одновременно в обеих текстовых отображениях, а, значит, может быть две рамки для этого отображения.


Рисунок 6-3. Две рамки в одном отображении.

Вследствие этого, одно и то же отображение может показываться в нескольких местах экрана одновременно. В таком случае это означает, что при изменении отображения должны обновиться несколько участков: отображение должно восстановить необходимые участки для каждой рамки отображения. Как следствие, механизм сообщений должен обеспечить обновление отображений для каждой его рамки. Это достигается подобно механизму сообщений при изменении моделей: с помощью широковещания для отображения (view broadcast). К счастью, стандартный механизм обновлений внутри оболочки автоматически обрабатывает второй уровень широковещания (см. ниже).
Сейчас сведем вместе типичные события, происходящие при взаимодействии пользователя и отображения. 

Обработка управляющих сообщений:
1. Какое-либо управляющее сообщение посылается отображению, обладающему фокусом ввода.
2. Отображение, обладающее фокусом ввода, интерпретирует сообщение и соответствующим образом изменяет свою модель.
3. Модель осуществляет широковещание сообщений, описывающих случившиеся изменения.

		Обработка сообщений модели.
4. Каждое отображение для данной модели получает сообщение модели.
5. Они определяют, как должно измениться изображение в связи с изменениями модели.
6. Они (отображения) осуществляют широковещание сообщений отображения, описывающих как должно измениться изображение.

Обработка сообщений отображения
7. Каждое отображение получает уведомительное сообщение по одному на каждую рамку отображения.
8. Каждый раз отображение перерисовывает свои рамки в соответствии с сообщениями отображения.

Этот механизм является основополагающим для BlackBox Component Framework. Его понимание является наиболее важной и сложной частью реализации отображений. Интерпретация различных управляющих сообщений, определенных в модуле Controllers, является более чем необходимой для понимания трудностей новой концепции. Более того, необходимо интерпретировать только интересующие нас управляющие сообщения, потому что неподходящие сообщения следует просто игнорировать.
Обычно шаги 6, 7 и 8 обрабатываются каркасом автоматически: отображения просто вызывают Views.Update для обновления всего отображения целиком или Views.UpdateIn для обновления какой-либо прямоугольной области. Вызов этих процедур обновления приводит к так называемому ленивому обновлению (lazy update), то есть оболочка помечает область для обновления, но перерисовка НЕ производится сразу. Вместо этого вначале завершаются команды, чтобы все структуры данных изменяемого отображения пришли в согласованное состояние. Только изображения отображений остаются теми же, что и до выполнения команд. Только после этого оболочка перерисовывает все области отображения, помеченные к обновлению. Это означает, что для каждой рамки, отрисованной в отображении, вызывается метод отображения Restore и в качестве аргумента передается рамка. Если отображение показывается в двух (или более) рамках разных окон, то обновляется два (или более) число раз, по одному разу на каждую рамку. Когда оболочка обновляет рамки внутри окна, происходит временное перенаправление операции в порт фонового пиксельного буфера. После обновления всех рамок окна, подлежащих перерисовке, оболочка копирует пиксельный буфер в пиксели экрана и отключает перенаправление операции перерисовки.
Эффектом от механизма буферизации и механизма ленивого обновления является минимизация мерцания. Механизм ленивого обновления (как для Apple MacOS, так и для Microsoft Windows) означает, что для сложных операций редактирования данная область экрана будет обновляться однократно. Это позволяет подавить мерцание, так как оно может возникнуть при быстрой многократной перерисовке какой-либо области. Механизм буферизации подавляет мерцание при отрисовке слоев "снизу-вверх" (так как это так же означает быструю многократную перерисовку). Например, вначале может отрисовываться фон одним цветом, а затем поверх рисуется прямоугольник с заливкой другого цвета. Благодаря механизму буферизации пользователь никогда не увидит ситуацию, когда фон отрисован, а прямоугольник - еще нет. 
Механизм ленивого обновления обладает еще одним преимуществом: целиком разделяется изменение модели и ее отрисовка. Без этого механизма было бы действительно сложно правильно реализовать обновление экрана при сложном редактировании. Например, рассмотрим перемещение выделения графического объекта в графическом редакторе. Вначале выделенный объект должен быть уделен со старой позиции, что означает, что объекты, которые ранее были скрыты, должны быть перерисованы (но не сам выделенный объект). Затем позиция объекта изменяется и он должен быть перерисован в новой позиции (если исходная и конечная позиции перекрываются, перерисовка произойдет дважды). Перерисовка может потребоваться несколько раз, так как одни и те же данные могут показываться в нескольких окнах. Но, конечно же, изменение позиции объекта происходит только единожды. Гораздо менее мудрено просто вызвать Views.UpdateIn для прямоугольников, которые требуют (могут требовать) обновления и позволить оболочке вызвать требуемые Restore позже.
Обычно механизма ленивого обновления вполне достаточно. Таким образом программирование отображений с использованием чистых сообщений отображения (explicit view messages) применяется, только если не требуется полного обновления всех рамок отображения. То есть для спецсимволов типа: отметки выделения, фокуса, каретки, "резиновых" рамок и тому подобных обратных связей в процессе перемещения мыши. Подобные метки иногда могут быть обработаны более эффективно, так как они являются непроизвольными: двойное их применение приводит к их подавлению. Поэтому часто метки переключаются пользовательскими сообщениями отображения в процессе команды, но не посредством механизма отложенного ленивого обновления. За исключением подобных легковесных отметок, всегда следует применять описанный выше механизм ленивого обновления. Следует отметить наличие метода Ports.Frame.MarkRect, служащего специально для отрисовки подобных меток.

6.7 Операции

Теперь, после того, как рассмотрены критические составляющие реализации отображений, можно представить второстепенные особенности. В следующей программе, являющейся вариантом приведенных выше, управляющие сообщения не интерпретируются напрямую. Вместо этого создается и выполняется объект операции (operation object). Операция обеспечивает возможности отката, как показано в нижеследующей программе. 
В этом примере определяется операция PasteCharOp. Процедура Do операции, выполняющая желаемые модификации, должна быть непроизвольной (involutory), то есть при повторном вызове она нейтрализует предыдущий вызов. Флаг PasteCharOp.do применяется для обозначения того, был ли символ PasteCharOp.char вставлен или удален из модели PasteCharOp.m. В любом случае процедура Do обязана обновить все отображения для модели PasteCharOp.m с помощью широковещания модели.
Процедура NewPasteCharOp создает новую операцию, процедура Models.Do выполняет эту операцию, то есть вызывается процедура Do операции, а собственно операция записывается для возможности дальнейшего отката. Наименование операции, определенной в команде Models.Do появляется в меню Edit после пунктов Undo или Redo соответственно.

MODULE ObxViews13;
(* Такой же, как ObxViews12, но генерирует операции с возможность отмены для вставки символа *)

	IMPORT Fonts, Ports, Stores, Models, Views, Controllers, Properties;

	CONST d = 20 * Ports.point;

	TYPE
		Model = POINTER TO RECORD (Models.Model)
			i: INTEGER;			(* позиция следующего свободного места в строке *)
			s: ARRAY 256 OF CHAR	(* строка *)
		END;

		View = POINTER TO RECORD (Views.View)
			model: Model
		END;

		PasteCharOp = POINTER TO RECORD (Stores.Operation)
			model: Model;
			char: CHAR;
			do: BOOLEAN
		END;


	(* PasteCharOp *)

	PROCEDURE (op: PasteCharOp) Do;
		VAR m: Model; msg: Models.UpdateMsg;
	BEGIN
		m := op.model;
		IF op.do THEN			(* do изменение операции *)
			m.s[m.i] := op.char; INC(m.i)	(* вставка символа в строку *)
		ELSE						(* отмена операции *)
			DEC(m.i)				(* удаление символа из строки *)
		END;
		m.s[m.i] := 0X;
		op.do := ~op.do;		(* переключатель между "do" и "undo" *)
		Models.Broadcast(m, msg)	(* обновить все отображения модели*)
	END Do;

	PROCEDURE NewPasteCharOp (m: Model; char: CHAR): PasteCharOp;
		VAR op: PasteCharOp;
	BEGIN
		NEW(op); op.model := m; op.char := char; op.do := TRUE;
		RETURN op
	END NewPasteCharOp;


	(* Модель *)

	PROCEDURE (m: Model) Internalize (VAR rd: Stores.Reader);
		VAR version: INTEGER;
	BEGIN
		rd.ReadVersion(0, 0, version);
		IF ~rd.cancelled THEN
			rd.ReadInt(m.i); rd.ReadString(m.s)
		END
	END Internalize;

	PROCEDURE (m: Model) Externalize (VAR wr: Stores.Writer);
	BEGIN
		wr.WriteVersion(0);
		wr.WriteInt(m.i); wr.WriteString(m.s)
	END Externalize;

	PROCEDURE (m: Model) CopyFrom (source: Stores.Store);
	BEGIN
		WITH source: Model DO
			m.i := source.i; m.s := source.s
		END
	END CopyFrom;


	(* Отображение *)

	PROCEDURE (v: View) Internalize (VAR rd: Stores.Reader);
		VAR version: INTEGER; st: Stores.Store;
	BEGIN
		rd.ReadVersion(0, 0, version);
		IF ~rd.cancelled THEN
			rd.ReadStore(st);
			v.model := st(Model)
		END
	END Internalize;

	PROCEDURE (v: View) Externalize (VAR wr: Stores.Writer);
	BEGIN
		wr.WriteVersion(0);
		wr.WriteStore(v.model)
	END Externalize;

	PROCEDURE (v: View) CopyFromModelView (source: Views.View; model: Models.Model);
	BEGIN
		v.model := model(Model)
	END CopyFromModelView;

	PROCEDURE (v: View) Restore (f: Views.Frame; l, t, r, b: INTEGER);
	BEGIN
		f.DrawString(d, d, Ports.black, v.model.s, Fonts.dir.Default())
	END Restore;

	PROCEDURE (v: View) ThisModel (): Models.Model;
	BEGIN
		RETURN v.model
	END ThisModel;

	PROCEDURE (v: View) HandleModelMsg (VAR msg: Models.Message);
	BEGIN
		Views.Update(v, Views.keepFrames)  (* восстановить v в любой рамке, отображающей его *)
	END HandleModelMsg;

	PROCEDURE (v: View) HandleCtrlMsg (f: Views.Frame; VAR msg: Controllers.Message;
										VAR focus: Views.View);
		VAR op: Stores.Operation;
	BEGIN
		WITH msg: Controllers.EditMsg DO
			IF msg.op = Controllers.pasteChar THEN
				op := NewPasteCharOp(v.model, msg.char);	(* сгенерировать операцию *)
				Models.Do(v.model, "Typing", op)	(* выполнить операцию *)
			END
		ELSE						(* пропустить остальные сообщения *)
		END
	END HandleCtrlMsg;

	PROCEDURE (v:View) HandlePropMsg (VAR msg: Properties.Message);
	BEGIN
		WITH msg: Properties.SizePref DO
			IF (msg.w = Views.undefined) OR (msg.h = Views.undefined) THEN
				msg.w := 10 * d; msg.h := 2 * d
			END
		| msg: Properties.FocusPref DO
			msg.setFocus := TRUE
		ELSE
		END
	END HandlePropMsg;

	PROCEDURE Deposit*;
		VAR v: View; m: Model;
	BEGIN
		NEW(m); m.i := 0; m.s := "";
		NEW(v); v.model := m; Stores.Join(v, m);
		Views.Deposit(v)
	END Deposit;

END ObxViews13.

 "ObxViews13.Deposit; StdCmds.Open"
Другие примеры с применением механизма отката/повтора посредством операций можно найти, например, в ObxOmosi или ObxLines.


6.8 Разделение интерфейса и реализации

В качестве последней версии простого отображения, изменим предыдущий вариант путем экспорта типов Model и View и разделения интерфейса и реализации этих двух типов. Для выполнения последнего вводится так называемые фабричные объекты (directory objects), которые создают (скрытые) предопределенные реализации абстрактных типов данных. Теперь пользователь обязан реализовать собственные версии абстрактных типов и предоставить их через собственный фабричный объект, который не может быть унаследован от реализации по умолчанию во избежание проблемы хрупких базовых классов. 
Рабочие подсистемы BlackBox могут дополнительно разделять модуль на два: один для модели, другой для отображения. Может быть введен и третий модуль с командами, если число и сложность команд тревожно высоко (например: TextModels, TextViews и TextCmds). Еще более сложное отображение следует, в свою очередь, разделять на отображения и контроллеры, каждый в своем модуле.

MODULE ObxViews14;
(* Такой же как ObxViews13, но интерфейсы и реализация разделены, и операция напрямую в процедуре вставки *)

	IMPORT Fonts, Ports, Stores, Models, Views, Controllers, Properties;

	CONST d = 20 * Ports.point;

	TYPE
		Model* = POINTER TO ABSTRACT RECORD (Models.Model) END;

		ModelDirectory* = POINTER TO ABSTRACT RECORD END;

		View* = POINTER TO ABSTRACT RECORD (Views.View) END;

		Directory* = POINTER TO ABSTRACT RECORD END;


		StdModel = POINTER TO RECORD (Model)
			i: INTEGER;			(* позиция следующего пустого места в строке *)
			s: ARRAY 256 OF CHAR	(* строка *)
		END;

		StdModelDirectory = POINTER TO RECORD (ModelDirectory) END;

		StdView = POINTER TO RECORD (View)
			model: Model
		END;

		StdDirectory = POINTER TO RECORD (Directory) END;

		PasteCharOp = POINTER TO RECORD (Stores.Operation)
			model: StdModel;
			char: CHAR;
			do: BOOLEAN
		END;


	VAR
		mdir-: ModelDirectory;
		dir-: Directory;


	(* Модель*)

	PROCEDURE (m: Model) Insert* (char: CHAR), NEW, ABSTRACT;
	PROCEDURE (m: Model) Remove*, NEW, ABSTRACT;
	PROCEDURE (m: Model) GetString* (OUT s: ARRAY OF CHAR), NEW, ABSTRACT;

	(* ModelDirectory *)

	PROCEDURE (d: ModelDirectory) New* (): Model, NEW, ABSTRACT;

	(* PasteCharOp *)

	PROCEDURE (op: PasteCharOp) Do;
		VAR m: StdModel; msg: Models.UpdateMsg;
	BEGIN
		m := op.model;
		IF op.do THEN				(* do изменение операции *)
			m.s[m.i] := op.char; INC(m.i);
		ELSE						(* отменить изменение операции *)
			DEC(m.i)				(* удалить символ из строки *)
		END;
		m.s[m.i] := 0X;
		op.do := ~op.do;			(* переключатель между "do" и "undo" *)
		Models.Broadcast(m, msg)	(* обновить все отображения модели*)
	END Do;


	(* StdModel *)

	PROCEDURE (m: StdModel) Internalize (VAR rd: Stores.Reader);
		VAR version: INTEGER;
	BEGIN
		rd.ReadVersion(0, 0, version);
		IF ~rd.cancelled THEN
			rd.ReadInt(m.i); rd.ReadString(m.s)
		END
	END Internalize;

	PROCEDURE (m: StdModel) Externalize (VAR wr: Stores.Writer);
	BEGIN
		wr.WriteVersion(0);
		wr.WriteInt(m.i); wr.WriteString(m.s)
	END Externalize;

	PROCEDURE (m: StdModel) CopyFrom (source: Stores.Store);
	BEGIN
		WITH source: StdModel DO
			m.i := source.i; m.s := source.s
		END
	END CopyFrom;


	PROCEDURE (m: StdModel) Insert (char: CHAR);
		VAR op: PasteCharOp;
	BEGIN
		NEW(op); op.model := m; op.char := char; op.do := TRUE;
		Models.Do(m, "insertion", op)
	END Insert;

	PROCEDURE (m: StdModel) Remove;
		VAR msg: Models.UpdateMsg;
	BEGIN
		DEC(m.i); m.s[m.i] := 0X;
		Models.Broadcast(m, msg)	(* обновить все отображения этой модели*)
	END Remove;

	PROCEDURE (m: StdModel) GetString (OUT s: ARRAY OF CHAR);
	BEGIN
		s := m.s$
	END GetString;


	(* StdModelDirectory *)

	PROCEDURE (d: StdModelDirectory) New (): Model;
		VAR m: StdModel;
	BEGIN
		NEW(m); m.s := "";  m.i := 0; RETURN m
	END New;



	(* Directory *)

	PROCEDURE (d: Directory) New* (m: Model): View, NEW, ABSTRACT;


	(* StdView *)

	PROCEDURE (v: StdView) Internalize (VAR rd: Stores.Reader);
		VAR version: INTEGER; st: Stores.Store;
	BEGIN
		rd.ReadVersion(0, 0, version);
		IF ~rd.cancelled THEN
			rd.ReadStore(st);
			IF st IS Model THEN
				v.model := st(Model)
			ELSE
				(* реализация конкретной модели не может быть загружена->
					было создано хранилище-нелегал *)
				rd.TurnIntoAlien(Stores.alienComponent)
				(* загрузка v отменена *)
			END
		END
	END Internalize;

	PROCEDURE (v: StdView) Externalize (VAR wr: Stores.Writer);
	BEGIN
		wr.WriteVersion(0);
		wr.WriteStore(v.model)
	END Externalize;

	PROCEDURE (v: StdView) CopyFromModelView (source: Views.View; model: Models.Model);
	BEGIN
		WITH source: StdView DO
			v.model := model(Model)
		END
	END CopyFromModelView;

	PROCEDURE (v: StdView) Restore (f: Views.Frame; l, t, r, b: INTEGER);
		VAR s: ARRAY 256 OF CHAR;
	BEGIN
		v.model.GetString(s);
		f.DrawString(d, d, Ports.black, s, Fonts.dir.Default())
	END Restore;

	PROCEDURE (v: StdView) ThisModel (): Models.Model;
	BEGIN
		RETURN v.model
	END ThisModel;

	PROCEDURE (v: StdView) HandleModelMsg (VAR msg: Models.Message);
	BEGIN
		Views.Update(v, Views.keepFrames)	(* восстановить v в любой рамке, отображающей его *)
	END HandleModelMsg;

	PROCEDURE (v: StdView) HandleCtrlMsg (f: Views.Frame;
										VAR msg: Controllers.Message;
										VAR focus: Views.View);
	BEGIN
		WITH msg: Controllers.EditMsg DO
			IF msg.op = Controllers.pasteChar THEN
				v.model.Insert(msg.char)	(*  отменяемая вставка *)
			END
		ELSE						(* пропустить другие сообщения *)
		END
	END HandleCtrlMsg;

	PROCEDURE (v:StdView) HandlePropMsg (VAR msg: Properties.Message);
	BEGIN
		WITH msg: Properties.SizePref DO
			IF (msg.w = Views.undefined) OR (msg.h = Views.undefined) THEN
				msg.w := 10 * d; msg.h := 2 *d 
			END
		| msg: Properties.FocusPref DO
			msg.setFocus := TRUE
		ELSE
		END
	END HandlePropMsg;


	(* StdDirectory *)

	PROCEDURE (d: StdDirectory) New* (m: Model): View;
		VAR v: StdView;
	BEGIN
		ASSERT(m # NIL, 20);
		NEW(v); v.model := m; Stores.Join(v, m);
		RETURN v
	END New;


	PROCEDURE Deposit*;
		VAR v: View;
	BEGIN
		v := dir.New(mdir.New());
		Views.Deposit(v)
	END Deposit;

	PROCEDURE SetModelDir* (d: ModelDirectory);
	BEGIN
		ASSERT(d # NIL, 20);
		mdir := d
	END SetModelDir;

	PROCEDURE SetDir* (d: Directory);
	BEGIN
		ASSERT(d # NIL, 20);
		dir := d
	END SetDir;

	PROCEDURE Init;
		VAR md: StdModelDirectory; d: StdDirectory;
	BEGIN
		NEW(md); mdir := md;
		NEW(d); dir := d
	END Init;

BEGIN
	Init
END ObxViews14.

 "ObxViews14.Deposit; StdCmds.Open"
Модуль DevMarkers представляет собой простой пример модуля BlackBox, отвечающий такому дизайну. Отображение маркера является простым отображением без модели, поэтому предоставляется только фабричный объект для создания новых отображений. Второй фабричный объект DevMarkers.StdDir сохраняет стандартную реализацию, обеспечиваемую данным модулем. Он может применяться для восстановления реализации по умолчанию или для повторного использования реализации по умолчанию (как экземпляр) в другом компоненте. 
Разделение определения типов и их реализации рекомендуется при проектировании новой подсистемы BlackBox. Однако простые типы отображения не для широкого применения или не для дальнейшего расширения могут распространяться без таких дополнительных усилий. 
Следует отметить, что мастер подсистем (subsystem wizard) (пункт меню Tools -> Create Subsystem...) помогает создавать шаблоны различных видов отображений. В частности возможно создавать раздельные модули для модели и отображений. Инструмент использует текстовые шаблоны, хранящиеся в Dev/Rsrc/New. Может быть интересным изучение файлов Models5, Views5 и Cmds5 в этом каталоге.