C++ Builder und XML

Die Arbeit mit XML unter C++ Builder ist einfach und intuitiv. Allerdings sollte man beachten, immer brav auf das Interface zu programmieren 🙂

void TFormMain::ReplyRequestTest( void )
{
    TXMLDocument *XMLDocument = new TXMLDocument(this);
    XMLDocument->Options = XMLDocument->Options << doNodeAutoIndent;
    XMLDocument->Active = true;
    XMLDocument->Version = "1.0";
    XMLDocument->Encoding = "ISO-8859-1";

    _di_IXMLDocument IDocument;
    XMLDocument->GetInterface( IDocument );

    _di_IXMLNode TerminalNode = IDocument->AddChild( "Test" );
    TerminalNode->SetAttribute( "TerminalGuid", Setting->GetTerminalGuid() );

    Logging->Write( XMLDocument->XML->Text );

    this->ClientSocket->Socket->SendText( XMLDocument->XML->Text );

    delete XMLDocument;
}

void TFormMain::ReplyRequestMessageChange( void )
{
    TXMLDocument *XMLDocument = new TXMLDocument(this);
    XMLDocument->Options = XMLDocument->Options << doNodeAutoIndent;
    XMLDocument->Active = true;
    XMLDocument->Version = "1.0";
    XMLDocument->Encoding = "ISO-8859-1";

    _di_IXMLDocument IDocument;

    XMLDocument->GetInterface( IDocument );

    _di_IXMLNode TerminalNode = IDocument->AddChild( "Message" );
    TerminalNode->SetAttribute( "TerminalGuid", Setting->GetTerminalGuid() );
    TerminalNode->SetAttribute( "MessageGuid", ClientDataSetTerminalMessageGuid->AsString );
    TerminalNode->SetAttribute( "MessageAcronym", ClientDataSetTerminalMessageAcronym->AsString );

    Logging->Write( XMLDocument->XML->Text );

    this->ClientSocket->Socket->SendText( XMLDocument->XML->Text );

    delete XMLDocument;

}

LME288

Es mag dem einen oder anderen schon passiert sein: Urplötzlich will sich ein Programm mit dem Embarcadero C++ Builder nicht mehr linken lassen:

Abhängigkeiten des Projekts werden überprüft...
Compilieren von Project1.cbproj (Debug, Win32)
bcc32 Befehlszeile für "Project1.cpp"
  c:\program files (x86)\embarcadero\studio\17.0\bin\bcc32.exe -D_DEBUG -DUSEPACKAGES -n.\Win32\Debug -I"c:\program files 
  (x86)\embarcadero\studio\17.0\include\windows\crtl\DE";"c:\program files (x86)\embarcadero\studio\17.0\include\windows\rtl\DE";"c:\program files 
.......
[VIELE, VIELE WEITERE ZEILEN FOLGEN HIER...]
.......
[ilink32 Warnung] Warning: C:/Users/huh/Documents/Embarcadero/Studio/Projekte/Win32/Debug/Project1.ils: 0x00040000 / 0x08000000
[ilink32 Warnung] Warning: unknown heap name   : 0x08000000 / 0x08000000
[ilink32 Warnung] Warning: Fehler gefunden (LME288)
[ilink32 Fehler] Error: Linken kann nicht ausgeführt werden
Misslungen
Verstrichene Zeit: 00:00:02.2

Hierzu findet man im Netz zahlreiche Tipps, vom Löschen irdgendwelcher Temporärdateien, Deinstallation diverser Programme bis hin zur Neuinstallation vom RAD Studio.

Zuerst dachte ich: “Alles Quatsch – die Lösung ist meist viel einfacher”. Ich ließ mich also auf folgendes Experiment, das aber nur temporär eine Lösung brachte:

In meinem Fall war es einfach so, dass ich die maximale Länge der Environment PATH Variable überschritten habe. Kein Scherz: Ist der Eintrag in PATH länger als 2047 Zeichen, dann funktioniert neben anderem auch das Embarcadero Studio nicht mehr wie erwartet.

Wie dieses Problem im Detail zu lösen ist, bleibt jedem selbst überlassen. Ich habe auf meinem Rechner nicht verwendete Pfade gelöscht und längere Pfade durch kürzere Links (z.B. “C:\_SYS\_BCC17\” anstelle von “C:\Program Files (x86)\Embarcadero\Studio\17.0”) ersetzt.

Weiterführende Literatur:

https://software.intel.com/en-us/articles/limitation-to-the-length-of-the-system-path-variable

Aber ja: Wie bereits angedeutet – das alles wäre zu schön gewesen um wahr zu sein. Für einige Zeit hält die Lösung, dann aber plötzlich treten die selben Probleme wie zu Beginn auf.

Ein weiterer Lösungsvorschlag ist die Datenausführungsverhinderung, wie hier beschrieben: http://www.winfaq.de/faq_html/Content/tip2000/onlinefaq.php?h=tip2323.htm

Auch hier währte die Freude nicht lange und das alte Spiel begann von vorne.

Nachdem das Problem unablässig wieder auftaucht, habe ich nochmal nach Lösungen gesucht. Hier ein paar Ansätze:

https://stackoverflow.com/questions/28929516/c-builder-xe7-lme288-error/34819111#34819111

Und tatsächlich gab es wieder eine Lösung, die auf Anhieb funktionierte: Das Setzten des “Large Address Aware Flag” löste umgehend mein Problem mit LME288.

Folgendes ist dabei zu tun:

  1. LAMarker.exe herunterladen von https://cc.embarcadero.com/Item/30459
  2. ilink32.exe damit patchen:
LAMarker.exe -M -Filink32.exe

Nachdem mich dieses Problem schon einige Jahre immer wieder verfolgte, bin ich nun zum ersten Mal zuversichtlich, dass dies tatsächlich die Lösung sein könnte.

Kurzfristig. Denn langfristig werde ich kein Geld mehr in die Lizenzen von Embarcadero mehr investieren.

Embarcadero C++ Builder, TDataModule und Vererbung

Nach ein paar kleiner Umbauten und Optimierungen im Programmcode erhielt ich beim Versuch ein Fenster zu öffnen ziemlich unerwartet den folgenden Fehler:

Im Projekt Project.exe ist eine Exception der Klasse EReadError mit der Meldung 'Eigenschaft ClientHeight existiert nicht' aufgetreten.

