Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# DataGridView: Long Text Truncated with "..." on the Left Side When the Column is Right-Aligned

I have a question about the cell truncation (replaced with "..."):

How to display the replacement "..." on the left side of a cell when the column is right-aligned?

I'm using non-equal-width font, so I cannot just count the characters to do some string manipulation as a workaround, I need a solution. I believe there should be.

To illustrate my question, I'm simulating my DataGridView here

Left Context (Right aligned column)        | Center Word | Right Context (Left aligned column)
                left context not truncated | CenterWord  | Right context not truncated
...Here is the long left context truncated | CenterWord  | Here is the long right context truncated...

I think I've made myself clear.

Thanks. Please help me out.

Peter

P.S.: the same question can be found at this link: http://objectmix.com/csharp/341736-datagridview-cell-format-question.html

like image 910
Peter Lee Avatar asked Oct 10 '10 21:10

Peter Lee


3 Answers

It's definitely an unusual thing to do - but (like anything else) it can be done. It's a question of measuring the size of the string and comparing it with the size of the cell. (Note that I assume that the data is entered by a user. If you're databinding you basically have to consume other events.)

This works but might need some fine tuning:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        dataGridView1.Columns.Add("col1", "col1");
        dataGridView1.Columns[0].CellTemplate.Style.Alignment = DataGridViewContentAlignment.MiddleRight;
        dataGridView1.Columns.Add("col2", "col2");
        dataGridView1.Columns.Add("col3", "col3");

        dataGridView1.Rows.Add();
        dataGridView1.CellEndEdit += new DataGridViewCellEventHandler(dataGridView1_CellEndEdit);
        dataGridView1.ColumnWidthChanged += new DataGridViewColumnEventHandler(dataGridView1_ColumnWidthChanged);              
    }

    void dataGridView1_ColumnWidthChanged(object sender, DataGridViewColumnEventArgs e)
    {
        if (e.Column.Index == 0)
        {
            // We need to truncate everything again when the width changes
            foreach (DataGridViewRow row in dataGridView1.Rows)
            {
                RightTruncateText(row.Cells[0]);
            }
        }
    }

    void RightTruncateText(DataGridViewCell cell)
    {                        
        // check if the content is too long:
        using (Graphics g = Graphics.FromHwnd(this.Handle))
        {
            SizeF size = g.MeasureString((string)cell.Tag, dataGridView1.Font); // NOTE: using the tag

            if (size.Width > cell.Size.Width)
            {
                StringBuilder truncated = new StringBuilder((string)cell.Tag);

                truncated.Insert(0, "...");

                // Truncate the string until small enough (NOTE: not optimized in any way!)                        
                while (size.Width > cell.Size.Width)
                {
                    truncated.Remove(3, 1);
                    size = g.MeasureString(truncated.ToString(), dataGridView1.Font);
                }
                cell.Value = truncated.ToString();
            }
            else
            {
                cell.Value = cell.Tag;
            }
        }
    }

    void dataGridView1_CellEndEdit(object sender, DataGridViewCellEventArgs e)
    {
        if (e.ColumnIndex == 0)
        {
            // Save the value in the tag but show the truncated value
            DataGridViewCell cell = dataGridView1.Rows[e.RowIndex].Cells[e.ColumnIndex];
            cell.Tag = cell.Value; // Saving the actual state
            RightTruncateText(cell);
        }
    }
}
like image 133
steinar Avatar answered Oct 08 '22 05:10

steinar


I've ended up implementing this by creating my own DataGridViewLeftCropTextBoxCell. Unfortunately DataGridViewTextBoxCell::Paint is bit a complicated method Reference Source .NET Framework 4.5.2 which uses many .NETs internal methods.

