wenmo8 发布的文章

在Winform开发中,我们往往除了常规的单表信息录入外,有时候设计到多个主从表的数据显示、编辑等界面,单表的信息一般就是控件和对象实体一一对应,然后调用API保存即可,主从表就需要另外特殊处理,本随笔介绍如何快速实现主从表编辑界面的处理,结合GridControl控件的GridView控件对象,实现数据在列表中的实时编辑,非常方便。

1、主从表的界面设计及展示

主从表一般涉及两个以上的表,一个是主表,其他的是从表的,在实际情况下,一般包含两个表较多,我们这里以两个表的主从表关系进行分析处理。

例如我们建立两个报销申请单表关系如下所示。

对于报销的主从表信息,我们可以在列表中进行展示,如下界面所示,分为两部分:一部分是主表信息,一部分是从表信息,单击主表信息后,显示对应从表的列表信息。

那么我们新增一条主表记录的时候,那么可以弹出一个新的界面进行数据的维护处理,方便我们录入主从表的信息,界面如下所示。

上面界面包括了主表信息,以及从表的信息(在GridView中实时录入)两部分,这样填写后统一进行提交处理。

2、主从表编辑界面的处理

这里主要介绍一下主从表的编辑界面处理,也就是上面这个界面的实现处理。

其中初始化GridView的代码如下所示。

        /// <summary>
        ///初始化明细表的GridView数据显示/// </summary>
        private voidInitDetailGrid()
{
//初始清空列 this.gridView1.Columns.Clear();//设置部分列隐藏 this.gridView1.CreateColumn("ID", "编号").Visible = false;this.gridView1.CreateColumn("Header_ID", "主表编号").Visible = false;this.gridView1.CreateColumn("Apply_ID", "申请单编号").Visible = false;//添加下拉列表列,并绑定数据源 this.gridView1.CreateColumn("FeeType", "费用类型", 100).CreateComboBox().BindDictItems("费用类型");//创建日期列并指定格式 var OccurTime = this.gridView1.CreateColumn("OccurTime", "发生时间", 120).CreateDateEdit();
OccurTime.EditMask
= "yyyy-MM-dd HH:mm";
OccurTime.DisplayFormat.FormatString
= "yyyy-MM-dd HH:mm";//创建数值列 this.gridView1.CreateColumn("FeeAmount", "费用金额").CreateSpinEdit();//创建备注列 this.gridView1.CreateColumn("FeeDescription", "费用说明", 200).CreateMemoEdit();//初始化GridView,可以新增列 this.gridView1.InitGridView(GridType.NewItem, false, EditorShowMode.MouseDownFocused, "");//转义列内容显示 this.gridView1.CustomColumnDisplayText += newCustomColumnDisplayTextEventHandler(gridView1_CustomColumnDisplayText);//处理单元格的样式 this.gridView1.RowCellStyle += newRowCellStyleEventHandler(gridView1_RowCellStyle);//不允许头部排序 this.gridView1.OptionsCustomization.AllowSort = false;//绘制序号 this.gridView1.CustomDrawRowIndicator += (s, e) =>{if (e.Info.IsRowIndicator && e.RowHandle >= 0)
{
e.Info.DisplayText
= (e.RowHandle + 1).ToString();
}
};
//对输入单元格进行非空校验 this.gridView1.ValidateRow += delegate(objectsender, ValidateRowEventArgs e)
{
var result = gridControl1.ValidateRowNull(e, new string[]
{
"FeeType"});
};
//新增行的内容初始化 this.gridView1.InitNewRow += (s, e) =>{
gridView1.SetRowCellValue(e.RowHandle,
"ID", Guid.NewGuid().ToString());
gridView1.SetRowCellValue(e.RowHandle,
"Header_ID", tempInfo.ID);
gridView1.SetRowCellValue(e.RowHandle,
"Apply_ID", tempInfo.Apply_ID);
gridView1.SetRowCellValue(e.RowHandle,
"OccurTime", DateTime.Now);
};
}
void gridView1_RowCellStyle(objectsender, DevExpress.XtraGrid.Views.Grid.RowCellStyleEventArgs e)
{
GridView gridView
= this.gridView1;if (e.Column.FieldName == "FeeAmount")
{
e.Appearance.BackColor
=Color.Green;
e.Appearance.BackColor2
=Color.LightCyan;
}
}
void gridView1_CustomColumnDisplayText(objectsender, DevExpress.XtraGrid.Views.Base.CustomColumnDisplayTextEventArgs e)
{
string columnName =e.Column.FieldName;if (e.Column.ColumnType == typeof(DateTime))
{
if (e.Value != null)
{
if (e.Value == DBNull.Value || Convert.ToDateTime(e.Value) <= Convert.ToDateTime("1900-1-1"))
{
e.DisplayText
= "";
}
else{
e.DisplayText
= Convert.ToDateTime(e.Value).ToString("yyyy-MM-dd HH:mm");//yyyy-MM-dd }
}
}
}

上面代码都有详细的备注,主要就是我们根据数据库表的关系,创建对应显示的字段即可,其中有需要隐藏的那么就不要显示(方便获取对应的值)

            //设置部分列隐藏
            this.gridView1.CreateColumn("ID", "编号").Visible = false;this.gridView1.CreateColumn("Header_ID", "主表编号").Visible = false;this.gridView1.CreateColumn("Apply_ID", "申请单编号").Visible = false;

如果需要绑定下拉列表类似的字段,那么创建对应的数据类型,然后调用绑定函数绑定即可,如下面代码

            //添加下拉列表列,并绑定数据源
            this.gridView1.CreateColumn("FeeType", "费用类型", 100).CreateComboBox().BindDictItems("费用类型");

如果是一些特殊的输入需要设置格式显示或者掩码,那么如下所示

            //创建日期列并指定格式
            var OccurTime = this.gridView1.CreateColumn("OccurTime", "发生时间", 120).CreateDateEdit();
OccurTime.EditMask
= "yyyy-MM-dd HH:mm";
OccurTime.DisplayFormat.FormatString
= "yyyy-MM-dd HH:mm";

另外有一个值得注意的就是我们新增一行从表记录的时候,需要记录一些主表的属性,这样的话,我们就是在行初始化的时候,赋值给从表的隐藏列即可。

            //新增行的内容初始化
            this.gridView1.InitNewRow += (s, e) =>{
gridView1.SetRowCellValue(e.RowHandle,
"ID", Guid.NewGuid().ToString());
gridView1.SetRowCellValue(e.RowHandle,
"Header_ID", tempInfo.ID);
gridView1.SetRowCellValue(e.RowHandle,
"Apply_ID", tempInfo.Apply_ID);
gridView1.SetRowCellValue(e.RowHandle,
"OccurTime", DateTime.Now);
};

在界面中如果我们需要显示主表的信息,那么就根据条件获取对应的主表记录对象,然后显示给界面控件即可。

        /// <summary>
        ///显示常规的对象内容/// </summary>
        /// <param name="info"></param>
        private voidDisplayInfo(ReimbursementInfo info)
{
tempInfo
= info;//重新给临时对象赋值,使之指向存在的记录对象 txtCategory.Text=info.Category;
txtReason.Text
=info.Reason;
txtTotalAmount.Value
=info.TotalAmount;
txtNote.Text
=info.Note;
}

而保存的时候,我们把界面内容重新赋值给对应的主表对象。

        /// <summary>
        ///编辑或者保存状态下取值函数/// </summary>
        /// <param name="info"></param>
        private voidSetInfo(ReimbursementInfo info)
{
info.Category
=txtCategory.Text;
info.Reason
=txtReason.Text;
info.TotalAmount
=txtTotalAmount.Value;
info.Note
=txtNote.Text;

info.ApplyDate
=DateTime.Now;
info.ApplyDept
= base.LoginUserInfo.DeptId;
info.CurrentLoginUserId
= base.LoginUserInfo.ID;
}

而我们需要获取GridView明细输入的时候,就通过一个函数遍历获取GridView的行记录,转换为相应的对象即可,如下所示。

        /// <summary>
        ///获取明细列表/// </summary>
        /// <returns></returns>
        private List<ReimbursementDetailInfo>GetDetailList()
{
var list = new List<ReimbursementDetailInfo>();for (int i = 0; i < this.gridView1.RowCount; i++)
{
var detailInfo = gridView1.GetRow(i) asReimbursementDetailInfo;if (detailInfo != null)
{
list.Add(detailInfo);
}
}
returnlist;
}

这样处理完这些信息后,我们就可以在主表保存的时候,同时保存明细表信息即可。

        /// <summary>
        ///新增状态下的数据保存/// </summary>
        /// <returns></returns>
        public override boolSaveAddNew()
{
ReimbursementInfo info
= tempInfo;//必须使用存在的局部变量,因为部分信息可能被附件使用 SetInfo(info);
info.Creator
= base.LoginUserInfo.ID;
info.CreateTime
=DateTime.Now;try{#region 新增数据 bool succeed = BLLFactory<Reimbursement>.Instance.Insert(info);if(succeed)
{
//可添加其他关联操作 var list =GetDetailList();foreach(var detailInfo inlist)
{
BLLFactory
<ReimbursementDetail>.Instance.InsertUpdate(detailInfo, detailInfo.ID);
}
return true;
}
#endregion}catch(Exception ex)
{
LogTextHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}
return false;
}

其中代码

BLLFactory<ReimbursementDetail>.Instance.InsertUpdate(detailInfo, detailInfo.ID);

可以对新增记录保存,也可以对存在的记录进行更新。

通过上面的介绍,我们可以看到不同的主从表其实逻辑还是很通用的,我们可以把它们的逻辑抽取出来,通过代码生成工具进行快速生成即可。

在我们一般的应用系统里面,由于系统是面向不同类型的用户,我们所看到的菜单会越来越多,多一点的甚至上百个,但是我们实际工作接触的菜单可能就是那么几个,那么对于这种庞大的菜单体系,寻找起来非常不便。因此对菜单的个性化配置就显得尤为重要,本篇随笔就是基于这样的理念,提供用户对可见菜单进行一个动态配置,只选自己喜欢、常用的菜单显示出来即可,菜单的配置存储在数据库里面,在不同的客户端体验都是一样。本篇随笔主要介绍实现这样的功能的一个完整思路,部分代码逻辑可供参考。

1、 菜单列表的动态个性化配置的过程

在我们有些软件里面,我们可能在界面上顶部放置菜单,也可能在界面的左侧放置树形列表菜单,这种情况都有可能,本篇摘取其中之一,左侧菜单进行一个介绍菜单的配置处理。

例如我们在左侧根据用户权限展示相关的菜单信息,动态生成整个列表展示,大致的界面效果如下所示。

然后在功能列表上提供一个右键的菜单进行菜单的刷新、配置管理,如下界面所示。

通过配置功能,我们让用户进入一个配置管理界面,在其中配置显示自己感兴趣的菜单,然后进行保存即可,保存后同时刷新界面的功能菜单显示。

以上几个界面效果就是为了介绍整个菜单配置管理的一般过程,之所以把界面效果放在前面介绍,就是能够让我们有一个类似原型设计方式的感性认识,了解了相关的处理过程,我们就可以着手通过编码的方式来实现这个处理逻辑了。

2、菜单动态个性化配置的功能实现

上面介绍了大概的界面效果,有了参考,我们可以把它的实现思路通过代码实现出来。

1)参数的数据存储

首先我们需要了解,用户配置可以通过XML保存在本地,也可以通过数据库存储保存在服务器,后者在分布式的客户端的时候,可以处处一样,这样就不会造成体验上的差异,因此我们这里采用存储在数据库的方案。

这个存储我们沿用我之前介绍过的配置管理组件(SettingsProvider.net),我在随笔《
Winform开发框架之参数配置管理功能实现-基于SettingsProvider.net的构建
》中对它的使用进行了详细的介绍。

这个配置管理组件SettingsProvider.net使用起来也是比较方便的,可以选择存储在本地的对象,也可以选择存储在数据库的存储对象。

首先我们先定义一个存储的参数类,这个是使用这个组件所必须的存储对象信息,如下代码所示。

    /// <summary>
    ///用来控制人员管理显示菜单的参数配置/// </summary>
    public classUserMenuParameter
{
[DefaultValue(
"")]
[Description(
"用户ID")]public string UserID { get; set; }


[Description(
"用户设置可见的菜单")]public Dictionary<string, bool> VisibleDict { get; set; }
}

需要获取或存储这个对象信息的时候,我们初始化几个管理类,如下代码所示。

        //参数存储所需的相关对象
        privateSettingsProvider settings;privateISettingsStorage store;private UserMenuParameter parameter;

然后在配置管理界面窗体里面,初始化这几个对象,如下代码所示。

                //PortableStorage: 在运行程序目录创建一个setting的文件记录参数数据//DatabaseStorage:在数据库TB_UserParameter表存储用户配置参数
                store = newDatabaseStorage(LoginUserInfo.ID);
settings
= newSettingsProvider(store);
parameter
= settings.GetSettings<UserMenuParameter>();

这样我们就可以根据用户的ID,获取对应记录的信息并转换为相关的对象了,如果我们需要把修改的信息写会到存储介质里面,代码如下所示。

            try{
parameter
= settings.GetSettings<UserMenuParameter>();
parameter.VisibleDict
=dict;
parameter.UserID
=LoginUserInfo.ID;
settings.SaveSettings
<UserMenuParameter>(parameter);

ProcessDataSaved(sender, e);
//触发外部事件 this.DialogResult =System.Windows.Forms.DialogResult.OK;
}
catch(Exception ex)
{
LogHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
return;
}

2)配置管理界面的实现

