I'm Michael Suodenjoki - a software engineer living in Kgs. Lyngby, north of Copenhagen, Denmark. This is my personal site containing my blog, photos, articles and main interests.
I'm Michael Suodenjoki - a software engineer living in Kgs. Lyngby, north of Copenhagen, Denmark. This is my personal site containing my blog, photos, articles and main interests.
Updated 2011.02.03 12:54 +0100 |
Ikke tilgængelig på Dansk
This article describes how you in C++/MFC can implement drag and drop tab order in Windows tab controls.
Looking for a C# version? Try this one: http://www.codeproject.com/cs/miscctrl/DraggableTabControl.asp
1 Introduction
1.1 The Tab Control Features
2 Basic Drag and Drop Functionality
2.1 Avoiding Trouble with Mouse Capture
2.2 Let's see some code
3 Drawing the Insertion Marker
4 Lets Drop It
5 Ahh, Scrolling You Say
6 Keeping Control of the Tab Sort Order
7 Conclusion
The code in this article requires:
Download the tabsort.zip source file for this article.
The tabsort.zip file contains the Visual Studio .NET solution and source files.
I recently worked in a project where the customer wanted to be able to order the tabs in a tab control in an easy manner. The idea of using drag and drop sprung into mind. For the user this is an easy way to do ordering instead of e.g. go into a setup dialog or something similar. Sorting is most useful if you have many tabs on a single line in which not all necessarily are visible. The user may need to scroll to the desired tab and this may be tedious if he has to do that every time. So the user may choose to order the tab control so that the most used tabs are displayed first and therefore always be visible. Of course you could setup the tab control as multi line (several rows of tabs), but it's my experience that multi lines confuse a lot of users.
Even though that we're going to implement drag and drop tab ordering, this doesn't mean that you shouldn't implement a dialog where the user can define the tab order. As always; what the user can do with the mouse should also be possible with the keyboard alone - and to some degree the other way around. That's good usability practice and helps people with disabilities.
Furthermore ordering using drag and drop is not uncommon in other Windows control. The best example is the header control (version 4.70 or later) in a list view control like the one on the right hand side of the Windows Explorer (see figure 1 below). In case of keyboard control the user can choose which columns to display, their order etc. from the Explorer's menu item "View | Choose details...".
Why can a tab control not order its tabs in the same manner? I don't know but maybe the guys at Microsoft have made some usability studies that can explain it. Please contact me if you know why or have suggestions for it. It would be nice to hear the reasons - if there are any.
I started out by doing a search on the internet. Maybe it was possible to buy some third-party tab controls that implemented tab ordering via drag and drop. Or maybe I could find some examples on www.codeguru.com, on www.codeproject.com or in some of the newsgroups. Interestingly it was not possible for me to find any. So I was left by doing the "fun" work myself.
Note: When you search in e.g. the MSDN Library you will retrieve a lot of hits on OLE Drag and Drop. Basically this is another way of doing copy and paste operations - something which is different from what we are doing with the tab control. You would use OLE Drag and Drop it if you want your users to be able to drop something within a tab.
The remainder of this article describes how I in MFC implemented basic tab order via drag and drop.
The TabControl class that this article describes supports the following features:
Note that one of the things not implemented in this article is a drag-image. If that was implemented it would help the user seeing which tab that he's actually dragging.
Since the tab control itself do not support drag and drop tab ordering we must implement it. The first issue is how we obtain basic drag and drop functionality. Drag and drop are usually initiated by the user clicking on the left mouse button and while keeping the left mouse button down dragging the mouse pointer to the location where a drop is done when the left mouse button is released. A common solution is to handle left mouse messages (events) as sketched in the pseudo code below:
On Left Mouse Button Down if were initiating a drag then { // We're dragging Capture the Mouse (so that all mouse events go into this window) // Start dragging bDragging = true } On Left Mouse Button Up if bDragging then { // We're dropping Release the Mouse Capture // Stop dragging bDragging = false // Make the drop } On Mouse Move if bDragging then { // Do something while dragging e.g. // 1) display drag 'n drop cursor (drop allowed vs. drop not allowed), and/or // 2) display drag image (the stuff that you're dragging), and/or // 3) display insertion marker (where the stuff will be dropped/inserted), and/or // 4) if necessary scroll the view so that we can drop/insert in places that were // out of view before we initiated the drag }
As the pseudo code suggest there are a different things to consider when the mouse is moving during the drag and drop process. I've outlined four issues and in the context of our tab control issue 3 and 4 are the most important, so I will focus on these two issues. Issue 3 is the subject of section 3 and issue 4 is the subject of section 5 . Issue 1 will not be of interest for our tab control implementation, since we not actually dropping something - we're merely moving (ordering) something. Issue 2 will not be implemented in this article. You can say its left as an exercise for the reader.
Let's ask ourselves whether the pseudo code above is correct? What happens if for some reason our control never receives a left mouse button up message? And can that happen? The answer is yes - it can happen. Chris Branch's article "Avoiding Trouble with Mouse Capture" from the Windows Developer Journal, December 1997 explains some of the problems that we should consider. If you have not already read it I suggest that you do. It will explain why we add an extra message handler to our pseudo code:
On WM_CAPTURECHANGED if bDragging then { // Stop dragging bDragging = false; // Make additional clean up (if necessary) }
Okay, let's get that into some MFC code. We define our new class TabControl which we derive from MFC's CTabCtrl and we define the three notification handler functions as illustrated above. We also define a member data variable named m_bDragging that specifies whether a drag 'n drop action is in progress:
class TabControl: public CTabCtrl
{
public:
TabControl();
// Command/Notification Handlers
afx_msg void OnLButtonDown( UINT nFlags, CPoint point );
afx_msg void OnLButtonUp( UINT nFlags, CPoint point );
afx_msg void OnMouseMove( UINT nFlags, CPoint point );
afx_msg void OnCaptureChanged( CWnd* );
private:
bool m_bDragging; // Specifies whether drag 'n drop is in progress.
DECLARE_MESSAGE_MAP()
};
The definition of the class looks something like the following:
BEGIN_MESSAGE_MAP(TabControl, CTabCtrl )
ON_WM_LBUTTONDOWN( )
ON_WM_LBUTTONUP( )
ON_WM_MOUSEMOVE( )
ON_WM_CAPTURECHANGED( )
END_MESSAGE_MAP()
//
// 'TabControl::TabControl'
//
TabControl::TabControl()
: m_bDragging(false)
{
}
//
// 'TabControl::OnLButtonDown'
//
// @mfunc Handler that is called when the left mouse button is activated.
// The handler examines whether we have initiated a drag 'n drop
// process.
//
void TabControl::OnLButtonDown( UINT nFlags, CPoint point )
{
if( DragDetect(point) )
{
// Yes, we're beginning to drag, so capture the mouse...
m_bDragging=true;
SetCapture();
// ...
}
else
{
CTabCtrl::OnLButtonDown(nFlags,point);
}
// Note: We're not calling the base classes CTabCtrl::OnLButtonDown
// every time, because we want to be able to drag a tab without
// actually selecting it first (so that it gets the focus).
}
//
// 'TabControl::OnLButtonUp'
//
// @mfunc Handler that is called when the left mouse button is released.
// Is used to stop the drag 'n drop process, releases the mouse
// capture and reorders the tabs accordingly to insertion (drop)
// position.
//
void TabControl::OnLButtonUp( UINT nFlags, CPoint point )
{
CTabCtrl::OnLButtonUp(nFlags,point);
if( m_bDragging )
{
// We're going to drop something now...
// Stop the dragging process and release the mouse capture
// This will eventually call our OnCaptureChanged which stops the dragging
ReleaseCapture();
}
}
//
// 'TabControl::OnMouseMove'
//
// @mfunc Handler that is called when the mouse is moved.
//
void TabControl::OnMouseMove( UINT nFlags, CPoint point )
{
CTabCtrl::OnMouseMove(nFlags,point);
// This code added to do extra check - shouldn't be strictly necessary!
if( !(nFlags & MK_LBUTTON) )
m_bDragging = false;
if( m_bDragging )
{
// ...
}
}
//
// 'TabControl::OnCaptureChanged'
//
// @mfunc Handler that is called when the WM_CAPTURECHANGED message is received. It notifies
// us that we do not longer capture the mouse. Therefore we must stop or drag 'n drop
// process. Clean up code etc.
//
void TabControl::OnCaptureChanged( CWnd* )
{
if( m_bDragging )
{
// Stop the dragging
m_bDragging = false;
// ...
}
}
Note that we're actually using the Windows SDK function DragDetect that will tell us whether a drag action is initiated. We could also have coded something similar ourselves, but it's a lot smarter using the DragDetect function.
Another thing to note is that we would like the user to order a tab without necessarily selecting the (set it in focus) tab first. This is a nice feature if for some reason it takes a long time to draw the contents of a tab. That's why we do not always call the base member of CTabCtrl::OnLButtonDown - it would namely select the tab.
When a drag and drop (order) process is in progress we would like - as the common header control - to display an insertion marker for the user so that he can see where the tab will be inserted.
Let's first define the function that paints the insertion marker given a specific mouse position. We do not assume anything about the position. It may be on a tab, it may be within the tab control or it may be outside the tab control. In any case the function should draw the insertion mark in between two tab's at the tab locations closest to the specified position. Lets define that when the mouse is in the left half of a tab the marker should be painted to the left and when is in the right half of a tab we paint it to the right. What about color? Lets use the color that also used when highlighting something. We can get that with the SDK function GetSysColor using COLOR_HOTLIGHT.
Since the insertion marker will change location during the drag and drop process we also need a way to delete old painted markers. We keep track of the current marker location within a member variable called m_InsertPosRect and when we want to delete it we simply invalidate the rectangle forcing a repaint of the part of the tab control where the marker were located. During the process of calculation of whether the position is on a tab we also have the opportunity of storing the actually (current) destination tab - the zero indexed tab number in which we the source tag will be inserted (dropped).
So we can extend our class with two member data variables and a new utility member function DrawIndicator:
class TabControl: public CTabCtrl
{
public:
// ...
private:
UINT m_nDstTab; // Specifies the destination tab (drop position).
CRect m_InsertPosRect;
// ...
// Utility members
bool DrawIndicator( CPoint point );
};
And the definition of the DrawIndicator member function itself:
#ifndef COLOR_HOTLIGHT
#define COLOR_HOTLIGHT 26
#endif
#define INDICATOR_WIDTH 2
#define INDICATOR_COLOR COLOR_HOTLIGHT
//
// 'TabControl::DrawIndicator'
//
// @mfunc Utility member function to draw the (drop) indicator of where the
// tab will be inserted.
//
bool TabControl::DrawIndicator(
CPoint point // @parm Specifies a position (e.g. the mouse pointer position) which
// will be used to determine whether the indicator should be
// painted to the left or right of the indicator.
)
{
TCHITTESTINFO hitinfo;
hitinfo.pt = point;
CRect rect;
if( GetItemRect( 0, &rect ) )
{
// Adjust position to top of tab control (allow the mouse the actually
// be outside the top of tab control and still be able to find the right
// tab index
hitinfo.pt.y = rect.top;
}
// Find the destination tab...
unsigned int nTab = HitTest( &hitinfo );
if( hitinfo.flags != TCHT_NOWHERE )
{
m_nDstTab = nTab;
}
else
{
if( m_nDstTab == GetItemCount() )
m_nDstTab--;
}
bool bRet = GetItemRect(m_nDstTab,&rect);
CRect newInsertPosRect(rect.left-1,rect.top,rect.left-1+INDICATOR_WIDTH, rect.bottom);
// Determine whether the indicator should be painted at the right of
// the tab - in which case we update the indicator position and the
// destination tab ...
if( point.x >= rect.right-rect.Width()/2 )
{
newInsertPosRect.MoveToX(rect.right-1);
m_nDstTab++;
}
if( newInsertPosRect != m_InsertPosRect )
{
// Remove the current indicator by invalidate the rectangle (forces repaint)
InvalidateRect(&m_InsertPosRect);
// Update to new insert indicator position...
m_InsertPosRect = newInsertPosRect;
}
// Create a simple device context in which we initialize the pen and brush
// that we will use for drawing the new indicator...
CClientDC dc( this );
CBrush brush(GetSysColor(INDICATOR_COLOR));
CPen pen(PS_SOLID,1,GetSysColor(INDICATOR_COLOR));
dc.SelectObject( &brush );
dc.SelectObject( &pen );
// Draw the insert indicator
dc.Rectangle(m_InsertPosRect);
return true; // success
NOTE: When the tab control has style TCS_VERTICAL | TCS_MULTILINE, the insertion mark should be horizontal instead of vertical. I've not implemented this - so again you can say its left as an exercise.
The call to DrawIndicator is put into the OnMouseMove function:
//
// 'TabControl::OnMouseMove'
//
// @mfunc Handler that is called when the mouse is moved.
//
void TabControl::OnMouseMove( UINT nFlags, CPoint point )
{
CTabCtrl::OnMouseMove(nFlags,point);
// This code added to do extra check - shouldn't be strictly necessary!
if( !(nFlags & MK_LBUTTON) )
m_bDragging = false;
if( m_bDragging )
{
// Draw the indicator
DrawIndicator(point);
}
}
To finish of the basic drag and drop feature we must implement our drop routine - or the one that's actually reorders the tabs.
So we declare and define a new utility member function ReorderTab as given below:
//
// 'TabControl::ReorderTab'
//
// @mfunc Reorders the tab by moving the source tab to the position of the
// destination tab.
//
// @devnote Currently the tab is fully reconstructed by first removing all
// tabs and then rebuild. We could be optimized by only moving
// the necessary tabs.
//
void TabControl::ReorderTab( unsigned int nSrcTab, unsigned int nDstTab )
{
if( nSrcTab == nDstTab )
return true; // Return success (we didn't need to do anything
// Remember the current selected tab
unsigned int nSelectedTab = GetCurSel();
// Get information from the tab to move (to be deleted)
TCHAR sBuffer[50];
TCITEM item;
item.mask = TCIF_IMAGE | TCIF_PARAM | TCIF_TEXT;
item.pszText = sBuffer;
item.cchTextMax = sizeof(sBuffer)/sizeof(TCHAR);
GetItem(nSrcTab,&item);
DeleteItem(nSrcTab);
// Insert it at new location
InsertItem( nDstTab-(m_nDstTab > m_nSrcTab ? 1 : 0), &item );
// Setup new selected tab
if( nSelectedTab == nSrcTab )
SetCurSel( nDstTab-(m_nDstTab > m_nSrcTab ? 1 : 0) );
else
{
if( nSelectedTab > nSrcTab && nSelectedTab < nDstTab )
SetCurSel( nSelectedTab-1 );
if( nSelectedTab < nSrcTab && nSelectedTab > nDstTab )
SetCurSel( nSelectedTab+1 );
}
// Force update of tab control
// Necessary to do so that notified clients ('users') - by selection change call
// below - can draw the tab contents in correct tab.
UpdateWindow();
NMHDR nmh;
nmh.hwndFrom = GetSafeHwnd();
nmh.idFrom = GetDlgCtrlID();
nmh.code = TCN_SELCHANGE;
// Notify parent that selection has changed
GetParent()->SendMessage(WM_NOTIFY, nmh.idFrom, (LPARAM)&nmh);
}
As you can see it is quite simple, we delete the source tag and insert it into the destination. Note that we adjust the insert destination by subtracting 1 if the destination tab is larger than the source tab. The reason is that a tab with lower position could have been deleted within the tab control by the DeleteItem call.
Also note that were doing some calculation to adjust the selected tab and that we notify the parent window about the selection change, so that any tab content can be redrawn.
So now you ask: Where have you initialized the m_nSrcTab member? Well I haven't really mentioned member data variable that before, but inside the OnLButtonDown function it seems to be a reasonably place to initialize the source tab, we just need to find it, which we do by calling HitTest, as illustrated below:
//
// 'TabControl::OnLButtonDown'
//
// @mfunc Handler that is called when the left mouse button is activated.
// The handler examines whether we have initiated a drag 'n drop
// process.
//
void TabControl::OnLButtonDown( UINT nFlags, CPoint point )
{
if( DragDetect(point) )
{
// Yes, we're beginning to drag, so capture the mouse...
m_bDragging=true;
// Find and remember the source tab (the one we're going to move/drag 'n drop)
TCHITTESTINFO hitinfo;
hitinfo.pt=point;
m_nSrcTab = HitTest( &hitinfo );
m_nDstTab = m_nSrcTab;
SetCapture();
}
else
{
CTabCtrl::OnLButtonDown(nFlags,point);
}
}
Up to now the drag and drop tab ordering may function well as long the tab control contain a few tabs. When more tabs are added so that the little scrollbar appears (actually its a up/down spin control) the user quickly finds it annoying that he cannot move the tab outside to the invisible tabs. So we need to implement scrolling while we're dragging. Basically, when the mouse is on the right of the tab control we scroll right, when the mouse is on the left of the tab control we scroll left.
I've seen a few methods around which are more or less user friendly. One method is based on a timer. When the mouse is outside the tab control a timer is setup to call a timer function in a regular interval. Inside the timer function a scroll message is sent to the tab control.
The method I will be using is simply to send the scroll messages inside the OnMouseMove.
To get the current scroll position we need to ask the associated spin control (if any). So we need to find the spin control first using a simple search algorithm - iterating through the tab controls children until the appropriate window with the spin control class come up or we simply calls FindWindowEx which do the same thing.
Before sending the scroll message (WM_HSCROLL) we invalidate the current marker position so that the scrolling doesn't make more "mysterious" markers appear.
//
// 'TabControl::OnMouseMove'
//
// @mfunc Handler that is called when the mouse is moved.
//
void TabControl::OnMouseMove( UINT nFlags, CPoint point )
{
CTabCtrl::OnMouseMove(nFlags,point);
// This code added to do extra check - shouldn't be strictly necessary!
if( !(nFlags & MK_LBUTTON) )
m_bDragging = false;
if( m_bDragging )
{
// Draw the indicator
DrawIndicator(point);
// Get the up-down (spin) control that is associated with the tab control
// and which contains scroll position information.
if( !m_pSpinCtrl )
{
CWnd * pWnd = FindWindowEx( GetSafeHwnd(), 0, _T("msctls_updown32"), 0 );
if( pWnd )
{
// DevNote: It may be somewhat of an overkill to use the MFC version
// of the CSpinButtonCtrl since were actually only using it
// for retrieving the current scroll position (GetPos). A simple
// HWND could have been enough.
m_pSpinCtrl = new CSpinButtonCtrl;
m_pSpinCtrl->Attach(pWnd->GetSafeHwnd());
}
}
CRect rect;
GetClientRect(&rect);
// Examine whether we should scroll left...
if( point.x < rect.left && m_pSpinCtrl )
{
int nPos = LOWORD(m_pSpinCtrl->GetPos());
if( nPos > 0 )
{
InvalidateRect(&m_InsertPosRect,false);
ZeroMemory(&m_InsertPosRect,sizeof(m_InsertPosRect));
SendMessage(WM_HSCROLL,MAKEWPARAM(SB_THUMBPOSITION,nPos-1),0);
}
}
// Examine whether we should scroll right...
if( point.x > rect.right && m_pSpinCtrl && m_pSpinCtrl->IsWindowVisible())
{
InvalidateRect(&m_InsertPosRect,false);
ZeroMemory(&m_InsertPosRect,sizeof(m_InsertPosRect));
int nPos = LOWORD(m_pSpinCtrl->GetPos());
SendMessage(WM_HSCROLL,MAKEWPARAM(SB_THUMBPOSITION,nPos+1),0);
}
}
}
Okay, so far so good. Until know everything seems to be fine. We can drag and drop order our tabs. But what happens if we want to display something inside the tabs?
For simplicity lets say that all tabs contains the same control in its contents but that - depending on the selected tab - the control displays something different. This is not an uncommon situation. Often a list view control is used inside that tabs, however in this example we will be using a simple static text control that displays the current selected tab number.
In more complex situations it is normal to use property sheets, which each define the content of a tab.
The code that do the drawing of the tab contents are usually located in the dialog (or view) that contains the tab control. Usually the dialog has setup a handler so that it can be notified about when a tab is selected.
Lets create a simple dialog (see figure 2) in where we can put our tab control and the static text control:
class TestDlg : public CDialog
{
public:
TestDlg( CWnd* pParent = NULL );
public:
enum { IDD = IDD_APPDIALOG };
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support
void OnClickTabCtrl(NMHDR* pNotifyStruct, LRESULT* pResult);
protected:
virtual BOOL OnInitDialog();
unsigned int m_nMaxTabs; // Max number of tabs to insert into tab control
TabControl m_TabControl;
CStatic m_TextControl;
DECLARE_MESSAGE_MAP()
};
And the definition of it:
// TestDlg dialog
//
BEGIN_MESSAGE_MAP(TestDlg, CDialog)
// Tab notifications
ON_NOTIFY(TCN_SELCHANGE, IDC_TAB , OnClickTabCtrl)
END_MESSAGE_MAP()
TestDlg::TestDlg( CWnd* pParent /*=NULL*/)
: CDialog(TestDlg::IDD, pParent), m_nMaxTabs(20)
{
}
void TestDlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
DDX_Control(pDX, IDC_TAB , m_TabControl);
DDX_Control(pDX, IDC_TEXT, m_TextControl);
}
BOOL TestDlg::OnInitDialog()
{
// Default MFC processing
CDialog::OnInitDialog();
TC_ITEM item;
item.mask = TCIF_TEXT;
for( unsigned int nTab = 0; nTab < m_nMaxTabs; nTab++ )
{
std::ostringstream ostr;
ostr << "Tab " << nTab;
std::string str = ostr.str();
item.pszText = const_cast<char*>(str.c_str());
item.cchTextMax = str.length();
m_TabControl.InsertItem( nTab, &item );
}
return(FALSE); // return TRUE unless you set the focus to a control
}
//
// 'TestDlg::OnClickTabCtrl'
//
// @mfunc Notification handler that is called whenever a tab has
// been selected.
//
void TestDlg::OnClickTabCtrl(NMHDR* pNotifyStruct, LRESULT* pResult)
{
// Get the currently active tab
int nCurTab = m_TabControl.GetCurSel();
std::ostringstream ostr;
ostr << "This is tab " << nCurTab;
m_TextControl.SetWindowText( ostr.str().c_str() );
// Set the return code
*pResult = 0;
}
So we could try executing this code. Do you see a problem? Well, if not, then ask yourself which value is returned by the m_TabControl.GetCurSel() call in the OnClickTabCtrl function above - especially after the user has changed the tab order.
The problem is that the tab control do not maintain its tab order. It simply returns the tab control indexes as if the control were not ordered. The static text control will display the wrong tab number. This is a problem that we need to fix. The tab control should maintain its tab order and whenever 'users' (clients) ask it about anything involving a tab index we internally in the tab control redirects (or recalculates) the correct tab index. So when the user (a client) ask about the current selection it should not return the tab index in the usual zero-indexed order but in the sorted order.
Lets first declare a member data variable m_TabOrder that will contains the tab controls internal tab order:
class TestDlg : public CDialog
{
...
private:
typedef std::vector<unsigned int> TabOrder;
TabOrder m_TabOrder; // Contains the current order of the tabs within the tab control.
bool m_bRedirect; // Specifies whether redirection should take place.
// Utility members
unsigned int Redirect( unsigned int nTab );
};
The question is where to update the member variable. The 'users' (the clients) of the tab control could actually send four types of messages: an insert tab message, an delete tab message, a retrieve tab information message and a set tab information message. As I see it there is really only place were we can capture all four types of message and make our redirection. In the tab controls WindowProc. The WindowProc is the procedure in which all window message are routed. Therefore it is an excellent place to make our redirection.
The WindowProc:
//
// 'TabControl::WindowProc'
//
// @mfunc Overridden WindowProc to take care of redirection of the tab indexes. If also
// updates the tab sort order variable m_TabOrder.
//
LRESULT TabControl::WindowProc( UINT message, WPARAM wParam, LPARAM lParam )
{
LRESULT nRes;
// Make indirections of appropriate messages
switch( message )
{
case TCM_INSERTITEM:
if( m_bRedirect )
{
unsigned int nIndex = wParam;
wParam = Redirect(wParam);
// Update tab sort order..
TabOrder::iterator it = m_TabOrder.begin();
while( it!=m_TabOrder.end() )
{
if( *it >= nIndex )
*it = (*it)+1;
it++;
}
// Insert new tab into tab sort order...
m_TabOrder.insert( m_TabOrder.begin()+wParam, nIndex );
}
nRes = CTabCtrl::WindowProc(message,wParam,lParam);
break;
case TCM_DELETEITEM:
if( m_bRedirect )
{
unsigned int nIndex = wParam;
wParam = Redirect(wParam);
// Update tab sort order..
TabOrder::iterator it = m_TabOrder.begin();
while( it!=m_TabOrder.end() )
{
if( *it > nIndex )
*it = (*it)-1;
it++;
}
// Delete the tab from the tab sort order...
m_TabOrder.erase( m_TabOrder.begin()+wParam );
}
nRes = CTabCtrl::WindowProc(message,wParam,lParam);
break;
case TCM_DELETEALLITEMS:
if( m_bRedirect )
m_TabOrder.clear();
nRes = CTabCtrl::WindowProc(message,wParam,lParam);
break;
// Set type messages (redirect tab index after call)
case TCM_HIGHLIGHTITEM:
case TCM_SETCURFOCUS:
case TCM_SETCURSEL:
case TCM_SETITEM:
if( m_bRedirect )
wParam = Redirect(wParam);
nRes = CTabCtrl::WindowProc(message,wParam,lParam);
break;
// Get type messages (redirect tab index after call)
case TCM_GETCURFOCUS:
case TCM_GETCURSEL:
case TCM_GETITEM:
case TCM_GETITEMRECT:
case TCM_HITTEST:
nRes = CTabCtrl::WindowProc(message,wParam,lParam);
if( m_bRedirect )
nRes = Redirect(nRes);
break;
default:
nRes = CTabCtrl::WindowProc(message,wParam,lParam);
break;
}
return nRes;
}
//
// 'TabControl::Redirect'
//
// @mfunc Redirects a tab index using the current tab sort order.
//
unsigned int TabControl::Redirect( unsigned int nTab )
{
if( nTab < m_TabOrder.size() )
nTab = m_TabOrder[nTab];
return nTab;
}
Note that when the type of message is a setting type we make the redirection before sending it to the base class and when its is a retrieval type we make the redirection before sending the message to the base class. In case of the insertion and deletion type message we make the appropriate updates of the tab sort order variable. All other messages are handled as normally.
There is one other problem though. When some of the our tab controls internal functions, such as the TabControl::ReorderTab, works with tab indexes, they should really not be redirected because we're working with indexes from the underlying control itself. So we need some way of controlling when to do the redirection. Therefore the member variable m_bRedirect is introduced. Per default it is true, indicating that all messages involving tab indexes should be redirected according to sort order. However when we internally want to work with the "correct" indexes we simply switches the m_bRedirect variable before calling any functions on the underlying tab control and switches it back before leaving the function. In the final code you will see these switches as sketched below - here from the ReorderTab code. Note that we're also updating the tab sort order inside the ReorderTab function.
bool TabControl::ReorderTab( unsigned int nSrcTab, unsigned int nDstTab )
{
if( nSrcTab == nDstTab )
return true; // Return success (we didn't need to do anything
m_bRedirect=!m_bRedirect;
...
GetItem(nSrcTab,&item);
DeleteItem(nSrcTab);
// Insert it at new location
InsertItem( nDstTab-(m_nDstTab > m_nSrcTab ? 1 : 0), &item );
// Update the tab order
TabOrder::iterator orgit = (m_TabOrder.begin()+m_nSrcTab);
unsigned int nTabToMove = *orgit;
m_TabOrder.erase( orgit );
m_TabOrder.insert( m_TabOrder.begin()+m_nDstTab-(m_nDstTab > m_nSrcTab ? 1 : 0), nTabToMove );
...
m_bRedirect=!m_bRedirect;
...
}
If somebody can come up with a better solution than using the m_bRedirect variable I would be happy to hear about it.
After introducing the m_Redirect variable in the necessary places in our code we have finally arrived with a good functioning tab control that allows drag and drop tab ordering. You can download the full functioning code including a sample test application from here.
I have shown you have you can in C++/MFC can implement a tab control that supports drag and drop tab ordering. Its been a fun process that really show the complexities of making good control behavior. One lesson of this article that I've learned is that implementation of basic control functionality is not a simple matter. There are a lot of features, special cases that the programmer has to take care of and test for. That's why the common control library is so useful. Somebody else have made all the hard work. And furthermore it allows applications to look similar.
There a few things that would be nice to include into the implementation. For example:
Hope you can use it.