1
votes

I need some help with Custom UI Editor code for a Ribbon in Word. The same code I've been using in Word 2010 does not work in Word 2019 any more. When I create a document based off a template the Ribbon is there and works. I save the document and close it. When I reopen the Ribbon is like "dead". It's not activated and the code no longer runs.

What has changed in Word 2019? What do I need to do to correct this issue?

Here is the Custom UI Code:

<customUI xmlns="http://schemas.microsoft.com/office/2009/07/customui">
<!--This is the Expense Template-->
    
    <ribbon>
        <tabs>
            <tab id="customTab3" label="Expense">
                <group id="customGroup110" label="Expense/Disbursement Calculate">
                    <button id="customButton110" size="large" label="Expense/Disbursement Calculate" imageMso="CreateReport" onAction="ExpenseCalculate" />
                </group>
            </tab>
        </tabs>
    </ribbon>
</customUI> 

Here is the code that is in the template:

Sub ExpenseCalculate(control As IRibbonControl)

' Edited Code On: 2/6/2012
' Edited Code By: Sheila Shines
' Code Change: Added code to test to see what Template a user is in before running code
    
    Dim rowz As Integer
    Dim theRow As Integer
    Dim rT As Integer
    Dim myTemplate As Template

    Set myTemplate = ActiveDocument.AttachedTemplate
    
    If UCase(myTemplate.Name) = "EXPENSE.DOTM" Or UCase(myTemplate.Name) = "EXPENSE.DOT" Then
        'MOVES TO BEGINNING OF DOCUMENT
        Selection.HomeKey unit:=wdStory, Extend:=wdMove

        'LINE DOWN
        Selection.MoveDown unit:=wdLine, Count:=1, Extend:=wdMove

        'LINE DOWN UNTIL INTO A TABLE
        While Selection.Information(wdWithInTable) = 0
            Selection.MoveDown unit:=wdLine, Count:=1, Extend:=wdMove
        Wend
    
        'MOVE TO START OF ROW OF THE TABLE
        Selection.StartOf unit:=wdRow, Extend:=wdMove
    
        'SELECTING TABLE
        ActiveDocument.Tables(1).Select
    
        'NUMBER OF ROWS IN THE TABLE
        rowz = Selection.Information(wdMaximumNumberOfRows)
    
        'MOVING LEFT ONE PLACE
        Selection.MoveLeft unit:=wdCharacter, Count:=1, Extend:=wdMove
    
        'ROW CONTAINING THE BEGINNING OF THE SELECTION
        theRow = Selection.Information(wdStartOfRangeRowNumber)
    
        'MOVING DOWN ONE LINE AT A TIME THROUGH TABLE
        While theRow < rowz
            Selection.MoveDown unit:=wdLine, Count:=1, Extend:=wdMove
            theRow = Selection.Information(wdStartOfRangeRowNumber)
        Wend
    
        'MOVING OVER TWO CELLS
        Selection.Move unit:=wdCell, Count:=2
    
        'DELETING INFORMATION OUT OF CELL IF ANY
        Selection.Range.Delete
    
        rowz = rowz - 1
        rT = Right$(Str$(rowz), Len(Str$(rowz)) - 1)
    
        'INSERTING FIELD INTO TABLE
        Selection.InsertFormula Formula:="=sum(c2:c" & CInt(rT) & ")", NumberFormat:="$#,##0.00;($#,##0.00)"
    
        'PRINTING DOCUMENT
        ActiveDocument.PrintOut
    Else
        MsgBox "You need to be in an Expense template in order to use this macro", vbCritical, "In wrong template"
        End
    End If
End Sub

Here is the solution that I have tried

What happens to the Word session ribbon after closing and reopening a document with a custom ribbon?

https://stackguides.com/questions/57841404/what-happens-to-the-word-session-ribbon-after-closing-and-reopening-a-document-w
2
Please use the edit link below your question and add a link to the related solution that you have tried.Timothy Rylatt

2 Answers

1
votes

The solution that you tried looks to be a real kludge and is definitely not something I would recommend. But as it partially worked for you it confirms the gut feeling that I had when I first read your question, that your ribbon needs to be invalidated (refreshed).

There is an answer here that gives the code you need, though I have a few issues with it.

At this point I have to explain that I do not put ribbons, or code, in document templates (I put all my code in a global template and then use ribbon callbacks to determine which controls should be visible/enabled) so I'm not sure if this will work for you.

To make this work you need to get a pointer to the ribbon when it loads. For that you need an onLoad callback and corresponding ribbon xml:

<customUI xmlns="http://schemas.microsoft.com/office/2009/07/customui" onLoad="ribbonOnLoad">

Callback module (BTW, I took the liberty of rewriting your code):

Option Explicit

'CopyMemory for ribbon retrieval
#If Win64 Then
Declare PtrSafe Sub CopyMemory Lib "kernel32" _
  Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As LongPtr)
#Else
Public Declare Sub CopyMemory Lib "kernel32" _
  Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)
#End If

Private rbnUI                As IRibbonUI

Public Property Get RibbonUI() As IRibbonUI
#If Win64 Then
  Dim lngRbnPtr                   As LongPtr
#Else
  Dim lngRbnPtr                   As Long
#End If
  Dim objRibbon                   As Object

  If rbnUI Is Nothing Then
    'the pointer is lost so retrieve it from the registry
    lngRbnPtr = GetSetting("Templates", "Ribbon", "Ribbon Pointer WD")
    CopyMemory objRibbon, lngRbnPtr, 4
    Set rbnUI = objRibbon
    ' clean up invalid object
    CopyMemory objRibbon, 0&, 4
    Set objRibbon = Nothing

  End If

  Set RibbonUI = rbnUI

End Property

Public Property Set RibbonUI(ribbon As IRibbonUI)
#If Win64 Then
  Dim lngRbnPtr                   As LongPtr
#Else
  Dim lngRbnPtr                   As Long
#End If

  Set rbnUI = ribbon
  lngRbnPtr = ObjPtr(ribbon)
  'save pointer to registry for safe keeping
  SaveSetting "Templates", "Ribbon", "Ribbon Pointer WD", lngRbnPtr
End Property

Private Sub ribbonOnLoad(ribbon As IRibbonUI)
  ' Store pointer to IRibbonUI
  Set RibbonUI = ribbon
End Sub

Public Sub ribbonInvalidate()
  On Error GoTo ProcError

  RibbonUI.Invalidate

ProcExit:
  'Clean up
  On Error Resume Next
  Exit Sub

ProcError:
  If Err.Number = 91 Then
    MsgBox "Unrecoverable Ribbon Error" & vbCrLf & "" & vbCrLf & _
      "Unable to refresh the ribbon. Please save and reopen " & Application.name & _
      ".", vbOKOnly + vbExclamation, "Ribbon Error"
  Else
    'add your own
  End If
End Sub

Sub ExpenseCalculate(control As IRibbonControl)

    Dim myTemplate As Template

    Set myTemplate = ActiveDocument.AttachedTemplate
    
    If UCase(myTemplate.name) = "EXPENSE.DOTM" Or UCase(myTemplate.name) = "EXPENSE.DOT" Then
        'INSERTING FIELD INTO TABLE
        With ActiveDocument.Tables(1).Rows.Last.Cells(3)
          .Range.Delete
          .Formula Formula:="=sum(above)", NumFormat:="$#,##0.00;($#,##0.00)"
        End With
    
        'PRINTING DOCUMENT
        ActiveDocument.PrintOut
    Else
        MsgBox "You need to be in an Expense template in order to use this macro", vbCritical, "In wrong template"
        End
    End If
  Set myTemplate = Nothing
End Sub

Next you need a means of invalidating the ribbon. For this I use a class module that hooks into Word's application events. This enables me to invalidate the ribbon each time ActiveDocument changes, i.e. creating or opening a document, switching between open documents.

Option Explicit

Public WithEvents appWord           As Word.Application

Private Sub appWord_DocumentChange()
  InvalidateRibbon
End Sub

The class module needs to be initialized when the template is loaded. This can be done using an AutoExec routine. I like to keep the actual initializing in a separate routine so that I can call it from a global error handling routine.

Public wordEvents               As clsWordEvents

Public Sub AutoExec()
  instantiateEventHandler

End Sub

Public Sub instantiateEventHandler()
  If wordEvents Is Nothing Then
    Set wordEvents = New clsWordEvents
    Set wordEvents.appWord = Word.Application
  End If
End Sub
0
votes

By default, when you create a .dotx document from a template, the macro code is not copied to the document. Instead, the document remains linked to the template and calls the template to run the macro.

Your macro theoretically checks whether the document is attached to a particular template, but since the macro code is only in the template, it can't run if the document is attached to a different template. So nothing happens.

BTW, your heavy use of the Selection object indicates you created this with the macro recorder. That's a good initial learning step, but the code will be more reliable and run faster if you switch all statements to use the Range object instead.