2020年8月1日星期六

efcore 新特性 SaveChanges Events

efcore saveChanges events

efcore 新特性 SaveChanges Events

Intro

昨天早上看到之前关注的一个 efcore 的 issue 被 closed ,于是看了一眼, ef core 新合并了一个 PR,在 DbContext 中增加了 SaveChanges 相关的几个事件,具体的变更可以参数 PR https://github.com/dotnet/efcore/pull/21862

Events

之前写过两篇关于 EF Core 做自动审计的文章

第一次的实现需要显式继承一个 AuditDbContext ,在有些需要没办法修改 DbContext 或者原有 DbContext 已经有继承某一个类,就没有办法用了,可以参考 https://www.cnblogs.com/weihanli/p/auto-audit-for-entity-framework.html

后面结合 AOP 改进了一版,通过一个审计切面逻辑完成自动审计,但是需要引入 AOP 组件支持,对于不想引入额外组件的项目来说也并非特别友好,可以参考 https://www.cnblogs.com/weihanli/p/12819634.html

在这个 PR 合并之后,我们可以通过 SavingChanges 事件获取保存之前 DbContext 的状态,通过 SavedChanges 事件来获取保存成功后的 DbContext 信息,SaveChangesFailed 事件获取保存失败信息

事件定义如下:

/// <summary>///  An event fired at the beginning of a call to <see cref="M:SaveChanges"/> or <see cref="M:SaveChangesAsync"/>/// </summary>public event EventHandler<SavingChangesEventArgs> SavingChanges;/// <summary>///  An event fired at the end of a call to <see cref="M:SaveChanges"/> or <see cref="M:SaveChangesAsync"/>/// </summary>public event EventHandler<SavedChangesEventArgs> SavedChanges;/// <summary>///  An event fired if a call to <see cref="M:SaveChanges"/> or <see cref="M:SaveChangesAsync"/> fails with an exception./// </summary>public event EventHandler<SaveChangesFailedEventArgs> SaveChangesFailed;

事件参数定义如下:

/// <summary>///  Base event arguments for the <see cref="M:DbContext.SaveChanges" /> and <see cref="M:DbContext.SaveChangesAsync" /> events./// </summary>public abstract class SaveChangesEventArgs : EventArgs{ /// <summary> ///  Creates a base event arguments instance for <see cref="M:DbContext.SaveChanges" /> ///  or <see cref="M:DbContext.SaveChangesAsync" /> events. /// </summary> /// <param name="acceptAllChangesOnSuccess"> The value passed to SaveChanges. </param> protected SaveChangesEventArgs(bool acceptAllChangesOnSuccess) {  AcceptAllChangesOnSuccess = acceptAllChangesOnSuccess; } /// <summary> ///  The value passed to <see cref="M:DbContext.SaveChanges" /> or <see cref="M:DbContext.SaveChangesAsync" />. /// </summary> public virtual bool AcceptAllChangesOnSuccess { get; }}/// <summary>///  Event arguments for the <see cref="DbContext.SavingChanges" /> event./// </summary>public class SavingChangesEventArgs : SaveChangesEventArgs{ /// <summary> ///  Creates event arguments for the <see cref="M:DbContext.SavingChanges" /> event. /// </summary> /// <param name="acceptAllChangesOnSuccess"> The value passed to SaveChanges. </param> public SavingChangesEventArgs(bool acceptAllChangesOnSuccess)  : base(acceptAllChangesOnSuccess) { }}/// <summary>///  Event arguments for the <see cref="DbContext.SavedChanges" /> event./// </summary>public class SavedChangesEventArgs : SaveChangesEventArgs{ /// <summary> ///  Creates a new <see cref="SavedChangesEventArgs" /> instance with the given number of entities saved. /// </summary> /// <param name="acceptAllChangesOnSuccess"> The value passed to SaveChanges. </param> /// <param name="entitiesSavedCount"> The number of entities saved. </param> public SavedChangesEventArgs(bool acceptAllChangesOnSuccess, int entitiesSavedCount) : base(acceptAllChangesOnSuccess) {  EntitiesSavedCount = entitiesSavedCount; } /// <summary> ///  The number of entities saved. /// </summary> public virtual int EntitiesSavedCount { get; }}/// <summary>///  Event arguments for the <see cref="DbContext.SaveChangesFailed" /> event./// </summary>public class SaveChangesFailedEventArgs : SaveChangesEventArgs{ /// <summary> /// Creates a new <see cref="SaveChangesFailedEventArgs"/> instance with the exception that was thrown. /// </summary> /// <param name="acceptAllChangesOnSuccess"> The value passed to SaveChanges. </param> /// <param name="exception"> The exception thrown. </param> public SaveChangesFailedEventArgs(bool acceptAllChangesOnSuccess, [NotNull] Exception exception)  : base(acceptAllChangesOnSuccess) {  Exception = exception; } /// <summary> /// The exception thrown during<see cref="M:DbContext.SaveChanges"/> or <see cref="M:DbContext.SaveChangesAsync"/>. /// </summary> public virtual Exception Exception { get; }}

