Explorerスタイルでリストビューの切り替え

[戻る]
ふぁいと! 2002/02/04(月) 21:17:39
独学で奮闘しています初心者です。

VC++ MFC(SDI)のエクスプローラスタイルで作成しています。
内容はLeftViewで選択したアイテムによって右側のリストビューに表示される
フォーマットが異なるものを作ろうとしています。(FormViewが数パターンあるもの)
いくつかうまくいったのですがファイルの詳細を出そうとするとうまくいきません。
ビルドするとエラーはないのですが実行するとコラムも何もなく真っ白です。
どなたかご指導お願いします。
以下はソースです。

//MainFrm  ツリーでアイテムを選択する
BOOL CMainFrame::Change_RightView(int FormNO)
{
m_wndSplitter.DeleteView( 0,1 );
CCreateContext context;
context.m_pCurrentDoc = GetActiveDocument();
int    n;

switch (FormNO){
    case 1:
    case 2:
    case 3:
            m_wndSplitter.CreateView( 0,1,RUNTIME_CLASS(CFormList),CSize 
            (100,100),&context );
       break;
    default:
       AfxMessageBox("エラー");
    }
    m_wndSplitter.RecalcLayout();
    
    return TRUE;
}


//リストビュー(FormView)
void CFormListAP::PreSubclassWindow() 
{
    CListCtrl& m_wndList = GetListCtrl();
    m_wndList.MoveWindow(0,0,6400,6400);
    m_wndList.DeleteAllItems();

    //コラムのヘッダの設定
    LVCOLUMN myColumn;
    myColumn.mask = LVCF_TEXT;
    RECT myRect;
    m_wndList.GetClientRect(&myRect);

    myColumn.cx = (myRect.right - myRect.left) * 1/ 2;
    myColumn.pszText = "名前";
    m_wndList.InsertColumn(0, &myColumn);

    myColumn.cx = (myRect.right - myRect.left) * 1/ 2;
    myColumn.pszText = "詳細";
    m_wndList.InsertColumn(1, &myColumn);

    // リストビューにアイテムを設定する
    static LVITEM myITEM;
    myITEM.mask = LVIF_TEXT;
    myITEM.iItem = 0;
    myITEM.iSubItem = 0;
    myITEM.pszText = "TEST";
    m_wndList.InsertItem(&myITEM);

    myITEM.iSubItem = 1;
    myITEM.pszText = "TEST";
    m_wndList.SetItem(&myITEM);

    long state;
    state = ::GetWindowLong(m_wndList.GetSafeHwnd(), GWL_STYLE);
    SetWindowLong(m_wndList.GetSafeHwnd(), GWL_STYLE, (state &
     ~LVS_TYPEMASK) | LVS_REPORT);

    CListView::PreSubclassWindow();

}

sara 2002/02/05(火) 11:02:46
 まず、ビューを切り替える必要があるのかということが問題だと思います。
 エクスプローラのスタイルで作るのでしたら、FormViewである必要もないし、ビューそのも
のを切り替える必要はないと思います。右側のビューの基本クラスをCListViewにして、切り替
えるタイミングで、リストビューの表示内容を作り直すだけで事足りるはずです。

 上記の点を踏まえて、あえて分割ウィンドウのビューを動的に切り替える必要があるのでした
ら、少々面倒な手順を踏む必要があります。

1.ビューの現在位置を保存する
2.ビューの対応するドキュメントを探す
3.ビューを破棄するときにドキュメントが自身を破棄しないようにする
 (CDocument* pDC がドキュメントとすると pDoc->m_bAutoDelete をFALSEにする必要
があります。ただし、FALSEにする前の状態を保存しておきます)
4.ビューを破棄します(ビューのDestoryWindow()メンバ関数をコールします)
5.3でFALSEにしたドキュメントのm_bAutoDelete の値を元に戻します
6.作成コンテキストを初期化します
7.CreateViewで新しいビューを作ります
8.新しいビューを1で保存した位置に配置します
9.新しいビューに、WM_INITIALUPDATE を送ります

 以上のようにやってみてください。

