使用 ElementUI 组件构建 Window 桌面应用探索与实践(WPF)

Tip: Beginners mind

Remember the days were you were a beginner. Or memorize, if you still are one. You have never learned enough. Think of yourself as you were a beginner, every day. Always try to see technologies from a beginners mind. You can accept corrections to your software better and leave the standard path if you need it more easily. There are some good ideas even from people who don't have your experience.

零、实现原理与应用案例设计

1、原理

基础实例 Demo 可以参照以下这篇博文,

基于.Net CEF 实现 Vue 等前端技术栈构建 Windows 窗体应用-CSDN博客文章浏览阅读389次。基于 .Net CEF 库,能够使用 Vue 等前端技术栈构建 Windows 窗体应用https://blog.csdn.net/weixin_47560078/article/details/133974513?spm=1001.2014.3001.5501原理非常简单,基于 .Net CEF 实现,用到的库为 CefSharp。

2、优势

  1. 可以使用Vue/React等前端技术美化页面,提升用户友好度
  2. 可以调度操作系统资源,例如打印机,命令行,文件
  3. 前后端开发可以并行

3、劣势

  1. 损失部分性能,占用系统资源更高
  2. 调试需要前后端分开调试,对开发人员要求高,需要懂前后端技术栈
  3. 非跨平台,仅支持Window

4、应用案例

从 Sqlite 数据库中加载数据,并渲染到前端,使用 Echart 进行可视化。

5、技术栈

Vite + Vue3 + TS + Echarts 5.4.3 + ElementUI(plus) + .NET Framework 4.7.2,开发环境为 Win10,VS2019,VS Code。

6、开发流程

  1. 整合 Vue + Vite + ElementUI +Echarts
  2. 把 JS 需要调用的 .Net 方法临时用 JS 方法代替
  3. 页面开发完毕,接着开发 .Net 方法,业务处理逻辑
  4. 导出 .Net 方法,临时 JS 方法替换为真正的 .Net 方法
  5. 最后发布测试

一、前端设计与实现

1、整合 Vue + Vite + ElementUI

# 创建 vite vue
cnpm create vite@latest
# element-plus 国内镜像 https://element-plus.gitee.io/zh-CN/
# 安装 element-plus
cnpm install element-plus --save

按需引入 element plus,

# 安装导入插件
cnpm install -D unplugin-vue-components unplugin-auto-import

在 main.ts 引入 element-plus 和样式,

javascript 复制代码
// app\src\main.ts
import { createApp } from 'vue'
//import './style.css'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
 
createApp(App).use(ElementPlus).mount('#app')

配置 vite,

javascript 复制代码
// app\vite.config.ts
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'
 
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
  ],
})

2、使用图标 Icon

javascript 复制代码
 cnpm install @element-plus/icons-vue

3、使用 ECharts

javascript 复制代码
cnpm install echarts

折线图示例,

html 复制代码
// src\components\MyChart.vue
<template>
  <div ref="chartDom" style="width:100%;height:200px;"> </div>
</template>

<script setup lang="ts">
import * as echarts from 'echarts';
import { ref, onMounted } from 'vue';
type EChartsOption = echarts.EChartsOption;

const chartDom = ref(null)

var option: EChartsOption;
option = {
  title: {
    text: 'Stacked Line'
  },
  tooltip: {
    trigger: 'axis'
  },
  legend: {
    data: ['Email', 'Union Ads', 'Video Ads', 'Direct', 'Search Engine']
  },
  grid: {
    left: '3%',
    right: '4%',
    bottom: '3%',
    containLabel: true
  },
  toolbox: {
    feature: {
      saveAsImage: {}
    }
  },
  xAxis: {
    type: 'category',
    boundaryGap: false,
    data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
  },
  yAxis: {
    type: 'value'
  },
  series: [
    {
      name: 'Email',
      type: 'line',
      stack: 'Total',
      data: [120, 132, 101, 134, 90, 230, 210]
    },
    {
      name: 'Union Ads',
      type: 'line',
      stack: 'Total',
      data: [220, 182, 191, 234, 290, 330, 310]
    },
    {
      name: 'Video Ads',
      type: 'line',
      stack: 'Total',
      data: [150, 232, 201, 154, 190, 330, 410]
    },
    {
      name: 'Direct',
      type: 'line',
      stack: 'Total',
      data: [320, 332, 301, 334, 390, 330, 320]
    },
    {
      name: 'Search Engine',
      type: 'line',
      stack: 'Total',
      data: [820, 932, 901, 934, 1290, 1330, 1320]
    }
  ]
};

onMounted(() => {
  var myChart = echarts.init(chartDom.value);
  myChart.setOption(option);

  // 自适应屏幕
  window.addEventListener('resize', () => {
    myChart.resize()
  })

})

</script>

<style scoped></style>

效果,

4、页签显示

MyChart 页面使用页签切换不同的图表,不同的图表封装为单独的一个组件,由 MyChart 页面按需引入,

html 复制代码
// src\components\MyChart.vue
<template>
  <el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
    <el-tab-pane label="PictorialBar" name="PictorialBar">
      <MyPictorialBar />
    </el-tab-pane>
    <el-tab-pane label="StackedLine" name="StackedLine">
      <MyStackedLine />
    </el-tab-pane>
    <el-tab-pane label="Scatter" name="Scatter">
      <MyScatter />
    </el-tab-pane>
    <el-tab-pane label="Bar" name="Bar">
      <MyBar />
    </el-tab-pane>
    <el-tab-pane label="Radar" name="Radar">
      <MyRadar />
    </el-tab-pane>
    <el-tab-pane label="Graph" name="Graph">
      <MyGraph />
    </el-tab-pane>
    <el-tab-pane label="Gauge" name="Gauge">
      <MyGauge />
    </el-tab-pane>
    <el-tab-pane label="Rich" name="Rich">
      <MyRich />
    </el-tab-pane>
  </el-tabs>
</template>

<script lang="ts" setup>
import { ref } from 'vue'
import type { TabsPaneContext } from 'element-plus'
import MyStackedLine from './MyStackedLine.vue'
import MyPictorialBar from './MyPictorialBar.vue'
import MyScatter from './MyScatter.vue'
import MyBar from './MyBar.vue'
import MyRadar from './MyRadar.vue'
import MyGraph from './MyGraph.vue'
import MyGauge from './MyGauge.vue'
import MyRich from './MyRich.vue'

const activeName = ref('PictorialBar')

