2023年2月

在一些耗时的操作过程中,在长时间运行时可能会导致用户界面 (UI) 处于停止响应状态,用户在这操作期间无法进行其他的操作,为了不使UI层处于停止响应状态,我们倾向推荐用户使用BackgroundWorker来进行处理,这个后台的线程处理,可以很好的实现常规操作的同时,还可以及时通知UI,包括当前处理信息和进度等,这个BackgroundWorker的处理在百度里面也是有很多使用的介绍,本篇随笔主要是做一些自己的使用总结,希望也能给读者提供一个参考。

在使用BackgroundWorker的过程中,我们可以定义自己的状态参数信息,从而实现线程状态的实时跟踪以及进度和信息提示,方便我们及时通知UI进行更新。本篇随笔主要针对一些数据采集过程的处理,在网上采集特定的数据往往需要耗时几个小时以上,如果采用常规的同步操作,比较麻烦,而如果引入一些SmartThreadPool这些第三方类库有显得臃肿,而且资源耗费的也很严重,因此使用BackgroundWorker相对比较轻型的方案比较吸引我。

采集的数据处理

例如是我采集数据的一个局部界面,主要是根据一些参数进行数据的采集,采集过程可以通过状态栏和右边的标签进行反馈,在状态栏显示采集进度等信息,实现比较友好的信息显示。

一般我们定义后台线程处理,可以在该窗体定义一个变量即可,如下代码所示。

        private BackgroundWorker worker = new BackgroundWorker();

