过眼•物影天狼

博客通天下 淡墨书豪辞

Flower

Archive for the ‘程序设计’ Category

【zz】 魔兽争霸 vs. 星际争霸 vs. 红色警戒

魔兽娱乐性强 比较搞笑 你常常越玩越轻松
星际竞技性强 比较严肃 你常常越玩越紧张
红警政治性强 比较偏激 你常常越玩越气愤

玩魔兽 就像唱卡拉ok 普通人练一首歌半个月 已经能赢得同伴的掌声
玩星际 就像唱京戏 曲不离口的练上一年 可能还唱不上调子
玩红警 就像说话 不用练就差不多水平 练了很多年说话的水平不见得高多少

学习魔兽 你能打赢两家疯狂电脑的时候 你和真人打就能取胜了
学习星际 你能打赢七家电脑 你还纳闷怎么还打不过真人
学习红警 你能打赢七家电脑 1000000 次 不见得和真人玩过

学习魔兽 两个小时你能死在相同的战术上八次 毫无还手之力
学习星际 两个小时你能死在迥异的战术上八次 毫无还手之力
学习红警 两年你都死在相同的得战术上无数次 毫无还手之力

魔兽里面 你专心练一个族往往就能够应付对同族异族4种情况打法
星际里面 人打虫的高手往往曾经就是虫打人的高手
红警里面 你学会一个国家就等于学会了所有国家

魔兽里面 熟练了几种套路就可以取胜
星际里面 熟练了几种套路还是被随机应变的对手牵着鼻子走
红警里面 熟练了几种套路,你会发现根本没用,只要熟练一种就可以了

魔兽里面 你利用计谋伏击或者包围了对方主力 对方却掏出回程扬长而走
星际里面 你会发现不仅有游击战还有阵地战、伏击战、空投战……
红警里面 你会发现什么战都是多余的,人多才是硬道理

魔兽里面 敌人无论离家多远都可以十秒内回救被你偷袭的基地
星际里面 你稍不留神就中了声东击西的诡计
红警里面 你必须时刻留神你得矿车

魔兽里面 你 5 分钟侦察一次还能对敌人兵种搭配了如指掌
星际里面 你 5 分钟侦察五次说不定得到的还是假情报
红警里面 你 5 分钟侦查一次,然后就再也用不着侦查了

魔兽里面 5 分钟不侦察你还能猜出来敌人部队构成
星际里面 3 分钟不侦察出门就可能全是克制你的兵种
红警里面 不用你去侦查地图上就能看见敌人的情况

魔兽里面 赢了一场大战就可以松口气 因为几乎稳操胜券
星际里面 赢了一场大战 正得意一下却发现刚刚大战中被一支奇兵偷袭得经济全毁
红警里面 赢了一场大战 你会觉得很幸运 你好多天都没有打过大战了。

魔兽里面 大战对决常常形势一边倒
星际里面 大战对决常常双方两败俱伤
红警里面 大战对决常常就像已经知道了结局颁奖典礼

魔兽里面 一次全军覆没 99% 可以打 GG
星际里面 十次全军覆说不定都不知鹿死谁手
红警里面 经常全军覆没是一种战斗方式

魔兽里面 你郁闷于虽然有顽强精神却在难以劣势中翻盘
星际里面 你郁闷于虽然有优势却被有顽强精神的对手翻盘
红警里面 你郁闷于必须有对方不知道的战术才能翻盘

魔兽里面 录像看到一半往往能知道结局
星际里面 录像看到结局你才发现开始的判断错了
红警里面 录像是什么都不知道

魔兽里面 初始的基地被拆毁就失去了希望
星际里面 两个人鏖战到调换基地位置甚至四海为家也不稀奇
红警里面 初始基地不仅可以被拆 还可以被占 被偷 被炸 自己还可以逃跑

魔兽里面 初始矿采完基本胜负就见分晓
星际里面 全地图的资源耗尽说不定才换来一个平局
红警里面 大家一直在抢资源很少出现平局

魔兽里面 你可以龟缩防守、偏安一隅
星际里面 你如果不及时扩张 除了初始矿点 其他矿点都有对方采矿的农民
红警里面 你必须去抢矿 这也是一种必须走的形势

魔兽里面 你把基地门口造满防御 敌人骂你猥琐赖皮
星际里面 你把基地门口造满防御 敌人不是直接空投到你家里就是直接一颗核弹敲开大门
红警里面 你必须在基地里面造满防御 敌人的飞机 飞行兵才不会占到便宜

魔兽里面 你可以用高级兵种轻松欺负低级兵种
星际里面 你发现原来小机枪也能"以小反上"地打航母
红警里面 你发现只有高级兵种才是王者

魔兽里面 没有对空部队看到空军常常就要选择逃跑
星际里面 你刚出来 4 个飞龙却被 3 队不对空的小狗强拆了基地
红警里面 飞行兵就是制胜的关键,别的都是摆设

魔兽里面 你会质疑"量变引起质变"的法则
星际里面 你会验证"量变引起质变"的法则
红警里面 你会质疑"有名气的公司比较负责"

魔兽里面 死掉一个兵会心痛半天
星际里面 你知道什么叫做前仆后继
红警里面 兵就是为了死掉的

魔兽里面 作战部队不敢过于分散
星际里面 作战常常要地图各点全面开花
红警里面 作战就是在几个特殊的地方进行

魔兽里面 即使知道敌人什么兵种配置有时候也赢不了
星际里面 知己知彼才真的百战不殆
红警里面 看到对方的兵种配置就知道对方的水平了

魔兽里面 规矩多 玩家发挥余地小 按部就班往往比突发奇想更奏效
星际里面 规矩少 玩家发挥余地大 按部就班往往陷于被动
红警里面 没规矩 玩家战术就一种 突发奇想只有在对菜鸟的时候才能用

魔兽里面 以不变应万变
星际里面 以万变应不变
红警里面 永远不变

魔兽里面 1 个英雄、道具可以四两拨千斤
星际里面 1 个隐形的单位可以四两拨千斤
红警里面 1 个高手对菜鸟可以四两拨千斤

魔兽里面 你为那个用光环照耀部队、高人一等的英雄而感到骄傲
星际里面 你发现引爆地雷和对方坦克同归于尽的那个小狂徒才是真正的英雄
红警里面 你为飞行兵拿下矿区而骄傲

魔兽里面 你会发现操作被人性化设计之后 如同一部傻瓜相机
星际里面 你会发现最简单的细节你也要亲手去处理
红警里面 你会发现可以自己处理的事情不是很多。可以边吃零食,边和高手对战

魔兽里面 你会发现 apm150 (点击速率)的时候已经会无聊到插旗
星际里面 你会发现 apm150 的时候才能勉强用用神族
红警里面 你会发现 apm150 是什么你都不知道,只知道手快很有用

魔兽里面 你觉得 12 个女巫按了 12 次 O 之后同时变了对方 12 个羊很有成就感
星际里面 你发现原来 12 运输机的地毯式空降也仅仅是操作的基本功而已
红警里面 你认为可以让 12 个坦克移动中躲掉攻击,就是操作了

