DrawingVisual в фоновом потоке С# WPF 3.5

231
29 января 2018, 04:00

В фоновом потоке формирую матрицу случайными числами:

public void Step()
{
    Point p;
    Brush b;
    Random r = new Random();
    for (int i = 0; i < mh; i++)
    {
        for (int j = 0; j < mw; j++)
        {
            p = new Point(i*5, j*5);
            int ch = r.Next(0, 100);
            if (ch <= 33)
            {
                b = Brushes.Green;
                vl.Add(new VisualList(p, b));
            }
            else if (ch > 66)
            {
                b = Brushes.Red;
                vl.Add(new VisualList(p, b));
            }
            else
            {
                b = Brushes.Yellow;
                vl.Add(new VisualList(p, b));
            }
        }
    }
}

И пытаюсь ее отрисовать с помощью DrawingVisual:

private void Print()
{
    visual = new DrawingVisual();
    using (DrawingContext dc = visual.RenderOpen())
    {
        for (int i = 0; i < vl.Count; i++)
        {
            VisualList vlist = vl[i];
            Brush brush = Brushes.Black;
            dc.DrawRectangle(vlist.Brushd, null, new Rect(vlist.Pointd, new Size(4, 4)));
        }
    }
    this.Dispatcher.BeginInvoke((Action)(() =>
    {
        drawingSurface.AddVisual(visual);
    }));
}

В итоге получаю ошибку: Необработанное исключение типа "System.Reflection.TargetInvocationException" в mscorlib.dll

Дополнительные сведения: Адресат вызова создал исключение. Как из фонового потока рисовать на форме?

Полный код

using System;
using System.Collections.Generic;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;
namespace matrix
{
/// <summary>
///     Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
    const int mh = 200;
    const int mw = 100;
    bool Stop = true;
    DrawingVisual visual;
    List<VisualList> vl = new List<VisualList>();
    public MainWindow()
    {
        InitializeComponent();
    }
    private void TestWindow_Loaded(object sender, RoutedEventArgs e)
    {
        Thread trMatrix = new Thread(Draw);
        trMatrix.SetApartmentState(ApartmentState.STA);
        trMatrix.Start();
    }

    private void Draw()
    {
        while (Stop)
        {
            Step();
            Print();
        }
    }
    private void Print()
    {
        visual = new DrawingVisual();
        using (DrawingContext dc = visual.RenderOpen())
        {
            for (int i = 0; i < vl.Count; i++)
            {
                VisualList vlist = vl[i];
                Brush brush = Brushes.Black;
                dc.DrawRectangle(vlist.Brushd, null, new Rect(vlist.Pointd, new Size(4, 4)));
            }
        }
        this.Dispatcher.BeginInvoke((Action)(() =>
        {
            drawingSurface.AddVisual(visual);
        }));
    }
    public void Step()
    {
        Point p;
        Brush b;
        Random r = new Random();
        for (int i = 0; i < mh; i++)
        {
            for (int j = 0; j < mw; j++)
            {
                p = new Point(i*5, j*5);
                int ch = r.Next(0, 100);
                if (ch <= 33)
                {
                    b = Brushes.Green;
                    vl.Add(new VisualList(p, b));
                }
                else if (ch > 66)
                {
                    b = Brushes.Red;
                    vl.Add(new VisualList(p, b));
                }
                else
                {
                    b = Brushes.Yellow;
                    vl.Add(new VisualList(p, b));
                }
            }
        }
    }
}
public class VisualList
{
    public VisualList(Point pointd, Brush brushd)
    {
        Pointd = pointd;
        Brushd = brushd;
    }
    public Point Pointd
    {
        get;
        set;
    }
    public Brush Brushd
    {
        get;
        set;
    }
}
}

Код класса рисования

using System.Collections.Generic;
using System.Windows.Controls;
using System.Windows.Media;
namespace TestDifferentWpf
{
class DrawingClass : Canvas
{
    private List<Visual> visuals = new List<Visual>();
    protected override int VisualChildrenCount
    {
        get
        {
            return visuals.Count;
        }
    }
    protected override Visual GetVisualChild(int index)
    {
        return visuals[index];
    }
    public void AddVisual(Visual visual)
    {
        visuals.Add(visual);
        base.AddVisualChild(visual);
        base.AddLogicalChild(visual);
    }
    public void DeleteVisual(Visual visual)
    {
        visuals.Remove(visual);
        base.RemoveVisualChild(visual);
        base.RemoveLogicalChild(visual);
    }
}
}

XAML

<Window x:Name="TestWindow" x:Class="matrix.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:TestDifferentWpf"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525" Loaded="TestWindow_Loaded">
<Grid Name="gTest">
    <local:DrawingClass x:Name="drawingSurface" Background="White" 
ClipToBounds="True"/>
</Grid>
</Window>
Answer 1

Нет, так, как вы хотите, нельзя: все FrameworkElement'ы, которые добавляются друг в друга как Child'ы, должны принадлежать одному потоку.

Поэтому так просто рисовать из фонового потока не получится.

Обычно никто не заморачивается и рисует в главном потоке. Но если очень хочется, вам придётся освоить PresentationSource и VisualTarget.

PresentationSource — это штука, позволяющая вставить в визуальное дерево какую-то совершенно чужую вещь. А VisualTarget позволяет соединять визуальные поддеревья, бегущие в разных потоках.

Вот примерная имплементация, которая производит рендеринг в фоновом потоке. Она сделана по этим источникам:

  • https://blogs.msdn.microsoft.com/dwayneneed/2007/04/26/multithreaded-ui-hostvisual/
  • http://gettinggui.com/creating-a-busy-indicator-in-a-separate-thread-in-wpf/

Итак, для начала, заводим нашу имплементацию PresentationSource:

// https://blogs.msdn.microsoft.com/dwayneneed/2007/04/26/multithreaded-ui-hostvisual/
// https://github.com/higankanshi/Meta.Vlc/blob/master/Meta.Vlc.Wpf
// /VisualTargetPresentationSource.cs
public class VisualTargetPresentationSource : PresentationSource, IDisposable
{
    public VisualTargetPresentationSource(HostVisual hostVisual)
    {
        _visualTarget = new VisualTarget(hostVisual);
        AddSource();
    }
    public override Visual RootVisual
    {
        get => _visualTarget.RootVisual;
        set
        {
            Visual oldRoot = _visualTarget.RootVisual;
            _visualTarget.RootVisual = value;
            RootChanged(oldRoot, value);
            if (value is UIElement rootElement)
            {
                rootElement.Measure(new Size(double.PositiveInfinity,
                                             double.PositiveInfinity));
                rootElement.Arrange(new Rect(rootElement.DesiredSize));
            }
        }
    }
    protected override CompositionTarget GetCompositionTargetCore() => _visualTarget;
    public override bool IsDisposed => _isDisposed;
    public void Dispose()
    {
        RemoveSource();
        _isDisposed = true;
    }
    private VisualTarget _visualTarget;
    private bool _isDisposed;
}

Остальную функциональность я упаковал в MainWindow, но её, вероятно, стоит разбить на вспомогательные классы.

Объяснение по коду. В конструкторе создаётся HostVisual, на который «наденется» VisualTarget через VisualTargetPresentationSource, и добавляется в визуальное дерево. Поскольку стандартные контролы не умеют добавлять Visual'ы, мы пользуемся DrawingClass из вопроса.

Далее, мы создаём STA-поток, в котором и будет происходить рендеринг поддерева, и запускаем его.

В потоке мы создаём VisualTargetPresentationSource. Затем, нам нужно сначала создать диспетчер, а потом выполнить на нём код, для этого используется трюк с InvokeAsync. (Dispatcher.Run() — блокирующая функция!)

