RSS

月別アーカイブ: 5月 2024

C#: NLog

NLog はサードパーティ製のロガーであり、高機能で使いやすい。

NLog を使うためには、NLog パッケージを自分のプロジェクトにインストールしておく必要がある。dotnet コマンドを使う場合の例を下に示す。この時点の 5.3.2 は最新のバージョン番号である。

dotnet add package NLog -- version 5.3.2

ログの設定は XML で行うことができるが、コードでも可能である。

ログの設定ファイルは、NLog.config という名前で実行ファイルと同じフォルダに置いておく必要がある。

NLog.config の例を示す。これは、GitHub 上の NLog チュートリアルのものである。この例では Info 以上のレベルはコンソールとログファイルに、すべてのレベルがコンソールに出力される。

ログが出力されるファイルは、”file.txt” であるが、フルパスを指定していないので実行ファイルと同じ場所に出力される。

ログの出力フォーマットはデフォルトの形式であるが、target タグで layout 属性を指定すればログのフォーマットを設定できる。

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

    <targets>
        <target name="logfile" xsi:type="File" fileName="file.txt" />
        <target name="logconsole" xsi:type="Console" />
    </targets>

    <rules>
        <logger name="*" minlevel="Info" writeTo="logconsole" />
        <logger name="*" minlevel="Debug" writeTo="logfile" />
    </rules>
</nlog>

次のようなコードによりログの設定を行うこともできる。これは GitHub 上の NLog チュートリアルのものである。

var config = new NLog.Config.LoggingConfiguration();

// Targets where to log to: File and Console
var logfile = new NLog.Targets.FileTarget("logfile") { FileName = "file.txt" };
var logconsole = new NLog.Targets.ConsoleTarget("logconsole");
            
// Rules for mapping loggers to targets            
config.AddRule(LogLevel.Info, LogLevel.Fatal, logconsole);
config.AddRule(LogLevel.Debug, LogLevel.Fatal, logfile);
            
// Apply config           
NLog.LogManager.Configuration = config;

ログの出力は次のようなコードで行うことができる。この例は Info レベルで出力するが、例えば Logger.Error() を使うと Error レベルで出力される。レベルには他に Debug, Warn, Critical がある。

Logger.Info("Hello world");

完全な Program.cs のコード例を示す。

// NLog のテスト
//   dotnet add package NLog --version 5.3.2
public static class Program
{
  private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();

  public static void Main(string[] args)
  {
    // ログの設定 (NLog.config を使わない場合)
    if (args.Length > 0)
    {
      var config = new NLog.Config.LoggingConfiguration();
      var logfile = new NLog.Targets.FileTarget("logfile") { FileName = @"C:\temp\nlogfile.txt" };
      var logconsole = new NLog.Targets.ConsoleTarget("logconsole");
      config.AddRule(NLog.LogLevel.Debug, NLog.LogLevel.Info, logconsole);
      config.AddRule(NLog.LogLevel.Debug, NLog.LogLevel.Fatal, logfile);
      NLog.LogManager.Configuration = config;
    }
    // ログメソッドの実行
    logger.Info("I: Hello world");
    logger.Debug("D: Hello world");
    logger.Error("E: Hello world");
    logger.Warn("W: Hello world");
    Console.WriteLine("Done.");
  }
}

次に NLog.config の例を示す。

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

    <targets>
        <target name="logfile" xsi:type="File" fileName="C:\\temp\\nlogfile.txt" />
        <target name="logconsole" xsi:type="Console" />
    </targets>

    <rules>
        <logger name="*" minlevel="Info" writeTo="logconsole" />
        <logger name="*" minlevel="Debug" writeTo="logfile" />
    </rules>
</nlog>

プロジェクトファイル (*.csproj) は次のようになっている。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="NLog" Version="5.3.2" />
  </ItemGroup>

</Project>

 
コメントする

投稿者: : 2024/05/17 投稿先 C#, dotNET

 

