65.9K
CodeProject 正在变化。 阅读更多。
Home

自动化多个 Excel 实例

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (20投票s)

2016年11月25日

CPOL

11分钟阅读

viewsIcon

37482

downloadIcon

1441

用于访问任何正在运行的 Excel 实例的 .NET 库。

引言

本文展示了如何创建 Microsoft Excel 的自动化,该自动化可以处理多个 Excel 实例,或者在其他实例可能正在运行时处理单个 Excel 实例。 

为什么?

通过 COM 互操作库(Microsoft.Office.Interop.Excel)公开的标准 Excel API 不支持多个 Excel 实例。 (我认为 Visual Studio Tools for Office 的情况也类似,它在很大程度上是 COM 互操作库的包装器,但我很少使用 VSTO。) 在该对象模型中,Application 是顶级对象,它不属于任何集合。 Application 实例只能通过几种有限的方式轻松获取:

  1. 如果您对 Microsoft.Office.Interop.Excel 命名空间中的任何对象有引用,则可以使用其 Application 属性来获取对包含它的 Application 的引用。 但缺点是,您需要从 Application 内部的一个对象开始引用,而如果还没有 Application,则很难获取它。
  2. 您可以使用 Application 的构造函数来启动一个新的 Excel 实例。 但是,这不能用于访问现有实例,并且新实例将处于自动化模式,这会阻止加载加载项和其他功能,并且并非总是可取的。
  3. 加载项可以访问它们加载的 Application 实例,但这不适用于独立应用程序。
  4. System.Runtime.InteropServices.Marshal 类可用于获取“活动”实例,但这不能控制获取哪个实例。

什么?

本文的重点是一个名为 ExcelExtensions 的类库,它提供了扩展 Excel 对象模型以支持多个 Excel 实例的类。 还有一个名为 ExcelAppManager 的演示应用程序,用于展示一个用例以及如何从其他代码中消耗 ExcelExtensions。 在页面顶部,您可以找到已编译的 ExcelExtensions 程序集以及完整的 ExcelAppManager 解决方案的下载链接。 无论是库还是演示应用程序,都远非完美,因此请在我发现任何错误时通知我,并且在未经彻底测试的情况下,请勿在生产环境中使用此库

这是我对 2016 年 4 月撰写的关于此主题的文章的第二次迭代, 很大程度上归功于这篇匿名文章,以及 StackOverflow 上一些用户的提示,还有 CodeProject 上在我上一篇文章中发表评论的成员。 我将在文章中包含一些相关链接,并在文章末尾的“链接”部分重新发布链接。

本文其余部分分为以下几个主要部分:

  • 要求
  • 约定
  • ExcelExtensions 库
  • 演示 walkthrough
  • 演示实现
  • 闭幕词
  • 链接
  • 历史

要求

源代码是用 C# 6 编写的,因此需要一些熟悉这些语法特性,并且需要 Visual Studio 2015 才能打开解决方案。 该程序集以 .NET 4.0 为目标,因此可以被旧应用程序使用。 部分实现使用 Win32 API 调用,但消费者无需了解该 API。 对 Windows 进程和窗口句柄有基本了解将很有帮助。 没有复杂的 Excel 自动化使用,但您应该知道 Microsoft.Office.Interop.Excel.Application 类是什么。

我自己不是 Win32 API 方面的专家,但在这个项目中我学到了很多关于它的知识。 如果代码违反了任何处理 Win32 的最佳实践,请告诉我。

注意,此代码尚未在所有 Excel 或 Windows 版本上进行测试。 (请帮助我测试所有版本。) 我认为此代码可能特别容易出现基于不同 Excel 和 Windows 版本的问题。

测试环境

  • Windows 7 64 位,Excel 2016 32 位

约定

注意,以“注意,”开头的段落通常包含重要或意外的详细信息,例如上面提到的环境条件。

为了使代码示例简短,我删除了 ZIP 文件中解决方案中的许多注释。 ZIP 文件中的解决方案具有相当好的 XML 注释覆盖。

我已在很大程度上省略了 using 指令,但您可以假设以下内容始终有效:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using XL = Microsoft.Office.Interop.Excel;

我也省略了命名空间声明,但请假设代码示例中的所有类都位于 ExcelExtensions 命名空间中。

注意,命名空间别名 XL。 我将此命名空间中的类型称为“XL 类型”,并将这些类型的实例称为“XL 对象”。


ExcelExtensions 库

库实现包含四个类:

  1. NativeMethods - 这个内部类封装了对非托管 Windows API 的调用。 通常,您无需了解此类中所有内容的具体工作原理,但基本理解将解释此库的一些怪异之处。
  2. ApplicationExtensionMethods - 这个公共类具有 XL.Application 的扩展方法。
  3. ProcessExtensionMethods - 这个公共类具有 System.Diagnostics.Process 的扩展方法。
  4. Session - 这个公共类代表了特定 Windows 会话中所有正在运行的 Excel 实例的集合。 它将 XL 类型的层次结构向上提升一个级别,以便 Session 包含 ApplicationApplication 包含 WorkbookWorkbook 包含 Worksheet

NativeMethods

此类封装了对非托管 Windows API 的调用。 它有五个公共方法,每个方法负责类的独立职责,并使用独立的 Windows API 调用。

注意,此类实现并非无懈可击;传递无效参数可能导致 COMException 或返回意外的 null 值。

以下是类的概览,其中每个职责的区域都已折叠。

internal static class NativeMethods {

    private const string USER32 = "User32.dll";
    private const string OLEACC = "Oleacc.dll";

    #region Process ID from handle

    #region Excel window from handle

    #region Excel App from handle
   
    #region Window Z  

    #region Bring Process to front
}

以下是每个区域:

#region Process ID from handle

//Gets the process ID of the process that owns the window with the given handle.
public static int ProcessIdFromWindowHandle(int windowHandle) {
    if (windowHandle == 0) throw new ArgumentOutOfRangeException(
        "Window handle cannot be 0.", nameof(windowHandle));

    int processId;
    GetWindowThreadProcessId(windowHandle, out processId);
    return processId;
}

[DllImport(USER32)]
private static extern int GetWindowThreadProcessId(int hWnd, out int lpdwProcessId);

#endregion

 

#region Excel window from handle

//Gets the XL.Window object with the given handle.
public static XL.Window ExcelWindowFromHandle(int handle) {
    XL.Window result;
    AccessibleObjectFromWindow(handle, windowObjectId, windowInterfaceId, out result);
    return result;
}

[DllImport(OLEACC)]
private static extern int AccessibleObjectFromWindow(int hwnd, uint dwObjectID, 
    byte[] riid, out XL.Window ppvObject);

private const uint windowObjectId = 0xFFFFFFF0;
private static byte[] windowInterfaceId = \
    new Guid("{00020400-0000-0000-C000-000000000046}").ToByteArray();

#endregion

 

#region Excel App from handle

//Gets an XL.Application object for the instances with the given main window handle.
public static XL.Application AppFromMainWindowHandle(int mainWindowHandle) {
    if (mainWindowHandle == 0) throw new ArgumentOutOfRangeException("Window handle cannot be 0.", nameof(mainWindowHandle));

    int childHandle = 0;
    EnumChildWindows(mainWindowHandle, NextChildWindowHandle, ref childHandle);

    var win = ExcelWindowFromHandle(childHandle);

    return win?.Application;
}

[DllImport(USER32)]
private static extern bool EnumChildWindows(int hWndParent, EnumChildCallback lpEnumFunc, 
    ref int lParam);

private delegate bool EnumChildCallback(int hwnd, ref int lParam);

private static bool NextChildWindowHandle(int currentChildHandle, ref int nextChildHandle) {
    const string excelClassName = "EXCEL7";

    var result = true;

    var className = GetClassName(currentChildHandle);
    if (className == excelClassName) {
        nextChildHandle = currentChildHandle;
        result = false;
    }
    return result;
}

#region Get class name

//Gets the name of the COM class to which the specified window belongs.
private static string GetClassName(int windowHandle) {
    var buffer = new StringBuilder(128);
    GetClassName(windowHandle, buffer, 128);
    return buffer.ToString();
}

[DllImport(USER32, CharSet = CharSet.Unicode)]
private static extern int GetClassName(int hWnd, StringBuilder lpClassName, int nMaxCount);

#endregion

#endregion

 

#region Window Z

//Gets the depth of the window with the given handle.
//This will return a negative number for invalid handles or invisible windows.
public static int GetWindowZ(int windowHandle) {
    var z = 0;
    //Count all windows above the starting window
    for (var h = new IntPtr(windowHandle);
        h != IntPtr.Zero;
        h = GetWindow(h, GW_HWNDPREV)) {

        z++;
    }
    return z;
}

[DllImport(USER32)]
private static extern IntPtr GetWindow(IntPtr hWnd, uint uCmd);

private const int GW_HWNDPREV = 3;
#endregion

 

#region Bring Process to front

//Brings the main window of the given process to the front of all windows.
public static bool BringToFront(Process process) {
    if (process == null) throw new ArgumentNullException(nameof(process));

    var handle = process.MainWindowHandle;
    if (handle == IntPtr.Zero) return false;
    try {
        SetForegroundWindow(handle);
        return true;
    }
    catch { return false; }
}

[DllImport(USER32)]
private static extern bool SetForegroundWindow(IntPtr hWnd);

#endregion

ProcessExtensionMethods

这是一个相当简单的静态类,包含 System.Diagnostics.Process 的扩展方法,以及一些 IEnumerable<Process> 的扩展方法。 它严重依赖 NativeMethods 来完成其脏活累活。

注意,如果使用了无效进程,或者 Excel 实例当前正忙(例如,当打开对话框窗口时),AsExcelApp 方法可能会引发 COMException 或返回 null

public static class ProcessExtensionMethods {
    
    //Gets the depth of the main window of the process.
    public static int MainWindowZ(this Process process) {
        if (process == null) throw new ArgumentNullException(nameof(process));

        return NativeMethods.GetWindowZ(process.MainWindowHandle.ToInt32());
    }

    //Gets the input sequence, with processes ordered by the depth of their main window.
    public static IEnumerable<Process> OrderByZ(this IEnumerable<Process> processes) {
        if (processes == null) throw new ArgumentNullException(nameof(processes));

        return processes
            .Select(p => new {
                Process = p,
                Z = MainWindowZ(p)
            })
            .Where(x => x.Z > 0) //Filter hidden instances
            .OrderBy(x => x.Z) //Sort by z value
            .Select(x => x.Process);
    }

    //Gets the process from the input sequence with the main window having the lowest Z value.
    public static Process TopMost(this IEnumerable<Process> processes) {
        if (processes == null) throw new ArgumentNullException(nameof(processes));

        return OrderByZ(processes).FirstOrDefault();
    }
    
    //Converts a Process to an Xl.Application.
    public static XL.Application AsExcelApp(this Process process) {
        if (process == null) throw new ArgumentNullException(nameof(process));

        var handle = process.MainWindowHandle.ToInt32();
        return NativeMethods.AppFromMainWindowHandle(handle);
    }
    
    //Determines if the process is currently visible.
    public static bool IsVisible(this Process process) {
        if (process == null) throw new ArgumentNullException(nameof(process));

        return process.MainWindowHandle.ToInt32() != 0;
    }
}

ApplicationExtensionMethods

这是另一个简单的扩展方法类,扩展了 XL.Application

public static class ApplicationExtensionMethods {

    //Gets the Session containing the given Application.
    public static Session Session(this XL.Application app) {
        if (app == null) throw new ArgumentNullException(nameof(app));

        using (var process = app.AsProcess()) {
            return new Session(process.SessionId);
        }
    }

    //Determines if the given Application is the most recently active one.
    public static bool IsActive(this XL.Application app) {
        if (app == null) throw new ArgumentNullException(nameof(app));

        return Equals(app, app.Session().TopMost);
    }

    //Activates the given Application.
    public static void Activate(this XL.Application app) {
        if (app == null) throw new ArgumentNullException(nameof(app));

        using (var process = app.AsProcess()) {
            NativeMethods.BringToFront(process);
        }
    }

    //Determines if the given Application is visible and accesible to automation.
    public static bool IsVisible(this XL.Application app) {
        if (app == null) throw new ArgumentNullException(nameof(app));

        try {
            using (var process = app.AsProcess()) {
                return app.Visible && process.IsVisible();
            }
        }
        catch (COMException x)
        when (x.Message.StartsWith("The message filter indicated that the application is busy.")
            || x.Message.StartsWith("Call was rejected by callee.")) {
            //This means the application is in a state that does not permit COM automation.
            //Often, this is due to a dialog window or right-click context menu being open.
            return false;
        }
    }

    //Converts an Application to a Process.
    public static Process AsProcess(this XL.Application app) {
        if (app == null) throw new ArgumentNullException(nameof(app));

        var mainWindowHandle = app.Hwnd;
        var processId = NativeMethods.ProcessIdFromWindowHandle(mainWindowHandle);
        return Process.GetProcessById(processId);
    }
}

Session

这是一个实例类,代表特定 Windows 会话中所有正在运行的 Excel 实例的集合。 它将 XL 类型层次结构向上提升一个级别,以便 Session 包含 ApplicationApplication 包含 WorkbookWorkbook 包含 Worksheet

public class Session {

    //Gets an instance representing the current Windows session.
    public static Session Current => new Session(Process.GetCurrentProcess().SessionId);

    //Creates a new instance for the given session.
    public Session(int sessionId) {
        SessionId = sessionId;
    }

    public int SessionId { get; }

    //Gets the sequence of currently running processes in this session named "Excel".
    private IEnumerable<Process> Processes =>
        Process.GetProcessesByName("EXCEL")
        .Where(p => p.SessionId == this.SessionId);

    //Gets the Application corresponding to a Process, or null if that fails.
    private static XL.Application TryGetApp(Process process) {
        try {
            return process.AsExcelApp();
        }
        catch {
            return null;
        }
    }

    //Gets the IDs for current Excel processes.
    public IEnumerable<int> ProcessIds =>
        Processes
        .Select(p => p.Id)
        .ToArray();

    //Gets the IDs for current Excel processes that are reachable by automation.
    public IEnumerable<int> ReachableProcessIds =>
        AppsImpl.Select(a => a.AsProcess().Id).ToArray();

    //Gets the IDs for the current Excel processes that are not reachable by automation.
    public IEnumerable<int> UnreachableProcessIds =>
        ProcessIds
        .Except(ReachableProcessIds)
        .ToArray();

    //Gets an untyped sequence of running Applications.
    public IEnumerable Apps => AppsImpl;

    //Gets a strongly-typed sequence of running Applications.
    private IEnumerable<XL.Application> AppsImpl =>
        Processes
        .Select(TryGetApp)
        .Where(a => a != null && a.AsProcess().IsVisible())
        .ToArray();

    //Gets the Application with the topmost window, or null if there are none.
    public XL.Application TopMost {
        get {
            var dict = AppsImpl.ToDictionary(
                keySelector: a => a.AsProcess(),
                elementSelector: a => a);

            var topProcess = dict.Keys.TopMost();

            if (topProcess == null) {
                return null;
            }
            else {
                try {
                    return dict[topProcess];
                }
                catch {
                    return null;
                }
            }
        }
    }

    //Gets the default Application that double-clicked files will open in, 
    //or null if there are none.
    public XL.Application PrimaryInstance {
        get {
            try {
                return (XL.Application)Marshal.GetActiveObject("Excel.Application");
            }
            catch (COMException x)
            when (x.Message.StartsWith("Operation unavailable")) {
                Debug.WriteLine("Session: Primary Excel instance unavailable.");
                return null;
            }
        }
    }
}

