2013-08-16 106 views
0

背景在自己的線程上運行RichTextBox?

我有我使用的基本上是喜歡我的WinForms應用程序控制臺一個RichTextBox控制。目前,我的應用程序範圍的記錄器使用委託和其中一個監聽器發佈消息是RTB。記錄器同步發送大量表示事件調用,狀態消息,操作結果等的短字符串(小於100個字符)。

使用BeginInvoke將大量這些短消息發佈到RTB可提供用戶界面響應,直到大量並行處理開始記錄很多消息,然後UI開始無序發佈項目,或者文本遠遠落後(數百毫秒)。我知道這一點,因爲當處理速度減慢或停止時,控制檯會在字後繼續寫一段時間。

我的臨時解決方案是同步調用UI並添加阻塞收集緩衝區。基本上從Logger中獲取許多小的項目,並將它們組合到一個字符串構建器中,以彙總到RTB。如果UI能夠跟上,緩衝區會發布條目,但是如果隊列變得太高,那麼它會聚合它們,然後發佈到UI。 RTB因此被更新爲零食,並且在記錄大量事物時看起來很詭異。

問題

我如何運行RichTextBox控制在自己的UI線程保持在同一Form響應其他按鈕,在頻繁但小追加操作?從研究中,我認爲我需要運行一個STA線程並調用Application.Run()來將RTB放在它自己的線程中,但是我發現的例子缺乏實質性代碼示例,並且似乎沒有任何教程(可能是因爲我想做的是不好的建議?)。此外,我不確定是否存在單個控件在其自身線程上相對於窗體其餘部分的任何缺陷。 (即任何關閉主窗體的問題,或者RTB的STA線程是否隨着窗體關閉而死亡?任何特殊處理?等等)

這應該證明一旦您添加3 Button s和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; 
     } 
    } 
} 
+1

請看[我示例](http://stackoverflow.com/a/16745054/643085)使用當前相關.Net Windows UI技術的類似事物。 –

+0

@HighCore Winforms和WPF可以混用嗎?我知道WPF的優點,但我在Winforms中使用Presenter First模式(從我的前驅者繼承了Winforms,我選擇了模式),所以我必須在重構12KCLOC的同時學習MVVM,XAML等並及時實施新的客戶功能,並保持部署的版本兼容性......而我使用的模式可能轉化爲MVVM/WPF ......我真的不知道在這個時候。下一個應用程序,我肯定會從頭開始,WPF和您的控制檯解決方案,謝謝。 – HodlDwon

+1

是的,您可以使用[ElementHost](http://msdn.microsoft.com/zh-cn/library/system.windows)將WPF內容集成到現有的Winforms應用程序(假定它運行在.Net 3.0或更高版本中)。 forms.integration.elementhost.aspx)。您不需要更改現有的體系結構/基礎結構,因此可能會逐步升級,或者將日誌查看器保留在WPF中,其餘部分保留爲winforms。 –

回答

1

發帖答案每OP的要求:

可以集成虛擬化,高性能的my example,豐富的,高度可定製的WPF登錄瀏覽器在您現有的WinForms應用程序通過使用ElementHost

在鏈接

enter image description here

完整的源代碼上述

相關問題