I am very much indebted to Steven Solie who was kind enough to answer all my questions and explain things I did not quite understand.
2. The Don'ts
It will be of greatest benefit to mention and rule out, right from the beginning, practices that are no longer recommended and that you should consider a no-no when writing new GUI code. Many of them are found in the various examples mentioned above (including Jim Tubbs' guide) and are therefore quite commonly used by programmers.
2.1 The Builder
From time to time, the question is asked in Amiga-related forums whether there is a GUI builder for ReAction. There is none. At the present moment, there is no GUI builder that would be suitable for OS4 user interface programming. Forget ReActor, forget Emperor - these tools are good for little more than sketching your GUI ideas. ReActor, placed on the Amiga Developer CD 2.1, is based on a concept (Resource Library) that was abandoned long ago. Emperor, while being amazingly comprehensive, will not generate OS4-compliant GUI code according to this guide.
2.2 The Macros
Many ReAction examples #include the file "reaction/reaction_macros.h" and construct the GUI using macros that substitute other functions. The macros were introduced with good intentions: a piece of GUI defined using them looks "more readable" in the code:
- LAYOUT_AddChild, ButtonObject,
- GA_RelVerify, TRUE,
- GA_Text, "My button",
- LAYOUT_AddChild, SliderObject,
- GA_RelVerify, TRUE,
- SLIDER_Min, -25,
- SLIDER_Max, 25,
- SLIDER_Orientation, SORIENT_HORIZ,
However, the macro include file has become a bit of a mess. It's inconsistent at places but more importantly - the ReAction macros effectively hide a certain coding practice that is now deprecated. This will be explained later in this guide (see 3.2 below); the important thing for now is: do not use the macros. Instead, change your coding style so that you do not have to #include the macro file at all.
2.3 The Autoinit
Quite a few ReAction examples are linked agains libauto, a static library that performs automatic initialization of system libraries, including ReAction classes. The autoinit is comfortable in that it saves time and makes the code shorter. However, you should not take the easy way and rely on libauto in developing your own software. Why?
First, you have much better control over things when opening the resources yourself. Programs that are meant to run across various AmigaOS versions and installations will need a more fine-tuned version control of your resources. This is quite impossible with automatic initialization via libauto. Second, libauto does not open all BOOPSI classes distributed with AmigaOS (let alone third-party ones), so chances are high that you'll need to perform manual opening anyway. And third, libauto opens ReAction classes using OpenLibrary(), which is another Don't to be mentioned:
2.4 OpenLibrary() for classes
ReAction classes are implemented as system libraries so traditionally they were supposed to be opened using the Exec function OpenLibrary(). This is not the case any more. OS4 has introduced a new function in Intuition, OpenClass(), which returns the class library base as well as something called the class pointer. The class pointer has actually been around since the beginning of BOOPSI but if you have been using the ReAction macros (see above), chances are you have not even noticed that there is such a thing. As the class pointer is highly relevant for OS4-compliant ReAction programming, it will be dealt with later on.
2.5 The Gadget and Image structures
Originally, BOOPSI gadgets and images were declared as pointers to struct Gadget and Image, respectively. This has changed. As all ReAction GUI elements - windows, requesters, gadgets, images, ARexx ports - are seen as objects, they are represented in the code by Object pointers (see 4.1 below).
2.6 Allocating GUI resources in obsolete ways
There are two types of system resource often used in ReAction GUIs: lists and ports. Lists are used by gadget classes (Chooser Gadget, ListBrowser Gadget, SpeedBar Gadget, RadioButton Gadget). A port will be needed for the program window to enable iconification and/or to receive AppMessages from Workbench.
Unfortunately, numerous ReAction examples teach you to allocate and initialize these resources in a way that is not recommended anymore: lists are defined as a data structure and then initialized via NewList(), and ports are created using the obsolete CreatePort() or CreateMsgPort() functions.
It is important to stress that under OS4, system resources (including message ports and lists) are meant to be allocated through AllocSysObject() or AllocSysObjectTags() - and of course freed by calling a corresponding FreeSysObject() when the resources are not needed anymore.
- /* Do not do this: */
- struct List list;
- /* Instead, do this: */
- struct List *list;
- list = (struct List *)IExec->AllocSysObject(ASOT_LIST, NULL);
- /* Do not do this: */
- struct MsgPort *port;
- port = IExec->CreatePort(0, 0);
- /* Instead, do this: */
- struct MsgPort *port;
- port = (struct MsgPort *)IExec->AllocSysObject(ASOT_PORT, NULL);
3. The Do's (and Why's)
This section describes general approaches that are required for OS4-compliant GUI programming. It clarifies (and goes into more detail) why the practices listed above have become deprecated, and makes further comments about certain aspects of ReAction GUI programming.
3.1 Use NewObject() directly instead of the macros
If you have a look at the ReAction macros include file, you will notice that the object creation macros (LayoutObject, ButtonObject etc.) are aliases for the Intuition function NewObject(). This is a generic BOOPSI function that calls the OM_NEW method, the result of which is a new instance of a BOOPSI object: a window, a gadget, an image etc. The process of creating an object instance from a certain class is called "instantiation".
NewObject() takes three arguments as input: a class pointer, a class public name (ClassID), and a tag list describing object properties. The object can be created (instantiated) from either the class pointer, or from its public name (if the class is public) - in other words, one of the first two arguments will be NULL. In both cases, of course, the particular class must be opened before you instantiate it through NewObject().
(Looking at the macros include file, you will see that some object creation macros use the class pointer while others use the public name. This is inconsistent, and presents just another reason for doing away with the macros. But there is more to it.)
3.2 Use the class pointer in NewObject() calls
In order to obtain the class pointer of a ReAction object, the particular class originally had to implement a special function that returns the pointer. Thus, the Button Gadget has the BUTTON_GetClass() function embedded in its binary, the Bitmap Image has BITMAP_GetClass() and so on. This function was typically passed as the first argument to NewObject() - if you used the macros you didn't even know that.
We have already mentioned above that ReAction classes are libraries. Taking into account what was said in the previous paragraph, the class library will always contain at least one function: the XYZ_GetClass() function. Now if the class pointer is obtained via calling this function, it is natural in OS4 that you have to open not only the class library but also its interface: otherwise you wouldn't be able to access the function in the library.
Luckily, there is an easier way. The new Intuition function OpenClass() returns both the class library base and the class pointer. If you keep the class pointer as a global variable you can then pass it to NewObject() as its first argument, which eliminates the need for the XYZ_GetClass() call. The advantages are the following:
- It's noticeably faster. The class pointer is only obtained once - at OpenClass() time - for each class type; on the other hand, when using the macros employing the XYZ_GetClass() functions, the pointer is retrieved each time the macro is executed. In other words, if your GUI consisted of 50 ReAction objects, the class pointer would be obtained 50 times. This is an unnecessary overhead.
- Using the class pointer from OpenClass() is also faster than using the public class name. Even if the class is a public one, prefer to use the pointer.
- Many ReAction classes implement just the one and only XYZ_GetClass() function in their library interface. If you eliminate the call to this function by using the class pointer obtained from OpenClass(), you do not need to open the interface at all! You only need the class library interface if you want to access special gadget functions like AllocListBrowserNode(). In other words, unless the class implements its own functions (currently these include: Layout Gadget, ListBrowser Gadget, Chooser Gadget, SpeedBar Gadget and RadioButton Gadget), you can save yourself from the hassle of opening its interface.
3.3 Only use functions designed for BOOPSI
If your GUI elements are created as BOOPSI objects, you are supposed to manipulate them using BOOPSI functions and methods only. Just like you would not add a ReAction gadget using the old AddGadget() function, you should not change ReAction window attributes via SetWindowAttrs() or SetWindowPointer(): they will work but are not future-proof because they set the window attributes directly, bypassing the BOOPSI system.
Most of the time, you will make do with just four or five Intuition functions to manipulate all your ReAction GUI objects: NewObject(), DisposeObject(), SetAttrs() and GetAttrs() are the fundamental ones. Gadgets, when they are already set up and displayed in the GUI, will require using the function SetGadgetAttrs() instead of SetAttrs() if you want to change their properties. This is because gadgets need some extra information to derive from the window or requester they reside in. Note that SetGadgetAttrs() is different from SetAttrs() in that it expects the gadget to be a pointer to struct Gadget. You must, then, use a type cast because your gadget will have been declared as an Object pointer (see 2.5 above).
4. The How-to's
We will now create, step by step in little code snippets, a simple ReAction GUI that consists of a window holding a single button. The tutorial will cover GUI preparation, creation and disposal, and will briefly comment on input handling. For the sake of keeping the examples short and clear, we turn a blind eye to possible initialization failures.
Although our little GUI will consist of two visible elements (the window and the button), we'll need to create three objects: at least one instance of the Layout Gadget class must exist in order to add the button to the window. All GUI objects in a ReAction window must be contained in a layout. The layout provides a mechanism for positioning and sizing objects, and is implemented as an "invisible gadget".
In order to use and instantiate our three objects, we need to declare the following variables:
- the class library base
- the class pointer
- the object instance pointer
As we are not going to use any special gadget functions, we do not need to open any interfaces (see the end of section 3.2 above). Declaring class library interface pointers here would, therefore, be pointless.
Objects are best referenced using a field of pointers, where the individual instances are identified by indexes. We will, first, use enumeration to define the indexes, and then we will declare all the necessary variables. The variables must be global or at least exist throughout the GUI lifetime.
- /* object identifiers (indexes) */
- /* class library bases */
- struct ClassLibrary *WindowBase, *LayoutBase, *ButtonBase;
- /* class pointers */
- Class *WindowClass, *LayoutClass, *ButtonClass;
- /* object pointer field */
- Object *objects[OID_LAST];
4.2 Opening and allocating resources
As the next step, we need to open our classes. We will use OpenClass() to obtain both the class library base and the class pointer in a single call. "52" is a sensible minimum version for the classes. However, remember that ReAction has gone through a lot of changes in the past few years and that some features may have been introduced later. If you need a certain functionality, always consult the class autodocs to see when the particular feature became available, and require a minimum class version accordingly.)
- WindowBase = IIntuition->OpenClass("window.class", 52, &WindowClass);
- LayoutBase = IIntuition->OpenClass("gadgets/layout.gadget", 52, &LayoutClass);
- ButtonBase = IIntuition->OpenClass("gadgets/button.gadget", 52, &ButtonClass);
Our window could also do with an application message port so we will declare and create one. Just like the class-related variables above, the message port pointer must remain available throughout the existence of the GUI:
- struct MsgPort *winAppPort;
- winAppPort = (struct MsgPort *)IExec->AllocSysObject(ASOT_PORT, NULL);
4.3 Creating the GUI
Then we go on to create the GUI by instantiating the window object and all of the objects contained within. The code may not be as clear as when using the ReAction macros but because we are using descriptive names here, I think the GUI definition is still quite readable:
- objects[OID_WINDOW] = IIntuition->NewObject(WindowClass, NULL,
- WA_Title, "Example",
- WA_DragBar, TRUE,
- WA_CloseGadget, TRUE,
- WA_SizeGadget, TRUE,
- WA_DragBar, TRUE,
- WA_DepthGadget, TRUE,
- WA_Activate, TRUE,
- WA_NewLookMenus, TRUE,
- WINDOW_IconifyGadget, TRUE,
- WINDOW_Position, WPOS_CENTERSCREEN,
- WINDOW_AppPort, winAppPort,
- WINDOW_Layout, objects[OID_LAYOUT] = IIntuition->NewObject(LayoutClass, NULL,
- LAYOUT_Orientation, LAYOUT_ORIENT_VERT,
- LAYOUT_SpaceOuter, TRUE,
- LAYOUT_DeferLayout, TRUE,
- LAYOUT_AddChild, objects[OID_BUTTON] = IIntuition->NewObject(ButtonClass, NULL,
- GA_ID, OID_BUTTON,
- GA_RelVerify, TRUE,
- GA_Text, "Press me!",
If all the NewObject() calls succeeded we can now access the individual object instances by using the respective indexes in the objects field. From now on, whenever a pointer to the window object is required, you will refer to it as objects[OID_WINDOW]; the button object instance is objects[OID_BUTTON], etc.
Our window is now ready to open. This is done by calling the WM_OPEN method: do not use the RA_OpenWindow() macro. The result of the call is a pointer to struct Window, Intuition's traditional window data structure. You may need to use this pointer in function SetGadgetAttrs() - see 3.3 above - so you either keep the return value, or you can obtain the pointer from the window object later when you need it.
- struct Window *window;
- window = (struct Window *)IIntuition->IDoMethod(objects[OID_WINDOW], WM_OPEN, NULL);
- When the window is opened, you can obtain the struct Window pointer
- from the object, using GetAttrs():
- WINDOW_Window, (uint32 *)&window,
4.4 Input handling
Now that the window is opened, you will pass program control to your event handling routine and wait for input. You may have noticed that the way input events are processed in ReAction differs from the technique described in the ROM Kernel Reference Manual. This deserves an explanation and requires a short visit to history.
What we now refer to as "the ReAction toolkit" originates from a third-party product called ClassAct. At that time, the Intuition Library contained certain bugs and the standard BOOPSI mechanism for handling input didn't work very well for the new classes. As a workaround, the ClassAct authors devised a new event processing mechanism based on calling the WM_HANDLEINPUT method. The method has since become an integral part of Intuition, and all windows based on ReAction's Window Class are expected to process input events using this method.
Note that in older ReAction examples the WM_HANDLEINPUT method call will probably have been aliased by the RA_HandleInput() macro. If you have copied the input handling routine from such a source, make sure to replace the macro with the corresponding method call:
- while ( (result = IIntuition->IDoMethod(winObj, WM_HANDLEINPUT, &code)) != WMHI_LASTMSG )
4.5 Resource disposal
When there's time to quit, we must do some clean-up of course. We will close the window, dispose of its object (which will also dispose of all the objects linked to the window, ie. the layout and the button), de-allocate the message port, and close all our classes:
- IIntuition->IDoMethod(objects[OID_WINDOW], WM_CLOSE);
- IExec->FreeSysObject(ASOT_PORT, winAppPort);
5. Final words
And that is all! I hope this little guide will contribute to a better understanding of how ReAction GUI programming should be done, and that it will encourage better coding for AmigaOS4. The things promoted here - especially the notion of doing away with the ReAction macros - may require a change in your mindset and programming practices but the advantages are greater than the sacrifices. Your reward will be more future-proof and system-friendlier software with a faster GUI.