WPF で 3D オブジェクトを回転させる

前回の WPF で 3D オブジェクトを表示するに引き続いて、今回は 3D オブジェクトを回転させます。
図のようにボタンを配置して、6 方向の回転ができるように実装します。

次のようにコードを追加・変更します。

<Window x:Class="DiceRotationWpf.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:DiceRotationWpf"
Title="Dice Rotation" Height="600" Width="900">
<Window.Resources>
<Style x:Key="FaceStyle" TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="White"/>
<Setter Property="Width" Value="30"/>
<Setter Property="Height" Value="30"/>
<Setter Property="TextAlignment" Value="Center"/>
<Setter Property="FontSize" Value="20"/>
</Style>
<Style x:Key="RepeatButtonStyle" TargetType="{x:Type RepeatButton}">
<Setter Property="Width" Value="50"/>
<Setter Property="Height" Value="50"/>
<Setter Property="FontSize" Value="32"/>
</Style>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid x:Name="DicePanel" Background="#FF333333">
<Grid Visibility="Hidden">
<TextBlock x:Name="Face1" Style="{DynamicResource FaceStyle}" Text="1" Background="#FF222222"/>
<TextBlock x:Name="Face2" Style="{DynamicResource FaceStyle}" Text="2" Background="#FFDF2C2C"/>
<TextBlock x:Name="Face3" Style="{DynamicResource FaceStyle}" Text="3" Background="#FFEE9319"/>
<TextBlock x:Name="Face4" Style="{DynamicResource FaceStyle}" Text="4" Background="#FFE3E60A"/>
<TextBlock x:Name="Face5" Style="{DynamicResource FaceStyle}" Text="5" Background="#FF29D214"/>
<TextBlock x:Name="Face6" Style="{DynamicResource FaceStyle}" Text="6" Background="#FF4444BB"/>
</Grid>
<Viewport3D>
<Viewport3D.Camera>
<PerspectiveCamera Position="0,0,10"/>
</Viewport3D.Camera>
<ModelVisual3D>
<ModelVisual3D.Content>
<AmbientLight/>
</ModelVisual3D.Content>
</ModelVisual3D>
<ModelVisual3D>
<ModelVisual3D.Transform>
<Transform3DGroup>
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D Axis="-0.8,0.3,0.5" Angle="60"/>
</RotateTransform3D.Rotation>
</RotateTransform3D>
<MatrixTransform3D x:Name="matrixTransform"/>
</Transform3DGroup>
</ModelVisual3D.Transform>
<!-- ModelVisual3D.Content は省略 -->
</ModelVisual3D>
</Viewport3D>
</Grid>
<Grid Background="#FFF8F8F8" Grid.Column="1" Width="300">
<Canvas Height="230" Width="230">
<RepeatButton Content="" CommandParameter="-x" Style="{DynamicResource RepeatButtonStyle}" Canvas.Left="90" Canvas.Top="20" Click="Rotate_Click"/>
<RepeatButton Content="" CommandParameter="+x" Style="{DynamicResource RepeatButtonStyle}" Canvas.Left="90" Canvas.Top="160" Click="Rotate_Click"/>
<RepeatButton Content="" CommandParameter="-y" Style="{DynamicResource RepeatButtonStyle}" Canvas.Left="20" Canvas.Top="90" Click="Rotate_Click"/>
<RepeatButton Content="" CommandParameter="+y" Style="{DynamicResource RepeatButtonStyle}" Canvas.Left="160" Canvas.Top="90" Click="Rotate_Click"/>
<RepeatButton Content="" CommandParameter="-z" Style="{DynamicResource RepeatButtonStyle}" Canvas.Left="160" Canvas.Top="20" Click="Rotate_Click"/>
<RepeatButton Content="" CommandParameter="+z" Style="{DynamicResource RepeatButtonStyle}" Canvas.Left="20" Canvas.Top="20" Click="Rotate_Click"/>
</Canvas>
</Grid>
</Grid>
</Window>
view raw MainWindow.xaml hosted with ❤ by GitHub
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media.Media3D;
namespace DiceRotationWpf
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
const double AngleDelta = 5.0;
static readonly Dictionary<string, Vector3D> Axes = new Dictionary<string, Vector3D>
{
{ "-x", Vector3D.Parse("-1,0,0") },
{ "+x", Vector3D.Parse("1,0,0") },
{ "-y", Vector3D.Parse("0,-1,0") },
{ "+y", Vector3D.Parse("0,1,0") },
{ "-z", Vector3D.Parse("0,0,-1") },
{ "+z", Vector3D.Parse("0,0,1") },
};
void Rotate_Click(object sender, RoutedEventArgs e)
{
var button = (RepeatButton)sender;
var command = (string)button.CommandParameter;
matrixTransform.Rotate(Axes[command], AngleDelta);
}
}
public static class Media3DUtility
{
public static void Rotate(this MatrixTransform3D transform, Vector3D axis, double angle)
{
var matrix = transform.Matrix;
matrix.Rotate(new Quaternion(axis, angle));
transform.Matrix = matrix;
}
}
}
view raw MainWindow.xaml.cs hosted with ❤ by GitHub

