В данном разделе рассматривается расширение Visual Studio через интеграцию в среду пользовательского инструментального окна. Будут затронуты вопросы регистрации и инициализации пользовательских окон в модулях вида VSPackage и Add-In, отображения в окне пользовательских компонентов, обработки событий и контроля состояния окна.
Эта статья устарела. Обновленную версию этой статьи вы можете прочитать здесь.
Инструментальные окна (tool window) — дочерние окна MDI (Multiple Document Interface) интерфейса Visual Studio предназначенные для вывода информации. Solution Explorer и Error List являются инструментальными окнами. Обычно содержимое инструментального окна не связывается с файлом и не содержит редакторов, для этого существуют специальные окна документов.
Модуль-расширение PVS-Studio интегрирует в IDE несколько инструментальных окон, главным из которых является окно вывода результатов анализа (PVS-Studio Output Window). Из него уже можно открыть другие окна, например окно поиска по списку. Окно вывода результатов PVS-Studio доступно из главного меню Visual Studio (PVS-Studio -> Show PVS-Studio Output Window) и автоматически открывается при запуске анализа.
В большинстве случаев IDE создаёт и использует только один экземпляр каждого инструментального окна (single instance window), причём экземпляр этого окна остаётся открытым до конца сеанса работы среды. При нажатии на кнопку закрытия такое окно скрывается, а при последующем обращении к нему оно вновь становится видимым, причём все отражённые в нём данные сохраняются. Однако создание и интеграция в IDE Multi-Instance пользовательских окон (т.е. окон, которые можно открывать по нескольку раз) также возможны. Инструментальное окно может быть закреплено за определённым контекстом интерфейса IDE (т.н. динамические окна). Такое окно будет автоматически показано пользователю при его попадании в данный контекст.
Интеграция инструментальных окон в IDE поддерживается в VSPackage и Add-In модулях расширений (причём реализации для этих видов модулей различаются), и требует задания их первоначальных параметров и регистрации в системном реестре.
Устанавливаемый вместе с Visual Studio SDK шаблон проекта VSPackage позволяет сгенерировать пользовательское инструментальное окно для создаваемого им проекта пакета модуля-расширения. Этот проект уже должен содержать все рассмотренные ниже элементы, поэтому его удобно использовать как образец при знакомстве с процессом интеграции пользовательских окон в Visual Studio.
Регистрация пользовательского окна в среде требует добавления информации о нём в специальный раздел системного реестра ветки Visual Studio. Этот процесс может быть автоматизирован с помощью генерации pkgdef файла, который будет содержать в себе всю необходимую регистрационную информацию об окне. Содержимое pkgdef файла задаётся с помощью специальных регистрационных атрибутов подкласса Package.
За непосредственную регистрацию пользовательского инструментального окна в модуль VSPackage отвечает атрибут ProvideToolWindow класса Package:
[ProvideToolWindow(typeof(MyWindowPane), Orientation =
ToolWindowOrientation.Right, Style = VsDockStyle.Tabbed, Window =
Microsoft.VisualStudio.Shell.Interop.ToolWindowGuids.Outputwindow,
MultiInstances = false, Transient = true, Width = 500, Height = 250,
PositionX = 300, PositionY = 300)]
Рассмотрим некоторые из параметров данного атрибута. Typeof указывает на пользовательскую реализацию клиентской области окна (ToolWindowPane). Параметр MultiInstances позволяет использовать окно в Multi-Instance режиме, т.е. с возможностью открывать несколько экземпляров окна одновременно. Параметры Orientaton, Size и Style позволяют задать первоначальное положение окна при первом открытии. Стоит помнить, что после первого открытия окна IDE будет сохранять его положение при дальнейших запусках среды. Transient определяет, будет ли окно открыто сразу после загрузки среды Visual Studio в случае, если оно уже было открыто в предыдущем сеансе IDE.
Стоит заметить, что инициализация пользовательского окна в VSPackage (что будет описано чуть позднее) не обязательно совпадает с инициализацией самого подкласса Package, для которого мы и указываем данный регистрационный атрибут. Например, после реализации инструментального окна для плагина PVS-Studio мы сталкивались с ситуацией, при которой окно оказывалось открытым (но не активным) среди нижних вкладок окон среды сразу после запуска Visual Studio, даже несмотря на то, что в атрибут ProvideToolWindow было передано Transient = true. При этом, несмотря на то, что сам плагин проходил инициализацию всегда на старте IDE, само окно оказывалось не до конца инициализированным вплоть до первого к нему обращения, что в частности было видно по неподгруженной иконке на вкладке.
Для окна может быть задана динамическая область видимости с помощью атрибута ProvideToolWindowVisibility, например:
[ProvideToolWindowVisibility(typeof(MyWindowPane),
/*UICONTEXT_SolutionExists*/"f1536ef8-92ec-443c-9ed7-fdadf150da82")]
В данном примере окно должно быть автоматически открыто при вхождении в UI контекст "Solution Exists".Заметьте, что для каждого пользовательского окна необходимо указывать отдельный атрибут, передавая его тип в качестве первого аргумента.
Для создания и отображения инструментального окна из VSPackage можно воспользоваться методом FindToolWindow класса Package. Данный метод возвращает ссылку на указанный toolwindow объект, создавая его в случае необходимости (при первом запуске single-instance окна). Пример вызова и отображения single-instance окна:
private void ShowMyWindow(object sender, EventArgs e)
{
ToolWindowPane MyWindow = this.FindToolWindow(typeof(MyToolWindow),
0, true);
if ((null == MyWindow) || (null == MyWindow.Frame))
{
throw new NotSupportedException(Resources.CanNotCreateWindow);
}
IVsWindowFrame windowFrame = (IVsWindowFrame) MyWindow.Frame;
ErrorHandler.ThrowOnFailure(windowFrame.Show());
}
В приведённом примере окно будет либо создано при первом вызове, либо показано, если оно было ранее скрыто. При этом третий параметр метода FindToolWindow типа bool определяет, будет ли создан новый экземпляр окна в случае, если он не был найден.
Для создания Multi-Instance инструментальных окон можно использовать метод CreateToolWindow, позволяющий создать окно с заданным идентификатором. Тогда метод вызова toolwindow будет выглядеть следующим образом:
private void CreateMyWindow(object sender, EventArgs e)
{
for (int i = 0; ; i++)
{
// Find existing windows.
var currentWindow =
this.FindToolWindow(typeof(MyToolWindow), i, false);
if (currentWindow == null)
{
// Create the window with the first free ID.
var window =
(ToolWindowPane)this.CreateToolWindow(typeof(MyToolWindow), i);
if ((null == window) || (null == window.Frame))
{
throw new
NotSupportedException(Resources.CanNotCreateWindow);
}
IVsWindowFrame windowFrame = (IVsWindowFrame)window.Frame;
ErrorHandler.ThrowOnFailure(windowFrame.Show());
break;
}
}
}
Обратите внимание, что теперь в метод FindToolWindow в качестве 3-его аргумента передаётся false, т.е. мы ищем свободный индекс, не инициализируя новые экземпляры окна.
При работе с single-instance окном, после первого вызова среда будет запоминать его последующие положение и размеры, сохраняя их каждый раз перед закрытием. Однако если по какой-либо причине всё же потребуется вручную задать размер и положение окна, это можно сделать с помощью метода SetFramePos класса IVsWindowFrame:
Guid gd = Guid.Empty;
windowFrame.SetFramePos(VSSETFRAMEPOS.SFP_fDockBottom, ref gd, 20, 20,
200, 200);
Стоит помнить, что вызов метода SetFramePos() должен выполняться после вызова метода Show().
Для модуля типа Add-In инициализировать пользовательское инструментальное окно можно с помощью методов EndDTE интерфейса Window2:
public void OnConnection(object application,
ext_ConnectMode connectMode, object addInInst, ref Array custom)
{
_applicationObject = (DTE2)application;
_addInInstance = (AddIn)addInInst;
EnvDTE80.Windows2 window;
AddIn add_in;
object ctlobj = null;
Window myWindow;
// Get the window object
add_in = _applicationObject.AddIns.Item(1);
window = (Windows2)_applicationObject.Windows;
// This section specifies the path and class name of the windows
// control that you want to host in the new tool window, as well as
// its caption and a unique GUID.
string assemblypath = "C:\\MyToolwindow\\MyToolWindowControl.dll";
string classname = " MyToolWindowControl.MyUserControl";
string guidpos = "{E87F0FC8-5330-442C-AF56-4F42B5F1AD11}";
string caption = "My Window";
// Creates the new tool window and inserts the user control into it.
myWindow = window.CreateToolWindow2(add_in, assemblypath,
classname, caption, guidpos, ref ctlobj);
myWindow.Visible = true;
}
В приведённом примере создаётся пользовательское инструментальное окно, использующее класс MyToolWindowControl.MyUserControl в качестве клиентской области. Класс MyToolWindowControl.MyUserControl может находиться либо в той же assembly, что и инициализирующий его add-in, либо в отдельной библиотеке, имеющей полную COM видимость (например, через опцию Register for COM interop в настройках проекта). В качестве MyUserControl может быть использован стандартный композитный пользовательский подклас UserControl.
Инструментальные окна состоят из каркаса-рамки клиентской области. При этом каркас окна предоставляется средой и отвечает за стыковку с другими компонентами интерфейса (docking), размер и положение окна. Клиентская область (pane) отображает содержимое окна, контролируемое пользователем. Инструментальные окна могут содержать пользовательские WinForms и WPF компоненты и предоставляют возможность обрабатывать такие стандартные события, как например OnShow, OnMove и т.п.
Пользовательское инструментальное окно, а точнее его клиентская область, реализуется путём наследования от класса, реализующего стандартное пустое окно IDE — ToolWindowPane:
[Guid("870ab1d8-b434-4e86-a479-e49b3c6797f0")]
public class MyToolWindow : ToolWindowPane
{
public MyToolWindow():base(null)
{
this.Caption = Resources.ToolWindowTitle;
this.BitmapResourceID = 301;
this.BitmapIndex = 1;
...
}
}
Атрибут Guid уникально идентифицирует каждое пользовательское окно. В случае если модуль создаёт несколько окон разного типа, каждое из них должно иметь свой уникальный Guid. Подкласс ToolWindowPane может в дальнейшем быть модифицирован для отображения в нём пользовательских компонентов и управления его состоянием.
Базовый класс ToolWindowPane реализует пустое инструментальное окно среды. Наследование от данного класса позволяет отобразить в этом окне пользовательские WinForms или WPF компоненты.
До версии Visual Studio 2008 инструментальные окна нативно поддерживали отображение WinForms пользовательских компонентов, а также могли отображать WPF компоненты с помощью WPF Interoperability компонента ElementHost. Начиная с Visual Studio 2010, инструментальные окна стали основываться на технологии WPF, но всё еще поддерживают загрузку и отображение WinForms компонентов в режиме совместимости.
Для отображения в окне пользовательского WinForms компонента можно воспользоваться переопределённым свойством Window у ToolWindowPane:
public MyUserControl control;
public MyToolWindow():base(null)
{
this.Caption = Resources.ToolWindowTitle;
this.BitmapResourceID = 301;
this.BitmapIndex = 1;
this.control = new MyUserControl();
}
public override IWin32Window Window
{
get { return (IWin32Window)control; }
}
При этом MyUserControl представляет собой обычный составной компонент типа System.Windows.Forms.UserControl, позволяющий отобразить любые другие пользовательские компоненты. Для отображения WPF компонентов в нём можно воспользоваться стандартным компонентом WPF ElementHost.
Начиная с версии Visual Studio 2010 появилась возможность нативно отображать WPF компоненты. Для этого нужно передать ссылку на ваш WPF компонент полю Content базового класса:
public MyToolWindow():base(null)
{
this.Caption = Resources.ToolWindowTitle;
this.BitmapResourceID = 301;
this.BitmapIndex = 1;
base.Content = new MyWPFUserControl();
}
Заметьте, что одновременное использование этих двух методов не поддерживается. При назначении WPF компонента в base.Content переопределённое свойство Window будет проигнорировано.
В основном окне Output Window модуля-расширения PVS-Studio расположена виртуальная таблица, основанная на open-source проекте SourceGrid, предназначенная для работы с результатами статического анализа. При этом таблица используется для отображения стандартной таблицы ADO.NET System.Data.Datatable, которая и используется для непосредственного хранения результатов работы анализатора. До версии 4.00 PVS-Studio использовал для вывода результатов анализа стандартное IDE окно Error List, однако, по мере развития возможностей анализатора, его функционала стало недостаточно. Помимо невозможности быть расширенным такими специфичными для статического анализатора элементами управления, как например механизмы для фильтрации и средства подавления ложных срабатываний, Error List, являясь обычным real grid элементом, позволял адекватно оперировать лишь 1-2 тысячами сообщений. Большее количество сообщений начинало приводить уже к заметным лагам всего интерфейса IDE. Практика же использования статического анализа показала, что для крупных проектов, как например Chromium или LLVM, общее количество диагностических сообщений (с учётом уже размеченных ложных срабатываний, пользовательских низкоуровневых и оптимизационных диагностик) вполне может достигать значений в несколько десятков тысяч.
Реализация собственного окна вывода результатов PVS-Studio на основе виртуального грида, связанного с таблицей БД, позволяет отображать и удобно работать с сотнями тысяч сообщений одновременно. Очень важным аспектом при работе с результатами статического анализа является также возможность их гибкой фильтрации, так как ручной просмотр для поиска реальных ошибок даже такого "небольшого" количество диагностических сообщений как 1-2 тысячи, практически невозможен. При хранении же результатов в таблице Datatable подобная фильтрация легко доступна с помощью простых SQL запросов, причём результаты наложения фильтров становятся видны практически моментально в отражении таблицы на виртуальном гриде.
Клиентская область инструментального окна (представленная нашим наследником от класса ToolWindowPane) может обрабатывать события взаимодействия пользователя с окном IDE. Для того чтобы подписаться на обработку этих событий можно воспользоваться интерфейсом IVsWindowFrameNotify3. Приведём пример реализации данного интерфейса:
public sealed class WindowStatus: IVsWindowFrameNotify3
{
// Private fields to keep track of the last known state
private int x = 0;
private int y = 0;
private int width = 0;
private int height = 0;
private bool dockable = false;
#region Public properties
// Return the current horizontal position of the window
public int X
{
get { return x; }
}
// Return the current vertical position of the window
public int Y
{
get { return y; }
}
// Return the current width of the window
public int Width
{
get { return width; }
}
// Return the current height of the window
public int Height
{
get { return height; }
}
// Is the window dockable
public bool IsDockable
{
get { return dockable; }
}
#endregion
public WindowStatus()
{}
#region IVsWindowFrameNotify3 Members
// This is called when the window is being closed
public int OnClose(ref uint pgrfSaveOptions)
{
return Microsoft.VisualStudio.VSConstants.S_OK;
}
// This is called when a window "dock state" changes.
public int OnDockableChange(int fDockable, int x, int y, int w,
int h)
{
this.x = x;
this.y = y;
this.width = w;
this.height = h;
this.dockable = (fDockable != 0);
return Microsoft.VisualStudio.VSConstants.S_OK;
}
// This is called when the window is moved
public int OnMove(int x, int y, int w, int h)
{
this.x = x;
this.y = y;
this.width = w;
this.height = h;
return Microsoft.VisualStudio.VSConstants.S_OK;
}
// This is called when the window is shown or hidden
public int OnShow(int fShow)
{
return Microsoft.VisualStudio.VSConstants.S_OK;
}
/// This is called when the window is resized
public int OnSize(int x, int y, int w, int h)
{
this.x = x;
this.y = y;
this.width = w;
this.height = h;
return Microsoft.VisualStudio.VSConstants.S_OK;
}
#endregion
}
Как видно из приведённого кода, реализующий интерфейс класс WindowStatus может обрабатывать такие изменения в состоянии пользовательского окна, как изменение размера, положения на экране, видимости и т.п. Теперь подпишем наше окно на обработку данных событий. Для этого переопределим метод OnToolWindowCreated у нашего класса-наследника ToolWindowPane:
public class MyToolWindow: ToolWindowPane
{
public override void OnToolWindowCreated()
{
base.OnToolWindowCreated();
// Register to the window events
WindowStatus windowFrameEventsHandler = new WindowStatus();
ErrorHandler.ThrowOnFailure(((IVsWindowFrame)this.Frame).SetProperty(
(int)__VSFPROPID.VSFPROPID_ViewHelper,
(IVsWindowFrameNotify3)windowFrameEventsHandler));
}
...
}
Контроль состояния окна можно осуществлять с помощью обработчиков событий нашей реализации интерфейса IVsWindowFrameNotify3.
Метод OnShow сообщает модулю-расширению об изменении статуса видимости инструментального окна, позволяя определить появление/скрытие окна для пользователя, например, когда пользователь переключает вкладку с одного окна на другое. Текущее состояние видимости можно узнать с помощью параметра fShow, соответствующего списку __FRAMESHOW.
Метод OnClose сообщает о закрытии каркаса окна, позволяя задать необходимое поведение IDE с помощью параметра pgrfSaveOptions, управляющего отображением диалога о сохранении открытого в окне документа (__FRAMECLOSE).
Метод OnDockableChange информирует модуль об изменении docking статуса окна. Параметр fDockable показывает, сцеплено ли окно с каким-либо другим, а остальные параметры указывают новый размер и координаты окна до или после сцепления.
Параметры методов OnMove и OnSize сообщают новые координаты и/или размер окна при его перетаскивании и маштабировании.