20140615

俄罗斯方块:win32api开发

俄罗斯方块:win32api开发

本文简述一门课程,演示win32api开发俄罗斯方块的开发过程。假设学生学习过C
语言,没学过或者学习C++不好,刚刚开始学习 win32api程序设计,还不懂消息循
环和注册窗口类。

1. 背景和原则

我这学期讲一门课,本科三年级,学生满员17人。一般接近满员,最低一次5人,
那天据林同学说,其他的同学都去看足球赛了。

课程名字叫做算法与程序设计实践3。第一堂课我照例要解释:到了"3"这个阶段,
就不讲算法了,只有实践。不过,后来看看算法也还是有一点应 用,比如从一个
线性表里删除符合条件的元素们,在线性表里查找符合条件的元素,这种难度的。

课是在机房上的,大部分时间学生和教师都看着显示,所以一学期下来,好多同学
和我见面可能都不太认识。不过我们对代码的形成过程更熟悉一些。

我试图贯彻下述原则:学生应该看到教师编程的过程,而不仅仅是结果;学生应该
看到在编辑器和编译器中的代码,而不是WORD或PPT里的;学 生应该先学会临模教
师的编程过程,而没有能力直接临模结果;学生甚至应该看到教师的错误及错误的
解决过程、教师的无知及检索过程,学生不应该 看到事先排练的完美的编程过程
和全知全能的教师,那样的过程和专家,学生模仿时无从下手。

所以,我课前不准备,在课堂上无意犯各种错误--偶尔演示学生们容易犯的错误--
及解决。在LOG文件中记录我们的计划和当前的进度,在画图 里画下原型。

所以,我假装对某些API和函数不熟悉,演示在MSDN和互联网中查找手册和解决方
案的步骤。单独做一些技术原型验证对API的调用结果的猜 想,而不是在工程的过
程中在项目代码中测试技术。有时,我知道问题在哪里,但是要先列出各种可能,
然后一一验证猜想(而不是直接解决,这似乎 是计算机本科生非常容易犯的错误,
如果解决了就认定那是问题的原因)。除了这两点,其余的时间我应该尽可能诚实。

有时候,学生会告诉我哪里错了,先于我发现问题的原因。这令我享受这样的教学
过程。

最终,我们--以我编码为主--实现了WIN32API开发的俄罗斯方块。

选择俄罗斯方块的原因,是因为小游戏的业务逻辑足够复杂,保证学生了解在相对
复杂的业务逻辑时的面临的问题和编程行为与toy作品不同;所使 用的到技术较
少,避免过多的机制 (数据库、网络等)分散学生的注意力,保证学生把精力集中
在对业务逻辑上。

选择win32api是课堂上投票的结果。选择C语言而没有使用C++有两个原因。一是学
生的C++掌握通常并不熟练;二是我希望学生能在项 目中发现面向对象的必要性和
优点,而不是仅因为学习过哪些语言而在工程中选用;三是希望演示用C也可以实
现基于对象的程序设计 (不是面向对象,不包括继承,仅包括方法与数据的内聚)。

2. 技术原型

涉及到的技术原型,要在工程开始前建立小项目,以验证对这些技术的掌握和对效
果的猜想。

要实验的技术列表,来源于需求。我们先不写代码,口头描述需求,然后分解需求
到所需的技术。这样就形成了技术列表。这个过程中,同时也形成了 定义,包括
名词和动词表。

这些技术原型也限定了除C语言以外需要掌握的技术,在这次开发当中。

技术原型包括:

* 使用GDI画图、擦除。用于画小块和移动小块。移动是根据视觉暂留在新的位置
上画图,并把旧位置上的小块以底色重画。

* 键盘消息响应。用于在不暂停小块下落的情况下接受玩家通过按键操纵小块左
移、右移、旋转、快速下落。

* 特定范围的随机数生成。用于在创建新的小块时,决定是哪个类型。类型计有S
型、L型、凸形、田形,及它们的旋转。

* 计时器 (timer),用于驱动小块定时下落,判断是否该清除一行,计分,刷新工
作区 (重画) 等。

* 在工作区输出文字。用于调试和显示分数。

最终形成的原型部分代码量如下。代码在附件中的 prototype目录下

画图 (及消息循环) ,draw,226行
擦除,eraser,263行
在工作区输出文字,textout,201行
按键消息响应,key,207行
随机数,random, 31行
计时器,timer,214行

