Sizing The ComboBox Drop Down Width - No Cut Off For Right Edge Placements

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

Delphi ComboBox Drop-Down List Width
Delphi ComboBox Drop-Down List Width. Delphi ComboBox Drop-Down List Width

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 hard core 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, I 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 - I call my 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;

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

Note that, when run, the drop down list for Combobox2 will be more wide 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 my near-right-edge combobox would solve the problem.

The All Might 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 :)