Excel vba - Can't insert a row using customized context menu and - excel

I am trying to create a context menu item to add a row on a sheet at the position of the selected cell and do some more stuff as well. This is done using a custom object of the class clsMyControls to handle all of my custom controls. The controls created, call a macro in a standard module, which forewards the command to the custom object MyControls. MyControls will foreward the command to another object using CallByName.
This way all of my custom Objects can use MyControls to create controls and to route the commands to their own methods.
The routing works fine. I can read the Address of the selected cell, alter the Value etc. However, when I try to insert or delete a row, nothing happens, not even an error.
Below is the code to reproduce the issue. It implements two ways to create the control and to call the Insert Method. The one I have issues with, and the simple way that works.
The second way does not use MyControls and creates its own control. This way it is possible to insert a row.
Both ways call the same Insert method of the same object.
EDIT: [
The difference is the parameters that are passed to the macro. As soon as a Parameter is built into the .onAction-String of the control, the insert method fails. WHY?
]
First, the (simplified) Class clsMyControls which is supposed to handle all my custom controls
Option Explicit
Option Base 1
Private myItems As Collection 'the collection to carry all controls created here
Private myObjects As Collection 'the collection of objects that create controls by means of this object
'____________________________
Private Sub Class_Initialize()
Set myItems = New Collection
Set myObjects = New Collection
End Sub
'____________________________
Public Sub ReturnFromMacro(Optional args As Variant)
Debug.Print "ReturnFromMacro " & Selection.Address
'Selection.EntireRow.Insert , CopyOrigin:=xlFormatFromLeftOrAbove 'It does not work here
CallByName myObjects(args(1)), args(2), VbMethod 'forewarding
End Sub
'____________________________
Public Sub CreateEntry(ObjectReference As Object, ProcedureName As String, Caption As String)
Dim control As Object
With Application.CommandBars("Cell").Controls
Set control = .Add(Type:=msoControlButton, Before:=1, Temporary:=True)
End With
With control
.Caption = Caption
.onAction = ThisWorkbook.name & "!myControlsMacro(" & Chr(34) & "InsertTest" & Chr(34) & "," & Chr(34) & "Insert" & Chr(34) & ")"
.Tag = Application.ThisWorkbook.name & "_clsMyControls"
.beginGroup = True
End With
myItems.Add control, Caption 'storing the newly created Control in a collection
myObjects.Add ObjectReference, ObjectReference.name 'storing the object in a collection to later call it
End Sub
Next, the Class clsInsertTest that uses myControls to create a custom control and owns the Insert method that is called.
In addition it creates an own custom control that bypasses the Object MyControls
Option Explicit
Dim control As Object 'the object to carry the Control that is created in this class
'____________________________
Property Get name() As String
name = "InsertTest"
End Property
'____________________________
Private Sub Class_Initialize()
'asking myControls to create a control, passing Reference THIS object, The method to be called, the name of teh control
MyControls.CreateEntry Me, "Insert", "InsertTest"
createOwnControl 'the simple way
End Sub
'____________________________
Public Sub Insert()
Debug.Print "Insert Called at " & Selection.Address
Selection.Value = "clsInsertTest was here"
Selection.EntireRow.Insert , CopyOrigin:=xlFormatFromLeftOrAbove
End Sub
'____________________________
Private Sub createOwnControl()
With Application.CommandBars("Cell").Controls
Set control = .Add(Type:=msoControlButton, Before:=1, Temporary:=True)
End With
With control
.Caption = "InsertTest2"
.onAction = ThisWorkbook.name & "!InserTest2Macro"
.Tag = Application.ThisWorkbook.name & "_clsMyControls"
.beginGroup = True
End With
End Sub
and last, the module containing initialization, cleanup and the Subs that are called by the custom controls and foreward to the objects
MyControlsMacro is called by the control built by MyControls
InsertTest2Macro is called by the control built by the InserTest Object directly
Option Base 1
Public InsertTest As clsInsertTest 'test object
Public MyControls As clsMyControls 'the Object to handle my controls
'____________________________
Sub CleanUp() 'what it says
Set InsertTest = Nothing
Set MyControls = Nothing
Dim control As Object
For Each control In Application.CommandBars("Cell").Controls
If control.Tag = Application.ThisWorkbook.name & "_clsMyControls" Then
control.Delete
End If
Next control
'Application.CommandBars("Cell").Reset 'just in case...
End Sub
'____________________________
Sub CreateTestObject() 'create my objects (calld at wb open)
Set MyControls = New clsMyControls
Set InsertTest = New clsInsertTest
End Sub
'____________________________
Public Sub myControlsMacro(ParamArray args() As Variant) 'the Sub to foreward the commands to my Controls handler
Dim handover() As String
Dim wert As Variant
Debug.Print "myControlsMacro called at " & Selection.Address
'Selection.EntireRow.Insert , CopyOrigin:=xlFormatFromLeftOrAbove 'does not work
'transforming the ParamArray "args" into an Array of strings to be able to pass it to the next method
For Each wert In args
If Not (Not handover) Then
ReDim Preserve handover(UBound(handover) + 1)
Else
ReDim handover(1)
End If
handover(UBound(handover)) = wert
Next
'calling the object to handle my Controls
MyControls.ReturnFromMacro handover
End Sub
'____________________________
Public Sub InserTest2Macro() ' the simple way
Debug.Print "InserTest2Macro called at " & Selection.Address
CallByName InsertTest, "Insert", VbMethod
End Sub

