プログレスバーの表示がおかしい

[戻る]
bun 2007/12/13(木) 17:04:17
まとめてファイルのコピーを行うアプリケーションにおいて、コピー中の状況を
プログレスバーを使用して表示しています。

WindowsXPまでは、このプログレスバーが
  CProgressCtrl::SetPos()
で設定した値通りに正確に表示されていましたが、WindowsVistaになったら、お
かしな表示をするようになりました。

プログレスバーの更新速度がゆっくりの時は正確に表示されるのですが、更新速
度が速くなると、設定した値より小さな値を表示しています。

そして、CProgressCtrl::SetPos()の設定で100%に達して、プログレスバーを閉じ
る頃になっても、表示上は数%〜10数%程度の値しかありません。

修正方法をご存じの方はいませんでしょうか。

よろしくお願いいたします。

wclrp ( 'o') 2007/12/13(木) 23:19:28
再描画がされず10%以降変化しないのか
100を指定しているのに10と表示されるのか
どっち?

100を指定しているのに10と表示されるなら
Vistaのバグ?
でもいまさらそんなバグある?

bun 2007/12/14(金) 07:37:16
wclrp ( 'o')さん、どうもです。

残念ながら、どっちかは分かりません。
  CProgressCtrl::SetPos(5)
  CProgressCtrl::SetPos(10)
    :
  CProgressCtrl::SetPos(100)
という更新をゆっくり行うと正常に表示されるからです。同じ更新を数秒ぐらいで行う
と、最初の5, 10辺りしか表示されません。
つまり、15以降の描画をする気があるんだけど、追いついていない(再描画されない)の
か、15以降の指定が無視されたのかは、プログレスバーのソースコードがない以上、不
明です。

> でもいまさらそんなバグある?
私も同感です。しかし、現象を見る限り、そう言いたくなってくるのも事実です。

もしかしたら、[ファイルのコピー]と[プログレスバーの更新]を同一スレッドで行って
いるために無理が生じているかもしれません。
一度、マルチスレッド化を行ってみたいと思います。
結果報告はまたさせていただきます。

PATIO 2007/12/14(金) 10:08:26
> もしかしたら、[ファイルのコピー]と[プログレスバーの更新]を同一スレッドで行って
> いるために無理が生じているかもしれません。
> 一度、マルチスレッド化を行ってみたいと思います。

実は、昨日読んでいて言われているような事が気になりました。
Vistaは描画系がごっそり変わっているのでファイル処理の関係で
描画を後回しにされているのではないかなと。
ファイルコピー処理をどんな風に処理しているのかなと思ってました。
やり方にもよると思いますけれど、シングルスレッドで確実に描画を
させるのであれば、コピー処理を細分化してタイマーイベントを使って
毎回、OSの制御を返すようにしないとうまくいかないのではないかと
思います。

ITO 2007/12/14(金) 13:09:15
うーん
1. ファイルの処理関連をスレッド処理にする。
2.リアルタイムでなくなるかもしれないけど、
  ある程度ファイル処理が進むまでプログレスバーを更新させない。
この2点行うと変わるんじゃないかと思いますが......
ファイル処理を優先させるのが大事だと思います。

FUKU 2007/12/15(土) 13:51:59
う〜ん??
当方で試してみたところ、どうやっても上記のような事は起こりませんが。。。
環境はVista(32bit) VS2005 SP1 MFCです (ダイアログアプリで実験)

因みに、
AeroをONにしてもOFFにしても同じ。
視覚テーマをVistaスタイル(緑色の立体プログレスバー)にしても
通常スタイル(ハイライトのプログレスバー)にしても同じ。

WindowsXPで正常に表示されていたのであれば一度、互換モードを
「Windows XP(ServicePack2)」
にして実行してみては?

2007/12/15(土) 23:05:30
メッセージループが別スレッドのようには読めないのですが、きちんと分けています
か?
確かメッセージループを受け持つスレッドで画面の更新をしても、描画メッセージ
が処理されるまでは確実に再描画されなかったように思います。
さらに描画メッセージのWM_PAINTはあらゆるメッセージの中で1、2を争うほど優
先度が低いとの話も聞いた覚えが有ります。
再描画域の問題も有る筈なので、再現は手間かも知れません。

「処理の合間合間にメッセージキューが空になるまでメッセージを処理する様書
く」「メッセージループスレッドとプログレスバーの更新等の処理スレッドを分け
る」ことはやっていますか?

やっているのならばのならこちらの誤解ですが、どうですか。

