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.
Related
I have CMFCRibbonBar control. I need to create my custom tooltip. My tooltip derives from CMFCToolTipCtrl and works quite well. But...
When I hover a ribbon button, tooltip shows up. That's great. But when I move the mouse out of the button, tooltip is closed. That is not what I want. I just need to be able to move the mouse on the tooltip and click the link that is on the tooltip. Imagine this is some kind of interactive tooltip. What can I do to achieve that?
OK, I've done something that is useful, but the outcome is not satisfying 100%.
So, first of all, create your own tooltip, inheriting from CMfcToolTipCtrl.
The idea is that:
- user may want to interact with your tooltip, or not. So we have to create some smart way from closing and showing the tooltip.
- We can assume, that when user hovers the tooltip with mouse, then he wants to interact.
Unfortunately whenever user moves the mouse from the ribbon button, the tooltip dissapears. But sometimes we can catch MouseMove inside it. But it's rather rare. So, we have to get the moment, when tooltip is closed by a system.
There is such a message that we can add to message map:
ON_NOTIFY_REFLECT(TTN_POP, &CAsInteractiveToolTip::OnPop)
Now, our OnPop will look like that (I am using pImpl idiom):
void CAsInteractiveToolTip::OnPop(NMHDR* pNMHDR, LRESULT* pResult)
{
if (m_pImpl->m_forceClose)
{
CMFCToolTipCtrl::OnPop(pNMHDR, pResult);
m_pImpl->m_forceOpened = false;
m_pImpl->m_forceClose = false;
m_pImpl->StopForceOpenTimer();
}
else
{
m_pImpl->StartForceOpenTimer();
}
*pResult = 0;
}
Now, what's happening here is:
- when tooltip is being closed, check if it's force closed by our code. If not, it means that it's closed by system. In such case, we have to give the user a chance to hover the mouse over our tooltip. So, we have to show the tooltip again (force it to show). This is done in timer method. StartForceOpenTimer is simple method that starts the timer:
void StartForceOpenTimer()
{
if (!m_forceOpenTimerActive)
{
m_self.SetTimer(IDT_FORCE_OPEN_TIMER, 100, (TIMERPROC)NULL);
m_forceOpenTimerActive = true;
}
}
Now, the magic starts in timer method:
void CAsInteractiveToolTip::OnForceTimer()
{
static DWORD waitForUserStartTime = 0;
static bool waitingForUserReaction = false;
if (!waitingForUserReaction)
{
//open and give the user chance to mouse move over it within 0.5 seconds
SetWindowPos(&wndTopMost, -1, -1, -1, -1, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
waitForUserStartTime = GetTickCount();
waitingForUserReaction = true;
return;
}
if (GetTickCount() - waitForUserStartTime > 500)
{
m_pImpl->StopForceOpenTimer();
m_pImpl->m_forceClose = true;
waitingForUserReaction = false;
m_pImpl->PopToolTip();
return;
}
if (m_pImpl->m_doForceOpen)
{
m_pImpl->StopForceOpenTimer();
waitingForUserReaction = false;
m_pImpl->m_forceOpened = true;
}
}
Overall idea is:
- force to show the tooltip
- wait about 0.5 second for a user to hover the mouse
- if user hovers the mouse over tooltip window, we can assume that he wants to interact. So we can leave the window opened.
- if user doens't interact with the window within 0.5 second, just close the tooltip again.
Now, PopToolTip method just starts another timer with interval of 100 ms.
And here is the other part of the magic:
void CAsInteractiveToolTip::OnPopTimer()
{
m_pImpl->StopForceOpenTimer();
KillTimer(IDT_POP_TIMER);
//Pop();
m_pImpl->m_forceClose = true;
m_pImpl->m_hdr.idFrom = 2;
m_pImpl->m_hdr.hwndFrom = GetSafeHwnd();
m_pImpl->m_hdr.code = (int)TTN_POP; //4294966774
GetParent()->SendMessage(WM_NOTIFY, 1, (LPARAM)&m_pImpl->m_hdr);
//GetParent()->SendMessage(WM_NOTIFY, 2, (LPARAM)&m_pImpl->m_hdr);
ShowWindow(SW_HIDE);
}
Now, this method should just pop (hide) the tooltip. But for some reason in my case calling Pop() method does nothing. So I would have to send WM_NOTIFY message with appropriate parameters (they are taken from my debug observations).
Now, OnPop will start again, but this time m_forceClose is set to true, so the tooltip will not show again (the first timer will not run).
Now the third part of the magic - Mouse Move. Just add it to your message map:
ON_WM_MOUSEMOVE()
And the method:
void CAsInteractiveToolTip::OnMouseMove(UINT nFlags, CPoint point)
{
m_pImpl->m_doForceOpen = true; //let the first timer know, that user wants to interact
CMFCToolTipCtrl::OnMouseMove(nFlags, point);
}
And you can just hide the tooltip when user clicks on it. Just:
void CAsInteractiveToolTip::OnLButtonDown(UINT nFlags, CPoint point)
{
m_pImpl->m_forceClose = true;
m_pImpl->PopToolTip();
}
This is not the ideal solution, but it somehow works. If anyone has any suggestions, I will be happy to hear them :)
C++ in Visual Studio 2008 in Windows XP SP3
I have a CProgressCtrl on a dialog that is being updated in a loop that takes a while to complete. It starts hidden, goes visible for the loop, then hides again when complete.
Within the loop I call a doEvents() method that processes the message queue. This is ensuring that the form is redrawn when I move it, and that the progress bar boxes increase when I update the bar with SetPos()
The first time I use the progress bar it works fine. However, on subsequent uses, the bar appears without a border, and the border isn't being redrawn when the boxes increase (these are still drawn correctly).
How can I force the border of the progress bar to be (re)drawn? I've tried Invalidate(), UpdateWindow() & RedrawWindow() but none of them make a difference.
Interestingly, whilst the loop is running, if I drag the dialog off screen and back again, the border is immediately redrawn.
In case it is relevant, here's the doEvents() method I'm using (taken from here)
void MyClass::doEvents()
{
MSG msg;
BOOL result;
while ( ::PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE ) )
{
result = ::GetMessage(&msg, NULL, 0, 0);
if (result == 0) // WM_QUIT
{
::PostQuitMessage(msg.wParam);
break;
}
else if (result == -1)
{
// Handle errors/exit application, etc.
}
else
{
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
}
}
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'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.
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.