代码改变世界

[翻译]在手机上阅读离线MSDN

  onm  阅读(306)  评论(0编辑  收藏  举报

由于MSDN推出了换肤功能,所以这个程序有些问题,需要将皮肤切换到classic,才可正常使用。原文地址链接失效,所有图片无法显示。

介绍

当我在火车上时,我需要用我的Windows Mobile 6阅读MSDN。我发现一个好的读者能读HTML文件,但是我没有HTML文件可以阅读。(I found a decent reader which can read HTML files, but I had no HTML file to read.)

这个工具用了WinForms和WebBrowser控件来显示MSDN标题树。你可以选择一个标题并且把所有它的子标题一起存储到一个单独的HTML文件中。这个工具从每个标题提取出重要的内容并且可以做一些选择性的重新排版。输出文件可以在手机上阅读。

背景

直接从网上阅读不是我的选择。我决定从MSDN上把内容下载下来。但是,它由许多小的页面呈现,并且手工的下载也不是我的选择。用网站下载工具也不是一个好主意,因为下载工具不懂得MSDN的主题结构,并且它会把整个页面下载,然而我仅感兴趣文章内容(没有页眉和页脚)。

我感兴趣于快速开发,所以我没有苦恼于开发多线程的程序和把应用分成很多虚拟的层。

使用工具

使用这个工具,你只需要运行它然后读取上面的说明,值得注意的是,试图把整个MSDN存储下来需要花费很长时间……

使用代码

首先,我们必须把MSDN的标题的树结构呈献给用户。我不知道任何的网络服务可以给出这样的信息,所以我决定放置一个webBrowserSelector网络浏览器控件,来打开MSDN主页,并且提取网页中的内容的表。然后用HTML源代码看MSDN页面,我注意到承载内容的表开始于id=tocVectorTreeId的元素。

1
void webBrowserSelector_DocumentCompleted(object sender,     WebBrowserDocumentCompletedEventArgs e){   ...   AddChildren(treeViewToc.Nodes,     webBrowserSelector.Document.GetElementById("tocVectorTreeId"));   ...}

AddChildren()方法添加孩子给树节点并且绑定到在浏览器中发现的标题。当用户点击扩展节点按钮时,我们必须在我们隐藏的网络浏览器中模拟点击来展开子标题并且把他们复制到treeview:

1
void treeViewToc_BeforeExpand(object sender, TreeViewCancelEventArgs e){    // topic nodes where children must be loaded from web    // indicated by single node with null in Tag    if (e.Node.Nodes.Count == 1 &&        e.Node.Nodes[0].Tag == null)    {        e.Node.TreeView.BeginUpdate();        e.Node.Nodes.Clear();        AddGrandchildren(e.Node, true);        e.Node.TreeView.EndUpdate();    }}

在MSDN的HTML中,每一个标题的节点有这样的结构:??如果IMG元素是class=’LibC_c’,那么这也许是一个有很多孩子的标题,但是他们还没有加载。为了加载他们,我们必须模拟用户在上面点击:

1
// all direct IMG children with class='LibC_c'// must be clicked in browser to expand themforeach (HtmlElement child in   from HtmlElement element in topicElement.Children   where      "IMG".Equals(element.TagName,      StringComparison.InvariantCultureIgnoreCase) &&      "LibC_c".Equals(element.GetAttribute("className"),      StringComparison.InvariantCultureIgnoreCase)   select element){   child.InvokeMember("click");}

当用户准备把主题存成一个文件的时候,我们必须:

  • 显示出一个文件存储对话框。
  • 自动的展开标题子节点(确保我们能得到完整的标题列表)。
  • 建立一个用来下载和合并的URL的列表。
  • 开始第一个URL的下载。
1
void saveTopicToolStripMenuItem_Click(object sender, EventArgs e){    ...    // select file name where to save    saveFileDialog1.FileName = node.Text;    if (saveFileDialog1.ShowDialog(this) != DialogResult.OK)        return;    // make sure all children is loaded    treeViewToc.BeginUpdate();    node.ExpandAll();    node.Collapse();    treeViewToc.EndUpdate();    // get all links from the root topic    TopicsUrlList.Clear();    foreach (HtmlElement item in topicElement.GetElementsByTagName("a"))    {        TopicsUrlList.Enqueue(item.GetAttribute("href"));    }    if (TopicsUrlList.Count > 0)    {        ...            StartNextTopicDownload();        ...    }   ...}

StartNextTopicDownload()用另一个隐藏的webBrowserLoader网络浏览器简单的导航到队列中下一个URL。一旦文档被下载,我们可以提取我们感兴趣的内容(有”mainSection”ID的地方)并且把它合并到内部的StringBuilder对象。我已经选择了把所有下载结果存到内存中,并且一旦所有下载完成清空缓存并合并成一个文件,但是当你从网络上获得它时,它很容易被存进一个文件。

有两个可以选择的特性可以关闭这个工具栏(There are two optional features, which can be turned off on the toolbar):移除超级链接,并且把图片存储到本地。移除超级链接有助于改善手机阅读体验(如果你轻击一个可执行超链接,读者将不能跟上它(if you tap on a processed hyperlink, the reader won't follow it)):

1
void RemoveHyperlinks(HtmlElement mainSection){    // remove all internet links    foreach (HtmlElement a in mainSection.GetElementsByTagName("a"))    {        a.SetAttribute("href", "");        a.OuterHtml = string.Format("<strong>{0}</strong>", a.OuterHtml);    }}

本地的图片选择性的存储用来在维持生成的HTML的图片。这个方式很像IE做的——存储图片在子文件夹并且更新子文件夹得指向。然而,有两个技巧可以保持图片数量和尺寸足够的小:

  • 同一URL的图片使用同样文件
  • URL图片不同,但是内容相同的用同样的文件

你可以在原文件中的方法CopyImagesToLocalFolder()找到逻辑。

兴趣点(Points of interest)

消失的异常

我发现一个很糟的情况,如果异常发生在网络浏览器控件事件处理内部,那么异常将被隐藏。所以,我用BeginInvoke()和匿名委托在事件处理之后运行我的代码。

用 .NET TreeView承载需求(Load on demand in .NET TreeView)

本地的Windows TreeView控件能指出一棵树节点是否有孩子。这样的节点有展开按钮即使他们没有孩子。这有助于在树中按需求加载。

不幸的是,.NET版本没有这样的特性,所有的孩子必须被创建来显示一个展开按钮。我用了一个小把戏来克服这个限制。当我知道一个父亲可能有孩子时,但是还没有孩子,我加了一个空的孩子节点。所有有效地孩子的Tag属性会被填上相关的HtmlElement,但是这些空孩子的Tag是空值null。我打断了BeforeExpand()事件,并且如果我检测到这个父亲需要加载孩子,我就移除空节点然后加载孩子。

从HtmlElement进入HTML类属性(attribute)

HtmlElement有一个GetAttribute()方法,但是如果你指定一个类的属性(attribute)它不会工作。很失望,然后读了一些MSDN,我发现className必须指定才能读取这个类属性(attribute)。所有其他的属性能被在他们自己的名字下读取。

有效地重用下载的图片

MSDN页面有许多图片。有时候这些图片看起来一样,但是它们有不同的URL来检索它们。我用了两个词典来有效地重新加载图片,两个词典都关联文件名称。如果我没有在词典里找到文件名,那么我就必须创建一个新文件并且把它的信息加到词典中。第一个词典用URL作为Key,第二个用下载的图片的MD5哈希码被转成Base64的字符串作为Key。

1
// sometimes same images may be located inside different files// using hash we can find twins and reuse themvar hash = Convert.ToBase64String(md5.ComputeHash(data));

历史

11月4日——文章创建。

许可

文章和附属源代码及文件,由The Code Project Open License (CPOL)授权。

关于作者

Sergey Galich Sergey在过去的15年里每一天都喜欢编程,最开始是Basic,ASM,Pascal,C++,X++和这几年的C#。用户交互设计,可用性,沟通性,同步和异步编程,游戏开发和3D图像——所有这些领域使他兴奋。

原文地址:http://www.codeproject.com/KB/mobile/MSDNDownloader.aspx
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· [AI/GPT/综述] AI Agent的设计模式综述
点击右上角即可分享
微信分享提示