Customizing Ribbon in Office 2007 using Open Office XML and VBA

Code for this article can be downloaded from here.

Ribbon:-

clip_image002

clip_image004

From http://office.microsoft.com/en-gb/help/use-the-ribbon-HA010089895.aspx :-“The Ribbon is designed to help you quickly find the commands that you need to complete a task. Commands are organized in logical groups, which are collected together under tabs. Each tab relates to a type of activity, such as writing or laying out a page. To reduce clutter, some tabs are shown only when needed. For example, the Picture Tools tab is shown only when a picture is selected.”

What to expect from this article:-

At the end of this article you should be able to customize the ribbon.

clip_image006

We will add a tab named “My Zone” containing two groups, “My links” and “My Contacts” containing your web site links and email addresses of your contacts.

clip_image008

When you click on any of the “links” in the “My Links” group, that link will get added to the document at the current position of the cursor.

clip_image010
clip_image012

Ribbon XML:-

From http://msdn.microsoft.com/en-us/library/aa942866.aspx:-

The Ribbon (XML) item enables you to customize a Ribbon by using XML. Use the Ribbon (XML) item if you want to customize the Ribbon in a way that is not supported by the Ribbon (Visual Designer) item

Example RibbonXML:-

<customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui" onLoad="RibbonLoad">
<ribbon>
<tabs>
<tab id="tabMyZone" label="My Zone">
<group id="grpLinks" label="My links">
<button id="Google" label="Google" tag="http://www.google.com"/>
<button id="Facebook" label="Facebook" tag="http://www.facebook.com"/>
<button id="Gmail" label="Gmail" tag="http://www.gmail.com"/>
</group>
<group id="grpEmailAddresses" label="My Contacts">
<button id="e1" label="Ashish Gupta" tag="ashish.kuber@wipro.com"/>
<button id="e2" label="Sachin Tendulkar" tag="sachinrt@yahoo.com"/>
<button id="e3" label="James Hetfield" tag="James.Hetfield@metallica.com"/>
</group>
</tab>
</tabs>
</ribbon>
</customUI>

How to add RibbonXML(CustomUI XML) to the word using OpenXML SDK

Pre-requisite :- Open XML SDK 2.0 which can be downloaded from here. (Download the OpenXMLSDKv2.msi which is about 4 MB in size)

Additional tools :-

a) The schema for ribbon XML which can be downloaded from here.

b) Custom UI Editor which allows you to add a custom UI part to a document. This can be downloaded from here.

Lets Start….

Create a console application and add a empty word document to it. The document should be macro-enabled (with .docm) extension. Add the Xml for the Ribbon as well. My solution explorer looks like this :-

clip_image002[8]

For the purpose of the example in this document we will have a Ribbon XML created already and we would put the same into an existing word document on run-time.

static string documentName = "document.docm";
static string ribbonXMLFileName = "Ribbon.xml";
static void Main(string[] args)
{
   byte[] updatedByteContent = AddRibbonToDocument(File.ReadAllBytes(documentName));
   if (updatedByteContent != null)
   {
         using (FileStream fileStream = new FileStream(documentName, FileMode.Create))
         {
             fileStream.Write(updatedByteContent, 0, updatedByteContent.Length);
         }

   }
   if (File.Exists(documentName)) 
   {
    Process.Start(documentName);
   }

}

Line 6:- Reads the contents of the document.docm file and pass the binary content to the AddRibbonToDocument() method which will add the ribbon to the document.

Line 7-14:- The binary content of the file (which also contains the ribbon now) is written to the same file as original and opened.

Add the below method in Program.cs. This method takes the binary content of a word file and adds the Ribbon XML to it.

public static byte[] AddRibbonToDocument(byte[] documentContent)
{
   byte[] updatedDocumentContent = null;
   if (documentContent != null)
   {
       using (MemoryStream memoryStream = new MemoryStream())
       {
         memoryStream.Write(documentContent, 0, documentContent.Length);
         string ribbonXMLAsString = GetRibbonXML().ToString();
         using (WordprocessingDocument myDoc = WordprocessingDocument.Open(memoryStream, true))
         {
           MainDocumentPart mainPart = myDoc.MainDocumentPart;
           if (myDoc.GetPartsCountOfType<RibbonExtensibilityPart>() > 0)
           {
                myDoc.DeletePart(myDoc.GetPartsOfType<RibbonExtensibilityPart>().First());
           }  

           RibbonExtensibilityPart ribbonExtensibilityPart = myDoc.AddNewPart<RibbonExtensibilityPart>();
           ribbonExtensibilityPart.CustomUI = new DocumentFormat.OpenXml.Office.CustomUI.CustomUI(ribbonXMLAsString);
           myDoc.CreateRelationshipToPart(ribbonExtensibilityPart);
         }

         updatedDocumentContent = memoryStream.ToArray();
       }

    }

    return updatedDocumentContent;
}

