2023年2月

使用DevExpress控件来做项目开发已经有很长一段时间了,在摸索开发到客户苛刻要求的过程中,其中碰到过很多问题需要解决的,随着一个个问题的解决,也留下很多对DevExpress控件的使用经验及教训,综合设计到的多个项目的问题,对这些开发常用的要点进行总结,方便别人也方便自己。提供这些解决方法,一个可以快速应用到项目中,二个也可以作为对界面开发的更高要求对待自己的项目,使得自己的东西更加完美,更加受欢迎。

1、 GridControl控件的数据显示的样式控制

如上两图所示,我们有时候需要控制列表访问过的颜色变化,或者是时间显示格式等内容,这个时候设置GridView的RowCellStyle即可实现,如下所示。

this.gridView1.RowCellStyle += new DevExpress.XtraGrid.Views.Grid.RowCellStyleEventHandler(gridView1_RowCellStyle);
        void gridView1_RowCellStyle(objectsender, DevExpress.XtraGrid.Views.Grid.RowCellStyleEventArgs e)
{
if (e.Column.FieldName == "PublishType")
{
if (e.CellValue != null && e.CellValue.ToString() == "中介")
{
e.Appearance.BackColor
=Color.DeepSkyBlue;
e.Appearance.BackColor2
=Color.LightCyan;
}
}
if (e.Column.FieldName == "PublishTime")
{
e.Column.DisplayFormat.FormatString
= "yyyy-MM-dd HH:mm:ss";
}
if (e.Column.FieldName == "Title")
{
string id = this.winGridViewPager1.gridView1.GetRowCellDisplayText(e.RowHandle, "Id");if(historyDict.ContainsKey(id))
{
e.Appearance.BackColor
=Color.DeepSkyBlue;
e.Appearance.BackColor2
=Color.LightCyan;
}
}
}

2、在LayoutControl布局中固定控件宽度

固定宽度后的真实效果。

为了使得界面统一性及更好的控制性,我们一般使用LayoutControl布局控件作为我们添加控件的布局容器,但是这个控件默认是对其中的控件进行按窗口比例进行缩放的,有些客户就不喜欢这些特点,因为他们的显示器可能是30寸的(夸张一点点,不过很多宽屏的),这样很多输入框就会被拉得很长,这样小小一个输入框,可能有很长的一段空白的距离,那样可能真的不好看,如下图所示。

设置固定宽度,其实不是很麻烦,需要设置几个属性即可

1) 设置控件的SizeConstraintsType为DevExpress.XtraLayout.SizeConstraintsType.
Custom
;

2)设置控件的FillControlToClientArea 为False

3)设置控件的ControlMaxSize的大小(必要时也可以设置ControlMinSize),设置例子如下所示。

3、GridControl中的GridView内容打印

由于GridView的良好封装性,实现打印的代码很简单。

        private void menu_Print_Click(objectsender, EventArgs e)
{
PrintableComponentLink link
= new PrintableComponentLink(newPrintingSystem());
link.Component
= this.gridControl1;
link.Landscape
= true;
link.PaperKind
=System.Drawing.Printing.PaperKind.A3;
link.CreateMarginalHeaderArea
+= newCreateAreaEventHandler(Link_CreateMarginalHeaderArea);
link.CreateDocument();
link.ShowPreview();
}
private void Link_CreateMarginalHeaderArea(objectsender, CreateAreaEventArgs e)
{
string title = string.Format("年度大体检-({0}年度)", this.txtYear.Text);
PageInfoBrick brick
=e.Graph.DrawPageInfo(PageInfo.None, title, Color.DarkBlue,new RectangleF(0, 0, 100, 21), BorderSide.None);

brick.LineAlignment
=BrickAlignment.Center;
brick.Alignment
=BrickAlignment.Center;
brick.AutoWidth
= true;
brick.Font
= new System.Drawing.Font("宋体", 11f, FontStyle.Bold);
}

4、设置GridView的行指示器(行头)显示行号


在我的
分页控件
以及
Winform开发框架
很多项目介绍里面,很多都显示了行号,其实这个在DevExpress中的实现很简单,如果需要,可以实现在自己的代码里面。

1) 先实现GridView的CustomDrawRowIndicator事件,实现代码如下所示。

        private void advBandedGridView1_CustomDrawRowIndicator(objectsender, DevExpress.XtraGrid.Views.Grid.RowIndicatorCustomDrawEventArgs e)
{
e.Appearance.TextOptions.HAlignment
=DevExpress.Utils.HorzAlignment.Far;if(e.Info.IsRowIndicator)
{
if (e.RowHandle >= 0)
{
e.Info.DisplayText
= (e.RowHandle + 1).ToString();
}
else if (e.RowHandle < 0 && e.RowHandle > -1000)
{
e.Info.Appearance.BackColor
=System.Drawing.Color.AntiqueWhite;
e.Info.DisplayText
= "G" +e.RowHandle.ToString();
}
}
}

2)然后设置GridView控件的IndicatorWidth为合适的宽度,如40左右则比较好。

这样设置后,就能顺利显示行号了,是不是很方便呢。

5、GridView表头多行显示(折行),表头及行内容居中操作

在一些自定义的列表中,我们为了合理显示表头的内容,可能会要求表头显示的文字可以折行显示,然后还需要内容居中显示,那么在DevExpress该如何操作呢,代码设置如下所示。

            //表头折行设置
            this.gridView1.ColumnPanelRowHeight = 40;this.gridView1.OptionsView.AllowHtmlDrawHeaders = true;this.gridView1.Appearance.HeaderPanel.TextOptions.WordWrap =DevExpress.Utils.WordWrap.Wrap;//表头及行内容居中显示
            this.gridView1.Appearance.Row.TextOptions.HAlignment =DevExpress.Utils.HorzAlignment.Center;this.gridView1.Appearance.HeaderPanel.TextOptions.HAlignment = DevExpress.Utils.HorzAlignment.Center;

