【実務・中級編】Windows Formsにおけるカスタム例外ダイアログの設計:スタックトレースとシステム情報をクリップボードへワンタッチコピーする機能の実装 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

Windows Formsの極意:情シスを救う!ワンタッチ診断付き「神」カスタム例外ダイアログの設計

開発現場で最も恐れられる瞬間の一つ。それは、ユーザーが「なんかエラーが出ました」と言って、エラー画面のスクショすら撮らずに席に戻ってくる瞬間だ。

「どんな操作で?」「エラーメッセージは何て?」「OSは何を使っている?」
これらを後追いで聞き出すのは、暗闇の中で針を探すようなものだ。情シスや開発者にとって、この不毛なラリーほどリソースを蝕むものはない。

今回は、Windows Forms(VB.NET)アプリケーションにおいて、予期せぬ例外をキャッチし、スタックトレースとシステム情報を美しく整形した上で、ワンタッチでクリップボードへ退避させる「カスタム例外ダイアログ」の設計と実装を伝授する。

単なる「MessageBox.Show」で済ませているそこのあなた。プロの業務アプリケーションとしての矜持を、ここで見せよう。

—

なぜデフォルトの例外ダイアログではダメなのか?

VB.NETの初心者や、場当たり的なコードを書くプログラマは、こうやりがえがちだ。

.net
‘ 【アンチパターン】これでは現場の誰も幸せにならない
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Try
‘ 何らかの処理
Throw New Exception(“DB接続に失敗しました。”)
Catch ex As Exception
MessageBox.Show(ex.Message, “エラー”, MessageBoxButtons.OK, MessageBoxIcon.Error)
End Try
End Sub

この実装の何が罪深いか?
1. 情報量ゼロ: ユーザーは「DB接続に失敗しました」としか見えず、どのサーバーか、なぜ失敗したのか(内部例外:InnerException)が隠蔽される。
2. コピー不可: 標準の `MessageBox` のメッセージボックスは、テキストを選択してコピーすることが地味に面倒(Ctrl+Cが効かないOSバージョンもある)である。
3. 環境情報の欠如: 「Windows 11なのか10なのか」「.NET Frameworkのバージョンは」「ユーザー権限は」といった、デバッグに必須の環境情報が一切ログに残らない。

プロが目指すべきは、「ユーザーには分かりやすく、開発者には詳細を」両立させる洗練された例外ハンドリング機構だ。

—

堅牢なカスタム例外ダイアログのアーキテクチャ

今回構築するカスタム例外ダイアログ(`ErrorDialog` フォーム)の要件は以下の通り。

1. モーダルかつトップmost: 致命的なエラーの際、背後のウィンドウを触らせず、確実にユーザーの注意を引く。
2. 視覚的階層: ユーザー向けのエラー概要と、開発者向けの技術詳細(スタックトレース)をタブやアコーディオン、あるいはスクロール可能なテキストボックスで明確に分離する。
3. ワンタッチコピー: 「詳細をコピー」ボタン一つで、環境情報、例外メッセージ、スタックトレースをクリップボードに美しく整形して格納する。

実装コード:プロダクション品質の `ErrorDialog.vb`

以下のコードは、そのままあなたのプロジェクトに組み込める完全版のフォームクラスだ。UIデザインはコード内で動的生成するか、Visual Studioのデザイナで配置したコントロール(TextBox, Button等)を想定しているが、今回はコピペですぐに概念を理解できるよう、堅牢なプロパティとイベント処理に絞って解説する。

.net
Imports System.Text
Imports System.Windows.Forms

Public Class ErrorDialog
Inherits Form

‘ UIコントロールの定義(デザイナを使用しない場合を想定した動的配置も可能だが、ここではプロパティ経由で操作)
Private ReadOnly _exception As Exception

‘ コントロール群(実際にはデザイナで配置してください)
Private txtMessage As TextBox
Private txtDetails As TextBox
Private btnCopy As Button
Private btnClose As Button

”’

”’ コンストラクタで例外オブジェクトを強制注入
”’

Public Sub New(ex As Exception)
MyBase.New()

