Creating a Custom Task Pane in PowerPoint 2007

To start off a series of blog posts, I'm going to talk about creating a Custom Task Pane using PowerPoint 2007 and Visual Studio Tools for Office. Before I get into any code, I wanted to point you to the resources you can use to do this and some screencasts which you should follow along with for more detailed information of how to do some of the nitty gritty details that I don't want to cover because they are well presented. Before we begin you should already have Visual Studio 2005 and Visual Studio Tools for Office, the current beta can be downloaded for free - see this blog post (dated 9/13/2006) for details.

There are 2 screencasts demonstrating how to add a Custom Task Pane to Office applications, specifically Word and Excel. You can find them here:

1. Extending the Office 2007 UI with a Custom Task Pane
2. Creating Custom Task Panes in Visual Studio Tools for Office v3 June CTP

Later in this series I will also refer to the screencasts which show how you customize the Ribbon and demonstrate how you can make a custom XML Part and load/save it from your add-in when loading/saving PowerPoint files. This series will be in C#. You can do everything I describe here in other .NET languages as well as from any language using COM, but I personally find C# to be the easiest because it is supported by wonderful tools like Visual Studio Tools for Office. I have no reason to believe that you can't do everything below in Visual Studio Express editions and by locating the appropriate places to plug in your add-in using the Registry. Having said this, I tried it at home and decided it wasn't worth the effort. I wanted to stop for a moment to plug Collin Coller's Copy Source as HTML add-in for Visual Studio 2005. If you are a blogger looking to show example code listings, this is a great tool.

So follow the Tim Patterson's screencast (1 in the list above) and pick PowerPoint instead of Word. I named my shared add-in project "StoredSelectionPane", and I inserted a custom control into the project (rather than creating a new project) called "TaskpaneControl".

Listing 1: Connect.cs: C# code to show the "Stored Selections" Custom Task Pane in this example.

    1 namespace StoredSelectionPane

    2 {

    3    using System;

    4    using Extensibility;

    5    using System.Runtime.InteropServices;

    6    using MSO = Microsoft.Office.Core;

    7    using PPT = Microsoft.Office.Interop.PowerPoint;

    8 

    9    [GuidAttribute("549D3E43-47D0-468D-91E0-EE3690BF53A2"), ProgId("StoredSelectionPane.Connect")]

   10    public class Connect : Object, Extensibility.IDTExtensibility2, MSO.ICustomTaskPaneConsumer

   11    {

   12       private PPT.Application pptApplication;

   13       private object addInInstance;

   14 

   15       public void OnConnection(object application, Extensibility.ext_ConnectMode connectMode, object addInInst, ref System.Array custom)

   16       {

   17          pptApplication = (PPT.Application) application;

   18          addInInstance = addInInst;

   19       }

   20 

   21       public void CTPFactoryAvailable(MSO.ICTPFactory CTPFactoryInst)

   22       {

   23          MSO.CustomTaskPane ctp =

   24             CTPFactoryInst.CreateCTP("StoredSelectionPane.TaskpaneControl", "Stored Selections", Type.Missing);

   25          ctp.Visible = true;

   26       }

   27 

   28       // Unimplemented interface members

   29       public void OnDisconnection(Extensibility.ext_DisconnectMode disconnectMode, ref System.Array custom) {}

   30       public void OnAddInsUpdate(ref System.Array custom) {}

   31       public void OnStartupComplete(ref System.Array custom) {}

   32       public void OnBeginShutdown(ref System.Array custom) {}

   33    }

   34 }

I just wanted to remind people that GUIDs are unique - don't copy/paste the code on line 9 of listing 1. Instead, follow the Wizards as demonstrated in the screencasts above. The important things to note above are on lines 12 and 17, where I changed the type from object to PPT.Application (line 7 aliases PPT to Microsoft.Office.Interop.PowerPoint both to keep the namespace explicit and for convenience). Note that in order to get access to Microsoft.Office.Interop.PowerPoint, you will need to add a reference to the PowerPoint 12 COM DLL to your project. Note that line 24 tells the Custom Task Pane factory to create a Custom Task Pane with StoredSelectionPane.TaskPaneControl as the ProgId. You need to create the control and name it appropriately if you want this method call to work... The ProgId for your control goes into a special place in the Registry when you install the project. Visual Studio and Visual Studio Tools for Office handle connecting all of this for you in your install project. When you create a new user control in C#, Visual Studio generates you a class to hold all your interactions. In this case, I named my custom control TaskpaneControl and added it to the StoredSelectionPane project. The generated code for my taskpane control starts like this (I added my aliases to this file as well. We will use them throughout):