More

除了上面的审计,你也可以使用通过这些事件,实现保存之前的自动更新数据库字段的值,比如 AddUpdate 操作数据时自动设置更新时间等信息

本文提到的特性还未正式发布,预计会在 .net5 下一个预览版中发布,如果想现在要尝试,请使用 efcore 的 daily build 的包,可以参考 https://github.com/dotnet/aspnetcore/blob/master/docs/DailyBuilds.md

Reference

  • https://github.com/dotnet/efcore/issues/15910
  • https://github.com/dotnet/efcore/pull/21862
efcore 新特性 SaveChanges Events西农东西网usps国际快递查询必看!亚马逊对CPC广告大幅修改,彻底改变广告投放与计费方法Flipkart领投印度生鲜平台Ninjacart 1000万美元C轮融资2019年独立站攻略NO5:Shopify如何做站内优化?2017年春运深圳长途汽车票提前几天订票?怎样买汽车票?2017年春节深圳世界之窗有什么活动?好玩吗?2017年春节深圳欢乐谷有什么优惠活动?

争议之人首发?曝国安迎来最强阵,中超或第一次出现“5外援”_金玟哉

原标题:争议之人首发?曝国安迎来最强阵,中超或第一次出现"5外援"

今晚,中超第二轮继续开打:北京国安VS武汉卓尔,石家庄永昌VS青岛黄海。相比较于第二场比赛,国安与卓尔的对决,可以看作是争夺小组前四之战。

由于上一场比赛吃到红牌,杨帆本轮停赛。那么,谁将与于大宝搭档中卫组合呢?据记者赵宇透露:金玟哉可以首发出战,与此同时,比埃拉、奥古斯托、巴坎布大概率会出现在首发阵容当中。

从最近这两天媒体报道的消息来看,国安的外援们已经全部找到状态。也就是说,他们迎来了最强阵。这里边,金玟哉是一个争议性人物。由于在韩国接受媒体采访时发表了不当言论,其饱受争议。现如今又传出了英超热刺有意他的消息,让其未来充满了扑朔迷离。无论日后走还是留,在球场上,金玟哉应该拿出职业素养了。这点,在韩国也是最被人看重的。

如果这四大外援同时登场的话,中超或第一次出现"5外援"同时在场上的情景:阿兰以内援身份注册,不占用外援名额。也许你会说,恒大应该已经用了吧。这个,还真没有。因为费南多跟洛国富一直在养伤中,恒大还没有"凑齐"。5外援+6国内球员的实力到底有多强,通过这场比赛,我们能够看出个大概来。

随着阿兰的到来,国安阵中的张玉宁的出场时间,必然受到压缩。现在,就看巴坎布的状态到底如何了。倘若他依旧踢着快乐足球,那么张玉宁还有机会与其竞争主力。如果巴坎布状态找回来了,张玉宁的机会就寥寥了。张玉宁之所以结束留洋生涯返回国内,是想获得更多的出场时间。下赛季,如果阿兰继续租借在队,张玉宁应该学一学韦世豪:重找下家了。

国安预测首发

门将:郭全博

后防线:李磊、于大宝、金玟哉、王刚

中场:比埃拉、奥古斯托、朴成、池忠国

锋线:巴坎布、阿兰返回搜狐,查看更多

责任编辑:

争议之人首发?曝国安迎来最强阵,中超或第一次出现"5外援"_金玟哉亚马逊常见收款方式订单处理订单处理与业绩表现查询Test Buy第十一轮贸易谈判结束,特朗普:加征的税可能取消一条假评卖116块!刚刚,亚马逊刷单上热搜国庆诺唯真邮轮游价格皇家加勒比邮轮旅游花费一般多少皇家加勒比邮轮旅游花费一般多少

新一轮1万亿美元的发钱计划蓄势中,库存限制下卖家难接招?

新一轮1万亿美元的发钱计划蓄势中,库存限制下卖家难接招?

亚马逊产品发布政策标红线、加拿大站点并入库存限制统一战线、美国新一轮1万亿美元的发钱计划提上日程。经济刺激计划加持下,卖家又将面临哪些挑战?

亚马逊商品详情信息页面规则收紧

近期,有不少卖家向美鸥网小编反馈,称他们的 ASIN 因违规而被禁止展示了……卖家们一头雾水,这究竟是怎么一回事?

亚马逊商品详情信息页面规则收紧,卖家以后发布产品都要遵循以下指南:

22.png

7月28日,亚马逊发布商品详情页面指南公告表示,为了提供良好的客户体验,卖家发布的商品务必符合我们的商品属性要求。在创建商品信息之前,必须遵循以下规定:

