Montag, 21. Mai 2018

Listen in FMX (StringGrid)

Listen in FMX, Nutzung des StringGrids

Im letzten Post ging es um die Nutzung von ListView- Steuerelementen der VCL als Standard- Stream für C++. Nun gibt es im FMX- Framework keine Unterstützung für das Windows- Steuerelement und es braucht eine andere Lösung. Dieses kann ein StringGrid sein.



Neuer Typ für die Spaltenausrichtung

Bevor ich mit diesem beginne, sortiere ich die bisherige Lösung um. Ich habe den obersten Teil der Headerdatei "MyStreamBuf.h" noch einmal komplett übernommen, um einen Abgleich zu ermöglichen.  



#ifndef MyStreamBuf
#define MyStreamBuf

#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>
   #include <FMX.Grid.hpp>
   #include <FMX.Grid.Style.hpp>
#endif

#include <MyDelphiHelper.h>

#include <iostream>
#include <sstream>
#include <stdexcept>
#include <vector>
#include <tuple>
#include <map>

enum class EMyAlign : int { undefined, left, center, right, unknown };
using tplList = std::tuple<std::string, EMyAlign, int>;

Da die Listenspalten im bisherigen Ansatz mit Hilfe einen Tuples aufgebaut wurden, in dem der VCL- Aufzählungstyp TAlignment für die Spaltenausrichtung verwendet wurde, den es in FMX so nicht gibt, bzw. der da anders aufgebaut ist, definiere ich mir zuerst eine eigene Aufzählung EMyAlign mit möglichen Spaltenausrichtungen. Mit Hilfe von diesem definiere ich den Tuple- Typ tplList zur Definition von Listenspalten neu.


Da ein wichtiges Element der Implementierung in der overfflow()- Methode liegt, und hier neben dem Zeichen '\n' auch eine Behandlung des Zeichens '\t' notwendig ist, definiere ich eine neue Basisklasse MyListStreamBufBase für alle Implementierungen von Listendarstellungen. Hier wird die overflow()- Methode zentral umgesetzt.

Da die jeweilige Zeilenschaltung nicht zentral ist, wird eine neue pure virtuelle Methode NewLine() definiert, die in den jeweils konkreten Klassen implementiert werden muss.


class MyListStreamBufBase : public MyStreamBufBase {
   public:
     MyListStreamBufBase(void) { }
     virtual ~MyListStreamBufBase(void) { }

     virtual int overflow(int c) {
       switch(c) {
         case '\n':
            Write();
            NewLine();
            break;
         case '\t':
            Write();
            break;
         default:
            os.put(c);
         }
       return c;
       }

     virtual void NewLine(void) = 0;
   };

Anpassungen der bisherigen Implementierung für VCL- TListView

Nun müssen einige Änderungen der bestehenden Klasse und Hilfsmethoden durchgeführt werden. Als erstes muss Methode AddColumns für die ListViews neu implementiert werden. 

inline void AddColumns(TListView* lv, std::vector<tplList> const& captions) {
   static std::map<EMyAlign, TAlignment> Align2 = {
                    { EMyAlign::undefined, taLeftJustify },
                    { EMyAlign::left,      taLeftJustify },
                    { EMyAlign::center,    taCenter },
                    { EMyAlign::right,     taRightJustify },
                    { EMyAlign::unknown,   taLeftJustify } };

   for(auto const& caption : captions) {
     TListColumn* nc = lv->Columns->Add();
     nc->Caption   = std::get<0>(caption).c_str();
     nc->Alignment = Align2[std::get<1>(caption)];
     nc->Width     = std::get<2>(caption);
     }

   }

Dafür definiere ich eine statische Variable mit einem assoziativen Array, in dem ich jedem Wert aus meinem vorher definierten EMyAlign - Aufzählung einen dazu passenden Wert aus der VCL- Aufzählung TAlignment zuordne. Dieses nutze ich in der Schleife zum Zuweisen der neuen zur Framework- abhängigen Aufzählung zum konkreten Wert. So habe ich in einem einfachen Schritt die Abhängigkeit von einem konkreten Framework eliminiert.

Abschließend muss ich die bisherige Klasse TMyListViewStreamBuf für die VCL anpassen. Dafür wird die bisherige Basisklasse ausgetauscht, die bisherige Methode overflow() wird hier gestrichen und stattdessen wird der Zeilenvorschub durch die neue virtuelle Methode NewLine() durchgeführt, die hier implementiert wird.


class MyListViewStreamBuf : public MyListStreamBufBase {
   private:
     TListView*  lvValue;
     TListItem*  lvItem;
     bool boNewItem;
   public:
     MyListViewStreamBuf(TListView* para, bool boClean = true) : MyListStreamBufBase() {
       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 void NewLine(void) { boNewItem = true; }

     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;
       }
   };


Implementierung für FMX- StringGrid

Damit kann ich mich jetzt um die Implementierung für StringGrids als Ausgabe für C++ Standard- Streams kümmern. Dazu füge ich den folgenden Quelltext in die Headerdatei "MyStreamBuf.h" ein und nutze hier auch wieder die bedingte Übersetzung.

