wpf开发系列

开发笔记

nuget工具篇

nuget包工具命令

//删除包
dotnet nuget delete -s https://nuget.lingyanspace.com/v3/index.json LingYanAutoUpdate 1.0.0 -k nugetlingyanspace --non-interactive

课纲目录

 模块Ⅰ:WPF高阶技术精讲

深入掌握自定义控件

主要是附加属性与控件加载部分

 深入掌握控件模板与数据模板

理解控件模板当中包含数据模板
数据模板当中又可以包含控件模板
来回组合学习
最终以模板选择器来弥补遗漏的部分

深入掌握资源样式与动态主题

资源字典如何加载
静态资源与动态资源区别
主题动态切换核心理念
以及后续写项目的规范

埋坑
此处本来是想带大家完完全全手写一个wpf控件库

但是由于很多技术涉及到c#代码,所以先讲后续,高阶通信模块讲完之后再返回这儿

然后还有MAUI部分公开课要把讲xaml基础控件正好中间多学点儿做个过渡。

但是主要的xaml技术就是这三节内容和B站wpf公开课那一套视频看完

 模块Ⅱ:高阶通信与高阶模式加并发控制

基础知识

1、进程与线程 :
2、进程与线程的区别。
3、线程生命周期(启动、运行、终止)。
4、同步与异步编程 :
5、同步与异步的基本概念。
6、异步编程的优势与挑战。
7、C# 中的多线程基础 :
8、Thread类和Task类。
9、如何创建和管理线程。
10、WPF 中的线程模型 :
11、UI 线程与后台线程的关系。
12、使用Dispatcher更新 UI。

协议解析与错误处理模块

1、Span<T> 优化二进制协议解析,使用Span<T>提高内存操作效率。
2、错误处理模式 :异常传播与CancellationToken的结合。
3、WPF 中的应用 :在 WPF 应用中处理网络通信错误,实现用户友好的错误提示机制。

虚拟通信模拟模块

1、TcpListener 模拟网络设备 :
创建一个简单的 TCP 服务器。
处理客户端连接和数据传输。
2、SignalR 的内存模拟 :
不依赖真实服务端的情况下,使用 SignalR 模拟实时通信。
3、WPF 中的应用 :
在 WPF 应用中集成虚拟通信模拟工具。
实现一个简单的聊天窗口或状态监控界面。

并发控制与任务调度模块

1、线程同步机制 :MonitorMutex,Semaphore等同步原语,Dispatcher与BackgroundWorker的协作。
2、异步编程与任务调度 :async/await的深入讲解,使用TaskScheduler实现优先级队列。
3、TPL Dataflow 数据流水线 :构建高效的生产者-消费者模型
4、WPF 中的应用 :在 WPF 中实现多线程任务调度,避免 UI 冻结问题。

高效数据处理模块

1、TPL 数据流 :构建数据流管道,实现并行数据处理。
2、内存管理 :使用Span<T>和Memory<T>减少内存分配。
3、WPF 中的应用 :在 WPF 中实现高性能的数据流处理,示例:实时处理传感器数据并在 UI 上显示。

高级通信技术模块

1、WebSocket 通信 :使用System.Net.WebSockets实现 WebSocket 客户端和服务端。
2、SignalR 实时通信 :构建基于 SignalR 的实时应用
3、跨平台通信 :使用 gRPC 或 RESTful API 实现跨平台通信。
4、WPF 中的应用 :在 WPF 中集成 WebSocket 或 SignalR,实现一个实时更新的仪表盘或聊天界面。

性能优化与调试模块

1、常见问题与解决方案 :死锁与竞争条件。内存泄漏与资源耗尽。
2、WPF 中的性能优化 :减少 UI 线程负担。使用虚拟化技术优化列表显示。

实践巩固

 实际项目实践模块
 
目标:
通过实际项目巩固所学知识。

项目主题 :
实现一个简单的聊天应用。
构建一个实时监控系统。

功能要求 :
支持多线程和异步通信。
使用协议解析和错误处理机制。
集成 SignalR 或 WebSocket。

