ゲームエフェクトデザイナーのブログ | A Real-Time VFX Artist's Blog

About Making Materials on UE, Making Tools with C#, etc

C# UE4のフォルダカラーを設定するリストを作成するツール

UE4のコンテンツブラウザのフォルダに色を設定することができますが、設定はユーザーごとに下記の ini ファイルに保存されます。

プロジェクト名\Saved\Config\Windows\EditorPerProjectUserSettings.ini


しかし texture フォルダが100個あるような場合、自前でリストを用意するのは大変です。
そこで、上記 ini ファイルに貼り付けるためのリストを生成するツールを作りました。

GitHubトップにあるbinaryフォルダ内の「UE4_MakeFolderColorList.exe」をクリックした先のページで「Download」ボタンを押してDLしてお使いください。

ツールを使ったフォルダカラー設定の流れ


起動した際の画面はこちら。
一通り設定を終えたら「リストアップ」ボタンを押すと右側のパネル内に一覧します。

f:id:moko_03_25:20190619230457j:plain

そちらを先ほどの ini ファイル内にペーストします。

一度でもフォルダカラーを設定していれば [PathColor] という項目があるのでその下に貼り付けます。項目が無ければ項目ごと貼り付けます。

f:id:moko_03_25:20190619230506j:plain

UE4でプロジェクトを開けばこの通り、フォルダカラーがバッチリ適用されています。

f:id:moko_03_25:20190619230514j:plain

 

実装部分についてのメモ


ツールのカラーダイアログで設定した値は各チャンネル 0~255 の 8bit です。
それを 0~1 の値に収めるために 255 で割りますが、それだけではダメで Math.Pow(x, 2.2) でガンマ補正分暗くしてやる必要があります。

こちらの処理を入れておかないと明るく表示されてしまいました。
フォルダカラーはリニアスペース扱いなのだと思います。

C# タスクスケジューラへタスクを登録するツール

作ってみました!

時間とユーザー名だけ自由に指定できるようにしたものです。
ログイン時のユーザー名も取得して表示しています。

f:id:moko_03_25:20190618012949j:plain

元々Windowsのタスクスケジューラで設定するタスクはXMLファイルでインポート/エクスポート可能なので、XMLをインポートする形にします。

タスク追加の実装


まずVisual StudioWindowsフォームアプリのプロジェクトを開始したら、ソリューションエクスプローラーの「参照」で右クリック、「参照の追加」でダイアログを開き、「COM」を選択して検索欄に「task」と入力。

すると「TaskScheduler 1.1 Type Library」が出てくるのでチェックをONにしてからOKボタンを押します。

次にUsingディレクティブに「TaskScheduler」を追加します。

ここまでが準備になります。

あとはこちらの記事の「XMLからインポートすることでタスクを登録する」の項目を参考に実装すればOKです。

最後に実行者がタスクをチェックできるように、タスクスケジューラを起動したい場合はusingディレクティブに「System.Diagnostics」を追加して、下記のように記述すればOK(ちなみに私の環境はwindows10です)。

Process.Start(@"C:\WINDOWS\system32\taskschd.msc");

これでうまくいきました!

ちなみに同じパス&名前のタスクがある場合は上書きされるだけでエラーにはなりません。

XMLの書き換えについて


おまけです。

XMLの特定のタグの値を書き換えるには、下記の方法が一番楽でした。
Usingディレクティブに「System.Xml」を追加するのをお忘れなくっ。

// 関数:XMLの置換
// 引数「path」はxmlの場所で「name」と「value」はXML内の <name>value</name>
private void WriteSettings(string path, string name, string value) { XmlDocument xml = new XmlDocument(); xml.Load(path); XmlNodeList xns = xml.GetElementsByTagName(name); foreach (XmlNode xn in xns) { xn.InnerText = value; } xml.Save(path); }

以上、これでタスクを登録するようなツールを作ってチーム内に配布できますね!

お試しあれ!

2019.6.19 追記:
自宅では登録できるのですが、会社で使ってみて登録できなかったので調査中‥
また、ログインユーザー名を入力できるようにしてますが意味が無かった模様。。

フリーの3Dモデリングソフト「XISMO2」の導入と基本操作

無料で商用利用可能で簡単なポリゴン編集ができてFBXで出力してUE4にもっていける3Dソフトは無いかなと思ったのですが、 GNU General Public License の「blender」の他に「XISMO2(キスモ)」という非商用 / 商用ともにフリーで利用できるソフトをゆーとさん(@yutoVR)に教えていただきました!(ありがとうございます!)

こちらは起動時のスプラッシュウインドウ。

