Search code examples
c#winformslistviewownerdrawn

Listview Problems with ownerdraw mode text rendering


I'm trying to implement an owner drawn ListView because the base control eats the tab character which I need to align values within a column.

Using an example from MSDN as a base I was able get close. The only problem I still have is that the periods of ellipsis used when the text doesn't fit in the column is much more closely spaced together than in the default text rendering; the the point that if the font is bold the periods run together into an underscore.

The program below demonstrates the problem. It has 4 ListViews: The two on the top are drawn using the default rendering. The two on the bottom are owner drawn, and the pair in the right side are bolded. For length reasons I removed everything I didn't need in order to demonstrate the problem, which is why the owner drawn ListViews don't have column headers.

Looking at a zoomed in screenshot the periods of the ellipsis in the owner drawn ListViews are spaced one pixel apart; those in the default drawing have two pixels of spacing. When bolding widens the periods to two pixels the owner drawn ones merge together into a solid mass that looks like an underscore.

There are other minor differences in the text rendering as well; but the ellipsis is the only one that's readily apparent without zooming. These differences do however make me suspect the problem is a more general issue. Possibly GDI vs GDI+ rendering? Except I thought that could only vary at the application level. Apparently not, toggling Application.SetCompatibleTextRenderingDefault() didn't affect anything.

using System;
using System.Drawing;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public class Form1 : Form
    {
        private void ListViewDrawSubItem(object sender, 
                                         DrawListViewSubItemEventArgs e)
        {
            ListView listView = sender as ListView;
            using (StringFormat sf = new StringFormat())
            {
                // Draw the standard background.
                e.DrawBackground();
                sf.SetTabStops(0, new float[] {12, 12, 12, 12, 12});
                sf.FormatFlags = sf.FormatFlags | StringFormatFlags.NoWrap;
                sf.Trimming = StringTrimming.EllipsisCharacter;

                // Draw the header text.
                // passing the controls font directly causes an 
                // ArguementException);
                using (Font headerFont = new Font(listView.Font.Name, 
                                                  listView.Font.Size, 
                                                  listView.Font.Style))
                {
                    e.Graphics.DrawString(e.SubItem.Text, headerFont, 
                                          Brushes.Black, e.Bounds, sf);
                }
            }
        }

        public Form1()
        {
            InitializeComponent();
            LoadData(listView1);
            LoadData(listView2);
            LoadData(listView3);
            LoadData(listView4);
        }

        private void LoadData(ListView listView)
        {
            listView.Columns.Add("first", 35);
            listView.Columns.Add("second", 75);

            for (int i = 0; i < 5; i++)
            {
                listView.Items.Add("test");
                listView.Items[i].SubItems.Add("test test test test");
            }
        }

        #region from Form1.Designer
        /// <summary>
        /// Required designer variable.
        /// </summary>
        private System.ComponentModel.IContainer components = null;

        /// <summary>
        /// Clean up any resources being used.
        /// </summary>
        /// <param name="disposing">true if managed resources should be 
        /// disposed; otherwise, false.</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Windows Form Designer generated code

        /// <summary>
        /// Required method for Designer support - do not modify
        /// the contents of this method with the code editor.
        /// </summary>
        private void InitializeComponent()
        {
            this.listView1 = new System.Windows.Forms.ListView();
            this.listView2 = new System.Windows.Forms.ListView();
            this.listView3 = new System.Windows.Forms.ListView();
            this.listView4 = new System.Windows.Forms.ListView();
            this.SuspendLayout();
            // 
            // listView1
            // 
            this.listView1.Location = new System.Drawing.Point(12, 12);
            this.listView1.Name = "listView1";
            this.listView1.Size = new System.Drawing.Size(121, 116);
            this.listView1.TabIndex = 0;
            this.listView1.UseCompatibleStateImageBehavior = false;
            this.listView1.View = System.Windows.Forms.View.Details;
            // 
            // listView2
            // 
            this.listView2.Font = new System.Drawing.Font("Microsoft Sans Serif", 
                                       8.25F, System.Drawing.FontStyle.Bold,
                                       System.Drawing.GraphicsUnit.Point, 
                                       ((byte)(0)));
            this.listView2.Location = new System.Drawing.Point(151, 12);
            this.listView2.Name = "listView2";
            this.listView2.Size = new System.Drawing.Size(121, 116);
            this.listView2.TabIndex = 1;
            this.listView2.UseCompatibleStateImageBehavior = false;
            this.listView2.View = System.Windows.Forms.View.Details;
            // 
            // listView3
            // 
            this.listView3.Location = new System.Drawing.Point(12, 134);
            this.listView3.Name = "listView3";
            this.listView3.OwnerDraw = true;
            this.listView3.Size = new System.Drawing.Size(121, 116);
            this.listView3.TabIndex = 2;
            this.listView3.UseCompatibleStateImageBehavior = false;
            this.listView3.View = System.Windows.Forms.View.Details;
            this.listView3.DrawSubItem += new 
                          System.Windows.Forms.DrawListViewSubItemEventHandler(
                          this.ListViewDrawSubItem);
            // 
            // listView4
            // 
            this.listView4.Font = new System.Drawing.Font("Microsoft Sans Serif", 
                                       8.25F, System.Drawing.FontStyle.Bold,
                                       System.Drawing.GraphicsUnit.Point, 
                                       ((byte)(0)));
            this.listView4.Location = new System.Drawing.Point(151, 134);
            this.listView4.Name = "listView4";
            this.listView4.OwnerDraw = true;
            this.listView4.Size = new System.Drawing.Size(121, 116);
            this.listView4.TabIndex = 3;
            this.listView4.UseCompatibleStateImageBehavior = false;
            this.listView4.View = System.Windows.Forms.View.Details;
            this.listView4.DrawSubItem += new 
                          System.Windows.Forms.DrawListViewSubItemEventHandler(
                          this.ListViewDrawSubItem);
            // 
            // Form1
            // 
            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(284, 262);
            this.Controls.Add(this.listView4);
            this.Controls.Add(this.listView3);
            this.Controls.Add(this.listView2);
            this.Controls.Add(this.listView1);
            this.Name = "Form1";
            this.Text = "Form1";
            this.ResumeLayout(false);

        }

        #endregion

        private System.Windows.Forms.ListView listView1;
        private System.Windows.Forms.ListView listView2;
        private System.Windows.Forms.ListView listView3;
        private System.Windows.Forms.ListView listView4;
        #endregion

        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
    }
}

