c# – 在WinForms中托管时管理WPF主题

我正在用
WPF替换旧的WinForms应用程序的部分,希望最终能够使用它来实现MVVM范例.

这项工作带给我的一个主要问题是在整个应用程序中保持WinForms组件使用的动态主题.这些主要是旧的2007年和2010年Office主题的集合.

我的计划是构建一个启动WPF应用程序的单例对象,添加一个资源字典,其中包含用于各种控件的颜色的DynamicResource连接,然后动态交换另一个实际包含颜色定义的资源字典,因为托管WinForms应用程序更改其主题.

只要在WPF窗口中托管WPF,这就完美地工作.如果WPF托管在WinForms容器中,则资源字典肯定会被换出,但视图不会刷新.我知道这一点,因为一旦我鼠标悬停在视图上的按钮上,它的颜色就会更新.

我最近将代码拆分成一个独立的解决方案来尝试测试它,所以我会在这里添加它们.此示例代码是一个独立的测试,在一个简单的WinForms项目中更改主题一次:

UserControlResourceDictionary.xaml

<SolidColorBrush x:Key="WhiteBrush" Color="White" />

<!--Region Containers-->

<Style TargetType="{x:Type UserControl}">
    <Setter Property="Background" Value="{DynamicResource DefaultBackgroundBrush}"/>
</Style>

<Style TargetType="{x:Type Panel}">
    <Setter Property="Background" Value="{DynamicResource DefaultBackgroundBrush}"/>
</Style>

<Style TargetType="{x:Type Grid}" BasedOn="{StaticResource {x:Type Panel}}"/>
<Style TargetType="{x:Type StackPanel}" BasedOn="{StaticResource {x:Type Panel}}"/>

<!--End Region Containers-->

<!--Region TextBox-->

<Style TargetType="{x:Type TextBox}">
    <Setter Property="FontSize" Value="11" />
    <Setter Property="FontWeight" Value="Normal" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type TextBox}">
                <Border BorderThickness="1"
                        BorderBrush="{DynamicResource TextBoxBorderBrush}"
                        Background="{DynamicResource TextBoxBackgroundBrush}"
                        x:Name="Border">
                    <ScrollViewer x:Name="PART_ContentHost" />
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsMouseOver" Value="true">
                        <Setter TargetName="Border" Property="Background"
                                Value="{DynamicResource TextBoxMouseOverBrush}" />
                    </Trigger>
                    <Trigger Property="IsKeyboardFocusWithin" Value="True">
                        <Setter TargetName="Border" Property="Background" Value="{DynamicResource TextBoxKeyboardFocusBrush}" />
                    </Trigger>
                    <Trigger Property="IsEnabled" Value="False">
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<!--End Region TextBox-->

<!--Region Button-->

<Style TargetType="{x:Type Button}">
    <Setter Property="SnapsToDevicePixels" Value="True" />
    <Setter Property="OverridesDefaultStyle" Value="True" />
    <Setter Property="Foreground" Value="{DynamicResource FontColorBrush}" />
    <Setter Property="FontSize" Value="11" />
    <Setter Property="Width" Value="90" />
    <Setter Property="Height" Value="25" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type Button}">
                <Border CornerRadius="{DynamicResource ButtonCornerRadius}"
                        BorderThickness="1"
                        BorderBrush="{DynamicResource DefaultButtonBorderBrush}"
                        Background="{DynamicResource DefaultButtonBrush}"
                        x:Name="Border">
                    <ContentPresenter Margin="2"
                                      HorizontalAlignment="Center"
                                      VerticalAlignment="Center"
                                      RecognizesAccessKey="True" />
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsMouseOver" Value="true">
                        <Setter TargetName="Border" Property="Background"
                                Value="{DynamicResource DefaultMouseOverBrush}" />
                    </Trigger>
                    <Trigger Property="IsPressed" Value="True">
                        <Setter TargetName="Border" Property="Background" Value="{DynamicResource ButtonPressBrush}" />
                        <Setter TargetName="Border" Property="BorderBrush" Value="{DynamicResource ButtonPressBorderBrush}" />
                    </Trigger>
                    <Trigger Property="IsDefaulted" Value="True">
                        <Setter TargetName="Border" Property="BorderBrush" Value="Black" />
                    </Trigger>
                    <Trigger Property="IsEnabled" Value="False">
                        <Setter TargetName="Border" Property="Background" Value="{DynamicResource DefaultDisabledBrush}" />
                        <Setter TargetName="Border" Property="BorderBrush"
                                Value="{DynamicResource DisabledBorderBrush}" />
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<!--End Region Button-->

