none
Windowsのテーマの反映による、コントロールの外観の切り替わりについて RRS feed

  • 質問

  • WPFのButtonコントロールなどは、Windowsのテーマ(AeroやClassic)を切り替えることによって、そのButtonコントロールの外観もテーマに合わせて変わります。おそらくButton.Templateが切り替わっているのだと思いますが、どのような仕組みになっているのでしょうか?
    そして、WindowsがAeroのときは、独自作成したTemplateAを適用、Classicのときは元々のままというのはできないでしょうか?

    2017年6月13日 10:01

回答

  • リソースは Window.Resourcesにスタイルが無ければ.App.Current.Resourcesでスタイルを探すというように、上流へむかって探索が行われます。
    そして、たとえば何も変更していない初期のプロジェクトでButtonのスタイルを

    var buttonStyle=App.Current.TryFindResource(typeof(System.Windows.Controls.Button)) as Style;

    のように取得すると、なにも定義してないのにスタイルが存在します。
    つまり、App.Current.Resourcesよりもさらに上流でスタイルが定義されていることがわかります。
    おそらくですが、そのWPFのシステムリソースのようなものがテーマに反応してスタイルを切り替えているのだと思われます。
    (テーマを切り替えるとStyleのHashCodeが変わるのでStyleが切り替わっていることは判ります

    そんな感じで開発者側でそのシステムリソースは変更できないので、その下位であるApp.Current.Resourceあたりでスタイルを定義してやれば、スタイルの上書きのようなことができることになります。

    Aero用のControlTemplateをまとめてリソースディクショナリに定義しておいて、Aeroの時だけそのリソースディクショナリを読み込んでみる

    <!-- App.xaml -->
    <Application x:Class="WpfApplication1.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 StartupUri="MainWindow.xaml">
        <Application.Resources>
        </Application.Resources>
    </Application>
    
    
    <!-- Dictionary1.xaml -->
    <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
        <Style TargetType="{x:Type Button}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type Button}">
                        <Grid>
                            <Ellipse HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Fill="{TemplateBinding Background}"
                                     Stroke="{TemplateBinding BorderBrush}" StrokeThickness="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},Path=BorderThickness.Left}">
                            </Ellipse>
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </ResourceDictionary>
        
    
    <!-- MainWindow.xaml -->
    <Window x:Class="WpfApplication1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="MainWindow" Height="350" Width="525">
        <Grid>
            <Button Width="100" Height="50"  />
        </Grid>
    </Window>


    using System;
    using System.Windows;
    
    namespace WpfApplication1
    {
        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
    
                //テーマの変更などを検出するイベント
                Microsoft.Win32.SystemEvents.UserPreferenceChanging += SystemEvents_UserPreferenceChanging;
    
                //現在のテーマ名を取得
                string themeName = System.Windows.Forms.VisualStyles.VisualStyleInformation.DisplayName;
                this.Title = themeName;
    
                UpdateTheme();
            }
    
    
            void SystemEvents_UserPreferenceChanging(object sender, Microsoft.Win32.UserPreferenceChangingEventArgs e)
            {
                if (e.Category == Microsoft.Win32.UserPreferenceCategory.VisualStyle)
                {
                    //テーマが変更された
                    UpdateTheme();
                }
            }
    
            //テーマに対応したControlTemplateの定義してあるリソースディクショナリ
            private static ResourceDictionary forAero;
    
            private static void UpdateTheme()
            {
                string themeName = System.Windows.Forms.VisualStyles.VisualStyleInformation.DisplayName;
                if (themeName == "Aero style")
                {
                    if (forAero == null)
                    {
                        forAero = new ResourceDictionary();
                        forAero.Source = GetResourceDictionaryUri("Dictionary1.xaml");
                    }
    
                    //Aero用の定義してあるリソースディクショナリをアプリケーションの大元のリソースに読み込んで適用する
                    App.Current.Resources.MergedDictionaries.Add(forAero);
                }
                else if (forAero != null)
                {
                    App.Current.Resources.MergedDictionaries.Remove(forAero);//適用したResourceDicrionaryを消す             
                }
            }
    
            /// <summary>読み込みされていないリソースディクショナリを読み込むためのUri</summary>
            /// <param name="xamlFileName">ビルド前のxamlファイル名</param>
            /// <returns>Uri</returns>
            private static Uri GetResourceDictionaryUri(string xamlFileName)
            {
                return new Uri(typeof(App).Namespace + ";component/" + xamlFileName, UriKind.Relative);
            }
    
        }
    }

    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    2017年6月13日 11:38

