Neues Forum für TeamDeveloper, SqlBase, TDMobile und ReportBuilder

Hi folks,

es gibt ein neues offenes Forum für TeamDeveloper, SqlBase, TDMobil und ReportBuilder hier: http://tdforum.daverabelink.net/

Leider hat OpenText das bislang offene Forum (https://support.guptatechnologies.com) geschlossen es ist nun read only.

Sie haben zwar ein neues Forum innerhalb von OpenText „My Support“ erstellt (https://knowledge.opentext.com/knowledge/cs.dll/Open/Knowledge). Aber das ist nur mit einem GLS-Vertrag zugänglich und auch dann nur nach einem komplizierten Registrierungsprozess.

Da die Gemeinschaft der TeamDeveloper-Entwickler  nicht allzu groß ist bezweifle ich, dass eine Menge Leute die Hürde zum offiziellen Forum nehmen und es so gar nicht erst zum Leben erwachen wird.

Damit die Gemeinschaft erhalten bleibt, tragt bitte zum neuen offenen Forum http://tdforum.daverabelink.net/ bei.

Happy coding

Advertisements

UTF-8 Strings mit Team Developer 3.1 erstellen

Neulich hatten wir die Aufgabe, mit Team Developer 3.1 einen Text in UTF-8 zu erstellen. Eine Recherche in der Windows API fördert zwei geeignete Funktionen zu Tage:

MultiByteToWideChar(..) und WideCharToMultiByte(..). Mit der ersten Funktion ist es möglich, irgendeine Codierung (ANSI, UTF-8 und eine Menge Codepages) in UTF-16 (WideChar) umzuwandeln. Mit der zweiten kann UTF-16 in irgendeine Codierung umwandeln. Der Weg ist also:
GuptaString — MultiByteToWideChar(..) –> UTF-16 — WideCharToMultiByte(..) –> UTF-8.

Die Bezeichnung der beiden Funktionen finde ich etwas verwirrend: Mit MultiByte ist eine Codierung aus einem großen Fundus gemeint – auch SingleByte-Codierungen wie ANSI. WideChar steht für die Windows-interne UTF-16LE-Codierung.

Stellt sich nur noch die Frage, wie man die Funktionen konkret in Gupta verwendet:

MultiByteToWideChar

mit den Parametern

_In_      UINT   CodePage = Codepage des umzuwandelnden String – in unserem Fall die Standard Windows ANSI Codepage – also die Konstante CP_ACP = 0x00
_In_      DWORD  dwFlags = Steuert Feinheiten in der Umwandlung einzelner Zeichen. Beispielsweise kann der Buchstabe „Ä“ in Unicode auf zwei Arten dargestellt werden. Einmal als einzelnes Zeichen Ä (U+00C4) mit dwFlags = MB_PRECOMPOSED (0x01)  oder als kombiniertes Zeichen aus A + ̈ (U+0041 und U+0308) mit dwFlags = MB_COMPOSITE (0x02)
_In_      LPCSTR lpMultiByteStr = der zu codierende String
_In_      int    cbMultiByte = Länge des zu codierenden Strings oder -1, wenn der zu codierende String mit 0 terminiert ist – was bei Gupta der Fall ist.
_Out_opt_ LPWSTR lpWideCharStr = Buffer, in den die Funktion den konvertierten UTF-16 String schreibt
_In_      int    cchWideChar = Länge des zur Verfügung gestellten Buffers in Zeichen(!) oder 0, wenn man nur die Länge des nötigen Buffers ermitteln möchte.

Zunächst muss man also diese Funktion aufrufen mit cchWideChar = 0 und erhält die nötige Länge des Buffers in Zeichen(!) zurück.
Aber was bedeutet das konkret? Wieviel Bytes sind das?
Die Antwort ist einfach – ein WideChar-Zeichen ist immer 2 Byte lang – also muss man den Rückgabewert mit 2 multiplizieren und kann per SalStrSetBufferLength(..) entsprechend Speicher bereitstellen.
Aber stimmt das wirklich? Denn Unicode umfasst mehr Zeichen als mit 2 Bytes codiert werden können. Ein Standardbeispiel ist der Violinenschlüssel musical_symbol_g_clef
(U+1D11E) mit der UTF-16-Codierung 0xD834 0xDD1E – also vier Bytes.
Die Antwort ist: Ja es stimmt wirklich. Ich habe das getestet. Liefert man den Violinenschlüssel in UTF-8 Codierung an (0xF0 0x9D 0x84 0x9E) gibt die Funktion MultiByteToWideChar(..) den Wert „2“ zurück – obwohl es sich in Wahrheit nur um ein Zeichen handelt.

WideCharToMultiByte

mit den Paramtern

_In_      UINT    CodePage = gewünschte Ziel-Codepage
_In_      DWORD   dwFlags = Flags die das Verhalten steuern, wenn ein umzuwandelndes Zeichen nicht in der Zielcodepage enthalten ist.
_In_      LPCWSTR lpWideCharStr = der zu codierende UTF-16 String
_In_      int     cchWideChar = Länge des zu codierenden Strings oder -1, wenn dieser terminiert ist
_Out_opt_ LPSTR   lpMultiByteStr = Buffer für den umgewandelten String
_In_      int     cbMultiByte = Länge des Buffers in Bytes
_In_opt_  LPCSTR  lpDefaultChar = Zeichen, das verwendet werden soll, wenn ein UTF-16-Zeichen nicht in der Zielcodepage enthalten ist
_Out_opt_ LPBOOL  lpUsedDefaultChar = Eigentlich eine Receive-Variable, die angibt, ob ein Default bei der Umwandlung verwendet werden muss. Für die Umwandlung nach UTF-8 muss die Variable aber mit dem Wert NULL übergeben werden. Das wiederum lässt Gupta leider nicht zu: Wenn man den Receive-Parameter für eine externe Funktion auf NULL setzt (z.B. so: Set bUsedDefaultChar = NUMBER_Null und damit die externe Funktion aufruft, bekommt man einen Laufzeitfehler). Die einzige Möglichkeit ist, in der Deklaration der externen Funktion diesen Parameter als BOOL zu definieren und den Wert 0 zu übergeben.)

StringToUtf8

Damit steht der Umwandlung nichts mehr im Weg:

Function: StringToUtf8  ! __exported
    Description: Converts a Gupta string to UTF-8
    Returns
        String:
    Parameters
        String: sAnsi
    Actions
        Return Utf16ToUtf8( StringToUtf16( sAnsi ) )

Function: StringToUtf16  ! __exported
    Description: Converts a Gupta string (ANSI) to UTF-16
    Returns
        String:
    Parameters
        String: sAnsi
    Local variables
        Number: nBufferUtf16Len
        String: sUtf16
    Actions
        If ( NOT sAnsi )
            Return ''
        Call SalStrSetBufferLength( sUtf16, 1 ) ! Buffer must be 1 or bigger - otherwise we get a Gupta error when calling MultiByteToWideChar(..)
        Set nBufferUtf16Len = MultiByteToWideChar( CP_ACP, MB_PRECOMPOSED, sAnsi, -1, sUtf16, 0 )
        Call SalStrSetBufferLength( sUtf16, nBufferUtf16Len * 2 ) !  Buffersize in "WideChars" -  must be multiplied by 2 to get buffersize in bytes
        Call MultiByteToWideChar( CP_ACP, MB_PRECOMPOSED, sAnsi, -1, sUtf16, nBufferUtf16Len )  !hier wieder Buffer in Characters
        Return sUtf16

Function: Utf16ToUtf8  ! __exported
    Description: Converts a UTF-16-String to UTF-8
    Returns
        String:
    Parameters
        String: sUtf16
    Local variables
        Number: nBufferUtf8Len
        String: sUtf8
    Actions
        If ( NOT sUtf16 )
            Return ''
        Call SalStrSetBufferLength( sUtf8, 1 ) ! Buffer must be 1 or bigger - otherwise we get a Gupta error when calling WideCharToMultiByte(..)
        Set nBufferUtf8Len = WideCharToMultiByte( CP_ACP, 0, sUtf16, -1, sUtf8, 0, STRING_Null, 0 )
        Call SalStrSetBufferLength( sUtf8, nBufferUtf8Len )
        Call WideCharToMultiByte( CP_ACP, 0, sUtf16, -1, sUtf8, nBufferUtf8Len, STRING_Null, 0 )
        Return sUtf8

Quellcode gibt’s hier: UtfStringConverion.apt

Happy coding!

Selbstreferenz mit Arrays aus Functional Classes in Gupta

Neulich hatte ich gezeigt, wie in Gupta eine Functional Class „sich selbst“ als Instance Variable enthalten kann. Man muss sie lediglich mit OBJ_Null initialisieren.
SomeClass: class1 = OBJ_Null
Martin fragte mich daraufhin, ob es auch möglich sei, ein Array der Functional Class als Instance Variable zu verwenden. Diese Datenstruktur ist äußerst nützlich, wenn man z.B. eine Baumstruktur abbilden möchte, wie sie in Dateisystemen vorkommt.
SomeClass: aClasses[*]
Leider quittiert der TeamDeveloper das mit einer Fehlermeldung beim Kompilieren. „Error: Circularly defined class.“ Martin hat daraufhin eine listige Lösung gefunden, den TD auszutricksen. Allerdings produziert dieser in meiner Entwicklungsumgebung Abstürze, wenn ich auf ein Arrayelement zugegriffen habe.
Eine andere Lösung für das Problem ist, eine weitere Listenklasse zu definieren:
Ausgehend von einem Datentyp (Functional Class) namens „Node“ wird eine weitere Klasse „ListNodes“ erstellt, die als Instance Variablen ein Array aus Nodes sowie einen internen Zähler enthält:
Functional Class: ListNodes
 Instance Variables
  Node: __nodes[*]
  Number: __nCountNodes
Diese Listenklasse kann nun in der Klasse „Node“ als Instance Variable verwendet werden, wenn sie mit OBJ_Null initialisiert wird:
Functional Class: Node
 Instance Variables
  ListNodes: __listChildNodes = OBJ_Null
Fügt man noch ein paar Zugriffsfunktionen hinzu, kann man damit richtig gut arbeiten:
Function: AddChildNode
 Returns
 Parameters
  Node: node
 Local variables
 Actions
  If ( __listChildNodes = OBJ_Null )
   Set __listChildNodes = new ListNodes
  Set __listChildNodes.aNodes[__listChildNodes.nCountNodes] = node
  Set __listChildNodes.nCountNodes = __listChildNodes.nCountNodes + 1
Function: CountChildNodes
 Returns
 Parameters
 Local variables
 Actions
  If ( __listChildNodes = OBJ_Null )
   Return 0
  Else
   Return __listChildNodes.nCountNodes
Function: GetChildNode
 Returns
  Node:
 Parameters
  Number: nIdx
 Local variables
 Actions
  If ( __listChildNodes = OBJ_Null )
   Return OBJ_Null
  Else
   If ( (nIdx >= 0) AND (nIdx < __listChildNodes.nCountNodes) )
    Return __listChildNodes.aNodes[nIdx]
   Else
    Return OBJ_Null
Function: RemoveChildNode
 Returns
 Parameters
  Number: nIdx
 Local variables
 Actions
  If ( __listChildNodes = OBJ_Null )
   ! do nothing
  Else
   If ( (nIdx >= 0) AND (nIdx < __listChildNodes.nCountNodes) )
    While ( nIdx < __listChildNodes.nCountNodes - 1)
     Set __listChildNodes.aNodes[nIdx] = __listChildNodes.aNodes[nIdx +1 ]
     Set nIdx = nIdx + 1
    Set __listChildNodes.aNodes[__listChildNodes.nCountNodes] = OBJ_Null
    Set __listChildNodes.nCountNodes = __listChildNodes.nCountNodes - 1
Im Anhang ist ein Beispiel, das die Directory-Struktur des Laufwerks C: ausliest (es kann allerdings eine Weile dauern, bis alle Informationen ausgelesen sind).
Happy Coding

Vorsicht bei SalStrLop (in TD3.1)

Wenn SalStrLop auf eine leere Zeichenfolge (sString) angewendet wird, können seltsame Dinge geschehen. Grund dafür ist, dass Gupta die interne Pufferlänge auf null setzt. Wenn diese Zeichenfolge mit einer anderen Zeichenfolge (sString2) verkettet wird, wird deren Puffer jedes Mal um eins vermindert. Wenn eine solche Zeichenfolge an eine externe DllFunktion übergeben wird, treten Abstürze auf, weil beim Kopieren des Strings nur nBufferLen Zeichen kopiert werden. In der Regel fehlt die terminierende Null. Den Rest könnt ihr euch denken.
Hier ist Code, der das Problem veranschaulicht:
Set sTest = ''
Set sTest2 = 'A'
Set nBuffer = SalStrGetBufferLength( sTest ) ! nBuffer is 1 - just the trailing zero
Set nBuffer = SalStrGetBufferLength( sTest2 ) ! nBuffer is 2 - 'A' and trailing zero
Call SalStrLop( sTest )
Set nBuffer = SalStrGetBufferLength( sTest ) ! nBuffer is 0!
Set sTest2 = sTest2 || sTest
Set nBuffer = SalStrGetBufferLength( sTest2 ) ! nBuffer is 1!!

Workaround: SalStrLop nur auf nicht leere Strings anwenden. Z.B. mit einer Funktion wie dieser:

!!CB!! 134
Function: StrLopSafe
 Description:
 Returns
  Number:
 Parameters
  Receive String: sIn
 Static Variables
 Local variables
 Actions
  If ( sIn )
   Return SalStrLop( sIn )
  Else
   Return 0

In TD6.3 existiert das Problem übrigens nicht.

Happy coding!

General Window Class-Funktionen über ein Window Handle aufrufen

Gupta bietet die Möglichkeit, General Window Classes zu definieren, die als Basisklasse für echte Window Classes wie z.B. Dialoge, FormWindows, Steuerelemente, etc. dienen können.

Des weiteren gibt es die Möglichkeit, Funktionen eines Fensters oder Steuerelements über dessen Window Handle aufzurufen. Dazu muss man den Handle mit einem sogenannten Window Handle Qualifier ausstatten:

Call hWnd.Frm1.SomeFunction()
  • hWnd ist das Window Handle eines Form-Windows.
  • Frm1 ist entweder der Templatename eines Fensters oder eine Fensterklasse.
  • Die Funktion SomeFunction() ist eine Funktion, die in Frm1 definiert ist.

Soweit, so gut. Das ist alles in den Gupta Books bzw. in der Hilfe dokumentiert.

Hat man jedoch die Funktion SomeFunction() in einer General Window Class definiert, von der Frm1 ableitet, müsste man den Aufruf so schreiben:

Call hWnd.GenWndCls.SomeFunction()

Doch leider bringt Gupta dann unnötigerweise den Compilefehler „Class GenWndCls, is a general window class. Hence it may serve only as a base class, and may not be qualified with a window handle“. Das ist sehr ärgerlich, da es oft sinnvoll ist, eine General Window Class als Basis z.B. für eine Form und einen Dialog zu verwenden. Glücklicherweise gibt es dafür einen einfachen Workaround:

Call hWnd.GenWndCls..SomeFunction()

Ja – ein einfacher Punkt mehr und schon akzeptiert Gupta den Ausdruck. Warum ist das so? Der ..-Operator vor einem Funktionsaufruf ist eine sogenannte late-bound reference die bewirkt, dass Gupta die genannte Funktion in allen abgeleiteten Klassen sucht und die „tiefste“ Implementierung aufruft. Wäre SomeFunction() auch in Frm1 definiert, würde diese Implementierung aufgerufen. Gibt es keine weitere Implementierung ruft Gupta klaglos die zunächst verschmäte Funktion auf.

Happy coding.

Selbstreferenz mit Functional Classes in Gupta

Mit Gupta Team Developer lassen sich Functional Class Datentypen (UDV) erstellen – vergleichbar mit Klassen in C# oder Java. Im Unterschied zu den genannten Frameworks sind Variablen, die auf diesen Datentypen basieren, bei Gupta im Normalfall nicht nur deklariert sondern auch instantiiert. Das hat einen großen positiven Effekt: Man vermeidet den in C# und Java recht verbreiteten Runtimefehler „NullReferenceException“.

Beispiel C#

class SomeClass
{
        public Int32 someMember;
}
public void SomeMethod()
{
    SomeClass someObject;
        // NullReferenceException
    someObject.someMember = 7;

        // this works
    someObject = new SomeClass();
    someObject.someMember = 7;

}

Beispiel Gupta

Functional Class: SomeClass
    Instance Variables:
        Number: nSomeMember

Function: SomeFunction
    Local variables:
        SomeClass: someObject
    Actions:
        ! this works
        Set someObject.nSomeMember = 7

Gupta instantiiert die Klasse in dem Moment, in dem die Funktion SomeFunction() aufgerufen wird.

Dieses Standardverhalten hat aber auch einen Nachteil: Die Klasse (UDV) SomeClass kann sich selbst nicht (ohne weiteres) als Instanzvariable enthalten:

Functional Class: SomeClass
 Instance Variables:
     ! results in compile error: Circularly defined class.
     SomeClass: cSomeChild

Denn sobald SomeClass instantiiert würde, würde auch dessen Instance Variable cSomeChild instantiiert, anschließend dessen Instance Variable cSomeChild usw.

Dasselbe Problem tritt auf, wenn Class1 eine Instanzvariable vom Typ Class2 besitzt und Class2 eine von Class1.

Aber es gibt eine fast nicht dokumentierte Möglichkeit, eben doch solche Konstrukte zu erstellen: Indem man die UDV in der Deklaration auf OBJ_Null setzt:

Functional Class: SomeClass
 Instance Variables:
     SomeClass: cSomeChild = OBJ_Null

Ein paar Anmerkungen dazu:

  • Ich habe diese Möglichkeit zufällig an einer einzigen Stelle in der Hilfedatei von Gupta entdeckt: Als Beispielcode bei der Beschreibung des Befehls SalObjIsNull.
  • Nur UDVs können bei der Deklaration festgelegt werden. Bei Number, String, DateTime etc. lässt der TD das leider nicht zu. Immerhin soll mit TD 6.3 eine Konstruktor-Funktion für UDVs kommen, in denen man u.a. Werte für Instanzvariablen festlegen kann.
  • Wenn man die UDV cSomeChild verwenden möchte, muss man vorher entweder eine andere, bereits bestehende Instanz desselben oder abgeleiteten Typs zuweisen oder mit per new-Befehl eine Instanz erzeugen:
Set cSomeChild = new SomeClass

Anwendungsmöglichkeiten

Es gibt eine Menge Szenarien, in denen es äußerst nützlich oder sogar notwendig ist, verschachtelte Klassen zu verwenden:

  • Abbildung von hierarchischen Datenstrukturen, wie z.B. XML-Dokumente oder überhaupt alle Datenstrukturen, die man in TreeViews mit mehr als einer Ebene darstellen möchte oder kann.
  • Doppelt verknüpfte Listenstrukturen
  • Publish- Subscriber-Klassen, die wechselseitig verknüpft werden
  • Baumstrukturen – z.B. Red-Black-Index-Bäume

Viel Spaß beim Experimentieren.

Happy coding.

Datum-Literal in Gupta

Neulich bin ich wieder über kleines, aber feines, undokumentiertes Feature im Gupta TeamDeveloper gestolpert: Das Datum-Literal.

Es kommt zwar nicht häufig vor, aber manchmal muss man ein festes Datum im Quelltext definieren. Bislang haben wir das mit

dtValue = SalDateConstruct( 2015, 11, 21 , 22, 35, 12)

erledigt.

Es geht aber auch einfacher und eleganter:

dtValue = 2015-11-21-22.35.12

und flexibler

dtValue = 2015-11-21

und exakter

dtValue = 2015-11-21-22.35.12.123456

bis hinunter zur Mikrosekunde.

Happy Coding.