WindowsFormsHostとGridSplitterのねばねばした関係
全ての画面を作るというわけにいかないプロジェクトもあるので、WindowsFormsHostをグリッド上に配置するというケースも時々あるかと思います。 そんなときに、GridSplitterを入れるとWindowsFormsのコントロールが酷い描画をするという話はよく聞きます。
そんなときの処方がこれです。
public class ImmediateWindowsFormsHost : WindowsFormsHost
{
public ImmediateWindowsFormsHost()
{
SizeChanged += ImmediateWindowsFormsHost_SizeChanged;
}
private void ImmediateWindowsFormsHost_SizeChanged(object sender, SizeChangedEventArgs e)
{
Child.Refresh();
}
}
あまり模範的なつくりではありませんが、要はリサイズしたときに即時再描画を行えばいいのです。 SizeChangedが最良かどうかもわかりませんが。
WindowsFormsHostの中身は別ウィンドウが単にWPFの画面上に乗っかっているにすぎません。 その性質を利用するということです。
ほんと・・・時として工数削減のための割り切りも必要になりますよね。
Msを16倍出し抜くC#+WPFの3回目(その前に)
スライドがどうも整ってないのでまだ公には出してないのですが。
今までくどくどと挙げてきたWPFボトルネックを全部避けてみるだけで、実際どの位速いパネルが生まれるのかというあたりをデモしました。
その時にちょっとおまけ話としてイベントの発射地点についてのまとめをしてみました。
MVVMが浸透した関係もあって、画面内のコントロールの使い方を誤る機会というものも多少減ったとは思うのです。
しかし、ScrollChangedとかSizeChangedとかのイベントの出所がどこのパスなのか、MicrosoftはMSDNに正確に記載すべきじゃないかと思うのです。
上記2イベントはレイアウトパスでサイズ等々計算が終わったところで発射されます。
このイベントを受けてしまって、ついコントロールの描画にかかわる何かを書き換えてしまうと、バインディングパスやレイアウトパスはやり直しになる可能性があります。
いつになったら描画パスに移行できるのやら。
InvalidateVisualとかInvalidate系のメソッドの使用を諌めるよりもよほどこちらを気を付けるようにアナウンスしていただきたいと願ってやみません。
もちろん、上記メソッドもレイアウトパスをやり直させる効果があります。しかし、バインディングパスまでやり直しになる危険はありません。
本当に恐れるべきは、イベントの発射地点がどのパスにあるかということです。
そのあたりも、ぜひリファレンスコードを読んでみていただきたいと思います。
イベントの名前をここの検索ボックスで入力するだけです。
Reference Source
Thickness = 1の線分がどのくらい遅いか試すコード
私がここ数日言っていたことは、ここのコードでそのまま試せます。
走らせながら、太さを調整してみるとわかると思います。
モニターのデフォルト倍率によって多少変化しますが、要は実描画時に何ピクセルの太さになるかで速度が決まります。
あと、太さは顕著に影響しますが、ブラシの透過率は実は全然影響しないことも書き添えておきたいと思います。
高速な線分描画とか
あんまりショッキングな書き方をしたくありませんので、現時点ではある開発機で発生した事象として記載します。
始点終点がランダムである場合、線の太さが√3に近いほど高速になるという実測データを得ました。
大まかに言いますと 最速は 1.71px~1.74pxくらいの太さ範囲です。
太さが1pxの線を引く場合の6倍程度の速度でした(見過ごせますか?これ)。
無論、水平垂直の線分の話ではなく、自由に引いた線分のことではあります。
斜めの線を引くと、当然デフォルトでアンチエイリアスがかかるのですが、どうもその部分はハードウェアの恩恵が低いらしくある程度線が太くないと実は無駄にCPUパワーを浪費するということらしいですね。
実証コードは、当初業務で作成したため、持ち出すことはできておりません(当然ですお仕事ですから)。
ちょっと暇がありましたら再度作成してここに出してみようとは思います。が、誰か実証コード書いていただけるとありがたいなぁとは思っていたり思っていたりします。
チップによっても違うかなとは思っているのですが、しかしWPFのアンチエイリアスは実はあんまりハードウェアの恩恵がありません(ご存知でしょうか)。なので、グラフィックチップによる差異は皆無あるいは僅差との推論をしております。
どなたか、計測結果を知らせていただければありがたいと思います。また、私の方でも及ぶ限りのベンチマークと実証コードを公開したいと思っております。
ちなみに、ある事実から予想は√2だったのですが、まんまとはずれてしまいました。関係者一同、√3かよ。という状態ですね。(笑)
さあ、これで自分のアプリで線分の描画を1.7くらいにできる人がいたら・・・・。いえ、何と言ったらよいやら。まあ、速度を堪能していただきたいです。活用法については、各自の技量としか。
仮想化しないでも速いCanvasの作り方
XAML Advent Calendar 2014 - Qiita
こちら向けの記事です。1日遅刻しました。ごめんなさい。
あちこちで書いて回っておりますがPanel.Childrenがとても重いのです。 遅い原因はこちら。 http://referencesource.microsoft.com/#PresentationFramework/Framework/System/Windows/Controls/Panel.cs,1001
同じ階層のVisualはVisualTreeに結合した順番でしか表示できないので、ChildrenやPanel.ZIndexが変更される都度、Panel.ZIndexを使ってこのRecomputeZStateが走ります。 レイアウトパスの直前に1回だけしてくれたらいいのにと思うかもしれませんが、残念ながらそのような作りにはなっていないのが現状です。
とすると、FrameworkElementに独自のChildrenを実装してやるしか手がない。ということになります。 幸いなことに、XAMLで記述する場合も [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] という属性をつけて、IList<UIElement>を実装してやればいいので、結構簡単なんです。
<Window x:Class="FastCanvas.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:FastCanvas"
Title="MainWindow">
<ScrollViewer HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible">
<local:Canvas x:Name="_canvas" Width="3200" Height="32000"></local:Canvas>
</ScrollViewer>
</Window>
今日の主役は仮想化しないで高速なCanvasなので、XAMLはこのくらいです。 XAMLの話の予定でしたのにすみません。 一応、100000個くらいのシェイプをゾロッと配置してみます。
using System.Windows;
using System.Windows.Media;
using System.Windows.Shapes;
namespace FastCanvas
{
/// <summary>
/// MainWindow.xaml の相互作用ロジック
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
for(int y=0 ; y<1000 ; y++)
{
for (int x = 0; x < 100; x++)
{
var rectangle = new Rectangle() { Width = 32, Height = 32, Fill = Brushes.Aquamarine, RadiusY = 12, RadiusX = 12 };
FastCanvas.Canvas.SetLocation(rectangle, new Point(x * 32, y * 32));
_canvas.Children.Add(rectangle);
}
}
}
}
}
そして、これが問題のCanvas
using System.Windows;
using System.Windows.Media;
using System.ComponentModel;
using System.Collections.ObjectModel;
namespace FastCanvas
{
public class Canvas : FrameworkElement
{
public static Point GetLocation(DependencyObject obj)
{
return (Point)obj.GetValue(LocationProperty);
}
public static void SetLocation(DependencyObject obj, Point value)
{
obj.SetValue(LocationProperty, value);
}
public static readonly DependencyProperty LocationProperty =
DependencyProperty.RegisterAttached("Location", typeof(Point), typeof(Canvas),
new FrameworkPropertyMetadata(default(Point),FrameworkPropertyMetadataOptions.AffectsArrange));
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public ObservableCollection<UIElement> Children{get; private set;}
public Canvas()
{
Children = new ObservableCollection<UIElement>();
Children.CollectionChanged += Children_CollectionChanged;
}
void Children_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if(e.OldItems != null)
{
foreach (UIElement oldItem in e.OldItems)
{
RemoveVisualChild(oldItem);
}
}
if(e.NewItems != null)
{
foreach (UIElement newItem in e.NewItems)
{
AddVisualChild(newItem);
}
}
}
protected override int VisualChildrenCount
{
get
{
return Children.Count;
}
}
protected override Visual GetVisualChild(int index)
{
return Children[index];
}
protected override Size ArrangeOverride(Size finalSize)
{
foreach(var child in Children)
{
var location = GetLocation(child);
child.Arrange(new Rect(location, child.DesiredSize));
}
return base.ArrangeOverride(finalSize);
}
protected override Size MeasureOverride(Size availableSize)
{
foreach (var child in Children)
{
var fe = child as FrameworkElement;
if( fe != null )
{
child.Measure(new Size(fe.Width,fe.Height));
}
}
return base.MeasureOverride(availableSize);
}
}
}
100000のシェイプを配置しても、Panel.Children.Addにあるようなもっさり感はありません。 ZIndexが不要ならこの程度で十分行けてしまうという程度で考えていただけると良いかなと思います。
FreezableにDataContextが伝播する仕掛け(その2)
素早く実証コードを書いてみました。
ViewModelとしては概ねこんな感じ
using System;
using System.Windows;
using System.Windows.Input;
using System.Diagnostics;
namespace WpfApplication1
{
class MainWindowViewModel : DependencyObject
{
public string MessageText{
get { return (string)GetValue(MessageTextProperty); }
set { SetValue(MessageTextProperty, value); }
}
public static readonly DependencyProperty MessageTextProperty =
DependencyProperty.Register("MessageText", typeof(string), typeof(MainWindowViewModel),
new PropertyMetadata("試験"));
}
class ShowMessageCommand : Freezable, ICommand
{
protected override Freezable CreateInstanceCore(){
return new ShowMessageCommand();
}
public bool CanExecute(object parameter){return true;}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter){
MessageBox.Show(Message);
}
public string Message{
get { return (string)GetValue(MessageProperty); }
set { SetValue(MessageProperty, value); }
}
public static readonly DependencyProperty MessageProperty =
DependencyProperty.Register("Message", typeof(string), typeof(ShowMessageCommand),
new PropertyMetadata(string.Empty,
(d, e) =>{
Debug.Fail("ここで止めると、StackTrace取れます。");
}));
}
}XAMLはこんな感じ
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<StackPanel>
<Button>
ボタン
<Button.Command>
<local:ShowMessageCommand Message="{Binding MessageText}"/>
</Button.Command>
</Button>
</StackPanel>
</Window>止まったところでデバッガに入り、スタックトレースを全部見る(当然ですが「外部コードを表示」のチェックはON)と通過箇所はわかります。
重要なポイントはここ。
WindowsBase.dll!System.Windows.DependencyObject.OnInheritanceContextChanged(System.EventArgs args) WindowsBase.dll!System.Windows.Freezable.AddInheritanceContext(System.Windows.DependencyObject context, System.Windows.DependencyProperty property) WindowsBase.dll!System.Windows.DependencyObject.ProvideSelfAsInheritanceContext(System.Windows.DependencyObject doValue, System.Windows.DependencyProperty dp)
Microsoftのリファレンスソースを読めば、DependencyObjectがFreezableだけ特別扱いして、継承属性を持つコンテキストをまるっと引き渡していることが読めるはずです。
とりあえず、「仕掛けがわからない」は解消できたのではないでしょうか。
問題は、Microsoftがこのあたりをprivateやinternalで固めているため、いつ何時実装を変更しても文句を言えないというところでしょう。
それにしても、これでは XAMLで扱うものは、FrameworkElement あたりを使わない場合は Freezable 一択ということのようですね。
ここまで書いておいてなんですが、何か大きな勘違いをしてないか不安です。
FreezableにDataContextが伝播する仕掛け(その1)
依存関係プロパティ(継承タイプ)を変更しますと。
このあたりをスタート地点としまして。
http://referencesource.microsoft.com/#WindowsBase/Base/System/Windows/DependencyObject.cs,1243
長い処理を経てここに到達します。
http://referencesource.microsoft.com/#WindowsBase/Base/System/Windows/DependencyObject.cs,869
DependencyObject内にFreezableのために特別な処理が入っています。
具体的には継承する依存関係プロパティは、下位のFreezableなDependencyObjectに伝播するという仕掛けの様子。
継承する依存関係プロパティという概念は FrameworkElementからのはずですが、
ここでは、それとは別の継承関係に関する情報です。
http://referencesource.microsoft.com/#WindowsBase/Base/System/Windows/DependencyObject.cs,2786
FrameworkElementの中にFreezableがあるとDataContextがFreezableから参照できて、Bindingできるメカニズムが長いこと謎と言われていた気がするのですが、実はDependencyObjectのなかに何やらあった模様です。Microsoftは資料は出してないけど、リファレンスコードにはそれなりのヒントがあるっぽいです。
ただ、これで正解かは実際に動かしてないのでちょっと確認不足です。
Binding自体はFreezableはDependencyObjectを継承しているから特に問題はないはずですし、自分に届いたコンテキストのリストはFreezableにありますので、そこから欲しい値を取ってきてBindingしているのです。
・・・のだと思うのですが、やっぱりもう少し調べる必要はありそうです。
Binding.csのリファレンスコードあたりをよく読めば、多分このあたりの問題は解消できそうです。
今日は考えかけなので、また今度掘り下げますね。
いずれにしても、仕組みがわからないから放置は良くありません。
ベジェ曲線のHitTest
WPFにあまり任せてはいけないのがベジェ曲線のHitTestです。
几帳面で抜け目ありませんが、きわめて低速です。
真面目に方程式を解く手法ももちろんあるのですが、実はあんまり工夫しないでも、そこそこに優良なコードは書けます。
概ねこんなノリです。動かしてないので間違っていたらすみません。
/// <summary>
/// 矩形とベジェの交差判定
/// </summary>
/// <param name="v">Vecter[4]で、始点、制御点1、制御点2、終点</param>
/// <param name="rect">交差しているか判定したい範囲。スタックに山積みさせないために ref 。面積が0は不可</param>
/// <returns>矩形とヒットしている場合true</returns>
/// <remarks></remarks>
bool HitTestBezier(Vector[] v, ref Rect rect)
{
var p0 = new Point(v[0].X, v[0].Y);
var p3 = new Point(v[3].X, v[3].Y);
// 始点か終点を含んでいれば、Hitしている。
if (rect.Contains(p0) || rect.Contains(p3))
return true;
var area = new Rect(p0, p3);
area.Union(new Rect(new Point(v[1].X,v[1].Y),new Point(v[2].X,v[2].Y)));
// ここらへんで小細工の余地はあります。幅や高さが1未満だったら直線と大差ないとか、いろいろ。
// 交差している可能性があるケースを判定
if(rect.IntersectsWith(area))
{
var v01 = (v[0] + v[1]) / 2;
var v12 = (v[1] + v[2]) / 2;
var v23 = (v[2] + v[3]) / 2;
var v0112 = (v01 + v12) / 2;
var v1223 = (v12 + v23) / 2;
var c = (v0112 + v1223) / 2;
// 2分割してさらに探査
return HitTestBezier(new[] { v[0], v01, v0112, c }, ref rect)
|| HitTestBezier(new[] { c, v1223, v23, v[3] }, ref rect);
}
// 可能性が無い
return false;
}
とりあえず、WPFに丸任せするよりは高速です。お試しください。
長さが20億ピクセルくらいあるベジェ曲線でも最悪32段ほど潜ればちゃんと交差判定できます。小細工をすればもっと浅くできます。
これで、画面内に有るか無いか仮想化に役立てようという考えです。
MSを16倍出し抜くなんとか2回目
Msを16倍出し抜くwpf開発2回目
先日やった勉強会資料2回目です。
簡素な仮想化パネルの元資料です。
イベント集約という手法
一長一短あります。
本日はイベント集約のこと。
イベントは大変便利ですが、1つのイベントにどの程度のリスナーが居るかを正確に管理するのは大切です。
WPFを手本にすると、バインディングパスとレイアウトパスに関して見事なイベント集約がなされていることに気づきます。
依存関係プロパティを書き換えるとバインディングは非同期で最後の変更だけが伝わります。
バインディングが終わると、メタデータのオプションや、計測と配置の無効化フラグを見ながら、極力1回ずつだけレイアウトパスが走ります。
特定の処理が終わるまで、イベントの有無だけを蓄積しておく。これが大雑把にイベント集約というものです。
使いこなすと最適化の効果は絶大。
ですが、イベント集約はパスの順序について明確な設計を要します。また、集約したイベントが正確性を欠くと大変なことに。
今日もそのバグで大変なことに。
簡素な仮想化パネル
こんなXAML
<Window x:Class="Test.FastCanvas.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Test.FastCanvas"
Title="MainWindow" Height="350" Width="525">
<ScrollViewer x:Name="scroller" HorizontalScrollBarVisibility="Visible">
<local:FastCanvas x:Name="canvas" />
</ScrollViewer>
</Window>こんなコード(usingは省略してます)
namespace Test.FastCanvas
{
/// <summary>
/// MainWindow.xaml の相互作用ロジック
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
for (int y = 0; y < 1000; y++)
{
for (int x = 0; x < 100; x++)
{
var element = new Ellipse() { Width = 32, Height = 32, Fill = Brushes.LightPink };
element.MouseEnter += (s,e) =>{ ((Ellipse)s).Fill = Brushes.Blue; };
element.MouseLeave += (s, e) => { ((Ellipse)s).Fill = Brushes.LightPink; };
Canvas.SetLeft(element, x * 32);
Canvas.SetTop(element, y * 32);
canvas.AddElement(element);
}
}
canvas.Height = 32 * 1000;
canvas.Width = 32 * 100;
scroller.ScrollChanged += scroller_ScrollChanged;
}
void scroller_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
var rect = new Rect(
scroller.HorizontalOffset,
scroller.VerticalOffset,
scroller.ViewportWidth,
scroller.ViewportHeight);
canvas.SetViewport(rect);
}
}
}こんなコントロール(usingは省略してます)
namespace Test.FastCanvas
{
public class FastCanvas : Canvas
{
private HashSet<UIElement> _virtualChildren = new HashSet<UIElement>();
public void AddElement(UIElement element)
{
_virtualChildren.Add(element);
}
public void RemoveElement(UIElement element)
{
_virtualChildren.Remove(element);
}
public void SetViewport(Rect rect)
{
foreach(FrameworkElement child in _virtualChildren)
{
var childRect = new Rect(Canvas.GetLeft(child), Canvas.GetTop(child), child.Width, child.Height);
if (!rect.IntersectsWith(childRect))
{
if (Children.Contains(child))
Children.Remove(child);
}
else
{
if (!Children.Contains(child))
Children.Add(child);
}
}
}
}
}分かり易く仮想化を説明する教材として作ったものです。
ScrollChangedを使うと古風なので(あとレイアウトパスが過剰に走って遅いので)、データバインディングでViewportを設定できるようにするとMVVM風ですよね。マルチバリューコンバーターの出番です。
起動時間と、マウスヒットテストは仮想化の有無だけで相当違います。それを体感するためのサンプルと思っていただけたら。
ちなみに、同じコードでFastCanvasをCanvasに入れ替え、ScrollChangedのイベントをやめるだけで、仮想化自体の威力は体感できると思います。
実は最近、Visualを細切れにしたらいいんじゃないかと思い
最近まで、よりも速い仮想化パネルを作るということに心血注いでいて一段落ついたところなのですが、まだいくつかやり残したことがあります。
というのは、あるチューニング中の出来事で、再描画領域がパネルの左上から右下まで突き抜けているベジェを変更するときに、極限まで再描画範囲を削ってみたら、画面にWPFらしからぬごみが残る現象が発生しました。
そこでふと思ったのです。
ビジュアルツリー自体は配下のどの範囲が書き換えられるかというところにはすごく厳しいのですが、じゃあ、隣接域を侵害しないように細切れに物を配置してみたらどうだろうと。
仮想化としては、その細切れを根元から着脱・・・・。
もしかして、今よりもっと速い方法につながりうるのではないかと思ったのです。
ただ、用心が必要なのは、見かけのコントロール1つが細切れになって10くらいのVisualになれば、メモリは消費する可能性が高いし、・・・・どっちがいいんでしょう。
次回の開発に入る前に、1日くらい時間を取らせてもらって、そういう実装の速度を試験してみることも必要かなと思った次第です。
ペンはブラシに負けます
ペンは仕掛けが複雑なので、図形を書く際には縁が要らないならブラシだけで書きましょう。場合によっては、四角形を回転拡縮して描いた方が速い事すら有ります。やや極端ですが。
余談ですが、最近短いのが多いのは、スマホ端末から書いているからです。