简体   繁体   English

用于(Excel)自动化插件的全局键盘挂钩(不是VSTO)

[英]Global Keyboard hook for (Excel) automation addin (not VSTO)

Apologies for the length of this post, but I think the background is necessary to convey what I'm trying to achieve. 对这篇文章的篇幅表示道歉,但我认为背景是传达我想要实现的目标的必要条件。

I've been tasked with updating an old Excel UDF addin (it was previously using the api code from Steve Dalton's book with JNI and just about every other technology imaginable). 我的任务是更新一个旧的Excel UDF插件(之前使用的是Steve Dalton与JNI的书中的api代码以及几乎所有其他可以想象的技术)。 The functions were very unsympathetic to Excel's calculation model – these functions took several ranges and wrote data back to them (in another thread) while at the same time allowing users to edit this data (which was then saved and uploaded to a remote server). 这些函数对Excel的计算模型非常不同情 - 这些函数需要几个范围并将数据写回(在另一个线程中),同时允许用户编辑这些数据(然后保存并上传到远程服务器)。 All this made it abysmally slow to load, but gave users the necessary flexibility to change the data as needed. 所有这些使加载速度极慢,但为用户提供了根据需要更改数据的必要灵活性。

I've migrated it to C# (it takes the data from a remote Java server via WSDL), but I found that most functions were being called 50+ times and it ran far, far slower than the original addin (it's an automation addin using Extensibility.IDTExtensibility2 – so no VSTO tricks available here). 我已将它迁移到C#(它通过WSDL从远程Java服务器获取数据),但我发现大多数函数被调用了50多次并且它运行得远远远远慢于原始插件(它是一个自动化插件使用Extensibility.IDTExtensibility2 - 所以这里没有可用的VSTO技巧)。

In a fit of desperation, I decided to try rewrite the swathes of UDFs to be array functions and not accept input (Excel would complain about overwriting an array) – obviously this is now several orders of magnitude faster, but missing the key requirement than users be able to modify the output data. 绝望之下,我决定尝试将UDF重写为数组函数而不接受输入(Excel会抱怨覆盖数组) - 显然现在这个数字要快几个数量级,但是缺少关键要求而不是用户能够修改输出数据。

Realising Excel didn't provide any before-edit-is-committed validation callback events (I'd played around with Worksheet.OnEntry and added a VBComponent but it isn't triggered before the error about overwriting an array or list data validation). 实现Excel没有提供任何在编辑之前提交的验证回调事件(我使用了Worksheet.OnEntry并添加了一个VBComponent,但是在覆盖数组或列表数据验证的错误之前没有触发它)。

I assumed it would be simple enough to implement a global keyboard hook so started writing some windows forms to intercept edits (just a TextBox for single form entries and a ComboBox for cells with list data validation) and also got it copying data from the clipboard into the selected range. 我认为实现一个全局键盘钩子很简单,所以开始编写一些窗口表格来拦截编辑(只有一个TextBox用于单个表单条目,一个ComboBox用于带有列表数据验证的单元格),并且还将它从剪贴板复制到选定的范围。

Currently all of this is driven from bespoke context-menu entries (or double click), which will not be accepted by the users – I must, as a minimum, be able to intercept F2, Ctl+V and direct typing on an active cell. 目前所有这些都来自定制的上下文菜单条目(或双击),用户不会接受 - 我必须至少能够截取F2,Ctl + V并直接在活动单元格上键入。 But I have no idea how to register a global keyboard hook within an automation addin. 但我不知道如何在自动化插件中注册全局键盘钩子。

So my question is; 所以我的问题是; Is it possible to intercept every edit attempt and provide my own handling? 是否有可能拦截每次编辑尝试并提供我自己的处理? Or failing that, how can I register a global keyboard hook to intercept F2, Ctl+V and direct typing on an active cell? 或者失败了,如何注册全局键盘钩子来拦截F2,Ctl + V并直接在活动单元格上键入?

I've tried the hook found here Using global keyboard hook (WH_KEYBOARD_LL) in WPF / C# but cannot get the App.xaml + App.xaml.cs working in this context (this is my first encounter with C# and windows programming in general), so may be someone just needs to enlighten me wrt the App.xaml + App.xaml.cs configuration (). 我已经尝试了在WPF / C#中使用全局键盘钩子(WH_KEYBOARD_LL)找到的钩子,但无法获得在此上下文中工作的App.xaml + App.xaml.cs(这是我第一次遇到C#和Windows编程) ,所以可能有人只需要通过App.xaml + App.xaml.cs配置()来启发我。

Please note; 请注意; this is not an VSTO addin, it's implemented using Extensibility.IDTExtensibility2. 这不是VSTO插件,它是使用Extensibility.IDTExtensibility2实现的。

