迁移到 Windows 功能区框架

依赖于传统菜单、工具栏和对话框的应用程序可以迁移到 Windows 功能区框架命令系统的丰富、动态和上下文驱动的 UI。 这是一种简单而有效的方法,可以现代化和振兴应用程序,同时提高其功能的可访问性、可用性和可发现性。

简介

通常,将现有应用程序迁移到功能区框架涉及以下内容:

  • 设计一个功能区布局和控件组织,该布局和控件组织公开现有应用程序的功能,并且足够灵活,可以支持新功能。
  • 调整现有应用程序的代码。
  • 将现有应用程序资源 () 字符串和图像迁移到功能区框架。

注意

应查看 功能区用户体验指南 ,以确定应用程序是否适合用于功能区 UI。

 

设计功能区布局

可以通过执行以下步骤来识别潜在的功能区 UI 设计和控件布局:

  1. 清点所有现有功能。
  2. 将此功能转换为功能区命令。
  3. 将命令组织到逻辑组中。

获取清单

在功能区框架中,由操作工作区或文档的状态或视图的应用程序公开的功能被视为命令。 命令的构成内容可能有所不同,具体取决于现有应用程序的性质和域。

下表列出了一组用于假设文本编辑应用程序的基本命令:

符号 ID 说明
ID_FILE_NEW 0xE100 新建文档
ID_FILE_SAVE 0xE103 保存文档
ID_FILE_SAVEAS 0xE104 另存为... (对话框)
ID_FILE_OPEN 0xE101 打开... (对话框)
ID_FILE_EXIT 0xE102 退出应用程序
ID_EDIT_UNDO 0xE12B 撤消
ID_EDIT_CUT 0xE123 剪切所选文本
ID_EDIT_COPY 0xE122 复制所选文本
ID_EDIT_PASTE 0xE125 粘贴剪贴板中的文本
ID_EDIT_CLEAR 0xE120 删除所选文本
ID_VIEW_ZOOM 1242 缩放... (对话框)

 

生成命令清单时,请超越现有菜单和工具栏。 请考虑用户与工作区交互的所有方式。 尽管并非每个命令都适合包含在功能区中,但此练习很可能公开以前被 UI 层遮盖的命令。

Translate

并非每个命令都需要在功能区 UI 中表示。 例如,输入文本、更改所选内容、滚动或使用鼠标移动插入点都有资格作为假设文本编辑器中的命令,但是,这些命令不适合在命令栏中公开,因为每个命令都涉及与应用程序的 UI 的直接交互。

相反,某些功能可能不会被视为传统意义上的命令。 例如,打印机页边距调整可以在功能区中表示为上下文选项卡或 应用程序模式下的一组微调控件,而不是隐藏在对话框中。

注意

记下可能分配给每个命令的数字 ID。 某些 UI 框架(如 Microsoft 基础类 (MFC) )定义命令的 ID,例如要0xE200) 的文件和编辑菜单 (0xE100。

 

组织

在尝试组织命令清单之前,应查看 功能区用户体验指南 ,了解实现功能区 UI 时的最佳做法。

通常,以下规则可应用于功能区命令组织:

  • 对文件或整个应用程序进行操作的命令很可能属于 应用程序菜单
  • 常用的命令(例如剪切、复制和粘贴 (如文本编辑器示例) 通常放置在默认主页选项卡上。在更复杂的应用程序中,它们可能会复制到功能区 UI 中的其他位置。
  • 重要命令或常用命令可能保证默认包含在 快速访问工具栏中。

功能区框架还通过 ContextPopup 视图提供 ContextMenu 和 MiniToolbar 控件。 这些功能不是必需的,但如果应用程序有一个或多个现有的上下文菜单,则迁移它们包含的命令可能会允许重用现有代码库,并自动绑定到现有资源。

由于每个应用程序都不同,因此请阅读指南,并尝试尽可能充分地应用它们。 功能区框架的目标之一是提供熟悉的一致用户体验,如果新应用程序的设计尽可能多地镜像现有功能区应用程序,这一目标将更容易实现。

调整代码

识别功能区命令并将其组织成逻辑分组后,将功能区框架合并到现有应用程序代码时涉及的步骤数取决于原始应用程序的复杂性。 一般情况下,有三个基本步骤:

  • 根据命令组织和布局规范创建功能区标记。
  • 将旧菜单和工具栏功能替换为功能区功能。
  • 实现 IUICommandHandler 适配器。

创建标记

命令列表及其组织和布局是通过功能区标记编译器使用的功能 区标记文件声明的。

注意

调整现有应用程序所需的许多步骤与启动新的功能区应用程序所需的步骤类似。 有关详细信息,请参阅有关新 功能区应用程序的创建功能区 应用程序教程演练。

 

功能区标记有两个主要部分。 第一部分是命令及其关联资源的清单, (字符串和图像) 。 第二部分指定控件在功能区上的结构和位置。

简单文本编辑器的标记可能如以下示例所示:

注意

本文稍后将介绍图像和字符串资源。

 

<?xml version="1.0" encoding="utf-8"?>
<Application xmlns="http://schemas.microsoft.com/windows/2009/Ribbon">

  <Application.Commands>
    <Command Name="cmdNew" Id="0xE100" Symbol="ID_CMD_NEW" LabelTitle="New document" />
    <Command Name="cmdSave" Id="0xE103" Symbol="ID_CMD_SAVE" LabelTitle="Save" />
    <Command Name="cmdSaveAs" Id="0xE104" Symbol="ID_CMD_SAVEAS" LabelTitle="Save as" />
    <Command Name="cmdOpen" Id="0xE101" Symbol="ID_CMD_OPEN" LabelTitle="Open" />
    <Command Name="cmdExit" Id="0xE102" Symbol="ID_CMD_EXIT" LabelTitle="Exit" />
    <Command Name="cmdUndo" Id="0xE12B" Symbol="ID_CMD_UNDO" LabelTitle="Undo" />
    <Command Name="cmdCut" Id="0xE123" Symbol="ID_CMD_CUT" LabelTitle="Cut" />
    <Command Name="cmdCopy" Id="0xE122" Symbol="ID_CMD_COPY" LabelTitle="Copy" />
    <Command Name="cmdPaste" Id="0xE125" Symbol="ID_CMD_PASTE" LabelTitle="Paste" />
    <Command Name="cmdDelete" Id="0xE120" Symbol="ID_CMD_DELETE" LabelTitle="Delete" />
    <Command Name="cmdZoom" Id="1242" Symbol="ID_CMD_ZOOM" LabelTitle="Zoom" />
  </Application.Commands>

  <Application.Views>
    <Ribbon>
      <Ribbon.ApplicationMenu>
        <ApplicationMenu>
          <MenuGroup>
            <Button CommandName="cmdNew" />
            <Button CommandName="cmdOpen" />
            <Button CommandName="cmdSave" />
            <Button CommandName="cmdSaveAs" />
          </MenuGroup>
          <MenuGroup>
            <Button CommandName="cmdExit" />
          </MenuGroup>
        </ApplicationMenu>
      </Ribbon.ApplicationMenu>
      <Ribbon.QuickAccessToolbar>
        <QuickAccessToolbar>
          <QuickAccessToolbar.ApplicationDefaults>
            <Button CommandName="cmdSave" />
            <Button CommandName="cmdUndo" />
          </QuickAccessToolbar.ApplicationDefaults>
        </QuickAccessToolbar>
      </Ribbon.QuickAccessToolbar>
      <Ribbon.Tabs>
        <Tab>
          <Group CommandName="grpClipboard" SizeDefinition="FourButtons">
            <Button CommandName="cmdPaste" />
            <Button CommandName="cmdCut" />
            <Button CommandName="cmdCopy" />
            <Button CommandName="cmdDelete" />
          </Group>
        </Tab>
        <Tab>
          <Group CommandName="grpView" SizeDefinition="OneButton">
            <Button CommandName="cmdZoom" />
          </Group>
        </Tab>
      </Ribbon.Tabs>
    </Ribbon>
  </Application.Views>

</Application>

为了避免重新定义由 UI 框架(如 MFC)定义的符号,前面的示例对每个 Command (ID_FILE_NEW 与 ID_CMD_NEW) 使用略有不同的符号名称。 但是,分配给每个命令的数字 ID 必须保持不变。

若要将标记文件集成到应用程序中,请配置自定义生成步骤以运行功能区标记编译器,UICC.exe。 生成的标头和资源文件随后合并到现有项目中。 如果示例文本编辑器功能区应用程序名为“RibbonPad”,则需要如下所示的自定义生成命令行:

UICC.exe res\RibbonPad_ribbon.xml res\RibbonPad_ribbon.bin 
         /header:res\RibbonPad_ribbon.h /res:res\RibbonPad_ribbon.rc2

标记编译器创建一个二进制文件、头文件 (H) 文件和一个资源 (RC) 文件。 由于现有应用程序可能具有现有的 RC 文件,因此请将生成的 H 和 RC 文件包含在该 RC 文件中,如以下示例所示:

#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//

#define _AFX_NO_SPLITTER_RESOURCES
#define _AFX_NO_OLE_RESOURCES
#define _AFX_NO_TRACKER_RESOURCES
#define _AFX_NO_PROPERTY_RESOURCES

#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
LANGUAGE 9, 1
#pragma code_page(1252)

#include "res\RibbonPad_ribbon.h"  // Ribbon resources
#include "res\RibbonPad_ribbon.rc2"  // Ribbon resources

#include "res\RibbonPad.rc2"  // non-Microsoft Visual C++ edited resources
#include "afxres.rc"    // Standard components
#include "afxprint.rc"  // printing/print preview resources
#endif
#endif    // not APSTUDIO_INVOKED

替换旧菜单和工具栏

将标准菜单和工具栏替换为旧版应用程序中的功能区需要满足以下条件:

  1. 从应用程序的资源文件中删除工具栏和菜单资源引用。
  2. 删除所有工具栏和菜单栏初始化代码。
  3. 删除用于将工具栏或菜单栏附加到应用程序的顶级窗口的任何代码。
  4. 实例化功能区框架。
  5. 将功能区附加到应用程序的顶级窗口。
  6. 加载已编译的标记。

重要

应保留现有状态栏和键盘快捷表,因为功能区框架不会替换这些功能。

 

以下示例演示如何使用 IUIFramework::Initialize 初始化框架:

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    if (CFrameWnd::OnCreate(lpCreateStruct) == -1)
        return -1;
    
    if (!m_RibbonBar.Create(this, WS_CHILD|WS_DISABLED|WS_VISIBLE|CBRS_TOP|CBRS_HIDE_INPLACE,0))
        return -1;      // fail to create

    if (!m_wndStatusBar.Create(this) || !m_wndStatusBar.SetIndicators(indicators,sizeof(indicators)/sizeof(UINT)))
        return -1;      // fail to create

    // Ribbon initialization
    InitRibbon(this, &m_spUIFramework);

    return 0;
}

以下示例演示如何使用 IUIFramework::LoadUI 加载已编译的标记:

HRESULT InitRibbon(CMainFrame* pMainFrame, IUnknown** ppFramework)
{
    // Create the IUIFramework instance.
    CComPtr<IUIFramework> spFramework;
    HRESULT hr = ::CoCreateInstance(CLSID_UIRibbonFramework, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&spFramework));
    if (FAILED(hr))
        return hr;
    
    // Instantiate the CApplication object.
    CComObject<CApplication>* pApplication;
    hr = CComObject<CApplication>::CreateInstance(&pApplication);   // Refcount is 0
    
    // Call AddRef on the CApplication object. The smart pointer will release the refcount when it is out of scope.
    CComPtr< CComObject<CApplication> > spApplication(pApplication);

    if (FAILED(hr))
        return hr;

    // Initialize and load the Ribbon.
    spApplication->Initialize(pMainFrame);

    hr = spFramework->Initialize(*pMainFrame, spApplication);
    if (FAILED(hr))
        return hr;

    hr = spFramework->LoadUI(GetModuleHandle(NULL), L"APPLICATION_RIBBON");
    if (FAILED(hr))
        return hr;

    // Return IUIFramework interface to the caller.
    hr = spFramework->QueryInterface(ppFramework);

    return hr;
}

