다음을 통해 공유


연습: 보기 장식, 명령 및 설정 만들기(열 안내선)

명령 및 뷰 효과를 사용하여 Visual Studio 텍스트/코드 편집기를 확장할 수 있습니다. 이 문서에서는 인기 있는 확장 기능인 열 안내선을 시작하는 방법을 보여 줍니다. 열 안내선은 특정 열 너비에 대한 코드를 관리하는 데 도움이 되도록 텍스트 편집기 뷰에 시각적으로 표시되는 밝은 선입니다. 특히 서식이 지정된 코드는 문서, 블로그 게시물 또는 버그 보고서에 포함하는 샘플에 중요할 수 있습니다.

이 연습에서는 다음과 같은 작업을 수행합니다.

  • VSIX 프로젝트 만들기

  • 편집기 뷰 도구 영역 추가

  • 설정 저장 및 가져오기에 대한 지원 추가(열 안내선 및 해당 색을 그릴 위치)

  • 명령 추가(열 안내선 추가/제거, 색 변경)

  • 편집 메뉴 및 텍스트 문서 상황에 맞는 메뉴에 명령을 배치합니다.

  • Visual Studio 명령 창에서 명령 호출에 대한 지원 추가

    이 Visual Studio 갤러리 확장을 사용하여 열 안내선 기능의 버전을 사용해 볼 수 있습니다.

    참고 항목

    이 연습에서는 Visual Studio 확장 템플릿에서 생성된 몇 가지 파일에 많은 양의 코드를 붙여넣습니다. 그러나 곧 이 연습에서는 GitHub에서 완료된 솔루션을 다른 확장 예제와 함께 참조합니다. 완성된 코드는 generictemplate 아이콘을 사용하는 대신, 실제 명령 아이콘이 있다는 측면에서 약간 다릅니다.

솔루션 설정

먼저 VSIX 프로젝트를 만들고 편집기 뷰 도구 영역을 추가한 다음, 명령을 추가합니다(명령을 소유하기 위해 VSPackage 추가). 기본 아키텍처는 다음과 같습니다.

  • 뷰당 ColumnGuideAdornment 개체를 만드는 텍스트 뷰 만들기 수신기가 있습니다. 이 개체는 필요에 따라 뷰 변경 또는 설정 변경, 열 안내서 업데이트 또는 다시 그리기에 대한 이벤트를 수신 대기합니다.

  • Visual Studio 설정 스토리지에서 읽기 및 쓰기를 처리하는 GuidesSettingsManager가 있습니다. 설정 관리자에는 사용자 명령(열 추가, 열 제거, 색 변경)을 지원하는 설정을 업데이트하는 작업도 있습니다.

  • 사용자 명령이 있는 경우 필요한 VSIP 패키지가 있지만 명령 구현 개체를 초기화하는 상용구 코드일 뿐입니다.

  • 사용자 명령을 실행하고 .vsct 파일에 선언된 명령에 대한 명령 처리기를 연결하는 ColumnGuideCommands 개체가 있습니다.

    VSIX. 파일 | 새로 만들기... 명령을 사용하여 프로젝트를 만듭니다. 왼쪽 탐색 창의 C#에서 확장성 노드를 선택하고 오른쪽 창에서 VSIX 프로젝트를 선택합니다. 이름 ColumnGuides를 입력하고 확인을 선택하여 프로젝트를 만듭니다.

    뷰 도구 영역. 솔루션 탐색기 프로젝트 노드에서 오른쪽 포인터 단추를 누릅니다. 추가 | 새 항목... 명령을 사용하여 새 뷰 도구 영역 항목을 추가합니다. 왼쪽 탐색 창에서 확장성 | 편집기를 선택하고 오른쪽 창에서 편집기 뷰포트 도구 영역을 선택합니다. 이름 ColumnGuideAdornment를 항목 이름으로 입력하고 추가를 선택하여 추가합니다.

    프로젝트에 두 개의 파일(및 참조 등) ColumnGuideAdornment.csColumnGuideAdornmentTextViewCreationListener.cs가 추가된 이 항목 템플릿을 볼 수 있습니다. 템플릿은 뷰에 자주색 사각형을 그립니다. 다음 섹션에서는 뷰 만들기 수신기에서 몇 개의 줄을 변경하고 ColumnGuideAdornment.cs의 내용을 바꿉니다.

    명령. 솔루션 탐색기의 프로젝트 노드에서 오른쪽 포인터 단추를 누릅니다. 추가 | 새 항목... 명령을 사용하여 새 뷰 도구 영역 항목을 추가합니다. 왼쪽 탐색 창의 확장성 | VSPackage를 선택하고 오른쪽 창에서 사용자 지정 명령을 선택합니다. ColumnGuideCommands를 항목 이름으로 입력하고 추가를 선택합니다. 여러 참조 외에도 명령 및 패키지를 추가하면 ColumnGuideCommands.cs, ColumnGuideCommandsPackage.csColumnGuideCommandsPackage.vsct도 추가됩니다. 다음 섹션에서는 첫 번째 파일과 마지막 파일의 내용을 바꾸어 명령을 정의하고 구현합니다.

텍스트 뷰 만들기 수신기 설정

편집기에서 기에서 ColumnGuideAdornmentTextViewCreationListener.cs를 엽니다. 이 코드는 Visual Studio에서 텍스트 뷰를 만들 때마다 처리기를 구현합니다. 뷰의 특성에 따라 처리기가 호출되는 시기를 제어하는 특성이 있습니다.

또한 코드는 도구 영역 계층을 선언해야 합니다. 편집기에서 뷰를 업데이트할 때 뷰에 대한 도구 영역 계층을 가져오고 해당 계층에서 도구 영역 요소를 가져옵니다. 특성이 있는 다른 계층을 기준으로 계층의 순서를 선언할 수 있습니다. 다음 줄을 바꿉니다.

[Order(After = PredefinedAdornmentLayers.Caret)]

다음 두 줄로 바꿉니다.

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

바꾼 줄은 도구 영역 계층을 선언하는 특성 그룹에 있습니다. 변경한 첫 번째 줄은 열 안내선이 표시되는 위치만 변경합니다. 뷰에서 텍스트 "앞에" 선을 그리면 텍스트 뒤 또는 아래에 표시됩니다. 두 번째 줄은 열 안내선 도구 영역이 문서의 개념에 맞는 텍스트 엔터티에 적용될 수 있다고 선언하지만, 예를 들어 도구 영역이 편집 가능한 텍스트에 대해서만 작동하도록 선언할 수 있습니다. 언어 서비스 및 편집기 확장 지점에 추가 정보가 있습니다.

설정 관리자 구현

GuidesSettingsManager.cs의 내용을 다음 코드(아래에 설명)로 바꿉니다.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    }
}

이 코드의 대부분은 설정 형식을 만들고 구문 분석합니다. "RGB(<int>,<int>,<int>) <int>, <int>, ...". 끝에 있는 정수는 열 안내선을 표시하려는 1부터 시작하는 열입니다. 열 안내선 확장은 모든 설정을 단일 설정 값 문자열로 캡처합니다.

코드 일부는 강조 표시하면 유용할 수 있습니다. 다음 코드 줄은 설정 스토리지에 대한 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에 대한 처리기를 추가합니다.

Visual Studio에서 뷰를 만드는 경우를 비롯하여 뷰의 각종 변경으로 인해 LayoutChanged 이벤트가 발생합니다. 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를 주 메뉴에 대해서만 표시 안 함 플래그로 처리합니다. 상황에 맞는 메뉴 또는 하위 메뉴에 동일한 명령 그룹을 배치하면 명령이 표시됩니다.

공통 패턴의 일부로, 열 안내선 확장은 단일 하위 메뉴를 포함하는 두 번째 그룹을 만듭니다. 그러면 하위 메뉴에는 4열 안내선 명령이 있는 첫 번째 그룹이 포함됩니다. 하위 메뉴를 포함하는 두 번째 그룹은 다양한 상황에 맞는 메뉴에 배치하는 재사용 가능한 자산으로, 해당 상황에 맞는 메뉴에 하위 메뉴를 배치합니다.

.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";

그런 다음, Symbols 선언에 다음 줄이 오도록 GUID를 .vsct 파일에 붙여넣습니다.

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

명령 집합 및 비트맵 이미지 파일의 GUID도 확장에 대해 고유해야 합니다.

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

그러나 코드가 작동하려면 이 연습에서 명령 집합 및 비트맵 이미지 GUID를 변경할 필요가 없습니다. 명령 집합 GUID는 ColumnGuideCommands.cs 파일의 선언과 일치해야 하지만 해당 파일의 내용도 바꿉니다. 따라서 GUID가 일치합니다.

.vsct 파일의 다른 GUID는 열 안내선 명령이 추가되는 기존 메뉴를 식별하므로 변경되지 않습니다.

파일 섹션. .vsct에는 명령, 배치 및 기호의 세 가지 외부 섹션이 있습니다. 명령 섹션은 명령 그룹, 메뉴, 단추 또는 메뉴 항목 및 아이콘에 대한 비트맵을 정의합니다. 배치 섹션은 그룹이 메뉴에서 이동하는 위치 또는 추가 배치가 기존 메뉴에서 이동하는 위치를 선언합니다. 기호 섹션은 .vsct 파일의 다른 곳에서 사용되는 식별자를 선언합니다. 이를 통해 GUID 및 16진수를 사용하는 경우보다 .vsct 코드를 더 쉽게 읽을 수 있습니다.

명령 섹션, 그룹 정의. 명령 섹션은 먼저 명령 그룹을 정의합니다. 명령 그룹은 메뉴에 표시되며 그룹을 구분하는 흐린 회색 선이 있는 명령입니다. 이 예제와 같이 그룹이 전체 하위 메뉴를 채울 수도 있으며 이 경우 회색 구분선이 표시되지 않습니다. .vsct 파일은 두 그룹을 선언합니다. 하나는 IDM_VS_MENU_EDIT의 부모가 되는 GuidesMenuItemsGroup(주 편집 메뉴)이고, 다른 하나는 IDM_VS_CTXT_CODEWIN의 부모가 되는 GuidesContextMenuGroup(코드 편집기 상황에 맞는 메뉴)입니다.

두 번째 그룹 선언에는 0x0600 우선 순위가 적용됩니다.

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

하위 메뉴 그룹을 추가할 상황에 맞는 메뉴의 끝에 열 안내선 하위 메뉴를 배치하는 것이 좋습니다. 그러나 가장 잘 알고 있다고 가정하고 0xFFFF 우선 순위를 사용하여 하위 메뉴가 항상 마지막이 되도록 해서는 안 됩니다. 해당 숫자로 실험하면서 상황에 맞는 메뉴에서 하위 메뉴가 배치되는 위치를 확인해야 합니다. 이 경우 0x0600은 사용자가 볼 수 있는 한, 메뉴의 맨 끝에 메뉴가 배치될 수 있을 만큼 충분히 높은 숫자이지만, 필요한 경우 다른 사용자가 해당 확장을 열 안내선 확장보다 낮게 디자인할 수 있는 공간이 확보됩니다.

명령 섹션, 메뉴 정의. 다음으로, 명령 섹션은 GuidesContextMenuGroup의 부모로 지정된 하위 메뉴 GuidesSubMenu를 정의합니다. GuidesContextMenuGroup은 모든 관련 상황에 맞는 메뉴에 추가하는 그룹입니다. 배치 섹션에서 코드는 이 하위 메뉴에 4열 안내선 명령이 있는 그룹을 배치합니다.

명령 섹션, 단추 정의. 그런 다음, 명령 섹션에서는 4열 안내선 명령인 메뉴 항목 또는 단추를 정의합니다. 위에서 설명한 CommandWellOnly는 명령이 주 메뉴에 배치될 때 보이지 않는 것을 의미합니다. 메뉴 항목 단추 선언 중 두 개(안내선 추가 및 안내선 제거)에는 AllowParams 플래그도 있습니다.

<CommandFlag>AllowParams</CommandFlag>

주 메뉴 배치와 함께 이 플래그를 사용하면 Visual Studio에서 명령 처리기를 호출할 때 명령이 인수를 수신할 수 있습니다. 사용자가 명령 창에서 명령을 실행하면 인수가 이벤트 인수의 명령 처리기에 전달됩니다.

명령 섹션, 비트맵 정의. 마지막으로 명령 섹션에서는 명령에 사용되는 비트맵 또는 아이콘을 선언합니다. 이 섹션은 프로젝트 리소스를 식별하고 사용된 1부터 시작되는 사용된 아이콘 인덱스를 나열하는 단순한 선언입니다. .vsct 파일의 기호 섹션은 인덱스로 사용되는 식별자의 값을 선언합니다. 이 연습에서는 프로젝트에 추가된 사용자 지정 명령 항목 템플릿과 함께 제공되는 비트맵 스트립을 사용합니다.

배치 섹션. 명령 섹션 다음에는 배치 섹션이 있습니다. 첫 번째 섹션은 코드가 위에서 설명한 첫 번째 그룹을 추가하는 위치로, 명령이 표시되는 하위 메뉴에 4열 안내선 명령을 유지합니다.

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

다른 모든 배치는 다른 편집기 상황에 맞는 메뉴에 GuidesContextMenuGroup(GuidesSubMenu 포함)을 추가합니다. 코드가 GuidesContextMenuGroup을 선언되었을 때 코드 편집기 상황에 맞는 메뉴의 부모로 지정되었습니다. 바로 이때문에 코드 편집기 상황에 맞는 메뉴에 대한 배치가 표시되지 않는 것입니다.

기호 섹션. 위에서 설명한 대로 기호 섹션은 .vsct 파일의 다른 곳에서 사용되는 식별자를 선언합니다. 이를 통해 GUID 및 16진수를 사용하는 경우보다 .vsct 코드를 더 쉽게 읽을 수 있습니다. 이 섹션의 중요한 점은 패키지 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);
                }
            }
        }

    }
}

참조 수정. 이 시점에서 참조가 누락되었습니다. 솔루션 탐색기의 참조 노드에서 오른쪽 포인터 단추를 누릅니다. 추가... 명령을 선택합니다. 참조 추가 대화 상자의 오른쪽 위 모서리에 검색 상자가 있습니다. 큰따옴표 없이 "editor"를 입력합니다. Microsoft.VisualStudio.Editor 항목을 선택하고(항목을 선택하는 것이 아니라 항목 왼쪽에 있는 확인란을 선택해야 함) 확인을 선택하여 참조를 추가합니다.

초기화. 패키지 클래스가 초기화되면 명령 구현 클래스에 대해 Initialize를 호출합니다. ColumnGuideCommands 초기화는 클래스를 인스턴스화하고 클래스 인스턴스와 패키지 참조를 클래스 멤버에 저장합니다.

클래스 생성자의 명령 처리기 후크업 중 하나를 살펴보겠습니다.

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

OleMenuCommand를 만듭니다. Visual Studio는 Microsoft Office 명령 시스템을 사용합니다. OleMenuCommand를 인스턴스화할 때 핵심 인수는 명령(AddColumnGuideExecuted), Visual Studio에서 명령(AddColumnGuideBeforeQueryStatus)이 있는 메뉴를 표시할 때 호출할 함수 및 명령 ID를 구현하는 함수입니다. Visual Studio는 메뉴에 명령을 표시하기 전에 쿼리 상태 함수를 호출하여 메뉴의 특정 표시에 대해 명령 자체를 보이지 않게 하거나 회색으로 표시하거나(예: 선택 사항이 없는 경우 복사 사용 안 함), 아이콘을 변경하거나 이름을 변경(예: Add Something에서 Remove Something으로)하는 등의 작업을 수행할 수 있습니다. 명령 ID는 .vsct 파일에 선언된 명령 ID와 일치해야 합니다. 명령 집합 및 열 안내선 추가 명령에 대한 문자열은 .vsct 파일과 ColumnGuideCommands.cs 간에 일치해야 합니다.

다음 줄에서는 사용자가 명령 창을 통해 명령을 호출할 때 도움이 됩니다(아래에 설명).

_addGuidelineCommand.ParametersDescription = "<column>";

쿼리 상태. 쿼리 상태 함수 AddColumnGuideBeforeQueryStatusRemoveColumnGuideBeforeQueryStatus는 일부 설정(예: 최대 안내선 수 또는 최대 열 수)을 확인하거나 제거할 열 안내선이 있는지 확인합니다. 조건이 맞으면 명령을 사용하도록 설정합니다. 쿼리 상태 함수는 Visual Studio에서 메뉴를 표시할 때마다 그리고 메뉴의 각 명령에 대해 실행되므로 효율적이어야 합니다.

AddColumnGuideExecuted 함수. 안내선 추가의 흥미로운 부분은 현재 편집기 뷰 및 캐럿 위치를 파악하는 것입니다. 먼저 이 함수는 명령 처리기의 이벤트 인수에 사용자가 제공한 인수가 있는지 확인하는 GetApplicableColumn을 호출하고, 없는 경우 편집기의 뷰를 확인합니다.

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

    return GetCurrentEditorColumn();
}

GetCurrentEditorColumn은 코드의 IWpfTextView 뷰를 가져오기 위해 상세히 확인해야 합니다. GetActiveTextView, GetActiveViewGetTextViewFromVsTextView를 추적하는 경우 이 작업을 수행하는 방법을 볼 수 있습니다. 다음 코드는 현재 선택 영역부터 시작하여 선택 영역의 프레임을 가져오고, 프레임의 DocView를 IVsTextView로 가져오고, IVsTextView에서 IVsUserData를 가져오고, 뷰 호스트를 가져온 후 마지막으로 IWpfTextView를 가져오는 추출된 관련 코드입니다.

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

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

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

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

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

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

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

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

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

   IWpfTextView textView = textViewHost.TextView;

IWpfTextView가 있으면 캐럿이 있는 열을 가져올 수 있습니다.

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

사용자가 클릭한 현재 열이 있는 상태에서 코드는 설정 관리자를 호출하여 열을 추가하거나 제거합니다. 설정 관리자는 모든 ColumnGuideAdornment 개체가 수신 대기하는 이벤트를 발생시킵니다. 이벤트가 발생하면 이러한 개체는 연결된 텍스트 뷰를 새 열 안내선 설정으로 업데이트합니다.

명령 창에서 명령 호출

열 안내선 샘플을 사용하면 명령 창의 두 명령을 확장성 형태로 호출할 수 있습니다. 보기 | 기타 창 | 명령 창 명령을 사용하는 경우 명령 창을 볼 수 있습니다. "edit"를 입력하고, 명령 이름 완성을 사용하고, 인수 120을 제공하여 명령 창과 상호 작용할 수 있으며 결과는 다음과 같습니다.

> Edit.AddColumnGuide 120
>

이 동작을 사용하도록 설정하는 샘플 조각은 .vsct 파일 선언, 명령 처리기를 연결할 때의 ColumnGuideCommands 클래스 생성자 및 이벤트 인수를 확인하는 명령 처리기 구현에 있습니다.

명령이 편집 메뉴 UI에 표시되지 않더라도 "<CommandFlag>CommandWellOnly</CommandFlag>"를 편집 주 메뉴의 배치뿐만 아니라 .vsct 파일에서도 보았습니다. 기본 편집 메뉴에 두면 Edit.AddColumnGuide와 같은 이름이 지정됩니다. 다음 4개의 명령을 포함하는 명령 그룹 선언은 그룹을 편집 메뉴에 직접 배치했습니다.

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

단추 섹션은 나중에 주 메뉴에서 보이지 않도록 명령을 CommandWellOnly로 선언하고 AllowParams를 사용하여 선언했습니다.

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

허용된 매개 변수에 대한 설명을 고려할 경우 ColumnGuideCommands 클래스 생성자의 명령 처리기가 코드를 연결하는 것을 보았습니다.

_addGuidelineCommand.ParametersDescription = "<column>";

현재 열에 대해 편집기 뷰를 확인하기 전에 GetApplicableColumn 함수가 OleMenuCmdEventArgs 값을 확인하는 것을 보았습니다.

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

확장 시도

이제 F5 키를 눌러 열 안내선 확장을 실행할 수 있습니다. 텍스트 파일을 열고 편집기 상황에 맞는 메뉴를 사용하여 안내선을 추가하고 제거한 다음, 색을 변경합니다. 텍스트(줄 끝을 통과한 공백이 아님)를 클릭하여 열 안내선을 추가하거나 편집기에서 줄의 마지막 열에 열 안내선을 추가합니다. 명령 창을 사용하고 인수를 사용하여 명령을 호출하는 경우 어디서나 열 안내선을 추가할 수 있습니다.

다른 명령 배치, 이름 변경, 아이콘 변경 등을 시도하려고 하며 Visual Studio에서 메뉴의 최신 코드를 표시하는 데 문제가 있는 경우 디버깅 중인 실험적 하이브를 다시 설정할 수 있습니다. Windows 시작 메뉴를 표시하고 "reset"을 입력합니다. 다음 Visual Studio 실험적 인스턴스 다시 설정 명령을 찾아서 실행합니다. 이 명령은 모든 확장 구성 요소의 실험적 레지스트리 하이브를 정리합니다. 구성 요소에서 설정을 정리하지 않으므로 Visual Studio의 실험적 하이브를 종료할 때 있었던 모든 안내선이 다음 시작 시 코드가 설정 저장소를 읽을 때도 여전히 존재합니다.

완료된 코드 프로젝트

곧 Visual Studio 확장성 샘플의 GitHub 프로젝트가 제공될 예정이며 여기에 완성된 프로젝트가 포함될 것입니다. 이 문서는 이러한 경우 해당 위치를 가리키도록 업데이트됩니다. 완료된 샘플 프로젝트에는 다른 GUID가 있을 수 있으며 명령 아이콘에 대해 다른 비트맵 스트립이 있을 수 있습니다.

이 Visual Studio 갤러리 확장을 사용하여 열 안내선 기능의 버전을 사용해 볼 수 있습니다.