2023年2月

在Winform里面,很多控件元素都是标准的,如图标、按钮、工具栏等等,所以一般设计标准的Winform界面比较快捷,但是往往这样的界面相对单调一些,特别在界面控件比较少的情况下,我们往往需要加入一些图片、背景什么来衬托一下,看起来图文并茂一些,本文主要介绍,如何在Winfrom里面利用各种控件的特点,设计一个相对比较美观、图文并茂的Winform界面。

1、界面效果分析

在一些场景里面,如一些进销存的系统里面,我们往往把一些相关的模块处理放在一起,如进货、退货、库存调入、调出、产品、库存、盘点等这些信息,就是一个场景里面的内容,有时候可以把它们放在一起,但是如果单纯的放几个按钮,可能会显得比较单调一些,我们为了丰富界面效果,把界面设计效果如下所示。

这个界面虽说不是特别华丽,但是也是比仅仅放几个标准的按钮来的美观一些,而且按钮之间也设置了一些箭头来指示他们的流程或者从属关系,因此在一些场合,我们更倾向使用这种直观、清晰的界面表达方式,同时也增加了一定的美感。

2、界面的设计过程

在上图的界面里面,设计过程也不麻烦,就是首先确定需要在界面上绘制什么,放置那些按钮,规划好这些后,就可以着手做界面的工作了。

我们分析下上面的界面,就是Winform界面上,放置一个背景图片,然后在界面适当的位置上添加对应的图片或者按钮即可,让对应的图标处理单击事件基本上就可以了,比较简单吧,呵呵。

1)绘制背景图片

为了达到最佳的界面效果,但我们不能再Winform界面上绘画,就只有退而求其次,在绘图工具上绘制一些较好的图形作为界面背景了,如可以使用强大万能的Photoshop,也可以使用Word,或者也可以使用其他绘图工具,能抓到老鼠的就是好猫!

我这里喜欢利用一些有现成图元的绘图工具如EDraw Max等等这些来绘制背景,预先留出放置图标按钮的位置,我们绘制一个背景如下所示。

绘制好这些背景,截图出来待用,记住要图片要预留一定的位置,并且具有一定的画布大小,我们想图片按正常方式设为背景即可,这样我们在窗口放大的时候,不至于直接看完图片了,截取出来的图片,左上角预留一定的位置即可。

2)设置窗体背景本增加图片

在我们预先新建的窗体上面,设置它的BackgroundImage背景图片,如下图所示。

上面效果是增加了图片按钮的效果,开始的话,是没有图片按钮的,这些是后来增加上去空白位置的,图标按钮,一般为了好看的效果,传统的Winform可以采用PictureBox,基于DevExpress的,则可以利用LabelControl标签控件的来处理,设置它的图片即可达到很好的效果的。

LabelControl几个设置如下所示。

还有需要设置ImageAlignToText的属性设置为TopCenter,这样图标才能很好的居中显示。

上面的图片是64×64的,一般可以在网站上根据关键字找一些自己喜欢的图标,下面几个图标搜索的网站如下所示。

http://www.iconfinder.com/

http://www.iconpng.com/

http://www.iconlet.com/

http://findicons.com/

http://www.iconseeker.com/

http://www.iconarchive.com/

从上面总能找到一些合适、免费的各种文件类型的图标,我们把它放到合适的场景上就非常棒的了。

背景及图片按钮设置好后,我们就可以处理它的单击事件,可以弹出对应的窗体了,有两种弹出窗体方式,一种是模式对话框,如下代码所示。

        private void btnPurchase_Click(objectsender, EventArgs e)
{
FrmPurchase dlg
= newFrmPurchase();
dlg.ShowDialog();
}

另一种是,打开放在多文档界面里面显示,如下代码所示。

        private void btnStock_Click(objectsender, EventArgs e)
{
ChildWinManagement.LoadMdiForm(Portal.gc.MainDialog,
typeof(FrmStock));
}
private void btnProduct_Click(objectsender, EventArgs e)
{
ChildWinManagement.LoadMdiForm(Portal.gc.MainDialog,
typeof(FrmProduct));
}

最后我们可以得到整体的界面体验效果如下所示。

以上就是图形化界面的设计点滴思路,希望对大家设计界面有所帮助。

在很多业务数据处理的场合,整条数据链的数据完整性是非常重要的,因为我们在系统里面,往往需要同时更新或者写入一些数据,如果其中任何一环处理错误,都应该逐条滚回,这种原子性的确保就是通过事务来进行的,本文介绍的这个事务处理,适用于我的所有开发框架,如Winform开发框架、混合式开发框架、Web框架等,本文主要介绍基于我的会员系统的一些事务处理案例,对事务的使用进行介绍和代码讲解。

由于上面介绍的这些框架都是基于业务逻辑层BLL层之上的,如复杂一点的混合式框架,在BLL层之上还有一个WCF服务层、或者Web API的数据提供层,因此为了适应多种框架的适用性,我们建议把业务规则封装在BLL层,这样各种应用框架使用的时候,代码就不用修改很多,而且业务逻辑统一,也很方便理解。

1、框架的事务支持

