Freitag, 4. Februar 2011

C#: Deine eigene Konsole (-Manager)

In diesem Tutorial erkläre ich, wie man eine Konsole in C# schreibt. Die Konsole ist für zwei Dinge gedacht: Anzeigen von Text und Ausführen von Kommandos. Erstmal brauchen wir einen Manager, der alle Textlinien und alle Kommandos enthält. Die Konsole wird über diesen Manager weder angezeigt, noch kann man direkt etwas darin eingeben. Dies wird in einer anderen Klasse gemacht, welche dann einfach auf die Funktionen dieses Managers zugreift.

Da dies ein Manager ist, wird er hier im Namespace BGE.Managers veröffentlicht.

Ich werde den Code hier fliessend kommentieren. Das heisst, der Code wird ab und zu unterbrochen für einen Kommentar, doch wenn man alles nacheinander eingibt, erhält man eine "brauchbare" Datei.

Da der Manager von überall aus gebraucht werden können soll, wird es nur statische Methoden und Variablen geben. Wer mag, kann dies auch als Singleton konzipieren. Ich sehe da keine Not dazu, denn wenn alles sowieso nur einmal vorhanden ist, warum dann extra EINE Instanz erstellen? (Stattdessen gibt es hier den Initialize-Befehl.)

Die Konsole soll Kommandos ausführen können. Diese können Parameter haben, müssen aber nicht. Diese Kommandos müssen dann in C# Funktionen ausführen, welche der Programmierer der Konsole bekannt gemacht hat. Wir brauchen also Delegaten (Funktionspointer), und zwar zwei Stück: Einer für Funktionen ohne Parameter und einen für Funktionen mit Parametern.

Da der Konsole sowieso ein string übergeben wird, mit Kommandonamen UND Parametern, muss der zweite Delegat auch nur einen string als Parameter haben - egal wieviele Parameter die Funktion nun "entgegennehmen" soll.

Der Manager stellt diese vier Funktionalitäten bereit - mehr braucht der "Anwender" nicht zu wissen:
ConsoleManager.Lines - gibt die Liste mit den Textlinien zurück. Dies wird später gebraucht, um den Text überhaupt anzuzeigen.
ConsoleManager.addLine(string) - fügt den string an die Textliste an.
ConsoleManager.addCommand(Delegat,string,[string],[bool]) - Dient dem Erstellen von Kommandos für die Konsole.
ConsoleManager.execute(string) - Dient dem Ausführen von Kommandos.

Wir werden Klassen im Namespace System.Collections.Generic brauchen, darum steht eine using-Direktive am Anfang.

[Start Code]

using SysCol = System.Collections.Generic;

namespace BGE.Managers
{
  delegate void VoidFunctionDelegate(void);
  delegate void StringFunctionDelegate(string s);
Nun brauchen wir eine Klasse, in der wir eine Liste und ein Dictionary haben.

Eine Liste enthält Objekte, die sozusagen schön nacheinander aufgereiht sind. Wenn man ein bestimmtes Objekt sucht, muss man wissen an welcher Stelle es ist und dann durch die Liste durchgehen bis man es hat. Die Liste brauchen wir für die Textlinien der Konsole.

Ein Dictionary kann man sich wirklich vorstellen wie ein Wörterbuch: Man hat ein Objekt (meist ein string) welches das gesuchte Wort/Objekt in "deiner" Sprache darstellt und kann damit das andere Objekt ("die Uebersetzung") heraussuchen. Das Dictionary brauchen wir für die Kommandos, wobei der NAME des Kommandos das zu suchende Wort darstellt und die Delegaten sind "die Uebersetzung" dazu und werden ausgeführt, wenn gefunden.

Da es jedoch ZWEI Delegaten sind, müssen sie erstmal in eine kleine Klasse gestopft werden, um sie dann im Dictionary verfügbar zu machen. Diese drei Klassen (BasisKlasse, VoidFunktion und StringFunktion) sind IN der ConsoleManager-Klasse, da sie auch NUR von der ConsoleManager-Klasse verwendet werden.
  public static class ConsoleManager
  {
    protected class consoleCommand
    {
      public string description;
      public bool bHasParameters=false;
    }
consoleCommand ist die Basisklasse. Hier ist noch kein Delegat vorhanden, aber ein string um eine kleine Erklärung zum Kommando hinzuzufügen. Mit bHasParameters kann der Manager dann entscheiden, ob er die Funktion mit oder ohne Parameter ausführen soll. Die Basisklasse braucht keinen Konstruktor.
    protected class voidConsoleCommand : consoleCommand
    {
      public VoidFunctionDelegate method;

      public voidConsoleCommand(VoidFunctionDelegate m, string d)
      {
        method = m;
        description = d;
        bHasParameters = false;
      }
    }

    protected class stringConsoleCommand : consoleCommand
    {
      StringFunctionDelegate method;

      public stringConsoleCommand(StringFunctionDelegate m, string d)
      {
        method = m;
        description = d;
        bHasParameters = true;
      }
    }
Diese beiden Klassen sind komplett gleich, bis auf die Tatsache, dass die eine mit VoidFunctionDelegate arbeitet und die andere mit StringFunctionDelegate. bHasParameters wird bei der zweiten Klasse auf true gesetzt!

Dem Konstruktor muss man direkt eine Methode und einen String übergeben. Andere Konstruktoren sind überflüssig.

Nun können wir die Liste und das Dictionary erstellen und auch gleich die Initialize Funktion, welche EINMAL ausgeführt werden MUSS, und zwar beim Start des Programms (entspricht dem Konstruktor beim Singleton). Die Textliste ist auch öffentlich verfügbar. Dafür ist dann das Property Lines zuständig, welches die Liste nur zurückgeben aber nicht setzen kann.
    protected SysCol.List<string> _textLines=null;
    public SysCol.List<string> Lines {get {return _textLines;}}

    protected SysCol.Dictionary<string, consoleCommand> _commands=null;

    public void Initialize()
    {
      if(_textLines==null)
        _textLines=new SysCol.List<string>();
      if(_commands==null)
        _commands=new SysCol.Dictionary<string, consoleCommand>;
    }
Jetzt kommen wir zu den für uns interessanten Funktionen. Erstmal die kleinen Funktionen. Die addLine-Funktion fügt einfach einen string zu der Textliste hinzu.
    static public void addLine(string s)
    {
      _textLines.Add(s);
    }
Die clear-Funktion löscht nur die Textlinien - die Kommandos bleiben erhalten.
    public static void clear()
    {
      _textLines.clear();
    }
Mit der IsCommandExisting-Funktion kann man überprüfen ob ein Kommando in der Kommandoliste vorhanden ist.
  public static bool IsCommandExisting(string commandName)
  {
    if(_commands.ContainsKey(BGE.Helpers.getWordFromString(commandName, 1).ToLower()))
      return true;
    return false;
  }
Jetzt kommen die WIRKLICH interessanten Funktionen: Kommandos erstellen und ausführen. Zuerst die beiden addCommand-Funktionen, eine für Funktionen ohne - und eine für Funktionen mit Parametern.
  public static void addCommand(string commandName, VoidFunctionDelegate methodToCall, string description="- no description -",bool overWriteExistingEntry=true)
  {
    if(_commands.ContainsKey(commandName.ToLower()))
    {
      if(overwriteExistingEntry)
      {
        _commands.Remove(commandName.ToLower());
        voidConsoleCommand command=new VoidConsoleCommand(method, description);
        _commands[commandName.ToLower()] = command;
      }
    }else{
        voidConsoleCommand command=new VoidConsoleCommand(method, description);
        _commands[commandName.ToLower()] = command;
    }
  }
..und dasselbe nochmal für die die Funktion mit Parametern. Hier ändert sich nur das voidConsoleCommand in stringConsoleCommand und VoidFunctionDelegate in StringFunctionDelegate
  public static void addCommand(string commandName, StringFunctionDelegate methodToCall, string description="- no description -",bool overWriteExistingEntry=true)
  {
    if(_commands.ContainsKey(commandName.ToLower()))
    {
      if(overwriteExistingEntry)
      {
        _commands.Remove(commandName.ToLower());
        stringConsoleCommand command=new stringConsoleCommand(method, description);
        _commands[commandName.ToLower()] = command;
      }
    }else{
        stringConsoleCommand command=new stringConsoleCommand(method, description);
        _commands[commandName.ToLower()] = command;
    }
  }
Erst mal schaut die Funktion ob das Kommando schon existiert. Wenn ja, wird sie nur überschrieben, wenn overwriteExistingEntry auf true ist.

Ansonsten wird das Kommando einfach neu erstellt, und zwar indem man eine Instanz der jeweiligen Klasse erstellt, ihr den Delegaten und die Erläuterung (description) übergibt und diese dann unter dem Kommandonamen im _commandos-Dictionary speichert.

Nun braucht es nur noch eine Funktion um die Kommandos auszuführen. Erst wird das erste Wort aus dem gegebenen string herausgesucht. Das ist das Kommando, der Rest sind Parameter. Danach wird geprüft ob es Parameter hat und ein voidConsoleCommand- oder ein stringConsoleCommand-Pointer erstellt welche auf das Kommando aus dem Dictionary zeigen. Das Kommando wird über die method des Pointers aufgerufen. Ich merke hier an, dass die consoleCommand-Klasse KEINE method hat, deshalb MUSS extra gecasted werden.
  public static void execute(string textLine)
  {
    string command=BGE.Helpers.getWordFromString(textLine).ToLower();

    if(_commands.Contains(command))
    {
      if(_commands.bHasParameter==false)
      {
        voidConsoleCommand c=(voidConsoleCommand)_commands[command];
        c.method();
      }else{
        stringConsoleCommand c=(stringConsoleCommand)_commands[command];
        c.method(BGE.Helpers.getAllAfterWord(textLine,1);
      }
    }
  }
Jetzt fehlt nur noch die abschliessende Klammer:
}
Und das wärs..

[END CODE]

Hier noch ein kleines Beispiel:

Erstmal zwei Funktionen, die wir der Konsole verfügbar machen wollen:
static void MyVoidFunction()
{
  ConsoleManager.addLine("You called MyVoidFunction");
}

static void MyStringFunction(string s)
{
  ConsoleManager.addLine("You called MyStringFunction with Parameters "+s.ToString());
}
Nun brauchen wir diese Funktionen der Konsole noch verfügbar zu machen...
ConsoleManager.addCommand("callVoid", MyVoidFunction, "a void function");
ConsoleManager.addCommand("callString",MyStringFunction, "a string function");
..und schon können wir sie ausführen..
ConsoleManager.execute("callVoid");
ConsoleManager.execute("callString with parameters");
Ich hoffe, das hilft...

Kommentare:

  1. bekomme schon bei "delegate void VoidFunctionDelegate(void);" = ungültiger Parametertype "void"

    AntwortenLöschen
  2. Versuch die Delegaten in die Klasse zu stopfen, das hab ich wohl falsch geschrieben.

    namespace BGE.Managers
    {
    public static class ConsoleManager
    {
    delegate void VoidFunctionDelegate(void)
    ......

    ..und eventuell noch public machen.

    AntwortenLöschen
  3. Alles muss static sein, siehe Fehler am Anfang. Da einfach noch überall static hinzufügen.

    AntwortenLöschen
  4. Kann es sein, das dieser Code nciht für das NF 2.0 geeignet ist?
    Könntest du den ganzen Code einmal posten?

    AntwortenLöschen