技術支援ツール・自動化 / Technical Tools & Automation
技術支援ツール・自動化
開発現場のボトルネックを、技術で快適さに変える。
WPF/C#を用いた高機能ツールの開発から、
Gitワークフローの自動化まで。
DesktopOrganizer
DesktopOrganizerは、デスクトップ整理を支援するWPFアプリです。Win32 APIの不安定さをUIへ漏らさないよう、Core / UI / Tests に分けて設計しました。
成果: Core / UI / Tests の3層構成でWin32 APIをUI層から分離し、マルチモニターD&D、非同期アイコン読込、詳細ログ、スマートシェルフを段階的に積み上げながら、常駐シェルフ管理を実際に使える形に仕上げました。
Win32の副作用をUIへ漏らさない
マルチモニター環境での整理を支援するデスクトップウィジェットです。見た目の軽さを保ちながらOSの低レイヤー制御を安全に扱えるよう、ロジックとビューの責務を明確に分離して設計しました。
ウィジェット UI(WPF / XAML)
Core / UI / Tests の3層構造
UIとOS制御を分ける3層構造
DesktopOrganizer.Core,
DesktopOrganizer.UI,
DesktopOrganizer.Tests
にプロジェクトが明確に分割。Win32
API操作はすべてCore層にカプセル化されており、UI層は純粋なデータバインディングに集中できる構造です。
CommunityToolkit.Mvvmで状態管理を整理し、設定画面の変化を追いやすくしました。コードビハインドへの依存はなくしています。
XAMLを駆使し、デスクトップ上でノイズにならない、自然で滑らかな視覚効果を実装しました。
設計の意図
見た目の軽さと、裏側のWin32制御の堅牢さは両立できると考えました。Core・UI・Testsを分けたのは、どちらも諦めずに済む構造にするためです。UI美観の調整と並行してD&D、座標補正、XamlParseException、詳細ログを詰め、体験の軽さを低レイヤーの安定化で支える作り方にこだわりました。
OS制御で詰まったこと
一番詰まったのは、見た目は単純でも座標系とOS側の都合が複雑に絡む点です。マルチモニターD&D修正、ドラッグオフセットの再計算、スクロール不具合、XamlParseException対応を繰り返し、操作感を整えるまで細かなズレを潰し続けました。
また、WorkerW 配置や Win32 連携は原因切り分けが難しく、後追いで詳細ログを入れました。最初から観測手段を厚くしておくべきだったと学びました。今作り直すなら、座標変換、モニター境界、シェル再生成のような壊れやすい経路を先に一覧化し、検証観点とログポイントを初期段階で固定します。
加えて、機能追加が進むほどシェルフ種別と表示要素が増えるため、次回は高解像度アイコンや2行表示のような見た目改善も含め、UI仕様の優先順位を早めに分離します。後半で磨き込みが集中しない進め方の方が、安定化と体験向上を両立しやすいと考えています。
実装の深堀り:WorkerW を使った常駐配置と MVVM の分離
利用者には「デスクトップ上で自然に整理できるウィジェット」として触れる状態を保ちつつ、内部では壁紙とアイコンの間へ安全に配置するためのOS制御を吸収しています。
DesktopOrganizer では、通常の最前面・最背面制御ではなく、デスクトップの壁紙とアイコンの間にウィンドウを置く構成を採用しています。見た目は軽いウィジェットですが、実装では Win32 API に踏み込む必要があるため、その副作用をUI層へ漏らさないことを重視しました。
1. WorkerW を生成してWPFウィンドウをぶら下げる処理
Windows シェルの管理下にある
Progman へ未公開メッセージ
0x052C を送り、壁紙の直上に使える
WorkerW を作成しています。その後、EnumWindows
で対象ウィンドウを見つけ、SetParent
でWPFウィンドウを接続しています。
// NativeMethods.cs: デスクトップの深層に介入するハック
IntPtr progman = FindWindow("Progman", null);
// 未公開メッセージ 0x052C を送信し、アイコンと壁紙の間に WorkerW を生成させる
SendMessageTimeout(progman, 0x052C, new IntPtr(0), IntPtr.Zero, SendMessageTimeoutFlags.SMTO_NORMAL, 1000, out _);
// EnumWindowsを用いて生成されたWorkerWを探し出し、
// SetParent API で WPF ウィンドウを WorkerW の子要素として結合する
SetParent(wpfWindowHandle, workerWHandle);
この処理はOS依存が強く、失敗時の切り分けも難しいため、P/Invokeまわりは
DesktopOrganizer.Core
に閉じ込めました。UI側は配置結果だけを受け取る形にして、通常のWPFアプリとして扱える範囲を保っています。
2. 副作用をUI層へ持ち込まない MVVM 構成
Zオーダー固定やマルチモニター座標の計算など、WPF単体では完結しない処理はCore側へ置き、ViewModelは状態とコマンドだけを持ちます。CommunityToolkit.Mvvm
を使って変更通知の流れをそろえたことで、UIの調整とOS制御を別々に確認できるようになりました。
Win32 APIフックを用いたZオーダー固定アーキテクチャ
SimpleZipper
SimpleZipperは、「失敗理由が分かること」を重視したWindows向けZIPツールです。圧縮、既存ZIP追加、パスワード、分割、ログ表示を一つの作業フローにまとめました。
成果: 圧縮、既存ZIP追加、パスワード、分割、ログ表示、異常系通知を一つのUIに統合し、処理状況と次の判断が把握しやすいZIP作業フローを構築しました。
失敗理由が分かるZIP操作にする
「ファイル操作で起きやすい異常系を明示的に扱う」ことを最優先にしたZIP圧縮ツールです。日常的なファイル操作で、何が起きたか分からないまま止まる状態を避けることを目的に開発しました。
ドラッグ&ドロップ / 圧縮 / AES / 分割
プログレス表示 / キャンセル操作
ファイルアクセス権限エラー・パス長制限・ディスク容量不足など、Windowsファイルシステム特有のトラブルをすべて網羅的にキャッチし、次のアクションを明示。
巨大ファイルの圧縮・解凍中もUIスレッドをブロックさせず、スムーズなプログレス表示と即時のキャンセル操作を可能に。
設計の意図
「動けばいい」ではなく、想定外の事態でどう振る舞うかを設計の中心に置きました。ファイルシステム特有のトラブルを整理し、次のアクションが分かることを必須要件にしています。説明文書、画面、.gitignoreも整えながら使い勝手を詰め、圧縮エンジンだけでなく配布物として迷いにくい形に整えました。
失敗時の安心感で迷ったこと
このツールで難しかったのは、圧縮処理そのものよりも「重い処理を回しながら、利用者に不安を残さない」ことです。非同期進捗、AES-256、分割圧縮を同じ画面で扱うと、機能を足すたびにUI停止や例外の見せ方が崩れやすくなります。だから、処理中に何を表示するか、キャンセルできる状態か、失敗時にどのメッセージを出すかを分けて考えました。
一方で、当時は作業の区切りを細かく残せておらず、後から見返したときに、何に詰まり、どこを改善したかが追いにくい構成になりました。今なら、例外分類、進捗通知、テーマ適用の節目ごとにコミットを分け、後から失敗理由や設計判断を辿れる形にします。
また、ファイル操作系ツールは権限、長いパス、容量不足、破損ZIPなど異常系の面積が広いので、次回は説明文書だけでなく画面上のエラー文言と再試行導線も先に設計します。圧縮成功率だけでなく、失敗時に迷わないことまで品質として扱う方針です。
実装の深堀り:圧縮処理を分離した非同期実行と進捗通知
利用者にとって重要なのは、圧縮が速いこと以上に「今どこまで進み、止めても安全か」が分かることです。そのため処理状況を途切れず返す設計を先に固めました。
SimpleZipper では、圧縮時間そのものよりも、処理中に何が起きているか分からなくなる状態を避けることを優先しました。重い圧縮処理はUIスレッドから分離し、進捗と中断可否を常に返す構成にしました。
1. BackgroundWorker を使った圧縮処理の分離
Ionic.Zip
の暗号化や分割圧縮は、長時間ブロックしやすい処理です。なので、保存本体はワーカースレッド側で実行し、進捗だけをUIへ返します。処理を分けたことで、ログ表示やキャンセル操作を止めずに続けられます。
// compressionWorker_DoWork メソッドの抜粋
using (ZipFile zip = new ZipFile()) {
zip.Encryption = EncryptionAlgorithm.WinZipAes256;
zip.Password = request.Password;
// 進捗イベントをUIスレッドへ安全にディスパッチ
zip.SaveProgress += (s, e) => {
if (e.EventType == ZipProgressEventType.Saving_EntryBytesRead) {
int progress = (int)((e.BytesTransferred * 100) / e.TotalBytesToTransfer);
worker.ReportProgress(progress, e.CurrentEntry.FileName);
}
};
zip.Save(outputPath); // ブロッキング処理はワーカースレッド内でのみ実行
}
これにより、大きなファイルでもプログレス表示が止まりにくく、失敗時もどの段階で止まったかが分かります。ここがこのツールの使い心地を決めている部分です。
2. 動的テーマ適用で画面ごとの差分を減らす
ThemeColors.cs
にテーマ色をまとめ、フォーム配下のコントロールへ再帰的に適用する仕組みを用意しました。個別画面ごとに色設定を書き散らさずに済むため、UI追加時の抜け漏れを減らせます。
非同期圧縮処理のイベント・ディスパッチパイプライン
GitPR Fluent
GitPR Fluentは、PR作成までのGit操作を順序ごと整理するWindows向けGitクライアントです。Git CLI / GitHub CLIを呼び出し、危ない操作をUI側で先に止めます。
成果: 差分確認、コミット、push、PR作成までの流れをGUIへ一本化し、依存関係ガイダンス、ログ改善、エラー対応強化、UTF-8設定見直し、他人のリポジトリへの直接push警告まで含めて、確認付きで進められるGit運用フローを形にしました。
事故りやすいGit操作を順序ごと整える
Git CLIを裏側でラップし、日本語でPull Request作成まで進められるようにしたツールです。開発フローの不便を埋めるだけでなく、実行順序のミスをUI側で減らすことを狙っています。
メイン画面(Git状態管理)
PR作成ダイアログ
C#からGitコマンドプロセスを安全に起動し、標準出力と標準エラーを並列に扱いながら状態を管理。後半ではUTF-8設定改善やエラー対応強化も積み、文字化けや詰まりを画面側へ持ち込まない方向で磨きました。
変更差分を読みやすくHTML形式に変換してローカル表示する機能を実装。後の大規模開発でもそのまま使っています。
設計の意図
小さな不便を埋める道具として始めましたが、実際には外部プロセス制御や差分パースの扱いを整理する題材になりました。ここで整えた考え方は、後の大規模開発でもそのまま使っています。差分改善、起動時読込のバックグラウンド化、依存関係案内、main直push警告など、機能追加より事故防止と判断支援に重心を置きました。
事故防止を優先して見えた課題
Git操作をGUIへ載せると、実際に難しいのはボタンを並べることではなく、危険な順序をどう塞ぐかです。差分改善、ログの見やすさ、エラー対応、依存関係ガイダンス、他人のRepoへのmain直接プッシュ警告を順に整え、使い方を説明する前に事故の芽をUIで摘むことを優先しました。
その一方で、Gitやghの実行結果は環境依存が強く、UTF-8設定や依存ツール不足のように、実装者の環境では再現しにくい詰まりも出ます。この種の問題は、コードだけで閉じずに事前診断と分かりやすいログが必要だと学びました。次回は初回起動時の診断、文字コード確認、権限不足検知をさらに前倒しで入れます。
また、実装を進めながら説明を後から厚くしていったため、操作制約や想定フローの整理が後追いになった部分もあります。今なら、禁止したい運用を先に文書化し、実装と説明の差分が広がらないよう進めます。Gitツールは仕様より運用ルールの見せ方が品質に出ると思っています。
実装の深堀り:Git CLI を安全に扱う非同期実行と画面制御
利用者視点では、コマンドの細部を覚えなくても安全な順序でPR作成まで進められることが価値です。その裏側を、CLI連携と画面制御で支えています。
GitPR Fluent では、GitやGitHub CLIの機能をそのまま呼び出しつつ、操作順序のミスをUI側で抑えることを目標にしました。なので、外部プロセスの実行方法と、ViewModelが持つ状態遷移の設計をセットで作っています。
1. 標準出力・標準エラーを並列で読むプロセス実行
git.exe や
gh.exe
は処理内容によって出力量が変わるため、読み取りを誤るとアプリ側で待ちが詰まります。そこで、標準出力と標準エラーを非同期に読み、終了コードと合わせて結果を返す実装にしました。
// プロセスの非同期実行と安全な出力パース
public async Task<string> ExecuteGitCommandAsync(string arguments) {
using var process = new Process {
StartInfo = new ProcessStartInfo {
FileName = "git", Arguments = arguments,
RedirectStandardOutput = true, RedirectStandardError = true,
UseShellExecute = false, CreateNoWindow = true
}
};
process.Start();
// デッドロックを防ぐため、標準出力とエラー出力を並列かつ非同期に読み取る
var readOut = process.StandardOutput.ReadToEndAsync();
var readErr = process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
if (process.ExitCode != 0) throw new GitException(await readErr);
return await readOut;
}
この方法なら、重い
git pull --rebase
や差分取得でもUIスレッドを塞ぎにくく、失敗時のメッセージもそのまま画面へ返せます。CLIラッパーとして必要な最低限の信頼性はここで担っています。
単一ReadToEndによるデッドロックと非同期並列読み取りの比較
2. MainViewModel で操作可能な状態を絞る
ローカル変更、ブランチ状況、PR作成可否などは
MainViewModel.cs にまとめ、INotifyPropertyChanged
と
ICommand.CanExecute
でボタン状態を切り替えています。利用者にGitの内部状態を理解させるのではなく、実行してよい操作だけを画面に出す考え方です。
ボタン有効状態(CanExecute)の遷移
非同期プロセス制御とMVVM状態管理のアーキテクチャ