Electron源码学习:让管理员运行的Electron(CEF)支持文件拖拽

Electron源码学习:让管理员运行的Electron(CEF)支持文件拖拽

背景

前段时间接到一个任务,需要从Windows桌面拖拽文件或文件夹到Electron网页中;网页本身是支持这种拖拽行为的;我们的程序是以管理员权限运行的,在测试的时候发现无法完成这个操作;文件拖拽到界面上时,直接显示了一个禁用的标志;如果换其他的方式来实现(比如:低权限运行,低权限蒙层),实在太过于复杂;没办法,只能尝试去研究为什么不能拖拽。

Windows从Vista开始,启用了UAC功能;然后造成了我们不能拖的原因是因为UIPI的机制;它是UAC功能之一;该功能启用时,用户无法完成地权到高权的文件拖拽操作;

先说结论

要实现本篇文章的目的需要源码编译Electron, 如果想要不改源码的情况下做到支持管理员文件拖放,那是非常麻烦的事情,本篇不覆盖这方面的内容,不过文末有思路。

Windows下拖放文件的方式如下:

  • WM_DROPFILES:仅支持接收文件“投下”消息;但能够管理员运行
  • IDropTarget: 功能全面,但不支持管理员下运行

Electron正是采用的IDropTarget的方式来实现文件拖放功能;但是IDropTarget因为UIPI的原因,该操作被阻止,且没有办法放行;有一些"非常规"的手段,可以临时性解决该问题(例如:关闭UAC),但这样操作也太粗暴了。

那么如何解决electron管理员拖放的问题?围绕上面提到的拖放方式来开展,即:

  1. 以管理员运行时,用WM_DROPFILES
  2. 以正常权限运行时,用IDropTarget;

改electron源码这里,很简单就能实现;往下看。

先介绍Windows下实现拖放文件的方法

  1. 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.
     }
    

    **管理员运行时:**需要用ChangeWindowMessageFilterChangeWindowMessageFilterEx放行拖放消息;并且给窗口加上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;
    }
    
  2. 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消息;

因此,我们大致方案如下:

  1. 如果是管理员运行,初始化DragDrop模块时,那么不要执行RegisterDragDrop;并开放WM_DROPFILES,WM_COPYDATA等消息;
  2. 窗口收到WM_DROPFILES消息时,模拟一个IDropTarget事件给对应的模块;

实操实现

electron版本6.1.12为例,其他版本大同小异;

  1. 首先找到文件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判断当前的运行权限是否为管理员运行;如果是管理员:

    1. 将当前指针赋值给窗口自定义属性drag-drop-callback-{2E8EAA96-6DA5-49CB-8DE7-042F0796CDB6};
    2. 放行WM_DROPFILES,WM_COPYGLOBALDATA,WM_COPYDATA消息;此处建议使用:ChangeWindowMessageFilterEx函数;
  2. 处理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时,

    1. 创建一个Drop数据对象;
    2. 模拟一次Enter事件(必需,在Render中,如果不先触发Enter,Drop事件会被丢弃)
    3. 模拟一次Drop事件(投放事件)

    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_
    
  3. 清理工作,在窗口关闭时,清空属性, 原代码如下(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},以避免非预期的使用;

  4. 结束;

结语:

以上方式的实现也并非完美,WM_DROPFILES因为仅仅是投放时才会触发,因此在拖放文件时,不能像IDropTarget一样可以设置鼠标样式,以及相应的移入移除事件;

最完美的办法应为:向explorer中注入一个dll,由该dll去检测拖拽事件,并模拟IDropTargetelectron发送对应的事件消息;

欢迎指正讨论;

完。

你可能感兴趣的:(electron,学习)