オーナードロウメニューについて

解決


moith  2003-12-10 11:42:45  No: 52812  IP: [192.*.*.*]

2つほど、質問させていただきます。

1つは、CMenu派生クラスを使用してオーナードロウメニューを作成しました。
(もちろん、DrawItem、MeasureItemをオーバーライドしています)

このクラスを使用して、AppendMenu()で「MF_OWNERDAW」を指定して、
TrackPopupMenu()で右クリックでポップアップメニューを表示させました。

しかし、サブメニューを持つアイテムのみ、表示がおかしいのです。
デバッグしてみたところ、サブメニューを持つアイテムのみ、
MeasureItem()が呼び出されていませんでした。
かわりに、「warning: unknown WM_MEASUREITEM for menu item 〜」
なるメッセージがでています。
どうも、アイテムのIDが認識できませんということみたいですが、
DrawItem()は、ちゃんと呼び出されて、描画されています。

どうすれば、サブメニューを持つアイテムもMeasureItem()が
呼び出されるのでしょうか?

一応、メニューを持っているウィンドウでWM_MEASUREITEMメッセージを
受信したら、MeasureItem()を呼び出すことで対応しています。

もう1つの質問は、質問のオーナードロウを行うと、Windows98では
正常に表示されるのですが、XP上で表示すると枠だけ表示され、
選択状態になったアイテムだけ描画されていきます。
また、この後、他のアプリのメニューも同じ現象になってしまいます。

解決策を教えていただけないでしょうか?

以上、どなたか解凍をお願いいたします。

編集 削除
なーめ  2003-12-11 04:11:44  No: 52813  IP: [192.*.*.*]

誰もレスしないんで、
ダイアログアプリで追跡してみようかと思ったけど、
どうもあなたの方法/現象をトレースできない。
MDI/SDI のフレームウィンドウのメニューなのか、ポップアップメニューなのか。
あなたのコードを示してください。
CMenu 派生クラスは Class Wizard で自動的に作ってくれないのでその作成手順も。
あと開発環境もね。(当方 P41.9G/Win2ksp4/VC++6.0sp5)

>> DrawItem()は、ちゃんと呼び出されて、描画されています

オーナ描画なら、OnDrawItem() ではないの?
http://www.microsoft.com/japan/developer/library/vcmfc/_mfcnotes_tn014.htm

>> 以上、どなたか解凍をお願いいたします

