VisualStateGroupが一つだけなら同じVisualStateGroup内のVisualStateは排他なので結果を見て判断することも難しくないですが、VisualStateGroupが複数になると異なるVisualStateGroup間のVisualStateは併存するので、複数のVisualStateの効果が重なることになり、何がどうなっているか判断に迷うことがあります。というか、かなり迷いました。
そこで、デバッグ用に現在のVisualStateを出力する添付プロパティを書いてみました。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Diagnostics; | |
using System.Linq; | |
using System.Threading.Tasks; | |
using System.Windows; | |
using System.Windows.Media; | |
public class VisualStateMonitor : DependencyObject | |
{ | |
public static int GetInterval(DependencyObject obj) | |
{ | |
return (int)obj.GetValue(IntervalProperty); | |
} | |
public static void SetInterval(DependencyObject obj, int value) | |
{ | |
obj.SetValue(IntervalProperty, value); | |
} | |
public static readonly DependencyProperty IntervalProperty = | |
DependencyProperty.RegisterAttached( | |
"Interval", | |
typeof(int), | |
typeof(VisualStateMonitor), | |
new FrameworkPropertyMetadata(0, OnIntervalChanged)); | |
private static void OnIntervalChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) | |
{ | |
var element = d as FrameworkElement; | |
if (element == null) | |
return; | |
var interval = (int)e.NewValue; | |
if (interval <= 0) | |
return; | |
Task.Run(async () => | |
{ | |
while (true) | |
{ | |
element.Dispatcher.Invoke(() => CheckVisualState(element)); | |
await Task.Delay(TimeSpan.FromSeconds(interval)); | |
} | |
}); | |
} | |
private static void CheckVisualState(FrameworkElement element) | |
{ | |
var groups = GetVisualStateGroups(element); | |
if (groups != null) | |
{ | |
foreach (var group in groups) | |
Debug.WriteLine($"Element: {element.Name} -> Group: {group.Name} -> State: {group.CurrentState?.Name}"); | |
} | |
} | |
private static IEnumerable<VisualStateGroup> GetVisualStateGroups(FrameworkElement element) | |
{ | |
if (VisualTreeHelper.GetChildrenCount(element) <= 0) // If the ControlTemplate has not been applied yet | |
return null; | |
foreach (var descendant in GetDescendants(element).OfType<FrameworkElement>()) | |
{ | |
var groups = VisualStateManager.GetVisualStateGroups(descendant)?.Cast<VisualStateGroup>(); | |
if (groups != null) | |
return groups; | |
} | |
return null; | |
} | |
private static IEnumerable<DependencyObject> GetDescendants(DependencyObject reference) | |
{ | |
if (reference == null) | |
yield break; | |
var queue = new Queue<DependencyObject>(); | |
do | |
{ | |
var parent = (queue.Count == 0) ? reference : queue.Dequeue(); | |
var count = VisualTreeHelper.GetChildrenCount(parent); | |
for (int i = 0; i < count; i++) | |
{ | |
var child = VisualTreeHelper.GetChild(parent, i); | |
queue.Enqueue(child); | |
yield return child; | |
} | |
} | |
while (queue.Count > 0); | |
} | |
} |
VisualStateの変化を捉える方法が見つからなかったのでIntervalで指定された秒ごとに永久ループで出力しますが、そこはあくまでデバッグ用ということで。
これをToggleButtonに使った例。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<Window x:Class="VisualStateTest.MainWindow" | |
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" | |
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" | |
xmlns:local="clr-namespace:VisualStateTest" | |
Title="MainWindow" | |
Height="200" Width="200"> | |
<Window.Resources> | |
<SolidColorBrush x:Key="ToggleButton.Normal.Background" Color="Orchid" Opacity="0.6"/> | |
<Color x:Key="ToggleButton.Selected.BackgroundColor">Gold</Color> | |
<ControlTemplate x:Key="ToggleButtonTemplate" TargetType="{x:Type ToggleButton}"> | |
<Grid> | |
<Border x:Name="border" | |
Background="{StaticResource ToggleButton.Normal.Background}" | |
BorderThickness="1" | |
BorderBrush="Gray"> | |
<ContentPresenter Margin="{TemplateBinding Padding}" | |
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" | |
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" | |
Content="{TemplateBinding Content}"/> | |
</Border> | |
<VisualStateManager.VisualStateGroups> | |
<VisualStateGroup x:Name="CommonStates"> | |
<VisualState x:Name="Normal"/> | |
<VisualState x:Name="MouseOver"> | |
<Storyboard> | |
<DoubleAnimation Storyboard.TargetName="border" | |
Storyboard.TargetProperty="(Button.Background).(SolidColorBrush.Opacity)" | |
To="0.8" | |
Duration="0:0:0.1"/> | |
</Storyboard> | |
</VisualState> | |
<VisualState x:Name="Pressed"> | |
<Storyboard> | |
<DoubleAnimation Storyboard.TargetName="border" | |
Storyboard.TargetProperty="(Button.Background).(SolidColorBrush.Opacity)" | |
To="1.0" | |
Duration="0:0:0.1"/> | |
</Storyboard> | |
</VisualState> | |
<VisualState x:Name="Disabled"/> | |
</VisualStateGroup> | |
<VisualStateGroup x:Name="CheckStates"> | |
<VisualState x:Name="Checked"> | |
<Storyboard> | |
<ColorAnimation Storyboard.TargetName="border" | |
Storyboard.TargetProperty="(Button.Background).(SolidColorBrush.Color)" | |
To="{StaticResource ToggleButton.Selected.BackgroundColor}" | |
Duration="0:0:0.1"/> | |
</Storyboard> | |
</VisualState> | |
<VisualState x:Name="Unchecked"/> | |
<VisualState x:Name="Indeterminate"/> | |
</VisualStateGroup> | |
</VisualStateManager.VisualStateGroups> | |
</Grid> | |
</ControlTemplate> | |
</Window.Resources> | |
<Grid> | |
<ToggleButton x:Name="TestToggleButton" | |
Template="{StaticResource ToggleButtonTemplate}" | |
Content="Test" | |
local:VisualStateMonitor.Interval="3"/> | |
</Grid> | |
</Window> |
// 初期状態 Element: TestToggleButton -> Group: CommonStates -> State: Normal Element: TestToggleButton -> Group: CheckStates -> State: Unchecked // カーソルを上に移動 Element: TestToggleButton -> Group: CommonStates -> State: MouseOver Element: TestToggleButton -> Group: CheckStates -> State: Unchecked // クリック Element: TestToggleButton -> Group: CommonStates -> State: MouseOver Element: TestToggleButton -> Group: CheckStates -> State: Checked // カーソルを上から外す Element: TestToggleButton -> Group: CommonStates -> State: Normal Element: TestToggleButton -> Group: CheckStates -> State: Checked // 再度クリックしてカーソルを外す Element: TestToggleButton -> Group: CommonStates -> State: Normal Element: TestToggleButton -> Group: CheckStates -> State: Uncheckedこれが分かれば簡単になるというものでもないですが、直接確認しながら作業できるのは随分違うと思います。実はもっと簡単にできる方法があるのかもしれませんが、とりあえず。
[追記] 添付プロパティの作成について
添付プロパティの構文の話ですが、依存関係プロパティの場合、PropertyMetadataのPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)のdはそのプロパティが定義されたクラスのインスタンスなので、これをキャストして処理をインスタンスメンバーに繋げることができます。一方、添付プロパティの場合、dはそのプロパティが添付されたFrameworkElementのインスタンスになるので、添付プロパティが定義されたクラスのインスタンスメンバーにそのままアクセスはできません。
したがって、FrameworkElementのインスタンスの処理はおおよそ以下に分かれます。
- 静的メソッドの中で処理を完結させる。デリゲートを駆使すればある程度複雑なこともできるが、状態をフィールドなどに保存できないので、行き届いた状態管理は難しい。
- 静的メソッドの中で、状態を静的フィールドなどに保存しながら処理する。アプリ中の一カ所でしか使われない場合や、静的な値が共有されても構わない場合に限られる。
- 添付プロパティの型をそのプロパティが定義されたクラスにすると、初期化時にe.NewValueにそのクラスのインスタンスが入ってくるので、それをキャストしてインスタンスメンバーに繋げる。WindowChromeで使われている方法で、複雑な処理も行いやすい。
0 コメント :
コメントを投稿