魔兽里面 你觉得操作 2 队多部队围杀、齐射、魔法、道具是多么华丽
星际里面 你才知道就连让 4 队雷车、2 队坦克整齐行进都不容易
红警里面 你盯着炮弹看,快落地的时候让自己的坦克躲,炮弹多的时候还真不容易

魔兽里面 连流星陨石都认识自己人和友军
星际里面 一个闪电放不好 可能自己被电死的比敌人的还多
红警里面 除了少数几个枪法好的兵种,其他都经常误伤自己人

魔兽里面 常常讲这是理所当然
星际里面 常常讲这也不是不可能
红警里面 常常讲这是不可能的

魔兽里面 常有某个玩家用某某流战术把所用的种族用成所在版本的王者之族
星际里面 你突然发现昨天似乎无敌的偶像今天就输在某个黑马手
红警里面 你知道自己只剩下一种战术的时候,你就是高手了

魔兽玩久了 才知道 效率是第一
星际玩久了 才知道 数量是第一
红警玩久了 才知道 经验是第一

魔兽玩久了 才知道 等级是第一
星际玩久了 才知道 经济是第一
红警玩久了 才知道 兵力是第一

魔兽玩久了 才知道 稳定娴熟是第一
星际玩久了 才知道 侦察应变是第一
红警玩久了 才知道 对偷袭方法了解是第一

魔兽玩久了 才知道什么叫做战斗
星际玩久了 才知道什么叫做战略
红警玩久了 才知道什么叫做按部就班

魔兽玩久了 你发现地图到现在为止还停留在陆战
星际玩久了 你发现从 WCG2001 开始官方地图就有岛战
红警玩久了 你发现地图是永远不变的

魔兽玩久了 你发现看 rep 要变换版本和收集地图实在厌烦
星际玩久了 你发现一个 400k 的 rep 记录了一场 3 小时的比赛
红警玩久了 你发现 rep 是什么你都不知道

魔兽玩久了 你会发现总有或多或少冷板凳单位
星际玩久了 你会发现没有一个单位是多余的
红警玩久了 你发现高手对战大多数单位都是多余的

魔兽玩久了 你会发现你所了解的魔兽知识越来越多
星际玩久了 你会发现你所不懂的星际知识越来越多
红警玩久了 你发现你所知道的红警知识没用的越来越多

魔兽玩久了 仿佛在考验你的耐心和熟练程度一般
星际玩久了 总有出乎你意料的东西令你眼前一亮
红警玩久了 想睡觉

魔兽玩久了 你发现刚练熟的高效打法随着版本更新、单位修改而不再应验
星际玩久了 你发现不但新战术发明的越来越快,而且被破解的也越来越快
红警玩久了 你发现战术越来越单一,破解方法越来越无用

魔兽玩久了 你发现战术大多跟着补丁变
星际玩久了 你发现战术大多跟着玩家变
红警玩久了 你发现战术就是偷袭和反偷袭

魔兽玩久了 你发现魔兽的未来掌握在补丁手里
星际玩久了 你发现星际的未来掌握在玩家手里
红警玩久了 你发现红警的未来掌握在新游戏手里

魔兽玩久了 觉得人在被魔兽玩
星际玩久了 觉得是人在玩星际
红警玩久了 觉得人和红警都在被游戏公司玩

魔兽玩久了 天天盼望下一个版本升级补丁调整单位属性
星际完久了 天天盼望不要出现 bug 这样就不用再有新补丁诞生
红警玩久了 天天盼望不要出新补丁,要不 bug 就没了

魔兽玩久了 忽然想起冰封王座 1.07 诞生到 1.20 几乎版版不同
星际玩久了 回忆起母巢之战 1.04 到 1.08 只做过两次单位属性变动就稳定至今
红警玩久了 算了一下 10 年了就出过一次补丁,还没把 bug 改掉

魔兽玩久了 才知道魔兽三确实比星际一画面好
星际玩久了 才知道魔兽在用孙子辈的游戏和星际一代的产品比较画面
红警玩久了 才知道同样是爷爷辈的游戏,差距怎么就那么大呢?

魔兽玩久了 才知道魔兽玩家说魔兽好却很多都没玩过甚至听说过魔兽 III 的爷爷和爸爸
星际玩久了 才知道星际的第一代已经快八岁了
红警玩久了 才知道红警已经六年每人玩了

魔兽玩久了 避免不了争论种族平衡性、英雄兵种单位 bug 性的口水战
星际完久了 你问哪个族最强 大家会告诉你三族一样厉害 根据兴趣爱好选择
红警玩久了 总是想说,咱们出飞行兵了,换种打法吧

魔兽玩久了 你不知道为什么魔兽玩家似乎也分了种族
星际玩久了 你会发现三族来自不同星球但各族玩家却似兄弟
红警玩久了 你会觉得每个国家几乎没有区别

魔兽玩久了 你发现各族玩家往往在为维护自己所用种族而争辩
星际玩久了 你发现无论何族玩家都在为维护共同的星际而争辩
红警玩久了 你会发现这个游戏一直在维护某些国家的政治利益

魔兽玩久了 你会品味什么是流行
星际玩久了 你会体会什么是经典
红警玩久了 你会明白什么是猥琐

魔兽玩久了 你才知道为什么魔兽如此热门
星际玩久了 你才知道为什么星际如此经典
红警玩久了 你才知道为什么红警如此冷门并且没有人玩

魔兽玩久了 你会喜欢上魔兽 别人说魔兽不好 你会火冒三丈 恨不得打骂他
星际玩久了 你会喜欢上星际 别人说星际不好 你会一笑而过 不屑和他争辩
红警玩久了 你会喜欢上红警 别人说红警不好 你会火冒三丈 不知道怎么争辩

魔兽玩久了 你慢慢体会到魔兽真的是一款好游戏
星际玩久了 你慢慢体会到星际越来越不像一款游戏
红警玩久了 你慢慢体会到一个好的公司比一款好的游戏重要的多

魔兽玩久了 你发现魔兽是如此精彩的游戏 给我们带来快乐
星际玩久了 你发现生活和思维方式已经有了星际的烙印
红警玩久了 你发现思维方式越来越简单了

魔兽玩久了 才发现原来有很多初中小朋友加入魔兽玩家行列
星际玩久了 才发现原来有很多成家立业的"大叔"还没退出星际玩家行列
红警玩久了 才发现原来有很多初中的小朋友和成家立业的大叔,不断加入和迅速退出这红警玩家的行列

魔兽玩久了 才知道世界上最远的距离不是中国电信和网通 而是魔兽精灵玩家和兽人玩家的心
星际玩久了 才知道 星品不好人品就不好
红警玩久了 利用 bug 在红警里不算人品太不好

魔兽玩久了 才知道 魔兽是暴雪 (Blizzard) 制造出来的最流行的精品大作
星际玩久了 才知道 星际是上帝借暴雪之手赐予玩家们的经典杰作
红警玩久了 才知道 西木 (Westwood) 为什么会输给暴雪

新发现的一个好东西 Script#

      Script# 就是一个用 C# 代码来写脚本语言,然后通过特定的编译器将其转换成 Javascript。

      不想在我这里说太多,作者的 blog 上面已经写的很清楚了。

      有兴趣的可以到这里看一下。

      另外还觉得这个东西生成的 Javascript 所引进的面向对象模型有点意思,等我研究一下再来说说。