const handleClick = (tab: TabsPaneContext, event: Event) => {
  console.log(tab, event)
}
</script>
<style>
.demo-tabs>.el-tabs__content {
  padding: 32px;
  color: #6b778c;
  font-size: 32px;
  font-weight: 600;
}</style>
html 复制代码
// src\components\MyPictorialBar.vue
<template>
    <div ref="chartDom" style="width:1200px;height:450px;"> </div>
</template>
    
<script setup lang="ts">
import * as echarts from 'echarts';
import { ref, onMounted } from 'vue';
type EChartsOption = echarts.EChartsOption;

// 前端 DEBUG 用
const ROOT_PATH = 'http://localhost:5173';
// 后端整合用
// const ROOT_PATH = 'http://wpf.test';

const paperDataURI =
  'data:image/png;base64,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';

const chartDom = ref(null);
let myChart: echarts.ECharts;
var option: EChartsOption;
option = {
  backgroundColor: '#0f375f',
  tooltip: {},
  legend: {
    textStyle: { color: '#ddd' }
  },
  xAxis: [
    {
      data: ['Christmas Wish List', '', 'Qomolangma', 'Kilimanjaro'],
      axisTick: { show: false },
      axisLine: { show: false },
      axisLabel: {
        margin: 20,
        color: '#ddd',
        fontSize: 14
      }
    }
  ],
  yAxis: {
    splitLine: { show: false },
    axisTick: { show: false },
    axisLine: { show: false },
    axisLabel: { show: false }
  },
  markLine: {
    z: -1
  },
  animationEasing: 'elasticOut',
  series: [
    {
      type: 'pictorialBar',
      name: 'All',
      emphasis: {
        scale: true
      },
      label: {
        show: true,
        position: 'top',
        formatter: '{c} m',
        fontSize: 16,
        color: '#e54035'
      },
      data: [
        {
          value: 13000,
          symbol: 'image://' + paperDataURI,
          symbolRepeat: true,
          symbolSize: ['130%', '20%'],
          symbolOffset: [0, 10],
          symbolMargin: '-30%',
          animationDelay: function (_dataIndex, params: any) {
            return params.index * 30;
          }
        },
        {
          value: '-',
          symbol: 'none'
        },
        {
          value: 8844,
          symbol:
            'image://' + ROOT_PATH + '/hill-Qomolangma.png',
          symbolSize: ['200%', '105%'],
          symbolPosition: 'end',
          z: 10
        },
        {
          value: 5895,
          symbol:
            'image://' + ROOT_PATH + '/hill-Kilimanjaro.png',
          symbolSize: ['200%', '105%'],
          symbolPosition: 'end'
        }
      ],
      markLine: {
        symbol: ['none', 'none'],
        label: {
          show: false
        },
        lineStyle: {
          color: '#e54035',
          width: 2
        },
        data: [
          {
            yAxis: 8844
          }
        ]
      }
    },
    {
      name: 'All',
      type: 'pictorialBar',
      barGap: '-100%',
      symbol: 'circle',
      itemStyle: {
        color: '#185491'
      },
      silent: true,
      symbolOffset: [0, '50%'],
      z: -10,
      data: [
        {
          value: 1,
          symbolSize: ['150%', 50]
        },
        {
          value: '-'
        },
        {
          value: 1,
          symbolSize: ['200%', 50]
        },
        {
          value: 1,
          symbolSize: ['200%', 50]
        }
      ]
    }
  ]
};

onMounted(() => {
    myChart = echarts.init(chartDom.value);
    myChart.setOption(option);
    window.addEventListener('resize', () => {
    myChart.resize()
  })
})

</script>
    
<style scoped></style>
    
html 复制代码
// src\components\MyStackedLine.vue
<template>
  <div ref="chartDom" style="width:1200px;height:450px;"> </div>
</template>
  
<script setup lang="ts">
import * as echarts from 'echarts';
import { ref, onMounted } from 'vue';
type EChartsOption = echarts.EChartsOption;
import { SaveAsImage } from '../api/ImageUtil'
import { exportSeriesData } from '../api/DataUtil'

const chartDom = ref(null)
let myChart: echarts.ECharts
var option: EChartsOption;


// 手动触发保存图像
const saveChartAsImage = () => {
  SaveAsImage(myChart.getDataURL()).then((res) => {
    console.log(res);
  });
}

onMounted(() => {
  myChart = echarts.init(chartDom.value);
  exportSeriesData().then((data) => {
    // console.log('exportSeriesData', data)
    option = {
      title: {
        text: 'Stacked Line'
      },
      tooltip: {
        trigger: 'axis'
      },
      legend: {
        data: ['Email', 'Union Ads', 'Video Ads', 'Direct', 'Search Engine']
      },
      grid: {
        left: '3%',
        right: '4%',
        bottom: '3%',
        containLabel: true
      },
      toolbox: {
        feature: {
          // saveAsImage: {},
          myTool1: {
            show: true,
            title: 'saveAsImage',
            icon: 'image://icon_download.png',
            iconStyle: {
              normal: {
                width: '20px',
                color: '#000',  // 图标颜色
                borderColor: '#000',  // 图标边框颜色
                borderWidth: 1,  // 图标边框宽度
                borderType: 'solid',  // 图标边框类型
                opacity: 1  // 图标透明度
              },
              emphasis: {
                color: '#ff0000'  // 鼠标悬停时的图标颜色
              }
            },
            onclick: function () {
              saveChartAsImage();
            }
          },
          // myTool2: {
          //   show: true,
          //   title: '自定义扩展方法',
          //   icon: 'image://https://echarts.apache.org/zh/images/favicon.png',
          //   onclick: function () {
          //     alert('myToolHandler2')
          //   }
          // },
          restore: {
            icon: 'image://icon_reset.png'
          },
          dataView: {
            icon: 'image://icon_detail.png'
          },
          // dataZoom: {},
        }
      },
      xAxis: {
        type: 'category',
        boundaryGap: false,
        data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
      },
      yAxis: {
        type: 'value'
      },
      series: data
    };
    myChart.setOption(option);
  })
  
  // 自适应屏幕
  window.addEventListener('resize', () => {
    myChart.resize()
  })

})

</script>
  
<style scoped></style>
  

5、导出图片

echarts 官方提供了五个工具,但是导出图片在 CEF 中是不可用的,其他工具都能正常使用,因此需要重写导出图片方法,思路如下:

1、前端获取 echarts 的图表 canvas

2、将 canvas 转为 Base64 字符串传给 .NET 方法

3、C# 解析 Base64 保存为图片