C#: Logging

単純に File.AppendAllText() でログを取る。

簡単なプログラムで簡単なログを取るのであれば、File.AppendAllText(string, string?) で十分である。

次の例は File.AppendAllText() を使ったログメソッド log(path, message, level) のコードである。

// 簡単にログを取る。
Action<Object> println = o => Console.WriteLine(o.ToString());
const string LOGFILE = @"C:\temp\logging.txt";

// ログをファイルに追加する。
void log(string path, string message, ErrorLevel level = ErrorLevel.INFO)
{
  var now = String.Format("yyyy-MM-dd HH:mm:ss ", DateTime.Now);
  var line = String.Format("{0:yyyy-MM-dd HH:mm:ss} {1} {2}\n", DateTime.Now, level, message);
  File.AppendAllText(path, line);
}


log(LOGFILE, "ログ情報");
log(LOGFILE, "エラー", ErrorLevel.ERROR);
log(LOGFILE, "警告", ErrorLevel.WARNING);

println("Done.");

// エラーレベル
enum ErrorLevel {DEBUG, INFO, WARNING, ERROR, CRITICAL};

TraceSource

TraceSource は System.Diagnostics 名前空間に含まれるクラスであり、ソースをトレースすることが目的のクラスである。

そのため、結果的にロギングに使用可能である。

次に簡単な使用例を示す。

// TraceSource のサンプル
using System.Diagnostics;
using static System.Diagnostics.TraceSource;

Action<Object> println = o => Console.WriteLine(o.ToString());

println("TraceSource のサンプル.");

// TraceSource インスタンスの名前
const string TSNAME = "testTraceSource";

// リスナの生成
const string LOGFILE = @"C:\temp\TraceSource.log";
var listener = new TextWriterTraceListener(LOGFILE, "LOGFILE");

// エラー以上のみ対応
var ts = new TraceSource(TSNAME, SourceLevels.Error);
ts.Listeners.Add(listener);
ts.TraceEvent(TraceEventType.Information, 0, "情報"); // 出力されない
ts.TraceEvent(TraceEventType.Warning, 1, "警告"); // 出力されない
ts.TraceEvent(TraceEventType.Error, 2, "エラー"); // 出力される
ts.TraceEvent(TraceEventType.Critical, 3, "致命的なエラー"); // 出力される

// ログ出力に反映する。
ts.Flush();
println("Done.");

Microsoft.Extensions.Logging

Microsoft.Extensios.Logging は本格的なロギングのためのインフラストラクチャであり、様々なシチュエーションに対応できる。

最も単純な例

次の例はコンソールにログを表示するだけの例である。

まず、次のコマンドでプロジェクトを作成する。

dotnet new console -o Logging
dotnet add package Microsoft.Extensions.Logging

Program.cs を次のソースのように書き換える。

/* ログの取り方 */
//  パッケージの追加 dotnet add package Microsoft.Extensions.Logging
using Microsoft.Extensions.Logging;

Action<Object> println = o => Console.WriteLine(o.ToString());

println("<< ログのテスト >>");
using ILoggerFactory factory = LoggerFactory.Create(builder => builder.AddConsole());
ILogger logger = factory.CreateLogger("Program");
logger.LogInformation("Hello World!");

Logging.csproj に Microsoft.Extensions.Logging.Console を追加する。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
    <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
  </ItemGroup>

</Project>

dotnet run コマンドでこのプログラムを実行すると、画面に次のように表示される。

<< ログのテスト >>
info: Program[0]
Hello World!

独自ログメソッド

次のコードで builder.AddConsole() はコンソールにログを出力する拡張メソッドである。

using ILoggerFactory factory = LoggerFactory.Create(builder => builder.AddConsole());

ここで出力をカスタマイズするには独自の拡張メソッドを定義する必要がある。

次のような拡張メソッド用のクラスを定義する。(ファイル名は任意)

