代码之家  ›  专栏  ›  技术社区  ›  user7393973

用VB.NET获取Excel运行实例

  •  1
  • user7393973  · 技术社区  · 6 年前

    我有以下工作代码 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 ( 1 , 2 )所以我换了线 ExcelApplication = ExcelObject.Application ExcelApplication = CType(ExcelObject, Excel.Application).Application 但这样做却引发了一个例外:

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

    我可以在不同的站点找到多个类似的引用,但是没有运气用 trial and error method .

    我的问题是如何 turn on option strict

    1 回复  |  直到 6 年前
        1
  •  3
  •   Jimi    6 年前

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

    以下方法使用 Console.WriteLine() 只是为了评估 (最终设置断点),某些对象的当前值。 这显然是多余的(必须在之前删除/注释掉 释放)。

    它创建了一个本地 List(Of Workbook) ,这是返回给调用方的:
    注意,每次创建互操作对象时,都会被mashalled并设置为nothing。
    为什么两者都是?调试时检查对象,您将看到。

    这个 Process.GetProcessesByName("EXCEL") 也是多余的。同样,仅用于计算返回的 Process 对象并检查其值。

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

    Visual Studio Version: 15.7.6 - 15.8.3
    .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
    

    返回的 (工作簿的)列表 包含活动对象。这些对象尚未被封送处理并且可以访问。

    你可以打电话给 FindOpenedWorkBooks() 方法如下:
    (一些值,如 WorkSheet.Columns.Count ,是毫无价值的。它们用于显示您访问每个 WorkSheet 每个ot中的值 Sheets 回来了,为了 WorkBooks 找到)

    这个 Excel.Range 为访问单元格值而创建的对象(此处为第一列标题):
    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
    

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

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

    同样,必须释放所有对象。 WorkBooks 必须是 .Close() 如果需要的话,保存它们的内容。这个 工作手册 集合必须是 .Close() d。
    使用 Excel.Application 主要对象 .Quit() 方法来通知操作的结束。
    .退出() 不会终止您创建的进程 .
    Marshal.FinalReleaseComObject(ExcelApplication) 用于完成它的发布。
    在这一点上 EXCEL 过程将结束。

    最后一条指令, Marshal.CleanupUnusedObjectsInCurrentContext()` ,是一种清理预防措施。
    也许没必要,但没什么大不了的:我们要离开这里。

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

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

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

    而不是:
    Dim MyWorkbook As Workbook = ExcelWorkbooks.Open([FilePath], False)

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

    因此,许多人喜欢实现不同的 图案 释放/释放互操作对象。

    你可以在这里看到很多种方式(主要是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() . 除此之外,你不知道你要终止什么。

    另外,关于托管/非托管代码中的COM封送处理的一些有趣的阅读资料 :

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

    汉斯·帕桑特谈编组和垃圾收集 :
    Understanding garbage collection in .NET

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

        2
  •  0
  •   user7393973    5 年前

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

    在大多数情况下,这已经足够了,但在打开多个Excel实例的特定实例中则不够。据我所知,只有在 Alt 启动Excel时的键,该键提示在新实例中启动Excel,或在某些程序中使用代码启动Excel。

    另一方面,问题中的代码确实有效,并解决了获取所有运行的Excel实例的问题。我唯一的问题是把它从后期绑定转换过来( Option Strict Off )提前装订( Option Strict On )这导致了一个直到现在我都找不到答案的错误。

    在一个 answer 在另一个问题中,我发现我必须替换参数 ppvObject 函数的 AccessibleObjectFromWindow 发件人:

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

    致:

    ByRef ppvObject As Excel.Window
    

    并更改变量的类型 ExcelObject 在声明中 Object Excel.Window (将其重命名为 ExcelWindow 在代码中)。