Search code examples
c#winformslistviewownerdrawn

Owner drawn list view does not autosize columns correctly


My listview (WinForms) derived class draws cells by its own to get an Excel like look from a two dimensional string array.

Autosize works somehow, but not exactly. The columns with bigger entries are wider, but still the ellipsis occurs on the biggest strings.

How can I get autosize working correctly?

BTW: DataGridView is unwanted due to crappy performance.

enter image description here

TableViewTest.cs:

public class TableViewTest : Form
{
    private LiftQD.TableView tableView1;

    public TableViewTest()
    {
        this.tableView1 = new LiftQD.TableView();
        this.SuspendLayout();
        this.tableView1.Dock = System.Windows.Forms.DockStyle.Fill;
        this.tableView1.HeaderStyle = System.Windows.Forms.ColumnHeaderStyle.Nonclickable;
        this.tableView1.Location = new System.Drawing.Point(0, 0);
        this.tableView1.Name = "tableView1";
        this.tableView1.OwnerDraw = true;
        this.tableView1.Scrollable = false;
        this.tableView1.Size = new System.Drawing.Size(284, 261);
        this.tableView1.TabIndex = 0;
        this.tableView1.UseCompatibleStateImageBehavior = false;
        this.tableView1.View = System.Windows.Forms.View.Details;
        this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
        this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
        this.ClientSize = new System.Drawing.Size(284, 261);
        this.Controls.Add(this.tableView1);
        this.Name = "TableViewTest";
        this.Text = "TableViewTest";
        this.ResumeLayout(false);

        tableView1.Table = new Table(new String[,]{{"abcdefg","hijklmnop"},{"1234567","all good children go to heaven"}});
    }
}

TableView.cs:

public class TableView: ListView
{
    Table table;
    bool isAdjustingLastColumn;

    public TableView()
    {
        DoubleBuffered = true;
        View = View.Details;

        // we draw gridlines ourselves
        GridLines = false;
        OwnerDraw = true;
        Scrollable = false;
        HeaderStyle = ColumnHeaderStyle.Nonclickable;
    }

    protected override void OnDrawColumnHeader(DrawListViewColumnHeaderEventArgs e)
    {
        if (e.ColumnIndex == Columns.Count-1)
        {
            e.Graphics.FillRectangle(SystemBrushes.Window, e.Bounds.Left, e.Bounds.Top, e.Bounds.Width-1, e.Bounds.Height-1);
        }
        else
        {
            e.Graphics.FillRectangle(SystemBrushes.Control, e.Bounds.Left, e.Bounds.Top, e.Bounds.Width-1, e.Bounds.Height-1);
            e.Graphics.DrawLine(SystemPens.ActiveBorder, e.Bounds.Left, e.Bounds.Bottom-1, e.Bounds.Right-1, e.Bounds.Bottom-1);
            e.Graphics.DrawLine(SystemPens.ActiveBorder, e.Bounds.Right-1, e.Bounds.Bottom-1, e.Bounds.Right-1, e.Bounds.Top);
            e.DrawText(TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter);
        }
    }

    protected override void OnDrawSubItem(DrawListViewSubItemEventArgs e)
    {
        if (e.ColumnIndex == Columns.Count-1)
        {
            e.Graphics.FillRectangle(SystemBrushes.Window, e.Bounds.Left, e.Bounds.Top, e.Bounds.Width-1, e.Bounds.Height-1);
        }
        else if (e.ColumnIndex == 0)
        {
            e.Graphics.FillRectangle(SystemBrushes.Control, e.Bounds.Left, e.Bounds.Top, e.Bounds.Width-1, e.Bounds.Height-1);
            e.Graphics.DrawLine(SystemPens.ActiveBorder, e.Bounds.Left, e.Bounds.Bottom-1, e.Bounds.Right-1, e.Bounds.Bottom-1);
            e.Graphics.DrawLine(SystemPens.ActiveBorder, e.Bounds.Right-1, e.Bounds.Bottom-1, e.Bounds.Right-1, e.Bounds.Top);
            e.DrawText(TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter);
        }
        else
        {
            e.Graphics.FillRectangle(SystemBrushes.Window, e.Bounds.Left, e.Bounds.Top, e.Bounds.Width-1, e.Bounds.Height-1);
            e.Graphics.DrawLine(SystemPens.ActiveBorder, e.Bounds.Left, e.Bounds.Bottom-1, e.Bounds.Right-1, e.Bounds.Bottom-1);
            e.Graphics.DrawLine(SystemPens.ActiveBorder, e.Bounds.Right-1, e.Bounds.Bottom-1, e.Bounds.Right-1, e.Bounds.Top);
            e.DrawText(TextFormatFlags.VerticalCenter | TextFormatFlags.SingleLine | TextFormatFlags.GlyphOverhangPadding | TextFormatFlags.WordEllipsis);
        }

    }

    public int TableWidth
    {
        get
        {
            if (Columns.Count < 2) return 0;
            return Items[Items.Count-1].SubItems[Columns.Count-2].Bounds.Right;
        }
    }

    public int TableHeight
    {
        get
        {
            if (Items.Count == 0) return 0;
            return Items[Items.Count-1].Bounds.Bottom;
        }
    }

    // sets the size of the last (empty) column so as to fill up the client area
    void adjustLastColumn()
    {
        if (!isAdjustingLastColumn)
        {
            isAdjustingLastColumn = true;

            if (Columns.Count > 0) Columns[Columns.Count-1].Width = Math.Max(ClientSize.Width-TableWidth,0);

            isAdjustingLastColumn = false;
        }
    }

    protected override void OnResize(EventArgs e)
    {
        adjustLastColumn();
        base.OnResize(e);
    }

    protected override void OnColumnWidthChanged(ColumnWidthChangedEventArgs e)
    {
        adjustLastColumn();
        base.OnColumnWidthChanged(e);
    }

    protected override void OnColumnWidthChanging(ColumnWidthChangingEventArgs e)
    {
        adjustLastColumn();
        base.OnColumnWidthChanging(e);
    }

    string GetExcelColumnName(int columnNumber)
    {
        int dividend = columnNumber;
        string columnName = String.Empty;
        int modulo;

        while (dividend > 0)
        {
            modulo = (dividend - 1) % 26;
            columnName = Convert.ToChar(65 + modulo).ToString() + columnName;
            dividend = (int)((dividend - modulo) / 26);
        } 

        return columnName;
    }

    public Table Table
    {
        get {return new Table(table);}
        set
        {
            table = new Table(value);

            if (table != null)
            {
                int columnCount = table.ColumnCount;
                int rowCount = table.RowCount;

                // one unnamed column for the row names (line numbers)
                Columns.Add("");

                for (int i=0; i<columnCount; i++)
                {
                    Columns.Add(GetExcelColumnName(i+1));
                }

                // another empty column that will be filling the remaining client area
                Columns.Add("");

                for (int j=0; j<rowCount; j++)
                {
                    ListViewItem lvi = new ListViewItem((j+1).ToString());

                    lvi.UseItemStyleForSubItems = false;                    

                    for (int i=0; i<columnCount; i++)
                    {
                        lvi.SubItems.Add(table[i,j]);
                    }

                    Items.Add(lvi);
                }

                AutoResizeColumns(ColumnHeaderAutoResizeStyle.HeaderSize);
                AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent);

                adjustLastColumn();
            }
        }
    }
}

Table.cs:

public class Table
{
    string[,] data;
    string name = "";
    string info = "";

    public string Name
    {
        get {return name;}
        set {if (value != null) name = value; else name = "";}
    }

    public string Info
    {
        get {return info;}
        set {if (value != null) info = value; else info = "";}
    }

    public Table(string[,] originalData)
    {
        data = (string[,])(originalData.Clone());
    }

    public Table(Table originalData)
    {
        data = (string[,])(originalData.data.Clone());
        string name = originalData.Name;
        string info = originalData.Info;
    }

    public int RowCount {get{return data.GetLength(1);}}

    public int ColumnCount {get{return data.GetLength(0);}}

    public string this[int xIndex, int yIndex]
    {
        get
        {
            if (xIndex >= 0 && xIndex < data.GetLength(0) && yIndex >= 0 && yIndex < data.GetLength(1))
                return data[xIndex, yIndex];
            else
                return "";
        }
    }
}

Solution

  • The default DrawText implementation seems to add non-removable padding. Use the TextRenderer class instead:

    // e.DrawText(TextFormatFlags.VerticalCenter | TextFormatFlags.SingleLine | 
    //            TextFormatFlags.GlyphOverhangPadding | TextFormatFlags.WordEllipsis);
    TextRenderer.DrawText(e.Graphics, e.SubItem.Text, e.Item.Font, e.Bounds,
                          SystemColors.WindowText, SystemColors.Window,
                          TextFormatFlags.VerticalCenter | TextFormatFlags.WordEllipsis);