Writing VBA procedure/function to an .xlam addin - vba

I have created a VBA project in Excel and within said project is a module that writes code to another module. I eventually password protected the project so as to keep the code hidden (not realizing right away that this would pose some pretty obvious issues).
When the user interacts with the file, the module attempts to run the code but, encounters an error (a password protected project can't modify itself! Hey we all have our duh moments :p).
So, as a way around this, I figured 'why not make the portion that is edited unprotected', whilst keeping the remainder of the file locked. So I figured I would create an unprotected .xlam add-in, then write said code to this portion. I am having some difficulty figuring how to do this though however. Below is the code I have written that writes code to another module:
Public Sub errorWrite()
Dim VBComp As VBIDE.VBComponent
Dim CodeMod As VBIDE.CodeModule
Dim s As String
Dim d As String
Dim lineNumb As Long
With ThisWorkbook.VBProject.VBComponents.item("NewModule")
.CodeModule.InsertLines j, "Public sub newModule()"
j = j + 1
With ThisWorkbook.VBProject.VBComponents.item("NewModule")
.CodeModule.InsertLines j, "(my code etc..)"
j = j + 1
With ThisWorkbook.VBProject.VBComponents.item("NewModule")
.CodeModule.InsertLines j, "end sub"
end sub
While I have found some info on Stack and elsewhere regarding .xlam add-ins (Updating an xlam add-in using VBA) for example, I'd rather just keep it as simple as possible. Suggestions and hints are much appreciated. Thanks!

Using an unprotected Addin seems like a reasonable approach. But the code you have posted writes to another Module within the Protected addin. You need to reference the other AddIn as a VBProject
Change the name of your Unprotected AddIn to a unique Name (it defaults to VBAProject and does not have to be unique)
Then create a reference to it like this
Dim VBP As VBProject
Set VBP = ThisWorkbook.VBProject.VBE.VBProjects("NameOfYourUnprotectedAddin")
Then all your code uses this reference instead of ThisWorkbook.VBProject
Eg
With VBP.VBComponents.item("NewModule")
.CodeModule.InsertLines j, "Public sub newModule()"
' etc
End With

so I was able to find a way that seems to work, I wrote the code using FSO, then save the file as a .bas module. From there I used the below to have the module imported directly into the .xlam addin as a module:
Public Sub importBas()
Dim VBP As VBProject
Set VBP = Workbooks("example.xlam").VBProject
VBP.VBComponents.Import ("C:\Users\...example.bas")
VBP.VBComponents.Remove
End Sub
Then, in the original module, I deleted procedure within the xlam after execution was finished. Therefore no code can be seen (assuming I remember to use error-handling and disable break mode :p)
I am still curious how to write it 'directly', so I will play around with it more and see if I get it, although this way works
Thank you :)

Related

Import source from txt or bas file

I want to store source code of VBA macro in .bas or .txt file and run it when user runs macro. I have macro that is used by multiple people, and I would like to store file on server to prevent them to use older versions of same macro. I found following line in stackoverflow and placed it in module code that should import code
'Library should be turned on Microsoft Visual Basic for Applications Extensibility 5.3
Option Explicit
Sub main()
Dim VBPrj As VBIDE.VBProject
Dim VBCom As VBIDE.VBComponent
Set VBPrj = Application.VBE.ActiveVBProject
Set VBCom = VBPrj.VBComponents("Module1")
VBCom.CodeModule.AddFromFile ("C:\Users\lietu\OneDrive\Documents\tests\Module1.txt")
End Sub
then I created txt file with following code in right location
Attribute VB_Name = "Module1"
Sub main()
MsgBox "Hello World"
End Sub
What I'm doing wrong?
It is not possible to execute code in a text file as a VBA "macro". The programming language/environment/interface simply does not work that way - no ifs, ands or buts - no workarounds.
The content of a text or bas file must be imported into a VBA project, using code similar to what's in the question.
In order to be able to use the VB Extensibility libraries that this code depends on, a specific security setting in the host Office application must be disabled, making this kind of approach unreliable, at best. The setting cannot be disabled using code, for understandable reasons.
Indeed, the approach proposed in the question would be a massive security risk...

Need code help on calling a macro from a new VTSO addin for Word

