Day After Day
tsurezure naru mamani...
ANOTHER DECADE

from 2022 when it's begining after/with CORONA Virus.

初めてのC# 備忘録

7月
7
2023
Back
Alt+HOME


最近言語に関する興味が結構あきらめ悪く続く。以前から気にはなっていたC#を試してみたくなっておもむろに始めてしまった。(最終更新 2023.12.12

先日までPythonでTLMForwarderを1.0.18までアップしてきたが、何か違う言語でないと無理なのかと言う局面に立ち至った。GUIであるという点が重荷なのかも知れない。(とにかく初めてだったので)どうせ初めてついでにと、全く同じ仕様のアプリをC#で作ってみることにした。


これで結果OKという、備忘録として逐次追加していきます。何の脈略も有りません。


Visual Basic 6の後、2010年前後に一時期 Visual Basic 2008/2010をかじった事が有って、何か?懐かしい感じがする。 当時重~く感じたIDEもSSDのお陰も有ってか快適。馴染むまで暫くやってみよう!!

FormMainの立ち上げ時に、設定値をFormConfigに書き込む


public partial class FormMain : Form
{
	private readonly string filePath = @"Config.ini";
	private readonly FormConfig formConfig = new();				// ここでインスタンスだけ作って表示はしない

	public FormMain()
	{
		InitializeComponent();
	}
	
	private void FormMain_Load(object sender, EventArgs e)			// メインフォームのロードイベント
	{
		/* 設定ファイルよりデータを読みConfigフォームに表示する関数呼び出し */
		ReadSettingsFromfile();
	}

	private void ReadSettingsFromfile()
	{
		// ファイルが存在するか確認
		if (File.Exists(filePath))
		{
			// ファイルからすべての行を読み込む
			string[] lines = File.ReadAllLines(filePath);		 // 設定ファイルからの行データを取得

			//各行を処理する
			foreach (string line in lines)
			{
				// セパレータ'='で分割
				string[] parts = line.Split('=');

				if (parts.Length == 2)
				{
					//項目名をテキストボックス名として取得
					string textBoxName = parts[0].Trim();	// keyとテキストボックス名を同じにしてある
					string textBoxText = parts[1].Trim();	// セパレータ'='の左と右


					// インスタンスの対応するテキストボックスに設定値を代入
					if (formConfig.Controls.ContainsKey(textBoxName))
					{
						TextBox textBox = (TextBox)frmSettings.Controls[textBoxName];
						textBox.Text = textBoxText;
					}
				}
			}
		}
	}
	
	/* 設定パネルを表示(データはメインフォーム立ち上げ時代入済み */
	private void BtnConfig_Click(object sender, EventArgs e)
	{
		formConfig.ShowDialog();					// インスタンスの表示のみ
	}

別フォームのインスタンスをクラス宣言の直下で定義しておくことで、そのフォームを立ち上げずとも、有るべきコントロールを利用することが出来る。 つまり、そのインスタンスに属するテキストボックスにテキストを保存したり、string str = formConfig.Name.Text; のようにして保存しているデータを読み出すことが出来る。 複数のテキストボックスが Typedefのように使えることになる。
もちろん、ボタンイベントでも新たにインスタンスは定義せず単に ShowDialog() するだけですでに記載済みの設定ページが開く。

Pythonのprint,Cのprintfのようにデバッグ出力したい


// satと言う配列の中身を item として一つずつ、IDEの出力に表示する。
foreach (string item in sat)
{
	Debug.WriteLine(item);
}

[ツール] → [オプション] → [デバッグ] → [全般]で、「出力ウィンドウの文字をすべてイミディエイトウィンドウにリダイレクトする」 にチェックしていると余分な出力が省かれて見易い。

リストを使い回すには慣れるまでグローバルにするのが簡単


public partial class FrmMain : Form
{
	private readonly List MyList = new();		// グローバルスコープでのリスト宣言

	private void MakeMyList()
	{
		// リストするデータitemの抽出・構成などの処理
		
		MyList.Add(item);
	}
	