ku 2007/12/17(月) 11:08:32
関係あるかどうか分かりませんが気になったことを。
アプリケーションウィザードの設定の
ユニコードライブラリを使用するのオンかオフかで、
ダイアログの見た目が変わります。
xpスタイルとvistaスタイル?
プログレスコントロールの見た目もかなり違います。

確認方法は
プログレスコントロールを貼り付け、メンバ変数を割り当てます
ボタン2つ追加して
1つめは
    xxx.SetRange(0, 100);
    xxx.SetPos(10);
2つめは
    xxx.SetPos(100);
とします。

1つめのボタンを押し、2つ目のボタンを押し、動作を確認します。
ユニコードを使用しない方は、比較的SetPos()に忠実な動作のように見えます。
ユニコードを使用する方(デフォルトで緑色?)の方は
2つめのSetPos(100)で、SetPos(20)SetPos(30)SetPos(40)...SetPos(100)
とやったような動作をします。

この辺はOSのなんらかの設定が効いているのかもしれないし
昔も似たような動作だけど、すばやく増えていたので見えなかったのかもしれない

最近、プログレスコントロールを使ったら、アレって思ったもので。

ITO 2007/12/17(月) 13:12:44
> プログレスバーの更新速度がゆっくりの時は正確に表示されるのですが、更新速
> 度が速くなると、設定した値より小さな値を表示しています。
うーーん、
 これを読む限りでは、プログレスバーの処理はうまくいっているんじゃないか
思います。
 ただ、速い処理になった時にSetPos()で設定する数値が変化してないのでは?

位置データをPosと仮定
 if(Pos == 10)
  {
    何らかのダミー処理
  ここでブレイク
 }
 ここで止まりますか?