javascript 复制代码
  toolbox: {
    feature: {
      // saveAsImage: {},
      myTool1: {
        show: true,
        title: 'saveAsImage',
        icon: 'path://M160 832h704a32 32 0 1 1 0 64H160a32 32 0 1 1 0-64zm384-253.696 236.288-236.352 45.248 45.248L508.8 704 192 387.2l45.248-45.248L480 584.704V128h64v450.304z',
        onclick: function () {
          saveChartAsImage();
        }
      },
      restore: {},
      dataView: {},
      dataZoom: {},
    }
  },
javascript 复制代码
// 手动触发保存图像
const saveChartAsImage = () => {
  console.log(myChart.getDataURL());
}

注意 ,这里 canvas 有一个跨域访问的问题,需要配置服务端允许跨域访问,否则会报错,

cs 复制代码
// InitCefSettings 允许跨域访问
settings.CefCommandLineArgs.Add("disable-web-security");

6、Vite 配置 ESLint

bash 复制代码
# 安装 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,

javascript 复制代码
// 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()],
    }),
  ],
})

配置 eslint 规则,

javascript 复制代码
// .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,
        "no-undef":1,
    }
}

补充:打包时需要修改 build 指令,

7、API 封装

调用 .NET exportSeriesData 方法获取图表数据,

javascript 复制代码
// src\api\DataUtil.ts

export const exportSeriesData = async (): Promise<any> => {
    await CefSharp.BindObjectAsync("exportDataUtil")
    return exportDataUtil.exportSeriesData()
}

调用 .NET saveAsImage 方法获取将 Base64 字符保存为图片,

javascript 复制代码
// src\api\ImageUtil.ts
export const SaveAsImage = async (data:string): Promise<any> => {
    await CefSharp.BindObjectAsync("imageUtil")
    return imageUtil.saveAsImage(data)
}

二、后端设计与实现

1、新建 WPF 项目

项目的初始化结构非常简单,与 WinForm 项目结构相似,App.xaml 是程式的主入口,MainWindow.xaml 是自定义窗口,App.xaml.cs(WPF) 与 Program.cs(WinForm) 相似,MainWindow(WPF) 与 Form1(相似)。

2、安装 CefSharp 程序包

CefSharp.Wpf

3、添加 ChromiumWebBrowser

可以直接在 MainWindow.xaml 中添加,

XML 复制代码
// MainWindow.xaml
<Window x:Class="CefWpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:CefWpfApp" 
        xmlns:wpf="clr-namespace:CefSharp.Wpf;assembly=CefSharp.Wpf"
        mc:Ignorable="d"
        Title="MainWindow" 
        WindowState="Maximized">
    <Grid>
        <!-- Add a xmlns:wpf="clr-namespace:CefSharp.Wpf;assembly=CefSharp.Wpf" attribute to your parent control -->
        <!-- Create a new instance in code or in `xaml` -->
        <Border Grid.Row="1" BorderBrush="Gray" BorderThickness="0,1">
            <wpf:ChromiumWebBrowser x:Name="Browser" Address="www.baidu.com"/>
        </Border>
    </Grid>
</Window>

也可以通过控件添加,

XML 复制代码
// MainWindow.xaml
<Window x:Class="CefWpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:CefWpfApp" 
        xmlns:wpf="clr-namespace:CefSharp.Wpf;assembly=CefSharp.Wpf" 
        mc:Ignorable="d"
        Title="MainWindow" 
        WindowState="Maximized">
    <Grid x:Name="grid">
        
    </Grid>
</Window>
cs 复制代码
// CefWpfApp\MainWindow.xaml.cs
using CefSharp;
using CefSharp.JavascriptBinding;
using CefSharp.Wpf;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace CefWpfApp
{
    
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {
      
        /// <summary>
        /// ChromiumWebBrowser
        /// </summary>
        private static ChromiumWebBrowser browser;

        public MainWindow()
        {
            InitializeComponent();
            AddChromiumWebBrowser();
        }

        /// <summary>
        /// Create a new instance in code or add via the designer
        /// </summary>
        private void AddChromiumWebBrowser()
        {
            browser = new ChromiumWebBrowser("http://wpf.test");
            this.grid.Children.Add(browser);

        }
    }
}

效果,

4、异步 UI 线程封装

cs 复制代码
using System;
using System.Windows.Threading;

namespace CefWpfApp.Dispatchers
{
    public static class DispatcherExtensions
    {
        /// <summary>
        /// Executes the Action asynchronously on the UI thread, does not block execution on the calling thread.
        /// </summary>
        /// <param name="dispatcher">the dispatcher for which the update is required</param>
        /// <param name="action">action to be performed on the dispatcher</param>
        public static void InvokeOnUiThreadIfRequired(this Dispatcher dispatcher, Action action)
        {
            if (dispatcher.CheckAccess())
            {
                action.Invoke();
            }
            else
            {
                dispatcher.BeginInvoke(action);
            }
        }
    }
}

直接在项目属性中配置,或者在 MainWindow 中使用 Icon 属性,

XML 复制代码
<Window x:Class="CefWpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"  
        Title="MainWindow" 
        WindowState="Maximized"
        Icon="./Logo/mylogo.ico">
    <Grid x:Name="grid">
        
    </Grid>
</Window>

6、配置右键菜单

cs 复制代码
        /// <summary>
        /// 自定义右键菜单
        /// </summary>
        public class CustomContextMenuHandler : IContextMenuHandler
        {
            /// <summary>
            /// 上下文菜单列表,在这里加菜单
            /// </summary>
            /// <param name="chromiumWebBrowser"></param>
            /// <param name="browser"></param>
            /// <param name="frame"></param>
            /// <param name="parameters"></param>
            /// <param name="model"></param>
            void IContextMenuHandler.OnBeforeContextMenu(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model)
            {
                if (model.Count > 0)
                {
                    // 添加分隔符
                    model.AddSeparator();
                }
                model.AddItem((CefMenuCommand)29501, "Show DevTools");
            }

            /// <summary>
            /// 上下文菜单指令,这里实现菜单要做的事情
            /// </summary>
            /// <param name="chromiumWebBrowser"></param>
            /// <param name="browser"></param>
            /// <param name="frame"></param>
            /// <param name="parameters"></param>
            /// <param name="commandId"></param>
            /// <param name="eventFlags"></param>
            /// <returns></returns>
            bool IContextMenuHandler.OnContextMenuCommand(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags)
            {
                if (commandId == (CefMenuCommand)29501)
                {
                    browser.GetHost().ShowDevTools();
                    return true;
                }
                return false;
            }

            void IContextMenuHandler.OnContextMenuDismissed(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame)
            {

            }

            bool IContextMenuHandler.RunContextMenu(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model, IRunContextMenuCallback callback)
            {
                // 必须返回 false
                return false;
            }
        }
