Win32 C 应用程序中的 Windows Ribbon Framework






4.92/5 (62投票s)
在 Win32 C 应用程序中添加原生的 Windows Ribbon Framework(无 MFC 或 ATL)。
引言
微软启动了一个名为 Hilo 的项目,旨在帮助原生程序员构建高性能、响应迅速的富客户端应用程序。本文将尝试用纯 C 语言探索 Hilo 的第 10 章(使用 Windows Ribbon
)。
背景

Ribbon 是一个很棒且视觉效果极佳的控件。正如 MSDN 所述,Ribbon
具备以下所有特性:
- 所有命令的单一 UI
- 可见且不言自明
- 带标签的分组
- 模态但非层级式
- 直接且即时
- 宽敞
- 拥有应用程序按钮和快速访问工具栏 (QAT)
- 最小化自定义
- 改进的键盘可访问性
我真正喜欢 Ribbon
的地方在于它的自适应性。当我们调整窗口大小时,Ribbon
控件会自动调整大小,我们无需编写特殊的函数来处理窗口布局变化。

Ribbon
的自适应性源于其表示与视觉属性 (UI) 和命令逻辑的分离。(稍后我们将讨论这一点。)
Ribbon
中另一个值得一提的绝佳功能是画廊 (galleries)。画廊本质上是一个带有图形的列表框控件,使用户能够进行实时预览。
要使用 Ribbon
,我们需要 Windows SDK 7.0(SDK 7.0 包含编译 XAML 文件和创建二进制文件 (BML 文件) 及资源文件所需的 uuic.exe)。对于使用我们应用程序的客户端,他们需要运行 Windows 7 或带有 SP2 和平台更新的 Windows Vista。
创建 Ribbon 并将其添加到我们的应用程序
Ribbon
的创建分为两个主要任务:
- 设计 UI (标记代码)
- 通过 COM 连接到我们的命令处理程序/回调
好的,让我们从 UI 设计开始。
Ribbon 的标记代码以 XAML 文件形式创建,其中标记包含两个元素:<Application.Views>
和 <Application.Commands>
。
<Application.Views>
有两种视图类型:Ribbon
视图和ContextPopup
视图。要了解这两种视图的区别,让我们看看下面的图片。Ribbon 视图包含以下子项:
Ribbon.ApplicationMenu
Ribbon.HelpButton
Ribbon.Tabs
Ribbon.ContextualTabs
Ribbon.QuickAccessToolbar
Ribbon.SizeDefinitions
而
ContextPopup
视图包含:ContextPopup.ContextMaps
ContextPopup.ContextMenus
ContextPopup.MiniToolbars
在我们的示例中,我们的
Ribbon
视图有三个子项:Ribbon.ApplicationMenu
包含“退出”按钮和 MRU 列表。Ribbon.Tabs
包含 4 组按钮。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>
我们将跳过这一部分,因为本文不会解释如何连接到我们的Ribbon
命令处理程序。现在,了解 Symbol 属性(例如IDC_CMD_EXIT
)将用于连接到我们的命令处理程序就足够了。... <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> ...
还记得我们之前讨论过的编译 XAML 文件和创建 BML 文件吗?现在是我们进行操作的时候了。但在我们编译之前创建的 XAML 文件之前,请确保 Visual Studio 可以找到 uuic.exe。
您可以将 Windows SDK 的 bin 文件夹添加到 Visual Studio 可执行文件目录中,如下面的图片所示。

我们现在准备好了吗?哦,等等。我们需要告诉 Visual Studio 如何编译我们的 Ribbon 标记以及输出什么。
您可以右键单击我们之前创建的(将要编译的)XAML 文件,然后在“常规”选项中将“项类型”更改为“自定义生成工具”,之后我们可以更改“命令行”选项(在新“常规”选项的“自定义生成工具”下)为类似这样的内容。
uicc.exe Ribbon.xml %(Filename).bml /header:%(Filename).h /res:%(Filename).rc
在“输出”选项中,输入此内容:
%(Filename).bml;%(Filename).h;%(Filename).rc
这将告诉 Visual Studio 编译我们的 XAML 文件,并创建二进制文件(Ribbon.bml)和资源文件(Ribbon.h 和 Ribbon.rc)。

