連載 .NETでWindowsアプリを作ろう

第3回 画像検索ツール本体をマルチスレッドで作ろう

デジタルアドバンテージ 遠藤 孝信
2005/10/29
Page1 Page2 Page3 Page4

■マルチスレッドによる検索処理

 [検索開始]ボタンがクリックされると、検索を別スレッドで開始します。お待たせしました、ここでやっとスレッドが登場します。

 [検索開始]ボタンのイベント・ハンドラでは、次のようにしてスレッドを作成し、そのStartメソッドにより検索スレッドの実行を開始します。「adding」が、検索スレッドとして実行されるメソッドの名前です。

private void buttonStart_Click(object sender, System.EventArgs e)
{
  keyword = textBoxKeyword.Text;

  // キーワードが未入力なら何もしない

  if (keyword == "")
    return;

  ……省略……

  // 別スレッドで画像を検索・追加
  Thread worker = new Thread(new ThreadStart(adding));
  worker.Name = "AddImage";
  worker.IsBackground = true;
  worker.Start();
}
Private Sub ButtonStart_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ButtonStart.Click
  keyword = TextBoxKeyword.Text

  ' キーワードが未入力なら何もしない
  If keyword Is "" Then
    Return
  End If

  ……省略……


  ' 別スレッドで画像を検索・追加
  Dim worker As Thread = New Thread(New ThreadStart(AddressOf adding))
  worker.Name = "AddImage"
  worker.IsBackground = True
  worker.Start()