cs 复制代码
        /// <summary>
        /// Create a new instance in code or add via the designer
        /// </summary>
        private void AddChromiumWebBrowser()
        {
            browser = new ChromiumWebBrowser("http://wpf.test");
            // 配置右键菜单
            browser.MenuHandler = new CustomContextMenuHandler();
            this.grid.Children.Add(browser);
        }

7、启动屏幕界面

直接引入图片,并将文件的属性设置为 "SplashScreen",

或者在 App.xaml.cs 中重写 OnStartUp 方法,

cs 复制代码
        /// <summary>
        /// 重写启动方法
        /// </summary>
        /// <param name="e"></param>
        protected override void OnStartup(StartupEventArgs e)
        {
            SplashScreen splashScreen = new SplashScreen("chromium-256.ico");
            // true => SplashScreen 自动关闭
            // false => SplashScreen 不关闭
            splashScreen.Show(false);
            // 配置 SplashScreen 关闭时间,1s
            // SplashScreen 的关闭时间是基于主窗口加载完成的时间。
            // 如果主窗口的加载时间比 1 秒要短,那么 SplashScreen 将会在主窗口加载完成后立即关闭。
            splashScreen.Close(new TimeSpan(0, 0, 1));
            base.OnStartup(e);

        }

注意:

1、Show 方法参数需要设置为 false 才不会自动关闭界面,如果主窗口的加载时间比 1 秒要短,那么 SplashScreen 将会在主窗口加载完成后立即关闭。

2、引入资源为 "嵌入的资源" 类型。

8、加载等待效果

XML 复制代码
<Window x:Class="CefWpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"  
        Title="MainWindow" 
        WindowState="Maximized">
    <Grid>
        <Grid x:Name="isLoading" Background="White" HorizontalAlignment="Center" Visibility="Collapsed">
            <Grid.Resources>
                <Style x:Key="rec" TargetType="Rectangle">
                    <Setter Property="Width" Value="10"/>
                    <Setter Property="Height" Value="30"/>
                    <Setter Property="Fill" Value="#409eff"/>
                </Style>
                <PowerEase x:Key="powerEase" Power="3" EasingMode="EaseInOut"/>
            </Grid.Resources>
            <Grid.Triggers>
                <EventTrigger RoutedEvent="Loaded">
                    <BeginStoryboard>
                        <Storyboard RepeatBehavior="Forever" Storyboard.TargetProperty="Height">
                            <DoubleAnimation Storyboard.TargetName="rec1" To="50" BeginTime="0:0:0.0" Duration="0:0:0.5" EasingFunction="{StaticResource powerEase}" AutoReverse="True"/>
                            <DoubleAnimation Storyboard.TargetName="rec2" To="50" BeginTime="0:0:0.2" Duration="0:0:0.5" EasingFunction="{StaticResource powerEase}" AutoReverse="True"/>
                            <DoubleAnimation Storyboard.TargetName="rec3" To="50" BeginTime="0:0:0.4" Duration="0:0:0.5" EasingFunction="{StaticResource powerEase}" AutoReverse="True"/>
                            <DoubleAnimation Storyboard.TargetName="rec4" To="50" BeginTime="0:0:0.6" Duration="0:0:0.5" EasingFunction="{StaticResource powerEase}" AutoReverse="True"/>
                            <DoubleAnimation Storyboard.TargetName="rec5" To="50" BeginTime="0:0:0.8" Duration="0:0:0.5" EasingFunction="{StaticResource powerEase}" AutoReverse="True"/>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
            </Grid.Triggers>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="15"/>
                <ColumnDefinition Width="15"/>
                <ColumnDefinition Width="15"/>
                <ColumnDefinition Width="15"/>
                <ColumnDefinition Width="15"/>
            </Grid.ColumnDefinitions>
            <Label Content="Loading"
           FontSize="18"
           FontFamily="Times New Roman"
           Foreground="#252c41"
           FontWeight="Bold"
           Grid.ColumnSpan="5"
           VerticalContentAlignment="Center"
           HorizontalContentAlignment="Center"
           Margin="0,80,0,0"/>
            <Rectangle Name="rec1" Grid.Column="0" Style="{StaticResource rec}"/>
            <Rectangle Name="rec2" Grid.Column="1" Style="{StaticResource rec}"/>
            <Rectangle Name="rec3" Grid.Column="2" Style="{StaticResource rec}"/>
            <Rectangle Name="rec4" Grid.Column="3" Style="{StaticResource rec}"/>
            <Rectangle Name="rec5" Grid.Column="4" Style="{StaticResource rec}"/>
        </Grid>
        <Grid x:Name="contentGrid">
           
        </Grid>
    </Grid>
</Window>
cs 复制代码
        /// <summary>
        /// 状态更新处理
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        private void OnLoadingStateChanged(object sender, LoadingStateChangedEventArgs args)
        {
            if (args.IsLoading)
            {
                // 在 UI 线程上执行操作
                Application.Current.Dispatcher.InvokeOnUiThreadIfRequired(() =>
                {
                    // 加载中
                    isLoading.Visibility = Visibility.Visible;
                });
            }
            else
            {
                Application.Current.Dispatcher.InvokeOnUiThreadIfRequired(() =>
                {
                    // 隐藏进度条
                    isLoading.Visibility = Visibility.Collapsed;
                });
            }
        }
cs 复制代码
// 加载状态变更
browser.LoadingStateChanged += OnLoadingStateChanged;

效果,

9、窗口对话框

可以使用 Windows Forms 控件,但是它的风格跟外观不一定能跟 WPF 兼容,

cs 复制代码
using Ookii.Dialogs.Wpf;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace CefWpfApp.Dialogs
{
    public class CustomerFolderBrowserDialog
    {
        /// <summary>
        /// 委托实现,显示文件夹浏览器,返回选中的文件夹路径
        /// </summary>
        /// <returns></returns>
        public static string ShowFolderBrowserDialog_WPF()
        {
            VistaFolderBrowserDialog folderDialog = new VistaFolderBrowserDialog
            {
                Description = "请选择文件夹",
                UseDescriptionForTitle = true,
                SelectedPath = @"D:\AwsomeWorkSpace\CEFWPFAPP\bin\Debug",
            };

            if (folderDialog.ShowDialog() == true)
            {
                return folderDialog.SelectedPath;
            }
            return "";
        }

        /// <summary>
        /// 委托实现,显示文件夹浏览器,返回选中的文件夹路径
        /// </summary>
        /// <returns></returns>
        public static string ShowFolderBrowserDialog_WinForms()
        {
            FolderBrowserDialog folderBrowserDialog = new FolderBrowserDialog()
            {
                Description = "请选择文件夹",
                ShowNewFolderButton = true,
                SelectedPath = @"D:\AwsomeWorkSpace\CEFWPFAPP\bin\Debug",
            };
            if (folderBrowserDialog.ShowDialog() == DialogResult.OK)
            {
                return folderBrowserDialog.SelectedPath;
            }
            return "";
        }
    }
}

为了更符合 WPF ,采用第三方 Ookii.Dialogs.Wpf 库,

在 MainWindow.xaml.cs 中创建异步方法 GetSelectedFolderPath(),

cs 复制代码
        /// <summary>
        /// 委托
        /// </summary>
        /// <returns></returns>        
        public delegate string MyFolderBrowserDialog();

        /// <summary>
        /// 获取文件夹路径
        /// </summary>
        public Task<string> GetSelectedFolderPath()
        {
            // 使用TaskCompletionSource来创建一个未完成的任务
            var tcs = new TaskCompletionSource<string>();

            // 在 UI 线程上执行操作
            Application.Current.Dispatcher.InvokeOnUiThreadIfRequired(() =>
            {
                // 执行回调方法,并获取结果
                string result = CustomerFolderBrowserDialog.ShowFolderBrowserDialog_WPF();
                // 将结果设置到任务完成源,并标记任务为成功
                tcs.SetResult(result);
            });

            // 返回任务对象
            return tcs.Task;
        }

效果,

10、Base64 转图片

cs 复制代码
// ImageUtil.cs
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Threading.Tasks;

namespace CefWpfApp.Utils
{
    public class ImageUtil
    {
        public async Task<object> SaveAsImage(string base64String)
        {
            // 结果
            Dictionary<String, Object> res = new Dictionary<String, Object>(3);

            if (string.IsNullOrEmpty(base64String))
            {
                // 返回结果
                res.Add("code", "0");
                res.Add("msg", "fail");
                res.Add("isSuccess", false);
                return res;
            }

            string path = await App.mainWindow.GetSelectedFolderPath();
            if (string.IsNullOrEmpty(path))
            {
                // 返回结果
                res.Add("code", "0");
                res.Add("msg", "fail");
                res.Add("isSuccess", false);
                return res;
            }

            // 将 Base64 字符串转换为字节数组,要去掉 header,否则会解析出错
            byte[] imageBytes = Convert.FromBase64String(base64String.Replace("data:image/png;base64,",""));

            // 创建内存流,并将字节数组写入流
            using (MemoryStream ms = new MemoryStream(imageBytes))
            {
                // 使用流创建图片对象
                Image image = Image.FromStream(ms);
                // 保存图片到文件
                image.Save(string.Format("{0}\\image-{1}.png", path, Guid.NewGuid().ToString()));
            }

            // 返回结果
            res.Add("code", "1");
            res.Add("msg", "ok");
            res.Add("isSuccess", true);
            return res;
        }
    }
}

注意,这里 SaveAsImage 方法的返回类型是 Task<object> 可等待的异步类型,需要配置 ConcurrentTaskExecution 属性为 true ,

cs 复制代码
CefSharpSettings.ConcurrentTaskExecution = true;

否则会报错,

11、数据实体类封装

创建两个实体类,SeriesEntity 用于返回 Echarts 图表数据实体,StackedLineEntity 用于返回数据库实体,

cs 复制代码
using System.Collections.Generic;

namespace CefWpfApp.Entity
{
    public class SeriesEntity
    {
        public string name { get; set; }
        public string type { get; set; }
        public string stack { get; set; }
        public List<int> data { get; set; }
    }
}
cs 复制代码
namespace CefWpfApp.Entity
{
    public class StackedLineEntity
    {
        public int Email { get; set; }
        public int Unio_Ads { get; set; }
        public int Video_Ads { get; set; }
        public int Direct { get; set; }
        public int Search_Engine { get; set; }

        public  StackedLineEntity(int Email, int Unio_Ads, int Video_Ads, int Direct, int Search_Engine)
        {
            this.Email = Email;
            this.Unio_Ads = Unio_Ads;
            this.Video_Ads = Video_Ads;
            this.Direct = Direct;
            this.Search_Engine = Search_Engine;
        }
    }
}

12、数据库工具类

工具类封装了连接字符串方法、创建数据库文件方法、生成模拟数据方法、查询数据表方法、处理空值方法,

cs 复制代码
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SQLite;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CefWpfApp.Utils
{
    public class MySqliteUtil
    {
       
        /// <summary>
        /// 生成模拟数据
        /// </summary>
        public static void GenerateStackedLineEntityData()
        {
            string connectionString = CreateConnectionString();
            using (SQLiteConnection connection = new SQLiteConnection(connectionString))
            {
                connection.Open();
                // 创建表
                using (var command = new SQLiteCommand("CREATE TABLE IF NOT EXISTS StackedLine (Id INTEGER PRIMARY KEY, Email INTEGER, Unio_Ads INTEGER, Video_Ads INTEGER, Direct INTEGER, Search_Engine INTEGER)", connection))
                {
                    command.ExecuteNonQuery();
                }
                
                // 插入数据
                for (int i = 0; i < 7; i++)
                {
                    using (var command = new SQLiteCommand("INSERT INTO StackedLine (Email, Unio_Ads, Video_Ads, Direct, Search_Engine) VALUES (@Email, @Unio_Ads, @Video_Ads, @Direct, @Search_Engine)", connection))
                    {
                        command.Parameters.AddWithValue("@Email", Math.Abs(Guid.NewGuid().GetHashCode() % 64));
                        command.Parameters.AddWithValue("@Unio_Ads", Math.Abs(Guid.NewGuid().GetHashCode() % 128));
                        command.Parameters.AddWithValue("@Video_Ads", Math.Abs(Guid.NewGuid().GetHashCode() % 64));
                        command.Parameters.AddWithValue("@Direct", Math.Abs(Guid.NewGuid().GetHashCode() % 128));
                        command.Parameters.AddWithValue("@Search_Engine", Math.Abs(Guid.NewGuid().GetHashCode() % 64));
                        command.ExecuteNonQuery();
                    }
                }
            }
        }

        /// <summary>
        /// 创建数据库文件
        /// </summary>
        /// <param name="fileName"></param>
        public static void CreateDBFile(string fileName)
        {
            string path = System.Environment.CurrentDirectory + @"/Data/";
            if (!Directory.Exists(path))
            {
                Directory.CreateDirectory(path);
            }
            string databaseFileName = path + fileName;
            if (!File.Exists(databaseFileName))
            {
                SQLiteConnection.CreateFile(databaseFileName);
            }
        }

        /// <summary>
        /// 生成连接字符串
        /// </summary>
        /// <returns></returns>
        private static string CreateConnectionString()
        {
            SQLiteConnectionStringBuilder connectionString = new SQLiteConnectionStringBuilder();
            connectionString.DataSource = @"IkunDB.db";
            return connectionString.ToString();
        }

        /// <summary>
        /// 对插入到数据库中的空值进行处理
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static object ToDbValue(object value)
        {
            if (value == null)
            {
                return DBNull.Value;
            }
            else
            {
                return value;
            }
        }

        /// <summary>
        /// 对从数据库中读取的空值进行处理
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static object FromDbValue(object value)
        {
            if (value == DBNull.Value)
            {
                return null;
            }
            else
            {
                return value;
            }
        }

        /// <summary>
        /// 执行非查询的数据库操作
        /// </summary>
        /// <param name="sqlString">要执行的sql语句</param>
        /// <param name="parameters">参数列表</param>
        /// <returns>返回受影响的条数</returns>
        public static int ExecuteNonQuery(string sqlString, params SQLiteParameter[] parameters)
        {
            string connectionString = CreateConnectionString();
            using (SQLiteConnection connection = new SQLiteConnection(connectionString))
            {
                connection.Open();
                using (SQLiteCommand command = connection.CreateCommand())
                {
                    command.CommandText = sqlString;
                    foreach (SQLiteParameter parameter in parameters)
                    {
                        command.Parameters.Add(parameter);
                    }
                    return command.ExecuteNonQuery();
                }
            }
        }

        /// <summary>
        /// 执行查询并返回查询结果第一行第一列
        /// </summary>
        /// <param name="sqlString">SQL语句</param>
        /// <param name="sqlparams">参数列表</param>
        /// <returns></returns>
        public static object ExecuteScalar(string sqlString, params SQLiteParameter[] parameters)
        {
            string connectionString = CreateConnectionString();
            using (SQLiteConnection connection = new SQLiteConnection(connectionString))
            {
                connection.Open();
                using (SQLiteCommand command = connection.CreateCommand())
                {
                    command.CommandText = sqlString;
                    foreach (SQLiteParameter parameter in parameters)
                    {
                        command.Parameters.Add(parameter);
                    }
                    return command.ExecuteScalar();
                }
            }
        }

        /// <summary>
        /// 查询多条数据
        /// </summary>
        /// <param name="sqlString">SQL语句</param>
        /// <param name="parameters">参数列表</param>
        /// <returns>返回查询的数据表</returns>
        public static DataTable GetDataTable(string sqlString, params SQLiteParameter[] parameters)
        {
            string connectionString = CreateConnectionString();
            using (SQLiteConnection connection = new SQLiteConnection(connectionString))
            {
                connection.Open();
                using (SQLiteCommand command = connection.CreateCommand())
                {
                    command.CommandText = sqlString;
                    foreach (SQLiteParameter parameter in parameters)
                    {
                        command.Parameters.Add(parameter);
                    }
                    DataSet ds = new DataSet();
                    SQLiteDataAdapter adapter = new SQLiteDataAdapter(command);
                    adapter.Fill(ds);
                    connection.Close();
                    return ds.Tables[0];
                }
            }
        }
    }
}

MySqliteService 调用数据库工具类获取图表数据并处理为 series 格式,

cs 复制代码
using CefWpfApp.Entity;
using CefWpfApp.Utils;
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SQLite;

namespace CefWpfApp.Service
{
    public class MySqliteService
    {
        /// <summary>
        /// 返回图表数据信息
        /// </summary>
        public static Object GetSeriesData()
        {
            List<SeriesEntity> seriesEntities = new List<SeriesEntity>(5);
            string[] series = new string[] { "Email", "Union Ads", "Video Ads", "Direct", "Search Engine" };
            List<StackedLineEntity> stackedLineEntities = GetStackedLineEntities();
            for (int i = 0; i < series.Length; i++)
            {
                SeriesEntity seriesEntity = new SeriesEntity();
                seriesEntity.name = series[i];
                seriesEntity.type = "line";
                seriesEntity.stack = "Total";
                List<int> data = new List<int>(7);

                switch (series[i])
                {
                    case "Email":
                        {
                            for (int j = 0; j < stackedLineEntities.Count; j++)
                            {
                                data.Add(stackedLineEntities[j].Email);
                            }
                            break;
                        }
                    case "Union Ads":
                        {
                            for (int j = 0; j < stackedLineEntities.Count; j++)
                            {
                                data.Add(stackedLineEntities[j].Unio_Ads);
                            }
                            break;
                        }
                    case "Video Ads":
                        {
                            for (int j = 0; j < stackedLineEntities.Count; j++)
                            {
                                data.Add(stackedLineEntities[j].Video_Ads);
                            }
                            break;
                        }
                    case "Direct":
                        {
                            for (int j = 0; j < stackedLineEntities.Count; j++)
                            {
                                data.Add(stackedLineEntities[j].Direct);
                            }
                            break;
                        }
                    case "Search Engine":
                        {
                            for (int j = 0; j < stackedLineEntities.Count; j++)
                            {
                                data.Add(stackedLineEntities[j].Search_Engine);
                            }
                            break;
                        }
                }
                seriesEntity.data = data;
                seriesEntities.Add(seriesEntity);
            }
            return seriesEntities;
        }

        /// <summary>
        /// 查询数据
        /// </summary>
        /// <returns></returns>
        public static List<StackedLineEntity> GetStackedLineEntities()
        {
            List<StackedLineEntity> stackedLineEntities = new List<StackedLineEntity>();

            string sqlQuery = "SELECT * FROM 'StackedLine' ORDER BY Id DESC LIMIT 7;";
            SQLiteParameter[] parameters = new SQLiteParameter[]{ };

            DataTable dt = MySqliteUtil.GetDataTable(sqlQuery, parameters);
            for (int i = 0; i < dt.Rows.Count; i++)
            {
                int Email = int.Parse(dt.Rows[i]["Email"].ToString());
                int Unio_Ads = int.Parse(dt.Rows[i]["Unio_Ads"].ToString());
                int Video_Ads = int.Parse(dt.Rows[i]["Video_Ads"].ToString());
                int Direct = int.Parse(dt.Rows[i]["Direct"].ToString());
                int Search_Engine = int.Parse(dt.Rows[i]["Search_Engine"].ToString());

                StackedLineEntity stackedLineEntity = new StackedLineEntity(Email, Unio_Ads, Video_Ads, Direct, Search_Engine);
                stackedLineEntities.Add(stackedLineEntity);
            }
            return stackedLineEntities;
        }
    }
}

