【代码】关于C#支持文件和文本框的简单日志实现

在软件开发过程中,日志记录是一个不可或缺的功能。它不仅能帮助开发者调试程序,还能在出现问题时提供重要的上下文信息。本文将介绍如何使用C#实现一个简单但功能完备的日志系统,支持同时输出到文件和文本框,并具备异步处理、级别过滤和颜色区分等实用特性。

一、功能概述

我们实现的SLog类具有以下特点:

  1. 支持同时输出到文件和界面文本框
  2. 异步处理日志,几乎不影响程序性能
  3. 支持不同日志级别(Trace, Debug, Info, Warn, Error)
  4. 文件日志支持按日期自动分割
  5. 界面日志支持不同级别显示不同颜色
  6. 提供事件机制支持自定义日志处理
  7. 线程安全,支持多线程环境
  8. 无第三方依赖

二、 核心实现

1. 日志级别定义

csharp 复制代码
public enum LogLevel
{
    Trace,
    Debug,
    Info,
    Warn,
    Error
}

我们定义了五种日志级别,从最详细的Trace到最严重的Error,满足不同场景的需求。

2. 异步处理机制

为了确保日志记录不会阻塞主线程,我们采用了生产者-消费者模式:

csharp 复制代码
private readonly ConcurrentQueue<LogItem> _logQueue = new ConcurrentQueue<LogItem>();
private readonly ManualResetEventSlim _logEvent = new ManualResetEventSlim(false);
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
private Task _processingTask;

public SLog()
{
    _processingTask = Task.Run(ProcessLogQueue, _cancellationTokenSource.Token);
}

当应用程序调用日志方法时,日志项被放入并发队列,然后通过ManualResetEventSlim通知后台处理线程。后台线程从队列中取出日志项并处理,这样主线程可以继续执行而不被阻塞。

3. 文件日志实现

文件日志支持按日期自动分割,通过文件路径中的日期占位符实现:

csharp 复制代码
private string GetFilePath(DateTime timestamp)
{
    if (string.IsNullOrEmpty(File))
        return null;

    return File
        .Replace("yyyy", timestamp.Year.ToString("D4"))
        .Replace("MM", timestamp.Month.ToString("D2"))
        .Replace("dd", timestamp.Day.ToString("D2"));
}

当日志处理跨越午夜时,系统会自动检测日期变化并切换到新的日志文件:

csharp 复制代码
// 检查是否需要创建新的日志文件
if (_fileWriter == null || _currentFileDate != timestamp.Date)
{
    _currentFileDate = timestamp.Date;
    _currentFilePath = GetFilePath(timestamp);
    // ...创建新文件
}

4. 界面日志与颜色支持

对于界面日志,我们特别添加了颜色区分功能,使不同级别的日志更加醒目:

csharp 复制代码
private void WriteToTextBox(string message, LogLevel level)
{
    if (TextBox.InvokeRequired)
    {
        TextBox.Invoke(new Action<string, LogLevel>(WriteToTextBox), message, level);
        return;
    }

    try
    {
        // 根据日志级别选择颜色
        Color color = GetColorForLevel(level);

        // 处理不同类型的文本框
        if (TextBox is RichTextBox richTextBox)
        {
            // 对于RichTextBox,可以设置颜色
            richTextBox.SelectionStart = richTextBox.TextLength;
            richTextBox.SelectionLength = 0;
            richTextBox.SelectionColor = color;
            richTextBox.AppendText(message + Environment.NewLine);
            richTextBox.SelectionColor = richTextBox.ForeColor;
        }
        else
        {
            // 对于普通TextBox,只能使用默认颜色
            TextBox.AppendText(message + Environment.NewLine);
        }

        TextBox.ScrollToCaret();
    }
    catch (Exception ex)
    {
        // 错误处理
    }
}

默认颜色设置为:

  • Trace: 灰色
  • Debug: 蓝色
  • Info: 黑色
  • Warn: 橙色
  • Error: 红色

5.6 事件机制

