带可禁用项的 ListBox






3.50/5 (4投票s)
提供带可禁用项的 ListBox 的代码。

引言
最近我需要创建一个应用程序,其中某些功能会根据客户的不同而启用或禁用。其中一组功能包含在一对 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
,以防您想自己修改 Items
和 ItemEnables
列表。
现在是激动人心的部分。版本 1.1 支持数据绑定。该控件公开了一个 EnableMember 属性,其功能与 DisplayMember 和 ValueMember 相同。EnableMember 将使用属性中的值来确定项是否已启用。它使用 Convert.ToBoolean
,因此它可以处理数值类型和字符串。如果您在数据绑定时未设置 EnableMember,则所有项将默认为启用。以下是 EnableMember
或 DataSource
更改时运行的代码:
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;
}
我还必须处理数据源中的项更改,这是通过注册 ListBox
的 DataManager
(实际上是 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
来完成的。我决定公开两个属性:EnabledItemColor
和 DisabledItemColor
来设置项的文本颜色。它们在构造函数中分别默认为黑色和灰色。我还添加了代码来处理 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”是 DrawMode
、SelectionMode
和 Sorted
属性已被隐藏。DrawMode
已被隐藏,因为将其更改为 Normal
时看不到禁用的项,而且如果您要将其更改为 OwnerDrawVariable
,则还需要更改 OnDrawItem
代码,因此届时您可以取消隐藏它,如果您想在 OwnerDrawFixed
和 OwnerDrawVariable
之间切换。
Sorted
未实现,因为为了保持两个列表(Items
和 ItemEnables
)的同步,我需要在类中编写自定义排序,这似乎有点浪费。而且我怀疑有多少人会经常使用它。如果有人希望添加它,请评论,我们再看。
SelectionMode
已被隐藏,因为多选选项会造成很多麻烦。我不知道如何处理具有中间禁用项的 Shift+Click,因此与 Sorted
一样,除非收到请求,否则我不会实现此功能。
历史
- 2009 年 7 月 29 日
- 更新至 v1.2
- 添加了
EnableMemberError
事件,当EnableMember
的值无法转换为布尔值时会触发该事件,并提供异常信息和项的索引。 - Bug 修复:构造函数后添加空数据源会引发异常。
- Bug 修复:更新绑定的数据源会引发异常。
- Bug 修复:Page Up/Down 和 Home/End 允许选择禁用的项。
- Bug 预防:禁止在这些功能实现之前更改
SelectionMode
和Sorted
。 - 2009 年 7 月 17 日
- 更新至 v1.1
- 添加了数据绑定和更好的绘制效果。
- 2009 年 7 月 10 日
- 提交文章。