然后设置表头的时候,设置内容使用\r\n来进行换行,如:体重\r\n(kg)

下面是我分页控件里面的例子设置,供参考。

            this.winGridViewPager1.AddColumnAlias("Weight", "体重\r\n(kg)");this.winGridViewPager1.AddColumnAlias("Pulse", "脉搏\r\n(次)");this.winGridViewPager1.AddColumnAlias("BloodSystolic", "收缩压");this.winGridViewPager1.AddColumnAlias("BloodDiastolic", "舒张压");this.winGridViewPager1.AddColumnAlias("SightLeft", "视力\r\n(左)");this.winGridViewPager1.AddColumnAlias("SightRight", "视力\r\n(右)");

6、控件内容输入问题

在客户使用软件的时候,反馈说有一些数字输入框、日期输入框,不支持键盘输入内容,经测试,确实是存在这个问题,默认的数字、日期等输入,都要切换输入法才可以输入,不能默认就支持数字的输入,那么是否有办法解决呢,当然办法总是有的。

其实只需要设置
ImeMode=
Off

即可解决问题,关于这个属性,大家可以参考下面的介绍。(就开发而言,记得逢数字,日期的控件,必须设置 ImeMode=Off ,否则你的软件使用客户会骂娘。

ImeMode 枚举:
指定一个值,该值是用来确定在选定了对象时该对象的输入法编辑器 (IME) 的状态。 以下是微软的解释:

成员名称 说明
Alpha 字母数字单字节字符 (SBC)。此设置仅对朝鲜语和日语 IME 有效。
AlphaFull 字母数字双字节字符。此设置仅对朝鲜语和日语 IME 有效。
Close Disable IME 被禁用。如果使用此设置,则用户无法从键盘打开 IME,而且 IME 浮动窗口处于隐藏状态。
Hangul 朝鲜文 SBC。此设置仅对朝鲜语 IME 有效。
HangulFull 朝鲜文 DBC。此设置仅对朝鲜语 IME 有效。
Hiragana 平假名 DBC。此设置仅对日语 IME 有效。
Inherit 继承父控件的 IME 模式。
Katakana 片假名 DBC。此设置仅对日语 IME 有效。
KatakanaHalf 片假名 SBC。此设置仅对日语 IME 有效。
NoControl 无(默认)。
Off IME 已关闭。此模式指示 IME 已关闭,这意味着该对象的行为方式与英语输入模式相同。 此设置仅对日语、简体中文和繁体中文 IME 有效。
On IME 已打开。此值指示 IME 已打开,可以输入中文或日语特定的字符。此设置仅对日语、简体中文和繁体中文 IME 有效。

7、多行表头的实现

有时候,我们为了一些特殊的需要,要对表头进行特别的排版,使其支持多行表头的效果,如上图所示,这样方便对各项内容进行归类显示,易于阅读,在DevExpress中应该如何实现这个效果呢?

1)先在设计模式将普通的GridView转换为BandedGridView或者AdvBandedGridView,这样才能支持这种多行表头的做法,如下所示。

2)定义一些字段,用来显示其中的内容,如下图所示。

3)定义一些Band列,设置相关的属性,并把设计界面中的字段列表拖动到对应过的Band列上面,这样就构成了一个Band列和字段内容的对应关系。

完成上面的绑定关系后,记得设置GridView控件的属性,使其不要显示原本的ColumnHeader等内容。

设置好这些内容,才能合理、完美显示出多行表头的信息。

8. 在GridControl控件中使用SearchLookUpEdit构建数据快速输入

一个实际的案例就是门诊的时候,医生用药的情况,除了选择其他内容外,主要的就是快速录入药品信息。我们平常去大一点 的医院看病,好像看到的多数操作都是这样。还有一种方式就是销售人员提供的报价单,从产品里面选择信息,然后修改下价格,这些场景都是很适合这样的操作的。下面是一个门诊的例子。

详细参考随笔文章《
在GridControl控件中使用SearchLookUpEdit构建数据快速输入

做了很多Winform的项目,对于数据导入,一直也有自己的理解,由于一般的业务系统,经常性的数据导入时很正常的业务需求,因为毕竟使用Excel来操作数据也很方便,或者由于系统之间的数据交换需要,我们需要提供一个入口给客户导入所需要的数据。但是导入数据的时候,不同的业务数据对应不同的Excel文件,很难做到统一,但如果是每个业务模型,都创建一个不同的导入界面来操作Excel数据,又会觉得可能某种程度上重复劳动,增加开发及维护成本。

那么有无一种介于两者之间的方法,来实现效率的最优化,并且能够统一利用好一个导入的界面呢,在开发领域,只要能想到的,一般也能做到,由于工作的需要,在我的
Winform开发框架
中引入了一个通用的数据导入模块,来实现这个既是统一,又是变化的业务需求,首先我们来看看能大致的模块功能介绍图,如下所示。

然后我们再来看看实际的导入模块操作界面,如下图所示

在最底的状态栏里面,但我们保存数据的时候,会调用后台线程进行数据保存,并显示数据导入的进度状态,由于是采用后台线程处理,不会阻塞当前的界面,在多文档的
Winform开发框架
界面中,可以切换到其他业务界面进行其他处理,不影响整体界面操作。

既然是导入界面统一,它肯定封装了一些常规操作,同时提供一些属性或者接口给外部调用对象进行操作,这样才能实现有机的统一,我们来看看具体的实现代码是如何的。

1)定义事件处理

        public delegate boolSaveDataHandler(DataRow dr);public eventSaveDataHandler OnDataSave;public event EventHandler OnRefreshData;

首先我们定义一个数据保存(单行)的处理事件,然后也定义一个数据保存后,刷新主体列表的数据刷新事件,这两个都是提供给调用者实现的逻辑。

我们在这个通用的导入数据窗体,需要的就是利用后台线程调用整个逻辑进行处理数据的导入及后续的刷新操作,如下所示。

        private BackgroundWorker worker = null;public delegate boolSaveDataHandler(DataRow dr);public eventSaveDataHandler OnDataSave;public eventEventHandler OnRefreshData;publicFrmImportExcelData()
{
InitializeComponent();
this.gridView1.OptionsBehavior.AutoPopulateColumns = true;
worker
= newBackgroundWorker();
worker.WorkerReportsProgress
= true;
worker.ProgressChanged
+= newProgressChangedEventHandler(worker_ProgressChanged);
worker.DoWork
+= newDoWorkEventHandler(worker_DoWork);
worker.RunWorkerCompleted
+= newRunWorkerCompletedEventHandler(worker_RunWorkerCompleted);
}
void worker_ProgressChanged(objectsender, ProgressChangedEventArgs e)
{
this.progressBar1.Value =e.ProgressPercentage;
}
void worker_RunWorkerCompleted(objectsender, RunWorkerCompletedEventArgs e)
{
this.progressBar1.Visible = false;this.progressBar1.Value = 0;if (OnRefreshData != null)
{
OnRefreshData(
null, null);
}
string tips = e.Result as string;if (!string.IsNullOrEmpty(tips))
{
MessageDxUtil.ShowTips(tips);
if (tips == "操作成功")
{
this.gridControl1.DataSource = null;
}
}
}

2)设置显示不同的模板文件

由于导入数据操作要应用于不同的业务数据,那么他们的模板肯定也不同,因此需要提供一个接口给外部,实现模板文件的修改及打开操作。

        /// <summary>
        ///设置导入模板标题,及文件路径/// </summary>
        /// <param name="title"></param>
        /// <param name="filePath"></param>
        public void SetTemplate(string title, stringfilePath)
{
this.lnkExcel.Text =title;this.lnkExcel.Tag =filePath;
}
private void lnkExcel_LinkClicked(objectsender, LinkLabelLinkClickedEventArgs e)
{
try{string templateFile = this.lnkExcel.Tag.ToString();
Process.Start(templateFile);
}
catch(Exception)
{
MessageDxUtil.ShowWarning(
"文件打开失败");
}
}

3)显示Excel数据

我们在数据导入的时候,最好提供一个数据的显示界面给客户,方便对导入数据的核对,这样可以提高体验效果以及对数据的核对操作,减少出错的几率。具体的实现代码如下所示。数据显示的操作,可以通过操作Excel数据库的方式进行读取,然后显示数据。(其中有些接口API来自我的
共用类库
,需要可以到我的随笔中了解相关的类库使用。

      private void btnBrowse_Click(objectsender, EventArgs e)
{
string file =FileDialogHelper.OpenExcel();if (!string.IsNullOrEmpty(file))
{
this.txtFilePath.Text =file;

ViewData();
}
}
private voidViewData()
{
if (this.txtFilePath.Text == "")
{
MessageDxUtil.ShowTips(
"请选择指定的Excel文件");return;
}
try{string connectString = string.Format(connectionStringFormat, this.txtFilePath.Text);string firstSheet =ExcelHelper.GetExcelFirstTableName(connectString);

myDs.Tables.Clear();
myDs.Clear();
this.gridControl1.DataSource = null;

OleDbConnection cnnxls
= newOleDbConnection(connectString);
OleDbDataAdapter myDa
= new OleDbDataAdapter(string.Format("select * from [{0}]", firstSheet), cnnxls);
myDa.Fill(myDs,
"【导入表】");this.gridControl1.DataSource = myDs.Tables[0];this.gridView1.PopulateColumns();
}
catch(Exception ex)
{
MessageBox.Show(ex.Message);
}
}

4)调用者给出保存数据的逻辑

由于是通用的数据导入操作,因此公用的导入界面,只能抛出相应的事件给外部进行数据保存的逻辑处理,数据导入页面只需要负责总体逻辑,具体的保存逻辑交给调用者实现,这样各司其职,共同把事情做好。下面是调用者(药品信息显示窗体中),对数据导入的操作逻辑实现。我们可以看到,它需要指定模板文件、数据刷新操作、数据保存操作,其他的交给通用数据导入界面进行处理即可。

       private string moduleName = "药品目录";private void btnImport_Click(objectsender, EventArgs e)
{
string templateFile = string.Format("{0}-模板.xls", moduleName);
FrmImportExcelData dlg
= newFrmImportExcelData();
dlg.SetTemplate(templateFile, System.IO.Path.Combine(Application.StartupPath, templateFile));
dlg.OnDataSave
+= newFrmImportExcelData.SaveDataHandler(ExcelData_OnDataSave);
dlg.OnRefreshData
+= newEventHandler(ExcelData_OnRefreshData);
dlg.ShowDialog();
}
void ExcelData_OnRefreshData(objectsender, EventArgs e)
{
BindData();
}
boolExcelData_OnDataSave(DataRow dr)
{
bool success = false;
DrugDetailInfo info
= newDrugDetailInfo();
info.DrugNo
= dr["药品编码"].ToString();
info.DrugName
= dr["药品名称"].ToString();
info.Manufacture
= dr["制造商"].ToString();
info.Formulations
= dr["剂型"].ToString();
info.Specification
= dr["规格"].ToString();
info.Unit
= dr["药品单位"].ToString();
info.Note
= dr["备注信息"].ToString();
info.StockQuantity
= ConvertHelper.ToInt32(dr["库存量"].ToString(), 0);

info.EditTime
=DateTime.Now;
info.Editor
=Portal.gc.LoginInfo.Name;
info.Dept_ID
=Portal.gc.LoginInfo.Dept_ID;
success
= BLLFactory<DrugDetail>.Instance.Insert(info);returnsuccess;
}

