Ермаков И. Е. График истории показаний / 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).
Ну и, конечно, можно сохранять и загружать наш график - как отдельным документом, так и вложенным в другие документы.