f:id:moko_03_25:20190616230701j:plain

下記の公式サイトからツール本体をDL可能です。
インストーラーによるインストールも必要なく、DLしてすぐ起動できます。

前身であるバージョン1の正式版は2016年4月8日にリリースされたようです。

バージョン2は2018年12月24日にリリースされたようですね。
2になってFBXに対応された模様です。

製作者の方のTwitterこちら

まだ公式サイトのWikiに情報があまりありませんが、バージョン1の解説動画が沢山上がっていて、若干UIが違いますがそちらでカバーできる感じです。

軽く触ってみましたので、ざっくりと基本操作についてメモしておきたいと思います。

XISMO2の概要


●公式の解説動画
xismo講座 第0回~概要 - YouTube

3Dモデリングソフトで読み方はキスモ。
「分かりやすく」「使いやすく」がコンセプトとのことです。

f:id:moko_03_25:20190616230417j:plain

また、3dsMaxのようにモディファイヤをスタックしていけるのが特徴のようです。

カメラ操作


・カメラの回転  マウス右ドラッグ
・カメラのパン  マウス中ドラッグ
・カメラのズーム マウス右 + 左ドラッグ ホイール
・カメラの画角  Shift + マウス中ドラッグ

メニュー上部の「カメラ」ボタンで平衡投影とパースをトグルできる。

プリミティブ


モデルの形状が決まったプロパティの値で定義された状態。

ポリゴン化


頂点・辺・面といった各要素を編集する場合は「ポリゴン化」する。
メニューの「ノード処理>ポリゴン化(Ctrl + E)」

メニュー上部の頂点・辺・面のアイコンのON/OFFは選択可能な要素。

ビュー上で各要素を選択するとハンドルが表示される。
・中央の水色の球をドラッグするとXYZ軸の移動
・Shift + ドラッグするとXYZ軸の拡大になる

UV展開


●公式の解説動画

xismo講座 UV展開 その1 - YouTube

初めて触ってみた際にまずはプリミティブモデルを作成してUVを編集してみたかったのですが、UVを編集する画面を表示しようと思ってメニューの「パネル」「表示」「UV」をざっと見て探せず仕舞いだったのですが、ビューを二画面にすると出てきます。

画面を分割した際にそれぞれ表示したいものを選ぶ方法が分かりませんでした。
二画面にして、片方はパース、片方は正面とかはできるのでしょうか?

また、二画面にした直後はUVを選択しようとUVビュー内をドラッグしても矩形選択のラインが表示されず困りましたが、ビュー上部のUV移動・拡大・回転のアイコンを一度でも押せば選択可能な状態になります。

UV編集周りはまだ快適ではありませんが、今後に期待です。

モデルの結合


現状は複数のモデルを1つにマージする方法を見つけられていません。

一応、ポリゴン化して頂点等を全選択して「選択部処理>コピー」、別のポリゴン化したモデルを選択して「選択部処理>追加ペースト」で1つのモデルにできました。

こちらを最初に試してUE4にもっていった際にはエラーが出てダメでしたが、もう一度同じ手順で試してみたところ正常に読み込めました。

FBXで出力


メニュー「ファイル>エクスポート>.fbx」でOKです。

プリミティブの状態で出力するとUE4でインポートした際にエラーが出る模様です。
ポリゴン化した場合はFBX出力オプションを色々変更しても正常にインポートできました。
ちなみにUE4のバージョンは4.22.1です。

UE4での実験のためにプリミティブモデルをちょこっと変形させてUVを編集した程度のモデルが欲しい場合がちょくちょくあるのですが、非常に便利っ!

リスト管理できるテキストエディタ「List Fusen」がバージョン2になりました!

1年半ほど前に「List Fusen」をリリースしましたが、不具合や使いづらい部分があったりご要望を頂いていたりしたので、一から作り直してバージョン2になりました!

f:id:moko_03_25:20190615205822j:plain

GitHubにアップしています。
ListFusen2.zip」にツール本体の実行ファイルと設定ファイルが入っていますので、こちらをクリックして飛んだ先の「Download」ボタンからDLしてお使いください。

ライセンスは MIT License です。

ちなみに起動時に「.NET Framwork 4.6 が無いけどDLする?」とダイアログが出る場合はお手数ですが「はい」を選んだ先のランタイムをDLしてインストールしてください。

バージョンアップの大きな特徴


「List Fusen 2」の大きな特徴をピックアップしました。

●ツリー構造に対応
 フォルダ機能を望む声が多かったので、リストをツリー構造にしました。
 階層に制限を入れていないので無限に入れ子にできます。