到这里,通用数据导入的操作基本上就结束了,我的处理方式是否和你的想法吻合呢,或者有更好的实现方式?

不过大家的总体思想,肯定是殊途同归,抽象封装统一的部分,并提供个性化的逻辑给外部进行处理,这样就可以实现综合的统一,提高整体的使用效率,较少今后维护的成本。

在这里顺便说一下,数据导出的操作,因为既然有导入,应该也有导出,所以我们也需要实现。它的操作代码不是很复杂,只需要把数据按照导入模板约定的字段名称导出即可,记得要和导入模板一致。

       private void btnExport_Click(objectsender, EventArgs e)
{
string file = FileDialogHelper.SaveExcel(string.Format("{0}.xls", moduleName));if (!string.IsNullOrEmpty(file))
{
List
<DrugDetailInfo> list = BLLFactory<DrugDetail>.Instance.GetAll();
DataTable dtNew
= DataTableHelper.CreateTable("序号|int,药品编码,药品名称,制造商,剂型,规格,药品单位,备注信息,库存量");
DataRow dr;
for (int i = 0; i < list.Count; i++)
{
dr
=dtNew.NewRow();
dr[
"序号"] = i + 1;
dr[
"药品编码"] =list[i].DrugNo;
dr[
"药品名称"] =list[i].DrugName;
dr[
"制造商"] =list[i].Manufacture;
dr[
"剂型"] =list[i].Formulations;
dr[
"规格"] =list[i].Specification;
dr[
"药品单位"] =list[i].Unit;
dr[
"备注信息"] =list[i].Note;
dr[
"库存量"] =list[i].StockQuantity;
dtNew.Rows.Add(dr);
}
try{string error = "";
AsposeExcelTools.DataTableToExcel2(dtNew, file,
outerror);if (!string.IsNullOrEmpty(error))
{
MessageDxUtil.ShowError(
string.Format("导出Excel出现错误:{0}", error));
}
else{if (MessageDxUtil.ShowYesNoAndTips("导出成功,是否打开文件?") ==System.Windows.Forms.DialogResult.Yes)
{
System.Diagnostics.Process.Start(file);
}
}
}
catch(Exception ex)
{
LogHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}
}

以上就是我的通用数据导入导出操作,其实利用代码生成工具Database2Sharp,选定表后,自动生成的WInform界面中,就已经自动生成以上导入、导出Excel的功能代码了,已经极大简化了重复输入代码的可能性了,只需要把界面调整一下就基本上OK了,以上一家之言,欢迎拍砖或者共同探讨。

记得我在很早之前,开始介绍我的
Winform开发框架
和我的WCF开发框架之初,我曾经给出下面的视图,介绍我整理的一个框架体系,其中包含有WInform开发框架以及我的Web开发框架,由于前段时间一直忙于Winform开发框架的提炼以及优化,并统一整理了很多Winform开发框架以及WCF开发框架的随笔文章。随着我的Winform逐步完善,终于有时间来整理介绍我的Web开发框架的事宜了,下面先介绍一下我最新优化整理的Web开发框架之权限管理系统,其中这个权限管理系统可以说是集众多宠爱于一身了,除了一贯的和代码生成工具集成,可生成基础性的框架代码外,还整合Winform开发框架继承而来的多数据库支持,在界面层,也就是Web权限管理系统,整合了JQuery的Easy-UI界面组件,功能强大的zTree控件、KindEditor在线编辑控件、界面层获取数据及保存使用基于JQuery的json数据操作,实现数据局部刷新等等操作,其中提供两种不同的菜单布局操作,非常方便应用于其他业务系统的界面。

首先在介绍之前,我们来贴几个Web权限系统的图片进行感性的了解先。

1)简洁的界面布局效果。这种效果顶部横幅比较紧凑,左边有一些常用的按钮操作,适合于菜单功能不太多的小业务系统,如我的Web权限系统。

2)功能强大的界面企业业务系统布局。这种界面效果适合于功能比较众多,菜单展示进行分类管理等业务系统。这种框架顶部的菜单为一级菜单,单击一级菜单可以在左边展示二级菜单,这种效果可以不用一次性列出所有系统的功能,而是分层次进行功能展示。


顶部的Tab选项卡每次打开页面的时候,增加一个Tab页,页面可以双击进行关闭,也可以右键弹出菜单进行更多操作,如下图所示。

如果需要了解整个系统的效果,也可以下载《
Web权限系统操作视屏
》进行全面的了解。

在提炼优化这个Web权限框架的过程中,碰到了不少的问题,一一进行解决,现总结一部分进行进行介绍。

1)使用JSON数据构造zTree

使用zTree确实比easy-ui自带的Tree好很多,功能也强大很多,由于我的Web权限中,各个模块几乎都需要树控件来展示相关的数据,如功能、组织机构等等。在对比了我自己的传统Tree、Easy-UI的Tree控件以及zTree后,发现使用zTree还是可以提高不少的界面分数的。但是其自带的例子,以及网上的例子,多数是使用预先弄好的树形数据,而我需要动态使用ashx进行树形数据的获取及生成,这确实费了一些周折来进行调试。

