Why is my window losing its HTMEME when I call SetWindowLongPtr(GWL_STYLE) on it? - c++

I'm coding a custom Win32 UI control that I want to incorporate visual themes in. I load themes in its WM_NCCREATE as such:
case WM_NCCREATE:
{
HTHEME hTheme = ::OpenThemeData(hWnd, L"EDIT");
assert(hTheme);
assert(::GetWindowTheme(hWnd) != 0);
}
return 1;
and then release them when control is destroyed:
case WM_DESTROY:
{
HTHEME hTheme = ::GetWindowTheme(hWnd);
assert(hTheme);
if(::CloseThemeData(hTheme) != S_OK)
{
assert(NULL);
}
}
break;
This works well, until someone tries to change that control's styles. The following call (just by itself without even changing any styles):
::SetWindowLongPtr(hChildWnd, GWL_STYLE, dwStyle);
will make GetWindowTheme on hChildWnd return NULL.
So, is it a bug or a feature?
PS. To make a reproducible Win32 example I had to adjust the stock Win32 solution from the VS 2017. (Here is its full source code.) The way it works is this: in it I create a small child control (shown in gray below) that has theme in question:
Then when you click on the white area of the main window, I try to change its styles and its theme disappears:
To see the full Win32 code for that project, I also posted it on PasteBin.

According to Window Styles document:
"After the window has been created, these styles cannot be modified,
except as noted."
Because this is not permitted, the theme engine does not always check for changed styles and in some circumstances will draw the caption based on old data. And the only guaranteed and supportable solution is for the application to destroy the window and recreate it with the new styles rather than trying to change them on the fly.
A similar discussion can be found:
http://social.msdn.microsoft.com/Forums/en/windowscompatibility/thread/7b5ef777-ff0d-4f15-afed-5588f93f0e23

Related

MFC's dialog-based app title bar highlighting visual artifacts on Windows 10 (i.e. bugs in CDialogEx)

