Страницы

Поиск по вопросам

воскресенье, 9 июня 2019 г.

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

В фоновом потоке формирую матрицу случайными числами:
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 { ///

/// Interaction logic for MainWindow.xaml /// public partial class MainWindow : Window { const int mh = 200; const int mw = 100; bool Stop = true; DrawingVisual visual; List vl = new List();
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 visuals = new List(); 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


Ответ

Нет, так, как вы хотите, нельзя: все 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 Step(Random r) { const int mh = 200; const int mw = 100;
List vl = new List();
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 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(); }

Комментариев нет:

Отправить комментарий