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

use*_*973 3 vb.net com excel exception excel-interop

我从这个答案中获取了以下工作代码:

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
Run Code Online (Sandbox Code Playgroud)

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

如果不在线查找,我永远不会猜测如何做到这一点,因为我不太了解它,所以这可能不是最好的方法,并且容易出现错误/错误我试图将选项严格打开1 , 2),所以我将行更改ExcelApplication = ExcelObject.ApplicationExcelApplication = CType(ExcelObject, Excel.Application).Application,但这样做会引发异常:

System.InvalidCastException无法将类型“System.__ComObject”的 COM 对象强制转换为接口类型“Microsoft.Office.Interop.Excel.Application”。此操作失败,因为对 IID 为“{000208D5-0000-0000-C000-000000000046}”的接口的 COM 组件上的查询接口调用因以下错误而失败:不支持此类接口。(HRESULT 异常:0x80004002 (E_NOINTERFACE))。

我可以在不同的网站上找到多个看起来相似的参考,但没有运气用试错法来修复它。

我的问题是,如果有人帮助我获得更好的代码或修复/解释任何其他问题,如何打开选项严格和奖励。

Jim*_*imi 6

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

以下方法Console.WriteLine()仅用于评估(最终设置断点)某些对象的当前值。它显然是多余的(必须在发布之前删除/注释掉)。

它创建一个 local List(Of Workbook),它返回给调用者:
请注意,每次创建 Interop 对象时都会对其进行编组并设置为空。
为什么两者都有?调试时检查对象,您就会看到。

Process.GetProcessesByName ("EXCEL")也是多余的。同样,仅用于评估返回的Process对象并检查它们的值。

使用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
Run Code Online (Sandbox Code Playgroud)

返回的List(Of Workbook)包含活动对象。这些对象尚未整理并且可以访问。

FindOpenedWorkBooks()您可以像这样调用该方法:(
某些值,如WorkSheet.Columns.Count,是没有价值的。这些值用于表明您访问返回的每个工作表中的每个工作表值,对于找到的所有工作簿

Excel.Range创建用于访问单元格值的对象(此处为第一个列标题):是
Dim CellRange As Excel.Range = CType(ws.Cells(1, 1), Excel.Range)一个新的 Interop 对象,因此在计算其值后将其释放。

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
Run Code Online (Sandbox Code Playgroud)

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

假设您必须打开一个新的 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()
Run Code Online (Sandbox Code Playgroud)

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

最后一条指令Marshal.CleanupUnusedObjectsInCurrentContext()`是清理预防措施。
甚至可能没有必要,但这并没有什么坏处:我们要退出这里。

当然,您可以在应用程序的初始化过程中实例化所有这些对象一次,然后在应用程序关闭时封送它们。
使用Form类时,它会创建一个Dispose()可用于此任务的方法。
如果您在自己的类中实现这些过程,请实现IDisposable 接口实现所需的 Dispose() 方法

但是,如果您不想或无法处理所有这些对象的实例化/销毁怎么办?
实例化新对象时,您可能更喜欢使用类型推断。所以你设置Option ExplicitOption Strict ON,同时保留Option Infer On。许多人都这样做。

所以你写一些类似的东西:
Dim MyWorkbook = ExcelWorkbooks.Open([FilePath], False)

代替:
Dim MyWorkbook As Workbook = ExcelWorkbooks.Open([FilePath], False)

有时很清楚已经创建了哪些对象来满足您的请求。
有时绝对不是。

因此,许多人更喜欢实现不同的模式来释放/处置互操作对象。

您可以在这里看到很多方法(主要是 C#,但它是相同的):
How do Iproperly clean up Excel interop object?

这个深思熟虑的实现:
应用程序在调用退出后不会退出

另外,这里描述了一种特殊的方式TnTinMn
Excel COM Object not gettingreleased

测试一下,找到你的方法:)。
切勿使用Process.Kill(). 除此之外,你不知道你要终止什么。

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

Visual Studio 工程团队:
Marshal.ReleaseComObject 被认为是危险的

Hans Passant 谈 COM 编组和垃圾收集
了解 .NET 中的垃圾收集

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