- PR -

WebBrowser コントロールを非同期で操作したい。

投稿者投稿内容
しん
常連さん
会議室デビュー日: 2004/10/08
投稿数: 20
投稿日時: 2008-12-21 03:39
WebBrowser コントロールについての質問です。

・使用環境:Windows XP SP2 / VB2005(.NET Framework 2.0 ) / IE 6.0

・質問内容

 WebBrowser コントロールの Web 画面に表示されたリンクをクリックしたり、
 コード内で WebBrowser の Navigate メソッドを使ってホームページに
 アクセスする処理は、フォームの GUI を処理するスレッドとは
 別スレッドで行われているのでしょうか?

 フォーム上に「 WebBrowser コントロール」と、「 URL 入力用のテキストボックス」と「進むボタン」を配置しました。そして「進むボタン」のクリックイベントハンドラに以下のようなコードを記述しました。

| Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
| '進むボタンをクリックした時
|
| 'ブラウザコントロールで Me.TextBox1.Text のURLへ移動する。
| Me.WebBrowser1.Navigate(Me.TextBox1.Text)
| End Sub

 「進むボタン」をクリックすると、「 WebBrowser コントロール」は、Navigate() メソッドによって「 URL 入力用テキストボックス」に書かれた URL 先にアクセスし、Web 画面を表示します。

 実際にプログラムを実行させて、「進むボタン」を1度クリックした後、「 WebBrowser コントロール」の Web 画面が切り替わる前に何度か続けてクリックしてみたのですが、Web 画面が切り替わる前でも、ボタンは反応していました。(クリックできました。)

 これは、フォームの GUI を扱うスレッドと「 WebBrowser コントロール」の画面を切り替える処理が別スレッドで動作しているということなのでしょうか?

 ただ、フォームのコンストラクタに以下のようなコードを記述すると、
プログラムを実行してからフォームが表示されるまで時間がかかるようになってしまいました。
(プログラムを実行してからしばらくの間フォーム自体が表示されません。)

| Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
| 'フォームが読み込まれた時
| Me.WebBrowser1.Navigate("http://yahoo.co.jp")
| End Sub

 フォームのコンストラクタ内で「 WebBrowser コントロール」に.Navigate("http://yahoo.co.jp") というメソッドを与えると、プログラムを実行してから実際にフォームが表示されるまで、時間がかかります。これは、WebBrowser コントロール内での処理が終了するまでフォームの GUI が応答しなくなっているということなのでしょうか?


 今後、プログラム内で「 WebBrowser コントロール」を操作する処理を作ろうと思っています。

 具体的には、URL リストが書かれたテキストファイルを読み込んで「 WebBrowser コントロール」でアクセスする処理と、「 WebBrowser コントロール」の Web画面を読み込んで、表示された情報をテキストとして取得する処理です。
この二つの処理は、どちらもボタンクリックで処理が開始される(任意のタイミングで処理が発生する)ということと、処理が重い(「 WebBrowser コントロール」が完全にページを読み込むのを待つ)ため、フォームの GUI を操作するスレッドとは異なるスレッドで、かつ二つの処理も別のスレッドで発生させたいと考えています。

 WebBrowser コントロール自体が内部的に別スレッドで動いているのであれば、自分で別スレッドを実装するコードを書く必要はないのでしょうか?

 自分で別スレッドを用意しなければならないのであれば、BackgroundWorker コントロールを使おうと考えていますが、「 WebBrowser コントロール」の Web 画面をメインフォームに表示する部分だけを別スレッドにするなどといったことは可能なのでしょうか。

どなたか、ご教授いただけませんでしょうか。
unibon
ぬし
会議室デビュー日: 2002/08/22
投稿数: 1532
お住まい・勤務地: 美人谷        良回答(20pt)
投稿日時: 2008-12-21 11:05
スレッドについてはあまり知りませんが、WebBrowser コントロールは良く使っている者です。


引用:

しんさんの書き込み (2008-12-21 03:39) より:
 これは、フォームの GUI を扱うスレッドと「 WebBrowser コントロール」の画面を切り替える処理が別スレッドで動作しているということなのでしょうか?


たとえば DoEvents 相当のことをすれば、別スレッドでなくてもこのようなことはできますから、これだけでは別スレッドかどうかは判明しないと思います。


引用:

しんさんの書き込み (2008-12-21 03:39) より:
 フォームのコンストラクタ内で「 WebBrowser コントロール」に.Navigate("http://yahoo.co.jp") というメソッドを与えると、プログラムを実行してから実際にフォームが表示されるまで、時間がかかります。これは、WebBrowser コントロール内での処理が終了するまでフォームの GUI が応答しなくなっているということなのでしょうか?


これは理由が良く分かりませんが、コンストラクター内で Navigate はタイミング的に早すぎるような気がします。


引用:

しんさんの書き込み (2008-12-21 03:39) より:
 WebBrowser コントロール自体が内部的に別スレッドで動いているのであれば、自分で別スレッドを実装するコードを書く必要はないのでしょうか?


私が WebBrowser コントロールを制御するプログラムを動かした場合、デュアルCPUのPCで動かしてタスクマネージャーのパフォーマンスタブに4つのCPU使用率が表示されるような環境であっても、全体の使用率は25%を超えることがないように思っています。
ですので、WebBrowser コントロールは、積極的に別スレッドを使うようにはなっていないのではないかと推測します。
これを確かめるのは簡単で、こういうデュアルCPUのPCで、JavaScript の for 文などで無限ループするページを複数の WebBrowser コントロールで表示してみて、タスクマネージャーで見て、複数のCPUが使われているかを確認すれば良いでしょう。


引用:

しんさんの書き込み (2008-12-21 03:39) より:
 自分で別スレッドを用意しなければならないのであれば、BackgroundWorker コントロールを使おうと考えていますが、「 WebBrowser コントロール」の Web 画面をメインフォームに表示する部分だけを別スレッドにするなどといったことは可能なのでしょうか。


あまり良くは分かりませんが、これは難しいんじゃないでしょうかね。
私は Navigate 先のページのリンクやボタンをプログラムから押したいので WebBrowser を使っていますが、もし、そういう必要がないのでしたら、WebBrowser を使わない方法をお勧めします。WebRequest クラスなどです。
ほかには、逆のストラテジーとしては、WebBrowser を使う部分だけ別プロセスにしてしまうなどです。
しん
常連さん
会議室デビュー日: 2004/10/08
投稿数: 20
投稿日時: 2008-12-22 17:33
unibon 様

コメント有難うございます。
今回、この事象を調べる上で unibon 様の過去の書き込みはいくつか拝見させて頂き、大変参考になりました。

引用:

引用:
--------------------------------------------------------------------------------


しんさんの書き込み (2008-12-21 03:39) より:
 これは、フォームの GUI を扱うスレッドと「 WebBrowser コントロール」の画面を切り替える処理が別スレッドで動作しているということなのでしょうか?

--------------------------------------------------------------------------------


たとえば DoEvents 相当のことをすれば、別スレッドでなくてもこのようなことはできますから、これだけでは別スレッドかどうかは判明しないと思います。



なるほど、とても勉強になります。DoEvents を行うだけでユーザーインターフェースのレスポンスを向上させられそうです。有難うございました。

引用:

引用:
--------------------------------------------------------------------------------


しんさんの書き込み (2008-12-21 03:39) より:
 フォームのコンストラクタ内で「 WebBrowser コントロール」に.Navigate("http://yahoo.co.jp") というメソッドを与えると、プログラムを実行してから実際にフォームが表示されるまで、時間がかかります。これは、WebBrowser コントロール内での処理が終了するまでフォームの GUI が応答しなくなっているということなのでしょうか?

--------------------------------------------------------------------------------


これは理由が良く分かりませんが、コンストラクター内で Navigate はタイミング的に早すぎるような気がします。



