MFC, EndDialog, repeated dialog creation causes unexpected behavior - c++

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

Related

MFC Win32 | LButtonUp not being received after clicking toolbar button (CMFCToolBarButton::OnClick)

What I'm trying to achieve
Well, the title might not have explained the problem very well, so here goes:
I am trying to create a Win32 app using MFC that lets you edit and inspect other windows.
I want the user to be able to select other windows.
I got inspired by the "Find Window Process" tool on the toolbar on sysinternals applications such as ProcessExplorer.
The way it works is you click, then the window disappears, and then you drag it over the window you want to select. A border pops up around it and when you let go, it selects the window the mouse is over.
My problem
The problem I was facing is that I don't know how to detect when the user lets go of the mouse on another window.
I detect mouse down using OnClick in CMFCToolBarButton
I tried using SetCapture() but that did nothing.
I tried using OnNcLButtonUp and OnLButtonUp but neither of them worked. (alongside SetCapture)
Here's my code so far (ChildView.cpp):
BEGIN_MESSAGE_MAP(CChildView, CWnd)
ON_WM_PAINT()
ON_UPDATE_COMMAND_UI(ID_TB_LOCATEWINDOW, &CChildView::EnableToolbarButton)
ON_UPDATE_COMMAND_UI(ID_TOOLS_MESSAGELAUNCHER, &CChildView::EnableToolbarButton)
ON_WM_XBUTTONUP()
// ON_WM_LBUTTONUP()
ON_WM_NCLBUTTONUP()
END_MESSAGE_MAP()
....
void CChildView::LocateWindow()
{
GetParentFrame()->ShowWindow(SW_MINIMIZE);
SetCapture();
}
void CChildView::OnNcLButtonUp(UINT nHitTest, CPoint point)
{
ReleaseCapture();
GetParentFrame()->ShowWindow(SW_NORMAL);
MessageBox(L"Stuff", L"");
CWnd::OnNcLButtonUp(nHitTest, point);
}
I want to mention that the LocateWindow function gets called when the toolbar button is clicked (as in mouse down, not mouse down AND up)
It is called from the OnClick function.
Here's the code for that:
(I replace the button with OnToolbarReset)
// CLocateWindowButton.cpp : implementation file
//
#include "pch.h"
#include "WindowHacker.h"
#include "MainFrm.h"
#include "CLocateWindowButton.h"
// CLocateWindowButton
IMPLEMENT_SERIAL(CLocateWindowButton, CMFCToolBarButton, 1)
// CLocateWindowButton member functions
CLocateWindowButton::CLocateWindowButton()
{
}
CLocateWindowButton::CLocateWindowButton(CMainFrame* mainFrame, UINT uiCmdID, LPCTSTR lpszText) : CMFCToolBarButton(uiCmdID, NULL, lpszText)
{
this->mainFrame = mainFrame;
}
BOOL CLocateWindowButton::OnClick(CWnd* pWnd, BOOL bDelay = TRUE) {
//(CMainFrame*)m_pWndParent->LocateWindow();
mainFrame->LocateWindow();
return FALSE;
}
void CLocateWindowButton::CopyFrom(const CMFCToolBarButton& src)
{
CMFCToolBarButton::CopyFrom(src);
mainFrame = ((CLocateWindowButton&)src).mainFrame;
}
//void CLocateWindowButton::AssertValid() const
//{
// CMFCToolBarButton::AssertValid();
//
// // TODO: Add your specialized code here and/or call the base class
//}
UPDATE:
It seems to work when I put it inside an LButtonDown event, it just seems to not work when it is being detected from OnClick in CMFCToolBarButton
I found that in CMFCToolBar::OnLButtonUp, after calling OnClick in the button, it recaptures the cursor, invalidating our SetCapture.
BUT if I return TRUE instead of FALSE in OnClick, the mouse is not recaptured.
So changing this:
BOOL CLocateWindowButton::OnClick(CWnd* pWnd, BOOL bDelay = TRUE) {
//(CMainFrame*)m_pWndParent->LocateWindow();
mainFrame->LocateWindow();
//ReleaseCapture();
this->mainFrame->SetCapture();
return FALSE;
}
To this:
BOOL CLocateWindowButton::OnClick(CWnd* pWnd, BOOL bDelay = TRUE) {
//(CMainFrame*)m_pWndParent->LocateWindow();
mainFrame->LocateWindow();
//ReleaseCapture();
this->mainFrame->SetCapture();
return TRUE; // The line is changed here
}
The message gets sent to CMainFrame instead.

Why is the owning window showing up above the owned window?

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.

How do I detect if a modeless CDialog has been closed?