ヒノカグツチが必要。(^^;;

編集 削除
moith  2003-12-11 14:11:17  No: 52814  IP: [192.*.*.*]

レスありがとうございます。

メニューはダイアログ上のOnRButtonUp()ないで、TrackPopupMenu()で
ポップアップメニューとして表示させています。
開発環境は、celeron700MHz/Windows98SE/VC++ sp5 です。

メニュークラスはClass Wizardで「MFC:generic CWnd」で
「CCustomMenu」を作成して、CWndからの継承をCMenuに書き換えました。
(宣言:class CCustomMenu : public CMenu{  〜  };)
そして、

virtual void DrawItem( LPDRAWITEMSTRUCT lpDrawItemStruct );
virtual void MeasureItem( LPMEASUREITEMSTRUCT lpMeasureItemStruct );

として、オーバーライドしています。

また、アイテム追加用関数を作成しました。CMenu::AppnedMenuとほとんど同じ仕様です。
(オーナードロウの場合、アイテム情報を自前で用意しなければならないようなので)

m_listItemPtrはCList型で、自前のアイテム情報構造体のポインタのリストです。

BOOL CCustomMenu::AppendItem
(UINT uFlags, UINT uID, LPCTSTR strText, HICON hIcon)
{
  //セパレータなら、アイテム情報は作成しません
  if( uFlags & MF_SEPARATOR )
    return CMenu::AppendMenu( MF_OWNERDRAW);

  //アイテム情報の設定
  PCUSTOMMENUITEM pitem = new CUSTOMMENUITEM;
  m_listItemPtr.AddTail(pitem);

  pitem->uID = uID;
  pitem->strText = strText;
  pitem->hIcon = hIcon;
  pitem->pSubMenu = NULL;

  //サブメニューの場合、新たにサブメニューを作成します
  if( uFlags & MF_POPUP )
  {
    CCustomMenu* pSubMenu = (CCustomMenu*)uID;

    pitem->pSubMenu = new CCustomMenu;
    pitem->pSubMenu->Attach(pSubMenu->Detach());

    uID = (UINT)(pitem->pSubMenu->GetSafeHmenu());
  }

  return CMenu::AppendMenu(uFlags | MF_OWNERDRAW, uID, (LPCTSTR)pitem);
}

これで、ポップアップメニューを以下のように表示させます。

void CTestCustomMenuDlg::OnRButtonUp(UINT nFlags, CPoint point) 
{
  CPoint pt = point;
  ClientToScreen(&pt);

  CCustomMenu sub2;
  sub2.CreatePopupMenu();
  sub2.AppendItem( NULL, 31, "menu3-1");

  CCustomMenu sub1;
  sub1.CreatePopupMenu();
  sub1.AppendItem( NULL, 21, "menu2-1");
  sub1.AppendItem( MF_POPUP, (UINT)&sub2, "menu2-2");
  sub1.AppendItem( NULL, 23, "menu2-3");

  CCustomMenu menu;
  menu.CreatePopupMenu();
  menu.AppendItem( NULL, 11, "menu1-1");
  menu.AppendItem( MF_POPUP, (UINT)&sub1, "menu1-2");
  menu.AppendItem( NULL, 13, "menu1-3");
  menu.TrackPopupMenu( TPM_LEFTALIGN, pt.x, pt.y, this);
  
  CDialog::OnRButtonUp(nFlags, point);
}

すると、MF_POPUPを指定しているアイテムだけオーバーライドした
CCustomMenu::MeasureItem()を呼び出してくれません。
(DrawItemはやっぱり呼び出されています。)

また、この代行策は、メニューアイテム情報を持つための親クラスを持たない
クラスを作成して、仮想関数でないDrawItem/MeaureItemを用意しました。

そして、メニュー呼び出したダイアログウィンドウのWindowProc()を
オーバーライドして、以下のようにしてオーナードロウを実現しました。


  if( message == WM_DRAWITEM && wParam == 0 )
  {
    DrawItem( (LPDRAWITEMSTRUCT)lParam);
  }
  else if( message == WM_MEASUREITEM && wParam == 0 )
  {
    MeasureItem( (LPMEASUREITEMSTRUCT)lParam);
  }

ただ、どちらの方法でもWindows98では通常に表示されますが
WinodwsXPでは最初の発言のような現象になります。

編集 削除
なーめ  2003-12-12 02:26:40  No: 52815  IP: [192.*.*.*]

まずは確認で。

CustomMenu.cpp:

BEGIN_MESSAGE_MAP(CCustomMenu, CWnd)
  //{{AFX_MSG_MAP(CCustomMenu)
    // メモ - ClassWizard はこの位置にマッピング用のマクロを追加または削除します。
  //}}AFX_MSG_MAP
END_MESSAGE_MAP()

このままでは CWnd のプロテクトメンバに引っかかるので
とりあえず、CWndをCCustomMenuとして通す。

CCustomMenu:Constructor();
CCustomMenu:Constructor();
CCustomMenu:Constructor();
CCustomMenu:Constructor();
CCustomMenu:Constructor();
CCustomMenu::MeasureItem(itemID=0000000B):
Warning: unknown WM_MEASUREITEM for menu item 0x4E80D03.
CCustomMenu::MeasureItem(itemID=0000000D):
CCustomMenu::DrawItem(itemID=0000000B):
CCustomMenu::DrawItem(itemID=04E80D03):
CCustomMenu::DrawItem(itemID=0000000D):
CCustomMenu:Destructor();
CCustomMenu:Destructor();
CCustomMenu:Destructor();
CCustomMenu:Destructor();
CCustomMenu:Destructor();

確かにそう出ますね。
でも、コンストラクタ、デストラクタが必要以上に呼ばれているねぇ。

void CCustomMenu::MeasureItem( LPMEASUREITEMSTRUCT lpMeasureItemStruct )
{
  TRACE("CCustomMenu::MeasureItem(itemID=%08X):\n",lpMeasureItemStruct->itemID);
}

TRACE() にブレークポイントを張り、これを呼ぶ元を探ります。

// Measure item implementation relies on unique control/menu IDs
void CWnd::OnMeasureItem(int /*nIDCtl*/, LPMEASUREITEMSTRUCT lpMeasureItemStruct)
{
  if (lpMeasureItemStruct->CtlType == ODT_MENU)
  {
    ASSERT(lpMeasureItemStruct->CtlID == 0);
    CMenu* pMenu;

    _AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData();
    if (pThreadState->m_hTrackingWindow == m_hWnd)
    {
      // start from popup
      pMenu = CMenu::FromHandle(pThreadState->m_hTrackingMenu);
    }
    else
    {
      // start from menubar
      pMenu = GetMenu();
    }

    pMenu = _AfxFindPopupMenuFromID(pMenu, lpMeasureItemStruct->itemID);
    if (pMenu != NULL)
      pMenu->MeasureItem(lpMeasureItemStruct);
    else
      TRACE1("Warning: unknown WM_MEASUREITEM for menu item 0x%04X.\n",
        lpMeasureItemStruct->itemID);
    >>>>....なるメッセージがでています。
    これだね!
  }
  else
  {
    CWnd* pChild = GetDescendantWindow(lpMeasureItemStruct->CtlID, TRUE);
    if (pChild != NULL && pChild->SendChildNotifyLastMsg())
      return;     // eaten by child
  }
  // not handled - do default
  Default();
}

_AfxFindPopupMenuFromID(pMenu, lpMeasureItemStruct->itemID);
が NULL を返しているのを突き止めたので、
ここにブレークポイントを張り、中に潜ります。


AFX_STATIC CMenu* AFXAPI _AfxFindPopupMenuFromID(CMenu* pMenu, UINT nID)
{
  ASSERT_VALID(pMenu);
  // walk through all items, looking for ID match
  UINT nItems = pMenu->GetMenuItemCount();
  for (int iItem = 0; iItem < (int)nItems; iItem++)
  {
    CMenu* pPopup = pMenu->GetSubMenu(iItem);
    if (pPopup != NULL)
    {
      // recurse to child popup
      pPopup = _AfxFindPopupMenuFromID(pPopup, nID);
      // check popups on this popup
      if (pPopup != NULL)
        return pPopup;
    }
    else if (pMenu->GetMenuItemID(iItem) == nID)
    {
      // it is a normal item inside our popup
      pMenu = CMenu::FromHandlePermanent(pMenu->m_hMenu);
      return pMenu;
    }
  }
  // not found
  return NULL;
}


コールスタック:
_AfxFindPopupMenuFromID(CMenu * 0x00344f98 {CCustomMenu}, unsigned int 7736337) line 1236
_AfxFindPopupMenuFromID(CMenu * 0x003451c0 {CCustomMenu}, unsigned int 7736337) line 1240 + 13 bytes
_AfxFindPopupMenuFromID(CMenu * 0x0012f944 {CCustomMenu}, unsigned int 7736337) line 1240 + 13 bytes
CWnd::OnMeasureItem(int 0, tagMEASUREITEMSTRUCT * 0x0012f690) line 1276 + 16 bytes
CWnd::OnWndMsg(unsigned int 44, unsigned int 0, long 1242768, long * 0x0012f53c) line 1930
CWnd::WindowProc(unsigned int 44, unsigned int 0, long 1242768) line 1585 + 30 bytes
AfxCallWndProc(CWnd * 0x0012fe74 {CDm02Dlg hWnd=0x00720156}, HWND__ * 0x00720156, unsigned int 44, unsigned int 0, long 1242768) line 215 + 26 bytes
AfxWndProc(HWND__ * 0x00720156, unsigned int 44, unsigned int 0, long 1242768) line 368

どうやら、サブメニューが存在していると
たとえ、現在の pMenu が目的のアイテムを示していても、
再帰してしまい、見つからないということになるようです。
Win98 だとここのロジックが違うのかな。
前述のように当方 Win2K なのですが。
どうなってます?

コールスタックを見る以上、
デフォルトで OnMeasureItem() -> MeasureItem() と呼ばれているので、
メッセージハンドラ:OnMeasureItem(), OnDrawItem() を追加し、
そこに処理を記述するのが筋だと思います。
どうでしょうか。
このようにすれば、DrawItem(), MeasureItem() を手書きで
追加する必要もなかったかと思うのですが。

編集 削除
moith  2003-12-12 10:07:28  No: 52816  IP: [192.*.*.*]

たしかに、OnMeausreItem/OnDrawItemメッセージハンドラでうまくいきます。

Win98でもコールスタックは同じような結果になります。

どうやら、CMenuの仮想関数 MeasureItem/DrawItemで処理するよりも
メニューのオーナーであるウィンドウでメッセージを捕らえたほうが
確実なようですね。

また、WinodwsXPでメニューの表示がおかしくなる現象ですが、
WinXPでは、初回のWM_DRAWITEMメッセージでは
DRAWITEMSTRUCT構造体のデバイスコンテキストでペン・ブラシなどを
選択するSelectObject()でエラーが起きていました。

このSelectObject()の戻り値(NULLになっていました)を、
DrawItemの終わりにデバイスコンテキストに戻していたので
ほかのメニューも表示がおかしくなっていたようです。

なぜ、Win98では初回のWM_DRAWITEMメッセージでSelectObject()が成功するのに、
WinXPでは失敗するのでしょう?

編集 削除
なーめ  2003-12-12 16:21:59  No: 52817  IP: [192.*.*.*]

>> WinodwsXPでメニューの表示がおかしくなる現象

パフォーマンスという意味ではいいかげんなコードですが、(^^;;
(CDC/CBitmap 関連は ダイアログのメンバにしておくべきだよね)
描画については、Win 2000 と Win XP Pro の両方で
ちゃんと動いてますよ。単なる BitBlt だけですが。

IDB_BITMAP1 は 48x48 のビットマップ。
これを16x16単位で切り出して使用しています。

void CCustomMenu::DrawItem( LPDRAWITEMSTRUCT lpDrawItemStruct )
{
  int cx = 0,cy = 0;
  switch( lpDrawItemStruct->itemID )
  {
    case 0x0B:
      cx = 16;
      break;
    case 0x0D:
      cy = 16;
      break;
    case 0x1F:
      cx = cy = 16;
      break;
    case 0x17:
      cx = 16; cy = 32;
      break;
  }

  HDC hDC = lpDrawItemStruct->hDC;
  CDC cdc;
  CDC cdcMem;
  cdc.Attach( hDC );
  CBitmap bmp,*pbmp;
  cdcMem.CreateCompatibleDC( &cdc );
  bmp.LoadBitmap( IDB_BITMAP1 );
  pbmp = cdcMem.SelectObject( &bmp );
  cdc.BitBlt( lpDrawItemStruct->rcItem.left,lpDrawItemStruct->rcItem.top,16,16,&cdcMem, cx,cy,SRCCOPY );
  cdcMem.SelectObject( pbmp );
  cdcMem.DeleteDC();
  bmp.DeleteObject();
  cdc.Detach();

  TRACE("CCustomMenu::DrawItem(itemID=%08X):\n",lpDrawItemStruct->itemID);
}

編集 削除
なーめ  2003-12-12 16:43:41  No: 52818  IP: [192.*.*.*]

↑この例、やはりポップアップのところだけ
MeasureItem がこないため、高さを32 にすると
うまくいきません(画像が重なります)。
やはり OnXXXX が筋ですかね。
MFC内部の方法に倣って、
OnDrawItem() から DrawItem() を呼び出せばよいかも。

話は変わりますけど、
実は私も今はまってまして、
構造化プログラミングという意味では、
メニューの構造とプログラムの構造は分けるべきだとおもい、
OnRButtonDown でメニューにサブメニューを差し込んで、
メニュー構造を構築するのではなく、
IDR_MENU なるリソースからツリーその構造を取り出して
CCustomMenu 側で描画しようと考えています。

ポイントはメニューリソースの構造を読み込むところなんですよ。
読み込んで、普通の(デフォルトの)動作はするんですが、
DrawItem が呼ばれないので、オーナドローにもセルフドローにも
ど.うにもならない。(^^;;

<ろ>

編集 削除
moith  2003-12-15 09:53:24  No: 52819  IP: [192.*.*.*]

XPで表示がおかしくなる現象については、デバイスコンテキストに
ペンやブラシを選択した時に失敗した場合は(SelectObject()の戻り値がNULL)、
これを元に戻さないようにすることで、回避できました。

Win98では、WM_DRAWITEMメッセージデバイスコンテキストの
SelectObject()が失敗することがないのに、
WinXPでは、なぜか、失敗することがあるようです。

「なーめ」さん、丁寧なレス、ありがとうございました。

編集 削除