WPF 界面开发 :
设计一个用户友好的界面。
实现动态更新和多线程交互。

聊天应用 :
使用 SignalR 实现实时消息传递。
使用Dispatcher更新聊天记录。

监控系统 :
使用 TPL Dataflow 处理传感器数据。
使用TaskScheduler优化任务调度。

模块Ⅲ:高阶项目实战(全栈)


 3.1 综合项目案例
 3.1.1 虚拟监控系统
- 使用ICollectionView实现动态数据过滤
- 基于VisualStateManager的报警状态可视化

 3.1.2 日志与权限管理
- 使用NLog实现日志分级(Debug/Info/Error)
- 基于角色的权限系统(RBAC)


 3.2 精细化案例
 3.2.1 数据可视化
- 使用OxyPlot实现动态波形图
- 基于WriteableBitmap的实时图像处理


 模块Ⅳ:前沿技术与扩展


 4.1 跨平台开发
 4.1.1 .NET MAUI深度集成
- 共享业务逻辑层与UI分离设计
- 使用SkiaSharp实现跨平台绘图

 4.1.2 WPF与Web技术结合
- 嵌入WebView2实现混合开发
- 使用WebAssembly与Blazor交互

 4.2 人工智能集成
 4.2.1 机器学习模型集成
- 使用ML.NET实现本地预测
- 基于ONNX的图像识别

 4.2.2 数据分析与可视化
- 使用LiveCharts实现动态仪表盘
- 基于Parallel.For的并行数据处理

打包篇

inno 

中文环境【ChineseSimplified.isl】

ChineseSimplified.isl

isl环境中文

Name: "english"; MessagesFile: "compiler:Default.isl"
Name: "chinesesimplified"; MessagesFile: "compiler:Languages\ChineseSimplified.isl"

完整的通信链路

OSI 七层模型与协议对照表

层次 功能 常见协议/标准 工业协议
物理层 定义硬件接口、电气特性、信号传输方式。 RS-232、RS-485、USB、Ethernet、CAN、Wi-Fi、蓝牙、ZigBee、LoRa、NFC Modbus RTU(基于RS-485)、CANopen(基于CAN)
数据链路层 提供可靠的数据帧传输,处理错误检测和纠正。 Ethernet(IEEE 802.3)、Wi-Fi(IEEE 802.11)、PPP、HDLC Profinet、EtherCAT
网络层 负责路由选择和逻辑地址分配,确保数据包跨网络传输。 IPv4、IPv6、ICMP、ARP Modbus TCP(基于IP)、CIP(Common Industrial Protocol)
传输层 提供端到端的通信服务,负责数据分段、重组和流量控制。 TCP、UDP MQTT(基于TCP)、CoAP(基于UDP)
会话层 管理会话(连接的建立、维护和终止)。 SMB、RPC OPC UA
表示层 数据格式转换、加密解密、数据压缩。 TLS/SSL、JSON、XML 无特定工业协议,但加密和数据格式在工业通信中广泛使用。
应用层 为用户提供直接的网络服务,定义应用程序之间的通信规则。 HTTP/HTTPS、FTP/SFTP、SMTP/IMAP/POP3、WebSocket、gRPC Modbus TCP(基于TCP)、OPC UA、BACnet(楼宇自动化控制网络)


IPC(进程间通信)方式与层次对照表

