2.5 Definition der C-Programme

Um nun zu dieser im Dialog Manager definierten Oberfläche ein C-Programm schreiben zu können, muss man folgendes wissen:

Diese einzelnen Punkte werden in den folgenden Kapiteln erläutert.

2.5.1 Kommunikation

2.5.1.1 Parameter

Der Dialog Manager bietet zunächst die Möglichkeit, bis zu acht Parameter an eine Funktion zu übergeben. Diese Einschränkung relativiert sich, wenn man in Betracht zieht, dass auch sogenannte Records übergeben werden können. Diese Records sind mit einer Struktur in C gleichzusetzen, da man deren Aufbau und Inhalt selber definieren und damit an seine Bedürfnisse anpassen kann. In einem Record können also mehrere Daten zusammengefasst übergeben werden.

Die Parameter können auf drei verschiedene Arten übergeben werden:

Die Bezeichnung input und output wird immer aus der Sicht der Anwendung betrachtet.

Bitte beachten Sie bei Verwendung von Records als Parameter auch das Kapitel „Funktionen mit Records als Parametern“.

2.5.1.2 Rückgabewerte

In C besitzen Funktionen selber einen Datentyp und können als Ergebnis einen Return-Wert zurückgeben. Auch mit dem Dialog Manager besteht die Möglichkeit, Funktionen von einem integralen Datentyp zu deklarieren, um Werte zurückgeben zu können. Eine function c boolean PUTADDR(record Address input); kann zum Beispiel true oder false als Return-Wert zurückliefern.

2.5.2 Abbildung der Dialogdatentypen

Ausgehend von der Beschreibung des Dialogs können die dort verwendeten Datentypen auf in C benutzbare Datentypen abgebildet werden. Diese Abbildung vom Dialog nach C ist eindeutig und kann der nachfolgenden unvollständigen Tabelle entnommen werden.

Dialogdatentyp

C-Datentyp

object

DM_ID

integer

DM_Integer

string

DM_String

boolean

DM_Boolean

2.5.3 Das Hauptprogramm

Bei Programmen, die mit dem Dialog Manager arbeiten sollen, sind spezielle Hauptprogramme notwendig, die vom Prinzip her immer gleich sind und daher aus den mitgelieferten Beispielen kopiert werden können.

Beim Aufbau des Hauptprogramms muss man allerdings unterscheiden, ob man eine lokale Anwendung oder einen Server in einer verteilten Umgebung entwickelt. Bei einer lokalen Anwendung muss eine Funktion mit Namen AppMain(), bei einer verteilten Anwendung müssen zwei Funktionen mit Namen AppInit() und AppFinish() bereitgestellt werden.

Diese Hauptprogramme müssen die vom Dialog Manager erzeugten Header-Dateien enthalten, damit auf die Definitionen des Dialog Managers zugegriffen werden kann.

2.5.3.1 Lokale Anwendungen

Der Aufbau der Funktion AppMain() sieht dabei wie folgt aus:

Diese Funktion hat den Namen AppMain() und sieht in etwa wie folgt aus:

int DML_c AppMain __2((int, argc), (char far * far *, argv))
{
  DM_ID dialogID;

  /* Initialisierung des Dialog Managers */
  if (!DM_Initialize (&argc, argv, 0))
  {
    DM_TraceMessage("could not initialize", DMF_LogFile);
    return (1);
  }

  /* Laden der Dialogdatei */
  switch(argc)
  {
    case 1:
      dialogID = DM_LoadDialog ("bsp.dlg", 0);
      break;
    case 2:
      dialogID = DM_LoadDialog (argv[1], 0);
      break;
    default:
      DM_TraceMessage("too many arguments", DMF_LogFile);
      return(1);
    break;
  }
  if (!dialogID)
  {
    DM_TraceMessage("could not load dialog", DMF_LogFile);
    return(1);
  }

  /* Eintrag der Adressen der Funktionen ohne Records */
  DM_BindFunctions (FuncMap, FuncCount, dialogID, 0, 0);

  /* Eintrag der Adressen der Funktionen mit Records */
  RecMInitTABLEDEMO(dialogID, 0);

  /* Starten des Dialoges und Eintritt in die Event-Loop */
  if (DM_StartDialog (dialogID, 0))
    DM_EventLoop (0);
  else
    return (1);

  return (0);
}

