Introduction
I've been working on an application that I intend to use on Windows XP and Windows 7 clients, but I want to support the new Ribbon control via the Windows Ribbon Framework. I also want to offer a traditional menu to those who prefer that style of interaction instead of the Ribbon.
After I played around with the API a while, I realized it would be fairly simple to support both the Ribbon and the traditional menu in one executable. In this article, I'll describe a sample app that I put together that shows how to accomplish support for both command-selection methods using only the Windows API and the Windows Ribbon Framework.
A Few Words about the Ribbon
The Ribbon was first introduced with Microsoft Office 2007 as a way to tame the huge menu and toolbar hierarchy that had evolved to present all of the suite's features to its users. It then made its way into Windows 7 in the Paint and WordPad applets and can now be found in several third-party applications.
As with most things Microsoft does, it seems that users either love or hate the Ribbon. I happen to love it, since I've always disliked toolbars and I find menus clumsy, at best. Since everyone doesn't share this opinion, and it's relatively cheap to include a menu, and since I want to support XP, Vista, and Windows 7 with a single executable, I'll try to satisfy as many people as I can by allowing users to select the command presentation method they prefer.
The Project
The application we'll be working with began its life as an adaptation of a scratch program that Raymond Chen uses as a framework for example programs in his articles. I adapted it to my own very thin C++ wrapper and added some things that I like to have in fully featured applications, then I set about making it work with the Ribbon. You may download the Visual C++ 2010 Express project and follow along below.
Prerequisites
First of all, you'll need Visual C++ 2010 Express (which is free) or one of its more sophisticated versions (which are not free) in order to compile the project. If you prefer to use a different compiler, you'll have to adapt the code and project as necessary. You'll also need the Windows 7 SDK version 7.1. Finally, in order to see the Ribbon interface, you'll need to run the application on Windows 7 or on Windows Vista SP2 or later with the Platform Update installed. When the application runs on Vista SP1 or earlier, it will only display the traditional menu.
Configuring Visual Studio Projects for Ribbon Support
Supporting the Ribbon in Visual Studio takes a little setup. First of all, once the SDK is installed, you need to change a setting in any new project in Visual Studio 2010 to take advantage of the SDK. In the project properties, under Configuration Properties>General, change the "Platform Toolset" property to "Windows7.1SDK."
Next, you'll need to add a new XML file to your project to contain your Ribbon markup. The Ribbon is defined by an XML markup schema described in detail on MSDN. This XML is compiled by a tool provided in the Windows 7.1 SDK, UICC. After you've added the XML file to your project, open the Property Pages for the file and change the "Item Type" property in General properties to "Custom Build Tool." Apply the setting, then in Custom Build Tool>General settings, change the command line to the following:
"$(WindowsSdkToolsDir)\bin\uicc" "%(Identity)"
"%(Filename).bin" /header:"%(Filename).h" /res:"%(Filename).rc2"
Next, define the outputs from the UICC compiler in the "Outputs" property:
%(Filename).bin;%(Filename).h;%(Filename).rc2;%(Outputs)
Make sure that you apply these changes for both the Debug and Release builds.
Finally, you'll need to add the output of the UICC compiler to the project's main resource script. These need to go in a particular location in the resource file. In Visual Studio 2010 Professional, switch to the resource view (Ctrl + Shift + E), right-click on the resource script, and select "Resource Includes" from the context menu. Add the header file produced by UICC (for example, Ribbon.h) at the top in the "Read-only symbol directives" window, and add the resource script produced by UICC (for example, Ribbon.rc2) to the Compile-time directives window, as shown below:
In Visual C++ 2010 Express, you'll need to edit the resource script code directly since there is no resource editor. Right-click on the resource script in Solution Explorer and select "View Code" from the context menu. Find the TEXTINCLUDE
sections and edit them as shown below:
#ifdef APSTUDIO_INVOKED
1 TEXTINCLUDE
BEGIN
"resource.h\0"
END
2 TEXTINCLUDE
BEGIN
"#include ""Ribbon.h""\r\n"
"#ifndef APSTUDIO_INVOKED\r\n"
"#include ""targetver.h""\r\n"
"#endif\r\n"
"#define APSTUDIO_HIDDEN_SYMBOLS\r\n"
"#include ""windows.h""\r\n"
"#undef APSTUDIO_HIDDEN_SYMBOLS\r\n"
"\0"
END
3 TEXTINCLUDE
BEGIN
"#include ""Ribbon.rc2""\r\n"
"\0"
END
#endif // APSTUDIO_INVOKED
With these changes in place, the resource script will include the control IDs created by the UICC compiler so that you may reuse them in your menu resource.
Running the Application
As I mentioned earlier, this project began as a scratch application. The idea was to have a starting point for new applications that would provide a lot of the boring, boilerplate stuff so that I could concentrate on the interesting bits of new applications. Figuring out how to integrate the Ribbon and make it work was not entirely straightforward, which I found disappointing. The answers to my questions were usually somewhere out on the Internet, but never in one place. That's why I decided to write this article to help those of you who might be having the same struggles I had.
After you've downloaded the Visual C++ 2010 Express project, open it in Express, build it, and run it. Or, if you prefer, just download the executable and run it (you will need to install the Visual C++ 2010 Redistributable if you have not already done so). On Windows 7, and on Windows Vista SP2 or later with the Platform Update installed, you'll see a window with a Ribbon control across the top. In the View tab of the Ribbon is a button that will allow the user to switch to a traditional menu bar. The View menu has a corresponding option to allow the user to display the Ribbon again. Most of the other commands will simply output a line of text in the client area describing the option that was selected.
On Windows XP, and on Windows Vista without the Platform Update, you'll instead see a window with a traditional menu. The "Show Ribbon" menu option is disabled because the application detects that the Ribbon API is not supported on those platforms.
Examining the Code
I won't go into too much detail about the usual Windows API elements of the code, or about the minimalist C++ framework it uses. I also won't get into the details of the Ribbon markup, which is covered in detail in the MSDN Ribbon documentation. Instead, I'll cover the bits of the code related to supporting the Ribbon and the menu.
There are two headers that need to be included to pick up the definitions of the Ribbon interfaces and their associated GUIDs:
#include <UIRibbon.h>
#include <UIRibbonPropertyHelpers.h>
Also, a function in the Desktop Window Manager needs to be called to get around some painting problems when the Ribbon is removed. Pull those function definitions in as well.
#include <dwmapi.h>
The application's initialization is performed in the App::Initialize
method. The first order of business is initializing COM:
if (FAILED(CoInitialize(NULL)))
{
ReportError(IDS_COINITIALIZE_FAILED);
retVal = false;
goto exitinit;
}
The comment about goto
is a subject of another article entirely, so we'll ignore that for now. It's necessary to initialize COM because the Ribbon API is a set of COM objects that implement the various Ribbon interfaces (IUIFramework
, IUIRibbon
, etc.). A little further down in the initialization method, the code reads the saved user settings, if they exist, then checks to see if the application should be using the Ribbon. The setting will be true
by default, no matter what OS the application is running on, so the application will call the CreateRibbon
method.
LoadAppSettings();
if (settings.isRibbon)
{
CreateRibbon();
}
The CreateRibbon
method uses the Ribbon API's COM objects to initialize and show the Ribbon.
bool App::CreateRibbon()
{
HRESULT hr = CoCreateInstance(
CLSID_UIRibbonFramework,
NULL,
CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&pFramework));
if (SUCCEEDED(hr))
{
killRibbon = false;
hr = pFramework->Initialize(GetHWND(), this);
if (SUCCEEDED(hr))
{
hr = pFramework->LoadUI(GetModuleHandle(NULL), L"APPLICATION_RIBBON");
if (SUCCEEDED(hr))
{
hr = pFramework->GetView(0, IID_PPV_ARGS(&pRibbon));
if (SUCCEEDED(hr))
{
settings.isRibbon = true;
}
}
}
}
if (FAILED(hr))
{
CloseRibbon();
}
return SUCCEEDED(hr);
}
If the COM objects are created and their methods are called successfully, the API will remove the main window's menu and display a Ribbon in its place. This method will fail on platforms that do not implement the Ribbon API, and since the application displays a menu on its main window by default, that menu will remain in place.
Next, the initial states of the various command options are set.
SetDirty(false);
SetRedo(false);
EnableMenuItem(GetMenu(GetHWND()), ID_SHOW_RIBBON,
IsRibbonSupported() ? MF_ENABLED : MF_GRAYED);
The ID_SHOW_RIBBON
constant represents the menu selection that enables the Ribbon when the UI is displaying the menu bar. This function uses the return value of the IsRibbonSupported
method to enable or disable the menu item. That method is implemented as follows:
bool App::IsRibbonSupported()
{
bool isRibbonSupported = false;
IUIFramework* pTmp = 0;
HRESULT hr = CoCreateInstance(
CLSID_UIRibbonFramework,
NULL,
CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&pTmp));
if (SUCCEEDED(hr))
{
isRibbonSupported = true;
pTmp->Release();
}
return isRibbonSupported;
}
All the method does is attempt to create a COM object provided by the Ribbon API. If the attempt fails, the method returns false
. This will always fail on XP and on Vista without the Platform Update, unless of course some enterprising developer implements all the necessary COM interfaces and objects on those platforms.
Implementing the COM Interfaces
The Ribbon API requires an application using the Ribbon to implement two COM interfaces: IUIApplication
and IUICommandHandler
. The IUIApplication
interface defines callbacks into the application that are called by the Ribbon API. The IUICommandHandler
interface is called for each command that is exposed on the Ribbon.
The Scratch Ribbon Project implements both interfaces in the App
object. This need not be the case, but it makes the shared implementation with the menu bar a little easier to accomplish. An alternative implementation would be to create a unique IUICommandHandler
implementation for each command.
In the App
class declaration in ScratchRibbonProject.h, the relevant methods are declared. The definitions are in ScratchRibbonProject.cpp, and we will examine each of them in turn.
First, we need to implement IUnknown
since all COM interfaces derive from this interface. The AddRef
and Release
methods are fairly straightforward.
ULONG STDMETHODCALLTYPE App::AddRef()
{
return InterlockedIncrement(&refCount);
}
ULONG STDMETHODCALLTYPE App::Release()
{
return InterlockedDecrement(&refCount);
}
Next is QueryInterface
, which should return a pointer to the requested interface if the object implements it.
HRESULT STDMETHODCALLTYPE App::QueryInterface(
REFIID riid,
void **ppvObject)
{
if (!ppvObject)
{
return E_INVALIDARG;
}
if (riid == IID_IUnknown)
{
*ppvObject = static_cast<IUnknown*>(
static_cast<IUIApplication*>(this));
}
else if (riid == __uuidof(IUICommandHandler))
{
*ppvObject = static_cast<IUICommandHandler*>(this);
}
else if (riid == __uuidof(IUIApplication))
{
*ppvObject = static_cast<IUIApplication*>(this);
}
else
{
*ppvObject = 0;
return E_NOINTERFACE;
}
AddRef();
return S_OK;
}
With that chore out of the way, we can move on to the interesting interfaces. The IUIApplication
interface specifies three methods: OnViewChanged
, OnCreateUICommand
, and OnDestroyUICommand
. The OnViewChanged
method is called by the Ribbon framework when the state of a Ribbon view changes. The verb
parameter specifies the action performed by the view.
HRESULT STDMETHODCALLTYPE App::OnViewChanged(
UINT32 viewId,
UI_VIEWTYPE typeID,
IUnknown* pView,
UI_VIEWVERB verb,
INT32 uReasonCode)
{
HRESULT hr = E_NOTIMPL;
if (UI_VIEWVERB_CREATE == verb)
{
IUIRibbon* pRibbon = NULL;
hr = pView->QueryInterface(IID_PPV_ARGS(&pRibbon));
if (SUCCEEDED(hr))
{
LoadRibbonSettings(pRibbon);
pRibbon->Release();
}
}
else if (UI_VIEWVERB_SIZE == verb)
{
RECT rect = {};
GetClientRect(GetHWND(), &rect);
OnSize(GetHWND(), 0, rect.right - rect.left, rect.bottom - rect.top);
}
else if (UI_VIEWVERB_DESTROY == verb)
{
IUIRibbon* pRibbon = NULL;
hr = pView->QueryInterface(IID_PPV_ARGS(&pRibbon));
if (SUCCEEDED(hr))
{
SaveRibbonSettings(pRibbon);
pRibbon->Release();
}
}
return hr;
}
The method is called with the UI_VIEWVERB_CREATE
and UI_VIEWVERB_DESTROY
verb constants at Ribbon initialization and tear-down, respectively. This implementation uses those calls to load the ribbon settings when the Ribbon view is created, and save them when it is destroyed. We'll look at the LoadRibbonSettings
and SaveRibbonSettings
methods a little later on.
The UI_VIEWVERB_SIZE
indicates that the Ribbon's size has changed (for example, the ribbon has been minimized). The application may need to respond to this notification to adjust any other windows that need to be moved or resized based on the new Ribbon size. In this example, the method adjusts the client area by calling the OnSize
message handler.
The OnCreateUICommand
method is called by the Ribbon framework for each command specified in the Ribbon markup. The application must return a pointer to an IUICommandHandler
interface that will handle each particular command. In this application, all of the commands are serviced by the App
object instance, so we just return the requested pointer and increment the reference count.
HRESULT STDMETHODCALLTYPE App::OnCreateUICommand(
UINT32 commandId,
UI_COMMANDTYPE typeID,
IUICommandHandler** commandHandler)
{
if (commandHandler)
{
*commandHandler = static_cast<IUICommandHandler*>(this);
AddRef();
return S_OK;
}
return E_INVALIDARG;
}
The OnDestroyUICommand
method is called each time a command is destroyed. This would give the application an opportunity to clean up its command handlers, if necessary, but in our case, there's nothing to do.
HRESULT STDMETHODCALLTYPE App::OnDestroyUICommand(
UINT32 commandId,
UI_COMMANDTYPE typeID,
IUICommandHandler* commandHandler)
{
return E_NOTIMPL;
}
Finally, we need to implement the two methods of the IUICommandHandler
interface, which will be shared by all of the Ribbon commands in our application. The UpdateProperty
method is called by the framework to request an update to a command's state. As an example of how to modify the enabled/disabled state, this application changes the state of the Save and Redo commands based on flags maintained by the App
object.
HRESULT STDMETHODCALLTYPE App::UpdateProperty(
UINT32 commandId,
REFPROPERTYKEY key,
const PROPVARIANT *currentValue,
PROPVARIANT *newValue)
{
if (newValue)
{
if (key.fmtid == UI_PKEY_Enabled.fmtid)
{
if (commandId == ID_CMD_SAVE)
{
(*newValue).boolVal = IsDirty() ? VARIANT_TRUE : VARIANT_FALSE;
}
else if (commandId == ID_CMD_REDO)
{
(*newValue).boolVal = CanRedo() ? VARIANT_TRUE : VARIANT_FALSE;
}
}
}
return S_OK;
}
At last, we come to the method that connects activation of the Ribbon commands to actual application code. Execute
is called with an execution verb constant of UI_EXECUTIONVERB_EXECUTE
when the application needs to respond to a command event. This application posts a WM_COMMAND
message equivalent to what would have been sent if the command had been selected from a menu. This, in turn, triggers the WM_COMMAND
handler in the WndProc
method.
HRESULT STDMETHODCALLTYPE App::Execute(
UINT32 commandId,
UI_EXECUTIONVERB verb,
const PROPERTYKEY *key,
const PROPVARIANT *currentValue,
IUISimplePropertySet *commandExecutionProperties)
{
if (verb == UI_EXECUTIONVERB_EXECUTE)
{
PostMessage(GetHWND(), WM_COMMAND, commandId, 0);
}
return S_OK;
}
The Ribbon XML
The Ribbon.xml file contains the markup that defines the application's Ribbon. This file is compiled by UICC and included in the resource script. Each command has an Id
attribute that specifies a numeric value sent to the application when the command is activated. This value is associated with the Symbol
attribute in the Ribbon.h file generated by UICC. Following are the definitions for the "New" and "Open" commands:
<Command Name="cmdNew" Id="0x0100" Symbol="ID_CMD_NEW" Keytip="N">
<Command.LabelTitle>New</Command.LabelTitle>
<Command.TooltipTitle>New (Ctrl+N)</Command.TooltipTitle>
<Command.TooltipDescription>Create a new document</Command.TooltipDescription>
<Command.LargeImages>
<Image Source="images/New-icon-32.bmp" Id="101"
Symbol="ID_NEW_LARGEIMAGE1" MinDPI="96" />
</Command.LargeImages>
<Command.SmallImages>
<Image Source="images/New-icon-16.bmp" Id="102"
Symbol="ID_NEW_SMALLIMAGE1" MinDPI="96" />
</Command.SmallImages>
</Command>
<Command Name="cmdOpen" Id="0x0103"
Symbol="ID_CMD_OPEN" Keytip="O">
<Command.LabelTitle>Open</Command.LabelTitle>
<Command.TooltipTitle>Open (Ctrl+O)</Command.TooltipTitle>
<Command.TooltipDescription>Open a document</Command.TooltipDescription>
<Command.LargeImages>
<Image Source="images/Open-icon-32.bmp" Id="103"
Symbol="ID_OPEN_LARGEIMAGE1" MinDPI="96" />
</Command.LargeImages>
<Command.SmallImages>
<Image Source="images/Open-icon-16.bmp" Id="104"
Symbol="ID_OPEN_SMALLIMAGE1" MinDPI="96" />
</Command.SmallImages>
</Command>
When this XML is compiled, the following constants will be defined in Ribbon.h:
#define ID_CMD_NEW 0x0100
#define ID_CMD_NEW_LabelTitle_RESID 60010
#define ID_CMD_NEW_Keytip_RESID 60011
#define ID_CMD_NEW_TooltipTitle_RESID 60012
#define ID_CMD_NEW_TooltipDescription_RESID 60013
#define ID_NEW_SMALLIMAGE1 102
#define ID_NEW_LARGEIMAGE1 101
#define ID_CMD_OPEN 0x0103
#define ID_CMD_OPEN_LabelTitle_RESID 60014
#define ID_CMD_OPEN_Keytip_RESID 60015
#define ID_CMD_OPEN_TooltipTitle_RESID 60016
#define ID_CMD_OPEN_TooltipDescription_RESID 60017
#define ID_OPEN_SMALLIMAGE1 104
#define ID_OPEN_LARGEIMAGE1 103
Most of these constants are used by various string and image resources, but the constants that will be used in the menu are ID_CMD_NEW
and ID_CMD_OPEN
.
The Menu Resource
Earlier, we modified ScratchRibbonProject.rc to include Ribbon.h so that the constants defined in that header by the Ribbon compiler could also be used by the menu resource.
IDC_APP_MENU MENU
BEGIN
POPUP "&File"
BEGIN
MENUITEM "&New\tCtrl+N", ID_CMD_NEW
MENUITEM "&Open\tCtrl+O", ID_CMD_OPEN
MENUITEM "&Save\tCtrl+S", ID_CMD_SAVE
MENUITEM "Save &As...\tCtrl+Shift+S", ID_CMD_SAVEAS
MENUITEM SEPARATOR
MENUITEM "E&xit\tAlt+F4", ID_CMD_EXIT
END
POPUP "&Edit"
BEGIN
MENUITEM "&Undo\tCtrl+Z", ID_CMD_UNDO
MENUITEM "&Redo\tCtrl+Y", ID_CMD_REDO
MENUITEM SEPARATOR
MENUITEM "Cu&t\tCtrl+X", ID_CMD_CUT
MENUITEM "&Copy\tCtrl+C", ID_CMD_COPY
MENUITEM "&Paste\tCtrl+V", ID_CMD_PASTE
MENUITEM "&Delete\tDel", ID_CMD_DELETE
END
POPUP "&View"
BEGIN
MENUITEM "Zoom &In\tCtrl+Plus", ID_CMD_ZOOMIN
MENUITEM "Zoom &Out\tCtrl+Minus", ID_CMD_ZOOMOUT
MENUITEM "&Normal Zoom\t Ctrl+0", ID_CMD_NORMALZOOM
MENUITEM SEPARATOR
MENUITEM "&Show Ribbon", ID_SHOW_RIBBON
END
POPUP "&Help"
BEGIN
MENUITEM "&View Help\tF1", ID_CMD_VIEWHELP
MENUITEM "&About\tCtrl+?", ID_CMD_ABOUT
END
END
The menu items defined in the menu resource now share the same numeric identifiers defined in Ribbon.xml. Activating any of these items, either on the Ribbon or the menu, will trigger the same handlers in the OnCommand
message handler.
void App::OnCommand(
HWND hwnd,
int id,
HWND hwndCtl,
UINT codeNotify)
{
switch (id)
{
case ID_CMD_NEW:
AppendText(L"New document\r\n");
SetDirty(true);
break;
case ID_CMD_OPEN:
AppendText(L"Open document\r\n");
break;
}
}
Swapping the Ribbon and the Menu
When the application is run for the first time on a platform that supports the Ribbon, the application displays the Ribbon by default. Activating the Show Ribbon command on the View tab will cause the Ribbon to be removed from the window, and the menu bar will be shown in its place. The identifier for that command is ID_HIDE_RIBBON
, and when it is activated, the following code is executed in the OnCommand
method:
case ID_HIDE_RIBBON:
PostMessage(hwnd, AM_SHOW_MENU, 0, 0);
break;
This puts the AM_SHOW_MENU
message (an application-defined message) into the message queue, and that is handled by the following code in App::WndProc
:
case AM_SHOW_MENU:
{
killRibbon = true;
CloseRibbon();
PostMessage(GetHWND(), AM_RESTORE_MENU, 0, 0);
}
return 0;
This code sets a flag (killRibbon
) to activate special message handling in order to work around some painting problems (which we'll examine later). It then calls the CloseRibbon
method.
void App::CloseRibbon()
{
if (pRibbon)
{
pRibbon->Release();
pRibbon = 0;
}
if (pFramework)
{
pFramework->Destroy();
pFramework->Release();
pFramework = 0;
}
settings.isRibbon = false;
}
This method releases the pRibbon
object, if set, calls the Destroy
method of the pFramework
object, releases the pFramework
object, and sets the pointers to null. Finally, it updates the settings
object to note that the ribbon is not being used.
The last thing that the AM_SHOW_MENU
handler does is post an AM_RESTORE_MENU
message, which is handled as follows:
case AM_RESTORE_MENU:
{
SetMenu(GetHWND(), hMenu);
SetDirty(IsDirty());
SetRedo(CanRedo());
EnableMenuItem(GetMenu(GetHWND()), ID_SHOW_RIBBON,
IsRibbonSupported() ? MF_ENABLED : MF_GRAYED);
}
return 0;
This restores the menu bar to the window with SetMenu
and enables or disables the menu items as necessary.
Going the other direction, if the user selects the Show Ribbon option from the View menu, the ID_SHOW_RIBBON
case in OnCommand
is triggered.
case ID_SHOW_RIBBON:
PostMessage(hwnd, AM_SHOW_RIBBON, 0, 0);
break;
This, in turn, activates a case in WndProc
.
case AM_SHOW_RIBBON:
{
if (IsRibbonSupported())
{
killRibbon = false;
ShowWindow(GetHWND(), SW_HIDE);
CreateRibbon();
ShowWindow(GetHWND(), SW_SHOW);
RECT rect = {};
GetClientRect(GetHWND(), &rect);
PostMessage(GetHWND(), WM_SIZE, 0,
MAKELPARAM(rect.right - rect.left, rect.bottom - rect.top));
}
}
return 0;
I ran into a number of painting problems in this code, and I eventually decided to just hide the window, restore the Ribbon, then show the window again. This is a little bit jarring, but until I can find a workaround for the painting problems, it's effective enough. (If you find a better workaround, please let me know.) I felt particularly dirty about posting a WM_SIZE
message, but that was the only reliable way I could find to adjust the client area properly. When I saw that the same technique was used in WTL 8.0, I didn't feel so bad anymore.
Saving and Restoring Ribbon Settings
Above, we saw that the OnViewChanged
method calls LoadRibbonSettings
when the Ribbon is initialized, and SaveRibbonSettings
when the ribbon is destroyed. The SaveRibbonSettings
method creates an IStream
object on a file in the user's application data directory, and passes that stream to the pRibbon->SaveSettingsToStream
method. The Ribbon framework will write its settings into this stream, after which the SaveRibbonSettings
method releases the IStream
and IStorage
objects.
bool App::SaveRibbonSettings(
IUIRibbon* pRibbon)
{
HRESULT hr = E_FAIL;
WCHAR pPath[MAX_PATH] = {};
if (BuildSettingsPath(pPath, L"ScratchRibbonProjectSettings.bin"))
{
IStorage* pStorage = 0;
hr = StgCreateStorageEx(pPath, STGM_CREATE|STGM_SHARE_EXCLUSIVE|STGM_READWRITE,
STGFMT_STORAGE, 0, NULL, NULL, __uuidof(IStorage), (void**)&pStorage);
if (SUCCEEDED(hr))
{
IStream* pStream = 0;
hr = pStorage->CreateStream(L"Ribbon",
STGM_CREATE|STGM_READWRITE|STGM_SHARE_EXCLUSIVE,
0, 0, &pStream);
if (SUCCEEDED(hr))
{
hr = pRibbon->SaveSettingsToStream(pStream);
pStream->Release();
}
pStorage->Release();
}
}
return SUCCEEDED(hr);
}
At Ribbon initialization, LoadRibbonSettings
opens the Ribbon stream in the file created by SaveRibbonSettings
and passes it to pRibbon->LoadSettingsFromStream
. This will restore the previously saved state of the Ribbon.
bool App::LoadRibbonSettings(
IUIRibbon* pRibbon)
{
HRESULT hr = E_FAIL;
WCHAR pPath[MAX_PATH] = {};
if (BuildSettingsPath(pPath, L"ScratchRibbonProjectSettings.bin"))
{
IStorage* pStorage = 0;
hr = StgOpenStorageEx(
pPath,
STGM_READ|STGM_SHARE_DENY_WRITE,
STGFMT_STORAGE,
0, NULL, NULL,
__uuidof(IStorage),
(void**)&pStorage);
if (SUCCEEDED(hr))
{
IStream* pStream = 0;
hr = pStorage->OpenStream( L"Ribbon", NULL,
STGM_READ|STGM_SHARE_EXCLUSIVE,0, &pStream);
if (SUCCEEDED(hr))
{
LARGE_INTEGER liStart = {0, 0};
ULARGE_INTEGER ulActual;
pStream->Seek(liStart, STREAM_SEEK_SET, &ulActual);
hr = pRibbon->LoadSettingsFromStream(pStream);
pStream->Release();
}
pStorage->Release();
}
}
return SUCCEEDED(hr);
}
Enabling and Disabling Ribbon Commands
To demonstrate how to disable Ribbon commands, the Save and Redo commands will change state based on the other commands chosen by the user. The Save command is disabled until the New or Undo commands are activated, and the Redo command is disabled until the Undo command is activated.
The state of the Save command is controlled by a dirty flag set in the App
object. If the current "document" is flagged as dirty, and therefore a candidate for a save operation, the Save command should be enabled. To set or clear the dirty flag, the application calls the SetDirty
method of the App
object.
void App::SetDirty(bool isDirtyInit)
{
isDirty = isDirtyInit;
if (pFramework)
{
pFramework->InvalidateUICommand(ID_CMD_SAVE, UI_INVALIDATIONS_STATE, NULL);
}
HMENU hMenu = GetMenu(GetHWND());
if (hMenu)
{
EnableMenuItem(hMenu, ID_CMD_SAVE, isDirty ? MF_ENABLED : MF_GRAYED);
}
}
This method modifies both the menu and Ribbon commands. It first checks to see if the pFramework
pointer is non-null. If it is, it points to an instance of the IUIFramework
interface implemented by the Ribbon API. It uses the pointer to call the InvalidateUICommand
method, specifying that the ID_CMD_SAVE
command should be invalidated. This instructs the framework to call the application's implementation of IUICommandHandler::UpdateProperty
, which will set the state of the command based on the dirty flag.
Various Clean-up Tasks
All that we have left to cover are the various clean-up actions to restore the client area and handle some painting problems. First of all, there is a read-only edit control in the main window's client area to display the descriptions of the selected Ribbon or menu commands. This control needs to be resized to fit around the Ribbon control when the main window is resized or the Ribbon size changes. This is accomplished by handling the WM_SIZE
message.
void App::OnSize(
HWND hwnd,
UINT state,
int cx,
int cy)
{
AdjustClientArea(cx, cy);
if (killRibbon && state != SIZE_MINIMIZED)
{
SetWindowPos(hwnd, NULL, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
}
}
The AdjustClientArea
function accepts the client width and height, and adjusts the edit window to find around the Ribbon.
void App::AdjustClientArea(
int cx,
int cy)
{
UINT32 ribbonHeight = 0;
if (pRibbon)
{
pRibbon->GetHeight(&ribbonHeight);
}
SendMessage(hEdit, WM_SETREDRAW, 0, 0);
MoveWindow(
hEdit,
0, ribbonHeight,
cx,
cy - ribbonHeight,
TRUE);
int textLen = GetWindowTextLength(hEdit);
SendMessage(hEdit, EM_SETSEL, static_cast<WPARAM>(textLen),
static_cast<LPARAM>(textLen));
SendMessage(hEdit, WM_SETREDRAW, 1, 0);
RedrawWindow(hEdit, NULL, NULL,
RDW_ERASE | RDW_FRAME | RDW_INVALIDATE | RDW_ALLCHILDREN);
SendMessage(hEdit, EM_SCROLLCARET, 0, 0);
}
AdjustClientArea
calls pRibbon->GetHeight
in order to get the height of the Ribbon, and then adjusts the size of the edit window accordingly.
The OnSize
method also checks to see if the killRibbon
flag is set. This flag is set when the user hides the ribbon and restores the menu. If the flag is set, OnSize
calls SetWindowPos
to trigger a WM_NCCALCSIZE
message.
There are also handlers for WM_SIZING
and WM_ACTIVATE
that check the killRibbon
flag and call SetWindowPos
if it is set. If it is not, they defer handling to the base-class version of WndProc
, which in turn calls the default handler function DefWindowProc
.
case WM_SIZING:
{
if (killRibbon)
{
switch (wParam)
{
case WMSZ_TOP:
case WMSZ_TOPLEFT:
case WMSZ_TOPRIGHT:
{
PRECT pRect = reinterpret_cast<PRECT>(lParam);
SetWindowPos(GetHWND(), NULL,
pRect->left, pRect->top,
pRect->right - pRect->left, pRect->bottom - pRect->top,
SWP_NOMOVE | SWP_NOZORDER | SWP_FRAMECHANGED);
}
break;
default:
WinApp::WndProc(msg, wParam, lParam);
}
return TRUE;
}
return WinApp::WndProc(msg, wParam, lParam);
}
case WM_ACTIVATE:
{
if (killRibbon)
{
if (wParam != WA_INACTIVE)
{
SetWindowPos(GetHWND(), NULL,
0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
}
return FALSE;
}
return WinApp::WndProc(msg, wParam, lParam);
}
The WM_NCCALCSIZE
handler does some additional work to draw the frame correctly.
case WM_NCCALCSIZE:
{
LRESULT result = WinApp::WndProc(msg, wParam, lParam);
if (killRibbon && wParam)
{
MARGINS margins = {};
DwmExtendFrameIntoClientArea(GetHWND(), &margins);
RECT adjustedRect = {};
AdjustWindowRectEx(&adjustedRect, GetWindowStyle(GetHWND()),
TRUE, GetWindowExStyle(GetHWND()));
LPNCCALCSIZE_PARAMS pParams = (LPNCCALCSIZE_PARAMS)lParam;
pParams->rgrc[0].top = pParams->rgrc[1].top + (-adjustedRect.top);
}
return result;
}
When the Ribbon is restored, the killRibbon
flag is cleared and normal processing occurs for these messages.
Notice that the WM_NCCALCSIZE
handler calls a Desktop Window Manager function, DwmExtendFrameIntoClientArea
, which is implemented in the DWM API library, dwmapi.dll. This library won't be available on Windows XP, however. We could use the LoadLibrary
function to load the DLL on demand, but that's a lot of hassle. Instead, we'll use the delay-load feature of Visual Studio to let the linker generate the necessary code to load the DLL the first time one of its exported functions is called.
Edit the project settings, add dwmapi.lib to the list of files in the "Additional Dependencies" property, then add dwmapi.dll to the "Delay Loaded DLLs" property.
Since we will never step into this code path in Windows XP, the application will never attempt to load dwmapi.dll.
Final Thoughts, Future Changes
The main thing that annoys me about this code is that the Ribbon graphics increase the size of the executable. For the users that prefer to use the menu, this is a waste of memory, but I could get around that problem by keeping the Ribbon resources in a separate DLL. I haven't tried implementing that yet, but I'll post an update when I do.
I've covered a lot of ground here, so please leave a comment if anything is still unclear.
Note: This project uses the Must Have Icons by VisualPharm.