ボタンとして RepeatButton を配置しています。
RepeatButton は、押したままにしておけば断続的に Click イベントが発生します。
また回転の状態を表すために、ModelVisual3D.Transform の中で MatrixTransform3D を使います。

回転には、回転軸と回転角度が必要です。

  • 回転軸は、ベクトルで表されます。
  • 回転角度は、回転軸の方向に右ねじを押し込む場合を正とします。

6 つのボタンはそれぞれ、x, y, z 軸を回転軸とした正方向または負方向の回転を表します。
1 回の Click イベントにつき 5° ずつ回転させています。
なお、カメラは z 軸上の正の位置から原点方向を見下ろし、右側が x 軸の正、上側が y 軸の正を表しています。

Click イベントハンドラーの中で、Matrix3D.Rotate メソッドを呼び出すことでオブジェクトを回転させます。
引数には、回転軸と回転角度を表す Quaternion (四元数) を指定します。
このように、回転軸と回転角度がわかっている場合は比較的簡単に実装ができます。

下図は、最初の状態から x 軸のまわりに -60° 回転させたところです。

全体のソースコードは DiceRotationWpf (GitHub) にあります。
マウスまたはタッチのドラッグ操作でも回転できるようになっています。

Dice Rotation

 

前回: WPF で 3D オブジェクトを表示する
次回: 3D における回転の表現と相互変換

作成したサンプル
DiceRotationWpf (GitHub)

バージョン情報
.NET Framework 4.5

参照
3-D グラフィックスの概要
3-D 変換の概要

WPF で 3D オブジェクトを表示する

WPF の 3D グラフィックスの機能を使って、3D オブジェクトを表示する方法を示します。
XAML でさいころのような立方体を描画することを目指します。

Dice (XAML)

先に XAML を示します。この後に簡単な説明が続きます。

<Window x:Class="DiceXamlWpf.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:DiceXamlWpf"
Title="Dice (XAML)" Height="600" Width="600">
<Window.Resources>
<Style x:Key="FaceStyle" TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="White"/>
<Setter Property="Width" Value="30"/>
<Setter Property="Height" Value="30"/>
<Setter Property="TextAlignment" Value="Center"/>
<Setter Property="FontSize" Value="20"/>
</Style>
</Window.Resources>
<Grid Background="#FF333333">
<Grid Visibility="Hidden">
<TextBlock x:Name="Face1" Style="{DynamicResource FaceStyle}" Text="1" Background="#FF222222"/>
<TextBlock x:Name="Face2" Style="{DynamicResource FaceStyle}" Text="2" Background="#FFDF2C2C"/>
<TextBlock x:Name="Face3" Style="{DynamicResource FaceStyle}" Text="3" Background="#FFEE9319"/>
<TextBlock x:Name="Face4" Style="{DynamicResource FaceStyle}" Text="4" Background="#FFE3E60A"/>
<TextBlock x:Name="Face5" Style="{DynamicResource FaceStyle}" Text="5" Background="#FF29D214"/>
<TextBlock x:Name="Face6" Style="{DynamicResource FaceStyle}" Text="6" Background="#FF4444BB"/>
</Grid>
<Viewport3D>
<Viewport3D.Camera>
<PerspectiveCamera Position="0,0,10"/>
</Viewport3D.Camera>
<ModelVisual3D>
<ModelVisual3D.Content>
<AmbientLight/>
</ModelVisual3D.Content>
</ModelVisual3D>
<ModelVisual3D>
<ModelVisual3D.Transform>
<Transform3DGroup>
<TranslateTransform3D OffsetZ="0" OffsetX="0" OffsetY="0"/>
<ScaleTransform3D ScaleZ="1" ScaleY="1" ScaleX="1"/>
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D Axis="0.6,0.3,0.7" Angle="-70"/>
</RotateTransform3D.Rotation>
</RotateTransform3D>
<TranslateTransform3D OffsetZ="0" OffsetX="0" OffsetY="0"/>
<TranslateTransform3D OffsetZ="-2" OffsetX="2" OffsetY="1"/>
</Transform3DGroup>
</ModelVisual3D.Transform>
<ModelVisual3D.Content>
<Model3DGroup>
<GeometryModel3D>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<VisualBrush Visual="{Binding ElementName=Face1}"/>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.Geometry>
<MeshGeometry3D Positions="-1,-1,1 1,-1,1 1,1,1 -1,1,1" TriangleIndices="0,1,2 0,2,3" TextureCoordinates="0,1 1,1 1,0 0,0"/>
</GeometryModel3D.Geometry>
</GeometryModel3D>
<GeometryModel3D>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<VisualBrush Visual="{Binding ElementName=Face2}"/>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.Geometry>
<MeshGeometry3D Positions="-1,-1,-1 1,-1,-1 1,-1,1 -1,-1,1" TriangleIndices="0,1,2 0,2,3" TextureCoordinates="0,1 1,1 1,0 0,0"/>
</GeometryModel3D.Geometry>
</GeometryModel3D>
<GeometryModel3D>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<VisualBrush Visual="{Binding ElementName=Face3}"/>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.Geometry>
<MeshGeometry3D Positions="1,-1,-1 1,1,-1 1,1,1 1,-1,1" TriangleIndices="0,1,2 0,2,3" TextureCoordinates="0,1 1,1 1,0 0,0"/>
</GeometryModel3D.Geometry>
</GeometryModel3D>
<GeometryModel3D>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<VisualBrush Visual="{Binding ElementName=Face4}"/>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.Geometry>
<MeshGeometry3D Positions="-1,1,-1 -1,-1,-1 -1,-1,1 -1,1,1" TriangleIndices="0,1,2 0,2,3" TextureCoordinates="0,1 1,1 1,0 0,0"/>
</GeometryModel3D.Geometry>
</GeometryModel3D>
<GeometryModel3D>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<VisualBrush Visual="{Binding ElementName=Face5}"/>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.Geometry>
<MeshGeometry3D Positions="1,1,-1 -1,1,-1 -1,1,1 1,1,1" TriangleIndices="0,1,2 0,2,3" TextureCoordinates="0,1 1,1 1,0 0,0"/>
</GeometryModel3D.Geometry>
</GeometryModel3D>
<GeometryModel3D>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<VisualBrush Visual="{Binding ElementName=Face6}"/>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.Geometry>
<MeshGeometry3D Positions="1,-1,-1 -1,-1,-1 -1,1,-1 1,1,-1" TriangleIndices="0,1,2 0,2,3" TextureCoordinates="0,1 1,1 1,0 0,0"/>
</GeometryModel3D.Geometry>
</GeometryModel3D>
</Model3DGroup>
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D>
</Grid>
</Window>
view raw MainWindow.xaml hosted with ❤ by GitHub

 

まず、Viewport3D を配置します。
3D オブジェクトを描画するには次の 3 種類のものが必要で、これらを Viewport3D に設定します。

カメラ
Viewport3D.Camera プロパティに指定します。PerspectiveCamera を使うと、遠近感が出ます。
既定では LookDirection="0,0,-1" UpDirection="0,1,0" となっており、
つまり z 軸の逆方向を向いており、y 軸の正方向が上になっています。

