Rovan

      一个犁牛半块田,收也凭天,荒也凭天, 清茶淡饭饱三餐,早也香甜,晚也香甜, 布衣得暖胜丝绵,长也可穿,短也可穿, 草舍茅屋有几间,行也安然,待也安然, 雨过天青驾小船,鱼在一边,酒在一边, 夜归儿女话灯前,今也有言,古也有言, 日上三竿我独眠,请是神仙,我是神仙.

首页 新随笔 联系 订阅 管理

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 - 11+ TerrestrialBranch.Substring(sexagenaryYear % 12 - 11);
        
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) 获取指定公历日期的农历天数,这个值根据大月或者小月取值是130或者129, MSDN上说的131显然是错的, 没有哪个农历月份会有31天。

int GetSexagenaryYear (DateTime time) 获取指定公历日期的农历年份的干支纪年,从160,分别是甲子、乙丑、丙寅、….癸亥, 比如戊戌变法、辛亥革命就是按这个来命名的。当然算八字也少不了这个。

int GetCelestialStem (int sexagenaryYear) 获取一个天支的天干, 110, 表示甲、乙、丙….,说白了就是对10取模。

int GetTerrestrialBranch (int sexagenaryYear) ) 获取一个干支的地支,, 112, 表示子、丑、寅、今年是狗年,那么今年年份的地支就是“戌”。

有了这几个方法,显示某天的农历月份日期、农历节日等都是小菜一碟,算命先生排八字用这几个方法,又快又准确,写出的代码也很短。
 

四、几种东亚农历类的区别

经过我的测试,ChineseLunisolarCalendar, JapaneseLunisolarCalendar, KoreanLunisolaCalendarr, TaiwanLunisolarCalendar这四种日历,无论哪一种,以200626日为参数,调用它们的GetMonth方法得到的结果都是1GetDayOfMonth得到的结果都是8。想想也是,我们过的端午节和韩国的不太可能不是一天。

但是调用GetYear方法得到结果就有区别了ChineseLunisolarCalendarKoreanLunisolarCalendar都返回2006,也就是公历纪年,TaiwanLunisolarCalendar的返回值是95,依然是民国纪年,JapaneseLunisolarCalendar的返回值是18, 平成纪年。

另外的一个区别是这四种日历的MinSupportedDateTimeMaxSupportedDateTime各不一样,以下是对照表:

日历类 MinSupportedDateTime MaxSupportedDateTime
ChineseLunisolarCalendar 公元19011月初1 公元21001229
TaiwanLunisolarCalendar 民国11月初1 民国1391229
JapaneseLunisolarCalendar 昭和351月初1 平成611229
KoreanLunisolarCalendar 公元9181月初1 公元20501229

韩国农历类支持的最小日期为918(也即高丽王朝建立的年份),以此而论,中国农历类支持的最小日期不说从商周算起,从汉唐算总该没问题吧?微软公司啊,又在“厚彼薄此”,唉。

其次,日本还以天皇纪年,如果哪天xxxx, 岂不是使用JapaneseLunisolarCalendar写出的程序都有问题啦?

 
 一、简介
过年是中国(以及日本、韩国等国)人民的第一大节日。你怎么知道哪天过年?查日历或者听别人说?程序员当然有程序员的办法,就是写程序啦。
虽然公历(俗称的阳历”)已经成了全世界的通用标准,而且也具有多方面的优越性。但在东亚地区,还是离不开农历”,春节、元宵、端午、中秋、重阳这些节日是农历的,大部份人的老爸老妈的生日也是农历的。
早在1.0框架出来的时候,我就认为微软公司不应该厚彼薄此,在.net框架中提供了希伯来历等,却没有提供更广泛使用的农历
而在.net 2.0中,微软公司终于做出了这个小小的改进。
.net 2.0System.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);
}

可以正常运行,结果是95xx(民国纪年),注释掉中间那条语句,结果是2006xx(也就是使用公历),将中间那条语句修改成:ci.DateTimeFormat.Calendar = new TaiwanLunisolarCalendar(),照样出错。查相关资料,原来DateTimeFormatCalendar属性只能为CultureInfoOptionalCalendars属性所指定范围。

于是再写一段代码测试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月9日

 在写这篇的正文之前,我要衷心感谢微软公司的Jeffrey Tan先生,他居然看懂了我的文理不通的英文提问,并且花费宝贵时间来钻研,帮我解决了一个星期来我百思不得其解的问题。我当时的问题是我写了一些语句来实现标准菜单命令,但是调试的时候怎么也得不到正确的结果,事实上我研究的两个例子用的方法和我的类似,它们却都可以正常执行。我一直没有找到原因,不得不求助支持。

 在“Form设计器尝试() 修改窗体上的控件属性”我提出了怎么删除控件的问题,我拙作的关注者山伟也提出过用什么方法实现控件对齐更简便。所有这些问题的答案是使用MenuCommandService, .net 1.0/1.1的办法是手工写一个实现IMenuCommandService接口的类,将其实例添加到服务容器中,而.net 2.0已经为我们提供了MenuCommandService

 打开主窗体代码界面,为Form1添加一个私有变量:

private MenuCommandService menuCommandService;
 修改Load事件代码,由于我们已经改了几次了,所以我这里全部帖出:
private void Form1_Load(object sender, EventArgs e)
{
    DesignSurface surface 
= new DesignSurface();

    toolBoxService 
= new DemoToolboxService();
    toolBoxService.ToolBox 
= new ListBox();

    toolBoxService.ToolBox.Items.Add(
"Point");

    toolBoxService.ToolBox.Items.Add(
new ToolboxItem(typeof(Button)));
    toolBoxService.ToolBox.Items.Add(
new ToolboxItem(typeof(TextBox)));
    toolBoxService.ToolBox.Items.Add(
new ToolboxItem(typeof(Label)));
    toolBoxService.ToolBox.Items.Add(
new ToolboxItem(typeof(TabControl)));
    toolBoxService.ToolBox.Items.Add(
new ToolboxItem(typeof(StatusBar)));

   
// Assembly a1=Assembly.LoadFrom(@"D:\Dotnet\MyControl.dll");
   
// toolBoxService.ToolBox.Items.Add(new ToolboxItem(a1.GetType("MyControl.ComboBoxField")));

    toolBoxService.ToolBox.Dock 
= DockStyle.Fill;
    
this.panel1.Controls.Add(toolBoxService.ToolBox);


    IServiceContainer container 
= surface.GetService(typeof(IServiceContainer)) as IServiceContainer;

    menuCommandService 
= new MenuCommandService(surface);

    
if (container != null)
    
{
        container.AddService(
typeof(IToolboxService), toolBoxService);
        container.AddService(
typeof(IMenuCommandService), menuCommandService);
    }


    surface.BeginLoad(
typeof(Form));
    Control view 
= (Control)surface.View;
    view.Dock 
= DockStyle.Fill;
    
this.splitContainer1.Panel1.Controls.Add(view);
    
this.propertyGrid1.SelectedObject = surface.ComponentContainer.Components[0];


    selectionService 
= surface.GetService(typeof(ISelectionService)) as ISelectionService;
    selectionService.SelectionChanged 
+= new EventHandler(selectionService_SelectionChanged);
}

 

我们以删除功能来做测试,在主窗体的设计界面上添加一个MenuStrip,  MenuStrip上添加Edit菜单项,在Edit菜单项下添加子菜单Delete, 设置其快捷键为Del, Delete菜单项写事件代码:

private void deleteToolStripMenuItem_Click(object sender, EventArgs e)
{
    menuCommandService.GlobalInvoke(StandardCommands.Delete);
}


运行方案,在设计器上添加几个控件,然后在选中一个或几个控件,按下Del键或者点击菜单Edit->Delete, 所选的控件就会被删除。

 其它的菜单命令如全选,如对齐等等,皆可如此实现。StandardCommands包含的命令实在太多了。

 我们的设计器除了序列化资源、生成代码、事件处理这三项功能没有实现外,其它的都已经大功告成。

最近我会比较忙,剩下的内容要过段时间才会写出来,谢谢一直支持鼓励我的各位朋友们!

相关章节:
Form设计器尝试() Start
Form设计器尝试() PropertyGrid
Form设计器尝试() 在窗体上添加控件
写Form设计器尝试(四) 修改窗体上的控件属性
写Form设计器尝试(五) 让设计器使用自定义控件

 
 

2005年11月6日

 在“写Form设计器尝试() 在窗体上添加控件”的评论中,热心关注者Leejee提出了自定义控件的问题。我于是作了一个小测试,来实现设计器中使用自定义控件。

先要准备一个自定义控件。新建一个Windows控件方案,命名为MyControl, 添加一个名为ComboBoxField的用户控件,在该用户控件上放一个Label和一个ComboBox, 生成解决方案。将生成的dll文件复制到测试目录D:\Dotnet

