次の方法で共有


WPF 描画の経過を逐次表示したい

質問

2016年4月29日金曜日 7:15

 WPFで、Canvasに数多くのグラフィックオブジェクトを描画し、その経過をスローモーションで表示させたいと思っています。例として、10万本のランダムなサイズのLineを0.1秒ごとに描画するという場合です。

この様に考えていますが、画面が更新されません。

   Private Sub MainWindow_Loaded(sender As Object, e As System.Windows.RoutedEventArgs) Handles Me.Loaded

        While (1)
            Me.Label1.Content = Now
            System.Threading.Thread.Sleep(100)
        End While

    End Sub

ーーーーーーーーーーー

この様にする方法もあるらしいのですが、処理中にこれを挟んでで実行しても画面が更新されません。

    Public Shared Sub doevent()
        Dim Frame = New DispatcherFrame
        Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background, New DispatcherOperationCallback(AddressOf ExitFrames), Frame)
        Dispatcher.PushFrame(Frame)
    End Sub

    Public Shared Function ExitFrames(frames As Object) As Object
        DirectCast(frames, DispatcherFrame).Continue = False
        Return Nothing
    End Function

御回答をお願い致します。

すべての返信 (6)

2016年4月29日金曜日 10:00 | 1 票

BeginInvokeではなくInvokeを使うといいです。
BeginInvokeは、メインスレッドで現在行わせている処理を抜けた後に、指定した処理をPriorityで指定した優先度で実行します。
Invokeは、現在行わせている処理を中断して、指定した処理をPriorityで指定した優先度で実行したのち、中断していた処理が続行されます。
処理の途中でWPFが描画するよりも低い優先度のダミー処理のInvokeを挟むことで、描画が終わったら続けて処理を行うことができます。

他の方法としてAsync/Awaitを使う方法もあります。

<Window x:Class="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">
    <DockPanel>
        <Button Click="Button2_Click" Content="Test2" DockPanel.Dock="Bottom" />
        <Button Click="Button1_Click" Content="Test1" DockPanel.Dock="Bottom" />
       
        <Canvas x:Name="canvas1" ClipToBounds="true"/>
    </DockPanel>
</Window>
Class MainWindow 
    Private Sub Button1_Click(sender As Object, e As RoutedEventArgs)
        Me.canvas1.Children.Clear()

        Dim nextTime As DateTime = DateTime.Now.AddSeconds(0.1)
        Dim x = Me.canvas1.ActualWidth / 2
        Dim y = Me.canvas1.ActualHeight / 2
        Dim cx As Double = x
        Dim cy As Double = y
        Dim rmax = Math.Min(Me.canvas1.ActualWidth / 2, Me.canvas1.ActualHeight / 2)
        Dim a As Double = 0

        For i = 0 To 1000
            Dim l As Line = New Line()
            l.Stroke = Brushes.Pink
            l.X1 = x
            l.Y1 = y
            Dim r = i / 1000.0 * rmax
            l.X2 = r * Math.Sin(i) ; cx
            l.Y2 = r * Math.Cos(i) ; cy

            canvas1.Children.Add(l)
            System.Threading.Thread.Sleep(1) '描画に時間がかかっているように見せるためのウェイト

            If (DateTime.Now >= nextTime) Then
                nextTime = DateTime.Now.AddSeconds(0.1)
                Me.Dispatcher.Invoke(Sub()
                                         '描画処理を更新させるためのダミー
                                     End Sub, Windows.Threading.DispatcherPriority.Loaded)
            End If
            x = l.X2
            y = l.Y2
        Next
    End Sub

    Private Async Sub Button2_Click(sender As Object, e As RoutedEventArgs)
        Me.canvas1.Children.Clear()

        Dim nextTime As DateTime = DateTime.Now.AddSeconds(0.1)
        Dim x = Me.canvas1.ActualWidth / 2
        Dim y = Me.canvas1.ActualHeight / 2
        Dim cx As Double = x
        Dim cy As Double = y
        Dim rmax = Math.Min(Me.canvas1.ActualWidth / 2, Me.canvas1.ActualHeight / 2)
        Dim a As Double = 0
        For i = 0 To 1000
            Dim l As Line = New Line()
            l.Stroke = Brushes.LightBlue
            l.X1 = x
            l.Y1 = y
            Dim r = i / 1000.0 * rmax
            l.X2 = r * Math.Sin(i) ; cx
            l.Y2 = r * Math.Cos(i) ; cy

            canvas1.Children.Add(l)
            System.Threading.Thread.Sleep(1) '描画に時間がかかっているように見せるためのウェイト

            If (DateTime.Now >= nextTime) Then
                nextTime = DateTime.Now.AddSeconds(0.1)
                Await Task.Delay(1)
            End If

            x = l.X2
            y = l.Y2
        Next
    End Sub
