2

Using some answers in StackOverflow I've created a searcheable TComboBox in Delphi. It works fine when you add it directly to a Form, but breaks as soon as you add it to a TPanel and I can't seem to figure out why.

Directly on the form:

Component directly on form After typing t: Component directly on form 2

Inside a panel:

Component inside a panel After typing t: Component inside a panel 2

Here is the component's code:

unit uSmartCombo;

interface

uses
  Vcl.StdCtrls, Classes, Winapi.Messages, Controls;

type
  TSmartComboBox = class(TComboBox)
  private
    FStoredItems: TStringList;
    procedure FilterItems;
    procedure CNCommand(var AMessage: TWMCommand); message CN_COMMAND;
    procedure RedefineCombo;
    procedure SetStoredItems(const Value: TStringList);
    procedure StoredItemsChange(Sender: TObject);
  protected
    procedure KeyPress(var Key: Char); override;
    procedure CloseUp; override;
    procedure Loaded; override;
    procedure DoExit; override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    property StoredItems: TStringList read FStoredItems write SetStoredItems;
  end;

procedure Register;

implementation

uses
  SysUtils, Winapi.Windows, Vcl.Forms;

procedure Register;
begin
   RegisterComponents('Standard', [TSmartComboBox]);
end;

constructor TSmartComboBox.Create(AOwner: TComponent);
begin
   inherited;
   FStoredItems := TStringList.Create;
   FStoredItems.OnChange := StoredItemsChange;
end;

destructor TSmartComboBox.Destroy;
begin
   FStoredItems.Free;
   inherited;
end;

procedure TSmartComboBox.DoExit;
begin
   inherited;
   RedefineCombo;
end;

procedure TSmartComboBox.Loaded;
var LParent: TWinControl;
    LPoint: TPoint;
begin
   inherited;
   if Items.Count > 0 then
      FStoredItems.Assign(Items);
   AutoComplete := False;
   Style := csDropDownList;

   // The ComboBox doesn't behave properly if the parent is not the form.
   // Workaround to pull it from any parenting
   //if not (Parent is TForm) then
   //begin
   //   LParent := Parent;
   //   while (not (LParent is TForm)) and Assigned(LParent) do
   //      LParent := LParent.Parent;
   //   LPoint := ClientToParent(Point(0,0), LParent);
   //   Parent := LParent;
   //   Left   := LPoint.X;
   //   Top    := LPoint.Y;
   //   BringToFront;
   //end;
end;

procedure TSmartComboBox.RedefineCombo;
var S: String;
begin
   if Style = csDropDown then
   begin
      if ItemIndex <> -1 then
         S := Items[ItemIndex];

      Style := csDropDownList;
      Items.Assign(FStoredItems);

      if S <> '' then
         ItemIndex := Items.IndexOf(S);
   end;
end;

procedure TSmartComboBox.SetStoredItems(const Value: TStringList);
begin
   if Assigned(FStoredItems) then
      FStoredItems.Assign(Value)
   else
      FStoredItems := Value;
end;

procedure TSmartComboBox.StoredItemsChange(Sender: TObject);
begin
   if Assigned(FStoredItems) then
   begin
      RedefineCombo;
      Items.Assign(FStoredItems);
   end;
end;

procedure TSmartComboBox.KeyPress(var Key: Char);
begin
   if CharInSet(Key, ['a'..'z']) and not (Style = csDropDown) then
   begin
      DroppedDown := False;
      Style := csDropDown;
   end;
   inherited;
   if not (Ord(Key) in [13,27]) then
      DroppedDown := True;
end;

procedure TSmartComboBox.CloseUp;
begin
   if Style = csDropDown then
      RedefineCombo;
   inherited;
end;

procedure TSmartComboBox.CNCommand(var AMessage: TWMCommand);
begin
   inherited;
   if (AMessage.Ctl = Handle) and (AMessage.NotifyCode = CBN_EDITUPDATE) then
      FilterItems;
end;

procedure TSmartComboBox.FilterItems;
var I: Integer;
    Selection: TSelection;
