使用VB.NET获取Excel的运行实例

时间:2018-08-09 14:45:44

标签: excel vb.net exception com excel-interop

我从this answer获取了以下工作代码:

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

它将用于获取所有打开/正在运行的Excel实例/应用程序的引用。

如果不在线查找它,我将永远不会猜出该怎么做,因为我对此不太了解,所以它可能不是最好的方法,bug/error prone是。 我正在尝试打开option strict 12),因此我将行ExcelApplication = ExcelObject.Application更改为ExcelApplication = CType(ExcelObject, Excel.Application).Application因此抛出异常:

  

System.InvalidCastException 无法将类型为“ System .__ ComObject”的COM对象转换为接口类型为“ Microsoft.Office.Interop.Excel.Application”。该操作失败,因为具有以下ID的IID为“ {000208D5-0000-0000-C000-000000000046}”的接口在COM组件上的查询接口调用由于以下错误而失败:不支持这种接口。 (来自HRESULT的异常:0x80004002(E_NOINTERFACE))。

我可以在不同的站点中找到多个与此相似的引用,但是还没有运气用trial and error method来解决它。

我的问题是,如果有人帮助我获得更好的代码或修复/解释任何其他问题,该如何turn on option strict并给予奖励。

2 个答案:

答案 0 :(得分:1)

关于主要目标,请访问现有Excel实例(运行EXCEL.EXE创建的打开的工作簿。当然,其中包括向Shell提出的打开与Excel相关的文件扩展名的请求。)
< / p>

  

以下方法仅使用Console.WriteLine()进行评估   (最终设置一个BreakPoint),即某些对象的当前值。   显然是多余的(必须先删除/注释掉)   发布)。

它将创建一个本地List(Of Workbook),该本地Process返回给调用方:
请注意,每次创建Interop对象时,都会对其进行混搭并将其设置为空。
为什么两者都这样?调试时检查对象,您会看到。

Process.GetProcessesByName("EXCEL")也是多余的。同样,仅用于评估返回的Visual Studio Version: 15.7.6 - 15.8.3对象并检查其值。

使用Marshal.GetActiveObject()访问Excel活动实例(如果有)
请注意,这将创建新的流程。我们正在访问现有实例。

.Net FrameWork version: 4.7.1
Option Strict: On, Option Explicit: On, Option Infer: Off
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)

返回的FindOpenedWorkBooks()包含活动对象。这些对象尚未编组并且可以访问。

您可以像这样调用WorkSheet.Columns.Count方法:
(有些值为Excel.Range的值一文不值。这些值用来表明您为找到的所有WorkSheet返回的Sheets的每个ot中访问每个WorkBooks的值)

创建的Dim CellRange As Excel.Range = CType(ws.Cells(1, 1), Excel.Range)对象用于访问单元格的值(此处为第一个列标题):
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 是一个新的Interop对象,因此在评估其值之后将其释放。

WorkBook

必须释放哪些对象?您创建的所有对象。

假设您必须打开一个新的Excel文件,并且要访问其中的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()
这将创建一个新的流程

WorkBooks

同样,必须释放所有对象。 WorkBooks必须.Close() d,并根据需要保存其内容。 .Close()集合必须为Excel.Application d。
使用.Quit()主对象.Quit()方法来通知操作结束。
EXCEL不会终止您创建的流程
Marshal.FinalReleaseComObject(ExcelApplication)用于完成发布。
此时Form过程将结束。

最后一条指令Marshal.CleanupUnusedObjectsInCurrentContext()`是一种清理预防措施。
可能甚至没有必要,但这并没有造成伤害:我们在这里退出。

当然,您可以在应用程序的初始化过程中实例化所有这些对象一次,然后在应用程序关闭时将它们编组。
使用Dispose()类时,它将创建一个Option Explicit方法,可用于此任务。
如果要在自己的类中实现这些过程,请实现IDisposable interfaceimplement the required Dispose() method

但是,如果您不想或无法照顾所有这些对象的实例化/销毁怎么办?
在实例化新对象时,可能更喜欢使用类型推断。因此,您在设置Option Strict的同时设置了ONOption Infer On Dim MyWorkbook = ExcelWorkbooks.Open([FilePath], False)。许多人这样做。

因此,您编写类似:
Dim MyWorkbook As Workbook = ExcelWorkbooks.Open([FilePath], False)

代替:
TnTinMn

有时候很明显已经创建了满足您要求的对象。
有时绝对不是。

因此,许多人更喜欢实现不同的 pattern 来释放/处理Interop对象。

您可以在这里看到很多方法(主要是c#,但是相同):
How do I properly clean up Excel interop objects?

这种周到的实现:
Application not quitting after calling quit

此外,Process.Kill()在此处描述的一种特殊方式:
Excel COM Object not getting released

测试,找到路吧:)。
切勿使用{{1}}。除其他外,您不知道要终止什么。

此外,有关托管/非托管代码中的COM编组的一些有趣读物

Visual Studio工程团队:
Marshal.ReleaseComObject Considered Dangerous

汉斯·帕森特(Hans Passant)关于COM编组和垃圾回收
Understanding garbage collection in .NET

有关运行时可调用包装(RCW)和COM可调用包装(CCW)的MSDN文档
Runtime Callable Wrapper
COM Callable Wrapper

答案 1 :(得分:0)

我之前标记为接受的other answer很棒,但是有一个陷阱(*),这是因为它仅获取活动对象,即第一个Excel进程。

在大多数情况下这已足够,但对于打开了多个Excel实例的特定实例而言,这还不够。据我所知,这只能通过在启动Excel时按住Alt键,提示在新实例中启动Excel或在某些程序中使用代码来实现。

另一方面,问题中的代码确实起作用,并解决了获取所有正在运行的Excel实例的问题。我唯一遇到的问题是将其从后期绑定(Option Strict Off)转换为早期绑定(Option Strict On),这导致了直到现在我都找不到答案的错误。

在另一个解决C#问题的问题中,在answer的帮助下,我发现必须从以下位置替换函数ppvObject的参数AccessibleObjectFromWindow

<MarshalAs(UnmanagedType.IUnknown)> ByRef ppvObject As Object

收件人:

ByRef ppvObject As Excel.Window

并将声明中的变量ExcelObject的类型从Object更改为Excel.Window(在代码中将其重命名为ExcelWindow也是一种很好的做法)。