●最小化に対応
 付箋紙ツールと謳っていましたが最小化できないと不便なので対応しました。

●スクロールバーの位置の記憶
 メモを切り替えても以前に見ていた時の位置を表示します。
 かなり便利になったと思います。

●起動時に前回終了時に閲覧していたメモを表示
 メモが増えてくるとこちらが無いと不便なので対応しました。

●文字数制限の引き上げ
 デフォルト設定の3万2767文字だと到達する可能性があるので20万文字に変更。

●自動バックアップに対応
 PCのクラッシュでメモが全て吹き飛んだというご報告を受けて対応しました。
 間隔は「1日」「1週間」「1ヵ月」から選べます。

●デザインを少し改変
 すっきりとした見た目になるようにしました。
 アイコンも見やすく修正しました。
 よく表示されるメッセージは専用のデザインのものにしました。

●メモの内容をXMLファイルで保持(data.xml
 ただしXMLファイルの改変は正常に読み込めなくなる可能性があるので非推奨です。

●ツールの設定をテキストファイルで保持(settings.txt)
 起動時にツールが画面外にいってしまったりした際などに書き換えられます。
 書き換え時には扱えない値を記入しないようご注意ください。
 初期設定に戻すにはファイルを削除するか設定ウインドウで初期化可能です。

●不具合の修正
 ツールを使っているうちにUIの表示が崩れる不具合を修正しました。
 (分割バーの位置・リストやメモ部分の表示領域が崩れることがありました)

●コードの最適化
 無駄が多くてひどいコードだったのが多少マシになったかと思います。

ご使用いただく際にひとつご注意があります。

デバッグが全然足りていない状態です。不具合が頻発する可能性があります。

もしも何かおかしな挙動に遭遇しましたら、作者のTwitterまたはメール<moko.vfxgmail.com>までお知らせいただけるとありがたいです。

既知の不具合

・メモに垂直タブが含まれていると保存に失敗する
 こちらのサイト記事の「TEXT▯TEXT」をメモにコピペして保存すると確認できます。
 https://letitride.hatenablog.com/entry/20120319/1332149944

今回諦めた実装

・親子関係を超えたメモの上下移動
 子も含めたカット&ペースト機能を追加して対応するかも知れません

・起動時に前回終了時のツリーの個々の開閉状態を復元

更新履歴


・2019.6.15 v.2.00 リリース!

・2019.6.16 v.2.01
 オートバックアップの出力パスの不具合を修正。

・2019.6.16 v.2.02
 オートバックアップ時に前回のバックアップ日時を更新するよう修正。
 設定ファイル読み込みエラー時に初期値を入れるよう修正。

・2019.6.16 v.2.03
 Ctrl+Sで保存した際に直前に編集したテキストを保存に反映するよう修正。

・2019.6.16 v.2.04
 対象の.NET Framworkを4.7.2から4.6に変更。

・2019.6.16 v.2.05
 .NET Framwork 引き下げにより Ctrl + A の全選択が不可になっていたので修正。

・2019.7.4 v.2.06
 多重起動を禁止にしました。
 最小化時に終了すると次回起動時に画面外に表示されてしまう不具合を修正。

・2019.7.15 v.2.07
 最小化直前にツールの位置や大きさ等を記憶するよう修正。
 終了時にツールの位置や大きさ等が不正な値だった場合に対応。
 フォントの設定が保存されず次回起動時に再現できていなかったので修正。

 

作者 / 著作権


moko

動作確認済みのOS


Windows10(64bit)

使用条件


本ツールはOSSオープンソース・ソフトウェア・ライセンス)です。
商用・非商用に関わらず(プライベートでもお仕事でも)ご自由にお使いください。
複製、改変、再配布も自由です。

免責事項


本ソフトウェアは予告なく機能を変更することがあります。
本ソフトウェアは動作環境を満たす全ての環境で正常に動作することを期待していますが、保証はできません。
本ソフトウェアの使用により生じたいかなる損害に関して、作者は一切の責任を負いません。

C# 初心者の次のステップ

まったくのプログラム初心者が .NET + C# の最低限の文法を使って簡単なWindowsフォームアプリを作れるようになってくると「コードの中身をもうちょっとうまく作れないかな」「便利な機能をもう少し知りたいな」と思うようになってきます。

そこで、そうした際に手を出すと良さそうな次のステップをメモしておきます

Visual Studio での F12 と Shift + F12

クラスや関数や変数が呼び出されている場所で名前を選択してF12を押すと、宣言している場所に一発でジャンプしてくれます。違うドキュメントでも関係なくジャンプし、標準で組み込まれているクラスや関数でもジャンプしてくれるため、中身を調べる時に非常に便利です。