偶然间发现一个 PI 的算法

long a = 10000, b = 0, c = 2800, d, e = 0, f[2801], g;

main() {
    for (; b - c; ) f[b++] = a / 5;
    for (; d = 0, g = c * 2; c -= 14, printf("%.4d", e + d / a), e = d % a);
    for (b = c; d += f[b] * a, f[b] = d % --g, d /= g--, --b; d *= b);
}

结果是
5926 5358 9793 2384 6264 3383 2795 0288 4197 1693 9937 5105 8209 7494 4592 3078 1640 6286 2089 9862 8034 8253 4211 7067 9821 4808 6513 2823 0664 7093 8446 0955 0582 2317 2535 9408 1284 8111 7450 2841 0270 1938 5211 0555 9644 6229 4895 4930 3819 6442 8810 9756 6593 3446 1284 7564 8233 7867 8316 5271 2019 0914 5648 5669 2346 0348 6104 5432 6648 2133 9360 7260 2491 4127 3724 5870 0660 6315 5881

CS0013 or CS0016 Compilation Errors in ASP.NET Web Applications

SYMPTOMS
When you view a Microsoft ASP.NET Application in a Web browser, you may receive the following error messages:

For the Microsoft .NET Framework version 1.1, the error message is the following:

CS0016: Could not write to output file 'c:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\Temporary
ASP.NET Files\application1\c11b43f6\cf3ec03\rizcntet.dll' . The directory name is invalid.

For the .NET Framework 1.0, the error message is the following:

CS0013: Unexpected error writing metadata to file
'C:\WINDOWS\Microsoft.NET\Framework\v1.0.3705\Temporary ASP.NET Files\application2\3fc72f26\eb731247\ev2bslce.dll'.
The directory name is invalid.

CAUSE
The system TEMP and TMP variables point to a folder that does not exist.

The compiler generates temporary files in the folder where the TEMP and the TMP variables point to before the files are copied to the Temporary ASP.NET Files folder. However, the folder where the system variables point to is deleted when you restart the computer. Therefore, the compiler cannot generate the temporary files.

RESOLUTION

  1. Create a temporary folder under %Systemroot%, and then name it Temp.
  2. Grant full permissions on the Temp folder to the aspnet user account in .NET Framework 1.0 or to the NETWORK SERVICE user account in .NET Framework 1.1.
  3. Right-click My Computer, and then click Properties.
  4. On the Advanced tab, click Environment Variables.
  5. Select the TEMP variable under System variables, and then click Edit.
  6. Type %SystemRoot%\TEMP in the Variable Value box, and then click OK.
  7. Repeat steps 5 and 6 to edit the TMP variable. Click OK two times.
  8. Click Start, and then click Run.
  9. To reset Internet Information Services (IIS), type iisreset on the command prompt.

Note If the error message that is mentioned in the "Symptoms" section of this article persists, restart the computer.

MORE INFORMATION
Steps to Reproduce the Behavior

  1. Start Microsoft Visual Studio .NET.
  2. Create a new ASP.NET Web Application project by using Microsoft Visual C# .NET or Microsoft Visual Basic .NET, and then name the project CompileTest.
  3. On the Build menu, click Build Solution.
  4. Right-click My Computer, and then click Properties.
  5. On the Advanced tab, click Environment Variables.
  6. Select the TEMP variable under System variables,
    and then click Edit.
  7. Type %SystemRoot%\TEMP1 in the Variable Value box to point to the nonexistent TEMP1 folder, and then click OK.
  8. 8.Repeat steps 6 and 7 to edit the TMP variable to point to the nonexistent TEMP1 folder.
  9. 9.Click OK two times.
  10. 10.To notice one of the error messages that are mentioned in the "Symptoms" section of this article, visit the following URL: http://localhost/CompileTest/WebForm1.aspx

Hermite 曲线的算法与实现

      摘要:本文主要介绍了一种自由曲线 Hermite 曲线的数学原理,并以此为依据构造了 Hermite 曲线的计算机算法,并在微软的 .net 框架下使用 GDI+ 实现。

      关键字:合成曲线,Hermite曲线,C#,GDI+

一、数学原理
      曲线是构建几何模型的最基本元素之一,主要分为解析曲线和合成曲线。解析曲线通常是先有曲线方程,然后才能把曲线画出来,这种方式对于曲线的构造者来说是非常复杂且不直观的,而且改变曲线的一些参数,设计者也无法立刻了解曲线形状会做怎样的变化。在作曲线设计时,设计者通常希望能先将大致形状用很直观的方式描绘出来,并能够很容易的依照所需要的形状作修改,因此合成曲线是比较合适的方式。

      合成曲线通常以参数的形式来表现,是由设计者根据其设计的需求和几何信息,去“合成”出这条曲线。这样由设计者输入的几何数据,就是曲线的控制点。

      一般的合成曲线,至少需要一个三次的参数式:

公式(1)

用向量表示为:

公式(2)

如何确定式中的参数,就形成了不同的合成曲线的构造方法。Hermite 曲线就是通过曲线的起点(P0)、终点(P1)、起点切向量(V0)和终点切向量(V1)来确定曲线的。改变这四个参数,就可以控制 Hermite 曲线的形状。图1就是构建 Hermite 曲线的示意图。

图1

当给定以上四个参数之后,如何来确定这条 Hermite 曲线呢?

首先将(2)式作一次微分得到:

公式(3)

然后将 u=0,u=1 代入(2)式和(3)式中,就可以得到参数式中的系数:

公式(4)

将(4)式作进一步整理,最后可以将 Hermite 曲线方程写成如下形式:

公式(5)

这就是 Hermite 曲线的参数方程。根据此方程,对于 P0=(-2,2),P1=(3,-1),V0=(8,10) 和 V1=(15,10) 这四个参数,可以构造如下的表格:

使用 Matlab 绘制此曲线,得到图2:

图2

      但是在做曲线设计时,Hermite 曲线仍然存在不少问题,例如在建立 Hermite 曲线时设计者必须输入曲线两端切向量的大小和方向,这对设计者来讲仍然是不直观的。另外,Hermite 曲线不具有区域控制的能力,在建立 Hermite 曲线时提供的是个参数,改变任何一项输入,整条曲线的形状都会发生变化,设计者很难对其进行局部的、小范围的修改。

二、算法
      有了以上的数学基础,构造算法就不是件困难的事情。

/// <summary>
/// 绘制Hermite曲线的核心方法
/// </summary>
/// <param name="pen">绘制曲线用的Pen对象</param>
/// <param name="p0">起点坐标</param>
/// <param name="p1">终点坐标</param>
/// <param name="v0">起点切向量</param>
/// <param name="v1">终点切向量</param>
private void DrawHermite(Pen pen,Point p0, Point p1, Point v0, Point v1) {
    // 计算出来的当前坐标
    int x = p0.X;
    int y = p0.Y;

    // 计算出来的前一个坐标,使用该两个做标连成一条线,来绘制曲线
    int preX, preY;

    // 根据参数计算每个点的坐标,参数的增量为0.01
    for (double i = 0.0; i <= 1.0; i = i + 0.01) {
        preX = x;
        preY = y;

        // 保存计算中间结果,避免重复计算,提高算法效率
        double i2 = i * i;
        double i3 = i2 * i;
        double express = 3 * i2 - 2 * i3;

        // 计算横坐标和纵坐标
        x = (int)((1 - express) * p0.X + express * p1.X + (i - 2 * i2 + i3) * v0.X + (i3 - i2) * v1.X);
        y = (int)((1 - express) * p0.Y + express * p1.Y + (i - 2 * i2 + i3) * v0.Y + (i3 - i2) * v1.Y);

        // 画线
        this.drawingSurface.DrawLine(pen, preX, preY, x, y);
    }

    this.drawingSurface.DrawLine(pen, x, y, p1.X, p1.Y);
}

