Creating TreeView with nodes and checkboxes - c++

I have created TreeView like this:
TreeView=CreateWindowEx(0, WC_TREEVIEW, TEXT("Tree View"), WS_VISIBLE | WS_CHILD, 0, 0, 200, 500, hwnd, (HMENU)ID_TREE_VIEW, GetModuleHandle(NULL), NULL);
Now I added one item to it like shown on this website.
It all okay, but after hours and hours of googling I still didn't found answer to these questions:
How to add subitems (nodes)?
How to add checkbox on each item (how to determine if specified checkbox is checked)?

EDIT #4:
In response to OPs request, I have added an example that removes checkbox from a parent node.
THE PROBLEM IS THAT CHECKBOX STILL APPEARS WHEN USER SELECTS A NODE AND PRESSES SPACEBAR.
This question solves that problem.
EDIT #3:
I have added the code that creates a node that is already checked.
It is the second child bode in the WM_CREATE handler.
END OF EDIT
case WM_CREATE:
{
// this is your treeview
TreeView = CreateWindowEx(0, WC_TREEVIEW,
TEXT("Tree View"), WS_VISIBLE | WS_CHILD,
0, 0, 200, 500,
hwnd, (HMENU)ID_TREE_VIEW, GetModuleHandle(NULL), NULL);
/************ enable checkboxes **************/
DWORD dwStyle = GetWindowLong( TreeView , GWL_STYLE);
dwStyle |= TVS_CHECKBOXES;
SetWindowLongPtr( TreeView , GWL_STYLE, dwStyle );
/************ add items and subitems **********/
// add root item
TVINSERTSTRUCT tvis = {0};
tvis.item.mask = TVIF_TEXT;
tvis.item.pszText = L"This is root item";
tvis.hInsertAfter = TVI_LAST;
tvis.hParent = TVI_ROOT;
HTREEITEM hRootItem = reinterpret_cast<HTREEITEM>( SendMessage( TreeView ,
TVM_INSERTITEM, 0, reinterpret_cast<LPARAM>( &tvis ) ) );
// and here is an example of removing a checkbox
// from a specific item/subitem in case you ever need it
TVITEM tvi;
tvi.hItem = hRootItem ;
tvi.mask = TVIF_STATE;
tvi.stateMask = TVIS_STATEIMAGEMASK;
tvi.state = 0;
TreeView_SetItem( TreeView, &tvi );
// add firts subitem for the hTreeItem
memset( &tvis, 0, sizeof(TVINSERTSTRUCT) );
tvis.item.mask = TVIF_TEXT;
tvis.item.pszText = L"This is first subitem";
tvis.hInsertAfter = TVI_LAST;
tvis.hParent = hRootItem;
HTREEITEM hTreeSubItem1 = reinterpret_cast<HTREEITEM>( SendMessage( TreeView ,
TVM_INSERTITEM, 0, reinterpret_cast<LPARAM>( &tvis ) ) );
// now we insert second subitem for hRootItem
memset( &tvis, 0, sizeof(TVINSERTSTRUCT) );
tvis.item.mask = TVIF_TEXT | TVIF_STATE; // added extra flag
tvis.item.pszText = L"This is second subitem";
tvis.hInsertAfter = TVI_LAST;
tvis.hParent = hRootItem;
// for demonstration purposes let us check this node;
// to do that add the following code, and add the extra flag for
// mask member like above
tvis.item.stateMask = TVIS_STATEIMAGEMASK;
tvis.item.state = 2 << 12;
HTREEITEM hTreeSubItem2 = reinterpret_cast<HTREEITEM>( SendMessage( TreeView ,
TVM_INSERTITEM, 0, reinterpret_cast<LPARAM>( &tvis ) ) );
// let us expand the root node so we can see if checked state is really set
TreeView_Expand( TreeView, hRootItem, TVE_EXPAND );
}
return 0L;
EDIT #2:
Here Is the part that explains how to check if item is checked ( it now properly checks when you click on a checkbox and when you press spacebar! ) :
case WM_NOTIFY:
{
LPNMHDR lpnmh = (LPNMHDR) lParam;
if( lpnmh->idFrom == ID_TREE_VIEW ) // if this is our treeview control
{
switch( lpnmh->code ) // let us filter notifications
{
case TVN_KEYDOWN: // tree has keyboard focus and user pressed a key
{
LPNMTVKEYDOWN ptvkd = (LPNMTVKEYDOWN)lParam;
if( ptvkd->wVKey == VK_SPACE ) // if user pressed spacebar
{
// get the currently selected item
HTREEITEM ht = TreeView_GetSelection( ptvkd->hdr.hwndFrom );
// Prepare to test items state
TVITEM tvItem;
tvItem.mask = TVIF_HANDLE | TVIF_STATE;
tvItem.hItem = (HTREEITEM)ht;
tvItem.stateMask = TVIS_STATEIMAGEMASK;
// Request the information.
TreeView_GetItem( ptvkd->hdr.hwndFrom, &tvItem );
// Return zero if it's not checked, or nonzero otherwise.
if( (BOOL)(tvItem.state >> 12) - 1 )
MessageBox( hwnd, L"Not checked!", L"", MB_OK );
else
MessageBox( hwnd, L"Checked!", L"", MB_OK );
}
}
return 0L; // see the documentation for TVN_KEYDOWN
case NM_CLICK: // user clicked on a tree
{
TVHITTESTINFO ht = {0};
DWORD dwpos = GetMessagePos();
// include <windowsx.h> and <windows.h> header files
ht.pt.x = GET_X_LPARAM(dwpos);
ht.pt.y = GET_Y_LPARAM(dwpos);
MapWindowPoints( HWND_DESKTOP, lpnmh->hwndFrom, &ht.pt, 1 );
TreeView_HitTest(lpnmh->hwndFrom, &ht);
if(TVHT_ONITEMSTATEICON & ht.flags)
{
// Prepare to receive the desired information.
TVITEM tvItem;
tvItem.mask = TVIF_HANDLE | TVIF_STATE;
tvItem.hItem = (HTREEITEM)ht.hItem;
tvItem.stateMask = TVIS_STATEIMAGEMASK;
// Request the information.
TreeView_GetItem( lpnmh->hwndFrom, &tvItem );
// Return zero if it's not checked, or nonzero otherwise.
if( (BOOL)(tvItem.state >> 12) - 1 )
MessageBox( hwnd, L"Not checked!", L"", MB_OK );
else
MessageBox( hwnd, L"Checked!", L"", MB_OK );
}
}
default:
break;
}
}
}
break;
The relevant idea for proper testing when spacebar is pressed is handling of TVN_KEYDOWN message.
We use this message to get NMTVKEYDOWN structure filled, which will give us virtual key code of the pressed button and the HWND of the treeview that sent the notification.
Now we use TreeView_GetItem() macro to get the currently selected node and we check its state the same way we did when we did hit testing.
My only problem is concerning this part from the documentation for TVN_KEYDOWN:
Return value
If the wVKey member of lParam is a character key code, the character
will be used as part of an incremental search. Return nonzero to
exclude the character from the incremental search, or zero to include
the character in the search. For all other keys, the return value is
ignored.
I just do not know what to do with the return result so I have put 0L.
Important note: If you need to return value from dialog box procedure use something like this:
SetWindowLongPtr( hwnd, DWLP_MSGRESULT, (LONG_PTR)1 );
return TRUE;
see the remarks for Return value in this documentation and use SetWindowLongPtr instead of SetWindowLong so you can support both x32 and x64 versions of Windows.
That would be all. Hopefully you have your problem solved. If you need further help leave a comment.
END OF EDIT
I have never done checking if tree item is checked but I believe that accepted answer to this question is the way to go.
NOTE:
I would highly appreciate if there someone who can provide code snippet for showing how to determine if treeview node is checked or not.

