Nie poruszam podstaw tworzenia rozszerzeń zaś opisuje jedynie w jaki sposób wykorzystać dostępne mechanizmy do pobrania informacji która nas interesuje. Wszystkich zainteresowanych pisaniem rozszerzeń pod VS 2010 zapraszam na strony MSDN, które naprawdę dobrze tłumaczą o co w tym wszystkim biega.
Nowy system rozszerzeń VS 2010 jest naprawdę przyjemny nie tylko pod względem możliwości takich jak chociażby zmiany sposobu wyświetlania komentarzy w kodzie ale także pod względem jego oprogramowania. Wszystko opiera się na MEF, więc jeżeli coś potrzebujemy to mówimy Import i to mamy. I do póki chcemy stworzyć proste rozszerzenia jesteśmy wstanie trzymać się tego co udostępnia nam najnowsze SDK. Jeżeli jednak w ciągu pierwszych kilkunastu minut czegoś nie znajdziemy oznacza to, że jesteśmy w głębokiej jaskini platońskiej. Problem polega na tym iż zaczynamy mieszać dostęp do obiektów COM z pięknym i zgrabnym .NETowym API. Z funkcji na funkcje musimy zmieniać sposób myślenia – dobra, wykonałem akcje X, teraz muszę sprawdzić czy operacja się udała poprzez porównanie rezultatu do wartości NON_ZERO.
Na taki właśnie problem natrafiłem podczas pisania jednego z rozszerzeń. To co chciałem osiągnąć to w elegancki sposób z wykorzystaniem nowego API pobrać informacje o czcionce i kolorze czcionki ustawionej w opcjach VS 2010. Po 2h prób i błędów okazało się, że niestety tego akurat feature w nowym VS z nowym API nie da się osiągnąć. Trzeba odwołać się do interfejsów operujących na COM, które zaś w dokumentacji za dużo nie mówią poza tym iż wartość zwracana to mieszanka 2-3 różnym enumeratorów – jak za pomocą takiego wyniku stworzyć czcionkę?
Przypadkiem szukając odpowiedzi na powyższe i inne pytania natrafiłem na forum MSDN w którym osoba dzieli się swoim fragmentem kodu do pobrania koloru czcionki. Wciąż jednak miałem problem z pobraniem czcionki – jednak głównie on leżał w mojej głupocie, gdyż sądziłem iż dana grupa ustawień fontów i kolorów w VS może mieć więcej niż jeden font, a metody takiej która by mi pozwalała zwrócić czcionkę w zależności od wartości – na przykład Text Editor > Comment nie znalazłem a próby podania w jakiś sposób nazwy kończyły się fiaskiem – więc zamiast sprawdzić co da się ustawić a co nie kombinowałem jak koń pod górę :) No ale w końcu się udało :)
A więc do rzeczy, by móc pobrać czcionkę i jej kolor z ustawień, do naszego projektu należy dodać następujące 4 referencje:
- Microsoft.VisualStudio.Shell – zawiera klasę VSConstants, posiadającą ogólnodostępne, zdefiniowane wartości jak chociażby wartość S_OK, która mówi czy operacja się powiodła;
- Microsoft.VisualStudio.Shell.Immutable.10.0 – posiada interfejs SVsServiceProvider umożliwiający nam pobierania usług COM;
- Microsoft.VisualStudio.Shell.Interop – zawiera interfejs IVsFontAndColorStorage umożliwiający nam odwołanie się do ustawień;
- System.Drawing – zawiera klasę Font umożliwiającą nam konwersje z dziwnych numerków na odpowiednią czcionkę.
Zanim jednak przejdziemy do kodu, kilka informacji, o których warto wiedzieć. Konfiguracja fontów i czcionek przechowywana jest w rejestrze pod kluczami (ludzie piszą iż powinny ustawienia być pod HKLM jednak u mnie ich tam napewno nie było):
- HKCU/Software/Microsoft/VisualStudio/10.0/FontsAndColors/GUID
- HKCU/Software/Microsoft/VisualStudio/10.0/FontsAndColors/Cache/GUID
Gdzie GUID określa daną kategorię ustawień. Nazwa kategorii to wartości z combobox Show settings for w oknie Tools -> Options -> Fonts and Colors.
Zaś GUID danej kategorii to jej unikatowy identyfikator, niestety stworzenie pary z Nazwy i GUIDa proste nie jest i wymaga regedita. Dzięki Show settings for wiemy jak się nazywa nasza kategoria, więc następnie w odpowiedniej ścieżce rejestru przeglądamy wszystkie klucze w poszukiwaniu wartości string CategoryName, która odpowiada nazwie z combobox.
Na szczęście mamy jeszcze drugą opcję możliwą do wykonania. Jeżeli wyeksportujemy ustawienia kolorów z VS to w pliku vssettings powinniśmy znaleźć element Category którego atrybut @Name posiada wartość Environment_FontsAndColors:
Element ten zawiera dzieci Category, które posiadają atrybut @GUID – atrybut ten odpowiada naszemu poszukiwanemu GUIDowi, zaś atrybut @Name od elementu Item określa ustawienia dla danego Display Item (bo wyjdzie masło maślane jak napiszę: dla elementu danej kategorii) z okna Fonts and Colors:
GUID kategorii jak i nazwa elementu Display Item są dwoma wymaganymi parametrami jeżeli chcemy pobrać wartość i muszą być znane odgórnie – nie ma lub ja nie znalazłem sposobu, pobrania ich za pomocą VS SDK, te parametry musimy znać i przeważnie ręcznie je wprowadzać, lub za pomocą jakiejś konfiguracji. Na szczęście GUID nie ulega zmianie przy kolejnych wersjach VS (przeważnie!, są takie które uległy zmianie i można je zobaczyć na przykład w pliku vsshelluuids.h), jednak nazwa elementu może nie tylko ulec zmianie jak i może całkowicie zniknąć w następnych wersjach – choć w to wątpię.
Tak przygotowani, możemy zaczynać zabawę. Pierwszym krokiem po tym jak dodaliśmy referencje i zebraliśmy nasze GUIDy i nazwy jest pobranie interfejsu, umożliwiającego nam pobieranie usług COM, w tym celu w klasie, która jest exportowana (bierze udział w kompozycji, jest to przeważnie XxxNameFactory jeżeli tworzymy projekty z szablonu VS) w VS Extension, dodajemy import:
// if we are creating new extensions that use MEF - not VSPackage // we can import SVsServiceProvider, null is just given to // distinguish property from field. [Import] private SVsServiceProvider _svsServiceProvider = null;
Teraz możemy za pomocą metody GetService pobrać usługę która nas interesuje:
var fontAndColorStorage = _svsServiceProvider.GetService(
typeof(IVsFontAndColorStorage)
) as IVsFontAndColorStorage;
Następnie musimy otworzyć storage dla danej kategorii – to dzięki niemu będziemy mieć dostęp do ustawień:
int openResult = fontAndColorStorage.OpenCategory(ref textEditorGuid, (uint)
(__FCSTORAGEFLAGS.FCSF_LOADDEFAULTS |
__FCSTORAGEFLAGS.FCSF_NOAUTOCOLORS));
if(openResult != VSConstants.S_OK) return null; // or throw exception
Teraz już pozostaje nam tylko i wyłącznie wywoływanie metod GetItem i GetFont z odpowiednimi PUSTYMI (przeważnie deklaracja nowej tablicy o rozmiarze 1) parametrami w celu pobrania wartości, które nas interesują. Po każdej operacji jednak musimy sprawdzić czy ona się udała, jeżeli nie to otwarty storage należy zamknąć:
// we will be calling this few times in code, therefore
// local function
Func<FontAndColorInfo, FontAndColorInfo> closeCategory = (fontAndColor) =>
{
int closeResult = fontAndColorStorage.CloseCategory();
if(closeResult == VSConstants.S_OK)
{
return fontAndColor;
}
throw new InvalidOperationException();
};
A tak wygląda cała funkcja, która przyjmując GUID kategorii i nazwę elementu, zwróci nam NULL lub obiekt z informacją o czcionce i jej kolorze:
// if we are creating new extensions that use MEF - not VSPackage
// we can import SVsServiceProvider, null is just given to
// distinguish property from field.
[Import]
private SVsServiceProvider _svsServiceProvider = null;
public void DummyCall()
{
// text editor GUID taken from:
// HKCU/Software/Microsoft/VisualStudio/10.0/FontsAndColors/GUID
// OR
// HKCU/Software/Microsoft/VisualStudio/10.0/FontsAndColors/Cache/GUID
// OR
// .vssettings file in section: Environment_FontsAndColors
Guid textEditorGuid = Guid.Parse("{A27B4E24-A735-4D1D-B8E7-9716E1E3D8E0}");
try
{
var result = GetFontAndColorInfo("Line Numbers", textEditorGuid);
if(result == null)
{
// load defaults
}
}
catch(Exception ex)
{
// handle, try by more specific than Exception
}
}
/// <remarks>
/// Colors solution taken and changed from:
/// <see cref="http://social.msdn.microsoft.com/Forums/en-US/vsx/thread/17da3d9c-7e5d-413f-b9c6-eeedc1ec991f/"/>
/// </remarks>
public FontAndColorInfo GetFontAndColorInfo(string textEditorItemName, Guid textEditorGuid)
{
var fontAndColorStorage = _svsServiceProvider.GetService(
typeof(IVsFontAndColorStorage)
) as IVsFontAndColorStorage;
if(fontAndColorStorage == null) return null; // or throw exception
// we will be calling this few times in code, therefore
// local function
Func<FontAndColorInfo, FontAndColorInfo> closeCategory = (fontAndColor) =>
{
int closeResult = fontAndColorStorage.CloseCategory();
if(closeResult == VSConstants.S_OK)
{
return fontAndColor;
}
throw new InvalidOperationException();
};
int openResult = fontAndColorStorage.OpenCategory(ref textEditorGuid, (uint)
(__FCSTORAGEFLAGS.FCSF_LOADDEFAULTS |
__FCSTORAGEFLAGS.FCSF_NOAUTOCOLORS));
if(openResult != VSConstants.S_OK) return null; // or throw exception
var colorableItemInfo = new ColorableItemInfo[1];
int getItemResult = fontAndColorStorage.GetItem(textEditorItemName, colorableItemInfo);
if(getItemResult != VSConstants.S_OK)
{
return closeCategory(null);
}
// Valid - NON_ZERO value
if(colorableItemInfo[0].bForegroundValid == 0)
{
return closeCategory(null);
}
var lineNumbersForeground = ConvertFromWin32Color((int)colorableItemInfo[0].crForeground);
// there is only one font and font size per category
// you can check that by playing with Tools -> Options -> Fonts And Colors
var fontInfos = new FontInfo[1];
var logFontW = new LOGFONTW[1];
int getFontResult = fontAndColorStorage.GetFont(logFontW, fontInfos);
if(getFontResult != VSConstants.S_OK)
{
return closeCategory(null);
}
// Font - System.Drawning.Font
// this can throw exception
var lineNumberFont = Font.FromLogFont(logFontW[0]);
// we can do this in this way too:
//var lineNumberFont = new Font(fontInfos[0].bstrFaceName, (float)fontInfos[0].wPointSize);
return closeCategory(new FontAndColorInfo
{
Foreground = lineNumbersForeground,
TypeFace = new FontFamily(lineNumberFont.FontFamily.Name),
FontSizeInPoints = lineNumberFont.SizeInPoints,
FontSize = lineNumberFont.Size
});
}
/// <see cref="http://www.alteridem.net/2007/08/23/win32-colorref-vs-net-color/"/>
public static Color ConvertFromWin32Color(int color)
{
int r = color & 0x000000FF;
int g = (color & 0x0000FF00) >> 8;
int b = (color & 0x00FF0000) >> 16;
return Color.FromArgb(255, (byte)r, (byte)g, (byte)b);
}
public class FontAndColorInfo
{
// System.Windows.Media.Color
public Color Foreground { get; set; }
// System.Windows.Media.FontFamily
public FontFamily TypeFace { get; set; }
public float FontSize { get; set; }
public float FontSizeInPoints { get; set; }
}
Poniżej zaś rezultat po połączeniu z rozszerzeniem nad którym pracuje:
Mówiąc szczerze, po tym jak się trochę pobawiłem rozszerzeniami dla VS, naprawdę zacząłem doceniać to co chłopaki z JetBrains i DevExpress robią. To wcale nie jest prosta sprawa operować na kodzie, UI, fontach itp. itd. To z początku może wydawać się proste, jednak im człowiek bardziej się zagłębi w bebechy tym bardziej widzi, że aby coś zmienić musi się naprawdę nieźle napracować a i także dobrze przemyśleć sprawę bo tutaj łatwo o pomyłkę, a zawieszający się VS jest jedną z najgorszych rzeczy jakie można załatwić użytkownikowi końcowemu dostarczając mu wadliwe oprogramowanie.
Tak czy siak, ja tam się cieszę bo czegoś się nauczyłem, zobaczymy czy będę wstanie to wykorzystać :)