三、使用 GDI+ 实现
      在 .net 框架下,使用 GDI+ 实现这个算法是件轻松的事情。但是在编程过程中仍然出现了几个问题。

      首先,如何确定两个起点和两个切向量。本程序采用了如下的方法:先选定一个点,然后拉出一条直线,以该点为起点(或终点)并以该直线的方向和长度作为起点(或终点)切向量的方向和大小。

      其次,本程序可以实现类似 PhotoShop 中的钢笔功能。所以有一个如何产生拉动的效果的问题。这对这个问题使用了两种不同的解决方法:1、对于画曲线,使用了两个画笔,一个用于绘制,一个用于擦除。当鼠标移动的时候,就会使用绘制的画笔绘制新曲线,并用擦除画笔擦除刚才的曲线。但该方法会导致另一个问题,就是当该曲线覆盖到其他线条上之后,当曲线离开后,该线条就会有部分被擦掉。但是想解决这个问题是很困难的。(不知道 PhotoShop 是如何实现的。)2、画直线的时候,使用了 GDI+ 中自带的一个 ControlPaint.DrawReversibleLine() 方法,该方法可以自己解决以上的问题。

      第三、像这样一遍一遍的重画和擦除,会很占用系统资源,但是没有什么更好的解决方法,从网上找到的文章来看,如果复写(Override)OnMouseDown, OnMouseMove 和 OnMouseUp 事件,会比处理此三个事件的方法要来得效率高一些,因此本程序的所有事件全部采用了这种方法。下面的这段代码,是复写了 OnMouseMove 事件,用于处理当鼠标按下左键移动的时候,产生的拉动效果。

protected override void OnMouseMove(MouseEventArgs e) {
    // 判断当鼠标移动的时候是否有鼠标左键按下
    if (e.Button == MouseButtons.Left) {
        // isContinuedDrawing 是个标志变量,标志所产时的动作是否为了画第二个参量
        if (!isContinedDrawing) {
            endPoint[0].X = e.X;
            endPoint[0].Y = e.Y;

            // 使用 ControlPaint 画直线
            ControlPaint.DrawReversibleLine(PointToScreen(startPoint[0])PointToScreen(previousPoint), Color.Black);
            ControlPaint.DrawReversibleLine(PointToScreen(startPoint[0]), PointToScreen(endPoint[0]), Color.Black);

            previousPoint = endPoint[0];
        } else {
            endPoint[1].X = e.X;
            endPoint[1].Y = e.Y; ControlPaint.DrawReversibleLine(PointToScreen(startPoint[1]), PointToScreen(previousPoint), Color.Black);
            
            Point _v0 = new Point();
            _v0.X = endPoint[0].X - startPoint[0].X;
            _v0.Y = endPoint[0].Y - startPoint[0].Y;

            Point _v1 = new Point();
            _v1.X = previousPoint.X - startPoint[1].X;
            _v1.Y = previousPoint.Y - startPoint[1].Y;

            // 擦除以前的曲线
            this.DrawHermite(erasePen,startPoint[0], startPoint[1], _v0, _v1);
            // 画新的曲线
            ControlPaint.DrawReversibleLine(PointToScreen(startPoint[1]), PointToScreen(endPoint[1]), Color.Black);
            _v1.X = endPoint[1].X - startPoint[1].X;
            _v1.Y = endPoint[1].Y - startPoint[1].Y;

            this.DrawHermite(drawPen,startPoint[0], startPoint[1], _v0, _v1);
            previousPoint = endPoint[1];
        }
    }

    // 调用父类中的 OnMouseOver 事件三
    base.OnMouseMove(e);
}

四、程序截图

五、参考文献

关于 Design Mode 的一点认识

      今天绝大部分时间用于解决了一个很小的问题,但是从中却了解到 Visual Studio.net 中 Design Mode 的一些实现方式。

      从头开始,先来说说我遇到的这个问题:如何判但一个实例所对应的类是否实现了某一个接口?这其实也算不上什么问题,我起初也是知道的,就是和判断某个对象是否是某个类的实例一样,使用 is 关键字就可以了。但问题偏偏没有那么简单。

      我的目的是想要实现这样一种功能,一个用户控件 (User Control) 被放到一个容器(之所以说是容器,因为它不一定只是 Form, 也有可能是 Tab page, Group box 等等)中,那么我需要这个用户控件去检查这个容器是否实现了我规定的一个接口,如果是才允许在这个容器中创建自己。

      于是,问题接踵而来。首先,如何得到这个用户控件所在的容器,通过搜索 MSDN 得到两个属性,一个是 UserControl.Container, 另一个则是 UserControl.Parent. 前一个似乎很像,不过它返回的是一个 IContainer 接口的实例,无法直接使用;而后者相对较好,返回一个 Control (该类是所有 Windows 控件,包括 Form 的基类)类的实例。很自然,我就开始在这个属性上下文章。

      假如我要指定实现的接口是 IMyInterface. 假如我使用 Form 作为该控件的容器,那么在 IDE 自动给我在默认窗体后面继承一个 System.Windows.Forms.Form 之外,还需要继承这个接口,这部分很简单但是当我使用 is 关键字进行如下判断时就出现了问题 this.Parent is IMyInterface (其中的 this 就是那个用户控件的实例),这个判断永远也通不过。后来我试图将其做一个强制类型转换,却得到了指定转换非法 (Spicified cast is invalid.) 的异常。原因就在于 this.Parent 返回一个 Control 类型,而该类型在 .net 类库中显然没有实现我的那个接口,自然转换非法是正常的。但如何解决呢?

      马上就能想到的自然是自己重写一个 Control 类,让该类实现我的接口。其实完全可以重写一个 Form 类,来实现接口。然后再使得你创建的 Form 从你这个扩展了的 Form 去继承,再作如上的转换就不会有问题了。我也就是这样做的,不过稍微画蛇添足了一点:

public abstract class MyCustomForm : System.Windows.Forms.Form,  IMyInterface {}

就是加了一个 abstract 关键字,以阻止用户从该类生成实例。这是就出现了下一个问题,从这个 MyCustomForm 继承的窗体无法在设计器中展现了!这个问题起初没有弄明白是怎么回事,于是我就先去掉 abstract 关键字,使得一切恢复正常。随后我在这个类中添加了一个返回 bool 类型的虚属性,并默认实现返回 false, 就像这样:
public virtual bool MyProperty {
    get {
        return false;
    }
}

然后我在我真正的 Form 中 (起名为 MainForm)覆盖这个属性,并返回 true. 之后我在用户控件的 Load 事件方法中调用这个属性,然而奇怪的是当向窗体中添加这个用户控件的时候(注意此时仍处于 Design Mode),该属性一直都为 false, 既基类中的实现,也就是说似乎属性没有被覆盖。这时我就想到了一个关于何时使用基类中的方法,何时使用覆盖了的方法的问题。因为可以想得到,当你把 MainForm 转到 Control 之后(通过调用 UserControl.Parent 实现)原有 MainForm 中多余的成员是否会被丢掉,当再转换成 IMyInterface 的实例时是否将无法找到覆盖过的方法,以至于直接去调用积类中的方法。似乎这是理所当然的,但是我随后写了一个类似的小程序试验,结果发现并不是这样,原因也很简单,这些转换只是在引用上发生的,已有的实例中的内容并不会被销毁,那么出现这个问题就太诡异了。

      仔细想呀想呀,联想到上面出现的那个 Design Mode 的问题,一切就都能揭示了。 简单的就一句话:Design Mode 在你操作时需要生成一个实例,然而该实例不是运行时的实例。具体来说,你在 Design Mode 中设计一个窗体,那么你会看见 IDE 自动为你创建了一个名叫 Form1 的类,并且继承自 System.Windows.Forms.Form, 同时生成一个可视化的窗体界面,这个界面是一个窗体的实例。但是这个实例不是 Form1 的实例,而是其基类 Form 的实例,原因就是,IDE 是在帮你设计这个 Form1, 显然此时这个 Form1 还不存在,存在的只是一堆代码而已。这样你就可以解释一切了,当我的基类被 abstract 的时候无法被实例化以后,自然设计器就没有办法为我创建这个类的实例使得我能进行可视化操作,但是你仍然可以将程序无误的跑起来。因为所谓的那个 Form1 并不是 abstract 的。也正因为如此,当我的 UserControl 在 Design Mode 时去调用 Interface 中的方法时,自然得到的就是基类中虚属性的实现,而在运行时,得到的就是覆盖了的属性的返回值,经过试验,答案却是是这样的。

      另外,你可使用 UserControl.DesignMode 来判断其是处于设计时还是运行时。

使用 C# 实现图像的边缘检测

      我本人对图像处理没什么兴趣,要不是这门课要交作业,我才懒得做这些东西。唉……不过程序写了,自然会有一点想法,发到 Blog 上,以备后用吧。但是,即便是写了程序,也仍然不知道作边缘检测的原理何在,只是模糊的知道大概是对图像灰度求梯度,梯度大的就是边缘了。但毕竟图像是离散化的,可以使用另外的方法求梯度,而不用像高数中那样拼命地算偏导数了。有很多学者提出了很多种不同性能的模板,只要按照模板作简单的四则运算就行了,当然这也是能用程序实现的关键。由于作为教学课程,所有的内容都是以简单的灰度图像来说明举例的,当然边缘检测也不例外,留作业写程序也是一样,所以马上就遇到的一个问题是如何将彩色图像转为灰度图像。在课程中,都是简单的认为灰度图像只有一个亮度,这自然是没错的,但是放到计算机里,灰度也是一种颜色,是颜色就要使用色彩模式(最常用的自然是 RGB 了),那么这种灰度到底应该是怎样的颜色编码呢?索性取向 Photoshop ,看一看各种灰度色调,终于有所发祥。其实也可以这么想,全黑是 #000000 ,全白是 #FFFFFF ,那么是不是只要 RGB 值都相等,这个颜色就是灰度色呢?试了一下,果然如此。这样就好办了,至少第一步知道了转换的目标是什么了。但马上就又有了一个问题,彩色图片的颜色这么多,那么如何知道哪种彩色颜色对应哪种灰度颜色呢?这一点我从 .net Framework 中找到了答案。其实我从一开始就想在 .net Framework 中寻找有没有直接将 RGB 转成灰度或者是 HIS 模式(因为 HIS 模式中的 I 就使亮度,自然就容易转成灰度了)的,是不是太奢望了,所以我也没抱太大的希望,但是在这过程中却发现 Color 中有关一个实例方法 GetBrightness() ,就是用来获得颜色亮度的,真是踏破铁鞋无觅处,得来全不费功夫。该方法返回一个 0~1 之间的浮点数,那么如果 RGB 每个各占一个字节的话,那么刚好可以用这个值去乘以 255 ,然后拼成一个 RGB ,这个颜色就是原始色彩所对应的灰色。程序代码如下:

//定义两个颜色变量,oColor为原始色彩,gColor为对应的灰度色彩
Color oColor,gColor;
//原始色彩的亮度
float brightness;
//灰度色彩用 RGB 来表示,由于 R=G=B 所以只用一个变量就可以了
int gRGB;
//遍历图像中的每个像素
for (int i = 0; i < oBmp.Width; i ++) {
    for (int j = 0; j < oBmp.Height; j ++) {
        //得到像素的原始色彩        
        oColor = oBmp.GetPixel(i,j);
        //得到该色彩的亮度
        brightness = oColor.GetBrightness();
        //用该亮度计算灰度
        gRGB = (int)(brightness * 255);
        //组成灰度色彩
        gColor = Color.FromArgb(gRGB,gRGB,gRGB);
        //最后将该灰度色彩赋予该像素
        gBmp.SetPixel(i,j,gColor); 
    }
}

其实还是很简单的。这之后就可以按照书中所说的模板游历的方法来进行边缘检测了。程序如下:
//template为模板,nThreshold是一个阈值,
//用来将模板游历的结果(也就是梯度)进行划分。
//大于阈值的和小于阈值的分别赋予两种颜色,白或黑来标志边界和背景
private void EdgeDectect(int[,] template,int nThreshold) {
    //取出和模板等大的原图中的区域
    int[,] gRGB = new int[3,3];
    //模板值结果,梯度
    int templateValue = 0;
    //遍历灰度图中每个像素
    for (int i = 1; i < gBmp.Width - 1; i ++) {
        for (int j = 1; j < gBmp.Height - 1; j ++) {
            //取得模板下区域的颜色,即灰度
            gRGB[0,0] = gBmp.GetPixel(i-1,j-1).R;
            gRGB[0,1] = gBmp.GetPixel(i-1,j).R;
            gRGB[0,2] = gBmp.GetPixel(i-1,j+1).R;
            gRGB[1,0] = gBmp.GetPixel(i,j-1).R;
            gRGB[1,1] = gBmp.GetPixel(i,j).R;
            gRGB[1,2] = gBmp.GetPixel(i,j+1).R;
            gRGB[2,0] = gBmp.GetPixel(i+1,j-1).R;
            gRGB[2,1] = gBmp.GetPixel(i+1,j).R;
            gRGB[2,2] = gBmp.GetPixel(i+1,j+1).R;
            //按模板计算
            for (int m = 0; m < 3; m ++) {
                for (int n = 0; n < 3; n ++) {
                    templateValue += template[m,n] * gRGB[m,n];
                }
            }
            //将梯度之按阈值分类,并赋予不同的颜色
            if (templateValue > nThreshold) {
                eBmp.SetPixel(i,j,Color.FromArgb(255,255,255)); //白
            } else {
                eBmp.SetPixel(i,j,Color.FromArgb(0,0,0)); //黑
            }
            templateValue = 0;
        }
    }
}

.net Remoting 中的几个重要概念和实现方法

应用程序域 (Application domain)
      一般的 Windows 应用程序都是以进程的方式在操作系统中运行,操作系统负责分配并管理程序所请求的资源。但是对于使用 .net 编写的托管程序而言,有一个特殊的进程被称为公共语言运行时 (CLR) ,该进程负责加载托管程序并运行。对于在 CLR 中运行的每一个托管程序而言,都有一个被称之为应用程序域的边界,每个托管程序都在自己的应用程序域中安全的运行。不同的应用程序域之间互不干扰。

上下文 (Application context)
      将应用程序域进一步细分,就形成了上下文,上下文确保一套常用约束和使用语法负责管理其中的所有对象访问。每个应用程序域至少包含一个上下文,称为默认上 下文 (Default context) 。除非某个对象明确的要求一个专门的上下文,否则运行时 (Common language runtime) 将在默认上下文中创建那个对象。

.net Remoting 边界
      对于应用程序而言应用程序域的边界是 .net Remoting 的边界。对应用程序域而言上下文的边界是 .net Remoting 边界,一个普通的对象无法穿越 .net Remoting 边界。

不可远程化对象和可远程化对象
一、不可远程化对象 (Non remotable object)
      在默认情况下,没有经过任何特殊处理的对象都是不可远程化对象。不可远程化对象无法以任何方式(拷贝或引用)被跨应用程序域的对象所访问,当企图把对象引用传递到其他的应用程序域中时,会有异常产生。
二、可远程化对象 (emotable object) 如果一个类的实例可以穿越 .net Remoting       边界并可以在边界外被访问,则该对象就是可远程化的。在应用程序域外可远程化对象被访问的方式有两种:可以通过对象的完整副本来访问,还可以通过对象的引用(在 .net Remoting 中是以代理的模式来实现的)来访问。

.net Remoting 的服务器和客户端
      .net Remoting 的服务器和客户端与以往的概念没有什么差别,但是在这里要强调的是 .net Remoting 的服器器和客户端显然是处于不同的应用程序域中,但是并不一定处于不同的计算机上。

按值列集 (Marshal by value) 和按引用列集 (Marshal by reference)
      一、当运行时可以获得一个对象的完整副本的时候,则该对象就可以以所谓按值列集的方式被传递到其他的应用程序域中。实现按值列集的途径就是在声明类的时候添加 Serializable 特性 (Attribute) 或者实现 ISerializable 接口。例如:

[Serializable]
class Foo {
    ...
}

因此能够按值列集的对象也被称为可序列化的对象。客户端将获得该对象的完整副本。

      二、当一个类直接或间接地从 MarshalByRefObject 类继承的时候,运行时就可以在客户端创建一个该对象的代理。例如:

class Foo : MarshalByRefObject {
    ...
}

上下文邦定 (Context bound)
      当一个类型的实例只停留在具体的上下文内的时候,该类型就是上下文邦定的类型,在这个域内的其它上下文中的对象不能直接访问该对象。上下文邦定类型通过继承 System.ContextBoundObject 类来实现。

代理 (Proxy)
      刚才提到,当对象以按引用列集的方式在应用程序域中传递的时候,运行时将在接收方创建一个该对象的代理。该代理可以想象为(通常也是这样实现的)一个封装了该对象所有或部分成员的接口,负责将接收方(客户端)的调用信息发送给发送方(服务器)。

通道 (Channel)
      通道用于在跨应用程序域的远程对象间传递消息 (Message) 。服务器将选择侦听请求的通道,而客户端则选择希望与服务器进行通信的通道。运行时提供了两种内置的通道 Http 通道和 Tcp 通道。

using System.Runtime.Remoting; // .net Remoting 命名空间
using System.Runtime.Remoting.Channels; // .net Remoting 通道命名空间
using System.Runtime.Remoting.Channels.Http; // .net Remoting Http 通道命名空间
using System.Runtime.Remoting.Channels.Tcp; // .net Remoting Tcp 通道命名空间
. . .
// ChannelServices 是一个工具类,里面封装了大量的与通道相关的静态方法
ChannelServices.RegisterChannel ( new HttpChannel() ); // 注册 Http 通道并使用默认端口
ChannelServices.RegisterChannel ( new TcpChannel(4242) ); // 注册 Tcp 通道并使用4242端口

激活 (Activation)
一、服务器端激活 (Server Activation)
      服务器端激活方式是指对象的生存周期(何时生成与何时被垃圾回收)由服务器来决定。服务器端激活有两种方式:单件 (Singleton) 和单调用 (SingleCall) 。
二、客户端激活 (Client Activation)
      客户端激活方式是指对象的生存周期由客户端来决定。客户端激活只有一种方式,称为 CAO (Client Activation Object) 。每一个客户端激活创建一个对象,该对象存在于如下两个事件之一到来之前:客户端失掉对对象的引用,对象租借过期。客户端激活模式可以存储每一个客户端的状态,并接受构造函数参数。可以使用如下的代码,在服务器端设置客户端激活的远程对象:
RemotingConfiguration.RegisterActivatedServiceType( typeof( SomeMBRType ) );

然后在客户端设置如下:
RemotingConfiguration.RegisterActivatedClientType( typeof( SomeMBRType ), "http://SomeURL" );

此处的 SomeURL 是指服务器端的地址或计算机名。

众所周知 (Well-known object) 的对象
      服务器端激活的类型我们就称之为众所周知的。在使用众所周知的对象时,服务器要进行如下的设置:对象的类型,何时以及如何实例化对象,和一个客户端用来与该类型联系的名称(或叫终端 end point )。而同时客户端要设置连接到哪一个服务器,并在终端上获得众所周知的类型。使用 RemotingConfiguration.RegisterWellKnownServiceType 这个方法来注册一个众所周知的类型,该方法需要提供三个参数:待注册的类型,传达给客户端的终端名称和激活模式。例如:

using System.Runtime.Remoting; // .net Remoting 名名空间
. . .
WellKnownServiceTypeEntry WKSTE =  new WellKnownServiceTypeEntry(
                     typeof( MyNameSpace.SomeMBRType ),
                     "SomeURI",
                     WellKnownObjectMode.SingleCall );
RemotingConfiguration.RegisterWellKnownServiceType(WKSTE);

其中先创建一个 WellKnownServiceTypeEntry 类型的对象,用于在 RegisterWellKnownServiceType 方法中传递参数。
typeof( MyNameSpace.SomeMBRType ) 这个参数待注册的远程对象的类型, SomeMBRType 是 MyNameSpace 命名空间下的一个类。
SomeURI 便是传达给客户端的终端名称。(它的用途在后面会介绍。)
WellKnownObjectMode.SingleCall 就是将该类型注册为单调用模式。

单件和单调用
      一、单调用:服务器为每个客户端的方法调用生成一个单调用对象,每个对象服务且仅      服务一个请求。只有当方法调用到达的时候才按需求创建对象,并且对象的生存期直至调用结束。单调用模式适用于无需保存状态的应用程序,是解决负载平衡的最好选择。可以通过如下的方法在服务器端将可远程化类型配置为单调用模式:

// RemotingConfiguration 是一个工具类
RemotingConfiguration.RegisterWellKnownServiceType(  
   typeof( SomeMBRType ),   // 从 MasharlByRef 继承来的数据的类型信息
   "SomeURI",                     // 发布该服务器端激活对象时使用的 URI
   WellKnownObjectMode.SingleCall ); // 设定为单调用模式

然后用如下的方法在客户端再配置一次:
RemotingConfiguration.RegisterWellKnownClientType(
   typeof( SomeMBRType ),               // 要从服务器端得到的远程类型
   "http://SomeWellKnownURL/SomeURI" ); // 众所周知的对象发布的 URL

      二、单件:服务器在任何情况下都只创建该类型的一个实例,客户端的所有请求都由这      一个实例来处理,且该实例的生存期与服务器的生存期相同。单件模式适用于由状态的应用 程序,但是这种模式的对象只能保存非客户端的状态。同上面的代码,只要将 WellKnownObjectMode 设置为 Singleton 就可以了。

众所周知对象的 URL
      服务器端激活的对象是在 URL 上发布的,该 URL 是被客户端众所周知的。众所周知对象的 URL 看上去如下:

ProtocolScheme://ComputerName:Port/ApplicationName/ObjectUri

其中 ProtocolScheme 代表 .net Remoting 通道所使用的协议,例如 Http 或 Tcp 等。
ComputerName 代表 .net Remoting 服务器的名称或地址。
Port 是注册通道时使用的端口。
ApplicationName 就是服务器端应用程序的名称。当使用 IIS 作为服务器端宿主的时候, ApplicationName 就变成了 IIS 的虚拟目录。
ObjectUri 就是在使用 RegisterWellKnownServiceType 时注册的 SomeURI 。 ObjectUri 必须以 .rem 或 .soap 结束,以区别是使用 Tcp 还是 Http 协议。

基于租借的生存期
      客户端激活对象的生存期由与对象相关的租借来控制,租借有一个租借期, .net Remoting 基础设施在租借过期的时候放弃对象的引用,每一次方法调用可以更新租借,客户端可以使用代理更新租借,发起者也可以更新租期。

客户端激活对象的 URL
      客户端激活对象不需要为每一个对象配备一个单独的 URL ,客户端激活对象的 URL 看上去如下:

ProtocolScheme://ComputerName:Port/ApplicationName

设计模式 — Builder

      上下文:一个产品有 n 个零件,通过构建产品的不同部分来生产不同的产品。

      解决方法:将一个产品的不同部件交由某个构建者的不同方法来实现,然后导演通过调用不同的方法,构建出不同的产品。

using System;

namespace Builder {
    /// <summary>
    /// 产品,其中包括两个部件
    /// </summary>
    public class Product {
        string part1 = string.Empty;
        string part2 = string.Empty;

        public string Part1 {
            get { return part1; }
            set {  part1 = value; }
        }

        public string Part2 {
            get { return part2; }
            set { part2 = value; }
        }
    }

    /// <summary>
    /// 构建者的接口标准
    /// </summary>
    public interface IBuilder {
        void BuildPart1();
        void BuildPart2();
        Product RetrieveProduct { get; }
    }

    /// <summary>
    /// 从接口派生出具体的构建者
    /// </summary>
    public class ConcreteBuilder:IBuilder {
        Product myProduct = new Product();

        #region IBuilder 成员
        public void BuildPart1() {
            // TODO:  添加 ConcreteBuilder.BuildPart1 实现
            myProduct.Part1 = "Part1 has been built!";
        }

        public void BuildPart2() {
            // TODO:  添加 ConcreteBuilder.BuildPart2 实现
            myProduct.Part2 = "Part2 has been built!";
        }

        public Product RetrieveProduct {
            get {
                // TODO:  添加 ConcreteBuilder.RetrieveProduct getter 实现
                return myProduct;
            }
        }
        #endregion
    }
 
    /// <summary>
    /// 导演通过调用具体构建者的不同构建方法,构建不同的部件,进而行成不同的产品
    /// 该导演生产了这样的三种产品
    /// 1、具有零件 1 和 2 的产品
    /// 2、只具有零件 1 的产品
    /// 3、只具有零件 2 的产品
    /// </summary>
    public class Director {
        Product product1, product2, product3;

        public Product Product1 {
            get { return product1; }
        }
        public Product Product2 {
            get { return product2; }
        }
        public Product Product3 {
            get { return product3; }
        }

        public Director() { }

        public void Construct() {
            ConcreteBuilder myCB1 = new ConcreteBuilder();
            myCB1.BuildPart1();
            myCB1.BuildPart2();
            product1 = myCB1.RetrieveProduct;

            ConcreteBuilder myCB2 = new ConcreteBuilder();
            myCB2.BuildPart1();
            product2 = myCB2.RetrieveProduct;

            ConcreteBuilder myCB3 = new ConcreteBuilder();
            myCB3.BuildPart2();
            product3 = myCB3.RetrieveProduct;
        }
    }

    /// <summary>
    /// 测试 Builder 模式
    /// </summary>
    class TestBuilder {
        /// <summary>
        /// 应用程序的主入口点。
        /// </summary>
        [STAThread]
        static void Main(string[] args) {
            Director myDirector = new Director();
            myDirector.Construct();
            Console.WriteLine(myDirector.Product1.Part1);
            Console.WriteLine(myDirector.Product1.Part2);
            Console.WriteLine(myDirector.Product2.Part1);
            Console.WriteLine(myDirector.Product2.Part2);
            Console.WriteLine(myDirector.Product3.Part1);
            Console.WriteLine(myDirector.Product3.Part2);

            Console.Read();
        }
    }
}

ASP.NET Starter Kit

      微软的东西使得程序员的工作变得越来越轻松,当然同时也使得靠吃程序员这碗饭过活的人日子越来越难过了。最近我在微软 MSDN 中文网站上又发现了新东西(当然对于经常关注微软动态,且经常浏览微软几大英文门户的人们来说,这也许算是老掉牙的东西了),一套开放源代码的、完全免费的且说不好具有什么种类版权的门户网站入门套件: ASP.NET Starter Kit 。据说如果你有足够的实力,经过重写的源代码甚至是可以直接拿出来卖钱的。(怎么越看越像 BSD 版权呢?)有这么好的东西,怎么能不尝试一下呢?以下就是我对 ASP.NET Starter Kit 体验的随笔。

一、什么是 ASP.NET Starter Kit
      关于这个问题我是无法给出什么权威性的解释的,但是现如今像这样的东西满网络里到处都是,大家肯定也不会陌生,只不过她系出名门,由微软的牛人们写出来的,地位自然显赫得多了。不要简单的望文生义,以为这套东西是为初学者准备的,虽然我还没有看到那里,但据说这个套件里的各个部分都使用了看起来令人兴奋的设计模式,绝对是学习和使用的不二之选。

      本套件包含五个部分, Portal Starter Kit (门户网站入门套件), Commerce Starter Kit (电子商务入门套件), Reports Starter Kit (报表产生入门套件), TimeTracker Starter Kit (项目追踪入门套件) 和 Community Starter Kit (论坛入门套件)。每一个部分都可以直接拿过来部署,也可以经过自己的定制,重新发布。据说已经有几个网站开始采用该套件中的 Portal 部分部署自己的门户网站,相应的内容大家可以到微软的网站上找到。

二、如何获得 ASP.NET Starter Kit
      当然是在微软的网站上,英文版的可以到以下网址找到: http://www.asp.net/default.aspx?tabindex=9&tabid=47 , 而中文版的可以到 MSDN 中文网站上去下载。

三、ASP.NET Starter Kit的版权声明
      随着 ASP.NET Starter Kit 的分发,微软也同时也分发了该套件的版权声明,在 EULA.rtf 文档中,如果真的有人想要用它做些什么东西的话,最好还是好好看一看。我粗略的看了一下,怎么看怎么像 BSD 版权,包括什么必须保留微软版权、不允许加入其它的有版权产品,不允许使用微软的标志等等。

四、ASP.NET Starter Kit主要部分介绍
      由于本人也是刚刚开始接触这个东西,加之目前有一个项目恰好可以用它来改,所以我首先选择了门户部分作为学习的突破口。

4.1 ASP.NET Portal
4.1.1 安装
      据文档中说,该套件应该包含一个 .exe 或 .msi 的安装程序,不幸的是我在相应的 Setup 目录下没有找到这样的程序(很可能是我找到的这个发布包有问题),所以只好用功能强大的,但是相对难使的命令行工具。在 Setup 目录下可以找到 Cmd Installer 目录,进到里面就会看到 PRTL.exe 的命令行安装程序、两个写好的用于本地和远程安装的示例批处理文件 LocalFullInstall.bat 和 RemoteFullInstall.bat 、用于安装数据库的数据库脚本和 PRTL.exe 安装程序的用户文档。根据该用户文档,PRTL命令的参数如下:

4.1.1.1 INSTALL_OPTION
      用于设定安装模式,有四个选项:

FULL 既安装 Web 又安装数据库。启用此选项需要提供所有的必选参数。
DB 仅安装指定的数据库实例。启用此选项需要提供 DB_FILES_DIR, DB_SERVER, DB_NAME, DB_OWNER_LOGIN, DB_OWNER_PWD, SQL_ADMIN_USER, SQL_ADMIN_PWD参数。
WEB 仅安装Web组件。启用此选项需要提供WEB_FILES_DIR, TARGET_DIR, IIS_SERVER, WEB_SITE, VDIR, DB_CONNECTION, DB_NAME, DB_SERVER, DB_OWNER_LOGIN, DB_OWNER_PWD 参数。
COPY_ONLY 仅将 WEB_FILES_DIR 目录下的所有内容拷贝到 IIS_SERVER 机器上的 TARGET_DIR目录下。启用此选项需要提供 IIS_SERVER, WEB_FILES_DIR, TARGET_DIR 参数。

4.1.1.2 WEB_FILES_DIR
      待安装的 Web 页面所在的相对或绝对路径。一般在你解开的压缩包里会有一个名为 PortalCSVS (如果是 VB 工程的话似乎就应该叫 PortalVBVS )的目录,此下的文件就是所谓的待安装的 Web 页面。

4.1.1.3 DB_FILES_DIR
      数据库脚本所在的相对或绝对路径。在 PRTL.exe 文件所在的目录里面有一个名为 SQL Scripts 的目录,它就是数据库脚本所在的目录。其中有四个 SQL 脚本文件。

4.1.1.4 TARGET_DIR
      WEB_FILES_DIR 目录下的内容将要被拷贝到的目标路径(相对或绝对均可)。在该目标路径下 PRTL 会创建一个子目录。

4.1.1.5 IIS_SERVER
      安装有 IIS 的机器的计算机名。同时该计算机也是 TARGET_DIR 所在的计算机。

4.1.1.6 WEB_SITE
      一个已经存在的 Web 站点, VDIR 参数指定的虚拟目录将会在该站点中创建。此参数应该提供站点属性中描述栏中的内容。例如默认网站。

4.1.1.7 VDIR
      将要安装的门户网站所在的虚拟目录的名字。建议该名称最好叫做 PortalCSVS (即与 分发包中 Web 页面内容所在的那个目录名相同,因为当用 VS.net 打开工程的时候,会默认到名称为 PortalCSVS 的虚拟目录下寻找站点)。

4.1.1.8 DB_CONNECTION
      门户应用程序连接数据库的方式。只能使用SQL选项, Windows 集成认证是不支持的。

4.1.1.9 DB_SERVER
      安装有 SQL Server 数据库的机器的计算机名。

4.1.1.10 DB_NAME
      SQL Server 中将要安装所有数据库对象的数据库名称。(不是数据库服务器实例的名称。)

4.1.1.11 DB_OWNER_LOGIN
      将要具有该数据库 Owner 角色的登陆名。如果该登陆名不存在,则会被创建。

4.1.1.12 DB_OWNER_PWD
      登陆名的密码。

4.1.1.13 SQL_ADMIN_USER
      具有可以创建数据库实例和用户权限的 SQL 用户名。

4.1.1.14 SQL_ADMIN_PWD
      该用户的密码。

4.1.1.15 LOG_FILE (Optional)
      安装日志的文件名。可以提供希望存储该日志的完整路径。如果没有提供该参数,日志文件将会被创建到同PRTL.exe相同的目录下。

例如在我的机器上,安装命令如下:
PRTL.exe INSTALL_OPTION=FULL WEB_FILES_DIR="..\..\PortalCSVS" DB_FILES_DIR="SQL Scripts" TARGET_DIR="C:\inetput\wwwroot\portal" IIS_SERVER="localhost" WEB_SITE="默认网站" VDIR="PortalCSVS" DB_CONNECTION=SQL DB_SERVER="localhost" DB_NAME="Portal" DB_OWNER_LOGIN="PortalAdmin" DB_OWNER_PWD="123" SQL_ADMIN_USER="SA" SQL_ADMIN_PWD="sa" LOG_FILE="installLog.txt"

生成的安装日志内容如下:
[2004-07-10 15:03:46] Setup Initialized …
[2004-07-10 15:03:46] Start Installing Web Components …
[2004-07-10 15:03:46] Copying web files from H:\Documents and Settings\MyUserProfile\My Documents\Visual Studio Projects\ASP.NET Portal (CSVS)\PortalCSVS to C:\inetput\wwwroot\portal\PortalCSVS
[2004-07-10 15:03:47] Updating web.config…
[2004-07-10 15:03:47] Updating EditHtml.aspx…
[2004-07-10 15:03:47] Creating Virtual Directory: PortalCSVS, on PASTHERO
[2004-07-10 15:03:48] Start Installing Database: localhost.Portal …
[2004-07-10 15:03:48] Executing SQL Script: 8b21cd7e-006f-4f09-8016-7a26f8dfd412CreateDB.sql
[2004-07-10 15:03:50] Executing SQL Script: f241dac2-1e7e-4e70-80ab-49e5ad575e98CreateDBObjects.sql
[2004-07-10 15:03:52] Executing SQL Script: c51bb364-0d31-4c67-9813-747f3ad813cfGrantPermission.sql
[2004-07-10 15:03:52] Executing SQL Script: 2f166c1a-ef9a-4422-841e-63026f5f13f2LoadData.sql
[2004-07-10 15:03:54] Setup Completed!