Line 4-8 :- The binary content of the document is put in a MemoryStream which will be used for any modification in the content.

Line 9 :– The ribbon XML is got from the GetRibbonXML() method which can either get the XML from a static file or dynamically construct from the database values.

Line 10 :- WordProcessingDocument object is initialized from the memorysteam of the document content. The second parameter of the constructor is “IsEditable” is set to true as we are going to modify its content.

Line 14- 15 :- Get the main document part from the wordprocessing document and delete any existing custom ribbon from it.

Line 17 – 19 :- Content of the ribbon XML needs to be added as a RibbonExtensibilityPart to the main document and a relationship will be created for the same. Infact any type of content you add to a document as a part, you must create its (part’s) relationship with the document so that document can load up that part when you open the document in MSWord (or in general MS Office).

So, at this point of time If you run the application, the document.docm should get opened and you should see the tab and the buttons.

clip_image008

More explanation on line 17-19

Document structure before you ran the above code :-

clip_image002[10]

Look at the _rels/.rels file. You don’t see anything related to Custom UI here.

clip_image004[6]

Document structure after the above code is run:-

You will see a customUI folder created here:-

clip_image006[5]

Open the CustomUI folder and the file inside it will contain the same ribbonXml you inserted using the code above:-

clip_image008[5]

Look at the _rels/.rels file. You will see a new entry stating the relationship with the CustomUI.xml file here.

clip_image010[6]

btw, If you click on any of the buttons now, nothing would happen as we haven’t added any interactivity features to those buttons.

Adding interactivity to the ribbon elements:-

1) Make a copy of the Document.docm and rename the copied document to “DocumentWithMacros.docm”. Open the developer tab and insert a “Module” to the project.

clip_image002[13]

 

2) Rename the module to OfficeMacroHelper:-

clip_image004[9]

3) Add the following macro code to the module. Then save and close the word file:-

Sub CreateTextHyperLink(control As IRibbonControl)
 CreateHyperlink control.Tag, control.id
End Sub

Sub InsertTextHyperlink(hyperlink As String, textTodisplay As String)
 CreateHyperlink hyperlink, textTodisplay
End Sub

Sub CreateEmailHyperLink(control As IRibbonControl)
 InsertEmailHyperliink control.Tag, control.Tag
End Sub

Sub InsertEmailHyperliink(hyperlink As String, textTodisplay As String)
 Dim emailLink As String
 emailLink = "mailto:" & hyperlink
 CreateHyperlink emailLink, textTodisplay
End Sub

Sub CreateHyperlink(address As String, textTodisplay As String)
 ActiveDocument.Hyperlinks.Add Anchor:=Selection.Range, address:="" & address & "", SubAddress:="", textTodisplay:="" & textTodisplay & ""
End Sub

4) At this point of time if you view the TheDocumentWithMacros.docm, you see vbaProject.bin. This file has got the binary form of all the macros the document contains. This is what we need to copy in our main document (document.docm).

clip_image002[15]

5) Now we have the macros in “TheDocumentWithMacro.docm”. Its time to call them from our main document “Document.docm”.We call those macro functions from the onAction event of the button.

<customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui">
    <ribbon>
        <tabs>
             <tab id="tabMyZone" label="My Zone">
             <group id="grpLinks" label="My links">
                <button id="Google"   label="Google" tag="http://www.google.com"  onAction="CreateTextHyperLink"/>
                <button id="Facebook" label="Facebook" tag="http://www.facebook.com" onAction="CreateTextHyperLink"/>
                <button id="Gmail" label="Gmail" tag="http://www.gmail.com"  onAction="CreateTextHyperLink"/>
             </group>

             <group id="grpEmailAddresses" label="My Contacts">
                <button id="e1" label="Ashish Gupta" tag="ashish.kuber@wipro.com"  onAction="CreateEmailHyperLink"/>
                <button id="e2" label="Sachin Tendulkar" tag="sachinrt@yahoo.com"        onAction="CreateEmailHyperLink"/>
                <button id="e3" label="James Hetfield" tag="James.Hetfield@metallica.com" onAction="CreateEmailHyperLink"/>
             </group>
        </tab>
        </tabs>
    </ribbon>
</customUI>

6) Copy the macro code from the “TheDocumentWithMacro.docm” to the “Document.docm”:-