ふぁいと! 2002/02/05(火) 19:18:35
saraさんありがとうございます。
自分なりにやったつもりですがうまくいきません。
理解していないからなのでしょうけれど・・・
実行すると"0x004071d4"の命令が"0x0000004c"のメモリを参照しました。
メモリが"writen"になることはできませんでした。というエラーメッセージが
でてしまいます。
申し訳ないのですが、どこがおかしいのか教えて頂けないでしょうか。

呼び出し側のMainFrm は前のままで、以下のソースだけかきかえました。

void CFormListAP::PreSubclassWindow() 
{
    CSplitterWnd m_wndSplitter;
    CCreateContext context;
    CDocTemplate* pDocT;
    CDocument* pDoc = GetDocument();
//    ASSERT_VALID(pDoc);

    CListCtrl& m_wndList = GetListCtrl();
    m_wndList.MoveWindow(0,0,6400,6400);
    m_wndList.DeleteAllItems();

    //コラムのヘッダの設定
    LVCOLUMN myColumn;
    myColumn.mask = LVCF_TEXT;
    RECT myRect, bkRect;
    m_wndList.GetClientRect(&myRect);
    bkRect = myRect;

    m_wndList.DeleteAllItems();
    pDoc->m_bAutoDelete = FALSE;
    m_wndList.DestroyWindow();

    pDoc->m_bAutoDelete = TRUE;

    m_wndSplitter.CreateView( 0,1,RUNTIME_CLASS(CFormListAP),CSize
                                   (100,100),&context );
    CListCtrl& m_WndList = GetListCtrl();
    m_WndList.GetClientRect(&myRect);

    myColumn.cx = (myRect.right - myRect.left) * 1/ 2;
    myColumn.pszText = "名前";
    m_WndList.InsertColumn(0, &myColumn);


    myColumn.cx = (myRect.right - myRect.left) * 1/ 2;
    myColumn.pszText = "詳細";
    m_WndList.InsertColumn(1, &myColumn);


    // リストビューにアイテムを設定する
    static LVITEM myITEM;
    myITEM.mask = LVIF_TEXT;
    myITEM.iItem = 0;
    myITEM.iSubItem = 0;
    myITEM.pszText = "TEST";
    m_wndList.InsertItem(&myITEM);

    myITEM.iSubItem = 1;
    myITEM.pszText = "TEST";
    m_WndList.SetItem(&myITEM);
    
    long state;
    state = ::GetWindowLong(m_WndList.GetSafeHwnd(), GWL_STYLE);
    SetWindowLong(m_WndList.GetSafeHwnd(), GWL_STYLE, (state & 
                       ~LVS_TYPEMASK) | LVS_REPORT);

    CListView::PreSubclassWindow();


    CDocTemplate* pTemplate;
    ASSERT_VALID(pTemplate);


    CFrameWnd* pFrame=pTemplate->CreateNewFrame(pDoc,NULL);
    if(pFrame==NULL)
    {
    return;
    }
    pTemplate->InitialUpdateFrame(pFrame,pDoc);


}

sara 2002/02/06(水) 09:53:56
まずCreateViewの使い方が変です。
それから、PreSubclassWindow()の中でいろいろやっているようですが、この中でリストビュ
ーにヘッダやアイテムを挿入すると実行時にエラーになると思います。(やってみたことがない
ので断言はできませんが)

ソースを見る限りでは、ごちゃごちゃと混乱しているようです。
まず、ビューの切り替えとビューの表示内容の更新を区別して、手順を整理しましょう。

ビューを切り替えるなら、次のように考えます。

例えば、LeftViewに A,B,Cの三種類のアイテムがあって、
 Aを選択すると右側のビューは CFormViewA になり、
 Bを選択すると   〃    CFormViewB になり、
 Cを選択すると   〃    CFormViewC になる
