Visual Studio の拡張機能(VSIX / VSPackage)で Tool Window を提供していると、「タブ(タイトル部分)を右クリックしたときのコンテキストメニューに、独自のコマンドを追加したい」と思う場面があります。しかし VS SDK の情報は点在していて、どのメニュー ID を親にすればよいか迷いがちです。この記事では、Tool Window の“タブ/枠(タイトル周り)”の右クリックメニューに項目を追加するためのメニュー ID と、.vsct(CommandTable)の実装例、そして“自分のツールウィンドウだけに表示する”実践的な制御までまとめます。
結論:Tool Window のタブ/タイトル右クリックメニューは VSCT で拡張できる
結論から言うと、可能です。Tool Window の「ウィンドウフレーム(枠)/タブ」のコンテキストメニューには既存のメニュー ID が用意されており、そこを Parent(親) に指定して .vsct に Group / Button を追加すれば、右クリックメニューに自分のコマンドを差し込めます。
ここで重要なのは、「Tool Window の右クリック」と一口に言っても、対象が 2 種類あることです。
まず整理:Tool Window の右クリックは“2種類”ある
Tool Window で発生する右クリックは、主に次の 2 つに分かれます。
| 右クリックする場所 | 対象 | 追加方法の基本 | よくある勘違い |
|---|---|---|---|
| ツールウィンドウの“中身” | WPF/WinForms のコントロール領域 | XAML / コントロール側の ContextMenu で作る | VSCT で追加しようとしてハマる |
| タブ/タイトル周り(枠) | Window Frame / Tab の UI | VSCT(.vsct)で既存メニュー ID にぶら下げる | どのメニュー ID を使うか分からない |
本記事で扱うのは後者、つまり 「ツールウィンドウのタブ/枠(タイトル周り)」の右クリックメニュー を拡張する話です。
使うべき既存メニュー ID:IDM_VS_CTXT_EZTOOLWINTAB / IDM_VS_CTXT_DOCKEDWINDOW
Tool Window のコンテキストメニュー(Window Frame / Tab)として、実務上まず押さえるべき ID は次の 2 つです。
| メニュー ID | 出る場所のイメージ | 狙いどころ | 実装上の使い分け |
|---|---|---|---|
| IDM_VS_CTXT_EZTOOLWINTAB | Tool Window のタブ側(ドキュメントタブ風) | タブ(タイトル部分)を右クリック | まずはここに追加すると“タブ右クリック”で見つかりやすい |
| IDM_VS_CTXT_DOCKEDWINDOW | ドッキングされたウィンドウ枠側 | タイトルバー/枠(ドッキング UI)を右クリック | ドッキング状態の見た目・操作に合わせてこちらにも追加する |
Visual Studio のレイアウトや表示形態(タブ表示か、ドッキング枠か)によって、ユーザーが右クリックする場所が変わります。「タブ右クリックで確実に出したい」なら EZTOOLWINTAB、「枠側でも出したい」なら DOCKEDWINDOW も追加、という設計が分かりやすいです。
実装の全体像:VSCT に Group / Button を追加する
やることはシンプルで、流れは次の通りです。
- .vsct に vsshlids.h / stdidcmd.h を Extern で取り込む(既存 ID を参照できるようにする)
- Groups を作り、Parent を guidSHLMainMenu + 対象メニュー ID に設定
- その Group を親にして Buttons(コマンド) を追加する(必要なら CommandPlacements で複数箇所に配置)
- 必要に応じて BeforeQueryStatus 等で「自分の Tool Window のときだけ表示」にする
次から、VSCT の具体例と、C# 側のコマンド実装例をセットで紹介します。
.vsct(CommandTable)の具体例
1) Extern で既存 ID を参照できるようにする
.vsct の先頭付近で、Visual Studio が持っている既存の ID 定義を取り込みます。ここが抜けると、IDM_VS_CTXT_EZTOOLWINTAB などを参照できず、ビルドでエラーになったり、別名で定義し直す羽目になったりします。
<Extern href="stdidcmd.h" />
<Extern href="vsshlids.h" />
配置場所は通常、<CommandTable> の直下(またはテンプレートが用意している所)に書きます。
2) Group を作り、Parent を guidSHLMainMenu + メニュー ID にする
右クリックメニューに“差し込み口”を作るイメージです。Group を作成し、その親を該当コンテキストメニューに設定します。
<Groups>
<Group guid="guidMyPackageCmdSet" id="grpToolWinTabCtx" priority="0x0600">
<Parent guid="guidSHLMainMenu" id="IDM_VS_CTXT_EZTOOLWINTAB" />
</Group>
<Group guid="guidMyPackageCmdSet" id="grpDockedWinCtx" priority="0x0600">
<Parent guid="guidSHLMainMenu" id="IDM_VS_CTXT_DOCKEDWINDOW" />
</Group>
</Groups>
priority は並び順に影響します。正解は 1 つではありませんが、最初は 0x0600 など中間的な値にして、必要なら調整するのが無難です。
| 項目 | 役割 | 実務での目安 |
|---|---|---|
| Group | メニュー内の“区切り(セクション)” | 自分のコマンド群をまとめ、他のメニューと自然に分離する |
| Parent | どのメニューに表示するか | guidSHLMainMenu + IDM_VS_CTXT_EZTOOLWINTAB / IDM_VS_CTXT_DOCKEDWINDOW |
| priority | 表示位置の調整 | まずは固定値で置き、必要に応じて微調整 |
3) Button(コマンド)を定義し、Group の下に配置する
次にコマンド本体(Button)を作ります。まずは EZTOOLWINTAB 側に素直にぶら下げ、同じコマンドを DOCKEDWINDOW 側にも出したい場合は CommandPlacement を追加するのが分かりやすい構成です。
<Buttons>
<Button guid="guidMyPackageCmdSet" id="cmdidMyToolWinAction" priority="0x0100" type="Button">
<Parent guid="guidMyPackageCmdSet" id="grpToolWinTabCtx" />
<Strings>
<ButtonText>My Tool Window の処理</ButtonText>
</Strings>
</Button>
</Buttons>
<CommandPlacements>
<CommandPlacement guid="guidMyPackageCmdSet" id="cmdidMyToolWinAction" priority="0x0100">
<Parent guid="guidMyPackageCmdSet" id="grpDockedWinCtx" />
</CommandPlacement>
</CommandPlacements>
これで、タブ側の右クリック(EZTOOLWINTAB)にも、ドッキング枠側の右クリック(DOCKEDWINDOW)にも同じコマンドを配置できます。
4) Symbols の例(GUID / ID の定義)
guid と id は、VSCT の Symbols で定義します。テンプレートに近い形でまとめると見通しが良くなります。
<Symbols>
<GuidSymbol name="guidMyPackageCmdSet" value="{11111111-2222-3333-4444-555555555555}">
<IDSymbol name="grpToolWinTabCtx" value="0x1020" />
<IDSymbol name="grpDockedWinCtx" value="0x1021" />
<IDSymbol name="cmdidMyToolWinAction" value="0x0100" />
</GuidSymbol>
</Symbols>
もちろん GUID はプロジェクトごとに固有のものを使います。既存の cmdset GUID(テンプレートが作るもの)を流用しても構いません。
C# 側(VSPackage/VSIX)の実装例:OleMenuCommand で登録する
VSCT でメニューに項目を置いても、クリック時の処理は C#(または他言語)側で実装します。ここでは VSIX の典型構成(AsyncPackage + OleMenuCommand)を前提にした、最小限で実務的な例を示します。
Package 側の前提(ProvideMenuResource)
VSCT を使うために、Package クラスに ProvideMenuResource が付いていることを確認します。テンプレートで作っていれば基本的に入っています。
[ProvideMenuResource("Menus.ctmenu", 1)]
public sealed class MyPackage : AsyncPackage
{
protected override async Task InitializeAsync(
CancellationToken cancellationToken,
IProgress<ServiceProgressData> progress)
{
await MyToolWinCommand.InitializeAsync(this);
}
}
※上のコード内に generics 記法がある場合、WordPress の貼り付けで崩れることがあります。必要ならコードブロック内では型を単純化しても問題ありません(本記事の主題はメニュー ID と VSCT です)。
コマンド登録(InitializeAsync)と実行処理
コマンドを OleMenuCommandService に登録します。右クリックメニューに出すだけなら、ここはよくあるテンプレ構成で十分です。
using System;
using System.ComponentModel.Design;
using System.Threading.Tasks;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
internal sealed class MyToolWinCommand
{
public const int CommandId = 0x0100;
public static readonly Guid CommandSet = new Guid("11111111-2222-3333-4444-555555555555");
// 自分の Tool Window の GUID(ToolWindowPane の Guid 属性と揃える)
private static readonly Guid MyToolWindowGuid = new Guid("AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE");
private readonly AsyncPackage _package;
private readonly IVsUIShell _uiShell;
private MyToolWinCommand(AsyncPackage package, OleMenuCommandService commandService, IVsUIShell uiShell)
{
_package = package;
_uiShell = uiShell;
var menuCommandId = new CommandID(CommandSet, CommandId);
var menuItem = new OleMenuCommand(Execute, menuCommandId);
// 重要:他のツールウィンドウにも出てしまうのを防ぐ
menuItem.BeforeQueryStatus += BeforeQueryStatus;
commandService.AddCommand(menuItem);
}
public static async Task InitializeAsync(AsyncPackage package)
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(package.DisposalToken);
var commandService = await package.GetServiceAsync(typeof(IMenuCommandService)) as OleMenuCommandService;
var uiShell = await package.GetServiceAsync(typeof(SVsUIShell)) as IVsUIShell;
if (commandService == null || uiShell == null)
return;
_ = new MyToolWinCommand(package, commandService, uiShell);
}
private void BeforeQueryStatus(object sender, EventArgs e)
{
ThreadHelper.ThrowIfNotOnUIThread();
var cmd = (OleMenuCommand)sender;
bool isMine = IsMyToolWindowActive();
cmd.Visible = isMine;
cmd.Enabled = isMine;
}
private bool IsMyToolWindowActive()
{
ThreadHelper.ThrowIfNotOnUIThread();
if (_uiShell.GetCurrentWindowFrame(out IVsWindowFrame frame) != VSConstants.S_OK || frame == null)
return false;
// アクティブなフレームが自分のツールウィンドウか判定
if (frame.GetGuidProperty((int)__VSFPROPID.VSFPROPID_GuidPersistenceSlot, out Guid slotGuid) == VSConstants.S_OK)
{
return slotGuid == MyToolWindowGuid;
}
return false;
}
private void Execute(object sender, EventArgs e)
{
ThreadHelper.ThrowIfNotOnUIThread();
VsShellUtilities.ShowMessageBox(
_package,
"Tool Window タブ/枠のコンテキストメニューから実行されました。",
"MyToolWinCommand",
OLEMSGICON.OLEMSGICON_INFO,
OLEMSGBUTTON.OLEMSGBUTTON_OK,
OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
}
}
この例のポイントは BeforeQueryStatus です。Tool Window のタブ/枠のコンテキストメニューは Visual Studio 全体の UI なので、VSCT だけで追加すると 他のツールウィンドウ(ソリューションエクスプローラー等)の右クリックにも出てしまう可能性があります。
そこで、アクティブな Window Frame が自分の Tool Window かどうかを判定して、該当時だけ Visible/Enabled を true にしています。拡張機能としての “行儀の良さ” に直結するので、実務では強くおすすめします。
VSCT で追加できるのは“枠(フレーム)のメニュー”。中身は別物
繰り返しになりますが、ここが混同ポイントです。
- タブ/タイトル(枠)の右クリック:Visual Studio が持つフレーム UI のメニュー → VSCT で拡張
- 中身(コントロール領域)の右クリック:あなたが作った WPF/WinForms の UI → XAML / コントロール側で ContextMenu
例えば WPF の中身に独自の右クリックメニューを出したいなら、VSCT ではなく次のように書くのが基本です。
<Grid>
<Grid.ContextMenu>
<ContextMenu>
<MenuItem Header="中身の右クリックメニュー" />
</ContextMenu>
</Grid.ContextMenu>
</Grid>
この「中身の ContextMenu」と、「タブ/枠の ContextMenu」を両方整えると、ユーザーがどこを右クリックしても期待した操作ができる UI になります。逆に、片方だけを VSCT で何とかしようとすると遠回りになりがちです。
実装チェックリスト:表示されない/出る場所が違う時の確認点
「ビルドは通るのにメニューに出ない」「思った場所に出ない」というときは、次の観点で切り分けると解決が速いです。
| 症状 | ありがちな原因 | 確認・対処 |
|---|---|---|
| 右クリックしても項目が一切出ない | Parent のメニュー ID が違う/狙いが違う | IDM_VS_CTXT_EZTOOLWINTAB(タブ)と IDM_VS_CTXT_DOCKEDWINDOW(枠)を両方試し、右クリック位置も変えて確認する |
| VSCT のビルドで ID が見つからない | Extern が不足 | <Extern href=”vsshlids.h” /> と <Extern href=”stdidcmd.h” /> を追加する |
| 表示されるが、他のツールウィンドウにも出る | VSCT は“メニューに置く”だけで、表示条件がない | BeforeQueryStatus で自分の Tool Window のときだけ Visible にする(本文の C# 例) |
| 表示されるが、クリックしても何も起きない | コマンド登録(OleMenuCommandService)ができていない | InitializeAsync が呼ばれているか、CommandId/Guid が VSCT の定義と一致しているか確認 |
| ある状態では出るが、別レイアウトだと出ない | タブ表示とドッキング枠でメニューが分かれる | EZTOOLWINTAB と DOCKEDWINDOW の 両方に配置(CommandPlacements を使う) |
| 更新したのに反映されない | Visual Studio のキャッシュや Experimental Instance の状態 | Experimental Instance の再起動、必要なら拡張機能の再インストールで確認 |
“自分のツールウィンドウだけ”に自然に出すための設計ヒント
Tool Window のタブ/枠のコンテキストメニューは、ユーザーにとって「その場のメニュー」です。そこに無関係なコマンドが並ぶと、拡張機能全体の印象が悪くなります。そこで、実務では次の設計にすると事故が減ります。
- まずは VSCT で追加し、期待した右クリック場所に出ることを確認する
- 次に BeforeQueryStatus で Visible/Enabled を制御し、他のウィンドウに出ないようにする
- コマンドが複数あるなら Group でまとめる(区切りがつき、見た目が整う)
- タブ側と枠側で操作感が変わる環境を想定し、必要なら両方に配置する
また、表示制御の判定は「現在のアクティブフレームが自分か」で十分なケースが多いです。より厳密に「右クリックされたフレームはどれか」を追いたい場合は別のサービスを使った判定も考えられますが、まずはシンプルに組むほうが保守しやすく、ユーザー体験も安定します。
まとめ:探していた“ぶら下げ先”はこの2つ
- Tool Window のタブ/枠(タイトル周り)の右クリックメニューには、既存メニュー ID がある
- タブ側は IDM_VS_CTXT_EZTOOLWINTAB、ドッキング枠側は IDM_VS_CTXT_DOCKEDWINDOW
- .vsct では Extern(vsshlids.h / stdidcmd.h) → Group(Parent をメニュー ID) → Button の順で追加する
- “他のツールウィンドウにも出る”問題は、BeforeQueryStatus で自分の Tool Window のときだけ表示にすると解決しやすい
- Tool Window の“中身”の右クリックは VSCT ではなく WPF/WinForms 側の ContextMenu が基本

コメント