Delphi’s TComboBox combines an edit box with a scrollable list – with the Style property defining the display style of the combo. When set to csDropDownList there’s no edit box – users can only pick from the predefined items displayed by the list. If you need to react, when the selection changes, you will write code for the OnSelect event.
Now, presume our combo is a kind of a filter selector – when the selection changes you need to update some other UI control. For example, combo lists invoices and selecting one (item from its list) displays the selected invoice items in some grid control.
Depending on the number of invoice items, where they are stored, how you fetch them, and so on – the actual code to (fetch items and) refresh the UI might take longer time – whatever longer means. If you scroll quickly through the combo items – OnSelect will get fired for each visited item in the list and your application might appear irresponsible, lagging and slow.
ChangeDelay / SelectDelay to the Rescue
I actually do have a similar “problem“ in my application. Using a combo to filter (hide or unhide specific nodes of) a TreeView displaying a huge XML file. Since the XML is heavy and the actual filtering code can take time to get executed – I needed a way to postpone the execution of the OnSelect event handler – at least until the user “has made up his/her mind” – and finished with the selection.
Sadly, the TComboBox does not have a property similar to ChangeDelay of TTreView’s. ChangeDelay specifies the delay between when a node is selected and when the tree’s OnChange event occur – and that’s precisely what I need for my Combo.
So, here’s how to implement SelectDelay for a TComboBox – to postpone OnSelect event handler execution
Firstly, place a TComboBox, named “ComboBox1”, on a form and set it up in form’s OnCreate event handler:
procedure TForm1.FormCreate(Sender: TObject); var i : integer; begin ComboBox1.Style := csDropDownList; for i := 1 to 30 do ComboBox1.Items.Add('Item ' + i.ToString); ComboBox1.ItemIndex := 0; end;
Next, handle Combo’s OnSelect event, to display the selected item in a TMemo (“Memo1”):
procedure TForm1.ComboBox1Select(Sender: TObject); begin Memo1.Lines.Insert(0, 'Selected: ' + ComboBox1.Items[ComboBox1.ItemIndex]); end;
Now, run the application, drop down the combo list and scroll through it with the keyboard (up / down keys) – you will see that the memo lists all items you “visited”.
What I actually need is to postpone the execution of the OnSelect handler code until the selection is “finished” – or at least until some time passes before the new selection.
Interpose and enrich with a timer
For this I’ll have a TComboBox interposer class to enrich the standard TComboBox with some additional properties and functionality.
Here’s how the interface looks:
TComboBox = class (Vcl.StdCtrls.TComboBox) strict private const DroppedDownSelectTimer = 1; strict private fDroppedDownSelectDelay: Cardinal; procedure WMTimer(var Message: TWMTimer); message WM_TIMER; public property DroppedDownSelectDelay : Cardinal read fDroppedDownSelectDelay write fDroppedDownSelectDelay default 0; protected procedure Select; override; end;
The trick is in overriding the Select protected method – the one responsible for raising the OnSelect (and OnChange) event.
My implementation looks like this:
procedure TComboBox.Select; begin if (DroppedDownSelectDelay > 0) AND DroppedDown then SetTimer(Handle, DroppedDownSelectTimer, DroppedDownSelectDelay, nil) else begin KillTimer(Handle, DroppedDownSelectTimer); inherited Select; end; end;
So, if the combo is in dropped down state (that’s purely when I needed it to postpone) and if DroppedDownSelectDelay is specified (that is not 0 milliseconds), we call the SetTimer Windows API.
The SetTimer will send the WM_TIMER message to our combo, after DroppedDownSelectDelay have passed, which we handle:
procedure TComboBox.WMTimer(var Message: TWMTimer); begin if Message.TimerID = DroppedDownSelectTimer then begin KillTimer(Handle, DroppedDownSelectTimer); inherited Select; end; end;
Calling SetTimer multiple times (before it fires) will just replace the existing timer with a new one. Once this happens (WM_Timer message) we call the original Select method (so OnSelect gets really raised). The KillTimer ensures our timer will fire only once.
Here’s the full source code to play with.
And that’s all folks, simple and elegant.