SLog类提供了事件机制,允许外部订阅日志事件:

csharp 复制代码
public event EventHandler<LogEventArgs> LogEvent;

// 触发日志事件
LogEvent?.Invoke(this, new LogEventArgs(
    logItem.Timestamp,
    logItem.Level,
    logItem.Message,
    logItem.ProcessId,
    logItem.ThreadId));

这样,开发者可以轻松地将日志重定向到其他系统,如数据库、网络或其他自定义存储。

三、使用示例

1. 基本用法

csharp 复制代码
// 创建日志实例
var log = new SLog
{
    File = "logs/app_yyyyMMdd.log", // 支持日期格式
    TextBox = richTextBox1, // 使用RichTextBox以支持颜色
    FileLogLevel = LogLevel.Info,   // 文件记录级别
    TextBoxLogLevel = LogLevel.Debug // 界面记录级别
};

// 记录不同级别的日志
log.Trace("这是一条跟踪日志");
log.Debug("这是一条调试日志");
log.Info("这是一条信息日志");
log.Warn("这是一条警告日志");
log.Error("这是一条错误日志");

// 使用完成后释放资源
log.Dispose();

2. 自定义颜色

csharp 复制代码
// 自定义颜色
log.TraceTextBoxColor = Color.DarkGray;
log.DebugTextBoxColor = Color.DarkBlue;
log.InfoTextBoxColor = Color.DarkGreen;
log.WarnTextBoxColor = Color.DarkOrange;
log.ErrorTextBoxColor = Color.DarkRed;

3. 事件订阅

csharp 复制代码
// 订阅日志事件
log.LogEvent += (sender, e) =>
{
    // 可以在这里处理自定义日志记录
    Console.WriteLine($"Custom log: {e.Message} at {e.Timestamp}");
};

四、实现要点

  1. 线程安全:使用ConcurrentQueue确保多线程环境下的安全性。
  2. 资源管理:实现IDisposable接口,确保资源正确释放。
  3. 异常处理:对文件和界面操作进行异常捕获,避免因日志错误影响主程序。
  4. 性能优化:异步处理和批量写入减少I/O操作次数。
  5. 灵活性:支持多种配置选项,满足不同场景需求。

五、完整源码