В коде мы создаём Random (один раз, а не на каждой итерации), и в цикле вызываем функции Step и Draw из вопроса. Я немного переписал функции, чтобы они работали не с полями, а с параметрами. Ну и между итерациями я вставил Task.Delay, чтобы не гонять вечный холостой цикл.

Вот весь код:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        var hv = new HostVisual();
        drawingSurface.AddVisual(hv);
        Thread trMatrix = new Thread(() => Draw(hv)) { IsBackground = true };
        trMatrix.SetApartmentState(ApartmentState.STA);
        trMatrix.Start();
    }
    void Draw(HostVisual outerhv)
    {
        VisualTargetPresentationSource vtps = new VisualTargetPresentationSource(outerhv);
        var dispatcher = Dispatcher.CurrentDispatcher;
        dispatcher.InvokeAsync(async () =>
        {
            Random r = new Random();
            while (true)
            {
                var vl = Step(r);
                var visual = Print(vl);
                vtps.RootVisual = visual;
                await Task.Delay(50);
            }
        });
        Dispatcher.Run();
        vtps.Dispose();
    }
    public List<VisualList> Step(Random r)
    {
        const int mh = 200;
        const int mw = 100;
        List<VisualList> vl = new List<VisualList>();
        for (int i = 0; i < mh; i++)
        {
            for (int j = 0; j < mw; j++)
            {
                Point p = new Point(i * 5, j * 5);
                int ch = r.Next(0, 3);
                Brush b = ch == 0 ? Brushes.Green :
                          ch == 1 ? Brushes.Yellow :
                          Brushes.Red;
                vl.Add(new VisualList(p, b));
            }
        }
        return vl;
    }
    private Visual Print(List<VisualList> vl)
    {
        var visual = new DrawingVisual();
        using (DrawingContext dc = visual.RenderOpen())
        {
            for (int i = 0; i < vl.Count; i++)
            {
                VisualList vlist = vl[i];
                Brush brush = Brushes.Black;
                dc.DrawRectangle(vlist.Brushd, null,
                                 new Rect(vlist.Pointd, new Size(4, 4)));
            }
        }
        return visual;
    }
}

Получилось вот что:

На старой системе пользоваться async/await может быть сложно, вместо этого приходится использовать таймер:

public void Draw(HostVisual outerhv)
{
    VisualTargetPresentationSource vtps = new VisualTargetPresentationSource(outerhv);
    var dispatcher = Dispatcher.CurrentDispatcher;
    DispatcherTimer dt = new DispatcherTimer(DispatcherPriority.Normal, dispatcher)
    {
        Interval = TimeSpan.FromMilliseconds(50),
        IsEnabled = true
    };
    Random r = new Random();
    dt.Tick += (o, args) =>
    {
        var vl = Step(r);
        var visual = Print(vl);
        vtps.RootVisual = visual;
    };
    Dispatcher.Run();
    vtps.Dispose();
}
READ ALSO
Как создать кастомный шоткат для unity editor, который будет по сочетанию клавиш создавать активной папке проекта файл Assembly Definition?

Как создать кастомный шоткат для unity editor, который будет по сочетанию клавиш создавать активной папке проекта файл Assembly Definition?

Как создать кастомный шоткат для unity editor, который будет по сочетанию клавиш создавать активной папке проекта файл Assembly Definition ?

183
Открытие окон в mvvm-light

Открытие окон в mvvm-light

Как средствами MVVM Light открывать окна в приложении ? Допустим, нужно открыть окно, в которое вводятся данные для добавление нового пользователя...

212
Падающие символы

Падающие символы

Требуется написать программу которая будет выводит цепочку падающих символовПервый снизу символ должен быть белый, второй зеленый, остальные...

264
SQL естественное соединение

SQL естественное соединение

Возникла проблема с запросом естественного соединенияСоздал запрос при создании DataAdapter select * from table1 natural join table2, но команда dataAdapter

214