Avaloniaログコンポーネントの実装と最適化ガイド

Avaloniaログコンポーネントの実装と最適化ガイド

Avaloniaに基づくログコンポーネントの実現案を深く解析し、インターフェースとファイルの二重出力メカニズムを検討し、最適化可能な改善点を提案する。

最后更新 2025/07/03 22:24
沙漠尽头的狼
预计阅读 6 分钟
分类
Avalonia UI
专题
Avalonia UIオープンソースプロジェクト C#オープンソースプロジェクト
标签
.NET C# Avalonia UI オープンソースプロジェクト オープンソースソース

背景は

Avalonia 目前没有富文本框可实现日志输出显示,但提供了SelectableTextBlock控件可以替换,这是站长实现的一个日志组件效果:

ログ時間、ログレベル、ログの詳細などを表示することができ、バックグラウンドはインターフェイスに出力されるほか、テキストファイルにも永続的に出力することができ、より多くのログ情報の表示は自己拡張することができ、次に実装プロセスを説明し、問題点を指摘し、PRを歓迎します。

使用する。

インストール::

NuGet\Install-Package CodeWF.LogViewer.Avalonia -Version 1.0.10.2

Viewの使い方:

xmlns:log="https://codewf.com"

<log:LogView />

ログの出力:

Logger.Debug("调试日志");
Logger.Info("信息日志");
Logger.Warn("警告日志");
Logger.Error("错误日志");
Logger.Fatal("致命日志");

実現すること

只说关键部分代码,具体代码可浏览CodeWF.LogViewer仓库。

程序通过Logger类输出日志,该类将日志信息缓存到ConcurrentQueue<LogInfo> Logs集合,Logger类定义如下:

public static class Logger
{
    public static LogType Level = LogType.Info;
    public static string LogDir = AppDomain.CurrentDomain.BaseDirectory;
    internal static readonly ConcurrentQueue<LogInfo> Logs = new();

    public static void RecordToFile()
    {
        Task.Run(async () =>
        {
            while (true)
            {
                while (TryDequeue(out var log))
                {
                    var content =
                        $"{log.RecordTime}: {log.Level.Description()} {log.Description}{Environment.NewLine}";
                    AddLogToFile(content);
                }

                await Task.Delay(TimeSpan.FromMilliseconds(100));
            }
        });
    }

    public static bool TryDequeue(out LogInfo info)
    {
        return Logs.TryDequeue(out info);
    }

    public static void Log(int type, string content)
    {
        var logType = (LogType)type;
        if (Level > logType) return;
        Logs.Enqueue(new LogInfo(logType, content));
    }

    public static void Debug(string content)
    {
        if (Level <= LogType.Debug)
        {
            Logs.Enqueue(new LogInfo(LogType.Debug, content));
        }
    }

    public static void Info(string content)
    {
        if (Level <= LogType.Info)
        {
            Logs.Enqueue(new LogInfo(LogType.Info, content));
        }
    }

    public static void Warn(string content)
    {
        if (Level <= LogType.Warn)
        {
            Logs.Enqueue(new LogInfo(LogType.Warn, content));
        }
    }

    public static void Error(string content, Exception? ex = null)
    {
        if (Level > LogType.Error) return;

        var msg = ex == null ? content : $"{content}\r\n{ex.ToString()}";

        Logs.Enqueue(new LogInfo(LogType.Error, msg));
    }

    public static void Fatal(string content, Exception? ex = null)
    {
        if (Level > LogType.Fatal) return;

        var msg = ex == null ? content : $"{content}\r\n{ex.ToString()}";

        Logs.Enqueue(new LogInfo(LogType.Fatal, msg));
    }

    public static void AddLogToFile(string msg)
    {
        try
        {
            var logFolder = System.IO.Path.Combine(LogDir, "Log");
            if (!Directory.Exists(logFolder))
            {
                Directory.CreateDirectory(logFolder);
            }

            var logFileName = System.IO.Path.Combine(logFolder, $"Log_{DateTime.Now:yyyy_MM_dd}.log");
            File.AppendAllText(logFileName, msg);
        }
        catch
        {
            // ignored
        }
    }
}

テキスト·ファイルへのログの出力のみ

如果只是输出日志到文本文件,而不需要输出到界面,需要主动调用Logger.RecordToFile()方法定时检查日志输出。

テキスト·ファイルとビューへのログの同時出力

前提:这里不需要调用Logger.RecordToFile()方法

我们先看视图LogView.axaml,该部分使用ScrollViewer包裹SelectableTextBlock,以实现日志滚动查看及日志文本的可选择复制:

<ScrollViewer
    x:Name="LogScrollViewer"
    HorizontalScrollBarVisibility="Auto"
    PointerPressed="LogScrollViewer_OnPointerPressed"
    VerticalScrollBarVisibility="Auto">
    <SelectableTextBlock
        x:Name="LogTextView"
        TextAlignment="Start"
        TextWrapping="Wrap">
        <SelectableTextBlock.ContextMenu>
            <ContextMenu x:Name="LogContextMenu">
                <MenuItem Click="Copy_OnClick" Header="复制" />
                <MenuItem Click="Clear_OnClick" Header="清空" />
                <MenuItem Click="Location_OnClick" Header="查看日志" />
            </ContextMenu>
        </SelectableTextBlock.ContextMenu>
    </SelectableTextBlock>
