OnRenderで再描画されない現象と対策
ハマったのでメモ
WPFアプリケーションであるコントロールをXamlではなくOnRenderメソッドをオーバーライドして自前で描画処理を実装していました。
ビルド、実行して問題なく動いていましたが、あることをするとOnRenderメソッドが呼ばれないことが発覚しました。
コントロールには次のような振る舞いを実装していました。
- あるプロパティを変更することでコントロールの背景色を切り替える。
- ControlTemplateは定義していない。OnRenderで描画処理を実装する。
そしてそのコントロールを画面に配置し、ボタンをクリックすると画面上のすべてのコントロールの状態を変更し、背景色を色替えするというものでした。
画面にはコントロールを数百個(以下の画面では300個)描画しています。
以下はその画面です。
画面右上のTestボタンをクリックすると全コントロールの状態を切り替えます。
以下はTestボタンをクリックした時の画面です。
なぜか一部のコントロールが色替えされていません。
コントロールと画面のコードは以下のとおりです。
- コントロールクラス
using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Media; namespace CustomControls { public class Symbol : Control { #region Dependency Properties /// <summary> /// TypeValue 依存関係プロパティを識別します。 /// </summary> public static readonly DependencyProperty TypeValueProperty = DependencyProperty.Register( "TypeValue", typeof(short), typeof(Symbol), new FrameworkPropertyMetadata( (short)0, FrameworkPropertyMetadataOptions.AffectsRender, TypeValuePropertyChanged)); #endregion #region Ctor /// <summary> /// コンストラクタ /// </summary> static Symbol() { // ------------------------------------------------------------- // コントロールテンプレート以外で必要な設定は // すべてここに記述します。 // ------------------------------------------------------------- DefaultStyleKeyProperty.OverrideMetadata(typeof(Symbol), new FrameworkPropertyMetadata(typeof(Symbol))); BackgroundProperty.OverrideMetadata(typeof(Symbol), new FrameworkPropertyMetadata(Brushes.White)); BorderBrushProperty.OverrideMetadata(typeof(Symbol), new FrameworkPropertyMetadata(Brushes.Black)); } #endregion #region Properties /// <summary> /// オブジェクトの状態種別を設定、取得します。 /// </summary> [Category("Custom")] [Description("オブジェクトの状態種別を設定、取得します。")] public short TypeValue { get { return (short)GetValue(TypeValueProperty); } set { SetValue(TypeValueProperty, value); } } #endregion #region Methods /// <summary> /// レイアウトシステムからの描画処理に参加します。 /// </summary> /// <param name="drawingContext">特定の要素に対する描画命令。 このコンテキストはレイアウト システムに提供されます。</param> protected override void OnRender(DrawingContext drawingContext) { base.OnRender(drawingContext); var background = TypeValue == 0 ? Brushes.Blue : Brushes.Red; var foreground = TypeValue == 0 ? Brushes.Aqua : Brushes.Orange; // 背景色と枠線色は同じ Background = background ?? Brushes.White; BorderBrush = background ?? Brushes.Black; Foreground = foreground ?? Brushes.Black; // --------------------------------------------------------- // 背景の角丸め長方形を描画 // --------------------------------------------------------- drawingContext.DrawRoundedRectangle( Background, null, new Rect(0, 0, ActualWidth, ActualHeight), 4.0d, 4.0d); } /// <summary> /// TypeValueプロパティの値が変更された際に呼ばれるイベントハンドラです。 /// </summary> /// <param name="d">イベント送信元オブジェクト</param> /// <param name="e">イベント引数オブジェクト</param> private static void TypeValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var self = d as Symbol; if (self != null) { self.OnValueTypeChanged((short)e.NewValue); } } /// <summary> /// TypeValueの値が変更されました。 /// </summary> /// <param name="newValue">変更後の値</param> private void OnValueTypeChanged(short newValue) { } #endregion } }
- View(Xaml)
<Window x:Class="RenderingTest.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Width="400" Height="400" Loaded="MainWindow_OnLoaded" WindowStartupLocation="CenterScreen"> <Grid> <Canvas x:Name="LayoutRoot" /> <Grid> <Button Width="75" Height="28" HorizontalAlignment="Right" VerticalAlignment="Top" Click="ButtonBase_OnClick" Content="Test" /> </Grid> </Grid> </Window>
- Viewクラス(コードビハインド)
using System; using System.Linq; using System.Windows; using System.Windows.Controls; using CustomControls; namespace RenderingTest { /// <summary> /// MainWindow.xaml の相互作用ロジック /// </summary> public partial class MainWindow : Window { /// <summary> /// 描画するオブジェクトの数 /// </summary> const int Maximum = 300; public MainWindow() { InitializeComponent(); } private void MainWindow_OnLoaded(object sender, RoutedEventArgs e) { var r = new Random(); foreach (var i in Enumerable.Range(0, Maximum)) { // サイズ固定 var item = new Symbol { Width = 12, Height = 12, }; // 描画座標はランダムに設定する。 LayoutRoot.Children.Add(item); Canvas.SetLeft(item, r.Next((int)(LayoutRoot.ActualWidth - 12))); Canvas.SetTop(item, r.Next((int)(LayoutRoot.ActualHeight - 12))); } } /// <summary> /// Testボタンのクリックイベントハンドラ /// </summary> /// <param name="sender">The source of the event.</param> /// <param name="e">The <see cref="RoutedEventArgs"/> instance containing the event data.</param> private void ButtonBase_OnClick(object sender, RoutedEventArgs e) { // LayoutRoot(Canvas) の子要素(Symbol) に対して状態変更 // これで全てのコントロールの背景色が切り替わる。 foreach (var child in LayoutRoot.Children) { var item = child as Symbol; if (item != null) { item.TypeValue = (short)(item.TypeValue == 0 ? 1 : 0); } } } } }
このまま実行してもコンパイルエラーも発生しなければ、BindingExceptionも出力ウィンドウに表示されません。
さて、困ったものです。
同様の現象に遭遇した人を探すべく、"WPF OnRender doesn't work"で検索してみました。
すると以下のページがトップに。
stackoverflow.com
まさしく同じ現象!!
しかし、回答者も結局原因の究明には至っていない模様…
試行錯誤し、ひとつひとつの処理をチェックしていった結果
犯人はSymblo.csの以下のコードだということが判明しました。
// 背景色と枠線色は同じ
Background = background ?? Brushes.White;
BorderBrush = background ?? Brushes.Black;
Foreground = foreground ?? Brushes.Black;
OnRenderメソッドを以下のように書き換えてみます。
/// <summary> /// レイアウトシステムからの描画処理に参加します。 /// </summary> /// <param name="drawingContext">特定の要素に対する描画命令。 このコンテキストはレイアウト システムに提供されます。</param> protected override void OnRender(DrawingContext drawingContext) { base.OnRender(drawingContext); var background = TypeValue == 0 ? Brushes.Blue : Brushes.Red; var foreground = TypeValue == 0 ? Brushes.Aqua : Brushes.Orange; // --------------------------------------------------------- // 背景の角丸め長方形を描画 // --------------------------------------------------------- drawingContext.DrawRoundedRectangle( background, null, new Rect(0, 0, ActualWidth, ActualHeight), 4.0d, 4.0d); }
Background, Foreground, BorderBrush への代入を削除しました。
さて、実行してみます。
- これが
- こうなる。
無事に背景色の色替えができました。
結局何が原因だったのか?
すみません。具体的な原因についてはわかっておりません。m(_ _)m
ただ、Backgroundなど描画用のプロパティを代入することでOnRenderが実行される→OnRender内部で再度代入→また、OnRenderが呼ばれる…
と実行していった結果StackOverflowExceptionのような例外がWPFレンダリングエンジン内部で発行され、途中で描画が止まったのでは…?
ただ、そうだとするなら例外が発行されないのもおかしいし、一部だけが再描画に成功しているのもおかしい。
ということで原因をご存知の方がいらっしゃったら教えて下さい。
結論
OnRenderメソッドで描画関連プロパティへの代入は厳禁!!