可以使用命令和视图效果扩展 Visual Studio 文本/代码编辑器。 本文介绍如何开始使用常用扩展功能:列参考线。 列参考线是在文本编辑器视图中绘制的视觉浅线,可帮助你将代码管理到特定列宽。 具体而言,格式化代码对于文档、博客文章或 bug 报告中包含的示例非常重要。
在此演练中,你将:
创建 VSIX 项目
添加编辑器视图修饰
添加对保存和获取设置的支持(在何处绘制列参考线及其颜色)
添加命令(添加/删除列参考线,更改其颜色)
在“编辑”菜单和文本文档上下文菜单上放置命令
添加对从 Visual Studio 命令窗口调用命令的支持
可以通过此 Visual Studio 库扩展来试用列参考线功能的一个版本。
注意
在本演练中,你将大量代码粘贴到 Visual Studio 扩展模板生成的几个文件中。 但是,本演练很快将参考 GitHub 上已完成的解决方案,其中包含其他扩展示例。 完成的代码略有不同,因为它具有真正的命令图标,而不是使用泛型模板图标。
设置解决方案
首先,创建 VSIX 项目,添加编辑器视图装饰,然后添加命令(这将添加 VSPackage 以拥有该命令)。 基本体系结构如下所示:
你有一个文本视图创建侦听器,可为每个视图创建一个
ColumnGuideAdornment
对象。 该对象侦听有关视图更改或设置更改的事件,并在需要时更新或重绘列参考线。有一个
GuidesSettingsManager
专门负责从 Visual Studio 的设置存储中进行读取和写入。 设置管理器还具有用于更新支持用户命令的设置的操作(添加列、删除列、更改颜色)。如果你有用户命令,则需要 VSIP 包,但它只是用于初始化命令实现对象的样板代码。
有一个
ColumnGuideCommands
对象,它负责运行用户命令,并为 .vsct 文件中声明的命令连接命令处理程序。VSIX。 使用 文件 |新建 ... 命令以创建项目。 在左侧导航窗格中选择 C# 下的 扩展性 节点,然后在右侧窗格中选择 VSIX 项目。 输入名称“ColumnGuides”,然后选择“确定”以创建项目。
视图修饰。 按解决方案资源管理器中项目节点上的右指针按钮。 选择“添加 | 新建项...”命令以添加新的视图修饰项。 选择左侧导航窗格中的 扩展性 | 编辑器,然后选择右窗格中的 编辑器视区装饰。 输入名称“ColumnGuideAdornment”作为项名称,然后选择“添加”进行添加。
可以看到此项目模板向项目添加了两个文件(以及引用等):ColumnGuideAdornment.cs 和 ColumnGuideAdornmentTextViewCreationListener.cs。 模板在视图上绘制紫色矩形。 在以下部分中,您将在视图创建监听器中更改几行代码,并替换 ColumnGuideAdornment.cs的内容。
命令。 在 解决方案资源管理器中,按项目节点上的右指针按钮。 选择“添加 | 新建项...”命令以添加新的视图修饰项。 选择 扩展性 |左侧导航窗格中的 VSPackage,并在右侧窗格中选择 自定义命令。 输入 ColumnGuideCommands 作为项名称,然后选择 添加。 除了几个引用,添加命令和包还添加了“ColumnGuideCommands.cs”、“ColumnGuideCommandsPackage.cs”和“ColumnGuideCommandsPackage.vsct”。 在下一部分中,将替换第一个和最后一个文件的内容以定义和实现命令。
设置文本视图创建侦听器
在编辑器中打开 ColumnGuideAdornmentTextViewCreationListener.cs。 每当 Visual Studio 创建文本视图对象时,此代码都实现一个处理程序。 有一些属性控制何时调用处理程序,具体取决于视图的特征。
代码还必须声明装饰层。 当编辑器更新视图时,它将获取视图的装饰层,并从中获取装饰元素。 可以使用属性声明图层相对于其他图层的顺序。 替换以下行:
[Order(After = PredefinedAdornmentLayers.Caret)]
使用以下两行:
[Order(Before = PredefinedAdornmentLayers.Text)]
[TextViewRole(PredefinedTextViewRoles.Document)]
所替换的行位于一组声明装饰层的属性中。 您更改的第一行只会更改列参考线的显示位置。 在视图中绘制文本“前面”的线条意味着它们显示在文本后面或下方。 第二行声明,列引导装饰适用于符合您对文档概念的文本实体。您也可以声明,例如,这些装饰仅用于可编辑文本。 更多信息可在 语言服务和编辑器扩展点 中找到。
实现设置管理器
将 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 thred, 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>,...”。 末尾的整数是需要列参考线的从一开始计数的列。 列参考线扩展将其所有设置捕获到一个设置值字符串中。
代码的某些部分值得突出显示。 以下代码行获取设置存储的 Visual Studio 托管包装器。 在大多数情况下,此 API 会通过 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
。
视图中的几种变化(包括 Visual Studio 创建视图时)都会触发 LayoutChanged
事件。 OnViewLayoutChanged
处理程序调用 AddGuidelinesToAdornmentLayer
来执行。 OnViewLayoutChanged
中的代码会根据字号变化、视图边距、水平滚动等变化来确定是否需要更新行位置。 UpdatePositions
中的代码会导致在字符之间或文本行中指定字符偏移处的文本列之后绘制参考线。
每当设置更改时,SettingsChanged
函数会根据新的设置重新创建所有 Line
对象。 设置行位置后,代码将从 ColumnGuideAdornment
装饰层中删除所有以前的 Line
对象,并添加新对象。
定义命令、菜单和菜单位置
声明命令和菜单、将命令或菜单组放在各种其他菜单上,以及挂接命令处理程序可能涉及很多内容。 本演练重点介绍此扩展中的命令的工作原理,但有关更深入的信息,请参阅 扩展菜单和命令。
代码简介
列向导扩展功能展示了一组相关命令的声明(如添加列、删除列、更改行颜色),并将该组置于编辑器上下文菜单的一个子菜单中。 列参考线扩展还会将命令添加到主“编辑”菜单中,但会保持它们不可见,这一常见模式将在后面讨论。
命令实现有三个部分:ColumnGuideCommandsPackage.cs、ColumnGuideCommandsPackage.vsct 和 ColumnGuideCommands.cs。 模板生成的代码将命令放在 工具 菜单上,弹出对话框作为实现。 可以查看在 .vsct 和 ColumnGuideCommands.cs 文件中实现的方式,因为它非常简单。 请替换以下文件中的代码。
包代码包含了所需的模板声明,使得 Visual Studio 能够发现扩展提供的命令并找到命令的放置位置。 包初始化时,它会实例化命令实现类。 有关与命令相关的包的详细信息,请参阅 扩展菜单和命令。
常见命令模式
列参考线扩展中的命令是 Visual Studio 中非常常见的模式的示例。 将相关命令放在组中,并将该组放在主菜单上,通常设置为“<CommandFlag>CommandWellOnly</CommandFlag>
”以使命令不可见。 在主菜单上(如 编辑)上放置命令会给他们很好的名称(例如 Edit.AddColumnGuide),这对于在 工具选项中重新分配键绑定时查找命令非常有用。 当从“命令窗口”调用命令时,它还能帮助你完成命令。
然后,将命令组添加到上下文菜单或子菜单,以便用户使用命令。 Visual Studio 仅将 CommandWellOnly
视为主菜单的不可见标志。 在上下文菜单或子菜单上放置同一组命令时,命令可见。
按照常见模式,列参考线扩展创建包含单个子菜单的第二个组。 子菜单又包含具有四列指南命令的第一个组。 包含子菜单的第二个组是放在各种上下文菜单上的可重用资产,它将子菜单放在这些上下文菜单上。
.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 有三个外部部分:命令、位置和符号。 命令部分定义了命令组、菜单、按钮或菜单项,以及用于图标的位图。 放置部分声明组在菜单上的位置,或在已有菜单中的额外放置。 symbols 节声明在 .vsct 文件中其他位置使用的标识符,这使得 .vsct 代码比在任何地方具有 GUID 和十六进制数字更具可读性。
命令部分,组定义。 命令部分首先定义命令组。 在菜单中看到的命令组是指由略显灰色的线条分隔开的各个命令组。 组还可以填充整个子菜单,在这种情况下你不会看到灰色分隔线,如本示例中所示。 .vsct 文件声明两个组,GuidesMenuItemsGroup
是IDM_VS_MENU_EDIT
的父组(主“编辑”菜单),GuidesContextMenuGroup
是 IDM_VS_CTXT_CODEWIN
的父组(代码编辑器的上下文菜单)。
第二个组声明具有 0x0600
优先级:
<Group guid="guidColumnGuidesCommandSet" id="GuidesContextMenuGroup"
priority="0x0600">
其思路是将列参考线子菜单放在添加子菜单组的任何上下文菜单的末尾。 但是,请不要自以为是地使用 0xFFFF
优先级来强制子菜单总是排在最后。 必须试验数字,以确定子菜单在所放上下文菜单中的位置。 在这种情况下,0x0600
已足以将其放在菜单中尽量让你看得到的末尾,同时留出空间使其他人能够将其扩展设计为低于该列参考线扩展(如果可取)。
命令部分,菜单定义。 接下来,命令部分定义了子菜单 GuidesSubMenu
,并把它归属于 GuidesContextMenuGroup
。 GuidesContextMenuGroup
是添加到所有相关上下文菜单的组。 在放置部分,代码将包含四列参考线命令的组放在此子菜单上。
命令部分,按钮的定义。 然后,命令部分定义属于四列参考线命令的菜单项或按钮。 上面讨论的 CommandWellOnly
意味着命令放在主菜单上时不可见。 其中两个菜单项按钮声明(添加参考线和删除参考线)也有 AllowParams
标志:
<CommandFlag>AllowParams</CommandFlag>
此标志不仅允许在主菜单中进行定位,还允许命令在 Visual Studio 调用命令处理程序时接收参数。 如果用户从命令窗口运行命令,则参数将传递到事件参数中的命令处理程序。
命令部分,位图定义。 最后,命令部分声明用于命令的位图或图标。 此部分是一个简单的声明,用于标识项目资源,并列出已用图标的从一开始计数的索引。 .vsct 文件的符号部分声明用作索引的标识符的值。 本演练使用随添加到项目的自定义命令项模板一起提供的位图条。
放置部分。 命令部分之后是放置部分。 第一个是代码将上面讨论的第一个组添加到显示命令的子菜单中,该组包含四列指南命令:
<CommandPlacement guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup"
priority="0x0100">
<Parent guid="guidColumnGuidesCommandSet" id="GuidesSubMenu" />
</CommandPlacement>
所有其他放置都会将 GuidesContextMenuGroup
(包含 GuidesSubMenu
)添加到其他编辑器上下文菜单中。 当代码声明 GuidesContextMenuGroup
时,它被设置为代码编辑器上下文菜单的父级。 这就是你看不到代码编辑器上下文菜单的位置的原因。
符号部分。 如上所述,符号节声明在 .vsct 文件中其他地方使用的标识符,这使得 .vsct 代码比在任何地方具有 GUID 和十六进制数字更具可读性。 本部分中的要点是,包 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 项(必须在项左侧的框中打勾,而不仅仅是选中该项),然后选择 确定 以添加引用。
初始化。 当包类初始化时,它会在命令实现类上调用 Initialize
。 ColumnGuideCommands
初始化类,并将类的实例和包的引用保存在类成员中。
让我们来看看来自类构造函数的其中一个命令处理程序挂钩:
_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,之后获取视图 host,最后获取 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
对象都会侦听的事件。 事件触发后,这些对象会使用新的列参考线设置更新其关联的文本视图。
从命令窗口调用命令
列指南示例允许用户通过命令窗口调用两个命令,以实现扩展性。 如果使用 视图 | 其他窗口 | 命令窗口 命令,您可以看到命令窗口。 可以输入“edit.”与命令窗口交互,在命令名称补全并提供参数 120 后,获得以下结果:
> Edit.AddColumnGuide 120
>
启用此行为的示例片段位于 .vsct 文件声明、ColumnGuideCommands
类构造函数(挂接命令处理程序时)以及检查事件参数的命令处理程序实现中。
你在 .vsct 文件中看到了“<CommandFlag>CommandWellOnly</CommandFlag>
”,在主“编辑”菜单中也看到了放置,尽管这些命令并未显示在“编辑”菜单 UI 中。 将它们放在主“编辑”菜单上,可向他们提供名称,例如“Edit.AddColumnGuide”。 包含以下四个命令的命令组声明将该组直接放置在编辑菜单上:
<Group guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup"
priority="0xB801">
<Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_EDIT" />
</Group>
按钮部分稍后声明命令 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 实验实例。 此命令清理实验性注册表分区中的所有扩展组件。 当你的代码在下次启动时读取设置存储时,它不会清除组件中的设置,因此当你关闭 Visual Studio 的实验配置单元时,你所拥有的任何参考线仍然存在。
已完成的代码项目
不久将有一个 Visual Studio 扩展性示例的 GitHub 项目,并且已完成的项目将在那里。 届时,本文将进行相应的更新。 完成的示例项目可能具有不同的 GUID,并且命令图标具有不同的位图条。
可以通过此 Visual Studio 库扩展来试用列参考线功能的一个版本。