Related

Window size of edit control/combobox is not properly adjusted when using MoveWindow or SetWindowPos

INTRODUCTION AND RELEVANT INFORMATION:
I am trying to implement listview control with editable items and subitems. Instead of regular listview look, items and subitems should have edit control, checkbox or combo box.
I am using raw WinAPI and C++. I am targeting Windows XP onwards.
MY EFFORTS TO SOLVE THE PROBLEM:
After researching here and on the Internet, I was able to only find examples in MFC. They all use LVN_BEGINLABELEDIT technique to implement this behavior.
Unfortunately I do not understand entirely this concept so I have decided to start from scratch ( I consider this also to be the best approach for improving ones programming skills ).
MY CONCEPT:
I have decided to catch NM_DBLCLK for listview and to get coordinates from there using ListView_GetItemRect or ListView_GetSubItemRect macro.
Then I would simply move the combobox/checkbox/edit control over corresponding item/subitem ( combobox/edit control/checkbox would be created as separate, hidden windows ).
After user finishes with input ( by pressing enter or changing focus ) I would simply hide the combobox/checkbox/edit control.
MY CURRENT RESULTS:
At the moment, I am stuck with the dimensions of combobox/edit control/checkbox not being the same as item/subitem dimensions, when moved above the item/subitem.
QUESTION:
Can my code example submitted below be improved to properly adjust combobox/edit control/checkbox window size to the size of the item/subitem? For now, I will only focus on this part of the problem, to keep this question as short as possible.
Here is the instruction for creating small application that illustrates the problem. Notice that I have tried to keep things as minimal as I could:
1.) Create default Win32 project in Visual Studio ( I use VS 2008 ).
2.) Add the following WM_CREATE handler to main window's procedure:
case WM_CREATE:
{
HWND hEdit = CreateWindowEx( 0,WC_EDIT, L"",
WS_CHILD | WS_VISIBLE | WS_BORDER | ES_CENTER | ES_AUTOHSCROLL,
250, 10, 100, 20, hWnd, (HMENU)1500, hInst, 0 );
HWND hComboBox = CreateWindowEx( 0,WC_COMBOBOX, L"",
WS_CHILD | WS_VISIBLE | WS_BORDER | CBS_DROPDOWNLIST,
100, 10, 100, 20, hWnd, (HMENU)1600, hInst, 0 );
HWND hwndLV = CreateWindowEx( 0, WC_LISTVIEW,
L"Editable Subitems",
WS_CHILD | WS_VISIBLE | WS_BORDER |
LVS_REPORT | LVS_SINGLESEL,
150, 100, 250, 150, hWnd, (HMENU)2000, hInst, 0 );
// set extended listview styles
ListView_SetExtendedListViewStyle( GetDlgItem( hWnd, 2000 ),
LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES | LVS_EX_DOUBLEBUFFER );
// add some columns
LVCOLUMN lvc = {0};
lvc.iSubItem = 0;
lvc.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM;
lvc.fmt = LVCFMT_LEFT;
for (long nIndex = 0; nIndex < 5; nIndex++ )
{
wchar_t txt[50];
swprintf_s( txt, 50, L"Column %d", nIndex + 1 );
lvc.iSubItem = nIndex;
lvc.cx = 60;
lvc.pszText = txt;
ListView_InsertColumn( GetDlgItem( hWnd,2000 ), nIndex, &lvc );
}
// add some items
LVITEM lvi;
lvi.mask = LVIF_TEXT;
lvi.iItem = 0;
for( lvi.iItem = 0; lvi.iItem < 10; lvi.iItem++ )
for (long nIndex = 0; nIndex < 5; nIndex++ )
{
wchar_t txt[50];
swprintf_s( txt, 50, L"Item %d%d", lvi.iItem + 1, nIndex + 1 );
lvi.iSubItem = nIndex;
lvi.pszText = txt;
if( ! nIndex ) // item
SendDlgItemMessage( hWnd, 2000,
LVM_INSERTITEM, 0,
reinterpret_cast<LPARAM>(&lvi) );
else // sub-item
SendDlgItemMessage( hWnd, 2000,
LVM_SETITEM, 0,
reinterpret_cast<LPARAM>(&lvi) );
}
}
return 0L;
3.) Add the following handler for WM_NOTIFY in main window's procedure:
case WM_NOTIFY:
{
if( ((LPNMHDR)lParam)->code == NM_DBLCLK )
{
switch( ((LPNMHDR)lParam)->idFrom )
{
case 2000: // remember, this was our listview's ID
{
LPNMITEMACTIVATE lpnmia = (LPNMITEMACTIVATE)lParam;
// SHIFT/ALT/CTRL/their combination, must not be pressed
if( ( lpnmia->uKeyFlags || 0 ) == 0 )
{
// this is where we store item/subitem rectangle
RECT rc = { 0, 0, 0, 0 };
if( (lpnmia->iSubItem) <= 0 ) // this is item so we must call ListView_GetItemRect
{
// this rectangle holds proper left coordinate
// since ListView_GetItemRect with LVIR_LABEL flag
// messes up rectangle's left cordinate
RECT rcHelp = { 0, 0, 0, 0 };
// this call gets the length of entire row
// but holds proper left coordinate
ListView_GetItemRect( lpnmia->hdr.hwndFrom,
lpnmia->iItem, &rcHelp, LVIR_BOUNDS );
// this call gets proper rectangle except for the left side
ListView_GetItemRect( lpnmia->hdr.hwndFrom,
lpnmia->iItem, &rc, LVIR_LABEL );
// now we can correct the left coordinate
rc.left = rcHelp.left;
}
else // it is subitem, so we must call ListView_GetSubItemRect
{
ListView_GetSubItemRect( lpnmia->hdr.hwndFrom,
lpnmia->iItem, lpnmia->iSubItem,
LVIR_BOUNDS, &rc );
}
// convert listview client coordinates to parent coordinates
// so edit control can be properly moved
POINT p;
p.x = rc.left;
p.y = rc.top;
ClientToScreen( lpnmia->hdr.hwndFrom, &p );
ScreenToClient( hWnd, &p );
MoveWindow( GetDlgItem( hWnd, 1500 ),
p.x, p.y,
rc.right - rc.left,
rc.bottom - rc.top, TRUE );
// set focus to our edit control
HWND previousWnd = SetFocus( GetDlgItem( hWnd, 1500 ) );
}
}
break;
default:
break;
}
}
}
break;
And this is the result I get:
You can clearly see that top and bottom border of the edit control are not drawn properly. As for combobox, the width is properly adjusted, but height remains the same.
I have tried substituting MoveWindow call with SetWindowPos but the result was the same.
After further tampering, I have found out that NMITEMACTIVATE bugs when returning the rectangle of a subitem, if listview doesn't have LVS_EX_FULLROWSELECT style set. You can see this by simply commenting out the part in my WM_CREATE handler where I set this style. Maybe I am doing something wrong and this "bug" may be caused by my code, but I don't see the problem.
EDITED on September, 17th 2014:
After testing the values for iItem and iSubItem members of NMITEMACTIVATE structure when listview doesn't have LVS_EX_FULLROWSELECT I can verify that the bug is not in my code. It always returns iItem to be 0, no matter which subitem I click. This explains the faulty behavior I got when removing this style.
If any further info is required please leave a comment and I will act as soon as possible.
Thank you for your time and efforts to help.
The issue you're facing is multi-faceted.
Firstly, the default font of the edit control is larger (higher) than that of the list-view. You can fix this one quite trivially, by first getting the font from the list-view and then setting it to the edit control. Doing this will then make the bottom border of the control visible.
The next issue is that the caret of the edit control needs a pixel above and below it, to ensure that the control doesn't have its borders interfered with. In addition to this 1 pixel of 'space' you then need another pixel for the border.
Added to this second point, the dimensions calculated by rc.right - rc.left and rc.bottom - rc.top are 1 pixel too small. Think of a rect that starts at 1,1 and extends to 2,2 - this is a rect of 4 pixels - 2 wide and 2 high. Simply subtracting the top/left from the bottom/right would give you a width/height of only 1 pixel each. To fix this, you need to add 1 to each of these subtractions.
Finally, since the caret is exactly the height of the 'client-area' of each item/sub-item, you need to make the edit control 2 pixels taller than the item/sub-item, and start 1 2 pixels higher than it does currently.
Here's the output I get when making the suggested changes:
And here's the changes/additions I made.
1. Get/Set the font. (inserted after creating the list-view and before setting its extended style)
HFONT lvFont = (HFONT)SendDlgItemMessage(hWnd, 2000, WM_GETFONT, 0, 0);
SendDlgItemMessage(hWnd, 1500, WM_SETFONT, (WPARAM)lvFont, TRUE);
2. Set the window position/size
MoveWindow( GetDlgItem( hWnd, 1500 ),
p.x, p.y-2,
1+ rc.right - rc.left,
1+ 2 + rc.bottom - rc.top, TRUE );
Finally, contrast this against the original output from your code:
UPDATE:
Here's a snapshot of the appearance when the built-in label editing functionality is used (LVS_EDITLABELS style)

