I'm having a bug in my code that's kicking my ass, so after much attempted debugging I finally decided to see if anyone else knew what my issue was.
I'm trying to add a grid object to a dialog that I have, but I keep hitting the assert mentioned in the title and I don't know why.
LONG myDialog::OnInitDialog(UINT wParam, LONG lParam)
{
BOOL bRet = super::OnInitDialog();
InitGridControl();
InitLayout();
myApp.ActiveDocChangeEvent->Attach(
RefMemberDelegate1(*this, &myDialog::OnNewDoc), this); // attach to event so I know when document is created
return bRet;
}
void myDialog::OnNewDoc(CDerivedDocument* pNewDoc)
{
pNewDoc->SetMyDialog(this); // when new document is created, set pointer to dialog
}
void myDialog::InitGridControl()
{
CRect rect;
// Get the grid area rectangle and set it up.
GetDlgItem(IDC_GRID)->GetClientRect(rect);
GetDlgItem(IDC_GRID)->MapWindowPoints(this, &rect); // replacing dummy image with the grid
m_Grid = new myGridCtrl;
bool result = m_Grid->Create(WS_CHILD | WS_BORDER | WS_VISIBLE | WS_TABSTOP, rect, this, IDC_GRID);
// Set the appropriate options
//...options...
m_Grid->InsertColumn(0, _T("Name"), 100); // doesn't seem to crash here, which means grid is created okay?
}
void myDialog::PopulateGridControl(BOOL bRedraw, CDerivedDocument * pDoc)
{
if (GetSafeHwnd() == NULL)
return;
// get handles to document and stuff
m_Grid->SetRedraw(FALSE); // ** ASSERT() CALL IS HERE **
m_Grid->RemoveAll();
// other stuff..
}
/////////////////////
// In CDocument, once it is created...
CDerivedDocument::SetMyDoc(myDialog * pDlg)
{
pDlg->PopulateGridControl(true,this);
}
Any idea what's going on? I mean, I only create the dialog once everything has been initialized, so there shouldn't be a problem there. m_Grid.Create() returns true, so creation is successful. Why is SetRedraw() hitting the assert that the m_hWnd isn't a handle to a window? Where does m_hWnd get set anyway?
Thanks for any help you can offer.
Cheers
Are you sure the dialog is created when you call
CDerivedDocument::SetMyDoc(myDialog * pDlg)?
What I see is that you are loading the grid (& dialog) from document, you should rather load the dialog and grid from the view using the document.
This may not be the direct cause of your assert trouble but nevertheless an improvement. It might just put things in the right order and fix this issue.
Related
I have this very strange issue. I'm trying to get a window hierarchy to be replicated. So on creating the 1st level dialog, I'm start the instance of the 2nd level dialog.
I've done this in many different ways, but it always shows up as the 2nd level being below the 1st level and then usually a zorder inversion happens (they flip positions). Occasionally, the inversion doesn't happen, but if I click on the owner, the owned immediately jumps to the top of the zorder.
Here are the main parts of a small example to show this happening:
const unsigned short WMA_DIALOGACTION = WM_APP+1;
// Button event handler for the 0th level
void CdialogcallingdialogsDlg::OnBnClickedDlgLvl1()
{
CDlgLvl1 x(this);
x.DoModal();
}
BEGIN_MESSAGE_MAP(CDlgLvl1, CDialogEx)
ON_WM_WINDOWPOSCHANGED()
ON_MESSAGE(WMA_DIALOGACTION, OnDialogAction)
END_MESSAGE_MAP()
void CDlgLvl1::OnWindowPosChanged(WINDOWPOS* lpwndpos)
{
if (!m_shownDlg) {
m_shownDlg = true;
PostMessage(WMA_DIALOGACTION);
}
}
// Level 1 dialog opening up level 2 dialog
LRESULT CDlgLvl1::OnDialogAction(WPARAM wParam, LPARAM lParam)
{
ShowWindow(SW_SHOW);
CDlgLvl2 x(this);
x.DoModal();
return LRESULT();
}
BEGIN_MESSAGE_MAP(CDlgLvl2, CDialogEx)
ON_WM_WINDOWPOSCHANGING()
END_MESSAGE_MAP()
// Level 2 dialog offseting its position
void CDlgLvl2::OnWindowPosChanging(WINDOWPOS* lpwndpos)
{
ASSERT(lpwndpos->hwnd == m_hWnd);
// Offset dialog to see the problem of dlg2 showing up below dlg1
if (!(lpwndpos->flags & SWP_NOMOVE)) {
lpwndpos->x += 10;
lpwndpos->y += 10;
}
}
In the example, you click on the button in the main dialog. That then starts up CDlgLvl1 which then starts up CDlgLvl2. The dialogs are the default dialogs except for the message handling that is shown here and a button on the main application dialog. If you look at it carefully, you can see the inversion.
What am I doing wrong? Perhaps there is a better way to do this?
In case it makes a difference, the issue is more pronounced under Windows 10 and doesn't seem to be visible on Windows 8.1.
A copy of the solution can be pulled from my git repo here:
https://github.com/Ma-XX-oN/dialog-calling-dialogs.git
I've just added some bitmaps on the dialogs to really show the issue, but I've not tested on my 8.1 box yet.
I did a recording of how it pops up and here is frame 0, 2, and 3 of that recording:
Frame 0
Frame 2
Frame 3
As you can see, LVL1 appears over LVL2 in Frame 2, and then flips position in Frame 3.
Full video can be found here.
Using this example project, I've not been able to replicate LVL1 staying overtop of LVL2, but I believe that the behaviour of the zorder inversion not happening is some sort of race condition.
The problem is caused when windows "transition animation" is enabled. WM_WINDOWPOSCHANGED is being sent before the animation is finished.
To fix this problem, you can simply disable the transition for the dialog:
BOOL CDlgLvl2::OnInitDialog()
{
BOOL res = CDialogEx::OnInitDialog();
BOOL attrib = TRUE;
DwmSetWindowAttribute(m_hWnd, DWMWA_TRANSITIONS_FORCEDISABLED, &attrib, sizeof(attrib));
return res;
}
If you don't want to disable the transition, you have to wait until this transition is finished. I don't know how to detect it or how to determine the transition time. It seems to be 250 milliseconds. SystemParametersInfo(SPI_SETMENUSHOWDELAY...) gives a value of 400 milliseconds which seems a bit too long.
Assuming we know the time, use SetTimer to run the function after transition is over:
BOOL CDlgLvl2::OnInitDialog()
{
BOOL res = CDialogEx::OnInitDialog();
ANIMATIONINFO info = { sizeof info };
SystemParametersInfo(SPI_GETANIMATION, sizeof(ANIMATIONINFO), &info, 0);
if (info.iMinAnimate)
SetTimer(1, 250, nullptr);
else
SetTimer(1, 1, nullptr);
return res;
}
void CDlgLvl2::OnTimer(UINT_PTR nIDEvent)
{
CDialogEx::OnTimer(nIDEvent);
if(nIDEvent == 1)
{
KillTimer(nIDEvent);
CDlgLvl2(this).DoModal();//note, PostMessage is not needed in SetTimer
}
}
Maybe the problem is caused because the 1st level dialog creates the 2nd one before it has a chance to display itself. And yes, this can vary from system to system. There's no really a fix, but I would suggest a workaround, employing a timer. Below is some code.
Header file for CDlgLvl1:
class CDlgLvl1 : public CDialogEx
{
.
.
.
protected:
UINT_PTR nIDTimer = 0; // Add this
};
Source file for CDlgLvl1:
BEGIN_MESSAGE_MAP(CDlgLvl1, CDialogEx)
.
.
ON_MESSAGE(WMA_DIALOGACTION, OnDialogAction)
ON_WM_TIMER()
END_MESSAGE_MAP()
BOOL CDlgLvl1::OnInitDialog()
{
CDialogEx::OnInitDialog();
nIDTimer = SetTimer(1, 250, NULL);
return TRUE;
}
void CDlgLvl1::OnTimer(UINT_PTR nIDEvent)
{
if (nIDTimer && nIDEvent == nIDTimer)
{
KillTimer(nIDTimer);
nIDTimer = 0;
PostMessage(WMA_DIALOGACTION);
return;
}
CDialogEx::OnTimer(nIDEvent);
}
LRESULT CDlgLvl1::OnDialogAction(WPARAM wParam, LPARAM lParam)
{
CDlgLvl2 x(this);
x.DoModal();
return 0;
}
The mechanism you provided to prevent the 2nd window being displayed multiple times (the m_shownDlg variable) has been replaced by the nIDTimer check.
Please experiment with the timer's elapse value. The one I suggest (250 - 1/4 sec) is OK for most systems and imperceptible to to the user.
I wrote this in the SO editor, no actual test in VS (so it may contain some few syntax errors - pls fix them if so).
Note: You do not need to override OnWindowPosChanging() if you only want to set the position of the 2nd dialog. It's relative to its parent, so you can simply set the X Pos and Y Pos properties of the dialog's resource.
I tried your project in Visual Studio 2019:
I ran it in DEBUG mode and it works fine. The third dialogue showed up as a child of the second dialog (that is, with the correct ZORDER). The same is true for RELEASE build.
See: https://www.dropbox.com/s/8f5z5ltq3vfc10r/Test.mp4?dl=0
Update
If one of my classes I had a timer and I did this:
void CChristianLifeMinistryEditorDlg::OnTimer(UINT_PTR nIDEvent)
{
READYSTATE eState = READYSTATE_UNINITIALIZED;
if (nIDEvent == PRINT_PREVIEW_TIMER)
{
eState = m_pPrintHtmlPreview->GetReadyState();
if (eState == READYSTATE_COMPLETE)
{
KillTimer(m_uPreviewTimer);
PostMessage(WM_COMMAND,
MAKELONG(IDC_BUTTON_PRINT_PREVIEW2, BN_CLICKED));
}
}
CResizingDialog::OnTimer(nIDEvent);
}
You could adapt the principle and then just simulate pressing the button to display the second next dialog. Might work.
So, I have a CDialog resource that I have had for a long time and I decided to add a statusbar to it. Here is the resource:
All controls fit nicely in the dialog. Now, at runtime this is what it looks like:
The tutorial I followed was here and for the most part it works. Here is my setup code:
///////////////////////////////
m_StatusBar.Create(this); //We create the status bar
m_StatusBar.SetIndicators(indicators, 2); //Set the number of panes
CRect rect;
GetClientRect(&rect);
//Size the two panes
m_StatusBar.SetPaneInfo(0, ID_INDICATOR_DATE,
SBPS_NORMAL, 200);
m_StatusBar.SetPaneInfo(1, ID_INDICATOR_MEETING_TYPE, SBPS_STRETCH, 0);
//This is where we actually draw it on the screen
RepositionBars(AFX_IDW_CONTROLBAR_FIRST, AFX_IDW_CONTROLBAR_LAST,
ID_INDICATOR_DATE);
GetDynamicLayout()->AddItem(m_StatusBar.GetSafeHwnd(),
CMFCDynamicLayout::MoveVertical(100), CMFCDynamicLayout::SizeHorizontal(100));
///////////////////////////////
I have tried without WindowsBlinds and the issues are still there.
So my issues are:
1/ The controls are overlapping the status bar. How do I accurately set these controls in the resource editor so this issue won't happen? How should it be resolved? Hit n miss?
2/ My dialog supports resizing by using the dynamic layouts and it has the OBM_SIZE in the bottom right:
void CResizingDialog::InitialiseResizeIcon(CBitmap& rBmpResize, CStatic& rLblResize, CWnd* pDialog)
{
CRect rcIcon, rcClient;
if (pDialog != nullptr)
{
rBmpResize.LoadOEMBitmap(OBM_SIZE);
rLblResize.Create(nullptr, WS_CHILD | WS_VISIBLE | SS_BITMAP,
CRect(0, 0, 16, 16), pDialog, IDC_STATIC_RESIZE);
rLblResize.SetBitmap(rBmpResize);
pDialog->GetClientRect(rcClient);
rLblResize.GetClientRect(rcIcon);
rLblResize.SetWindowPos(&CWnd::wndTop,
rcClient.right - rcIcon.Width(),
rcClient.bottom - rcIcon.Height(), 0, 0, SWP_NOSIZE);
CMFCDynamicLayout *pDynamicLayout = pDialog->GetDynamicLayout();
if (pDynamicLayout != nullptr)
{
CMFCDynamicLayout::MoveSettings moveSettings = CMFCDynamicLayout::MoveHorizontalAndVertical(100, 100);
CMFCDynamicLayout::SizeSettings sizeSettings = CMFCDynamicLayout::SizeNone();
pDynamicLayout->AddItem(rLblResize.GetSafeHwnd(), moveSettings, sizeSettings);
}
}
}
How do I avoid the issue that you can see there now in the bottom right?
Update
It looked like I should use CreateEx And use this style SBARS_SIZEGRIP. Then stop creating my own resize icon. I assume the two grippers will look the same. So this might be one of the answers.
I tried using the above flag but unfortunately I can't use it:
This gripper is not consistent with the other one I am using so I need to keep my original one instead.
Update 2
I now realise that the gripper is always created anyway, so I had two grippers there! I have now derived my own statusbar class and switched off the default gripper:
BOOL CCreateReportStatusBar::PreCreateWindow(CREATESTRUCT& cs)
{
BOOL bRet = CStatusBar::PreCreateWindow(cs);
cs.style &= ~SBARS_SIZEGRIP;
return bRet;
}
So now I only have the one gripper. But my two issues still remain.
Update 3
I stumbled over this. In theory if I override this DrawGripper method I should be able to render my own gripper instead. Doesn't work. The method is never called.
Update 4
I decided not to fight the system. I have let the status bardraw the themes gripper and I have adjusted my resizing dialog class to also draw the themes gripper. So all is good.
i have created CMFCPropertyGridCtrl inside an CDockablePane and i want to replace this CMFCPropertyGridCtrl with a new one, then i override OnEraseBkgnd.
OnEraseBkgnd called only in the application start, and when i want to call it by Invalidate or InvalidateRect it didn't fire.
How can i call OnEraseBkgnd?
Thanks in advance.
void CCL2PropertiesPane::HostPropertyGridControl(CMFCPropertyGridCtrl* pPropertyGridControl)
{
if(NULL == pPropertyGridControl)
return;
if(m_pPropertyGridControl)
RemoveCurrentPropertyGridControl();
m_pPropertyGridControl = pPropertyGridControl;
SetWindowText(m_pPropertyGridControl->GetName());
CRect clientRectangle;
GetClientRect(&clientRectangle);
m_pPropertyGridControl->Create(WS_CHILD | WS_VISIBLE, clientRectangle, this, PROPERTIES_DOCKABLE_PANE_ID);
}
//--------------------------------------------------------------------------------
void CCL2PropertiesPane::RemoveCurrentPropertyGridControl()
{
m_pPropertyGridControl = NULL;
SetWindowText(GetPaneName());
CRect clientRectangle;
GetClientRect(&clientRectangle);
//here i want to call OnEraseBkgnd
InvalidateRect(clientRectangle);
//Invalidate();
}
//--------------------------------------------------------------------------------
BOOL CCL2PropertiesPane::OnEraseBkgnd(CDC* pDC)
{
CRect clientRectangle;
GetClientRect(&clientRectangle);
CBrush whiteBrush(RGB(250, 250, 250));
pDC->FillRect(clientRectangle, &whiteBrush);
return TRUE;
}
Add ON_WM_ERASEBKGND to CCL2PropertiesPane's message map to properly erase the background. Or move the FillRect function in to OnPaint.
Regarding:
void CCL2PropertiesPane::RemoveCurrentPropertyGridControl()
{
m_pPropertyGridControl = NULL;
...
}
Above code is appropriate for initialization, but it doesn't remove or destroy anything. The control is still there, it only makes the program forget how to find the control. To hide the control use:
m_pPropertyGridControl->ShowWindow(SW_HIDE);
To destroy the control use DestroyWindow(), but that's not recommended with above setup.
I need to adjust the dialog window dynamically based on its size. To do so I employ the following technique:
I load it up and get its size from the CDialog::OnInitDialog() handler.
If the size is too big, I end the dialog by calling CDialog::EndDialog
And then update global variable and reinit the dialog-derived class again with the size adjustment.
What happens is that on the second pass, some APIs start acting strangely. For instance, MessageBox does not show (thus all ASSERT macros stop working) and some SetWindowText APIs crash the app. Any idea why?
Here're the code snippets:
#define SPECIAL_VALUE -1
//From CWinApp-derived class
BOOL CWinAppDerivedClass::InitInstance()
{
//...
for(;;)
{
CDialogDerivedClass dlg(&nGlobalCounter);
m_pMainWnd = &dlg;
if(dlg.DoModal() != SPECIAL_VALUE)
break;
}
//...
}
And then from the dialog class itself:
//From CDialogDerivedClass
BOOL CDialogDerivedClass::OnInitDialog()
{
//The following API shows message box only on the 1st pass, why?
::MessageBox(NULL, L"1", L"2", MB_OK);
//...
if(checkedDialogSizeIndicatesReload)
{
this->EndDialog(SPECIAL_VALUE);
return FALSE;
}
//Continue loading dialog as usual
...
}
EDIT: I noticed by chance that if I comment out the following line it seems to work. Any idea why?
//m_pMainWnd = &dlg;
Variable dlg is not yet a window at the place where you are setting m_pMainWnd (the dialog box is displayed only after OnInitInstance returns TRUE); the Following code should work:
for(;;)
{
CDialogDerivedClass dlg(&nGlobalCounter);
// m_pMainWnd = &dlg;
if(dlg.DoModal() != SPECIAL_VALUE)
break;
}
m_pMainWnd = &dlg;
InitDialog is the last message processed before the dialog window appears on the screen - you can detect and adjust the size in place and not have the kind of funky global variable thing you are doing.
if(checkedDialogSizeIndicatesReload)
{
// look up SetWindowPos -
// I am nt sure if there is another parameter or not that is optional
int x,y,cx,cy;
WINDOWPLACEMENT wp;
GetWindowPlacement(&wp);
// calc new size here
SetWindowPos(this,x,y,cx,cy);
}
// window appears when the message handler returns
I have a class derived from CTreeCtrl. In OnCreate() I replace the default CToolTipCtrl object with a custom one:
int CMyTreeCtrl::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CTreeCtrl::OnCreate(lpCreateStruct) == -1)
return -1;
// Replace tool tip with our own which will
// ask us for the text to display with a TTN_NEEDTEXT message
CTooltipManager::CreateToolTip(m_pToolTip, this, AFX_TOOLTIP_TYPE_DEFAULT);
m_pToolTip->AddTool(this, LPSTR_TEXTCALLBACK);
SetToolTips(m_pToolTip);
// Update: Added these two lines, which don't help either
m_pToolTip->Activate(TRUE);
EnableToolTips(TRUE);
return 0;
}
My message handler looks like this:
ON_NOTIFY_EX(TTN_NEEDTEXT, 0, &CMyTreeCtrl::OnTtnNeedText)
However I never receive a TTN_NEEDTEXT message. I had a look with Spy++ and it also looks like this message never gets sent.
What could be the problem here?
Update
I'm not sure whether this is relevant: The CTreeCtrl's parent window is of type CDockablePane. Could there be some extra work needed for this to work?
Finally! I (partially) solved it:
It looks like the CDockablePane parent window indeed caused this problem...
First I removed all the tooltip-specific code from the CTreeCtrl-derived class. Everything is done in the parent pane window.
Then I edited the parent window's OnCreate() method:
int CMyPane::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CDockablePane::OnCreate(lpCreateStruct) == -1)
return -1;
const DWORD dwStyle = WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN |
TVS_CHECKBOXES | TVS_DISABLEDRAGDROP | TVS_HASBUTTONS | TVS_HASLINES | TVS_LINESATROOT |
TVS_INFOTIP | TVS_NOHSCROLL | TVS_SHOWSELALWAYS;
// TREECTRL_ID is a custom member constant, set to 1
if(!m_tree.Create(dwStyle, m_treeRect, this, TREECTRL_ID ) )
{
TRACE0("Failed to create trace tree list control.\n");
return -1;
}
// m_pToolTip is a protected member of CDockablePane
m_pToolTip->AddTool(&m_tree, LPSTR_TEXTCALLBACK, &m_treeRect, TREECTRL_ID);
m_tree.SetToolTips(m_pToolTip);
return 0;
}
Unforunately we cannot simply call AddTool() with less parameters because the base class will complain in the form of an ASSERT about a uFlag member if there is no tool ID set.
And since we need to set the ID, we also need to set a rectangle. I created a CRect member and set it to (0, 0, 10000, 10000) in the CTor. I have not yet found a working way to change the tool's rect size so this is my very ugly workaround. This is also why I call this solution partial. Update: I asked a question regarding this.
Finally there is the handler to get the tooltip info:
// Message map entry
ON_NOTIFY(TVN_GETINFOTIP, TREECTRL_ID, &CMobileCatalogPane::OnTvnGetInfoTip)
// Handler
void CMyPane::OnTvnGetInfoTip(NMHDR *pNMHDR, LRESULT *pResult)
{
LPNMTVGETINFOTIP pGetInfoTip = reinterpret_cast<LPNMTVGETINFOTIP>(pNMHDR);
// This is a CString member
m_toolTipText.ReleaseBuffer();
m_toolTipText.Empty();
// Set your text here...
pGetInfoTip->pszText = m_toolTipText.GetBuffer();
*pResult = 0;
}
I believe you still have to enable the tooltip, even though you are replacing the builtin.
EnableToolTips(TRUE);
Well, since that did not work for you and since no-one more expert has offered any help, here a few more suggestions from me. Although they are lame, they might get you moving again:
Make sure your OnCreate() rotine is actually being executed.
Enable the tool tip BEFORE you replace it.
The code I use to do this looks like this. ( I confess I do not understand all the details, I copied it from some sample code, it worked and so I never looked at it any more. )
// Enable the standard tooltip
EnableToolTips(TRUE);
// Disable the builtin tooltip
CToolTipCtrl* pToolTipCtrl = (CToolTipCtrl*)CWnd::FromHandle((HWND)::SendMessage(m_hWnd, LVM_GETTOOLTIPS, 0, 0L));
I haven't tried in a CTreeCtrl but I think you should call RelayEvent for the tooltip ctrl to know when the tooltip has to be displayed. Try this:
MyTreeCtrl.h:
virtual BOOL PreTranslateMessage(MSG* pMsg);
MyTreeCtrl.cpp:
BOOL CMyTreeCtrl::PreTranslateMessage(MSG* pMsg)
{
m_pToolTip.Activate(TRUE);
m_pToolTip.RelayEvent(pMsg);
return CTreeCtrl::PreTranslateMessage(pMsg);
}
I hope this help.
Don't you have to override OnToolHitTest()?
(old) Resource 1
(old) Resource 2:
In addition to returning the hit code (nHit), you also have to fill out the TOOLINFO struct. Here's how VIRGIL does it in CMainFrame::OnToolHitTest:
int nHit = MAKELONG(pt.x, pt.y);
pTI->hwnd = m _ hWnd;
pTI->uId = nHit;
pTI->rect = CRect(CPoint(pt.x-1,pt.y-1),CSize(2,2));
pTI->uFlags |= TTF _ NOTBUTTON;
pTI->lpszText = LPSTR _ TEXTCALLBACK;
Most of this is obvious—like setting hwnd and uId—but some of it is less so. I set the rect member to a 2-pixel-wide, 2-pixel-high rectangle centered around the mouse location. The tooltip control uses this rectangle as the bounding rectangle of the "tool," which I want to be tiny, so moving the mouse anywhere will constitute moving outside the tool. I set TTF _ NOTBUTTON in uFlags because the tooltip is not associated with a button. This is a special MFC flag defined in afxwin.h; MFC uses it to do help for tooltips. There's another MFC-extended flag for tooltips, TTF _ ALWAYSTIP. You can use it if you want MFC to display the tip even when your window is not active.
You may have noticed that so far I haven't told MFC or the tooltip or the TOOLINFO what the actual text of the tip is. That's what LPSTR _ TEXTCALLBACK is for. This special value tells the tooltip control (the internal, thread-global one that MFC uses) to call my window back to get the text. It does this by sending my window a WM _ NOTIFY message with notification code TTN _ NEEDTEXT.
Try to specifically handle all tooltip ids:
ON_NOTIFY_EX_RANGE(TTN_NEEDTEXT, 0, 0xFFFF, &CMyTreeCtrl::OnNeedTipText)
If that doesn't work, you may have to manually call RelayEvent() from PreTranslateMessage().