C#自定义列表框GUI

5
我有一个课程列表,但不同的子项有不同需要显示的属性。
我想实现的是在GUI中拥有一个类似于列表框的控件,使每个子项都可以按照自己的方式显示它的属性-而不是为每个类使用相同预定义的列。
我设想的是像传输接口(下图)一样,每个类都可以绘制自己的条目,显示一些文本、进度条(如果相关)、等等。
如何在C#中实现这一点?
感谢您的帮助。

2
Silverlight,WPF还是WinForms? - Daniel A. White
3
C# 不是 GUI,你使用什么 GUI 工具包?WinForms?WPF?SilverLight? - Albin Sunnanbo
2个回答

18

让您的列表项实现一个接口,该接口提供了显示所需的一切:

public interface IDisplayItem
{
    event System.ComponentModel.ProgressChangedEventHandler ProgressChanged;
    string Subject { get; }
    string Description { get; }
    // Provide everything you need for the display here
}

传输对象不应该自己显示。您不应将领域逻辑(业务逻辑)和显示逻辑混合使用。

自定义 ListBox: 为了以自己的方式显示列表框项,您需要从System.Windows.Forms.ListBox派生自己的列表框控件。在构造函数中将您的列表框的DrawMode属性设置为DrawMode.OwnerDrawFixedDrawMode.OwnerDrawVariable(如果项目大小不相同)。如果使用 OwnerDrawVariable,则还需要覆盖OnMeasureItem,以告诉列表框每个项目的大小。

public class TransmissionListBox : ListBox
{
    public TransmissionListBox()
    {
        this.DrawMode = DrawMode.OwnerDrawFixed;
    }

    protected override void OnDrawItem(DrawItemEventArgs e)
    {
        e.DrawBackground();
        if (e.Index >= 0 && e.Index < Items.Count) {
            var displayItem = Items[e.Index] as IDisplayItem;
            TextRenderer.DrawText(e.Graphics, displayItem.Subject, e.Font, ...);
            e.Graphics.DrawIcon(...);
            // and so on
        }
        e.DrawFocusRectangle();
    }
}

你可以让原始的传输类实现 IDisplayItem 接口,或者创建一个专门用于此目的的类。只要它们实现了接口,列表中也可以有不同类型的对象。关键是显示逻辑本身由控制类负责,传输类(或其他类)只提供所需信息。

例子: 由于与马克的持续讨论,我决定在这里包含一个完整的示例。让我们定义一个模型类:

public class Address : INotifyPropertyChanged
{
    private string _Name;
    public string Name
    {
        get { return _Name; }
        set
        {
            if (_Name != value) {
                _Name = value;
                OnPropertyChanged("Name");
            }
        }
    }

    private string _City;
    public string City
    {
        get { return _City; }
        set
        {
            if (_City != value) {
                _City = value;
                OnPropertyChanged("City");
                OnPropertyChanged("CityZip");
            }
        }
    }

    private int? _Zip;
    public int? Zip
    {
        get { return _Zip; }
        set
        {
            if (_Zip != value) {
                _Zip = value;
                OnPropertyChanged("Zip");
                OnPropertyChanged("CityZip");
            }
        }
    }

    public string CityZip { get { return Zip.ToString() + " " + City; } }

    public override string ToString()
    {
        return Name + "," + CityZip;
    }

    #region INotifyPropertyChanged Members

    public event PropertyChangedEventHandler PropertyChanged;

    private void OnPropertyChanged(string propertyName)
    {
        var handler = PropertyChanged;
        if (handler != null) {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    #endregion
}

这是一个自定义的 ListBox:

public class AddressListBox : ListBox
{
    public AddressListBox()
    {
        DrawMode = DrawMode.OwnerDrawFixed;
        ItemHeight = 18;
    }

    protected override void OnDrawItem(DrawItemEventArgs e)
    {
        const TextFormatFlags flags = TextFormatFlags.Left | TextFormatFlags.VerticalCenter;

        if (e.Index >= 0) {
            e.DrawBackground();
            e.Graphics.DrawRectangle(Pens.Red, 2, e.Bounds.Y + 2, 14, 14); // Simulate an icon.

            var textRect = e.Bounds;
            textRect.X += 20;
            textRect.Width -= 20;
            string itemText = DesignMode ? "AddressListBox" : Items[e.Index].ToString();
            TextRenderer.DrawText(e.Graphics, itemText, e.Font, textRect, e.ForeColor, flags);
            e.DrawFocusRectangle();
        }
    }
}

在一个表单中,我们放置了这个AddressListBox和一个按钮。在表单中,我们放置了一些初始化代码和一些按钮代码,用来改变我们的地址。我们这样做是为了看到我们的列表框是否会自动更新:

public partial class frmAddress : Form
{
    BindingList<Address> _addressBindingList;

    public frmAddress()
    {
        InitializeComponent();

        _addressBindingList = new BindingList<Address>();
        _addressBindingList.Add(new Address { Name = "Müller" });
        _addressBindingList.Add(new Address { Name = "Aebi" });
        lstAddress.DataSource = _addressBindingList;
    }

    private void btnChangeCity_Click(object sender, EventArgs e)
    {
        _addressBindingList[0].City = "Zürich";
        _addressBindingList[1].City = "Burgdorf";
    }
}

当按钮被点击时,AddressListBox中的项目会自动更新。请注意,仅定义了ListBox的DataSource。DataMember和ValueMember为空。


我不太明白。你是指创建一个专门的类来处理显示,绑定到列表项,并可添加到列表框控件中吗?那么是否有可能显示不同属性的不同项?你有任何链接或示例可进一步解释这个问题吗?谢谢! - Mark
通过检查我的示例代码,我注意到它是错误的。不需要循环,因为每个项目都会调用一次OnDrawItem。我编辑了示例并用索引检查替换了循环。 - Olivier Jacot-Descombes
1
在WinForms中,您可以通过调用Invalidate()方法(或其重载之一)来触发控件的重绘。例如:myListBox.Invalidate();IDisplayItem接口可以具有一个float Progress { get; }属性,该属性公开传输的当前状态。每当IDisplayItem公开的属性发生更改时,您都必须使列表框失效。另一种方法是使用数据绑定。将列表框绑定到对象绑定源,并在显示的项中实现INotifyPropertyChanged。数据绑定的神奇之处就在于它会自动让列表框显示更改! - Olivier Jacot-Descombes
1
我进行了一些测试。如果使用 System.ComponentModel.BindingList<T>,它似乎可以更好地工作。在我的测试中,我将 List<T> 转换为绑定列表:var bindingList = new BindingList<Address>(addressList); addressBindingSource.DataSource = bindingList;。我没有设置 Display 或 ValueMember。 - Olivier Jacot-Descombes
1
好的,我之前的陈述是基于一段代码的,而那段代码还做了其他事情,这导致了混淆。我决定制作一个完整的例子来创建明确的事实(请参见编辑#2)。Mark 是正确的,如果使用 BindingList<T>,则不需要 BindingSource。该示例仅使用了 BindingList<T> - Olivier Jacot-Descombes
显示剩余7条评论

1

很遗憾,我正在使用WinForms。抱歉在我的原始帖子中没有说明。但是这些链接对于WPF看起来非常棒 - 正是我想要做的事情。 - Mark

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接