上面提到的 CApplication 类必须实现一对组件对象模型 (COM) 功能区框架定义的接口: IUIApplicationIUICommandHandler

IUIApplication 在框架和应用程序之间提供main回调接口 (例如,功能区的高度通过 IUIApplication::OnViewChanged) 而提供单个命令的回调以响应 IUIApplication::OnCreateUICommand

提示: 某些应用程序框架(如 MFC)要求在呈现应用程序的文档空间时考虑功能区栏的高度。 在这些情况下,需要添加隐藏窗口来覆盖功能区栏,并强制文档空间达到所需的高度。 有关此方法的示例,其中基于 IUIRibbon::GetHeight 方法返回的功能区高度调用布局函数,请参阅 HTMLEditRibbon 示例

以下代码示例演示 IUIApplication::OnViewChanged 实现:

// This is the Ribbon implementation that is done by an application.
// Applications have to implement IUIApplication and IUICommandHandler to set up communication with the Windows Ribbon.
class CApplication
    : public CComObjectRootEx<CComSingleThreadModel>
    , public IUIApplication
    , public IUICommandHandler
{
public:

    BEGIN_COM_MAP(CApplication)
        COM_INTERFACE_ENTRY(IUIApplication)
        COM_INTERFACE_ENTRY(IUICommandHandler)
    END_COM_MAP()

    CApplication() : _pMainFrame(NULL)
    {
    }

    void Initialize(CMainFrame* pFrame)
    {
        // Hold a reference to the main frame.
        _pMainFrame = pFrame;
    }

    void FinalRelease()
    {
        // Release the reference.
        _pMainFrame = NULL;
        __super::FinalRelease();
    }

    STDMETHOD(OnViewChanged)(UINT32 nViewID, __in UI_VIEWTYPE typeID, __in IUnknown* pView, UI_VIEWVERB verb, INT32 uReasonCode)
    {
        HRESULT hr;

        // The Ribbon size has changed.
        if (verb == UI_VIEWVERB_SIZE)
        {
            CComQIPtr<IUIRibbon> pRibbon = pView;
            if (!pRibbon)
                return E_FAIL;

            UINT ulRibbonHeight;
            // Get the Ribbon height.
            hr = pRibbon->GetHeight(&ulRibbonHeight);
            if (FAILED(hr))
                return hr;

            // Update the Ribbon bar so that the main frame can recalculate the child layout.
            _pMainFrame->m_RibbonBar.SetRibbonHeight(ulRibbonHeight);
            _pMainFrame->RecalcLayout();
        }

        return S_OK;
    }

    STDMETHOD(OnCreateUICommand)(UINT32 nCmdID, 
                               __in UI_COMMANDTYPE typeID,
                               __deref_out IUICommandHandler** ppCommandHandler)
    {
        // This application uses one command handler for all ribbon commands.
        return QueryInterface(IID_PPV_ARGS(ppCommandHandler));
    }

    STDMETHOD(OnDestroyUICommand)(UINT32 commandId, __in UI_COMMANDTYPE typeID,  __in_opt  IUICommandHandler *commandHandler)
    {        
        return E_NOTIMPL;
    }

private:
    CMainFrame* _pMainFrame;
};