UPDATE EDIT: @TimWilliams and @CharlesWilliams enquired why my previous version , that did read-write on it's functions' arguments, had so many repeat invocations. 更新编辑: @TimWilliams和@CharlesWilliams询问为什么我之前的版本 ,它对函数的参数进行了读写,有如此多的重复调用。

All the functions have a mandatory id key parameter, most also take a date or date range, the following is what occurred (in quite a large workbook ~30 sheets with graphs): 所有函数都有一个强制的id键参数,大多数也采用日期或日期范围,以下是发生的事情(在相当大的工作簿~30张图表中):

  1. When the workbook is first loaded the function calls all fire with stale (previously saved) values but these are disregarded as the first line of C# in each function is a (quick) test against the backing model to see whether the model has been loaded, no function arguments are inspected/unmarshalled. 首次加载工作簿时,该函数会使用过时(先前保存的)值调用所有fire,但这些值被忽略,因为每个函数中的第一行C#是针对支持模型的(快速)测试,以查看模型是否已加载,没有检查/解组函数参数。

  2. The user chooses to load a model via webservices or previously serialized file. 用户选择通过webservices或以前的序列化文件加载模型。

  3. Visual updates are disabled and a setup sheet is populated with some data, in order; 禁用可视更新,并按顺序使用一些数据填充设置表; some key dates (beginning and end of dates - various date ranges on different sheets are calculated with EMONTH +/- 12), some other static data (author/editor name etc), and finally the mandatory key id (that identifies the model data) 一些关键日期(日期的开始和结束 - 不同工作表上的各种日期范围用EMONTH +/- 12计算),一些其他静态数据(作者/编辑者名称等),最后是强制性密钥id(用于标识模型数据) )

  4. Now each function's method in the automation addin has the id key so if data is found it's returned otherwise default values from function parameters are used. 现在自动化插件中的每个函数的方法都有id键,所以如果找到数据则返回,否则使用函数参数的默认值。 (Note: The model maintains a copy of the objects' fields requested by functions so it knows what has been output. For further invocations, if data is present in the cache layer it is updated (hand rolled clone of either original model data or previous cache value) with the incoming function parameters - the diff of cache layer and model data is later uploaded to a webservice) - Not well explained "cache" is actually a wrapper around same data structure as the model data (注意:模型维护了函数请求的对象字段的副本,因此它知道输出了什么。对于进一步的调用,如果缓存层中存在数据,则更新(原始模型数据或之前的手动克隆)缓存值)与传入的函数参数 - 缓存层的差异和模型数据稍后上传到web服务) - 没有很好地解释“缓存”实际上是与模型数据相同的数据结构的包装器

  5. If data is returned (indicating first load), it is put onto a blocking queue that is served by a worker thread that writes the data back to the ranges specified. 如果返回数据(表示首次加载),则将其放入由工作线程提供服务的阻塞队列,该工作线程将数据写回指定的范围。

The abysmal performance seems to stem from very long chains of dependant functions (confusing Excel?), and the fact that the the functions seem to be invoked before their dependencies, eg 糟糕的表现似乎源于非常长的依赖函数链(令人困惑的Excel?),以及函数似乎在它们的依赖之前被调用的事实,例如

Given DATE_RANGE is a chain A1-An, of An=EMONTH(An-1,12) where A1 is a constant LAST_DATE from the setup page that has already been populated 给定DATE_RANGE是链A1-An,An = EMONTH(An-1,12),其中A1是已经填充的设置页面的常量LAST_DATE

Then fn(MODEL_ID, DATE_RANGE) is invoked once the named cell MODEL_ID is populated, but DATE_RANGE has incorrect values, and fn is called repeatedly as each EMONTH completes, and the function methods try to convert ranges to dates (if invalid dates then returning early). 然后在填充命名单元格MODEL_ID时调用fn(MODEL_ID,DATE_RANGE),但DATE_RANGE具有不正确的值,并且在每个EMONTH完成时重复调用fn,并且函数方法尝试将范围转换为日期(如果无效日期则返回到早期)。 Meanwhile the worker thread starts throwing application busy exceptions (and so re-queuing the ranges writes and sleeping for arbitrary period of 250ms). 同时,工作线程开始抛出应用程序繁忙异常(因此重新排队范围写入和休眠250ms的任意时间段)。 Eventually the contention subsides, but you'll have a chance to make and starting drinking a coffee first (probably even grind the beans too). 最终争论消退了,但你有机会先制作并开始喝咖啡(甚至可能会磨豆子)。

Having written this horrid code, I considered just writing dates out to the setup sheet and then waiting for calculation to stop before updating the MODEL_ID - this would go some way to reducing the number of functions calls. 写完这个可怕的代码后,我考虑将日期写入设置表,然后在更新MODEL_ID之前等待计算停止 - 这将减少函数调用的数量。 However intercepting just the edits, holding those updates in the model and marking the corresponding range as dirty seemed much cleaner. 然而,只是拦截编辑,在模型中保存这些更新并将相应的范围标记为脏似乎更清晰。

I think the available options are, either; 我认为可用的选项也是;

  • In the edit intercept version, try the vb hook OnKey with a call for every possible ASCII function to callback into a parameterized C# command (the VB code can at least be generated in a loop) 在编辑拦截版本中,尝试使用vb钩子OnKey调用每个可能的ASCII函数来回调参数化的C#命令(VB代码至少可以在循环中生成)
  • Try the edit intercept version as a VSTO addin (this should give me the key bindings) 尝试编辑拦截版本作为VSTO插件(这应该给我键绑定)
  • Use ExcelDNA - it's looking tempting for the (previous) read-write range parameter version (which may prove of adequate performance (which would probably indicate a logical bug in my Excel handling code). 使用ExcelDNA - 它看起来很诱人(先前的)读写范围参数版本(可能证明具有足够的性能(这可能表明我的Excel处理代码中存在逻辑错误)。

(Apologies again for the length and lack of clarity) (再次为长度和缺乏清晰度道歉)

I can't help you with the global keyboard hook, but you really should look at Excel DNA or Addin Express to dramatically improve the performance of your C# UDFs (they interface .NET with the XLL C API which is MUCH faster than c# automation). 我无法帮助你使用全局键盘钩子,但你真的应该看看Excel DNA或Addin Express来显着提高你的C#UDF的性能(他们使用XLL C API将.NET与c#自动化相比更快) 。
Both Excel DNA and Addin Express also have threads in their support forums discussing how to rewrite data back from UDFs to other ranges. Excel DNA和Addin Express在他们的支持论坛中也有讨论如何将数据从UDF重写回其他范围的线程。 IIRC Excel DNA discusses the separate thread approach and Addin Express discusses using command-equivalent type UDFs to trigger a hidden XLM function IIRC Excel DNA讨论了单独的线程方法,Addin Express讨论了使用命令等效类型UDF来触发隐藏的XLM函数

And personally I think its going to be very difficult to make your global keyboard hook approach work unobtrusively and efficiently also in all circumstances (multiple workbooks open, VBA, DDE etc etc). 而且我个人认为,在所有情况下(多个工作簿打开,VBA,DDE等),使您的全局键盘钩子方法在不显眼和高效的情况下工作将非常困难。

You should be able to solve your calculation chain problem since it sounds like a discrete set of sequential steps. 您应该能够解决计算链问题,因为它听起来像是一组离散的连续步骤。
If using C++/XLL you would make the function arguments type P which would ensure they were calculated by Excel before being passed to the UDF. 如果使用C ++ / XLL,您将使函数参数类型为P,这将确保它们在传递给UDF之前由Excel计算。 I think Ex cel DNA/addin Express should have the same effect if the parameters are defined as anything other than Object. 我认为如果将参数定义为除Object以外的任何参数,Ex cel DNA / addin Express应该具有相同的效果。

Excel calculates cells in a LIFO sequence which is set by the previous final calculation sequence and any cells that have been entered/changed: so the last formula changed gets calculated first. Excel计算LIFO序列中的单元格,该序列由先前的最终计算序列和已输入/更改的任何单元格设置:因此,首先计算最后更改的公式。
So you should enter the formulae in your DATE_RANGE chain in reverse sequence (last one in the chain first) 因此,您应该以相反的顺序在DATE_RANGE链中输入公式(首先在链中的最后一个)
Presumably you are already switching to Manual calculation mode at the start of this process. 据推测,您已在此过程开始时切换到手动计算模式。 So it might be as simple as writing out the setup sheet and dates, then forcing a calculation (Application.calculate) then updating the MODEL_ID, then forcing another calculation. 所以它可能就像写出设置表和日期一样简单,然后强制计算(Application.calculate)然后更新MODEL_ID,然后强制进行另一次计算。

And of course using Excel DNA the overhead per function call would be much lower anyway. 当然,使用Excel DNA,无论如何,每个函数调用的开销都会低得多。
see http://fastexcel.wordpress.com/2011/07/07/excel-udf-technology-choices-snakes-ladders-with-vba-vb6-net-c-com-xll-interop/ http://fastexcel.wordpress.com/2011/07/07/excel-udf-technology-choices-snakes-ladders-with-vba-vb6-net-c-com-xll-interop/

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM