前段时间接到一个任务,需要从Windows桌面拖拽文件或文件夹到Electron网页中;网页本身是支持这种拖拽行为的;我们的程序是以管理员权限运行的,在测试的时候发现无法完成这个操作;文件拖拽到界面上时,直接显示了一个禁用的标志;如果换其他的方式来实现(比如:低权限运行,低权限蒙层),实在太过于复杂;没办法,只能尝试去研究为什么不能拖拽。
Windows从Vista开始,启用了UAC
功能;然后造成了我们不能拖的原因是因为UIPI
的机制;它是UAC
功能之一;该功能启用时,用户无法完成地权到高权的文件拖拽操作;
要实现本篇文章的目的,需要源码编译Electron
, 如果想要不改源码的情况下做到支持管理员文件拖放,那是非常麻烦的事情,本篇不覆盖这方面的内容,不过文末有思路。
Windows下拖放文件的方式如下:
WM_DROPFILES
:仅支持接收文件“投下”消息;但能够管理员运行;IDropTarget
: 功能全面,但不支持管理员下运行;Electron
正是采用的IDropTarget
的方式来实现文件拖放功能;但是IDropTarget
因为UIPI
的原因,该操作被阻止,且没有办法放行;有一些"非常规"的手段,可以临时性解决该问题(例如:关闭UAC),但这样操作也太粗暴了。
那么如何解决electron管理员拖放的问题?围绕上面提到的拖放方式来开展,即:
WM_DROPFILES
;IDropTarget
;改electron源码这里,很简单就能实现;往下看。
WM_DROPFILES消息:接收到该消息时,代表用户"投"下了一个文件;处理示例如下:
int dragged_file_count = DragQueryFile(hDropInfo, 0xFFFFFFFF, 0, 0);
for (int i = 0; i < dragged_file_count; i++)
{
TCHAR szPath[MAX_PATH] = { 0 };
DragQueryFile(hDropInfo, i, szPath, MAX_PATH);
//...
// do something.
}
**管理员运行时:**需要用ChangeWindowMessageFilter
或ChangeWindowMessageFilterEx
放行拖放消息;并且给窗口加上WS_EX_ACCEPTFILES
属性;处理示例如下:
HWND InitInstance(HINSTANCE hInstance, int nCmdShow)
{
hInst = hInstance; // Store instance handle in our global variable
HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);
if (!hWnd)
{
return 0;
}
// 该函数给当前的窗口加上属性WS_EX_ACCEPTFILES,
// 也可以在CreateWindow时附加属性WS_EX_ACCEPTFILES来替代;
::DragAcceptFiles(hWnd, true);
// 解除以下消息UIPI的限制;
CHANGEFILTERSTRUCT changeFilterStruct = { sizeof(CHANGEFILTERSTRUCT), 0 };
BOOL ret = ChangeWindowMessageFilterEx(hWnd, WM_DROPFILES, MSGFLT_ALLOW, &changeFilterStruct);
ret = ChangeWindowMessageFilterEx(hWnd, WM_COPYDATA, MSGFLT_ALLOW, &changeFilterStruct);
ret = ChangeWindowMessageFilterEx(hWnd, 0x0049, MSGFLT_ALLOW, &changeFilterStruct);
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
return hWnd;
}
IDropTarget
,这是一个由Ole所支持的拖拽方案;比WM_DROPFILE
更强大,更好用;相比于WM_DROPFILES,通过改方法可以监听到文件的拖入,悬停,离开,投放事件;首先,先实现一个IDropTarget
实例,如下:
// dragdrop.h
#include
#include
class MyDropTarget : public IDropTarget
{
public:
MyDropTarget()
{
m_cRef = 0;
}
// IUnknown方法
STDMETHODIMP QueryInterface(REFIID iid, void** ppvObject)
{
if (iid == __uuidof(IUnknown) || iid == __uuidof(IDropTarget))
{
*ppvObject = this;
AddRef();
return S_OK;
}
else
return E_NOINTERFACE;
}
STDMETHODIMP_(ULONG) AddRef()
{
return ++m_cRef;
}
STDMETHODIMP_(ULONG) Release()
{
if (--m_cRef == 0)
{
delete this;
return 0;
}
return m_cRef;
}
// IDropTarget方法
STDMETHODIMP DragEnter(IDataObject* pDataObj, DWORD grfKeyState, POINTL pt, DWORD* pdwEffect)
{
// 在这里处理拖拽进入事件
return S_OK;
}
STDMETHODIMP DragOver(DWORD grfKeyState, POINTL pt, DWORD* pdwEffect)
{
// 在这里处理拖拽悬停事件
return S_OK;
}
STDMETHODIMP DragLeave()
{
// 在这里处理拖拽离开事件
return S_OK;
}
STDMETHODIMP Drop(IDataObject* pDataObj, DWORD grfKeyState, POINTL pt, DWORD* pdwEffect)
{
// 在这里处理拖拽释放事件
return S_OK;
}
private:
long m_cRef;
};
首先先实例化MyDropTarget
,然后使用RegisterDragDrop
进行注册; 使用示例片段如下:
// main.cpp
...
// 注意:只能使用OleInitialize初始化,不能使用CoInitialize初始化;
// 否则RegisterDragDrop将会失败,且返回E_OUTOFMEMORY, 详情查看MSDN;
OleInitialize(NULL);
...
HWND hWnd = InitInstance();
...
MyDropTarget *dropTarget = new MyDropTarget();
// 注册
HRESULT hr = RegisterDragDrop(hWnd, dropTarget);
...
RevokeDragDop(hWnd);
dropTarget->Release();
前情提要:一旦程序使用了IDropTarget
的方式,那么WM_DROPFILS
的方式将不会生效;也就是说,如果一旦调用了RegisterDragDrop
,那么不管你是管理员运行,还是标准账号运行,你都将收不到WM_DROPFILES
消息;
因此,我们大致方案如下:
DragDrop
模块时,那么不要执行RegisterDragDrop
;并开放WM_DROPFILES
,WM_COPYDATA
等消息;IDropTarget
事件给对应的模块;以electron
版本6.1.12为例,其他版本大同小异;
首先找到文件src\ui\base\dragdrop\drop_target_win.cc
,找到void DropTargetWin::Init(HWND hwnd)
函数;原函数如下:
void DropTargetWin::Init(HWND hwnd) {
DCHECK(!hwnd_);
DCHECK(hwnd);
HRESULT result = RegisterDragDrop(hwnd, this);
DCHECK(SUCCEEDED(result));
}
修改为:
void DropTargetWin::Init(HWND hwnd) {
DCHECK(!hwnd_);
DCHECK(hwnd);
if (IsUserAnAdmin()) {
SetPropW(hwnd, L"drag-drop-callback-{2E8EAA96-6DA5-49CB-8DE7-042F0796CDB6}",
this);
BOOL ret = ChangeWindowMessageFilter(WM_DROPFILES, MSGFLT_ADD);
ret = ChangeWindowMessageFilter(0x0049,
MSGFLT_ADD); // 0x0049 == WM_COPYGLOBALDATA
ret = ChangeWindowMessageFilter(WM_COPYDATA, MSGFLT_ADD);
} else {
HRESULT result = RegisterDragDrop(hwnd, this);
DCHECK(SUCCEEDED(result));
}
}
以上代码中,首先通过IsUserAnAdmin
判断当前的运行权限是否为管理员运行;如果是管理员:
drag-drop-callback-{2E8EAA96-6DA5-49CB-8DE7-042F0796CDB6}
;WM_DROPFILES
,WM_COPYGLOBALDATA
,WM_COPYDATA
消息;此处建议使用:ChangeWindowMessageFilterEx
函数;处理WM_DROPFILES
,读者可自行选择处理的位置;可以选择chromium的消息处理函数;也可以选择消息流经的任何地方;本篇选择electron
的消息处理函数NativeWindowViews::PreHandleMSG
,位于src\electron\atom\browser\native_window_views_win.cc
中;新增消息处理分支:
case WM_DROPFILES: {
IDropTarget* drop_target =
(IDropTarget*)GetPropW(GetAcceleratedWidget(),
L"drag-drop-callback-{2E8EAA96-6DA5-49CB-8DE7-"
L"042F0796CDB6}");
if (!drop_target) {
return false;
}
IDataObject* drop_data = nullptr;
// Create IDataObject and IDropSource COM objects
if (S_OK != CDataObject::CreateDropObject((HGLOBAL)w_param, &drop_data)) {
return false;
}
POINT pt = {};
GetCursorPos(&pt);
DWORD cursor_effect = 0;
drop_target->DragEnter(drop_data, MK_LBUTTON, {pt.x, pt.y}, &cursor_effect);
drop_target->Drop(drop_data, 0, {pt.x, pt.y}, &cursor_effect);
drop_data->Release();
return true;
}
break;
以上代码中,当窗口收到WM_DROPFILES时,
Drop数据对象的创建函数CDataObject::CreateDropObject
的相关实现如下:
// custom_drop_data_impl.hpp
#ifndef CUSTOM_DROP_DATA_IMPL_HPP_
#define CUSTOM_DROP_DATA_IMPL_HPP_
#include
#include
#include
class CEnumFormatEtc : public IEnumFORMATETC {
public:
//
// IUnknown members
//
HRESULT __stdcall QueryInterface(REFIID iid, void** ppvObject) {
// check to see what interface has been requested
if (iid == IID_IEnumFORMATETC || iid == IID_IUnknown) {
AddRef();
*ppvObject = this;
return S_OK;
} else {
*ppvObject = 0;
return E_NOINTERFACE;
}
}
ULONG __stdcall AddRef(void) {
// increment object reference count
return InterlockedIncrement(&m_lRefCount);
}
ULONG __stdcall Release(void) {
// decrement object reference count
LONG count = InterlockedDecrement(&m_lRefCount);
if (count == 0) {
delete this;
return 0;
} else {
return count;
}
}
//
// IEnumFormatEtc members
//
HRESULT __stdcall Next(ULONG celt,
FORMATETC* pFormatEtc,
ULONG* pceltFetched) {
ULONG copied = 0;
// validate arguments
if (celt == 0 || pFormatEtc == 0)
return E_INVALIDARG;
// copy FORMATETC structures into caller's buffer
while (m_nIndex < m_nNumFormats && copied < celt) {
DeepCopyFormatEtc(&pFormatEtc[copied], &m_pFormatEtc[m_nIndex]);
copied++;
m_nIndex++;
}
// store result
if (pceltFetched != 0)
*pceltFetched = copied;
// did we copy all that was requested?
return (copied == celt) ? S_OK : S_FALSE;
}
HRESULT __stdcall Skip(ULONG celt) {
m_nIndex += celt;
return (m_nIndex <= m_nNumFormats) ? S_OK : S_FALSE;
}
HRESULT __stdcall Reset(void) {
m_nIndex = 0;
return S_OK;
}
HRESULT __stdcall Clone(IEnumFORMATETC** ppEnumFormatEtc) {
HRESULT hResult;
// make a duplicate enumerator
hResult = CreateEnumFormatEtc(m_nNumFormats, m_pFormatEtc, ppEnumFormatEtc);
if (hResult == S_OK) {
// manually set the index state
((CEnumFormatEtc*)*ppEnumFormatEtc)->m_nIndex = m_nIndex;
}
return hResult;
}
//
// Construction / Destruction
//
CEnumFormatEtc(FORMATETC* pFormatEtc, int nNumFormats) {
m_lRefCount = 1;
m_nIndex = 0;
m_nNumFormats = nNumFormats;
m_pFormatEtc = new FORMATETC[nNumFormats];
// copy the FORMATETC structures
for (int i = 0; i < nNumFormats; i++) {
DeepCopyFormatEtc(&m_pFormatEtc[i], &pFormatEtc[i]);
}
}
~CEnumFormatEtc() {
if (m_pFormatEtc) {
for (ULONG i = 0; i < m_nNumFormats; i++) {
if (m_pFormatEtc[i].ptd)
CoTaskMemFree(m_pFormatEtc[i].ptd);
}
delete[] m_pFormatEtc;
}
}
private:
LONG m_lRefCount; // Reference count for this COM interface
ULONG m_nIndex; // current enumerator index
ULONG m_nNumFormats; // number of FORMATETC members
FORMATETC* m_pFormatEtc; // array of FORMATETC objects
public:
static void DeepCopyFormatEtc(FORMATETC* dest, FORMATETC* source) {
// copy the source FORMATETC into dest
*dest = *source;
if (source->ptd) {
// allocate memory for the DVTARGETDEVICE if necessary
dest->ptd = (DVTARGETDEVICE*)CoTaskMemAlloc(sizeof(DVTARGETDEVICE));
// copy the contents of the source DVTARGETDEVICE into dest->ptd
*(dest->ptd) = *(source->ptd);
}
}
//
// "Drop-in" replacement for SHCreateStdEnumFmtEtc. Called by
// CDataObject::EnumFormatEtc
//
static HRESULT CreateEnumFormatEtc(UINT nNumFormats,
FORMATETC* pFormatEtc,
IEnumFORMATETC** ppEnumFormatEtc) {
if (nNumFormats == 0 || pFormatEtc == 0 || ppEnumFormatEtc == 0)
return E_INVALIDARG;
*ppEnumFormatEtc = new CEnumFormatEtc(pFormatEtc, nNumFormats);
return (*ppEnumFormatEtc) ? S_OK : E_OUTOFMEMORY;
}
};
class CDataObject : public IDataObject {
public:
//
// IUnknown members
//
HRESULT __stdcall QueryInterface(REFIID iid, void** ppvObject) override {
// check to see what interface has been requested
if (iid == IID_IDataObject || iid == IID_IUnknown) {
AddRef();
*ppvObject = this;
return S_OK;
} else {
*ppvObject = 0;
return E_NOINTERFACE;
}
}
ULONG __stdcall AddRef(void) override {
// increment object reference count
return InterlockedIncrement(&m_lRefCount);
}
ULONG __stdcall Release(void) override {
// decrement object reference count
LONG count = InterlockedDecrement(&m_lRefCount);
if (count == 0) {
delete this;
return 0;
} else {
return count;
}
}
//
// IDataObject members
//
HRESULT __stdcall GetData(FORMATETC* pFormatEtc,
STGMEDIUM* pMedium) override {
int idx;
//
// try to match the requested FORMATETC with one of our supported formats
//
if ((idx = LookupFormatEtc(pFormatEtc)) == -1) {
return DV_E_FORMATETC;
}
//
// found a match! transfer the data into the supplied storage-medium
//
pMedium->tymed = m_pFormatEtc[idx].tymed;
pMedium->pUnkForRelease = 0;
switch (m_pFormatEtc[idx].tymed) {
case TYMED_HGLOBAL:
pMedium->hGlobal = DupMem(m_pStgMedium[idx].hGlobal);
// return S_OK;
break;
default:
return DV_E_FORMATETC;
}
return S_OK;
}
HRESULT __stdcall GetDataHere(FORMATETC* pFormatEtc,
STGMEDIUM* pMedium) override {
// GetDataHere is only required for IStream and IStorage mediums
// It is an error to call GetDataHere for things like HGLOBAL and other
// clipboard formats
//
// OleFlushClipboard
//
return DATA_E_FORMATETC;
}
HRESULT __stdcall QueryGetData(FORMATETC* pFormatEtc) override {
return (LookupFormatEtc(pFormatEtc) == -1) ? DV_E_FORMATETC : S_OK;
}
HRESULT __stdcall GetCanonicalFormatEtc(FORMATETC* pFormatEct,
FORMATETC* pFormatEtcOut) override {
// Apparently we have to set this field to NULL even though we don't do
// anything else
pFormatEtcOut->ptd = NULL;
return E_NOTIMPL;
}
HRESULT __stdcall SetData(FORMATETC* pFormatEtc,
STGMEDIUM* pMedium,
BOOL fRelease) override {
return E_NOTIMPL;
}
HRESULT __stdcall EnumFormatEtc(DWORD dwDirection,
IEnumFORMATETC** ppEnumFormatEtc) override {
if (dwDirection == DATADIR_GET) {
// for Win2k+ you can use the SHCreateStdEnumFmtEtc API call, however
// to support all Windows platforms we need to implement IEnumFormatEtc
// ourselves.
return CEnumFormatEtc::CreateEnumFormatEtc(m_nNumFormats, m_pFormatEtc,
ppEnumFormatEtc);
} else {
// the direction specified is not support for drag+drop
return E_NOTIMPL;
}
}
//
// IDataObject::DAdvise
//
HRESULT __stdcall DAdvise(FORMATETC* pFormatEtc,
DWORD advf,
IAdviseSink* pAdvSink,
DWORD* pdwConnection) override {
return OLE_E_ADVISENOTSUPPORTED;
}
//
// IDataObject::DUnadvise
//
HRESULT __stdcall DUnadvise(DWORD dwConnection) override {
return OLE_E_ADVISENOTSUPPORTED;
}
//
// IDataObject::EnumDAdvise
//
HRESULT __stdcall EnumDAdvise(IEnumSTATDATA** ppEnumAdvise) override {
return OLE_E_ADVISENOTSUPPORTED;
}
//
// Constructor / Destructor
//
CDataObject(FORMATETC* fmtetc, STGMEDIUM* stgmed, int count) {
m_lRefCount = 1;
m_nNumFormats = count;
m_pFormatEtc = new (std::nothrow) FORMATETC[count];
m_pStgMedium = new (std::nothrow) STGMEDIUM[count];
for (int i = 0; i < count; i++) {
m_pFormatEtc[i] = fmtetc[i];
m_pStgMedium[i] = stgmed[i];
}
}
~CDataObject() {
// cleanup
if (m_pFormatEtc)
delete[] m_pFormatEtc;
if (m_pStgMedium)
delete[] m_pStgMedium;
}
private:
HGLOBAL DupMem(HGLOBAL hMem) {
// lock the source memory object
DWORD len = GlobalSize(hMem);
PVOID source = GlobalLock(hMem);
// create a fixed "global" block - i.e. just
// a regular lump of our process heap
PVOID dest = GlobalAlloc(GMEM_FIXED, len);
memcpy(dest, source, len);
GlobalUnlock(hMem);
return dest;
}
int LookupFormatEtc(FORMATETC* pFormatEtc) {
for (int i = 0; i < m_nNumFormats; i++) {
if ((pFormatEtc->tymed & m_pFormatEtc[i].tymed) &&
pFormatEtc->cfFormat == m_pFormatEtc[i].cfFormat &&
pFormatEtc->dwAspect == m_pFormatEtc[i].dwAspect) {
return i;
}
}
return -1;
}
//
// any private members and functions
//
LONG m_lRefCount;
FORMATETC* m_pFormatEtc;
STGMEDIUM* m_pStgMedium;
LONG m_nNumFormats;
public:
static HRESULT CreateDropObject(HGLOBAL hDrop, IDataObject** ppDataObject) {
if (!ppDataObject) {
return E_INVALIDARG;
}
FORMATETC fmtetc = {CF_HDROP, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
STGMEDIUM stgmed = {TYMED_HGLOBAL, {0}, 0};
// transfer the drop handle into the IDataObject
stgmed.hGlobal = hDrop;
return CreateDataObject(&fmtetc, &stgmed, 1, ppDataObject);
}
static HRESULT CreateDataObject(FORMATETC* fmtetc,
STGMEDIUM* stgmeds,
UINT count,
IDataObject** ppDataObject) {
if (ppDataObject == 0)
return E_INVALIDARG;
*ppDataObject = new CDataObject(fmtetc, stgmeds, count);
return (*ppDataObject) ? S_OK : E_OUTOFMEMORY;
}
};
#endif // !CUSTOM_DROP_DATA_IMPL_HPP_
清理工作,在窗口关闭时,清空属性, 原代码如下(src\ui\views\widget\desktop_aura\desktop_drag_drop_client_win.cc
):
void DesktopDragDropClientWin::OnNativeWidgetDestroying(HWND window) {
if (drop_target_.get()) {
RevokeDragDrop(window);
drop_target_ = nullptr;
}
}
改为:
void DesktopDragDropClientWin::OnNativeWidgetDestroying(HWND window) {
if (drop_target_.get()) {
if (IsUserAnAdmin()) {
RemovePropW(window,
L"drag-drop-callback-{2E8EAA96-6DA5-49CB-8DE7-042F0796CDB6}");
} else {
RevokeDragDrop(window);
}
drop_target_ = nullptr;
}
}
清空属性drag-drop-callback-{2E8EAA96-6DA5-49CB-8DE7-042F0796CDB6}
,以避免非预期的使用;
结束;
以上方式的实现也并非完美,WM_DROPFILES
因为仅仅是投放时才会触发,因此在拖放文件时,不能像IDropTarget
一样可以设置鼠标样式,以及相应的移入移除事件;
最完美的办法应为:向explorer中注入一个dll
,由该dll
去检测拖拽事件,并模拟IDropTarget
向electron
发送对应的事件消息;
欢迎指正讨论;
完。