public static void CopyMacro(byte[] documentWithMacroContent, WordprocessingDocument document)
{
   const string vbaProjectRelationShipType = "http://schemas.microsoft.com/office/2006/relationships/vbaProject";
   VbaProjectPart vbaProjectPart = null;
   using (MemoryStream memoryStream = new MemoryStream(documentWithMacroContent, false))
   {
       using (WordprocessingDocument documentWithMacro = WordprocessingDocument.Open(memoryStream, false))
       {
           MainDocumentPart mainPart = documentWithMacro.MainDocumentPart;
           foreach (IdPartPair partPair in mainPart.Parts)
           {
             if (partPair.OpenXmlPart.RelationshipType == vbaProjectRelationShipType)
             {
                vbaProjectPart = (VbaProjectPart)partPair.OpenXmlPart;
                break;
             }
           }
 
           MainDocumentPart mainPart1 = document.MainDocumentPart;
           ExtendedPart extendedPart = null;
           foreach (IdPartPair partPair in mainPart1.Parts)
           {
             if (partPair.OpenXmlPart.RelationshipType == vbaProjectRelationShipType)
             {
                 extendedPart = (ExtendedPart)partPair.OpenXmlPart;
                 break;
             }
           }

           if (extendedPart != null)
           mainPart1.DeletePart(extendedPart);

           if (vbaProjectPart != null)
           mainPart1.AddPart<VbaProjectPart>(vbaProjectPart);
        }

    }

}

Line 11- 18 :- Get the VbaProjectPart from the document with macro.

Line 20-29  :- Get the VbaProjectPart from the document.

Line 31-32  :- Delete the existing VbaProjectPart from the document If any.

Line 34-35  :- Add the VbaProjectPart got from the Line (125-133) to the document.

7) Get the binary content of the “TheDocumentWithMacro.docm” (line 7).

static string fileName = "document.docm";
static string ribbonXMLFileName = "Ribbon.xml";
static string documentWithMacroFileName = "TheDocumentWithMacro.docm";
static byte[] documentWithMacroContent;
static void Main(string[] args)
{
   documentWithMacroContent = File.ReadAllBytes(documentWithMacroFileName);
   byte[] updatedByteContent = AddRibbonToDocument(File.ReadAllBytes(fileName));
   if (updatedByteContent != null)
   {
      using (FileStream fileStream = new FileStream(fileName, FileMode.Create))
      {
         fileStream.Write(updatedByteContent, 0, updatedByteContent.Length);
      }
   }
   
   if (File.Exists(fileName)) Process.Start(fileName);
}

8) Modify the AddRibbonToDocument() to add call to CopyMacro() method (line 20).

public static byte[] AddRibbonToDocument(byte[] documentContent)
{
  byte[] updatedDocumentContent = null;
  if (documentContent != null)
  {
     using (MemoryStream memoryStream = new MemoryStream())
     {
        memoryStream.Write(documentContent, 0, documentContent.Length);
        string ribbonXMLAsString = GetRibbonXML().ToString();
        using (WordprocessingDocument myDoc = WordprocessingDocument.Open(memoryStream, true))
        {
           MainDocumentPart mainPart = myDoc.MainDocumentPart;
           if (myDoc.GetPartsCountOfType<RibbonExtensibilityPart>() > 0)
                myDoc.DeletePart(myDoc.GetPartsOfType<RibbonExtensibilityPart>().First());

           RibbonExtensibilityPart ribbonExtensibilityPart = myDoc.AddNewPart<RibbonExtensibilityPart>();
           ribbonExtensibilityPart.CustomUI = new DocumentFormat.OpenXml.Office.CustomUI.CustomUI(ribbonXMLAsString);
           myDoc.CreateRelationshipToPart(ribbonExtensibilityPart);
           CopyMacro(documentWithMacroContent, myDoc);
        }
       updatedDocumentContent = memoryStream.ToArray();
     }
  }
  return updatedDocumentContent;
}

At this point of time, If you run the application you should see the buttons on the newly added tab. Clicking on the buttons embed links on the document.

NOTE :- If you are like me, you must be thinking of creating the VBProject dynamically to the document.docm file rather than maintaining another file (ThedocumentWithMacro.docm) and copying from the same. Although theoretically Its possible, Its way too complex to implement or I just dont have the time to implement that way. See this msdn.microsoft.com/en-us/library/cc313094(v=office.12).aspx.

Advertisements

, ,

  1. Adding custom pane to the ribbon in Microsoft Word 2007 using OpenXML SDK 2.0 and VBA « Working hard leads to working smart.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Random Thoughts

The World as I see it

Simple Programmer

Making The Complex Simple

Ionic Solutions

Random thoughts on software construction, design patterns and optimization.

Long (Way) Off

A tragic's view from the cricket hinterlands

%d bloggers like this: