编程那点事 编程那点事编程那点事

DevExpress WPF主题选择器只显示15个主题?深度排查与完整解决方案

最近接手一个企业级WPF项目时,遇到一个让人头疼的问题:明明系统检测到154个DevExpress主题,但主题切换器里只显示15个。客户抱怨找不到他们习惯的Metropolis Dark主题,技术文档翻了个遍,最后发现是一个容易被忽视的配置问题。

今天分享这次排查经历,希望能帮其他开发者少走弯路。

问题是怎么发现的

用户反馈的异常现象

项目上线后第二天,客户就反映主题切换功能"不正常":

客户描述

  • 我们公司一直用Metropolis Dark主题,现在找不到了
  • 主题选择器里只有Windows 11、Office 2019这些新主题
  • 明明说有150多个主题,怎么只显示十几个?

实际情况验证: 打开主题选择器的Gallery控件,确实只能看到15个主题。但是在代码里加断点查看Theme.Themes.Count,返回值是154。这就奇怪了——系统明明加载了所有主题,为什么UI上只显示一小部分?

初步排查思路

我首先怀疑是XAML配置的问题。检查了BarSplitItemThemeSelectorBehavior的相关属性:

<dxb:BarSplitItemThemeSelectorBehavior
    UseSvgGlyphs="True"
    ShowSelectedThemeGlyph="True"
    ShowLegacyThemes="True"
    ShowTouchThemes="False">
</dxb:BarSplitItemThemeSelectorBehavior>

ShowLegacyThemes已经设置为True了,按理说应该显示所有老主题才对。继续查DevExpress官方文档,也没找到明确说明。

转折点:对比官方示例代码

DevExpress自带了一个MailClient示例程序,它的主题切换器工作正常。我把两个项目的代码逐行对比,终于找到了两个关键差异。

问题根源深度解析

根源一:使用了轻量级主题包

打开项目的.csproj文件,发现引用的是:

<PackageReference Include="DevExpress.Wpf.ThemesLW" Version="24.2.3" />

这里的ThemesLW就是问题所在。LW是Lightweight(轻量级)的缩写,这个包只包含DevExpress最新的现代主题,大约15个左右:

ThemesLW包含的主题

  • Windows 11 Light/Dark/Compact (3个)
  • Windows 10 Light/Dark/LightCompact/DarkCompact (4个)
  • Office 2019 Black/White/Colorful/HighContrast (4个)
  • Visual Studio 2019 Blue/Dark/Light (3个)
  • 其他零星现代主题 (1-2个)

ThemesLW不包含的主题

  • Metropolis系列 (Dark/Light)
  • Seven系列
  • Office 2007/2010/2013/2016 全系列
  • Visual Studio 2010/2017 系列
  • 所有触摸优化主题

也就是说,即使你的代码逻辑完全正确,只要用的是ThemesLW包,物理上就不可能显示150多个主题——因为根本没装那么多主题。

根源二:强制使用轻量级模式的配置

App.xaml.cs的静态构造函数里,发现了这行代码:

static App()
{
    CompatibilitySettings.UseLightweightThemes = true;
}

这个设置的作用是强制DevExpress使用轻量级主题引擎。即使你后来安装了完整主题包,这个设置也会让程序只加载轻量级主题,忽略其他主题。

这个配置项一般是在创建项目时,向导自动生成的。如果你当时选择了"仅使用现代主题"选项,向导就会添加这行代码。问题是后期很少有人会想到回头检查这里。

为什么官方会提供两种主题包

理解了问题原因后,我查了下DevExpress为什么要搞两套主题包。原来是从版本20.x开始,DevExpress重构了主题引擎,目的是:

轻量级主题包的优势

  • 程序体积更小(减少约50MB)
  • 启动速度更快(少加载100多个主题文件)
  • 内存占用更低(只保留现代主题)
  • 符合现代UI设计趋势

完整主题包的优势

  • 向后兼容老项目
  • 满足特定行业需求(如金融系统习惯用Office 2010风格)
  • 提供更多选择

对于新项目,官方推荐用轻量级包。但如果客户有老主题需求,或者要从老版本迁移,就必须用完整包。

