使用 VB.NET 获取 Excel 的 运行 个实例
Get running instances of Excel with VB.NET
我从 中获取了以下工作代码:
Option Compare Binary
Option Explicit On
Option Infer On
Option Strict Off
Imports Microsoft.Office.Interop
Imports System.Collections.Generic
Imports System.Runtime.InteropServices
Friend Module Module1
Private Declare Function GetDesktopWindow Lib "user32" () As IntPtr
Private Declare Function EnumChildWindows Lib "user32.dll" (ByVal WindowHandle As IntPtr, ByVal Callback As EnumWindowsProc, ByVal lParam As IntPtr) As Boolean
Private Declare Function GetClassName Lib "user32.dll" Alias "GetClassNameA" (ByVal hWnd As IntPtr, ByVal lpClassName As String, ByVal nMaxCount As Integer) As Integer
Private Delegate Function EnumWindowsProc(ByVal hwnd As IntPtr, ByVal lParam As Int32) As Boolean
Private Declare Function AccessibleObjectFromWindow Lib "oleacc" (ByVal Hwnd As IntPtr, ByVal dwId As Int32, ByRef riid As Guid, <MarshalAs(UnmanagedType.IUnknown)> ByRef ppvObject As Object) As Int32
Private lstWorkBooks As New List(Of String)
Public Sub Main()
GetExcelOpenWorkBooks()
End Sub
Private Sub GetExcelOpenWorkBooks()
EnumChildWindows(GetDesktopWindow(), AddressOf GetExcelWindows, CType(0, IntPtr))
If lstWorkBooks.Count > 0 Then MsgBox(String.Join(Environment.NewLine, lstWorkBooks))
End Sub
Public Function GetExcelWindows(ByVal hwnd As IntPtr, ByVal lParam As Int32) As Boolean
Dim Ret As Integer = 0
Dim className As String = Space(255)
Ret = GetClassName(hwnd, className, 255)
className = className.Substring(0, Ret)
If className = "EXCEL7" Then
Dim ExcelApplication As Excel.Application
Dim ExcelObject As Object = Nothing
Dim IDispatch As Guid
AccessibleObjectFromWindow(hwnd, &HFFFFFFF0, IDispatch, ExcelObject)
If ExcelObject IsNot Nothing Then
ExcelApplication = ExcelObject.Application
If ExcelApplication IsNot Nothing Then
For Each wrk As Excel.Workbook In ExcelApplication.Workbooks
If Not lstWorkBooks.Contains(wrk.Name) Then
lstWorkBooks.Add(wrk.Name)
End If
Next
End If
End If
End If
Return True
End Function
End Module
它将用于获取所有 opened/running Excel instances/applications.
的引用
如果不在网上查找,我永远不会猜到该怎么做,因为我不太了解它,所以这可能不是最好的方法 bug/error prone. I'm trying to turn option strict on (1, 2) 所以我更改了行 ExcelApplication = ExcelObject.Application
到 ExcelApplication = CType(ExcelObject, Excel.Application).Application
但这样做会抛出异常:
System.InvalidCastException Unable to cast COM object of type 'System.__ComObject' to interface type 'Microsoft.Office.Interop.Excel.Application'. This operation failed because the Query Interface call on the COM component for the interface with IID '{000208D5-0000-0000-C000-000000000046}' failed due to the following error: No such interface supported. (Exception from HRESULT: 0x80004002 (E_NOINTERFACE)).
我可以在不同的站点找到多个看起来相似的引用,但没有运气用 trial and error method 修复它。
我的问题是如何 turn on option strict 如果有人帮助我获得更好的代码或 fix/explain 任何其他问题,我会得到奖励。
关于主要 objective,访问现有 Excel 实例的已打开工作簿(创建 运行 EXCEL.EXE
。当然包括对 Shell 打开 Excel-associated 文件扩展名)。
The following method uses Console.WriteLine()
just to evaluate
(eventually setting a BreakPoint), the current values of some objects.
It's clearly redundant (has to be deleted/commented out before
release).
它创建一个本地List(Of Workbook)
,这是返回给调用者的:
请注意,每次创建 Interop object 时都会进行编组并设置为空。
为什么两者都有?调试时检查 objects,你会看到。
Process.GetProcessesByName("EXCEL")也是多余的。同样,仅用于计算返回的 Process
object 并检查它们的值。
使用 Marshal.GetActiveObject()
访问 Excel 活动实例(如果有)
请注意,这将 不会 创建一个新进程。我们正在访问现有实例。
Visual Studio Version: 15.7.6 - 15.8.3
.Net FrameWork version: 4.7.1
Option Strict: On, Option Explicit: On, Option Infer: On
Public Function FindOpenedWorkBooks() As List(Of Workbook)
Dim OpenedWorkBooks As New List(Of Workbook)()
Dim ExcelInstances As Process() = Process.GetProcessesByName("EXCEL")
If ExcelInstances.Count() = 0 Then
Return Nothing
End If
Dim ExcelInstance As Excel.Application = TryCast(Marshal.GetActiveObject("Excel.Application"), Excel.Application)
If ExcelInstance Is Nothing Then Return Nothing
Dim worksheets As Sheets = Nothing
For Each WB As Workbook In ExcelInstance.Workbooks
OpenedWorkBooks.Add(WB)
worksheets = WB.Worksheets
Console.WriteLine(WB.FullName)
For Each ws As Worksheet In worksheets
Console.WriteLine(ws.Name)
Marshal.ReleaseComObject(ws)
Next
Next
Marshal.ReleaseComObject(worksheets)
worksheets = Nothing
Marshal.FinalReleaseComObject(ExcelInstance)
Marshal.CleanupUnusedObjectsInCurrentContext()
ExcelInstance = Nothing
Return OpenedWorkBooks
End Function
返回的 List(Of Workbook)
包含活动的 object。那些 object 尚未编组并且可以访问。
您可以这样调用 FindOpenedWorkBooks()
方法:
(有些值,如 WorkSheet.Columns.Count
,毫无价值。这些值用于表明您访问了每个 WorkSheet values in each of the Sheets returned, for all the WorkBooks 找到的值)
Excel.Range
object 为访问 Cell 的值而创建(此处为第一列 Header):
Dim CellRange As Excel.Range = CType(ws.Cells(1, 1), Excel.Range)
是一个新的 Interop object,因此在评估其价值后发布。
Private ExcelWorkBooks As List(Of Workbook) = New List(Of Workbook)()
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
ExcelWorkBooks = FindOpenedWorkBooks()
If ExcelWorkBooks IsNot Nothing Then
Dim WBNames As New StringBuilder()
For Each wb As Workbook In ExcelWorkBooks
WBNames.AppendLine(wb.Name)
Dim sheets As Sheets = wb.Worksheets
Console.WriteLine($"Sheets No.: { sheets.Count}")
For Each ws As Worksheet In sheets
Console.WriteLine($"WorkSheet Name: {ws.Name} Columns: {ws.Columns.Count} Rows: {ws.Rows.Count}")
Dim CellRange As Excel.Range = CType(ws.Cells(1, 1), Excel.Range)
Console.WriteLine(CellRange.Value2.ToString)
Marshal.ReleaseComObject(CellRange)
Marshal.ReleaseComObject(ws)
Next
Marshal.ReleaseComObject(sheets)
Next
MessageBox.Show(WBNames.ToString())
End If
End Sub
必须释放哪些 object?您创建的所有 object。
假设您必须打开一个新的 Excel 文件并且您想要访问其中的一个 WorkBook
。
(这将创建一个新进程)
Dim WorkBook1Path As String = "[Some .xlsx Path]"
Dim ExcelApplication As New Excel.Application()
Dim ExcelWorkbooks As Workbooks = ExcelApplication.Workbooks
Dim MyWorkbook As Workbook = ExcelWorkbooks.Open(WorkBook1Path, False)
Dim worksheets As Sheets = MyWorkbook.Worksheets
Dim MyWorksheet As Worksheet = CType(worksheets("Sheet1"), Worksheet)
'(...)
'Do your processing here
'(...)
Marshal.ReleaseComObject(MyWorksheet)
Marshal.ReleaseComObject(worksheets)
MyWorkbook.Close(False) 'Don't save
Marshal.ReleaseComObject(MyWorkbook)
ExcelWorkbooks.Close()
Marshal.ReleaseComObject(ExcelWorkbooks)
ExcelApplication.Quit()
Marshal.FinalReleaseComObject(ExcelApplication)
Marshal.CleanupUnusedObjectsInCurrentContext()
同样,必须释放所有 object。 WorkBooks
必须是 .Close()
d,如果需要,它们的内容会被保存。 WorkBooks
collection 必须是 .Close()
d.
使用Excel.Application
mainobject.Quit()
方法通知操作结束
.Quit()
不会终止您创建的进程。
Marshal.FinalReleaseComObject(ExcelApplication)
用于完成它的发布。
此时EXCEL
进程将结束。
最后一条指令 Marshal.CleanupUnusedObjectsInCurrentContext()` 是 clean-up 预防措施。
可能甚至没有必要,但这并没有什么坏处:我们要退出这里了。
当然,您可以在应用程序的初始化过程中将所有这些 object 实例化一次,然后在应用程序关闭时封送它们。
当使用 Form
class 时,它会创建一个可用于此任务的 Dispose()
方法。
如果您在自己的 class 中实施这些过程,请实施 IDisposable interface and implement the required Dispose() method.
但是,如果您不想或不能照顾所有这些 objects instantiation/destruction 怎么办?
可能,您更喜欢在实例化新 object 时使用类型推断。所以你设置 Option Explicit
和 Option Strict
ON
,同时保持 Option Infer On
。许多人这样做。
所以你这样写:
Dim MyWorkbook = ExcelWorkbooks.Open([FilePath], False)
而不是:
Dim MyWorkbook As Workbook = ExcelWorkbooks.Open([FilePath], False)
有时很清楚 object(s) 已经(已经)创建了什么来满足您的要求。
有时绝对不是。
因此,许多人更喜欢实施不同的 模式 到 Release/Dispose Interop objects.
这里可以看到很多方法(c#为主,但大同小异):
How do I properly clean up Excel interop objects?
这个深思熟虑的实现:
Application not quitting after calling quit
此外,TnTinMn
在这里描述的一种特殊方式:
Excel COM Object not getting released
测试,找到你的方法:)。
永远不要使用 Process.Kill()
。除其他外,您不知道要终止什么。
此外,managed/unmanaged 代码:
中关于 COM 编组的一些有趣读物
Visual Studio工程团队:
Marshal.ReleaseComObject Considered Dangerous
Hans Passant 谈 COM 编组和垃圾 Collection:
Understanding garbage collection in .NET
关于运行时可调用包装器 (RCW) 和 COM 可调用包装器 (CCW) 的 MSDN 文档
Runtime Callable Wrapper
COM Callable Wrapper
which I had previously marked as accepted is great, but there is a catch (*),也就是它只获取主动对象,第一个Excel进程。
在大多数情况下这就足够了,但在打开多个 Excel 实例的特定情况下就不够了。据我所知,这只能通过在启动 Excel 时按住 Alt
键来提示在新实例中启动 Excel 或使用某些程序中的代码来实现。
另一方面,问题中的代码确实有效并解决了获取 Excel 的所有 运行 个实例的问题。我遇到的唯一问题是将它从后期绑定 (Option Strict Off
) 转换为早期绑定 (Option Strict On
),这导致了一个直到现在我都找不到答案的错误。
在另一个解决 C# 问题的问题中的 answer 的帮助下,我发现我必须将函数 AccessibleObjectFromWindow
的参数 ppvObject
替换为:
<MarshalAs(UnmanagedType.IUnknown)> ByRef ppvObject As Object
收件人:
ByRef ppvObject As Excel.Window
并将声明中变量 ExcelObject
的类型从 Object
更改为 Excel.Window
(在代码中将其重命名为 ExcelWindow
也是一个好习惯)。
我从
Option Compare Binary
Option Explicit On
Option Infer On
Option Strict Off
Imports Microsoft.Office.Interop
Imports System.Collections.Generic
Imports System.Runtime.InteropServices
Friend Module Module1
Private Declare Function GetDesktopWindow Lib "user32" () As IntPtr
Private Declare Function EnumChildWindows Lib "user32.dll" (ByVal WindowHandle As IntPtr, ByVal Callback As EnumWindowsProc, ByVal lParam As IntPtr) As Boolean
Private Declare Function GetClassName Lib "user32.dll" Alias "GetClassNameA" (ByVal hWnd As IntPtr, ByVal lpClassName As String, ByVal nMaxCount As Integer) As Integer
Private Delegate Function EnumWindowsProc(ByVal hwnd As IntPtr, ByVal lParam As Int32) As Boolean
Private Declare Function AccessibleObjectFromWindow Lib "oleacc" (ByVal Hwnd As IntPtr, ByVal dwId As Int32, ByRef riid As Guid, <MarshalAs(UnmanagedType.IUnknown)> ByRef ppvObject As Object) As Int32
Private lstWorkBooks As New List(Of String)
Public Sub Main()
GetExcelOpenWorkBooks()
End Sub
Private Sub GetExcelOpenWorkBooks()
EnumChildWindows(GetDesktopWindow(), AddressOf GetExcelWindows, CType(0, IntPtr))
If lstWorkBooks.Count > 0 Then MsgBox(String.Join(Environment.NewLine, lstWorkBooks))
End Sub
Public Function GetExcelWindows(ByVal hwnd As IntPtr, ByVal lParam As Int32) As Boolean
Dim Ret As Integer = 0
Dim className As String = Space(255)
Ret = GetClassName(hwnd, className, 255)
className = className.Substring(0, Ret)
If className = "EXCEL7" Then
Dim ExcelApplication As Excel.Application
Dim ExcelObject As Object = Nothing
Dim IDispatch As Guid
AccessibleObjectFromWindow(hwnd, &HFFFFFFF0, IDispatch, ExcelObject)
If ExcelObject IsNot Nothing Then
ExcelApplication = ExcelObject.Application
If ExcelApplication IsNot Nothing Then
For Each wrk As Excel.Workbook In ExcelApplication.Workbooks
If Not lstWorkBooks.Contains(wrk.Name) Then
lstWorkBooks.Add(wrk.Name)
End If
Next
End If
End If
End If
Return True
End Function
End Module
它将用于获取所有 opened/running Excel instances/applications.
的引用如果不在网上查找,我永远不会猜到该怎么做,因为我不太了解它,所以这可能不是最好的方法 bug/error prone. I'm trying to turn option strict on (1, 2) 所以我更改了行 ExcelApplication = ExcelObject.Application
到 ExcelApplication = CType(ExcelObject, Excel.Application).Application
但这样做会抛出异常:
System.InvalidCastException Unable to cast COM object of type 'System.__ComObject' to interface type 'Microsoft.Office.Interop.Excel.Application'. This operation failed because the Query Interface call on the COM component for the interface with IID '{000208D5-0000-0000-C000-000000000046}' failed due to the following error: No such interface supported. (Exception from HRESULT: 0x80004002 (E_NOINTERFACE)).
我可以在不同的站点找到多个看起来相似的引用,但没有运气用 trial and error method 修复它。
我的问题是如何 turn on option strict 如果有人帮助我获得更好的代码或 fix/explain 任何其他问题,我会得到奖励。
关于主要 objective,访问现有 Excel 实例的已打开工作簿(创建 运行 EXCEL.EXE
。当然包括对 Shell 打开 Excel-associated 文件扩展名)。
The following method uses
Console.WriteLine()
just to evaluate (eventually setting a BreakPoint), the current values of some objects. It's clearly redundant (has to be deleted/commented out before release).
它创建一个本地List(Of Workbook)
,这是返回给调用者的:
请注意,每次创建 Interop object 时都会进行编组并设置为空。
为什么两者都有?调试时检查 objects,你会看到。
Process.GetProcessesByName("EXCEL")也是多余的。同样,仅用于计算返回的 Process
object 并检查它们的值。
使用 Marshal.GetActiveObject()
访问 Excel 活动实例(如果有)
请注意,这将 不会 创建一个新进程。我们正在访问现有实例。
Visual Studio Version: 15.7.6 - 15.8.3
.Net FrameWork version: 4.7.1
Option Strict: On, Option Explicit: On, Option Infer: On
Public Function FindOpenedWorkBooks() As List(Of Workbook)
Dim OpenedWorkBooks As New List(Of Workbook)()
Dim ExcelInstances As Process() = Process.GetProcessesByName("EXCEL")
If ExcelInstances.Count() = 0 Then
Return Nothing
End If
Dim ExcelInstance As Excel.Application = TryCast(Marshal.GetActiveObject("Excel.Application"), Excel.Application)
If ExcelInstance Is Nothing Then Return Nothing
Dim worksheets As Sheets = Nothing
For Each WB As Workbook In ExcelInstance.Workbooks
OpenedWorkBooks.Add(WB)
worksheets = WB.Worksheets
Console.WriteLine(WB.FullName)
For Each ws As Worksheet In worksheets
Console.WriteLine(ws.Name)
Marshal.ReleaseComObject(ws)
Next
Next
Marshal.ReleaseComObject(worksheets)
worksheets = Nothing
Marshal.FinalReleaseComObject(ExcelInstance)
Marshal.CleanupUnusedObjectsInCurrentContext()
ExcelInstance = Nothing
Return OpenedWorkBooks
End Function
返回的 List(Of Workbook)
包含活动的 object。那些 object 尚未编组并且可以访问。
您可以这样调用 FindOpenedWorkBooks()
方法:
(有些值,如 WorkSheet.Columns.Count
,毫无价值。这些值用于表明您访问了每个 WorkSheet values in each of the Sheets returned, for all the WorkBooks 找到的值)
Excel.Range
object 为访问 Cell 的值而创建(此处为第一列 Header):
Dim CellRange As Excel.Range = CType(ws.Cells(1, 1), Excel.Range)
是一个新的 Interop object,因此在评估其价值后发布。
Private ExcelWorkBooks As List(Of Workbook) = New List(Of Workbook)()
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
ExcelWorkBooks = FindOpenedWorkBooks()
If ExcelWorkBooks IsNot Nothing Then
Dim WBNames As New StringBuilder()
For Each wb As Workbook In ExcelWorkBooks
WBNames.AppendLine(wb.Name)
Dim sheets As Sheets = wb.Worksheets
Console.WriteLine($"Sheets No.: { sheets.Count}")
For Each ws As Worksheet In sheets
Console.WriteLine($"WorkSheet Name: {ws.Name} Columns: {ws.Columns.Count} Rows: {ws.Rows.Count}")
Dim CellRange As Excel.Range = CType(ws.Cells(1, 1), Excel.Range)
Console.WriteLine(CellRange.Value2.ToString)
Marshal.ReleaseComObject(CellRange)
Marshal.ReleaseComObject(ws)
Next
Marshal.ReleaseComObject(sheets)
Next
MessageBox.Show(WBNames.ToString())
End If
End Sub
必须释放哪些 object?您创建的所有 object。
假设您必须打开一个新的 Excel 文件并且您想要访问其中的一个 WorkBook
。
(这将创建一个新进程)
Dim WorkBook1Path As String = "[Some .xlsx Path]"
Dim ExcelApplication As New Excel.Application()
Dim ExcelWorkbooks As Workbooks = ExcelApplication.Workbooks
Dim MyWorkbook As Workbook = ExcelWorkbooks.Open(WorkBook1Path, False)
Dim worksheets As Sheets = MyWorkbook.Worksheets
Dim MyWorksheet As Worksheet = CType(worksheets("Sheet1"), Worksheet)
'(...)
'Do your processing here
'(...)
Marshal.ReleaseComObject(MyWorksheet)
Marshal.ReleaseComObject(worksheets)
MyWorkbook.Close(False) 'Don't save
Marshal.ReleaseComObject(MyWorkbook)
ExcelWorkbooks.Close()
Marshal.ReleaseComObject(ExcelWorkbooks)
ExcelApplication.Quit()
Marshal.FinalReleaseComObject(ExcelApplication)
Marshal.CleanupUnusedObjectsInCurrentContext()
同样,必须释放所有 object。 WorkBooks
必须是 .Close()
d,如果需要,它们的内容会被保存。 WorkBooks
collection 必须是 .Close()
d.
使用Excel.Application
mainobject.Quit()
方法通知操作结束
.Quit()
不会终止您创建的进程。
Marshal.FinalReleaseComObject(ExcelApplication)
用于完成它的发布。
此时EXCEL
进程将结束。
最后一条指令 Marshal.CleanupUnusedObjectsInCurrentContext()` 是 clean-up 预防措施。
可能甚至没有必要,但这并没有什么坏处:我们要退出这里了。
当然,您可以在应用程序的初始化过程中将所有这些 object 实例化一次,然后在应用程序关闭时封送它们。
当使用 Form
class 时,它会创建一个可用于此任务的 Dispose()
方法。
如果您在自己的 class 中实施这些过程,请实施 IDisposable interface and implement the required Dispose() method.
但是,如果您不想或不能照顾所有这些 objects instantiation/destruction 怎么办?
可能,您更喜欢在实例化新 object 时使用类型推断。所以你设置 Option Explicit
和 Option Strict
ON
,同时保持 Option Infer On
。许多人这样做。
所以你这样写:
Dim MyWorkbook = ExcelWorkbooks.Open([FilePath], False)
而不是:
Dim MyWorkbook As Workbook = ExcelWorkbooks.Open([FilePath], False)
有时很清楚 object(s) 已经(已经)创建了什么来满足您的要求。
有时绝对不是。
因此,许多人更喜欢实施不同的 模式 到 Release/Dispose Interop objects.
这里可以看到很多方法(c#为主,但大同小异):
How do I properly clean up Excel interop objects?
这个深思熟虑的实现:
Application not quitting after calling quit
此外,TnTinMn
在这里描述的一种特殊方式:
Excel COM Object not getting released
测试,找到你的方法:)。
永远不要使用 Process.Kill()
。除其他外,您不知道要终止什么。
此外,managed/unmanaged 代码:
中关于 COM 编组的一些有趣读物Visual Studio工程团队:
Marshal.ReleaseComObject Considered Dangerous
Hans Passant 谈 COM 编组和垃圾 Collection:
Understanding garbage collection in .NET
关于运行时可调用包装器 (RCW) 和 COM 可调用包装器 (CCW) 的 MSDN 文档
Runtime Callable Wrapper
COM Callable Wrapper
在大多数情况下这就足够了,但在打开多个 Excel 实例的特定情况下就不够了。据我所知,这只能通过在启动 Excel 时按住 Alt
键来提示在新实例中启动 Excel 或使用某些程序中的代码来实现。
另一方面,问题中的代码确实有效并解决了获取 Excel 的所有 运行 个实例的问题。我遇到的唯一问题是将它从后期绑定 (Option Strict Off
) 转换为早期绑定 (Option Strict On
),这导致了一个直到现在我都找不到答案的错误。
在另一个解决 C# 问题的问题中的 answer 的帮助下,我发现我必须将函数 AccessibleObjectFromWindow
的参数 ppvObject
替换为:
<MarshalAs(UnmanagedType.IUnknown)> ByRef ppvObject As Object
收件人:
ByRef ppvObject As Excel.Window
并将声明中变量 ExcelObject
的类型从 Object
更改为 Excel.Window
(在代码中将其重命名为 ExcelWindow
也是一个好习惯)。