C#

老子叫甜甜 提交于 2020-03-01 16:33:21

一、写在前面

在工作中需要实现一个场景,有一个名单类的数据需要维护,这个维护工作需要有一个复核功能,为了方便复核时对名单变更情况有一个良好的掌握,需要做一个便跟前后名单的对比功能。

功能实现后效果如下图:

其中,修改前名单、修改后名单、前后名单对比三个部分都使用了封装后的ListView控件保存数据

二、步骤一:封装ListView

封装ListView主要是为了保证对“前后名单对比”部分数据的着色。虽然微软原生的ListView就支持了对数据项进行着色,但因为“前后名单对比”部分使用了分组功能,点击分组标题时,默认选中分组内的全部数据,这回导致分组内的数据颜色都变为黑色。为改变这一情况,我们需要创建一个继承ListView的类ListViewEnhanced,代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;

namespace NameListComparer
{
    class ListViewEnhanced : ListView
    {
        /// <summary>
        /// call SendMessage using hit test structures
        /// </summary>
        [DllImport("User32.dll")]
        static extern int SendMessage(IntPtr hWnd, int msg, int wParam, ref LVHITTESTINFO lParam);     

        #region Windows constants
        /// <summary>
        /// WndProc message for the left mouse button down
        /// </summary>
        const int WM_LBUTTONUP = 0x0201;
        /// <summary>
        /// offset for the first SendMessage for a ListView
        /// </summary>
        const int LVM_FIRST = 0x1000;
        /// <summary>
        /// ListView SendMessage to check for an item hit test
        /// </summary>
        const int LVM_HITTEST = (LVM_FIRST + 18);
        /// <summary>
        /// ListView SendMessage to check for a sub-item hit test
        /// </summary>
        const int LVM_SUBITEMHITTEST = (LVM_FIRST + 57);
        #endregion Windows constants

        /// <summary>
        /// see http://msdn.microsoft.com/en-us/library/bb774754%28v=VS.85%29.aspx
        /// </summary>
        [Flags]
        internal enum LVHITTESTFLAGS : uint
        {
            LVHT_NOWHERE = 0x00000001,
            LVHT_ONITEMICON = 0x00000002,
            LVHT_ONITEMLABEL = 0x00000004,
            LVHT_ONITEMSTATEICON = 0x00000008,
            LVHT_ONITEM = (LVHT_ONITEMICON | LVHT_ONITEMLABEL | LVHT_ONITEMSTATEICON),
            LVHT_ABOVE = 0x00000008,
            LVHT_BELOW = 0x00000010,
            LVHT_TORIGHT = 0x00000020,
            LVHT_TOLEFT = 0x00000040,
            // Vista/Win7+ only
            LVHT_EX_GROUP_HEADER = 0x10000000,
            LVHT_EX_GROUP_FOOTER = 0x20000000,
            LVHT_EX_GROUP_COLLAPSE = 0x40000000,
            LVHT_EX_GROUP_BACKGROUND = 0x80000000,
            LVHT_EX_GROUP_STATEICON = 0x01000000,
            LVHT_EX_GROUP_SUBSETLINK = 0x02000000,
        }

        /// <summary>
        /// see http://msdn.microsoft.com/en-us/library/bb774754%28v=VS.85%29.aspx
        /// </summary>
        [StructLayout(LayoutKind.Sequential)]
        struct LVHITTESTINFO
        {
            public POINT pt;
            public LVHITTESTFLAGS flags;
            public int iItem;
            public int iSubItem;
            // Vista/Win7+
            public int iGroup;
        }

        /// <summary>
        /// see http://msdn.microsoft.com/en-us/library/dd162805%28v=VS.85%29.aspx
        /// </summary>
        [StructLayout(LayoutKind.Sequential)]
        struct POINT
        {
            public POINT(int x, int y)
            {
                this.x = x;
                this.y = y;
            }
            public int x;
            public int y;
        }

        /// <summary>
        /// convert the IntPtr LParam to an Point.
        /// </summary>
        private static POINT LParamToPoint(IntPtr lparam)
        {
            return new POINT(lparam.ToInt32() & 0xFFFF, lparam.ToInt32() >> 16);
        }

        protected override void WndProc(ref Message m)
        {
            //the link uses WM_LBUTTONDOWN but I found that it doesn't work
            if (m.Msg == WM_LBUTTONUP)
            {
                LVHITTESTINFO info = new LVHITTESTINFO();

                //The LParamToPOINT function I adapted to not bother with 
                //  converting to System.Drawing.Point, rather I just made 
                //  its return type the POINT struct
                info.pt = LParamToPoint(m.LParam);

                //if the click is on the group header, exit, otherwise send message
                if (SendMessage(this.Handle, LVM_SUBITEMHITTEST, -1, ref info) != -1)
                    if ((info.flags & LVHITTESTFLAGS.LVHT_EX_GROUP_HEADER) != 0)
                        return; //*
            }
            base.WndProc(ref m);
        }
    }
}

三、步骤二:建立存放单个名单的对象

存放单个名单的对象,可根据业务系统自身情况量身定制,下面代码是一个我实现的MemberInfo类,包含成员编码、成员名称、成员称号三个属性:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace NameListComparer
{
    public class MemberInfo
    {
        /// <summary>
        /// 成员信息
        /// </summary>
        /// <param name="memCode">成员编码</param>
        /// <param name="memName">成员名称</param>
        /// <param name="memTitle">成员称号</param>
        public MemberInfo(string memCode, string memName, string memTitle = "")
        {
            this.MemCode = memCode;
            this.MemName = memName;
            this.MemTitle = memTitle;
        }

        /// <summary>
        /// 编号
        /// </summary>
        private string _memCode;
        /// <summary>
        /// 编号
        /// </summary>
        public string MemCode
        {
            get
            {
                return _memCode;
            }
            set
            {
                _memCode = value;
            }
        }

        /// <summary>
        /// 编号
        /// </summary>
        private string _memName;
        /// <summary>
        /// 编号
        /// </summary>
        public string MemName
        {
            get
            {
                return _memName;
            }
            set
            {
                _memName = value;
            }
        }

        /// <summary>
        /// 称号
        /// </summary>
        private string _memTitle;
        /// <summary>
        /// 称号
        /// </summary>
        public string MemTitle
        {
            get
            {
                return _memTitle;
            }
            set
            {
                _memTitle = value;
            }
        }
    }
}

四、步骤三:创建自定义控件

创建一个继承UserControl的自定义控件MemberComparer,如下图所示:

三个ListView的View属性,都要设置成System.Windows.Forms.View.Details

MemberComparer控件的代码如下:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Collections;

namespace NameListComparer
{
    /// <summary>
    /// 自定义控件:用于比较修改前和修改后的名单
    /// </summary>
    public partial class MemberComparer : UserControl
    {
        /// <summary>
        /// 自定义控件:用于比较修改前和修改后的名单
        /// </summary>
        public MemberComparer()
        {
            InitializeComponent();
        }

        /// <summary>
        /// Load函数
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void MemberComparer_Load(object sender, EventArgs e)
        {
            //创建最左侧僵尸列(必须)
            ColumnHeader chPreZombie = new ColumnHeader();
            chPreZombie.Name = "zombie";
            chPreZombie.Text = "";
            chPreZombie.Width = 0;
            chPreZombie.TextAlign = HorizontalAlignment.Center;
            lvwPreData.Columns.Add(chPreZombie);
            //成员编码
            ColumnHeader chPreMemCode = new ColumnHeader();
            chPreMemCode.Text = "成员编码";
            chPreMemCode.Width = 100;
            chPreMemCode.TextAlign = HorizontalAlignment.Center;
            lvwPreData.Columns.Add(chPreMemCode);
            //成员名称
            ColumnHeader chPreMemName = new ColumnHeader();
            chPreMemName.Text = "成员名称";
            chPreMemName.Width = 100;
            chPreMemName.TextAlign = HorizontalAlignment.Center;
            lvwPreData.Columns.Add(chPreMemName);
            ColumnHeader chPreMemTitle = new ColumnHeader();
            //成员称号
            chPreMemTitle.Text = "成员称号";
            chPreMemTitle.Width = 100;
            chPreMemTitle.TextAlign = HorizontalAlignment.Center;
            lvwPreData.Columns.Add(chPreMemTitle);
            //为ListView添加横向滚动条
            chPreMemTitle.Width = 110; //你没看错,这个功能就是这么写的
            //指定排序规则
            lvwPreData.ListViewItemSorter = new ListViewItemComparer(1); 
            //僵尸列不允许拖动
            lvwPreData.ColumnWidthChanging += (obj, arg) =>
            {
                ColumnHeader header = lvwPreData.Columns[arg.ColumnIndex];
                if (header.Name == "zombie")
                {
                    arg.Cancel = true;
                }
                arg.NewWidth = lvwPreData.Columns[arg.ColumnIndex].Width;
            };

            //创建最左侧僵尸列(必须)
            ColumnHeader chPostZombie = new ColumnHeader();
            chPostZombie.Name = "zombie";
            chPostZombie.Text = "";
            chPostZombie.Width = 0;
            chPostZombie.TextAlign = HorizontalAlignment.Center;
            lvwPostData.Columns.Add(chPostZombie);
            //成员编码
            ColumnHeader chPostMemCode = new ColumnHeader();
            chPostMemCode.Text = "成员编码";
            chPostMemCode.Width = 100;
            chPostMemCode.TextAlign = HorizontalAlignment.Center;
            lvwPostData.Columns.Add(chPostMemCode);
            //成员名称
            ColumnHeader chPostMemName = new ColumnHeader();
            chPostMemName.Text = "成员名称";
            chPostMemName.Width = 100;
            chPostMemName.TextAlign = HorizontalAlignment.Center;
            lvwPostData.Columns.Add(chPostMemName);
            ColumnHeader chPostMemTitle = new ColumnHeader();
            //成员称号
            chPostMemTitle.Text = "成员称号";
            chPostMemTitle.Width = 100;
            chPostMemTitle.TextAlign = HorizontalAlignment.Center;
            lvwPostData.Columns.Add(chPostMemTitle);
            //为ListView添加横向滚动条
            chPostMemTitle.Width = 110; //你没看错,这个功能就是这么写的
            //指定排序规则
            lvwPostData.ListViewItemSorter = new ListViewItemComparer(1);
            //僵尸列不允许拖动
            lvwPostData.ColumnWidthChanging += (obj, arg) =>
            {
                ColumnHeader header = lvwPostData.Columns[arg.ColumnIndex];
                if (header.Name == "zombie")
                {
                    arg.Cancel = true;
                }
                arg.NewWidth = lvwPostData.Columns[arg.ColumnIndex].Width;
            };

            //创建最左侧僵尸列(必须)
            ColumnHeader chCmpZombie = new ColumnHeader();
            chCmpZombie.Name = "zombie";
            chCmpZombie.Text = "";
            chCmpZombie.Width = 0;
            chCmpZombie.TextAlign = HorizontalAlignment.Center;
            lvwCmpData.Columns.Add(chCmpZombie);
            //成员编码
            ColumnHeader chCmpMemCode = new ColumnHeader();
            chCmpMemCode.Text = "成员编码";
            chCmpMemCode.Width = 100;
            chCmpMemCode.TextAlign = HorizontalAlignment.Center;
            lvwCmpData.Columns.Add(chCmpMemCode);
            //成员名称
            ColumnHeader chCmpMemName = new ColumnHeader();
            chCmpMemName.Text = "成员名称";
            chCmpMemName.Width = 100;
            chCmpMemName.TextAlign = HorizontalAlignment.Center;
            lvwCmpData.Columns.Add(chCmpMemName);
            ColumnHeader chCmpMemTitle = new ColumnHeader();
            //成员称号
            chCmpMemTitle.Text = "成员称号";
            chCmpMemTitle.Width = 100;
            chCmpMemTitle.TextAlign = HorizontalAlignment.Center;
            lvwCmpData.Columns.Add(chCmpMemTitle);
            //为ListView添加横向滚动条
            chCmpMemTitle.Width = 110; //你没看错,这个功能就是这么写的
            //指定排序规则
            lvwCmpData.ListViewItemSorter = new ListViewItemComparer(1);
            //僵尸列不允许拖动
            lvwCmpData.ColumnWidthChanging += (obj, arg) =>
            {
                ColumnHeader header = lvwCmpData.Columns[arg.ColumnIndex];
                if (header.Name == "zombie")
                {
                    arg.Cancel = true;
                }
                arg.NewWidth = lvwCmpData.Columns[arg.ColumnIndex].Width;
            };
        }

        /// <summary>
        /// ListView比较规则
        /// </summary>
        class ListViewItemComparer : IComparer
        {
            /// <summary>
            /// 按第几列进行比较(首列为第0列)
            /// </summary>
            private int col;
            /// <summary>
            /// ListView比较规则,默认以第0列比较
            /// </summary>
            public ListViewItemComparer()
            {
                col = 0;
            }
            /// <summary>
            /// ListView比较规则,指定以第几列比较
            /// </summary>
            /// <param name="column"></param>
            public ListViewItemComparer(int column)
            {
                col = column;
            }
            /// <summary>
            /// 比较函数
            /// </summary>
            /// <param name="x"></param>
            /// <param name="y"></param>
            /// <returns></returns>
            public int Compare(object x, object y)
            {
                return String.Compare(((ListViewItem)x).SubItems[col].Text, ((ListViewItem)y).SubItems[col].Text);
            }
        }

