이전글 을 참고 하자 (http://scripter.co.kr/274)
이전 버전은 쓰레드 안에서 View 를 생성하여 while 루프 안에서 RenderTargetBitmap(이하 RTB) 로 찍어 내는 방식이었다.
하지만 쓰레드 안에서 생성한 Element 는 CompositionTarget 등 Dispathcer 관련 서비스를 이용 할 수 없었다.
그래서 전역 메모리 공유 (멤버 변수) 를 통하여 좌표 라던지 크기 등 GUI 요소에 간접 접근 하는 방법 밖에 없었다.
직접 접근 하려면 , 아래와 같은 에러를 볼 수 있다.

그래서 앞서 설명 한 것 처럼 간접 접근을 통하여 while 루프에서 변경을 체크 하여 적용 할 수 밖에 없었는데, 이러한 점 때문에 약간의 딜레이를 감수 해야 했었다.
그리고 이러한 점을 개선 한 방법을 기록한다.
원문 : http://blogs.msdn.com/b/dwayneneed/archive/2007/04/26/multithreaded-ui-hostvisual.aspx
우선 아래와 같은 클래스가 필요하다. VisualTargetPresentationSource.cs
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Data;
using System.Windows.Controls;
using System.Windows.Markup;
namespace JUtil
{
/// original post : http://blogs.msdn.com/b/dwayneneed/archive/2007/04/26/multithreaded-ui-hostvisual.aspx
///
///
///
/// <summary>
/// The VisualTargetPresentationSource represents the root
/// of a visual subtree owned by a different thread that the
/// visual tree in which is is displayed.
/// </summary>
/// <remarks>
/// A HostVisual belongs to the same UI thread that owns the
/// visual tree in which it resides.
///
/// A HostVisual can reference a VisualTarget owned by another
/// thread.
///
/// A VisualTarget has a root visual.
///
/// VisualTargetPresentationSource wraps the VisualTarget and
/// enables basic functionality like Loaded, which depends on
/// a PresentationSource being available.
/// </remarks>
public class VisualTargetPresentationSource : PresentationSource
{
public VisualTargetPresentationSource(HostVisual hostVisual)
{
_visualTarget = new VisualTarget(hostVisual);
}
public override Visual RootVisual
{
get
{
return _visualTarget.RootVisual;
}
set
{
Visual oldRoot = _visualTarget.RootVisual;
// Set the root visual of the VisualTarget. This visual will
// now be used to visually compose the scene.
_visualTarget.RootVisual = value;
// Hook the SizeChanged event on framework elements for all
// future changed to the layout size of our root, and manually
// trigger a size change.
FrameworkElement rootFE = value as FrameworkElement;
if (rootFE != null)
{
rootFE.SizeChanged += new SizeChangedEventHandler(root_SizeChanged);
rootFE.DataContext = _dataContext;
// HACK!
if (_propertyName != null)
{
Binding myBinding = new Binding(_propertyName);
myBinding.Source = _dataContext;
rootFE.SetBinding(TextBlock.TextProperty, myBinding);
}
}
// Tell the PresentationSource that the root visual has
// changed. This kicks off a bunch of stuff like the
// Loaded event.
RootChanged(oldRoot, value);
// Kickoff layout...
UIElement rootElement = value as UIElement;
if (rootElement != null)
{
rootElement.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
rootElement.Arrange(new Rect(rootElement.DesiredSize));
}
}
}
public object DataContext
{
get { return _dataContext; }
set
{
_dataContext = value;
var rootElement = _visualTarget.RootVisual as FrameworkElement;
if (rootElement != null)
{
rootElement.DataContext = _dataContext;
}
}
}
// HACK!
public string PropertyName
{
get { return _propertyName; }
set
{
_propertyName = value;
var rootElement = _visualTarget.RootVisual as TextBlock;
if (rootElement != null)
{
if (!rootElement.CheckAccess())
{
throw new InvalidOperationException("What?");
}
Binding myBinding = new Binding(_propertyName);
myBinding.Source = _dataContext;
rootElement.SetBinding(TextBlock.TextProperty, myBinding);
}
}
}
public event SizeChangedEventHandler SizeChanged;
public override bool IsDisposed
{
get
{
// We don't support disposing this object.
return false;
}
}
protected override CompositionTarget GetCompositionTargetCore()
{
return _visualTarget;
}
private void root_SizeChanged(object sender, SizeChangedEventArgs e)
{
SizeChangedEventHandler handler = SizeChanged;
if (handler != null)
{
handler(this, e);
}
}
private VisualTarget _visualTarget;
private object _dataContext;
private string _propertyName;
}
/// <summary>
/// The VisualWrapper simply integrates a raw Visual child into a tree
/// of FrameworkElements.
/// </summary>
[ContentProperty("Child")]
public class VisualWrapper<T> : FrameworkElement where T : Visual
{
public T Child
{
get
{
return _child;
}
set
{
if (_child != null)
{
RemoveVisualChild(_child);
}
_child = value;
if (_child != null)
{
AddVisualChild(_child);
}
}
}
protected override Visual GetVisualChild(int index)
{
if (_child != null && index == 0)
{
return _child;
}
else
{
throw new ArgumentOutOfRangeException("index");
}
}
protected override int VisualChildrenCount
{
get
{
return _child != null ? 1 : 0;
}
}
private T _child;
}
/// <summary>
/// The VisualWrapper simply integrates a raw Visual child into a tree
/// of FrameworkElements.
/// </summary>
public class VisualWrapper : VisualWrapper<Visual>
{
}
}
그리고 렌더링 할 UserControl 에 아레와 같은 코드를 넣는다.
주요 기능은 DispatcherTimer 로 지속적으로 화면을 캡처 하여 비트맵으로 만든다.
이때 CompositionTarget 보다 DispatcherTimer가 더욱 지속적이고 처리 속도도 원활 했다.
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;
using Tracker.util;
namespace CrossUiThreading
{
/// <summary>
/// Interaction logic for ThreadView.xaml
/// </summary>
public partial class ThreadView : UserControl
{
//test props
public static ThreadView _this;
public static Bitmap _bitmap;
public static BitmapSource _bsrc;
public static int _updateCount = 0;
public static Planerator.Planerator PL2;
public static int _fps;
private System.Windows.Media.Imaging.RenderTargetBitmap capture;
private FPSCounter _fpsCounter = new FPSCounter();
public ThreadView()
{
InitializeComponent();
_this = this;
this.Loaded += ThreadView_Loaded;
}
void ThreadView_Loaded(object sender, RoutedEventArgs e)
{
capture = new System.Windows.Media.Imaging.RenderTargetBitmap((int)this.ActualWidth, (int)this.ActualHeight, 96, 96, System.Windows.Media.PixelFormats.Default);
this.Arrange(new System.Windows.Rect(0, 0, 1320, 320));
DispatcherTimer timer = new DispatcherTimer();
timer.Interval = TimeSpan.FromTicks(100);
timer.Tick += timer_Tick;
timer.Start();
PL2 = _pl2;
}
void timer_Tick(object sender, EventArgs e)
{
Render();
}
float c = 0;
private void Render()
{
//test move
double v = 500 + Math.Cos(c) * 100;
c += 0.1f;
Canvas.SetLeft(_thumb, v);
_p1.RotationX = v;
//bitmap capture
capture.Render(this);
_bitmap = BitmapConvert.BsrcToBitmap(capture);
_fps = _fpsCounter._CalculateFrameRate();
_updateCount++;
}
}
}
마지막으로 활용이다.
하단에 보면 Invoke 가 가능함을 테스트 할 수 있는 코드가 있다.
using JUtil;
using System;
using System.Threading;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using Tracker.util;
namespace CrossUiThreading
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private static AutoResetEvent s_event = new AutoResetEvent(false);
private FPSCounter _fpsConter = new FPSCounter();
private int old_count = 0;
public MainWindow()
{
InitializeComponent();
this.Loaded += MainWindow_Loaded;
}
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
this.Loaded -= MainWindow_Loaded;
CreateMediaElementOnWorkerThread();
CompositionTarget.Rendering += CompositionTarget_Rendering;
}
void CompositionTarget_Rendering(object sender, EventArgs e)
{
this.Title = string.Format("{0} , {1}", _fpsConter._CalculateFrameRate() , ThreadView._fps);
if (old_count != ThreadView._updateCount)
{
System.Drawing.Bitmap bmp = ThreadView._bitmap;
if (bmp == null)
return;
_img.Source = BitmapConvert.ToBitmapSourceForGDI(bmp);
}
old_count = ThreadView._updateCount;
}
private HostVisual CreateMediaElementOnWorkerThread()
{
HostVisual hostVisual = new HostVisual();
Thread thread = new Thread(new ParameterizedThreadStart(MediaWorkerThread));
thread.SetApartmentState(ApartmentState.STA);
thread.IsBackground = true;
thread.Start(hostVisual);
s_event.WaitOne();
return hostVisual;
}
private void MediaWorkerThread(object arg)
{
HostVisual hostVisual = (HostVisual)arg;
VisualTargetPresentationSource visualTargetPS = new VisualTargetPresentationSource(hostVisual);
s_event.Set();
ThreadView threadview = new ThreadView();
visualTargetPS.RootVisual = threadview;//CreateMediaElement();
System.Windows.Threading.Dispatcher.Run(); //중요!
}
/*
* 다른 스레드에서 속성 접근 가능
*
* */
private void Window_KeyUp_1(object sender, KeyEventArgs e)
{
if (Key.A == e.Key)
{
//별도 쓰레드에서 생성한 객체의 Dispatcher.Invoke 가능!
ThreadView._this.Dispatcher.Invoke(new Action(()=>{
ThreadView.PL2.RotationZ++;
}));
}
//에러!
//ThreadView.PL2.RotationZ++;
}
}
}
이로서 다른 쓰레드에서 생성한 GUI 요소 일 지라도 Dispatcher 로 가능하다.
System.Windows.Threading.Dispatcher.Run();
이것을 성공시키기 위한 아주 중요한 부분이다.
프로젝트 :
CrossUiThreading.zip
댓글을 달아 주세요