解决了参数的获取及存储功能后,我们需要编写一个界面来管理用户的菜单配置,也就是我们前面介绍的菜单配置管理界面。

我们这个界面的定义代码如下所示。

其中参数的数据存储就是应用了前面介绍的代码,这里需要根据用户的配置项初始化树形菜单的显示处理,通过InitTree的函数实现菜单的显示。

在显示菜单前,我们先介绍一下功能菜单显示的规则,仅当参数存在对应记录,并且该记录显式设置不可见,菜单才不可见,否则默认菜单是可以看到的。

这样确保了,在参数没有配置前,所有的菜单对当前用户是可见的,只有用户设置为不不可见,该菜单才不显示为不可见。

        /// <summary>
        ///获取菜单是否可见。///仅当参数存在对应记录,并且该记录显式设置不可见,菜单才不可见,否则默认菜单是可以看到的。/// </summary>
        /// <param name="id">菜单ID</param>
        /// <returns></returns>
        private bool GetVisibleMenu(stringid)
{
bool result = true;if (parameter != null)
{
var dict =parameter.VisibleDict;if(dict != null &&dict.ContainsKey(id))
{
result
=dict[id];
}
}
returnresult;
}

显示菜单的相关处理逻辑,就是根据上面的判断,然后确定是否勾选记录,如下代码所示。

存储用户勾选的记录的时候,我们需要遍历整个树节点,判断勾选了那些选项,然后把它保存数据库即可。

        /// <summary>
        ///递归获取选中的树节点集合/// </summary>
        /// <param name="node">树节点</param>
        /// <param name="dict">字典集合</param>
        /// <returns></returns>
        private Dictionary<string, bool> GetTreeSelection(TreeNode node, Dictionary<string, bool>dict)
{
if (node.Tag != null)
{
var check =node.Checked;var menuId = string.Concat(node.Tag);if(!dict.ContainsKey(menuId))
{
dict.Add(menuId, check);
}
}
foreach (TreeNode child innode.Nodes)
{
GetTreeSelection(child, dict);
}
returndict;
}

参数的保存操作如下所示。

        /// <summary>
        ///保存用户配置信息/// </summary>
        private void btnOK_Click(objectsender, EventArgs e)
{
//获取用户勾选的树列表,存放在字典集合里面 var dict = new Dictionary<string, bool>();foreach(TreeNode node in this.treeView1.Nodes)
{
GetTreeSelection(node, dict);
}
try{//重新获取参数信息,并设置新值后保存 parameter = settings.GetSettings<UserMenuParameter>();
parameter.VisibleDict
=dict;
parameter.UserID
=LoginUserInfo.ID;
settings.SaveSettings
<UserMenuParameter>(parameter);

ProcessDataSaved(sender, e);
//触发外部事件 this.DialogResult =System.Windows.Forms.DialogResult.OK;
}
catch(Exception ex)
{
LogHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
return;
}
}

3)主界面的相关处理

以上处理完成后,我们在主界面的工具栏右键菜单添加一个菜单项,用来进入配置界面的,如下逻辑代码所示。

        private void tool_MenuSetting_ItemClick(objectsender, DevExpress.XtraBars.ItemClickEventArgs e)
{
MenuSetting();
}
/// <summary> ///配置菜单项/// </summary> private voidMenuSetting()
{
FrmMenuSetting dlg
= newFrmMenuSetting();
dlg.OnDataSaved
+= (s, arg) =>{//用户保存参数后,提示用户更新树形列表 InitToolbar();
};
dlg.ShowDialog();
}

这样界面配置参数并保存后,界面的树形菜单会及时得到更新处理。

另外,我们主界面的树形列表,也要根据配置参数的信息作相关的调整,如果用户配置了不显示某个菜单,那么主界面也要根据配置参数控制显示。

3、总结

以上就是整个菜单列表的动态个性化配置管理的整体思路和实现步骤代码,主要的界面考量还是以用户的视觉来考虑界面的布局和功能,如果在几百个菜单项中寻找几个常用的菜单,每次是一个比较耗时无聊的操作,因此提供一个个性化的界面,根据工作情况的不同,显示一些和自己相关的功能即可。

例如有些情况下,我们的菜单显示,希望通过工具栏的方式进行控制显示,如下界面效果所示。

那么配置维护界面还是差不多,只是我们控制工具栏的显示逻辑有所不同而已,对于RibbonPage及其功能菜单的动态生成处理如下所示。

本篇随笔主要还是希望读者借鉴配置存储和菜单个性化管理的思路,具体的逻辑会因用户界面的不同,使用的控件不同而有所差异,不过总体思路是一致的即可。

例如有些参数的配置管理,可以统一使用一个配置管理界面进行维护,如我之前的随笔介绍的界面功能一样。

希望本篇随笔对你有所启发,感谢耐心阅读。

在一个给客户做的项目中,界面要求修改增加通用工具栏按钮的事件处理,也就是在主界面中放置几个固定的功能操作按钮,打开不同的页面的时候,实现对应页面的功能处理,这种和我标准的界面处理方式有所不同,标准的列表界面,一般在界面中放置了一些常规的按钮,如查询/更新、新建、编辑、删除、导入、导出等常规操作,现在是需要把这些提升到主界面的层次上放置按钮,这种处理方式也算是提供一种方便吧。本篇随笔介绍实现这个功能的思路和代码实现逻辑。

1、界面功能介绍

这里要实现的通用工具栏按钮的事件处理,具体的界面是这样的,我们处于一个多文档的主界面中,在主界面的顶部工具栏放置这些通用的工具栏按钮,以便提高相关的处理便利和效率。

从上面的界面我们可以看到,程序主界面的顶部工具栏上有一些常规的操作,包括如查询、新建、编辑、删除、导入、导出等按钮,可以为程序的功能操作提供方便。

当我们打开一个新的窗体页面,那么对应的按钮事件也应该和这个窗体相关,或者我们切换到其他窗体,那么按钮的处理事件也应该跟着变化到对应的窗体上,这种就是我们需要的效果。

上面的逻辑也就是我们新建一个窗体、或者切换一个窗体,那么需要通知顶部工具栏进行更新或者进行一个对接的处理才可以。

2、界面窗体基类的处理

由于我们想尽可能的减少开发的工作量,因此希望在基类进行一些事件或者接口的封装,以便降低新建列表窗体的时候的代码编写。

对于一个如查询操作的处理事件,我们需要定义一个接口来实现这个功能,这样我们才可以在打开的窗体的时候,把它转换为对应的接口处理。

例如我们定义一个IMenuAction的接口,以更新事件为例,需要实现三部分的接口,一个是事件定义,一个是判断是否包含更新事件,一个是具体的处理逻辑。

    /// <summary>
    ///定义菜单中的通用按钮事件/// </summary>
    public interfaceIMenuAction
{
/// <summary> ///更新按钮事件/// </summary> eventEventHandler Refresh_MenuEvent;/// <summary> ///是否包含更新事件/// </summary> bool HasEventRefresh { get; }/// <summary> ///更新操作/// </summary> void ProcessRefresh(object sender, EventArgs e);

对于其他总共6个标准的按钮事件处理,我们也同样一样定义,那么整个IMenuAction的接口定义如下所示。

    /// <summary>
    ///定义菜单中的通用按钮事件/// </summary>
    public interfaceIMenuAction
{
/// <summary> ///更新按钮事件/// </summary> eventEventHandler Refresh_MenuEvent;/// <summary> ///新建按钮事件/// </summary> eventEventHandler Add_MenuEvent;/// <summary> ///编辑按钮事件/// </summary> eventEventHandler Edit_MenuEvent;/// <summary> ///删除按钮事件/// </summary> eventEventHandler Delete_MenuEvent;/// <summary> ///导入按钮事件/// </summary> eventEventHandler Import_MenuEvent;/// <summary> ///导出按钮事件/// </summary> eventEventHandler Export_MenuEvent;/// <summary> ///是否包含更新事件/// </summary> bool HasEventRefresh { get; }/// <summary> ///是否包含增加事件/// </summary> bool HasEventAdd { get; }/// <summary> ///是否包含编辑事件/// </summary> bool HasEventEdit { get; }/// <summary> ///是否包含删除事件/// </summary> bool HasEventDelete { get; }/// <summary> ///是否包含导入事件/// </summary> bool HasEventImport { get; }/// <summary> ///是否包含导出事件/// </summary> bool HasEventExport { get; }/// <summary> ///更新操作/// </summary> void ProcessRefresh(objectsender, EventArgs e);/// <summary> ///增加操作/// </summary> void ProcessAdd(objectsender, EventArgs e);/// <summary> ///编辑操作/// </summary> void ProcessEdit(objectsender, EventArgs e);/// <summary> ///删除操作/// </summary> void ProcessDelete(objectsender, EventArgs e);/// <summary> ///导入操作/// </summary> void ProcessImport(objectsender, EventArgs e);/// <summary> ///导出操作/// </summary> void ProcessExport(objectsender, EventArgs e);
}

那么对于列表界面的基类窗体,我们除了让他继承自XtraForm这个标准的窗体外,我们还让它实现对应的IMenuAction接口,如下是列表界面基类BaseDock的窗体定义。

    /// <summary>
    ///用于一般列表界面的基类/// </summary>
    public partial class BaseDock : XtraForm, IMenuAction

那么这个BaseDock的基类需要实现那些通用按钮事件的接口,如下所示。

        #region 通用按钮菜单事件
        /// <summary>
        ///更新按钮事件/// </summary>
        public eventEventHandler Refresh_MenuEvent;/// <summary>
        ///根据事件判断是否有/// </summary>
        public boolHasEventRefresh
{
get{return Refresh_MenuEvent != null;
}
}
/// <summary> ///调用事件定义/// </summary> public void ProcessRefresh(objectsender, EventArgs e)
{
if (Refresh_MenuEvent != null)
{
Refresh_MenuEvent(sender, e);
}
}

.......................
#endregion

通过基类的这样处理,我们就可以在具体列表窗体里,赋值给对应的事件就可以了。

3、列表窗体界面的处理

例如我们处理一个应用菜单的列表管理界面,定义窗体如下所示,继承了我们所实现IMenuAction的基类BaseDock。

    /// <summary>
    ///应用菜单管理/// </summary>
    public partial class FrmApplicationMenu : BaseDock

那么我们需要指定它的几个处理事件,初始化代码如下所示。

        /// <summary>
        ///初始化公用菜单按钮的处理/// </summary>
        private voidInitMenuAction()
{
this.Refresh_MenuEvent += (s, e) =>{ btnSearch_Click(s, e); };this.Add_MenuEvent += (s, e) =>{ btnAddNew_Click(s, e); };this.Edit_MenuEvent += (s, e) =>{ winGridViewPager1_OnEditSelected(s, e); };this.Delete_MenuEvent += (s, e) =>{ winGridViewPager1_OnDeleteSelected(s, e); };this.Import_MenuEvent += (s, e) =>{ btnImport_Click(s, e); };this.Export_MenuEvent += (s, e) =>{ btnExport_Click(s,e);};
}

这样我们就知道对应接口是如何和具体的页面事件发生关系了。

4、主窗体界面的处理

我们在构建主界面的左侧功能树的时候,我们通过树列表的选中事件加载对应的窗体,具体代码如下所示。

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

在这个函数里面,我们最终是通过配置动态构建对应的窗体,LoadPlugInForm逻辑的最终实现的部分代码如下所示。

    var form = LoadMdiForm(this.mainForm, objType, isShowDialog);
RefreshButton(form);

这里我们构建窗体或者激活窗体都获得一个窗体对象,然后刷新功能按钮的状态处理即可。

    /// <summary>
    ///更新按钮状态/// </summary>
    /// <param name="form">当前窗体</param>
    public voidRefreshButton(Form form)
{
this.CurrentForm =form;
IMenuAction action
= form asIMenuAction;if (action != null)
{
//事件处理后再判断 menuButton.refresh.Enabled =(action.HasEventRefresh);
menuButton.add.Enabled
=(action.HasEventAdd);
menuButton.edit.Enabled
=(action.HasEventEdit);
menuButton.delete.Enabled
=(action.HasEventDelete);
menuButton.import.Enabled
=(action.HasEventImport);
menuButton.export.Enabled
=(action.HasEventExport);
}
}

这样的处理,就可以实现了在主界面的多文档处理中,不管我们是新建一个列表窗体,还是激活切换到一个其他的窗体的时候,按钮的状态和对应的窗体绑定,实现随时更新显示。

当然通用工具栏按钮的事件我们需要初始化,它是根据当前选中的窗体进行统一的处理的,具体代码如下所示。

        /// <summary>
        ///初始化菜单按钮的事件处理/// </summary>
        private voidInitMenuEvent()
{
//事件绑定//先实现事件绑定 menuButton.add.ItemClick += (s, e) =>{if (CurrentForm != null)
{
IMenuAction action
= CurrentForm asIMenuAction;if (action != null)
{
action.ProcessAdd(s, e);
}
}
};
menuButton.edit.ItemClick
+= (s, e) =>{if (CurrentForm != null)
{
IMenuAction action
= CurrentForm asIMenuAction;if (action != null)
{
action.ProcessEdit(s, e);
}
}
};
menuButton.delete.ItemClick
+= (s, e) =>{if (CurrentForm != null)
{
IMenuAction action
= CurrentForm asIMenuAction;if (action != null)
{
action.ProcessDelete(s, e);
}
}
};
menuButton.import.ItemClick
+= (s, e) =>{if (CurrentForm != null)
{
IMenuAction action
= CurrentForm asIMenuAction;if (action != null)
{
action.ProcessImport(s, e);
}
}
};
menuButton.export.ItemClick
+= (s, e) =>{if (CurrentForm != null)
{
IMenuAction action
= CurrentForm asIMenuAction;if (action != null)
{
action.ProcessExport(s, e);
}
}
};
menuButton.refresh.ItemClick
+= (s, e) =>{if (CurrentForm != null)
{
IMenuAction action
= CurrentForm asIMenuAction;if (action != null)
{
action.ProcessRefresh(s, e);
}
}
};
}

上面这个事件只需要绑定一次,不会造成按钮的事件多次绑定问题,虽然一次绑定,它的具体处理还是和当前窗体有关,它会把当前窗体转换为对应的IMenuAction接口,然后调用对应的处理函数实现功能的绑定。

以上就是我对于实现这种通用按钮事件处理的思路和具体逻辑代码,供大家参考学习,有错漏之处还请多多包涵。

最近需要向客户发送一些宣传资料,Excel列表里面有一两百个记录,本来想手写就算了,估摸着也花不了多少时间,不过写完一个信封我就后悔了,整天敲着键盘,书写的字太难看了,而且感觉手还是有点累。才第一个啊,想着后面还有那么多,感觉整个人头都大了,只好放弃,太没技术含量了。然后寻找有无一些套打的的软件,不过找来找去还是没有满意的,还是自己写一个套打的软件好了,这个小小的还是有点技术含量,呵呵。本篇随笔基于这个困惑,整理了一个信封套打以及批量打印的实现过程,软件可以实现自动批量的信封套打,一两百个的信封地址,也是一阵子的功夫就打印完成了,感觉小有成就,而且以后就基于这个模式来批量打印信封,方便了很多。

1、信封套打的实现思路

基于套打的处理,我在几年前的随笔都有一些随笔介绍,如《
Web打印的解决方案之证件套打
》、《
基于MVC4+EasyUI的Web开发框架经验总结(16)--使用云打印控件C-Lodop打印页面或套打报关运单信息
》这两篇随笔基本思路还是采用打印控件C-Lodop(
http://www.lodop.net/
)来进行打印的,我这样的套打小程序,最好还是做成Winform程序比较方便,因此在Winform里面整合网页浏览就可以实现这个控件的打印处理了。

另外,我们根据打印的地址信息,动态的生成HTML内容即可,基于这样的理念,我们动态生成HTML可以考虑使用NVelocity的模板化处理技术,这个可以参考下我的随笔《
使用NVelocity生成内容的几种方式
》进行了解,这个NVelocity用来处理模板化的内容生成还是非常方便的。

前面介绍的这些内容,其实就是基于C-Lodop 和 NVelocity进行一个信封批量套打的处理实现技术。

这些技术都是之前研究实现过的东西,用起来应该驾轻就熟了,也就不枉费精力去找其他信封套打软件了。

我们先来介绍一下整体的打印效果,首先我们要有一些特制的信封或者普通信封,这样才能基于这个基础上进行套打,把邮政编码、地址和联系人等信息打印上去。

然后你需要有一个打印设备,我这里采用了一个佳能的喷墨打印机(当然其他的也没问题)。

最后就是基于软件的基础上进行套打了,软件可以单个打印,也可以导入Excel进行套打才可以解决实际问题。

一般是在单个打印没问题后,进行批量打印,可以省却大量的时间,下面是基于这个套打软件打印的信封,如果手写的话,不知道写到何年何月了。

2、信封套打的实现过程

下面我们来介绍这个信封套打的实现过程。

首先我们先做一个测试页面,启动C-Lodop的设计界面,我们往里面添加一些信封所需要的元素,如套打的背景图片、邮政编码的文本、地址文本、人名称呼等信息后,大致调整到合适的位置即完成了相关的设计,界面设计效果如下所示。

然后我们通过获取设计样式下的代码

生成代码里面就可以看到具体的内容了。

有了这些信息,我们就可以创建一个HTML模板,然后利用NVelocity来动态生成相关的变量信息即可,HTML模板文件中的逻辑代码(JS代码)如下所示。

接着在我们模板文件中的打印预览函数编写如下所示。

这样就可以实现标准格式的浏览了,文本显示位置不一定和背景完全吻合,背景只是作为参考处理而已。

完成这些就基本上是大功告成了,但是我们打印的时候,我们需要注意在打印设置中采用的纸张,如果采用A4的卡纸宽度和自定义的卡纸宽度的设置是不同的,我们建议根据信封的尺寸自定义格式设置,卡纸的位置就和信封宽度一致即可,防止信封进纸的时候晃动导致位置发生偏差。下面的是我对打印信封的用户自定义设置对话框,采用实际的尺寸(毫米)进行设置即可。

前面介绍的是当个信封打印预览然后进行打印,打印的按钮事件处理代码如下所示。

传入相关的键值参数后,可以实现相关的数据绑定,然后打开HTML后,就可以执行相关的脚本进行信封打印了。

而对于批量打印,处理方式和单个打印有所差异,就是不会每个都介入预览操作,我们是需要把一批对象进行打印。

那么我们如果要实现批量的数据打印,就需要利用JS里面的数组操作,把对应的对象放到里面,然后批量进行打印就可以了。

而这个数据的生成,我们就是利用NVelocity的模板函数进行处理即可。

完成这些,我们就可以传入对应的打印集合,让其在NVelocity模板里面生成对应的HTML代码,生成对应的对象加入到打印的数组里面,从而完成批量的数据打印了。

批量套打是在我们确认单个打印格式吻合已有信封的位置情况下,进行批量的打印处理。

一旦开启批量打印,我们的剩下的工作就是不断的往打印机上面放置足够的信封即可,可以边喝茶边等待完成,悠然自得的了。

以上就是基于NVelocity+C-LODOP控件实现的信封套打整个思路和实现方式,对于我们技术人来说,做一个这样的软件所花费的时间,比手写几百个信封浪费的时间更有意义,所获得的成就感也是有那么一些的。

总之,技术就是为生活服务。

Swift是苹果推出的一个比较新的语言,它除了借鉴语言如C#、Java等内容外,好像还采用了很多JavaScript脚本里面的一些脚本语法,用起来感觉非常棒,作为一个使用C#多年的技术控,对这种比较超前的语言非常感兴趣,之前也在学习ES6语法的时候学习了阮一峰的《
ECMAScript 6 入门
》,对JavaScript脚本的ES6语法写法叹为观止,这种Swift语言也具有很多这种脚本语法的特点,可以说这个Swift在吸收了Object C的优点并摒弃一些不好的东西外,同时吸收了大量新一代语言的各种特点,包括泛型、元祖等特点。我在学习Swift的时候,发现官方的语言介绍文章(
The Swift Programming Language
)还是非常浅显易懂,虽然是英文,不过代码及分析说明都很到位,就是内容显得比较多一些,而我们作为技术人员,一般看代码就很好了解了各种语法特点了,基于这个原因,我对官网的案例代码进行了一个摘要总结,以代码的方式进行Swift语言的语法特点介绍,总结一句话就是:快看Sample代码,速学Swift语言。

