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

Microsoft Active Accessibility 简介

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (18投票s)

2007年4月5日

CPOL

23分钟阅读

viewsIcon

129460

downloadIcon

2710

本文详细介绍了一系列旨在解释 MSAA 架构及其使用方法的文章。

引言

这是我希望撰写的关于 Microsoft Active Accessibility (MSAA) 技术系列文章的第一篇。我目前的大部分工作都围绕这个主题,但似乎很少有关于该主题的有用介绍。首先,我想对 MSAA 进行介绍,以便读者能理解它的作用及其存在的原因。在打下基础之后,我们可以继续探讨如何在应用程序中实现它。

在本系列的最后几篇文章中,我希望能提供一些新工具,以帮助开发和调试您启用了 MSAA 的应用程序,甚至可能探讨如何使用 MSAA 进行应用程序自动化。这些工具旨在替代微软提供的那些似乎远不够好用的工具。这些工具的源代码将开放给您查阅。工具的开发将采用本系列文章中展示的相同概念,因此应该能为您提供一个关于实现使用 MSAA 应用程序的视角。

本系列其余的文章将视我的时间而定。由于无法确定时间,每篇文章都将独立成篇,即使没有其他文章也能提供相关信息。祝您阅读愉快!

什么是辅助功能?

在此背景下,“辅助功能”(Accessibility)是指以一种方式创建您的应用程序或框架,使其能够被有某些身体障碍的用户使用,例如失明、低视力、听力不佳或耳聋。根据您开发应用程序的地点或目标用户,您可能在法律上被要求确保您的应用程序具有辅助功能。在使用 Microsoft Windows 进行开发时,有两种主要的辅助技术可供您使用。

第一种也是最简单的一种是系统进入高对比度(High Contrast,HC)模式的能力。在此模式下,系统会进入用户选择的颜色方案,旨在提供前景和背景之间的巨大对比度。这主要针对低视力用户。如果您想看看您的系统在高对比度模式下的样子,可以按住(左 ALT + 左 SHIFT + PRINTSCRN)。您也可以进入“控制面板”->“辅助功能选项”->“显示”进行设置。一旦激活,您会看到您的系统切换到一种对比鲜明的颜色方案。默认的高对比度方案是黑色背景和白色前景。请注意,标题栏的颜色也会改变。在开发应用程序时,记住这一点可能很重要,因为这种切换是基于系统颜色的。这些颜色可以通过 ::GetSysColor(..) API 获取。在您所有使用自定义颜色的地方,即使在 HC 模式下,这些颜色也将保持不变。这可能会使您的应用程序对于有视力障碍的用户更难使用。在可能的情况下,您应尽量使用系统颜色来保持应用程序的辅助功能。

应用程序可用的第二种辅助功能形式是 MSAA。如果您的应用程序仅使用标准的 Win32 控件,那么您很可能几乎不需要做任何额外工作就能实现辅助功能。微软为其所有控件提供了接口。如果您正在开发一套自定义控件工具包,或者只是为您的应用程序开发一个单一的控件,那么您可能需要为这些自定义控件添加 MSAA 支持。由于 Windows 不知道您控件的用途、它是什么或其结构如何,您需要能够传达这些信息,以便屏幕阅读器、放大镜以及其他辅助技术(Assistive Technologies,ATs)设备(如盲文显示器)能够正确地向用户传达关于您控件的信息。以下各节将更详细地介绍 MSAA 的工作原理以及为特定控件实现它需要做些什么。

MSAA 之谜的各个组成部分

在 MSAA 方面,需要了解两个主要机制:MSAA 事件模型和 IAccessible COM 接口。事件模型是应用程序/控件用来通知系统某个特定事件已在控件内发生的机制。这可以是控件刚刚获得焦点或用户修改了控件的值等事件。IAccessible 接口是一个 COM 接口,可以与控件相关联地实现,以便 AT 可以查询有关控件的信息。该接口可用于获取当前值、确定控件的状态(选中、聚焦、禁用等),甚至执行诸如在 MSAA DOM 中移动之类的操作。

