wenmo8 发布的文章

在国际化环境下,越来越多的程序需要做多语言版本,以适应各种业务需求的变化。在Winform应用程序中实现多语言也有常规的处理方式处理,不过需要针对每个语言版本,重新修改Winform界面的显示,对一些常规的辅助类,也需要引入一个统一的资源管理类来处理多语言的问题,相对比较繁琐。本篇随笔针对多语言的需求,希望尽量避免繁琐的操作,既能符合本地语种开发人员的开发习惯,又能快速实现Winform程序的多语言场景处理。

1、多语言开发的困惑和思路

在常规的多语言版本程序中,开发总是伴随着很多不愉快的事情,大概列举一些仅供参考:

1)对窗体的多语言处理时,维护多个语言版本的界面非常繁琐;

2)多语言处理的时候,以资源参照的时候,默认键值为一些英文字符串或者单词,不太符合如中文语境的开发,调整代码则需要很多工作量;

3)对于已开发好的程序,全面引入多语言的处理代码,需要大量修改;

4)对于大量中文的多语言处理,工作量望而却步;

5)对于常规Resx文件的处理觉得繁琐

6)缺乏一个统一处理多语言需求的方案

在多语言的处理上,我一直希望找出一种高效的处理方式,由于我的Winform开发框架中很多模块是现成的,希望能够使用继承处理的方式,实现最简化的处理;

同时大量中文的英文(针对英文版本)翻译也是一个头痛的事情,突然想到百度的翻译API接口可以利用,那么我们可以利用翻译接口实现开始的翻译,然后对资源进行一定的调整则可以提高效率和准确率。

对于编辑和承载多语言的信息,我一直觉得JSON格式挺好的,可以利用它序列化为字典集合,通过字典获取对应键值的多语言版本字符串也是很高效的一种方式,那么就决定用JSON来存储多语言信息了,易读好用。

对于多余的处理逻辑,尽量封装为独立的模块,可以在多个模块中进行调用处理。

2、多语言的处理实现

在思考多语言的合理处理方案过程中,参考了另一位博友的文章《
分享两种实现Winform程序的多语言支持的解决方案
》,思路有点符合我的期望,因此吸收了一些处理思想进行处理,目的就是提高开发效率。

1)多语言的信息存储和加载

首先,我们来看看多语言处理的目录和格式问题,目录大概是根据多语言的简称进行放置,如下所示。

这个目录就是会输出到debug或者Release的运行目录中,我们就是根据相对于运行目录进行资源读取即可,所有模块共用同一的多语言文件,我们可以把各个模块基础通用的多语言文件放在Basic.json文件中,也可以根据模块独立起名,主程序如TestMultiLanguage的多语言文件我则放在TestMultiLanguage.json文件中。实际上目录名称是为了区分而已,程序加载的时候,会把目录下面所有的JSON文件进行加载,读取里面的键值作为资源的字典参照。

多语言的JSON文件是标准的Json格式,只是我们只用键值的字典参考即可,不需要使用复杂的JSON对象格式,如下是basic.json文件的部分内容。

这些资源文件采用
中文-英文
的参照方式,我们以我们常规的母语开发,即使我们不做多语言,也不影响代码的正常处理,我们只需要把窗体上和代码里面的中文提取出来,然后进行多语言处理(如变为英文)即可。

由于我们使用键值字典对象的JSON内容,那么我们就可以把这些内容序列号为字典集合,如下代码我们可以通过 JSON.NET 组件把它们序列化为字典集合,这些字典集合就是我们用来做多语言的关键。

            var content =File.ReadAllText(file, Encoding.UTF8);if (!string.IsNullOrEmpty(content))
{
var dict = JsonConvert.DeserializeObject<Dictionary<string, string>>(content);foreach (string key indict.Keys)
{
//遍历集合如果语言资源键值不存在,则创建,否则更新 if (!resources.ContainsKey(key))
{
resources.Add(key, dict[key]);
}
else{
resources[key]
=dict[key];
}
}
}