If ex Is Nothing Then Throw New ArgumentNullException(NameOf(ex))
_exception = ex

InitializeComponentCustom()
BindExceptionData()
End Sub

”’

”’ フォームの初期化とレイアウト構築
”’

Private Sub InitializeComponentCustom()
Me.Text = “システム エラー”
Me.Size = New Size(600, 450)
Me.StartPosition = FormStartPosition.CenterScreen
Me.FormBorderStyle = FormBorderStyle.FixedDialog
Me.MaximizeBox = False
Me.MinimizeBox = False

‘ ユーザー向けメッセージ表示エリア
txtMessage = New TextBox() With {
.Multiline = True,
.ReadOnly = True,
.ScrollBars = ScrollBars.Vertical,
.Location = New Point(12, 12),
.Size = New Size(560, 80),
.BackColor = SystemColors.Control
}

‘ 開発者向け詳細(スタックトレース等)表示エリア
txtDetails = New TextBox() With {
.Multiline = True,
.ReadOnly = True,
.ScrollBars = ScrollBars.Vertical,
.Location = New Point(12, 100),
.Size = New Size(560, 240),
.Font = New Font(“Consolas”, 9.0F) ‘ 等幅フォントで美しく
}

‘ クリップボードコピーボタン
btnCopy = New Button() With {
.Text = “詳細をクリップボードへコピー(&C)”,
.Location = New Point(12, 355),
.Size = New Size(220, 30)
}
AddHandler btnCopy.Click, AddressOf BtnCopy_Click

‘ 閉じるボタン
btnClose = New Button() With {
.Text = “閉じる(&X)”,
.Location = New Point(452, 355),
.Size = New Size(120, 30),
.DialogResult = DialogResult.OK
}

Me.Controls.AddRange(New Control() {txtMessage, txtDetails, btnCopy, btnClose})
Me.AcceptButton = btnClose
End Sub

”’

”’ 例外情報およびシステム環境情報をコントロールへバインドする
”’

Private Sub BindExceptionData()
‘ ユーザー向けメッセージ
txtMessage.Text = “アプリケーションで予期せぬエラーが発生しました。” & Environment.NewLine &
“お手数ですが、情シス担当者へ本画面の情報をコピーしてご報告ください。” & Environment.NewLine &
“エラー概要: ” & _exception.Message

‘ 詳細情報の構築
Dim sb As New StringBuilder()
sb.AppendLine(“=== [System Environment] ===”)
sb.AppendLine($”OS Version : {Environment.OSVersion}”)
sb.AppendLine($”Machine Name: {Environment.MachineName}”)
sb.AppendLine($”User Name : {Environment.UserName}”)
sb.AppendLine($”.NET Runtime: {Environment.Version}”)
sb.AppendLine($”Working Set : {Environment.WorkingSet / 1024 / 1024:N2} MB”)
sb.AppendLine()
sb.AppendLine(“=== [Exception Details] ===”)
sb.AppendLine($”Type : {_exception.GetType().FullName}”)
sb.AppendLine($”Message: {_exception.Message}”)
sb.AppendLine($”Source : {_exception.Source}”)
sb.AppendLine()
sb.AppendLine(“=== [Stack Trace] ===”)
sb.AppendLine(_exception.StackTrace)

‘ 内部例外(InnerException)の再帰的走査(データベースエラーなどで極めて重要)
Dim inner = _exception.InnerException
Dim depth As Integer = 1
While inner IsNot Nothing
sb.AppendLine()
sb.AppendLine($”=== [Inner Exception (Depth {depth})] ===”)
sb.AppendLine($”Type : {inner.GetType().FullName}”)
sb.AppendLine($”Message: {inner.Message}”)
sb.AppendLine($”Stack Trace:”)
sb.AppendLine(inner.StackTrace)
inner = inner.InnerException
depth += 1
End While

txtDetails.Text = sb.ToString()
End Sub

”’

”’ クリップボードへ整形済みテキストをワンタッチコピー
”’

