ComboBox doesn't behave the same inside panel

331 views Asked by At

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.

1

There are 1 answers

0
Ehab On

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().