对于产品说明和要点,请勿包括你的姓名,电子邮件地址,网站URL,公司特定信息,有关你出售的其他产品的详细信息或促销语言(例如"促销"或"免费送货")。此外,我们的产品发布政策还禁止在商品详情页面中包含卖家,公司或配送信息。

亚马逊在公告中也讲到了违规者的处罚措施,表示会开始禁止在店铺中展示违反亚马逊商品详情页面规则和卖家行为准则的的列表。如果卖家的ASIN因违规而被禁止,将通过卖方中心的业绩通知获知相关处罚。

与此同时,卖家若是违规了也有补救措施:卖家可以通过"管理库存"中的编辑链接或使用上传数据批量更新来移除不允许的文字内容。商品信息更正后,卖家发布的商品将在 24 小时内自动恢复,且该 ASIN 将处于在售状态。

加拿大站也开始实行库存限制

一点都不意外!在美国站、欧洲站和日本站之后,加拿大站也加入了入仓限制的大军。

21.jpg

还是熟悉的配方,还是熟悉的味道。亚马逊方面表示,将对在加拿大站的亚马逊物流商品实施ASIN级别的数量限制,多数商品将有足够的仓储空间,可用于2到4个月的商品销售。

亚马逊发布入仓限制的新闻原文如下:

23.png

虽然中国卖家在加拿大的销量对于美国来说要小得多,影响相对美国站也小很多,但还是有不少卖家在叠加效应下表示难以承受。

又发钱!美国新一轮经济刺激政策

24.png


虽然亚马逊最近出的政策没一个是卖家想"听"的,但最近美国新一轮的发钱计划却让卖家又忧又喜。

周一晚上,参议院共和党人公布了一项新的1万亿美元的冠状病毒救助计划,称之为"医疗救助法"。

符合《医疗救助法》规定的支付额和收入门槛与第一轮刺激计划相同。年收入7.5万美元以下的个人最高可获得1200美元,年收入15万美元以下的已婚夫妇最高可获得2400美元,以及每名17岁以下符合条件的儿童可获得500美元。

据悉,这一轮新的1200美元的刺激支出可能会惠及数百万美国人。此前,大部分美国人领到补助金之后疯狂消费,亚马逊也迎来了一波流量暴涨,一定程度上缓解了美国市场消费力疲软的状况,很多卖家都希望这次也能如此,但又为当前严峻疫情下的美国站现状担忧不已。


(来源:美鸥网  Yuan.)


查看原文:https://www.ikjzd.com/home/126840

2017年Sam大叔跨境电商全国巡讲梦想之旅•深圳站:https://www.kjyunke.com/courses/449

"Google+Facebook"双核流量引擎打造闭门集训营:https://www.kjyunke.com/courses/308

FBA解释与实操:https://www.kjyunke.com/courses/93

新一轮1万亿美元的发钱计划蓄势中,库存限制下卖家难接招?

为张庆鹏抱不平?专家都看不下去了:山东队球员的表现对不起他_陶汉林

原标题:为张庆鹏抱不平?专家都看不下去了:山东队球员的表现对不起他

北京时间7月31日,CBA季后赛附加赛,山东队最终以120-129不敌青岛队,惨遭淘汰出局。此役,山东队39岁老将张庆鹏疯狂轰下37分,却依旧独木难支,无法为山东队换来一胜。看到这里,就连专家都看不下去了,直言山东队除了陶汉林之外,其余球员都对不起张庆鹏。

作为一个39岁的老将,本赛季很有可能会是张庆鹏生涯最后一战。而在与青岛队一战,山东队如果输给对手,那么,这一场球就会成为张庆鹏的谢幕战。尽管在上半场没有表现的机会,不过,在下半场开始后,张庆鹏便开启了疯狂进攻模式,第三节单节砍下13分,第四节又独得24分,下半场狂轰37分。

然而,无奈山东队其他球员并没有发挥出来,除了砍下33分10板的陶汉林之外,山东便再也没有球员得分超过15分。在看到山东队球员的表现,再看看张庆鹏的表现,著名篮球专家朱彦硕就直言不讳的表示,山东队除了陶汉林之外,其余球员都对不起张庆鹏。

前八一男篮名宿吴谦同样被张庆鹏打服了。他表示,40岁的老头,拼下37分,战斗至最后一刻,是个战士。

著名篮球专家苏群则联想到孙铭徽。"张庆鹏和孙铭徽,隔了两代的中国控卫,一个得37分,一个得45分,但都被淘汰了,CBA独一无二的单败淘汰季后赛,太残酷。"返回搜狐,查看更多

责任编辑:

为张庆鹏抱不平?专家都看不下去了:山东队球员的表现对不起他_陶汉林FBA实战平台入驻不用愁,招a商经理有话说Snapchat玩法介绍海关总署:关于调整出口危险货物包装生产企业代码的公告!亚马逊CPC打法,你知道多少?流量增长乏力?私域流量或成跨境电商卖家突围新策略!成都旅行团以往国庆诺唯真邮轮游价格以往国庆诺唯真邮轮游价格

