轉載請說明原出處,謝謝~~: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模式的拖拽實現完畢後,出現了一個奇怪的問題:
- 某些網頁中被拖拽的內容鬆開後,會託拽失敗,回到原位
- 某些網頁中被拖拽的內容鬆開後,就會執行網頁的跳轉操作
剛碰到這個問題,從現象來看,我以爲是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);
文檔最後說到在拖拽操作完成後,需要同步或異步的調用DragSourceEndedAt和DragSourceSystemDragEnded方法來通知拖拽接口。我在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並完成拖拽。這個不是這篇分享的重點,我就不另外寫了。