共用方式為


逐步解說︰建立檢視裝飾、命令和設定 (分欄輔助線)

您可以使用命令和檢視效果來擴充 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 Managed 包裝函式。 在大多數情況下,這會在 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),以及檢視事件 LayoutChangedClosed 新增處理常式。

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 檔案宣告兩個群組,GuidesMenuItemsGroup 群組的上層是IDM_VS_MENU_EDIT (主編輯功能表),而 GuidesContextMenuGroup 群組的上層則是 IDM_VS_CTXT_CODEWIN (程式碼編輯器的內容功能表)。

第二個群組宣告具有 0x0600 優先順序:

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

其概念是將分欄輔助線子功能表,放在您新增子功能表群組的任何內容功能表的結尾。 但是,您不應該假設您最了解並透過使用優先順序 0xFFFF 強制將子功能表始終排在最後。 您必須進行實驗,以查看您的子功能表出現在您放置它的內容功能表中的位置。 在此情況下,0x0600 是夠高的,可以將它放在您能看到的功能表的末尾,但如果需要,也可以為其他人留出空間,使其設計的擴充功能的優先順序低於分欄輔助線擴充功能。

命令區段,功能表定義。 接下來,命令區段會定義子選單 GuidesSubMenu,它的上層是 GuidesContextMenuGroupGuidesContextMenuGroup 是您新增至所有相關內容功能表的群組。 在位置區段中,程式碼會將具有四欄輔助線命令的群組放在這個子功能表上。

命令區段、按鈕定義。 命令區段接著會定義四欄輔助線命令的功能表項目或按鈕。 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。 命令初始化只會具現化類別,而建構函式會連結所有命令處理常式。

使用下列程式碼取代 GuidesSettingsManager.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) 和命令識別碼時所要呼叫的。 Visual Studio 會先呼叫查詢狀態函式,再顯示功能表上的命令,以便命令可以在功能表的特定顯示中隱藏或變灰 (例如,如果沒有選取項目,停用 複製)、變更其圖示,或甚至變更其名稱 (例如,從 [新增某個項目] 變更為 [移除某個項目] )等等。 命令識別碼必須符合 .vsct 檔案中宣告的命令識別碼。 命令集和分欄輔助線新增命令的字串必須在 .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 建構函式以及檢查事件引數的命令處理常式的實作中。

即使命令未顯示在編輯功能表 UI 中,您也會在 .vsct 檔案中看到「<CommandFlag>CommandWellOnly</CommandFlag>」以及編輯主選單中的位置。 將它們放在主編輯功能表上會為它們命名類似 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 的 [開始] 功能表 ,然後輸入「reset」。 尋找並執行命令,重設下一個Visual Studio 實驗執行個體。 此命令會清除所有擴充功能元件的實驗登錄區。 它不會清除元件中的設定,所以當您的程式碼在下次啟動時讀取設定存放區時,關閉 Visual Studio 的實驗登錄區時所擁有的任何輔助線都會保留在那裡。

已完成的程式碼專案

很快就會有一個 Visual Studio 可擴充性範例的 GitHub 專案,完成的專案也會在那裡。 當這種情況發生時,本文將進行更新以指出這一點。 已完成的範例專案可能會有不同的 guid,而且命令圖示會有不同的點陣圖條。

您可以使用這個 Visual Studio 組件庫擴充功能來試用分欄輔助線功能的版本。