简单的拖放操作示例






4.73/5 (79投票s)
2004年12月10日
7分钟阅读

752668

21038
基础知识以及我一路学到的一些东西。
引言
信不信由你,我从未编写过需要使用拖放操作的应用程序。我现在正在做一个项目,希望使用这个功能,所以我开始寻找一个关于拖放如何工作的简单示例。令人惊讶的是,我找不到一个——我找到的所有示例似乎都涉及树形控件,或者没有专门说明如何使用拖放。虽然简单,但在编写一个小型演示程序时,我发现了一些东西,所以我决定写一个关于拖放的新手教程。
创建一个简单的图像查看器
我决定用一个简单的、单张图像查看器来演示拖放操作,该查看器能够查看 JPG、BMP 和 PNG 文件。
STAThread 属性
第一步是确保你的 Main
方法被 [STAThread]
属性标记。如果缺少此属性,你将无法进行拖放操作(实际上,如果你尝试运行程序,程序会显示一个错误消息——这不是程序的问题,而是 .NET 的问题)。
[STAThread]
static void Main()
{
Application.Run(new Form1());
}
AllowDrop 属性
第一步是确定你想启用拖放操作的控件,并将 AllowDrop
属性设置为 true
。通常这是应用程序的 Form
控件。对于 Form
,只需在设计器中将 AllowDrop
属性设置为 true
即可。
但是,如果你想为特定控件(例如 PictureBox
)启用拖放操作呢?奇怪的是,这个属性定义在基类 Control
中,但在 PictureBox
的属性中却不可用。
并且,如果你有
PictureBox pb=new PictureBox();
然后你开始输入
pb.Allow
Intellisense 也不会自动完成。然而,这并不意味着你不能手动设置这个属性!看起来 PictureBox
是唯一一个不公开此属性的控件,原因不明。
无论如何,由于演示应用程序除了显示图像之外不做任何其他事情,因此在这种情况下,为窗体启用拖放操作而不是仅为 PictureBox
启用拖放操作是很有意义的。
拖放事件
下一步是连接拖放事件(请注意,PictureBox
控件在设计器中也不显示这些事件,但它们确实存在,并且你可以使用它们)。
由于我们将拖放功能添加到窗体中,我们可以使用设计器来添加我们几乎总是关心的四个事件的方法:
DragEnter
DragOver
DragDrop
DragLeave
DragEnter 事件
这很简单——当鼠标进入一个启用了拖放的控件时,就会触发此事件。它为你提供了机会来
- 检查
DragEventArgs
以查看正在拖到你的控件上的DataObject
是否是你能够接受的。 - 执行任何你可能需要的来处理数据的初始化。
- 设置
Effect
属性以指示该项将被复制还是移动到控件中。
DragEventArgs
DragEventArgs
包含几个我们感兴趣的属性:
AllowedEffect
- 源拖放事件允许的操作。我们主要关心的是源是否允许复制或移动。Data
- 实际包含我们感兴趣的数据的IDataObject
。Effect
- 我们希望为该对象设置的目标效果。X
,Y
- 鼠标位置。这是屏幕坐标。KeyState
- 可用于检查 SHIFT、CTRL 和 ALT 键以及鼠标按钮的状态。
在我的示例中,我关心两件事:
- 对象是否可以复制到我的应用程序中?
- 数据是否是我的应用程序接受的格式?
特别是,该应用程序接受从 Explorer 拖动的图像,所以我关心文件名。因此,OnDragEnter
方法开始如下:
private void OnDragEnter(object sender, System.Windows.Forms.DragEventArgs e)
{
Debug.WriteLine("OnDragEnter");
string filename;
validData=GetFilename(out filename, e);
所有实际工作都在 GetFilename
方法中完成。
protected bool GetFilename(out string filename, DragEventArgs e)
{
bool ret=false;
filename=String.Empty;
if ( (e.AllowedEffect & DragDropEffects.Copy) == DragDropEffects.Copy)
{
Array data=((IDataObject)e.Data).GetData("FileName") as Array;
if (data != null)
{
if ( (data.Length ==1) && (data.GetValue(0) is String) )
{
filename=((string[])data)[0];
string ext=Path.GetExtension(filename).ToLower();
if ( (ext==".jpg") || (ext==".png") || (ext==".bmp") )
{
ret=true;
}
}
}
}
return ret;
}
首先,我测试源是否允许复制数据。其次,我根据文件名获取数据。拖放图像时,数据支持多种格式,可以在调试器中查看:
我们看到“FileName”是可接受的格式之一,所以我将请求以 FileName 格式的数据。但是,我们还没有完成。返回的对象实际上是一个 Array
,正如观察窗口中所示:
所以,我想做的是:
验证只拖放了一个数据(而不是文件名集合),并且,虽然可能有点多余,但我希望验证这个唯一条目的数据类型是 string
。完成所有这些验证后,我就可以获取文件名,提取扩展名,并确认它是“.jpg”、“.bmp”或“.png”之一。如果一切正常,该方法将返回 true
,表示成功,并附带文件名。
管理数据
现在我们有了文件名,我们该怎么处理它?在我的应用程序中,我想显示一个小图像,为用户提供一些反馈,显示即将被拖放到应用程序上的图片。这是一个很好的例子,说明有时你需要智能地管理数据。我想处理两个问题:
- 如果正在拖动的图像与应用程序正在显示的图像相同怎么办?
- 将图像加载到应用程序是一个耗时的过程,我不想通过加载图像来拖慢应用程序的主线程。
OnDragEnter
方法的其余部分处理此逻辑,此外还确定我们是否将拖放效果设置为 Copy
。这是完整的实现:
private void OnDragEnter(object sender, System.Windows.Forms.DragEventArgs e)
{
Debug.WriteLine("OnDragEnter");
string filename;
validData=GetFilename(out filename, e);
if (validData)
{
if (lastFilename != filename)
{
thumbnail.Image=null;
thumbnail.Visible=false;
lastFilename=filename;
getImageThread=new Thread(new ThreadStart(LoadImage));
getImageThread.Start();
}
else
{
thumbnail.Visible=true;
}
e.Effect=DragDropEffects.Copy;
}
else
{
e.Effect=DragDropEffects.None;
}
}
注意,我首先检查正在显示的图像是否再次被拖动到应用程序上。如果是,我们就有图像了,并且准备好显示缩略图(此代码中有一个潜在的小错误——你能找出是什么吗?)
如果这是一个新图像,我将启动一个线程来实际加载图像并在准备好时将其放入缩略图中。让我们看看那个线程和相关函数:
public delegate void AssignImageDlgt();
protected void LoadImage()
{
nextImage=new Bitmap(lastFilename);
this.Invoke(new AssignImageDlgt(AssignImage));
}
protected void AssignImage()
{
thumbnail.Width=100;
// 100 iWidth
// ---- = ------
// tHeight iHeight
thumbnail.Height=nextImage.Height * 100 / nextImage.Width;
SetThumbnailLocation(this.PointToClient(new Point(lastX, lastY)));
thumbnail.Image=nextImage;
}
图像被加载到 nextImage
中。由于我们正在操作应用程序线程中管理的控件,我使用 Invoke
方法来执行委托,以便实际的缩略图控件操作由应用程序线程而不是我们的工作线程完成。这是操作 UI 对象的“安全”方式。
DragOver 事件
我没有意识到的一点是,DragOver
事件会重复触发,即使我没有移动鼠标。根据应用程序的操作,更新视觉提示可能会花费大量时间。我们当然不希望在用户没有移动鼠标时浪费 CPU 时间。因此,我正在测试鼠标位置是否已更改:
private void OnDragOver(object sender, System.Windows.Forms.DragEventArgs e)
{
Debug.WriteLine("OnDragOver");
if (validData)
{
if ( (e.X != lastX) || (e.Y != lastY) )
{
SetThumbnailLocation(this.PointToClient(new Point(e.X, e.Y)));
}
}
}
再次,我们调用一个单独的方法来实际设置缩略图位置:
protected void SetThumbnailLocation(Point p)
{
if (thumbnail.Image==null)
{
thumbnail.Visible=false;
}
else
{
p.X-=thumbnail.Width/2;
p.Y-=thumbnail.Height/2;
thumbnail.Location=p;
thumbnail.Visible=true;
}
}
此方法根据 nextImage
是否存在来确定缩略图的位置和可见性(在工作线程完成加载图像之前,它将不存在)。
DragLeave 事件
当鼠标离开我们的应用程序时,我们希望隐藏当前正在显示的任何视觉提示:
private void OnDragLeave(object sender, System.EventArgs e)
{
Debug.WriteLine("OnDragLeave");
thumbnail.Visible=false;
}
DragDrop 事件
在此事件处理程序中,我们想验证要拖放的数据是否有效。这很简单,使用 OnDragEnter
事件处理程序设置的 validData
标志。但是,如果工作线程尚未完成加载图像,我们需要等待。并且由于工作线程使用 Invoke
方法,我们不能简单地使用 Join
方法,将应用程序置于等待状态直到工作线程完成。这样做会阻止 Invoke
方法执行。因此,我们必须测试线程是否仍在运行,并告诉应用程序继续处理事件:
private void OnDragDrop(object sender, System.Windows.Forms.DragEventArgs e)
{
Debug.WriteLine("OnDragDrop");
if (validData)
{
while (getImageThread.IsAlive)
{
Application.DoEvents();
Thread.Sleep(0);
}
thumbnail.Visible=false;
image=nextImage;
AdjustView();
if ( (pb.Image != null) && (pb.Image != nextImage) )
{
pb.Image.Dispose();
}
pb.Image=image;
}
}
在我们确保工作线程已终止后,我正在调整应用程序的 PictureBox
,使其以正确的比例显示图像。我还手动处理任何先前图像的处置,以释放图像资源,而不是等待垃圾回收。但我必须确保,如果用户将正在查看的同一图像拖动到应用程序中,我不会实际处置该图像,因为它已经是当前图像了!
结论
虽然拖放操作本身很简单,但需要进行一些规划:
- 你为用户提供的视觉提示。
- 如何处理这些视觉提示,如果它们最初设置起来耗时。
- 在拖放过程中如何管理这些视觉资源。
- 如何针对重复操作进行优化。
- 如何在放置完成后进行清理。
我认为这些对新手来说很有用。这一切并非那么简单!