</ScrollViewer>

LogView.axaml.cs内调用RecordLog()方法定时读取缓存日志,并再调用LogNotifyHandler方法写入界面,及调用Logger.AddLogToFile方法写入文本文件,代码不多,下面是核心部分:

partial class LogView : UserControl
{
    // ...
    private void RecordLog()
    {
        if (_isRecording) return;

        _isRecording = true;

        Task.Run(async () =>
        {
            while (true)
            {
                while (Logger.TryDequeue(out var log)) LogNotifyHandler(log);

                await Task.Delay(TimeSpan.FromMilliseconds(100));
            }
        });
    }

    private void LogNotifyHandler(LogInfo logInfo)
    {
        if (Logger.Level > logInfo.Level) return;

        _synchronizationContext.Post(o =>
        {
            var inlines = _textView.Inlines;
            try
            {
                if (inlines?.Count > MaxCount)
                {
                    for (var i = 0; i < 3; i++)
                    {
                        var needRemoveElement = inlines.First();
                        if (needRemoveElement != null)
                        {
                            inlines.Remove(needRemoveElement);
                        }
                    }
                }

                var start = _textView.Text.Length;

                inlines?.Add(
                    new Run($"{logInfo.RecordTime}")
                    {
                        Foreground = new SolidColorBrush(Color.Parse("#8C8C8C")),
                        BaselineAlignment = BaselineAlignment.Center
                    });
                inlines?.Add(GetLevelInline(logInfo.Level));
                inlines?.Add(new Run(logInfo.Description)
                {
                    Foreground = new SolidColorBrush(Color.Parse("#262626")),
                    BaselineAlignment = BaselineAlignment.Center
                });
                inlines?.Add(new Run(Environment.NewLine));

                Logger.AddLogToFile(
                    $"{logInfo.RecordTime}: {logInfo.Level.Description()} {logInfo.Description}{Environment.NewLine}");

                _textView.SelectionStart = start;
                _textView.SelectionEnd = _textView.Text.Length;
                _scrollViewer.ScrollToEnd();
            }
            catch
            {
                // ignored
            }
        }, null);
    }

    private Span GetLevelInline(LogType level)
    {
        var content = level.Description();

        // 创建宽度为零的透明文本,用于复制使用
        // TODO:复制还是有问题,会错位
        var zeroWidthText = new Run($"【{content}】")
        {
            Foreground = Brushes.Transparent, FontSize = 0.001
        };

        // 视觉显示的文本,不会被复制使用
        var border = new Border
        {
            BorderBrush = GetLevelForeground(level),
            Background = GetLevelBackground(level),
            BorderThickness = new Thickness(1),
            CornerRadius = new CornerRadius(2),
            Padding = new Thickness(8, 0),
            Margin = new Thickness(8, 2),
            VerticalAlignment = VerticalAlignment.Center,
            IsHitTestVisible = false,
            Child = new TextBlock
            {
                Text = content,
                Foreground = GetLevelForeground(level),
                IsHitTestVisible = false
            }
        };
        var levelSpan = new Span();
        levelSpan.Inlines.Add(zeroWidthText);
        levelSpan.Inlines.Add(border);
        return levelSpan;
    }
    // ...
}

ログの種類に基づいて前景、背景色などを取得するコードは省略していますが、これは問題ありません。

存在する問題は

看上面的GetLevelInline方法,该方法生成日志级别块,使用的Border套日志级别描述(调试、错误等),实现日志类型带边框效果,但复制存在问题:

选择的7:56 调试 模块块并按Ctrl + C复制,再粘贴到记事本,复制出来是调试】模块名称A-,很明显的错位问题。

复制的应该是文本内容,但Border是不允许复制的,所以代码中留了注释创建宽度为零的透明文本,用于复制使用

// 创建宽度为零的透明文本,用于复制使用
// TODO:复制还是有问题,会错位
var zeroWidthText = new Run($"【{content}】")
{
    Foreground = Brushes.Transparent, FontSize = 0.001
};

詳しくは言いませんが、解決策はありますか?PRをお待ちしております。

まとめまとめまとめ

この記事は主にPRを求めています,コンポーネントがあなたに役立つ場合,歓迎,倉庫アドレス:

  • CodeWF.LogViewer:https://github.com/dotnet9/CodeWF.LogViewer

次の記事カスタムTabItem境界実装を共有する:

Keep Exploring

延伸阅读

更多文章
同分类 / 同标签 2026/01/11

Avalonia ClipboardとDataGridの問題点

Avaloniaデスクトップソフトウェアの最近の開発で解決された2つの問題を文書化します:クリップボードのコピーのクラッシュ、タブの切り替えDataGridのキートン、原因の分析と解決策

继续阅读
同分类 / 同标签 2025/08/09

Avalonia:Resx/XML/JSONフォーマットをシームレスにサポートするAvaloniaの多言語ソリューション

Avaloniaフレームワーク用に特別に設計された多言語管理ライブラリで、プラグインアーキテクチャを通じて多言語サポートロジックを再構築し、従来のResxリソースファイルと互換性があるだけでなく、XMLとJSONフォーマットのサポートを追加し、型セーフなリソース参照、動的言語切り替えなどの機能を提供し、多言語開発をより簡単かつ効率的にします。

继续阅读