UI 设计部分我们完成了,现在可以进入 COM 部分了(即初始化 Ribbon
控件并将标记资源绑定到我们的应用程序)。
我们首先需要创建一个基本的窗口应用程序,它将作为 Ribbon
的宿主。
像往常一样,我们从 WinMain
开始。
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
MSG msg;
if(CoInitialize(0))
return FALSE;
MyRegisterClass(hInstance);
// Perform application initialization.
if (!InitInstance (hInstance, nCmdShow))
{
return FALSE;
}
// Main message loop.
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
CoUninitialize();
return (int) msg.wParam;
}
然后,我们注册窗口类。
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;
}
继续创建窗口。
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;
}
最后:窗口过程。
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;
}
我们的基本窗口应用程序就完成了。现在终于可以进入 COM 部分了。在继续之前,本文不会详细介绍 COM 编程。我建议阅读 Jeff Glatt 关于用纯 C 编写 COM 程序系列文章。
我们知道每个 COM 对象必须有 3 个函数:QueryInterface
、AddRef
和 Release
。我们将开始创建这 3 个函数。首先是 QueryInterface
,此函数将我们的 GUID
与我们已定义的 IID_IUIAPPLICATION
进行比较。如果匹配,则将我们的 IUIApplication
接口指向我们的 Ribbon
对象,并进行引用计数。如果不匹配,则简单返回 E_NOINTERFACE
。
对于 AddRef
和 Release,我们将只增加或减少引用计数。
/*
Must have functions for COM object:
1. QueryInterface will compare GUID,
and if it matches then fill the object
and do reference counting
2. AddRef and Release increment and
decrement the count
*/
HRESULT STDMETHODCALLTYPE QueryInterface(IUIApplication *This, REFIID vtblID,
void **ppv)
{
/* No match */
if(!IsEqualIID(vtblID, &IID_IUIAPPLICATION)
&& !IsEqualIID(vtblID, &IID_IUnknown)) {
*ppv = 0;
return(E_NOINTERFACE);
}
/* Point our IUIApplication interface to our Ribbon obj */
*ppv = This;
/* Ref count */
This->lpVtbl->AddRef(This);
return(NOERROR);
}
ULONG STDMETHODCALLTYPE AddRef(IUIApplication *This)
{
return(InterlockedIncrement(&OutstandingObjects));
}
ULONG STDMETHODCALLTYPE Release(IUIApplication *This)
{
return InterlockedDecrement(&OutstandingObjects);
}
由于每个将要使用 Ribbon
框架的应用程序都将实现 IUIApplication
接口(这是回调入口点方法定义的地方),因此还有另外 3 个必须有的函数:OnViewChanged
、OnCreateUICommand
、OnDestroyUICommand
。
OnViewChanged
在视图(Ribbon
或ContextPopup
视图)发生变化时被调用。OnCreateUICommand
用于将Command
(我们在标记代码中之前指定)与IUICommandHandler
(定义用于收集Command
信息和处理Command
事件的方法的接口)进行绑定。- 最后一个,
OnDestroyUICommand
(顾名思义)在应用程序销毁时被调用。
我们目前只是为这 3 个函数返回 E_NOTIMPL
,因为我们还没有实现它们。
/*
3 must have functions for Ribbon application.
For now we're leaving this as a stub
*/
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;
}
请记住,我们是通过 COM 访问 Ribbon
框架的。这就是为什么现在我们的 VTable
必须包含并以 QueryInterface
、AddRef
、Release
开始,然后是我们的 IUIApplication
函数。请注意顺序!
/*
IUIApplicationVtbl is defined in UiRibbon.h and
CINTERFACE symbol stops IntelliSense from
complaining about undefined IUIApplicationVtbl identifier
*/
IUIApplicationVtbl myRibbon_Vtbl = {QueryInterface,
AddRef,
Release,
OnViewChanged,
OnCreateUICommand,
OnDestroyUICommand};
接下来,我们准备初始化 Ribbon
。了解 Ribbon
框架如何与我们的应用程序交互很有价值,请看这个图。

首先,我们需要创建一个具有 Ribbon
框架 CLSID
的对象。
/*
Because we're only creating a single object of CLSID_UIRibbonFramework,
it's better to use CoCreateInstance
*/
hr = CoCreateInstance(&CLSID_UIRibbonFramework, NULL, CLSCTX_INPROC_SERVER,
&IID_IUIFRAMEWORK, (VOID **)&g_pFramework);
然后,我们调用 COM 的 QueryInterface
函数,该函数依次获取 IUIApplication
对象指针。此对象指针将在 Ribbon
框架的 Initialize
函数中传递,当我们尝试将宿主应用程序与 Ribbon
框架连接时。
IUIApplication *pApplication = NULL;
/* allocate pApplication */
pApplication = (IUIApplication *)GlobalAlloc(GMEM_FIXED, sizeof(IUIApplication));
if(!pApplication) {
return(FALSE);
}
/*
Point our pApplication to myRibbon_Vtbl that contains
standard IUnknown method (QueryInterface, AddRef, Release)
and callback for our IUIApplication interface
(OnViewChanged, OnCreateUICommand, OnDestroyUICommand).
*/
(IUIApplicationVtbl *)pApplication->lpVtbl = &myRibbon_Vtbl;
hr = pApplication->lpVtbl->QueryInterface(pApplication, &IID_IUIAPPLICATION, &ppvObj);
hr = g_pFramework->lpVtbl->Initialize(g_pFramework, hWnd, (IUIApplication *)ppvObj);
如果一切顺利,我们可以继续将标记资源与我们的应用程序绑定,现在我们的应用程序将在运行时知道何时进行命令相关的回调。LoadUI
函数将处理此问题。
请看 LoadUI
的参数。第二个参数是我们的应用程序实例(我们在这里简单地向 GetModuleHandle
函数传递 NULL
以获取用于创建调用进程的文件句柄,即我们自己的应用程序)。第三个参数是我们编译好的二进制标记(默认为 APPLICATION_RIBBON
)。
hr = g_pFramework->lpVtbl->LoadUI(g_pFramework, GetModuleHandle(NULL),
L"APPLICATION_RIBBON");
现在我们的 InitializeFramework
函数已完成,我们需要做的就是调用我们在窗口过程中新创建的函数,就在 WM_CREATE
部分。
case WM_CREATE:
if(!InitializeFramework(hWnd))
return(FALSE);
break;
并且不要忘记进行清理。调用 Destroy
函数将释放并销毁 Ribbon
框架对象的任何实例。
hr = ((IUIFramework *)g_pFramework)->lpVtbl->Destroy(g_pFramework);
g_pFramework = NULL;
case WM_DESTROY:
DestroyRibbon();
PostQuitMessage(0);
break;
好的,我们完成了!
您现在可以测试您的 Ribbon
应用程序了。希望一切顺利,您可以看到 Windows Ribbon
已附加到您的应用程序。
下一步?
接下来,我们将讨论 Ribbon
的命令处理程序,即我们的应用程序将如何响应任何属性更新请求或响应执行事件。