Search
Categories
Articles
Rainmeter関連
ファイル置き場
お知り合いなど

スポンサーサイト

--.--.-- | スポンサー広告

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

[Rainmeter-dev] 過剰にRefreshされる問題

2009.11.04 | Rainmeter-dev // Issue/マルチモニタ関連

0 Comments

(※いろんな記事に分散してたのを統合しました)

Rainmeter0.14.1 ~ 1.0で、壁紙や視覚効果の設定を変更すると、解像度変更とは関係ないのにスキンが"Refresh"されてしまう問題についてのまとめです。
本家でのコード変更についてのコメントや、私自身の変更案、コミット予定/済みなものなどが入り混じっていますので、決して公開されているビルドに反映されているものばかりではありません。ご注意ください。

※Rainmeter 1.1ではRefresh処理が省かれたことにより、処理が軽減されてクラッシュする可能性も小さくなりました。

■更新履歴(※更新のたびに、たぶん一番上にあがってきます)
- 2009.09.xx : タイマーを使って遅延処理するように変更(メッセージループの処理を阻害しないようにする)
- 2009.11.04 : タイマー形式を使わず、CTrayWindowでメッセージ検知して全スキンウィンドウを更新するように変更して、一旦終わり

以下、続きに格納。

Rainmeterが1.0になって気になったのは、壁紙設定とか解像度変更と関係ない設定を変えただけでスキンがむやみやたらとRefreshされるところ。
例えば「システムのプロパティ」の視覚効果からマウスカーソルの影の設定を変更しただけで、ログが下のように。Refreshされすぎです。

DEBUG: (00:00:33.656) Refreshing (Name: "SPECTRE_analog" Ini: "AnalogClock.ini")
DEBUG: (00:00:33.703) Refreshing (Name: "Pixel_LA\Main" Ini: "PLA_Main.ini")
DEBUG: (00:00:34.125) Refreshing (Name: "SPECTRE_analog" Ini: "AnalogClock.ini")
DEBUG: (00:00:34.172) Refreshing (Name: "Pixel_LA\Main" Ini: "PLA_Main.ini")
DEBUG: (00:00:34.594) Refreshing (Name: "Pixel_LA\HWMon" Ini: "PLA_HWMon.ini")
DEBUG: (00:00:35.359) Refreshing (Name: "SPECTRE_analog" Ini: "AnalogClock.ini")
DEBUG: (00:00:35.406) Refreshing (Name: "Pixel_LA\Main" Ini: "PLA_Main.ini")
DEBUG: (00:00:35.781) Refreshing (Name: "Pixel_LA\HWMon" Ini: "PLA_HWMon.ini")
DEBUG: (00:00:36.562) Refreshing (Name: "SPECTRE_analog" Ini: "AnalogClock.ini")
DEBUG: (00:00:36.609) Refreshing (Name: "Pixel_LA\Main" Ini: "PLA_Main.ini")
DEBUG: (00:00:36.953) Refreshing (Name: "Pixel_LA\HWMon" Ini: "PLA_HWMon.ini")
DEBUG: (00:00:37.687) Refreshing (Name: "Pixel_LA\Drive" Ini: "PLA_Drive.ini")
DEBUG: (00:00:37.828) Refreshing (Name: "Pixel_LA\Drive" Ini: "PLA_Drive.ini")
DEBUG: (00:00:37.953) Refreshing (Name: "Pixel_LA\Drive" Ini: "PLA_Drive.ini")
DEBUG: (00:00:38.078) Refreshing (Name: "Pixel_LA\HWMon" Ini: "PLA_HWMon.ini")
DEBUG: (00:00:38.797) Refreshing (Name: "Pixel_LA\Drive" Ini: "PLA_Drive.ini")
DEBUG: (00:00:38.922) Refreshing (Name: "Pixel_LA\Main" Ini: "PLA_Main.ini")
DEBUG: (00:00:39.266) Refreshing (Name: "Pixel_LA\HWMon" Ini: "PLA_HWMon.ini")
DEBUG: (00:00:40.031) Refreshing (Name: "Pixel_LA\Drive" Ini: "PLA_Drive.ini")
DEBUG: (00:00:40.156) Refreshing (Name: "Pixel_LA\Main" Ini: "PLA_Main.ini")
DEBUG: (00:00:40.500) Refreshing (Name: "Pixel_LA\HWMon" Ini: "PLA_HWMon.ini")
DEBUG: (00:00:41.328) Refreshing (Name: "Pixel_LA\Drive" Ini: "PLA_Drive.ini")
DEBUG: (00:00:41.437) Refreshing (Name: "Pixel_LA\Drive" Ini: "PLA_Drive.ini")
DEBUG: (00:00:41.562) Refreshing (Name: "Pixel_LA\HWMon" Ini: "PLA_HWMon.ini")
DEBUG: (00:00:42.406) Refreshing (Name: "Pixel_LA\Drive" Ini: "PLA_Drive.ini")
DEBUG: (00:00:42.547) Refreshing (Name: "Pixel_LA\Main" Ini: "PLA_Main.ini")
DEBUG: (00:00:42.969) Refreshing (Name: "Pixel_LA\HWMon" Ini: "PLA_HWMon.ini")
DEBUG: (00:00:43.812) Refreshing (Name: "Pixel_LA\Drive" Ini: "PLA_Drive.ini")
DEBUG: (00:00:43.953) Refreshing (Name: "Pixel_LA\Main" Ini: "PLA_Main.ini")
DEBUG: (00:00:44.312) Refreshing (Name: "Pixel_LA\HWMon" Ini: "PLA_HWMon.ini")
DEBUG: (00:00:45.172) Refreshing (Name: "Pixel_LA\Drive" Ini: "PLA_Drive.ini")
DEBUG: (00:00:45.297) Refreshing (Name: "Pixel_LA\Drive" Ini: "PLA_Drive.ini")