首先要准备基于ashx处理程序生成的Tree数据,树的数据使用JSON格式。如功能定义的树形数据如下所示。

        /// <summary>
        ///递归获取树形信息/// </summary>
        private string GetTreeJson(int PID, string folderIcon, stringleafIcon)
{
string condition = string.Format("PID={0}", PID);
List
<FunctionInfo> nodeList = BLLFactory<Function>.Instance.Find(condition);
StringBuilder content
= newStringBuilder();foreach (FunctionInfo model innodeList)
{
int ParentID = (model.PID == -1 ? 0: model.PID);//string tempMenu = string.Format("{{ id:{0}, pId:{1}, name:\"{2}\",icon:\"{3}\" }},", model.ID, ParentID, model.Name, imgsrc); //简单的作法 string subMenu = this.GetTreeJson(model.ID, folderIcon, leafIcon);string parentMenu = string.Format("{{ \"id\":{0}, \"pId\":{1}, \"name\":\"{2}\"", model.ID, ParentID, model.Name);if (string.IsNullOrEmpty(subMenu))
{
if (!string.IsNullOrEmpty(leafIcon))
{
parentMenu
+= string.Format(",\"icon\":\"{0}\" }},", leafIcon);
}
else{
parentMenu
+= "},";
}
}
else{if (!string.IsNullOrEmpty(folderIcon))
{
parentMenu
+= string.Format(",\"icon\":\"{0}\" }},", folderIcon);
}
else{
parentMenu
+= "},";
}
}

content.AppendLine(parentMenu.Trim());
content.AppendLine(subMenu.Trim());
}
returncontent.ToString().Trim();
}

然后就是页面的调用了,这里为了增强体验效果,使用了基于javascript的JQuery的异步操作进行数据处理,而不是基于aspx后台页面的处理,如下所示。

        //重新加载树形结构(异步)
function reloadTree() {
$(
"#loading").show();
$.getJSON(
"http://www.cnblogs.com/AjaxHandler/FunctionJson.ashx?r=" + Math.random() + "&op=tree", function (json) {
$.fn.zTree.init($(
"#treeDemo"), setting, json);
$.fn.zTree.getZTreeObj(
"treeDemo").expandAll(true);var treeObj = $.fn.zTree.getZTreeObj("treeDemo");var treeNodes =treeObj.getNodes();if (treeNodes != null) {
loadData(treeNodes[
0].id);
}
});
$(
"#loading").fadeOut(500);
}

这样处理的效果是页面只是局部刷新,体验很好。

2)基于JQuery的数据加载及保存操作

由于JQuery的方便性及良好体验性,我统一了数据的获取及保存操作。下面给出相关的处理代码供参考。

        //加载制定的对象数据
function loadData(id) {
$(
"#loading").show();
$.getJSON(
"http://www.cnblogs.com/AjaxHandler/FunctionJson.ashx?r=" + Math.random() + "&op=findbyid&id=" +id, function (json) {
$(
"#txtID").val(json.ID);
$(
"#txtName").val(json.Name);
$(
"#txtControlID").val(json.ControlID);
$(
"#txtPID").val(json.PID);
});

$(
'#lbxRoles').empty();
$.getJSON(
"http://www.cnblogs.com/AjaxHandler/RoleJson.ashx?r=" + Math.random() + "&op=getrolesbyfunction&id=" +id, function (json) {
$.each(json, function (i, item) {
$(
'#lbxRoles').append('<option value="' + item.ID + '">' + item.Name + '</option>');
});
});
$(
"#loading").fadeOut(500);
}
//保存对象数据 function saveData() {
$.ajax({
type:
'POST',
url:
'http://www.cnblogs.com/AjaxHandler/FunctionJson.ashx?r=' + Math.random() + '&op=insert',
async:
false,
data: { ID: $(
"#txtID").val(), Name: $("#txtName").val(), ControlID: $("#txtControlID").val(), PID: $("#txtPID").val() },
success: function (id) {
alert(
"操作成功!");
reloadTree();
if (id != "")
loadData(id);
},
error: function (xhr, status, error) {
alert(
"操作失败"); //xhr.responseText }
});
}

3)在数据操作等待的时候,页面中间显示Loading效果。

整个系统,在各种请求操作,我都统一了作法,在界面显示Loading的等待效果,任务结束后关闭,这种效果在费事的操作,用户体检会好一些,下面看看其效果以及实现代码。

增加下面javascript脚本

        //对象居中的函数,调用例子:$("#loading").center();
        jQuery.fn.center = function() {this.css("position", "absolute");this.css("top", Math.max(0, (($(window).height() - this.outerHeight()) / 2) +$(window).scrollTop())+ "px");this.css("left", Math.max(0, (($(window).width() - this.outerWidth()) / 2) +$(window).scrollLeft())+ "px");return this;
}
//初始化对象 $(document).ready(function() {

$(
"#loading").center();//loading的图片显示居中 });

然后再页面Body后面增加一行代码即可(默认loading图片不显示的哦)。

    <divid="loading"style="display: none;"><imgalt="数据正在加载中..."src="http://images.cnblogs.com/loading02.gif" /></div>

4)控件数据清空操作

由于添加和编辑公用界面控件元素,因此我们在要添加数据的时候,需要清空或者设置某些控件的值,但我们的控件可能比较多,一种好的方法是利用JQuery的选择器功能来进行有目的的控件清空操作。

如下面的例子所示。

        //新增清空控件
        functionaddData() {
$(
"#txtPID").val($("#txtID").val());
$(
"input[type=text][id*='txt']").val("");
$(
"textarea[id*='txt']").empty();
$(
"select[id*='lbx']").empty();
}

其中$("#txtPID").val($("#txtID").val());是把当前的用户作为添加数据的上级,其他的就是清空控件的数据了,不同的类型控件清空的步骤有些不同。

5)Ashx处理程序的安全性考虑