Win32 Toolbar Dropdown Button Message Handling

I have a toolbar to a window in my application. I have read MSDN docs and can handle command and notification messages. My problem is with a dropdown menu attached to a button on the toolbar. The menu items open modal dialog boxes and wait for user to finalize settings changes. After the user clicks 'ok' or 'cancel' focus returns to the main window, however, the toolbar is still under the impression that the left mouse button is down so everytime i drag the mouse over the toolbar button that was clicked the button takes the 'button is checked' state and 'appears' to be pressed.
Is the problem related to the tracking of mouse events?
Here is notification message handling within window proc:
case WM_NOTIFY:
{
LPNMHDR lpnm = ( ( LPNMHDR )lParam );
LPNMTOOLBAR lpnmTB = ( ( LPNMTOOLBAR )lParam );
switch( lpnm->code )
{
case TBN_DROPDOWN:
{
// Get the coordinates of the button.
RECT rc;
SendMessage( lpnmTB->hdr.hwndFrom, TB_GETRECT, ( WPARAM )lpnmTB->iItem, ( LPARAM )&rc );
// Convert to screen coordinates.
MapWindowPoints( lpnmTB->hdr.hwndFrom, HWND_DESKTOP, ( LPPOINT )&rc, 2 );
// handle dropdown menus
return HandleTexEditDropdown( hWnd, lpnmTB, rc );
}
default:
break;
}
break;
}
Here is handle TexEditDropdown():
LRESULT CALLBACK CWindowManager::HandleTexEditDropdown( HWND hWnd, LPNMTOOLBAR lpnm, RECT &rc )
{
HRESULT hr = S_OK;
switch( lpnm->iItem )
{
case IDM_EDITTEXTURE_FILL:
{
// Get the menu.
HMENU hMenuLoaded = LoadMenu( GetModuleHandle( NULL ), MAKEINTRESOURCE( IDR_BUCKETFILL ) );
// Get the submenu for the first menu item.
HMENU hPopupMenu = GetSubMenu( hMenuLoaded, 0 );
// Set up the pop-up menu.
// In case the toolbar is too close to the bottom of the screen,
// set rcExclude equal to the button rectangle and the menu will appear above
// the button, and not below it.
TPMPARAMS tpm;
tpm.cbSize = sizeof( TPMPARAMS );
tpm.rcExclude = rc;
// Show the menu and wait for input.
// If the user selects an item, its WM_COMMAND is sent.
INT nCmd = TrackPopupMenuEx( hPopupMenu,
TPM_LEFTALIGN | TPM_LEFTBUTTON | TPM_VERTICAL | TPM_RETURNCMD,
rc.left, rc.bottom, hWnd, &tpm );
DestroyMenu( hMenuLoaded );
switch( nCmd )
{
case IDM_BUCKETFILLSETTINGS:
DialogBox( GetModuleHandle( NULL ),
MAKEINTRESOURCE( IDD_TEXEDITBUCKETFILL ),
hWnd,
( DLGPROC )TexEditSettingsProc );
break;
default:
return 0;
}
SendMessage( ( HWND )lpnm->hdr.hwndFrom, TB_MARKBUTTON, lpnm->iItem, MAKELPARAM( FALSE, 0 ) );
UpdateWindow( hWnd );
SetStateChange();
return 1;
}
case IDM_EDITTEXTURE_RECOVER:
{
// Get the menu.
HMENU hMenuLoaded = LoadMenu( GetModuleHandle( NULL ), MAKEINTRESOURCE( IDR_RECOVER ) );
// Get the submenu for the first menu item.
HMENU hPopupMenu = GetSubMenu( hMenuLoaded, 0 );
// Set up the pop-up menu.
// In case the toolbar is too close to the bottom of the screen,
// set rcExclude equal to the button rectangle and the menu will appear above
// the button, and not below it.
TPMPARAMS tpm;
tpm.cbSize = sizeof( TPMPARAMS );
tpm.rcExclude = rc;
// Show the menu and wait for input.
// If the user selects an item, its WM_COMMAND is sent.
INT nCmd = TrackPopupMenuEx( hPopupMenu,
TPM_LEFTALIGN | TPM_LEFTBUTTON | TPM_VERTICAL | TPM_RETURNCMD,
rc.left, rc.bottom, hWnd, &tpm );
DestroyMenu( hMenuLoaded );
switch( nCmd )
{
case IDM_RECOVERFILE:
{
WCHAR wcs[ MAX_PATH ] = L"",
wcsFiletype[ MAX_PATH ] = L"",
wcsFilename[ MAX_PATH ] = L"";
D3DXIMAGE_INFO info;
// get currently loaded image info
if( FAILED( hr = CTextureEditor::GetImageInfo( &info ) ) )
{
DebugStringDX( ClassName, "Failed to CTextureEditor::GetImageInfo() at AuxiliaryViewportProcess()", __LINE__, hr );
break;
}
if( !CTextureEditor::CatImageFileType( info.ImageFileFormat, wcsFiletype ) )
{
DebugStringDX( ClassName, "Invalid image filetype at AuxiliaryViewportProcess()", __LINE__, hr );
break;
}
wsprintf( wcs, L"GDEImage Filetype (*%s)", wcsFiletype );
memcpy( &wcs[ 26 ], L"*", sizeof( WCHAR ) );
memcpy( &wcs[ 27 ], wcsFiletype, 4 * sizeof( WCHAR ) );
// Declare and initialize an OPENFILENAME struct to use for OpenFile Dialog
OPENFILENAME ofn;
ZeroMemory( ( void* )&ofn, sizeof( OPENFILENAME ) );
ofn.lStructSize = sizeof( ofn ); // SEE NOTE BELOW
ofn.hwndOwner = hWnd;
ofn.lpstrTitle = L"Recover Image From File";
ofn.lpstrFilter = wcs;
ofn.lpstrFile = wcsFilename;
ofn.nMaxFile = MAX_PATH;
ofn.Flags = OFN_EXPLORER | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY;
ofn.lpstrDefExt = L"image file";
ofn.lpstrInitialDir = app.GetBinDirectory();
// OpenFile Dialog
if( GetOpenFileName( &ofn ) )
{
if( FAILED( hr = CTextureEditor::Recover( 0, wcsFilename ) ) )
{
DebugStringDX( ClassName, "Failed to CTextureEditor::Recover() at AuxiliaryViewportProc()", __LINE__, hr );
break;
}
}
break;
}
default:
return 0;
}
SendMessage( ( HWND )lpnm->hdr.hwndFrom, TB_MARKBUTTON, lpnm->iItem, MAKELPARAM( FALSE, 0 ) );
UpdateWindow( hWnd );
SetStateChange();
return 1;
}
default:
return 0;
}
return 0;
}
I think the returned value for TBN_DROPDOWN is not correct -- you are returning 1 which maps to TBDDRET_NODEFAULT meaning "The drop-down was not handled", you'll need to return TBDDRET_DEFAULT or TBDDRET_TREATPRESSED.

