자습서: 보기 장식, 명령, 및 설정(열 안내선) 만들기
명령 및 보기 효과를 사용하여 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.cs 및 ColumnGuideAdornmentTextViewCreationListener.cs. 템플릿은 보기에 자주색 사각형을 그립니다. 다음 섹션에서는 보기 만들기 수신기에서 몇 줄을 변경하고 ColumnGuideAdornment.cs내용을 바꿉니다.
명령. 솔루션 탐색기프로젝트 노드에서 오른쪽 포인터 단추를 누릅니다. 선택 새 항목 ... 추가 | 명령을 선택하여 새로운 보기 장식 항목을 추가합니다. 왼쪽 탐색 창에서 확장성 | VSPackage을 선택하고, 오른쪽 창에서 사용자 지정 명령을 선택합니다. ColumnGuideCommands 이름을 항목 이름으로 입력하고 추가를 선택합니다. 여러 참조 외에도 명령 및 패키지를 추가하면 ColumnGuideCommands.cs, ColumnGuideCommandsPackage.cs및 ColumnGuideCommandsPackage.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
개체 목록을 유지합니다.
생성자(ColumnGuideAdornmentTextViewCreationListener
에서 Visual Studio가 새로운 뷰를 만들 때 호출됨)는 Line
개체의 열 가이드를 생성합니다. 또한 생성자는 GuidesSettingsManager
정의된 SettingsChanged
이벤트 및 뷰 이벤트 LayoutChanged
및 Closed
대한 처리기를 추가합니다.
LayoutChanged
이벤트는 Visual Studio에서 보기를 생성할 때를 포함하여, 보기에서 여러 종류의 변경이 발생했을 때 발생합니다.
OnViewLayoutChanged
처리기는 AddGuidelinesToAdornmentLayer
을 실행하도록 호출합니다.
OnViewLayoutChanged
코드는 글꼴 크기 변경, 여백 보기, 가로 스크롤 등과 같은 변경 내용에 따라 줄 위치를 업데이트해야 하는지 여부를 결정합니다.
UpdatePositions
코드는 텍스트 줄의 지정된 문자 오프셋에 있는 텍스트 열 바로 뒤 또는 문자 사이에 안내선을 그립니다.
설정을 변경할 때마다 SettingsChanged
함수는 새 설정이 무엇이든 모든 Line
개체를 다시 만듭니다. 줄 위치를 설정한 후 코드는 ColumnGuideAdornment
표시 계층에서 이전 Line
개체를 모두 제거하고 새 개체를 추가합니다.
명령, 메뉴 및 메뉴 배치 정의
명령 및 메뉴를 선언하고, 다양한 다른 메뉴에 명령 또는 메뉴 그룹을 배치하고, 명령 처리기를 연결하는 데 많은 것이 있을 수 있습니다. 이 연습에서는 이 확장에서 명령이 작동하는 방식을 강조 표시하지만 자세한 내용은 확장 메뉴 및 명령참조하세요.
코드 소개
열 안내선 확장은 함께 속한 명령 그룹(열 추가, 열 제거, 선 색 변경)을 선언한 다음 편집기 상황에 맞는 메뉴의 하위 메뉴에 해당 그룹을 배치하는 방법을 보여 줍니다. 열 안내선 확장은 기본 편집 메뉴에 명령을 추가하지만, 이를 보이지 않게 유지하며 그 방법은 아래에서 일반적인 패턴으로 설명됩니다.
명령 구현에는 ColumnGuideCommandsPackage.cs, ColumnGuideCommandsPackage.vsct 및 ColumnGuideCommands.cs 세 부분으로 구성됩니다. 템플릿에서 생성된 코드는 도구 메뉴에 명령을 배치하여 대화 상자를 구현으로 표시합니다. .vsct 구현되는 방법과 ColumnGuideCommands.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>
GUID. Visual Studio에서 명령 처리기를 찾아서 호출하려면 ColumnGuideCommandsPackage.cs 파일에 선언된 패키지 GUID(프로젝트 항목 템플릿에서 생성됨)가 .vsct 파일에 선언된 패키지 GUID(위에서 복사)와 일치하는지 확인해야 합니다. 이 샘플 코드를 다시 사용하는 경우 이 코드를 복사한 다른 사용자와 충돌하지 않도록 다른 GUID가 있는지 확인해야 합니다.
ColumnGuideCommandsPackage.cs 이 줄을 찾아 따옴표 사이에 GUID를 복사합니다.
public const string PackageGuidString = "ef726849-5447-4f73-8de5-01b9e930f7cd";
그런 다음 Symbols
선언에 다음 줄이 있도록 .vsct 파일에 GUID를 붙여넣습니다.
<GuidSymbol name="guidColumnGuideCommandsPkg"
value="{ef726849-5447-4f73-8de5-01b9e930f7cd}" />
명령 집합 및 비트맵 이미지 파일의 GUID도 확장에 대해 고유해야 합니다.
<GuidSymbol name="guidColumnGuidesCommandSet"
value="{c2bc0047-8bfa-4e5a-b5dc-45af8c274d8e}">
<GuidSymbol name="guidImages" value="{2C99F852-587C-43AF-AA2D-F605DE2E46EF}">
그러나 코드가 작동하도록 이 연습에서 명령 집합 및 비트맵 이미지 GUID를 변경할 필요는 없습니다. 명령 집합 GUID는 ColumnGuideCommands.cs 파일의 선언과 일치해야 하지만 해당 파일의 내용도 바꿉니다. 따라서 GUID가 일치합니다.
.vsct 파일의 다른 GUID는 열 안내선 명령이 추가되는 기존 메뉴를 식별하므로 변경되지 않습니다.
파일 섹션. .vsct 명령, 배치 및 기호의 세 가지 외부 섹션이 있습니다. 명령 섹션은 명령 그룹, 메뉴, 단추 또는 메뉴 항목 및 아이콘에 대한 비트맵을 정의합니다. 배치 섹션에서는 그룹이 메뉴로 이동하는 위치 또는 기존 메뉴에 대한 추가 배치를 선언합니다. 기호 섹션은 .vsct 파일의 다른 곳에서 사용되는 식별자를 선언합니다. 따라서 .vsct 코드는 GUID 및 16진수 숫자가 있는 것보다 더 쉽게 읽을 수 있습니다.
명령 섹션에서정의는 그룹화됩니다. 명령 섹션은 먼저 명령 그룹을 정의합니다. 명령 그룹은 그룹을 구분하는 약간의 회색 선이 있는 메뉴에 표시되는 명령입니다. 이 예제와 같이 그룹이 전체 하위 메뉴를 채울 수도 있으며 이 경우 회색 구분선이 표시되지 않습니다.
.vsct 파일은 IDM_VS_MENU_EDIT
부모인 GuidesMenuItemsGroup
(기본 편집 메뉴) 및 IDM_VS_CTXT_CODEWIN
부모인 GuidesContextMenuGroup
(코드 편집기 상황에 맞는 메뉴)의 두 그룹을 선언합니다.
두 번째 그룹 선언에는 0x0600
우선 순위가 있습니다.
<Group guid="guidColumnGuidesCommandSet" id="GuidesContextMenuGroup"
priority="0x0600">
하위 메뉴 그룹을 추가할 상황에 맞는 메뉴의 끝에 열 안내선 하위 메뉴를 배치하는 것이 좋습니다. 그러나 가장 잘 알고 있다고 가정하고 0xFFFF
우선 순위를 사용하여 하위 메뉴가 항상 마지막이 되도록 해서는 안 됩니다. 숫자를 실험하여 하위 메뉴가 배치되는 상황에 맞는 메뉴에 있는 위치를 확인해야 합니다. 이 경우 볼 수 있는 한 메뉴의 끝에 0x0600
을 배치할 수 있을 만큼 높지만, 다른 사람이 필요한 경우 그들의 확장을 컬럼 가이드 확장보다 낮게 디자인할 수 있는 여지를 남겨 줍니다.
명령 섹션, 메뉴 정의. 다음으로는 명령 섹션에서 하위 메뉴 GuidesSubMenu
를 정의하며, 이는 GuidesContextMenuGroup
에 소속됩니다.
GuidesContextMenuGroup
모든 관련 상황에 맞는 메뉴에 추가하는 그룹입니다. 배치 섹션에서 코드는 이 하위 메뉴에 4열 안내선 명령을 사용하여 그룹을 배치합니다.
명령 섹션에서 단추 정의는. 그런 다음 명령 섹션에서는 4열 안내선 명령인 메뉴 항목 또는 단추를 정의합니다. 위에서 설명한 CommandWellOnly
주 메뉴에 배치할 때 명령이 보이지 않는 것을 의미합니다. 메뉴 항목 단추 선언 중 두 개(안내선 추가 및 가이드 제거)에도 AllowParams
플래그가 있습니다.
<CommandFlag>AllowParams</CommandFlag>
이 플래그를 사용하면 기본 메뉴 배치와 함께 Visual Studio에서 명령 처리기를 호출할 때 인수를 수신할 수 있습니다. 사용자가 명령 창에서 명령을 실행하면 인수가 이벤트 인수의 명령 처리기에 전달됩니다.
명령 섹션, 비트맵 정의. 마지막으로 명령 섹션에서는 명령에 사용되는 비트맵 또는 아이콘을 선언합니다. 이 섹션은 프로젝트 리소스를 식별하고 사용된 아이콘의 1부터 시작하는 인덱스를 나열하는 간단한 선언입니다. .vsct 파일의 기호 섹션은 인덱스로 사용되는 식별자의 값을 선언합니다. 이 연습에서는 프로젝트에 추가된 사용자 지정 명령 항목 템플릿과 함께 제공되는 비트맵 스트립을 사용합니다.
배치 항목. 명령 섹션 뒤의 배치 섹션입니다. 첫 번째는 코드가 위에서 논의된 네 열 안내 명령을 포함한 첫 번째 그룹을 명령이 나타나는 하위 메뉴에 추가하는 경우입니다.
<CommandPlacement guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup"
priority="0x0100">
<Parent guid="guidColumnGuidesCommandSet" id="GuidesSubMenu" />
</CommandPlacement>
다른 모든 배치는 GuidesContextMenuGroup
(GuidesSubMenu
포함)를 다른 편집기 상황에 맞는 메뉴에 추가합니다. 코드가 GuidesContextMenuGroup
을 선언했을 때, 코드 편집기의 컨텍스트 메뉴에 부모로 설정되었습니다. 따라서 코드 편집기 상황에 맞는 메뉴에 대한 배치가 표시되지 않습니다.
기호 섹션. 위에서 설명한 대로 기호 섹션은 .vsct 파일의 다른 곳에서 사용되는 식별자를 선언합니다. 따라서 .vsct 코드는 GUID 및 16진수 숫자가 있는 것보다 더 쉽게 읽을 수 있습니다. 이 섹션의 중요한 점은 패키지 GUID가 패키지 클래스의 선언에 동의해야 한다는 것입니다. 또한 명령 집합 GUID는 명령 구현 클래스의 선언에 동의해야 합니다.
명령 구현
ColumnGuideCommands.cs 파일은 명령을 구현하고 처리기를 연결합니다. Visual Studio에서 패키지를 로드하고 초기화하면 패키지가 명령 구현 클래스에서 Initialize
호출합니다. 명령 초기화는 단순히 클래스를 인스턴스화하고 생성자는 모든 명령 처리기를 연결합니다.
ColumnGuideCommands.cs 파일의 내용을 다음 코드로 바꿉니다(아래 설명).
using System;
using System.ComponentModel.Design;
using System.Globalization;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.TextManager.Interop;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio;
namespace ColumnGuides
{
/// <summary>
/// Command handler
/// </summary>
internal sealed class ColumnGuideCommands
{
const int cmdidAddColumnGuide = 0x0100;
const int cmdidRemoveColumnGuide = 0x0101;
const int cmdidChooseGuideColor = 0x0102;
const int cmdidRemoveAllColumnGuides = 0x0103;
/// <summary>
/// Command menu group (command set GUID).
/// </summary>
static readonly Guid CommandSet =
new Guid("c2bc0047-8bfa-4e5a-b5dc-45af8c274d8e");
/// <summary>
/// VS Package that provides this command, not null.
/// </summary>
private readonly Package package;
OleMenuCommand _addGuidelineCommand;
OleMenuCommand _removeGuidelineCommand;
/// <summary>
/// Initializes the singleton instance of the command.
/// </summary>
/// <param name="package">Owner package, not null.</param>
public static void Initialize(Package package)
{
Instance = new ColumnGuideCommands(package);
}
/// <summary>
/// Gets the instance of the command.
/// </summary>
public static ColumnGuideCommands Instance
{
get;
private set;
}
/// <summary>
/// Initializes a new instance of the <see cref="ColumnGuideCommands"/> class.
/// Adds our command handlers for menu (commands must exist in the command
/// table file)
/// </summary>
/// <param name="package">Owner package, not null.</param>
private ColumnGuideCommands(Package package)
{
if (package == null)
{
throw new ArgumentNullException("package");
}
this.package = package;
// Add our command handlers for menu (commands must exist in the .vsct file)
OleMenuCommandService commandService =
this.ServiceProvider.GetService(typeof(IMenuCommandService))
as OleMenuCommandService;
if (commandService != null)
{
// Add guide
_addGuidelineCommand =
new OleMenuCommand(AddColumnGuideExecuted, null,
AddColumnGuideBeforeQueryStatus,
new CommandID(ColumnGuideCommands.CommandSet,
cmdidAddColumnGuide));
_addGuidelineCommand.ParametersDescription = "<column>";
commandService.AddCommand(_addGuidelineCommand);
// Remove guide
_removeGuidelineCommand =
new OleMenuCommand(RemoveColumnGuideExecuted, null,
RemoveColumnGuideBeforeQueryStatus,
new CommandID(ColumnGuideCommands.CommandSet,
cmdidRemoveColumnGuide));
_removeGuidelineCommand.ParametersDescription = "<column>";
commandService.AddCommand(_removeGuidelineCommand);
// Choose color
commandService.AddCommand(
new MenuCommand(ChooseGuideColorExecuted,
new CommandID(ColumnGuideCommands.CommandSet,
cmdidChooseGuideColor)));
// Remove all
commandService.AddCommand(
new MenuCommand(RemoveAllGuidelinesExecuted,
new CommandID(ColumnGuideCommands.CommandSet,
cmdidRemoveAllColumnGuides)));
}
}
/// <summary>
/// Gets the service provider from the owner package.
/// </summary>
private IServiceProvider ServiceProvider
{
get
{
return this.package;
}
}
private void AddColumnGuideBeforeQueryStatus(object sender, EventArgs e)
{
int currentColumn = GetCurrentEditorColumn();
_addGuidelineCommand.Enabled =
GuidesSettingsManager.CanAddGuideline(currentColumn);
}
private void RemoveColumnGuideBeforeQueryStatus(object sender, EventArgs e)
{
int currentColumn = GetCurrentEditorColumn();
_removeGuidelineCommand.Enabled =
GuidesSettingsManager.CanRemoveGuideline(currentColumn);
}
private int GetCurrentEditorColumn()
{
IVsTextView view = GetActiveTextView();
if (view == null)
{
return -1;
}
try
{
IWpfTextView textView = GetTextViewFromVsTextView(view);
int column = GetCaretColumn(textView);
// Note: GetCaretColumn returns 0-based positions. Guidelines are 1-based
// positions.
// However, do not subtract one here since the caret is positioned to the
// left of
// the given column and the guidelines are positioned to the right. We
// want the
// guideline to line up with the current caret position. e.g. When the
// caret is
// at position 1 (zero-based), the status bar says column 2. We want to
// add a
// guideline for column 1 since that will place the guideline where the
// caret is.
return column;
}
catch (InvalidOperationException)
{
return -1;
}
}
/// <summary>
/// Find the active text view (if any) in the active document.
/// </summary>
/// <returns>The IVsTextView of the active view, or null if there is no active
/// document or the
/// active view in the active document is not a text view.</returns>
private IVsTextView GetActiveTextView()
{
IVsMonitorSelection selection =
this.ServiceProvider.GetService(typeof(IVsMonitorSelection))
as IVsMonitorSelection;
object frameObj = null;
ErrorHandler.ThrowOnFailure(
selection.GetCurrentElementValue(
(uint)VSConstants.VSSELELEMID.SEID_DocumentFrame, out frameObj));
IVsWindowFrame frame = frameObj as IVsWindowFrame;
if (frame == null)
{
return null;
}
return GetActiveView(frame);
}
private static IVsTextView GetActiveView(IVsWindowFrame windowFrame)
{
if (windowFrame == null)
{
throw new ArgumentException("windowFrame");
}
object pvar;
ErrorHandler.ThrowOnFailure(
windowFrame.GetProperty((int)__VSFPROPID.VSFPROPID_DocView, out pvar));
IVsTextView textView = pvar as IVsTextView;
if (textView == null)
{
IVsCodeWindow codeWin = pvar as IVsCodeWindow;
if (codeWin != null)
{
ErrorHandler.ThrowOnFailure(codeWin.GetLastActiveView(out textView));
}
}
return textView;
}
private static IWpfTextView GetTextViewFromVsTextView(IVsTextView view)
{
if (view == null)
{
throw new ArgumentNullException("view");
}
IVsUserData userData = view as IVsUserData;
if (userData == null)
{
throw new InvalidOperationException();
}
object objTextViewHost;
if (VSConstants.S_OK
!= userData.GetData(Microsoft.VisualStudio
.Editor
.DefGuidList.guidIWpfTextViewHost,
out objTextViewHost))
{
throw new InvalidOperationException();
}
IWpfTextViewHost textViewHost = objTextViewHost as IWpfTextViewHost;
if (textViewHost == null)
{
throw new InvalidOperationException();
}
return textViewHost.TextView;
}
/// <summary>
/// Given an IWpfTextView, find the position of the caret and report its column
/// number. The column number is 0-based
/// </summary>
/// <param name="textView">The text view containing the caret</param>
/// <returns>The column number of the caret's position. When the caret is at the
/// leftmost column, the return value is zero.</returns>
private static int GetCaretColumn(IWpfTextView textView)
{
// This is the code the editor uses to populate the status bar.
Microsoft.VisualStudio.Text.Formatting.ITextViewLine caretViewLine =
textView.Caret.ContainingTextViewLine;
double columnWidth = textView.FormattedLineSource.ColumnWidth;
return (int)(Math.Round((textView.Caret.Left - caretViewLine.Left)
/ columnWidth));
}
/// <summary>
/// Determine the applicable column number for an add or remove command.
/// The column is parsed from command arguments, if present. Otherwise
/// the current position of the caret is used to determine the column.
/// </summary>
/// <param name="e">Event args passed to the command handler.</param>
/// <returns>The column number. May be negative to indicate the column number is
/// unavailable.</returns>
/// <exception cref="ArgumentException">The column number parsed from event args
/// was not a valid integer.</exception>
private int GetApplicableColumn(EventArgs e)
{
var inValue = ((OleMenuCmdEventArgs)e).InValue as string;
if (!string.IsNullOrEmpty(inValue))
{
int column;
if (!int.TryParse(inValue, out column) || column < 0)
throw new ArgumentException("Invalid column");
return column;
}
return GetCurrentEditorColumn();
}
/// <summary>
/// This function is the callback used to execute a command when a menu item
/// is clicked. See the Initialize method to see how the menu item is associated
/// to this function using the OleMenuCommandService service and the MenuCommand
/// class.
/// </summary>
private void AddColumnGuideExecuted(object sender, EventArgs e)
{
int column = GetApplicableColumn(e);
if (column >= 0)
{
GuidesSettingsManager.AddGuideline(column);
}
}
private void RemoveColumnGuideExecuted(object sender, EventArgs e)
{
int column = GetApplicableColumn(e);
if (column >= 0)
{
GuidesSettingsManager.RemoveGuideline(column);
}
}
private void RemoveAllGuidelinesExecuted(object sender, EventArgs e)
{
GuidesSettingsManager.RemoveAllGuidelines();
}
private void ChooseGuideColorExecuted(object sender, EventArgs e)
{
System.Windows.Media.Color color = GuidesSettingsManager.GuidelinesColor;
using (System.Windows.Forms.ColorDialog picker =
new System.Windows.Forms.ColorDialog())
{
picker.Color = System.Drawing.Color.FromArgb(255, color.R, color.G,
color.B);
if (picker.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
GuidesSettingsManager.GuidelinesColor =
System.Windows.Media.Color.FromRgb(picker.Color.R,
picker.Color.G,
picker.Color.B);
}
}
}
}
}
참조를 수정합니다. 이 시점에서 참조가 누락되었습니다. 솔루션 탐색기의 참조 노드에서 오른쪽 포인터 단추를 누릅니다. 추가 ... 명령을 선택합니다. 참조 추가 대화 상자의 오른쪽 위 모서리에 검색 상자가 있습니다. 큰따옴표 없이 "편집기"를 입력합니다. Microsoft.VisualStudio.Editor 항목을 선택하고(항목을 선택하는 것뿐만 아니라 항목 왼쪽에 있는 확인란을 선택해야 합니다) 확인 버튼을 선택하여 참조를 추가합니다.
초기화. 패키지 클래스가 초기화되면 명령 구현 클래스에서 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는 메뉴에 명령을 표시하기 전에 쿼리 상태 함수를 호출하여 메뉴의 특정 표시에 대해 명령 자체를 보이지 않거나 회색으로 표시할 수 있도록 합니다(예: 선택 사항이 없는 경우 복사 사용 안 함), 아이콘을 변경하거나 이름을 변경합니다(예: 무언가를 제거할 항목 추가). 등등. 명령 ID는 .vsct 파일에 선언된 명령 ID와 일치해야 합니다. 명령 집합 및 열 안내선 추가 명령에 대한 문자열은 .vsct 파일과 ColumnGuideCommands.cs간에 일치해야 합니다.
다음 줄에서는 사용자가 명령 창을 통해 명령을 호출할 때 도움이 됩니다(아래 설명).
_addGuidelineCommand.ParametersDescription = "<column>";
쿼리 상태. 쿼리 상태 함수는 AddColumnGuideBeforeQueryStatus
RemoveColumnGuideBeforeQueryStatus
일부 설정(예: 최대 안내선 수 또는 최대 열 수)을 확인하거나 제거할 열 안내선이 있는지 확인합니다. 조건이 맞으면 명령을 사용하도록 설정합니다. 쿼리 상태 함수는 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
, GetActiveView
및 GetTextViewFromVsTextView
를 따라가면 어떻게 하는지 알 수 있습니다. 다음 코드는 현재 선택부터 시작하여 선택 영역의 프레임을 가져오고, 프레임의 DocView를 IVsTextView가져오고, IVsTextView에서 IVsUserData 가져오고, 뷰 호스트를 가져오고, 마지막으로 IWpfTextView를 가져오는 등 추상화된 관련 코드입니다.
IVsMonitorSelection selection =
this.ServiceProvider.GetService(typeof(IVsMonitorSelection))
as IVsMonitorSelection;
object frameObj = null;
ErrorHandler.ThrowOnFailure(selection.GetCurrentElementValue(
(uint)VSConstants.VSSELELEMID.SEID_DocumentFrame,
out frameObj));
IVsWindowFrame frame = frameObj as IVsWindowFrame;
if (frame == null)
<<do nothing>>;
...
object pvar;
ErrorHandler.ThrowOnFailure(frame.GetProperty((int)__VSFPROPID.VSFPROPID_DocView,
out pvar));
IVsTextView textView = pvar as IVsTextView;
if (textView == null)
{
IVsCodeWindow codeWin = pvar as IVsCodeWindow;
if (codeWin != null)
{
ErrorHandler.ThrowOnFailure(codeWin.GetLastActiveView(out textView));
}
}
...
if (textView == null)
<<do nothing>>
IVsUserData userData = textView as IVsUserData;
if (userData == null)
<<do nothing>>
object objTextViewHost;
if (VSConstants.S_OK
!= userData.GetData(Microsoft.VisualStudio.Editor.DefGuidList
.guidIWpfTextViewHost,
out objTextViewHost))
{
<<do nothing>>
}
IWpfTextViewHost textViewHost = objTextViewHost as IWpfTextViewHost;
if (textViewHost == null)
<<do nothing>>
IWpfTextView textView = textViewHost.TextView;
IWpfTextView가 있으면 캐럿이 있는 열을 가져올 수 있습니다.
private static int GetCaretColumn(IWpfTextView textView)
{
// This is the code the editor uses to populate the status bar.
Microsoft.VisualStudio.Text.Formatting.ITextViewLine caretViewLine =
textView.Caret.ContainingTextViewLine;
double columnWidth = textView.FormattedLineSource.ColumnWidth;
return (int)(Math.Round((textView.Caret.Left - caretViewLine.Left)
/ columnWidth));
}
사용자가 클릭한 현재 열을 사용하여 코드는 설정 관리자를 호출하여 열을 추가하거나 제거합니다. 설정 관리자는 모든 ColumnGuideAdornment
개체가 수신 대기하는 이벤트를 트리거합니다. 이벤트가 발생하면 이 객체들은 연결된 텍스트 보기를 새 열 안내선 설정으로 업데이트하여 변경합니다.
명령 창에서 명령 호출
열 안내선 샘플을 사용하면 사용자가 명령 창에서 두 명령을 확장성 형태로 호출할 수 있습니다. 보기 | 기타 창 | 명령 창 명령을 사용하면 명령 창을 볼 수 있습니다. "edit"를 입력하고 명령 이름 완성 및 인수 120을 제공하여 명령 창과 상호 작용할 수 있는 결과는 다음과 같습니다.
> Edit.AddColumnGuide 120
>
이 동작을 사용하도록 설정하는 샘플의 조각은 .vsct 파일 선언, 명령 처리기를 연결할 때 ColumnGuideCommands
클래스 생성자 및 이벤트 인수를 확인하는 명령 처리기 구현에 있습니다.
.vsct 파일 뿐만 아니라 편집 메인 메뉴의 위치에서도 "<CommandFlag>CommandWellOnly</CommandFlag>
"를 보았지만, 명령은 편집 메뉴 UI에 표시되지 않습니다. 기본 편집 메뉴에 배치하면 Edit.AddColumnGuide와 같은 이름을 가지게 됩니다. 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가 있을 수 있으며 명령 아이콘에 대해 다른 비트맵 스트립이 있습니다.
비주얼 스튜디오 갤러리 확장을 사용하여 열 안내선 기능의 한 버전을 체험해 볼 수 있습니다.
관련 콘텐츠
- 편집기 내부
- 편집기 및 언어 서비스 확장
- 언어 서비스 및 편집기 확장 지점
- 확장 메뉴 및 명령
- 메뉴 하위 메뉴 추가
- 편집기 항목 템플릿 사용하여 확장 만들기