ITO 2007/12/17(月) 13:14:49
修正です(^^;
誤 if(Pos == 10)

正 if(Pos == 100)
※100は適切な値にして下さい。

ITO 2007/12/17(月) 13:17:32
追伸、
推測ですけど、
Vistaは、XPに比べてウインドウメッセージループの更新速度が遅いのかな?

PATIO 2007/12/17(月) 14:41:44
私的にはVistaは表示の処理が重いのでXPの表示処理よりもスキップされる
閾値が低くてちょっと忙しくなると描画がスキップされてしまうのではないかと
思っていたりします。

bun 2007/12/17(月) 22:23:21
中間報告です。

まず、みなさんの読み通り、プログレスバーが追従しない場合は、SetPos()の速度が速
い以外の条件がありました。
1つはWindows-XP用のルナスタイルのマニフェストがあててあり、このマニフェストを
消すと高速SetPos()でも追従するようになりました。
要は視覚テーマをVistaスタイル(緑色の立体プログレスバー)にしたときだけ追従しなく
なる感じです。

あと、マルチスレッド化ですが、かなり苦労しています。
本格的なマルチスレッドプログラミングは初めてで、メインスレッドと、ファイルコピ
ーを行うスレッド双方で複数のウィンドウを持ってしまっているため、うまくウィンド
ウ間の通信ができないようです。
もう少し頑張ってみます。

そんな感じです。
また、何か分かりましたら報告します。

FUKU 2007/12/17(月) 23:22:01
マニフェスト絡みのようなので、こちらで試した
Vistaスタイルのマニフェスト(正常確認)を示しておきます。

<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity version="1.0.0.0" processorArchitecture="x86"
name="Microsoft.Winweb.HogeHoge.exe" type="win32"/>
<description>Description</description>
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32" name="Microsoft.Windows.Common-Controls"
version="6.0.0.0" processorArchitecture="x86"
publicKeyToken="xxxxxxxxxxxxxxxxxxx" language="*"/>

あと、ファイルコピーを別スレッドでするなら
コピーが1つ終わる度に、プログレスバーに対して
SendMessage(PBM_SETPOSかな?)を送ると簡単かもですね

PATIO 2007/12/18(火) 10:59:34
ファイルコピーのスレッドはワーカースレッドにして
ウインドウは持たせないようにした方がすっきりすると思いますけれど、
何かコピースレッド側にウインドウを持たせないといけない訳があるんでしょうか?

bun 2007/12/18(火) 14:28:40
FUKU様
ありがとうございます。
現在いる場所にVista環境がないため、帰宅してから確認してみたいと思います。
なんとなく、FUKU様の助言通りで直りそうな気がしてきました(^-^)

PATIO様
基本的にそういう作りです。
ただ、すでにできあがっているコードが、ファイルコピー処理中に、ウィンドウ(このウ
ィンドウがViewと通信してしまっている)生成を伴う処理をいくつか含んでおり、この段
階でそれらを取り除くには、大きな工数がかかるということです。
今、その作業を、行っております。
私自身、一から設計し直すなら、PATIO様のおっしゃるとおりの設計にするのは毎
違いないです。

また、何か分かりましたら報告します。

bun 2007/12/19(水) 13:29:17
FUKU様
いただいたマニフェストで試してみましたが、残念ながら駄目でした。

PATIO様
マルチスレッド化において、問題となっている部分が、昨日まで思っていた部分
とは違うようです。
VC++2005 SP1を使用して、MFCプログラミングをしていますが、
プログレスバーの横に、スタティックテキストを置き、「50%」のように表示し
ていますが、スタティックテキストにCString型のDDX変数を割り当てて、
  UpdateData(FALSE);
で、毎回、表示値を更新しています。このUpdateData(FALSE)の際に、
CDataExchangeクラスのコンストラクタで、
  ASSERT_VALID(pDlgWnd);
に失敗しているようです(wincore.cpp 3977行目)。
マルチスレッドプログラミングにおけるタブーがよく分かっていないのですが、
ワーカースレッド内から、別スレッドで作成されたウィンドウオブジェクト(MFC
のCHandleMapに登録されたもの)にアクセスしてはいけないのでしょうか?
別スレッドで作成されたウィンドウにアクセスするときは、ユーザー定義メッセ
ージを使用するといいと書かれた記事も目に入りました。

問題を切り分けるために、問題のアプリケーションのプログレスバーを含むダイ
アログだけを切り出し、新規ダイアログアプリに貼り付けてみました。
結果はそれでも駄目でした。
さらに、切り分けるために、さらにダイアログの機能を削ってみようと思います。
参考のため、現状、ダイアログの機能を列挙しておきます。
・処理中を示すために、風車が常に回っている
  (WM_TIMERで1秒毎にCStaticに2種類のアイコンを交互に張り直しているだけ)
・プログレスバーは2本張りついている
  (1本は全フォルダでの%表示、もう一本はフォルダ内での%表示)
・それぞれのプログレスバーには、横にスタティックテキストがある
  (プログレスバーの値を「50%」の用に表示)
・タイトルバーにはコピー中のフォルダ名が表示されている

よろしくお願いいたします。

ITO 2007/12/20(木) 10:14:16
> マルチスレッドプログラミングにおけるタブーがよく分かっていないのですが、
> ワーカースレッド内から、別スレッドで作成されたウィンドウオブジェクト(MFC
> のCHandleMapに登録されたもの)にアクセスしてはいけないのでしょうか?
僕の考えです。
間違っているところはフォロー願います。
僕の考えでは、
「ウインドウハンドル(HWND)を扱う処理をワ−カスレッドで行わない。」
ということですね。
どのようなタイミングか分らないですが、「HWND = NULL」になる時があります。
そのときにワ−カスレッドで処理を行うと例外が発生してアプリが異常終了します。
但し、親スレッドが「CView、CDialog」関連でコントロールのウインドウハンドル
(HWND)がいつでも取得できる場合は別です。
それでも、「HWND = NULL」の時処理しないようにするぐらいの考慮は必要だと思いま
す。
あと、「いつでも終了できるようにしておく。」
ことぐらいですね。

「ユーザー インターフェイス スレッド」は、終了処理の関係でシングルスレッドに
とどめておいたほうがいいと思います。

bun 2007/12/20(木) 10:25:31
中間報告です。

問題の再現する最低条件を追求調査しました。
  CProgressCtrl::SetPos(0)
  CProgressCtrl::SetPos(5)
  CProgressCtrl::SetPos(10)
    :
  CProgressCtrl::SetPos(100)
を1回通り行うだけの場合、
視覚テーマをVistaスタイル(緑色の立体プログレスバー)にしても問題は再現し
ません。ただし、連続して複数回行うと、0〜15ぐらいを往復します。
(例) プログレスバーが m_CtlProgress に Attach()してあるとして、
    for(int nIdx=0; nIdx<=100; nIdx+=5)
    {
        ::Sleep(5);
        m_CtlProgress.SetPos(nIdx);
    }
  は正常動作(0〜100まで表示)します。
  しかし、
    for(int nLoop=0; nLoop<10; nLoop++)
    {
        for(int nIdx=0; nIdx<=100; nIdx+=5)
        {
            ::Sleep(5);
            m_CtlProgress.SetPos(nIdx);
        }
    }
  は、0〜15ぐらいを往復します。
  そして、最後のループ完了後、100になります。

どうやら、描画が追いついていないという線が非常に濃くなりました。

bun 2007/12/20(木) 14:45:08
前回の調査結果により、WM_PAINTによる描画が破棄されている可能性が高いと
判断し、以下のキーワードでネット検索を行いました。
  「Vista 描画 破棄」

その結果、ついに核心に迫る、検索結果にたどり着きました。
> [DWM]ディスプレイのリフレッシュレートを超える画面更新は破棄されるよう
> になった (ティアリング回避のため).

詳細は分かりませんが、Vista(Aero)ではダブルバッファリング(オフスクリーン
描画?)が標準のようです。
おそらく、WM_PAINTを高速に投げると、オフスクリーンに描画されるだけで、実
画面への転送はディスプレイのリフレッシュレートで行われるということかな?
と理解しました。

マルチスレッド化の方もコーディングが完了しましたが、やはり効果はありませ
んでした。どうやら、今回の件とは関係なさそうです。

こうなってくると解決方法は、いかの2つぐらいしか思いつきません。
1) 高速描画をやめる
  WM_PAINTを高速に投げ続ける、つまりCProgressCtrl::SetPos()を呼びまくる
  と描画落ちすると思われる。
  あまりに高速になるときは、CProgressCtrl::SetPos()を呼ぶ回数を減らす。
  ただし、0% と 100% が表示されないと、不自然さを感じるので、呼ぶ回数を
  減  らした結果、SetPos()の値を絞り込む際に 0% と 100% mには優先権を持
  たせる。
