I'm trying to obtain the value for Teller from an userform in my module. Here's my userform code:
Private Sub UserForm_Initialize()
Dim LastRow As Long
Dim i As Long
Dim Teller As Long
Dim chkBox As MSForms.CheckBox
Teller = 1
LastRow = Worksheets("Sheet").Cells(Rows.Count, 1).End(xlUp).Row
For i = 1 To LastRow
If Worksheets("Sheet").Cells(i, 1).Value = Worksheets("Sheet").Range("S1").Value Then
Set chkBox = Me.Controls.Add("Forms.CheckBox.1", "CheckBox_" & Teller)
chkBox.Caption = Worksheets("Sheet").Cells(i, 9).Value
chkBox.Left = 5
chkBox.Top = 25 + ((Teller - 1) * 20)
Teller = Teller + 1
End If
Next i
End Sub
And my module code:
Dim p As Long
Dim x As String
With UserForm
.Show
For p = 1 To .Teller
x= .Controls("CheckBox_" & p).Caption
MsgBox (x)
Next p
End
End With
UserForm.Teller won't give me the value of Teller. How do I get this?
Teller isn't an object or method of the userform, it's a variable you have defined in the userform_initialize module.
What you could do is declare the variable as a public variable in your module and then call it in your userform. You do this by declaring the variable above your subroutine in the module as public as below.
Public Teller As Long
Then you would just use
For p = 1 to teller
in your module
You would need to reset the variable to 0 manually at the end of the code/ when the userform is closed if you want it to reset on each run
Thanks
Related
I have created an UserForm in Excel. The UserForm has a ListBox and a CheckBox added to it.
I have written VBA code to populate the ListBox with data in the 1st column of the UserForm_Data worksheet. I am attempting to add a Select All CheckBox to the UserForm. When I click on the CheckBox once, the check mark does not appear but the If Me.CheckBox.Value = True section of the Checkbox1_Change event is executed and all the items in the ListBox are selected. The check mark appears only when I click the CheckBox the second time. The Excel VBA code and an image of the UserForm are attached.
Option Explicit
Private Sub ListBox1_Change()
Dim i As Long
If CheckBox1.Value = True Then
For i = 0 To Me.ListBox1.ListCount - 1
If Me.ListBox1.Selected(i) = False Then
Me.CheckBox1.Value = False
End If
Next i
End If
End Sub
Private Sub CheckBox1_Change()
Dim i As Long
If Me.CheckBox1.Value = True Then
With Me.ListBox1
For i = 0 To .ListCount - 1
.Selected(i) = True
Next i
End With
Else
i = 0
End If
End Sub
Private Sub UserForm_Initialize()
Dim rng1 As Range
Dim ws1 As Worksheet
Dim i, lastRow As Long
Dim list1 As Object
Dim string1 As String
Dim array1 As Variant
Set list1 = CreateObject("System.Collections.ArrayList")
Set ws1 = ThisWorkbook.Worksheets("UserForm_data")
lastRow = ws1.UsedRange.Rows.Count
Me.ListBox1.Clear
For i = 2 To lastRow
string1 = CStr(ws1.Cells(i, 1).Value)
If Not list1.Contains(string1) Then
list1.Add string1
End If
Next i
array1 = list1.ToArray
Me.Caption = "UserForm1"
Me.ListBox1.list = array1
Me.ListBox1.MultiSelect = 1
Me.CheckBox1.Value = False
End Sub
There are two steps you can take to address this:
There's a chance that simply adding a DoEvents at the end of the CheckBox1_Change event will force the redraw.
If that doesn't work, add the following line just above the DoEvents and test it again... this encourages a screen update...
Application.WindowState = Application.WindowState
One approach is to use global flags to toggle on and off the control event handlers. Here is what the updated events would look like:
Option Explicit
Private Sub ListBox1_Change()
Dim i As Long
If Not AllowListBoxEvents Then Exit Sub
AllowCheckBoxEvents = False
If CheckBox1.Value = True Then
For i = 0 To ListBox1.ListCount - 1
If ListBox1.Selected(i) = False Then CheckBox1.Value = False
Next i
End If
AllowCheckBoxEvents = True
End Sub
Private Sub CheckBox1_Change()
Dim i As Long
If Not AllowCheckBoxEvents Then Exit Sub
AllowListBoxEvents = False
If CheckBox1.Value = True Then
For i = 0 To ListBox1.ListCount - 1
ListBox1.Selected(i) = True
Next i
End If
AllowListBoxEvents = True
End Sub
Make sure you set the "Allow" variables to True in the Initialize event.
I have developed the following two subs which create and remove a collection of checkboxes next to a listobject. Each distinct ID in the listobject gets a checkbox. Like this I can approve the listobject entries.
The code is the follwing:
Public CBcollection As Collection
Public CTRLcollection As Collection
Sub create_chbx()
If Approval.CBcollection Is Nothing Then
Dim i As Integer
Dim tbl As ListObject
Dim CTRL As Excel.OLEObject
Dim CB As MSForms.CheckBox
Dim sht As Worksheet
Dim L As Double, T As Double, H As Double, W As Double
Dim rng As Range
Dim ID As Long, oldID As Long
Set CBcollection = New Collection
Set CTRLcollection = New Collection
Set sht = ActiveSheet
Set tbl = sht.ListObjects("ApprovalTBL")
Set rng = tbl.Range(2, 1).Offset(0, -1)
W = 10
H = 10
L = rng.Left + rng.Width / 2 - W / 2
T = rng.Top + rng.Height / 2 - H / 2
For i = 1 To tbl.ListRows.count
ID = tbl.Range(i + 1, 1).Value
If Not (ID = oldID) Then
Set CTRL = sht.OLEObjects.Add(ClassType:="Forms.CheckBox.1", Link:=False, DisplayAsIcon:=False, Left:=L, Top:=T, Width:=W, Height:=H)
Set CB = CTRL.Object
CBcollection.Add Item:=CB
CTRLcollection.Add Item:=CTRL
End If
Set rng = rng.Offset(1, 0)
T = rng.Top + rng.Height / 2 - H / 2
oldID = ID
Next i
End If
End Sub
Sub remove_chbx()
If Not Approval.CBcollection Is Nothing Then
With Approval.CBcollection ' Approval is the module name
While .count > 0
.Remove (.count)
Wend
End With
With Approval.CTRLcollection
While .count > 0
.Item(.count).Delete
.Remove (.count)
Wend
End With
Set Approval.CBcollection = Nothing
Set Approval.CTRLcollection = Nothing
End If
End Sub
This all works pretty well. No double checkboxes and no errors if there are no checkboxes. I am developing an approval scheme were I need to develop and test other modules. If I now run this sub:
Sub IdoStupidStuff()
Dim i As Integer
Dim Im As Image
i = 1
Set Im = i
End Sub
It will give me an error. If I then try to run one of my checkbox subs they will not work properly anymore. The collection is deleted by the error and I am no longer able to access the collections. Why does this happen and am I able to counter act this other then just not causing errors? Is there a better way to implement such a system were loss of collections is not an issue?
You could wrap the Collection object in a Property and let it handle the object creation:
Private mCollection As Collection
Public Property Get TheCollection() As Collection
If mCollection Is Nothing Then Set mCollection = New Collection
Set TheCollection = mCollection
End Property
To call it:
TheCollection.Count
Try On Error Resume Next before the line that causes the error. It will skip the problem and your vairables will still be available.
However this will not solve your error. Try to make a seperate hidden sheet in your workbook to store your global variables so they won't go missing.
f.ex.:
Private Sub CreateSheet()
Dim ws As Worksheet
With ThisWorkbook
Set ws = .Sheets.Add(After:=.Sheets(.Sheets.Count))
ws.Name = "Global"
.Worksheets("Global").Visible = False
End With
End Sub
I think this is less complicated than I am making it but being a novice with VBA I haven't been able to find the answer after a couple days of googling and playing with different code.
I have a macro that:
Opens a file in whatever folder path is named
Searches for specific text in the file to find the start of a specific section, i.e. A100
Finds the end of this section, i.e. A110 (variable in length depending on the file)
Copy and pastes the cells in this range to another specific range, i.e. O1:O10
Populates a userform with a checkbox for each cell in this new variable length range
I now need the user to click which checkboxes they want and then the captions for these checkboxes to be saved as an array that I can then call on later in the macro.
i.e. if they clicked dog, cat, and bird from the checkboxes, the output would be dog,cat,bird
Because of the variable length of the range and number of checkboxes, I can't figure out how to have it loop through each one and concatenate the correct values.
I think there is probably a way to cut out the copy pasting of the values to populate the userform with also, but this was the only way I could figure out that part given the variable length of the range.
Below is the code that generates the userform after the range has been copy pasted.
Private Sub UserForm_Initialize()
Dim curColumn As Long
Dim i As Long
Dim codeRow As Long
Dim chkBox As msforms.CheckBox
curColumn = 15
codeRow = Range("O20").End(xlUp).Row
For i = 1 To codeRow
Set chkBox = Me.Controls.Add("Forms.CheckBox.1", "CheckBox_" & i)
chkBox.Caption = Worksheets(1).Cells(i, curColumn).Value
chkBox.Left = 5
chkBox.Top = 5 + ((i - 1) * 20)
Next i
End Sub
Insert moduel code.
Public vCheck()
Bellows in form code.
Private Sub UserForm_Initialize()
Dim curColumn As Long
Dim i As Long
Dim codeRow As Long
Dim chkBox As msforms.CheckBox
curColumn = 15
codeRow = Range("O20").End(xlUp).Row
ReDim vCheck(0)
For i = 1 To codeRow
Set chkBox = Me.Controls.Add("Forms.CheckBox.1", "CheckBox_" & i)
ReDim Preserve vCheck(1 to i)
vCheck(i) = Worksheets(1).Cells(i, curColumn).Value
chkBox.Caption = vCheck(i)
chkBox.Left = 5
chkBox.Top = 5 + ((i - 1) * 20)
Next i
End Sub
Listbox was the way to go. Here is the updated code for others looking for help with the same issue:
Private Sub UserForm_Initialize()
Dim myLBox As msforms.ListBox
Dim codeRow As Long
codeRow = Range("O20").End(xlUp).Row
ListBox1.RowSource = "O1:O" & codeRow
End Sub
Private Sub CommandButton1_Click()
Dim rRange As Range
Dim lCount As Long 'Counter
On Error GoTo ErrorHandle
Set rRange = Range("P1")
With ListBox1
For lCount = 0 To .ListCount - 1
If .Selected(lCount) = True Then
rRange.Offset(lCount, 0).Value = .List(lCount)
End If
Next
End With
BeforeExit:
Set rRange = Nothing
Unload Me
Exit Sub
ErrorHandle:
MsgBox Err.Description
Resume BeforeExit
End Sub
I am now trying to achieve something like the query function in Google Sheets. Obviously in this GIF, someone has already done that. I wonder how they could do that in Excel / VBA.
My specific question is: in VBA, how to fill other cells' formulas by entering a formula in a specific cell? (replicate the function used in this GIF and not using VBA + advanced filter)
Enter a formula in cell A3
Press CTRL + SHIFT + ENTER
Receive results
This is what I got so far:
The code in a standard module:
Sub run_sql_sub(sql)
On Error Resume Next
Set cn = CreateObject("ADODB.Connection")
Set rs = CreateObject("ADODB.Recordset")
With cn
.ConnectionString = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" & _
This Workbook.FullName _
& ";Extended Properties=""Excel 12.0 Xml;HDR=YES;IMEX=1"";"
.Open
End With
rs.Open sql, cn
Application.ScreenUpdating = False
ActiveSheet.Range("A1:XFD1048576").ClearContents
For intColIndex = 0 To rs.Fields.Count - 1
Range("A1").Offset(0, intColIndex).Value = rs.Fields(intColIndex).Name
Next
Range("A2").CopyFromRecordset rs
Application.ScreenUpdating = True
rs.Close: cn.Close: Set rs = Nothing: Set cn = Nothing
End Sub
And this code is in activesheet's module:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim KeyCells As Range
Set KeyCells = ActiveSheet.Range("A1")
If Not Application.Intersect(KeyCells, Range(Target.Address)) _
Is Nothing Then
If InStr(KeyCells.Value2, "mi_sql") > 0 Then
sql = Right(KeyCells.Value2, Len(KeyCells.Value2) - Len("mi_sql "))
run_sql_sub sql
End If
End If
End Sub
Update 08.04.2019: found a solution
' Code in standard Module
Public collectCal As Collection
Private ccal As CallerCal
Sub subResizeKQ(caller As CallerInfo)
On Error Resume Next
Application.EnableEvents = False
If caller.Id <> "" Then
Application.Range(caller.Id).ClearContents
Application.Range(caller.Id).Resize(caller.rows, caller.cols).FormulaArray = caller.FomulaText
End If
Application.EnableEvents = True
End Sub
Function ResizeKQ(value As Variant) As Variant
If ccal Is Nothing Then Set ccal = New CallerCal
If collectCal Is Nothing Then Set collectCal = New Collection
Dim caller As New CallerInfo
Dim rows As Long, cols As Long
Dim arr As Variant
arr = value
rows = UBound(arr, 1) - LBound(arr, 1) + 1
cols = UBound(arr, 2) - LBound(arr, 2) + 1
Dim rgcaller As Range
Set rgcaller = Application.caller
caller.Id = rgcaller.Address(True, True, xlA1, True, True)
caller.rows = rgcaller.rows.Count
caller.cols = rgcaller.Columns.Count
caller.FomulaText = rgcaller.Resize(1, 1).Formula
If caller.rows <> rows Or caller.cols <> cols Then
caller.rows = rows
caller.cols = cols
collectCal.Add caller, caller.Id
End If
ResizeKQ = arr
End Function
Function fRandArray(numRow As Long, numCol As Long) As Variant
Application.Volatile True
ReDim arr(1 To numRow, 1 To numCol)
For i = 1 To numRow
For j = 1 To numCol
arr(i, j) = Rnd
Next
Next
fRandArray = ResizeKQ(arr)
End Function
'--------------------------------------------------------------------------
' code in Class Module name CallerCal
Private WithEvents AppEx As Application
Private Sub AppEx_SheetCalculate(ByVal Sh As Object)
Dim caller As CallerInfo
If collectCal Is Nothing Then Exit Sub
For Each caller In collectCal
subResizeKQ caller
collectCal.Remove caller.Id
Set caller = Nothing
Next
Set collectCal = Nothing
End Sub
Private Sub Class_Initialize()
Set AppEx = Application
End Sub
Private Sub Class_Terminate()
Set AppEx = Nothing
End Sub
'--------------------------------------------------------------------------
' code in Class Module name CallerInfo
Public rows As Long
Public cols As Long
Public Id As String
Public FomulaText As String
To test it, go to Excel Sheet, enter the following test formula in A1:
=fRandArray(10,10)
P.S: If anyone is using Excel 365 Insider Program, Microsoft has published this kind of formula called Dynamic Array Function:
https://support.office.com/en-ie/article/dynamic-arrays-and-spilled-array-behavior-205c6b06-03ba-4151-89a1-87a7eb36e531
I agree with the other comments - MS doesn't seem to provide a way to do this natively, and any way of doing it directly would probably involve some Excel-breaking memory manipulation.
However...
I suggest taking your method one step further and generalizing it
Copy and paste this class into a text file, then import it into VBA (which allows Attribute VB_PreDeclaredID = True and Attribute Item.VB_UserMemId = 0):
RangeEdit
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "RangeEdit"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = False
Private colRanges As Collection
Private colValues As Collection
Private Sub Class_Initialize()
Set colRanges = New Collection
Set colValues = New Collection
End Sub
Public Property Let Item(rng_or_address As Variant, value As Variant)
Attribute Item.VB_UserMemId = 0
colRanges.Add rng_or_address
colValues.Add value
End Property
Public Sub flush(sh As Worksheet)
Application.EnableEvents = False
While colRanges.Count > 0
If TypeName(colRanges(1)) = "Range" Then
colRanges(1).value = colValues(1)
ElseIf TypeName(colRanges(1)) = "String" Then
sh.Range(colRanges(1)).value = colValues(1)
End If
colRanges.Remove 1
colValues.Remove 1
Wend
Application.EnableEvents = True
End Sub
Make your Workbook_SheetChange method the following:
Private Sub Workbook_SheetChange(ByVal sh As Object, ByVal Target As Range)
RangeEdit.flush sh
End Sub
Now you can create a UDF that modifies other cells. The way it works is it queues up all the modifications you make and only runs them after the cell loses focus. The syntax allows you to treat it almost like the regular Range function. You can run it either with an address string or with an actual range (though you might want to add an error if it's not either one of those).
Here is a quick example UDF that can be run from an Excel cell formula:
Public Function MyUDF()
RangeEdit("A1") = 4
RangeEdit("B1") = 6
RangeEdit("C4") = "Hello everyone!"
Dim r As Range
Set r = Range("B12")
RangeEdit(r) = "This is a test of using a range variable"
End Function
For your specific case, I would replace
For intColIndex = 0 To rs.Fields.Count - 1
Range("A1").Offset(0, intColIndex).Value = rs.Fields(intColIndex).Name
Next
with
For intColIndex = 0 To rs.Fields.Count - 1
RangeEdit(Range("A1").Offset(0, intColIndex)) = rs.Fields(intColIndex).Name
Next
And to copy the recordset I would define the following function (it assumes that the recordset cursor is set to the first record. if you Move it previously you might want to have rs.MoveFirst in there):
Public Sub FormulaSafeRecordsetCopy(rs As ADODB.Recordset, rng As Range)
Dim intColIndex As Long
Dim intRowIndex As Long
While Not rs.EOF
For intColIndex = 0 To rs.Fields.Count - 1
RangeEdit(rng.Offset(intRowIndex, intColIndex)) = rs.Fields(intColIndex).value
Next
rs.MoveNext
intRowIndex = intRowIndex + 1
Wend
End Sub
I have a vlookup userform which autofills the details in the form based on the seat n°.
Now I want to incoroporate a ref edit to paste these data from the text box to the cells the user chooses with the refedit. Hence i would need some help in going about these. This is the code i have used. I potentially want to insert 3 refedit boxes for user to select the cell they want to paste each of the data (Name,Dept and Ext No.) from the textbox.
See my code below:
Option Explicit
Private Sub Frame1_Click()
End Sub
Private Sub TextBox1_Exit(ByVal Cancel As MSForms.ReturnBoolean)
Dim answer As Integer
answer = TextBox1.Value
TextBox2.Value = WorksheetFunction.VLookup(answer, Sheets("L12 - Data Sheet").Range("B:E"), 2, False)
TextBox3.Value = WorksheetFunction.VLookup(answer, Sheets("L12 - Data Sheet").Range("B:E"), 3, False)
TextBox4.Value = WorksheetFunction.VLookup(answer, Sheets("L12 - Data Sheet").Range("B:E"), 4, False)
End Sub
Private Sub TextBox2_Change()
End Sub
Private Sub TextBox3_Change()
End Sub
Private Sub TextBox4_Change()
End Sub
Private Sub CancelButton_Click()
Unload Me
End
End Sub
I have tried figuring out a code to solve this issue but I am getting an object required error. My rngcopy would be textbox2.value (Name) and the rngpaste location would be the ref edit 1.
This is the code
Private Sub PasteButton_Click()
Dim rngCopy As Range, rngPaste As Range
Dim wsPaste As Range
Dim answer As Integer
answer = TextBox1.Value
If RefEdit1.Value <> "" Then
TextBox2.Value = WorksheetFunction.VLookup(answer, Sheets("L12 - Data Sheet").Range("B:E"), 2, False)
Set rngCopy = TextBox2.Value
Set wsPaste = ThisWorkbook.Sheets(Replace(Split(TextBox2.Value, "!")(0), "'", ""))
Set rngPaste = wsPaste.Range(Split(TextBox2.Value, "!")(1))
rngCopy.Copy rngPaste
Else
MsgBox "Please select an Output range"
End If
End Sub
You should get the row index with Match and expose it to the form so it can be used by the copy function.
And to set the target pointed by a Ref control, just evalute the .Value property with Range():
Range(RefEdit.Value).cells(1, 1) = Worksheet.Cells(row, column)
The form:
The code:
' constants to define the data
Const SHEET_DATA = "L12 - Data Sheet"
Const COLUMN_SEAT = "B"
Const COLUMNN_NAME = "C"
Const COLUMN_DEPT = "D"
Const COLUMN_EXTNO = "E"
Private Sheet As Worksheet
Private RowIndex As Long
Private Sub TxtSeatNo_Change()
Dim seatno
'clear the fields first
Me.TxtName.value = Empty
Me.TxtDept.value = Empty
Me.TxtExtNo.value = Empty
RowIndex = 0
If Len(TxtSeatNo.value) Then
Set Sheet = ThisWorkbook.Sheets(SHEET_DATA)
On Error Resume Next
' get the seat number to either string or double
seatno = TxtSeatNo.value
seatno = CDbl(seatno)
' get the row index containing the SeatNo
RowIndex = WorksheetFunction.match(seatno, _
Sheet.Columns(COLUMN_SEAT), _
0)
On Error GoTo 0
End If
If RowIndex Then
' copy the values from the sheet to the text boxes
Me.TxtName.value = Sheet.Cells(RowIndex, COLUMNN_NAME)
Me.TxtDept.value = Sheet.Cells(RowIndex, COLUMN_DEPT)
Me.TxtExtNo.value = Sheet.Cells(RowIndex, COLUMN_EXTNO)
End If
End Sub
Private Sub BtCopy_Click()
If RowIndex < 1 Then Exit Sub
' copy the current values to the cells pointed by the ref controls
If Len(Me.RefName.value) Then _
Range(Me.RefName.value) = Sheet.Cells(RowIndex, COLUMNN_NAME)
If Len(Me.RefDept.value) Then _
Range(Me.RefDept.value) = Sheet.Cells(RowIndex, COLUMN_DEPT)
If Len(Me.RefExtNo.value) Then _
Range(Me.RefExtNo.value) = Sheet.Cells(RowIndex, COLUMN_EXTNO)
End Sub
Private Sub BtlClose_Click()
' close the form
Unload Me
End Sub
You are declaring your rngCopy as a Range Object and then later on you are binding it to a .value method of the range object.
Set rngCopy = TextBox2.Value
This is likely where you are encountering errors. Try declaring a string and assigning it to your copy value.
Dim string1 As String
string1 = TextBox2.Value
Step through your code editor with the LOCALS window open, and watch what happens to your rngCopy object when you assign a string to it.