Why do I not receive the WM_MENUCHAR message?

I implemented an IContextMenu3 interface and I am trying to capture keystrokes for my own custom accelerator. The problem is that if i hover over my submenu in the root menu I dont get any WM_MENUCHAR messages whereas if I hover over a submenu that is inside one of my submenu's then I do.
I know that the WM_INITMENUPOPUP message only gets sent if there is a child. The WM_MENUCHARhas the caveat that no accelerators are bound to the key. I know this caveat to be held since when I press a key, I get the distinctive 'no-accelerator' beep.
Is there another caveat that I am not aware of?
This is the smallest code I can get that reproduces the problem:
HRESULT CFolderViewImplContextMenu::QueryContextMenu(HMENU hmenu, UINT uMenuIndex, UINT idCmdFirst, UINT idCmdLast, UINT /* uFlags */)
{
UINT uID = idCmdFirst;
HMENU hSubmenu = CreatePopupMenu();
MENUITEMINFO mii = { 0 };
mii.cbSize = sizeof(MENUITEMINFO);
mii.fMask = MIIM_SUBMENU | MIIM_ID | MIIM_STRING;
mii.dwTypeData = str_toWchar("test");
mii.wID = uID++;
mii.hSubMenu = hSubmenu;
InsertMenuItem ( hmenu, 0, TRUE, &mii );
InsertMenu ( hSubmenu, 0, MF_BYPOSITION, uID++, L"&Notepad" );
InsertMenu ( hSubmenu, 1, MF_BYPOSITION , uID++, L"&Internet Explorer" );
HMENU hSubmenu2 = CreatePopupMenu();
MENUITEMINFO mii2 = {0};
mii2.cbSize = sizeof(MENUITEMINFO);
mii2.fMask = MIIM_ID | MIIM_TYPE | MIIM_SUBMENU;
mii2.fType = MFT_OWNERDRAW;
mii2.wID = uID++;
mii2.hSubMenu = hSubmenu2;
InsertMenuItem ( hSubmenu, 0, TRUE, &mii2 );
InsertMenuA ( hSubmenu2, 0, MF_BYPOSITION, uID++, "");
return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, uID - idCmdFirst );
}
WM_MENUCHAR is forwarded only for submenus. (It can't be forwarded for top-level menu items because that would be a Catch-22. You want to forward it to the context menu handler for the menu item that the the key corresponds to, but you can't do that until you have the answer to WM_MENUCHAR!)
How about this:
If you are handling IContextMenu3 messages, consequently WM_DRAWITEM, you can use WindowFromDC() to get menu window's HWND from WM_DRAWITEM, then subclass it and catch WM_KEYDOWN or do whatever you like with it.
I tried it (was doing some other stuff than this) and it works.
The problem is the first item in the submenu. If the first item in the submenu is also a submenu, then the message doesnt get passed. So I instead put a normal item there.

