Sonntag, 6. Mai 2018

Verwendung von Standardstreams zur Ausgabe in Memofeldern

Verwendung von Standardstreams mit VCL und FMX

Ich höre immer wieder, dass die meisten C++ Bücher und Beispiele, die man im Internet findet, mit dem C++Builder im speziellen, aber eben auch generell mit C++ in der heutigen Zeit nicht mehr funktionieren würden. Diese würden die Standardstreams verwenden, und die würden ja bei der Nutzung einer grafischen Oberfläche, sei es nun Windows, Android oder auch IOS nicht funktionieren. Und wer würde dann noch Anwendungen für die Konsole oder Eingabeaufforderung schreiben? Daran würde sich dann sogar sehr schnell die generelle Schwäche von C++ zeigen, eigentlich aus der Vergangenheit zu stammen, nicht mehr modern zu sein. Für mich zeigt es allerdings nur eines. Nämlich, dass viele die C++ Streams bisher nicht verstanden haben.

Die meisten schaffen es vielleicht noch, für eigene Datentypen die Eingabe- und Ausgabe- Operatoren zu schreiben, aber generell scheinen die Streams bei vielen C++- Programmierern nicht beliebt zu sein. Dabei bieten sie uns viele Möglichkeiten, und funktionieren von beiden Seiten. Wir können unabhängig voneinander Operatoren definieren, die die Streams nutzen, um unsere eigenen Datentypen auszugeben oder einzulesen. Wir können aber auch an der Basis Veränderungen vornehmen. Also bestimmen, wohin (bzw. woher) die Daten geschrieben oder gelesen werden. Beim Verwenden solcher veränderten Streams spricht man oft vom umlenken, manche bezeichnen es auch "Verbiegen". Ich werde das für die Standardausgabe machen, im ersten Schritt werden ich hier ein Memo- Feld der VCL verwenden, um dann die Klasse auch wieder für FMX verfügbar zu machen. Außerdem werde ich einen Wrapper schreiben, der über RAII die Kontrolle über einen beliebigen Stream übernimmt und Funktionen zum "Verbiegen" anbietet. Beim Ende der Lebensdauer wird aber auf jeden Fall der vorherige Zustand wiederherstellt. 

In einem späteren Blogpost werde ich dann weitere sinnvolle Steuerelemente verwenden, wie die Statuszeile für das Loggen mit clog, oder sogar ein Listview- Steuerelement für die Ausgabe von Listen. Da Listview- Steuerelemente nur unter Windows in der Form existieren und damit mit der VCL werden wir dieses dann einfach in einem StringGrid und FMX nutzen.

Wichtig bei der Verwendung der Stream- Klassen, ist zu verstehen, dass es eine strikte Unterscheidung zwischen dem nationalen Zeichensatz (narrow, char) und dem internationalen Zeichensatz (wide, wchar_t) gibt. Hier wird nicht willkürlich etwas verändert, wie wir es am Beispiel der Delphi- Stringtypen ab der Version 2009 erlebt haben, so müssen sie sich selber bewusst entscheiden, oder eben für alle Varianten Implementierungen vornehmen. Ich möchte hier nur die nationalen Zeichen betrachten, sollten sie in ihren Streams auch mit internationalen Zeichen arbeiten wollen, ist es sicher leicht von ihnen selber erweiterbar. Und auch heute dürfte der nationale Zeichensatz für die meisten Fälle ausreichen. 

Um eine neue Ausgabemöglichkeit für einen Stream zu definieren, müssen wir eine neue Klasse als Ableitung der Klasse std::streambuf aus der C++- Standard Klassenbibliothek definieren. Diese Klasse finden Sie in der Headerdatei <iostream>. Memofelder werden in der VCL über die Klasse TMemo aus der Headerdatei <Vcl.StdCtrl.hpp> angesprochen. Wir nehmen einen Zeiger auf diese als privates Datenelement und nutzen es als Parameter für den Konstruktor.

Nun müssten wir nur die geerbte Methode Overflow überschreiben. Dazu hängen wir einfach jedes Zeichen an die Text- Eigenschaft des Memo- Feldes. Damit haben wir eine erste Variante fertiggestellt. Wenn wir dieses 1 zu 1 übertragen, werden wir feststellen, dass der Zeilenumbruch verloren geht, wir müssen als das Zeichen '\n' extra behandeln. Außerdem kann man ein char- Zeichen nicht einfach an die Texteigenschaft hängen, dabei werden Umlaute nicht korrekt behandelt, die nationalen Zeichensatzeigenschaften nicht berücksichtet. Anders bei einem Zeichenfeld. Deshalb müssen wir diesen Umweg nutzen, und initialisieren ein Feld mit 2 Zeichen. So entsteht ein erster Entwurf.



#ifndef MyStreamBuf
#define MyStreamBuf

#include <iostream>
#include <Vcl.StdCtrls.hpp>

class MyMemoStreamBuf : public std::streambuf {
   private:
      TMemo* value;
   public:
      MyMemoStreamBuf(TMemo* para) {
         value = para;
         }

      ~MyMemoStreamBuf(void) { value = 0; }

      int overflow(int c) {
         if(c == '\n')
            value->Text = value->Text + "\r\n";
            else {
               char szFeld[2] = { c, 0 };
               value->Text = value->Text + szFeld;
               }
         return c;
         }
   };

Das können wir auch gleich ausprobieren, und erstellen eine VCL- Formularanwendung mit einem Formular, dass neben einem Steuerelement vom Typ TMemo mit dem Namen Memo1 noch den Schalter "Button1" enthält. Außerdem implementieren wir die Ereignisse OnCreate and OnDestroy in dem Formular, um die Standardeingabe zu verändern, bzw. wiederherzustellen. Hier nutze ich der Einfachheit halber einfach eine globale Variable old_cout als Zeiger auf std::streambuf. Das folgende Listing zeigt die Sourcedatei. 


//---------------------------------------------------------------------------

#include <vcl.h>
#pragma hdrstop

#include "MainFormVCL.h"
#include "MyStreamBuf.h"
#include <iomanip>
#include <locale>
using namespace std;
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TfrmMain *frmMain;
//---------------------------------------------------------------------------
__fastcall TfrmMain::TfrmMain(TComponent* Owner) : TForm(Owner) { }

//---------------------------------------------------------------------------
streambuf* old_cout;  // globale Variable um bisherigen Streambuffer zu speichern
void __fastcall TfrmMain::FormCreate(TObject *Sender) {
   old_cout = cout.rdbuf(new MyMemoStreamBuf(Memo1));
   cout.imbue(locale("german_Germany"));
   cout << setiosflags(ios::showpoint) << setiosflags(ios::fixed);
   }

//---------------------------------------------------------------------------
void __fastcall TfrmMain::FormDestroy(TObject *Sender) {
   delete cout.rdbuf(old_cout);
   }

//---------------------------------------------------------------------------
void __fastcall TfrmMain::Button1Click(TObject *Sender) {
   double x = 3.2;
   cout << "Heute ist ein wunderschöner Tag!" << endl
        << "Antwort auf alle Fragen: " << 42 << endl
        << "Pi = " << setprecision(4) << 3.14159265359 << endl;
   cout << "x  = " << setprecision(4) << x << endl;
   cout << "Dieses ist ein Test";
   return;
}

//---------------------------------------------------------------------------


Bevor wir jetzt eine Anpassung für FireMonkey vornehmen, schauen wir uns das ganze noch mal genauer an. Noch würde das meiste auch mit dem klassischen Compiler laufen. Hier gibt es aber ein Problem mit dem +=- Operator für Strings, deshalb der Weg über die Addition auf sich selber. Der Operator "+=" ist wieder etwas, was man in Delphi nicht kennt, so ist ohnehin nicht mit einer Optimierung zu rechnen. Trotzdem wird jetzt jedes Zeichen einzelnd übergeben, der String erweitert. Diese Umsetzung ist also nicht besonders effizent, und bei längeren Texten wird es exponentiell langsamer. Die Lösung ist hier die gepufferte Ausgabe. Dafür verwendet ich einfach ein Element der Klasse std::ostringstream, dieses stellt alle notwendigen Operationen bereit. Außerdem bleibt der bisherige Inhalt in dem Memofeld stehen. Das kann gewollt sein, muss es aber nicht. Allerdings besteht danach keine Möglichkeit mehr, auf dieses Element zuzugreifen. Deshalb ergänze ich im Konstruktor einen boolschen Parameter, den ich per default auf true setze, so dass das Memofeld gelöscht wird. Außerdem nutzen wir jetzt auch die Hilfsfunktionen aus dem Blog Post Konvertierung der Delphi Stringtypen (template, Spezialisierung) für die Konvertierung des C++ Strings zur VCL. Das folgende Listing zeigt die neue Implementierung, die jetzt gepuffert wird.




#ifndef MyStreamBuf
#define MyStreamBuf

#include <MyDelphiHelper.h>

#include <iostream>
#include <sstream>
#include <Vcl.StdCtrls.hpp>

class MyMemoStreamBuf : public std::streambuf {
   private:
      TMemo*             value;
      std::ostringstream os;
   public:
      MyMemoStreamBuf(TMemo* para, bool boClean = true ) {
         value = para;
         if(boClean) value->Lines->Clear();
         }

      ~MyMemoStreamBuf(void) { value = 0; }

      int overflow(int c) {
         if(c == '\n') {
            if(os.str().length() > 0) {
               value->Lines->Add(__ToVCL(os.str()));
               }
            else {
               value->Lines->Add(L"");
               }
            os.str("");
            }
         else {
            os.put(c);
            }
         return c;
         }
   };

Nun werden Sie sicher beim Ausprobieren die Unterschiede bemerken. Der ursprüngliche Text "Memo1" in der obersten Zeile ist verschwunden, wir haben das Feld beim Zuordnen geleert. Allerdings ist auch die letzte Zeile nicht ausgegeben worden. Dadurch das wir nur die mit einem std::endl oder '\n' beendeten Zeilen an das Memofeld übertragen, liegt diese noch in unserem privaten Datenelement "os". Wir müssen bei dieser Implementierung also immer darauf achten, die Ausgaben auch abzuschließen.


Das können wir jetzt für das FMX- Framework anpassen. Da an dieser Stelle die beiden Frameworks eine identische Schnittstelle haben, muss lediglich die Headerdatei <Vcl.StdCtrls.hpp> gegen die FMX- Variante ausgetauscht werden. Hier benutzen wir wieder die Schalter für bedingte Kompilierung, die ich im Post RAII - Mauszeiger für VCL + FMX erstmals benutzt habe. Hier nur der Beginn der Datei, die Klasse ist unverändert.


#ifndef MyStreamBuf
#define MyStreamBuf

#include <MyDelphiHelper.h>

#include <iostream>
#include <sstream>

#if !defined BUILD_WITH_VCL && !defined BUILD_WITH_FMX
   #error Für diese Anwendung muss eine Framework- Variante ausgewählt werden
#endif

#if defined BUILD_WITH_VCL
   #include <Vcl.StdCtrls.hpp>
#endif

#if defined BUILD_WITH_FMX
   #include <Fmx.StdCtrls.hpp>
#endif

Nun schauen wir uns ein Beispiel an, das ursprünglich aus einer Schulung im Jahr 2001 stammt. Ich habe später nur die Header- Dateien dem neuen Standard angepasst. Dafür gibt es eine Headerdatei, die nur den Prototypen der Funktion roll_the_dice() enthält. Und ja, man kann in C++ immer noch strukturiert entwickeln, und das ist auch gut so. Im Gegensatz zu anderen Sprachen. Mit dieser Methode sollen zufällige Würfelergebnisse ausgewertet werden.


#ifndef roll_the_diceH
#define roll_the_diceH

void roll_the_dice(unsigned int iCount, unsigned int iValues = 6);

#endif

Die Methode hat zwei Parameter. Der erste ist die Anzahl der zufälligen Versuche, der zweite die Anzahl der Seiten des Würfels. Dieser ist per default- Value auf 6 gesetzt, aber Dank der Abstraktion könnten auch andere Würfel, oder Münzwürfe simuliert werden.


#include "roll_the_dice.h"

#include <cstdlib>   // for randomize, random
#include <iostream>  // for cout, cerr, endl
#include <vector>    // for vector
#include <algorithm> // for min_element, max_element
#include <iomanip>   // for right, setw, setprecision, setiosflags
#include <exception> // for exception

using namespace std;

void roll_the_dice(unsigned int iCount, unsigned int iValues) {
   randomize();

   cout << "roll the dice ..." << endl
        << "Values: " << iValues << endl
        << "Count:  " << iCount  << endl << endl;
   try {
      vector<int>           vecDiced;
      vecDiced.assign(iValues, 0);
      unsigned int i;

      // roll the dice
      for(i = 0; i < iCount; ++i) vecDiced[random(iValues)]++;
         // print results
         for(i = 0; i < iValues; ++i) {
            cout << setw( 3) << right << i + 1 << setw(10) << right << vecDiced[i]
                 << setw(10) << setprecision(4) << right << (vecDiced[i] * 100.0) / iCount
                 << "%" << endl;
            }

         // determine and print minimum und maximum
         vector<int>::iterator it;
         it = min_element(vecDiced.begin(), vecDiced.end());
         cout << endl
              << "Minimum = " << setw(5) << right << it - vecDiced.begin() + 1 << endl;
         it = max_element(vecDiced.begin(), vecDiced.end());
         cout << "Maximum = " << setw(5) << right << it - vecDiced.begin() + 1 << endl;
   }
   catch(exception &ex) {
      cerr << endl << "c++ error in the function roll_the dice"
           << endl << ex.what() << endl;
   }
   return;
   }

#endif

Dieses können wir nun in ein Programm einbinden. Dafür entwerfen wir ein VCL oder FMX- Programm mit folgendem Hauptformular. Hier gezeigt am FMX- Beispiel, aber in beiden Varianten ist das Beispiel schnell zusammengeklickt. In den Projektoptionen setzen wir den include- Pfad zu unserem Verzeichnis mit den Headerdateien, außerdem definieren wir den passenden Compilerschalter (für FMX ist dieses BUILD_WITH_FMX).



In der Implementierung nutze ich neben meiner Standardausgabe für Memo- Felder auch meine Klasse TMyWait aus dem Post RAII - Mauszeiger für VCL + FMX und die Methoden aus der Headerdatei <MyDelphiHelper.h> (Konvertierung der Delphi Stringtypen). Diese enthalten bereits Unterscheidungen für VCL und FMX. 

Mit Ausnahme der Headerdatei für das Framework <fmx.h>, die für die VCL <vcl.h> ist, und dem Namen der Headerdatei für dieses Formular (hier "MainFormFMX.h") sind beide Varianten identisch.

Allerdings gibt es bei der Lokalisierung in Android Probleme, hier gilt einfach das gesetzte System. Deshalb auch hier wieder ein bisschen bedingte Übersetzung.


//---------------------------------------------------------------------------
#include <fmx.h>
#pragma hdrstop

#include "MainFormFMX.h"
#include "roll_the_dice.h"

#include <MyStreamBuf.h>
#include <MyDelphiHelper.h>
#include <MyForm.h>

#include <iostream>
#include <iomanip>

#ifndef __ANDROID__
   #include <locale>
#endif
using namespace std;
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.fmx"
TfrmMain *frmMain;

streambuf *old_cout, *old_cerr;

//---------------------------------------------------------------------------
__fastcall TfrmMain::TfrmMain(TComponent* Owner) : TForm(Owner) {  }

//---------------------------------------------------------------------------
void __fastcall TfrmMain::FormCreate(TObject *Sender) {
   old_cout = cout.rdbuf(new MyMemoStreamBuf(Memo1));
   cout << setiosflags(ios::showpoint) << setiosflags(ios::fixed);
   old_cerr = cerr.rdbuf(new MyMemoStreamBuf(Memo2));
   #ifndef __ANDROID__
      cout.imbue(locale("german_Germany"));
      cerr.imbue(locale("german_Germany"));
   #endif
   }

//---------------------------------------------------------------------------
void __fastcall TfrmMain::FormDestroy(TObject *Sender) {
   delete cout.rdbuf(old_cout);
   delete cerr.rdbuf(old_cerr);
   }

//---------------------------------------------------------------------------
void __fastcall TfrmMain::Button1Click(TObject *Sender) {
   try {
      TMyWait wait;
      roll_the_dice(__FromVCL(Edit1->Text));
      }
   catch(exception& ex) {
      cerr << "error in the function call:" << endl
           << ex.what() << endl;
      }
   return;
   }

//---------------------------------------------------------------------------


Nun können wir dieses Programm übersetzen, und die fast 20 Jahre alte Methode, damals nach gültigem Standard implementiert, übersetzbar mit jedem beliebigem Standard C++- Compiler, funktioniert genauso, wie zum Erstellungszeitpunkt. Durch einige Vorarbeiten mit Hilfe des C++Builder ist diese jetzt auch für verschiedene Plattformen übersetzbar. Noch funktioniert das ganze auch mit dem klassischen Compiler im C++Builder. Soviel zum gewöhnungsbedürftigen Dialekt. Es zeigt einfach den Vorteil des Industriestandards für C++.

Allerdings ist die Standardbibliothek mit dem Android- Compiler des C++Builder nicht komplett. So wird beim Übersetzen die Headerdatei <cstdlib> angemeckert. Aber vielleicht ist das auch der Zeitpunkt, die in die Jahre gekommene Methode etwas aufzufrischen. Deshalb möchte ich für alle auf Clang basierende Compiler des C++Builder die Erzeugung der Zufallszahlen an den C++11 Standard anpassen. Aber auch nur für diese, so nutze ich hier den Compiler- Schalter __clang__ der in diesem Fall definiert ist. 

So bleibt der Code für den älteren klassischen Compiler lauffähig, und prinzipiell sogar unverändert.


#include "roll_the_dice.h"

#include <iostream>  // for cout, cerr, endl
#include <vector>    // for vector
#include <algorithm> // for min_element, max_element
#include <iomanip>   // for right, setw, setprecision, setiosflags
#include <exception> // for exception

#if defined __clang__
   #include <functional>;
   #include <random>;
#else
   #include <cstdlib>   // for randomize, random
#endif

using namespace std;

void roll_the_dice(unsigned int iCount, unsigned int iValues) {
   #if defined __clang__
      minstd_rand      rand_gen(std::time(static_cast<std::time_t *&rt;(0)));
      uniform_int_distribution<int> _my_uniform(0, iValues - 1);
      function<int()> generateValue = bind(_my_uniform, rand_gen);
   #else
      randomize();
   #endif

   cout << "roll the dice ..." << endl
        << "Values: " << iValues << endl
        << "Count:  " << iCount  << endl << endl;
   try {
      vector<int>           vecDiced;
      vecDiced.assign(iValues, 0);
      unsigned int i;

      // roll the dice
      for(i = 0; i < iCount; ++i) 
         #if defined __clang__
            vecDiced[generateValue()]++;
         #else
            vecDiced[random(iValues)]++;
         #endif

         // print results
         for(i = 0; i < iValues; ++i) {
            cout << setw( 3) << right << i + 1 << setw(10) << right << vecDiced[i]
                 << setw(10) << setprecision(4) << right << (vecDiced[i] * 100.0) / iCount
                 << "%" << endl;
            }

         // determine and print minimum und maximum
         vector<int>::iterator it;
         it = min_element(vecDiced.begin(), vecDiced.end());
         cout << endl
              << "Minimum = " << setw(5) << right << it - vecDiced.begin() + 1 << endl;
         it = max_element(vecDiced.begin(), vecDiced.end());
         cout << "Maximum = " << setw(5) << right << it - vecDiced.begin() + 1 << endl;
   }
   catch(exception &ex) {
      cerr << endl << "c++ error in the function roll_the dice"
           << endl << ex.what() << endl;
   }
   return;
   }

#endif


Ich hoffe, dass ich mit diesem Beispiel zeigen konnte, dass ein bisschen Vorarbeit hilft, nicht nur plattformunabhängige Programme zu erstellen, sondern auch historischen oder wie einige sagen, legacy Code weiterzuverwenden. Nur ist es ja eben eine der Ideen hinter C++ den "historischen" Code und damit riesige Investitionen zu schützen, anstatt jedem Hype zu folgen. Sie haben jetzt jedenfalls ihr erstes Android- Programm erstellt, und das nur, weil sie im C++Builder die Zielumgebung gewählt haben. Wenn Sie jetzt ein Gerät an ihren Rechner angeschlossen haben, sollte die Umgebung diese auch automatisch auf diesem installieren (passende Entwickleroptionen vorausgesetzt). Und entgegen der Behauptung in dem Artikel "Eine IDE, sie zu knechten" (Heise c't 10/2018)" ist der Dialekt des C++Builder hier auch nicht ungewöhnlich, und wie andere Vergleiche des Autors aus der Luft gegriffen. Und die sehen, dass sauber geschriebene Funktionen, selbst wenn sie 20 Jahre alt sind, mit kleinen Änderungen immer nach funktionieren. Welchem Ziel der Autor (oder die c't) auch immer folgte, so dass er native Compiler in der Bewertung herabsetzen wollte, sehr einseitig und zudem schlecht recherchiert schlussfolgerte, um Script- Frameworks als Lösung zu präsentieren, kann sicher nur der Autor beantworten.

Wir haben aber auch den Werkzeugkasten, mit dem wir unsere Programme an verschiedene Situationen anpassen können. Wir müssen es eben nur lernen, und verantwortungsbewusst mit unseren Möglichkeiten umgehen. Dazu sei gesagt, dieser Blog ersetzt nicht, dass sie ein Buch kaufen, und sich noch einmal auf einmal "auf die Schulbank" setzen. Er soll auch keine Seminare und Schulungen ersetzen. Aber auch nach vielen Jahren wird C++ nicht langweilig.
;