1 // taken from a control written by Nishant Sivakumar. 2 // http://www.codeproject.com/cs/combobox/DotNetMultiColumnComboBox.asp 3 // http://www.51aspx.com/CodeFile/FengfanSell/Market/MultiColumnComboBox.cs.html 4 // Bugfixes or Suggestions can be sent to dcaillouet@littlerock.org 5 6 7 using System; 8 using System.Windows.Forms; 9 using System.Collections; 10 using System.Collections.ObjectModel; 11 using System.ComponentModel; 12 using System.Drawing; 13 using System.Globalization; 14 15 namespace WindowsFormsApplication1 16 { 17 public class MultiColumnComboBox : ComboBox 18 { 19 private bool _AutoComplete; 20 private bool _AutoDropdown; 21 private Color _BackColorEven = Color.White; 22 private Color _BackColorOdd = Color.White; 23 private string _ColumnNameString = ""; 24 private int _ColumnWidthDefault = 75; 25 private string _ColumnWidthString = ""; 26 private int _LinkedColumnIndex; 27 private TextBox _LinkedTextBox; 28 private int _TotalWidth = 0; 29 private int _ValueMemberColumnIndex = 0; 30 31 private Collection<string> _ColumnNames = new Collection<string>(); 32 private Collection<int> _ColumnWidths = new Collection<int>(); 33 34 public MultiColumnComboBox() 35 { 36 DrawMode = DrawMode.OwnerDrawVariable; 37 38 // If all of your boxes will be RightToLeft, uncomment 39 // the following line to make RTL the default. 40 //RightToLeft = RightToLeft.Yes; 41 42 // Remove the Context Menu to disable pasting 43 ContextMenu = new ContextMenu(); 44 } 45 46 public event System.EventHandler OpenSearchForm; 47 48 public bool AutoComplete 49 { 50 get 51 { 52 return _AutoComplete; 53 } 54 set 55 { 56 _AutoComplete = value; 57 } 58 } 59 60 public bool AutoDropdown 61 { 62 get 63 { 64 return _AutoDropdown; 65 } 66 set 67 { 68 _AutoDropdown = value; 69 } 70 } 71 72 public Color BackColorEven 73 { 74 get 75 { 76 return _BackColorEven; 77 } 78 set 79 { 80 _BackColorEven = value; 81 } 82 } 83 84 public Color BackColorOdd 85 { 86 get 87 { 88 return _BackColorOdd; 89 } 90 set 91 { 92 _BackColorOdd = value; 93 } 94 } 95 96 public Collection<string> ColumnNameCollection 97 { 98 get 99 { 100 return _ColumnNames; 101 } 102 } 103 104 public string ColumnNames 105 { 106 get 107 { 108 return _ColumnNameString; 109 } 110 111 set 112 { 113 // If the column string is blank, leave it blank. 114 // The default width will be used for all columns. 115 if (!Convert.ToBoolean(value.Trim().Length)) 116 { 117 _ColumnNameString = ""; 118 } 119 else if (value != null) 120 { 121 char[] delimiterChars = { ',', ';', ':' }; 122 string[] columnNames = value.Split(delimiterChars); 123 124 if (!DesignMode) 125 { 126 _ColumnNames.Clear(); 127 } 128 129 // After splitting the string into an array, iterate 130 // through the strings and check that they're all valid. 131 foreach (string s in columnNames) 132 { 133 // Does it have length? 134 if (Convert.ToBoolean(s.Trim().Length)) 135 { 136 if (!DesignMode) 137 { 138 _ColumnNames.Add(s.Trim()); 139 } 140 } 141 else // The value is blank 142 { 143 throw new NotSupportedException("Column names can not be blank."); 144 } 145 } 146 _ColumnNameString = value; 147 } 148 } 149 } 150 151 public Collection<int> ColumnWidthCollection 152 { 153 get 154 { 155 return _ColumnWidths; 156 } 157 } 158 159 public int ColumnWidthDefault 160 { 161 get 162 { 163 return _ColumnWidthDefault; 164 } 165 set 166 { 167 _ColumnWidthDefault = value; 168 } 169 } 170 171 public string ColumnWidths 172 { 173 get 174 { 175 return _ColumnWidthString; 176 } 177 178 set 179 { 180 // If the column string is blank, leave it blank. 181 // The default width will be used for all columns. 182 if (!Convert.ToBoolean(value.Trim().Length)) 183 { 184 _ColumnWidthString = ""; 185 } 186 else if (value != null) 187 { 188 char[] delimiterChars = { ',', ';', ':' }; 189 string[] columnWidths = value.Split(delimiterChars); 190 string invalidValue = ""; 191 int invalidIndex = -1; 192 int idx = 1; 193 int intValue; 194 195 // After splitting the string into an array, iterate 196 // through the strings and check that they're all integers 197 // or blanks 198 foreach (string s in columnWidths) 199 { 200 // If it has length, test if it's an integer 201 if (Convert.ToBoolean(s.Trim().Length)) 202 { 203 // It's not an integer. Flag the offending value. 204 if (!int.TryParse(s, out intValue)) 205 { 206 invalidIndex = idx; 207 invalidValue = s; 208 } 209 else // The value was okay. Increment the item index. 210 { 211 idx++; 212 } 213 } 214 else // The value is a space. Use the default width. 215 { 216 idx++; 217 } 218 } 219 220 // If an invalid value was found, raise an exception. 221 if (invalidIndex > -1) 222 { 223 string errMsg; 224 225 errMsg = "Invalid column width '" + invalidValue + "' located at column " + invalidIndex.ToString(); 226 throw new ArgumentOutOfRangeException(errMsg); 227 } 228 else // The string is fine 229 { 230 _ColumnWidthString = value; 231 232 // Only set the values of the collections at runtime. 233 // Setting them at design time doesn't accomplish 234 // anything and causes errors since the collections 235 // don't exist at design time. 236 if (!DesignMode) 237 { 238 _ColumnWidths.Clear(); 239 foreach (string s in columnWidths) 240 { 241 // Initialize a column width to an integer 242 if (Convert.ToBoolean(s.Trim().Length)) 243 { 244 _ColumnWidths.Add(Convert.ToInt32(s)); 245 } 246 else // Initialize the column to the default 247 { 248 _ColumnWidths.Add(_ColumnWidthDefault); 249 } 250 } 251 252 // If the column is bound to data, set the column widths 253 // for any columns that aren't explicitly set by the 254 // string value entered by the programmer 255 if (DataManager != null) 256 { 257 InitializeColumns(); 258 } 259 } 260 } 261 } 262 } 263 } 264 265 public new DrawMode DrawMode 266 { 267 get 268 { 269 return base.DrawMode; 270 } 271 set 272 { 273 if (value != DrawMode.OwnerDrawVariable) 274 { 275 throw new NotSupportedException("Needs to be DrawMode.OwnerDrawVariable"); 276 } 277 base.DrawMode = value; 278 } 279 } 280 281 public new ComboBoxStyle DropDownStyle 282 { 283 get 284 { 285 return base.DropDownStyle; 286 } 287 set 288 { 289 if (value != ComboBoxStyle.DropDown) 290 { 291 throw new NotSupportedException("ComboBoxStyle.DropDown is the only supported style"); 292 } 293 base.DropDownStyle = value; 294 } 295 } 296 297 public int LinkedColumnIndex 298 { 299 get 300 { 301 return _LinkedColumnIndex; 302 } 303 set 304 { 305 if (value < 0) 306 { 307 throw new ArgumentOutOfRangeException("A column index can not be negative"); 308 } 309 _LinkedColumnIndex = value; 310 } 311 } 312 313 public TextBox LinkedTextBox 314 { 315 get 316 { 317 return _LinkedTextBox; 318 } 319 set 320 { 321 _LinkedTextBox = value; 322 323 if (_LinkedTextBox != null) 324 { 325 // Set any default properties of the Linked Textbox here 326 _LinkedTextBox.ReadOnly = true; 327 _LinkedTextBox.TabStop = false; 328 } 329 } 330 } 331 332 public int TotalWidth 333 { 334 get 335 { 336 return _TotalWidth; 337 } 338 } 339 340 protected override void OnDataSourceChanged(EventArgs e) 341 { 342 base.OnDataSourceChanged(e); 343 344 InitializeColumns(); 345 } 346 347 protected override void OnDrawItem(DrawItemEventArgs e) 348 { 349 base.OnDrawItem(e); 350 351 if (DesignMode) 352 return; 353 354 e.DrawBackground(); 355 356 Rectangle boundsRect = e.Bounds; 357 int lastRight = 0; 358 359 Color brushForeColor; 360 if ((e.State & DrawItemState.Selected) == 0) 361 { 362 // Item is not selected. Use BackColorOdd & BackColorEven 363 Color backColor; 364 backColor = Convert.ToBoolean(e.Index % 2) ? _BackColorOdd : _BackColorEven; 365 using (SolidBrush brushBackColor = new SolidBrush(backColor)) 366 { 367 e.Graphics.FillRectangle(brushBackColor, e.Bounds); 368 } 369 brushForeColor = Color.Black; 370 } 371 else 372 { 373 // Item is selected. Use ForeColor = White 374 brushForeColor = Color.White; 375 } 376 377 using (Pen linePen = new Pen(SystemColors.GrayText)) 378 { 379 using (SolidBrush brush = new SolidBrush(brushForeColor)) 380 { 381 if (!Convert.ToBoolean(_ColumnNames.Count)) 382 { 383 e.Graphics.DrawString(Convert.ToString(Items[e.Index]), Font, brush, boundsRect); 384 } 385 else 386 { 387 // If the ComboBox is displaying a RightToLeft language, draw it this way. 388 if (RightToLeft.Equals(RightToLeft.Yes)) 389 { 390 // Define a StringFormat object to make the string display RTL. 391 StringFormat rtl = new StringFormat(); 392 rtl.Alignment = StringAlignment.Near; 393 rtl.FormatFlags = StringFormatFlags.DirectionRightToLeft; 394 395 // Draw the strings in reverse order from high column index to zero column index. 396 for (int colIndex = _ColumnNames.Count - 1; colIndex >= 0; colIndex--) 397 { 398 if (Convert.ToBoolean(_ColumnWidths[colIndex])) 399 { 400 string item = Convert.ToString(FilterItemOnProperty(Items[e.Index], _ColumnNames[colIndex])); 401 402 boundsRect.X = lastRight; 403 boundsRect.Width = (int)_ColumnWidths[colIndex]; 404 lastRight = boundsRect.Right; 405 406 // Draw the string with the RTL object. 407 e.Graphics.DrawString(item, Font, brush, boundsRect, rtl); 408 409 if (colIndex > 0) 410 { 411 e.Graphics.DrawLine(linePen, boundsRect.Right, boundsRect.Top, boundsRect.Right, boundsRect.Bottom); 412 } 413 } 414 } 415 } 416 // If the ComboBox is displaying a LeftToRight language, draw it this way. 417 else 418 { 419 // Display the strings in ascending order from zero to the highest column. 420 for (int colIndex = 0; colIndex < _ColumnNames.Count; colIndex++) 421 { 422 if (Convert.ToBoolean(_ColumnWidths[colIndex])) 423 { 424 string item = Convert.ToString(FilterItemOnProperty(Items[e.Index], _ColumnNames[colIndex])); 425 426 boundsRect.X = lastRight; 427 boundsRect.Width = (int)_ColumnWidths[colIndex]; 428 lastRight = boundsRect.Right; 429 e.Graphics.DrawString(item, Font, brush, boundsRect); 430 431 if (colIndex < _ColumnNames.Count - 1) 432 { 433 e.Graphics.DrawLine(linePen, boundsRect.Right, boundsRect.Top, boundsRect.Right, boundsRect.Bottom); 434 } 435 } 436 } 437 } 438 } 439 } 440 } 441 442 e.DrawFocusRectangle(); 443 } 444 445 protected override void OnDropDown(EventArgs e) 446 { 447 base.OnDropDown(e); 448 449 if (_TotalWidth > 0) 450 { 451 if (Items.Count > MaxDropDownItems) 452 { 453 // The vertical scrollbar is present. Add its width to the total. 454 // If you don't then RightToLeft languages will have a few characters obscured. 455 this.DropDownWidth = _TotalWidth + SystemInformation.VerticalScrollBarWidth; 456 } 457 else 458 { 459 this.DropDownWidth = _TotalWidth; 460 } 461 } 462 } 463 464 protected override void OnKeyDown(KeyEventArgs e) 465 { 466 // Use the Delete or Escape Key to blank out the ComboBox and 467 // allow the user to type in a new value 468 if ((e.KeyCode == Keys.Delete) || 469 (e.KeyCode == Keys.Escape)) 470 { 471 SelectedIndex = -1; 472 Text = ""; 473 if (_LinkedTextBox != null) 474 { 475 _LinkedTextBox.Text = ""; 476 } 477 } 478 else if (e.KeyCode == Keys.F3) 479 { 480 // Fire the OpenSearchForm Event 481 if (OpenSearchForm != null) 482 { 483 OpenSearchForm(this, System.EventArgs.Empty); 484 } 485 } 486 } 487 488 // Some of the code for OnKeyPress was derived from some VB.NET code 489 // posted by Laurent Muller as a suggested improvement for another control. 490 // http://www.codeproject.com/vb/net/autocomplete_combobox.asp?df=100&forumid=3716&select=579095#xx579095xx 491 protected override void OnKeyPress(KeyPressEventArgs e) 492 { 493 int idx = -1; 494 string toFind; 495 496 DroppedDown = _AutoDropdown; 497 if (!Char.IsControl(e.KeyChar)) 498 { 499 if (_AutoComplete) 500 { 501 toFind = Text.Substring(0, SelectionStart) + e.KeyChar; 502 idx = FindStringExact(toFind); 503 504 if (idx == -1) 505 { 506 // An exact match for the whole string was not found 507 // Find a substring instead. 508 idx = FindString(toFind); 509 } 510 else 511 { 512 // An exact match was found. Close the dropdown. 513 DroppedDown = false; 514 } 515 516 if (idx != -1) // The substring was found. 517 { 518 SelectedIndex = idx; 519 SelectionStart = toFind.Length; 520 SelectionLength = Text.Length - SelectionStart; 521 } 522 else // The last keystroke did not create a valid substring. 523 { 524 // If the substring is not found, cancel the keypress 525 e.KeyChar = (char)0; 526 } 527 } 528 else // AutoComplete = false. Treat it like a DropDownList by finding the 529 // KeyChar that was struck starting from the current index 530 { 531 idx = FindString(e.KeyChar.ToString(), SelectedIndex); 532 533 if (idx != -1) 534 { 535 SelectedIndex = idx; 536 } 537 } 538 } 539 540 // Do no allow the user to backspace over characters. Treat it like 541 // a left arrow instead. The user must not be allowed to change the 542 // value in the ComboBox. 543 if ((e.KeyChar == (char)(Keys.Back)) && // A Backspace Key is hit 544 (_AutoComplete) && // AutoComplete = true 545 (Convert.ToBoolean(SelectionStart))) // And the SelectionStart is positive 546 { 547 // Find a substring that is one character less the the current selection. 548 // This mimicks moving back one space with an arrow key. This substring should 549 // always exist since we don't allow invalid selections to be typed. If you're 550 // on the 3rd character of a valid code, then the first two characters have to 551 // be valid. Moving back to them and finding the 1st occurrence should never fail. 552 toFind = Text.Substring(0, SelectionStart - 1); 553 idx = FindString(toFind); 554 555 if (idx != -1) 556 { 557 SelectedIndex = idx; 558 SelectionStart = toFind.Length; 559 SelectionLength = Text.Length - SelectionStart; 560 } 561 } 562 563 // e.Handled is always true. We handle every keystroke programatically. 564 e.Handled = true; 565 } 566 567 protected override void OnSelectedValueChanged(EventArgs e) 568 { 569 base.OnSelectedValueChanged(e); //Added after version 1.3 on 01/31/2008 570 571 if (_LinkedTextBox != null) 572 { 573 if (_LinkedColumnIndex < _ColumnNames.Count) 574 { 575 _LinkedTextBox.Text = Convert.ToString(FilterItemOnProperty(SelectedItem, _ColumnNames[_LinkedColumnIndex])); 576 } 577 } 578 } 579 580 protected override void OnValueMemberChanged(EventArgs e) 581 { 582 base.OnValueMemberChanged(e); 583 584 InitializeValueMemberColumn(); 585 } 586 587 private void InitializeColumns() 588 { 589 if (!Convert.ToBoolean(_ColumnNameString.Length)) 590 { 591 PropertyDescriptorCollection propertyDescriptorCollection = DataManager.GetItemProperties(); 592 593 _TotalWidth = 0; 594 _ColumnNames.Clear(); 595 596 for (int colIndex = 0; colIndex < propertyDescriptorCollection.Count; colIndex++) 597 { 598 _ColumnNames.Add(propertyDescriptorCollection[colIndex].Name); 599 600 // If the index is greater than the collection of explicitly 601 // set column widths, set any additional columns to the default 602 if (colIndex >= _ColumnWidths.Count) 603 { 604 _ColumnWidths.Add(_ColumnWidthDefault); 605 } 606 _TotalWidth += _ColumnWidths[colIndex]; 607 } 608 } 609 else 610 { 611 _TotalWidth = 0; 612 613 for (int colIndex = 0; colIndex < _ColumnNames.Count; colIndex++) 614 { 615 // If the index is greater than the collection of explicitly 616 // set column widths, set any additional columns to the default 617 if (colIndex >= _ColumnWidths.Count) 618 { 619 _ColumnWidths.Add(_ColumnWidthDefault); 620 } 621 _TotalWidth += _ColumnWidths[colIndex]; 622 } 623 624 } 625 626 // Check to see if the programmer is trying to display a column 627 // in the linked textbox that is greater than the columns in the 628 // ComboBox. I handle this error by resetting it to zero. 629 if (_LinkedColumnIndex >= _ColumnNames.Count) 630 { 631 _LinkedColumnIndex = 0; // Or replace this with an OutOfBounds Exception 632 } 633 } 634 635 private void InitializeValueMemberColumn() 636 { 637 int colIndex = 0; 638 foreach (String columnName in _ColumnNames) 639 { 640 if (String.Compare(columnName, ValueMember, true, CultureInfo.CurrentUICulture) == 0) 641 { 642 _ValueMemberColumnIndex = colIndex; 643 break; 644 } 645 colIndex++; 646 } 647 } 648 } 649 }