When Amiga was released in 1985 the availability of two interfaces to interact with the computer was quite a novel feature. Amiga's can be operated via command line interface and graphical user interface. For the latter free movable windows are very important. In this tutorial we will open a window using AmigaOS routines and perform some simple drawing. Along the way we have to deal with Tags, variable parameter, messaging, drawing principles and intuition structures.
The part of AmigaOS that handles Windows, Mouse and/or Keyboard input is named Intuition and is accessible by the developer through intuition.library and can be combined with use of graphics.library for drawing routines. There are more things involved but these two libraries are the most important. Both libraries with their functions can be accessed in Free Pascal with the units named intuition and agraphics. The latter had to be named AGraphics because Free Pascal itself already has a (generic) unit named graphics and which is available in the Lazarus component library. To prevent issues due to name collision we decided to change the name for Amiga platforms to agraphics. Unfortunately we have to deal with such name collisions for some of the units/libraries and also applies to some of the declared structures. A list with names that had to be changed can be found at our Specifics wiki Page.
Some really cool innovations that the Amiga offers are hidden from the average user. The Taglist feature is such an innovation. Imagine you want to design a function that opens a window and which requires a lot of parameters in order to accomplish this. F.e. x and y coordinates, width, height, the type of window and even more if wanted. If you want to put them all into the parameter list of a declaration of a function or procedure then the list of parameters will become significant in length quickly and because of that such a lengthy list becomes very tedious to use. Usually such long lists of parameters can be avoided by using a record instead so that you would only have to supply a pointer to this record and which could then contain all parameters and other relevant information. But, after years of developing you'll notice that you need to add more and more parameters simply because more and more (powerful) features are added to their parameter list in order to be able to make use of this newly added window functionality.
One way to solve that without destroying compatibility to old programs is, to create a new function or record which includes these new fields as well. MS-Windows for example did that very often. You will find many functions/records with an additional suffix "Ex" which is the extended version that is able to support new properties.
Amiga took a different path: Tags. You supply a list of Tags and Value pairs to the function. Every supplied Tag is just a simple integer number and corresponds to a single property of the window. Just as an example (a fictional example though, let's assume we have the following TAG-value-pairs: Left = 1, Top = 2, Width = 3, Height = 4). If we also assume that you ignore all Tags that you don't know about and by default fill all other (unknown) properties which are not provided with some meaningful default values then this offers a rather ingenious and clever way of being able to add new tag properties to such a tag-list without actually breaking either backward or forward compatibility.
A taglist could look like this (this is actually part of a real taglist that is used to open a window):
As can been seen from the table above, there are different types (integer, string and boolean) that can be assigned as a Tag-value although the Tag's property name itself dictates which type that should actually be. But, in order to keep things simple we should be able to transfer all these different types using the same interface. There is a very simple but rather clever way that can solve this problem for us and that is by declaring tag values to be of type "PtrUInt" (or "NativeUInt", which is the same). This type is an unsigned integer value which is guaranteed to have enough room in order to even store a pointer. This way we are able to supply all our Tag-Values by simply casting them to PtrUInt. For example strings can be converted this way as a PChar (zero terminated C like strings) and would then be treated as any other common Pointer. Instead of casting (which is a rather tedious thing to do) there is also another solution available and that is offered by unit Utility, that offers a special set of wrapper functions named AsTag(). AsTag() will automatically convert all possible types into a PtrUint. You can find them in the utility unit. Another notable feature of such tag-lists is that the list itself always needs to end with a special closing tag named TAG_END. That let every other function that work with tag-lists know where the actual end of the list is located. The TAG_END-Tag does not really require an actual tag-value. Since its common for a picture to be able to tell more than a thousand words, adding a list of tag-value pairs to a list looks something like:
Free Pascal is smart enough to create the string for WA_Title as type PChar. Be aware that this is not possible if the string is calculated at this point. Besides that, we also have to take into account whether or not the PChar that is going to be supplied to the function is copied or not (but more on that later).
Now, with all this gained knowledge let's try to open a window om our workbench. The Amiga Api offers quite some different functions that open a window:
- OpenWindow() - This is an older declaration/interface that was used before taglists where invented but is still available for compatibility reasons.
- OpenWindowTagList() - This function requires a link to a tag-list for the second parameter. You create the tag-list before making the actual call to the function. This is very convenient in case you wish to manipulate the tag-list beforehand or programmaticaly.
- OpenWindowTags() - This function has a
array of PtrUIntas second parameter which can be used to directly supply a list of tags and corresponding tag-values to the function.
For example a simple way to create a window (with the given parameter) will be:
OpenWindowTags(nil, [ WA_Width, AsTag(300), WA_Height, AsTag(200), WA_Title, AsTag('My Window'), WA_DepthGadget, AsTag(True), WA_SizeGadget, AsTag(True), TAG_END ])
Of course we need to close the Window again, with CloseWindow() which needs the pointer to the window we created. This is returned by the OpenWindow call and has the type PWindow so we need to put that into a variable.
program window; uses utility, intuition; var win: PWindow; begin win := OpenWindowTags(nil, [ WA_Width, AsTag(300), WA_Height, AsTag(200), WA_Title, AsTag('My Window'), WA_DepthGadget, AsTag(True), WA_SizeGadget, AsTag(True), TAG_END ]); CloseWindow(win); end.
This program will open a window and close it directly again. We can introduce a
Sleep(1000) to at least see the window. Next step is to keep the window open until the user pressed the close gadget, as you are used to it. For that we have to react to the event of clicking the gadget.
currently the window does not have a close gadget, we need to activate that on the openwindow call with this flag:
WA_Flags, WFLG_CloseGadget. Flags are parameter for the window. If you need several flags at once, just or them together.
To have the close gadget is not enough we also have to activate the event, we supply an additional tag
WA_IDCMP, IDCMP_CLOSEWINDOW which tells the system that we want to be notified when the window gets closed. To get this event message we have to check the message port of the window. The message port is something like a post box where events about that window arrive. You can use the function GetMsg() from the exec unit. Now we need the message port of the window which is a field of the window we created called
win^.UserPort. First we have to wait until a message appears using WaitPort(), it blocks the execution until a message appears in the message port and gives other tasks a chance to do something. Which is much better than asking the port as fast as possible until a message appears. After we get the message we can inspect the contents of it, if we finished with this message we reply to the sender that we processed the message an we do not need it anymore. The call to do that is ReplyMsg(). Do not access the message after this call usually the sender of the message will free the memory of it. The usual flow will be:
repeat WaitPort(Win^.UserPort); Msg := GetMsg(Win^.UserPort); // Process Msg ReplyMsg(Msg); until done;
Because we only called for a single message if a message arrive it will be our message, but because we later want to react on more messages we have to process the message first. Because that message is send to window by intuition it's type is not just PMessage but PIntuiMessage which has the field
Msg^.IClass which defines the type of message received. If that field is
IDCMP_CLOSEWINDOW that message is a close window event. putting all together it looks like that:
program window; uses exec, sysutils, utility, intuition; var win: PWindow; done: Boolean = False; Msg: PMessage; begin win := OpenWindowTags(nil, [ WA_Width, AsTag(300), WA_Height, AsTag(200), WA_Title, AsTag('My Window'), WA_DepthGadget, AsTag(True), WA_SizeGadget, AsTag(True), WA_Flags, WFLG_CloseGadget, WA_IDCMP, IDCMP_CLOSEWINDOW, TAG_END ]); repeat WaitPort(Win^.UserPort); Msg := GetMsg(Win^.UserPort); case PIntuiMessage(Msg)^.IClass of IDCMP_CloseWindow: Done := True; end; ReplyMsg(Msg); until done; CloseWindow(win); end.