完整解决方案(三步走)

第一步:替换NuGet包

操作方法一:直接编辑.csproj

找到项目文件(右键项目 → 编辑项目文件),定位到这一行:

<PackageReference Include="DevExpress.Wpf.ThemesLW" Version="24.2.3" />

改成:

<PackageReference Include="DevExpress.Wpf.Themes.All" Version="24.2.3" />

保存后,Visual Studio会自动还原新包。

操作方法二:通过NuGet包管理器

  1. 右键项目 → 管理NuGet程序包
  2. 在"已安装"选项卡找到DevExpress.Wpf.ThemesLW
  3. 点击卸载
  4. 在"浏览"选项卡搜索DevExpress.Wpf.Themes.All
  5. 安装对应版本(注意版本号要和其他DevExpress包一致)

注意事项

  • 确保所有DevExpress包版本一致(都是24.2.3或都是23.x.x)
  • 如果提示依赖冲突,先卸载所有DevExpress包,再统一安装新版本
  • 替换后程序体积会增加约50MB,这是正常的

第二步:修改应用程序初始化配置

打开App.xaml.cs文件,找到静态构造函数,做两处修改:

using DevExpress.Xpf.Core;

public partial class App : Application
{
    static App()
    {
        // 关键修改1:允许使用完整主题包
        CompatibilitySettings.UseLightweightThemes = false;
    }

    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        // 关键修改2:注册所有预定义主题
        Theme.RegisterPredefinedPaletteThemes();

        // 其他初始化代码...
        // 比如设置默认主题:
        // ApplicationThemeHelper.ApplicationThemeName = "MetropolisDark";
    }
}

代码解释

  1. UseLightweightThemes = false

    • 告诉DevExpress可以加载完整主题包
    • 必须在静态构造函数中设置(应用启动时最早执行)
    • 如果不设置,即使安装了Themes.All包也不会生效
  2. Theme.RegisterPredefinedPaletteThemes()

    • 注册所有预定义的调色板主题(包括Legacy主题)
    • 必须在OnStartup中调用,早于主窗口创建
    • 如果缺少这行,主题可能加载但无法在选择器中显示

我踩过的坑: 一开始只改了第一处,忘了调用RegisterPredefinedPaletteThemes()。结果主题数量从15个增加到了30个,但Metropolis等主题还是没出现。后来加了这个方法调用才全部显示。

第三步:清理编译缓存并重新生成

这一步很容易被忽略,但非常重要。因为旧的ThemesLW包的DLL文件可能还缓存在binobj目录里,导致程序运行时还是加载旧版本。

清理步骤

# 方法1:使用PowerShell命令
# 在项目根目录打开PowerShell
Remove-Item -Recurse -Force .\bin\, .\obj\

# 方法2:使用Visual Studio
# 在菜单栏选择:生成 → 清理解决方案
# 然后:生成 → 重新生成解决方案

如果遇到文件锁定错误

错误 MSB3021: 无法复制文件"...\DevExpress.Xpf.Themes.v24.2.dll",
因为它正被另一个进程使用。

解决办法:

  1. 关闭所有Visual Studio实例
  2. 重启电脑(如果是调试进程卡住)
  3. 或者用Process Explorer找到占用文件的进程并结束

重新生成

# 恢复NuGet包
dotnet restore

# 编译项目
dotnet build --configuration Release

# 或者在Visual Studio中按F6

编译成功后,运行程序验证。

XAML配置优化建议

虽然修改了代码和包引用,但如果XAML配置不当,也可能导致主题显示不全。这里提供一个经过生产环境验证的完整配置:

基础版(只显示主题)

