1

In this SO post, it is suggested to use IAutoComplete together with TStringsAdapter to implement auto-complete. The following code tries to follow the suggestion but fails enabling the autocomplete feature without compilation & runtime exceptioncomplaining unmatched/inconsistent interface... Could you help to comment about the underlying reason and the work around ?

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, AxCtrls, StdVCL, ActiveX, ComObj;

const
  IID_IAutoComplete         = '{00bb2762-6a77-11d0-a535-00c04fd7d062}';
  IID_IAutoComplete2        = '{EAC04BC0-3791-11d2-BB95-0060977B464C}';
  CLSID_AutoComplete: TGUID = '{00BB2763-6A77-11D0-A535-00C04FD7D062}';

type

  IAutoComplete = interface(IUnknown)
    [IID_IAutoComplete]
    function Init(hwndEdit: HWND; punkACL: IUnknown; pwszRegKeyPath: PWideChar;
      pwszQuickComplete: PWideChar): HResult; stdcall;
    function Enable(fEnable: Boolean): HResult; stdcall;
   end;

  IAutoComplete2 = interface(IAutoComplete)
    [IID_IAutoComplete2]
    function SetOptions(dwFlag: DWORD): HResult; stdcall;
    function GetOptions(out dwFlag: DWORD): HResult; stdcall;
  end;

  TStringsAdapterCracker = class(TStringsAdapter);

  TForm1 = class(TForm)
    ComboBox1: TComboBox;
    procedure FormCreate(Sender: TObject);
  private
    FAutoComplete: IAutoComplete2;
    FStrings: IUnknown;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
var
  hEditControl: THandle;
begin
  With ComboBox1 do begin
    with Items do begin
      BeginUpdate;
      Clear;
      Add('Alpha');
      Add('Beta');
      Add('Gamma');
      Add('Delta');
      EndUpdate;
    end;
    AutoComplete := False;
    ItemIndex := 0;
  end;

  FAutoComplete := CreateComObject(CLSID_AutoComplete) as IAutoComplete2;
  hEditControl := GetWindow(ComboBox1.Handle, GW_CHILD);
  FStrings := TStringsAdapterCracker(TStringsAdapter.Create(ComboBox1.Items))._NewEnum;
  OleCheck(FAutoComplete.Init(hEditControl, FStrings, nil, nil));
end;

end.

Note that related SO posts (here and here) use TEnumString to implement IEnumString manually instead of TStringsAdapter to work with IAutoComplete

Community
  • 1
  • 1