逆に、宣言している場所で名前を選択して Shift + F12 を押すと使用先をリストアップしてくれて、リストをダブルクリックでジャンプしてくれます。

どちらも非常に便利で必須です。

Visual Studio のステップ実行とウォッチリストの利用

初心者であっても、この2つはデバッグに必須になります

Visual Studio のプロジェクトのプロパティの利用

ツールの設定などを自前でテキストファイルやXMLjsonファイルで読み書きするような実装をしなくても、Visual Studio の機能でプロパティを定義しておけば、実行ファイル名 + exe.config というファイル名でツール本体と同じ場所に勝手に保存してくれます。中身はXML形式で記述されているようです。

Fontの情報をstringに変換してファイルに読み書きしたいと思っても、専用のコンバータ関数の利用がうまくいかなかくてこちらに頼ったりしました。

●ソリューション内に複数のプロジェクトを追加して切り替える

同じような機能をテストする場合などにいちいち新しいソリューションを作らずにプロジェクトを追加して、切り替えて実行するのが便利です。

ソリューションエクスプローラーのソリューションで右クリック>追加>プロジェクトで追加して、ソリューションで右クリック>プロパティでプロジェクトの並び順を変えたり実行するプロジェクトを指定できます。

●複数のクラスを扱う

1つのcsファイル内に複数のクラスを書いてnewして使ってみたり、1つのプロジェクトに複数のcsファイルを作ってnewして使ってみたり。

●ライブラリを導入してみる

NuGetから有名で初心者でも扱えそうなライブラリを入れて使ってみたり。

●英語と日本語が切り替えできるよう試してみる

フォームデザイナを使って環境に応じて変える方法と、言語設定を判断する変数を用意してツールチップやメッセージボックスの文章を全てifで分岐させる方法の両方とも試してみたり。

●一般的な設定ファイルを試してみる

「ini」「json」「xml」「yml」などがあります。
インポート / エクスポートの解説記事も探すとすぐ見つかります。

●テキストのバイナリファイルでの出力や暗号化

設定ファイルをユーザーが触れないようにしたい場合などに試してみると良いです。

●プロパティ

最初は「変数(フィールド)と何が違うのかよく分からん」という気持ちになりますが、それでも覚えるためにあえて使ってみると良いと思います。

定番な形として、変数はprivateで定義しておき実際にはpublicにしたプロパティで変数に対して値をget / setできるように記述しても良いですし、プロパティだけいきなり書いて利用してみるのでも良いと思います。

便利な点としては「getのみ」「setのみ」といった制限を与えられることと、getやsetの際に値を好きに加工することができる点かと思います。

●アクセス修飾子static

ツールの設定などを格納しておくためのクラスを作った際に、newせずに直接 get・set したい場合があります。値をメモリに常駐しておいていつでもアクセスできるようにする感じです。

●参照渡し

何かの関数の引数に変数の値だけを渡すのではなく、変数そのものを渡したい時が出てきます。そういう時に参照渡しを利用すると「あ、こういうことか」と理解できます。

●列挙体

よく例に挙げられるのは曜日ですが、monday~sundayといった汎用的に使う固有名詞の綴りをstring型で書いて渡すのは不便ですし綴りのミスを修正しようと思って探すような場合にも不便です。

そこで列挙体で定義しておいて登録順のindexで渡すという使い方ができます。

●構造体

色んな型のフィールドの集まりをまとめておくのに使います。

元々はそれだけの機能だったようですが、メソッドも定義できるようになったりしてクラスとの違いが初心者にはよく分からない感じになっています。「構造体 クラス 違い」みたいな単語で検索すると処理速度の違いとか細々とした仕様の違いについての解説が出てきますが、初心者の段階ではそこまでは知らなくて良いのかなと思ったりもします。

ジェネリック

色々な型をまとめて定義できるもの、とだけ覚えておくのでも良いと思います。

●イベントの自作

ボタンクリックイベントなどは何気なく利用していたと思いますが、イベントを自分で定義して使いたい場面が出てきます。

そうすると、理解しないといけないことが色々とあります。
例えばこちら。

・デリゲート
 コールバック
・匿名メソッド

こちらが参考になります。


ラムダ式

煩雑な記述を簡素化するスタイルですが、この存在だけでも知っておかないと調べものの際に「突然、謎の記述に遭遇した」ように見えて混乱します。

こちらの記事がとても分かりやすいと思いました。

ただ、ベテランプログラマな方に聞いても「書く側は楽で良いものの見る側にとっては可読性が悪い」ということでした。

LINQ