IAccessible 接口

首先要介绍的是 IAccessible 接口及其内部工作原理。这是必要的,因为这里解释的一些基本概念,尤其是与 MSAA DOM 相关的概念,是理解 MSAA 事件如何传输到系统,然后又如何被 AT 查询以获取更详细信息的关键。请注意,本节将详细介绍 MSAA 接口的各个方面,包括 DOM 构建、角色、名称和描述等标识元素以及状态数据。对于单个控件,您可能不会关心构建一个完整的元素 DOM,而只是希望在一个现有的 MSAA DOM 中实现单个元素。尽管大多数人可能只关注较小的情况,但我想提供尽可能多的细节,以便那些希望了解全局的人能够看到所有部分是如何工作的。在此基础上,您应该能够推断出单个元素所需的所有信息。那些可能需要构建整个 MSAA DOM 的人,通常是那些正在组建一个包含自定义控件的完整 UI 工具包的人。因为那时您不能依赖微软的控件,所以提供 MSAA 接口的责任就落在了您身上。

应用程序的 MSAA DOM 是一个树形结构的元素组,定义了应用程序的可访问元素。这意味着每个可供最终用户使用的控件都应该出现在 DOM 中。对于一个标准应用程序,即使用标准 Win32 控件的应用程序,每个控件元素,如编辑框、列表、列表项、树、树项、菜单等,都将在 MSAA DOM 中有相关的元素。如果您有一个仅用于视觉目的的自定义控件,例如间隔控件或其他有助于视觉/可用控件布局的元素,则不必为其实现 IAccessible。由于用户无法与此控件交互,并且它不向用户提供任何信息,因此可以将其从 DOM 中省略。这甚至可以被认为是一种好的做法,因为这样 DOM 会更紧凑,从而更高效地遍历。

下面您可以看到我通过检查 Windows 资源管理器中的文件夹视图获取的 MSAA DOM 视图。

Screenshot - msaa_intro1.jpg

这张图片向我们展示了几点。首先,我从控件级别而不是窗口级别开始检查。我们得到的是一个标准的列表控件,以及我所有的文件/文件夹,它们作为列表中的元素显示出来。

首先,让我们以上图为参考,来研究一下 MSAA DOM 的结构,之后我们再探讨可以通过每个元素上的 IAccessible 接口传达的各种信息。请注意,在上面的结构中,第一个元素是列表控件本身。列表中的每个项目都作为该容器的子元素存在。你也可以在树的底部看到列标题的显示。这些元素也都作为列表的子元素存在,尽管在不同的层级。

在我们深入探讨如何使用 IAccessible 接口来遍历和读取 MSAA DOM 之前,请看下面的代码块。这就是实际的 COM 接口的样子。

interface IAccessible : IDispatch
{
  HRESULT STDMETHODCALLTYPE get_accParent([retval][out] 
      IDispatch **ppdispParent);        
  HRESULT STDMETHODCALLTYPE get_accChildCount(
      [retval][out] long *pcountChildren);        
  HRESULT STDMETHODCALLTYPE get_accChild([in] VARIANT varChild, 
      [retval][out] IDispatch **ppdispChild);        
  HRESULT STDMETHODCALLTYPE get_accName([in] VARIANT varChild,
      [retval][out] BSTR *pszName);        
  HRESULT STDMETHODCALLTYPE get_accValue([in] VARIANT varChild, 
      [retval][out] BSTR *pszValue);
  HRESULT STDMETHODCALLTYPE get_accDescription([in] VARIANT varChild, 
      [retval][out] BSTR *pszDescription);
  HRESULT STDMETHODCALLTYPE get_accRole([in] VARIANT varChild, 
      [retval][out] VARIANT *pvarRole);
  HRESULT STDMETHODCALLTYPE get_accState([in] VARIANT varChild, 
      [retval][out] VARIANT *pvarState);
  HRESULT STDMETHODCALLTYPE get_accHelp([in] VARIANT varChild, 
      [retval][out] BSTR *pszHelp);
  HRESULT STDMETHODCALLTYPE get_accHelpTopic([out] BSTR *pszHelpFile, 
      [in] VARIANT varChild,[retval][out] long *pidTopic);
  HRESULT STDMETHODCALLTYPE get_accKeyboardShortcut([in] VARIANT varChild,
      [retval][out] BSTR *pszKeyboardShortcut);
  HRESULT STDMETHODCALLTYPE get_accFocus([retval][out] VARIANT *pvarChild);
  HRESULT STDMETHODCALLTYPE get_accSelection([retval][out] 
      VARIANT *pvarChildren);
  HRESULT STDMETHODCALLTYPE get_accDefaultAction([in] VARIANT varChild, 
      [retval][out] BSTR *pszDefaultAction);        
  HRESULT STDMETHODCALLTYPE accSelect([in] long flagsSelect, 
      [in] VARIANT varChild);
  HRESULT STDMETHODCALLTYPE accLocation([out] long *pxLeft, 
      [out] long *pyTop,[out] long *pcxWidth,[out] long *pcyHeight,
      [in] VARIANT varChild);        
  HRESULT STDMETHODCALLTYPE accNavigate([in] long navDir, 
      [in] VARIANT varStart, [retval][out] VARIANT *pvarEndUpAt);
  HRESULT STDMETHODCALLTYPE accHitTest([in] long xLeft, [in] long yTop,
      [retval][out] VARIANT *pvarChild);
  HRESULT STDMETHODCALLTYPE accDoDefaultAction([in] VARIANT varChild);
  HRESULT STDMETHODCALLTYPE put_accName([in] VARIANT varChild,
      [in] BSTR szName);
  HRESULT STDMETHODCALLTYPE put_accValue([in] VARIANT varChild, 
      [in] BSTR szValue);
};

在构建和遍历 MSAA DOM 时,需要理解两个要点。要点 #1 是并非所有元素都实现 IAccessible 接口。要点 #2 是所有元素都可以通过一个子 ID 找到。给定元素下的子元素通过一个从 1 开始的索引来定位。有一个特殊的子 ID 叫做 CHILDID_SELF(这个常量等于 0),当与像 get_accChild 这样的函数一起使用时,它返回元素本身而不是子元素。

在 MSAA DOM 中,实现 IAccessible 接口的元素称为复杂元素,而未实现此接口的元素称为简单元素。如果一个控件有子项,它就会实现该接口,因此是一个复杂元素。如果它没有实现 IAccessible,就无法访问其子元素。没有子元素的元素则不需要实现该接口。有关该元素的信息可以通过传入相关的子 ID 从其复杂的父元素获取。在上图所示的 DOM 中,我们有一个列表控件,它是一个复杂元素。在列表本身之下,存在所有的列表项。由于列表项没有子元素,因此不实现 IAccessible,它们是简单元素。

可访问数据

提供 IAccessible 接口是为了让辅助技术(AT)能够获取有关非标准控件的信息,以便向用户提供关于屏幕上正在发生的事情的反馈。为此,有许多不同的信息可供使用。下面是通过 IAccessible 可以检索到的与用户相关的数据位列表。我省略了诸如**父级**、**子级**、**子级数量**、**键盘快捷键**等内容,因为我想它们已经相当不言自明了。

  • 角色(Role) - 给定控件的角色是 AT 用来确定控件预期行为的依据。您应该在审查了 MSAA 提供的所有可用角色后,选择最适合您控件的角色。例如,如果您正在创建一个可以用鼠标点击,甚至可以用空格键激活的控件,那么它将与标准按钮具有非常相似的特性。在这种情况下,您会为其分配一个 ROLE_SYSTEM_PUSHBUTTON 的角色。请参阅 MSDN 获取可用的 MSAA 角色完整列表
  • 名称(Name) - 控件的可访问名称应该是一个简短的标识字符串。在大多数情况下,如果您的控件有可见的文本,那么可访问名称就是这个文本。例如,一个带有文本“我是个按钮”的按钮,应该通过 get name 函数返回“我是个按钮”。一个更复杂的例子是编辑字段。如果您有一个用户应该输入用户名的编辑字段,那么您的控件应该向调用者返回“用户名”或类似的变体。在许多情况下,您会有一个静态文本控件向用户显示该字段应输入什么内容。在这些情况下,从静态文本控件中检索明眼用户看到的文本并返回它可能会更容易。但是请注意,保持名称简短且切中要害是一种好的做法。过多的文本信息可能会导致失明或低视力用户感到困惑。
  • 值(Value) - 控件的值完全取决于控件是什么。例如,按钮没有值。它是否被按下由其状态决定。一个带有值的控件的好例子是编辑字段。一个可访问名称为“用户名”的字段,其值将是用户在字段中键入的内容或通过编程设置的内容。请记住,这必须始终与控件中的实际值保持同步,以便用户始终能获得对控件内容的准确口头表述。
  • 描述(Description) - 可访问描述是一个稍微开放的字段。这个属性可以被不同的 AT 供应商以多种不同的方式使用。在大多数情况下,最好用一个稍微更详细的描述来填充这个字段,说明该元素的作用。之前我们讨论过为用户名编辑字段返回“用户名”。如果我们为这个字段想一个描述,它或许可以设置为“请输入您的用户名以访问 Delta Corp. 安全网络”。
  • 状态(State) - 状态就是控件的状态。状态以一个 32 位长整型中设置的位集合的形式返回给调用者。所支持的状态将取决于您实现的控件。有关所有可用状态常量的列表,请参阅 MSDN

MSAA 事件

如上所述,MSAA 事件是应用程序/控件用来通知系统有关该控件的事件已经发生的机制。辅助技术(AT)可以确定这些事件何时发生,并通过通知用户来作出响应。一个例子是,如果用户在运行应用程序时开启了屏幕阅读器(如 Windows 讲述人),并且发生了焦点更改事件。当焦点移动到兼容 MSAA 的控件时,会生成一个事件并传递给操作系统。屏幕阅读器会监听这些事件,并通过向用户读出新获得焦点的控件的名称来响应此事件。同样的例子也适用于控件的状态。例如,如果焦点停留在一个复选框上,用户使用空格键来更改该控件的选中状态。一个事件将被触发,最终屏幕阅读器会通过通知用户复选框的新状态来响应。

微软提供了一个相当全面的事件列表,可以通过系统发送以通知所有监听的 AT。如果您想查看所有可用事件的列表,可以在这个 MSDN 网站上查看。所有事件都通过 API 调用 NotifyWinEvent(..) 传输到操作系统。该函数的原型如下所示。

void WINAPI NotifyWinEvent(
  DWORD event,
  HWND hwnd,
  LONG idObject,
  LONG idChild
);

请注意,我一直谨慎地表示所有事件都是传输到系统(OS),而不是直接传输给 AT。所有可访问性事件都通过 NotifyWinEvent(..) 调用推送,然后由操作系统完成后续工作。所有希望响应系统事件的辅助技术都通过使用一个钩子(hook)来实现。该钩子在每个事件之后被调用,AT 可以从那里决定如何处理它。下面您将看到用于创建这种钩子的函数的原型。

HWINEVENTHOOK WINAPI SetWinEventHook(
  UINT eventMin,
  UINT eventMax,
  HMODULE hmodWinEventProc,
  WINEVENTPROC lpfnWinEventProc,
  DWORD idProcess,
  DWORD idThread,
  UINT dwflags
);

// The event procedure need to use this hook must have the 
// following prototype. (WINEVENTPROC)

VOID CALLBACK WinEventProc(
  HWINEVENTHOOK hWinEventHook,
  DWORD event,
  HWND hwnd,
  LONG idObject,
  LONG idChild,
  DWORD dwEventThread,
  DWORD dwmsEventTime
);

整合起来

