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

スポンサーサイト

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

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

[Rainmeter-dev] ウィンドウの移動処理

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

0 Comments

ウィンドウの移動処理についてのまとめです。
私的なまとめですので、公開されているビルドに反映されているものではありません。ご注意ください。

■対象となるRainmeterのバージョン
0.14.1 のマルチモニタ対応版 以降 ~ 1.1 (r306)

■更新履歴(※更新のたびに、たぶん一番上にあがってきます)
- 2009.11.07 : Step.1: スキンウィンドウのドラッグ中は位置保存を行わないようにする(フラグ管理の修正)
- 2009.11.07 : Step.2: "On Desktop"なスキンウィンドウを正常にドラッグできないことがある問題を修正する
- 2009.11.10 : Step.3: スキンウィンドウを上方向の画面外にドラッグしたときに、画面内に戻ってこないようにする
- 2009.11.12 : Step.4: ドラッグ可能な状態でもLeftMouseUpActionを実行できるようにする(移動処理の総まとめ)
- 2009.11.22 : ドラッグ中に、スキンウィンドウが元の位置(ドラッグ開始位置)に描画されてしまうことがある問題を修正

※!RainmeterMoveの修正については、マルチモニタ対応の記事に書きました。

以下、続きに格納。

■スキンウィンドウのドラッグ中は位置保存を行わないようにする

Issue 111の記事で書いていた、m_Draggingフラグがtrueを維持したままになる問題の解決編です。
なんでそんな変な状態になるのか、前回の修正ではどうしてダメだったかは前回の記事と合わせてご覧いただければ……。

m_Draggingフラグが何に使われているかというと、ドラッグ終了後の位置保存のためです。
まず、このフラグはマウス左ボタンが押されたときにtrueにセットされ、左ボタンが離されたときにfalseにセットされます(たぶん、最初に組み込んだ人の想定では、そうなるはずでした)。ドラッグ終了時にOnMove関数が呼ばれ、m_SavePositonとm_Draggingがどちらもtrueであれば、位置を保存する仕組みになっています。

実際にイベントが起こると思われる順に並べてみると、WM_NCLBUTTONDOWN(左ボタンDOWNでドラッグ開始) → (ドラッグ中) → WM_NCLBUTTONUP(左ボタンUPでドラッグ終了) → WM_MOVE(OnMove関数で現在の座標を保存) ……と、ずいぶん簡略化しましたがこういう順番です。

……が、この仕組みでは位置が保存できません。上の順で考えてみると、OnMove関数を処理する前に、m_Draggingフラグがfalseになってしまうからです。でも、実際にやってみると保存されています。何故かといえば、前回の記事に書いたように、WM_NCLBUTTONUPメッセージが飛んでこなくてm_Draggingフラグがfalseにならず、ずっとtrueを維持してしまうからです。
(※WM_NCLBUTTONDOWNメッセージは受け取れて、WM_NCLBOTTONUPメッセージが受け取れない理由は、ドラッグのためにシステムがマウスイベントをキャプチャしてしまうからです。実際にはドラッグ終了時にWM_LBUTTONUPメッセージが飛んできていますが、スキンウィンドウのウィンドウプロシージャには届きません)

そこで、前回はOnMove関数の最後でフラグをリセットするようにしました。そうすることによって、上のイベントの起こる順に照らしても問題がないように思えたのですが、実際には、「ドラッグ中にウィンドウの内容を表示する」を有効にしている環境では問題がありました。よって、この手は使えず、振り出しに戻りました。

ここまでが前回のおさらい(長い)。
挙動としては、位置も保存されるし別に問題ないように思えますが、難点もあります。一度スキンウィンドウをドラッグしたらフラグが立ちっぱなしになるわけで、その状態でウィンドウをドラッグし続けると、ドラッグ中にも関わらず位置が保存され続けてしまいます。しかも結構な頻度です。……結果的には、ドラッグ終了位置が保存されるので動作自体に支障はないのですが、やっぱりフラグはtrue/falseをしっかりしておいたほうが(後々の拡張を考えると)いいし、位置の保存もドラッグ終了後に一度だけ処理するようにしたほうがスマートです。

ドラッグ開始時にtrue、終了時にfalseにしたいので、その目的で用意されているメッセージを探してみると、WM_ENTERSIZEMOVE / WM_EXITSIZEMOVEがありました。
メッセージの流れは大雑把に下のようになります。(*)の部分は「ドラッグ中にウィンドウの内容を表示する」の有効無効でループする部分が変わります(無効なら破線、有効なら実線のループ)。

WM_ENTERSIZEMOVE と WM_EXITSIZEMOVE を使う

