演练:创建视图修饰、命令和设置(列参考线)

可以使用命令和视图效果扩展 Visual Studio 文本/代码编辑器。 本文介绍如何开始使用常用扩展功能、列参考线。 列参考线是在文本编辑器视图中绘制的视觉上的浅色线条,可帮助你将代码管理到特定的列宽。 具体来说,格式化代码对于你在文档、博客文章或错误报告中包含的示例可能很重要。

在此演练中,将:

  • 创建 VSIX 项目

  • 添加编辑器视图修饰

  • 添加对保存和获取设置的支持(在何处绘制列参考线及其颜色)

  • 添加命令(添加/删除列参考线、更改其颜色)

  • 在“编辑”菜单和文本文档上下文菜单上放置命令

  • 添加对从 Visual Studio 命令窗口调用命令的支持

    可以使用此 Visual Studio 库扩展试用列参考线功能的版本。

    注意

    在本演练中,你将大量代码粘贴到 Visual Studio 扩展模板生成的几个文件中。 但是,本演练很快将参考 GitHub 上的完整解决方案和其他扩展示例。 完成的代码略有不同,因为它具有真正的命令图标,而不是使用通用模板图标。

设置解决方案

首先,创建 VSIX 项目,添加编辑器视图装饰,然后添加命令(这将添加 VSPackage 以拥有该命令)。 基本体系结构如下:

  • 你有一个文本视图创建侦听器,它为每个视图创建一个 ColumnGuideAdornment 对象。 此对象会根据需要侦听有关视图更改或设置更改、更新或重绘列参考线的事件。

  • 有一个 GuidesSettingsManager 可以处理 Visual Studio 设置存储中的读写操作。 设置管理器还具有用于更新支持用户命令的设置的操作(添加列、删除列、更改颜色)。

  • 如果你有用户命令,则需要 VSIP 包,但它只是用于初始化命令实现对象的样板代码。

  • 有一个 ColumnGuideCommands 对象运行用户命令,并连接 .vsct 文件中声明的命令的命令处理程序。

    VSIX。 使用文件 | 新建 ... 命令创建项目。 在左侧导航窗格中选择 C# 下的可扩展性节点,然后在右窗格中选择 VSIX 项目。 输入名称 ColumnGuides,然后选择确定以创建项目。

    视图修饰。 在解决方案资源管理器中的项目节点上按右指针按钮。 选择添加 | 新建项 ...,添加新视图修饰项的命令。 在左侧导航窗格中选择可扩展性 | 编辑器,在右侧窗格中选择编辑器视图修饰。 输入名称 ColumnGuideAdornment 作为项名称,然后选择添加以添加。

    可以看到此项目模板向项目中添加了两个文件(以及引用等):ColumnGuideAdornment.csColumnGuideAdornmentTextViewCreationListener.cs。 模板在视图上绘制一个紫色矩形。 在下一节中,你将更改视图创建侦听器中的几行,并替换 ColumnGuideAdornment.cs 的内容。

    命令。 在解决方案资源管理器中,按项目节点上的右指针按钮。 选择添加 | 新建项 ...,添加新视图修饰项的命令。 在左侧导航窗格中选择可扩展性 | VSPackage,在右侧窗格中选择自定义命令。 输入名称 ColumnGuideCommands 作为项名称,然后选择添加。 除了几个引用外,添加命令和包还添加了 ColumnGuideCommands.csColumnGuideCommandsPackage.csColumnGuideCommandsPackage.vsct。 在下一节中,你将替换第一个和最后一个文件的内容来定义和实现命令。

设置文本视图创建侦听器

在编辑器中打开 ColumnGuideAdornmentTextViewCreationListener.cs。 这段代码在 Visual Studio 创建文本视图时实现了一个处理程序。 根据视图的特性,有一些属性可以控制何时调用处理程序。

代码还必须声明一个修饰层。 当编辑器更新视图时,它将获取视图的修饰层,并从中获取修饰元素。 可以使用属性声明层相对于其他层的排序。 替换以下行:

[Order(After = PredefinedAdornmentLayers.Caret)]

使用以下两行:

[Order(Before = PredefinedAdornmentLayers.Text)]
[TextViewRole(PredefinedTextViewRoles.Document)]

替换的行位于一组声明修饰层的属性中。 你更改的第一行只会更改列参考线的显示位置。 在视图中的文本“之前”绘制线条意味着它们出现在文本的后面或下面。 第二行声明列参考线修饰适用于适合文档概念的文本实体,但可以声明修饰,例如,仅可用于可编辑文本。 有关详细信息,请参阅语言服务和编辑器扩展点

实现设置管理器

GuidesSettingsManager.cs 的内容替换为以下代码(如下所述):

using Microsoft.VisualStudio.Settings;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Settings;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Media;

namespace ColumnGuides
{
    internal static class GuidesSettingsManager
    {
        // Because my code is always called from the UI thred, this succeeds.
        internal static SettingsManager VsManagedSettingsManager =
            new ShellSettingsManager(ServiceProvider.GlobalProvider);

        private const int _maxGuides = 5;
        private const string _collectionSettingsName = "Text Editor";
        private const string _settingName = "Guides";
        // 1000 seems reasonable since primary scenario is long lines of code
        private const int _maxColumn = 1000;

        static internal bool AddGuideline(int column)
        {
            if (! IsValidColumn(column))
                throw new ArgumentOutOfRangeException(
                    "column",
                    "The parameter must be between 1 and " + _maxGuides.ToString());
            var offsets = GuidesSettingsManager.GetColumnOffsets();
            if (offsets.Count() >= _maxGuides)
                return false;
            // Check for duplicates
            if (offsets.Contains(column))
                return false;
            offsets.Add(column);
            WriteSettings(GuidesSettingsManager.GuidelinesColor, offsets);
            return true;
        }

        static internal bool RemoveGuideline(int column)
        {
            if (!IsValidColumn(column))
                throw new ArgumentOutOfRangeException(
                    "column", "The parameter must be between 1 and 10,000");
            var columns = GuidesSettingsManager.GetColumnOffsets();
            if (! columns.Remove(column))
            {
                // Not present.  Allow user to remove the last column
                // even if they're not on the right column.
                if (columns.Count != 1)
                    return false;

                columns.Clear();
            }
            WriteSettings(GuidesSettingsManager.GuidelinesColor, columns);
            return true;
        }

        static internal bool CanAddGuideline(int column)
        {
            if (!IsValidColumn(column))
                return false;
            var offsets = GetColumnOffsets();
            if (offsets.Count >= _maxGuides)
                return false;
            return ! offsets.Contains(column);
        }

        static internal bool CanRemoveGuideline(int column)
        {
            if (! IsValidColumn(column))
                return false;
            // Allow user to remove the last guideline regardless of the column.
            // Okay to call count, we limit the number of guides.
            var offsets = GuidesSettingsManager.GetColumnOffsets();
            return offsets.Contains(column) || offsets.Count() == 1;
        }

        static internal void RemoveAllGuidelines()
        {
            WriteSettings(GuidesSettingsManager.GuidelinesColor, new int[0]);
        }

