CefSharp 实现类似 Electron 可拖拽区功能

Electron 文档 中这样写道:

可拖拽区:默认情况下, 无边框窗口是不可拖拽的。 应用程序需要在 CSS 中指定 -webkit-app-region: drag 来告诉 Electron 哪些区域是可拖拽的(如操作系统的标准标题栏),在可拖拽区域内部使用 -webkit-app-region: no-drag 则可以将其中部分区域排除。 请注意, 当前只支持矩形形状。

如果能让 CefSharp 实现像 Electron 这样的功能,那我就能在设置了无边框的窗体上美滋滋地自定义标题栏,例如:Material Design, Fluent Design… 而不仅仅是 WinForm 自带的风格。

效果 GIF

成品 Example

传送到 GitHub

Why CefSharp?

Why should you consider CEF as an alternative to Electron? Because it is more flexible and allows very powerful combinations of different technologies. – 来源于网络

个人觉得 CefSharp 是直接上 CEF,后端是 C#,你想干嘛就干嘛,这点很爽。

其次是做的 APP 体积会比 Electron 小,占用内存也会比较小。

参见:传送门

实现过程

你可以选择跳过这里,来节约时间,直接看源码:Form1.cs

思路是这样的:监听鼠标点击事件,当鼠标左键按下的时候,判断是否在 设置了 CSS -webkit-app-region: drag; 的区域内,若 True 则执行像操作系统的标准标题栏那样的拖拽。

鼠标点击事件

首先我尝试直接用 Control.MouseDown += ...,但这是不行的,作为一名 Ctrl+C Ctrl+V 开发者,疯狂翻资料,打开一溜选项卡,知道了原因是:传送门

The underlying CEF library captures all mouse events and does not bubble them up to WinForms. There’s no way around this for WinForms - if you want to handle mouse events you need to do so in JavaScript.

Your alternative is to use the WPF CefSharp control, as this uses the underlying CEF library’s OSR (Off Screen Rendering) mode, in which WPF is in charge of all keyboard and mouse events, and it passes them onto CEF. This would give you an opportunity to intercept whatever events you wanted.

CefSharp 监听鼠标事件需要这样写:传送门

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class ChromeWidgetMessageInterceptor : NativeWindow
{
public ChromeWidgetMessageInterceptor(IntPtr chromeWidgetHostHandle)
{
AssignHandle(chromeWidgetHostHandle);
}

const int WM_MOUSEMOVE = 0x0200;
const int WM_MOUSELEAVE = 0x02A3;

protected override void WndProc(ref Message m)
{
base.WndProc(ref m);

switch (m.Msg) {
case WM_MOUSEMOVE:
Console.WriteLine("WM_MOUSEMOVE");
break;
case WM_MOUSELEAVE:
Console.WriteLine("WM_MOUSELEAVE");
break;
}
}
}

Stackoverflow: C# Detecting a Click in WndProc and calling a function after click happened

WndProc 函数作用:主要在程序中拦截并处理系统消息和自定义消息

比如:Windows 程序会产生很多消息,比如你单击鼠标,移动窗口都会产生消息。这个函数就是默认的消息处理函数。你可以重载这个函数来制定自己的消息处理流程.

在 CS 中,可以重写 WndProc 函数,来捕捉所有发生有窗口消息。这样,我们就可以”篡改”传入的消息,而人为的让窗口改变行为。

参见:传送门

WndProc 函数可以监听到这个窗体的任何鼠标事件,也就是说在任何 Control 上的鼠标事件都会触发 WndProc 函数。

通过重写 WndProc 函数,则可实现当鼠标左键按下 (WM_LBUTTONDOWN = 0x0201) 执行 拖动系统的标准标题栏 操作。

附:WindProc 各种 Message 的常量定义代码 / 各种 Message 的具体解释文档

我们需要的 Message 是 WM_LBUTTONDOWN,文档传送门

Posted when the user presses the left mouse button while the cursor is in the client area of a window. If the mouse is not captured, the message is posted to the window beneath the cursor. Otherwise, the message is posted to the window that has captured the mouse.

A window receives this message through its WindowProc function.

所以,监听鼠标点击事件问题就解决了。

模拟 WinForm 标题栏拖动

至于 拖动系统的标准标题栏 的操作代码怎么写,我又逛起了 Google

找到了:传送门:Make a borderless form movable?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public const int WM_NCLBUTTONDOWN = 0xA1;
public const int HT_CAPTION = 0x2;

[System.Runtime.InteropServices.DllImportAttribute("user32.dll")]
public static extern int SendMessage(IntPtr hWnd, int Msg, int wParam, int lParam);
[System.Runtime.InteropServices.DllImportAttribute("user32.dll")]
public static extern bool ReleaseCapture();