<dxb:BarSplitButtonItem Content="主题"
                        dxb:BarManager.ShowGlyphsInPopupMenus="False"
                        GlyphSize="Large">
    <dxb:BarSplitButtonItem.PopupControl>
        <dxb:PopupControlContainer>
            <dxb:GalleryControl>
                <dxb:Gallery ColCount="4"
                             IsItemCaptionVisible="True"
                             ItemGlyphLocation="Top"
                             AllowHoverImages="False">
                </dxb:Gallery>
            </dxb:GalleryControl>
        </dxb:PopupControlContainer>
    </dxb:BarSplitButtonItem.PopupControl>
    
    <dxmvvm:Interaction.Behaviors>
        <dxb:BarSplitItemThemeSelectorBehavior
            UseSvgGlyphs="True"
            ShowSelectedThemeGlyph="True"
            ShowLegacyThemes="True"
            ShowTouchThemes="False">
        </dxb:BarSplitItemThemeSelectorBehavior>
    </dxmvvm:Interaction.Behaviors>
</dxb:BarSplitButtonItem>

进阶版(带"显示更多/更少"切换)

这个版本模仿了Office的主题选择器,默认只显示常用主题,点击按钮后展开全部:

<dxb:BarSplitButtonItem x:Name="themesItem"
                        Content="主题"
                        dxb:BarManager.ShowGlyphsInPopupMenus="False"
                        GlyphSize="Large"
                        ItemClickBehaviour="None">
    <!-- Tag用于存储展开/收起状态 -->
    <dxb:BarSplitButtonItem.Tag>
        <sys:Boolean>False</sys:Boolean>
    </dxb:BarSplitButtonItem.Tag>
    
    <dxb:BarSplitButtonItem.PopupControl>
        <dxb:PopupControlContainer>
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="*"/>
                    <RowDefinition Height="Auto"/>
                </Grid.RowDefinitions>
                
                <!-- 主题Gallery -->
                <dxb:GalleryControl Grid.Row="0">
                    <dxb:Gallery ColCount="4"
                                 ItemClickCommand="{DXCommand '@s.Tag.IsOpen=false'}" 
                                 Tag="{Binding RelativeSource={RelativeSource AncestorType=dxb:PopupControlContainer}}"
                                 IsItemCaptionVisible="True"
                                 ItemGlyphLocation="Top"
                                 AllowHoverImages="False"
                                 Style="{StaticResource {dxbt:GalleryThemeSelectorThemeKey ResourceKey=InRibbonDropDownGalleryStyle}}">
                    </dxb:Gallery>
                </dxb:GalleryControl>
                
                <!-- 展开/收起按钮 -->
                <StackPanel Grid.Row="1" Tag="{Binding ElementName=themesItem}">
                    <Separator Margin="0"/>
                    <Button Content="{DXBinding '@s.Tag.Tag ? `显示更少` : `显示更多`'}"  
                            Command="{DXCommand '@s.Tag.Tag = !@s.Tag.Tag'}" 
                            Tag="{x:Reference themesItem}" 
                            BorderThickness="0"
                            HorizontalAlignment="Stretch"
                            Padding="8,4"/>
                </StackPanel>
            </Grid>
        </dxb:PopupControlContainer>
    </dxb:BarSplitButtonItem.PopupControl>
    
    <dxmvvm:Interaction.Behaviors>
        <dxb:BarSplitItemThemeSelectorBehavior
            UseSvgGlyphs="True"
            ShowSelectedThemeGlyph="True"
            ShowLegacyThemes="{Binding ElementName=themesItem, Path=Tag}"
            ShowTouchThemes="False">
        </dxb:BarSplitItemThemeSelectorBehavior>
    </dxmvvm:Interaction.Behaviors>
</dxb:BarSplitButtonItem>

配置要点

  • ShowLegacyThemes绑定到themesItem.Tag,实现动态显示/隐藏
  • ItemClickCommand关闭Popup,避免选择主题后菜单还开着
  • ColCount="4"设置4列显示,可根据屏幕尺寸调整
  • ShowTouchThemes="False"隐藏触摸优化主题(如果不需要平板支持)

验证方案是否生效

视觉验证

运行程序,打开主题选择器:

预期结果

  • 初始状态显示15个现代主题(Windows 11、Office 2019、VS 2019系列)
  • 点击"显示更多"后,Gallery区域扩展,显示所有154个主题
  • 能看到以下Legacy主题:
    • Metropolis Dark / Metropolis Light
    • Seven / Seven Classic
    • Office 2007 Blue / Black / Silver
    • Office 2010 Blue / Black / Silver
    • Office 2013 / Office 2013 Dark Gray / Office 2013 Light Gray
    • Office 2016 Colorful / Dark Gray / White
    • VS2010 / VS2017 Blue / Dark / Light

如果还是只显示15个

  • 检查UseLightweightThemes是否为false
  • 确认调用了Theme.RegisterPredefinedPaletteThemes()
  • 查看输出窗口是否有主题加载错误

代码验证

OnStartup方法中添加诊断代码:

protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);
    
    Theme.RegisterPredefinedPaletteThemes();
    
    // 诊断:输出所有已加载的主题
    Debug.WriteLine($"总主题数: {Theme.Themes.Count}");
    
    var legacyThemes = Theme.Themes
        .Where(t => t.Name.Contains("Office 2007") || 
                    t.Name.Contains("Metropolis") || 
                    t.Name.Contains("Seven"))
        .Select(t => t.Name);
    
    Debug.WriteLine("Legacy主题列表:");
    foreach (var theme in legacyThemes)
    {
        Debug.WriteLine($"  - {theme}");
    }
}

运行后查看输出窗口,应该能看到类似这样的输出:

总主题数: 154
Legacy主题列表:
  - Office2007Blue
  - Office2007Black
  - Office2007Silver
  - Office2010Blue
  - Office2010Black
  - Office2010Silver
  - MetropolisDark
  - MetropolisLight
  - Seven
  - SevenClassic
  ...

如果Legacy主题列表为空,说明RegisterPredefinedPaletteThemes()没有正确执行。

常见错误与排查技巧

错误一:编译时找不到ThemesLW

完整错误信息

FileNotFoundException: 未能加载文件或程序集"DevExpress.Xpf.ThemesLW.v24.2, 
Version=24.2.3.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a"或它的某一个依赖项。

原因: 代码中还有对ThemesLW的引用,但NuGet包已卸载。

排查步骤

  1. 全局搜索项目中的ThemesLW字符串(Ctrl+Shift+F)
  2. 检查.csprojApp.xamlAssemblyInfo.cs等文件
  3. 查看是否有其他项目依赖了ThemesLW包(在多项目解决方案中常见)

解决方法

  • 删除objbin目录
  • 确认.csproj中没有ThemesLW引用
  • dotnet restore重新还原包
  • 如果是多项目解决方案,确保所有项目都用Themes.All

错误二:主题显示乱码或图标不显示

现象: 主题选择器打开后,主题名称显示为乱码,或者预览图标不显示。

原因: 缺少SVG Glyphs支持,或者图标资源文件损坏。

解决方法

  1. 确认设置了UseSvgGlyphs="True"
  2. 检查是否安装了DevExpress.Wpf.Core包(包含图标资源)
  3. 如果还不行,改用普通图标:
    <dxb:BarSplitItemThemeSelectorBehavior
        UseSvgGlyphs="False"
        ShowSelectedThemeGlyph="True">
    

错误三:切换主题后程序崩溃

完整错误信息

InvalidOperationException: 此操作要求在主题更改期间重新应用模板

原因: 某些自定义控件在主题切换时没有正确更新。

解决方法: 在App.xaml.cs中添加主题切换处理:

public App()
{
    InitializeComponent();
    
    // 订阅主题变更事件
    ApplicationThemeHelper.ApplicationThemeChanged += (s, e) =>
    {
        // 强制所有窗口刷新
        foreach (Window window in Application.Current.Windows)
        {
            window.UpdateLayout();
        }
    };
}

错误四:"显示更多"按钮点击无反应

现象: 点击"显示更多"按钮,主题数量没有变化。

排查

  1. 确认ShowLegacyThemes正确绑定到Tag:
    ShowLegacyThemes="{Binding ElementName=themesItem, Path=Tag}"
    
  2. 检查themesItem的Tag初始值是否为Boolean类型
  3. 用Snoop工具查看运行时绑定是否生效

调试技巧: 在按钮的Command中添加输出验证:

<Button Content="{DXBinding '@s.Tag.Tag ? `显示更少` : `显示更多`'}"  
        Command="{DXCommand Execute='@s.Tag.Tag = !@s.Tag.Tag; 
                                    System.Diagnostics.Debug.WriteLine(\"Tag changed to: \" + @s.Tag.Tag)'}" 
        Tag="{x:Reference themesItem}"/>

性能优化与最佳实践

主题加载性能优化

加载154个主题会增加程序启动时间。如果对启动速度敏感,可以考虑延迟加载:

public partial class App : Application
{
    static App()
    {
        CompatibilitySettings.UseLightweightThemes = false;
    }

    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        
        // 不立即注册所有主题,等用户打开主题选择器时再加载
        // Theme.RegisterPredefinedPaletteThemes(); // 注释掉
        
        // 先加载常用主题
        LoadCommonThemes();
    }
    
    private void LoadCommonThemes()
    {
        // 只注册最常用的几个Legacy主题
        Theme.RegisterTheme(Theme.MetropolisDark);
        Theme.RegisterTheme(Theme.MetropolisLight);
        Theme.RegisterTheme(Theme.Office2016Colorful);
    }
}

然后在主题选择器第一次打开时,再注册全部主题:

private bool allThemesLoaded = false;

private void ThemesItem_PopupOpening(object sender, EventArgs e)
{
    if (!allThemesLoaded)
    {
        Theme.RegisterPredefinedPaletteThemes();
        allThemesLoaded = true;
    }
}

性能对比(实测数据,仅供参考):

  • 启动时加载所有主题:程序启动耗时增加约200-300ms
  • 延迟加载:首次打开主题选择器增加约100-150ms延迟,但启动速度不受影响

主题选择器UI优化

建议一:分组显示主题

将154个主题分组,方便用户查找:

// 在ViewModel中对主题进行分组
public class ThemeSelectorViewModel
{
    public ObservableCollection<ThemeGroup> ThemeGroups { get; set; }
    
    public ThemeSelectorViewModel()
    {
        ThemeGroups = new ObservableCollection<ThemeGroup>
        {
            new ThemeGroup 
            { 
                Name = "现代主题", 
                Themes = Theme.Themes.Where(t => t.Name.Contains("Windows11") || 
                                                  t.Name.Contains("Office2019")).ToList() 
            },
            new ThemeGroup 
            { 
                Name = "经典主题", 
                Themes = Theme.Themes.Where(t => t.Name.Contains("Office20") && 
                                                  !t.Name.Contains("2019")).ToList() 
            },
            // 更多分组...
        };
    }
}

建议二:记住用户上次选择

// 保存选择到本地配置
private void SaveThemePreference(string themeName)
{
    Settings.Default.LastSelectedTheme = themeName;
    Settings.Default.Save();
}

// 启动时恢复
protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);
    Theme.RegisterPredefinedPaletteThemes();
    
    var savedTheme = Settings.Default.LastSelectedTheme;
    if (!string.IsNullOrEmpty(savedTheme))
    {
        ApplicationThemeHelper.ApplicationThemeName = savedTheme;
    }
}

兼容性最佳实践

平滑迁移策略

如果你的项目已经在使用ThemesLW包,并且有生产用户,不要直接强推完整包的更新。建议:

  1. 提供配置选项,让用户选择:

    <configuration>
      <appSettings>
        <add key="EnableAllThemes" value="false"/>
      </appSettings>
    </configuration>
    
  2. 双版本发布

    • 标准版:继续使用ThemesLW(体积小)
    • 完整版:使用Themes.All(功能全)
  3. 逐步迁移

    • 第一阶段:新用户默认用完整包
    • 第二阶段:提示老用户升级
    • 第三阶段:统一使用完整包

写在最后

DevExpress的主题系统确实很强大,但配置项繁多,官方文档又分散在各处,经常让人摸不着头脑。这次解决主题选择器问题,前后花了大半天时间,翻了十几页文档,甚至反编译了DevExpress的DLL查看源码。

最后发现,问题的核心就是三点:

  1. 包要用对(Themes.All vs ThemesLW)
  2. 配置要改对(UseLightweightThemes = false)
  3. 注册要调对(RegisterPredefinedPaletteThemes)

编程那点事 更专业 更方便

登录

找回密码

注册