End Sub
[検索開始]ボタンのクリック時に実行されるイベント・ハンドラ(上:C#、下:VB.NET)

 ここでは、スレッドのIsBackgroundプロパティをtrueに設定して、プロセス(アプリケーション)の終了時には検索スレッドも終了するようにしています。また、Nameプロパティによりスレッドに名前を付けているのは、VS.NETの[出力]ウィンドウでスレッドが終了したのを確認しやすくするためです(これがないと「名前がありません」という名前で表示されてしまいます)。

 さて、検索スレッドとして実行されるaddingメソッドは次のようになっています。

public void adding()
{
  do
  {
    foreach (WebImage wi in google.GetImages(keyword))
    {
      if (working == false)
        break;

      using (Image img = downloadImage(wi.SmallImageURL))
      {
        if (img != null)
        {
          webImages.Add(wi);
          addToThumbViewer(img);
        }
      }
    }
  } while (google.MoveNext() && working);

  working = false; // 停止ボタンを押さずに抜けたときのため
  readyToStart();
}
Sub adding()
  Do
    For Each wi As WebImage In google.GetImages(keyword)
      If working = False Then
        Exit For
      End If

      Dim img As Image = downloadImage(wi.SmallImageURL)
      If Not img Is Nothing Then
        webImages.Add(wi)
        addToThumbViewer(img)
      End If
      img.Dispose()
    Next
  Loop While google.MoveNext() And working

  working = False ' 停止ボタンを押さずに抜けたときのため
  readyToStart()
End Sub
検索を実行し、結果をサムネイル画像コントロールに追加するaddingメソッド(上:C#、下:VB.NET)

 このメソッドは、検索結果がなくなる(MoveNextメソッドがfalseを返す)か、workingフラグがfalseになるまで、検索とサムネイル画像の追加のループを繰り返します。

 ループ内ではまずgoogle.GetImages(keyword)により、Googleでの検索を実行して、その結果を得ます。変数googleが参照しているGoogleImageオブジェクトの利用方法については連載第1回目で解説しています。

 このGetImagesメソッドのパラメータには検索キーワードを渡しますが、パラメータとして、テキストボックスに入力されている文字列を示す「textBoxKeyword.Text」を直接渡していないことに注意してください。

 同様に、サムネイル画像コントロールに画像を追加するのに、「thumbViewer1.Add(img)」としてコントロールのAddメソッドを直接呼び出さずに、addToThumbViewerメソッドを呼び出している点にも注意してください。

 このようにしている理由は、このaddingメソッドがUIスレッドとは別の検索スレッドで実行されるためです。Windowsアプリケーションでは、UIスレッド以外のスレッドからのフォーム上のコントロールの操作(プロパティの読み書きやメソッドの呼び出しなど)は、その動作が保証されていないのです。プログラマーにとっては単に面倒なだけの話ですが、そういう仕様なのでここではそれに従うしかありません。もちろんこのルールをうまく回避するための仕組みも用意されています。

 検索スレッドからコントロールのメソッドを呼び出すには、それを行う別のメソッドと、そのメソッドのデリゲートを作成し、コントロール(通常はフォーム)のInvokeメソッドでそのデリゲートを呼び出してやります。

 デリゲートの宣言と、サムネイル画像コントロールに画像を追加するaddToThumbViewerメソッドは次のようになっています。ちなみに、このメソッドはUIスレッドからも検索スレッドからも呼び出せるようにしています*

* これらについての詳細は、.NET TIPS:Windowsフォームで別スレッドからコントロールを操作するには?で解説されています。
 
delegate void AddToThumbViewerDelegate(Image img);
void addToThumbViewer(Image img)
{
  if (this.InvokeRequired)
  {
    this.Invoke(
      new AddToThumbViewerDelegate(addToThumbViewer),
      new object[] {img});
  }
  else
  {
    this.Text = String.Format(
            "{0} - 検索結果: {1} 件 ({2} ページ)",
            AppName, ++numFound, google.CurrentPage);
    thumbViewer1.Add(img);
  }
}
Delegate Sub addToThumbViewerDelegate(ByVal img As Image)
Sub addToThumbViewer(ByVal img As Image)
  If Me.InvokeRequired Then
    Me.Invoke( _
     New addToThumbViewerDelegate(AddressOf addToThumbViewer), _
     New Object() {img})
  Else
    numFound += 1
    Me.Text = String.Format( _
    "{0} - 検索結果: {1} 件 ({2} ページ)", _
    appName, numFound, google.CurrentPage)
    ThumbViewer1.Add(img)
  End If
End Sub
UIスレッド上でサムネイル画像コントロールに画像を追加するaddToThumbViewerメソッド(上:C#、下:VB.NET)

 このメソッド後半のelseブロック部分のコードは、Invokeメソッドによりデリゲート経由で呼び出されて、実際にはUIスレッド内で実行されます。このため、この部分ではコントロールを直接操作することができます。

 ここではタイトルバーに表示する検索結果の件数も設定しています。当然ながら、フォームのTextプロパティ設定もUIスレッド以外のスレッドからは動作が保証されません。

■検索の停止処理

 検索の実行中に[停止]ボタンが押されると、検索スレッドを終了させて新たに検索できる状態にします。

 スレッドを中断する1つの手段として、ThreadクラスにはAbortメソッドが用意されていますが、このメソッドではスレッドの処理がどこで中断されるか予測が付かず少々危険です*。これは、アプリケーションを終了させるためにPCの電源を切るようなものです。

* このため、Abortメソッドを呼び出すと対象となるスレッドでThreadAbortException例外が発生するようになっています。

 すでに述べているように、今回ではUIスレッドでworkingフラグをfalseに設定することにより、検索スレッドが自分でこのフラグを確認し、ループを抜け出すようにしています。

 ただしこのために、[停止]ボタンをクリックしても検索がすぐに終了しないことがあります(画像のダウンロード中の場合など)。ここでは[停止]ボタンがクリックされてから検索スレッドが完全に終了するまでの間は、Panelコントロールを無効化(Enableプロパティをfalseに設定)して、その間はアプリケーションを操作できないようにしています。

■サムネイル画像のクリック

 最後にサムネイル画像コントロール上で表示されているサムネイル画像がクリックされたときの処理を実装しましょう。サムネイル画像のクリックに関しては、次の3つのパターンがあります。

  • 左ボタン・クリック時には画像のあるWebページのURLをステータスバーに表示

  • 左ボタン・ダブルクリック時には、サムネイル画像の元画像をブラウザで表示

  • 右ボタン・ダブルクリック時には、画像のあるWebページをブラウザで表示

 サムネイル画像コントロール上でマウスがクリックされると、MouseDownイベントが発生します。このイベントのイベント・ハンドラとなるメソッドをまずは作成しましょう。

 C#では、サムネイル画像コントロールを選択してから、プロパティウィンドウで雷マークをクリックし、[MouseDown]の横のコンボボックスをダブルクリックしてください。

MouseDownイベント・ハンドラの追加(C#の場合)
サムネイル画像コントロールを選択してから[MouseDown]の横のコンボボックスをダブルクリックする。

 VB.NETの場合には、エディタ画面の左上にあるコンボボックスから「ThumbViewer1」を選択し、さらにその右隣のコンボボックスで「MouseDown」を選択します。

MouseDownイベント・ハンドラの追加(C#の場合)
エディタ画面の左上にあるコンボボックスから「ThumbViewer1」を選択し、さらにその右  隣のコンボボックスで「MouseDown」を選択する。

 以上の操作により、MouseDownイベント・ハンドラが自動的に作成されます。このメソッドの内容は次のようになります。

private void thumbViewer1_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e)
{
  WebImage wi = (WebImage)webImages[thumbViewer1.SelectedIndex];
  statusBar1.Text = wi.DetailPageURL;

  if (e.Clicks == 2) // ダブルクリック
  {
    System.Diagnostics.Process.Start(
      e.Button == MouseButtons.Left
        ? wi.LargeImageURL : wi.DetailPageURL);
  }
}
Private Sub ThumbViewer1_MouseDown(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles ThumbViewer1.MouseDown
  Dim wi As WebImage = CType(webImages(ThumbViewer1.SelectedIndex), WebImage)
  StatusBar1.Text = wi.DetailPageURL

  If e.Clicks = 2 Then     ' ダブルクリック
    System.Diagnostics.Process.Start( _
      IIf(e.Button = MouseButtons.Left, _
        wi.LargeImageURL, wi.DetailPageURL))
  End If
End Sub
サムネイル画像のクリック時に実行されるイベント・ハンドラ(上:C#、下:VB.NET)

 ここではクリックされた画像のインデックス(=画像を追加した順番)をthumbViewer1.SelectedIndexにより取得し、同じインデックス番号を持つWebImageオブジェクトをコレクションであるwebImagesフィールドから取り出します。

 そしてまず、ステータスバーにそのWebページのURLを表示します。

 次にダブルクリック時の処理を行います。マウスが何回クリックされたかは、このメソッドのパラメータeのClicksプロパティで分かります。またボタンが左か右かは、パラメータeのButtonプロパティから分かります。

 特定のURLをWebブラウザで表示するにはProcessクラス(System.Diagnostics名前空間)のStartメソッドを呼び出すだけで実現できます*

* これについては「.NET TIPS:プログラムからブラウザやメーラを起動するには?」を参照してください。

 以上でアプリケーションの主要な処理の解説は終わりです。検索部分や表示部分をあらかじめコンポーネント化して作成しておいたので、本体はシンプルに記述できたと思います。

 さて最終回となる次回では、Amazon検索用のプラグインなどを作成し、今回作成したアプリケーション本体も少し改造して起動時にプラグインをロードし、検索先を選択できるようにする予定です。お楽しみに。End of Article


 INDEX
  .NETでWindowsアプリを作ろう
  第3回 画像検索ツール本体をマルチスレッドで作ろう
    1.Google用プラグイン・プロジェクトの追加とテスト
    2.サムネイル画像コントロールの登録とテスト
    3.アプリケーションの画面設計とコーディング
  4.マルチスレッドによる検索処理
 
インデックス・ページヘ  「.NETでWindowsアプリを作ろう 」


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メールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間