ご指摘有難うございます。ブラウザ(IE 等)のように、起動したらホームページが自動的に表示されるようにしたいのですが、確かにコンストラクター内で、Navigate は不自然な気がします。どうすればよいか考えたのですが、やはり Form_Load のタイミングしか思いつかず・・・。

引用:

引用:
--------------------------------------------------------------------------------


しんさんの書き込み (2008-12-21 03:39) より:
 WebBrowser コントロール自体が内部的に別スレッドで動いているのであれば、自分で別スレッドを実装するコードを書く必要はないのでしょうか?

--------------------------------------------------------------------------------


私が WebBrowser コントロールを制御するプログラムを動かした場合、デュアルCPUのPCで動かしてタスクマネージャーのパフォーマンスタブに4つのCPU使用率が表示されるような環境であっても、全体の使用率は25%を超えることがないように思っています。
ですので、WebBrowser コントロールは、積極的に別スレッドを使うようにはなっていないのではないかと推測します。
これを確かめるのは簡単で、こういうデュアルCPUのPCで、JavaScript の for 文などで無限ループするページを複数の WebBrowser コントロールで表示してみて、タスクマネージャーで見て、複数のCPUが使われているかを確認すれば良いでしょう。



有難うございます。デュアルCPUのPCを持っていないので試せないのですが、WebBrowser コントロールは積極的に別スレッドを使うようにはなっていない、ということであれば、いろいろと糸がつながってきます。m(。_。)m
確かにその通りだと思いました。
Unibon 様の過去の書き込みをよく探し、勉強したいと思います。

引用:

引用:
--------------------------------------------------------------------------------


しんさんの書き込み (2008-12-21 03:39) より:
 自分で別スレッドを用意しなければならないのであれば、BackgroundWorker コントロールを使おうと考えていますが、「 WebBrowser コントロール」の Web 画面をメインフォームに表示する部分だけを別スレッドにするなどといったことは可能なのでしょうか。

--------------------------------------------------------------------------------


あまり良くは分かりませんが、これは難しいんじゃないでしょうかね。
私は Navigate 先のページのリンクやボタンをプログラムから押したいので WebBrowser を使っていますが、もし、そういう必要がないのでしたら、WebBrowser を使わない方法をお勧めします。WebRequest クラスなどです。
ほかには、逆のストラテジーとしては、WebBrowser を使う部分だけ別プロセスにしてしまうなどです。



私の無茶な話に付き合ってくださって有難うございます

1.Navigate 先のページを表示させてユーザーにリンククリックやボタンクリックの
  操作をさせたい。
2.Navigate 先のページでプログラムからリンクを取得したり、リンクをクリック(と同等の処理を)してさらに別のページへ移動したい。

上記二つのことをやりたいと思っています。

・WebBrowser1 は、普通にフォームに配置して、ユーザーに通常のブラウザ機能を提供したいと思っています。(1.) またここでは、通常のブラウザ操作のほかに、ブラウザに表示された情報をテキストに書き出すというユーザー操作も実装したいと思っています。ブラウザ画面表示が必要なので、WebBrowser を使おうと思っています。

・WebBrowser2 は、プログラム内で動的に生成し、プログラム内から各URLへアクセスし、情報を取得します。(2.) WebBrowser を使って、HtmlDocument(小文字?)クラスを使ってタグ走査をしたいと思ってます。


以下、試した内容です。

1.の処理にて、まずはプログラム起動時にどこかのホームページを表示させる部分を作りたいと思い、とりあえずコンストラクタから、別スレッドを起動させてみました。「WebBrowser を使う部分だけを別プロセスにする」ということを試そうと思ったのですが、エラーが出てうまくいきませんでした。


フォームに BackgrondWorkerコントロール「bgwWeb」 を貼り付け、
フォームのコンストラクタ内で、bgwWeb.RunWorkerAsync() を実行。

BackgroundWorker1_doWork(sender,e) ハンドラ内にて

'WebBrowser インスタンスを新規作成
Dim wb as New WebBrowser