照明
通常のオブジェクトと同様に、Viewport3D.Children プロパティにモデルとして追加します。
AmbientLight を使うと、影がなく、一様な明るさでオブジェクトが表示されます。

オブジェクト
主に ModelVisual3D として定義し、Viewport3D.Children プロパティに追加します。

以下では、ModelVisual3D の設定について書いていきます。
GeometryModel3D でオブジェクトを定義し、ModelVisual3D.Content プロパティにそれを指定します。
複数のオブジェクトをグループ化するには、Model3DGroup を使います。
GeometryModel3D には、マテリアルとジオメトリを指定します。

マテリアル

GeometryModel3D.Material プロパティで、何を表示するかを指定します。
ここでは、さいころの面を描くために 2D の TextBlock を用意しておき、それをテクスチャとして指定しています。
2D の Brush をテクスチャとして利用するには、DiffuseMaterial を使います。

また、BackMaterial プロパティに、Material プロパティと同じものを指定すれば、裏面には表面を反転したものが表示されます。
つまり、「3」の裏面は「ε」のようになります。

ジオメトリ

GeometryModel3D.Geometry プロパティで、どこに表示するかを指定します。
具体的には、MeshGeometry3D を利用して、次のものを指定します。

点の集合 (Positions プロパティ)
メッシュを構成する三角形に分割した場合に、頂点となる点の集合を指定します。

三角形の集合 (TriangleIndices プロパティ)
Positions プロパティで指定した点に0からインデックスを付けて、メッシュを構成する三角形の頂点を列挙します。
三角形の頂点の順番は、表側から見て反時計回りになるように指定します。

テクスチャの位置 (TextureCoordinates プロパティ)
Positions プロパティで指定した各点が、マテリアルの中のどの点に対応するかを指定します。
マテリアルは 2D なので、左上が (0, 0)、右下が (1, 1) です。

例えば、

<MeshGeometry3D Positions="-1,-1,1 1,-1,1 1,1,1 -1,1,1" TriangleIndices="0,1,2 0,2,3" TextureCoordinates="0,1 1,1 1,0 0,0"/>

とした場合は、P0(-1, -1, 1), P1(1, -1, 1), P2(1, 1, 1), P3(-1, 1, 1) の 4 点があり、
メッシュは P0P1P2 と P0P2P3 の 2 つの三角形で構成され、
P0 が左下、P1 が右下、P2 が右上、P3 が左上となるようにテクスチャを描画するという意味になります。
なお、これらの値を XAML 上で列挙するときの区切り文字としては、カンマまたはスペースの両方が使えます。

アフィン変換 (移動、スケール、回転など) について

3D オブジェクトのアフィン変換は、ModelVisual3D.Transform プロパティで指定します。

コードでの記述について

XAML の代わりに C# などの .NET のコードで記述することもできます。
同一の処理が繰り返される場合には、コードで生成したほうが簡単に表現できることもあります。

 

次回: WPF で 3D オブジェクトを回転させる

作成したサンプル
Wpf3DSample (GitHub): コードで描画した例もあります。

バージョン情報
.NET Framework 4.5

参照
3-D グラフィックスの概要
3-D 変換の概要

WPF で外枠にはみ出さないようにする

WPF で、Border や Grid などの枠の中に、その領域をはみ出すようなコントロールを配置して、
それを Transform などで移動させるとします。
すると、そのコントロールの一部が切り取られてしまったり、外枠にはみ出てしまうなど、期待通りの動作にならないことがあります。
以下では、この現象をどうすれば回避できるかを試していきます。
(追記: Canvas で ClipToBounds="True" とすればよい、とコメントを頂いたため、後半に追記しました。)

Blend for Visual Studio で WPF アプリケーション プロジェクトを作成して、
Border の中に Rectangle を配置して、Rectangle を期待通りに動かせるかどうかを調べます。

Window の領域を 4 つに分けて、以下の方法を試してみます。

  • 左上: Border の中に Rectangle を配置するだけ
  • 右上: Border と Rectangle の間に Canvas を配置する
  • 左下: Border と Rectangle の間に ScrollViewer と Canvas を配置する
  • 右下: Border と Rectangle の間に ScrollContentPresenter と Canvas を配置する