无废话设计模式(8)结构型模式

0-前言

  组合模式定义(Composite):将对象组合成树形结构以表示“部分-整体”的层次结构。

                组合模式使得用户对单个对象和组合对象的使用具有一致性;

1-实现

1-1、简单UML图:

 

1-2、代码实现:

 

//1、抽象父类(公司父类)abstract class Company{ protected String Name; //部门名称 public Company(String name) {  this.Name = name; } public abstract void Add(Company c); //增加 public abstract void Remove(Company c); //移除 public abstract void Display(int depth); //显示 public abstract void LineofDuty(); //履行部门职责}//2、枝节点(具体公司类)class ConcreteCompany extends Company{ private List<Company> childrenList = new ArrayList<Company>(); public ConcreteCompany(String companyname) {  super(companyname); } @Override public void Add(Company c) {  childrenList.add(c); } @Override public void Remove(Company c) {  childrenList.remove(c); } @Override public void Display(int depth) {  for(int i=0;i<depth;i++)  {   System.out.print('-');  }  System.out.print(Name + '\n');  for (int i = 0;i<childrenList.size();i++)  {   childrenList.get(i).Display(depth + 2);  } } @Override public void LineofDuty() {  for (int i = 0;i<childrenList.size();i++)  {   childrenList.get(i).LineofDuty();  } }}//3-1、叶子节点(具体公司部门类--人力资源部)class HRDepartment extends Company{ public HRDepartment(String companyname) {  super(companyname); } @Override public void Add(Company c) { } @Override public void Remove(Company c) { } @Override public void Display(int depth) {  for(int i=0;i<depth;i++)  {   System.out.print('-');  }  System.out.print(Name + '\n'); } @Override public void LineofDuty() {  System.out.println(Name + ": 招聘员工"); }}//3-2、叶子节点(具体公司部门类--财务部)class FinanceDepartment extends Company{ public FinanceDepartment(String companyname) {  super(companyname); } @Override public void Add(Company c) { } @Override public void Remove(Company c) { } @Override public void Display(int depth) {  for(int i=0;i<depth;i++)  {   System.out.print('-');  }  System.out.print(Name + '\n'); } @Override public void LineofDuty() {  System.out.println(Name + ": 发放工资"); }

 

客户端:

  ConcreteCompany root = new ConcreteCompany("深圳总公司");  root.Add(new HRDepartment("深圳总公司人力资源部"));  root.Add(new FinanceDepartment("深圳总公司财务部"));  ConcreteCompany cp1 = new ConcreteCompany("昆明分公司");  cp1.Add(new HRDepartment("昆明分公司人力资源部"));  cp1.Add(new FinanceDepartment("昆明分公司财务部"));  root.Add(cp1);  ConcreteCompany cp2 = new ConcreteCompany("云大办事处");  cp2.Add(new HRDepartment("云大办事处人力资源部"));  cp2.Add(new FinanceDepartment("云大办事处财务部"));  cp1.Add(cp2);  ConcreteCompany cp3 = new ConcreteCompany("昆工办事处");  cp3.Add(new HRDepartment("昆工办事处人力资源部"));  cp3.Add(new FinanceDepartment("昆工办事处财务部"));  cp1.Add(cp3);  System.out.println("结构图:");  root.Display(1);  System.out.println("职责:");  root.LineofDuty();

 

 

运行结果:

 

 

2-应用场景简单总结

1、当体现部分与整体层次的结构时;

2、希望用户可以忽略组合对象与单个对象的不同,统一地使用组合结构中所有对象时;

 

无废话设计模式(8)结构型模式孙琦运费设置Listing优化细节要点想乱收我FBA费用,没门!卖家一招成功获得大额赔偿 2018旺季美国线销售额达$1260亿 / 美国际贸委会对我国电子烟发起337调查亚马逊澳大利亚站推出Renewed计划 / 天猫国际发布双11"三新"策略2017五一去大小梅沙露营天气怎么样?带什么衣服去?2017年五一深圳什么地方最适合旅游?深圳金沙湾怎么样?2017年五一深圳什么旅游景点最美?

2020年7月31日星期五

