MFC OnMeasureItem & OnDrawItem in menu of MDI multidoc application - c++

(Update, see original question below)
After doing a bit of digging, I'm basically trying to understand the following; In the context of an MDI application, if a menu (which is associated with a specific CChildWnd) has an MF_OWNERDRAW, why are the ON_WM_MEASUREITEM and ON_WM_DRAWITEM events send to the CMainWnd instead of the CChildWnd?
In my InitInstance, the document template is registered and the associated menu is modified to add the MF_OWNERDRAW:
BOOL CMyApp::InitInstance()
{
// ...
CMultiDocTemplate* pDocTemplate;
pDocTemplate = new CMultiDocTemplate(
IDR_CHILDFRAME,
RUNTIME_CLASS(CFooDoc),
RUNTIME_CLASS(CFooWnd),
RUNTIME_CLASS(CFooView)
);
if (pDocTemplate->m_hMenuShared != NULL) {
CMenu* pMenu = CMenu::FromHandle(pDocTemplate->m_hMenuShared);
// Add MF_ONWERDRAW to the items that need it.
pMenu->ModifyMenu([item_id], MF_BYCOMMAND | MF_OWNERDRAW, [item_id]);
}
AddDocTemplate(pDocTemplate);
// ...
}
So, once the document template is registered, the menu associated with the document/frame is modified to add the MF_ONWERDRAW flag to each of the required items (the color selection items in my case).
However, why are the OnMeasureItem and OnDrawItem events send to the CMainWnd and not the CFooWnd? And how can I direct the events to the CFooWnd instead?
The reason I'am asking, if I have 5 different types of documents in my MDI application, each needing custom menus, then the CMainWnd basically becomes a mess of message handling. The logical place for the custom menu logic is in the CChildWnd, not the CMainWnd.
Original question:
I'm doing some work on a very old application (MFC 4.2) and I'm running into a problem with drawing in a menu item.
The original application has a menu to select a color and it actually draws the colors in the menu when opened so it easier for the user to select the color.
The behavior for this implemented in CMainWnd using the OnMeasureItem and the OnDrawItem.
class CMainWnd : public CMDIFrameWnd
{
DECLARE_DYNCREATE(CMainWnd)
protected:
afx_msg void OnMeasureItem(int, LPMEASUREITEMSTRUCT);
afx_msg void OnDrawItem(int, LPDRAWITEMSTRUCT);
DECLARE_MESSAGE_MAP()
};
Then, in the implementation (omitted bits and pieces for brevity):
BEGIN_MESSAGE_MAP(CMainWnd, CMDIFrameWnd)
ON_WM_MEASUREITEM()
ON_WM_DRAWITEM()
END_MESSAGE_MAP()
void CMainWnd::OnMeasureItem(int nIDCtl, LPMEASUREITEMSTRUCT lpmis)
{
lpmis->itemWidth = ::GetSystemMetrics(SM_CYMENU) * 4;
lpmis->itemHeight = ::GetSystemMetrics(SM_CYMENU) * 1;
}
void CMainWnd::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpdis)
{
CDC dc;
dc.Attach(lpdis->hDC);
CBrush* pBrush;
// draw the hover/selection rectangle
pBrush = new CBrush(::GetSysColor((lpdis->itemState & ODS_SELECTED) ? COLOR_HIGHLIGHT :
COLOR_MENU));
dc.FrameRect(&(lpdis->rcItem), pBrush);
delete pBrush;
// load a checkbox icon into a bitmap
BITMAP bm;
CBitmap bitmap;
bitmap.LoadOEMBitmap(OBM_CHECK);
bitmap.GetObject(sizeof(bm), &bm);
// if color/item selected then draw the checkbox
if (lpdis->itemState & ODS_CHECKED) {
CDC dcMem;
dcMem.CreateCompatibleDC(&dc);
CBitmap* pOldBitmap = dcMem.SelectObject(&bitmap);
dc.BitBlt(
lpdis->rcItem.left + 4,
lpdis->rcItem.top + (((lpdis->rcItem.bottom - lpdis->rcItem.top) - bm.bmHeight) / bm.bmWidth,
bm.bmHeight,
&dcMem,
0,
0,
SRCCOPY
);
dcMem.SelectObject(pOldBitmap);
}
// draw the actual color bar
pBrush = new CBrush(CPaintDoc::m_crColors[lpdis->itemID - ID_COLOR_BLACK]);
CRect rect = lpdis->rcItem;
rect.DeflateRect(6, 4);
rect.left += bm.bmWidth;
dc.FillRect(rect, pBrush);
delete pBrush;
dc.Detach();
}
What the OnDrawItem does is; it draws a horizontal color bar with a color, prefixed by a check icon if that color is selected and the menu item being hovered over is highlighted by a box being drawn around it.
However, since I'm turning this application into a Multidoc application and I don't really feel that this logic should be in the CMainWnd (since none of the other documents will have this type of menu), but that it should be part of the CChildWnd (which inherits from CMDIChildWnd).
But when I move this logic to that class, when I run the application, I get following message in the console logger:
Warning: unknown WM_MEASUREITEM for menu item 0x0082.
And none of the custom menu behavior seems to work.
so, the question is; How can move the custom behavior of a menu into the frame class of an MDI document rather than having it located in the application main frame?

I figured out a work around. Not ideal but I can understand that this is a quirk in the framework, i.e. the menu seems to be part of the MainWnd so from a technical point of view, that is where the ON_WM_MEASUREITEM and ON_WM_DRAWITEM would be handled.
Anyhow, my work around. Basically capture the events in the MainWnd and then delegate the behaviour to the ChildWnd. The trick here (I guess) is to figure out what ChildWnd to delegate to since in an MDI application there can be any number of different ChildWnd's (each with their own Document and View types).
The work around:
void CMainWnd::OnMeasureItem(int nIDCtl, LPMEASUREITEMSTRUCT lpmis)
{
CMDIChildWnd* pActiveWnd = MDIGetActive();
if(pActiveWnd && pActiveWnd->IsWindowVisible())
{
if(pActiveWnd->IsKindOf(RUNTIME_CLASS(CMyChildWnd))) {
CMyChildWnd* pMyChildWnd = (CMyChildWnd*)pActiveWnd;
CMyChildWnd->DoMeasureItem(nIDCtl, lpmis);
}
}
}
void CMainWnd::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpdis)
{
CMDIChildWnd* pActiveWnd = MDIGetActive();
if(pActiveWnd && pActiveWnd->IsWindowVisible())
{
if(pActiveWnd->IsKindOf(RUNTIME_CLASS(CMyChildWnd))) {
CMyChildWnd* pMyChildWnd = (CMyChildWnd*)pActiveWnd;
CMyChildWnd->DoDrawItem(nIDCtl, lpdis);
}
}
}
Pretty straight forward, in the context of the MainWnd, get a pointer to the active MDI ChildWnd, check if it is active, then check the type by using IsKindOf and RUNTIME_CLASS and if so, voila, delegate the behavior to the ChildWnd. To DoMeasureItem and the DoDrawItem are just public methods implemented on the ChildWnd (see question for details).