2016-03-24 09_10_59-Rational - C++Builder 10 Seattle - UnitFormMain.h [Angehalten - Thread 16168] [E

Dabei war interessant, dass der Fehler nicht wirklich in Zusammenhang mit meinen zuletzt vorgenommenen Code-Änderungen stehen konnte.

Die Fehlermeldung im Debugger war nicht sehr Aufschlussreich – die Ausgabe in der Laufzeit (nachdem ich “Fortsetzen” gewählt hatte) gab mir allerdings den Zielführenden Hinweis: “Fehler beim Lesen von Invoice.ClientHeight”.

2016-03-24 09_12_08-Rational

Ich wusste, dass “Invoice” ein auf TDataModule basierendes Objekt innerhalb des aktuellen Fensters (TFormInvoice) ist: TDataModule / TInvoice. Aber weder meine Klasse TInvoice noch deren Basis (TDataModule) verfügen über eine Eigenschaft namens ClientHeight. Das haben gewöhnlich nur Formulare.

Nach einigen Überlegungen fiel mir ein, dass ich wenige Builds zuvor eine Ebene “TDataModuleBaseRecord” zwischen TDataModule und TInvoice einschob: Die neue Klasse sollte Erbtante für bestimmte ähnliche Kindklassen werden.

Im SVN konnte ich folgende Änderungen finden:

TInvoice.h

///////////////////////////////////
// ALT:
///////////////////////////////////
#include "DataChangedObserver.h"
#include "DataSavedObserver.h"

class TInvoice : public TDataModule
{
    __published:
    // Programmcode...
}

//////////////////////////////////
// NEU:
//////////////////////////////////
#include "DataModuleBaseRecord.h"
#include "DataChangedObserver.h"
#include "DataSavedObserver.h"

class TInvoice : public TDataModuleBaseRecord
{
    __published:
    // Programmcode...
}

TInvoice.cpp

/////////////////////////////
// ALT:
/////////////////////////////
#pragma hdrstop

#include "Invoice.h"

#pragma package(smart_init)
#pragma classgroup "Vcl.Controls.TControl"
#pragma resource "*.dfm"

__fastcall TInvoice::TInvoice(
    TComponent* Owner,
    long AInvoiceId,
    TCustomer *ACustomer
)
    :
    TDataModule(Owner),
    InvoiceId( AInvoiceId ),
    Customer( ACustomer )
{
}

//////////////////////
// NEU:
//////////////////////
#pragma hdrstop

#include "Invoice.h"

#pragma package(smart_init)
#pragma classgroup "Vcl.Controls.TControl"
#pragma resource "*.dfm"

__fastcall TInvoice::TInvoice(
    TComponent* Owner,
    long AInvoiceId,
    TCustomer *ACustomer
)
    :
    TDataModuleBaseRecord(Owner),
    InvoiceId( AInvoiceId ),
    Customer( ACustomer )
{
}

Die neu eingeführte “Erbtante” hatte auch nichts ungwöhnliches an sich:

//////////////
// Header:
//////////////
#ifndef UnitDataModuleBaseRecordH
#define UnitDataModuleBaseRecordH

#include <System.Classes.hpp>

class TDataModuleBaseRecord : public TDataModule
{
    __published:
    private:
    public:
        __fastcall TDataModuleBaseRecord(TComponent* Owner);

        bool IsLoaded;
};

#endif

//////////////
// CPP:
//////////////
#pragma hdrstop

#include "UnitDataModuleBaseRecord.h"

#pragma package(smart_init)
#pragma classgroup "Vcl.Controls.TControl"
#pragma resource "*.dfm"

__fastcall TDataModuleBaseRecord::TDataModuleBaseRecord(TComponent* Owner)
    :
    TDataModule( Owner ),
    IsLoaded( false )
{
}

/////////////
// DFM:
/////////////
object DataModuleBaseRecord: TDataModuleBaseRecord
  OldCreateOrder = False
  Height = 501
  Width = 505
end

Bisher nichts ungewöhnliches, doch dann der Blick auf die Invoice.dfm ließ mich etwas staunen:

///////////////////////////
// ALT:
///////////////////////////
object Invoice: TInvoice
  OldCreateOrder = False
  OnCreate = DataModuleCreate
  Height = 626
  Width = 672
  object FDTableInvoice: TFDTable
    Connection = Database.FDConnection
    UpdateOptions.UpdateTableName = 'Invoice'
    TableName = 'Invoice'
    Left = 92
    Top = 133
  end

/////////////////
// NEU:
/////////////////
object Invoice: TInvoice
  Left = 0
  Top = 0
  ClientHeight = 588
  ClientWidth = 656
  Color = clBtnFace
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -11
  Font.Name = 'Tahoma'
  Font.Style = []
  OldCreateOrder = False
  OnCreate = DataModuleCreate
  PixelsPerInch = 96
  TextHeight = 13
  object FDTableInvoice: TFDTable
    Connection = Database.FDConnection
    UpdateOptions.UpdateTableName = 'Invoice'
    TableName = 'Invoice'
    Left = 92
    Top = 133
  end

TInvoice ist einfach so von einem Kind von TDataModule zu einem von TForm mutiert. Die Frage ist: Warum?

Die Antwort ist: Weil im DFM der Hinweis “Inherited” fehlt.

Lösung: Bei der nachträglichen Änderung der Vererbungshierarchie hätte diese neue Vererbung im DFM bekannt gemacht werden sollen.

Aus

object Invoice: TInvoice
  Left = 0

hätte

inerited Invoice: TInvoice
  Left = 0

werden sollen.

Das ist eine sehr gemeine, schwerwiegende und nicht leicht zu findende Stolperfalle für C++ Builder Programmierer: Erstens kümmern sich C++ Entwickler nur ungern um den von der IDE fabrizierten Delphi-Code und zweitens tauchen die Probleme mitunter erst Tage später auf. Dadurch fängt man gerne an der falschen Stelle zu suchen an.

Warum die Folgen erst Tage später aufscheinen können weiß ich ebenso wenig wie warum die IDE hier keine elegante Möglichkeit bietet das Problem zu beheben und der Betroffene zum Texteditor greifen muss.

Kennt hier jemand eine bessere Lösung?

__fastcall

Im Borland/Embarcadero C++ Builder trifft man immer wieder auf das __fastcall Schlüsselwort. Die IDE setzt es bei allen Event-Funktionen ein, und man ist dann nicht immer gar so sicher, ob man das jetzt auch tun soll, oder nicht.

Hier habe ich einen interessanten Artikel gefunden, der beschreibt was __fastcall ist und wie man damit richtig umzugehen hat:

http://bcbjournal.org/articles/vol4/0004/When_to_use___fastcall.htm

Die Antwort in Kürze:

Verwende __fastcall nicht, wenn du nicht unbeding musst 🙂

 

TClientDataSet duplizieren

Von einer Zeile eines TClientDataSet eine Kopie erstellen? “Nichts Einfacheres” habe ich mir gedacht – da war ich allerdings etwas voreilig. Wie bei anderen Dingen auch, so findet sich auch hierzu in den Dokumentationen von Borland/CodeGear/Embarcadero nur spärlich Information.

Warum einfach, wenn man’s mit dem CPP Builder auch machen kann?

Gewiss: Man könnte Spalte für Spalte die Werte zwischenspeichern und nach einem TClientDataSet::Append() einfach wieder einfügen. Nicht sehr elegant. Es sollte doch nur die aktive Row (bzw. deren Inhalte) eines ClientDataSet (CDS) in eine neue Zeile kopiert werden.

Nach einigem Hin und Her und der Berücksichtigung etlicher umständlicher Ratschläge aus diversen Foren habe ich dann doch noch einen einigermaßen gangbaren Weg gefunden:

// Angenommen TClientDataSet *CDS ist ein Member der Klasse

// Zuerst müssen wir von unserem CDS einen Clone erstellen
TClientDataSet *ClonedSet = new TClientDataSet( this );
ClonedSet->CloneCursor( this->CDS, false );

// Jetzt kann eine neue Zeile angelegt werden
this->CDS->Append();

// Feldweise die Daten kopieren, allerdings nicht:
// 1. das PK-Feld (wenn es sich um ein AutoIncrement-Feld handelt
// 2. die Felder die ReadOnly sind
for ( int i = 0; i < this->CDS->FieldCount; i++ )
{
    if
    (
        this->CDS->Fields->Fields[i]->AutoGenerateValue != arAutoInc &&
        this->CDS->Fields->Fields[i]->ReadOnly == false
    )
    {
        // Vorsicht, Falle: nicht einfach per Index die beiden 
        // DataSets durchiterieren, diese sind nämlich nicht 
        // zwingend gleich. Umweg über FieldByName verwenden!
        this->CDS->Fields->Fields[i]->Value =
            ClonedSet->FieldByName(
                this->CDS->Fields->Fields[i]->FieldName 
            )->Value;
    }
}

// Speichern und aktualiseren, damit auch die per JOIN 
// verbundenen Spalten befüllt werden 
this->CDS->ApplyUpdates(-1); 
this->CDS->Refresh(); 
delete ClonedSet;

Ich vermute schwer, dass hier noch einiges zu ergänzen wäre, aber ich will hier nur den Kern des Problems eruieren.

Menü’s verschoben

Hier beschreibe ich einen “Bug”, der einen richtig zum Narren halten kann. Zumindest hat er das mit mir getan: Die Hauptmenü’s von Programmen sind plötzlich (fast) alle nach links verschoben.

Seit längerer Zeit arbeite ich an einer kleinen Software mit grafischer Oberfläche, die ich mit dem Embarcadero C++ Builder XE2 entwickle. Ich wollte, dass die GUI richtig toll aussieht, und habe mich daher für de Verwendung von TMS Components entschieden.

Da ich an diesem Projekt meist zu Hause arbeite, mache ich sehr viel an meinem privaten Windows7-Notebook. Alles war sehr toll, bis mir eines Tages eine Unschönheit in der GUI auffiel: Das Hauptmenü war irgendwie verschoben:

R[el]Ational_2015-04-13_10-18-28-

Das Menü zeigte plötzlich nach links, anstelle dass es wie gewohnt symmetrisch nach unten ausgerichtet war.

Auf meinem Zweitcomputer hatte ich ebenfalls eine Entwicklungsumgebung eingerichtet und dort war alles so, wie ich es erwarten würde:

R[el]Ational_2015-04-13_10-18-28

Nach etlichen Untersuchungen und Vergleichen glaubte ich die Ursache gefunden zu haben: Es mussen die TMS Components sein, da ich von diesen 2 unterschiedliche Versionen im Einsatz hatte. Da diese eh schon etliche Semester auf dem Buckel hatten entschied ich mich kurzerhand zu einem Upgrade und installierte die neueste Version auf meinem Notebook.

Eine Stunde und runde 250€ später war das Ergebnis ernüchternd: alles war beim alten. Und erst jetzt fiel mir auf, dass auch andere Programme von diesem Problem betroffen waren. So z.B. Notepad, das vermutlich nicht auf TMS Components zurück greift:

Unbenannt - Editor_2015-04-13_10-01-30

Nach einiger Recherche fand ich heraus, dass es sich um ein Problem von Windows 7 handelt und dass dieses Problem häufig in Zusammenhang mit einem Upgrade auf SP1 stehen könnte: Es seien die Tablet PC Einstellungen, die auf “Linkshand” eingerichtet werden müssen:

Tablet PC-Einstellungen_2015-04-13_10-22-15

Der Dialog sei in der Systemsteuerung hinterlegt. Bei mir war das allerdings nicht so, ich musste den Dialog mit folgendem Befehl aufrufen:

Start / Ausführen und:

shell:::{80F3F1D5-FECA-45F3-BC32-752C152E456E}

Option Linkshändig aktivieren hat dann das Problem behoben

 

Kind-Klasse von TDataModule oder TForm und Stack Overflow

In C++ Builder Programmen kann es bei der Vererbung von TDataModule oder TForm Klassen zu einer bösen Überraschung in Form eines Stapelüberlaufs kommen:

class TInvoices : public TDataModule
{
    __published:

    private:
        int FCustomerId;
    public:
        // standard constructor
        __fastcall TInvoices( TComponent* Owner );
        // überladener constructor mit zusätzlichem parameter
        __fastcall TInvoices( TComponent* Owner, int ACustomerId );

        __property int CustomerId = { read=FCustomerId, write=FCustomerId };
};

Grund für den Stack Overflow ist der 2. Parameter (“int ACustomerId”) im überladenen constructor, der nachträglich eingefügt wurde.

Mögliche Lösungen des Problems:

  • Hinter dem 2. Parameter weitere Parameter anfügen
  • Die beiden Parameter vertauschen (int/TComponet)
  • Den Datentyp des 2. Parameter ändern (z.B. unsigned long anstelle von int)

Form->Close() und __property

Das mag manchem C++ Builder Programmierer schon widerfahren sein: Man denkt sich nichts böses, schließt eine Form und plötzlich erscheint eine noch nicht da gewesene AV:

Benachrichtigung über Debugger-Exception_2015-03-31_15-27-27

In diesem Fall ist der Debugger gleich zur Setter-Methode einer __property gesprungen:

void __fastcall TFormSetup::SetDatabaseConnected( bool AConnected )
{
    this->FDatabaseConnected = AConnected;
   /*
      Do something else...
   */
}

Hier noch das Entsprechende aus dem Header:

#ifndef UnitFormSetupH
#define UnitFormSetupH

#include <System.Classes.hpp>
#include <Vcl.Controls.hpp>
#include <Vcl.StdCtrls.hpp>
#include <Vcl.Forms.hpp>

class TFormSetup : public TForm
{
    __published:
        /*
        ....
        */
    private:
        bool FDatabaseConnected;
    public:
        __fastcall TFormSetup(TComponent* Owner);

        void __fastcall SetDatabaseConnected( bool AConnected );

        __property bool DatabaseConnected = { 
            read=FDatabaseConnected, 
            write=SetDatabaseConnected };
};

#endif

Grund für die AV ist nun, dass die TForm beim Schließen auch noch gleich eine geöffnete Datenbank-Komponente schließt (nicht im Code-Beispiel angeführt). Diese wiederum setzt im AfterDisconnect()-Event die Property DatabaseConnected auf false.

Zu diesem Zeitpunkt sind die Klassenmember aber bereits nicht mehr verfügbar, daher muss dies im Setter der __property berücksichtigt werden:

void __fastcall TFormSetup::SetDatabaseConnected( bool AConnected )
{
    if ( this != NULL )
    {
        this->FDatabaseConnected = AConnected;
        /*
        Do something else...
        */
    }
}

Dies ist ein funktionierender Weg. Ob er elegant ist, sei dahingestellt. Sollte ich einen eleganteren finden, werde ich ihn posten. Sollte jemand einen solchen kennen, bitte ich um Kommenar 🙂

SelectDirectory

Beim Versuch einen Auswahldialog für ein Verzeichnis zu erstellen, wollte der Compiler zuerst nicht complilieren.

AnsiString s;
SelectDirectory( s, TSelectDirOpts(), 0 );

Das wurde ganz einfach mit folgendem Fehler abgelehnt:

[BCC32 Fehler] UnitMain.cpp(28): E2285 Keine Übereinstimmung für 'SelectDirectory(AnsiString,TSelectDirOpts,int)' gefunden
 Vollständiger Parser-Kontext
 UnitMain.cpp(24): Analyse: void _fastcall TFormMain::ButtonBrowseClick(TObject *)

Ein Blick in den Header Vcl.FileCtrl.hpp hat dann auch gleich auf die Antwort hingewiesen:

extern PACKAGE bool __fastcall SelectDirectory(
  System::UnicodeString &Directory, 
  TSelectDirOpts Options, 
  int HelpCtx)/* overload */;

extern PACKAGE bool __fastcall SelectDirectory(
  const System::UnicodeString Caption, 
  const System::WideString Root, 
  System::UnicodeString &Directory, 
  TSelectDirExtOpts Options = (TSelectDirExtOpts() << TSelectDirExtOpt::sdNewUI ), 
  Vcl::Controls::TWinControl* Parent = (Vcl::Controls::TWinControl*)(0x0))/* overload */;

Lösung: Verzeichnisname muss vom Typ UnicodeString sein.

UnicodeString s;
SelectDirectory( s, TSelectDirOpts(), 0 );

Anmerkung: SelectDirectory wirft den selben Fehler, wenn anstelle eines AnsiString einfach nur eine Property Variable übergeben wird:

__property System::UnicodeString s {...