Saisir des textes avec mise en forme et les afficher tels quels grâce aux composants de TMS Software

Rédigé le 30 août 2017, cet article a fait l'objet d'un ajout suite à la prise en compte d'une demande d'évolution par TMS Software en date du 10 octobre 2017. Vous pouvez donc le lire dans son intégralité, mais n'utilisez que le code source final dans vos programmes.

Dans tous les projets comportant une base de données nous avons besoin de proposer des champs de saisie de textes à nos utilisateurs. Il arrive parfois que nous devions prévoir également une mise en forme évoluée de ces textes par exemple pour ensuite générer des courriers, des emails ou des PDF.

En standard, Delphi ne propose pas de composant évolué pour saisir des textes avec mise en forme. On peut bidouiller des trucs à partir d'un TMemo mais ça reste toujours compliqué à mettre en oeuvre.

Il n'en propose pas non plus pour l'affichage de textes avec mise en forme. On peut afficher des textes avec des styles, cumuler l'affichage de textes sous différents formats (gras, italique, soulignés, en couleur, de différentes typos, ...) les uns à la suite des autres grâce à des TLabel dans un TFlowLayout, même gérer des événements sur certains (pour cliquer sur des URL par exemple), mais ça reste quelque chose de lourd et fastidieux à programmer.

Heureusement pour nous il existe TMS Software : l'un des partenaires historiques de Borland / Codegear / Inprise / Embarcadero qui propose de très nombreux composants pour Delphi, C++ Builder et maintenant Lazarus.

Dans ces composants ils en ont deux qui nous intéressent pour Firemonkey : TTMSFMXRichEditor et TTMSFMXHTMLText. Des équivalents existent également pour la VCL et Lazarus. Pour le cas qui nous occupe, ils sont disponibles dans le pack de base de composants TMS pour Firemonkey. Ce pack de composants n'est pas gratuit, mais il n'est pas cher et peu rendre de grands services, tout comme la librairie de composants / fonctions / algorithmes de cryptographie dont j'ai déjà parlé.

Le composant TTMSFMXRichEditor est un éditeur de texte avec mise en forme. Il gère un sous ensemble du format RTF et permet des exports sous différentes formes vers du HTML, du PDF, du RTF et du RTE (son format natif de stockage).

Le composant TTMSFMXHTMLText quant à lui permet d'afficher un sous-ensemble de la norme HTML.

Composant de base pour les textes affichés dans les autres composants de TMS Software, il permet d'afficher des textes avec des images, du gras, du souligné, de l'italique et un peu tout ce qu'on veut tant qu'on sait le coder en HTML. Il n'exécute en revanche pas de Javascript, c'est simplement un moteur d'affichage qui nous permet de gagner un temps fou lors de la création de certains écrans.

Depuis près d'un an maintenant je travaille sur une application mobile qui contient une base documentaire devant être à la fois sérieuse, lisible et visuellement attractive. Le tout fonctionne avec du versioning des textes, plusieurs langues possibles et plusieurs auteurs possibles intervenant de façon déconnectée sur la même base de textes.

Pour la saisie de tout ça il me fallait quelque chose permettant de gérer simplement de la mise en forme et le composant Rich Editor de TMS Software était idéal. Restait la question du stockage, de la synchronisation des informations et surtout de leur affichage sur l'application mobile.

Actuellement TMS Software ne propose pas de passer le contenu de son éditeur enrichi à un composant d'affichage. Je leur ai soumis plusieurs demandes en ce sens afin de nous simplifier les choses. Rien n'est vraiment bloquant puisqu'on peut facilement trouver les informations nécessaires pour faire ce qu'on veut en cherchant un peu dans leurs sources. J'ai toute confiance en eux pour faire évoluer tout ça au fil des versions.

En attendant le composant miracle, je vais vous proposer deux solutions pour parvenir à afficher un texte enrichi grâce aux composants TTMSFMXRichEditor et TTMSFMXHTMLText.

Saisie et enregistrement d'un texte enrichi

Je ne vais pas dire grand chose sur la création du texte à afficher. Il suffit de se reporter à la doc du composant TTMSFMXRichEditor et aux exemples fournis.

Pour faire bref il vous suffit de placer un composant TTMSFMXRichEditor sur une fiche, un composant TTMSFMXRichEditorFormatToolBar pour gérer la mise en forme et un composant TTMSFMXRichEditorEditToolBar pour les UNDO/REDO et la gestion du fichier (ouvrir, sauvegarder, ...). Associez ces trois composants entre eux et c'est opérationnel : vous avez un traitement de textes.

Pour finir, enregistrez le contenu de votre texte au format natif du rich editor : un fichier .RTE Ce fichier contient tout : texte, mise en forme et images. Il ne vous reste plus qu'à l'embarquer avec votre application mobile ou le synchroniser via Internet.

Vous pouvez aussi générer du HTML mais si vous incluez des images dans votre texte elles seront générées sous forme de fichiers séparés. Cela fera autant de fichiers supplémentaires à synchroniser ou embarquer avec votre application rendant les tests, la mise en production et la maintenance de l'application plus compliquée.

Cette phase étant terminée, ayant un fichier RTE, il ne vous reste qu'à l'afficher où vous le voulez.

Afficher un texte enrichi avec un code propre (version temporaire)

Nous allons utiliser le composant TTMSFMXHTMLText pour l'affichage en le plaçant dans un TVertScrollBox. Cela permet de gérer les débordements du texte hors de l'écran. A cause d'une petite anomalie il faudra ajouter cette ligne dans l'événement onResize du TVertScrollBox.

procedure TForm1.VertScrollBox1Resized(Sender: TObject);
begin
  TMSFMXHTMLText1.Width := VertScrollBox1.ClientWidth;
end;

On s'assure ainsi que le texte prend bien toute la largeur de sa zone d'affichage. Faites le même s'il est avec un alignement TAlignLayout.Top (recommandé), car un bogue ne le prend pas toujours en compte correctement.

Comme le composant TTMSFMXHTMLText ne prend pas en charge le format RTE, il faut ajouter un composant TTMSFMXRichEditor à votre programme. Il devra être caché et inactif pour être tranquille. Mettez donc ses propriétés Visible et Enabled à false.

L'affichage du texte mis en forme se fait ensuite très simplement grâce à ces lignes de code :

  TMSFMXRichEditor1.LoadFromFile(TPath.Combine(TPath.GetDocumentsPath, 'demo.rte'));
  TMSFMXRichEditor1.SpaceAsNbsp := false;
  TMSFMXHTMLText1.Text := TMSFMXRichEditor1.ContentAsPlainHTML(TPath.GetDocumentsPath);

Bien entendu vous devez adapter le chemin d'accès par rapport à votre fichier RTE lors de son chargement.

Le texte apparaît correctement mis en forme avec ces lignes mais il y a deux problèmes :

  • les images n'apparaissent pas
  • des fichiers images sont créés lors de l'opération et ne sont pas supprimés

Ce qu'il faut savoir c'est que le composant TTMSFMXHTMLText prend les images qu'il doit afficher sur un TTMSFMXBitmapContainer. Il ne sait pas les récupérer depuis un fichier. Les tags HTML IMG doivent donc avoir un SRC correspondant à une image stockée dans le TTMSFMXBitmapContainer qui lui est associé.

Le composant TTMSFMXRichEditor ne propose (à ce jour) pas de méthode pour alimenter un TTMSFMXBitmapContainer lorsqu'il exporte son source HTML. Il ne sort les images que sous forme de fichiers (ou dans un format interne non géré par TTMSFMXHTMLText et défini par sa propriété HTMLImages). Il faut donc ruser un peu.

Après lecture du source du composant TTMSFMXRichEditor on peut en déduire comment en récupérer les images et les transférer nous-mêmes dans le TTMSFMXBitmapContainer à ajouter sur notre fiche et à associer au TTMSFMXHTMLText. Voici le code à utiliser.

  imgidx := 0;
  if (TMSFMXRichEditor1.Context.Content.Count > 0) then
    for i := 0 to TMSFMXRichEditor1.Context.Content.Count - 1 do
      if (TMSFMXRichEditor1.Context.Content.Items[i] is TPictureElement) then
      begin
        pic := TMSFMXRichEditor1.Context.Content.Items[i] as TPictureElement;
        stream := tmemorystream.Create;
        try
          pic.Picture.SaveToStream(stream);
          with TMSFMXBitmapContainer1.Items.Add do
          begin
            Name := 'image' + imgidx.ToString + '.PNG';
            Bitmap := tbitmap.CreateFromStream(stream);
            inc(imgidx);
          end;
        finally
          stream.Free;
        end;
      end;

Le basculement depuis la liste des images liées à l'éditeur vers le conteneur se fait par un flux et on parcourt l'arborescence du DOM de l'éditeur pour récupérer toutes les images dans le même ordre que le fait le module d'export HTML de l'éditeur enrichi.

Une fois les images récupérées, il ne reste plus qu'à modifier le code d'affichage pour qu'il les prenne en compte. Ca se fait simplement en modifiant les URL accédant aux fichiers locaux.

TMSFMXHTMLText1.Text := TMSFMXRichEditor1.ContentAsPlainHTML(TPath.GetDocumentsPath).Replace('file://' + TPath.GetDocumentsPath, '');

Ceci étant réglé, les images et le texte s'affichent correctement mais des fichiers PNG restent créés sur l'appareil à chaque affichage. Il ne reste plus qu'à les supprimer dans une nouvelle boucle ou les laisser puisque de toute façon ils reviendront.

Le code complet à utiliser devient donc celui-ci:

implementation

{$R *.fmx}

uses System.ioutils;

procedure TForm1.FormCreate(Sender: TObject);
var
  i: integer;
  imgidx: integer;
  pic: TPictureElement;
  stream: tmemorystream;
begin
  TMSFMXRichEditor1.LoadFromFile(TPath.Combine(TPath.GetDocumentsPath,
    'demo.rte'));
  TMSFMXRichEditor1.SpaceAsNbsp := false;

  imgidx := 0;
  if (TMSFMXRichEditor1.Context.Content.Count > 0) then
    for i := 0 to TMSFMXRichEditor1.Context.Content.Count - 1 do
      if (TMSFMXRichEditor1.Context.Content.Items[i] is TPictureElement) then
      begin
        pic := TMSFMXRichEditor1.Context.Content.Items[i] as TPictureElement;
        stream := tmemorystream.Create;
        try
          pic.Picture.SaveToStream(stream);
          with TMSFMXBitmapContainer1.Items.Add do
          begin
            Name := 'image' + imgidx.ToString + '.PNG';
            Bitmap := tbitmap.CreateFromStream(stream);
            inc(imgidx);
          end;
        finally
          stream.Free;
        end;
      end;

  TMSFMXRichEditor1.HTMLImages := THTMLImageGeneration.igFile;
  TMSFMXHTMLText1.Text := TMSFMXRichEditor1.ContentAsPlainHTML(TPath.GetDocumentsPath).Replace('file://' + TPath.GetDocumentsPath, '');
  TMSFMXHTMLText1.Width := VertScrollBox1.ClientWidth;
end;

procedure TForm1.VertScrollBox1Resized(Sender: TObject);
begin
  TMSFMXHTMLText1.Width := VertScrollBox1.ClientWidth;
end;

end.

Ca, c'est la version propre mais elle génère inutilement des fichiers et donc augmente la consommation de batterie et l'espace disque utilisé par l'application.

Afficher un texte enrichi en bidouillant le composant de TMS Software (à éviter mais efficace)

Ayant réussi à afficher les textes avec les images il ne reste en réalité qu'un seul problème : les fichiers créés inutilement.

Pour le moment aucune valeur de TTMSFMXRichEditor.HTMLImages ne permet de générer proprement le HTML sans créer ces fichiers. Il faut donc y remédier.

Avant de continuer je tiens à rappeler que modifier les fichiers sources de composants tiers implique que ceux-ci seront écrasés lors de l'installation d'une nouvelle version. Vous devez soit faire la modification à chaque fois, soit copier le fichier dans le dossier de votre projet et vous assurer à chaque nouvelle version qu'il n'y a pas d'incompatibilité. Il est donc préférable de ne pas trop jouer à ça. De plus pour une question de licence vous ne devez en aucun cas distribuer les sources de composants tiers, même modifiés, sans en avoir eu l'autorisation préalable.

Le bidouillage que je vous propose ici fait l'objet d'une demande de modification auprès de TMS Software et sera peut-être intégré nativement dans une future version de l'éditeur enrichi. Pensez à faire un tour de temps en temps sur leur page des demandes d'évolutions afin de voter pour celles qui vous intéressent et d'ajouter les vôtres. Les composants évoluent grâce aux demandes de leurs utilisateurs. Il est important de faire ce feedback avec les équipes de TMS Software.

Les modifications qui suivent sont à faire dans l'unité FMX.TMSRichEditorBase.

Comme aucune valeur de TTMSFMXRichEditor.HTMLImages ne permet d'éviter la création des fichiers d'images, on va en ajouter une. Je l'ai appelée igNoFile puisqu'elle partira de igFile et excluera toute notion de chemin d'accès et d'export d'image. Trouvez la ligne de déclaration du type THTMLImageGeneration et ajoutez la nouvelle valeur.

  THTMLImageGeneration = (igFile, igInline, igID, igNone, igNoFile);

Trouvez ensuite le bloc de code permettant la génération des tags IMG et donc la gestion des images. C'est dans la méthode TTMSFMXRichEditorBase.GetContentAsHTML Pour trouver le bon endroit, cherchez tout simplement le case concernant HTMLImages et sa valeur igFile. Ajoutez ensuite le code permettant de traiter la nouvelle valeur.

        case HTMLImages of
        igFile:
//code source de TMS Software non reproduit, n'y touchez pas

        igNoFile:
          begin
            imgname := 'image' + inttostr(imgidx) + '.' + imgext;
            s := s + '<IMG src="' + imgname + '"' + picsz + '>';
          end;

// suite du code de TMS Software
        igInline:

Avec cette simple modification on peut simplifier un peu le code d'affichage qui devient :

