Delphi组件和屏幕阅读器

3
我使用Delphi和C++Builder,需要关注屏幕阅读器的几个问题。
我在form表单上有一个来自TWinControl的按钮。如果我给这个按钮设置标题,屏幕阅读器在按钮获取焦点时会朗读它。但是,有些情况下我会用只带图像而没有标题的按钮。如果没有标题,屏幕阅读器会保持沉默。为了让屏幕阅读器说出这个按钮的内容,我应该怎么办?
类似地,对于从TGraphicControl继承的表单上的图像,我应该如何告诉屏幕阅读器鼠标悬停在对象上时说什么?
我已经研究过IAccessible包装器,但如果可能的话,我不想扩展我们使用的每个控件。

一个 TGraphic 无法获得焦点,它甚至不是一个组件。你可能想说的是 TGraphicControl,但它仍然无法获得焦点。这是 TGraphicControl 的主要目的之一。 - Jerry Dodge
谢谢Jerry。我确实是指TGraphicControl,并相应更新了问题。我的意思是鼠标悬停在控件上,而不是获取焦点。我很好奇其他人为处理表单上TGraphicControls的某种替代文本想出了什么样的想法。 - Mike Nowakowski
1个回答

3
然而,有些情况下我会使用只有图片没有标题的按钮。如果没有标题,屏幕阅读器就不会说任何内容。我该怎么做才能让屏幕阅读器说出这个按钮是什么呢?为了让屏幕阅读器正常工作,按钮的一个IAccessible实现必须提供所需的文本。默认情况下,操作系统为许多UI控件(包括按钮)提供了默认的IAccessible实现。因此,您可以简单地使用所有者绘制方式手动绘制按钮。然后,您可以为默认的IAccessible实现设置其标准标题Caption,然后在绘制按钮时可以不包含标题Caption。否则,您可以直接处理WM_GETOBJECT消息以检索按钮的默认IAccessible实现,然后对其进行包装,以便返回所需的文本并将其他所有内容委托给默认实现。例如:
type
  TMyAccessibleText = class(TInterfacedObject, IAccessible)
  private
    fAcc: IAccessible;
    fAccessibleText: string;
  public:
    constructor Create(Acc: IAccessible; AccessibleText: string);

    function QueryInterface(const IID: TGUID; out Obj): HResult; virtual; stdcall;

    function GetTypeInfoCount(out Count: Integer): HResult; stdcall;
    function GetTypeInfo(Index, LocaleID: Integer; out TypeInfo): HResult; stdcall;
    function GetIDsOfNames(const IID: TGUID; Names: Pointer; NameCount, LocaleID: Integer; DispIDs: Pointer): HResult; stdcall;
    function Invoke(DispID: Integer; const IID: TGUID; LocaleID: Integer; Flags: Word; var Params; VarResult, ExcepInfo, ArgErr: Pointer): HResult; stdcall;

    function Get_accParent(out ppdispParent: IDispatch): HResult; stdcall;
    function Get_accChildCount(out pcountChildren: Integer): HResult; stdcall;
    function Get_accChild(varChild: OleVariant; out ppdispChild: IDispatch): HResult; stdcall;
    function Get_accName(varChild: OleVariant; out pszName: WideString): HResult; stdcall;
    function Get_accValue(varChild: OleVariant; out pszValue: WideString): HResult; stdcall;
    function Get_accDescription(varChild: OleVariant; out pszDescription: WideString): HResult; stdcall;
    function Get_accRole(varChild: OleVariant; out pvarRole: OleVariant): HResult; stdcall;
    function Get_accState(varChild: OleVariant; out pvarState: OleVariant): HResult; stdcall;
    function Get_accHelp(varChild: OleVariant; out pszHelp: WideString): HResult; stdcall;
    function Get_accHelpTopic(out pszHelpFile: WideString; varChild: OleVariant; out pidTopic: Integer): HResult; stdcall;
    function Get_accKeyboardShortcut(varChild: OleVariant; out pszKeyboardShortcut: WideString): HResult; stdcall;
    function Get_accFocus(out pvarChild: OleVariant): HResult; stdcall;
    function Get_accSelection(out pvarChildren: OleVariant): HResult; stdcall;
    function Get_accDefaultAction(varChild: OleVariant; out pszDefaultAction: WideString): HResult; stdcall;
    function accSelect(flagsSelect: Integer; varChild: OleVariant): HResult; stdcall;
    function accLocation(out pxLeft: Integer; out pyTop: Integer; out pcxWidth: Integer; out pcyHeight: Integer; varChild: OleVariant): HResult; stdcall;
    function accNavigate(navDir: Integer; varStart: OleVariant; out pvarEndUpAt: OleVariant): HResult; stdcall;
    function accHitTest(xLeft: Integer; yTop: Integer; out pvarChild: OleVariant): HResult; stdcall;
    function accDoDefaultAction(varChild: OleVariant): HResult; stdcall;
    function Set_accName(varChild: OleVariant; const pszName: WideString): HResult; stdcall;
    function Set_accValue(varChild: OleVariant; const pszValue: WideString): HResult; stdcall;
  end;