End Class

InvokeはPriorityをInputよりも高くしておかないとボタンクリックで別の処理が走ってしまうので注意。
Async/AwaitだとTaskDelayで待機中にボタンクリックで別の処理が走ってしまうので注意。

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


2016年4月29日金曜日 12:25

 御回答ありがとうございます。こちらで作成中のプログラムに埋め込んで実験してみましたが、

  System.Threading.Thread.Sleep(100) の部分を書き換えて、停止させるステップを100ms刻み等、長くすると全ての描画が終わるまでCanvas1に何も表示されず、いきなり一瞬で全ての描画が完了してしまうので、途中経過を観察したデバッグができないのです。

下のサンプルプログラムで、  Canvas1.Children.Add(rt)の後に確実に描画を止めて、確認したいです。
どのようにしたら、良いでしょうか。
(御教示いただいたコードを改造させていただきました。)

なお、asyncを使ったコードについては、私のVS2010ではコンパイルできず、動作が確認できませんでした。

Private Sub Button1_Click(sender As Object, e As RoutedEventArgs) Handles Button1.Click
        Me.Canvas1.Children.Clear()

        Dim nextTime As DateTime = DateTime.Now.AddSeconds(0.1)
        Dim i As Integer
        Dim rt As Rectangle

        For i = 0 To 200 Step 14
            rt = New Rectangle
            rt.Width = i
            rt.Height = 20
            rt.Fill = Brushes.Blue

            Canvas1.Children.Add(rt)

            System.Threading.Thread.Sleep(100) '描画に時間がかかっているように見せるためのウェイト

            If (DateTime.Now >= nextTime) Then
                nextTime = DateTime.Now.AddSeconds(0.1)
                Me.Dispatcher.Invoke(Sub()
                                         '描画処理を更新させるためのダミー
                                     End Sub, Windows.Threading.DispatcherPriority.Loaded)
            End If
        Next

    End Sub


2016年4月29日金曜日 12:50

Visual Basic 2012 (VB.NET 11) 以降が使えるかどうかが不明なときは、うかつにAsync/Awaitを使用したコードは提示しないほうがよいと思われます。逆に言うと質問者のほうは質問する際に少なくともコンパイラのバージョンくらいはきちんと提示するべきですが……

ファイル読み書きや画像処理など、本当にヘビーな処理をサブスレッド上で徐々に実行して、進捗をプログレスバーで表示するとかいう目的であれば非同期処理をするのがセオリーですが、単に時間経過とともに徐々に表示させるというアニメーションが目的であれば、タイマーで十分かと思われます。 Async/Awaitを過度に使うと、スレッドのコンテキストスイッチが頻繁に発生するせいでレスポンスに影響を及ぼすこともありますので。特に初心者相手であれば、まずシングルスレッドでなんとかできる方法を提示したほうがよいと思います。

MainWindow.xaml:

<Window
    x:Class="MainWindow"
    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:WpfCanvasAnimTest1"
    mc:Ignorable="d"
    Title="MainWindow"
    WindowStartupLocation="CenterScreen"
    SizeToContent="WidthAndHeight"
    ResizeMode="CanMinimize"
    Loaded="Window_Loaded"
    >
    <Canvas Name="canvas1" Width="400" Height="400">
    </Canvas>
</Window>