ドラッグ開始/終了時の処理はそれぞれWM_ENTERSIZEMOVE / WM_EXITSIZEMOVEに任せるので、WM_MOVE(OnMove関数)では座標を保持するだけにします。座標が変わった時に呼ばれるので、m_ScreenX/Yには最新の座標が保持できているはずです。

/*
** OnMove
**
** Stores the new place of the window
**
*/
LRESULT CMeterWindow::OnMove(WPARAM wParam, LPARAM lParam) 
{
    // Store the new window position
    m_ScreenX = (SHORT)LOWORD(lParam);
    m_ScreenY = (SHORT)HIWORD(lParam);
 
    return 0;
}

※OnMove関数は、!RainmeterMoveを使ったときにも呼ばれます(ドラッグではないので、WM_WINDOWPOSCHANGINGとWM_MOVEだけ)。

肝心のWM_ENTERSIZEMOVE(OnEnterSizeMove関数)と、WM_EXITSIZEMOVE(OnExitSizeMove関数)は……

/*
** OnEnterSizeMove
**
** Starts dragging and stores the old place of the window.
**
*/
LRESULT CMeterWindow::OnEnterSizeMove(WPARAM wParam, LPARAM lParam) 
{
    DebugLog(L"*Dragging: ENTER");
    m_Dragging = true;
 
    return 0;
}
/*
** OnExitSizeMove
**
** Ends dragging and writes the new place of the window to config file.
**
*/
LRESULT CMeterWindow::OnExitSizeMove(WPARAM wParam, LPARAM lParam) 
{
    if (m_SavePosition)
    {
        DebugLog(L"OnExitSizeMove: WriteConfig()");
        WriteConfig();
    }
 
    DebugLog(L"*Dragging: EXIT");
    m_Dragging = false;
 
    return 0;
}

フラグの管理をこの2つの関数で行い、ファイルへの保存もここに移しています。これで移動完了時にのみ保存が行われるようになりました。
(ここではフラグを全然使ってませんが、ドラッグ中の判定が必要な別の用途で使います)

* * *

■"On Desktop"なスキンウィンドウを正常にドラッグできないことがある問題を修正する

マルチモニタ環境でスキンウィンドウのドラッグをテストしているうちに、変な挙動に出会いました。
ドラッグしようとしてスキンウィンドウをクリックすると、なぜかクリックした位置ではない場所(右下)にズレて表示されてしまいます。ドラッグ中もそのズレを維持したままで、ドラッグを終了してもやはりズレた位置のままで保存されます。そのズレたスキンウィンドウをクリックすると、再び同じだけズレて……といった感じ。
よくよく調べてみると、それが起こるのは、「プライマリモニタの左(上)に別のモニタがある」「スキンウィンドウが"On Desktop"に設定されている」という2つの条件が揃ったときでした。「プライマリモニタの左(上)に別のモニタがある」というのは、具体的にはデスクトップ(仮想スクリーン)の左上端座標が負の値になっている状態です。プライマリモニタの左上端の座標(0,0)を基準にして、デスクトップの左上端座標が(-1024,-768)だったとすると、スキンをクリックした位置から(1024,768)分、右下方向にズレます。

解像度1280x1024のプライマリモニタの左側に1024x768のモニタが存在すると仮定すると、デスクトップ(仮想スクリーン)のサイズは2304x1792となり、その左上端座標は(-1024,0)になります。"On Desktop"でないスキンウィンドウの場合、そのウィンドウはデスクトップに所属するトップレベルウィンドウなので、移動処理もこの座標空間を使って行われます。ですが、"On Desktop"の場合、そのウィンドウは"Progman"の子ウィンドウとして(中途半端に)存在しています。そのせいか、移動処理はその"Progman"のクライアント座標空間で行われます。

"Progman"自体はデスクトップと同じサイズ(2304x1792)、同じ左上端座標(-1024,0)を持っていますが、その左上端の点をクライアント座標として表すとなると、(0,0)です。"Bottom"なウィンドウだと(-1024,0)に存在するのに、"On Desktop"なウィンドウだと(0,0)に存在すると認識されてしまい、その座標が中途半端にトップレベルウィンドウと同様にスクリーン座標として処理されてしまって、ズレてしまうという、そんな感じです。デスクトップ左上端が(0,0)だと、クライアント座標の左上端も(0,0)と変わらないので、表面化しません。

