AGB  ·  Datenschutz  ·  Impressum  







Anmelden
Nützliche Links
Registrieren
Zurück Delphi-PRAXiS Programmierung allgemein Programmieren allgemein mORMot: Einen JSON-Viewer mit DocVariant und Virtual TreeView realisieren

mORMot: Einen JSON-Viewer mit DocVariant und Virtual TreeView realisieren

Ein Thema von mytbo · begonnen am 25. Apr 2023
Antwort Antwort
mytbo

Registriert seit: 8. Jan 2007
455 Beiträge
 
#1

mORMot: Einen JSON-Viewer mit DocVariant und Virtual TreeView realisieren

  Alt 25. Apr 2023, 01:37
Inspiriert durch diese Frage im Forum, wird im siebten Artikel der mORMot Vorstellungsreihe gezeigt, wie mit Hilfe von DocVariant und Virtual TreeView ein Viewer zur Anzeige von Daten im JSON-Format erstellt wird. Im Zusammenspiel zeigen die beiden Komponenten ihre Stärken. Als Datenpool die eierlegende Wollmilchsau DocVariant, hier in Form von TDocVariantData und der TVirtualStringTree als ultraschnelle Anzeige für hierarchische Datenstrukturen. Schnell heißt hier, die Anzeige steht für eine 25MB große JSON-Datei in weniger als 100 Millisekunden. Ich hoffe, die Lust auf Weiterlesen ist geweckt.

Die enthaltenen Verweise zur Dokumentation verlinken zur aktuell verfügbaren mORMot1 Hilfe. Für das Beispiel wird mORMot2 verwendet. Die Namen für Klassen und Funktionen können sich leicht unterscheiden. Im Anhang befindet sich der Sourcecode und das ausführbare Programm. Disclaimer: Der Sourcecode ist weder getestet noch optimiert. Er sollte mit Delphi ab Version 10.3 funktionieren. Die Benutzung der zur Verfügung gestellten Materialien erfolgt auf eigene Gefahr.

Die Beispiel-Anwendung ist ein einfacher JSON-Viewer. Das Laden eines Dokuments mit einer Größe von mehreren 100MBs ist möglich. Begrenzt wird es vom vorhandenen Arbeitsspeicher oder der maximalen Länge eines Strings. Mit dem Beispiel Quelltext für das Programm bekommt man:
  • Ein Dokument kann aus einer Datei geladen oder über das Clipboard importiert werden. Es lässt sich wieder im gepackten Format als Datei speichern.
  • Einzelne Zweige eines Dokuments können als Datei gespeichert oder als Objekt mit Erweiterung Stammknoten als Attribut ins Clipboard exportiert werden.
  • Jeder Zweig kann in einem eigenen Ansichtsfenster ausgelagert dargestellt und mit Doppelklick auf den Reiter geschlossen werden. Bei multipler Ansicht ist die Löschfunktion gesperrt.
  • Einen einfachen Editor, um Werte von Attributen zu bearbeiten oder Einträge respektive ganze Zweige zu löschen.
Grundsätzliches Vorwissen
Um DocVariant zu verstehen, muss man sich mit dem Typ Variant auskennen. Zur Umsetzung werden keine obskuren Hacks, sondern geschickt die vorhandenen Möglichkeiten genutzt. Wem die Klasse System.Variants.TInvokeableVariantType etwas sagt, kann den folgenden Absatz überlesen. Wichtig: Die Ausführungen gelten für 32-Bit und ab Delphi XE7. Für ältere Versionen ist TVariantManager das Stichwort. Die Erklärungen sind nur rudimentär, deshalb begleitend die beiden Einstiegslinks in die Delphi Hilfe zum weiteren Nachlesen: System.Variant, System.Variants.TInvokeableVariantType.

1) Am Anfang der System.Variant
Ein Variant wird als 16-Byte großes Record gespeichert. Die ersten beiden Bytes sind für das Typ-Feld (VType) einer Variante reserviert. Für die System-Typen sind Konstanten definiert, z.B. ist es für Boolean die Konstante varBoolean mit dem Wert 11. Der Wertebereich darf die ersten 12 Bits umfassen. Es können eigene Typen registriert werden. Für diese soll der Typ-Wert ab 272 beginnen. Es gibt Funktionen zum Abfragen des Types:
Delphi-Quellcode:
var
  v: Variant;
begin
  v := 'Test';
  if VarIsType(v, varUString) then
    ShowMessage(Format('%d - %s', [VarType(v), VarTypeAsText(VarType(v))])); // Result: 258 - UnicodeString
Ein Variant lässt sich nach TVarData hart casten. Das ist möglich, weil gilt: SizeOf(Variant) = SizeOf(TVarData) und die interne Anordnung nachgebildet wird.
Delphi-Quellcode:
var
  v: Variant;
begin
  v := True;
  if TVarData(v).VType = varBoolean then
  begin
    var b: Boolean := TVarData(v).VBoolean;
    ShowMessage(BoolToStr(b, True)); // Result: True
2) DocVariant, ein Variant mit Struktur
Das Geniale an einem DocVariant ist, dass es eine beliebig komplexe Datenstruktur aus Objekt(en) und/oder Arrays, oder aus Kombinationen von beiden sein kann. Nur der zur Verfügung stehende Arbeitsspeicher ist die Grenze. In der mORMot Hilfe steht: "A custom variant type used to store any JSON/BSON document-based content - i.e. name/value pairs for objects, or an array of values (including nested documents), stored in a TDocVariantData memory structure". Die DocVariant Syntax sieht für Pascal Entwickler etwas gewöhnungsbedürftig aus, weil es eher an eine Scriptsprache erinnert. Mehr dazu in der Hilfe nachlesen. Ein DocVariant lässt sich auf mehrere Arten erstellen. Ein Beispiel zum Kennenlernen:
Delphi-Quellcode:
var
  v: Variant;
begin
  TDocVariant.NewFast(v);
  v.a := 10;
  v.b := 3.3;
  v.c := True;
  
  // DocVariant: Count=3, Value(0)=10, Value('b')=3,30, NameIndex('b')=2
  ShowMessage(Format('DocVariant: Count=%d, Value(0)=%d, Value(''b'')=%f, NameIndex(''b'')=%d',
    [Integer(v._Count), Integer(v.Value(0)), Double(v.Value('b')), Integer(v.NameIndex('b'))]));
Es wird ein DocVariant vom Typ Objekt mit den Variant-Optionen [dvoReturnNullForUnknownProperty, dvoValueCopiedByReference] erzeugt. Beim Erstellen kann der Typ und/oder die Variant-Option(en) angeben, oder eine der neun New... Initialisierungsfunktionen mit sinnvoller Vorauswahl verwendet werden. Was man auch im Beispiel sieht, ist die Möglichkeit Pseudo-Eigenschaften oder -Funktionen aufzurufen. Von diesen gibt es eine stattliche Anzahl: _Count, _Kind, _JSON, _(idx), Value(idx), Value(Name), Name(idx), Add(Item), Add(Name, Value), Exists(Name), Delete(idx), Delete(Name) und NameIndex(Name).

3) Ein Variant und seine Pseudo-Eigenschaften und -Funktionen
Wie oben beschrieben, besitzt jeder Variant einen Typ-Wert. Dieser Wert ist gleichzeitig Selektor, der bestimmt, welche Bearbeitungsklasse herangezogen wird. Für selbst definierte Variant-Typen, zum einfachen und schnellen Zugriff auf alle Erweiterungen, verwendet mORMot die eigene Basisklasse TSynInvokeableVariantType, abgeleitet von TInvokeableVariantType. Diese Klasse ist Vorfahre der Klassen: TDocVariant, TBsonVariant, TSqlDBRowVariantType und TOrmTableRowVariant. Mit der Funktion SynRegisterCustomVariantType werden diese Klassen im System registriert. Eine Objektinstanz der Klasse TDocVariant wird nach der Registrierung in der Variable DocVariantType gespeichert. Wichtiger in der Praxis ist die Variable DocVariantVType. Sie enthält den VarType Wert, mit dem ein DocVariant im System registriert ist. Immer wenn jetzt ein Variant mit dem Typ-Wert DocVariantVType unterwegs ist, steht im Hintergrund die Klasse TDocVariant zur Übernahme der eigentlichen Arbeit parat. Am einfachsten lässt es sich nachvollziehen, wenn in der Unit mormot.core.variants Breakpoints an den Anfang der Funktionen TSynInvokeableVariantType.DispInvoke, TDocVariant.DoFunction und TDocVariant.DoProcedure gesetzt werden. Schritt für Schritt lässt sich dann nachverfolgt, wie die Abläufe im Hintergrund vonstattengehen.