上面我提到的几个框架,在底层我们主要是采用了微软的Enterprise Library的数据库访问模块,因此它能够很好抽象各种数据库的事务,以适应各种不同数据库的事务处理。使用微软的Enterprise Library模块,可以很好支持SQLSever、Oracle、Mysql、Access、SQLite等数据库。

开发框架,常见的分层模式,可以分为UI层、BLL层、DAL层、IDAL层、Entity层、公用类库层等等

框架的基类我们封装了大量的通用性处理函数,包括数据访问层、业务逻辑层的基类,所有的基类函数基本上都带有一个
DbTransaction trans = null
的定义,就是我们可以采用事务,也可以默认不采用事务,是一个可选性的事务参数。

如数据访问类的部分接口定义如下所示。

    /// <summary>
    ///数据访问层的接口/// </summary>
    public interface IBaseDAL<T> whereT : BaseEntity
{
/// <summary> ///插入指定对象到数据库中/// </summary> /// <param name="obj">指定的对象</param> /// <param name="trans">事务对象</param> /// <returns>执行成功返回True</returns> bool Insert(T obj, DbTransaction trans = null);/// <summary> ///根据指定对象的ID,从数据库中删除指定对象/// </summary> /// <param name="key">指定对象的ID</param> /// <param name="trans">事务对象</param> /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns> bool Delete(object key, DbTransaction trans = null);/// <summary> ///更新对象属性到数据库中/// </summary> /// <param name="obj">指定的对象</param> /// <param name="primaryKeyValue">主键的值</param> /// <param name="trans">事务对象</param> /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns> bool Update(T obj, object primaryKeyValue, DbTransaction trans = null);/// <summary> ///查询数据库,检查是否存在指定ID的对象/// </summary> /// <param name="key">对象的ID值</param> /// <param name="trans">事务对象</param> /// <returns>存在则返回指定的对象,否则返回Null</returns> T FindByID(object key, DbTransaction trans = null);

.....................
//其他操作 }

数据访问接口和数据访问类的实现图示如下所示。

因此在最高级的抽象基类AbstractBaseDAL的数据访问层里面,都有大量关于事务的接口可以使用,同样在BLL层也一样,里面大多数就是基于对AbstractBaseDAL的包装处理,同样预留了
DbTransaction trans = null
的参数方便我们进行事务性的处理。

同样,在BLL层的基类BaseBLL里面的接口,也同样包含了这样的事务参数的,因此在框架里面整合事务是一件非常方便的事情,因为所有接口都预留了使用事务的可能,方便我们整合各个方法。

2、框架事务接口的使用

在框架的DAL层和BLL层,我们都定义了很多公用的、带有事务参数的接口,如果我们在业务处理里面,使用事务的话,那么也是很方便的事情。如在

事务的处理,除了包含对数据库的更新修改需要使用外,如果在事务的处理中,查询数据等接口也要一并采用事务处理,否则就容易发生锁住堵塞的情况,因此,如果使用事务,那么事务参数是全程需要传入来进行数据的检索、修改等操作的。

事务的标准使用过程,一般是先创建事务对象,然后在各个接口里面使用事务处理,如下代码示例所示。

            DbTransaction trans = base.CreateTransaction();if (trans != null)
{
string sql =.............;base.SqlExecute(sql, trans);

sql
=.............;base.SqlExecute(sql, trans);try{
trans.Commit();
result
= true;
}
catch{
trans.Rollback();
throw;
}
}

如果我们定义的处理接口,只是其中事务处理的一个环节,那么我们就需要在接口定义的时候,预留有
DbTransaction trans = null
的参数,示例代码如下所示。

        /// <summary>
        ///添加积分/// </summary>
        /// <param name="id">会员ID</param>
        /// <param name="points">积分(正数为加,负数为减)</param>
        /// <returns></returns>
        public bool AddPoints(string id, decimal points, DbTransaction trans = null)
{
string sql = string.Format("Update {0} Set TotalPoints = TotalPoints + {1} Where ID='{2}'", tableName, points, id);return SqlExecute(sql, trans) > 0;
}
/// <summary> ///添加消费金额/// </summary> /// <param name="id">会员ID</param> /// <param name="amount">消费金额(正数为加,负数为减)</param> /// <returns></returns> public bool AddConsumption(string id, decimal amount, DbTransaction trans = null)
{
//消费的时候,同时修改累计消费金额和最后到店时间 string sql = string.Format("Update {0} Set TotalConsumption = TotalConsumption + {1}, LastConsumptionDate=getdate() Where ID='{2}'", tableName, amount, id);return SqlExecute(sql, trans) > 0;
}

在BLL层,我们同样可以对这些接口预留事务接口,对数据访问的DAL 层进行封装的。