ScrollViewer を使うことで、外側にはみ出さないようにします。
スクロールバーを消すために、VerticalScrollBarVisibility プロパティを Disabled に設定します。
また、ScrollContentPresenter は、ScrollViewer の中で使われているコントロールです。

すると、Blend for Visual Studio のデザイナーでは次のように表示されます。
この時点で、右上の方法では外枠の Border の上に描画されてしまうことがわかります。

FrameLayoutWpf

また、この 4 つの Rectangle に対して、TranslateZoomRotateBehavior を設定します。
(Blend for Visual Studio のアセットからドラッグ アンド ドロップします。)
これで、実行時にタッチ操作などでコントロールを動かせるようになります。

全体の XAML は次のようになります。

<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:FrameLayoutWpf"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
x:Class="FrameLayoutWpf.MainWindow"
mc:Ignorable="d"
Title="Frame Layout" Height="600" Width="600">
<Window.Resources>
<Style x:Key="BorderStyle" TargetType="{x:Type Border}">
<Setter Property="BorderBrush" Value="#99339900"/>
<Setter Property="BorderThickness" Value="15"/>
</Style>
<Style x:Key="RectangleStyle" TargetType="{x:Type Rectangle}">
<Setter Property="Width" Value="350"/>
<Setter Property="Height" Value="350"/>
<Setter Property="Fill">
<Setter.Value>
<LinearGradientBrush EndPoint="1,1" StartPoint="0,0">
<GradientStop Color="#FFFF9900" Offset="0"/>
<GradientStop Color="White" Offset="1"/>
</LinearGradientBrush>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Border Style="{DynamicResource BorderStyle}">
<Rectangle Style="{DynamicResource RectangleStyle}">
<i:Interaction.Behaviors>
<ei:TranslateZoomRotateBehavior/>
</i:Interaction.Behaviors>
</Rectangle>
</Border>
<Border Style="{DynamicResource BorderStyle}" Grid.Column="1">
<Canvas>
<Rectangle Style="{DynamicResource RectangleStyle}">
<i:Interaction.Behaviors>
<ei:TranslateZoomRotateBehavior/>
</i:Interaction.Behaviors>
</Rectangle>
</Canvas>
</Border>
<Border Style="{DynamicResource BorderStyle}" Grid.Row="1">
<ScrollViewer VerticalScrollBarVisibility="Disabled">
<Canvas>
<Rectangle Style="{DynamicResource RectangleStyle}">
<i:Interaction.Behaviors>
<ei:TranslateZoomRotateBehavior/>
</i:Interaction.Behaviors>
</Rectangle>
</Canvas>
</ScrollViewer>
</Border>
<Border Style="{DynamicResource BorderStyle}" Grid.Row="1" Grid.Column="1">
<ScrollContentPresenter>
<ScrollContentPresenter.Content>
<Canvas>
<Rectangle Style="{DynamicResource RectangleStyle}">
<i:Interaction.Behaviors>
<ei:TranslateZoomRotateBehavior/>
</i:Interaction.Behaviors>
</Rectangle>
</Canvas>
</ScrollContentPresenter.Content>
</ScrollContentPresenter>
</Border>
</Grid>
</Window>
view raw MainWindow.xaml hosted with ❤ by GitHub

このアプリケーションを実際に動かしてみます。

FrameLayoutWpf

  • 左上: 初期配置の領域以外の部分は切り取られてしまいます。
  • 右上: Rectangle が枠外にはみ出します。
  • 左下: 期待通りです。
  • 右下: 期待通りの動作ですが、ScrollContentPresenter.Content が既定プロパティではないため、
    上の階層がデザインツールに表示されません。

 

ここで、よねやんさんから、Canvas の ClipToBounds プロパティの値を True にすればよい、とコメントを頂きました。

右上の Canvas を変更して実行してみます。

<Border Style="{DynamicResource BorderStyle}" Grid.Column="1">
<Canvas ClipToBounds="True">
<Rectangle Style="{DynamicResource RectangleStyle}">
<i:Interaction.Behaviors>
<ei:TranslateZoomRotateBehavior/>
</i:Interaction.Behaviors>
</Rectangle>
</Canvas>
</Border>
view raw MainWindow.xaml hosted with ❤ by GitHub