到目前为止,我们已经讨论了 IAccessible 接口和由控件生成的事件。大多数 AT 通过以某种方式通知用户来响应事件。要做到这一点,它们通常需要从控件本身获取信息,这意味着它们需要从我们这里获得一个 IAccessible 接口。所以,目前对我们来说的问题是:我们如何向外部应用程序提供一个 IAccessible 接口?答案很简单:WM_GETOBJECT。当一个 AT 收到一个事件,或者通过遍历父窗口的 MSAA DOM 到达我们的窗口时,它们可以使用各种 Win32 API 函数,如 AccessibleObjectFromWindowAccessibleObjectFromEvent 来获取一个接口。在底层,Windows 会向服务器应用程序发送一个 WM_GETOBJECT 消息。然后,我们将相关的接口通过 LresultFromObject API 调用处理后,作为消息的返回值返回给调用者。要看到这个过程的实际操作,请关注我们接下来要构建的示例应用程序!

将信息付诸实践

现在你对这场小舞蹈中的所有参与者都有了相当的背景了解,是时候开始应用这些知识了。为了演示如何创建一个带有 MSAA 支持的自定义控件,我们将构建一个使用革命性新控件“UglyButton”的应用程序。注意,这个控件可不是普通的按钮,它是一个内部包含另外两个按钮的按钮。这个控件本身只有一个 HWND。控件内部包含的两个按钮没有 HWND,但会作为 MSAA DOM 的一部分呈现给用户。我们还将能够在两个内部按钮上方添加一行文本,因为我们就是这么狂野不羁的人。

简要说明:我不会深入讲解如何创建自定义控件和/或如何处理标准 Windows 消息等内部工作原理。我期望您已经具备了 Windows 编程的整体经验,如果您需要理解控件本身是如何构建的,请参阅这里

应用程序

我们将构建一个实现我们“丑陋按钮”控件的应用程序,并在主窗口中放置一个。这是一个标准的 Win32 应用程序,没有 MFC 支持。我选择不使用 MFC 的原因是,我想揭示在一个原本没有辅助功能支持的控件上实现 MSAA 的真实面貌。MFC 在其 CWnd 基类中内置了对 IAccessible 接口的支持。一旦你理解了所有东西是如何协同工作的,回头去使用 MFC 的内置支持应该会很简单。但首先理解更复杂的方法很重要,因为你可能会发现自己需要使用一个非 MFC 的 UI 类库。

另外需要注意的是,我们将要创建的控件没有太多功能或对诸如调整大小、移动等操作的标准支持。这只是为了说明 MSAA 方面的内容。好了,免责声明说完了,我们继续前进!

我们的应用程序有三个主要部分:核心应用程序文件,其中包含应用程序的消息泵;丑陋按钮控件文件;当然还有我们的可访问代理对象,它实现了 IAccessible。我假设阅读本文的读者可以浏览 UglyButton.cpp/h 文件并理解其中大部分内容。内容不多,除了与我们为该控件实现 MSAA 相关的地方外,我不会在这里浪费时间去讲解。应用程序文件也是如此。这让我们专注于实现 IAccessible 的对象,即 CAccProxyObject

这里有一点非常重要需要注意。我们为每个丑陋按钮在窗口属性中存储一个 CAccProxyObject。这个对象作为我们丑陋按钮控件的代理。我们将一个指向按钮数据结构的指针推入这个代理中让它持有。这是一个重要的概念,因为如果我们的丑陋按钮在接口本身的引用计数降到零之前消失了,代理对象中的按钮数据将被置空。这使得所有持有该接口的客户端应用程序,如果它们试图在它所代表的元素消失后使用该接口,就会得到一个特殊的错误码 CO_E_OBJNOTCONNECTED。如果你不实现代理,你可能会通过一个在窗口和子元素仍然有效时获取的接口,引用一个不再存在的窗口和子元素。

审查代理对象

此时,您可能需要花点时间回顾一下 IAccessible 对象的代码。大部分代码都相当容易阅读,我试图尽可能地多加注释,以便尽可能容易地理解发生了什么以及为什么。虽然我不打算逐个函数讲解(我想您可以自己完成),但我想就使用和与该接口交互的几个要点进行说明。

使用子 ID