procedure TForm1.FormCreate(Sender: TObject);
var
  i: integer;
  imgidx: integer;
  pic: TPictureElement;
  stream: tmemorystream;
begin
  TMSFMXRichEditor1.LoadFromFile(TPath.Combine(TPath.GetDocumentsPath, 'demo.rte'));
  TMSFMXRichEditor1.SpaceAsNbsp := false;

  imgidx := 0;
  if (TMSFMXRichEditor1.Context.Content.Count > 0) then
    for i := 0 to TMSFMXRichEditor1.Context.Content.Count - 1 do
      if (TMSFMXRichEditor1.Context.Content.Items[i] is TPictureElement) then
      begin
        pic := TMSFMXRichEditor1.Context.Content.Items[i] as TPictureElement;
        stream := tmemorystream.Create;
        try
          pic.Picture.SaveToStream(stream);
          with TMSFMXBitmapContainer1.Items.Add do
          begin
            Name := 'image' + imgidx.ToString + '.PNG';
            Bitmap := tbitmap.CreateFromStream(stream);
            inc(imgidx);
          end;
        finally
          stream.Free;
        end;
      end;

  TMSFMXRichEditor1.HTMLImages := THTMLImageGeneration.igNoFile;
  TMSFMXHTMLText1.Text := TMSFMXRichEditor1.ContentAsPlainHTML;
  TMSFMXHTMLText1.Width := VertScrollBox1.ClientWidth;
end;

Idéalement il faudrait pousser jusqu'à l'alimentation du TTMSFMXBitmapContainer au même endroit que la gestion du tag IMG. Cela ferait économiser du temps CPU. Malheureusement il n'est pas accessible depuis le composant TTMSFMXRichEditor donc pour le moment on doit conserver sa boucle de remplissage extérieure.

En attendant que TMS Software nous propose mieux, nous avons ainsi une solution pour afficher des textes enrichis, contenant aussi des images, en ne trimballant qu'un seul fichier contenant à la fois la mise en forme, le texte et ses visuels.

Dans des cas d'applications plus simples, où vous pouvez vous permettre de tout faire avant publication du programme ou de l'application, je vous recommande pour le moment de faire un export HTML proposé nativement dans TTMSFMXRichEditor puis d'embarquer le source HTML et les images directement dans votre programme. En revanche, lorsque vous avez de nombreuses pages de textes, que ceux-ci sont amenés à évoluer une fois l'application diffusée, il est préférable de mettre en place un système de synchronisation via Internet et de limiter le nombre de fichiers à traiter.


Suite à ma demande de modification concernant le composant Rich Editor, TMS Software a publié le 10 octobre 2017 un type d'export igReference. Il n'est donc plus nécessaire de modifier le code source de leur librairie pour récupérer les références aux images provenant d'un éditeur de texte enrichi.

Le code à utiliser est le même que ci-dessus avec igNoFile, dans lequel il faut utiliser igReference à la place de igNoFile.

procedure TForm1.FormCreate(Sender: TObject);
var
  i: integer;
  imgidx: integer;
  pic: TPictureElement;
  stream: tmemorystream;
begin
{$IFDEF MSWINDOWS}
  TMSFMXRichEditor1.LoadFromFile('C:\Users\Public\Documents\tmssoftware\TMS FMX UI Pack Demos\PDF\Richeditor\demo.rte');
{$ELSE}
  TMSFMXRichEditor1.LoadFromFile(TPath.Combine(TPath.GetDocumentsPath,
    'demo.rte'));
{$ENDIF}
  TMSFMXRichEditor1.SpaceAsNbsp := false;

  imgidx := 0;
  if (TMSFMXRichEditor1.Context.Content.Count > 0) then
    for i := 0 to TMSFMXRichEditor1.Context.Content.Count - 1 do
      if (TMSFMXRichEditor1.Context.Content.Items[i] is TPictureElement) then
      begin
        pic := TMSFMXRichEditor1.Context.Content.Items[i] as TPictureElement;
        stream := tmemorystream.Create;
        try
          pic.Picture.SaveToStream(stream);
          with TMSFMXBitmapContainer1.Items.Add do
          begin
            Name := 'image' + imgidx.ToString + '.PNG';
            Bitmap := tbitmap.CreateFromStream(stream);
            inc(imgidx);
          end;
        finally
          stream.Free;
        end;
      end;

  TMSFMXRichEditor1.HTMLImages := THTMLImageGeneration.igReference;
  TMSFMXHTMLText1.Text := TMSFMXRichEditor1.ContentAsPlainHTML;
  TMSFMXHTMLText1.Width := VertScrollBox1.ClientWidth;
end;

Notez quand même que selon les appareils cette technique n'est pas hyper véloce. L'utilisation de caches est fortement recommandée, notamment sur les mobiles et tablettes.


Mug Toucan DX dans la baie de RioMug carte postale Sydney