        /// <summary>
        /// 清空所有数据
        /// </summary>
        public void EmptyAllData()
        {
            lvwPreData.Items.Clear();
            lvwPostData.Items.Clear();
            lvwCmpData.Items.Clear();
            lvwCmpData.Groups.Clear();
        }

        /// <summary>
        /// 初始化名单数据
        /// </summary>
        /// <param name="preMembers"></param>
        /// <param name="postMembers"></param>
        public void SetMemberData(MemberInfo[] preMembers, MemberInfo[] postMembers)
        {
            EmptyAllData();

            //数据更新,UI暂时挂起
            this.lvwPreData.BeginUpdate();
            for (int i = 0; i < preMembers.Length; i++)   //添加10行数据 
            {
                if (preMembers[i] != null)
                {
                    ListViewItem lvi = new ListViewItem();
                    lvi.SubItems.Add(preMembers[i].MemCode);
                    lvi.SubItems.Add(preMembers[i].MemName);
                    lvi.SubItems.Add(preMembers[i].MemTitle);
                    this.lvwPreData.Items.Add(lvi);
                }
            }
            //结束数据处理,UI界面一次性绘制
            this.lvwPreData.EndUpdate();

            //数据更新,UI暂时挂起
            this.lvwPostData.BeginUpdate();
            for (int i = 0; i < postMembers.Length; i++)   //添加10行数据 
            {
                if (postMembers[i] != null)
                {
                    ListViewItem lvi = new ListViewItem();
                    lvi.SubItems.Add(postMembers[i].MemCode);
                    lvi.SubItems.Add(postMembers[i].MemName);
                    lvi.SubItems.Add(postMembers[i].MemTitle);
                    this.lvwPostData.Items.Add(lvi);
                }
            }
            //结束数据处理,UI界面一次性绘制
            this.lvwPostData.EndUpdate();

            //数据更新,UI暂时挂起
            this.lvwCmpData.BeginUpdate();

            //添加分组
            ListViewGroup lvgAdd = new ListViewGroup();
            lvgAdd.Header = "新加入成员";
            lvgAdd.Name = "add";
            lvgAdd.HeaderAlignment = HorizontalAlignment.Left;
            ListViewGroup lvgDelete = new ListViewGroup();
            lvgDelete.Header = "已删除成员";
            lvgDelete.Name = "delete";
            lvgDelete.HeaderAlignment = HorizontalAlignment.Left; 
            ListViewGroup lvgNoChange = new ListViewGroup();
            lvgNoChange.Header = "未变动成员";
            lvgNoChange.Name = "nochange";
            lvgNoChange.HeaderAlignment = HorizontalAlignment.Left;
            lvwCmpData.Groups.Add(lvgAdd);
            lvwCmpData.Groups.Add(lvgDelete);
            lvwCmpData.Groups.Add(lvgNoChange);
            lvwCmpData.ShowGroups = true; //显示分组

            //新增加成员
            IEnumerable<MemberInfo> memberAdd =
                from member1 in postMembers
                where !(from member2 in preMembers
                        where member1.MemCode == member2.MemCode
                        select member2).Any()
                select member1;
            foreach (MemberInfo member in memberAdd)
            {
                ListViewItem lvi = new ListViewItem();
                lvi.ForeColor = Color.Blue;
                lvi.SubItems.Add(member.MemCode);
                lvi.SubItems.Add(member.MemName);
                lvi.SubItems.Add(member.MemTitle);
                lvgAdd.Items.Add(lvi);
                this.lvwCmpData.Items.Add(lvi);
            }

            //已删除成员
            IEnumerable<MemberInfo> memberDelete =
                from member1 in preMembers
                where !(from member2 in postMembers 
                        where member1.MemCode == member2.MemCode 
                        select member2).Any()
                select member1;
            foreach (MemberInfo member in memberDelete)
            {
                ListViewItem lvi = new ListViewItem();
                lvi.ForeColor = Color.Red;
                lvi.SubItems.Add(member.MemCode);
                lvi.SubItems.Add(member.MemName);
                lvi.SubItems.Add(member.MemTitle);
                lvgDelete.Items.Add(lvi);
                this.lvwCmpData.Items.Add(lvi);
            }

            //未变动成员
            IEnumerable<MemberInfo> memberNoChange =
                from member1 in preMembers
                from member2 in postMembers
                where member1.MemCode == member2.MemCode
                select member1;
            foreach (MemberInfo member in memberNoChange)
            {
                ListViewItem lvi = new ListViewItem();
                lvi.ForeColor = Color.Black;
                lvi.SubItems.Add(member.MemCode);
                lvi.SubItems.Add(member.MemName);
                lvi.SubItems.Add(member.MemTitle);
                lvgNoChange.Items.Add(lvi);
                this.lvwCmpData.Items.Add(lvi);
            }

            //结束数据处理,UI界面一次性绘制
            this.lvwCmpData.EndUpdate();
        }
    }
}