        private static bool IsValidColumn(int column)
        {
            // zero is allowed (per user request)
            return 0 <= column && column <= _maxColumn;
        }

        // This has format "RGB(<int>, <int>, <int>) <int> <int>...".
        // There can be any number of ints following the RGB part,
        // and each int is a column (char offset into line) where to draw.
        static private string _guidelinesConfiguration;
        static private string GuidelinesConfiguration
        {
            get
            {
                if (_guidelinesConfiguration == null)
                {
                    _guidelinesConfiguration =
                        GetUserSettingsString(
                            GuidesSettingsManager._collectionSettingsName,
                            GuidesSettingsManager._settingName)
                        .Trim();
                }
                return _guidelinesConfiguration;
            }

            set
            {
                if (value != _guidelinesConfiguration)
                {
                    _guidelinesConfiguration = value;
                    WriteUserSettingsString(
                        GuidesSettingsManager._collectionSettingsName,
                        GuidesSettingsManager._settingName, value);
                    // Notify ColumnGuideAdornments to update adornments in views.
                    var handler = GuidesSettingsManager.SettingsChanged;
                    if (handler != null)
                        handler();
                }
            }
        }

        internal static string GetUserSettingsString(string collection, string setting)
        {
            var store = GuidesSettingsManager
                            .VsManagedSettingsManager
                            .GetReadOnlySettingsStore(SettingsScope.UserSettings);
            return store.GetString(collection, setting, "RGB(255,0,0) 80");
        }

        internal static void WriteUserSettingsString(string key, string propertyName,
                                                     string value)
        {
            var store = GuidesSettingsManager
                            .VsManagedSettingsManager
                            .GetWritableSettingsStore(SettingsScope.UserSettings);
            store.CreateCollection(key);
            store.SetString(key, propertyName, value);
        }

        // Persists settings and sets property with side effect of signaling
        // ColumnGuideAdornments to update.
        static private void WriteSettings(Color color, IEnumerable<int> columns)
        {
            string value = ComposeSettingsString(color, columns);
            GuidelinesConfiguration = value;
        }

        private static string ComposeSettingsString(Color color,
                                                    IEnumerable<int> columns)
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendFormat("RGB({0},{1},{2})", color.R, color.G, color.B);
            IEnumerator<int> columnsEnumerator = columns.GetEnumerator();
            if (columnsEnumerator.MoveNext())
            {
                sb.AppendFormat(" {0}", columnsEnumerator.Current);
                while (columnsEnumerator.MoveNext())
                {
                    sb.AppendFormat(", {0}", columnsEnumerator.Current);
                }
            }
            return sb.ToString();
        }

        // Parse a color out of a string that begins like "RGB(255,0,0)"
        static internal Color GuidelinesColor
        {
            get
            {
                string config = GuidelinesConfiguration;
                if (!String.IsNullOrEmpty(config) && config.StartsWith("RGB("))
                {
                    int lastParen = config.IndexOf(')');
                    if (lastParen > 4)
                    {
                        string[] rgbs = config.Substring(4, lastParen - 4).Split(',');

                        if (rgbs.Length >= 3)
                        {
                            byte r, g, b;
                            if (byte.TryParse(rgbs[0], out r) &&
                                byte.TryParse(rgbs[1], out g) &&
                                byte.TryParse(rgbs[2], out b))
                            {
                                return Color.FromRgb(r, g, b);
                            }
                        }
                    }
                }
                return Colors.DarkRed;
            }

            set
            {
                WriteSettings(value, GetColumnOffsets());
            }
        }

        // Parse a list of integer values out of a string that looks like
        // "RGB(255,0,0) 1, 5, 10, 80"
        static internal List<int> GetColumnOffsets()
        {
            var result = new List<int>();
            string settings = GuidesSettingsManager.GuidelinesConfiguration;
            if (String.IsNullOrEmpty(settings))
                return new List<int>();

            if (!settings.StartsWith("RGB("))
                return new List<int>();

            int lastParen = settings.IndexOf(')');
            if (lastParen <= 4)
                return new List<int>();

            string[] columns = settings.Substring(lastParen + 1).Split(',');

            int columnCount = 0;
            foreach (string columnText in columns)
            {
                int column = -1;
                // VS 2008 gallery extension didn't allow zero, so per user request ...
                if (int.TryParse(columnText, out column) && column >= 0)
                {
                    columnCount++;
                    result.Add(column);
                    if (columnCount >= _maxGuides)
                        break;
                }
            }
            return result;
        }

        // Delegate and Event to fire when settings change so that ColumnGuideAdornments
        // can update.  We need nothing special in this event since the settings manager
        // is statically available.
        //
        internal delegate void SettingsChangedHandler();
        static internal event SettingsChangedHandler SettingsChanged;

    }
}

大部分代码创建并解析设置格式:"RGB(<int>,<int>,<int>) <int>, <int>, ...". 末尾的整数是你需要列参考线的基于一的列。 列参考线扩展在单个设置值字符串中捕获其所有设置。

代码中有一些部分值得强调。 以下代码行获取设置存储的 Visual Studio 托管包装器。 在大多数情况下,此 API 会通过 Windows 注册表进行抽象化,但此 API 独立于存储机制。

internal static SettingsManager VsManagedSettingsManager =
    new ShellSettingsManager(ServiceProvider.GlobalProvider);

Visual Studio 设置存储使用类别标识符和设置标识符来唯一标识所有设置:

private const string _collectionSettingsName = "Text Editor";
private const string _settingName = "Guides";

不必使用 "Text Editor" 作为类别名称。 你可以选择喜欢的任何内容。

前几个函数是更改设置的入口点。 它们检查高级约束,例如允许的最大参考线数。 然后,它们调用 WriteSettings,它组成一个设置字符串并设置属性 GuideLinesConfiguration。 设置此属性会将设置值保存到 Visual Studio 设置存储中,并触发 SettingsChanged 事件以更新所有与文本视图关联的 ColumnGuideAdornment 对象。

有几个入口点函数,例如 CanAddGuideline,用于实现更改设置的命令。 当 Visual Studio 显示菜单时,它会查询命令实现,以查看命令当前是否已启用、其名称是什么,等等。 下面介绍了如何为命令实现挂接这些入口点。 有关命令的详细信息,请参阅扩展菜单和命令

实现 ColumnGuideAdornment 类

为每个可以有修饰的文本视图实例化 ColumnGuideAdornment 类。 此类会根据需要侦听有关视图更改或设置更改、更新或重绘列参考线的事件。

ColumnGuideAdornment.cs 的内容替换为以下代码(如下所述):

using System;
using System.Windows.Media;
using Microsoft.VisualStudio.Text.Editor;
using System.Collections.Generic;
using System.Windows.Shapes;
using Microsoft.VisualStudio.Text.Formatting;
using System.Windows;

namespace ColumnGuides
{
    /// <summary>
    /// Adornment class, one instance per text view that draws a guides on the viewport
    /// </summary>
    internal sealed class ColumnGuideAdornment
    {
        private const double _lineThickness = 1.0;
        private IList<Line> _guidelines;
        private IWpfTextView _view;
        private double _baseIndentation;
        private double _columnWidth;