ExportDataUtil 调用 MySqliteService 导出数据给 JS,

cs 复制代码
using CefWpfApp.Service;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CefWpfApp.Utils
{
    public class ExportDataUtil
    {
        /// <summary>
        /// 导出图表数据
        /// </summary>
        /// <returns></returns>
        public Object ExportSeriesData()
        {
            return MySqliteService.GetSeriesData();
        }
    }
}

13、导出 .Net 方法

cs 复制代码
        /// <summary>
        /// 导出类方法
        /// </summary>
        public static void ExposeDotnetClass()
        {
            browser.JavascriptObjectRepository.ResolveObject += (sender, e) =>
            {
                // 注册 ImageUtil 实例
                DoRegisterDotNetFunc(e.ObjectRepository, e.ObjectName, "imageUtil", new ImageUtil());
                // 注册 ExportDataUtil 实例
                DoRegisterDotNetFunc(e.ObjectRepository, e.ObjectName, "exportDataUtil", new ExportDataUtil());
                
                // 注册其他实例 ...
            };

            browser.JavascriptObjectRepository.ObjectBoundInJavascript += (sender, e) =>
            {
                var name = e.ObjectName;
                Debug.WriteLine($"Object {e.ObjectName} was bound successfully.");
            };

        }
        /// <summary>
        /// 注册 DoNet 实例
        /// </summary>
        /// <param name = "repo" > IJavascriptObjectRepository </ param >
        /// < param name="eventObjectName">事件对象名</param>
        /// <param name = "funcName" > 方法名 </ param >
        /// < param name="objectToBind">需要绑定的DotNet对象</param>
        private static void DoRegisterDotNetFunc(IJavascriptObjectRepository repo, string eventObjectName, string funcName, object objectToBind)
        {

            if (eventObjectName.Equals(funcName))
            {
                if (!IsSetNameConverter)
                {
                    repo.NameConverter = new CamelCaseJavascriptNameConverter();
                    IsSetNameConverter = true;
                }

                BindingOptions bindingOptions = null;
                bindingOptions = BindingOptions.DefaultBinder;
                //repo.NameConverter = null;
                //repo.NameConverter = new CamelCaseJavascriptNameConverter();
                repo.Register(funcName, objectToBind, isAsync: true, options: bindingOptions);
            }
        }
cs 复制代码
        /// <summary>
        /// Create a new instance in code or add via the designer
        /// </summary>
        private void AddChromiumWebBrowser()
        {
            // 本地代理域
            browser = new ChromiumWebBrowser("http://wpf.test");
            // 菜单
            browser.MenuHandler = new CustomContextMenuHandler();
            // 加载状态变更
            browser.LoadingStateChanged += OnLoadingStateChanged;
            // 导出 .Net 方法
            ExposeDotnetClass();
            this.contentGrid.Children.Add(browser);
        }

14、引入前端静态资源

前端打包,

cs 复制代码
npm run build

然后将打包文件引入 .NET 项目中,注册本地域,

cs 复制代码
            // 本地代理域
            settings.RegisterScheme(new CefCustomScheme
            {
                SchemeName = "http",
                DomainName = "wpf.test",
                SchemeHandlerFactory = new FolderSchemeHandlerFactory(rootFolder: @"..\..\..\CefWpfApp\Web",
                            hostName: "wpf.test", //Optional param no hostname/domain checking if null
                            defaultPage: "index.html") //Optional param will default to index.html
            });

15、配置 CEF (补充)

配置本地网页的访问域、缓存目录、跨域等信息,

cs 复制代码
// CefWpfApp\App.xaml.cs
using CefSharp;
using CefSharp.SchemeHandler;
using CefSharp.Wpf;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;

namespace CefWpfApp
{
    /// <summary>
    /// App.xaml 的交互逻辑
    /// </summary>
    public partial class App : Application
    {
        /// <summary>
        /// 主窗口实例
        /// </summary>
        public static MainWindow mainWindow;

        public App()
        {
            // 初始化 CEF
            InitCefSettings();
            // 实例化主窗口
            mainWindow = new MainWindow();
            this.MainWindow = mainWindow; 
            // 显示主窗口
            this.MainWindow.Show();
        }

        /// <summary>
        /// 重写启动方法
        /// </summary>
        /// <param name="e"></param>
        protected override void OnStartup(StartupEventArgs e)
        {
            SplashScreen splashScreen = new SplashScreen("chromium-256.ico");
            // true => SplashScreen 自动关闭
            // false => SplashScreen 不关闭
            splashScreen.Show(false);
            // 配置 SplashScreen 关闭时间,1s
            // SplashScreen 的关闭时间是基于主窗口加载完成的时间。
            // 如果主窗口的加载时间比 1 秒要短,那么 SplashScreen 将会在主窗口加载完成后立即关闭。
            splashScreen.Close(new TimeSpan(0, 0, 1));
            base.OnStartup(e);

        }

        /// <summary>
        /// 初始化 CEF 配置
        /// </summary>
        private static void InitCefSettings()
        {

#if ANYCPU
            CefRuntime.SubscribeAnyCpuAssemblyResolver();
#endif

            // Pseudo code; you probably need more in your CefSettings also.
            var settings = new CefSettings()
            {
                //By default CefSharp will use an in-memory cache, you need to specify a Cache Folder to persist data
                CachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "CefSharp\\Cache")
            };

            //Example of setting a command line argument
            //Enables WebRTC
            // - CEF Doesn't currently support permissions on a per browser basis see https://bitbucket.org/chromiumembedded/cef/issues/2582/allow-run-time-handling-of-media-access
            // - CEF Doesn't currently support displaying a UI for media access permissions
            //
            //NOTE: WebRTC Device Id's aren't persisted as they are in Chrome see https://bitbucket.org/chromiumembedded/cef/issues/2064/persist-webrtc-deviceids-across-restart
            settings.CefCommandLineArgs.Add("enable-media-stream");
            //https://peter.sh/experiments/chromium-command-line-switches/#use-fake-ui-for-media-stream
            settings.CefCommandLineArgs.Add("use-fake-ui-for-media-stream");
            //For screen sharing add (see https://bitbucket.org/chromiumembedded/cef/issues/2582/allow-run-time-handling-of-media-access#comment-58677180)
            settings.CefCommandLineArgs.Add("enable-usermedia-screen-capturing");