Private Sub BtnCopy_Click(sender As Object, e As EventArgs)
Try
Clipboard.SetText(txtDetails.Text)
MessageBox.Show(“エラー詳細をクリップボードにコピーしました。” & Environment.NewLine &
“メールやチャットツールに貼り付けてご報告ください。”,
“コピー完了”, MessageBoxButtons.OK, MessageBoxIcon.Information)
Catch ex As Exception
MessageBox.Show(“クリップボードへのコピーに失敗しました: ” & ex.Message,
“エラー”, MessageBoxButtons.OK, MessageBoxIcon.Warning)
End Try
End Sub

End Class

—

現場で活きる!ファイルやデータベース連携の注意点

このダイアログを表示するだけでは「対症療法」に過ぎない。真に堅牢な業務アプリを構築するためには、以下のインフラ的配慮が不可欠である。

1. ログのローカル自動保存(ファイル連携)

ユーザーが「コピーし忘れて画面を閉じてしまった」という悲劇を防ぐため、ダイアログを表示するその瞬間に、ローカルのアプリデータフォルダ(例: `%AppData%\YourCompany\YourApp\Logs`)へ例外をサイレント出力する実装を必ず挟むこと。

.net
Private Shared Sub WriteErrorLog(ex As Exception)
Try
Dim logDir As String = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), “YourCompany”, “Logs”)
If Not Directory.Exists(logDir) Then Directory.Exists(logDir)

Dim logPath As Path = Path.Combine(logDir, $”Error_{DateTime.Now:yyyyMMdd_HHmmss}.log”)
File.WriteAllText(logPath, ex.ToString(), Encoding.UTF8)
Catch
‘ ログ書き込み自体の失敗でアプリケーションがクラッシュするのを防ぐため、例外は握りつぶす
End Sub
End Sub

2. データベース連携時のトランザクション破棄

DB処理中に例外が発生した場合、接続(Connection)やトランザクション(Transaction)が中途半端なロックを保持したまま放置されることがある。
`Try…Catch…Finally` 構文の `Finally` 句、あるいは `Using` ステートメントを徹底し、リソースのリークを絶対に防ぐこと。

—

グローバル例外ハンドラへの組み込み

最後に、このカスタムダイアログをアプリケーション全体のエントリポイントにフックする。
`Application.ThreadException` や `AppDomain.CurrentDomain.UnhandledException` を用いることで、コードの至るところで `Try-Catch` を書かなくとも、網の目のように予期せぬクラッシュを拾うことができる。

.net
Friend Module Program

Sub Main()
Application.EnableVisualStyles()
Application.SetCompatibleTextRenderingDefault(False)

‘ UIスレッドの未処理例外をキャッチ
AddHandler Application.ThreadException, AddressOf UIThreadException

‘ 非UIスレッドの未処理例外をキャッチ
AddHandler AppDomain.CurrentDomain.UnhandledException, AddressOf NonUIThreadException

‘ メインフォームの起動
Application.Run(New MainForm())
End Sub

Private Sub UIThreadException(sender As Object, e As Threading.ThreadExceptionEventArgs)
Using dlg As New ErrorDialog(e.Exception)
dlg.ShowDialog()
End Using
End Sub

Private Sub NonUIThreadException(sender As Object, e As UnhandledExceptionEventArgs)
Dim ex As Exception = TryCast(e.ExceptionObject, Exception)
If ex IsNot Nothing Then
Using dlg As New ErrorDialog(ex)
dlg.ShowDialog()
End Using
End If
End Sub
End Module

—

チーフアーキテクトからの総括

ソフトウェアの品質は、ハッピーパス(正常系)の美しさではなく、エッジケース(異常系)の優しさで決まる。

今回紹介したカスタム例外ダイアログは、単にエラーを表示するだけのツールではない。ユーザーのフラストレーションを最小限に抑え、開発者・情シスへ正確なデバッグ情報を1秒で伝達するための「業務効率化のキーストーン」である。

泥臭い「なんか動かないんですけど」という問い合わせの時代は、今日で終わりだ。洗練された例外設計で、あなたのアプリケーションを真のプロフェッショナル・プロダクトへと昇華させてほしい。

タイトルとURLをコピーしました