コレクションから簡単に望むものを取り出したり、中身を入れ替えたりと、コレクションに対して楽に柔軟に編集が可能になるもののようです。

調べものをするとたまに出てきて、ラムダ式で書かれていたりしていてよく分からないままコピペで利用することが多くなってきます。なのでラムダ式から学ぶと良さそうです。

LINQの記述が分かると、データベースとのやり取りやXMLとのやり取りがスマートに書けるようです。私の場合はこのあたりはまだちゃんと理解できていないまま使っていたりします。。

●非同期処理

ツールで何か時間のかかる処理を行っている間ツールが固まってしまい他の操作ができなくなります。それが長く続くと、ユーザーがツールをクリックした際に「応答がありません」と表示されて終了を促される状態になってしまいます(実際にはちゃんと処理を続けていて終了待ちだったとしても)。

そこで、固まらないようにしてプログレスバーを表示したり、処理中も触って問題ない他の機能を使えるようにしたりすると良さそうです。

そのために非同期処理が必要になってきます。

※随時更新

C# TreeViewの親子階層内で指定のノードを選択する

TreeViewで指定のノードを楽に選択する方法が用意されていない感じがします。

FullPathを使う方法


ノードのプロパティ FullPath で、例えば選択中のノードのフルパスを取得できます。

そうしてあらかじめ保存しておいた FullPath 情報を利用して、後からまたそのノードを選択する際に FullPath が利用できます。

ただし、同階層に同じ名称のノードがあると破綻するようです。

なので FullPath が利用できるのは、Windowsエクスプローラーのように同階層に同じ名前を許さない条件下に限られそうです。

Index を使う方法


ノードのプロパティ Index を使って順番を記憶しておいて、それを使って選択する方法です。

しかしそもそもTreeViewの仕様上、ノード自身とその1つ上の親か1つ下の子供しかアクセスできないようで、階層を辿っていくには再帰処理を自前で組んでやる必要があります。

うーん面倒。。

単純に階層関係無しにノードをツリー最上段から順に数えていって何番目かをGETして、それを使って選択できたりすると非常に楽なんですが‥例えば下図で選択中のノードは「6つ目のノード」という感じで。

f:id:moko_03_25:20190613142627j:plain

仕方ないので、再帰処理で実現したいと思います。

まずは選択中のノードのIndexと、親ノードのIndexを取得してみます。

private void GetSelNodeIndex()
{
	// 選択中のノードを取得
	TreeNode tn = treeView1.SelectedNode;

	// 選択中のノードのIndexを取得
	int i = tn.Index;

	// 親のノードを取得
	TreeNode tnP = tn.Parent;
	int iP;
	// 親がある場合
	if (tnP != null)
	{
		// 親のノードのIndexを取得
		 iP = tnP.Index;

		MessageBox.Show("選択中のノードのIndex: " + i.ToString() + "\r\n" +
					"親ノードのIndex: " + iP.ToString());
	}
	else
	{
		MessageBox.Show("選択中のノードのIndex: " + i.ToString() + "\r\n" +
					"親ノードのIndex: " + "なし");
	}
}

// GET
private void Button1_Click(object sender, EventArgs e)
{
	GetSelNodeIndex();
}

親が存在しないと例外が発生するので対処しておきます。

実行してボタンを押すとこんな感じ。

f:id:moko_03_25:20190613143215j:plain

次のステップとして、こちらを再帰処理にしてindexをコレクションに格納します。

// 変数
List<int> listNodeId = new List<int>();

// 選択ノードのIndexを取得
private void GetSelNodeIndex()
{
	// リストとラベルを空にする
	listNodeId.Clear();
	label1.Text = "";

	// 選択ノードを取得
	TreeNode tn = treeView1.SelectedNode;

	// 選択ノードのIndexを取得してListに格納
	listNodeId.Add(tn.Index);

	// 親ノードを取得
	TreeNode tnP = tn.Parent;

	// 再帰処理
	GetSelNodeRecursive(tnP);

	// ラベルに表示(とりあえず雑に)
	foreach (var item in listNodeId)
	{
		label1.Text += item.ToString() + " ";
	}
}
// 再帰処理
private void GetSelNodeRecursive(TreeNode tnP)
{
	// 親がある場合
	if (tnP != null)
	{
		// 親ノードのIndexを取得してListの先頭に挿入
		listNodeId.Insert(0, tnP.Index);

		// さらに親に対して実行
		GetSelNodeRecursive(tnP.Parent);
	}
}

うまくいきました。

f:id:moko_03_25:20190613150654g:plain

次は、GETで保存しておいたノードを後から選択します。

先ほどと逆に再帰処理でリストから順に親から子へIndexを辿っていくようにします。

// 最終的に選択したいノードを宣言
TreeNode selNode;

// 選択ノードのIndexを取得
private void SetSelNodeIndex()
{
	// リストからトップ階層のノードを取得
	TreeNode tnP = treeView1.Nodes[listNodeId[0]];
	// 最終的に選択したいノードに一旦指定
	selNode = tnP;

	// リストのIndex指定用
	int i = 1;

	// リストに子のIndex情報がある場合
	if (listNodeId.Count > i)
	{
		SetSelNodeRecursive(tnP, i);
	}

	treeView1.Focus();
	treeView1.SelectedNode = selNode;
}
// 再帰処理
private void SetSelNodeRecursive(TreeNode tnP, int i)
{
	// リストから1つ下の子ノードを取得
	TreeNode tnC = tnP.Nodes[listNodeId[i]];
	// 最終的に選択したいノードに指定
	selNode = tnC;

	// カウントアップ
	i++;

	// リストに子のIndex情報がある場合
	if (listNodeId.Count > i)
	{
		SetSelNodeRecursive(tnC, i);
	}
}

これで当初やりたかった「指定したノードを選択する」ことができるようになりました!

f:id:moko_03_25:20190613153657g:plain


しかし‥もっと良い方法がある気がします。

ご存じの方いらっしゃったらご教授ください。。

C# XMLへの階層構造の保存メモ3(TreeView編)

前回に続き、今度は実践編ということで元々やりたかったことを再帰処理で行ってみます。

フォームはこんな感じ。
あらかじめTreeViewに適当に孫以上の深い階層を追加しておきます。

f:id:moko_03_25:20190610235639j:plain

実装はこんな感じ。
using System.Xml.Linq;」を先頭に追加するのをお忘れなく。。

private void Form1_Load(object sender, EventArgs e)
{
	// ツリーを全て展開
	treeView1.ExpandAll();
}

private void Button1_Click(object sender, EventArgs e)
{
	XmlSave();
}

private void XmlSave()
{
	// XMLの内容の定義
	var xml = new XDocument(new XDeclaration("1.0", "utf-8", "yes"));

	// 構造ごとの要素を定義
	XElement Sample = new XElement("サンプル");
	xml.Add(Sample);

	// TreeViewのノード情報をnodesコレクションに格納
	TreeNodeCollection nodes = treeView1.Nodes;
	// ループ
	foreach (TreeNode node in nodes)
	{
		// 自分自身のノードを定義  
		XElement n = new XElement("ルートノード");
		xml.Root.Add(n);

		// 再帰処理用
		XmlRecursive(node, n);
	}

	// TextBox1に表示
	textBox1.Text = xml.Declaration.ToString();
	textBox1.Text = xml.ToString();
}

// 再帰処理用
private void XmlRecursive(TreeNode node, XElement n)
{
	XElement name = new XElement("Name", node.Text);
	n.Add(name);

	// ループ
	foreach (TreeNode tn in node.Nodes)
	{
		// 自分自身のノードを定義  
		XElement n2 = new XElement("子ノード");
		n.Add(n2);

		// 自分自身を呼び出す
		XmlRecursive(tn, n2);
	}
}

結果はこんな感じ。

f:id:moko_03_25:20190610235658g:plain

ノードのラベル名は<Name>タグに格納しています。
同じように、ノードに紐づけたクラスインスタンスの情報‥例えばノードと紐づけたTextBoxの内容など色々な情報をタグにして埋め込むことができます。

これが不要で単純にノードのラベル名がそのままタグ名になるだけで良いなら、こんなまわりくどくする必要は無くて、もっと簡略化できると思います。

ちなみに文字列をXMLに書き出す際、改行が失われてしまいます。
なので改行を表す特殊文字に置き換えて読み書きすることで対応しました。
「memo.Text.Replace("\r\n", "[:NL:]");」みたいな感じです。
タブスペースは何もしなくても保持されました。

置換の方法はこちらをどうぞ。

XMLの書き出しは初めて試した状態なので、もっと良い方法があればぜひコメントくださいっ。

C# XMLへの階層構造の保存メモ2(TreeView編)

前回に続き、今回はTreeViewのラベル名をXMLへ書き出してみます。

こちらに再帰処理で全てのノード名を取得するサンプルが記載されています。

こちらを参考に少しずつ進めてみました。

まずはフォームを用意してあらかじめTreeViewにノードを登録しておきました。

f:id:moko_03_25:20190609185007j:plain

ボタンクリックイベントでルートノードのラベル名をXMLで書き出してみました。
例によって「using System.Xml.Linq;」を先頭に追加するのをお忘れなく。。

