2015年3月3日火曜日

log4netでテキストボックスにログを出力する

わ~久しぶりのサンプルコードだ~

表題の件は既にstackoverflowで回答が出ているのですが、
多少バージョンアップしたものが出来たので公開してみることにしました。


以下の機能が追加されています。
  • TextBoxNameプロパティで、フォーム直下以外のテキストボックスも指定できるようにしました。
  • MaxLinesプロパティで、テキストボックスに表示する最大行数を設定できるようにしました。
  • (多分)フォーム以外のスレッドからログが出力されても問題ないようになりました。

namespace Mericle.log4net
{
    using System;
    using System.Threading;
    using System.Windows.Forms;
    using log4net.Appender;
    using log4net.Core;

    /// <summary>
    /// log4net にテキストボックスへのアペンダを提供します。
    /// 開いているフォームの中から、
    /// 設定したフォーム名・テキストボックス名が一致するテキストボックスに書き込みを行います。
    /// http://stackoverflow.com/questions/14114614/
    /// </summary>
    public class TextBoxAppender 
        : AppenderSkeleton
    {
        /// <summary>
        /// ログを追記するメソッドのデリゲートです。
        /// </summary>
        /// <param name="loggingEvent">ロギングイベント。</param>
        protected delegate void AppendLogHandler(LoggingEvent loggingEvent);

        /// <summary>
        /// フォーム名を取得または設定します。
        /// </summary>
        /// <value>フォーム名。</value>
        public string FormName { get; set; }
        
        /// <summary>
        /// テキストボックス名を取得または設定します。
        /// </summary>
        /// <value>テキストボックス名。</value>
        /// <remarks>
        /// テキストボックスがタブやグループの中に存在する場合は、
        /// テキストボックス名の前にタブやグループの名前を"."区切りで設定してください。
        /// 例:tabControl1.tabPage1.textBox1
        /// </remarks>
        public string TextBoxName { get; set; }

        /// <summary>
        /// テキストボックスに出力する最大行数を取得または設定します。
        /// </summary>
        /// <value>テキストボックスに出力する最大行数。</value>
        /// <remarks>
        /// 最大行数を超えた場合、最初に出力されたログから順番に削除されます。
        /// 0以下を設定した場合、最大行数の設定は無効となります。
        /// </remarks>
        public int MaxLines { get; set; }

        /// <summary>
        /// ログを出力する対象のフォームです。
        /// </summary>
        /// <value>ログを出力する対象のテキストボックス。</value>
        protected Form Form { get; set; }

        /// <summary>
        /// ログを出力する対象のテキストボックスです。
        /// </summary>
        /// <value>ログを出力する対象のテキストボックス。</value>
        protected TextBox TextBox { get; set; }

        /// <summary>
        /// ログをテキストボックスに追記します。
        /// 追記するテキストボックスが見つからない場合は何も行いません。
        /// </summary>
        /// <param name="loggingEvent">ロギングイベント</param>
        protected override void Append(LoggingEvent loggingEvent)
        {
            // 書き込み先のテキストボックスを特定します。
            if (TextBox == null || Form == null)
            {
                if (string.IsNullOrEmpty(FormName) || 
                    string.IsNullOrEmpty(TextBoxName))
                {
                    return;
                }

                Form = Application.OpenForms[FormName];
                if (Form == null)
                {
                    return;
                }

                TextBox = GetTextBox(Form, TextBoxName);
                if (TextBox == null)
                {
                    return;
                }

                Form.FormClosing += (s, e) => { Form = null; TextBox = null; };
            }

            if (Form.InvokeRequired)
            {
                // フォームのスレッドに合わせてログを追記します。
                // 出力元のスレッドを阻害しないように別スレッドで追記します。
                new Thread(InvokeAppendLog(loggingEvent)).Start();
            }
            else
            {
                AppendLog(loggingEvent);
            }
        }

        /// <summary>
        /// フォームからテキストボックス名を基にテキストボックスを取得します。
        /// 取得に失敗した場合は、<see langword="null"/>を返します。
        /// テキストボックスが階層構造を持つ場合は、
        /// "."区切りで階層を指定することができます。
        /// </summary>
        /// <param name="form">フォーム。</param>
        /// <param name="textBoxName">テキストボックス名。</param>
        /// <returns>テキストボックス。</returns>
        protected virtual TextBox GetTextBox(Control form, string textBoxName)
        {
            var control = form;
            var names = textBoxName.Split('.');
            foreach (var name in names)
            {
                control = control.Controls[name];
                if (control == null)
                {
                    return null;
                }
            }

            return control as TextBox;
        }

        /// <summary>
        /// フォームに同期してログをテキストボックスに追記するメソッドを取得します。
        /// </summary>
        /// <param name="loggingEvent">ロギングイベント。</param>
        /// <returns>フォームに同期してログをテキストボックスに追記するメソッド。</returns>
        protected virtual ThreadStart InvokeAppendLog(LoggingEvent loggingEvent)
        {
            return () => 
            {
                try
                {
                    Form.Invoke(new AppendLogHandler(AppendLog), new object[] { loggingEvent });
                }
                catch (ObjectDisposedException)
                {
                    // スレッドのタイミングによっては、解放した後に呼び出されてしまいます。
                    // あまり長くない期間なので、判定せずに例外をキャッチして無視しています。
                }
            };
        }

        /// <summary>
        /// ログをテキストボックスに追記する実体のメソッドです。
        /// 必要であれば対象のフォームと同じスレッドで書き込めるように分割しています。
        /// </summary>
        /// <param name="loggingEvent">ロギングイベント。</param>
        protected virtual void AppendLog(LoggingEvent loggingEvent)
        {
            RemoveUnnecessaryLines();

            TextBox.AppendText((Layout == null)
                ? loggingEvent.RenderedMessage + Environment.NewLine
                : RenderLoggingEvent(loggingEvent));
        }

        /// <summary>
        /// テキストボックスが<seealso cref="MaxLines"/>を超えていれば、
        /// 余計な行を先頭から削除します。
        /// </summary>
        /// <remarks>
        /// 最大行数を超えた場合、最初に出力されたログから順番に削除されます。
        /// <seealso cref="MaxLines"/>に0以下を設定した場合、最大行数の設定は無効となります。
        /// </remarks>
        protected virtual void RemoveUnnecessaryLines()
        {
            // 0以下は無効とみなします。
            if (MaxLines <= 0)
            {
                return;
            }

            var lines = TextBox.Lines;
            if (lines.Length <= MaxLines)
            {
                return;
            }

            var removeLines = lines.Length - MaxLines;
            TextBox.Text = string.Join(
                Environment.NewLine,
                lines,
                removeLines,
                lines.Length - removeLines);
        }
    }
}

設定例はこんな感じです。

<appender name="textbox" type="Mericle.log4net.TextBoxAppender, Mericle.log4net">
    <formName value="MericleForm" />
    <textBoxName value="tabControl1.tabPage1.textBox1" />
    <maxLines value="100" />
    <layout type="log4net.Layout.PatternLayout">
    <conversionPattern value="%d{yyyy/MM/dd HH:mm:ss.fff},%-5p,%m%n" />
    </layout>
</appender>

名前空間はブログに転記する際に変えたので、
そこで問題があったらすいません。

VB.NETの方は適当に読み替えてください。

0 件のコメント:

コメントを投稿