Table des matières

1. Préambule
2. Création de l’AddIn « MyCompany.WordAddIn »
3. Création de menus dans le Ribbon
4. Affichage d’un panneau WPF adjacent à la vue Document
5. Affichage de la sélection courante au clic sur le bouton du panneau
6. Gestion d’un contexte spécifique à chaque document ouvert (multi-instances)

Téléchargement des sources:
MyCompany.WordAddIn Sources Download    (Nécessite le framework .net 4.0)

1. Préambule

De manière générale un AddIn est un module qui une fois chargé ajoute des fonctionnalités à un logiciel. Dans le cas présent nous nous intéresserons au développement d’un AddIn pour Word 2010. Les fonctionnalités proposées par l’API de Microsoft sont nombreuses et permettent notamment :

  • De proposer des menus supplémentaires (nouvel onglet dans le Ribbon*, modification des menus contextuels,…)
  • De manipuler le document (ajout/suppression d’éléments tels que du texte, des tableaux, des images, du header, des propriétés, …)
  • De contrôler Word (enregistrement du fichier sous un format particulier, …)

Note : Il ne faut pas confondre un Template Word d’un AddIn Word. Un « template » défini un type de document spécifique et peut contenir des macros, un contenu par défault, des styles spécifiques,…

A travers ce mini projet nous allons aborder les problématiques suivantes :
1. Création d’un menu dans le Ribbon
2. Ajout d’un panneau adjacent au document
3. Intégration de WPF
4. Manipulation du document courant (affichage de la sélection courante)
5. Gestion Multi-instances, définition d’un contexte
6. Menus contextuels sur le document

Ribbon: Introduit depuis Office 2007, il s’agit du ruban disposé en haut des fenêtres. Il regroupe les menus par fonctionnalités (par onglet puis par groupes).

2. Création de l’AddIn « MyCompany.WordAddIn »

Fenêtre de création du projet

Le wizard crée pour nous la base d’un AddIn Word à travers la classe ThisAddIn. Par défaut, deux méthodes sont proposées : ThisAddIn_Startup et ThisAddIn_Shutdown. Elles sont déclenchées à l’ouverture de la première fenêtre de Word et à la fermeture de la dernière fenêtre de Word.

3. Création de menus dans le Ribbon

Il est possible d’ajouter des onglets au Ribbon par défaut en ajoutant un nouvel objet de type Ribbon (Visual Designer) ou Ribbon (XML). Nous utilisons le premier d’entre eux qui fournit un éditeur graphique.

Ajout de l’élément Ribbon (Visual Designer)

Dans l’éditeur on définit les propriétés de l’onglet :

  • ControlIdType : Custom
  • Label : « MyCompany »

On ajoute 3 boutons :

  • ActionPane (pour ouvrir un panel à côté du document)
  • Start (commencer à utiliser les fonctionnalités)
  • Stop (arrêter d’utiliser les fonctionnalités)

Résultat de l’ajout du ribbon

4. Affichage d’un panneau WPF adjacent à la vue Document

Pour le moment, nous n’utiliserons pas les boutons « Start » et « Stop ». Notre but est d’afficher un contrôle en WPF à côté du document.

Il n’est pas possible d’insérer directement des vues WPF. Pour cela nous créons un UserControl WinForms nommé « ActionPaneContainer » qui servira d’hôte à notre composant WPF.

Ajout d’un “User Control” WinForms

Création du contrôle WPF « ActionPaneControl »

Ajout d’un “User Control” WPF

Nous ajoutons dès à présent un bouton dans le contrôle :

Création du contenu du UserControl WPF

Retour dans ActionPaneContainer : on ajout un objet de type « ElementHost » disponible dans la catégorie « WPF Interoperability ».

Important : Une fois l’ElementHost ajouté on définit sa propriété « Dock » à « Fill » afin qu’il utilise tout l’espace disponible dans la fenêtre.

Ajout du composant “ElementHost” pour héberger WPF dans WinForms

Il faut maintenant définir que le UserControl WinForm va héberger le UserControl WPF. Pour cela on passe en vue « Code » et modifions le constructeur tel que :

public ActionPaneContainer()
{
    InitializeComponent();
    this.elementHost1.Child = new ActionPaneControl();
}

Au clic sur le bouton « Action Pane » du ribbon, on ajoute le panneau à côté du document s’il n’existe pas déjà. Pour cela il faut enregistrer l’enregistrer à la collection « Globals.ThisAddIn.CustomTaskPanes ».

Ribbon1.cs

