I am trying to develop a function in VBA that returns the result to the current worksheet. It is intended that the function opens up another spreadsheet, extracts some data, does some processing and returns a value to the worksheet that called the function.
Everything works well when I call the function from the "immediate" window in the VBA editor. However, when I transfer the call to a worksheet the function behavior deviates from the expected when an attempt is made to open the other workbook (AreaBook). The object, AreaBook, remains as a pointer to nothing.
I've tried hard coding the filename; again a call to the function works from the immediate window but not when called from a workbook.
Any ideas?
Public Function pointInWhichArea(FileName As String, SheetName As String, areaID As String, ByVal pointLong As Single, ByVal pointLat As Single) As Variant ', testPointLon As Single, testPointLat As Single) As Variant
Dim a, b, c As Integer
Dim colAreaID, colLat, colLon As Integer
Dim AreaBook As Workbook
Dim AreaSheet As Worksheet
Dim polygonPoints() As pointType
Dim testPoint As pointType
Dim found As Boolean
' extract the point details
testPoint.x = pointLong
testPoint.y = pointLat
' set the workbook and sheet objects
FileName = filePath + FileName ' open the Area definition file
Set AreaBook = Workbooks.Open(FileName) ' <<<< PROBLEM HERE
Set AreaSheet = AreaBook.Sheets(SheetName)
a = 1 ' identify the Polygon ID, latitude and longitude columns column
While AreaSheet.Cells(1, a).Value <> ""
Select Case Worksheets(SheetName).Cells(1, a).Value
Case Is = areaID
colAreaID = a
Case Is = "Latitude"
colLat = a
Case Is = "Longitude"
colLon = a
End Select
a = a + 1
Wend
a = 2 ' loop through all points in the area list
b = a ' remember the beginning of the polygon
found = False
While (AreaSheet.Cells(a, colAreaID).Value <> "" And found = False)
If AreaSheet.Cells(a, colAreaID).Value <> AreaSheet.Cells(a + 1, colAreaID).Value Then ' test for the end of this polygon
c = a ' remember the end of the polygon
ReDim polygonPoints(b To c) As pointType ' array to capture the poylgon
For a = b To c ' loop through each point
polygonPoints(a).x = AreaSheet.Cells(a, colLon).Value ' extract the longitude of the point
polygonPoints(a).y = AreaSheet.Cells(a, colLat).Value ' extract the latitude of the point
Next a
b = a ' remember the beginning of the next polygon
If pointInArea(testPoint, polygonPoints) = True Then ' test if the point is in the current polygon
pointInWhichArea = AreaSheet.Cells(a - 1, colAreaID).Value ' return the area label
found = True
End If
Else
a = a + 1
End If
Wend
AreaBook.Close
End Function
I'm afraid a worksheet function cannot be used to affect other cells or workbooks - you can't add a function to cell A1 and expect the result to appear in cell B2.
In the same way you can't add a function in cell A1 and expect it to open another workbook to get its answer.
That's why it works in the immediate window and not as an Excel function.
You may be able to define a link to the other workbook and then reference that, but you can't get the function to physically open the other workbook.
Worksheet Functions (and user defined variants of these) are limited in terms of what they can and cannot do, here is a small excerpt from a Microsoft article regarding the matter:
https://support.microsoft.com/en-us/kb/170787
A user-defined function called by a formula in a worksheet cell cannot
change the environment of Microsoft Excel. This means that such a
function cannot do any of the following:
Insert, delete, or format cells on the spreadsheet.
Change another cell's value.
Move, rename, delete, or add sheets to a workbook.
Change any of the environment options, such as calculation mode or screen views.
Add names to a workbook.
Set properties or execute most methods.
The page goes on to state that:
Any environmental changes should be made through the use of a Visual Basic subroutine.
So in short, you cannot do what you're attempting to do with a worksheet UDF (User-Defined Function) and will need to change it to a sub routine.
Related
So I have a form that executes a VBA script via a macro. The purpose of said script is to open Excel, create a new workbook, gather information from several tables and export them to a formatted spreadsheet. Each person has a sheet with their name, and the relevant data is printed in said sheet. It works perfectly for the most part. Only one problem... The table in Access where the name and demographics data is gathered from is formatted to sort by last name ascending alphabetically. The VBA script exports it in the order the names were entered. I want my VBA script to respect the formatting in the database table, and I would prefer not to have to add an alphabetizing subroutine to my VBA script.
Table A Format: ID, Active, Last, First, Role, Traveler, Resident, Preceptee, Phone, Completion
Table B Format: ID, Course, Course ID, Offered, HLC, Course Type
Last in Table A called "Roster" is the field with which I want my VBA script to sort alphabetically. The database is already configured to do this.
Thanks in advance!
VBA Code:
Option Compare Database
' This module exports the database to a spreadsheet with specific formatting when called from a Macro
' Each Employee will have a sheet named thier last name which will contain all HLC modules they have completed in a list
' It is specific to this Database, but can be adapted to others.
' Version 1.0 Stable
Public Function ExportXLS(TblA As String, TblB As String, Optional names As String, Optional specific As Boolean)
'****************'
'Set up variables'
'****************'
Dim ctrA As Integer
Dim ctrB As Integer
Dim var As Long
Dim str As String
Dim excel As Object 'Pointer to Excel Application
Dim book As Object 'Pointer to Excel Workbook
Dim sheet As Object 'Pointer to Excell Sheet
Dim Roster As DAO.Recordset
Dim Course As DAO.Recordset
Dim Child As DAO.Recordset
Dim last_name As DAO.Recordset 'Matrix of pointers that will hold parts of the tables to be printed to the corresponding Excel sheets
Dim course_name As DAO.Recordset 'Matrix of pointers that will hold parts of the tables to be printed to the corresponding Excel sheets
'********************************************************'
'Initialize our tables into thier recordsets for analysis'
'********************************************************'
Set Roster = CurrentDb.OpenRecordset(TblA)
Set Course = CurrentDb.OpenRecordset(TblB)
str = "SELECT Last FROM Roster"
Set last_name = CurrentDb.OpenRecordset(str)
str = "SELECT Course FROM [Course List]"
Set course_name = CurrentDb.OpenRecordset(str)
'**************************************************************************'
'Create the new excel file with default parameters and print the cover page'
'**************************************************************************'
Set excel = CreateObject("Excel.Application")
Set book = excel.Workbooks.Add
excel.Visible = True
Set sheet = book.Worksheets("Sheet1")
str = "Coversheet"
sheet.Name = str
sheet.Range("B2") = "HLC Database Export tool V1.0"
sheet.Range("B3") = "Written by Levi T Jackson, RN, BSN"
sheet.Range("B4") = "All rights reserved, Copyright 2021"
sheet.Range("B5") = "For use only by Emory Healhtcare, and others with permissions"
'**********************************'
'Main Loop, where the magic happens'
'**********************************'
ctrA = 0
Roster.MoveFirst
last_name.MoveFirst
Do Until last_name.EOF 'Move through the list of last names in the table Roster, one at a time
If Roster!Active = True Then 'No need to report on inactive employees, use access query for that
Set Child = Roster!Completion.Value 'Open a Recordset for the multivalued field Completion in Roster
ctrB = 1
If Child.EOF = True Then 'save the number of records for printing, or set to 0
var = 0
Else
Child.MoveLast
var = Child.RecordCount
Child.MoveFirst
End If
Course.MoveLast
If Child.EOF = False Then 'Avoid errors by not processing a page if no completion records exist
Set sheet = book.sheets.Add(After:=book.Worksheets(book.Worksheets.count)) 'For active employees, make a new sheet and switch to it, and set its name to the current last name from Roster
sheet.Activate
sheet.Range("A1").SELECT
str = Roster!Last & ", " & Roster!First
sheet.Name = str
sheet.Range("B2") = "Courses Completed"
Do Until Child.EOF 'If there are records in Completion for the current name, print them, move on when done
Course.MoveFirst
course_name.MoveFirst
Do Until Course.EOF
If Course![Course ID] = CInt(Child!Value.Value) Then
sheet.Range("D" & Mid(coordinates(ctrB), 2, Len(coordinates(ctrB)) - 1)) = Course![Course ID] 'prints course ID next to the name
sheet.Range("D2") = "'" & CStr(var) & " / " & CStr(Course.RecordCount) 'Prints number of records in completions
sheet.Range("B3") = "Course Name"
sheet.Range("D3") = "Course ID"
sheet.Range(coordinates(ctrB)) = Course!Course 'Prints course name
ctrB = ctrB + 1
Course.MoveLast
Course.MoveNext
Else
Course.MoveNext
course_name.MoveNext
End If
Loop
Child.MoveNext
Loop
End If
ctrA = ctrA + 1 'I might use this later in code updates, counts how manmy records are processed
Child.Close
excel.ActiveSheet.Cells.SELECT 'Selects all of the cells
excel.ActiveSheet.Cells.EntireColumn.AutoFit 'Does the "autofit" for all columns
sheet.Range("A1").SELECT 'Selects the first cell to unselect all cells
End If
Roster.MoveNext
last_name.MoveNext
Loop
'Clean up recordsets
last_name.Close
course_name.Close
Roster.Close
Set Roster = Nothing
Course.Close
Set Course = Nothing
End Function
'Converts the iteration of the print course sub loop into a sheet coordinate cell and returns it as a string
'This function is here so that later a more complicated printing coordinate system can be easily added as the database grows larger
Private Function coordinates(num As Integer) As String
coordinates = "B" & CStr(num + 4)
End Function
Add an order by clause to your OpenRecordset statements.
I use VBA code to bold fragments of text in a multi-line cell. Everything works fine except that, when I double-click on the cell after the bolding was set, the bolding "jumps" by one character to the right, as seen on the picture below:
This is the code snippet I use to bold the dates:
rng.Characters(Start:=lngStart, Length:=lngEnd).Font.Bold = True
I have a gut feeling that this might have something to do with the line breaks. I use a concatenation with Chr(10) to create them. (E.g. strLine1 & Chr(10) & strLine2).
I'm running Office 365, Excel Version 1908.
EDIT:
This is the code I use to bold the cells:
Public Sub BoldActionDates(lsoTarget As ListObject, strColumnName As String, lngDataRow As Long)
Const LNG_LENGTH_DATE_STRING As Long = 10
Dim colStartingPoints As Collection
Dim varIndex As Variant
Dim rngAction As Range
Set rngAction = lsoTarget.ListColumns(strColumnName).DataBodyRange(lngDataRow)
Set colStartingPoints = GetStartingPointsDateStrings(rngAction.Value)
If colStartingPoints.Count = 0 Then Exit Sub
rngAction.Font.Bold = False
For Each varIndex In colStartingPoints
rngAction.Characters(Start:=CLng(varIndex), Length:=LNG_LENGTH_DATE_STRING).Font.Bold = True
Next varIndex
End Sub
GetStartingPointsDateStrings returns a Collection that contains the values of the starting points, e.g. for the example shown above the Collection looks like this:
Also, there is no BeforeDoubleClick-Event in the worksheet, only an unrelated one for BeforeRightClick.
It is very simple but yet I can't figure it out. Maybe because it cannot be done? Regardless here we go:
I would like to use a combobox that will, when selected, input cells with text values, functions and reference to external cells.
First line of the options would be to have the name populated.
Second line is a formula that would change from course to course.
Third line would provide a cell with a reference to another cell's content from another file. So if multiple course file are used I can have one master file that if I change the content of a cell the change will reflect on all the course file cells that are referring to it once updated.
This is in crude code form what I would like it to perform.
Private Sub ComboBox1_Change()
If Me.ComboBox1.Value = "ITCourse" Then
Worksheets("PARADE STATE").Range("I1").Value = "ITCourse"
Worksheets("Data Base").Range("C1").Value = IF(V9>70,"Prep Week",IF(V9>65,"Week 1",IF(V9>60,"Week 2",IF(V9>55,"Week 3",IF(V9>50,"Week 4",IF(V9>45,"Week 5",IF(V9>40,"Week 6",IF(V9>35,"Week 7",IF(V9>30,"Week 8",IF(V9>25,"Week 9",IF(V9>20,"Week 10",IF(V9>15,"Week 11",IF(V9>10,"Week 12",IF(V9>5,"Week 13",IF(V9>0,"Week 14")))))))))))))))
Worksheets("Week 1").Range("B2").Value = 'N:\ITcourse\00 - Data Base\[ITcourse.xlsx]Sheet'!$A$3
End If
If Me.ComboBox1.Value = "HRCourse" Then
Worksheets("PARADE STATE").Range("I1").Value = "HRCourse"
Worksheets("Data Base").Range("C1").Value = IF(V9>40,"Prep Week",IF(V9>35,"Week 1",IF(V9>30,"Week 2",IF(V9>25,"Week 3",IF(V9>20,"Week 4",IF(V9>15,"Week 5",IF(V9>10,"Week 6",IF(V9>5,"Week 7",IF(V9>5,"Week 8")))))))))
Worksheets("Week 1").Range("B2").Value = 'N:\ITcourse\00 - Data Base\[HRcourse.xlsx]Sheet'!$A$3
End If
End Sub
Thank you!
You need a function that returns the number of weeks for any given course name. This function should use a Dictionary to store the information, and the dictionary may be loaded from a dedicated worksheet.
Function WeeksPerCourse(courseName As String) As Long
Static dict As Scripting.Dictionary
If dict Is Nothing Then
' Fill the dictionary here. Note that it is better
' to load the values from a dedicated, hidden worksheet
Set dict = CreateObject("Scripting.Dictionary")
dict("ITCourse") = 14
dict("HRCourse") = 8
' Etc...
End If
WeeksPerCourse = dict(courseName)
End Function
With this function available, your procedure can be simplified like follows:
Private Sub ComboBox1_Change()
Dim course As Sting: course = Trim(ComboBox1.value)
Worksheets("PARADE STATE").Range("I1").value = course
'Dim nWeek As Long
'nWeek = WeeksPerCourse(course) - Worksheets("PARADE STATE").Range("V9").value / 5
'Worksheets("Data Base").Range("C1").value = IIf(nWeek < 1, "Prep Week", "Week " & nWeek)
Worksheets("Data Base").Range("C1").Formula = "= ""Week "" & INT((WeeksPerCourse('PARADE STATE'!I1) - 'PARADE STATE'!V9)/5)"
Worksheets("Week 1").Range("B2").Formula= "='N:\ITcourse\00 - Data Base\[" & course & ".xlsx]Sheet'!$A$3"
End Sub
Cell value of B6 = 'Trading Income
=VLOOKUP(B6,'\\myComp.myComp.com\abc\Treas\P&L\data\[DataUS.xls]Smith, Bob'!$A$1:$D$2000,2,FALSE)
This returns (5,555,529.00)
However, say I wanted to place
'\\myComp.myComp.com\abc\Treas\P&L\data\[DataUS.xls]Smith, Bob'!$A$1:$D$2000
in a cell (Lets say B7). How would I structure the VLOOKUP?
I tried:
VLOOKUP(B6, B7, 2, FALSE)
And it returns #N/A
Thank you
Try
=VLOOKUP(B6;'\\myComp.myComp.com\abc\Treas\P&L\data\[DataUS.xls]Smith, Bob'!$A$1:$D$2000;2;FALSE)
You have to use INDIRECT, see this.
In your case, use INDIRECT(B7) instead of B7.
Try this:
=VLOOKUP(B6, INDIRECT(B7), 2, FALSE)
Just ensure that cell B7 contains the exact path i.e. written exactly in the same manner it is written in the formula.
Now adding macros doe
sn't really increase the level of complexity, at least I have not seen any case as yet. On the contrary a macro, if done correctly, increase efficiency,
dependency and certainty of the results.
I suggest this solution which includes macros, provides the simplicity and flexibility of building the link to external data using a Name to hold the External Reference created using user input, which is split in the different parts of the external link to make it easier changes i.e. Path, Filename, Worksheet and Range.
This includes the creation of five Names to handle the linked formula, here is when you might feel like you got it right when mentioning “increasing the level of complexity”, however we can use the power and flexibility of macros not only to produce the expected outcome in a report, analysis, etc.; but also to build forms, reports, graphs, data, etc. and by using macros it also eliminates the need for insanity checks, which at times make us insane, providing and excellent tool to reinstate, review and even change the parameters of large projects when required.
The code provided below includes the creation of the Names, also the refresh the Name that holds the External Link Reference once the user changes any part of the external reference in the worksheet.
First we run this code to create the names (copy this in a module)
Option Explicit
Option Base 1
Sub FmlLnk_WshAddNames()
Const kRowIni As Byte = 2
Const kCol As Byte = 3
Const kWshTrg As String = "Sht(1)"
Dim aNames As Variant
aNames = fNames_Get
Dim WshTrg As Worksheet
Dim bRow As Byte
Dim b As Byte
Set WshTrg = ThisWorkbook.Worksheets(kWshTrg)
With WshTrg
For b = 1 To UBound(aNames)
bRow = IIf(b = 1, kRowIni, 1 + bRow)
.Names.Add Name:=aNames(b), RefersTo:=.Cells(bRow, kCol)
.Names(aNames(b)).Comment = "Name to create link to external range"
Next: End With
End Sub
Function fNames_Get() As Variant
fNames_Get = Array("_Path", "_Filename", "_Worksheet", "_Range")
End Function
Now that the Names to hold the parts of the external link are created we add the worksheet event to automatically update the name holding the External Link Reference (see https://msdn.microsoft.com/EN-US/library/office/ff198331.aspx)
To go to the event procedures for the Worksheet that contains the formula right-click the sheet tab and click “View Code” on the shortcut menu.
Copy the code below in the Worksheet code
Option Explicit
Option Base 1
Private Sub Worksheet_BeforeDoubleClick(ByVal RngTrg As Range, bCancel As Boolean)
Const kFmlLnk As String = "_FmlLnk"
Dim aNames As Variant, vName As Variant
aNames = fNames_Get
Dim WshThs As Worksheet
Dim bLnkExt As Boolean
Dim sLnkExt As String
Set WshThs = RngTrg.Worksheet
With WshThs
Application.Goto .Cells(1), 1
Rem Validate ActiveCell
bLnkExt = False
For Each vName In aNames
If .Names(vName).RefersToRange.Address = RngTrg.Address Then
bLnkExt = True
Exit For
End If: Next
Rem Reset Name Link External
If bLnkExt Then
Rem Built External Formula Link
sLnkExt = "=" & Chr(39) & .Names(aNames(1)).RefersToRange.Value2 & _
"[" & .Names(aNames(2)).RefersToRange.Value2 & "]" & _
.Names(aNames(3)).RefersToRange.Value2 & Chr(39) & Chr(33) & _
.Names(aNames(4)).RefersToRange.Value2
Rem Add External Formula Link Name
.Names.Add Name:=kFmlLnk, RefersTo:=sLnkExt
.Names(kFmlLnk).Comment = "Name to link external range in Formula"
End If: End With
End Sub
This procedure will run every time the users double-clicks in any of the four Names created in the worksheets that holds the External Link Formula
The Formula to use the external link name is:
=VLOOKUP($B9,_FmlLnk,3,0)
I have a button in Access (2003) that transfers data to Excel (also 2003). It opens the Excel workbook, then cycles through the Access subforms and transfers data.
To give more information on how this works, Excel has a range called "Tables" which contains the names of the Access subforms ("Main", "Demographics", "History", etc). Excel also has a range for each of the names in that first range. For example, the range "Demographics" contains a series of field names ("FirstName", "LastName", etc). So the first loop moves through the subforms, and the nested loop moves through the field names. Each field then passes the value in it over to excel. Excel also has ranges for "Demographics_Anchor" and "History_Anchor" etc, which is the first value in the column next to each range (ie the range Demographics has firstname, lastname, and to the right is where the data would go. So the first item in the range is FirstName, to the right "Demographics_Anchor" is where firstname will go. Then LastName goes to Demographics_Anchor offset by 1 - or 1 cell down from the anchor).
Dim ThisForm As Form
Dim CForm As Object
Dim CTab As TabControl
Dim CControl As Control
Dim CurrentTab As Variant
Dim CControlName As Variant
Dim CControlValue As String
Dim Code As Control
Dim counter1 As Integer
Dim appExcel As Object
Dim Anchor As Object
Dim PageRange As Object
Dim ControlNameRange As Object
strpath = "C:\blah\blah\filename.xlsm"
Set appExcel = CreateObject("Excel.Application")
appExcel.Workbooks.Open Filename:=strpath, UpdateLinks:=1, ReadOnly:=True
Set wbk = appExcel.ActiveWorkbook
Set PageRange = appExcel.Range("Tables")
'set Access environment
Set ThisForm = Forms("frmHome")
Set CTab = ThisForm.Controls("Subforms")
'export the data from Access Forms to Excel
For Each CurrentTab In PageRange
If CurrentTab = "Main" Then
Set CForm = ThisForm
Else
CTab.Pages(CurrentTab).SetFocus
Set CForm = ThisForm.Controls(CurrentTab & " Subform").Form
End If
Set ControlNameRange = appExcel.Range(CurrentTab)
Set Anchor = appExcel.Range(CurrentTab & "_Anchor")
counter1 = 0
For Each CControlName In ControlNameRange
Set CControl = CForm.Controls(CControlName)
CControl.SetFocus
Anchor.Offset(RowOffset:=counter1).Value = CControl.Value
counter1 = counter1 + 1
Next CControlName
Next CurrentTab
I hope this explains what is going on in the code. I just can't figure out why this keeps bombing out with type mistmatch (error 13).
The data DOES transfer. It goes through the entire code and every piece of data correctly gets transferred over. It bombs out at the end as if it goes through the code 1 last time when it shouldn't. I did confirm that every range is correct and doesn't contain any null values. The code bombs out on this line: Set CControl = CForm.Controls(CControlName) which is towards the bottom of the second loop.
Please help! I've spent weeks working with this code and had no luck. This exact code works in every other database I've worked with.
You are getting the name of the control CControlName from your Excel Range, but then setting the value of this control to the control on the Access form Set CControl = CForm.Controls(CControlName). From this, the most likely explanation is probably that the CControlName isn't actually on the Access form (perhaps a typo?).
In the VBA IDE, go under the Tools Menu, select Options and then select the General tab. Under the Error Trapping section, select the "Break on All Errors" option and click "OK" to set the preference. Run your code again; when an error is encountered VBA will stop processing on the line that caused the error. Check the value of CControlName and make sure it actually exists on the Access form.