Listview in Unity

14Screen

I’ve grown very attached to the many possibilities of the ListView control from the Windows Forms library in .NET and decided to try and make something similar in Unity using the standard GUI system.

So without further ado, the code:

ListView.cs

<pre>using UnityEngine;
using System.Collections.Generic;
using System.Linq;

public class ListView<T>
    {
    public List<T> Items = new List<T>();
    public List<ListViewColumn<T>> Columns = new List<ListViewColumn<T>>();
    public int Selected = -1;
    public T SelectedItem 
    {
        get 
        {
            return Selected>-1?Items[Selected]:default(T);
        }
        set 
        {
            Selected = Items.IndexOf(value);
        }
    }
    private float scroll = 0;
    public Rect Rect;

    public ListView(Rect position)
        {
        Rect = position;
        }

    public void Draw(Vector2 offset)
        {
        if (Items.Count==0)
        {
            Selected = -1;
        }
        var mouse = new Vector2(Input.mousePosition.x - offset.x, (Screen.height - Input.mousePosition.y) - offset.y);
        GUI.Box(Rect, "");
        var useScroll = Rect.height - 24 < Items.Count * 24;
        var s = Columns.Sum(x => x.Width);
        var w = Columns.Select(x => x.Width * (Rect.width - (useScroll ? 16 : 0)) / s).ToArray();
        var ws = 0f;
        for (int i = 0; i < Columns.Count; i++)
            {
            if (Columns[i].IsAction)
                {
                GUI.Box(new Rect(Rect.x + ws, Rect.y, w[i], 24), Columns[i].Name);
                }
            else
                {
                if (GUI.Button(new Rect(Rect.x + ws, Rect.y, w[i], 24), Columns[i].Name))
                    {
                    Selected = Columns[i].Sort(Items, Selected);
                    }
                }
            ws += w[i];
            }
        if (Columns.Count == 0)
            {
            return;
            }
        var count = (int)Mathf.Floor((Rect.height - 24) / 24);
        for (int i = 0; i < count; i++)
            {
            var ii = i + (int)scroll;
            if (ii >= Items.Count)
                {
                break;
                }
            int j = 0;
            ws = 0;
            foreach (var column in Columns)
                {
                if (column.IsAction)
                    {
                    if (GUI.Button(new Rect(Rect.x + ws, Rect.y + 24 + i * 24, w[j], 24), column.ActionName))
                        {
                        column.Action(Items[ii]);
                        }
                    }
                else
                    {
                    GUI.Box(new Rect(Rect.x + ws, Rect.y + 24 + i * 24, w[j], 24), column.Output(Items[ii]));
                    }
                ws += w[j];
                j++;
                }
            if (Input.GetMouseButtonUp(0) &&
                mouse.x >= Rect.xMin &&
                mouse.x <= Rect.xMax - (useScroll ? 16 : 0) &&
                mouse.y >= Rect.y + 24 + i * 24 &&
                mouse.y <= Rect.y + 48 + i * 24)
                {
                Selected = ii;
                }
            }
        if (useScroll)
            {
            scroll = Mathf.Round(
                    GUI.VerticalScrollbar(
                    new Rect(Rect.x + Rect.width - 16, Rect.y, 16, Rect.height),
                    scroll,
                    1,
                    0,
                    Items.Count - count + 1));
            if (scroll < 0)
                {
                scroll = 0;
                }
            }
        if (Selected >= scroll && Selected < scroll+count)
            {
            var i = Selected - scroll;
            GUI.Box(new Rect(Rect.x, Rect.y + 24 + i * 24, Rect.width - (useScroll ? 16 : 0), 24), "");
            }
        }
    }
</pre>

ListViewColumn.cs

<pre>using UnityEngine;
using System.Collections.Generic;
using System.Linq;

public class ListViewColumn<T>
    {
    public readonly bool IsAction;
    public string Name;
    public readonly string ActionName;
    public readonly System.Action<T> Action;
    public readonly System.Func<T, string> Output;
    private readonly System.Comparison<T> Compare;
    public float Width;
    public bool SortAsc = true;

    public ListViewColumn(string name, string actionName, System.Action<T>  action, float w = 1)
        {
        Name = name;
        ActionName = actionName;
        Action = action;
        IsAction = true;
        Width = w;
        }

    public ListViewColumn(string name, System.Func<T, string> output, System.Comparison<T> compare, float w = 1)
        {
        Name = name;
        Output = output;
        IsAction = false;
        Width = w;
        Compare = compare;
        }

    public int Sort(List<T> input, int i)
        {
        T obj = default(T);
        if (i != -1)
            {
            obj = input[i];
            }
        input.Sort(Compare);
        if (!SortAsc)
            {
            input.Reverse();
            }
        SortAsc = !SortAsc;
        return i == -1 ? -1 : input.IndexOf(obj);
        }
    }
</pre>

So the way it works is that you create a new ListView, which is a generic class, so you need to specify a type.

You then add columns, which can either be descriptive or actions, depending on which constructor you invoke. Descriptive columns take an anonymous function that translates elements in the list into a string to display and a Comparison function that allows you to sort the column. Action columns take an anonymous function that does something with the selected item, this can also be handled by using selection and separate buttons, of course. Each column also takes an optional width parameter, which sets its width ratio compared to other elements. So if you have two columns with widths 3 and 1, the first column will fill 75% of the ListView area and the second will fill 25%.

Lastly, call the draw function in the OnGUI method or in a window content method. The offset argument should be either zero or the top left corner of the window it resides in. This is because I handle the selection of items manually.

An example of use:

<pre>
People = new ListView<PeopleClass>(new Rect(0, 0, 256, 256));
People.Columns.Add(
       new ListViewColumn<PeopleClass>("Name",
                                       x => x.Name,
                                       (x,y) => x.Name.CompareTo(y.Name), 2));
People.Columns.Add(
       new ListViewColumn<PeopleClass>("Income",
                                      x => x.Income.ToString("N"),
                                      (x,y) => x.Income.CompareTo(y.Income)));
People.Columns.Add(
       new ListViewColumn<PeopleClass>("Action",
                                      "Kill",
                                      x => Population.Kill(x)));
People.Items.AddRange(MyPeople);
</pre>