SOUser
  • 3,802
  • 5
  • 33
  • 63
  • 1
    What is the compilation error, which line does it occur? – David Heffernan Dec 16 '15 at 17:12
  • The error is run-time and the error message is localized. Furthermore, it happens in the last line of calling Windows API and thus I could not step in to debug. – SOUser Dec 16 '15 at 17:14
  • `thus I could not step in to debug` - yes, you can. Go into project options and enable "use debug DCUs" checkbox – Arioch 'The Dec 16 '15 at 17:28
  • blind guess: your `FAutoComplete` var is declared `IAutoComplete2 ` type - the type that does NOT have `.init` method - so you just can NOT call on it, you have to use `IAutoComplete` interface – Arioch 'The Dec 16 '15 at 17:31
  • So you still won't tell us what the error message is, nor where it is, and you won't debug. Not cool. – David Heffernan Dec 16 '15 at 17:31
  • You can find a working example of using `IAutoComplete2` in [this answer](http://stackoverflow.com/a/5465826/62576). – Ken White Dec 16 '15 at 21:40
  • @DavidHeffernan Sorry for the confusion. There are no compile or runtime error (exception). The feature is not enabled. Please see edit. – SOUser Dec 16 '15 at 23:36
  • @Arioch'The Could we step in to debug those `stdcall` IAutoComplete methods ? – SOUser Dec 16 '15 at 23:38
  • @KenWhite That link is included at the end of the question. It has used TEnumString to implement IEnumString manually instead of TStringsAdapter to work with IAutoComplete, as suggested by the link at the top of the question... – SOUser Dec 16 '15 at 23:40
  • 1
    I didn't say it showed how to use TStringsAdapter. Your code is totally wrong with regard to how it uses IAutoComplete and IAutoComplete2, and I linked to code that shows how to correctly use those interfaces. If I'd thought the linked question was the entire answer, I would have closed this as a duplicate. Implement the correct use of IAutoComplete in your code, and *then* worry about how to convert it to use TStringsAdapter rather than TEnumString. – Ken White Dec 17 '15 at 00:03
  • @KenWhite Thank you for your suggestion ! Not sure whether it is allowed to change the original (questionable) code in the question drastically.... – SOUser Dec 17 '15 at 00:30
  • 1
    You don't have to change anything drastically. Simply create a `TEnumStrings` implementation that enumerates the `TComboBox.Items` directly, and then replace `FStrings := TStringsAdapterCracker(TStringsAdapter.Create(ComboBox1.Items))._NewEnum;` with `FStrings := TEnumStrings.Create(ComboBox1.Items);` instead. – Remy Lebeau Dec 17 '15 at 00:40
  • `Could we step in to debug those stdcall ` - only in direct x86 assembler. They were not written by you in Delphi - so you do not have source code for those. You can open in Delphi View/Debug Windows/CPU - and step inside machine code, but that hardly would give any helping hand to you – Arioch 'The Dec 17 '15 at 10:18

1 Answers1

5

Could you help to comment about the underlying reason and the work around ?

The reason the code fails is because the TStringsAdapters constructor tries to load a StdVCL type library and fails, raising a "library not registered" error:

constructor TStringsAdapter.Create(Strings: TStrings);
var
  StdVcl: ITypeLib;
begin
  OleCheck(LoadRegTypeLib(LIBID_STDVCL, 4, 0, 0, StdVcl)); // <-- fails!
  inherited Create(StdVcl, IStrings);
  FStrings := Strings;
end;

The TStringsAdapter object is being constructed in the form's OnCreate event, which is triggered after the form's constructor has exited, so the exception does not abort construction or terminate the process, but it does reach a default exception handler that displays an error popup message. Also, the exception is bypassing the call to FAutoComplete.Init(), so no enumerator is created or registered for the ComboBox.

Even though you have added StdVCL to your uses clause, that is not enough to get the StdVCL type library registered on the machine that your app is running on. You would have to distribute and register that type library as part of your app's installation setup.

The workaround is to use a TEnumString implementation that simply enumerates the TStrings values directly, thus avoiding that requirement. As well as it has a little bit less runtime overhead then using TStringsAdapter (whose _NewEnum() method creates a separate TStringsEnumerator object to perform the actual enumeration, so you are actually creating 2 objects instead of 1), but at the expense of having to write a bit more code to implement it, eg:

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ActiveX, ComObj;

const
  IID_IAutoComplete         = '{00bb2762-6a77-11d0-a535-00c04fd7d062}';
  IID_IAutoComplete2        = '{EAC04BC0-3791-11d2-BB95-0060977B464C}';
  CLSID_AutoComplete: TGUID = '{00BB2763-6A77-11D0-A535-00C04FD7D062}';

type
  IAutoComplete = interface(IUnknown)
    [IID_IAutoComplete]
    function Init(hwndEdit: HWND; punkACL: IUnknown; pwszRegKeyPath: PWideChar;
      pwszQuickComplete: PWideChar): HResult; stdcall;
    function Enable(fEnable: Boolean): HResult; stdcall;
   end;

  IAutoComplete2 = interface(IAutoComplete)
    [IID_IAutoComplete2]
    function SetOptions(dwFlag: DWORD): HResult; stdcall;
    function GetOptions(out dwFlag: DWORD): HResult; stdcall;
  end;

  TForm1 = class(TForm)
    ComboBox1: TComboBox;
    procedure FormCreate(Sender: TObject);
  private
    FAutoComplete: IAutoComplete;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

{ TEnumString }

type
  TEnumString = class(TInterfacedObject, IEnumString)
  private
    FStrings: TStrings;
    FCurrIndex: integer;
  public
    //IEnumString
    function Next(celt: Longint; out elt;
        pceltFetched: PLongint): HResult; stdcall;
    function Skip(celt: Longint): HResult; stdcall;
    function Reset: HResult; stdcall;
    function Clone(out enm: IEnumString): HResult; stdcall;
    //VCL
    constructor Create(AStrings: TStrings; AIndex: Integer = 0);
  end;

constructor TEnumString.Create(AStrings: TStrings; AIndex: Integer = 0);
begin
  inherited Create;
  FStrings := AStrings;
  FCurrIndex := AIndex;
end;

function TEnumString.Clone(out enm: IEnumString): HResult;
begin
  enm := TEnumString.Create(FStrings, FCurrIndex);
  Result := S_OK;
end;

function TEnumString.Next(celt: Integer; out elt;
  pceltFetched: PLongint): HResult;
type
  TPointerList = array[0..0] of Pointer; //avoid bug of Classes.pas declaration TPointerList = array of Pointer;
var
  I: Integer;
  wStr: WideString;
begin
  I := 0;
  while (I < celt) and (FCurrIndex < FStrings.Count) do
  begin
    wStr := FStrings[FCurrIndex];
    TPointerList(elt)[I] := CoTaskMemAlloc(2 * (Length(wStr) + 1));
    StringToWideChar(wStr, TPointerList(elt)[I], 2 * (Length(wStr) + 1));
    Inc(I);
    Inc(FCurrIndex);
  end;
  if pceltFetched <> nil then
    pceltFetched^ := I;
  if I = celt then
    Result := S_OK
  else
    Result := S_FALSE;
end;

function TEnumString.Reset: HResult;
begin
  FCurrIndex := 0;
  Result := S_OK;
end;

function TEnumString.Skip(celt: Integer): HResult;
begin
  if (FCurrIndex + celt) <= FStrings.Count then
  begin
    Inc(FCurrIndex, celt);
    Result := S_OK;
  end
  else
  begin
    FCurrIndex := FStrings.Count;
    Result := S_FALSE;
  end;
end;

{ TForm1 }

procedure TForm1.FormCreate(Sender: TObject);
var
  hEditControl: THandle;
  LStrings: IUnknown;
  LAC2: IAutoComplete2;
begin
  with ComboBox1 do
  begin
    with Items do
    begin
      BeginUpdate;
      try
        Clear;
        Add('Alpha');
        Add('Beta');
        Add('Gamma');
        Add('Delta');
      finally
        EndUpdate;
      end;
    end;
    AutoComplete := False;
    ItemIndex := 0;
  end;

  FAutoComplete := CreateComObject(CLSID_AutoComplete) as IAutoComplete;
  hEditControl := GetWindow(ComboBox1.Handle, GW_CHILD); // alternatively, use GetComboBoxInfo() to get the Edit HWND
  LStrings := TEnumString.Create(ComboBox1.Items);
  OleCheck(FAutoComplete.Init(hEditControl, LStrings, nil, nil));
  if Supports(FAutoComplete, IAutoComplete2, LAC2) then
  begin
    // use SetOption as needed...
    OleCheck(LAC2.SetOptions(...));
  end;
end;

end.

Also, keep in mind that if the TComboBox's HWND is ever recreated at runtime, you will have to create a new IAutoComplete object and call init() on it to provide the new HWND. So you should subclass the TComboBox to handle recreation messages, or better would be to use an interceptor class so you can override the TComboBox.CreateWnd() method directly, eg:

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ActiveX, ComObj;

const
  IID_IAutoComplete         = '{00bb2762-6a77-11d0-a535-00c04fd7d062}';
  IID_IAutoComplete2        = '{EAC04BC0-3791-11d2-BB95-0060977B464C}';
  CLSID_AutoComplete: TGUID = '{00BB2763-6A77-11D0-A535-00C04FD7D062}';

type
  IAutoComplete = interface(IUnknown)
    [IID_IAutoComplete]
    function Init(hwndEdit: HWND; punkACL: IUnknown; pwszRegKeyPath: PWideChar;
      pwszQuickComplete: PWideChar): HResult; stdcall;
    function Enable(fEnable: Boolean): HResult; stdcall;
   end;

  IAutoComplete2 = interface(IAutoComplete)
    [IID_IAutoComplete2]
    function SetOptions(dwFlag: DWORD): HResult; stdcall;
    function GetOptions(out dwFlag: DWORD): HResult; stdcall;
  end;

  TComboBox = class(StdCtrls.TComboBox)
  private
    FAutoComplete: IAutoComplete;
  protected
    procedure CreateWnd; override;
    procedure DestroyWnd; override;
  end;

  TForm1 = class(TForm)
    ComboBox1: TComboBox;
    procedure FormCreate(Sender: TObject);
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

{ TEnumString }

type
  TEnumString = class(TInterfacedObject, IEnumString)
  private
    FStrings: TStrings;
    FCurrIndex: integer;
  public
    //IEnumString
    function Next(celt: Longint; out elt;
        pceltFetched: PLongint): HResult; stdcall;
    function Skip(celt: Longint): HResult; stdcall;
    function Reset: HResult; stdcall;
    function Clone(out enm: IEnumString): HResult; stdcall;
    //VCL
    constructor Create(AStrings: TStrings; AIndex: Integer = 0);
  end;

constructor TEnumString.Create(AStrings: TStrings; AIndex: Integer = 0);
begin
  inherited Create;
  FStrings := AStrings;
  FCurrIndex := AIndex;
end;

function TEnumString.Clone(out enm: IEnumString): HResult;
begin
  enm := TEnumString.Create(FStrings, FCurrIndex);
  Result := S_OK;
end;

function TEnumString.Next(celt: Integer; out elt;
  pceltFetched: PLongint): HResult;
type
  TPointerList = array[0..0] of Pointer; //avoid bug of Classes.pas declaration TPointerList = array of Pointer;
var
  I: Integer;
  wStr: WideString;
begin
  I := 0;
  while (I < celt) and (FCurrIndex < FStrings.Count) do
  begin
    wStr := FStrings[FCurrIndex];
    TPointerList(elt)[I] := CoTaskMemAlloc(2 * (Length(wStr) + 1));
    StringToWideChar(wStr, TPointerList(elt)[I], 2 * (Length(wStr) + 1));
    Inc(I);
    Inc(FCurrIndex);
  end;
  if pceltFetched <> nil then
    pceltFetched^ := I;
  if I = celt then
    Result := S_OK
  else
    Result := S_FALSE;
end;

function TEnumString.Reset: HResult;
begin
  FCurrIndex := 0;
  Result := S_OK;
end;

function TEnumString.Skip(celt: Integer): HResult;
begin
  if (FCurrIndex + celt) <= FStrings.Count then
  begin
    Inc(FCurrIndex, celt);
    Result := S_OK;
  end
  else
  begin
    FCurrIndex := FStrings.Count;
    Result := S_FALSE;
  end;
end;

{ TComboBox }

procedure TComboBox.CreateWnd;
var
  hEditControl: THandle;
  LStrings: IUnknown;
  LAC2: IAutoComplete2;
begin
  inherited;
  FAutoComplete := CreateComObject(CLSID_AutoComplete) as IAutoComplete;
  hEditControl := GetWindow(Handle, GW_CHILD); // alternatively, use GetComboBoxInfo() to get the Edit HWND
  LStrings := TEnumString.Create(Items);
  OleCheck(FAutoComplete.Init(hEditControl, LStrings, nil, nil));
  if Supports(FAutoComplete, IAutoComplete2, LAC2) then
  begin
    // use SetOption as needed...
    OleCheck(LAC2.SetOptions(...));
  end;
end;

procedure TComboBox.DestroyWnd;
begin
  FAutoComplete := nil;
  inherited;
end;

{ TForm1 }

procedure TForm1.FormCreate(Sender: TObject);
begin
  with ComboBox1 do
  begin
    with Items do
    begin
      BeginUpdate;
      try
        Clear;
        Add('Alpha');
        Add('Beta');
        Add('Gamma');
        Add('Delta');
      finally
        EndUpdate;
      end;
    end;
    AutoComplete := False;
    ItemIndex := 0;
  end;
end;

end.
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • to make It cleaner perhaps worth checking if FAutoComplete was really created, just like you added OleCheck over other calls – Arioch 'The Dec 17 '15 at 10:24
  • is it correct that `TEnumString.Skip` might set `FCurrIndex == FStrings.Count == High+1` and still return `S_OK` not `S_FALSE` ? – Arioch 'The Dec 17 '15 at 10:30
  • 1
    @Arioch'The: `CreateComObject()` raises an exception if it cannot create the requested object. `OleCheck()` raises an exception if it is passed a failure HRESULT value. And yes, the behavior of `Skip()` is correct. The return value indicates whether `celt` number of items was skipped or not. Think of what happens when `FStrings.Count` is 1, `FCurrIndex` is 0, and `celt` is 1: `(0 + 1) <= 1` should set `FCurrIndex` to `FStrings.Count` (preventing further skipping/reading) and return `S_OK` because `celt` items are skipped. – Remy Lebeau Dec 17 '15 at 17:56