Global Event for multiple types - vba

Is it possible to write something like global event listener? I would like to have a listener for more objects (TextBox, CheckBox, OptionButton, Label, ...). and have the listener in my class. I have some normal events, so I my idea looks like this:
Public WithEvents eventGlobLst As <DontKnowWhat>SomeType</DontKnowWhat>
Sub setListener(controlObj As SomeType)
Set eventGlobList = controlObj
End Sub
From my run-method I'm calling sub which sets the listener
For Each pages In csDialgog.MultiPage.Pages
For Each objectControl In pages.Controls
Set eventClass = New ControlsClass
eventClass.setListener objectControl
universalObjectCollection.Add eventClass
Next
Next
This works fine with classic events. Finally I have some event handler:
Private Sub EventGlobLstnr_AfterUpdate()
Functions.GlobalChange
End Sub
I would like to know if exist some ancestor of all objects which I can use. Or I must write the listener for every type separately and set them same GlobalChange.

The answer is no, you're probably better off with self-writing code. Here's a good link that allows you to write into VBA editor
Programming the VBA editor - Chip Pearson
This code was taken from Chip Pearson:
Creating An Event Procedure
This code will create a Workbook_Open event procedure. When creating an event procedure, you should use the CreateEventProc method so that the correct procedure declaration and parameter list is used. CreateEventProc will create the declaration line and the end of procedure line. It returns the line number on which the event procedure begins.
Sub CreateEventProcedure()
Dim VBProj As VBIDE.VBProject
Dim VBComp As VBIDE.VBComponent
Dim CodeMod As VBIDE.CodeModule
Dim LineNum As Long
Const DQUOTE = """" ' one " character
Set VBProj = ActiveWorkbook.VBProject
Set VBComp = VBProj.VBComponents("ThisWorkbook")
Set CodeMod = VBComp.CodeModule
With CodeMod
LineNum = .CreateEventProc("Open", "Workbook")
LineNum = LineNum + 1
.InsertLines LineNum, " MsgBox " & DQUOTE & "Hello World" & DQUOTE
End With
End Sub
Using .CreateEventProc, you can create an event for each elements that you want to "catch"
I believe this is the only way of achieving the goal you want.
Cheers,
kpark

Related

My macro saves PDF attachment from one sender/subject. How get it to handle multiple sender/subjects?

I have a code that can automaticaly move a PDF from a received message to a folder of my choice, but what I really need is in fact to be able to move a file to a specific folder depending of the sender.
The code below works for only one sender, How do I add more senders and more folder locations?
Private WithEvents Items As Outlook.Items
Private Sub Application_Startup()
Dim olApp As Outlook.Application
Dim objNS As Outlook.NameSpace
Set olApp = Outlook.Application
Set objNS = olApp.GetNamespace("MAPI")
Set Items = objNS.GetDefaultFolder(olFolderInbox).Items
End Sub
Private Sub Items_ItemAdd(ByVal item As Object)
On Error GoTo ErrorHandler
'Only act if it's a MailItem
Dim Msg As Outlook.MailItem
If TypeName(item) = "MailItem" Then
Set Msg = item
'Change variables to match need. Comment or delete any part unnecessary.
If (Msg.SenderName = "Marc, Test") And _
(Msg.Subject = "Heures") And _
(Msg.Attachments.Count >= 1) Then
'Set folder to save in.
Dim olDestFldr As Outlook.MAPIFolder
Dim myAttachments As Outlook.Attachments
Dim Att As String
'location to save in. Can be root drive or mapped network drive.
Const attPath As String = "C:\Users\NAEC02\Test\"
' save attachment
Set myAttachments = item.Attachments
Att = myAttachments.item(1).DisplayName
myAttachments.item(1).SaveAsFile attPath & Att
' mark as read
Msg.UnRead = False
End If
End If
ProgramExit:
Exit Sub
ErrorHandler:
MsgBox Err.Number & " - " & Err.Description
Resume ProgramExit
End Sub
Before answering your question, some comments on your existing code.
You are running this code within Outlook. You do not need olApp. You only need a reference to the Outlook application if you are trying to access your emails from Excel or some other Office product.
I am surprised how often I see On Error GoTo ErrorHandler because I have never found a use from this statement.
If I am coding for myself, I want execution to stop on the statement causing the problem so I can understand what is happening without guessing from the error message. If execution stops on the statement causing the error, I can restart the code if I can immediately fix the error.
If I am developing for a client, I want, at worst, a user-friendly message. Err.Number & " - " & Err.Description is not my idea of a user-friendly message. It does not even tell me which email caused the problem. For a client, I would have something like:
Dim ErrDesc as String
Dim ErrNum as Long
: : :
On Error Resume Next
Statement that might give an error
ErrNum = Err.Num
ErrDesc = Err.Description
On Error GoTo 0
If ErrNum <> 0 Then
Code to handle errors that can occur with
this statement in a user-friendly manner.
End If
Today Dim Att As String is fine because you remember what Att is. Will you remember when you update this macro in six or twelve months? Will a colleague updating this macro know what Att is? I would call it AttName or perhaps AttDsplName.
You say the code saves PDF attachments but you do not check for this. To a VBA macro, logos, images, signatures and other files are also attachments. Also you assume the attachment you wish to save is Attachments(1). If there are several attachments, the logos, images and signatures could come first.
You have:
'Set folder to save in.
Dim olDestFldr As Outlook.MAPIFolder
You do not set olDestFldr and you do not move the email to a different folder. Do you want to do this?
Now to your question. I have included the code for two methods of achieving your objective and I discuss another two methods. However, before showing you the code, I suspect I need to introduce you to Variants. Consider:
Dim A As Long
Dim B As String
Dim C As Double
Dim D As Variant
I have declared A to C as a long integer, a string and a double. These variables can never be anything else and must be used in accordance with the rules for their type. I can write A = A + 1 or A = A * 5. Providing the new value for A does not exceed the maximum value for a long integer, these statements are fine. But I cannot write A = "House" because "House" is not an integer. I can write B = "House" because "House" is a string. I can write B = "5" and then A = A + B because VBA will perform implicit conversions if it can. That is, VBA can convert string "5" to integer 5 and add it to A.
I can also write:
D = 5
D = D + A
D = "House"
D is a Variant which means it can hold any type of data. Here I assign 5 to D then add A so for these two statements, D is holding an integer. I then change my mind and assign a string to D. This is not very sensible code but it is valid code. D can hold much more than an integer and a string. In particular, it can hold an array. Consider:
ReDim D(0 To 2)
D(0) = "House"
D(1) = A + 5
D(2) = 3.7
Following the ReDim statement, it is as though D has been converted to an array and I use array syntax to access the elements of D. D(0) contains "House", D(1) contains 5 more than the current value of A and D(2) contains double 3.7.
I can achieve the same effect with:
D = Array("House", A + 5, 3.7)
I am sure you agree this is easier. Array is a function that can take a large number of parameters and returns a Variant array containing those parameters which I have assigned to D. I do not normally advise mixing types within a variant array since it is very easy to get yourself into a muddle. However, it is valid VBA and I have found it invaluable with particularly difficult problems. Normally, I would not use function Array, I would write:
D = VBA.Array("House", A + 5, 3.7)
With VBA.Array, the lower bound of the array is guaranteed to be zero. With Array, the lower bound depends on the Option Base statement. I have never seen anyone use the Option Base statement, but I do not like to risk having my code changed by someone adding this statement. Search for “VBA Option Base statement” to discover what this statement does.
The following code demonstrates my first method of achieving your objective:
Option Explicit
Sub Method1()
Dim DiscFldrCrnt As Variant
Dim DiscFldrs As Variant
Dim Inx As Long
Dim SenderNameCrnt As Variant
Dim SenderNames As Variant
Dim SubjectCrnt As Variant
Dim Subjects As Variant
SenderNames = VBA.Array("Doe, John", "Early, Jane", "Friday, Mary")
Subjects = VBA.Array("John's topic", "Jane's topic", "Mary's topic")
DiscFldrs = VBA.Array("DoeJohn", "EarlyJane", "FridayMary")
For Inx = 0 To UBound(SenderNames)
SenderNameCrnt = SenderNames(Inx)
SubjectCrnt = Subjects(Inx)
DiscFldrCrnt = DiscFldrs(Inx)
' Code to process SenderNameCrnt, SubjectCrnt and DiscFldrCrnt
Debug.Print SenderNameCrnt & " " & SubjectCrnt & " " & DiscFldrCrnt
Next
End Sub
If you copy this code to a module, you can run it and see what it does. If you work slowly through it, you should be able to understand what it is doing. Come back with questions if necessary but the more you can discover for yourself, the faster you will develop your own skills.
Note: the disc folders have names such as “DoeJohn”. I am assuming you would have something like "C:\Users\NAEC02\Test\" as a root folder and you would save the attachment to "C:\Users\NAEC02\Test\DoeJohn\".
I use this method when I have a small number of values I need to link. It relies on SenderNames(#), Subjects(#) and DiscFldrs(#) being associated. As the number of different combinations increase, it can be difficult to keep the three arrays in step. Method2 solves that problem.
Sub Method2()
Dim DiscFldrCrnt As Variant
Dim Inx As Long
Dim SenderNameCrnt As Variant
Dim SubjectCrnt As Variant
Dim TestValues As Variant
TestValues = Array("Doe, John", "John's topic", "John", _
"Early, Jane", "Jane's topic", "Jane", _
"Friday, Mary", "Mary's topic", "Mary")
For Inx = LBound(TestValues) To UBound(TestValues) Step 3
SenderNameCrnt = TestValues(Inx)
SubjectCrnt = TestValues(Inx + 1)
DiscFldrCrnt = TestValues(Inx + 2)
' Code to process SenderNameCrnt, SubjectCrnt and DiscFldrCrnt
Debug.Print SenderNameCrnt & " " & SubjectCrnt & " " & DiscFldrCrnt
Next
End Sub
Here I have placed all the values in a single array. If I want to add a new sender, I add another three elements to the end of the array which I find this easier to manage. For the code to process the three values, Method1 and Method2 are identical.
The principle disadvantage of Method2 compared with Method1 is that the total number of values is reduced. I like to see all my code so I do not like statements that exceed the width of the screen. This limits my lines to about 100 characters. I use the continuation character to spread the statement over several lines but there is a maximum of 24 continuation lines per statement. With Method1, I am spreading the values over three arrays and therefore three statements so I can have three times as many values. In practice this is not a real limit. Both Method1 and Method2 become too difficult to manage before the VBA limits are reached.
The real disadvantage of Method1 and Method2 is that every change requires the services of a programmer. If user maintenance is important, I use Method3 which reads a text file into arrays or Method4 which reads from an Excel worksheet. I have not included code for either Method3 or Method4 but can add one or both if you need this functionality. I find most users prefer a worksheet but those with a favourite text editor prefer a text file.
In the middle of both Method1 and Method2 I have:
' Code to process SenderNameCrnt, SubjectCrnt and DiscFldrCrnt
Debug.Print SenderNameCrnt & " " & SubjectCrnt & " " & DiscFldrCrnt
You need to replace these statements with a variation of your existing code. I have no easy method of testing the following code so it is untested but it should give you are start.
This is a new version of Items_ItemAdd designed to work with either of my methods.
Private Sub Items_ItemAdd(ByVal Item As Object)
Const DiscFldrRoot As String = "C:\Users\NAEC02\Test\"
' * There is no need to write Outlook.MailItem because (1) you are within Outlook
' and (2) there is no other type of MailItem. You only need to specify Outlook
' for folders since there are both Outlook and Scripting folders. Note:
' "Scripting" is the name of the library containing routines for disc folders.
' * Do not spread your Dim statements throughout your sub. There are languages
' where you can declare variables within code blocks but VBA is not one of those
' languages. With VBA, you can declare variables for an entire sub or function,
' for an entire module or for an entire workbook. If you spread your Dim
' statements out it just makes them hard to find and you are still declaring
' them at the module level.
Dim DiscFldrCrnt As Variant
Dim InxA As Long
Dim Msg As MailItem
Dim SenderNameCrnt As Variant
Dim SubjectCrnt As Variant
' You also need the arrays from whichever of Method1 or Method2 you have chosen
If TypeName(item) = "MailItem" Then
' Only interested in MailItems
Set Msg = Item
' Code from Method1 or Method2 with the code below in the middle
End If
End Sub
Insert the body of Method1 or Method2, whichever you chose, in the middle of the above code. Then insert the following code in the middle of that code.
With Msg
If .Attachments.Count = 0 Then
' Don't bother to check MailItem if there are no attachments
Else
If .Subject <> SubjectCrnt Then
' Wrong subject so ignore this MailItem
ElseIf .SenderName <> SenderNameCrnt Then
' Wrong sender name so ignore this MailItem
Else
' SenderName and Subject match so save any PDF attachments
For InxA = 1 to .Attachments.Count
If LCase(Right$(.Attachments(InxA).DisplayName, 4)) = ".pdf" Then
' Warning: SaveAsFile overwrites existing file with the same name
.Attachments(InxA).SaveAsFile DiscFldrRoot & DiscFldrCrnt & _
.Attachments(InxA).DisplayName
End If
End With
Next
End If
End With

Check if Private Sub Workbook.open() is empty

I have a user who needs to execute a macro after an Excel file was opened, and who needs to know (programatically) if the current Excel file's Private Sub Workbook.open() routine is empty.
Is there any way to keep this information in memory after the workbook is opened so that if the user needs to run his macro this information is available. Something along a persistent global var would be ideal. But i'm not sure if it's possible.
Thanks!
This code below (inside a regular module) loops through all the VB Project components (including ThisWorkbook module), and checks if the module name is "ThisWorkbook".
Once it finds "ThisWorkbook" module, it checks the total number of code lines inside that module, if it's 0, it raises a MsgBox that it's empty. If it's not, then it checks to see if it can find a "Workbook_Open" string inside the code. If it does, it counts the total number of lines (not empty lines) of code between the "Workbook_Open" line and the closest "End Sub" line.
Check_WorkBookModule_Contents Code
Option Explicit
Sub Check_WorkBookModule_Contents()
Const PROC_NAME = "ThisWorkbook"
Dim VBProj As VBIDE.VBProject
Dim VBComp As VBIDE.VBComponent
Dim CodeMod As VBIDE.CodeModule
Dim i As Long, j As Long, SubLinesCount As Long
Dim ModuleCodeLinesCount As Long
Set VBProj = ActiveWorkbook.VBProject
' loop through all modules, worksheets and other objects in VB Project
For Each VBComp In VBProj.VBComponents
Set CodeMod = VBComp.CodeModule
Debug.Print CodeMod.Name ' <-- for debug
If CodeMod.Name Like PROC_NAME Then ' <-- check if module name is "ThisWorkbook"
' if total of code lines in "ThisWorkbook" module is empty
If CodeMod.CountOfLines = 0 Then
MsgBox CodeMod.Name & " module is empty"
Exit Sub
End If
SubLinesCount = 0 ' reset counter
' loop through all code lines inside current module
For i = 1 To CodeMod.CountOfLines
If Len(CodeMod.Lines(i, 1)) > 0 Then
' if the name of current sub is found within the current code line
If CodeMod.Lines(i, 1) Like "*Workbook_Open*" Then
For j = i + 1 To CodeMod.CountOfLines
If Len(CodeMod.Lines(j, 1)) > 0 And Not CodeMod.Lines(j, 1) Like "End Sub*" Then
SubLinesCount = SubLinesCount + 1
End If
Next j
If SubLinesCount > 0 Then
MsgBox CodeMod & " module, has an event of 'Workbook_Open' , with total of " & SubLinesCount & " lines of code"
Exit Sub
Else
MsgBox CodeMod & " module, has an event of 'Workbook_Open' , but it's empty !"
Exit Sub
End If
End If
End If
Next i
End If
Next VBComp
End Sub
Note: In order to access the VB Project Module, you need to follow the 2 steps below:
Step 1: Add "Trust access to the VBA project object model" , go to Developer >> Macro Security >> then add a V to the Trust access to the VBA project object model.
Step 2: Add Reference to your VB project, add "Microsoft Visual Basic for Applications Extensibility 5.3"

VBA procedure cannot save file including dynamically created procedure

I am creating a series of files automatically, although these files are yet only drafts I have to show them to relevant managers. So I want to be sure that they understand files they get are only first drafts.
I tried to include code into "Open, Workbook" event for each new created file using code from http://www.cpearson.com/Excel/VBE.aspx to show messagebox with some warning for the managers:
Sub CreateEventProcedure(wb, code)
Dim VBProj As VBIDE.VBProject
Dim VBComp As VBIDE.VBComponent
Dim CodeMod As VBIDE.CodeModule
Dim LineNum As Long
Const DQUOTE = """" ' one " character
Set VBProj = wb.VBProject
Set VBComp = VBProj.VBComponents("ThisWorkbook")
Set CodeMod = VBComp.CodeModule
With CodeMod
LineNum = .CreateEventProc("Open", "Workbook")
LineNum = LineNum + 1
.InsertLines LineNum, " MsgBox " & DQUOTE & code & DQUOTE
End With
End Sub
When I am running the script I see, while the screen is updating or during debugging, that it creates the code correctly without an error...However when i open any of the newly created files the code is not there (i.e. the messagebox is not shown when the file is opened). Just fyi, I save files as .xlsm (macro-enabled)
wb.SaveAs Filename:=file_path, FileFormat:=xlOpenXMLWorkbookMacroEnabled
My tip is that somehow the files are saved without a VBA part...
Can you please help me?
So your file doesnt contain any vba code at all? I think you are overcomplicating it or im just missing the intention but I always just use something like:
Private Sub Workbook_Open()
MsgBox "oi managers this is a draft"
End Sub

