Das MUI Grundgerüst

In diesem Artikel geht es um das Grundgerüst eines MUI-Programms. Dieses wird nicht mehr tun, als ein Fenster zu öffnen, darin einen Text auszugeben und beim beenden eine Abfrage durchzuführen.

Auch wenn ich sehr gerne C++ eingesetzt hätte, es kommt immer wieder zu Problemen, die ich nicht lösen konnte. Also, zurück zu C. Auch keine Dramatik, früher habe ich nur in C auf dem Amiga programmiert. Da gehen dann zwar ein paar Bequemlichkeiten flöten, damit kann ich aber leben.

Einrichtung

Wir brauchen ein paar Dinge, um ein MUI-Programm in C auf dem AmigaOS 3.x schreiben zu können. Ich habe mich für folgende Kombination entschieden:

  • ADE (für GCC)
  • MUI (um MUI-Programme ausführen und compilieren zu können)
  • Redit (als Editor)

Ich bin etwas betrübt, dass GoldED mittlerweile kommerziell vertrieben wird. Er war früher mein Standard-Editor und ich hätte ihn auch gerne wieder eingesetzt. Da ich leider auch nicht mehr auf StormC zurückgreifen kann, muss für den Anfang eben ein alternativer Editor her und hier habe ich mich derzeit für Redit entschieden.

Das Ganze hat aber auch etwas Gutes. Ich habe ein Projekt, welches ich in Angriff nehmen kann und muss mir nicht erst etwas aus den Fingern ziehen. Ich schreibe eine IDE in MUI! Kostenlos, quelloffen, so dass auch andere Entwickler sich bei Interesse daran beteiligen können.

ADE

Zuerst installieren wir ADE, um GCC nutzen zu können. ADE findet man im Aminet. Wo auch sonst? So ein bisschen erinnert das Aminet an die Paketquellen unter Linux. Es müsste nur noch einen entsprechenden Paketmanager dafür geben. Na, mal schauen, ob sich da nicht irgendwann was schreiben lässt!

Ist das Archiv runtergeladen und entpackt, hat man den Ordner ADE. Diesen kopiert man einfach dorthin, wo man ihn gerne haben würde. Ich habe ihn direkt auf DH0: liegen, die bei mir HD0 heisst.

Hat man das gemacht, muss man noch folgende Zeilen in die User-Startup eintragen:

;BEGIN ADE
Execute HD0:ADE/ADE-Startup DIR HD0:ADE
assign AmiTCP: ADE:
;END ADE

Anstelle von HD0:ADE/ muss der Ort angegeben werden, wo man das ADE Verzeichnis hinkopiert hat.

Die Zeile

assign AmiTCP: ADE:

steht so nicht in der Beschreibung im Aminet. Ich habe sie hinzugefügt, weil jeder Aufruf von gcc oder g++ immer wieder die Fehlermeldung produziert, dass AmiTCP: nicht gefunden wurde. Warum genau diese überhaupt gesucht wird, ist mir überhaupt nicht klar. Was will denn der Compiler im Internet? Egal. In der Emulation kann mein AmigaOS ohnehin nicht ins Internet, von daher tue ich ihm einfach den Gefallen.

Anmerkung:

gcc/g++ sucht die Datei "usergroup.library" und zwar im Verzeichnis "AmiTCP:libs/". Diese findet sich auch in "ADE:libs/". Deshalb setze ich "AmiTCP:" auf "ADE:"

MUI

Für MUI brauchen wir zwei Archive. Beide finden sich im Aminet.

Nach dem entpacken von mui38usr.lha findet man im Ordner MUI eine Installationsdatei. Die einfach ausführen, die entsprechenden Schritte abarbeiten und fertig.

Nach dem entpacken von mui38dev.lha hat man den Ordner Developer. Darin findet sich der Ordner C und darin der Ordner Include. Also:

MUI/Developer/C/Include

Den kompletten Inhalt kopieren wir nach ADE:include.

