65.9K
CodeProject 正在变化。 阅读更多。
Home

带可禁用项的 ListBox

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.50/5 (4投票s)

2009年7月10日

CPOL

4分钟阅读

viewsIcon

52954

downloadIcon

507

提供带可禁用项的 ListBox 的代码。

Demo project in action.

引言

最近我需要创建一个应用程序,其中某些功能会根据客户的不同而启用或禁用。其中一组功能包含在一对 ListBoxes 中,但我找不到任何方法来禁用列表中的某些项。我决定创建自己的类,继承自 ListBox,这就是我最终得到的结果。

现在支持数据绑定!事实上,这并没有我想象的那么复杂。更多信息如下。

Using the Code

DisableListBox 包含一个布尔值列表 ListEnables,它定义了 Items 中的哪些项是启用和禁用的。ListEnables[i] 为 true 表示 Items[i] 已启用。我提供了添加/插入/删除/删除特定项和清空项的函数,因此您无需担心使两个列表保持同步。

public void AddItem(object item, bool enabled);

public void InsertItem(int index, object item, bool enabled);

public void RemoveItem(object item);

public void RemoveItemAt(int index);

public void ClearItems();

现在,如果您想在添加项后启用或禁用它们,我创建了一些简单的方​​法来处理这个问题。

public void EnableItem(object item);

public void EnableItemAt(int index);

public void DisableItem(object item);

public void DisableItemAt(int index);

我仍然暴露了 ItemEnables,以防您想自己修改 ItemsItemEnables 列表。

现在是激动人心的部分。版本 1.1 支持数据绑定。该控件公开了一个 EnableMember 属性,其功能与 DisplayMember 和 ValueMember 相同。EnableMember 将使用属性中的值来确定项是否已启用。它使用 Convert.ToBoolean,因此它可以处理数值类型和字符串。如果您在数据绑定时未设置 EnableMember,则所有项将默认为启用。以下是 EnableMemberDataSource 更改时运行的代码:

private void RefreshItemEnables()
{
    // Get enable property
    GetEnableProperty();

    // Clear enable list
    ItemEnables.Clear();

    // Fill enable list
    for (int i = 0; i < Items.Count; i++)
        ItemEnables.Add(ProduceEnable(i));

    // Ensure disabled items are not selected
    for (int i = SelectedItems.Count - 1; i >= 0; i--)
        if (!ItemEnables[SelectedIndices[i]])
            SelectedItems.Remove(SelectedItems[i]);
}

private void GetEnableProperty()
{
    // If it should be bound to a property
    if (DataSource != null && EnableMember != string.Empty)
    {
        // Clear property
        enableProperty = null;

        // Find property
        foreach (PropertyDescriptor property in DataManager.GetItemProperties())
            if (property.Name == enableMember)
                enableProperty = property;
    }
}

private bool ProduceEnable(int i)
{
    // If databound and enable property is set
    if (DataSource != null && enableProperty != null)
        try
        {
            // Convert property to boolean
            return Convert.ToBoolean(enableProperty.GetValue(Items[i]));
        }
        // Object couldn't be converted to boolean
        catch (InvalidCastException)
        {
            return false;
        }
    else
        return true;
}

我还必须处理数据源中的项更改,这是通过注册 ListBoxDataManager(实际上是 CurrencyManager)事件来完成的。

void DataManager_ListChanged(object sender, ListChangedEventArgs e)
{
    switch (e.ListChangedType)
    {
        // Handle items being added
        case ListChangedType.ItemAdded:
            ItemEnables.Insert(e.NewIndex, ProduceEnable(e.NewIndex));
            break;
        // Handle items being deleted
        case ListChangedType.ItemDeleted:
            ItemEnables.RemoveAt(e.NewIndex);
            break;
    }
}

void DataManager_ItemChanged(object sender, ItemChangedEventArgs e)
{
    // Handle items changing
    if (e.Index > -1)
        SetEnabledAt(e.Index, ProduceEnable(e.Index));
}

大部分工作都花在了确保禁用项无法被选中。为此,我必须重写 WndProc 并捕获以下消息:

// Page Up/Down
private const int VK_PRIOR = 0x21;
private const int VK_NEXT = 0x22;

