Sizing the ComboBox Drop Down Width

Ensures Drop-Down List Is Visible When Drop-Down List Is Displayed

Programming language
ermingut/Getty Images

The TComboBox component combines an edit box with a scrollable "pick" list. Users can select an item from the list or type directly into the edit box.

Drop Down List

When a combo box is in dropped down state Windows draws a list box type of control to display combo box items for selection.

The DropDownCount property specifies the maximum number of items displayed in the drop-down list.

The width of the drop-down list would, by default, equal the width of the combo box.

When the length (of a string) of items exceeds the width of the combobox, the items are displayed as cut-off!

TComboBox does not provide a way to set the width of its drop-down list :(

Fixing The ComboBox Drop-Down List Width

We can set the width of the drop-down list by sending a special Windows message to the combo box. The message is CB_SETDROPPEDWIDTH and sends the minimum allowable width, in pixels, of the list box of a combo box.

To hardcode the size of the drop-down list to, let's say, 200 pixels, you could do:


SendMessage(theComboBox.Handle, CB_SETDROPPEDWIDTH, 200, 0);

This is only ok if you are sure all your theComboBox.Items are not longer than 200 px (when drawn).

To ensure we always have the drop-down list display enough wide, we can calculate the required width.

Here's a function to get the required width of the drop-down list and set it:


procedure ComboBox_AutoWidth(const theComboBox: TCombobox);
const
HORIZONTAL_PADDING = 4;
var
itemsFullWidth: integer;
idx: integer;
itemWidth: integer;
begin
itemsFullWidth := 0;
// get the max needed with of the items in dropdown state
for idx := 0 to -1 + theComboBox.Items.Count do
begin
itemWidth := theComboBox.Canvas.TextWidth(theComboBox.Items[idx]);
Inc(itemWidth, 2 * HORIZONTAL_PADDING);
if (itemWidth > itemsFullWidth) then itemsFullWidth := itemWidth;
end;
// set the width of drop down if needed
if (itemsFullWidth > theComboBox.Width) then
begin
//check if there would be a scroll bar
if theComboBox.DropDownCount < theComboBox.Items.Count then
itemsFullWidth := itemsFullWidth + GetSystemMetrics(SM_CXVSCROLL);
SendMessage(theComboBox.Handle, CB_SETDROPPEDWIDTH, itemsFullWidth, 0);
end;
end;

The width of the longest string is used for the width of the drop-down list.

When to call ComboBox_AutoWidth?
If you pre-fill the list of items (at design time or when creating the form) you can call the ComboBox_AutoWidth procedure inside the form's OnCreate event handler.

If you dynamically change the list of combo box items, you can call the ComboBox_AutoWidth procedure inside the OnDropDown event handler - occurs when the user opens the drop-down list.

A Test
For a test, we have 3 combo boxes on a form. All have items with their text more wide than the actual combo box width. The third combo box is placed near the right edge of the form's border.

The Items property, for this example, is pre-filled - we call our ComboBox_AutoWidth in the OnCreate event handler for the form:


//Form's OnCreate
procedure TForm.FormCreate(Sender: TObject);
begin
ComboBox_AutoWidth(ComboBox2);
ComboBox_AutoWidth(ComboBox3);
end;

We've not called ComboBox_AutoWidth for Combobox1 to see the difference!

Note that, when run, the drop-down list for Combobox2 will be wider than Combobox2.

The Entire Drop-Down List Is Cut Off For "Near Right Edge Placement"

For Combobox3, the one placed near the right edge, the drop-down list is cut off.

Sending the CB_SETDROPPEDWIDTH will always extend the drop-down list box to the right. When your combobox is near the right edge, extending the list box more to the right would result in the display of the list box being cut off.

We need to somehow extend the list box to the left when this is the case, not to the right!

The CB_SETDROPPEDWIDTH has no way of specifying to what direction (left or right) to extend the list box.

Solution: WM_CTLCOLORLISTBOX

Just when the drop-down list is to be displayed Windows sends the WM_CTLCOLORLISTBOX message to the parent window of a list box - to our combo box.

Being able to handle the WM_CTLCOLORLISTBOX for the near-right-edge combobox would solve the problem.

The Almighty WindowProc
Each VCL control exposes the WindowProc property - the procedure that responds to messages sent to the control. We can use the WindowProc property to temporarily replace or subclass the window procedure of the control.

Here's our modified WindowProc for Combobox3 (the one near the right edge):


//modified ComboBox3 WindowProc
procedure TForm.ComboBox3WindowProc(var Message: TMessage);
var
cr, lbr: TRect;
begin
//drawing the list box with combobox items
if Message.Msg = WM_CTLCOLORLISTBOX then
begin
GetWindowRect(ComboBox3.Handle, cr);
//list box rectangle
GetWindowRect(Message.LParam, lbr);
//move it to left to match right border
if cr.Right <> lbr.Right then
MoveWindow(Message.LParam,
lbr.Left-(lbr.Right-clbr.Right),
lbr.Top,
lbr.Right-lbr.Left,
lbr.Bottom-lbr.Top,
True);
end
else
ComboBox3WindowProcORIGINAL(Message);
end;

If the message our combo box receives is WM_CTLCOLORLISTBOX we get its window's rectangle, we also get the rectangle of the list box to be displayed (GetWindowRect). If it appears that the list box would appear more to the right - we move it to the left so that combo box and list box right border is the same. As easy as that :)

If the message is not WM_CTLCOLORLISTBOX we simply call the original message handling procedure for the combo box (ComboBox3WindowProcORIGINAL).

Finally, all this can work if we have set it correctly (in the OnCreate event handler for the form):


//Form's OnCreate
procedure TForm.FormCreate(Sender: TObject);
begin
ComboBox_AutoWidth(ComboBox2);
ComboBox_AutoWidth(ComboBox3);
//attach modified/custom WindowProc for ComboBox3
ComboBox3WindowProcORIGINAL := ComboBox3.WindowProc;
ComboBox3.WindowProc := ComboBox3WindowProc;
end;

Where in the form's declaration we have (entire):


type
TForm = class(TForm)
ComboBox1: TComboBox;
ComboBox2: TComboBox;
ComboBox3: TComboBox;
procedure FormCreate(Sender: TObject);
private
ComboBox3WindowProcORIGINAL : TWndMethod;
procedure ComboBox3WindowProc(var Message: TMessage);
public
{ Public declarations }
end;

And that's it. All handled :)