笔记承接:承接上一节完全自定义控件,本节课实现两类轻量化自定义控件

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(继承TextBox)

3.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 核心考点

  1. 扩展控件:继承已有可视化控件,不用手动绘制界面

  2. 赋值后必须调用Refresh(),否则界面不会实时刷新样式

  3. [Description]特性:对外暴露可视化属性

四、第二类:自定义定时器组件 UserControl2

4.1 控件VS组件(必考选择题)

  • 控件(Control):可视化、可以拖拽到窗体界面、继承Control

  • 组件(Component):无界面、后台运行、不可渲染,直接继承Component,例如原生Timer、ImageList

4.2 三类定时器终极区分(期末高频)

定时器类型

命名空间

事件

是否阻塞UI

UI修改要求

窗体定时器

System.Windows.Forms

Tick

不阻塞

可直接修改控件,无需Invoke

后台定时器

System.Timers

Elapsed

后台线程执行

跨线程,修改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修复

  1. 原生构造方法重复赋值_timer:先全局初始化、构造再次赋值,浪费资源,保留写法不报错,属于冗余代码

  2. 后台Timer触发myTick事件,事件内部修改UI控件,必须使用Invoke,否则程序闪退

  3. 必须添加?空判断:myTick?.Invoke,未绑定事件直接调用会空指针报错

五、主窗体调用代码 Form1

5.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

七、易错考点汇总

  1. 继承Component生成组件、继承Control生成控件

  2. System.Timers定时器属于后台子线程,事件内部修改UI必须Invoke

  3. 自定义事件触发必须加空判断 事件?.Invoke(),防止空引用异常

  4. 扩展控件修改样式,必须调用Refresh()刷新界面

  5. [Description]特性仅作用:设计器属性面板展示中文备注

 HttpClient实现分片+断点续传文件下载

笔记承接:承接自定义控件、多线程课程,WinForm网络编程大题案例

核心功能:选择保存路径、HTTP分片下载、断点续传、进度条实时刷新、取消下载、网络接口请求

核心知识点:HttpClient请求、HTTP请求方法、URI资源定位、Range分片请求、取消令牌、跨线程UI更新

二、前置网络基础(必考简答题)

2.1 客户端 & 服务端

  • 客户端:发送请求、接收数据(本机程序)

  • 服务端:存储资源、响应请求(云端服务器)

2.2 URL 和 URI 区别

  • URL:网络资源地址字符串,例如 http://127.0.0.1:8080

  • URI:URL转为资源唯一定位对象,校验地址合法性、解析域名、端口,程序请求必须使用URI

2.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 (startPosition==totalSize)
                {
                    throw new Exception("文件已经下载好了");
                }
            }
4.3.3 第三步 循环分片下载

文件流追加写入、请求头划定分片范围、实时刷新下载进度

            //文件流:不存在则创建,存在则追加写入(断点续传核心)
            using (var fs = new FileStream(filePath,startPosition==0?FileMode.Create:FileMode.Append))
            {
                //循环条件:未取消 + 未下载完毕
                while (!cts.IsCancellationRequested && startPosition<totalSize)
                {
                    //划定分片字节范围:每次下载1000字节
                    long start = startPosition;
                    long end = start + 1000;
                    //边界容错:末尾分片不能超出文件总大小
                    if (end>totalSize-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?page=1&size=24&tag_id=20&update_status=1&pay_status=1&label_dimension_origin=1&sort=1";
            //发送Get请求,获取接口报文
            var res =  httpClient.GetAsync(p1);
            //控制台打印响应文本
            Console.WriteLine(res.Result.Content.ReadAsStringAsync().Result);
        }
    }
}

五、原生代码致命BUG(改错大题)

  1. 阻塞死锁BUG:大量使用 .Result 同步取值,卡死异步线程、极易UI卡死,考试高频改错

  2. 全局HttpClient复用请求头:分片Range请求头不会自动清空,二次下载错乱

  3. HEAD请求部分服务器禁止访问:部分云端服务器屏蔽HEAD方法,直接报错

  4. 缺少Http响应状态码校验:未判断404、403报错,直接读取响应

  5. 文件流未刷新:极端情况文件缓存不落地,下载完成文件为空

六、报文结构考点

6.1 请求报文组成

请求行 + 请求头 + 请求体

6.2 响应报文组成

响应行 + 响应头 + 响应体

6.3 Range请求头作用

向服务器指定请求字节区间,实现文件分片、断点续传,是文件下载核心请求头

七、期末简答题背诵

  1. HEAD请求和GET请求区别? HEAD:仅请求响应头,获取文件信息,速度快;GET:请求完整响应体,获取资源数据

  2. 断点续传实现原理? 读取本地已缓存文件长度,通过Range请求头指定下载起始字节,接续下发下载

  3. 为什么使用全局HttpClient? 频繁创建HttpClient会重复创建网络套接字,造成端口占用、内存泄漏,全局复用优化性能

  4. 下载刷新UI为什么需要Invoke? 下载运行在子线程,WinForm禁止跨线程操作控件,Invoke切回主线程刷新界面

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