Fertig.

Redit

Auch Redit findet sich natürlich im Aminet. Zu installieren gibt es da nicht viel. Ich habe die Datei Redit einfach nach C: kopiert, damit sie von überall aufgerufen werden kann.

Das Grundgerüst

Hat alles geklappt, können wir mit dem programmieren anfangen.

Hier soll nun ein Grundgerüst aufgebaut werden, auf welchem spätere Anwendungen basieren können.

default.h

Wir beginnen mit der Header-Datei. Die kann man eigentlich nach belieben benennen, solange ein .h dahinter steht. Ich habe mich für default.h entschieden. Warum? Na, weil es eben so etwas wie der Standard sein soll, auf dem zukünftige Anwendungen beruhen.

Wir fangen an, indem wir die benötigten Dateien includieren:

#include <libraries/mui.h>
#include <proto/exec.h>
#include <proto/dos.h>
#include <proto/muimaster.h>
#include <libraries/gadtools.h>
#include <libraries/iffparse.h>
#include <proto/intuition.h>
#include <proto/graphics.h>

Da ist schon viel Zeug dabei, was man in diesem Grundgerüst eigentlich nicht braucht. Da wir aber später noch komplexere Anwendungen schreiben wollen, bauen wir das einfach direkt mit ein.

Nun definieren wir MAKE_ID, um den Fenstern später IDs zuweisen zu können. MUI benötigt das, also spendieren wir es ihm:

#define MAKE_ID(a,b,c,d) ((ULONG) (a)<<24 | (ULONG) (b)<<16 | (ULONG) (c)<<8 | (ULONG) (d))

MAKE_ID wird uns später noch in der default.c begegnen.

Da wir auch noch ein paar Libaries benötigen, deklatieren wir die entsprechenden Zeiger:

struct IntuitionBase *IntuitionBase;
struct GfxBase *GfxBase;
struct Library *MUIMasterBase;

Für das Grundgerüst würde auch nur MUIMasterBase ausreichen, doch wie oben schon geschrieben, es soll ja auch mal komplexer werden!

Da wir aber die Libraries brauchen und nicht nur eine Deklaration, schreiben wir uns jetzt noch eine Funktion, die wir dann später einfach aufrufen können und die für das öffnen der Libraries sorgt:

BOOL openLibs()
{
.
.
.
}

Darin öffnen wir jetzt eine Library nach der anderen und prüfen dabei, ob auch alles geklappt hat:

if(!(IntuitionBase = (struct IntuitionBase *) OpenLibrary("intuition.library", 39))) return FALSE;

Wir öffnen also die intuition.library und prüfen, ob sie auch geöffnet werden konnte. Wenn ja, geht es weiter in der Funktion. Wenn nicht, wird die Funktion mit FALSE beendet.

  if(!(GfxBase = (struct GfxBase *) OpenLibrary("graphics.library", 0)))
  {
    CloseLibrary((struct Library *)IntuitionBase);
    return FALSE;
  }

Das gleiche Spiel mit graphics.library. Wurde sie erfolgreich geöffnet, geht es weiter. Wenn nicht, dann wir die intuition.library wieder geschlossen und wieder die Funktion mit FALSE beendet.

  if(!(MUIMasterBase=OpenLibrary(MUIMASTER_NAME, MUIMASTER_VMIN)))
  {
    CloseLibrary((struct Library *)GfxBase);
    CloseLibrary((struct Library *)IntuitionBase);
    return FALSE;
  }

Und weiter geht es mit der muimaster.library. Klappt das, geht es weiter. Wenn nicht, werden die geöffneten Libraries geschlossen und die Funktion mit FALSE beendet. Wer hätte es gedacht?

return TRUE;

Wenn bis hierhin alles geklappt hat, kann die Funktion mit TRUE beendet werden. Komplett sieht das dann so aus:

BOOL openLibs()
{
  if(!(IntuitionBase = (struct IntuitionBase *) OpenLibrary("intuition.library", 39))) return FALSE;

  if(!(GfxBase = (struct GfxBase *) OpenLibrary("graphics.library", 0)))
  {
    CloseLibrary((struct Library *)IntuitionBase);
    return FALSE;
  }

  if(!(MUIMasterBase=OpenLibrary(MUIMASTER_NAME, MUIMASTER_VMIN)))
  {
    CloseLibrary((struct Library *)GfxBase);
    CloseLibrary((struct Library *)IntuitionBase);
    return FALSE;
  }
  
  return TRUE;
}

Wir sind ja anständige Programmierer und wenn wir Libraries öffnen, dann schliessen wir sie auch wieder, wenn wir sie nicht mehr brauchen. Dafür basteln wir uns erneut eine Funktion:

void closeLibs()
{
  if(IntuitionBase) CloseLibrary((struct Library *)IntuitionBase);

  if(GfxBase) CloseLibrary((struct Library *)GfxBase);

  if(MUIMasterBase) CloseLibrary(MUIMasterBase);
}

Damit wäre die Header-Datei auch schon fertig!

default.c

Hier wird das Programm starten. Das heisst, hier drin befindet sich die main() Funktion. Aber zuerst wieder includieren!

#include <stdio.h>

#include "default.h"

Die Datei stdio.h brauchen wir für Funktionen wie printf() zum Beispiel. Ich denke aber, wer sich an die Programmierung mit MUI wagt, der weiss das schon. Da gehe ich also nicht wirklich drauf ein.

Für einen ungeübten Entwickler hier schnell die Anmerkung, die default.h wird nicht mit <> eingebunden! Warum nicht? Weil sie nicht im Include-Verzeichnis liegt, sondern in dem selben Ordner, wie default.c! Die spitzen Klammern sagen also: “Such im Include-Verzeichnis!”. Während die Anführungsstriche sagen: “Such dort, wo die Datei liegt, von der du aufgerufen wurdest!”.

Und schon kommt die main() Funktion!

int main(int argc, char *argv[])
{
.
.
.
}

Das sollte eigentlich immer so aussehen. Man gibt dem Betriebssystem nach dem beenden einen Wert zurück und kann beim Aufruf des Programms Parameter angeben. Sollte bekannt sein.

Gut. Lasset uns deklarieren!

	Object *app, *win1;
	ULONG signals;
	BOOL running = TRUE;

Zuerst brauchen wir Objekte. Eines für das Programm selbst app und eines für das Fenster win1. Die Variablen kann man beliebig benennen, seit kreativ! Oder belasst es einfach so.

Signale sind sehr wichtig! Ohne sie hätte man zwar ein Fenster, es würde aber auf nichts reagieren. Also ULONG signals;.

Anders als Kommandozeilen-Programme, läuft ein MUI-Programm in einer Schleife. Das heisst, dass Programm erreicht nicht einen bestimmten Punkt und wartet dort auf eine Eingabe. Wäre auch ziemlich unsinnig. Sagen wir, wir haben da ein komplexes Programm mit diversen Strings, Buttons und was weiss ich. Da wäre es ja unsinnig, wenn man immer darauf warten würde, dass ein bestimmter Button gedrückt wird. Das Programm wäre ziemlich nutzlos. Also läuft hier eine Schleife, die wartet auf ein Signal und die will man ja irgendwann auch beenden können. Deshalb deklarieren wir die Variable running und setzen sie auf TRUE. Sprich, ja, dass Programm läuft!

	if(!openLibs())
	{
		printf("Cannot open libs\n");
		return FALSE;
	}

Okay. Nun öffnen wir die Libraries. Dafür haben wir uns ja die Funktion openLibs() gebaut und die rufen wir jetzt auf. Natürlich prüfen wir, was da zurück kommt. Ist es TRUE, dann kann es weitergehen. Wenn nicht geben wir in der Kommandozeile aus, dass wir die Libraries nicht öffnen können und beenden das Programm.

	app = ApplicationObject,