打开设计器方案,在工程中添加对MyControl.Dll的引用,在主窗体的代码中添加Using MyControl; 然后在有形如toolBoxService.ToolBox.Items.Add(.);的最后加上一行代码:

toolBoxService.ToolBox.Items.Add(new ToolboxItem(typeof(MyControl.ComboBoxField)));

运行方案,确实可以象使用其它标准控件一样使用这个自定义控件。嘿嘿,没有一点意外,还是和以前的试验一样简单。
但是仔细想一想,出问题了,我们需要在工程中添加对控件所在文件的引用,需要在代码中写控件的类名。也就是说我们在写设计器时,就要知道我们要使用哪些自定义控件。而我们在VS中添加自定义控件时,VS事先并不知道我们要加的是什么。要实现这个功能怎么办?理所当然地要用“反射”。

在工程中删除我们刚才添加的引用,并在主窗体代码中删除我们刚写的那两条语句。
在前面写第二条语句的地方写上:

Assembly a1=Assembly.LoadFrom(@"D:\Dotnet\MyControl.dll");
toolBoxService.ToolBox.Items.Add(
new ToolboxItem(a1.GetType("MyControl.ComboBoxField")));

运行方案,效果和前面的一样。好像这里也将动态库文件名以及类型名称硬编码到了代码中,但是可以很简单地做到让这两个字符串从配置文件中读出,这样就可以实现在设计器使用时随意使用自定义控件了

相关章节:
Form设计器尝试() Start
Form设计器尝试() PropertyGrid
Form设计器尝试() 在窗体上添加控件
写Form设计器尝试(四) 修改窗体上的控件属性

后记: 这篇是昨晚写的, 我后来在睡觉时觉得还有点不对, 就是我这个方法是在载入工具箱前就要使用反射, 我猜测VS的做法是在需要使用此控件时才使用反射。这个问题的解决方法是修改ToolboxService。我会在以后的一个较为完善的版本中实现这个功能。2005/11/07

 
 

2005年11月3日

 在vs的目录下发现了这个dll, 顾名思义应该是提供了一个向导框架的。我想了解一下怎么用它,google了半天,也没有找到相关资料,看来只好有空的时候去研究一下了。

 
 

2005年11月1日

在上一次的尝试中,我们已经可以进行控件的添加了。但后来我发现了一个bug, 就是画好一个控件后,居然还可以接着画出这个控件,这不符合我们的习惯。一般情况下我们希望画好控件后,鼠标变回选择状态。这个功能在.net 2.0以前的做法是实现IToolboxServicevoid SelectedToolboxItemUsed()方法,但是在.net 2.0中我们已经可以用更简单的办法,前面讨论过,在.net 2.0中我们是通过继承ToolboxService类而不是完全实现IToolboxService的方式来简化工具箱功能。分析ToolboxService的源代码,可以看到它已经实现了SelectedToolboxItemUsed()方法, 其代码如下:
void IToolboxService.SelectedToolboxItemUsed()
{
      
this.SelectedItemContainerUsed();
}


protected virtual void SelectedItemContainerUsed()
{
      
this.SelectedItemContainer = null;
}

很显然,它缺省的方式将SelectedItemContainer属性值赋值为null, 这时候我们可以想到一个简便的办法是并不需要重载SelectedItemContainerUsed(), 而是修改SelectedItemContainer属性的set方法,找到上次尝试的代码,在SelectedItemContainer属性的set方法上写上:

if (value == null)
{
    toolBox.SelectedIndex 
= -1;
}

 

不过后来我觉得还是象vs的工具箱一样,提供一个”Point”的“虚”工具更方便。这个功能只要在我们前面的代码上对几个小地方稍作修改就可以了,就不帖代码了。
接下来的步骤是实现能在PropertyGrid中随意修改任何控件的属性。要做到这点也很简便,只要为DesignSurfaceSelectionService实现一个SelectionChanged事件就行了。
切换到Form1的代码窗口,为窗体类添加一个私有成员:

private ISelectionService selectionService;
然后在Load事件的最后加上:
selectionService = surface.GetService(typeof(ISelectionService)) as ISelectionService;
selectionService.SelectionChanged 
+= new EventHandler(selectionService_SelectionChanged);
当然也为窗体类加上以下事件方法:
void selectionService_SelectionChanged(object sender, EventArgs e)
{
    
object[] selection;
    
if (selectionService.SelectionCount == 0)
        propertyGrid1.SelectedObject 
= null;
    
else
    
{
        selection 
= new object[selectionService.SelectionCount];
        selectionService.GetSelectedComponents().CopyTo(selection, 
0);
        propertyGrid1.SelectedObjects 
= selection;
    }

}