[C#] (原创)一步一步教你自定义控件——01,TrackBar

一、前言

技术没有先进落后之分,只有合不合适。

WinForm有着非常多的优点,在使用WinForm久了之后,难免会觉得WinForm自带的某些控件外观上有些许朴素、或者功能上有些不如意,自然而然便想去美化这些控件,或者给控件添加一些额外功能,而这便是自定义控件的意义所在。

自定义控件的难度并不大,但是却处在一个比较尴尬的位置:

1,一般的教材不会讲——因为还是有难度的,而且一般用不上;

2,而网上或书上所找到的自定义控件相关知识教程里,大多都是给一个已完成的自定义控件,再附上源码,只有了了注释和说明。毕竟难度不大,懂的自然懂,而且对懂的人来说,看别人的自定义控件往往是为了看一下实现的思路或某个点的实现方法,因为很多都是一点就透。

对于初学者而言,要想掌握自定义控件,就需要花费不少的时间去学习那些源代码、去模仿、去练习、去摸索,最后一步步去归纳总结出适合自己的一条路。当掌握了之后,回头看去,会发现其实真的不难,耗费的时间与学习的难度并不成正比,这些额外的时间就花费在了摸索和总结上了。

我也是这样一步步走来的,所以不想让大家再花费这么多的时间去掌握一项并不太难的知识,便有了这篇文章。

在本文中,我会从零开始,带着大家一步一步去实现一个自定义控件,同时会分享一些我的经验之谈,相信看完的你,一定会有所收获。

本篇的自定义控件是:TrackBar

本文地址:https://www.cnblogs.com/lesliexin/p/13265707.html


 

二、前期分析

(一)为什么需要去自定义控件?

我们来分析一下为什么要去自定义控件。

以本文要实现的TrackBar为例,最主要的原因便 是系统自带的TrackBar太过朴素,所以需要一款比较好看的TrackBar控件。

系统自带的TrackBar:

预想的TrackBar样式:

(二)实现目标

在实现一个自定义控件前,我们要确定一下我们要实现的目标,比如外观、功能、特点等。

1,外观

个人经验之谈

在设计预想样式时,可以何用任何方式,只要自己可以看明白就行,但是还是推荐使用绘图软件去做一个示意图,主要是因为在自定义控件时,往往会需要用到一些坐标、宽、高等值,特别是和GDI+有关时。使用绘图软件则可以去准确和清晰的标注出来这些信息,并进行相关的计算。

我想实现的TrackBar的外观样式如下:

2,功能

参考系统的TrackBar,可以将所需要的功能归为下面几点:

(1)支持鼠标点击。

(2)支持鼠标拖动。

(3)支持修改颜色。 

3,特点

既然全实现自己的TrackBar,肯定要有自己的特点。

(1)支持颜色调整,包括背景色和前景色。

(2)支持圆角显示,和直角显示。

(三)技术分析

在自定义控件的目标定好之后,接下来便是分析实现上述目标所需要的技术。

1,整体实现

 自定义的TrackBar从逻辑上可以分为两层:背景条(Bar)和滑块(Slider)。

在具体实现时也是按照这两层的思路去分层实现。

2,主要技术

通过上面的分析的示意,我们发现GDI+可以实现上述目标,所以我们的主要技术便是——GDI+。

3,圆角和直角的实现

直角可以使用GDI+中的Graphics.DrawLine去实现。那么圆角怎么实现呢?

其实也很简单,仍然使用Graphics.DrawLine实现,不过在创建Pen时,需要设置一下LineCap,通过LineCap可以实现多种样式,除了圆角外,还有菱形、箭头等等。

具体的设置后文会讲解,此处不再赘述。

MSDN中关于LineCap的说明如下:

指定可用线帽样式,Pen 对象以该线帽结束一段直线。

 


 

三、开始实现

(一)前期准备

1,创建自定义控件类库项目

个人经验之谈

建议创建自定义控件时,将自定义控件写在一个单独的类库里。主要的目的是提高复用性,同时也方便管理,以及方便控件间的相互调用。

关于控件间的相互调用:

因为控件除了单个的自定义控件外,还有用户控件(UserControl)——实现某些复杂功能的时候,往往就需要用到用户控件。用户控件往往是多个控件的组合,所以将控件放到一个类库中可以方便的调用,修改也方便。

启动VS(本文使用的VS2019),添加新的 类库(.NET Framework)项目,起好项目名称并选好位置,点击创建。

个人经验之谈

关于框架的选择。

在实际应用当中,框架版本要根据自定义控件所服务的项目去选择。因为是自定义控件,所以兼容性很高,往往.Net 2.0就可以实现绝大部分效果。所以,可以根据具体的项目去选择框架的版本,当然也可以选一个.Net 2.0,然后在实现完成之后编译成不同框架版本。

2,添加类

在项目名称上右击,选择添加-类,输入类名:LTrackBar.cs,确定。

个人经验之谈

关于类名

在起自定义控件的名称时,最好不要和系统控件名称一样,那样会导致二义性,平白增加代码量。

所以可以统一加一个前缀或后缀,如:TextBoxEx,PanelPlus。本文便是统一加上前缀”L“——LTrackBar

3,添加继承

在添加继承时,根据具体的需要去选择不同的继承。比如要对ComboBox的一拉选项添加不同的颜色,就继承ComboBox并进行重绘;比如要让TextBox支持透明,就继承TextBox进行重写等等。

在本例的LTrackBar中,通过前文的分析发现很简单,所以可以继承基础的Control类。

(1)添加继承

在类名后输入”:Control“

(2)添加引用

上一步里会发现”Control“显示代表错误的波浪线,我们将鼠标悬浮在上面,在弹出的提示按钮上点击,选择”将引用添加到System.Windows.Forms.dll",然后"Control"下面的波浪线将会消失,并变为浅蓝色。

(3)修改可访问性。

由于是一个单独的类库,并且LTrackBar是一个独立的控件,所以我们需要将类的可访问性修改为Public。

4,添加自定义属性

个人经验之谈

关于参数命名

对于公共参数,个人建议添加一个统一的前缀。主要原因有两点:

1,在视图设计界面中的属性窗口中,无论是“按分类排序”还是“按字母排序”,都可以使控件所公开的自定义属性集中在一起。

按分类排序:

按字母排序:

2,在代码编辑界面,可以在输入统一的前缀后,将该控件的所以自定义属性都在代码提示窗口中显示在一起,方便选择。

(1)颜色相关

通过前文可知,我们涉及到的颜色有两个——背景条颜色和滑块颜色。所以我们添加两个属性,其中的“Invalidate()”是为了在修改该属性值后立刻使控件重绘。

(2)圆角相关

(3)最大值与最小值

如TrackBar一样,我们也需要有最大值和最小值,由于我的需要很简单,所以只支持整型(int)。

首先,最小值应该大于0,然后最小值要小于最大值,所以最小值如下:

其次,最大值也应该大于最小值。

(4)当前值 

用来获取或设置当前LTrackBar所代表的值。

当前值需要在最大值和最小值之间,同时我们需要知道值发生了变化,所以添加了一个委托事件LValueChanged,关于委托和事件此处不展开讲,因为不懂也不影响使用,就像固定公式一样往上套就行了。只需要知道其作用是让调用本控件的人知道当前的值发生了变化。

(5)方向

LTrackBar支持横向显示,也支持竖向显示。

在横向显示时,分为两种情况:1,左端为最小值(L_Minimum),右端为最大值(L_Maximum);2,左端为最大值(L_Maximum),右端为最小值(L_Minimum)。

在竖向显示时,分为两种情况:1,顶部为最小值(L_Minimum),底部为最大值(L_Maximum);2,顶部为最大值(L_Maximum),底部为最小值(L_Minimum)。

综上,共有4种情况,所以我们先创建一个枚举。

同样为了方便统一管理,新建一个类专门存放枚举信息。

之后,创建一个Orientation枚举类型的属性:

上面的那两个if语句的作用是为了实现在改变方向后,自动交换控件的宽和高。

(6)宽度/高度

像TrackBar只能在设计器中调整宽度一样,LTrackBar也只能调整宽度(横向显示时)或高度(竖向显示),所以需要一个属性来控制。

为了实现只能调整宽度/高度,需要重写SetBoundsCore方法,MSDN上关于SetBoundsCore的说明如下:

我们需要对其进行重写,以限制只能调整宽度或高度:

由于VS的强大,所以在重写时非常方便:

 

(7)增加描述信息

在公开属性上加入Catagory(分组),Description(描述)。之后便可以在属性窗口看到相应的分类和说明。

 

5,添加事件

为了获取LTrackBar的当前值,以及在值改变时执行某些操作,所以需要增加一个事件。事件数据则为当前值(L_Value)。

(1)新建类,继承自EventArgs。

(2)新建委托和事件

 6,重写方法

通过前文的分析,我们知道主要用到了GDI+,同时支持鼠标点击、拖动。所以我们需要重写以下这些方法。

其中,OnPaint事件是用来画显示界面的。Mouse相关的事件是与实现鼠标操作相关的。

为了知道当前鼠标的状态(进入、离开、按下、松开),需要定义一个枚举:

下面是每个重写方法的具体说明:

(1)OnMouseEnter方法

标识着鼠标进入,只需要设置一下鼠标状态即可。

(2)OnMouseLeave方法

同上

(3)OnMouseUp方法

同上

 

(4)OnMouseDown方法

当鼠标点击了控件时会触发本事件。在鼠标点击后,控件应该重绘界面,主要是滑块(Slider)的变化,同时滑块(Slider)所代表的值也应该发生变化,同时引发LValueChanged事件。

(5)OnMouseMove方法

当鼠标在控件上移动时触发本事件,在实际操作时都是在在按着鼠标左键并拖动,所以要判断鼠标的状态(mouseStatus)是否是按下(Down)。其他同上。

在OnMouseDown和OnMouseMove中,有一个方法:pPointToValue(),其作用便是将鼠标的坐标值转换为对应代表的值。其代码如下:

其代码很简单,就是计算鼠标落点占控件宽度/高度的比例,再乘以值的范围就得到了代表的值。在下文中有示意图讲解,本处不再赘述。

(6)OnPaint方法

本方法是控件实现的核心。几乎只要涉及控件重绘和自定义控件,都兔不了要重写OnPaint方法。

在OnPaint方法中,我们主要完成两部分的操作:

1)画背景条(Bar)

