• 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

  • 相关阅读:
    C++实现数字媒体三维图像渲染
    C++实现数字媒体三维图像变换
    C++实现数字媒体二维图像变换
    C++实现glut绘制点、直线、多边形、圆
    语音识别之梅尔频谱倒数MFCC(Mel Frequency Cepstrum Coefficient)
    Java中的BigDecimal类精度问题
    spring 手册
    bootstrap 参考文档
    springBoot入门文章
    JNDI
  • 原文地址:https://www.cnblogs.com/qianlifeng/p/2296553.html
Copyright © 2020-2023  润新知