以下是运行时界面:


好象是大功告成了,不过控件只能添加,却怎么删除不了。

当然,还有一些必不可少的功能,如将设计内容序列化到资源文件或者生成代码文件等等。胡适先生说:“自古成功尝试始”,我想一定会在以后几节的尝试中成功实现这些功能的。

相关章节:
Form设计器尝试()
写Form设计器尝试(二) PropertyGrid
写Form设计器尝试(三) 在窗体上添加控件

 
 

2005年10月27日

 .net 2.0以前的版本中想实现在窗体设计器中添加控件的做法是定义一个实现IToolboxService接口的“服务”,然后添加到ServiceContainer中,具体方式可以参照http://www.divil.co.uk/net/articles/designers/hosting.asp 。这个地址在www.windowsforms.net Code hero中也可以找到。

.net 2.0简化了这个作业,它已经为我们提供了一个实现IToolboxService接口的抽象类ToolboxService,我们要做的就是写一个类继承自ToolboxService, 要简便很多。

具体操作是新建一个继承自ToolboxService的类,名为DemoToolboxService, 加上必要的using语句,在所继承的类名上按鼠标右键,点“Implement Abstract Class,已经帮我们自动完成了DemoToolboxService的框架,由于我们需要在设计器窗体上显示一个工具箱,就象VS左侧的那个工具面板,不过我们现在做一个简单一点的,就用ListBox, DemoToolboxService中添加一个类型为ListBox的私有成员,并封装成属性。稍微改动一下,实现几个必要的方法,代码如下:

using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing.Design;
using System.Windows.Forms;

namespace FormDesigner
{
    
class DemoToolboxService :ToolboxService
    
{

        
private ListBox toolBox;

        
public ListBox ToolBox
        
{
            
get return toolBox; }
            
set { toolBox = value; }
        }


        
protected override CategoryNameCollection CategoryNames
        
{
            
get
            
{
                
return null;
            }

        }


        
//实现带分类的工具列表,由于目前不分类,所以即为全部工具
        protected override System.Collections.IList GetItemContainers(string categoryName)
        
{
            ToolboxItem[] t 
= new ToolboxItem[this.toolBox.Items.Count];
            
this.toolBox.Items.CopyTo(t, 0);

            
return t;
        }


        
//实现工具列表
        protected override System.Collections.IList GetItemContainers()
        
{
            ToolboxItem[] t 
= new ToolboxItem[this.toolBox.Items.Count];
            
this.toolBox.Items.CopyTo(t, 0);

            
return t;
        }


        
protected override void Refresh()
        
{
        }


        
protected override string SelectedCategory
        
{
            
get
            
{
                
return null;
            }

            
set
            
{
            }

        }


        
//实现工具选择
        protected override ToolboxItemContainer SelectedItemContainer
        
{
            
get
            
{
                
if (toolBox.SelectedIndex > 0)
                
{
                    
return new ToolboxItemContainer((ToolboxItem)toolBox.SelectedItem);
                }

                
return null;
            }

            
set
            
{
            }

        }

    }

}


   

切换到主Form的设计界面,在那个SplitContainer左侧再放上一个Panel, 做为将要完成的工具箱的容器,再切换到Form的代码编辑界面,添加一个类型为DemoToolService的私有成员toolBoxService, 修改Load事件代码,加上以下语句:

toolBoxService = new DemoToolboxService();

toolBoxService.ToolBox 
= new ListBox();

toolBoxService.ToolBox.Items.Add(
new ToolboxItem(typeof(Button)));
toolBoxService.ToolBox.Items.Add(
new ToolboxItem(typeof(TextBox)));
toolBoxService.ToolBox.Items.Add(
new ToolboxItem(typeof(Label)));
toolBoxService.ToolBox.Items.Add(
new ToolboxItem(typeof(TabControl)));
toolBoxService.ToolBox.Items.Add(
new ToolboxItem(typeof(StatusBar)));

toolBoxService.ToolBox.Dock 
= DockStyle.Fill;
this.panel1.Controls.Add(toolBoxService.ToolBox);

IServiceContainer container 
= surface.GetService(typeof(IServiceContainer)) as IServiceContainer;

 
if (container != null)
{
container.AddService(
typeof(IToolboxService), toolBoxService);
}

  运行方案,并试着在所设计的窗体上加上几个控件,界面如下:

 
 