private void Form1_Load(object sender, EventArgs e)
{
	// ツリーを全て展開
	treeView1.ExpandAll();
}

private void Button1_Click(object sender, EventArgs e)
{
	XmlSave();
}

public void XmlSave()
{
	// XMLの内容の定義
	var xml = new XDocument(new XDeclaration("1.0", "utf-8", "yes"));

	// 構造ごとの要素を定義
	XElement 学校;
	XElement クラス;
	XElement 生徒;

	// <私立ほにゃ高校>タグを作成してxmlに追加
	学校 = new XElement("私立ほにゃ高校");
	xml.Add(学校);

	// TreeViewのノード情報をnodesコレクションに格納
	TreeNodeCollection nodes = treeView1.Nodes;

	// ルートノードに対するループ処理
	foreach (TreeNode node in nodes)
	{
		// < クラス > タグを作成してRoot(つまり学校)に追加
		クラス = new XElement(node.Text);
		xml.Root.Add(クラス);
	}

	// TextBox1に表示
	textBox1.Text = xml.Declaration.ToString();
	textBox1.Text = xml.ToString();
}

うまくいきました。

f:id:moko_03_25:20190609184916j:plain

次にルートノードのひとつ下の階層を<生徒>タグ、そしてさらにその下に<名前>タグの入れ子にしてXMLに書き出してみました。

public void XmlSave()
{
	// XMLの内容の定義
	var xml = new XDocument(new XDeclaration("1.0", "utf-8", "yes"));

	// 構造ごとの要素を定義
	XElement 学校;
	XElement クラス;
	XElement 生徒;

	// <私立ほにゃ高校>タグを作成してxmlに追加
	学校 = new XElement("私立ほにゃ高校");
	xml.Add(学校);

	// TreeViewのノード情報をnodesコレクションに格納
	TreeNodeCollection nodes = treeView1.Nodes;

	// ルートノードに対するループ処理
	foreach (TreeNode node1 in nodes)
	{
		// < クラス > タグを作成してRoot(つまり学校)に追加
		クラス = new XElement(node1.Text);
		xml.Root.Add(クラス);

		// 子ノードに対するループ処理
		foreach (TreeNode node2 in node1.Nodes)
		{
			// <生徒>タグを作成して名前をぶら下げてから<クラス>タグに追加
			生徒 = new XElement("生徒");
			生徒.Add(new XElement("名前", node2.Text));
			クラス.Add(生徒);
		}
	}

	// TextBox1に表示
	textBox1.Text = xml.Declaration.ToString();
	textBox1.Text = xml.ToString();
}

うまくいきました。

f:id:moko_03_25:20190609190953j:plain

いよいよ本番である再帰処理は次の記事にまとめました。

C# XMLへの階層構造の保存メモ1(基本編)

C#XMLファイルに親・子・孫‥といった階層構造を書き込みたい場合についてのメモです。

そもそもXMLを初めて扱うので基礎的なあたりを解説している記事を探して参考にしたのですが、こちらがとても入門として分かりやすくてオススメです。


という訳で試してみました。
Windowsフォームアプリでボタンをテキストボックスを配置。

f:id:moko_03_25:20190609152105j:plain

中身はこちらのように記述。
自分でも理解しやすいように、学校>クラス>名前と住所‥という内容にしました。
using System.Xml.Linq;」を先頭に追加するのをお忘れなく。。

private void Button1_Click(object sender, EventArgs e)
{
	XmlSave();
}

public void XmlSave()
{
	// XMLのヘッダー情報
	XDeclaration ヘッダー = new XDeclaration("1.0", "utf-8", "yes");
	// XMLの内容の定義
	XDocument xml = new XDocument(ヘッダー);

	// 学校の要素を定義
	XElement 学校;
	// <私立ほにゃ高校>タグを作成
	学校 = new XElement("私立ほにゃ高校");

	// 学校の要素をxmlに追加(これがRootになる)
	xml.Add(学校);

	// クラスの要素を定義
	XElement クラス;

	// <一組>タグを作成
	クラス = new XElement("一組");
	// <一組>タグに<名前>タグと<住所>タグをデータ込みでぶら下げる
	クラス.Add(new XElement("名前", "石坂"));
	クラス.Add(new XElement("住所", "静岡"));
	// Root(つまり学校)にぶら下げる
	xml.Root.Add(クラス);

	// <二組>タグを作成
	クラス = new XElement("二組");
	// <二組>タグに<名前>タグと<住所>タグをデータ込みでぶら下げる
	クラス.Add(new XElement("名前", "太郎"));
	クラス.Add(new XElement("住所", "福岡"));
	// Root(つまり学校)にぶら下げる
	xml.Root.Add(クラス);

	// TextBox1に表示
	textBox1.Text = xml.Declaration.ToString();
	textBox1.Text = xml.ToString();
}

 実行してbutton1を押すとtextBox1にバッチリ表示されました!

