Tear-off Rebars






4.46/5 (8 votes)
Jan 27, 2000

175137

2059
This article Implements the functionality similar to the Office 2000 toolbars
Some of the interesting features in Microsoft Office products are those dealing with toolbars. Office toolbars behave similarly to rebars (coolbars) in that the user receives direct animated feedback as the bar is dragged and rearranged. Unlike rebars however, Office toolbars also behave like the standard MFC toolbars in that the user may "tear" a toolbar off of the application frame and float it in a separate window.
Visual Studio 6.0 introduced the CReBar
class, and corresponding support
in the MFC App Wizard. The primary drawback to using this new feature is that
you lose the docking features of the traditional MFC toolbars. This article
demonstrates a simple way to endow MFC applications using CReBar with
tear-off toolbar capabilities.
Disclaimer: The solution presented here is intended to implement the basic functionality described above. Some readers may desire more complex behavior, such as side docking or bottom docking. I invite you to write such capabilities and submit them to CodeProject yourself. Unfortunately I don't have the time to write additional features (and my product does not require them.) I did play with side-docking for a few moments and decided against getting into a bloody battle with bad-rebar behavior and ugly MFC docking code.
To get started, download the TearoffRebar.zip file accompanying this article and run the demo. If you like it and want to add its functionality to your application, read on.
The tear-off rebar functionality presented here works by subclassing CReBar
,
and retrofitting your CFrameWnd
to use a subclassed CMiniDockFrameWnd
,
the class that MFC uses to host floating toolbars. The new classes live in the
files TearOffReBar.h and TearOffReBar.cpp. The following instructions assume you
are working with an app-wizard generated project. Your results may vary if you
aren't (you may have to think on your own a bit…)
Step 1. Add the files TearOffReBar.h and TearOffReBar.cpp to your project.
Step 2.
#include
TearOffReBar.h in your MainFrm.h file, and add the lineDECLARE_REBAR_DOCKING()
inside yourCMainFrame
class.By the way, if you're thinking I've gone a little overboard using title-case with my file and class names you may be right. However, I started out thinking I really just wanted a widget to replace
CReBar
and I ended up naming my classes to match. If the world revolved around me, I would have called itCRebar
.Step 3. Change member variable
m_wndReBar
to be of typeCTearOffRebar
, instead ofCReBar
. If you application doesn't useCReBar
, then you should go make it do so before going any further. If you're deeply entrenched with traditional MFC toolbars, you may end up bastardizing some code.Step 4. In your
OnCreate()
method of CMainFrame, make a call toEnableRebarDocking()
. If you have any calls to plain oldEnableDocking()
, remove them or comment them out.
That's about it. Assuming your app doesn't do anything wild or crazy, you should be able to dock and undock your toolbars from your rebar.
How it all works:
The rebar common control is kind enough to source the notification messages
RBN_BEGINDRAG
and RBN_ENDDRAG
. If we hande these and WM_MOUSEMOVE
, we can
write code to watch for and intervene when the user starts dragging toolbars around:
BOOL CTearOffReBar::OnBeginDrag(NMHDR* pNotifyStruct, LRESULT* result) { NMREBAR* pnmrb = reinterpret_cast<NMREBAR*>(pNotifyStruct); // cache info about the band that is being dragged: REBARBANDINFO rbi; memset(&rbi, 0, sizeof(rbi)); rbi.cbSize = sizeof(rbi); rbi.fMask = RBBIM_CHILD; SendMessage(RB_GETBANDINFO,pnmrb->uBand, (LPARAM)&rbi); m_hwndBand = rbi.hwndChild; return TRUE; }
The main thing to see here is that we're storing away the hwnd of the bar (band) being dragged. We do this because we wouldn't be able to get it later otherwise. Remember that a rebar is a just window that hosts child windows (bands) that are your actual toolbar windows.
Here's the method where all the fun happens. Try to keep in mind that I was going for simplicity.
void CTearOffReBar::OnMouseMove(UINT nFlags, CPoint pt) { // default: CReBar::OnMouseMove(nFlags, pt); // ensure dragging: if (this != GetCapture() || NULL == m_hwndBand) return; // are we going to tear off? CRect rcl; GetClientRect(&rcl); rcl.InflateRect(10, 10); if (rcl.PtInRect(pt)) return; // we're done tracking... ReleaseCapture(); // we'll need these: UINT uBand = BandFromHWND(m_hwndBand); CFrameWnd* pFrame = reinterpret_cast<CFRAMEWND*>(AfxGetMainWnd()); ASSERT_KINDOF(CFrameWnd, pFrame); CControlBar* pBar = reinterpret_cast<CCONTROLBAR*> (CWnd::FromHandle(m_hwndBand)); ASSERT_KINDOF(CControlBar, pBar); DWORD dwStyle = pBar->m_dwStyle; // retrieve and cache band information: REBARBANDINFO rbi; char aszBuff[_MAX_PATH]; memset(&rbi, 0, sizeof(rbi)); rbi.cbSize = sizeof(rbi); rbi.fMask = RBBIM_BACKGROUND | RBBIM_CHILDSIZE | RBBIM_COLORS | RBBIM_HEADERSIZE | RBBIM_IDEALSIZE | RBBIM_ID | RBBIM_IMAGE | RBBIM_LPARAM | RBBIM_SIZE | RBBIM_STYLE | RBBIM_TEXT | RBBIM_CHILD; rbi.cch = sizeof(aszBuff); rbi.lpText = aszBuff; SendMessage(RB_GETBANDINFO, uBand, (LPARAM)&rbi); // hide then delete the band: SendMessage(RB_SHOWBAND, uBand, FALSE); SendMessage(RB_DELETEBAND, uBand); pBar->ShowWindow(SW_SHOW); pBar->SetOwner(pFrame); // determine where to float thebar at: MapWindowPoints(NULL, &pt,1); SetCursor(AfxGetApp()->LoadStandardCursor(IDC_ARROW)); // float the bar: pBar->EnableDocking(CBRS_ALIGN_ANY); CPoint ptCreate(pt); ptCreate.Offset(-10,-10); pFrame->FloatControlBar(pBar, ptCreate); pBar->EnableDocking(0); // pass the cached band info to the new frame to store: CMiniReBarDockFrameWnd* pMiniFrame = (CMiniReBarDockFrameWnd*)pBar->GetParentFrame(); pMiniFrame->SetRebarBandInfo(rbi, dwStyle); // user continues to drag the new parent-frame: pBar->GetParentFrame()->SendMessage(WM_NCLBUTTONDOWN, HTCAPTION, MAKELPARAM(pt.x, pt.y)); m_hwndBand = NULL; AfxGetMainWnd()->RedrawWindow(); }
The code is pretty well commented, but I'll give you the play-by-play here too. What's happening is that the rebar is receiving mouse move messages. We can check to see if these moves are the result of the user dragging a band by checking a) whether the rebar has mouse-capture, and b) whether we've stowed away the hwnd of the band being dragged. If either of these two checks fail, we're not interested in continuing.
Next we determine whether we should undock the bar. This check simply determines whether the current mouse point is outside of the rebar window's rectangle by at least 10 pixels.
If we've decided to undock the bar, we need to determine what "band" it lives in.
To do this we call BandFromHWND()
, which iterates through the rebar's bands
and searches for the band with the desired hwnd. You might wonder why we
didn't just save away the band-ID in OnBeginDrag()
. The answer is that the band-ID will
change if the band is moved within the rebar, which is the likely case if the user
is dragging the band. Once we have the band-ID, we retrieve and store the band's
attributes in a REBARBANDINFO
structure. We'll be using these stored attributes
later when the band is re-docked in the rebar.
Now we're free to delete the band from the rebar. This is accomplished by sending
the rebar a RB_DELETEBAND
message. The preceding RB_SHOWBAND
message aids in
avoiding some weird redraw issues I encountered when first writing the code.
With the
band gone, we're free to re-parent the toolbar window into a new top-level mini-frame
window. Just about the last thing I wanted to do was write a bunch
of code to do this, so I decided to leverage some code already built into CFrameWnd
,
namely CFrameWnd::FloatControlBar()
. This function takes a control bar and
re-parents it into a newly created CMiniDockFrameWnd
. Unfortunately,
CMiniDockFrameWnd
doesn't cut the mustard for us later on, so there is some
other magic to force CFrameWnd to use my new class CMiniReBarDockFrameWnd.
Yes, that's a mouthful.
Once we've called FloatControlBar()
, we need only to perform a few housekeeping
details. The first is to store with the control bar the REBARBANDINFO
of the
old band that was previously home to the bar. This is done with a call to
CMiniReBarDockFrameWnd::SetRebarBandInfo()
. The second detail is to seamlessly
allow the user to continue dragging the new frame window. This is easily accomplished
by faking out the new frame window by sending it a WM_NCLBUTTONDOWN
message,
which is the message generated by a user manually clicking in the frame's caption
area.
I said a moment ago that there was some magic involved to have the
CFrameWnd
make us a CMiniReBarDockFrameWnd
instance instead of a CMiniDockFrameWnd.
The magic is revealed if you check out the DECLARE_REBAR_DOCKING()
macro and the
corresponding call to EnableRebarDocking()
. This code does what the CFrameWnd::EnableDocking()
method does: it tells the CFrameWnd
what class to use for hosting floating control
bars.
OK, so you can see how we "tear off" a toolbar from the rebar and float it in
its own frame window. The reverse procedure is similar, and is implemented in
CMiniReBarDockFrameWnd::OnMoving()
:
void CMiniReBarDockFrameWnd::OnMoving(UINT fwSide, LPRECT pRect) { // default: CMiniDockFrameWnd::OnMoving(fwSide, pRect); // are we going to dock? DWORD dwInsertPos(0); bool bHorizontal(false); CTearOffReBar* pRebar = CalcDockingReBar(&dwInsertPos, &bHorizontal); if (NULL == pRebar) return; // get the toolbar... CControlBar*pBar = (CControlBar*)GetWindow(GW_CHILD)->GetWindow(GW_CHILD); pBar->m_dwStyle = m_dwStyle; pBar-> CalcFixedLayout(0, bHorizontal); // insert a new band into the rebar: pRebar-> SendMessage(RB_INSERTBAND, dwInsertPos, (LPARAM)&m_rbi); CRect rclReBar; pBar->GetWindowRect(&rclReBar); CPoint pt(rclReBar.left-5, rclReBar.top+5); m_wndDockBar.RemoveControlBar(pBar);pBar->m_pDockBar = NULL; // fake a button click so the dragging operation // moves seamlessly into the rebar internal dragging code: ::MapWindowPoints(NULL, pRebar->GetSafeHwnd(), &pt,1); pRebar->SetActiveWindow(); pRebar->PostMessage(WM_LBUTTONDOWN, MK_LBUTTON, MAKELPARAM(pt.x, pt.y)); }
This function is also pretty straightforward. It is called as the user is
moving (dragging) the CMiniReBarDockFrameWnd
window. It calls the helper
function CMiniReBarDockFrameWnd::CalcDockingBar()
to
determine whether or not to dock with the rebar (if you decide to have multiple
rebars in your frame window, you should extend this function to identify the
correct one for docking). Assuming a rebar is located,
the function inserts a new rebar-band and re parents the control bar. The logic in
this function also figures out whether the band should be inserted or
append in the rebar's internal list (so that the band is inserted
at the top or bottom of the rebar). Once this is done, OnMoving()
uses the
same trick we saw earlier to seamlessly continue the dragging operation
within the rebar itself.
Wrap-up
That's it, folks! One last point: If you're looking to implement more Office 2000 toolbar features, my personal recommendation is to check out a library called BCGControlBar. This library has the most complete set of free toolbar-enhancements I've run across. Although BCGControlBar does not support tear-off rebars, my intention is to submit my technique for inclusion in that library.