65.9K
CodeProject 正在变化。 阅读更多。
Home

Win32 C 应用程序中的 Windows Ribbon Framework

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (62投票s)

2010 年 10 月 21 日

CPOL

6分钟阅读

viewsIcon

192602

downloadIcon

8413

在 Win32 C 应用程序中添加原生的 Windows Ribbon Framework(无 MFC 或 ATL)。

引言

微软启动了一个名为 Hilo 的项目,旨在帮助原生程序员构建高性能、响应迅速的富客户端应用程序。本文将尝试用纯 C 语言探索 Hilo 的第 10 章(使用 Windows Ribbon)。

背景

Ribbon 是一个很棒且视觉效果极佳的控件。正如 MSDN 所述,Ribbon 具备以下所有特性:

  1. 所有命令的单一 UI
  2. 可见且不言自明
  3. 带标签的分组
  4. 模态但非层级式
  5. 直接且即时
  6. 宽敞
  7. 拥有应用程序按钮和快速访问工具栏 (QAT)
  8. 最小化自定义
  9. 改进的键盘可访问性

我真正喜欢 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.hRibbon.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 个函数:QueryInterfaceAddRefRelease。我们将开始创建这 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 个必须有的函数:OnViewChangedOnCreateUICommandOnDestroyUICommand

  • OnViewChanged 在视图(RibbonContextPopup 视图)发生变化时被调用。
  • 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 必须包含并以 QueryInterfaceAddRefRelease 开始,然后是我们的 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 的命令处理程序,即我们的应用程序将如何响应任何属性更新请求或响应执行事件。

© . All rights reserved.