というようなことを実現するとします。

○リストビューの表示(ヘッダやアイテムの設定)はCFormViewA、CFormViewB、CFormViewC
それぞれのOnInitialUpdate()関数の中で行う。

○ビューを切り替える関数を実装する
void ChangeViewInSplitter(CSplitterWnd* pSplitter, CRuntimeClass* pViewClass)
{
   ASSERT_VALID( pSplitter );
   ASSERT( pViewClass != NULL );
   ASSERT( pViewClass->IsDeriverdFrom( RUNTIME_CLASS( CView ) ) );
   CWnd* pPaneWnd = pSplitter->GetPane( 0, 1 );
   ASSERT_KINDOF( CView, pPaneWnd );
   CView* pCurrentView = static_cast<CView*>( pPaneWnd );
   ASSERT_VALID( pCurrentView );
   if( pCurrentView->IsKindOf( pViewClass ) )
      return;    // ビュークラスが同じなので切り替える必要なし
   CRect rcView;
   pCurrentView->GetWindowRect(&rcView);   // ビューの現在位置の保存
   CView* pActiveView->pSplitter->GetParentFrame()->GetActiveView();
   BOOL bSaveActive = (pActiveView == NULL ) || ( pActiveView == pCurrentView );
   // 起動状態の保存
   CDocument* pDoc = pCurrentView->GetDocument();   // ドキュメントの取得
   ASSERT_VALID( pDoc );
   BOOL bSaveAutoDelete = pDoc->m_bAutoDelete;
   pDoc->m_bAutoDelete = FALSE;   // ドキュメントを破棄しないように
   pCurrentView->DestroyWindow();   // ビューを破棄する
   pDoc->m_bAutoDelete = bSaveAutoDelete;   // ドキュメントの状態を元に戻す

   // コンテキストの初期化
   CCreateContext context;
   context.m_pNewDocTemplate = NULL;
   context.m_pLastView = NULL;
   context.m_pCurrentFrame = NULL;
   context.m_pNewViewClass = pViewClass;
   context.m_pCurrentFrame = NULL;

   pSplitter->CreateView( 0, 1, pViewClass, rcView.Size(), &context);   // 新し
いビューの作成

   CView* pNewView = static_cast<CView*>(pSplitter->GetPane( 0, 1);   // 新しい
ビューの取得
   ASSERT_VALID( pNewView );
   ASSERT_KINDOF( CView, pNewView );   

   pSplitter->ScreenToClient( &rcView );
   pNewView->MoveWindow( &rcView, TRUE );   // 前のビューの位置に配置する
   if( bSaveActive )
      pSplitter->GetParentFrame()->SetActiveView( pNewView );   // アクティブにす
る
   pNewView->GetParentFrame()->InitialUpdateFrame( pDoc, TRUE );
} 

 こんな感じで。ここでは、切り替えるビューの位置を固定にしていますが、引数指定にしたも
のを作っておくと、のちのち便利でしょう。

○アイテム選択の変化したメッセージを受けてコールされる関数の中で、ビューを切り替える
   switch(item){
   case A:
      ChangeViewInSplitter( &wndSplitter, RUNTIME_CLASS( CFormViewA );
      break;
   case B:
      ChangeViewInSplitter( &wndSplitter, RUNTIME_CLASS( CFormViewB );
      break;
   case C:
      ChangeViewInSplitter( &wndSplitter, RUNTIME_CLASS( CFormViewC );
      break;
   default:
      AfxMessageBox("エラー");
      break;
   }

 ビューの種類は同じで、表示内容を更新する場合は別の処理をします。
 例えば、ChangeViewInSplitter()関数の戻り値はvoid型にしてありますが、BOOL型にし
て、同ビュー(同種類)の場合はFALSEを返すようにして、FALSEが返ってきたら表示更新用の
関数をコールするとか。

以上、参考にして下さい。(タイプミスがあってもご容赦を)

ふぁいと! 2002/02/06(水) 16:13:10
saraさんご親切にありがとうございます。
教えていただいたソースを使わせていただきましたが、最初のほうでつまづいて
います。情けない質問かもしれませんが教えて頂けないでしょうか?
宜しくお願いします。

CWnd* pPaneWnd = pSplitter->GetPane(0,1);
の行が実行時エラーとなります。(winsplit.cpp   Lin:364)
見てみると ASSERT(pView != NULL);  のところでpViewがNULLの
ためでした。
switch〜case で関数に渡すクラスもダイアログのないCListViewだけでなく
ダイアログのあるCFormViewでも同じ結果となりました。
(GetDlgItemがでてきたので画面のIDがないとエラーになるのではと思ったので)


//---ソース-------------------------------------------------------------//
void CMainFrame::ChangeViewInSplitter(CSplitterWnd *pSplitter, CRuntimeClass 
*pViewClass)
{
   // ビューを切り替える関数
   ASSERT_VALID(pSplitter);
   ASSERT(pViewClass != NULL);

   //↓エラーのためコメント。"error C2039: 'IsDerivedForm' : 'CRuntimeClass' の
       メンバではありません。"
// ASSERT(pViewClass->IsDerivedForm(RUNTIME_CLASS(CView)));
// ASSERT(pViewClass->IsDerivedForm(CRuntimeClass* pViewCalss));

// CWnd* pPaneWnd = (CWnd*)pSplitter->GetPane(0,1);
   CWnd* pPaneWnd = pSplitter->GetPane(0,1);    //← ASSERTがでる

//-----------------------------------------------------------------------//



//--エラーの部分 winsplit.cpp   Lin:364あたり----------------------------//
CWnd* CSplitterWnd::GetPane(int row, int col) const
{
    ASSERT_VALID(this);

    CWnd* pView = GetDlgItem(IdFromRowCol(row, col));
    ASSERT(pView != NULL);  // panes can be a CWnd, but are usually CViews
    return pView;
}
//----------------------------------------------------------------------//

sara 2002/02/06(水) 17:54:12
 分割ビューをどのように作成しているのでしょうか?(もちろん、ビューの切り替えとは別に
作成していますよね?)
 念のため、例を書いておきますので、確認してください。

BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT /*lpcs*/, CCreateContext* 
pContext)
{
   CRect rect;
   GetClientRect( &rect );
   CSize size = rect.Size();
   size.cx /= 2;
   size.cy /= 2;
   if( !m_wndSplitter.CreateStatic(this, 1, 2) ||
       !m_wndSplitter.CreateView(0,0, RUNTIME_CLASS(CLeftView), size, pContext) 
||
       !m_wndSplitter.CreateView(0,1, RUNTIME_CLASS(CRightView), size, 
pContext) ){
       TRACE0("スプリットウィンドウの作成に失敗\n");
       return FALSE;
    }
    return TRUE;
}

 それから、前回付記したChangeViewInSplitter()関数はグローバル関数として定義している
ので、その違いに注意してください。

ふぁいと! 2002/02/06(水) 18:49:44
saraさんありがとうございます。たびたび申し訳ありません。

●分割ビューですがもちろん作成しています。
以下はその部分です。(分割ビューの右ペインCListViewではなくはただのFormView)

やはりCWnd* pPaneWnd = pSplitter->GetPane(0,1);でASSERTがでてしまいます。

●あと前回、
>○リストビューの表示(ヘッダやアイテムの設定)はCFormViewA、CFormViewB、CFormViewC
> それぞれのOnInitialUpdate()関数の中で行う。

と教えていただきましたが、デバックモードで追って確かめたのですがOnInitialUpdate()を
通らなかったためPreSubclassWindow()に記述していました。(なんでなのかはわからず)

//--ソース------------------------------------------------------//
BOOL CMainFrame::OnCreateClient( LPCREATESTRUCT /*lpcs*/,
    CCreateContext* pContext)
{

    // 分割ウィンドウを作成します
    if (!m_wndSplitter.CreateStatic(this, 1, 2))
    return FALSE;

    if (!m_wndSplitter.CreateView(0, 0, RUNTIME_CLASS(CLeftView), CSize(200, 
        100), pContext) ||
        !m_wndSplitter.CreateView(0, 1, RUNTIME_CLASS(CFormInit), CSize(50,
        100), pContext))
    {
    m_wndSplitter.DestroyWindow();
    return FALSE;
    }

    return TRUE;
}

//------------------------------------------------------------------//

sara 2002/02/07(木) 09:45:56
> デバックモードで追って確かめたのですがOnInitialUpdate()を
> 通らなかったためPreSubclassWindow()に記述していました。(なんでなのかはわからず)

 とありますが、これはまずいと思います。
 これまでの過程がどうなっているか、そこまではちょっと手が出せないので、まず、現在コー
ディング中のものとは別に、分割ビューの簡単なサンプルを作ってみてはいかがでしょうか? 
それから、成功したものと現在のものを比較してみるとわかり易いと思います。

 例えば、左側がTreeViewで右側がListView(レポート形式)のものを作ってみましょう。

@MFC AppWizardに従い、SDIのアプリケーションを新規作成します。(プロジェクト名:
CSplitTestとします)
 このとき、ステップ6/6で アプリケーションクラスのリストが表示されますが、ここで
 CSplitTestView のクラス名を CSplitListViewに、基本クラスをCListViewに変更しま
す。

ACTreeViewを基本クラスとしたMFCクラスCSplitTreeViewを新規作成します。 
 
BCMainFrameクラスに
 CSplitterWnd m_wndSplitter;
を追加して、 OnCreateClient()関数をオーバーライドして分割ビューを作成します。

 上記のソースサンプルを次のように変更する
   CLeftView  => CSplitTreeView
   CRightView => CSplitListView

CCSplitTreeViewクラスのOnInitialUpdate()関数をオーバーライドして適当なアイテムを挿
入する
 CTreeCtrl& treeCtrl = GetTreeCtrl();
  treeCtrl.InsertItem("ITEM1");
  treeCtrl.InsertItem("ITEM2");
 (このくらい書けば十分)

DCSplitListViewクラスのPreCreateWindow()関数をオーバーライドして次の1行を追加する
  cs.style |= LVS_REPORT;

ECSplitListViewクラスのInitialUpdate()関数内で適当にリストのヘッダカラムを追加して
みる
  CListCtrl& listCtrl = GetListCtrl();
  TCHAR title[][20] = {_T("列1"), _T("列2")};
  int width[100,100};
  LV_COLUMN listcol;
  for( int i=0; i<sizeof(title) / sizeof(TCHAR)*20); i++){
    listcol.pszText = title[i];
    listcol.cx = width[i];
    listCtrl.InsertColumn(i,&listcol);
  }

このくらいなら10分くらいで試せると思いますが。
それとも、これくらいの段階までは問題がなかったのでしょうか? 

sara 2002/02/07(木) 10:13:40
先日の関数ChangeViewInSplitter()のソースはいくつかミスがあったので訂正します。

誤>   ASSERT( pViewClass->IsDeriverdFrom( RUNTIME_CLASS( CView ) ) );
正>   ASSERT( pViewClass->IsDerivedFrom( RUNTIME_CLASS( CView ) ) );
            // IsDerivedFromのスペルが間違っていました

誤>   CView* pActiveView->pSplitter->GetParentFrame()->GetActiveView();
正>   CView* pActiveView = pSplitter->GetParentFrame()->GetActiveView();
            // 最初の"->"は"="の間違いでした

誤>   CView* pNewView = static_cast<CView*>(pSplitter->GetPane( 0, 1); 
正>   CView* pNewView = static_cast<CView*>(pSplitter->GetPane( 0, 1));
            // 最後の括弧が閉じていませんでした

追加>  コンテキストの初期化のところに次の1行を追加してください。
      context.m_pCurrentDoc = pDoc;


以上の3点です。
不正確なものを載せて申し訳ありませんでした。m(_)m

ふぁいと! 2002/02/07(木) 13:50:50
saraさんChangeViewInSplitter()の修正版ソースとCSplitTestViewの件
ご親切にありがとうございます。

●デバッグのときに3つのうち2つは修正していたのですが、最初の
ASSERT(pViewClass->IsDerivedFrom(RUNTIME_CLASS(CView)));
は以下のようなエラーがでていたのでコメントしていました。

//↓"error C2039: 'IsDerivedForm' : 'CRuntimeClass' のメンバではありません。"
// ASSERT(pViewClass->IsDerivedFrom(RUNTIME_CLASS(CView)));
  
●CWnd* pPaneWnd = pSplitter->GetPane(0,1);でASSERTがでるのとは関係なさそうでした
ので。


●CSplitTestViewではちゃんとOnInitUpdate()を通ります。
ChangeViewInSplitter()は上記のとおりASSERTがでてエラーになるため試して
いませんが。最初のMFC AppWizardで右側がListViewを選択していれば
OnInitUpdate()を通るのでしょうか?関係ないような気がしますけど・・・

sara 2002/02/07(木) 15:36:32
なぜ OnInitUpdate()を通らないのかは、そんな状況になった経験もないので私にはわかりませ
ん。

 とりあえず、CSplitTestの表示が上手くいったなら、そのサンプルで
ChangeViewInSplitter() を実現してみましょう。

@CFormViewを基本クラスとするCFormViewAを新規作成します。

ACSplitTreeView に
  BOOL m_bInit;
 というメンバを追加し、コンストラクタで FALSEを代入します。

BCSplitTreeView の OnInitialUpdate()関数を修正します。
  if(!m_bInit ){
   CTreeCtrl& treeCtrl = GetTreeCtrl();
    treeCtrl.InsertItem("ITEM1");
    treeCtrl.InsertItem("ITEM2");
    m_bInit = TRUE;
  }

CCSplitTreeView で TVN_SELCHANGED メッセージ関数を追加します
void CSplitTreeView::OnSelchanged(NMHDR* pNMHDR, LRESULT* pResult) 
{
  NM_TREEVIEW* pNMTreeView = (NM_TREEVIEW*)pNMHDR;

  if( m_bInit ){
    CTreeCtrl& treeCtrl = GetTreeCtrl();
    HTREEITEM item = treeCtrl.GetSelectedItem();
    CString strItem1 = _T("ITEM1");
    CMainFrame* wnd = (CMainFrame*)AfxGetApp()->m_pMainWnd;
    if( treeCtrl.GetItemText(item) == strItem1 )
      ChangeViewInSplitter(&wnd->m_wndSplitter, RUNTIME_CLASS(CSplitListView));
    else
      ChangeViewInSplitter(&wnd->m_wndSplitter, RUNTIME_CLASS(CFormViewA));
  }
  *pResult = 0;
}

D ChangeViewInSplitter()の修正と追加の部分を確認してください
 (特にコンテキストの初期化の部分でドキュメントの代入をしていないと実行時にエラーにな
るので、注意してください)

これで左側のTreeViewのアイテムを選択すると、右側のビューの切り替えが出来るはずです。
試してみてください。

ふぁいと! 2002/02/07(木) 21:09:09
[[解決]]
saraさん動作しました!
ご親切にありがとうございました。
ListViewもちゃんとOnInitUpdate()を通りますし、
ChangeViewInSplitter()でもASSERTがでませんでした。

でも違うところといえば最初のMFC AppWizardで右側がListViewを選択したか
CFormViewを選択したか・・・という違いくらいです。
やはり選択時に必要な機能がインプリメントされていないからなのでしょうか??
saraさん本当にありがとうございました。
私も今後頑張りますので、これからも宜しくお願いします。

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