        /// <summary>
        /// Creates editor column guidelines
        /// </summary>
        /// <param name="view">The <see cref="IWpfTextView"/> upon
        /// which the adornment will be drawn</param>
        public ColumnGuideAdornment(IWpfTextView view)
        {
            _view = view;
            _guidelines = CreateGuidelines();
            GuidesSettingsManager.SettingsChanged +=
                new GuidesSettingsManager.SettingsChangedHandler(SettingsChanged);
            view.LayoutChanged +=
                new EventHandler<TextViewLayoutChangedEventArgs>(OnViewLayoutChanged);
            _view.Closed += new EventHandler(OnViewClosed);
        }

        void SettingsChanged()
        {
            _guidelines = CreateGuidelines();
            UpdatePositions();
            AddGuidelinesToAdornmentLayer();
        }

        void OnViewClosed(object sender, EventArgs e)
        {
            _view.LayoutChanged -= OnViewLayoutChanged;
            _view.Closed -= OnViewClosed;
            GuidesSettingsManager.SettingsChanged -= SettingsChanged;
        }

        private bool _firstLayoutDone;

        void OnViewLayoutChanged(object sender, TextViewLayoutChangedEventArgs e)
        {
            bool fUpdatePositions = false;

            IFormattedLineSource lineSource = _view.FormattedLineSource;
            if (lineSource == null)
            {
                return;
            }
            if (_columnWidth != lineSource.ColumnWidth)
            {
                _columnWidth = lineSource.ColumnWidth;
                fUpdatePositions = true;
            }
            if (_baseIndentation != lineSource.BaseIndentation)
            {
                _baseIndentation = lineSource.BaseIndentation;
                fUpdatePositions = true;
            }
            if (fUpdatePositions ||
                e.VerticalTranslation ||
                e.NewViewState.ViewportTop != e.OldViewState.ViewportTop ||
                e.NewViewState.ViewportBottom != e.OldViewState.ViewportBottom)
            {
                UpdatePositions();
            }
            if (!_firstLayoutDone)
            {
                AddGuidelinesToAdornmentLayer();
                _firstLayoutDone = true;
            }
        }

        private static IList<Line> CreateGuidelines()
        {
            Brush lineBrush = new SolidColorBrush(GuidesSettingsManager.GuidelinesColor);
            DoubleCollection dashArray = new DoubleCollection(new double[] { 1.0, 3.0 });
            IList<Line> result = new List<Line>();
            foreach (int column in GuidesSettingsManager.GetColumnOffsets())
            {
                Line line = new Line()
                {
                    // Use the DataContext slot as a cookie to hold the column
                    DataContext = column,
                    Stroke = lineBrush,
                    StrokeThickness = _lineThickness,
                    StrokeDashArray = dashArray
                };
                result.Add(line);
            }
            return result;
        }

        void UpdatePositions()
        {
            foreach (Line line in _guidelines)
            {
                int column = (int)line.DataContext;
                line.X2 = _baseIndentation + 0.5 + column * _columnWidth;
                line.X1 = line.X2;
                line.Y1 = _view.ViewportTop;
                line.Y2 = _view.ViewportBottom;
            }
        }

        void AddGuidelinesToAdornmentLayer()
        {
            // Grab a reference to the adornment layer that this adornment
            // should be added to
            // Must match exported name in ColumnGuideAdornmentTextViewCreationListener
            IAdornmentLayer adornmentLayer =
                _view.GetAdornmentLayer("ColumnGuideAdornment");
            if (adornmentLayer == null)
                return;
            adornmentLayer.RemoveAllAdornments();
            // Add the guidelines to the adornment layer and make them relative
            // to the viewport
            foreach (UIElement element in _guidelines)
                adornmentLayer.AddAdornment(AdornmentPositioningBehavior.OwnerControlled,
                                            null, null, element, null);
        }
    }

}

此类的实例保留了相关联的 IWpfTextView 和在视图上绘制的 Line 对象列表。

构造函数(Visual Studio 创建新视图时从 ColumnGuideAdornmentTextViewCreationListener 调用)创建列参考线 Line 对象。 构造函数还会为 SettingsChanged 事件(在 GuidesSettingsManager 中定义)和视图事件 LayoutChanged 以及 Closed添加处理程序。

LayoutChanged 事件是由于视图中的几种更改而触发的,包括 Visual Studio 创建视图时。 OnViewLayoutChanged 处理程序调用 AddGuidelinesToAdornmentLayer 来执行。 OnViewLayoutChanged 中的代码确定是否需要根据字体大小更改、视图边距、水平滚动等更改来更新行位置。 UpdatePositions 中的代码导致在字符之间或文本行中指定字符偏移量的文本列之后绘制参考线。

每当设置更改时,函数 SettingsChanged 函数只会使用新设置重新创建所有 Line 对象。 设置行位置后,代码将从 ColumnGuideAdornment 修饰层中删除所有以前的 Line 对象,并添加新对象。

定义命令、菜单和菜单位置

声明命令和菜单、在各种其他菜单上放置命令或菜单组以及挂接命令处理程序可能有很多。 本演练重点介绍此扩展中的命令的工作原理,但有关更深入的信息,请参阅扩展菜单和命令

代码简介

列参考线扩展显示声明属于一组命令(添加列、删除列、更改行颜色),然后将该组放置在编辑器上下文菜单的子菜单上。 “列参考线”扩展还会将命令添加到主编辑菜单,但使其不可见,下面将作为常见模式进行讨论。

命令实现有三个部分:ColumnGuideCommandsPackage.cs、ColumnGuideCommandsPackage.vsct 和 ColumnGuideCommands.cs。 模板生成的代码将命令放在工具 菜单上,该命令会弹出一个对话框作为实现。 可以查看如何在 .vsctColumnGuideCommands.cs 文件中实现它,因为它很简单。 可以替换下面这些文件中的代码。

包代码包含 Visual Studio 发现扩展提供命令,并查找命令放置位置所需的样本声明。 包初始化时,它会实例化命令实现类。 有关与命令相关的包的详细信息,请参阅扩展菜单和命令

常见命令模式

列参考线扩展中的命令是 Visual Studio 中非常常见的模式的一个示例。 将相关命令放在组中,并将该组放在主菜单上,通常设置为“<CommandFlag>CommandWellOnly</CommandFlag>”以使命令不可见。 将命令放在主菜单上(如编辑)可以为它们命名(如 Edit.AddColumnGuide),这对于在工具选项中重新分配键绑定时查找命令非常有用。 当从命令窗口调用命令时,它也有助于完成。

然后,可以将该组命令添加到上下文菜单或子菜单中,以便用户使用命令。 Visual Studio 仅被 CommandWellOnly 视为主菜单的不可见性标志。 当你将同一组命令放置在上下文菜单或子菜单上时,这些命令是可见的。

作为常见模式的一部分,“列参考线”扩展创建了第二个组,其中包含一个子菜单。 子菜单依次包含第一组四列参考线命令。 第二组包含子菜单,是你放置在各种上下文菜单上的可重用资产,它将子菜单放在这些上下文菜单上。

.vsct 文件

.vsct 文件声明命令及其所在位置,以及图标等。 将 .vsct 文件的内容替换为以下代码(如下所述):

<?xml version="1.0" encoding="utf-8"?>
<CommandTable xmlns="http://schemas.microsoft.com/VisualStudio/2005-10-18/CommandTable" xmlns:xs="http://www.w3.org/2001/XMLSchema">

  <!--  This is the file that defines the actual layout and type of the commands.
        It is divided in different sections (e.g. command definition, command
        placement, ...), with each defining a specific set of properties.
        See the comment before each section for more details about how to
        use it. -->

  <!--  The VSCT compiler (the tool that translates this file into the binary
        format that VisualStudio will consume) has the ability to run a preprocessor
        on the vsct file; this preprocessor is (usually) the C++ preprocessor, so
        it is possible to define includes and macros with the same syntax used
        in C++ files. Using this ability of the compiler here, we include some files
        defining some of the constants that we will use inside the file. -->

  <!--This is the file that defines the IDs for all the commands exposed by
      VisualStudio. -->
  <Extern href="stdidcmd.h"/>

  <!--This header contains the command ids for the menus provided by the shell. -->
  <Extern href="vsshlids.h"/>

  <!--The Commands section is where commands, menus, and menu groups are defined.
      This section uses a Guid to identify the package that provides the command
      defined inside it. -->
  <Commands package="guidColumnGuideCommandsPkg">
    <!-- Inside this section we have different sub-sections: one for the menus, another
    for the menu groups, one for the buttons (the actual commands), one for the combos
    and the last one for the bitmaps used. Each element is identified by a command id
    that is a unique pair of guid and numeric identifier; the guid part of the identifier
    is usually called "command set" and is used to group different command inside a
    logically related group; your package should define its own command set in order to
    avoid collisions with command ids defined by other packages. -->

    <!-- In this section you can define new menu groups. A menu group is a container for
         other menus or buttons (commands); from a visual point of view you can see the
         group as the part of a menu contained between two lines. The parent of a group
         must be a menu. -->
    <Groups>

      <!-- The main group is parented to the edit menu. All the buttons within the group
           have the "CommandWellOnly" flag, so they're actually invisible, but it means
           they get canonical names that begin with "Edit". Using placements, the group
           is also placed in the GuidesSubMenu group. -->
      <!-- The priority 0xB801 is chosen so it goes just after
           IDG_VS_EDIT_COMMANDWELL -->
      <Group guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup"
             priority="0xB801">
        <Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_EDIT" />
      </Group>

      <!-- Group for holding the "Guidelines" sub-menu anchor (the item on the menu that
           drops the sub menu). The group is parented to
           the context menu for code windows. That takes care of most editors, but it's
           also placed in a couple of other windows using Placements -->
      <Group guid="guidColumnGuidesCommandSet" id="GuidesContextMenuGroup"
             priority="0x0600">
        <Parent guid="guidSHLMainMenu" id="IDM_VS_CTXT_CODEWIN" />
      </Group>

    </Groups>

    <Menus>
      <Menu guid="guidColumnGuidesCommandSet" id="GuidesSubMenu" priority="0x1000"
            type="Menu">
        <Parent guid="guidColumnGuidesCommandSet" id="GuidesContextMenuGroup" />
        <Strings>
          <ButtonText>&Column Guides</ButtonText>
        </Strings>
      </Menu>
    </Menus>

    <!--Buttons section. -->
    <!--This section defines the elements the user can interact with, like a menu command or a button
        or combo box in a toolbar. -->
    <Buttons>
      <!--To define a menu group you have to specify its ID, the parent menu and its
          display priority.
          The command is visible and enabled by default. If you need to change the
          visibility, status, etc, you can use the CommandFlag node.
          You can add more than one CommandFlag node e.g.:
              <CommandFlag>DefaultInvisible</CommandFlag>
              <CommandFlag>DynamicVisibility</CommandFlag>
          If you do not want an image next to your command, remove the Icon node or
          set it to <Icon guid="guidOfficeIcon" id="msotcidNoIcon" /> -->

      <Button guid="guidColumnGuidesCommandSet" id="cmdidAddColumnGuide"
              priority="0x0100" type="Button">
        <Parent guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup" />
        <Icon guid="guidImages" id="bmpPicAddGuide" />
        <CommandFlag>CommandWellOnly</CommandFlag>
        <CommandFlag>AllowParams</CommandFlag>
        <Strings>
          <ButtonText>&Add Column Guide</ButtonText>
        </Strings>
      </Button>

      <Button guid="guidColumnGuidesCommandSet" id="cmdidRemoveColumnGuide"
              priority="0x0101" type="Button">
        <Parent guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup" />
        <Icon guid="guidImages" id="bmpPicRemoveGuide" />
        <CommandFlag>CommandWellOnly</CommandFlag>
        <CommandFlag>AllowParams</CommandFlag>
        <Strings>
          <ButtonText>&Remove Column Guide</ButtonText>
        </Strings>
      </Button>

      <Button guid="guidColumnGuidesCommandSet" id="cmdidChooseGuideColor"
              priority="0x0103" type="Button">
        <Parent guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup" />
        <Icon guid="guidImages" id="bmpPicChooseColor" />
        <CommandFlag>CommandWellOnly</CommandFlag>
        <Strings>
          <ButtonText>Column Guide &Color...</ButtonText>
        </Strings>
      </Button>

      <Button guid="guidColumnGuidesCommandSet" id="cmdidRemoveAllColumnGuides"
              priority="0x0102" type="Button">
        <Parent guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup" />
        <CommandFlag>CommandWellOnly</CommandFlag>
        <Strings>
          <ButtonText>Remove A&ll Columns</ButtonText>
        </Strings>
      </Button>
    </Buttons>

    <!--The bitmaps section is used to define the bitmaps that are used for the
        commands.-->
    <Bitmaps>
      <!--  The bitmap id is defined in a way that is a little bit different from the
            others:
            the declaration starts with a guid for the bitmap strip, then there is the
            resource id of the bitmap strip containing the bitmaps and then there are
            the numeric ids of the elements used inside a button definition. An important
            aspect of this declaration is that the element id
            must be the actual index (1-based) of the bitmap inside the bitmap strip. -->
      <Bitmap guid="guidImages" href="Resources\ColumnGuideCommands.png"
              usedList="bmpPicAddGuide, bmpPicRemoveGuide, bmpPicChooseColor" />
    </Bitmaps>

  </Commands>

  <CommandPlacements>

    <!-- Define secondary placements for our groups -->

    <!-- Place the group containing the three commands in the sub-menu -->
    <CommandPlacement guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup"
                      priority="0x0100">
      <Parent guid="guidColumnGuidesCommandSet" id="GuidesSubMenu" />
    </CommandPlacement>

    <!-- The HTML editor context menu, for some reason, redefines its own groups
         so we need to place a copy of our context menu there too. -->
    <CommandPlacement guid="guidColumnGuidesCommandSet" id="GuidesContextMenuGroup"
                      priority="0x1001">
      <Parent guid="CMDSETID_HtmEdGrp" id="IDMX_HTM_SOURCE_HTML" />
    </CommandPlacement>

    <!-- The HTML context menu in Dev12 changed. -->
    <CommandPlacement guid="guidColumnGuidesCommandSet" id="GuidesContextMenuGroup"
                      priority="0x1001">
      <Parent guid="CMDSETID_HtmEdGrp_Dev12" id="IDMX_HTM_SOURCE_HTML_Dev12" />
    </CommandPlacement>

    <!-- Similarly for Script -->
    <CommandPlacement guid="guidColumnGuidesCommandSet" id="GuidesContextMenuGroup"
                      priority="0x1001">
      <Parent guid="CMDSETID_HtmEdGrp" id="IDMX_HTM_SOURCE_SCRIPT" />
    </CommandPlacement>

    <!-- Similarly for ASPX  -->
    <CommandPlacement guid="guidColumnGuidesCommandSet" id="GuidesContextMenuGroup"
                      priority="0x1001">
      <Parent guid="CMDSETID_HtmEdGrp" id="IDMX_HTM_SOURCE_ASPX" />
    </CommandPlacement>

    <!-- Similarly for the XAML editor context menu -->
    <CommandPlacement guid="guidColumnGuidesCommandSet" id="GuidesContextMenuGroup"
                      priority="0x0600">
      <Parent guid="guidXamlUiCmds" id="IDM_XAML_EDITOR" />
    </CommandPlacement>

  </CommandPlacements>

  <!-- This defines the identifiers and their values used above to index resources
       and specify commands. -->
  <Symbols>
    <!-- This is the package guid. -->
    <GuidSymbol name="guidColumnGuideCommandsPkg"
                value="{e914e5de-0851-4904-b361-1a3a9d449704}" />

    <!-- This is the guid used to group the menu commands together -->
    <GuidSymbol name="guidColumnGuidesCommandSet"
                value="{c2bc0047-8bfa-4e5a-b5dc-45af8c274d8e}">
      <IDSymbol name="GuidesContextMenuGroup" value="0x1020" />
      <IDSymbol name="GuidesMenuItemsGroup" value="0x1021" />
      <IDSymbol name="GuidesSubMenu" value="0x1022" />
      <IDSymbol name="cmdidAddColumnGuide" value="0x0100" />
      <IDSymbol name="cmdidRemoveColumnGuide" value="0x0101" />
      <IDSymbol name="cmdidChooseGuideColor" value="0x0102" />
      <IDSymbol name="cmdidRemoveAllColumnGuides" value="0x0103" />
    </GuidSymbol>

    <GuidSymbol name="guidImages" value="{2C99F852-587C-43AF-AA2D-F605DE2E46EF}">
      <IDSymbol name="bmpPicAddGuide" value="1" />
      <IDSymbol name="bmpPicRemoveGuide" value="2" />
      <IDSymbol name="bmpPicChooseColor" value="3" />
    </GuidSymbol>

    <GuidSymbol name="CMDSETID_HtmEdGrp_Dev12"
                value="{78F03954-2FB8-4087-8CE7-59D71710B3BB}">
      <IDSymbol name="IDMX_HTM_SOURCE_HTML_Dev12" value="0x1" />
    </GuidSymbol>

    <GuidSymbol name="CMDSETID_HtmEdGrp" value="{d7e8c5e1-bdb8-11d0-9c88-0000f8040a53}">
      <IDSymbol name="IDMX_HTM_SOURCE_HTML" value="0x33" />
      <IDSymbol name="IDMX_HTM_SOURCE_SCRIPT" value="0x34" />
      <IDSymbol name="IDMX_HTM_SOURCE_ASPX" value="0x35" />
    </GuidSymbol>

    <GuidSymbol name="guidXamlUiCmds" value="{4c87b692-1202-46aa-b64c-ef01faec53da}">
      <IDSymbol name="IDM_XAML_EDITOR" value="0x103" />
    </GuidSymbol>
  </Symbols>

