cef osr拖拽功能實現

轉載請說明原出處,謝謝~~:https://redrain.blog.csdn.net/article/details/107105312

cef顯示web分爲窗口模式和離屏渲染模式(osr,off screen rendering)。窗口模式使用起來比較簡單,基本的功能都已經實現,包括web內部的拖拽。而osr模式需要實現相關接口比較麻煩

窗口模式:

窗口模式的拖拽控制接口只需要關心CefDragHandler。

class CefDragHandler : public virtual CefBaseRefCounted {
 public:
  typedef cef_drag_operations_mask_t DragOperationsMask;

  ///
  // Called when an external drag event enters the browser window. |dragData|
  // contains the drag event data and |mask| represents the type of drag
  // operation. Return false for default drag handling behavior or true to
  // cancel the drag event.
  ///
  /*--cef()--*/
  virtual bool OnDragEnter(CefRefPtr<CefBrowser> browser,
                           CefRefPtr<CefDragData> dragData,
                           DragOperationsMask mask) {
    return false;
  }

  ///
  // Called whenever draggable regions for the browser window change. These can
  // be specified using the '-webkit-app-region: drag/no-drag' CSS-property. If
  // draggable regions are never defined in a document this method will also
  // never be called. If the last draggable region is removed from a document
  // this method will be called with an empty vector.
  ///
  /*--cef()--*/
  virtual void OnDraggableRegionsChanged(
      CefRefPtr<CefBrowser> browser,
      const std::vector<CefDraggableRegion>& regions) {}
};

其中CefDragHandler::OnDragEnter在web中有內容被拖拽時被調用,這時可以根據拖拽的內容,決定是否要阻止拖拽。

CefDragHandler::OnDraggableRegionsChanged是讓web內部自己設置一個拖拽區域,然後通知給c++,讓c++把這塊區域也設置爲非客戶區,用戶可以拖拽這塊區域來移動整個窗口

 

osr模式:

離屏渲染模式需要自己實現拖拽接口,離屏渲染繼承了CefRenderHandler接口,其中有兩個方法是實現拖拽的:

  // Called when the user starts dragging content in the web view. Contextual
  // information about the dragged content is supplied by |drag_data|.
  // (|x|, |y|) is the drag start location in screen coordinates.
  // OS APIs that run a system message loop may be used within the
  // StartDragging call.
  //
  // Return false to abort the drag operation. Don't call any of
  // CefBrowserHost::DragSource*Ended* methods after returning false.
  //
  // Return true to handle the drag operation. Call
  // CefBrowserHost::DragSourceEndedAt and DragSourceSystemDragEnded either
  // synchronously or asynchronously to inform the web view that the drag
  // operation has ended.
  ///
  /*--cef()--*/
  virtual bool StartDragging(CefRefPtr<CefBrowser> browser,
                             CefRefPtr<CefDragData> drag_data,
                             DragOperationsMask allowed_ops,
                             int x,
                             int y) {
    return false;
  }

  ///
  // Called when the web view wants to update the mouse cursor during a
  // drag & drop operation. |operation| describes the allowed operation
  // (none, move, copy, link).
  ///
  /*--cef()--*/
  virtual void UpdateDragCursor(CefRefPtr<CefBrowser> browser,
                                DragOperation operation) {}

其中StartDragging方法是web開始拖拽時的回調,在這裏可以按照windows系統的拖拽模塊來實現一個阻塞的拖拽功能。參照cef demo的寫法,把osr_dragdrop_win.h、osr_dragdrop_win.cc、osr_dragdrop_events.h這三個文件搬過來,裏面實現了windows的拖拽需要的DropTargetWin類。把cef demo的代碼搬過來填充到StartDragging裏。

爲了讓DropTargetWin可以正常工作,需要實現osr_dragdrop_events.h中的OsrDragEvents接口。

除了這些工作,就是windows窗口需要實現拖拽功能,需要調用一個api RegisterDragDrop,這個api讓窗口的拖拽事件與DropTargetWin關聯,當窗口收到拖拽相關消息時會通知DropTargetWin,DropTargetWin再去調用browser中對應一些接口來通知web進行拖拽響應。

理論上實現完這些步驟就可以完成拖拽了。具體的實現代碼可以參考cef client demo。

 

我遇到的坑:

我的osr模式的拖拽實現完畢後,出現了一個奇怪的問題:

  1. 某些網頁中被拖拽的內容鬆開後,會託拽失敗,回到原位
  2. 某些網頁中被拖拽的內容鬆開後,就會執行網頁的跳轉操作

