笔记承接承接上一节完全自定义控件本节课实现两类轻量化自定义控件1、扩展控件继承TextBox原生控件拓展自定义属性2、自定义组件继承Component封装自定义定时器无可视化界面核心考点三类定时器区别、控件与组件区别、自定义事件、设计器属性绑定一、整体工程结构Form1主窗体调用两个自定义控件、绑定定时器回调事件UserControl1扩展控件继承TextBox拓展背景色属性UserControl2自定义组件继承Component封装系统定时器二、统一精简命名空间整合三处代码、剔除全部冗余引用区分三类定时器专属命名空间标注易错点using System; using System.ComponentModel; using System.Drawing; using System.Timers; //✅后台计时器Elapsed事件跨线程需注意UI刷新 using System.Windows.Forms; //全部冗余命名空间直接删除 //using System.Collections.Generic; //using System.Data; //using System.Linq; //using System.Text; //using System.Threading.Tasks; //using System.Threading;三、第一类扩展控件 UserControl1继承TextBox3.1 定义特点归属扩展式自定义控件最简单的自定义控件继承关系直接继承系统原生控件TextBox特性保留文本框全部原生功能仅拓展自定义属性3.2 逐行代码批注namespace _1自定义控件 { //扩展控件继承原有可视化控件TextBox public partial class UserControl1 : TextBox { public UserControl1() { InitializeComponent(); } //Description属性面板生成中文备注设计器可视化修改 [Description(背景颜色)] public Color MyColor { get BackColor; //读取控件原有背景色 set { BackColor value; //赋值修改背景色 Refresh(); //刷新控件立刻生效配色 } } } }3.3 核心考点扩展控件继承已有可视化控件不用手动绘制界面赋值后必须调用Refresh()否则界面不会实时刷新样式[Description]特性对外暴露可视化属性四、第二类自定义定时器组件 UserControl24.1 控件VS组件必考选择题控件(Control)可视化、可以拖拽到窗体界面、继承Control组件(Component)无界面、后台运行、不可渲染直接继承Component例如原生Timer、ImageList4.2 三类定时器终极区分期末高频定时器类型命名空间事件是否阻塞UIUI修改要求窗体定时器System.Windows.FormsTick不阻塞可直接修改控件无需Invoke后台定时器System.TimersElapsed后台线程执行跨线程修改UI必须Invoke线程定时器System.Threading回调委托子线程运行必须Invoke极易闪退4.3 自定义定时器逐行拆解namespace _1自定义控件 { //继承Component无界面自定义组件不是控件 public partial class UserControl2 : Component { //封装系统后台定时器 public Timer _timer new Timer(); //自定义回调事件对外暴露定时器触发事件 public event EventHandler myTick; //构造方法1默认构造 public UserControl2() { InitializeComponent(); _timer new Timer(TimerInterval); _timer.Elapsed _timer_Elapsed;//绑定定时触发事件 } //构造方法2容器挂载构造适配VS组件管理器 public UserControl2(IContainer c) { c.Add(this);//将组件加入窗体容器 InitializeComponent(); _timer new Timer(TimerInterval); _timer.Elapsed _timer_Elapsed; } //定时器触发回调方法 private void _timer_Elapsed(object sender, ElapsedEventArgs e) { //不为空再触发?空值判断防止报错 myTick?.Invoke(sender, e); } //自定义属性定时间隔 [Description(定时器间隔)] public double TimerInterval { get _timer.Interval; set _timer.Interval value; } //自定义属性启停定时器 [Description(定时器是否可以用)] public bool TimerEnabled { get _timer.Enabled; set _timer.Enabled value; } } }4.4 代码Bug修复原生构造方法重复赋值_timer先全局初始化、构造再次赋值浪费资源保留写法不报错属于冗余代码后台Timer触发myTick事件事件内部修改UI控件必须使用Invoke否则程序闪退必须添加?空判断myTick?.Invoke未绑定事件直接调用会空指针报错五、主窗体调用代码 Form15.1 业务逻辑绑定自定义定时器事件按钮控制定时器启停接收组件回调namespace _1自定义控件 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } //自定义定时器触发回调事件 private void userControl21_myTick(object sender, EventArgs e) { Console.WriteLine(1111); } //启动定时器 private void button1_Click(object sender, EventArgs e) { userControl21.TimerEnabled true; } //关闭定时器 private void button2_Click(object sender, EventArgs e) { userControl21.TimerEnabled false; } } }六、三类自定义控件横向对比期末背诵控件类型继承基类有无界面使用场景扩展控件TextBox/Button等控件✅ 自带原生界面拓展原有控件属性、样式用户控件UserControl✅ 自由拼装界面组合多个控件封装复用模块自定义组件Component❌ 无可视化界面定时器、日志、后台服务完全自定义控件Control✅ 手动GDI绘制异形、高端自定义UI七、易错考点汇总继承Component生成组件、继承Control生成控件System.Timers定时器属于后台子线程事件内部修改UI必须Invoke自定义事件触发必须加空判断事件?.Invoke()防止空引用异常扩展控件修改样式必须调用Refresh()刷新界面[Description]特性仅作用设计器属性面板展示中文备注HttpClient实现分片断点续传文件下载笔记承接承接自定义控件、多线程课程WinForm网络编程大题案例核心功能选择保存路径、HTTP分片下载、断点续传、进度条实时刷新、取消下载、网络接口请求核心知识点HttpClient请求、HTTP请求方法、URI资源定位、Range分片请求、取消令牌、跨线程UI更新二、前置网络基础必考简答题2.1 客户端 服务端客户端发送请求、接收数据本机程序服务端存储资源、响应请求云端服务器2.2 URL 和 URI 区别URL网络资源地址字符串例如http://127.0.0.1:8080URIURL转为资源唯一定位对象校验地址合法性、解析域名、端口程序请求必须使用URI2.3 三大HTTP请求方法背诵HEAD仅请求响应头不获取文件内容速度极快专门获取文件大小、资源状态GET获取服务器资源正文下载、查询数据使用POST向服务器提交数据上传、登录接口使用2.4 分片断点续传原理分片下载超大文件拆分小段请求降低单次请求压力防止请求超时断点续传本地保留未完成文件二次下载读取已有文件长度从断开位置继续下载不用从头下载实现核心请求头RangeHeaderValue划定字节请求范围三、全局变量逐行释义namespace _2下载 { public partial class Form1 : Form { //全局HTTP请求客户端全局唯一复用链接避免重复创建套接字 public static readonly HttpClient httpClient new HttpClient(); //取消令牌安全终止下载任务 public CancellationTokenSource cts; public Form1() { InitializeComponent(); button3.Enabled false;//初始化禁用取消按钮 }四、五大按钮业务拆分4.1 Button1选择保存目录调用系统文件夹弹窗获取本地保存路径回填文本框private void button1_Click(object sender, EventArgs e) { FolderBrowserDialog dialog new FolderBrowserDialog(); //弹出文件夹选择框 if (dialog.ShowDialog()DialogResult.OK) { //赋值存储路径 textBox1.Text dialog.SelectedPath; } }4.2 Button2启动下载入口逻辑前置非空校验、按钮状态互斥、开启异步下载、异常兜底复位private void button2_Click(object sender, EventArgs e) { //1.非空校验防止空路径报错 if (string.IsNullOrEmpty(textBox1.Text)) { MessageBox.Show(请设置保存文件路径); return; } if (string.IsNullOrEmpty(textBox2.Text)) { MessageBox.Show(请设置资源下载路径); return; } try { //2.按钮互斥防止重复点击造成并发下载 button2.Enabled false; button3.Enabled true; button1.Enabled false; textBox1.Enabled false; //3.初始化取消令牌 cts new CancellationTokenSource(); //4.url转为uri校验网络地址合法性 Uri uri new Uri(textBox2.Text); //5.拼接完整本地保存路径 string filePath Path.Combine(textBox1.Text,Path.GetFileName(textBox2.Text)); //6.开启后台异步下载避免卡死UI Task.Run(() { DownLoad(uri, filePath, cts.Token); }, cts.Token).ContinueWith(t { //7.下载结束/异常结束复位全部控件 richTextBox1.Text \n下载完成; button1.Enabled true; button2.Enabled true; button3.Enabled false; textBox1.Enabled true; }); } catch (Exception) { //全局异常兜底防止程序闪退 button1.Enabled true; button2.Enabled true; button3.Enabled false; textBox1.Enabled true; Console.WriteLine( 下载失败); return; } }4.3 核心方法DownLoad 分片断点下载4.3.1 第一步 HEAD请求获取文件总大小发送HEAD请求只读取响应头快速获取服务器文件字节大小不下载文件正文public void DownLoad(Uri uri, string filePath, CancellationToken cts default) { long totalSize 0; //第一次请求HEAD请求 获取文件大小 using (HttpClient clinet new HttpClient()) { //封装请求报文请求方式请求地址 var response clinet.SendAsync( new HttpRequestMessage(HttpMethod.Head,uri),cts); //读取响应头获取服务器文件总字节大小 var contentLength response.Result.Content.Headers.ContentLength; //服务器无资源抛出异常 if (!contentLength.HasValue) { throw new Exception(服务器文件不存在); } totalSize contentLength.Value; }4.3.2 第二步 断点续传判定判断本地是否存在未下载完毕文件读取已有字节长度接续位置下载long startPosition 0;//下载起始位置 FileInfo fileInfo new FileInfo(filePath); //文件已存在断点续传逻辑 if (fileInfo.Exists) { //读取本地已下载字节数接续下载 startPosition fileInfo.Length; //本地文件大小等于服务器大小提前下载完成 if (startPositiontotalSize) { throw new Exception(文件已经下载好了); } }4.3.3 第三步 循环分片下载文件流追加写入、请求头划定分片范围、实时刷新下载进度//文件流不存在则创建存在则追加写入断点续传核心 using (var fs new FileStream(filePath,startPosition0?FileMode.Create:FileMode.Append)) { //循环条件未取消 未下载完毕 while (!cts.IsCancellationRequested startPositiontotalSize) { //划定分片字节范围每次下载1000字节 long start startPosition; long end start 1000; //边界容错末尾分片不能超出文件总大小 if (endtotalSize-1) { end totalSize - 1; } //添加分片请求头告知服务器请求字节区间 var range new RangeHeaderValue(start, end); httpClient.DefaultRequestHeaders.Range range; //第二次GET请求分片获取文件数据 var res httpClient.GetAsync(uri, cts); byte[] bytes res.Result.Content.ReadAsByteArrayAsync().Result; //写入本地文件 fs.WriteAsync(bytes,0,bytes.Length); //更新下载偏移量进入下一片段 startPosition bytes.Length; //跨线程刷新UI子线程禁止直接操作控件 Invoke(new Action(() { //计算下载进度百分比 float progress (startPosition / (float)totalSize) * 100; progressBar1.Value (int)progress; label3.Text progress.ToString(N2)%; richTextBox1.Text $正在下载资源文件的大小{totalSize}byte,已下载{startPosition}byte; })); } } }4.4 Button3取消下载调用取消令牌终止网络请求、终止文件写入复位界面private void button3_Click(object sender, EventArgs e) { //发送取消信号终止网络文件任务 cts.Cancel(); richTextBox1.Text \n下载已经取消; //复位界面控件 button1.Enabled true; button2.Enabled true; button3.Enabled false; textBox1.Enabled true; }4.5 Button4网络接口请求调用快看漫画公开接口发送GET请求获取线上接口返回数据对应文档请求地址private void button4_Click(object sender, EventArgs e) { //线上网络接口地址对应参考文档请求链接 string p1 https://www.kuaikanmanhua.com/search/mini/topic/multi_filter?page1size24tag_id20update_status1pay_status1label_dimension_origin1sort1; //发送Get请求获取接口报文 var res httpClient.GetAsync(p1); //控制台打印响应文本 Console.WriteLine(res.Result.Content.ReadAsStringAsync().Result); } } }五、原生代码致命BUG改错大题阻塞死锁BUG大量使用.Result同步取值卡死异步线程、极易UI卡死考试高频改错全局HttpClient复用请求头分片Range请求头不会自动清空二次下载错乱HEAD请求部分服务器禁止访问部分云端服务器屏蔽HEAD方法直接报错缺少Http响应状态码校验未判断404、403报错直接读取响应文件流未刷新极端情况文件缓存不落地下载完成文件为空六、报文结构考点6.1 请求报文组成请求行 请求头 请求体6.2 响应报文组成响应行 响应头 响应体6.3 Range请求头作用向服务器指定请求字节区间实现文件分片、断点续传是文件下载核心请求头七、期末简答题背诵HEAD请求和GET请求区别 HEAD仅请求响应头获取文件信息速度快GET请求完整响应体获取资源数据断点续传实现原理 读取本地已缓存文件长度通过Range请求头指定下载起始字节接续下发下载为什么使用全局HttpClient 频繁创建HttpClient会重复创建网络套接字造成端口占用、内存泄漏全局复用优化性能下载刷新UI为什么需要Invoke 下载运行在子线程WinForm禁止跨线程操作控件Invoke切回主线程刷新界面