Related

CTabCtrl ItemAction & ItemState

I’ve created my own CXTabCtrl that extends CTabCtrl and override the DrawItem Function.
During the phase of rewriting the DrawItem Function, I wasn’t able to differentiate between this two states of CTabCtrl Item:
CTabCtrl item is selected and have focus.
CTabctrl item is selected but doesn’t have focus.
By focus I mean the Focus rectangle is not drawing. Here are two images that will help identify the two states:
Here’s the DrawItem current code, in which I can detect the selected states, but still Unable to detect the focus states.
Here’s a part of the DrawItem current code, in which I can detect the selected states, but still Unable to detect the focus states.
void CXtabCtrl::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC);
BOOL bFontSuccess = FALSE;
CFont* def_font = NULL;
CFont font_italic;
TC_ITEM tci;
CRect rect(lpDrawItemStruct->rcItem);
wchar_t szTabText[256];
wmemset(szTabText,_T('\0'),256);
RECT rectComplet;
GetClientRect(&rectComplet);
CBrush brtmp(ColorCategoryBackgroundTop);
int nbItem = GetItemCount();
tci.mask = TCIF_TEXT;
tci.pszText = szTabText;
tci.cchTextMax = sizeof(szTabText) -1;
GetItem(lpDrawItemStruct->itemID, &tci);
BOOL bSelect = (lpDrawItemStruct->itemState & ODS_SELECTED) &&
(lpDrawItemStruct->itemAction & (ODA_SELECT | ODA_DRAWENTIRE));
BOOL bfocus = (lpDrawItemStruct->itemState & ODS_FOCUS) &&
(lpDrawItemStruct->itemAction & (ODA_FOCUS | ODA_DRAWENTIRE));
if (bSelect)//Draw In a Specific Way
if (bFocus) //Draw In a Specific Way
}
So, I would be grateful if someone can describe the proper way to detect the two states of a CTabCtrl Item “Selected & Focused”, “Selected & But not focused”
For a standard tab control, the UI will not always draw the focus rectangle. To see the focus rectangle, the tab control must have WS_TABSTOP flag.
The focus rectangle will then be visible when user clicks the Tab key to go through the dialog's controls, or when Alt key is pressed and tab control has focus.
The focus rectangle should be drawn automatically for owner draw tab control when applicable. Make sure WS_TABSTOP is set for tab control (in dialog editor, go to tab control's properties and set "Tabstop = true")
BOOL focused = selected && (GetFocus() == this); will always be TRUE when user clicks on the tab control. ODS_NOFOCUSRECT will indicate if focus rectangle is not requested by the UI. See the example below.
Side note, sizeof(szTabText) returns the wrong value for wchar_t. Use _countof(szTabText) or sizeof(szTabText)/sizeof(*szTabText)
void CXtabCtrl::DrawItem(LPDRAWITEMSTRUCT di)
{
CDC* pDC = CDC::FromHandle(di->hDC);
TC_ITEM tci;
wchar_t text[256] = { 0 };
tci.mask = TCIF_TEXT;
tci.pszText = text;
tci.cchTextMax = _countof(text);
GetItem(di->itemID, &tci);
BOOL selected = di->itemState & ODS_SELECTED;
BOOL focused = selected && (GetFocus() == this);
//The UI may not be drawing focus rectangle, even if we click on the tab
if(di->itemState & ODS_NOFOCUSRECT)
focused = FALSE;
CString str;
if(selected) str += L"SEL ";//indicate selected
if(focused) str += L"FOC ";//indicate focused
CRect rect(di->rcItem);
pDC->TextOut(rect.left, rect.top, str);
}

C++ CButton showing only bitmap icon

I'm using C++ on Visual Studio 2012 update 4, and I have a Dialog where I want to display a button showing a bitmap (.bmp file), without borders
I have extended CButton to add my tooltip, and so on.
Using the Resource View to open the Dialog .rc file, I set the button Property Bitmap to true. Then, from the Dialog OnInitDialog function, I used this code to set the bitmap, identified as IDB_HELP
myButton.SetBitmap((HBITMAP)LoadImage(AfxGetApp()->m_hInstance,
MAKEINTRESOURCE(IDB_HELP), IMAGE_BITMAP, 16, 16, LR_COLOR));
But it displays this and I don't want that half-border.
I tried making it Flat and Transparent in the Resource View, but it only gets uglier.
Then I tried to only draw the image by setting Owner Draw to true and then redefining DrawItem in my button class, but I can't quite figure that out either.
Any easy way to make an icon-only button?
You have to use owner draw button or custom draw. Below is a simple example, it uses icon instead of bitmap (it's easier to assign transparent background for it)
class CMyButton:public CButton
{
void OnPaint()
{
CPaintDC dc(this);
CRect rc = dc.m_ps.rcPaint;
dc.FillSolidRect(&rc, GetSysColor(COLOR_3DFACE));
BOOL offset = (BST_PUSHED & GetState()) ? 1 : 0;
int w = 24;
int h = 24;
HICON hicon = (HICON)LoadImage(AfxGetApp()->m_hInstance, MAKEINTRESOURCE(IDC_ICON),
IMAGE_ICON, w, h, LR_DEFAULTCOLOR);
DrawIconEx(dc, offset, offset, hicon, w, h, 0, 0, DI_NORMAL);
DestroyIcon(hicon);
}
DECLARE_MESSAGE_MAP()
};
BEGIN_MESSAGE_MAP(CMyButton, CButton)
ON_WM_PAINT()
END_MESSAGE_MAP()
Usage:
BOOL CMyDialog::OnInitDialog()
{
BOOL res = CDialogEx::OnInitDialog();
static CMyButton bn;
bn.SubclassDlgItem(IDC_BUTTON1, this);
return res;
}
You do NOT need to do your own icon painting algorithm if you use a CMFCButton and you are a comfortable using an ICO file instead of a BMP. Although you can directly say in your resources file a button is of this type, I do not recommend it, because it adds an unmaintainable hexadecimal piece of text on the rc file. And if you use several rc files, one for each language, it's really devilish!
So lets go. In your form class, declare a member
CMFCButton m_button1;
The DoDataExchange should look like:
void MyDialog::DoDataExchange(CDataExchange* pDX)
{
__super::DoDataExchange(pDX);
DDX_Control(pDX, IDC_BUTTON1, m_button1);
// ...
}
Then the OnInitDialog should be something like:
BOOL CMyDialog::OnInitDialog()
{
if(!__super::OnInitDialog())
return FALSE;
m_button1.m_nFlatStyle= CMFCButton::BUTTONSTYLE_NOBORDERS;
m_button1.SetIcon(IDI_HELP);
return TRUE;
}
Use CMFCbutton and set the border style to BUTTONSTYLE_NOBORDERS;
Use a .ico instead of png for pictures.
Also points to note:
Load the library or exe which has the icon.
Pass the dll/exe loaded handle to loadicon.
Use MFC button handle to set the icon with property set as Noborder.
Example code:
m_HResdll = LoadLibrary("C:\\Repos\\iFIX\\SCADABin\\en\\UAAClientConfigurationRes.dll");
//m_hTrustIcon = LoadIcon(m_HResdll, MAKEINTRESOURCE(IDI_ICON1));
m_hTrustIcon = (HICON)LoadImage(m_HResdll, MAKEINTRESOURCE(IDI_ICON2),1,18,22, LR_DEFAULTCOLOR);
unsigned int err = GetLastError();
m_btnTrustIcon.SetIcon(m_hTrustIcon);
this->m_btnTrustIcon.EnableWindow(true);

Creating a custom MessageBox class for MFC

I'm trying to create a Modeless Messagebox class in MFC 6.0. The end result should look pretty similar to this:
I'm trying to add an icon with the following, but it doesn't seem to work.
HICON hIcon = LoadIcon(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDI_ERROR));
SetIcon(hIcon, FALSE);
Also, is there a way to set the background color of the top part of a dialog to a different color from the rest of the dialog?
There is some design issues here, for this type of interface the program should save automatically. If user is deleting the file then confirm it with modeless dialog. This way data is not lost if user is impatient. Anyway, you can just override OnPaint etc. and draw the icon
void CMyDlg::OnPaint()
{
CDialog::OnPaint();
//draw white rectangle on top-half of dialog
//draw lt-gray rectangle at bottom-half, GetSysColor(COLOR_BTNFACE)
HICON hIcon = LoadIcon(NULL, IDI_ERROR);
dc.DrawIcon(x, y, hIcon);
DestroyIcon(hIcon);
}
BOOL CMyDlg::OnEraseBkgnd(CDC* pDC)
{
return 1;
}
You might also need to override OnCtlColor

CMFCCaptionMenuButton alternative?

I need to create a caption bar button for a CDockablePane which will call up a menu with various options. I tried to use CMFCCaptionMenuButton and the button and menu show up but the message map methods for the menu ids don't fire. The MFC documentation states that CMFCCaptionMenuButton is meant for internal infrastructure and not really for your code.
So assuming that is what my problem is should I be using a CMFCCaptionBarButton and then making a separate popup menu? Has anyone made a similar caption bar based menu in MFC before?
Here's some slimmed down code snippets in case I just made a stupid mistake in hooking up the events:
BEGIN_MESSAGE_MAP(CDockPane, CDockablePane)
ON_COMMAND(ID_MORPH_BROWSER, OnMorphBrowser)
END_MESSAGE_MAP()
void CDockPane::OnPressButtons(UINT nHit)
{
// only for custom button handling don't call base
// close, maximize, and pin will be handled by default
switch (nHit)
{
case ID_MORPHTEST:
{
CMorphMenuButton* pButton = dynamic_cast<CMorphMenuButton*>(m_arrButtons.GetAt(m_morphIndex));
pButton->ShowMenu(this);
break;
}
}
}
void CDockPane::SetCaptionButtons()
{
CDockablePane::SetCaptionButtons(); // for close, pin etc
m_morphIndex = m_arrButtons.Add(new CMorphMenuButton(ID_MORPHTEST));
}
void CDockPane::OnMorphBrowser()
{
// do stuff on menu item click
}
Edit: Removed previous code no longer in use
Now that the sound of crickets chirping has dwindled in the background I guess I'll post the workaround I currently have in place:
Instead of inheriting and extending CMFCCaptionMenuButton I build my class by extending CMFCCaptionButton. I then create a menu and provide a ShowMenu method to be explicitly called when handling the custom button events as well as overriding GetIconID to return a particular system icon for the button for each menu added to the caption bar ending up with something like this for the example outlined in the question:
#pragma once
// CMorphMenuButton command target
class CMorphMenuButton : public CMFCCaptionButton
{
public:
CMorphMenuButton(UINT nHit);
virtual ~CMorphMenuButton();
virtual CMenuImages::IMAGES_IDS GetIconID (BOOL bHorz, BOOL bMaximized) const;
void ShowMenu(CWnd* pWnd);
private:
CMenu m_dockMenu;
CMenu* m_subMenu;
};
// MorphMenuButton.cpp : implementation file
//
#include "stdafx.h"
#include "MorphMenuButton.h"
// CMorphMenuButton
CMorphMenuButton::CMorphMenuButton(UINT nHit)
: CMFCCaptionButton(nHit)
{
SetMiniFrameButton(); // already defaulted?
m_dockMenu.LoadMenu(IDR_DOCKPANE); // resource ID for dock pane menus
}
CMorphMenuButton::~CMorphMenuButton()
{
m_dockMenu.DestroyMenu();
}
CMenuImages::IMAGES_IDS CMorphMenuButton::GetIconID(BOOL bHorz, BOOL bMaximized) const
{
return CMenuImages::IdArrowForward;
}
void CMorphMenuButton::ShowMenu(CWnd* pWnd)
{
CRect windowRect, buttonRect;
pWnd->GetWindowRect(&windowRect);
buttonRect = GetRect();
CPoint menuPos(windowRect.left + buttonRect.right, windowRect.top + buttonRect.bottom);
m_subMenu = m_dockMenu.GetSubMenu(0);
if (!m_subMenu->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, menuPos.x, menuPos.y, pWnd))
{
DWORD id = GetLastError();
wchar_t errMsg[256];
FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, 0, id, 0, errMsg, sizeof(errMsg), 0);
MessageBox(0, errMsg, L"Error", MB_OK);
}
}
The setting of caption bar buttons and handling of click events for both buttons and menus are the same as defined in the question and this works.

win32 CTabctrl: Drawing "child" windows in the active tab

I have a resizable dialog that contains a CTabCtrl, the tab control has 4 tabs that when clicked on displays one of four different CTreeCtrls.
I have derived a class from CTabCtrl, which keeps track of its "child" controls like so:
...
class Container: public CTabCtrl {
vector<CWnd*> _children;
....
int Container::AddTab(CWnd* Child) {
CString txt;Child->GetWindowText(txt);
_children.push_back(Child);
int idx = this->InsertItem(this->GetItemCount(), txt, 0);
if(idx == 0) {
CRect c;
this->GetWindowRect(&c);
GetParent()->ScreenToClient(&c);
this->AdjustRect(FALSE, c);
Child->SetWindowPos(&wndTop, c.left, c.top, c.Width(), c.Height(), SWP_SHOWWINDOW);
this->SetCurSel(idx);
} else Child->ShowWindow(SW_HIDE);
return idx;
}
And I attempt to draw the child controls like so:
void Container::OnTabChanging(NMHDR*, LRESULT* pResult) { // hide the changed from tab
int selected = this->GetCurSel();
if(selected != -1)
{
// move old window to bottom of the zorder and hide
_children[selected]->SetWindowPos(&wndBottom, 0, 0, 0, 0, SWP_NOSIZE|SWP_NOMOVE|SWP_HIDEWINDOW);
ASSERT(!_children[selected]->IsWindowVisible());
}
*pResult = 0;
}
// show the child for the tab being changed to
void CNodeContainer::OnTabChanged(NMHDR* pNMHDR, LRESULT* pResult) {
int selected = this->GetCurSel();
ASSERT(selected!=-1);
CRect c;
this->GetWindowRect(&c);
GetParent()->ScreenToClient(&c);
this->AdjustRect(FALSE, c);
_children[selected]->SetWindowPos(&wndTop, c.left, c.top, c.Width(), c.Height(), SWP_SHOWWINDOW|SWP_FRAMECHANGED);
*pResult = 0;
}
However the child controls, whilst they appear, don't always draw correctly, they sort of mix up their content together and only show the right content when i click on them (the actual tree controls).
Is this the best way of drawing and moving windows around in the zorder, what am I missing?
Many thanks
bg
Instead of just changing the z-order of your children, completely hide every child except the top one. I use the same approach in a custom CTabCtrl and it works fine.
Its fixed now - the problem came from the fact that the in the resize code for the tabctrl, I was using movewindow to move the child windows into place - This was changing the zorder of the child windows.
This could solve the problem after your window or tab apears. Try to use
this->RedrawWindow();
In OnTabChanging() function before it returns.