共用方式為


逐步解說:建立檢視裝飾、命令及設定(欄位指引)

您可以使用命令和檢視效果來擴充 Visual Studio 文字/程式代碼編輯器。 本文說明如何開始使用熱門的擴充功能:欄位指引。 欄位導引線是在文字編輯器介面上以視覺上呈現的輕線條,協助您將程式碼管理到特定的欄位寬度。 具體而言,格式化的程式代碼對於您在檔、部落格文章或 Bug 報告中所包含的範例而言非常重要。

在此操作指南中,您將:

  • 建立 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,然後在右窗格中選擇 [自定義命令]。 輸入 name 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 thread, 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 管理包裝函式以存取設定儲存。 在大多數情況下,這會在 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 有三個外部區段:命令、位置和符號。 命令區段會定義命令群組、功能表、按鈕或功能表項,以及圖示的點陣圖。 放置區段宣告群組在功能表中的位置,或新增位置到既有的功能表上。 symbols 區段會宣告 .vsct 檔案中其他地方使用的標識符,這使得 .vsct 程式代碼比在任何地方擁有 GUID 和十六進位數位更容易閱讀。

Commands 區段,群組定義。 命令區段會先定義命令群組。 命令群組是您在功能表中看到的命令,其中略有灰色線條會分隔群組。 群組也可能填滿整個子功能表,如本範例所示,在此案例中看不到灰色分隔線。 .vsct 檔案會宣告兩個群組:隸屬於 IDM_VS_MENU_EDITGuidesMenuItemsGroup(主要 [編輯] 功能表),以及隸屬於 IDM_VS_CTXT_CODEWINGuidesContextMenuGroup(程式代碼編輯器的操作功能表)。

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

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

這個想法是將欄位指引子功能表放在您新增子功能表群組之任何右鍵選單的結尾。 但是,您不應該自以為是而假設自己最清楚,並強制子選單永遠是最後一個,這是通過使用 0xFFFF的優先順序來實現的。 您必須實驗數位,以查看子菜單位於您放置它的內容功能表上的位置。 在這種情況下,0x0600 足夠高,可以將它放在選單的末尾,但如果覺得有必要,仍然留有空間給其他人設計其擴展模組,使其低於欄位指引擴展模組。

命令區段,選單定義。 接下來,命令區段會定義子選單 GuidesSubMenu,其父系為 GuidesContextMenuGroupGuidesContextMenuGroup 是您新增至所有相關內容功能表的群組。 在位置設定區段中,程式代碼會將四欄指南命令的群組放在這個子選單上。

命令區段,按鈕定義。 命令區段接著會定義作為四欄導線命令的功能表項或按鈕。 如上所述,CommandWellOnly表示當命令放置在主菜單上時是不可見的。 其中兩個選單項目按鈕宣告(新增指南和移除指南)也有 AllowParams 標記:

<CommandFlag>AllowParams</CommandFlag>

此旗標不僅會啟用主選單中的位置,還允許在 Visual Studio 執行命令處理程式時接收自變數的命令。 如果使用者從命令視窗執行命令,自變數會傳遞至事件自變數中的命令處理程式。

命令區段,點陣圖定義。 最後,命令區段會宣告用於命令的點陣圖或圖示。 本節是一個簡單的宣告,可識別專案資源,並列出單一索引的已使用圖示。 .vsct 檔案的符號區段宣告用作索引的識別碼值。 本引導式說明會使用新增至專案的自訂命令項目範本所提供的點陣圖圖條。

Placements 區段。 命令區段之後是 placements 區段。 第一個是程式代碼將上述討論的第一個群組,包含四欄指南命令,新增至命令出現的子功能表中:

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

所有其他位置都會將 GuidesContextMenuGroup(其中包含 GuidesSubMenu)新增至其他編輯器環境功能表。 當程式代碼宣告 GuidesContextMenuGroup時,它會父系至程式代碼編輯器的操作功能表。 這就是為什麼您看不到程式代碼編輯器操作功能表的位置。

符號區段。 如上所述,symbols 區段會宣告 .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),以及命令識別碼。 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 檢視。 如果您追蹤 GetActiveTextViewGetActiveViewGetTextViewFromVsTextView,您可以查看如何執行此作業。 下列程式碼是在目前選取範圍中擷取的相關程式碼:首先,取得選取範圍的框架,接著將框架的 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 物件接聽的事件。 當事件觸發時,這些物件會更新與它們相關的文字視圖為新的欄位指南設定。

從命令視窗叫用命令

欄位指引範例可讓使用者從命令視窗中執行兩個命令,作為擴充功能的一種方法。 如果您使用 檢視 |其他 Windows |命令視窗 命令,您可以看到命令視窗。 您可以輸入 「edit.」 來與命令視窗互動,並使用命令名稱完成並提供自變數 120,您有下列結果:

> Edit.AddColumnGuide 120
>

啟用此行為的範例片段位於 .vsct 檔案宣告、連結命令處理程式時 ColumnGuideCommands 類別建構函式,以及檢查事件自變數的命令處理程序實作。

您在 .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 開始功能表,然後輸入「重設」。 尋找並執行命令,重設下一個 Visual Studio 實驗實例。 此命令會清除所有擴充元件的實驗性登錄區。 它不會從元件中移除設定,因此當您關閉 Visual Studio 的實驗區時,您所設置的任何指引在下次啟動時仍會保留在設定存儲中。

已完成的程式代碼專案

即將會有 Visual Studio 擴充性範例的 GitHub 專案,而且已完成的專案將會在 GitHub 上。 本文將在情況發生後更新,以指向該處。 已完成的範例專案可能會有不同的 GUID,並且用於命令的圖示會有不同的點陣圖序列。

您可以使用這個 Visual Studio 資源庫 擴充功能來試用欄位指引功能的版本,