</CommandTable>

GUIDS。 若要让 Visual Studio 找到命令处理程序并调用它们,需要确保 ColumnGuideCommandsPackage.cs 文件(从项目项模板生成)中声明的包 GUID 与 .vsct 文件中声明的包 GUID 匹配(从上面复制)。 如果重新使用此示例代码,应确保具有不同的 GUID,这样你就不会与可能复制此代码的其他人冲突。

ColumnGuideCommandsPackage.cs 中找到此行,并从引号之间复制 GUID:

public const string PackageGuidString = "ef726849-5447-4f73-8de5-01b9e930f7cd";

然后,将 GUID 粘贴到 .vsct 文件中,以便在 Symbols 声明中具有以下行:

<GuidSymbol name="guidColumnGuideCommandsPkg"
            value="{ef726849-5447-4f73-8de5-01b9e930f7cd}" />

命令集和位图图像文件的 GUID 对于扩展而言也应是唯一的:

<GuidSymbol name="guidColumnGuidesCommandSet"
            value="{c2bc0047-8bfa-4e5a-b5dc-45af8c274d8e}">
<GuidSymbol name="guidImages" value="{2C99F852-587C-43AF-AA2D-F605DE2E46EF}">

但是,在此演练中,无需更改命令集和位图图像 GUID 即可使代码正常工作。 命令集 GUID 需要与 ColumnGuideCommands.cs 文件中的声明匹配;但你也可以替换该文件的内容;因此 GUID 将匹配。

.vsct 文件中的其他 GUID 标识添加列参考线命令的预先存在的菜单,因此它们永远不会更改。

文件部分.vsct 有三个外部部分:命令、位置和符号。 命令部分定义了命令组、菜单、按钮或菜单项以及图标的位图。 位置部分声明了组在菜单上的位置或在预先存在的菜单上的其他位置。 符号部分声明了 .vsct 文件中其他地方使用的标识符,这使得 .vsct 代码比到处都有 GUID 和十六进制数更具可读性。

命令部分,组定义。 命令部分首先定义命令组。 命令组是你在菜单中看到的命令,这些命令组之间用灰色细线隔开。 一个组也可能填充整个子菜单,如本例所示,在本例中,看不到灰色分隔线。 .vsct 文件声明两个组,即 GuidesMenuItemsGroupIDM_VS_MENU_EDIT 的父级(主编辑菜单),GuidesContextMenuGroupIDM_VS_CTXT_CODEWIN 的父级(代码编辑器的上下文菜单)。

第二个组声明具有 0x0600 优先级:

<Group guid="guidColumnGuidesCommandSet" id="GuidesContextMenuGroup"
             priority="0x0600">

其思路是将列参考线子菜单放在添加子菜单组的任何上下文菜单的末尾。 但是,不应假设你最了解,并通过使用优先级 0xFFFF 来强制子菜单总是在最后。 必须尝试这个数字,看看你的子菜单在你放置它的上下文菜单上的位置。 在这种情况下,0x0600 足够高,可以将其放在菜单的末尾,只要你可以看到;但如果需要的话,它为其他人将其扩展设计为低于列参考线扩展留出了空间。

命令部分,菜单定义。 接下来,命令部分定义了子菜单 GuidesSubMenu,是 GuidesContextMenuGroup 的父级。 GuidesContextMenuGroup 是你添加到所有相关上下文菜单中的组。 在放置部分,代码将带有四列参考线命令的组放置在此子菜单上。

