转载请说明原出处,谢谢~~: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并完成拖拽。这个不是这篇分享的重点,我就不另外写了。
Redrain
QQ:491646717
2020.7.3
来源:oschina
链接:https://my.oschina.net/u/4355947/blog/4335558