I'm not sure why am I getting this visual artifact?
Here's how to repro:
I'm using Visual Studio 2017 Community. Create a new C++ -> MFC project:
Then specify "dialog based":
Then build as "Debug" x86 app and run it.
So I'm running it on Windows 10.
When this dialog-based process has focus, it looks as I would expect it:
but if I switch keyboard focus to some other app (by clicking on it), this dialog-based process still retains its title bar color:
I'm not sure if it's just a matter of a visual glitch or if there's a deeper mess-up with the window message handling. How do I correct it? (This wasn't an issue with older MFC projects.)
I managed to replicate your problem and found a quick fix for it.
You need to add the WM_ACTIVATE message handler to your main dialog, comment out the base class OnActivate and modify it like this:
void CMFCApplication1Dlg::OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized)
{
//CDialogEx::OnActivate(nState, pWndOther, bMinimized);
// TODO: Add your message handler code here
this->Default();
}
CWnd::Default call is needed to keep the active/inactive visualization of the default button.
OK, as much as I appreciate #VuVirt's solution, it doesn't completely remove all the bugs that are shipped in the default Dialog-based solution in VS2017. It solves the title bar focus issue, but while continuing to develop my project I encountered another bug. So I'm copy-and-pasting it from my comment to his answer:
There's still some kinda screw-up there. I'm not sure if it's related to this fix or not. Ex: If you create a button and then in its handler try to do: CFileDialog d(TRUE, NULL, NULL, OFN_HIDEREADONLY | OFN_EXPLORER, NULL, this); d.DoModal(); to open a file picker dialog. When file picker opens up, close it and see if the title bar of the parent MFC dialog window goes back to being active. In my case it remains inactive until I click onto the Windows taskbar and then back onto that MFC app.
After banging my head against the wall trying to see what is going on there, I decided to try an earlier solution proposed by #zett42 in the comments to my original question (i.e. to replace CDialogEx with CDialog) and it worked! All the bugs are gone!
So here's my verdict: CDialogEx is buggy af.
The resolution is quite simple: When you create a new dialog-based project use project-wide find-and-replace (in the Edit menu) and replace all occurrences of CDialogEx with CDialog. And that is it. (I tried to use VS2017's refactoring tool for that but it messed it up and didn't replace it all. So simple search-and-replace does the job.)
And if you think that you'll be missing some functionality without CDialogEx, then you won't. All it does (besides introducing bugs) is that it adds background images and colors to the dialog.
So until MS fixes those glaring bugs in their templates I'm sticking with this approach.
This seems to be a bug in CDialogImpl::OnActivate and CDialogImpl::OnNcActivate:
void CDialogImpl::OnNcActivate(BOOL& bActive)
{
if (m_Dlg.m_nFlags & WF_STAYACTIVE)
bActive = TRUE;
if (!m_Dlg.IsWindowEnabled())
bActive = FALSE;
}
void CDialogImpl::OnActivate(UINT nState, CWnd* pWndOther)
{
m_Dlg.m_nFlags &= ~WF_STAYACTIVE;
CWnd* pWndActive = (nState == WA_INACTIVE) ? pWndOther : &m_Dlg;
if (pWndActive != NULL)
{
BOOL bStayActive = (pWndActive->GetSafeHwnd() == m_Dlg.GetSafeHwnd()
|| pWndActive->SendMessage(WM_FLOATSTATUS, FS_SYNCACTIVE));
if (bStayActive)
m_Dlg.m_nFlags |= WF_STAYACTIVE;
}
else
{
m_Dlg.SendMessage(WM_NCPAINT, 1);
}
}
This is meant to give CDialogEx the ability to stay active, for example, when CMFCPopupMenu is shown.
But m_Dlg.SendMessage(WM_NCPAINT, 1) is a suspicious call. The usage doesn't match the documentation for WM_NCPAINT:
Parameters
wParam
A handle to the update region of the window. The update region is clipped to the window frame.
lParam
This parameter is not used.
Additionally, OnNcActivate has an override based on IsWindowEnabled(). This seems to be a patch to fix the earlier problem in OnActivate. But it causes problems elsewhere, for example when using CFileDialog in CDialogEx
Suggested solution:
Modify CDialogEx::OnActivate so that it runs the default procedure. Or, change it such that it will force repaint.
BOOL CDialogEx::OnNcActivate(BOOL active)
{
if(m_nFlags & WF_STAYACTIVE)
active = TRUE;
return(BOOL)DefWindowProc(WM_NCACTIVATE, active, 0L);
}
void CDialogEx::OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized)
{
Default();
}
or
void CDialogEx::OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized)
{
Default();
//save the previous flag
UINT previous_flag = m_nFlags;
m_nFlags &= ~WF_STAYACTIVE;
// Determine if this window should be active or not:
CWnd* pWndActive = (nState == WA_INACTIVE) ? pWndOther : this;
if(pWndActive != NULL)
{
BOOL bStayActive = pWndActive->GetSafeHwnd() == GetSafeHwnd() ||
pWndActive->SendMessage(WM_FLOATSTATUS, FS_SYNCACTIVE);
if(bStayActive)
m_nFlags |= WF_STAYACTIVE;
}
if(previous_flag != m_nFlags && previous_flag & WF_STAYACTIVE)
{
//if the flag is changed,
//and if WF_STAYACTIVE was previously set,
//then OnNcActivate had handled it wrongly, do it again
SendMessage(WM_NCACTIVATE, FALSE); //<- less wrong!
}
}
This should work with CMFCPopupMenu for example. The MFC menu will open without deactivating the dialog.
I am not sure what SendMessage(WM_FLOATSTATUS, FS_SYNCACTIVE) is for, I haven't been able to test it... If it's necessary, it seems the code could be added on OnNcActivate, and then OnActivate is left alone.

Reset Layout of CDockablePane