本来は解像度の変更やマルチモニタ関連の設定が変更されたら処理したいんだろうと思いますが、現状ではWM_SETTINGCHANGEが送られてきたら、その送られてきた内容に関わらず処理をするようになっています。WM_SETTINGCHANGEはシステム関連の設定が変更されたときに送られてくるものなので、いろんなものが送られてきます(時には一気に)。どういう設定が変更されたかは、wParam(またはlParam)を見ればわかります。そのあたりはWM_SETTINGCHANGESystemParametersInfo APIについて調べると詳しく書かれています。

該当箇所はMeterWindow.cppのOnSettingChange()です。

/*
** OnSettingChange
**
** Called when resolution changes
**
*/
LRESULT CMeterWindow::OnSettingChange(WPARAM wParam, LPARAM lParam) 
{
    m_Monitors.count = 0;
    Refresh(false);
 
    // Commented: Calling DefWindowProc seems to cause crash sometimes
    return 0;  // DefWindowProc(m_Window, m_Message, wParam, lParam);
}

コメントでは「解像度が変更されたら呼ばれる」とされているものの、実は解像度を変更しても呼ばれないこともあります(画面のプロパティから変更すると呼ばれるが、解像度変更を伴うフルスクリーンゲームでは呼ばれなかったり)。

少なくともうちの環境では、このWM_SETTINGCHANGEでは純粋に解像度変更だけに絞ってキャッチすることができませんでした。解像度と関係ありそうなSPI_SETWORKAREAあたりが送られてくるかなーと思いきや、全く送られてこないし。
別の方法を探してみると、WM_DISPLAYCHANGEなんていうズバリそのものがあるじゃあないですか。それに書き換えてみたら、解像度変更のみに反応してくれるようになりました。他の設定で誤爆しないので快適。
とはいえ、元々WM_SETTINGCHANGEを使っていた理由がわからないし、もし他の設定変更にも反応させる必要があるなら、wParamとlParamの中身を見て処理するかしないかを決めるようにしたほうがいいんじゃないかなと思います。

