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

Office 2010 VSTO 附加组件的自定义功能区帮助

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.59/5 (6投票s)

2012 年 12 月 6 日

CPOL

6分钟阅读

viewsIcon

60941

downloadIcon

1082

为 VSTO 插件的 Ribbon 控件提供自定义的上下文敏感帮助。

Opening a chm file when pressing f1 for help

引言

如果您曾经编写过包含功能区的 Office 插件,您可能会注意到当您将鼠标悬停在功能区上时弹出的“按 F1 获取帮助”屏幕提示。如果您和我一样,您可能会想知道如何显示自定义帮助,并且对无法做到这一点感到有些失望。

本文介绍了影响屏幕提示的设置,以及一种可用于为自定义 VSTO 应用程序级别插件中的功能区控件显示自定义帮助的方法。

背景

VSTO 应用程序插件允许开发人员扩展 Office 应用程序。虽然每个 Office 应用程序的模型各不相同,但功能区等控件的核心实现是相同的。

功能区控件的一个关键特性是屏幕提示,它允许您输入每个控件功能描述。对于自定义插件,屏幕提示的底部显示一个齿轮图标、您的友好插件名称以及“按 F1 获取插件帮助”消息。

Add-in SuperTips sample

您可以通过“文件...选项...常规”设置来控制是否显示屏幕提示及其超级提示。

对于任何自定义插件,按 F1 会打开通用的“在 Office 程序中查看、管理和安装插件”帮助主题。本文的目的是展示一种改变这种行为以显示自定义帮助的方法。

Office 应用程序使用 Microsoft Active Accessibility (MSAA) 将 UI 信息暴露给外部世界。MSAA 的使用是本文代码实现的核心。有关此主题的介绍,请参阅 Code Project 文章 使用 Microsoft Active Accessibility (MSAA) 进行 UI 自动化

使用代码

当用户按下 F1 键并且功能区处于自定义帮助的上下文中时,RibbonHelpContext 会引发 HelpActivated 事件。该事件使用 HelpActivatedEventArgs 类定义上下文内容。

您需要使用 IHelpCollection 的实例来定义自定义帮助的内容,该实例公开了一个 RibbonHelpItem 项的字典。这允许上下文知道它为哪些控件提供自定义帮助(代码的一个不希望有的副作用是,您可以为功能区的任何部分提供帮助)。

RibbonHelpItem 描述了两个属性:KeyHelpIdKey 描述控件的路径,是一个以冒号分隔的层次结构中 ID 的列表。HelpId 就是一个标识符,您可以使用它来激活所需的帮助行为。

在我的示例中,我通过序列化 XML 文件实现了 IHelpCollection

<?xml version="1.0" encoding="utf-8"?>
<Help xmlns="http://addin-help">
    
    <!-- Repeat this for each ribbon in your add-in -->
    <AddInHelp key="OfficeHelpSampleRibbon">
        <!-- An entry for each control you want to provide custon help for -->
        <HelpItem key="OfficeHelpSampleRibbon:group1:button1" helpId="1001" />
    </AddInHelp>

</Help>

要使用 RibbonHelpContext,请挂钩插件的启动和关闭事件以初始化和处置 RibbonHelpContext 的实例。在关闭事件中调用 Dispose() 以确保正确移除 Windows API 挂钩尤其重要。

//...
using RibbonHelp.Core;
//...
namespace AddIns.Namespace
{
    public partial class ThisAddIn
    {
        private RibbonHelpContext ribbonHelp;

        private void ThisAddIn_Startup(object sender, System.EventArgs e)
        {
            // get an instance of IHelpCollection from somewhere...
            var hc = HelpCollection.FromXmlString(Resources.Help);
            
            // initialize the local instance
            ribbonHelp = new RibbonHelpContext(hc);

            // decide what you want to do when the help is activated...
            ribbonHelp.HelpActivated += (o, args) => Debug.WriteLine(args.HelpItem.HelpId);
        }

        private void ThisAddIn_Shutdown(object sender, System.EventArgs e)
        {
            // this bit is important to ensure the api hooks are removed correctly
            if (ribbonHelp != null) ribbonHelp.Dispose();
        }
        //...
    }
}

运行示例

示例解决方案包含 Excel、Word、PowerPoint 和 Outlook 2010 的应用程序级别插件。每个示例都有一个功能区,并使用如上所述的 RibbonHelpContext 实例。

在调试模式下运行任何示例时,您可以使用跟踪窗格来概览当前上下文。您可以通过单击每个功能区示例左上角的“切换跟踪窗格”按钮来显示/隐藏跟踪窗格。

Trace pane

在调试模式下运行任何示例时,如果控件上下文标签为绿色,则按 F1 将会显示自定义帮助主题。

实现

大部分工作由两个类完成:RibbonShowHideContextRibbonFocusContext。这两个类都使用 SetWinEventHook 挂钩 MSAA 事件,并使用 AccessibleObjectFrom* P/Invoke API 调用。这两个类都继承自 RibbonContextBase,它负责挂钩事件。

//..
public abstract class RibbonContextBase : IRibbonContext
{
    protected IntPtr Hook { get; private set; }
    protected IHelpItems HelpCollection { get; private set; }

    private GCHandle hookDelegateHandle;
    private readonly WinEventProc hookDelegate;
    internal IWinApi Win32;

    internal RibbonContextBase(IHelpItems helpCollection, IWinApi win32, 
				WinEvent eventMin, WinEvent eventMax)
    {
        if (helpCollection == null)
            throw new ArgumentNullException("helpCollection");

        if (win32 == null)
            throw new ArgumentNullException("win32");

        this.HelpCollection = helpCollection;
        this.Win32 = win32;

        this.hookDelegate = Callback;
        this.hookDelegateHandle = GCHandle.Alloc(this.hookDelegate);

        this.Hook = this.Win32.SetWinEventHook(
            (uint)eventMin,
            (uint)eventMax,
            IntPtr.Zero, hookDelegate, 0, (uint)AppDomain.GetCurrentThreadId(), 0);
    }

    //..
    internal abstract void Callback(IntPtr hWinEventHook, WinEvent eventType,
                                  IntPtr hwnd, uint idObject, 
                                  uint idChild, uint dwEventThread,
                                  uint dwmsEventTime);
    //..
}

RibbonShowHideContextRibbonFocusContext 的组合允许跟踪鼠标和键盘交互。

RibbonShowHideContext

RibbonShowHideContext 跟踪两个 MSAA 事件:EVENT_OBJECT_SHOWEVENT_OBJECT_HIDE。它不跟踪每一个 MSAA 显示和隐藏事件,因为事件太多,效率低下。

为防止这种情况,RibbonShowHideContext 使用 MouseContext 实例,这是一个简单的 Windows 挂钩实现,使用 SetWindowsHookEx API 调用来跟踪 WM_MOUSEMOVE 消息。每当鼠标位于功能区上下文中时,ribbonshowhidecontext 将处理消息并设置任何所需的上下文。它使用 AccessibleObjectFromPoint P/Invoke API 来确定当前上下文中的控件。

//..
public class RibbonShowHideContext : RibbonContextBase
{
    //..
    internal override void Callback(IntPtr hWinEventHook, WinEvent eventType, IntPtr hwnd, 
        uint idObject, uint idChild, uint dwEventThread, uint dwmsEventTime)
    {
        //-> don't do anything if we're not in the ribbon
        if (!this.mouseInContext) return;

        //..
    	Point cursorPosition;
	if (Win32.GetCursorPos(out cursorPosition) == 0) return;

    	IAccessible accFromPoint; 
    	object childFromPoint;
    	Win32.AccessibleObjectFromPoint(cursorPosition, out accFromPoint, out childFromPoint);
    	//..
    }
    //..
}

RibbonFocusContext

RibbonFocusContext 跟踪一个 MSAA 事件:EVENT_OBJECT_FOCUS。它处理消息并设置任何所需的上下文。与 RibbonShowHideContext 不同,它会处理每一个 EVENT_OBJECT_FOCUS 消息。它使用 AccessibleObjectFromEvent P/Invoke API 来确定当前上下文中的控件。

//..
public class RibbonFocusContext : RibbonContextBase
{
    //..
    internal override void Callback(IntPtr hWinEventHook, WinEvent eventType, IntPtr hwnd, 
        uint idObject, uint idChild, uint dwEventThread, uint dwmsEventTime)
    {
    //..
        IAccessible accEventObject; 
    	object child; 
    	if(Win32.AccessibleObjectFromEvent(hwnd, idObject, 
    	          idChild, out accEventObject, out child) != 0x0) 
		return;
    	//..
    }
    //..
}

这两个类实际上都在导航控件的父层次结构/路径,然后检查该路径是否在提供的 IHelpCollection 中。

路径是通过导航从相关 AccessibleObjectFrom* P/Invoke 函数发现的对象所拥有的 IAccessible 接口找到的。

//..RibbonContextBase
private static readonly string[] RootList = 
  new[] { RibbonConstants.RibbonLower, RibbonConstants.Ribbon, RibbonConstants.RibbonTabs };

private List<string> GetAccessiblePathContext(IAccessible accessibleObj)
{
    var contextName = accessibleObj.accName[0];
    var lastName = this.ContextControl;
    var newContextPathList = new List<string>();
            
    if (!string.IsNullOrEmpty(contextName)) 
    {
        //-> Don't repeat
        if (contextName == this.ContextControl) return this.contextPathList;

        lastName = contextName;
        newContextPathList.Add(contextName);
    }

    IAccessible accParent = accessibleObj.accParent;

    while (accParent != null)
    {
        contextName = accParent.accName[0];

        if (contextName.IsNullOrEmpty())
        {
            accParent = accParent.accParent;
            continue;
        }

        //-> For when the Ribbon is collapsed
        if (contextName == RibbonConstants.RibbonTabs && newContextPathList.Count == 1)
        {
            var stateUint = Convert.ToUInt32(accessibleObj.accState[0]);
            var selectable = AccessibleState.HasState(stateUint, 
					Win.AccessibleStates.STATE_SYSTEM_SELECTABLE);

            if (selectable)
            {
                newContextPathList.Insert(0, "*Tab*");
                break;
            }
        }

        //-> once we hit any Ribbon root container we're not interested in any more hierarchy
        if (contextName.EqualsAny(StringComparison.OrdinalIgnoreCase, RootList))
            break;

        if (contextName != lastName)
        {
            newContextPathList.Add(contextName);
            lastName = contextName;
        }

        accParent = accParent.accParent;
    }

    newContextPathList.Reverse();
    return newContextPathList;
}

关注点

屏幕提示样式设置和行为

使用 MSAA 事件跟踪上下文的一个有趣副作用是,其行为几乎自然地反映了屏幕提示样式选项的设置。有三种设置,并且这些设置对不能接受键盘焦点的功能区控件有一种细微的影响。只有当选中了某个项时,功能区控件才能准备好接受键盘焦点。

ScreenTip options

除了“不显示屏幕提示”设置之外,当您在任何功能区项的上下文中按 F1 时,您将被导向“在 Office 程序中查看、管理和安装插件”帮助主题。

当“不显示屏幕提示”开启时,尚未获得键盘焦点的功能区控件将引导您进入 Office 帮助索引。

自定义帮助提供程序的行为最终工作方式非常相似。当功能区获得键盘焦点时,您将被导向自定义帮助主题;否则,您将被导向 Office 帮助索引。

一个例外似乎是 RibbonComboBoxRibbonComboBox 中的项不会引发 EVENT_OBJECT_FOCUS 事件,因此只能由 RibbonShowHideContext 跟踪,而这又依赖于屏幕提示行为。

考虑到我们使用显示和隐藏事件来跟踪上下文,如果没有任何内容显示,我们就没有什么可以跟踪的,这种行为是有道理的。它还表明,功能区控件只有在准备好接受键盘焦点后才会引发 EVENT_OBJECT_FOCUS 事件——正如其文档化行为所述。

这有点太粗暴了

这可能很明显,但以这种方式跟踪功能区并覆盖 F1 键意味着您可以改变整个功能区的帮助行为。虽然这可能听起来像是一个不错的副作用,但我实际上对此感到有点不安。

在实现键盘挂钩时,有一些建议遵循的规则,其中之一是始终使用 CallNextHookEx 将调用传递给链中的下一个处理程序。为了在我们的自定义帮助上下文中阻止默认的 Excel 帮助打开,在激活自定义帮助时不会传递该调用。

这可能会导致依赖 F1 键的其他进程无法正常工作。此外,我们无法确定是否有其他进程阻止我们的行为正常工作。我只在具有 Office 2010 的开发人员 Windows 7 工作站上测试过此代码。我相信还有一些安全和环境方面的考虑因素未被考虑在内。

我希望不必考虑这些

最终,我想编写外观专业、感觉也专业的插件。我希望有一种更好、有文档记录、内置的方法来做到这一点。这可能只是我个人的想法,但我认为开发人员希望有某种方式提供和集成自定义帮助,“按 F1 获取插件帮助”屏幕提示对开发人员和最终用户来说既不友好也不直观。

历史

First version.

© . All rights reserved.