Awesome Chrome Form UI - 界面设计与实现

本文详细介绍了如何构建 Awesome Chrome Form UI,包括整合 Vue + Vite + ElementUI + ESLint,自定义 FolderBrowserDialog,JS 注入,窗口 API 设计,事件机制以及远程调试的实现。同时,文章提供了相关参考资料,帮助读者深入理解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

上苍不会让所有幸福集中到某个人身上,得到了爱情未必拥有金钱;拥有金钱未必得到快乐;得到快乐未必拥有健康;拥有健康未必一切都会如愿以偿。知足常乐的心态才是淬炼心智、净化心灵的最佳途径。一切快乐的享受都属于精神,这种快乐把忍受变为享受,是精神对于物质的胜利。这便是人生哲学。

——杨绛

一、Guide

Awesome Chrome Form UI - 框架设计与基础实现-CSDN博客文章浏览阅读817次,点赞26次,收藏21次。Awesome Chrome Form UI - 框架设计与基础实现https://blog.csdn.net/weixin_47560078/article/details/135182049在前面我们已经实现了最基础的框架功能,现在来补充完善这个框架的其他模块,

  1. 新增前端 UI
  2. 修改应用启动逻辑,新增抽象类和接口类
  3. 窗口无边框样式,圆边,阴影
  4. 窗口事件与 API
  5. FolderBrowserDialog 对话框
  6. 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());
        }
    }
}

效果

参考资料

DevTools Snippetsdevtools-snippets is a collection of helpful JavaScript code snippets to use inside of browser devtools.icon-default.png?t=N7T8https://bgrins.github.io/devtools-snippets/

Awesome Chrome Form UI - 框架设计与基础实现-CSDN博客文章浏览阅读871次,点赞26次,收藏21次。Awesome Chrome Form UI - 框架设计与基础实现https://blog.csdn.net/weixin_47560078/article/details/135182049

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

金汐脉动 | PulseTide

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值