using Microsoft.Extensions.Logging;

/* 拡張メソッド用のクラス */
public static class LoggingBuilderExtends
{
  public static ILoggingBuilder AddConsole(this Microsoft.Extensions.Logging.ILoggingBuilder builder)
  {
    return builder;
  }

  public static void LogInformation(this ILogger logger, string? message, params object?[] args)
  {
    Console.WriteLine(message??"");
  }
}

Program.cs は変更なしであるが、.csproj ファイルから Microsoft.Extensions.Logging.Console の行は削除する。

このプロジェクトを実行すると、次のように表示される。

<< ログのテスト >>
Hello World!

ログのファイル出力

拡張メソッドを独自に書けば、自由にログ出力が行える。

ただ、Microsoft.Extensios.Logging の様式に従うのなら、TraceSource と併用してファイル出力するのがよさそうである。

次のソースは TraceSource を併用してログをファイル出力する例である。

ただし、これは独自に作ったソースなので、Microsoft.Extensions.Logging の本来のやり方かどうかはわからない。

Program.cs

// TraceSource を利用した Logging
using Microsoft.Extensions.Logging;
using System.Diagnostics;
using static System.Diagnostics.TraceSource;

Action<Object> println = o => Console.WriteLine(o.ToString());

println("<< ログのテスト with TraceSource >>");

// TraceSource リスナの構築
const string LOGFILE = @"C:\temp\TraceSource.log";
var stream = File.AppendText(LOGFILE);
var listener = new TextWriterTraceListener(stream);

// ロガー構築
using ILoggerFactory factory = LoggerFactory.Create(builder => builder.AddTraceSource("Test"));
ILogger logger = factory.CreateLogger<Program>();
logger.LogInformation("Information with TraceSource", listener);
logger.LogError("Error with TraceSource", listener);
println("Done.");

拡張メソッドを含むクラス

using Microsoft.Extensions.Logging;
using System.Diagnostics;
using static System.Diagnostics.TraceSource;

public static class ExtendMethods
{
  public static ILoggingBuilder AddTraceSource(this Microsoft.Extensions.Logging.ILoggingBuilder builder)
  {
    return builder;
  }

  public static void LogInformation(this ILogger logger, string? message, params object?[] args)
  {
    if (args.Length == 0 || args[0] == null)
      return;
    TextWriterTraceListener listener = (TextWriterTraceListener)args[0];
    var now = String.Format("{0:yyyy-MM-dd HH:mm:ss } ", DateTime.UtcNow);
    listener.WriteLine(now + message);
    listener.Flush();
  }
 
  public static void LogError(this ILogger logger, string? message, params object?[] args)
  {
    if (args.Length == 0 || args[0] == null)
      return;
    TextWriterTraceListener listener = (TextWriterTraceListener)args[0];
    var now = String.Format("{0:yyyy-MM-dd HH:mm:ss } ", DateTime.UtcNow);
    listener.WriteLine(now + message);
    listener.Flush();
  }
 }

プロジェクトファイル

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
    <PackageReference Include="Microsoft.Extensions.Logging.TraceSource" Version="8.0.0" />
  </ItemGroup>

</Project>

サードパーティ製のロガー

NLog

NLog は Microsoft.Extensions.Logging と共に使用可能なサードパーティ製のロガーである。

Log4Net

Log4Net は Java 用のロガー Log4J と互換性のあるロガーである。

 
コメントする

投稿者: : 2024/05/16 投稿先 C#, dotNET

 

タグ: ,

Windows 11: スリープを行うとすぐにスリープが解除される。

スリープを行うとスリープがすぐに解除されてしまうことがある。

これはバックグランドで動作しているプロセスがスリープを邪魔しているためで、Windows Update などで発生することがある。

しかし、そうでない場合は、ネットワーク系のプロセスのどれかが邪魔をしているようである。

これを防ぐにはデバイスマネージャを起動し、次のようにするとよい。

  • 「ネットワークアダプタ」を展開する。
  • LAN または Wifi ネットワークアダプタを選んでプロパティを表示する。
  • 「電源の管理」タブを選ぶ。
  • 「電力の節約のために・・・電源をオフできるようにする」のチェックを外す。
  • OK ボタンを押してダイアログボックスを閉じる。
  • デバイスマネージャを閉じる。

デバイスマネージャは Windows キーを押しながらXキーを押し、表示された一覧の中から実行できる。

 
コメントする

投稿者: : 2024/05/14 投稿先 Windows

 

タグ: ,

C#: .NET のバージョンを取得するには

次のサンプルようにすると .NET バージョンを取得できる。

結果は文字列として取得できる。

// Get System version
using System.Runtime.InteropServices;

var netVersion = RuntimeEnvironment.GetSystemVersion();
Console.WriteLine(".NET Version: " + netVersion);

実行例

> dotnet run
.NET Version: v8.0.4
 
コメントする

投稿者: : 2024/05/13 投稿先 C#, dotNET

 

タグ:

C#: マルチプラットフォーム対応の ImageSharp

C# で画像処理を行う場合、System.Drawing 名前空間のクラスを使うことが多いが、これはマルチプラットフォーム対応ではない、つまり Windows のみでしか動作しない。

マルチプラットフォーム対応のパッケージはいくつかあるが、一番、使いやすく軽量で高性能なライブラリが SIX LABORS ImageSharp であるらしい。

これはサードパーティ製のライブラリであるが、オープンソースで開発されていて、基本的に無償で利用できる。

インストール

自分のプロジェクトでこれを利用するには、ImageSharp (ファミリー) をインストールする必要がある。ImageSharp には次の3つのパッケージがある。

  • ImageSharp
  • ImageSharp.Drawing
  • ImageSharp.Web

ImageSharp は基本的なパッケージ、ImageSharp.Drawing は図形描画と文字列描画機能が追加されたパッケージである。

さらに ImageSharp.Web は ImageSharp の機能に SVG ファイルのレンダリング、キャンバスへの描画、データURLの生成が追加されている。

dotnet コマンドを使ってプロジェクトに ImageSharp をインストールするには、次のようにして行う。

dotnet add package SixLabors.ImageSharp

Visual Studio を使う場合は、ソリューションエクスプローラーでプロジェクトを選び、コンテキストメニューの「追加」により「依存関係 / フレームワーク」の参照で SixLabors.ImageSharp を検索してそのプロジェクトにインストールを行う。

基本的な使い方

using の追加

  • using SixLabors.ImageSharp;
  • using SixLabors.ImageSharp.Processing;

Image オブジェクトの作成

  • var image = Image.Load(path);

ここで、path は画像ファイルのパス名、Image は ImageSharp の Image クラスである。この Image クラスは System.Drawing の Image との互換性はない。

画像の表示

ImageSharp 自身には画像を簡単に表示できる機能はないようである。Windows.Forms アプリであれば、PictureBox を利用して画像を表示するのが簡単である。(Windows 専用になるが)

画像情報

Image には Width, Height のような読み出し専用プロパティがある。

画像のファイル保存

Image には SaveAsPng(newpath) のようなファイル保存用メソッドが用意されている。

画像のリサイズ

画像サイズを変更するには、Mutate メソッドを使用する。(あるいは Clone メソッド)

このメソッドはリサイズだけでなく一般に画像を操作するときに使用し、パラメータとして画像操作メソッド (この例ではラムダ式) を取る。

Mutate メソッドのパラメータとしてとれる画像操作は Namespace SixLabors.ImageSharp.Processing で定義されている。

private void imageResize(Image image, int newWidth, int newHeight)
{
    image.Mutate(x => x.Resize(newWidth, newHeight));
}

(注意) Clone メソッドは image のDeep Copy が作成される。

Mutate と Clone メソッド

Mutate と Clone メソッドはイメージ操作をする際に使用するが、これは ProcessingExtensions クラスに含まれる。

  • public static void Mutate(this Image source, Action operation)
  • public static Image Clone(this Image source, Action operation)

これらのメソッドはオーバーロードされており、上で示したものは基本的な形式である。詳しくは、Class ProcessingExtensions を参照のこと。

パラメータ this Image source は対象の Image オブジェクトである。 this が付いているのでこのメソッドは拡張メソッドである。

Action は Action デリゲートであり、System 名前空間で次のように定義されている。

public delegate void Action();

(注意) このデリゲートを使用すると、カスタム デリゲートを明示的に宣言せずに、メソッドをパラメーターとして渡すことができる。

Action のメソッドは、Namespace SixLabors.ImageSharp.Processing で定義されているクラスの描画メソッドである。

(例) Class DrawRectangleExtensions
public static IImageProcessingContext Draw(this IImageProcessingContext source, Pen pen, RectangleF shape)

ImageSharp.Drawing

ImageSharp.Drawing 名前空間は ImageSharp の拡張であり、図形や文字の描画を行うときに使用する。

使用する際には ImageSharp だけでなく、次のように別途インストールする必要がある。(dotnet コマンドを使用する場合)

dotnet add package SixLabors.ImageSharp.Drawing

次の例は、描画領域を定義し、そこに直線を引いて PNG 画像としてファイル保存する。

// ImageSharp.Drawing のテスト
using System.Numerics;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;

Action<Object> println = o => Console.WriteLine(o.ToString());
const string savePath = @"C:\temp\line.png";

// 描画領域を作成
using (var image = new Image<Rgba32>(240, 240))
{
  // ペンを作成
  Pen pen = Pens.Solid(Color.Black, 2);
  // 線を引く (Class DrawLineExtensions,DrawLine メソッド)
  image.Mutate(x => x.DrawLine(pen, new PointF(0, 0), new PointF(239, 239)));
  // PNG 画像としてファイル保存
  image.SaveAsPng(savePath);
}
println($"Saved to {savePath}");

ImageSharp.Web

ImageSharp.Web は ASP.NET と共に使用するミドルウェアである。

リクエストに応じてキャッシュしておいた画像をクライアントへ送信したり、画像を加工して送信するのに使用できる。

 
コメントする

投稿者: : 2024/05/11 投稿先 C#, dotNET

 

タグ: ,

C#: Windows.Forms のリソースと設定の利用

Visual Studio で Windows.Forms アプリのリソースと設定を利用するのは簡単である。

Visual Studio 2022 では次のようにして使用する。

メニューの「プロジェクト / ???? のプロパティ」を選んでプロパティ画面を開く。

「リソース」の「アセンブリリソースを作成する/開く」を選ぶ。

リソースには文字列、イメージ、アイコン、音声 などがあるが、最初は文字列リソースの作成画面が開く。

文字列以外のリソースを作成する場合は、上の画像の「左上」の「文字列」をクリックしてその他の項目に変更できる。

リソースはアプリケーション内で変更できないが、「設定」は変更可能である。

「設定」を作成するには、上の画面で「アプリケーション設定を作成する/開く」をクリックする。

そうすると、次のような画面が開く。この例ではフォームの幅と高さを設定している。デフォルトで酒類は「文字列」なので整数に変更してある。

プログラム内で、リソースと設定をアクセスするには次のような using 行を追加しておくと便利である。

   using MyRes = global::UsingResource.Properties.Resources;
using MySet = global::UsingResource.Properties.Settings;

この例で UsingResource はアプリ名である。

次のサンプルは、文字列と画像リソースを表示する。さらに設定としてフォームサイズを記憶しておき、次回起動したときに同じサイズでフォームを表示するものである。

このコードからわかるようにリソースの画像は pictureBox.Image プロパティに直接代入できる。