<追記1>
上記では解像度変更の検出をWM_SETTINGCHANGEからWM_DISPLAYCHANGEを使うように変更しましたが、作業領域の変更の検出にも対応できるようにしてみました。作業領域の変更通知はタスクバーを動かしたり、サイズをいじったりすると飛んできます。

まず、OnSettingChange()と同様にOnDisplayChange()を作って、WM_DISPLAYCHANGEが飛んできたときの処理をするようにします(MeterWindow.hにも宣言を記述)。

/* 
** WndProc
**
** The window procedure for the Meter
**
*/
LRESULT CALLBACK CMeterWindow::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    /* snip */
 
    BEGIN_MESSAGEPROC
    /* snip */
    MESSAGE(OnSettingChange, WM_SETTINGCHANGE)
    MESSAGE(OnDisplayChange, WM_DISPLAYCHANGE)
    END_MESSAGEPROC
}

OnSettingChange()とOnDisplayChange()の中身は下記の通り(Refresh()の呼び方は前回とは違ってr195準拠)。

/*
** OnSettingChange
**
** Called when workarea changes
**
*/
LRESULT CMeterWindow::OnSettingChange(WPARAM wParam, LPARAM lParam) 
{
    if (wParam == SPI_SETWORKAREA)
    {
        DebugLog(L"* OnSettingChange(SPI_SETWORKAREA): Skin=\"%s\"", GetSkinName().c_str());
 
        m_Monitors.count = 0;
        PostMessage(m_Window, WM_DELAYED_REFRESH, (WPARAM)NULL, (LPARAM)NULL);
    }
 
    return 0;
}
 
/*
** OnDisplayChange
**
** Called when resolution changes
**
*/
LRESULT CMeterWindow::OnDisplayChange(WPARAM wParam, LPARAM lParam) 
{
    DebugLog(L"* OnDisplayChange(): Skin=\"%s\"", GetSkinName().c_str());
 
    m_Monitors.count = 0;
    PostMessage(m_Window, WM_DELAYED_REFRESH, (WPARAM)NULL, (LPARAM)NULL);
 
    return 0;
}

解像度変更はOnDisplayChange()で処理して、作業領域変更はOnSettingChange()で処理します。
OnSettingChange()はそのままだといろんなものの変更通知が飛んでくるので、wParamを見てSPI_SETWORKAREAなら処理するようにしてやります。

うちの環境では下のようにログ出力されました。

※解像度を変更した
DEBUG: (00:00:12.468) * OnDisplayChange(): Skin="SPECTRE_analog"
DEBUG: (00:00:12.468) Refreshing (Name: "SPECTRE_analog" Ini: "AnalogClock.ini")
DEBUG: (00:00:12.859) * OnDisplayChange(): Skin="Pixel_LA\Main"
DEBUG: (00:00:12.875) Refreshing (Name: "Pixel_LA\Main" Ini: "PLA_Main.ini")
DEBUG: (00:00:13.000) * OnDisplayChange(): Skin="Pixel_LA\HWMon"
DEBUG: (00:00:13.000) Refreshing (Name: "Pixel_LA\HWMon" Ini: "PLA_HWMon.ini")
DEBUG: (00:00:13.562) * OnDisplayChange(): Skin="Pixel_LA\Drive"
DEBUG: (00:00:13.562) Refreshing (Name: "Pixel_LA\Drive" Ini: "PLA_Drive.ini")

※タスクバーを固定してたので、それを外した
DEBUG: (00:01:22.796) * OnSettingChange(SPI_SETWORKAREA): Skin="SPECTRE_analog"
DEBUG: (00:01:22.796) Refreshing (Name: "SPECTRE_analog" Ini: "AnalogClock.ini")
DEBUG: (00:01:22.843) * OnSettingChange(SPI_SETWORKAREA): Skin="Pixel_LA\Main"
DEBUG: (00:01:22.843) Refreshing (Name: "Pixel_LA\Main" Ini: "PLA_Main.ini")
DEBUG: (00:01:22.984) * OnSettingChange(SPI_SETWORKAREA): Skin="Pixel_LA\HWMon"
DEBUG: (00:01:22.984) Refreshing (Name: "Pixel_LA\HWMon" Ini: "PLA_HWMon.ini")
DEBUG: (00:01:23.562) * OnSettingChange(SPI_SETWORKAREA): Skin="Pixel_LA\Drive"
DEBUG: (00:01:23.562) Refreshing (Name: "Pixel_LA\Drive" Ini: "PLA_Drive.ini")

※タスクバーのサイズを変えた
DEBUG: (00:03:09.109) * OnSettingChange(SPI_SETWORKAREA): Skin="SPECTRE_analog"
DEBUG: (00:03:09.109) Refreshing (Name: "SPECTRE_analog" Ini: "AnalogClock.ini")
DEBUG: (00:03:09.156) * OnSettingChange(SPI_SETWORKAREA): Skin="Pixel_LA\Main"
DEBUG: (00:03:09.156) Refreshing (Name: "Pixel_LA\Main" Ini: "PLA_Main.ini")
DEBUG: (00:03:09.312) * OnSettingChange(SPI_SETWORKAREA): Skin="Pixel_LA\HWMon"
DEBUG: (00:03:09.312) Refreshing (Name: "Pixel_LA\HWMon" Ini: "PLA_HWMon.ini")
DEBUG: (00:03:09.906) * OnSettingChange(SPI_SETWORKAREA): Skin="Pixel_LA\Drive"
DEBUG: (00:03:09.921) Refreshing (Name: "Pixel_LA\Drive" Ini: "PLA_Drive.ini")

マルチモニタの設定変更は環境がないので、NVIDIAのテレビへのクローン出力オンオフと、テレビ側解像度変更だけ試してみましたが、どちらもOnDisplayChange()が呼ばれていました。
(その後、メインモニタ+テレビでの水平スパンも試してみましたが、こちらでもOnDisplayChange()が呼ばれました)

あとは、必要であればOnSettingChange()のwParamを見て補足する対象を増やしていけばいいかなと。

<追記2>
元々はマルチモニタの設定を再取得するためにOnSettingChange()を使っていたみたいだから、作業領域の変更には反応せずOnDisplayChange()だけに任せたほうがいいのかも……。
ああ、でもタスクバー動かしたりして作業領域のサイズや始点が変わると、場合によってはスキンが隠れちゃったりするから動かす必要があるのかな。で、それに必要な下の変数を取得しなおしてスキンを移動させるにはRefresh()させたほうが楽と……。やっぱりあったほうがいいのかもしれない。

#WORKAREAX# is the X-position of the work area.
#WORKAREAY# is the Y-position of the work area.
#WORKAREAWIDTH# is the width of the work area.
#WORKAREAHEIGHT# is the height of the work area.

あとは……作業領域の変更でマルチモニタの設定をリセットする必要はなさそうかなぁ。マルチモニタの設定が変わるとたぶんOnDisplayChange()のほうが呼ばれるだろうし。

* 2009.09.xx 追記 *

どうやら、タスクバーを動かしたりして作業領域が変更されたときに飛んでくるWM_SETTINGCHANGE(SPI_SETWORKAREA)が、場合によってはちゃんと送られてこないままになってしまうぽい。送信側がタイムアウトと判定して途中で送信をやめちゃうような感じ。

結構負荷のかかるEnigmaスキンを使ってテスト。
OnSettingChange()内の処理コード2行をコメントアウトして、何も処理せず素通しさせてみると全部のウィンドウ分届き、処理コード部分をSleep(300)とかに置き換えてみるとウィンドウ3つ分くらいしか届かない。
WM_SETTINGCHANGEはポストメッセージキューよりも優先される送信メッセージキューに届いてるみたいだけど、なぜかRefresh用のPostMessage()のほうが優先されて実行されてしまうことが多いので、そこの処理に時間がかかると(特にEnigma Home(設定用のやつ)のような巨大なスキンをRefreshすると)、次のGetMessage()まで待てずにタイムアウトしてしまうのかもしれない(ここの仕組みがよくわからないので、勉強しないと……)。