Office2007BlackStyle.xaml

<!--Region Colors-->

<Color x:Key="ButtonLight">
    #EDEEF0
</Color>

<Color x:Key="ButtonDark">
    #BBC0C6
</Color>

<Color x:Key="ButtonDisableLight">
    #F3F6F8
</Color>

<Color x:Key="ButtonDisableDark">
    #CBD5DF
</Color>

<Color x:Key="ButtonPressLight">
    #F4BC81
</Color>

<Color x:Key="ButtonPressDark">
    #EB7A05
</Color>

<Color x:Key="ButtonMouseOverLight">
    #FBEDBD
</Color>

<Color x:Key="ButtonMouseOverDark">
    #F4B100
</Color>

<Color x:Key="DefaultButtonBorderColor">
    #898785
</Color>

<Color x:Key="TextBoxBorderColor">
    #ABC1DE
</Color>

<Color x:Key="DisabledBorderColor">
    #A1BDCF
</Color>

<Color x:Key="ButtonPressBorderColor">
    #9B8259
</Color>

<Color x:Key="FontColor">
    #464646
</Color>

<Color x:Key="BackgroundColor">
    #535353
</Color>

<Color x:Key="GroupBoxColor">
    #1E1E1E
</Color>

<!--End Region Colors-->

<CornerRadius x:Key="ButtonCornerRadius">
    2
</CornerRadius>


<!--Region Brushes-->

<SolidColorBrush x:Key="DefaultButtonBorderBrush" Color="{DynamicResource DefaultButtonBorderColor}" />
<SolidColorBrush x:Key="TextBoxBorderBrush" Color="{DynamicResource TextBoxBorderColor}"/>
<SolidColorBrush x:Key="DisabledBorderBrush" Color="{DynamicResource DisabledBorderColor}" />
<SolidColorBrush x:Key="DefaultLabelBrush" Color="{DynamicResource ButtonLight}" />
<SolidColorBrush x:Key="ButtonPressBorderBrush" Color="{DynamicResource ButtonPressBorderColor}"/>
<SolidColorBrush x:Key="FontColorBrush" Color="{DynamicResource FontColor}"/>
<SolidColorBrush x:Key="DefaultBackgroundBrush" Color="{DynamicResource BackgroundColor}"/>
<SolidColorBrush x:Key="GroupBoxColorBrush" Color="{DynamicResource GroupBoxColor}"/>
<SolidColorBrush x:Key="TextBoxBackgroundBrush" Color="White"/>
<SolidColorBrush x:Key="TextBoxMouseOverBrush" Color="White"/>
<SolidColorBrush x:Key="TextBoxKeyboardFocusBrush" Color="White"/>

<LinearGradientBrush x:Key="DefaultButtonBrush" StartPoint="0,0" EndPoint="0,1.0">
    <GradientStop Color="{DynamicResource ButtonLight}" Offset="0" />
    <GradientStop Color="{DynamicResource ButtonDark}" Offset=".5" />
    <GradientStop Color="{DynamicResource ButtonLight}" Offset="1" />
</LinearGradientBrush>

