C# - 使用自定義控件實現名單修改的比較功能

1、寫在前面

在工做中須要實現一個場景,有一個名單類的數據須要維護,這個維護工做須要有一個複覈功能,爲了方便複覈時對名單變動狀況有一個良好的掌握,須要作一個便跟先後名單的對比功能。c#

功能實現後效果以下圖:
windows

其中,修改前名單、修改後名單、先後名單對比三個部分都使用了封裝後的ListView控件保存數據
框架

2、步驟一:封裝ListView

封裝ListView主要是爲了保證對「先後名單對比」部分數據的着色。雖然微軟原生的ListView就支持了對數據項進行着色,但由於「先後名單對比」部分使用了分組功能,點擊分組標題時,默認選中分組內的所有數據,這回致使分組內的數據顏色都變爲黑色。爲改變這一狀況,咱們須要建立一個繼承ListView的類ListViewEnhanced,代碼以下:ide

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);
        }
    }
}

3、步驟二:創建存放單個名單的對象

存放單個名單的對象,可根據業務系統自身狀況量身定製,下面代碼是一個我實現的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;
            }
        }
    }
}

4、步驟三:建立自定義控件

建立一個繼承UserControl的自定義控件MemberComparer,以下圖所示:
post

三個ListView的View屬性,都要設置成System.Windows.Forms.View.Detailsui

MemberComparer控件的代碼以下:this

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控件的幾個坑)編碼

一、ListView最左側的列,是沒法設置對齊規則的,所以我將它設置成一個長度爲0的列(即上面代碼中的「殭屍列」),這一列的長度在ColumnWidthChanging事件中被指定爲不能經過鼠標拉伸。對於用戶而言,能夠認爲這一列是沒有的。spa

二、將ListView的Scrollable設置成true後,能夠作到列被拉伸超過ListView時會下方會自動出現滾動條,但若是窗口被打開時列的總長度就已經超出ListView的顯示範圍,滾動條則不會默認出現,而是須要拉動一下列才能出現。所以在代碼中,須要找一列作一下調整,就像下面這行代碼作的這樣,這行代碼看上去沒有什麼意義,倒是爲了規避ListView的一個缺陷。

//爲ListView添加橫向滾動條
chCmpMemTitle.Width = 110;

三、對ListView內數據進行分組後,單擊分組標題會全選該組下數據,若是對該組下數據進行了着色,則着色會消失,這個問題須要重寫ListView的WndProc方法,在第二節已有描述。

四、使用ListView內的BeginUpdate和EndUpdate函數可有效避免因不斷刷新控件致使顯示器上內容的閃動。

五、ListView的排序規則需本身指定,需實現一個繼承自System.Collections.IComparer的類,放到ListView的ListViewItemSorter屬性下。

5、步驟四:調用控件

在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);
        }
    }
}

這段代碼運行的效果,和本文一開始的那張圖片是同樣的。

6、參考資料

一、ListView的使用方法,我參考了這篇博客

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

二、禁用ListView下分組標題欄的單擊全選功能,我參考了下面的資料

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

三、另外一個資料是爲分組標題欄添加單擊事件的,資料2的答題者就參考了它

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

7、附言

一、我我的感受.NET中的ListView控件並很差駕馭,不是由於使用它的規則有多難,而是由於這個控件的坑比較多

二、本文中程序的一個DEMO能夠在這個地址下載到:http://pan.baidu.com/s/1qWRVrac

三、個人Windows版本爲Win7旗艦版,VS版本爲2012,編譯目標框架爲.NET Framework 4

END

相關文章
相關標籤/搜索