Import lines of code

Can we read scripts or lines of code to a module in vba? Like we have the include function in php.
For example:
We store this in Excel somewhere and call the range as xyz
line 1 of code
line 2 of code
line 3 of code
Then while running a macro we call this like
Sub my_macro()
xyz
End Sub
Basically I want to run a few lines of code repetitively but don't want to create another macro and pass the parameters.
This can be done using the Microsoft Visual Basic for Applications Extensibility 5.3 (VBIDE) library. There's some great examples at CPearson.com. I typically use this to insert snippets of code while I'm developing. I would personally be uncomfortable executing code stored in an excel sheet, but I tested this and it does work.
My worksheet:
A
1 MsgBox "I'm a test."
2 MsgBox "So am I."
I set up an empty subroutine that we will then insert into from the excel sheet.
Private Sub ProcToModify()
End Sub
And the subroutine that will actually insert the code into ProcToModify:
Sub ModifyProcedure()
Dim VBProj As VBIDE.VBProject
Dim VBComp As VBIDE.VBComponent
Dim CodeMod As VBIDE.CodeModule
Dim StartLine As Long
Dim NumLines As Long
Dim ProcName As String
Set VBProj = ActiveWorkbook.VBProject
Set VBComp = VBProj.VBComponents("Module1") ' specify module to modify
Set CodeMod = VBComp.CodeModule
Dim ws As Worksheet
Dim rng As Range
Dim cell As Range
Set ws = ThisWorkbook.ActiveSheet 'change this accordingly
Set rng = ws.Range("A1:A2") 'and this
For Each cell In rng
ProcName = "ProcToModify"
With CodeMod
StartLine = .ProcStartLine(ProcName, vbext_pk_Proc)
NumLines = .ProcCountLines(ProcName, vbext_pk_Proc)
.InsertLines StartLine + NumLines - 2, cell.Value 'insert each line at the end of the procedure to get them in the correct order.
End With
Next cell
End Sub
Called at runtime like this:
Public Sub main()
ModifyProcedure
ProcToModify
End Sub
One Big Gotchya:
Before running this code, you need to go to Excel>>File>>Options>>Trust Center>>Trust Center Settings>>Macro Settings and check the "Trust access to the VBA project object model".
I would imagine that's because allowing access to the project object is a fairly concerning security risk.
From the cpearson.com site I linked to earlier:
CAUTION: Many VBA-based computer viruses propagate themselves by
creating and/or modifying VBA code. Therefore, many virus scanners may
automatically and without warning or confirmation delete modules that
reference the VBProject object, causing a permanent and irretrievable
loss of code. Consult the documentation for your anti-virus software
for details.

