Tutorial:Window

From Freepascal Amiga wiki
Jump to: navigation, search

Start Next

About Windows

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.

Tags, Taglists

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):

Tag Value
WA_Width 300
WA_Height 200
WA_Title 'My Window'
WA_DepthGadget True
WA_SizeGadget True
WA_DragBar True

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:

Tag Value
WA_Width AsTag(300)
WA_Height AsTag(200)
WA_Title AsTag('My Window')
WA_DepthGadget AsTag(True)
WA_SizeGadget AsTag(True)
WA_DragBar AsTag(True)
TAG_END

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 PtrUInt as 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),
    WA_DragBar, 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),
    WA_DragBar, 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.

Events

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 OpenWindow() call: 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_DragBar, 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.

This way you get a window which behaves more or less like you would expect from a normal intuition window. Close bring to front/send to back and dragging works as expected but the size gadget do not work. The reason for this is that we did not set minimum and maximum sizes for the window to rescale to if we set the additional tags for the open window call:

    WA_MaxWidth, AsTag(640),
    WA_MaxHeight, AsTag(256),
    WA_MinWidth, AsTag(100),
    WA_MinHeight, AsTag(100),

An empty window is not very impressive so we draw some stuff into. Drawing is always done with the functions found in agraphics unit working on the rastport of the window, which is something like a drawing board. As start we clear the contents of the Window and then draw a text to it. Clearing the Contents of window is easy, just call SetRast() wit the rastport of the window and the Pen you want to use to clear (e.g. 0 = background color). To write a text we have to define the text color, text mode, text position and print the actual text. Basically you have 2 pens Pen A (= Foreground pen) and Pen B (= Background Pen), we only want to use a single color here because we cleared the background already so we only need to set pen A using SetAPen() (e.g. Pen 1 = black) now we set the modus of the text printing, we only want the A Pen used so we "jam" one color to the rastport, Draw mode = JAM1 (if we want to use foreground and background color = jam two colors DrawMode = JAM2) The call to do that is: SetDrMd(). At last we define a position in the window where we want to print the text (e.g. Pixel x=50, y=50) The call for that is called Move() in Amiga API but this names clashed with the move() function in the system unit (to copy memory areas), therefore this function is renamed gfxmove() (You can check the Specifics Page for a list of such renamed functions.) Everything is setup so we can print out the text using the Text() function which is renamed to GFXMove() with the same reason. We call this function directly after creation of the window.

program window;
uses
  exec, sysutils, utility, intuition, agraphics;

procedure Paint(AWin: PWindow);
var
  str: AnsiString;
begin
  SetRast(AWin^.RPort, 0);
  str := 'Hello World';
  SetAPen(AWin^.RPort, 1);
  SetDrMd(AWin^.RPort, JAM1);
  GFXMove(AWin^.RPort, 50, 50);
  GFXText(AWin^.RPort, PChar(str), Length(str));
end;
  
var
  win: PWindow;
  done: Boolean = False;
  Msg: PMessage;
begin 
  win := OpenWindowTags(nil, 
   [
    WA_Width, AsTag(300),
    WA_Height, AsTag(200),
    WA_MaxWidth, AsTag(640),
    WA_MaxHeight, AsTag(256),
    WA_MinWidth, AsTag(100),
    WA_MinHeight, AsTag(100),
    WA_Title, AsTag('My Window'),
    WA_DepthGadget, AsTag(True),
    WA_SizeGadget, AsTag(True),
    WA_DragBar, AsTag(True),
    WA_Flags, WFLG_CloseGadget,
    WA_IDCMP, IDCMP_CLOSEWINDOW,
    TAG_END  
   ]);
  Paint(Win); 
  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.

if we run this, we notice two things, first our drawing destroys the window decoration and if we make the window as small as possible and big again, part of the "Hello World" is gone. The first thing is easy to solve. We create a simple window, but with that the rastport we draw to is the complete window, including the window decoration, so we can either care about that on every drawing or we change to a "Zero Zero" Window which gives you a rastport without the window decoration. "Zero Zero" because the 0,0 point is the left top edge of the user drawable area and not the real 0,0 of the window. It also protects the window decoration from overdrawing. To set our window to a "Zero Zero" window we need to add a new Flag to the OpenWindow taglist:

  WA_Flags, WFLG_CloseGadget or WFLG_GIMMEZEROZERO,

Now the window looks like it should be, background and a black text in it, without decoration destroyed. When you resize the window (or move an other window in front of your window) it might be needed to refresh the drawing. Intution is able to send us a message when this is needed. The message is called IDCMP_REFRESHWINDOW with the same method as the Close window message we have to activate that message and process it, when it arrives. Activating be adding it to the WA_IDCMP tag of the openwindow tag list. And process it in the main loop calling the Paint() procedure again (thats the reason I created it as a procedure) all together it looks like that:

program window;
uses
  exec, sysutils, utility, intuition, agraphics;

procedure Paint(AWin: PWindow);
var
  str: AnsiString;
begin
  SetRast(AWin^.RPort, 0);
  str := 'Hello World';
  SetAPen(AWin^.RPort, 1);
  SetDrMd(AWin^.RPort, JAM1);
  GFXMove(AWin^.RPort, 50, 50);
  GFXText(AWin^.RPort, PChar(str), Length(str));
end;
  
var
  win: PWindow;
  done: Boolean = False;
  Msg: PMessage;
begin 
  win := OpenWindowTags(nil, 
   [
    WA_Width, AsTag(300),
    WA_Height, AsTag(200),
    WA_MaxWidth, AsTag(640),
    WA_MaxHeight, AsTag(256),
    WA_MinWidth, AsTag(100),
    WA_MinHeight, AsTag(100),
    WA_Title, AsTag('My Window'),
    WA_DepthGadget, AsTag(True),
    WA_SizeGadget, AsTag(True),
    WA_DragBar, AsTag(True),
    WA_Flags, WFLG_CloseGadget or WFLG_GIMMEZEROZERO,
    WA_IDCMP, IDCMP_CLOSEWINDOW or IDCMP_REFRESHWINDOW,
    TAG_END  
   ]);
  Paint(Win); 
  repeat
    WaitPort(Win^.UserPort);
    Msg := GetMsg(Win^.UserPort);
    case PIntuiMessage(Msg)^.IClass of
      IDCMP_CloseWindow: Done := True;
      IDCMP_REFRESHWINDOW: Paint(Win);
    end;
    ReplyMsg(Msg);
  until done;
  CloseWindow(win);
end.

And finally we have it, a fully functioning Amiga API window with a little text inside. Of course you can extend the Paint() procedure with more lines or rectangles, and you can use the GZZHeight and GZZWidth fields of the win to determine the actual size of your drawing area.

Start Next