加载多语言处理的时候,我们遍历相对目录下的lang/***里面的文件即可实现多语言信息的加载,如下代码所示。

        /// <summary>
        ///根据语言初始化信息。///加载对应语言的JSON信息,把翻译信息存储在全属性resources里面。/// </summary>
        /// <param name="language">默认的语言类型,如zh-Hans,en-US等</param>
        private void LoadLanguage(string language = "")
{
if (string.IsNullOrEmpty(language))
{
language
=System.Threading.Thread.CurrentThread.CurrentUICulture.Name;
}
this.resources = new Dictionary<string, string>();string dir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, string.Format("lang/{0}", language));if(Directory.Exists(dir))
{
var jsonFiles = Directory.GetFiles(dir, "*.json", SearchOption.AllDirectories);foreach (string file injsonFiles)
{
LoadFile(file);
}
}
}

我们把多语言的加载和翻译处理,放在一个独立的项目上,如我定义为框架的一个模块:WHC.Framework.Language

这样我们在各个模块中使用多语言处理过程的时候,包含这个模块就可以了。

2)多语言信息的翻译

做多语言的版本程序,翻译工作也是一个繁琐的工作,如果你是非常精通各种语言(如中文、英文、日文等等),那当然不在话下,不过我们做开发的多少也是会一些的,如英语吧,即时不能非常准确,那么也可以做到差不多,但是做这个还是累,还容易敲打错别字,那么用第三方提供的翻译API来预处理后调整,结果就简化很多了,可以极大提高效率的。

这里以我们经常使用的百度翻译来实现(用Google翻译也可以,增加接口实现即可)

百度翻译接口的使用,你先注册一个开发账户,获得相应的秘钥信息就可以使用免费的翻译接口了(
http://api.fanyi.baidu.com/api/trans/product/index
)。

有了这些准备后,就可以利用C#代码进行翻译处理了。

百度翻译的接口处理代码如下所示。

        /// <summary>
        ///百度接口翻译/// </summary>
        /// <param name="inputString">输入字符串</param>
        /// <param name="from">源内容语言</param>
        /// <param name="to">目标语言</param>
        /// <returns></returns>
        private static string BaiduTranslate(string inputString, string from = "zh", string to = "en")
{
string content = "";string appId = "你的APPID";string securityId = "你的秘钥";int salt = 0;

StringBuilder signString
= newStringBuilder();string md5Result = string.Empty;//1.拼接字符,为了生成sign signString.Append(appId);
signString.Append(inputString);
signString.Append(salt);
signString.Append(securityId);
//2.通过md5获取sign byte[] sourceMd5Byte =Encoding.UTF8.GetBytes(signString.ToString());
MD5 md5
= newMD5CryptoServiceProvider();byte[] destMd5Byte =md5.ComputeHash(sourceMd5Byte);
md5Result
= BitConverter.ToString(destMd5Byte).Replace("-", "");
md5Result
=md5Result.ToLower();try{//3.获取web翻译的json结果 WebClient client = newWebClient();string url = string.Format("http://api.fanyi.baidu.com/api/trans/vip/translate?q={0}&from=zh&to=en&appid={1}&salt={2}&sign={3}", inputString, appId, salt, md5Result);byte[] buffer =client.DownloadData(url);string result =Encoding.UTF8.GetString(buffer);var trans = JsonConvert.DeserializeObject<TranslationJson>(result);if (trans != null)
{
content
= trans.trans_result[0].dst;
content
=StringUtil.ToProperCase(content);
}
}
catch(Exception ex)
{
Debug.WriteLine(ex);
}
returncontent;
}

其中把JSON转换为类对象需要两个类,对翻译结果进行转换,如下代码所示。

    internal classTranslationJson
{
public string from { get; set; }public string to { get; set; }public List<TranslationResult> trans_result { get; set; }
}
internal classTranslationResult
{
public string src { get; set; }public string dst { get; set; }
}

这样我们在多语言处理的时候,可以对默认输入为空的键值进行翻译即可(如英文翻译)。

    //遍历集合进行翻译
    var value =dict[key];if (string.IsNullOrWhiteSpace(value))
{
//如果值为空,那么调用翻译接口处理 var newValue = TranslationHelper.Translate(key, from, to);if (!string.IsNullOrWhiteSpace(newValue))
{
dict[key]
=newValue;
}
}

然后重新更新我们的资源文件就可以了

    //不排序
    var newContent =JsonConvert.SerializeObject(dict, Formatting.Indented);

File.WriteAllText(file, newContent, Encoding.UTF8);

如果需要对键值进行排序,那么使用SortDictionary进行包装下即可

    //进行排序
    SortedDictionary<string, string> sortedDict = new SortedDictionary<string, string>(dict);var newContent = JsonConvert.SerializeObject(sortedDict, Formatting.Indented);

在多语言处理的时候,我们一般不必要一次填写完毕中英文对照的资源,我们可以先把字典键值的键写出来,值保留为空,如下文件所示。

运行程序的时候,让翻译的接口先行翻译,然后我们再对翻译的资源进行调整,适应我们程序的语境即可,翻译后的内容后如下所示。

好了,弹药都准备好了,就看我们如何使用, 下一步介绍如何使用这些资源。

3、多语言在界面中的应用

前面介绍都是为程序界面准备好对应的多语言资源内容,我们在程序启动的时候,可以通过常规的方式,设置界面的CurrentUICulture区域信息,如下代码所示。

            //界面多语言//System.Threading.Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("zh-Hans");//中文界面
            System.Threading.Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("en-US");//英文界面

然后我们在Winform程序中开发设计我们的界面内容,例如设计一个普通的界面如下所示。

这个窗体我们添加了几个按钮,并设置它的中文显示内容,它的基类默认还是保持它的DevExpress基类 XtraForm,如下所示。

    /// <summary>
    ///测试多语言的窗体界面/// </summary>
    public partial class Form1 : XtraForm

那么我们如果要自动实现多语言的处理,那么还需要在窗体的Load或者Shown事件里面实现处理,如下代码所示。

        private void Form1_Shown(objectsender, EventArgs e)
{
//窗体加载并显示后,对窗体实现多语言处理 if (!this.DesignMode)
{
LanguageHelper.InitLanguage(
this);
}
}

如果我们为每个窗体都需要添加这些代码,也是繁琐的事情,那么我们可以把这个处理逻辑,放到我们常规自定义的窗体基类里面(如BaseForm),那么我们就不需要任何额外的代码了。

所需的就是集成窗体基类即可,这也是我们一般开发都做的事情,通过继承使得我们的代码又省去了。

    /// <summary>
    ///测试多语言的窗体界面/// </summary>
    public partial class Form1 : BaseForm

那么我们真正关注的就是我们前面介绍的逻辑代码实现了

LanguageHelper.InitLanguage(this);

这个辅助类,主要就是在窗体初始化后,遍历界面的所有类型控件,对控件进行相应的多语言处理。

    /// <summary>
    ///对界面控件进行多语言的处理辅助类/// </summary>
    public classLanguageHelper
{
/// <summary> ///初始化语言/// </summary> public static voidInitLanguage(Control control)
{
//如果没有资源,那么不必遍历控件,提高速度 if (!JsonLanguage.Default.HasResource)return;//使用递归的方式对控件及其子控件进行处理 SetControlLanguage(control);foreach (Control ctrl incontrol.Controls)
{
InitLanguage(ctrl);
}
//工具栏或者菜单动态构建窗体或者控件的时候,重新对子控件进行处理 control.ControlAdded += (sender, e) =>{
InitLanguage(e.Control);
};
}

通过递归的方式,我们可以对常规的如GridControl,工具栏、NavBar导航栏、菜单、按钮等资源进行统一的多语言处理,而这里面对于我们开发应用程序界面,都不需要额外的担心,极大的提高了效率。

下面是几个常规的界面,我们来体验下英文版本的界面效果。

这些英文界面我们只需要把界面的中文提取出来放到JSON文件中,自动翻译再调整即可,然后界面继承保持BaseForm或者BaseDock这些窗体基类不变,只是调整了这些基类的加载,增加一行代码就可以顺利实现了多语言的效果了。

这样我们就把核心的工作放在提取界面中的中文资源并进行整理即可,这是核心的工作但翻译也基本不用自己从头做,窗体代码几乎不需要做其他修改就实现了我们所需要的多语言效果了,这样做极大提高了开发效率,对于我们已经开发好的模块,更是四两拨千斤了。

我在上篇随笔《
在WinForm应用程序中快速实现多语言的处理
》里面介绍了Winform开发中多语言的处理解决方案,整个多语言解决方案是以实际需求为驱动,以减少代码改动,高效处理为目的,通过基类继承的方式减少代码修改,通过引入翻译API方式减少翻译处理时间,本随笔继续深化这个多语言处理方案的介绍,是指整合在开发框架中进行无缝的使用。

1、多语言的处理步骤

1)提取多语言处理JSON文件

我们通过把程序界面或者代码里面的中文提取出来,放到JSON文件中,对不同模块可以分为不同的JSON文件,如下所示。

然后使用辅助类的接口调用实现英文内容的翻译,如下代码所示:

            //界面汉化System.Threading.Thread.CurrentThread.CurrentUICulture= new System.Globalization.CultureInfo("en-US");//英文界面//如果语言包有空白的翻译内容,可以调用下面语句(百度翻译)获得翻译内容,减少手工翻译的繁琐//翻译后将内容进行审核或调整一下即可使用//正式发布程序需要屏蔽这个调用JsonLanguage.Default.Translate();

可以把内容翻译并调整好。

这些JSON资源文件,按照程序运行的相对目录
Lang/en-US/ABC.json
目录放置即可,可以根据不同的模块或者需求分开不同的文件,程序加载多语言信息的时候,会全部进行加载。

2)继承窗体基类和使用统一的信息提示辅助类

准备好多语言文件后,那么多语言的处理还需要一些辅助的处理才能实现,由于我们已经把多语言处理的逻辑放在了窗体的基类,如BaseForm窗体基类里面,如果原来窗体就是继承了这个基类,那么多语言处理逻辑会在窗体加载后自动进行处理的了。

通过继承关系的处理,我们可以不用修改子窗体任何代码就可以自动具有多语言的处理过程了,子窗体在加载完毕后,自动遍历内部控件实现多语言的处理逻辑。

如果我们一些窗体,不方便集成这些基类,那么也可以使用增加代码的方式进行处理即可,在窗体的Load或者Shown事件里面实现处理,如下代码所示。

        private void Form1_Shown(objectsender, EventArgs e)
{
//窗体加载并显示后,对窗体实现多语言处理 if (!this.DesignMode)
{
LanguageHelper.InitLanguage(
this);
}
}

还有一种特殊的界面,就是一些信息的提示,如消息框的提示,这种默认的处理方式,如下面是一般的消息提示代码。

XtraMessageBox.Show("欢迎使用Winform开发框架", "提示信息", MessageBoxButtons.OK, MessageBoxIcon.Information);

我们在框架里面也是通过辅助类封装的方式进行提示的,因此多语言也只需要修改这个辅助类就可以了,不用修改我们窗体里面已完成的代码。

MessageDxUtil.ShowTips("欢迎使用Winform开发框架");

由于我们在框架里面统一使用消息提示处理,那么多语言的处理,在其内部实现即可,如下所示。

        /// <summary>
        ///显示一般的提示信息/// </summary>
        /// <param name="message">提示信息</param>
        /// <param name="args">字符串里面的参数内容</param>
        /// <returns></returns>
        public static DialogResult ShowTips(string message, params object[] args)
{
//对消息的内容进行多语言处理 message = JsonLanguage.Default.GetString(message);if (args != null)
{
message
= string.Format(message, args);
}
returnDevExpress.XtraEditors.XtraMessageBox.Show(message, Caption_Tips, MessageBoxButtons.OK, MessageBoxIcon.Information);
}

如果是带有一些参数占位符的多语言处理,我们一样可以实现它。

MessageDxUtil.ShowTips("用户【{0}】身份验证正确", loginName);

通过窗体基类BaseForm内部逻辑的修改,以及一些辅助类如MessageDxUtil的修改,我们悄悄的就实现了多语言的处理逻辑,原来的代码还是不用修改就支持了多语言的处理,大大节省了工作量。

当然如果是一些特殊的情况,我们还是可以通过统一的多语言处理逻辑来获得它的内容,如下代码所示。

JsonLanguage.Default.GetString(message);

3、模块化的多语言支持

除了上面介绍的一些总体逻辑,我们在Winform开发过程中,往往也是以模块化的开发方式来提高开发效率,如利用框架的公用类库、基础界面模块、分页控件模块、字典模块、权限管理系统等等,这些模块都是整个模块化过程中的一部分,当然多语言的支持也是很必要,由于我们已经通过基类和多余与模块独立的方式来简化它,但是里面的多语言资源我们也应该以模块方式提供,翻译好并集成在系统中使用即可。

上面的Basic.json是框架整个基础的界面(如分页、字典等)英文参照资源,Security.json为权限系统的界面资源,Commons.json为一些其他模块的多语言资源等等 。

以及权限系统的多语言参照资源

例如分页控件的展示界面,我们的英文版效果如下所示(以用户管理界面为例)

编辑界面效果如下所示

权限管理系统整体的界面效果如下所示,包括工具栏、菜单、选项卡,以及里面的所有控件,测试均表现OK。

当然,里面的数据源肯定还是中文的,除非我们系统的数据全部以英文语境进行录入。

在之前版本的代码生成工具Database2Sharp中,由于代码生成都是考虑Winform和Web通用的目的,因此Winform界面或者Web界面都是单独生成的,在工具中生成相应的界面后,复制到项目里面去使用即可。由于我们Winform的界面是模块化的方式独立开发,因此对某些表来说,我们可以独立开发模块,那么它的Winform窗体界面也应该是在独立的项目上,开发完毕后,我们在主体框架里面通过菜单动态配置即可加入到系统中使用的。

1、框架插件化回顾

在Winform开发框架的程序中,实现基于插件化的应用,如下所示。

通过在权限系统中的预先的配置,独立模块的窗体可以进行动态加载解析,就能实现插件模块的热插拔功能了。实际插件化框架的菜单配置界面效果如下所示。

下图是菜单管理里面的 “Winform窗体类型” 信息的具体内容。

通过动态的配置,可以实现模块的独立开发,动态整合的插件化特性,非常适合我们企业的分工开发,从而提高开发效率。

2、快速生成Winform框架的界面项目

前面我们介绍了,通过模块化的独立来发,动态整合,可以提高开发效率。

其实就是我们对大型项目的横向切割,把大型项目分为各个独立的模块,给不同的开发人员或者团队进行开发,可以极大提高效率,也减少对某个人的过度依赖,也同时可以极大提高开发效率。

在实际使用中,一些客户就提出其实模块化的Winform界面项目可以使用代码生成工具一键生成,和其他模块一样,选定数据库后生成项目一并生成,这样可以更加人性化,减少创建空白界面项目,然后引入所需DLL和文件引用等操作0所花费的时间。

如一般我们创建一个基于DevExpress的空白项目过程是这样的。

先准备创建一个基于DevExpress的模板项目

接着就是选择一个空白的DevExpress项目,这样可以在创建项目的时候,包含一些基础的DevExpress应用,减少我们后续引用DLL的工作。

然后就可以看到生成空白的窗体项目如下所示。

这样离我们开发独立的模块界面项目,还是有很多工作要做,如还需要引入额外的DevExpress所需的DLL,引入框架支持的相关DLL,另外还有一些业务逻辑层和数据访问层等类文件需要包含进去(这么做是减少文件太多的依赖,一个UIDx程序集或者ClientDx程序集就可以了)。

如果我们能够在生成其他项目工程的时候,一并生成该模块的UI项目就可以减少这部分的时间花费,要知道我们开发人员的时间也是很宝贵的,只应该花在有意义,有深度的地方。

如对于普通的Winform界面项目,我们生成后的项目结构视图如下所示。

而对于混合框架方案的项目,我们生成的界面项目结构视图如下所示。

这样就是我们最终要做的视图,一键就可以快速生成了,包含了相关的DLL应用,也包含了一些所需的文件应用,省却了很多繁琐的处理过程。

下面我们可以回顾下两种不同框架的架构分层图示。

普通Winform框架的分层架构图。

我们在Winform开发框架、混合式开发框架、Web开发框架等各种开发框架,底层都是支持多种数据库的,如MS SQLServer、Oracle、MySQL、SQLite、PostgreSQL等等,如下图所示。

混合框架的多种方式支持

混合框架的分层架构图

在一些耗时的操作过程中,在长时间运行时可能会导致用户界面 (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控件上绑定图片的几种操作方式,方便我们在项目中参考使用。