Nun füllen wir die Variable app. Hier geht es eigentlich erst los mit MUI. Die Variable soll also ein ApplicationObject sein. MUI will so etwas haben, also sind wir so gnädig.

		MUIA_Application_Title,       "Default",
		MUIA_Application_Version,     "$VER: Default X.X (XX.XX.XX)",
		MUIA_Application_Copyright,   " ",
		MUIA_Application_Author,      " ",
		MUIA_Application_Description, " ",
		MUIA_Application_Base,        " ",

Dann möchte MUI gerne ein paar Informationen haben. Titel, Version usw. Bei Version sollte man $VER: voranstellen. Dann kann man später in der Kommandozeile die Version des Programms abrufen.

Was man hier jetzt aber überall einträgt, kann man sich quasi würfeln. Es empfiehlt sich jedoch, die entsprechenden Werte sinnvoll zu setzen!

Jetzt kommt der spannende Teil:

		MUIA_Application_Window, win1 = WindowObject,
			MUIA_Window_Title, "Window Title",
			MUIA_Window_ID,    MAKE_ID('D','E','F','A'),
			WindowContents, VGroup,
				Child, MUI_MakeObject(MUIO_Label, "Default MUI Application", NULL),
			End,
		End,

Wir erstellen das Fenster! Dieses ist ein MUIA_Application_Window und das hätte gerne ein paar Informationen. Ausserdem soll es in der Variable win1 gespeichert werden. Man weiss ja nie, ob man es irgendwann vielleicht mal schliessen und wieder öffnen will? Zugegeben, beim Hauptfenster wird das weniger passieren, aber trotzdem gehört es in eine Variable.

Titel sollte klar sein. Da kommt das rein, was später in der Titelleiste stehen soll.

Bei der ID kommt dann die definierte Funktion MAKE_ID zum Einsatz. Hier kann man im Prinzip seine Buchstaben auch würfeln. Wichtig ist nur, man sollte nicht zwei Fenster die gleiche ID zuweisen!

WindowContents ist dann der richtig spannende Teil. Denn hier baut man alles das ein, was später mal im Fenster erscheinen soll. Buttons, Listen, Strings usw.

MUI organisiert seine Child in Gruppen. HGroup und VGroup zum Beispiel. Also horizontale Gruppen, in welchen die darin befindlichen Childs in einer Reihe angeordnet werden, während sie in vertikalen Gruppen untereinander erscheinen. Soweit ist das ja logisch.

Da wir kein leeres Fenster wollen, basteln wir hier einfach ein MUIO_Label rein. Also nichts anderes, wie eine Beschriftung, ein kleiner Text. Was da drin stehen soll, kommt dahinter in die Anführungszeichen.

Nun heisst es noch, mit End, erst den WindowContents beenden und mit dem nächsten End, MUI_Application_Window.

End;

Mit End; beenden wir dann auch noch das ApplicationObject und das Fenster ist fertig definiert. Feuern wir es ab!

	if(!app)
	{
		printf("Cannot create application.\n");

                closeLibs();

		return FALSE;
	}

Wir wollen also jetzt, dass das Fenster auch angezeigt wird. Sollte dabei irgendetwas schiefgehen, geben wir wieder eine Fehlermeldung aus und beenden das Programm! Da wir ja ordentlich sind, schliessen wir auch die Libraries dabei wieder.

	DoMethod(win1, MUIM_Notify, MUIA_Window_CloseRequest, TRUE,
		app, 2, MUIM_Application_ReturnID, MUIV_Application_ReturnID_Quit);

Das Programm soll aber auch bitte über den Quit-Button beendet werden können. Dafür müssen wir mit DoMethod() angeben, dass beim drücken des Buttons etwas passiert. Es soll MUIV_Application_ReturnID_Quit zurückgegeben werden.

	set(win1, MUIA_Window_Open, TRUE);