I have found a solution here:
Excel VBA CommandBar.OnAction with params is difficult / does not perform as expected
The single quotes in the .onAction and removing brackets made it work.
.onAction = ThisWorkbook.name & "!'myControlsMacro " & Chr(34) & "InsertTest" &_
Chr(34) & "," & Chr(34) & "Insert" & Chr(34) & "'"

Related

Vba, Programatically assign a macro to a "Shape" inside shapegroup

Thanks in advance, not sure why this wouldn't work.
I want to assign a macro to each button inside a shape group on load.
Inside Module:
Private Const SideNavName As String = "SideNav"
Public Sub SetSideNavigationOnAllSheets()
Dim ws As Worksheet
Dim oShape As Shape
For Each ws In ActiveWorkbook.Sheets
'check to see if sidenav shape/group exists in sheet
If Common.ShapeExists(ws, SideNavName) Then
' get side nav
For Each oShape In ws.Shapes(SideNavName).GroupItems
' only need the nav buttons not container
If Left(oShape.Name, 3) = "Nav" Then
Debug.Print ws.Name, oShape.Name
oShape.TextFrame.Characters.Text = "btn 1" ' pull from DB
oShape.OnAction = "'" & ActiveWorkbook.Name & "'!FolderSelectorButton" ' ERRORS OUT HERE
End If
'
Next
End If
Next
End Sub
Public Sub FolderSelectorButton()
Debug.Print 1
End Sub
Seems VBA doesn't like setting the OnAction property for Shapes that have been grouped. Solution is to store details of the group, ungroup it, update the OnAction property then re-create the group.
Replace your two lines setting the TextFrame and OnAction of the oShape object with the following:
' save then ungroup the Shapes
Dim oShpGrp As Shape, sShapeNames() As String, i As Long
Set oShpGrp = ws.Shapes(SideNavName)
ReDim sShapeNames(1 To oShpGrp.GroupItems.Count)
For i = 1 To oShpGrp.GroupItems.Count
sShapeNames(i) = oShpGrp.GroupItems.Item(i).Name
Next i
oShpGrp.Ungroup
' update Shape
oShape.TextFrame.Characters.Text = "btn 1" ' pull from DB
oShape.OnAction = "'" & ActiveWorkbook.Name & "'!FolderSelectorButton" ' ERRORS OUT HERE
' re-group the Shapes
Set oShpGrp = oShpGrp.Parent.Shapes.Range(sShapeNames).Group
oShpGrp.Name = SideNavName
This assumes that the group is a single-level group (ie it is not a group embedded within another group)

click event not working on programmatically / dynamically created optionbutton