	// 一例としてComboBoxのクリックイベントで利用
	private void ComboBox_SelectionIndexChanged(object sender, EventArgs e)
	{
		foreach (string[] line in MyList)
		{
			if (line[0] == ComboBox.Text)
			{
				Label.Text = line[1];
			}
		}
	}
}

private List MakeMyList()のように関数内でリストの宣言・作成をして、return MyList で他の関数に渡すのが推奨されているが、 グローバルを多用せず、コードの可読性を損なわなければ理解しやすい。

C#で作ったDLLを動的に切り替えて、選択した衛星毎に処理を変える


/* DLLファイルのパスを作成 */
string dllFilePath = "./dll/" + LblNoradID.Text + ".dll";		// カレントディレクトリー/dll/53106.dll

/* dllが供給されているかチェック */
if (File.Exists(dllFilePath))
{
	/* DLLをロードする */
	Assembly assembly = Assembly.LoadFrom(dllFilePath);

	/* 利用するクラスの完全修飾名(名前空間を含む"namespace.class")を指定 */
	string className = "_53106._53106";

	/* クラスのTypeを取得する */
	Type classType = assembly.GetType(className);

	/* クラスのインスタンスを生成する */
	object classInstance = Activator.CreateInstance(classType);

	/* メソッドの名前を指定して呼び出す */
	MethodInfo method = classType.GetMethod("ReceiveDataFormat");
	method.Invoke(classInstance, null);
}
else
{	/* ファイルが無い旨を知らせる */	
	MessageBox.Show("Not exists " + dllFilePath + " for " + CmbSatName.Text);
}

アプリケーション運用ログの作成


using Serilog;

public static void CreateLogFile()
{
			
	Log.Logger = new LoggerConfiguration()
		.MinimumLevel.Information()						  // Information以上に重要なもののみ
		.MinimumLevel.Override("Microsoft", Serilog.Events.LogEventLevel.Warning) // MicrosoftからのWarning以上も
		.Enrich.FromLogContext()
		.WriteTo.File(
			@".\\syslog\\TLMForwarder-.log",
			outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] {Message}{NewLine}{Exception}",
			rollingInterval: RollingInterval.Day,
			retainedFileCountLimit: 10
		)
		.CreateLogger();        // 上記設定でLoggerインスタンスを作成

	try
	{
		Log.Information("Starting Application and Logger...");
	}
	catch (Exception ex)
	{
		Log.Fatal(ex, "The Application has closed.");
	}
	// 終了時には必ず	Log.CloseAndFlush(); を実行する (quitボタンなど)
}

/*-------------------------------------------------------------------------------------*/
using Serilog;

praivate void SomeThing()
{
	// 何らかの処理関数
	Log.Information("処理に関する情報・結果など");
	Log.Warning("注意など");
	Log.Error("エラーメッセージ");
	Log.Debug("デバッグに必要なコメントなど");
}


JSONファイルで設定の別ファイル化や複数のログファイルに対応できる。

その他のファイル作成とネーミング


private readonly FrmSettings frmSettings = new();

string directory = frmSettings.TxtDirectory.Text;

try
{
	char lastChar = directory[^1];
	if (lastChar != '\\')				// D:\sample\data  のように最後が \ でない
	{
		directory += "\\";			// D:\sample\data\ にする
	}
}
catch (IndexOutOfRangeException)
{
	MessageBox.Show("Settingsでフォルダを指定してください。");
	return;
} 

/* フォルダが存在しない時は作成する */
if (!Directory.Exists(directory))
{
	Directory.CreateDirectory(directory);
}

/* ファイル名を作成する */
string timeStamp = DateTime.UtcNow.ToString("s").Replace(":", "");		// UTCを使用している
string fileName = "ファイル名" + "_" + timeStamp + "_" + ".txt";		// filename_2023-09-22T003032.txt
string filePath = Path.Combine(directory, fileName);		// D:\sample\data\filename_2023-09-22T003032.txt

/* ファイルパスを使ってファイルを作成 */
File.Create(filePath).Close();			// この時フォルダが存在しない場合フォルダも作成する。