座標がズレて届くのはWM_WINDOWPOSCHANGING(OnWindowPosChanging関数)だけっぽいので、移動関連処理もその中にあるし、そこで補正を入れてズレを回避するようにします。

    if ((wp->flags & SWP_NOMOVE) == 0) 
    {
        if (m_Dragging)
        {
            if (m_WindowZPosition == ZPOSITION_ONDESKTOP)
            {
                // Correct the Progman's client coordinates to the virtual screen coordinates
                POINT pos = {wp->x, wp->y};
                if (ClientToScreen(GetAncestor(m_Window, GA_PARENT), &pos))
                {
                    wp->x = pos.x;
                    wp->y = pos.y;
                }
                else
                {
                    // Suppose that the top-left coordinates of the virtual screen are (0,0) of the Progman's client coordinates
                    wp->x += m_Monitors.vsL;
                    wp->y += m_Monitors.vsT;
                }
            }
        }

"On Desktop"の場合、取得した座標をクライアント座標だと仮定して、ClientToScreen APIを使い、クライアント座標からスクリーン座標へと変換してやります。
(関数が失敗したら、単純に足しこむことで変換しています)

まずこの変換処理を入れてやることで、この後に続くスナップ処理などもスクリーン座標で行えます。MonitorFromWindowを使うとズレた位置のままモニタのハンドルを返してしまうので、修正後の座標を元にMonitorFromRectを使って取得するように変更します。

        if (m_SnapEdges && !(GetKeyState(VK_CONTROL) & 0x8000 || GetKeyState(VK_SHIFT) & 0x8000))
        {
            // only process movement (ignore anything without winpos values)
            if(wp->cx != 0 && wp->cy != 0)
            {
                RECT workArea;
                SystemParametersInfo(SPI_GETWORKAREA, 0, &workArea, 0);  // gets the work area of the primary screen
 
                //HMONITOR hMonitor = MonitorFromWindow(m_Window, MONITOR_DEFAULTTONULL);  // returns incorrect monitor when the window is "On Desktop"
                RECT windowRect = {wp->x, wp->y, (wp->x + m_WindowW), (wp->y + m_WindowH)};
                HMONITOR hMonitor = MonitorFromRect(&windowRect, MONITOR_DEFAULTTONULL);

本来なら、これらの処理を加えた後に再びクライアント座標に戻してやって、他の関数内の処理でもクライアント座標→スクリーン座標の変換処理をしてやって……という作業が必要なのかもしれませんが、どうにもうまいこと纏まらずズレる結果に終わったので、ここにスクリーン座標へと変換する処理を加えるだけになってしまいました。そのせいなのかどうかわかりませんが、Windows 7(on VirtualBox)で動作チェックすると、ドラッグしたときにスキンウィンドウが点滅しちゃうのですが、移動処理自体はなんとかうまく動いてるようです("Progman"の子ウィンドウとして存在してるので、再描画がうまくいかない?)。
この点滅は、下のようにWM_MOVE(OnMove関数)でウィンドウを再描画してやると、若干軽減できるようです。

/*
** OnMove
**
** Stores the new place of the window
**
*/
LRESULT CMeterWindow::OnMove(WPARAM wParam, LPARAM lParam) 
{
    // Store the new window position
    m_ScreenX = (SHORT)LOWORD(lParam);
    m_ScreenY = (SHORT)HIWORD(lParam);
 
    // Redraw itself if the window is "On Desktop"
    if (m_WindowZPosition == ZPOSITION_ONDESKTOP)
    {
        UpdateTransparency(m_TransparencyValue, false);
    }
 
    return 0;
}

 

* * *

同じ条件で、もうひとつ別の問題があり、Zオーダーを"On Desktop"から何かにしたり、何かから"On Desktop"にしたときに、表示位置が勝手に補正されてズレてしまいます。これはChangeZPos関数内で親ウィンドウを変更する際に使っているSetParent APIのせいで、これがWM_WINDOWPOSCHANGINGメッセージを送ってしまうらしく、表示位置が勝手に(クライアント座標をスクリーン座標と勘違いして or その逆に)補正されてズレてしまいます。この必要ない移動を抑制するために、SetParentから呼ばれたことを示すフラグを新たに設けて、移動するのを防ぐことにします。

        case ZPOSITION_ONDESKTOP:
            // Set the window's parent to progman, so it stays always on desktop
            HWND ProgmanHwnd = FindWindow(L"Progman", L"Program Manager");
            if (ProgmanHwnd && (parent != ProgmanHwnd))
            {
                m_PreventMoving = true;  // Prevent moving the window by SetParent
                SetParent(m_Window, ProgmanHwnd);
            }
            else
            {
                return;        // The window is already on desktop
            }
            break;
        }
 
        if (zPos != ZPOSITION_ONDESKTOP && (parent != GetDesktopWindow()))
        {
            m_PreventMoving = true;  // Prevent moving the window by SetParent
            SetParent(m_Window, NULL);
        }

ChangeZPos関数でSetParent APIを呼び出す前に、フラグを立てておきます。

    if ((wp->flags & SWP_NOMOVE) == 0) 
    {
        if (m_PreventMoving)
        {
            wp->flags |= SWP_NOMOVE;
            m_PreventMoving = false;
 
            return DefWindowProc(m_Window, m_Message, wParam, lParam);
        }
 
        if (m_Dragging)
        {

OnWindowPosChanging関数では、フラグが立っていたら移動しないようにSWP_NOMOVEフラグを追加して、処理を終えます。これで移動は抑制されるはずです。

* * *

■スキンウィンドウを上方向の画面外にドラッグしたときに、画面内に戻ってこないようにする

このタイトルだと「???」って感じですが、例えば、メモ帳か何か適当なウィンドウのタイトルバーを右クリックして出てくるメニューから「移動」を選び、移動モードになったら上カーソルキーを押して画面外に持っていってください。移動自体はできますが、エンターキーを押すなどして移動先を確定させると、恐らくタイトルバー分程度だけ画面外に残って、残りの領域は画面内へと戻されてくるはずです。この振る舞いはマウスでドラッグしたときか、移動メニューを使って移動させたときに起こります。

Rainmeterのウィンドウも同様に、(1.0時代の巨大なEnigma homeスキンで試してみるとよくわかりますが、)同じように画面内へと戻されてきます。これは上方向の場合にだけ起こって、左方向、右方向、下方向では起こりません。たぶんWindowsが位置確定の際に、ドラッグ可能なようにタイトルバー部分をほんの少し画面内に残そうとしてるのかなと思うのですが、Rainmeterにとってはいらぬお節介になることもあります。
(ちなみにこれは"On Desktop"なスキンでは起こりません。内部では"Progman"のクライアント領域を移動してるだけだからかもしれません)

具体的に考えます。Issue 115の話にも少し出てきますが(Winspectorによるログで、"where did that -15 came from?"なところが実際に戻されてるところ)、これは別にY座標が負になるから戻されているわけではありません(それは単に設定ファイルから負の座標を読み込めないために起こっていた問題)。じゃあどういうとき?というと、デスクトップ(仮想スクリーン)の上端を越えていったときに戻されます。大抵の環境では、この上端はY=0になっていると思いますが、プライマリモニタの上側にモニタを配置してデスクトップの上端を負の値にしてみれば、Y=0を越えてずっと上まで持っていけるのがわかります。これは0.14でも同様です。

この振る舞いを抑制するために、WM_MOVING(OnMoving関数)を使って移動先のスクリーン座標を取得しておいて、WM_WINDOWPOSCHANGING(OnWindowPosChanging関数)が用意する座標の代わりにその座標値を使うようにします。この補正の仕掛け自体は、ドラッグ終了位置でのSnap/KeepOnScreen処理をしないといけないので、WM_WINDOWPOSCHANGING(OnWindowPosChanging関数)に作ります。

/*
** OnMoving
**
** Tracks the new place of the window
**
*/
LRESULT CMeterWindow::OnMoving(WPARAM wParam, LPARAM lParam) 
{
    if (m_Dragging)
    {
        // Track the new window position
        LPRECT r = (LPRECT)lParam;
        m_DraggingX = r->left;
        m_DraggingY = r->top;
    }
 
    return TRUE;
}

OnMoving関数では、新たに用意したm_DraggingX/Yにドラッグ移動先の座標を格納します。このOnMoving関数はWM_WINDOWPOSCHANGING / WM_MOVEとは違い、「ドラッグ中にウィンドウの内容を表示する」の有効無効に関わらず、ウィンドウをドラッグすると繰り返し送られてきます。また、スクリーン座標として取得できるので、"On Desktop"かそうでないかで処理を分ける必要もなさそうです(クライアント→スクリーン座標変換の処理も、この座標を使うことで置き換えられそう)。

    if ((wp->flags & SWP_NOMOVE) == 0) 
    {
        /* snip */
 
        if (m_Dragging)
        {
            if (GetKeyState(VK_ESCAPE) & 0x8000)  // canceled
            {
                // Restore the old window position
                wp->x = m_DraggingStartX;
                wp->y = m_DraggingStartY;
 
                return DefWindowProc(m_Window, m_Message, wParam, lParam);
            }
 
            // This prevents pushing the window back into the screen when the Y-coordinate of the window locates beyond the top of the virtual screen.
            // In addition, in case of "On Desktop", this corrects the Progman's client coordinates to the virtual screen coordinates.
            wp->x = m_DraggingX;
            wp->y = m_DraggingY;
        }
 
        if (m_SnapEdges && !(GetKeyState(VK_CONTROL) & 0x8000 || GetKeyState(VK_SHIFT) & 0x8000))
        {

OnWindowPosChanging関数での仕掛けです。ドラッグ中のみエスケープキーの状態を確認して、ドラッグの中止を検知します。
エスケープキーが押されていないときは、WM_MOVING(OnMoving関数)で取得した座標にすり替えて後続の処理をします。"On Desktop"のときの座標変換処理も、この座標にすり替えてやることで(たぶん)必要なくなりました。
エスケープキーが押されている(中止する)ときは、あらかじめWM_ENTERSIZEMOVE(OnEnterSizeMove関数)で保持したドラッグ開始位置にすり替えてやります(後続の処理をしてもいいけれど、元々あった位置に戻す処理なので、特に何もせずに処理を戻しています)。それを反映させたOnEnterSizeMove関数は下の通り。代入処理が増えただけです。

/*
** OnEnterSizeMove
**
** Starts dragging and stores the old place of the window.
**
*/
LRESULT CMeterWindow::OnEnterSizeMove(WPARAM wParam, LPARAM lParam) 
{
    DebugLog(L"*Dragging: ENTER");
    // Store the window position for drag canceled
    m_DraggingStartX = m_ScreenX;
    m_DraggingStartY = m_ScreenY;
    m_Dragging = true;
 
    return 0;
}

これで押し戻される振る舞いは抑制できるようになりましたが、ドラッグ終了/中止の検知をエスケープキーの状態を調べて……などという、本来ならWindowsが暗黙のうちにやってくれるような作業をアプリ側で組んでしまうと、意図しない振る舞いを生むことになるので、出来る限り避けたいところです。今回入れた仕組みではエスケープキーだけしか見ていませんが、中止理由としてはそれ以外にも「Windowsキー(またはアプリケーションキー)が押された」「Alt+Tabが押された」「メッセージボックスが表示された」など、いろんなものがあります。それら全てを網羅するのは面倒だし(検知方法が別々に分かれているし、今後のWindowsの仕様変更にも対応できない)、何より非生産的です。

現時点ではエスケープキーでの中止にしか対応していないため、それ以外の中止が起きると、元の場所には戻らず、中止されたときのドラッグ位置に移動したという扱いになります。

* * *

■ドラッグ可能な状態でもLeftMouseUpActionを実行できるようにする

元々、フラグ管理をどうこうしようとしていた理由のひとつに、「ドラッグ可能なスキンウィンドウでは、LeftMouseUpActionが実行できない」という仕様を取り払ってみようというものがありました。現状でもDragMarginsを設定することによって実行可能な領域を作ったり、スキン自体を工夫してダミーのLeftMouseDownAction→LeftMouseUpActionと繋げて実行させたりすることができますが、こういう設定をしなくても実行できるような仕組みが取れないかどうか、いろいろと試してみました。次のような仕様を考えてみます。

- LeftMouseDownActionが定義されていたら、ドラッグ処理はせず、LeftMouseUpActionも定義されていれば実行する(現状の仕様)
- スキンがドラッグされなかったときは、LeftMouseUpActionが定義されていれば実行する
- スキンがドラッグされたときは、LeftMouseUpActionが定義されていても実行しない

そもそも、LeftMouseUpActionが実行されない理由は、記事の冒頭にも少し書きましたが、マウスの左ボタンを離したときにWM_NCLBUTTONUPが送られてこないためです。

/*
** OnLeftButtonDown
**
** Runs the action when left mouse button is down
**
*/
LRESULT CMeterWindow::OnLeftButtonDown(WPARAM wParam, LPARAM lParam) 
{
    /* snip */
 
    if (redraw)
    {
        Redraw();
    }
    else if(!DoAction(pos.x, pos.y, MOUSE_LMB_DOWN, false))
    {
        // Run the DefWindowProc so the dragging works
        return DefWindowProc(m_Window, m_Message, wParam, lParam);
    }
 
    return 0;
}

OnLeftButtonDown関数では、DoAction関数にてLeftMouseDownActionが実行されなければ、DefWindowProc関数に処理を任せています。それ以外では0を返しています。
0を返すとドラッグ処理は行われず、通常通りWM_(NC)LBUTTONUPが送られ、LeftMouseUpActionが実行されます。もし0を返さずにDefWindowProc関数を処理すると、WM_NCHITTEST(OnNcHitTest関数)の戻り値がHTCAPTIONであれば、システムはドラッグ処理のためにマウスイベントをキャプチャします。このキャプチャはWM_LBUTTONUPが送られてくるまで続き、このWM_LBUTTONUPはウィンドウには届けられません。よって、どうしてもWM_(NC)LBUTTONUPを処理したいなら、WM_EXITSIZEMOVEを代わりに使うか、そこからWM_(NC)LBUTTONUPをpostしてやります。

でも、検知したい条件は「マウスは押下したけれどドラッグはしなかったとき」です。この条件で処理をしたいとなると、そもそもWM_ENTERSIZEMOVE / WM_EXITSIZEMOVEは送られてきませんので、不十分です。そこで、ドラッグによる移動処理の大本といえるWM_SYSCOMMANDのSC_MOVEを新たに処理することにします。

記事冒頭にある遷移図は、このSC_MOVEを処理するうちの一部分です。SC_MOVEを加えると、大雑把にこんな感じになります。

SC_MOVE を使う

最初の遷移図は、DefWindowProcにSC_MOVEを渡したときに起こる処理の流れでした。
マウス左ボタンが押下され、ドラッグ処理のためにDefWindowProcへと渡されると、まずはWM_SYSCOMMANDがwParam=SC_MOVEで送られてきます。通常はこれがそのままDefWindowProcに渡され、マウスの移動が検知されたら、破線の先にあるWM_ENTERSIZEMOVEからWM_EXITSIZEMOVEの処理を実行します。DefWindowProcに渡さなければ、これらは実行されません。

この仕組みを利用して、WM_ENTERSIZEMOVE(OnEnterSizeMove関数)が呼ばれたら、ドラッグ処理がなされたことを知らせるためのフラグを立てるようにします。もしドラッグ処理から戻ってきたときにフラグが立っていれば移動処理の後始末をして、立っていなければLeftMouseUpActionを実行することにします。

まずOnEnterSizeMove/OnExitSizeMove関数から。

/*
** OnEnterSizeMove
**
** Starts dragging
**
*/
LRESULT CMeterWindow::OnEnterSizeMove(WPARAM wParam, LPARAM lParam) 
{
    if (m_Dragging)
    {
        DebugLog(L"*Dragging: ENTER");
        m_Dragged = true;  // Don't post WM_NCLBUTTONUP message!
    }
 
    return 0;
}
/*
** OnExitSizeMove
**
** Ends dragging
**
*/
LRESULT CMeterWindow::OnExitSizeMove(WPARAM wParam, LPARAM lParam) 
{
    if (m_Dragging)
    {
        DebugLog(L"*Dragging: EXIT");
    }
 
    return 0;
}

どちらの関数も、新たに作るOnSysCommand関数へ処理の大半を移したので、ドラッグされたことを知らせるためのフラグ(m_Dragged)を立ててやる以外にはすることがありません。本来これらの関数は、SC_MOVE以外にSC_SIZEを処理する時にも呼ばれるため、(Rainmeterでは起こらないはずですが)念のため、SC_MOVEのときにはm_Draggingフラグを立て、それが立っていれば処理をするようにしています。

次に、新しく作るOnSysCommand関数です。

/*
** OnSysCommand
**
** Handle dragging the window
**
*/
LRESULT CMeterWindow::OnSysCommand(WPARAM wParam, LPARAM lParam) 
{
    if ((wParam & 0xFFF0) != SC_MOVE)
    {
        return DefWindowProc(m_Window, m_Message, wParam, lParam);
    }
 
    // --- SC_MOVE ---
 
    // Prepare the dragging flags
    m_DraggingStartX = m_DraggingX = m_ScreenX;
    m_DraggingStartY = m_DraggingY = m_ScreenY;
    m_Dragging = true;
    m_DraggingWithMouse = ((wParam & 0x000F) == 2);  // mouse triggered
    m_Dragged = false;
 
    LRESULT result = DefWindowProc(m_Window, m_Message, wParam, lParam);  // perform SC_MOVE
 
    if (m_Dragged)
    {
        DebugLog(L"SC_MOVE: Move coords: \"%i, %i\" to \"%i, %i\"", m_DraggingStartX, m_DraggingStartY, m_ScreenX, m_ScreenY);
 
        ScreenToWindow();
 
        // Write the new place of the window to config file
        if (m_SavePosition)
        {
            DebugLog(L"SC_MOVE: WriteConfig()");
            WriteConfig();
        }
    }
    else if (m_DraggingWithMouse)
    {
        // Post WM_NCLBUTTONUP message
        PostMessage(m_Window, WM_NCLBUTTONUP, (WPARAM)HTCAPTION, lParam);
    }
 
    // Clear the dragging flags
    m_DraggingStartX = m_DraggingX = 0;
    m_DraggingStartY = m_DraggingY = 0;
    m_Dragging = false;
    m_DraggingWithMouse = false;
    m_Dragged = false;
 
    return result;
}

WM_SYSCOMMANDは、SC_MOVE以外にもいろいろなものが送られてくるので、最初にSC_MOVEかどうかをチェックして、違っていればDefWindowProc送りにしています。また、SC_MOVEにもメニューから発生したものと、マウスのドラッグで発生したものがあるので、事前にそれがマウスで起こったものかどうかをチェックしておきます(Rainmeterではマウス以外は起こらないはずですが、いちおう)。

ドラッグ移動処理のためにDefWindowProcを呼んで、再び処理が戻ってきたときに、m_Draggedが立っていれば、移動先の座標を保存します。立っていなければドラッグ処理が行われていないということなので、LeftMouseUpActionの実行のためにWM_NCLBUTTONUPをpostしています。

これで、他の回避方法をとることなく、振る舞い次第でドラッグをとるかLeftMouseUpActionをとるかを選べるようになりました。

* 2009.11.22 追記 *

まずは前提となる1.1(次は1.2?)の修正の話から……。

r313r315で、特定の条件下でスキンウィンドウを移動させたときに位置が正しく反映されない問題を修正しました。ネタ元はこちら
修正内容は2つあって、1つはマルチモニタ対応のほうにも書いた「WindowToScreen関数が負の座標を0として扱ってしまう」問題への対応で、もう1つは、スキンウィンドウの移動後に、移動先の座標が移動前の座標で上書きされてしまう問題への対応です。

ネタ元のスキンでは、!RainmeterMoveを使ってスキンウィンドウを負のX座標値へと移動させています。その際に呼ばれるMoveWindow関数はこんな感じです。

void CMeterWindow::MoveWindow(int x, int y)
{
    SetWindowPos(m_Window, NULL, x, y, 0, 0, SWP_NOZORDER | SWP_NOSIZE | SWP_NOACTIVATE);
 
    if (m_SavePosition)
    {
        WriteConfig();
    }
}

SetWindowPos APIを使って指定座標へ移動させ、必要であれば移動後の座標をRainmeter.iniへ書き出しています。SetWindowPos APIを使うと、ウィンドウを移動させる際にWM_WINDOWPOSCHANGINGとWM_MOVEを処理してから戻ってくるので、WriteConfig関数で書き出すときにはm_ScreenX/Yはちゃんと移動後の座標になっています。

元々のOnMove関数はこんな感じ(必要ないコメント部分は端折ってあります)。

LRESULT CMeterWindow::OnMove(WPARAM wParam, LPARAM lParam) 
{
    // Store the new window position
    m_ScreenX = (SHORT)LOWORD(lParam);
    m_ScreenY = (SHORT)HIWORD(lParam);
 
    if (m_SavePosition && m_Dragging)
    {
        ScreenToWindow();
        WriteConfig();
    }
 
    return 0;
}

!RainmeterMoveで処理されるときは、m_ScreenX/Yに移動後の座標を代入するだけで、位置の保存はしません。

これだけを見ると別に問題なさそうに思えますが、スキン側で!RainmeterMoveの前に呼んでいる!RainmeterShowMeter/HideMeterのせいで、他の部分の処理内容が少し変わってしまいます。

void CMeterWindow::ShowMeter(const WCHAR* name)
{
    if (name == NULL || wcslen(name) == 0) return;
 
    std::list<CMeter*>::iterator j = m_Meters.begin();
    for( ; j != m_Meters.end(); j++)
    {
        if (wcsicmp((*j)->GetName(), name) == 0)
        {
            (*j)->Show();
            m_ResetRegion = true;    // Need to recalculate the window region
            return;
        }
    }
 
    DebugLog(L"Unable to show the meter %s (there is no such thing in %s)", name, m_SkinName.c_str());
}

例えば!RainmeterShowMeterで呼ばれるShowMeter関数では、指定されたMeterを表示するように設定する以外に、m_ResetRegionをtrueにしています。このフラグが立っていると、次回Updateのタイミング(大抵は!RainmeterMoveでの処理を済ませた直後のUpdateのタイミング)で、スキンウィンドウのサイズを再計算するためのResetWindow関数と、その処理の終わりにWindowToScreen関数が呼ばれます。WindowToScreen関数は、位置を表す文字列m_WindowX/Yを元に実座標m_ScreenX/Yを更新する関数です。この関数が呼ばれることで、まずここで1つ目の問題である座標値0リセットが起こります。ウィンドウも負の座標値から0位置に移動させられます。

1つ目の問題を修正しても、特定の条件下では2つ目の問題が残ります。この問題は、SavePositionが無効な状態で起こります。具体的には、ScreenToWindow関数が処理されないせいです。
ScreenToWindow関数は、m_ScreenX/Yや補助フラグを元に文字列m_WindowX/Yを再作成する関数です。この関数は、SavePositionが有効ならばドラッグ/BANG問わず処理されます(WriteConfig関数内で呼ばれる)が、無効なら処理されず、m_WindowX/Yは古い状態のままになってしまいます。この状態でWindowToScreen関数を処理してしまえば、古い座標値で上書きされてしまうというわけです。

そこで、結局のところ有効無効関係なく処理する必要があるのであれば、OnMove関数のif文中にあるものを外に出してしまえば……ということで、r313ではこんなふうにしてありました。

LRESULT CMeterWindow::OnMove(WPARAM wParam, LPARAM lParam) 
{
    // Store the new window position
    m_ScreenX = (SHORT)LOWORD(lParam);
    m_ScreenY = (SHORT)HIWORD(lParam);
 
    ScreenToWindow();
 
    if (m_SavePosition && m_Dragging)
    {
        WriteConfig();
    }
 
    return 0;
}

ふー、これでいいかなーと思い、動作テストもしてコミットしたのですが……問題アリでした。
OnMove関数はドラッグ/BANGとは別の要因でも呼ばれるので、例えばWindowToScreen関数を通らずマルチモニタ情報が取得できていない状態でScreenToWindow関数が呼ばれる、なんていうことが起きてしまいます(たぶん、Rainmeterを起動して1つ目のウィンドウを作って表示しようとした直後とか)。ScreenToWindow関数はそういう状態を想定してないので、初期値の@1のモニタ情報を見に行って(当然からっぽですが)、変な座標値を表す文字列を作ってしまいます。

if文中に隔離されてたのはそういう理由もあるのかな、ということで、r315で少し変更してコミットしました。

void CMeterWindow::MoveWindow(int x, int y)
{
    SetWindowPos(m_Window, NULL, x, y, 0, 0, SWP_NOZORDER | SWP_NOSIZE | SWP_NOACTIVATE);
 
    ScreenToWindow();
 
    if (m_SavePosition)
    {
        WriteConfig();
    }
}
LRESULT CMeterWindow::OnMove(WPARAM wParam, LPARAM lParam) 
{
    // Store the new window position
    m_ScreenX = (SHORT)LOWORD(lParam);
    m_ScreenY = (SHORT)HIWORD(lParam);
 
    if (m_Dragging)
    {
        ScreenToWindow();
 
        if (m_SavePosition)
        {
            WriteConfig();
        }
    }
 
    return 0;
}

BANGのときはMoveWindow関数の側で処理して、ドラッグのときはOnMove関数の側で処理するように変更して、対応終了。

ちなみに、r315の更新ログで"異常終了する"って書いてしまいましたが、あれは勘違いです。異常終了する!ってわかった(勘違いした)のが、マルチモニタ環境でのテスト用dllを作成していたときで、それにはマルチモニタ対応コードを部分的にマージしてあったので、ScreenToWindow関数が@1のモニタ情報を読みに行ったところで空っぽのvectorの[0]をアクセスして異常終了してしまっていたのでした。元々のコードでは配列なので(そこは確保済み領域)、チェックしてませんがたぶん大丈夫なはずです。

……と、ここまでが前提(長い)。

ドラッグ処理を改善させるために修正したコードでは、OnMove関数からScreenToWindow関数が消えてしまってます。ドラッグ終了後(SC_MOVEのところ)には処理されるのでいいようにも思えてしまうのですが……ドラッグ中にWindowToScreen関数が処理されてしまうような設定もあったので(DynamicWindowSize=1になってると、毎Update時にm_ResetRegionがtrueにされてうんぬん)、やっぱりここにも必要です。

LRESULT CMeterWindow::OnMove(WPARAM wParam, LPARAM lParam) 
{
    // Store the new window position
    m_ScreenX = (SHORT)LOWORD(lParam);
    m_ScreenY = (SHORT)HIWORD(lParam);
 
    if (m_Dragging)
    {
        ScreenToWindow();
    }
 
    // Redraw itself if the window is "On Desktop"
    if (m_WindowZPosition == ZPOSITION_ONDESKTOP)
    {
        UpdateTransparency(m_TransparencyValue, false);
    }
 
    return 0;
}

これで大丈夫なはず。

« デスクトップウィンドウとシェルウィンドウ useLocalTime »

- Comments
0 Comments

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


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

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