csharp 复制代码
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Tools
{
    public class SLog : IDisposable
    {
        #region 外部定义
        public enum LogLevel
        {
            Trace,
            Debug,
            Info,
            Warn,
            Error
        }

        public class LogEventArgs : EventArgs
        {
            public DateTime Timestamp { get; set; }
            public LogLevel Level { get; set; }
            public string Message { get; set; }
            public int ProcessId { get; set; }
            public int ThreadId { get; set; }

            public LogEventArgs(DateTime timestamp, LogLevel level, string message, int processId, int threadId)
            {
                Timestamp = timestamp;
                Level = level;
                Message = message;
                ProcessId = processId;
                ThreadId = threadId;
            }
        }
        #endregion

        #region 内部对象
        private readonly ConcurrentQueue<LogItem> _logQueue = new ConcurrentQueue<LogItem>();
        private readonly ManualResetEventSlim _logEvent = new ManualResetEventSlim(false);
        private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
        private Task _processingTask;
        private StreamWriter _fileWriter;
        private DateTime _currentFileDate;
        private string _currentFilePath;
        private bool _isDisposed;
        private readonly object _fileLock = new object();

        // 默认颜色设置
        private static readonly Color TraceColor = Color.Gray;
        private static readonly Color DebugColor = Color.Blue;
        private static readonly Color InfoColor = Color.Black;
        private static readonly Color WarnColor = Color.Orange;
        private static readonly Color ErrorColor = Color.Red;
        #endregion

        #region 外部对象
        public string File { get; set; }
        public TextBoxBase TextBox { get; set; }
        public LogLevel FileLogLevel { get; set; } = LogLevel.Trace;
        public LogLevel TextBoxLogLevel { get; set; } = LogLevel.Trace;
        public event EventHandler<LogEventArgs> LogEvent;
        #endregion

        #region 外部函数
        public SLog()
        {
            _processingTask = Task.Run(ProcessLogQueue, _cancellationTokenSource.Token);
        }

        public void Trace(string message) => Log(LogLevel.Trace, message);
        public void Debug(string message) => Log(LogLevel.Debug, message);
        public void Info(string message) => Log(LogLevel.Info, message);
        public void Warn(string message) => Log(LogLevel.Warn, message);
        public void Error(string message) => Log(LogLevel.Error, message);
        #endregion

        #region 内部函数
        private void Log(LogLevel level, string message)
        {
            var logItem = new LogItem
            {
                Timestamp = DateTime.Now,
                Level = level,
                Message = message,
                ProcessId = Process.GetCurrentProcess().Id,
                ThreadId = Thread.CurrentThread.ManagedThreadId
            };

            _logQueue.Enqueue(logItem);
            _logEvent.Set();
        }

        private void ProcessLogQueue()
        {
            // 写日志
            Action processLogItem = () =>
            {
                while (_logQueue.TryDequeue(out var logItem))
                {
                    ProcessLogItem(logItem);
                }
            };
            while (!_cancellationTokenSource.Token.IsCancellationRequested)
            {
                _logEvent.Wait(_cancellationTokenSource.Token);
                _logEvent.Reset();

                processLogItem();
                while (_logQueue.TryDequeue(out var logItem))
                {
                    ProcessLogItem(logItem);
                }
            }
            // 处理剩余日志项
            processLogItem();
        }

        private void ProcessLogItem(LogItem logItem)
        {
            // 触发日志事件
            LogEvent?.Invoke(this, new LogEventArgs(
                logItem.Timestamp,
                logItem.Level,
                logItem.Message,
                logItem.ProcessId,
                logItem.ThreadId));

            // 格式化日志消息
            var logMessage = FormatLogMessage(logItem);

            // 写入文件
            if (!string.IsNullOrEmpty(File) && logItem.Level >= FileLogLevel)
            {
                lock (_fileLock)
                {
                    WriteToFile(logMessage, logItem.Timestamp);
                }
            }

            // 写入文本框
            if (TextBox != null && logItem.Level >= TextBoxLogLevel)
            {
                WriteToTextBox(logMessage, logItem.Level);
            }
        }

        private string FormatLogMessage(LogItem logItem)
        {
            return $"[{logItem.Timestamp:HH:mm:ss.ff}] [{logItem.Level}] [{logItem.ProcessId},{logItem.ThreadId}] {logItem.Message}";
        }

        private void WriteToFile(string message, DateTime timestamp)
        {
            try
            {
                // 检查是否需要创建新的日志文件
                if (_fileWriter == null || _currentFileDate != timestamp.Date)
                {
                    _currentFileDate = timestamp.Date;
                    _currentFilePath = GetFilePath(timestamp);

                    // 确保目录存在
                    var directory = System.IO.Path.GetDirectoryName(_currentFilePath);
                    if (!string.IsNullOrEmpty(directory) && !System.IO.Directory.Exists(directory))
                    {
                        System.IO.Directory.CreateDirectory(directory);
                    }

                    _fileWriter?.Close();
                    _fileWriter = new StreamWriter(_currentFilePath, true, Encoding.UTF8)
                    {
                        AutoFlush = true
                    };
                }

                _fileWriter.WriteLine(message);
            }
            catch (Exception ex)
            {
                // 日志写入失败时,尝试输出到调试器
                Console.WriteLine($"Failed to write log to file: {ex.Message}");
            }
        }

        private string GetFilePath(DateTime timestamp)
        {
            if (string.IsNullOrEmpty(File))
                return null;

            return File
                .Replace("yyyy", timestamp.Year.ToString("D4"))
                .Replace("MM", timestamp.Month.ToString("D2"))
                .Replace("dd", timestamp.Day.ToString("D2"));
        }

        private void WriteToTextBox(string message, LogLevel level)
        {
            if (TextBox.InvokeRequired)
            {
                TextBox.Invoke(new Action<string, LogLevel>(WriteToTextBox), message, level);
                return;
            }

            try
            {
                // 根据日志级别选择颜色
                Color color = GetColorForLevel(level);

                // 处理不同类型的文本框
                if (TextBox is RichTextBox richTextBox)
                {
                    // 对于RichTextBox,可以设置颜色
                    richTextBox.SelectionStart = richTextBox.TextLength;
                    richTextBox.SelectionLength = 0;
                    richTextBox.SelectionColor = color;
                    richTextBox.AppendText(message + Environment.NewLine);
                    richTextBox.SelectionColor = richTextBox.ForeColor;
                }
                else
                {
                    // 对于普通TextBox,只能使用默认颜色
                    TextBox.AppendText(message + Environment.NewLine);
                }

                TextBox.ScrollToCaret();
            }
            catch (Exception ex)
            {
                // 文本框写入失败时,尝试输出到调试器
                Console.WriteLine($"Failed to write log to textbox: {ex.Message}");
            }
        }

        private Color GetColorForLevel(LogLevel level)
        {
            switch (level)
            {
                case LogLevel.Trace: return TraceColor;
                case LogLevel.Debug: return DebugColor;
                case LogLevel.Info: return InfoColor;
                case LogLevel.Warn: return WarnColor;
                case LogLevel.Error: return ErrorColor;
                default: return TextBox.ForeColor;
            }
        }

        public void Dispose()
        {
            if (_isDisposed) return;

            _isDisposed = true;
            _cancellationTokenSource.Cancel();
            _logEvent.Set();

            try
            {
                _processingTask?.Wait(1000);
            }
            catch (AggregateException)
            {
                // 任务取消时可能抛出异常,可以忽略
            }

            _cancellationTokenSource.Dispose();
            _logEvent.Dispose();
            lock (_fileLock)
            {
                _fileWriter?.Close();
                _fileWriter?.Dispose();
            }
        }

        private struct LogItem
        {
            public DateTime Timestamp { get; set; }
            public LogLevel Level { get; set; }
            public string Message { get; set; }
            public int ProcessId { get; set; }
            public int ThreadId { get; set; }
        }
        #endregion
    }
}

