欢迎光临
我们一直在努力

C# 扩展控件+组件式自定义定时器控件

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

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

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

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

  • [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修复

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

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

  • 必须添加?空判断: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

    七、易错考点汇总

  • 继承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: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(改错大题)

  • 阻塞死锁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切回主线程刷新界面

  • 赞(0)
    未经允许不得转载:171主机测评 » C# 扩展控件+组件式自定义定时器控件
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址