1

I have an Excel macro enabled workbook which contains dozens of ActiveX Control command buttons used to run various macros. Recently, either due to a Windows or Excel update, or an unresponsive workbook that went into Excel Repair, the ActiveX Controls became corrupted and the command buttons were all converted to images and ceased to function.

This issue has been documented and described here: Microsoft Excel ActiveX Controls Disabled? and ActiveX controls changing to pictures (Excel/VBA)

However the responses and accepted answers all point to how to fix the problem from reoccurring: delete existing .EXD files, not how to reverse the damage that was done in the original file after the Repair version had been saved. In our case, we added many new macros and many additional sheets of data before the problem with the command buttons was noticed. So going back to a previous version and rebuilding the file is more problematic than reversing the damage that was done by the repair.

I found that despite the corruption, the Subs on the individual Sheets remain intact, and the name of the existing CommandButton became the name of the image after corruption (such as Private Sub CommandButton1_Click() ... the name of the image matched the name of the command button: CommandButton1.

I came up with the following code which might be helpful to others, but would like to know if there is a better way to extract the original ActiveX control "Caption" that existed before the corruption. Currently the "CreateButton" Sub below requires hard-coding which sheets contain each button, the style I want for each button, and the appropriate caption.

Is there a way to extract this data from the image and add it to the array in the Function GetImageProperties?

Is it possible to capture the "Caption" or "Font" or "Font Size" data from the Command Button?

For the Sub CodeInserter, is there a way to search within the Sheet's VBA Object for the Sub's title (ex: "Private Sub CommanButton1_Click()"), then look for matching InnerCode (ex: Call updateCostsFromLookupLists); if the InnerCode does not exist within the sub, add it to the sub?

I also have a lot to learn with coding, so if anyone wants to suggest better ways to achieve the result: open to any suggestions on how to improve my code.

Loop through all selected sheets and add worksheets to array:

Option Explicit

Sub RepairMissingButtons()
    Dim a As Long, S As Long
    Dim selShArray() As Worksheet
    
    S = 0
   'For Each selSh In ActiveWindow.SelectedSheets put into array
    For a = 1 To ActiveWindow.SelectedSheets.count
        S = S + 1
        ReDim Preserve selShArray(1 To S)
        Set selShArray(S) = ActiveWindow.SelectedSheets(S)
    Next a
    
    Call LoopThroughImages(selShArray())
End Sub

Loop through Shapes within each of the selected worksheets to find the command buttons that were converted to images (caution: if other images are on the sheet they'll be converted too):

Sub LoopThroughImages(ByRef selectedArr() As Worksheet)
    'Adapted from: https://exceloffthegrid.com/vba-code-to-insert-move-delete-and-control-pictures/
    Dim shp As Shape
    Dim ws As Worksheet
    Dim arr
    Dim x, y, w, h, z, imgName, tlc
    Dim btn As Button
    Dim btnObj
    Dim shName As Variant
    
    For Each shName In selectedArr()
        Set ws = Worksheets(shName.name)
        With ws
            ws.Select  '.Select seems to be necessary: if multiple sheets are selected at once, unable to delete the picture using .Activate
            For Each shp In ws.Shapes
                If shp.Type = msoPicture Then
                    arr = GetImageProperties(shp.name)
                    x = arr(1)
                    y = arr(2)
                    w = arr(3)
                    h = arr(4)
                    z = arr(5)
                    imgName = arr(6)
                    tlc = arr(7)
       
                    Call CreateButton(x, y, w, h, imgName) 'sub to create ActiveX command button
                    
            '    Alternate option to creating new ActiveX command button is to create a new Form Control Button
    '                Set btn = ws.Buttons.Add(x, y, w, h)
    '                btn.OnAction = "Success"
    '                btn.Caption = imgName
                    
                    shp.Delete  'delete the underlying picture image that was created by Excel repair
                End If
            Next shp
            If ws.Shapes.count = 0 And Left(ws.name, 2) <> "x_" Then
                Call CreateButton(371.25, 786, 105.75, 33.75, "CommandButton1")
                Call CreateButton(337.5, 786, 105.75, 33.75, "CommandButton2")
                Call CreateButton(435.75, 786, 105.75, 33.75, "CommandButton3")
                Call CreateButton(305.25, 786, 105.75, 33.75, "CommandButton4")
            End If
        End With
    Next shName
End Sub

Below is where I'm extracting the data from the Image. Is it possible to capture the "Caption" or "Font" or "Font Size" data from the Command Button? I think this may have disappeared when button was converted to an image.

Function GetImageProperties(name As String)
    Dim myImage As Shape
    Dim ws As Worksheet
    Dim arr(1 To 7) As String
    
    Set ws = ActiveSheet
    Set myImage = ws.Shapes(name)
    arr(1) = myImage.Top
    arr(2) = myImage.Left
    arr(3) = myImage.width
    arr(4) = myImage.Height
    arr(5) = myImage.ZOrderPosition
    arr(6) = myImage.name
    arr(7) = myImage.TopLeftCell   
 
    GetImageProperties = arr
End Function

Using the properties extracted from the Image, create a new ActiveX Command button. Unfortunately my approach requires hard-coding in, for each worksheet and each button within each sheet: the name of Captions, BackColor, Fonts, and Font-Sizes lost from corruption. Is there a way to extract any of this from original image?

Sub CreateButton(cellTop, cellLeft, cellwidth, cellheight, btnName)

    Dim Obj As Object
    Dim shName As String
    Dim code As String
    Dim innerCode As String
    Dim searchCode As String

    'create button
    Set Obj = ActiveSheet.OLEObjects.Add(ClassType:="Forms.CommandButton.1", Link:=False, DisplayAsIcon:=False, Left:=CSng(cellLeft), Top:=CSng(cellTop), width:=CSng(cellwidth), Height:=CSng(cellheight))
    Obj.name = btnName
    shName = ActiveSheet.name
    With Obj.Object
        Select Case shName
            Case "x_Key"
                .Font.name = "Bookman Old Style"
                .Font.Bold = False
                Select Case btnName
                    Case "CommandButton1"
                        .Caption = "Create New Kit"
                        .Font.Size = 26
                        .BackColor = RGB(255, 255, 153) 'yellow
                    Case "CommandButton2"
                        .Caption = "Update"
                    Case "CommandButton3"
                        .Caption = "Start New Merge"
                        .Font.Size = 14
                        .BackColor = RGB(192, 255, 192) 'green
                    Case "CommandButton4"
                        .Caption = "Edit or Delete Kit"
                        .Font.Size = 14
                        .BackColor = RGB(255, 192, 255) 'pink
                End Select
            Case "x_Merge"
                .Font.name = "Bookman Old Style"
                .Font.Bold = False
                Select Case btnName
                    Case "OpenMergeUFBtn"
                        .Caption = "Start New Merge"
                        .Font.Size = 24
                        .BackColor = RGB(255, 255, 153) 'yellow
                End Select
            Case "x_PhysicalInventory"
                .Font.name = "Calibri"
                .Font.Bold = False
                .Font.Size = 11
                .BackColor = RGB(255, 255, 153) 'yellow
                Select Case btnName
                    Case "CommandButton1"
                        .Caption = "Update From InvCount"
                    Case "CommandButton2"
                        .Caption = "Update Orders Matrix from Lookup Lists"
                    Case "CommandButton3"
                        .Caption = "Update Uniques"
                End Select
            Case "x_ControlPanel"
                .Font.name = "Calibri"
                .Font.Bold = False
                .Font.Size = 11
                .BackColor = RGB(255, 255, 153) 'yellow
                Select Case btnName
                    Case "CommandButton1"
                        .Caption = "Import POS Inventory"
                    Case "CommandButton2"
                        .Caption = "Create Kits"
                    Case "CommandButton3"
                        .Caption = "Start New Merge"
                    Case "CommandButton4"
                        .Caption = "Update Uniques"
                    Case "CommandButton5"
                        .Caption = "Import SLS Catalog"
                    Case "CommandButton6"
                        .Caption = "Edit or Delete Kit"
                        .BackColor = RGB(255, 192, 255) 'pink
                    Case "CommandButton7"
                        .Caption = "Create Order Log"
                    Case "CommandButton8"
                        .Caption = "POS Import"
                    Case "CommandButton9"
                        .Caption = "Update Physical Inv"
                    Case "CommandButton10"
                        .Caption = "Update From InvCount"
                    Case "vendSheetBtn"
                        .Caption = "Create Vendor Sheets"
                End Select
            Case "x_Uniques"
                .Font.name = "Calibri"
                .Font.Bold = False
                .Font.Size = 11
                .BackColor = RGB(255, 255, 153) 'yellow
                Select Case btnName
                    Case "UpdateUniquesBtn"
                        .Caption = "Update Uniques"
                End Select
            Case "x_Template"
                .Font.name = "Adobe Fangsong Std R"
                .Font.Bold = False
                .Font.Size = 14
                .BackColor = RGB(255, 255, 153) 'yellow
                Select Case btnName
                    Case "ClearPricesBtn" 'CommandButton4
                        .Caption = "Clear Pricing"
                        .BackColor = RGB(255, 192, 255) 'pink
                    Case "PosCostBtn" 'CommandButton2
                        .Caption = "POS Data"
                    Case "LookupListBtn" 'CommandButton1
                        .Caption = "LookupList"
                    Case "HtmlBtn" 'CommandButton3
                        .Caption = "HTML"
                End Select
            Case Else
                .Font.name = "Adobe Fangsong Std R"
                .Font.Bold = False
                .Font.Size = 14
                .BackColor = RGB(255, 255, 153) 'yellow
                If Left(shName, 2) <> "x_" Then
                    innerCode = "  'from ClassUpdateCosts module" & vbCrLf
                    Select Case btnName
                        Case "CommandButton4", "ClearPricesBtn"
                            .Caption = "Clear Pricing"
                            .BackColor = RGB(255, 192, 255) 'pink
                            innerCode = innerCode & "    Call deletePricesFromClassSheet"
                        Case "CommandButton2", "PosCostBtn"
                            .Caption = "POS Data"
                            innerCode = innerCode & "    Call updateCostsFromPOSInventory"
                        Case "CommandButton1", "LookupListBtn"
                            .Caption = "LookupList"
                            innerCode = innerCode & "    Call updateCostsFromLookupLists"
                        Case "CommandButton3", "HtmlBtn"
                            .Caption = "HTML"
                            innerCode = innerCode & "    Call updateClassesFreightLaborWebHTML"
                    End Select
                    searchCode = "Private Sub " & btnName & "_Click()"
                    code = CodeBuilder(btnName, innerCode)
                    Call CodeInserter(ActiveSheet, code, searchCode)
                End If
        End Select
    End With
End Sub

In some cases I wanted to inject missing code into the Worksheet's VBA Object. First by building the code and then inserting. I found that if I searched for the code in it's entirety and it was not an exact match, the macro would quit and give an Ambiguous Code error if it encountered an existing sub with the same name (such as Sub CommandButton1_Click()), so I just searched for the header.

It would be helpful to know how to search within a Sub with the same named header to see if the Call I need within the sub already exists and if not (see innerCode), add it within the Sub. I believe this might be possible using ProcBodyLine, ProcCountLines, ProcOfLine, ProcStartLine ('https://learn.microsoft.com/en-us/office/vba/language/reference/visual-basic-add-in-model/properties-visual-basic-add-in-model#procbodyline) but having difficulty making that work:

Function CodeBuilder(btnName, innerCode)
    Dim code As String
    code = "Private Sub " & btnName & "_Click()" & vbCrLf
    code = code & innerCode & vbCrLf
    code = code & "End Sub"
    CodeBuilder = code
End Function

Sub CodeInserter(wsName As Worksheet, code As String, searchCode As String)
    Dim existingCode As String
    Dim Found As Boolean
 'add macro at the end of the sheet module
    With ActiveWorkbook.VBProject.VBComponents(ActiveSheet.CodeName).CodeModule
        If .CountOfLines <> 0 Then
            existingCode = .Lines(1, .CountOfLines)
            If InStr(existingCode, searchCode) > 0 Then Found = True Else Found = False
        End If
        If Found = False Or .CountOfLines = 0 Then
            .InsertLines .CountOfLines + 1, code
        End If
    End With
End Sub

Any suggestions on improving the code is appreciated!

braX
  • 11,506
  • 5
  • 20
  • 33
mrSteveW
  • 11
  • 3
  • I would restore from backup. Windows has shadow copies under "Previous Versions". If you don't have shadow copies or proper backups, no time like the present! – HackSlash Dec 02 '21 at 23:01
  • https://winaero.com/restore-previous-versions-windows-10/ – HackSlash Dec 02 '21 at 23:04
  • Thanks @HackSlash. Considered, but previous version would be missing a lot of new macros and data that was entered after the Repair and before we noticed the problem with the buttons being corrupted. Could run a DIFF program to find all the differences, but as this has happened more than once, decided to try an approach of reversing the damage done programmatically. – mrSteveW Dec 03 '21 at 00:11

0 Answers0