Delayed Close Programmatically Dropped TComboBox (If Mouse NOT Over Combo and Combo’s List) Using Multi Threading

The Combo Box Windows control (aka TComboBox in Delphi) is one of the most frequently used user interface elements along with buttons and edits in Windows applications. TComboBox control represents an edit box with a scrollable drop-down list attached to it. Users can select an item from the list or type directly into the edit box. When the Style property is set to csDropDownList the combo allows to display a list of predefined items a user can select from the combo’s drop down list.

I’ve had a request from a user of my application: can the selection list be dropped down and then closed if no selection has been made in some period of time? Well, the user is asking. So, certainly it can!

The task to drop combo’s list is super easy: just set DroppedDown property to true (this sends CB_SHOWDROPDOWN to the combo). Then wait – but do not block the main thread !! – so some multithreading needed. Ok, use Delphi’s PPL. Finally, once the wait period is over, close the combo’s list – but, let’s say, only if the user is not hovering the mouse over combo’s items!

Looks pretty straightforward.

The only part to figure out is how to know if the mouse is over the list. Luckily, there’s a super handy Windows API call: GetComboBoxInfo. This function fills in the tagCOMBOBOXINFO structure (TComboBoxInfo record in Delphi’s implementation) with handles to the combo’s list box, edit box and the combo box itself. Therefore: get the mouse coordinates, and if over combo’s list (or combo itself) – simply leave it open.

Threaded Approach to Dropping Down and Closing Combo’s List

And here’s the resulting procedure: ComboDropAndCloseDelayed. Feed it with a TComboBox instance and set the needed delay time. Combo’s list will show, time will pass, and if the user is not over the combo’s list with the mouse (so still deciding of the selection) – close the list.

EDIT: Warning: the wanted quick and dirty approach is not the way to go. I “forgot” that TWinControl.Handle is not thread safe. Please read Remy’s comment….

//how not to do it :)
procedure ComboDropAndCloseDelayed(const theCombo : TComboBox; const closeDelayMS : Cardinal = 2500);
begin
  TTask.Run(
    procedure
    var
      cbi : TComboBoxInfo;
    begin
      //No-no! TWinControl.Handle is *not* thread safe! 
      cbi.cbSize := SizeOf(TComboBoxInfo);
      if NOT GetComboBoxInfo(theCombo.Handle, cbi) then Exit;

      TThread.Queue(nil,
        procedure
        begin
          theCombo.DroppedDown := true;
        end);

      Sleep(closeDelayMS);

      TThread.Queue(nil,
        procedure
        var
          undrop : boolean;
          hw : THandle;
        begin
          undrop := true;

          if theCombo.HandleAllocated AND theCombo.DroppedDown then
          begin
            hw := WindowFromPoint(Mouse.CursorPos);

            undrop := (hw <> 0) AND (hw <> cbi.hwndCombo) AND (hw <> cbi.hwndList) AND (hw <> cbi.hwndItem);

            if undrop then
              theCombo.DroppedDown := false;
          end;
        end);
    end);
end;

And how to use (example):

procedure TMainForm.Button1Click(Sender: TObject);
begin
  //if no selection already: drop the list
  if ComboBox1.ItemIndex = -1 then
    ComboDropAndCloseDelayed(ComboBox1);
end;

That’s it. Nice and quick and neat …

2 thoughts on “Delayed Close Programmatically Dropped TComboBox (If Mouse NOT Over Combo and Combo’s List) Using Multi Threading

  1. Remy Lebeau

    I would NOT suggest using a worker thread for this. A timer, or even TThread.ForceQueue() with its ADelay parameter, would be more appropriate. The TWinControl.Handle property is NOT thread-safe! You can really mess up your UI by reading that property in a worker thread, if the main UI thread happens to perform a HWND recreation at the exact same moment (which can and does happen from time to time).

    I would suggest something more like this instead (10.4.x and later):

    procedure CloseIfNotHovering(const theCombo : TComboBox; const closeDelayMS : Cardinal = 2500);
    begin
    TThread.ForceQueue(nil,
    procedure
    var
    cbi : TComboBoxInfo;
    hw : HWND;
    begin
    if not theCombo.DroppedDown then Exit;
    cbi.cbSize := SizeOf(TComboBoxInfo);
    if not GetComboBoxInfo(theCombo.Handle, cbi) then Exit;
    hw := WindowFromPoint(Mouse.CursorPos);
    if (hw cbi.hwndCombo) and (hw cbi.hwndList) and (hw cbi.hwndItem) then
    theCombo.DroppedDown := False
    else
    CloseIfNotHovering(theCombo, closeDelayMS);
    end,
    closeDelayMS
    );
    end;

    procedure ComboDropAndCloseDelayed(const theCombo : TComboBox; const closeDelayMS : Cardinal = 2500);
    begin
    theCombo.DroppedDown := True;
    CloseIfNotHovering(theCombo, closeDelayMS);
    // TODO: subclass the TComboBox’s Parent to cancel the queued close procedure if CBN_CLOSEUP is received…
    end;

    Alternatively (if using an earlier version of Delphi):

    procedure ComboCloseTimerProc(Wnd: HWND; uMsg: UINT; uIDTimer: UINT; dwTime: DWORD); stdcall;
    var
    cbi : TComboBoxInfo;
    hw : HWND;
    begin
    if SendMessage(Wnd, CB_GETDROPPEDSTATE, 0, 0) = 1 then
    begin
    cbi.cbSize := SizeOf(TComboBoxInfo);
    if GetComboBoxInfo(Wnd, cbi) then
    begin
    hw := WindowFromPoint(Mouse.CursorPos);
    if (hw = cbi.hwndCombo) or (hw = cbi.hwndList) or (hw = cbi.hwndItem) then Exit;
    end;
    KillTimer(Wnd, uIDTimer);
    SendMessage(Wnd, CB_SHOWDROPDOWN, 0, 0);
    end;

    function ComboSubclassProc(Wnd: HWND; uMsg: UINT; wParam: WPARAM; lParam: LPARAM; uIdSubclass: UINT_PTR; dwRefData: DWORD_PTR): LRESULT; stdcall;
    begin
    case uMsg of
    WM_NCDESTROY: begin
    RemoveWindowSubclass(Wnd, @ComboSubclassProc, uIdSubclass);
    end;
    WM_COMMAND: begin
    if (HIWORD(wParam) = CB_CLOSEUP) and (HWND(lParam) = HWND(dwRefData)) then
    begin
    KillTimer(HWND(lParam), 12345);
    RemoveWindowSubclass(Wnd, @ComboSubclassProc, uIdSubclass);
    end;
    end;
    end;
    Result := DefSubclassProc(Wnd, uMsg, wParam, lParam);
    end;

    procedure ComboDropAndCloseDelayed(const theCombo : TComboBox; const closeDelayMS : Cardinal = 2500);
    var
    hw: HWND;
    begin
    hw := theCombo.Handle;
    theCombo.DroppedDown := True;
    SetTimer(hw, 12345, closeDelayMS, @ComboCloseTimerProc);
    SetWindowSubclass(theCombo.Parent.Handle, @ComboSubclassProc, 1, DWORD_PTR(hw));
    end;

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.