3. 开发过程的里程碑

技术原型确定以后,再重新回到需求,并把需求排期。争取每次课程限定完成一个
功能。

需求排期遵循的原则是:优先完成对其他功能无信赖的部分;优先完成核心功能。

以下是开发过程中的里程碑。

1) 生成块。
2) 计时器驱动,块自动下降
3) 键盘控制块 旋转、快速下降、左移、右移
4) 落到底或粘在底部已存在块上 (if (conficted || touch_bottom) stick)
5) 删除一行:删除一行,把之上的行下降一行
6) 计分:消除一行和多行分值不同

以下功能在本学期没有实现。

7) 生成新块前,在预览区显示下一个块
8) 分数积累到一定程度 (?),加快块下落的速度

开发过程以git版本控制方式记录了历史,每个重要功能一次commit,以日期作为
message。

4. 定义

我们在开发前用示意图约定了一些定义,作为词汇表。排版原因,我在这里有文字
解释一下。

俄罗斯方块元素:工作区上绘图的最小单位,是一个小方格。俄罗斯方块的名字
Terris 即四元素,因为每个当前块由4个元素组成。

数组元素:即C语言中的数组元素,数组中的某一个。提出这个定义是为了区别于
俄罗斯方块的元素。

当前块 (current block) :正在移动的由四个元素构成的块。有S型、L型、田字
型等类型。

已存在的块 (exist block) :堆积在工作区底部的,已经粘成一团的元素。

像素坐标,世界坐标。像素坐标是由GDI绘图定义的,世界坐标由我们定义,以元
素为单位,左上是原点 (0,0) ,向右向下递增。

stick。当前块接触到已存在的块,或者当前块接触到工作区底部,此时应该把当
前块加入到已存在的块中,然后生成新的当前块;如果导致已存 在的块中某一行
充满元素,需要按游戏规则删除此行,然后把已存在的块中此行以上的元素降落一行。

5. 数据结构及流程

以下介绍当前块、已存在块、键盘操作、删除已存在块中的一行的数据结构和流程。

5.1 当前块

当前块中,包括当前块的以下数据:当前坐标,上一次的坐标 (用以擦除) ,当前
类型 (接下来会解释),上一次的类型 (用于旋转)。结构体如下,整个程序中只有
这个结构体的唯一实例。

struct struct_block{
int x;
int y; /* row 0, col 0 */
int old_x;
int old_y;
int* type;
int* old_type;
};

当前块的类型使用数组实现,如下,分别是一字型、田字型、凸字型。

int line_v_block[]={0, 0, 0, 1, 0, 2, 0, 3};
int line_h_block[]={0,0,1,0,2,0,3,0};
int tian_block[]={0, 0, 0, 1, 1, 0, 1, 1};
int tu_v_block[]={0,1,1,0,1,1,2,1};
int tu_h_block[]={0,1,1,0,1,1,1,2};

数组中的每两个数值 (数据中的元素)代表一个当前块中的元素的坐标,计8个数值
代表4个元素。

生成块时,

current_block.type = line_v_block;

指定了当前块的元素。

绘图时,遍历"类型数组",把每个元素绘出。无论何种类型,都遵循这一流程,从
而实现"以数据作为代码":类型数组即数据,遍历"类型数 组"、在旋转时改变类
型等即为引擎。

旋转的代码示例,改变类型 (的指针) :

if(current_block.type == line_v_block)
{
current_block.type = line_h_block;
}

平移的代码示例,改变横坐标:

current_block.x -= 1;

自动下降的代码示例,改变上一次的纵坐标和当前纵坐标。

if(! is_conflicted() && ! is_touch_bottom())
{
current_block.old_y = current_block.y;
current_block.y = current_block.y + 1;
}
else
{
stick();
generate_block();
}

快速下降:
纵坐标 增加 所有元素中到达底部 (或已存在块中同一横坐标的顶) 的最短距离。

貌似题外话,helper函数:is_conflicted(),判断当前块是否接触到已存在
块;is_touch_bottom(),判断 当前块是否触底;匹配横坐标,给出当前块的底坐
标;求当前块距离底部的最短距离。等等。