namespace UsingResource
{
    using MyRes = global::UsingResource.Properties.Resources;
    using MySet = global::UsingResource.Properties.Settings;

    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        /// <summary>
        /// フォームがロードしたとき
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Form1_Load(object sender, EventArgs e)
        {
            // リソースの使用
            label1.Text = MyRes.AppName;
            label2.Text = MyRes.Version;
            pictureBox1.Image = MyRes.FILE_T01;
            pictureBox2.Image = MyRes.PNG1;

            // 設定の使用 (フォームサイズの復元)
            this.Width = MySet.Default.FormWidth > 120 ? MySet.Default.FormWidth : 120;
            this.Height = MySet.Default.FormHeight > 90 ? MySet.Default.FormHeight : 90;
        }

        /// <summary>
        /// フォームが閉じるとき
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            // フォームサイズを保存する。
            MySet.Default.FormWidth = this.Width;
            MySet.Default.FormHeight= this.Height;
            MySet.Default.Save();
        }
    }
}
 
コメントする

投稿者: : 2024/05/06 投稿先 C#, dotNET

 

タグ: , ,

C#: マウスホイールの使い方

Visual Studio 2022 でマウスホイール・イベントハンドラを作ろうとすると、マウスイベントの中にマウスホイール・イベントが見つからない。

将来の Visual Studio バージョンでは追加される可能性もあるが、現在のところは手動でイベントハンドラを追加する必要がある。

まず、「private void WheelEventHandler(object? sender, System.Windows.Forms.MouseEventArgs e)」というハンドラを追加する。(ただし、メソッド名は任意)

このハンドラをフォームロードの中などで MouseWheel イベントに追加する。

MouseWheel += WheelEventHandler;

これにより、マウスホイールが操作されたとき、WheelEventHandler が呼び出されるようになる。

次にマウスホールが回されたときのイベント内容がこのハンドラ内で e.Delta という MouseEventArgs のプロパティで取得できる。

この値は、マウスホイール回転量であり、方向により正負の値を取る。

次のサンプルは、pictureBox に表示されている画像をマウスホイールにより拡大・縮小表示する例である。

この例では Ctrl キーを押しながらマウスホイールを回した時だけ反応するようにしている。

この「Ctrl キーを押しながら」の判別は Keys.Control が 「Control.ModifierKeys & Keys.Control」と等しいかと言うことで判別している。

        /// <summary>
        /// マウスホイールイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void WheelEventHandler(object? sender, System.Windows.Forms.MouseEventArgs e)
        {
            int w, h;
            const float a = 1.2f;

            if (String.IsNullOrEmpty(this.ImagePath))
            {
                return;
            }

            int d = e.Delta;

            var c = Control.ModifierKeys & Keys.Control;
            if (c == Keys.Control)
            {
                var path = GetImagePath(); //  Path.GetDirectoryName(this.ImagePath) + @"\" + this.ImageFiles[this.Index];
                Size size = GetImageSize(path);
                float imageAspectRatio = (float)size.Width / (float)size.Height;
                // 拡大・縮小
                panel1.Dock = DockStyle.None;
                if (d < 0)
                {
                    // 縮小
                    w = (int)Math.Round((float)panel1.Width / a);
                    h = (int)Math.Round((float)panel1.Height / a);
                    panel1.Size = new Size(w, h);
                    pictureBox1.Size = new Size(w, h);
                }
                else
                {
                    // 拡大
                    w = (int)Math.Round((float)panel1.Width * a);
                    h = (int)Math.Round((float)panel1.Height * a);
                    panel1.Size = new Size(w, h);
                    pictureBox1.Size = new Size(w, h);
                }
                pictureBox1.SizeMode = PictureBoxSizeMode.Zoom;
                pictureBox1.ImageLocation = path;
            }
            else
            {
                // スクロール
 
            }
        }
 
コメントする

投稿者: : 2024/05/02 投稿先 C#, dotNET

 

タグ: ,