Oberon/ETH Oberon/Tutorial/GadgetsProg
这些教程页面由 André Fischer (afi) 编写,在 Hannes Marais 的编辑协助下,托管在 ETHZ,并保留在 ETH 许可 下。相关内容可在系统中通过 Book.Tool 找到。扩展内容也可在 纸质上 获得。一些教程页面位于 WayBack 存档 中。
提供足够的关于工具编程的信息,使您能够使用新的令人兴奋的应用程序扩展 Oberon 系统 3。
估计时间:90 分钟。
工具系统建立在基本的 Oberon 系统 3 版本之上,通过添加用于编程用户界面的特殊模块和约定来实现。从本质上讲,它引入了称为工具的新型对象。工具是用户可以在运行时组合以构建用户界面的用户界面元素。它们遵守严格的协议,允许它们嵌入许多不同的应用程序中。核心模块称为工具,它提供了系统的基类。工具 模块依赖于提供剪切操作的进一步模块(Display3 和 Printer3),一个管理属性的模块(Attributes),以及一个用于特殊效果的模块(Effects)。在这些模块之上,存在一个层次结构的模块,每个模块实现一个新的工具类型。其中许多模块是 Oberon 系统 3 的标准配置。
学习如何编程工具的最佳方法是阅读简单但功能完备的示例的源代码。
- Skeleton.Mod 是如何编程 可视化工具 的示例。它实现了一个可以移动、调整大小、复制、打印和着色的彩色小块。
- Complex.Mod 是如何编程 模型工具 的示例。它实现了一个用于复数的模型工具。
- DocumentSkeleton.Mod 是如何编程 文档工具 的示例。它实现了一个文档,该文档包含一个 面板,并且仅存储其颜色。
这三个示例中的每一个都可以用作创建新的、自定义的和面向应用程序的工具类型的基础:可视化工具、模型工具和文档工具。
编程新的工具时,您将需要以下内容
1 - 新工具的新类型,通常通过扩展现有的“基”类型来创建。以下是一个用于扩展类型声明的骨架
TYPE MyGadget* = POINTER TO MyGadgetDesc; MyGadgetDesc* = RECORD (BaseType) (* additional (private) fields *) END;
基类型可能是例如
- Gadgets.FrameDesc 用于可视化工具
- Gadgets.ObjDesc 用于模型工具
- Documents.DocumentDesc 用于文档工具。
扩展现有工具时,该工具的记录类型将用作基类型。为了确保工具可以扩展,记录和指针类型都应导出。
2 - 消息处理程序.
3 - 新过程.
创建工具的新实例与 Oberon 系统中的其他所有操作一样,都是通过命令完成的。模块 M 包含一个过程 P,其任务是动态分配某个对象类型的新实例。这称为对象的 New 过程。执行 New 过程 M.P(这通常称为生成器字符串)会导致创建该对象类型的新实例。新的对象实例将初始化为默认状态,并准备好接受消息(即它完全功能齐全)。
以下是一个典型的新过程
PROCEDURE New*; VAR F: MyGadget; BEGIN NEW(F); (* assign message handler *) F.handle := MyHandler; (* initialize private and inherited fields of F, e.g. F.W, F.H for a visual gadget*) ... (* "export" the newly created gadget *) Objects.NewObj := F END New;
Handler 是一个标准的 Oberon 消息处理程序类型,用于类 Object 和消息基本类型 ObjMsg(参见 Objects)
Handler = PROCEDURE (obj: Objects.Object; VAR M: Objects.ObjMsg);
在现实的对象面向环境中,消息很少由第一个接收方完全处理。通常,它们会通过一个复杂的对象网络传递。因此,给定工具的处理程序只处理应该与基类型中处理方式不同的消息。它将所有其他消息传递给基类型的处理程序(例如,可视化工具的 Gadgets.framehandle)。
工具中有两个重要的消息类
- 来自 Display.FrameMsg 的消息:Display 模块中的帧消息在帧间通信中发挥着核心作用。这些构建了通信协议,允许帧彼此通信,而无需了解彼此的内部工作。后者对于将外国或未知对象集成到系统中以及应用程序需要彼此交换对象至关重要。FrameMsg 的定义如下:
FrameMsg = RECORD (Objects.ObjMsg) F: Frame; (* target frame *) x, y, res: INTEGER END;
F 在 FrameMsg 中起着核心作用。它确定消息的目标或目标帧。通常,消息的目标帧是未知的。例如,当模型更新消息被广播时,就会发生这种情况,在这种情况下,F 字段将被设置为 NIL。
- 不来自 Display.FrameMsg 的消息:这些消息通常可以直接发送到接收对象,方法是调用其处理程序(obj.handle(obj, msg))。例如,Objects.AttrMsg
一个典型消息处理程序如下所示
PROCEDURE MyHandler*(F: Objects.Object; VAR M: Objects.ObjMsg); BEGIN WITH F: MyGadget DO IF M IS Display.FrameMsg THEN (* only for visual gadgets - not for model gadgets *) WITH M: Display.FrameMsg DO IF (M.F = NIL) OR (M.F = F) THEN (* handle messages derived from Display.FrameMsg here: Display.DisplayMsg, Display.ModifyMsg, Display.PrintMsg, Display.SelectMsg, Display.ConsumMsg, Oberon.InputMsg, Oberon.ControlMsg, ... *) END END ELSIF Objects.AttrMsg THEN (* get, set and enumerate attributes *) ELSIF Objects.FileMsg THEN (* load and store of the gadget *) ELSIF Objects.CopyMsg THEN (* making a copy of the gadget *) ELSE (* unknown msg, framehandler might know it *) Gadgets.framehandle(F, M) END END END MyHandler;
备注
- 当消息只被部分处理或根本没有被处理时,应该调用基类型的处理程序。
- 为了确保工具可以稍后扩展,FrameHandler 应该导出。
- 模型工具应忽略 Display.FrameMsg 系列的消息。
来自 Display.FrameMsg 的消息
[edit | edit source]Display.DisplayMsg
[edit | edit source]DisplayMsg 向单个或所有框架广播重绘请求。它被定义如下
DisplayMsg = RECORD (Display.FrameMsg) id: INTEGER; (* frame, area *) u, v, w, h: INTEGER END;
当目标 (F) 为 NIL 时,表示所有框架。当 id 设置为 Display.area 时,应重新绘制目标框架内的区域 u、v、w、h。这些坐标相对于目标组件的左上角(因此 v 通常为负)。
一个特殊的显示掩码数据结构 (Display3.Mask) 用于指示组件的哪些区域可见。它被指定为一组不重叠的矩形。绘图原语通过此掩码发出,其效果是将它们剪切到组件的可见区域。
因此,处理 Display.DisplayMsg 可能如下所示
IF (M.F = NIL) OR (M.F = F) THEN (* message addressed to this frame *) (* calculate display coordinates *) x := M.x + F.X; y := M.y + F.Y; w := F.W; h := F.H; IF M IS Display.DisplayMsg THEN WITH M: Display.DisplayMsg DO IF (M.id = Display.frame) OR (M.F = NIL) THEN Gadgets.MakeMask(F, x, y, M.dlink, R); RestoreFrame(F, R, x, y, w, h) ELSIF M.id = Display.area THEN Gadgets.MakeMask(F, x, y, M.dlink, R); Display3.AdjustMask(R, x + M.u, y + h - 1 + M.v, M.w, M.h); RestoreFrame(F, R, x, y, w, h) END END ELSIF ...
备注
- 组件通常是矩形的,其大小由 F.W 和 F.H 描述。x、y 是矩形左下角的坐标。
- 通常使用 Display3 模块的绘图例程来绘制组件。
Display.PrintMsg
[edit | edit source]这是一个请求框架打印自身的请求。它被定义如下
PrintMsg = RECORD (Display.FrameMsg) id: INTEGER; (* contents, view *) pageno: INTEGER END;
当目标为 NIL 时,表示整个组件树。当 id 设置为 view 时,框架必须以它在显示屏上显示的形式打印自身。当 id 设置为 contents 时,它应该打印其完整内容(例如它可能显示的文本)。按照惯例,x、y 坐标指示框架左下角的绝对打印机坐标。框架可以假设打印机驱动程序已初始化。
打印也可以使用剪切掩码完成。所有可用于显示掩码 (Display3) 的原语,也可用于打印 (Printer3)。一个主要区别是打印掩码使用打印机坐标存储。与显示掩码一样,提供了一个特殊的例程来计算组件的打印掩码 (Gadgets.MakePrinterMask)。
Oberon.InputMsg
[edit | edit source]此消息将鼠标和键盘输入发送到框架。它被定义如下
InputMsg = RECORD (Display.FrameMsg) id: INTEGER; (* track, consume *) keys: SET; X, Y: INTEGER; ch: CHAR; fnt: Fonts.Font; col, voff: SHORTINT END;
跟踪鼠标
[edit | edit source]当 Oberon 事件循环检测到鼠标移动或鼠标按钮被按下时,它会向受影响的查看器发送一个跟踪消息 (id = Oberon.track)。组件可以在收到 track message 时执行任何操作。但是,如果可能,它应该遵守 Oberon 约定。
通常,组件有一个控制边界,组件在其中响应鼠标组合以进行调整大小、移动、删除和复制。这些鼠标组合由 Gadgets.framehandle 处理,因此鼠标只需要在组件的工作区内进行跟踪。Gadgets.InActiveArea 检查鼠标是否在工作区内。
鼠标点击通常记录在跟踪循环中。在这个循环中,鼠标驱动程序被直接读取,并记录点击。当所有三个按钮都恢复向上时,循环终止。
因此,鼠标跟踪的编程方式可能如下
PROCEDURE MyHandler*(F: Objects.Object; VAR M: Objects.ObjMsg); ... ELSIF M IS Oberon.InputMsg THEN WITH M: Oberon.InputMsg DO IF (M.id = Oberon.Track) & Gadgets.InActiveArea(F, M) THEN TrackMouse(F, M.X, M.Y, M.keys) ... END MyHandler; PROCEDURE TrackMouse(F: MyGadget; VAR X, Y: INTEGER; VAR keysum: SET); VAR keys: SET; BEGIN keys := keysum; WHILE keys # {} DO Effects.TrackMouse(keys, X, Y, Effects.Arrow); keysum := keysum+keys END; IF keysum = Effects.middle THEN (* execute F *) ELSIF ... END TrackMouse;
编程插入符
[edit | edit source]当按下键盘键时,会广播一个 consume message (id = Oberon.consume)。但是,由于 Oberon 事件循环不知道 插入符 当前设置在哪个框架中,因此消息的接收者是未知的 (F = NIL)。只有包含插入符的框架应该使用该字符。
实现插入符的组件通常有一个 BOOLEAN 字段,指示插入符是否已设置。因此,MyGadgetDesc 的定义可能如下所示
MyGadgetDesc* = RECORD (Gadgets.Frame) caret: BOOLEAN; (* other data *) END
caret 字段在 New 过程 中初始化为 FALSE。然后可以按如下方式实现插入符的处理
PROCEDURE MyHandler*(F: Objects.Object; VAR M: Objects.ObjMsg); VAR x, y, w, h: INTEGER; BEGIN WITH F: MyGadget DO IF M IS Display.FrameMsg THEN (* Display.FrameMsg messages *) WITH M: Display.FrameMsg DO IF (M.F = NIL) OR (M.F = F) THEN (* calculate display coordinates *) x := M.x + F.X; y := M.y + F.Y; w := F.W; h := F.H; IF M IS Display.DisplayMsg THEN ... ELSIF M IS Oberon.InputMsg THEN WITH M: Oberon.InputMsg DO IF M.id = Oberon.track THEN IF (M.keys = {Effects.left}) & Gadgets.InActiveArea(F, M) THEN IF ~F.caret THEN Oberon.Defocus(); F.caret := TRUE END; SetCaret(F, x, y) ... END ELSIF (M.id = Oberon.consume) & F.caret THEN ConsumeChar(F, M.ch); M.res := 0 ... END END ELSIF M IS Oberon.ControlMsg THEN WITH M: Oberon.ControlMsg DO IF M.id IN {Oberon.defocus, Oberon.neutralize} THEN IF F.caret THEN F.caret := FALSE; RemoveCaret(F) END ... END END ... END END (* IF (M.F = NIL) OR (M.F = F) *) END (* WITH M: Display.FrameMsg *) (* other messages *) END END END MyHandler;
Oberon.ControlMsg
[edit | edit source]此消息更改组件的状态。它被定义如下
ControlMsg = RECORD (Display.FrameMsg) id: INTEGER; (* defocus, neutralize, mark *) X, Y: INTEGER END;
当目标 (F) 为 NIL 时,表示所有框架。当 id 设置为 Oberon.defocus 时,则组件应删除其插入符。如果 id 设置为 Oberon.neutralize,则组件应删除其包含的所有标记(插入符和选择)。有关如何使用此消息的示例,请参阅 编程插入符。
对象消息
[edit | edit source]Objects 模块的消息对所有组件都是通用的。
Objects.AttrMsg
[edit | edit source]在 Oberon 系统 3 中,对象 属性管理 是通过向对象发送 Objects.AttrMsg 消息来严格完成的。
通常,对于我们的案例研究示例,您会按如下方式处理这些消息
PROCEDURE MyHandler*(F: Objects.Object; VAR M: Objects.ObjMsg); ... ELSIF M IS Objects.AttrMsg THEN THEN WITH M: Objects.AttrMsg DO IF M.id = Objects.get THEN IF M.name = "Gen" THEN M.class := Objects.String; M.s := "MyGadget.New"); M.res := 0 ELSIF M.name = "Color" THEN M.class := Objects.Int; M.i := F.mycol; M.res := 0 ELSE Gadgets.framehandle(F, M) END ELSIF M.id = Objects.set THEN IF M.name = "Color" THEN IF M.class = Objects.Int THEN F.mycol := SHORT(M.i); M.res := 0 ELSIF M.class = Objects.String THEN (2a) Attributes.StrToInt(M.s, M.i); F.mycol := SHORT(M.i); M.res := 0 (* ELSE ignore *) (2b) END ELSE Gadgets.framehandle(F, M) END ELSIF M.id = Objects.enum THEN (3) M.Enum("Color"); Gadgets.framehandle(F, M) END END ... END MyHandler;
注释
该对象只能处理已添加到基类型中的属性。其他属性由基类型处理程序处理。
(1) id=Objects.get,返回命名属性的值。 每个对象至少应该处理“Gen”属性,即返回 New 过程 字符串。
(2) id=Objects.set,更改命名属性的值。
(3) id=Objects.enum,通过重复调用 M.Enum(扩展属性)来枚举每个属性。
Objects.FileMsg
[edit | edit source]FileMsg 消息的目的是从顺序文件加载和存储对象,以及将对象存储到顺序文件中。
FileMsg = RECORD (ObjMsg) id: INTEGER; (* id = load, store *) len: LONGINT; R: Files.Rider END;
通常,对于我们的案例研究示例,您会按如下方式处理这些消息
PROCEDURE MyHandler*(F: Objects.Object; VAR M: Objects.ObjMsg); ... ELSIF M IS Objects.FileMsg THEN WITH M: Objects.FileMsg DO IF M.id = Objects.store THEN (1) Files.WriteInt(M.R, F.mycol) ELSIF M.id = Objects.load THEN (2) Files.ReadInt(M.R, F.mycol) END; Gadgets.framehandle(F, M) END ... END MyHandler;
注释
该对象只能处理已添加到基类型中的属性。其他属性由基类型处理程序处理。
(1) id=Objects.load,请求对象将其数据存储到由骑手 M.R 指定的文件中。
(2) id=Objects.store,然后请求对象从由骑手 M.R 指定的文件中加载其数据。
为了使对象的加载和存储在不同的 Oberon 平台之间保持可移植性,请使用 Files 模块的程序,这些程序读取和写入不同的 Oberon 基本类型(例如 WriteInt、WriteString 等)。
Objects.CopyMsg
[edit | edit source]CopyMsg 类型的消息用于创建给定对象的精确副本。
CopyMsg = RECORD (ObjMsg) id: INTEGER; (* id = shallow | deep *) obj: Object END;
我们区分 浅 和 深 复制。当必须创建浅复制时,尽可能多地保留对原始组件的引用未解析,而在深复制的情况下,通过递归创建组件的副本来解析所有引用。请注意,在这两种情况下,复制消息至少会通过表示原始对象的部分完整数据结构。
Objects.CopyMsg
PROCEDURE MyHandler*(F: Objects.Object; VAR M: Objects.ObjMsg); VAR F1: Frame; ... ELSIF M IS Objects.CopyMsg THEN WITH M: Objects.CopyMsg DO IF M.stamp = F.stamp THEN M.obj := F.dlink (* Copy message arrives again *) ELSE (* First time copy message arrives *) NEW(F1); F.stamp := M.stamp; (1) F.dlink := F1; (* Copy private data *) F1.mycol := F.mycol; ... (* Copy data of base type *) Gadgets.CopyFrame(M, F, F1); M.obj := F1 END END ... END MyHandler;
注释:
(1) 同一个复制消息可能到达不止一次。因此,时间戳字段用于检测对象的副本是否已创建。
编程新的文档类型
[edit | edit source]加载和存储文档
[edit | edit source]文档不需要处理 Objects.FileMsg 类型的消息。文档的加载和存储由其基类型 (Documents.Document) 的两个过程变量字段 Load 和 Store 完成。因此,文档的 New 过程如下所示
PROCEDURE NewDoc*; VAR D: Documents.Document; BEGIN NEW(D); (* assign procedures *) D.Load := Load; D.Store := Store; D.handle := DocHandler; D.W := 250; D.H := 200; Objects.NewObj := D END NewDoc;
其中 Load 定义如下
PROCEDURE Load(D: Documents.Document); VAR obj: Objects.Object; tag, x, y, w, h: INTEGER; name: ARRAY 64 OF CHAR; F: Files.File; R: Files.Rider; BEGIN (* create a child gadget for the document *) obj := Gadgets.CreateObject("Panels.NewPanel"); WITH obj: Gadgets.Frame DO x := 0; y := 0; w := 250; h := 200; F := Files.Old(D.name); IF F # NIL THEN Files.Set(R, F, 0); Files.ReadInt(R, tag); IF tag = Documents.Id THEN Files.ReadString(R, name); Files.ReadInt(R, x); Files.ReadInt(R, y); Files.ReadInt(R, w); Files.ReadInt(R, h); (* read data specific to this document type *) ... ELSE (* not a document header, create an empty child (obj), D.name := <new doc> *) END ELSE (* create an empty child (obj), D.name := <new doc> *) END; D.X := x; D.Y := y; D.W := w; D.H := h; Documents.Init(D, obj) END END Load;
备注
- 所有文档文件都有一个标题,包含标签、名称、x、y、w 和 h。
- 子组件不需要是面板,任何组件都可以使用。
其中 Store 的定义如下:
PROCEDURE Store(D: Documents.Document); VAR obj: Gadgets.Frame; F: Files.File; R: Files.Rider; BEGIN (* get the child gadget *) obj := D.dsc(Gadgets.Frame); F := Files.New(D.name); Files.Set(R, F, 0); (* write the document header *) Files.WriteInt(R, Documents.Id); Files.WriteString(R, <gen string of this document type>); Files.WriteInt(R, D.X); Files.WriteInt(R, D.Y); Files.WriteInt(R, D.W); Files.WriteInt(R, D.H); (* write data specific to this document type *) ... Files.Register(F) END Store;
与所有其他小工具相比,文档具有三个额外的只读属性(参见 Objects.AttrMsg)
- 菜单:指定菜单栏内容的字符串属性。 此字符串的语法是:
menu = { command [ "[" caption "]" ] " " }. command = moduleName "." commandName. caption = string.
- 图标:指定图标的字符串属性,该图标在将文档使用 Desktops.MakeIcons * 缩放到图标时使用。 该字符串给出 Icons.Lib 中图片的完整名称。
- 自适应:布尔属性,指定文档在作为 Oberon 浏览器打开时是否应该动态改变其大小。
PROCEDURE DocHandler(D: Objects.Object; VAR M: Objects.ObjMsg);
BEGIN
WITH D: Documents.Document DO
IF M IS Objects.AttrMsg THEN
WITH M: Objects.AttrMsg DO
IF M.id = Objects.get THEN
IF M.name = "Gen" THEN
M.class := Objects.String;
M.s := <gen string of this document type>; M.res := 0
ELSIF M.name = "Adaptive" THEN
M.class := Objects.Bool; M.b := TRUE; M.res := 0
ELSIF M.name = "Icon" THEN
M.class := Objects.String; M.s := "Icons.Tool"; M.res := 0
ELSIF M.name = "Menu" THEN
M.class := Objects.String;
M.s := "Desktops.StoreDoc[Store]"; M.res := 0
ELSE Documents.Handler(D, M)
END
ELSE Documents.Handler(D, M)
END
END
...
ELSE Documents.Handler(D, M)
END
END
END DocHandler;
通常,没有必要显式处理 Display.DisplayMsg 和 Display.ModifyMsg 消息。 Documents.Handler 负责将这些消息委托给菜单栏和子小工具。 但是,如果例如文档的大小限制为最小或最大大小,则在调用 Documents.Handler 之前可以更改 Display.ModifyMsg 消息。
A
C
D
目标帧
Display.DisplayMsg
Display.FrameMsg
Display.PrintMsg
Display3.Mask
Documents.DocumentDesc
DocumentSkeleton.Mod
G
Gadgets.FrameDesc
Gadgets.ObjDesc
Gen 属性
H
I
K
L
M
N
O
Oberon.ControlMsg
Oberon.InputMsg
Objects.AttrMsg
Objects.CopyMsg
Objects.FileMsg
S
T
1996 年 7 月 23 日修订
1997 年 5 月 30 日安装