[Calc] Construire un add-in

Venez découvrir tous les tutoriels, modèles et autres foires aux questions afin de maîtriser rapidement votre suite bureautique favorite.

Modérateur : Vilains modOOs

Règles du forum
Aucune question dans cette section !
Cette section est faite pour présenter les tutoriels. Si vous avez une question sur l'installation, le fonctionnement ou l'utilisation, vous devez poster dans la section du module où se produit le problème.

Ce tutoriel vous a-t-il aidé ou répondu à votre problème ?

Oui
5
50%
Non
2
20%
En partie
3
30%
 
Nombre total de votes : 10

Avatar de l’utilisateur
Hubert Lambert
SuppOOrter
SuppOOrter
Messages : 1214
Inscription : 06 avr. 2016 09:26

[Calc] Construire un add-in

Message par Hubert Lambert »

  
Après avoir un peu exploré les possibilités offertes par les composants de type "add-in" pour Calc, et compte tenu de l'importante dispersion de l'information disponible (par ailleurs souvent obsolète), je vous propose ci-après un petit retour d'expérience en forme de tutoriel.


A. Qu'est-ce qu'un "add-in" ?
Un "add-in" est un type d'extension permettant l'intégration complète de fonctions personnalisées dans le module Calc. Un fonction ajoutée comme add-in est ainsi disponible depuis l'assistant de fonctions et expose ses paramètres dans la barre de formule, comme une fonction native.

Le tutoriel se propose ainsi d'ajouter deux fonctions de type texte :
  • JOINDRE (plage [, séparateur]) pour concaténer le texte contenu dans une plage de cellule ;
  • SCINDER (texte [, séparateur]) pour scinder un texte sur plusieurs cellules (fonction matricielle donc).
:idea: Ces deux fonctions sont inspirées d'un précédent exercice disponible ici.


B. Prérequis
Une connaissance minimale du langage python et de la passerelle PyUno sera sans doute utile pour suivre la suite. De manière générale, une bonne connaissance de l'API d'OpenOffice est également nécessaire pour le développement d'une extension, quelle qu'elle soit.

L'installation du "software development kit" (SDK) est requise puisque celui-ci inclut les outils de développement nécessaires (utilitaires de compilation et fichiers de définition d'interfaces).


C. Avertissement
L'exemple développé dans ce tutoriel est écrit en python, un langage de script parfaitement adapté au développement d'extensions, mais un add-in peut évidemment être écrit dans n'importe quel langage supporté par OpenOffice (C++, Java...) pour autant qu'il autorise l'implémentation de classes ou d'objets nouveaux. Il n'est donc pas possible d'écrire une telle extension en basic.

La version de l'interpréteur python embarqué est différente entre OpenOffice (python 2) et LibreOffice (python 3). En outre, les dernières moutures de LibreOffice intègrent une version de PyUno légèrement différente. Il est important d'en être conscient si l'on souhaite développer une fonction compatible avec les deux suites.

Ce tutoriel est basé sur un environnement linux "classique" (Linux Mint) pour des raisons purement pratiques, entre autres l'accès aisé aux compilateurs et aux librairies requises. Mais il devrait pouvoir s'adapter sans trop de difficultés à d'autres systèmes d'exploitation.


D. Les composants d'un add-in
Un add-in est composé, comme une extension classique ("add-on"), d'une série de fichiers compressés dans un fichier zip portant l'extension ".oxt". Un add-in doit contenir au minimum les éléments suivants :
  1. un fichier binaire (extension .rdb) permettant d'ajouter à la base de registre la définition des fonctions personnelles ("interface") ;
  2. un fichier xml (extension .xcu) contenant les informations requises pour l'interface utilisateur : noms et descriptions des fonctions, noms et descriptions des paramètres, localisation, etc.
  3. le fichier d'implémentation proprement dit (extension ".py" en l'occurrence) ;
  4. un fichier description.xml contenant les éléments d'information relatifs à l'extension ;
  5. un sous-répertoire META-INF contenant un fichier manifest.xml, lequel permet au programme d'identifier les éléments précités.
