Introduction
I created this simple and attractive two-level tab bar for use exclusively in dialog boxes. It can be used to improve property pages and dialog-based applications.
This project was developed using Microsoft Visual C++ 2008. It requires WTL 8 and GDI+. It was tested on Windows XP, Windows Vista, and Windows 7 Beta 1.
Inspiring the Look
I took a screenshot of the Microsoft Office Word 2007 Ribbon bar and played with it in Paint to come up with a simplified Ribbon interface:
But there are many examples of two-level tabs, so I put together some of my favorites:
The things I like from the above are:
- the look of the selected first-level tab (tab) in (1), (5), and (6)
- the look of the selected second-level tab (sub-tab) in (4) and (6)
- the icons in (1)
- the gradient fills in (4)
- the simple hot-tracking in (2) and (5)
But I also wanted:
- the tabs on "glass" – at least on Vista and better. There should only be a background fill when the Desktop Window Manager (i.e., Aero) is not available.
- the icon in the selected sub-tab only to be in color. The other icons could be recolored (specifically, toned) to the tab color – I call this, "color-washing" the icons.
- to be able to change the colors easily.
Design Decisions
Windowless Control
The code extends the top of the non-client area by the height of the tab bar. The "sheet of glass" is extended onto this area. The tabs are drawn entirely on the non-client area.
Draw with GDI+
All of the drawing is done with the help of GDI+. I did not want to use bitmaps, because I wanted to be able to easily change the colors of my ribbon tab bar.
The only graphics needed are optional icons for the sub-tabs. Only full color icons are necessary. GDI+ is used to color-wash the icons (for unselected sub-tabs). Color-washing basically:
- converts the icon to grayscale; and
- adds color to the icon
It is accomplished easily with a ColorMatrix.
Animation
I wanted a subtle animation (dissolve/cross-fade) when the state changed, i.e., when an item was hot-tracked, or the tab or sub-tab selection changed. This is easily accomplished in Vista or better with BeginBufferedAnimation/EndBufferedAnimation.
Now, BeginBufferedAnimation expects you to draw the initial state and the final state in an animation, and it will take care of drawing the intermediate frames. That is what is done in the sample in the MSDN documentation for BeginBufferedAnimation. However, I discovered (quite by accident!) that if I delete the DC hdcFrom (that is, DeleteDC(hdcFrom)) returned by BeginBufferedAnimation, then Windows will auto-magically use what is already drawn as the initial state, so I only needed to draw the final state. That saved me from having to deal with caching or re-drawing an initial state every time the state changed.
Keyboard Control
The standard tab control can be controlled with the keyboard arrow keys. If the tab is in a property sheet, then there is some awkward (in my opinion) tabbing from the tabs back to the pages.
I wanted to keep it simple, so I decided that tabs should have access keys. For example, the tab "&Properties" is displayed, initially, without the "p" mnemonic ("Properties"). After the user presses the ALT key, the mnemonic is underlined (“Properties”). Pressing ALT+P will select the Properties tab. When Properties is the active tab, its "p" is no longer a mnemonic - after all, one cannot re-select an already selected tab. That means that "p" can be used again, either in another tab or a sub-tab. This functionality helps to reduce mnemonic collisions.
To get this functionality, all I needed to do was generate an accelerator table and then let TranslateAccelerator do its job (in PreTranslateMessage). The accelerator table is updated every time the selection changes.
Using the Code
Include RibbionDialog.h and derive a dialog box from CRibbonDialogImpl (instead of CDialogImpl). Then, add CHAIN_MSG_MAP(CRibbonDialogImpl<...>) to the beginning of your message map.
Adding Tabs and Sub-Tabs
To add a tab, just call AddTab with a string resource ID. To add a sub-tab, call AddSubTab with a string resource ID and an optional icon resource ID – the sub-tab will be added to the last tab. For example:
AddTab(ID_TAB1);
AddSubTab(ID_TAB1SUBTAB1, IDI_ICON1);
AddSubTab(ID_TAB1SUBTAB2, IDI_ICON2);
AddTab(ID_TAB2);
AddSubTab(ID_TAB2SUBTAB1, IDI_ICON3);
AddSubTab(ID_TAB2SUBTAB2, IDI_ICON4);
AddSubTab(ID_TAB2SUBTAB3, IDI_ICON5);
You have to add at least one tab, and every tab must have at least one sub-tab.
Handling the Notification of the Changed Selection
The string resource IDs also function as command identifiers. When a selection changes, you will receive a WM_COMMAND notification message. Use COMMAND_ID_HANDLER or MSG_WM_COMMAND in your message map to handle these messages:
BEGIN_MSG_MAP_EX(CMainDlg)
CHAIN_MSG_MAP(CRibbonDialogImpl<CMainDlg>)
MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)
...
COMMAND_ID_HANDLER(ID_TAB1SUBTAB1, OnTab1Subtab1Selected)
MSG_WM_COMMAND(OnCommand)
...
END_MSG_MAP()
(The low-order word of wParam will be the string resource ID of the selected sub-tab. The high-order word of wParam will be 0.)
Changing the Colors
Just change the color constants in the header file to modify the look of your ribbon tab bar.
About the Sample Application
The sample application shows how the ribbon tab bar could be used in a re-sizable dialog box. When a dialog is re-sized, you'll notice that:
- there is no flicker; and
- animation is temporarily turned off
Final Remarks
I tried to follow the KISS principle, and I believe this project is, indeed, simple – but still functional and beautiful. I hope you think so, too. :-)
History
Raj collects postcards. He has been known to wear a bow tie on occasion. He's an optimist.
"Don't take life too seriously. You'll never get out alive." ~ Elbert Hubbard