        /// <summary>
        ///添加积分/// </summary>
        /// <param name="id">会员ID</param>
        /// <param name="points">积分(正数为加,负数为减)</param>
        /// <returns></returns>
        public bool AddPoints(string id, decimal points, DbTransaction trans = null)
{
IMember dal
= baseDal asIMember;returndal.AddPoints(id, points, trans);
}
/// <summary> ///添加消费金额/// </summary> /// <param name="id">会员ID</param> /// <param name="amount">消费金额(正数为加,负数为减)</param> /// <returns></returns> public bool AddConsumption(string id, decimal amount, DbTransaction trans = null)
{
IMember dal
= baseDal asIMember;returndal.AddConsumption(id, amount, trans);
}

预留这样的事务参数,对于我们在整合各个处理过程有很大的帮助,否则任何一环没有事务参数,处理就可能出现问题,这也是为什么框架的基类参数,都提供了一个事务参数的可选参数,就是这个道理。

如整合上面积分和金额的处理过程,就是会员系统里面必要的一环,而且这些数据需要连贯在一起进行修改,不能再中间任何一个环节发生数据部分丢失或者不成功的操作,因此我们必须使用事务处理过程。

例如,我在会员消费的过程处理中,其中包含了对消费商品主表、明细表的记录,还包括对库存的调整、还有会员消费、积分赠送等等细粒度的处理,因此我们在事务里面可以按照下面的方式进行处理。

                DbTransaction trans = base.CreateTransaction();if (trans != null)
{
try{bool success = BLLFactory<MemberConsumption>.Instance.Insert(info, trans);if(success)
{
//保存消费明细记录 foreach (ConsumptionDetailInfo detail indetailList)
{
BLLFactory
<ConsumptionDetail>.Instance.Insert(detail, trans);//减少对应库存 string note = "会员消费";double quantity = (-1) *detail.Quantity;
BLLFactory
<Stock>.Instance.ModifyQuantity(info.Corp_ID, info.Shop_ID, info.Creator, detail.ProductNo, quantity, note, trans);
}
//增加消费金额 BLLFactory<Member>.Instance.AddConsumption(info.Member_ID, info.Amount, trans);//添加积分(消费一元积一分) BLLFactory<Member>.Instance.AddPoints(info.Member_ID, info.Amount, trans);//减少余额 decimal subBalance = (-1) *info.Amount;
BLLFactory
<Member>.Instance.AddBalance(info.Member_ID, subBalance, trans);
}

trans.Commit();
result.Success
= true;
}
catch(Exception ex)
{
LogTextHelper.Error(ex);
trans.Rollback();
result.ErrorMessage
=ex.Message;
}
}

上面的方法就是一个完整的事务处理过程,就是在会员消费的情况下发生的,如果我们需要考虑多种应用框架的封装,如Web、Web API、Winform、WCF等方式的调用,那么我们就把它放到了业务逻辑层BLL层进行封装,如下就是BLL层的方法。

        /// <summary>
        ///保存消费记录,同时修改库存/// </summary>
        /// <param name="info">消费主记录</param>
        /// <param name="detailList">消费明细列表</param>
        /// <returns></returns>
        public CommonResult SaveConsumption(MemberConsumptionInfo info, List<ConsumptionDetailInfo> detailList)

这样我们在Web API的调用的时候,就可以不在使用这个特定的
DbTransaction trans = null
参数了,因此上面的方法体就是一个最小的操作单元了。

3、混合框架中Web API的封装和调用

在我的混合式开发框架基础上,我们服务提供可以是传统Winform、WCF,以及WebAPI的方式,框架的效果图如下所示。

对于Web API模式,我们对BLL业务逻辑层进行了封装,它的APIController的方法也就是说如下所示。

        /// <summary>
        ///保存消费记录,同时修改库存/// </summary>
        /// <param name="info">消费主记录</param>
        /// <param name="detailList">消费明细列表</param>
        /// <returns></returns>
[HttpPost]public CommonResult SaveConsumption(JObject param, string token, string signature, string timestamp, string nonce, stringappid)
{
//如果用户签名检查不通过,则抛出MyApiException异常。 base.CheckTokenAndSignatrue(token, signature, timestamp, nonce, appid);dynamic obj =param;if (obj != null)
{
MemberConsumptionInfo info
=obj.info;
List
<ConsumptionDetailInfo> detailList =obj.detailList;return BLLFactory<MemberConsumption>.Instance.SaveConsumption(info, detailList);
}
else{throw new MyApiException("传递参数错误");
}
}

这里面的方法有很多参数,第一个JObject param是一个动态对象的定义参数,具体可以参考《
Web API接口设计经验总结
》里面介绍的“动态对象的接口定义”节点了解。

由于这个处理过程是对数据进行了修改等重要的处理,因此参数需要增加签名数据,以及Token身份的标识,而且整个接口是公布在HTTPS协议的基础上,因此 接口的安全性是得到了非常好的保证。

对于在混合框架中,访问Web API的接口安全性方面,可以参考我前面的文章《
Web API应用架构在Winform混合框架中的应用(1)
》的“Web API访问的安全性考虑”节点了解。整个框架里面,最有保证、最方便的、适应最广泛应用的就是基于Web API的方式接入了,它不仅可以在桌面程序进行处理,也可以在移动端(包括APP和微信公众号等),使用Web API的接口进行数据的获取和提交。WCF方式虽然功能强大,但是相对显得笨重一些,而且数据安全性,需要服务端和客户端采用X509证书进行通讯,对移动端则是很难的一件事情。

言归正传,上面的事务处理,在它的基础上Web API接口进行了封装,我们调用Web API就不需要进行事务的参数传递了,因为它已经是一个操作的整体了,要么成功,要么全部失败滚回即可。

为了适应在混合框架的Winform里面进行调用,我们还是需要对刚才的Web API接口进行了客户端的封装,给Web API传递对应的参数(通过POST方式提交JSON参数给Web API接口),具体的代码如下所示。