<LinearGradientBrush x:Key="DefaultDisabledBrush" StartPoint="0,0" EndPoint="0,1.0">
    <GradientStop Color="{DynamicResource ButtonDisableLight}" Offset="0" />
    <GradientStop Color="{DynamicResource ButtonDisableDark}" Offset=".5" />
    <GradientStop Color="{DynamicResource ButtonDisableLight}" Offset="1" />
</LinearGradientBrush>

<LinearGradientBrush x:Key="ButtonPressBrush" StartPoint="0,0" EndPoint="0,1.0">
    <GradientStop Color="{DynamicResource ButtonPressLight}" Offset="0" />
    <GradientStop Color="{DynamicResource ButtonPressDark}" Offset=".5" />
    <GradientStop Color="{DynamicResource ButtonPressLight}" Offset="1" />
</LinearGradientBrush>

<LinearGradientBrush x:Key="DefaultMouseOverBrush" StartPoint="0,0" EndPoint="0,1.0">
    <GradientStop Color="{DynamicResource ButtonMouseOverLight}" Offset="0" />
    <GradientStop Color="{DynamicResource ButtonMouseOverDark}" Offset=".5" />
    <GradientStop Color="{DynamicResource ButtonMouseOverLight}" Offset="1" />
</LinearGradientBrush>

<!--End Region Brushes-->

AppHost.cs

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Threading;

namespace EmbeddedWPFTest
{
    public static class AppHost
    {
        private static readonly object AppLock = Guid.NewGuid();
        private static Application _application;
        private static ResourceDictionary _currentTheme;
        private static ResourceDictionary _controlDictionary;
        private static ResourceDictionary _resourceDictionary;
        private static Dictionary<string, ResourceDictionary> _themes;

        public static Dispatcher Dispatcher { get; set; }

        public static Application CurrentApplication
        {
            get
            {
                lock (AppLock)
                {
                    if (_application == null)
                    {
                        _application = new Application();

                        LoadDictionaries();
                        InitializeApplication();

                   }
                }

                return _application;
            }
        }

        private static void InitializeApplication()
        {
            Application.Current.Resources.MergedDictionaries.Add(_resourceDictionary);
            Application.Current.Resources.MergedDictionaries.Add(_controlDictionary);
            _currentTheme = Application.LoadComponent(
        new Uri(@"/EmbeddedWPFTest;component/Resources/Office2007BlueStyle.xaml",
            UriKind.Relative))
    as ResourceDictionary;
            Application.Current.Resources.MergedDictionaries.Add(_currentTheme);
        }

        public static void ChangeTheme()
        {
            Application.Current.Resources.MergedDictionaries.Remove(_currentTheme);
            InitializeApplication();


                    _currentTheme = Application.LoadComponent(
                            new Uri(@"/EmbeddedWPFTest;component/Resources/Office2007BlackStyle.xaml",
                                UriKind.Relative))
                        as ResourceDictionary;
                    Application.Current.Resources.MergedDictionaries.Add(_currentTheme);


        }

        private static void LoadDictionaries()
        {
            _resourceDictionary =
                Application.LoadComponent(new Uri(@"/EmbeddedWPFTest;component/Resources/ResourceDictionary.xaml",
                    UriKind.Relative)) as ResourceDictionary;
            _controlDictionary =
                Application.LoadComponent(new Uri(@"/EmbeddedWPFTest;component/Resources/UserControlResourceDictionary.xaml",
                    UriKind.Relative)) as ResourceDictionary;
            _themes = new Dictionary<string, ResourceDictionary>
            {
                {
                    "Office2007BlueStyle",
                    Application.LoadComponent(
                            new Uri(@"/EmbeddedWPFTest;component/Resources/Office2007BlueStyle.xaml",
                                UriKind.Relative))
                        as ResourceDictionary
                },
                {
                    "Office2007BlackStyle",
                    Application.LoadComponent(
                            new Uri(@"/EmbeddedWPFTest;component/Resources/Office2007BlackStyle.xaml",
                                UriKind.Relative))
                        as ResourceDictionary
                }
            };
        }
    }
}

