visual studio 插件开发(4) -- 拦截文本编辑器中的输入事件

拦截文本编辑器中的键盘事件是很常见的一个需求。就我来说,我需要检测用户有没有输入特定的字符,然后进行一些处理。在VS中并没有现成的键盘事件供你调用。如果需要监听键盘事件,需要实现一系列的方法。下面我们来介绍并实现。
 
对于vs中每一个正在编辑的文档(其实也是一个window窗口),如果我们需要知道他里面发生的消息/事件,就我目前所知的有两个方法:
1. 给这个文档TextView增加CommandFilter , 拦截vs传递过来且被包装好的各种消息。
2. 得到正在编辑窗口的句柄,然后通过子类化这个窗口来 得到正在发生的事件(注意这里得到和拦截的区别。得到是指你只知道发生了什么,当你不能改变它的routing)
 
两种方法我都进行过尝试。先尝试的第二种。因为他“看起来”简单一点,一旦我们子类化了这个编辑窗口我们就可以使用我们熟悉的winform处理消息的一些方式来进行处理,从而抛开令人困惑的COM表达。但是不幸的是,第二种一直没有成功,总是不能得到当前TextView中的消息。无奈,转向第一种方式。Here we go!
 
要使用第一个方法总体分两步走:
1. 打开每个文档的时候,自动给这个文档添加Filter。以便我们能够知道里面发生的一些消息。
2. 第二部就是实现这个具体的filter
 
拦截第一步
先从单独的文档消息拦截开始,看代码:
 
    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,不然就仅仅是弹出一个对话框而不会换行了。
在这里顺便记录一下我在做这一块放下的错误,我当时错误把返回值搞成了这样:
 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的。代码如下:
 
 
    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方法中,这样插件运行后就可以注册事件了。
            IConnectionPointContainer textManager = (IConnectionPointContainer)GetService(typeof(SVsTextManager));
            Guid interfaceGuid = typeof(IVsTextManagerEvents).GUID;
            textManager.FindConnectionPoint(ref interfaceGuid, out tmConnectionPoint);
            tmConnectionPoint.Advise(new TextManagerEventSink(), out tmConnectionCookie);

  

说实话,这句话我现在也迷糊。这种做法在很多的事件注册中都遇到过,大家就当固定用法吧,这东西没什么所以然来。
 
最后,回顾一下拦截编辑器中消息的步骤:
  1. 创建一个自己的CommandFilter
  2. 在TextManager的OnRegisterView中向文档注册这个commandFilter
  3. 在合适的地方注册TextManager事件,以便让OnRegisterView被触发

 

参考文档:

http://www.ngedit.com/a_intercept_keys_visual_studio_text_editor.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值