visual studio 插件开发(4) -- 拦截文本编辑器中的输入事件
拦截文本编辑器中的键盘事件是很常见的一个需求。就我来说,我需要检测用户有没有输入特定的字符,然后进行一些处理。在VS中并没有现成的键盘事件供你调用。如果需要监听键盘事件,需要实现一系列的方法。下面我们来介绍并实现。
对于vs中每一个正在编辑的文档(其实也是一个window窗口),如果我们需要知道他里面发生的消息/事件,就我目前所知的有两个方法:
1. 给这个文档TextView增加CommandFilter ,拦截vs传递过来且被包装好的各种消息。
2. 得到正在编辑窗口的句柄,然后通过子类化这个窗口来得到正在发生的事件(注意这里得到和拦截的区别。得到是指你只知道发生了什么,当你不能改变它的routing)
两种方法我都进行过尝试。先尝试的第二种。因为他“看起来”简单一点,一旦我们子类化了这个编辑窗口我们就可以使用我们熟悉的winform处理消息的一些方式来进行处理,从而抛开令人困惑的COM表达。但是不幸的是,第二种一直没有成功,总是不能得到当前TextView中的消息。无奈,转向第一种方式。Here we go!
要使用第一个方法总体分两步走:
1. 打开每个文档的时候,自动给这个文档添加Filter。以便我们能够知道里面发生的一些消息。
2. 第二部就是实现这个具体的filter
拦截第一步
先从单独的文档消息拦截开始,看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | public class CommandFilter : IOleCommandTarget { public IOleCommandTarget NextCommandTarget; public int QueryStatus( ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, IntPtr pCmdText) { return NextCommandTarget.QueryStatus( ref pguidCmdGroup, cCmds, prgCmds, pCmdText); } public int Exec( ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut) { if (pguidCmdGroup == typeof (VSConstants.VSStd2KCmdID).GUID) { switch (nCmdID) { case ( uint )VSConstants.VSStd2KCmdID.RETURN: MessageBox.Show( "enter" ); break ; default : break ; } } return NextCommandTarget.Exec(pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut); } } |
可以看到,所谓的filter就是一个类继承了IOleCommandTarget 然后实现里面的方法(如果你想搞vsx,那么你就一定要习惯里面各种各样的看起来不是很容易理解的接口)。IOleCommandTarget 有两个方法,一个查询,一个执行。执行之前先查询。为什么要这样做?还记得我吗vsx2里面讲动态菜单的时候,也要先查询一个queryStatus吗?我猜想的就是如果你需要提前做什么属性更改的动作,那么这个方法提供了一个很好的时机。在这里我们没有特殊的要求,所以直接返回NextCommandTarget.QueryStatus。
这里解释一下NextCommandTarget这个对象。我们在前面也已经提到了这种拦截方式可以截断消息的传递,也就是说如果你不返回这个NextCommandTarget,那么默认的操作就失效了。举个例子,我在vs里面按下了enter键,如果我exec里面什么也不写,那么你在vs里面将看不到任何变化,因为消息被你截断了。vs收不到任何需要操作的消息了。至于NextCommandTarget这个对象怎么赋值,我们后面会提到。
现在我们主要看exec里面的内容。当代码执行到这里,那就是vs真正需要执行一些操作了。正如我们前面提到这个方式拦截到的消息都是经过vs包装过的。怎么包装的?通过guid和cmdId。这两个组合就是典型的一个命令,所以,你收到的其实是一个命令而不是一个实际的物理消息。可能说的有点迷惑,举个例子吧。我在vs里面按下了ctrl+z,通常这个命令是撤销上一次的操作。那么我们在这里接收到的其实就是这个command而不是ctrl+z这个物理按键,我们不知道用户按下了什么(不管这种方式如何,我们目前也只能接受这个现状了)。VSConstants.VSStd2KCmdID这个对象里面包含了各种各样的命令,我们作为测试使用了return这个命令,即回车键按下的事件。做完后,记得返回NextCommandTarget.Exec,不然就仅仅是弹出一个对话框而不会换行了。
在这里顺便记录一下我在做这一块放下的错误,我当时错误把返回值搞成了这样:
1 2 | NextCommandTarget.Exec(pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut); return VSConstants.S_OK; |
我当时的理解是,NextCommandTarget.Exec是告诉vs执行他默认的动作,然后返回vs_ok告诉shell我这一步正确完成了。如果你这样么做了,那么你会发现你的一些命令(如ctrl+s保存命令)失效了。就这么个问题,折磨了我两天,以至于我第一次到over stackflow和MSDN上提问。还好,大家都很热情,很快得到回复了,还是感到比较惊喜的。
提问链接:
Jared Parsons的回复(vsVim插件的作者),他的博客:http://blogs.msdn.com/jaredpar

看过上面的链接,你应该知道为什么那样是错的了。所以说,自认为害死人那...
拦截第二步
仅仅做过上面的代码之后,还不能正确拦截消息。因为没有将这个filter加入到文档中去。为了方便,我们是在每次打开文档的时刻进行注册的这个filter的。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public class TextManagerEventSink : IVsTextManagerEvents { public void OnRegisterMarkerType( int iMarkerType) { } public void OnRegisterView(IVsTextView pView) { CommandFilter filter = new CommandFilter(); pView.AddCommandFilter(filter, out filter.NextCommandTarget); } public void OnUnregisterView(IVsTextView pView) { } public void OnUserPreferencesChanged(VIEWPREFERENCES[] pViewPrefs, FRAMEPREFERENCES[] pFramePrefs, LANGPREFERENCES[] pLangPrefs, FONTCOLORPREFERENCES[] pColorPrefs) { } } |
通过IVsTextManagerEvents 这个接口,我们可以在它的OnRegisterView方法中注册每个打开的文档,为每个文档增加filter。这里你可以看到NextCommandTarget怎样赋值的吧?!其他的方法我们暂时用不到所以我们先不管。做好了这一步后,就该关注如何让vs使用这个textManager来进行文档的注册了(你不用就直接声明个TextManagerEventSink 类,肯定不会触发注册事件的)。那么做怎么让VS注册这个Manager事件呢,看下面的代码,这段代码可以写在initialize方法中,这样插件运行后就可以注册事件了。
1 2 3 4 | IConnectionPointContainer textManager = (IConnectionPointContainer)GetService( typeof (SVsTextManager)); Guid interfaceGuid = typeof (IVsTextManagerEvents).GUID; textManager.FindConnectionPoint( ref interfaceGuid, out tmConnectionPoint); tmConnectionPoint.Advise( new TextManagerEventSink(), out tmConnectionCookie); |
说实话,这句话我现在也迷糊。这种做法在很多的事件注册中都遇到过,大家就当固定用法吧,这东西没什么所以然来。
最后,回顾一下拦截编辑器中消息的步骤:
- 创建一个自己的CommandFilter
- 在TextManager的OnRegisterView中向文档注册这个commandFilter
- 在合适的地方注册TextManager事件,以便让OnRegisterView被触发
参考文档:
http://www.ngedit.com/a_intercept_keys_visual_studio_text_editor.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· .NET周刊【3月第1期 2025-03-02】
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器