.

Sunday, October 20, 2019

Sizing the ComboBox Drop Down Width

Sizing the ComboBox Drop Down 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 hardcode the size of the drop-down list to, lets 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. Heres 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 forms 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 TestFor 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 forms border. The Items property, for this example, is pre-filled - we call our ComboBox_AutoWidth in the OnCreate event handler for the form: //Forms OnCreate procedure TForm.FormCreate(Sender: TObject); begin ComboBox_AutoWidth(ComboBox2); ComboBox_AutoWidth(ComboBox3); end; Weve 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 WindowProcEach 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. Heres 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 windows 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): //Forms 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 forms 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 thats it. All handled :)

No comments:

Post a Comment