开发helper函数的目的,是为了使程序整体流程清晰。保障整体清晰的方法之一,
是要求每个函数内容不得超过一屏。如果超过了,就需要折解 出 helper 函数。
在主流程中调用 helper 函数,而把helper函数体移出主流程,这样主流程代码长
度就下降了。这和小学写作文的时候,老师要求先拉大纲是一个道理。经常有同学
说,在开发过程中 会发现新的功能,在开发遇到新的技术,没有做原型的,因此
难以把握大纲。这都说明把握大纲和做计划的能力还差,需要通过练习来训练。这
和小学 生写着写着作文发现需要查字典,或者写跑题了,是一个道理。我们的成
长并非认识的字多了,而是能预见到将会用到哪些字 (甚至表达手法、写作素材)。

此外,在面向对象中,有些的函数会成为game (或者 current block 或者 exist
block )的成员函数。这在开发中会认识到,如果它们与数据能内聚在一个类中,
该是多么方便,因此了解面向对象的在信息隐藏方面的优势。这些函数应归属于哪
个类, 是由哪个类承担这个责任决定的。

5.2 已存在块

已存在块中包括以下数据结构:块的长度 (事实上,是块的长度*2,代码中以横坐
标和纵坐标作为两个数组元素) ,已存在块数组。如下。

int exist_block_size=0;
int exist_block[(maxx+1)*(maxy+1)];

这种数据结构,及当前块的数据结构,把横纵坐标无差别地,不以结构体地方式放
在数组中,在后续开发中带来了麻烦。不过由于课程时间有限,后 来,我未对此
做出修改。应该逐渐演化程序结构,形成以元素作为结构体的数组。再开发出一些
helper甚至成员函数,遍历时以俄罗斯方块元素 为单位,而不是当前代码中的以
数组元素为单位。

对已存在块数据结构操作的函数之一是 stick,用于在当前块触底 (或触及已存在
块)时,把当前块中的元素移到已存在块中。

有不少helper函数,基本都是通过遍历 exist_block,按匹配条件读其中的坐标。
包括:匹配横坐标,给出已存在块的顶坐标 int get_exist_block_top(int x)。


5.3 键盘操作 & 动作序列

玩家操作块这一操作,由键盘消息响应开始。我们不在键盘响应中处理这一事件,
而是只在这里记住这个动作,加入动作序列中。这是后来的版本。最 初的版本,
我们也不在键盘响应中处理事件,而是调用 block.cpp 中的函数。原则是:凡依
赖win32api的,放在 tetris.cpp 中,如 timer, 键盘响应,绘图;凡是与业务逻
辑有关,平台无关的,放在 block.cpp 中。接收向上箭头,是键盘响应,平台相
关,所以放在 tetris.cpp 中;此时调用的 rotate,用于改变当前块的类型或坐
标,平台无关,所以放在 block.cpp 中。

动作序列的数据结构如下。在动作序列数组buffer_action_seq中,数组动作元素
(动作) 的类型是 枚举 action。

enum action{ action_left=1, action_right=2, action_speed_down=3,
action_rotate=4, action_down_auto=5, action_na=0};
action buffer_action_seq[action_size]={action_na};
int buffer_action_cursor = 0;

由玩家触发键盘消息开始,流程如下。

1)键盘消息响应:

buffer_action_seq[buffer_action_cursor++] = action_rotate;在动作序列中加
入一个动作。这对应于设计模式中的 commander 模式要解决的问题。

2)在timer中自动下降

timer中 buffer_action_seq[buffer_action_cursor++] = action_down_auto; 在
动作序列中加入一个动作。

3)在timer中触发WM_PAINT

timer 中 InvalidateRect 触发 WM_PAINT

4)WM_PAINT中执行动作序列

erase_old_block_seq(hdc);

erase_old_block_seq (hdc) 遍历动作序列,按每个动作改变当前块坐标,然后擦
除由于动作产生的旧块。遍历动作序列以后,就完成了自上个 timer 周期以来所
有的动作,擦除了这期间产生的所有旧块。