实现 IUICommandHandler 适配器

根据原始应用程序的设计,使用多个命令处理程序实现或调用现有应用程序命令逻辑的单个桥接命令处理程序可能更容易。 许多应用程序使用WM_COMMAND消息实现此目的,只要提供一个通用命令处理程序,只需将WM_COMMAND消息转发到顶级窗口就足够了。

但是,此方法需要对 “退出 ”或 “关闭”等命令进行特殊处理。 由于功能区在处理窗口消息时无法销毁,因此应将WM_CLOSE消息发布到应用程序的 UI 线程,并且不应进行同步处理,如以下示例所示:

// User action callback, with transient execution parameters.
    STDMETHODIMP Execute(UINT nCmdID,
        UI_EXECUTIONVERB verb, 
        __in_opt const PROPERTYKEY* key,
        __in_opt const PROPVARIANT* ppropvarValue,
        __in_opt IUISimplePropertySet* pCommandExecutionProperties)
    {       
        switch(nCmdID)
        {
        case cmdExit:
            ::PostMessage(*_pMainFrame, WM_CLOSE, nCmdID, 0);
            break;
        default:
            ::SendMessage(*_pMainFrame, WM_COMMAND, nCmdID, 0);
        }
        return S_OK;
    }

    STDMETHODIMP UpdateProperty(UINT32 nCmdID, 
                                __in REFPROPERTYKEY key,
                                __in_opt  const PROPVARIANT *currentValue,
                                __out PROPVARIANT *newValue) 
    {        
        return S_OK;
    }

迁移资源

定义命令清单、声明功能区的结构并调整为托管功能区框架的应用程序代码后,最后一步是为每个命令指定字符串和图像资源。

注意

字符串和图像资源通常在标记文件中提供。 但是,可以通过实现 IUICommandHandler::UpdateProperty 回调方法以编程方式生成或替换它们。

 

字符串资源

Command.LabelTitle 是为 Command 定义的最常见字符串属性。 这些标签作为选项卡、组和单个控件的文本标签呈现。 旧菜单项中的标签字符串通常可以重新用于 Command.LabelTitle ,而无需进行太多编辑。

但是,随着功能区的出现,以下约定发生了变化:

  • 不再需要用于指示对话启动命令的省略号 (...) 后缀。
  • 与 (&) 仍可用于指示菜单中出现的命令的键盘快捷方式,但框架支持的 Command.Keytip 属性也实现了类似的目的。

参考文本编辑器示例,可以指定 LabelTitle 和 Keytip 的以下字符串:

符号 原始字符串 LabelTitle 字符串 键提示字符串
ID_FILE_NEW &新增功能 &新增功能 N
ID_FILE_SAVE &救 &救 S
ID_FILE_SAVEAS 另存为 &... 另存为& A
ID_FILE_OPEN &打开。。。 &打开 O
ID_FILE_EXIT 退出(&X) 退出(&X) X
ID_EDIT_UNDO &撤消 撤消 Z
ID_EDIT_CUT Cu&t Cu&t X
ID_EDIT_COPY &复制 &复制 C
ID_EDIT_PASTE &粘贴 &粘贴 V
ID_EDIT_CLEAR &删除 &删除 D
ID_VIEW_ZOOM &缩放。。。 缩放 Z

 

下面是应在大多数命令上设置的其他字符串属性的列表:

现在可以使用指定的所有字符串和图像资源声明选项卡、组和其他功能区 UI 功能。

以下功能区标记示例演示了各种字符串资源:

<Application.Commands>
    <!-- Tabs -->
    <Command Name="tabHome" LabelTitle="Home" Keytip="H" />
    <Command Name="tabView" LabelTitle="View" Keytip="V" />

    <!-- Groups -->
    <Command Name="grpClipboard" LabelTitle="Clipboard" />
    <Command Name="grpZoom" LabelTitle="Zoom" />

    <!-- App menu commands -->
    <Command Name="cmdNew" Id="0xE100" Symbol="ID_CMD_NEW" LabelTitle="New document" Keytip="N" >
      <Command.TooltipTitle>New (Ctrl+N)</Command.TooltipTitle>
      <Command.TooltipDescription>Create a new document.</Command.TooltipDescription>
    </Command>
    <Command Name="cmdSave" Id="0xE103" Symbol="ID_CMD_SAVE" LabelTitle="Save" Keytip="S">
      <Command.TooltipTitle>Save (Ctrl+S)</Command.TooltipTitle>
      <Command.TooltipDescription>Save the current document.</Command.TooltipDescription>
    </Command>
    <Command Name="cmdSaveAs" Id="0xE104" Symbol="ID_CMD_SAVEAS" LabelTitle="Save as" Keytip="A">
      <Command.TooltipDescription>Save the current document with a new name.</Command.TooltipDescription>
    </Command>
    <Command Name="cmdOpen" Id="0xE101" Symbol="ID_CMD_OPEN" LabelTitle="Open" Keytip="O">
      <Command.TooltipTitle>Open (Ctrl+O)</Command.TooltipTitle>
      <Command.TooltipDescription>Open a document.</Command.TooltipDescription>
    </Command>
    <Command Name="cmdExit" Id="0xE102" Symbol="ID_CMD_EXIT" LabelTitle="Exit" Keytip="X">
      <Command.TooltipDescription>Exit the application.</Command.TooltipDescription>
    </Command>

    <!-- ...other commands -->

  </Application.Commands>

图像资源

功能区框架支持的图像格式,这些格式提供比先前菜单和工具栏组件支持的图像格式更丰富的外观。

对于Windows 8及更高版本,功能区框架支持以下图形格式:32 位 ARGB 位图 (BMP) 文件和可移植网络图形 (PNG) 具有透明度的文件。

对于 Windows 7 及更早版本,图像资源必须符合 Windows 中使用的标准 BMP 图形格式。

注意

现有图像文件可以转换为任一格式。 但是,如果图像文件不支持抗锯齿和透明度,结果可能不太令人满意。

 

无法在功能区框架中为图像资源指定单个默认大小。 但是,若要支持控件的 自适应布局 ,可以指定两种大小 (大小) 的图像。 功能区框架中的所有图像都根据每英寸点数 (dpi) 分辨率缩放,具体呈现大小取决于此 dpi 设置。 有关详细信息 ,请参阅指定功能区图像资源

以下示例演示如何在标记中引用一组特定于 dpi 的图像:

<Command Name="cmdNew" Id="0xE100" Symbol="ID_CMD_NEW" LabelTitle="New document" Keytip="N" >
      <Command.TooltipTitle>New (Ctrl+N)</Command.TooltipTitle>
      <Command.TooltipDescription>Create a new document.</Command.TooltipDescription>
      <Command.LargeImages>
        <Image Source="cmdNew-32px.png" MinDPI="96" />
        <Image Source="cmdNew-40px.png" MinDPI="120" />
        <Image Source="cmdNew-48px.png" MinDPI="144" />
        <Image Source="cmdNew-64px.png" MinDPI="192" />
      </Command.LargeImages>
      <Command.SmallImages>
        <Image Source="cmdNew-16px.png" MinDPI="96" />
        <Image Source="cmdNew-20px.png" MinDPI="120" />
        <Image Source="cmdNew-24px.png" MinDPI="144" />
        <Image Source="cmdNew-32px.png" MinDPI="192" />
      </Command.SmallImages>
    </Command>

指定功能区图像资源