今天某个群里有人问为什么拖拽文件到程序里的操作无效。正好我遇到过这个问题,就翻出来自己以前写的代码,改成一个类,算做这个问题的解决方案。

导致拖拽无效的原因可以看这篇文章:Q: Why Doesn』t Drag-and-Drop work when my Application is Running Elevated? - A: Mandatory Integrity Control and UIPI ,其中也给出了解决的方法,但对于WinForm而言不太够,因为需要处理其他额外的细节。

我已经忘记当初我写这些代码时的心路历程了,所以就不仔细分析,就说关键的内容:

  1. 使程序能够接收相关消息。
  2. 对WinForm而言会以自己的方式处理拖拽,所以控制项启用了AllowDrop属性,则无法接收WM_DROPFILES消息。
  3. 同第2条,即使程序可以接收消息,也需要手动处理消息和引发事件。这里我选择的时使用Control.DoDragDrop()方法。

其他更多细节见代码:

public sealed class FileDropHandler : IMessageFilter, IDisposable
{

#region native members

[DllImport("user32.dll", SetLastError = true, CallingConvention = CallingConvention.Winapi)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool ChangeWindowMessageFilterEx(IntPtr hWnd, uint message, ChangeFilterAction action, in ChangeFilterStruct pChangeFilterStruct);

[DllImport("shell32.dll", SetLastError = false, CallingConvention = CallingConvention.Winapi)]
private static extern void DragAcceptFiles(IntPtr hWnd, bool fAccept);

[DllImport("shell32.dll", SetLastError = false, CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Winapi)]
private static extern uint DragQueryFile(IntPtr hWnd, uint iFile, StringBuilder lpszFile, int cch);

[DllImport("shell32.dll", SetLastError = false, CallingConvention = CallingConvention.Winapi)]
private static extern void DragFinish(IntPtr hDrop);

[StructLayout(LayoutKind.Sequential)]
private struct ChangeFilterStruct
{
public uint CbSize;
public ChangeFilterStatu ExtStatus;
}

private enum ChangeFilterAction : uint
{
MSGFLT_RESET,
MSGFLT_ALLOW,
MSGFLT_DISALLOW
}

private enum ChangeFilterStatu : uint
{
MSGFLTINFO_NONE,
MSGFLTINFO_ALREADYALLOWED_FORWND,
MSGFLTINFO_ALREADYDISALLOWED_FORWND,
MSGFLTINFO_ALLOWED_HIGHER
}

private const uint WM_COPYGLOBALDATA = 0x0049;
private const uint WM_COPYDATA = 0x004A;
private const uint WM_DROPFILES = 0x0233;

#endregion

private const uint GetIndexCount = 0xFFFFFFFFU;

private Control _ContainerControl;

private readonly bool _DisposeControl;

public Control ContainerControl { get; }

public FileDropHandler(Control containerControl) : this(containerControl, false) { }

public FileDropHandler(Control containerControl, bool releaseControl)
{
_ContainerControl = containerControl ?? throw new ArgumentNullException("control", "control is null.");

if (containerControl.IsDisposed) throw new ObjectDisposedException("control");

_DisposeControl = releaseControl;

var status = new ChangeFilterStruct() { CbSize = 8 };

if (!ChangeWindowMessageFilterEx(containerControl.Handle, WM_DROPFILES, ChangeFilterAction.MSGFLT_ALLOW, in status)) throw new Win32Exception(Marshal.GetLastWin32Error());
if (!ChangeWindowMessageFilterEx(containerControl.Handle, WM_COPYGLOBALDATA, ChangeFilterAction.MSGFLT_ALLOW, in status)) throw new Win32Exception(Marshal.GetLastWin32Error());
if (!ChangeWindowMessageFilterEx(containerControl.Handle, WM_COPYDATA, ChangeFilterAction.MSGFLT_ALLOW, in status)) throw new Win32Exception(Marshal.GetLastWin32Error());
DragAcceptFiles(containerControl.Handle, true);

Application.AddMessageFilter(this);
}

public bool PreFilterMessage(ref Message m)
{
if (_ContainerControl == null || _ContainerControl.IsDisposed) return false;
if (_ContainerControl.AllowDrop) return _ContainerControl.AllowDrop = false;
if (m.Msg == WM_DROPFILES)
{
var handle = m.WParam;

var fileCount = DragQueryFile(handle, GetIndexCount, null, 0);

var fileNames = new string[fileCount];

var sb = new StringBuilder(262);
var charLength = sb.Capacity;
for (uint i = 0; i < fileCount; i++)
{
if (DragQueryFile(handle, i, sb, charLength) > 0) fileNames[i] = sb.ToString();
}
DragFinish(handle);
_ContainerControl.AllowDrop = true;
_ContainerControl.DoDragDrop(fileNames, DragDropEffects.All);
_ContainerControl.AllowDrop = false;
return true;
}
return false;
}

public void Dispose()
{
if (_ContainerControl == null)
{
if (_DisposeControl && !_ContainerControl.IsDisposed) _ContainerControl.Dispose();
Application.RemoveMessageFilter(this);
_ContainerControl = null;
}
}
}

这个类虽不优雅,但用法很简单:

public FileDropHandler FileDroper; //全局的

private void Form1_Load(object sender, EventArgs e)
{
FileDroper = new FileDropHandler(label1); //初始化
}

//这是 label1 的DragEnter事件。
private void label1_DragEnter(object sender, DragEventArgs e)
{
var paths = e.Data.GetData(typeof(string[]));
}

这个类是专供WinForm用的,虽然很多人说WinForm过时了,然而我还是喜欢用它写小工具。这个类很容易就改成WPF版的,这里不赘述。自己查资料即可,关键词是「消息」。

当然如果你觉得这个类太糙,也可以用复杂的方式处理消息,自己搞一套事件,或者自己实现一个IDataObject类型来存数据……

此代码基于WTFPL发布。


推荐阅读:
相关文章