I have the following code that programmatically / dynamically creates a frame and adds an option button:
Private Sub ComboBox1_Change()
Dim cb1234Frame As MsForms.Frame
Dim opbtn1 As MsForms.OptionButton
Set cb1234Frame = RT_Graph_Form.Controls.Add("Forms.Frame.1")
With cb1234Frame
.Top = 132
.Left = 12
.Height = 30
.Width = 144
.Caption = "Number of Graphs to Display"
End With
Set opbtn1 = cb1234Frame.Controls.Add("Forms.OptionButton.1")
With opbtn1
.Top = 6
.Left = 6
.Height = 18
.Width = 21.75
.Caption = "1"
End With
End Sub
But then this does not work:
Private Sub opbtn1_Click()
MsgBox "Test Successful!!"
End Sub
The problem is that event handlers need to be bound at compile-time: you cannot create an event handler for a dynamically created control.
Add a new class module to your project, call it DynamicOptionButton. The role of this class is to wrap the MSForms control and have a compile-time reference to it:
Option Explicit
Private WithEvents Wrapper As MSForms.OptionButton
Public Sub Initialize(ByVal ctrl As MSForms.OptionButton)
Set Wrapper = ctrl
End Sub
Private Sub Wrapper_Click()
MsgBox "Works!"
End Sub
Note that only a subset of the events will be available to handle: what events are available, depend on the interface you're declaring the wrapper reference with - MSForms.Control has a number of events (and properties), MSForms.OptionButton has another set: you may need to declare both interfaces (i.e. 2 wrappers for the same object) in order to access all the members.
Now in your form's declarations section, you'll need to hold a reference to all wrappers, otherwise the objects just fall out of scope and the handlers won't work. A Collection can do that:
Option Explicit
Private ControlWrappers As Collection
Private Sub UserForm_Initialize()
Set ControlWrappers = New Collection
End Sub
'...
Private Sub CreateOptionButton()
Dim ctrl As MSForms.OptionButton
Set ctrl = Me.Controls.Add("Forms.OptionButton.1")
'set properties...
Dim wrap As DynamicOptionButton
Set wrap = New DynamicOptionButton
wrap.Initialize ctrl
ControlWrappers.Add wrap
End Sub
Be careful to never reference the form's class name in the form's own code-behind: the global-scope RT_Graph_Form identifier refers to a VBA-controlled "default instance" auto-instantiated object that may or may not be the actual form instance that's being shown. You want to add your dynamic controls to Me.Controls, not RT_Graph_Form.Controls.
Now, we can handle events of controls spawned at run-time, but there's another problem: the event handler in the DynamicOptionButton class has no reference to the form it's on!
Or does it?
Every MSForms control has a Parent property; you can get ahold of the parent UserForm by recursively going up the Parent property until the returned reference is a UserForm - and from there you can access everything that's publicly exposed.
I'm not sure it's appropriate, but I managed to do-ish it.
I'm creating the userform from thisworkbook direcly so everything is stored there. I did not need the parent property anywhere.
Option Explicit
Const FolderPath As String = "C:"
Public TESTS As New Collection
Public CONTROLWRAPPERS As New Collection
Sub gotothere()
On Error GoTo bleh
Call Shell("explorer.exe" & " " & FolderPath & "\" & ThisWorkbook.ActiveSheet.Range("C14").Value, vbNormalFocus)
Exit Sub
bleh: Call Shell("explorer.exe" & " " & FolderPath, vbNormalFocus)
End Sub
Sub ChooseFolder()
Call Createform
End Sub
Private Sub Createform()
Set TESTS = Nothing
Call listalltests
Call Module1.MakeUserForm
Dim i As Integer
For i = 1 To TESTS.Count
Call CreateCommandbuttonButton(i)
Next i
Formol.Show vbModeless
End Sub
Private Sub listalltests()
Dim objFSO As Object
Dim objFolder As Object
Dim objSubFolder As Object
Dim i As Integer
Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFolder = objFSO.GetFolder(FolderPath & "\")
i = 1
For Each objSubFolder In objFolder.subfolders
TESTS.Add objSubFolder.Name
i = i + 1
Next objSubFolder
End Sub
Private Sub CreateCommandbuttonButton(pos As Integer)
Dim ctrl As MSForms.CommandButton
Set ctrl = Formol.Controls.Add("Forms.commandbutton.1")
With ctrl
.Caption = TESTS(pos)
If (pos * 20 + 2) > 600 Then
.Left = 130
.Top = (pos - 29) * 26 + 2
.Width = 102
Else
.Left = 12
.Top = pos * 26 + 2
.Width = 102
End If
End With
Dim wrap As DynamicOptionButton
Set wrap = New DynamicOptionButton
wrap.Initialize ctrl
CONTROLWRAPPERS.Add wrap
End Sub
The MakeUserForm function is stored in a module and just check if there is a form named formol and if not create it with a certain width & height. it's an empty form.
The class is the exact same as the one made by mathieu except for the Wrapper_click event.

Glitch when using RefEdit_Change Event in a VBA UserForm

The following should happen:
1. UserForm with 2 RefEdit controls is shown
2. The first RefEdit is used to select a range
3. The RefEdit_Change event adjusts the second RefEdit control to .offset(0,1) of the range
Here my code until now:
Module1:
Dim frmSelectXY As New frmSelectImportData
With frmSelectXY
.Show
.DoStuffWithTheSelectedRanges
End With
UserForm: frmSelectImportData
Option Explicit
Private Type TView
IsCancelled As Boolean
xrng As Range
yrng As Range
End Type
Private this As TView
Public Property Get IsCancelled() As Boolean
IsCancelled = this.IsCancelled
End Property
Public Property Get yrng() As Range
Set yrng = this.yrng
End Property
Public Property Get xrng() As Range
Set xrng = this.xrng
End Property
'Here is where the fun happens
Private Sub RefEdit1_Change()
'RefEdit2.Value = RefEdit1.Value
If InStr(1, RefEdit1.Value, "[") <> 0 And InStr(1, RefEdit1.Value, "!") <> 0 Then
RefEdit2.Value = Range(RefEdit1.Value).offset(0, 1).Address(External:=True)
ElseIf InStr(1, RefEdit1.Value, "!") <> 0 Then
RefEdit2.Value = Range(RefEdit1.Value).offset(0, 1).Parent.Name & "!" & Range(RefEdit1.Value).offset(0, 1).Address(External:=False)
Else
RefEdit2.Value = Range(RefEdit1.Value).offset(0, 1).Address(External:=False)
End If
End Sub
Private Sub SaveBTN_Click()
Set this.xrng = Range(RefEdit1.Value)
Set this.yrng = Range(RefEdit2.Value)
If Not validate Then
MsgBox "x-values and y-values need to have the same size."
Else
Me.Hide
End If
End Sub
Function validate() As Boolean
validate = False
If this.xrng.count = this.yrng.count Then validate = True
End Function
RefEdit1_Change should adjust the value of RefEdit2 such that it will show the reference to the column just next to it or better .offest(0,1) to it.
But that isn't what happens.. the value doesn't get changed. As soon as the User clicks into RefEdit2 if RefEdit1 has already been changed, the program aborts without error message. If you Cancle the UserForm I have also experienced hard crashes of excel. I have temporarily fixed the problem by rebuilding the UserForm from scratch and renaming the RefEdits. But at some point it reapeared. It seems as if it is an Excel/VBA inherent problem.
Does anybody know how to fix this?
Ugly hacks and workarounds are welcome, anything is better than, abort without error message.
you need to enclose Range(RefEdit1.Value).offset(0, 1).Parent.Name in ' so
="'" & Range(RefEdit1.Value).offset(0, 1).Parent.Name & "'!"

VBA Combobox / automatically generate code

I've got a question concerning combobox in Excel.
I've got an excel sheet that by default contains two comboboxes and their number is described by a variable x (x=2 by default). Each combobox is scripted to behave in a particular way in subs, for example I've got: private sub ComboBox1_DropButtonClick().
Nonetheless, sometimes I need to increase the number of these boxes by changing the value of X. I may need up to 10 comboboxes in total. Now the question is whether there's any way in which I can set the behaviour of an infinite number of comboboxes (for example in the event of DropButtonClick). What I did was to write a code for each of those comboboxes, so I've got a sub for ComboBox1_DropButtonClick(), ComboBox2_DropButtonClick(), ComboBox3_DropButtonClick(), etc.. The code varies a bit, but it's repeatable. So it all looks rather dumb and I'm searching for some more ingenious solution. Maybe all those comboboxes can be scripted in one go? If there's any way to do it, please share it with me.
Thanks, Wojciech.
[edit] Location of my code (marked in grey):
Screenshot from VBA editor in VBA
Here is some code to dynamically add controls to an Excel Userform, and add the code behind. The code added will make it display a MessageBox when the ComboBox receives a KeyDown.
The code is somewhat commented, but let me know if you have questions :)
Option Explicit
Sub CreateFormComboBoxes(NumberOfComboBoxes As Long)
Dim frm As Object
Dim ComboBox As Object
Dim Code As String
Dim i As Long
'Make a blank form called 'UserForm1', or any name you want
'make sure it has no controls or any code in it
Set frm = ThisWorkbook.VBProject.VBComponents("UserForm1")
With frm
For i = 1 To NumberOfComboBoxes
Set ComboBox = .designer.Controls.Add("Forms.ComboBox.1")
'Set the properties of the new controls
With ComboBox
.Width = 100
.Height = 20
.Top = 20 + ((i - 1) * 40) 'Move the control down
.Left = 20
.Visible = True
.ZOrder (1)
.Name = "ComboBox" & i
End With
'Add your code for each module, you can add different code, by adding a if statement here
'And write the code depending on the name, index, or something else
Code = Code & vbNewLine & "Private Sub " & "ComboBox" & i & "_KeyDown(ByVal KeyCode As MSForms.ReturnInteger, ByVal Shift As Integer)" & _
vbNewLine & " MsgBox(""hi"")" & vbNewLine & "End Sub"
Next
'Add the code
.CodeModule.InsertLines 2, Code
End With
End Sub
'Run this
Sub Example()
CreateFormComboBoxes 5
End Sub
**Edit**
I figured I might as well add the other approach for adding controls dynamically to an Excel sheet. I'd recommend sticking to UserForms, but, here's a method that should help out when controls are needed in a Sheet.
Sub addCombosToExcelSheet(MySheet As Worksheet, NumberOfComboBoxes As Long, StringRangeForDropDown As String)
Dim i As Long
Dim combo As Shape
Dim yPosition As Long
Dim Module As Object
yPosition = 20
For i = 1 To NumberOfComboBoxes
yPosition = (i - 1) * 50
'Create the shape
Set combo = MySheet.Shapes.AddFormControl(xlDropDown, 20, yPosition, 100, 20)
' Range where the values are stored for the dropDown
combo.ControlFormat.ListFillRange = StringRangeForDropDown
combo.Name = "Combo" & i
Code = "Sub Combo" & i & "_Change()" & vbNewLine & _
" MsgBox(""hi"")" & vbNewLine & _
"End Sub"
'Add the code
With ThisWorkbook
'Make sure Module2 Exits and there is no other code present in it
Set Module = .VBProject.VBComponents("Module2").CodeModule
Module.AddFromString (Code)
End With
'Associate the control with the action, don't include the () at the end!
combo.OnAction = "'" & ActiveWorkbook.Name & "'!Combo" & i & "_Change"
Next
End Sub
Sub Example()
Dim sht As Worksheet: Set sht = ThisWorkbook.Sheets(1)
addCombosToExcelSheet sht, 10, "Sheet1!$A$1:$A$10"
End Sub

error to fetch array from dictionary list in vba

in my project in vba i have 2 combobox like this:
combobox1=zahedan,zabol
combobox2=621,54130
and 2 arrays which i declare to save list of subitems zahedan and zabol. i use a dictionary to fetch selected array when combobox1 change like this:
Option Explicit
Option Base 1
Dim dicArrays As Scripting.Dictionary
Dim hoze621code
Dim hoze621name
Dim hoze54130code
Dim hoze54130name
when form active program do somethings like below:
Private Sub Worksheet_Activate()
hoze621code = Array(54101, 54102)
hoze621name = array("test1", "test2")
hoze54130code = Array(5421, 5422)
hoze54130name = array("test3", "test4")
Set dicArrays = New Scripting.Dictionary
dicArrays.Add "hoze621name", hoze621name
dicArrays.Add "hoze621code", hoze621code
dicArrays.Add "hoze54130name", hoze54130name
dicArrays.Add "hoze54130code", hoze54130code
now when i use combobox1.change to fetch list of selected area it show me runtimes error 91 or 13? this is combobox1.change code:
Private Sub ComboBox1_Change()
Dim arrayname2(), arraycode2() As String
arrayname2 = dicArrays("hoze" & ComboBox2.List(ComboBox1.ListIndex) & "name")
arraycode2 = dicArrays("hoze" & ComboBox2.List(ComboBox1.ListIndex) & "code")
// do somthings with selected array
End Sub
i think it's ok but not working! any body to help me?
edited: to add some info about variables scoping
not so clear how the code you showed is spread over your project modules
for me to have something that compiled I had to declare as Public thse variables to be shared between UserForm code and module and/or worksheet code, like follows
'code where Private Sub Worksheet_Activate() is placed
Option Explicit
Public hoze621code
Public hoze621name
Public hoze54130code
Public hoze54130name
Public dicArrays As Scripting.Dictionary
next, for sure you have to declare both arrayname2() and arraycode2() variables as of Variant type
Private Sub ComboBox1_Change()
Dim arrayname2() As Variant, arraycode2() As Variant
arrayname2 = dicArrays("hoze" & ComboBox2.List(ComboBox1.ListIndex) & "name")
arraycode2 = dicArrays("hoze" & ComboBox2.List(ComboBox1.ListIndex) & "code")
'do somthings with selected array
' like:
Dim i As Long
MsgBox "arrayname2 has " & UBound(arrayname2) + 1 & " elements"
For i = 0 To UBound(arrayname2)
MsgBox vbTab & "element " & i & ": " & arrayname2(i)
Next i
End Sub

Resources