zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    HDU 4069 Squiggly Sudoku
    SPOJ 1771 Yet Another NQueen Problem
    POJ 3469 Dual Core CPU
    CF 118E Bertown roads
    URAL 1664 Pipeline Transportation
    POJ 3076 Sudoku
    UVA 10330 Power Transmission
    HDU 1426 Sudoku Killer
    POJ 3074 Sudoku
    HDU 3315 My Brute
  • 原文地址:https://www.cnblogs.com/qianlifeng/p/2296553.html
Copyright © 2011-2022 走看看