constructor TMyAccessibleText.Create(Acc: IAccessible; AccessibleText: string);
begin
  inherited Create;
  fAcc := Acc;
  fAccessibleText := AccessibleText;
end;

function TMyAccessibleText.QueryInterface(const IID: TGUID; out Obj): HResult; virtual; stdcall;
begin
  if IID = IID_IAccessible then
    Result := inherited QueryInterface(IID, Obj)
  else
    Result := fAcc.QueryInterface(IID, Obj);
end;

function TMyAccessibleText.GetTypeInfoCount(out Count: Integer): HResult; stdcall;
begin
  Result := fAcc.GetTypeInfoCount(Count);
end;

function TMyAccessibleText.GetTypeInfo(Index, LocaleID: Integer; out TypeInfo): HResult; stdcall;
begin
  Result := fAcc.GetTypeInfo(Index, LocaleID, TypeInfo);
end;

function TMyAccessibleText.GetIDsOfNames(const IID: TGUID; Names: Pointer; NameCount, LocaleID: Integer; DispIDs: Pointer): HResult; stdcall;
begin
  Result := fAcc.GetIDsOfNames(IID, Names, NameCount, LocaleID, DispIDs);
end;

function TMyAccessibleText.Invoke(DispID: Integer; const IID: TGUID; LocaleID: Integer; Flags: Word; var Params; VarResult, ExcepInfo, ArgErr: Pointer): HResult; stdcall;
begin
  Result := fAcc.Invoke(DispID, IID, LocaleID, Flags, Params, VarResult, ExcepInfo, ArgErr);
end;

function TMyAccessibleText.Get_accParent(out ppdispParent: IDispatch): HResult; stdcall;
begin
  Result := fAcc.Get_accParent(ppdispParent);
end;

function TMyAccessibleText.Get_accChildCount(out pcountChildren: Integer): HResult; stdcall;
begin
  Result := fAcc.Get_accChildCount(pcountChildren);
end;

function TMyAccessibleText.Get_accChild(varChild: OleVariant; out ppdispChild: IDispatch): HResult; stdcall;
begin
  Result := fAcc.Get_accChild(varChild, ppdispChild);
end;

function TMyAccessibleText.Get_accName(varChild: OleVariant; out pszName: WideString): HResult; stdcall;
begin
  Result := fAcc.Get_accName(varChild, pszName);
end;

function TMyAccessibleText.Get_accValue(varChild: OleVariant; out pszValue: WideString): HResult; stdcall;
begin
  if varChild = CHILDID_SELF then
  begin
    pszValue := fAccessibleText;
    Result := S_OK;
  end else
    Result := fAcc.Get_accValue(varChild, pszValue);
end;

function TMyAccessibleText.Get_accDescription(varChild: OleVariant; out pszDescription: WideString): HResult; stdcall;
begin
  Result := fAcc.Get_accDescription(varChild, pszDescription);
end;

function TMyAccessibleText.Get_accRole(varChild: OleVariant; out pvarRole: OleVariant): HResult; stdcall;
begin
  Result := fAcc.Get_accRole(varChild, pvarRole);
end;

function TMyAccessibleText.Get_accState(varChild: OleVariant; out pvarState: OleVariant): HResult; stdcall;
begin
  Result := fAcc.Get_accState(varChild, pvarState);
end;

function TMyAccessibleText.Get_accHelp(varChild: OleVariant; out pszHelp: WideString): HResult; stdcall;
begin
  Result := fAcc.Get_accHelp(varChild, pszHelp);
end;

function TMyAccessibleText.Get_accHelpTopic(out pszHelpFile: WideString; varChild: OleVariant; out pidTopic: Integer): HResult; stdcall;
begin
  Result := fAcc.Get_accHelpTopic(pszHelpFile, varChild, pidTopic);
end;

function TMyAccessibleText.Get_accKeyboardShortcut(varChild: OleVariant; out pszKeyboardShortcut: WideString): HResult; stdcall;
begin
  Result := fAcc.Get_accKeyboardShortcut(varChild, pszKeyboardShortcut);
end;

function TMyAccessibleText.Get_accFocus(out pvarChild: OleVariant): HResult; stdcall;
begin
  Result := fAcc.Get_accFocus(pvarChild);
end;

function TMyAccessibleText.Get_accSelection(out pvarChildren: OleVariant): HResult; stdcall;
begin
  Result := fAcc.Get_accSelection(pvarChildren);