void erase_old_block_seq(HDC hdc) 片断如下:
for (i = 0; i < buffer_action_cursor; i++)
{
switch (buffer_action_seq[i])
{
case action_left:
move_left();
erase_old_block(hdc);
break;


在序列里的每个动作中,move_left 改坐标, erase_old_block(hdc) 擦除旧块.

5)WM_PAINT画新的当前块和已存在块

draw_current_block(hdc);
draw_exist_block(hdc);

因为重绘比计算花费的时间要多,作为性能优化,如果当前块与旧块坐标完全相
同,不重画。

另,另一个版本的动作序列,不使用枚举和swtich-case,通过把函数作为消息传
递给责任者,实现disptach:

void (*next_action)() = move_still;
next_action = move_left

其中 move_left是一个函数。next_action这样的元素 (类型是函数) 组成一个数
组,作为动作序列。执行动作序列时,用下面这样的代码:

while ( next_action++ != action_termination )
next_action;

由于 next_action 既是函数,也是数组元素的指针,因此上述代码不是伪代码,
而是可以执行的。这类似于 jump table 技术,数组元素的类型函数,可以遍历数
组,执行元素对应的函数。

5.4 删除一行 & 计分数

每个 timer 中,都调用 void kill_all_full_lines()。它遍历 exist block,凡
符合满行条件的,调用 kill_block_in_line 删除该行,调用
move_exist_block_down_line 把该行以上的 exist_block 下降一行。

这三个 helper 函数都是通过遍历 exist block 中的每个元素,匹配坐标条件,
然后删除数组元素或者改变数组元素的值。如前所述,由于 exist block 封装中
未使用 俄罗斯方块元素,所以这些遍历都写得非常丑陋。

删除一行以后,累积删除的行数。全删以后,根据删除的行数进行 switch-case,
向全局变量 score 累加分数。在下个timer中,把 score 用 textout 输出到工作区。

6. 回顾和检讨

6.1 数据结构,封装,循环条件

由于最初的 (也是最终的)数据结构设计偷了懒,后来又没有足够的时间修改,此
前已经提及两次,exist block的结构过于贴近平台,而远离需求。exist block的
颗粒度太低,是以 int 为类型的 数组元素,对应于需求中的 俄罗斯方块元素 中
的横纵坐标之一。某个数组元素到底是横坐标还是纵坐标,到底是第几个俄罗斯方
块元素,这些都需要由代码实现。这样,按需求写helper函数的时候,遍 历的元
素选取、终止条件,都遇到了麻烦。我在课堂上写作时需要考虑,有时还会错。经
验说明,当我需要仔细考虑,或者讲述时间较长时,学生听懂 可能已经有相当难
度了。终止条件错误的bug,在代码中存在两三处,导致在 exist block够多时,
即游戏进行一段时间,工作区中会出现莫名其妙的俄罗斯方块元素。这个bug在最
后阶段才解决。

这个故事告诉我们,设计不好,对编码实现的难度要求就会提高。战略失误,战役
和战斗就不容易打。领导决策肤浅,要求下属跑死,结果也是白扯。 道理都是一
样的。

6.2 不要对付过去

在开发中间的某堂课,我们发现当前块移动时后面留了尾迹,擦得不干净。这些那
堂课快结束了。为了能让学生在课后重复我课堂上的工作,所以我" 对付"了代
码,由局部刷新改为刷新整个工作区,包括背景。这样尾迹表面上清除掉了。

之后,延续了这段"对付"的代码。直到期末将至,我才发现这段"对付"掩盖了另一
些bug,坐标移动的bug导致除非刷新整个工作区就有尾 迹。这个bug在最后阶段才
解决。

6.3 并行,timer

有文章指出,初学者非常不容易理解的程序概念包括:赋值、递归和迭代、并行。
本程序中有几个埋得比较深的bug,是由于我对并行没有足够警惕 造成的。

timer, 键盘响应,WM_PAINT会并行发生。当其中一个未处理完的时候,另一个可
能就开始执行;甚至timer未处理完的时候,另一个timer也可能会开 始。而这些
并行的代码,都调用了 block.cpp。比如有时导致其中一个正改坐标尚未完成,另
一个开始刷新工作区,这样工作区里就出现个元素,位置是乱七八糟的。

并行的处理,需要 原子操作、进程间通信、避免重入 等概念。上述提到的动作序
列,目的之一就是希望擦除旧的当前块这一动作只在 timer 中发生。

在本课程中,应该不期待学生具备这些操作系统中的知识。不过我还没有想到该如
何设计才能规避这些知识。不过我猜应该类似于不用线程也能设计出 贪吃蛇,应
该有依赖更浅显知识的设计手段,比如单纯轮询,而不用事件响应、消息循环。有
哪位知道,请赐教,谢谢。

6.4 猜想后,应该先验证,然后再修改

学生们通常把验证猜想和实施解决归约成了一步,我也经常如此。下文中的他们,
包括我。

他们观察到问题,然后做出猜想。这是正常步骤。

但是他们不以实验验证猜想是正确的,急急按猜想修改代码。如果问题消失了,
好,他们假设抓住了问题的原因;如果问题还在,就再做个猜想,然后 又马上修
改。甚至更糟糕,没有退回到上一步的起点,就在当前工作代码上"继续"修改,让
各个猜想累加起来,最终问题解决的时候甚至不知道是什 么原因。

应该先设计实验,按猜想的模型,如果怎样就会怎样。验证猜想以后,再去解决。
比如假设由于 timer 和 keyboard事件响应 同步导致画图混乱,那么,不应该着
争写进程通信,而是 应该先选用简单粗暴的手段 去除同步,以更大的颗粒度作为
原子操作,验证猜想。如果猜想正确,现象应该有所改变。虽然影响性能和效果,
但这并不是准备最终采用的代码,只是用来验证猜 想的。当猜想验证以后,再去
想效果更好的方案真正解决,比如建立个变量作为信号灯。

6.5 不要轻易更换技术方案,试图绕过问题

这个方面,我最初是发现计算机本科的同学倾向强烈。经常有方案,明明再向前一
步就能解决,他们却在此时换了方案。问为什么。答:因为这个技术 解决不了这
个问题。

确定"不"是极其困难的,甚至比确定"能"要难上很多。你不能,并非就能确定这个
方案不能。

需要充分了解你所使用的技术,对它能够完成的任务有足够和明确的自信。同时,
对用来替换的方案能解决何种问题,也应该明确。做原型验证,根据 理论推论,
这些都是解决之道。见到工具,拿来就用,偏听偏信别人的评论,就太草率了;一
旦发现并非万能良药,转身就去寻找就的手段,这就更草 率了。

6.6 版本控制

为了让学生能看到开发的过程,我上课时用文件系统做了版本控制,每次课一个目
录,有时压缩成zip。课程结束以后,一个版本一个版本加入 git,然后commit,
操作了两个小时(?),其间又担心整错了,苦不堪言。

下次一定要从最开始就做版本控制。还要在 commit 前把 debug, pch, sdf 等二
进制垃圾手动删除。

7. 附件

附件是以git版本控制的代码及日志,在这里[http://download.csdn.net/detail
/younggift /7499881]。

protype下是技术原型。

tetris下的是俄罗斯方块项目本身。早先的版本是VS2010的,最后一天的是VS2012
的。你可以仅代码部分添加进win32工程, 以适应你的VS版本,或者dev c++版本。

log0.txt是课堂上的日志。log1.txt是最后一天前期的日志。log2.doc是最后一天
后期的日志,因为需要截图,所以改成用 word。

pic.bmp是图片,用来说明定义的。

branch是一个分支,我忘了它是否加入了 trunk,留在那里备用,以防遗漏。

--------------------

博客会手工同步到以下地址:

[http://giftdotyoung.blogspot.com]

[http://blog.csdn.net/younggift]

20140601

电子书阅读及工具

电子书阅读及工具

我现在看电子书,除了计算机以外,还常使用 ipad mini 和 kindle。所讨论的工
具都在这三个平台上。

后半部分讲的内容才是工具,前半部分是历史回顾。只对工具感兴趣的,请向下翻页。

1.

我最初读的电子读物,应该是 qbasic 的手册。当时还不知道有电子读物或者手册
这回事,常问张仕鹏师兄问题,只要见他在机房,就死皮赖脸地去问。有一次问到
红色的颜色编号到底是多少,他终于受 不了,告诉我按F1键,说:你自己看手册
吧。然后,我就开始漫长的手册阅读生涯,也因此有幸学会 RTFM 比身边常有一个
高手要重要得多。感谢张师兄教会我这一点。

此后很多年,也有不少同学问过我类似的问题,比如向上箭头的扫描码是什么,某
个函数的参数是什么类型,线程的使用。我都会想起张师兄,然后说:你 自己看
手册吧。他们有的回答,那太长了,有的回答,是英文的啊。我有时苦口婆心讲一
遍手册里的内容,也有时苦口婆心地讲"未来的世界就是这样了, 你越来越没有汉
语可看了",更多的时候我就只是闭嘴。人家需要的是具体的帮助,不是人生导师。

qbasic的手册当然只能是英文的。我讲的是另一个时代的故事,跟现在有所不同,
那个时候还不兴管师兄叫做师兄或者学长,我就只是直接叫张仕鹏 的名字,他比
我高两届。那个时候也没有互联网,没有在线词典,我也没钱买文曲星,只有后来
才买得起的一本40块钱的英汉大词典,太厚,不能每次搬 着去机房。而
且,qbasic的手册是不拆不扣的英文的。如同现在的手册,它也会援引其他的章
节,那么就跳过去,看完再跳回来。

我和我的同学,就是这样学习了qbasic,还有 turbo C的使用。看纸质书,图书馆
可以借到;看手册,随机带着。系里确实也开了C语言课,不过那是我们自己学会
以后很久的事了。

我本科毕业前读的最有份量的电子书是 windows资源大全,也是英文的。它的纸质
品后来我看到过,一寸多厚。我根据其中的一章学会了 借助 novell netware操作
系统做win95无盘工作站。毕业论文的一部分跟它有关。答辩委员会里有幸遇到了
后来的校长。他问:你这工作里有什么创新啊。我说:啥 也没有,就依据了一本
书--然后用手比划了一下厚度。就这样通过了答辩。

那本书的电子文档其实非常小,似乎一张或两张1.44M的软盘就可以拷下。似乎是
从win95光盘中解压出来的,是chm更可能是hlp格式的。

后来读了很多格式,txt的,word的,pdf的,不一而足。从最初到现在,都有人跟
我提到过电子书的诸多缺点,没有油墨的清香,没有情调不能 煮茶而读,记笔记
不方便,不习惯,版权等等。

我依然保持使用电子读物的习惯,甚至大批购买纸质品的同时,也仍然大量阅读电
子书,因为反对电子书的理由不足以说服我。油墨的清香、情调、版权这 些事,
适合矫情 (我长期以为这两个字是嚼性...)的人群。对于穷人来说,逮到什么版本
就是什么版本,有的读就不错,还要什么自行车啊。"不习惯"这件事,习惯习惯就
好 了。我们翻纸质品书页的习惯,也不过是训练出来的,这里既没有感情,也没
有文化可言。

记笔记确实是个问题。不过,我长期不把笔记写在书页上,因为书是图书馆的。涂
抹要罚款。所以,只要再准备一张纸,或者笔记本,把笔记都写在上面就 好。所
以,电子书也可以一样处理,笔记写在他处。

2. 工具

目前在所有的系统和格式下,我阅读时的要求都包括以下几个方面:书籍管理、格
式转换、上传下载、切白边、做笔记、阅读本身。

2.1 书籍管理

最佳的书籍管理工具是 目录系统 (文件系统) 本身,可以使用windows里的资源管
理器 建立和删除目录,通过文件名标注书的内容。加过批注的,我会加上*.young
批注.pdf 这样的字样。找书的时候,在子目录下搜索文件名。

另一个我常用的书籍管理工具是 calibre [http://www.calibre-ebook.com/]。
不同的操作系统版本,我用过 linux 和 windows 下的,都不错。可以标注书目的
书名、作者、出版社等信息。还可以加标签,不同于目录,这样每本书可以属于几
个类别。不过我的经验是,标注的工作确实总觉得 需要,但是有了这个功能以
后,基本不用。我们读书的数量,可能还远远没有达到以后找不到的程度。

我还曾经用过论文管理工具,endnote什么的。能导出作者、篇名、期刊、年月
等,写论文的时候确实方便。我问过导师,你咋不用endnote 呢。他用文件系统管
理,写每篇论文的时候把参考文献也放在论文旁边的目录里。他说:一共就引那么
二三十篇,不值得用工具;各种工具都会过时 (或者有适用条件?),但是文件系
统总是有效。

2.2 格式转换

kindle适合看小说,非PDF扫描的,ipad mini适合看漫画和有图的文档,还有扫描
版的电子书。台式机和笔记本适合看各种书,只是不能窝在椅子里,因为那样太远
就看不清显示器了。

它们能阅读的格式各有不同,效果也有好坏。所以我需要根据环境和需要,把电子
书由一种格式转换为另一种。

最常用的是 calibre。它能把word,pdf,txt,html转成 kindle 的 mob 格式,还能
转为适合 ipad 的 pdf 或 epub 格式。格式转换以后,可以用 calibre 架设www
服务器,用 kindle/ipad 从内网连接下载电子书。

另一种常用的转换工具是 amazon 本身提供的邮箱,注册的时候会提供给你。从认
证过的邮箱向它发邮件,标题是 convert ,把word/pdf/txt电子书作为附件。然
后过一会儿,可以用kindle从网上同步到这本书,已经转换为 kindle 的 azw 格式。

txt有时会遇到问题。一个是编码,calibre识别gb2312或utf-8有问题,可能会转
出乱码来。我也不知道怎么解决。这种时候,只好 用 amazon 的邮箱来转换。另
一个是自动换行,有时会转出大段大段没换行的段落来。使用阅读器apabi看,自
动换行还没出过问题。

2.3 上传下载

电子书到手,可能会需要从一个设备转到另一个设备,这就是我说的上传下载。

如果是小文件,在 PC机 和 ipad 之间,可以使用百度云。百度云支持在ipad上导
出并用别的阅读工具打开电子书。批注以后,如果阅读工具支持导出,也可以导出
到百度云,再上传。

准备在kindle上阅读的小文件,可以使用 amazon 的邮件,由PC机发送到邮箱,再
由kindle从网上同步下来。

如果是大的文件,100M以下,可以用 calibre 在PC机架设 www 服务器,用
kindle 或 ipad 的浏览器下载。只走内网,速度很快。

ipad 上的 goodreader 也可以架设 www服务器,用PC的浏览器从 ipad 下载 到
PC,我用这个功能保存在页面上做笔记加了批注的PDF文件。goodreader也支持从
pc的浏览器上传文件到ipad。

如果是更大的文件,就得把 ipad/kindle 通过USB线接到计算机上了。

2.4 切白边

纸书有白边,而 ipad/kindle 的屏幕以外本来就存在边缘,再加上纸书扫描的白
边,字或画面就显得有点小。对我这样视力不好的人,格式不友好。所以需要切白
边。白边切掉了,字就大了不 少。

我常用的切白边工具是 briss [http://sourceforge.net/projects/briss/]。
个java程序。载入电子书的时候有点慢,它把所有的页面都读进 来,然后叠印显
示出来,就像很多层琉酸纸那样。这样,一目了然地能够看到哪些部分是不能切掉
的。briss会自动判断白边范围,作为建议值。我的 经验,它的判断相当准确。你
也可以手动设置,设置好边界以后,另存一个文件。另存出来的文件并不比原来的
文件大多少,而且保存这一动作一点也不费 时间,瞬间完成,因为据说 briss 并
没有"真正"切掉白边,而是设置了新的显示范围。

另一个切白边的工具是 goodreader,它在阅读文件的同时,可以设置显示范围。

以上两个工具,显示范围都可以设置为奇偶页不同。

2.5 做笔记

我读电子书做笔记,常用的方法包括:另找张纸记 (或另记一个文件) ,拷屏然后
用画图类的工具在上面记,在电子书的页上上画 (或写字,或圈范围)。

另记一个文件的,我用 mybase, org-mode,txt/word+文件系统。

在画面上批注并另存的,一般用 evernote 保存归类。

直接在画面上批注的,用 goodreader, acrobat reader。

2.6 阅读工具

我用过的阅读工具,觉得不错的如下。

kindle原生系统,没有使用多看。事实上,没试过多看,所以不知道好坏,用
kindle的时间没发现有必要从英文菜单改为中文的。

goodreader,ipad mini 上的。能做笔记,能上传下载,能读很多格式。

ibook,是 ipad mini 上的,苹果产的。如果不做笔记看小说,还不错。

acrobat reader,ipad mini上有,pc端也有。能做笔记。

apabi,是 ipad mini上的,读txt格式好。

calibre 不是阅读工具,因为功能强大,总结一下。能上传下载,能图书管理,能
格式转换。

2.7 没有解决的问题

还有些问题没有解决。比如竖版阅读还是很令人头疼,看一行就要翻一下。要是有
工具能识别出来转成横版就好了。

3.

既使有这些工具,书也还是得一行一行由你自己来读。就像即使这个世界摆在你的
面前,人生也还得你自己亲自来过。说到这里,我想起在QQ上请同学帮 忙做题的
同学,总是想说:以后你的工资,也由别人代领吗?或者想说:你喜欢的,从来就
不是读书本身,而是读书可能带来的利益。

--------------------

博客会手工同步到以下地址:

[http://giftdotyoung.blogspot.com]

[http://blog.csdn.net/younggift]

20140528

从ssh登录缓慢说起

我在单位开了台实验用机,用SSH远程登录。之前一直用得不错,这两天发现断了。联系了两位相关的同事帮忙,分别对虚拟机和网络测试,都正常。我晚上回家再测,由于白天有了对比,晚上等待就更耐心一些--不到30秒,登录成功了。登录以后,速度还可以。

这说明网络、虚拟机都没啥问题。我以前遇到过SSH登录缓慢而登录以后速度不错的情况,比如我的WINDWOS机器连接LINUX用sftp的时候。后来解决了,这次的解决方案也大致如下。

解决方法:

修改 /etc/ssh/sshd_config,这是ssh服务端的设置文件。

1. UseDNS no

2. 把

GSSAPIAuthentication yes

改为

GSSAPIAuthentication no

然后重启sshd,/etc/init.d/sshd restart

再重新连接,登录速度快了。

导致以上问题的原因的基本原理是,ssh客户端在连接ssh服务端的时候,服务端根据客户端的IP地址去反向解析这个IP地址的域名。如果反向解析的过程中需要连接的外部世界的服务器速度很慢,或者干脆是不可达的,那么ssh服务端会等到DNS反向解析这个动作超时,这会导致十几秒或长时间ssh客户端傻等在那。没耐心如我者,就会判定网断了或者服务器完蛋了。

ssh服务端的这些动作可以禁止掉,不过开发者认为这些动作是必要的,安全中不应缺少的组成部分,所以默认设置了。这让我想起些别的。

Thunderbird,似乎译作 雷鸟(?)是非常著名的电子邮件客户端。似乎跟当年的foxmail一样著名,或者更著名。foxmail的作者后来加盟了腾讯,开发出了个著名产品,叫做微信。我用过一段时间Thunderbird,觉得还不错。后来,后来....这个故事告诉我们,时代并非总是越变越好的。

后来,我用thunderbird收gmail的邮件。之所以要这么干,是因为连接gmail的速度令人难以忍受。令人难以忍受的意思并非慢得不行,因为它有时也飞快,瞬间完成载入。令人难以忍受的原因是,速度并不稳定,抽疯一样,有时飞快,有时极慢。我慢慢养成了一个不良习惯,每次载入慢的时候,就开始在心里或者出声地骂相关责任人。

我以为用thunderbird收邮件,批量都收下来以后再看,感觉会好些。没有想到的是,或者说thunderbird的开发者也没有想到的事情发生了。软件工程中有所谓"你永远也不知道用户会多么愚蠢地使用你开发的软件",thunderbird开发组也绝对不能想到地球上还有我们这样一群用户的境遇吧。当我点击某封邮件,邮件头显示的是这封邮件的标题,但是邮件的正文仍然是刚刚读过的一封,直到几分钟以后,邮件的正文才刷新出来。如果没注意,会以为A标题正对应着B正文。Thunderbird开发组可能绝不能想到,这世界上有个地方收gmail邮件的时候慢到需要进度条指示,如果进度条有百分比更好。

sshd的开发组可能也不能想到,有些地方,虽非物理隔离的内网,但是许多地方亦不可达。饱食者不能理解何不食肉糜,那些优秀的软件产品的设计者可能也不能理解我们吧。就像微软的WORD和VS咔咔升级,越整工具栏和浮动工具栏越多,最后能编程和写文字的地方只剩下了一小点儿。当然,他们是有道理的,因为他们的目标用户是用大~~~显示器的。我当年用15英寸CRT球面显示器坚持了N多年,所以才养成了全屏编辑的习惯。当然,微软们还有另一个道理,就是我们中大多数人没有付钱,因此并非他们的目标用户。这与另一个案例类似。当年光荣公司宣布三国某版本游戏不再支持简体中文,我们中国不少用户义愤填膺,在BBS里一顿骂,骂光荣之愚蠢,竟然放弃大陆这么大的市场。几楼以后,有个网友悠悠地说:你们买过光荣用户哪怕一个拷贝吧,好意思说自己是用户是市场?

但是 thunderbird 和 sshd 不同,它们都是免费的,gmail也是免费的,整个地球都可以尽情拥有,而我们似乎不在其中。有个片子你看过没,叫做《日本锁国》。

--------------------

博客会手工同步到以下地址: