0

バックグラウンド

RichTextBox基本的に、WinForms アプリケーションのコンソールのように使用しているコントロールがあります。現在、アプリケーション全体のロガーはデリゲートを使用してメッセージを投稿し、リスナーの 1 つがこの RTB です。ロガーは、イベント呼び出し、ステータス メッセージ、操作結果などを示す短い (100 文字未満) 文字列を多数同期的に送信します。

BeginInvoke を使用してこれらの短いメッセージを RTB に大量にポストすると、大量の並列処理によって大量のメッセージのログが開始され、UI が順不同でアイテムをポストし始めるか、テキストが大幅に遅れる (数百ミリ秒) まで、UI の応答性が提供されます。処理が遅くなったり停止したりすると、コンソールはしばらくの間、あとがきを書き続けるため、これを知っています。

私の一時的な解決策は、UI を同期的に呼び出し、ブロッキング コレクション バッファーを追加することでした。基本的に、Logger から多くの小さな項目を取得し、それらを stringbuilder で組み合わせて、RTB にまとめて投稿します。バッファは、UI が追いつくことができる場合はアイテムをポストしますが、キューが高くなりすぎると、アイテムを集約してから UI にポストします。このように、RTB は断片的に更新され、多くのものがログに記録されている場合は不安定に見えます。

質問

RichTextBox独自の UI スレッドでコントロールを実行して、Form頻繁ではあるが小さな追加操作中に他のボタンを同じ応答状態に保つにはどうすればよいですか? 調査の結果、RTB を独自のスレッドに配置するには、STA スレッドを実行してそれを呼び出す必要があると思いますApplication.Run()が、見つかった例には実質的なコード サンプルがなく、チュートリアルもないようです (おそらく、することはお勧めできませんか?)。また、フォームの残りの部分と比較して、単一のコントロールが独自のスレッドにあるという落とし穴があるかどうかもわかりませんでした。(つまり、メイン フォームを閉じる際に問題が発生したり、RTB の STA スレッドがフォームを閉じるだけで終了したりしますか?特別な処理はありますか?など)

Buttonこれは、フォームに 3と aを追加すると問題が発生することを示していますRichTextBox。私が本質的に達成したいのは、RTB を独自のスレッドに置くことによって BufferedConsumer を取り除くことです。このコードのほとんどは、私のメイン アプリケーションから逐語的にハッキングされたものです。

    using System;
    using System.Collections.Concurrent;
    using System.Diagnostics;
    using System.Drawing;
    using System.Runtime.InteropServices;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        // Fields
        private int m_taskCounter;
        private static CancellationTokenSource m_tokenSource;
        private bool m_buffered = true;
        private static readonly object m_syncObject = new object();

        // Properties
        public IMessageConsole Application_Console { get; private set; }
        public BufferedConsumer<StringBuilder, string> Buffer { get; private set; }

        public Form1()
        {
            InitializeComponent();

            m_tokenSource = new CancellationTokenSource();
            Application_Console = new RichTextBox_To_IMessageConsole(richTextBox1);

            Buffer =
                new BufferedConsumer<StringBuilder, string>(
                  p_name: "Console Buffer",
                  p_appendBuffer: (sb, s) => sb.Append(s),
                  p_postBuffer: (sb) => Application_Console.Append(sb));

            button1.Text = "Start Producer";
            button2.Text = "Stop All";
            button3.Text = "Toggle Buffering";

            button1.Click += (o, e) => StartProducerTask();
            button2.Click += (o, e) => CancelAllProducers();
            button3.Click += (o, e) => ToggleBufferedConsumer();
        }

        public void StartProducerTask()
        {
            var Token = m_tokenSource.Token;
            Task
                .Factory.StartNew(() =>
                {
                    var ThreadID = Interlocked.Increment(ref m_taskCounter);
                    StringBuilder sb = new StringBuilder();

                    var Count = 0;
                    while (!Token.IsCancellationRequested)
                    {
                        Count++;
                        sb.Clear();
                        sb
                            .Append("ThreadID = ")
                            .Append(ThreadID.ToString("000"))
                            .Append(", Count = ")
                            .AppendLine(Count.ToString());

                        if (m_buffered)
                            Buffer
                                .AppendCollection(sb.ToString()); // ToString mimicks real world Logger passing strings and not stringbuilders
                        else
                            Application_Console.Append(sb);

                        Sleep.For(1000);
                    }
                }, Token);
        }
        public static void CancelAllProducers()
        {
            lock (m_syncObject)
            {
                m_tokenSource.Cancel();
                m_tokenSource = new CancellationTokenSource();
            }
        }
        public void ToggleBufferedConsumer()
        {
            m_buffered = !m_buffered;
        }
    }

    public interface IMessageConsole
    {
        // Methods
        void Append(StringBuilder p_message);
    }

    // http://stackoverflow.com/a/5706085/1718702
    public class RichTextBox_To_IMessageConsole : IMessageConsole
    {
        // Constants
        private const int WM_USER = 0x400;
        private const int WM_SETREDRAW = 0x000B;
        private const int EM_GETEVENTMASK = WM_USER + 59;
        private const int EM_SETEVENTMASK = WM_USER + 69;
        private const int EM_GETSCROLLPOS = WM_USER + 221;
        private const int EM_SETSCROLLPOS = WM_USER + 222;

        //Imports
        [DllImport("user32.dll")]
        private static extern IntPtr SendMessage(IntPtr hWnd, Int32 wMsg, Int32 wParam, ref Point lParam);

        [DllImport("user32.dll")]
        private static extern IntPtr SendMessage(IntPtr hWnd, Int32 wMsg, Int32 wParam, IntPtr lParam);

        // Fields
        private RichTextBox m_richTextBox;
        private bool m_attachToBottom;
        private Point m_scrollPoint;
        private bool m_painting;
        private IntPtr m_eventMask;
        private int m_suspendIndex = 0;
        private int m_suspendLength = 0;

        public RichTextBox_To_IMessageConsole(RichTextBox p_richTextBox)
        {
            m_richTextBox = p_richTextBox;
            var h = m_richTextBox.Handle;

            m_painting = true;

            m_richTextBox.DoubleClick += RichTextBox_DoubleClick;
            m_richTextBox.MouseWheel += RichTextBox_MouseWheel;
        }

        // Methods
        public void SuspendPainting()
        {
            if (m_painting)
            {
                m_suspendIndex = m_richTextBox.SelectionStart;
                m_suspendLength = m_richTextBox.SelectionLength;
                SendMessage(m_richTextBox.Handle, EM_GETSCROLLPOS, 0, ref m_scrollPoint);
                SendMessage(m_richTextBox.Handle, WM_SETREDRAW, 0, IntPtr.Zero);
                m_eventMask = SendMessage(m_richTextBox.Handle, EM_GETEVENTMASK, 0, IntPtr.Zero);
                m_painting = false;
            }
        }
        public void ResumePainting()
        {
            if (!m_painting)
            {
                m_richTextBox.Select(m_suspendIndex, m_suspendLength);
                SendMessage(m_richTextBox.Handle, EM_SETSCROLLPOS, 0, ref m_scrollPoint);
                SendMessage(m_richTextBox.Handle, EM_SETEVENTMASK, 0, m_eventMask);
                SendMessage(m_richTextBox.Handle, WM_SETREDRAW, 1, IntPtr.Zero);
                m_painting = true;
                m_richTextBox.Invalidate();
            }
        }
        public void Append(StringBuilder p_message)
        {
            var WatchDogTimer = Stopwatch.StartNew();
            var MinimumRefreshRate = 2000;

            m_richTextBox
                .Invoke((Action)delegate
                {
                    // Last resort cleanup
                    if (WatchDogTimer.ElapsedMilliseconds > MinimumRefreshRate)
                    {
                        // m_richTextBox.Clear(); // Real-world behaviour

                        // Sample App behaviour
                        Form1.CancelAllProducers();
                    }

                    // Stop Drawing to prevent flickering during append and
                    // allow Double-Click events to register properly
                    this.SuspendPainting();
                    m_richTextBox.SelectionStart = m_richTextBox.TextLength;
                    m_richTextBox.SelectedText = p_message.ToString();

                    // Cap out Max Lines and cut back down to improve responsiveness
                    if (m_richTextBox.Lines.Length > 4000)
                    {
                        var NewSet = new string[1000];
                        Array.Copy(m_richTextBox.Lines, 1000, NewSet, 0, 1000);
                        m_richTextBox.Lines = NewSet;
                        m_richTextBox.SelectionStart = m_richTextBox.TextLength;
                        m_richTextBox.SelectedText = "\r\n";
                    }
                    this.ResumePainting();

                    // AutoScroll down to display newest text
                    if (m_attachToBottom)
                    {
                        m_richTextBox.SelectionStart = m_richTextBox.Text.Length;
                        m_richTextBox.ScrollToCaret();
                    }
                });
        }

        // Event Handler
        void RichTextBox_DoubleClick(object sender, EventArgs e)
        {
            // Toggle
            m_attachToBottom = !m_attachToBottom;

            // Scroll to Bottom
            if (m_attachToBottom)
            {
                m_richTextBox.SelectionStart = m_richTextBox.Text.Length;
                m_richTextBox.ScrollToCaret();
            }
        }
        void RichTextBox_MouseWheel(object sender, MouseEventArgs e)
        {
            m_attachToBottom = false;
        }
    }

    public class BufferedConsumer<TBuffer, TItem> : IDisposable
        where TBuffer : new()
    {
        // Fields
        private bool m_disposed = false;
        private Task m_consumer;
        private string m_name;
        private CancellationTokenSource m_tokenSource;
        private AutoResetEvent m_flushSignal;
        private BlockingCollection<TItem> m_queue;

        // Constructor
        public BufferedConsumer(string p_name, Action<TBuffer, TItem> p_appendBuffer, Action<TBuffer> p_postBuffer)
        {
            m_name = p_name;
            m_queue = new BlockingCollection<TItem>();
            m_tokenSource = new CancellationTokenSource();
            var m_token = m_tokenSource.Token;
            m_flushSignal = new AutoResetEvent(false);

            m_token
                .Register(() => { m_flushSignal.Set(); });

            // Begin Consumer Task
            m_consumer = Task.Factory.StartNew(() =>
            {
                //Handler
                //    .LogExceptions(ErrorResponse.SupressRethrow, () =>
                //    {
                // Continuously consumes entries added to the collection, blocking-wait if empty until cancelled
                while (!m_token.IsCancellationRequested)
                {
                    // Block
                    m_flushSignal.WaitOne();

                    if (m_token.IsCancellationRequested && m_queue.Count == 0)
                        break;

                    // Consume all queued items
                    TBuffer PostBuffer = new TBuffer();

                    Console.WriteLine("Queue Count = " + m_queue.Count + ", Buffering...");
                    for (int i = 0; i < m_queue.Count; i++)
                    {
                        TItem Item;
                        m_queue.TryTake(out Item);
                        p_appendBuffer(PostBuffer, Item);
                    }

                    // Post Buffered Items
                    p_postBuffer(PostBuffer);

                    // Signal another Buffer loop if more items were Queued during post sequence
                    var QueueSize = m_queue.Count;
                    if (QueueSize > 0)
                    {
                        Console.WriteLine("Queue Count = " + QueueSize + ", Sleeping...");
                        m_flushSignal.Set();

                        if (QueueSize > 10 && QueueSize < 100)
                            Sleep.For(1000, m_token);      //Allow Queue to build, reducing posting overhead if requests are very frequent
                    }
                }
                //});
            }, m_token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
        }
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        protected virtual void Dispose(bool p_disposing)
        {
            if (!m_disposed)
            {
                m_disposed = true;
                if (p_disposing)
                {
                    // Release of Managed Resources
                    m_tokenSource.Cancel();
                    m_flushSignal.Set();
                    m_consumer.Wait();
                }
                // Release of Unmanaged Resources
            }
        }

        // Methods
        public void AppendCollection(TItem p_item)
        {
            m_queue.Add(p_item);
            m_flushSignal.Set();
        }
    }

    public static partial class Sleep
    {
        public static bool For(int p_milliseconds, CancellationToken p_cancelToken = default(CancellationToken))
        {
            //p_milliseconds
            //    .MustBeEqualOrAbove(0, "p_milliseconds");

            // Exit immediate if cancelled
            if (p_cancelToken != default(CancellationToken))
                if (p_cancelToken.IsCancellationRequested)
                    return true;

            var SleepTimer =
                new AutoResetEvent(false);

            // Cancellation Callback Action
            if (p_cancelToken != default(CancellationToken))
                p_cancelToken
                    .Register(() => SleepTimer.Set());

            // Block on SleepTimer
            var Canceled = SleepTimer.WaitOne(p_milliseconds);

            return Canceled;
        }
    }
}
4

1 に答える 1

1

OPのリクエストに従って回答を投稿:

ElementHostを使用して、既存の winforms アプリケーションに、仮想化された高性能でリッチで高度にカスタマイズ可能な WPF ログ ビューアーのを統合できます。

ここに画像の説明を入力

上記のリンクの完全なソースコード

于 2013-08-16T20:07:40.093 に答える