[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

댓글을 달아 주세요

  1. BlogIcon Michael Kors Handbags 2015.02.05 04:43  수정/삭제  댓글쓰기

    일를Michael Kors Outet Online
    Michael Kors Handbags
    Michael Kors Outet Online
    Michael Kors Outet
    Michael Kors Purses
    Michael Kors Outet Online
    Michael Kors Outet Online
    Michael Kors Purses
    Michael Kors Handbags
    Michael Kors Outet를일

Powerd by Tistory, designed by criuce
rss