剛碰到這個問題,從現象來看,我以爲是osr模式中一些鼠標座標處理有問題,調試了2天也沒發現問題。與cef demo反覆對比也沒發現什麼差異。最終看StartDragging方法的描述時注意到一點:

  ///
  // Called when the user starts dragging content in the web view. Contextual
  // information about the dragged content is supplied by |drag_data|.
  // (|x|, |y|) is the drag start location in screen coordinates.
  // OS APIs that run a system message loop may be used within the
  // StartDragging call.
  //
  // Return false to abort the drag operation. Don't call any of
  // CefBrowserHost::DragSource*Ended* methods after returning false.
  //
  // Return true to handle the drag operation. Call
  // CefBrowserHost::DragSourceEndedAt and DragSourceSystemDragEnded either
  // synchronously or asynchronously to inform the web view that the drag
  // operation has ended.
  ///
  /*--cef()--*/
  virtual bool StartDragging(CefRefPtr<CefBrowser> browser,
                             CefRefPtr<CefDragData> drag_data,
                             DragOperationsMask allowed_ops,
                             int x,
                             int y);

文檔最後說到在拖拽操作完成後,需要同步異步的調用DragSourceEndedAtDragSourceSystemDragEnded方法來通知拖拽接口。我在StartDragging中的確同步調用了這兩個方法,然後繼續看這兩個方法的文檔:

  ///
  // Call this method when the drag operation started by a
  // CefRenderHandler::StartDragging call has ended either in a drop or
  // by being cancelled. |x| and |y| are mouse coordinates relative to the
  // upper-left corner of the view. If the web view is both the drag source
  // and the drag target then all DragTarget* methods should be called before
  // DragSource* mthods.
  // This method is only used when window rendering is disabled.
  ///
  /*--cef()--*/
  virtual void DragSourceEndedAt(int x, int y, DragOperationsMask op) = 0;

  ///
  // Call this method when the drag operation started by a
  // CefRenderHandler::StartDragging call has completed. This method may be
  // called immediately without first calling DragSourceEndedAt to cancel a
  // drag operation. If the web view is both the drag source and the drag
  // target then all DragTarget* methods should be called before DragSource*
  // mthods.
  // This method is only used when window rendering is disabled.
  ///
  /*--cef()--*/
  virtual void DragSourceSystemDragEnded() = 0;

文檔裏描述DragTarget* 等方法需要在DragSource*等方法之前被調用,於是我下斷點調試,發現的確是DragTarget*等方法在DragSource*之後被調用了。

原因是我開始了cef的多線程消息循環multi_threaded_message_loop)。DragTarget*等方法在主程序的ui線程(因爲用了多線程消息循環,所以主程序的ui線程和cef的ui線程是兩個獨立線程)裏被調用了。他們內部發現線程並不是cef的ui線程,所以會被DragTarget*等方法的調用轉到cef的ui線程。從而導致DragTarget*等方法的調用被延遲了,所以導致了最終的bug。

 

但是爲什麼DragTarget*等方法會在主程序的ui線程裏觸發呢?DragTarget*等方法是在StartDragging調用了win32的api ::DoDragDop而從同步觸發的,StartDragging是在cef的ui線程被觸發的,怎麼同步觸發到DragTarget*等方法就變成了主程序的ui線程了?

最終我發現是我之前說道的win32 api RegisterDragDrop的一個細節,我在主程序的ui線程裏調用了這個api,如果在cef的ui線程裏調用。那麼DragTarget*等方法就會在cef的ui線程裏被觸發了。bug就解決了!

RegisterDragDrop內部會在調用這個API的線程裏創建一個窗口,用過這個窗口來做消息循環模擬阻塞的過程,所以哪個線程調用RegisterDragDrop,就會在哪個線程阻塞並觸發IDragTarget回調。見https://docs.microsoft.com/zh-cn/windows/win32/api/ole2/nf-ole2-registerdragdrop

總結:

執行::DoDragDrop時,會在調用RegisterDragDrop的線程觸發的DragOver、DragLeave、Drop、Drop回調
進而調用browser_->GetHost()->DragTargetDragEnter、DragTargetDragOver、DragTargetDragLeave、DragTargetDrop
這幾個cef接口內部發現不在cef ui線程觸發,則會轉發到cef ui線程
導致DragSourceEndedAt接口被調用時有部分DragTarget*方法沒有被調用
最終拖拽效果就會有問題,詳見DragSourceEndedAt接口描述
所以在cef ui線程調用RegisterDragDrop,讓後面一系列操作都在cef ui線程裏同步執行,則沒問題
    
RegisterDragDrop內部會在調用這個API的線程裏創建一個窗口,用過這個窗口來做消息循環模擬阻塞的過程
所以哪個線程調用RegisterDragDrop,就會在哪個線程阻塞並觸發IDragTarget回調
見https://docs.microsoft.com/zh-cn/windows/win32/api/ole2/nf-ole2-registerdragdrop

題外話:

 

對於普通需求來說這樣已經足夠了,每一個browser對象都分配了一個對應的CefClient,都有對應的拖拽的實現。不過cef demo裏面的實現是拖拽功能必須限制一個窗口內部只有一個browser,而我的需求是一個窗口內多個osr browser,每個browser都可以執行拖拽操作。爲此我另外重寫了cef demo附帶的DropTargetWin,可以讓一個窗口支持同時嵌入多個osr browser並完成拖拽。這個不是這篇分享的重點,我就不另外寫了。

 

Redrain

QQ:491646717

2020.7.3

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章