1、语法速览

var myVariable = 42myVariable= 50let myConstant= 42

变量定义用var,常量则用let,类型自行推断。

let apples = 3let oranges= 5let appleSummary= "I have \(apples) apples."let fruitSummary= "I have \(apples + oranges) pieces of fruit."

用括号包含变量

let quotation = """I said"I have \(apples) apples."And then I said"I have \(apples + oranges) pieces of fruit."
"""

代码通过三个双引号来包含预定格式的字符串(包括换行符号),左侧缩进空格省略。

var shoppingList = ["catfish", "water", "tulips", "blue paint"]
shoppingList[
1] = "bottle of water" var occupations =["Malcolm": "Captain","Kaylee": "Mechanic",
]
occupations[
"Jayne"] = "Public Relations"

数组和字典集合初始化符合常规,字典后面可以保留逗号结尾

let emptyArray =[String]()
let emptyDictionary
= [String: Float]()

初始化函数也比较简洁。

let individualScores = [75, 43, 103, 87, 12]var teamScore = 0
for score inindividualScores {if score > 50{
teamScore
+= 3}else{
teamScore
+= 1}
}
print(teamScore)

控制流的if-else这些和其他语言没有什么差异,for ... in 则是迭代遍历的语法,控制流方式还支持其他的while、repeat...while等不同的语法。

var optionalString: String? = "Hello"print(optionalString==nil)var optionalName: String? = "John Appleseed"
var greeting = "Hello!"
if let name =optionalName {
greeting
= "Hello, \(name)"}

这部分则是可空类型的使用,以及可空判断语句的使用,可空判断语句在Swift中使用非常广泛,这种相当于先求值再判断是否进入大括符语句。

let vegetable = "red pepper"
switchvegetable {case "celery":
print(
"Add some raisins and make ants on a log.")case "cucumber", "watercress":
print(
"That would make a good tea sandwich.")case let x where x.hasSuffix("pepper"):
print(
"Is it a spicy \(x)?")default:
print(
"Everything tastes good in soup.")
}

Switch语法和常规的语言不同,这种简化了一些语法,每个子条件不用显式的写break语句(默认就是返回的),多个条件逗号分开即可公用一个判断处理。

let interestingNumbers =["Prime": [2, 3, 5, 7, 11, 13],"Fibonacci": [1, 1, 2, 3, 5, 8],"Square": [1, 4, 9, 16, 25],
]
var largest = 0 for (kind, numbers) ininterestingNumbers {for number innumbers {if number >largest {
largest
=number
}
}
}
print(largest)

上面字典遍历的方式采用for...in的方式进行遍历,另外通过(
kind,
numbers)的方式进行一个参数的解构过程,把字典的键值分别付给kind,numbers这两个参数。

var total = 0
for i in 0..<4{
total
+=i
}
print(total)

上面的for...in循环采用了一个语法符号..<属于数学半封闭概念,从0到4,不含4,同理还有全封闭符号:...全包含左右两个范围的值。

func greet(person: String, day: String) ->String {return "Hello \(person), today is \(day)."}
greet(person:
"Bob", day: "Tuesday")

上面是函数的定义,以func关键字定义,括号内是参数的标签、名称和类型内容,返回值通过->指定。

上面函数需要输入参数名称,如果不需要参数名称,可以通过下划线省略输入,如下

func greet(_ person: String, on day: String) ->String {return "Hello \(person), today is \(day)."}
greet(
"John", on: "Wednesday")

另外参数名称可以使用标签名称。

func greet(person: String, from hometown: String) ->String {return "Hello \(person)!  Glad you could visit from \(hometown)."}
print(greet(person:
"Bill", from: "Cupertino"))//Prints "Hello Bill! Glad you could visit from Cupertino."

嵌套函数如下所示。

func returnFifteen() ->Int {var y = 10func add() {
y
+= 5}
add()
returny
}
returnFifteen()

复杂一点的函数的参数可以传入函数进行使用,这种类似闭包的处理了

func hasAnyMatches(list: [Int], condition: (Int) -> Bool) ->Bool {for item inlist {ifcondition(item) {return true}
}
return false}
func lessThanTen(number: Int)
->Bool {return number < 10}var numbers = [20, 19, 7, 12]
hasAnyMatches(list: numbers, condition: lessThanTen)

下面是一个闭包的函数,闭包通过in 来区分参数和返回的函数体

numbers.map({ (number: Int) -> Int inlet result= 3 *numberreturnresult
})

class Shape {var numberOfSides = 0func simpleDescription()->String {return "A shape with \(numberOfSides) sides."}
}

类的定义通过class关键字进行标识,默认的权限是internal,在项目模块内部可以访问的,非常方便。

使用则如下所示,可以通过点语法直接获取属性和调用方法。

var shape =Shape()
shape.numberOfSides
= 7 var shapeDescription = shape.simpleDescription()

class NamedShape {var numberOfSides: Int = 0
    varname: String

init(name: String) {
self.name
=name
}

func simpleDescription()
->String {return "A shape with \(numberOfSides) sides."}
}

类通过使用init的指定名称作为构造函数,使用deinit来做析构函数,使用self来获取当前的类引用,类似于其他语言的this语法,super获取基类的引用。

其他的处理方式如继承、重写的语法和C#类似。