すべての返信

  • リソースは Window.Resourcesにスタイルが無ければ.App.Current.Resourcesでスタイルを探すというように、上流へむかって探索が行われます。
    そして、たとえば何も変更していない初期のプロジェクトでButtonのスタイルを

    var buttonStyle=App.Current.TryFindResource(typeof(System.Windows.Controls.Button)) as Style;

    のように取得すると、なにも定義してないのにスタイルが存在します。
    つまり、App.Current.Resourcesよりもさらに上流でスタイルが定義されていることがわかります。
    おそらくですが、そのWPFのシステムリソースのようなものがテーマに反応してスタイルを切り替えているのだと思われます。
    (テーマを切り替えるとStyleのHashCodeが変わるのでStyleが切り替わっていることは判ります

    そんな感じで開発者側でそのシステムリソースは変更できないので、その下位であるApp.Current.Resourceあたりでスタイルを定義してやれば、スタイルの上書きのようなことができることになります。

    Aero用のControlTemplateをまとめてリソースディクショナリに定義しておいて、Aeroの時だけそのリソースディクショナリを読み込んでみる

    <!-- App.xaml -->
    <Application x:Class="WpfApplication1.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 StartupUri="MainWindow.xaml">
        <Application.Resources>
        </Application.Resources>
    </Application>
    
    
    <!-- Dictionary1.xaml -->
    <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
        <Style TargetType="{x:Type Button}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type Button}">
                        <Grid>
                            <Ellipse HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Fill="{TemplateBinding Background}"
                                     Stroke="{TemplateBinding BorderBrush}" StrokeThickness="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},Path=BorderThickness.Left}">
                            </Ellipse>
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </ResourceDictionary>
        
    
    <!-- MainWindow.xaml -->
    <Window x:Class="WpfApplication1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="MainWindow" Height="350" Width="525">
        <Grid>
            <Button Width="100" Height="50"  />
        </Grid>
    </Window>


    using System;
    using System.Windows;
    
    namespace WpfApplication1
    {
        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
    
                //テーマの変更などを検出するイベント
                Microsoft.Win32.SystemEvents.UserPreferenceChanging += SystemEvents_UserPreferenceChanging;
    
                //現在のテーマ名を取得
                string themeName = System.Windows.Forms.VisualStyles.VisualStyleInformation.DisplayName;
                this.Title = themeName;
    
                UpdateTheme();
            }
    
    
            void SystemEvents_UserPreferenceChanging(object sender, Microsoft.Win32.UserPreferenceChangingEventArgs e)
            {
                if (e.Category == Microsoft.Win32.UserPreferenceCategory.VisualStyle)
                {
                    //テーマが変更された
                    UpdateTheme();
                }
            }
    
            //テーマに対応したControlTemplateの定義してあるリソースディクショナリ
            private static ResourceDictionary forAero;
    
            private static void UpdateTheme()
            {
                string themeName = System.Windows.Forms.VisualStyles.VisualStyleInformation.DisplayName;
                if (themeName == "Aero style")
                {
                    if (forAero == null)
                    {
                        forAero = new ResourceDictionary();
                        forAero.Source = GetResourceDictionaryUri("Dictionary1.xaml");
                    }
    
                    //Aero用の定義してあるリソースディクショナリをアプリケーションの大元のリソースに読み込んで適用する
                    App.Current.Resources.MergedDictionaries.Add(forAero);
                }
                else if (forAero != null)
                {
                    App.Current.Resources.MergedDictionaries.Remove(forAero);//適用したResourceDicrionaryを消す             
                }
            }
    
            /// <summary>読み込みされていないリソースディクショナリを読み込むためのUri</summary>
            /// <param name="xamlFileName">ビルド前のxamlファイル名</param>
            /// <returns>Uri</returns>
            private static Uri GetResourceDictionaryUri(string xamlFileName)
            {
                return new Uri(typeof(App).Namespace + ";component/" + xamlFileName, UriKind.Relative);
            }
    
        }
    }

    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    2017年6月13日 11:38
  • gekkaさん

    ありがとうございます。とてもわかりやすいです。

    そういう仕組みだったんですね、参考にいたします。

    2017年6月14日 0:32