I have followed this question to make a non-modal/modeless dialog:
How to display a non-modal CDialog?
I'm using MFC/C++ in VS2008. I'm more fluent with C# and .net than with MFC and C++.
I have a menu item in my form that launches the dialog. There can only be one instance of the dialog opened. The dialog displays fine. I can close it by clicking the X in the corner and it closes when I close the main form. The problem I am having is the dialog cannot be opened again after I click the X to close the dialog. I know it is because the pointer is never set back to NULL.
I have this in my form's header file:
CChildDialog *m_pDialog;
I have this part in my form's constructor:
m_pDialog = NULL;
When clicking on a menu item I have this code in the menu item's method (I modified it from the other SO answer because I only want one instance of the dialog opened):
if(m_pDialog == NULL)
{
// Invoking the Dialog
m_pDialog = new CChildDialog();
BOOL ret = m_pDialog->Create(IDD_CHILDDIALOG, this);
if (!ret) //Create failed.
{
AfxMessageBox(_T("Error creating Dialog"));
}
m_pDialog->ShowWindow(SW_SHOW);
}
Now I know I need to execute this part and set the pointer to NULL, but I don't know where to put this:
// Delete the dialog once done
delete m_pDialog;
m_pDialog = NULL;
Do I need to keep monitoring if the dialog has been disposed? Is there an event triggered to the parent form when the dialog is closed?
If you want to recycle the contents of the window after closing it with X, you have to handle the WM_CLOSE message in your dialog:
void CChildDialog::OnClose()
{
ShowWindow(SW_HIDE);
}
Then in the code that opens the window:
if(m_pDialog == NULL)
{
// Invoking the Dialog
m_pDialog = new CChildDialog();
BOOL ret = m_pDialog->Create(IDD_CHILDDIALOG, this);
if (!ret) //Create failed.
{
AfxMessageBox(_T("Error creating Dialog"));
}
}
m_pDialog->ShowWindow(SW_SHOW); //moved outside the if(m_pDialog == NULL)
Hope it can help
If you want to delete the modeless dialog, then just do so.
If you want to delete the dialog's object when the user closed the modeless dialog you might take a look at WM_PARENTNOTIFY. If a child window is destroyed and the child windows has not the extended window style WS_EX_NOPARENTNOTIFY set, then windows sends a WM_PARENTNOTIFY with wParam=WM_DESTROY to the parent window. You should implement a handler for that message in the parent window and check if it's the modeless dialog that is being destroyed.
I had the question drafted up and was ready to post it, but then I had an idea and ended up solving my own problem. So for anyone else who has an issue with detecting the closing of a modeless dialog, this is what I did:
void Form1::MenuItemMethod()
{
if(m_pDialog == NULL)
{
// Invoking the Dialog
m_pDialog = new CChildDialog();
BOOL ret = m_pDialog->Create(IDD_CHILDDIALOG, this);
if (!ret) //Create failed.
{
AfxMessageBox(_T("Error creating Dialog"));
}
m_pDialog->ShowWindow(SW_SHOW);
}
else
{
// cannot check if visible at the beginning of method because
// pointer could be NULL and will throw an exception
if(m_pDialog->IsWindowVisible())
{
return;
}
m_pDialog->DestroyWindow();
m_pDialog = NULL;
MenuItemMethod();
}
}
I just ended up checking if the modeless dialog is visible after clicking on the form's menu item again. If it is visible, don't do anything. If not, destroy the existing non-visible dialog, set the pointer to NULL, and recursively call the method again. Since the pointer is now NULL, it should recreate the dialog normally and then return to normal operation.
you have to delete the memory in PostNcDestroy like this
void CChildDialog ::PostNcDestroy()
{
CDialog::PostNcDestroy();
GetParent()->PostMessage(WM_WIN_CLOSED,0,0);
delete this;
}
and send a user defined message to the parent window that your window is closed. In the parent window add a message handler for WM_WIN_CLOSED like
LRESULT CMainDialog::OnMyMethod(WPARAM wParam, LPARAM lParam)
{
m_pDialog = NULL;
return 0;
}

ASSERT(::IsWindow(m_hWnd)) issue, not sure what's wrong

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.

Discard ALT key press in CMainFrame

I'm having the following code:
CMainFrame* pFrame = new CMainFrame;
if (!pFrame)
return FALSE;
m_pMainWnd = pFrame;
// create and load the frame with its resources
pFrame->LoadFrame(IDR_APP_MAINFRAME,
WS_OVERLAPPEDWINDOW | FWS_ADDTOTITLE, NULL,
NULL);
// The one and only window has been initialized, so show and update it
pFrame->ShowWindow(SW_SHOWMAXIMIZED);
The problem is, when I press <ALT>, the menu(IDR_APP_MAINFRAME) will popup.
How can I always hide the menu and do not response to presss?
I had heard this is due to an accelerator control in MFC, but I couldn't see the control in my project solution which is using VS2008..
In your CMainFrame override PreCreateWindow and destroy the menu. Try something like this:
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
if(cs.hMenu!=NULL)
{
::DestroyMenu(cs.hMenu);
cs.hMenu = NULL;
}
return CFrameWnd::PreCreateWindow(cs);
}