命令部分,按钮定义。 然后,命令部分定义了作为四列参考线命令的菜单项或按钮。 如上所述,CommandWellOnly 表示命令在放置在主菜单上时不可见。 两个菜单项按钮声明(添加参考线和删除参考线)也有一个 AllowParams 标志:

<CommandFlag>AllowParams</CommandFlag>

此标志在 Visual Studio 调用命令处理程序时,除了具有主菜单位置外,还允许命令接收参数。 如果用户从命令窗口运行命令,则参数将在事件参数中传递给命令处理程序。

命令部分,位图定义。 最后,命令部分声明用于命令的位图或图标。 本部分是一个简单的声明,用于标识项目资源,并列出已用图标的基于一个索引。 .vsct 文件的符号部分声明了用作索引的标识符的值。 本演练使用添加到项目中的自定义命令项模板提供的位图条。

位置部分。 命令部分之后是放置部分。 第一个是代码将上述包含四列参考线命令的第一组添加到命令出现的子菜单中:

<CommandPlacement guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup"
                  priority="0x0100">
  <Parent guid="guidColumnGuidesCommandSet" id="GuidesSubMenu" />
</CommandPlacement>

所有其他位置都将 GuidesContextMenuGroup(包含 GuidesSubMenu)添加到其他编辑器上下文菜单中。 当代码声明 GuidesContextMenuGroup 时,它被置于代码编辑器的上下文菜单的父级。 这就是为什么你看不到代码编辑器上下文菜单的位置。

符号部分。 如上所述,符号部分声明了 .vsct 文件中其他地方使用的标识符,这使得 .vsct 代码比到处都有 GUID 和十六进制数更具可读性。 本节的重点是包 GUID 必须与包类中的声明一致。 并且,命令集 GUID 必须与命令实现类中的声明一致。

实现命令

ColumnGuideCommands.cs 文件实现命令并挂接处理程序。 当 Visual Studio 加载包并初始化包时,该包又会调用 Initialize 命令实现类。 命令初始化只是实例化类,构造函数将挂接所有命令处理程序。

ColumnGuideCommands.cs 文件的内容替换为以下代码(如下所述):

using System;
using System.ComponentModel.Design;
using System.Globalization;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.TextManager.Interop;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio;

namespace ColumnGuides
{
    /// <summary>
    /// Command handler
    /// </summary>
    internal sealed class ColumnGuideCommands
    {

        const int cmdidAddColumnGuide = 0x0100;
        const int cmdidRemoveColumnGuide = 0x0101;
        const int cmdidChooseGuideColor = 0x0102;
        const int cmdidRemoveAllColumnGuides = 0x0103;

        /// <summary>
        /// Command menu group (command set GUID).
        /// </summary>
        static readonly Guid CommandSet =
            new Guid("c2bc0047-8bfa-4e5a-b5dc-45af8c274d8e");

        /// <summary>
        /// VS Package that provides this command, not null.
        /// </summary>
        private readonly Package package;

        OleMenuCommand _addGuidelineCommand;
        OleMenuCommand _removeGuidelineCommand;

        /// <summary>
        /// Initializes the singleton instance of the command.
        /// </summary>
        /// <param name="package">Owner package, not null.</param>
        public static void Initialize(Package package)
        {
            Instance = new ColumnGuideCommands(package);
        }

        /// <summary>
        /// Gets the instance of the command.
        /// </summary>
        public static ColumnGuideCommands Instance
        {
            get;
            private set;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ColumnGuideCommands"/> class.
        /// Adds our command handlers for menu (commands must exist in the command
        /// table file)
        /// </summary>
        /// <param name="package">Owner package, not null.</param>
        private ColumnGuideCommands(Package package)
        {
            if (package == null)
            {
                throw new ArgumentNullException("package");
            }

            this.package = package;

            // Add our command handlers for menu (commands must exist in the .vsct file)

            OleMenuCommandService commandService =
                this.ServiceProvider.GetService(typeof(IMenuCommandService))
                    as OleMenuCommandService;
            if (commandService != null)
            {
                // Add guide
                _addGuidelineCommand =
                    new OleMenuCommand(AddColumnGuideExecuted, null,
                                       AddColumnGuideBeforeQueryStatus,
                                       new CommandID(ColumnGuideCommands.CommandSet,
                                                     cmdidAddColumnGuide));
                _addGuidelineCommand.ParametersDescription = "<column>";
                commandService.AddCommand(_addGuidelineCommand);
                // Remove guide
                _removeGuidelineCommand =
                    new OleMenuCommand(RemoveColumnGuideExecuted, null,
                                       RemoveColumnGuideBeforeQueryStatus,
                                       new CommandID(ColumnGuideCommands.CommandSet,
                                                     cmdidRemoveColumnGuide));
                _removeGuidelineCommand.ParametersDescription = "<column>";
                commandService.AddCommand(_removeGuidelineCommand);
                // Choose color
                commandService.AddCommand(
                    new MenuCommand(ChooseGuideColorExecuted,
                                    new CommandID(ColumnGuideCommands.CommandSet,
                                                  cmdidChooseGuideColor)));
                // Remove all
                commandService.AddCommand(
                    new MenuCommand(RemoveAllGuidelinesExecuted,
                                    new CommandID(ColumnGuideCommands.CommandSet,
                                                  cmdidRemoveAllColumnGuides)));
            }
        }

        /// <summary>
        /// Gets the service provider from the owner package.
        /// </summary>
        private IServiceProvider ServiceProvider
        {
            get
            {
                return this.package;
            }
        }

        private void AddColumnGuideBeforeQueryStatus(object sender, EventArgs e)
        {
            int currentColumn = GetCurrentEditorColumn();
            _addGuidelineCommand.Enabled =
                GuidesSettingsManager.CanAddGuideline(currentColumn);
        }

        private void RemoveColumnGuideBeforeQueryStatus(object sender, EventArgs e)
        {
            int currentColumn = GetCurrentEditorColumn();
            _removeGuidelineCommand.Enabled =
                GuidesSettingsManager.CanRemoveGuideline(currentColumn);
        }

        private int GetCurrentEditorColumn()
        {
            IVsTextView view = GetActiveTextView();
            if (view == null)
            {
                return -1;
            }

            try
            {
                IWpfTextView textView = GetTextViewFromVsTextView(view);
                int column = GetCaretColumn(textView);

                // Note: GetCaretColumn returns 0-based positions. Guidelines are 1-based
                // positions.
                // However, do not subtract one here since the caret is positioned to the
                // left of
                // the given column and the guidelines are positioned to the right. We
                // want the
                // guideline to line up with the current caret position. e.g. When the
                // caret is
                // at position 1 (zero-based), the status bar says column 2. We want to
                // add a
                // guideline for column 1 since that will place the guideline where the
                // caret is.
                return column;
            }
            catch (InvalidOperationException)
            {
                return -1;
            }
        }

        /// <summary>
        /// Find the active text view (if any) in the active document.
        /// </summary>
        /// <returns>The IVsTextView of the active view, or null if there is no active
        /// document or the
        /// active view in the active document is not a text view.</returns>
        private IVsTextView GetActiveTextView()
        {
            IVsMonitorSelection selection =
                this.ServiceProvider.GetService(typeof(IVsMonitorSelection))
                                                    as IVsMonitorSelection;
            object frameObj = null;
            ErrorHandler.ThrowOnFailure(
                selection.GetCurrentElementValue(
                    (uint)VSConstants.VSSELELEMID.SEID_DocumentFrame, out frameObj));

            IVsWindowFrame frame = frameObj as IVsWindowFrame;
            if (frame == null)
            {
                return null;
            }

            return GetActiveView(frame);
        }

        private static IVsTextView GetActiveView(IVsWindowFrame windowFrame)
        {
            if (windowFrame == null)
            {
                throw new ArgumentException("windowFrame");
            }

            object pvar;
            ErrorHandler.ThrowOnFailure(
                windowFrame.GetProperty((int)__VSFPROPID.VSFPROPID_DocView, out pvar));

            IVsTextView textView = pvar as IVsTextView;
            if (textView == null)
            {
                IVsCodeWindow codeWin = pvar as IVsCodeWindow;
                if (codeWin != null)
                {
                    ErrorHandler.ThrowOnFailure(codeWin.GetLastActiveView(out textView));
                }
            }
            return textView;
        }

        private static IWpfTextView GetTextViewFromVsTextView(IVsTextView view)
        {

            if (view == null)
            {
                throw new ArgumentNullException("view");
            }

            IVsUserData userData = view as IVsUserData;
            if (userData == null)
            {
                throw new InvalidOperationException();
            }

            object objTextViewHost;
            if (VSConstants.S_OK
                   != userData.GetData(Microsoft.VisualStudio
                                                .Editor
                                                .DefGuidList.guidIWpfTextViewHost,
                                       out objTextViewHost))
            {
                throw new InvalidOperationException();
            }

            IWpfTextViewHost textViewHost = objTextViewHost as IWpfTextViewHost;
            if (textViewHost == null)
            {
                throw new InvalidOperationException();
            }

            return textViewHost.TextView;
        }

        /// <summary>
        /// Given an IWpfTextView, find the position of the caret and report its column
        /// number. The column number is 0-based
        /// </summary>
        /// <param name="textView">The text view containing the caret</param>
        /// <returns>The column number of the caret's position. When the caret is at the
        /// leftmost column, the return value is zero.</returns>
        private static int GetCaretColumn(IWpfTextView textView)
        {
            // This is the code the editor uses to populate the status bar.
            Microsoft.VisualStudio.Text.Formatting.ITextViewLine caretViewLine =
                textView.Caret.ContainingTextViewLine;
            double columnWidth = textView.FormattedLineSource.ColumnWidth;
            return (int)(Math.Round((textView.Caret.Left - caretViewLine.Left)
                                       / columnWidth));
        }

        /// <summary>
        /// Determine the applicable column number for an add or remove command.
        /// The column is parsed from command arguments, if present. Otherwise
        /// the current position of the caret is used to determine the column.
        /// </summary>
        /// <param name="e">Event args passed to the command handler.</param>
        /// <returns>The column number. May be negative to indicate the column number is
        /// unavailable.</returns>
        /// <exception cref="ArgumentException">The column number parsed from event args
        /// was not a valid integer.</exception>
        private int GetApplicableColumn(EventArgs e)
        {
            var inValue = ((OleMenuCmdEventArgs)e).InValue as string;
            if (!string.IsNullOrEmpty(inValue))
            {
                int column;
                if (!int.TryParse(inValue, out column) || column < 0)
                    throw new ArgumentException("Invalid column");
                return column;
            }

            return GetCurrentEditorColumn();
        }

        /// <summary>
        /// This function is the callback used to execute a command when a menu item
        /// is clicked. See the Initialize method to see how the menu item is associated
        /// to this function using the OleMenuCommandService service and the MenuCommand
        /// class.
        /// </summary>
        private void AddColumnGuideExecuted(object sender, EventArgs e)
        {
            int column = GetApplicableColumn(e);
            if (column >= 0)
            {
                GuidesSettingsManager.AddGuideline(column);
            }
        }

        private void RemoveColumnGuideExecuted(object sender, EventArgs e)
        {
            int column = GetApplicableColumn(e);
            if (column >= 0)
            {
                GuidesSettingsManager.RemoveGuideline(column);
            }
        }

        private void RemoveAllGuidelinesExecuted(object sender, EventArgs e)
        {
            GuidesSettingsManager.RemoveAllGuidelines();
        }

        private void ChooseGuideColorExecuted(object sender, EventArgs e)
        {
            System.Windows.Media.Color color = GuidesSettingsManager.GuidelinesColor;

            using (System.Windows.Forms.ColorDialog picker =
                new System.Windows.Forms.ColorDialog())
            {
                picker.Color = System.Drawing.Color.FromArgb(255, color.R, color.G,
                                                             color.B);
                if (picker.ShowDialog() == System.Windows.Forms.DialogResult.OK)
                {
                    GuidesSettingsManager.GuidelinesColor =
                        System.Windows.Media.Color.FromRgb(picker.Color.R,
                                                           picker.Color.G,
                                                           picker.Color.B);
                }
            }
        }

    }
}

修复引用。 此时缺少引用。 在解决方案资源管理器中的引用节点上按右指针按钮。 选择添加 ... 命令。 添加引用对话框的右上角有一个搜索框。 输入“编辑器”(不含双引号)。 选择 Microsoft.VisualStudio.Editor 项(必须选中项左侧的框,而不仅仅是选择该项),然后选择确定添加引用。

初始化。 当包类初始化时,它会调用命令实现类的 InitializeColumnGuideCommands 初始化实例化类,并将类实例和包引用保存在类成员中。

让我们看看类构造函数中的一个命令处理程序挂接:

_addGuidelineCommand =
    new OleMenuCommand(AddColumnGuideExecuted, null,
                       AddColumnGuideBeforeQueryStatus,
                       new CommandID(ColumnGuideCommands.CommandSet,
                                     cmdidAddColumnGuide));

创建一个 OleMenuCommand。 Visual Studio 使用 Microsoft Office 命令系统。 实例化 OleMenuCommand 时的关键参数是实现命令 (AddColumnGuideExecuted) 的函数、当 Visual Studio 显示带有命令 (AddColumnGuideBeforeQueryStatus) 和命令 ID 的菜单时要调用的函数。 Visual Studio 在菜单上显示命令之前调用查询状态函数,以便该命令可以使其在菜单的特定显示中不可见或变灰(例如,如果没有选择,则禁用复制),更改其图标,甚至更改其名称(例如,从“添加内容”到“删除内容”)等等。 命令 ID 必须与 .vsct 文件中声明的命令 ID 匹配。 命令集和列参考线添加命令的字符串必须在 .vsct 文件和 ColumnGuideCommands.cs 之间匹配。

当用户通过命令窗口调用命令时,以下行提供了帮助(如下所述):

_addGuidelineCommand.ParametersDescription = "<column>";

查询状态。 查询状态函数 AddColumnGuideBeforeQueryStatusRemoveColumnGuideBeforeQueryStatus 检查一些设置(例如最大参考线数或最大列数),或者是否有要删除的列参考线。 如果条件合适,它们将启用命令。 查询状态函数需要高效,因为它们在每次 Visual Studio 显示菜单时以及菜单上的每个命令时都会运行。

AddColumnGuideExecuted 函数。 添加参考线的有趣部分是找出当前编辑器视图和插入点位置。 首先,此函数调用 GetApplicableColumn,它检查命令处理程序的事件参数中是否存在用户提供的参数;如果没有,则检查编辑器的视图:

private int GetApplicableColumn(EventArgs e)
{
    var inValue = ((OleMenuCmdEventArgs)e).InValue as string;
    if (!string.IsNullOrEmpty(inValue))
    {
        int column;
        if (!int.TryParse(inValue, out column) || column < 0)
            throw new ArgumentException("Invalid column");
        return column;
    }

    return GetCurrentEditorColumn();
}

GetCurrentEditorColumn 必须进行一些挖掘才能获取 IWpfTextView 代码视图。 如果跟踪 GetActiveTextViewGetActiveView 以及 GetTextViewFromVsTextView,可以了解如何执行此操作。 以下代码是提取的相关代码,从当前选择开始,然后获取选择的帧,然后获取帧的 DocView 作为 IVsTextView,然后从 IVsTextView 获取 IVsUserData,然后获取视图主机,最后获取 IWpfTextView:

   IVsMonitorSelection selection =
       this.ServiceProvider.GetService(typeof(IVsMonitorSelection))
           as IVsMonitorSelection;
   object frameObj = null;

ErrorHandler.ThrowOnFailure(selection.GetCurrentElementValue(
                                (uint)VSConstants.VSSELELEMID.SEID_DocumentFrame,
                                out frameObj));

   IVsWindowFrame frame = frameObj as IVsWindowFrame;
   if (frame == null)
       <<do nothing>>;

...
   object pvar;
   ErrorHandler.ThrowOnFailure(frame.GetProperty((int)__VSFPROPID.VSFPROPID_DocView,
                                                  out pvar));

   IVsTextView textView = pvar as IVsTextView;
   if (textView == null)
   {
       IVsCodeWindow codeWin = pvar as IVsCodeWindow;
       if (codeWin != null)
       {
           ErrorHandler.ThrowOnFailure(codeWin.GetLastActiveView(out textView));
       }
   }

...
   if (textView == null)
       <<do nothing>>

   IVsUserData userData = textView as IVsUserData;
   if (userData == null)
       <<do nothing>>

   object objTextViewHost;
   if (VSConstants.S_OK
           != userData.GetData(Microsoft.VisualStudio.Editor.DefGuidList
                                                            .guidIWpfTextViewHost,
                                out objTextViewHost))
   {
       <<do nothing>>
   }

   IWpfTextViewHost textViewHost = objTextViewHost as IWpfTextViewHost;
   if (textViewHost == null)
       <<do nothing>>

   IWpfTextView textView = textViewHost.TextView;

获得 IWpfTextView 后,可以获取插入符号所在的列:

private static int GetCaretColumn(IWpfTextView textView)
{
    // This is the code the editor uses to populate the status bar.
    Microsoft.VisualStudio.Text.Formatting.ITextViewLine caretViewLine =
        textView.Caret.ContainingTextViewLine;
    double columnWidth = textView.FormattedLineSource.ColumnWidth;
    return (int)(Math.Round((textView.Caret.Left - caretViewLine.Left)
                                / columnWidth));
}

当用户点击当前列时,代码只会调用设置管理器来添加或删除该列。 设置管理器将触发所有 ColumnGuideAdornment 对象都会侦听的事件。 当事件触发时,这些对象会使用新的列参考线设置更新其关联的文本视图。

从命令窗口调用命令

使用列参考线示例,用户可以从命令窗口调用两个命令,作为一种可扩展性。 如果使用视图 | 其他窗口 | 命令窗口命令,则可以看到命令窗口。 可以通过输入“edit.”与命令窗互,在命令名完成并提供参数 120 后,你将得到以下结果:

> Edit.AddColumnGuide 120
>

启用此行为的示例片段位于 .vsct 文件声明、ColumnGuideCommands 类构造函数挂钩命令处理程序时,以及检查事件参数的命令处理程序实现。

你在 .vsct 文件中看到了“<CommandFlag>CommandWellOnly</CommandFlag>”,并在编辑主菜单中看到了位置,即使这些命令没有显示在编辑菜单 UI 中。 将它们放在主编辑菜单上,可以为它们命名为 Edit.AddColumnGuide。 保存四个命令的命令组声明直接将组放置在编辑菜单上:

<Group guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup"
             priority="0xB801">
        <Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_EDIT" />
      </Group>

按钮部分稍后声明了命令 CommandWellOnly,以使其在主菜单上不可见,并用 AllowParams 声明它们:

<Button guid="guidColumnGuidesCommandSet" id="cmdidAddColumnGuide"
        priority="0x0100" type="Button">
  <Parent guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup" />
  <Icon guid="guidImages" id="bmpPicAddGuide" />
  <CommandFlag>CommandWellOnly</CommandFlag>
  <CommandFlag>AllowParams</CommandFlag>

在类构造函数中看到 ColumnGuideCommands 命令处理程序挂钩代码,该代码提供了允许的参数的说明:

_addGuidelineCommand.ParametersDescription = "<column>";

在检查当前列的编辑器视图之前,你看到了 GetApplicableColumn 函数检查 OleMenuCmdEventArgs 的值:

private int GetApplicableColumn(EventArgs e)
{
    var inValue = ((OleMenuCmdEventArgs)e).InValue as string;
    if (!string.IsNullOrEmpty(inValue))
    {
        int column;
        if (!int.TryParse(inValue, out column) || column < 0)
            throw new ArgumentException("Invalid column");
        return column;
    }

试用扩展

现在,可以按 F5 执行列参考线扩展。 打开文本文件,并使用编辑器的上下文菜单添加参考线、删除它们并更改其颜色。 在文本中单击(而不是行末的空格)以添加列参考线,或者编辑器将其添加到该行的最后一列。 如果使用命令窗口并使用参数调用命令,则可以在任意位置添加列参考线。

如果要尝试不同的命令位置、更改名称、更改图标等,并且 Visual Studio 在菜单中显示最新代码时遇到任何问题,则可以重置正在调试的实验配置单元。 打开 Windows“开始”菜单,然后键入“重置”。 查找并运行命令,重置下一个 Visual Studio 实验实例。 此命令清理所有扩展组件的实验性注册表配置单元。 它不会清除组件中的设置,因此当你的代码在下次启动时读取设置存储时,你关闭 Visual Studio 实验性配置单元时所拥有的任何参考线仍然存在。

已完成的代码项目

不久将有一个 Visual Studio 扩展性示例的 GitHub 项目,并且已完成的项目将在那里。 当这种情况发生时,本文将进行更新。 完成的示例项目可能具有不同的 GUID,并且命令图标将具有不同的位图条。

可以使用此 Visual Studio 库扩展试用列参考线功能的版本。