FrameLayoutWpf

期待通りです。
というわけで、ScrollViewer などは不要になりました。
Canvas を配置して、ClipToBounds="True" を設定するだけです。

 

作成したサンプル
FrameLayoutWpf (GitHub)

バージョン情報
.NET Framework 4.5

参照
UIElement.ClipToBounds プロパティ

XAML マークアップ拡張を自作する

例えば XAML でデータ バインディングをするときに、

Text="{Binding Message}"

のような記法を用いますが、これは XAML マークアップ拡張と呼ばれ、
Binding クラスが MarkupExtension クラスを継承しているためにこの形式の記述ができるようになっています。

今回は例として、構成ファイル (.config) の <appSettings> の値を読み込む
XAML マークアップ拡張クラスを自作してみたいと思います。
先にコードを示します。

using System;
using System.Configuration;
using System.Windows;
using System.Windows.Markup;
namespace MarkupExWpf
{
[MarkupExtensionReturnType(typeof(object))]
public class AppSettingsExtension : MarkupExtension
{
public string Key { get; set; }
public AppSettingsExtension()
{
}
public AppSettingsExtension(string key)
{
Key = key;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
if (serviceProvider == null) throw new ArgumentNullException(nameof(serviceProvider));
var type = GetTargetPropertyType(serviceProvider);
if (type == null) return DependencyProperty.UnsetValue;
var value = ConfigurationManager.AppSettings[Key];
if (type.IsValueType && value == null) return DependencyProperty.UnsetValue;
return type.IsEnum ?
Enum.Parse(type, value) :
Convert.ChangeType(value, type);
}
static Type GetTargetPropertyType(IServiceProvider serviceProvider)
{
var provider = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
if (provider == null) return null;
var property = provider.TargetProperty as DependencyProperty;
if (property == null) return null;
return property.PropertyType;
}
}
}

マークアップ拡張クラスでは、ProvideValue メソッドを実装して実際に設定する値を返します。
設定先となる依存関係プロパティを取得するには、
ProvideValue メソッドに渡される serviceProvider から IProvideValueTarget を取り出します。

さて、App.config の <appSettings> に設定値を追加して、
MainWindow.xaml 上の UI コントロールのプロパティにそれらの値を設定します。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<appSettings>
<add key="WindowHeight" value="600" />
<add key="WindowWidth" value="400" />
<add key="ResizeMode" value="NoResize" />
<add key="Message" value="Hello, world." />
<add key="FontSize" value="36" />
</appSettings>
</configuration>
view raw App.config hosted with ❤ by GitHub
<Window x:Class="MarkupExWpf.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MarkupExWpf"
Title="Markup Ex" Height="{local:AppSettings WindowHeight}" Width="{local:AppSettings WindowWidth}"
ResizeMode="{local:AppSettings ResizeMode}" FontSize="{local:AppSettings FontSize}">
<Grid>
<TextBlock Text="{local:AppSettings Message}"/>
</Grid>
</Window>
view raw MainWindow.xaml hosted with ❤ by GitHub

クラス名にサフィックスとして Extension を付けておくと、サフィックスを除いた名前で記述できます。
値を設定している部分は、

Height="{local:AppSettings Key=WindowHeight}"

と書いてもよいですが、Key を引数に取るコンストラクターが存在することで、

Height="{local:AppSettings WindowHeight}"

と書けるようになります。
なお、この部分は次のようにも書けます。

<Window>
    <Window.Height>
        <local:AppSettings Key="WindowHeight"/>
    </Window.Height>
</Window>

MarkupExWpf

 

前回: プロパティ変更をタイマーで同期して通知する

作成したサンプル
MarkupExWpf (GitHub)

バージョン情報
C# 6.0
.NET Framework 4.5

参照
MarkupExtension クラス
XAML のマークアップ拡張機能の概要
型コンバーターおよびマークアップ拡張機能で使用できるサービス コンテキスト

プロパティ変更をタイマーで同期して通知する

