連載

Windows業務アプリケーション開発 Q&A #6

グレープシティ株式会社 八巻 雄哉
2006/11/18

本記事は、業務アプリケーション向けコンポーネントのベンダであるグレープシティのテクニカル・サポート担当に対して、実際にプログラマーから問い合わせがあった質問を取り上げて解説しています。

VS 2005で実行するとスレッド処理で例外が発生

 Visual Studio .NET 2003(以下、VS 2003)で作成したWindowsアプリケーションをVisual Studio 2005(以下、VS 2005)へ移行する作業をしています。VS 2003のときは問題なく動作していたコードなのですが、VS 2005で実行すると、

「有効ではないスレッド間の操作: コントロールが作成されたスレッド以外のスレッドからコントロール '……' がアクセスされました。」

という例外が発生してしまいます。.NET Framework 2.0(以下、.NET 2.0)ではスレッド関係の処理に何か変更があったのでしょうか?

 Windowsフォームのコントロールは、基本的に複数のスレッドからアクセスされることは考慮されていません(スレッドセーフではありません)。そのため、コントロールの操作は必ずそのコントロールを作成したスレッドから行うようにする必要があります。

 このことは、.NET Framework 1.x(以下、.NET 1.x)でも.NET 2.0でも同じですが、.NET 2.0ではスレッドセーフでない方法によるコントロールへのアクセスが検出され、それを行った場合には例外が発生するようになりました。

 では、このスレッドセーフでない方法によるコントロールへのアクセスとはどのようなものか、実際に確認してみましょう。

 Windowsアプリケーションのプロジェクトを新規作成し、フォームにButtonコントロールとTextBoxコントロールをそれぞれ1つずつ配置して下記のコードを記述してください。ここでは言語としてVisual Basicを使用します。

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
  Dim newThread As New System.Threading.Thread( _
    New System.Threading.ThreadStart(AddressOf Me.ThreadProcedure))
  newThread.Start() ' スレッドの開始
End Sub

' 別のスレッドで実行されるメソッド
Private Sub ThreadProcedure()
  Me.TextBox1.Text = "安全ではないスレッド操作"
End Sub
リスト1 Windowsフォーム・コントロールのスレッドセーフでない呼び出し

 上記のコードを実行すると、VS 2003の場合には特に例外は発生しませんが、VS 2005で実行した場合には下記の例外が発生することを確認できるかと思います。

有効ではないスレッド間の操作: コントロールが作成されたスレッド以外のスレッドからコントロール 'TextBox1' がアクセスされました。
VS 2005でリスト1を実行した場合に発生する例外メッセージ

 上記のようなコードは、文法的に間違っているわけではありませんが、結果が保証されない行儀の悪いコードであるといえます。VS 2003では問題なく動いてしまっていたコードも、VS 2005では安全ではないコードとして警告されるようになったというわけです。

 では、正しいコードはどのようになるのか、ここではInvokeメソッドを使う方法と.NET 2.0から追加されたBackgroundWorkerコンポーネントを使う方法の2つを紹介します。

■Invokeメソッドを使う方法(.NET 1.x、.NET 2.0)

.NET TIPS:Windowsフォームで別スレッドからコントロールを操作するには?

 WindowsフォームのコントロールにはInvokeメソッドが用意されており、このメソッドを使うことでコントロールに対する操作をコントロールの作成されたスレッドで実行させることができます。

 先ほどのリスト1のコードを下記のように書き換えます。このコード例では、InvokeRequiredプロパティを使ってInvokeメソッドを呼び出す必要があるかどうかを判定するようにしておき、必要がある場合(そのコードが別のスレッドで実行されている場合)にはInvokeメソッドでもう一度SetTextメソッドを呼び出しています。

Delegate Sub SetTextCallback(ByVal mText As String)

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
  Dim newThread = New System.Threading.Thread( _
    New System.Threading.ThreadStart(AddressOf Me.ThreadProcedure))
  newThread.Start()' スレッドの開始
End Sub

' 別のスレッドで実行されるメソッド
Private Sub ThreadProcedure()
  Me.SetText("Invokeメソッドを使った安全なスレッド操作")
End Sub

Private Sub SetText(ByVal mText As String)
  If Me.TextBox1.InvokeRequired Then
    ' 別のスレッドなのでInvokeメソッド経由による呼び出しが必要
    Dim d As New SetTextCallback(AddressOf SetText)
    Me.Invoke(d, New Object() { mText })
  Else
    ' ここは必ずメインのスレッドで実行される
    Me.TextBox1.Text = mText
  End If
End Sub
リスト2 Windowsフォーム・コントロールのInvokeメソッドを使ったスレッドセーフな呼び出し

■BackgroundWorkerコンポーネントを使う方法(.NET 2.0のみ)

Visual Basic 2005 ここが便利! 第4回 Background Workerで夢のマルチスレッドがついに!
.NET TIPS:時間のかかる処理をバックグラウンドで実行するには?

 BackgroundWorkerコンポーネントを使えば、イベント・ハンドラとしてスレッド処理を書くことができます。面倒なデリゲートの作成もInvokeメソッドの呼び出しも必要なくなるので非常に便利です。

 BackgroundWorkerコンポーネントには、下記の3つのイベントが用意されています。