我们在系统中,多数都是调用ashx进行数据处理,虽然一般业务系统在VPN或者内网中运行,但是也要考虑用户没登陆的时候,不运行调用ashx程序,这样可以提高数据的安全性。

默认的ashx处理程序是没有Session的操作的,所以我们需要修改其继承接口(多增加
IReadOnlySessionState
的继承)

,然后才能调用Session来进行判断。

    /// <summary>
    ///权限功能操作类/// </summary>
    [WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo
=WsiProfiles.BasicProfile1_1)]public classFunctionJson : IHttpHandler, IReadOnlySessionState
{
public voidProcessRequest(HttpContext context)
{
//类对象要显式的实现接口 IReadOnlySessionState,才能访问Session数据 if (context.Session["Identity"] == null)
{
throw new ArgumentException("用户未登录!");
}

6)Tab界面布局的兼容。

Web权限系统提供了两种常用的菜单布局进行管理,一般对于常用的业务系统肯定是没问题了。

另外一种效果是系统复杂的时候用的,可以定义一级菜单和关联的二级菜单。

对于后者,一级菜单打开的时候,可以关联打开一个新的页面,并且刷新二级菜单的关系。

        <!----------- 一级导航 ------------------>
        <ulclass="navigation"style="display:block">
            <li><ahref="#"onclick="showSubMenu('ListUser.aspx', '用户管理', 'default')">权限管理</a></li>
            <li><ahref="#"onclick="showSubMenu('../Commonpage/MyJob.aspx', '事务中心开发中...', 'point', 'icon-organ')">事务中心</a></li>
            <li><ahref="#"onclick="showSubMenu('../Commonpage/building.htm', '合同起草开发中...', '')">合同起草</a></li>
            <li><ahref="#"onclick="showSubMenu('../Commonpage/building.htm', '合同管理开发中...', '')">合同管理</a></li>
            <li><ahref="#"onclick="showSubMenu('../Commonpage/building.htm', '查询打印开发中...', '')">查询打印</a></li>
            <li><ahref="#"onclick="showSubMenu('../Commonpage/building.htm', '知识管理开发中...', '')">知识管理</a></li>
            <li><ahref="#"onclick="showSubMenu('../Commonpage/building.htm', '系统管理开发中...', '')">系统管理</a></li>
        </ul>

好了,很多其他的特点,以后继续介绍,欢迎多多提出宝贵意见。

在项目开发中,往往会碰到一些非常规的需求,每次碰到这种情况,都需要花费时间来整理自己的思路,然后参考网络上其他人的实现方式或者作法,有时候可以找到一些相同的模块进行改进即可符合需求,但往往很多是需要自己潜心研究,然后提炼优化,虽然探索过程还是比较开心,不过时间肯定是需要花不少的。我每次碰到这种情况,都会沉下心来,力求把这种的模块做得更好一点,方便以后的重用,这样每次抱着这样的态度,着实积累了不少好的东西,也可以为后面的项目夯实基础。

在一次Winform的项目开发过程中,客户需要对一些体检数据等参数进行曲线展示,其实图表控件有很多,但是我印象比较深的还是开源的
ZedGraph控件
,这个既可以用在Web上,也可以用在Winform上的开源控件,有着简单易用的特点,因此我会先考虑是否可以满足要求。在需求中,我需要定制显示曲线报表的内容,按照每行一个人员的数据,然后再每行中展现该人员的相关图表信息,而且这种的报表要可以实现打印的功能。在经过一段时间的摸索及实现提炼,得到了比较满意的效果,先来进行总体的介绍先把。

通过把好的思路,有用的技巧进行积累整合到Winform开发框架中,方便自己,也方便别人,提高工作效率。

1、按每行一个用户的数据显示曲线报表图形

一行一个人员的曲线数据,可以对同一曲线项目进行对比,方便用户的实际业务对比操作。

2、每个曲线图形可以双击打开,进行放大缩小的操作,方便用户查看。

由于在一个界面中展示多个图表图形的时候,会比较小,为了更直观显示曲线数据,可以通过单独打开一个新的窗口进行曲线缩放操作,支持鼠标的滚动放大缩小,同时
ZedGraph
支持绘图点的信息提示,非常友好。

3、提供自定义打印及文档导出功能

由于用户控件是自定义组装的,因此要实现自定义的打印功能才可以,这个自定义打印的东西确实需要慢慢测试研究,这个地方花了不少时间。

通过在DevExpress打印界面中展示预览效果,方便可以进行打印确认操作,以及预览最终的效果,并支持把文档导出到PDF或者图片中,非常方便易用。

以上就是这个曲线报表的主要几个特点,不过这样的曲线,基本上能够满足我们日常的一些数据曲线的展现的了。

实现上我们需要把需求和界面拆分,首先我们在一个设计主界面,在住界面上防止一个TableLayout的布局控件,方便我们动态添加每个单一的控件进去。

4、曲线报表具体实现过程及思路

1)设计报表显示主界面

2)设计曲线报表组件