:idea: Pour éviter toute mésaventure liée à l'emploi des caractères accentués, tous ces fichiers seront encodés en utf-8.


  1. Le fichier RDB
La création d'un fichier RDB nécessite deux étapes préalables : l'écriture d'un fichier de définition IDL et sa compilation dans un format intermédiaire URD.

   1.a. Le fichier IDL
Les nouvelles fonctions doivent être définies au moyen d'un langage d'interface spécifique, nommé IDL.

Recopier le code ci-dessous et le sauver en tant que XTutoAddIn.idl :

Code : Tout sélectionner

#include <com/sun/star/uno/XInterface.idl>

module org { module forumfr { module TutoAddin {

    interface XTutoAddIn
    {
        string pyjoin(
            [in] sequence< sequence< string > > strings,
            [in] any separator
            );

        sequence< sequence< string > > pysplit(
            [in] string basestring,
            [in] any separator
            );
    };

};};};
Commentaires
  • L'instruction #include permet d'importer les interfaces utilisées par le fichier de définition. Un add-in requiert au minimum l'interface XInterface.
    :idea: On trouve encore plusieurs sites expliquant que les interfaces XAddIn, XServiceName et optionnellement XCompatibilityNames sont également nécessaires. Ces informations sont aujourd'hui obsolètes : l'interface XAddIn n'est plus requise et les deux autres sont remplacées par le fichier de configuration XCU décrit plus loin.
  • La seconde ligne définit l'espace de nom propre à la nouvelle interface.
  • La troisième ligne ouvre la définition de l'interface XTutoAddIn, qui comporte la "signature" des deux fonctions.
    Chaque fonction est définie par son type et par son nom, suivis du type et du nom de chaque paramètre (ceux-ci précédés du code de direction [in] sur lequel il n'est pas utile de s'étendre ici).
Notes sur quelques types particuliers
  • Les types disponibles pour un add-in sont décrits (en anglais) ici.
  • Le type any renvoie notamment la valeur void ("vide", ou None en python) lorsque le paramètre n'est pas fourni : c'est donc ce type qu'il faut retenir pour un paramètre optionnel.
  • Le type com.sun.star.table.XCellRange permet d'obtenir en argument, non les valeurs de la plage, mais l'objet uno CellRange correspondant, ce qui permet notamment d'accéder aux propriétés des cellules référencées.
  • Le type com.sun.star.beans.XPropertySet permet d'obtenir en argument l'objet "document" contenant la formule, offrant donc l'accès aux propriétés de celui-ci. Ce type, qui ne peut être défini que pour un seul argument par fonction, est en outre implicite : il est "invisible" pour l'utilisateur, Calc se chargeant de fournir l'argument au moment d'évaluer la formule (l'extension numbertext par exemple utilise un argument de ce type pour accéder à la langue par défaut de l'interface).
   1.b. Le fichier URD
Le fichier IDL est compilé en un fichier binaire URD grâce à la commande idlc, fournie avec le SDK. Exemple de syntaxe :

Code : Tout sélectionner

./idlc -I ../idl/ /chemin/vers/XTutoAddIn.idl
L'option -I permet de spécifier l'emplacement des fichiers d'interface utilisés (dans l'exemple com/sun/star/uno/XInterface.idl).

Le fichier URD est créé dans le même répertoire que le fichier IDL.

   1.c. Compilation finale et fichier RDB
Le fichier URD peut enfin être transformé en un fichier RDB à l'aide de la commande regmerge (disponible désormais dans le répertoire d'installation d'OpenOffice) :

Code : Tout sélectionner

./regmerge /chemin/vers/XTutoAddIn.rdb /UCR /chemin/vers/XTutoAddIn.urd
L'option /UCR correspond à la clé de registre sous laquelle les définitions de fonctions sont supposées être enregistrées.


  2. Le fichier XCU
Le fichier XCU fournit au programme toutes les informations utiles pour l'affichage des noms de fonctions et de paramètres dans l'assistant de fonctions.

Recopier le code ci-dessous et le sauver en tant que TutoAddIn.xcu :

Code : Tout sélectionner

<?xml version="1.0" encoding="UTF-8"?>
<oor:component-data xmlns:oor="http://openoffice.org/2001/registry" xmlns:xs="http://www.w3.org/2001/XMLSchema" oor:name="CalcAddIns" oor:package="org.openoffice.Office">
<node oor:name="AddInInfo">
<node oor:name="org.forumfr.tutoriels.TutoAddin" oor:op="replace">
<node oor:name="AddInFunctions">
    <node oor:name="pyjoin" oor:op="replace">
        <prop oor:name="DisplayName">
            <value xml:lang="en">JOIN</value>
            <value xml:lang="fr">JOINDRE</value>
        </prop>
        <prop oor:name="Description">
            <value xml:lang="en">Concatenate strings from the referred range with a given separator.</value>
            <value xml:lang="fr">Concatène les textes de la plage source avec un séparateur donné.</value>
        </prop>
        <prop oor:name="Category"><value>Text</value></prop>
        <node oor:name="Parameters">
            <node oor:name="strings" oor:op="replace">
                <prop oor:name="DisplayName">
                    <value xml:lang="en">range</value>
                    <value xml:lang="fr">plage</value>
                </prop>
                <prop oor:name="Description">
                    <value xml:lang="en">The range containing the strings to concatenate.</value>
                    <value xml:lang="fr">La plage contenant les textes à concaténer.</value>
                </prop>
            </node>
            <node oor:name="separator" oor:op="replace">
                <prop oor:name="DisplayName">
                    <value xml:lang="en">separator</value>
                    <value xml:lang="fr">séparateur</value>
                </prop>
                <prop oor:name="Description">
                    <value xml:lang="en">String used as separator (defaults to space).</value>
                    <value xml:lang="fr">Texte utilisé comme séparateur (espace par défaut).</value>
                </prop>
            </node>
        </node>
    </node>
    <node oor:name="pysplit" oor:op="replace">
        <prop oor:name="DisplayName">
            <value xml:lang="en">SPLIT</value>
            <value xml:lang="fr">SCINDER</value>
        </prop>
        <prop oor:name="Description">
            <value xml:lang="en">Split a string with a given separator.</value>
            <value xml:lang="fr">Scinde un texte avec un séparateur spécifique.</value>
        </prop>
        <prop oor:name="Category"><value>Text</value></prop>
        <node oor:name="Parameters">
            <node oor:name="basestring" oor:op="replace">
                <prop oor:name="DisplayName">
                    <value xml:lang="en">string</value>
                    <value xml:lang="fr">texte</value>
                </prop>
                <prop oor:name="Description">
                    <value xml:lang="en">The string to be splitted.</value>
                    <value xml:lang="fr">Le texte à scinder.</value>
                </prop>
            </node>
            <node oor:name="separator" oor:op="replace">
                <prop oor:name="DisplayName">
                    <value xml:lang="en">separator</value>
                    <value xml:lang="fr">séparateur</value>
                </prop>
                <prop oor:name="Description">
                    <value xml:lang="en">String used as separator (defaults to space).</value>
                    <value xml:lang="fr">Texte utilisé comme séparateur (espace par défaut).</value>
                </prop>
            </node>
        </node>
    </node>
</node>
</node>
</node>
</oor:component-data>
Commentaires

Code : Tout sélectionner

<node oor:name="org.forumfr.tutoriels.TutoAddin" oor:op="replace">
Cette ligne est importante : c'est elle qui permet de faire le lien avec le fichier d'implémentation (voir plus loin). Il importe donc de recopier strictement le nom d'implémentation défini dans ce dernier.

Code : Tout sélectionner