イベント名 発生するタイミング
DoWork RunWorkerAsyncメソッドが呼び出されたときに発生する
ProgressChanged ReportProgressメソッドが呼び出されたときに発生する
RunWorkerCompleted DoWorkイベント・ハンドラの処理が完了したとき、キャンセルされたとき、例外が発生したときに発生する
BackgroundWorkerコンポーネントで用意されている3つのイベント

 このうち、DoWorkイベント・ハンドラとなるメソッドは別スレッドで実行されます。通常、このイベントを使って時間のかかる処理などを記述します。

 ProgressChangedイベントは非同期操作の進行状況をユーザーに報告するために使用されるイベントで、そのイベント・ハンドラはコントロールが作成されたスレッドで実行されます。このため、このイベントを利用してWindowsフォーム・コントロールにアクセスする処理を非同期に実行することができます。

 ツールボックスの[コンポーネント]タブからBackgroundWorkerコンポーネントを選択してフォームに追加し、プロパティ・ウィンドウでBackgroundWorkerコンポーネントのWorkerReportsProgressプロパティをTrueに設定します。このプロパティをTrueに設定することで、DoWorkイベント・ハンドラ内からReportProgressメソッドを呼び出してProgressChangedイベントを発生させることができます。

 先ほどのコードを下記のように書き換えます。

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
  ' 別スレッドでの実行を開始
  Me.BackgroundWorker1.RunWorkerAsync()
End Sub

' このメソッドは別スレッドで実行される
Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
  ' ProgressChangedイベントを発生させる
  BackgroundWorker1.ReportProgress(50)
End Sub

' このメソッドはコントロールを作成したスレッドで実行される
Private Sub BackgroundWorker1_ProgressChanged(ByVal sender As System.Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
  ' 進行状況をプログレス・バーに表示するコード例
  ' e.ProgressPercentageにはReportProgressメソッドの
  ' のパラメータが受け渡される
  ' ProgressBar1.Value = e.ProgressPercentage

  Me.TextBox1.Text = "BackgroundWorkerコンポーネントを使った安全なスレッド操作"
End Sub
リスト3 BackgroundWorkerコンポーネントを使ったスレッドセーフな呼び出し

 なお、操作しようとするコントロールのCheckForIllegalCrossThreadCallsプロパティの値をFalseに設定することで、VS 2005でもVS 2003で実行していたときと同じように例外を発生させないようにすることは可能です。ただし、コードの実行結果はまったく保証されませんので、根本的な解決にはなりません。

【参考】MSDN: 方法 : Windows フォーム コントロールのスレッド セーフな呼び出しを行う

 

10ポイントに設定したはずのフォントが9.75ポイントに

 Visual Basic 6.0やVisual Studio 2005で、フォームやコントロールのフォントの大きさを[フォント]ダイアログを使用して10ポイントに設定した場合、なぜか9.75ポイントに設定されてしまいます。なぜこのような動作となるのでしょうか?

 確かに[フォント]ダイアログを使用してフォント・サイズを設定すると、なぜか設定した値から微妙にずれた値が設定されてしまうことがあります。一見すると間違った動作に思えるこの現象ですが、「ポイント」という単位のフォントが実際にはどのように画面に表示されるのかを理解することで、それが誤解であることが分かります。

 まず、ポイントという単位はどのような単位なのでしょうか? 1ポイントは1/72インチと決められています。つまりフォントを10ポイントで設定した場合、そのフォントの大きさは10/72インチになります。

 次に、この10/72インチのフォントをディスプレイに表示することを考えてみましょう。Windowsの画面はデフォルトでは96dpiに設定されています。「dpi」というのは「Dot Per Inch」の略ですので、96dpiの場合1/96インチのものが1ドットとして表示されるということになります。

 では、10/72インチのフォントを96dpiの画面で表示すると何ドットになるのでしょう?

  • 96 dpi × 10/72 インチ = 13.333333333333333333……

 13.3333……と割り切れない値になってしまいます。つまり、10ポイントと設定しても、ディスプレイで正確に10ポイントの大きさを表現することはできないということになります。

 では9.75ポイントの場合はどうでしょうか?

  • 96 dpi × 9.75/72 インチ = 13

 13ドットと割り切れる値になりました。

 これらの結果から分かるように、[フォント]ダイアログがフォントのサイズを画面に表示できる適切な値に自動的に変換してくれていたというわけです。

 ちなみに、画面を120dpiに変更し、[フォント]ダイアログからフォント・サイズを10ポイントに設定すると、今度は9.75ポイントではなく10.2ポイントに設定されます。

  • 120 dpi × 10.2/72 インチ = 17

 [フォント]ダイアログにとっては、「良かれと思ってしたことが…」といったところでしょうか。End of Article


八巻 雄哉(やまき ゆうや)
グレープシティ株式会社 テクニカルエバンジェリスト

2003年グレープシティ入社。PowerToolsシリーズのテクニカル・サポートを担当する傍ら、製品開発やマーケティングにも従事。現在は.NETとPowerToolsシリーズ普及のため、エバンジェリストとして活動中。.NET Framework 3.0の中で一番興味があるのはWPF。http://d.hatena.ne.jp/Yamaki/にてBlogを公開中。


インデックス・ページヘ  「Windows業務アプリケーション開発 Q&A」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)
- PR -

注目のテーマ

業務アプリInsider 記事ランキング

本日 月間
ソリューションFLASH