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ć :)