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)