| |
-
常用链接
留言簿(13)
我参与的团队
随笔分类(19)
随笔档案(17)
文章分类
.net
积分与排名
最新评论

阅读排行榜
评论排行榜
Powered by: 博客园
模板提供:沪江博客
|
|
|
|
|
发新文章 |
|
|
2007年7月20日
在所有基于关系数据库的软件系统中, 有两种数据模式是最常用到的: 一种我称之为单表, 以微软的NorthWind例子数据库为例, 如Categories表是最基本的模式, 较为复杂一点的是Products表, 它的某些字段是外键, 其值来源于其它表。大部份基本资料适用于这种模式。 一种我称之为主从表,它的最基本模式由两个有一对多关系的表构成, 如Orders与Order Details。更复杂一点的模式可能是一个主表对应多个从表,或者从表之下又有从表。大部份单据比如入库单、领料单、财务凭证等适用于此种模式。 我们已经花太多时间去写针对这两种数据模式的 界面设计、 新增修改删除功能实现、 报表设计、 权限管理、 多语言支持等等。这些工作大部份都是重复的机械劳动。 Luna框架的功能是不需要再为这些做重复工作。开发者定义好数据模型和数据字典后,可以自动生成数据表、中间层代码、客户端界面。同时又可以保留充分的灵活性。开发者可以把绝大部份时间用于写业务逻辑。比如对于一张入库单,开发者需要写的代码是保存/或者审核这张单据时, 同时修改库存数量、库存成本。 这样的轮子市面上已经有很多了,但大多总有这样那样的缺点。我终于决定自已也来做一个。 这个是单表数据模式的界面示例:  标准工具栏为:新增、查询、编辑、删除、打印、四个位置导航、刷新、保存、撤消编辑。
2006年12月23日
上篇: MDI窗体的工具栏合并(ToolStrip Merging)这个模型的大概构成是这样的: 1个MDI主窗体,1~n个MDI子窗体; 主窗体上一般会有1个主菜单栏,1~n个主工具栏; 子窗体设计时上可能有0~1个主菜单栏,0~1个主工具栏,运行时子窗体的菜单栏和工具栏都显示在主窗体上,与主窗体的菜单栏和工具栏合并。(实际上也许会有多个工具栏,但先简化为一个工具栏的模型,实现了一个工具栏的合并后,要实现多个工具栏合并也并非难事)。 对于菜单栏的合并,无论是传统的Windows API编程或者Window Forms,都已经完美地解决了,不再赘述。 工具栏合并的过程最好在子窗体的激活和关闭处理处理,一个容易想到的方法是写在子窗体的事件代码中,要么需在每一个子窗体的代码文件写上类似处理的代码,要么让所有子窗体继承自一个写好相关事件处理代码的窗体。前者显然不可取,后者在单根继承的时代,剥夺了子窗体从其它窗体继承的可能,也不是一个好办法。同时还有一个弊端是子窗体要访问主窗体的内容,耦合度太高。 最佳方案也自然而然地出来了,即在主窗体的MdiChildActivate事件中处理(不能望文生义以为仅仅是子窗体的激活会触发此事件,事实上子窗体的关闭也会触发此事件)。这个时候应该把主窗体从上一个子窗体合并来的工具栏内容清除掉(如果有的话),并将新激活的子窗体上的待合并的工具栏内容合并到主窗体的工具栏上(如果新激活的子窗体上无待合并的工具栏内容或者MdiChildActivate是关闭最后一个子窗体触发的时候则无此步)。为简化代码,假设子窗体的工具栏总是合并到主窗体工具栏的最后位置。 这时候,主窗体必须知道子窗体的工具栏的构成,完全解耦是不太可能的。一种办法是将子窗体的工具栏控件的作用范围设置为public, 我采取了另外一个办法,就是定义了一个接口,让需合并工具栏的子窗口实现这个接口即可。 接口代码如下:
using System; using System.Collections.Generic; using System.Text; using System.Windows.Forms;
namespace Osmanthus.WinForms { interface IToolStripMergableForm {
ToolStrip MergedToolStrip { get; } } }
让子窗体实现这个接口,所要写的代码也简单,只有一句,即:
public ToolStrip MergedToolStrip { get { return this.toolStrip1; } }
剩下的内容似乎理想当然地是写主窗体的MdiChildActivate事件处理代码,不过我想增加重用性,就把它写成了一个组件,以后只要把该组件拖放在MDI主窗体,并设置此组件 MainToolStrip属性 为主窗体的工具栏(其实也可以写成自动获取默认属性值,即主窗体上的最后一个ToolStrip控件),不需对再对主窗体写一行代码。这个部件的代码内容如下(其中为实现组件对Form的存取参照了Chris Sells的大作,我不知道是否有其它的更好办法):
using System; using System.Collections.Generic; using System.Text; using System.Windows.Forms; using System.ComponentModel; using System.ComponentModel.Design;
namespace Osmanthus.WinForms { class SdiToolStripMerger : Component, ISupportInitialize { private Form hostingForm; private ToolStrip mainToolStrip; private IToolStripMergableForm currentMdiChild = null;
private List<ToolStripItem> toolStripItemList = new List<ToolStripItem>();
public ToolStrip MainToolStrip { get { return mainToolStrip; } set { mainToolStrip = value; } }
[BrowsableAttribute(false)] public Form HostingForm { get { if ((hostingForm == null) && this.DesignMode) { IDesignerHost designer = this.GetService(typeof(IDesignerHost)) as IDesignerHost; if (designer != null) { hostingForm = designer.RootComponent as Form; } } return hostingForm; } set { hostingForm = value; //这里需要改进一下, 应该一旦设置了host form,就不能再修改. } }
#region ISupportInitialize Members
public void BeginInit() { }
public void EndInit() { if ((!DesignMode) && (hostingForm != null)) { hostingForm.MdiChildActivate += new EventHandler(UpdateToolStrip); }; } #endregion
void UpdateToolStrip(object sender, EventArgs e) { //清除从上个激活子窗体合并来的工具栏内容 if (currentMdiChild != null) { foreach (ToolStripItem toolItem in toolStripItemList) { currentMdiChild.MergedToolStrip.Items.Add(toolItem); } toolStripItemList.Clear(); }
IToolStripMergableForm form = (IToolStripMergableForm)hostingForm.ActiveMdiChild; //如当前激活的子窗体实现了 IToolStripMergableForm接口,则合并其工具栏内容 if (form != null) {
currentMdiChild = form; foreach (ToolStripItem toolItem in currentMdiChild.MergedToolStrip.Items) { toolStripItemList.Add(toolItem); } foreach (ToolStripItem toolItem in toolStripItemList) { mainToolStrip.Items.Add(toolItem); } } else { currentMdiChild = null; toolStripItemList.Clear(); } } } }
2006/12/25 附记:今天早上发现了.net Framework 2.0新增加的一个类ToolStripManager,使用此类可以简化上述代码。
MDI一个比较显著的特性是菜单合并。即子窗体的菜单能合并到主窗体的菜单上。假想一下如果没有这个功能,主窗体和子窗体各显示一个菜单栏,那人机交互该有多糟糕。工具栏是一个与菜单栏同等重要的窗体界面元素,如果也能实现合并功能,将为MDI界面的应用开发增色不少。我们可以象典型的MDI应用一样,只将工具栏放在主窗口上,在主窗体的工具栏中放上全局通用的工具项,而针对特定子窗体的工具项在子窗体中设计,显示的时候与主窗体的工具栏合并显示。这样一则可以在做到界面的一致性的同时也将MDI主窗体和子窗体之间的耦合降到很低,其次在支持插件开发时也很有意义。 示例如下, 主窗体  显示某子窗体时:  显示另外一个子窗体时:  其中变化的工具项是在子窗体中设计的。虽然显示在主窗体的工具栏中,但还是从属于子窗体,在事件处理等操作中与原来并无二致。
2006年3月26日
我自已写的扩展TreeView控件, 实现了三态CheckBox和RadioButton:
2006年3月18日
2006年2月6日
五、写自已的日期格式化器
昨天看了一篇文章,说目前大家用的“农历”这个术语是文革时期才有的,目的是反封建。这里为了省事,还是继续使用这个术语。而英文名称ChineseLunisolarCalendar太长,我自己的代码中就用ChineseCalendar为相关功能命名,这个名字也还过得去吧。
我原先设想自定义一个类,使得能写出这样的代码:
string s= DateTime.Now.ToString(new MyFormatProvider());
就能得出我想要的农历日期字符串,经过测试却失败了,依据我的分析,微软公司在.net框架中把日期时间型的格式写死了,只能依据相关的地区采用固定的几种显示格式,没法再自行定义。而前文已经说过,而所有的相关格式微软公司都放到一个名为culture.nlp的文件中(这个文件在以前的.net框架是一个独立的文件,在.net 2.0被作为一个资源编译到mscorlib.dll中。) (我的这个不能为DateTime写自已的格式化器的观点没有资料佐证,如有不当之处,请大家指正)
虽然不能为DataTime写自定义的格式器,但还有另外一个途径,就是为String类的Format方法写自定义格式化器,我测试了一下,效果还不错,调用方式如下:
string s= String.Format(new ChineseCalendarFormatter(), "{0:D}",DateTime.Now);
可以得到“二〇〇六年正月初九”
string s= String.Format(new ChineseCalendarFormatter(), "{0:d}",DateTime.Now);
可以得到“丙戌年正月初九”
虽然没有前面所设想的方便,但也还能接受,全部代码帖出如下:
第一个类,主要是封装了农历的一些常用字符和对日历处理的最基本功能
using System; using System.Collections.Generic; using System.Text;
using System.Globalization;
public static class ChineseCalendarHelper { public static string GetYear(DateTime time) { StringBuilder sb = new StringBuilder(); int year = calendar.GetYear(time); int d; do { d = year % 10; sb.Insert(0, ChineseNumber[d]); year = year / 10; } while (year > 0); return sb.ToString(); }
public static string GetMonth(DateTime time) { int month = calendar.GetMonth(time); int year = calendar.GetYear(time); int leap = 0;
//正月不可能闰月 for (int i = 3; i <= month; i++) { if (calendar.IsLeapMonth(year, i)) { leap = i; break; //一年中最多有一个闰月 }
} if (leap > 0) month--; return (leap == month + 1 ? "闰" : "") + ChineseMonthName[month - 1]; }
public static string GetDay(DateTime time) { return ChineseDayName[calendar.GetDayOfMonth(time) - 1]; }
public static string GetStemBranch(DateTime time) { int sexagenaryYear = calendar.GetSexagenaryYear(time); string stemBranch = CelestialStem.Substring(sexagenaryYear % 10 - 1, 1) + TerrestrialBranch.Substring(sexagenaryYear % 12 - 1, 1); return stemBranch; }
private static ChineseLunisolarCalendar calendar = new ChineseLunisolarCalendar(); private static string ChineseNumber = "〇一二三四五六七八九"; public const string CelestialStem = "甲乙丙丁戊己庚辛壬癸"; public const string TerrestrialBranch = "子丑寅卯辰巳午未申酉戌亥"; public static readonly string[] ChineseDayName = new string[] { "初一","初二","初三","初四","初五","初六","初七","初八","初九","初十", "十一","十二","十三","十四","十五","十六","十七","十八","十九","二十", "廿一","廿二","廿三","廿四","廿五","廿六","廿七","廿八","廿九","三十"}; public static readonly string[] ChineseMonthName = new string[] { "正", "二", "三", "四", "五", "六", "七", "八", "九", "十", "十一", "十二" }; }
第二个类为自定义格式化器:
using System; using System.Collections.Generic; using System.Text;
using System.Globalization; using System.Threading;
public class ChineseCalendarFormatter : IFormatProvider, ICustomFormatter { //实现IFormatProvider public object GetFormat(Type formatType) { if (formatType == typeof(ICustomFormatter)) return this; else return Thread.CurrentThread.CurrentCulture.GetFormat(formatType); }
//实现ICustomFormatter public string Format(string format, object arg, IFormatProvider formatProvider) { string s; IFormattable formattable = arg as IFormattable; if (formattable == null) s = arg.ToString(); else s = formattable.ToString(format, formatProvider); if (arg.GetType() == typeof(DateTime)) { DateTime time = (DateTime)arg; switch (format) { case "D": //长日期格式 s = String.Format("{0}年{1}月{2}", ChineseCalendarHelper.GetYear(time), ChineseCalendarHelper.GetMonth(time), ChineseCalendarHelper.GetDay(time)); break; case "d": //短日期格式 s = String.Format("{0}年{1}月{2}", ChineseCalendarHelper.GetStemBranch(time), ChineseCalendarHelper.GetMonth(time), ChineseCalendarHelper.GetDay(time)); break; case "M": //月日格式 s = String.Format("{0}月{1}", ChineseCalendarHelper.GetMonth(time), ChineseCalendarHelper.GetDay(time)); break; case "Y": //年月格式 s = String.Format("{0}年{1}月", ChineseCalendarHelper.GetYear(time), ChineseCalendarHelper.GetMonth(time)); break; default: s = String.Format("{0}年{1}月{2}", ChineseCalendarHelper.GetYear(time), ChineseCalendarHelper.GetMonth(time), ChineseCalendarHelper.GetDay(time)); break; } } return s; } }
这段代码中间处理格式那部份稍做改进,就可以支持更多的日期格式。
有了这两段代码为原型,要实现计算和显示一个日期的农历日期及其它功能,基本上就很容易了。
2006年2月4日
三、农历类的使用
既然.net框架不支持直接将日期转换成农历格式的字符串,那么要将显示农历格式的日期,就只要自已写代码了。不过由于已经有了ChineseLunisolarCalendar类实现了公历转换为农历日期的功能,所以要写这样的代码也比较简单。需要用到ChineseLunisolarCalendar以下几个主要方法:
int GetYear (DateTime time) 获取指定公历日期的农历年份,使用的还是公历纪元。在每年的元旦之后春节之前农历的纪年会比公历小1,其它时候等于公历纪年。虽然农历使用传说中的耶稣生日纪元似乎不太妥当,不过我们确实已经几十年没有实行一个更好的纪年办法,也只有将就了。
int GetMonth (DateTime time) 获取指定公历日期的农历月份。这里要注意了,由于农历有接近三分之一的年份存在闰月,则在这些年份里会有十三个,而具体哪一个月是闰月也说不准,这里不同于希伯来历。以今年为例,今年闰七月,则此方法在参数为闰七月的日期是返回值为 8,参数为农历十二月的日期时返回值为13
bool IsLeapMonth ( int year, int month) 获取指定农历年份和月份是否为闰月,这个函数和上个函数配合使用就可以算出农历的月份了。
int GetDayOfMonth (DateTime time) 获取指定公历日期的农历天数,这个值根据大月或者小月取值是1到30或者1到29, MSDN上说的1到31显然是错的, 没有哪个农历月份会有31天。
int GetSexagenaryYear (DateTime time) 获取指定公历日期的农历年份的干支纪年,从1到60,分别是甲子、乙丑、丙寅、….癸亥, 比如戊戌变法、辛亥革命就是按这个来命名的。当然算八字也少不了这个。
int GetCelestialStem (int sexagenaryYear) 获取一个天支的天干, 从1到10, 表示甲、乙、丙….,说白了就是对10取模。
int GetTerrestrialBranch (int sexagenaryYear) ) 获取一个干支的地支,, 从1到12, 表示子、丑、寅、…今年是狗年,那么今年年份的地支就是“戌”。
有了这几个方法,显示某天的农历月份日期、农历节日等都是小菜一碟,算命先生排八字用这几个方法,又快又准确,写出的代码也很短。
四、几种东亚农历类的区别
经过我的测试,ChineseLunisolarCalendar, JapaneseLunisolarCalendar, KoreanLunisolaCalendarr, TaiwanLunisolarCalendar这四种日历,无论哪一种,以2006年2月6日为参数,调用它们的GetMonth方法得到的结果都是1,GetDayOfMonth得到的结果都是8。想想也是,我们过的端午节和韩国的不太可能不是一天。
但是调用GetYear方法得到结果就有区别了ChineseLunisolarCalendar和KoreanLunisolarCalendar都返回2006,也就是公历纪年,TaiwanLunisolarCalendar的返回值是95,依然是民国纪年,JapaneseLunisolarCalendar的返回值是18, 平成纪年。
另外的一个区别是这四种日历的MinSupportedDateTime和MaxSupportedDateTime各不一样,以下是对照表:
| 日历类 |
MinSupportedDateTime |
MaxSupportedDateTime |
| ChineseLunisolarCalendar |
公元1901年1月初1 |
公元2100年12月29 |
| TaiwanLunisolarCalendar |
民国1年1月初1 |
民国139年12月29 |
| JapaneseLunisolarCalendar |
昭和35年1月初1 |
平成61年12月29 |
| KoreanLunisolarCalendar |
公元918年1月初1 |
公元2050年12月29 |
韩国农历类支持的最小日期为918年(也即高丽王朝建立的年份),以此而论,中国农历类支持的最小日期不说从商周算起,从汉唐算总该没问题吧?微软公司啊,又在“厚彼薄此”,唉。
其次,日本还以天皇纪年,如果哪天xxxx, 岂不是使用JapaneseLunisolarCalendar写出的程序都有问题啦?
一、简介 过年是中国(以及日本、韩国等国)人民的第一大节日。你怎么知道哪天过年?查日历或者听别人说?程序员当然有程序员的办法,就是写程序啦。 虽然公历(俗称的“阳历”)已经成了全世界的通用标准,而且也具有多方面的优越性。但在东亚地区,还是离不开“农历”,春节、元宵、端午、中秋、重阳这些节日是农历的,大部份人的老爸老妈的生日也是农历的。 早在1.0框架出来的时候,我就认为微软公司不应该“厚彼薄此”,在.net框架中提供了希伯来历等,却没有提供更广泛使用的“农历”。 而在.net 2.0中,微软公司终于做出了这个小小的改进。 .net 2.0在System.Globalization命名空间中新增加了EastAsianLunisolarCalendar 类及以继承它的ChineseLunisolarCalendar, JapaneseLunisolarCalendar, KoreanLunisolarCalendar, TaiwanLunisolarCalendar等几个类。LunisolarCalendar顾名思义应为“阴阳历”,我的理解是因为我们所用的农历虽然按照月亮公转来编月份,但用“闰月”的方式来调整年份与地球公转的误差,严格意义上来说是结合了月亮公转和地球公转的成份,因此属于“阴阳历”。但我这里还是按照习惯称之为“农历” 。
二、新的农历类还是没有公民待遇 为了测试新的日历类,我兴冲冲地写了几句代码:(省略了调用这个方法的其它代码)
private string getDateString(DateTime dt) { CultureInfo ci = new CultureInfo("zh-CN"); ci.DateTimeFormat.Calendar = new ChineseLunisolarCalendar(); return dt.ToString("D",ci); }
运行报错,错误信息是:"Not a valid calendar for the given culture "
为了说明问题,继续测试
private string getDateString(DateTime dt) { CultureInfo ci = new CultureInfo("zh-TW"); ci.DateTimeFormat.Calendar = new TaiwanCalendar(); return dt.ToString("D",ci); }
可以正常运行,结果是95年x月x日(民国纪年),注释掉中间那条语句,结果是2006年x月x日(也就是使用公历),将中间那条语句修改成:ci.DateTimeFormat.Calendar = new TaiwanLunisolarCalendar(),照样出错。查相关资料,原来DateTimeFormat的Calendar属性只能为CultureInfo的OptionalCalendars属性所指定范围。
于是再写一段代码测试OptionalCalendars的内容,对于zh-CN语言,惟一可用于日期格式的calendar是本地化的GregorianCalendar(也就是公历)。对于zh-TW,可用于日期格式的calendar是美国英语和本地化的GregorianCalendar以及TaiwanCalendar(即公历的年份减1911),都没有包括农历。
也就是说.net2.0虽然提供了农历类,但对它的支持并不及同样有闰月的希伯来历。我查资料的时候找到了博客堂的一篇文章http://blog.joycode.com/percyboy/archive/2004/09/17.aspx ,作者在一年半以前发现了农历类不支持日期格式化的问题,并认为这是一个bug。当然还算不上bug,只不过微软没有重视而已(责任在微软吗?我想应该不是,在商业社会我们有多重视微软就会有多重视。和以色列比起来,我们对传统文化的重视程度差得太远)。
2005年11月10日
| |