とすると、ここで以下のエラーが発生しました。
「現在のスレッドはシングル スレッド アパートメントでないため、ActiveX コントロール '8856f961-340a-11d0-a96b-00c04fd705a2' をインスタンス化できません。」
bgwWeb のスレッドにて得られた WebBrowser をフォームに表示したいと思っていました。

BackgroundWorker クラスの .DoWork ハンドラ内では、WebBrowser インスタンスは作れないのでしょうか?

以下、入力したコード内容です。

引用:

Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
'フォームが読み込まれた時

'ホームページに移動
Me.bgwWeb.RunWorkerAsync("http://www.yahoo.co.jp")
End Sub

Private Sub bgwWeb_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles bgwWeb.DoWork
' bgwWeb による別スレッド処理
' ウェブブラウザを使う部分を当スレッドにしてしまう。
' ウェブブラウザオブジェクトを作成してwebサーフを行い、
' ウェブブラウザオブジェクトを返す。

' このメソッドへのパラメータ
Dim url As String = CType(e.Argument, String)

' senderの値
Dim worker As System.ComponentModel.BackgroundWorker _
= CType(sender, System.ComponentModel.BackgroundWorker)

' 時間のかかる処理
Dim wb As New WebBrowser    【←ここでエラー発生】

wb.Navigate(url)
While wb.IsBusy Or Not (wb.ReadyState = WebBrowserReadyState.Complete)
'読み込み待ち
application.DoEvents
End While

e.Result = wb
End Sub