I migrated my MFC MDI application to use the new MFC Feature Pack. I have many toolbars and dockable panes. As far as I understand, the location and size of each of them is saved in the registry when closing the application, and loaded when loading the main frame.
I want to add a feature in my application to reset the layout of the toolbars/panes to the original layout.
I have tabbedpanes also in my application.
sometimes I will dock separate panes to tabbed panes.
Is there a way to actually reset the layout of my application AFTER it has been loaded?
Visual Studio has a similar feature called "Reset Window Layout".
I am getting samples in the internet for restoring mainframe window using SetWindowPlacement() and GetWindowPlacement().
I don't know how to use these functions for toolbars and CDockablePanes and achieve my requirement?
Is there any other solution apart from using SetWindowPlacement() and GetWindowPlacement()?
I am able to meet my requirement using below code.
void CMainFrame::OnPanesResetLayout()
{
CDockingManager* pDockMgr = GetDockingManager();
if (pDockMgr == NULL)return;
CRect rect;
rect.SetRectEmpty();
ClientToScreen(rect);
SetRedraw(FALSE);
CObList list;
pDockMgr->GetPaneList(list, TRUE,0,TRUE);
// UnDock and hide DockingControlBars
POSITION pos;
for (pos = list.GetHeadPosition(); pos != NULL;)
{
CBasePane* pBarNext = (CBasePane*) list.GetNext(pos);
if (!::IsWindow(pBarNext->m_hWnd))continue;
CDockablePane* pBar = DYNAMIC_DOWNCAST(CDockablePane, pBarNext);
if (pBar != NULL)
{
if(pBar->IsAutoHideMode()) pBar->SetAutoHideMode(FALSE, CBRS_ALIGN_ANY);/*ToggleAutoHide();*/
if (pBar->IsMDITabbed ())
continue;
pBar->UndockPane();
ShowPane(pBar, FALSE,FALSE, FALSE);
}
CMFCToolbar* pToolBar = DYNAMIC_DOWNCAST(CMFCToolbar, pBarNext);
if(pToolBar)
pToolBar->m_recentDockInfo.m_recentSliderInfo.m_rectDockedRect = rect;
}
m_wndBar1.DockToFrameWindow(CBRS_LEFT,m_wndBar1.GetAHRestoredRect());
ShowPane(m_wndBar1, TRUE,FALSE, FALSE);
m_wndBar2.DockToFrameWindow(CBRS_RIGHT,m_wndBar2.GetAHRestoredRect());
ShowPane(m_wndBar2, TRUE,FALSE, FALSE);
//for tabbed pane
CTabbedPane *pTabbedPane;
m_wndTab1.DockToFrameWindow(CBRS_BOTTOM,m_wndTab1.GetAHRestoredRect());
m_wndTab2.AttachToTabWnd(&m_wndTab1, DM_SHOW, FALSE,reinterpret_cast<CDockablePane**>(&pTabbedPane));
m_wndTab3.AttachToTabWnd(&m_wndTab1, DM_SHOW, FALSE,reinterpret_cast<CDockablePane**>(&pTabbedPane));
ShowPane(m_wndTab1, TRUE,FALSE, FALSE);
ShowPane(m_wndTab2, TRUE,FALSE, FALSE);
ShowPane(m_wndTab3, TRUE,FALSE, FALSE);
SetRedraw(TRUE);
RedrawWindow(NULL, NULL, RDW_INVALIDATE | RDW_UPDATENOW | RDW_ERASE | RDW_ALLCHILDREN);
AdjustClientArea();
}
As described in my comment above, one option to restore the initial layout from a running application would be to use the methods provided by CDockablePane, specifically
AttachToTabWindow
DockToWindow and
ShowPane
A second option, which would require restarting your application, is to call EnableLoadDockState(FALSE) in the constructor of your CFrameWndEx derived class. This would prevent loading the stored dock state and, as a consequence, restore the initial layout.
A simple method to solve this is to delete from registry key all keys which store the panels info: "BasePane" and "Pane" from "Workspace" registry folder from your app registry entry :) Easy.

How to get ribbon control in MFC feature pack to process ID_WINDOW_TILE_VERT

I'm porting an older MFC application to use MFC feature pack with ribbon UI and have found the ribbon UI doesn't process MDI windows tiling commands such ID_WINDOW_TILE_VERT. Is there a way to enable this functionality?
Single stepping through the MFC source I get as far as the following in C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\src\mfc\winmdi.cpp, which seems correct;
BOOL CMDIFrameWnd::OnMDIWindowCmd(UINT nID)
{
ASSERT(m_hWndMDIClient != NULL);
UINT msg;
UINT wParam = 0;
switch (nID)
{
default:
return FALSE; // not for us
case ID_WINDOW_ARRANGE:
msg = WM_MDIICONARRANGE;
break;
case ID_WINDOW_CASCADE:
msg = WM_MDICASCADE;
break;
case ID_WINDOW_TILE_HORZ:
wParam = MDITILE_HORIZONTAL;
// fall through
case ID_WINDOW_TILE_VERT:
ASSERT(MDITILE_VERTICAL == 0);
msg = WM_MDITILE;
break;
}
::SendMessage(m_hWndMDIClient, msg, wParam, 0);
return TRUE;
}
I have also tried calling
MDITile(MDITILE_HORIZONTAL);
directly, which essentially does the same thing and does not work.
From some experimentation, when the MFC mdi interface is based on CMDIFrameWndEx frames hosting dockable panes based CMDIChildWndEx and tabbed documents are enabled, floating windows are not available and hence neither are tiling or cascading.
To enable tiling, simply remove the line
EnableMDITabbedGroups(TRUE, mdiTabParams);
from your CMainFrame::OnCreate method. The downside is you also lose your nice tabbed document UI. FWIW, I also tried calling EnableDocking(CBRS_FLOAT_MULTI) after enabling tabbed groups, but it does not make any difference. Also under discussion here
Update: In order to keep the tabbed interface and split the screens, the following alternative works well to split a single horizontal view with multiple tabs into two views, with current tab in the new view.
void SplitViews(CMDIFrameWndEx *pFrame)
{
pFrame->MDITabNewGroup();
pFrame->MDITabMoveToNextGroup();
}

MFC: CToolTipCtrl::SetTipBkColor not working

I am trying to customize my tooltips with dark background so that they are clearly visbile. I have tried using the CToolTipCtrl::SetTipBkColor for that purpose. But I am still seeing the default style tooltip with silver gradient background with dropshadow.
Please find the sample code I have used for this purpose.
BOOL CAboutDlg::OnInitDialog()
{
CDialogEx::OnInitDialog();
m_ToolTipCtrl.Create(this, TTS_ALWAYSTIP | TTS_NOPREFIX );
m_ToolTipCtrl.Activate(TRUE);
m_ToolTipCtrl.SetDelayTime(TTDT_INITIAL, 0);
m_ToolTipCtrl.SetTipBkColor(RGB(255,0,0));
CWnd* pWnd = GetDlgItem(IDC_BUTTON1);
m_ToolTipCtrl.AddTool(pWnd,_T("TOOLTIP Displayed"));
return TRUE; // return TRUE unless you set the focus to a control
// EXCEPTION: OCX Property Pages should return FALSE
}
Upon searching I have found that I need to disable visual styles. I don't know what it really means. I am thinking it has something to do with CToolTipCtrl:::SetWindowTheme but have no clue of what value needs to be passed.
Simply pass a blank string for the theme:
m_ToolTipCtrl.SetWindowTheme("");

Maximized Window Restores to Full Screen

Using CWnd::ShowWindow(SW_SHOWMAXIMIZED) maximizes my app window as expected.
However, when clicking the restore button on the app (or double clicking the title-bar), the restored size is the same size as the maximized window, which is confusing for the user.
Using this alternative code has the same problem:
WINDOWPLACEMENT wndpl;
GetWindowPlacement(&wndpl);
wndpl.showCmd = SW_SHOWMAXIMIZED;
SetWindowPlacement(&wndpl);
How can I keep the default un-maximized size when restoring.
I've solved my problem, and the solution might solve yours too. My problem was that even though I called SetWindowPlacement(&wndpl) within CMainFrame::OnCreate the window was not properly restored if it was maximized. I added two lines of code before SetWindowPlacement, and now it works as expected.
CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
...
// Obtain wndpl, maybe from registry
AfxGetApp()->m_nCmdShow = wndpl.showCmd;
wndpl.showCmd = SW_SHOW;
SetWindowPlacement(&wndpl);
}
These two lines helps underlying code not to mess things up when calling ActivateFrame, which calls ShowWindow with parameter obtained from CWinApp::m_nCmdShow.
All information are in the file with extension .RC. I never used a Maximize/Restore procedures though you should look for a 'DIALOGEX' for the same window. You can change it using any editor (notepad, ultraedit etc.)