MainWindow.xaml.vb:

Option Strict On
Imports System.Windows.Threading

Class MainWindow
    Dim dispatcherTimer As New DispatcherTimer()
    Dim linesList As New List(Of Line)
    Dim counter As Integer = 0
    ' 10万要素の生成と追加はかなり重い。
    'Const TotalCount As Integer = 100 * 1000
    Const TotalCount As Integer = 10 * 1000
    'Const TotalCount As Integer = 30

    Sub New()
        InitializeComponent()
        Dim width = Me.canvas1.Width
        Dim height = Me.canvas1.Height
        Dim rnd = New Random()
        For x As Integer = 0 To TotalCount - 1
            Dim line = New Line()
            line.Stroke = Brushes.Crimson
            line.StrokeThickness = 2
            line.Opacity = 0
            line.X1 = rnd.NextDouble() * width
            line.X2 = rnd.NextDouble() * width
            line.Y1 = rnd.NextDouble() * height
            line.Y2 = rnd.NextDouble() * height
            Me.linesList.Add(line)
            Me.canvas1.Children.Add(line)
        Next
        Me.dispatcherTimer.Interval = TimeSpan.FromMilliseconds(100)
        AddHandler Me.dispatcherTimer.Tick, AddressOf dispatcherTimer_Tick
        ' Space キーで停止/再開。
        AddHandler Me.KeyDown, Sub(s, e)
                                   If e.Key = Key.Space Then
                                       If Me.dispatcherTimer.IsEnabled Then
                                           Me.dispatcherTimer.Stop()
                                       Else
                                           Me.dispatcherTimer.Start()
                                       End If
                                   End If
                               End Sub
    End Sub

    Private Sub dispatcherTimer_Tick(sender As Object, e As EventArgs)
        Me.counter ;= 1
        If Me.counter > TotalCount - 1 Then
            Me.counter = TotalCount - 1
        End If
        If 0 <= Me.counter And Me.counter < Me.linesList.Count() Then
            Dim line = Me.linesList(Me.counter)
            line.Opacity = 1
        End If
    End Sub

    Private Sub Window_Loaded(sender As Object, e As RoutedEventArgs)
        Me.dispatcherTimer.Start()
    End Sub
End Class

なおImmediateモードのDirect3D/Direct2Dとは違い、WPFはRetainedモードを主軸に設計されているので、大量のデータを表示する際は描画手法を十分に検討する必要があるかと。最近のグラフィックスハードウェアであればDirect3D/OpenGLによる10万ポリライン描画など朝飯前ですが、WPFでの10万個のUIElement生成と表示はかなりヘビーなので、単純なライン描画としてはオーバースペックなSystem.Windows.Shapes.Lineを使うより、もう少し軽量な方法を考えたほうがよいと思います。

WPF Charting Performance Comparisons (the Battle Continues)


2016年4月30日土曜日 3:54

syghさん、御回答ありがとうございます。

私の説明も悪かったかもしれませんが、経過を逐次表示したいというのはCanvas上へのグラフィックオブジェクトの書き換えであって、Lineオブジェクトを追加するだけの目的ではありません。今組んでいるプログラムについて説明させていただくと、Canvasにオブジェクト(モーフィングしながら変化する)を表示し、100ms待ち、Canvasを消去するという処理の繰り返しなのですが、余りにも処理が早すぎて途中経過が観察できないのを問題にしています。
お答えいただいた方法では、オブジェクト透明にして先に全て描画完了し、後から一つづつ表示させていますね。指定したWaitで正確にステップ表示されることは確認できましたが、手法違いで使えませんでした。しかし、一つ一つのオブジェクトのサイズが小さいような場合には利用価値はあると思います。


2016年4月30日土曜日 6:33