class Square: NamedShape {varsideLength: Double

init(sideLength: Double, name: String) {
self.sideLength
=sideLength
super.init(name: name)
numberOfSides
= 4}

func area()
->Double {return sideLength *sideLength
}

override func simpleDescription()
->String {return "A square with sides of length \(sideLength)."}
}
let test
= Square(sideLength: 5.2, name: "my test square")
test.area()
test.simpleDescription()

类的属性使用get、set语法关键字,和C#类似

class EquilateralTriangle: NamedShape {var sideLength: Double = 0.0init(sideLength: Double, name: String) {
self.sideLength
=sideLength
super.init(name: name)
numberOfSides
= 3}varperimeter: Double {
get {
return 3.0 *sideLength
}
set {
sideLength
= newValue / 3.0}
}

class TriangleAndSquare {vartriangle: EquilateralTriangle {
willSet {
square.sideLength
=newValue.sideLength
}
}
varsquare: Square {
willSet {
triangle.sideLength
=newValue.sideLength
}
}

类属性的赋值可以进行观察,如通过willSet在设置之前调用,didSet在设置之后调用,实现对属性值得监控处理。

enum Rank: Int {case ace = 1
    casetwo, three, four, five, six, seven, eight, nine, tencasejack, queen, king
func simpleDescription()
->String {switchself {case.ace:return "ace" case.jack:return "jack" case.queen:return "queen" case.king:return "king" default:returnString(self.rawValue)
}
}
}
let ace
=Rank.ace
let aceRawValue
= ace.rawValue

和类及其他类型一样,枚举类型在Swift中还可以有方法定义,是一种非常灵活的类型定义,这个和我们之前接触过的一般语言有所差异。

enum ServerResponse {caseresult(String, String)casefailure(String)
}

let success
= ServerResponse.result("6:00 am", "8:09 pm")
let failure
= ServerResponse.failure("Out of cheese.")switchsuccess {caselet .result(sunrise, sunset):
print(
"Sunrise is at \(sunrise) and sunset is at \(sunset).")caselet .failure(message):
print(
"Failure... \(message)")
}

struct Card {varrank: Rankvarsuit: Suit
func simpleDescription()
->String {return "The \(rank.simpleDescription()) of \(suit.simpleDescription())"}
}
let threeOfSpades
=Card(rank: .three, suit: .spades)
let threeOfSpadesDescription
= threeOfSpades.simpleDescription()

结构类型和类的各个方面很类似,结构支持构造函数,方法定义,属性等,重要一点不同是结构在代码传递的是副本,而类实例传递的是类的引用。

protocol ExampleProtocol {varsimpleDescription: String { get }
mutating func adjust()
}

这里的协议,类似很多语言的接口概念,不过比常规语言(包括C#)的接口更加多样化、复杂化一些。

Swift的协议,可以有部分方法实现,协议可以可选,继承其他协议等等。

extension Int: ExampleProtocol {varsimpleDescription: String {return "The number \(self)"}
mutating func adjust() {
self
+= 42}
}
print(
7.simpleDescription)

扩展函数通过extension进行标识,可以为已有的类进行扩展一些特殊的方法处理,这个类似C#的扩展函数。

func send(job: Int, toPrinter printerName: String) throws ->String {if printerName == "Never Has Toner"{throwPrinterError.noToner
}
return "Job sent"}

异常处理中,函数声明通过throws关键字标识有异常抛出,在函数里面通过throw进行异常抛出处理。

而在处理有异常的地方进行拦截,则通过do...catch的方式进行处理,在do的语句里面,通过try来拦截可能出现的异常,默认catch里面的异常名称为error。

do{
let printerResponse
= try send(job: 1040, toPrinter: "Bi Sheng")
print(printerResponse)
}
catch{
print(error)
}

可以对多个异常进行判断处理

do{
let printerResponse
= try send(job: 1440, toPrinter: "Gutenberg")
print(printerResponse)
}
catchPrinterError.onFire {
print(
"I'll just put this over here, with the rest of the fire.")
}
catchlet printerError as PrinterError {
print(
"Printer error: \(printerError).")
}
catch{
print(error)
}

还可以通过使用try?的方式进行友好的异常处理,如果有异常返回nil,否者获取结果赋值给变量

let printerSuccess = try? send(job: 1884, toPrinter: "Mergenthaler")
let printerFailure
= try? send(job: 1885, toPrinter: "Never Has Toner")

var fridgeIsOpen = falselet fridgeContent= ["milk", "eggs", "leftovers"]

func fridgeContains(_ food: String)
->Bool {
fridgeIsOpen
= truedefer {
fridgeIsOpen
= false}

let result
=fridgeContent.contains(food)returnresult
}
fridgeContains(
"banana")
print(fridgeIsOpen)

使用defer的关键字来在函数返回前处理代码块,如果有多个defer函数,则是后进先出的方式进行调用,最后的defer先调用,依次倒序。

func makeArray<Item>(repeating item: Item, numberOfTimes: Int) ->[Item] {var result =[Item]()for _ in 0..<numberOfTimes {
result.append(item)
}
returnresult
}
makeArray(repeating:
"knock", numberOfTimes: 4)

Swift支持泛型,因此可以大大简化很多函数的编写,提供更加强大的功能。

enum OptionalValue<Wrapped>{casenonecasesome(Wrapped)
}
var possibleInteger: OptionalValue<Int> =.none
possibleInteger
= .some(100)

func anyCommonElements<T: Sequence, U: Sequence>(_ lhs: T, _ rhs: U) ->Bool
where T.Iterator.Element: Equatable, T.Iterator.Element
==U.Iterator.Element {for lhsItem inlhs {for rhsItem inrhs {if lhsItem ==rhsItem {return true}
}
}
return false}
anyCommonElements([
1, 2, 3], [3])

泛型的参数支持where的关键字进行泛型类型的约束,如可以指定泛型的参数采用什么协议或者继承哪个基类等等。