4) TDocVariantData, das Geheimnis hinter DocVariant
Kein Geheimnis, aber eine clevere Idee. Die Struktur von TDocVariantData ist wie folgt definiert:
Delphi-Quellcode:
TDocVariantData = record
private
  // note: this structure uses all TVarData available space: no filler needed!
  VType: TVarType; // 16-bit
  VOptions: TDocVariantOptions; // 16-bit
  VName: TRawUtf8DynArray; // pointer
  VValue: TVariantDynArray; // pointer
  VCount: integer; // 32-bit
Wer richtig zählt, kommt auf 16-Bytes. Damit gilt: SizeOf(Variant) = SizeOf(TVarData) = SizeOf(TDocVariantData) . Wer schreibt TDocVariant.New(v); ruft folgende Funktion auf:
Delphi-Quellcode:
procedure TDocVariantData.Init(aOptions: TDocVariantOptions);
begin
  VType := DocVariantVType;
  aOptions := aOptions - [dvoIsArray, dvoIsObject];
  VOptions := aOptions;
  pointer(VName) := nil; // to avoid GPF when mapped within a TVarData/variant
  pointer(VValue) := nil;
  VCount := 0;
end;
Es initialisiert sich eine Struktur zur Aufnahme von Name-Value Pairs. Es stehen über 150 Eigenschaften und Funktionen, davon mehr als 30 unterschiedliche Init-Varianten, zur Verfügung.

5) Immer auf der sicheren Seite
Man kann einen Variant nach TDocVariantData hart casten. Besser ist es, dafür die Funktion _Safe zu verwenden. Diese Funktion sorgt dafür, dass immer ein DocVariant zurückgeliefert wird. Wenn kein realer vorhanden ist, dann ein Fake-Variant. Dieser ist in der Konstante DocVariantDataFake definiert. Damit lässt es sich schön spuken. Jeder Strich ist wichtig:
Delphi-Quellcode:
var
  v: Variant;
begin
  v := _JSON('{"First": "Tom", "Last": "Selleck", "Profession": "Actor"}');
  // First: Tom, Last: Selleck, Profession: Actor
  ShowMessage(Format('First: %s, Last: %s, Profession: %s', [v.First, v.Last, v.Profession]));

  // What happens if you write the following?
  // Example 1:
  var isPeaPuree: Boolean := TDocVariantData(v).O['One'].O['Two'].O['Three'].B['IsPeaPuree'];
  ShowMessage(Format('Does he like pea puree? %s', [BoolToStr(isPeaPuree, True)]));
  // Result: "Does he like pea puree? False"

  // Example 2:
  _Safe(v).O_['One'].O_['Two'].O_['Three'].B['IsPeaPuree'] := True;
  ShowMessage(v._JSON);
  // Result: "{"First":"Tom","Last":"Selleck","Profession":"Actor","One":{"Two":{"Three":{"IsPeaPuree":true}}}}"
Wie schnell einzelne Bibliotheken beim Einlesen einer JSON-Datei mit 1MB Größe sind, hierzu ein paar Benchmark-Werte:
Plattform Bibliothek Durchsatz MB/s
FPC/Delphi mORMot TOrmTableJson
507
FPC/Delphi mORMot TDocVariantData
169
FPC/Delphi mORMot DynArrayLoadJson
332
FPCfpjson
24
FPCjsontools
38
FPClgenerics
48
FPCSuperObject
10,5
DelphiDelphi's system.json
5,8
DelphiJsonDataObjects
103
DelphiSuperObject
35
DelphiX-SuperObject
1,5
DelphiGrijjy
54
DelphidwsJSON
97
DelphiWinSoft JSON
27
Die Tabelle zeigt, wie herausragend schnell die mORMot Klassen sind. Sie belegen mit deutlichem Vorsprung die ersten drei Plätze. Auch diese Zahlen geben Sicherheit.

JSON-Viewer
Eines vorweg: Das Design des Viewers ist hübsch hässlich. Da noch einige nette Funktionen auf meiner Festplatte liegen, wird das Styling auf Version 2, wahrscheinlicher in die Ewigkeit verschoben.

Mit dem erworbenen Vorwissen ausgestattet, jetzt zur konkreten Implementierung in der Anwendung. Der Viewer ist ausgelagert und von einem Panel abgeleitet. Damit ist die Anzeige von der Logik getrennt und lässt sich wie eine Komponente handhaben. Sie besteht nur aus wenigen Funktionen. Für den Durchgriff auf die Daten sind Methodenreferenzen definiert. Ihr Aufbau ist wie folgt:
Delphi-Quellcode:
TJsonTreePresenter = class(TCustomPanel)
strict private
  FTree: TVirtualStringTree;
  FDocData: PDocVariantData;
  FDocDataName: String;
private
  ...
  procedure DoTreeInitNode(pmSender: TBaseVirtualTree; pmParentNode, pmNode: PVirtualNode; var pmvInitialStates: TVirtualNodeInitStates);
  procedure DoTreeInitChildren(pmSender: TBaseVirtualTree; pmNode: PVirtualNode; var pmvChildCount: Cardinal);
Zur Abbildung des Dokuments sind nur wenige Zeilen Quelltext notwendig:
Delphi-Quellcode:
procedure TJsonTreePresenter.DoTreeInitNode(pmSender: TBaseVirtualTree; pmParentNode, pmNode: PVirtualNode; var pmvInitialStates: TVirtualNodeInitStates);
var
  title: String;
  docVD: PDocVariantData;
begin
  if pmParentNode = Nil then
  begin
    title := FDocDataName;
    docVD := FDocData;
  end
  else
  begin
    docVD := PJsonDocNode(pmParentNode.GetData).docVData;
    if docVD <> Nil then
    begin
      title := DocVariantPairCaption(docVD, pmNode.Index);
      docVD := _Safe(docVD.Values[pmNode.Index]);
    end;
  end;

  if (docVD <> Nil)
    and (docVD.VarType = DocVariantVType) then
  begin
    var nodeData: PJsonDocNode := PJsonDocNode(pmNode.GetData);
    nodeData.nodeName := title;
    nodeData.docVData := docVD;
    if docVD.Count > 0 then
      Include(pmvInitialStates, ivsHasChildren);
  end;
end;

procedure TJsonTreePresenter.DoTreeInitChildren(pmSender: TBaseVirtualTree; pmNode: PVirtualNode; var pmvChildCount: Cardinal);
var
  docVD: PDocVariantData;
begin
  docVD := PJsonDocNode(pmNode.GetData).docVData;
  if docVD <> Nil then
    pmvChildCount := docVD.Count;
end;
Für den Zugriff auf den Datenbaum sind die Funktionen EditSelectedDocData, DeleteSelectedDocData und ProcessSelectedDocNode vorhanden. Ich hoffe, ich habe am Anfang nicht zu viel versprochen und das Weiterlesen hat sich gelohnt. Wer keine ausreichend große JSON-Datei zum Testen zur Verfügung hat, kann sich Daten von GitHub laden.

Zusammenfassung
mORMot ist gut dokumentiert. Die Hilfe umfasst mehr als 2500 Seiten. Davon enthalten die ersten ca. 650 Seiten einen sehr lesenswerten allgemeinen Teil, der Rest ist API Dokumentation. mORMot muss nicht in der IDE installierten werden! Es reicht aus, die Bibliothek mit den Static-Dateien in ein Verzeichnis zu kopieren und die entsprechenden Bibliothekspfade anzulegen. In 3 Minuten ist es auf dem Rechner und bei Nichtgefallen in 3 Sekunden wieder spurlos entfernt. Es stehen viele Beispiele und ein freundliches Forum zur Verfügung.

In der mORMot Reihe bisher veröffentlicht:
  1. ORM und DocVariant kurz vorgestellt
  2. ZIP-Datei als Datenspeicher mit verschlüsseltem Inhalt
  3. Einführung in methodenbasierte Services - Server und Client
  4. Einführung in Interface-based Services - Server und Client
  5. Mustache Editor mit integriertem HTTP-Server zum Anzeigen von HTML Seiten
  6. Mediator zur Anbindung von FastReport mit Vorschau und/oder Designer
Bis bald...
Thomas
Angehängte Dateien
Dateityp: zip JsonViewerSource.zip (6,6 KB, 51x aufgerufen)
Dateityp: zip JsonViewerExe.zip (1,39 MB, 45x aufgerufen)
  Mit Zitat antworten Zitat
Themen-Optionen Thema durchsuchen
Thema durchsuchen:

Erweiterte Suche
Ansicht

Forumregeln

Es ist dir nicht erlaubt, neue Themen zu verfassen.
Es ist dir nicht erlaubt, auf Beiträge zu antworten.
Es ist dir nicht erlaubt, Anhänge hochzuladen.
Es ist dir nicht erlaubt, deine Beiträge zu bearbeiten.

BB-Code ist an.
Smileys sind an.
[IMG] Code ist an.
HTML-Code ist aus.
Trackbacks are an
Pingbacks are an
Refbacks are aus

Gehe zu:

Impressum · AGB · Datenschutz · Nach oben
Alle Zeitangaben in WEZ +1. Es ist jetzt 14:58 Uhr.
Powered by vBulletin® Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz