0

为这篇文章的长度道歉,但我认为背景对于传达我想要实现的目标是必要的。

我的任务是更新旧的 Excel UDF 插件(它以前使用来自 Steve Dalton 的 JNI 书中的 api 代码以及几乎所有其他可以想象的技术)。这些函数对 Excel 的计算模型非常不友好——这些函数需要几个范围并将数据写回它们(在另一个线程中),同时允许用户编辑这些数据(然后将其保存并上传到远程服务器)。所有这些都使得加载速度非常慢,但为用户提供了必要的灵活性,可以根据需要更改数据。

我已经将它迁移到 C#(它通过 WSDL 从远程 Java 服务器获取数据),但我发现大多数函数被调用了 50 多次,而且它的运行速度比原始插件慢得多(它是一个自动化插件,使用Extensibility.IDTExtensibility2 - 所以这里没有可用的 VSTO 技巧)。

绝望之下,我决定尝试将 UDF 的大片重写为数组函数并且不接受输入(Excel 会抱怨覆盖数组)——显然这现在快了几个数量级,但比用户缺少关键要求可以修改输出数据。

意识到 Excel 没有提供任何编辑提交前验证回调事件(我玩过 Worksheet.OnEntry 并添加了一个 VBComponent 但在覆盖数组或列表数据验证的错误之前没有触发它)。

我认为实现一个全局键盘钩子会很简单,所以开始编写一些 Windows 表单来拦截编辑(只是一个用于单个表单条目的 TextBox 和一个用于具有列表数据验证的单元格的 ComboBox),并将数据从剪贴板复制到选定的范围。

目前,所有这些都是由定制的上下文菜单条目(或双击)驱动的,用户不会接受 - 我必须至少能够拦截 F2、Ctl+V 和在活动单元格上直接键入. 但我不知道如何在自动化插件中注册全局键盘挂钩。

所以我的问题是;是否可以拦截每次编辑尝试并提供我自己的处理?或者如果失败了,我如何注册一个全局键盘钩子来拦截 F2、Ctl+V 并在活动单元格上直接键入?

我已经尝试过在 WPF / C# 中使用全局键盘钩子 (WH_KEYBOARD_LL)找到的钩子,但无法让 App.xaml + App.xaml.cs 在这种情况下工作(这是我第一次遇到 C# 和 Windows 编程) ,所以可能有人只需要在 App.xaml + App.xaml.cs 配置()中启发我。

请注意; 这不是 VSTO 插件,它是使用 Extensibility.IDTExtensibility2 实现的。

更新编辑: @TimWilliams 和@CharlesWilliams 询问为什么我以前的版本(在它的函数参数上进行了读写)有如此多的重复调用。

所有的函数都有一个强制性的 id 键参数,大多数也需要一个日期或日期范围,以下是发生的事情(在相当大的工作簿中,大约 30 张图表):

  1. 首次加载工作簿时,函数调用 all fire 并使用陈旧(以前保存的)值,但这些被忽略,因为每个函数中 C# 的第一行是对支持模型的(快速)测试,以查看模型是否已加载,没有函数参数被检查/解组。

  2. 用户选择通过 web 服务或先前序列化的文件加载模型。

  3. 视觉更新被禁用,设置表按顺序填充了一些数据;一些关键日期(日期的开始和结束 - 不同工作表上的各种日期范围用 EMONTH +/- 12 计算),一些其他静态数据(作者/编辑名称等),最后是强制性的关键 id(标识模型数据)

  4. 现在自动化插件中的每个函数的方法都有 id 键,因此如果找到数据,则返回它,否则使用函数参数的默认值。(注意:模型维护函数请求的对象字段的副本,因此它知道已输出什么。对于进一步的调用,如果缓存层中存在数据,则更新它(原始模型数据或先前模型数据的手动克隆缓存值)与传入的函数参数 - 缓存层和模型数据的差异稍后上传到 Web 服务) - 没有很好地解释“缓存”实际上是与模型数据相同的数据结构的包装器

  5. 如果返回数据(表示第一次加载),则将其放入由工作线程提供服务的阻塞队列,该工作线程将数据写回指定的范围。

糟糕的性能似乎源于非常长的依赖函数链(混淆 Excel?),以及函数似乎在它们的依赖项之前被调用的事实,例如

给定 DATE_RANGE 是链 A1-An,An=EMONTH(An-1,12) 其中 A1 是来自已填充的设置页面的常量 LAST_DATE

然后 fn(MODEL_ID, DATE_RANGE) 在命名单元格 MODEL_ID 填充后调用,但 DATE_RANGE 的值不正确,并且在每个 EMONTH 完成时重复调用 fn,并且函数方法尝试将范围转换为日期(如果日期无效,则提前返回)。同时,工作线程开始抛出应用程序繁忙异常(因此将范围写入重新排队并休眠 250 毫秒的任意时间)。最终,争论平息了,但您将有机会先制作并开始喝咖啡(甚至可能还会研磨咖啡豆)。

编写了这个可怕的代码后,我考虑将日期写入设置表,然后在更新 MODEL_ID 之前等待计算停止 - 这将在某种程度上减少函数调用的数量。然而,只截取编辑,将这些更新保存在模型中并将相应的范围标记为脏似乎更干净。

我认为可用的选项是;

  • 在编辑拦截版本中,尝试使用 vb 钩子 OnKey 调用每个可能的 ASCII 函数以回调到参数化的 C# 命令(VB 代码至少可以在循环中生成)
  • 尝试将编辑拦截版本作为 VSTO 插件(这应该给我键绑定)
  • 使用 ExcelDNA - (以前的)读写范围参数版本看起来很诱人(这可能证明有足够的性能(这可能表明我的 Excel 处理代码中存在逻辑错误)。

(再次为篇幅和不够清晰表示歉意)

4

2 回答 2

1

我无法帮助您使用全局键盘挂钩,但您确实应该查看 Excel DNA 或 Addin Express 以显着提高 C# UDF 的性能(它们将 .NET 与 XLL C API 接口,这比 c# 自动化快得多) .
Excel DNA 和 Addin Express 在其支持论坛中也有讨论如何将数据从 UDF 重写回其他范围的主题。IIRC Excel DNA 讨论了单独的线程方法,Addin Express 讨论了使用命令等效类型的 UDF 来触发隐藏的 XLM 函数

而且我个人认为,要让您的全局键盘挂钩方法在所有情况下都不引人注目且有效地工作是非常困难的(打开多个工作簿、VBA、DDE 等)。

于 2011-10-24T18:26:43.363 回答
1

您应该能够解决您的计算链问题,因为它听起来像是一组离散的连续步骤。
如果使用 C++/XLL,您可以将函数参数设为 P 类型,以确保它们在传递给 UDF 之前由 Excel 计算。我认为如果将参数定义为 Object 以外的任何参数,则 Ex cel DNA/addin Express 应该具有相同的效果。

Excel 计算 LIFO 序列中的单元格,该序列由上一个最终计算序列和任何已输入/更改的单元格设置:因此最后更改的公式首先计算。
所以你应该在你的 DATE_RANGE 链中以相反的顺序输入公式(链中的最后一个)
大概您已经在此过程开始时切换到手动计算模式。所以它可能就像写出设置表和日期一样简单,然后强制计算 (Application.calculate) 然后更新 MODEL_ID,然后强制进行另一个计算。

当然,使用 Excel DNA,每次函数调用的开销无论如何都会低得多。
请参阅http://fastexcel.wordpress.com/2011/07/07/excel-udf-technology-choices-snakes-ladders-with-vba-vb6-net-c-com-xll-interop/

于 2011-10-25T08:00:38.693 回答