今天某個群里有人問為什麼拖拽文件到程序里的操作無效。正好我遇到過這個問題,就翻出來自己以前寫的代碼,改成一個類,算做這個問題的解決方案。

導致拖拽無效的原因可以看這篇文章: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發布。


推薦閱讀:
相关文章