如何在 Excel-VBA 中以编程方式取消切换所有自定义功能区按钮(仅适用于断点)?

问题描述 投票:0回答:1
excel vba breakpoints ribbon togglebutton
1个回答
0
投票

首先,我们需要一种在工作簿中存储数据的方法。您可以使用命名范围或其他内容,但我将使用我的库 LibExcelBookItems,因为它非常易于使用。

LibExcelBookItems
模块的代码也在这里:

'''=============================================================================
''' Excel VBA Tools
''' -----------------------------------------------
''' https://github.com/cristianbuse/Excel-VBA-Tools
''' -----------------------------------------------
''' MIT License
'''
''' Copyright (c) 2018 Ion Cristian Buse
'''
''' Permission is hereby granted, free of charge, to any person obtaining a copy
''' of this software and associated documentation files (the "Software"), to
''' deal in the Software without restriction, including without limitation the
''' rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
''' sell copies of the Software, and to permit persons to whom the Software is
''' furnished to do so, subject to the following conditions:
'''
''' The above copyright notice and this permission notice shall be included in
''' all copies or substantial portions of the Software.
'''
''' THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
''' IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
''' FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
''' AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
''' LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
''' FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
''' IN THE SOFTWARE.
'''=============================================================================

'*******************************************************************************
'' Description:
''    - Simple strings can be stored/retrieved using CustomXMLParts per book
''    - This module encapsulates the XML logic and exposes easy-to-use methods
''      without the need to write actual XML
'' Public/Exposed methods:
''    - BookItem - parametric property Get/Let
''    - GetBookItemNames
'' Notes:
''    - To delete a property simply set the value to a null string
''      e.g. BookItem(ThisWorkbook, "itemName") = vbNullString
'*******************************************************************************

Option Explicit
Option Private Module

Private Const XML_NAMESPACE As String = "ManagedExcelCustomXML"
Private Const rootName As String = "root"

'*******************************************************************************
'Returns the Root CustomXMLPart under the custom namespace
'part is created if missing!
'*******************************************************************************
Private Function GetRootXMLPart(ByVal book As Workbook) As CustomXMLPart
    Const xmlDeclaration As String = "<?xml version=""1.0"" encoding=""UTF-8""?>"
    Const rootTag As String = "<" & rootName & " xmlns=""" & XML_NAMESPACE _
                            & """></" & rootName & ">"
    Const rootXmlPart As String = xmlDeclaration & rootTag
    '
    With book.CustomXMLParts.SelectByNamespace(XML_NAMESPACE)
        If .Count = 0 Then
            Set GetRootXMLPart = book.CustomXMLParts.Add(rootXmlPart)
        Else
            Set GetRootXMLPart = .Item(1)
        End If
    End With
End Function

'*******************************************************************************
'Clears all CustomXMLParts under the custom namespace
'*******************************************************************************
Private Sub ClearRootXMLParts(ByVal book As Workbook)
    With book.CustomXMLParts.SelectByNamespace(XML_NAMESPACE)
        Dim i As Long
        For i = .Count To 1 Step -1
            .Item(i).Delete
        Next i
    End With
End Sub

'*******************************************************************************
'Get the Root Node under the custom namespace
'Node is created if missing!
'*******************************************************************************
Private Function GetRootNode(ByVal book As Workbook) As CustomXMLNode
    Dim root As CustomXMLNode
    If root Is Nothing Then
        With GetRootXMLPart(book)
            Dim nsPrefix As String
            nsPrefix = .NamespaceManager.LookupPrefix(XML_NAMESPACE)
            Set root = .SelectSingleNode("/" & nsPrefix & ":" & rootName & "[1]")
        End With
    End If
    Set GetRootNode = root
End Function

'*******************************************************************************
'Get an XML Node. Create it if missing
'*******************************************************************************
Private Function GetNode(ByVal book As Workbook _
                       , ByVal nodeName As String _
                       , ByVal addIfMIssing As Boolean) As CustomXMLNode
    Dim node As CustomXMLNode
    Dim expr As String
    '
    With GetRootNode(book)
        expr = .XPath & "/" & nodeName & "[1]"
        Set node = .SelectSingleNode(expr)
        If node Is Nothing And addIfMIssing Then
            .AppendChildNode nodeName
            Set node = .SelectSingleNode(expr)
        End If
    End With
    Set GetNode = node
End Function

'*******************************************************************************
'Retrieves/sets a book property value from a CustomXMLNode
'*******************************************************************************
Public Property Get BookItem(ByVal book As Workbook _
                           , ByVal itemName As String) As String
    ThrowIfInvalid book, itemName
    Dim node As CustomXMLNode
    Set node = GetNode(book, itemName, False)
    If Not node Is Nothing Then BookItem = node.Text
End Property
Public Property Let BookItem(ByVal book As Workbook _
                           , ByVal itemName As String _
                           , ByVal itemValue As String)
    ThrowIfInvalid book, itemName
    If LenB(itemValue) = 0 Then
        Dim node As CustomXMLNode
        Set node = GetNode(book, itemName, False)
        If Not node Is Nothing Then node.Delete
    Else
        GetNode(book, itemName, True).Text = itemValue
    End If
End Property
Private Sub ThrowIfInvalid(ByRef book As Workbook, ByRef itemName As String)
    Const methodName As String = "BookItem"
    If book Is Nothing Then
        Err.Raise 91, methodName, "Book not set"
    ElseIf LenB(itemName) = 0 Then
        Err.Raise 5, methodName, "Invalid item name"
    End If
End Sub

'*******************************************************************************
'Returns a collection of custom node names within the custom namespace
'*******************************************************************************
Public Function GetBookItemNames(ByVal book As Workbook) As Collection
    If book Is Nothing Then Err.Raise 91, "GetBookItemNames", "Book not set"
    '
    Dim coll As New Collection
    With GetRootNode(book).ChildNodes
        Dim i As Long
        ReDim arr(0 To .Count - 1)
        For i = 1 To .Count
            coll.Add .Item(i).BaseName
        Next i
    End With
    Set GetBookItemNames = coll
End Function

调用示例:

BookItem(ThisWorkbook, "myVar") = myTextValue
Debug.Print BookItem(ThisWorkbook, "myVar")

现在我们有了一种持久存储数据的方法,让我们确保在状态丢失的情况下可以恢复功能区。替换这个:

Option Explicit

Public myRibbon As IRibbonUI
Public pressed As Boolean
----------------------------------------------------------------------------------

Sub OnRibbonLoad(ribbon As IRibbonUI)

    Set myRibbon = ribbon
    
End Sub

这样:

Option Explicit

Private myRibbon As IRibbonUI

#If Mac Then
    #If VBA7 Then
        Private Declare PtrSafe Function CopyMemory Lib "/usr/lib/libc.dylib" Alias "memmove" (Destination As Any, Source As Any, ByVal Length As LongPtr) As LongPtr
    #Else
        Private Declare Function CopyMemory Lib "/usr/lib/libc.dylib" Alias "memmove" (Destination As Any, Source As Any, ByVal Length As Long) As Long
    #End If
#Else 'Windows
    'https://msdn.microsoft.com/en-us/library/mt723419(v=vs.85).aspx
    #If VBA7 Then
        Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As LongPtr)
    #Else
        Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)
    #End If
#End If

Sub OnRibbonLoad(ByVal ribbon As IRibbonUI)
    Set SafeRibbon = ribbon
End Sub

'===============================================================================
'Set/Get Ribbon object
'===============================================================================
Private Property Set SafeRibbon(ByVal ribbonUI As IRibbonUI)
    Set myRibbon = ribbonUI
    Dim mustAvoidSaveDialog As Boolean
    mustAvoidSaveDialog = ThisWorkbook.IsAddin And ThisWorkbook.Saved
    BookItem(ThisWorkbook, "BookPtr") = CStr(ObjPtr(ThisWorkbook))
    BookItem(ThisWorkbook, "RibbonPtr") = CStr(ObjPtr(myRibbon))
    If mustAvoidSaveDialog Then ThisWorkbook.Saved = True
End Property
Public Property Get SafeRibbon() As IRibbonUI
    If myRibbon Is Nothing Then
        If BookItem(ThisWorkbook, "BookPtr") = CStr(ObjPtr(ThisWorkbook)) Then
            'Restore ribbon
            #If Win64 Then
                Dim ptr As LongLong
                Const ptrSize As Long = 8
            #Else
                Dim ptr As Long
                Const ptrSize As Long = 4
            #End If
            Dim obj As Object
            '
            ptr = Int(BookItem(ThisWorkbook, "RibbonPtr"))
            CopyMemory obj, ptr, ptrSize 'Unmanaged - reference not counted
            Set myRibbon = obj
            CopyMemory obj, 0, ptrSize 'Reference count not decremented
        End If
    End If
    Set SafeRibbon = myRibbon
End Property

请注意,

myRibbon
现在是私有的,并且只能对
SafeRibbon
参数属性进行调用。 另请注意,我删除了
pressed
模块成员 - 我们不再需要它了。


当一个宏已经在循环中运行时,我们不能简单地调用另一个宏。我们必须等待第一个退出。我们可以使用异步调用来实现这一点。

我们还可以使用图书项目库来检索发生失效时最后按下的按钮的状态:

Sub GetPressed(control As IRibbonControl, ByRef returnedVal)
    Dim lastIDPressed As String
    '
    lastIDPressed = BookItem(ThisWorkbook, "lastIDPressed")
    returnedVal = (lastIDPressed = control.ID)
    If Len(lastIDPressed) > 0 Then AsyncMacro lastIDPressed
End Sub

Sub AnyButtonGotPressed(control As IRibbonControl, IsPressed)
    If Not IsPressed Then
        BookItem(ThisWorkbook, "lastIDPressed") = vbNullString
        Exit Sub
    End If
    '
    BookItem(ThisWorkbook, "lastIDPressed") = control.ID
    If Not isMacroRunning Then AsyncMacro control.ID
End Sub

Private Sub AsyncMacro(ByVal ctrlID As String)
    Application.OnTime Now(), CallbackName("AsyncMacroCallback", ctrlID)
End Sub

Private Function CallbackName(ByVal funcName As String, ByVal ctrlID As String) As String
    CallbackName = "'" & Replace(ThisWorkbook.Name, "'", "''") _
                 & "'!'" & funcName & " """ & ctrlID & """'"
End Function

Public Sub AsyncMacroCallback(Optional ByVal ctrlID As String)
    isMacroRunning = True
    Select Case ctrlID
        Case "customButton1": FirstMacro ctrlID
        Case "customButton2": SecondMacro ctrlID
        Case "customButton3": ThirdMacro ctrlID
        Case Else
            '...
    End Select
    BookItem(ThisWorkbook, ctrlID) = CStr(False)
    SafeRibbon.InvalidateControl ctrlID
    isMacroRunning = False
End Sub

其中

Private isMacroRunning As Boolean
在模块级别声明。


最终代码可能如下所示:

Option Explicit

Private myRibbon As IRibbonUI
Private isMacroRunning As Boolean

#If Mac Then
    #If VBA7 Then
        Private Declare PtrSafe Function CopyMemory Lib "/usr/lib/libc.dylib" Alias "memmove" (Destination As Any, Source As Any, ByVal Length As LongPtr) As LongPtr
    #Else
        Private Declare Function CopyMemory Lib "/usr/lib/libc.dylib" Alias "memmove" (Destination As Any, Source As Any, ByVal Length As Long) As Long
    #End If
#Else 'Windows
    'https://msdn.microsoft.com/en-us/library/mt723419(v=vs.85).aspx
    #If VBA7 Then
        Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As LongPtr)
    #Else
        Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)
    #End If
#End If

Sub OnRibbonLoad(ByVal ribbon As IRibbonUI)
    Set SafeRibbon = ribbon
End Sub

'===============================================================================
'Set/Get Ribbon object
'===============================================================================
Private Property Set SafeRibbon(ByVal ribbonUI As IRibbonUI)
    Set myRibbon = ribbonUI
    Dim mustAvoidSaveDialog As Boolean
    mustAvoidSaveDialog = ThisWorkbook.IsAddin And ThisWorkbook.Saved
    BookItem(ThisWorkbook, "BookPtr") = CStr(ObjPtr(ThisWorkbook))
    BookItem(ThisWorkbook, "RibbonPtr") = CStr(ObjPtr(myRibbon))
    If mustAvoidSaveDialog Then ThisWorkbook.Saved = True
End Property
Public Property Get SafeRibbon() As IRibbonUI
    If myRibbon Is Nothing Then
        If BookItem(ThisWorkbook, "BookPtr") = CStr(ObjPtr(ThisWorkbook)) Then
            'Restore ribbon
            #If Win64 Then
                Dim ptr As LongLong
                Const ptrSize As Long = 8
            #Else
                Dim ptr As Long
                Const ptrSize As Long = 4
            #End If
            Dim obj As Object
            '
            ptr = Int(BookItem(ThisWorkbook, "RibbonPtr"))
            CopyMemory obj, ptr, ptrSize 'Unmanaged - reference not counted
            Set myRibbon = obj
            CopyMemory obj, 0, ptrSize 'Reference count not decremented
        End If
    End If
    Set SafeRibbon = myRibbon
End Property

Sub GetPressed(control As IRibbonControl, ByRef returnedVal)
    Dim lastIDPressed As String
    '
    lastIDPressed = BookItem(ThisWorkbook, "lastIDPressed")
    returnedVal = (lastIDPressed = control.ID)
    If Len(lastIDPressed) > 0 Then AsyncMacro lastIDPressed
End Sub

Sub AnyButtonGotPressed(control As IRibbonControl, IsPressed)
    If Not IsPressed Then
        BookItem(ThisWorkbook, "lastIDPressed") = vbNullString
        Exit Sub
    End If
    '
    BookItem(ThisWorkbook, "lastIDPressed") = control.ID
    If Not isMacroRunning Then AsyncMacro control.ID
End Sub

Private Sub AsyncMacro(ByVal ctrlID As String)
    Application.OnTime Now(), CallbackName("AsyncMacroCallback", ctrlID)
End Sub

Private Function CallbackName(ByVal funcName As String, ByVal ctrlID As String) As String
    CallbackName = "'" & Replace(ThisWorkbook.Name, "'", "''") _
                 & "'!'" & funcName & " """ & ctrlID & """'"
End Function

Public Sub AsyncMacroCallback(Optional ByVal ctrlID As String)
    isMacroRunning = True
    Select Case ctrlID
        Case "customButton1": FirstMacro ctrlID
        Case "customButton2": SecondMacro ctrlID
        Case "customButton3": ThirdMacro ctrlID
        Case Else
            '...
    End Select
    BookItem(ThisWorkbook, ctrlID) = CStr(False)
    SafeRibbon.InvalidateControl ctrlID
    isMacroRunning = False
End Sub

Sub FirstMacro(ButtonName As String)
    Debug.Print "Enter macro 1 " & Now
    On Error GoTo CleanExit
    Do While BookItem(ThisWorkbook, "lastIDPressed") = ButtonName
        With ThisWorkbook.Sheets(1)
            .Range("A1").Select
            If ActiveCell.Address <> .Range("A1") Then
                'Here comes stuff to do with the cell, if the user clicks somewhere out of "A1"
            End If
            DoEvents
        End With
    Loop
CleanExit:
    Debug.Print "Exit macro 1 " & Now
End Sub
Sub SecondMacro(ButtonName As String)
    Debug.Print "Enter macro 2 " & Now
    On Error GoTo CleanExit
    Do While BookItem(ThisWorkbook, "lastIDPressed") = ButtonName
        With ThisWorkbook.Sheets(1)
            .Range("B2").Select
            If ActiveCell.Address <> .Range("B2") Then
                'Here comes stuff to do with the cell, if the user clicks somewhere out of "B2"
            End If
            DoEvents
        End With
    Loop
CleanExit:
    Debug.Print "Exit macro 2 " & Now
End Sub
Sub ThirdMacro(ButtonName As String)
    Debug.Print "Enter macro 3 " & Now
    On Error GoTo CleanExit
    Do While BookItem(ThisWorkbook, "lastIDPressed") = ButtonName
        With ThisWorkbook.Sheets(1)
            .Range("C3").Select
            If ActiveCell.Address <> .Range("C3") Then
                'Here comes stuff to do with the cell, if the user clicks somewhere out of "C3"
            End If
            DoEvents
        End With
    Loop
CleanExit:
    Debug.Print "Enter macro 3 " & Now
End Sub

请注意,实际的宏(第一、第二、第三)都不需要对

InvalidateControl
进行更多调用,并且我们也不需要在
InvalidateControl
方法中将所有控件循环到
AnyButtonGotPressed
.

© www.soinside.com 2019 - 2024. All rights reserved.