private void Form1_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
ReleaseCapture();
SendMessage(Handle, WM_NCLBUTTONDOWN, HT_CAPTION, 0);
}
}

This essentially does exactly the same as grabbing the title bar of a window, from the window manager’s point of view.

这样,当鼠标拖动到屏幕边界最大化窗口的功能也有了。

注:SendMessage(...) 必须在 UI 线程中执行,且必须异步执行,否则会卡死

1
2
3
4
5
6
// 必须是 BeginInvoke 异步执行,否则在 win7 环境下会卡死
winForm.BeginInvoke((MethodInvoker)delegate
{
ReleaseCapture();
SendMessage(winForm.Handle, WM_NCLBUTTONDOWN, HT_CAPTION, 0);
});

得到设置 CSS -webkit-app-region: drag 的区域

CefSharp 项目中的 CefSharp.Wpf.Example 写过 Handle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// Copyright © 2016 The CefSharp Authors. All rights reserved.
//
// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.

using System;
using System.Collections.Generic;
using System.Drawing;
using CefSharp.Enums;

namespace CefSharp.Wpf.Example.Handlers
{
public class DragHandler : IDragHandler, IDisposable
{
public event Action<Region> RegionsChanged;

public Region draggableRegion; // 需要在当按下鼠标左键时判断指针是否正处于该区域中

bool IDragHandler.OnDragEnter(IWebBrowser browserControl, IBrowser browser, IDragData dragData, DragOperationsMask mask)
{
return false;
}

void IDragHandler.OnDraggableRegionsChanged(IWebBrowser browserControl, IBrowser browser, IList<DraggableRegion> regions)
{
//By default popup browers are native windows in WPF so we cannot handle their drag using this method
if (browser.IsPopup == false)
{
//NOTE: I haven't tested with dynamically adding removing regions so this may need some tweaking
draggableRegion = null;

if (regions.Count > 0)
{
//Take the individual Region and construct a complex Region that represents them all.
// 得到设置了 CSS -webkit-app-region 的所有区域
foreach (var region in regions)
{
var rect = new Rectangle(region.X, region.Y, region.Width, region.Height);

if (draggableRegion == null)
{
draggableRegion = new Region(rect);
}
else
{
if (region.Draggable) // -webkit-app-region: drag
{
draggableRegion.Union(rect);
}
else // -webkit-app-region: no-drag
{
//In the scenario where we have an outer region, that is draggable and it has
// an inner region that's not, we must exclude the non draggable.
// Not all scenarios are covered in this example.
draggableRegion.Exclude(rect);
}
}
}
}

var handler = RegionsChanged;

if (handler != null)
{
handler(draggableRegion);
}
}
}

public void Dispose()
{
RegionsChanged = null;
}
}
}

判断指针坐标是否在 Region 内

通过上面的代码,我们得到了 Region draggableRegion

如何判断鼠标指针 (x, y) 是否处于 draggableRegion 区域中?

参阅 Stackoverflow:How to know if a GraphicsPath contains a point in C#

A GraphicsPath has the IsVisible method to check if a point is in the path.

1
2
3
bool isInPath = draggableRegion.IsVisible(x, y)
// 或
bool isInPath = draggableRegion.IsVisible(Point)

官方文档:Region.IsVisible Method

WndProc 函数,如何获取鼠标指针坐标 (x, y) ?

方法一

处理 message.LParam 得到 x, y

1
2
ushort x = LOWORD((uint)message.LParam);
ushort y = HIWORD((uint)message.LParam);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static ushort LOWORD(uint value)
{
return (ushort)(value & 0xFFFF);
}

public static ushort HIWORD(uint value)
{
return (ushort)(value >> 16);
}

public static byte LOWBYTE(ushort value)
{
return (byte)(value & 0xFF);
}

public static byte HIGHBYTE(ushort value)
{
return (byte)(value >> 8);
}

方法二

直接 new 一个 Point

Stackoverflow: WndProc Hook lParam to xy cords?

1
Point point = new Point(message.LParam.ToInt32());

选择方法二,简单直接。

Ending

觉得其他有用的链接:

GitHub: /CefSharp.WinForms.Example/BrowserTabUserControl.cs

GitHub: /CefSharp.Example/Resources/DraggableRegionTest.html

GitHub: /CefSharp.Wpf.Example/Handlers/DragHandler.cs

Stackoverflow: programmatically mouse click in another window

C# 重写WndProc 拦截 发送 系统消息 + windows消息常量值(2)

C# 利用委托跨线程调用UI控件完整使用方法

Stackoverflow C# – 在玻璃上渲染控件:发现解决方案,需要双缓冲/完善

本站文章除注明转载外均为原创,未经允许不要转载哇. ヾ(゚ー゚ヾ) http://qwqaq.com/57a3ffd3.html
分享到