なお、ToString("s").Replace(":", "") の意味は "s" で 2023-09-22T00:30:32 となり ':' が挿入される。 これはファイル名として使えない文字でなので .Replace(":", "") で ':' を取り除いている。

フォルダ内の特定ファイルを削除


/* 特定フォルダ内の空のファイルが有れ削除する */
string[] files = Directory.GetFiles(directory);
foreach (string file in files)
{
	FileInfo fileInfo = new(file);
	if (fileInfo.Length == 0)		// サイズゼロの空ファイル
	{
		File.Delete(file);
	}
}
			
/* フォルダ内にサブフォルダがある場合 */
string[] files = Directory.GetFiles(directory, "*", SearchOption.AllDirectories);

Formのテキストデータをコントロールの順にテキストファイルに書き込む


public void SaveDatatoFile()
{
	/* ファイルにデータを書き込む */
	using (StreamWriter writer = new(filePath))
	{
		foreach (Control control in Controls.OfType<TextBox>().Reverse())
		{
			if (control is TextBox textBox)
			{
				string key = textBox.Name[3..];		// テキストボックス名の頭文字Txtを省き keyとし
				string value = textBox.Text;		// テキストの内容をvalueとして
				string line = $"{key}={value}";		// イコールで繋いだものを
				writer.WriteLine(line);             	// 一行ずつ書き込む
			}
		}
	}
}

問題は、foreach (Control control in Controls) でOKなのだが、最初に作った(手書き)ファイルの項目順と逆になる場合が有る。 その時は、Controls.OfType<TextBox>().Reverse() とすると全く逆順に出来る。(<TextBox>は代替文字ではなく、そのまま記述)

受信データを受信した長さに縮める


C#では下記のように変数の長さを指定する必要が有るため、指定した長さのデータとして余分なデータエリアを持ち回る事になる。 そこで受信するたびに、受信した長さに切り詰める。

/* 受信用バッファを定義 */
byte[] buffer = new byte[1024];

// 受信が無くても1秒に一度ループさせる
client.ReceiveTimeout = 1000;			// clientは Socketで定義し、Connectで接続済みとする

// 受信ループ(clientStop変数がTrueになったらループを出る)
while (!clientStop)
{
	try
	{
		// ソケットからデータを受信、実長さも取得
		int bytes = client.Receive(buffer);

		// 実長さに沿った一時バッファを生成
		byte[] tmpBuff = new byte[bytes];

		// 受信データ(1024バイト)を実長さにカット
		Array.Copy(buffer, tmpBuff, bytes);

		// メインスレッドにデータ(実長さ)を渡す

	}
	catch
	{
		// タイムアウトでループしても空回しする
		continue;
	}
}

タイムアウトを設けているのは、受信の無い時、ループが回らずクローズ操作(clientStop = true;)をしても、 クライアント及びサブスレッドが閉じないため1秒ごとにループさせる。その時は空データが発生しないよう catch で単にループさせる。

主な時刻表示パターン


string timestamp = DateTime.Now.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ");

yyyy:	 	年(4桁)
MM: 		月(2桁)
dd: 		日(2桁)
T		接続文字(spaceでも可)
HH: 		時間(2桁)
mm: 		分(2桁)
ss: 		秒(2桁)
fff: 		ミリ秒(3桁)
Z: 		タイムゾーン(ZはUTCを表します)

timestamp 変数には "2023-10-13T15:05:00.187Z" のような形式のタイムスタンプ文字列が格納される。(テレメトリーはこの表記)

非同期関数の返り値と変数のスコープに関する(._.)φ


private static something()
{
	string data = someData.Trim();
	string url = "https://database.server.com"
	
	// データを非同期送信関数に渡し、返り値を受け取る
	string responce = SendToServer(data, url).Result;	// 返り値を受けるには呼び出し関数に.Resultを付ける
}

private static async Task SendToServer(string someData, string webUrl)
{
	string data = someData.Trim();
	string url = webUrl.Trim();

	// データをエンコードする
	StringContent content = new(data, Encoding.UTF8, "application/json");

	// UDPソケットを作成
	using HttpClient client = new();

	// 送信
	client.Timeout = TimeSpan.FromSeconds(2);		// 応答の待ち時間を指定する(2秒)
	
	HttpResponseMessage res = await client.PostAsync(url, payload);

	// 返り値用に変数を定義					// if文の内側で代入された値を、外側でリターンするには
	string responce = string.Empty;				// リターンと同じレベル(if文の外側)で定義しておく

	// レスポンスを文字列にする
	if (res != null)
	{
		responce = await res.Content.ReadAsStringAsync();	// 返り値をセット(if分内側)

	}
	else
	{
		responce = $"Error: BadRequest";			// 返り値をセット(if分内側)
	}

	// 結果を呼び出し元へ返す
	return responce;					// リターンする
}

異なるスレッド間のデータ書き込み(TxtBox)


private void UpdateTextBox(string text)
{
	if (TxtSample.InvokeRequired)
	{
		TxtSample.Invoke(new Action(UpdateTextBox), text);
	}
	else
	{
		TxtSample.Text = text;
	}
}

private void SomeBackgroundThreadFunction()
{
	string response = //何らかのバックグラウンド処理を行う

	// UIスレッド上でテキストボックスを更新する
	UpdateTextBox(response);
}

private void BtnStartProcessing_Click(object sender, EventArgs e)
{
	// バックグラウンドスレッドを開始する
	Thread backgroundThread = new Thread(SomeBackgroundThreadFunction);
	backgroundThread.Start();
}

TxtBoxは一般的に初期スレッド(UIスレッド)で起ち上げられるフォームに存在します。
その初期スレッドから別スレッド(backgroundThread)を起ち上げ、何らかの処理をした結果を、初期スレッド上の TxtBox.Textに書き込もうとするとエラーが発生します。 それを回避して安全に書き込む事が出来る様にする方法です。

異なるスレッド間のデータ書き込み(DataGridView)


private void UpdateGrid(string data)
{
	if (grdGrid.InvokeRequired)
	{
		grdGrid.Invoke(new Action(UpdateGrid), data, counter);
	}
	else
	{
		// DataGridView のセル内容を更新する
		if (data != null && counter >= 0)
		{
			GrdDigipeater.Rows.Insert(
				counter,
				data[0].Trim(),
				data[1].Trim(),
				data[2].Trim(),
				data[3].Trim()
			);	
		}
		
		// 縦スクロールバーが有る場合、表示可能な最終行まで下げる
		GrdDigipeater.FirstDisplayedScrollingRowIndex 
				= GrdDigipeater.Rows.GetLastRow(DataGridViewElementStates.Visible);
	}
}

private void SomeBackgroundThreadFunction()
{
	counter++;		// データ作成回数などをカウントする-->グリッドの行番号になるものとする
	string[] data = // 受信データ等を配列にする

	// UIスレッド上でDataGridViewのセルを更新する
	UpdateGrid(data, counter);
}

private void BtnStartProcessing_Click(object sender, EventArgs e)
{
	// バックグラウンドスレッドを開始する
	Thread backgroundThread = new Thread(SomeBackgroundThreadFunction);
	backgroundThread.Start();
}

DataGridViewである条件で行に色付け


	GrdDigipeater.Rows.Insert(
		row,
		data[0].Trim(),
		data[1].Trim(),
		data[2].Trim(),
		data[3].Trim()
	);

	// data[0]が特定の内容の時行全体を色分けする
	DataGridViewCellStyle stringColor = new();
	if (data[0].Trim() == [比較データ}
	{
		// 背景を黒にし、文字をオレンジにする
		stringColor.ForeColor = Color.Orange; 
		stringColor.BackColor = Color.Black;
		GrdDigipeater.Rows[row].DefaultCellStyle = stringColor;
	}
	else
	{
		// 条件が合わない時はデフォルトに戻す
		GrdDigipeater.Rows[row].DefaultCellStyle = null;
	}

データを表示させてから、色指定するところがみそ。

Back
Alt+HOME