private void btnAction_Click(object sender, RibbonControlEventArgs e)
{
    Microsoft.Office.Interop.Word.Application MyApplication = Globals.ThisAddIn.Application;
 
    #region On n'ajoute pas de panneau si aucun document n'est ouvert
    if (MyApplication.Documents.Count == 0)
    {
        return;
    }
    #endregion
 
    #region Récupération de l'état courant
    //Récupération de la fenêtre active
    Microsoft.Office.Interop.Word.Window currentWindow = MyApplication.ActiveWindow;
 
    //Récupération du panneau s'il existe pour la fenêtre active
    CustomTaskPane custompane = Globals.ThisAddIn.CustomTaskPanes.Where(pane => pane.Window == currentWindow).FirstOrDefault();
    #endregion
 
    #region Création du panneau s'il n'existe pas
    if (custompane == null)
    {
        //Création du container
        ActionPaneContainer container = new ActionPaneContainer();
 
        //Ajout du container à la liste des panneaux personnalisés (CustomTaskPanes)
        custompane = Globals.ThisAddIn.CustomTaskPanes.Add(container, "Action Pane", currentWindow);
 
        //On positionne le panneau à gauche du document
        custompane.DockPosition = Microsoft.Office.Core.MsoCTPDockPosition.msoCTPDockPositionLeft;
    }
    #endregion
 
    //On rend le panneau visible
    custompane.Visible = true;
}

Attention : Ne pas oublier de passer la propriété « Visible » du CustomTaskPane à « true »

On lance l’addin, au clic sur « Action Pane », notre UserControl WPF apparait bien.

Affichage du panneau adjacent à la vue Document

Il est alors possible d’ouvrir plusieurs documents, chaque fenêtre aura son propre panneau au clic sur le bouton « ActionPane ».

Bien que plusieurs instances de Word soient en cours d’exécution, la collection « Globals.ThisAddIn.CustomTaskPanes » reste partagée et contiendra autant de panneaux que de documents ouverts.

Important : La fermeture d’un document, n’entraîne pas la suppression du panneau dans collection « Globals.ThisAddIn.CustomTaskPanes ». Il convient donc de supprimer le panneau à la fermeture du document si nécessaire.

  1. Au chargement du Ribbon nous allons nous abonner à l’évènement « DocumentBeforeClose »
private void Ribbon1_Load(object sender, RibbonUIEventArgs e)
{
    Globals.ThisAddIn.Application.DocumentBeforeClose +=new Microsoft.Office.Interop.Word.ApplicationEvents4_DocumentBeforeCloseEventHandler(Application_DocumentBeforeClose);
}
  1. Si le panneau existe pour le document en cours de fermeture on le supprime
void Application_DocumentBeforeClose(Microsoft.Office.Interop.Word.Document Doc, ref bool Cancel)
{
    //Récupération de la fenêtre active
    Microsoft.Office.Interop.Word.Window currentWindow = Doc.ActiveWindow;
 
    //Récupération du panneau s'il existe pour la fenêtre active
    CustomTaskPane custompane = Globals.ThisAddIn.CustomTaskPanes.Where(pane => pane.Window == currentWindow).FirstOrDefault();
 
    //Suppression du panneau
    if(custompane != null)
    {
        Globals.ThisAddIn.CustomTaskPanes.Remove(custompane);
    }
}

5. Affichage de la sélection courante au clic sur le bouton du panneau

Dans ActionPaneControl, nous nous abonnons à l’évènement Click du bouton.

private void btnShowSelection_Click(object sender, RoutedEventArgs e)
{
    //On vérifie que le document existe bien
    if(Globals.ThisAddIn.Application.Documents.Count == 0)
    {
        return;    
    }
 
    //Récupération du texte sélectionné dans Word
    string textSelection = Globals.ThisAddIn.Application.Selection.Text;
 
    //Affichage de la sélection
    MessageBox.Show(textSelection, "Selection");
}

Important :
Ne pas comparer la propriété «Globals.ThisAddIn.Application.ActiveDocument» avec « null ». En effet lorsqu’aucun document n’est ouvert la propriété Globals.ThisAddIn. Application.ActiveDocument n’existe pas. L’astuce consiste à compter le nombre de documents ouverts ; si celui-ci est supérieur à 0 on peut obtenir la sélection courante.

Résultat :

Affichage de la sélection courante

6. Gestion d’un contexte spécifique à chaque document ouvert (multi-instances)

Comme nous l’avons vu, un AddIn Word est avant tout prévu pour ajouter des fonctionnalités communes sur tous les documents ouverts mais dans certains cas il peut être utile voire indispensable de gérer chaque document ouvert différemment.

Afin d’illustrer cette problématique nous allons mettre en pratique les boutons « Start » et « Stop » en les rendant exclusif.

Dans le designer on désactive par défaut les boutons « Action Pane » et « Stop », le but étant de les rendre disponibles une fois le bouton « Start » cliqué.

On ajoute les méthodes au clic sur ces 2 boutons :

private void btnStart_Click(object sender, RibbonControlEventArgs e)
{
    btnStart.Enabled = false;
 
    //Activation des fonctionalités
    btnStop.Enabled = true;
    btnAction.Enabled = true;
}
 
