Introduction
When you generate a multi-document interface application with Visual Studio Wizard, you can switch between several opened documents using key combination Ctrl+Tab. And every time you press Tab key while Ctrl key is down, you will switch the next open document. If you want to return on some previous open document, you can do it by walking through to already open documents. If you test this behavior on professional editors, including Visual Studio, you will notice that this behavior is a little optimized. In what way? If you release Ctrl key after every document switched and you press Ctrl+Tab again, you will activate the previous open document, not the next one. You can see this inside Visual Studio editor, Notepad++, and a lot of professional editors, even on some browsers, like Opera (this is one of the reason why I prefer Opera browser. :) At this time, Chrome doesn't have such behavior, or at least I didn't know to have it.
Background
Let's make things more clear. There are two cases:
- Case 1. Ctrl+Tab->switch next document. Keep Ctrl key pressed. Press again Tab key->switch next document. Keep Ctrl key pressed. Press Tab key again->switch next document. And so on.
- Case 2. Ctrl+Tab->switch next document. Release Ctrl key. Press again Ctrl+Tab->switch next document. Release Ctrl key. Press again Ctrl+Tab key->switch next document.
Those two cases reveal that even if you use different key combinations, you will have the same behavior on default MDI application. This will not happen on Visual Studio editor. In case 2, Visual Studio will activate the previous document, not the next one. This is more convenient. Why? Suppose you have 10 documents open. You activate document 3. Then you activate document 10. If you go back on document 3 and want to navigate to document 10 again, you will have to pass through all the documents from 3 to 10. And what if you have more than 10 documents open?
Using the Code
Ok. So, the professional editors can handle easily this document switching and activation behavior by delivering the last visited document to the user at Ctrl+Tab key combination. My solution adds this improvement at the default MDI application with some minimal changes.
These changes are:
- Adding in
CMainFrame
an array of pointers for opened childframe.
CTypedPtrArray<CPtrArray, CMDIChildWnd*> m_arrChild;
And then handling here adding, removing and synchronizing the opened childframes with this array of pointers.
ON_MESSAGE(WMU_CHILDFRAMEADDED, &CMainFrame::OnChildFrameAdded)
ON_MESSAGE(WMU_CHILDFRAMEREMOVED, &CMainFrame::OnChildFrameRemoved)
ON_MESSAGE(WMU_CHILDFRAMEACTIVATE, &CMainFrame::OnChildFrameActivate)
ON_MESSAGE(WMU_CHILDFRAMEACTIVATED, &CMainFrame::OnChildFrameActivated)
...
...
LRESULT CMainFrame::OnChildFrameAdded(WPARAM wParam, LPARAM lParam)
{
for (int i = 0; i < m_arrChild.GetSize(); ++i)
{
if (reinterpret_cast<CMDIChildWnd*>(lParam) == m_arrChild.GetAt(i))
{
return 0;
}
}
m_arrChild.InsertAt(0, (CMDIChildWnd*)lParam);
return 1;
}
LRESULT CMainFrame::OnChildFrameRemoved(WPARAM wParam, LPARAM lParam)
{
for (int i = 0; i < m_arrChild.GetSize(); ++i)
{
if (reinterpret_cast<CMDIChildWnd*>(lParam) == m_arrChild.GetAt(i))
{
m_arrChild.RemoveAt(i);
break;
}
}
return 1;
}
LRESULT CMainFrame::OnChildFrameActivate(WPARAM wParam, LPARAM lParam)
{
for (int i = 0; i < m_arrChild.GetSize(); ++i)
{
if (reinterpret_cast<CMDIChildWnd*>(lParam) == m_arrChild.GetAt(i))
{
CMDIChildWnd* pMDIChild = m_arrChild.GetAt(i);
if (pMDIChild->IsIconic())
pMDIChild->ShowWindow(SW_RESTORE);
MDIActivate(pMDIChild);
CMDIChildWndEx* pMDIChildEx = DYNAMIC_DOWNCAST(CMDIChildWndEx, pMDIChild);
if (NULL != pMDIChildEx->GetSafeHwnd() && pMDIChildEx->IsTabbedMDIChild())
{
CMFCTabCtrl* pTabCtrl = pMDIChildEx->GetRelatedTabGroup();
if (NULL != pTabCtrl->GetSafeHwnd())
{
const int nTab = pTabCtrl->GetTabFromHwnd(pMDIChildEx->GetSafeHwnd());
pTabCtrl->SetActiveTab(nTab);
}
}
break;
}
}
return 1;
}
LRESULT CMainFrame::OnChildFrameActivated(WPARAM wParam, LPARAM lParam)
{
for (int i = 0; i < m_arrChild.GetSize(); ++i)
{
if (reinterpret_cast<CMDIChildWnd*>(lParam) == m_arrChild.GetAt(i))
{
m_arrChild.RemoveAt(i);
break;
}
}
m_arrChild.InsertAt(0, (CMDIChildWnd*)lParam);
return 1;
}
- In
childframe
, send to mainframe the information about creating, destroying and activate the childframe
.
CChildFrame::CChildFrame() noexcept
{
::PostMessage(AfxGetMainWnd()->GetSafeHwnd(), WMU_CHILDFRAMEADDED, 0,
reinterpret_cast<LPARAM>(this));
}
CChildFrame::~CChildFrame()
{
::PostMessage(AfxGetMainWnd()->GetSafeHwnd(), WMU_CHILDFRAMEREMOVED,
0, reinterpret_cast<LPARAM>(this));
}
void CChildFrame::OnMDIActivate(BOOL bActivate, CWnd* pActivateWnd, CWnd* pDeactivateWnd)
{
CMDIChildWndEx::OnMDIActivate(bActivate, pActivateWnd, pDeactivateWnd);
if(bActivate)
::PostMessage(AfxGetMainWnd()->GetSafeHwnd(), WMU_CHILDFRAMEACTIVATED,
0, reinterpret_cast<LPARAM>(this));
}
- Add
CWindowsManagerDialog
class to your project for listing the opened childframe
s when you press Ctrl+Tab.
CWindowsManagerDialog
is an adaptation for CMFCWindowsManagerDialog
, because this one has a different purpose.
- Handling
PreTranslateMessage
in CMainFrame
and CWindowsManagerDialog
to follow the user key presses.
BOOL CMainFrame::PreTranslateMessage(MSG* pMsg)
{
if (WM_KEYDOWN == pMsg->message && VK_TAB == pMsg->wParam &&
GetAsyncKeyState(VK_CONTROL) < 0 && m_arrChild.GetSize() > 1)
{
CWindowsManagerDialog* pDlg = new CWindowsManagerDialog;
pDlg->Create(CWindowsManagerDialog::IDD, this);
pDlg->ShowWindow(SW_SHOW);
return TRUE;
}
return CMDIFrameWndEx::PreTranslateMessage(pMsg);
}
With all of these changes, you will have the document switching and activation behavior like Visual Studio editor. This code will run without any problem if you have CChildFrame
derived from CMDIChildWnd
or CMDIChildWndEx
as well, and your application has or not the tabbed childframe
, the code will automatically know it.
Known issues: The attached zip file contain an example of this approach. And it is built in Visual Studio 2017. In CMainFrame
is using the IsTabbedMDIChild()
call. This call might be unavailable in older Visual Studio versions. Is this case, you should write it into your CMainFrame
header like that:
public:
BOOL IsTabbedMDIChild() const { return NULL != GetSafeHwnd() && AreMDITabs(); }
and everything should be alright.
If you think this is useful, you can adapt it, modify it and implement it as you wish. Enjoy it!
Points of Interest
With the simple changes from above, anyone who has an MDI application can turn it into one that behaves like a professional editor does.
History
- 5th January, 2020: Published article
- 27th September, 2023: Corrected documents order at closing