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++把这块区域也设置为非客户区,用户可以拖拽这块区域来移动整个窗口
离屏渲染模式需要自己实现拖拽接口,离屏渲染继承了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
对于普通需求来说这样已经足够了,每一个browser对象都分配了一个对应的CefClient,都有对应的拖拽的实现。不过cef demo里面的实现是拖拽功能必须限制一个窗口内部只有一个browser,而我的需求是一个窗口内多个osr browser,每个browser都可以执行拖拽操作。为此我另外重写了cef demo附带的DropTargetWin,可以让一个窗口支持同时嵌入多个osr browser并完成拖拽。这个不是这篇分享的重点,我就不另外写了。