通信方式 功能 适用层次 应用场景 优点 缺点
匿名管道 单向或双向通信,适用于父子进程之间的数据传递。 操作系统层(不属于OSI模型,但常用于应用层之上)。 本地进程间通信,适用于简单的数据传递。 简单易用,操作系统原生支持。 仅限本地通信,数据量较小,父子进程关系限制。
命名管道 支持无亲缘关系的进程间通信,支持本地或网络通信。 操作系统层(不属于OSI模型,但常用于应用层之上)。 本地或网络进程间通信,适用于跨进程的数据传递。 支持跨进程和跨网络通信,灵活性高。 实现复杂度较高,性能受限于操作系统。
消息队列 通过消息队列传递数据,支持异步通信。 操作系统层(不属于OSI模型,但常用于应用层之上)。 本地或分布式系统中的任务调度和消息传递。 支持异步通信,适合任务队列和事件驱动模型。 消息队列可能存在阻塞,消息大小有限制。
共享内存 多个进程共享一块内存区域,速度快。 操作系统层(不属于OSI模型,但常用于应用层之上)。 实时性要求高的本地进程间通信。 速度快,适合大数据量传输。 需要进程同步机制(如信号量)避免竞争,开发复杂度高。
信号量 用于进程同步,避免资源竞争。 操作系统层(不属于OSI模型,但常用于应用层之上)。 进程间同步和资源管理。 简单高效,适合资源锁定和同步。 仅用于同步,不适合数据传输。
套接字(Socket) 支持本地和网络通信,基于 TCP/UDP。 传输层 本地或网络通信,适用于客户端与服务器之间的通信。 支持本地和远程通信,灵活性高,适合分布式系统。 需要手动管理连接和协议,开发复杂度较高。
信号(Signal) 用于进程间的简单通知机制。 操作系统层(不属于OSI模型)。 进程间的简单事件通知(如终止、暂停)。 简单高效,适合轻量级通知。 仅支持简单的信号传递,不适合复杂数据通信。
文件映射(Memory-Mapped Files) 通过文件共享内存区域,支持进程间通信。 操作系统层(不属于OSI模型,但常用于应用层之上)。 本地进程间通信,适用于大数据量的共享。 速度快,适合大数据量传输,支持持久化。 需要同步机制避免竞争,依赖文件系统。
gRPC 基于 HTTP/2 的高性能 RPC 框架。 应用层 微服务之间的高效通信。 高性能,支持流式通信,跨语言支持。 不适合浏览器直接使用,消息格式为二进制,调试较复杂。
REST API 基于 HTTP 的轻量级通信方式。 应用层 请求-响应模式的通信,如数据查询,前端与后端之间的通信,适用于 Web 应用。 简单易用,广泛支持,适合标准化的请求-响应模式。 不支持实时通信,延迟较高,需频繁轮询实现实时性。
WebSocket 基于 TCP 的全双工通信协议。 应用层 实时性要求高的前端与后端通信(如聊天应用、实时数据推送)。 延迟低,性能高,支持全双工通信,适合高并发场景。 需要手动管理连接和消息格式,开发复杂度较高。
SignalR 基于 WebSocket/SSE/Long Polling 的实时通信框架。 应用层 聊天、通知、仪表盘、多人协作。 自动选择最佳协议,开发简单,支持广播和组通信。 如果无法使用 WebSocket,性能可能下降(如 Long Polling)。
MQTT 基于 TCP 的轻量级发布/订阅协议。 应用层 物联网设备的轻量级通信。 轻量级,低带宽消耗,支持 QoS(服务质量)等级,适合低功耗设备。 需要专门的 MQTT Broker,消息格式简单,不适合复杂数据结构。
D-Bus Linux 系统中用于进程间通信的消息总线。 操作系统层(不属于OSI模型,但常用于应用层之上)。 Linux 系统中的进程间通信(如桌面环境组件之间的通信)。 高效,支持广播和点对点通信,适合 Linux 环境。 仅适用于 Linux 系统,跨平台支持较差。
ZeroMQ 高性能消息队列库,支持多种通信模式(如发布/订阅、请求/响应)。 应用层 分布式系统中的高性能通信。 高性能,支持多种通信模式,跨平台支持好。 需要手动管理消息格式和连接,学习曲线较陡峭。

补充说明


关于 Modbus 的位置说明

协议的组合应用示例

1. 下位机与上位机

2. 上位机与远程服务器

3. 远程服务器与前端客户


总结

基础篇

依赖属性propdb

        public int MyProperty
        {
            get { return (int)GetValue(MyPropertyProperty); }
            set { SetValue(MyPropertyProperty, value); }
        }

        // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty MyPropertyProperty =
            DependencyProperty.Register("MyProperty", typeof(int), typeof(ownerclass), new PropertyMetadata(0));