2) プログレスバーだけVistaスタイルをやめる
  Vistaスタイルのマニフェストがあたっていても、以下のAPIを使用して、特定
  のコントロールだけVistaスタイルを外すことができるので、プログレスバー
  だけVistaスタイルを外してしまう。

他にも良い方法を思いつく方はいらっしゃいますでしょうか?

よろしくお願いいたします。

bun 2007/12/20(木) 14:47:33
VistaスタイルをやめるAPIを載せるのを忘れました。
  SetWindowTheme()
です。

よろしくお願いいたします。

gak 2007/12/20(木) 15:31:55
このスレを文字列検索したけど、出ていないようなので一応書いとく。
「UpdateWindow() で強制的にプログレスバーの描画を更新してもダメ?」

> 本格的なマルチスレッドプログラミングは初めてで
とあるので念のため書いとくと、基本的にウィンドウが所属するスレッド以外から
ウィンドウ操作をするのは×だよ。
タイミングによっては処理の衝突が起こってデッドロックに陥ったり等問題がでる。

bun 2007/12/20(木) 16:52:18
gak様

UpdateWindow(), RedrawWindow() とも駄目でした。

> とあるので念のため書いとくと、基本的にウィンドウが所属するスレッド以外から
> ウィンドウ操作をするのは×だよ。
自分でもそういう結論に達していたのですが、太鼓判を押していただいたおかげで、
自信が持てました。
ありがとうございます。

bun 2007/12/20(木) 22:06:28
[[解決]]
ちょっと?な部分も残っていますが、これにて解決としたいと思います。

FUKU 2007/12/20(木) 22:18:55
今更な感じですが当方でも同じ現象を確認しました。
環境はVista(32bit) VS2005 SP1 MFCです (ダイアログアプリで実験)

この現象の発生条件を整理すると、
・実行ファイルにVistaスタイルのマニフェストを組み込んでいる
・短時間に連続してSetPos()を呼んでいる
の2点のようです。

UpdateWindow(),RedrawWindow(),CWinThread::PumpMessage()等
色々試してみましたが駄目みたいですね。

個人的な憶測ですが、VistaスタイルのSetPos()は、
「指定された位置までのバーを描画する」ではなく
「指定された位置へのバー移動アニメーションを開始する」
に変更されているのではないでしょうか?

例えば、範囲を0-100にして m_Progress.SetPos(100) した場合
XPでは一瞬でバーが(100%の状態に)描画されるのに対して、
Vistaの場合は0.5秒くらい掛けてバーが増えていくのが分かります。
(びよぉ〜んって感じですかね)

とりあえず、XPと同じ視覚テーマなら回避できるようですが...

bun 2007/12/21(金) 08:57:37
FUKU様

解決後に話が続いちゃってますが(^^;)

> 例えば、範囲を0-100にして m_Progress.SetPos(100) した場合
> XPでは一瞬でバーが(100%の状態に)描画されるのに対して、
> Vistaの場合は0.5秒くらい掛けてバーが増えていくのが分かります。
> (びよぉ〜んって感じですかね)
もしかして、プログレスバーの目盛りが戻ることを想定していない?
まさかね。

今回の私のように、
ダブルプログレスバーだと、主バーは目盛りが戻ることはありませんが、
副バーは戻らざるを得ないのですが...

まぁ、根本治療(プログレスバー自体の修正)は無理そうですので、対症
療法で行くことにします。

古いスレッドには追加発言できません。