66 votes

Fenêtre d'enregistrement élégante en WinForms C#

Je suis à la recherche d'idées sur une manière efficace d'implémenter une fenêtre de journal pour une application Windows forms. Dans le passé, j'en ai implémenté plusieurs en 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 de divers événements. Il est principalement utilisé dans les applications de collecte de données où l'on peut être curieux de savoir comment une transaction particulière s'est déroulée. Dans ce cas, il n'est pas nécessaire que le journal soit permanent ou enregistré dans un fichier.

Tout d'abord, quelques exigences proposées :

  • Efficace et rapide ; si des centaines de lignes sont écrites dans le journal en succession rapide, il doit consommer un minimum de ressources et de temps.
  • Pouvoir offrir un défilement variable jusqu'à 2000 lignes environ. Tout ce qui est plus long n'est pas nécessaire.
  • Le surlignage et la couleur sont préférés. Les effets de police ne sont pas nécessaires.
  • Découper automatiquement les lignes lorsque la limite de défilement est atteinte.
  • Défilement automatique au fur et à mesure de l'ajout de nouvelles données.
  • Bonus mais pas obligatoire : Interrompre le défilement automatique en cas d'interaction manuelle, par exemple lorsque l'utilisateur consulte l'historique.

Ce que j'ai utilisé jusqu'à présent pour écrire et découper le journal :

J'utilise le code suivant (que j'appelle depuis d'autres fils) :

// 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
    }
}

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

La recherche d'une solution à ce problème n'a jamais été vraiment satisfaisante. Certains suggèrent de couper l'excès en fonction du nombre de caractères plutôt que du nombre de lignes dans une RichTextBox. J'ai également vu des ListBox utilisées, mais je n'ai pas essayé avec succès.

0 votes

De plus, j'ai eu l'occasion de constater que RTF provoquait des plantages avec la synchronisation des threads que SyncLock était incapable d'empêcher lorsque j'en avais besoin à l'époque. +1

0 votes

Bonjour JYelton. Je comprends que j'aurais pu poser une question, mais cela m'aiderait beaucoup si vous pouviez me faire part du fonctionnement de votre fenêtre d'enregistrement comme suggéré ici [ [stackoverflow.com/a/2196198/5588347]](https://stackoverflow.com/a/2196198/5588347])

0 votes

@AshishSrivastava Cela fait de nombreuses années. Essentiellement, le composant log utilise une file d'attente comme tampon circulaire pour stocker des lignes de texte (un objet de la classe log). Une minuterie modifie périodiquement le contenu d'une RichTextBox pour afficher toutes les lignes de la file d'attente. L'objet de classe log contient la ligne de texte, mais aussi son horodatage et une couleur. Je vais devoir chercher pour trouver le code, mais si j'ai un peu de temps, je le ferai.

31voto

John Knoeller Points 20754

Je vous recommande de ne pas utiliser de contrôle comme journal. Au lieu de cela, écrivez un journal collection qui possède les propriétés souhaitées (sans compter les propriétés d'affichage).

Ensuite, écrivez le petit bout de code nécessaire pour déverser cette collection dans divers éléments de l'interface utilisateur. Personnellement, je mettrais SendToEditControl y SendToListBox dans mon objet de journalisation. J'ajouterais 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, surtout, de réduire la charge de travail de l'interface utilisateur lorsque le journal évolue rapidement.

L'important est de ne pas lier l'exploitation forestière à un élément de l'interface utilisateur, c'est une erreur. Un jour, vous voudrez peut-être travailler sans tête.

À long terme, une bonne interface utilisateur pour un enregistreur est probablement un contrôle personnalisé. Mais à court terme, vous voulez simplement déconnecter votre logging de tout autre système d'information. spécifique de l'interface utilisateur.

0 votes

J'aime cette suggestion parce que j'avais pensé à écrire les événements de log dans une classe personnalisée, et à la faire mettre à jour périodiquement un contrôle d'interface utilisateur, ce qui permettrait de mieux gérer les occasions où un grand nombre d'événements sont écrits en même temps. +1

0 votes

J'ai supposé que la partie "séparation" était une évidence.

1 votes

@Neil : D'après mon expérience, la plupart des débutants semblent utiliser les contrôles en tant que stockage des données et ne réalisent que plus tard que c'est une erreur, et à ce moment-là, il est difficile de se détendre.

31voto

Stefan Points 112

Voici quelque chose que j'ai créé en me basant sur un logger beaucoup plus sophistiqué que j'ai écrit il y a quelque temps.

Il permet de colorer la boîte de liste en fonction du niveau d'enregistrement, supporte Ctrl+V et Right-Click pour la copie au format RTF, et gère l'enregistrement dans la boîte de liste à partir d'autres fils de discussion.

Vous pouvez modifier le nombre de lignes conservées dans la 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 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;
            }
        }
    }
}

2 votes

Comment modifier ce système pour que les messages longs soient transférés à la ligne suivante au lieu d'être tronqués ?

14voto

m_eiman Points 126

Je conserverai ce code ici pour m'aider à l'avenir lorsque je voudrai à nouveau utiliser une RichTextBox pour enregistrer les lignes colorées. Le code suivant supprime la première ligne dans une 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 trop de temps pour comprendre que le fait de définir SelectedRtf sur "" ne fonctionnait pas, mais que le fait de le définir sur un RTF "correct" sans contenu textuel était correct.

3 votes

J'aime cette réponse parce qu'elle répond à la question initiale, ce qui est important pour quelqu'un qui veut savoir comment faire cela dans une zone de texte riche qui n'a peut-être rien à voir avec l'enregistrement.

6voto

Neil N Points 14566

Je dirais que ListView est parfait pour cela (en mode d'affichage détaillé), et c'est exactement ce que j'utilise dans quelques applications internes.

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

2 votes

C'est aussi ce que j'utilise. J'écris les événements du journal dans l'ordre inverse, en les insérant en haut et en les retirant en bas. Je piège également tous les événements de descente ou de défilement de la souris afin de ne pas lutter contre l'utilisateur si une mise à jour est effectuée. Si vos messages sont plus longs que la fenêtre, vous pouvez utiliser une infobulle pour les éléments supplémentaires ( stackoverflow.com/questions/192584/ ) ou inclure une zone de texte séparée à côté de la zone de liste pour contenir les détails.

0 votes

Le problème serait l'utilisation de BeginUpdate()/EndUpdate() parce que la partie du programme qui envoie les événements du journal ne "sait" pas nécessairement s'il y aura une multitude d'événements ou non. Cela ne veut pas dire qu'il n'est pas possible de les mettre en œuvre, par exemple autour de la boucle for qui contient tous les événements générateurs de journaux.

0 votes

J'avais l'habitude d'utiliser ListBox, mais je viens d'essayer ListView. Il semble fonctionner plus rapidement et de manière moins excentrique, je vais donc l'utiliser à partir de maintenant, bien que j'aime la capacité de ListBox à avoir des éléments à hauteur variable ou à dessin par le propriétaire, ListView peut-il faire de telles choses ?

5voto

Daniel Pryden Points 22167

J'ai récemment mis en œuvre quelque chose de similaire. Notre approche a consisté à conserver un tampon circulaire des enregistrements de défilement et à peindre manuellement le texte du journal (avec Graphics.DrawString). Ensuite, si l'utilisateur souhaite revenir en arrière, copier le texte, etc., nous disposons d'un bouton "Pause" qui permet de revenir à un contrôle TextBox normal.

0 votes

Une autre bonne suggestion, je me suis demandé si la création d'un contrôle d'interface utilisateur personnalisé ou hérité serait plus facile que d'essayer de travailler avec des contrôles existants. Le seul problème que je vois est que pendant une "pause", le formatage des couleurs serait perdu pendant que le défilement automatique est en attente.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X