I am by no means an expert in Python but have reasonable familiarity with Access and Excel VBA. If I knew Python better and earlier in my amateur programming career I would never have attempted to do what I set out to do below using any VBA code at all. Given the investment in time in VBA code I had to find a way....
I have spent the last several days/week trying to find a way to talk Python->Access VBA with the following requirements:
- call Access VBA function from Python
- send parameter to Access VBA function from Python
- return value from Access VBA function to Python
I made attempts with clr using pythonnet and IronPython and found more and more confusing and unclear error messages and exceptions. I tried the above suggested DoCmd.RunMacro method but Access macros do not return values. Tried a new(ish) version of Access macros (2010) called data macros which do have an action called SetReturnVar but they do not talk to VBA unless through the more traditional macros and as I mentioned above, traditional macros do not return values. Today I read some Microsoft documentation (Access Application.Run method) more carefully:
Access Application.Run method
I am not completely aware of the implications of that statement "you can't set a reference to an individual Microsoft Access database from any application other than Microsoft Access" but it occurred to me that a lot of the Python<->Office Application articles seemed more successful when talking Python<->Excel VBA. I reasoned that because I had been able to run Excel VBA <-> Access VBA in the past and if Python <-> Excel VBA worked as well as I had read, then a solution (albeit convoluted) seemed possible (I think the proper programmers call it a hack).
After about 1 1/2 hours of cutting/pasting code snippets and debugging:
Python
from win32com.client import Dispatch
FILELOC = r'C:\Users\Desktop\PyExcel.xlsm'
PROGNAME='Excel.Application'
num = 4
#open excel workbook containing VBA code
#...could do more to ensure excel isn't already running
xl = Dispatch(PROGNAME)
xl.Visible = True
#open excel file containing the VBA code
#...could do more to check if file is already open, etc
xl.Workbooks.Open(Filename=FILELOC)
#call to VBA code within excel
rtrn_int = xl.Run("RunCOMObject", num)
#print return value
print(rtrn_int)
#Quit excel-this doesn't work very well and there are articles about
#Python or the COM object not being able to actually remove Excel
#from the task manager
xl.Quit()
Excel VBA
Option Explicit
Private Const ACCESS_FILELOC As String = "C:\Users\Desktop\Test.accdb"
Private Const TEMP_FILELOC As String = "C:\Users\Desktop\TestTemp.accdb"
Function RunCOMObject(intNum As Integer) As Integer
Dim objAcc As Object, objProject As Object
Dim accAppl As Access.Application
Dim MyAppl As String
MyAppl = "Access.Application"
If Not IsRunning(MyAppl) Then 'Access not running, simply start
'up Access and open file
Set accAppl = CreateObject(MyAppl) 'start Access
accAppl.Visible = True
accAppl.OpenCurrentDatabase (ACCESS_FILELOC) 'open file
Else: 'Access is running
On Error Resume Next
Set accAppl = GetObject(, MyAppl) 'assign the running application
'to a variable
On Error GoTo Err_File_Open 'use an error in attempting to rename
'the database of interest to determine if the open file is the
'desired file
Name ACCESS_FILELOC As TEMP_FILELOC 'rename the file of interest
Name TEMP_FILELOC As ACCESS_FILELOC 'file was successfully renamed
'therefore not open
Call NoFileOrOther(accAppl, MyAppl)
End If
Err_File_Open:
'Required Access file is open
RunCOMObject = accAppl.Run("TestLink", intNum) 'run the VBA function in
'Access
accAppl.CloseCurrentDatabase 'close database
accAppl.Quit 'quit Access
Set accAppl = Nothing
End Function
Function IsRunning(ByVal MyAppl As String) As Boolean
Dim applRef As Object
On Error Resume Next 'error occurs if GetObject is unable to find a
'running version of the application
Set applRef = GetObject(, MyAppl) 'attempt to obtain the required
'application object
If Not applRef Is Nothing Then 'if application is already running
Set applRef = Nothing
IsRunning = True
Else 'application is not running
IsRunning = False
End If
Set applRef = Nothing
End Function
Sub NoFileOrOther(accAppl As Access.Application, MyAppl As String)
On Error GoTo Err_No_FileOpen
If accAppl.CurrentProject.Name <> "" Then 'Access active with another a
'different file open
Set accAppl = CreateObject(MyAppl) 'start a new instance of Access
accAppl.Visible = True
accAppl.OpenCurrentDatabase (ACCESS_FILELOC) 'open file
End If
Exit Sub
Err_No_FileOpen:
accAppl.OpenCurrentDatabase (ACCESS_FILELOC) 'in the event of Access
'being active without a database open
End Sub
Access VBA
This is a completely trivial example and belies the amount of code I had already written in Access VBA to warrant this workaround but it serves to demonstrate the methodology
Option Compare Database
Option Explicit
Function TestLink(intNum As Integer) As Integer
TestLink = intNum + 10
End Function
Python Output:
14
Success!!!! The number was initially 4 in Python and was sent as a parameter to Excel and Access where it had 10 added to it before being returned via Excel to Python print(rtrn_int) = 14.
If anyone knows how to definitively (ie through similar rigour demonstrated above) send arguments from Python -> Access VBA and return a value to Python without using Excel VBA as an intermediary I would be very happy to hear from you. Alternatively methods using pythonnet referencing clr would be similarly appreciated.