我有我使用的基本上是喜歡我的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;
}
}
}
請看[我示例](http://stackoverflow.com/a/16745054/643085)使用當前相關.Net Windows UI技術的類似事物。 –
@HighCore Winforms和WPF可以混用嗎?我知道WPF的優點,但我在Winforms中使用Presenter First模式(從我的前驅者繼承了Winforms,我選擇了模式),所以我必須在重構12KCLOC的同時學習MVVM,XAML等並及時實施新的客戶功能,並保持部署的版本兼容性......而我使用的模式可能轉化爲MVVM/WPF ......我真的不知道在這個時候。下一個應用程序,我肯定會從頭開始,WPF和您的控制檯解決方案,謝謝。 – HodlDwon
是的,您可以使用[ElementHost](http://msdn.microsoft.com/zh-cn/library/system.windows)將WPF內容集成到現有的Winforms應用程序(假定它運行在.Net 3.0或更高版本中)。 forms.integration.elementhost.aspx)。您不需要更改現有的體系結構/基礎結構,因此可能會逐步升級,或者將日誌查看器保留在WPF中,其餘部分保留爲winforms。 –