Programmatically set DLL search path in VBA macro

The problem
I have a word template which uses VBA's Declare statement to link to a dll, whose path can be determined within the VBA macro
I want to delploy this to the users %APPDATA%\Microsoft\Word\STARTUP directory
I DON'T want to permanently change the user's PATH environment variable (temporarily would be OK, but this doesn't seem to work as they don't get refreshed until application restart)
Attempted solution
I tried dynamically adding the code with the Declare statements using ThisDocument.VBProject.CodeModule.AddFromString(code) which works when loading the template from a normal directory, but when the template is within Word\STARTUP, it gives the following error:
Run-time error '50289':
Can't perform operation since the
project is protected.
And setting the registry key "HKEY___LOCAL_MACHINE\Software\Microsoft\Office\11.0\Word\Security\AccessVBOM" to 1 doesn't fix this when the template is in Word\STARTUP
I'm really struggling to find a solution. If anyone knows a way to do this, that would be great.
Frankly, I don't know what's the problem with using all those VBA code injection, assembly generation for LoadLibrary() calls, etc techniques that I've seen used for this simple task. In my project I use simple code to load dll from the same location as the workbook, like this:
Declare Function MyFunc Lib "MyDll.dll" (....) As ...
Sub Test()
....
ChDir ActiveWorkbook.Path
... = MyFunc(....)
End Sub
Excel 2003 at least, has no problem loading the dll from the current path, Set ChDir to whatever path your DLL has. You might also need to change your current drive which is separate from current path. You have to do it only once, before the first function call, after it the DLL stays attached no matter where your current path is, so you may do it once in workbook_open and not bother about the path later. I provide an empty dummy function in the DLL just for this pupose. I don't think MS Word is any different on this.
Private Declare Sub Dummy Lib "MyDLL.dll" ()
Private Sub Workbook_Open()
ChDrive Left$(Me.Path, 1)
ChDir Me.Path
Dummy
End Sub
You can use LoadLibrary api.
For example in my projects the code looks like this:
If LibraryLoaded() Then
Call MyFunc ...
End If
Public Function LibraryLoaded() As Boolean
Static IsLoaded As Boolean
Static TriedToLoadAlready As Boolean
If TriedToLoadAlready Then
LibraryLoaded = IsLoaded
Exit Function
End If
Dim path As String
path = VBAProject.ThisWorkbook.path
path = Left(path, InStrRev(path, "\") - 1)
IsLoaded = LoadLibrary(path & "\bin\" & cLibraryName)
TriedToLoadAlready = True
LibraryLoaded = IsLoaded
End Function
There is another really really ugly solution, but this blogger figured it out, and I can't figure out any other way:
http://blogs.msdn.com/pranavwagh/archive/2006/08/30/How-To-Load-Win32-dlls-Dynamically-In-VBA.aspx
Basically, you write a procedure that creates a code module in VBA during runtime. This module must create a reference to the dll and it must create a dummy function (or procedure) as part of this module that calls the dll. Then, from your code, you use Application.Run(dummyfunction(), arg1, arg2...). This is necessary because otherwise, the project will not compile because dummyfunction isn't yet a function.
You'll notice in his code, he uses InputBox() to get the location of the .dll but obviously you could get the location from a range in the spreadsheet. The following code snippet may be useful.
Dim cm As CodeModule
Dim vbc As VBComponent
Set cm = Application.VBE.ActiveVBProject.VBComponents.Add(vbext_ct_StdModule).CodeModule
cm.AddFromString (decString & funcString)
cm.Name = "MyNewModule"
Set vbc = cm.Parent
Application.VBE.ActiveVBProject.VBComponents.Remove vbc
'decString' and 'funcString' were just strings I constructed like his 'ss'. The snippet shows how you can rename the code module so that you could delete it later if needed. Obviously, this just deletes it right after it is created, and you probably wouldn't want to do that, but at least it shows you how it would be done.
Having said all that, we mostly just write .exe's now and shell out. If you need VBA to wait on the shell to finish, there are solutions for that issue as well.
Here's what I ended up doing, using Pranav Wagh's methodology linked above and code from C Pearson's site (http://www.cpearson.com/excel/vbe.aspx). This code prompts the user to select the path to the dll using an Open File window, builds a new module with a Declare Function with the inputted path and a function to execute a handshake with the dll. The purpose-built function in the dll returns a 1 if successful:
Public rtn As Integer
Sub LinkToDll()
Dim path As String, default As String
MsgBox "Select Geo_DLL.dll file from next window"
With Application.FileDialog(msoFileDialogOpen)
.AllowMultiSelect = False
.Title = "Select Geo_DLL.dll file"
If .Show = True Then
path = .SelectedItems(1)
End If
End With
'Add a module
Dim VBProj As VBIDE.VBProject
Dim VBComp As VBIDE.VBComponent
Set VBProj = ActiveWorkbook.VBProject
Set VBComp = VBProj.VBComponents.Add(vbext_ct_StdModule)
VBComp.Name = "LinkModule"
'Add procedure to module
Dim CodeMod As VBIDE.CodeModule
Dim LineNum As Long
Set VBComp = VBProj.VBComponents("LinkModule")
Set CodeMod = VBComp.CodeModule
With CodeMod
LineNum = .CountOfLines + 1
.InsertLines LineNum, "Declare Function RegDll Lib " & Chr(34) & path & Chr(34) & " (ByRef rtn As Integer)"
LineNum = LineNum + 1
.InsertLines LineNum, "Sub runthisfunc(rtn)"
LineNum = LineNum + 1
.InsertLines LineNum, "On Error Resume Next"
LineNum = LineNum + 1
.InsertLines LineNum, "rtn = 0"
LineNum = LineNum + 1
.InsertLines LineNum, "RegDll rtn"
LineNum = LineNum + 1
.InsertLines LineNum, "If rtn = 1 Then MsgBox (" & Chr(34) & "DLL linked" & Chr(34) & ")"
LineNum = LineNum + 1
.InsertLines LineNum, "If rtn = 0 Then MsgBox (" & Chr(34) & "DLL not found" & Chr(34) & ")"
LineNum = LineNum + 1
.InsertLines LineNum, "End Sub"
End With
'This is what CodeMod.InsertLines is writing:
'--------------------------------------------
'Declare Function RegDll Lib "C:\path\Geo_DLL.dll" (ByRef rtn As Integer)
'Sub runthisfunc(rtn)
'On Error Resume Next
'rtn = 0
'RegDll rtn
'If rtn = 1 Then MsgBox ("DLL Linked")
'If rtn = 0 Then MsgBox (DLL not found")
'End Sub
Application.Run "runthisfunc", rtn
'Delete Module
VBProj.VBComponents.Remove VBComp
End Sub
However, once I turned the workbook (xlsm) into an addin (xlam) I found that Excel wouldn't let the macro create new modules so my LinkToDll wouldn't work. The fix was to put the Declare Function back into LinkToDll with just the dll file name ("Geo_DLL.dll") as the Lib along with the runthisfunc sub. I found having the user simply select the dll file via the Open File window was enough to point Excel to the dll even with only the file name in the Lib portion of the Declare Function statement.
Chris
In my case code below worked. I added "ChDir (ThisWorkbook.Path)"
after function. But I tested it only on my laptop. I don't know if it works on network.
Option Explicit
' Declare the function that is in the DLL
Private Declare PtrSafe Function suntransitForEXL Lib _
"sampadll.dll" (ByRef lat As Double, ByRef lon As Double, ByRef dy As Integer, ByRef mnt As Integer, ByRef yr As Integer, ByRef tmz As Double) As Double
' use function on worksheet
Function noon(latitude As Double, longtitude As Double, day As Integer, month As Integer, year As Integer, timezone As Double) As Double
ChDir (ThisWorkbook.Path) ' Set working directory to current.
Dim decimaltime As Double
Dim hour As Integer
Dim minute As Integer
Dim second As Integer
decimaltime = suntransitForEXL(latitude, longtitude, day, month, year, timezone)
hour = Fix(decimaltime)
minute = Fix((decimaltime - hour) * 60)
second = Fix(((decimaltime - hour) * 60 - minute) * 60)
noon = TimeSerial(hour, minute, second)
End Function