Exporting Access query results by record to new worksheet in file - excel

I'm trying to workout how to split the unique records of a query to new worksheets in the same excel workbook (template file). My access query has the following fields:
Project Number,Project Name,Task Number,Project Sponsor,Full Year Budget,APR,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec,Jan,Feb,Mar,Full,Year Forecast.
I want to be able to have a new worksheet for each Project Number and list the relevant data, and also rename the worksheet as the Project Number...I have a complete mind block after spending hours playing around with other code for similar requests, but cannot get anything to behave the way I need it to?
Does any one have a ideas or could point me in the right direction, I'm not a complete novice at vba but this one has me well and truly stuck.
Many thanks :)
Hi All, thanks for the suggestions, I've managed to cobble together the code to spilt the data and export to individual worksheets and its working ok.I now need to copy over any associated data from another query to the relevant worksheet in a "table" below the other data, but I'm not having much luck. Either it will copy one record to one of the worksheets or all of the records to a single worksheet irrespective. Can anyone point me in the right direction?
Option Compare Database
Global iter As Integer
Sub Loop_Practice2()
Dim rs As DAO.Recordset
Dim ProjectNumber As DAO.Recordset
Dim i As Integer
Dim j As Integer
Dim Worksheet_Count As Integer
Dim sSql As String
Dim Project_Count As Integer
Dim iCol As Integer
Dim mypath As String
Dim mvalue As String
Dim myfile As String
Dim mynewfile As String
Dim mynewpath As String
Dim wb As Excel.Workbook
Dim WS As Excel.Worksheet
Dim sFile As String
mypath = Application.CurrentProject.Path & "\"
myfile = ("PIN Export Template.xlsx")
mynewpath = (Application.CurrentProject.Path & "\")
mynewfile = ("PIN Export Template.xlsx - " & Format(Now(), "yyyy-mm-dd") & ".xlsx")
sFile = mypath & myfile
' ' Use Dir to check if file exists
If Dir(sFile) = "" Then
' if file does not exist display message
MsgBox "Could not find the file " & sFile & " - Please ensure it is in the same location as the database."
Exit Sub
End If
'Open Excel
Excel.Application.Visible = True
Excel.Application.Workbooks.Open (sFile)
'Define Access Query to be exported
Set ProjectNumber = CurrentDb.OpenRecordset("SELECT DISTINCT qry_MP_PDP_PIN_Analysis_Step_01_FY_Position_Monthly.[Project Number] from qry_MP_PDP_PIN_Analysis_Step_01_FY_Position_Monthly")
If ProjectNumber.EOF Then Exit Sub
ProjectNumber.MoveLast
Project_Count = ProjectNumber.RecordCount - 1
ProjectNumber.MoveFirst
'Create individual PIN sheets from Query Dataset
Excel.Application.Worksheets("PIN").Select
Worksheet_Count = Excel.Application.Worksheets("PIN").Select
Do Until Worksheet_Count = Project_Count
Worksheets("PIN").Copy After:=Worksheets("PIN")
If iter = 0 Then
iter = 1
End If
ActiveSheet.Name = ("PIN") & iter
iter = iter + 1
Worksheet_Count = Worksheet_Count + 1
Loop
j = 1
'Add qry_MP_PDP_PIN_Analysis_Step_01_FY_Position_Monthly data
Do Until ProjectNumber.EOF
sSql = "SELECT *"
sSql = sSql & " FROM qry_MP_PDP_PIN_Analysis_Step_01_FY_Position_Monthly"
sSql = sSql & " Where qry_MP_PDP_PIN_Analysis_Step_01_FY_Position_Monthly.[Project Number]=" & ProjectNumber("[Project Number]")
Set rs = CurrentDb.OpenRecordset(sSql, dbOpenDynaset)
Set Pin_Sheet = ActiveWorkbook.Sheets("PIN" & j)
'Rename the PIN sheet to individual Project Number
Pin_Sheet.Name = ProjectNumber("[Project Number]")
'Create PIN Analysis Column Headings
For iCol = 0 To rs.Fields.Count - 1
Pin_Sheet.Cells(13, iCol + 4).Value = rs.Fields(iCol).Name
Next
'Populate PIN_Analysis_Step_01_FY_Position_Monthly Data
Pin_Sheet.Cells(14, 4).CopyFromRecordset rs
j = j + 1
ProjectNumber.MoveNext
Loop
Excel.Application.ActiveWorkbook.SaveAs (mynewpath & mynewfile)
Set Pin_Sheet = Nothing
Set ProjectNumber = Nothing
Set ProjectNumber2 = Nothing
Set rs = Nothing
Set ProjectNumber = Nothing
Set wb = Nothing
Set WS = Nothing
CurrentDb.Close
ActiveWorkbook.Close
Excel.Application.Quit
End Sub

AS 'Erik von Asmuth' Suggested it is a broad question split into a different task and share your code. what you have attempted until now.
I can only point you to one article written by Daniel Pineault . He had created a function called ExportRecordset2XLS through which you can pass your recordset, Sheet Name etc.
you have to create a loop for different project number and pass as an argument to this function. you also need to modify this code to handle differnt task as per your requiremnts.
https://www.devhut.net/2017/03/15/ms-access-vba-export-recordset-to-excel/

Related

Subscript out of range error in Excel VBA when I add a new worksheet at the end of the list of worksheets only when the VB window is closed

I have built an Excel Workbook that is intended for evaluation of an organization as a whole and then evaluation of each of several sites for that organization. There is an initial assessment and then an on-site assessment for the organization, and for each facility. Depending on the organization, the number of facilities will vary. There is a first "Configuration" tab where the user of the workbook enters (or copies and pastes) the list of facilities and determines which facilities are to be included in the evaluation.
The second and third worksheets are the assessment for the organization as a whole, and the fourth and fifth worksheets are template assessment forms for the facilities.
Once the list of facilities is entered, the user clicks on a button labeled "Create Facility Tabs" that steps through the facility list and creates the needed worksheets for each facility.
If the list is fresh (starting from its initial form), then the template worksheets are renamed for the first facility and new worksheets are created for the remainder.
If there are already worksheets identified, the software checks each facility to see if its page already exists, and only creates additional worksheets for newly added facilities.
When additional worksheets are needed, the code first counts the number of additional worksheets that are needed (two for each facility), creates those worksheets, and then steps through them copying the template contents onto the forms and the change code for the worksheets into the worksheet's module.
The software works perfectly over and over again when I have the VBA window open. It does everything it is supposed to do. However, when I close the VBA window, the code creates all the worksheets, copies everything into the first worksheet, and then raises a Subscript Out of Range error. Any ideas what I am doing wrong?
Here is the code:
Public Sub CreateFacilities()
Dim row As Long
Dim facility_name As String
Dim facility_list As String
Dim facilities As Variant
Dim include As Boolean
Dim ws_init As Worksheet
Dim ws_fac As Worksheet
Dim ws_new_init As Worksheet
Dim ws_new_fac As Worksheet
Dim ws_config As Worksheet
Dim facility_count As Long
Dim tabs_to_create As Long
Dim fac_initial_range As Range
Dim fac_initial_address As String
Dim fac_onsite_range As Range
Dim fac_onsite_address As String
Dim message As String
Dim title As String
Dim answer As Variant
Dim code_line As String
Dim b_width As Long
Dim c_width As Long
Dim counter As Long
Dim init_sheet_number As Long
Dim fac_sheet_number As Long
Dim tab_count As Long
title = "Creating Facility Tabs"
message = "Before you execute this function you should" & vbCrLf & "add any study-specific questions to the" & vbCrLf
message = message & "Initial Assessment - Facility1 and" & vbCrLf & "On-Site Assessment - Facility1 tabs so" & vbCrLf
message = message & "they will be included on the created facility tabs" & vbCrLf & vbCrLf
message = message & "Do you wish to continue?"
answer = MsgBox(message, vbYesNo + vbQuestion, title)
If answer = vbNo Then
Exit Sub
End If
Set ws_config = ThisWorkbook.Sheets("Configuration")
Set ws_init = ThisWorkbook.Sheets(4)
Set ws_fac = ThisWorkbook.Sheets(5)
b_width = ws_init.Columns("B").ColumnWidth
c_width = ws_init.Columns("C").ColumnWidth
Set fac_initial_range = ws_init.Range("A1:C" & Trim(Str(FindLastRow(ws_init))))
Set fac_onsite_range = ws_fac.Range("A1:C" & Trim(Str(FindLastRow(ws_fac))))
fac_initial_address = fac_initial_range.address
fac_onsite_address = fac_onsite_range.address
code_line = ThisWorkbook.VBProject.VBComponents("Module1").CodeModule.Lines(1, 50) 'get code for each new worksheet
facility_list = "" 'get list of facilities
facility_count = 0
For row = 4 To 54
facility_name = ThisWorkbook.Sheets("Configuration").cells(row, 2).value
include = ThisWorkbook.Sheets("Configuration").cells(row, 4).value
If facility_name = "" Then 'reached the end of the list
Exit For
Else:
If include Then 'the Do Assessment column is marked TRUE
If Not WorksheetExists(facility_name) Then 'the tabs for this facility do not already exist
facility_list = facility_list & facility_name & ","
End If
End If
End If
Next row
facility_list = Left(facility_list, Len(facility_list) - 1) 'remove trailing comma
If facility_list = "" Then 'no new facilities were added to the list
MsgBox "There were no facilties specified for inclusion"
Exit Sub
End If
facilities = Split(facility_list, ",") 'there is a list of facilities to add
facility_count = UBound(facilities) + 1
If ActiveWorkbook.Sheets.Count = 5 Then 'no facility tabs have been added
If facility_count = 1 Then 'there is only one facility - no tabs need to be added
tabs_to_create = 0
facility_name = facilities(0)
ws_init.Name = CreateInitialTabName(facility_name)
ws_fac.Name = CreateOnSiteTabName(facility_name)
Else:
tabs_to_create = (facility_count - 1) * 2
ActiveWorkbook.Sheets.Add After:=ActiveWorkbook.Sheets(ActiveWorkbook.Sheets.Count), Count:=tabs_to_create
For counter = LBound(facilities) To UBound(facilities)
facility_name = facilities(counter)
If counter = 0 Then 'rename the first two facility worksheets that already exist
ws_init.Name = CreateInitialTabName(facility_name)
ws_fac.Name = CreateOnSiteTabName(facility_name)
Else: 'for the rest, add worksheets and copy template content and code
init_sheet_number = ((counter - 1) * 2) + 6
fac_sheet_number = init_sheet_number + 1
Set ws_new_init = ActiveWorkbook.Sheets(init_sheet_number) 'create initial assessment sheet for facility
ws_new_init.Columns("B").ColumnWidth = b_width
ws_new_init.Columns("C").ColumnWidth = c_width
ws_new_init.Name = CreateInitialTabName(facility_name)
fac_initial_range.Copy Destination:=ws_new_init.Range("A1")
ThisWorkbook.VBProject.VBComponents(ws_new_init.CodeName).CodeModule.AddFromString code_line
Set ws_new_fac = ActiveWorkbook.Sheets(fac_sheet_number) 'create on-site assessment sheet for facility
ws_new_fac.Columns("B").ColumnWidth = b_width
ws_new_fac.Columns("C").ColumnWidth = c_width
ws_new_fac.Name = CreateOnSiteTabName(facility_name)
fac_onsite_range.Copy Destination:=ws_new_fac.Range("A1")
ThisWorkbook.VBProject.VBComponents(ws_new_fac.CodeName).CodeModule.AddFromString code_line
End If
Next counter
End If
Else: 'there are more than 5 tabs in the workbook - some were already added
tab_count = ActiveWorkbook.Sheets.Count
tabs_to_create = facility_count * 2
ActiveWorkbook.Sheets.Add After:=ActiveWorkbook.Sheets(tab_count), Count:=tabs_to_create
For counter = LBound(facilities) To UBound(facilities)
facility_name = facilities(counter)
init_sheet_number = (counter * 2) + (tab_count + 1)
fac_sheet_number = init_sheet_number + 1
Set ws_new_init = ActiveWorkbook.Sheets(init_sheet_number)
Set ws_new_fac = ActiveWorkbook.Sheets(fac_sheet_number)
ws_new_init.Name = CreateInitialTabName(facility_name)
ws_new_fac.Name = CreateOnSiteTabName(facility_name)
ws_new_init.Columns("B").ColumnWidth = b_width
ws_new_fac.Columns("B").ColumnWidth = b_width
ws_new_init.Columns("C").ColumnWidth = c_width
ws_new_fac.Columns("C").ColumnWidth = c_width
fac_initial_range.Copy Destination:=ws_new_init.Range("A1")
fac_onsite_range.Copy Destination:=ws_new_fac.Range("A1")
ThisWorkbook.VBProject.VBComponents(ws_new_init.CodeName).CodeModule.AddFromString code_line
ThisWorkbook.VBProject.VBComponents(ws_new_fac.CodeName).CodeModule.AddFromString code_line
Next counter
End If
ws_config.Activate
MsgBox Str(facility_count) & " facilities added"
End Sub

VBA to extract file information, add any new information after last row of data

Sub GetFileList()
Dim xFSO As Object
Dim xFolder As Object
Dim xFile As Object
Dim objOL As Object
Dim Msg As Object
Dim xPath As String
Dim thisFile As String
Dim i As Integer
Dim lastrow As Long
xPath = Sheets("UI").Range("D7")
Set xFSO = CreateObject("Scripting.FileSystemObject")
Set xFolder = xFSO.GetFolder(xPath)
i = 1
For Each xFile In xFolder.Files
i = i + 1
Worksheets("Info").Cells(i, 1) = xPath
Worksheets("Info").Cells(i, 2) = Left(xFile.Name, InStrRev(xFile.Name, ".") - 1)
Worksheets("Info").Cells(i, 3) = Mid(xFile.Name, InStrRev(xFile.Name, ".") + 1)
Worksheets("Info").Cells(i, 6) = Left(FileDateTime(xFile), InStrRev(FileDateTime(xFile), " ") - 1)
Next
Set Msg = Nothing
Worksheets("Info").Visible = True
Worksheets("Info").Activate
End Sub
The code to extract file information from a folder. The issue is when I change the folder path, it overwrites on the previously fetched data.
Sheet -UI is where the sub executed on press of button, Sheet Info is the place where the data needs to be pasted.
How to write the code to add a new row of data after the data which is already available. If the sheet is blank then add data from the 1st ROW otherwise add data from the LAST ROW.
Sheets("UI").Range("A1").End(xlDown).Select
i = Selection.Row + 1
Try replacing
i = 1
with
i = Worksheets("Info").UsedRange.Rows.Count + 1
This will set i to 1 the first time around, and to the first free row ever after. New data will be added below the existing data, if there is any.

Using For loop for selecting specific Fields in Access and pasting them to excel

I made a specific Access Form that has many field with same names only the number at end changes. I have fields named "Code1", "Code2", "Code3" etc.
I would like to paste the fields from Form view to Excel Cells. There is about 150 fields and I don't want to add them one by one.
I made a button that opens Excel template, and made a For loop but I am stuck. This is the idea:
Set MyXL = CreateObject("Excel.Application")
With MyXL
.Application.Visible = True
.Workbooks.Open "F:\0. Main\01.Templates\Order.xltx"
Dim broj As Variant
broj = UCase(ID)
Dim Kod As Variant
Dim Tip As Variant
Dim Kolic As Variant
For i = 1 To 30
-------> Kod = Code(i).Value
.Worksheets("Sheet1").Cells(11 + i, 2).Value = Kod
-------> Tip = Type(i).Value
.Worksheets("Sheet1").Cells(11 + i, 3).Value = Tip
-------> Tip = Qty(i).Value
.Worksheets("Sheet1").Cells(11 + i, 3).Value = Kolic
Next i
I don't know how to include FieldName + (number) in For loop
Please check if a simple paste of the whole recordset directly to Excel's range will work (will overwrite starting at L2 as much as needed):
.Worksheets("Sheet1").Cells(12, 2).CopyFromRecordset
If this works, you may want to clear the range before pasting on it:
.Worksheets("Sheet1").Range("L2:N100000").ClearContent
If that doesn't help, you may want to loop over the recordset within Access e. g. like this:
Set rs = CurrentDb.OpenRecordset("WhatEver", dbOpenDynaset)
If Not (rs.BOF And rs.EOF) Then
rs.MoveFirst
i = 1
Do
... = rs.Fields("Code" & i).Value
...
rs.MoveNext
i = i + 1
Loop Until i > 30 ' or Until rs.EOF
End If
Option Compare Database
Sub Export()
Dim tableName As String
tableName = InputBox("What is the name of the table you want to export?")
Dim outputFileName As String
outputFileName = CurrentProject.Path & "\Export_" & Format(Date, "yyyyMMdd") & ".xls"
DoCmd.TransferSpreadsheet acExport, acSpreadsheetTypeExcel9, tableName, outputFileName, True
Set myXl = CreateObject("Excel.Application")
myXl.Visible = True
myXl.workbooks.Open outputFileName
End Sub

Writing to Named Cells in Excel from Access

I've Searched Forums here and I can't seem to get this code to work.
I am Trying to Open a Workbook in Excel, and then populate a few of the Cells(Named Ranges). I can successfully open the workbook(the workbook has a bit of VBA that runs when it opens as well, formatting stuff only) but when I get down to the inputting information I get a 'Run-Time Error "438" Object Doesn't support this property or method.'
From the Previous answers on other similar questions I have done everything the way it was suggested however, I can't seem to get it to work.
Option Compare Database
Option Explicit
Public Sub MaterialInput()
Dim xlapp As Excel.Application
Dim wb As Excel.Workbook
Dim ws As Excel.Worksheet
Dim RsClient As Recordset
Dim RsJobsite As Recordset
Dim db As Database
Dim ClientSTR As String
Dim JobsiteSTR As String
Dim customer As Variant
Set db = CurrentDb
JobsiteSTR = "SELECT T1Jobsites.JobsiteNickName FROM T1Jobsites WHERE T1Jobsites.JobsiteID = 1" ' & Form_LEM.TxtJobsiteID
Set RsJobsite = db.OpenRecordset(JobsiteSTR, dbOpenSnapshot, dbSeeChanges)
ClientSTR = "SELECT T1Companies.CompanyName " & _
"FROM T1Companies INNER JOIN T1Jobsites ON T1Companies.CompanyID = T1Jobsites.CompanyId " & _
"WHERE (((T1Jobsites.JobsiteID)=1))"
'ClientSTR = "SELECT T1Companies.CompanyName FROM T1Companies INNER JOIN T1Jobsites ON T1Companies.CompanyID = T1Jobsites.CompanyID" & _
" WHERE T1JobsitesID = 1" '& Form_LEM.TxtJobsiteID
Set RsClient = db.OpenRecordset(ClientSTR, dbOpenSnapshot, dbSeeChanges)
Set xlapp = CreateObject("excel.application")
Set wb = xlapp.Workbooks.Open("C:\Users\coc33713\Desktop\VISION - EXCEL FILES\VISIONCOUNT.xlsm")
Set ws = xlapp.Worksheets("CountSheet")
xlapp.Visible = True
'Tried this second after reading another forum
'the comments Recordset will be the actual values used, but I can't get the String "TEST" to work
wb.ws.Range("Client").Value = "TEST" 'RsClient!CompanyName
'Tried this way first
xlapp.ws.Range("'SiteName'").Value = "Test" 'RsJobsite!JobsiteNickName"
xlapp.ws.Range(Date).Value = "Test" 'Form_LEM.TxtDate
xlapp.ws.Range(ProjectName).Value = "Test" 'Form_LEM.TxtPlant
xlapp.ws.Range(ScaffoldID).Value = "Test" 'Form_LEM.cboScaffnum.Value
xlapp.ws.Range(ScaffoldNumber).Value = "Test" 'Form_LEM.cboScaffnum.Column(1)
Set xlapp = Nothing
Set wb = Nothing
Set ws = Nothing
Set RsClient = Nothing
Set RsJobsite = Nothing
Set db = Nothing
End Sub
As a Sidenote this is not a form it is just spreadsheet
Thank you everyone!
Use
ws.Range("Client").Value = "Test"
Or
Dim sName as String
sName = "Client"
ws.Range(sName).Value = "Test"
Reason being is that you have the ws object set already, so there is no need to assign parentage to it again. In fact, trying to do so will break syntax rules.
FWIW (not your issue - that is solved by Scott's answer): Note that
Set ws = xlapp.Worksheets("CountSheet")
should be
Set ws = wb.Worksheets("CountSheet").
Using xlapp.Worksheets("CountSheet")
is effectively xlApp.ActiveWorkbook.Worksheets("CountSheet") which might be (and probably is) xlApp.Workbooks("VISION - EXCEL FILES\VISIONCOUNT.xlsm").Worksheets("CountSheet") but it is better to do it correctly rather than leave it to chance.
Thank you guys!
This should do what you want.
Sub DAOFromExcelToAccess()
' exports data from the active worksheet to a table in an Access database
' this procedure must be edited before use
Dim db As Database, rs As Recordset, r As Long
Set db = OpenDatabase("C:\FolderName\DataBaseName.mdb")
' open the database
Set rs = db.OpenRecordset("TableName", dbOpenTable)
' get all records in a table
r = 3 ' the start row in the worksheet
Do While Len(Range("A" & r).Formula) > 0
' repeat until first empty cell in column A
With rs
.AddNew ' create a new record
' add values to each field in the record
.Fields("FieldName1") = Range("NamedRange1").Value
.Fields("FieldName2") = Range("NamedRange2").Value
.Fields("FieldNameN") = Range("NamedRangeN").Value
' add more fields if necessary...
.Update ' stores the new record
End With
r = r + 1 ' next row
Loop
rs.Close
Set rs = Nothing
db.Close
Set db = Nothing
End Sub

Getting the range used to define error bars with VBA

I have an Excel chart. One of the series has X and Y error bars, defined from worksheet ranges.
I want to get via VBA those ranges (not set them). Is this possible?
Jon Peltier has an article about error bars on his blog here
Quoting from that:
Programmatically Defining Custom Error Bars
The command to add error bars using Excel is: {Series}.ErrorBar
Direction:={xlX or xlY}, Include:=xlBoth, Type:=xlCustom, _
Amount:={positive values}, MinusValues:={negative values} Values can be a single numerical value, for example, 1, an comma-separated
array of numerical values in curly braces, such as {1,2,3,4}, or a
range address in R1C1 notation. For values in Sheet1!$G$2:$G$10, enter
the address as Sheet1!R2C7:R10C7. Combine both plus and minus in the
same command. In Excel 2007, if you don’t want to show a particular
error bar, you must enter a value of zero in this command. In 2003,
you can enter a null string “”. In Excel 2003, the range address must
begin with an equals sign, =Sheet1!R2C7:R10C7; Excel 2007 accepts the
address with or without the equals sign. Single values or arrays may
be entered with or without the equals sign in either version of Excel.
In a post on Ozgrid, Jon Peltier says
the range for custom error bar values is not exposed to VBA
If Jon says it can't be done, it can't be done.
I know I'm 8 years late to the party here... but I stumbled upon this while scouring the web for the answer to this same question. I came up empty too, so I decided to devise my own solution, and figured I might as well post it on the off chance that someone else ends up here.
It works by extracting the workbook XML to a temporary folder, locating the error bar reference in the XML, and returning it as a Range object. You therefore have to save changes to the workbook before the function will work. If you change the error bar range without saving, the function will still return the old range from the most recent save. It also will not work on files from Excel 2003 or earlier (.xls).
It's anything but elegant... but at least this is technically possible!
To use: just copy the code below into a standard module, and call GetErrorBarRange(MySeries.ErrorBars, enErrorBarPlus) for the source range of the positive error bar, or GetErrorBarRange(MySeries.ErrorBars, enErrorBarMinus) for the source range of the negative error bar (where MySeries.ErrorBars is some ErrorBars object). Passing the optional third argument AutoSave:=True will save the containing workbook automatically before looking for the error bar source ranges.
' Created by Ryan T. Miller in 2022
' You may use this code in your own work however you wish. It'd be real swell of you
' to leave this credit in if you do, but I'm not gonna force you to.
Option Explicit
Option Private Module
Public Enum EnErrorBarPlusMinus
enErrorBarPlus
enErrorBarMinus
End Enum
Private moFSO As Object
' Get error bar source range from ErrorBars object
Public Function GetErrorBarRange(oErrorBars As ErrorBars, _
PlusMinus As EnErrorBarPlusMinus, _
Optional AutoSave As Boolean) As Range
Dim oFile As Object
Dim strTempDir As String
Dim strSubfolder As String
Dim oSeries As Series
Dim oChart As Chart
Dim oSheet As Object
Dim oWb As Workbook
Dim strPrefix As String
Dim strSeriesName As String
Dim strChartName As String
Dim strSheetName As String
Dim strXMLFile As String
Dim strXPath As String
Dim strCurrentSheet As String
Dim strRelId As String
Dim strDrawingXml As String
Dim strChartXml As String
Dim strErrValType As String
Dim strErrBarType As String
Dim strErrBarFormula As String
Dim rngResult As Range
On Error GoTo CleanUp
If Not (PlusMinus = enErrorBarMinus _
Or PlusMinus = enErrorBarPlus) Then Exit Function
Set moFSO = CreateObject("Scripting.FileSystemObject")
Application.Cursor = xlWait
' Set Series, Chart, Sheet, and Workbook objects
Set oSeries = oErrorBars.Parent
Set oChart = oSeries.Parent.Parent
If TypeOf oChart.Parent Is ChartObject Then
' Chart is on a worksheet
Set oSheet = oChart.Parent.Parent
strPrefix = "work"
Else
' Chart is on its own chart sheet
Set oSheet = oChart
strPrefix = "chart"
End If
Set oWb = oSheet.Parent
If AutoSave Then oWb.Save
' Name of the series, chart & its parent sheet
strSeriesName = oSeries.Name
strChartName = oChart.Parent.Name
strSheetName = oSheet.CodeName
strTempDir = ExtractWorkbookXMLToTemp(oWb)
' Loop over worksheet/chartsheet XML files & find the one where /worksheet/sheetPr/#codeName=strSheetName
' Then get strRelId from /worksheet/drawing/#r:id
' This is the ID which specifies which relationship links the sheet to the drawings.
strSubfolder = moFSO.BuildPath(strTempDir, "xl\" & strPrefix & "sheets")
strXPath = "/x:" & strPrefix & "sheet/x:sheetPr/#codeName"
For Each oFile In moFSO.GetFolder(strSubfolder).Files
strXMLFile = moFSO.BuildPath(strSubfolder, oFile.Name)
strCurrentSheet = GetXPathFromXMLFile(strXMLFile, strXPath)
If strSheetName = strCurrentSheet Then Exit For
Next oFile
strXPath = "/x:" & strPrefix & "sheet/x:drawing/#r:id"
strRelId = GetXPathFromXMLFile(strXMLFile, strXPath)
' Open the _rels XML associated with the correct sheet.
' Then get strDrawingXml from /Relationships/Relationship[#Id='strRelId']/#Target
' This is the name of the drawing XML.
strSubfolder = strSubfolder & "\_rels"
strXMLFile = moFSO.BuildPath(strSubfolder, moFSO.GetFileName(strXMLFile)) & ".rels"
strXPath = "/rel:Relationships/rel:Relationship[#Id='" & strRelId & "']/#Target"
strDrawingXml = GetXPathFromXMLFile(strXMLFile, strXPath)
strDrawingXml = Replace$(Replace$(strDrawingXml, "../", "/"), "/", "\")
' Open the correct drawing XML file (strDrawingXml)
' Then get strRelId from xdr:wsDr//xdr:graphicFrame[xdr:nvGraphicFramePr/xdr:cNvPr/#name='strChartName']/a:graphic/a:graphicData/c:chart/#r:id
' Or, if oSheet is a ChartSheet, there will only be 1 chart, so just get xdr:wsDr//xdr:graphicFrame/a:graphicData/a:graphic/c:chart/#r:id
' This is the ID which specifies which relationship links the drawing to the chart.
strXMLFile = moFSO.BuildPath(strTempDir, "xl" & strDrawingXml)
strXPath = "xdr:wsDr//xdr:graphicFrame" & _
IIf(TypeOf oChart.Parent Is ChartObject, "[xdr:nvGraphicFramePr/xdr:cNvPr/#name='" & strChartName & "']", vbNullString) & _
"/a:graphic/a:graphicData/c:chart/#r:id"
strRelId = GetXPathFromXMLFile(strXMLFile, strXPath)
' Open the _rels associated with the correct drawing XML.
' Then get strChartXml = /Relationships/Relationship[#Id='strRelId']/#Target
' This is the name of the chart XML.
strSubfolder = moFSO.GetParentFolderName(strXMLFile) & "\_rels"
strXMLFile = moFSO.BuildPath(strSubfolder, moFSO.GetFileName(strXMLFile)) & ".rels"
strXPath = "/rel:Relationships/rel:Relationship[#Id='" & strRelId & "']/#Target"
strChartXml = GetXPathFromXMLFile(strXMLFile, strXPath)
strChartXml = Replace$(Replace$(strChartXml, "../", "/"), "/", "\")
' Open the correct chart XML file (strChartXml)
strXMLFile = moFSO.BuildPath(strTempDir, "xl" & strChartXml)
' Get error bar value type. If the error bar is set to a Range then this must be 'cust'.
strXPath = "/c:chartSpace/c:chart//c:ser[c:tx//c:v[text()='" & strSeriesName & "']]/c:errBars/c:errValType/#val"
strErrValType = GetXPathFromXMLFile(strXMLFile, strXPath)
' Get error bar type. This can be "minus", "plus", or "both" depending on which error bar(s) exist(s).
strXPath = "/c:chartSpace/c:chart//c:ser[c:tx//c:v[text()='" & strSeriesName & "']]/c:errBars/c:errBarType/#val"
strErrBarType = GetXPathFromXMLFile(strXMLFile, strXPath)
' Get the Range address for either the "minus" or "plus" error bar and set it to the final result.
If strErrValType = "cust" Then
strXPath = "/c:chartSpace/c:chart//c:ser[c:tx//c:v[text()='" & strSeriesName & "']]/c:errBars"
If PlusMinus = enErrorBarMinus And (strErrBarType = "both" Or strErrBarType = "minus") Then
strXPath = strXPath & "/c:minus/c:numRef/c:f"
ElseIf PlusMinus = enErrorBarPlus And (strErrBarType = "both" Or strErrBarType = "plus") Then
strXPath = strXPath & "/c:plus/c:numRef/c:f"
EndIf
strErrBarFormula = GetXPathFromXMLFile(strXMLFile, strXPath)
strErrBarFormula = "'[" & oWb.Name & "]" & Replace$(strErrBarFormula, "!", "'!")
Set rngResult = Application.Range(strErrBarFormula)
End If
Set GetErrorBarRange = rngResult
CleanUp:
' Delete the temporary extracted XML data
With moFSO
If .FolderExists(strTempDir) Then .DeleteFolder strTempDir
End With
Set moFSO = Nothing
' Free the cursor
Application.Cursor = xlDefault
End Function
' Get the value of an XML node by an XPath search string
Private Function GetXPathFromXMLFile(ByVal strXMLFile As String, ByVal strXPath As String) As String
Dim objXMLDoc As Object
Dim strNS As String
Dim objXMLNode As Object
' Load the XML file
Set objXMLDoc = CreateObject("MSXML2.DOMDocument.6.0")
objXMLDoc.Load strXMLFile
' These are all the XML namespaces related to the current task
strNS = Join$(Array( _
"xmlns:x=""http://schemas.openxmlformats.org/spreadsheetml/2006/main""", _
"xmlns:r=""http://schemas.openxmlformats.org/officeDocument/2006/relationships""", _
"xmlns:mc=""http://schemas.openxmlformats.org/markup-compatibility/2006""", _
"xmlns:x14ac=""http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac""", _
"xmlns:xr=""http://schemas.microsoft.com/office/spreadsheetml/2014/revision""", _
"xmlns:xr2=""http://schemas.microsoft.com/office/spreadsheetml/2015/revision2""", _
"xmlns:xr3=""http://schemas.microsoft.com/office/spreadsheetml/2016/revision3""", _
"xmlns:rel=""http://schemas.openxmlformats.org/package/2006/relationships""", _
"xmlns:xdr=""http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing""", _
"xmlns:a=""http://schemas.openxmlformats.org/drawingml/2006/main""", _
"xmlns:c=""http://schemas.openxmlformats.org/drawingml/2006/chart""", _
"xmlns:c16r2=""http://schemas.microsoft.com/office/drawing/2015/06/chart""" _
))
objXMLDoc.SetProperty "SelectionLanguage", "XPath"
objXMLDoc.SetProperty "SelectionNamespaces", strNS
objXMLDoc.resolveExternals = True
' Select the XML node and return its text value
Set objXMLNode = objXMLDoc.SelectSingleNode(strXPath)
If Not objXMLNode Is Nothing Then
GetXPathFromXMLFile = objXMLNode.Text
End If
End Function
' If workbook path is a OneDrive URL or a network share URL then resolve it to a local path with a drive letter
Private Function LocalFilePath(ByVal strFilePath As String)
strFilePath = OneDriveLocalFilePath(strFilePath)
strFilePath = NetworkLocalFilePath(strFilePath)
LocalFilePath = strFilePath
End Function
' If workbook path is a OneDrive URL then resolve it to a local path with a drive letter
Private Function OneDriveLocalFilePath(ByVal strFilePath As String) As String
Dim strOneDrivePath As String
Dim strLocalPath As String
If strFilePath Like "*my.sharepoint.com*" Then
strOneDrivePath = Environ$("OneDriveCommercial")
If strOneDrivePath = vbNullString Then strOneDrivePath = Environ$("OneDrive")
strLocalPath = strOneDrivePath & "/" & Split(strFilePath, "/", 7)(6)
OneDriveLocalFilePath = Replace$(strLocalPath, "/", "\")
ElseIf strFilePath Like "*d.docs.live.net*" Then
strOneDrivePath = Environ$("OneDriveConsumer")
If strOneDrivePath = vbNullString Then strOneDrivePath = Environ$("OneDrive")
strLocalPath = strOneDrivePath & "/" & Split(strFilePath, "/", 5)(4)
OneDriveLocalFilePath = Replace$(strLocalPath, "/", "\")
Else
OneDriveLocalFilePath = strFilePath
End If
End Function
' If workbook path is a network share URL then resolve it to a local path with a drive letter
Private Function NetworkLocalFilePath(ByVal strFilename As String) As String
On Error Resume Next
Dim ShellScript As Object
Dim i As Long
Dim strDriveLetter As String * 1
Dim strRemotePath As String
Set ShellScript = CreateObject("WScript.Shell")
For i = 97 To 122 ' a to z
strDriveLetter = Chr$(i)
strRemotePath = ShellScript.RegRead("HKEY_CURRENT_USER\Network\" & strDriveLetter & "\RemotePath")
If Err.Number = 0 Then
If strFilename Like strRemotePath & "*" Then
NetworkLocalFilePath = Replace$(strFilename, strRemotePath, UCase$(strDriveLetter) & ":", Count:=1)
Exit Function
End If
Else
Err.Clear
End If
Next i
NetworkLocalFilePath = strFilename
End Function
' Extract workbook XML to temporary directory
Private Function ExtractWorkbookXMLToTemp(oWb As Workbook) As String
Dim strTempDir As String
Dim strExt As String
Dim strTempWb As String
Dim strWbLocal As String
Dim strZipFile As String
On Error GoTo CleanUp
' Create a temporary copy of the workbook
With moFSO
strTempDir = .BuildPath(Environ$("TEMP"), _
Replace$(.GetTempName, ".tmp", vbNullString))
strExt = .GetExtensionName(oWb.Name)
strTempWb = strTempDir & "." & strExt
strWbLocal = LocalFilePath(oWb.FullName)
.CopyFile strWbLocal, strTempWb
End With
' Rename the temporary copy from .xls_ to .zip
strZipFile = strTempDir & ".zip"
Name strTempWb As strZipFile
' Unzip the .zip file to a temporary folder
MkDir strTempDir
UnzipFiles strZipFile, strTempDir
' Return the name of the temporary directory
ExtractWorkbookXMLToTemp = strTempDir
CleanUp:
' Delete the temporary ZIP file
With moFSO
If .FileExists(strZipFile) Then .DeleteFile strZipFile
End With
End Function
' Unzip all the files in 'varZipFile' into the folder 'varDestDir'
Private Sub UnzipFiles(ByVal varZipFile As Variant, ByVal varDestDir As Variant)
Dim oShellApp As Object
Const NO_PROGRESS_DIALOG As Integer = &H4
Set oShellApp = CreateObject("Shell.Application")
If Not varDestDir Like "*\" Then varDestDir = varDestDir & "\"
With oShellApp
.Namespace(varDestDir).CopyHere .Namespace(varZipFile).Items, NO_PROGRESS_DIALOG
End With
On Error Resume Next
With oShellApp
Do Until .Namespace(varZipFile).Items.Count = .Namespace(varDestDir).Items.Count
Application.Wait Date + (VBA.Timer + 1!) / 86400
Loop
End With
On Error GoTo 0
End Sub

Resources