f:id:moko_03_25:20190609152112j:plain

ただし、実際にはもうちょっと複雑な構造‥例えばこんな感じにしたいですよね。

学校
 クラス
  先生
   名前
  生徒
   名前
  生徒
   名前
 クラス
  先生
   名前
  生徒
   名前


この場合はこちらのように記述すればOKです。

private void Button1_Click(object sender, EventArgs e)
{
	XmlSave();
}

public void XmlSave()
{
	// XMLの内容の定義
	var xml = new XDocument(new XDeclaration("1.0", "utf-8", "yes"));

	// 構造ごとの要素を定義
	XElement 学校;
	XElement クラス;
	XElement 先生;
	XElement 生徒;

	// <私立ほにゃ高校>タグを作成してxmlに追加
	学校 = new XElement("私立ほにゃ高校");
	xml.Add(学校);

	// <クラス>タグを作成してRoot(つまり学校)に追加
	クラス = new XElement("クラス");
	xml.Root.Add(クラス);
	// <先生>タグを作成して名前をぶら下げてから<クラス>タグに追加
	先生 = new XElement("先生");
	先生.Add(new XElement("名前", "浅野先生"));
	クラス.Add(先生);
	// <生徒>タグを作成して名前をぶら下げてから<クラス>タグに追加 * 人数分
	生徒 = new XElement("生徒1");
	生徒.Add(new XElement("名前", "飯尾"));
	クラス.Add(生徒);
	生徒 = new XElement("生徒2");
	生徒.Add(new XElement("名前", "上田"));
	クラス.Add(生徒);

	// <クラス>タグを作成してRoot(つまり学校)に追加
	クラス = new XElement("クラス");
	xml.Root.Add(クラス);
	// <先生>タグを作成して名前をぶら下げてから<クラス>タグに追加
	先生 = new XElement("先生");
	先生.Add(new XElement("名前", "江原先生"));
	クラス.Add(先生);
	// <生徒>タグを作成して名前をぶら下げてから<クラス>タグに追加 * 人数分
	生徒 = new XElement("生徒1");
	生徒.Add(new XElement("名前", "大谷"));
	クラス.Add(生徒);

	// XMLファイルに保存する
	xml.Save(@"sample.xml");

	// TextBox1に表示
	textBox1.Text = xml.Declaration.ToString();
	textBox1.Text = xml.ToString();
}

実行してみると、うまくいきました!

f:id:moko_03_25:20190609161443j:plain

躓いた点が一つあったのですが、<クラス>タグの定義の部分を「クラス = new XElement("クラス", "一組");」といったように値を入れてしまうと、階層構造が崩れました。子をぶら下げる箱として用意する場合には値を入れたらダメなんですかね。。

続きはこちら

C# XMLファイルを読み込んでTreeViewにノードを追加する

XMLについての基本的なあたりはこちらの記事を参考にさせていただきました。

標準でクラスが用意されていてこんなにお手軽に読み書きできるとは!

ただ、XML入れ子構造になっていて孫要素が1つの親の中に複数ある場合にどう記述したら良いのかに詰まって結構時間を消費してしまいました。。

‥とそんな中、まさに探していたサンプルが!

しかもすごくシンプルです。これで良かったのかあ~~。

という訳でこのようにうまくいきました!

f:id:moko_03_25:20190609033620g:plain

Teratailで質問した際の回答も、なるほどこれでいけるのね!と。。
https://teratail.com/questions/193924

それから「再帰処理」をちゃんと調べてみました。

こちらC#の調べものでよくお世話になるサイトですが、再帰処理についてのスライドがとても分かりやすかったです。

それからXMLの操作を調べるとLINQを使った記述が沢山出てくるのですが、未だにLINQをちゃんと勉強してないんでそろそろやらないとなと。。

ただ、先月にSQLを一通りやったのが少し役立ってる感じがあります。

あとXML周りを覚えるにもこちらの書籍が良いようで、レビューを色々読んでみても入門を終えた人向けにとても良書と評価されていて購入を考えています。

実戦で役立つ C#プログラミングのイディオム/定石&パターン

実戦で役立つ C#プログラミングのイディオム/定石&パターン

 

Kindle版はこちら。


その他の情報もペタリ。

XElement Class (System.Xml.Linq) | Microsoft Docs