这些天我在家里用C#实现一个资源管理器控件,功能和Windows自己的那个类似,我想把它实现的和Windows的尽量接近 — 当然是越接近越好啦。
问题马上就来了,.NET目前似乎并不是很擅长搞这个,或者说那些相关的Windows API还没被收入进FCL(WinFX/.NET 3.0应该已经收录了吧),这下可麻烦了:
- 地址栏
MFC里面有个CComboBoxEx类,可以用来做带层次缩进和图标的ComboBox,实现资源管理器的地址栏。但是.NET没有,不过还好可以继承System.Windows.Forms.ComboBox类,然后重写OnDrawItem方法,搞定
- 文件图标
.NET 2.0终于开始支持这个功能了,就是Icon.ExtractAssociatedIcon方法,不过它返回的总是文件的大图标.... 而Windows XP里面一个文件可以包含大中小三种尺寸的图标(每种还有三等色深所以一共是九种),所以还是得调用SHGetFileInfo这个API;
还有些图标是在shell32.dll里头的(比如文件夹的图标),那样还得把它们枚举出来... 这个得用ExtractIconEx;
此外还要维护一个ImageList,不然占用太多Icon句柄,会发生GDI Failue的... 这个还好,FCL已经有这个类支持了
- 文件类型信息
每种文件类型都有一个Description,比如说.doc就是Microsoft Word Document,也记录在注册表里头。这个问题我在CSDN已经回答了N遍,不想再打一遍了
- 文件夹背景图
这个暂时还没功夫去研究,并且我认为这个功能相对不是那么的重要。还有文件夹缩略图之类的... 不是一般的麻烦,日后再说
- 左边的树形视图
.NET自带了TreeView控件,只要有Icon,剩下的工作就很容易了
- 右键菜单
菜单里有很多项目都是记录在注册表里面的。这个又是个很大的话题... 总而言之就是麻烦得要死。不过那些Cut、Copy、Delete、Properties什么的操作的实现倒是简单,可以直接用ShellExecuteEx这个API
- 快捷方式
解析.lnk文件到目标文件,可以直接打开文件流读取里面的字符,在某个偏移量的某个位置是可以找到的,不过这样不“安全”,还是用API吧。MSDN说可以使用IShellLink和IPersistFile这两个COM接口,代码虽然很ugly但是不算很长。问题又来了,IShellLink没有继承IDispatch,只是继承了IUnknown,这意味着tlbimp.exe也帮不了忙(貌似),得自己声明那些接口和组件原型... 阿门,我今天搞了一晚上,还是不大对头,想睡觉了...
- 主文件视图
这个可以用.NET自带的ListView控件应付,和Windows一样它也带了5种浏览模式(Title、List、Detail等等),不用自己实现
- 文件/文件夹的Tooltip
Tooltip的内容并不是固定的... 比如说doc文件的Tooltip还会包括Author等信息,mp3文件则会有Duration之类的,这个要用Structure Storage
相关的COM组件,超级麻烦,暂时不想管了~
- .....
.....
不过今天也不是没有收获,在Google上找到了这么个网站:
http://www.vbaccelerator.com/home/NET/Code/Libraries/Shell_Projects/index.asp
他们把很多Shell相关的东西给封装了一下,比如我一直郁闷的文件快捷方式的问题,他们已经提供了解决方案。早知道有这个,我就不用那么费劲了。
最近老夫心情很好,上海天气也前所未有的暖和,所以这篇超级流水账纯属YY。
想必是被公司里的数据库高手惯坏了,这两年来自我感觉离数据库距离越来越远,以至于SQL Server 2005发布这么久 — 连SP1都出来了 — 我都还没把它装到硬盘上;如此严重的轻视MSDN Subscriber帐号的存在,在还有三天MVP帐号就要到期之前(美丽的Sisley阿姨快救救我吧),我在想这世上还有做.NET的人不会数据库吗?自己到底还算不算一个.NETer or MSer?出于这个严重问题的考虑,我决定,为了支持微软(以增加连任MVP的可能性),我决定,去MSDN当一个先,再晚就来不及了!
Initially我看上了SQL Server 2005 Developer Edition,原因是我的机器跑的是Windows XP SP2,而Windows XP没有Enterprise Edition,并且SQL Server 2005没有Professional Edition,作为一个“出于学习和研究目的”的Developer,当然选择Developer Edition啦。在当当当当当当当当当当了一天两夜之后,把ISO载入DAEMON(此时突然想起微软寄来的DVD里应该已经带了,我浪费了全人类36小时的宝贵的电力能源),安装,点开开始菜单,晕,SQL Server 2000的Enterprise Manager去哪儿了我说?MSN上随便抓个人一问,子曰现在那个玩意的名字叫SQL Server 2005 Management Studio,啊,变高级了,不过可惜我找不到,开始菜单里没有啊,卖糕的,没有这个东西该怎么管理数据库啊,搜索硬盘也没找到那个EXE(子曾经曰过是SSMS.EXE),算了,归为RPWT,重装。结果我成功的证明了我没有RPWT,因为我还是没有找到那个Management Studio。此时突然想起我以前一个Manager的口头禅:
“微软的东西就是烂!”
阿弥陀佛,迫于人家管理层的淫威,我不能说“实际上是你自己脑子有病”或者是“晚上吃药忘了开灯吧”,等等,虽然那些从小到大学会的脏话在脑海里已经翻腾了好多年;只能说“没有吧,你的机器又出啥问题了我帮你看看”。
受CSDN论坛影响,我的潜意识也变得越来越浮躁,再加上我不得不边看球边装机器的客观原因,所以我心安理得的没有按照正规流程(看文档 -> 查资料 -> 自个儿琢磨 -> 再看文档,循环到Stack Overflow为止)研究问题原因,直接在MSN上又抓了个人问。不巧的是这位大哥似乎也在看球(和我不同的是我可以边看电视边看电脑,他不行),好不容易等到中场的时候那位大哥才来了一句:
“啊?你居然还在用Windows XP?我不用XP已经好多年了都。”
言下之意是我这个火星来的也应该学学地球人怎么用Windows Server 2003了,但是我认为我64-bit的Windows XP SP2号称使用了Windows Server 2003的内核,理论上一样的;况且以前SQL Server 2000都没问题,根据寡人这些年来观察猪奔跑的经验,新版本应该也可以的。算了,人家还有一个意思就是“我不对使用Windows XP的人提供技术支持”。搞不懂了,这年头装个OS都能让人产生优越感(特别是那帮装了Macintosh的,老觉得我的XP就是盗版了他们Apple买来的界面技术)。
由于已经确定这个不是我的RPWT,再加上那位大哥的误导,我开始怀疑我的OS起来。原因是我的XP是经过nLite RIP的,万一那天脑袋不清醒弄丢了什么组件,造成一些超自然现象也在情理之中。但是为了这种破问题就重装系统值得吗,太无能了吧?为了免受江湖高手的耻笑,我打算坚持在PC上使用现有系统,在另一台刚买的DELL本本上再试一次(sigh,由于买的是DELL的机器,我还是没能逃脱来自黑社会人士的鄙视,地球真的是一个很奇怪的星球)。这个时候终于注意到MSDN 上赫然写着:
New in Subscriber Downloads - SQL Server 2005 Express Edition with Advanced Services
SQL Server Express with Advanced Services has all of the features in SQL Server 2005 Express Edition, plus you can:
- Easily manage and administer SQL Server Express with a new easy-to-use graphical management tool -- SQL Server 2005 Management Studio Express (SSMSE).
- Issue full-text queries against plain character-based data in SQL Server tables. Full-text queries can include words and phrases, or multiple forms of a word or phrase.
- Run SQL Server Reporting Services reports on local relational data.
并且还集成了SP1,大小是235MB。下载了安装,YOXI,我终于把它搞定了。
BTW,非MSDN订阅用户可以在这里下载:
http://msdn.microsoft.com/vstudio/express/sql/download/
----- Updated on 7/1/2006
MVP RENEWED 
----- Updated on 12/2/2006
打算回复这个post的这个评论:
“技术含量不高的文章,谁叫你是mvp呢,严格来讲,mvp最好少写这样的文章,mvp就象明星,哪一天当了明星,你是高手也好,不是高手也好,你就得认真些,冒办法.”
不知道是越活越糊涂还是越活越明白,现在我最喜欢的是“郑渊洁”式的风格。参考:http://blog.sina.com.cn/u/482646730100019v
“多年以来,我们的社会里都有一个被普遍认可的观念,那就是认为公众人物应该保持良好的形象。这种想法今天仍然非常流行,这在某种程度上说明了我们这个社会整体上仍然欠缺理性的思考能力。我们只能说我们希望公众人物保持良好形象,不该说公众人物应该保持良好形象。当公众人物做了让你讨厌的事情(但不涉及是非对错)的时候,你可以批评他、谴责他、讽刺他、甚至骂他,但不能说他应该怎么怎么样。这种蛮横的公众想法的本质其实是:“你不应该做让我讨厌的事情,我不喜欢你这样。”这不是莫名其妙吗?很多有精神疾病的人容易这样一厢情愿。”
一大堆实用类库
有一帮家伙成天没事,而且貌似有点无聊,所以搞了一大堆乱七八糟毫无联系的项目,最后标以Free Software放在网上,就成了Mentalis.org。和一般的垃圾软件下载站所不同的是,他们的东西着实都还不错,而且全都是原创,比较符合寡人的胃口,而且今天我也很无聊,所以写篇BLOG分享之:
http://www.mentalis.org/soft/projects.qpx
http://www.mentalis.org/soft/classes.qpx
GPL和BSD
Mentalis.org这个网站的第一个特别之处就是他们的No GPL的Mark(右图)。而且这个Mark是覆盖在Free Software之上的,看的出来他们很不喜欢GPL这个License。想起我的FtpClient Library也是基于GPL,也曾遭到网友反对。这个怎么说呢,我用GPL,不管你怎么想,但最起码你很难理直气壮的说我做的不对。我明白,对于开源软件/库/代码的使用者来说,还是BSD更“爽”一点。这也许是因为BSD协议并不限制你把代码使用于商业项目中,不像GPL那样会强制你的软件也开源。
事实上,我现在也越来越不喜欢GPL了,我打算把我的那个项目升级到一个新版本,然后改成BSD(这样做对于GPL协议来说是合法的)。原来用GPL的原因是我小气,怕别人下载了改改就拿去卖钱,我不爽(呵呵)。现在想法转变,原因是GPL这种防君子不防小人的条文,最起码在我周围这一大圈面积的地方是起不到什么作用的。而实际情况更糟,小人没能防成,君子们却都被我拒之门外了。既然如此,干脆好人做到底,就BSD吧。(看起来我的确是不如Mentalis.org那帮人那么伟大,反对GPL居然是因为这种理由)
Creative Commons License
上个月我在这个BLOG的Title下面加了个图标,就是CC - Some Rights Reserved那个。对于网上发表的文本作品,我打算都使用这个Creative Commons License。有个网站很贴心,知道我等懒人已经懒到连License都不想抽时间写了(要死啊),所以发明了个License制作向导:http://creativecommons.org。这个也推荐大家一用。
Donate
关于很多非盈利性质的软件,往往都喜欢在自己网站的角落里加一个Donate链接(往往是基于PayPal的在线支付系统)。不知道各位看官对这个现象有何看法,反正我是觉得很别扭。这就像那些穷酸的艺人,一边在路边卖艺,一边等着路人把硬币扔到自己面前的瓷碗里,而瓷碗里的那些钱真的是少得可怜。他们真的有很多共同点:
- 不指望自己做的东西能卖钱
你会拿一个自己写的class就去卖钱吗,除非你实现了个什么前无古人的算法什么的,不然会遭鄙视的
- 都小有成就但别人只要愿意往往也能办到
大部分 - 我不是说全部 - 开源软件实际上是学雷锋做好事,并无多少创意,只能说多少有几分新意吧
- 互相看不起
古代文人相轻,现代程序员相轻。这一点,甚至直接体现在论坛回帖和BLOG的回复里面
- 没钱(文雅一点的说法就是,收入相对较低)
古往今来从不缺有才无财的人,而古代很多秀才们往往希望能把自己的字画卖出去赚几个小钱,好供养老婆孩子还有高堂,却又不敢大声吆喝。何必那,现在已经21世纪了,拜托,这不是什么可耻的事情
- 却没人同情
盗版软件就不说了,你可能还是坚持认为一切商业行为都是可耻的,所以盗版有理。那开源软件呢,你是不是也该出于良心考虑去Donate一把?可是我周围却从没见过甚至听说过有人去做过这种事情(阿门,好像我也没做过)
我知道我这么说一定会惹毛一大堆人,不过千万不要觉得我有恶意,我可一点都没有看不起他们的意思;相反那些开源软件的作者都是热心的网络大侠,技术好、人品也好。我只是感慨一下而已(不知道为啥一看到那个漂亮的Donate Button就联想到那种装了些硬币和毛票的瓷碗),不是想在这里下结论。欢迎拍砖。
扯太远了,以上仅代表个人观点,收笔~ 
正则表达式是设计为用来匹配文本的,可是论坛里头老是有这种问题:
“谁能帮我写个正则表达式,验证从0到180的数字。”
我说,正则表达式不是用来干这个的,匹配和验证是不一样的概念,数据验证过程可能包含很复杂的逻辑,而这个时候要用文本匹配来模拟,就变得很不科学。虽然,这个问题对于正则表达式并不是无解,可以写成这样:
^([0-9]{1,2}|1[0-7][0-9]|180)$
但用编程语言来判断显然方便直观的多,(x >= 0 && x <= 180)就可以了。你大概觉得上头那个正则表达式还不算复杂,那么看看这个:
“匹配从874到142857的数字。”
那正则表达式就得写成:
^(87[4-9]|8[8-9][0-9]|9[0-9]{2}|[1-9][0-9]{3,4}|1[0-3][0-9]{4}|14[0-1][0-9]{3}|142[0-7][0-9]{2}|1428[0-4][0-9]|14285[0-7])$
尽管还是可以做到,不过这个就有点走火入魔了。
不过呢,如果你非得这么做(比如客户逼你,我就是...),那我写了个小工具来干这个事情(写这种正则表达式,毕竟还是有规律可循)。下面是截图:


刚才那两个正则表达式,也是用这个工具生成的。代码在这里。
P.S.
这个工具目前尚不支持小数和负数,也许你可以基于我提供的代码写个更完整的版本出来。(不过,真的有必要吗?)
此外,这个工具尚未经过严格测试。发现问题了请给我发邮件或者在这里留言,多谢。
// MainForm.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Globalization;
using System.Text;
using System.Windows.Forms;
namespace XGenerator
{
public partial class MainForm : Form
{
IFormatProvider formatter = new NumberFormatInfo();
public MainForm()
{
InitializeComponent();
#if DEBUG
tbMin.Text = "874";
tbMax.Text = "142857";
#endif
}
private void btnGenerate_Click(object sender, EventArgs e)
{
int minValue, maxValue;
if (!int.TryParse(tbMin.Text, out minValue) || !int.TryParse(tbMax.Text, out maxValue)
|| minValue > maxValue)
{
rtbResult.Text = string.Format("Please fill the two text boxes correctly.");
return;
}
string min = minValue.ToString(formatter), max = maxValue.ToString(formatter);
if (minValue == maxValue)
{
rtbResult.Text = min; // == max
return;
}
int minLength = min.Length, maxLength = max.Length;
StringBuilder result = new StringBuilder();
int currentNum, startNum, endNum;
result.Append("^(");
if (minValue == 0)
{
if (minLength == maxLength)
result.AppendFormat("[0-{0}]|", maxValue);
else
result.AppendFormat("{0}|", Build0To9Block(1, maxLength - 1));
}
else if (minLength == maxLength)
{
string commonPart = null;
for (int i = 0; i < minLength; i++)
{
if (min[i] == max[i]) continue;
else
{
if (i == 0) break;
else
{
result.Append(commonPart = min.Substring(0, i));
result.Append("(");
min = min.Remove(0, i);
minLength -= i;
max = max.Remove(0, i);
maxLength -= i;
break;
}
}
}
for (int i = minLength - 1; i > 0; i--)
{
currentNum = (int)min[i] - 48; // char -> int
startNum = i == minLength - 1 ? currentNum : currentNum + 1;
endNum = 9;
if (endNum < startNum) continue;
result.AppendFormat("{0}{1}{2}|", min.Substring(0, i),
BuildBlock(startNum, endNum),
Build0To9Block(minLength - i - 1));
}
startNum = (int)min[0] - 47;
endNum = (int)max[0] - 49;
if (startNum <= endNum)
result.AppendFormat("{0}{1}|", BuildBlock(startNum, endNum), Build0To9Block(minLength - 1));
for (int i = 1; i < maxLength; i++)
{
currentNum = (int)max[i] - 48; // char -> int
startNum = 0;
endNum = i == maxLength - 1 ? currentNum : currentNum - 1;
if (endNum < startNum) continue;
result.AppendFormat("{0}{1}{2}|", max.Substring(0, i),
BuildBlock(startNum, endNum),
Build0To9Block(maxLength - i - 1));
}
if (result[result.Length - 1] == '|') result.Remove(result.Length - 1, 1);
if (commonPart != null) result.Append(")");
}
else
{
for (int i = minLength - 1; i >= 0; i--)
{
currentNum = (int)min[i] - 48; // char -> int
startNum = i == minLength - 1 ? currentNum : currentNum + 1;
endNum = 9;
if (endNum < startNum) continue;
result.AppendFormat("{0}{1}{2}|", min.Substring(0, i),
BuildBlock(startNum, endNum),
Build0To9Block(minLength - i - 1));
}
if (minLength < maxLength - 1)
result.AppendFormat("[1-9]{0}|", Build0To9Block(minLength, maxLength - minLength + 1));
}
if (maxLength > minLength)
{
for (int i = 0; i < maxLength; i++)
{
currentNum = (int)max[i] - 48; // char -> int
startNum = i > 0 ? 0 : 1;
endNum = i == maxLength - 1 ? currentNum : currentNum - 1;
if (endNum < startNum) continue;
result.AppendFormat("{0}{1}{2}|", max.Substring(0, i),
BuildBlock(startNum, endNum),
Build0To9Block(maxLength - i - 1));
}
}
if (result[result.Length - 1] == '|') result.Remove(result.Length - 1, 1);
result.Append(")$");
rtbResult.Text = result.ToString();
Clipboard.SetText(rtbResult.Text);
}
private string Build0To9Block(int count)
{
return Build0To9Block(count, count);
}
private string Build0To9Block(int min, int max)
{
if (min == 0 && max == 0) return "";
else if (min == 1 && max == 1) return "[0-9]";
else if (min == max) return string.Format("[0-9]{{{0}}}", min);
else return string.Format("[0-9]{{{0},{1}}}", min, max);
}
private string BuildBlock(int min, int max)
{
if (min == max) return min.ToString(formatter);
else return string.Format("[{0}-{1}]", min, max);
}
}
}
// MainForm.Designer.cs
namespace XGenerator
{
partial class MainForm
{
///
/// Required designer variable.
///
private System.ComponentModel.IContainer components = null;
///
/// Clean up any resources being used.
///
/// true if managed resources should be disposed; otherwise, false.
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
///
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
///
private void InitializeComponent()
{
this.lbMin = new System.Windows.Forms.Label();
this.lbMax = new System.Windows.Forms.Label();
this.tbMin = new System.Windows.Forms.TextBox();
this.tbMax = new System.Windows.Forms.TextBox();
this.rtbResult = new System.Windows.Forms.RichTextBox();
this.btnGenerate = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// lbMin
//
this.lbMin.AutoSize = true;
this.lbMin.Location = new System.Drawing.Point(13, 13);
this.lbMin.Name = "lbMin";
this.lbMin.Size = new System.Drawing.Size(27, 13);
this.lbMin.TabIndex = 0;
this.lbMin.Text = "Min:";
//
// lbMax
//
this.lbMax.AutoSize = true;
this.lbMax.Location = new System.Drawing.Point(12, 36);
this.lbMax.Name = "lbMax";
this.lbMax.Size = new System.Drawing.Size(30, 13);
this.lbMax.TabIndex = 1;
this.lbMax.Text = "Max:";
//
// tbMin
//
this.tbMin.BackColor = System.Drawing.SystemColors.Info;
this.tbMin.Location = new System.Drawing.Point(47, 11);
this.tbMin.Name = "tbMin";
this.tbMin.Size = new System.Drawing.Size(285, 20);
this.tbMin.TabIndex = 2;
this.tbMin.Text = "0";
//
// tbMax
//
this.tbMax.BackColor = System.Drawing.SystemColors.Info;
this.tbMax.Location = new System.Drawing.Point(47, 34);
this.tbMax.Name = "tbMax";
this.tbMax.Size = new System.Drawing.Size(285, 20);
this.tbMax.TabIndex = 3;
this.tbMax.Text = "0";
//
// rtbResult
//
this.rtbResult.BackColor = System.Drawing.SystemColors.Info;
this.rtbResult.Font = new System.Drawing.Font("Microsoft Sans Serif", 11.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.rtbResult.Location = new System.Drawing.Point(12, 60);
this.rtbResult.Name = "rtbResult";
this.rtbResult.ReadOnly = true;
this.rtbResult.Size = new System.Drawing.Size(452, 187);
this.rtbResult.TabIndex = 4;
this.rtbResult.Text = "";
//
// btnGenerate
//
this.btnGenerate.Location = new System.Drawing.Point(338, 11);
this.btnGenerate.Name = "btnGenerate";
this.btnGenerate.Size = new System.Drawing.Size(126, 43);
this.btnGenerate.TabIndex = 5;
this.btnGenerate.Text = "Generate";
this.btnGenerate.UseVisualStyleBackColor = true;
this.btnGenerate.Click += new System.EventHandler(this.btnGenerate_Click);
//
// MainForm
//
this.AcceptButton = this.btnGenerate;
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(476, 259);
this.Controls.Add(this.btnGenerate);
this.Controls.Add(this.rtbResult);
this.Controls.Add(this.tbMax);
this.Controls.Add(this.tbMin);
this.Controls.Add(this.lbMax);
this.Controls.Add(this.lbMin);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
this.MaximizeBox = false;
this.Name = "MainForm";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "Numberic Regular Expressions Generator";
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Label lbMin;
private System.Windows.Forms.Label lbMax;
private System.Windows.Forms.TextBox tbMin;
private System.Windows.Forms.TextBox tbMax;
private System.Windows.Forms.RichTextBox rtbResult;
private System.Windows.Forms.Button btnGenerate;
}
}