class MyListViewStreamBuf : public MyListStreamBufBase {
   private:
     TStringGrid*  lvValue;
     int iColumn, iRow;
   public:
     MyListViewStreamBuf(TStringGrid* para, bool boClean = true) : MyListStreamBufBase() {
       lvValue = para;
       iColumn    = 0;
       iRow       = 0;
       lvValue->ReadOnly = true;

       lvValue->Options <<= TGridOption::RowSelect;
       lvValue->Options >>= TGridOption::ColumnMove;
       lvValue->Options <<= TGridOption::AlwaysShowSelection;

       if(boClean) lvValue->RowCount = 0;
       }


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

     virtual void NewLine(void) { iColumn = 0; }

     virtual void Write(void) {
       if(iColumn == 0) {
         lvValue->RowCount += 1;
         iRow = lvValue->RowCount - 1;
         }
       lvValue->Cells[iColumn++][iRow] = os.str().c_str();
       os.str("");
       return;
       }
   };

Die Umsetzung erfolgt mit Hilfe des privaten Datenelements lvValue vom Typ eines Zeigers auf ein TStringGrid. Dazu kommt eine Ganzzahl mit der aktuellen Spaltennummer. Diese werden im Konstruktor als Parameter übergeben, und die Spaltennummer auf 0 gesetzt. Dabei bedeutet die 0 eine neue leere Zeile, der Wert zeigt also später immer die Anzahl der schon zugefügten Spalten in einer neuen Zeile. Außerdem gibt es eine Variable mit der aktuellen Anzahl der Zeilen, die für die Write()- Methode genutzt wird. Auch diese wird im Konstruktur mit 0 initialisiert. Dann folgen noch einige Einstellungen für das StringGrid, um ein einheitliches Aussehen als Liste zu gewährleisten. Da es nur um eine Ausgabe geht, wird das Stringgrid auf nur lesen gesetzt, außerdem wird immer die ganze Zeile ausgewählt, und auch bei nicht aktivem Fokus angezeigt. Außerdem verhindere ich die Verschiebung von Spalten zur Laufzeit, um eine fehlerhafte Ausgabe zu gewährleisten.

Im Destruktur wird der Zeiger auf das konkrete Datenelement auf 0 gesetzt, nicht nur um ein Löschen zu verhindern, sondern auch zur Vereinfachung eines Reviews. Jeder wird erkennen, dass hier keine Löschung erfolgen darf.

In der Methode NewLine() wird lediglich die Spaltennummer iColumn auf 0 gesetzt.

Die eigentliche Ausgabe in das konkrete StringGrid- Steuerelement erfolgt auch hier in der virtuellen Write()- Methode durchgeführt. Wenn die Spaltennummer iColumn == 0 ist, wird durch den Zugriff auf die Eigenschaft RowCount des Steuerelements eine neue Zeile eingefügt. Nachdem dieses passiert ist, wird es in der Varaible iRow abgelegt. Im zweiten Absatz erfolgt über die Cells- Eigenschaft die eigentliche Ausgabe. Mit dem Postfix Inkrement- Operator wird die Spaltennummer nach der Ausgabe erhöht. Anschließend wird der Buffer für die neue Spalte geleert. 

Auch hier brauchen wir eine Hilfsmethode AddColumns() zum Aufbau der Spalten. Dabei wird ebenfalls ein konkretes Steuerelement als erster Parameter übergeben, gefolgt von einem Vektor mit unseren, durch Einführung der Aufzählung EMyAlign plattformunabhängigen Spaltendefinitionen.


inline void AddColumns(TStringGrid* lv, std::vector<tplList> const& captions) {
   static std::map<EMyAlign, TTextAlign> Align2 = {
                    { EMyAlign::undefined, TTextAlign::Leading },
                    { EMyAlign::left,      TTextAlign::Leading },
                    { EMyAlign::center,    TTextAlign::Center },
                    { EMyAlign::right,     TTextAlign::Trailing },
                    { EMyAlign::unknown,   TTextAlign::Leading } };

   for(auto const& caption : captions) {
     TStringColumn* col = new TStringColumn(lv);
     //col->TextSettings->HorzAlign = Align2[std::get<1>(caption)];
     col->Header = std::get<0>(caption).c_str();
     col->Width  = std::get<2>(caption);
     lv->AddObject(col);
     }
   }

Leider verändert sich das FMX- Framework aktuell noch. So werden Eigenschaften verschoben, der Zugriff ist teilweise durch spezielle, der COM- Syntax folgenden Schnittstellen etwas gewöhnungsbedürftig. Dieses trifft in der aktuellen Version des C++Builder 10.2 leider die Spaltenausrichtung im Grid. Ich habe das einfach auskommentiert, da es zur Darstellung des Vorgehens nicht notwendig ist. Gerade diese ständigen Veränderungen in Frameworks sind ja zentrale Begründung für die Kapslung dieser Frameworks von ihrem Business- Quellcode und wird hier gezeigt.


Als letztes wird die spezielle Activate()- Methode für das template in der Klasse TMyStreamWrapper ergänzt.

#if defined BUILD_WITH_FMX
template<>
inline void TMyStreamWrapper::Activate<TStringGrid>(TStringGrid* element) {
   Check();
   old = str.rdbuf(new MyListViewStreamBuf(element));
   }
#endif


;