Use of standard streams for output in memo fields

Sensible use of the VCL / FMX with C++

additional paragraph shouldn't visible

 

 

Use of standard streams with VCL and FMX

I hear again and again that most of the C++ books and examples you can find on the internet would not work with C++Builder in particular, but also with C++ in general nowadays. They would use the standard streams, and they would not work with a graphical user interface, be it Windows, Android or even IOS. And who would then still write applications for the console or command prompt? This would very quickly show the general weakness of C++, that it is actually from the past, that it is no longer modern. But for me it shows only one thing. Namely, that many have not understood the C++ streams yet.

Most of them may still manage to write input and output operators for their own data types, but in general the streams do not seem to be popular with many C++ programmers. But they offer us many possibilities, and work from both sides. We can independently define operators that use the streams to output or read our own data types. But we can also make changes at the base. So we can determine where (or from where) the data is written or read. When using such modified streams, we often speak of redirecting, some call it "bending". I will do this for the standard output, in the first step I will use a memo field of the VCL to make the class available for FMX again. I will also write a wrapper that takes control of any stream via RAII and offers functions to "bend" it. But at the end of the lifetime the previous state will be restored in any case. 

In a later blog post I will use other useful controls like the status bar for logging with clog, or even a listview control for list output. Since listview controls only exist in Windows in the form and with the VCL we will simply use this in a StringGrid and FMX.

When using the stream classes, it is important to understand that there is a strict distinction between the national character set (narrow, char) and the international character set (wide, wchar_t). Here, nothing is changed arbitrarily, as we have seen with the example of the Delphi string types from version 2009 on, so you have to make a conscious decision yourself, or just implement for all variants. I only want to look at the national characters here, if you want to work with international characters in your streams, it is surely easy to extend it by yourself. And even today the national character set should be sufficient for most cases. 

To define a new output option for a stream, we have to define a new class as a derivation of the class std::streambuf from the C++- standard class library. This class can be found in the header file <iostream>. Memo fields are addressed in the VCL via the class TMemo from the header file <Vcl.StdCtrl.hpp>. We take a pointer to these as a private data element and use it as a parameter for the constructor.

Now we would only have to overwrite the inherited method Overflow. To do this, we simply append each character to the text property of the memo field. With this we have completed a first variant. If we transfer this 1 to 1, we will notice that the line break is lost, we have to treat it as the character '\n' extra. Furthermore, you cannot simply append a char character to the text property, umlauts are not handled correctly, the national character set properties are not taken into account. This is different with a character field. So we have to use this detour and initialize a field with 2 characters. Thus a first draft is created.



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


We can also try this out and create a VCL form application with a form that contains a TMemo type control named Memo1 and the button "Button1". We also implement the OnCreate and OnDestroy events in the form to change or restore the standard input. For simplicity's sake I simply use a global variable old_cout as pointer to std::streambuf. The following listing shows the source file. 
 



#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;  // global variable to store previous stream buffer
void __fastcall TfrmMain::FormCreate(TObject *Sender) {
   old_cout = cout.rdbuf(new MyMemoStreamBuf(Memo1));
   cout.imbue(locale("german_Germany")); // only for national code, this for germany, alternate de_DE
   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 << "Today is a beautiful day!" << endl
        << "Answer to all questions: " << 42 << endl
        << "Pi = " << setprecision(4) << 3.14159265359 << endl;
   cout << "x  = " << setprecision(4) << x << endl;
   cout << "This is a test";
   return;
}

Now before we make an adaptation for FireMonkey, let's take a closer look at the whole thing. Most of it would still work with the classic compiler. But here there is a problem with the +=- operator for strings, that's why the way over the addition on itself. The operator "+=" is again something you don't know in Delphi, so you can't expect an optimization anyway. Nevertheless, every character is now passed individually, the string is extended. So this implementation is not very efficient, and with longer texts it gets exponentially slower. The solution here is buffered output. I simply use an element of the class std::ostringstream, which provides all necessary operations. In addition, the previous content remains in the memo field. This can be desired, but it does not have to be. However, there is no possibility to access this element afterwards. Therefore I add a boolean parameter in the constructor, which I set to true by default, so that the memo field is deleted. In addition, we now use the helper functions from the blog post Conversion of Delphi String Types (template, specialization) for the conversion of the C++ string to the VCL. The following listing shows the new implementation which is now buffered.


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