附加属性propa

    public static int GetMyProperty(DependencyObject obj)
    {
        return (int)obj.GetValue(MyPropertyProperty);
    }

    public static void SetMyProperty(DependencyObject obj, int value)
    {
        obj.SetValue(MyPropertyProperty, value);
    }

    // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty MyPropertyProperty =
        DependencyProperty.RegisterAttached("MyProperty", typeof(int), typeof(ownerclass), new PropertyMetadata(0));

xaml资源键类型

定义方式 优点 缺点 适用场景
普通字符串键 简单直观,易于使用 命名冲突风险,无法跨程序集共享 小型项目,局部资源
类型键 自动应用,无需显式引用 灵活性较低 全局样式,基础样式复用
静态资源键 强类型支持,可维护性高 定义稍复杂 大型项目,组件化开发
ComponentResourceKey 跨程序集支持,语义化标识 定义和使用复杂 组件库开发,主题或样式库
动态资源键 动态绑定,灵活性高 性能开销 主题切换,多语言支持

//普通字符串键
<Style x:Key="MyButtonStyle" TargetType="Button" />

//类型键(隐式样式)
<Style TargetType="Button">
    <Setter Property="Background" Value="LightBlue" />
</Style>

//静态资源键
public static class ResourceKeys
{
    public static readonly string CloseButtonStyle = "CloseButtonStyle";
}

<Style x:Key="{x:Static local:ResourceKeys.CloseButtonStyle}" TargetType="Button" />

//组件资源键ComponentResourceKey

    public partial class DataTemplateKeys
    {
        public static ComponentResourceKey ItemClose => new ComponentResourceKey(typeof(DataTemplateKeys), "S.DataTemplate.Item.Close");
    }
<DataTemplate x:Key="{ComponentResourceKey ResourceId=S.DataTemplate.Item.Close, TypeInTargetAssembly={x:Type local:DataTemplateKeys}}">

//静态资源键与组件资源键结合

public static class ResourceKeys
{
    public static readonly ComponentResourceKey CloseButtonStyleKey = new ComponentResourceKey(typeof(ResourceKeys), "CloseButtonStyle");
}

<Style x:Key="{x:Static local:ResourceKeys.CloseButtonStyleKey}" TargetType="Button" />

//动态资源键
<Button Style="{DynamicResource MyButtonStyle}" />

编译篇

编译后事件

:: 检查Lib、Dll文件夹路径是否存在
IF NOT EXIST "$(TargetDir)libs" (
    MD "$(TargetDir)libs"
)

:: 将指定的dll、xml、pdb、config文件移动到libs文件夹
move "$(TargetDir)*.dll" "$(TargetDir)libs"
move "$(TargetDir)*.xml" "$(TargetDir)libs"
move "$(TargetDir)*.pdb" "$(TargetDir)libs"
move "$(TargetDir)*.config" "$(TargetDir)libs"

:: 将runtimes文件夹移动到libs文件夹
move "$(TargetDir)runtimes" "$(TargetDir)libs"

:: 把主程序的相关文件从libs转移出来
move "$(TargetDir)libs\NLog.config" "$(TargetDir)NLog.config"
move "$(TargetDir)libs\$(ProjectName).exe.config" "$(TargetDir)$(ProjectName).exe.config"
move "$(TargetDir)libs\$(ProjectName).exe.xml" "$(TargetDir)$(ProjectName).exe.xml"
move "$(TargetDir)libs\$(ProjectName).pdb" "$(TargetDir)$(ProjectName).pdb"

加扫描文件夹

 <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <!-- 添加对libs文件夹的搜索路径 -->
      <probing privatePath="libs"/>      
    </assemblyBinding>
  </runtime>

扩展篇

HttpCilent发送文件有进度