元々、PostMessage()も別の目的で遅らせてRefreshさせるために変更されたものだけれど、これでも受信を阻害してしまうなら別の遅らせる方法を……ということでタイマー化が現実的なのかな。Refresh処理をアバウトに500ms程度遅らせて実行されるように(その500msの間に更新タイマーやマウスタイマーのような定期処理と、WM_SETTINGCHANGEの受信をするように)変更してみます。まだまだいろいろと考える余地あり。

/*
** OnSettingChange
**
** Called when workarea changes
**
*/
LRESULT CMeterWindow::OnSettingChange(WPARAM wParam, LPARAM lParam) 
{
    if (wParam == SPI_SETWORKAREA)
    {
        DebugLog(L"* OnSettingChange: Name=\"%s\"", GetSkinName().c_str());
 
        // Start the timer
        if(0 == SetTimer(m_Window, DELAYEDTIMER, 500, NULL))
        {
            PostMessage(m_Window, WM_DELAYED_REFRESH, (WPARAM)NULL, (LPARAM)NULL);
        }
 
        return 0;
    }
 
    return DefWindowProc(m_Window, m_Message, wParam, lParam);
}

新しくDELAYEDTIMERを定義して、タイマーを設定します(設定に失敗したら、今まで通りPostMessage()で済ます)。
タイマー処理は、他のタイマー同様、OnTimer()に追加します。

    else if(wParam == DELAYEDTIMER)
    {
        KillTimer(m_Window, DELAYEDTIMER);
        OnDelayedRefresh((WPARAM)NULL, (LPARAM)NULL);
    }

500ms経ってDELAYEDTIMERの処理が呼ばれたら、もうタイマーは必要ないので設定を解除します。その後、OnDelayedRefresh()を呼び出してRefreshを実行します。

***