需要注意的是,IAccessible 接口中几乎每个函数都要求您传递一个子 ID。如果您还记得上面的内容,子 ID 用于引用复杂元素下包含的元素。在我们的“丑陋按钮”案例中,接口是为按钮本身实现的,这是我们的复杂元素。这个元素也可以通过特殊的子 ID CHILDID_SELF 来引用。在该元素下有两个简单元素,即两个内部按钮。这两个按钮的子 ID 分别是 1 和 2。如果我们有更多动态内容,比如一个列表框,其中列表项本身就是子项,那么我们就需要使用一个更动态的过程来确定子 ID。但就我们的目的而言,只要知道我们的按钮有两个内部按钮,就可以让我们用子 ID 1 和 2 来静态地引用它们。

您可以在下面我们示例程序的 MSAA DOM 图像中看到我们按钮控件的结构。

Screenshot - msaa_intro2.jpg

由于子 ID 的概念在所有函数中都是相同的,我将只快速过一遍其中一个函数,其余的留给您自己复习。我们将看一下最常用的函数之一,get_accName。该函数的实现如下所示。

STDMETHODIMP CAccProxyObject::get_accName(VARIANT varChild, BSTR *pszName)
{
  HRESULT retCode = DATACHECK(mData);
  if (SUCCEEDED(retCode))
  {
    if (pszName && VALID_CHILDID(varChild))
    {
      GENBTNDATA* pData = NULL;
      switch (varChild.lVal)
      {
        case CHILDID_SELF:
          pData = &mData->btnSelf;
          break;
        case 1:
          pData = &mData->btnOne;
          break;
        case 2:
          pData = &mData->btnTwo;
          break;
      };
            
      if (pData)
      {
        // First preference goes to the set accessible name. 
        // If no accessible name is available 
        if (pData->pszAccName)
          *pszName = ::SysAllocString(pData->pszAccName);
        else if (wcscmp(L"", pData->szText) != 0)
          *pszName = ::SysAllocString(pData->szText);
      }
    }
    else
      retCode = E_INVALIDARG;
  }
  return retCode;
}

正如您从上面的代码中看到的,实现这个函数并没有太多内容。该函数的第一个参数是我们要检索其名称的项目的子 ID。在这个函数中使用了两个此处代码未显示的宏。DATACHECK 是一个验证 mData 是否为非空的宏。如果它不为 null,返回码将被设置为 S_OK。如果我们的数据元素为 null,则返回码被设置为 CO_E_OBJNOTCONNECTED。记住,这正是拥有代理对象的全部意义所在。另一个宏是 VALID_CHILDID。这个宏只是验证变体的类型是 VT_I4,并且变体结构中的 lVal 成员在 CHILDID_SELF (0) 和 2 之间。如果它不在此范围内,那么我们收到了一个错误的子 ID,需要返回无效参数错误码。

一旦我们确定我们有一个有效的数据元素并且我们的子 ID 是正确的,我们就可以继续检索所请求的信息。switch 语句接收传入的子 ID,并获取与外部按钮或两个内部按钮之一相关联的数据块。然后,我们只需创建字符串的副本并将其设置到调用者提供的存储位置。他们负责释放字符串的内存。您可能还会注意到,我们支持两种不同类型的可访问名称。如果有人调用 put_accName 函数来手动覆盖可访问名称,我们会将其存储在 pszAccName 中。如果此元素从未被设置,那么我们只需尝试使用按钮元素本身上的任何可见文本。

检索接口

现在我们已经介绍了接口,了解如何获取它会很有帮助。下面的一小段代码就是将我们的 IAccessible 接口返回给客户端应用程序所需的全部内容。

case WM_GETOBJECT:
  {
    // This is the message that we must handle so that we can return the
    // IAccessible pointer that relates to this control.
    CAccProxyObject* pProxy = (CAccProxyObject*)GetProp(hWnd, MAKEINTATOM(
        UBPROPATOM_CACC));
    if (pProxy)
    {
      LRESULT lRes = LresultFromObject(IID_IAccessible, wParam, static_cast(
          pProxy));
      return lRes;
    }
  }
  break;