Private Sub bgwWeb_RunWorkerCompleted(ByVal sender As Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles bgwWeb.RunWorkerCompleted
' 元のスレッドにて処理結果を取得
If Not (e.Error Is Nothing) Then
' エラーが発生した場合
MessageBox.Show(e.Error.Message, "エラー")
ElseIf e.Cancelled Then
MessageBox.Show("キャンセルされました")
Else
' 処理結果の取得
Dim rWB As WebBrowser = e.Result
' ↑New 演算子はいらない?(ここでのe.Result はメインスレッドの所有物?)
Me.Panel1.Controls.Add(rWB) 'フォームのPanel1 にrWBを格納して表示
rWB.Dock = DockStyle.Fill
End If
End Sub






[ メッセージ編集済み 編集者: しん 編集日時 2008-12-22 19:08 ]
しん
常連さん
会議室デビュー日: 2004/10/08
投稿数: 20
投稿日時: 2008-12-23 02:03
引用:

「現在のスレッドはシングル スレッド アパートメントでないため、ActiveX コントロール '8856f961-340a-11d0-a96b-00c04fd705a2' をインスタンス化できません。」



について、いろいろ試行錯誤し、(DoWork ハンドラの先頭に <STAThread> をつけるなど、いろいろな箇所に <STAThread> をつけてみましたが何も変わりませんでした)
「インスタンス化できない」ということなので、インスタンスは作らないで参照で引っ張ってくるという方法を思いつきました。

BackgroundWorker を RunWorkerAsync にて走らせる際、RunWorkerAsync(New WebBrowser) というように引数に WebBrowser インスタンスを渡すと、
上記エラーは回避できました。
(BackgroundWorker 内で WebBrowser インスタンスを生成せずに WebBrowser 操作を
 行うことはできるようです。)

ただし、このようにして作成した WebBrowser に対して .Navigate("http://yahoo.co.jp") などとメソッドを呼び出そうとすると、

引用:

'WebBrowser' コントロールのウィンドウ ハンドルを取得できません。ウィンドウなしの ActiveX コントロールはサポートされていません。



といったエラーが出て止まってしまいます。
どなたか、このようなエラーを回避する方法をご存知の方はいらっしゃらないでしょうか。

また、ここで操作する WebBrowser インスタンスは、あくまでも呼び出し元の
スレッドで作成されたものなので不安もありますが・・・。
このようにして作成したインスタンスについて、競合やデッドロックが起こる可能性はあるのでしょうか・・・。

コードは以下の通りです。

引用:

Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
'フォームが読み込まれた時

'別スレッドスタート
Me.bgwWeb.RunWorkerAsync(new WebBrowser)
End Sub

'別スレッド処理
Private Sub bgwWeb_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles bgwWeb.DoWork
' ウェブブラウザを操作するためのスレッド
' 引数のウェブブラウザオブジェクトに対して処理を行い
' ウェブブラウザオブジェクトを返す。

' このメソッドへのパラメータ
Dim bgWorkerArg As WebBrowser = CType(e.Argument, WebBrowser)

' senderの値
Dim worker As System.ComponentModel.BackgroundWorker _
= CType(sender, System.ComponentModel.BackgroundWorker)

' 時間のかかる処理
Dim wb As System.Windows.Forms.WebBrowser = bgWorkerArg '引数で WebBrowser を受け取る

wb.Navigate("http://yahoo.co.jp")  ' ←ここでエラー

e.Result = wb
End Sub

Private Sub bgwWeb_RunWorkerCompleted(ByVal sender As Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles bgwWeb.RunWorkerCompleted

If Not (e.Error Is Nothing) Then
' エラーが発生した場合
MessageBox.Show(e.Error.Message, "エラー")
ElseIf e.Cancelled Then
MessageBox.Show("キャンセルされました")
Else
' 処理結果の取得
Dim rWB As WebBrowser = e.Result
' ↑New 演算子はいらない?(ここでのe.Result はメインスレッドの所有物?)
rWB.Dock = DockStyle.Fill
Me.WebBrowser1 = rWB
'古い Me.WebBrowser1 の内容はどこへ?
End Sub




[ メッセージ編集済み 編集者: しん 編集日時 2008-12-23 05:53 ]
渋木宏明(ひどり)
ぬし
会議室デビュー日: 2004/01/14
投稿数: 1155
お住まい・勤務地: 東京
投稿日時: 2008-12-23 07:52
引用:

引用:

'WebBrowser' コントロールのウィンドウ ハンドルを取得できません。ウィンドウなしの ActiveX コントロールはサポートされていません。



といったエラーが出て止まってしまいます。
どなたか、このようなエラーを回避する方法をご存知の方はいらっしゃらないでしょうか。



ありません。

根本的に使い方が間違っているからそうなるので、正しい使い方をする以外に、正常な動作を得ることはできません。

WebBrowser を含む、多くの Windows.Forms コントロールは、Win32 レベルで動作する「ネイティブ コントロール」のラッパです。

Windows.Forms コントロールを new しただけでは「ネイティブコントロールとしての実体」が作成されない(=通常、Show() した時に作成されます)ので、この場合の Native() メソッドの呼び出しのような、「ネイティブコントロール」に対する操作は実行できません。

引用:

また、ここで操作する WebBrowser インスタンスは、あくまでも呼び出し元の
スレッドで作成されたものなので不安もありますが・・・。



最も安全な使い方は、メインスレッドで生成したフォームにコントロールを貼り付けておき、ワーカースレッドからはそれらに対して Invoke() を使用して操作することです。
unibon
ぬし
会議室デビュー日: 2002/08/22
投稿数: 1532
お住まい・勤務地: 美人谷        良回答(20pt)
投稿日時: 2008-12-23 11:25
引用:

しんさんの書き込み (2008-12-22 17:33) より:
ご指摘有難うございます。ブラウザ(IE 等)のように、起動したらホームページが自動的に表示されるようにしたいのですが、確かにコンストラクター内で、Navigate は不自然な気がします。どうすればよいか考えたのですが、やはり Form_Load のタイミングしか思いつかず・・・。


まず、コンストラクターと Form_Load は違うものです。私はコンストラクターかと思っていましたが、提示されたコードを見たら Form_Load のことを指されているのですよね。良くは知りませんが Form_Load なら大丈夫じゃないでしょうか?私も試してみましたが Form_Load で Navigate しても普通に動きました。
あと、候補としては Form_Activated あたりでやるという手もあります。ただ、このイベントは、ウィンドウを最小化・元のサイズに戻す、を繰り返すと何度もおこるイベントですので、別途フラグ変数などを使って最初の1回だけに限定する必要はあります。

引用:

しんさんの書き込み (2008-12-22 17:33) より:
有難うございます。デュアルCPUのPCを持っていないので試せないのですが、WebBrowser コントロールは積極的に別スレッドを使うようにはなっていない、ということであれば、いろいろと糸がつながってきます。m(。_。)m


前回書いたような JavaScript の無限(に近い)ループで実際に試してみましたが、1つのフォームに貼った複数の WebBrowser に同時に Navigate しても、処理は並列化されないみたいです。直列化されて順番に処理されているようです。(タスクマネージャーで4CPUに見える環境であっても。)

ただ、思うのですが、WebBrowser は普通のページならば、ネット通信の待ち時間がかかるだけであり、Flash や JavaScript を多用したような重いページでないかぎり、CPU使用率を消費することはそれほどなく、あまり並列処理にこだわる必要はないと思います。現状でもすでに非同期としては動くはずです。きちんと DocumentCompleted のイベントを使って処理していれば十分だと思うのですが、これではダメなのでしょうか?

引用:

しんさんの書き込み (2008-12-22 17:33) より:
1.Navigate 先のページを表示させてユーザーにリンククリックやボタンクリックの
  操作をさせたい。
2.Navigate 先のページでプログラムからリンクを取得したり、リンクをクリック(と同等の処理を)してさらに別のページへ移動したい。

上記二つのことをやりたいと思っています。


だとしたらやはり WebBrowser を使うほうがよいのかもしれませんね。WebRequest や WebClient だと、リンクやボタンをクリックするためには自前でそういう遷移をする処理を書かなくてはならず大変です。

引用:

しんさんの書き込み (2008-12-22 17:33) より:
以下、試した内容です。

1.の処理にて、まずはプログラム起動時にどこかのホームページを表示させる部分を作りたいと思い、とりあえずコンストラクタから、別スレッドを起動させてみました。「WebBrowser を使う部分だけを別プロセスにする」ということを試そうと思ったのですが、エラーが出てうまくいきませんでした。


ちょっとこれは私は分からないので、今のところはパスです。
なお、私が前回書いた「別プロセス」というのは、WebBrowser を貼ったフォームを作りそれをひとつのEXEとして完成させて、それをまったく別のEXEからDCOMやActiveXうんぬんのプロセス間通信で制御するといった、とても単純なことを想定していました。
unibon
ぬし
会議室デビュー日: 2002/08/22
投稿数: 1532
お住まい・勤務地: 美人谷        良回答(20pt)
投稿日時: 2008-12-23 11:54
引用:

しんさんの書き込み (2008-12-21 03:39) より:
 ただ、フォームのコンストラクタに以下のようなコードを記述すると、
プログラムを実行してからフォームが表示されるまで時間がかかるようになってしまいました。
(プログラムを実行してからしばらくの間フォーム自体が表示されません。)

| Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
| 'フォームが読み込まれた時
| Me.WebBrowser1.Navigate("http://yahoo.co.jp")
| End Sub

 フォームのコンストラクタ内で「 WebBrowser コントロール」に.Navigate("http://yahoo.co.jp") というメソッドを与えると、プログラムを実行してから実際にフォームが表示されるまで、時間がかかります。これは、WebBrowser コントロール内での処理が終了するまでフォームの GUI が応答しなくなっているということなのでしょうか?


ちょっと話を戻しますが、私の環境で実際にこれと同じことをやっても、フォームはすぐに表示されます。なにか環境が違うんでしょうかね?
私の環境は Windows XP + IE7.0 + Visual C# 2008 Express Edition + .NET Framework 3.5 です。
渋木宏明(ひどり)
ぬし
会議室デビュー日: 2004/01/14
投稿数: 1155
お住まい・勤務地: 東京
投稿日時: 2008-12-23 12:30
引用:

あと、候補としては Form_Activated あたりでやるという手もあります。



Form_Shown なんてのもあります。

スキルアップ/キャリアアップ(JOB@IT)