Listing 2: TaskpaneControl.cs (code editor view): The user control generated code.

    1 using System;

    2 using System.Collections.Generic;

    3 using System.ComponentModel;

    4 using System.Drawing;

    5 using System.Data;

    6 using System.Text;

    7 using System.Windows.Forms;

    8 using MSO = Microsoft.Office.Core;

    9 using PPT = Microsoft.Office.Interop.PowerPoint;

   10 

   11 namespace StoredSelectionPane

   12 {

   13    public partial class TaskpaneControl : UserControl

   14    {

   15       public TaskpaneControl()

   16       {

   17          InitializeComponent();

   18       }

We currently have a plain vanilla control. The idea I had for this control was to allow the user to save and recall shape selections. When working with large and complicated slides with hundreds of shapes, I find it useful to be able to quickly select a set of shapes which have some meaningful relationship. So with that concept in mind, we're going to add a ListView control, and some buttons. The buttons will be inside of a FlowLayoutPanel that is docked to the top of the pane and set to auto size to ensure that it is tall enough to show all controls when the user resizes the pane. The ListView control will be docked to fill the remaining space below the buttons. For buttons, I'm going to add "Add", "Remove", "Select", "Show", and "Hide". For each button, I have set AutoSize to true, AutoSizeMode to GrowAndShrink, and I have renamed the buttons from the generated names to match their text. For the ListView, I set the View type to be List (the default is Large Icon) I have turned off MultiSelect so that you can only work with one selection at a time. I have also turned off HideSelection so that when the control is out of focus you can still see the selected item. Double clicking each button creates callback methods and attaches them to their appropriate controls in the generated code. At this point, it is important to think about what we are going to do. Each entry in our ListView will be associated with a "Stored Selection". For convenience, I have written a small class to facilitate this. We will expand its functionality later, but first we need to talk about the constructor and members:

Listing 3: The StoredSelection helper class constructor.

    1    public class StoredSelection : ListViewItem

    2    {

    3       private int m_slideId = -1;

    4       private List<int> m_ids = new List<int>();

    5       public StoredSelection(PPT.Selection selection) : base("Empty Selection")

    6       {

    7          if(selection.Type != PPT.PpSelectionType.ppSelectionShapes)

    8             return;

    9          m_slideId = selection.SlideRange.SlideID;

   10          PPT.ShapeRange range = selection.HasChildShapeRange ? selection.ChildShapeRange

   11                                                              : selection.ShapeRange;

   12 

   13          string selectionName = "Shape Selection (";

   14          bool fFirst = true;

   15          foreach (PPT.Shape shape in range)

   16          {

   17             m_ids.Add(shape.Id);

   18             selectionName += fFirst ? shape.Name : ", " + shape.Name;

   19             fFirst = false;

   20          }

   21          selectionName += ")";

   22          base.Text = selectionName;

   23       }

   24    }

The StoredSelection class is called with a PowerPoint selection. Rather than associating each stored selection with an item in our list view, we will subclass the ListViewItem to store these selections inside the ListView. We set the default string to "Empty Selection". If the user of this class constructs us with a non-shape selection, this is an error case, but since we will need a valid m_slideId in order to do anything, the defaultly initialized m_slideId = -1 will ensure that in this case we won't be usable. Line 10 demonstrates that you have 2 kinds of shape selections, and we need to handle them both separately throughout to support working with children of group shapes. In order to consume our StoredSelection class so far, let's implement the AddButton callback.

Listing 4: Getting the PPT.Application and implementing the AddButton callback

1 private PPT.Application m_app = null;

2 public void SetApp( PPT.Application app )

3 {

4 m_app = app;

5 }

6

7 private void AddButton_Click(object sender, EventArgs e)

8 {

9 PPT.Selection selection = m_app.ActiveWindow.Selection;

10 if (selection.Type != PPT.PpSelectionType.ppSelectionShapes)

11 return;

12 SelectionListView.Items.Add(new StoredSelection(selection));

13 }

Note that for convenience, we will add a pointer to an instance of the PPT.Application interface to our Taskpane. We need to be very careful here. Do not be tempted to set m_app to "new PPT.ApplicationClass()". That will create a new instance of PowerPoint, which will keep alive your add-in after closing the instance of PowerPoint which you connected to. Go back to Connect.cs and add the following between lines 24 and 25 in listing 1 above:

         ((TaskpaneControl)ctp.ContentControl).SetApp(pptApplication);

This will let us use the application class without having to worry about new instance lifetimes keeping alive PowerPoint DLL instances on your machine after closing. Now lets implement the other buttons, starting with Remove:

Listing 5: Get the currently selected item and do something with it (part 1)

    1       private StoredSelection GetSelection()

    2       {

    3          if (SelectionListView.SelectedItems == null ||

    4             SelectionListView.SelectedItems.Count != 1)

    5             return null;

    6          return (StoredSelection)SelectionListView.SelectedItems[0];

    7       }

    8 

    9       private void RemoveButton_Click(object sender, EventArgs e)

   10       {

   11          StoredSelection selection = GetSelection();

   12          if( selection != null )

   13             SelectionListView.Items.Remove(selection);

   14       }

And now you can add and remove stored selections from the list. The rest of the buttons we will delegate the action down to some new methods on the StoredSelection class.

Listing 6: Get the currently selected item and do something with it (part 2)

    1       private void SelectButton_Click(object sender, EventArgs e)

    2       {

    3          StoredSelection selection = GetSelection();

    4          if (selection != null)

    5             selection.Select(m_app.ActiveWindow);

    6       }

    7 

    8       private void ShowButton_Click(object sender, EventArgs e)

    9       {

   10          StoredSelection selection = GetSelection();

   11          if (selection != null)

   12             selection.SetVisible(m_app.ActiveWindow, true);

   13       }

   14 

   15       private void HideButton_Click(object sender, EventArgs e)

   16       {

   17          StoredSelection selection = GetSelection();

   18          if (selection != null)

   19             selection.SetVisible(m_app.ActiveWindow, false);

   20       }

So now we just need to implement Select and SetVisible. Note that we pass along the active window to these methods. We will use the active window to search through the shapes on the slide, and do something for the shapes whose ids match those in the list. Remember that we stored the Shape.Id in the List<int> in the StoredSelection class (see listing 3 lines 4 and 17).

Listing 7: Implement StoredSelection.Select

    1       public void Select(PPT.DocumentWindow activeWindow)

    2       {

    3          if (m_slideId < 0 || activeWindow.Selection.SlideRange.SlideID != m_slideId)

    4             return;

    5 

    6          // Iterate over all shapes in the current slide

    7          PPT.Shapes shapes = activeWindow.Selection.SlideRange.Shapes;

    8          activeWindow.Selection.Unselect();

    9          foreach (int id in m_ids)

   10             foreach (PPT.Shape shape in shapes)

   11                Select(shape, id);

   12       }

   13 

   14       private void Select(PPT.Shape shape, int id)

   15       {

   16          if (shape.Id == id)

   17          {

   18             try

   19             {

   20                shape.Select(MSO.MsoTriState.msoFalse /*Replace*/);

   21             }

   22             catch (Exception e)

   23             {

   24             }

   25          }

   26          else if (shape.Type == MSO.MsoShapeType.msoGroup)

   27          {

   28             // Recurse over sub-shapes. id could be the Shape.Id of a child.

   29             foreach (PPT.Shape childShape in shape.GroupItems)

   30                Select(childShape, id);

   31          }

   32       }

There are a few important issues to consider when selecting. This implementation replaces the shape selection with a new selection built up. Note that PPT.Shape.Select will throw an exception if you attempt to select a shape which is not selectable. This is undersireable. If for example, you have a selection which includes shapes that are hidden, it would be good to select just the shapes you can select. On line 8, we clear the selection. On line 20, we add the shape whose ID matches. lines 9-11 and 29-30 ensure that we search recursively through the whole tree of shapes including iterating through groups. Finally, we implement SetVisible.

Listing 8: Implement SetVisible

    1       public void SetVisible(PPT.DocumentWindow activeWindow, bool fVisible)

    2       {

    3          if (m_slideId < 0 || activeWindow.Selection.SlideRange.SlideID != m_slideId)

    4             return;

    5 

    6          // Iterate over all shapes in the current slide

    7          PPT.Shapes shapes = activeWindow.Selection.SlideRange.Shapes;

    8          foreach (int id in m_ids)

    9             foreach (PPT.Shape shape in shapes)

   10                SetVisible(shape, id, fVisible);

   11       }

   12 

   13       private void SetVisible(PPT.Shape shape, int id, bool fVisible)

   14       {

   15          if (id == shape.Id)

   16          {

   17             try

   18             {

   19                shape.Visible = fVisible ? MSO.MsoTriState.msoTrue : MSO.MsoTriState.msoFalse;

   20             }

   21             catch (Exception e)

   22             {

   23             }

   24          }

   25          else if (shape.Type == MSO.MsoShapeType.msoGroup)

   26          {

   27             // Recurse over sub-shapes. id could be the Shape.Id of a child.

   28             foreach (PPT.Shape childShape in shape.GroupItems)

   29                SetVisible(childShape, id, fVisible);

   30          }

   31       }

This is almost identical to the previous code listing. We have one line less because we didn't need to reset the selection. The signature of these methods is slightly different so we can pass down the value to set the visibility to for all shapes in the stored selection. Note that if you have nested shapes, and you set their visibility to true, that operation does not affect the visibility of their parent group shape, but if you set the visibility of a group shape, it does set the visibility of its children (and their children).

Next time, we will address some flaws in this approach and add support to save and load custom XML for this add-in.

This posting is provided AS-IS with no warranties expressed or implied.