end;

function TMyAccessibleText.Get_accDefaultAction(varChild: OleVariant; out pszDefaultAction: WideString): HResult; stdcall;
begin
  Result := fAcc.Get_accDefaultAction(varChild, pszDefaultAction);
end;

function TMyAccessibleText.accSelect(flagsSelect: Integer; varChild: OleVariant): HResult; stdcall;
begin
  Result := fAcc.accSelect(flagsSelect, varChild);
end;

function TMyAccessibleText.accLocation(out pxLeft: Integer; out pyTop: Integer; out pcxWidth: Integer; out pcyHeight: Integer; varChild: OleVariant): HResult; stdcall;
begin
  Result := fAcc.accLocation(pxLeft, pyTop, pcxWidth, pcyHeight, varChild);
end;

function TMyAccessibleText.accNavigate(navDir: Integer; varStart: OleVariant; out pvarEndUpAt: OleVariant): HResult; stdcall;
begin
  Result := fAcc.accNavigate(navDir, varStart, pvarEndUpAt);
end;

function TMyAccessibleText.accHitTest(xLeft: Integer; yTop: Integer; out pvarChild: OleVariant): HResult; stdcall;
begin
  Result := fAcc.accHitTest(xLeft, yTop, pvarChild);
end;

function TMyAccessibleText.accDoDefaultAction(varChild: OleVariant): HResult; stdcall;
begin
  Result := fAcc.accDoDefaultAction(varChild);
end;

function TMyAccessibleText.Set_accName(varChild: OleVariant; const pszName: WideString): HResult; stdcall;
begin
  Result := fAcc.Set_accName(varChild, pszName);
end;

function TMyAccessibleText.Set_accValue(varChild: OleVariant; const pszValue: WideString): HResult; stdcall;
begin
  if varChild = CHILDID_SELF then
  begin
    fAccessibleText := pszValue;
    Result := S_OK;
  end else
    Result := fAcc.Set_accValue(varChild, pszValue);
end;

type
  TBitBtn = class(Vcl.Buttons.TBitBtn)
  private
    procedure WMGetObject(var Message: TMessage): message WM_GETOBJECT;
  public
    MyAccessibleText: string;
  end;

  TMyForm = class(TForm)
    Button1: TBitBtn;
    ...
    procedure FormCreate(Sender: TObject);
    ...
  end;

procedure TMyForm.FormCreate(Sender: TObject);
begin
  Button1.MyAccessibleText := 'There is an image here';
end;

procedure TBitBtn.WMGetObject(var Message: TMessage);
var
  Acc: IAccessible;
begin
  inherited;
  if (Message.LParam = OBJID_CLIENT) and (Message.Result > 0) and (Caption = '') and (MyAccessibleText <> '') then
  begin
    if ObjectFromLresult(Message.Result, IAccessible, Message.WParam, Acc) = S_OK then
    begin
      Acc := TMyAccessibleText.Create(Acc, MyAccessibleText) as IAccessible;
      Message.Result := LresultFromObject(IAccessible, Message.WParam, Acc);
    end;
  end;
end;

同样地,对于从TGraphic继承的表单上的图像,我应该如何告诉屏幕阅读器在对象获得焦点时说什么?
首先,TGraphic不是一个组件类,它是TPicture使用的图像数据的包装器,TPicture本身是TImage使用的帮助程序。我认为你的意思是TGraphicControl(TImage派生自此)。
默认情况下,基于TGraphicControl的组件不能直接被屏幕阅读器访问,因为它没有自己的窗口,因此甚至OS本身也不知道它的存在。
如果您想让屏幕阅读器与图形控件交互,您必须从父组件提供IAccessible的完整实现(具有窗口),并让其公开关于其图形子对象的其他可访问性信息。
我已经研究了IAccessible包装器,但如果可能的话,我更希望不扩展我们使用的每个控件。
很抱歉,但是您必须这样做(除非您找到一个实现所需功能的第三方实现)。 VCL根本没有实现任何IAccessible功能,因此,如果您需要超出OS为您提供的自定义功能,则必须在自己的代码中手动实现它。

@KenWhite:操作系统本身为许多标准UI控件提供了默认实现,包括按钮。 - Remy Lebeau
@KenWhite:作为额外的书签,请查看我几年前对于在Delphi中创建可访问UI组件的回答,链接为my answer - Remy Lebeau
我完全忘记了看过那个问题。我刚刚查看了一下,发现我不仅已经点赞了它,而且这个问题还被收藏了,所以我可以把它作为参考。我一定是老了。 :-) 感谢你的提醒。 - Ken White
感谢你提供详细的信息,Remy。你关于创建无障碍UI组件的其他文章也很有帮助。这为我提供了几个前进的选择。 - Mike Nowakowski

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接