本段代码有如下几点需要注意(可以理解为ListView控件的几个坑)

1、ListView最左侧的列,是无法设置对齐规则的,因此我将它设置成一个长度为0的列(即上面代码中的“僵尸列”),这一列的长度在ColumnWidthChanging事件中被指定为不能通过鼠标拉伸。对于用户而言,可以认为这一列是没有的。

2、将ListView的Scrollable设置成true后,可以做到列被拉伸超过ListView时会下方会自动出现滚动条,但如果窗口被打开时列的总长度就已经超出ListView的显示范围,滚动条则不会默认出现,而是需要拉动一下列才能出现。因此在代码中,需要找一列做一下调整,就像下面这行代码做的这样,这行代码看上去没有什么意义,却是为了规避ListView的一个缺陷。

//为ListView添加横向滚动条
chCmpMemTitle.Width = 110;

3、对ListView内数据进行分组后,单击分组标题会全选该组下数据,如果对该组下数据进行了着色,则着色会消失,这个问题需要重写ListView的WndProc方法,在第二节已有描述。

4、使用ListView内的BeginUpdate和EndUpdate函数可有效避免因不断刷新控件导致显示器上内容的闪动。

5、ListView的排序规则需自己指定,需实现一个继承自System.Collections.IComparer的类,放到ListView的ListViewItemSorter属性下。

五、步骤四:调用控件

在FormMain中添加一个Dock为Fill的控件MemberComparer,实现FormMain的Load函数,代码如下:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace NameListComparer
{
    public partial class FormMain : Form
    {
        public FormMain()
        {
            InitializeComponent();
        }

        private void FormMain_Load(object sender, EventArgs e)
        {
            MemberInfo[] preMemberInfo = new MemberInfo[] 
            {
                new MemberInfo("10001", "王伦", "白衣秀士"),
                new MemberInfo("10005", "吴用", "智多星"),
                new MemberInfo("10003", "宋江", "及时雨"),
                new MemberInfo("10004", "卢俊义", "玉麒麟"),
                new MemberInfo("10002", "晁盖", "托塔天王"),
                new MemberInfo("10007", "花荣", "小李广")
            };

            MemberInfo[] postMemberInfo = new MemberInfo[] 
            {
                new MemberInfo("10003", "宋江", "及时雨"),
                new MemberInfo("10004", "卢俊义", "玉麒麟"),
                new MemberInfo("10008", "张顺", "浪里白条"),
                new MemberInfo("10009", "周通", "小霸王"),
                new MemberInfo("10010", "时迁", "鼓上蚤"),
                new MemberInfo("10005", "吴用", "智多星"),
                new MemberInfo("10006", "林冲", "豹子头"),
                new MemberInfo("10007", "花荣", "小李广")
            };

            cmpMembers.SetMemberData(preMemberInfo, postMemberInfo);
        }
    }
}

这段代码运行的效果,和本文一开始的那张图片是一样的。

六、参考资料

1、ListView的使用方法,我参考了这篇博客

http://blog.csdn.net/xiaohan2826/article/details/8603015

2、禁用ListView下分组标题栏的单击全选功能,我参考了下面的资料

http://stackoverflow.com/questions/10532039/listview-group-header-click-disable-select-all-in-windows-7

3、另一个资料是为分组标题栏添加单击事件的,资料2的答题者就参考了它

https://social.msdn.microsoft.com/Forums/windows/en-US/eccdf58a-dd06-4ae3-908d-5f5863c01d64/listviewgroupclicked-event?forum=winforms

七、附言

1、我个人感觉.NET中的ListView控件并不好驾驭,不是因为使用它的规则有多难,而是因为这个控件的坑比较多

2、本文中程序的一个DEMO可以在这个地址下载到:http://pan.baidu.com/s/1qWRVrac

3、我的Windows版本为Win7旗舰版,VS版本为2012,编译目标框架为.NET Framework 4

END

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!