[WPF] 멀티쓰레딩 에서 GUI 렌더링과 그것의 속성을 외부에서 접근하기

이전글 을 참고 하자 (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



Yamecoder 야매코더_
C# 2015.01.13 21:47
Powerd by Tistory, designed by criuce
rss