Search code examples
c#mysqlwinformsdatagridview.net-7.0

Highlight specific characters in all Cells of a DataGridView


When I do a character search in the DataGridView, I want only the searched specific characters to be selected (I give it a red box), without selecting all characters in cell. How do I do it?

I wrote code, and the result looks like this:

Output expected:

I want only the searched specific character to be selected (I give it a red box), without selecting all characters in cell

private void AddCustomer_DataGridView_CellFormatting(object? sender, DataGridViewCellFormattingEventArgs e)
{
    try
    {
        if (e.RowIndex >= 0 && e.ColumnIndex >= 0)
        {
            if (!String.IsNullOrEmpty(AddCustomer_SearchTextBox.Text) && e.Value != null)
            {
                string strValue = (String)e.Value;
                if (strValue.Contains(AddCustomer_SearchTextBox.Text))
                {
                    DataGridViewCellStyle? cellStyle = e.CellStyle;
                    if (cellStyle != null)
                    {
                        // Problem ini here, how to select only specific characters in a cell (not all characters)
                        cellStyle.BackColor = ColorTranslator.FromHtml("#0078D7");
                        cellStyle.ForeColor = ColorTranslator.FromHtml("#FFFFFF");
                    }
                }
            }
        }
    }
    catch (Exception ex)
    {
        ExLogger.LogException(ex, "");
        MessageBox.Show(ex.ToString(), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
}

// It works normally

public List<Customer>? SearchCustomers(int count, int minOffset, int maxOffset, string keyword, string sortExpression = "ASC")
{
    if (count <= 0)
    {
        return new List<Customer>();
    }

    int limit = 1 + (maxOffset - minOffset);
    if (limit < 0)
    {
        limit = 0;
    }

    int offset = minOffset - 1; // start
    if (offset < 0)
    {
        offset = 0;
    }
    if (offset >= count)
    {
        offset = count - 1;
    }

    string sql = @"SELECT customer.ID, customer.created, customer.name, customer.place_of_birth, customer.date_of_birth, customer.gender_id, customer.address, customer.neighbourhood_hamlet, customer.urban_village, customer.subdistrict, customer.religion_id, customer.marital_status_id, customer.profession, customer.citizenship_id, customer.email, customer.phone_number, customer.send_me 
                    FROM customer 
                        INNER JOIN gender ON gender_id = gender.ID 
                        INNER JOIN religion ON religion_id = religion.ID 
                        INNER JOIN marital_status ON marital_status_id = marital_status.ID 
                        INNER JOIN citizenship ON citizenship_id = citizenship.ID 
                    WHERE customer.ID LIKE @ID OR 
                          customer.created LIKE @created OR 
                          customer.name LIKE @name OR 
                          customer.place_of_birth LIKE @place_of_birth OR 
                          customer.date_of_birth LIKE @date_of_birth OR 
                          gender.gender_name LIKE @gender_id OR 
                          customer.address LIKE @address OR 
                          customer.neighbourhood_hamlet LIKE @neighbourhood_hamlet OR 
                          customer.urban_village LIKE @urban_village OR 
                          customer.subdistrict LIKE @subdistrict OR 
                          religion.religion_name LIKE @religion_id OR 
                          marital_status.marital_name LIKE @marital_status_id OR 
                          customer.profession LIKE @profession OR 
                          citizenship.citizenship_name LIKE @citizenship_id OR 
                          customer.email LIKE @email OR 
                          customer.phone_number LIKE @phone_number OR 
                          customer.send_me LIKE @send_me 
                    ORDER BY STR_TO_DATE(customer.created, '%d/%m/%Y %H:%i:%s') " + sortExpression + " LIMIT " + limit + " OFFSET " + offset;

    object[] parms = { "@ID",  '%'+ keyword + '%',
                       "@created",  '%'+ keyword + '%',
                       "@name",  '%'+ keyword + '%',
                       "@place_of_birth",  '%'+ keyword + '%',
                       "@date_of_birth",  '%'+ keyword + '%',
                       "@gender_id",  '%'+ keyword + '%',
                       "@address",  '%'+ keyword + '%',
                       "@neighbourhood_hamlet",  '%'+ keyword + '%',
                       "@urban_village",  '%'+ keyword + '%',
                       "@subdistrict",  '%'+ keyword + '%',
                       "@religion_id",  '%'+ keyword + '%',
                       "@marital_status_id",  '%'+ keyword + '%',
                       "@profession",  '%'+ keyword + '%',
                       "@citizenship_id",  '%'+ keyword + '%',
                       "@email",  '%'+ keyword + '%',
                       "@phone_number",  '%'+ keyword + '%',
                       "@send_me",  '%'+ keyword + '%'
                     };

    return db.Read(sql, Make, parms).ToList();
}

enter image description here


Solution

  • You can handle the CellPainting event to highlight sections of text inside the Cells of a DataGridView.
    The StringFormat.SetMeasurableCharacterRanges() method can be used to create CharacterRange elements that are then fed to Graphics.MeasureCharacterRanges(), to generate Regions that describe the bounds of sections of text in those ranges.

    Some things to keep in mind:

    1. You need to call e.PaintBackground() and e.PaintContent() to reset the rendering of the Cells when these need to be refreshed, otherwise you end up painting the same graphic multiple times over the same graphic surface
    2. You need to set e.Handled = true when you draw your custom graphic content, otherwise it's not rendered
    3. The alignment of the text in the Cells needs to be considered. It's usually centered vertically, but it could be also centered horizontally, or something else (e.g., aligned Left / Top). Since it's under our control, we can adjust the StringFormat's vertical and horizontal alignment accordingly. In the example, I assume the default layout and specify [StringFormat].LineAlignment = StringAlignment.Center;
    4. The text of one or more Cells may contain the search string multiple times, so better use a simple Regex to find all the matching sections of the text, so we can highlight all
    5. You can fill the content of a Region, but if you need a Rectangle instead, since some drawing methods do not accept a Region as the drawing area, you can convert a Region to a RectangleF with [Region].GetBounds([Graphics]). Use Rectangle.Round() to generate a Rectangle
    6. Always use the e.CellBounds value to define the bounds of the text you're submitting to the methods used to both measure the text and render the graphics; if you find yourself adjusting these bounds with magic numbers, there's something wrong in the procedure. If you need to move a Region, use [Region].Translate(); to move a Rectangle, use [Rectangle].Offset() or [Rectangle].Inflate()
    7. You need to dispose all disposable objects. This includes (here) the StringFormat and Brushes / Pens you create, unless you use stock objects as those you get from the Brushes, Pens and SystemBrushes classes. Explicitly disposing of these object is very important, they hold unmanaged resources. The GC cannot help you

    private void someDataGridView_CellPainting(object sender, DataGridViewCellPaintingEventArgs e)
    {
        if (e.ColumnIndex < 0 || e.RowIndex < 0) return;
        e.PaintBackground(e.ClipBounds, true);
        e.PaintContent(e.ClipBounds);
    
        if (string.IsNullOrEmpty(searchString)) return;
    
        var cellValue = e.FormattedValue?.ToString();
        if (string.IsNullOrEmpty(cellValue)) return;
    
        var positions = Regex.Matches(cellValue, searchString);
        if (positions.Count == 0) return;
    
        using (var format = new StringFormat(StringFormatFlags.FitBlackBox)) {
            // The Cell's vertical alignment is usually centered. Adjust as required
            format.LineAlignment = StringAlignment.Center;
            if (e.CellStyle.WrapMode == DataGridViewTriState.False || 
                e.CellStyle.WrapMode == DataGridViewTriState.NotSet) {
                format.FormatFlags |= StringFormatFlags.NoWrap;
            }
    
            // Generates ranges for all matching strings found 
            format.SetMeasurableCharacterRanges(positions.OfType<Match>()
                  .Select(m => new CharacterRange(m.Index, m.Length)).ToArray());
            // Generate Regions that contain the search text
            var regions = e.Graphics.MeasureCharacterRanges(cellValue, e.CellStyle.Font, e.CellBounds, format);
    
            using (var brush = new SolidBrush(Color.FromArgb(80, Color.Fuchsia))) {
                foreach (var region in regions) {
                    e.Graphics.FillRegion(brush, region);
                    // And / or draw a rectangle around the claculated box
                    e.Graphics.DrawRectangle(Pens.Red, Rectangle.Round(region.GetBounds(e.Graphics)));
                }
            }
        }
        e.Handled = true;
    }
    

    You can subscribe to the KeyDown event of a TextBox to handle the Enter key and invalidate your DataGridView when the search string changes:

    private string searchString = "";
    
    private void searchTextBox_KeyDown(object sender, KeyEventArgs e) {
        if (e.KeyCode == Keys.Enter) {
            e.SuppressKeyPress = true;
            searchString = (sender as Control).Text;
            someDataGridView.Invalidate();
        }
    }
    

    This is how it works:

    DataGridView CellPainting