Solution

  • I've found a contingent implementation for the draw sub item method. The main caveats I have are that the tab size is fixed (although I could drop to win32 if necessary to change it); and that the combination of flags that I need while working on my machine is reported to be mutually incompatible in MSDN.

    private void ListViewDrawSubItem(object sender, DrawListViewSubItemEventArgs e)
    {
        //toggle colors if the item is highlighted 
        if (e.Item.Selected && e.Item.ListView.Focused)
        {
            e.SubItem.BackColor = SystemColors.Highlight;
            e.SubItem.ForeColor = e.Item.ListView.BackColor;
        }
        else if (e.Item.Selected && !e.Item.ListView.Focused)
        {
            e.SubItem.BackColor = SystemColors.Control;
            e.SubItem.ForeColor = e.Item.ListView.ForeColor;
        }
        else
        {
            e.SubItem.BackColor = e.Item.ListView.BackColor;
            e.SubItem.ForeColor = e.Item.ListView.ForeColor;
        }
    
        // Draw the standard header background.
        e.DrawBackground();
            
        //add a 2 pixel buffer the match default behavior
        Rectangle rec = new Rectangle(e.Bounds.X + 2, e.Bounds.Y+2, 
                                      e.Bounds.Width - 4, e.Bounds.Height-4);
    
        //TODO  Confirm combination of TextFormatFlags.EndEllipsis and 
        //TextFormatFlags.ExpandTabs works on all systems.  MSDN claims they're 
        //exclusive but on Win7-64 they work.
        TextFormatFlags flags = TextFormatFlags.Left | TextFormatFlags.EndEllipsis | 
                                TextFormatFlags.ExpandTabs |
                                TextFormatFlags.SingleLine;
    
        //If a different tab stop than the default is needed, will have to p/invoke 
        //DrawTextEx from win32.
        TextRenderer.DrawText(e.Graphics, e.SubItem.Text, e.Item.ListView.Font, rec, 
                              e.SubItem.ForeColor, flags);
    }