Delphi 组件和屏幕阅读器

Delphi Components and Screen Readers

我使用 Delphi 和 C++Builder,并且有一些关于屏幕 readers 的辅助功能问题。

我在窗体上有一个从 TWinControl 派生出来的按钮。如果我在按钮上放置标题,屏幕 reader 会在按钮处于焦点时读给我听。但是,在某些情况下,我会使用带有图像但没有标题的按钮。如果没有标题,屏幕 reader 什么也不会说。我该怎么做才能让屏幕 reader 说出这个按钮是什么?

类似地,对于从 TGraphicControl 派生的窗体上的图像。当鼠标悬停在 object 上时,我如何告诉屏幕 reader 要说什么?

我研究了 IAccessible 包装器,但如果可能的话,我不希望扩展我们使用的每个控件。

However, there are cases where I use buttons with an image and no caption. The screen reader doesn’t say anything if there is no caption. What can I do to have the screen reader say what this button is?

按钮的 IAccessible 实现必须向屏幕 reader 提供所需的文本。默认情况下,OS 为许多 UI 控件(包括按钮)提供默认 IAccessible 实现。

因此,您可以做的一个简单技巧是手动绘制按钮所有者,然后您可以将其标准 Caption 设置为默认 IAccessible 实现正常使用,然后您可以在绘制按钮时不包含 Caption

否则,您可以直接处理 WM_GETOBJECT 消息以检索按钮的默认 IAccessible 实现,然后包装它,这样您就可以 return 您想要的文本并将其他所有内容委托给默认实现。例如:

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;

Similarly for an image on a form which descends from TGraphic. How can I tell the screen reader what to say when the object gets focus?

首先,TGraphic 不是组件 class。例如,它是 TPicture 使用的图像数据的包装器,它本身是 TImage 使用的助手。我假设你的意思是 TGraphicControl 而不是(TImage 派生自)。

基于 TGraphicControl 的组件在默认情况下不能直接被屏幕 reader 访问,因为它没有自己的 window,因此甚至不知道OS 本身。

如果您希望屏幕 reader 与图形控件交互,您必须从 Parent 组件(确实有 window) 并让它公开有关其图形子项的其他辅助功能信息。

I’ve looked into the IAccessible wrapper, but I would prefer not to extend every control we use if at all possible.

抱歉,您必须这样做(除非您可以找到满足您需要的第 3 方实施)。 VCL 根本不实现任何 IAccessible 功能,因此如果您需要自定义它超出 OS 为您提供的功能,您必须在自己的代码中手动实现它。