2.5.3.2 Verteilte Anwendungen

Bei verteilten Anwendungen müssen zwei C-Funktionen in der Anwendung bereitgestellt werden, um die Anwendung initialisieren bzw. beenden zu können. Die Initialisierungsfunktion heißt AppInit(), die Beendigungsfunktion AppFinish().

Der Aufbau der Funktion AppInit() sieht dabei wie folgt aus:

In der Funktion AppFinish() müssen dann die Aktionen durchgeführt werden, die ein kontrolliertes Beenden der Anwendung ausführen. Aufrufe an den Dialog Manager sind hierbei nicht notwendig, die Server-Anwendung wird automatisch nach der Rückkehr der Funktion beendet.

Beispiel

Funktion AppInit()

int DML_c DM_CALLBACK AppInit __4((DM_ID, appl), (DM_ID, dialog),
                                  (int, argc), (char far * far *, argv))
{
  DM_BindFunctions(ApplFuncMap, ApplFuncCount, appl, 0, DMF_Silent);
  return 0;
}

Funktion AppFinish()

int DML_c DM_CALLBACK AppFinish __2((DM_ID, appl), (DM_ID, dialog))
{
  return 0;
}

2.5.4 Hilfsmittel für die C-Programmierung

Damit die im Dialog getroffenen Definitionen für Parameter der C-Funktionen nicht zweimal definiert werden müssen, kann der Simulator des Dialog Managers aus einer Dialogdatei die für das C-Programm notwendigen Dateien generieren. Diese Dateien sollten auf keinen Fall verändert werden, da sie die Schnittstelle vom Dialog Manager zur Anwendung darstellen.

In den Dateien werden die Prototypen der Funktionen und ggf. die Abbildungen der Records des Dialog Managers auf Strukturen für das C-Programm definiert. Die Definition der Prototypen sollte als Funktionsdefinition im C-Programm benutzt werden, um einen konsistenten und fehlerfreien Funktionsaufruf zu gewährleisten.

2.5.4.1 Funktionen ohne Records als Parameter

Das Simulationsprogramm des Dialog Managers erzeugt eine Include-Datei, die alle Prototypen der im Dialog Manager definierten Funktionen enthält. Diese kann dann in die entsprechenden C-Sourcen eingebunden werden. Der jeweilige C-Compiler überprüft die Funktionsdefinitionen mit der Realisierung der Funktion und teilt dann Unterschiede mit.

Das Erzeugen dieser Prototypendatei erfolgt durch die Startoption +writeproto <Name der Header-Datei>.

Für unser Beispiel sieht die Kommandozeile wie folgt aus:

idm  +writeproto  bsp.h  bsp.dlg

Durch diesen Aufruf entsteht die Datei bsp.h.

DM_Boolean DML_default DM_ENTRY FILLTAB __((DM_ID Table,
                                            DM_Integer Number,
                                            DM_Integer Header));

void DML_default DM_CALLBACK CONTENT __((DM_ContentArgs *args));

DM_Boolean DML_default DM_ENTRY FILECLOSE __((void));

Anstelle dieser Option kann auch die Startoption +writefuncmap verwendet werden, mit der sowohl die Prototypen als auch die Funktionstabelle erzeugt werden. Die Funktionstabelle wird dabei in einer C-Datei abgelegt, so dass zur Anbindung der Funktionen die enthaltene Funktion BindFunctions_<Name des Dialogs> aufgerufen werden müsste.

In der Funktion AppMain() werden diese Funktionen, die dem Dialog Manager zur Verfügung gestellt werden sollen, in die FuncMap eingetragen. Das ist eine Struktur, die die Adressen der Funktionen bereitstellt, damit im Dialog Manager auf diese Definitionen im C-Programm zugegriffen werden kann. Die definierte Variable FuncCount gibt dabei die Anzahl der in der Tabelle definierten Funktionen an. Es ist darauf zu achten, dass nur die Funktionen in AppMain() eingetragen werden, die keine Records als Parameter besitzen, da diese über einen anderen Mechanismus an den Dialog angebunden werden.

#define FuncCount (sizeof(FuncMap) / sizeof(FuncMap[0]))

static DM_FuncMap far FuncMap[] = {
  { "CONTENT",  (DM_EntryFunc) CONTENT   },
  { "FILLTAB",  (DM_EntryFunc) FILLTAB   },
  { "FILECLOSE",(DM_EntryFunc) FILECLOSE },
};

2.5.4.2 Funktionen mit Records als Parametern

Das Simulationsprogramm des IDM erzeugt eine Include-Datei mit den Prototypen der im Dialog Manager definierten Funktionen mit Records als Parametern und die für ihren Aufruf notwendigen Funktionen über eine Startoption. Die dabei entstehende C-Datei muss mit übersetzt und zu der Anwendung dazu gelinkt werden. Die entstehende Include-Datei sollte für die eigentliche Funktionsanbindung verwendet werden, denn darin sind auch die Strukturen definiert.

Dieses erfolgt durch die Startoption +writetrampolin <Basis-Name> <Dialog-Name>

Dabei wird mit Basis-Name der Name einer Datei angegeben, an den die Endungen .h und .c angehängt werden, um die für den Aufruf der C-Funktionen notwendigen Dateien zu generieren. In der .h-Datei werden unter anderem die Prototypen der Funktionen und die Abbildung der Records auf C-Strukturen definiert.

In der .c-Datei werden zum einen die Funktionen mit Records als Parameter in die RecMap[] eingetragen und so dem Dialog Manager bekannt gemacht.

Zum anderen werden dort die Funktionen generiert, die in der Funktion AppMain() aufgerufen werden müssen, um auch die hier definierten Funktionen an den Hauptdialog zu binden.

Für unser Beispiel sieht die Kommandozeile wie folgt aus:

idm +writetrampolin  tram_bsp  bsp.dlg

Durch diesen Aufruf entstehen die Dateien

Diese Include-Datei tram_bsp.h hat für unser Beispiel folgendes Aussehen:

#ifndef _INCLUDED_bsp_tram_
#define _INCLUDED_bsp_tram_

typedef struct {
  DM_String  NName;
  DM_String  FirstName;
  DM_String  City;
  DM_String  Street;
  DM_Integer Country;
} RecAddress;

DM_Boolean DML_default DM_ENTRY GETADDR __((RecAddress *));
DM_Boolean DML_default DM_ENTRY PUTADDR __((RecAddress *));
DM_Boolean RecMInitTABLEDEMO __((DM_ID dialog, DM_ID modID));

#endif

Die generierten C- und Header-Dateien müssen mit übersetzt und zu der Anwendung hinzu gelinkt werden. Die Header-Dateien sollten benutzt werden, um auf die im Dialog getroffenen Definitionen der Übergabestruktur zugreifen zu können und die Definitionen der Funktionen in das C-Programm zu übernehmen.

2.5.5 Funktionen für das Tablefield

Funktionen, die den Inhalt von Objekten mit Listencharakter füllen oder abfragen, sind in Bezug auf das Schichtenmodell eine Ausnahme. In diesem Fall ist eine DM_-Funktion angebracht, damit die Anwendungen performant ablaufen können. Das Füllen eines Tablefields muss mit Hilfe eines Vektors erfolgen. Dieser Bereich wird dann von der Anwendung gefüllt und anschließend dem Objekt zugewiesen. Durch diese Vorgehensweise wird bei lokalen Anwendungen das ständige Flackern des Tablefields beim Füllen verhindert und bei verteilten Anwendungen zusätzlich eine sehr hohe Performanzsteigerung gegenüber der Einzelzuweisung erreicht. Im einzelnen sieht die Vorgehensweise wie folgt aus:

Das Auslesen von Werten läuft analog zum Abfragen der Werte im Objekt:

2.5.5.1 Funktion FILLTAB()

Diese Funktion übernimmt das initiale Füllen des Tablefields. Dazu wird dieser Funktion die ID des Tablefields und die Anzahl der zu füllenden Elemente übergeben.

/*
 * Funktion zum Fuellen des Tablefields bei Dialogstart
 * Parameter:
 *  table  ist die ID des Tablefields, das gefuellt wird
 *  number ist die Anzahl der Datensaetze, die geladen werden
 *  header ist die Anzahl der Header des Tablefields
 * Rueckgabewert:
 *  Boolescher Wert, der anzeigt, ob die Daten erfolgreich geladen wurden
 */
DM_Boolean DML_default DM_ENTRY FILLTAB __3((DM_ID, table),
                                            (DM_Integer, number),
                                            (DM_Integer, header))
{
  DM_Boolean retval = DM_FALSE;
  int i;
  int spos;
  int vpos;
  int length;

  static char ** strvec;
  static char *  buffer;
  static char *  start;
  static char *  end;

  DM_VectorValue vector;
  DM_Value startIdx;
  DM_Value endIdx;

  strvec = (char **) DM_Malloc ( (ROWS * COLUMNS) * sizeof (char*) );
  buffer = (char *) DM_Malloc (STR_LEN * sizeof (char) );

  if (! (f = fopen("c_bsp.dat", "r+")) )
  {
    DM_TraceMessage("Cannot open In-File", DMF_LogFile);
    return(retval);
  }

  /* erste Zelle des Tablefields, die gefuellt wird */
  startIdx.type = DT_index;
  startIdx.value.index.first = (ushort) header;
  startIdx.value.index.second = 1;

  /* letzte Zelle des Tablefields, die gefuellt wird */
  endIdx.type = DT_index;
  endIdx.value.index.first = (ushort) number;
  endIdx.value.index.second = COLUMNS;

  spos = 0;
  vpos = 0;

  vector.type = DT_string;
  vector.vector.stringPtr = strvec;

  while (!feof(f) && (spos++ < ROWS))
  {
    if (fgets(buffer, STR_LEN - 1, f))
    {
      i = 0;
      end = buffer;
      while (*end && isspace(*end))
        end++;
      while (*end && (i++ < COLUMNS))
      {
        start = end;
        length = 1;
        while (*end && !isspace(*end))
        {
          length++;
          end++;
        }
        if (*end)
          *end++ = '\0';
        strvec[vpos] = (char *) DM_Malloc((length + 1)* sizeof(char));
        strcpy(strvec[vpos],start);
        vpos++;
        while (*end && isspace(*end))
          end++;
      }
      while (i++ < COLUMNS)
      {
        strvec[vpos] = (char *) DM_Malloc(sizeof(char));
        strvec[vpos++] = "\0";
      }
    }
  }
  vector.count = (ushort) vpos;
  if (DM_SetVectorValue(table, AT_field, &startIdx,
      (DM_Value *) 0, &vector, 0))
  {
    retval = DM_TRUE;
  }

  /* Freigabe nur mit Dialog Manager Funktionen! */
  while (--vpos >= 0)
  {
    DM_Free(strvec[vpos]);
  }
  DM_Free(buffer);
  return retval;
}

2.5.5.2 Funktion CONTENT()

Mit Hilfe dieser Funktion soll das Nachladen des Tablefields erreicht werden. Immer wenn der Benutzer in einen Bereich scrollt, der noch nicht gefüllt ist, wird vom Dialog Manager automatisch diese Funktion aufgerufen.

Die Parameter sind fest vom Dialog Manager definiert und können daher nicht geändert werden. Diese Nachlade-Funktionen erhalten als Parameter die Struktur DM_ContentArgs, in der die notwendigen Informationen gespeichert sind.

In object wird die ID des Tablefields übergeben. Die Felder visfirst und vislast enthalten die erste und letzte sichtbare Zeile, die Felder loadfirst und loadlast die erste und letzte zu ladende Zeile. Dies sind jedoch nur die Minimalwerte, die Anwendung darf jederzeit mehr als die angegebenen Zeilen laden. In diesem Beispiel werden immer fünf Zeilen geladen.

/*
 * Funktion zum dynamischen Auffuellen des Tablefields beim scrollevent,
 * wenn Zeilen im Tablefield erreicht werden, die noch nicht gefuellt sind.
 */
void DML_default DM_CALLBACK CONTENT __1((DM_ContentArgs *, args))
{
  int i;
  int spos;
  int vpos;
  int length;

  static char ** strvec;
  static char *  buffer;
  static char *  start;
  static char *  end;

  DM_VectorValue vector;
  DM_Value startIdx;
  DM_Value endIdx;

  /* erste Zelle des Tablefields, die gefuellt wird */
  startIdx.type = DT_index;
  startIdx.value.index.first = args->loadfirst - 1;
  startIdx.value.index.second = 1;

  /* letzte Zelle des Tablefields, die gefuellt wird */
  endIdx.type = DT_index;
  endIdx.value.index.first = startIdx.value.index.first +
                             (ushort)CONT_ROWS - 1;
  endIdx.value.index.second = COLUMNS;

  vector.type = DT_string;
  vector.vector.stringPtr = strvec;

  spos = 0;
  vpos = 0;

  /*
   * Einlesen des Inhalts und Aufbereiten der Daten
   * HIER: GELÖSCHT!
   */

  vector.count = (ushort) vpos;

  DM_SetVectorValue(args->object, AT_field,
                    &startIdx, &endIdx,
                    &vector, 0);

  /*
   * Freigabe der durch den Dialog Manager allokierten Speicherbereiche
   * nur mit Dialog Manager Funktionen!
   */
  while (--vpos >=0 )
    DM_Free(strvec[vpos]);

  DM_Free(buffer);

  /*
   * Sollte das Ende der Datei erreicht werden, wird dieses dem Dialog Manager
   * durch ein externes Event bekannt gegeben, damit er darauf reagieren kann.
   * Hier wird eine Messagebox ausgegeben.
   * Da in Callback-Funktionen auf DM_- Funktionen verzichtet werden soll,
   * hier ein Beispiel, wie der Dialog Manager auf Ereignisse reagieren kann.
   * Die Funktion DM_QueueExtEvent ist die einzige Funktion,
   * die sicher abgearbeitet wird.
   */
  if(feof(f))
  {
    DM_Value *null = 0;
    DM_QueueExtEvent (args->object,9999,0,null, DMF_DontTrace);
  }
}

2.5.5.3 Funktion FILECLOSE()

Funktion zum Schließen der Datei, wenn der Dialog beendet wird.

DM_Boolean DML_default DM_ENTRY FILECLOSE __((void))
{
  if (! (fclose (f)) )
    return (DM_TRUE);
  return (DM_FALSE);
}

2.5.6 Funktionen mit Records als Parameter

Im Gegensatz zu den Funktionen für das Tablefield können die nachfolgenden Funktionen ohne Wissen über den Dialog auskommen und sollten das auch. Diese Funktionen werden daher ohne jeglichen Aufruf an den Dialog Manager in Standard-C geschrieben. Lediglich der Eintrag in der FuncMap zeigt, dass es sich um Funktionen handelt, die vom Dialog Manager aufgerufen werden.

2.5.6.1 Funktion GETADDR()

Diese Funktion soll zu einem gegebenen Namen den restlichen Datensatz an die Oberfläche liefern. Damit auf die Definitionen des Dialoges zugegriffen werden kann, wurde mit +writetrampolin eine entsprechende C-Struktur generiert.

/*
 * Funktion zum Holen der vollstaendigen Daten eines im Tablefield
 * angewaehlten Datensatzes. Hier muesste eine Funktion mit Dateiverarbeitung
 * geschrieben werden. Zur Vereinfachung wird der zu fuellende Record mit
 * einem festen Wert gefuellt.
 * Parameter:
 *  Address ist ein Record, der teilweise im Dialog Manager gefuellt wurde
 *              und im Programm um die fehlenden Eintraege ("Strasse")
 *              ergaenzt wird
 * Rueckgabewert:
 *  Boolescher Wert, der anzeigt, ob die Daten erfolgreich geholt wurden
 */
DM_Boolean DML_default DM_ENTRY GETADDR __1((RecAddress *, Address))
{
  Address->Street = "Neue Strasse";
  return (DM_TRUE);
}

2.5.6.2 Funktion PUTADDR()

Diese Funktion soll die vom Benutzer geänderten Daten wieder abspeichern. Damit auf die Definitionen des Dialoges zugegriffen werden kann, wurde mit dem Simulationsprogramm über die Option +writetrampolin eine entsprechende C-Struktur generiert.

/*
 * Funktion zum Schreiben der im Dialog geaenderten Daten in die Datei.
 * Diese Funktion ist hier nicht ausprogrammiert.
 */
DM_Boolean DML_default DM_ENTRY PUTADDR __1((RecAddress *, Address))
{
  Address->Street = "";
  return(DM_TRUE);
}