注意,公共 Apps 属性返回 IEnumerable,而不是 IEnumerable<XL.Application>。 这是因为 XL.Application(像所有 XL 类型一样)是一个嵌入式 COM 互操作类型,因此不能用作公共成员的泛型类型参数。 但是,在消耗代码中将此集合的元素安全地转换为 IEnumerable<XL.Application> 是完全安全的,如下所示:

XL.Application[] myApps = mySession.Apps.OfType<XL.Application>().ToArray();

有关嵌入互操作类型的更多信息,请参阅此 MSDN 文章

注意,TopMostPrimaryInstance 之间的区别。TopMost 是拥有最前面窗口的实例,这很可能是用户最后点击的那个;处理多个实例时,这可能会频繁更改。PrimaryInstance 是双击文件将打开的默认实例;这通常在现有 PrimaryInstance 关闭之前不会改变。


演示 walkthrough

ExcelAppManager 是一个 WinForms 应用程序,允许您监视 Excel 实例,还可以更新任何 Excel 实例中任何工作表的 A1 单元格的值。 窗体左上角有一些用于启动或清理进程的按钮,左下角有用于更新 A1 单元格的控件,右侧显示了当前正在运行的 Excel 实例的类似 JSON 的树状视图。 首先,我们将介绍右侧的显示,然后更新单元格值。

监视 Excel 实例

这是应用程序在没有 Excel 实例运行时的情况。

 

单击“启动 Excel 实例”来启动一个 Excel 实例,并观察右侧的表单相应更新。

 

新的 Excel 2016 实例

 

创建一个新的工作簿,窗体将相应更新。

 

打开多个工作簿通常会将它们的详细信息添加到表单中。 请注意,多个工作簿在同一个 Excel 实例中正常打开。

 

再次单击“启动 Excel 实例”以启动另一个进程。

 

注意,如果在 Excel 中打开了对话框窗口、上下文菜单或鼠标悬停工具提示,则应用程序将无法通过自动化访问,并会暂时从表单右侧的显示中消失。


更新单元格值

要更新单元格值,请先选中“暂停更新显示”复选框。 这可以防止组合框中选定的值不断重置自身,但也会冻结表单右侧的 JSON 显示。 如果您在暂停更新时打开或关闭新的 Excel 实例、工作簿或工作表,则需要取消暂停更新以刷新组合框中的项目。

 

“ProcessID”组合框允许您按进程 ID 选择一个正在运行的 Excel 实例。 此组合框将始终有项目。

 

“Book Name”组合框允许您从上面选定的 Excel 实例中按名称选择一个工作簿。 

 

“Sheet Name”组合框允许您从上面选定的书中按名称选择一个工作表。 选择工作表后,其 A1 单元格的值将显示在下面的只读文本框中。

 

单击“更改单元格值”会将选定工作表的 A1 单元格的值更新为“New A1 Value”文本框中的值。


演示实现

我不会在这里发布应用程序的所有代码,但我将描述其基本结构,并提供一些最有趣部分的示例代码。 该应用程序有四个类:

  1. MainForm - 这是 UI,包含应用程序的核心逻辑。 这是我将进一步研究的唯一一个类。
  2. Program - 这是标准的 WinForms 应用程序入口点;它只创建一个 MainForm 实例。
  3. AppSerializer - 此类具有以类似 JSON 的格式序列化 XL 对象的方法,MainForm 使用这些方法来显示当前会话的状态。
  4. ProcessLauncher - 此类具有启动和处理进程的方法,这些方法由 MainForm 的按钮单击事件调用,以便可以轻松观察多个 Excel 实例。

MainForm

此类是 UI,并且包含一些核心应用程序逻辑。 由于此类中有许多内容,因此我已将源代码分组到各个区域。 第一个代码示例显示了类构造函数及其字段,但在末尾有三个折叠的区域。

using System.Drawing;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

public partial class MainForm : Form {

    public MainForm() {
        InitializeComponent();

        launcher = new ProcessLauncher();
        session = Session.Current;

        //Start a background task to update the "Session" textbox.
        var task = new Task((Action)UpdateDisplayLoop);
        task.Start();
    }

    #region Fields

    //Used to launch and cleanup processes
    private readonly ProcessLauncher launcher;

    //Used to get access to currently running Excel instances.
    private readonly Session session;

    //Determines whether the UI data will refresh periodically or not.
    //This must be set to false to allow the user to make drop-down selections.
    private bool IsPaused {
        get { return isPaused; }
        set {
            isPaused = value;
            cmb_ProcessID.Enabled = value;
            cmb_BookName.Enabled = value;
            cmb_SheetName.Enabled = value;
            txt_Session.BackColor = value ? Color.LightGray : Color.White;
        }
    }
    private bool isPaused = false;

    #endregion


    #region Update loop

    #region Control event handlers

    #region Update selection options

}

如您所见,构造函数获取一个 ProcessLauncher 和一个 Session 实例,并将它们存储在其字段中。 ProcessLauncher 将用于启动新的 Excel 实例,或清理从此应用程序启动的任何实例。 Session 可以被查询以获取当前正在运行的 Excel 实例的 XL.Application 对象。 构造函数还启动一个后台 Task,该任务会不断更新 UI,我们稍后将对其进行检查。 最后一个字段 isPaused 确定 UI 更新是暂停(true)还是启用(false);IsPaused 属性还会更新几个控件的显示。


更新循环非常简单。 在一个无限循环中,它检查更新是否被暂停,如果没有暂停,则将 Session 的字符串表示形式写入文本框,然后等待 100 毫秒。

#region Update loop

//Updates the display, then waits. Repeats forever.
private void UpdateDisplayLoop() {
    while (true) {
        if (!isPaused) {
            Invoke((MethodInvoker)UpdateDisplay);
        }
        Thread.Sleep(100);
    }
}

private void UpdateDisplay() {
    var sessionText = ExcelSerializer.SerializeSession(this.session);
    txt_Session.Text = sessionText;
    ResetSelections();
}

#endregion

控件事件处理程序部分非常直接。 “暂停更新”复选框链接到 IsPaused 属性。 表单左上角的进程管理按钮使用 ProcessLauncher。 表单左下角的控件调用定义在最后一个区域中的函数。

注意,以下缩写用于控件名称:

  • chk = CheckBox
  • btn = Button
  • cmb = ComboBox
#region Control event handlers

private void chk_PauseUpdates_CheckedChanged(object sender, EventArgs e) {
    IsPaused = chk_PauseUpdates.Checked;
}


private void btn_LaunchTaskMgr_Click(object sender, EventArgs e) =>
    this.launcher.LaunchTaskManager();

private void btn_LaunchExcel_Click(object sender, EventArgs e) =>
    this.launcher.LaunchExcelInstance();

private void btn_CleanUpExcel_Click(object sender, EventArgs e) =>
    this.launcher.Dispose();


private void cmb_ProcessID_SelectedIndexChanged(object sender, EventArgs e) =>
    TryUpdateSelectableBookNames();

private void cmb_BookName_SelectedIndexChanged(object sender, EventArgs e) =>
    TryUpdateSelectableSheetNames();

private void cmb_SheetName_SelectedIndexChanged(object sender, EventArgs e) =>
    TryUpdateOldCellValue();

private void btn_ChangeCell_Click(object sender, EventArgs e) =>
    TryChangeCellValue();

#endregion

最后一个区域最大,但没有一个方法过于复杂。

ResetSelections 清除表单左下角控件的内容,并作为更新循环的一部分被调用。 

TryXxx 方法由控件事件处理程序调用,每个方法都尝试更新特定控件的内容或执行任务,但如果无法执行,则刷新显示。 例如,如果用户在 Book1 已关闭后选择“Book1”,则显示将刷新,因为该选择无效。