とはいえ、「作業領域が変更されるたびにRefreshされる」ということは、タスクバーを自動開閉にしてる人はたぶん何度も何度もRefreshされるんですよね。そんな振る舞いはいただけないので、やはり単純に配置変更のみを行うような仕組みにしないといけなそうです。
r220では、今まで上記していたようなやり方ではなく、Refreshの代わりに(たぶん)移動とリサイズだけをするようにコードが置き換えられましたが、手元で試したら想像したようには動いてなさそうでした(現時点では、#VARIABLE#な変数がRefresh以外で変わることを想定しておらず、前回RefreshまたはUpdateしたタイミングでの計算済みX/Y/W/Hしか持っていないので)。

この部分を修正するためには、マルチモニタの知識や座標変換についていろいろと勉強しないといけないので、今のところは何も手をつけられなそうです。
とりあえずはメッセージ処理とマルチモニタ関連の勉強をしてきます……。まだまだ知識が足りない。

***

現時点では、r220でのRainyの変更で、実質的に過剰なRefreshは抑えられているので、もしさらに必要そうな変更があれば、1.1が正式リリースされた後に提案してみたいと思います。

変更するとすれば、WM_SETTINGCHANGEではSPI_SETWORKAREAの場合だけに絞るのと、後は影響のある#VARIABLE#の再読み込みと、それに伴って式の結果が変わるようなWindowX/WindowYなどの再読み込み&移動などでしょうか。
とはいえ、「別にそんなことまでしなくても、手動で"Refresh All"すれば適正な位置やサイズに戻るよね」といえばその通りなので、無理に変更する必要もないんだろうなという状況です。

***

作業領域が変更されたときは、

- 各モニタにおける作業領域が置き換わる
- 仮想領域(全部のモニタを繋げた領域)が置き換わる

と思われるので、追記2と3ではOnSettingChange()からマルチモニタ設定をリセットする処理を省いちゃったけれど、入れておいた方が無難かもしれない。
リセットしたからといって、特別に負荷が跳ね上がるとか、挙動がおかしくなるといったことはない……と思われる。
(r220では処理が入っているので、そのまま)

* 2009.11.04 追記 *

各スキンウィンドウがそれぞれにWM_***CHANGEメッセージを検知して処理する方式だと、マルチモニタ情報のリセットがそのスキンウィンドウの数分行われてしまうため、とっても非効率です。1回行われたらそれ以外では行わないようにしたいところですが、スキンウィンドウ自体でそれを実装するのは面倒です。そこで、常に非表示ながらトップレベルウィンドウを持つCTrayWindowでメッセージを検知し、全スキンウィンドウに対して処理をするようにします。
(各スキンウィンドウでの検知はやめて、移動処理用の関数を用意するだけにする)

WM_DISPLAYCHANGE / WM_SETTINGCHANGEの検知は、CTrayWindow::WndProcで行います。

    case WM_DISPLAYCHANGE:
    case WM_SETTINGCHANGE:
        if (uMsg == WM_DISPLAYCHANGE || wParam == SPI_SETWORKAREA)  // WM_DISPLAYCHANGE || (WM_SETTINGCHANGE && SPI_SETWORKAREA)
        {
            CMeterWindow::ResetMultiMonitorInfo();  // Reset
 
            // Deliver WM_DISPLAYCHANGE / WM_SETTINGCHANGE message to all meter windows
            std::map<std::wstring, CMeterWindow*>& windows = Rainmeter->GetAllMeterWindows();
            std::map<std::wstring, CMeterWindow*>::iterator iter = windows.begin();
            for( ; iter != windows.end(); iter++)
            {
                PostMessage((*iter).second->GetWindow(), WM_DELAYED_MOVE, (WPARAM)uMsg, (LPARAM)0);
            }
        }
        return 0;

※マルチモニタ情報のリセットは新しい方式に変更してあります。

メッセージを検知したら、マルチモニタの情報をリセットしてから、全スキンウィンドウに対し移動処理をするようメッセージをポストします(直接呼ぶよりはこっちのほうがメッセージループを阻害しなくていい?)。

CMeterWindowには、そのメッセージに対応する関数を定義しておきます。

/*
** OnDelayedMove
**
** Handles delayed move
**
*/
LRESULT CMeterWindow::OnDelayedMove(WPARAM wParam, LPARAM lParam) 
{
    //DebugLog(L"* OnDelayedMove: Name=\"%s\", wp=0x%08X, lp=0x%08X", GetSkinName().c_str(), wParam, lParam);
 
    if (m_NativeTransparency)
    {
        ReadConfig();
 
        // Move the window to correct position
        ResizeWindow(true);
 
        if (m_KeepOnScreen) 
        {
            MapCoordsToScreen(m_ScreenX, m_ScreenY, m_WindowW, m_WindowH);
        }
 
        SetWindowPos(m_Window, NULL, m_ScreenX, m_ScreenY, m_WindowW, m_WindowH, SWP_NOZORDER | SWP_NOACTIVATE);
    }
    else
    {
        // With copy transparency we'll do a full refresh
        PostMessage(m_Window, WM_DELAYED_REFRESH, (WPARAM)NULL, (LPARAM)NULL);
    }
 
    return 0;
}

スキンウィンドウ側の処理は、マルチモニタ情報のリセット処理がなくなったくらいで、他はほぼ変わりません。新しい表示位置を再計算するためにWindowX/Yの再読み込みが必要なので、ReadConfig関数を追加で呼んでいます。ReadSkin関数も呼べると、MeasureやMeterの設定に使われた変数の再計算も行われていいのですが、そこまでやるとRefreshとほとんど変わらない処理になってしまうので、今回はスキンウィンドウの位置を移動させるだけにとどめています。

これで一段落!

« [WDM] Aero Glassを有効にしてみる [Rainmeter] Rainmeter 1.1 正式版がリリースされました! »

- Comments
0 Comments

管理者にだけ表示を許可する
- Trackbacks
0 Trackbacks


この記事にトラックバックする(FC2ブログユーザー)

上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。