[Windows Forms] 自己畫 ListView (1)

  • 19568
  • 0

偶然看到一個問題,『Listview 預設選取某行』, 內容大意是說如何當 ListView.Focused = fasle 的狀態下,還能夠保持反白。

最簡單的解法,當然就是 Hideselection = false,但這樣在失去焦點的狀態背景會變成很淺的灰色,老人家眼睛不好可能會看不出來。

另外一個解法,就是自己畫那些 ListViewItem,其實也不是太難,我們一步步來做這個簡單的試驗。

1. 先建立一個簡單的 Form

就擺上一個 ListView 、Button、TextBox 以及 CheckBox,主角是 ListView,其他只是為了測試焦點而已。

2. 初始化 ListView

為了避免講不清楚,我們不在設計畫面上調整屬性,直接寫一個 InitialListView() method 來初始化設定與資料。
 

private void InitialListView()
{
    listView1.View = View.Details;
    listView1.GridLines = true;
    listView1.LabelEdit = false;
    listView1.FullRowSelect = true;
    listView1.Columns.Add("編號", 100);
    listView1.Columns.Add("測試文字", 100);
    for (int i = 0; i < 10; i++)
    {
        var item = new ListViewItem($"No.{i}");
        item.SubItems.Add($"文字{i}");
        listView1.Items.Add(item);
    }
}

然後在 Form 的建構式中呼叫它
 

public Form1()
{
    InitializeComponent();
    InitialListView();
}

接著執行這個程式,先隨便在 ListView 選擇一個 Row

然後把焦點移向另外一個控制項,比方 Button,你會發現原來被選擇的那一項背景又恢復成預設的樣式。

3. 修改 InitialListView()

為了達成在失去焦點時還能保持背景樣式,我們得要自己畫 ListView 上的項目。第一個步驟是設定 ListView 的 OwnerDraw 屬性為 true,這表示我們要自己畫;一旦設定了這個屬性為 true,除了項目以外,Column Header 也得自己畫。我們需要處理兩個 Draw 的行為,(1) 當 DrawSubItem 事件被引發時會呼叫相對應的事件委派函式畫 SubItems (2) 當 DrawColumnHeader 事件被引發的時候會呼叫相對應的事件委派函式畫 Column Header。因此將 InitialListView() 修改如下:

private void InitialListView()
{
    listView1.View = View.Details;
    listView1.GridLines = true;
    listView1.LabelEdit = false;
    listView1.FullRowSelect = true;
    listView1.Columns.Add("編號", 100);
    listView1.Columns.Add("測試文字", 100);
    for (int i = 0; i < 10; i++)
    {
        var item = new ListViewItem($"No.{i}");
        item.SubItems.Add($"文字{i}");
        listView1.Items.Add(item);
    }
    listView1.OwnerDraw = true;
    listView1.DrawSubItem += ListView1_DrawSubItem;
    listView1.DrawColumnHeader += ListView1_DrawColumnHeader;
}
4. 撰寫 ListView1_DrawColumnHeader

這一篇先不管 Column Header,因此維持原來系統的渲染方式,只要寫下 e.DrawDefault = true; 即可。
 

private void ListView1_DrawColumnHeader(object sender, DrawListViewColumnHeaderEventArgs e)
{
    e.DrawDefault = true;
}
5. 撰寫 ListView1_DrawSubItem

公堂上先假設一下,當一個列被選擇的時候,背景是 SkyBlue,文字顏色是白色;當列不在選擇狀態的時候,恢復原設定背景,文字為黑色。
 

private void ListView1_DrawSubItem(object sender, DrawListViewSubItemEventArgs e)
{
    if (e.Item.Selected)
    {
        e.Graphics.FillRectangle(new SolidBrush(Color.SkyBlue), e.Bounds);
        e.Graphics.DrawString(e.Item.Text, listView1.Font, new SolidBrush(Color.White), e.Bounds.Location);
    }
    else
    {
        e.DrawBackground();
        e.Graphics.DrawString(e.Item.Text, listView1.Font, new SolidBrush(Color.Black), e.Bounds.Location);         
    }
}

請注意,一定要先畫背景再畫文字,否則你的字會被蓋住。執行結果如下圖:

6. 讓 ListView 的選擇項目在 ListView 取得焦點和不在焦點上的時候展現不同的背景色

接著,我們希望當 ListView 沒有取得焦點的時候,背景變成紅色,該怎麼修改 ListView1_DrawSubItem 方法呢 ? 其實很簡單,只要判斷 ListView.Focused 屬性做不同的處理即可。

private void ListView1_DrawSubItem(object sender, DrawListViewSubItemEventArgs e)
{
    if (e.Item.Selected)
    {
        if (((ListView)sender).Focused)
        {
            e.Graphics.FillRectangle(new SolidBrush(Color.SkyBlue), e.Bounds);
        }
        else
        {
            e.Graphics.FillRectangle(new SolidBrush(Color.Red), e.Bounds);
        }
        e.Graphics.DrawString(e.Item.Text, listView1.Font, new SolidBrush(Color.White), e.Bounds.Location);
    }
    else
    {
        e.DrawBackground();
        e.Graphics.DrawString(e.Item.Text, listView1.Font, new SolidBrush(Color.Black), e.Bounds.Location);
    }
}

這樣就完成了一個簡易的自訂 ListView 渲染行為,很簡單吧。
程式碼範例位於 https://github.com/billchungiii/DrawListViewSamples