情報を出し惜しみするのはやめましょう。回答者はエスパーではありません。ロクな前提条件も提示・説明せずに、まともな回答が得られると思いますか? 少なくとも現在問題となっているプログラムの内容をきちんと提示・説明するくらいのことはするべきです。なお「私の説明悪かったかもしれませんが」というのは、ある意味「こんなことも推測できないのか」ということを暗に含んでいる侮辱だと思います。少なくともフォーラムでは、1を聞いて10を知れ、という無茶振りはやめてください。人に分かりやすく説明する能力というのは、あらゆる場面で必要とされるスキルです。説明を理解できないほうがおかしい、という考え方は厳につつしんでください。

お説教はこのあたりにして本題に戻りますが、要するにCanvasに要素を徐々に追加することだけでなく、既存のすべての要素の状態(位置や色など?要説明)を徐々に変化させることも必要、ということですか?

であれば、やはり普通にタイマーもしくはSystem.Windows.Media.CompositionTarget.Renderingイベントを使って、100ミリ秒よりもずっと短い間隔で定期的にCanvas内のすべての要素の状態を時間遷移とともに書き換えていけばよいだけではないでしょうか。WPF云々よりも、単にリアルタイムアニメーションプログラミングの基礎的な内容にすぎないと思われます。提示したサンプルでOpacityを制御しているのはあくまで一例にすぎず、実際に時間経過とともに何をどうするかは自分の頭で考えて応用・工夫してください。

一般的にリアルタイムコンピュータグラフィックスというのは、最終的に画面に表示されるまでの途中経過(個別の描画命令の実行結果)をインタラクティブに見せるようなことはせず、「裏画面(バックバッファ/セカンダリバッファ)への描画がすべて完了してから表画面(フロントバッファ/プライマリバッファ)に一気に転送する」という仕組みになっています。WPFに限らず、たとえばゲームやシミュレーションなどのリアルタイム・インタラクティブ系では、オブジェクトの生成・破棄・状態変更やオブジェクト間の衝突判定、ユーザー入力の判定などのオフスクリーン処理を実行した後にフレーム描画とフレームバッファ転送を実行する、という一連のフレーム処理が秒間60回ほど繰り返し行なわれます。決して描画要素の状態を変更したタイミングで即座に描画が行なわれるわけではありません。それこそモーフなどはゲームやアニメなどで頻繁に使われますが、タイムラインに沿って遷移・補間の途中経過を見せるためには当然フレームごとに状態を少しずつ変化させ、その状態に応じたレンダリングを行なう工夫が必要となります。どうも基礎や原理を理解せずに盛大に勘違いしたまま進めてしまっているように見受けられるため、まずはリアルタイムコンピュータグラフィックスの基礎と、アニメーションの基礎を勉強されたほうがよいと思います。Adobe Flashなどのアニメーションツールや、Blender/LightWaveなどの統合型3DCGツールを持っているのであれば、それらを使ってみるのもよいでしょう。場合によってはDirect3D/OpenGLを直接叩いてみて勉強するのもアリだと思います。いずれにせよ、プログラミング初心者であるにもかかわらずWPFだけしかやったことがないのであれば、視野狭窄に陥り、物事の本質や原理を見失う可能性が非常に高いです。

なお、もし既存の要素の状態を遷移させるのであれば、System.Windows.Media.Animation.Storyboardを使ってアニメーションさせる手法もありますが、10万要素となるとパフォーマンス的に厳しくなると思われます。また、タイマーの最小時間分解能には限界があり、System.Windows.Threading.DispatcherTimerの場合はせいぜい20ミリ秒くらいまでだと考えておいたほうがよいです。そのほか、DispatcherTimer.TickイベントやCompositionTarget.Renderingイベントにて、度を超えた重い処理を実行すると、アプリケーションの応答性が低下する羽目になります。しかしこのあたりの感覚も、フレームレートを明示的に制御するプログラムの実装経験がなければピンとこないでしょう。


2016年4月30日土曜日 10:46

このフォーラムは、多くの人が共通に悩む、或いはそうなり易いテーマについて討論する場所ですから、一個人が開発中のプロジェクトに対してどこまでも詳細な情報を提示するというのは、誤った方向であると考えていますし、私の質問も抽象的だったということは初学者であるということでお許しいただきたいと思います。もう返答は結構です。

syghさん以外の方からの回答を、お待ちしております。