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 日安裝