        public CommonResult SaveConsumption(MemberConsumptionInfo info, List<ConsumptionDetailInfo>detailList)
{
var action = "SaveConsumption";var postData = new{
info
=info,
detailList
=detailList
}.ToJson();
string url =GetPostUrlWithToken(action);return JsonHelper<CommonResult>.ConvertJson(url, postData);
}

这个过程就是封装了对Web API的调用,并通过JSON数据返回的方式,把他们转换为对应的结果对象,这里的结果是一个通用的结果集对象CommonResult 。

这样我们在Winform的客户端里面就有了统一的调用方式了,非常方便简洁,代码如下所示。

                //获取消费明细
                List<ConsumptionDetailInfo> detailList =GetConsumptionDetail();//保存消费明细,以及在后台利用事务处理各种关系的修改
                CommonResult result = CallerFactory<IMemberConsumptionService>.Instance.SaveConsumption(info, detailList);

最后来一个基于Web API的云会员管理系统的界面作为佐证,这个界面就是使用上面的事务实现多种数据关系的处理的。

开发框架中使用事务处理的文章介绍:

1)
Winform开发框架里面使用事务操作的原理及介绍

2)
Winform开发框架之通用数据导入导出操作的事务性操作完善

3)
使用事务操作SQLite数据批量插入,提高数据批量写入速度,源码讲解

关于Web API的知识和框架使用方面的文章,可以参考下面系列:

Web API应用架构在Winform混合框架中的应用(1)

Web API应用架构在Winform混合框架中的应用(2)--自定义异常结果的处理

Web API接口设计经验总结

Web API应用架构在Winform混合框架中的应用(3)--Winfrom界面调用WebAPI的过程分解

Web API应用架构在Winform混合框架中的应用(4)--利用代码生成工具快速开发整套应用

Web API应用架构在Winform混合框架中的应用(5)--系统级别字典和公司级别字典并存的处理方式

我在之前很多文章里面,介绍过Winform主界面的开发,基本上都是标准的界面,在顶部放置工具栏,中间区域则放置多文档的内容,但是在顶部菜单比较多的时候,就需要把菜单分为几级处理,如可以在顶部菜单放置一二级菜单,这种方式在一般功能点不算太多的情况下,呈现的界面效果较为直观、也较为美观。不过随着一些系统功能的增多,这种方式可能就会显得工具栏比较拥挤,那么我们是否可以在左侧放置一个树形列表,这样通过树形列表的收缩折叠,就可以放置非常多的菜单功能了。

1、菜单的树形列表展示

一般情况下,树形列表的显示可以分为多个节点,节点可以收缩也可以展开,当然节点是有不同的图标的了。这样就可以把很多功能点整合在一个树列表里面了,树的节点也可以分为很多级别,很多层次


如果我们想按照业务的范畴来区分,也可以分为多个模块展示,类似选项卡的方式,一个模块的功能菜单列表集合在一起展示,如下所示。

上面这样的折叠展示,有利于业务范畴的区分,并且可以让树菜单菜单不会很大,是一种比较好的界面组织方式。

2、菜单的动态配置
管理

上面介绍了树形菜单的展示,以及如何组织菜单的内容,做好这些,就为我们奠定了界面菜单组织的雏形了。

那么问题来了,我们一般是需要根据系统创建很多菜单的,如果是能通过配置的方式,这样才能较好的管理这些菜单,而且可以动态给菜单指定权限,实现不同角色用户的权限控制。

那么我们就需要在系统里面引入一个菜单管理模块,实现菜单的配置管理功能,方便我们后面的动态创建菜单操作。

通过菜单的配置,我们可以指定菜单的图标,是否可见,是否展开,权限控制点,以及菜单触发点击后,处理的窗体对象等信息,有了这些基础信息,我们就很方便把菜单在树形列表里面进行合适、美观的展示了。

3、菜单动态构建的实现

前面介绍了,如何在数据库里面对菜单数据进行了存储,这样我们就可以在系统主界面里面,动态的构建属性列表进行菜单的展示操作了。

首先,我们需要在设计时刻对主界面的布局进行一定的设计,放置一些初始化的树形列表,方便查看效果。至于里面的内容,我们可以根据数据库的菜单配置,动态从数据库里面获取菜单信息,在左侧树形列表里面进行构建。

我们可以通过一个辅助类进行菜单的动态创建,如下所示。

        private voidInitToolbar()
{
TreeMenuHelper helper
= new TreeMenuHelper(this, this.nvBarMenu, this.imageList1);
helper.Init();
}

也就是辅助类,传入当前窗体,以及左侧的导航控件等参数后,我们在辅助类里面封装对应的动态构建菜单的逻辑处理。

首先我们动态创建的开始,先要清空原来控件展示的菜单内容,并重新从数据库里面获取,如下代码所示。

            //清空所有导航控件的内容
