Procédure pas à pas : Créer un ornement d’affichage, des commandes et des paramètres (repères de colonnes)
Vous pouvez étendre l’éditeur de texte/code Visual Studio avec des commandes et des effets d’affichage. Cet article vous montre comment prendre en main une fonctionnalité d’extension populaire, des guides de colonnes. Les repères de colonnes sont des lignes visuellement claires dessinées sur la vue de l’éditeur de texte pour vous aider à gérer votre code sur des largeurs de colonne spécifiques. Plus précisément, le code mis en forme peut être important pour les exemples que vous incluez dans des documents, des billets de blog ou des rapports de bogues.
Dans cette procédure pas à pas, vous procédez comme suit :
Créer un projet VSIX
Ajouter un ornement d’affichage de l’éditeur
Ajouter la prise en charge de la sauvegarde et de la consultation des paramètres (où dessiner les repères de colonne et leur couleur)
Ajouter des commandes (ajouter/supprimer des repères de colonne, modifier leur couleur)
Placer les commandes dans le menu Modifier et les menus contextuels du document texte
Ajouter la prise en charge de l’appel des commandes à partir de la fenêtre commande Visual Studio
Vous pouvez essayer une version de la fonctionnalité de guides de colonnes avec cette extension Visual Studio Gallery .
Remarque
Dans cette procédure pas à pas, vous collez une grande quantité de code dans quelques fichiers générés par les modèles d’extension Visual Studio. Mais bientôt, cette procédure pas à pas fait référence à une solution terminée sur GitHub avec d’autres exemples d’extension. Le code terminé est légèrement différent car il a des icônes de commande réelles au lieu d’utiliser des icônes génériques de modèle.
Configurer la solution
Tout d’abord, vous créez un projet VSIX, ajoutez un ornement d’affichage d’éditeur, puis ajoutez une commande (qui ajoute un VSPackage pour posséder la commande). L’architecture de base est la suivante :
Vous disposez d’un gestionnaire pour la création d'affichages textuels qui crée un objet
ColumnGuideAdornment
par instance. Cet objet écoute les événements relatifs à la modification de l’affichage ou des paramètres, et met à jour ou redessine les repères de colonnes si nécessaire.Il existe un
GuidesSettingsManager
qui gère la lecture et l’écriture à partir du stockage des paramètres Visual Studio. Le gestionnaire de paramètres dispose également d’opérations pour mettre à jour les paramètres qui prennent en charge les commandes utilisateur (ajouter une colonne, supprimer une colonne, modifier la couleur).Il existe un package VSIP nécessaire si vous avez des commandes utilisateur, mais il s’agit simplement d’un code réutilisable qui initialise l’objet d’implémentation des commandes.
Il existe un objet
ColumnGuideCommands
qui exécute les commandes utilisateur et connecte les gestionnaires de commandes pour les commandes déclarées dans le fichier .vsct.VSIX. Utiliser le fichier | Nouvelle commande ... pour créer un projet. Choisissez le nœud Extensibilité sous C# dans le volet de navigation gauche et choisissez Projet VSIX dans le volet droit. Entrez le nom ColumnGuides, puis choisissez OK pour créer le projet.
Ornementation de la vue. Appuyez sur le bouton pointeur droit sur le nœud du projet dans l’Explorateur de solutions. Choisir l'Ajouter | Nouvel élément ... commande pour ajouter un nouvel élément d’ornement d’affichage. Choisissez Extensibilité | Éditeur dans le volet de navigation gauche et choisissez Habillage de la fenêtre de visualisation de l'éditeur dans le volet droit. Entrez le nom ColumnGuideAdornment comme nom de l’élément et choisissez Ajouter pour l’ajouter.
Vous pouvez voir que ce modèle d’élément a ajouté deux fichiers au projet (ainsi que des références, etc.) : ColumnGuideAdornment.cs et ColumnGuideAdornmentTextViewCreationListener.cs. Les modèles affichent un rectangle violet sur la vue. Dans la section suivante, vous modifiez quelques lignes dans l’écouteur de création de vue et remplacez le contenu de ColumnGuideAdornment.cs.
Commandes. Dans Explorateur de solutions, appuyez sur le bouton pointeur droit sur le nœud du projet. Choisir l'Ajouter | Nouvel élément ... commande pour ajouter un nouvel élément d’ornement d’affichage. Choisissez Extensibilité | VSPackage dans le volet de navigation gauche et choisissez Commande personnalisée dans le volet droit. Entrez le nom ColumnGuideCommands comme nom de l’élément et choisissez Ajouter. En plus de plusieurs références, l’intégration des commandes et du package a également inclus ColumnGuideCommands.cs, ColumnGuideCommandsPackage.cset ColumnGuideCommandsPackage.vsct. Dans la section suivante, vous remplacez le contenu des premiers et derniers fichiers pour définir et implémenter les commandes.
Configurer l'écouteur pour la création d'une vue de texte
Ouvrez ColumnGuideAdornmentTextViewCreationListener.cs dans l’éditeur. Ce code implémente un gestionnaire pour chaque fois que Visual Studio crée des vues de texte. Il existe des attributs qui contrôlent le moment où le gestionnaire est appelé en fonction des caractéristiques de la vue.
Le code doit également déclarer une couche d’ornement. Lorsque l'éditeur met à jour les vues, il obtient les couches d'ornement de la vue et de là obtient les éléments d'ornement. Vous pouvez déclarer l’ordre de votre couche par rapport à d’autres avec des attributs. Remplacez la ligne suivante :
[Order(After = PredefinedAdornmentLayers.Caret)]
avec ces deux lignes :
[Order(Before = PredefinedAdornmentLayers.Text)]
[TextViewRole(PredefinedTextViewRoles.Document)]
La ligne que vous avez remplacée se trouve dans un groupe d’attributs qui déclarent une couche d’ornement. La première ligne que vous avez modifiée change uniquement lorsque les lignes de repère de colonne s’affichent. Dessiner les lignes « avant » le texte dans la vue signifie qu’elles apparaissent derrière ou sous le texte. La deuxième ligne déclare que les ornements du repère de colonne s’appliquent aux entités de texte qui correspondent à votre notion de document, mais vous pouvez déclarer l’ornement, par exemple, pour fonctionner uniquement pour du texte modifiable. Vous trouverez plus d'informations dans Points d'extension du service de langage et de l'éditeur
Implémenter le gestionnaire de paramètres
Remplacez le contenu du GuidesSettingsManager.cs par le code suivant (expliqué ci-dessous) :
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;
}
}
La plupart de ce code crée et analyse le format des paramètres : « RGB(<int>,<int>,<int>) <int>, <int>, ... Les entiers à la fin sont les colonnes uni-basées sur lesquelles vous souhaitez des repères de colonnes. L’extension de guide de colonne capture tous ses paramètres dans une chaîne de paramètre unique.
Il existe certaines parties du code qui méritent d’être mises en surbrillance. La ligne de code suivante obtient l'encapsulation managée de Visual Studio pour le stockage des paramètres. Dans la plupart des cas, cette abstraction recouvre Registre Windows, mais l'API est indépendante du mécanisme de stockage.
internal static SettingsManager VsManagedSettingsManager =
new ShellSettingsManager(ServiceProvider.GlobalProvider);
Le stockage des paramètres Visual Studio utilise un identificateur de catégorie et un identificateur de paramètre pour identifier de manière unique tous les paramètres :
private const string _collectionSettingsName = "Text Editor";
private const string _settingName = "Guides";
Vous n’avez pas besoin d’utiliser "Text Editor"
comme nom de catégorie. Vous pouvez choisir tout ce que vous aimez.
Les premières fonctions sont les points d’entrée pour modifier les paramètres. Ils vérifient les contraintes de haut niveau comme le nombre maximal de repères autorisés. Ensuite, ils appellent WriteSettings
, qui compose une chaîne de paramètres et définit la propriété GuideLinesConfiguration
. La définition de cette propriété enregistre la valeur des paramètres dans le magasin de paramètres Visual Studio et déclenche l’événement SettingsChanged
pour mettre à jour tous les objets ColumnGuideAdornment
, chacun associé à une vue de texte.
Il existe quelques fonctions de point d’entrée, telles que CanAddGuideline
, qui sont utilisées pour implémenter des commandes qui modifient les paramètres. Lorsque Visual Studio affiche des menus, il interroge les implémentations de commandes pour voir si la commande est actuellement activée, quel est son nom, et ainsi de suite. Vous voyez ci-dessous comment raccorder ces points d’entrée pour les implémentations de commandes. Pour plus d’informations sur les commandes, consultez Étendre les menus et les commandes.
Implémenter la classe ColumnGuideAdornment
La classe ColumnGuideAdornment
est instanciée pour chaque vue de texte qui peut avoir des ornements. Cette classe écoute les événements relatifs à la modification de la vue et des paramètres, ainsi que la mise à jour ou le redessin des repères de colonnes si nécessaire.
Remplacez le contenu du ColumnGuideAdornment.cs par le code suivant (expliqué ci-dessous) :
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);
}
}
}
Les instances de cette classe conservent le IWpfTextView associé et une liste d'objets Line
dessinés sur la vue.
Le constructeur (appelé à partir de ColumnGuideAdornmentTextViewCreationListener
lorsque Visual Studio crée de nouvelles vues) crée les objets repère de colonne Line
. Le constructeur ajoute également des gestionnaires pour l’événement SettingsChanged
(défini dans GuidesSettingsManager
) et les événements d’affichage LayoutChanged
et Closed
.
L’événement LayoutChanged
se déclenche en raison de plusieurs types de modifications dans la vue, notamment lorsque Visual Studio crée la vue. Le gestionnaire OnViewLayoutChanged
appelle AddGuidelinesToAdornmentLayer
pour s'exécuter. Le code de OnViewLayoutChanged
détermine s'il doit mettre à jour les positions des lignes en fonction des modifications telles que les changements de taille de police, les marges d'affichage, le défilement horizontal, etc. Le code de UpdatePositions
provoque le dessin des lignes de repère entre les caractères ou juste après la colonne de texte qui se trouve au décalage de caractère indiqué dans la ligne de texte.
Chaque fois que les paramètres modifient la fonction SettingsChanged
recrée simplement tous les objets Line
avec les nouveaux paramètres. Après avoir défini les positions de ligne, le code supprime tous les objets Line
précédents de la couche d’ornement ColumnGuideAdornment
et ajoute les nouveaux.
Définir les commandes, les menus et les emplacements de menu
Il peut être complexe de déclarer des commandes et des menus, en intégrant des groupes de commandes ou de menus dans divers autres menus et en connectant des gestionnaires de commandes. Cette procédure pas à pas met en évidence le fonctionnement des commandes dans cette extension, mais pour plus d’informations, consultez Étendre les menus et les commandes.
Présentation du code
L’extension Guides de colonne montre la déclaration d’un groupe de commandes qui appartiennent ensemble (ajouter une colonne, supprimer une colonne, modifier la couleur de ligne), puis placer ce groupe dans un sous-menu du menu contextuel de l’éditeur. L’extension Guides de colonne ajoute également les commandes au menu principal Modifier, mais les conserve invisibles, décrites comme un modèle commun ci-dessous.
Il existe trois parties à l’implémentation des commandes : ColumnGuideCommandsPackage.cs, ColumnGuideCommandsPackage.vsct et ColumnGuideCommands.cs. Le code généré par les modèles place une commande dans le menu Outils de qui affiche une boîte de dialogue en tant qu’implémentation. Vous pouvez voir comment cela est implémenté dans les fichiers .vsct et ColumnGuideCommands.cs, car il est simple. Vous remplacez le code dans ces fichiers ci-dessous.
Le code du package contient des déclarations réutilisables requises pour Visual Studio afin de découvrir que l’extension offre des commandes et de trouver où placer les commandes. Lorsque le package initialise, il instancie la classe d’implémentation des commandes. Pour plus d’informations sur les packages liés aux commandes, consultez Étendre les menus et les commandes.
Modèle de commandes commun
Les commandes de l’extension Guides de colonne sont un exemple de modèle très courant dans Visual Studio. Vous placez les commandes associées dans un groupe et vous placez ce groupe dans un menu principal, souvent avec «<CommandFlag>CommandWellOnly</CommandFlag>
» défini pour rendre la commande invisible. Le fait de placer des commandes dans les menus principaux (tels que Modifier) leur donne des noms agréables (tels que Edit.AddColumnGuide), qui sont utiles pour rechercher des commandes lors de la réa assignation de liaisons de clés dans Options d’outils. Il est également utile pour obtenir la complétion lors de l'invocation de commandes à partir de la fenêtre de commande .
Vous ajoutez ensuite le groupe de commandes aux menus contextuels ou sous-menus dans lesquels vous attendez que l’utilisateur utilise les commandes. Visual Studio traite CommandWellOnly
comme un indicateur d’invisibilité uniquement pour les menus principaux. Lorsque vous placez le même groupe de commandes dans un menu contextuel ou un sous-menu, les commandes sont visibles.
Dans le cadre du modèle commun, l’extension Guides de colonne crée un deuxième groupe qui contient un sous-menu unique. Le sous-menu contient à son tour le premier groupe avec les commandes de repère à quatre colonnes. Le deuxième groupe qui contient le sous-menu est la ressource réutilisable que vous placez sur différents menus contextuels, qui place un sous-menu sur ces menus contextuels.
Fichier .vsct
Le fichier .vsct déclare les commandes et leur emplacement, ainsi que les icônes, et ainsi de suite. Remplacez le contenu du fichier .vsct par le code suivant (expliqué ci-dessous) :
<?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. Pour que Visual Studio recherche vos gestionnaires de commandes et les appelle, vous devez vous assurer que le GUID de package déclaré dans le fichier ColumnGuideCommandsPackage.cs (généré à partir du modèle d’élément de projet) correspond au GUID de package déclaré dans le fichier .vsct (copié à partir de ci-dessus). Si vous réutilyez cet exemple de code, vous devez vous assurer que vous disposez d’un GUID différent afin de ne pas entrer en conflit avec toute autre personne qui a peut-être copié ce code.
Recherchez cette ligne dans ColumnGuideCommandsPackage.cs et copiez le GUID entre les guillemets :
public const string PackageGuidString = "ef726849-5447-4f73-8de5-01b9e930f7cd";
Ensuite, collez le GUID dans le fichier .vsct afin que vous ayez la ligne suivante dans vos déclarations de Symbols
:
<GuidSymbol name="guidColumnGuideCommandsPkg"
value="{ef726849-5447-4f73-8de5-01b9e930f7cd}" />
Les GUID du jeu de commandes et le fichier image bitmap doivent également être uniques pour vos extensions :
<GuidSymbol name="guidColumnGuidesCommandSet"
value="{c2bc0047-8bfa-4e5a-b5dc-45af8c274d8e}">
<GuidSymbol name="guidImages" value="{2C99F852-587C-43AF-AA2D-F605DE2E46EF}">
Toutefois, vous n’avez pas besoin de modifier les GUID de l’ensemble de commandes et de l’image bitmap dans cette procédure pas à pas pour que le code fonctionne. Le GUID du jeu de commandes doit correspondre à la déclaration dans le fichier ColumnGuideCommands.cs, mais vous remplacerez également le contenu de ce fichier ; par conséquent, les GUID correspondront.
Les autres GUID du fichier .vsct identifient les menus préexistants auxquels les commandes de repère de colonne sont ajoutées, de sorte qu’elles ne changent jamais.
Sections de fichier. Le .vsct a trois sections externes : commandes, placements et symboles. La section commandes définit des groupes de commandes, des menus, des boutons ou des éléments de menu et des bitmaps pour les icônes. La section des placements précise où les groupes sont positionnés dans les menus ou ajoutés à des menus existants. La section symboles déclare les identificateurs utilisés ailleurs dans le fichier .vsct, ce qui rend le code .vsct plus lisible que d’avoir des GUID et des nombres hexadécimaux partout.
Section commandes, définitions de groupes. La section commandes définit d’abord les groupes de commandes. Les groupes de commandes sont des commandes que vous voyez dans les menus avec de légères lignes grises séparant les groupes. Un groupe peut également remplir un sous-menu entier, comme dans cet exemple, et vous ne voyez pas les lignes de séparation grises dans ce cas. Les fichiers .vsct déclarent deux groupes, le GuidesMenuItemsGroup
qui dépend de l'IDM_VS_MENU_EDIT
(le menu principal Modifier) et le GuidesContextMenuGroup
qui dépend du IDM_VS_CTXT_CODEWIN
(menu contextuel de l’éditeur de code).
La deuxième déclaration de groupe a une priorité 0x0600
:
<Group guid="guidColumnGuidesCommandSet" id="GuidesContextMenuGroup"
priority="0x0600">
L’idée est de placer le sous-menu repères de colonnes à la fin de n’importe quel menu contextuel auquel vous ajoutez le groupe de sous-menus. Mais vous ne devez pas supposer que vous connaissez le mieux et forcer le sous-menu à toujours être en dernier en utilisant une priorité de 0xFFFF
. Vous devez expérimenter le nombre pour voir où se trouve votre sous-menu sur les menus contextuels où vous le placez. Dans ce cas, 0x0600
est suffisamment élevé pour le placer à la fin des menus autant que vous pouvez voir, mais il laisse la marge de manœuvre à quelqu’un d’autre pour concevoir leur extension afin qu'elle soit inférieure à celle des repères de colonne, si cela est souhaitable.
section des commandes, définition du menu. Ensuite, la section de commande définit le sous-menu GuidesSubMenu
, associé à GuidesContextMenuGroup
. Le GuidesContextMenuGroup
est le groupe que vous ajoutez à tous les menus contextuels pertinents. Dans la section placements, le code place le groupe avec les commandes de repère à quatre colonnes dans ce sous-menu.
Section commandes , définitions de boutons. La section commandes définit ensuite les éléments de menu ou les boutons qui sont les commandes de repère à quatre colonnes. CommandWellOnly
, décrit ci-dessus, signifie que les commandes sont invisibles lorsqu’elles sont placées dans un menu principal. Deux des déclarations de bouton d’élément de menu (ajouter un repère et supprimer le repère) ont également un indicateur AllowParams
:
<CommandFlag>AllowParams</CommandFlag>
Ce paramètre permet, en plus d'avoir des emplacements dans le menu principal, à la commande de recevoir des arguments lorsque Visual Studio appelle le gestionnaire de commandes. Si l’utilisateur exécute la commande à partir de la fenêtre de commande, l’argument est transmis au gestionnaire de commandes dans les arguments d’événement.
Sections de commande , définitions de bitmaps. Enfin, la section commandes déclare les bitmaps ou les icônes utilisées pour les commandes. Cette section est une déclaration simple qui identifie la ressource de projet et répertorie les index basés sur un seul index d’icônes utilisées. La section symboles du fichier .vsct déclare les valeurs des identificateurs utilisés comme index. Cette procédure pas à pas utilise la bande bitmap fournie avec le modèle d’élément de commande personnalisé ajouté au projet.
Section Placements. Après la section commandes vient la section placements. La première est l’emplacement où le code ajoute le premier groupe décrit ci-dessus qui contient les commandes de repère à quatre colonnes dans le sous-menu où les commandes apparaissent :
<CommandPlacement guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup"
priority="0x0100">
<Parent guid="guidColumnGuidesCommandSet" id="GuidesSubMenu" />
</CommandPlacement>
Tous les autres placements ajoutent le GuidesContextMenuGroup
(qui contient le GuidesSubMenu
) à d’autres menus contextuels de l’éditeur. Lors de la déclaration du GuidesContextMenuGroup
, le code a été parenté au menu contextuel de l'éditeur de code. C’est pourquoi vous ne voyez pas de placement pour le menu contextuel de l’éditeur de code.
Section Symboles. Comme indiqué ci-dessus, la section symboles déclare les identificateurs utilisés ailleurs dans le fichier .vsct, ce qui rend le code .vsct plus lisible que d’avoir des GUID et des nombres hexadécimaux partout. Les points importants de cette section sont que le GUID du package doit accepter la déclaration dans la classe de package. Et le GUID du jeu de commandes doit être en accord avec la déclaration dans la classe d’implémentation de commande.
Implémenter les commandes
Le fichier ColumnGuideCommands.cs implémente les commandes et connecte les gestionnaires. Lorsque Visual Studio charge le package et l’initialise, le package appelle à son tour Initialize
sur la classe d’implémentation des commandes. L’initialisation des commandes instancie simplement la classe, et le constructeur connecte tous les gestionnaires de commandes.
Remplacez le contenu du fichier ColumnGuideCommands.cs par le code suivant (expliqué ci-dessous) :
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);
}
}
}
}
}
Fixer les références. Vous ne disposez pas d’une référence à ce stade. Appuyez sur le bouton pointeur droit sur le nœud Références dans l’Explorateur de solutions. Choisissez la commande Ajouter .... La boîte de dialogue Ajouter une référence contient une zone de recherche dans le coin supérieur droit. Entrez « editor » (sans guillemets doubles). Choisissez l’élément microsoft.VisualStudio.Editor (vous devez cocher la case à gauche de l’élément, pas simplement sélectionner l’élément) et choisir OK pour ajouter la référence.
Initialisation. Lorsque la classe de package initialise, elle appelle Initialize
sur la classe d’implémentation des commandes. L'initialisation ColumnGuideCommands
instancie la classe et enregistre l’instance de la classe ainsi que la référence du package dans les membres de la classe.
Examinons l’un des raccordements du gestionnaire de commandes à partir du constructeur de classe :
_addGuidelineCommand =
new OleMenuCommand(AddColumnGuideExecuted, null,
AddColumnGuideBeforeQueryStatus,
new CommandID(ColumnGuideCommands.CommandSet,
cmdidAddColumnGuide));
Vous créez un OleMenuCommand
. Visual Studio utilise le système de commandes Microsoft Office. Les arguments clés lors de l’instanciation d’un OleMenuCommand
sont la fonction qui implémente la commande (AddColumnGuideExecuted
), la fonction à appeler lorsque Visual Studio affiche un menu avec la commande (AddColumnGuideBeforeQueryStatus
) et l’ID de commande. Visual Studio appelle la fonction d’état de requête avant d’afficher une commande dans un menu afin que la commande puisse se rendre invisible ou grisée pour un affichage particulier du menu (par exemple, désactiver Copier s’il n’y a pas de sélection), modifier son icône ou même modifier son nom (par exemple, à partir d’Ajouter quelque chose à supprimer), et ainsi de suite. L’ID de commande doit correspondre à un ID de commande déclaré dans le fichier .vsct. Les chaînes du jeu de commandes et celles de la commande ajout de repères de colonnes doivent correspondre entre le fichier .vsct et le fichier ColumnGuideCommands.cs.
La ligne suivante fournit de l’aide lorsque les utilisateurs appellent la commande via la fenêtre commande (expliquée ci-dessous) :
_addGuidelineCommand.ParametersDescription = "<column>";
État de requête. Les fonctions d’état de requête AddColumnGuideBeforeQueryStatus
et RemoveColumnGuideBeforeQueryStatus
vérifient certains paramètres, par exemple le nombre maximal de guides ou le nombre maximal de colonnes, et également s’il existe un guide de colonne à supprimer. Ils activent les commandes si les conditions sont appropriées. Les fonctions d’état des requêtes doivent être efficaces, car elles s’exécutent chaque fois que Visual Studio affiche un menu et pour chaque commande du menu.
Fonction AddColumnGuideExecuted. La partie intéressante de l’ajout d’un guide consiste à déterminer la vue actuelle de l’éditeur et la position du curseur. Tout d’abord, cette fonction appelle GetApplicableColumn
, qui vérifie s’il existe un argument fourni par l’utilisateur dans les arguments d’événement du gestionnaire de commandes, et s’il n’y a aucun, la fonction vérifie l’affichage de l’éditeur :
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
doit creuser un peu pour obtenir IWpfTextViewune vue du code. Si vous suivez GetActiveTextView
, GetActiveView
et GetTextViewFromVsTextView
, vous pouvez voir comment procéder. Le code suivant représente le code pertinent abstrait, en commençant par la sélection actuelle, puis en obtenant la fenêtre de la sélection, ensuite en accédant au DocView de la fenêtre en tant que IVsTextView, puis en obtenant un IVsUserData à partir de l'IVsTextView, suivi de l'acquisition d'un hôte de vue, et enfin de l'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;
Une fois que vous disposez d’un IWpfTextView, vous pouvez obtenir la colonne où se trouve le curseur :
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));
}
Avec la colonne active dans laquelle l’utilisateur a cliqué, le code appelle simplement le gestionnaire de paramètres pour ajouter ou supprimer la colonne. Le gestionnaire de paramètres déclenche l’événement auquel tous les objets ColumnGuideAdornment
écoutent. Lorsque l’événement se déclenche, ces objets mettent à jour leurs vues de texte associées avec de nouveaux paramètres de repère de colonne.
Appeler la commande à partir de la fenêtre commande
L’exemple de repères de colonnes permet aux utilisateurs d’appeler deux commandes à partir de la fenêtre de commande en tant que fonctionnalité d'extensibilité. Si vous utilisez la commande Vue | Autres fenêtres | Fenêtre de commande, vous pouvez voir la Fenêtre de commande. Vous pouvez interagir avec la fenêtre de commande en entrant « edit ». Avec la saisie semi-automatique du nom de la commande et en fournissant l’argument 120, vous disposez du résultat suivant :
> Edit.AddColumnGuide 120
>
Les éléments de l’exemple qui activent ce comportement se trouvent dans les déclarations de fichier .vsct .vsct, le constructeur de classe ColumnGuideCommands
lorsqu’il connecte des gestionnaires de commandes et les implémentations du gestionnaire de commandes qui vérifient les arguments d’événement.
Vous avez vu «<CommandFlag>CommandWellOnly</CommandFlag>
» dans le fichier .vsct ainsi que les emplacements dans le menu principal Modifier, bien que les commandes ne soient pas affichées dans l'interface utilisateur du menu Modifier. Les avoir dans le menu principal Modifier leur donne des noms tels que Edit.AddColumnGuide. Déclaration de groupe de commandes qui contient les quatre commandes placées directement dans le menu Modifier :
<Group guid="guidColumnGuidesCommandSet" id="GuidesMenuItemsGroup"
priority="0xB801">
<Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_EDIT" />
</Group>
La section boutons a ultérieurement déclaré les commandes CommandWellOnly
pour les garder invisibles dans le menu principal et les déclarer avec 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>
Vous avez vu le code de connexion du gestionnaire de commandes dans le constructeur de la classe ColumnGuideCommands
, qui fournit une description du paramètre autorisé :
_addGuidelineCommand.ParametersDescription = "<column>";
Vous avez vu la fonction GetApplicableColumn
vérifie OleMenuCmdEventArgs
pour obtenir une valeur avant de vérifier la vue de l’éditeur pour une colonne active :
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;
}
Essayer votre extension
Vous pouvez maintenant appuyer sur F5 pour exécuter votre extension Guides de colonne. Ouvrez un fichier texte et utilisez le menu contextuel de l’éditeur pour ajouter des lignes de repère, les supprimer et modifier leur couleur. Cliquez dans le texte (pas d’espace blanc passé la fin de la ligne) pour ajouter un repère de colonne, ou l’éditeur l’ajoute à la dernière colonne de la ligne. Si vous utilisez la fenêtre commande et appelez les commandes avec un argument, vous pouvez ajouter des repères de colonnes n’importe où.
Si vous souhaitez essayer différents placements de commandes, modifier les noms, modifier les icônes, et ainsi de suite, et que vous rencontrez des problèmes avec Visual Studio montrant le dernier code dans les menus, vous pouvez réinitialiser la ruche expérimentale dans laquelle vous déboguez. Affichez le menu Démarrer Windows et tapez « réinitialiser ». Recherchez et exécutez la commande Réinitialiser l’instance expérimentale Visual Studio suivante. Cette commande nettoie la ruche du registre expérimental de tous les composants d’extension. Il ne nettoie pas les paramètres des composants. Par conséquent, les repères que vous aviez lorsque vous arrêtez la ruche expérimentale de Visual Studio sont toujours là lorsque votre code lit le magasin de paramètres lors du prochain lancement.
Projet de code terminé
Il y aura bientôt un projet GitHub d’exemples d’extensibilité Visual Studio, et le projet terminé sera là. Cet article sera mis à jour pour rediriger là-bas quand cela se produit. L’exemple de projet terminé peut avoir des guid différents et aura une bande de bitmaps différente pour les icônes de commande.
Vous pouvez essayer une version de la fonctionnalité des guides de colonnes avec cette extension Visual Studio Gallery .