            // 允许跨域访问
            settings.CefCommandLineArgs.Add("disable-web-security");

            // 本地代理域
            settings.RegisterScheme(new CefCustomScheme
            {
                SchemeName = "http",
                DomainName = "wpf.test",
                SchemeHandlerFactory = new FolderSchemeHandlerFactory(rootFolder: @"..\..\..\CefWpfApp\Web",
                            hostName: "wpf.test", //Optional param no hostname/domain checking if null
                            defaultPage: "index.html") //Optional param will default to index.html
            });

            //Perform dependency check to make sure all relevant resources are in our output directory.
            Cef.Initialize(settings, performDependencyCheck: true, browserProcessHandler: null);
            // 允许 JS 调用 Task<T> 类型返回值的方法
            CefSharpSettings.ConcurrentTaskExecution = true;
        }

    }
}

16、前后端分离部署(补充)

如果不引入静态资源,不想在本地代理 Web ,则可以通过配置远程 URL 来实现前后端分离部署,

cs 复制代码
npm run dev

开启前端,注意,这里 ROOT_PATH URL 前缀跟 Local 相同,用于访问 public 下的静态资源(图片/JSON),如果是在 .NET 中代理,则需要保持跟 CEF 配置相同,例如 "http://wpf.test"。

注释掉 CEF 配置,

配置构造方法参数,

17、关闭 WPF 调试器(补充)

三、SQLITE 环境搭建(补充)

1、Sqlite 下载

# https://www.sqlite.org/download.html
https://www.sqlite.org/2023/sqlite-tools-win-x64-3440000.zip
https://www.sqlite.org/2023/sqlite-dll-win-x64-3440000.zip

2、Sqlite 配置

将解压后的两个文件夹整合到一个文件夹 Sqlite3 作为安装目录,然后配置 PATH 环境变量,在命令提示符打开,输入,

sqlite3

看到上面的结果就代表安装完成。

3、C# 使用 Sqlite 示例

安装 System.Data.Sqlite 包,这里使用的版本是 1.0.118,

cs 复制代码
// Sqlite 增删查改示例
using System;
using System.Data.SQLite;

namespace SQLiteExample
{
    class Program
    {
        static void Main(string[] args)
        {
            // 连接数据库 example.db
            using (var connection = new SQLiteConnection("Data Source=example.db;Version=3;"))
            {
                connection.Open();

                // 创建表
                using (var command = new SQLiteCommand("CREATE TABLE IF NOT EXISTS Users (Id INTEGER PRIMARY KEY, Name TEXT, Age INTEGER)", connection))
                {
                    command.ExecuteNonQuery();
                }

                // 插入数据
                using (var command = new SQLiteCommand("INSERT INTO Users (Name, Age) VALUES (@name, @age)", connection))
                {
                    command.Parameters.AddWithValue("@name", "John");
                    command.Parameters.AddWithValue("@age", 25);
                    command.ExecuteNonQuery();
                }

                // 查询数据
                using (var command = new SQLiteCommand("SELECT * FROM Users", connection))
                {
                    using (var reader = command.ExecuteReader())
                    {
                        while (reader.Read())
                        {
                            int id = Convert.ToInt32(reader["Id"]);
                            string name = reader["Name"].ToString();
                            int age = Convert.ToInt32(reader["Age"]);

                            Console.WriteLine($"ID: {id}, Name: {name}, Age: {age}");
                        }
                    }
                }

                // 更新数据
                using (var command = new SQLiteCommand("UPDATE Users SET Age = @age WHERE Name = @name", connection))
                {
                    command.Parameters.AddWithValue("@age", 30);
                    command.Parameters.AddWithValue("@name", "John");
                    command.ExecuteNonQuery();
                }

                // 删除数据
                using (var command = new SQLiteCommand("DELETE FROM Users WHERE Name = @name", connection))
                {
                    command.Parameters.AddWithValue("@name", "John");
                    command.ExecuteNonQuery();
                }
            }
        }
    }
}

4、使用指令查询数据

cs 复制代码
# 打开数据库
.open IkunDB.db
# 查询数据库
.databases
# 查询所有表
.tables
# 查询某个表数据
SELECT * FROM tableName;
# 退出
.quit

四、Demo 成品效果

1、前后端分离部署

2、前后端合并部署

3、保存的图表

参考资料

1、WPF 介绍 | Microsoft Learn

2、在 Visual Studio 2019 中创建第一个 WPF 应用 - .NET Framework | Microsoft Learn

3、Quick Start For MS .Net 4.x · cefsharp/CefSharp Wiki · GitHub

4、一个 Vue 3 UI 框架 | Element Plus

5、基于.Net CEF 实现 Vue 等前端技术栈构建 Windows 窗体应用-CSDN博客

6、使用 ElementUI 组件构建 Window 桌面应用探索与实践(WinForm)-CSDN博客

7、Apache ECharts

8、Examples - Apache ECharts

9、快速上手 - Handbook - Apache ECharts

10、【精选】WPF:Loading等待动画、加载动画_wpf 加载动画_pandawangyt的博客-CSDN博客

11、Documentation - Apache ECharts

12、SQLite Home Page

13、SQLite 教程 | 菜鸟教程

相关推荐
IT良1 小时前
c#增删改查 (数据操作的基础)
开发语言·c#
yufei-coder1 小时前
掌握 C# 中的 LINQ(语言集成查询)
windows·vscode·c#·visual studio
Jiaberrr3 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
5967851546 小时前
DotNetty ChannelRead接收数据为null
tcp/ip·c#
长路 ㅤ   7 小时前
cherry-markdown开源markdown组件详细使用教程
vue·markdown组件
weixin_464078077 小时前
C#串口温度读取
开发语言·c#
明耀10 小时前
WPF RadioButton 绑定boolean值
c#·wpf
一丝晨光11 小时前
Java、PHP、ASP、JSP、Kotlin、.NET、Go
java·kotlin·go·php·.net·jsp·asp
Death20012 小时前
Qt 中的 QListWidget、QTreeWidget 和 QTableWidget:简化的数据展示控件
c语言·开发语言·c++·qt·c#
Death20012 小时前
Qt 3D、QtQuick、QtQuick 3D 和 QML 的关系
c语言·c++·qt·3d·c#