Related
Private Declare PtrSafe Function ShellExecute _
Lib "shell32.dll" Alias "ShellExecuteA" (ByVal hwnd As Long, ByVal lpOperation As String, ByVal lpFile As String, _
ByVal lpParameters As String, ByVal lpDirectory As String, ByVal nShowCmd As Long) As Long
Const SW_NORMAL = 1
Sub mensaje_masivo()
Dim ran As Range
Dim x
Dim mensaje As String
Application.ScreenUpdating = False
For Each ran In Hoja1.Range("TablaEnvio[mensase a enviar]")
mensaje = VBA.Replace("whatsapp://send?phone=" & ran.Offset(0, 1).Value & "&text=" & ran.Offset(0, 0) & " Saludos.", " ", "%20")
x = ShellExecute(hwnd, "Open", mensaje, &O0, &O0, SW_NORMAL)
Call SendKeys("{ENTER}", True)
Application.Wait Now + TimeValue("00:00:03")
Call SendKeys("{ENTER}", True)
Windows(ThisWorkbook.Name).Activate
Next ran
Application.ScreenUpdating = True
MsgBox "Mensajes enviados con exito.", vbInformation
End Sub
Hi guys, I have a probelm with this code. the messagens not send and I think that problen is the new version of whatsapp deskopt
I have been using this code to download images with excel and rename files, but all of sudden the size of the downloaded file is coming 1.47 kb and the file is not readable.
Can you help where i am going wrong.
Any help will be appreciated.
Option Explicit
Private Declare PtrSafe Function URLDownloadToFile Lib "urlmon" _
Alias "URLDownloadToFileA" (ByVal pCaller As Long, _
ByVal szURL As String, ByVal szFileName As String, _
ByVal dwReserved As Long, ByVal lpfnCB As Long) As Long
Dim Ret As Long
Sub Sample()
Dim FolderName As String
Dim ws As Worksheet
Dim LastRow As Long, i As Long
Dim strPath As String
FolderName = Range("$B$2").Value
Set ws = Sheets("Sheet1")
LastRow = ws.Range("A" & Rows.Count).End(xlUp).Row
For i = 4 To LastRow
strPath = FolderName & ws.Range("A" & i).Value & ".jpg"
Ret = URLDownloadToFile(0, ws.Range("B" & i).Value, strPath, 0, 0)
If Ret = 0 Then
ws.Range("C" & i).Value = "Downloaded"
Else
ws.Range("C" & i).Value = "Error"
End If
Next i
End Sub
I found an answer in other site: Try investigating the downloaded file using some text editor, Notepad, for example. Maybe, instead of a JPG file, you will see in "filename.jpg" a text or HTML containing some error message!, perhaps related to security stuffs (in my case, Google was asking for login in google to download the file). Maybe the file is not publicly available, and you cannot download it using your code because Google expects a password.
I am trying to export an Excel chart in SVG format using VBA.
Set objChrt = ActiveChart.Parent
objChrt.Activate
Set curChart = objChrt.Chart
curChart.Export fileName:=fileName, FilterName:="SVG"
If I replace "SVG" by "PNG", the export works exactly as intended and produces a valid PNG file. However, "SVG" results in an empty file. (Manually, there is an option to save as SVG inside Excel 365, so the export filter exists).
According to the documentation, Filtername is "The language-independent name of the graphic filter as it appears in the registry.", but I couldn't find anything like that in the registry, and either way, it's hard to imagine the SVG filtername being named anything other than "SVG".
Is there a way to export a Chart in SVG format using VBA?
Note: There is another question about Chart.export producing an empty file, and the fix was to use ChartObject.Activate before the export. This question is different because the code works correctly with "PNG" but fails with "SVG" (so it's not an issue related to activation or visibility). Also the recommended fix does not work.
Exporting in vector format:
If your main issue is exporting the charts in some vector format, I recommend just exporting as PDF, as this is very easy:
Set curChart = objChrt.Chart
objChrt.ExportAsFixedFormat xlTypePDF, "YourChart"
The PDF now contains your chart as a vector graphic and PDF is a widely supported format for further processing.
If you absolutely need to convert the chart to .svg you can do so from the command line (and therefore easily automatable) using the open-source software Inkscape or so I thought :/.
Converting to SVG:
Unfortunately, the Inkscape conversion didn't seem to work for me so I implemented it using the open-source pdf rendering toolkit Poppler. (Install instructions at the bottom of this post)
This library provides the command line utility pdftocairo, which will be used in the following solution:
Sub ExportChartToSVG()
Dim MyChart As ChartObject
Set MyChart = Tabelle1.ChartObjects("Chart 1")
Dim fileName As String
fileName = "TestExport"
Dim pathStr As String
pathStr = ThisWorkbook.Path
' Export chart as .pdf
MyChart.Chart.ExportAsFixedFormat Type:=xlTypePDF, _
FileName:=pathStr & "\" & fileName
' Convert .pdf file to .svg
Dim ret As Double
ret = Shell("cmd.exe /k cd /d """ & pathStr & """ & " & _
"pdftocairo -svg -f 1 -l 1 " & fileName & ".pdf", vbHide)
End Sub
Note that the text in the resulting .svg file isn't selectable and the file is larger than the file generated by manual export (241 KB vs. 88 KB in my test). The file is definitely infinite resolution, so not that weird bitmap embedded in a .svg file one occasionally sees but comes with another little problem:
Unfortunately, the ExportAsFixedFormat method creates a PDF 'page' where the graphic is positioned on the page depending on the position on the worksheet. The .svg conversion unfortunately keeps this 'page' format. I had to learn that getting rid of this problem is not as simple as I initially thought because excel does not support custom page sizes and therefore exporting a chart as .pdf without white borders seems pretty much impossible, see this bountied but unsolved question (Edit: I solved it in the following part and also posted my method as answer to that question). I tried several methods they didn't even think of in this linked question and still didn't manage to get it done properly using only Excel, it might be possible depending on your printer drivers but I'm not going that way...
Exporting to clean SVG without the white bars:
The easiest workaround is to just use Word to properly export the chart as .pdf:
Sub ExportChartToSVG()
Dim MyWorksheet As Worksheet
Set MyWorksheet = Tabelle1
Dim MyChart As ChartObject
Set MyChart = MyWorksheet.ChartObjects(1)
Dim fileName As String
fileName = "TestExport"
Dim pathStr As String
pathStr = ThisWorkbook.Path
'Creating a new Word Document
'this is necessary because Excel doesn't support custom pagesizes
'when exporting as pdf and therefore unavoidably creates white borders around the
'chart when exporting
Dim wdApp As Object
Set wdApp = CreateObject("Word.Application")
wdApp.Visible = False
Dim wdDoc As Object
Set wdDoc = wdApp.Documents.Add
MyChart.Copy
wdDoc.Range.Paste
Dim shp As Object
Set shp = wdDoc.Shapes(1)
With wdDoc.PageSetup
.LeftMargin = 0
.RightMargin = 0
.TopMargin = 0
.BottomMargin = 0
.PageWidth = shp.Width
.PageHeight = shp.Height
End With
shp.Top = 0
shp.Left = 0
wdDoc.saveas2 fileName:=pathStr & "\" & fileName, FileFormat:=17 '(wdExportFormatPDF)
wdApp.Quit 0 '(wdDoNotSaveChanges)
Set wdApp = Nothing
Set wdDoc = Nothing
Set shp = Nothing
' Convert .pdf file to .svg
Dim ret As Double
ret = Shell("cmd.exe /k cd /d """ & pathStr & """ & " & "pdftocairo -svg -f 1 -l 1 " & fileName & ".pdf", vbHide)
End Sub
The resulting .pdf and .svg look exactly the same as the manually exported .svg, with only the .pdf having selectable text. The .pdf file remains in the folder. If necessary, it can easily be deleted later via VBA code...
If this method is used to export a larger number of charts, I strongly recommend moving it into a class and having the class hold an instance of the Word application, so it doesn't constantly reopen and close Word. It has the added benefit of making the actual code to export very terse and clean.
Class-based method for exporting to clean SVG:
The code for exporting becomes very simple:
Sub ExportChartToSVG()
Dim MyWorksheet As Worksheet
Set MyWorksheet = Tabelle1
Dim MyChart As ChartObject
Set MyChart = MyWorksheet.ChartObjects(1)
Dim fileName As String
fileName = "TestExport"
Dim filePath As String
filePath = ThisWorkbook.Path & Application.PathSeparator
Dim oShapeExporter As cShapeExporter
Set oShapeExporter = New cShapeExporter
' Export as many shapes as you want here, before destroying oShapeExporter
' cShapeExporter can export objets of types Shape, ChartObject or ChartArea
oShapeExporter.ExportShapeAsPDF MyChart, filePath, fileName
Set oShapeExporter = Nothing
End Sub
Code for class module called cShapeExporter:
Option Explicit
Dim wdApp As Object
Dim wdDoc As Object
Private Sub Class_Initialize()
Set wdApp = CreateObject("Word.Application")
wdApp.Visible = False
Set wdDoc = wdApp.Documents.Add
' Setting margins to 0 so we have no white borders!
' If you want, you can set custom white borders for the exported PDF here
With wdDoc.PageSetup
.LeftMargin = 0
.RightMargin = 0
.TopMargin = 0
.BottomMargin = 0
End With
End Sub
Private Sub Class_Terminate()
' Important: Close Word instance as the object is destroyed.
wdApp.Quit 0 '(0 = wdDoNotSaveChanges)
Set wdApp = Nothing
Set wdDoc = Nothing
End Sub
Public Sub ExportShapeAsPDF(xlShp As Object, _
filePath As String, _
Optional ByVal fileName As String = "")
' Defining which objects can be exported, maybe others are also supported,
' they just need to support all the methods and have all the properties used
' in this sub
If TypeName(xlShp) = "ChartObject" Or _
TypeName(xlShp) = "Shape" Or _
TypeName(xlShp) = "ChartArea" Then
'fine
Else
MsgBox "Exporting Objects of type " & TypeName(xlShp) & _
" not supported, sorry."
Exit Sub
End If
xlShp.Copy
wdDoc.Range.Paste
Dim wdShp As Object
Set wdShp = wdDoc.Shapes(1)
With wdDoc.PageSetup
.PageWidth = wdShp.Width
.PageHeight = wdShp.Height
End With
wdShp.Top = 0
wdShp.Left = 0
' Export as .pdf
wdDoc.saveas2 fileName:=filePath & fileName, _
FileFormat:=17 '(17 = wdExportFormatPDF)
wdShp.Delete
End Sub
Installing the Poppler utility:
I'm assuming you are using Windows here, on Linux getting Poppler is trivial anyway...
So on Windows, I'd suggest installing it using the chocolatey packet manager for Windows. To install chocolatey, you can follow these instructions (takes <5 min).
When you have chocolatey, you can install Poppler with the simple command
choco install poppler
and you are ready to run the code I proposed for converting .pdf to .svg.
If you prefer installing Poppler in a different way, there are various options described here, but I'd like to add some notes about some of the methods:
Downloading the binaries didn't work for me, running the utility would always result in errors.
Installing via Anaconda (conda install -c conda-forge poppler) somehow didn't work for me either. The installation just failed.
Installing via the Windows Subsystem for Linux did work, and the utility worked too, but if you don't already have wsl including a distribution installed you will have to download and install several hundred MB ob data which might be overkill.
If you have MiKTeX installed, the utility is supposed to be included (and was in my case). I tried the utility from my MiKTeX install, and somehow it didn't work.
Exporting to .svg without any external applications using only Excel and VBA
I had to create a new answer because there wasn't enough space in my other one. Personally, I would prefer to use this solution, as there are no external dependencies.
I can now confidently answer this question: Is there a way to export a Chart in SVG format using VBA?
Yes.
It's a hacky mess but it works for now... at least on my machine.
And I tried to create a simple interface for the code so you don't really have to understand it to use it. Still, first I will explain how it works, what problems exist that had to be overcome, and how I managed to solve them. Then, I give a short and simple usage example and instructions. So if you are not interested in the technicalities, you can skip to the easy part.
What's the idea?
The code basically tries to just use the manual export method. There are several problems with this, the first being yet another bug in the Chart.Export method. Chart.Export Interactive:=True is supposed to open the desired dialog box, but this just doesn't work. By leveraging rarely used and even undocumented shortcuts (Probably not, but I had to find one of them with the brute force method), the export window can be opened very reliably using SendKeys "+{F10}" followed by SendKeys "g". The first hurdle is taken, but the trouble has only just begun!
It turns out, that opening a modal Dialog stops all code execution in the entire Application. Even if we call code in another application instance before we open the dialog, how can we keep it running there and return at the same time to finish opening the dialog? It sounds impossible because VBA is strictly single-threaded...
Well, it turns out, the single threading is not quite so strict :) The solution is called Application.OnTime, which starts a procedure at a predetermined time in the future. That procedure has to run in a different instance of Excel.Application because Application.OnTime will only start a procedure if the application is in certain modes (Ready, Copy, Cut, or Find), and having VBA code running or having a modal dialog open are certainly not among those. Therefore, before the dialog is opened, we have to create a background instance of the Excel app, insert VBA code into it and call that code, which will then schedule other code to start running in the background instance once the dialog is open. Note: Because we want to insert the code automatically into the background instance, we need to enable Trust access to the VBA project object model.
The next question is: How can we work with the Windows dialog box using only VBA code? I tried very hard to avoid more SendKeys but unfortunately, some problems were just out of my league. I managed to get all the window and control handles of the dialog via EnumChildWindows and used the information to insert text into the "FileName" ComboBox. Since you can also insert the path there, the only problems left were selecting ".svg" in the FileFormat ComboBox and clicking the "Save" Button.
Changing the selection in the Combobox is relatively easy using Windows API functions but the problem is to actually get it to register the change. It appeared to have changed in the dialog but when I clicked "Save" it still saved as .png. I spent hours in Spy++ monitoring the messages that are sent during a manual change but I wasn't able to reproduce them with VBA. The language is truly horrible for low-level tasks, trying to align bits with VBA is a pain. Anyways, because of this, it had to be SendKeys again for changing the file format and pressing 'Save'.
I tried to be very careful with the SendKeys usage, implementing various safety checks, and pulling the target window to the front before every usage, but you can never be 100% safe with it.
Because the method requires a background instance of an app once again, I implemented a class for a ShapeExporter object again. Creating the object opens the background app, destroying the object closes it.
Simple usage guide
The following procedure will export all ChartObjects in the specified worksheet to the folder the workbook is saved in.
Sub ExportEmbeddedChartToSVG()
Dim MyWorksheet As Worksheet
Set MyWorksheet = Application.Worksheets("MyWorksheet")
'Creating the ShapeExporter object
Dim oShapeExporter As cShapeExporter
Set oShapeExporter = New cShapeExporter
'Export as many shapes as you want here, before destroying oShapeExporter
Dim oChart As ChartObject
For Each oChart In MyWorksheet.ChartObjects
'the .ExportShapeAsSVG method of the object takes three arguments:
'1. The Chart or Shape to be exported
'2. The target filename
'3. The target path
oShapeExporter.ExportShapeAsSVG oChart, oChart.Name, ThisWorkbook.Path
Next oChart
'When the object goes out of scope, its terminate procedure is automatically called
'and the background app is closed
Set oShapeExporter = Nothing
End Sub
For the code to work, you must first:
Trust access to the VBA project object model (for reason see detailed description of the macro)
Create a class module, rename it to "cShapeExporter", and paste the following code into it:
'Class for automatic exporting in SVG-Format
'Initial author: Guido Witt-Dörring, 09.12.2020
'https://stackoverflow.com/a/65212838/12287457
'Note:
'When objects created from this class are not properly destroyed, an invisible
'background instance of Excel will keep running on your computer. In this
'case, you can just close it via the Task Manager.
'For example, this will happen when your code hits an 'End' statement, which
'immediately stops all code execution, or when an unhandled error forces
'you to stop code execution manually while an instance of this class exists.
Option Explicit
#If VBA7 Then
Private Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
Private Declare PtrSafe Function SetForegroundWindow Lib "user32" (ByVal hWnd As LongPtr) As Boolean
Private Declare PtrSafe Function IsWindowVisible Lib "user32" (ByVal hWnd As LongPtr) As Boolean
Private Declare PtrSafe Function IsIconic Lib "user32" (ByVal hWnd As LongPtr) As Boolean
Private Declare PtrSafe Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As LongPtr
#Else
Private Declare Sub Sleep Lib "kernel32" (ByVal lngMilliSeconds As Long)
Private Declare Function SetForegroundWindow Lib "user32" (ByVal hwnd As Long) As Boolean
Private Declare Function IsWindowVisible Lib "User32" (ByVal hWnd As Long) As Boolean
Private Declare Function IsIconic Lib "User32" Alias "IsIconic" (ByVal hWnd As long) As boolean
Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As Long
#End If
Private NewXlAppInstance As Excel.Application
Private xlWbInOtherInstance As Workbook
Private Sub Class_Initialize()
Set NewXlAppInstance = New Excel.Application
Set xlWbInOtherInstance = NewXlAppInstance.Workbooks.Add
NewXlAppInstance.Visible = False
On Error Resume Next
xlWbInOtherInstance.VBProject.References.AddFromFile "scrrun.dll"
xlWbInOtherInstance.VBProject.References.AddFromFile "FM20.dll"
On Error GoTo 0
Dim VbaModuleForOtherInstance As VBComponent
Set VbaModuleForOtherInstance = xlWbInOtherInstance.VBProject.VBComponents.Add(vbext_ct_StdModule)
VbaModuleForOtherInstance.CodeModule.AddFromString CreateCodeForOtherXlInstance
End Sub
Private Sub Class_Terminate()
NewXlAppInstance.DisplayAlerts = False
NewXlAppInstance.Quit
Set xlWbInOtherInstance = Nothing
Set NewXlAppInstance = Nothing
End Sub
Public Sub ExportShapeAsSVG(xlShp As Object, FileName As String, FilePath As String)
'Check if path exists:
If Not ExistsPath(FilePath) Then
If vbYes = MsgBox("Warning, you are trying to export a file to a path that doesn't exist! Continue exporting to default path? " & vbNewLine & "Klick no to resume macro without exporting or cancel to debug.", vbYesNoCancel, "Warning") Then
FilePath = ""
ElseIf vbNo Then
Exit Sub
ElseIf vbCancel Then
Error 76
End If
End If
If TypeName(xlShp) = "ChartObject" Or TypeName(xlShp) = "Shape" Or TypeName(xlShp) = "Chart" Or TypeName(xlShp) = "ChartArea" Then
'fine
Else
MsgBox "Exporting Objects of type " & TypeName(xlShp) & " not supported, sorry."
Exit Sub
End If
If TypeName(xlShp) = "ChartArea" Then Set xlShp = xlShp.Parent
retry:
SetForegroundWindow FindWindow("XLMAIN", ThisWorkbook.Name & " - Excel")
If Not Application.Visible Then 'Interestingly, API function "IsWindowVisible(Application.hWnd)" doesn't work here! (maybe because of multi monitor setup?)
MsgBox "The workbook must be visible for the svg-export to proceed! It must be at least in window mode!"
Application.WindowState = xlNormal
Application.Visible = True
Sleep 100
GoTo retry
End If
If IsIconic(Application.hWnd) Then 'Interestingly "Application.WindowState = xlMinimized" doesn't work here!"
MsgBox "The workbook can't be minimized for the svg-export to proceed! It must be at least in window mode!"
Application.WindowState = xlNormal
Sleep 100
GoTo retry
End If
'check if background instance still exists and start support proc
On Error GoTo errHand
NewXlAppInstance.Run "ScheduleSvgExportHelperProcess", Application.hWnd, ThisWorkbook.Name, FileName, FilePath
On Error GoTo 0
Sleep 100
xlShp.Activate
SetForegroundWindow FindWindow("XLMAIN", ThisWorkbook.Name & " - Excel")
SendKeys "+{F10}"
DoEvents
SendKeys "g"
DoEvents
Exit Sub
errHand:
MsgBox "Error in ShapeExporter Object. No more shapes can be exported."
Err.Raise Err.Number
End Sub
Public Function ExistsPath(ByVal FilePath As String) As Boolean
Dim oFso As Object
Dim oFolder As Object
Set oFso = CreateObject("Scripting.FileSystemObject")
'Setting the Folder of the Filepath
On Error GoTo PathNotFound
Set oFolder = oFso.GetFolder(Left(Replace(FilePath & "\", "\\", "\"), Len(Replace(FilePath & "\", "\\", "\")) - 1))
On Error GoTo 0
ExistsPath = True
Exit Function
PathNotFound:
ExistsPath = False
End Function
Private Function CreateCodeForOtherXlInstance() As String
Dim s As String
s = s & "Option Explicit" & vbCrLf
s = s & "" & vbCrLf
s = s & "#If VBA7 Then" & vbCrLf
s = s & " Public Declare PtrSafe Sub Sleep Lib ""kernel32"" (ByVal dwMilliseconds As Long)" & vbCrLf
s = s & " Private Declare PtrSafe Function GetForegroundWindow Lib ""user32"" () As LongPtr" & vbCrLf
s = s & " Private Declare PtrSafe Function GetWindowText Lib ""user32"" Alias ""GetWindowTextA"" (ByVal hWnd As LongPtr, ByVal lpString As String, ByVal cch As Long) As Long" & vbCrLf
s = s & " Private Declare PtrSafe Function FindWindow Lib ""user32"" Alias ""FindWindowA"" (ByVal lpClassName As String, ByVal lpWindowName As String) As LongPtr" & vbCrLf
s = s & " Private Declare PtrSafe Function SetForegroundWindow Lib ""user32"" (ByVal hWnd As LongPtr) As Boolean" & vbCrLf
s = s & " Private Declare PtrSafe Function SendMessage Lib ""user32"" Alias ""SendMessageA"" (ByVal hWnd As LongPtr, ByVal wMsg As Long, ByVal wParam As LongPtr, lParam As Any) As LongPtr" & vbCrLf
s = s & " Private Declare PtrSafe Function GetClassName Lib ""user32"" Alias ""GetClassNameA"" (ByVal hWnd As LongPtr, ByVal lpStr As String, ByVal nMaxCount As Long) As Long" & vbCrLf
s = s & " Private Declare PtrSafe Function EnumChildWindows Lib ""user32"" (ByVal hWndParent As LongPtr, ByVal lpEnumFunc As LongPtr, ByVal lParam As LongPtr) As Boolean" & vbCrLf
s = s & " Private Declare PtrSafe Function GetWindowTextLength Lib ""user32"" Alias ""GetWindowTextLengthA"" (ByVal hWnd As LongPtr) As Long" & vbCrLf
s = s & " Private Declare PtrSafe Function GetWindowLongPtr Lib ""user32"" Alias ""GetWindowLongPtrA"" (ByVal hWnd As LongPtr, ByVal nindex As Long) As LongPtr" & vbCrLf
s = s & "#Else" & vbCrLf
s = s & " Public Declare Sub Sleep Lib ""kernel32"" (ByVal lngMilliSeconds As Long)" & vbCrLf
s = s & " Private Declare Function GetForegroundWindow Lib ""user32"" () As Long" & vbCrLf
s = s & " Private Declare Function GetWindowText Lib ""user32"" Alias ""GetWindowTextA"" (ByVal hwnd As Long, ByVal lpString As String, ByVal cch As Long) As Long" & vbCrLf
s = s & " Private Declare Function FindWindow Lib ""user32"" Alias ""FindWindowA"" (ByVal lpClassName As String, ByVal lpWindowName As String) As Long" & vbCrLf
s = s & " Private Declare Function SetForegroundWindow Lib ""user32"" (ByVal hwnd As Long) As Boolean" & vbCrLf
s = s & " Private Declare Function SendMessage Lib ""user32"" Alias ""SendMessageA"" (ByVal hwnd As Long, ByVal wMsg As Long, ByVal wParam As Long, lParam As Any) As Long" & vbCrLf
s = s & " Private Declare Function GetClassName Lib ""user32"" Alias ""GetClassNameA"" (ByVal hwnd As Long, ByVal lpStr As String, ByVal nMaxCount As Long) As Long" & vbCrLf
s = s & " Private Declare Function EnumChildWindows Lib ""User32"" (ByVal hwndParent As Long, ByVal lpEnumFunc As Long, ByVal lParam As Long) As boolean" & vbCrLf
s = s & " Private Declare Function GetWindowTextLength Lib ""User32"" Alias ""GetWindowTextLengthA"" (ByVal hwnd As Long) As Long" & vbCrLf
s = s & " Private Declare Function GetWindowLongPtr Lib ""User32"" Alias ""GetWindowLongPtrA"" (ByVal hwnd As Long, ByVal nindex As Long) As Long" & vbCrLf
s = s & "#End If" & vbCrLf
s = s & "" & vbCrLf
s = s & "Private Const GWL_ID = -12" & vbCrLf
s = s & "" & vbCrLf
s = s & "Private Const WM_SETTEXT = &HC" & vbCrLf
s = s & "" & vbCrLf
s = s & "'Const for this Application:" & vbCrLf
s = s & "Private Const dc_Hwnd = 1" & vbCrLf
s = s & "Private Const dc_ClassName = 2" & vbCrLf
s = s & "Private Const dc_CtlID = 3" & vbCrLf
s = s & "Private Const dc_CtlText = 4" & vbCrLf
s = s & "" & vbCrLf
s = s & "Private Const Window_Search_Timeout As Single = 5#" & vbCrLf
s = s & "Public ChildWindowsPropDict As Object" & vbCrLf
s = s & "" & vbCrLf
s = s & "#If VBA7 Then" & vbCrLf
s = s & " Private Function GetCtlText(ByVal hctl As LongPtr) As String" & vbCrLf
s = s & "#Else" & vbCrLf
s = s & " Private Function GetCtlText(ByVal hctl As Long) As String" & vbCrLf
s = s & "#End If" & vbCrLf
s = s & " Dim ControlText As String" & vbCrLf
s = s & " On Error GoTo WindowTextTooLarge" & vbCrLf
s = s & " ControlText = Space(GetWindowTextLength(hctl) + 1)" & vbCrLf
s = s & " GetWindowText hctl, ControlText, Len(ControlText)" & vbCrLf
s = s & " GetCtlText = ControlText 'Controls Text" & vbCrLf
s = s & " Exit Function" & vbCrLf
s = s & " " & vbCrLf
s = s & "WindowTextTooLarge:" & vbCrLf
s = s & " ControlText = Space(256)" & vbCrLf
s = s & " On Error GoTo -1" & vbCrLf
s = s & " GetWindowText hctl, ControlText, Len(ControlText)" & vbCrLf
s = s & " GetCtlText = ControlText 'Controls Text" & vbCrLf
s = s & "End Function" & vbCrLf
s = s & "" & vbCrLf
s = s & "#If VBA7 Then" & vbCrLf
s = s & " Private Function EnumChildProc(ByVal hWnd As LongPtr, ByVal lParam As LongPtr) As Long" & vbCrLf
s = s & "#Else" & vbCrLf
s = s & " Private Function EnumChildProc(ByVal hWnd As Long, ByVal lParam As Long) As Long" & vbCrLf
s = s & "#End If" & vbCrLf
s = s & " Dim ClassName As String" & vbCrLf
s = s & " Dim subCtlProp(1 To 4) As Variant" & vbCrLf
s = s & " " & vbCrLf
s = s & " subCtlProp(dc_Hwnd) = hWnd 'Controls Handle" & vbCrLf
s = s & " " & vbCrLf
s = s & " ClassName = Space(256)" & vbCrLf
s = s & " GetClassName hWnd, ClassName, Len(ClassName)" & vbCrLf
s = s & " subCtlProp(dc_ClassName) = Trim(CStr(ClassName)) 'Controls ClassName" & vbCrLf
s = s & " " & vbCrLf
s = s & " subCtlProp(dc_CtlID) = GetWindowLongPtr(hWnd, GWL_ID) 'Controls ID" & vbCrLf
s = s & " " & vbCrLf
s = s & " subCtlProp(dc_CtlText) = GetCtlText(hWnd) 'Controls Text 'Doesn't always work for some reason..." & vbCrLf
s = s & " '(sometimes returns """" when Spy++ finds a string)" & vbCrLf
s = s & " ChildWindowsPropDict.Add key:=CStr(hWnd), Item:=subCtlProp" & vbCrLf
s = s & " " & vbCrLf
s = s & " 'continue to enumerate (0 would stop it)" & vbCrLf
s = s & " EnumChildProc = 1" & vbCrLf
s = s & "End Function" & vbCrLf
s = s & "" & vbCrLf
s = s & "#If VBA7 Then" & vbCrLf
s = s & " Private Sub WriteChildWindowsPropDict(hWnd As LongPtr)" & vbCrLf
s = s & "#Else" & vbCrLf
s = s & " Private Sub WriteChildWindowsPropDict(hWnd As Long)" & vbCrLf
s = s & "#End If" & vbCrLf
s = s & " On Error Resume Next" & vbCrLf
s = s & " Set ChildWindowsPropDict = Nothing" & vbCrLf
s = s & " On Error GoTo 0" & vbCrLf
s = s & " Set ChildWindowsPropDict = CreateObject(""Scripting.Dictionary"")" & vbCrLf
s = s & " EnumChildWindows hWnd, AddressOf EnumChildProc, ByVal 0&" & vbCrLf
s = s & "End Sub" & vbCrLf
s = s & "" & vbCrLf
s = s & "Private Function ExistsFileInPath(ByVal FileName As String, ByVal FilePath As String, Optional warn As Boolean = False) As Boolean" & vbCrLf
s = s & " Dim oFso As Object" & vbCrLf
s = s & " Dim oFile As Object" & vbCrLf
s = s & " Dim oFolder As Object" & vbCrLf
s = s & " " & vbCrLf
s = s & " Set oFso = CreateObject(""Scripting.FileSystemObject"")" & vbCrLf
s = s & " 'Setting the Folder of the Filepath" & vbCrLf
s = s & " On Error GoTo PathNotFound" & vbCrLf
s = s & " Set oFolder = oFso.GetFolder(Left(Replace(FilePath & ""\"", ""\\"", ""\""), Len(Replace(FilePath & ""\"", ""\\"", ""\"")) - 1))" & vbCrLf
s = s & " On Error GoTo 0" & vbCrLf
s = s & " " & vbCrLf
s = s & " 'Writing all Filenames of the Files in the Folder to flStr" & vbCrLf
s = s & " For Each oFile In oFolder.Files" & vbCrLf
s = s & " If oFile.Name = FileName Then" & vbCrLf
s = s & " ExistsFileInPath = True" & vbCrLf
s = s & " Exit Function" & vbCrLf
s = s & " End If" & vbCrLf
s = s & " Next oFile" & vbCrLf
s = s & " " & vbCrLf
s = s & " ExistsFileInPath = False" & vbCrLf
s = s & " Exit Function" & vbCrLf
s = s & " " & vbCrLf
s = s & "PathNotFound:" & vbCrLf
s = s & " If warn Then MsgBox ""The path "" & Chr(10) & FilePath & Chr(10) & "" was not found by the function ExistsFileInPath."" & Chr(10) & ""Returning FALSE""" & vbCrLf
s = s & " ExistsFileInPath = False" & vbCrLf
s = s & "End Function" & vbCrLf
s = s & "" & vbCrLf
s = s & "#If VBA7 Then" & vbCrLf
s = s & " Public Sub ScheduleSvgExportHelperProcess(ByVal Wb1hwnd As LongPtr, ByVal Wb1Name As String, ByVal SvgFileName As String, ByVal SvgFilePath As String)" & vbCrLf
s = s & "#Else" & vbCrLf
s = s & " Public Sub ScheduleSvgExportHelperProcess(ByVal Wb1hwnd As Long, ByVal Wb1Name As String, ByVal SvgFileName As String, ByVal SvgFilePath As String)" & vbCrLf
s = s & "#End If" & vbCrLf
s = s & " If Not Wb1hwnd = FindWindow(""XLMAIN"", Wb1Name & "" - Excel"") Then" & vbCrLf
s = s & " MsgBox ""Error finding Wb1hwnd - something unforseen happened!""" & vbCrLf
s = s & " GoTo badExit" & vbCrLf
s = s & " End If" & vbCrLf
s = s & " " & vbCrLf
s = s & " Application.OnTime Now + TimeValue(""00:00:02""), ""'SvgExportHelperProcess """""" & CStr(Wb1hwnd) & """""", """""" & Wb1Name & """""", """""" & SvgFileName _" & vbCrLf
s = s & " & """""", """""" & SvgFilePath & """"""'"", Now + TimeValue(""00:00:015"")" & vbCrLf
s = s & " Exit Sub" & vbCrLf
s = s & "badExit:" & vbCrLf
s = s & " MsgBox ""Shutting down background instance of excel.""" & vbCrLf
s = s & " Application.DisplayAlerts = False" & vbCrLf
s = s & " Application.Quit" & vbCrLf
s = s & "End Sub" & vbCrLf
s = s & "" & vbCrLf
s = s & "Public Sub SvgExportHelperProcess(ByVal Wb1hwndStr As String, ByVal Wb1Name As String, ByVal SvgFileName As String, ByVal SvgFilePath As String)" & vbCrLf
s = s & " #If VBA7 And Win64 Then" & vbCrLf
s = s & " Dim Wb1hwnd As LongPtr" & vbCrLf
s = s & " Wb1hwnd = CLngPtr(Wb1hwndStr)" & vbCrLf
s = s & " Dim dlgHwnd As LongPtr" & vbCrLf
s = s & " Dim tempHctrl As LongPtr" & vbCrLf
s = s & " #Else" & vbCrLf
s = s & " Dim Wb1hwnd As LongPtr" & vbCrLf
s = s & " Wb1hwnd = CLng(Wb1hwndStr)" & vbCrLf
s = s & " Dim dlgHwnd As Long" & vbCrLf
s = s & " Dim tempHctrl As Long" & vbCrLf
s = s & " #End If" & vbCrLf
s = s & " Dim i As Long" & vbCrLf
s = s & " Dim stopTime As Single" & vbCrLf
s = s & " " & vbCrLf
s = s & " 'Find dialog window handle" & vbCrLf
s = s & " stopTime = Timer() + Window_Search_Timeout" & vbCrLf
s = s & " Do" & vbCrLf
s = s & " dlgHwnd = 0" & vbCrLf
s = s & " Sleep 15" & vbCrLf
s = s & " DoEvents" & vbCrLf
s = s & " SetForegroundWindow Wb1hwnd 'FindWindow(""XLMAIN"", Wb1Name & "" - Excel"")" & vbCrLf
s = s & " Sleep 150" & vbCrLf
s = s & " dlgHwnd = FindWindow(""#32770"", vbNullString)" & vbCrLf
s = s & " Loop Until Timer() > stopTime Or dlgHwnd <> 0" & vbCrLf
s = s & " " & vbCrLf
s = s & " If dlgHwnd = 0 Then" & vbCrLf
s = s & " MsgBox ""Couldn't find dialog window handle!""" & vbCrLf
s = s & " GoTo errHand" & vbCrLf
s = s & " End If" & vbCrLf
s = s & " " & vbCrLf
s = s & " 'Enumerate the child windows of the dialog and write their properties to a dictionary" & vbCrLf
s = s & " WriteChildWindowsPropDict dlgHwnd" & vbCrLf
s = s & "" & vbCrLf
s = s & " 'the first window of class ""Edit"" inside ChildWindowsPropDict will be the filename box" & vbCrLf
s = s & " Dim v As Variant" & vbCrLf
s = s & " For Each v In ChildWindowsPropDict.items" & vbCrLf
s = s & " If Left(CStr(v(dc_ClassName)), Len(CStr(v(dc_ClassName))) - 1) = ""Edit"" Then" & vbCrLf
s = s & " tempHctrl = v(dc_Hwnd)" & vbCrLf
s = s & " 'send message" & vbCrLf
s = s & " SendMessage tempHctrl, WM_SETTEXT, 0&, ByVal SvgFilePath & ""\"" & SvgFileName" & vbCrLf
s = s & " 'we don't need this hwnd anymore" & vbCrLf
s = s & " ChildWindowsPropDict.Remove CStr(v(dc_Hwnd))" & vbCrLf
s = s & " Exit For" & vbCrLf
s = s & " End If" & vbCrLf
s = s & " Next v" & vbCrLf
s = s & "" & vbCrLf
s = s & "retry:" & vbCrLf
s = s & " SetForegroundWindow dlgHwnd" & vbCrLf
s = s & " " & vbCrLf
s = s & " SendKeys ""{TAB}""" & vbCrLf
s = s & " Sleep 250" & vbCrLf
s = s & " SetForegroundWindow dlgHwnd" & vbCrLf
s = s & " For i = 1 To 10" & vbCrLf
s = s & " SendKeys ""{DOWN}""" & vbCrLf
s = s & " Sleep 100" & vbCrLf
s = s & " SetForegroundWindow dlgHwnd" & vbCrLf
s = s & " Next i" & vbCrLf
s = s & " " & vbCrLf
s = s & " SendKeys ""~""" & vbCrLf
s = s & " Sleep 100" & vbCrLf
s = s & " SetForegroundWindow dlgHwnd" & vbCrLf
s = s & " SendKeys ""~""" & vbCrLf
s = s & " Sleep 50" & vbCrLf
s = s & " " & vbCrLf
s = s & " 'give the keystrokes time to process" & vbCrLf
s = s & " Sleep 300" & vbCrLf
s = s & "" & vbCrLf
s = s & " 'Wait until the file appears in the specified path:" & vbCrLf
s = s & " Dim cleanFileName As String" & vbCrLf
s = s & " If InStr(1, Right(SvgFileName, 4), "".svg"", vbTextCompare) = 0 Then" & vbCrLf
s = s & " cleanFileName = SvgFileName & "".svg""" & vbCrLf
s = s & " Else" & vbCrLf
s = s & " cleanFileName = SvgFileName" & vbCrLf
s = s & " End If" & vbCrLf
s = s & " " & vbCrLf
s = s & " Dim retryTime As Single" & vbCrLf
s = s & " retryTime = Timer + 5" & vbCrLf
s = s & " stopTime = Timer + 60 '1 minute timeout." & vbCrLf
s = s & " 'relatively long in case a file already exists dialog appears..." & vbCrLf
s = s & " Do Until ExistsFileInPath(SvgFileName, SvgFilePath, False)" & vbCrLf
s = s & " Sleep 700" & vbCrLf
s = s & " DoEvents" & vbCrLf
s = s & " If Timer > retryTime Then" & vbCrLf
s = s & " 'check if graphic export dialog is top window" & vbCrLf
s = s & " If dlgHwnd = GetForegroundWindow Then GoTo retry" & vbCrLf
s = s & " End If" & vbCrLf
s = s & " If Timer > stopTime Then GoTo timeoutHand" & vbCrLf
s = s & " Loop" & vbCrLf
s = s & " " & vbCrLf
s = s & " Exit Sub" & vbCrLf
s = s & "errHand:" & vbCrLf
s = s & " MsgBox ""Error in the helper process""" & vbCrLf
s = s & " GoTo badExit" & vbCrLf
s = s & " " & vbCrLf
s = s & "timeoutHand:" & vbCrLf
s = s & " MsgBox ""Timeout. It seems like something went wrong creating the file. File "" & cleanFileName & "" didn't appear in folder "" & SvgFilePath & "".""" & vbCrLf
s = s & " GoTo badExit" & vbCrLf
s = s & " " & vbCrLf
s = s & "badExit:" & vbCrLf
s = s & " MsgBox ""Shutting down background instance of excel.""" & vbCrLf
s = s & " Application.DisplayAlerts = False" & vbCrLf
s = s & " Application.Quit" & vbCrLf
s = s & "End Sub" & vbCrLf
s = s & "" & vbCrLf
CreateCodeForOtherXlInstance = s
End Function
When you copy a chart to the clipboard, Excel adds lots of different clipboard formats. Since version 2011 (Application.Build >= 13426), this now includes "image/svg+xml".
So all we have to do is find that format on the clipboard and save it to a file. Which turns out to be fairly annoying.
Private Declare PtrSafe Function OpenClipboard Lib "user32" (ByVal hwnd As LongPtr) As Long
Private Declare PtrSafe Function GetClipboardData Lib "user32" (ByVal wFormat As Long) As LongPtr
Private Declare PtrSafe Function EnumClipboardFormats Lib "user32" (ByVal wFormat As Long) As Long
Private Declare PtrSafe Function GetClipboardFormatName Lib "user32" _
Alias "GetClipboardFormatNameW" _
(ByVal wFormat As Long, _
ByVal lpString As LongPtr, _
ByVal nMaxCount As Integer) As Integer
Private Declare PtrSafe Function CloseClipboard Lib "user32" () As Long
Private Declare PtrSafe Function GlobalUnlock Lib "Kernel32" (ByVal hMem As LongPtr) As LongPtr
Private Declare PtrSafe Function GlobalLock Lib "Kernel32" (ByVal hMem As LongPtr) As LongPtr
Private Declare PtrSafe Function GlobalSize Lib "Kernel32" (ByVal hMem As LongPtr) As LongPtr
Private Declare PtrSafe Function CreateFile Lib "Kernel32" _
Alias "CreateFileA" (ByVal lpFileName As String, _
ByVal dwDesiredAccess As Long, _
ByVal dwShareMode As Long, _
ByVal lpSecurityAttributes As LongPtr, _
ByVal dwCreationDisposition As Long, _
ByVal dwFlagsAndAttributes As Long, _
ByVal hTemplateFile As LongPtr) As LongPtr
Private Declare PtrSafe Function WriteFile Lib "Kernel32" _
(ByVal hFile As LongPtr, _
ByVal lpBuffer As LongPtr, _
ByVal nNumberOfBytesToWrite As Long, _
ByRef lpNumberOfBytesWritten As Long, _
ByVal lpOverlapped As LongPtr) As Long
Private Declare PtrSafe Function CloseHandle Lib "Kernel32" (ByVal hObject As LongPtr) As Long
Sub SaveClipboard(formatName As String, filename As String)
Dim fmtName As String
Dim fmt As Long
Dim length As Long
Dim wrote As Long
Dim data As LongPtr
Dim fileHandle As LongPtr
Dim content As LongPtr
Dim ret As Long
If OpenClipboard(ActiveWindow.hwnd) = 0 Then
Exit Sub
End If
fmt = 0
Do
fmt = EnumClipboardFormats(fmt)
If fmt = 0 Then Exit Do
fmtName = String$(255, vbNullChar)
length = GetClipboardFormatName(fmt, StrPtr(fmtName), 255)
If length <> 0 And Left(fmtName, length) = formatName Then
data = GetClipboardData(fmt)
length = CLng(GlobalSize(data))
content = GlobalLock(data)
' use win32 api file handling to avoid copying buffers
fileHandle = CreateFile(filename, &H120089 Or &H120116, 0, 0, 2, 0, 0)
ret = WriteFile(fileHandle, content, length, wrote, 0)
CloseHandle fileHandle
GlobalUnlock data
Exit Do
End If
Loop
CloseClipboard
If fmt = 0 Then
MsgBox "Did not find clipboard format " & formatName
Exit Sub
End If
End Sub
Then just copy the chart and save the svg;
shape.Copy
SaveClipboard "image/svg+xml", "C:\temp\output.svg"
If you don't need .svg in particular then .emf is another vector format. It does not work directly from Excel but it does work using a 'helper' PowerPoint app:
Sub ExportChartToEMF(ByVal ch As Chart, ByVal filePath As String)
Const methodName As String = "ExportChartToEMF"
Const ppShapeFormatEMF As Long = 5
'
If ch Is Nothing Then Err.Raise 91, methodName, "Chart not set"
'
Dim pp As Object
Dim slide As Object
Dim errNumber As Long
'
Set pp = CreateObject("PowerPoint.Application")
With pp.Presentations.Add(msoFalse) 'False so it's not Visible
Set slide = .Slides.AddSlide(.Slides.Count + 1, .Designs(1).SlideMaster.CustomLayouts(1))
End With
'
ch.Parent.Copy
On Error Resume Next
slide.Shapes.Paste.Export filePath, ppShapeFormatEMF
errNumber = Err.Number
On Error GoTo 0
'
pp.Quit
If Err.Number <> 0 Then Err.Raise Err.Number, methodName, "Error while exporting to file"
End Sub
You would use it like:
ExportChartToEMF ActiveChart, "[FolderPath]\[FileName].emf"
If you really need .svg then unfortunately the functionality is not exposed to VBA although it works manually in Excel and PowerPoint via the Save as Picture dialog (right-click on chart shape).
In short, you cannot fully automate the export of chart to .svg file unless you go through an intermediate format (like .emf or .pdf) or manually saving to .svg via the Save as Picture dialog.
I am trying to make a function check if a .pdf file exists. If it exists I want to print the file.
Function PrintPDF(PartNum As String)
Dim DirFile As String
DirFile = "\\SERVER5\hpfiles\Company\Drawings\PDF-SL8\" & PartNum & ".pdf"
If Dir(DirFile) = "" Then
Exit Function
Else
DirFile.PrintOut
End If
End Function
I get a compile error saying Invalid Qualifier. I assume its because DirFile is a string.
How do I use this string as the targeted file for printing?
Correct, you can't print a string. One way is to use a shell command and print the file like this:
Option Explicit
Declare Function apiShellExecute Lib "shell32.dll" Alias "ShellExecuteA" ( _
ByVal hwnd As Long, _
ByVal lpOperation As String, _
ByVal lpFile As String, _
ByVal lpParameters As String, _
ByVal lpDirectory As String, _
ByVal nShowCmd As Long) _
As Long
Public Sub PrintFile(ByVal strPathAndFilename As String)
Call apiShellExecute(Application.hwnd, "print", strPathAndFilename, vbNullString, vbNullString, 0)
End Sub
Function PrintPDF(PartNum As String)
Dim DirFile As String
DirFile = "\\SERVER5\hpfiles\Company\Drawings\PDF-SL8\" & PartNum & ".pdf"
If Dir(DirFile) = "" Then Exit Function
PrintFile (DirFile)
End Function
Adapted from this post
Can an Excel VBA macro, running in one instance of Excel, access the workbooks of another running instance of Excel? For example, I would like to create a list of all workbooks that are open in any running instance of Excel.
Cornelius' answer is partially correct. His code gets the current instance and then makes a new instance. GetObject only ever gets the first instance, no matter how many instances are available. The question I believe is how can you get a specific instance from among many instances.
For a VBA project, make two modules, one code module, and the other as a form with one command button named Command1. You might need to add a reference to Microsoft.Excel.
This code displays all the name of each workbook for each running instance of Excel in the Immediate window.
'------------- Code Module --------------
Option Explicit
Declare Function FindWindowEx Lib "User32" Alias "FindWindowExA" (ByVal hWnd1 As Long, ByVal hWnd2 As Long, ByVal lpsz1 As String, ByVal lpsz2 As String) As Long
Declare Function GetClassName Lib "User32" Alias "GetClassNameA" (ByVal hWnd As Long, ByVal lpClassName As String, ByVal nMaxCount As Long) As Long
Declare Function IIDFromString Lib "ole32" (ByVal lpsz As Long, ByRef lpiid As UUID) As Long
Declare Function AccessibleObjectFromWindow Lib "oleacc" (ByVal hWnd As Long, ByVal dwId As Long, ByRef riid As UUID, ByRef ppvObject As Object) As Long
Type UUID 'GUID
Data1 As Long
Data2 As Integer
Data3 As Integer
Data4(7) As Byte
End Type
'------------- Form Module --------------
Option Explicit
Const IID_IDispatch As String = "{00020400-0000-0000-C000-000000000046}"
Const OBJID_NATIVEOM As Long = &HFFFFFFF0
'Sub GetAllWorkbookWindowNames()
Sub Command1_Click()
On Error GoTo MyErrorHandler
Dim hWndMain As Long
hWndMain = FindWindowEx(0&, 0&, "XLMAIN", vbNullString)
Do While hWndMain <> 0
GetWbkWindows hWndMain
hWndMain = FindWindowEx(0&, hWndMain, "XLMAIN", vbNullString)
Loop
Exit Sub
MyErrorHandler:
MsgBox "GetAllWorkbookWindowNames" & vbCrLf & vbCrLf & "Err = " & Err.Number & vbCrLf & "Description: " & Err.Description
End Sub
Private Sub GetWbkWindows(ByVal hWndMain As Long)
On Error GoTo MyErrorHandler
Dim hWndDesk As Long
hWndDesk = FindWindowEx(hWndMain, 0&, "XLDESK", vbNullString)
If hWndDesk <> 0 Then
Dim hWnd As Long
hWnd = FindWindowEx(hWndDesk, 0, vbNullString, vbNullString)
Dim strText As String
Dim lngRet As Long
Do While hWnd <> 0
strText = String$(100, Chr$(0))
lngRet = GetClassName(hWnd, strText, 100)
If Left$(strText, lngRet) = "EXCEL7" Then
GetExcelObjectFromHwnd hWnd
Exit Sub
End If
hWnd = FindWindowEx(hWndDesk, hWnd, vbNullString, vbNullString)
Loop
On Error Resume Next
End If
Exit Sub
MyErrorHandler:
MsgBox "GetWbkWindows" & vbCrLf & vbCrLf & "Err = " & Err.Number & vbCrLf & "Description: " & Err.Description
End Sub
Public Function GetExcelObjectFromHwnd(ByVal hWnd As Long) As Boolean
On Error GoTo MyErrorHandler
Dim fOk As Boolean
fOk = False
Dim iid As UUID
Call IIDFromString(StrPtr(IID_IDispatch), iid)
Dim obj As Object
If AccessibleObjectFromWindow(hWnd, OBJID_NATIVEOM, iid, obj) = 0 Then 'S_OK
Dim objApp As Excel.Application
Set objApp = obj.Application
Debug.Print objApp.Workbooks(1).Name
Dim myWorksheet As Worksheet
For Each myWorksheet In objApp.Workbooks(1).Worksheets
Debug.Print " " & myWorksheet.Name
DoEvents
Next
fOk = True
End If
GetExcelObjectFromHwnd = fOk
Exit Function
MyErrorHandler:
MsgBox "GetExcelObjectFromHwnd" & vbCrLf & vbCrLf & "Err = " & Err.Number & vbCrLf & "Description: " & Err.Description
End Function
I believe that VBA is more powerful than Charles thinks ;)
If there is only some tricky way to point to the specific instance from GetObject and CreateObject we'll have your problem solved!
EDIT:
If you're the creator of all the instances there should be no problems with things like listing workbooks. Take a look on this code:
Sub Excels()
Dim currentExcel As Excel.Application
Dim newExcel As Excel.Application
Set currentExcel = GetObject(, "excel.application")
Set newExcel = CreateObject("excel.application")
newExcel.Visible = True
newExcel.Workbooks.Add
'and so on...
End Sub
I think that within VBA you can get access to the application object in another running instance. If you know the name of a workbook open within the other instance, then you can get a reference to the application object. See Allen Waytt's page
The last part,
Dim xlApp As Excel.Application
Set xlApp = GetObject("c:\mypath\ExampleBook.xlsx").Application
Allowed me to get a pointer to the application object of the instance that had ExampleBook.xlsx open.
I believe "ExampleBook" needs to be the full path, at least in Excel 2010. I'm currently experimenting with this myself, so I will try and update as I get more details.
Presumably there may be complications if separate instances have the same workbook open, but only one may have write access.
Thanks to this great post I had a routine to find return an array of all Excel applications currently running on the machine. Trouble is that I've just upgraded to Office 2013 64 bit and it all went wrong.
There is the usual faff of converting ... Declare Function ... into ... Declare PtrSafe Function ..., which is well documented elsewhere. However, what I couldn't find any documentation on is that fact that the window hierarchy ('XLMAIN' -> 'XLDESK' -> 'EXCEL7') that the original code expects has changed following this upgrade. For anyone following in my footsteps, to save you an afternoon of digging around, I thought I'd post my updated script. It's hard to test, but I think it should be backwards compatible too for good measure.
Option Explicit
#If Win64 Then
Private Declare PtrSafe Function FindWindowEx Lib "user32" Alias "FindWindowExA" (ByVal hWnd1 As LongPtr, ByVal hWnd2 As LongPtr, ByVal lpsz1 As String, ByVal lpsz2 As String) As LongPtr
Private Declare PtrSafe Function GetClassName Lib "user32" Alias "GetClassNameA" (ByVal Hwnd As LongPtr, ByVal lpClassName As String, ByVal nMaxCount As LongPtr) As LongPtr
Private Declare PtrSafe Function IIDFromString Lib "ole32" (ByVal lpsz As LongPtr, ByRef lpiid As UUID) As LongPtr
Private Declare PtrSafe Function AccessibleObjectFromWindow Lib "oleacc" (ByVal Hwnd As LongPtr, ByVal dwId As LongPtr, ByRef riid As UUID, ByRef ppvObject As Object) As LongPtr
#Else
Private Declare Function FindWindowEx Lib "user32" Alias "FindWindowExA" (ByVal hWnd1 As Long, ByVal hWnd2 As Long, ByVal lpsz1 As String, ByVal lpsz2 As String) As Long
Private Declare Function GetClassName Lib "user32" Alias "GetClassNameA" (ByVal hwnd As Long, ByVal lpClassName As String, ByVal nMaxCount As Long) As Long
Private Declare Function IIDFromString Lib "ole32" (ByVal lpsz As Long, ByRef lpiid As UUID) As Long
Private Declare Function AccessibleObjectFromWindow Lib "oleacc" (ByVal hwnd As Long, ByVal dwId As Long, ByRef riid As UUID, ByRef ppvObject As Object) As Long
#End If
Type UUID 'GUID
Data1 As Long
Data2 As Integer
Data3 As Integer
Data4(7) As Byte
End Type
Const IID_IDispatch As String = "{00020400-0000-0000-C000-000000000046}"
Const OBJID_NATIVEOM As LongPtr = &HFFFFFFF0
' Run as entry point of example
Public Sub Test()
Dim i As Long
Dim xlApps() As Application
If GetAllExcelInstances(xlApps) Then
For i = LBound(xlApps) To UBound(xlApps)
If xlApps(i).Workbooks(1).Name <> ThisWorkbook.Name Then
MsgBox (xlApps(i).Workbooks(1).Name)
End If
Next
End If
End Sub
' Actual public facing function to be called in other code
Public Function GetAllExcelInstances(xlApps() As Application) As Long
On Error GoTo MyErrorHandler
Dim n As Long
#If Win64 Then
Dim hWndMain As LongPtr
#Else
Dim hWndMain As Long
#End If
Dim app As Application
' Cater for 100 potential Excel instances, clearly could be better
ReDim xlApps(1 To 100)
hWndMain = FindWindowEx(0&, 0&, "XLMAIN", vbNullString)
Do While hWndMain <> 0
Set app = GetExcelObjectFromHwnd(hWndMain)
If Not (app Is Nothing) Then
If n = 0 Then
n = n + 1
Set xlApps(n) = app
ElseIf checkHwnds(xlApps, app.Hwnd) Then
n = n + 1
Set xlApps(n) = app
End If
End If
hWndMain = FindWindowEx(0&, hWndMain, "XLMAIN", vbNullString)
Loop
If n Then
ReDim Preserve xlApps(1 To n)
GetAllExcelInstances = n
Else
Erase xlApps
End If
Exit Function
MyErrorHandler:
MsgBox "GetAllExcelInstances" & vbCrLf & vbCrLf & "Err = " & Err.Number & vbCrLf & "Description: " & Err.Description
End Function
#If Win64 Then
Private Function checkHwnds(xlApps() As Application, Hwnd As LongPtr) As Boolean
#Else
Private Function checkHwnds(xlApps() As Application, Hwnd As Long) As Boolean
#End If
Dim i As Integer
For i = LBound(xlApps) To UBound(xlApps)
If xlApps(i).Hwnd = Hwnd Then
checkHwnds = False
Exit Function
End If
Next i
checkHwnds = True
End Function
#If Win64 Then
Private Function GetExcelObjectFromHwnd(ByVal hWndMain As LongPtr) As Application
#Else
Private Function GetExcelObjectFromHwnd(ByVal hWndMain As Long) As Application
#End If
On Error GoTo MyErrorHandler
#If Win64 Then
Dim hWndDesk As LongPtr
Dim Hwnd As LongPtr
#Else
Dim hWndDesk As Long
Dim Hwnd As Long
#End If
Dim strText As String
Dim lngRet As Long
Dim iid As UUID
Dim obj As Object
hWndDesk = FindWindowEx(hWndMain, 0&, "XLDESK", vbNullString)
If hWndDesk <> 0 Then
Hwnd = FindWindowEx(hWndDesk, 0, vbNullString, vbNullString)
Do While Hwnd <> 0
strText = String$(100, Chr$(0))
lngRet = CLng(GetClassName(Hwnd, strText, 100))
If Left$(strText, lngRet) = "EXCEL7" Then
Call IIDFromString(StrPtr(IID_IDispatch), iid)
If AccessibleObjectFromWindow(Hwnd, OBJID_NATIVEOM, iid, obj) = 0 Then 'S_OK
Set GetExcelObjectFromHwnd = obj.Application
Exit Function
End If
End If
Hwnd = FindWindowEx(hWndDesk, Hwnd, vbNullString, vbNullString)
Loop
On Error Resume Next
End If
Exit Function
MyErrorHandler:
MsgBox "GetExcelObjectFromHwnd" & vbCrLf & vbCrLf & "Err = " & Err.Number & vbCrLf & "Description: " & Err.Description
End Function
I had a similar problem/goal.
And I got ForEachLoops answer working, but there is a change that needs made.
In the bottom function (GetExcelObjectFromHwnd), he used the workbook index of 1 in both debug.print commands. The result is you only see the first WB.
So I took his code, and put a for loop inside GetExcelObjectFromHwnd, and changed the 1 to a counter. the result is I can get ALL active excel workbooks and return the information I need to reach across instances of Excel and access other WB's.
And I created a Type to simplify retrieving of the info and pass it back to the calling subroutine:
Type TargetWBType
name As String
returnObj As Object
returnApp As Excel.Application
returnWBIndex As Integer
End Type
For name I simply used the base filename, e.g. "example.xls". This snippet proves the functionality by spitting out the value of A6 on every WS of the target WB. Like so:
Dim targetWB As TargetWBType
targetWB.name = "example.xls"
Call GetAllWorkbookWindowNames(targetWB)
If Not targetWB.returnObj Is Nothing Then
Set targetWB.returnApp = targetWB.returnObj.Application
Dim ws As Worksheet
For Each ws In targetWB.returnApp.Workbooks(targetWB.returnWBIndex).Worksheets
MsgBox ws.Range("A6").Value
Next
Else
MsgBox "Target WB Not found"
End If
So now the ENTIRE module that ForEachLoop originally made looks like this, and I've indicated the changes I made. It does have a msgbox popup, whcih I left in the snippet for debugging purposes. Strip that out once it's finding your target. The code:
Declare Function FindWindowEx Lib "User32" Alias "FindWindowExA" (ByVal hWnd1 As Long, ByVal hWnd2 As Long, ByVal lpsz1 As String, ByVal lpsz2 As String) As Long
Declare Function GetClassName Lib "User32" Alias "GetClassNameA" (ByVal hWnd As Long, ByVal lpClassName As String, ByVal nMaxCount As Long) As Long
Declare Function IIDFromString Lib "ole32" (ByVal lpsz As Long, ByRef lpiid As UUID) As Long
Declare Function AccessibleObjectFromWindow Lib "oleacc" (ByVal hWnd As Long, ByVal dwId As Long, ByRef riid As UUID, ByRef ppvObject As Object) As Long
Type UUID 'GUID
Data1 As Long
Data2 As Integer
Data3 As Integer
Data4(7) As Byte
End Type
'------------- Form Module --------------
Option Explicit
Const IID_IDispatch As String = "{00020400-0000-0000-C000-000000000046}"
Const OBJID_NATIVEOM As Long = &HFFFFFFF0
'My code: added targetWB
Sub GetAllWorkbookWindowNames(targetWB As TargetWBType)
On Error GoTo MyErrorHandler
Dim hWndMain As Long
hWndMain = FindWindowEx(0&, 0&, "XLMAIN", vbNullString)
Do While hWndMain <> 0
GetWbkWindows hWndMain, targetWB 'My code: added targetWB
hWndMain = FindWindowEx(0&, hWndMain, "XLMAIN", vbNullString)
Loop
Exit Sub
MyErrorHandler:
MsgBox "GetAllWorkbookWindowNames" & vbCrLf & vbCrLf & "Err = " & Err.Number & vbCrLf & "Description: " & Err.Description
End Sub
'My code: added targetWB
Private Sub GetWbkWindows(ByVal hWndMain As Long, targetWB As TargetWBType)
On Error GoTo MyErrorHandler
Dim hWndDesk As Long
hWndDesk = FindWindowEx(hWndMain, 0&, "XLDESK", vbNullString)
If hWndDesk <> 0 Then
Dim hWnd As Long
hWnd = FindWindowEx(hWndDesk, 0, vbNullString, vbNullString)
Dim strText As String
Dim lngRet As Long
Do While hWnd <> 0
strText = String$(100, Chr$(0))
lngRet = GetClassName(hWnd, strText, 100)
If Left$(strText, lngRet) = "EXCEL7" Then
GetExcelObjectFromHwnd hWnd, targetWB 'My code: added targetWB
Exit Sub
End If
hWnd = FindWindowEx(hWndDesk, hWnd, vbNullString, vbNullString)
Loop
On Error Resume Next
End If
Exit Sub
MyErrorHandler:
MsgBox "GetWbkWindows" & vbCrLf & vbCrLf & "Err = " & Err.Number & vbCrLf & "Description: " & Err.Description
End Sub
'My code: added targetWB
Public Function GetExcelObjectFromHwnd(ByVal hWnd As Long, targetWB As TargetWBType) As Boolean
On Error GoTo MyErrorHandler
Dim fOk As Boolean
fOk = False
Dim iid As UUID
Call IIDFromString(StrPtr(IID_IDispatch), iid)
Dim obj As Object
If AccessibleObjectFromWindow(hWnd, OBJID_NATIVEOM, iid, obj) = 0 Then 'S_OK
Dim objApp As Excel.Application
Set objApp = obj.Application
'My code
Dim wbCount As Integer
For wbCount = 1 To objApp.Workbooks.Count
'End my code
'Not my code
Debug.Print objApp.Workbooks(wbCount).name
'My code
If LCase(objApp.Workbooks(wbCount).name) = LCase(targetWB.name) Then
MsgBox ("Found target: " & targetWB.name)
Set targetWB.returnObj = obj
targetWB.returnWBIndex = wbCount
End If
'End My code
'Not my code
Dim myWorksheet As Worksheet
For Each myWorksheet In objApp.Workbooks(wbCount).Worksheets
Debug.Print " " & myWorksheet.name
DoEvents
Next
'My code
Next
'Not my code
fOk = True
End If
GetExcelObjectFromHwnd = fOk
Exit Function
MyErrorHandler:
MsgBox "GetExcelObjectFromHwnd" & vbCrLf & vbCrLf & "Err = " & Err.Number & vbCrLf & "Description: " & Err.Description
End Function
I repeat, this works, and using the variables within the TargetWB type I am reliably accessing workbooks and worksheets across instances of Excel.
The only potential problem I see with my solution, is if you have multiple WB's with the same name. Right now, I believe it would return the last instance of that name. If we add an Exit For into the If Then I believe it will instead return the first instance of it. I didn't test this part thouroughly as in my application there is only ever one instance of the file open.
Just to add to James MacAdie's answer, I think you do the redim too late because in the checkHwnds function you end up with an out of range error as you're trying to check values up to 100 even though you haven't yet populated the array fully? I modified the code to the below and it's now working for me.
' Actual public facing function to be called in other code
Public Function GetAllExcelInstances(xlApps() As Application) As Long
On Error GoTo MyErrorHandler
Dim n As Long
#If Win64 Then
Dim hWndMain As LongPtr
#Else
Dim hWndMain As Long
#End If
Dim app As Application
' Cater for 100 potential Excel instances, clearly could be better
ReDim xlApps(1 To 100)
hWndMain = FindWindowEx(0&, 0&, "XLMAIN", vbNullString)
Do While hWndMain <> 0
Set app = GetExcelObjectFromHwnd(hWndMain)
If Not (app Is Nothing) Then
If n = 0 Then
n = n + 1
ReDim Preserve xlApps(1 To n)
Set xlApps(n) = app
ElseIf checkHwnds(xlApps, app.Hwnd) Then
n = n + 1
ReDim Preserve xlApps(1 To n)
Set xlApps(n) = app
End If
End If
hWndMain = FindWindowEx(0&, hWndMain, "XLMAIN", vbNullString)
Loop
If n Then
GetAllExcelInstances = n
Else
Erase xlApps
End If
Exit Function
MyErrorHandler:
MsgBox "GetAllExcelInstances" & vbCrLf & vbCrLf & "Err = " & Err.Number & vbCrLf & "Description: " & Err.Description
End Function
I don't believe this is possible using only VBA because the highest level object you can get to is the Application object which is the current instance of Excel.