Loop Through All Subfolders - VBA - Queue method - vba

I've made use of Cor_blimey's queue method to write all the folders and subfolders of a drive to an excel sheet, as follows:
Public Sub NonRecursiveMethod()
Dim fso, oFolder, oSubfolder, oFile, queue As Collection
Set fso = CreateObject("Scripting.FileSystemObject")
Set queue = New Collection
queue.Add fso.GetFolder("your folder path variable") 'obviously replace
Do While queue.Count > 0
Set oFolder = queue(queue.count)
queue.Remove(queue.count) 'dequeue
'...insert any folder processing code here...'
'*...(Here I write the name of the folder to the excel sheet)*.
For Each oSubfolder In oFolder.SubFolders
queue.Add oSubfolder 'enqueue
Next oSubfolder
For Each oFile In oFolder.Files
'...insert any file processing code here...
Next oFile
Loop
End Sub
I've tried the "LIFO" version (as above) and the "FIFO" version, but neither of them produces a standard alphabetical listing. The above version lists the drive in exact reverse alphabetical order, and the "FIFO" version produces a list in normal alphabetical order, but it lists only the first-level folders, then starts again and lists all the second-level folders, again in alphabetical order, then the third level of folders, again starting over from "A", etc. As a result, the subfolders are not listed under their parent folder.
Does anyone know what I can do to get a standard tree structure, in alphabetical order by folder and subfolder name?
TIA
Les
Update: for some reason I can't manage to show all the comments on this thread or write a new comment. But I wanted to thank everybody, in particular #Rosenfeld, and say that I'm eager to try the solution using dir but am currently swamped with work. I'll report back in a few days when I get a chance to stumble around.

