VCL ListViews as Standard C++ Stream

additional paragraph, normally not visble

 

 

Some of the previous posts were about using GUI elements for the standard C++ streams. Besides the memo field, these were labels and edit fields. And for the VCL also the status line. I'm sure you can build some interesting applications with this, but I'd like to go a step further and use a ListView control. This control is only available for Windows, and therefore only for the VCL. For FMX you could implement a similar wrapper for a StringGrid, and thus emulate the function of a ListView. But not to use the original control under Windows would be stupid. Compatibility is not abandonment but searching for suitable abstractions. Actually, the previous posts should have already shown what powerful abstractions are available to us as C++ programmers, and the renunciation of possibilities is certainly not one of them.

But back to an implementation for using a listview, I just want to define an additional separator to switch from one list column to the next. This can be a tabulator, but also a ";"- character would be conceivable. Let's start and look at the following listing. Again a conditional translation, as this can only be available when using the VCL. Even if I will have to make my own implementation for the overflow() method later, I will use my base class "MyStreamBufBase", which was introduced in the posting Additional GUI elements and C++ streams. So I can use the std::ostringstream "os" defined in this, but I also have to implement the pure virtual method Write().

 

Link to Additional GUI elements and C++ streams if finished


#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

Additionally we need three data elements. This is a pointer to TListView "lvValue", this corresponds to the whole TListView in the display (i.e. the table with headings and data) and is passed as parameter in the constructor. Additionally a pointer to a TListItem "lvItem", this corresponds to a row within the table, and the boolean flag "boNewItem", which indicates whether the stream is currently at the beginning of a new row or in the middle of it.

In the constructor, the corresponding concrete TListView element is taken as a pointer, as well as the parameter "boClean", which, as seen in the other implementations, determines whether the previous content should be deleted. Here also the element "lvItem" is set to 0, and the boolean flag "boNewItem" to true, to create the controlled initial state. The purpose of constructors is to guarantee the inner state of an instance, so I use the properties of TListView ViewStyle = vsReport and RowSelect = true to create a defined initial state for my tables (display as table, always select the whole row, caption not editable). If the 2nd parameter is passed with true, the previous, possibly existing data rows will be deleted.

The destructor is quickly finished, here the two pointers "lvValue" and "lvItem" are set to 0. In general, this is compatible to C++98, otherwise it is better to use nullptr. The data element remains part of the VCL interface and must not be deleted.

In the method overflow() I have to find the special characters. Besides the character '\n' for the line break, this is the character '\t' for the column break. I have nested two simple selection commands (if) one behind the other, alternatively a multiple selection (switch) would be conceivable here. Thanks to the current developments in programming there are more elegant solutions today, I will take up exactly this topic again in a later post. But back to the topic.


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

Again, the Write() method does the actual work and writes the buffered contents to the table. Only the line break is done by setting the variable "boNewitem" to true also in the overflow() method. 

The actual work is also done by the Write() method. This is not really magic. If the flag "boNewItem" is true, a new item is created with the help of the VCL control (lvValue->Items->Add()) and the content from the buffer is written into the property Caption. Afterwards the property "boNewItem" is set to false. But if "boNewItem" is already false, a new column with the content of the buffer is appended to the item with the method lvItem->SubItems->Add(). In both cases the content of the buffer is then emptied and the text for the next column is collected.

Finally we add again a suitable method for template specialization in our class "TMyStreamWrapper".


#if defined BUILD_WITH_VCL

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

#endif

Finally, I define a new type as tuple for the column description within the conditional translation for the VCL. The first element defines a column heading, the second the alignment (attention, here dependence to the VCL type "TAlignment") and the third the column width. In addition, there is a helper method AddColumns() for setting the columns in the list view, to which I pass a vector with these column definitions next to the pointer to the concrete element of type TListView. Here, the possibly existing columns are first deleted before the new columns are built using the vector and the tuple.


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

   }

This concludes the preparations. Let us look at the following test program. With the help of the VCL and the IDE I have created the following main window. It consists of 2 panels, the ListView element and a memo field, the status bar and the button. Between the ListView and the memo field there is a splitter, all three are arranged together on the 2nd panel. On the first panel there is a button to execute the action. For the memo field I set the font to "Courier New", a font with fixed character spacing. The list view is simply stored, with the Align = alClient.

The following listing shows the test program. Here cout is mapped to the list view, cerr to the memo field and clog to the status line. In the FormCreate() method, the standard streams are redirected, some settings for the streams are made, and the list columns are built in the table. I use the new C++11 initialization syntax. I also define a helper structure "TMyNum" for the localization of the streams, which is derived from numpunct<char> and is used for output control.

 


//---------------------------------------------------------------------------
#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) { }

//---------------------------------------------------------------------------
// settings for german language
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;
   }

The method btnActionClick() is the actual test program. This consists again of 100% of standard C++ and can be compiled with any appropriate C++ compiler on any conceivable platform. Therein lies the real quality of a software architect to develop suitable abstractions in order to write later reusable and portable code. This cannot be achieved by any framework.

Also in this code I use a vector with tuple for simplicity, which contains the data to be displayed and evaluated in the table. The usual German comma and the thousand point is used. In the stream cerr the summary is done, the stream clog is used to log the information and cout for the output of the values. The following picture shows the successful output.

Kommentar verfassen

* These fields are required

Kommentare

Keine Kommentare

About the Author

 


Über den Autor

Volker Hillmann

CEO, Developer, Germany, Berlin
Volker Hillmann was born in 1965 and holds a degree in mathematics with a focus on databases and data security. He has been programming in C since 1988. After first touches on a Unix machine with Turbo C 1.5 on PCs. That's how he got to know C++ in 1991 and since then he is programming in different areas with C++. After some experience in the insurance and banking industry, he founded adecc Systemhaus GmbH in 2000, of which he is still the CEO. He is also MVP at embarcadero Germany.

More posts by this author