【IT168专稿】在之前关于Visual Studio 2010的新特性介绍文章中,我们已经喊出了“全体程序员注意:Ribbon全面来袭!”的口号。在这里,我们不再去强调Ribbon界面的易用和高效,也不去强调和证明它将成为未来应用程序界面的一种趋势。为了让我们的应用程序“Ready for Windows 7”,我们有必要根据实际的情况需要,为我们的应用程序添加Ribbon界面。
在前面的文章中,我们介绍了如何为MFC应用程序添加Ribbon界面,但是这种方法只适用于基于MFC的Windows应用程序。如果我们的应用程序是标准的Windows应用程序该怎么办呢?为了推广Ribbon界面,为了讨好程序员们,让程序员可以轻松地为各种应用程序创建Ribbon界面,微软真是不遗余力,不断地推出新的Ribbon界面接口。为了可以让所有基于C++的普通Windows应用程序都可以添加Ribbon界面,微软在Windows 7 SDK中,又推出了基于COM的接口——Scenic Ribbon API。
利用Scenic Ribbon API,我们可以更加灵活方便地为各种Windows应用程序添加Ribbon界面,改善应用程序的用户体验。在接下来的文章中,我们就介绍一下如何利用这种新的方式为普通Windows应用程序添加Ribbon界面。
我们都知道,Ribbon界面是随着Office 2007一同发布的。在过去的几年中,微软不断地吸取用户对Ribbon界面的反馈,不断地对其进行改进,到了Windows 7,微软正式加入了命名为Scenic Ribbon的第二代Ribbon界面,即取代第一代Ribbon的升级版本。随着Scenic Ribbon成为Windows 7的一部分以及微软所提供的Scenic Ribbon API,这意味着今后第三方程序开发者能在自己开发的应用程序中免费用上这种界面形式。Scenic Ribbon作为第二代Ribbon界面,所适用的是所有的Windows应用程序,和前代有所不同是理所当然的。例如,由于最初Ribbon界面是Office专用,因此使用Ribbon界面的Office 2007程序窗口左上角都有一个比较显眼的Office图标按钮。当应用到其他程序之后,这个图标显然不能继续存在,因此在Scenic Ribbon中,这个位置就被相应应用程序自身的图标所取代。另外,为了满足各种应用程序的需要,Scenic Ribbon还添加了部分Ribbon控件,使得Ribbon界面的控件更加丰富,能够应对各种应用程序对Ribbon界面控件的需要。值得一提的是,Office作为Ribbon界面的开山鼻祖,到了Office的下一个版本Office 2010,也全面使用了Scenic Ribbon以替代第一代Ribbon界面。
图1 Office 2010全面使用Scenic Ribbon界面
Scenic Ribbon API概述
为了帮助程序员们能够轻松地在各种环境下实现Ribbon界面,从传统的菜单式界面过渡到Ribbon界面,针对不同的应用环境,微软给我们提供了多种技术方案供我们选择。除了之前我们在Visual Studio 2010新特性介绍文章中所介绍的针对MFC应用程序的各种Ribbon界面类之外,在Windows 7中,微软还为我们提供了用于为非托管代码(Native)应用程序创建Ribbon界面的Scenic Ribbon API的解决方案。另外,针对基于WPF的托管代码应用程序,微软还将提供另外的基于WPF的Ribbon界面解决方案。
图2 微软所提供的Ribbon界面解决方案
在前面的文章中,我们已经介绍了随着Visual Studio 2008 SP1一同发布的MFC Ribbon解决方案。在这篇文章中,我们重点介绍介绍一下随着Windows 7一同发布的Windows Scenic Ribbon API解决方案。
Scenic Ribbon API是一套基于COM的非托管API。通过这套API,我们可以为所有基于Windows的非托管应用程序创建Ribbon界面面板,添加Ribbon控件,处理Ribbon控件的各种动作,执行某种业务逻辑等等。而至于Ribbon界面的布局,各个控件的位置和属性等,则是通过基于XAML的xml文件来进行定义和描述的。编译器会将这个XML文件编译成相应的资源文件,头文件和二进制的BML文件。而Scenic Ribbon API最终会读取这些描述控件信息的文件并创建Ribbon界面。在业务逻辑层,Scenic Ribbon API提供了相应的Execute函数来处理所有控件的消息,在其中我们可以完成相应的业务逻辑。整个过程如下图所示:
图3 Scenic Ribbon API的工作流程
从图中我们可以看到,整个Scenic Ribbon API是基于MVC(Model-View-Controller)模式架构,高度分离的,这跟WPF的模式有些近似:通过业务逻辑和界面设计的分离,可以很好地在软件开发中进行分工,程序员专注于应用程序的业务逻辑,而UI设计师可以专注于界面的设计和用户体验的优化。
图4 新建Win32项目
现在我们就可以编译这个项目,我们会得到一个常规菜单式界面的普通Windows应用程序。这种呆板的界面是不是有些看得厌烦了呢?现在,我们就来为这个应用程序改头换面,添加上时尚的Ribbon界面。在上文中,我们已经介绍过Ribbon界面的描述是通过xml文件来实现的,所以我们新添加一个XML文件到项目中,然后将其编辑如下:
<Application.Commands>
<Command Name="TabHome" Symbol="cmdTabHome" Id="30000" />
</Application.Commands>
<Application.Views>
<Ribbon>
<Ribbon.Tabs>
<Tab CommandName="TabHome">
</Tab>
</Ribbon.Tabs>
</Ribbon>
</Application.Views>
</Application>
在这个XML文件中我们可以看到,整个XML文件被分成了两个部分:命令(Commands)和视图(Views)。命令部分会定义各个控件的属性,包括命令的名字(Name),符号(Symbol),标签文本,图标,命令提示等等。这里需要特别注意的是,控件的名字会在下面的视图部分用到,用来指示同一个控件,而控件的符号,则会在对控件消息进行处理时用到,用来指示消息来自哪一个控件。一个更加完整的命令部分看起来像下面这个样子:
<!--Tab页面-->
<Command Name="TabHome" Symbol="cmdTabHome" Id="30000" LabelTitle="主页" />
<!--主要分组-->
<Command Name="GroupMain" Symbol="cmdGroupMain" Id="30001" LabelTitle="组控件"/>
<Command Name="MyButton" Symbol="cmdMyButton" Id="30002" LabelTitle="按钮控件">
<Command.TooltipTitle>按钮控件</Command.TooltipTitle>
<Command.TooltipDescription>点击这个按钮控件,获得当前系统时间。</Command.TooltipDescription>
<Command.LargeImages>
<Image Source="Button_Image.bmp"/>
</Command.LargeImages>
<Command.SmallImages>
<Image Source="Button_Image.bmp"/>
</Command.SmallImages>
</Command>
</Application.Commands>
在这段代码中,我们定义了一个Tab分页TabHome,然后定义了一个分组GroupMain,最后,我们还定义了一个按钮MyButton,同时定义了这个按钮的标签文本,工具提示,大小图标等。
XML文件的命令部分负责控件的定义,而在视图部分,则是将上面定义的各种命令进行布局,将他们组织到各种控件容器中去,比如分页,分组,工具栏和应用程序菜单等等。同时,视图部分还负责将控件跟各个具体的控件类型绑定起来。一个完整的视图部分看起来像这个样子:
<Ribbon>
<Ribbon.Tabs>
<Tab CommandName='TabHome'>
<Group CommandName='GroupMain' SizeDefinition='OneButton'>
<Button CommandName='MyButton'/>
</Group>
</Tab>
</Ribbon.Tabs>
</Ribbon>
</Application.Views>
上面的XML文件很好地反映了控件之间的嵌套关系:Tab页面包含一个分组,而这个分组又包含一个按钮控件。这样,我们就将我们在命令部分定义的各个控件布局到了Ribbon面板上。
编译Ribbon界面定义文件
完成Ribbon界面定义文件的编辑后,我们需要用UICC.exe将其编译成一种优化后的二进制格式和一个我们通常所用的.rc文件。同时,一个包含了所有控件声明的头文件也会一并产生。这里需要说明的是,UICC.exe是随着Windows 7 SDK一同发布的,要正确地使用UICC.exe,你需要首先正确的安装Windows 7 SDK。
为了完成Ribbon界面定义文件的编译,我们必须定义xml文件的自定义编译过程。在xml文件的属性对话框中,指定其自定义编译命令
同时,指定其编译输出为:
图5 Ribbon界面定义文件的自定义编译
这样,我们在编译解决方案的时候,UICC.exe会 将XML文件编译成二进制的BML文件和头文件.h和.rc资源文件的过程。而这些编译输出,都是我们将在后面用到的。
图6 UICC.exe的编译输出
最后,我们需要将编译生成的.rc资源文件引入到我们项目的资源文件中,在项目的资源文件中插入新编译生成的.rc文件,这就表示我们可以使用Ribbon界面定义文件中定义的各种控件作为界面资源了。
//
// 插入新的Ribbon界面资源定义文件
#include "ribbonres.rc"
// Icon
//
为Ribbon界面创建宿主对象
以上的步骤,我们只是完成了Ribbon界面资源的准备,为了实现Ribbon界面的创建,我们还需要为Ribbon界面创建一个宿主对象,这个宿主对象可以接受来自Ribbon界面的不同控件消息,完成Ribbon界面的创建和初始化。
首先,我们需要为项目添加ATL支持,在项目属性中,选择使用静态ATL或者动态ATL:
图7 添加ATL支持
然后,我们为项目添加一个cpp文件,并在其中实现一个派生自IUIApplication的类,这就是我们为Ribbon界面创建的宿主对象类。
#include "stdafx.h"
//ATL/COM header files:
#include <atlbase.h>
#include <atlcom.h>
#include <initguid.h>
// 引入<uiribbon.h>,其中定义了实现Ribbon界面所需要的所有接口
#include <uiribbon.h>
IUIFramework* g_pFramework = NULL;
// 宿主对象类
class CApplication
: public CComObjectRootEx<CComMultiThreadModel>
, public IUIApplication
{
public:
BEGIN_COM_MAP(CApplication)
COM_INTERFACE_ENTRY(IUIApplication)
END_COM_MAP()
STDMETHOD(OnViewChanged)(UINT32 nViewID, __in UI_VIEWTYPE typeID, __in IUnknown* pView, UI_VIEWVERB verb, INT32 uReasonCode)
{
return E_NOTIMPL;
}
STDMETHOD(OnCreateUICommand)(UINT32 nCmdID, __in UI_COMMANDTYPE typeID, __deref_out IUICommandHandler** ppCommandHandler)
{
return E_NOTIMPL;
}
STDMETHOD(OnDestroyUICommand)(UINT32 commandId, __in UI_COMMANDTYPE typeID, __in_opt IUICommandHandler* pCommandHandler)
{
return E_NOTIMPL;
}
};
在这段代码中,我们首先引入了ATL和COM需要的头文件,同时,为了使用Ribbon界面,我们引入了Ribbon界面的头文件
初始化Ribbon界面
完成Ribbon界面的宿主对象后,我们就可以利用这个对象来创建和初始化Ribbon界面了。在Ribbon.cpp中实现一个全局函数进行Ribbon界面的初始化:
/// 初始化Ribbon界面
HRESULT InitRibbon(HWND hWindowFrame)
{
// 调用CoCreateInstance创建一个CLSID_UIRibbonFramework的实例,
// 创建成功后,我们将得到一个IUIFramework对象,
// 并保存在g_pFramework这个实例对象中
//
HRESULT hr = ::CoCreateInstance(CLSID_UIRibbonFramework, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&g_pFramework));
if(FAILED(hr))
{
return hr;
}
// 创建一个宿主对象CApplication
CComObject<CApplication> *pApplication = NULL;
hr = CComObject<CApplication>::CreateInstance(&pApplication);
if(FAILED(hr))
{
return hr;
}
// 利用Ribbon Framework对象初始化宿主对象
hr = g_pFramework->Initialize(hWindowFrame, pApplication);
if(FAILED(hr))
{
return hr;
}
// 加载Ribbon界面资源,也就是加载XML文件对Ribbon界面的描述
// Scenic Ribbon API将以此为根据创建Ribbon界面
g_pFramework->LoadUI(GetModuleHandle(NULL), L"APPLICATION_RIBBON");
if(FAILED(hr))
{
return hr;
}
return S_OK;
};
在这段代码中,我们通过这样三个步骤来完成Ribbon的初始化:
• 首先我们调用CoCreateInstance创建IUIFramework对象
• 然后然后创建宿主对象CApplication,并利用IUIFramework对象初始化Ribbon界面的宿主对象CApplication
• 最后通过IUIFramework对象加载Ribbon界面资源,以此来完成整个Ribbon界面的初始化过程。
整个过程如下图所示:
图8 Ribbon界面的创建和初始化过程
将Ribbon界面整合到应用程序
完成Ribbon界面的创建和初始化后,这就意味着我们可以在应用程序中使用Ribbon界面了。我们知道Windows Scenic Ribbon API是基于COM接口的,所以要使用Ribbon界面,我们首先需要进行COM的初始化工作,然后调用Ribbon界面的初始化函数完成Ribbon界面和应用程序的整合。修改项目默认创建的RibbonApp.cpp,进行COM的初始化和Ribbon界面的初始化。
首先,我们引入相应的头文件和全局函数声明:
//
#include "stdafx.h"
#include "RibbonApp.h"
#include <atlbase.h>
CComModule _Module;
extern HRESULT InitRibbon(HWND hWindowFrame);
extern void DestroyRibbon();
在应用程序的主函数_tWinMain中,进行COM的初始化和调用完成后的释放:
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow)
{
// 初始化COM
CoInitialize(NULL);
// …
// 释放相应资源
CoUninitialize();
return (int) msg.wParam;
}
然后,我们就可以在InitInstance函数中进行Ribbon界面的初始化了。在显示窗口ShowWindow函数被调用之前,我们添加Ribbon界面的初始化代码。另外,为了防止Ribbon界面窗口在整个窗口缩放的过程中发生闪烁,我们修改了CreateWindow函数的参数,添加了WS_CLIPCHILDREN风格:
{
HWND hWnd;
hInst = hInstance; // Store instance handle in our global variable
// 修改CreateWindows的参数,防止窗口闪烁
hWnd = CreateWindow(szWindowClass, szTitle,
WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL,
hInstance, NULL);
/*
hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
*/
if (!hWnd)
{
return FALSE;
}
// 调用InitRibbon函数,完成Ribbon界面的创建和初始化
HRESULT hr = InitRibbon(hWnd);
if (FAILED(hr))
{
return FALSE;
}
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
return TRUE;
}
最后,我们还需要实现Ribbon界面的销毁。我们创建一个函数DestroyRibbon来完成Ribbon界面的销毁工作:
{
if (g_pFramework)
{
g_pFramework->Destroy();
g_pFramework->Release();
g_pFramework = NULL;
}
}
然后在主窗口销毁的时候,调用这个函数就可以了。在WndProc函数中,在处理WM_DESTROY消息的时候调用DestroyRibbon函数,实现Ribbon界面的销毁:
DestroyRibbon();
PostQuitMessage(0);
break;
至此,大功告成。现在我们可以编译运行整个项目,就可以看到原来的菜单式界面被更换成了全新的Ribbon界面,整个界面焕然一新了。
图9 旧貌换新颜
为了迎接Windows 7,应用程序们都行动起来,打扮打扮啊~~