I have created a new addin with a ribbon in MVS. On click of button1 I want to run a macro that is stored in a .dotm file in the Startup folder in Word. The .dotm file is called MyMacros and the macro is titled "TableMacro".
The module name in Word is titled NewMacros and the top rows of the macro in Word are:
Sub TableMacro()
`
` TableMacro
I am sure the macro is started with the code below but even this is guess:
Private Sub Button1_Click_1(sender As Obeject, e As RibbonControlEventArgs) Handles Button1.Click
`code to call TableMacro'
End Sub
I know how to write macros but I have no idea the code needed to trigger the macro stored in the MyMacros.dotm file.
To search all global templates, including the Building Block template, from a VSTO add-in, you can use this:
Dim wApp = Globals.ThisAddIn.Application
Dim i As Integer, Tmplt As Word.Template = Nothing
For i = 1 To wApp.Templates.Count
If wApp.Templates(i).Name = "MyMacros.dotm" Then
Tmplt = wApp.Templates(i)
wApp.Run(Tmplt.Name & "!TableMacro")
End If
Next
The value of performing it this way is you now have an object variable set to a specific global template and you can then get at AutoText, Styles, etc. and of course macros that are stored in that specific global template.
Your VSTO code has a Microsoft.Office.Interop.Word.Application object. Say you're storing that reference in a variable named hostApp, you could do this:
hostApp.Run("TableMacro")
That requires the .dotm file to be the "active" document. If the document isn't active and you have a reference to it (say, theDocument), I think this might work (untested):
hostApp.Run(theDocument.Name & "!TableMacro")
The object VB.NET uses is the same one VBA uses, so if Application.Run "MyMacros!TableMacro" works in VBA, it will work in VB.NET. I'd try to fiddle in VBA first to get the syntax right - you get instant feedback, vs needing to build and launch the host, load the add-in and test the thing with VSTO.
The following Run syntax worked for me from within a VSTO Add-in to run VBA code in a Template loaded as an add-in. It uses the module name plus the macro name.
Keep in mind that Run can only work with public subs...
Globals.ThisAddIn.Application.Run("Module1.TestPublicVarx")

VBA list of filepaths of linked objects in document

I have a number of large Microsoft Word documents with many linked files from many Microsoft Excel spreadsheets. When opening a Word document, even with the 'update linked files at open' option unchecked:
Word still checks each link at its source by opening and closing the relevant excel spreadsheet for each individual link (so for x number of links, even if from the same spreadsheet, Word will open and close the spreadsheet x times). This means opening documents takes a very long time.
I have found that documents open faster if the spreadsheets containing the source of linked objects are already open, so Word doesn't keep opening, closing, reopening them.
So far, the beginnings of a solution I have is to create a list of all the filepaths of the linked objects, done by following VBA code:
Sub TypeArray()
Dim List(), Path As String
Dim i, x As Integer
Dim s As InlineShape
Dim fso As FileSystemObject, ts As TextStream
Set fso = New FileSystemObject
Set ts = fso.OpenTextFile("C:\MyFolder\List.txt", 8, True)
With ts
.WriteLine (ActiveDocument.InlineShapes.Count)
End With
For Each s In ActiveDocument.InlineShapes
Path = s.LinkFormat.SourcePath & "\" _
& s.LinkFormat.SourceName
With ts
.WriteLine (Path)
End With
Next s
End Sub
'--------------------------------------------------------------------------------------
Private Sub WriteStringToFile(pFileName As String, pString As String)
Dim intFileNum As Integer
intFileNum = FreeFile
Open pFileName For Append As intFileNum
Print #intFileNum, pString
Close intFileNum
End Sub
'--------------------------------------------------------------------------------------
Private Sub SendFileToNotePad(pFileName As String)
Dim lngReturn As Long
lngReturn = Shell("NOTEPAD.EXE " & pFileName, vbNormalFocus)
End Sub
which works well, but can only be used after a document is already open, which defeats its purpose.
So, finally, my question(s) are these:
1) Is there a way to run this code (or any better, more efficient code - suggestions are welcome) before opening a Word document and waiting through the long process of checking each link at its source?
2) Is there a way to avoid all this and simply have Word not check the links when it I open a document?
Sorry for the long question, and thank you for the help!
If I am not wrong there should be Document_Open event according to msdn. This should actually be a before open document and should be fired before updating links (at least it in excel it is fired before calculation).
Try opening the files on document open. Then you will face another problem, and so when to close the files, but that is a much easier thing to do. (probably document_close event...)
EDITTED:
As comments state, this is too late. You can create a word opener (as a single app or as an addin). The logic basically is:
'1) on something_open run GetOpenFileName dialog
'2) before opening the real thing, open all files accompanied
'3) open the document itself
'4) close all files
'5) close the opener itself
This is not the most trivial way, but I use this logic for exampe to make sure, that my applications always runs in a fresh copy of excel etc. But I understand that this is a workaround rather then a solution.
If you are still looking for something on this front, I created the following in a combination of VBA and VB.NET (in VS 2010) to show what can be done quite easily using that system. If VB.NET is no use to you, sorry, but there are reasons why I don't really want to spend time on the pure VBA approach.
At present, it is a "console" application which means you'll probably see a box flash up when it runs, but also means that you are more likely to be able to create this app without VS if you absolutely had to (AFAICR the VB.NET /compiler/ is actually free). It just fetches the link info. (i.e. there's currently no facility to modify links).
The overview is that you have a small piece of VBA (say, in your Normal template) and you need an open document. The VBA starts a Windows Shell, runs the VB.NET program and passes it the full path name of the document you want to open.
The VB.NET program opens the .docx (or whatever) and looks at all the Relationships of type "oleObject" that are referenced from the Main document part (so right now, the code ignores headers, footers, footnotes, endnotes and anywhere else you might have a link)
The VB.NET program automates Word (which we know is running) and writes each link URL into a sequence of Document Variables in the active document. These variables are called "Link1", "Link2", etc. If there are no links (I haven't actually tested that path properly) or the program can't find the file, "Link0" should be set to "0". Otherwise it should be set to the link count.
The shell executes synchronously, so your VBA resumes when it's done. Then you either have 0 links, or a set of links that you can process.
The VBA is like this:
Sub getLinkInfo()
' the full path name of the program, quoted if there are any spaces in it
' You would need to modify this
Const theProgram As String = """C:\VBNET\getmaindocumentolelinks.exe"""
' You will need a VBA reference to the "Windows Script Host Object Model"
Dim oShell As WshShell
Set oShell = CreateObject("WScript.Shell")
' plug your document name in here (again, notice the double quotes)
If oShell.Run(theProgram & " ""c:\a\testdocexplorer.docx""", , True) = 0 Then
With ActiveDocument.Variables
For i = 1 To CInt(.Item("Link0").Value)
Debug.Print .Item("Link" & CStr(i))
Next
End With
Else
MsgBox "Attempt to retrieve links failed"
End If
End Sub
For the VB.NET, you would need the Office Open XML SDK (I think it's version 2.5). You need to make references to that, and Microsoft.Office.Interop.Word.
The code is as follows:
Imports System.Collections.Generic
Imports System.Linq
Imports System.Text
Imports System.IO
Imports System.Xml
Imports System.Xml.Linq
Imports DocumentFormat.OpenXml.Packaging
Imports Word = Microsoft.Office.Interop.Word
Module Module1
Const OLEOBJECT As String = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/oleObject"
Sub Main()
Dim s() As String = System.Environment.GetCommandLineArgs()
If UBound(s) > 0 Then
Dim wordApp As Word.Application
Try
wordApp = GetObject(, "Word.Application")
Dim targetDoc As Word.Document = wordApp.ActiveDocument
Try
Dim OOXMLDoc As WordprocessingDocument = WordprocessingDocument.Open(path:=s(1), isEditable:=False)
Dim linkUris As IEnumerable(Of System.Uri) = From rel In OOXMLDoc.MainDocumentPart.ExternalRelationships _
Where rel.RelationshipType = OLEOBJECT _
Select rel.Uri
For link As Integer = 0 To linkUris.Count - 1
targetDoc.Variables("Link" & CStr(link + 1)).Value = linkUris(link).ToString
Next
targetDoc.Variables("Link0").Value = CStr(linkUris.Count)
OOXMLDoc.Close()
Catch ex As Exception
targetDoc.Variables("Link0").Value = "0"
End Try
Finally
wordApp = Nothing
End Try
End If
End Sub
End Module
I originally wrote the .NET code as a COM object, which would be slightly easier to use from VBA, but significantly harder to set up on the .NET side and (frankly) much harder to modify & debug as you have constantly to close Word to release the references to the COM DLLs.
If you actually wanted to fix up the LINK paths, as far as I can tell, modifying them in the relationship records is enough to get Word to update the relevant LINK fields when it opens Word, which saves having to modify the XML code for the LINK fields as well. But that's another story...
I just found out that you can set/modify a DelayOleSrvParseDisplayName registry entry and a NoActivateOleLinkObjAtOpen registry entry to modify the global behaviour:
See http://support.microsoft.com/kb/970154
I also found that activedocument.fields can contain links to external objects (in my case, an Excel sheet).
Use this code to parse them:
for each f in activedocument.fields
debug.print f.code
next
And use activedocument.fields(FIELDNUMBER) to select each object, to figure out where it is in the document.
Maybe also activedocument.Variables and activedocument.Hyperlinks can contain links to external objects? (not in my case).

Calling an External VBA from VBScript

I am using a program called mathtype to pull some equation objects out of a word document. I've written code in VBA that works perfectly using their API, but I have to translate it to a VBScript file. I have looked all over google, but have not found any solution on how (If it is even possible) to call a VBA library from VBScript.
VBScript can't see the MathTypeSDK Objects/Functions.
If not possible, how would I encase the macro I need to run in a globally available word file and call it from the VBScript?
Edit: Got it! Unfortunately the approaches below, while helpful, did not work for my situation. I found something closer: Embedding the macro in a global file and calling it through the Word Objects Run command.
objWord.Run "Normal.NewMacros.RunMain"
Here is an approach which might work for you. I tested this simple example.
Class "clsTest" in file "Tester.docm":
Public Sub Hello()
MsgBox "Hello"
End Sub
Class "Instancing" is marked "PublicNotCreatable".
Module in "Tester.docm":
Public Function GetClass() As clsTest
Set GetClass = New clsTest
End Function
In your vbscript:
Dim fPath, fName
fPath = "C:\Documents and Settings\twilliams\Desktop\"
fName = "Tester.docm"
Dim wdApp, o
Set wdApp = CreateObject("word.application")
wdApp.visible=true
wdapp.documents.open fPath & fName
Set o = wdApp.Run("GetClass")
o.Hello
Set o=nothing
Again - I only tested this simple example: you'll have to adapt it to your situation and try it out.
Word-VBA was not made to create reusable libraries, I suppose (for usage in external programs).
One way to reuse existing Word-VBA code is, however, run Word via WScript.Shell.Run using the /m<macroname> command line switch (see http://support.microsoft.com/kb/210565/en-us for details). This, has the restriction that evertime you need to call a specific macro, a Word process is started again, running that macro, and ends afterwards. Means, if you need just one call to your Word.VBA for a specfific task, this may be ok, but if you need a lot of interprocess communication between your VBScript and your VBA macro, you should look for a different solution.

How do I programatically add a reference to a VBA project?

I'm deploying an early bound styled VBA module that needs Scripting.Dictionary and RegExp.
The script, predictably, fails when it runs on another computer.
The user has to go to Tools->Reference in the VBA IDE and add a reference to those two libraries manually to make it work.
Hence lies the problem. Asking the non-technical end user to go to the IDE and manually add references is asking way too much of them.
The other alternative is to rewrite the whole (very long script written by someone else) to use late binding. I rather not take this path if there are other methods.
As an altervative, some people suggest adding a reference programatically like so:
Application.VBE.ActiveVBProject.References.AddFromFile [Path to library]
Is this the correct solution and if so are there any downsides of this strategy?
If not, are there other methods that will to enable the code to remain early bound yet does not require references to be added manually by the user.
Suggestions involving direct calls to the Win32/64 API are also welcome.
Thanks.
In my own limited environment (small # of other people using spreadsheets I develop, relatively standard machine setups), if I create the file and add the references, and then give a copy to someone else, they can open it with no problems and not have to do anything, so keep that in mind with this answer. (I'm wondering why that doesn't work for you.) Also, this was with Excel.
Rather than adding a reference from a file path, you might consider using the GUID property instead.
Here is some code I once used to automatically create references in a newly created workbook. (It's part of a script that would export code, references, and unit tests on worksheets to text for use with Subversion and then later reconstitute the workbook from the text files.) You might find it useful to your situation. (EH and cleanup removed to keep it short...)
'Export refs in existing workbook to text file
Private Sub exportRefs_(srcWbk As Workbook)
Dim fs As FileSystemObject
Set fs = New FileSystemObject
Dim tsout As TextStream
Set tsout = fs.CreateTextFile(fs.BuildPath(getTargetPath_(srcWbk), "refs.refs"))
Dim ref As Reference
For Each ref In Application.ThisWorkbook.VBProject.References
Call tsout.WriteLine(ref.GUID)
Next ref
'<EH + cleanup...>
End Sub
'Add refs to newly created workbook based on previously exported text file
Private Sub importRefs_(wbk As Workbook, path As String)
Dim fs As FileSystemObject
Set fs = New FileSystemObject
Dim tsin As TextStream
Set tsin = fs.OpenTextFile(path)
Dim line As String
Dim ref As Reference
While Not tsin.AtEndOfStream
line = tsin.ReadLine()
Set ref = Nothing
On Error Resume Next
Set ref = wbk.VBProject.References.AddFromGuid(line, 0, 0)
On Error GoTo 0
If ref Is Nothing Then
Debug.Print "add failed: " & line
End If
Wend
'<EH + cleanup...>
End Sub
Like, I said, limited environment, but hopefully it helps.