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)
MyCompany.WordAddIn Sources (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 »
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.
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.
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)
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.
Création du contrôle WPF « ActionPaneControl »
Nous ajoutons dès à présent un bouton dans le contrôle :
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.
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.
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.
- 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); } |
- 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 :
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 :
Une fois cliqué, le bouton « Action Pane » est disponible :
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.
Par rapport à notre panneau, cela provoque également un bug, en effet seul le panneau de la fenêtre active va se fermer:
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 :
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
Comments