一. 前言
自己很少做WinForm方面的开发,最近公司的一个大型项目中,客户要求Client使用WinForm,Server使用java。以前对WinForm的认识并不深入,在这次开发过程中,陆续接触到了一些WinForm方面的技术。其中,今天要跟大家讨论的就是关于WinForm窗体的创建以及传值方面的东西。
二. 概述
以前认为WinForm的程序一般都是编译为.exe,程序中的窗体直接new出来,然后根据要求,进行show或者showDialog。但是,在公司目前的这个项目中,所有的模块都编译为.dll(不管是不是窗体),然后通过反射进行创建以及show。不清楚大家在自己的项目中是否这样使用。在此,分享出来与大家一起讨论。如果有更好的实现思路,希望能在回复中进行分享。谢谢!
三. 关键点的实现
1. 原理
(1)首先,我们需要一个配置文件,将程序中使用到的.dll文件名,窗体名等写入该配置文件。这个配置文件大家可以根据需要使用不同的类型,例如ini,xml,txt等等,在项目中,我们使用的是 ini,大体结构如
############################### # 文件名<例如:FuncConfig.ini> ############################### [FUNCINFO] # 功能编号=.dll文件名,窗体编号,功能名,功能所属模块编号,权限…… # # …… # |
例如: ############################### # 文件名<例如:FuncConfig.ini> ############################### [FUNCINFO] # CMM0100L01=CMM0100,CMM0100F01,人员数据检索,报表管理,00 # |
当然,这个文件的格式可以根据程序的需要来更改。这里只是给出一个示例.注意:给出的窗体应该在指定的.dll文件中。
(2) 程序使用给出的窗体编号去加载对应的.dll,然后在该.dll中将窗体类加载并show出来。
2.代码实现
(1)代码的流程如下图所示:
① 实现此功能的关键代码在FuncTransitCtrl类中,该类中,有两个方法,分别为ShowDialog和Show,分别对应两种show出窗体的方式。我们首先来看ShowDialog里面的代码。
- 首先,该方法接收两个参数,分别为窗体ID和需要传递给该窗体的参数集合,我们来看下方法定义
////// 以ShowDialog的方式显示指定的窗体 /// /// 功能编号 /// 要发送的数据 ///被show出的窗体 public Form ShowDialog(String funcID, SendData sendData)
SendData是一个我们自己定义数据结构,专门用于存储要发送给窗体的参数。等会儿,我们将会看到它的代码。现在,我们只需要明白它是做什么的就可以了
- 接着,我们来讲解方法里面的每段代码。
对窗体编号进行非空判断。
Form form = null;//返回的窗体 if (String.IsNullOrEmpty(funcID)) { gLogger.ErrorLog("FormTransitCtrl", "没有设定窗体ID。", null); MessageBox.Show("指定的功能界面启动失败","错误",MessageBoxButtons.OK,MessageBoxIcon.Hand); return form; }
gLogger是用于写日志的一个类型。相信大家在项目里都有自己一套专门处理日志的方法,在这里就不多说了。
获取与该功能有关的信息。在这里,我们的程序就会去读取配置文件,把跟该窗体编号有关的信息全部读取出来,并保存到一个类中。我们分几步来看代码,首先,定义个用于保存功能相关 信息的类型:
////// 保存功能信息的类型 /// public class EihoFuncInfo { private string _DllFileName; private string _EihoFuncID; private string _FormID; private string _WindowTitle; public string DllFileName { get; set; } public string EihoFuncID { get; set; } public string FormID { get; set; } public string WindowTitle { get; set; } }
接着,在FuncTransitCtrl定义一个静态方法,用于读取配置文件,并将获取的信息赋值给EihoFuncInfo的各字段。首先我们来看看如何读取配置文件中的信息(以我提供的配置文件格式为准。)这段代码大家可以简略看过,因为大家的配置文件格式不一定一致:
////// 读取ini文件中的信息 /// /// ini文件名 /// 读取的段落 /// 读取的key /// ini文件路径 ///读取到的信息 public static String GetIni(String strFileName, String strSection, String strKey, String strPath = "") { String str2 = strPath; //如果没有指明路径,则使用FuncTransitCtrl类中默认的ini路径 if (String.IsNullOrEmpty(strPath)) { str2 = FuncTransitCtrl.SystemPath + @"ini\"; } String tmpPath = str2 + strFileName; if (!File.Exists(tmpPath)) { //写日志 throw new FileNotFoundException(); } //读取key对应的value String lpReturnedString = GetPrivateIniProfileString(strSection, strKey, tmpPath); return lpReturnedString; }
GetIni是主要的方法,它位于ConfigUtil类中。在该方法中,调用了一个名为GetPrivateIniProfileString的方法,这个方法会去到配置文件中去匹配段落,找到匹配的段落后,在查找该段落下的key,然后返回对应key的value值,该类型的完整代码如下:
using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.IO;namespace Demo{ ////// 读取各种配置文件的类型 /// public class ConfigUtil { //段落开始和结束的括号 private const String SECTION_START_STR = "["; private const String SECTION_END_STR = "]"; //注释符号 private const String COMMENT_STR = ";"; private const String COMMENT_STR2 = "#"; //区分key与value的等号 private const String KEY_AND_VALUE_SEPARATOR = "="; ////// 读取ini文件中的信息 /// /// ini文件名 /// 读取的段落 /// 读取的key /// ini文件路径 ///读取到的信息 public static String GetIni(String strFileName, String strSection, String strKey, String strPath = "") { String str2 = strPath; //如果没有指明路径,则使用FuncTransitCtrl类中默认的ini路径 if (String.IsNullOrEmpty(strPath)) { str2 = FuncTransitCtrl.SystemPath + @"ini\"; } String tmpPath = str2 + strFileName; if (!File.Exists(tmpPath)) { //写日志 throw new FileNotFoundException(); } //读取key对应的value String lpReturnedString = GetPrivateIniProfileString(strSection, strKey, tmpPath); return lpReturnedString; } ////// 读取ini配置文件中指定key的值 /// /// 要读取的段落 /// 读取的key /// ini文件路径 ///key对应的value private static String GetPrivateIniProfileString(String strSection, String strKey, String strPath) { FileStream fs = new FileStream(strPath, FileMode.Open, FileAccess.Read); StreamReader sr = new StreamReader(fs, Encoding.UTF8); //是否查找key bool isSearchKey = false; //值 String strValue = String.Empty; try { String strLine = String.Empty; while ((strLine = sr.ReadLine()) != null) { String strGetSection = GetSection(strLine); //如果是一个段落 if (!String.IsNullOrEmpty(strGetSection)) { if (isSearchKey == true) { return strValue; } //如果找到的段落和参数不一致,则继续循环查找 if (strGetSection != strSection) { continue; } //找到了一致的,修改该变量,准备查找该段落下的key与value isSearchKey = true; } //不是一个段落,则查找key else { if (!isSearchKey) { continue; } //该行是否为一个注释 if (IsComment(strLine)) { continue; } //获取区分key,value的符号位置 int iPos = GetKeyAndValuePos(strLine); if (iPos < 0) { continue; } //获取key String strGetKey = strLine.Substring(0, iPos).Trim(); //如果与参数的不一致,继续循环查找 if (strGetKey != strKey) { continue; } //取得value strValue = strLine.Substring(iPos + 1).Trim(); break; } } } catch (IOException ex) { //写日志 throw new Exception(ex.Message, ex); } finally { sr.Close(); fs.Close(); } return strValue; } ////// 获取字符串中,[]之间的内容 /// /// ///[]之间的内容 private static String GetSection(String strLine) { String strTrimLine = strLine.Trim(); //如果没有段落开始的括号,则不是一个段落,不进行处理 if (!strTrimLine.StartsWith(SECTION_START_STR)) { return String.Empty; } int iPos = strTrimLine.IndexOf(SECTION_END_STR);//获得段落结束括号的位置 return strTrimLine.Substring(1, iPos - 1);//返回[]之间的字符串 } ////// 判断该行是否为一个注释 /// /// ///private static bool IsComment(String strLine) { String strTrimLine = strLine.Trim(); return (strTrimLine.StartsWith(COMMENT_STR) || strTrimLine.StartsWith(COMMENT_STR2)); } /// /// 获取区分key与value的符号的位置 /// /// 一个包含key=value的字符串 ///区分符号的位置 private static int GetKeyAndValuePos(String strLine) { return strLine.IndexOf(KEY_AND_VALUE_SEPARATOR); } }}
得到了配置文件中的信息后,我们就可以提取里面的信息了。代码如下:
////// 获取指定编号的功能的所有信息 /// /// 功能编号 ///功能信息类型 public static EihoFuncInfo GetEihoFuncInfo(String funcID) { EihoFuncInfo funcInfo = null; if (!String.IsNullOrEmpty(funcID)) { try { //获取配置的value信息 String strFuncInfo = ConfigUtil.GetIni("FuncConfig.ini", "FUNCINFO", funcID,""); funcInfo = new EihoFuncInfo(); //分别提取各项信息 funcInfo.EihoFuncID = funcID; funcInfo.DllFileName = strFuncInfo.Split(',')[0]; funcInfo.FormID = strFuncInfo.Split(',')[1]; funcInfo.WindowTitle = strFuncInfo.Split(',')[2]; } catch (Exception ex) { //写日志的代码 throw ex; } } return funcInfo; }
现在我们获得了必须的信息(窗体编号,所在程序集名称等),那么,接下来,我们就可以使用反射进行处理了。
//获取功能相关的信息 EihoFuncInfo funcInfo = GetEihoFuncInfo(funcID); if (funcInfo == null) { gLogger.ErrorLog("FormTransitCtrl", "没有取得与该功能相关的信息。", null); MessageBox.Show("指定的功能界面启动失败", "错误", MessageBoxButtons.OK, MessageBoxIcon.Hand); return form; } Assembly assembly = null; try { assembly = Assembly.LoadFrom(funcInfo.DllFileName + ".dll"); } catch (Exception ex) { gLogger.ErrorLog("FormTransitCtrl", "DLL加载失败,DLL名:" + funcInfo.DllFileName, null); MessageBox.Show("指定的功能界面启动失败", "错误", MessageBoxButtons.OK, MessageBoxIcon.Hand); return form; }
然后遍历该程序集里面的类型,找到与我们传入的FormID相匹配的,就将该窗体创建出来,代码如下:
bool flag = false; //获得窗体ID String formID = "fm" + funcInfo.FormID; //遍历程序集中的所有类型 foreach (Type type in assembly.GetTypes()) { //找到与窗体匹配的 if (type.Name.ToUpper() == formID.ToUpper()) { flag = true; try { //调用构造函数创建 form = (Form)type.GetConstructor(Type.EmptyTypes).Invoke(null); form.Text = funcInfo.WindowTitle; SetFuncID(form, funcID); } catch(Exception ex) { gLogger.ErrorLog("FormTransitCtrl", "窗体创建失败。DLL名:" + funcInfo.DllFileName, null); MessageBox.Show("指定的功能界面启动失败", "错误", MessageBoxButtons.OK, MessageBoxIcon.Hand); return form; } //处理需要传递的参数 if ((sendData != null) && (sendData.GetSendData() != null)) { IDictionaryEnumerator enumerator = ((Dictionary)sendData.GetSendData()).GetEnumerator(); while (enumerator.MoveNext()) { KeyValuePair current = (KeyValuePair )enumerator.Current; String key = current.Key.ToString(); String value = current.Value.ToString(); ReceiveData.AddParame(key, value, form); } } _form = form; _form.ShowDialog(); form = _form; } } //没有找到匹配的窗体类型 if (!flag) { gLogger.ErrorLog("FormTransitCtrl", "没有找到匹配的窗体类型。DLL名:" + funcInfo.DllFileName, null); MessageBox.Show("指定的功能界面启动失败", "错误", MessageBoxButtons.OK, MessageBoxIcon.Hand); } return form;
至此,我们的主体代码应该是完成了,FuncTransitCtrl的完整代码如下:
using System;using System.Collections;using System.Collections.Generic;using System.Linq;using System.Text;using System.Windows.Forms;using System.Reflection;namespace Demo{ ////// 对窗体进行控制的类型 /// public class FuncTransitCtrl { ////// 配置文件路径 /// public static String _systemPath; private static Dictionary
这是我们加载并show出窗体的代码,那么,传递的参数是怎么处理的呢?这里牵涉到两个类,分别为SendData和ReceiveData,看名字大家就知道是做什么的啦!首先来看看SendData的代码,如下:
using System;using System.Collections.Generic;using System.Linq;using System.Text;namespace Demo{ public class SendData { private Dictionary_datas; public SendData() { _datas = new Dictionary (); } public Object GetSendData() { return _datas; } public void AddData(String key, String value) { if (_datas.ContainsKey(key)) { _datas.Remove(key); } _datas.Add(key, value); } public void RemoveData(String key) { _datas.Remove(key); } }}
接下来,我们来看看ReceiveData类的代码。它与SendData有些不一样,虽然都是Key/Value集合,但是有那么多窗体,程序怎么知道哪些参数属于哪些窗体呢。所以,它内部使用一个嵌套的Key/Value集合来保存参数。key是某窗体的句柄值,而value则是属于该窗体的又一个Key/Value集合,我们来看代码,如下:
using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Windows.Forms;namespace Demo{ ////// 用于保存窗体之间接收参数的类型 /// public class ReceiveData { //用于保存接收参数的集合 private static Dictionary> _parameTable; static ReceiveData() { _parameTable = new Dictionary >(); } /// /// 添加参数到集合 /// /// 参数key /// 参数value /// 参数所属窗体 public static void AddParame(String key, String value, Form oForm) { IntPtr handle = oForm.Handle; Dictionaryparames = null; if (_parameTable.ContainsKey((long)handle)) { parames = _parameTable[(long)handle]; } else { parames = new Dictionary (); _parameTable.Add((long)handle, parames); } if (parames.ContainsKey(key)) { parames.Remove(key); } parames.Add(key, value); } /// /// 获取指定key的value /// /// 参数key /// 参数所属窗体 ///参数value public static String GetParame(String key, Form oForm) { IntPtr handle = oForm.Handle; if (_parameTable.ContainsKey((long)handle)) { Dictionaryparames = _parameTable[(long)handle]; if (parames.ContainsKey(key)) { return parames[key]; } } return ""; } }}
代码非常简单,这里就不作过多的解释了。然后我们在再次回到FuncTransitCtrl类的ShowDialog方法中,在反射创建出窗体和show出窗体之间,加入传参数的代码,如下:
//处理需要传递的参数 if ((sendData != null) && (sendData.GetSendData() != null)) { IDictionaryEnumerator enumerator = ((Dictionary)sendData.GetSendData()).GetEnumerator(); while (enumerator.MoveNext()) { KeyValuePair current = (KeyValuePair )enumerator.Current; String key = current.Key.ToString(); String value = current.Value.ToString(); ReceiveData.AddParame(key, value, form); } }
至此,主体代码就完成了。接下来是程序的配置。
(1) 首先,整个项目应该有个入口的.exe,例如入口为A.exe,另外有B.dll,C.dll等等,在这些.dll里面有很多窗体。在任何一个.dll或者.exe里面,我们都可以使用ShowDiglog来反射调用指定的窗体。
(2) 其次,配置文件的路径。默认情况下,我是使用Environment.GetEnvironmentVariable(“USERPROFILE”)来获取C:\Users\[当前用户]这个目录,将我们的配置文件放在这个目录下。回忆一下FuncTransitCtrl,里面有个SystemPath的属性,用于获取和设置配置文件路径,代码如下:
////// 获取或者设置配置文件路径 /// public static String SystemPath { get { if (String.IsNullOrEmpty(_systemPath)) { return (Environment.GetEnvironmentVariable("USERPROFILE") + @"\AppData\Local\EMM\"); } return _systemPath; } set { _systemPath = value; } }
最后,完整的代码下载在。在使用之前仍然再在这里啰嗦几句。
(1) FuncConfig.ini配置文件放到C:\Users\ja\AppData\Local\EMM\ini目录下,当让,你也可以修改程序的SystemPath,指向你自己的目录
(2) Demo,CMM0100,MainDemo编译后生成的程序集统一放在了bin目录下,而不是他们各自的bin目录。
(3) 注意窗体和程序集的命名应和配置文件中的保持一致
(4) 最后,示例中一些无关主体功能实现的代码(例如日志之类的),给出的是空实现。
结语
不知道大家的项目中是否也是这样处理,我想,使用这种方式加载窗体,好处是显而易见的。不同的模块由不同的人开发,编译打包为dll,不同的.dll之间的窗体调用通过反射封装。整个项目走一个.exe入口启动。
好了,这一集中,分享的是ShowDialog的方式,下一集中,跟大家分享Show的方式。谢谢!