So. Das Programm ist gestartet, jetzt wollen wir auch das Fenster sehen. Das ist ja in win1 gespeichert und wir öffnen es jetzt, indem wir MUIA_Window_Open auf TRUE setzen. Ich sagte ja, es ist durchaus sinnvoll, sein Fenster in einer Variable zu speichern ;).

while(running)
{
    ULONG id = DoMethod(app, MUIM_Application_Input, &signals);

    switch(id)
    {
            case MUIV_Application_ReturnID_Quit:
                if((MUI_RequestA(app, 0, 0, "Quit?", "_Yes|_No", "\33cAre you sure?", 0)) == 1)
                    running = FALSE;
            break;
    }
    if(running && signals) Wait(signals);
}

Hier ist jetzt die erwähnte Schleife. Solange also running auf TRUE gesetzt ist, wird diese Schleife immer wieder abgerufen. Dabei wird in der neu deklarierten Variable id der entsprechende Wert des Signals gespeichert. Dafür haben wir schliesslich auch signals deklariert!

Nun werten wir das eintreffende Signal aus. Kommt MUIV_Application_ReturnID_Quit an, starten wir eine kleine Abfrage, ob wir auch sicher sind, dass Programm beenden zu wollen. Klickt man hier auf Yes, wird 1 zurückgegeben. Kommt hier also eine 1, wird running auf FALSE gesetzt und nach Durchlauf der Schleife die Schleife verlassen.

Am Ende der Schleife checken wir dann, ob running und signals noch TRUE sind. Ist dem so, so warten wir genau an dieser Stelle auf das nächste Signal.

	set(win1, MUIA_Window_Open, FALSE);

Wenn die Schleife verlassen wurde, soll ja auch das ganze Programm beendet werden. Also soll auch das Fenster verschwinden. Deshalb setzen wir es auf FALSE. Ja, wenn das Programm beendet wurde, würde auch das Fenster von selbst verschwinden. Wir sind aber anständige Programmierer!

  if(app) MUI_DisposeObject(app);

Okay. Wenn in app irgendetwas drin steht, soll MUI das jetzt entfernen. Deshalb MUI_DisposeObject();.

	closeLibs();

Was macht der brave Programmierer am Ende seines Programms? Richtig! Er schliesst, was er vorher geöffnet hat. Also closeLibs();.

	exit(TRUE);

Alles geschlossen, gelöscht und sauber? Dann beenden wir das Programm mit exit(TRUE);. Weil hat ja alles prima geklappt.

Komplett sieht das dann so aus:

#include <stdio.h>

#include "default.h"

int main(int argc, char *argv[])
{
	Object *app, *win1;
	ULONG signals;
	BOOL running = TRUE;

	if(!openLibs())
	{
		printf("Cannot open libs\n");
		return FALSE;
	}

	app = ApplicationObject,
		MUIA_Application_Title,       "Default",
		MUIA_Application_Version,     "$VER: Default X.X (XX.XX.XX)",
		MUIA_Application_Copyright,   " ",
		MUIA_Application_Author,      " ",
		MUIA_Application_Description, " ",
		MUIA_Application_Base,        " ",

		MUIA_Application_Window, win1 = WindowObject,
			MUIA_Window_Title, "Window Title",
			MUIA_Window_ID,    MAKE_ID('D','E','F','A'),
			WindowContents, VGroup,
				Child, MUI_MakeObject(MUIO_Label, "Default MUI Application", NULL),
			End,
		End,
	End;

	if(!app)
	{
		printf("Cannot create application.\n");

		closeLibs();

		return FALSE;
	}

	DoMethod(win1, MUIM_Notify, MUIA_Window_CloseRequest, TRUE,
		app, 2, MUIM_Application_ReturnID, MUIV_Application_ReturnID_Quit);

	set(win1, MUIA_Window_Open, TRUE);

	while(running)
	{
		ULONG id = DoMethod(app, MUIM_Application_Input, &signals);

		switch(id)
		{
				case MUIV_Application_ReturnID_Quit:
					if((MUI_RequestA(app, 0, 0, "Quit?", "_Yes|_No", "\33cAre you sure?", 0)) == 1)
						running = FALSE;
				break;
		}
		if(running && signals) Wait(signals);
	}

	set(win1, MUIA_Window_Open, FALSE);

  if(app) MUI_DisposeObject(app);

	closeLibs();

	exit(TRUE);
}

