Статьи

Интеграция пользовательских инструментов Windows в среду Visual Studio

Около года назад в нашем блоге мы опубликовали серию статей о разработке плагинов для Visual Studio на C #. Мы недавно пересмотрели эти материалы и добавили новые разделы, и теперь приглашаем вас взглянуть на обновленную версию руководства в виде серии статей здесь, на DZone.

Другие статьи в серии можно найти здесь:

В этой статье (четвертой в серии) рассказывается о расширении среды разработки Visual Studio путем интеграции настраиваемого окна пользовательских инструментов в среду. Мы также обсудим вопросы регистрации и инициализации окна в модулях VSPackage и надстройки, а также хостинга пользовательских компонентов.

Вступление

Окна инструментов являются дочерними окнами интерфейса Visual Studio MDI (Multiple Document Interface), и они отвечают за представление различных частей информации пользователю. Обозреватель решений и список ошибок — примеры окон инструментов. Обычно содержимое окон инструментов не связано ни с какими файлами и не содержит редакторов, поскольку для таких задач зарезервированы отдельные окна документов .

Например, пакет расширений PVS-Studio объединяет несколько окон инструментов в IDE, причем основным является окно вывода. Все остальные его окна инструментов могут быть открыты из этого главного окна, например, окно поиска для сетки. Само окно вывода PVS-Studio можно открыть из главного меню Visual Studio (PVS-Studio -> Показать окно вывода PVS-Studio), но оно также будет вызываться автоматически при каждом запуске анализа.

В большинстве случаев среда IDE создает и использует только один экземпляр для каждого из своих окон инструментов, и этот экземпляр будет сохраняться до тех пор, пока сама среда IDE не будет закрыта. Следовательно, нажатие кнопки «Закрыть» в окне инструмента фактически скрывает его, и когда это окно вызывается во второй раз, оно снова становится видимым, сохраняя, таким образом, все содержащиеся в нем данные, прежде чем «закрыться». Но все же возможно ли создать в инструментальной среде несколько окон инструментов, которые могут существовать в нескольких экземплярах одновременно. Окно инструмента также может быть связано с определенным контекстом пользовательского интерфейса (так называемым динамическим окном), и такое окно будет автоматически отображаться, когда пользователь входит в этот контекст.

Интеграция окна инструментов в IDE поддерживается расширениями VSPackage и надстройками, хотя методы для этого различны. Требуется указание начальных настроек окна и его регистрация в системном реестре.

Регистрация и инициализация пользовательского инструмента Windows

Шаблон проекта VSPackage, который устанавливается вместе с Visual Studio SDK, позволяет создать пример окна инструмента в проекте расширения, который генерирует этот шаблон. Такой проект должен уже содержать все основные компоненты, которые будут описаны ниже, поэтому его можно было бы удобно использовать в качестве примера для экспериментов с процессом интеграции окна Visual Studio для плагинов VSPackage.

Регистрация, инициализация и вызов окна инструментов в VSPackage

Регистрация пользовательского пользовательского окна в среде требует записи данных, которые определяют это окно, в специальный раздел куста реестра 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» включает режим нескольких экземпляров для окна, в котором несколько экземпляров окна могут быть открыты одновременно. Параметры ориентации, размера и стиля определяют начальную позицию окна, когда пользователь открывает его в первый раз. Следует отметить, что позиция, заданная этими параметрами, будет использоваться только один раз, когда окно инструмента отображается в первый раз. На всех последующих итерациях открытия этого окна IDE будет восстанавливать свою позицию на экране по сравнению с предыдущей, то есть позицией до закрытия окна. «Переходный»Параметр указывает, будет ли окно автоматически открываться после загрузки среды Visual Studio, если оно уже было открыто во время предыдущего сеанса среды IDE.

Также следует помнить, что инициализация пользовательского окна с помощью VSPackage (сама инициализация будет рассмотрена позже) не обязательно происходит одновременно с инициализацией подкласса Package, для которого мы предоставили этот атрибут регистрации. Например, после реализации окна инструментов для плагина PVS-Studio мы столкнулись с проблемой, при которой наше пользовательское окно автоматически открывалось (но не фокусировалось / отображалось) и помещалось среди других вкладок окна внизу главного окна, и это было сделано сразу после запуска Visual Studio, даже если мы передали параметр «Transient = true» в атрибут ProvideToolWindow. Хотя сам плагин всегда инициализируется при запуске IDE, окно не было полностью инициализировано до первого обращения к нему,что было видно по поврежденному значку на вышеупомянутой вкладке.

Динамический контекст видимости можно указать для окна с помощью атрибута ProvideToolWindowVisibility :

[ProvideToolWindowVisibility(typeof(MyWindowPane), 
/*UICONTEXT_SolutionExists*/"f1536ef8-92ec-443c-9ed7-fdadf150da82")]

В этом примере окно автоматически отображается, когда пользователь входит в контекст пользовательского интерфейса «Решение существует». Обратите внимание, что для каждого из окон инструментов пользователя требуется отдельный атрибут, и тип окна должен быть передан в качестве первого аргумента.

Метод FindToolWindow подкласса Package можно использовать для создания и отображения окна инструментов из расширения VSPackage. Этот метод возвращает ссылку на указанный объект окна инструмента, создавая его при необходимости (например, в случае, если окно одного экземпляра вызывается впервые). Ниже приведен пример вызова окна инструмента с одним экземпляром:

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 указывает, должен ли быть создан новый экземпляр окна, если метод не смог найти уже существующий.