WM_COMMAND not being received by Dialog Procedure

I am trying to use a context menu I created for a Dialog Box, but the WM_COMMAND message which is supposed to be sent when I click a menu item is not reaching the Dialog Procedure.
I put an if statement in the message loop to check for WM_COMMAND, and it is being registered but not being sent to my Dialog Procedure.
Yes, I am using if( !IsDialogMessage( hwndListDialog, &msg ) ) to call TranslateMessage and DispatchMessage.
This is how I initialised my context menu:
listitemmenu = CreatePopupMenu();
listmenuitem1.cbSize = sizeof( menuitem1 );
listmenuitem1.fMask = MIIM_TYPE;
listmenuitem1.fType = MFT_STRING;
listmenuitem1.hSubMenu = NULL;
listmenuitem1.dwTypeData = "Copy Imgur Link";
InsertMenuItem( listitemmenu, 0, true, &listmenuitem1 ); //insert menu item
listmenuitem2.cbSize = sizeof( menuitem1 );
listmenuitem2.fMask = MIIM_TYPE;
listmenuitem2.fType = MFT_STRING;
listmenuitem2.hSubMenu = NULL;
listmenuitem2.dwTypeData = "Copy Imgur Delete Link";
InsertMenuItem( listitemmenu, 1, true, &listmenuitem2 ); //insert menu item
listmenuitem3.cbSize = sizeof( menuitem1 );
listmenuitem3.fMask = MIIM_TYPE;
listmenuitem3.fType = MFT_STRING;
listmenuitem3.hSubMenu = NULL;
listmenuitem3.dwTypeData = "Rename";
InsertMenuItem( listitemmenu, 2, true, &listmenuitem3 ); //insert menu item
listmenuitem4.cbSize = sizeof( menuitem1 );
listmenuitem4.fMask = MIIM_TYPE;
listmenuitem4.fType = MFT_STRING;
listmenuitem4.hSubMenu = NULL;
listmenuitem4.dwTypeData = "Remove from list";
InsertMenuItem( listitemmenu, 3, true, &listmenuitem4 ); //insert menu item
This is how I track the menu:
TrackPopupMenuEx( listitemmenu, TPM_LEFTBUTTON | TPM_NOANIMATION | TPM_HORIZONTAL | TPM_VERTICAL, cpos.x, cpos.y, ListControl, NULL );
This is the switch case that I am trying to use to interact with my menu:
case WM_COMMAND:
switch( LOWORD(lParam) )
{
case 0:
printf("copy link");
break;
case 1:
printf("copy deletion link");
break;
case 2:
printf("Rename");
break;
case 3:
printf("Remove");
break;
}
break;
From the documentation for TrackPopupMenuEx:
hwnd [in]
Type: HWND
A handle to the window that owns the shortcut menu. This window receives all messages from the menu.
You passed ListControl as the hwnd parameter, so the WM_COMMAND is going to that window, not your dialog box. If you want the WM_COMMAND to go to your dialog box, then pass your dialog box as the hwnd.
I believe (though don't remember exactly, this was a long time ago...) the problem is that you don't specify ID for your menu items. The WM_COMMAND that's expected to arrive at your window procedure should carry lParam that's equal to the selected item ID. However if you don't specify the item ID it gets IDC_STATIC by default, which is an invalid value.
Try the following:
listmenuitem1.cbSize = sizeof( menuitem1 );
listmenuitem1.fMask = MIIM_TYPE | MIIM_ID;
// ...
listmenuitem1.wID = /* the ID you want to see in processing WM_COMMAND */;
// ...
InsertMenuItem( listitemmenu, 0, true, &listmenuitem1 ); //insert menu item
P.S. You may also specify TPM_RETURNCMD in TrackPopupMenuEx, so that the return value will be the selected item ID
I've come up with a solution, but it's sort of a ghetto workaround.
Rather than processing WM_COMMAND in the Dialog Proc where it cannot be received, I am processing it in the message loop.
if( !IsDialogMessage( hwndListDialog, &msg ) )
{
TranslateMessage(&msg);
DispatchMessage(&msg);
} else {
if( WM_COMMAND == msg.message )
{
switch( LOWORD( msg.wParam ) )
{
case 200:
break;
case 201:
break;
case 202:
break;
case 203:
break;
}
}
}
I still hope someone can help me get WM_COMMAND inside my Dialog Procedure (especially since WM_COMMAND from other dialog controls are reaching the Dialog Proc), but this helps me move forward for the moment.
Shouldn't your switch be with wParam and not lParam when handling WM_COMMAND? For menus, the high word of wParm is 0 and the menu ID is in the low word of wParam. You should also replace ListControl with the handle of your dialog to receive message in TrackPopupMenu as others said.

MFC, File New with dialog to prompt for paramters and multiple Doc Types?

I have really tough question.
I have a program with six different kinds of MDI Windows.
There needs to be a dialog prompt to prompt the graphic dimensions (Height, Width) and then create six different kinds of MDI Window depending on what kind of graphics.
Question is:
I already got 6 MDI types working (to some extent).
What do I do to create a customized File|New to accommodate the need for a custom dialog screen?
For example, in MDI, there's a generic File|New dialog box. I wish to customize that in MFC.
First of all you'll need to override the CDocument function that handles opening of new files; it's probably one of CDocument::OnNewDocument or CDocument::OnOpenDocument. I've also done this in the past by adding the following message map to CMainFrame, not sure which one the correct method is:
ON_COMMAND( ID_FILE_OPEN, &CMainFrame::OnFileOpen )
Then, to display a customized New File dialog, use the WINAPI GetOpenFileName function. Note that you must use this function, MFC's CFileDialog will not work. Here's a code snippet from the CMainFrame::OnFileOpen function I listed above in the message map:
/**
* Hook procedure for OPENFILENAME structure. This function processes messages from the custom
* controls added to the standard "Open File" dialog.
*
* #param [in] hdlg
* handle of the hdlg.
* #param [in] uiMsg
* message describing the user interface.
* #param [in] wParam
* the wParam field of the message.
* #param [in] lParam
* the lParam field of the message.
*
* #return If the return value is zero, the default dialog box procedure processes the message;
* otherwise it ignores the message.
*/
UINT_PTR CALLBACK OpenHexFileHook( HWND hdlg, UINT uiMsg, WPARAM wParam, LPARAM lParam )
{
(void)(wParam);
if( !::IsWindow( hdlg ) ) {
return 0;
}
switch( uiMsg ) {
case WM_NOTIFY:
/* Sent when an event occurs or the custom controls require information */
{
LPOFNOTIFY pOfnNotify = (LPOFNOTIFY)lParam;
if( pOfnNotify == NULL ) {
return 0;
}
CWinApp *app = AfxGetApp();
switch( pOfnNotify->hdr.code ) {
case CDN_INITDONE:
/* Sent when the system has finished arranging the controls in the dialog box */
{
CString rv;
LRESULT idx;
/* Configure the Hex file format ComboBox */
HWND hHexFmt = GetDlgItem( hdlg, IDC_HEX_FILE_FORMAT );
ASSERT( hHexFmt );
/* Clear existing content */
::SendMessage( hHexFmt, CB_RESETCONTENT, 0, 0 );
/* Add new items to the list */
::SendMessage( hHexFmt, CB_ADDSTRING, 0, (LPARAM)_T("INTEL 32") );
::SendMessage( hHexFmt, CB_ADDSTRING, 0, (LPARAM)_T("INTEL 16") );
::SendMessage( hHexFmt, CB_ADDSTRING, 0, (LPARAM)_T("INTEL 8") );
/* Read user's last selection from registry */
rv = app->GetProfileString( _T("Settings"), _T("HexFormat"), _T("INTEL 32") );
idx = ::SendMessage( hHexFmt, CB_FINDSTRINGEXACT, (WPARAM)-1, (LPARAM)((LPCTSTR)rv) );
if( idx == CB_ERR ) {
/* On error select the first item */
idx = 0;
}
/* Set current selection to the previously selected item's index */
::SendMessage( hHexFmt, CB_SETCURSEL, idx, 0 );
/* Configure the Bytes per address ComboBox */
HWND hBytePerLoc = GetDlgItem( hdlg, IDC_BYTES_PER_ADDR );
ASSERT( hBytePerLoc );
/* Clear existing content */
::SendMessage( hBytePerLoc, CB_RESETCONTENT, 0, 0 );
/* Add new items to the list */
::SendMessage( hBytePerLoc, CB_ADDSTRING, 0, (LPARAM)_T("1") );
::SendMessage( hBytePerLoc, CB_ADDSTRING, 0, (LPARAM)_T("2") );
::SendMessage( hBytePerLoc, CB_ADDSTRING, 0, (LPARAM)_T("4") );
/* Read user's last selection from registry */
rv = app->GetProfileString( _T("Settings"), _T("BytesPerAddr"), _T("1") );
idx = ::SendMessage( hBytePerLoc, CB_FINDSTRINGEXACT, (WPARAM)-1, (LPARAM)((LPCTSTR)rv) );
if( idx == CB_ERR ) {
/* On error select the first item */
idx = 0;
}
/* Set current selection to the previously selected item's index */
::SendMessage( hBytePerLoc, CB_SETCURSEL, idx, 0 );
break;
}
case CDN_FILEOK:
/* Sent when the user specifies a file name and clicks the OK button */
{
/* Save user selection for the Hex file format ComboBox */
HWND hHexFmt = GetDlgItem( hdlg, IDC_HEX_FILE_FORMAT );
ASSERT( hHexFmt );
/* Get current selection's index */
LRESULT idx = ::SendMessage( hHexFmt, CB_GETCURSEL, 0, 0 );
if( idx != CB_ERR ) {
/* Get current selection's text length */
LRESULT len1 = ::SendMessage( hHexFmt, CB_GETLBTEXTLEN, idx, 0 );
if( len1 != CB_ERR ) {
TCHAR *text = new TCHAR[len1 + 1];
/* Get current selection's text */
LRESULT len2 = ::SendMessage( hHexFmt, CB_GETLBTEXT, idx, (LPARAM)text );
if( len1 == len2 ) {
/* Write string to registry */
app->WriteProfileString( _T("Settings"), _T("HexFormat"), text );
}
delete[] text;
}
}
/* Save user selection for the Bytes per address ComboBox */
HWND hBytePerLoc = GetDlgItem( hdlg, IDC_BYTES_PER_ADDR );
ASSERT( hBytePerLoc );
/* Get current selection's index */
idx = ::SendMessage( hBytePerLoc, CB_GETCURSEL, 0, 0 );
if( idx != CB_ERR ) {
/* Get current selection's text length */
LRESULT len1 = ::SendMessage( hBytePerLoc, CB_GETLBTEXTLEN, idx, 0 );
if( len1 != CB_ERR ) {
TCHAR *text = new TCHAR[len1 + 1];
/* Get current selection's text */
LRESULT len2 = ::SendMessage( hBytePerLoc, CB_GETLBTEXT, idx, (LPARAM)text );
if( len1 == len2 ) {
/* Write string to registry */
app->WriteProfileString( _T("Settings"), _T("BytesPerAddr"), text );
}
delete[] text;
}
}
break;
}
default:
/* Do nothing */
break;
}
break;
}
default:
/* Do nothing */
break;
}
return 0;
}
void CMainFrame::OnFileOpen()
{
std::basic_string<TCHAR> filterSpec;
/* Use *.hex filter */
filterSpec = _T("Hex Files (*.hex)");
filterSpec += (std::basic_string<TCHAR>::value_type)'\0';
filterSpec += _T("*.hex");
filterSpec += (std::basic_string<TCHAR>::value_type)'\0';
OPENFILENAME ofn = { sizeof(ofn) };
TCHAR filePath[MAX_PATH] = { 0 };
TCHAR fileTitle[MAX_PATH] = { 0 };
ofn.hwndOwner = AfxGetMainWnd()->GetSafeHwnd();
ofn.hInstance = AfxGetInstanceHandle();
ofn.lpstrFilter = filterSpec.c_str();
ofn.lpstrCustomFilter = NULL;
ofn.nMaxCustFilter = 0;
ofn.nFilterIndex = 1;
ofn.lpstrFile = filePath;
ofn.nMaxFile = sizeof(filePath) / sizeof(TCHAR);
ofn.lpstrFileTitle = fileTitle;
ofn.nMaxFileTitle = sizeof(fileTitle) / sizeof(TCHAR);
ofn.lpstrInitialDir = NULL;
ofn.lpstrTitle = _T("Select Intel HEX file");
ofn.Flags = OFN_ENABLETEMPLATE | OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST |
OFN_EXPLORER | OFN_ENABLESIZING | OFN_DONTADDTORECENT |
OFN_HIDEREADONLY | OFN_ENABLEHOOK;
ofn.lpstrDefExt = _T("hex");
ofn.lCustData = NULL;
ofn.lpfnHook = OpenHexFileHook;
ofn.lpTemplateName = MAKEINTRESOURCE(IDD_HEX_FILE_OPEN);
ofn.FlagsEx = 0;
if( GetOpenFileName( &ofn ) != false ) {
AfxGetApp()->m_pDocManager->OpenDocumentFile( ofn.lpstrFile );
SetTitle( ofn.lpstrFileTitle );
}
}
In that project I added 2 ComboBoxes to the standard File Open dialog. These MSDN docs describe how to customize the standard dialogs. This is the resource file (.rc2) customization that I used:
//
// HexViewer.RC2 - resources Microsoft Visual C++ does not edit directly
//
#ifdef APSTUDIO_INVOKED
#error this file is not editable by Microsoft Visual C++
#endif //APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
// Add manually edited resources here...
/////////////////////////////////////////////////////////////////////////////
//
// File Open Dialog Template
//
IDD_HEX_FILE_OPEN DIALOGEX 0, 0, 370, 40
STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS
EXSTYLE WS_EX_CONTROLPARENT
FONT 8, "MS Shell Dlg", 0, 0, 0x1
BEGIN
LTEXT "&HEX file format:", IDC_HEX_FILE_FORMAT_TEXT, 67, 2, 58, 8, SS_NOTIFY
COMBOBOX IDC_HEX_FILE_FORMAT, 130, 0, 164, 100,
CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP
LTEXT "&Bytes per address:", IDC_BYTES_PER_ADDR_TEXT, 67, 20, 63, 8, SS_NOTIFY
COMBOBOX IDC_BYTES_PER_ADDR, 130, 18, 164, 100,
CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP
END
/////////////////////////////////////////////////////////////////////////////
Note that this method of customization is recommended only if you're programming for Windows XP and older versions. From Vista onwards MS recommends using the Common Item Dialog API for customizing, but I've never used that.