Udostępnij za pośrednictwem


Przewodnik: tworzenie ozdoby widoku, poleceń i ustawień (prowadnice kolumn)

Edytor tekstu/kodu programu Visual Studio można rozszerzyć za pomocą poleceń i efektów wyświetlania. W tym artykule pokazano, jak rozpocząć pracę z popularną funkcją rozszerzenia, przewodnikami kolumn. Prowadnice kolumn są wizualnie jasnymi liniami rysowanymi w widoku edytora tekstów, aby ułatwić zarządzanie kodem do określonych szerokości kolumn. W szczególności sformatowany kod może być ważny w przypadku przykładów uwzględnionych w dokumentach, wpisach w blogu lub raportach o błędach.

W tym przewodniku wykonasz następujące kroki:

  • Tworzenie projektu VSIX

  • Dodawanie ozdobności widoku edytora

  • Dodawanie obsługi zapisywania i pobierania ustawień (gdzie można rysować prowadnice kolumn i ich kolor)

  • Dodawanie poleceń (dodawanie/usuwanie prowadnic kolumn, zmienianie ich koloru)

  • Umieść polecenia w menu Edycja i menu kontekstowym dokumentu tekstowego

  • Dodawanie obsługi wywoływania poleceń w oknie poleceń programu Visual Studio

    Możesz wypróbować wersję funkcji przewodników kolumn za pomocą tego rozszerzenia galeriiprogramu Visual Studio.

    Uwaga

    W tym przewodniku wklejasz dużą ilość kodu do kilku plików wygenerowanych przez szablony rozszerzeń programu Visual Studio. Jednak wkrótce ten przewodnik będzie odwoływać się do ukończonego rozwiązania w usłudze GitHub z innymi przykładami rozszerzeń. Ukończony kod jest nieco inny, ponieważ ma prawdziwe ikony poleceń zamiast używać ikon generictemplate.

Konfiguracja rozwiązania

Najpierw należy utworzyć projekt VSIX, dodać ozdobę widoku edytora, a następnie dodać polecenie (które dodaje pakiet VSPackage do własnego polecenia). Podstawowa architektura jest następująca:

  • Masz odbiornik tworzenia widoku tekstu, który tworzy ColumnGuideAdornment obiekt na widok. Ten obiekt nasłuchuje zdarzeń dotyczących zmiany widoku lub zmiany ustawień, aktualizowania lub ponownego rysowania prowadnic kolumn w razie potrzeby.

  • Istnieje element GuidesSettingsManager , który obsługuje odczytywanie i zapisywanie z magazynu ustawień programu Visual Studio. Menedżer ustawień zawiera również operacje aktualizowania ustawień, które obsługują polecenia użytkownika (dodaj kolumnę, usuń kolumnę, zmień kolor).

  • Istnieje pakiet VSIP, który jest niezbędny, jeśli masz polecenia użytkownika, ale jest to tylko standardowy kod, który inicjuje obiekt implementacji poleceń.

  • ColumnGuideCommands Istnieje obiekt, który uruchamia polecenia użytkownika i podłącza programy obsługi poleceń dla poleceń zadeklarowanych w pliku vsct.

    VSIX. Korzystanie z pliku | Nowy... polecenie , aby utworzyć projekt. Wybierz węzeł Rozszerzalność w obszarze C# w okienku nawigacji po lewej stronie i wybierz pozycję Projekt VSIX w okienku po prawej stronie. Wprowadź nazwę ColumnGuides i wybierz przycisk OK , aby utworzyć projekt.

    Wyświetl ozdoby. Naciśnij przycisk prawego wskaźnika w węźle projektu w Eksplorator rozwiązań. Wybierz pozycję Dodaj | Nowy element ... polecenie , aby dodać nowy element zdobienia widoku. Wybieranie rozszerzalności | Edytor w okienku nawigacji po lewej stronie i wybierz pozycję Widok edytoraPrzydzielenie w okienku po prawej stronie. Wprowadź nazwę ColumnGuideAdornment jako nazwę elementu i wybierz pozycję Dodaj , aby ją dodać.

    Ten szablon elementu umożliwia dodanie dwóch plików do projektu (a także odwołań itd.): ColumnGuideAdornment.cs i ColumnGuideAdornmentTextViewCreationListener.cs. Szablony narysują fioletowy prostokąt w widoku. W poniższej sekcji zmienisz kilka wierszy w odbiorniku tworzenia widoku i zastąpisz zawartość pliku ColumnGuideAdornment.cs.

    Polecenia. W Eksplorator rozwiązań naciśnij przycisk prawego wskaźnika w węźle projektu. Wybierz pozycję Dodaj | Nowy element ... polecenie , aby dodać nowy element zdobienia widoku. Wybieranie rozszerzalności | Pakiet VSPackage w okienku nawigacji po lewej stronie i wybierz pozycję Polecenie niestandardowe w okienku po prawej stronie. Wprowadź nazwę ColumnGuideCommands jako nazwę elementu i wybierz pozycję Dodaj. Oprócz kilku odwołań dodano również polecenia i pakiet ColumnGuideCommands.cs, ColumnGuideCommandsPackage.cs i ColumnGuideCommandsPackage.vsct. W poniższej sekcji zastąp zawartość pierwszych i ostatnich plików, aby zdefiniować i zaimplementować polecenia.

Konfigurowanie odbiornika tworzenia widoku tekstu

Otwórz plik ColumnGuideAdornmentTextViewCreationListener.cs w edytorze. Ten kod implementuje procedurę obsługi za każdym razem, gdy program Visual Studio tworzy widoki tekstowe. Istnieją atrybuty, które kontrolują, gdy program obsługi jest wywoływany w zależności od cech widoku.

Kod musi również zadeklarować warstwę ozdobną. Gdy edytor aktualizuje widoki, pobiera warstwy ozdobne dla widoku i z niego pobiera elementy ozdobne. Kolejność warstwy można zadeklarować względem innych z atrybutami. Zastąp następujący wiersz:

[Order(After = PredefinedAdornmentLayers.Caret)]

z tymi dwoma wierszami:

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

Zamieniony wiersz znajduje się w grupie atrybutów, które deklarują warstwę ozdobną. Pierwszy wiersz, który został zmieniony, zmienia się tylko w miejscu, w którym są wyświetlane wiersze przewodnika kolumny. Rysowanie wierszy "przed" tekstem w widoku oznacza, że pojawiają się one za tekstem lub pod nim. Drugi wiersz deklaruje, że ozdobniki przewodnika kolumn mają zastosowanie do jednostek tekstowych pasujących do pojęcia dokumentu, ale można zadeklarować ozdobę, na przykład, aby pracować tylko dla edytowalnego tekstu. Więcej informacji można znaleźć w temacie Language service and editor extension points (Punkty rozszerzenia usługi językowej i edytora)

Implementowanie menedżera ustawień

Zastąp zawartość pliku Guides Ustawienia Manager.cs następującym kodem (wyjaśniono poniżej):

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;

    }
}

Większość tego kodu tworzy i analizuje format ustawień: "RGB(<int,int,int<>><)< int>, int, <int>>, ...". Liczby całkowite na końcu są kolumnami opartymi na jednym miejscu, w których mają być prowadnice kolumn. Rozszerzenie przewodników kolumn przechwytuje wszystkie jego ustawienia w ciągu wartości pojedynczego ustawienia.

Istnieje kilka części kodu, które warto wyróżnić. Poniższy wiersz kodu pobiera zarządzaną otokę programu Visual Studio dla magazynu ustawień. W większości przypadków jest to abstrakcja w rejestrze systemu Windows, ale ten interfejs API jest niezależny od mechanizmu magazynu.

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

Magazyn ustawień programu Visual Studio używa identyfikatora kategorii i identyfikatora ustawienia, aby jednoznacznie zidentyfikować wszystkie ustawienia:

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

Nie trzeba używać "Text Editor" jako nazwy kategorii. Możesz wybrać dowolny element.

Pierwsze kilka funkcji to punkty wejścia umożliwiające zmianę ustawień. Sprawdzają ograniczenia wysokiego poziomu, takie jak dozwolona maksymalna liczba przewodników. Następnie wywołają WriteSettingsmetodę , która komponuje ciąg ustawień i ustawia właściwość GuideLinesConfiguration. Ustawienie tej właściwości powoduje zapisanie wartości ustawień w magazynie ustawień programu Visual Studio i wyzwolenie SettingsChanged zdarzenia w celu zaktualizowania wszystkich ColumnGuideAdornment obiektów skojarzonych z widokiem tekstowym.

Istnieje kilka funkcji punktu wejścia, takich jak CanAddGuideline, które są używane do implementowania poleceń, które zmieniają ustawienia. Gdy program Visual Studio wyświetla menu, wysyła zapytania do implementacji poleceń, aby sprawdzić, czy polecenie jest obecnie włączone, jaka jest jego nazwa itd. Poniżej pokazano, jak podłączyć te punkty wejścia dla implementacji poleceń. Aby uzyskać więcej informacji na temat poleceń, zobacz Rozszerzanie menu i poleceń.

Implementowanie klasy ColumnGuideAdornment

Klasa ColumnGuideAdornment jest tworzone dla każdego widoku tekstowego, który może mieć ozdoby. Ta klasa nasłuchuje zdarzeń dotyczących zmieniania lub zmieniania ustawień widoku oraz w razie potrzeby aktualizowania lub ponownego rysowania prowadnic kolumn.

Zastąp zawartość pliku ColumnGuideAdornment.cs następującym kodem (wyjaśniono poniżej):

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);
        }
    }

}

Wystąpienia tej klasy przechowują skojarzone obiekty IWpfTextView i listę Line obiektów narysowanych w widoku.

Konstruktor (wywoływany podczas ColumnGuideAdornmentTextViewCreationListener tworzenia nowych widoków przez program Visual Studio) tworzy obiekty przewodnika Line kolumn. Konstruktor dodaje również programy obsługi dla SettingsChanged zdarzenia (zdefiniowane w GuidesSettingsManagerpliku ) oraz zdarzenia widoku LayoutChanged i Closed.

Zdarzenie LayoutChanged jest uruchamiane z powodu kilku rodzajów zmian w widoku, w tym podczas tworzenia widoku przez program Visual Studio. Procedura OnViewLayoutChanged obsługi wywołuje polecenie AddGuidelinesToAdornmentLayer do wykonania. Kod w OnViewLayoutChanged programie określa, czy musi aktualizować pozycje wierszy na podstawie zmian, takich jak zmiany rozmiaru czcionki, rynny widoku, przewijanie poziome itd. Kod w programie UpdatePositions powoduje rysowanie wierszy prowadnic między znakami lub tuż po kolumnie tekstu, która znajduje się w określonym przesunięciu znaku w wierszu tekstu.

Za każdym razem, gdy ustawienia zmienią SettingsChanged funkcję, po prostu ponownie utworzy wszystkie Line obiekty z dowolnymi nowymi ustawieniami. Po ustawieniu pozycji wiersza kod usuwa wszystkie poprzednie Line obiekty z ColumnGuideAdornment warstwy ozdobnej i dodaje nowe.

Definiowanie poleceń, menu i umieszczania menu

Istnieje wiele do deklarowania poleceń i menu, umieszczania grup poleceń lub menu w różnych innych menu i podłączania procedur obsługi poleceń. W tym przewodniku przedstawiono sposób działania poleceń w tym rozszerzeniu, ale aby uzyskać więcej informacji, zobacz Rozszerzanie menu i poleceń.

Wprowadzenie do kodu

Rozszerzenie Prowadnice kolumn pokazuje deklarowanie grupy poleceń, które należą do siebie (dodawanie kolumny, usuwanie kolumny, zmienianie koloru wiersza), a następnie umieszczenie tej grupy w menu podrzędnym menu kontekstowego edytora. Rozszerzenie Prowadnice kolumn dodaje również polecenia do głównego menu Edycja , ale zachowuje je niewidoczne, omówione jako typowy wzorzec poniżej.

Implementacja poleceń składa się z trzech części: ColumnGuideCommandsPackage.cs, ColumnGuideCommandsPackage.vsct i ColumnGuideCommands.cs. Kod wygenerowany przez szablony umieszcza polecenie w menu Narzędzia , które wyświetla okno dialogowe jako implementację. Sposób implementacji można sprawdzić w plikach .vsct i ColumnGuideCommands.cs , ponieważ jest to proste. Zastąp kod w poniższych plikach.

Kod pakietu zawiera deklaracje kociołowe wymagane dla programu Visual Studio, aby dowiedzieć się, że rozszerzenie oferuje polecenia i gdzie umieścić polecenia. Gdy pakiet inicjuje, tworzy wystąpienie klasy implementacji poleceń. Aby uzyskać więcej informacji na temat pakietów odnoszących się do poleceń, zobacz Rozszerzanie menu i poleceń.

Typowy wzorzec poleceń

Polecenia w rozszerzeniu Przewodniki kolumn są przykładem bardzo typowego wzorca w programie Visual Studio. Polecenia pokrewne należy umieścić w grupie i umieścić tę grupę w menu głównym, często z ustawieniem "<CommandFlag>CommandWellOnly</CommandFlag>", aby polecenie było niewidoczne. Umieszczenie poleceń w menu głównym (na przykład Edycja) daje im ładne nazwy (takie jak Edit.AddColumnGuide), które są przydatne do znajdowania poleceń podczas ponownego przypisywania powiązań kluczy w obszarze Opcje narzędzi. Przydatne jest również uzyskanie ukończenia podczas wywoływania poleceń z okna poleceń.

Następnie należy dodać grupę poleceń do menu kontekstowych lub menu podrzędnych, w których oczekujesz, że użytkownik będzie używać poleceń. Program Visual Studio traktuje CommandWellOnly jako flagę widoczności tylko dla menu głównych. Gdy umieszczasz tę samą grupę poleceń w menu kontekstowym lub w menu podrzędnym, polecenia są widoczne.

W ramach wspólnego wzorca rozszerzenie Przewodniki kolumn tworzy drugą grupę, która zawiera jedno menu podrzędne. Menu podrzędne z kolei zawiera pierwszą grupę z czterokolumnamiowymi poleceniami przewodnika. Druga grupa, która zawiera menu podrzędne, to zasób wielokrotnego użytku umieszczany w różnych menu kontekstowych, który umieszcza podrzędne menu w tych menu kontekstowych.

Plik vsct

Plik vsct deklaruje polecenia i miejsce ich działania wraz z ikonami itd. Zastąp zawartość pliku vsct następującym kodem (wyjaśniono poniżej):

<?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>

Identyfikatory GUID. Aby program Visual Studio znalazł programy obsługi poleceń i wywołał je, należy upewnić się, że identyfikator GUID pakietu zadeklarowany w pliku ColumnGuideCommandsPackage.cs (wygenerowany na podstawie szablonu elementu projektu) jest zgodny z identyfikatorem GUID pakietu zadeklarowanym w pliku vsct (skopiowanym z powyższego). Jeśli używasz ponownie tego przykładowego kodu, upewnij się, że masz inny identyfikator GUID, aby nie powodować konfliktu z innymi osobami, które mogły skopiować ten kod.

Znajdź ten wiersz w pliku ColumnGuideCommandsPackage.cs i skopiuj identyfikator GUID między znakami cudzysłowu:

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

Następnie wklej identyfikator GUID w pliku vsct , aby w deklaracjach był wyświetlany następujący wiersz Symbols :

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

Identyfikatory GUID zestawu poleceń i plik obrazu mapy bitowej również powinny być unikatowe dla rozszerzeń:

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

Nie trzeba jednak zmieniać zestawu poleceń i identyfikatorów GUID obrazu mapy bitowej w tym przewodniku, aby kod działał. Identyfikator GUID zestawu poleceń musi być zgodny z deklaracją w pliku ColumnGuideCommands.cs , ale zastąp też zawartość tego pliku, dlatego identyfikatory GUID będą zgodne.

Inne identyfikatory GUID w pliku vsct identyfikują istniejące menu, do których dodano polecenia przewodnika kolumny, więc nigdy się nie zmieniają.

Sekcje plików. Element .vsct zawiera trzy sekcje zewnętrzne: polecenia, umieszczanie i symbole. Sekcja poleceń definiuje grupy poleceń, menu, przyciski lub elementy menu oraz mapy bitowe dla ikon. Sekcja umieszczania deklaruje, gdzie grupy przechodzą do menu lub dodatkowych miejsc w istniejących menu. Sekcja symboli deklaruje identyfikatory używane w innym miejscu w pliku vsct, co sprawia, że kod vsct jest bardziej czytelny niż identyfikatory GUID i liczby szesnastkowy wszędzie.

Sekcja poleceń, definicje grup. Sekcja poleceń najpierw definiuje grupy poleceń. Grupy poleceń to polecenia widoczne w menu z niewielkimi szarymi liniami oddzielającym grupy. Grupa może również wypełnić całe menu podrzędne, tak jak w tym przykładzie, i nie widzisz szarych wierszy oddzielających w tym przypadku. Pliki .vsct deklarują dwie grupy, GuidesMenuItemsGroup które są nadrzędne do IDM_VS_MENU_EDIT (głównego menu Edycja ) i GuidesContextMenuGroup nadrzędne IDM_VS_CTXT_CODEWIN (menu kontekstowe edytora kodu).

Druga deklaracja grupy ma 0x0600 priorytet:

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

Chodzi o umieszczenie podrzędnego menu przewodników kolumn na końcu dowolnego menu kontekstowego, do którego dodajesz grupę menu podrzędnego. Nie należy jednak zakładać, że wiesz najlepiej i wymusić, aby menu podrzędne zawsze było ostatnie przy użyciu priorytetu .0xFFFF Musisz poeksperymentować z liczbą, aby zobaczyć, gdzie znajduje się menu podrzędne, w menu kontekstowym, w którym je umieszczasz. W tym przypadku jest wystarczająco wysoki, aby umieścić go na końcu menu tak daleko, 0x0600 jak widać, ale pozostawia miejsce dla kogoś innego, aby zaprojektować ich rozszerzenie, aby było niższe niż rozszerzenie prowadnic kolumny, jeśli jest to pożądane.

Sekcja poleceń, definicja menu. Następnie sekcja poleceń definiuje podrzędne menu GuidesSubMenu, nadrzędne do .GuidesContextMenuGroup Jest GuidesContextMenuGroup to grupa dodana do wszystkich odpowiednich menu kontekstowych. W sekcji umieszczania kod umieszcza grupę za pomocą czterokolumnach poleceń przewodnika w tym menu podrzędnym.

Sekcja poleceń, definicje przycisków. Sekcja poleceń definiuje następnie elementy menu lub przyciski, które są czterokolumna prowadnice poleceń. CommandWellOnly, omówione powyżej, oznacza, że polecenia są niewidoczne w przypadku umieszczania w menu głównym. Dwie deklaracje przycisku elementu menu (dodaj przewodnik i usuń przewodnik) mają również flagę AllowParams :

<CommandFlag>AllowParams</CommandFlag>

Ta flaga umożliwia, wraz z umieszczaniem menu głównego, polecenie do odbierania argumentów, gdy program Visual Studio wywołuje program obsługi poleceń. Jeśli użytkownik uruchamia polecenie z okna poleceń, argument jest przekazywany do programu obsługi poleceń w argumentach zdarzeń.

Sekcje poleceń, definicje map bitowych. Na koniec sekcja poleceń deklaruje mapy bitowe lub ikony używane dla poleceń. Ta sekcja jest prostą deklaracją identyfikującą zasób projektu i wyświetlającą jeden indeks używanych ikon. Sekcja symboli pliku vsct deklaruje wartości identyfikatorów używanych jako indeksy. W tym przewodniku użyto paska mapy bitowej dostarczonego z niestandardowym szablonem elementu polecenia dodanym do projektu.

Sekcja Umieszczanie. Po sekcji poleceń znajduje się sekcja umieszczania. Pierwszy z nich to miejsce, w którym kod dodaje pierwszą grupę omówionych powyżej, która zawiera cztery kolumny poleceń przewodnika do menu podrzędnego, w którym są wyświetlane polecenia:

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

Wszystkie inne umieszczania dodają GuidesContextMenuGroup element (zawierający GuidesSubMenu) do innych menu kontekstowych edytora. Gdy kod zadeklarował GuidesContextMenuGroupelement , był nadrzędny w menu kontekstowym edytora kodu. Dlatego nie widzisz położenia menu kontekstowego edytora kodu.

Sekcja symboli. Jak wspomniano powyżej, sekcja symboli deklaruje identyfikatory używane gdzie indziej w pliku vsct, co sprawia, że kod vsct jest bardziej czytelny niż identyfikatory GUID i liczby szesnastkowy wszędzie. Ważne kwestie w tej sekcji są takie, że identyfikator GUID pakietu musi być zgodny z deklaracją w klasie pakietu. Ponadto identyfikator GUID zestawu poleceń musi być zgodny z deklaracją w klasie implementacji poleceń.

Implementowanie poleceń

Plik ColumnGuideCommands.cs implementuje polecenia i podłącza programy obsługi. Gdy program Visual Studio ładuje pakiet i inicjuje go, pakiet z kolei wywołuje Initialize klasę implementacji poleceń. Polecenia inicjuje po prostu utworzenie wystąpienia klasy, a konstruktor podłącza wszystkie programy obsługi poleceń.