begin
   SendMessage(Handle, CB_GETEDITSEL, WPARAM(@Selection.StartPos), LPARAM(@Selection.EndPos));

   Items.BeginUpdate;
   Try
      if Text <> '' then
      begin
         Items.Clear;
         for I := 0 to FStoredItems.Count - 1 do
            if (Pos(Uppercase(Text), Uppercase(FStoredItems[I])) > 0) then
               Items.Add(FStoredItems[I]);
      end
      else
         Items.Assign(FStoredItems);
   Finally
      Items.EndUpdate;
   End;

   SendMessage(Handle, CB_SETEDITSEL, 0, MakeLParam(Selection.StartPos, Selection.EndPos));
end;

end.

Any help in how I can proceed to figure out why this is happening would be greatly appreciated!

Edit 1:

After doing some extra debugging, I've noticed the messages being sent to the ComboBox differ from the ones inside the panel. A CBN_EDITUPDATE is never sent, like @Sherlock70 mentioned in the comments, which makes the FilterItems procedure never trigger.

I've also noticed the form behaves strangely after using the ComboBox inside the panel, sometimes freezing and even not responding, like it gets stuck in a loop.
This unpredictable behavior has made me move away from this approach, and I'm probably going to take an alternate route to create a "searchable ComboBox".
Going to leave the question open if someone wants to figure it out and maybe even use the component.

Bruno Kinast
  • 1,068
  • 4
  • 17
  • Why does the "direct" version's list afterwards still have 3 items and not only 2? Where does the empty item come from? – AmigoJack May 16 '22 at 14:19
  • See also: [https://stackoverflow.com/questions/44070905/tcombobox-how-to-adjust-drop-down-list-height-while-it-is-dropped-down](https://stackoverflow.com/questions/44070905/tcombobox-how-to-adjust-drop-down-list-height-while-it-is-dropped-down) – USauter May 16 '22 at 14:23
  • @AmigoJack It's just the drop down list height not being calculated right, it only has 2 items. – Bruno Kinast May 16 '22 at 14:34
  • I've tried @USauter suggestion to fix the height but it doesn't work for me. The call to `CB_SETMINVISIBLE` is there (using XE2), but it never changes in height. I have Runtime Themes disabled so maybe that has something to do with it. – Bruno Kinast May 16 '22 at 14:36
  • 1
    Have you tried debugging? Set a breakpoint in your `FilterItems` routine and check when it's called and how it behaves for both the wanted (on `TForm`) and unwanted (on `TPanel`) behaviour. If you an't determine why these are different then post the additional information you have found. – Rob Lambden May 17 '22 at 07:20
  • 1
    Something with those messages and handles is not right. My first try was to make sure that messages where really sent and read by the ComboBox. But that did not change anything. I noticed that the EDITUPDATE command is never received by the parented combo, so my guess is the Panel is interfering. – Sherlock70 May 19 '22 at 12:37

1 Answers1

0

I hope this will help someone in future even after 7 months of the question. Setting the style of a Combobox will destroy the window handle of that Combobox and create a new one. This means windows will free your control's Window Handle and create a new one.
You are setting your Combobx style while searching and this is wrong. Try removing Style := from your code and test it again you will get the same results for Combobox on a form and Combobox on a panel or other TWinControl.
As you can see in the following code, setting Style will call RecreateWnd.

procedure TCustomComboBox.SetStyle(Value: TComboBoxStyle);
begin
  if FStyle <> Value then
  begin
    FStyle := Value;
    if Value = csSimple then
      ControlStyle := ControlStyle - [csFixedHeight] else
      ControlStyle := ControlStyle + [csFixedHeight];
    RecreateWnd;
  end;
end;

RecreateWnd will call DestroyHandle()

procedure TWinControl.CMRecreateWnd(var Message: TMessage);
var
  WasFocused: Boolean;
begin
  WasFocused := Focused;
  DestroyHandle;
  UpdateControlState;
  if WasFocused and (FHandle <> 0) then Windows.SetFocus(FHandle);
end;

Then DestroyHandle will call DestroyWnd() which will call DestroyWindowHandle().

Ehab
  • 284
  • 1
  • 9