然后设计一个空白的布局FlowLayout控件,用来摆放一个或者多个的曲线报表项目,例如体重曲线、视力曲线、血压曲线等项目的,实现代码如下所示。

        public voidBindData()
{
switch(CurveData.CurveType)
{
caseCurveType.体重:
BindWeight();
break;caseCurveType.身长:
BindHeight();
break;caseCurveType.脉搏:
BindPulse();
break;caseCurveType.血压:
BindBlood();
break;caseCurveType.视力:
BindSight();
break;caseCurveType.暗适应时间:
BindDarkAdapTime();
break;caseCurveType.体温:
BindTemperature();
break;caseCurveType.全部:#region 全部 if (CurveData.CheckType ==CheckType.季度小体检)
{
BindWeight();
BindPulse();
BindBlood();
BindSight();
BindDarkAdapTime();
}
else if (CurveData.CheckType ==CheckType.年度大体检)
{
BindWeight();
BindHeight();
BindPulse();
BindBlood();
BindSight();
}
else if (CurveData.CheckType ==CheckType.飞行前体检)
{
BindTemperature();
BindPulse();
BindBlood();
}
break;#endregion}
}
        private voidBindWeight()
{
DataTable dt
= null;if (CurveData.CheckType ==CheckType.年度大体检)
{
dt
= BLLFactory<LargeCheckSurgical>.Instance.GetWeightData(CurveData.StartDate,
CurveData.EndDate, CurveData.PilotID);
}
else if (CurveData.CheckType ==CheckType.季度小体检)
{
dt
= BLLFactory<SmallCheck>.Instance.GetWeightData(CurveData.StartDate,
CurveData.EndDate, CurveData.PilotID);
}

WeightCurve curve
= newWeightCurve();
curve.CurveData
=CurveData;
curve.dataTable
=dt;this.layoutPanel1.Controls.Add(curve);
}

其他代码不在赘述。

3)设计曲线项目组件

由于曲线报表涉及很多展示的项目,每项又有一些不同,因此我们为不同的项目设计一个组件,如体重曲线如下所示,在一个自定义控件上面放置一个
ZedGraph
组件,设计好这个组件的相关属性和事件。

这个控件默认是英文的,所以如果需要使用中文菜单,需要自己汉化一下代码,然后编译出来自己使用即可。

实现代码如下所示

            GraphPane myPane =zgc.GraphPane;
myPane.CurveList.Clear();
//设置标题及坐标轴的说明 myPane.Title.Text = string.Format("【{0}】体重曲线", CurveData.PilotName);
myPane.XAxis.Title.Text
= "体检日期";
myPane.YAxis.Title.Text
= "体重(Kg)";

PointPairList list
= newPointPairList();foreach(DataRow row indataTable.Rows)
{
DateTime checkDate
= Convert.ToDateTime(row["CheckDate"].ToString());double x = (double) newXDate(checkDate);double y = Convert.ToInt32(row["Weight"].ToString());

list.Add(x, y);
}

LineItem myCurve
= myPane.AddCurve("体重", list, Color.Red, SymbolType.Diamond);
myCurve.Symbol.Fill
= new Fill(Color.White);

4)设计图表打印模块

打印的时候,需要自己在打印原件上进行图形的绘制,这一个是比较复杂的调试过程,开始总是想着是否可以把控件打印出来就OK,可是这种操作,一旦界面遮挡,就打印不出实际的效果了,所以只好类似绘图一样,使用自定义绘制方式。

这样我计算好每个控件的大小尺寸(包括Lable控件、曲线图表控件),然后挨着绘制即可,主要代码如下所示。

        protected virtual void DrawRow(BrickGraphics graph, int rowIndex, int col, Control ctrl, floatleft)
{
graph.BackColor
=Color.White;

RectangleF bounds
= new RectangleF(left, 0, ctrl.Width, ctrl.Height);
bounds.Y
= (rowIndex - 1) *bounds.Height;if (ctrl isLabel)
{
TextBrick brick
=graph.DrawString(ctrl.Text, bounds);
brick.HorzAlignment
=DevExpress.Utils.HorzAlignment.Center;
brick.VertAlignment
=DevExpress.Utils.VertAlignment.Center;const int LeftPadding = 4;
brick.Padding
= newPaddingInfo(LeftPadding, brick.Padding.Right, brick.Padding.Top, brick.Padding.Bottom);
}
else{int width =ctrl.Size.Width;int height =ctrl.Size.Height;
Bitmap bm
= newBitmap(width, height);
ctrl.DrawToBitmap(bm,
new Rectangle(0, 0, width, height));

ImageBrick brick
=graph.DrawImage(bm, bounds);
brick.SizeMode
=ImageSizeMode.ZoomImage;
}
}

好了,整个曲线报表的显示效果及实现思路及部分核心代码,都已经介绍完毕了,在整个过程中,除了经验外,我觉得最重要的就是要细心、耐心及用心,项目开发就是把各种技巧、各种思路都集中起来,才可以快速高效的开发出高质量、客户反映好的项目出来。

在业务系统的操作过程中,有时候,用户需要切换用户进行重新登录,这种情况有时候是因为一个人管理多个用户账号,希望通过不同的账号登录进行管理不同的资料,另一种情况是酒店的换班操作,另一个人接替前面的人进行系统维护管理。这种重新登录其实也是一种友好的操作之一,试想一下,换个账号登录,就需要推出系统,重新寻找运行程序才可以,而且如果系统启动较慢一点的,还需要等待,所以实现重新登录,有时候也是必要的。因此实现这个功能,也是体现我们开发的系统注重细节的表现。

另外,自动登录(其实是接受通过命令行参数进行登录)也是很常见的,有时候,让客户端记住用户的账号密码,我们在后台通过调动命令行方式进行登录,让系统程序接收到相关的参数值即可进行登录了。

1、系统重新登录实现

大致的思路,就是登录系统后,在系统菜单中有一项重新登录的功能入口,单击可以要求客户重新输入密码进行登录,如下所示。

代码实现就是通过把初始化的时候,用户相关的操作放到一个函数里面,保证重新执行这个函数操作就能重新刷新登录用户信息即可。如下所示。

在InitUserRelated函数里面,我们把用户相关的初始化操作放在里面,其中包括显示登录用户信息、用户可操作按钮或者菜单、首页信息等相关项目,代码如下所示。

        /// <summary>
        ///初始化用户相关的系统信息/// </summary>
        private voidInitUserRelated()
{
ChildWinManagement.LoadMdiForm(
this, typeof(FirstPage));//欢迎页面 #region 初始化系统名称 try{string Manufacturer = config.AppConfigGet("Manufacturer");string ApplicationName = config.AppConfigGet("ApplicationName");string AppWholeName = string.Format("{0}-{1}", Manufacturer, ApplicationName);
Portal.gc.gAppUnit
=Manufacturer;
Portal.gc.gAppMsgboxTitle
=AppWholeName;
Portal.gc.gAppWholeName
=AppWholeName;this.Text = AppWholeName + " ";this.notifyIcon1.BalloonTipText =AppWholeName;this.notifyIcon1.BalloonTipTitle =AppWholeName;this.notifyIcon1.Text =AppWholeName;string userName =Portal.gc.LoginInfo.RealName;if (string.IsNullOrEmpty(userName))
{
userName
=Portal.gc.LoginInfo.Name;
}
UserStatus
= string.Format("当前用户:{0}({1})", userName, Portal.gc.RoleInfo.RoleName);
CommandStatus
= string.Format("欢迎使用 {0}", Portal.gc.gAppWholeName);
}
catch{ }#endregionInitAuthorizedUI();//根据权限屏蔽 InitSkinGallery();
UserLookAndFeel.Default.SetSkinStyle(
"Office 2010 Blue");
}

其中
InitAuthorizedUI
就是判断用户有哪些权限的函数,根据权限系统获取到的功能点,在这里对界面元素进行重新刷新,有权限的就显示,没有的就隐藏即可,如下所示。

        /// <summary>
        ///根据权限屏蔽功能/// </summary>
        private voidInitAuthorizedUI()
{
this.tool_Report.Enabled = Portal.gc.HasFunction("Report");this.tool_Dict.Enabled = Portal.gc.HasFunction("Dictionary");this.tool_ItemDetail.Enabled = Portal.gc.HasFunction("ItemDetail");this.tool_Purchase.Enabled = Portal.gc.HasFunction("Purchase");this.tool_StockSearch.Enabled = Portal.gc.HasFunction("StockSearch");this.tool_TakeOut.Enabled = Portal.gc.HasFunction("TakeOut");this.tool_WareHouse.Enabled = Portal.gc.HasFunction("WareHouse");//this.menu_run_systemLog.Enabled = Portal.gc.HasFunction("LoginLog"); this.tool_Settings.Enabled = Portal.gc.HasFunction("Parameters");this.tool_MonthlyStatistic.Enabled = Portal.gc.HasFunction("MonthlyStatistic");this.tool_AnnualStatistic.Enabled = Portal.gc.HasFunction("AnnualStatistic");this.tool_ClearAll.Enabled = Portal.gc.HasFunction("ClearAllData");this.tool_ImportItemDetail.Enabled = Portal.gc.HasFunction("ImportItemDetail");
}

这样封装好后,我们需要重新登录就方便了,我们在重新登录的菜单操作里面,实现代码如下所示。

        private void btnRelogin_ItemClick(objectsender, DevExpress.XtraBars.ItemClickEventArgs e)
{
if (MessageDxUtil.ShowYesNoAndWarning("您确定需要重新登录吗?") !=DialogResult.Yes)return;


Portal.gc.MainDialog.Hide();

Login dlg
= newLogin();
dlg.StartPosition
=FormStartPosition.CenterScreen;if (DialogResult.OK ==dlg.ShowDialog())
{
if(dlg.bLogin)
{
CloseAllDocuments();
InitUserRelated();
}

}
dlg.Dispose();
Portal.gc.MainDialog.Show();
}

实现上面的操作过程,基本上就完成了重新登录的操作了。

2、系统自动登录实现

系统自动登录有时候很必要,在用户自己绝对信任的电脑上,自动登录对用户来说,很方便友好的,君不见,QQ如此、旺旺如此等等。其实实现思路就是通过给exe执行文件传递登录参数即可,必要时登录的参数值还可以进行加密,给第三方进行运行调用,以前就做过一个在Web上自动启动桌面程序Visio应用软件的操作,其实原理就是一样,通过传递参数给执行文件实现的。

[STAThread]static void Main(string[] args)
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(
false);if (args.Length > 0)
{
LoginByArgs(args);
}
else{
LoginNormal(args);
}
}
       /// <summary>
        ///使用参数化登录/// </summary>
        /// <param name="args"></param>
        private static void LoginByArgs(string[] args)
{
CommandArgs commandArgs
=CommandLine.Parse(args);if (commandArgs.ArgPairs.Count > 0)
{
#region 获取用户参数 string userName = string.Empty;string identity = string.Empty;foreach (KeyValuePair<string, string> pair incommandArgs.ArgPairs)
{
if ("U".Equals(pair.Key, StringComparison.OrdinalIgnoreCase))
{
userName
=pair.Value;
}
if ("P".Equals(pair.Key, StringComparison.OrdinalIgnoreCase))
{
identity
=pair.Value;
}
}
#endregion if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(identity))
{
bool bLogin =Portal.gc.LoginByIdentity(userName.Trim(), identity);if(bLogin)
{
ShowMainDialog();
}
else{
LoginNormal(args);
}
}
}
}

有时候,即使觉得用户不需要通过命令行登录,那么我们自己为了避免开发过程中,启动程序时候,总是需要输入用户账号密码的问题,也可以使用模拟自动登录的方式解决。

我们只需要在项目的属性里面输入内置的用户名密码,这样我们测试起来就不用登录那么麻烦了。

以上就是Winform开发框架中对于系统重新登录以及系统自动登录(命令行登录)的思想思路及方式,欢迎大家提供更好的思路及技巧,或者进行探讨,谢谢支持。