然后就是对这个后台线程处理对象的一些事件进行实现即可,如下代码所示

    public partial classMainFrame : BaseForm
{
/// <summary> ///增加一个变量来记录线程状态/// </summary> private bool IsThreadRunning = false;private BackgroundWorker worker = newBackgroundWorker();publicMainFrame()
{
InitializeComponent();

Portal.gc.InitData();

worker.WorkerSupportsCancellation
= true; //支持取消 worker.WorkerReportsProgress = true; //支持报告进度 worker.DoWork += worker_DoWork; //处理过程 worker.RunWorkerCompleted += worker_RunWorkerCompleted; //完成操作 worker.ProgressChanged += worker_ProgressChanged; //报告进度 }

例如进度条的通知,主要就是计算总任务的数量,以及当前完成的人数数量,我们实现代码如下所示

        /// <summary>
        ///进度条的通知/// </summary>
        void worker_ProgressChanged(objectsender, ProgressChangedEventArgs e)
{
this.barProgress.EditValue =e.ProgressPercentage;
CollectStateInfo stateInfo
= e.UserState asCollectStateInfo;if (stateInfo != null)
{
var message = string.Format("正在采集 {0} 的 {1} , 项目名称为:{2}", stateInfo.TotalRecords, stateInfo.CompletedRecord + 1, stateInfo.CurrentItemName);this.lblTips.Text =message;this.barTips.Caption =message;//记录运行位置 JobParameterHelper.SaveData(newCurrentJobParameter(stateInfo));
}
}

这里我们看到了,这个里面使用了一个自定义的状态参数CollectStateInfo ,这个是我们用来在后台进程处理过程中传递的一个对象,可以记录当前采集的相关信息,CollectStateInfo 类的定义如下所示。

    /// <summary>
    ///状态对象数据/// </summary>
    public classCollectStateInfo
{
/// <summary> ///当前期数(年份+期数)/// </summary> public string YearQSNumber { get; set; }/// <summary> ///任务开始时间/// </summary> public DateTime StartTime { get; set; }private DateTime m_EndTime =DateTime.Now;/// <summary> ///任务开始时间/// </summary> publicDateTime EndTime
{
get{returnm_EndTime;
}
set{//设置结束时间的时候,获取耗时 m_EndTime =value;this.TimeSpanUsed = value.Subtract(this.StartTime);
}
}
/// <summary> ///任务用时/// </summary> public TimeSpan TimeSpanUsed { get; set; }/// <summary> ///任务数量/// </summary> public int TotalRecords { get; set; }private int m_CompletedRecord = 0;/// <summary> ///完成数量/// </summary> public intCompletedRecord
{
get{returnm_CompletedRecord;
}
set{
m_CompletedRecord
=value;if (TotalRecords > 0)
{
this.CurrentProgress = Convert.ToInt32(value * 100.0 /TotalRecords);
}
}
}
/// <summary> ///当前进度/// </summary> public int CurrentProgress { get; set; }/// <summary> ///当前采集的项目/// </summary> public string CurrentItemName { get; set; }/// <summary> ///默认构造函数/// </summary> /// <param name="total"></param> publicCollectStateInfo()
{
this.StartTime =DateTime.Now;this.EndTime =DateTime.Now;
}
/// <summary> ///构造函数/// </summary> /// <param name="total">任务数量</param> /// <param name="qsNumber">采集当前期数</param> public CollectStateInfo(int total, string qsNumber, int completed) :this()
{
this.TotalRecords =total;this.YearQSNumber =qsNumber;this.CompletedRecord =completed;
}

}

上面的对象,主要用来记录任务的总数,以及当前进行的数量,还包括一些其他信息,如任务的开始时间,结束时间等等,我们可以把一些常规的任务信息,放到这里面来传递即可。

另一个后台进程处理的关键事件就是处理过程的代码实现,主要就是采集处理的逻辑内容,如下所示。

        void worker_DoWork(objectsender, DoWorkEventArgs e)
{
CollectStateInfo info
= e.Argument asCollectStateInfo;if (info != null)
{
LinkJob job
= newLinkJob();var stateInfo = job.Execute(this.worker, info);
e.Result
=stateInfo;
}
}

这个里面我么主要到它的e.Argument 就是我们传递的对象,通过类型转换我们就可以获得对应的信息,然后进行具体的处理了。

另外一个就是当整个后台进程完成处理后,我们需要进行相关的提示和状态处理,实现代码如下所示。

        void worker_RunWorkerCompleted(objectsender, RunWorkerCompletedEventArgs e)
{
//还原按钮状态 InitCollectState();
IsThreadRunning
= false;string message = "采集操作完成";
CollectStateInfo stateInfo
= e.Result asCollectStateInfo;if (stateInfo != null && stateInfo.CompletedRecord ==stateInfo.TotalRecords)
{
message
+= string.Format(",完成采集网址{0}个,耗时为:{1}分钟{2}秒。", stateInfo.TotalRecords, stateInfo.TimeSpanUsed.Minutes, stateInfo.TimeSpanUsed.Seconds);//清空数据即可 JobParameterHelper.ClearData();
}
else{
message
+= string.Format(",用户取消处理,耗时为:{1}分钟{2}秒。", stateInfo.TotalRecords, stateInfo.TimeSpanUsed.Minutes, stateInfo.TimeSpanUsed.Seconds);
}
MessageDxUtil.ShowTips(message);
}

而我们开始任务,则通过按钮触发后台线程的异步接口调用即可,如下代码所示。

                if (!worker.IsBusy)
{
this.btnStartCollect.ImageOptions.Image =Resources.Button_Stop;this.lblTips.Text = "数据采集中....,单击按钮可停止采集";this.btnStartCollect.Text = "停止采集";var totalCount = BLLFactory<URLLink>.Instance.GetRecordCount();//数量为总数 var stateInfo = newCollectStateInfo(totalCount, yearQSNumber, skipCount);

worker.RunWorkerAsync(stateInfo);
//改变状态 IsThreadRunning = !IsThreadRunning;
}

这里面我们设置提示开始采集数据后,然后构建一个可以用于传递的线程采集对象给后台线程,通过异步调用worker.RunWorkerAsync(stateInfo); 即可实现任务的开始操作。

如果任务总之,我们调用取消接口即可。

                if (MessageDxUtil.ShowYesNoAndWarning("采集正在进行中,您确认停止采集吗?") ==System.Windows.Forms.DialogResult.Yes)
{
worker.CancelAsync();
//改变状态 IsThreadRunning = !IsThreadRunning;//还原按钮状态 InitCollectState();
}

启动采集界面进行相应的处理即可,如下所示。

采集过程的进度可以通过状态栏实时的显示出来,这个有赖于我们定义的状态类,可以很方便进行UI的信息通知。

以上就是使用后台 线程BackgroundWorker处理任务的一些总结,希望给读者带来一些参考价值,在我们做一些耗时的操作的时候,可以考虑使用这个后台线程BackgroundWorker处理任务,从而实现较好的界面通知,也不会造成UI界面的停顿卡死状态。

我们知道,基于DevExpress的开发Winform的项目界面的时候,GridControl控件是经常用来绑定数据的,一般以常规的字符内容为主,有时候也会有图片的显示需要,那么如果显示图片,我们应该如何实现呢?本篇随笔介绍基于原生GridControl控件的图片绑定显示操作和基于我封装的分页控件(封装GridControl的分页控件)两种图片绑定显示的操作。

1、基于原生的GridControl控件图片绑定

绑定图片,一般我们可以在单元格里面绑定byte[]类型或者Image类型,那么控件就会自动显示图片出来,当然我们也可以自定义对图片路径转换为图片然后显示的,不过就是额外需要增加一些处理而已。

本例子针对这三种方式分别进行介绍,图片的绑定操作。

为了方便演示,我们创建一个菜单对象类,然后构建一些数据用于列表的绑定操作,如下代码所示。

    /// <summary>
    ///模拟一个菜单的对象,包括各种类型的图片信息/// </summary>
    public classMenuInfo
{
/// <summary> ///编号/// </summary> public string ID { get; set; }/// <summary> ///图标名称/// </summary> public string Name { get; set; }/// <summary> ///图片路径/// </summary> public string ImageFilePath {get;set;}/// <summary> ///图标字节/// </summary> public virtual byte[] EmbedIcon { get; set; }/// <summary> ///图标图片对象/// </summary> public Image ImageIcon { get; set; }/// <summary> ///构造函数///为了展示的方便,在构造函数里面构造相应的数据/// </summary> publicMenuInfo()
{
this.ID =Guid.NewGuid().ToString();this.Name = "测试图片菜单";this.ImageFilePath = Path.Combine(System.Environment.CurrentDirectory, "app.ico");if (File.Exists(this.ImageFilePath))
{
this.EmbedIcon = FileUtil.FileToBytes(this.ImageFilePath);this.ImageIcon = ImageHelper.ImageFromUrl(this.ImageFilePath);
}
}

创建GridColumn的时候,我们可以利用GridVIew的扩展函数CreateColumn进行创建几个不同的列,如下代码所示。

   public partial classFrmRepositoryItemImageEdit : BaseForm
{
publicFrmRepositoryItemImageEdit()
{
InitializeComponent();

CreateGridView();
}
/// <summary> ///创建gridView1列表所需显示的列/// </summary> private voidCreateGridView()
{
//创建一个隐藏的ID列 this.gridView1.CreateColumn("ID", "ID").Visible = false;//串一个名称的列,并指定宽度 this.gridView1.CreateColumn("Name", "名称", 150);//创建一个图片路径的列,并指定它的编辑控件类型为RepositoryItemImageEdit//并为这个列实现ParseEditValue的方法,用于解析路径为具体的图片显示 this.gridView1.CreateColumn("ImageFilePath", "图片路径绑定", 100).CreateImageEdit().ParseEditValue += (s, e) =>{if (e.Value != null && e.Value is string && e.Value.ToString() != string.Empty)
{
e.Value
= Image.FromFile(string.Concat(e.Value));
e.Handled
= true;
}
};
//创建图片字节的列,用于显示图片 this.gridView1.CreateColumn("EmbedIcon", "图片字节绑定", 100);//创建图片对象的列,用于显示图片 this.gridView1.CreateColumn("ImageIcon", "图片对象绑定", 100);
}

上面代码是创建GridView所需要显示的列信息,那么我们准备好数据源绑定到列表控件上就可以了,如下代码所示。

        /// <summary>
        /// 绑定列表数据
        /// </summary>
        private void BindData()
        {
            //构造只有一个记录的集合
            List<MenuInfo> menuList = new List<MenuInfo>()
            {
                new MenuInfo()
            };

            //绑定数据源到列表控件上
            this.gridControl1.DataSource = menuList;
        } 

2、基于分页控件的图片绑定

很多时候,我们需要对数据库的数据进行分页显示,以提高显示的速度和效率,那么利用分页控件就可以获得很多这样统一的界面和高效率显示数据的好处,基于分页控件的处理本质上和上面的过程差不多,不过处理的代码需要变化一下,从而可以正常的实现图片绑定显示操作。

    /// <summary>
    ///基于分页控件的图片显示案例/// </summary>
    public partial classFrmRepositoryItemImageEdit2 : BaseForm
{
publicFrmRepositoryItemImageEdit2()
{
InitializeComponent();

CreateGridView();
}
/// <summary> ///创建gridView1列表所需显示的列/// </summary> private voidCreateGridView()
{
this.winGridViewPager1.OnPageChanged += newEventHandler(winGridViewPager1_OnPageChanged);this.winGridViewPager1.OnRefresh += newEventHandler(winGridViewPager1_OnRefresh);this.winGridViewPager1.AppendedMenu = this.contextMenuStrip1;this.winGridViewPager1.ShowLineNumber = true;this.winGridViewPager1.BestFitColumnWith = false;//是否设置为自动调整宽度,false为不设置 this.winGridViewPager1.gridView1.DataSourceChanged += newEventHandler(gridView1_DataSourceChanged);
}
/// <summary> ///绑定数据后,分配各列的宽度/// </summary> private void gridView1_DataSourceChanged(objectsender, EventArgs e)
{
//对图片路径的列,重新使用RepositoryItemPictureEdit类型//然后对该列的控件的ParseEditValue和FormatEditValue函数进行实现,从而实现路径到图片的显示 var edit = this.winGridViewPager1.gridView1.Columns.ColumnByFieldName("ImageFilePath").CreatePictureEdit();
edit.ParseEditValue
+= (s, se) =>{if (se.Value != null && se.Value.GetType() == typeof(string) && se.Value.ToString() != string.Empty)
{
if (File.Exists(string.Concat(se.Value)))
{
var picture = ImageHelper.ImageFromUrl(string.Concat(se.Value));
se.Value
=picture;
se.Handled
= true;

}
}
};
edit.FormatEditValue
+= (s, se) =>{if (File.Exists(string.Concat(se.Value)))
{
var picture = ImageHelper.ImageFromUrl(string.Concat(se.Value));
se.Value
=picture;
se.Handled
= true;
}
};
if (this.winGridViewPager1.gridView1.Columns.Count > 0 && this.winGridViewPager1.gridView1.RowCount > 0)
{
//统一设置100宽度 foreach (DevExpress.XtraGrid.Columns.GridColumn column in this.winGridViewPager1.gridView1.Columns)
{
column.Width
= 120;
}
//可特殊设置特别的宽度 GridView gridView = this.winGridViewPager1.gridView1;if (gridView != null)
{
//gridView.SetGridColumWidth("Note", 200); }
}
}

而在分页控件的数据绑定的时候,我们指定列名的中文名即可,如下代码所示

        /// <summary>
        ///绑定列表数据/// </summary>
        private voidBindData()
{
#region 添加别名解析 this.winGridViewPager1.DisplayColumns = "Name,ImageFilePath,EmbedIcon,ImageIcon";this.winGridViewPager1.AddColumnAlias("ID", "编号");this.winGridViewPager1.AddColumnAlias("Name", "名称");this.winGridViewPager1.AddColumnAlias("ImageFilePath", "图片路径绑定");this.winGridViewPager1.AddColumnAlias("EmbedIcon", "图片字节绑定");this.winGridViewPager1.AddColumnAlias("ImageIcon", "图片对象绑定");this.winGridViewPager1.gridView1.OptionsBehavior.Editable = true;this.winGridViewPager1.gridView1.OptionsBehavior.ReadOnly = false;#endregion //构造只有一个记录的集合 List<MenuInfo> menuList = new List<MenuInfo>()
{
newMenuInfo()
};
this.winGridViewPager1.DataSource =menuList;
}

以上就是基于GridControl控件上绑定图片的几种操作方式,方便我们在项目中参考使用。

最近有空下来先停下脚步,全面整理下框架相关主题的内容,包括框架模块内容的介绍,DevExpress界面开发,框架快速开发等主题的内容,一个目的是分享给读者,还有一个重要的目的也是全面整理下自己这十几年的成果和开发心得,本篇主要是针对公用类库模块做一些内容的介绍,整个类库其实涉及面包括原生.NET公用类库模块、扩展公用类库模块、DevExpress界面类库模块几个部分,这些基类能够给框架和项目开发带来几个特点:简单、高效、统一。

1、开发视频的规划

整个开发视频部分的相关主题的内容,目前规划的大概包括:框架模块内容的介绍、DevExpress界面开发、框架快速开发几个方面的内容,如下截图所示。

2、视频PPT提纲

本篇随笔主要介绍公用类库部分,在介绍每个部分内容前,我都会用心的规划的总体的内容,然后总结一下PPT的介绍提纲,希望针对内容提纲做一个视频的介绍,这部分的提纲如下所示。

3、开发视频内容

完整视频内容如下所示,欢迎大家观看并提出宝贵意见。

在软件开发过程中,为了节省开发时间,提高开发效率,统一用户处理界面,尽可能使用成熟、功能强大的分页控件,这款Winform环境下的分页控件,集成了数据分页、内容提示、数据打印、数据导出、表头中文转义等很多功能,由于集成性很好,省却很多功夫,专注客户的业务及变化即可,否则一项表头的中文转换就够呛,还不说数据的分页,由于整合性、一致性、稳定性等特点,客户使用感觉比较好。

开发视频内容

完整视频内容如下所示,欢迎大家观看并提出宝贵意见。

在很多项目里面,字典管理是必备的项目模块,而这个又是比较通用的功能,因此可以单独做成一个通用字典管理,通过集成的方式在项目中重复使用,这样可以避免代码的重复维护管理,对企业来说,也可以更好管理核心的代码,实现统一的版本更替。通用的字典管理模块,主要包括了字典类型的管理、字典数据的管理,省市区数据管理,用户参数内容管理等内容,另外为了方便,还可以加入批量添加数据的功能,以便更适合实际使用场景,加快字典数据的快速录入。本篇系列视频主要是介绍通用字典管理模块的内容。

开发视频内容

完整视频内容如下所示,欢迎大家观看并提出宝贵意见。

也可以在腾讯视频用搜索
广州爱奇迪
进行观看在线视频。