2)画滑块(Slider)

这便是OnPaint方法的完整代码:

protected override void OnPaint(PaintEventArgs e){ base.OnPaint(e); pValueToPoint(); e.Graphics.SmoothingMode = SmoothingMode.HighQuality; Pen penBarBack = new Pen(_BarColor, _BarSize); Pen penBarFore = new Pen(_SliderColor, _BarSize); float fCapHalfWidth = 0; float fCapWidth = 0; if (_IsRound) {  fCapWidth = _BarSize;  fCapHalfWidth = _BarSize / 2.0f;  penBarBack.StartCap = LineCap.Round;  penBarBack.EndCap = LineCap.Round;  penBarFore.StartCap = LineCap.Round;  penBarFore.EndCap = LineCap.Round; } float fPointValue = 0; if (_Orientation == Orientation.Horizontal_LR || _Orientation == Orientation.Horizontal_RL) {  e.Graphics.DrawLine(penBarBack, fCapHalfWidth, Height / 2f, Width - fCapHalfWidth, Height / 2f);  fPointValue = mousePoint.X;  if (fPointValue < fCapHalfWidth) fPointValue = fCapHalfWidth;  if (fPointValue > Width - fCapHalfWidth) fPointValue = Width - fCapHalfWidth; } else {  e.Graphics.DrawLine(penBarBack, Width / 2f, fCapHalfWidth, Width / 2f, Height - fCapHalfWidth);  fPointValue = mousePoint.Y;  if (fPointValue < fCapHalfWidth) fPointValue = fCapHalfWidth;  if (fPointValue > Height - fCapHalfWidth) fPointValue = Height - fCapHalfWidth; } if (_Orientation == Orientation.Horizontal_LR) {  e.Graphics.DrawLine(penBarFore, fCapHalfWidth, Height / 2f, fPointValue, Height / 2f); } else if (_Orientation == Orientation.Horizontal_RL) {  e.Graphics.DrawLine(penBarFore, fPointValue, Height / 2f, Width - fCapHalfWidth, Height / 2f); } else if (_Orientation == Orientation.Vertical_TB) {  e.Graphics.DrawLine(penBarFore, Width / 2f, fCapHalfWidth, Width / 2f, fPointValue); } else {  e.Graphics.DrawLine(penBarFore, Width / 2f, fPointValue, Width / 2f, Height - fCapHalfWidth); }}
OnPaint

