Trong quá trình bảo trì một dự án WinForms, tôi gặp phải yêu cầu hiển thị nhiều cột dữ liệu trên ComboBox, tương tự như tính năng có sẵn trong các thư viện như DevExpress. Thay vì sử dụng giải pháp kết hợp TextBox và ListView (thường thấy trên web), tôi quyết định tận dụng GDI+ để tự xây dựng control tùy chỉnh. Dưới đây là cách thực hiện.
1. Tạo lớp Component kế thừa từ ComboBox
Trong một dự án WinForms mới, thêm một lớp component và cho nó kế thừa lớp ComboBox:
public partial class CustomMultiColumnCombo : ComboBox
{
// Các thuộc tính và phương thức sẽ được thêm sau
}
Điểm quan trọng là thiết lập thuộc tính DrawMode = OwnerDrawVariable để có thể tự vẽ các item.
2. Code xử lý chính
Dưới đây là phần code cốt lõi cho việc tính toán kích thước cột và vẽ lại từng item:
// Lưu tên cột và độ rộng
private string[] columnNames;
private float[] columnWidths;
private const int columnPadding = 5;
// Khởi tạo cột từ DataSource
private void InitColumns()
{
var properties = DataManager.GetItemProperties();
int count = properties.Count;
columnNames = new string[count];
columnWidths = new float[count];
for (int i = 0; i < count; i++)
columnNames[i] = properties[i].Name;
}
// Tính tổng chiều rộng dropdown
private float GetTotalDropDownWidth()
{
float sum = 0;
foreach (float w in columnWidths)
sum += w + columnPadding;
return sum + SystemInformation.VerticalScrollBarWidth;
}
protected override void OnDropDown(EventArgs e)
{
base.OnDropDown(e);
DropDownWidth = (int)GetTotalDropDownWidth();
}
protected override void OnMeasureItem(MeasureItemEventArgs e)
{
base.OnMeasureItem(e);
if (DesignMode) return;
InitColumns();
for (int i = 0; i < columnNames.Length; i++)
{
string text = Convert.ToString(FilterItemOnProperty(Items[e.Index], columnNames[i]));
SizeF size = e.Graphics.MeasureString(text, Font);
columnWidths[i] = Math.Max(columnWidths[i], size.Width);
}
e.ItemWidth = (int)GetTotalDropDownWidth();
}
protected override void OnDrawItem(DrawItemEventArgs e)
{
base.OnDrawItem(e);
if (DesignMode) return;
// Vẽ nền
e.Graphics.FillRectangle((e.State & DrawItemState.Selected) != 0
? Brushes.Bisque : Brushes.White, e.Bounds);
int left = e.Bounds.Left;
using (Pen separatorPen = new Pen(SystemColors.GrayText))
using (SolidBrush textBrush = new SolidBrush(ForeColor))
{
for (int i = 0; i < columnNames.Length; i++)
{
string value = Convert.ToString(FilterItemOnProperty(Items[e.Index], columnNames[i]));
Rectangle cellRect = new Rectangle(left, e.Bounds.Top,
(int)columnWidths[i] + columnPadding, e.Bounds.Height);
e.Graphics.DrawString(value, Font, textBrush, cellRect);
// Vẽ đường kẻ dọc giữa các cột
if (i < columnNames.Length - 1)
e.Graphics.DrawLine(separatorPen, cellRect.Right, cellRect.Top,
cellRect.Right, cellRect.Bottom);
left = cellRect.Right;
}
}
e.DrawFocusRectangle();
}
Giải thích ngắn gọn: Phương thức InitColumns lấy danh sách các cột từ nguồn dữ liệu. OnMeasureItem tính toán độ rộng cần thiết cho mỗi cột dựa trên nội dung. OnDrawItem vẽ từng ô với nền phù hợp (khi được chọn sẽ có màu Bisque, không thì trắng) và thêm đường kẻ dọc để phân cách.
3. Tích hợp vào Form
Sau khi build solution, bạn sẽ thấy control tùy chỉnh trong Toolbox. Kéo nó vào form hoặc khởi tạo trong code. Đoạn code dưới đây minh họa cách gán thủ công trong constructor của form (thay vì code thiết kế tự động):
private CustomMultiColumnCombo customCombo;
public Form1()
{
InitializeComponent();
customCombo = new CustomMultiColumnCombo();
customCombo.DrawMode = DrawMode.OwnerDrawVariable;
customCombo.DropDownStyle = ComboBoxStyle.DropDownList;
customCombo.Location = new Point(20, 30);
customCombo.Size = new Size(200, 25);
Controls.Add(customCombo);
}
4. Gán dữ liệu và lấy giá trị
Sử dụng DataTable làm nguồn dữ liệu:
DataTable source = new DataTable("Student");
source.Columns.Add("ID", typeof(int));
source.Columns.Add("Name", typeof(string));
source.Columns.Add("City", typeof(string));
source.Columns.Add("Country", typeof(string));
source.Rows.Add(1, "Alice", "New York", "USA");
source.Rows.Add(2, "Bob", "London", "UK");
source.Rows.Add(3, "Charlie", "Tokyo", "Japan");
customCombo.DataSource = source;
customCombo.DisplayMember = "Name"; // Cột hiển thị trên ComboBox khi đóng
customCombo.ValueMember = "ID";
Để lấy dữ liệu từ dòng được chọn, xử lý sự kiện SelectedIndexChanged:
private void customCombo_SelectedIndexChanged(object sender, EventArgs e)
{
DataTable dt = customCombo.DataSource as DataTable;
if (dt != null && customCombo.SelectedIndex >= 0)
{
DataRow row = dt.Rows[customCombo.SelectedIndex];
// Gán giá trị vào các TextBox tương ứng
textBox1.Text = row["Name"].ToString();
textBox2.Text = row["City"].ToString();
textBox3.Text = row["Country"].ToString();
}
}
5. Xử lý tìm kiếm khi gõ phím
Thêm override cho OnKeyPress để hỗ trợ lọc tự động:
protected override void OnKeyPress(KeyPressEventArgs e)
{
if (!char.IsControl(e.KeyChar))
{
string searchText = Text.Substring(0, SelectionStart) + e.KeyChar;
int exactIdx = FindStringExact(searchText);
if (exactIdx == -1)
{
int matchIdx = FindString(searchText);
if (matchIdx != -1)
{
DroppedDown = true;
SelectedIndex = matchIdx;
SelectionStart = searchText.Length;
SelectionLength = Text.Length - SelectionStart;
}
else
{
e.KeyChar = (char)0; // Hủy ký tự không tìm thấy
}
}
else
{
DroppedDown = false;
}
}
else if (e.KeyChar == (char)Keys.Back && SelectionStart > 0)
{
string prevText = Text.Substring(0, SelectionStart - 1);
int idx = FindString(prevText);
if (idx != -1)
{
SelectedIndex = idx;
SelectionStart = prevText.Length;
SelectionLength = Text.Length - SelectionStart;
}
}
e.Handled = true;
}
Kết quả đạt được
Khi chạy ứng dụng, dropdown của ComboBox sẽ hiển thị nhiều cột với nền sáng khi không focus và màu vàng nhạt khi di chuột. Bạn có thể dễ dàng tùy chỉnh màu sắc, kiểu chữ, hoặc thêm đường viền cho bảng.