barControl.Controls.Clear();
barControl.Groups.Clear();
barControl.Items.Clear();
this.imageList.Images.Clear();//限定显示几个导航选项卡 barControl.NavigationPaneMaxVisibleGroups = 3;//约定菜单共有3级,第一级为大的类别,第二级为小模块分组,第三级为具体的菜单 List<MenuNodeInfo> menuList = BLLFactory<SysMenu>.Instance.GetTree(Portal.gc.SystemType);if (menuList.Count == 0) return;

然后我们会对菜单进行遍历,并判断是否具有对应的权限点,如果没有对应的权限,那么对应菜单的子菜单也不会进一步展示。

            //递归遍历所有的菜单,进行分级展示
            foreach (MenuNodeInfo firstInfo inmenuList)
{
//如果没有菜单的权限,则跳过 if (!Portal.gc.HasFunction(firstInfo.FunctionId)) continue;

创建菜单的时候,我们注意到整个菜单项是动态构建的,因此我们需要根据NavBarControl的控件属性,动态构建对应的选项卡NavBarGroup、展示容器NavBarGroup、树形对象TreeView、树形节点TreeNode等内容,如下代码所示。

                TreeView treeView = newTreeView();
treeView.Dock
=DockStyle.Fill;
treeView.ImageList
= this.imageList;
treeView.ItemHeight
= 30;//设置高度,显示更美观 NavBarGroupControlContainer container= newNavBarGroupControlContainer();
container.Size
= new System.Drawing.Size(213, 412);
container.Controls.Add(treeView);
barControl.Controls.Add(container);
//加载图标 this.imageList.Images.Add(LoadIcon(firstInfo.Icon));int index = this.imageList.Images.Count - 1;//最后一个序号 NavBarGroup group= newNavBarGroup();
group.Caption
=firstInfo.Name;
group.ControlContainer
=container;
group.Expanded
= true;
group.GroupClientHeight
= 410;
group.GroupStyle
=NavBarGroupStyle.ControlContainer;
group.LargeImageIndex
=index;
group.SmallImageIndex
=index;
barControl.Groups.Add(group);
//创建一级列表 TreeNode pNode = newTreeNode();
pNode.Text
=firstInfo.Name;
pNode.Tag
=firstInfo.WinformType;
pNode.ImageIndex
=index;
pNode.SelectedImageIndex
=index;
treeView.Nodes.Add(pNode);
//递归创建子列表 AddTreeItems(pNode, firstInfo.Children);

通过递归的方式,我们就很容易递归构建了所有层次的树形菜单,并进行合适的展示了。

菜单的单击事件,我们通过一个函数代码实现对它进行处理就可以了。

                //处理树形菜单的点击操作,如果TAG存在,则解析并加载对应的页面到多文档里面
                treeView.AfterSelect += (sender, e) =>{string tag = e.Node.Tag as string;if (!string.IsNullOrEmpty(tag))
{
LoadPlugInForm(tag);
}
};

这里面就是对它的AfterSelect 事件进行处理,实现我们动态加载窗体对象到多文档界面的处理了。

其中加载窗体是根据菜单配置的选项,动态构建界面出来的,具体分析代码如下所示。

        /// <summary>
        ///加载插件窗体/// </summary>
        private void LoadPlugInForm(stringtypeName)
{
try{string[] itemArray = typeName.Split(new char[] { ',', ';'});string type = itemArray[0].Trim();string filePath = itemArray[1].Trim();//必须是相对路径//判断是否配置了显示模式,默认窗体为Show非模式显示 string showDialog = (itemArray.Length > 2) ? itemArray[2].ToLower() : "";bool isShowDialog = (showDialog == "1") || (showDialog == "dialog");string dllFullPath =Path.Combine(Application.StartupPath, filePath);
Assembly tempAssembly
=System.Reflection.Assembly.LoadFrom(dllFullPath);if (tempAssembly != null)
{
Type objType
=tempAssembly.GetType(type);if (objType != null)
{
LoadMdiForm(
this.mainForm, objType, isShowDialog);
}
}
}
catch(Exception ex)
{
LogTextHelper.Error(
string.Format("加载模块【{0}】失败,请检查书写是否正确。", typeName), ex);
}
}

加载多文档的操作,就是在集合里面判断是否存在,如果没有存在就创建,否则就激活显示即可,具体处理如下所示。

        /// <summary>
        ///唯一加载某个类型的窗体,如果存在则显示,否则创建。/// </summary>
        /// <param name="mainDialog">主窗体对象</param>
        /// <param name="formType">待显示的窗体类型</param>
        /// <returns></returns>
        public Form LoadMdiForm(Form mainDialog, Type formType, boolisShowDialog)
{
Form tableForm
= null;bool bFound = false;if (!isShowDialog) //如果是模态窗口,跳过 {foreach (Form form inmainDialog.MdiChildren)
{
if (form.GetType() ==formType)
{
bFound
= true;
tableForm
=form;break;
}
}
}
//没有在多文档中找到或者是模态窗口,需要初始化属性 if (!bFound ||isShowDialog)
{
tableForm
=(Form)Activator.CreateInstance(formType);//如果窗体集成了IFunction接口(第一次创建需要设置) IFunction function = tableForm asIFunction;if (function != null)
{
//初始化权限控制信息 function.InitFunction(Portal.gc.LoginUserInfo, Portal.gc.FunctionDict);//记录程序的相关信息 function.AppInfo = newAppInfo(Portal.gc.AppUnit, Portal.gc.AppName, Portal.gc.AppWholeName, Portal.gc.SystemType);
}

}
if(isShowDialog)
{
tableForm.ShowDialog();
}
else{
tableForm.MdiParent
=mainDialog;
tableForm.Show();
}
tableForm.BringToFront();
tableForm.Activate();
returntableForm;
}

