Fenêtre de journal élégante dans WinForms C #

Je cherche des idées sur une manière efficace de mettre en place une fenêtre de journal pour une application Windows Forms. Dans le passé, j’ai implémenté plusieurs applications utilisant TextBox et RichTextBox mais je ne suis toujours pas totalement satisfait de la fonctionnalité.

Ce journal est destiné à fournir à l’utilisateur un historique récent des différents événements, principalement utilisés dans les applications de collecte de données, où l’on peut être curieux de savoir comment une transaction particulière est terminée. Dans ce cas, le journal ne doit pas être permanent ni enregistré dans un fichier.

Premièrement, certaines exigences proposées:

  • Efficace et rapide; Si des centaines de lignes sont écrites sur le journal en succession rapide, il doit consumr un minimum de ressources et de temps.
  • Être capable d’offrir un défilement variable de 2000 lignes ou plus. Tout ce qui est plus long n’est pas nécessaire.
  • La mise en évidence et la couleur sont préférées. Effets de police non requirejs
  • Découpez automatiquement les lignes lorsque la limite de défilement est atteinte.
  • Faites défiler automatiquement lorsque de nouvelles données sont ajoutées.
  • Bonus mais pas obligatoire: Pause du défilement automatique lors d’une interaction manuelle, par exemple si l’utilisateur navigue dans l’historique.

Ce que j’ai utilisé jusqu’ici pour écrire et couper le journal:

J’utilise le code suivant (que j’appelle des autres threads):

// rtbLog is a RichTextBox // _MaxLines is an int public void AppendLog(ssortingng 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 } } 

Le problème avec cette approche est que chaque fois que TrimLog est appelé, je perds le formatage des couleurs. Avec un TextBox régulier, cela fonctionne très bien (avec un peu de modification bien sûr).

La recherche d’une solution à ce problème n’a jamais été vraiment satisfaisante. Certains suggèrent de réduire l’excédent par le nombre de caractères au lieu du nombre de lignes dans un RichTextBox. J’ai aussi vu des ListBox utilisés, mais je ne l’ai pas essayé avec succès.

Je vous recommande de ne pas utiliser de contrôle pour votre journal. Au lieu de cela, écrivez une classe de collecte de journaux qui possède les propriétés souhaitées (n’incluant pas les propriétés d’affichage).

Ensuite, écrivez le petit code nécessaire pour sauvegarder cette collection dans divers éléments de l’interface utilisateur. Personnellement, je SendToListBox méthodes SendToEditControl et SendToListBox dans mon object de journalisation. J’appendais probablement des capacités de filtrage à ces méthodes.

Vous pouvez mettre à jour le journal de l’interface utilisateur aussi souvent que nécessaire, ce qui vous permet d’obtenir les meilleures performances possibles et, plus important encore, de réduire la surcharge de l’interface utilisateur lorsque le journal change rapidement.

L’important n’est pas de lier votre enregistrement à une partie de l’interface utilisateur, c’est une erreur. Un jour, vous voudrez peut-être courir sans tête.

À long terme, une bonne interface utilisateur pour un enregistreur est probablement un contrôle personnalisé. Mais à court terme, vous souhaitez simplement déconnecter votre enregistrement de n’importe quelle partie de l’interface utilisateur.

Voici quelque chose que j’ai rassemblé sur la base d’un enregistreur beaucoup plus sophistiqué que j’ai écrit il y a un moment.

Cela prend en charge la couleur dans la zone de liste en fonction du niveau de journalisation, prend en charge Ctrl + V et clic droit pour copier en tant que RTF, et gère la connexion à la ListBox à partir d’autres threads.

Vous pouvez remplacer le nombre de lignes conservées dans le ListBox (2000 par défaut) ainsi que le format du message en utilisant l’une des surcharges du constructeur.

 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 ssortingng DEFAULT_MESSAGE_FORMAT = "{0} [{5}] : {8}"; private const int DEFAULT_MAX_LINES_IN_LISTBOX = 2000; private bool _disposed; private ListBox _listBox; private ssortingng _messageFormat; private int _maxEnsortingesInListBox; 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].ToSsortingng()); } 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.DrawSsortingng(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, ssortingng message) { EventTime = DateTime.Now; Level = level; Message = message; } public readonly DateTime EventTime; public readonly Level Level; public readonly ssortingng 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 > _maxEnsortingesInListBox) { _listBox.Items.RemoveAt(0); } if (!_paused) _listBox.TopIndex = _listBox.Items.Count - 1; } private ssortingng 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 ssortingng.Format("", (int)level); } } private ssortingng FormatALogEventMessage(LogEvent logEvent, ssortingng messageFormat) { ssortingng message = logEvent.Message; if (message == null) { message = ""; } return ssortingng.Format(messageFormat, /* {0} */ logEvent.EventTime.ToSsortingng("yyyy-MM-dd HH:mm:ss.fff"), /* {1} */ logEvent.EventTime.ToSsortingng("yyyy-MM-dd HH:mm:ss"), /* {2} */ logEvent.EventTime.ToSsortingng("yyyy-MM-dd"), /* {3} */ logEvent.EventTime.ToSsortingng("HH:mm:ss.fff"), /* {4} */ logEvent.EventTime.ToSsortingng("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) { SsortingngBuilder selectedItemsAsRTFText = new SsortingngBuilder(); 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.ToSsortingng()); Clipboard.SetData(DataFormats.Rtf, selectedItemsAsRTFText.ToSsortingng()); } } public ListBoxLog(ListBox listBox) : this(listBox, DEFAULT_MESSAGE_FORMAT, DEFAULT_MAX_LINES_IN_LISTBOX) { } public ListBoxLog(ListBox listBox, ssortingng messageFormat) : this(listBox, messageFormat, DEFAULT_MAX_LINES_IN_LISTBOX) { } public ListBoxLog(ListBox listBox, ssortingng messageFormat, int maxLinesInListbox) { _disposed = false; _listBox = listBox; _messageFormat = messageFormat; _maxEnsortingesInListBox = 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(ssortingng message) { Log(Level.Debug, message); } public void Log(ssortingng format, params object[] args) { Log(Level.Debug, (format == null) ? null : ssortingng.Format(format, args)); } public void Log(Level level, ssortingng format, params object[] args) { Log(level, (format == null) ? null : ssortingng.Format(format, args)); } public void Log(Level level, ssortingng 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; } } } } 

Je vais stocker ceci ici pour aider Future Me quand je veux utiliser un RichTextBox pour enregistrer à nouveau des lignes colorées. Le code suivant supprime la première ligne d’un RichTextBox:

 if ( logTextBox.Lines.Length > MAX_LINES ) { logTextBox.Select(0, logTextBox.Text.IndexOf('\n')+1); logTextBox.SelectedRtf = "{\\rtf1\\ansi\\ansicpg1252\\deff0\\deflang1053\\uc1 }"; } 

Il m’a fallu beaucoup de temps pour comprendre que le paramètre SelectedRtf ne fonctionnait pas, mais que le paramétrer sur “RTF” sans contenu textuel est correct.

J’ai récemment implémenté quelque chose de similaire. Notre approche consistait à conserver un tampon circulaire des enregistrements de défilement et à peindre le texte du journal manuellement (avec Graphics.DrawSsortingng). Ensuite, si l’utilisateur veut faire défiler, copier du texte, etc., nous avons un bouton “Pause” qui retourne à un contrôle TextBox normal.

Je dirais que ListView est parfait pour cela (en mode d’affichage Detail) et c’est exactement ce pour quoi je l’utilise dans quelques applications internes.

Conseil utile: utilisez BeginUpdate () et EndUpdate () si vous savez que vous allez append / supprimer un grand nombre d’éléments à la fois.

Si vous souhaitez mettre en évidence et mettre en forme les couleurs, je vous suggérerais un RichTextBox.

Si vous voulez le défilement automatique, utilisez le ListBox.

Dans les deux cas, liez-le à un tampon circulaire de lignes.