Чтобы создать окно инструмента с несколькими экземплярами, можно использовать метод CreateToolWindow . Это позволяет создать окно с предварительно определенным идентификатором. Вот пример вызова такого окна:

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 в качестве третьего аргумента получает значение «ложь», т.е. мы ищем незанятый индекс перед инициализацией нового экземпляра окна.

Как было упомянуто выше, среда сохранит положение окна после его закрытия. Но если по какой-либо причине необходимо указать размер и положение окна, это можно сделать с помощью метода SetFramePos интерфейса IVsWindowFrame:

Guid gd = Guid.Empty;
windowFrame.SetFramePos(VSSETFRAMEPOS.SFP_fDockBottom, ref gd, 20, 20, 
  200, 200);

Вызов SetFramePos () всегда должен выполняться только после выполнения метода Show ().

Создание и вызов окна из расширения надстройки

Окно пользовательского инструмента может быть инициализировано из расширения надстройки с помощью интерфейса EnvDTE 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 может быть расположен в той же сборке, что и надстройка, которая его инициализирует, или автономная сборка может предоставить ему полную видимость COM (через опцию «Зарегистрироваться для взаимодействия COM» в настройках проекта). , Обычный составной подкласс UserControl может использоваться как MyUserControl.

Реализация окна инструмента пользователя в модуле VSPackage

Окно инструмента состоит из рамки и клиентской области. Фрейм предоставляется средой и отвечает за стыковку с другими объектами интерфейса среды, а также за размер и положение самого окна. Клиентская область — это панель, контролируемая пользователем, в которой находится содержимое окна. Окна инструментов могут содержать созданные пользователем компоненты 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, ToolWindows предоставляла только собственные поддерживаемые пользовательские компоненты WinForm, хотя все еще можно было размещать компоненты WPF через объект ElementHost совместимости WPF. Начиная с 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 и может содержать любой другой пользовательский компонент внутри себя. 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 игнорируется.

Главное окно PVS-Studio «Вывод» нашего модуля расширения содержит виртуальную сетку на основе SourceGrid.проект с открытым исходным кодом. Это окно предоставляет интерфейс для обработки результатов статического анализа. Сама сетка связана с обычной таблицей ADO.NET типа System.Data.Datatable, которая используется для хранения результатов анализа. До версии 4.00 расширения PVS-Studio оно использовало обычное окно IDE «Список ошибок», но по мере развития анализатора возможности этого окна по умолчанию стали недостаточными. Помимо невозможности расширения такими специфическими элементами пользовательского интерфейса статического анализа, как, например, механизмы подавления ложных срабатываний и фильтрации, список ошибок сам по себе является «реальной» сеткой, поскольку он хранит все отображаемые элементы внутри себя. Следовательно, эта сетка позволяет адекватно обрабатывать 1-2 тыс. Сообщений одновременно, с точки зрения производительности,поскольку большее количество сообщений уже может привести к заметному отставанию пользовательского интерфейса среды. С другой стороны, наша собственная практика использования статического анализа в относительно крупных проектах, таких как Chromium или LLVM, продемонстрировала общее количество диагностических сообщений (с учетом всех отмеченных ложных тревог и диагностики низкого уровня пользователя) может легко достичь десятки тысяч или даже больше.

Таким образом, благодаря реализации настраиваемого окна вывода на основе виртуальной сетки, подключенной к таблице БД, PVS-Studio может отображать и обеспечивать удобную обработку сразу для сотен тысяч диагностических сообщений одновременно. Кроме того, возможность удобной и гибкой фильтрации результатов анализа является довольно важным аспектом работы со статическим анализатором, поскольку ручная проверка даже такого «крошечного» количества сообщений, как 1-2 Кб, практически невозможна для одного пользователь. Хранение результатов анализа в объекте Datatable само по себе обеспечивает довольно удобный механизм фильтрации, основанный на простых запросах SQL, тем более, что результаты таких запросов становятся видимыми непосредственно внутри связанной виртуальной сетки.

Инструмент обработки событий Windows

Клиентская область окна инструмента (представленная нашим подклассом ToolWindowPane) может обрабатывать обычные события взаимодействия с пользовательским интерфейсом. Интерфейс 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

}

Как видно из приведенного выше примера кода, класс WindowsStatus, реализующий интерфейс, способен обрабатывать такие изменения состояния окна, как изменения размера окна, положения, свойств видимости и так далее. Теперь давайте подпишемся на наше окно для обработки этих событий. Для этого необходимо переопределить метод 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 информирует пакет об изменениях статуса стыковки окна. Параметр fDockable указывает, пристыковано ли окно к другому; другие параметры управляют размером и положением окна до и после события стыковки.

Параметры методов «OnMove» и «OnSize» предоставляют координаты и размер окна во время его перетаскивания или изменения размера.

Ссылки

  1. MSDN. Виды окон
  2. MSDN. Инструмент Windows
  3. MSDN. Основы окна инструментов.
  4. MSDN. Окно инструментов Пошаговые руководства.
  5. MSDN. Организация и использование Windows в Visual Studio.
  6. MZ-Tools. HOWTO: Понимание состояний ToolWindow в Visual Studio.