我们画呀画,很有成就感了,但是左侧的工具箱也太丑了一点,右边那个PropertyGrid居然不因我们选择了不同的控件而改变,一点都不听话。

但无论怎样,到现在为止,我们写的代码也只有30行左右。

相关章节:
Form设计器尝试()
写Form设计器尝试(二) PropertyGrid

下节预告:Form设计器尝试() 修改窗体上的控件属性

 
 

2005年10月16日

如果要修改这个设计时Form的其它属性该怎么办呢?接下来的更简单了。
切换到
Form1的设计界面,往上面放一个SplitContainer, 再在这个SplitContainer的右panel上放一个PropertyGrid, 并将其Dock属性值设置成Fill
切换到代码编辑界面,将上篇中输入的代码的最后一行改成:

this.splitContainer1.Panel1.Controls.Add(view);
再加上一行:
this.propertyGrid1.SelectedObject = surface.ComponentContainer.Components[0];

运行项目,现在就可以编辑那个设计时Form的其它属性了。

 


除了一点点拖放操作之外,只写了六行代码,就实现这样的功能,有意思吧?

我的这个专题的目标是写一个能用的窗体设计器,有什么用呢?比如您的程序发布后,用户觉得某个控件的位置需要调整,某个控件的字体颜色需要修改,没关系,用户自行修改就是了。更复杂的一点的应用是用户希望在某个单据中增加一个字段,没关系,用户或者实施人员自已加就是了,不要改代码,不要重新编译。甚至用户想增加一些处理,也可以由实施人员现场在设计器中写代码,系统能将其编译好,并在运行时调用。

相关章节:
Form设计器尝试()

下节预告:Form设计器尝试() 在窗体上添加控件。


 
 

2005年10月10日

    Eddie Sheffield作为微软之外的第一个发现启用Form Designer的人,确实是了不起
    几年过后的今天,已经可以找到一些关于Form Designer的资料,虽然不多也不全面,但毕竟还是有一些。我手上的资料是:
    1、《Dissecting a C# Application Inside SharpDevelop》中文版,第16章专门讲Form设计器;
   
2windowsforms.net上下载的一个小例子,显然也是从SharpDevelop得到的思路;
    3WinRes这个工具,随VS2005/.net FrameWork2.0 带的一个资源本地化的工具。可以反编译看看它的源码。

    对比123, 我发现两者虽然都实现了Form Designer,但设计思路迥然不同显然WinRes用的办法要简明。原因是.net 2.0增强了设计器的功能并简化了设计器的调用方法。由于时间限制,短期内写一个功能完善的Form设计器可能难。我想做的是做一些测试,得出基于.net 2.0写一个Form设计器的基本方法。
    我想先写一个没有任何功能但能看到设计器样式的小例子来测试。以后再慢慢增加设计的功能,比如控件的添加删除,属性的设置,保存文件等功能。
    我们的步骤如下:
    VS2005(我用的版本是RC)新建一个C# WindowsForm方案, 在工程的引用中增加System. Design,  Form1的代码中先添加

using System.ComponentModel.Design;

 

    然后双击Form, FormLoad事件中写以下代码:

DesignSurface surface = new DesignSurface();
surface.BeginLoad(
typeof(Form));
Control view 
= (Control)surface.View;
view.Dock 
= DockStyle.Fill;
this.Controls.Add(view);

 

    然后运行方案,这是最简单也没任何用处的窗体设计器了,但至少我们能调整那个被设计Form的大小:)

 

2005年10月7日

显然VS2005报表服务是源于SQL Server报表服务。但是VS2005报表服务可以工作于两种模式:远程模式/本地模式。在本地模式时不依赖于SQL Server报表服务。功能强大又不失灵活性和可伸缩性。

虽然目前VS2005还带了水晶报表,但我敢打赌,微软会在未来的VS中放弃水晶报表了,如果不是Oscar,就在Oscar的下一个版本。如果是现在开始进行的新项目,我建议采用新的报表系统。

其次,整个报表系统是基于.net框架,可以很方便地自已写一个报表设计器。这在以前会是一个比较复杂的工程。这样在发布的时候,用户就不需要买报表系统的许可了。

其报表文件是XML格式的,可以写一个程序自动生成报表文件,并再由用户做调整和修改。

posted on 2006-03-18 14:12  Ruxuan  阅读(751)  评论(0编辑  收藏  举报