土曜日に書いたツールバードッキングで嫌な感じ事件ですが、無事(?)に解決いたしました。
当初は闘う前から負けていた(だってドッキング部分はいろいろなサイトで難解と言われていましたから)のですが、きよのさんの協力もあってやる気が出まして(笑)、駄目元でデバッグをしていったところ、それっぽい原因を見つけ出すことができました。
折角ですので、VC6.0を使ってどうやって問題を解決したかなどを簡単に書き記しておこうと思います。
さてさて、ドッキングしているツールバーをドラッグ&ドロップした際、ドロップした位置より下側にずれてドッキングしていまう、というのが今回の不具合です(詳しくは12日の日記を参照してください)。
なんとなく「ドロップしたあとのツールバー位置を決める処理」に不具合があるんだろうなぁとは予想出来ましたので、その辺をさぐろうと思ったのですが、ソースを見るに当たってまずは大体どんなかんじでドッキングしたツールバーなどを管理しているのか、データ構造を探ることにしました(その方がソース見ていて解りやすくなると思いまして)。
もともとCToolBarやCStatusBarのヘルプを見たときに、それらがCControlBarというクラスから派生していたのを何度か目にしていたため、「きっとメインフレームがCControlBarの一覧を持ってるんだろう」という憶測はしていました。
早速、CMainFrameの親クラスを参照機能でたどって行くとCFrameWndというクラスに
CPtrList m_listControlBars;
という名前がそのままの変数を見つけました。
ためしに一つずつポインタを取得してみてGetWindowRectでウィンドウ座標を取り、その位置に四角形を描画してみたのですが、思ったとおり、全てのツールバーやステータスバーが格納されていました。
しかし実際にはそれだけでなく、謎のクラスCDockBarというクラス(これもCControlBarの派生クラス)が上下左右に1つずつ存在していました。どうやらこのCDockBarというのがツールバーのドッキングを受け入れるバーのようです(受け入れるバーと受け入れられるツールバーが同じリスト内に存在している、というのも不思議な気がしますが)。
#ちなみにCDockBarのコンストラクタにブレークポイントを設定してみたところ、
上下左右の4つだけでなく、ツールバーを浮かせた状態にした際にも生成されるようです。
CDockBarがドッキングを受け入れるバーならば、そのCDockBarがドッキングしているコントロールバーの座標を持っているんじゃないかと思い、さっそくソースを見ていったところ、
CPtrArray m_arrBars; // each element is a CControlBar
という怪しいポインタ配列を見つけたので、例によって一つずつポインタを取得してみてGetWindowRectでウィンドウ座標を取り、その位置に四角形を描画してみましたが、またも思ったとおり、ドッキングされているツールバーなどが格納されていました。
ところが、中にはNULLという情報も含まれており、どういったルールでこの配列の中身が決まっているかが気になりました。
そこで、ツールバーのドッキング状態をいろいろ変えながらm_arrBarsがどのように変化するか試したところ、上の行から順番にコントロールバーのポインタが格納されており、NULLは「コントロールバーの改行」を意味するようでした。
つまり、
↑このような状態では、
m_arrBars[0] = NULL; m_arrBars[1] = ツールバー1のアドレス; m_arrBars[2] = NULL; m_arrBars[3] = ツールバー2のアドレス; m_arrBars[4] = NULL; m_arrBars[5] = ツールバー3のアドレス; m_arrBars[6] = NULL;
このような配列状態になり、
↑このような状態では、
m_arrBars[0] = NULL; m_arrBars[1] = ツールバー1のアドレス; m_arrBars[2] = ツールバー4のアドレス; m_arrBars[3] = NULL; m_arrBars[4] = ツールバー2のアドレス; m_arrBars[5] = NULL; m_arrBars[6] = ツールバー3のアドレス; m_arrBars[7] = NULL;
このような配列状態になりました。
「・・・ということは、ツールバーをドロップした際、この配列への挿入場所を導き出す処理がうまくいかず、こちらが期待する位置にツールバーが配置されず、下の段にドッキングしてしまうのではないか」、というような考えにいきつきました。
データ構造は大体解りましたので(?)ようやくここからドッキング部分のソースを追うことにします。
・・・どうやって追うのか?
10秒ぐらい悩みましたが、ドッキング後には、メインフレームのRecalLayoutが呼び出されると思い、ここにブレークポイントを設置し、実際にツールバーをドラッグ&ドロップしてみたところ、見事にひっかかりました。
そこからコールスタックウィンドウを駆使して呼び出し元を辿り、
CDockContext::EndDrag()という関数の最初の方でまたブレークポイントを設置し、また実行してドラッグ&ドロップして停止させて、今度はステップ実行でひとつずつ怪しいところが無いか見ていきました。
しばらく追っていき、
CDockContext::EndDrag() → CFrameWnd::DockControlBar() → CDockBar::DockControlBar() →CDockBar::Insert() この関数内で先に出た「CPtrArray m_arrBars;」というポインタ配列へバーを挿入している事が判明しました。
というわけでこの関数の先頭にブレークポイントを設定して、実行→ツールバーをドラッグ&ドロップ→ブレークポイントで停止させ、
今度は変数ウォッチに、この関数内で使用している変数を表示させ、変数の変化を見ながらステップ実行していきました。
このCDockBar::Insert()という関数、完璧に理解したわけではありませんが、
配列の先頭(=一番上のコントロールバー)から順番に座標を照らし合わせながら、
ドッキングしたいコントロールバーの「順番(配列番号)」を求め、ポインタを挿入している関数だと思われます。
int CDockBar::Insert(CControlBar* pBarIns, CRect rect, CPoint ptMid) { ASSERT_VALID(this); ASSERT(pBarIns != NULL); int nPos = 0; int nPosInsAfter = 0; int nWidth = 0; int nTotalWidth = 0; BOOL bHorz = m_dwStyle & CBRS_ORIENT_HORZ; for (nPos = 0; nPos < m_arrBars.GetSize(); nPos++) { CControlBar* pBar = GetDockedControlBar(nPos); if (pBar != NULL && pBar->IsVisible()) { CRect rectBar; pBar->GetWindowRect(&rectBar); ScreenToClient(&rectBar); nWidth = max(nWidth, bHorz ? rectBar.Size().cy : rectBar.Size().cx - 1); if (bHorz ? rect.left > rectBar.left : rect.top > rectBar.top) nPosInsAfter = nPos; } else // end of row because pBar == NULL { nTotalWidth += nWidth - afxData.cyBorder2; nWidth = 0; if ((bHorz ? ptMid.y : ptMid.x) < nTotalWidth) { if (nPos == 0) // first section m_arrBars.InsertAt(nPosInsAfter+1, (CObject*)NULL); m_arrBars.InsertAt(nPosInsAfter+1, pBarIns); return nPosInsAfter+1; } nPosInsAfter = nPos; } } // create a new row m_arrBars.InsertAt(nPosInsAfter+1, (CObject*)NULL); m_arrBars.InsertAt(nPosInsAfter+1, pBarIns); return nPosInsAfter+1; }
この関数でどんな処理が行われているか、私の解釈で簡単に説明しますと・・・
コントロールバーというのは種類によってバーの高さが異なりますので、行の中で一番高いサイズをnWidthという変数で記憶しておいて、改行(=NULL)または表示されていないバーが来たら「高さの合計」を示すnTotalWidth変数にnWidthを足す、というような処理が入っています。
この高さ合計と、ドッキングしたい場所のY座標(中央)を比較し、条件が合えばその位置にバーを挿入、というような感じです。
#垂直状態のドッキングも考慮しているとはいえ、何故「nWidth」という名前なのかはちょっとわかりません・・・
さて、デバッグ実行しているときに気が付いたのが、改行と表示されていないバーが連続で来たときなどに、
nTotalWidth += nWidth - afxData.cyBorder2;
という処理が実行されると、nTotalWidthが減るという事です。
nWidthは0ですが、afxData.cyBorder2は0ではないため(2だったかな?)
改行&表示されていないバーが来るたびに
nTotalWidthの値がどんどん減っていきました。
・・・これってバグじゃないの?
この部分の不具合疑惑が浮上し、ここをなんとか書き換えて実行しようと思いました。
書き換えたい気持ちは山々なのですが、ここはMFCソース内部、変えるのが困難な部分です。
誰か変えた人いないの!?ってなかんじでぐぐってみましたが、ヒットせず_| ̄|○
しょうがないので自分でやることに(^^;
まず、CDockBar::Insertという関数はvirtual関数ではないため、オーバーライドさせることが出来ません。そんなわけで、virtual関数が出てくるまで呼び出し元をたどりました。
CDockContext::StartDrag(CPoint pt);
という関数が仮想関数であることがわかったため、
この関数からCDockBar::Insertまでのすべての呼び出し処理を変更すればCDockBar::Insert処理を別のものに変えられそうでした。
具体的には
CDockContext::StartDrag() → CDockContext::Track() → CDockContext::EndDrag() → CFrameWnd::DockControlBar() → CDockBar::DockControlBar() →CDockBar::Insert()
これら全ての関数を修正する必要があるという事です(´・ω・`)ショボーン
クラス別に見て
- CDockContext
- CFrameWnd
- CDockBar
これら3つがありますで、これらから新たにクラスを派生させ、そこで別関数を用意すれば良いという事です。
(CFrameWndに関してはCMainFrameという関数がありますので、クラスを新しく用意する必要はありませんね。)
で、これらのクラスから新たに
CDockContext
→CMyDockContext
CDockBar
→CMyDockBar
というクラスを作るのはまぁ出来るだろうとは思うのですが、
いままでCDockContextクラスをnewしていた部分を今度はCMyDockContextクラスをnewさせるようにしないといけないわけで、それが出来るかどうかの実験から始めました。
CDockContextのコンストラクタにブレークポイントを設定して生成タイミングなどを調査した結果、
CControlBar::EnableDocking();関数内でnewされている事が解りました。
CToolBarから新たにクラスを派生させ、そこからEnableDocking()を作る、というのが普通かもしれませんが、
なにしろツールバー以外にもコントロールバーというのは多数ありますから(ダイアログバーなど)
少し汚い手ですが、
EnableDocking();させた後で全コントロールバーに対してメンバ変数m_pDockContextを見て、インスタンスあれば一旦deleteして、CMyDockContextをnewさせ、保持させておくという手段を用いました(爆
CDockBarの方はCFrameWnd::EnableDocking時に生成されるようでしたので、その関数のソースをそのままコピペしてnew CDockBar部分を書き換えました。
これでクラスのインスタンスをごっそり変える事が出来ましたので、
CMyDockContextや、CMyDockBarのメンバ変数をコピペしたりして
各関数を実装します。
CMyDockContext::StartDrag() → CMyDockContext::TrackDebug() → CMyDockContext::EndDragDebug() → CMainFrame::DockControlBarDebug() → CMyDockBar::DockControlBarDebug() →CMyDockBar::InsertDebug()
というように呼ばれるわけです。
CMyDockContext::EndDragDebug() → CMainFrame::DockControlBarDebug()
というように、クラスが異なる呼び出しの場合には、
呼び出す際の型キャストも必要です。
CMainFrame* pMainFrame = dynamic_cast<CMainFrame*>(m_pDockSite); if (pMainFrame) { pMainFrame->DockControlBarDebug(m_pBar, pDockBar, &rect); } else { m_pDockSite->DockControlBar(m_pBar, pDockBar, &rect); }
こんな感じで最終的にCMyDockBar::InsertDebug()が呼ばれるようにし、
これでようやく・・・ようやくCDockBar::Insert部分をいじれるようになりました_| ̄|○
早速例の「高さの合計を示すnTotalWidth変数にnWidth足す」という部分に修正を試みます。
#if 1 if (nWidth > 0) { nTotalWidth += nWidth - afxData.cyBorder2; } #else nTotalWidth += nWidth - afxData.cyBorder2; #endif
これで実行してみたところ、ドッキング不具合は発生しなくなりました!
・・・長かった。
そんなわけで治ったっぽいプロジェクトはこちらです。
今後同じような問題にぶちあたった人のお役に立てられれば幸いです・・・。
・・・Microsoftさん、貴重な休日を返して(T_T