Ермаков И. Е. График истории показаний / Ermakov I. E. The chart of instrument reading history

Постановка задачи (отсюда):

Есть вот такая вот задачка: из некой базы данных (пускай рэндом, для простоты) идет периодическое считывание данных в режиме реального времени, пусть, для определенности, раз в 10 сек. Требуется в реальном времени отображать графики считанных значений в зависимости от времени. Ось абсцисс расположена вертикально, ось ординат - горизонтально, рисование идет «сверху вниз», т.е. более поздняя по времени считывания точка добавляется снизу. Требуется реализовать прокрутку мышкой за скролл (чтоб в любой момент времени можно было посмотреть, что там было х минут назад). Сверху рисуется заголовок, он при прокрутке графика не двигается. «А ну-ка, оберонщики!»

Вступление

Разработаем полноценное отображение BlackBox («вьюшку»), которое будет поддерживать рисование, прокрутку, обработку ввода пользователя, сохранение загрузку и копирование. Для этого нам потребуется пунктуально описать требуемые процедуры и обработку необходимых сообщений - далее BlackBox Framework сам обеспечит все аспекты поведения нашей вьюшки, позволяя затем использовать её в составных документах среды как угодно, где угодно и с чем угодно…

Для понимания дальнейшего рекомендуется ознакомиться с главами 1-5 документации BlackBox.

Определение общей структуры

Начнём с общего проектирования. Как известно, один из главных принципов здесь - разделение на мелкие, обозримые, самостоятельные части («разделяй и властвуй», по-видимому, впервые отрефлексировано в такой формулировке Информатикой-21). Чем и займёмся… Тем более, что для этого достаточно в нашем случае применить стандартные паттерны компонентного программирования в Блэкбоксе.

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

Таким образом, мы должны реализовать для нашего компонента модель и непосредственно графическое отображение. Выделим их в отдельные модули: MyHistoryModels и MyHistoryViews.

Кроме того, будем использовать стандартную для BlackBox схему сокрытия реализации, при которой модуль экспортирует только абстрактный базовый интерфейс (POINTER TO ABSTRACT RECORD), скрывая расширяющий его тип-реализацию. Вовне модуль экспортирует также объект-фабрику (dir: Directory), который служит для создания экземпляров типа. Могут инсталлироваться различные фабрики, подменяя таким образом реализацию прозрачно для всех других модулей. Подробно этот паттерн описан в Главе 3 «Приёмы проектирования» документации BlackBox.

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

Разработка модели для хранения данных

Продумаем интерфейс, необходимый для накопления и чтения измерений.

Продолжая применять принцип «разделяй и властвуй»… Напрашивается применение паттерна CRM (Carrier-Rider-Mapper, Носитель-Курьер-Проектор) (смотрим Главу 3 «Приёмы проектирования» документации BlackBox).

В нашем случае носитель данных позволяет последовательно накапливать измерения. Считывание данных ведётся через курьер - Reader. Никаких излишеств в базовые типы добавлять не следует. Например, интерполяция при необходмости всегда может быть реализована через обёртку-проектор.

Важное лирическое отступление: Ни в коем случае (ни в интерфейсе, ни в реализации) не пытаемся угадать все возможные варианты использования «на будущее» - при таком подходе получаются переусложнённые и нежизнеспособные продукты (см. Принцип Калашникова Информатики-21, принципы эволюции и разных системных подходов). Вместо этого смиряемся с непредсказуемостью будущего и пытаемся ухватить некий малый базис, инварианты, которые будут нужны в достаточно широком спектре случаев достаточно приличное время.

Получаем следующий интерфейс модуля MyHistoryModels:

DEFINITION MyHistoryModels;
 
	IMPORT Models, Stores;
 
	TYPE
		Model = POINTER TO ABSTRACT RECORD (Models.Model(Stores.Store))
			(m: Model) AddSample (dt: INTEGER; f: REAL), NEW, ABSTRACT;
			(* 
				Добавление очередного измеренного значения.
				dt - прошедший интервал времени;
				f - значение измерения.
				Предусловие:
				20	dt > 0
			*)
 
			(m: Model) Length (): INTEGER, NEW, ABSTRACT;
			(*
			Время последнего измерения (интервал, прошедший между первым и последним измерением)
			*)
 
			(m: Model) NewReader (): Reader, NEW, ABSTRACT
			(*
			Создать новый курьер.
			Постусловие:
				reader.eod = FALSE;
				остальные поля не имеют смысла.
			*)
 
		END;
 
		Reader = POINTER TO ABSTRACT RECORD 
			t1, t2: INTEGER;
			f1, f2: REAL;
			(rd: Reader) Base (): Model, NEW, ABSTRACT;
			(rd: Reader) Read, NEW, ABSTRACT;
			(rd: Reader) ReadPrev, NEW, ABSTRACT;
			(rd: Reader) SetPos (t: INTEGER), NEW, ABSTRACT
		END;
 
	VAR
		dir-: Directory;
		stdDir-: Directory;
 
	PROCEDURE SetDir (d: Directory);
 