在一个 MSAA 事件被处理后,或者当 Win32 API 函数之一在我们的窗口上使用时,一个 WM_GETOBJECT 消息将被发送给我们,请求我们返回一个我们窗口的可访问接口。由于我们应该已经创建了我们的接口,我们只需从窗口属性中查找它,并调用 LresultFromObject API 调用,将其转换为我们需要返回的值。由于我们的代理对象是持久的,这就是我们在这里需要做的全部工作。如果你是按需创建这些对象(不推荐),那么在调用 LresultFromObject 之后,你需要调用 Release() 方法,因为它会替你执行一次 AddRef()。就是这样!有了这段代码,客户端应用程序现在就可以获取你的 IAccessible 实现的副本了。

生成 MSAA 事件

最后但同样重要的是我们对 MSAA 事件的实现。对于这个示例,我没有费心去实现我们按钮可能拥有的所有可能的事件。相反,我挑选了与按钮控件最相关的那些。

当用户与应用程序交互时,特别是与我们的按钮交互时,我们需要将 MSAA 事件传递给操作系统,以便它能通知所有正在监听的辅助技术(ATs)有事情发生了。您可以阅读上面提供的参考资料来查看所有事件的列表,但让我们具体看一个。

if (PtInRect(&lpData->btnOne.rcBounds, point))
{
  lpData->btnOne.bPushed = TRUE;
  lpData->btnOne.bHasKbFocus = TRUE;
  lpData->btnTwo.bHasKbFocus = FALSE;            
  lpData->btnSelf.bHasKbFocus = FALSE;            
  
  NotifyWinEvent(EVENT_OBJECT_STATECHANGE, hWnd, (LONG)&lpData->btnOne, 1);
}
else if(PtInRect(&lpData->btnTwo.rcBounds, point))
{
  lpData->btnTwo.bPushed = TRUE;            
  lpData->btnOne.bHasKbFocus = FALSE;
  lpData->btnTwo.bHasKbFocus = TRUE;
  lpData->btnSelf.bHasKbFocus = FALSE;            
  
  NotifyWinEvent(EVENT_OBJECT_STATECHANGE, hWnd, (LONG)&lpData->btnTwo, 2);
}
else
{
  lpData->btnSelf.bPushed = TRUE;
  lpData->btnOne.bHasKbFocus = FALSE;
  lpData->btnTwo.bHasKbFocus = FALSE;
  lpData->btnSelf.bHasKbFocus = TRUE;            

  NotifyWinEvent(EVENT_OBJECT_STATECHANGE, hWnd, (LONG)&lpData->btnSelf, 
      CHILDID_SELF);
}

这段代码来自左键按下的处理程序。当有人按下鼠标左键时,鼠标下方的按钮进入按下状态。由于我们按钮的状态现在已经改变,我们需要通知系统这个变化已经发生。为此,我们使用 NotifyWinEvent Win32 API 函数并传入状态改变常量。另请注意,我们必须传入与该事件相关的子 ID。对于我们的按钮控件,我们传入一个自定义标识符(一个指向按钮数据的指针)作为对象 ID。虽然在我们的示例应用程序中,我们没有对这个 ID 做任何特殊处理,但在一个更复杂的应用程序中,可以使用这个自定义数据元素来方便地访问生成该事件的元素。客户端不能使用这些数据,所以它到底是什么对他们来说并不重要。它仅仅是服务器应用程序引用回事件或项目的一种方式。

这与您为自定义控件将要支持的所有事件类型应遵循的模型是相同的。请点击上面章节中的链接,查看微软提供的所有事件列表。

结论

本文到此结束。我希望它对那些正在考虑为自定义控件或库实现 MSAA 的人有所帮助。在我的下一篇文章中,我将从客户端的角度探讨如何使用 MSAA。我打算在编写一个替代微软提供的 event32.exe 的工具时进行此项工作,以便观察通过系统推送的可访问事件。

关注点

历史

2007年4月3日 -- 首次发布。版本 1

© . All rights reserved.