Now you will certainly notice the differences when trying it out. The original text "Memo1" in the top line has disappeared, we have emptied the field while assigning. However, the last line has not been output either. Because we only transfer the lines ended with std::endl or '\n' to the memo field, it is still in our private data element "os". So with this implementation we always have to make sure that the output is completed.


We can now adapt this for the FMX framework. Since the two frameworks have an identical interface at this point, only the header file <Vcl.StdCtrls.hpp> must be exchanged for the FMX variant. Here we use again the switches for conditional compilation, which I used for the first time in Post RAII - mouse pointer for VCL + FMX. Here only the beginning of the file, the class is unchanged.


#ifndef MyStreamBuf
#define MyStreamBuf

#include <MyDelphiHelper.h>

#include <iostream>
#include <sstream>

#if !defined BUILD_WITH_VCL && !defined BUILD_WITH_FMX
   #error A framework variant must be selected for this application
#endif

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

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

Now let's look at an example that originally came from a training course in 2001. I later only adapted the header files to the new standard. For this purpose there is a header file which contains only the prototype of the function roll_the_dice(). And yes, you can still develop in a structured way in C++, and that's good. In contrast to other languages. This method is intended to evaluate random cube results.


#ifndef roll_the_diceH
#define roll_the_diceH

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

#endif

The method has two parameters. The first is the number of random attempts, the second the number of sides of the dice. This is set to 6 by default, but thanks to the abstraction other dice or coin tosses could be simulated.

 



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


We can now integrate this into a program. For this we design a VCL or FMX- program with the following main form. Shown here with the FMX- example, but in both variants the example is quickly clicked together. In the project options we set the include- path to our directory with the header files, also we define the appropriate compiler switch (for FMX this is BUILD_WITH_FMX).

In the implementation I use my standard output for memo fields as well as my class TMyWait from the post RAII - mouse pointer for VCL + FMX and the methods from the header file <MyDelphiHelper.h> (conversion of Delphi string types). These already contain distinctions for VCL and FMX. 

With the exception of the header file for the framework <fmx.h>, which is <vcl.h> for the VCL, and the name of the header file for this form (here "MainFormFMX.h") both variants are identical.

But there are problems with the localization in Android, here simply the set system applies. Therefore again a bit of conditional translation.


#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"));  // national settings for germany, alternate de_DE
      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;
   }

Now we can compile this program, and the almost 20 years old method, then implemented according to the valid standard, translatable with any standard C++ compiler, works exactly as it was at the time of writing. Through some preliminary work with the help of the C++Builder, it is now translatable for different platforms. It still works with the classic compiler in C++Builder. So much for the dialect that takes getting used to. It simply shows the advantage of the industry standard for C++.

But the standard library is not complete with the Android compiler in C++Builder. For example, the header file <cstdlib> gets nasty when compiling. But maybe this is the time to refresh the outdated method a bit. Therefore I would like to adapt the generation of random numbers for all clang based compilers of C++Builder to the C++11 standard. But also only for these, I use the compiler switch __clang__ which is defined in this case. 

So the code remains executable for the older classical compiler, and in principle even unchanged.


#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

I hope that with this example I was able to show that a little bit of groundwork helps not only to create platform independent programs, but also to reuse historical or, as some say, legacy code. But one of the ideas behind C++ is to protect the "historical" code and thus huge investments instead of following every hype. Anyway, they have now created their first Android program, and that's only because they chose the target environment in C++Builder. If you now have a device connected to your computer, the environment should automatically install it on it (assuming you have the appropriate developer options). And contrary to what is claimed in the article "An IDE to enslave you" (Heise c't 10/2018)", the dialect of C++Builder is not unusual here either, and like other comparisons by the author, it is taken from thin air. And they see that even if a function is 20 years old, it will always work with minor changes. Whatever goal the author (or the c't) followed, so that he wanted to lower the rating of native compilers, very one-sided and also badly researched, concluded to present script frameworks as a solution, surely only the author can answer.

But we also have the toolbox with which we can adapt our programs to different situations. We just have to learn how to do it and use our possibilities responsibly. It should be said that this blog is no substitute for buying a book and then "going to school" again. It is also not intended to replace seminars and training courses. But even after many years C++ does not get boring.

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