Creating a Dynamic Menu – An Example

Posted on January 9th, 2007 in Excel, Office 2007, The Ribbon by Ken Puls

The history of posts on the Ribbon can be found at the Ribbon Example Table of Contents.

In this blogpost, I thought it might be nice to try a real example of something that someone may actually want to to. This example adds a menu to my XLG group, and populates it with all Excel files in a directory specified in a worksheet cell. Clicking the menu item will launch the file, and an additional button control will rebuild the menu if the file path in the worksheet cell is changed. One thing to be aware of, since this is an example only, is that no checking of the directory is done.

This example differs from the others I've blogged about, as it makes a truly dynamic menu. The name of the directory can be changed, and the number of files will vary from directory to directory. This creates a challenge, as we need to feed back XML to the Ribbon on demand, as well as record the details of each of the files since we have no collection to access later. I'm going to make an attempt to explain it all, so hopefully I hit the mark with it.

Base XML Required

The first thing that we need to do is add some XML to our file in order to set up a dynamic menu. This XML will start with an onLoad statement to capture the RibbonUI object (to allow menu updates), and also sets up the correct XML tags for the dynamic menu:

XML:
  1. <customUI onLoad="captureRibbonUI" xmlns="http://schemas.microsoft.com/office/2006/01/customui">
  2. <ribbon startFromScratch="false">
  3. <tabs>
  4. <tab id = "xlgTab" label="XLG" insertAfterMso="TabView">
  5. <group id="TestTab" label="My Testing Tab">
  6. <dynamicMenu id="menu1" label="My Menu" getContent="GetContent" />
  7. </group>
  8. </tab>
  9. </tabs>
  10. </ribbon>
  11. </customUI>

Notice that the rest of the details, namely the tab and group information is still the same as in past posts. We'll still have an "XLG" tab with a "TestTab" group on that tab. In addition, we'll have a menu item on that tab called "My Menu"

Capturing the RibbonUI object when the Workbook is opened
The first thing I do now is add a custom property to the ThisWorkbook module, allowing us to capture the RibbonUI object. Capturing this object will allow us to "invalidate" the RibbonX code, forcing it to rebuild the menu when we ask it to. The code for this (to go in the ThisWorkbook module) is:

Visual Basic:
  1. 'Private variables to hold state of Ribbon and Ribbon controls
  2. Private pRibbonUI As IRibbonUI
  3.  
  4. Public Property Let ribbonUI(iRib As IRibbonUI)
  5. 'Set RibbonUI to property for later use
  6. Set pRibbonUI = iRib
  7. End Property
  8.  
  9. Public Property Get ribbonUI() As IRibbonUI
  10. 'Retrieve RibbonUI from property for use
  11. Set ribbonUI = pRibbonUI
  12. End Property

Next, we need to add the routine we specified in the XML onLoad statement. It's this code that will actually be triggered when the workbook loads, and passes the RibbonUI object to our property for storage. This routine looks as follows, and goes in a Standard module:

Visual Basic:
  1. Private Sub CaptureRibbonUI(ribbon As IRibbonUI)
  2. 'Set the RibbonUI to a workbook property for later use
  3. ThisWorkbook.ribbonUI = ribbon
  4. End Sub

How do we hold our file paths?
What we're going to do in this project is identify all the Excel files in a directory, and add the name of each one to a menu item. In order to open the files later, though, we'll need to be able to call up the file path somehow. In the old commandbar objects, we we could assign that path to the Tag property, or even the ToolTipText property, then retrieve it from there. Not so with the new UI, unfortunately. There is no collection to reference, and all you can get is the ID of the button clicked.

So how do we do it? I can think of two ways.

The first would be to build an index in a worksheet with the primary key (if I can call it that) being the control's ID. You'd then leverage a lookup to get back what you need. It would work, but I'd rather avoid using the worksheet like this if I could. (Just personal preference.)

The method that I've elected to use is a little more complicated, but keeps everything in code and out of the users' way. It involves creating a custom collection in order to store our items for use later. This rest of the article will be based on using this method.

To set up our Collection, we first need to create a new class module called "clsFilePaths", to which we'll add the following code:

Visual Basic:
  1. Public sFilePath As String