try
            {
                var lcaolSelectTeam = await this.ToGetSelectTeam();
                if (lcaolSelectTeam.Code != 20000)
                {
                    throw new Exception(lcaolSelectTeam.Message);
                }
                var localToken = await this.ToGetUserToken();
                if (localToken.Code != 20000)
                {
                    throw new Exception(localToken.Message);
                }
                var taskworkFloderBody = await this.ToGetTaskworkProxyFloder();
                if (taskworkFloderBody.Code != 20000)
                {
                    throw new Exception(taskworkFloderBody.Message);
                }
                var rootTaskworkFloder = taskworkFloderBody.Data.PathCombine(teamTaskwrokId.ToString());
                HttpClientHandler handler = new HttpClientHandler();
                ProgressMessageHandler progressMessageHandler = new ProgressMessageHandler(handler);
                progressMessageHandler.HttpSendProgress += (sender, e) =>
                {
                    action.Invoke(e.ProgressPercentage);
                };
                using (HttpClient httpClient = new HttpClient(progressMessageHandler))
                {
                    httpClient.BaseAddress = new Uri("https://lycg.lingyanspace.com/");
                    httpClient.DefaultRequestHeaders.Add("Authorization", localToken.Data);
                    using (var multipartFormData = new MultipartFormDataContent())
                    {
                        var bom = rootTaskworkFloder.PathCombine("bom").FileCombine("default.json");
                        if (File.Exists(bom) && needUploadCloudModel.BOM)
                        {
                            AddFile(multipartFormData, "bom", bom);
                        }
                        var bIfc = rootTaskworkFloder.PathCombine("bifc").FileCombine("default.ifc");
                        if (File.Exists(bIfc) && needUploadCloudModel.BIFC)
                        {
                            AddFile(multipartFormData, "bIfc", bIfc);
                        }
                        var nc1Files = Directory.GetFiles(rootTaskworkFloder.PathCombine("nc1"), "*.nc1", SearchOption.TopDirectoryOnly).ToList();
                        if (nc1Files != null && nc1Files.Count > 0 && needUploadCloudModel.NC1)
                        {
                            nc1Files.ForEach(f =>
                            {
                                AddFile(multipartFormData, "nc1Files", f);
                            });
                        }
                        var dxfFiles = Directory.GetFiles(rootTaskworkFloder.PathCombine("dxf"), "*.dxf", SearchOption.TopDirectoryOnly).ToList();
                        if (dxfFiles != null && dxfFiles.Count > 0 && needUploadCloudModel.DXF)
                        {
                            dxfFiles.ForEach(f =>
                            {
                                AddFile(multipartFormData, "dxfFiles", f);
                            });
                        }
                        var aifcFiles = Directory.GetFiles(rootTaskworkFloder.PathCombine("aifc"), "*.ifc", SearchOption.TopDirectoryOnly).ToList();
                        if (aifcFiles != null && aifcFiles.Count > 0 && needUploadCloudModel.AIFC)
                        {
                            aifcFiles.ForEach(f =>
                            {
                                AddFile(multipartFormData, "aifcFiles", f);
                            });
                        }
                        var pifcFiles = Directory.GetFiles(rootTaskworkFloder.PathCombine("pifc"), "*.ifc", SearchOption.TopDirectoryOnly).ToList();
                        if (pifcFiles != null && pifcFiles.Count > 0 && needUploadCloudModel.PIFC)
                        {
                            pifcFiles.ForEach(f =>
                            {
                                AddFile(multipartFormData, "pifcFiles", f);
                            });
                        }
                        var drawingFiles = Directory.GetFiles(rootTaskworkFloder.PathCombine("drawing"), "*.pdf", SearchOption.TopDirectoryOnly).
                            Concat(Directory.GetFiles(rootTaskworkFloder.PathCombine("drawing"), "*.dwg", SearchOption.TopDirectoryOnly)).ToList();
                        if (drawingFiles != null && drawingFiles.Count > 0 && needUploadCloudModel.Drawing)
                        {
                            drawingFiles.ForEach(f =>
                            {
                                AddFile(multipartFormData, "drawingFiles", f);
                            });
                        }
                        var response = await httpClient.PutAsync($"/api/Team/UploadTeamTaskworkBatchData?teamId={lcaolSelectTeam.Data.Id}&teamTaskwrokId={teamTaskwrokId}", multipartFormData);
                        if (response.IsSuccessStatusCode)
                        {
                            var data = await response.Content.ReadAsStringAsync();
                            var jsonBody = JsonConvert.DeserializeObject<ResponceBody<string>>(data);
                            return jsonBody;
                        }
                        else
                        {
                            throw new Exception(await response.Content.ReadAsStringAsync());
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                return new ResponceBody<string>(40000, ex.Message, null);
            }

编号排序


    public class StringSortComparer : IComparer<string>
    {
        public bool MatchCase { get; }
        public StringSortComparer(bool matchCase)
        {
            MatchCase = matchCase;
        }
        private int CharCompare(char a, char b, bool matchCase)
        {
            char _a = char.MinValue, _b = char.MinValue;
            if (matchCase) { _a = a; _b = b; }
            else { _a = char.ToUpper(a); _b = char.ToUpper(b); }
            if (_a > _b) return 1;
            if (_a < _b) return -1;
            return 0;
        }
        public int Compare(string x, string y)
        {
            // 如果 y 为空,则 y 应该排在最后面
            if (string.IsNullOrEmpty(y)) return -1;
            // 如果 x 为空,而 y 不为空,则 x 应该排在 y 之前
            if (string.IsNullOrEmpty(x)) return 1;
            int len;
            if (x.Length > y.Length) len = x.Length;
            else len = y.Length;
            string numericx = "";
            string numericy = "";
            for (int i = 0; i < len; i++)
            {
                char cx = char.MinValue;
                char cy = char.MinValue;
                if (i < x.Length) cx = x[i];
                if (i < y.Length) cy = y[i];
                if (cx >= 48 && cx <= 57) numericx += cx;
                if (cy >= 48 && cy <= 57) numericy += cy;
                if (i == len - 1)
                {
                    if (numericx.Length > 0 && numericy.Length > 0)
                    {
                        if (decimal.Parse(numericx) < decimal.Parse(numericy)) return -1;
                        if (decimal.Parse(numericx) > decimal.Parse(numericy)) return 1;
                    }
                    return CharCompare(cy, cy, MatchCase);
                }
                if ((cx >= 48 && cx <= 57) && (cy >= 48 && cy <= 57)) continue;
                if (numericx.Length > 0 && numericy.Length > 0)
                {
                    if (decimal.Parse(numericx) < decimal.Parse(numericy)) return -1;
                    if (decimal.Parse(numericx) > decimal.Parse(numericy)) return 1;
                }
                if (CharCompare(cx, cy, MatchCase) == 0) continue;
                return CharCompare(cx, cy, MatchCase);
            }
            return 0;
        }
    }

下载文件进度

 public class HttpHelper
    {
        /// <summary>
        /// 下载单个文件
        /// </summary>
        /// <param name="action"></param>
        /// <param name="netWrokUrl"></param>
        /// <param name="localUrl"></param>
        /// <returns></returns>
        /// <exception cref="Exception"></exception>
        public static async Task<long> DownloadSingleFile(Action<double> action, string netWrokUrl, string localUrl)
        {
            long totalBytesReceived = 0;
            var progress = new Progress<HttpDownloadProgress>(p =>
            {
                if (p.TotalBytesToReceive.HasValue)
                {
                    totalBytesReceived = (long)p.BytesReceived;
                    double percent = (double)p.BytesReceived / p.TotalBytesToReceive.Value * 100.0;
                    action.Invoke(percent);
                }
                else
                {
                    LoggerHelper.DefaultLogger($"特殊情况:{netWrokUrl}的TotalBytesToReceive无值");
                }
            });
            var fileBytes = await new HttpClient().GetByteArrayAsync(new Uri(netWrokUrl), progress, CancellationToken.None);
            if (File.Exists(localUrl))
            {
                File.Delete(localUrl);
            }
            await localUrl.SaveLocalFileAsync(new MemoryStream(fileBytes));
            return totalBytesReceived;
        }
        private static async Task<long> DownloadSingleFile(Action<long, long> progressAction, string networkUrl, string localUrl, long totalBytes)
        {
            long bytesReceived = 0;
            var progress = new Progress<HttpDownloadProgress>(p =>
            {
                bytesReceived = (long)p.BytesReceived;
                progressAction(bytesReceived, totalBytes);
            });
            using (var httpClient = new HttpClient())
            {
                var fileBytes = await httpClient.GetByteArrayAsync(new Uri(networkUrl), progress, CancellationToken.None);
                if (File.Exists(localUrl))
                {
                    File.Delete(localUrl);
                }
                using (var fileStream = new FileStream(localUrl, FileMode.CreateNew))
                {
                    await fileStream.WriteAsync(fileBytes, 0, fileBytes.Length);
                }
                return bytesReceived;
            }
        }
        /// <summary>
        /// 下载文件集合
        /// </summary>
        /// <param name="overallProgressAction"></param>
        /// <param name="files"></param>
        /// <returns></returns>
        public static async Task DownloadMultipleFiles(Action<double> overallProgressAction,Dictionary<string, string> files)
        {
            var downloadTasks = new List<Task<long>>();
            long totalFileSize = 0;
            Dictionary<string, long> fileSizes = new Dictionary<string, long>();
            // 首先,预估所有文件的大小(可以通过HEAD请求或是其他方式获取)
            using (var httpClient = new HttpClient())
            {
                foreach (var file in files)
                {
                    var response = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, file.Key));
                    long contentLength = response.Content.Headers.ContentLength ?? 0;
                    fileSizes[file.Key] = contentLength;
                    totalFileSize += contentLength;
                }
            }
            // 存储每个文件的已接收字节
            Dictionary<string, long> receivedBytes = new Dictionary<string, long>();
            foreach (var file in files)
            {
                string networkUrl = file.Key;
                string localUrl = file.Value;
                Task<long> downloadTask = DownloadSingleFile(
                    (bytesReceived, totalBytes) =>
                    {
                        receivedBytes[networkUrl] = bytesReceived;
                        // 计算总体进度
                        long totalReceived = 0;
                        foreach (var received in receivedBytes.Values)
                        {
                            totalReceived += received;
                        }
                        double overallProgress = (double)totalReceived / totalFileSize * 100.0;
                        overallProgressAction(overallProgress);
                    },
                    networkUrl,
                    localUrl,
                    fileSizes[networkUrl]
                );
                downloadTasks.Add(downloadTask);
            }
            // 等待所有下载任务完成
            long[] results = await Task.WhenAll(downloadTasks);
        }
    }

下载byte

 public static class HttpClientExtension
    {
        private const int BufferSize = 262144;

        public static async Task<byte[]> GetByteArrayAsync(this HttpClient client, Uri requestUri, IProgress<HttpDownloadProgress> progress, CancellationToken cancellationToken)
        {
            if (client == null)
            {
                throw new ArgumentNullException(nameof(client));
            }

            using (var responseMessage = await client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false))
            {
                responseMessage.EnsureSuccessStatusCode();

                var content = responseMessage.Content;
                if (content == null)
                {
                    return Array.Empty<byte>();
                }

                var headers = content.Headers;
                var contentLength = headers.ContentLength;
                using (var responseStream = await content.ReadAsStreamAsync().ConfigureAwait(false))
                {
                    var buffer = new byte[BufferSize];
                    int bytesRead;
                    var bytes = new List<byte>();

                    var downloadProgress = new HttpDownloadProgress();
                    if (contentLength.HasValue)
                    {
                        downloadProgress.TotalBytesToReceive = (ulong)contentLength.Value;
                    }
                    progress?.Report(downloadProgress);

                    while ((bytesRead = await responseStream.ReadAsync(buffer, 0, BufferSize, cancellationToken).ConfigureAwait(false)) > 0)
                    {
                        bytes.AddRange(buffer.Take(bytesRead));

                        downloadProgress.BytesReceived += (ulong)bytesRead;
                        progress?.Report(downloadProgress);
                    }

                    return bytes.ToArray();
                }
            }
        }
    }
    public struct HttpDownloadProgress
    {
        public ulong BytesReceived { get; set; }

        public ulong? TotalBytesToReceive { get; set; }
    }