トップ回答者
MDIタブウィンドウのフローティング化

質問
-
現在、Win7 Pro SP1+VS2010にてMDIタブ化のアプリを改造しています。
改造の主題は、タブ化されている子ウィンドウ(View)を「VS2010のエディター部」のようにフローティング
やドッキング、分割させる事が出来ないか?ということです。
MSDNのサンプルソース「VisualStudioDemo」でも、上記の部分は実装されておらず、あくまで
コントロールバー(ファイルビューやリソースビュー、Output等)の部分がドッキング/フローティング
出来るに留まっております。
VS2008 feature packのCDockablePaneやCTabbedPane等を組み込んでみましたが、
上手く動作しません。MDIの場合、MainFrame-ChildFrame-Viewとなっており、Viewを
CDockablePaneクラス派生に置き換えてみましたが、ChildFrameが存在するので見た目も
動作も何も変化無しです。
又、MSDNによると「CMFCTabCtrl」はタブの切り離しが実現出来る、とありますので、CMDIFrameWndEx
クラスの内部にあるであろうCMDITabCtrlを置き換えることができないだろうか、とも考え、調べていますが
ヒントらしきものも掴めない状態です。
具体性に欠ける質問で恐縮いたしますが、何かヒントだけでもご教授いただけませんでしょうか?
VS2010と同様の動作が可能であれば、Doc-Viewアーキテクチャにこだわりはありません。
実のところ、1つのDocを複数のViewで表示させているアプリなのです。見た目やインターフェースは
MDIタブウィンドウアプリ(コントロールバー付き)とほぼ同じものです。
どうぞよろしくお願い申し上げます。
回答
-
お陰さまで、タブ→フローティングの独自実装もそれなりの形になってまいりました。
みなさまにはご意見をいただいたり、投稿をご覧いただいたり、ありがとうございました。
ここで、現状をご報告して本件を解決済みとさせていただきたいと思います。
どうもありがとうございました。
前提、改変については既述のとおり。その後、実装した内容は、
MainFrameのOnFloat()ハンドラ内で、
CChildFrameSub(フローティング状態の新ChildFrame)をnew→Create
(WS_CHILDではなくWS_POPUPで、ウィンドウクラスはNULL指定("MDICLIENT"を指定すると動かない)
(newした新ChildFrameのポインタはMainFrame内で管理する)
元のChildFrameからFrameText(ドキュメント名)を取得し、新ChildFrameに設定
(ウィンドウタイトルを引き継がないと、タブからフローティングしたように見えないので)
フロートさせるViewの親ウィンドウを変更(SetParent())
元のChildFrameにWM_CLOSEを送信して閉じる
(対象のTABを非表示かRemoveする事も考えたのですが上手く動作しなかったので、単純にChildFrameをCLOSEしました)
となります。これで、TABからウィンドウを切り離し、フローティング(MainFrameの外で)するようになります。
今度はフローティングからTABに戻す方法ですが、↑とほぼ逆の事をやってあげました。
(新ChildFrameに「TABに戻す」コンテキストメニューを実装し、OnTabBack()ハンドラを追加、その中での処理です)
ただ、TAB内のChildFrameやViewを手で一から書くのは大変だったので標準実装されているのOnFileNew()で
空のTABとViewを作成し、
ウィンドウテキストの移動(ドキュメントタイトルです)
TAB内のViewをCLOSE
フローティングしているChildFrame内のViewの親を変更
フローティングしているChildFrameのCLOSE
という流れで見た目だけですが、TAB←→フローティングの動きが出来ました。
なお、上で質問している新ChildFrameをCreateした時のウィンドウサイズの問題ですが、原因が判りましたので報告しておきます。
新ChildFrameのCreate時にはCRect構造体で元のViewのサイズを引き継ぐように指定しています。
ここで指定しているサイズは間違っていませんでした。どこかでサイズが書き代わったのですが、コードを追いかけると、
新ChildFrameのベースクラスのCFrameWndEx::PreCreateWindow()内でm_Impl.RestorePosition(cs)
を行っており、どうやらレジストリに退避しているウィンドウサイズを無理やり引っ張ってきてそれでcsを書き代えていました。
それ自体は間違いではないでしょうけども、今回は邪魔な事だったので、もう一度csを上書きしサイズを元に戻すことで
対応しました。
長くなりましたが、最後までお付き合いいただきまして大変ありがとうございました。
今回は大変勉強になりました。これをベースに既存アプリに適用させていこうと思います。
また、今後もお力添えを賜りたく、何卒よろしくお願い申し上げます。
ありがとうございました。
- 回答としてマーク wolfie5150 2012年6月14日 7:32
すべての返信
-
CMFCBaseTabCtrl :: AddTab( , , , TRUE)
とか、
CMFCBaseTabCtrl::EnableTabDetach( int iTab, BOOL bEnable);
とかの話でしょうか。
(参) http://msdn.microsoft.com/en-us/library/cc137828(v=vs.90).aspx
そういう簡単な問題じゃないかもね(vv;)。
外してたらごめんなさい。 -
ご返信ありがとうございます。ご指摘の箇所はCMFCTabCtrl絡みで拝見しておりまして、
サンプルコードも書いてみたのですが、まさしく「そういう簡単な問題」ではございませんでした。
CMFCTabCtrlでデタッチ出来るのは出来そうですが、デタッチする対象(タブの中身)は、
CDockablePane派生でなければならない(もしくはラッパーを自動生成される)ようですし、
タブから切り離す処理は開発者側で書いてあげなければいけなさそうです。(マウスドラッグイベント等)
今のところ、手詰まり感でいっぱいです。現行のMFCでの実装は難しいのかもしれないですね。
-
私の想像したのと間違っていたらごめんなさい。
サンプルのなかに、「MDITabsDemo」というのがあります(http://msdn.microsoft.com/ja-jp/library/bb982946(v=vs.90).aspx)
これをビルドし、アプリを立ち上げると、左側にプロパティペインが出ます。
このプロパティの中から「EnableMDITabs」という項目があるので、この値をいじってみてください。 -
Yominetさま
ご意見、ありがとうございます。このサンプルはダウンロードして実際に動作させていたのですが、
仰られるプロパティペインの値の事は気が付きませんでした。早速、「Enable MDI Tabs」プロパティを
いじってみましたが、やはり少々イメージとは異なっておりました。
このサンプルでのタブ化→フローティングはあくまでもMainFrame内に限られておりますが、実現したい
のはMainFrameの「外」に独立したウィンドウとしてフローティングさせたい、という事なのです。
ですが、貴重なお時間を割いてご意見を賜り感謝いたします。ありがとうございました。
-
みなさま、大変お世話になっております。
たくさんの方にご覧いただき、感謝いたします。
さて、現在(6/11)のところ、本件について現状のMFCの標準機能では実現が難しいそうだろうとなりまして、
タブ上で右クリメニュー→(追加)フローティング→別画面(ダイアログか?)を生成→タブの内容を別画面に描画→タブを消去(Hide?)
という処理を自前で実装してみる方向も検討してみることになりました。(もしMFC標準機能で実現する方法がございましたら、引き続き
情報をお待ちしております)
そちらの方でも質問等があるやもしれませんので今後もどうぞよろしくお願いします。
このたびは誠にありがとうございました。
-
たびたびのご指摘を感謝いたします。
まさしく仰る通りですね。
まだ実装出来ていませんが、気になるのは、元々はMainFrameの子供としてChildFrame-Viewが存在し、タブを形成しています。
その状態で、NewChildFrameを作成し元のViewの親をこれに変更してやるという事なんですが、NewChildFrameもMainFrame
の子供である必要がありますよね?(あくまでも同一アプリ内ですから)そうすると、MainFrameの「外」に独立したような形でNewChildFrame
を生成できるのでしょうか・・・?(←実際にやってみた方が早いですね^^;)
ご意見、ありがとうございました。
-
>NewChildFrameもMainFrame
>の子供である必要がありますよね?(ややあいまいなので、正確に書いてみると・・・
新しいフレームウインドウであるNewChildFrameはWS_POPUPではありますが、
WS_CHILDフラグは持ちません。この意味では「子」ではありません。
ですが、CreateWindow(Ex)()するときに、親ウインドウのハンドルとして、
MainFrame のHWNDを与えるべきだと考えます。この意味では「子」ですね。(蛇足)MDIの仕組みを引きずったままでも、このような関係のウインドウを
作ることができるわけですが、当然このフレームは親のクライアント領域の外に
表示されます。まぁ、ちょっと変わったMDIにできるわけですね。
当たり前ですが、このPOPUP版MDI子ウインドウは最大化はできますが、
最小化できません(vv;)。 -
確証はないですが CMDIFrameWndEx と CMDIChildWndEx を使って MDI を構成したらできたりしませんか?
サンプルの「VisualStudioDemo」でドッキングペインを MDI 部にドッキングはできるようなので・・・・。
※但し、何故か一度ドッキングすると外に出せないですが・・・・。
--------------------------------------------------------------------------------------------------------
サンプルをもう少し調べてみたら
CMDIFrameWndEx::TabbedDocumentToControlBar
CMDIFrameWndEx::ControlBarToTabbedDocument
を使うことで CDockablePane 派生インスタンスを MDI タブ化とコントロールバー化することは可能なようです。
なので VS2010 と同様動作は可能みたいですが、Doc-View は無理という感じですかね・・・。
- 編集済み kyano30 2012年6月11日 21:06 追記
-
みなさま、大変お世話になっております。
本件につきまして現状をご報告(追加の質問もあるのですが・・・)させていただきます。
前提:
VS2010にて、MFCアプリ(タブ付きMDI、DBなし、複合ドキュメントなし、Doc-Viewアーキテクチャーあり、Unicodeライブラリ
MFC標準、MFC共有DLL、ドッキング可能ペインなし)を生成し、以下のような改変を行っています。
改変:
MDIのタブ部分で右クリのコンテキストメニューを追加→CMainFrame::OnShowMDITabContextMenu()
(メニューは「フロート」だけ)
フロート選択時のメッセージハンドラ追加→CMainFrame::OnFloat()
この中で、新フレームを生成、既存Viewの親を変更、対象のタブをDelete、という流れで実装しているところです。
問題点:
OnFloat()内で新フレームを生成する際に、CreateWindowEx()をCallしています。Unicodeライブラリ版ですので、
実際にはCreateWindowExW()が呼ばれています。コードは↓です。
m_hSubWnd = ::CreateWindowEx(WS_EX_OVERLAPPEDWINDOW, (LPCTSTR)"MDICLIENT", (LPCTSTR)strTitle, WS_VISIBLE | WS_POPUP | WS_OVERLAPPEDWINDOW, rect.left, rect.top, rect.Width(), rect.Height(), this->m_hWnd, NULL, NULL, NULL);
[ strTitle→タブに表示されているドキュメントのタイトル、rect→タブに表示されているViewのRect ]
これだと、NULLが返ってきて生成されませんでした。ところが、CreateWindowExA()を明示的にCallしてやると、きちんと生成されるのです。
m_hSubWnd = ::CreateWindowExA(WS_EX_OVERLAPPEDWINDOW, (LPCSTR)"MDICLIENT", (LPCSTR)"テストフローティングウィンドウ", WS_VISIBLE | WS_POPUP | WS_OVERLAPPEDWINDOW, rect.left, rect.top, rect.Width(), rect.Height(), this->m_hWnd, NULL, NULL, NULL);
[ "テストフローティングウィンドウ"→CStringのstrTitleをconst char*に変換する方法が判らなかったのでテキストをベタ書きしました ]
その後、移動対象のViewの親ウィンドウをSetParent()で変更してやり、元の親ウィンドウ(ChildFrame)をDestroy()することで、なんとなくタブから
Viewを切り離せるようには見せかけられるところまではできました(細かな問題には目をつぶっていますが)。ただ、
なぜ、CreateWindowExW()がXで、CreateWindowExA()が○なのか、全く理由が判りません。本当はタブに表示されているドキュメントのタイトル
を新しいフレームのウィンドウタイトルに表示する為に、上で取得したstrTitleをパラメタに渡したいので、CreateWindowExW()を使用したいところです。
またまた、質問になってしまいましたが、みなさんのお知恵を拝借できれば、と思います。
どうぞ、よろしくお願いいたします。
-
たびたび失礼いたします。上の質問は取り消しさせていただきます。申し訳ございません。
先ほど投稿いたしました↑の方法(CreateWindowEx())では、タブから切り離した後(SetParent()後)のViewの親
(GetParentFrame()で取得できるCFrameWnd)が、MainFrameになってしまう事が判りました。ですので、手法を変更しました。
MainFrame::OnFloat()内で新フレームを生成する際に、新ChildFrameクラス(class ChildFrameSub : public CFrameWnd)
を定義し、それをCreateする事にしました。その後、Viewの親を新ChildFrameに変更すれば、親がMainFrameになることも無くなるはずでしたが・・・
そのCreateが失敗して落ちてしまいます。
m_wndChildFrameSub.Create(_T("MDICLIENT"), (LPCTSTR)strTitle, WS_VISIBLE | WS_POPUP | WS_OVERLAPPEDWINDOW, rect, this, NULL, 0, NULL);
この中で落ちているのですが、VisualStudioのエラーメッセージは、
「ChangeParent.exe の 0x770015de でハンドルされていない例外が発生しました: 0xC015000F: アクティブ化を解除しているアクティブ化コンテキストは、最近アクティブ化されたものではありません。」
発生箇所は、
wincore.cppの723行目
HWND hWnd = ::AfxCtxCreateWindowEx(cs.dwExStyle, cs.lpszClass,
cs.lpszName, cs.style, cs.x, cs.y, cs.cx, cs.cy,
cs.hwndParent, cs.hMenu, cs.hInstance, cs.lpCreateParams);でCallされている先の、afxcomctrl32.hの426行目
AFX_ISOLATIONAWARE_STATICLINK_FUNC(HWND ,CreateWindowExW,(DWORD dwExStyle,LPCWSTR lpClassName,LPCWSTR lpWindowName,DWORD dwStyle,int X,int Y,int nWidth,int nHeight,HWND hWndParent,HMENU hMenu,HINSTANCE hInstance,LPVOID lpParam),(dwExStyle,lpClassName,lpWindowName,dwStyle,X,Y,nWidth,nHeight,hWndParent,hMenu,hInstance,lpParam),NULL)
で落ちています。
元々のCreate文が何か間違っているのか、クラス定義「class ChildFrameSub : public CFrameWnd」が間違っているのかだとは思うのですが、
糸口がつかめない状態です。何卒、ご教授賜りたく、よろしくお願い申し上げます。
-
まず、(LPCTSTR)strTitle
についてですが、strTitleの型(又はクラス)が明示されていないので、
どうすれば良いかのアドバイスができませんが、
みたところ、UNICODEのプロジェクトのようなので、strTitleは
UNICODE文字列と可換なものでなければなりません。
なにか無理なキャストをしていませんでしょうか。次に、ウインドウクラス名「MDICLIENT」は定義済みの名称です。
このクラスのウインドウを作成するには、それ用の特別な構造体
CLIENTCREATESTRUCTでデータを渡さなければなりません。
このあたりは理解してますでしょうか。または、これは意図したコードでしょうか。
-
たびたびのご返信を感謝いたします。
ご確認いただきました件についてですが、
1.(LPCTSTR)strTitle
これは、元々のタブに表示されているドキュメントのタイトルを取得したCString型の変数です。それを新ChildFrameのウィンドウ枠に表示する為に
(LPCTSTR)キャストして渡しています。
2.「MDICLIENT」をウィンドウクラスに指定してCreateWindowを行う際には、ご指摘の通り、CLIENTCREATESTRUCTを渡さねばならない事
は他のサイトで記載がありましたので存じておりました。但し、「CFrameWnd::Create()」は、この構造体を渡すI/Fになっていないので、気になり
ながらも上では何も触れずに進めていました。
<この間に、少しコードを変更して試しています>
上の点も気になって、このケースでウィンドウクラスに「MDICLIENT」を指定するのは間違っているような気がしてきましたので、NULLに変更しました
(MSDNには、「 仲澤@失業者さまににご指摘いただいたお陰で気づくことが出来ました。ありがとうございました。ただ、新たな問題が・・・
上のコード内の「rect」ですが、元々のViewのサイズを取得して、そのサイズで新しいChildFrameを作成しようとしています。コードは、↓
CMDIChildWndEx* pActiveFrame = (CMDIChildWndEx *)MDIGetActive(); //タブ内のChildFrameを取得
CChangeParentView* pActiveView = (CChangeParentView *)pActiveFrame->GetWindow(GW_CHILD); //対象のViewを取得
CRect rect;
pActiveView->GetWindowRect(rect); //Viewのサイズを取得といった内容です。理屈としてはViewのサイズが取得できるはずなんですが、何故か、MainFrame(メインのウィンドウ)のサイズを取得しているようで、
Createした新ChildFrameが元のアプリと同じサイズに出来上がってしまいました。この点についてはもう少し自力で調査してみます。
しかし、みなさんのお陰で少しづつですが糸口が見えて参りました。ありがとうございました。
今後もどうぞよろしくお願い申し上げます。
-
なんだか独自実装する方向に傾いているようなので口を挟むのも何なのですが・・・。
CDockablePane で実装できるようなのに実現できなかった理由を考えてみましたが、ChildFrame を使わずに CDockablePane 派生インスタンスをCMDIFrameWndEx::ControlBarToTabbedDocument を使ったドッキングを試していないのでは?
ヘルプにも記述がありますがフローティング化ができるのは CDockablePane 派生インスタンス をドッキングさせた場合のみで、デモを動かした結果からも CDockablePane 派生インスタンスに関しての手動操作ならドッキング&フローティングは動作してるように見えます。
※VS2010 の様にドラッグでの取り外しは実装されていませんが、コーディング可能なのではないですかね・・・・。
こちらの機能で実装しても View と違うので色々問題があるでしょうから、お勧めできるほどの差がなく蛇足になります。
-
お陰さまで、タブ→フローティングの独自実装もそれなりの形になってまいりました。
みなさまにはご意見をいただいたり、投稿をご覧いただいたり、ありがとうございました。
ここで、現状をご報告して本件を解決済みとさせていただきたいと思います。
どうもありがとうございました。
前提、改変については既述のとおり。その後、実装した内容は、
MainFrameのOnFloat()ハンドラ内で、
CChildFrameSub(フローティング状態の新ChildFrame)をnew→Create
(WS_CHILDではなくWS_POPUPで、ウィンドウクラスはNULL指定("MDICLIENT"を指定すると動かない)
(newした新ChildFrameのポインタはMainFrame内で管理する)
元のChildFrameからFrameText(ドキュメント名)を取得し、新ChildFrameに設定
(ウィンドウタイトルを引き継がないと、タブからフローティングしたように見えないので)
フロートさせるViewの親ウィンドウを変更(SetParent())
元のChildFrameにWM_CLOSEを送信して閉じる
(対象のTABを非表示かRemoveする事も考えたのですが上手く動作しなかったので、単純にChildFrameをCLOSEしました)
となります。これで、TABからウィンドウを切り離し、フローティング(MainFrameの外で)するようになります。
今度はフローティングからTABに戻す方法ですが、↑とほぼ逆の事をやってあげました。
(新ChildFrameに「TABに戻す」コンテキストメニューを実装し、OnTabBack()ハンドラを追加、その中での処理です)
ただ、TAB内のChildFrameやViewを手で一から書くのは大変だったので標準実装されているのOnFileNew()で
空のTABとViewを作成し、
ウィンドウテキストの移動(ドキュメントタイトルです)
TAB内のViewをCLOSE
フローティングしているChildFrame内のViewの親を変更
フローティングしているChildFrameのCLOSE
という流れで見た目だけですが、TAB←→フローティングの動きが出来ました。
なお、上で質問している新ChildFrameをCreateした時のウィンドウサイズの問題ですが、原因が判りましたので報告しておきます。
新ChildFrameのCreate時にはCRect構造体で元のViewのサイズを引き継ぐように指定しています。
ここで指定しているサイズは間違っていませんでした。どこかでサイズが書き代わったのですが、コードを追いかけると、
新ChildFrameのベースクラスのCFrameWndEx::PreCreateWindow()内でm_Impl.RestorePosition(cs)
を行っており、どうやらレジストリに退避しているウィンドウサイズを無理やり引っ張ってきてそれでcsを書き代えていました。
それ自体は間違いではないでしょうけども、今回は邪魔な事だったので、もう一度csを上書きしサイズを元に戻すことで
対応しました。
長くなりましたが、最後までお付き合いいただきまして大変ありがとうございました。
今回は大変勉強になりました。これをベースに既存アプリに適用させていこうと思います。
また、今後もお力添えを賜りたく、何卒よろしくお願い申し上げます。
ありがとうございました。
- 回答としてマーク wolfie5150 2012年6月14日 7:32