コマンドとビュー効果を使用して、Visual Studio のテキスト/コード エディターを拡張できます。 この記事では、一般的な拡張機能の使用を開始する方法、列ガイドについて説明します。 列ガイドは、テキスト エディターのビューに描画される視覚的に明るい線で、コードを特定の列幅に管理するのに役立ちます。 具体的には、ドキュメント、ブログの投稿、バグ レポートに含めるサンプルでは、書式設定されたコードが重要になる場合があります。
このチュートリアルでは、次の操作を行います。
VSIX プロジェクトを作成する
エディター ビューの装飾を追加する
設定の保存と取得のサポートを追加 (列ガイドとその色を描画する場所)
コマンドの追加 (列ガイドの追加/削除、色の変更)
[編集] メニューとテキスト ドキュメントのコンテキスト メニューにコマンドを配置する
Visual Studio コマンド ウィンドウからコマンドを呼び出すためのサポートを追加する
この Visual Studio ギャラリー 拡張機能を使用して、列ガイド機能のバージョン試すことができます。
手記
このチュートリアルでは、Visual Studio 拡張機能テンプレートによって生成されたいくつかのファイルに大量のコードを貼り付けます。 ただし、間もなくこのチュートリアルでは、GitHub の完成したソリューションと他の拡張機能の例を参照します。 完成したコードは、generictemplate アイコンを使用する代わりに実際のコマンド アイコンがある点で若干異なります。
ソリューションを設定する
まず、VSIX プロジェクトを作成し、エディター ビューの装飾を追加してから、コマンドを追加します (コマンドを所有する VSPackage を追加します)。 基本的なアーキテクチャは次のとおりです。
ビューごとに
ColumnGuideAdornment
オブジェクトを作成するテキスト ビュー作成リスナーがあります。 このオブジェクトは、ビューの変更または設定の変更に関するイベントをリッスンし、必要に応じて列ガイドを更新または再描画します。Visual Studio 設定ストレージからの読み取りと書き込みを処理する
GuidesSettingsManager
があります。 設定マネージャーには、ユーザー コマンドをサポートする設定を更新するための操作もあります (列の追加、列の削除、色の変更)。ユーザー コマンドがある場合に必要な VSIP パッケージがありますが、コマンド実装オブジェクトを初期化する定型コードにすぎません。
ユーザー コマンドを実行し、.vsct ファイルで宣言されたコマンドのコマンド ハンドラーをフックする
ColumnGuideCommands
オブジェクトがあります。VSIX。 ファイル | 新規作成 コマンドを使用してプロジェクトを作成します。 左側のナビゲーション ウィンドウの [C# の下にある 機能拡張 ノードを選択し、右側のウィンドウで [VSIX プロジェクト 選択します。 名前 ColumnGuides を入力し、[OK] を選択してプロジェクトを作成します。
ビューの表示要素。 ソリューション エクスプローラーのプロジェクト ノードの右ポインター ボタンを押します。 追加 |新しい項目 ... コマンドを使用して、新しいビュー装飾項目を追加します。 機能拡張 | エディター を左側のナビゲーションウィンドウで選択し、右側のウィンドウで エディタービューポート装飾 を選択します。 項目名として「ColumnGuideAdornment」という名前を入力し、[追加] を選択して追加します。
この項目テンプレートは、プロジェクトに 2 つのファイル (参照など) を追加したことがわかります。ColumnGuideAdornment.cs と ColumnGuideAdornmentTextViewCreationListener.cs。 テンプレートは、ビューに紫色の四角形を描画します。 次のセクションでは、ビュー作成リスナーの 2 行を変更し、ColumnGuideAdornment.csの内容を置き換えます。
コマンド。 ソリューション エクスプローラー で、プロジェクト ノードの右ポインター ボタンを押します。 追加 |新しい項目 ... コマンドを使用して、新しいビュー装飾項目を追加します。 機能拡張を選択する |VSPackage 左側のナビゲーション ウィンドウで、右側のウィンドウ カスタム コマンド を選択します。 項目名として「ColumnGuideCommands」という名前を入力し、[追加] を選択します。 いくつかの参照に加えて、コマンドとパッケージを追加すると、ColumnGuideCommands.cs、ColumnGuideCommandsPackage.cs、および ColumnGuideCommandsPackage.vsctも追加されました。 次のセクションでは、最初と最後のファイルの内容を置き換えて、コマンドを定義して実装します。
テキスト ビュー作成リスナーを設定する
エディターで ColumnGuideAdornmentTextViewCreationListener.cs を開きます。 このコードは、Visual Studio がテキスト ビューを作成するたびにハンドラーを実装します。 ビューの特性に応じてハンドラーを呼び出すタイミングを制御する属性があります。
コードでは、装飾レイヤーも宣言する必要があります。 エディターは、ビューを更新すると、ビューの装飾レイヤーを取得し、そこから装飾要素を取得します。 属性を使用して、レイヤーの順序を他のユーザーに対して相対的に宣言できます。 次の行を置き換えます。
[Order(After = PredefinedAdornmentLayers.Caret)]
次の 2 行を使用します。
[Order(Before = PredefinedAdornmentLayers.Text)]
[TextViewRole(PredefinedTextViewRoles.Document)]
置換した行は、装飾レイヤーを宣言する属性のグループ内にあります。 変更した最初の行は、列ガイド行が表示される場所のみを変更します。 ビュー内のテキストの "前" に線を描画すると、テキストの背後または下に表示されます。 2 行目では、列ガイドの表示要素がドキュメントの概念に適合するテキスト エンティティに適用できることを宣言していますが、たとえば、編集可能なテキストに対してのみ機能するように装飾を宣言することもできます。 詳細については、言語サービスとエディターの拡張ポイントに関する記事を参照してください。
設定マネージャーを実装する
GuidesSettingsManager.cs の内容を次のコードに置き換えます (以下で説明します)。
using Microsoft.VisualStudio.Settings;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Settings;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Media;
namespace ColumnGuides
{
internal static class GuidesSettingsManager
{
// Because my code is always called from the UI thread, this succeeds.
internal static SettingsManager VsManagedSettingsManager =
new ShellSettingsManager(ServiceProvider.GlobalProvider);
private const int _maxGuides = 5;
private const string _collectionSettingsName = "Text Editor";
private const string _settingName = "Guides";
// 1000 seems reasonable since primary scenario is long lines of code
private const int _maxColumn = 1000;
static internal bool AddGuideline(int column)
{
if (! IsValidColumn(column))
throw new ArgumentOutOfRangeException(
"column",
"The parameter must be between 1 and " + _maxGuides.ToString());
var offsets = GuidesSettingsManager.GetColumnOffsets();
if (offsets.Count() >= _maxGuides)
return false;
// Check for duplicates
if (offsets.Contains(column))
return false;
offsets.Add(column);
WriteSettings(GuidesSettingsManager.GuidelinesColor, offsets);
return true;
}
static internal bool RemoveGuideline(int column)
{
if (!IsValidColumn(column))
throw new ArgumentOutOfRangeException(
"column", "The parameter must be between 1 and 10,000");
var columns = GuidesSettingsManager.GetColumnOffsets();
if (! columns.Remove(column))
{
// Not present. Allow user to remove the last column
// even if they're not on the right column.
if (columns.Count != 1)
return false;
columns.Clear();
}
WriteSettings(GuidesSettingsManager.GuidelinesColor, columns);
return true;
}
static internal bool CanAddGuideline(int column)
{
if (!IsValidColumn(column))
return false;
var offsets = GetColumnOffsets();
if (offsets.Count >= _maxGuides)
return false;
return ! offsets.Contains(column);
}
static internal bool CanRemoveGuideline(int column)
{
if (! IsValidColumn(column))
return false;
// Allow user to remove the last guideline regardless of the column.
// Okay to call count, we limit the number of guides.
var offsets = GuidesSettingsManager.GetColumnOffsets();
return offsets.Contains(column) || offsets.Count() == 1;
}
static internal void RemoveAllGuidelines()
{
WriteSettings(GuidesSettingsManager.GuidelinesColor, new int[0]);
}
private static bool IsValidColumn(int column)
{
// zero is allowed (per user request)
return 0 <= column && column <= _maxColumn;
}
// This has format "RGB(<int>, <int>, <int>) <int> <int>...".
// There can be any number of ints following the RGB part,
// and each int is a column (char offset into line) where to draw.
static private string _guidelinesConfiguration;
static private string GuidelinesConfiguration
{
get
{
if (_guidelinesConfiguration == null)
{
_guidelinesConfiguration =
GetUserSettingsString(
GuidesSettingsManager._collectionSettingsName,
GuidesSettingsManager._settingName)
.Trim();
}
return _guidelinesConfiguration;
}
set
{
if (value != _guidelinesConfiguration)
{
_guidelinesConfiguration = value;
WriteUserSettingsString(
GuidesSettingsManager._collectionSettingsName,
GuidesSettingsManager._settingName, value);
// Notify ColumnGuideAdornments to update adornments in views.
var handler = GuidesSettingsManager.SettingsChanged;
if (handler != null)
handler();
}
}
}
internal static string GetUserSettingsString(string collection, string setting)
{
var store = GuidesSettingsManager
.VsManagedSettingsManager
.GetReadOnlySettingsStore(SettingsScope.UserSettings);
return store.GetString(collection, setting, "RGB(255,0,0) 80");
}
internal static void WriteUserSettingsString(string key, string propertyName,
string value)
{
var store = GuidesSettingsManager
.VsManagedSettingsManager
.GetWritableSettingsStore(SettingsScope.UserSettings);
store.CreateCollection(key);
store.SetString(key, propertyName, value);
}
// Persists settings and sets property with side effect of signaling
// ColumnGuideAdornments to update.
static private void WriteSettings(Color color, IEnumerable<int> columns)
{
string value = ComposeSettingsString(color, columns);
GuidelinesConfiguration = value;
}
private static string ComposeSettingsString(Color color,
IEnumerable<int> columns)
{
StringBuilder sb = new StringBuilder();
sb.AppendFormat("RGB({0},{1},{2})", color.R, color.G, color.B);
IEnumerator<int> columnsEnumerator = columns.GetEnumerator();
if (columnsEnumerator.MoveNext())
{
sb.AppendFormat(" {0}", columnsEnumerator.Current);
while (columnsEnumerator.MoveNext())
{
sb.AppendFormat(", {0}", columnsEnumerator.Current);
}
}
return sb.ToString();
}
// Parse a color out of a string that begins like "RGB(255,0,0)"
static internal Color GuidelinesColor
{
get
{
string config = GuidelinesConfiguration;
if (!String.IsNullOrEmpty(config) && config.StartsWith("RGB("))
{
int lastParen = config.IndexOf(')');
if (lastParen > 4)
{
string[] rgbs = config.Substring(4, lastParen - 4).Split(',');
if (rgbs.Length >= 3)
{
byte r, g, b;
if (byte.TryParse(rgbs[0], out r) &&
byte.TryParse(rgbs[1], out g) &&
byte.TryParse(rgbs[2], out b))
{
return Color.FromRgb(r, g, b);
}
}
}
}
return Colors.DarkRed;
}
set
{
WriteSettings(value, GetColumnOffsets());
}
}
// Parse a list of integer values out of a string that looks like
// "RGB(255,0,0) 1, 5, 10, 80"
static internal List<int> GetColumnOffsets()
{
var result = new List<int>();
string settings = GuidesSettingsManager.GuidelinesConfiguration;
if (String.IsNullOrEmpty(settings))
return new List<int>();
if (!settings.StartsWith("RGB("))
return new List<int>();
int lastParen = settings.IndexOf(')');
if (lastParen <= 4)
return new List<int>();
string[] columns = settings.Substring(lastParen + 1).Split(',');
int columnCount = 0;
foreach (string columnText in columns)
{
int column = -1;
// VS 2008 gallery extension didn't allow zero, so per user request ...
if (int.TryParse(columnText, out column) && column >= 0)
{
columnCount++;
result.Add(column);
if (columnCount >= _maxGuides)
break;
}
}
return result;
}
// Delegate and Event to fire when settings change so that ColumnGuideAdornments
// can update. We need nothing special in this event since the settings manager
// is statically available.
//
internal delegate void SettingsChangedHandler();
static internal event SettingsChangedHandler SettingsChanged;
}
}
このコードのほとんどは、設定形式 "RGB(<int>,<int>,<int>) <int>, <int>, ...") を作成して解析します。 最後の整数は、垂直グリッド ガイドを生成する 1 ベースの列です。 列ガイド拡張機能は、すべての設定を 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
で定義) のハンドラー、およびビュー イベントの LayoutChanged
と Closed
も追加します。
LayoutChanged
イベントは、Visual Studio がビューを作成するときなど、ビュー内のいくつかの種類の変更が原因で発生します。 OnViewLayoutChanged
ハンドラーによって AddGuidelinesToAdornmentLayer
が呼び出されて実行されます。 OnViewLayoutChanged
のコードは、フォント サイズの変更、表示の余白、水平スクロールなどの変更に基づいて行の位置を更新する必要があるかどうかを判断します。 UpdatePositions
のコードにより、ガイド線は文字間に描かれるか、またはテキスト行内で指定された文字オフセットの位置にある列の直後に描かれます。
設定が変更されるたびに、SettingsChanged
関数は、新しい設定が何であれ、すべての Line
オブジェクトを再作成します。 行の位置を設定した後、コードは ColumnGuideAdornment
装飾レイヤーから以前のすべての Line
オブジェクトを削除し、新しいオブジェクトを追加します。
コマンド、メニュー、およびメニューの配置を定義する
コマンドやメニューを宣言し、さまざまなメニューにコマンドまたはメニューのグループを配置し、コマンドハンドラーを関連付けるには、多くの労力が必要です。 このチュートリアルでは、この拡張機能でのコマンドの動作について説明しますが、詳細については、「メニューとコマンドの拡張を参照してください。
コードの概要
列ガイド拡張機能は、一緒に属するコマンドのグループの宣言 (列の追加、列の削除、行の色の変更) を示し、そのグループをエディターのコンテキスト メニューのサブメニューに配置します。 列ガイド拡張機能は、コマンドをメイン [編集] メニューにも追加しますが、以下の一般的なパターンとして説明するように、非表示のままにします。
コマンドの実装には、ColumnGuideCommandsPackage.cs、ColumnGuideCommandsPackage.vsct、ColumnGuideCommands.csの 3 つの部分があります。 テンプレートによって生成されたコードは、Tools メニューにコマンドを配置し、実装としてダイアログ ボックスをポップします。 .vsct および ColumnGuideCommands.cs ファイルで実装される方法は簡単であるため、確認できます。 以下のファイルのコードを置き換えます。
パッケージ コードには、拡張機能がコマンドを提供していることを検出し、コマンドを配置する場所を見つけるために Visual Studio に必要な定型宣言が含まれています。 パッケージが初期化されると、コマンド実装クラスがインスタンス化されます。 コマンドに関連するパッケージの詳細については、「メニューとコマンドを拡張する」を参照してください。
一般的なコマンド パターン
列ガイド拡張機能のコマンドは、Visual Studio の非常に一般的なパターンの例です。 関連するコマンドをグループに含め、そのグループをメイン メニューに配置します。多くの場合、"<CommandFlag>CommandWellOnly</CommandFlag>
" を設定してコマンドを非表示にします。 メイン メニュー (Editなど) にコマンドを配置すると、適切な名前 (Edit.AddColumnGuideなど) が付けられます。これは、Tools Optionsでキー バインドを再割り当てするときにコマンドを見つけるのに役立ちます。 コマンド ウィンドウからコマンドを呼び出すときに、完了を取得する場合にも役立ちます。
次に、コマンドのグループを、ユーザーがコマンドを使用することを想定しているコンテキスト メニューまたはサブメニューに追加します。 Visual Studio では、CommandWellOnly
はメイン メニューのみの非表示フラグとして扱います。 コンテキスト メニューまたはサブメニューに同じコマンド グループを配置すると、コマンドが表示されます。
共通パターンの一部として、列ガイド拡張機能は、1 つのサブメニューを保持する 2 つ目のグループを作成します。 サブメニューには、4 列のガイド コマンドを含む最初のグループが含まれています。 サブメニューを保持する 2 番目のグループは、さまざまなコンテキスト メニューに配置する再利用可能なアセットです。これにより、それらのコンテキスト メニューにサブメニューが配置されます。
.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";
次に、GUID を .vsct ファイルに貼り付けて、Symbols
宣言に次の行を含めます。
<GuidSymbol name="guidColumnGuideCommandsPkg"
value="{ef726849-5447-4f73-8de5-01b9e930f7cd}" />
コマンド セットとビットマップ イメージ ファイルの GUID も、拡張機能に対して一意である必要があります。
<GuidSymbol name="guidColumnGuidesCommandSet"
value="{c2bc0047-8bfa-4e5a-b5dc-45af8c274d8e}">
<GuidSymbol name="guidImages" value="{2C99F852-587C-43AF-AA2D-F605DE2E46EF}">
ただし、コードを動作させるために、このチュートリアルのコマンド セットとビットマップ イメージ GUID を変更する必要はありません。 コマンド セット GUID は、ColumnGuideCommands.cs ファイル内の宣言と一致する必要がありますが、そのファイルの内容も置き換えます。そのため、GUID は一致します。
.vsct ファイル内の他の GUID は、列ガイド コマンドが追加される既存のメニューを識別するため、変更されることはありません。
ファイルのセクション。 .vsct には、コマンド、配置、シンボルという 3 つの外部セクションがあります。 コマンド セクションでは、コマンド グループ、メニュー、ボタンまたはメニュー項目、およびアイコンのビットマップを定義します。 placements セクションでは、グループがメニューのどこに移動するか、既存のメニューに追加の配置を行うかを宣言します。 symbols セクションは、.vsct ファイル内の別の場所で使用される識別子を宣言します。これにより、.vsct コードは、どこでも GUID や 16 進数を持つよりも読みやすくなります。
コマンド セクションでは、定義がグループ化されています。 コマンド セクションでは、最初にコマンド グループを定義します。 コマンドのグループは、メニューに表示されるコマンドで、グループを区切る灰色の線がわずかに表示されます。 グループは、この例のようにサブメニュー全体を塗りつぶすこともできます。この場合、灰色の区切り線は表示されません。 .vsct ファイルは、IDM_VS_MENU_EDIT
(メイン 編集 メニュー) に親される GuidesMenuItemsGroup
と、IDM_VS_CTXT_CODEWIN
(コード エディターのコンテキスト メニュー) に親されている GuidesContextMenuGroup
の 2 つのグループを宣言します。
2 番目のグループ宣言には、0x0600
優先順位があります。
<Group guid="guidColumnGuidesCommandSet" id="GuidesContextMenuGroup"
priority="0x0600">
この考え方は、サブメニュー グループを追加するコンテキスト メニューの末尾に、列ガイドのサブメニューを配置することです。 しかし、自分が最善であると仮定し、0xFFFF
の優先順位を使用してサブメニューを常に最後にするのは控えるべきです。 この数値を試して、サブメニューが配置されているコンテキスト メニューのどこにあるかを確認する必要があります。 この場合、0x0600
はメニューの最後に表示されるのに十分な高さですが、それが望ましい場合は、他の誰かが拡張機能を列ガイド拡張機能よりも低く設計する余地があります。
コマンドセクション 、メニュー定義。 次に、コマンド セクションでは、GuidesContextMenuGroup
に親されるサブメニュー GuidesSubMenu
を定義します。 GuidesContextMenuGroup
は、関連するすべてのコンテキスト メニューに追加するグループです。 配置セクションでは、4列構成のガイドコマンドを持つグループがこのサブメニューに配置されます。
コマンドセクション、ボタンの定義。 次に、コマンド セクションでは、4 列ガイド コマンドであるメニュー項目またはボタンを定義します。 CommandWellOnly
、上記で説明したコマンドは、メイン メニューに配置すると非表示であることを意味します。 メニュー項目ボタン宣言の 2 つ (ガイドの追加とガイドの削除) にも、AllowParams
フラグがあります。
<CommandFlag>AllowParams</CommandFlag>
このフラグを使用すると、メイン メニューの配置と共に、Visual Studio がコマンド ハンドラーを呼び出すときに引数を受け取るコマンドが有効になります。 ユーザーがコマンド ウィンドウからコマンドを実行した場合、引数はイベント引数のコマンド ハンドラーに渡されます。
コマンド セクション 、ビットマップ定義。 最後に、コマンド セクションでは、コマンドに使用されるビットマップまたはアイコンを宣言します。 このセクションは、プロジェクト リソースを識別し、使用されるアイコンの 1 から始まるインデックスを一覧表示する簡単な宣言です。 .vsct ファイルの symbols セクションは、インデックスとして使用される識別子の値を宣言します。 このチュートリアルでは、プロジェクトに追加されたカスタム コマンド項目テンプレートで提供されるビットマップ ストリップを使用します。
配置セクション。 コマンド セクションの後に placements セクションがあります。 1 つ目は、上記で説明した 4 列のガイド コマンドを保持する最初のグループを、コマンドが表示されるサブメニューに追加する場所です。
<CommandPlacement guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup"
priority="0x0100">
<Parent guid="guidColumnGuidesCommandSet" id="GuidesSubMenu" />
</CommandPlacement>
その他の配置はすべて、(GuidesSubMenu
を含む) GuidesContextMenuGroup
を他のエディター コンテキスト メニューに追加します。 コードが GuidesContextMenuGroup
を宣言すると、それはコード エディターのコンテキスト メニューに親として設定されます。 そのため、コード エディターのコンテキスト メニューの配置が表示されません。
シンボル セクション。 前述のように、symbols セクションは、.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 項目を選択し (項目を選択するだけでなく、項目の左側にあるチェック ボックスをオンにする必要があります)、[OK] 選択して参照を追加します。
初期化。 パッケージ クラスは、初期化時に、コマンド実装クラスで Initialize
を呼び出します。 ColumnGuideCommands
初期化では、クラスがインスタンス化され、クラス インスタンスとパッケージ参照がクラス メンバーに保存されます。
クラス コンストラクターのコマンド ハンドラーフックアップの 1 つを見てみましょう。
_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
オブジェクトがリッスンするイベントを起動します。 イベントが発生すると、これらのオブジェクトは、関連するテキスト ビューを新しい列ガイド設定で更新します。
コマンド ウィンドウからコマンドを呼び出す
列ガイドのサンプルを使用すると、ユーザーは拡張の形式としてコマンド ウィンドウから 2 つのコマンドを呼び出すことができます。 ビューを使用する場合 |その他のウィンドウ |コマンド ウィンドウ コマンド ウィンドウが表示されます。 コマンド ウィンドウを操作するには、「編集」と入力し、コマンド名の入力候補を指定し、引数 120 を指定すると、次のような結果になります。
> Edit.AddColumnGuide 120
>
この動作を有効にするサンプルの部分は、.vsct ファイル宣言、コマンド ハンドラーをフックするときの ColumnGuideCommands
クラス コンストラクター、およびイベント引数をチェックするコマンド ハンドラーの実装にあります。
コマンドは [編集] メニューの UI に表示されませんが、.vsctファイルに "<CommandFlag>CommandWellOnly</CommandFlag>
" があり、[編集] メイン メニューには配置があることを確認しました。 メインの 編集 メニューに置くと、Edit.AddColumnGuideなどの名前が付けられます。 4 つのコマンドを保持するコマンド グループ宣言は、編集 メニューにグループを直接配置しました。
<Group guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup"
priority="0xB801">
<Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_EDIT" />
</Group>
buttons セクションでは、コマンドをメイン メニューで非表示にしておく CommandWellOnly
を宣言し、AllowParams
で宣言しました。
<Button guid="guidColumnGuidesCommandSet" id="cmdidAddColumnGuide"
priority="0x0100" type="Button">
<Parent guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup" />
<Icon guid="guidImages" id="bmpPicAddGuide" />
<CommandFlag>CommandWellOnly</CommandFlag>
<CommandFlag>AllowParams</CommandFlag>
ColumnGuideCommands
クラスのコンストラクターのコマンド ハンドラーのフック コードによって、許可されるパラメーターの説明が提供されることを確認しました。
_addGuidelineCommand.ParametersDescription = "<column>";
GetApplicableColumn
関数は、エディターのビューで現在の列をチェックする前に、OleMenuCmdEventArgs
値をチェックしました。
private int GetApplicableColumn(EventArgs e)
{
var inValue = ((OleMenuCmdEventArgs)e).InValue as string;
if (!string.IsNullOrEmpty(inValue))
{
int column;
if (!int.TryParse(inValue, out column) || column < 0)
throw new ArgumentException("Invalid column");
return column;
}
拡張機能を試す
F5 押して、列ガイド拡張機能を実行できるようになりました。 テキスト ファイルを開き、エディターのコンテキスト メニューを使用して、ガイド行の追加、削除、色の変更を行います。 (行の末尾の後の空白ではなく) テキスト内をクリックすると垂直グリッド ガイドが追加され、それ以外の場合は、エディターによって行の最後の列に追加されます。 コマンド ウィンドウを使用し、引数を指定してコマンドを呼び出す場合は、任意の場所に列ガイドを追加できます。
さまざまなコマンドの配置、名前の変更、アイコンの変更などを試したい場合、メニューに最新のコードが表示されている Visual Studio で問題が発生した場合は、デバッグ中の実験用ハイブをリセットできます。 Windowsのスタートメニュー 表示し、「リセット」と入力します。 コマンド を探して実行し、次の Visual Studio Experimental Instanceをリセットします。 このコマンドは、すべての拡張機能コンポーネントの実験用レジストリ ハイブをクリーンアップします。 コンポーネントから設定がクリーンアップされないため、Visual Studio の試験的なハイブをシャットダウンしたときに使用したガイドは、次回の起動時にコードが設定ストアを読み取ったときに残ります。
完成したコード プロジェクト
Visual Studio Extensibility サンプルの GitHub プロジェクトが間もなく存在し、完成したプロジェクトがそこに存在します。 この記事は、それが発生したときにそこを指し示すために更新されます。 完成したサンプル プロジェクトには異なる guid があり、コマンド アイコン用に異なるビットマップ ストリップが含まれる場合があります。
この Visual Studio ギャラリー 拡張機能を使用して、列ガイド機能のバージョン試すことができます。