END MyHistoryModels.

В этом же модуле целесообразно реализовать службу, которая подключает модель к датчику и выполняет периодический съём показаний. В качестве датчика будем рассматривать любую процедуру с сигнатурой вида: SensorProc = PROCEDURE (num: INTEGER): REAL, которая получает номер датчика и выдаёт текущее показание.

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

Таким образом, пополняем интерфейс модуля следующими элементами:

MODULE MyHistoryModels;
	...
	TYPE
		SensorProc = PROCEDURE (num: INTEGER): REAL;
	...
	PROCEDURE Connect (IN sensorProc: ARRAY OF CHAR; sensorNum: INTEGER; buffer:
Model; interval: INTEGER; tRatio, fRatio: REAL);
	(* interval - период съёма показаний в милисекундах;
tRatio и fRatio - множители времени и величины, с которыми показания
заносятся в модель *)
	PROCEDURE Disconnect (buffer: Model);

Далее исполняем стандартную реализацию модели - скрытые в модуле типы StdModel, StdReader, StdDirectory - и службы съёма показаний. Не «паримся» по поводу особого быстродействия и проч. (см. лирическое отступление выше) - делаем нормальную, стандартную реализацию. При необходимости всегда может быть разработан модуль с альтернативной реализацией, которая будет прозрачно динамически подключена вместо стандартной (для этого и нужны фабрики - Directories).

MODULE MyHistoryModels;
 
(*
	Учебный пример
	(С) 2008 И. Ермаков
*)
 
	IMPORT Views, Models, Stores, Services, Meta, StdLog;
 
	CONST
		pageSize = 128*128;
		version = 0;
 
	TYPE
		Model* = POINTER TO ABSTRACT RECORD (Models.Model)
			sensor: Sensor
		END;
 
		Reader* = POINTER TO ABSTRACT RECORD
			t1*, t2*: INTEGER;
			f1*, f2*: REAL
		END;
 
		Directory* = POINTER TO ABSTRACT RECORD END;
 
		StdModel = POINTER TO RECORD (Model)
			n: INTEGER;
			first, last: Page		
		END;
 
		Page = POINTER TO RECORD
			prev, next: Page;
			n: INTEGER;
			t: ARRAY pageSize OF INTEGER;
			f: ARRAY pageSize OF REAL
		END;
 
		StdReader = POINTER TO RECORD (Reader)
			base: StdModel;
			page: Page;
			n: INTEGER
		END;
 
		StdDirectory = POINTER TO RECORD (Directory) END;
 
		SensorProc* = PROCEDURE (num: INTEGER): REAL;
 
		Sensor = POINTER TO RECORD (Services.Action)
			m: Model;
			proc: Meta.Name;
			num, interval: INTEGER;
			lastT: LONGINT;
			tRatio, fRatio: REAL;
			it: Meta.Item;
			adr: SensorProc
		END;
 
	VAR
		dir-, stdDir-: Directory;
 
	(* Model *)
 
	PROCEDURE (m: Model) AddSample* (dt: INTEGER; f: REAL), NEW, ABSTRACT;
	(* 
		Добавление очередного измеренного значения.
		dt - прошедший интервал времени;
		f - значение измерения.
		Предусловие:
			20	dt > 0
	*)
 
	PROCEDURE (m: Model) Length* (): INTEGER, NEW, ABSTRACT;
	(*
		Время последнего измерения (интервал, прошедший между первым и последним измерением)
	*)
 
	PROCEDURE (m: Model) NewReader* (): Reader, NEW, ABSTRACT;
	(*
		Создать новый курьер.
		Постусловие:
			reader.eod = FALSE;
			остальные поля не имеют смысла.
	*)
 
	(* Reader *)
 
	PROCEDURE (rd: Reader) Base* (): Model, NEW, ABSTRACT;
	(* Модель, к которой привязан курьер *)
 
	PROCEDURE (rd: Reader) SetPos* (t: INTEGER), NEW, ABSTRACT;
	(* Считать предшествующий и последующий замеры для момента времени t.
		Предусловие
			20	t >= 0
 
		Постусловие:
				rd.t1 <= t
				rd.f1 - значение замера в момент времени t1
				rd.f2 - значение замера в момент времени t2
			если нет замеров после t
				rd.t2 = rd.t1
			если есть замеры после t
				(rd.t1 < rd.t2) & (rd.t <= rd.t2)
	*)
 
 
	PROCEDURE (rd: Reader) Read*, NEW, ABSTRACT;
	(*
		Считать очередной замер.
 
		Постусловие:
			rd.f1 - значение замера в момент времени t1
			rd.f2 - значение замера в момент времени t2
 
			если Read вызван первый раз после создания курьера
				rd.t1 = 0
			если Read вызван не первый раз
				rd.t1 = rd.t2' (Прим.: t2' - прошлое t2)
 
			если очередной замер был считан
				rd.t1 < rd.t2
			если был достигнут конец замеров
				rd.t1 = rd.t2
	*)
 
	PROCEDURE (rd: Reader) ReadPrev*, NEW, ABSTRACT;
	(*
		Считать предыдущий замер.
 
		Постусловие:
			rd.f1 - значение замера в момент времени t1
			rd.f2 - значение замера в момент времени t2
 
			если Read вызван первый раз после создания курьера
				rd.t1 = 0
			если Read вызван не первый раз и rd.t1' # 0
				rd.t2 = rd.t1' (Прим.: t1' - прошлое t1)
			если был достигнуто начало замеров
				rd.t1 = 0
	*)
 
	(* Directory *)
 
	PROCEDURE (d: Directory) New* (f0: REAL): Model, NEW, ABSTRACT;
	(*
		Создать новую модель для накопления данных.
		f0 - значение в начальный (нулевой) момент времени
	*)
 
	(* StdModel *)
 
	PROCEDURE NewPage (tLast, dt: INTEGER; f: REAL): Page;
		VAR p: Page;
	BEGIN
		NEW(p);
		p.t[0] := tLast + dt; p.f[0] := f;
		p.n := 1;
		RETURN p
	END NewPage;
 
	PROCEDURE AddToPage (p: Page; dt: INTEGER; f: REAL);
	BEGIN
		p.t[p.n] := p.t[p.n-1] + dt; p.f[p.n] := f;
		INC(p.n)
	END AddToPage;
 
	PROCEDURE (m: StdModel) AddSample (dt: INTEGER; f: REAL);
		VAR msg: Models.UpdateMsg;
	BEGIN
		ASSERT(dt > 0, 20);
		IF m.last.n < pageSize THEN
			AddToPage(m.last, dt, f)
		ELSE
			m.last.next := NewPage(m.last.t[pageSize-1], dt, f);
			m.last.next.prev := m.last;
			m.last := m.last.next
		END;
		INC(m.n);
		Models.Broadcast(m, msg)
	END AddSample;
 
	PROCEDURE (m: StdModel) Length (): INTEGER;
	BEGIN
		IF m.last # NIL THEN
			RETURN m.last.t[m.last.n-1]
		ELSE
			RETURN 0
		END
	END Length;
 
	PROCEDURE (m: StdModel) NewReader (): Reader;
		VAR rd: StdReader;
	BEGIN
		ASSERT(m.first # NIL, 20);
		NEW(rd);
		rd.base := m;
		RETURN rd
	END NewReader;
 
	(* Model loading, saving and copying *)
 
	PROCEDURE (m: StdModel) Externalize (VAR wr: Stores.Writer);
		VAR p: Page; i: INTEGER;
				n: INTEGER;
	BEGIN
		wr.WriteVersion(version);
		wr.WriteInt(m.n);
		p := m.first; n := 0;
		WHILE p # NIL DO
			i := 0;
			FOR i := 0 TO p.n-1 DO
				wr.WriteInt(p.t[i]);
				wr.WriteReal(p.f[i]);
				INC(n)
			END;
			p := p.next
		END
	END Externalize;
 
	PROCEDURE (m: StdModel) Internalize (VAR rd: Stores.Reader);
		VAR ver, n, i, t0, t: INTEGER; f: REAL;
	BEGIN
		rd.ReadVersion(0, version, ver);
		IF ~rd.cancelled THEN
			rd.ReadInt(n);
			rd.ReadInt(t); rd.ReadReal(f);
			m.n := 1;
			NEW(m.first); m.last := m.first;
			m.first.f[0] := f; m.first.n := 1;
			t0 := 0;
			FOR i := 1 TO n-1 DO
				rd.ReadInt(t); rd.ReadReal(f);
				m.AddSample(t - t0, f);
				t0 := t
			END
		END
	END Internalize;
 
	PROCEDURE (m: StdModel) CopyFrom (source: Stores.Store);
		VAR p: Page; i: INTEGER;
	BEGIN
		WITH source: StdModel DO
			p := source.first;
			NEW(m.first); m.last := m.first;
			WHILE p # NIL DO
				FOR i := 0 TO p.n-1 DO
 
					m.last.t[i] := p.t[i]; m.last.f[i] := p.f[i]
				END;
				m.last.n := p.n;
				p := p.next;
				IF p # NIL THEN
					NEW(m.last.next); m.last := m.last.next
				END
			END;
			m.n := source.n
		END
	END CopyFrom;
 
	(* StdReader *)
 
	PROCEDURE (rd: StdReader) Base (): Model;
	BEGIN
		RETURN rd.base
	END Base;
 
	PROCEDURE LookAtPage (p: Page; t: INTEGER): INTEGER;
		VAR l, r, c: INTEGER;
	BEGIN
		(* Pre p.t[0] <= t <= p.t[p.n-1] *)
		l := 0; r := p.n-1;
		WHILE l < r-1 DO
			c := (r+l) DIV 2;
			IF p.t[c] <= t THEN l := c
			ELSE (* t < p.t[c] *) r := c END
		END;
		(* Post
			(p.n > 1) & (l = r-1) & (p.t[l] <= t) & (t < p.t[r])
			OR (p.n = 1) & (l = r) & (p.t[l] <= t)
		*)
		RETURN l
	END LookAtPage;
 
	PROCEDURE ExtractIt (rd: StdReader);
	BEGIN
		rd.t1 := rd.page.t[rd.n];
		rd.f1 := rd.page.f[rd.n];
		IF rd.n + 1 < rd.page.n THEN
			rd.t2 := rd.page.t[rd.n+1];
			rd.f2 := rd.page.f[rd.n+1]
		ELSIF rd.page.next # NIL THEN
			rd.t2 := rd.page.next.t[0];
			rd.f2 := rd.page.next.f[0]
		ELSE
			rd.t2 := rd.t1;
			rd.f2 := rd.f1
		END
	END ExtractIt;
 
	PROCEDURE (rd: StdReader) SetPos (t: INTEGER);
		VAR p1, p2: Page;
	BEGIN
		ASSERT((t >= 0), 20);
		p2 := rd.base.first;
		WHILE (p2 # NIL) & (p2.t[p2.n-1] < t) DO
			p1 := p2; p2 := p2.next
		END;
		(* Post:
			(p1 = NIL) OR (p1.t[last] < t),
			(p2 = NIL) OR (t <= p2.t[last]),
			(p1 # NIL) OR (p2 # NIL)
			==>
			Post:
			(p1 = NIL) & (p2 # NIL) & (t <= p2.t[last])
			OR (p1 # NIL) & (p2 # NIL) & (p1.t[last] < t) & (t <= p2.t[last])
			OR (p1 # NIL) & (p2 = NIL) & (p1.t[last] < t)
		*)
		IF p1 = NIL THEN
			rd.page := p2;
			rd.n := LookAtPage(p2, t);
		ELSIF p2 # NIL THEN
			IF t >= p2.t[p2.n-1] THEN
				rd.page := p2;
				rd.n := LookAtPage(p2, t)
			ELSE
				rd.page := p1;
				rd.n := pageSize-1
			END
		ELSE
			rd.page := p1;
			rd.n := pageSize
		END;
		ExtractIt(rd)
	END SetPos;
 
	PROCEDURE (rd: StdReader) Read;
	BEGIN
		IF rd.page # NIL THEN
			IF rd.n + 1 < rd.page.n THEN
				INC(rd.n)
			ELSIF rd.page.next # NIL THEN
				rd.page := rd.page.next;
				rd.n := 0
			END;
		ELSE
			rd.page := rd.base.first; rd.n := 0
		END;
		ExtractIt(rd)
	END Read;
 
	PROCEDURE (rd: StdReader) ReadPrev;
	BEGIN
		IF rd.page # NIL THEN
			IF rd.n > 0 THEN
				DEC(rd.n)
			ELSIF rd.page.prev # NIL THEN
				rd.page := rd.page.prev;
				rd.n := pageSize
			END;
		ELSE
			rd.page := rd.base.first; rd.n := 0
		END;
		ExtractIt(rd)
	END ReadPrev;
 
	(* StdDirectory *)
 
	PROCEDURE (d: StdDirectory) New (f0: REAL): StdModel;
		VAR m: StdModel;
	BEGIN
		NEW(m);
		m.first := NewPage(0, 0, f0); m.last := m.first;
		m.n := 1;
		RETURN m
	END New;
 
	PROCEDURE SetDir* (d: Directory);
	BEGIN
		ASSERT(d # NIL, 20);
		dir := d
	END SetDir;
 
	(* Интерактивное получение данных *)
 
	PROCEDURE OpenSensor (s: Sensor);
		VAR val: RECORD (Meta.Value)
					proc: SensorProc
				END;
				ok: BOOLEAN;
	BEGIN
		s.adr := NIL;
		Meta.LookupPath(s.proc, s.it);
		IF s.it.Valid() & (s.it.obj = Meta.procObj) THEN
			s.it.GetVal(val, ok);
			IF ok THEN s.adr := val.proc END
		END
	END OpenSensor;
 
	PROCEDURE (s: Sensor) Do;
		VAR f: REAL; t: LONGINT;
	BEGIN
		IF ~s.it.Valid() THEN (* модуль был выгружен *)
			OpenSensor(s)				
		END;
		IF s.adr # NIL THEN
			f := s.adr(s.num);
			t := Services.Ticks();
			s.m.AddSample(MAX(1, SHORT(ENTIER((t - s.lastT) * s.tRatio))), f * s.fRatio);
			s.lastT := t
		END;
		Services.DoLater(s, t + s.interval)
	END Do;
 
	PROCEDURE Connect* (IN sensorProc: ARRAY OF CHAR; sensorNum: INTEGER;
buffer: Model; interval: INTEGER; tRatio, fRatio: REAL);
		VAR s: Sensor;
	BEGIN
		ASSERT(buffer.sensor = NIL, 20);
		NEW(s);
		s.m := buffer; buffer.sensor := s;
		s.proc := sensorProc$;
		s.num := sensorNum;
		s.interval := interval;
		s.tRatio := tRatio; s.fRatio := fRatio;
		s.lastT := Services.Ticks();
		OpenSensor(s);
		IF s.adr # NIL THEN
			Services.DoLater(s, Services.now)
		END
	END Connect;
 
	PROCEDURE Disconnect* (buffer: Model);
	BEGIN
		ASSERT(buffer.sensor # NIL, 20);
		Services.RemoveAction(buffer.sensor);
		buffer.sensor := NIL
	END Disconnect;
 
	PROCEDURE Init;
		VAR d: StdDirectory;
	BEGIN
		NEW(d); stdDir := d; dir := d
	END Init;
 
BEGIN
	Init
END MyHistoryModels.

Тестируем нашу модель

Здоровое стремление - как можно раньше начать компилировать, а затем и выполнять своё детище. Однако эффект это приносит вкупе с методом пошаговой детализации и уточнения (см. Никлаус Вирт "Систематическое программирование"), при разработке «методом тыка» ничего хорошего не получается (см. Информатика-21 О дисциплине программирования). Подход итеративной разработки с коротким циклом между рабочими версиями используется в таких известных сегодня процессах разработки ПО, как Extremal Programming и в других, так называемых «гибких подходах».

Напишем модуль MyHistoryTest:

MODULE MyHistoryTest;
 
	IMPORT HistoryModels := MyHistoryModels, ObxRandom;
 
	VAR
		m: HistoryModels.Model;
 
	PROCEDURE Sensor* (num: INTEGER): REAL;
	BEGIN
		RETURN -1 + ObxRandom.Uniform() * 2
	END Sensor;
 
	PROCEDURE Start*;
	BEGIN
		HistoryModels.Connect("MyHistoryTest.Sensor", 0, m, 10, 1, 1)
	END Start;
 
	PROCEDURE Stop*;
	BEGIN
		HistoryModels.Disconnect(m)
	END Stop;
 
BEGIN
	m := HistoryModels.dir.New(0)
END MyHistoryTest.
 
^Q MyHistoryTest.Start
^Q MyHistoryTest.Stop

Пробуем выполнять сбор данных. Как пронаблюдать, что он действительно работает? Выделяем имя модуля MyHistoryTest и выполняем команду меню Блэкбокса Info→Global Variables, затем навигируемся по структуре данных нашей модели, убеждаясь, что всё идёт как надо. Для тестирования можно переставить константу MyHistoryModels.pageSize значением поменьше - чтобы не так долго ждать перескока страницы данных.

Разработка отображения

Разработка отображения является несложным делом. Как обычно, продумаем интерфейс - абстрактный тип MyHistoryViews.View. Закладываем только самое важное (без чего «жизнь не мила»). Не забываем, что любые самые бурные фантазии те, кому они будут реально нужны, смогут реализовать отдельно в своих модулях - и преспокойно подключить их к нашей модели. Или даже использовать внутри себя (по схеме «обёртки», с делегированием) наше отображение, наслоив поверх него дополнительные функции.

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

В итоге получаем следующий интерфейс:

DEFINITION MyHistoryViews;
 
	IMPORT Views, MyHistoryModels;
 
	TYPE
		View = POINTER TO ABSTRACT RECORD (Views.View(Stores.Store))
			(v: View) GetScale (OUT unitsIn1t, unitsIn1f: INTEGER), NEW, ABSTRACT;
			(v: View) SetScale (unitsIn1t, unitsIn1f: INTEGER), NEW, ABSTRACT
		END;
 
		Directory = POINTER TO ABSTRACT RECORD 
			(d: Directory) New (m: MyHistoryModels.Model): View, NEW, ABSTRACT
		END;
 
	VAR
		dir-: Directory;
		stdDir-: Directory;
 
	PROCEDURE SetDir (d: Directory);
 
END MyHistoryViews.

Масштаб указывается в универсальных единицах среды (см. документацию модуля Ports, 1 mm = 36000 universal units) на 1 единицу времени (вертикаль) или измеренной величины (горизонталь).

Ниже приведена реализация отображения со всей нужной функциональностью. Пояснения:

  • Прокрутка. Полосы прокрутки в среде поддерживаются автоматически окнами, в которых открыты документы либо, если нужна прокрутка вложенного куда-то отображения, используются стандартные блоки прокрутки (StdScrollers, команда меню Tools→Add Scroller). Отображение может поддерживать прокрутку двумя способами. Наиболее простой - просто становиться большого размера. Ничего больше делать не нужно, полосы прокрутки появятся сами. Но это годится только для не слишком больших отображений (две-три страницы максимум). Отображение, работающее с объёмными данными, должно явно знать размер страницы прокрутки, позицию показываемых в настоящий момент данных и общую их длину. Эту информацию отображение сообщает среде, обрабатывая в процедуре HandleCtrlMsg сообщение Controllers.PollSectionMsg. Полосы прокрутки, кто бы их не обеспечивал, сами покажут текущее состояние. Чтобы обработать события прокрутки, изменяя своё состояние, отображение должно обработать сообщение Controllers.ScrollMsg. И следует не забыть сообщить среде, что отображение приспосабливается по размеру к предоставленному ему окну, обработав в процедуре HandlePropMsg сообщение Properties.ResizePref.
  • В процедуре HandleModelMsg обрабатывается сообщение об изменениях в модели - для своевременной перерисовки.
MODULE MyHistoryViews;
 
	IMPORT HistoryModels := MyHistoryModels, Views, Stores, Ports, Controllers, Models, Properties, Fonts;
 
	CONST
		version = 0;
 
	TYPE
		View* = POINTER TO ABSTRACT RECORD (Views.View) END;
		Directory* = POINTER TO ABSTRACT RECORD END;
 
		StdView = POINTER TO RECORD (View)
			m: HistoryModels.Model;
			ut, uf: INTEGER;
			plotColor: INTEGER;
			rd: HistoryModels.Reader;
			topT: INTEGER
		END;
 
		StdDirectory = POINTER TO RECORD (Directory) END;
 
	VAR
		dir-, stdDir-: Directory;
 
	PROCEDURE (v: View) SetScale* (unitsIn1t, unitsIn1f: INTEGER), NEW, ABSTRACT;
	PROCEDURE (v: View) GetScale* (OUT unitsIn1t, unitsIn1f: INTEGER), NEW, ABSTRACT;
 
	PROCEDURE (d: Directory) New* (m: HistoryModels.Model): View, NEW, ABSTRACT;
 
	PROCEDURE InitV (v: StdView);
	BEGIN
		v.rd := v.m.NewReader()
	END InitV;
 
	PROCEDURE (v: StdView) SetScale (unitsIn1t, unitsIn1f: INTEGER);
	BEGIN
		ASSERT((unitsIn1t > 0) & (unitsIn1f > 0), 20);
		v.ut := unitsIn1t; v.uf := unitsIn1f;
		Views.Update(v, Views.keepFrames)
	END SetScale;
 
	PROCEDURE (v: StdView) GetScale (OUT unitsIn1t, unitsIn1f: INTEGER);
	BEGIN
		unitsIn1t := v.ut; unitsIn1f := v.uf;
	END GetScale;
 
	PROCEDURE (v: StdView) HandlePropMsg (VAR msg: Properties.Message);
		VAR stdProp: Properties.StdProp; prop: Properties.Property;
	BEGIN
		WITH msg: Properties.ResizePref DO
			msg.fixed := FALSE;
			msg.verFitToWin := TRUE
		| msg: Properties.PollMsg DO
			NEW(stdProp); stdProp.color.val := v.plotColor;
			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.plotColor := prop.color.val END
				ELSE
				END;
				prop := prop.next
			END;
			Views.Update(v, Views.keepFrames)
		ELSE
		END
	END HandlePropMsg;
 
	PROCEDURE DrawPlot (f: Views.Frame; w, h: INTEGER; v: StdView; topT, bottomT: INTEGER; color: INTEGER);
	BEGIN
		v.rd.SetPos(topT);
		REPEAT
			f.DrawLine(w DIV 2 + SHORT(ENTIER(v.rd.f1*v.uf)), (v.rd.t1 - topT) * v.ut,
				w DIV 2 + SHORT(ENTIER(v.rd.f2*v.uf)), (v.rd.t2 - topT) * v.ut, 0, color);
			v.rd.Read
		UNTIL (v.rd.t1 > bottomT) OR (v.rd.t1 = v.rd.t2)
	END DrawPlot;
 
	PROCEDURE DrawBack (f: Views.Frame; w, h: INTEGER);
		VAR font: Fonts.Font;
	BEGIN
		f.DrawLine(SHORT(ENTIER(w / 2)), 0, SHORT(ENTIER(w / 2)), h, 0, Ports.black);
		font := Fonts.dir.Default();
		f.DrawString(0, font.size, Ports.black, "Press + / - for vertical scaling", font);
		f.DrawString(0, 2*font.size, Ports.black, "or w / s for horizontal scaling.", font);
	END DrawBack;
 
	PROCEDURE (v: StdView) Restore (f: Views.Frame; l, t, r, b: INTEGER);
		VAR w, h, topT, bottomT, page: INTEGER;
	BEGIN
		v.context.GetSize(w, h); page := SHORT(ENTIER(h / v.ut));
		DrawBack(f, w, h);
		topT := v.topT;
		bottomT := v.topT + SHORT(ENTIER(h / v.ut));
		INC(topT, MAX(0, SHORT(ENTIER(page * t / h)-1)));
		DEC(bottomT, MAX(0, SHORT(ENTIER(page * (h - b) / h)-1)));
		DrawPlot(f, w, h, v, topT, bottomT, v.plotColor)
	END Restore;
 
	PROCEDURE (v: StdView) HandleCtrlMsg (f: Views.Frame; VAR msg: Controllers.Message; VAR focus: Views.View);
		VAR w, h, page: INTEGER;
	BEGIN
		v.context.GetSize(w, h);
		page := SHORT(ENTIER(h / v.ut));
		WITH msg: Controllers.PollSectionMsg DO
			IF msg.vertical THEN
				IF v.m.Length() > page THEN
					msg.wholeSize := v.m.Length();
					msg.partPos := v.topT;
					msg.partSize := SHORT(ENTIER(h / v.ut));
					msg.valid := TRUE;
				END;
				msg.done := TRUE
			END
		| msg: Controllers.ScrollMsg DO
			IF msg.vertical THEN
				CASE msg.op OF
            | Controllers.decLine: v.topT := MAX(0, v.topT - page DIV 10 -1)
            | Controllers.incLine: v.topT := MIN(v.topT + page DIV 10 + 1, v.m.Length())
            | Controllers.decPage:  v.topT := MAX(0, v.topT - page)
            | Controllers.incPage: v.topT := MIN(v.topT + page, v.m.Length())
            | Controllers.gotoPos: v.topT := MAX(0, MIN(msg.pos, v.m.Length()))
				END;
				msg.done := TRUE;
				Views.Update(v, Views.keepFrames)
			END;
		| msg: Controllers.EditMsg DO
			IF msg.op = Controllers.pasteChar THEN
				CASE msg.char OF
				| '+': v.SetScale(SHORT(ENTIER(v.ut * 1.5))+1, v.uf)
				| '-': v.SetScale(MAX(1, SHORT(ENTIER(v.ut / 1.5))), v.uf)
				| 'w', 'W': v.SetScale(v.ut, SHORT(ENTIER(v.uf * 1.5))+1)
				| 's', 'S': v.SetScale(v.ut, MAX(1, SHORT(ENTIER(v.uf / 1.5))))
				ELSE
				END;
				Views.Update(v, Views.keepFrames)
			END
		ELSE
		END
	END HandleCtrlMsg;
 
	PROCEDURE (v: StdView) HandleModelMsg (VAR msg: Models.Message);
	BEGIN
		WITH msg: Models.UpdateMsg DO
			Views.Update(v, Views.keepFrames)
		END
	END HandleModelMsg;
 
	PROCEDURE (v: StdView) Externalize (VAR wr: Stores.Writer);
	BEGIN
		wr.WriteVersion(version);
		wr.WriteStore(v.m);
		wr.WriteInt(v.ut); wr.WriteInt(v.uf)		
	END Externalize;
 
	PROCEDURE (v: StdView) Internalize (VAR rd: Stores.Reader);
		VAR ver: INTEGER; m: Stores.Store;
	BEGIN
		rd.ReadVersion(0, version, ver);
		IF ~rd.cancelled THEN
			rd.ReadStore(m);
			IF ~(m IS HistoryModels.Model) THEN
				rd.TurnIntoAlien(Stores.alienComponent);
				RETURN
			END;
			v.m := m(HistoryModels.Model);
			rd.ReadInt(v.ut); rd.ReadInt(v.uf);
			InitV(v)
		END
	END Internalize;
 
	PROCEDURE (v: StdView) ThisModel (): HistoryModels.Model;
	BEGIN
		RETURN v.m
	END ThisModel;
 
	PROCEDURE (v: StdView) CopyFromModelView (source: Views.View; model: Models.Model);
	BEGIN
		WITH source: StdView DO
			v.m := model(HistoryModels.Model);
			v.ut := source.ut; v.uf := source.uf;
			v.plotColor := source.plotColor;
			InitV(v);
			v.topT := source.topT
		END
	END CopyFromModelView;
 
	PROCEDURE (d: StdDirectory) New (m: HistoryModels.Model): StdView;
		VAR v: StdView;
	BEGIN
		ASSERT(m # NIL, 20);
		NEW(v); v.m := m;
		Stores.Join(v, m);
		v.ut := 360;
		v.uf := 50 * 36000;
		v.plotColor := Ports.red;
		InitV(v);
		RETURN v
	END New;
 
	PROCEDURE SetDir* (d: Directory);
	BEGIN
		ASSERT(d # NIL, 20);
		dir := d
	END SetDir;
 
	PROCEDURE Init;
		VAR d: StdDirectory;
	BEGIN
		NEW(d);
		stdDir := d; dir := d
	END Init;
 
BEGIN
	Init
END MyHistoryViews.

Общее испытание

Окончательно модуль MyHistoryTest выглядит следующим образом:

MODULE MyHistoryTest;
 
	IMPORT HistoryModels := MyHistoryModels, ObxRandom, HistoryViews := MyHistoryViews, Views, StdCmds;
 
	VAR
		m: HistoryModels.Model;
 
	PROCEDURE Sensor* (num: INTEGER): REAL;
	BEGIN
		RETURN -1 + ObxRandom.Uniform() * 2
	END Sensor;
 
	PROCEDURE Start*;
	BEGIN
		HistoryModels.Connect("MyHistoryTest.Sensor", 0, m, 10, 1, 1)
	END Start;
 
	PROCEDURE Stop*;
	BEGIN
		HistoryModels.Disconnect(m)
	END Stop;
 
	PROCEDURE Open*;
		VAR v: HistoryViews.View;
	BEGIN
		v := HistoryViews.dir.New(m);
		Views.Deposit(v); StdCmds.Open
	END Open;
 
BEGIN
	m := HistoryModels.dir.New(0)
END MyHistoryTest.
 
^Q MyHistoryTest.Start
^Q MyHistoryTest.Stop
^Q MyHistoryTest.Open

Запустите сбор данных и откройте отображение - вы увидите бегущий график. Клавишами +/- можно изменять масштаб по вертикали, клавишами w / s - по горизонтали. Чтобы изменить цвет графика, нужно выделить отображение как монолит (см. Руководства пользователя в документации среды) - например, Edit→Select Document (Ctrl+Space), а затем через меню Attributes задать цвет.

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

А вот выделив нашу вьюшку (см. на абзац выше) и нажав Ctrl-C (Правка→Копировать), мы выполним полное копирование её вместе с моделью (среда вызовет тот же метод, но предварительно выполнит копирование модели). Затем вьюшку можно вставить куда угодно - в текст, на форму и т.п. - при этом обернув блоком прокрутки (Tools→Add Scroller).

Ну и, конечно, можно сохранять и загружать наш график - как отдельным документом, так и вложенным в другие документы.

© 2005-2024 OberonCore и коллектив авторов.