But at first I let base class draw background and borders (and if there's no sensible foreground, just leave it).

Then measure text and shrink it until it fits value bounds.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Drawing;

namespace Project
{
    public class DataGridViewLeftCropTextBoxCell : DataGridViewTextBoxCell
    {

        /// <summary>
        /// Paints contents
        /// </summary>
        /// <param name="graphics"></param>
        /// <param name="clipBounds"></param>
        /// <param name="cellBounds"></param>
        /// <param name="rowIndex"></param>
        /// <param name="cellState"></param>
        /// <param name="value"></param>
        /// <param name="formattedValue"></param>
        /// <param name="errorText"></param>
        /// <param name="cellStyle"></param>
        /// <param name="advancedBorderStyle"></param>
        /// <param name="paintParts"></param>
        protected override void Paint( Graphics graphics, Rectangle clipBounds, Rectangle cellBounds, int rowIndex, DataGridViewElementStates cellState, object value, object formattedValue, string errorText, DataGridViewCellStyle cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts )
        {
            string formattedString = formattedValue as string;

            // Nothing to draw
            if (String.IsNullOrEmpty( formattedString )) {
                base.Paint( graphics, clipBounds, cellBounds, rowIndex, cellState, value, formattedValue, errorText, cellStyle, advancedBorderStyle, paintParts );
                return;
            }

            // Draw parently without foreground
            base.Paint( graphics, clipBounds, cellBounds, rowIndex, cellState, value, formattedValue, errorText, cellStyle, advancedBorderStyle, paintParts & ~DataGridViewPaintParts.ContentForeground );

            // No foreground?
            if ((paintParts & DataGridViewPaintParts.ContentForeground) == DataGridViewPaintParts.None) {
                return;
            }

            // Calculate value bounds
            Rectangle borderWidths = BorderWidths( advancedBorderStyle );
            Rectangle valBounds = cellBounds;
            valBounds.Offset( borderWidths.X, borderWidths.Y );
            valBounds.Width -= borderWidths.Right;
            valBounds.Height -= borderWidths.Bottom;

            bool cellSelected = (cellState & DataGridViewElementStates.Selected) != 0;

            // Prepare text flags
            TextFormatFlags flags = ComputeTextFormatFlagsForCellStyleAlignment( this.DataGridView.RightToLeft == RightToLeft.Yes, cellStyle.Alignment, cellStyle.WrapMode );
            if ((flags & TextFormatFlags.SingleLine) != 0) {
                flags |= TextFormatFlags.EndEllipsis;
            }

            // Prepare size of text
            Size s = TextRenderer.MeasureText( graphics,
                formattedString,
                cellStyle.Font
            );

            // Text fits into bounds, just append
            if (s.Width < valBounds.Width) {
                TextRenderer.DrawText( graphics,
                    formattedString,
                    cellStyle.Font,
                    valBounds,
                    cellSelected ? cellStyle.SelectionForeColor : cellStyle.ForeColor,
                    flags );
                return;
            }

            // Prepare 
            StringBuilder truncated = new StringBuilder( formattedString );
            truncated.Insert( 0, "..." );

            // Truncate the string until it's small enough 
            while ((s.Width > valBounds.Width) && (truncated.Length > 5)) {
                truncated.Remove( 3, 1 );
                formattedString = truncated.ToString();
                s = TextRenderer.MeasureText( graphics,
                    formattedString,
                    cellStyle.Font
                );
            }

            TextRenderer.DrawText( graphics,
                formattedString,
                cellStyle.Font,
                valBounds,
                cellSelected ? cellStyle.SelectionForeColor : cellStyle.ForeColor,
                flags
            );
        }
    }
}

And you can also create your own column type:

class DataGridViewLeftCropTextBoxColumn : DataGridViewTextBoxColumn
{
    public override DataGridViewCell CellTemplate
    {
        get { return new DataGridViewLeftCropTextBoxCell(); }
        set { base.CellTemplate = value; }
    }
}

I've borrowed text shrinking from steinar's answer and TextFormatFlags ComputeTextFormatFlagsForCellStyleAlignment from .NET Framework Reference Source.

like image 34
Vyktor Avatar answered Oct 08 '22 05:10

Vyktor


I have made an workaround, it's working except the "..." (truncation works well)

    protected override void OnCellPainting(DataGridViewCellPaintingEventArgs e)
    {
        base.OnCellPainting(e);

        if (e.RowIndex >= 0 && e.ColumnIndex >= 0 &&
            CustomRightToLeftColumnNames.Contains(this.Columns[e.ColumnIndex].Name))
        {
            // Method 2:
            e.PaintBackground(e.CellBounds, true);

            if (e.FormattedValue != null)
            {
                TextFormatFlags flags = TextFormatFlags.RightToLeft         |
                                        TextFormatFlags.VerticalCenter      |
                                        TextFormatFlags.Right               |
                                        TextFormatFlags.LeftAndRightPadding;// |
                                        //TextFormatFlags.EndEllipsis;
                TextRenderer.DrawText
                (
                    e.Graphics,
                    e.FormattedValue.ToString(),
                    e.CellStyle.Font,
                    e.CellBounds,
                    e.CellStyle.ForeColor,
                    flags
                );
            }

            e.Handled = true;
        }
    }

The only problem with this solution is I don't know how to Set the TextFormatFlags to get the right behavior I want, exactly same as when DataGridView.RightToLeft = Yes.

If I turn on TextFormatFlags.EndEllipsis, the three dots "..." will appear on the left side, but it truncates from the right end of the string.

I'm not sure which flag of TextFormatFlags enumeration to turn on.

like image 1
Peter Lee Avatar answered Oct 08 '22 05:10

Peter Lee