Zastąp zawartość pliku ColumnGuideCommands.cs następującym kodem (wyjaśniono poniżej):

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);
                }
            }
        }

    }
}

Poprawki odwołań. W tym momencie brakuje odwołania. Naciśnij prawy wskaźnik w węźle Odwołania w Eksplorator rozwiązań. Wybierz polecenie Dodaj ... . Okno dialogowe Dodawanie odwołania zawiera pole wyszukiwania w prawym górnym rogu. Wprowadź "editor" (bez podwójnych cudzysłowów). Wybierz element Microsoft.VisualStudio.Editor (należy zaznaczyć pole wyboru po lewej stronie elementu, a nie tylko wybrać element) i wybrać przycisk OK, aby dodać odwołanie.

Inicjowanie. Gdy klasa pakietu inicjuje, wywołuje Initialize klasę implementacji poleceń. Inicjowanie ColumnGuideCommands tworzy wystąpienie klasy i zapisuje wystąpienie klasy oraz odwołanie do pakietu w składowych klasy.

Przyjrzyjmy się jednemu z podpięć programu obsługi poleceń z konstruktora klasy:

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

Utworzysz element OleMenuCommand. Program Visual Studio używa systemu poleceń pakietu Microsoft Office. Kluczowymi argumentami podczas tworzenia wystąpienia OleMenuCommand elementu jest funkcja, która implementuje polecenie (AddColumnGuideExecuted), funkcję do wywołania, gdy program Visual Studio wyświetla menu z poleceniem (AddColumnGuideBeforeQueryStatus) i identyfikatorem polecenia. Program Visual Studio wywołuje funkcję stanu zapytania przed wyświetleniem polecenia w menu, aby polecenie było niewidoczne lub wyszarzone dla określonego wyświetlania menu (na przykład wyłączenie opcji Kopiuj , jeśli nie ma zaznaczenia), zmień jej ikonę, a nawet zmień jego nazwę (na przykład z dodaj coś do usunięcia). i tak dalej. Identyfikator polecenia musi być zgodny z identyfikatorem polecenia zadeklarowanym w pliku vsct . Ciągi zestawu poleceń i prowadnice kolumn dodają polecenie muszą być zgodne między plikiem vsct a kolumną ColumnGuideCommands.cs.

Poniższy wiersz zapewnia pomoc w przypadku wywoływania polecenia przez użytkowników za pośrednictwem okna poleceń (wyjaśnione poniżej):

_addGuidelineCommand.ParametersDescription = "<column>";

Stan zapytania. Funkcje AddColumnGuideBeforeQueryStatus stanu zapytania i RemoveColumnGuideBeforeQueryStatus sprawdź niektóre ustawienia (takie jak maksymalna liczba prowadnic lub maksymalna kolumna) lub jeśli istnieje przewodnik po kolumnie do usunięcia. Włączają polecenia, jeśli warunki są prawidłowe. Funkcje stanu zapytania muszą być wydajne, ponieważ są uruchamiane za każdym razem, gdy program Visual Studio wyświetla menu i dla każdego polecenia w menu.

AddColumnGuideExecuted, funkcja. Interesującą częścią dodawania przewodnika jest ustalenie bieżącego widoku edytora i lokalizacji karetki. Najpierw ta funkcja wywołuje GetApplicableColumnmetodę , która sprawdza, czy w argumentach zdarzeń programu obsługi poleceń znajduje się argument dostarczony przez użytkownika, a jeśli nie ma ich, funkcja sprawdza widok edytora:

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 aby uzyskać IWpfTextView wgląd w kod, trzeba się trochę zagłębić. Jeśli prześlesz ślad za pomocą GetActiveTextView, GetActiveViewi GetTextViewFromVsTextView, możesz zobaczyć, jak to zrobić. Poniższy kod jest odpowiednim kodem abstrakcyjnym, zaczynając od bieżącego zaznaczenia, a następnie uzyskując ramkę zaznaczenia, a następnie uzyskując ramkę DocView jako IVsTextViewelement , a następnie uzyskując element IVsUserData z widoku IVsTextView, a następnie uzyskując hosta widoku, a na koniec 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;

Gdy masz widok IWpfTextView, możesz uzyskać kolumnę, w której znajduje się daszek:

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));
}

Po kliknięciu bieżącej kolumny, w której użytkownik kliknął, kod po prostu wywołuje menedżera ustawień, aby dodać lub usunąć kolumnę. Menedżer ustawień uruchamia zdarzenie, do którego nasłuchują wszystkie ColumnGuideAdornment obiekty. Gdy zdarzenie zostanie wyzwolony, te obiekty aktualizują skojarzone widoki tekstowe przy użyciu nowych ustawień przewodnika po kolumnie.

Wywołaj polecenie w oknie polecenia

Przykładowe przewodniki kolumn umożliwiają użytkownikom wywoływanie dwóch poleceń z okna poleceń jako formy rozszerzalności. Jeśli używasz widoku | Inne systemy Windows | Polecenie Okno polecenia, można wyświetlić okno polecenia. Możesz wchodzić w interakcję z oknem poleceń, wprowadzając ciąg "edit." i z uzupełnianiem nazwy polecenia i podając argument 120, otrzymasz następujący wynik:

> Edit.AddColumnGuide 120
>

Fragmenty przykładu, które umożliwiają to zachowanie, znajdują się w deklaracjach plików vsct , ColumnGuideCommands konstruktora klasy podczas podłączania procedur obsługi poleceń i implementacji programu obsługi poleceń, które sprawdzają argumenty zdarzeń.

W pliku vsct zobaczyliśmy ciąg "<CommandFlag>CommandWellOnly</CommandFlag>", a także umieszczanie w menu głównym Edytuj, mimo że polecenia nie są wyświetlane w interfejsie użytkownika menu Edycja. Posiadanie ich w głównym menu Edycja daje im nazwy, takie jak Edit.AddColumnGuide. Deklaracja grupy poleceń zawierająca cztery polecenia umieszczone w grupie bezpośrednio w menu Edycja :

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

Sekcja przycisków później zadeklarowała polecenia CommandWellOnly , aby zachować je niewidoczne w menu głównym i zadeklarować je za pomocą polecenia 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>

Zobaczyliśmy, że program obsługi poleceń podłącza kod w konstruktorze ColumnGuideCommands klasy pod warunkiem opisu dozwolonego parametru:

_addGuidelineCommand.ParametersDescription = "<column>";

GetApplicableColumn Funkcja sprawdza OleMenuCmdEventArgs wartość przed sprawdzeniem widoku edytora dla bieżącej kolumny:

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;
    }

Wypróbuj rozszerzenie

Teraz możesz nacisnąć klawisz F5 , aby wykonać rozszerzenie Prowadnice kolumn. Otwórz plik tekstowy i użyj menu kontekstowego edytora, aby dodać wiersze przewodnika, usunąć je i zmienić kolor. Kliknij tekst (nie biały znak przekazany na końcu wiersza), aby dodać prowadnicę kolumny, lub edytor dodaje go do ostatniej kolumny w wierszu. Jeśli używasz okna poleceń i wywołujesz polecenia z argumentem, możesz dodać prowadnice kolumn w dowolnym miejscu.

Jeśli chcesz wypróbować różne umieszczanie poleceń, zmienić nazwy, zmienić ikony itd., a masz problemy z programem Visual Studio pokazującym najnowszy kod w menu, możesz zresetować eksperymentalną gałąź, w której debugujesz. Uruchom menu Start systemu Windows i wpisz "reset". Wyszukaj i uruchom polecenie Resetuj następne wystąpienie eksperymentalne programu Visual Studio. To polecenie czyści gałąź rejestru eksperymentalnego wszystkich składników rozszerzenia. Ustawienia nie są czyszczone ze składników, więc wszystkie przewodniki po zamknięciu eksperymentalnej gałęzi programu Visual Studio są nadal dostępne, gdy kod odczytuje magazyn ustawień podczas następnego uruchamiania.

Ukończony projekt kodu

Wkrótce pojawi się projekt GitHub przykładów rozszerzalności programu Visual Studio, a ukończony projekt będzie dostępny. Ten artykuł zostanie zaktualizowany, aby wskazać, kiedy tak się stanie. Ukończony przykładowy projekt może mieć różne identyfikatory GUID i będzie miał inny pasek map bitowych dla ikon poleceń.

Możesz wypróbować wersję funkcji przewodników kolumn za pomocą tego rozszerzenia galeriiprogramu Visual Studio.