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 }