六、总结

本文介绍的SLog类是一个简单但功能完备的日志系统,它结合了文件记录和界面显示的优势,并提供了丰富的自定义选项。通过异步处理和级别过滤,它在提供详细日志信息的同时,几乎不影响应用程序的性能。颜色区分功能使得在界面中查看日志更加直观,而事件机制则为扩展功能提供了可能。

这个实现不依赖任何第三方库,可以直接集成到任何C#项目中,特别是Windows Forms应用程序。开发者可以根据实际需求进一步扩展功能,如添加日志文件大小限制、网络日志传输等。

希望这个简单的日志实现能为您的项目开发提供便利,同时也欢迎根据实际需求进行修改和扩展。

相关推荐
Eiceblue3 小时前
使用 C# 操作 Excel 工作表:添加、删除、复制、移动、重命名
服务器·开发语言·c#·excel
对不起初见i3 小时前
MyBatis-Plus 全方位深度指南:从入门到精通
java·数据库·mybatis
娶不到胡一菲的汪大东3 小时前
C#第五讲 函数的用法
开发语言·c#
wow_DG3 小时前
【MySQL ✨】MySQL 入门之旅 · 第九篇:聚合函数与分组查询
数据库·mysql
木易 士心4 小时前
Jetpack Room 从入门到精通
android·数据库
qh0526wy4 小时前
DUCKLAKE 同步数据库
数据库·oracle
^辞安4 小时前
什么是Mvcc
java·数据库·mysql
coding-fun5 小时前
SuperScript:C#脚本编辑器、C#脚本引擎
开发语言·c#·编辑器
dephixf5 小时前
C#开发一个WinCC浏览器组件,WinCC脚本调用直接打开Web应用
c#·mom·scada·wincc·wincc浏览器