GetSelectedXxx 方法分别尝试返回一个对应于特定控件中选择的 XL 对象。

UpdateXxx 方法分别用给定 XL 对象的数据替换特定控件的内容。

#region Update selection options

private void ResetSelections() {
    UpdateSelectableProcessIds(this.session);
    cmb_BookName.Items.Clear();
    cmb_SheetName.Items.Clear();
    txt_OldValue.Text = "";
}

private void TryUpdateSelectableBookNames() {
    var app = GetSelectedApp();
    if (app == null) { UpdateDisplay(); }
    else { UpdateSelectableBookNames(app); }
}

private void TryUpdateSelectableSheetNames() {
    var book = GetSelectedBook();
    if (book == null) { UpdateDisplay(); }
    else { UpdateSelectableSheetNames(book); }
}

private void TryUpdateOldCellValue() {
    var sheet = GetSelectedSheet();
    if (sheet == null) { UpdateDisplay(); }
    else { UpdateOldCellValue(sheet); }
}

private void TryChangeCellValue() {
    var sheet = GetSelectedSheet();
    if (sheet == null) {
        UpdateDisplay();
    }
    else {
        xlRange cell = sheet.Cells[1, 1];
        var text = txt_NewValue.Text;
        cell.Value = text;
    }
}

#region Get current selections

private XL.Application GetSelectedApp() {
    XL.Application result = null;
    var text = cmb_ProcessID.Text;
    if (text.Length > 0) {
        var processId = int.Parse(text);
        result = session.Apps.OfType<XL.Application>()
            .FirstOrDefault(a => a.AsProcess().Id == processId);
    }
    return result;
}

private XL.Workbook GetSelectedBook() {
    XL.Workbook result = null;
    var app = GetSelectedApp();
    if (app != null) {
        var text = cmb_BookName.Text;
        if (text.Length > 0) {
            try {
                result = app.Workbooks[text];
            }
            catch {
                result = null;
            }
        }
    }
    return result;
}

private XL.Worksheet GetSelectedSheet() {
    XL.Worksheet result = null;
    var book = GetSelectedBook();
    if (book != null) {
        var text = cmb_SheetName.Text;
        if (text.Length > 0) {
            try {
                result = book.Sheets[text];
            }
            catch {
                result = null;
            }
        }
    }
    return result;
}

#endregion

#region Update displayed data

private void UpdateSelectableProcessIds(Session session) {
    cmb_ProcessID.Items.Clear();
    foreach (var id in session.ReachableProcessIds) {
        cmb_ProcessID.Items.Add(id);
    }
}

private void UpdateSelectableBookNames(XL.Application app) {
    cmb_BookName.Items.Clear();
    foreach (XL.Workbook book in app.Workbooks) {
        cmb_BookName.Items.Add(book.Name);
    }
}

private void UpdateSelectableSheetNames(XL.Workbook book) {
    cmb_SheetName.Items.Clear();
    foreach (XL.Worksheet sheet in book.Sheets) {
        cmb_SheetName.Items.Add(sheet.Name);
    }
}

private void UpdateOldCellValue(XL.Worksheet sheet) {
    XL.Range cell = sheet.Cells[1, 1];
    var text = cell.Text;
    txt_OldValue.Text = text;
}

#endregion

#endregion

闭幕词

希望这篇文章能有所帮助。 请记住,此库中仍存在 bug,并且并非所有 Windows 和 Excel 版本都经过测试。 如果您发现任何 bug,或对使用方法或设计选择有任何疑问,请告诉我。

我目前还在开发一个名为ExcelBrowser 的 WPF 应用程序,该应用程序使用了本文概述的技术。 它将充当当前用户 Windows 会话中所有正在运行的 Excel 实例的“对象浏览器”窗口。 此处看到的 ExcelExtensions 库实际上是从其代码库的一部分派生出来的,因此实现方式略有不同。


链接


历史

  • 发布日期 2016/11/23
  • 上一篇文章发布日期 2016/02/23
© . All rights reserved.