[Windows Mobile .NET CF] 中英字典 – Day9
写了一些网络应用相关软件, 靠着网络, 手机成了强大的工具界面.
但是接下来我想写一些可以不需要网络的 Windows Mobile Applications, 比方说, 中英字典.
字典这样的软件其实不难写, 门槛就在于字典档…
可以从网络上找到一些免费的字典档, 比方说 pydict
根据研究它的字典档, 有几个特性:
1. 依照 a ~ z 分开存放英文数据
2. 已经排序 (a-z)
3. 是 big5 编码.
4. 用 ‘=’ 切割数据, 分别为 英文=中文=音标
根据 pydict 的程序内容, 我写了一个最基本的 class 来代表一笔字典档的数据,
也把音标以及词性转换出来, 程序如下:
///
/// 代表一笔中英文字典数据
///
public class dictdata
{
///
/// file pos , 内部使用
///
public long filepos { get; set; }
///
/// english
///
public string eng { get; set; }
///
/// chinese
///
public string chinese { get; set; }
///
/// soundmark
///
public string soundmark { get; set; }
///
/// 词性表
///
private static string[] prop = new string[] {
" ", " ", " ", "<<形容词>>", "<<副词>>", "art. ",
"<<连接词>>", "int. ", "<<名词>>", " ", " ", "num. ",
"prep. ", " ", "pron. ", "<<动词>>", "<<助动词>>",
"<<非及物动词>>", "<<及物动词>>", "vbl. ", " ", "st. ",
"pr. ", "<<过去分词>>", "<<复数>>", "ing. ", " ", "<<形容词>>",
"<<副词>>", "pla. ", "pn. ", " " };
///
/// 显示中文内容
///
public string displaychinese
{
get
{
//根据 词性表 改变内文
StringBuilder result = new StringBuilder();
for (int i = 0; i < chinese.Length; i++)
{
int idx = Convert.ToInt32(chinese[i]);
if (idx < prop.Length)
{
if (result.Length != 0)
{
result.Append("rn");
}
result.Append(prop[idx]);
}
else
result.Append(chinese[i]);
}
return result.ToString();
}
}
///
/// 音标表
///
private static Dictionary soundmarktable = new Dictionary() {
{0x01,"I"},
{0x02,"E"},
{0x03,"ae"},
{0x04,"a"},
{0x06,"c"},
{0x0b,"8"},
{0x0e, "U"},
{0x0f, "^"},
{0x10, "2"},
{0x11, "2*"},
{0x13, "2~"},
{0x17, "l."},
{0x19, "n_"},
{0x1c, "&"},
{0x1d, "S"},
{0x1e, "3"},
};
public string displaysoundmark
{
get
{
// 根据音标表显示
StringBuilder result = new StringBuilder();
for (int i = 0; i < soundmark.Length; i++)
{
int idx = Convert.ToInt32(soundmark[i]);
if ((i == 0) && (idx == 0x65))
continue;
string founddisplay;
if (soundmarktable.TryGetValue(idx, out founddisplay) == true)
result.Append(founddisplay);
else
result.Append(soundmark[i]);
}
return result.ToString();
}
}
public string displaysoundmarkandchinese
{
get
{
return "音标:" + displaysoundmark + "rn" + displaychinese;
}
}
private static char[] splitchar = new char[] { '=' };
public dictdata(string data, long pos)
{
filepos = pos;
if (string.IsNullOrEmpty(data) == false)
{
string[] parts = data.Split(splitchar);
eng = (parts.Length >= 1) ? parts[0] : string.Empty;
chinese = (parts.Length >= 2) ? parts[1] : string.Empty;
soundmark = (parts.Length >= 3) ? parts[2] : string.Empty;
}
else
{
eng = string.Empty;
chinese = string.Empty;
soundmark = string.Empty;
}
}
public override string ToString()
{
return eng;
}
} ,> ,>
我打算展示两种方法来查询这样的字典档,
一种是直接在文件做搜寻, 另一种则是直接全部载入到内存做搜寻.
当然是全部载入到内存, 直接利用 .NET Framework 的 Container Class 搜寻简单得多.
但是大家应该知道, 之所以简单得多, 是因为 .NET Framework 已经帮我们做掉很多了.
所谓倒吃甘蔗, 所以先介绍如何在文件直接搜寻.
因为是已经根据英文排序好的数据, 所以就要善用排序搜寻.
已经排序好的数据, 又快又好写的搜寻方法就是 BinarySearch.
虽然 .NET Framework 有 BinarySearch, 但是由于文件是以 byte 为单位,
而数据却是以一行一行为单位, 所以内建的 BinarySearch 是不能用的,
所以我们不但要自己写, 还要在计算的时候做 byte 转换到一行一行的数据.
也就是说, 这算是有一点变化的 BinarySearch 欧 ^_^
(在下的本行可以算是搜寻吧?! 所以这一定要写得好一点才不会丢脸!)
BinarySearch 的主要函数程序 :
///
/// 用 Binary Search 找到英文字 index , 找不到就返回最接近的.
///
///
///
///
///
///
///
public dictdata SeekToLine(Stream r, string index, long zonebegin, long zoneend, Encoding encode)
{
// 默认 middle 为 readpreline.
r.Position = (zonebegin + zoneend) / 2;
dictdata middledata = ReadPreLine(r, encode);
// 找到正确的英文字
if (string.Compare(middledata.eng, index, true) == 0)
return middledata;
// read pre line 找不到正确的英文字, 有可能是 middle 要采用 readnextline...
if (middledata.filepos == zonebegin)
{
r.Position = (zonebegin + zoneend) / 2;
middledata = ReadNextLine(r, encode);
// 找到正确的英文字
if (string.Compare(middledata.eng, index, true) == 0)
return middledata;
// 找不到正确的英文字
if (middledata.filepos == zoneend)
return middledata;
}
string middleindexlow = middledata.eng.ToLower();
int cmp = index.CompareTo(middleindexlow);
if (cmp < 0)
{
// 搜寻 Binray Tree 左边
return SeekToLine(r, index, zonebegin, middledata.filepos, encode);
}
else
{
// 搜寻 Binray Tree 右边
return SeekToLine(r, index, middledata.filepos, zoneend, encode);
}
}
当然, 要搭配将 byte index 转换为以一行一行数据为基本的函数码:
///
/// 往前搜寻直到发现 endtag, 回传的 position 指向 endtag 下一个 byte
///
///
///
///
private long seekback(Stream r, int endtag)
{
long currentpos = r.Position;
while (currentpos > 0)
{
currentpos--;
r.Position = currentpos;
if (r.ReadByte() == endtag)
return currentpos+1;
}
r.Position = 0;
return 0;
}
///
/// 往后搜寻直到发现 begintag, 回传的 position 指向 begintag 下一个 byte
///
///
///
///
private long seeknext(Stream r, int begintag)
{
long currentpos = r.Position;
long finalpos = r.Length;
while (currentpos < finalpos)
{
currentpos++;
r.Position = currentpos;
if (r.ReadByte() == begintag)
return currentpos + 1;
}
r.Position = finalpos;
return finalpos;
}
///
/// 读出 stream r 目前位置的前一行
///
///
///
///
public dictdata ReadPreLine(Stream r, Encoding encode)
{
long pos = seekback(r, 0x0a);
StreamReader endReader = new StreamReader(r, encode);
try
{
return new dictdata(endReader.ReadLine(), pos);
}
finally
{
endReader.DiscardBufferedData();
}
}
///
/// 读出 stream r 目前位置的下一行
///
///
///
///
public dictdata ReadNextLine(Stream r, Encoding encode)
{
long pos = seeknext(r, 0x0a);
StreamReader sr = new StreamReader(r, encode);
try
{
return new dictdata(sr.ReadLine(), pos);
}
finally
{
sr.DiscardBufferedData();
}
}
于是, 我们就可以写出英文找到中文的搜寻程序:
///
/// 找回最接近 index 的数笔数据, 最多回传 maxcount 笔
///
///
///
///
///
///
private List SeekData(Stream r, string index, Encoding encode, int maxcount)
{
dictdata firstdata = SeekToLine(r, index, 0, r.Length, encode);
return ReadData(r, firstdata.filepos, encode, maxcount);
}
///
/// 读取数据
///
///
///
///
///
///
private List ReadData(Stream r, long beginpos, Encoding encode, int maxcount)
{
List result = new List();
r.Position = beginpos;
StreamReader sr = new StreamReader(r, encode);
try
{
while (maxcount-- > 0)
result.Add(new dictdata(sr.ReadLine(), 0));
}
finally
{
sr.DiscardBufferedData();
}
return result;
}
///
/// 查询英文, 回传最多 maxcount 个字典数据
///
///
///
///
public List EnglishToChinese(string english, int maxcount)
{
if (english.Length == 0)
return new List();
string filename = Path.Combine(libpath, english[0] + ".lib");
if ((filename != lastopenfile) || (lastopenfs == null))
{
if (lastopenfs != null)
{
lastopenfs.Dispose();
lastopenfs = null;
}
if (File.Exists(filename))
{
lastopenfile = filename;
lastopenfs = File.OpenRead(filename);
}
}
if (lastopenfs != null)
{
// 当 english term 长度为 1 时, 我们可以做加速的动作
// 通常第一行就是该英文.
if (english.Length == 1)
{
lastopenfs.Position = 0;
StreamReader sr = new StreamReader(lastopenfs, encode);
try
{
dictdata firstitem = new dictdata(sr.ReadLine(), 0);
if (firstitem.eng == english.ToLower())
{
// 是的, 找到第一行就是我们要的
return ReadData(lastopenfs, 0, encode, maxcount);
}
}
finally
{
sr.DiscardBufferedData();
}
}
return SeekData(lastopenfs, english.ToLower(), encode, maxcount);
}
else
return new List();
}
我们当然可以做中翻英的功能, 很简单, 很暴力:
public List ChineseToEnglish(string chinese)
{
var result = new List();
List libfiles = new List(Directory.GetFiles(libpath, "*.lib"));
libfiles.Sort();
foreach (string libfile in libfiles)
{
using (FileStream fs = File.OpenRead(libfile))
using (StreamReader sr = new StreamReader(fs, encode))
{
string linedata;
while ((linedata = sr.ReadLine()) != null)
{
var dictitem = new dictdata(linedata, 0);
if (dictitem.chinese.IndexOf(chinese) >= 0)
{
// found.
result.Add(dictitem);
}
}
}
}
return result;
}
如果, 内存够大 (整个字典档统统载入内存大约会耗费 10MB),
就直接在内存搜寻, 那么整个程序会简单的多…
所以, 我们可以设计一个共通的界面, 让外部使用的人可以轻松切换内存搜寻,
或是文件搜寻.
///
/// 字典界面
///
public interface IDict : IDisposable
{
List EnglishToChinese(string english, int maxcount);
List ChineseToEnglish(string chinese);
}
于是, 文件搜寻的程序会像这样:
///
/// 使用 pydict 的字典档, 不载入内存, 直接在文件中搜寻
///
public class dict : IDict
{
///
/// dict lib path
///
private string libpath;
///
/// 上次打开的文件名称, 加速用
///
private string lastopenfile;
///
/// 上次打开的 FileStream, 加速用
///
private FileStream lastopenfs;
///
/// 编码
///
private static Encoding encode = Encoding.GetEncoding("Big5");
public dict()
{
libpath =
Path.Combine(
System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase),
"lib");
}
// 略...内容就是上面提到的...
#region IDisposable 成员
public void Dispose()
{
if (lastopenfs != null)
{
lastopenfs.Dispose();
lastopenfile = null;
lastopenfs = null;
}
}
#endregion
}
而整个在内存搜寻的字典程序 (是不是比直接在文件上面搜寻简单多了!):
///
/// 将 pydict 的字典档载入内存, 直接在内存中搜寻
///
public class memdict : IDict
{
///
/// dict lib path
///
private string libpath;
///
/// 全部的字典档内容
///
private List allsorteddict;
private static Encoding encode = Encoding.GetEncoding("Big5");
public memdict()
{
libpath =
Path.Combine(
System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase),
"lib");
List libfiles = new List(Directory.GetFiles(libpath, "*.lib"));
libfiles.Sort();
allsorteddict = new List();
foreach (string libfile in libfiles)
{
using (FileStream fs = File.OpenRead(libfile))
using (StreamReader sr = new StreamReader(fs, encode))
{
string linedata;
while ((linedata = sr.ReadLine()) != null)
{
allsorteddict.Add(new dictdata(linedata, 0));
}
}
}
}
///
/// 查询英文, 回传最多 maxcount 个字典数据
///
///
///
///
public List EnglishToChinese(string english, int maxcount)
{
if (english.Length == 0)
return new List();
int idx = allsorteddict.BinarySearch(new dictdata(english.ToLower() + "=", 0),
new ComparisonComparer((x,y) => x.eng.CompareTo(y.eng)));
bool isfound = (idx >= 0);
if (isfound == false)
idx = ~idx;
var result = new List();
for (int i = idx; (i < allsorteddict.Count) && (i < (idx + maxcount)); i++)
{
result.Add(allsorteddict[i]);
}
return result;
}
public List ChineseToEnglish(string chinese)
{
var result = new List();
foreach (var dictitem in allsorteddict)
{
if (dictitem.chinese.IndexOf(chinese) >= 0)
{
// found.
result.Add(dictitem);
}
}
return result;
}
#region IDisposable 成员
public void Dispose()
{
}
#endregion
}
欧, 还要搭配一个小工具把 Comparesion delegate 转为实做 IComparer 的对象:
///
/// 将 Comparesion delegate 转为一个实做 Comparer 的对象
///
///
public sealed class ComparisonComparer : IComparer
{
private readonly Comparison comparison;
public ComparisonComparer(Comparison comparison)
{
this.comparison = comparison;
}
public int Compare(T x, T y)
{
return comparison(x, y);
}
}
是的, 按照惯例, 功能部分的程序写完了,
就来拖拉 UI 啦
你可以看到很简单的几个设计, 在上面的 TextBox 输入文字,
如果是英翻中, 因为 BinarySearch 很快 (不论文件或是内存搜寻皆然),
所以我们可以作即时搜寻, 这点就是网络不容易作到的事情.
而如果切换为中翻英, 就要用暴力法查询, 会需要等待, 所以不能作即时搜寻,
要靠右边的搜寻按键.
中翻英的搜寻功能就做在上方 TextBox 的 TextChanged 触发函数:
private void textBox1_TextChanged(object sender, EventArgs e)
{
// 中翻英没办法做到即时查询
if (IsEnglishToChinese == false)
return;
int maxcount = 20;
var dicitems = dic.EnglishToChinese(textBox1.Text, maxcount);
updatelist(dicitems,
(dicitems.Count > 0) ?
(String.Compare(dicitems[0].eng, textBox1.Text, true) == 0) : false);
}
private void updatelist(List dictdatas, bool shoulddisplayfirst)
{
listBox1.BeginUpdate();
try
{
listBox1.Items.Clear();
foreach (var ditem in dictdatas)
listBox1.Items.Add(ditem);
if ((dictdatas.Count > 0) && (shoulddisplayfirst == true))
{
listBox1.SelectedIndex = 0;
textBox2.Text = dictdatas[0].displaysoundmarkandchinese;
}
else
textBox2.Text = string.Empty;
}
finally
{
listBox1.EndUpdate();
}
}
然后, 我们可以在使用者点选左边候选英文列表时, 在右边显示中文内容:
private void listBox1_SelectedIndexChanged(object sender, EventArgs e)
{
dictdata dict = listBox1.SelectedItem as dictdata;
if (dict != null)
textBox2.Text = dict.displaysoundmarkandchinese;
else
textBox2.Text = string.Empty;
}
中文查询的功能就做在 Search Button 按下的时候, UI 也需要显示等待的状况:
private void button1_Click(object sender, EventArgs e)
{
// 英翻中已经做到即时查询, 不需要再查一次
if (IsEnglishToChinese == true)
return;
Cursor.Current = Cursors.WaitCursor;
try
{
var result = dic.ChineseToEnglish(textBox1.Text);
updatelist(result, true);
}
finally
{
Cursor.Current = Cursors.Default;
}
}
最后, 切换中翻英, 英翻中的程序:
private void menuItem4_Click(object sender, EventArgs e)
{
updateEnglishToChineseStatus(false);
}
private void updateEnglishToChineseStatus(bool isengtochinese)
{
IsEnglishToChinese = isengtochinese;
menuItem4.Checked = !IsEnglishToChinese;
menuItem5.Checked = IsEnglishToChinese;
this.Text = IsEnglishToChinese ? "英翻中" : "中翻英";
}
private void menuItem5_Click(object sender, EventArgs e)
{
updateEnglishToChineseStatus(true);
}
因为我们设计了统一继承的界面, 所以要切换内存搜寻就很简单啰:
private void menuItem3_Click(object sender, EventArgs e)
{
if (dic is memdict)
return; // 已经载入内存了
dic.Dispose();
Cursor.Current = Cursors.WaitCursor;
try
{
dic = new memdict();
}
finally
{
Cursor.Current = Cursors.Default;
}
}
使用的范例画面如下:
…
是的, 打算朝范例迈进啊~~~
原始文件若包含了所有字典档会传不上来..
我仅仅保留 a 的字典档, 其他得有兴趣的人自己补上就好 : wm6dict.zip
原文:大专栏 [Windows Mobile .NET CF] 中英字典 – Day9