Das war es auch schon!

makefile

Das ist eigentlich optional. Aber will man wirklich jedes Mal gcc mit den ganzen Parametern starten? Nö! Also basteln wir uns ein Makefile.

Zuerst setzen wir ein paar Variablen:

objects = default.o
appname = Default
option = -O3 -m68000 -noixemul

Mit objects geben wir an, wie die Objekt-Datei heissen soll. Unser Programm heisst default, also nennen wir das auch default.o. Bei anderen Programmen, die andere Namen tragen, steht dann hier irgendwas.o. Ganz klar.

Da unser Programm compiliert nicht a.out heissen soll, taufen wir es und setzen deshalb appname auf Default.

Wie schon gesagt, gcc hat ganz gerne Parameter. Die setzen wir unter option.

-03 heisst in dem Fall, er soll optimieren. Den Wert kann man auch höher setzen, oder weglassen. Aber -03 ist eigentlich immer ein ganz guter Wert.

-m68000 sagt, es soll für den 68000 Prozessor optimiert werden. Hier könnte man zum Beispiel auch -m68030 reinschreiben. Dann kann es aber sein, dass es unterhalb eines 68030 nicht mehr läuft!

-noixemul heisst eigentlich nur, dass ixemul nicht eingebunden werden soll. Das dient dazu, Programme aus zum Beispiel der Linux-Welt besser portieren zu können. Brauchen wir nicht, als lassen wir es auch weg.

default:	$(objects)
	gcc -o $(appname) $(objects) $(option)

Jetzt geht die Linkerei los. Wie man es sich fast schon denken kann, default: ist wieder der Name unseres Programms. $(objects) gibt nun das an, was wir unter objects angegeben haben, genauso wie $(option).

Jetzt kommt auch gcc ins Spiel. Der Parameter -o gibt an, wie dass compilierte Programm heissen soll.

default.o:	default.c default.h
	gcc -c default.c $(option)

Ich denke, dazu ist nicht mehr viel zu sagen. Lediglich sollte klar sein, dass man hier die .c Datei angeben muss, in der sich main() befindet.

clean:
	rm $(objects) $(appname)

Schliesslich wollen wir auch aufräumen können. Beispielsweise, um das Programm erneut zu compilieren. Findet gcc die default.o Datei kann es nämlich gut sein, dass behauptet wird, alles wäre “up-to-date”. Dann wird nichts compiliert!

Komplett sieht das dann so aus:

# 
# Target OS: Amiga OS3.X
# Compiler : GCC
#

objects = default.o
appname = Default
option = -O3 -m68000 -noixemul

default:	$(objects)
	gcc -o $(appname) $(objects) $(option)
	
default.o:	default.c default.h
	gcc -c default.c $(option)
	
clean:
	rm $(objects) $(appname)
	

Fertig.

Finale

Wir haben jetzt alles zusammen. Das heisst, wir können unser Programm compilieren!

Dazu starten wir eine Shell, wechseln in das Verzeichnis, indem wir die Dateien gespeichert haben und dann geben wir make ein!

Na da schau her! Erst hat gcc das Programm compiliert und dann auch noch gelinkt! Heisst das etwa, es hat funktioniert? Es gibt nur einen Weg, um es herauszufinden! Wir tippen default ein und schauen was passiert.

Klasse! Es hat geklappt! Das Programm wird gestartet, dass Fenster geöffnet und wenn wir auf den Button drücken, kommt die Abfrage! Noch ein Klick auf Yes und das Programm wird brav wieder beendet. Wir sind am Ziel!

Schreib einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert