Sonntag, 13. Mai 2018

VCL ListViews als Standard C++ Stream

VCL ListViews als Standard C++ Stream

In einigen der vorherigen Posts ging es um die Verwendung von GUI- Elementen für die C++- Standard- Streams. Neben dem Memofeld waren das auch Labels und Editfelder. Und für die VCL auch die Statuszeile. Damit lassen sich sicher schon einige interessante Anwendungen zusammenbauen, aber ich möchte jetzt noch einen Schritt weiter gehen, und ein ListView- Steuerelement verwenden. Dieses gibt es in der Form nur unter Windows, und damit auch nur für die VCL. Für FMX könnte man einen ähnlichen Wrapper für ein StringGrid implementieren, und damit die Funktion eines ListViews nachbilden. Aber das originale Steuerelement unter Windows nicht zu verwenden wäre sicherlich dumm. Kompatibilität ist nicht Verzicht sondern das Suchen geeigneter Abstraktionen. Eigentlich sollten die bisherigen Posts schon gezeigt haben, welche mächtigen Abstraktionen uns als C++- Programmierer zur Verfügung stehen, und das Verzicht auf Möglichkeiten sicher nicht zu diesen gehört.


Aber zurück zu eine Implemetierung zur Nutzung eines Listview, ich möchte hier einfach ein zusätzliches Trennzeichen definieren, um von einer Listenspalte zur nächsten zu wechseln. Das kann ein Tabulator sein, aber auch ein ";"- Zeichen wäre denkbar. Beginnen wir und schauen uns das folgende Listing an. Auch hier wieder eine bedingte Übersetzung, da dieses nur bei Nutzung der VCL verfügbar sein kann. Auch wenn ich später eine eigene Implementierung für die overflow() Methode machen muss, nutze ich meine im Posting Weitere GUI- Elemente und C++- Streams eingeführte Basisklass "MyStreamBufBase". Ich kann also den in dieser definierten std::ostringstream "os" verwenden, muss aber auch die pure virtuelle Methode Write() implementieren. 

#ifdef BUILD_WITH_VCL

class MyListViewStreamBuf : public MyStreamBufBase {
   private:
     TListView*  lvValue;
     TListItem*  lvItem;
     bool boNewItem;
   public:
     MyListViewStreamBuf(TListView* para, bool boClean = true) : MyStreamBufBase() {
       lvValue = para;
       lvItem    = 0;
       if(boClean) lvValue->Items->Clear();
       lvValue->ViewStyle = vsReport;
       lvValue->RowSelect = true;

       boNewItem = true;
       }


     virtual ~MyListViewStreamBuf(void) {
       lvValue = 0;
       lvItem  = 0;
       }

     virtual int overflow(int c) {
       if(c == '\n') {
         Write();
         //lvItem->MakeVisible(false);
         boNewItem = true;
         }
       else {
         if(c == '\t') {
            Write();
            }
         else {
            os.put(c);
            }
         }
       return c;
       }

     virtual void Write(void) {
       if(boNewItem) {
         lvItem = lvValue->Items->Add();
         lvItem->Caption = os.str().c_str();
         boNewItem = false;
         }
       else {
         lvItem->SubItems->Add(os.str().c_str());
         }

       os.str("");
       return;
       }

   };

#endif

Zusätzliche benötigen wir drei Datenelemente. Das ist ein Zeiger auf TListView "lvValue", dieses entspricht dem gesamten TListView in der Anzeige (also der Tabelle mit Überschriften und Daten) und wird im Konstruktor als Parameter übergeben. Dazu ein Zeiger auf einen TListItem "lvItem", dieses entspricht einer Zeile innerhalb der Tabelle, sowie das boolsche Flag "boNewItem", das angibt ob sich der Stream gerade am Anfang einer neuen Zeile befindet, oder mitten in dieser.

Im Konstruktor wird das entsprechende konkrete TListView- Element als Zeiger übernommen, außerdem der Parameter "boClean", der, wie in den anderen Implementierungen schon gesehen, bestimmt, ob der bisherige Inhalt gelöscht werden soll. Hier wird auch das Element "lvItem" auf 0 gesetzt, und das boolsche Flag "boNewItem" auf true, um den kontrollierten Anfangszustand herzustellen. Der Sinn von Konstruktoren ist es ja, den inneren Zustand einer Instanz zu gewährleisten, deshalb stelle ich mit den Eigenschaften des TListView ViewStyle = vsReport und RowSelect = true einen definierten Ausgangszustand für meine Tabellen her (Anzeige als Tabelle, immer die ganze Zeile auswählen, Caption nicht editierbar). Sollte der 2. Parameter mit true übergeben werden, werden die bisherigen, eventuell vorhandenen Datenzeilen gelöscht.

Der Destrukor ist schnell fertig, hier werden die beiden Zeiger "lvValue" und "lvItem" auf 0 gesetzt. Generell gilt hier wieder eine Kompatibilität zu C++98, anderenfalls sollte besser nullptr verwendet werden. Das Datenelement bleibt Bestandteil der VCL- Oberfläche, und darf nicht gelöscht werden.

In der Methode overflow() muss ich die speziellen Zeichen heraussuchen. Neben dem Zeichen '\n' für den Zeilenwechsel ist dieses hier das Zeichen '\t" für den Spaltenwechsel. Ich habe hier zwei einfache Auswahl- Befehle (if) hintereinander geschachtelt, denkbar wäre hier auch alternativ eine Mehrfachauswahl (switch). Dank der aktuellen Entwicklungen in der Programmierung gibt es heute aber elegantere Lösungen, ich werde genau dieses Thema in einem späteren Post noch einmal aufgreifen. Aber zurück zum Thema.

       switch(c) {
         case '\n':
            Write();
            boNewItem = true;
            break;
         case '\t':
            Write();
            break;
         default:
            os.put(c);
         }

Auch hier übernimmt die Write()- Methode die eigentliche Arbeit und schreibt die gepufferten Inhalte in die Tabelle. Nur der Zeilenumbruch wird durch das Setzen der Variable "boNewitem" auf true auch in der overflow()- Methode durchgeführt. 

Die eigentliche Arbeit übernimmt auch hier die Write()- Methode. Dabei ist auch das eigentlich keine Magie. Wenn das Kennzeichen "boNewItem" true ist, wird eine neues Item mit Hilfe des VCL- Steuerelements erzeugt (lvValue->Items->Add()) und der Inhalt aus dem Puffer in die Eigenschaft Caption geschrieben. Danach wird die Eigenschaft "boNewItem" auf false gestellt. Falls "boNewItem" aber schon false ist, wird an das Item mit der Methode lvItem->SubItems->Add() eine neue Spalte mit dem Inhalt des Puffers angehängt. In beiden Fällen wird anschließend der Inhalt des Puffers geleert und den Text für die nächste Spalte zu sammeln.

Abschließend ergänzen wir wieder eine passende Methode zur template- Spezialisierung in unser Klasse "TMyStreamWrapper".

#if defined BUILD_WITH_VCL

template<>
inline void TMyStreamWrapper::Activate<TListView>(TListView* element) {
   Check();
   old = str.rdbuf(new MyListViewStreamBuf(element));
   }

#endif

Abschließend definiere ich mir innerhalb der bedingten Übersetzung für die VCL einen neuen Typ als tuple für die Spaltenbeschreibung. Dabei definiert das erste Element eine Spaltenüberschrift, das zweite die Ausrichtung (Achtung, hier Abhängigkeit zum VCL- Typ "TAlignment") und das dritte die Spaltenbreite. Dazu kommt eine Hilfsmethode AddColumns() zum Setzen der Spalten im Listview, dem ich neben dem Zeiger auf das konkrete Element vom Typ TListView einen vector mit eben diesen Spaltendefinitionen übergebe. Hier werden erst die eventuell vorhandenen Spalten gelöscht, bevor die neuen Spalten mit Hilfe des Vektors und der tuple aufgebaut werden.

using tplList = std::tuple<std::string, TAlignment, int>;

inline void AddColumns(TListView* lv, std::vector<tplList> const& captions) {
   lv->Columns->Clear();
   for(auto const& caption : captions) {
     TListColumn* nc = lv->Columns->Add();
     nc->Caption   = std::get<0>(caption).c_str();
     nc->Alignment = std::get<1>(caption);
     nc->Width     = std::get<2>(caption);
     }

   }

Damit sind die Vorbereitungen abgeschlossen. Schauen wir uns dafür folgenden Testprogramm an. Mit Hilfe der VCL und der IDE habe ich folgendes Hauptfenster zusammengestellt. Es besteht aus 2 Panels, dem ListView- Element und einem Memofeld, der Statuszeile und dem Schalter. Zwischen dem ListView und dem Memofeld befindet sich ein Splitter, alle drei sind zusammen auf dem 2. Panel angeordnet. Auf dem ersten Panel befindet sich ein Schalter zum Ausführen der Aktion. Für das Memofeld habe ich die Schriftart auf "Courier New" gesetzt, eine Schriftart mit fixem Zeichenabstand. Das Listview ist einfach nur abgelegt, mit dem Align = alClient.



Das folgende Listing zeigt das Testprogramm. Hier werden cout auf das Listview, cerr auf das Memofeld und clog auf die Statuszeile umgelegt. In der FormCreate()- Methode werden die Standard- Streams umgelenkt, einige Einstellungen für die Streams vorgenommen, und die Listenspalten in der Tabelle aufgebaut. Dabei nutze ich die neue C++11 Initialisierungssyntax. Außerdem definiere ich für die Lokalisierung der Streams eine Hilfsstruktur "TMyNum", die von numpunct<char> abgeleitet ist und für die Ausgabesteuerung genutzt wird.

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

#include "MyStreamBuf.h"

#include "MainFormListVCL.h"
#include <iostream>
#include <iomanip>
#include <string>
using namespace std;
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"

TfrmMain *frmMain;
TMyStreamWrapper old_cout(cout), old_cerr(cerr), old_clog(clog);
//---------------------------------------------------------------------------
__fastcall TfrmMain::TfrmMain(TComponent* Owner) : TForm(Owner) { }

//---------------------------------------------------------------------------
struct TMyNum : public numpunct<char> {
   char do_decimal_point ()    const { return ','; }
   char do_thousands_sep ()    const { return '.'; }
   string_type do_truename ()  const { return "ja"; }
   string_type do_falsename () const { return "nein"; }
   std::string do_grouping ()  const { return "\3";   }
};

TMyNum newNumPunct;

void __fastcall TfrmMain::FormCreate(TObject *Sender) {
   AddColumns(ListView1, {
           tplList { "Nummer",     taRightJustify, 100 },
           tplList { "Anrede",     taLeftJustify,  120 },
           tplList { "Vorname",    taLeftJustify,  220 },
           tplList { "Name",       taLeftJustify,  220 },
           tplList { "Umsatz",     taRightJustify, 170 },
           tplList { "Prov.-Satz", taCenter,       120 },
           tplList { "Provision",  taRightJustify, 170 } } );

   old_cout.Activate(ListView1);
   old_cerr.Activate(Memo1);
   old_clog.Activate(StatusBar1);
   locale loc(locale("de_DE"), &newNumPunct);
   cout.imbue(loc);
   cout << setiosflags(ios::fixed);
   cerr.imbue(loc);
   cerr << setiosflags(ios::fixed);

   clog << "Programm bereit." << endl;
   }
//---------------------------------------------------------------------------

void __fastcall TfrmMain::btnActionClick(TObject *Sender) {
   using tplData = tuple<string, string, string, double, double>;
   char tab = '\t';
   vector<tplData> datas = {
       tplData { "Herr", "Gustav", "Meyer",      0.20,  223134.50 },
       tplData { "Herr", "Bernd",  "Karenbauer", 0.15,  718452.00 },
       tplData { "Frau", "Lena",   "Wagner",     0.25,  526784.50 },
       tplData { "Herr", "Hans",   "Holle",      0.20,  235120.50 },
       tplData { "Frau", "Conny",  "Lehmann",    0.30,  128720.00 },
       tplData { "Herr", "Jens",   "Xanten",     0.15,  925721.50 },
       tplData { "Frau", "Anne",   "Schmidt",    0.20,  130312.00 },
       tplData { "Frau", "Maria",  "Lohmen",     0.10, 1245372.00 } };

   double flSales = 0.0, flShares = 0.0;
   int    iCount = 0;
   for(auto data : datas) {
     double flShare = get<3>(data) * get<4>(data);
     flSales  += get<4>(data);
     flShares += flShare;
     cout << ++iCount << tab << get<0>(data) << tab
         << get<1>(data) << tab << get<2>(data) << tab
         << setprecision(2) << get<4>(data) << tab
         << setprecision(0) << get<3>(data) * 100 << "%" << tab
         << setprecision(2) << flShare << endl;
     }
   clog << "Daten ausgegeben." << endl;
   cerr << "Ingesamt " << iCount << (iCount != 1 ? " Sätze" : " Satz")
      << " ausgegeben" << endl
      << setw(15) << left << "Umsatz: "
      << setw(15) << right << setprecision(2) << flSales << endl
      << setw(15) << left  << "Provision:"
      << setw(15) << right << setprecision(2) << flShares << endl;
   }

Die Methode btnActionClick() ist das eigentliche Testprogramm. Dieses besteht wieder zu 100% aus Standard C++, ist als mit jedem entsprechenden C++- Compiler auf jeder denkbaren Plattform übersetzbar. Darin liegt die eigentliche Qualität eines Softwarearchitekten, geeignete Abstraktionen zu entwickeln, um später wiederverwendbare und portablen Code zu schreiben. Das kann von keinem Framework geleistet werden.

Auch in diesem Code bediene ich mich der Einfachheit halber eines Vektors mit tuple, die die Daten enthalten, die in der Tabelle angezeigt und ausgewertet werden sollen. Dabei wird das im deutschen übliche Komma und der Tausender- Punkt verwendet. Im Stream cerr erfolgt die Zusammenfassung, der Stream clog dient zum loggen der Informationen und cout für die Ausgabe der Werte. Das folgende Bild zeigt die erfolgreiche Ausgabe.


;