replace inlineshapes using word VBA - 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!

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.

Editing hyperlink to remove file name from path

I'm trying to alter hyperlinks within an open Outlook email to remove the filename from the path. For example I want A:\test\folder1\file.txt to become A:\test\folder1
We use SharePoint Online to store files and often want to open the file location instead. Modifying the link makes this happen (we have SharePoint mapped as a network share).
This code alters the entire hyperlink. I assume I need to discard everything after the final backslash.
Sub HyperLinkChange()
Dim objDoc As Object
Dim tmpLink As Object
On Error Resume Next
If ActiveInspector.EditorType = olEditorWord Then
' use WordEditor Inspector
Set objDoc = ActiveInspector.WordEditor
For Each tmpLink In objDoc.Hyperlinks
tmpLink.Address = "test123"
Next tmpLink
End If
End Sub
You can use the InStrRev to search for a backslash starting from the end, and Left function to truncate your string. For more detail, see the InStrRev documentation and Left documentation.
After adding an additional variable Dim pos as Long, find the position of the last backslash with pos = InStrRev(tmpLink.Address, "\", , vbTextCompare). This is the postion starting from beginning of the address text, not the end. In your example address A:\test\folder1\file.txt, the position of the last backslash is 16.
Then tmpLink.Address = Left(tmpLink.Address, pos - 1) returns everything to the left of that backslash.

store word content in variable

How do I copy the entire content (approx 2 pages) of a Word document in VBA and store in a variable?
I keep trying several things, none of which works:
Dim mainData As String
ThisDocument.Activate
ActiveDocument.WholeStory 'error on this line
mainData = Selection.Text
With 'record macro' I can simulate selecting a piece or the entire text, but I can't simulate storing that into a variable.
The above code throws
'This command is not available because no document is open',
but hadn't I first activated this (the current) document, and then selected it (ActiveDocument.WholeStory)?
Why doesn't this work?
Later edit: I managed to do the selection like this:
Dim sText As String
Application.Selection.ClearFormatting
Application.Selection.WholeStory
sText = Application.Selection.Text
MsgBox sText
but the problem is I can't store the entire text (2 pages) in a variable. Part of it is truncated. Would you know how to store word by word (I only need a word at a time anyway)?
Later edit. I applied strReverse on the text to find out the text is actually stored entirely in the variable, just not fully displayed in the message box.
Don't use ThisDocument in code, unless you specifically want to address the file in which the code is stored and running. ThisDocument is the "code name" of that file.
Instead, use ActiveDocument to mean the document currently active in the Word window.
An addition, if you want the Selection in the currently active document, there's no reason to activate it - it's already active.
So to get the entire document content in a string
Dim mainData As String
mainData = ActiveDocument.Content.Text
where Content returns the entire main body's text as a Range object.
Note: The MsgBox has an upper character limit. If you're working with long text strings and want to see what they hold the following has more (but not "infinite") capacity:
Debug.Print mainData
All you need is:
Dim mainData As String
mainData = ActiveDocument.Range.Text

Visual Basic - Getting basic information about another program

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.

Implement auto-increment with Word macro

I'm writing a Word/VBA macro for a document template. Every time a user saves/creates a new document from the template, the document needs an ID embedded in the text. How can I (as simple as possible) implement auto-increment for this ID? The ID is numeric.
The system has to have some kind of mechanism to avoid different documents getting the same IDs, but the load is very low. About 20 people will use this template (on our intranet), creating something like 20 new documents a week altogether.
I've toyed with the idea of having a text file that I lock and unlock from the macro, or call a PHP page with an SQLite database, but is there other, smarter solutions?
Note that I can't use UUID or GUID, since the IDs need to be usable by humans as well as machines. Our customers must be able to say over the phone: "... and about this, then, with ID 436 ...?"
Gave some further thought to this, and here is another approach you may want to consider. If you're not interested in a catalog of previous IDs, then you could simply use a custom document property to store the last ID that was used.
In Word 97-2003, you can add a custom property by going to "File / Properties", choosing the custom tab and assigning a name and value there. Adding a custom document property in Word 2007 is a bit more buried and off the top of my head, I think it's "Office Button / Prepare / Document Properties", choose the little drop down box for advanced properties and you'll get the same ol' pre-2007 dialog.
In the example below, I called mine simply "DocumentID" and assigned it an initial value of zero.
The relevant bit of code to update a Custom document property is:
ThisDocument.CustomDocumentProperties("DocumentID").Value = NewValue
As a proof of concept, I created a .dot file and used the following code in the Document_New() event:
Sub UpdateTemplate()
Dim Template As Word.Document
Dim NewDoc As Word.Document
Dim DocumentID As DocumentProperty
Dim LastID As Integer
Dim NewID As Integer
'Get a reference to the newly created document
Set NewDoc = ActiveDocument
'Open the template file
Set Template = Application.Documents.Open("C:\Doc1.dot")
'Get the custom document property
Set DocumentID = Template.CustomDocumentProperties("DocumentID")
'Get the current ID
LastID = DocumentID.Value
'Use any method you need for determining a new value
NewID = LastID + 1
'Update and close the template
Application.DisplayAlerts = wdAlertsNone
DocumentID.Value = NewID
Template.Saved = False
Template.Save
Template.Close
'Remove references to the template
NewDoc.AttachedTemplate = NormalTemplate
'Add your ID to the document somewhere
NewDoc.Range.InsertAfter ("The documentID for this document is " & NewID)
NewDoc.CustomDocumentProperties("DocumentID").Value = NewID
End Sub
Good luck!
You could handle this entirely through VBA using Word and Excel (or Access I suppose, but I have an unnatural aversion towards using Access).
First, create a new Excel workbook and store it in a location that you can access through your word document (mine is C:\Desktop\Book1.xls). You may even want to seed the values by entering a numeric value into cell A1.
In your word document, you would enter this into your Document_Open() subroutine:
Private Sub Document_Open()
Dim xlApp As Excel.Application
Dim xlWorkbook As Excel.Workbook
Dim xlRange As Excel.Range
Dim sFile As String
Dim LastID As Integer
Dim NewID As Integer
'Set to the location of the Excel "database"
sFile = "C:\Desktop\Book1.xls"
'Set all the variables for the necessary XL objects
Set xlApp = New Excel.Application
Set xlWorkbook = xlApp.Workbooks.Open(sFile)
'The used range assumes just one column in the first worksheet
Set xlRange = xlWorkbook.Worksheets(1).UsedRange
'Use a built-in Excel function to get the max ID from the used range
LastID = xlApp.WorksheetFunction.Max(xlRange)
'You may want to come up with some crazy algorithm for
'this, but I opted for the intense + 1
NewID = LastID + 1
'This will prevent the save dialog from prompting the user
xlApp.DisplayAlerts = False
'Add your ID somewhere in the document
ThisDocument.Range.InsertAfter (NewID)
'Add the new value to the Excel "database"
xlRange.Cells(xlRange.Count + 1, 1).Value = NewID
'Save and close
Call xlWorkbook.Save
Call xlWorkbook.Close
'Clean Up
xlApp.DisplayAlerts = True
Call xlApp.Quit
Set xlWorkbook = Nothing
Set xlApp = Nothing
Set xlRange = Nothing
End Sub
I realize this is a tall procedure, so by all means re-factor it to your heart's content. This was just a quick test I whipped up. Also, you'll need to add a reference to the Excel Object Library through References in VBA. Let me know if you have any questions about how that works.
Hope that helps!
Well you have to store the next ID number somewhere. The text file idea is as good as any. You just have to handle the possibility of it being locked or unaccessible for some reason.
Using a database for one number is overkill.
Off the top of my head:
Use Excel as your external DB with Automation.
Explore the several SQLite COM wrappers (Litex comes to mind).
"text file that I lock and unlock from the macro" would be the safest approach.
The DOCID file would only have one number: the last ACTUALLY used ID.
A) You read the file (not in write/append mode) and store on a variable on your document DOC_ID =FILE_ID+1 and save the doc. Tentatively you kill the DOCID file, open/create for read-write sotring your DOC_ID. Close the file. If all went well including Close, you're safe, otherwise, back to A).
You might want to consider: if no file is found create it with this document ID +100, as a measure of recovering from no-UPS disasters whilst in A)
I'm too tired to check if it might create a deadlock under concurrency scenario... it might.
If you feel its worth it, I can put code here.
It seems I found a way to open and update a text file with exclusive rights, which means that there will be no concurrency problems:
Private Function GetNextID(sFile As String) As Integer
Dim nFile As Integer
nFile = FreeFile
On Error Resume Next
Open sFile For Binary Access Read Write Lock Read Write As #nFile
If Err.Number <> 0 Then
' Return -1 if the file couldn't be opened exclusively
GetNextID = -1
Err.Clear
Exit Function
End If
On Error GoTo 0
GetNextID = 1 + Val(Input(LOF(nFile), #nFile))
Put #nFile, 1, CStr(GetNextID)
Close #nFile
End Function
Simply call this function until it doesn't return -1 anymore. Neat.