private void btnStop_Click(object sender, RibbonControlEventArgs e)
{
    btnStop.Enabled = false;
 
    //Activation des fonctionalités
    btnStart.Enabled = true;
    btnAction.Enabled = true;
}

On exécute le projet :

Par défaut seul le bouton « Start » est disponible :

Etat au démarrage de Word

Une fois cliqué, le bouton « Action Pane » est disponible :

Changement des états des boutons du Ribbon

Le bouton « Stop » cache le panneau

Problématique :

Nous avons une seule instance de Ribbon partagée pour tous les documents ouverts. Par conséquence, lorsque que l’on clique sur « Start » sur un document, les boutons « Action Pane » et « Stop » s’activent sur toutes les fenêtres de Word.

Les états des boutons sont partagés par toutes les instances de Word

Par rapport à notre panneau, cela provoque également un bug, en effet seul le panneau de la fenêtre active va se fermer:

Les états des boutons sont partagés par toutes les instances de Word

Solution :

Pour pallier à ce problème nous allons introduire une notion de contexte lié à chaque fenêtre Word. Cet objet permettra de définir l’état de chaque menu et sera utilisé à chaque changement de fenêtre courante.

On crée une classe WordContext telle que :

public class WordContext
{
    public bool BtnStartState { get; set; }
    public bool BtnStopState { get; set; }
    public bool BtnActionState { get; set; }
    public bool IsStarted
    {
        get
        {
            return !BtnStartState;
        }
    }
 
    public WordContext()
    {
        BtnStartState = true;
        BtnStopState = false;
        BtnActionState = false;
    }
 
    ///
 /// Renvoie un contexte ou tout est désactivé ///

///Contexte ou tout est désactivépublic static WordContext GetDefault() { WordContext ctx = new WordContext() { BtnStartState = false, BtnStopState = false, BtnActionState = false }; return ctx; } }

Dans la classe Ribbon1, on crée un dictionnaire qui va associer une fenêtre à un contexte et ajouter une référence au context en cours d’utilisation.

Dictionary ContextCollection;
WordContext CurrentContext;

Dans la méthode Ribbon1_Load, on instancie le dictionnaire et on s’abonne à l’évènement DocumentChange ce qui nous donne:

private void Ribbon1_Load(object sender, RibbonUIEventArgs e)
{
    #region PART 1
    Globals.ThisAddIn.Application.DocumentBeforeClose += new Microsoft.Office.Interop.Word.ApplicationEvents4_DocumentBeforeCloseEventHandler(Application_DocumentBeforeClose);
    #endregion
 
    #region PART 3
    //Instanciation du dictionnaire
    ContextCollection = new Dictionary();
 
    //Abonnement à l'évènement DocumentChange
    Globals.ThisAddIn.Application.DocumentChange += new Microsoft.Office.Interop.Word.ApplicationEvents4_DocumentChangeEventHandler(Application_DocumentChange);
    #endregion
}

Nous mettons notre gestion de contexte au changement de document actif :

void Application_DocumentChange()
{
    Microsoft.Office.Interop.Word.Application MyApplication = Globals.ThisAddIn.Application;
 
    //Initialisation du contexte à charger avec des valeurs par défaut
    WordContext contextToLoad = WordContext.GetDefault();
 
    #region On n'ajoute pas de panneau si aucun document n'est ouvert
    if (MyApplication.Documents.Count > 0)
    {
 
        //Création du contexte s'il n'existe pas
        Microsoft.Office.Interop.Word.Window currentWindow = MyApplication.ActiveWindow;
        if(!ContextCollection.ContainsKey(currentWindow))
        {
            ContextCollection.Add(currentWindow, new WordContext());
        }
 
        //Récupération du contexte existant
        contextToLoad = ContextCollection[currentWindow];
    }
    #endregion
 
    //Chargement du contexte
    this.btnStart.Enabled = contextToLoad.BtnStartState;
    this.btnStop.Enabled = contextToLoad.BtnStopState;
    this.btnAction.Enabled = contextToLoad.BtnActionState;
 
    //CurrentContext
    CurrentContext = contextToLoad;
}
 
///
 /// Récupération de l'état actuel des fonctionalités pour mettre à jour le contexte ///

private void UpdateCurrentContext() { CurrentContext.BtnStartState = this.btnStart.Enabled; CurrentContext.BtnStopState = this.btnStop.Enabled; CurrentContext.BtnActionState = this.btnAction.Enabled; }

Enfin pour maintenir notre contexte à jour nous appelons UpdateCurrentContext après chaque modification. Dans notre exemple à la fin des méthodes « btnStart_Click » et « btnStop_Click »

Au passage d’une fenêtre à l’autre la mise à jour est totalement transparente pour l’utilisateur :

Gestion de contextes associés à chaque instance de Word

7. Ajout d’un menu contextuel sur un objet en particulier

A titre d’exemple notre objectif est d’afficher le nombre de colonnes et de lignes d’un tableau lorsque que l’on clic droit dessus. Petite difficulté supplémentaire, on souhaite proposer ce nouveau menu uniquement s’il ne contient pas de texte.

Nous utilisons l’évènement WindowBeforeRightClick qui précède l’affichage du menu contextuel au clic droit. Cela nous permet d’évaluer si on doit ou non afficher ce menu à l’écran.

Nous ajoutons les propriétés suivantes:

#region Propriétés liées au clic droit
Microsoft.Office.Core._CommandBarButtonEvents_ClickEventHandler eventHandler;
private Selection LastSelection;
#endregion

On instancie l’évènement dans Ribbon1_Load :

eventHandler = new Microsoft.Office.Core._CommandBarButtonEvents_ClickEventHandler(MyButton_Click);

Il faut ensuite traiter l’évènement WindowsBeforeClick :

void Application_WindowBeforeRightClick(Microsoft.Office.Interop.Word.Selection Sel, ref bool Cancel)
{
    #region Suppression du menu s'il existe déjà
    //La commandebar "Table Text" représente le menu utilisé lors d'un clic droit sur un tableau
    Microsoft.Office.Core.CommandBar contextMenu = Globals.ThisAddIn.Application.CommandBars["Table Text"];
 
    //Récupération du bouton
    Microsoft.Office.Core.CommandBarButton button = (Microsoft.Office.Core.CommandBarButton)contextMenu.FindControl(Microsoft.Office.Core.MsoControlType.msoControlButton, Type.Missing, "MyCompanyCountBtn", true, true);
 
    //Suppression de tous les boutons qui ont le tag "MyCompanyCountBtn"
    while ((button != null))
    {
        button.Delete(true);
        button = (Microsoft.Office.Core.CommandBarButton)contextMenu.FindControl(Microsoft.Office.Core.MsoControlType.msoControlButton, Type.Missing, "MyCompanyCountBtn", true, true);
    }
    #endregion
 
    #region Vérifie si la sélection courante contient au moins un tableau
    if (Sel.Tables.Count == 0)
        return;
    #endregion
 
    #region Vérifie si la sélection courante contient au moins un tableau
    if (Sel.Tables.Count == 0)
        return;
    #endregion
 
    #region Ajoute le menu si le tableau ne contient pas de texte
    LastSelection = Sel;
    Microsoft.Office.Interop.Word.Table selectedTable = Sel.Tables[1];
    string tableText = selectedTable.Range.Text;
 
    //Suppression des retours
    tableText = tableText.Replace("\r\a", "");
    if(string.IsNullOrWhiteSpace(tableText))
    {
        //Ajout du bouton au menu
        object missing = Type.Missing;
        Microsoft.Office.Core.MsoControlType menuItem = Microsoft.Office.Core.MsoControlType.msoControlButton;
        Microsoft.Office.Core.CommandBarButton myButton = (Microsoft.Office.Core.CommandBarButton)Globals.ThisAddIn.Application.CommandBars["Table Text"].Controls.Add(menuItem, missing, missing, 1, true);
        myButton.Style = Microsoft.Office.Core.MsoButtonStyle.msoButtonCaption;
        myButton.Caption = "Display nb of columns/rows";
        myButton.Tag = "MyCompanyCountBtn";
        myButton.BeginGroup = true;
        //On s'abonne une seule fois à l'évènement
        if (eventHandler == null)
        {
            eventHandler = new Microsoft.Office.Core._CommandBarButtonEvents_ClickEventHandler(MyButton_Click);
            myButton.Click += eventHandler;
        }
    }
    #endregion
}
 
On ajoute le traitement au clic sur le menu pour afficher le nombre de lignes et de colonnes du tableau courant
void MyButton_Click(Microsoft.Office.Core.CommandBarButton Ctrl, ref bool CancelDefault)
{
    Microsoft.Office.Interop.Word.Table selectedTable = LastSelection.Tables[1];
    int nbRows = selectedTable.Rows.Count;
    int nbColumns = selectedTable.Columns.Count;
 
    string message = string.Format("Il y a {0} lignes et {1} colonnes", nbRows, nbColumns);
    MessageBox.Show(message, "Info", MessageBoxButton.OK, MessageBoxImage.Information);
}

Résultat :

Au clic droit sur un tableau vide, le menu « Display nb if columns/rows » apparait

Ajout d’un item dans le menu contextuel sur un tableau vide

 

Affichage du nombre de lignes et de colonnes au clic sur le nouveau menu

 

Last modified: 2 July 2014

Author

Comments

Write a Reply or Comment

Your email address will not be published.