Introduction
There's a project started at Microsoft called Hilo with the aim of helping native programmers to build high performance, responsive rich client applications. This article will try to explore Hilo's chapter 10 (Using the Windows Ribbon
) in pure C.
Background
Ribbon is a nice control & highly visual. And as stated on MSDN, Ribbon
has all these characteristics:
- Single UI for all command
- Visible & self explanatory
- Labelled grouping
- Modal but not hierarchical
- Direct & immediate
- Spacious
- Has application button & quick access toolbar (QAT)
- Minimal customization
- Improved keyboard accessibility
What I really like from Ribbon
is that it's adaptive. When we resize window, Ribbon
control will be automatically resized, we don't need to write special function to handle window layout changes.
The adaptiveness of Ribbon
is because of the separation of presentation and visual attributes (UI) and command logic. (We'll talk about this later.)
Another great feature in Ribbon
that is worth mentioning is galleries. Galleries is basically a list box control with graphics inside it, enabling user to do live previewing.
In order to use Ribbon
, we need Windows SDK 7.0 (SDK 7.0 contains uuic.exe needed for compiling XAML file and creating binary (BML file) and resource file). And for the client using our application, they need to run Windows 7 or Windows Vista with SP2 & platform updates.
Creating Ribbon and Adding It To Our Application
The creation of Ribbon
is divided into 2 major tasks:
- Designing UI (markup code)
- Making connection to our command handler / callback through COM
OK, let's start with the design of UI.
Ribbon's markup code is created as an XAML file, where the markup consists of 2 elements:
<Application.Views>
and <Application.Commands>
.
<Application.Views>
There're 2 type of views: Ribbon
view and ContextPopup
view. To see the difference between these 2 views, let's us see the pictures below:
Ribbon view has the following children:
Ribbon.ApplicationMenu
Ribbon.HelpButton
Ribbon.Tabs
Ribbon.ContextualTabs
Ribbon.QuickAccessToolbar
Ribbon.SizeDefinitions
While ContextPopup
view has these:
ContextPopup.ContextMaps
ContextPopup.ContextMenus
ContextPopup.MiniToolbars
In our sample, our Ribbon
view has 3 children:
Ribbon.ApplicationMenu
contains Exit Button and MRU List
Ribbon.Tabs
contains 4 groups of buttons
Ribbon.QuickAccessToolbar
...
<Application.Views>
<Ribbon>
<Ribbon.ApplicationMenu>
<ApplicationMenu CommandName="cmdFileMenu">
<ApplicationMenu.RecentItems>
<RecentItems CommandName="cmdMRUList" MaxCount="1" />
</ApplicationMenu.RecentItems>
<MenuGroup Class="MajorItems">
<Button CommandName="cmdExit" />
</MenuGroup>
</ApplicationMenu>
</Ribbon.ApplicationMenu>
<Ribbon.Tabs>
<Tab CommandName="cmdTab1">
<Group CommandName="cmdGroup1" SizeDefinition="OneButton">
<Button CommandName="cmdButton1" />
</Group>
<Group CommandName="cmdGroup2" SizeDefinition="TwoButtons">
<Button CommandName="cmdButton2" />
<Button CommandName="cmdButton3" />
</Group>
<Group CommandName="cmdGroup3" SizeDefinition="ThreeButtons">
<Button CommandName="cmdButton1" />
<Button CommandName="cmdButton2" />
<Button CommandName="cmdButton3" />
</Group>
<Group CommandName="cmdGroup4" SizeDefinition="FiveOrSixButtons">
<Button CommandName="cmdButton3" />
<Button CommandName="cmdButton4" />
<ToggleButton CommandName="cmdToggleButton1" />
<Button CommandName="cmdButton5" />
<ToggleButton CommandName="cmdToggleButton2" />
</Group>
</Tab>
</Ribbon.Tabs>
<Ribbon.QuickAccessToolbar>
<QuickAccessToolbar CommandName="cmdQat" />
</Ribbon.QuickAccessToolbar>
</Ribbon>
</Application.Views>
...
<Application.Commands>
We'll skip this part, as this article will not explain about connecting to our Ribbon
command handler yet. Right now, it's adequate to know that Symbol attribute (e.g.: IDC_CMD_EXIT
) will be used to connect to our command handler:
...
<Application.Commands>
<Command Name="cmdGroup1">
<Command.SmallImages>
<Image Id="201">res/Button_Image.bmp</Image>
</Command.SmallImages>
</Command>
<Command Name="cmdGroup2"/>
<Command Name="cmdGroup3"/>
<Command Name="cmdGroup4"/>
<Command Name="cmdButton1">
<Command.LabelTitle>
<String Id ="210">Button 1</String>
</Command.LabelTitle>
<Command.LargeImages>
<Image Id="211">res/AddTableL.bmp</Image>
</Command.LargeImages>
<Command.SmallImages>
<Image Id="212">res/AddTableS.bmp</Image>
</Command.SmallImages>
</Command>
<Command Name="cmdToggleButton2">
<Command.LabelTitle>
<String Id ="270">ToggleButton 2</String>
</Command.LabelTitle>
<Command.SmallImages>
<Image Id="271">res/Copy.bmp</Image>
</Command.SmallImages>
</Command>
<Command Name="cmdQat"/>
<Command Name="cmdFileMenu"/>
<Command Name="cmdMRUList">
<Command.LabelTitle>
<String Id ="280">MRU List</String>
</Command.LabelTitle>
</Command>
<Command Name="cmdExit" Symbol="IDC_CMD_EXIT">
<Command.LabelTitle>
<String Id ="290">Exit Button</String>
</Command.LabelTitle>
<Command.LargeImages>
<Image Id ="291">res/ExitL.bmp</Image>
</Command.LargeImages>
</Command>
</Application.Commands>
...
Remember about compiling XAML file and creating BML file we talked before? Now, it's the time for us to do this. But before we compile the XAML file we created earlier, please make sure uuic.exe can be found by Visual Studio.
You can add Windows SDK's bin folder to Visual Studio Executable Directories like the picture below:
Are we ready now? Oh, wait a minute. We need to tell Visual Studio how to compile our Ribbon markup and what to output.
You can do right clicking on the XAML file we created before (that we're going to compile), and change Item Type in the General option to Custom Build Tool, after that we can change Command Line option (in the new General option under Custom Build Tool) to something like this:
uicc.exe Ribbon.xml %(Filename).bml /header:%(Filename).h /res:%(Filename).rc
And in the Outputs option, write this:
%(Filename).bml;%(Filename).h;%(Filename).rc
This will tell Visual Studio to compile our XAML file and create binary file (Ribbon.bml) and resource files (Ribbon.h and Ribbon.rc).
We're done with designing UI part, now we can move on to the COM part. (That's initializing the Ribbon
control and binding the markup resource to our application.)
We need to create a basic window application that will act as the host of the Ribbon
first.
As usual, we start with WinMain
:
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
MSG msg;
if(CoInitialize(0))
return FALSE;
MyRegisterClass(hInstance);
if (!InitInstance (hInstance, nCmdShow))
{
return FALSE;
}
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
CoUninitialize();
return (int) msg.wParam;
}
And then, we register window class:
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEX wcl;
wcl.cbSize = sizeof(WNDCLASSEX);
wcl.hInstance = hInstance;
wcl.lpszClassName = g_szClassName;
wcl.lpfnWndProc = WndProc;
wcl.style = 0;
wcl.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wcl.hIconSm = NULL;
wcl.hCursor = LoadCursor(NULL, IDC_ARROW);
wcl.lpszMenuName = NULL;
wcl.cbClsExtra = 0;
wcl.cbWndExtra = 0;
wcl.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH);
if(!RegisterClassEx(&wcl)) {
return FALSE;
}
return TRUE;
}
Continue with the creation of the window:
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
HWND hWnd;
hWnd = CreateWindow(g_szClassName, g_szAppTitle,
WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
if (!hWnd)
{
return FALSE;
}
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
return TRUE;
}
And finally: the window procedure:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_CREATE:
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
That's it for our basic window application. Now can finally move to the COM part. Before we continue, this article won't go into details about COM programming. I would suggest Jeff Glatt's series of article about programming COM in plain C.
We know that every COM object must have 3 functions: QueryInterface
, AddRef
, and Release
. We'll start creating these 3 functions. First QueryInterface
, this function compares our GUID
with our already defined IID_IUIAPPLICATION
. If it matches, point our IUIApplication
interface to our Ribbon
obj, and do reference counting. And if it doesn't match, simply return E_NOINTERFACE
.
For AddRef
and Release, we'll only increment or decrement the reference count.
HRESULT STDMETHODCALLTYPE QueryInterface(IUIApplication *This, REFIID vtblID,
void **ppv)
{
if(!IsEqualIID(vtblID, &IID_IUIAPPLICATION)
&& !IsEqualIID(vtblID, &IID_IUnknown)) {
*ppv = 0;
return(E_NOINTERFACE);
}
*ppv = This;
This->lpVtbl->AddRef(This);
return(NOERROR);
}
ULONG STDMETHODCALLTYPE AddRef(IUIApplication *This)
{
return(InterlockedIncrement(&OutstandingObjects));
}
ULONG STDMETHODCALLTYPE Release(IUIApplication *This)
{
return InterlockedDecrement(&OutstandingObjects);
}
Because every application that's going to use Ribbon
framework will be implementing IUIApplication
interface (this is where callback entry-point methods defined), there're another 3 must have functions: OnViewChanged
, OnCreateUICommand
, OnDestroyUICommand
.
OnViewChanged
is called when there's a change in view (Ribbon
or ContextPopup
view)
OnCreateUICommand
is called to bind Command
(we specified before in our markup code) with IUICommandHandler
(interface that defines method for gathering Command
information and handling Command
events)
- And the last one,
OnDestroyUICommand
is (as we can see from the name of the function) called when the application is destroyed.
We're now simply returning E_NOTIMPL
for these 3 functions, because we're not yet implementing it.
HRESULT STDMETHODCALLTYPE OnViewChanged(IUIApplication *This, UINT32 viewId,
UI_VIEWTYPE typeID, IUnknown *view, UI_VIEWVERB verb, INT32 uReasonCode)
{
return E_NOTIMPL;
}
HRESULT STDMETHODCALLTYPE OnCreateUICommand(IUIApplication *This, UINT32 commandId,
UI_COMMANDTYPE typeID, IUICommandHandler **commandHandler)
{
return E_NOTIMPL;
}
HRESULT STDMETHODCALLTYPE OnDestroyUICommand (IUIApplication *This, UINT32 commandId,
UI_COMMANDTYPE typeID, IUICommandHandler *commandHandler)
{
return E_NOTIMPL;
}
Remember that we're accessing Ribbon
framework through COM? That's why now our VTable
must have and must be started with QueryInterface
, AddRef
, Release
and followed by our IUIApplication
's functions. Please watch for the order!
IUIApplicationVtbl myRibbon_Vtbl = {QueryInterface,
AddRef,
Release,
OnViewChanged,
OnCreateUICommand,
OnDestroyUICommand};
Next, we're ready to initialize the Ribbon
. It's worth knowing how Ribbon
framework interacts with our application, take a look at this diagram:
First, we need to create an object with Ribbon
framework CLSID
:
hr = CoCreateInstance(&CLSID_UIRibbonFramework, NULL, CLSCTX_INPROC_SERVER,
&IID_IUIFRAMEWORK, (VOID **)&g_pFramework);
And then we call our COM's QueryInterface
function where in turn we get IUIApplication
object pointer. This object pointer will be passed in Ribbon
framework's Initialize
function as we try to connect our host application with Ribbon
framework.
IUIApplication *pApplication = NULL;
pApplication = (IUIApplication *)GlobalAlloc(GMEM_FIXED, sizeof(IUIApplication));
if(!pApplication) {
return(FALSE);
}
(IUIApplicationVtbl *)pApplication->lpVtbl = &myRibbon_Vtbl;
hr = pApplication->lpVtbl->QueryInterface(pApplication, &IID_IUIAPPLICATION, &ppvObj);
hr = g_pFramework->lpVtbl->Initialize(g_pFramework, hWnd, (IUIApplication *)ppvObj);
If everything goes well, we can continue to bind the markup resource with our application, and now our application knows when to makes command-related callbacks at run time. LoadUI
function will handle this.
Take a look at LoadUI
's params. The second param is our application instance (in which we're simply passing NULL
to GetModuleHandle
function to get a handle to the file used to create the calling process, that's our own application). And the third param is our compiled binary markup (default will be APPLICATION_RIBBON
).
hr = g_pFramework->lpVtbl->LoadUI(g_pFramework, GetModuleHandle(NULL),
L"APPLICATION_RIBBON");
Now that our we're done with our InitializeFramework
function, all we need to do is call our newly created function in our window procedure, right in WM_CREATE
section.
case WM_CREATE:
if(!InitializeFramework(hWnd))
return(FALSE);
break;
And don't forget to do the clean up. Calling Destroy
function will release and destroy any instance of Ribbon
framework objects.
hr = ((IUIFramework *)g_pFramework)->lpVtbl->Destroy(g_pFramework);
g_pFramework = NULL;
case WM_DESTROY:
DestroyRibbon();
PostQuitMessage(0);
break;
OK, we're done!
You may test your Ribbon
application now. I hope everything goes well, and you can see Windows Ribbon
attached to your application.
What's Next?
Next, we'll be talking about Ribbon
's command handlers or how our application will respond to any property update requests or respond to execute events.