cref - Elegante ventana de registro en WinForms C#
remarks c# (6)
Aquí hay algo que inventé basado en un registrador mucho más sofisticado que escribí hace un tiempo.
Esto admitirá el color en el cuadro de lista según el nivel de registro, admite Ctrl + V y clic con el botón derecho para copiar como RTF y maneja el registro en el ListBox desde otros subprocesos.
Puede anular el número de líneas retenidas en el ListBox (2000 de forma predeterminada), así como el formato del mensaje utilizando una de las sobrecargas del constructor.
using System;
using System.Drawing;
using System.Windows.Forms;
using System.Threading;
using System.Text;
namespace StackOverflow
{
public partial class Main : Form
{
public static ListBoxLog listBoxLog;
public Main()
{
InitializeComponent();
listBoxLog = new ListBoxLog(listBox1);
Thread thread = new Thread(LogStuffThread);
thread.IsBackground = true;
thread.Start();
}
private void LogStuffThread()
{
int number = 0;
while (true)
{
listBoxLog.Log(Level.Info, "A info level message from thread # {0,0000}", number++);
Thread.Sleep(2000);
}
}
private void button1_Click(object sender, EventArgs e)
{
listBoxLog.Log(Level.Debug, "A debug level message");
}
private void button2_Click(object sender, EventArgs e)
{
listBoxLog.Log(Level.Verbose, "A verbose level message");
}
private void button3_Click(object sender, EventArgs e)
{
listBoxLog.Log(Level.Info, "A info level message");
}
private void button4_Click(object sender, EventArgs e)
{
listBoxLog.Log(Level.Warning, "A warning level message");
}
private void button5_Click(object sender, EventArgs e)
{
listBoxLog.Log(Level.Error, "A error level message");
}
private void button6_Click(object sender, EventArgs e)
{
listBoxLog.Log(Level.Critical, "A critical level message");
}
private void button7_Click(object sender, EventArgs e)
{
listBoxLog.Paused = !listBoxLog.Paused;
}
}
public enum Level : int
{
Critical = 0,
Error = 1,
Warning = 2,
Info = 3,
Verbose = 4,
Debug = 5
};
public sealed class ListBoxLog : IDisposable
{
private const string DEFAULT_MESSAGE_FORMAT = "{0} [{5}] : {8}";
private const int DEFAULT_MAX_LINES_IN_LISTBOX = 2000;
private bool _disposed;
private ListBox _listBox;
private string _messageFormat;
private int _maxEntriesInListBox;
private bool _canAdd;
private bool _paused;
private void OnHandleCreated(object sender, EventArgs e)
{
_canAdd = true;
}
private void OnHandleDestroyed(object sender, EventArgs e)
{
_canAdd = false;
}
private void DrawItemHandler(object sender, DrawItemEventArgs e)
{
if (e.Index >= 0)
{
e.DrawBackground();
e.DrawFocusRectangle();
LogEvent logEvent = ((ListBox)sender).Items[e.Index] as LogEvent;
// SafeGuard against wrong configuration of list box
if (logEvent == null)
{
logEvent = new LogEvent(Level.Critical, ((ListBox)sender).Items[e.Index].ToString());
}
Color color;
switch (logEvent.Level)
{
case Level.Critical:
color = Color.White;
break;
case Level.Error:
color = Color.Red;
break;
case Level.Warning:
color = Color.Goldenrod;
break;
case Level.Info:
color = Color.Green;
break;
case Level.Verbose:
color = Color.Blue;
break;
default:
color = Color.Black;
break;
}
if (logEvent.Level == Level.Critical)
{
e.Graphics.FillRectangle(new SolidBrush(Color.Red), e.Bounds);
}
e.Graphics.DrawString(FormatALogEventMessage(logEvent, _messageFormat), new Font("Lucida Console", 8.25f, FontStyle.Regular), new SolidBrush(color), e.Bounds);
}
}
private void KeyDownHandler(object sender, KeyEventArgs e)
{
if ((e.Modifiers == Keys.Control) && (e.KeyCode == Keys.C))
{
CopyToClipboard();
}
}
private void CopyMenuOnClickHandler(object sender, EventArgs e)
{
CopyToClipboard();
}
private void CopyMenuPopupHandler(object sender, EventArgs e)
{
ContextMenu menu = sender as ContextMenu;
if (menu != null)
{
menu.MenuItems[0].Enabled = (_listBox.SelectedItems.Count > 0);
}
}
private class LogEvent
{
public LogEvent(Level level, string message)
{
EventTime = DateTime.Now;
Level = level;
Message = message;
}
public readonly DateTime EventTime;
public readonly Level Level;
public readonly string Message;
}
private void WriteEvent(LogEvent logEvent)
{
if ((logEvent != null) && (_canAdd))
{
_listBox.BeginInvoke(new AddALogEntryDelegate(AddALogEntry), logEvent);
}
}
private delegate void AddALogEntryDelegate(object item);
private void AddALogEntry(object item)
{
_listBox.Items.Add(item);
if (_listBox.Items.Count > _maxEntriesInListBox)
{
_listBox.Items.RemoveAt(0);
}
if (!_paused) _listBox.TopIndex = _listBox.Items.Count - 1;
}
private string LevelName(Level level)
{
switch (level)
{
case Level.Critical: return "Critical";
case Level.Error: return "Error";
case Level.Warning: return "Warning";
case Level.Info: return "Info";
case Level.Verbose: return "Verbose";
case Level.Debug: return "Debug";
default: return string.Format("<value={0}>", (int)level);
}
}
private string FormatALogEventMessage(LogEvent logEvent, string messageFormat)
{
string message = logEvent.Message;
if (message == null) { message = "<NULL>"; }
return string.Format(messageFormat,
/* {0} */ logEvent.EventTime.ToString("yyyy-MM-dd HH:mm:ss.fff"),
/* {1} */ logEvent.EventTime.ToString("yyyy-MM-dd HH:mm:ss"),
/* {2} */ logEvent.EventTime.ToString("yyyy-MM-dd"),
/* {3} */ logEvent.EventTime.ToString("HH:mm:ss.fff"),
/* {4} */ logEvent.EventTime.ToString("HH:mm:ss"),
/* {5} */ LevelName(logEvent.Level)[0],
/* {6} */ LevelName(logEvent.Level),
/* {7} */ (int)logEvent.Level,
/* {8} */ message);
}
private void CopyToClipboard()
{
if (_listBox.SelectedItems.Count > 0)
{
StringBuilder selectedItemsAsRTFText = new StringBuilder();
selectedItemsAsRTFText.AppendLine(@"{/rtf1/ansi/deff0{/fonttbl{/f0/fcharset0 Courier;}}");
selectedItemsAsRTFText.AppendLine(@"{/colortbl;/red255/green255/blue255;/red255/green0/blue0;/red218/green165/blue32;/red0/green128/blue0;/red0/green0/blue255;/red0/green0/blue0}");
foreach (LogEvent logEvent in _listBox.SelectedItems)
{
selectedItemsAsRTFText.AppendFormat(@"{{/f0/fs16/chshdng0/chcbpat{0}/cb{0}/cf{1} ", (logEvent.Level == Level.Critical) ? 2 : 1, (logEvent.Level == Level.Critical) ? 1 : ((int)logEvent.Level > 5) ? 6 : ((int)logEvent.Level) + 1);
selectedItemsAsRTFText.Append(FormatALogEventMessage(logEvent, _messageFormat));
selectedItemsAsRTFText.AppendLine(@"/par}");
}
selectedItemsAsRTFText.AppendLine(@"}");
System.Diagnostics.Debug.WriteLine(selectedItemsAsRTFText.ToString());
Clipboard.SetData(DataFormats.Rtf, selectedItemsAsRTFText.ToString());
}
}
public ListBoxLog(ListBox listBox) : this(listBox, DEFAULT_MESSAGE_FORMAT, DEFAULT_MAX_LINES_IN_LISTBOX) { }
public ListBoxLog(ListBox listBox, string messageFormat) : this(listBox, messageFormat, DEFAULT_MAX_LINES_IN_LISTBOX) { }
public ListBoxLog(ListBox listBox, string messageFormat, int maxLinesInListbox)
{
_disposed = false;
_listBox = listBox;
_messageFormat = messageFormat;
_maxEntriesInListBox = maxLinesInListbox;
_paused = false;
_canAdd = listBox.IsHandleCreated;
_listBox.SelectionMode = SelectionMode.MultiExtended;
_listBox.HandleCreated += OnHandleCreated;
_listBox.HandleDestroyed += OnHandleDestroyed;
_listBox.DrawItem += DrawItemHandler;
_listBox.KeyDown += KeyDownHandler;
MenuItem[] menuItems = new MenuItem[] { new MenuItem("Copy", new EventHandler(CopyMenuOnClickHandler)) };
_listBox.ContextMenu = new ContextMenu(menuItems);
_listBox.ContextMenu.Popup += new EventHandler(CopyMenuPopupHandler);
_listBox.DrawMode = DrawMode.OwnerDrawFixed;
}
public void Log(string message) { Log(Level.Debug, message); }
public void Log(string format, params object[] args) { Log(Level.Debug, (format == null) ? null : string.Format(format, args)); }
public void Log(Level level, string format, params object[] args) { Log(level, (format == null) ? null : string.Format(format, args)); }
public void Log(Level level, string message)
{
WriteEvent(new LogEvent(level, message));
}
public bool Paused
{
get { return _paused; }
set { _paused = value; }
}
~ListBoxLog()
{
if (!_disposed)
{
Dispose(false);
_disposed = true;
}
}
public void Dispose()
{
if (!_disposed)
{
Dispose(true);
GC.SuppressFinalize(this);
_disposed = true;
}
}
private void Dispose(bool disposing)
{
if (_listBox != null)
{
_canAdd = false;
_listBox.HandleCreated -= OnHandleCreated;
_listBox.HandleCreated -= OnHandleDestroyed;
_listBox.DrawItem -= DrawItemHandler;
_listBox.KeyDown -= KeyDownHandler;
_listBox.ContextMenu.MenuItems.Clear();
_listBox.ContextMenu.Popup -= CopyMenuPopupHandler;
_listBox.ContextMenu = null;
_listBox.Items.Clear();
_listBox.DrawMode = DrawMode.Normal;
_listBox = null;
}
}
}
}
Estoy buscando ideas sobre una forma eficiente de implementar una ventana de registro para una aplicación de formularios de Windows. En el pasado he implementado varios usando TextBox y RichTextBox pero todavía no estoy totalmente satisfecho con la funcionalidad.
Este registro está destinado a proporcionar al usuario un historial reciente de varios eventos, principalmente utilizados en aplicaciones de recopilación de datos, donde uno podría tener curiosidad por cómo se completó una transacción en particular. En este caso, el registro no necesita ser permanente ni guardarse en un archivo.
Primero, algunos requisitos propuestos:
- Eficiente y rápido; si se escriben cientos de líneas en el registro en sucesión rápida, debe consumir recursos y tiempo mínimos.
- Ser capaz de ofrecer una desviación variable de hasta 2000 líneas más o menos. Cualquier cosa más larga es innecesaria.
- El resaltado y el color son preferidos. Efectos de fuente no requeridos
- Recorta automáticamente las líneas a medida que se alcanza el límite de desplazamiento.
- Desplazarse automáticamente a medida que se agregan nuevos datos.
- Bonificación pero no requerida: Pause el desplazamiento automático durante la interacción manual, como si el usuario está explorando el historial.
Lo que he estado usando hasta ahora para escribir y recortar el registro:
Utilizo el siguiente código (que llamo desde otros hilos):
// rtbLog is a RichTextBox
// _MaxLines is an int
public void AppendLog(string s, Color c, bool bNewLine)
{
if (rtbLog.InvokeRequired)
{
object[] args = { s, c, bNewLine };
rtbLog.Invoke(new AppendLogDel(AppendLog), args);
return;
}
try
{
rtbLog.SelectionColor = c;
rtbLog.AppendText(s);
if (bNewLine) rtbLog.AppendText(Environment.NewLine);
TrimLog();
rtbLog.SelectionStart = rtbLog.TextLength;
rtbLog.ScrollToCaret();
rtbLog.Update();
}
catch (Exception exc)
{
// exception handling
}
}
private void TrimLog()
{
try
{
// Extra lines as buffer to save time
if (rtbLog.Lines.Length < _MaxLines + 10)
{
return;
}
else
{
string[] sTemp = rtxtLog.Lines;
string[] sNew= new string[_MaxLines];
int iLineOffset = sTemp.Length - _MaxLines;
for (int n = 0; n < _MaxLines; n++)
{
sNew[n] = sTemp[iLineOffset];
iLineOffset++;
}
rtbLog.Lines = sNew;
}
}
catch (Exception exc)
{
// exception handling
}
}
El problema con este enfoque es que cada vez que se llama a TrimLog, pierdo el formato de color. Con un TextBox regular, esto funciona bien (con un poco de modificación, por supuesto).
Las búsquedas de una solución para esto nunca han sido realmente satisfactorias. Algunos sugieren recortar el exceso por recuento de caracteres en lugar de recuento de líneas en un RichTextBox. También he visto usar ListBoxes, pero no lo he probado con éxito.
Diría que ListView es perfecto para esto (en el modo de visualización detallada), y es exactamente el mismo para el que lo uso en algunas aplicaciones internas.
Consejo útil: utilice BeginUpdate () y EndUpdate () si sabe que va a agregar / eliminar muchos elementos a la vez.
Guardaré esto aquí como una ayuda para Future Me cuando quiera usar un RichTextBox para volver a registrar líneas de color. El siguiente código elimina la primera línea en un RichTextBox:
if ( logTextBox.Lines.Length > MAX_LINES )
{
logTextBox.Select(0, logTextBox.Text.IndexOf(''/n'')+1);
logTextBox.SelectedRtf = "{//rtf1//ansi//ansicpg1252//deff0//deflang1053//uc1 }";
}
Me llevó demasiado tiempo darme cuenta de que configurar "SelectedRtf" para "" no funcionaba, pero que establecerlo en RTF "apropiado" sin contenido de texto está bien.
Recientemente implementé algo similar. Nuestro enfoque era mantener un buffer en anillo de los registros scrollback y simplemente pintar el texto de registro manualmente (con Graphics.DrawString). Entonces, si el usuario quiere desplazarse hacia atrás, copiar texto, etc., tenemos un botón de "Pausa" que vuelve a un control de cuadro de texto normal.
Recomiendo que no use un control como su registro en absoluto. En su lugar, escriba una clase de recopilación de registros que tenga las propiedades que desee (sin incluir las propiedades de visualización).
Luego, escriba el poco de código que se necesita para volcar esa colección a una variedad de elementos de la interfaz de usuario. Personalmente, pondría los métodos SendToEditControl
y SendToListBox
en mi objeto de registro. Probablemente agregaría capacidades de filtrado a estos métodos.
Puede actualizar el registro de la interfaz de usuario solo con la frecuencia que sea razonable, ofreciéndole el mejor rendimiento posible y, lo que es más importante, le permite reducir la sobrecarga de la interfaz de usuario cuando el registro está cambiando rápidamente.
Lo importante es no atar tu registro a una pieza de UI, eso es un error. Algún día es posible que desees correr sin cabeza.
A largo plazo, una buena IU para un registrador es probablemente un control personalizado. Pero a corto plazo, solo desea desconectar su registro de cualquier pieza específica de UI.
Si desea resaltar y formato de color, sugeriría un RichTextBox.
Si desea el desplazamiento automático, utilice el ListBox.
En cualquier caso, agréguelo a un buffer circular de líneas.