我的目标是,当托管WinForms应用程序更改其主题时,我可以连接到该事件,并让AppHost更改为相应的资源字典.为简单起见,我遗漏了部分代码.在我正在运行的测试中,它是在视图中按下按钮时发生的.同样,如果视图托管在WPF容器中,它可以正常工作,但是在WinForms容器中托管,它不会刷新或重绘.

最佳答案 我尝试了在互联网上找到的所有解决方案.我能让它工作的唯一方法是创建视图的新副本.这不是最优雅的解决方案,但我认为只要为每个视图保留底层视图模型的相同实例,它就不是一个糟糕的解决方案.

现在,我正在使用MVVMLight来更新此代码库. MVVMLight带有一个漂亮的messenger实用程序,可以方便模块之间的异步通信.我决定在我的AppHost类和WinForms主机之间进行通信.

新的AppHost.cs

public static class AppHost
    {
        private static readonly object AppLock = Guid.NewGuid();
        private static Application _application;
        private static ResourceDictionary _currentTheme;
        private static ResourceDictionary _controlDictionary;
        private static ResourceDictionary _resourceDictionary;
        private static Dictionary<string, ResourceDictionary> _themes;
        private static KryptonManager _kryptonManager;
        private static IMessenger _messengerInstance;

        /// <summary>
        /// Gets or sets an instance of a <see cref="IMessenger" /> used to
        /// broadcast messages to other objects. If null, this class will
        /// attempt to broadcast using the Messenger's default instance.
        /// </summary>
        private static IMessenger MessengerInstance
        {
            get
            {
                return _messengerInstance ?? Messenger.Default;
            }
            set
            {
                _messengerInstance = value;
            }
        }

        public static Application CurrentApplication
        {
            get
            {
                lock (AppLock)
                {
                    if (_application == null)
                    {
                        _application = new Application();

                        LoadDictionaries();
                        InitializeApplication();
                        ChangeTheme(PaletteModeManager.Custom);

                        KryptonManager.GlobalPaletteChanged += KryptonManagerGlobalPaletteChanged;
                        _kryptonManager = new KryptonManager();
                    }
                }

                return _application;
            }
        }

        private static void KryptonManagerGlobalPaletteChanged(object sender, EventArgs e)
        {
            ChangeTheme(_kryptonManager.GlobalPaletteMode);
        }

        private static void InitializeApplication()
        {
            Application.Current.Resources.MergedDictionaries.Add(_resourceDictionary);
            Application.Current.Resources.MergedDictionaries.Add(_controlDictionary);
        }

        public static void ChangeTheme(PaletteModeManager manager)
        {
            Application.Current.Resources.MergedDictionaries.Remove(_currentTheme);

            switch (manager)
            {
                case PaletteModeManager.Office2007Blue:
                    _currentTheme = _themes["Office2007BlueStyle"];
                    Application.Current.Resources.MergedDictionaries.Add(_currentTheme);
                    break;
                case PaletteModeManager.Office2007Black:
                    _currentTheme = _themes["Office2007BlackStyle"];
                    Application.Current.Resources.MergedDictionaries.Add(_currentTheme);
                    break;
                default:
                    _currentTheme = _themes["Office2007BlueStyle"];
                    Application.Current.Resources.MergedDictionaries.Add(_currentTheme);
                    break;
            }

            MessengerInstance.Send(new ThemeChangedMessage());
        }

        private static void LoadDictionaries()
        {
            _resourceDictionary =
                Application.LoadComponent(new Uri(@"/ApplicationHost;component/Resources/ResourceDictionary.xaml",
                    UriKind.Relative)) as ResourceDictionary;
            _controlDictionary =
                Application.LoadComponent(new Uri(@"/ApplicationHost;component/Resources/UserControlResourceDictionary.xaml",
                    UriKind.Relative)) as ResourceDictionary;
            _themes = new Dictionary<string, ResourceDictionary>
            {
                {
                    "Office2007BlueStyle",
                    Application.LoadComponent(
                            new Uri(@"/ApplicationHost;component/Resources/Office2007BlueStyle.xaml",
                                UriKind.Relative))
                        as ResourceDictionary
                },
                {
                    "Office2007BlackStyle",
                    Application.LoadComponent(
                            new Uri(@"/ApplicationHost;component/Resources/Office2007BlackStyle.xaml",
                                UriKind.Relative))
                        as ResourceDictionary
                }
            };
        }
    }

