Electron源码学习:让管理员运行的Electron(CEF)支持文件拖拽
背景
前段时间接到一个任务,需要从Windows桌面拖拽文件或文件夹到Electron网页中;网页本身是支持这种拖拽行为的;我们的程序是以管理员权限运行的,在测试的时候发现无法完成这个操作;文件拖拽到界面上时,直接显示了一个禁用的标志;如果换其他的方式来实现(比如:低权限运行,低权限蒙层),实在太过于复杂;没办法,只能尝试去研究为什么不能拖拽。
Windows从Vista开始,启用了UAC
功能;然后造成了我们不能拖的原因是因为UIPI
的机制;它是UAC
功能之一;该功能启用时,用户无法完成地权到高权的文件拖拽操作;
先说结论
要实现本篇文章的目的,需要源码编译Electron
, 如果想要不改源码的情况下做到支持管理员文件拖放,那是非常麻烦的事情,本篇不覆盖这方面的内容,不过文末有思路。
Windows下拖放文件的方式如下:
WM_DROPFILES
:仅支持接收文件“投下”消息;但能够管理员运行;IDropTarget
: 功能全面,但不支持管理员下运行;
Electron
正是采用的IDropTarget
的方式来实现文件拖放功能;但是IDropTarget
因为UIPI
的原因,该操作被阻止,且没有办法放行;有一些"非常规"的手段,可以临时性解决该问题(例如:关闭UAC),但这样操作也太粗暴了。
那么如何解决electron管理员拖放的问题?围绕上面提到的拖放方式来开展,即:
- 以管理员运行时,用
WM_DROPFILES
; - 以正常权限运行时,用
IDropTarget
;
改electron源码这里,很简单就能实现;往下看。
先介绍Windows下实现拖放文件的方法
-
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 <windows.h> #include <shlobj.h> 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
等消息; - 窗口收到WM_DROPFILES消息时,模拟一个
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数据对象;
- 模拟一次Enter事件(必需,在Render中,如果不先触发Enter,Drop事件会被丢弃)
- 模拟一次Drop事件(投放事件)
Drop数据对象的创建函数
CDataObject::CreateDropObject
的相关实现如下:// custom_drop_data_impl.hpp #ifndef CUSTOM_DROP_DATA_IMPL_HPP_ #define CUSTOM_DROP_DATA_IMPL_HPP_ #include <windows.h> #include <ObjIdl.h> #include <ShlObj.h> 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
发送对应的事件消息;
欢迎指正讨论;
完。