4、系统界面的总体效果

最后,为了更好理解整个动态菜单的界面效果,贴出几个做好的界面展示图,供参考学习。

1)标准界面的处理方式

2)树形列表界面的处理方式

打开多文档页面后如下所示。

在Winform界面中窗体我们一般使用多文档进行展示,也就是类似一般的选项卡的方式可以加载多个窗体界面。一般来说,我们如果打开新的窗体,给窗体传递参数是很容易的事情,但是在框架层面,一般的窗体是通过动态创建的,一般传入窗体的类型,在多文档集合里面判断,如果存在则激活,如果不存在则创建的方式,所以我们传递参数会碰到一些问题。本文即使介绍如何在这种方式下,给窗体对象传递参数,从而实现相应的数据处理功能。

不管是主体界面中,左侧包含树形列表,还是顶部包含工具栏的情况,都可能涉及打开窗体的时候,传递一些初始化参数,方便窗体的更新显示的情况,这种的处理相对直接传值的方式需要复杂一点,我们可以通过接口、事件的方式进行处理,下面我来介绍一下整个实现的方式。

1、多文档窗体的构建或者激活

在我的Winform开发框架里面,我们加载多文档窗体的时候,都是统一采用一种方式来进行构建不存在或者激活已有窗体的,代码如下所示。

        private void tool_Purchase_ItemClick(objectsender, DevExpress.XtraBars.ItemClickEventArgs e)
{
ChildWinManagement.LoadMdiForm(
this, typeof(FrmPurchase));
}
private void tool_TakeOut_ItemClick(objectsender, DevExpress.XtraBars.ItemClickEventArgs e)
{
ChildWinManagement.LoadMdiForm(
this, typeof(FrmTakeOut));
}
private void tool_StockSearch_ItemClick(objectsender, DevExpress.XtraBars.ItemClickEventArgs e)
{
ChildWinManagement.LoadMdiForm(
this, typeof(FrmStockSearch));
}

而这个LoadMdiForm的函数,主要判断多文档集合里面是否有对应的对象,没有这创建,有则激活显示即可,代码如下所示。

        /// <summary>
        ///唯一加载某个类型的窗体,如果存在则显示,否则创建。/// </summary>
        /// <param name="mainDialog">主窗体对象</param>
        /// <param name="formType">待显示的窗体类型</param>
        /// <param name="json">传递的参数内容,自定义Json格式</param>
        /// <returns></returns>
        public static Form LoadMdiForm(Form mainDialog, Type formType, stringjson)
{
bool bFound = false;
Form tableForm
= null;foreach (Form form inmainDialog.MdiChildren)
{
if (form.GetType() ==formType)
{
bFound
= true;
tableForm
=form;break;
}
}
if (!bFound)
{
tableForm
=(Form) Activator.CreateInstance(formType);
tableForm.MdiParent
=mainDialog;
tableForm.Show();
}

tableForm.BringToFront();
tableForm.Activate();
returntableForm;
}

这种方式构建的多文档界面如下所示。

2、多文档窗体传参数的实现处理

首先,为了实现这个方式,我们需要先创建一个接口,是我们窗体界面的基类,实现这个接口,然后在加载的时候,转换为对应的接口处理就可以了,具体接口代码如下所示。

    /// <summary>
    ///使用ChildWinManagement辅助类处理多文档加载的窗体,在构建或激活后,触发一个通知窗体的事件,方便传递相关参数到目标窗体。///为了更加通用的处理,传递的参数使用JSON定义格式的字符串。/// </summary>
    public interfaceILoadFormActived
{
/// <summary> ///窗体激活的事件处理/// </summary> /// <param name="json">传递的参数内容,自定义JSON格式</param> void OnLoadFormActived(stringjson);
}

这里参数为了通用,我们定义为字符串的JSON内容,方便实现更加强大的参数处理。

修改好这些,我们需要在基类窗体 BaseForm 实现这个增加的接口,如下所示。

    /// <summary>
    ///常规界面基类/// </summary>
    public partial class BaseForm : XtraForm, IFunction, ILoadFormActived

实现这个接口很容易,为了更加方便业务窗体(继承自基类窗体BaseForm),我们提供一个事件进行处理,具体代码如下所示。

    /// <summary>
    ///常规界面基类/// </summary>
    public partial classBaseForm : DevExpress.XtraEditors.XtraForm, IFunction, ILoadFormActived
{
/// <summary> ///定义一个窗体激活后的处理委托类型/// </summary> /// <param name="json"></param> public delegate void FormActiveHandler(stringjson);/// <summary> ///使用ChildWinManagement辅助类处理多文档加载的窗体,在构建或激活后,触发一个通知窗体的事件,方便传递相关参数到目标窗体。///为了更加通用的处理,传递的参数使用JSON定义格式的字符串。/// </summary> public event FormActiveHandler LoadFormActived;

同时,我们实现接口,就是直接调用事件就可以了,具体代码如下所示。

        /// <summary>
        ///窗体激活的事件处理/// </summary>
        /// <param name="json">传递的参数内容,自定义JSON格式</param>
        public virtual void OnLoadFormActived(stringjson)
{
//默认什么也没做//如果需要处理传参数,则在这里处理参数Json即可 if (LoadFormActived != null)
{
LoadFormActived(json);
}
}

这样我们就完成了基类窗体的处理了,前面我们介绍了动态构建加载窗体的时候,是使用了LoadMdiForm的函数,既然我们的接口实现了上面的ILoadFormActived接口,那么我们动态创建或者激活窗体的时候,那么就使用这个接口进行处理一下,以便实现对应事件的处理操作了。因此我们的窗体加载函数修改代码,如下所示。

        /// <summary>
        ///唯一加载某个类型的窗体,如果存在则显示,否则创建。/// </summary>
        /// <param name="mainDialog">主窗体对象</param>
        /// <param name="formType">待显示的窗体类型</param>
        /// <param name="json">传递的参数内容,自定义Json格式</param>
        /// <returns></returns>
        public static Form LoadMdiForm(Form mainDialog, Type formType, stringjson)
{
bool bFound = false;
Form tableForm
= null;foreach (Form form inmainDialog.MdiChildren)
{
if (form.GetType() ==formType)
{
bFound
= true;
tableForm
=form;break;
}
}
if (!bFound)
{
tableForm
=(Form) Activator.CreateInstance(formType);
tableForm.MdiParent
=mainDialog;
tableForm.Show();
}
//窗体激活的时候,传递对应的参数信息 ILoadFormActived formActived = tableForm as ILoadFormActived;
if (formActived != null
)
{
formActived.OnLoadFormActived(json);
}


tableForm.BringToFront();
tableForm.Activate();
returntableForm;
}

还记得我们前面打开一个多文档窗体的代码,就是利用这个接口进行创建或者激活指定类型的窗体的,如下所示。

ChildWinManagement.LoadMdiForm(this, typeof(FrmItemDetail));

那么我们增加了新的函数参数Json后,我们如果需要传递一个指定的参数给对应的窗体,那么就修改下调用即可。例如下面,为了测试,我传入一个动态构建的类信息,然后转换为Json字符串信息给接收窗体,并进行加载窗体。

            //使用自定义参数调用
            var obj = new { ItemNo = "123456789", ItemName = "测试名称"};var param =JsonConvert.SerializeObject(obj, Formatting.Indented);

ChildWinManagement.LoadMdiForm(
this, typeof(FrmItemDetail), param);

前面我们介绍了基类窗体,已经实现定义了一个事件,并对这些通知的接口进行处理,具体如下所示。

那么我们前面加载的
FrmItemDetail
需要做哪些工作呢,就是实现对事件的处理即可,如下所示。

这样我们就能够通过实现对应的事件,把整个通知事件的处理处理完毕了,我们来看看最终的界面效果,如下所示,接收到的窗体事件后,会弹出一个提示对话框在右下角了。

当然实际上我们可以做的更多,如可以传递一些具体的信息,让它在界面上进行显示。

如其中一个客户基于我的Winform开发框架基础上进行的参数传递案例界面如下所示。

本文只是对其中在框架层面对不同多文档窗体的传值进行的一个研究探索和实现,其中的理念是基于常用的接口和事件驱动的方式进行处理,以期达到方便、高效的目的,如果您有更好的建议,也希望多多交流。

在我们做系统开发的时候,都会面临一个安装包制作的问题,如何把我们做好的系统,通过安装包工具整合成一个安装包给客户进行安装。安装包的优势就是一步步安装就可以了,不用复制一大堆文件给客户,还怕缺少那个文件导致系统运行出错。本文主要介绍基于DevExpress的Winform程序安装包的制作内容,包括VS2010和VS2013/V2105的不同处理方式。

我们知道,VS2010是最后一个可以使用微软安装包制作工具的VS版本了,所以后面VS2012/2013/2015版本都需要使用第三方安装包制作工具,微软推荐使用Install Shield打包工具,本文以这两个环境进行介绍安装包的制作出来。

1、VS2010使用微软安装包制作工具

在该版本及之前的VS 开发工具,都是使用微软自带的安装包工具进行打包,方便快捷,虽然也听说过 Install Shield等大名鼎鼎的安装包制作,但是集成的东西,总是感觉方便一些,而且也能够应付大多数的安装包制作情景,不知道为什么微软把它从后续的版本中移除。

不过庆幸VS2010版本还是继续能够使用其内置的安装包制作工具,本小节介绍的就是基于这个内置安装包制作工具做我们的DevExpressWinform程序。

1)准备好待发布的安装文件

做安装包的时候,一般可以使用项目引用的方式来构建(如活动项目输出的方式),不过我们可能需要对生成的DLL进行一下混淆等特殊处理,所以一般我们是创建一个目录来放置需要发布的文件的。如下所示,我在Bin目录下创建一个DeployedMis的目录,用来存储待发布的文件的。