(Believe it or not, that's it for the Class Module. :) )

To complete the setup for our Collection, we then go back to our Standard module, and insert the following line at the top of the module (after any Option lines):

Visual Basic:
  1. 'Collection to hold the file paths
  2. Dim FilePaths As New Collection

Populating the Dynamic Menu

Now it's time to focus on adding the dynamic content to the menu. In our XML, we specified a routine called "GetContent" to populate the dynamic menu. We've got our Collection set up and ready, so we need to work out how to do this. Our complication is that we need to feed the XML back to the Ribbon to be executed. What I've done to deal with this is:

  • Made the GetContent routine start the process of building the XML
  • It then looks in the directory and figures out what files need to be added
  • For each file it sends the details to a custom function to create the required XML
  • That XML is added to the XML string
  • The details of the file are then added to the Collection
  • Once all files have been dealt with the XML is then passed back to the Ribbon for inclusion

Please note that each button is set up to call the btnCentral macro when clicked. This central macro handler will be important later.

The idea behind the AddButtonXML routine is that it will eventually hold all possible parameters to return dynamic vs static content to return the string. For now, however, I've only dealt with the tags that I need for the example. (To be clear, I'm not using the dynamic portion within, but felt while I was working on each tag that I might as well give all options possible for them.) I tried to comment them appropriately so that you can follow what they do. Both of the routines below go in the Standard module:

Visual Basic:
  1. Private Sub GetContent(control As IRibbonControl, ByRef returnedVal)
  2. 'Populate a menu item
  3. Dim sXML As String
  4. Dim lFiles As Long
  5. Dim lFileCount As Long
  6. Dim fso As Object, objFiles As Object, objFile As Object
  7.  
  8. 'Set error handling
  9. On Error GoTo CloseTags
  10.  
  11. 'Open the XML string
  12. sXML = "<" & "menu xmlns=""http://schemas.microsoft.com/office/2006/01/customui"">"
  13.  
  14. 'Check for files in the directory
  15. Set fso = CreateObject("Scripting.FileSystemObject")
  16. Set objFiles = fso.GetFolder(ActiveSheet.Range("B1")).Files
  17.  
  18. 'Cycle through files, adding Excel files to the menu
  19. For Each objFile In objFiles
  20. If LCase(Right(objFile.Name, 3)) = "xls" Then
  21. lFiles = lFiles + 1
  22. sXML = AddButtonXML(sXML, "Button" & lFiles, False, objFile.Path, _
  23. False, objFile.Name, False, True, "FileSaveAsExcel97_2003", "btnCentral")
  24.  
  25. 'Add the file path to a collection of objects for later retrieval
  26. Dim sFileTip As New clsFilePaths
  27. sFileTip.sFilePath = objFile.Path
  28. FilePaths.Add Item:=sFileTip, Key:=CStr(lFiles)
  29. Set sFileTip = Nothing
  30. End If
  31. Next objFile
  32.  
  33. CloseTags:
  34. 'Close the menu string
  35. sXML = sXML & "<" & "/menu>"
  36. 'Return the completed XML to the RibbonUI
  37. returnedVal = sXML
  38. End Sub
  39.  
  40. Private Function AddButtonXML(sXML As String, id As String, _
  41. Optional bDynaSupertip As Boolean = False, Optional sSupertip As String, _
  42. Optional bDynaLabel As Boolean = False, Optional sLabel As String, _
  43. Optional bDynaImg As Boolean = False, Optional bImgMSO As Boolean = False, Optional sImg As String, _
  44. Optional sOnAction As String) As String
  45.  
  46. 'Add the button id
  47. sXML = sXML & "<" & "button id=""" & id & """"
  48.  
  49. 'Add the description
  50. If Not sSupertip = vbNullString Then
  51. If bDynaSupertip = False Then
  52. sXML = sXML & " supertip=""" & sSupertip & """"
  53. Else
  54. sXML = sXML & " getSupertip=""" & sSupertip & """"
  55. End If
  56. End If
  57.  
  58. 'Add the label
  59. If Not sLabel = vbNullString Then
  60. If bDynaLabel = False Then
  61. sXML = sXML & " label=""" & sLabel & """"
  62. Else
  63. sXML = sXML & " getLabel=""" & sLabel & """"
  64. End If
  65. End If
  66.  
  67. 'Add the image
  68. If Not sImg = vbNullString Then
  69. If bDynaImg = False Then
  70. If bImgMSO = False Then
  71. sXML = sXML & " image=""" & sImg & """"
  72. Else
  73. sXML = sXML & " imageMso=""" & sImg & """"
  74. End If
  75. Else
  76. If bImgMSO = False Then
  77. sXML = sXML & " getImage=""" & sImg & """"
  78. Else
  79. sXML = sXML & " getImageMso=""" & sImg & """"
  80. End If
  81. End If
  82. End If
  83.  
  84. 'Add the macro
  85. If Not sOnAction = vbNullString Then sXML = sXML & " onAction=""" & sOnAction & """"
  86.  
  87. 'Close the XML tags
  88. sXML = sXML & " />"
  89.  
  90. 'Place the XML in the function return
  91. AddButtonXML = sXML
  92. End Function

With all of the above code in place, your menu should now generate providing that you put a valid path, which holds at least one Excel file, in Cell B1.

Reacting to the Button Click
It's time to make sure that we can open our file. Remember that we used a collection to capture the file path earlier? Well now the trick is to get that out.

Since you cannot use just numbers as your ID for the XML code, we need to convert the ID returned by clicking the button into the index of our collection. This is why we started our Button ID's at 1 and kept growing; it matches the index of the Collection exactly. We can get the ID when a button is clicked, so we just need to extract the number from it, and use it to query our collection. The code below is the central button handler, and does exactly that. It also goes in the standard module:

Visual Basic:
  1. Private Sub btnCentral(control As IRibbonControl)
  2. 'Open the file
  3. Workbooks.Open Filename:=FilePaths(CLng(Mid(control.id, 7))).sFilePath
  4. End Sub

Updating the menu for a new directory

The final part of this article deals with updating to a new directory, and getting the new files to show on the Ribbon.

This portion is actually done by leveraging the Invalidate method of the RibbonUI object. Remember way back in the beginning how we captured the RibbonUI to a custom workbook property? This is the reason. The routine I use does two things:

  1. Invalidates the RibbonX, forcing a rebuild of the dynamic control
  2. Clears the collection in preparation for the new menu items

The code, once again to be placed in the Standard module, looks like this:

Visual Basic:
  1. Private Sub Invalidate()
  2. Dim Num As Long
  3. 'Invalidate the menu, allowing a rebuild
  4. ThisWorkbook.ribbonUI.InvalidateControl "menu1"
  5.  
  6. 'Remove all items from the collection (as they are about to be rebuilt)
  7. For Num = 1 To FilePaths.Count
  8. 'Remove first object until none left
  9. FilePaths.Remove 1
  10. Next Num
  11.  
  12. End Sub

And finally, we need to give the user a way to trigger this once they put their file path in cell B1. While we could have gone more complicated, such as adding a button to the menu or group, I elected to go the simple route. I dropped a form control on the worksheet and linked it to the following macro:

Visual Basic:
  1. Public Sub RebuildMenu()
  2. 'Rebuild the menu
  3. Call Invalidate
  4. End Sub

Conclusion

Adding a dynamic menu is much different than it was in Excel version prior to 2007, since we have to create our own collection objects and properties to hold the information we need. With a little organization, however, it's not impossible.

I have prepared a copy of a workbook that includes all the code used above, as the post may be a bit choppy to follow. Please feel free to download it and comment away. It is a Zip file, as I can't upload xlsm files to my blog yet. :(
The only instructions for use are:

  • Enable macros ;)
  • Change the path to a valid file path, which holds at least one excel file, in cell B1 (you can include or ignore the trailing \)
  • Press the button to update
  • Try the menu on the XLG Tab's "Test Tab" group

:)

13 Responses to 'Creating a Dynamic Menu – An Example'

Subscribe to comments with RSS or TrackBack to 'Creating a Dynamic Menu – An Example'.

  1. Maurice said,

    on February 8th, 2007 at 7:30 am

    Thanks! This is what I was looking for and works exactly as outlined.

    I modified it for my much simpler task of updating the menu in the ribbon depending on how the user of my Excel add-in progresses through the application: e.g. change the menu from "Run Step 1" to "Run Step 2" and so on. It works perfectly for this scenario.

  2. nick said,

    on April 22nd, 2008 at 4:41 am

    Is it possible to create ribbon icons with code instead of icons in a menu ?

  3. Ken Puls said,

    on April 22nd, 2008 at 8:27 pm

    Hi Nick,

    I'm not sure I totally follow what you're asking, but if you are asking about loading images on the fly, rather than hard coding their names, then Yes, you can do this. You'll need to use VBA's LoadPicture method to load JPG or GIF files for your pictures. If you want to use PNG, then you'll have to work with an API.

    For reference, examples of both can be found in my book, which can be ordered from the Amazon link at left. Chapter 8 deals strictly with pictures and galleries, and it comes with down-loadable example files.

  4. Bren said,

    on July 29th, 2008 at 4:29 am

    Have you noticed an issue with dynamic menus not loading when ther are no workbooks open. Is there a solution to this?

  5. Ken Puls said,

    on August 6th, 2008 at 8:53 pm

    Hi Bren,

    I actually haven't noticed this, no. It may be something to do with the way you've programmed them? Maybe you touch something, like calculation, that relies on a workbook being loaded?

  6. Bren said,

    on August 20th, 2008 at 7:12 am

    Further to my comment above, it's actually when no workbook that is open that the getVisible and getEnabled functions run really, really slow.

    Is there any solution to this?

  7. Brian said,

    on June 16th, 2009 at 6:29 am

    Ken:

    I've tried using the dynamicMenu approach, but have discovered two things that don't work so well:
    - When you click the menu item, you don't see the buttons underneath until the NEXT time you click it.
    - Worse, when you lose the reference to the ribbon object, the getContent no longer works.

    Is there a way to form the entire XML menu code via VBA during the startup of the add-in, then load it, rather than loading a "skeleton" XML menu structure with dynamic menus and updating it in that way? FYI, I have an application that creates command bars and buttons based on database values -- so it wouldn't work for us to create a single "static" XML menu structure -- and I'd love to create an equivalent ribbon for our 2007 users. The bars/buttons don't have to change after the add-in is loaded, so the complexity in creating the menus is only limited to the startup of the add-in.

  8. Ken Puls said,

    on June 16th, 2009 at 8:15 pm

    Hi Brian,

    Re #1: I agree, that's irritating. I don't recall that it always did that, but I'm wondering if the behaviour changed with a service pack as I also see that with some of my add-ins now. I haven't had the time to dig into it deeply yet.

    Re #2: Yup. Once you lose the ribbon object, it's over and you can't get it back unless you re-open the workbook. I REALLY hope this gets fixed in Office 2010.

    With regards to your last point, that would be a great feature. Unfortunately there is no way to do it at this time. I'm also hoping that we see something to solve this in Office 2010, but I don't know that I'd bet much on it.

  9. Charles said,

    on March 17th, 2010 at 12:40 pm

    Between your blog and "RibbonX Customizing the Office 2007 Ribbon" I have been able to do much of what I was hoping to do. So let me say "thanks" first of all. However, I am spinning my wheels on one point. I have a few dynamic menus that read from system folders that work fine. What I would like to do is have a dynamic submenu that refers to subfolders. Can this be handled from the call back side where XML is generated in the VBA code (similar to the example in ch 9) or does this need to be handled in the XML that gets zipped into .xlsm file? Can it be handled? Any code or advice would be much appreciated.

  10. Ken Puls said,

    on March 18th, 2010 at 7:58 pm

    Hi Charles,

    Since the dynamicmenu callback essentially just creates the appropriate XML on the fly I don't see any reason why you couldn't have your callback insert the XML to create a new dynamic menu as a submenu insted of a button. You'd just need to write the vba to make it happen.

  11. Mehmet said,

    on June 4th, 2010 at 5:22 am

    Hi Ken,
    I have a question similar to Brian's #3.
    Thera are two hard-coded labels in the XML:
    label="My Testing Tab"
    label="My Menu"
    Is there any way to change these during run time?
    I simply wish to create a multi-lingual menu system where all lables are re-defined during run time.
    Regards, Mehmet

  12. Ken Puls said,

    on June 4th, 2010 at 7:28 am

    Hi Mehmet,

    Yes, absolutely. You can use the getLabel callback for the tabs, which allows you to feed them content dynamically. It's not nearly as complicated as the dynamicMenu though.

    Have you got the RibbonX book? There is an example of setting up a Multi-Lingual UI on pages 455-458.

  13. Mehmet said,

    on June 4th, 2010 at 7:32 am

    Sorry!
    Just realised that getLabel, etc. are valid in Tabs and Groups:

    id="MyGroup"
    getLabel="MySheet.GetLabel"
    getScreentip="MySheet.GetScreentip"
    getSupertip="MySheet.GetSupertip"

Post a comment