C# のコードでデータ バインディング (1) で書いた通り、
Binding Source は INotifyPropertyChanged か DependencyObject でなければ
プロパティ変更通知ができないため、永続的に値が反映される OneWay バインディングはできません。

そこで、プロパティ変更通知機能を持たない既存のオブジェクトをタイマーで監視して
プロパティ変更通知を発生させる DynamicSyncProxy<T> クラスというのを作ってみました。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Dynamic;
using System.Linq;
using System.Reflection;
using System.Threading;
namespace DynamicBindingWpf
{
public static class DynamicSyncProxy
{
public static DynamicSyncProxy<T> ToDynamicSyncProxy<T>(this T target, int intervalInMilliseconds = 1000) =>
new DynamicSyncProxy<T>(target, intervalInMilliseconds);
public static bool IsIndexer(this PropertyInfo property) =>
property.Name == "Item" && property.GetIndexParameters().Length > 0;
}
public class DynamicSyncProxy<T> : DynamicObject, INotifyPropertyChanged
{
T Target;
Dictionary<string, PropertyInfo> Properties;
Dictionary<string, object> PropertyValuesCache;
Timer SyncTimer;
public DynamicSyncProxy(T target, int intervalInMilliseconds = 1000)
{
Target = target;
Properties = Target.GetType().GetProperties()
.Where(p => !p.IsIndexer())
.ToDictionary(p => p.Name);
PropertyValuesCache = Properties.Values
.ToDictionary(p => p.Name, p => p.GetValue(Target));
SyncTimer = new Timer(o => SyncPropertyValues(), null, intervalInMilliseconds, intervalInMilliseconds);
}
public T GetTargetObject() => Target;
#region DynamicObject
public override IEnumerable<string> GetDynamicMemberNames()
{
return Properties.Keys;
}
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
result = Properties[binder.Name].GetValue(Target);
return true;
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
Properties[binder.Name].SetValue(Target, value);
return true;
}
#endregion
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propertyName) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
#endregion
#region Sync
void SyncPropertyValues()
{
foreach (var name in Properties.Keys)
SyncPropertyValue(name);
}
void SyncPropertyValue(string propertyName)
{
var oldValue = PropertyValuesCache[propertyName];
var newValue = Properties[propertyName].GetValue(Target);
if (Equals(oldValue, newValue)) return;
PropertyValuesCache[propertyName] = newValue;
NotifyPropertyChanged(propertyName);
}
#endregion
}
}
view raw DynamicSyncProxy.cs hosted with ❤ by GitHub

DynamicObject クラスを継承し、既存のオブジェクトを内部に持ちます。
そのオブジェクトのプロパティへのアクセスを透過するプロキシとして機能します。

さて、プロパティ変更通知機能を持たない TextModel クラスを用意して、
DynamicSyncProxy<T> を介してデータ バインディングを設定します (dynamic 型として扱います)。

using System;
namespace DynamicBindingWpf
{
public class AppModel
{
public dynamic TextModel { get; } = new TextModel().ToDynamicSyncProxy(300);
}
public class TextModel
{
string _Input;
public string Input
{
get { return _Input; }
set
{
_Input = value;
Output = _Input?.ToUpper();
}
}
public string Output { get; private set; }
}
}
view raw AppModel.cs hosted with ❤ by GitHub
<Window x:Class="DynamicBindingWpf.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:DynamicBindingWpf"
Title="Dynamic Binding" Height="400" Width="600" FontSize="36">
<Window.DataContext>
<local:AppModel/>
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBox Text="{Binding TextModel.Input, UpdateSourceTrigger=PropertyChanged}" TextWrapping="Wrap"/>
<TextBlock Text="{Binding TextModel.Output}" TextWrapping="Wrap" Grid.Row="1"/>
</Grid>
</Window>
view raw MainWindow.xaml hosted with ❤ by GitHub

この例では 0.3 秒ごとに値が同期されます。

DynamicBinding

 

前回: ExpandoObject を使ったデータ バインディング
次回: XAML マークアップ拡張を自作する

作成したサンプル
DynamicBindingWpf (GitHub)

バージョン情報
C# 6.0
.NET Framework 4.5

参照
データ バインディングの概要
C# のコードでデータ バインディング (1)