然后我们在Debug或者目录里面,把对应的exe或者DLL文件进行混淆操作,我们一般可以选.NET Reactor混淆工具进行处理一下,如下所示。

然后我们把生成的Debug目录的文件复制一份到DeployedMis目录,并把混淆生成的文件替换原有的文件就可以了。最后顺带捎上汉化文件(如zh-Hans目录里面的文件)、RDLC报表文件、Excel导入模板和其他资源文件,如下图所示。

2)制作VS安装包

目前我们就准备好发布的文件了,接下来的就是要启动安装包的制作方面的事情了,先在VS解决方案里面创建一个VS安装包工程项目,如下所示。

然后设置好安装包的一些基础信息,如公司信息、产品名称等关键信息,也就是在安装包的项目里面打开属性,得到界面如下图所示。

然后我们在文件系统视图里面,创建相关的目录,并加入相关的文件,注意【应用程序文件夹】目录下的文件就是我们安装到客户目录的文件内容,在这里加入一些主程序exe,然后会自动引入一些DLL的文件的,所以最终得到下面的工程界面截图。

我们在exe的主程序上面右键,创建一个快捷方式,设置好图标后,复制到用户桌面就会成为一个快捷方式的连接了。

如果我们需要为程序添加一个卸载快捷方式,那么我们在【应用程序文件夹】里面加入C:\Windows\System32\msiexec.exe的系统文件,然后创建一个它的快捷方式,复制到对应的目录上,如下界面所示。

卸载的快捷键还需要设置一些安装包的产品ID才能进行卸载处理,如下所示。

其中属性需要设置Arguments参数为:-x {CB83AA8B-4504-48FF-BF3B-E269CC3244EA},其中-x后面的参数就是我们安装项目的ProductCode内容。

弄好这些,我们编译整个安装包就可以生成一个符合实际生产需要的安装包了,安装包同时也具有程序的卸载功能快捷键了,在Win10系统中的菜单如下图所示。

有时候,为了方便检查是否主程序的自动引用少了某些文件,我们还可以检查【检测到的依赖项】列表,查核是否少了某些文件,如果都符合,那么就肯定没问题了。

如上图所示,我们看到添加主程序后的自动检测的引用文件,就包含了DevExpress的相关DLL了,这样我们做成安装包后,客户安装这个安装包就可以使用DevExpress的界面内容了,而不需要在客户端完整的安装一套DevExpress组件了。

2、VS2013及以上,使用InstallShield安装包制作工具

在VS2013上,我们就需要使用InstallShield进行安装包制作了,我曾经在之前的博客里介绍过利用InstallShield制作安装包的操作《
基于InstallShield2013LimitedEdition的安装包制作
》,在使用VS2013创建安装包之前,我们需要安装一个InstallShield的版本,其中LimitedEdition是一个可以申请免费账号使用的版本,当然专业版InstallShield是收费,而且费用也不低的了。使用LimitedEdition,我们也可以创建一般的安装包,本文主要介绍基于LimitedEdition版本的安装包制作。

安装完毕LimitedEdition版本后,我们可以在VS的新建项目里面,有一个安装包的创建工程模板了。

在做基于DevExpress的Winfrom系统InstallShield安装包,我们一样要做上面的事情,就是需要准备一个专门的目录用来放置编译并混淆好的DLL,这样我们在InstallShield LimitedEdition加入主程序的时候,会自动引入所需的DLL以及方便添加所需的资源文件了。

InstallShield工具提供了辅助的各个步骤的操作,基本上我们按照要求处理就可以实现了,具体操作指引界面如下所示。

例如,我在云会员系统的客户端安装包,就是使用这个InstallShield工具进行打包的,设置主安装项目的信息如下所示。

在安装包的制作过程中,最为关键的就是所需文件的添加处理了,如果添加正确,就能正常进行编译、安装操作,否则就容易编译过不了。

我们在【Application Files】里面,添加所需的文件,主要添加主程序以及所需的一些文件,相关的引用DLL可以通过依赖进入。操作如下所示。

如果我们单击左边【Files and Folders】,我们就可以更加详细的操作整个安装包的文件和目录内容了。

如可以查看主程序文件的依赖文件操作。

单击上图右键菜单的【Dependencies from scan at build...】的时候,我们可以看到主程序集带入的相关DLL引用,里面有很多DevExpress相关的DLL,我们可以通过检查这个列表,确认是否少了一些特殊的DLL引用,如果少了,我们再添加对应的文件到安装包目录里面就可以了。

接着可以在其他步骤增加快捷方式,如增加启动程序、卸载程序的快捷方式,如下所示。

我们也可以通过【Shortcuts】功能进入更加直观的界面显示,如下所示。

以及图标修改等操作,还有就是可以修改修改默认安装界面,最后可以得到安装包的界面如下所示。

以上就是两种不同方式的安装包制作的过程,希望能够帮助您在使用并发布DevExpress程序的时候有所帮助,也就是如果在VS2010上开发,我们可以使用微软内置的安装包制作工具,如果是VS2012、VS2013、VS2015等版本上,我们就可以使用InstallShield工具进行安装包制作了。两者制作的总体思路和配置信息是差不多的,不过使用界面和方式有所差异,习惯就好。