// End/Home
private const int VK_END = 0x23;
private const int VK_HOME = 0x24;

// Arrow keys
private const int VK_LEFT = 0x25;
private const int VK_UP = 0x26;
private const int VK_RIGHT = 0x27;
private const int VK_DOWN = 0x28;

private const int WM_KEYDOWN = 0x100;
private const int WM_MOUSEMOVE = 0x200;
private const int WM_LBUTTONDOWN = 0x201;

首先,我处理了鼠标选择。

// Intercept mouse selection
if (m.Msg == WM_MOUSEMOVE || m.Msg == WM_LBUTTONDOWN)
{
    // Get mouse location
    Point clickedPt = new Point();
    clickedPt.X = lParam & 0x0000FFFF;
    clickedPt.Y = lParam >> 16;

    // If point is on a disabled item, ignore mouse
    for (int i = 0; i < Items.Count; i++)
        if (!ItemEnables[i] && GetItemRectangle(i).Contains(clickedPt))
            return;
}

然后是键盘选择(为简洁起见,我只展示了一半的代码)。

// Intercept keyboard selection
if (m.Msg == WM_KEYDOWN)
    // Handle single down
    if (wParam == VK_DOWN || wParam == VK_RIGHT)
    {
        // Select next enabled item
        for (int i = SelectedIndex + 1; i < Items.Count; i++)
            if (ItemEnables[i])
            {
                SelectedIndex = i;
                break;
            }

        return;
    }
    // Handle single up
    else if (wParam == VK_UP || wParam == VK_LEFT)
    {
        ...
    }
    // Handle page up
    else if (wParam == VK_PRIOR)
    {
        // Ignore if empty
        if (ItemEnables.Count == 0)
            return;

        // Get current selected index
        int currentIndex = Math.Max(0, SelectedIndex);

        // Get number of items to jump
        int toJump = NumVisibleItems() - 1;

        // Check if there are enough items to jump a full page
        if (currentIndex >= toJump)
        {
            // Jump at least a full page if possible
            for (int i = currentIndex - toJump; i >= 0; i--)
                if (ItemEnables[i])
                {
                    SelectedIndex = i;
                    return;
                }
        }
        // If there aren't enough items, try to jump as far as possible
        else
            toJump = currentIndex;

        // Jump as far as possible without ending on a disabled item
        for (int i = currentIndex - toJump; i <= currentIndex; i++)
            if (ItemEnables[i])
            {
                SelectedIndex = i;
                break;
            }

        return;
    }
    // Handle page down
    else if (wParam == VK_NEXT)
    {
        ...
    }
    // Handle end
    else if (wParam == VK_END)
    {
        // Select closest enabled item to end
        for (int i = ItemEnables.Count - 1; i >= 0; i--)
            if (ItemEnables[i])
            {
                SelectedIndex = i;
                break;
            }

        return;
    }
    // Handle home
    else if (wParam == VK_HOME)
    {
        ...
    }

最后的任务是处理禁用项的绘制。这是通过在构造函数中设置 DrawMode = DrawMode.OwnerDrawFixed; 后重写 OnDrawItem 来完成的。我决定公开两个属性:EnabledItemColorDisabledItemColor 来设置项的文本颜色。它们在构造函数中分别默认为黑色和灰色。我还添加了代码来处理 RightToLeft 设置,并确保其显示效果与 ListBox 完全相同。

protected override void OnDrawItem(DrawItemEventArgs e)
{
    // Stops control from throwing errors if empty or in design mode
    if (e.Index > -1 && !suspendDraw && !IsDesignMode())
    {
        // Draw the background
        e.DrawBackground();

        // Select color to use
        Color color;
        if (Enabled && ItemEnables[e.Index])
            if ((e.State & DrawItemState.Selected) == DrawItemState.Selected)
                color = Color.White;
            else
                color = EnabledItemColor;
        else
            color = DisabledItemColor;

        // Align text
        Rectangle shiftedBounds;
        TextFormatFlags alignment;
        if (base.RightToLeft == RightToLeft.No)
        {
            // To look the same as ListBox, the bounds have to be shifted
            shiftedBounds = new Rectangle(e.Bounds.X - 1, e.Bounds.Y, e.Bounds.Width,
                e.Bounds.Height);
            alignment = TextFormatFlags.Left;
        }
        else
        {
            // To look the same as ListBox, the bounds have to be shifted
            shiftedBounds = new Rectangle(e.Bounds.X + 2, e.Bounds.Y, e.Bounds.Width,
                e.Bounds.Height);
            alignment = TextFormatFlags.Right;
        }

        // Get string to display
        string displayString = GetItemText(Items[e.Index]);

        // Draw the string
        TextRenderer.DrawText(e.Graphics, displayString, e.Font, shiftedBounds, color,
            alignment);

        // Draw the focus rectangle
        e.DrawFocusRectangle();
    }

    // Call base OnDrawItem
    base.OnDrawItem(e);
}