I'd like for the output to the sheet to look like the results of a tree command
Seems to me the simplest would be to just use the Tree command.
Here is one way, but the details could certainly be changed:
Execute a Tree command on the base folder
Write the output to some text file (location and name specified in the code)
Open the file as a text file in Excel
Split into columns on the vertical bar (Unicode character 9474) that the Tree command uses to differentiate levels
I use the WSH.Run method as that allows the CMD window to be easily hidden
One could use the WSH.Exec method to pipe the output directly to a VBA variable, but it is much harder to hide the CMD window (meaning, in another application, I've not been able to) :-)
One could also Import the text file into the same workbook instead of opening a new file. I will leave that exercise to you if you choose to do it.
Option Explicit
'set referennce to Windows Script Host Object Model
Sub DirTree()
Dim sBaseFolder As String, sTempFile As String
Dim WSH As WshShell
Dim sCMD As String
Dim lErrCode As Long
'Many ways to set starting point
sBaseFolder = Environ("HOMEDRIVE") & "\"
sTempFile = Environ("TEMP") & "\Tree.txt"
'Command line
sCMD = "CMD /c tree """ & sBaseFolder & """ > """ & sTempFile & """"
Set WSH = New WshShell
lErrCode = WSH.Run(sCMD, xlHidden, True)
If Not lErrCode = 0 Then
MsgBox "Error in execution: Code - " & lErrCode
Else
'Open the file
Workbooks.OpenText Filename:=sTempFile, Origin:=xlMSDOS, _
StartRow:=1, DataType:=xlDelimited, TextQualifier:=xlDoubleQuote, _
ConsecutiveDelimiter:=False, Tab:=False, Semicolon:=False, Comma:=False _
, Space:=False, Other:=True, OtherChar:=ChrW(&H2502), _
FieldInfo:=Array(Array(1, 1), Array(2, 1))
End If
End Sub
Here is a screenshot of the beginning of the output when run on my C: drive
EDIT: Since you now mention that you want the links to be clickable, an approach using dir would probably be simpler, especially since you can provide arguments to the dir command that will result in full paths being returned.
I used a class module so as to have a User Defined Object, which would have the necessary information; and a dictionary of these objects after appropriate filtering.
I chose to display merely the folder name in the cell, but the the screen tip will show the full path.
Note the References that need to be set (in the code). Also note that the class module must be renamed: cTree
EDIT 2: The Regular and Class modules were edited to allow for optional listing of the files. Note that the macro now has an argument, so it must be called from another macro or from the immediate window, to include the argument. (The argument could also be obtained from an Input box, user form, etc, but I did it this way for now because it is simpler.
I did not add hyperlinks for the files, thinking it would get confusing as different programs and dialogs (other than the file explorer) would be opening depending on the extension.
Class Module
Option Explicit
'Rename Class Module: cTree
Private pFullPath As String
Private pFolderName As String
Private pLevel As Long
Private pFile As String
Private pFiles As Dictionary
Public Property Get FullPath() As String
FullPath = pFullPath
End Property
Public Property Let FullPath(Value As String)
pFullPath = Value
End Property
Public Property Get FolderName() As String
FolderName = pFolderName
End Property
Public Property Let FolderName(Value As String)
pFolderName = Value
End Property
Public Property Get Level() As Long
Level = pLevel
End Property
Public Property Let Level(Value As Long)
pLevel = Value
End Property
Public Property Get Files() As Dictionary
Set Files = pFiles
End Property
Public Function ADDfile(Value As String)
pFiles.Add Value, Value
End Function
Private Sub Class_Initialize()
Set pFiles = New Dictionary
pFiles.CompareMode = TextCompare
End Sub
Regular Module
Option Explicit
'Set reference to Windows Script Host Object Model
' Microsoft Scripting Runtime
Sub GetDirList(bInclFiles As Boolean)
Const sDIRargs As String = " /A-S-L-H /S"
Dim sBaseFolder As String, sTempFile As String
Dim WSH As WshShell
Dim sCMD As String
Dim lErrCode As Long
Dim FSO As FileSystemObject, TS As TextStream
Dim S As String, sFN As String
Dim V As Variant, W As Variant
Dim I As Long
Dim lMaxLevel As Long
Dim lMinLevel As Long
Dim dctTrees As Dictionary, cT As cTree
Dim wsRes As Worksheet
Dim vRes As Variant, rRes As Range
'Add worksheet if needed
On Error Resume Next
Set wsRes = Worksheets("TreeLink")
If Err.Number = 9 Then
Set wsRes = Worksheets.Add
wsRes.Name = "TreeLink"
End If
On Error GoTo 0
Set rRes = wsRes.Cells(1, 1)
'Many ways to set starting point
sBaseFolder = Environ("HOMEDRIVE") & "\"
sTempFile = Environ("TEMP") & "\DirList.txt"
'CommandLine
sCMD = "CMD /c dir """ & sBaseFolder & """" & sDIRargs & " > " & sTempFile
Set WSH = New WshShell
lErrCode = WSH.Run(sCMD, xlHidden, True)
If Not lErrCode = 0 Then
MsgBox "Error in execution: Code - " & lErrCode
Stop
Else
'Read in the relevant data
Set dctTrees = New Dictionary
Set FSO = New FileSystemObject
Set TS = FSO.OpenTextFile(sTempFile, ForReading, False, TristateUseDefault)
lMaxLevel = 0
V = Split(TS.ReadAll, vbCrLf)
For I = 0 To UBound(V)
Do Until V(I) Like " Directory of *"
If I = UBound(V) Then Exit For
I = I + 1
Loop
Set cT = New cTree
S = Mid(V(I), 15)
'Can exclude certain directories at this point
'To exclude all that start with a dot:
If Not S Like "*\.*" Then
With cT
.FullPath = S
.FolderName = Right(S, Len(S) - InStrRev(S, "\"))
.Level = Len(S) - Len(Replace(S, "\", ""))
lMaxLevel = IIf(lMaxLevel > .Level, lMaxLevel, .Level)
dctTrees.Add Key:=S, Item:=cT
I = I + 1
'Only run for file list
If bInclFiles = True Then
Do
sFN = V(I)
If Not sFN Like "*<DIR>*" _
And sFN <> "" Then
'add the files
dctTrees(S).ADDfile Mid(sFN, 40)
End If
I = I + 1
Loop Until V(I) Like "*# File(s)*"
End If
End With
End If 'End of directory exclusion "if" statement
Next I
lMinLevel = dctTrees(dctTrees.Keys(0)).Level
I = 0
With rRes.Resize(columnsize:=lMaxLevel + 1).EntireColumn
.Clear
.HorizontalAlignment = xlLeft
End With
Application.ScreenUpdating = False
For Each V In dctTrees.Keys
Set cT = dctTrees(V)
With cT
I = I + 1
rRes.Worksheet.Hyperlinks.Add _
Anchor:=rRes(I, .Level - lMinLevel + 1), _
Address:="File:///" & .FullPath, _
ScreenTip:=.FullPath, _
TextToDisplay:=.FolderName
For Each W In .Files.Keys
I = I + 1
rRes(I, .Level - lMinLevel + 2) = W
Next W
End With
Next V
Application.ScreenUpdating = True
End If
End Sub
Results without File Listing
Results with File Listing

I know you are using a non-recursion method, but admittedly I wanted to try my hand at using recursion to solve the task (particularly for anyone who may need this in the future).
Note: I am not certain that the Scripting.FileSystem Folders/Files collections are always alphabetical so I am assuming they are in this case, but I could be mistaken.
From brief tests I am not noticing any kind of performance issue with recursion though, depending on the directory size, there certainly could be one.
Finally, the 'CleanOutput' argument in the main Function is used to determine if hierarchy relationships are displayed in the output.
Method Used to Test/Output
Sub Test()
Dim fso As Scripting.FileSystemObject
Set fso = New Scripting.FileSystemObject
Dim Folder As Scripting.Folder
Set Folder = fso.GetFolder("C:")
Dim Test As Variant
Test = GetDirectoryFromScriptingFolder(Folder, True)
ActiveSheet.Range("A1").Resize(UBound(Test, 1), UBound(Test, 2)).value = Test
End Sub
Main Function
Private Function GetDirectoryFromScriptingFolder(ByVal InputFolder As Scripting.Folder, Optional CleanOutput As Boolean = False) As Variant
' Uses recursion to return an organized hierarchy that represents files/folders in the input directory
Dim CurrentRow As Long
CurrentRow = 1
Dim CurrentColumn As Long
CurrentColumn = 1
Dim OutputDirectory As Variant
ReDim OutputDirectory(1 To GetDirectoryLength(InputFolder), 1 To GetDirectoryDepth(InputFolder))
WriteFolderHierarchy InputFolder, OutputDirectory, CurrentRow, CurrentColumn, CleanOutput
' Adjust current column so that files in the parent directory are properly indented
WriteFileHierarchy InputFolder, OutputDirectory, CurrentRow, CurrentColumn + 1, CleanOutput
GetDirectoryFromScriptingFolder = OutputDirectory
End Function
Functions Used in Recursion
Private Sub WriteFolderHierarchy(ByVal InputFolder As Scripting.Folder, ByRef InputHierarchy As Variant, ByRef CurrentRow As Long, ByVal CurrentColumn As Long, ByVal CleanOutput As Boolean)
If Not IsArray(InputHierarchy) Then Exit Sub
InputHierarchy(CurrentRow, CurrentColumn) = InputFolder.Name
CurrentRow = CurrentRow + 1
Dim StartRow As Long
Dim SubFolder As Folder
For Each SubFolder In InputFolder.SubFolders
' Use recursion to write the files/folders of each subfolder to the directory
StartRow = CurrentRow
WriteFolderHierarchy SubFolder, InputHierarchy, CurrentRow, CurrentColumn + 1, CleanOutput
WriteFileHierarchy SubFolder, InputHierarchy, CurrentRow, CurrentColumn + 2, CleanOutput
If CleanOutput Then
For StartRow = StartRow To CurrentRow
InputHierarchy(StartRow, CurrentColumn) = "||"
Next
End If
Next
End Sub
Private Sub WriteFileHierarchy(ByVal InputFolder As Scripting.Folder, ByRef InputHierarchy As Variant, ByRef CurrentRow As Long, ByVal CurrentColumn As Long, ByVal CleanOutput As Boolean)
If Not IsArray(InputHierarchy) Then Exit Sub
Dim SubFile As File
For Each SubFile In InputFolder.Files
' Write the Files to the Hierarchy
InputHierarchy(CurrentRow, CurrentColumn) = SubFile.Name
If CleanOutput Then InputHierarchy(CurrentRow, CurrentColumn - 1) = "--"
CurrentRow = CurrentRow + 1
Next
End Sub
Helper Functions (Depth and Length)
Private Function GetDirectoryLength(ByVal InputFolder As Scripting.Folder) As Long
Dim TotalLength As Long
' Include a base of 1 to account for the input folder
TotalLength = 1 + InputFolder.Files.Count
Dim SubFolder As Scripting.Folder
For Each SubFolder In InputFolder.SubFolders
' Add 1 to the total to account for the subfolder.
TotalLength = TotalLength + GetDirectoryLength(SubFolder)
Next
GetDirectoryLength = TotalLength
End Function
Private Function GetDirectoryDepth(ByVal InputFolder As Scripting.Folder) As Long
Dim TotalDepth As Long
Dim SubFolder As Scripting.Folder
Dim MaxDepth As Long
Dim NewDepth As Long
For Each SubFolder In InputFolder.SubFolders
NewDepth = GetDirectoryDepth(SubFolder)
If NewDepth > MaxDepth Then
MaxDepth = NewDepth
End If
Next
If MaxDepth = 0 Then MaxDepth = 1
' Add 1 for the Parent Directory
GetDirectoryDepth = MaxDepth + 2
End Function
What is essentially happening is this:
We take an input Folder and determine the dimensions of the hierarchy
for that file
Next, we define an output array using those dimensions.
Using a row counter and column counter, we allow the recursion functions to write their recursive results directly to the hierarchy
This hierarchy is returned, and the main routine puts this straight to the sheet
Next Steps that You Could Take
I noticed a few things doing this
There is no information other than the file name, which, depending on
the application, may make the method useless
All files are included
in the output, not just important ones (non-important files being
temp, hidden, etc.)
Even with the CleanOutput option there isn't an easy way of diagramming the relationships between parents and children.
Overall though this should suffice, depending on your needs. You can make adjustments as needed. If you have questions, just ask :).

I don't think LIFO or FIFO matters, just take a look at this idea.
Sub GetFilesInFolder(SourceFolderName As String)
'--- For Example:Folder Name= "D:\Folder Name\"
Dim FSO As Scripting.FileSystemObject
Dim SourceFolder As Scripting.folder, SubFolder As Scripting.folder
Dim FileItem As Scripting.File
Set FSO = New Scripting.FileSystemObject
Set SourceFolder = FSO.GetFolder(SourceFolderName)
'--- This is for displaying, whereever you want can be configured
r = 14
For Each FileItem In SourceFolder.Files
Cells(r, 2).Formula = r - 13
Cells(r, 3).Formula = FileItem.Name
Cells(r, 4).Formula = FileItem.Path
Cells(r, 5).Formula = FileItem.Size
Cells(r, 6).Formula = FileItem.Type
Cells(r, 7).Formula = FileItem.DateLastModified
Cells(r, 8).Formula = "=HYPERLINK(""" & FileItem.Path & """,""" & "Click Here to Open" & """)"
r = r + 1 ' next row number
Next FileItem
Set FileItem = Nothing
Set SourceFolder = Nothing
Set FSO = Nothing
End Sub
ii) User wants to get the list of all files inside a folder as well as Sub-folders
Copy and Paste the below Code and this will list down the list of all the files inside the folder as well as sub-folders. If there are other files which are there in some other Sub-folders then it will list down all files from each and Every Folders and Sub-folders.
Sub GetFilesInFolder(SourceFolderName As String, Subfolders As Boolean)
'--- For Example:Folder Name= "D:\Folder Name\" and Flag as Yes or No
Dim FSO As Scripting.FileSystemObject
Dim SourceFolder As Scripting.folder, SubFolder As Scripting.folder
Dim FileItem As Scripting.File
'Dim r As Long
Set FSO = New Scripting.FileSystemObject
Set SourceFolder = FSO.GetFolder(SourceFolderName)
'--- This is for displaying, whereever you want can be configured
r = 14
For Each FileItem In SourceFolder.Files
Cells(r, 2).Formula = r - 13
Cells(r, 3).Formula = FileItem.Name
Cells(r, 4).Formula = FileItem.Path
Cells(r, 5).Formula = FileItem.Size
Cells(r, 6).Formula = FileItem.Type
Cells(r, 7).Formula = FileItem.DateLastModified
Cells(r, 8).Formula = "=HYPERLINK(""" & FileItem.Path & """,""" & "Click Here to Open" & """)"
r = r + 1 ' next row number
Next FileItem
'--- This is the Function to go each and Every Folder and get the Files. This is a Nested-Function Calling.
If Subfolders = True Then
For Each SubFolder In SourceFolder.Subfolders
ListFilesInFolder SubFolder.Path, True
Next SubFolder
End If
Set FileItem = Nothing
Set SourceFolder = Nothing
Set FSO = Nothing
End Sub
File Manager using Excel Macro in Excel Workbook
I have created one File Manager using the above Code. It basically fetches the list of Files from Folders and Sub-folders and list them. It fetches other details of the files as well like File Size, Last modified, path of the File, Type of the File and a hyperlink to open the file directly from the excel by clicking on that.
It looks something like below:
Here is the link to download the full Workbook.
http://learnexcelmacro.com/wp/2011/11/how-to-get-list-of-all-files-in-a-folder-and-sub-folders/
Click on the button that is named 'Download Now'.

Related

How to populate last saved user and last saved date of a file

I have the code below to get file names from folders.
Sub GetFileNames_Assessed_As_T2()
Dim sPath As String, sFile As String
Dim iRow As Long, iCol As Long
Dim ws As Worksheet: Set ws = Sheet9
'declare and set the worksheet you are working with, amend as required
sPath = "Z:\NAME\T2\"
'specify directory to use - must end in ""
sFile = Dir(sPath)
Do While sFile <> ""
LastRow = ws.Cells(ws.Rows.Count, "I").End(xlUp).Row 'get last row on Column I
Filename = Left(sFile, InStrRev(sFile, ".") - 1) 'remove extension from file
Set FoundFile = ws.Range("I1:I" & LastRow).Find(what:=Filename, lookat:=xlWhole) 'search for existing filename
If FoundFile Is Nothing Then ws.Cells(LastRow + 1, "I") = Filename 'if not found then add it
sFile = Dir ' Get next filename
Loop
End Sub
I need an adjustment to fetch the following and populate it on the spreadsheet:
File last updated by (Column O)
File last updated date (Column P)
Hyperlink the file to the spreadsheet (Column Q)
Here is an example accessing the extended document properties via Dsofile.dll. 32 bit version is here. I am using re-written 64 bit alternative by robert8w8. After installation, of 64 bit version in my case, you go Tools >References >Add a reference to DSO OLE Document Properties Reader 2.1. It enables to access extended properties of closed files. Obviously, if the info is not available, it cannot be returned.
I have an optional filemask test in there which can be removed.
The DSO function is my re-write of a great sub that lists many more properties by xld here.
Option Explicit
Public Sub GetLastestDateFile()
Dim FileSys As Object, objFile As Object, myFolder As Object
Const myDir As String = "C:\Users\User\Desktop\TestFolder" '< Pass in your folder path
Set FileSys = CreateObject("Scripting.FileSystemObject")
Set myFolder = FileSys.GetFolder(myDir)
Dim fileName As String, lastRow As Long, arr(), counter As Long
With ThisWorkbook.Worksheets("Sheet1") '<== Change to your sheet where writing info to
lastRow = .Cells(.Rows.Count, "P").End(xlUp).Row 'find the last row with data in P
For Each objFile In myFolder.Files 'loop files in folder
fileName = objFile.Path
If FileSys.GetExtensionName(fileName) = "xlsx" Then 'check if .xlsx
arr = GetExtendedProperties(fileName)
counter = counter + 1
.Cells(lastRow + counter, "O") = arr(0) 'Last updated
.Cells(lastRow + counter, "P") = arr(1) 'Last save date
.Hyperlinks.Add Anchor:=.Cells(lastRow + counter, "Q"), Address:=objFile.Path '<== Add hyperlink
End If
Next objFile
End With
End Sub
Public Function GetExtendedProperties(ByVal FileName As String) As Variant
Dim fOpenReadOnly As Boolean, DSO As DSOFile.OleDocumentProperties
Dim oSummProps As DSOFile.SummaryProperties, oCustProp As DSOFile.CustomProperty
Dim outputArr(0 To 1)
Set DSO = New DSOFile.OleDocumentProperties
DSO.Open FileName, fOpenReadOnly, dsoOptionOpenReadOnlyIfNoWriteAccess
Set oSummProps = DSO.SummaryProperties
outputArr(0) = oSummProps.LastSavedBy
outputArr(1) = oSummProps.DateLastSaved
GetExtendedProperties = outputArr
End Function
Other:
Hyperlinks.Add method
In my case I could not use the DSO library from dsofile.dll (one needs to be admin to install it and register it...), so I came up with another solution to get some OLE properties of Office documents without opening them. It appears that (some of?) these Extended Properties are also accessible via the Shell:
Function GetDateLastSaved_Shell32(strFileFullPath$)
strFolderPath$ = Left(strFileFullPath, Len(strFileFullPath) - Len(Dir(strFileFullPath)))
strFileName$ = Dir(strFileFullPath)
'using late binding here
'to use early binding with Dim statements you need to reference the Microsoft Shell Controls And Automation library, usually available here:
'C:\Windows\SysWOW64\shell32.dll
'Example:
'Dim shlShell As Shell32.Shell
Set shlShell = CreateObject("Shell.Application") 'Variant/Object/IShellDispatch6
'Set shlFolder = shlShell.Namespace(strFolderPath) 'does not work when using late binding, weird...*
Set shlFolder = shlShell.Namespace(CStr(strFolderPath)) 'works...
'Set shlFolder = shlShell.Namespace(strFolderPath & "") 'works...
'Set shlFolder = shlShell.Namespace(Left$(strFolderPath, Len(strFolderPath))) 'works...
'*also mentioned here without an explanation...
'https://stackoverflow.com/questions/35957930/word-vba-shell-object-late-binding
Set shlShellFolderItem = shlFolder.ParseName(strFileName)
'all of the following returns the same thing (you have the returned Data Type indicated on the right)
'but the first one is said by MSDN to be the more efficient way to get an extended property
GetDateLastSaved_Shell32 = shlShellFolderItem.ExtendedProperty("{F29F85E0-4FF9-1068-AB91-08002B27B3D9} 13") 'Date
'GetDateLastSaved_Shell32 = shlShellFolderItem.ExtendedProperty("System.Document.DateSaved") 'Date
'GetDateLastSaved_Shell32 = shlShellFolderItem.ExtendedProperty("DocLastSavedTm") 'Date 'legacy name
'GetDateLastSaved_Shell32 = shlFolder.GetDetailsOf(shlShellFolderItem, 154) '?String?
End Function
To list all extended properties (Core, Documents, etc.), you can use this:
For i = 0 To 400
vPropName = shlFolder.GetDetailsOf(Null, i)
vprop = shlFolder.GetDetailsOf(shlShellFolderItem, i)
Debug.Print i, vPropName, vprop
If i Mod 10 = 0 Then Stop
Next
You can find more info about the "efficient way" on MSDN: ShellFolderItem.ExtendedProperty method
You can also find the list of FMTIDs and PIDSIs in propkey.h from Windows SDK or somewhere in C:\Program Files (x86)\Windows Kits\10\Include\***VERSION***\um\ if you have Visual Studio installed.

Running List of CMD lines from Excel

Can anyone help please with the following requirements?
Requirement A:
I'd like to create a loop to run a list of command strings in CMD as long as there's a non-zero value in column C. I think I need to define a variable i for my starting row as this will always be the same, and then run Shell(), pulling the command string from the corresponding cell in Row i, Column F. While Cells(i, "C") is not blank, keep going, increasing i by 1.
Requirement B:
I'd also like to link this macro to work in a directory deposited in a cell by an earlier macro that listed all the files in a selected directory.
This is what I have, without any looping..
Sub Run_Renaming()
Dim CommandString As Long
Dim i As Integer
i = 5
'Other steps:
'1 - need to pick up variable (directory of files listed, taken from first macro
'when doing manually, I opened command, went to correct directory, then pasted
'the commands. I'm trying to handle pasting the commands. I'm not sure if I need
'something to open CMD from VBA, then run through the below loop, or add opening
'CMD and going to the directory in each iteration of the below loop...
'2 - Need to say - Loop below text if Worksheets("Batch Rename of Files").Cells(i, "C").Value is no blank
CommandString = Worksheets("Batch Rename of Files").Cells(i, "F").Value
Call Shell("cmd.exe /S /K" & CommandString, vbNormalFocus)
'Other steps:
'3 - need to increase i by 1
'4 - need to check if C column is blank or not
'5 - need to end of C column is blank
End Sub
Background:
I'm creating a file renaming tool for a friend. They can use excel, but no programming languages or command prompt. Because of this, I don't want to have any steps, like creating a batch file suggested here, that would complicate things for my friend.
I've created an excel file with:
Tab 1 - a template sheet to create a new file name list. Works by concatenating several cells, adding a filetype, and outputting to a range of cells. Tab two links to this range when creating the renaming command strings for CMD
Tab 2 -
Button 1 - Sub rename() below. VBA to list files in a selected directory in Column C
Column F creates a command line that will rename File A as File B based on inputs to Tab 1 i.e. ren "File 1" "A1_B1_C1.xlsx"
Button 2 - Refers to a renaming macro (requirement 1 and 2 above) that picks up the selected directory from Button 1 and runs through all the renaming command strings while in that directory
Sub rename()
Dim xRow As Long
Dim xDirect$, xFname$, InitialFoldr$
InitialFoldr$ = "C:\"
Worksheets("Batch Rename of Files").Activate
Worksheets("Batch Rename of Files").Range("C4").Activate
With Application.FileDialog(msoFileDialogFolderPicker)
.InitialFileName = Application.DefaultFilePath & "\"
.Title = "Please select a folder to list Files from"
.InitialFileName = InitialFoldr$
.Show
If .SelectedItems.Count <> 0 Then
xDirect$ = .SelectedItems(1) & "\"
xFname$ = Dir(xDirect$, 7)
Do While xFname$ <> ""
ActiveCell.Offset(xRow) = xFname$
xRow = xRow + 1
xFname$ = Dir
Loop
End If
End With
End Sub
Caveats:
1) I am not entirely clear on how you data etc is laid out so i am offering a way of achieving your goal that involves the elements i am clear on.
2) To be honest, personally, i would do as much using arrays or a dictionary as possible rather than going backwards and forwards to worksheets.
However...
Following the outline of your requirements and a little rough and ready, we have:
1) Using your macro rename (renamed as ListFiles and with a few minor tweaks) to write the chosen folder name out to Range("A1") in Worksheets("Batch Rename of Files") and the file names to Column C.
2) Using a second macro RenameFiles to pick up the rename shell commands from Column F of Worksheets("Batch Rename of Files"); write these out to a batch file on the desktop; add an additional first line command that sets the working directory to the chosen folder given in Range("A1") (Requirement A). The shell command executes the .bat file, completes the renaming (Requirement B) and then there is a line to remove the .bat file.
I am guessing this is a more efficient way of achieving your goal than looping the column F range executing a command one at a time.
I have not sought to optimize code in any further ways (i have added a few existing typed functions.) There are a number of other improvements that could be made but this was intended to help you achieve your requirements.
Let me know how it goes!
Tab1 layout (Sheet containing new file names):
Batch Rename of Files layout (Sheet containing output of the first macro and the buttons ):
Layout of Worksheet Batch Rename of File
In a standard module called ListFiles:
Option Explicit
Public Sub ListFilesInDirectory()
Dim xRow As Long
Dim xDirect$, xFname$, InitialFoldr$ 'type hints not really needed
Dim wb As Workbook
Dim wsTab2 As Worksheet
Set wb = ThisWorkbook
Set wsTab2 = wb.Worksheets("Batch Rename of Files")
InitialFoldr$ = "C:\"
Dim lastRow As Long
lastRow = wsTab2.Cells(wsTab2.Rows.Count, "C").End(xlUp).Row
wsTab2.Range("C4:C" & lastRow).ClearContents 'Get rid of any existing file names
wsTab2.Range("C4").Activate
With Application.FileDialog(msoFileDialogFolderPicker)
.InitialFileName = Application.DefaultFilePath & "\"
.Title = "Please select a folder to list Files from"
.InitialFileName = InitialFoldr$
.Show
If .SelectedItems.Count <> 0 Then
xDirect$ = .SelectedItems(1) & "\"
xFname$ = Dir(xDirect$, 7)
wsTab2.Range("A1") = xDirect$
Do While xFname$ <> vbNullString
ActiveCell.Offset(xRow) = xFname$
xRow = xRow + 1
xFname$ = Dir
Loop
End If
End With
End Sub
In a standard module called FileRenaming:
Option Explicit
Sub RenameFiles()
Dim fso As New FileSystemObject
Dim stream As TextStream
Dim strFile As String
Dim strPath As String
Dim strData As Range
Dim wb As Workbook
Dim wsTab2 As Worksheet
Dim currRow As Range
Set wb = ThisWorkbook
Set wsTab2 = wb.Worksheets("Batch Rename of Files")
strPath = wsTab2.Range("A1").Value2
If strPath = vbNullString Then
MsgBox "Please ensure that Worksheet Batch Rename of Files has a directory path in cell A1"
Else
If Right$(Trim$(strPath), 1) <> "\" Then strPath = strPath & "\"
strFile = "Rename.bat"
Dim testString As String
Dim deskTopPath As String
deskTopPath = Environ$("USERPROFILE") & "\Desktop" 'get desktop path as this is where .bat file will temporarily be saved
testString = fso.BuildPath(deskTopPath, strFile) 'Check if .bat already exists and delete
If Len(Dir(testString)) <> 0 Then
SetAttr testString, vbNormal
Kill testString
End If
Set stream = fso.CreateTextFile(deskTopPath & "\" & strFile, True) 'create the .bat file
Dim lastRow As Long
lastRow = wsTab2.Cells(wsTab2.Rows.Count, "C").End(xlUp).Row
Set strData = wsTab2.Range("F4:F" & lastRow) 'Only execute for as many new file names as present in Col C (in place of your until blank requirement)
stream.Write "CD /D " & strPath & vbCrLf
For Each currRow In strData.Rows 'populate the .dat file
stream.Write currRow.Value & vbCrLf
Next currRow
stream.Close
Call Shell(testString, vbNormalFocus)
Application.Wait (Now + TimeValue("0:00:01")) 'As sometime re-naming doesn't seem to happen without a pause before removing .bat file
Kill testString
MsgBox ("Renaming Complete")
End If
End Sub
Buttons code in Worksheet Batch Rename of Files
Private Sub CommandButton1_Click()
ListFilesInDirectory
End Sub
Private Sub CommandButton2_Click()
RenameFiles
End Sub
Example .bat file contents:
VERSION 2
And here is a different version using a dictionary and passing parameters from one sub to another. This would therefore be a macro associated with only one button push operation i.e. there wouldn't be a second button. The single button would call ListFiles which in turn calls the second macro. May require you to go in to tools > references and add in Microsoft Scripting Runtime reference.
Assumes you have a matching number of new file names in Col D of tab 1 as the number of files found in the folder (as per your script to obtain files in folder). I have removed the obsolete type references.Shout out to the RubberDuck VBA add-in crew for the add-in picking these up.
In one standard module:
Option Explicit
Public Sub ListFiles()
Dim xDirect As String, xFname As String, InitialFoldr As String
Dim wb As Workbook
Dim ws As Worksheet
Dim dict As New Scripting.Dictionary
Dim counter As Long
Set wb = ThisWorkbook
Set ws = wb.Worksheets("Tab1") 'Worksheet where new file names are
counter = 4 'row where new file names start
InitialFoldr = "C:\"
With Application.FileDialog(msoFileDialogFolderPicker)
.InitialFileName = Application.DefaultFilePath & "\"
.Title = "Please select a folder to list Files from"
.InitialFileName = InitialFoldr
.Show
If .SelectedItems.Count <> 0 Then
xDirect = .SelectedItems(1) & "\"
xFname = Dir(xDirect, 7)
Do While xFname <> vbNullString
If Not dict.Exists(xFname) Then
dict.Add xFname, ws.Cells(counter, "D") 'Or which ever column holds new file names. This add to the dictionary the current name and new name
counter = counter + 1
xFname = Dir
End If
Loop
End If
End With
RenameFiles xDirect, dict 'pass directory path and dictionary to renaming sub
End Sub
In another standard module:
Public Sub RenameFiles(ByVal folderpath As String, ByRef dict As Dictionary)
Dim fso As New FileSystemObject
Dim stream As TextStream
Dim strFile As String
Dim testString As String
Dim deskTopPath As String
strFile = "Rename.bat"
deskTopPath = Environ$("USERPROFILE") & "\Desktop"
testString = fso.BuildPath(deskTopPath, strFile)
'See if .dat file of same name already on desktop and delete (you could overwrite!)
If Len(Dir(testString)) <> 0 Then
SetAttr testString, vbNormal
Kill testString
End If
Set stream = fso.CreateTextFile(testString, True)
stream.Write "CD /D " & folderpath & vbCrLf
Dim key As Variant
For Each key In dict.Keys
stream.Write "Rename " & folderpath & key & " " & dict(key) & vbCrLf 'write out the command instructions to the .dat file
Next key
stream.Close
Call Shell(testString, vbNormalFocus)
Application.Wait (Now + TimeValue("0:00:01")) 'As sometime re-naming doesn't seem to happen without a pause before removing .bat file
Kill testString
' MsgBox ("Renaming Complete")
End Sub
Scripting run time reference:
Adding runtime reference
Additional method for finding the desktop path. Taken from Allen Wyatt:
In a standard module add the following:
Public Function GetDesktop() As String
Dim oWSHShell As Object
Set oWSHShell = CreateObject("WScript.Shell")
GetDesktop = oWSHShell.SpecialFolders("Desktop")
Set oWSHShell = Nothing
End Function
Then in the rest of the code replace any instances of deskTopPath =..... e.g.:
deskTopPath = Environ$("USERPROFILE") & "\Desktop"
With
desktopPath = GetDesktop

Convert embed images to linked

I'm trying to fix the macro, shown below.
It is intended to convert embed images to linked (via IncludePicture). However, in it's current state, images are added at the bottom of the document. Obviously, it's far from being perfect. Instead, macro should replace embed images with the linked ones, one by one, like shown here:
How to fix it?
Also, note: Macro should be launched from another file. So, you need two documents: one with macro and one with images. It's not good, but it's how it works currently.
Code:
Sub MakeDocMediaLinked()
Application.ScreenUpdating = False
Dim StrOutFold As String, Obj_App As Object, Doc As Document, Rng As Range
Dim StrDocFile As String, StrZipFile As String, StrMediaFile As String
With Application.Dialogs(wdDialogFileOpen)
If .Show = -1 Then
.Update
Set Doc = ActiveDocument
End If
End With
If Doc Is Nothing Then Exit Sub
With Doc
' ID the document to process
StrDocFile = .FullName
StrOutFold = Split(StrDocFile, ".")(0) & "_Media"
.Close SaveChanges:=False
End With
' Test for existing output folder, create it if it doesn't already exist
If Dir(StrOutFold, vbDirectory) = "" Then MkDir StrOutFold
' In case the output folder is not empty. Also, in case the file has no media
On Error Resume Next
' Delete any files in the output folder
Kill StrOutFold & "\*.*"
' Create a Shell App for accessing the zip archives
Set Obj_App = CreateObject("Shell.Application")
' Define the zip name
StrZipFile = Split(StrDocFile, ".")(0) & ".zip"
' Create the zip file, by simply copying to a new file with a zip extension
FileCopy StrDocFile, StrZipFile
' Extract the zip archive's media files to the temporary folder
Obj_App.NameSpace(StrOutFold & "\").CopyHere Obj_App.NameSpace(StrZipFile & "\word\media\").Items
' Delete the zip file - the loop takes care of timing issues
Do While Dir(StrZipFile) <> ""
Kill StrZipFile
Loop
' Restore error trapping
On Error GoTo 0
' Get the temporary folder's file listing
StrMediaFile = Dir(StrOutFold & "\*.*", vbNormal)
Documents.Open FileName:=StrDocFile
With ActiveDocument
' Process the temporary folder's files
While StrMediaFile <> ""
.Range.InsertAfter vbCr
Set Rng = .Paragraphs.Last.Range
.Fields.Add Range:=Rng, Type:=wdFieldEmpty, PreserveFormatting:=False, _
Text:="INCLUDEPICTURE """ & Replace(StrOutFold & "\" & StrMediaFile, "\", "\\") & """ \d"
' Get the next media file
StrMediaFile = Dir()
Wend
.Fields.Update
End With
Application.ScreenUpdating = True
End Sub
You could also parse the XML returned by Document.Content.XML to extract all the images. Then update each source with the path of the external image and write back the XML with Document.Content.InsertXML.
Writing the XML back automatically adds a linked field which seem to be one of your requirement.
It's faster that working with the clipboard and it doesn't alter the style of the shape. Though, you might need to tweak the code to handle specific cases.
Private Declare PtrSafe Function CryptStringToBinaryW Lib "Crypt32" (ByVal pszString As LongPtr, ByVal cchString As Long, ByVal dwFlags As Long, ByRef pbBinary As Byte, ByRef cbBinary As Long, ByVal pdwSkip As LongPtr, ByVal pdwFlags As LongPtr) As Boolean
Public Sub Example()
SaveAslinkedImages ActiveDocument, "c:\temp\myfile-no-img.docx"
End Sub
Public Sub SaveAslinkedImages(Doc As Document, fname As String)
Dim objXml As Object, binData As Object, binName$, nodes, node
Dim imgPath$, docDir$, imgDir$, i&, data() As Byte
Set objXml = VBA.CreateObject("Msxml2.DOMDocument.6.0")
objXml.Async = False
objXml.validateOnparse = False
' parse xml document '
objXml.LoadXML Doc.Content.XML
' add namespaces for SelectNodes '
objXml.setProperty "SelectionNamespaces", _
objXml.DocumentElement.getAttributeNode("xmlns:w").XML & " " & _
objXml.DocumentElement.getAttributeNode("xmlns:v").XML
' create the media folder '
docDir = Left(fname, InStrRev(fname, "\") - 1)
imgDir = Left(fname, InStrRev(fname, ".") - 1) & "_media"
MakeDir imgDir
' iterate each image data '
For Each binData In objXml.SelectNodes("//w:binData")
binName = binData.getAttribute("w:name")
' get all the nodes referencing the image data '
Set nodes = objXml.SelectNodes("//v:imagedata[#src='" & binName & "']")
If nodes.Length Then ' if any '
' build image path '
imgPath = imgDir & "\" & Mid(binName, InStrRev(binName, "/") + 1)
' save base64 data to file '
DecodeBase64 binData.Text, data
SaveBytesAs data, imgPath
' remove the data '
binData.ParentNode.RemoveChild binData
' for each image '
For Each node In nodes
' set id '
node.ParentNode.setAttribute "id", node.ParentNode.getAttribute("o:spid")
' remove o namespace '
node.ParentNode.Removeattribute "o:spid"
node.Removeattribute "o:title"
' set external image source '
node.setAttribute "src", imgPath
Next
End If
Next
' write back the xml and save the document '
Doc.Content.InsertXML objXml.XML
Doc.SaveAs2 fname
End Sub
Public Sub SaveBytesAs(data() As Byte, path As String)
Open path For Binary Access Write As #5
Put #5, 1, data
Close #5
End Sub
Public Sub MakeDir(path As String)
If Len(Dir(path, vbDirectory)) Then Exit Sub
MakeDir Left(path, InStrRev(path, "\") - 1)
MkDir path
End Sub
Public Function DecodeBase64(str As String, out() As Byte) As Boolean
Dim size As Long
size = ((Len(str) + 3) \ 4) * 3
ReDim out(0 To size - 1) As Byte
DecodeBase64 = CryptStringToBinaryW(StrPtr(str), Len(str), 1, out(0), size, 0, 0)
If size - 1 < UBound(out) Then ReDim Preserve out(0 To size - 1)
End Function
This is where your code is going astray:-
With ActiveDocument
.Range.InsertAfter vbCr
Set Rng = .Paragraphs.Last.Range
You are inserting a carriage return at the end of the document (which actually inserts a new blank paragraph) and then add a field in that paragraph. Obviously, you want the field somewhere else.
Meanwhile, if you wish to delete the links you should let your code do that. I haven't been able to figure out whether your code makes an attempt in that direction but presume that it extracts the picture's path from the link. So, the link should be located and deleted after giving up its path, and the field inserted in its place.
Here's my attempt. I did make an assumption that the shapes in the document would be a Inline Shape. I mocked this up on my computer with inline shapes.
Important Prerequistes
I'm using early binding of the Scripting.FileSystemObject and the Scripting.Dictionary. In order for this to function with no other changes to the code, please add a reference to the Microsoft Scripting Runtime.
How it works
The code iterates through each shape in the document chosen and saves each shape to a local folder. Once each shape is saved the shape is then deleted. From here the filename (key) and the range (value) of the InlineShape is saved into a dictionary. After this process has been done for each shape, the field with the INCLUDEPICTURE details are added by iterating through the dictionary to get the values needed.
Code
Option Explicit
Sub SOExample()
On Error GoTo Errhand:
Application.ScreenUpdating = False
Dim FileName As String
Dim doc As Document
Dim rng As Range ' Used to keep track of where the shape was before being deleted
Dim shp As Word.InlineShape 'I think you want to iterate inline shapes which generally are pictures
Dim i As Long ' Counter
Dim fso As FileSystemObject ' used for File Operations/etc
Dim tmpPics As String: tmpPics = GetDesktop & "Temp Pics" 'default folder on the desktop for temp storage
Dim picData() As Byte ' To hold picture information
Dim pos As Variant
Dim fileNumb As Long
'This section was untouched
With Application.Dialogs(wdDialogFileOpen)
If .Show = -1 Then
.Update
Set doc = ActiveDocument
End If
End With
'Make sure we have an object to work with
If doc Is Nothing Then Exit Sub
'Get a reference to FSO
Set fso = New FileSystemObject
'Delete files or create folder where needed
If fso.FolderExists(tmpPics) Then
fso.DeleteFile (tmpPics & "\*"), True
Else
fso.CreateFolder tmpPics
End If
'Create a dictionary to store the file name and range
'We need to do one pass through each image and save them, then delete the sheet
'As we go we are going to add the filename into our dictionary as the key, and -
'add the range of the remove image as the value. We use that range later to add the INCLUDEPICTURE portion
Dim mydict As New Scripting.Dictionary: Set mydict = New Scripting.Dictionary
'iterate each inlineShape...you may need to alter this as I'm unsure if this is the only type needed
'To be extracted. Sections of code grabbed from:
'https://stackoverflow.com/questions/6512392/how-to-save-word-shapes-to-image-using-vba
For Each shp In doc.InlineShapes
fileNumb = FreeFile
i = i + 1
'Build a temporary file name for our temp folder
FileName = tmpPics & "\Image " & CStr(i) & ".emf"
'Write the file as an EMF file
Open FileName For Binary Access Write As fileNumb
picData = shp.Range.EnhMetaFileBits
pos = 1
Put fileNumb, pos, picData
Close fileNumb
Set rng = shp.Range
'Add the details to our dictionary for iteration later
'I'm not adding the text here as, at least for me, adding this field adds another shape
'On the next iteration, it was trying to apply the same steps...creating what I'm assuming is an inifinite loop
If Not mydict.Exists(FileName) Then mydict.Add FileName, rng
shp.Delete
Set rng = Nothing
Next
Dim var As Variant
'Go through our dictionary, and add the fields into our document
For Each var In mydict.Keys
doc.Fields.Add Range:=mydict(var), _
Text:="INCLUDEPICTURE """ & Replace(var, "\", "\\") & """ \d"
Next
CleanExit:
Application.ScreenUpdating = True
Exit Sub
Errhand:
Debug.Print Err.Number, Err.Description
Select Case Err.Number
'Add error handler here
End Select
Resume CleanExit
End Sub
'A small helper function to get a path to the desktop
Private Function GetDesktop() As String
Dim oWSHShell As Object: Set oWSHShell = CreateObject("WScript.Shell")
GetDesktop = oWSHShell.SpecialFolders("Desktop") & "\"
Set oWSHShell = Nothing
End Function
One way would be to copy the image to the clipboard with Selection.Copy and to save it as a PNG from there. Then replace the image with an external link with Document.InlineShapes.AddPicture.
To handle the duplicated images, hash each image and keep track of the computed code.
I would also rescale the shape before an after to keep the original resolution.
Public Sub Example()
SaveAsExternImages ActiveDocument, "c:\temp\myfile-no-img.docx"
End Sub
Public Sub SaveAsExternImages(doc As Document, fname As String)
Dim sh As InlineShape, rg As Range, docDir, imgDir, imgPath, imgHash
Dim hDib, scaleW, scaleH, i As Long
Dim imgPaths As New Collection
Dim imgs As New Collection
' create the media folder and set the relative directory '
docDir = Left(fname, InStrRev(fname, "\") - 1)
imgDir = Left(fname, InStrRev(fname, ".") - 1) & "_media"
MakeDir imgDir
' clean clipboard '
Call OpenClipboard: Call EmptyClipboard: Call CloseClipboard
' select images '
For Each sh In doc.InlineShapes
Select Case sh.Type
Case wdInlineShapeLinkedPicture, wdInlineShapePicture
imgs.Add sh
End Select
Next
' handle each image '
For Each sh In imgs
' store/reset the scale '
scaleW = sh.ScaleWidth
scaleH = sh.ScaleHeight
sh.ScaleWidth = 100
sh.ScaleHeight = 100
' copy shape to the clipboard '
sh.Select
doc.Application.Selection.Copy
' get clipboard as DIB (device independent bitmap) '
If OpenClipboard() Then Else Err.Raise 9, , "OpenClipboard failed"
hDib = GetClipboardData(8) ' 8 = CF_DIB = BITMAPINFO '
If hDib Then Else Err.Raise 9, , "GetClipboardData failed"
' get image hash code from DIB (CRC32) '
imgHash = GetDIBHashCode(hDib)
' save as PNG if hash not already present in the collection '
If TryGetValue(imgPaths, imgHash, imgPath) = False Then
i = i + 1
imgPath = SaveDIBtoPNG(hDib, imgDir & "\image" & i & ".png")
imgPath = Mid(imgPath, Len(docDir) + 2) ' make relative '
imgPaths.Add imgPath, CStr(imgHash)
End If
' dispose clipboard '
Call EmptyClipboard
Call CloseClipboard
' replace the shape with a linked picture and restore the scale '
Set rg = sh.Range
sh.Delete
doc.Application.ChangeFileOpenDirectory docDir ' set relative folder '
Set sh = doc.InlineShapes.AddPicture(imgPath, True, False, rg)
sh.ScaleWidth = scaleW
sh.ScaleHeight = scaleH
Next
doc.SaveAs2 fname
End Sub
Related functions/procedures:
Private Declare PtrSafe Function CLSIDFromString Lib "ole32" (ByVal lpsz As LongPtr, pclsid As Byte) As Long
Private Declare PtrSafe Function RtlComputeCrc32 Lib "ntdll" (ByVal start As Long, ByRef data As Any, ByVal Size As Long) As Long
Private Declare PtrSafe Function GlobalLock Lib "kernel32" (ByVal hMem As LongPtr) As LongPtr
Private Declare PtrSafe Function GlobalUnlock Lib "kernel32" (ByVal hMem As LongPtr) As Boolean
Private Declare PtrSafe Function GlobalSize Lib "kernel32" (ByVal hMem As LongPtr) As LongPtr
Private Declare PtrSafe Function OpenClipboard Lib "user32" (Optional ByVal hwnd As LongPtr) As LongPtr
Private Declare PtrSafe Function GetClipboardData Lib "user32" (ByVal wFormat As Integer) As LongPtr
Private Declare PtrSafe Function EmptyClipboard Lib "user32" () As Long
Private Declare PtrSafe Function CloseClipboard Lib "user32" () As Long
Private Declare PtrSafe Function GdiplusStartup Lib "gdiplus" (token As LongPtr, cfg As Any, ByVal hook As LongPtr) As Long
Private Declare PtrSafe Function GdiplusShutdown Lib "gdiplus" (ByVal token As LongPtr) As Long
Private Declare PtrSafe Function GdipCreateBitmapFromGdiDib Lib "gdiplus" (ByVal hdr As LongPtr, ByVal data As LongPtr, img As LongPtr) As Long
Private Declare PtrSafe Function GdipSaveImageToFile Lib "gdiplus" (ByVal img As LongPtr, ByVal path As LongPtr, riid As Byte, ByVal cfg As LongPtr) As Long
Private Declare PtrSafe Function GdipDisposeImage Lib "gdiplus" (ByVal img As LongPtr) As Long
Private Function GetDIBHashCode(hDib) As Long
Dim pDib As LongPtr, bmSize As Long, sz As Long
pDib = GlobalLock(hDib)
If pDib Then Else Err.Raise 9, , "GlobalLock failed"
GetDIBHashCode = RtlComputeCrc32(0, ByVal pDib, GlobalSize(hDib))
GlobalUnlock hDib
End Function
Private Function SaveDIBtoPNG(hDib, filePath As String) As String
Dim cfg(0 To 7) As Long, clsid(0 To 15) As Byte, pDib As LongPtr, hGdi As LongPtr, hImg As LongPtr
CLSIDFromString StrPtr("{557CF406-1A04-11D3-9A73-0000F81EF32E}"), clsid(0) ' PNG encoder '
cfg(0) = 1& ' GdiplusVersion '
pDib = GlobalLock(hDib) ' lock BITMAPINFOHEADER + image bytes '
If pDib Then Else Err.Raise 9, , "GlobalLock failed"
If GdiplusStartup(hGdi, cfg(0), 0) Then Err.Raise 9, , "GdiplusStartup failed"
If GdipCreateBitmapFromGdiDib(pDib, pDib + 40, hImg) Then Err.Raise 9, , "GdipCreateBitmapFromGdiDib failed"
If GdipSaveImageToFile(hImg, StrPtr(filePath), clsid(0), 0) Then Err.Raise 9, , "GdipSaveImageToFile failed"
If GdipDisposeImage(hImg) Then Err.Raise 9, , "GdipDisposeImage failed"
If GdiplusShutdown(hGdi) Then Err.Raise 9, , "GdiplusShutdown failed"
GlobalUnlock hDib
SaveDIBtoPNG = filePath
End Function
Private Function TryGetValue(obj As Collection, Key, outValue) As Boolean
On Error Resume Next
outValue = obj.Item(CStr(Key))
TryGetValue = Err.Number = 0
End Function
Private Sub MakeDir(path)
If Len(Dir(path, vbDirectory)) = False Then
MkDir path
ElseIf Len(Dir(path & "\")) Then
Kill path & "\*"
End If
End Sub
By locating each image and put the link in its position this code will acheive what you want. Note that the original file will be overwritten if you save the modified document. See my comments in the code for more info.
Code now works for duplicates as well
Option Explicit
Const IMAGEBASENAME = "image"
Const IMAGEEXTENSION = ".jpeg" 'Images in .zip file are all .jpg
Sub MakeDocMediaLinked()
Dim StrOutFold As String
Dim Obj_App As Object
Dim Doc As Document
Dim Rng As Range
Dim StrDocFile As String
Dim StrZipFile As String
Dim StrMediaFile As String
Dim objShape As InlineShape
Dim imgNum As Integer
Dim imgCount As Integer
Dim imgName As String
Dim imgNames As New Collection
Dim i As Integer
Dim doDir As Boolean
Application.ScreenUpdating = False
With Application.Dialogs(wdDialogFileOpen)
If .Show = -1 Then
.Update
Set Doc = ActiveDocument
End If
End With
If Doc Is Nothing Then Exit Sub
With Doc
StrDocFile = .FullName ' ID the document to process
StrOutFold = Split(StrDocFile, ".")(0) & "_Media"
.Close SaveChanges:=False
End With
If Dir(StrOutFold, vbDirectory) = "" Then MkDir StrOutFold ' Test for existing output folder, create it if it doesn't already exist
'*
'* Delete any files in the output folder. On Error Resume Next not used
'*
If Dir(StrOutFold & "\*.*", vbNormal) <> "" Then Kill StrOutFold & "\*.*"
' Create a Shell App for accessing the zip archives
Set Obj_App = CreateObject("Shell.Application")
' Define the zip name
StrZipFile = Split(StrDocFile, ".")(0) & ".zip"
' Create the zip file, by simply copying to a new file with a zip extension
FileCopy StrDocFile, StrZipFile
' Extract the zip archive's media files to the temporary folder
Obj_App.NameSpace(StrOutFold & "\").CopyHere Obj_App.NameSpace(StrZipFile & "\word\media\").Items
Do While Dir(StrZipFile) <> "" ' Delete the zip file - the loop takes care of timing issues
Kill StrZipFile
Loop
StrMediaFile = Dir(StrOutFold & "\*.*", vbNormal) ' Get the temporary folder's file listing
Documents.Open FileName:=StrDocFile
With ActiveDocument
imgCount = .InlineShapes.Count
For imgNum = 1 To imgCount
'*
'* Get the (next) image
'*
Set objShape = .InlineShapes(imgNum)
'*
'* Get the original full path of the image
'*
imgName = objShape.AlternativeText
'*
'* Look for possible duplicate
'*
'* Add the ordinal number as the item and the path as the key to avoid duplicates
'* If we get an error here then the image is a duplicate of a previous one
'* The ordinal number in imgNames identifies the image to use in the _Media folder
'*
i = imgNames.Count 'Current count
doDir = True ' Assume no duplicate
On Error Resume Next
imgNames.Add imgNum, imgName
On Error GoTo 0 'Always reset error handling after Resume
If i = imgNames.Count Then 'Duplicate found, build the duplicate's file name
StrMediaFile = IMAGEBASENAME & imgNames(imgName) & IMAGEEXTENSION
doDir = False 'Do not read a new file
End If
'*
'* Get the range where we want the link to appear
'*
Set Rng = objShape.Range
'*
'* Delete the image from the document
'*
objShape.Delete
'*
'* Replace the image with a link to a saved disk image in the *_Media folder
'*
.Fields.Add Range:=Rng, Type:=wdFieldEmpty, PreserveFormatting:=False, _
Text:="INCLUDEPICTURE """ & Replace(StrOutFold & "\" & StrMediaFile, "\", "\\") & """ \d"
If doDir Then StrMediaFile = Dir() ' Get the next media file since we had no duplicate this time
Next imgNum
.Fields.Update
End With
Set imgNames = Nothing
Application.ScreenUpdating = True
End Sub
John, yet another attempt. Runs fine with your test document and my docs as well.
Made the code 2 pass.
I found that sometimes original .jpg files will be saved as .jpeg files in the .zip file
Also sometimes .png files will be saved in .zip file as .jpeg.
I did not put any effort on finding out why. Instead I modified my code to cope with this fact.
Here is the result which will handle any number of duplicates.
'********************************************************************
'* Replace original images with links to locally extracted images
'* Ver. 1.02 2017-10-04 peakpeak
'*
Option Explicit
Const IMAGEBASENAME = "image"
Const JPEG = "jpeg"
Const JPG = "jpg"
Sub MakeDocMediaLinked()
Dim Doc As Document
Dim Rng As Range
Dim StrOutFold As String
Dim StrDocFile As String
Dim StrZipFile As String
Dim imgName As String
Dim StrMediaFile As String
Dim imgNum As Integer
Dim imgCount As Integer
Dim i As Integer
Dim ordinalNum As Integer
Dim imgOrdinals As New Collection
Dim objShape As InlineShape
Dim Obj_App As Object
Application.ScreenUpdating = False
With Application.Dialogs(wdDialogFileOpen)
If .Show = -1 Then
.Update
Set Doc = ActiveDocument
End If
End With
If Doc Is Nothing Then Exit Sub
With Doc
StrDocFile = .FullName ' ID the document to process
StrOutFold = Split(StrDocFile, ".")(0) & "_Media"
.Close SaveChanges:=False
End With
If Dir(StrOutFold, vbDirectory) = "" Then MkDir StrOutFold ' Test for existing output folder, create it if it doesn't already exist
'*
'* Delete any files in the output folder. On Error Resume Next not used
'*
If Dir(StrOutFold & "\*.*", vbNormal) <> "" Then Kill StrOutFold & "\*.*"
' Create a Shell App for accessing the zip archives
Set Obj_App = CreateObject("Shell.Application")
' Define the zip name
StrZipFile = Split(StrDocFile, ".")(0) & ".zip"
' Create the zip file, by simply copying to a new file with a zip extension
FileCopy StrDocFile, StrZipFile
' Extract the zip archive's media files to the temporary folder
Obj_App.NameSpace(StrOutFold & "\").CopyHere Obj_App.NameSpace(StrZipFile & "\word\media\").Items
Do While Dir(StrZipFile) <> "" ' Delete the zip file - the loop takes care of timing issues
Kill StrZipFile
Loop
StrMediaFile = Dir(StrOutFold & "\*.*", vbNormal) ' Get the temporary folder's file listing
Documents.Open FileName:=StrDocFile
With ActiveDocument
imgCount = .InlineShapes.Count
'*
'* Pass 1, collect ordinal numbers for all unique images
'*
ordinalNum = 1
For imgNum = 1 To imgCount
Set objShape = .InlineShapes(imgNum)
imgName = objShape.AlternativeText 'Contains the full path to the original inserted image
i = imgOrdinals.Count 'Current count of image ordinals
On Error Resume Next
imgOrdinals.Add ordinalNum, imgName 'Error if duplicate
On Error GoTo 0 'Always reset error handling after Resume
If i <> imgOrdinals.Count Then ordinalNum = ordinalNum + 1 'Ordinal added
Next imgNum
'*
'* Pass 2, replace images with links
'*
For imgNum = 1 To imgCount
'*
'* Get the (next) image
'*
Set objShape = .InlineShapes(imgNum)
'*
'* Get the original full path of the image
'*
imgName = objShape.AlternativeText 'Contains the full path to the original inserted image
'*
'* Original extension and extension in the .zip file might differ due to internal algorithms in Word
'* Get the image file name in *_Media folder based on its ordinal number and regardless of original extension
'*
StrMediaFile = Dir(StrOutFold & "\" & IMAGEBASENAME & imgOrdinals(imgName) & ".*", vbNormal)
'*
'* Get the range where we want the link to appear
'*
Set Rng = objShape.Range
'*
'* Delete the image from the document
'*
objShape.Delete
'*
'* Replace the image with a link to a saved disk image in the *_Media folder
'*
.Fields.Add Range:=Rng, Type:=wdFieldEmpty, PreserveFormatting:=False, _
Text:="INCLUDEPICTURE """ & Replace(StrOutFold & "\" & StrMediaFile, "\", "\\") & """ \d"
Next imgNum
.Fields.Update
End With
Set imgOrdinals = Nothing
Application.ScreenUpdating = True
End Sub
New solution
Method
For each InlineShape (working in reverse), if it's a wdInlineShapePicture
Copy it to a temporary document
Save the temporary document as .docx
Copy the temporary document as a .zip file
Extract the contents of the *.zip/word/media folder to a temporary folder
Move and rename the only file in that folder to the destination folder
Delete the shape
Create a field that links to the newly processed file, where the shape used to be
Code
Option Explicit
Sub Example()
MakeDocMediaLinked ActiveDocument
End Sub
Sub MakeDocMediaLinked(ByRef Doc As Document)
' iterate through each image
Dim i As Long
Dim shapeCollection As InlineShapes
Dim tempDoc As Document
Dim fso As New FileSystemObject ' early binding; add a reference to Microsoft Scripting Runtime (scrrun.dll)
Dim oShell As New Shell32.Shell ' early binding; add a reference to Microsoft Shell Controls and Automation (shell32.dll)
Dim currentMediaFileNameSource As String
Dim currentMediaFileNameNew As String
Dim shp As InlineShape
Dim rngToRemove As Range, rngToInsertInto As Range
Const tempDocFilePathDoc As String = "C:\test\temp.docx"
Const tempDocFilePathZip As String = "C:\test\temp.zip"
Const tempMediaFolderPath As String = "C:\test\temp\"
Const destMediaFolderPath As String = "C:\test\images\"
MakePath tempMediaFolderPath ' make the temporary folder in which to store an image, if it doesn't already exist
MakePath destMediaFolderPath ' make the images folder in which to store the images, if it doesn't already exist
Set tempDoc = Application.Documents.Add(Visible:=False) ' create the temp doc, hide it
tempDoc.SaveAs2 FileName:=tempDocFilePathDoc ' save the temp doc
Set shapeCollection = Doc.InlineShapes
For i = shapeCollection.Count To 1 Step -1 ' working backwards through the collection
Set shp = shapeCollection(i)
If shp.Type = wdInlineShapePicture Then
tempDoc.Range.Delete ' clear the temp doc
tempDoc.Range.FormattedText = shp.Range.FormattedText ' copy the image into the temp doc
tempDoc.Save ' save the temp doc
fso.CopyFile tempDocFilePathDoc, tempDocFilePathZip ' copy the temp doc and rename to a temp zip file (will overwrite existing zip)
oShell.NameSpace(tempMediaFolderPath).CopyHere oShell.NameSpace(tempDocFilePathZip & "\word\media\").Items ' copy the one media file to a destination
currentMediaFileNameSource = Dir(tempMediaFolderPath) ' get the name of the media file
currentMediaFileNameNew = "media-" & i & Mid(currentMediaFileNameSource, InStrRev(currentMediaFileNameSource, ".")) ' names the files media-4.jpeg, media-3.png, etc.
fso.CopyFile tempMediaFolderPath & currentMediaFileNameSource, destMediaFolderPath & currentMediaFileNameNew ' copy and rename the file into the destination folder
fso.DeleteFile tempMediaFolderPath & currentMediaFileNameSource, True ' delete the temporary file
Set rngToRemove = shp.Range ' set the range that we will be removing, i.e. the shape range
Set rngToInsertInto = shp.Range ' set the range that we will be inserting the field into, i.e. the start of the shape range (1)
rngToInsertInto.Collapse wdCollapseStart ' set the range that we will be inserting the field into, i.e. the start of the shape range (2)
rngToRemove.Delete ' remove the shape
Doc.Fields.Add Range:=rngToInsertInto, Type:=wdFieldEmpty, PreserveFormatting:=False, _
Text:="INCLUDEPICTURE """ & Replace(destMediaFolderPath & currentMediaFileNameNew, "\", "\\") & """ \d" ' 4. add the field, we refer to destMediaFolderPath & currentMediaFileNameNew in the field definition
End If
Next i
tempDoc.Close SaveChanges:=False ' close the temp doc
fso.DeleteFile tempDocFilePathZip, True ' delete the temporary zip
fso.DeleteFile tempDocFilePathDoc, True ' delete the temporary doc
fso.DeleteFolder Left(tempMediaFolderPath, Len(tempMediaFolderPath) - 1), True ' delete the temporary folder
Set fso = Nothing
Set oShell = Nothing
End Sub
Sub MakePath(ByVal tempPath As String)
Dim fso As New FileSystemObject
Dim path() As String
Dim path2() As String
Dim i As Long
Do While Right(tempPath, 1) = "\" ' remove any ending slashes
tempPath = Left(tempPath, Len(tempPath) - 1)
Loop
path = Split(tempPath, "\")
ReDim path2(LBound(path) To UBound(path))
i = LBound(path)
path2(i) = path(i)
If Not fso.FolderExists(path2(i) & "\") Then Exit Sub ' if the drive doesn't even exist, then exit
For i = LBound(path) + 1 To UBound(path)
path2(i) = path2(i - 1) & "\" & CleanPath(path(i))
If Not fso.FolderExists(path2(i) & "\") Then fso.CreateFolder path2(i)
Next i
Set fso = Nothing
End Sub
Function CleanPath(ByVal tempPath As String)
Dim i As Long
Dim invalidChars As Variant
invalidChars = Array("/", ":", "*", "?", """", "<", ">", "|")
For i = LBound(invalidChars) To UBound(invalidChars)
tempPath = Replace(tempPath, invalidChars(i), " ")
Next i
CleanPath = tempPath
End Function
After
images folder
document (showing fields)

Is there method similar to 'Find' available when we Loop through folder (of files) using Dir Function in excel vba?

As we know, we use Find() method to find whether a string or any Microsoft Excel data type exists in an excel.
(Usually we do it on set of data)
I want to know if any such method available when we loop through folder(of files) using Dir function.
Situation:
I have an excel - 'FileNames.xlsx' in which 'Sheet1' has names of files having extensions .pdf/.jpg/.jpeg/.xls/.xlsx/.png./.txt/.docx/ .rtf in column A.
I have a folder named 'Folder' which has most(or all) of the files from 'FileNames.xlsx'.
I have to check whether all the file-names mentioned in the 'FileNames.xlsx' exist in 'Folder'.
For this I have written the below VBScript(.vbs):
strMessage =Inputbox("Enter No. of Files in Folder","Input Required")
set xlinput = createobject("excel.application")
set wb123 =xlinput.workbooks.Open("E:\FileNames.xlsx")
set sh1 =wb123.worksheets("Sheet1")
For i = 2 to strMessage +1
namei = sh1.cells(i,1).value
yesi = "E:\Folder"+ namei +
If namei <> yesi Then
sh1.cells(i,1).Interior.Color = vbRed
Else
End If
Next
msgbox "Success"
xlinput.quit
As I wasn't able to get the required Output I tried it recording a small Excel VBA Macro. (Changed FileNames.xlsx to FileNames.xlsm)
Sub LoopThroughFiles()
Dim lastRow As Long
lastRow = Sheets("Sheet1").UsedRange.Rows.Count
Dim MyFolder As String
Dim filename As Range
Dim MyFile As String
MyFolder = "E:\Folder"
For Each filename In Worksheets("Sheet1").Range("A2A:" & lastRow)
MyFile = Dir(MyFolder & "\*.xlsx")
'Here I actually need to pass all file extensions to Dir
Do While MyFile <> ""
If filename = MyFile Then
'Do Nothing
Else
filename.Interior.Color = vbRed
MyFile = Dir
Next
End Sub
The above is a failed attempt.
I thought of trying it with method similar to Find()
Sub LoopThroughFiles()
Dim lastRow As Long
'Dim LastFile As Long
'Is there need of it (LastFile variable)? I kept this variable
'to save (prior known) count of files in folder.
lastRow = Sheets("Sheet1").UsedRange.Rows.Count
'LastFile = 'Pass count of Files in folder to this variable.
Dim fileName As Range
For Each fileName In Worksheets("Sheet1").Range("A2:A" & lastRow)
Dim rngFnder As Range
On Error Resume Next
'Error at below line.
Set rngFnder = Dir("E:\Folder\").Find(filename)
'This line gives me error 'Invalid Qualifier'
'I am trying to use method similar to Find()
If rngFnder Is Nothing Then
filename.Interior.Color = vbRed
End If
Next
End Sub
But, I couldn't achieve the result. Can anyone tell me is there any such function available to 'Find' whether all filenames in an excel exist in a folder after looping through folder using Dir?
As per my knowledge, Dir function works with only one file extension at a time.
Is it possible to use Dir function for multiple file extensions at a time?
Expected Output:
Assume I have 8 filenames in 'FileNames(.xlsx/.xlsm)'. Out of which Arabella.pdf and Clover.png are not found in 'Folder', Then I want to color cells for these filenames in red background in excel as in below image.
Sub LoopThroughFiles()
Dim lastRow As Long
lastRow = Sheets("Sheet1").UsedRange.Rows.Count
Dim MyFolder As String
Dim filename As Range
Dim MyFile As String
MyFolder = "E:\Folder"
For Each filename In Worksheets("Sheet1").Range("A2:A" & lastRow)
MyFile = MyFolder & "\" & filename
If Not FileExists(MyFile) Then
filename.Interior.Color = vbRed
End If
Next
End Sub
Public Function FileExists(strFullpathName As String) As Boolean
If Dir(strFullpathName) <> "" Then
FileExists = True
Else
FileExists = False
End If
End Function
You can output a list of the files that are contained in the folder. I found a really helpful tutorial on that here: http://software-solutions-online.com/2014/03/05/list-files-and-folders-in-a-directory/#Jump1
If you then loop through both the original and the output lists and look for a match. Easiest is to first colour them all red, and un-colour the matches. Else you would need an additional if-statement that states: When you reach the last element in the original list, and no match has been found, then colour red.
Edit: For continuity's sake I copied the code bits of the link I mentioned above:
Getting all file names form within 1 folder:
Sub Example1()
Dim objFSO As Object
Dim objFolder As Object
Dim objFile As Object
Dim i As Integer
'Create an instance of the FileSystemObject
Set objFSO = CreateObject("Scripting.FileSystemObject")
'Get the folder object
Set objFolder = objFSO.GetFolder("D:StuffFreelancesWebsiteBlogArraysPics")
i = 1
'loops through each file in the directory and prints their names and path
For Each objFile In objFolder.Files
'print file name
Cells(i + 1, 1) = objFile.Name
'print file path
Cells(i + 1, 2) = objFile.Path
i = i + 1
Next objFile
End Sub

list full details from files in folders and sub folder in excel

I'm trying to create a document register which will in the end list certain files within a chosen folder and all sub folders, currently I have the code which is below which lists the files and their path. Though I can't think about what I need to add to this code to get the excel sheet to create a column which lists the file type "PDF","TXT",DWG" etc. Then another column which uses a predefined list to show what type of file these are (i.e. pdf=document, DWG=CAD file etc.).
The next thing I want to add is then a hyperlink which is generated from the path column.
Finally is there a way in which I can make excel ignore previously collected data, as the folder which the data will be collected from is updated regularly and I would like to be able to just run the VBA so it will ignore sub folders which is has already pulled data from.
Any help would be most appreciated.
Option Explicit
'the first row with data
Const ROW_FIRST As Integer = 5
'This is an event handler. It exectues when the user
'presses the run button
Private Sub btnGet_Click()
'determines if the user selects a directory
'from the folder dialog
Dim intResult As Integer
'the path selected by the user from the
'folder dialog
Dim strPath As String
'Filesystem object
Dim objFSO As Object
'the current number of rows
Dim intCountRows As Integer
Application.FileDialog(msoFileDialogFolderPicker).Title = _
"Select a Path"
'the dialog is displayed to the user
intResult = Application.FileDialog( _
msoFileDialogFolderPicker).Show
'checks if user has cancled the dialog
If intResult <> 0 Then
strPath = Application.FileDialog(msoFileDialogFolderPicker _
).SelectedItems(1)
'Create an instance of the FileSystemObject
Set objFSO = CreateObject("Scripting.FileSystemObject")
'loops through each file in the directory and prints their
'names and path
intCountRows = GetAllFiles(strPath, ROW_FIRST, objFSO)
'loops through all the files and folder in the input path
Call GetAllFolders(strPath, objFSO, intCountRows)
End If
End Sub
'''
'This function prints the name and path of all the files
'in the directory strPath
'strPath: The path to get the list of files from
'intRow: The current row to start printing the file names
'in
'objFSO: A Scripting.FileSystem object.
Private Function GetAllFiles(ByVal strPath As String, _
ByVal intRow As Integer, ByRef objFSO As Object) As Integer
Dim objFolder As Object
Dim objFile As Object
Dim i As Integer
i = intRow - ROW_FIRST + 1
Set objFolder = objFSO.GetFolder(strPath)
For Each objFile In objFolder.Files
'print file name
Cells(i + ROW_FIRST - 1, 1) = objFile.Name
'print file path
Cells(i + ROW_FIRST - 1, 2) = objFile.Path
i = i + 1
Next objFile
GetAllFiles = i + ROW_FIRST - 1
End Function
'''
'This function loops through all the folders in the
'input path. It makes a call to the GetAllFiles
'function. It also makes a recursive call to itself
'strFolder: The folder to loop through
'objFSO: A Scripting.FileSystem object
'intRow: The current row to print the file data on
Private Sub GetAllFolders(ByVal strFolder As String, _
ByRef objFSO As Object, ByRef intRow As Integer)
Dim objFolder As Object
Dim objSubFolder As Object
'Get the folder object
Set objFolder = objFSO.GetFolder(strFolder)
'loops through each file in the directory and
'prints their names and path
For Each objSubFolder In objFolder.subfolders
intRow = GetAllFiles(objSubFolder.Path, _
intRow, objFSO)
'recursive call to to itsself
Call GetAllFolders(objSubFolder.Path, _
objFSO, intRow)
Next objSubFolder
End Sub
Solution: Make the following changes in Function GetAllFiles - it works for me:
After Dim i As Integer, add:
Dim Extension As String
After Cells(i + ROW_FIRST - 1, 2) = objFile.Path, add:
Extension = Right(objFile.Path, Len(objFile.Path) - InStrRev(objFile.Path, "."))
Cells(i + ROW_FIRST - 1, 3) = Extension
Cells(i + ROW_FIRST - 1, 4) = objFile.Type
Cells(i + ROW_FIRST - 1, 5).Formula = "=HYPERLINK(""" & objFile.Path & """,""Link"")"
Explanation: The Extension variable is populated by looking for a dot . in the filename and only using what is right of the dot. This is then added to the next column. The description of that extension is taken from the file object's Type attribute. Lastly, the rightmost column is filled with the =HYPERLINK function pointing at the file and its path.
Edit: Edited after hint of #TimWilliams (thank you!), I simplified the above code. If you need customized file type descriptions, use the below approach instead and replace
Cells(i + ROW_FIRST - 1, 4) = objFile.Type
with
On Error Resume Next
Cells(i + ROW_FIRST - 1, 4) = Application.WorksheetFunction.VLookup(Extension, _
ActiveWorkbook.Sheets("filetypes").Range("A:B"), 2, False)
Before running this, you need to add one worksheet called filetypes and put the most common extensions in column A and their long text / explanation into column B:
To get a list like that without much work, you could copy what you find on this website, and just remove the dots . using the Search & Replace function.