上苍不会让所有幸福集中到某个人身上,得到了爱情未必拥有金钱;拥有金钱未必得到快乐;得到快乐未必拥有健康;拥有健康未必一切都会如愿以偿。知足常乐的心态才是淬炼心智、净化心灵的最佳途径。一切快乐的享受都属于精神,这种快乐把忍受变为享受,是精神对于物质的胜利。这便是人生哲学。
——杨绛
一、Guide
Awesome Chrome Form UI - 框架设计与基础实现-CSDN博客文章浏览阅读817次,点赞26次,收藏21次。Awesome Chrome Form UI - 框架设计与基础实现https://blog.csdn.net/weixin_47560078/article/details/135182049在前面我们已经实现了最基础的框架功能,现在来补充完善这个框架的其他模块,
- 新增前端 UI
- 修改应用启动逻辑,新增抽象类和接口类
- 窗口无边框样式,圆边,阴影
- 窗口事件与 API
- FolderBrowserDialog 对话框
- JS 注入及其应用
二、UI 框架
使用纯 HTML/JavaScript/CSS 技术构建 UI 框架,
1、整合 Vue + Vite + ElementUI + ESLint
# 创建 vite vue
cnpm create vite@latest
# element-plus 国内镜像 https://element-plus.gitee.io/zh-CN/
# 安装 element-plus
cnpm install element-plus --save
# 安装导入插件
cnpm install -D unplugin-vue-components unplugin-auto-import
# 使用 icon
cnpm install @element-plus/icons-vue
# 安装 eslint
cnpm i -D eslint @babel/eslint-parser
# 初始化配置
npx eslint --init
# 安装依赖
cnpm i @typescript-eslint/eslint-plugin@latest eslint-plugin-vue@latest @typescript-eslint/parser@latest
# 安装插件
cnpm i -D vite-plugin-eslint
配置 vite,
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import eslintPlugin from 'vite-plugin-eslint'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
// ESLint 插件配置
eslintPlugin({
include: ['src/**/*.js', 'src/**/*.vue', 'src/*.js', 'src/*.vue']
}),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
})
在 main.ts 引入 element-plus 样式,
// src\main.ts
import { createApp } from 'vue'
//import './style.css'
import App from './App.vue'
import 'element-plus/dist/index.css'
createApp(App).mount('#app')
配置 eslint 规则,
// .eslintrc.cjs
module.exports = {
"env": {
"browser": true,
"es2021": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:vue/vue3-essential"
],
"overrides": [
{
"env": {
"node": true
},
"files": [
".eslintrc.{js,cjs}"
],
"parserOptions": {
"sourceType": "script"
}
}
],
"parserOptions": {
"ecmaVersion": "latest",
"parser": "@typescript-eslint/parser",
"sourceType": "module"
},
"plugins": [
"@typescript-eslint",
"vue"
],
"rules": {
"@typescript-eslint/no-explicit-any": 1,
"no-console": 1,
"no-debugger": 1,
"no-undefined": 1,
}
}
修改 vite 打包指令,
// package.json
// ......
"build": "vite build"
// ......
三、FolderBrowserDialog(补充)
1、原生 FolderBrowserDialog
ShowFolderBrowserDialog 使用原生 System.Windows.Forms 库,
using System.Windows.Forms;
namespace AwesomeChromeFormUI.Dialogs
{
public class DefaultFolderBrowserDialog
{
/// <summary>
/// 显示文件夹浏览器,返回选中的文件夹路径
/// </summary>
/// <returns></returns>
public static string ShowFolderBrowserDialog()
{
FolderBrowserDialog folderBrowserDialog = new FolderBrowserDialog()
{
Description = "请选择文件夹",
ShowNewFolderButton = true,
SelectedPath = @"D:\MyCodeSpace\AwesomeChromeFormUI",
};
if (folderBrowserDialog.ShowDialog() == DialogResult.OK)
{
return folderBrowserDialog.SelectedPath;
}
return "";
}
}
}
2、自定义 FolderBrowserDialog
引用 Ookii 库,这里使用的版本是 4.0.0,
Ookii.Dialogs.WinForms
ShowVistaFolderBrowserDialog 自定义实现,可以看到用法大致相同,
using Ookii.Dialogs.WinForms;
using System.Windows.Forms;
namespace AwesomeChromeFormUI.Dialogs
{
public class DefaultFolderBrowserDialog
{
/// <summary>
/// 显示文件夹浏览器,返回选中的文件夹路径
/// </summary>
/// <returns></returns>
public static string ShowFolderBrowserDialog()
{
FolderBrowserDialog folderBrowserDialog = new FolderBrowserDialog()
{
Description = "请选择文件夹",
ShowNewFolderButton = true,
SelectedPath = @"D:\MyCodeSpace\AwesomeChromeFormUI",
};
if (folderBrowserDialog.ShowDialog() == DialogResult.OK)
{
return folderBrowserDialog.SelectedPath;
}
return "";
}
/// <summary>
/// 显示文件夹浏览器,返回选中的文件夹路径
/// </summary>
/// <returns></returns>
public static string ShowVistaFolderBrowserDialog()
{
VistaFolderBrowserDialog folderDialog = new VistaFolderBrowserDialog
{
Description = "请选择文件夹",
UseDescriptionForTitle = true,
SelectedPath = @"D:\MyCodeSpace\AwesomeChromeFormUI",
};
if (folderDialog.ShowDialog() == DialogResult.OK)
{
return folderDialog.SelectedPath;
}
return "";
}
}
}
3、拓展 GetSelectedFolderPath
/// <summary>
/// 获取文件夹路径
/// </summary>
/// <param name="form"></param>
/// <param name="useDefaultDialog"></param>
/// <returns></returns>
public static Task<string> GetSelectedFolderPath(this Form form,bool useDefaultDialog=false)
{
// 使用TaskCompletionSource来创建一个未完成的任务
TaskCompletionSource<string> tcs = new TaskCompletionSource<string>();
// 在 UI 线程上执行操作
form.InvokeOnUiThreadIfRequired(() =>
{
// 执行回调方法,并获取结果
string result = useDefaultDialog? DefaultFolderBrowserDialog.ShowFolderBrowserDialog():DefaultFolderBrowserDialog.ShowVistaFolderBrowserDialog();
// 将结果设置到任务完成源,并标记任务为成功
tcs.SetResult(result);
});
// 返回任务对象
return tcs.Task;
}
4、ApplicationBuilder 新增参数
JS 调用 Task<T> 返回值的方法必须开启 ConcurrentTaskExecution 配置,
CefSharpSettings.ConcurrentTaskExecution = true;
ApplicationBuilder 调整如下,
using AwesomeChromeFormUI.ChromiumForms;
using AwesomeChromeFormUI.Interfaces;
using AwesomeChromeFormUI.Interfaces.Implements;
using CefSharp;
using System.Windows.Forms;
namespace AwesomeChromeFormUI.Builder
{
public class ApplicationBuilder
{
private bool _isConcurrentTaskExecution;
public ApplicationBuilder()
{
this._isConcurrentTaskExecution = false;
}
public ApplicationBuilder(bool isConcurrentTaskExecution)
{
this._isConcurrentTaskExecution = isConcurrentTaskExecution;
}
/// <summary>
/// 在一个 BaseForm 中运行应用
/// </summary>
public void Run()
{
try
{
// We're going to manually call Cef.Shutdown below, this maybe required in some complex scenarios
CefSharpSettings.ShutdownOnExit = false;
// 允许 JS 调用 Task<T> 类型返回值的方法
CefSharpSettings.ConcurrentTaskExecution = this._isConcurrentTaskExecution;
// 初始化 CEF
IConfigurationExecuter executer = new CefConfigurationExecuter();
executer.Execute();
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(BaseForm.Instance);
}
finally
{
// Shutdown before your application exists or it will hang.
Cef.Shutdown();
}
}
}
}
5、方法导出示例
/// <summary>
/// 获取文件夹路径
/// </summary>
/// <returns></returns>
public Task<string> GetSelectedFolderPath()
{
return BaseForm.Instance.GetSelectedFolderPath();
}
四、JS 注入(补充)
1、注入 jq
网页加载完成后注入 jq,
browser.FrameLoadEnd += (s, e) => {
if (e.Frame.IsMain)
{
e.Frame.ExecuteJavaScriptAsync(@"(function () {
if (window.jQuery) {
console.log('jQuery already in use!');
return;
}
if (!window.jQuery) {
var dollarInUse = !!window.$;
var s = document.createElement('script');
s.setAttribute('src', 'https://code.jquery.com/jquery-3.6.0.min.js');
s.addEventListener('load', function () {
console.log('jQuery loaded!');
if (dollarInUse) {
jQuery.noConflict();
console.log('`$` already in use; use `jQuery`');
}
});
document.body.appendChild(s);
}
})();");
}
};
2、注入 css 样式
加载网页时注入 js 请求静态资源,
browser.FrameLoadStart += (s, e) =>
{
e.Frame.ExecuteJavaScriptAsync(@"(function () {
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'http://localhost:5173/public/index.css';
document.head.appendChild(link);
})();");
};
3、注入 div
使用 js 比较繁琐,如果已经注入 jq,我们可以使用 jq 在网页上注入自定义 div,
browser.FrameLoadEnd += (s, e) => {
if (e.Frame.IsMain)
{
e.Frame.ExecuteJavaScriptAsync(@"(function () {
addTitleDiv();
/** 新增标题框 */
function addTitleDiv() {
// 标题框 html
const htmlstr = `<div class='my-title-bar'>自定义标题框</div>`;
// 生成标题框 div
const titleDiv = $(htmlstr);
// 获取首个 div 对象
const firstDiv = $('div:first');
// 渲染标题框
firstDiv.before(titleDiv);
}
})();");
}
};
4、注入鼠标特效
browser.FrameLoadStart += (s, e) =>
{
e.Frame.ExecuteJavaScriptAsync(@"
(function () {
// 创建一个 style 元素
var styleElement = document.createElement('style');
// 添加样式内容
styleElement.innerHTML = `
.wave {
width: 100px;
height: 100px;
position: absolute;
background-color: #eb4d4b;
border-radius: 50%;
opacity: 0;
pointer-events: none;
animation: waveAnim 1s linear;
}
@keyframes waveAnim {
0% {
transform: scale(0);
opacity: 0.5;
}
100% {
transform: scale(3);
opacity: 0;
}
}`;
// 将 style 元素添加到 head 标签中
document.head.appendChild(styleElement);
})();
");
};
browser.FrameLoadEnd += (s, e) => {
if (e.Frame.IsMain)
{
e.Frame.ExecuteJavaScriptAsync(@"(function () {
document.addEventListener('click', function (event) {
var wave = document.createElement('div');
wave.className = 'wave';
wave.style.left = (event.clientX - 50) + 'px';
wave.style.top = (event.clientY - 50) + 'px';
document.getElementsByClassName('hpapp')[0].appendChild(wave);
setTimeout(function () {
wave.remove();
}, 1000);
});
})();");
}
};
五、窗口 API
1、GlobalFormHandleCache
缓存全局窗口句柄与上一次窗口状态,
using System.Drawing;
namespace AwesomeChromeFormUI.Entity
{
public class FormLocationEntity
{
/// <summary>
/// 上一次宽度
/// </summary>
public int lastWidth { get; set; }
/// <summary>
/// 上一次高度
/// </summary>
public int lastHeight { get; set; }
/// <summary>
/// 上一次位置
/// </summary>
public Point lastLocation { get; set; }
/// <summary>
/// 是否最大化窗口
/// </summary>
public bool isMaximized { get; set; }
}
}
using AwesomeChromeFormUI.Entity;
using System.Collections.Generic;
namespace AwesomeChromeFormUI.Cache
{
public class GlobalFormHandleCache
{
private Dictionary<string, FormLocationEntity> _formhandleCache;
/// <summary>
/// 单例模式
/// </summary>
private static readonly GlobalFormHandleCache _cache = null;
public static GlobalFormHandleCache Cache
{
get { return _cache; }
}
/// <summary>
/// 私有化构造函数
/// </summary>
private GlobalFormHandleCache()
{
this._formhandleCache = new Dictionary<string, FormLocationEntity>();
}
/// <summary>
/// 静态构造函数
/// </summary>
static GlobalFormHandleCache()
{
_cache = new GlobalFormHandleCache();
}
/// <summary>
/// 新增缓存
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void Add(string key, FormLocationEntity value)
{
this._formhandleCache.Add(key, value);
}
/// <summary>
/// 更新缓存
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void Update(string key, FormLocationEntity value)
{
if (this._formhandleCache.ContainsKey(key))
{
this._formhandleCache[key] = value;
return;
}
this.Add(key, value);
}
/// <summary>
/// 移除缓存
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public bool Remove(string key)
{
return this._formhandleCache.Remove(key);
}
/// <summary>
/// 获取缓存
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public FormLocationEntity GetValueByKey(string key)
{
if (this._formhandleCache.ContainsKey(key))
{
return this._formhandleCache[key];
}
return null;
}
}
}
2、窗口弧度与阴影
DwmApiNativeMethods 为窗口描绘阴影,
using System;
using System.Runtime.InteropServices;
namespace AwesomeChromeFormUI.Native
{
public class DwmApiNativeMethods
{
[StructLayout(LayoutKind.Sequential)]
public struct MARGINS
{
public int Left;
public int Right;
public int Top;
public int Bottom;
public MARGINS(int left, int right, int top, int bottom)
{
Left = left;
Right = right;
Top = top;
Bottom = bottom;
}
}
/// <summary>
/// 该函数的实现方式决定了阴影只能出现在窗口的右侧和底部
/// </summary>
/// <param name="hWnd"></param>
/// <param name="pMarInset"></param>
/// <returns></returns>
[DllImport("dwmapi.dll")]
public static extern int DwmExtendFrameIntoClientArea(IntPtr hWnd, ref MARGINS pMarInset);
}
}
BaseForm 重写 OnPaint 方法绘制弧度,
/// <summary>
/// 窗口配置
/// </summary>
private void InitMainViewConfiguration()
{
MainViewConfiguration configuration = DefaultIMainViewConfiger.CreateCustomMainViewConfiguration();
// 宽度
Width = configuration.Width;
// 高度
Height = configuration.Height;
// 标题
Text = configuration.Text;
// logo
Icon = configuration.Icon;
// 启动位置
StartPosition = configuration.StartPosition;
// 窗口边框
FormBorderStyle = configuration.FormBorderStyle;
// 窗口状态
WindowState = configuration.WindowState;
if (configuration.FormBorderStyle == FormBorderStyle.None)
{
// 启用双缓冲绘制,以减少闪烁
DoubleBuffered = true;
// 设置窗口背景颜色
BackColor = Color.AliceBlue;
TransparencyKey = BackColor;
// 设置窗口区域为圆角
Region = new Region(Drawing2DUtil.GetRoundedRect(new Rectangle(0, 0, Width, Height), 20));
// 添加窗口阴影
SetShadow();
}
}
/// <summary>
/// 重写窗体的OnPaint方法,并在其中绘制具有圆角的窗口边框
/// </summary>
/// <param name="e"></param>
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
// 获取窗口的客户区域
Rectangle rect = new Rectangle(0, 0, this.ClientSize.Width - 1, this.ClientSize.Height - 1);
// 创建一个圆角路径
GraphicsPath path = Drawing2DUtil.GetRoundedRect(rect, 20); // 这里的20是圆角的半径
// 绘制窗口背景
e.Graphics.FillPath(Brushes.LightGray, path);
// 绘制窗口边框
using (Pen pen = new Pen(Color.DarkGray, 2))
{
e.Graphics.DrawPath(pen, path);
}
}
private const int CS_DROPSHADOW = 0x20000;
/// <summary>
/// 重写方法,获取创建控件句柄时所需要的创建参数
/// </summary>
protected override CreateParams CreateParams
{
get
{
CreateParams cp = base.CreateParams;
cp.ClassStyle |= CS_DROPSHADOW;
return cp;
}
}
/// <summary>
/// 窗口阴影的辅助方法
/// </summary>
private void SetShadow()
{
// 设置窗口阴影
var margins = new DwmApiNativeMethods.MARGINS(-1, -1, -1, -1);
DwmApiNativeMethods.DwmExtendFrameIntoClientArea(this.Handle, ref margins);
}
效果,
3、无边框窗口拖拽
拓展 DragWindow 方法,
using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace AwesomeChromeFormUI.CommonExtensions
{
public static class FormExtensions
{
[DllImport("user32.dll")]
public static extern bool ReleaseCapture();
[DllImport("user32.dll")]
public static extern bool SendMessage(IntPtr hwnd, int wMsg, int wParam, int IParam);
/// <summary>
/// 系统命令
/// </summary>
public const int WM_SYSCOMMAND = 0x0112;
/// <summary>
/// 移动窗口的系统命令
/// </summary>
public const int SC_MOVE = 0xF010;
/// <summary>
/// 鼠标位于窗口的标题栏上
/// </summary>
public const int HTCAPTION = 0x0002;
/// <summary>
/// 无边框窗口拖拽
/// SC_MOVE + HTCAPTION 是将移动窗口的命令与标题栏的点击组合起来,以便在拖动标题栏时移动窗口
/// 当用户在当前窗口按住鼠标左键并拖动时,鼠标位置会被识别为位于标题栏上,从而触发移动窗口的操作
/// </summary>
public static void DragWindow(this Form form)
{
form.InvokeOnUiThreadIfRequired(() => {
ReleaseCapture();
SendMessage(form.Handle, WM_SYSCOMMAND, SC_MOVE + HTCAPTION, 0);
});
}
}
}
BaseForm 通过生命周期事件管理 Cache ,
/// <summary>
/// 初始化窗口事件
/// </summary>
private void InitBaseFormEventHanlders()
{
// 显示窗口前加载 URL
Load += (sender, e) =>
{
this._browser.Load(this._browserUrl);
GlobalFormHandleCache.Cache.Add(
this.Handle.ToString(),
new FormLocationEntity()
{
lastHeight = this.Height,
lastWidth = this.Width,
lastLocation = this.Location,
isMaximized = this.WindowState == FormWindowState.Maximized
});
};
// 关闭窗口前释放资源
FormClosing += (sender, e) =>
{
this._browser.Dispose();
GlobalFormHandleCache.Cache.Remove(this.Handle.ToString());
};
// 自适应窗口大小
Resize += (sender, e) =>
{
//this._browser.Width = Width;
this._browser.Width = ClientSize.Width;
//this._browser.Height = Height;
this._browser.Height = ClientSize.Height;
};
}
获取当前活动窗口,
/// <summary>
/// 获取当前活动窗体
/// </summary>
/// <returns></returns>
private static Form GetCurrentForm()
{
Form currentForm = null;
// 使用 Control.Invoke 将代码调用到 UI 线程上
// 这里假设至少有一个打开的窗体 BaseForm
Control control = Application.OpenForms[0];
control.Invoke((MethodInvoker)delegate
{
foreach (Form form in Application.OpenForms)
{
if (form.InvokeRequired)
{
// 如果窗体的 InvokeRequired 属性为 true,则使用委托来获取焦点状态
bool focused = (bool)form.Invoke(new Func<bool>(() => form.ContainsFocus));
if (focused)
{
currentForm = form;
break;
}
}
else
{
// 否则直接访问窗体的 ContainsFocus 属性
if (form.ContainsFocus)
{
currentForm = form;
break;
}
}
}
});
return currentForm;
}
处理鼠标信息,
/// <summary>
/// 处理鼠标信息
/// </summary>
/// <param name="browser"></param>
public static void SetMouseDownJavascriptMessageReceived(this ChromiumWebBrowser browser)
{
browser.JavascriptMessageReceived += MouseDownJavascriptMessageReceived;
}
/// <summary>
/// 接收前端 js 传的 msg
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static void MouseDownJavascriptMessageReceived(object sender, JavascriptMessageReceivedEventArgs e)
{
if (e.Message != null)
{
dynamic ret = e.Message;
switch (ret.type)
{
case SystemConstant.MOUSERDOWN:
{
GetCurrentForm().DragWindow();
break;
}
case SystemConstant.MINIMIZED:
{
GetCurrentForm().ChangeWindowState(SystemConstant.MINIMIZED);
break;
}
case SystemConstant.CLOSE:
{
GetCurrentForm().ChangeWindowState(SystemConstant.CLOSE);
break;
}
case SystemConstant.NORMALIZED:
{
GetCurrentForm().ChangeWindowState(SystemConstant.NORMALIZED);
break;
}
case SystemConstant.RELOAD:
{
e.Browser.Reload();
break;
}
default: break;
}
};
}
自定义处理窗口状态,
/// <summary>
/// 处理窗口状态:最大化/正常化/最小化/关闭
/// </summary>
/// <param name="type"></param>
public static void ChangeWindowState(this Form form,string type)
{
form.InvokeOnUiThreadIfRequired(() => {
if (type.Equals(SystemConstant.MINIMIZED))
{
form.WindowState = FormWindowState.Minimized;
return;
}
if (type.Equals(SystemConstant.MAXIMIZED))
{
form.WindowState = FormWindowState.Maximized;
return;
}
if (type.Equals(SystemConstant.NORMALIZED))
{
string key = form.Handle.ToString();
FormLocationEntity formLocationEntity = GlobalFormHandleCache.Cache.GetValueByKey(key);
if(formLocationEntity == null)
{
return;
}
if (!formLocationEntity.isMaximized)
{
// 更新缓存
formLocationEntity.lastWidth = form.Width;
formLocationEntity.lastHeight = form.Height;
formLocationEntity.lastLocation = form.Location;
formLocationEntity.isMaximized = true;
GlobalFormHandleCache.Cache.Update(key, formLocationEntity);
// 调整窗口宽高
form.Height = Screen.PrimaryScreen.WorkingArea.Height;
form.Width = Screen.PrimaryScreen.WorkingArea.Width;
form.Location = new Point(0,0);
// 设置窗口区域为圆角
form.Region = new Region(Drawing2DUtil.GetRoundedRect(new Rectangle(0, 0, form.Width, form.Height), 20));
// 任务栏高度
//int taskbarHeight = Screen.PrimaryScreen.Bounds.Height - Screen.PrimaryScreen.WorkingArea.Height;
return;
}
// 调整窗口宽高
form.Height = formLocationEntity.lastHeight;
form.Width = formLocationEntity.lastWidth;
form.Location = formLocationEntity.lastLocation;
// 设置窗口区域为圆角
form.Region = new Region(Drawing2DUtil.GetRoundedRect(new Rectangle(0, 0, form.Width, form.Height), 20));
// 更新缓存
formLocationEntity.isMaximized = false;
GlobalFormHandleCache.Cache.Update(key, formLocationEntity);
return;
}
if (type.Equals(SystemConstant.CLOSE))
{
form.Close();
return;
}
});
}
效果,
4、自定义可拖拽区域
实现思路:通过自定义 div 属性指定可拖拽区域,在网页加载完成时,注入 js 监听区域,当该区域发生鼠标拖拽事件时,触发窗口的拖拽动作,
4.1、js 注入
// 获取具有 data-acfui-drag-region 属性的所有 div 元素
const divElements = document.querySelectorAll('div[data-acfui-drag-region]');
// 遍历获取到的元素
divElements.forEach((element) => {
// 获取 data-acfui-drag-region 属性的值
// const valueOfCustomRegion = element.getAttribute('data-acfui-drag-region');
// console.log(valueOfCustomRegion);
// 为每个元素添加鼠标监听事件
element.addEventListener('mousedown', function (event) {
const msg = { type: 'Mousedown' };
CefSharp.PostMessage(msg);
}, false);
});
4.2、data-acfui-drag-region 属性
<template>
<div class="common grid-content">
<div class="common my-button">
<el-button id="minimized-button" @click="minimizedWindow" type="danger" circle />
<el-button id="normalized-button" @click="normalizedWindow" type="primary" circle />
<el-button id="close-button" @click="closeWindow" type="default" circle />
</div>
<div data-acfui-drag-region class="common my-title-bar" id="my-title">
<div> <el-text tag="b">{{mytitle}}</el-text> </div>
</div>
</div>
</template>
4.3、运行效果
5、 API
导出 API,
using AwesomeChromeFormUI.Attributes;
using AwesomeChromeFormUI.CommonExtensions;
using AwesomeChromeFormUI.Constants;
using AwesomeChromeFormUI.Utils;
namespace AwesomeChromeFormUI.Api
{
/// <summary>
/// 主窗口 API 封装
/// </summary>
[JavascriptObject]
public class MainViewApi
{
/// <summary>
/// 关闭主窗口
/// </summary>
public void CloseMainView()
{
MainViewUtil.GetCurrentForm().ChangeWindowState(SystemConstant.CLOSE);
}
/// <summary>
/// 最小化主窗口
/// </summary>
public void MinimizedMainView()
{
MainViewUtil.GetCurrentForm().ChangeWindowState(SystemConstant.MINIMIZED);
}
/// <summary>
/// 正常化主窗口
/// </summary>
public void NormalizedMainView()
{
MainViewUtil.GetCurrentForm().ChangeWindowState(SystemConstant.NORMALIZED);
}
/// <summary>
/// 最大化主窗口
/// </summary>
public void MaximizedMainView()
{
MainViewUtil.GetCurrentForm().ChangeWindowState(SystemConstant.NORMALIZED);
}
}
}
前端使用,
export const minimizedMainView = async () => {
await CefSharp.BindObjectAsync("mainViewApi")
return mainViewApi.minimizedMainView()
}
export const closeMainView = async () => {
await CefSharp.BindObjectAsync("mainViewApi")
return mainViewApi.closeMainView()
}
export const normalizedMainView = async () => {
await CefSharp.BindObjectAsync("mainViewApi")
return mainViewApi.normalizedMainView()
}
<script lang="ts" setup>
import { minimizedMainView, closeMainView, normalizedMainView } from '../apis/MainViewApi'
/**最小化窗口 */
const minimizedWindow = () => {
minimizedMainView();
}
/**关闭窗口 */
const closeWindow = () => {
closeMainView();
}
/**最大/正常窗口 */
const normalizedWindow = () => {
normalizedMainView();
}
</script>
效果,
六、App 抽象类与 MainView 接口
using AwesomeChromeFormUI.ChromiumForms;
using AwesomeChromeFormUI.Interfaces;
using AwesomeChromeFormUI.Interfaces.Implements;
using CefSharp;
using System;
namespace AwesomeChromeFormUI.App
{
/// <summary>
/// Cef 应用抽象类
/// </summary>
public abstract class CefApp : IDisposable
{
protected CefApp()
{
}
/// <summary>
/// 主窗口
/// </summary>
private IMainView _mainView;
protected IMainView MainView { get { return _mainView; } }
/// <summary>
/// 释放非托管资源
/// </summary>
public void Dispose()
{
GC.SuppressFinalize(this);
}
/// <summary>
/// 初始化
/// </summary>
protected abstract void PlatformInitialize();
/// <summary>
/// 主循环
/// </summary>
protected abstract void PlatformRunMessageLoop();
/// <summary>
/// 关闭
/// </summary>
protected abstract void PlatformShutdown();
/// <summary>
/// 退出
/// </summary>
protected abstract void PlatformQuitMessageLoop();
/// <summary>
/// 创建主窗口
/// </summary>
/// <returns></returns>
protected abstract IMainView CreateMainView();
public void Quit()
{
PlatformQuitMessageLoop();
}
/// <summary>
/// 运行入口
/// </summary>
/// <returns></returns>
public int Run(string[] args)
{
// 解析 args
// TODO
// 运行内部方法
return RunInternal(true);
}
/// <summary>
/// Run 内部运行逻辑
/// </summary>
/// <param name="isConcurrentTaskExecution"></param>
/// <returns></returns>
private int RunInternal(bool isConcurrentTaskExecution)
{
// We're going to manually call Cef.Shutdown below, this maybe required in some complex scenarios
CefSharpSettings.ShutdownOnExit = false;
// 允许 JS 调用 Task<T> 类型返回值的方法
CefSharpSettings.ConcurrentTaskExecution = isConcurrentTaskExecution;
// 初始化 CEF
IConfigurationExecuter executer = new CefConfigurationExecuter();
executer.Execute();
// 初始化应用
PlatformInitialize();
// 创建主窗口
this._mainView = CreateMainView();
// 应用运行主循环
PlatformRunMessageLoop();
// 释放主窗口资源
this._mainView.Dispose();
this._mainView = null;
// Shutdown before your application exists or it will hang.
Cef.Shutdown();
// 关闭应用
PlatformShutdown();
return 0;
}
}
}
using AwesomeChromeFormUI.ChromiumForms;
using System.Windows.Forms;
namespace AwesomeChromeFormUI.App
{
public class CefAppImpl : CefApp
{
/// <summary>
/// 创建主窗口
/// </summary>
/// <returns></returns>
protected override IMainView CreateMainView()
{
return new MainViewImpl(this);
}
/// <summary>
/// 初始化应用
/// </summary>
protected override void PlatformInitialize()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
}
/// <summary>
/// 退出应用
/// </summary>
protected override void PlatformQuitMessageLoop()
{
Application.Exit();
}
/// <summary>
/// 应用主循环
/// </summary>
protected override void PlatformRunMessageLoop()
{
Application.Run();
}
/// <summary>
/// 关闭应用
/// </summary>
protected override void PlatformShutdown()
{
}
}
}
using CefSharp.WinForms;
using System;
namespace AwesomeChromeFormUI.ChromiumForms
{
public interface IMainView : IDisposable
{
ChromiumWebBrowser CurrentBrowser { get; }
}
}
七、Program 运行主入口
using AwesomeChromeFormUI.App;
using System;
namespace ExampleApp
{
static class Program
{
/// <summary>
/// 应用程序的主入口点。
/// </summary>
[STAThread]
static int Main(string[] args)
{
using (var application = new CefAppImpl())
{
return application.Run(args);
}
}
}
}
八、远程调试(补充)
配置远程调试端口,
settings.RemoteDebuggingPort = 8090;
访问 http://localhost:8090/,显示 Inspectable WebContents 这意味着远程调试端口已成功配置,并且需要重新打开开发者工具来连接到该端口,
打开Chrome浏览器,输入,
chrome://inspect/#devices
配置目标ip和port,这里为,
localhost:8090
配置完后刷新,会检测到对应的远程页面,点击 inspect 进入远程页面,
就可以进行远程调试了,
九、事件机制(补充)
1、使用 js 原生事件
前端
// index.js
class EventEmitter {
constructor() {
this.events = {};
}
// 监听事件
on(eventName, listener) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(listener);
}
// 触发事件
emit(eventName, ...args) {
const listeners = this.events[eventName];
if (listeners) {
listeners.forEach(listener => {
listener.apply(null, args);
});
}
}
}
const emitter = new EventEmitter();
// 监听事件
emitter.on('event1', (payload) => {
console.log(`Event 1 triggered with payload: ${payload}`);
// 获取 div 元素
var divElement = document.getElementById("guid");
// 修改文本值
divElement.innerText = payload.data;
});
点击按钮 1 触发事件,
<!-- index.html -->
<!doctype html>
<html lang="zh" data-server-rendered="true">
<head>
<title>Demo</title>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
</head>
<body>
<div>
<button id="b1">js event</button>
<div id="guid"></div>
</div>
<script async>
window.onload = async function () {
await CefSharp.BindObjectAsync("guidUtil");
const button1 = document.getElementById('b1');
button1.addEventListener('click', () => {
guidUtil.createGuid();
});
};
</script>
<script type="text/javascript" src="index.js"></script>
</body>
</html>
后端
新增 json 拓展,
Newtonsoft.Json
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace AwesomeChromeFormUI.CommonExtensions
{
public static class JsonExtensions
{
public static string ToJson(this object obj)
{
var timeConverter = new IsoDateTimeConverter { DateTimeFormat = "yyyy-MM-dd HH:mm:ss" };
return JsonConvert.SerializeObject(obj, timeConverter);
}
}
}
定义 EventEmitter,
using AwesomeChromeFormUI.Cache;
using System;
using System.Windows.Forms;
namespace AwesomeChromeFormUI.Emitter
{
public class EventEmitter
{
public static void EmitByJs(string eventName, string payload)
{
// 因为 Browser 属于 UI 控件,必须在 UI 线程上执行
Control control = Application.OpenForms[0];
control.Invoke((MethodInvoker)delegate
{
foreach (Form form in Application.OpenForms)
{
bool focused = (bool)form.Invoke(new Func<bool>(() => form.ContainsFocus));
if (focused)
{
GlobalBrowserCache
.Cache
.GetValueByKey(form.Handle.ToString())
.GetBrowser()
.MainFrame
.ExecuteJavaScriptAsync($"emitter.emit('{eventName}', {payload});");
break;
}
}
});
}
}
}
导出方法,
using AwesomeChromeFormUI.Attributes;
using AwesomeChromeFormUI.CommonExtensions;
using AwesomeChromeFormUI.Emitter;
using System;
namespace ExampleApp.Utils
{
[JavascriptObject]
public class GuidUtil
{
public void CreateGuid()
{
var payload = new { data = Guid.NewGuid().ToString(), code = 100, msg = "OK" };
EventEmitter.EmitByJs("event2", payload.ToJson());
}
}
}
效果
2、使用 mitt 库
前端
# 安装 mitt
cnpm install mitt
将事件总线实例绑定到 window 对象上,
// src\main.ts
import { createApp } from 'vue'
import App from './App.vue'
import 'element-plus/dist/index.css'
import mitt from 'mitt'
// 将事件总线实例绑定到 window 对象上
window.$EventBus = mitt()
createApp(App).mount('#app')
MittTest,
// src\components\MittTest.vue
<template>
<el-button @click="emitEvent" type="primary">Get Guid - {{ primary }}</el-button>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { createGuid } from '../apis/GuidUtil.ts'
const primary = ref('guid')
// 监听事件
window.$EventBus.on('myEvent', (payload: any) => {
console.log('Event received:', payload.message);
primary.value = payload.message;
});
const emitEvent = () => {
createGuid();
};
</script>
后端
public static void EmitByMitt(string eventName, string payload)
{
// 因为 Browser 属于 UI 控件,必须在 UI 线程上执行
Control control = Application.OpenForms[0];
control.Invoke((MethodInvoker)delegate
{
foreach (Form form in Application.OpenForms)
{
bool focused = (bool)form.Invoke(new Func<bool>(() => form.ContainsFocus));
if (focused)
{
GlobalBrowserCache
.Cache
.GetValueByKey(form.Handle.ToString())
.GetBrowser()
.MainFrame
.ExecuteJavaScriptAsync($"window.$EventBus.emit('{ eventName }', {payload});");
break;
}
}
});
}
using AwesomeChromeFormUI.Attributes;
using AwesomeChromeFormUI.CommonExtensions;
using AwesomeChromeFormUI.Emitter;
using System;
namespace ExampleApp.Utils
{
[JavascriptObject]
public class GuidUtil
{
public void CreateGuid()
{
var payload = new { data = Guid.NewGuid().ToString(), code = 100, msg = "OK" };
//EventEmitter.EmitByJs("event1", payload.ToJson());
EventEmitter.EmitByMitt("myEvent", payload.ToJson());
}
}
}