Návod: Vytvoření doplňku zobrazení, příkazů a nastavení (vodítka sloupců)
Můžete rozšířit editor textu a kódu sady Visual Studio o příkazy a efekty zobrazení. V tomto článku se dozvíte, jak začít s oblíbenou funkcí rozšíření, vodítky pro sloupce. Vodítka sloupců jsou vizuálně světlé čáry nakreslené v zobrazení textového editoru, které vám pomůžou spravovat kód na konkrétní šířku sloupců. Konkrétně formátovaný kód může být důležitý pro ukázky, které zahrnete do dokumentů, blogových příspěvků nebo zpráv o chybách.
V tomto průvodci uděláte následující:
Vytvoření projektu VSIX
Přidejte ozdobu pro zobrazení editoru
Přidání podpory pro ukládání a získávání nastavení (kde kreslit vodítka sloupců a jejich barvu)
Přidání příkazů (přidání nebo odebrání vodítek sloupců, změna jejich barvy)
Umístěte příkazy do nabídky 'Upravit' a do kontextových nabídek textového dokumentu.
Přidání podpory pro vyvolání příkazů z příkazového okna sady Visual Studio
Verzi funkce vodítek sloupců si můžete vyzkoušet pomocí tohoto rozšíření Galerie Visual Studio .
Poznámka
V tomto názorném postupu vložíte velké množství kódu do několika souborů generovaných šablonami rozšíření sady Visual Studio. Brzy ale tento názorný postup bude odkazovat na dokončené řešení na GitHubu s dalšími příklady rozšíření. Dokončený kód se mírně liší v tom, že má skutečné ikony příkazů místo použití generictemplate ikon.
Nastavení řešení
Nejprve vytvoříte projekt VSIX, přidáte ozdobu zobrazení editoru a poté přidáte příkaz (který přidá balíček VSPackage, aby vlastnil příkaz). Základní architektura je následující:
Máte posluchač pro vytváření textového zobrazení, který vytvoří objekt
ColumnGuideAdornment
pro každé zobrazení. Tento objekt naslouchá událostem o změně zobrazení nebo změně nastavení a podle potřeby aktualizuje nebo překresluje vodítka sloupců.Existuje
GuidesSettingsManager
, která zpracovává čtení a zápis z úložiště nastavení sady Visual Studio. Správce nastavení má také operace pro aktualizaci nastavení, která podporují uživatelské příkazy (přidání sloupce, odebrání sloupce, změna barvy).Existuje balíček VSIP, který je nezbytný, pokud máte uživatelské příkazy, ale je to jen často používaný kód, který inicializuje objekt implementace příkazů.
Existuje objekt
ColumnGuideCommands
, který spouští uživatelské příkazy a připojí obslužné rutiny příkazů pro příkazy deklarované v souboru .vsct.VSIX. Použijte příkaz Soubor | Nový ... k vytvoření projektu. V levém navigačním podokně zvolte uzel rozšiřitelnosti pod C# a v pravém podokně zvolte projekt VSIX. Zadejte název ColumnGuides a výběrem OK vytvořte projekt.
Zobrazit ozdobu. Stiskněte pravé tlačítko ukazatele na uzlu projektu v Průzkumníku řešení. Zvolte příkaz Přidat | Nová položka ... pro přidání nové položky zobrazení ozdob. Vyberte položku Rozšiřitelnost | Editor v levém navigačním podokně a v pravém podokně zvolte Editor Viewport Adornment. Zadejte název ColumnGuideAdornment jako název položky a zvolte Přidat pro přidání.
Tato šablona položky přidala do projektu dva soubory (a také odkazy atd.): ColumnGuideAdornment.cs a ColumnGuideAdornmentTextViewCreationListener.cs. Šablony nakreslely fialový obdélník v zobrazení. V následující části změníte několik řádků v naslouchacím procesu vytváření zobrazení a nahradíte obsah ColumnGuideAdornment.cs.
Příkazy. V Průzkumníka řešenístiskněte tlačítko pravého ukazatele na uzlu projektu. Zvolte příkaz Přidat | Nová položka ... pro přidání nového přídborného prvku zobrazení. Vyberte Rozšiřitelnost | Balíček VSPackage v levém navigačním podokně a vyberte Vlastní příkaz v pravém podokně. Jako název položky zadejte název ColumnGuideCommands a zvolte Přidat. Kromě několika odkazů přidání příkazů a balíčku také vedlo k přidání ColumnGuideCommands.cs, ColumnGuideCommandsPackage.csa ColumnGuideCommandsPackage.vsct. V následující části nahradíte obsah prvního a posledního souboru, který definuje a implementuje příkazy.
Nastavit posluchače pro textové zobrazení
Otevřete ColumnGuideAdornmentTextViewCreationListener.cs v editoru. Tento kód implementuje obslužnou rutinu pro pokaždé, když Visual Studio vytvoří zobrazení textu. Existují atributy, které řídí, kdy obslužná rutina je volána v závislosti na charakteristikách zobrazení.
Kód také musí deklarovat vrstvu ozdoby. Když editor aktualizuje zobrazení, získá vrstvy ozdob pro zobrazení a z těchto vrstev potom získá jednotlivé prvky ozdob. Můžete deklarovat pořadí vrstvy vzhledem k ostatním s atributy. Nahraďte následující řádek:
[Order(After = PredefinedAdornmentLayers.Caret)]
s těmito dvěma řádky:
[Order(Before = PredefinedAdornmentLayers.Text)]
[TextViewRole(PredefinedTextViewRoles.Document)]
Řádek, který jste nahradili, je ve skupině atributů, které deklarují ozdobnou vrstvu. První řádek, který jste změnili, změní pouze to, kde se zobrazují vodicí čáry sloupce. Kreslení čar "před" textu v zobrazení znamená, že se zobrazují za textem nebo pod textem. Druhý řádek deklaruje, že dekorace vodítka sloupců se vztahují na textové entity, které odpovídají vašemu pojmu dokumentu, ale můžete deklarovat dekoraci, například aby fungovala jen pro upravitelný text. Další informace najdete v bodech rozšíření služby language a editoru.
Implementace správce nastavení
Obsah GuidesSettingsManager.cs nahraďte následujícím kódem (vysvětleno níže):
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;
}
}
Většina tohoto kódu vytvoří a parsuje formát nastavení: "RGB(<int>,<int>,<int>) <int>, <int>, ...". Celá čísla na konci označují sloupce, které jsou číslované od jedničky, kde chcete mít vodítka sloupců. Rozšíření vodítka sloupců zachycuje všechna jeho nastavení v jednom řetězci hodnoty nastavení.
Existuje několik částí kódu, které stojí za zvýraznění. Následující řádek kódu získává spravovaný obal Visual Studio pro nastavení úložiště. Ve většině případů poskytuje abstrakci nad registrem Windows, avšak toto rozhraní API je nezávislé na způsobu ukládání dat.
internal static SettingsManager VsManagedSettingsManager =
new ShellSettingsManager(ServiceProvider.GlobalProvider);
Úložiště nastavení sady Visual Studio používá identifikátor kategorie a identifikátor nastavení k jednoznačné identifikaci všech nastavení:
private const string _collectionSettingsName = "Text Editor";
private const string _settingName = "Guides";
Jako název kategorie nemusíte používat "Text Editor"
. Můžete si vybrat cokoliv, co se vám líbí.
Prvních několik funkcí je vstupními body pro změnu nastavení. Kontrolují omezení vysoké úrovně, jako je maximální povolený počet průvodců. Potom zavolají WriteSettings
, který sestaví nastavovací řetězec a nastaví vlastnost GuideLinesConfiguration
. Nastavení této vlastnosti uloží hodnotu nastavení do úložiště nastavení sady Visual Studio a aktivuje událost SettingsChanged
, aby se aktualizovaly všechny ColumnGuideAdornment
objekty, které jsou přidružené k textovému zobrazení.
Existuje několik funkcí vstupních bodů, například CanAddGuideline
, které se používají k implementaci příkazů, které mění nastavení. Když Visual Studio zobrazuje nabídky, dotazuje se implementace příkazů, aby zjistil, jestli je příkaz aktuálně povolený, jaký je jeho název atd. Níže se dozvíte, jak tyto vstupní body připojit k implementaci příkazů. Další informace o příkazech naleznete v tématu Rozšíření nabídek a příkazů.
Implementovat třídu ColumnGuideAdornment
Je instanciovaná třída ColumnGuideAdornment
pro každé textové zobrazení, které může obsahovat ozdoby. Tato třída naslouchá událostem o změně zobrazení nebo změně nastavení a podle potřeby aktualizuje nebo překresluje vodítka sloupců.
Obsah ColumnGuideAdornment.cs nahraďte následujícím kódem (vysvětleno níže):
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);
}
}
}
Instance této třídy uchovávají přidružené IWpfTextView a seznam Line
objektů nakreslených v zobrazení.
Konstruktor (volaný z ColumnGuideAdornmentTextViewCreationListener
, když Visual Studio vytváří nová zobrazení) vytváří objekty vodítka sloupce Line
. Konstruktor také přidává obslužné rutiny pro událost SettingsChanged
(definovanou v GuidesSettingsManager
) a události zobrazení LayoutChanged
a Closed
.
Událost LayoutChanged
se vyvolá kvůli několika druhům změn v zobrazení, včetně toho, kdy Visual Studio vytvoří zobrazení. Obslužná rutina OnViewLayoutChanged
volá AddGuidelinesToAdornmentLayer
ke spuštění. Kód v OnViewLayoutChanged
určuje, zda je potřeba aktualizovat pozice řádků na základě změn, jako jsou změny velikosti písma, žlábky v zobrazení, vodorovné posouvání a podobně. Kód v UpdatePositions
způsobí, že se vodicí čáry vykreslí mezi znaky nebo těsně za textovým sloupcem, který je na specifikovaném posunu znaků v řádku textu.
Při každé změně nastavení funkce SettingsChanged
znovu vytvoří všechny Line
objekty bez ohledu na to, co jsou nová nastavení. Po nastavení umístění čáry kód odebere všechny předchozí Line
objekty z okrasné vrstvy ColumnGuideAdornment
a přidá nové objekty.
Definování příkazů, nabídek a umístění nabídek
K deklarování příkazů a nabídek, umístění skupin příkazů nebo nabídek do různých dalších nabídek a připojení obslužných rutin příkazů může být hodně. Tento návod ukazuje, jak fungují příkazy v tomto rozšíření, ale podrobnější informace najdete v tématu Rozšíření nabídek a příkazů.
Úvod do kódu
Rozšíření Vodítka sloupců zobrazuje deklarování skupiny příkazů, které patří dohromady (přidání sloupce, odebrání sloupce, změna barvy čáry) a následné umístění této skupiny do pod menu místní nabídky editoru. Rozšíření Vodítka sloupců také přidává příkazy do hlavní nabídky Upravit, ale zachovává je neviditelné, jak je uvedeno jako běžný vzor níže.
Implementace příkazů má tři části: ColumnGuideCommandsPackage.cs, ColumnGuideCommandsPackage.vsct a ColumnGuideCommands.cs. Kód generovaný šablonami přidá do nabídky Tools příkaz, který jako výsledek zobrazuje dialogové okno. Můžete se podívat, jak se implementuje v .vsct a ColumnGuideCommands.cs soubory, protože je to jednoduché. Kód v těchto souborech nahradíte níže.
Kód balíčku obsahuje standardní deklarace vyžadované pro Visual Studio ke zjištění, že rozšíření nabízí příkazy a ke zjištění, kam příkazy umístit. Při inicializaci balíčku se vytvoří instance třídy implementace příkazů. Další informace o balíčcích souvisejících s příkazy naleznete v tématu Rozšíření nabídek a příkazů.
Vzor běžných příkazů
Příkazy rozšíření Sloupcové vodítka představují příklad velmi běžného vzoru ve Visual Studio. Související příkazy vložíte do skupiny a tuto skupinu umístíte do hlavní nabídky, často s nastavením "<CommandFlag>CommandWellOnly</CommandFlag>
", aby byl příkaz neviditelný. Když umístíte příkazy do hlavních nabídek (například Upravit), dají jim pěkné názvy (například Edit.AddColumnGuide), které jsou užitečné pro hledání příkazů při opětovném přiřazování klíčových vazeb v Tools Options. Je také užitečné pro automatické doplňování při vyvolání příkazů z příkazového okna.
Potom přidáte skupinu příkazů do kontextových nabídek nebo dílčích nabídek, kde očekáváte, že uživatel bude tyto příkazy používat. Visual Studio považuje CommandWellOnly
za příznak neviditelnosti jen pro hlavní nabídky. Když umístíte stejnou skupinu příkazů do místní nabídky nebo dílčí nabídky, příkazy se zobrazí.
V rámci běžného vzoru vytvoří rozšíření Vodítka sloupců druhou skupinu, která zahrnuje jednu podnabídku. Dílčí nabídka obsahuje první skupinu příkazů průvodce ve čtyřech sloupcích. Druhá skupina, která obsahuje podnabídku, je opakovaně použitelný prostředek, který umístíte do různých kontextových nabídek, což umístí podnabídku do těchto kontextových nabídek.
Soubor .vsct
Soubor .vsct deklaruje příkazy a jejich umístění spolu s ikonami atd. Obsah souboru .vsct nahraďte následujícím kódem (vysvětleno níže):
<?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>
GUIDS. Aby Visual Studio našlo obslužné rutiny příkazů a vyvolalo je, musíte zajistit, aby GUID balíčku deklarovaný v souboru ColumnGuideCommandsPackage.cs (vygenerovaný ze šablony položky projektu) odpovídal GUID balíčku deklarovanému v souboru .vsct (zkopírovaného z výše). Pokud tento vzorový kód znovu použijete, měli byste se ujistit, že máte jiný identifikátor GUID, abyste nebyli v konfliktu s kýmkoli jiným, kdo by mohl tento kód zkopírovat.
Najděte tento řádek v ColumnGuideCommandsPackage.cs a zkopírujte identifikátor GUID z uvozovek:
public const string PackageGuidString = "ef726849-5447-4f73-8de5-01b9e930f7cd";
Potom vložte identifikátor GUID do souboru .vsct tak, abyste měli v deklaracích Symbols
následující řádek:
<GuidSymbol name="guidColumnGuideCommandsPkg"
value="{ef726849-5447-4f73-8de5-01b9e930f7cd}" />
Identifikátory GUID sady příkazů a bitmapového souboru by měly být jedinečné i pro vaše rozšíření.
<GuidSymbol name="guidColumnGuidesCommandSet"
value="{c2bc0047-8bfa-4e5a-b5dc-45af8c274d8e}">
<GuidSymbol name="guidImages" value="{2C99F852-587C-43AF-AA2D-F605DE2E46EF}">
Nemusíte ale měnit identifikátory GUID pro sadu příkazů a rastrové obrázky v tomto návodu, aby kód fungoval. Identifikátor GUID sady příkazů musí odpovídat deklaraci v souboru ColumnGuideCommands.cs, ale obsah tohoto souboru také nahradíte; identifikátory GUID se proto budou shodovat.
Ostatní identifikátory GUID v souboru .vsct identifikují existující nabídky, do kterých se přidávají příkazy průvodce sloupcem, takže se nikdy nemění.
Soubor oddíly. .vsct má tři vnější části: příkazy, umístění a symboly. Oddíl příkazů definuje skupiny příkazů, nabídky, tlačítka nebo položky nabídky a rastrové obrázky pro ikony. Oddíl umístění deklaruje, kde skupiny přecházejí do nabídek nebo dalších umístění do již existujících nabídek. Oddíl symbolů deklaruje identifikátory používané jinde v souboru .vsct, což činí .vsct kód čitelnějším než mít všude GUIDy a šestnáctková čísla.
sekce příkazů , a definice skupin. Oddíl příkazů nejprve definuje skupiny příkazů. Skupiny příkazů jsou ty, které v nabídce vidíte oddělené jemnými šedými liniemi. Skupina může také vyplnit celou dílčí nabídku, jako v tomto příkladu, a v tomto případě nevidíte šedé oddělovací čáry. Soubory .vsct deklarují dvě skupiny, GuidesMenuItemsGroup
, která má za rodiče IDM_VS_MENU_EDIT
(hlavní nabídka Upravit) a GuidesContextMenuGroup
, která má za rodiče IDM_VS_CTXT_CODEWIN
(kontekstová nabídka editoru kódu).
Druhá deklarace skupiny má prioritu 0x0600
:
<Group guid="guidColumnGuidesCommandSet" id="GuidesContextMenuGroup"
priority="0x0600">
Cílem je umístit podnabídku vodítek sloupců na konec jakékoli místní nabídky, do které přidáte skupinu podnabídek. Neměli byste však předpokládat, že víte nejlépe, a snažit se vynutit, aby dílčí nabídka byla vždy poslední pomocí priority 0xFFFF
. Musíte experimentovat s číslem, abyste zjistili, kde se nachází vaše podnabídka v kontextových nabídkách, kam ji umístíte. V tomto případě je 0x0600
natolik vysoko, že ho lze umístit na konec menu, dokud to je viditelné, ale zároveň ponechává prostor pro někoho jiného navrhnout své rozšíření tak, aby bylo nižší než rozšíření pro vodítka sloupců, pokud je to žádoucí.
Oddíl Příkazy, definice nabídky. Dále oddíl příkazu definuje podnabídku GuidesSubMenu
, která je podřazená GuidesContextMenuGroup
.
GuidesContextMenuGroup
je skupina, kterou přidáte do všech relevantních kontextových nabídek. V části Umístění kód umístí skupinu pomocí čtyřsloupců vodicích příkazů v této dílčí nabídce.
Oddíl Příkazy, definice tlačítek. Oddíl příkazů pak definuje položky nabídky nebo tlačítka, které jsou příkazy pro čtyřsloupcové vodítka.
CommandWellOnly
, popsáno výše, znamená, že příkazy jsou při umístění v hlavní nabídce neviditelné. Dvě deklarace tlačítek položky nabídky (přidat průvodce a odebrat průvodce) mají také příznak AllowParams
:
<CommandFlag>AllowParams</CommandFlag>
Tento příznak umožňuje příkazu přijímat argumenty, a to spolu s umístěním v hlavní nabídce, když Visual Studio vyvolá obslužnou rutinu příkazu. Když uživatel spustí příkaz z okna příkazového řádku, argument se předá obslužné rutince příkazu v rámci argumentů události.
oddíly příkazů, definice rastrových obrázků. Oddíl příkazů nakonec deklaruje rastrové obrázky nebo ikony použité pro příkazy. Tato část je jednoduchá deklarace, která identifikuje zdroj projektu a obsahuje jeden index použitých ikon. Část symbolů souboru .vsct deklaruje hodnoty identifikátorů použitých jako indexy. Tento názorný postup používá rastrový pruh, který je součástí vlastní šablony položky příkazu přidané do projektu.
Sekce umístění . Za částí příkazů následuje část umístění. Prvním místem je, kde kód přidá první výše probíranou skupinu, která obsahuje příkazy čtyřsloupcového vodítka do podnabídky, kde se příkazy objeví:
<CommandPlacement guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup"
priority="0x0100">
<Parent guid="guidColumnGuidesCommandSet" id="GuidesSubMenu" />
</CommandPlacement>
Všechna ostatní umístění přidávají GuidesContextMenuGroup
(která obsahuje GuidesSubMenu
) do jiných kontextových nabídek editoru. Když kód deklaroval GuidesContextMenuGroup
, byl nadřazený místní nabídce editoru kódu. Proto nevidíte umístění místní nabídky editoru kódu.
Sekce Symboly. Jak je uvedeno výše, oddíl symbolů deklaruje identifikátory používané jinde v souboru .vsct, díky čemuž je .vsct kód čitelnější než mít identifikátory GUID a šestnáctkové čísla všude. Významné body v této části jsou, že identifikátor GUID balíčku musí souhlasit s deklarací v třídě balíčku. A identifikátor GUID sady příkazů musí být v souladu s deklarací ve třídě implementující příkaz.
Implementace příkazů
Soubor ColumnGuideCommands.cs implementuje příkazy a připojí obslužné rutiny. Když Visual Studio načte balíček a inicializuje ho, balíček pak zavolá Initialize
na třídu implementace příkazů. Inicializace příkazů jednoduše vytvoří instanci třídy a konstruktor připojí všechny obslužné rutiny příkazů.
Obsah souboru ColumnGuideCommands.cs nahraďte následujícím kódem (vysvětleno níže):
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);
}
}
}
}
}
Oprava odkazů. V tuto chvíli chybí odkaz. Stiskněte tlačítko pravého ukazatele na uzlu Reference v Průzkumníku řešení. Zvolte příkaz Přidat .... Dialogové okno Přidat odkaz obsahuje vyhledávací pole v pravém horním rohu. Zadejte "editor" (bez dvojitých uvozovek). Zvolte položku Microsoft.VisualStudio.Editor (musíte zaškrtnout políčko vlevo od položky, ne jen vybrat položku) a zvolit OK přidat odkaz.
inicializace. Když se třída balíčku inicializuje, volá Initialize
u třídy implementace příkazů. Inicializace ColumnGuideCommands
vytvoří instanci třídy a uloží instanci třídy a odkaz na balíček do členů třídy.
Pojďme se podívat na jedno z připojení příkazové obslužné rutiny v konstruktoru třídy:
_addGuidelineCommand =
new OleMenuCommand(AddColumnGuideExecuted, null,
AddColumnGuideBeforeQueryStatus,
new CommandID(ColumnGuideCommands.CommandSet,
cmdidAddColumnGuide));
Vytvoříte OleMenuCommand
. Visual Studio používá příkazový systém Microsoft Office. Klíčové argumenty při vytváření instance OleMenuCommand
je funkce, která implementuje příkaz (AddColumnGuideExecuted
), funkce, která se má volat, když Visual Studio zobrazí nabídku s příkazem (AddColumnGuideBeforeQueryStatus
) a ID příkazu. Visual Studio volá funkci stavu dotazu před zobrazením příkazu v nabídce, aby příkaz mohl být pro konkrétní zobrazení nabídky neviditelný nebo šedý (například zakázání Kopírovat, pokud není nic vybráno), změnil svou ikonu nebo dokonce změnil svůj název (například z možnosti Přidat něco k Odebrat něco), a tak dále. ID příkazu musí odpovídat ID příkazu deklarovanému v souboru .vsct. Řetězce pro sadu příkazů a příkaz přidání vodítek sloupců musí odpovídat mezi souborem .vsct a ColumnGuideCommands.cs.
Následující řádek poskytuje pomoc při vyvolání příkazu pomocí příkazového okna (vysvětleno níže):
_addGuidelineCommand.ParametersDescription = "<column>";
stav dotazu. Funkce stavu dotazu AddColumnGuideBeforeQueryStatus
a RemoveColumnGuideBeforeQueryStatus
zkontrolují některá nastavení (například maximální počet vodítek nebo maximální počet sloupců) nebo pokud existuje vodítko sloupce, které chcete odebrat. Pokud jsou podmínky správné, povolí příkazy. Funkce dotazu na stav musí být efektivní, protože se spustí pokaždé, když Visual Studio zobrazí nabídku a pro každý příkaz v nabídce.
funkce AddColumnGuideExecuted. Zajímavou částí přidání průvodce je zjištění aktuálního zobrazení editoru a umístění kurzoru. Nejprve tato funkce volá GetApplicableColumn
, která kontroluje, zda existuje argument zadaný uživatelem v argumentech události obslužné rutiny příkazů, a pokud neexistuje, funkce zkontroluje zobrazení editoru:
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
musí trochu kopat, aby získal IWpfTextView zobrazení kódu. Pokud trasujete GetActiveTextView
, GetActiveView
a GetTextViewFromVsTextView
, uvidíte, jak to udělat. Následující kód je relevantní kód abstrahovaný, počínaje aktuálním výběrem, získáním rámce výběru a následným získáním rámeček DocView jako IVsTextView, získáním IVsUserData z IVsTextView, získáním hostitele zobrazení a nakonec 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;
Jakmile máte IWpfTextView, můžete zjistit sloupec, ve kterém se nachází kurzor.
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));
}
S aktuálním sloupcem, na který uživatel klikl, stačí, když kód zavolá správci nastavení, aby sloupec přidal nebo odebral. Správce nastavení aktivuje událost, na kterou naslouchají všechny ColumnGuideAdornment
objekty. Když se událost aktivuje, tyto objekty aktualizují přidružená textová zobrazení pomocí nového nastavení vodící čáry sloupců.
Vyvolání příkazu z příkazového okna
Ukázka příruček sloupců umožňuje uživatelům vyvolat dva příkazy z příkazového okna jako způsob rozšiřitelnosti. Pokud použijete příkaz Zobrazení | Ostatní okna | Příkazové okno, zobrazí se Příkazové okno. S příkazovým oknem můžete pracovat tak, že zadáte "edit" a s dokončováním názvu příkazu a zadáním argumentu 120 získáte následující výsledek:
> Edit.AddColumnGuide 120
>
Části ukázky, které umožňují toto chování, jsou v deklaracích souboru .vsct, konstruktoru třídy ColumnGuideCommands
, když připojuje obslužné rutiny příkazů, a v implementacích obslužných rutin příkazů, které kontrolují argumenty událostí.
<CommandFlag>CommandWellOnly</CommandFlag>
V souboru .vsct jste viděli i umístění v hlavní nabídce Upravit, přestože se příkazy v uživatelském rozhraní nabídky Upravit nezobrazují. Pokud je budete mít v hlavní nabídce Upravit, budou mít názvy jako Edit.AddColumnGuide. Deklarace skupiny příkazů, která obsahuje čtyři příkazy, umístila skupinu přímo do nabídky Upravit.
<Group guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup"
priority="0xB801">
<Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_EDIT" />
</Group>
Sekce tlačítek později deklarovala příkazy CommandWellOnly
, aby byly neviditelné v hlavní nabídce, a označila je jako 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>
Viděli jste kód napojení obslužné rutiny příkazu v konstruktoru třídy ColumnGuideCommands
, který poskytl popis povoleného parametru:
_addGuidelineCommand.ParametersDescription = "<column>";
Viděli jste, že funkce GetApplicableColumn
kontroluje hodnotu OleMenuCmdEventArgs
před kontrolou zobrazení editoru pro aktuální sloupec:
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;
}
Vyzkoušejte si rozšíření.
Teď můžete stisknutím klávesy F5 spustit rozšíření Vodítka sloupců. Otevřete textový soubor a pomocí místní nabídky editoru přidejte vodicí čáry, odeberte je a změňte jejich barvu. Kliknutím do textu (ne za koncem řádku do prázdného místa) přidáte vodítko sloupce, jinak ho editor přidá do posledního sloupce na řádku. Pokud použijete příkazové okno a vyvoláte příkazy s argumentem, můžete vodítka sloupců přidat kamkoli.
Pokud chcete vyzkoušet různá umístění příkazů, změnit názvy, ikony a podobně a máte problémy, že Visual Studio nezobrazuje nejnovější kód v nabídkách, můžete resetovat experimentální úložiště, ve kterém právě ladíte. Vyvolejte nabídky Start systému Windows a zadejte "resetovat". Vyhledejte a spusťte příkaz, Resetovat následující experimentální instanci sady Visual Studio. Tento příkaz vyčistí experimentální podregistr registru od všech komponent rozšíření. Nastavení ze součástí nevyčistí, takže jakékoli příručky, které jste měli při vypnutí experimentálního podregistru sady Visual Studio, zůstávají a při dalším spuštění, kdy váš kód čte úložiště nastavení, jsou stále k dispozici.
Dokončený projekt kódu
Brzy bude k dispozici projekt GitHubu s ukázkami rozšiřitelnosti sady Visual Studio a dokončený projekt tam bude. Tento článek bude aktualizován tak, aby odkazoval tam, když k tomu dojde. Dokončený vzorový projekt může mít různé identifikátory GUID a pro ikony příkazů bude mít odlišné bitmapové proužky.
Verzi funkce vodítek sloupců si můžete vyzkoušet pomocí tohoto rozšíření galerie Visual Studio .
Související obsah
- Uvnitř editoru
- rozšíření editoru a jazykových služeb
- bodů rozšíření služby jazyka a editoru
- rozšíření nabídek a příkazů
- Přidání podnabídky do nabídky
- Vytvoření rozšíření pomocí šablony položek editoru