所以这里,当AppHost从WinForms主机接收通知(在这个实例中是Krypton)时,它会换出正确的颜色资源字典,然后使用MVVMLight的消息传递功能发出异步消息,以通知侦听器主题已更改.

BaseFormsWrapper.cs

public class BaseFormsWrapper : UserControl
{
    public Panel PanelBasePanel;
    private ElementHost _wpfHost;
    private IMessenger _messengerInstance;

    public BaseFormsWrapper()
    {
        InitializeComponent();
        MessengerInstance.Register<ThemeChangedMessage>(this, HandleThemeChanged);
    }

    private void HandleThemeChanged(ThemeChangedMessage obj)
    {
        var instance = Activator.CreateInstance(_wpfHost.Child.GetType());
        var oldView = _wpfHost.Child;
        _wpfHost.Child = (UIElement) instance;

        var view = oldView as ViewBase;
        var newView = instance as ViewBase;

        if ((view != null) && (newView != null))
        {
            newView.DataContext = view.DataContext;
        }
    }

    /// <summary>
    /// Gets or sets an instance of a <see cref="IMessenger" /> used to
    /// broadcast messages to other objects. If null, this class will
    /// attempt to broadcast using the Messenger's default instance.
    /// </summary>
    private IMessenger MessengerInstance
    {
        get
        {
            return _messengerInstance ?? Messenger.Default;
        }
        set
        {
            _messengerInstance = value;
        }
    }

    public UIElement HostedControl
    {
        get { return _wpfHost.Child; }
        set { _wpfHost.Child = value; }
    }

    private void InitializeComponent()
    {
        PanelBasePanel = new Panel();
        _wpfHost = new ElementHost();
        PanelBasePanel.SuspendLayout();
        SuspendLayout();
        // 
        // panelBasePanel
        // 
        PanelBasePanel.AutoSizeMode = AutoSizeMode.GrowAndShrink;
        PanelBasePanel.Controls.Add(_wpfHost);
        PanelBasePanel.Dock = DockStyle.Fill;
        PanelBasePanel.Location = new Point(0, 0);
        PanelBasePanel.Margin = new Padding(0);
        PanelBasePanel.Name = "PanelBasePanel";
        PanelBasePanel.Size = new Size(1126, 388);
        PanelBasePanel.TabIndex = 0;
        // 
        // wpfHost
        // 
        _wpfHost.BackColor = SystemColors.ControlLightLight;
        _wpfHost.BackgroundImageLayout = ImageLayout.None;
        _wpfHost.Dock = DockStyle.Fill;
        _wpfHost.Location = new Point(0, 0);
        _wpfHost.Name = "_wpfHost";
        _wpfHost.Size = new Size(1126, 388);
        _wpfHost.TabIndex = 0;
        _wpfHost.Child = null;
        // 
        // BaseFormsWrapper
        // 
        AutoScaleDimensions = new SizeF(6F, 13F);
        AutoSize = true;
        Controls.Add(PanelBasePanel);
        Name = "BaseFormsWrapper";
        Size = new Size(1126, 388);
        PanelBasePanel.ResumeLayout(false);
        ResumeLayout(false);
    }
}

在让GC处理旧视图实例之前,我通过将其填充到新视图实例中来保留其DataContext(视图模型).

我仍在研究我的概念验证工作项目,以确保所有这些工作在完成步骤时,但在我的初始测试中似乎是活泼和实用的.

点赞