关注点

我花了很多时间来尝试让 DisableListBox 的名称在设计模式下显示,就像 ListBox 一样。基本上,这需要将 DrawMode 设置为设计器中的 Normal,而在其他情况下为 OwnerDrawFixed。以下是处理此问题的所有代码:

// Set to normal so name shows up in design mode
private DrawMode drawMode = DrawMode.Normal;

/// <summary>
/// Gets or sets the drawing mode for the control.
/// </summary>
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
public override DrawMode DrawMode
{
    get { return drawMode; }
    set
    {
        drawMode = value;

        // Keeps base.DrawMode set to Normal so name shows up in the designer
        if (!IsDesignMode()) base.DrawMode = value;
    }
}

/// <summary>
/// Initializes a new instance of the DisableListBox class.
/// </summary>
public DisableListBox()
{
    DrawMode = DrawMode.OwnerDrawFixed;
}

private bool IsDesignMode()
{
    return DesignMode || LicenseManager.UsageMode == LicenseUsageMode.Designtime;
}

IsDesignMode() 方法是存在的,因为 DesignMode 有时不太好用。您可以在 此处 阅读更多相关信息。

特别感谢 MSDN 论坛上的 Hans Passant (nobugz) 和 Nishant Sivakumar 在此过程中给予的帮助。

已知 Bug(s)

在数据绑定时更改 EnableMember 会导致已启用的项变为禁用项。如果先前启用的项是选中的项,则会被取消选中。不幸的是,CurrencyManager(负责数据绑定)没有“未选中”的位置。如果您在选择其他项之前更改了数据源,列表将刷新,并且禁用的项将被选中。

我不知道如何解决这个问题,因为总有可能所有项都被禁用,所以我无法将其设置为另一个项而不是取消选择。此外,在这种情况下,当项被重新选中时,OnSelectedIndexChanged 事件甚至不会触发。我对此感到束手无策,欢迎任何建议。

其他“Bug”是 DrawModeSelectionModeSorted 属性已被隐藏。DrawMode 已被隐藏,因为将其更改为 Normal 时看不到禁用的项,而且如果您要将其更改为 OwnerDrawVariable,则还需要更改 OnDrawItem 代码,因此届时您可以取消隐藏它,如果您想在 OwnerDrawFixedOwnerDrawVariable 之间切换。

Sorted 未实现,因为为了保持两个列表(ItemsItemEnables)的同步,我需要在类中编写自定义排序,这似乎有点浪费。而且我怀疑有多少人会经常使用它。如果有人希望添加它,请评论,我们再看。

SelectionMode 已被隐藏,因为多选选项会造成很多麻烦。我不知道如何处理具有中间禁用项的 Shift+Click,因此与 Sorted 一样,除非收到请求,否则我不会实现此功能。

历史

  • 2009 年 7 月 29 日
    • 更新至 v1.2
    • 添加了 EnableMemberError 事件,当 EnableMember 的值无法转换为布尔值时会触发该事件,并提供异常信息和项的索引。
    • Bug 修复:构造函数后添加空数据源会引发异常。
    • Bug 修复:更新绑定的数据源会引发异常。
    • Bug 修复:Page Up/Down 和 Home/End 允许选择禁用的项。
    • Bug 预防:禁止在这些功能实现之前更改 SelectionModeSorted
  • 2009 年 7 月 17 日
    • 更新至 v1.1
    • 添加了数据绑定和更好的绘制效果。
  • 2009 年 7 月 10 日
    • 提交文章。
© . All rights reserved.