<node oor:name="AddInFunctions">
    <node oor:name="pyjoin" oor:op="replace">
        [...]
    </node>
    <node oor:name="pysplit" oor:op="replace">
        [...]
    </node>
</node>
Chaque fonction est décrite dans un nœud dépendant d'un nœud principal AddInFunctions. Un nœud "fonction" comprend les propriétés suivantes :
  • DisplayName : le nom qui sera affiché dans Calc ;
  • Description : la description utilisée par l'assistant de fonction ;
  • Category : la catégorie utilisée par l'assistant de fonction. Les catégories disponibles sont (en anglais uniquement) : Database, Date&Time, Financial, Information, Logical, Mathematical, Matrix, Statistical, Spreadsheet, Text et Add-In.
Il existe également une propriété CompatibilityName, non utilisée dans le cadre de ce tutoriel mais requise pour des besoins de compatibilité avec Excel lorsqu'un même add-in est développé pour les deux suites.

Chaque nœud "fonction" possède également un nœud subordonné Parameters qui comprend la description de chacun des paramètres attendus :

Code : Tout sélectionner

        <node oor:name="Parameters">
            <node oor:name="strings" oor:op="replace">
                [...]
            </node>
            <node oor:name="separator" oor:op="replace">
                [...]
            </node>
        </node>
Un paramètre est décrit, de manière similaire à une fonction, avec les propriétés DisplayName et Description.


  3. Le fichier d'implémentation
Recopier le code ci-dessous et le sauver en tant que TutoAddIn.py :

Code : Tout sélectionner

# -*- coding: utf-8 -*- 

import unohelper
from itertools import chain
from com.sun.star.lang import IllegalArgumentException
from org.forumfr.TutoAddin import XTutoAddIn


class TutoAddIn(unohelper.Base, XTutoAddIn):
    def __init__(self, ctx):
        pass

    def pyjoin(self, strings, separator):
        sep = separator==None and " " or separator
        s = tuple(chain.from_iterable(strings))
        try:
            return sep.join(s)
        except AttributeError:
            raise IllegalArgumentException(u'Second argument must be a string.', self, 2)

    def pysplit(self, basestring, separator):
        if separator == "":
            return ((basestring,),)
        else:
            try:
                r = basestring.split(separator)
                return tuple(zip(r))
            except TypeError:
                raise IllegalArgumentException(u'Second argument must be a string.', self, 2)


g_ImplementationHelper = unohelper.ImplementationHelper()
g_ImplementationHelper.addImplementation(TutoAddIn, "org.forumfr.tutoriels.TutoAddin", ())
Commentaires
  • Il est indispensable d'importer le module unohelper, qui contient toutes les macros utilisées par la passerelle PyUno pour la création et l'enregistrement de l'extension, en particulier la classe ImplementationHelper.
  • La fonction addImplementation de ImplementationHelper attend trois arguments :
    • ‒ la ou les objets/classes implémentés par l'extension ;
      ‒ un nom d'implémentation unique (il ne doit pas entrer en conflit avec d'autres implémentations), qui doit correspondre au nom repris dans le fichier XCU décrit plus haut ;
      ‒ une liste des services implémentés (aucun dans le cas présent).

  4. Le fichier description.xml
Recopier le code ci-dessous et le sauver en tant que description.xml :

Code : Tout sélectionner

<?xml version="1.0" encoding="UTF-8"?>
<description xmlns="http://openoffice.org/extensions/description/2006" 
xmlns:d="http://openoffice.org/extensions/description/2006" 
xmlns:xlink="http://www.w3.org/1999/xlink">

    <version value="0.1" />

    <identifier value="org.forumfr.tutoaddin" /> 

    <dependencies> 
        <OpenOffice.org-minimal-version value="3.0" d:name="OpenOffice.org 3.0"/> 
    </dependencies> 

    <display-name>
        <name lang="en">Two custom functions to illustrate Calc add-in how-to.</name>
        <name lang="fr">Deux fonctions pour illustrer la création d'un add-in pour Calc.</name>
    </display-name>

</description>
Commentaires
  • Ce fichier est commun à toute extension et ses éléments sont décrits en détail dans le guide du développeur (explications en français ici).
  • L'identifiant (<identifier value="org.forumfr.tutoaddin" />) est indépendant du nom d'implémentation décrit plus haut mais doit lui-même être unique : il permet d'identifier sans ambiguïté une extension, notamment dans le contexte de mises à jour.

  5. Le fichier manifest.xml
Ce fichier est commun à toute extension et doit être placé, par convention, dans un sous-répertoire nommé META-INF. En déclarant chacun des composants de l'extension (nature et localisation), il permet au programme de les connaître.

Recopier le code ci-dessous et le sauver en tant que manifest.xml :

Code : Tout sélectionner

<manifest:manifest>
<manifest:file-entry manifest:media-type="application/vnd.sun.star.uno-typelibrary;type=RDB" 
	manifest:full-path="XTutoAddIn.rdb"/> 
<manifest:file-entry manifest:media-type="application/vnd.sun.star.configuration-data" 
	manifest:full-path="TutoAddIn.xcu"/> 
<manifest:file-entry manifest:media-type="application/vnd.sun.star.uno-component;type=Python" 
	manifest:full-path="TutoAddIn.py"/> 
</manifest:manifest>
Commentaires
  • La déclaration du fichier description.xml n'est pas requise. La liste des fichiers à déclarer et la manière de le faire sont détaillées dans le guide du développeur.

E. Création de l'extension proprement dite
  1. Sélectionner l'ensemble de fichiers créés plus haut (en respectant l'arborescence) et les compresser au format zip ;
  2. renommer le fichier obtenu en TutoAddIn.oxt ;
  3. installer.
Voilà ! L'extension est désormais fonctionnelle.

:idea: Pour s'assurer que l'intégration à Calc est complète, il est important de relancer le programme immédiatement après l'installation.


F. Utilisation avec basic
Il est également possible d'utiliser une fonction ajoutée par add-in dans une macro basic :

‒ soit en instanciant l'extension comme un service

Code : Tout sélectionner

sub main
    addin = createUnoService("org.forumfr.tutoriels.TutoAddin")
    noms = addin.pysplit("Saint-Remy-en-Bouzemont-Saint-Genest-et-Isson","-")
    for each row in noms
        for each col in row
            print col
        next col
    next row
end sub
‒ soit en utilisant, de manière classique, la méthode callFunction de l'interface XFunctionAccess

Code : Tout sélectionner

sub main
    fs = createUnoService("com.sun.star.sheet.FunctionAccess")
    func = "org.forumfr.tutoriels.TutoAddin.pysplit"
    noms = fs.callFunction(func, array("Saint-Remy-en-Bouzemont-Saint-Genest-et-Isson","-"))
    for each row in noms
        for each col in row
            print col
        next col
    next row
end sub

G. Conclusion
Si vous avez lu ce tutoriel jusqu'ici, les éléments indispensables à la construction d'un add-in n'ont donc plus de secret pour vous :) .

Il est évident toutefois qu'expliquer avec toute la clarté nécessaire un processus comme celui-ci n'est pas aisé, et dépend beaucoup des a priori que chacun peut avoir sur les éléments d'information allant de soi. Vos commentaires et questions sont donc les bienvenus.
 Ajout : 14.10.2016
Petite modification dans le fichier TutoAddIn.py 
Pièces jointes
TutoAddIn.oxt
Fichier test - v2
(3.63 Kio) Téléchargé 433 fois
TutoAddIn.oxt
Extension contenant les fichiers décrits dans le tutoriel
(3.59 Kio) Téléchargé 402 fois
AOOo 4.1.7 sur Win10
AOOo 4.1.x sur Linux Mint
LibreOffice 5.x/6.x sur Linux Mint
--
| « Nos défauts devraient nous donner une qualité : l'indulgence pour les défauts des autres » (Rivarol)
Répondre