Visual Studio 2022 の「出力」ウィンドウを右クリックしたときに、自作ツール用のメニューを追加したい──そう思って試してみると、DocumentContextMenu にぶら下げても全く出てこない…というハマり方をしがちです。本記事では、出力ウィンドウが属している本当のコンテキストメニュー ID と、VSIX(拡張機能)での実装手順を、最小構成の .vsct と C# コード付きで詳しく解説します。
Visual Studio の「出力」ウィンドウに独自コマンドを追加したい
まず今回のゴールを整理します。
- 対象:Visual Studio 2022(VSIX 拡張機能)
- 場所:「出力 (Output)」ウィンドウの右クリック(コンテキスト)メニュー
- やりたいこと:
- 選択したテキストを元に独自処理を実行するメニュー項目を追加したい
- できれば「出力」ウィンドウ以外では表示したくない
このとき、多くの人が最初に試すのが、Command Explorer で DocumentWindowCommandSet / DocumentContextMenu を探して .vsct に設定する方法です。しかし、これはエディタ(ドキュメント)用のコンテキストメニューであり、「出力」ウィンドウには適用されません。
答えから言うと、「出力」ウィンドウの右クリックメニューは IDM_VS_CTXT_RESULTSLIST にぶら下がっています。つまり、.vsct での親メニュー指定を
- Document 系メニュー → ×
guidSHLMainMenu/IDM_VS_CTXT_RESULTSLIST→ ○
とすることで、はじめて「出力」ウィンドウのコンテキストメニューに自作コマンドを表示できます。
なぜ DocumentContextMenu ではダメなのか
まず、Visual Studio の「コンテキストメニュー」がどう整理されているのかを軽く押さえておきます。
| コンテキスト種別 | 代表的な ID | 代表的な対象 | 出力ウィンドウとの関係 |
|---|---|---|---|
| ドキュメントエディタ系 | DocumentContextMenu(Command Explorer 上) | C# / C++ / XAML などのエディタ | 出力ウィンドウとは別物 |
| 結果一覧(Results List)系 | IDM_VS_CTXT_RESULTSLIST | 出力ウィンドウ、エラー一覧など | 出力ウィンドウはここ |
| ソリューションエクスプローラー系 | IDM_VS_CTXT_SOLNNODE など | プロジェクト / ファイルノード | 今回は関係なし |
Command Explorer 上で見ると「ドキュメントのコンテキストメニュー」がそれらしく見えますが、出力ウィンドウは Text Editor ではなく別カテゴリの「結果系ウィンドウ」扱いです。Visual Studio SDK の vsshlids.h には、こうしたコンテキストメニュー用の ID が IDM_VS_CTXT_* という形で多数定義されており、その中の一つが IDM_VS_CTXT_RESULTSLIST です。
従って、.vsct で「親メニュー」を Document 系にしてしまうと、出力ウィンドウとはそもそもコンテキストが違うため、どんなに正しくコマンドを定義しても一切表示されません。
最小構成の .vsct で「出力」ウィンドウにメニューを追加する
ここからは実際の .vsct の最小構成例を元に、どこをどう書けばよいかを分解して解説します。
.vsct の全体イメージ
まずは、出力ウィンドウ右クリックに「選択テキストで処理する」というメニューを1つだけ追加する、最小構成の .vsct 例です。
<?xml version="1.0" encoding="utf-8"?>
<CommandTable xmlns="http://schemas.microsoft.com/VisualStudio/2005-10-18/CommandTable">
<Extern href="stdidcmd.h" />
<Extern href="vsshlids.h" />
<Commands package="guidMyPackage">
<Groups>
<!-- 出力ウィンドウの右クリックに載せるグループ -->
<Group guid="guidMyCmdSet" id="MyMenuGroup" priority="0x0600">
<Parent guid="guidSHLMainMenu" id="IDM_VS_CTXT_RESULTSLIST" />
</Group>
</Groups>
<Buttons>
<Button guid="guidMyCmdSet" id="Command1Id" priority="0x0100" type="Button">
<Parent guid="guidMyCmdSet" id="MyMenuGroup" />
<Strings>
<ButtonText>選択テキストで処理する</ButtonText>
</Strings>
</Button>
</Buttons>
</Commands>
<Symbols>
<GuidSymbol name="guidMyPackage" value="{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}" />
<GuidSymbol name="guidMyCmdSet" value="{YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY}">
<IDSymbol name="MyMenuGroup" value="0x1020" />
<IDSymbol name="Command1Id" value="0x0100" />
</GuidSymbol>
</Symbols>
</CommandTable>
この中で特に重要なのは、次の2点です。
<Extern href="vsshlids.h" />を追加していること<Parent guid="guidSHLMainMenu" id="IDM_VS_CTXT_RESULTSLIST" />を使っていること
guidSHLMainMenu と IDM_VS_CTXT_RESULTSLIST は Visual Studio 側で定義されている GUID / ID なので、自分で値を考える必要はありません。Extern で vsshlids.h を読み込むことで、これらのシンボルが解決される仕組みになっています。
.vsct の各要素の意味
| 要素 | 役割 | 今回のポイント |
|---|---|---|
<Extern> | Visual Studio 標準のコマンド ID を参照する | vsshlids.h を参照することで IDM_VS_CTXT_RESULTSLIST が使える |
<Group> | 複数のコマンドをまとめて配置する「グループ」 | Parent でどのメニューにぶら下がるかを指定 |
Parent guid="guidSHLMainMenu" id="IDM_VS_CTXT_RESULTSLIST" | 親メニューの指定 | ここが「出力」ウィンドウ右クリックのコンテキストに対応する |
<Button> | 実際のコマンド(メニュー項目)を定義 | 表示名(ButtonText)やグループを指定 |
<Symbols> | 自分の GUID / ID を定義 | GUID は VSIX マニフェストのものと揃える |
AsyncPackage 側の準備:ProvideMenuResource とコマンド登録
.vsct を書いただけではコマンドは動きません。VSIX プロジェクト側で AsyncPackage にいくつか設定を行い、コマンドの実装と紐付ける必要があります。
AsyncPackage に属性を付与する
拡張機能側のパッケージクラス(通常 MyPackage など)に、最低限次のような属性を付与します。
[PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)]
[InstalledProductRegistration("My Extension", "Sample command on Output window", "1.0")]
[ProvideMenuResource("Menus.ctmenu", 1)]
[Guid("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")]
public sealed class MyPackage : AsyncPackage
{
}
ここで重要なのは [ProvideMenuResource("Menus.ctmenu", 1)] です。これがないと、せっかく作成した .vsct が読み込まれず、メニュー項目自体が登録されません。
InitializeAsync でコマンドを登録する
次に、InitializeAsync で OleMenuCommandService を取得し、コマンドを登録します。
protected override async Task InitializeAsync(
CancellationToken cancellationToken,
IProgress<ServiceProgressData> progress)
{
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
if (await GetServiceAsync(typeof(IMenuCommandService)) is OleMenuCommandService mcs)
{
var cmdSet = new Guid("YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY"); // guidMyCmdSet
var cmdId = new CommandID(cmdSet, 0x0100); // Command1Id
var menuItem = new OleMenuCommand(ExecuteCommand, cmdId);
menuItem.BeforeQueryStatus += UpdateCommandStatus;
mcs.AddCommand(menuItem);
}
}
ここで使っている GUID と ID は、.vsct の GuidSymbol / IDSymbol に定義したものと一致させる必要があります。
guidMyCmdSet→ C# 側のcmdSetCommand1Id (0x0100)→ C# 側のCommandIDの ID 部分
「出力」ウィンドウの選択テキストを取得する
今回の主目的は「選択したテキストで処理する」ことなので、出力ウィンドウの選択文字列を取得する必要があります。出力ウィンドウは内部的には IVsTextView として扱うことができるため、IVsTextManager からアクティブビューを取得し、GetSelectedText を呼び出せば簡単に取得できます。
using Microsoft.VisualStudio.TextManager.Interop;
using Microsoft.VisualStudio.Shell;
using Task = System.Threading.Tasks.Task;
private async Task<string?> GetSelectedTextAsync()
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
var textMgr = await GetServiceAsync(typeof(SVsTextManager)) as IVsTextManager;
if (textMgr is null)
{
return null;
}
textMgr.GetActiveView(fMustHaveFocus: 1, pBuffer: null, ppView: out IVsTextView view);
if (view is null)
{
return null;
}
view.GetSelectedText(out var selected);
return string.IsNullOrEmpty(selected) ? null : selected;
}
上記は AsyncPackage のメソッドとして実装しているイメージです。ExecuteCommand(メニュー選択時のハンドラ)から呼び出して使います。
private async void ExecuteCommand(object sender, EventArgs e)
{
try
{
var selected = await GetSelectedTextAsync();
if (string.IsNullOrEmpty(selected))
{
await VS.MessageBox.ShowAsync("情報",
"出力ウィンドウでテキストが選択されていません。");
return;
}
// ここで好きな処理を行う
// 例: 外部ツールに渡す、正規表現で解析する、など
await VS.MessageBox.ShowAsync("選択テキスト", selected);
}
catch (Exception ex)
{
await VS.MessageBox.ShowAsync("エラー", ex.ToString());
}
}
このように、出力ウィンドウがアクティブな状態で右クリックメニューからコマンドを実行すれば、選択テキストを簡単に取得して任意の処理に渡すことができます。
「出力」ウィンドウのときだけメニューを表示したい
ここまでの実装だと、同じ IDM_VS_CTXT_RESULTSLIST を使っている他のウィンドウ(例:エラー一覧など)でもメニューが表示されてしまいます。場合によってはそれでも問題ありませんが、「出力ウィンドウだけに絞りたい」ケースもよくあります。
その場合は、BeforeQueryStatus(OleMenuCommand.BeforeQueryStatus)イベントで、アクティブなツールウィンドウを判定し、不要なときは Visible = false にする方法がシンプルです。
BeforeQueryStatus で可視性を制御する例
using Microsoft.VisualStudio.Shell.Interop;
private void UpdateCommandStatus(object sender, EventArgs e)
{
ThreadHelper.ThrowIfNotOnUIThread();
if (sender is not OleMenuCommand cmd)
{
return;
}
var shell = GetService(typeof(SVsUIShell)) as IVsUIShell;
if (shell is null)
{
cmd.Visible = false;
return;
}
shell.GetCurrentActiveToolWindow(out var frame);
if (frame is null)
{
cmd.Visible = false;
return;
}
// ツールウィンドウの GUID を取得
frame.GetGuidProperty(
(int)__VSFPROPID.VSFPROPID_GuidPersistenceSlot,
out var guid);
// 出力ウィンドウの GUID(固定値)
var outputGuid = new Guid(ToolWindowGuids.Outputwindow);
cmd.Visible = guid == outputGuid;
cmd.Enabled = cmd.Visible;
}
こうすることで、エラー一覧など他の Results List 系ウィンドウではメニューを非表示にし、出力ウィンドウがアクティブなときだけ表示・有効化されるようになります。
コンテキストメニュー ID の調べ方とよくある勘違い
最後に、「なぜ IDM_VS_CTXT_RESULTSLIST と分かるのか?」という点についても触れておきます。これは主に次の2つのアプローチで確認できます。
Command Explorer を使う場合
- Visual Studio 付属の Command Explorer(拡張機能など)で、出力ウィンドウを右クリックした状態からメニューを特定する
- このとき「DocumentWindowCommandSet / DocumentContextMenu」ではなく、Results List 系のメニューに紐付いていることを確認できる
Command Explorer では GUID / ID の組み合わせがそのまま表示されるので、そこから guidSHLMainMenu / IDM_VS_CTXT_RESULTSLIST といった情報を辿ることができます。
vsshlids.h を確認する場合
- Visual Studio SDK には
vsshlids.hが含まれており、そこにIDM_VS_CTXT_*が大量に定義されています - この中に
#define IDM_VS_CTXT_RESULTSLISTといった形で定義が存在します
「IDM_VS_CTXT_OUTPUTWINDOW という ID は存在しないのか?」と探したくなりますが、その名前の ID は定義されていません。出力ウィンドウはあくまで「Results List ファミリー」の一員として扱われている、という理解がポイントです。
よくあるハマりポイントまとめ
| 現象 | ありがちな原因 | 確認ポイント |
|---|---|---|
| メニューがまったく表示されない | 親メニューが Document 系になっている / ProvideMenuResource がない | guidSHLMainMenu × IDM_VS_CTXT_RESULTSLIST を使っているか |
| アイコン・テキストが出ない | .vsct の <Buttons> に正しく定義されていない | guidMyCmdSet と ID が C# 側と一致しているか |
| クリックしても何も起きない | InitializeAsync でコマンド登録していない | OleMenuCommandService.AddCommand を呼んでいるか |
| 出力ウィンドウ以外にもメニューが出る | 可視性制御をしていない | BeforeQueryStatus でツールウィンドウ GUID を判定しているか |
実用例:出力ログからチケット番号・URL を抽出する
ここまでで基礎的なところは一通り揃ったので、少しだけ実用的な使い方の例も紹介しておきます。例えば、ビルドログや CI ログにチケット番号や URL が頻繁に出てくる状況を想像してみてください。
- ログの一部を選択
- 右クリック → 「チケット番号を開く」メニュー
- 選択テキストからチケット番号を正規表現で抽出し、Issue トラッカーをブラウザで開く
といったことは、先ほどの GetSelectedTextAsync を起点に簡単に実装できます。イメージとしてはこんな感じです。
private static readonly Regex TicketRegex =
new Regex(@"TICKET-(\d+)", RegexOptions.Compiled);
private async void ExecuteCommand(object sender, EventArgs e)
{
var selected = await GetSelectedTextAsync();
if (string.IsNullOrEmpty(selected))
{
await VS.MessageBox.ShowAsync("情報", "テキストを選択してください。");
return;
}
var match = TicketRegex.Match(selected);
if (!match.Success)
{
await VS.MessageBox.ShowAsync("情報", "チケット番号が見つかりません。");
return;
}
var ticketNo = match.Value;
var url = $"https://example.com/issues/{ticketNo}";
System.Diagnostics.Process.Start(new ProcessStartInfo
{
FileName = url,
UseShellExecute = true
});
}
このように、出力ウィンドウを単なる「ログの表示場所」としてではなく、「右クリック1つで外部の仕組みと連携するローコード的 UI」として活用できるのが、コンテキストメニュー拡張の大きなメリットです。
全体の流れをおさらい
ここまでの内容を、実装作業のステップとして整理し直してみます。
| ステップ | 内容 | ポイント |
|---|---|---|
| 1 | VSIX プロジェクトを作成 | AsyncPackage をベースにする |
| 2 | .vsct を用意 | Extern vsshlids.h と IDM_VS_CTXT_RESULTSLIST を忘れない |
| 3 | ProvideMenuResource 属性を追加 | これがないと .vsct が読み込まれない |
| 4 | InitializeAsync でコマンド登録 | OleMenuCommandService から OleMenuCommand を作る |
| 5 | 選択テキスト取得ロジックを実装 | SVsTextManager → IVsTextView.GetSelectedText |
| 6 | BeforeQueryStatus で可視性制御(必要に応じて) | 出力ウィンドウ以外では Visible=false に |
| 7 | 必要なビジネスロジックを実装 | 外部ツール連携、URL オープンなど自由に拡張 |
まとめ:出力ウィンドウは「Results List」として扱うのが正解
最後に、本記事のポイントを簡潔にまとめます。
- Visual Studio の「出力」ウィンドウはドキュメントエディタではなく Results List 系のコンテキストに属している
- そのため、
DocumentWindowCommandSet / DocumentContextMenuを親にしてもメニューは表示されない - .vsct では、親メニューとして
guidSHLMainMenuIDM_VS_CTXT_RESULTSLIST
ProvideMenuResource属性とInitializeAsyncでのコマンド登録を忘れないこと- 選択テキストは
SVsTextManager/IVsTextViewから簡単に取得できる BeforeQueryStatusでアクティブツールウィンドウを判定すれば、「出力」ウィンドウ専用のメニューとして振る舞わせることも可能
一見ささいな違いに見えますが、「出力」ウィンドウがどのコンテキストメニューにぶら下がっているかを正しく押さえておくことで、無駄な試行錯誤を避け、拡張の幅を大きく広げることができます。ビルドログやテスト結果、デバッグ出力など、日々眺めている「出力」ウィンドウを、ちょっとした自動化や外部連携の入口として活用してみてください。

コメント