Jun 222019
 

By default, the width of the drop down list of a TComboBox is the same as the width of the control itself, and even in the latest Delphi version there apparently is no property to set it.

Why is that so? Good question. There are probably many third party controls that offer this because it is rather simple to implement. But on the other hand, if it is that simple, why isn’t it a feature of the default control? It can really be a pain in the lower back that some entries are just not displayed correctly as seen in the picture above.

Setting the drop down width is as simple as sending the CB_SETDROPPEDWIDTH message to the control’s handle:

SendMessage(TheCombobox.Handle, CB_SETDROPPEDWIDTH, MinimumWidthInPixels, 0);

It does not allow to shrink the width of the drop down list though, because it sets the minimum width, not the actual width. There is this answer on StackOverflow for that particular problem. The result isn’t very visually appealing though, because the list is left aligned rather than right.

Of course that can easily be fixed by adding the width difference to all coordinates passed to MoveWindow.

But the normal use case is probably that the list is not wide enough, not that it’s too wide.

While they help, these solutions still leave something to wish for: Usually you don’t want to set a fixed width in pixels, you want to make the drop down list wide enough so it fits the entries. So ideally you will take the list of entries, calculate their text width, optionally add the width of the scroll bar and set that as the width for the drop down list.

Of course, I am not the first one who want to do that. I found an ancient article by Zarko Gajic on ThoughtCo (formerly known as about.com) that deals with this issue. The result looks like this:

The article even goes a lot further. When do you call that code? Of course only after you added the items to the list. So, why not put it into the OnDropdown event of the ComboBox? (Because that clutters the sources and you also have to create an event handler for each of these ComboBoxes on your form, that’s why.). It also handles the case where the ComboBox is placed near the right border of the form and claims that it gets truncated there. I could not reproduce that problem. This looks fine to me:

So, why am I blogging about this? As said above, there are plenty of other articles on the web that handle these issues.

Last week I investigated several cases where the SendMessage code did not work at all. It turned out that somewhere between the SendMessage call in the form’s constructor and the time the user clicked the drop down button the window handle of the ComboBox got recreated, losing the changed drop down width. A solution for this would have been to put the code into the OnDropDown event. That would have required 4 event handlers, one for each of the ComboBoxes on that particular form, (or a shared event handler for all 4). I don’t like that, I prefer a solution that once and for all solves that problem by calling some function in the constructor and then forget about. So I went and wrote one:

// taken from:
// https://www.thoughtco.com/sizing-the-combobox-drop-down-width-1058301
// (previously about.com)
// by Zarko Gajic

procedure TComboBox_AutoWidth(_cmb: TCustomComboBox);
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 + _cmb.Items.Count do begin
    itemWidth := _cmb.Canvas.TextWidth(_cmb.Items[Idx]);
    Inc(itemWidth, 2 * HORIZONTAL_PADDING);
    if (itemWidth > itemsFullWidth) then
      itemsFullWidth := itemWidth;
  end;
  // set the width of drop down if needed
  if (itemsFullWidth > _cmb.Width) then begin
    //check if there would be a scroll bar
    if TComboBoxHack(_cmb).DropDownCount < _cmb.Items.Count then
      itemsFullWidth := itemsFullWidth + GetSystemMetrics(SM_CXVSCROLL);
    SendMessage(_cmb.Handle, CB_SETDROPPEDWIDTH, itemsFullWidth, 0);
  end;
end;

type
  TComboAutoWidthActivator = class(TWindowProcHook)
  protected
    procedure NewWindowProc(var _Msg: TMessage); override;
  public
    constructor Create(_cmb: TCustomComboBox);
  end;

{ TComboAutoWidthActivator }

constructor TComboAutoWidthActivator.Create(_cmb: TCustomComboBox);
begin
  inherited Create(_cmb);
end;

procedure TComboAutoWidthActivator.NewWindowProc(var _Msg: TMessage);
begin
  if _Msg.Msg = CBN_DROPDOWN then
    TComboBox_AutoWidth(TCustomComboBox(FCtrl));
  inherited;
end;

function TComboBox_ActivateAutoWidth(_cmb: TCustomComboBox): TObject;
begin
  Result := TComboAutoWidthActivator.Create(_cmb);
end;

The TCombobox_AutoWidth procedure is taken from Zarko Gajic’s article, but adapted to take a TCustomComboBox parameter rather than TComboBox (And while I am writing this I have got a few ideas what to improve on it.) The TComboAutoWidthActivator class is based on the TWindowProcHook class I used earlier. It simply hooks the control’s WindowProc, checks for the CBN_DROPDOWN message (which will later cause the OnDropDown event to be called) and calls TCombobox_AutoWidth.

So, my form’s contructor now looks like this:

constructor TMyForm.Create(_Owner: TComponent);
begin
  inherited;

  TCombobox_ActiveAutoWidth(cmb_One);
  TCombobox_ActiveAutoWidth(cmb_Two);
  TCombobox_ActiveAutoWidth(cmb_Three);
  TCombobox_ActiveAutoWidth(cmb_Four);
end;

Much cleaner than the solution with all those OnDropDown handlers.

The code above is part of u_dzVclUtils of my dzlib.

One possible improvement would be to have yet another function to enumerate over all controls on a form, find all ComboBoxes and call for them. I’m Not sure it’s worth the trouble, but it wouldn’t be rocket science either.

If you would like to comment on this article, go to this post in the international Delphi Praxis forum.

 Posted by on 2019-06-22 at 12:21