Visual Basic - Getting basic information about another program - vba

Alright, I am currently working on an Updater which is updating several files such as executable and drivers. I don't want to replace every single file every time I push an update and rather just replace the file that is actually getting updated. An easy way would be reading the Version of the program but I don't know how to read the required information. I went through IO.File but didn't find anything useful.

Try this for looping through a folder's files.
Option Explicit
Sub Macro1()
Dim varFile As Variant
Dim i As Integer
Dim objShell
Dim objDir
Set objShell = CreateObject("Shell.Application")
Set objDir = objShell.Namespace("C:\DIRECTORY_OF_FILES_HERE\")
For Each varFile In objDir.Items
Debug.Print objDir.GetDetailsOf(varFile, 156) ' 156 = File Version
Next
' Use this to see the numbers of all the attributes, e.g. 156 = File Version
'For i = 0 To 300
' Debug.Print i, objDir.GetDetailsOf(objDir.Items, i)
'Next
End Sub

You could create a separate file that will indicate the version of each individual files you will be updating. This is generally how I do things.
Say you have a file for this, we'll call it "version.txt". This is ideally what you would set it up like this: (you could do a lines format or javascript, whatever you feel most comfortable with)
mainapplication.version = 2.1
So, in your updater method, you would set the latest version as something like 2.2.
Dim alatestVersion As Double
alatestVersion = 2.2
You could then parse the text file and create a method to check if the component is of the latest version or not. Following this, if mainapplicationVersion != alatestVersion then [update].
Hope this helps.

Related

How to avoid runtime error 5152 when trying to add a URL as shape to a Word document

I am trying to place a QR code generated through an API (api.qrserver,com) in a Word table using VBA. For certain reasons, the option of simply using "DisplayBarcode" is not possible.
This is the call to the API:
sURL = "https://api.qrserver.com/v1/create-qr-code/?data=" & UTF8_URL_Encode(VBA.Replace(QR_Value, " ", "+")) & "&size=240x240"
It seems to work well. I tried with a GET command and retrieved a string that - as I interpret - contains the QR code in png format.
Now, when I try to add the picture as a shape using
Set objGrafik = ActiveDocument.Shapes.AddPicture(sURL, True)
the call fails with runtime error 5152. As far as I could determine until now, the Addpicture method expects a pure filename and does not allow any of the following characters: /|?*<>:".
I also tried to store the GET result in an object variable:
Set oQRCode = http.responseText
but there I get the error "object required".
Research on the internet regarding a solution to either make the URL assignment work or to store the result as a picture didn't retrieve any useful results. Thanks in advance for your support
I am not sure that any of the ways you could insert something into Word (e.g. Shapes.AddPicture, InlineShapes.AddPicture, Range.InsertFile etc. will let you do that from any old https Url, although it seems to work for some Urls.
However, as it happens, you can use an INCLUDEPICTURE field to do it. FOr example
{ INCLUDEPICTURE https://api.qrserver.com/v1/create-qr-code/?data=Test&size=100x100 }
Here's some sample VBA to do that
Sub insertqrcode()
Dim sUrl As String
Dim f As Word.Field
Dim r1 As Word.Range
Dim r2 As Word.Range
' This is just a test - plug your own Url in.
sUrl = "https://api.qrserver.com/v1/create-qr-code/?data=abcdef&size=100x100"
' Pick an empty test cell in a table
Set r1 = ActiveDocument.Tables(1).Cell(5, 4).Range
' We have to remove the end of cell character to add the field to the range
' Simplify
Set r2 = r1.Duplicate
r2.Collapse WdCollapseDirection.wdCollapseStart
Set f = r2.Fields.Add(r2, WdFieldType.wdFieldIncludePicture, sUrl, False)
' If you don't want the field code any more, do this
f.Unlink
' check that we have a new inline shape that we could work with more
' if necessary
Debug.Print r1.InlineShapes.count
Set f = Nothing
Set r2 = Nothing
Set r1 = Nothing
End Sub
Using INCLUDEPICTURE works even on the current version of Mac Word (although I haven't tested that specific piece of VBA on Mac).
The only other way I have seen to do it uses WinHTTP to get the result from the web service, ADODB to stream it out to a local disk file, then AddPicture or whatever to include that file, so much more complicated and won't work on Mac either.

Random File Selector?

It's been years since I've used Visual Basic. I downgraded from 2017 to 2010 (The version I was using while I was in school). I figured VB would be the best way to attempt a solution. (Although I'm sure there are other languages that would do it as well.)
I'm looking to get back into programming. Let me get to the problem.
My friend has an ever growing amount of text documents in a folder, and he wants a program to choose one at random, and open it.
I thought I'd put a TextBox with a Button that would let him open the folder where he stores his files. Then this program would read the number of text files in that folder, and randomly generate a number between one and that number, select, and open the document with its default program (if it's text, notepad; if it's DocX then word.)
I've been sitting at a blinking cursor for 45 minutes. I've gone on YouTube for help with this project.
Any advice, or help you guys can give me? Does this need to be simplified?
That sounds like a reasonable strategy to me.
It might be worth displaying some sort of progress to the user, say by putting the name of current file name being read into the status bar, in case there's a long delay reading the file names due to the large number of files in the folder, and/or a slow-running network drive. If you do this, remember to put a DoEvents into your loop to allow screen updates to display.
There's a separate thread on how to open files in their native handler here.
Hope this helps - good luck!
Option Explicit
Public oFSO As Object
Public arrFiles()
Public lngFiles As Long
Sub Main()
Dim sPath As String
sPath = InputBox("Enter folder path", "Folder path")
' clear starting point
lngFiles = 0
Erase arrFiles
Set oFSO = CreateObject("Scripting.FileSystemObject")
Call recurse(sPath)
Randomize
Dim lngRandomFileNumber As Long
lngRandomFileNumber = CLng(lngFiles * Rnd) + 1
MsgBox "This is random file, that will be opened: " & arrFiles(lngRandomFileNumber)
Call CreateObject("Shell.Application").Open(arrFiles(lngRandomFileNumber))
End Sub
Sub recurse(sPath As String)
Dim oFolder As Object
Dim oSubFolder As Object
Dim oFile As Object
Set oFolder = oFSO.GetFolder(sPath)
'Collect file information
For Each oFile In oFolder.Files
lngFiles = lngFiles + 1
ReDim Preserve arrFiles(lngFiles + 1)
arrFiles(lngFiles) = sPath & "\" & oFile.Name
Next oFile
'looking for all subfolders
For Each oSubFolder In oFolder.SubFolders
'recursive call
Call recurse(oSubFolder.path)
Next oSubFolder
End Sub
You can paste this code in any VBA supporting application (MS Access, MS Excel, MS Word), call VBA editor (Shift + F11) and paste this code. After that press F5 and select Main() function. You'll see prompt to enter folder path, and after that you would get random file path.
I think it should be understandable in practice to see what program do
Updated: #Belladonna mentioned it clearly, to open file in default program.
NB: This code is passes through subfolders also, if you want to exclude subfolders, you should comment the recursive call block in recurce function

replace inlineshapes using word VBA

I have a word document with many images in it and I wish to be able to select a file path (in which the new images are located and numbered i.e. 1 to 100) to then replace the existing images with the new images.
I have read a few posts of others retrieving the properties of existing inlineshapes to achieve this, but I have also read people succeeding with the following method which seems much simpler (I just haven't been able to get the code working fully yet). Currently the code will run and replace the last image perfectly fine, but then stops with error '438' - Object doesn't support this property or method when it tries to replace the second last image.
The code is as follows:
Sub Replace_images()
Dim rg As Word.Range
Dim i As Long
Dim doc As Word.Document
Dim path As String
'Ensure pictures are numbered with no leading zeros (in folder) & are .jpg
path = "C:\filepathtopictures\"
Set doc = ActiveDocument
For i = doc.InlineShapes.Count To 1 Step -1
Set rg = doc.InlineShapes(i).Range
doc.InlineShapes(i).Delete
rg = doc.InlineShapes.AddPicture(path & i & ".jpg", False, True, rg)
Next i
End Sub
I don't understand how the using the addpicture doesn't work for the next image when nothing has change from the last image. If someone could please explain why it doesn't work or tell me what needs to be changed that would be great!

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).

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.