对于
Visual Studio 2022,
SENya的答案 只能部分地或“有时”起作用:更改 VS 主题不会立即正确更改颜色,大约有10% 的时间。此外,从暗色主题更改为亮色主题通常看起来可以正常工作,但在重新启动 Visual Studio 后,黑色而不是亮色通常被使用(超过一半的时间)。所有这些都是非确定性的。
经过一些调试,我理解问题如下:调用
IVsFontAndColorCacheManager.ClearCache()
会删除注册表键
"Software\Microsoft\VisualStudio\17.0_4d51a943Exp\FontAndColors\Cache\{75A05685-00A8-4DED-BAE5-E7A50BFA929A}\ItemAndFontInfo"
,这是字体和颜色的缓存。在自定义主题更改函数完成后,有时(但不总是)其他 Visual Studio 组件会立即更新字体和颜色缓存。即它调用类似于
fontAndColorStorage.OpenCategory(ref mFontAndColorCategoryGUID, (uint)__FCSTORAGEFLAGS.FCSF_READONLY | (uint)__FCSTORAGEFLAGS.FCSF_LOADDEFAULTS)
的东西。
注意
FCSF_LOADDEFAULTS
。这会导致 Visual Studio 重新创建注册表键。然而,显然它没有使用更新的
IClassificationFormatMap
中的颜色,而是使用设置在
ClassificationFormatDefinition
本身上的颜色,这些颜色没有被更新。因此,更改主题会立即更改显示的颜色(因为
IClassificationFormatMap
已更新),但是注册表缓存最终会出现错误的颜色。在 VS 重新启动后,它将使用缓存的值,因此最终会出现错误的颜色。通过在
ClassificationFormatDefinition
实例上也更改颜色,问题似乎已经解决了。
详情
在我的VSDoxyHighlighter(Github)中,我按照SENya的答案进行了适应:
首先,创建一些辅助类来存储默认文本格式:
public class TextProperties
{
public readonly Color? Foreground;
public readonly Color? Background;
public readonly bool IsBold;
public readonly bool IsItalic;
public TextProperties(Color? foreground, Color? background, bool isBold, bool isItalic)
{
Foreground = foreground;
Background = background;
IsBold = isBold;
IsItalic = isItalic;
}
}
然后是处理主题相关内容的主要类,名为DefaultColors
:
[Export]
public class DefaultColors : IDisposable
{
DefaultColors()
{
VSColorTheme.ThemeChanged += VSThemeChanged;
mCurrentTheme = GetCurrentTheme();
}
public void Dispose()
{
if (mDisposed) {
return;
}
mDisposed = true;
VSColorTheme.ThemeChanged -= VSThemeChanged;
}
public Dictionary<string, TextProperties> GetDefaultFormattingForCurrentTheme()
{
return GetDefaultFormattingForTheme(mCurrentTheme);
}
public void RegisterFormatDefinition(IFormatDefinition f)
{
mFormatDefinitions.Add(f);
}
private enum Theme
{
Light,
Dark
}
static private Dictionary<string, TextProperties> GetDefaultFormattingForTheme(Theme theme)
{
switch (theme) {
case Theme.Light:
return cLightColors;
case Theme.Dark:
return cDarkColors;
default:
throw new System.Exception("Unknown Theme");
}
}
private void VSThemeChanged(ThemeChangedEventArgs e)
{
ThreadHelper.ThrowIfNotOnUIThread();
var newTheme = GetCurrentTheme();
if (newTheme != mCurrentTheme) {
mCurrentTheme = newTheme;
ThemeChangedImpl();
}
}
private void ThemeChangedImpl()
{
ThreadHelper.ThrowIfNotOnUIThread();
var fontAndColorStorage = ServiceProvider.GlobalProvider.GetService<SVsFontAndColorStorage, IVsFontAndColorStorage>();
var fontAndColorCacheManager = ServiceProvider.GlobalProvider.GetService<SVsFontAndColorCacheManager, IVsFontAndColorCacheManager>();
fontAndColorCacheManager.CheckCache(ref mFontAndColorCategoryGUID, out int _);
if (fontAndColorStorage.OpenCategory(ref mFontAndColorCategoryGUID, (uint)__FCSTORAGEFLAGS.FCSF_READONLY) != VSConstants.S_OK) {
throw new System.Exception("Failed to open font and color registry.");
}
IClassificationFormatMap formatMap = mClassificationFormatMapService.GetClassificationFormatMap(category: "text");
try {
formatMap.BeginBatchUpdate();
ColorableItemInfo[] colorInfo = new ColorableItemInfo[1];
foreach (var p in GetDefaultFormattingForTheme(mCurrentTheme)) {
string classificationTypeId = p.Key;
TextProperties newColor = p.Value;
if (fontAndColorStorage.GetItem(classificationTypeId, colorInfo) != VSConstants.S_OK) {
IClassificationType classificationType = mClassificationTypeRegistryService.GetClassificationType(classificationTypeId);
var oldProp = formatMap.GetTextProperties(classificationType);
var oldTypeface = oldProp.Typeface;
var foregroundBrush = newColor.Foreground == null ? null : new SolidColorBrush(newColor.Foreground.Value);
var backgroundBrush = newColor.Background == null ? null : new SolidColorBrush(newColor.Background.Value);
var newFontStyle = newColor.IsItalic ? FontStyles.Italic : FontStyles.Normal;
var newWeight = newColor.IsBold ? FontWeights.Bold : FontWeights.Normal;
var newTypeface = new Typeface(oldTypeface.FontFamily, newFontStyle, newWeight, oldTypeface.Stretch);
var newProp = TextFormattingRunProperties.CreateTextFormattingRunProperties(
foregroundBrush, backgroundBrush, newTypeface, null, null,
oldProp.TextDecorations, oldProp.TextEffects, oldProp.CultureInfo);
formatMap.SetTextProperties(classificationType, newProp);
}
}
foreach (IFormatDefinition f in mFormatDefinitions) {
f.Reinitialize();
}
}
finally {
formatMap.EndBatchUpdate();
fontAndColorStorage.CloseCategory();
if (fontAndColorCacheManager.ClearCache(ref mFontAndColorCategoryGUID) != VSConstants.S_OK) {
throw new System.Exception("Failed to clear cache of FontAndColorCacheManager.");
}
}
}
private Theme GetCurrentTheme()
{
var referenceColor = VSColorTheme.GetThemedColor(EnvironmentColors.ToolWindowBackgroundColorKey);
return (referenceColor != null && referenceColor.B < 100) ? Theme.Dark : Theme.Light;
}
static readonly Dictionary<string, TextProperties> cLightColors = new Dictionary<string, TextProperties> {
{ IDs.ID_command, new TextProperties(foreground: Color.FromRgb(0, 75, 0), background: null, isBold: true, isItalic: false) },
{ IDs.ID_parameter1, new TextProperties(foreground: Color.FromRgb(0, 80, 218), background: null, isBold: true, isItalic: false) },
};
static readonly Dictionary<string, TextProperties> cDarkColors = new Dictionary<string, TextProperties> {
{ IDs.ID_command, new TextProperties(foreground: Color.FromRgb(140, 203, 128), background: null, isBold: true, isItalic: false) },
{ IDs.ID_parameter1, new TextProperties(foreground: Color.FromRgb(86, 156, 214), background: null, isBold: true, isItalic: false) },
};
private Theme mCurrentTheme;
private const string cFontAndColorCategory = "75A05685-00A8-4DED-BAE5-E7A50BFA929A";
Guid mFontAndColorCategoryGUID = new Guid(cFontAndColorCategory);
[Import]
private IClassificationFormatMapService mClassificationFormatMapService = null;
[Import]
private IClassificationTypeRegistryService mClassificationTypeRegistryService = null;
private List<IFormatDefinition> mFormatDefinitions = new List<IFormatDefinition>();
private bool mDisposed = false;
}
这里需要注意几点:
- 不应手动创建
DefaultColors
的实例,而是应该通过MEF(例如通过Import
属性)创建单个实例。请参见下面。
- 当前的VS主题是通过检查一些当前活动的背景颜色来确定的,如this answer所示。原则上可以检查轻、蓝、蓝(高对比度)、暗等VS主题是否处于活动状态。然而,由于用户可以安装其他主题,因此列表是无限的。因此,仅检查背景颜色更通用和更健壮。
ClassificationFormatDefinition
定义(表示Visual Studio用于各种分类的文本格式)预计将通过RegisterFormatDefinition()
在DefaultColors
实例上注册自己。
- 要接收有关Visual Studio主题更改的通知,我们订阅
VSColorTheme.ThemeChanged
。还要注意,事件会多次触发每个主题更改。由于在ThemeChangedImpl()
中执行所有更新代码多次是不必要的,因此我们检查新旧主题是否不同。
- 对主题更改的反应在
ThemeChangedImpl()
中。这是主要基于SENya的答案的代码,但添加了先前通过RegisterFormatDefinition()
注册的ClassificationFormatDefinition
的Reinitialize()
调用。
为了完整起见,ID_command
和ID_parameter1
是一些自定义的标识符,用于识别ClassificationFormatDefinition
(见下文):
public static class IDs
{
public const string ID_command = "VSDoxyHighlighter_Command";
public const string ID_parameter1 = "VSDoxyHighlighter_Parameter1";
}
现在,实际的ClassificationFormatDefinition
是这样定义的:
它们继承自一个接口IFormatDefinition
(可以传递给DefaultColors.RegisterFormatDefinition()
函数)。
public interface IFormatDefinition
{
void Reinitialize();
}
所有的ClassificationFormatDefinition
都基本相同:它们在构造时设置文本属性(颜色、加粗、斜体等),以适应当前的颜色主题。这是通过查询DefaultColors.GetDefaultFormattingForCurrentTheme()
函数来完成的。此外,它们会在DefaultColors
上注册并实现Reinitialize()
方法(由DefaultColors
调用)。由于始终如一,我为它们定义了一个基类FormatDefinitionBase
:
internal abstract class FormatDefinitionBase : ClassificationFormatDefinition, IFormatDefinition
{
protected FormatDefinitionBase(DefaultColors defaultColors, string ID, string displayName)
{
if (defaultColors == null) {
throw new System.ArgumentNullException("VSDoxyHighlighter: The 'DefaultColors' to a FormatDefinition is null");
}
mID = ID;
mDefaultColors = defaultColors;
mDefaultColors.RegisterFormatDefinition(this);
DisplayName = displayName;
Reinitialize();
}
public virtual void Reinitialize()
{
TextProperties color = mDefaultColors.GetDefaultFormattingForCurrentTheme()[mID];
ForegroundColor = color.Foreground;
BackgroundColor = color.Background;
IsBold = color.IsBold;
IsItalic = color.IsItalic;
}
protected readonly DefaultColors mDefaultColors;
protected readonly string mID;
}
最后,实际定义看起来像这样:
[Export(typeof(EditorFormatDefinition))]
[ClassificationType(ClassificationTypeNames = IDs.ID_command)]
[Name(IDs.ID_command)]
[UserVisible(true)]
[Order(After = /*Whatever is appropriate for your extension*/)]
internal sealed class CommandFormat : FormatDefinitionBase
{
[ImportingConstructor]
public CommandFormat(DefaultColors defaultColors)
: base(defaultColors, IDs.ID_command, "VSDoxyHighlighter - Command")
{
}
}
[Export(typeof(EditorFormatDefinition))]
[ClassificationType(ClassificationTypeNames = IDs.ID_parameter1)]
[Name(IDs.ID_parameter1)]
[UserVisible(true)]
[Order(After = /*Whatever is appropriate for your extension*/)]
internal sealed class ParameterFormat1 : FormatDefinitionBase
{
[ImportingConstructor]
public ParameterFormat1(DefaultColors defaultColors)
: base(defaultColors, IDs.ID_parameter1, "VSDoxyHighlighter - Parameter 1")
{
}
}
请注意构造函数的标记为
ImportingConstructor
,这样MEF就会自动创建一个
DefaultColors
类的单个实例并将其传递给构造函数。
因此,总结一下:
ClassificationFormatDefinition
由MEF创建。同时,MEF还创建了一个DefaultColors
实例,并将其传递给ClassificationFormatDefinition
。 ClassificationFormatDefinition
设置默认颜色,并提供一个函数来允许在主题更改时重新初始化。为了实现这一点,它也在DefaultColors
实例上注册自己。
DefaultColors
确定当前主题并包含每个主题的默认颜色。
DefaultColors
侦听VSColorTheme.ThemeChanged
事件,如果触发,则清除Visual Studio的字体和颜色缓存,更新当前分类格式映射(以显示新颜色),并使用新颜色更新所有自定义ClassificationFormatDefinition
实例(以便在通过VS重新创建字体和颜色缓存时使用正确的颜色)。