在OnPain方法用到了一个方法:pValueToPoint(),其作用是将值转换为相应坐标。代码如下:

private void pValueToPoint(){ float fCapHalfWidth = 0; float fCapWidth = 0; if (_IsRound) {  fCapWidth = _BarSize;  fCapHalfWidth = _BarSize / 2.0f; } float fRatio = Convert.ToSingle(_Value-_Minimum) / (_Maximum - _Minimum); if (_Orientation == Orientation.Horizontal_LR) {  float fPointValue = fRatio * (Width - fCapWidth) + fCapHalfWidth;  mousePoint = new PointF(fPointValue, fCapHalfWidth); } else if (_Orientation == Orientation.Horizontal_RL) {  float fPointValue = Width - fCapHalfWidth - fRatio * (Width - fCapWidth);  mousePoint = new PointF(fPointValue, fCapHalfWidth); } else if (_Orientation == Orientation.Vertical_TB) {  float fPointValue = fRatio * (Height - fCapWidth) + fCapHalfWidth;  mousePoint = new PointF(fCapHalfWidth, fPointValue); } else {  float fPointValue = Height - fCapHalfWidth - fRatio * (Height - fCapWidth);  mousePoint = new PointF(fCapHalfWidth, fPointValue); }}
pValueToPoint

之所以没有注释,实在是太过浅显无可注释,单纯的看代码很难理解,下面我将通过示意图的方法讲解,其实只要看了示意图,就会恍然大悟,会发现其实很简单。

7,示意图解

对于LTrackBar而言,有两种样式:直角和圆角。这两种的实现并没有太大不同,主要是Pen的LineCap属性不同,LineCap说明见前文。

(以下将以横向、从左到右的样式(_Orientation = Orientation.Horizontal_LR)进行讲解,其他类同,不多赘述。)

示意图1:

我在图中标注了一些点,主要用来详解。

上图中的B点(Rect.B、Round.B)即是当前鼠标点击的点,也是代表当前值的点,也是蓝色条的宽度。

示意图2:

在LineCap=Round时,其在绘制的线条两端会各绘制一个半圆,如上图中紫色所示。其半圆直径等于线条宽度。

下面我会讲解一下上面那些代码中的那些算式是怎么来的。

(1)直角

1)计算

已知:

起始点:Rect.A;

结束点:Rect.C;

点Rect.A 对应的值为: L_Minimum;

点Rect.C 对应的值为: L_Maximum;

鼠标可点击范围=控件宽度 = Bar.Width;

实际取值范围 = (L_Maximum-L_Minimum);

鼠标点击处的X值=点Rect.B = Slider.Width;

鼠标点击处的X值与鼠标可点击范围的比值=该点击处对应的实际值与取值范围的比值,即:

对应值/取值范围=Slider.Width/Bar.Width;

所以:

对应值(_Value)=Slider.Width/Bar.Width*(L_Maximum-L_Minimum);

由于最左侧的点Rect.A并不是0,而是对应着L_Minimum,所以,最后得到的真实值(L_Value)=_Value+L_Minimum;

2)绘制
设置Pen的宽度=Bar.Height

所以要从控件高度的中间开始绘制,其起终坐标如下:

起点:(Rect.A)=(0,Bar.Height/2);

终点:(Rect.C)=(Bar.Width,Bar.Height/2);

(2)圆角

1)计算

已知:

因为设置了圆角(LineCap=Round),所以线条两端会各绘制一个半圆(示意图中紫色半圆所示),其半圆直径等于线条宽度。

那么其开始点便不再是点Round.A,而是点Round.D,同理,其结束点也不是点Round.C,而是点Round.E。

点Round.D 对应的值为: L_Minimum;

点Round.E 对应的值为: L_Maximum;

鼠标可点击范围=控件宽度减去两个半圆的宽度 = (Bar.Width-Bar.Height);

实际取值范围 = (L_Maximum-L_Minimum);

鼠标点击处的X值 (点Round.B) = (Slider.Width-Bar.Height/2);(注意:此时鼠标点击处所产生的视觉效果范围是(Round.A~Round.F),但其真正移动的范围是(Round.D~Round.B)。)

鼠标点击处的X值与鼠标可点击范围的比值=该点击处对应的实际值与取值范围的比值,即:

对应值/取值范围= (Slider.Width-Bar.Height/2)/ (Bar.Width-Bar.Height);

所以:

对应值(_Value)= (Slider.Width-Bar.Height/2)/ (Bar.Width-Bar.Height)*(L_Maximum-L_Minimum);

由于可点击的最左侧的点Round.D对应着L_Minimum,所以,最后得到的真实值(L_Value)=_Value+L_Minimum;

2)绘制

设置Pen的宽度=Bar.Height,所以要从控件高度的中间开始绘制。

又因为设置LineCap=Round,导致两端各绘制了一个半圆,所以其起点和终点的坐标也应减去相应的值:

起点:(Round.D)=(Bar.Height/2,Bar.Height/2);

终点:(Round.E)=(Bar.Width-Bar.Height/2,Bar.Height/2);


 

四,效果演示及调整优化

1,演示

我们在项目上右键,选择生成,之后在同一解决方案下新建一WinForm项目,此时在工具箱的最上层会有我们的自定义控件——LTrackBar。

如图:

 

我们选中并添加到主界面上,并设置相应的属性。

同时添加一个label,用来显示当前的值。 

其实效果如下:

在实际运行时,我们会发现在点击和拖动时,控件会有闪烁(由于GIF录制帧率,所以上面的动图不看不闪烁)。

为了解决闪烁的问题,我们在LTrackBar的构造函数上添加对双缓冲的支持。

个人经验之谈

关于双缓冲

一般而言,只要涉及到了GDI+,都会使用双缓冲技术去减少闪烁,而且使用也很简单,就两行代码而已:

SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);

当然,ControlStyles还有很多属性,其作用也各有作用,在以后的文章中如果有用到我会再说明的。

2,默认事件

默认事件,顾名思义,就是双击控件时自动生成的事件,像双击Button时的Click事件,双击TextBox时的TextChanged事件等。

要实现这种效果,需要在代码的最上面加上DefaultEvent事件,如下:

 

 其中“LValueChanged”就是我们要设置的默认事件。这样在我们双击LTrackBar时,便会自动生成该事件。


 

五、结束语

通篇下来,其实可以发现并没有用到多深的知识,更多的是想像力,解放你的思想,不要被常规所束缚。


 

六、源代码及工程下载

https://files.cnblogs.com/files/lesliexin/LTrackBar.7z

 

[C#] (原创)一步一步教你自定义控件——01,TrackBar贝恩eBay全品类备战旺季高峰会crowd 什么是dropshipping?跨境电商Dropshipping订单模式详解!Shopee订单退货退款这些Q4旺季大杀招,你可能永远想不到!2017年中秋节去西冲海滩玩好不好?2017深圳西冲三门岛有什么超级好玩的娱乐项目?2017深圳欢乐谷狂欢节什么时候开幕?