拖放式 ListBox
DragDropListBox,一个派生自ListBox的控件,允许在多选模式下进行拖放操作。
引言
当我尝试教ListBox
如何启用多选进行拖放操作时,我很快意识到这个问题没有简单的解决方案。事实上,我在互联网上找到的所有关于这个主题的资料(包括微软的)都在讲述同一个故事:“在ListBox
中使用多选进行拖放操作是不可能的!”
然而,我找到了一个方法。
WinForms控件包含一个非常基础的拖放架构,但它们并不能开箱即用地提供拖放功能。你仍然需要编写大量的(并不那么直观的)代码才能让拖放生效。
在本文中,我将向您展示如何创建一个名为DragDropListBox
的新控件,该控件可以立即进行拖放操作,并解决了多选问题。
特点
我们希望通过派生自ListBox
来创建一个新控件。它应该支持以下功能,而无需额外编码:
- 在不同的列表框之间进行拖放。
- 在同一个列表框内进行拖放,以重新排序项目。
- 一次拖放多个项目(对于
ListBox
来说并不显而易见)。 - 通过属性窗口在设计时微调拖放行为。
- 提供视觉反馈。
子类化
从另一个类派生一个新类称为子类化。让我们通过子类化ListBox
来创建一个名为DragDropListBox
的新控件。
public class DragDropListBox : ListBox
{
// TODO: Enhance ListBox!
}
我们将不得不为不同的鼠标和拖放事件编写事件代码。在子类化控件时,这不是通过将方法附加到事件来完成的,而是通过重写基类的事件方法来完成的。
// WRONG:
this.MouseDown += new MouseEventHandler(DragDropListBox_MouseDown);
// RIGHT:
protected override void OnMouseDown(MouseEventArgs e)
{
base.OnMouseDown(e);
// Your code to go here...
}
另外,别忘了调用基类的事件方法,因为它包含了引发事件的代码!
拖放行为的微调
可以通过拖放进行几项操作:
- 将项目从一个控件移动到另一个控件。这将从源控件中移除项目。
- 将项目从一个控件复制到另一个控件。
- 在单个控件内重新排序项目。
我们的新控件的用户并不总是希望允许所有这些操作。如果DragDropListBox
之一被用作一种工具箱,那么工具应该被复制到目标,而不是从工具箱中移除。另一方面,如果一个DragDropListBox
列出了可用选项,另一个列出了已选选项,那么已选选项应该从可用选项列表中移除。用户也可能希望禁止将内容拖放到工具箱上或更改可用选项的顺序。
为了允许对所有这些内容进行微调,我们引入了新的布尔属性:
AllowReorder
IsDragDropCopySource
IsDragDropMoveSource
IsDragDropTarget
当有四个或更多DragDropListBox
时,可能会出现另一个问题。例如,假设我们有两个DragDropListBox
对,每对都代表可用项目和已选项目的列表。假设一对处理猫,另一对处理狗。我们应该只能在两个猫DragDropListBox
之间移动猫,在两个狗DragDropListBox
之间移动狗。为此,我们引入一个字符串属性:
DragDropGroup
我们希望DragDropListBox
仅允许在具有相同DragDropGroup
的DragDropListBox
之间进行拖放操作。通常,此属性为空字符串。但在本例中,我们可以将两个猫DragDropListBox
的DragDropGroup
属性设置为"cats"
。我们可以将两个狗DragDropListBox
的DragDropGroup
留空,或者例如将其设置为"dogs"
。它们只需要与"cats"
不同即可。
例如,这里是其中一个属性的代码:
private bool _isDragDropCopySource = true;
[Category("Behavior (drag-and-drop)"), DefaultValue(true),
Description("Indicates whether ...")]
public bool IsDragDropCopySource
{
get { return _isDragDropCopySource; }
set { _isDragDropCopySource = value; }
}
请注意 Attributes:Category
将此属性放置在“行为(拖放)”名为的新组中,位于属性窗口中。我们将把所有这五个属性都放在这个组中。DefaultValue
定义了属性的默认值。Description
是——嗯——属性的描述,它将自动显示在智能感知工具提示和属性窗口中。
我们稍后会看到细节。
多选问题
拖放是如何工作的?在扩展选择模式下,用户通过单击项目来选择项目。然后,他可以通过按住Shift键并单击另一个项目来选择一个范围。通过按住Ctrl键,他可以单独选择和取消选择项目。现在,如果他单击其中一个选定的项目(在按住左鼠标按钮的同时)并开始移动鼠标,则会启动拖放操作。
这在ListView
控件中工作得很好,但在ListBox
控件中却不行。为什么?让我们做一个小实验。请在Windows资源管理器中选择几个文件。(Windows资源管理器使用ListView
。)将鼠标指向一个选定的文件。按住左鼠标按钮。不要移动鼠标。什么也没发生。现在释放鼠标按钮。这将取消选择除您单击的文件之外的所有文件。请注意,在鼠标按钮按下时,文件仍然被选中。这很好,因为拖放操作是在鼠标按钮按下时开始的。
但是,如果您对ListBox
重复相同的实验,您会注意到,一旦按下鼠标按钮,选择就会丢失!这不好!
为了弥补这个缺陷,我们需要跟踪选择。幸运的是,ListBox
有一个名为SelectedIndexChanged
的事件。这听起来正是我们需要的。让我们重写相应的事件方法:
protected override void OnSelectedIndexChanged(EventArgs e)
{
base.OnSelectedIndexChanged(e);
SaveSelection();
// Save the selection,
// so that we can restore it later.
}
SelectedIndexChanged
在选择更改时触发,即使它是通过编程方式完成的。但是,我们不希望在恢复选择时保存选择。让我们定义一个标志_restoringSelection
,在恢复选择时将其设置为。我们还需要一个数组_selectionSave
,我们在其中存储选定项目的索引。现在,让我们看一下SaveSelection
方法:
private int[] _selectionSave = new int[0];
private bool _restoringSelection = false;
private void SaveSelection()
{
if (!_restoringSelection && SelectionMode == SelectionMode.MultiExtended) {
SelectedIndexCollection sel = SelectedIndices;
if (_selectionSave.Length != sel.Count) {
_selectionSave = new int[sel.Count];
}
SelectedIndices.CopyTo(_selectionSave, 0);
}
}
这里没什么特别的。如果我们没有恢复选择且启用了多选模式,那么我们就使用SelectedIndices
集合的CopyTo
方法将选定的索引复制到_selectionSave
数组中。
注意:还有一个选择模式SelectionMode.MultiSimple
,它逐个选择或取消选择项目。这里不需要恢复选择。恢复选择也会反转刚刚完成的选择更改。使用SelectionMode.MultiSimple
仍然可以拖动多个项目;但是,您必须在选择最后一个项目时开始拖动(因为单击一个已选项目会取消选择它)。
我们还需要一个相应的恢复选择的方法:
private void RestoreSelection(int clickedItemIndex)
{
if (SelectionMode == SelectionMode.MultiExtended &&
Control.ModifierKeys == Keys.None &&
Array.IndexOf(_selectionSave, clickedItemIndex) >= 0) {
_restoringSelection = true;
foreach (int i in _selectionSave) {
SetSelected(i, true);
}
SetSelected(clickedItemIndex, true);
_restoringSelection = false;
}
}
如果用户想要拖动项目,他将不会按住Shift或Ctrl等修饰键。如果他按下了,他可能还在编辑选择。只有当用户单击一个之前已经选定的项目时,我们才会恢复选择。_selectionSave
存储了点击之前的状态。首先,让我们设置前面提到的标志_restoringSelection
。我们现在可以通过为每个存储的项目索引调用SetSelected
来重新选择项目。然后再次调用SetSelected
来单击的项目,以使其成为当前项目。(这也修复了ListBox
的一个奇怪的bug,即当在以编程方式选择项目后单击列表时,会选择过多的项目。)最后,让我们清除该标志。
我们稍后将看到如何在OnMouseDown
事件方法中调用RestoreSelection
。
发起拖放
必须显式启动拖放过程。为了做到这一点,我们必须检测开始拖动操作的条件。如果用户单击了一个选定的项目并移动了鼠标,但没有释放鼠标按钮,则满足这些条件。因此,涉及MouseDown
、MouseUp
和MouseMove
事件。在OnMouseDown
中,我们记录初始鼠标位置。在OnMouseUp
中,我们检测鼠标按钮的释放。在OnMouseMove
中,我们查看自用户单击以来鼠标移动了多远。
private Rectangle _dragOriginBox = Rectangle.Empty;
protected override void OnMouseDown(MouseEventArgs e)
{
base.OnMouseDown(e);
int clickedItemIndex = IndexFromPoint(e.Location);
if (clickedItemIndex >= 0 && MouseButtons == MouseButtons.Left &&
(_isDragDropCopySource || _isDragDropMoveSource || _allowReorder) &&
(GetSelected(clickedItemIndex) || Control.ModifierKeys == Keys.Shift)) {
RestoreSelection(clickedItemIndex);
Size dragSize = SystemInformation.DragSize;
_dragOriginBox = new Rectangle(new Point(e.X -
(dragSize.Width / 2), e.Y - (dragSize.Height / 2)), dragSize);
}
}
protected override void OnMouseUp(MouseEventArgs e)
{
base.OnMouseUp(e);
_dragOriginBox = Rectangle.Empty;
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (_dragOriginBox != Rectangle.Empty &&
!_dragOriginBox.Contains(e.X, e.Y)) {
DoDragDrop(new DataObject("IDragDropSource", this),
DragDropEffects.All);
_dragOriginBox = Rectangle.Empty;
}
}
在OnMouseDown
中,我们在执行任何操作之前测试所有先决条件是否满足。首先,让我们看看用户是否单击了一个项目。ListBox
方法IndexFromPoint
如果在给定的坐标内有一个列表项,则返回一个正索引值。用户是否按下了左鼠标按钮?此外,我们上面描述的微调属性必须允许我们的列表框要么成为拖放操作的可能来源,要么允许重新排序列表项。单击的项目必须被选中。如果按下了Shift键,GetSelected
不会返回正确的值(我不知道为什么),但那时项目无论如何都会被选中。在OnMouseDown
和RestoreSelection
中确定所有这些条件花费了我很多时间。整个过程非常棘手,因为ListBox
并不总是按照预期行事。
如果满足这些条件,我们可以继续。如前所述,单击选择可能会破坏它——让我们通过调用我们的方法RestoreSelection
来恢复它。因为我们想知道鼠标是否移动了足够远,所以我们定义了一个Rectangle _dragOriginBox
,该矩形定义了鼠标在实际启动拖放过程之前必须越过的边界。SystemInformation.DragSize
告诉我们这个矩形需要多大。矩形以单击位置为中心。
在OnMouseUp
中,我们检查用户是否释放了鼠标按钮。如果他在拖放启动之前释放了鼠标按钮,那么到目前为止游戏就结束了。将我们的矩形设置为Rectangle.Empty
会告诉OnMouseMove
不要启动拖放。
在OnMouseMove
中,我们检查矩形是否已定义以及鼠标是否越过了边界。如果是这样,我们可以通过调用DoDragDrop
来启动拖放过程。此方法需要传递给目标的数据。我们可以传递任何我们想要的数据。稍后将详细介绍。
Reflection(反射)
是时候思考一下了。拖放建立了一个源控件和目标控件之间的通信链接。如前所述,源控件可以向目标控件传递任何信息。但是,目标控件需要从源控件了解什么?嗯,当然,它想知道正在删除哪些项目。但这还不够!源控件是否允许移动/复制,并且它是否在同一个DragDropGroup
中?如果项目必须移动,则必须有一种方法从源中删除它们。最后,目标需要一种方法来告诉源在拖放操作完成后引发一个事件。
满足目标控件的一个简单方法是给它一个指向源DragDropListBox
的引用。然而,这有一个缺点,即拖放只能在两个DragDropListBox
之间进行。如何将拖放功能扩展到其他控件?我们可以创建一个专门的数据类来传输所需信息。然而,一种更简单、更通用的方法是将此信息保留在我们的DragDropListBox
类中,但将相关方法和属性分组到一个接口中:
public interface IDragDropSource
{
string DragDropGroup { get; }
bool IsDragDropCopySource { get; }
bool IsDragDropMoveSource { get; }
object[] GetSelectedItems();
void RemoveSelectedItems(ref int rowIndexToAjust);
void OnDropped(DroppedEventArgs e);
}
public class DragDropListBox : ListBox, IDragDropSource
{
// ...
}
现在,在启动拖放过程时,我们仍然传递一个DragDropListBox
引用,但目标控件将其视为IDragDropSource
(传递给DoDragDrop
方法的数据必须打包在DataObject
中)。
// The source control initiates drag-and-drop in OnMouseMove:
DoDragDrop(new DataObject("IDragDropSource", this),
DragDropEffects.All);
// The target control retrieves the information in OnDragDrop:
IDragDropSource src = drgevent.Data.GetData("IDragDropSource")
as IDragDropSource;
现在,任何实现IDragDropSource
的控件都可以作为拖放源。
我们已经实现了IDragDropSource
的属性,但仍然需要为方法提供实现:
public object[] GetSelectedItems()
{
object[] items = new object[SelectedItems.Count];
SelectedItems.CopyTo(items, 0);
return items;
}
public void RemoveSelectedItems(ref int itemIndexToAjust)
{
for (int i = SelectedIndices.Count - 1; i >= 0; i--) {
int at = SelectedIndices[i];
Items.RemoveAt(at);
if (at < itemIndexToAjust) {
itemIndexToAjust--;
}
}
}
public virtual void OnDropped(DroppedEventArgs e)
{
var dropEvent = Dropped;
if (dropEvent != null) {
dropEvent(this, e);
}
}
RemoveSelectedItems
中的奇怪的ref
参数itemIndexToAjust
是什么意思?如果项目正在被重新排序(而不是移动或复制),源控件和目标控件是相同的。如果要移除的项目位于插入点之前,则插入点会发生变化。当然,我们可以先在新的位置插入项目,然后再从旧位置移除它们。但是,插入项目可能会更改(可能很多)要删除的项目的索引。从移除开始更容易。因此,我们将打算的插入点索引作为ref
参数传递给RemoveSelectedItems
,并要求RemoveSelectedItems
为我们调整它。我们还注意从后往前移除项目,以保留尚未移除的项目的索引。
OnDropped
在拖放操作完成时被调用,以引发Dropped
事件。事件本身声明为:
[Category("Drag Drop"),
Description("Occurs when a extended DragDropListBox " +
"drag-and-drop operation is completed.")]
public event EventHandler<DroppedEventArgs> Dropped;
DroppedEventArgs
定义如下:
public enum DropOperation
{
Reorder,
MoveToHere,
CopyToHere,
MoveFromHere,
CopyFromHere
}
public class DroppedEventArgs : EventArgs
{
public DropOperation Operation { get; set; }
public IDragDropSource Source { get; set; }
public IDragDropSource Target { get; set; }
public object[] DroppedItems { get; set; }
}
视觉反馈
我之前提到了视觉提示。它只是由一个水平线组成,当将项目拖到一个DragDropListBox
上时,该水平线会显示出来,指示放置位置。
绘制这条线很容易,但移除它却很棘手。移除一个视觉元素意味着重绘该元素隐藏的内容。我的第一次尝试只是重绘白色背景。但这效果不好。列表可能有带有彩色背景的选定元素。我选择绘制宽度为两像素的视觉提示线。因此,它可能部分覆盖正常背景,部分覆盖选定背景。我也可能隐藏部分文本。特别是,字符的下降部分(如y或g)。
在VB的旧版本中,可以选择以反转模式绘制。反转背景的颜色总能提供良好的对比度,即使在异构背景上也是如此。而且,更重要的是,通过两次绘制相同的内容,原始外观会自动恢复。这正是我们需要的!但不幸的是,System.Drawing
中没有反转的笔或画笔;但是,可以通过使用Win32 API提供的函数来实现这一点。
我们还需要确定视觉提示的确切位置,并且需要记住它的位置以便以后可以移除它。我决定将所有这些功能放在一个名为VisualCue
的新类中。这是VisualCue
(略有删节):
public class VisualCue
{
public const int NoVisualCue = -1;
public VisualCue(ListBox listBox) { _listBox = listBox; }
public void Clear()
{
if (_index != NoVisualCue) {
Draw(_index);
// Draws in inverted mode and
// thus deletes the visual cue;
_index = NoVisualCue;
}
}
public void Draw(int itemIndex)
{
// ...
// Get the coordinates of the line
if (_listBox.Sorted) {
// Let's draw a vertical line on the
// left of the list if the list is sorted,
// since items could just be dropped anywhere.
rect = _listBox.ClientRectangle;
// ...
} else {
rect = _listBox.GetItemRectangle(itemIndex);
// ...
}
IntPtr hdc = Win32.GetDC(IntPtr.Zero); // Get device context.
Win32.SetROP2(hdc, Win32.R2_NOT); // Switch to inverted mode.
Win32.MoveToEx(hdc, l1p1.X, l1p1.Y, IntPtr.Zero);
Win32.LineTo(hdc, l1p2.X, l1p2.Y);
// ...
Win32.ReleaseDC(IntPtr.Zero, hdc); // Release device context.
_index = itemIndex;
}
public int Index { get { return _index; } }
}
此类本身依赖于一个静态类Win32
,该类包含所有API声明。System.Drawing
的常规绘图方法在反转模式下不起作用,因此我们需要为所有绘图使用API函数。
拖放继续进行!
为了让拖放生效,我们还有很多工作要做。到目前为止,我们还没有处理拖放事件。在此之前,让我们定义两个辅助方法:
GetDragDropEffect
确定正在执行的拖放操作,可以是None
、Move
或Copy
。
private DragDropEffects GetDragDropEffect(DragEventArgs drgevent)
{
const int CtrlKeyPlusLeftMouseButton = 9; // KeyState.
DragDropEffects effect = DragDropEffects.None;
// Retrieve the source control
// of the drag-and-drop operation.
IDragDropSource src =
drgevent.Data.GetData("IDragDropSource") as IDragDropSource;
if (src != null && _dragDropGroup == src.DragDropGroup) {
// The stuff being draged is compatible.
if (src == this) {
// Drag-and-drop happens within this control.
if (_allowReorder && !this.Sorted) {
// We can not reorder, if list is sorted.
effect = DragDropEffects.Move;
}
} else if (_isDragDropTarget) {
// If only Copy is allowed then copy. If Copy and Move
// are allowed, then Move, unless the Ctrl-key is pressed.
if (src.IsDragDropCopySource &&
(!src.IsDragDropMoveSource || drgevent.KeyState ==
CtrlKeyPlusLeftMouseButton)) {
effect = DragDropEffects.Copy;
} else if (src.IsDragDropMoveSource) {
effect = DragDropEffects.Move;
}
}
}
return effect;
}
DropIndex
获取项目在其之前放置项目的索引。索引根据鼠标的垂直位置计算。如果放置位置位于列表中的最后一个项目之后,则返回最后一个项目的索引+1(等于Item.Count
)。
private int DropIndex(int yScreen)
{
// The DragEventArgs gives us screen coordinates.
// Convert the screen coordinates to client coordinates.
int y = PointToClient(new Point(0, yScreen)).Y;
// Make sure we are inside of the client rectangle.
// If we are on the border of the ListBox,
// then IndexFromPoint does not return a match.
if (y < 0) {
y = 0;
} else if (y > ClientRectangle.Bottom - 1) {
y = ClientRectangle.Bottom - 1;
}
int index = IndexFromPoint(0, y);
// The x-coordinate doesn't make any difference.
if (index == ListBox.NoMatches) {
// Not hovering over an item
return Items.Count;
// Append to the end of the list.
}
// If hovering below the middle of the item,
// then insert after the item.
Rectangle rect = GetItemRectangle(index);
if (y > rect.Top + rect.Height / 2) {
index++;
}
int lastFullyVisibleItemIndex = TopIndex +
ClientRectangle.Height / ItemHeight;
if (index > lastFullyVisibleItemIndex) {
// Do not insert after the last fully visible item
return lastFullyVisibleItemIndex;
}
return index;
}
当鼠标进入放置目标时,我们必须通过设置拖放效果来设置正确的鼠标光标。
禁止放置!
复制
移动
当离开放置目标时,我们需要移除视觉提示,如果之前有绘制过。
protected override void OnDragEnter(DragEventArgs drgevent)
{
base.OnDragEnter(drgevent);
drgevent.Effect = GetDragDropEffect(drgevent);
}
protected override void OnDragLeave(EventArgs e)
{
base.OnDragLeave(e);
_visualCue.Clear();
}
在鼠标移动到放置目标上时,我们必须不断调整鼠标光标和视觉提示的外观。
protected override void OnDragOver(DragEventArgs drgevent)
{
base.OnDragOver(drgevent);
drgevent.Effect = GetDragDropEffect(drgevent);
if (drgevent.Effect == DragDropEffects.None) {
return;
}
// Everything is fine, give a visual cue
int dropIndex = DropIndex(drgevent.Y);
if (dropIndex != _visualCue.Index) {
_visualCue.Clear();
_visualCue.Draw(dropIndex);
}
}
现在,到了最后一个方法!这里就是实际放置的地方。我们必须:
- 清除视觉提示
- 检索拖动项目数据
- 注意列表的排序状态
- 插入放置的项目
- 从源中移除所有选定的项目(如果是移动操作)
- 调整目标中的选择
- 在目标中引发
Dropped
事件 - 在源中引发
Dropped
事件
protected override void OnDragDrop(DragEventArgs drgevent)
{
base.OnDragDrop(drgevent);
_visualCue.Clear();
// Retrieve the drag item data.
// Conditions have been testet in OnDragEnter
// and OnDragOver, so everything should be ok here.
IDragDropSource src =
drgevent.Data.GetData("IDragDropSource")
as IDragDropSource;
object[] srcItems = src.GetSelectedItems();
// If the list box is sorted, we don't know
// where the items will be inserted
// and we will have troubles selecting the inserted
// items. So let's disable sorting here.
bool sortedSave = Sorted;
Sorted = false;
// Insert at the currently hovered row.
int row = DropIndex(drgevent.Y);
int insertPoint = row;
if (row >= Items.Count) {
// Append items to the end.
Items.AddRange(srcItems);
} else { // Insert items before row.
foreach (object item in srcItems) {
Items.Insert(row++, item);
}
}
// Remove all the selected items from the source, if moving.
DropOperation operation;
// Remembers the operation for the event we'll raise.
if (drgevent.Effect == DragDropEffects.Move) {
int adjustedInsertPoint = insertPoint;
src.RemoveSelectedItems(ref adjustedInsertPoint);
if (src == this) { // Items are being reordered.
insertPoint = adjustedInsertPoint;
operation = DropOperation.Reorder;
} else {
operation = DropOperation.MoveToHere;
}
} else {
operation = DropOperation.CopyToHere;
}
// Adjust the selection in the target.
ClearSelected();
if (SelectionMode == SelectionMode.One) {
// Select the first item inserted.
SelectedIndex = insertPoint;
} else if (SelectionMode != SelectionMode.None) {
// Select the inserted items.
for (int i = insertPoint; i < insertPoint +
srcItems.Length; i++) {
SetSelected(i, true);
}
}
// Now that we've selected the inserted items,
// restore the "Sorted" property.
Sorted = sortedSave;
// Notify the target (this control).
DroppedEventArgs e = new DroppedEventArgs() {
Operation = operation,
Source = src,
Target = this,
DroppedItems = srcItems
};
OnDropped(e);
// Notify the source (the other control).
if (operation != DropOperation.Reorder) {
e = new DroppedEventArgs() {
Operation = operation == DropOperation.MoveToHere ?
DropOperation.MoveFromHere : DropOperation.CopyFromHere,
Source = src,
Target = this,
DroppedItems = srcItems
};
src.OnDropped(e);
}
}
请注意,如果我们在移动或复制,Dropped
事件会同时为目标控件和源控件引发。但是,对于源控件,MoveToHere
和CopyToHere
之类的放置操作会被更改为MoveFromHere
和CopyFromHere
。
Using the Code
DragDropListBox
无需额外编码即可使用。通过右键单击工具箱并执行“选择项...”快捷菜单命令,将DragDropListBox
添加到工具箱。然后,单击“浏览...”按钮并选择Oli.Controls.dll。DragDropListBox
应出现在工具箱的“所有Windows Forms”选项卡底部。将其拖放到您的窗体上,并在属性窗口中设置属性。当您在Visual Studio 2008中开发自定义控件或用户控件时,新控件会自动出现在工具箱中。
如果设置了DragDropListBox
的DataSource
属性,拖放将不起作用,因为在使用数据绑定时,无法直接在ListBox
中插入项目。
结论
我们从一个简单的想法开始,最终得到了一个相当复杂的代码。和往常一样,魔鬼藏在细节里。但现在,我们有了一个不错的控件和一个用于小型拖放框架的基础,该框架可以扩展到其他控件。