分类

课内:
不限
类型:
不限 毕业设计 课程设计 小学期 大作业
汇编语言 C语言 C++ JAVA C# JSP PYTHON PHP
数据结构与算法 操作系统 编译原理 数据库 计算机网络 软件工程 VC++程序设计
游戏 PC程序 APP 网站 其他
评分:
不限 10 9 8 7 6 5 4 3 2 1
年份:
不限 2018 2019 2020 2021

资源列表

  • 基于C++的数据库可扩容哈希

    一、项目介绍主要涉及可扩展哈希在数据库中的应用。
    读入由 tpc-h 生成的 lineitem.tbl,以 L_ORDERKEY 属性作为键值将记录放入合适的哈希桶内。读入测试文件 testinput.in 内的数据,数据中包含多个需要查询的键值。将通过键值查询得到的所有记录都输出到 testoutput.out 文件中。算法实现分为两大部分,第一部分是建立索引,第二部分是查询。建立索引是将输入的每一条记录根据指定的键值放入合适的哈希桶内,当哈希桶已满时,需要进行分裂。查询是根据输入的键值返回具有相同键值的记录,返回的记录可能有不止一条。
    二、项目环境
    系统:Windows 8.1 专业版 64 位
    处理器:Intel® Core(TM) i3 CPU M 350 @ 2.27GHz 2.27 GHz
    内存:2 GB 金士顿 DDR3 1333MHZ
    硬盘:希捷 ST9320 320GB 7200 转/分
    语言:C++
    编辑器:Visual Studio 2013

    三、项目架构本项目共有7 个文件:

    main.cpp:主文件,程序的入口
    Manager.h (.cpp):主控程序,实现功能的主体
    Buffer.h (.cpp):缓存池,管理内存中各缓存页
    Index.h (.cpp):目录页,存储哈希值和桶号的对应关系
    Page.h (.cpp):页的基本类,实现一个页的基本功能
    Function.h (.cpp):项目中要用到的通用函数,如取哈希值,取键值等
    option.h:程序的如最大使用页数,哈希方式等参数的定义文件

    无论最大使用页数和哈希方式,项目运行时目录页占用 1 页,读写文件缓冲用占用一页,其他页都用来存储记录桶。
    四、主要数据结构4.1 基本页
    每个 Page 页在内存中的分布如下:

    其中蓝色背景的为 contents 的内容。记录的具体内容从 contents 的头向尾扩展,对应的记录信息如长度,偏移值等从 contents 的尾向头扩展。需要写入或写出时将 Tong 强制转换为 char*类型,按长度为 8192 进行存储或读取。
    4.2 缓冲池
    每个缓冲池有一个Page*的数组,用来存储各Page页的指针。pageId数组用来存储每个Page对应的pageId。ref_bit, pin_count数组分别存储各page的时钟标志和pin标记(用于时钟算法),dirty数组标志各page的修改状态,以确定换出时是否需要写回硬盘。
    4.3 目录页
    size 表示目录页的总大小,包括在硬盘的部分。globalDepth 表示全局深度,start 表示目录当前在内存部分的头下标,pageId 表示以 start + (数组下标)为哈希值的桶的 pageId。
    目录总的结构为:

    五、主要算法5.1 低位哈希算法5.1.1 哈希方法直接将 key 值截取低 globalDepth 位。比如 13 二进制为 1101,则当全局深度为 1 时返回 1, 全局深度为 2 时返回 1,全局深度为 3 时返回 5, 全局深度大于等于 4 时返回值都为 13。
    程序中使用的方法是 key 值对 1 左移 globalDepth 位后的值取余:key % (1 << depth)
    5.1.2 桶分裂算法假设原来桶中的记录哈希值为 hashKey,则分裂后桶中的记录的哈希值可能为 hashKey 或者 1 << localDepth + hashKey,即原来的 hashKey 最高位前补 0 或 1。所以桶分裂后桶中的记录会根据记录的 key 值的哈希判断是留在旧桶还是移动到哈希值为 1<< localDepth + hashKey 的桶中。另外由于采取的是变长记录,删除掉一条记录后需要对之后的所有记录进行移动,所以旧桶对移出的记录不立即删除,而是将其长度置为-1,等桶分裂完成后再一次性将旧桶中所有长度为-1 的记录删除。
    void Buffer::spiltPage 当桶中存在下一条记录时循环: 计算该条记录的哈希值并与当前桶的哈希值进行比较 两者相同则进行下一次循环 两者不同则将该记录插入新桶中,并将该记录在当前桶的长度置为-1对旧桶进行更新操作
    void Page::adjustPage 当桶中存在下一条记录时取该条记录进行循环得到该条记录的长度,判断其是否为-1 是,则继续循环 否,则取在其前面的距离最远一条长度为-1的记录,覆盖为该条记录更新桶的记录数和可用空间偏移
    5.1.2.1 目录维护桶分裂时需要对目录进行相应的维护,这里分两种情况:
    localDepth < globalDepth
    这时需要将目录中新的哈希值所对应的桶号改为新桶桶号,同时如果需要的话,还要把另外一些关联的哈希值的对应桶号也改为新桶桶号。比如当前全局深度为 7,一个局部深度为 5,哈希值为 00110 的桶要进行分裂,这时不仅要将哈希值 01 00110 对应的桶更改为新桶桶号,还要将哈希值 11 00110 对应桶号做相同更改。
    void Manager::insert Value = 0 循环 Offset = value << (localDepth + 1) Hash = oldHash + (1 << localDepth) + offset 当得到的哈希值超出允许位数时退出循环 设置hash对应的桶号为新桶号 Value++
    localDepth == globalDepth
    这时仅需要将目录翻倍后将哈希值为 oldHash + 1 << globalDepth 对应的桶号更改为新桶的桶号即可。
    5.2 高位哈希算法5.2.1 哈希方法从第 23 位开始从高到低将 key 值截取 globalDepth 位。比如 2816 二进制为 0000 0000 0000 1011 0000 0000,则当全局深度小于等于 12 时都返回 0, 全局深度为 13 时返回 1,全局深度为 14 时返回 4, 全局深度为 15 时返回值为 5 等等。
    程序中使用的方法是 key 值右移 23-globalDepth 位后与 007fffff(16)取与的结果:
    (key ≫ (23 − globalDepth) ) & 007
    5.2.2 桶分裂算法假设原来桶中的记录哈希值为 hashKey,则分裂后桶中的记录的哈希值可能为 hashKey << 1 或者(hashKey << 1) + 1,即原来的 hashKey 的两倍或者两倍加一。这里不会将旧桶中的记录分到两个新桶,而是将旧桶对应的哈希值翻倍,之后再像低位扩展一样,根据旧桶中的记录的哈希值进行判断,哈希值恰好是原哈希值两倍的记录留在旧桶,其他移往新桶,并在全部记录都移动好后再对旧桶进行整理。
    5.2.2.1 目录维护桶分裂时需要对目录进行相应的维护,这里也分两种情况:
    localDepth < globalDepth
    这里跟低位扩展一样,不仅需要将新桶对应的哈希值更新,还需要对关联的哈希桶的哈希值进行更新。与低位扩展不同的是,高位扩展是在最低位后补 0 或 1。比如当前全局深度为 7,一个局部深度为 5,哈希值为 00110 的桶要进行分裂,这时不仅要将哈希值 00110 10 对应的桶更改为新桶桶号,还要将哈希值 00110 11 对应桶号做相同更改。
    void Manager::insert start = localHash << (globalDepth - localDepth) size = 1 << (globalDepth - localDepth - 1) for (int j = size; j < (size << 1);j++) 设置start + j 对应的桶号为新桶号
    localDepth == globalDepth
    这时仅需要将目录翻倍后将哈希值为 (oldHash << 1) + 1 对应的桶号更改为新桶的桶号即可。
    5.3 时钟算法每个桶都有一个 pin 标志位和 ref 标志位。Pin 表示该页不可被换出,ref 表示当前页的时钟标志。
    int Buffer::choseByClock 从 currentPage 开始循环取 buffer 中的下一页(首尾循环) 若当前页的 pin 与 ref 均为 false 则退出循环 否则如果当前页 pin 为 false,将其 ref 设为 false 返回当前位,并把 currentPage 设为下一页
    5.4 插入记录算法 void Manager::insert 对将要插入的记录的键值进行哈希得到哈希值 通过得到的哈希值查找 index 目录,找到其应放入的页的pageID 如果该页不在内存中 用时钟算法得到一可用来替换的页 如果该页的 dirty位为 true,表示该页已修改 将该页写回硬盘 从硬盘读入要插入的页,放于替换出去的页的内存中将该记录插入内存中对应的页 如果插入不成功,则需要进行分裂 页分裂 递归调用自身再次进行插入 修改该页dirty位为 true
    六、性能测试 以下为软件在本人电脑中运行的截图:
    低位扩展哈希,8 页

    低位扩展哈希,128 页

    高位扩展哈希,8页

    高位扩展哈希,128 页

    可得到表格:
    哈希方式和缓存页数与程序效率的联系

    通过对比低位哈希和高位哈希时的建立索引耗时和查找记录耗时,可以看出建立索引时高位扩展比低位扩展速度要快很多,相差了两个数量级。这很大程度上是因为 listitem.tbl 文件本身是有序的,这就导致高位扩展建立索引时数据会按照顺序写入桶中,并且前期因为数据的哈希值都为 0,导致桶和目录的不断分裂,而由于数据的有序性,后来插入的数据的桶命中率会很高,这就大大降低了 I/O 次数,最终导致了高位哈希的建立索引的速度较快。
    而查找数据时低位哈希却比高位哈希快了 3 倍多,这应该是因为测试时 testinput.in 文件中的数据是随机生成的,高位扩展发挥不了其优势,由于数据的随机性目录页的命中率和缓存页的命中率都下降的缘故,而低位扩展数据分布得较为均匀,所以命中率会较高,最终导致低位扩展速度较快。
    而对比 8 页和 128 页还可以发现无论是低位还是高位扩展,更多的缓存页都会一定程度上提高索引和搜索时的速度,但提高程度并不是特别明显。
    哈希方式和缓存页数与存储空间和 I/O的联系

    首先可以明显看出高位哈希的 I/O 次数和 I/O 用时占比明显低于低位扩展,其中的原因在上一步已经分析过,是因为高位扩展本身所需的 I/O 次数比较少,占比也就会比较少。
    目录大小方面,高位索引的目录也比低位索引的小,但哈希桶的数量却比低位扩展的少,这应该是由于低位扩展的数据分布不均匀才导致桶的数目少但目录却更大的结果。
    七、性能优化
    尽量使用位运算

    在一些频繁使用的函数如哈希函数中尽量使用位运算而避免使用乘除法,一些简单的如乘二除二的运算也用左右移位来替代
    将调用次数较多的函数改为宏函数或去掉函数调用直接计算

    优化程序时利用 VS 的性能报告发现一些需要频繁使用的函数用时占比较多,将其改为宏调用或直接计算后可加快 10% ~ 15%的程序用时
    使用Win API代替 C 库函数 (暂未实现)

    直接调用 Win API:CreateFile,ReadFile,WriteFile,SetFilePointer。使用 Win API 会降低程序的可移植性,但是对于数据库这种对性能要求较高的系统,必然会针对平台进行优化,所以使用 Win API 是很正确的做法
    预计效率要比使用 fread 等高不少。但由于 CreateFile 和 ReadFile 和 WriteFile 会自动把原本单字符的\n 读入或写出为\n\r 的双字符,修改成本高,经过一天的尝试均告失败后,由于时间关系,最终决定暂时不做这部分的优化

    让 BUFF 在空间上连续

    申请 BUFF 时候,让所有页位于一个连续的空间内,这样能够显著提高 Cache 的命中率,最终能够提升速率
    0 评论 9 下载 2020-09-04 19:16:58 下载需要10点积分
  • 基于Newban的Nancyj字体Email签名工具

    一 需求分析Newban是一个输出mail签名的工具,能够在终端将字符进行“图形化”输出。本程序就是用所给字体文件,在屏幕上输出该字体的签名,并要求实现如下功能:

    能设定输出宽度
    能设定输出对齐方式,可以设置左对齐、居中对齐、右对齐
    能指定字体

    基本要求如下所示:

    字体信息必须以文件形式存放,文件名为 nancyj。输出时,从字体文件中取出相应字符图形
    输出宽度信息以命令行参数形式传递给程序,如果省略宽度信息,默认为80个字符宽度。参数开关为-w。例如,如果程序名为newban,则运行时指定输出宽度为200个字符的命令为:newban –w 200
    对齐方式以命令行参数形式传递给程序,如省略,默认为左对齐。参数开关为-l,-c,-r。对齐方式可以指定其中之一。-l表示左对齐,-c表示居中对齐,-r表示右对齐。例如:指定输出为右对齐方式,则命令格式为:newban –r
    可以同时设定输出的宽度和对齐方式。例如,命令:newban–r–w 80则指定输出右 对齐,输出宽度为80字符
    (提高)可以设定输出字体,命令行参数用-f,后面接字体文件名。例如,以字体standard.data字体为输出字体,则命令为:newban –f standard.data
    程序运行后,接受用户输入,按回车后以指定格式和宽度显示内容,直到用户输入quit 结束
    字体文件中应含如下字符的图形信息:a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 0 1 2 3 4 5 6 7 8 9

    二 程序设计2.1 总体设计
    2.2 详细设计2.2.1 主程序设计 1.主函数原型:int main(int argc, char*argv[])
    2.功能:在主函数里调用其他功能函数,实现功能。
    流程图如下所示:
    说明:

    主函数中没有结束标志,当调用输入函数时,遇到 quit命令,会退出程序
    num_word是单词数,每次循环都需要初始化,以免对二次输入产生影响
    判断命令行参数,将输出宽度信息存储在width中,将对其方式信息存储在 alignment中,将字体文件名信息存储在 filename中

    判断命令行参数的具体流程如下:

    2.2.2 读取文件 1.函数原型:void read()
    2.功能:读取字体文件中的图形信息,并存入三维数组 Type中。
    流程图如下所示:

    说明:

    读取的高度信息,用 atoi 函数转化为整型数字,存储在 high 中
    读取的图形信息,存储在三维数组 Type 中
    由于读取是用的 fgets 函数,每个字符串后面均有一个换行符,所以要去掉换行符

    2.2.3 用户输入 1.函数原型:void put_in()
    2.功能:接收用户输入,并计算单词数。
    流程图如下所示:

    说明:

    用户输入的信息存储在 Text中,用 strcmp 函数与“quit“比较,判断是否退出程序
    计算的单词数存储在 num_word中
    调用控制单词长度函数,根据单词长度增加单词数,以达到控制单词长度的目的

    2.2.4 控制单词长度 1.函数原型:void iswordover()
    2.功能:判断每个单词的图形字符数总和,如果一个单词的字符数总和大于了规定宽度,则增加单词数,以便于下面利用储存单词函数把过长的部分储存成另一个单词。
    流程图如下所示:

    说明:

    功能主要判断每一个单词是否超过 width,根据超过的部分的长度增加单词数,以达到控制单词长度的目的。
    2.2.5 储存单词 1.函数原型:void make_word()
    2.功能:把每一个单词存放到在主函数中申请的num_word个动态数组中,以便于输出时调用。同时调用相邻字母衔接处的判断函数函数 issame()和相邻字母衔接处的修改函数alter() 实现相邻字母共用相同的字符。
    流程图如下所示:

    说明:

    将Text中的每一个字符进行处理
    将单词存储在动态数组 p_word 中,根据指针移动控制单词序数
    遇到空格,或者 p_word 储存的单词的长度达到规定宽度,移动指针到下一个单词,将下面的字符图形储存到下面的单词,达到将单词一个一个储存的目的
    在将字符图形连接到 p_word 前,先调用衔接处判断函数,判断是否需要修改衔接处,需要则调用衔接处修改函数

    2.2.6 判断相邻字母衔接处 1.函数原型:int issame(int j)
    2.功能:判断相邻字母的衔接处的字符是否相同或者有一方为空格。
    流程图如下所示:

    说明:

    当首尾衔接处的字符都相同,或者由一方为空格,则返回真值 1;否则返回 0
    2.2.7 修改相邻字母衔接处 1.函数原型:void alter(int j)
    2.功能:修改相邻字母衔接处的字符,实现相邻字母共用相同的字符串。
    流程图如下所示:

    说明:

    copy、alter3是动态数组。
    在修改之前,要将图形信息复制给copy,以防改变字体形状数组 Type 中的值

    2.2.8 缓存处理 1.函数原型:void buffer()
    2.功能:将储存好的单词进行连接处理,判断何时输出与怎样输出。 流程图如下所示:
    说明:

    Out_word是用于输出的动态数组,储存的单词在这里进行连接和输出
    当判断out_word的长度加上空格长度加上将要连接的单词长度超过规定长度,则先输出,在进行覆盖,以达到控制输出宽度的目的
    空格,指的是在字体文件最后储存的一个空格图形,在 Type中的下标为62

    2.2.9 输出 1.函数原型:void put_out()
    2.功能:将out_word中储存的短语输出,同时调用输出对齐方式的控制函数out_type()函数计算出每行左边要输出的空格数。
    流程图如下所示:

    说明:

    为了控制输出的对齐方式,便采用在左端输出空格的方式实现,调用 out_type()函数计算空格数,在每行输出 out_word之前先输出空格
    2.2.10 控制输出格式 1.函数原型:int out_type()
    2.功能:根据规定的输出宽度和out_word中字符串宽度,计算出输出时左方应输出的空格数。
    流程图如下所示:

    说明:

    alignment为对齐方式信息,width为规定的宽度,strlen(out_word->line)是要输出图形的长度
    当对齐方式为左对齐时,返回 0
    当对齐方式为左对齐时,返回(width-strlen(out_word->line))/2
    当对齐方式为左对齐时,返回 width-strlen(out_word->line)

    2.2.11 字体文件
    文件概况:第一行为此字体文件的字体形状的高度信息;再往下为字体形状信息,按竖排分布,每次用 fgets函数读取一行
    文件制作程序:为了方便制作字体文件,编写了一个程序,可以将横排分布的字体文件转换为竖排分布
    文件截图如下所示:


    三 运行测试3.1 默认字体(nancyj)、对齐方式(左对齐)、输出宽度(80)
    3.2 居中对齐”-c”开关
    3.3 右对齐”-r”开关
    3.4 输出宽度”-w”开关
    3.5 切换文件”-f”开关(big字体)

    (nscript字体)

    3.6 字体文件制作程序 (源文件)

    (目的文件)
    1 评论 23 下载 2018-10-21 10:55:52 下载需要7点积分
  • 基于C语言实现的流星雨模拟课程设计

    一、课程设计内容及要求程序模拟一组流星飞向地面的情景。地面用多行#来表示,流星用大写字母不表示。程序产生一组流星(比如10个),从屏幕顶部下降飞向地面。一组流星中,每个流星的字符颜色是随机的,下降的位置是随机的,下降的速度也是随机的。一个流星下落只能去掉一个#号,当最后一行地面有#被去掉时,程序终止。
    二、设计思路
    程序在下降过程中,程序必须知道流星的字符、颜色、位置、速度,因此程序需要定义以下几个数组变量:存放流星字符的数组,存放流星字符颜色的数组,存放流星行位置的数组,存放流星列位置的数组,存放流星下降速度的数组
    可定义二维数组screen表示地面和天空,此数组是一个24行81列的字符数组。上面的行表示天空,数组单元的值是空格;最下面的几行(如5行)表示地面,数组单元的值是’#’;整个屏幕的大小是80*25,即25行80列, 为了在输出最后一行时不换行滚屏,程序只能利用上面的24行空间。把数组定义成81列的目的是,每行的最后字符赋值成’\0’,就可以按照字符串的方式输出每行文本了
    程序首先输出地面和天空,即输出定义的二维数组screen中的字符串,前19行是空行,后5行是#号。这样screen[24][81]的字符矩阵就与整个屏幕对应起来
    然后随时机产生一组流星数据,包括字符、颜色、位置和速度。速度用一次下降多少行来表示,最大的速度是4。由于要随机产生这些数据,因此需要调用random函数。random函数的原型是 int random(int num); 这个函数产生一个0—num-1之间的一个随机数。 流星字符可以这样产生:random(26)+’A’; 流星字符的颜色可以这样产生:random(16)+1;流星下降的位置可以这样产生:random(4)+1;流星的行位置一开始都是1; 流星的列位置可以这样产生:random(80)+1;但要保证所有流星的列位置不能相同(设计一个算法来判断)
    调用random之前,用randomize()库函数进行初始化。 两个库函数都在stdlib.h文件中
    每个流星按照自己的速度下落,所谓的下落就是逐行移动流星字符:在下一行新的位置上显示流星字符,在原来的位置上显示空格以便擦除流星字符,然后再延迟等待几十毫秒。这样循环往复就构成了流星下落的动画。 但要注意,流星的速度各不相同,而一次下落多行的流星也要逐行下落
    如果流星的新位置所对应的screen的单元格的值是’#’,则表示撞到了地面。 这种情况下在流星的新位置上输出空格,擦除#号,并且对screen相应的单元赋值为空格,流星字符也要赋值为空格,以表示流星消失
    当screen[23]中任何一个单元格是空格时,程序终止

    三、程序框图
    四、程序中遇到的问题及解决方案在C++程序设计中,我发现所给的设计思路是对的,但是里面提到的引用的库函数是不可执行的,例如randomize()这个函数,在VC中不能使用,于是我便选择了一个与其功能相似的rand()函数代替它;另外,在控制字母颜色的函数选择上,起初选用的textcolor()函数,但是一样不适用,于是经过再三思考后,选择了在课本中学习过的switch()语句,达到了预期的效果。
    0 评论 143 下载 2018-10-20 22:48:36 下载需要9点积分
  • 基于C++的图书管理系统

    一、需求分析图书管理系统功能如下:

    添加书籍
    删除书籍:当系统中存在此书时,删除该书籍,否则提示用户删除失败
    借阅书籍:当系统中存在此书且未被借阅时,将书的状态设置为“借出”,否则提示用户借阅失败
    归还书籍:当系统中存在未归换的该书时,将书的状态设置为“可用”,否则提示用户归还失败
    查询全部:输出系统中所有的书籍(书名升序)以及他们的状态
    查询书名:输出系统中所有与书名同名的书以及他们的状态
    书籍计数:输出系统的藏书量或某一本书的副本数量

    二、文件间关系

    MyLibrary.h与MyLibrary.cpp。定义并实现了Book类和Library类。前者用以表示书籍,后者构成整个系统。这部分属于类的设计范畴。(注:类设计者部分没有实现输出,输出由“使用者”实现)
    frame.h与frame.cpp。定义并实现了在main.cpp中使用了的函数。这些函数全部与输出有关
    main.cpp。程序运行的入口。直接调用library库和frame的函数。此文件内实现了简单的交互界面
    主要工作委托给std::multiset实现

    三、数据设计
    3.1 State 枚举enum State {Borrowed, Available}; //Borrowed表示被借出,Available表示书籍可用。
    3.2 Book 类class Book {public: //定义类型别名以实现抽象 typedef multiset<Book> data_t; //不允许书籍默认构造。只允许通过传入书名构造。将书籍状态设为“可用” Book() = delete; Book(const name_t& name): name(name), state(Available) { } ~Book() = default; //使用书名比较书籍间的相互关系 bool operator<(constBook& other) const { return name < other.name; } bool operator==(constBook& other) const { return name == other.name; } //一些访问和修改的函数 //关于书籍名字和书籍状态(“可用”,“已借”) State GetState() const {return state; } name_t GetName() const {return name; void SetName(constname_t& _name) { name = _name; } void SetState(State newState){ state = newState; }private: name_t name; State state;};
    3.3 std::multiset<Book>用以储存数据3.4 Library类classLibrary {public: //定义一系列类型类型别名实现抽象 typedef multiset<Book> data_t; typedef typename Book::name_t name_t; typedef typename data_t::iterator Iterator; typedef vector<Book> Info_t; typedef string file_name_t; //只允许该类默认构造,不允许复制 Library() = default; Library(const Library&) = delete; Library& operator=(const Library&) =delete; //分别为增加,删除,借阅,归还书籍。 void Insert(const Book& book) {libraryData.insert(book); } bool Remove(const name_t& name); bool Borrow(const name_t& name); bool GiveBack(const name_t& name); //返回该类中书籍的相关信息 vector<Book> GetBookInfo(constname_t& name) const ; vector<Book> GetAllBookInfo() const; //返回该类中书籍的数目 size_t Count(const name_t& name) const {return libraryData.count(Book(name)); } size_t NumOfBook() const { returnlibraryData.size(); } //保存信息到文件,从文件读取信息 //接受文件名为参数 void SaveToFile(const string& fileName)const; bool LoadFromFile(const string& fileName);private: //储存书籍的容器 data_t libraryData; //内部辅助函数。输入书籍的名字,返回一对迭代器(std::pair)指向一个区间范围。该范围内的书籍都有相同名字name inline auto _find(const name_t& name)const -> const pair<Iterator, Iterator> { return libraryData.equal_range(Book(name)); }};
    四、函数间关系(指frame文档内的函数)
    说明:

    所有函数的第一个参数都为ostream& os,用以接受输出流。在main中都将该参数赋为cout。调用者可根据实际情况修改
    所有函数都返回void
    下层的四个函数相互独立

    // 打印“>>>”用以实现交互界面inline void PrintPromt(ostream& os);// 打印指导信息。每次运行该系统都会输出该内容void PrintInstroduction(ostream& os);// 打印帮助信息。在系统中输入help会调用该函数输出帮助void PrintHelp(ostream& os);// 打印错误信息。该函数会将参数s直接打印出来(加上换行符)inline void PrintError(ostream& os, const string& s);// 将系统中的全部书籍以表格形式输出。接受library类。通过调用PrintTitle(打印表格)和PrintAllBook(打印所有书籍)执行打印inline void PrintTable(ostream& os, const Library& lib);// 打印表格的题头。执行实际的打印行为inline void PrintTitle(ostream& os);// 打印系统中的全部书籍。通过调用PrintBookInOneLine打印书籍void PrintAllBook(ostream& os, const Library& lib);// 实际执行打印行为的函数。每次只打印一行。接受Book的对象为参数inline void PrintBookInOneLine(ostream& os, const Book& book);
    五、数据交流
    注:

    user指这个程序实际的用户(甲方)
    main指Libraryclass的使用者
    Library指Libraryclass的底层实现
    file指储存信息的本地文件

    不提供跨层交流数据的接口。如main向file交换数据必须通过library。

    从user到main。通过cin交换信息。
    从main到user。通过frame.h和frame.cpp中定义的一系列函数进行输出到终端。(见函数间关系)
    从Library到main。vector<Book> Library::GetBookInfo(constname_t& name) const ;vector<Book> Library::GetAllBookInfo()const;通过Library类中定义的两个函数传送书籍信息到main。
    以及通过Library成员函数的返回值传递信息。
    从main到class。通过Library类中的成员函数的参数传递来传递数据。
    从Library到file。main向Library提供文件名,library根据该文件名存放数据入file中。
    file到library。main向library提供文件名,library根据该文件名向file读取数据。

    六、main函数的算法流程

    输入:输入使用getline读取整行
    是否语法错误:使用正则表达式检查语法错误。若格式不匹配,输出错误提示信息,重新等待输入
    是否逻辑错误:调用Library类并查看返回值。若返回false表示出现逻辑错误。输出提示信息,重新等待输入
    执行命令:命令被执行

    七、合法的命令
    add <bookname>:添加一本书。
    rm <bookname>:删除一本书
    brw <bookname>:借出一本书
    ret <bookname>:归还一本书
    cnt <bookname>:统计该书的数目
    cnta:统计所有(all)书的数目
    prt <bookname>:打印该书相关信息
    prta:打印所有书的信息
    save:将信息储存到本地文件
    load:载入本地文件中的信息

    八、使用的正则
    匹配命令的格式:^(quit|help|save|load|prta|cnta)(\s.*)?$|^(add|rm|brw|ret|cnt|prt)
    匹配整个输入的格式:^(quit|help|save|load|prta|cnta|(add|rm|brw|ret|cnt|prt)[a-z\sA-Z_]+(\w|_)*)$
    1 评论 327 下载 2018-10-20 18:38:59 下载需要8点积分
  • 基于C++的多项式计算器

    一、 实验环境1.1 编程语言和开发工具
    语言采用ANSI C++(C++11)
    开发工具:vim,g++,git,Visual Stdio(用于生成exe文件)

    1.2 编码规范编码规范中所有没有涉及到的内容,参见googlestyle C++

    类名

    类名采用开头大写的方式命名
    变量

    局部变量采用小驼峰法命名类私有和保护的成员变量末尾加下划线类公用变量命名同局部变量
    函数

    函数采用大驼峰命名法命名类的私有成员函数末尾加下划线,如 voidshrink_();get/set/isXXX等函数采用开头小写get/set/is,其后接开头大写单词的方式命名.如isValid, setName
    其他

    常量采用字母k + 大驼峰命名法命名。但类中的const变量依旧采用类变量的方式命名类型名称(typedef)采用下划线命名法命名。单词最后加_t。如name_t左大括号“{”与前一语句在同一行

    二、分析与设计2.1 需求分析
    系统储存多项式
    多项式相加
    多项式相减
    多项式与常数 数乘运算
    多项式代入某点求值
    系统显示储存的多项式
    显示帮助

    2.2 系统功能图
    2.3 类结构设计
    2.4 细节设计以下+表示public,-表示private,#表示protected
    以 [+-#] operation(type param) : return-type
    或 [+-#] attribute : type等形式给出。self代表类本身(Polynomial)

    接口设计:

    +typedef vector<double> coefficient_t;
    +typedef Polynomial self;
    +Polynomial(string); // 构造函数,对输入作parsing。输入应形为”(1, 3)(2, 4)(4, 6)”
    +operator rel(const self&) : bool //关系运算符。rel 为 == 与 !=,判断两个多项式是否相等。
    +operator op(const self&) : self //运算符。op为+,-,*中的一个
    +operator op(const self&) : self& //运算符。op为+=,-=,*=中的一个
    +operator op(double) : self //数乘运算符。op为, =中的一个
    +operator<<(ostream&, self) :ostream& // 作cout用
    +getDegree() : size_t // 返回该多项式的次数
    +setDegree(int) : void // 设置多项式的次数。若设置的次数低于多项式原本的次数,设置被拒绝。
    +getCoefficient(): vector<double> // 返回一个数组,储存多项式系数
    +derivation() : self // 返回该多项式求导后得到的多项式
    +evaluate(double) : double // 返回多项式代入某点值后的值

    成员函数:

    -shrink_() : void // 简化多项式。即将多项式系数为零且没有必要储存的项舍去。
    数据成员:

    -coefficient : vector<double> //储存多项式的系数
    静态成员:

    +isValid(string) : bool // 接受一个代表多项式的字符串,判断其是否合法+pattern_ : const string // 用于正则表达式的字符串。判断多项式输入是否合法。用于isValid+regex_ : regex //判断输入是否合法的正则。使用了pattern_+pattern_iterate_ : string //用于正则表达式的字符串。用于提取括号。+regex_iterate_ : const regex // 提取括号。使用了pattern_iterate_+poly_name_pattern_ : const string //用于正则表达式。检测用户提供的多项式的名字是否合法+regex_poly_name_ : regex // 正则表达式。判断用户提供的多项式的名字是否合法。

    2.5 正则匹配
    匹配一个单词


    提取一个括号


    判断输入是否合法

    三、测试心得本次项目相比于前几次项目,难度较低。唯一的难点是parsing,即如何将用户的输入正确地转换为多项式。parsing可以用灵活的正则表达式完成。其次的难点是用户交互界面,即如何用尽量简短的代码完成交互界面的需求,写出DRY(don’t repeat yourself)的代码。
    相比之下,多项式类的实现反而显得难度较低。可以采用vector<double>储存按索引储存系数,或采用足够大的double[]储存系数。(本项目不考虑指数为负的情况)。
    本次项目有些地方可以更加细致。例如根据“面向接口编程”思想,“交互”应该与“数据”和“接口”分离开。Polynomial类应该从一个Interface_Polynomial类继承而来。由前者作为接口,后者作为实现。main函数中所有函数的参数都使用Interface_Polynomial而非Polynomial。
    本次项目还可以拓展成允许矩阵运算的计算器。将两者结合在一起。
    本次项目让我对C++的使用更加熟练。让我对正则的书写更为了解。加深了我的对面向对象的理解。
    2 评论 25 下载 2018-10-20 16:32:23 下载需要7点积分
  • 基于C++的平面形状编辑器的设计与实现

    一 需求分析参考如下给出的类层次关系,实现一个平面上的形状编辑程序序。要求如下:


    按照下面类图给出的层次关系来定义类
    所有形状支持无参数构造,有参数构造,拷贝构造,析构
    所有形状支持平移操作,需要重载 operator+
    所有形状(除去无意义的),均支持计算周长
    所有形状(除去无意义的),均支持 Draw()操作,此时只要要显示形状的名称,位置等信息
    需要实现一个 CShapeArray类,该类类似一个数组,用来存放放编辑过程中的平面形状。该类需要支持:添加,插入,删除,查询,复制等等操作。可以支持形状编辑中需要的针对形状的操作
    主程序中实现用户输入形状及其参数,然后把形状存入 6中定义的 CShapeArray。在输入形状的同时,用户可以查询当前已经输入入的形状(可按名称(需要对每个平面形状加入名称),位置来查询)。支持用用户对形状的复制,粘贴(粘贴时假设用户指定粘贴的位置)。同时支持用户对对形状的删除操作
    输入和处理好的形状可以存入文件,并从文件中读入
    支持对当前所有形状的 Draw()

    二 程序实现2.1 开发环境本程序使用Visual Studio 2010编写,并按照规范风格,将类、函数、变量的声明、其他头文件的引用等写在头文件(*.h文件)中,并使用头文件保护符(#pragma once)保护以免重复编译,具体函数的定义等则写在对应的源文件中。函数的注释说明写在头文件函数声明处,函数内部具体代码说明写在源文件代码处。
    2.2 类与变量声明2.2.1 类声明本程序定义了以下几个类(其中由于 CPoint为VS中的保留类名,改用 CCPoint):
    class CShape;class CCircle:public CShape;class CCPoint:public CShape;class CLine:public CShape;class CRectangle:public CShape;class CPolygon:public CShape;class CTriangle:public CPolygon;class CShapeArray;
    2.2.2 全局变量及全局函数声明声明如下所示:
    /**********************全局变量声明**********************///保存到的文件名const static char *fname = "D:\\dat.txt";// CShapeArray对象CShapeArray arr;//记录要复制的形状所在位置int copyPos = -1;/************************函数声明************************///输入数字进行选择int input(char *, int);//清屏void clr();//暂停void pause();//选择操作void inputOp();//选择形状void inputShape();//查询void inputQuery();//输入位置查询void inputPos();//输入名称查询void inputName();//复制void inputCopy();//粘贴void inputPaste();//删除void inputDel();//保存至文件void saveToFile();//从文件读取void loadFromFile();//输入矩形CRectangle* inputRectangle();//输入圆形CCircle* inputCircle();//输入多边形CPolygon* inputPolygon();//输入三角形CTriangle* inputTriangle();//输入点CCPoint* inputPoint();//输入直线CLine* inputLine();
    2.3 类定义2.3.1 CShape 基类 CShape定义如下:
    class CShape{ public: const static int SHAPE_CIRCLE = 1; const static int SHAPE_POINT = 2; const static int SHAPE_LINE = 3; const static int SHAPE_POLYGON = 4; const static int SHAPE_RECTANGLE = 5; const static int SHAPE_TRIANGLE = 6; //无参数构造 CShape(void); //有参数构造 CShape(char*); //析构函数 ~CShape(void); //拷贝构造 CShape(const CShape&); //获取名称 char *getName(); //显示名称等信息,使用虚函数 virtual void Draw(void); //计算并显示周长,使用虚函数 virtual void Calc(void); //保存至文件 virtual void saveToFile(ofstream&); //从文件读取 virtual CShape& loadFromFile(ifstream&); //判断形状是否存在 virtual int exist(); protected: //图形的名称 char* name; int shape;};
    其中函数
    virtual void Draw(void);virtual void Calc(void);virtual void saveToFile(ofstream&);virtual CShape& loadFromFile(ifstream&);virtual int exist();
    为虚函数,具体根据继承的子类不同而有不同定义,体现了 C++的多态性;
    变量 const static int SHAPE_CIRCLE等用于区分不同子类的具体类型,用于全局函数 loadFromFile()从文件加载形状信息。
    子类 CCircle, CCPoint, CLine, CRectangle, CPolygon继承于基类 CShape,类CTriangle 继承于类 CPolygon,分别表示具体的形状,并分别定义了各自的 Draw,Calc,saveToFile,loadFromFile等函数。
    2.3.2 CShapeArray类 CShapeArray用于存放编辑过程中的平面形状,支持:添加,插入,删除,查询,复制等操作。使用 vector<CShape*> vec来保存添加进的 CShape 对象的指针。
    其定义如下:
    class CShapeArray{ public: CShapeArray(void); ~CShapeArray(void); //添加 void add(CShape*); //插入 void insert(int, CShape); //删除 void del(int); //清除全部 void clear(void); //查询 CShape* get(int); //复制 void copy(int, int); //显示所有元素 void drawAll(void); //获取元素数目 int getSize(void); //根据名称查询 int findByName(char*); private: vector<CShape*> vec;};
    三 程序运行过程程序启动即进入功能选择主菜单。用户通过输入数字并按回车进行功能的选择,如果用户输入的不是数字,或数字超出了输入范围,则提示输入错误需要重新输入(该功能由 input函数实现)。添加形状时,根据用户选择的形状不同,要求输入的参数也不同,如果输入的参数能构成相应形状(如三角形两边之和要大于第三边,此功能由 exist函数实现),则添加该形状至对象 CShapeArray arr中,否则提示错误而不保存。
    输入形状的名称时,支持输入中英文,并可包含空格。
    本程序能自动纠正部分用户输入错误,以及从文件读取时发生的错误,并给出相应提示信息。
    四 运行测试启动后提示输入数字选择操作:

    选择操作输入形状,选择要输入的形状为圆形:

    输入圆心坐标和半径,按回车键确认:

    选择操作“2.查询已输入”,选择“3.显示全部”:

    保存至文件(默认保存至 D:\dat.txt中):

    从文件读取(默认从 D:\dat.txt中读取):
    1 评论 12 下载 2018-10-18 15:10:45 下载需要9点积分
  • 基于C++的简易数据库的开发与测试

    一 开发说明1.1 总体说明本次项目以c++语言编写简易数据库,数据库为<key:value>的简单形式,在本项目中,限定key为整数且不考虑溢出问题,value为字符串类型,不可为空,长度最长为19(其中第20位为\0字符)。主体程序面向用户提供四种主要操作,分别为查找、添加、删除和修改。文件中数据结构主要采用B+树,实现了对删除的结点的空间回收。数据库cache模拟系统中的cache以利用文件读取的局部性来增加读写速度。文件以二进制形式打开以便于管理。
    1.2 文件设计说明1.2.1 索引文件设计说明索引文件前4个字节为根节点所在地址,若为0则树为空,初始时。接着8个字节为第一个空白位置,初始时为8,即文件尾。然后依次是每个节点。每个节点分为三个部分,第一部分为12个字节,四个整数,分别是父节点地址、父节点在节点中的位置(从1开始)和当前节点关键码的个数,根节点父节点地址为0。第二部分为当前节点的关键码和其孩子的地址,若节点为叶节点,则为当前节点的关键码和关键码对应的值在数据文件中的地址的负数(因此可以根据孩子地址的正负来直接区别内部节点和叶子结点)。第三部分为下一个叶子节点的地址,若节点为内部节点,则该部分无意义。空白位置组成单项链表,最后一项始终为文件末尾。删除节点后将地址链接到链表头部。
    1.2.2 数据文件设计说明数据文件前四个字节为第一个空白位置,初始时为4。之后为数据,每条数据占用20个字节。空白位置组成单项链表,最后一项始终为文件末尾。删除节点后将地址链接到链表头部。
    1.2.3 日志文件设计说明用户版和正确性测试版中每次操作会将相关信息写入日志文件,主要用于程序调试。
    1.2.4 正确性测试文件及性能测试文件设计说明文件第一个数字为需要进行的操作,数字1、2、3的含义与用户版对应,其后是需要操作的key,根据操作种类确定后面是否有value。正确性测试文件中4的含义为测试所有key。性能测试文件名最后数字问测试循环单位,而非测试数据量。
    1.3 类设计说明1.3.1 Cache类(Cache.h)1.3.1.1 总体说明cache类模拟系统中的cache,将最近使用过的数据放入其中,利用局部性加快数据的读写速度。cache大小固定,为16384,即2^14。cache中每个数据有四个值:int key、string value、bool dirty、bool valid。其中valid表示这个数据是否有效,dirty表示这个key对应的值是否被修改过。
    1.3.1.2 主要函数// 构造函数,确定cache大小,将所有有效位置为0Cache();// 从cache中取值,将取回的值放在value中,返回值表示是否取值成功bool select(const int key, string &value);// 向cache中插入值,若cache中原来有值且被修改过,则将原来的key和value存入*oldKey和*oldValue。返回值含义为0表示插入失败,1表示原来没有值或值没有被修改,2表示原来有值且被修改过int insert(const int key, const string&value,int *oldKey,string*oldValue);// 从cache中删除,若cache中存有key且有效位为true,则将有效位置为falsevoid remove(const int key)// 更新cache中的值,若cache中没有key,返回false,否则更新值且将dirty置为truebool update(const int key, string &value)// 将cache中的内容写会,所有没有保存修改的key和value存入*key和*value,交由索引文件和数据文件保存void save(vector<int> *key, vector<string>*value)
    1.3.2 Database类1.3.2.1 总体说明Database类为数据库的主体实现。持有变量scale为B+树的阶数。三个文件流分别对应索引文件、数据文件和日志文件,常量ZERO用于写入空指针。
    1.3.2.2 主要流程(对应用户版)查找

    用户输入关键码
    查找cache,若找到,返回对应的值
    否则查找索引文件,若没有找到,返回false
    否则根据地址查找数据文件,取出相应的值
    将关键码和值插入cache中
    若有必要,更新cache中原来的关键码和值
    根据返回值输出结果

    添加

    用户输入关键码和值
    若cache中存在关键码,返回false
    否则查找索引文件,若存在关键码,返回false
    否则在数据文件中插入值并获得位置
    根据查找结果在索引文件中加入关键码和数据地址
    将关键码和值插入cache中
    若有必要,更新cache中原来的关键码和值
    根据返回值输出结果

    删除

    用户输入关键码
    在cache中删除此关键码
    查找索引文件,若关键码不存在,返回false
    否则根据查找结果在数据文件中删除值
    根据查找结果在索引文件中删除关键码
    根据返回值输出结果

    修改

    用户输入关键码和值
    在cache中更新关键码和值,若成功,返回true
    否则在索引文件中查找关键码,若没找到,返回false
    否则根据查找结果在数据文件中更新值
    将关键码和值插入cache中
    若有必要,更新cache中原来的关键码和值
    根据返回值输出结果

    1.3.2.3 主要函数// 构造函数,文件不存在初始化文件Database();// 数据文件查找void dataFile_find(const int dataAddress, string &value);// 数据文件添加,返回数据地址int dataFile_add(const string&value);// 数据文件删除void dataFile_delete(const int dataAddress);// 数据文件替换void dataFile_replace(const int dataAddress, string &value);// 索引文件查找,*size为最后一个节点关键码个数。返回值含义为0表示树为空或key小于最小值,1表示命中,2表示在两节点之间,4表示大于最后一个节点最大值。若命中,*indexAddress存放命中叶节点地址,*pos为节点位置,*dataAddress为值在数据文件中的地址。若不命中,*indexAddress存放应该插入叶节点地址,*pos为插入位置(1表示在第一个关键码之后),*dataAddress无意义。对每个节点采用二分查找法int indexFile_find(int key, int *indexAddress, int *pos, int *size,int *dataAddress);// 索引文件添加void indexFile_add(const int key, const int dataAddress, const int indexAddress,int pos, int size);// 索引文件添加并处理上溢void indexFile_addAndOverflow(const int key, const int dataAddress,const int indexAddress, const int pos, const int size);// 索引文件删除bool indexFile_delete(const int indexAddress, const int pos, int size);// 索引文件当前节点向左兄弟借一个关键码void indexFile_borrowLeft(const int indexAddress,int size, const int left, int leftSize,int parent, int parentPosition);// 索引文件当前节点向右兄弟借一个关键码void indexFile_borrowRight(const int indexAddress,int size, const int right, int rightSize,int parent, int parentPosition);// 索引文件当前节点和左兄弟合并void indexFile_mergeLeft(const int indexAddress,const int size, const int left, int leftSize);// 索引文件当前节点和右兄弟合并void indexFile_mergeRight(const int indexAddress,int size, const int right, const int rightSize);// 索引文件删除并处理下溢void indexFile_deleteAndUnderflow(const int indexAddress,const int pos, int size);// 更新到文件bool file_update(const int key, string &value);// 对外接口查找bool select(const int key, string &value);// 对外接口插入bool insert(const int key, const string&value);// 对外接口删除bool remove(const int key);// 对外接口更新bool update(const int key, string &value);// 析构函数,保存cache,关闭文件~Database();
    1.4 版本设计说明1.4.1 用户版用户版无法测试,故没有数据结构维护正确答案,每次打开数据库若索引文件或数据文件不存在则新建文件并初始化,用户操作的相关信息会写入日志文件。
    1.4.2 正确性测试版默认情况下每次清空索引文件和数据文件,从测试文件中读取测试数据进行测试。若遇到错误则停止,否则输出通过并进入手动测试界面。可以通过更改文件打开方式和使用注释中的代码进行数据库关闭后再打开的测试(主要用于测试cache)。在简易测试模式下需手动注释cache的引用进行测试。(否则数据均在cache中,难以找出错误)简易测试为针对B+树规模为5时构造层数为3的B+树,之后人为计算各种增删改查的特殊情况进行测试。随机测试模式随机生成测试数据和增删改查操作进行测试。
    1.4.3 性能测试版默认情况下每次清空索引文件和数据文件,从测试文件中读取数据测试数据进行测试。测试完成后显示测试所用时间(单位为毫秒),用户输入0退出。
    1.5 开发环境
    代码编写环境:Microsoft Visual Studio 2017 community
    测试环境:windows 8.1

    1.6 其他说明辅助项目auxiliary目录下CreateTestFile项目用于生成正确性测试和性能测试所需要的文件。
    二 测试报告2.1 正确性测试分别运行简易测试和随机测试文件,测试结果均没有异常。

    2.2 性能测试2.2.1 测试结果当B+树的规模不同,数据量为1000000,插入次数为10000、循环测试次数为10000、删除次数为1000(删除后会随机读取10条记录)时,平均每次操作所用时长如下(总操作次数1033088)



    规模
    64
    128
    256
    512
    1024
    2048




    操作总时间/ms
    795088
    677865
    643788
    634600
    630898
    664984


    单次操作时间/ms
    0.76962
    0.65615
    0.62317
    0.61427
    0.61069
    0.64369



    数据折线图如下所示:

    在测试中,程序所占CPU为15%左右,所占内存为2-3M。由此推测程序没有进入死循环且没有内存泄漏。
    测试过程截图如下所示:







    2.2.2 测试分析和猜想根据B+树性质,对于阶数m数据量为N的单次操作时间复杂度为logmN。在本次测试中N几乎不变,故随着m增大,单次操作所需时间减少,这在m不很大的情况下有所体现。说明B+树对查找效率有更大的帮助,当m过大时,单次查找所需时间增加,猜想这是因为结点大小超过内存访问时单个物理页的面积,从而破坏整体读写,使B+树的优点无法充分体现。这说明B+树设计的合理性,也体现了本次设计达到了预期要求。
    1 评论 34 下载 2018-10-18 10:32:08 下载需要7点积分
  • 基于C++的通讯录系统的设计与实现

    一 需求分析通讯录系统可帮助使用者管理归纳通讯录名单,达到添加,删除,修改,保存等需求。
    二 系统设计2.1 功能模块设计通讯录主要功能为:添加通讯录成员,修改成员,删除成员,按需求搜索查看成员,保存为文档。
    如下图所示:

    系统各模块的功能具体描述为:
    1、添加成员模块
    提供界面让使用者输入希望加入的通讯录成员的各类信息(姓名,电话,住址, QQ,邮箱等),并检查格式是否有误。若格式无误,则将该通讯录信息通过二进制文件方式储存在./contact文件目录下。
    2、修改成员模块
    使用者可以重写已有的通讯录成员,增加或删除除姓名以外的各个信息。一条通 讯录成员可以拥有多个电话号码或QQ。
    3、删除成员模块
    使用者可以选择某个不希望继续使用的通讯录成员并删除他们。
    4、搜索查看成员模块
    使用者通过各种方式查询已添加的通讯录成员,并决定是否修改或删除它们。提供的方法有:精准查询,模糊查询,按分类查询等。
    2.2 系统架构设计系统开发使用Template Method设计模式和Strategy Patten 两种设计模式,较好的封装所需函数,使得主程序入口开发环节只需关注Contact.h头文件即可实现。
    具体类之间的耦合关系见下图:

    类的关系设计如下图所示:

    各类的具体功能和说明如下:

    class Person; 提供基本的数据存储结构
    classContactInterface; 提供主函数菜单,策略选择方法。是Contact类的一个接口,MainStrategy的调用者
    classContactInit; 提供初始化程序所需函数。同样是Contact类的一个接口
    classContact; 具体实现了两个接口的方法。MainStrategy的决策者。同时面向调用者(main.cpp)。但注意Contact不提供任何public方法。需要通过两个接口调用
    classCheckInterface; 提供检查函数
    classMainStrategy; Strategy Patten设计模式。同时包含子类公用的方法
    classMainNewMenu; class MainDelMenu; class MainMdfMenu; 分别override doMainStrategy()函数,实现新建,删除,修改功能
    classMainVewMenuInterface; override doMainStrategy()函数,ViewStrategy的调用者
    classMainVewMenu; ViewStrategy的决策者
    class ViewStrategy; StrategyPatten
    classViewAllMenu; class ViewExactMenu;

    class ViewFuzzyMenu; class ViewCategoryMenu; 分别override doViewStrategy()函数,实现所有查找,精确查找,模糊查找,按类查找功能。
    三 系统实现3.1 系统实现流程 通讯录系统实现流程如下图所示:

    3.2 类的实现 系统包含Person,Contact, ContactInterface, ContactInit等类,具体类结构声明如下:
    Person类:
    class Person {Public: char name[MAXNAME]; char sex; char tel[MAXTEL]; char addr[MAXADDR]; char zip[MAXZIP]; char mail[MAXMAIL]; char qq[MAXQQ]; char category[MAXCTGY]; Person(); ~Person();};
    ContactInterface类:
    class CheckInterface {public: bool check(Person&, const bool _check_repe) const; bool check_exact(const Person&, const string) const; virtual ~CheckInterface(){};private: vector<string> part_tq(const Person&, const char* const) const;};
    ContactInit类:
    class ContactInit {public: virtual int refresh() const = 0; virtual void welcome() const= 0; virtual ~ContactInit(){};};
    Contact类:
    class Contact : public ContactInterface, public ContactInit{private: MainStrategy* setMainStrategy(int); public: Contact(); ~Contact(); int refresh() const; void welcome() const;};
    MainStrategy类:
    class MainStrategy : public CheckInterface{public: MainStrategy(); virtual ~MainStrategy(); virtual int doMainStrategy() = 0;protected: void printAll() const; void print_prsn(const Person&, const string, bool) const; bool delete_prsn(Person&) const; int modify_prsn(Person&) const; //Way to modify a spefic Person member, with 0->success, -1->fail};
    MainViewMenuInterface类:
    class MainVewMenuInterface : public MainStrategy{public:private: ViewStrategy* viewStrategy; virtual ViewStrategy* setViewStrategy(int) = 0; virtual int view(Person* v_Person) const; public: MainVewMenuInterface(); virtual ~MainVewMenuInterface(); virtual int doMainStrategy();};
    四 系统测试4.1 登录界面系统运行开始的界面如图所示:

    主要通过选择结构和循环结构实现界面的前进和后退。例如,第一个登录界面出现5个选择:1.新建,2.删除,3.修改,4.浏览,5.退出
    4.2 添加联系人在开始界面输入“1”即添加新的成员:

    若显示 Information Entry Success! 则录入数据成功。若显示Information Error! 则录入数据失败。如图则因为在电话(TEL)中出现了中文字符。随后将返回主界面。

    4.3 删除联系人在主界面输入2可删除成员:

    如我们希望删除(2)数据,则键入2:

    就可以得到(2)的详细数据。输入y/n即可选择是否删除该成员。随后程序将返回主界面。
    4.4 修改联系人在主界面下输入3可以修改已有的成员:我们希望修改刚刚加入的成员(1)的电话号码,同时加入他所有常用的QQ号码:

    键入1, 可修改第一个人的信息,并按照需求修改信息:


    确认无误后即可修改信息,随后返回主界面。
    4.4 搜索联系人输入4即可按需求分类查看搜索成员:

    键入1进行精准匹配,该模式下只匹配名字:如输入“严恩伟”后匹配信息如图:

    随后可以根据自身需求选择1-3选项。此处不再演示。
    在view模式下键入2进行模糊匹配,该模式匹配名字,电话,地址。只要出现匹配字符即给与显示。如输入“181”后显示:

    选择1-3即可进入其详细页面。此处不再演示。
    在view模式下键入3进行分类(category)匹配。该模式会列出所有的分类,并可根据用户选择的分类进行罗列(其中未设置的分类被标记为Unset):

    根据提示选择1-2即可进入相应页面。选择0即可退出。
    在view模式下键入4进行全局匹配。该模式会列出所有的成员:

    在view模式下键入5退出view模式。
    在主菜单中键入5退出程序。
    1 评论 115 下载 2018-10-16 10:52:44 下载需要8点积分
  • 基于C++的学生生活系统设计与实现

    一 需求分析需要设计并实现如下场景:在那山的那边,湖的那边,有一所学校,学校里有一幢宿舍楼,宿舍楼有若干层,每一层有若干房间 ,一群学生快乐地生活在这里。他们每天可做的事情有:

    换宿舍,从一个房间搬到另一个房间
    退学,亦即搬出宿舍楼
    入学,亦即搬入宿舍楼
    吃饭,吃饭会增加体重,花费金钱
    学习,学习会消耗体重,增加魅力(注:学习是唯一可以直接提升魅力值的活动,这个养成游戏的价值导向还是蛮正确的)
    化妆(女生独有),化妆会花费金钱,提升容貌
    运动(男生独有),运动会消耗体重,增加健康(注:体重、容貌、健康与魅力之间存在一定的转化关系)
    谈恋爱,学生可以向其他的某个学生提出恋爱请求,被求爱的学生依据二者魅力值之差按照某种概率答应对方的求爱请求,从而建立双方的恋爱关系
    分手,处于恋爱关系的两个学生中的任何一方都可以提出分手,依据二者魅力值之差按照某种概率分手成功,从而断开二者之间的恋爱关系

    此外,宿舍楼还可进行扩建,亦即增加楼层,增加某层的房间数,以及增加某个房间可容纳的人数(不考虑这种任意增加的物理可能性),经过一段时间的生活后可对学生的状态信息进行查询。
    二 系统架构设计系统功能模块如下图所示:

    三 程序设计思路按照功能模块将程序分为初始化模块、读取执行活动指令模块以及查询模块。
    3.1 初始化模块要从sample_config.txt文件读取初始配置,并且将数据保存为全局变量从而可供所有文件使用,该文件内容在各个文件中都能使用到;从dispatch.txt文件读取初始宿舍的具体信息,并且建立最初的宿舍。
    3.2 读取执行活动指令模块文件读取方面,首先用文件流的方式读取,并且都使用split函数将指令分解成vector\<string\>,然后通过比较字符串判断指令的格式以及翻译指令内容,然后根据指令调用对应的功能函数。
    对各种指令和功能进行分析,可以归纳出主要有两个具体的对象进行活动:学生和宿舍,因此需要分别构造Student类和Dorm类并且对活动结果进行记录。
    学生从事吃饭、学习、恋爱活动。学生自身具有名字、性别、魅力值等属性,但是根据性别的差异有不同的特殊属性,因此用类的继承即可记录共有的属性值以及各自特有的属性值;学生从事的吃饭、学习等活动应作为类的成员函数进行实现,才更加符合对象独自完成的活动,而且吃饭、学习虽是公有活动,但是具体的实现方法根据性别会有不同,因此将这些共有的活动用虚函数在Student类中声明,并且在派生类中根据性别调用特有的实现方法;因学生的性别不同而导致的活动实现差异通过Student抽象基类指针统一调用,在具体的派生类中则分别实现其具体的活动过程;恋爱活动为多对象的活动,因此作为全局函数来实现;所有的活动都会引起一些属性的改变,只需根据规则或者公式进而实现即可。
    宿舍则主要进行入学、退学、搬宿舍、改变楼层等活动。首先是宿舍的结构,用结构体ROOM表示房间,其中包含人员、容量、性别(不能男女混住,因此需要有性别区别)等具体信息,再用vector\<ROOM\>表示一层楼,再嵌套一层vector表示一幢宿舍;其次改变楼层的活动add可以用重载的方法使得在增加楼层、房间、床位的函数实现上简化编写,在增加楼层等时需要按初始值初始化房间;入学等人员的活动则是对宿舍内部的信息进行修改,将这些活动设计为Dorm的成员函数更为妥当;由于需要存储所有具体的学生对象,在Dorm类中声明为共有成员变量可以使Dorm类与Student类的关联更为紧密,并且用映射的方法可以使学生对象得到共享,并且方便了查询;入学、退学、搬宿舍则是对学生对象的修改和房间信息的修改,需要两个地方同时修改,而且内存的分配和回收也要注意。
    3.3 查询模块显示查询界面,通过输入序号选择查询,并且对屏幕进行适时的更新或者清屏;
    调用查询函数,根据输入的指令调用相应的功能函数,并且在屏幕中输出查询的结果;
    实现功能,找出属性值的最值以及范围查询可以通过将所有的Student类的指针用vector保存,再按属性的值得大小排序,则可求出最值,范围查询则直接遍历比较即可输出结果;倘若需要分性别查询,则分别建立男生vector和女生vector进行排序;对于学生具体信息可通过名字直接在map中找到对应的对象,查找房间号则可以按下标找到对应房间,最后输出查找结果即可。
    四 详细设计4.1 对象结构及功能Student类作为抽象基类为男生和女生声明共同的成员变量和类成员函数来表示男女学生共有的属性以及学生个体的活动,例如:名字、性别、体重等共有的属性值,以及学习、吃饭等共有的学生活动。将共有的活动设计成虚函数是由于活动相同但是具体的实现不同,例如女生的魅力值的计算公式则与男生魅力值计算公式不同。
    Girl类、Boy类通过继承Student类并且实现其所有虚函数从而实现学生活动得出可实例化的具体类,并且分别添加各自的特有属性:容貌值和健康值;引用了Student指针使得学生活动会根据具体Girl类或Boy类来进行函数的调用,实现多态。Girl类和Boy类中还添加实现了weight_add()函数等私有函数,是为了使调用eat、study等学生活动函数时对于其它属性值的相应改变更为明显、直观,而且对学生活动的公式修改更为方便,直接修改增量函数即可。
    Dorm类实现建立宿舍的结构,用vector的方式分别记录楼层、房间,每个房间则用结构体保存人名、宿舍人员性别以及容量;用map\<string, Student\*\>记录所有入住的学生,实现从名字映射到具体的学生,从而通过名字共享了学生数据,方便对学生信息进行修改,并且还方便了在宿舍进行搬宿舍等活动中一旦对学生进行了修改,在修改学生信息时可直接通过名字找到该学生对象并且修改房号或者查询;add成员函数分别重载实现了增加楼层、增加房间和增加床位;enrol、quit、move分别实现入学、退学、搬宿舍。
    Init_value.h中的命名空间变量是用于保存初始化配置信息,并且可供各个文件使用。
    4.2 公有函数实现Initial.h包含初始化的功能函数:initv,读入配置信息初始为Init_value中的配置信息赋值;inits,读入宿舍的初始人员信息构建宿舍,通过调用Dorm的enrol函数即可实现入学。
    love.h的court和breakup分别实现求爱和分手功能,因为恋爱功能是两个对象,因此作为全局函数更为合适;通过公式计算出是否成功恋爱或者分手,从而对两个对象属性分别进行修改。
    read.h则从instructions.txt文件读取指令,并且通过split和比较选择将读入的数据翻译为指令并且调用对应的指令函数,需要对指令的格式进行判断并且显示出异常的指令。
    query.h包含了各种关于查询的显示和调用函数,首先对各个界面的内容进行显示,然后通过输入的内容对查询的内容进行具体选择查询,然后分别访问Student类或者Dorm类的成员变量对其具体信息进行查询。
    五 关键代码实现Student类虚函数:
    public: virtual bool eat(int){return true;} virtual bool study(int){return true;} virtual bool ownact(int){return true;} virtual int person_value(){return 0;}
    实现学生活动:
    ownact(int m){ cost_add(0, m); charm_add(0, looks_add(m), 0);}//根据固定公式计算得出增量cost_add(int f, intm){}
    Dorm类:
    class Dorm{public: vector<FLOOR_NUM> floors; map<string,Student*> students; };
    实现宿舍活动:
    //入学enrol(){ if (s) p = new Girl(); else p = new Boy(); floorsfl.member.push_back(n); students[n] = p;}//搬宿舍move(){ tmp.erase(iter); floorsflfrom.member.push_back(n); students[n]->floor_number = flto; students[n]->room_number = rmto;}//退学quit(){ tmp.erase(it); delete iter->second; students.erase(iter); }//增加fls层楼add(int fls){}//对fl层增加rms间房add(int fl, intrms){}//对fl层、rm房增加beds张床add(int fl, int rm,int beds){}
    实现追求功能(分手相类似):
    court(Student *a,Student *b){ if (fall_love(a,b)) { a->lover.push_back(b->name); b->lover.push_back(a->name); }}bool fall_love(Student*a, Student *b){}
    体重、金钱、魅力值查询:
    sort(vec.begin(),vec.end(),cmp1); sort(g_vec.begin(),g_vec.end(),cmp1); sort(b_vec.begin(),b_vec.end(),cmp1); qw_output(vec); // 若范围是全体学生则输入vec qw_output(b_vec); // 若范围是全体男生则输入b_vec qw_output(g_vec); // 若范围是全体女生则输入g_vec
    查询容貌、健康信息:
    query_person(){ sort(vec.begin(),vec.end(),cmp4); cout << (*iter)->person_value();}
    查询学生具体信息:
    query_student(){ i = d.students.find(str);}
    1 评论 11 下载 2018-10-15 22:37:56 下载需要7点积分
  • 基于C#语言的计算器的设计与实现

    一 需求分析利用c#语言实现一个计算器程序,包括普通计算器模式、科学计算器模式;实现实数(包括正数、负数、0)的加、减、乘、除、平方等基本远算,并实现非负数的开方运算。
    二 程序设计与实现首先设计主窗体,包括两个TextBox用来显示数据,使用数据绑定实现,在Model中实现INotifyPropertyChanged接口,以起到数据更改通知的功能。一个Button和Panel作为菜单来切换计算器模式。最后一个Panel用来显示不同模式的计算器输入面板;
    其次是标准计算器输入面板。包括常用的数字和运算符以及Delete,CE和清除按钮。科学计算模式也只是多了几个运算符而已。在后置代码中获取被单击的按钮的名称,调用Model中的Print方法在TextBox中显示数据。
    Model中声明两个字符串变量作为两个TextBox中的数据,还包括Print方法,一方面得到按钮的名称,另一方面调用此程序中最重要的逻辑处理方法PrintText方法。传进去两个值,即当前TextBox中显示的数据,输出两个值作为TextBox在一系列处理之后应显示的数据。
    PrintAndExpression类主要负责处理TextBox的显示和生成算术表达式。GetValue方法根据名字返回应显示的字符串。IsOpreation方法判断字符串是数字,一元操作符还是二元操作符及其他符号。PrintText处理显示格式。大致为当输入数字的时候在textBox1中,每按下一个操作符即将textBox1中的数字连接上操作符显示在textBox2中,同时处理简单的一元运算符的运算,生成表达式。最终将表达式传递给AnalyExpressions。将解析表达式运算之后的结果返回。
    AnalyExpressions解析表达式,利用堆栈来实现,将中缀表达式分解之后保存在字符串数组中,即所谓的“后缀表达式”。大致规则为:

    将中缀表达式翻译成后缀表达式
    输入中缀表达式: A+B*(C+D)-E/F
    中缀表达式翻译成后缀表达式的方法如下:

    从左向右依次取得数据ch
    如果ch是操作数,直接输出
    如果ch是运算符(含左右括号),则:

    如果ch = “(“,放入堆栈
    如果ch = “)”,依次输出堆栈中的运算符, 直到遇到”(“为止
    如果ch不是”)”或者”(“,那么就和堆栈顶点位置的运算符top做优先级比较

    如果ch优先级比top高,那么 将ch放入堆栈
    如果ch优先级低于或者等于 top,那么输出top,然后将ch放入堆栈



    如果表达式已经读取完成,而堆栈中还有运算符时,依次由顶端输出


    三 程序目录结构程序目录结构如下图所示:

    Data文件夹

    Model.cs为程序执行所操作的数据
    Method文件夹

    PrintAndExpression.cs为事件处理程序所调用的方法,主要用于处理显示数据AnalyExpression.cs用于将算术表达式解析为“后缀表。达式数组”并计算返回计算结果
    View文件

    NormCalculator.cs为用户控件显示标准计算器视图ScienceCalculator.cs用户控件显示科学计算器视图
    MainForm.cs为程序主窗体
    Program.cs为程序入口
    四 测试运行如下图所示:



    1 评论 160 下载 2018-10-15 10:30:58 下载需要8点积分
  • 基于贪心策略直接搜索算法和极大极小博弈树算法的智能人机博弈五子棋游戏

    一、问题分析五子棋是双人博弈棋类益智游戏,由围棋演变而来,属纯策略型。棋盘通常15*15,即15行,15列,共225个交叉点,即棋子落点;棋子由黑白两色组成,黑棋123颗,白棋122颗。游戏规则为黑先白后,谁先五子连成一条直线谁赢,其中直线可以是横的、纵的、45度、135度。
    本次Java编程我的目的是现实人机对战,即游戏者一方是人,另一方计算机。这就要求程序不仅要具备五子棋的基本界面,还要编程指导计算机与人进行对弈。为了使程序尽可能智能,我采用了贪心策略、传统搜索算法、极大极小博弈树算法,对应游戏玩家的3个等级:简单、中等、困难。
    二、功能设计我的程序基本功能是实现人机对弈五子棋。人和电脑交替下棋,谁先五子连成一条直线谁就赢。下面是我程序的功能模块:

    等级设置核心功能是实现不同策略与算法的对比运用,纯贪心策略实现简单等级对手,直接搜索算法实现中等等级对手,极大极小博弈树算法实现困难等级对手。对应程序中的3选1单选按钮。
    悔棋功能模拟栈机制实现人悔棋,不限步长的悔棋。对应程序中的悔棋按钮。
    棋面绘制根据不同机计算机的屏幕分辨率,绘制逼真的棋盘。
    图片引入两张古典的人物图片,生动模拟对弈双方。人物图片旁的黑白棋钵图片显示黑白棋归属。
    背景设置支持用户选择背景,包括棋盘、棋盘边框、窗口边框,彰显个性。
    音乐播放下棋时有棋子落地的声音,一方胜利时有五子连成一片的声音。同时在设置背景时相应的改变整个对弈过程中的背景音乐。
    时间显示在棋盘正上方有一模拟文本框显示当前棋局用时。
    其他小功能支持和棋、认输、开启新游戏、退出游戏等操作。

    三、数据结构与算法设计3.1 数据结构部分3.1.1 当前棋局的存储结构我的五子棋程序选择通常用到的15*15棋盘,可以开二维数组PositionFlag = new int[15][15],PositionFlag[i][j]为0表示(i, j)点尚无棋,为1表示(i, j)点是人的棋子,为2表示(i, j)点是机器的棋子。之所以选择二维数组,主要原因有两点:

    本程序需要频繁随机访问15*15的交叉点,对应查询该点状态以及改变该点状态,随机访问是数组的特点。
    15*15=225开二维数组的内存需求相对现在内存为2G及以上的计算机完全可以接受,且数组实现简单、操作方便。

    基于以上两点,尽管创建动态的顺序表—链表可能可以节省少量内存(可以只存当前有棋的点,原数组对应位置为0的点可以不存),但选择数组的优势完全在上述两点体现了出来。
    3.1.2 实现悔棋操作的数据结构由于每次悔棋只需回退当前几步,后进先出原则,这正是栈这种典型数据结构的设计思想,于是我选择栈。我自己先写了用自定义数组模拟的栈,但由于是学Java语言且由于悔棋的存储空间需要随当前步数增大而增大(由于每局最多下225步,即最多要悔225步,所以自己开个225的数组完全可以避免存储空间自增长的问题且内存完全可以接受,之所以不用自定义数组而用ArrayList类主要是为了尝试Java中STL的用法),所有我最终改为用Java类库中的ArrayList类。
    确定用ArrayList类实现栈机制后就必须考虑每个ArrayList单元具体存储什么。刚开始我存储的是当前的棋局,即整个局面,而每个局面对应一个二维数组,这样是很占用内存的。试想一下,在最坏情况下,225个ArrayList单元,每个单元存放一个15*15的二维数组,尽管225*15*15在Java的内存管理机制下不会爆栈,但也是极不划算的。之所以说不划算,是因为有更好的解决方案。由于每次悔棋只是在回退倒数一步,多步悔棋只需循环回退,所以可以只存储当前棋局最后一步的下法,对应一个二维点,完全可以自定义一个二维坐标类chessOneStep。
    3.2 算法设计部分Java语言是面向对象的语言。我在进行五子棋游戏编程是总共传创建了11个自定义的类。在编写程序的过程中,我有一个明显的体验就是面向对象编程就是一项有关对象设计和对象接口技术,很多关键的技术就是如何设计自定义的对象。
    下面我先概括给出我的所有类的作用:

    mainFrame类:主框架类,我应用程序的入口
    chessPositon类:主控类,这个类是我程序的核心类,负责控制双方的下棋,以及调用其他的类完成当前棋局的显示绘制
    chessPanel类:面板类,调用其他底层类完成当前棋局的显示绘制
    chessBoard类:棋盘绘制类,负责棋盘的绘制
    chessImage类:文件类,包含各种资源(背景图片、背景音乐)以及静态全局变量(public static Type)
    chessButton类:组件类,定义各种组件,包括按钮、单选按钮、文本框等
    chessMusic类:音乐类,负责调用Java类库完成背景音乐、下棋音乐、取胜音乐等的播放
    chessPiece类:棋局类,定义棋局二维数组数据结构并完成相关操作
    chessList类:栈类,完成悔棋等操作
    chessOneStep类:棋子类,定义每步坐标以及下在该处获得的估价值
    myCompare类:排序类,完成chessOneStep类的自定义排序

    五子棋程序类调用关系图如下所示:

    四、详细设计4.1 mainFrame类作为我的五子棋程序的主类,mainFrame类主要实例化相关的对象,如chessbutton,chessborad等,从而完成框架的创建。更重要的是实例化chessposition,这是本程序的核心类,控制游戏双方行棋过程完成人机互动下棋,然后将MyChessPosition与鼠标响应addMouseListener()关联起来。
    4.2 chessMusic类一个好的游戏必须给人一种身临其境的感觉,而声音是营造这种氛围的重要因素。参照网上各游戏运行商的音乐配置,我选择相关逼真的声音。包括背景音乐、下棋棋子落到棋盘发出的声音以及一方胜出的配乐。所有这些功能的实现,依赖于自定义的chessMusic类,采用AudioInputStream配合Clip的方式完成音乐播放的软硬件工作,然后定义两个接口chessmusic(String Name)和Stop(),前者完成播放功能,后者完成关闭当前音乐功能。因为音频文件相对较大,而我的程序提供在不同背景乐之间切换的功能,所以在打开另一个音频文件之前必须关闭前一个正在播放的音频文件,防止出现溢出。
    4.3 chessImage类适当的动画或图片能给游戏玩家带来美的体验。所以我的五子棋程序界面在不失和谐的前提下引入了尽可能多的图片,包括对弈双方、棋钵等。图片引入的具体工作通过语句import javax.imageio.ImageIO完成。同时,由于图片要在用到它的类中被访问,为了避免频繁调用函数,我直接将图片相关联的对象定义为public static,表明是公用的、静态的。进一步引申开去,我将程序中用到的静态全局变量都定义在chessImage类中。具体如下:
    public static Date begin;//每局开始时间 public static Date cur;//每局结束时间 public static chessOneStep LineLeft;//结束端点1 public static chessOneStep LineRight;//结束端点2 public static boolean IsGameOver;//是否只有一方获胜 public static int ColorOfBackGround[][]= {{255, 227, 132},{0,255,127},{218,165,32}};//背景颜色 public static int ColorOfWindows[][]= {{ 60,179,113},{245,245,245},{122,122,122}};//背景颜色 public static int WitchMatch;//背景搭配 public static String MusicOfBackGround;//背景音乐 public static int CurrentStep;//记录当前步数 public static int Rank;//设置难度等级 public static boolean IsSurrender;//判断是否认输 public static boolean IsTie;//判断是否认输 public static String Message;//输出提示信息 public static Image IconImage;// 图标 public static Image blackBoard;//白棋盘 public static Image whiteBoard;//黑棋盘 public static Image blackChess;// 白棋棋子图片 public static Image whiteChess;// 白棋棋子图片 public static Image RightPlayer;//白棋棋罐图片 public static Image LeftPlayer;//白棋玩家头像图片 public static String path = "src/";// 图片的保存路径
    4.4 chessButton类这个是程序的组件类。定义了各种功能键,完善程序功能,营造逼真的人机对战游戏效果。分为3类:
    4.4.1 按钮组件本程序有5个按钮,支持和棋、认输、新游戏、退出、悔棋等。认输和和棋按钮终止当前的棋局,给出相应的提示信息;退出按钮调用系统System.exit(0)的函数正常返回;悔棋按钮调用后面要介绍的chessList类实现悔棋;新游戏按钮则刷新当前棋局准备下一轮,要将记录当前棋局的二维数组全部置0,刷新当前棋局开始时间等。
    4.4.2 单选按钮组件游戏界面支持设置个性化界面,包括背景颜色与背景音乐,跟重要的一点是设置难度(简单、中等、困难)。单选按钮只能多选一。背景颜色主要是存储相关颜色搭配方案的RGB颜色,开2维数组,即对应RGB3原色数组的一维数组,然后通过改变WitchMatch全局变量的值来有用户自己选择颜色搭配,不同的颜色搭配对应不同的背景音乐表达一致的主题。难度设置主要是改变计算机的下棋算法,不同难度通过Rank判断进入不同的程序分支,实现不同智能等级的计算机下棋水平。
    4.4.3 文本框在不同的单选按钮前添加相应的文本框,提示用户可以实现的功能。同时我用颜色模拟出显示当前棋局耗用时间的文本框。
    不论按钮还是单选按钮都要关联相应的消息,把相应功能的实现放在消息响应处理函数理。这些主要是实现Java库提供的消息响应接口里的方法。
    4.5 chessPiece类主要完成当前棋面的存储,存储棋面的数据结构为二维数组int [][]PositionFlag;然后定义获取、设置某点以及整个棋面的状态的方法。
    SetPositionFlag(intx, int y, int flag) //设置(x,y)处的状态为flagGetPositionFlag(intx, int y) //获取(x,y)处的状态SetAllFlag(intNewFlag) //设置当前整个棋面的状态为NewFlagGetAllFlag() //获取当前整个棋面的状态DrawChessPiece(Graphicsg) //绘制当前局面的棋子
    4.6 chessBoard类功能为绘制棋盘线。由于围棋的棋盘比较复杂,横线、竖线较多,且为了使棋盘美观,还要自定义窗口边框、棋盘边框、对弈双方边框等,对线宽、线型也有一定要求。有时要单像素线条,有时要多像素线条。对于多像素线条,我主要用了2种方法。
    4.6.1 方法一在需要绘制多像素线条处首先绘制一条单像素线,然后根据线宽要求上下平移适当像素达到绘制多像素的目的。这样的方法适合绘制水平线或竖直线,绘制其他斜率的线条容易造成走样。在没有想到比较好的反走样编程思想后我选择了调用Java库中已经封装好的函数。
    4.6.2 方法二为了克服方法一绘制非水平或竖直线时造成的走样,同时也为了更进一步学习Java语言,我猜想肯定会有类似OpenGL中设置线宽的画刷,于是上网百度找到了相应的画刷Stroke类。通过Java库实现绘制不同线宽的直线,达到了反走样效果。
    4.7 chessOneStep类这个类是为了配合chessList类实现悔棋以及在计算机下棋算法实现返回有效状态点而设计的。主要数据成员为
    private int x, y, weight; //其中x,y表示点坐标,weight表示将棋下到该点获得的估价值
    主要方法如下:
    GetX() //获得当前对象的x坐标GetY() //获得当前对象的y坐标GetWeight() //获得当前对象的(x,y)处的估价值
    4.8 chessList类程序支持悔棋功能,为了实现悔棋,自定义了chessList类。这个类主要通过引入java.util.ArrayList和java.util.List实现集合的数据类型。然后自定义一些方法,如下:
    AddStep(chessOneStep OneStep) //添加一步棋到List中GetSize() //获得当前List的大小ClearList() //清空ListRemoveLast() //删去List中的最后元素
    由于每次删除当前List中的最后一个元素,实现后进先出,所以可以模拟栈的功能实现悔棋。
    4.9 myCompare类由于在计算机下棋的极大极小博弈树算法中需要对自定义对象chessOneStep按weight进行排序,所以引入了myCompare类,通过实现Comparator接口中的compare方法完成自定义对象排序。
    4.10 chessPanel类程序的自定义面板类,主要负责完成当前框架内容的显示。这是一个重要的与框架和图形显示密切相关的类。主要数据成员为
    private chessboard MyChessBoard; //当前显示棋盘private chesspiece MyChessPiece; //当前显示整个棋面的状态
    主要方法如下:
    chesspanel(chessboard MyChessBoard1, chesspiece MyChessPiece1)//构造函数,分别用MyChessBoard1和MyChessPiece1初始化MyChessBoard和MyChessPiecedisplay(chessboard MyChessBoard1, chesspieceMyChessPiece1)//自定义显示回调函数,调用repaint()完成重新绘制游戏界面paintComponent(Graphics g)//核心方法,调用各种函数完成具体的绘制工作
    4.11 chessPositon类程序算法核心类,总的功能是控制人和计算机轮流下棋,以及调用chessPanel类中的display(chessboard, chesspiece )方法完成界面的实时刷新。关于chessPositon类,我在此将重点介绍。chessPosition类的主要数据成员如下:
    privatestatic chessboard MyChessBoard; //当前显示棋盘publicstatic chesspiece MyChessPiece; //当前显示整个棋面的状态privatestatic chesspanel Mychesspanel; //当前显示面板publicstatic chesslist MyChessList=new chesslist();//当前下棋集合,用于悔棋finalprivate static int INF = (1 << 30);// 表示正无穷大的常量,用于极大极小博弈数搜索算法publicstatic boolean CanGo; //控制当前下棋一方
    类的设计集中体现在成员方法的设计上。实现人机对战,只有语言是远远不够的,还要加入算法,用算法引导计算机下棋。下面介绍该类的方法成员:
    chessposition(chesspanel , chessboard ,chesspiece ) //带有参数的构造函数chessposition() // 不带参数的构造函数mouseClicked(MouseEvent event)
    鼠标响应函数,负责人的下棋,根据鼠标点击的位置转换得到所在棋盘的相对位置。如果该位置不合法,即超出棋盘有效范围,点击无响应;如果该位置上已有棋,弹出消息框给出提示。这二者都要求重新给出下棋位置,即当前鼠标响应无效…直到点击到棋盘有效区域。
    IsOver(int Array,int x,int y)
    判断当前int[][]Array对应的棋局是否结束,即一方五子连成一条直线。此处有两种思路,一种对当前棋面上的所有棋子都进行一次判断,具体为水平方向、竖直方向、与水平线成45度方向、与水平线成135度方向,只要有一个方向五子连成一条直线就说明有一方获胜,游戏结束;另一种思路为只在当前下棋的4个方向进行判断,我的程序采用的是第二种,所以IsOver方法除了int[][]Array参数外,还有x, y参数,(x, y)表示当前下棋的坐标点。
    display()
    通过调用自定义面板类的显示回调函数用于重新显示游戏界面,达到每下一步棋及时更新游戏界面的目的。
    GetValue(int flag, int num)
    估值函数,根据经验把棋局分成只有1颗棋相连,2颗棋相连且两端被封死,2颗棋相连且一端封死另一端活的,2颗棋相连且两端都是活的,同理3颗棋、4颗棋也各自可分3种情况。不同的情况对应不同的估价值。估价值的设定是决定计算机一方是否智能的一个关键因素。
    GetPredictValue(int flag, int num)
    对未连成一片但通过再下一颗子就能连成一片的局面进行估值,这在双方下棋的有限步骤内是能产生重要影响的。如果每局棋仅考虑当前一步,是不可取的。
    Evaluate(int Array, int x, int y)
    根据棋面具体情况以及预先设定的估值函数,对某个点对应的局面进行评估。由于每次双方只能下一颗棋,所以可以每次取当前局面的所有点中对应估值最大值点的估值作为整个局面的估值。
    计算机下棋方法1
    GetGreedNext()
    对应难度等级为简单,采用贪心思想。每次下棋前在求得最有利点下棋,而是否最有利只是通过一步评估。算法伪码描述为(Max取负无穷大):
    for(行i从0到15) { For(列j从0到15) { If((i,j)对应的位置无棋) { a.假设放上一颗由人控制的棋,求估价值; b.假设放上一颗由计算机控制的棋,求估价值; c.取二者中较大值作为(i,j)处的估价值tmp; d.取tmp与Max较大值赋值给Max. } } }
    最终Max对应的点就是当前整个局面中最大的估值点。至于上述为什么要考虑双方都在该点下棋的情况呢?主要原因为下五子棋是个攻防兼备的过程,不仅要考虑自己对自己最有利,还要考虑对对手最不利,通俗来讲就是在自己赢的时候不能让对手先赢。
    计算机下棋方法2
    GetSearchNext(int LookLength)derectSearch(int [][]Array,boolean who,int deepth)
    直接搜索法,对应难度等级为中等。每步棋最多有225个不同下法,若采用直接搜索法则对应的孩子节点有225个(在下棋过程中会逐渐减少),即每层有最多225个节点待扩展,这就决定了直接搜索进行不超过2次,主要原因有两点:

    采用深度优先搜索需要递归,递归中状态过多可能会爆栈,我们知道递归是用栈机制来实现的;采用宽度优先搜索又需要存储为扩展的节点,这对内存容量要求很高。不管深搜还是广搜,在时间复杂度为O(N^m)的情况下都是不能接受的。其中N为当前棋局的待扩展节点,最大225;m为搜索的深度。
    综上所述,在采用直接搜索法时搜索深度不能太深,严格来说是应该控制在2层以内,在计算机运算速度在10^7次每秒的情况下,理论和实验都表明超过2层就会变得很慢且这种趋势成指数级增长。直接搜索算法伪代码为:
    GetSearch(boolean flag,int deep) { 如果deep等于0,返回当前棋局估值; for(行i从0到15) { For(列j从0到15) { If((i,j)对应的位置无棋) { 如果轮到计算机下棋,置标志位为2 GetSearch(!flag,deep-1); 如果轮到人下棋,置标志位为1; GetSearch(!flag,deep-1); } } } }
    计算机下棋方法3
    GetMinMaxsearchNext(int LookLength)MinMaxsearch(int [][]Array,boolean who, int deepth)
    极大极小博弈树法,对应难度等级为困难。五子棋是个博弈游戏,当前在寻找对自己最有利的下棋点时要尽可能保证对对手最不利,这种思想可以用极大极小博弈树完美阐释。关于极大极小博弈树在这里就不多说了。就算用极大极小博弈树,关于每次扩展节点与扩展深度的问题也是无法回避的。每次扩展节点不能太多,扩展深度不能太深,但若简单限制二者,就会使计算机游戏方表示出的智能水平有限。在每次扩展节点时可以采用贪心策略,首先对当前所有可扩展节点进行一次估值,选择其中估价值最大的节点进行扩展,这是一个很好的优化。好的开始很可能产生好的结果,良性循环。本题我是选择最好的5个节点进行扩展,扩展深度为4层,因为五子连成一条直线就可以取胜,故选择4层是合理的。
    下面给出极大极小博弈树算法的伪代码
    MinMaxsearch(int [][]Array,boolean who, int deepth) { 如果到达了搜索深度,返回当前棋局估值; for(行i从0到15) { For(列j从0到15) { If((i,j)对应的位置无棋) { 对当前棋局进行估值,结果存在List表中; } } } 取其中最大的5个节点进行MinMaxsearch()估值。 }
    由于取到对方最大的可以干扰对方,即在对方最有利的坐标点下上自己的棋可以“损人利己”,何乐不为?每次都取最大的这与极大极小博弈树算法有些不太一样,但对于本题是有效的。
    五、源程序清单由于整个程序比较长,在这里只贴出我认为较为重要的代码。

    源程序1 每局棋数据结构源程序2 控制人机交互下棋源程序3 完成面板的绘制
    5.1 源程序1 每局棋数据结构import java.awt.Graphics;public class chesspiece { private int[][] PositionFlag = new int[15][15];// 表示格子上的棋子类型,0表示黑子,1表示白字 public void SetPositionFlag(int x, int y, int flag) { PositionFlag[x][y] = flag; } public void SetAllFlag(int[][] NewFlag) { PositionFlag = NewFlag; } public int GetPositionFlag(int x, int y) { return PositionFlag[x][y]; } public int[][] GetAllFlag() { return PositionFlag; } public void DrawChessPiece(Graphics g) {// 画棋子 for (int i = 0; i < 15; i++) {// 扫描棋盘中所有的棋子 for (int j = 0; j < 15; j++) { int x = (int) (chessboard.Left) + i * (int) (chessboard.Inc) - 15;// 把棋子在棋盘中对应的下标转化成在游戏中的坐标 int y = 25 + j * (int) (chessboard.Inc) - 15; if (GetPositionFlag(i, j) == 1) {// 如果指定位置的棋子是黑色棋子 g.drawImage(chessimage.whiteChess, x, y, null); } else if (GetPositionFlag(i, j) == 2) {// 如果指定位置的棋子是白色棋子, g.drawImage(chessimage.blackChess, x, y, null); } } } }}
    5.2 源程序2 控制人机交互下棋import java.awt.event.MouseAdapter;import java.awt.event.MouseEvent;import java.util.ArrayList;import java.util.Collections;import javax.swing.JOptionPane;public class chessposition extends MouseAdapter { private static chessboard MyChessBoard; public static chesspiece MyChessPiece; private static chesspanel Mychesspanel; public static chesslist MyChessList = new chesslist(); final private static int INF = (1 << 30); // 表示正无穷大的常量 public static boolean CanGo; public chessposition(chesspanel Mychesspanel1, chessboard MyChessBoard1, chesspiece MyChessPiece1) { chessposition.Mychesspanel = Mychesspanel1; chessposition.MyChessBoard = MyChessBoard1; chessposition.MyChessPiece = MyChessPiece1; CanGo = true; } public chessposition() { } public void mouseClicked(MouseEvent event) { if (!CanGo) return; // 获取鼠标点击的棋盘相对位置 int x = event.getX() - (int) (chessboard.Left); int y = event.getY() - 25; int Max = (int) (chessboard.Inc * 14) + 39;// 棋盘最右、下边 int Min = 0;// 棋盘最左、上边 if ((x < Min) || (x > Max) || (y < Min) || (y > Max))// 无效棋子 return; int Inc = (int) chessboard.Inc; int NextX = x / Inc; int NextY = y / Inc; new chessmusic("下棋.wav"); if (0 != MyChessPiece.GetPositionFlag(NextX, NextY)) { JOptionPane.showMessageDialog(JOptionPane.getRootFrame(), "您的走法违规,该处已有棋子!", "温馨提示", JOptionPane.ERROR_MESSAGE); return; } chessOneStep OneStep = new chessOneStep(NextX, NextY, 0); MyChessList.AddStep(OneStep);// 保留当前下得棋 MyChessPiece.SetPositionFlag(NextX, NextY, 1); chessimage.CurrentStep++; if (chessimage.CurrentStep == 225) { chessimage.Message = "伯仲之间 ,胜负难分!"; CanGo = false; } if (IsOver(MyChessPiece.GetAllFlag(), NextX, NextY)) new chessmusic("取胜.wav"); display(); if (0 == chessimage.Rank) GetGreedNext(); else if (1 == chessimage.Rank) GetSearchNext(1); else if (2 == chessimage.Rank) GetMinMaxsearchNext(3); } // 计算机采用一步攻防贪心策略下棋 public void GetGreedNext() { int NextX, NextY; if (!CanGo) return; // 完全裸下 int MaxWei = -INF; int idX = -1; int idY = -1; for (int i = 0; i < 15; i++) { for (int j = 0; j < 15; j++) { if (0 == MyChessPiece.GetPositionFlag(i, j)) { MyChessPiece.SetPositionFlag(i, j, 2); int tmp = Evaluate(MyChessPiece.GetAllFlag(), i, j); if (tmp >= MaxWei) { MaxWei = tmp; idX = i; idY = j; } MyChessPiece.SetPositionFlag(i, j, 1); tmp = Evaluate(MyChessPiece.GetAllFlag(), i, j); if (tmp > MaxWei) { MaxWei = tmp; idX = i; idY = j; } MyChessPiece.SetPositionFlag(i, j, 0); } } } NextX = idX; NextY = idY; new chessmusic("下棋.wav"); if (-1 == NextX && -1 == NextY) { if (chessimage.CurrentStep == 225) { chessimage.Message = "伯仲之间 ,胜负难分!"; CanGo = false; } return; } chessimage.CurrentStep++; chessOneStep OneStep = new chessOneStep(NextX, NextY, 0); MyChessList.AddStep(OneStep);// 保留当前下得棋 MyChessPiece.SetPositionFlag(NextX, NextY, 2); if (IsOver(MyChessPiece.GetAllFlag(), NextX, NextY)) new chessmusic("取胜.wav"); display(); } public void display() {// 用于重新显示游戏界面 Mychesspanel.display(MyChessBoard, MyChessPiece); } // 添加棋子后只需判断水平、竖直、成45、135度角上是否连成5个 public boolean IsOver(int[][] Array, int x, int y) { boolean flag = false; int num = 1; int k = 1; while (x - k >= 0 && Array[x][y] == Array[x - k][y]) { num++; k++; } chessimage.LineLeft = new chessOneStep(x - k + 1, y, 0); k = 1; while (x + k < 15 && Array[x][y] == Array[x + k][y]) { num++; k++; } chessimage.LineRight = new chessOneStep(x + k - 1, y, 0); if (num >= 5) flag = true; if (!flag) { num = 1; k = 1; while (y - k >= 0 && Array[x][y] == Array[x][y - k]) { num++; k++; } chessimage.LineLeft = new chessOneStep(x, y - k + 1, 0); k = 1; while (y + k < 15 && Array[x][y] == Array[x][y + k]) { num++; k++; } chessimage.LineRight = new chessOneStep(x, y + k - 1, 0); if (num >= 5) flag = true; } if (!flag) { num = 1; k = 1; while (y - k >= 0 && x - k >= 0 && Array[x][y] == Array[x - k][y - k]) { num++; k++; } chessimage.LineLeft = new chessOneStep(x - k + 1, y - k + 1, 0); k = 1; while (y + k < 15 && x + k < 15 && Array[x][y] == Array[x + k][y + k]) { num++; k++; } chessimage.LineRight = new chessOneStep(x + k - 1, y + k - 1, 0); if (num >= 5) flag = true; } if (!flag) { num = 1; k = 1; while (y + k < 15 && x - k >= 0 && Array[x][y] == Array[x - k][y + k]) { num++; k++; } chessimage.LineLeft = new chessOneStep(x - k + 1, y + k - 1, 0); k = 1; while (y - k >= 0 && x + k < 15 && Array[x][y] == Array[x + k][y - k]) { num++; k++; } chessimage.LineRight = new chessOneStep(x + k - 1, y - k + 1, 0); if (num >= 5) flag = true; } if (flag) { chessimage.IsGameOver = true; if (1 == Array[x][y]) chessimage.Message = "获胜了,恭喜您!"; else chessimage.Message = "失败了,振作点!"; CanGo = false; } return flag; } public int GetMax(int a, int b) { return a < b ? b : a; } // 预先设定一些规则估值,对已连成一片的 public int GetValue(int flag, int num) { int ret = 0; if (1 == num) ret = 0; if (2 == num) { if (0 == flag)// 死2 ret = 3; else if (1 == flag)// 单活2 ret = 50; else ret = 100;// 双活2 } else if (3 == num) { if (0 == flag)// 死3 ret = 5; else if (1 == flag)// 单活3 ret = 200; else ret = 5000;// 双活3 } else if (4 == num) { if (0 == flag)// 死4 ret = 10; else if (1 == flag)// 单活4 ret = 8000; else ret = 500000; } else if (5 == num) { ret = 10000000; } return ret; } // 对未连成一片但通过再下一颗子就能连成一片的局面进行估值 public int GetPredictValue(int flag, int num) { int ret = 0; if (0 == flag || num <= 2) ret = 0; else { if (1 == flag) { if (3 == num) ret = 10; else if (4 == num) ret = 50; else ret = 200; } else { if (3 == num) ret = 100; else if (4 == num) ret = 5000; else ret = 8000; } } return ret; } // 以下棋点为中心,查看总得分,此评判方法为贪心法 public int Evaluate(int[][] Array, int x, int y) { int ret = 0; int num, k, tag; boolean lflag, rflag; // 先估值一连成一片的 // 水平线 k = 1; num = 1; lflag = true; rflag = true; while (x - k >= 0 && Array[x][y] == Array[x - k][y]) { num++; k++; } if (!(x - k >= 0 && 0 == Array[x - k][y])) lflag = false; k = 1; while (x + k < 15 && Array[x][y] == Array[x + k][y]) { num++; k++; } if (!(x + k < 15 && 0 == Array[x + k][y])) rflag = false; num = (num < 5 ? num : 5); if (lflag && rflag) { tag = 2; } else { if (lflag || rflag) tag = 1; else tag = 0; } ret += GetValue(tag, num); // 竖直线 k = 1; num = 1; lflag = true; rflag = true; while (y - k >= 0 && Array[x][y] == Array[x][y - k]) { num++; k++; } if (!(y - k >= 0 && 0 == Array[x][y - k])) lflag = false; k = 1; while (y + k < 15 && Array[x][y] == Array[x][y + k]) { num++; k++; } if (!(y + k < 15 && 0 == Array[x][y + k])) rflag = false; num = (num < 5 ? num : 5); if (lflag && rflag) { tag = 2; } else { if (lflag || rflag) tag = 1; else tag = 0; } ret += GetValue(tag, num); // 135度 k = 1; num = 1; lflag = true; rflag = true; while (y - k >= 0 && x - k >= 0 && Array[x][y] == Array[x - k][y - k]) { num++; k++; } if (!(y - k >= 0 && x - k >= 0 && 0 == Array[x - k][y - k])) lflag = false; k = 1; while (y + k < 15 && x + k < 15 && Array[x][y] == Array[x + k][y + k]) { num++; k++; } if (!(y + k < 15 && x + k < 15 && 0 == Array[x + k][y + k])) rflag = false; num = (num < 5 ? num : 5); if (lflag && rflag) { tag = 2; } else { if (lflag || rflag) tag = 1; else tag = 0; } ret += GetValue(tag, num); // 45度 k = 1; num = 1; lflag = true; rflag = true; while (y + k < 15 && x - k >= 0 && Array[x][y] == Array[x - k][y + k]) { num++; k++; } if (!(y + k < 15 && x - k >= 0 && 0 == Array[x - k][y + k])) lflag = false; k = 1; while (y - k >= 0 && x + k < 15 && Array[x][y] == Array[x + k][y - k]) { num++; k++; } if (!(y - k >= 0 && x + k < 15 && 0 == Array[x + k][y - k])) rflag = false; num = (num < 5 ? num : 5); if (lflag && rflag) { tag = 2; } else { if (lflag || rflag) tag = 1; else tag = 0; } ret += GetValue(tag, num); // 能成连成一片的 // 水平线 int add; int leftadd, rightadd; boolean leftflag, rightflag; int lvalue, rvalue; k = 1; num = 1; lflag = true; rflag = true; leftflag = true; rightflag = true; leftadd = 0; rightadd = 0; while (x - k >= 0 && Array[x][y] == Array[x - k][y]) { num++; k++; } if (!(x - k >= 0 && 0 == Array[x - k][y])) lflag = false; else { add = k + 1;// 跳过空格 while (x - add >= 0 && Array[x][y] == Array[x - add][y]) { leftadd++; add++; } if (!(x - add >= 0 && 0 == Array[x - add][y]))// 堵死了 leftflag = false; } k = 1; while (x + k < 15 && Array[x][y] == Array[x + k][y]) { num++; k++; } if (!(x + k < 15 && 0 == Array[x + k][y])) rflag = false; else { add = k + 1;// 跳过空格 while (x + add < 15 && Array[x][y] == Array[x + add][y]) { rightadd++; add++; } if (!(x + add < 15 && 0 == Array[x + add][y]))// 堵死了 rightflag = false; } if (leftflag && rflag) { tag = 2; } else { if (leftflag || rflag) tag = 1; else tag = 0; } lvalue = GetPredictValue(tag, num + 1 + leftadd); if (lflag && rightflag) { tag = 2; } else { if (lflag || rightflag) tag = 1; else tag = 0; } rvalue = GetPredictValue(tag, num + 1 + rightadd); ret += GetMax(lvalue, rvalue); // 竖直线 k = 1; num = 1; lflag = true; rflag = true; leftflag = true; rightflag = true; leftadd = 0; rightadd = 0; while (y - k >= 0 && Array[x][y] == Array[x][y - k]) { num++; k++; } if (!(y - k >= 0 && 0 == Array[x][y - k])) lflag = false; else { add = k + 1;// 跳过空格 while (y - add >= 0 && Array[x][y] == Array[x][y - add]) { leftadd++; add++; } if (!(y - add >= 0 && 0 == Array[x][y - add]))// 堵死了 leftflag = false; } k = 1; while (y + k < 15 && Array[x][y] == Array[x][y + k]) { num++; k++; } if (!(y + k < 15 && 0 == Array[x][y + k])) rflag = false; else { add = k + 1;// 跳过空格 while (y + add < 15 && Array[x][y] == Array[x][y + add]) { rightadd++; add++; } if (!(y + add < 15 && 0 == Array[x][y + add]))// 堵死了 rightflag = false; } if (leftflag && rflag) { tag = 2; } else { if (leftflag || rflag) tag = 1; else tag = 0; } lvalue = GetPredictValue(tag, num + 1 + leftadd); if (lflag && rightflag) { tag = 2; } else { if (lflag || rightflag) tag = 1; else tag = 0; } rvalue = GetPredictValue(tag, num + 1 + rightadd); ret += GetMax(lvalue, rvalue); // 135度 k = 1; num = 1; lflag = true; rflag = true; leftflag = true; rightflag = true; leftadd = 0; rightadd = 0; while (y - k >= 0 && x - k >= 0 && Array[x][y] == Array[x - k][y - k]) { num++; k++; } if (!(y - k >= 0 && x - k >= 0 && 0 == Array[x - k][y - k])) lflag = false; else { add = k + 1;// 跳过空格 while (y - add >= 0 && x - add >= 0 && Array[x][y] == Array[x - add][y - add]) { rightadd++; add++; } if (!(y - add >= 0 && x - add >= 0 && 0 == Array[x - add][y - add]))// 堵死了 rightflag = false; } k = 1; while (y + k < 15 && x + k < 15 && Array[x][y] == Array[x + k][y + k]) { num++; k++; } if (!(y + k < 15 && x + k < 15 && 0 == Array[x + k][y + k])) rflag = false; else { add = k + 1;// 跳过空格 while (y + add < 15 && x + add < 15 && Array[x][y] == Array[x + add][y + add]) { rightadd++; add++; } if (!(y + add < 15 && x + add < 15 && 0 == Array[x + add][y + add]))// 堵死了 rightflag = false; } if (leftflag && rflag) { tag = 2; } else { if (leftflag || rflag) tag = 1; else tag = 0; } lvalue = GetPredictValue(tag, num + 1 + leftadd); if (lflag && rightflag) { tag = 2; } else { if (lflag || rightflag) tag = 1; else tag = 0; } rvalue = GetPredictValue(tag, num + 1 + rightadd); ret += GetMax(lvalue, rvalue); k = 1; num = 1; leftflag = true; rightflag = true; leftadd = 0; rightadd = 0; while (y + k < 15 && x - k >= 0 && Array[x][y] == Array[x - k][y + k]) { num++; k++; } if (!(y + k < 15 && x - k >= 0 && 0 == Array[x - k][y + k])) lflag = false; else { add = k + 1;// 跳过空格 while (y + add < 15 && x - add >= 0 && Array[x][y] == Array[x - add][y + add]) { rightadd++; add++; } if (!(y + add < 15 && x - add >= 0 && 0 == Array[x - add][y + add]))// 堵死了 rightflag = false; } k = 1; while (y - k >= 0 && x + k < 15 && Array[x][y] == Array[x + k][y - k]) { num++; k++; } if (!(y - k >= 0 && x + k < 15 && 0 == Array[x + k][y - k])) rflag = false; else { add = k + 1;// 跳过空格 while (y - add >= 0 && x + add < 15 && Array[x][y] == Array[x + add][y - add]) { rightadd++; add++; } if (!(y - add >= 0 && x + add < 15 && 0 == Array[x + add][y - add]))// 堵死了 rightflag = false; } if (leftflag && rflag) { tag = 2; } else { if (leftflag || rflag) tag = 1; else tag = 0; } lvalue = GetPredictValue(tag, num + 1 + leftadd); if (lflag && rightflag) { tag = 2; } else { if (lflag || rightflag) tag = 1; else tag = 0; } rvalue = GetPredictValue(tag, num + 1 + rightadd); ret += GetMax(lvalue, rvalue); return ret; } // 计算机人工智能中直接搜索下棋,向前看LookLength步 public void GetSearchNext(int LookLength) { if (!CanGo) return; chessOneStep Option = derectSearch(MyChessPiece.GetAllFlag(), true, LookLength); int NextX = Option.GetX(); int NextY = Option.GetY(); new chessmusic("下棋.wav"); if (-1 == NextX && -1 == NextY) { if (chessimage.CurrentStep == 225) { chessimage.Message = "伯仲之间 ,胜负难分!"; CanGo = false; } return; } chessimage.CurrentStep++; chessOneStep OneStep = new chessOneStep(NextX, NextY, 0); MyChessList.AddStep(OneStep);// 保留当前下得棋 MyChessPiece.SetPositionFlag(NextX, NextY, 2); if (IsOver(MyChessPiece.GetAllFlag(), NextX, NextY)) new chessmusic("取胜.wav"); display(); } // 直接暴搜 public chessOneStep derectSearch(int[][] Array, boolean who, int deepth) { if (0 == deepth)// 返回当前局面的评估函数值 { int MaxWei = -INF; int idX = -1, idY = -1; for (int i = 0; i < 15; i++) { for (int j = 0; j < 15; j++) { // 5000,8000 if (0 == Array[i][j]) { Array[i][j] = 2; int tmp1 = Evaluate(Array, i, j); Array[i][j] = 1; int tmp2 = Evaluate(Array, i, j); if (tmp2 >= 10000000 && MaxWei < 10000000)// 机器未到死四且人到了活3 { MaxWei = tmp2 + 10000000; idX = i; idY = j; } else if (tmp2 >= 500000 && MaxWei < 500000) { MaxWei = tmp2 + 500000; idX = i; idY = j; } else if (tmp2 >= 10000 && MaxWei < 10000) { MaxWei = tmp2 + 10000; idX = i; idY = j; } else if (tmp1 > tmp2 && tmp1 > MaxWei) { MaxWei = tmp1; idX = i; idY = j; } else if (tmp2 > tmp1 && tmp2 > MaxWei) { MaxWei = tmp2; idX = i; idY = j; } Array[i][j] = 0; } } } return new chessOneStep(idX, idY, MaxWei); } chessOneStep ret = new chessOneStep(-1, -1, -INF); for (int i = 0; i < 15; i++) { for (int j = 0; j < 15; j++) { if (0 == Array[i][j]) { if (who) Array[i][j] = 2; else Array[i][j] = 1; chessOneStep tmp = derectSearch(Array, !who, deepth - 1); Array[i][j] = 0; if (tmp.GetWeight() > ret.GetWeight()) ret = tmp; } } } return ret; } // 计算机人工智能中极大极小法搜索下棋,向前看LookLength步 public void GetMinMaxsearchNext(int LookLength) { chessOneStep Option = MinMaxsearch(MyChessPiece.GetAllFlag(), true, LookLength); int NextX = Option.GetX(); int NextY = Option.GetY(); new chessmusic("下棋.wav"); if (-1 == NextX && -1 == NextY) { if (chessimage.CurrentStep == 225) { chessimage.Message = "伯仲之间 ,胜负难分!"; CanGo = false; } return; } chessimage.CurrentStep++; chessOneStep OneStep = new chessOneStep(NextX, NextY, 0); MyChessList.AddStep(OneStep);// 保留当前下得棋 MyChessPiece.SetPositionFlag(NextX, NextY, 2); if (IsOver(MyChessPiece.GetAllFlag(), NextX, NextY)) new chessmusic("取胜.wav"); display(); } // 极大极小博弈搜索 public chessOneStep MinMaxsearch(int[][] Array, boolean who, int deepth) { if (0 == deepth)// 返回当前局面的评估函数值 { int MaxWei = -INF; int idX = -1, idY = -1; for (int i = 0; i < 15; i++) { for (int j = 0; j < 15; j++) { if (0 == Array[i][j]) { Array[i][j] = 2; int tmp = Evaluate(Array, i, j); if (tmp >= MaxWei) { MaxWei = tmp; idX = i; idY = j; } Array[i][j] = 1; tmp = Evaluate(Array, i, j); if (tmp > MaxWei) { MaxWei = tmp; idX = i; idY = j; } Array[i][j] = 0; } } } return new chessOneStep(idX, idY, MaxWei); } if (who)// 轮到己方,取极大值 { chessOneStep ret = new chessOneStep(-1, -1, -INF); ArrayList<chessOneStep> TmpList = new ArrayList<chessOneStep>(); for (int i = 0; i < 15; i++) { for (int j = 0; j < 15; j++) { if (0 == Array[i][j]) { Array[i][j] = 2; TmpList.add(new chessOneStep(i, j, Evaluate(Array, i, j))); Array[i][j] = 0; } } } Collections.sort(TmpList, new MyCompare()); int num = TmpList.size() < 5 ? TmpList.size() : 5; for (int i = 0; i < num; i++) { chessOneStep t = TmpList.get(i); Array[t.GetX()][t.GetY()] = 2; chessOneStep tmp = MinMaxsearch(Array, !who, deepth - 1); if (tmp.GetWeight() > ret.GetWeight()) ret = tmp; Array[t.GetX()][t.GetY()] = 0; } return ret; } else // 轮到对手,取极小值 { chessOneStep ret = new chessOneStep(-1, -1, INF); ArrayList<chessOneStep> TmpList = new ArrayList<chessOneStep>(); for (int i = 0; i < 15; i++) { for (int j = 0; j < 15; j++) { if (0 == Array[i][j]) { Array[i][j] = 1; TmpList.add(new chessOneStep(i, j, Evaluate(Array, i, j))); Array[i][j] = 0; } } } Collections.sort(TmpList, new MyCompare()); int num = TmpList.size() < 5 ? TmpList.size() : 5; for (int i = 0; i < num; i++) { chessOneStep t = TmpList.get(i); Array[t.GetX()][t.GetY()] = 1; chessOneStep tmp = MinMaxsearch(Array, !who, deepth - 1); if (tmp.GetWeight() < ret.GetWeight()) ret = tmp; Array[t.GetX()][t.GetY()] = 0; } return ret; } }}
    5.3 源程序3 完成面板的绘制import java.awt.BasicStroke;import java.awt.Color;import java.awt.Font;import java.awt.Graphics;import java.awt.Graphics2D;import java.awt.Stroke;import java.awt.geom.Line2D;import java.util.Date;import javax.swing.JPanel;public class chesspanel extends JPanel { private static final long serialVersionUID = 1L; private chessboard MyChessBoard = new chessboard(); private chesspiece MyChessPiece = new chesspiece(); public chesspanel(chessboard MyChessBoard1, chesspiece MyChessPiece1) { MyChessBoard = MyChessBoard1; MyChessPiece = MyChessPiece1; } //自定义显示回调函数 public void display(chessboard MyChessBoard1, chesspiece MyChessPiece1) { MyChessBoard = MyChessBoard1; MyChessPiece = MyChessPiece1; this.repaint(); } //Java库刷新函数 public void paintComponent(Graphics g) {// paint(Graphics g)// // {//此时遇到的问题是只有鼠标经过是才显示button super.paintComponent(g); setBackground(new Color( chessimage.ColorOfBackGround[chessimage.WitchMatch][0], chessimage.ColorOfBackGround[chessimage.WitchMatch][1], chessimage.ColorOfBackGround[chessimage.WitchMatch][2]));// 设置背景色 //画棋盘、棋子 if (MyChessBoard != null && MyChessPiece != null) { MyChessBoard.DrawChessBoard(g);// 绘制棋盘背景 MyChessPiece.DrawChessPiece(g);// 绘制盘面棋子 } // 绘制两个玩家 g.drawImage(chessimage.LeftPlayer, 25, 25, this); g.drawImage(chessimage.RightPlayer, (int) (chessboard.scrSize.width - chessboard.Left + 50), 25, this); //画棋盘 g.drawImage(chessimage.whiteBoard, 25, 25, this); g.drawImage(chessimage.blackBoard, (int) (chessboard.scrSize.width - chessboard.Left + 250), 25, this); //显示文字提示信息 if (chessimage.Message != "") { if (chessimage.IsGameOver) { Graphics2D g2d = (Graphics2D) g; Stroke stroke = g2d.getStroke(); g2d.setStroke(new BasicStroke(10, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_ROUND)); g2d.setColor(Color.pink); g2d.draw(new Line2D.Float( (float) (chessboard.Left + chessboard.Inc * chessimage.LineLeft.GetX()), (float) (25 + chessboard.Inc * chessimage.LineLeft.GetY()), (float) (chessboard.Left + chessboard.Inc * chessimage.LineRight.GetX()), (float) (25 + chessboard.Inc * chessimage.LineRight.GetY())));// 五子连线 g2d.setStroke(stroke); } g.setColor(Color.red); g.setFont(new Font("楷体", Font.BOLD, 86)); g.drawString(chessimage.Message, (int) (chessboard.Left - 50), (int) (chessboard.Low - 7 * chessboard.Inc)); } // 设置游戏时间 g.setColor(Color.blue); g.fillRect((int) (chessboard.Left + 260), 0, 140, 20);// 左边人的下角 g.setColor(Color.yellow); g.setFont(new Font("楷体", Font.BOLD, 20)); chessimage.cur = new Date(); Long m = chessimage.cur.getTime() - chessimage.begin.getTime(); Long H = m / (60 * 60 * 1000); m = m % (60 * 60 * 1000); Long M = m / (60 * 1000); m = m % (60 * 1000); m = m / 1000; String dif = "时间" + H + ":" + M + ":" + m; g.drawString(dif, (int) (chessboard.Left + 280), 20); }}
    六、运行结果截图 游戏界面1(背景乐《高山流水》)

    游戏界面2(背景乐《赛马》)

    游戏界面3(背景乐《笑傲江湖琴箫合奏》)

    用户一方取胜界面

    计算机一方取胜界面

    七、课程总结总的感觉,跟着老师学习Java很容易上手。Java与C、C++是当今最为流行的语言,而Java是其中产生最晚、最充满活力的编程语言。这也就促使我对学习Java有着浓厚的兴趣。
    学习Java与学习其他语言一样,首先是掌握基本语法与控制结构。Java是C语言的升华版,没有指针、没有运算符重载、没有头文件等,而且在编程过程中不用用户手动管理内存,Java提供垃圾回收站自动收回运行过程中释放过的内存空间。Java不提供boolean型与整型数据的自动转换,如if(int)会报语法错,这在C系语言中是完全可以的。而且不同数据类型的转换都要强制进行,如Int x=1.0会报语法错误,必须强制类型转换。这样的风格使Java编程简单、规范。
    Java是一种面向对象的编程语言。对象定义为相关数据与方法的集合。Java编程实现可视化比其他语言方便易懂,只需new 实例化该类型对象,然后添加自定义的界面。Java不支持多继承,多继承会使程序结构较为复杂,Java中单继承与接口的引入可以实现多继承的功能。接口是一些共用的、抽象的数据与方法,但实现接口时必须实现接口中所有的抽象方法;为了避免这种问题,可以引入事件适配器类,这是一种抽象类,但在继承他们创建新类时,可以不现实所有的方法,只需实现需要实现的方法即可。
    Java提供功能齐全的异常类,处理程序运行时的出错问题。Try块监视程序段,catch进行异常处理。用户可以自定义异常类及异常处理机制。
    此外,Java在web网页应用方面有着很重要的应用。由于目前我没有进行网络方面的编程,所以在此就不展开。
    此外,我觉得老师的授课方式很好:在结课后要求每人上交自己的程序,这就能充分调动学生的编程积极性。之前我就对游戏有着浓厚的兴趣,尤其是棋类游戏,在老师布置任务后,我选择了五子棋来挑战自己。虽说之前做ACM,对算法比较敏感,但做游戏需要可视化,而且是自己不熟悉的Java语言,这是一个很大的挑战。但我眼中的IT从业人员应该是勇于挑战、善于学习、迎难而上的,于是我毅然选择了五子棋,最终达到了自己想要的效果。
    1 评论 62 下载 2018-10-06 22:18:20 下载需要8点积分
  • 基于Cocos2d-x实现的RunOrDie小游戏

    一、项目阐述1.1 简介本作是玩家通过鼠标控制角色移动,利用道具击杀怪物得分的一款游戏。
    1.2 功能玩家通过鼠标控制角色移动,地图上会不定时在随机位置出现随机移动的怪物,玩家需要避开这些怪物,并且利用随机出现的道具击杀怪物,击杀怪物数量越多,分数越高。
    1.3 道具
    小刀:角色周围会出现一把围绕着其旋转的小刀,在小刀攻击范围内的怪物将会被消灭,效果持续一段时间 炸弹:形成一次大范围伤害,消灭爆炸区域内的怪物 散射子弹:角色向 8 个方向各发射一枚有穿透性的子弹,子弹路径上的怪物将被消灭
    1.4 亮点
    操作简单,易于理解 简单的色调,清新的画风
    二、项目展示游戏主界面
    主界面有 4 个选项,分别跳转到另外 4 个子界面;主界面的背景有着不同的动画效果。

    游戏界面
    玩家用鼠标光标控制角色移动,用道具消灭怪物,道具视觉效果如下 。
    小刀道具:

    爆炸:

    散射子弹:

    游戏结束:

    排行榜:
    本地储存玩家的成绩,并按先后排名,背景图案有着不同的动画效果。

    规则说明:

    五、 项目难点及解决方案5.1 素材问题在期中 Project 展示的时候,就提到我们的期末 Project 主要卡在了动画素材的寻找上面,算是为了避开这个问题吧,我们以对称的几何图形为基本素材,对图片进行加工处理后,利用 cocos 的基础动画也能做出很不错的效果。至于颜色方面,由于几何图形的颜色不好互相搭配,所以与期中 Project 时我们的项目欢快的颜色搭配相反,这次我们干脆统一一个黑白的色调,这样看起来反而更加简洁精炼。素材来源方面的困难就这样解决了吧。
    5.2 素材处理虽然素材来源方面的困难这样解决了,但是不对图片进行后续的处理时很难达到想要的效果的,而其实我们小组 4 位成员都不会用 PS 软件,也对图像处理没什么经验。但为了更好的效果,只能下载了 PS,一边问身边的同学,一边百度摸索,一步步做出自己想要的图片效果,才有了现在的这个看起来很不错的游戏!
    5.3 在游戏玩家获得道具且道具结束时会出现报错由于道具在结束后将其从场景中移除,而在判断碰撞的调度器里,我们会获取道具的位置,这道具精灵已为空,所以出现错误。对此我们在道具消失前的 0.05 秒便停止判断碰撞,这样避免了访问空精灵的位置。
    5.4 在创建数据库过程中,无法引用 sqlite3.h这个问题虽然困扰了不少时间,但是解决方法却比较简单,就是要先下载一个sqlite3 的包,然后需要在加入一行代码:#pragma comment(lib, “sqlite3.lib”)。
    5.5 对于怪物的移动由于怪物如果只追着玩家时,最终所有怪物都会比较聚集在中心位置。而这里我们添加了另一组随机移动的怪物,尽量使得怪物都能比较分散地分布在地图上。
    5.6 在制作发散子弹由于发散子弹是从 8 个方向发射出去,而一开始打算只创建一个精灵,去实现 8个方向发射。而显然这难以实现,后来跟组员讨论一波后,觉得通过创建 8 个精灵去实现这一效果,虽然这样的话在判断碰撞时要考虑 8 个精灵比较多,但是这也是我们能想到相对较好的方法了。
    1 评论 17 下载 2018-10-06 21:35:59 下载需要11点积分
  • 基于WIN32汇编实现的仿Windows计算器

    摘要使用Win32编程设计一个功能及界面风格类似于Windows计算器的计算器程序,只要求实现标准型计算器。
    主要实现的功能:包含基本的四则运算、倒数运算、平方根运算。并支持存储区的存储、清除、调出、累加等功能。
    关键词:win32,Windows计算器,汇编,四则运算,倒数运算,平方根运算
    AbstractUsing Win32 programming to design a calculator program with a functional and interface style similar to the Windows calculator.It’s only a standard calculator.
    Mainly implemented functions including basic four arithmetic operations, reciprocal operations, and square root operations. It also supports the clearing, recalling, and accumulating functions of the storage area.
    Keywords:Win32, Windows calculator program, Assembly Language , Arithmetic, Countdown, Square root operation
    1 系统分析与设计1.1 系统分析本程序为Win32窗口应用程序,因此采用Windows开发包的文档中规定的Windows程序标准框架进行编程设计。

    1.2 系统设计按照Windows程序标准框架,主程序用于获得并保存本程序的句柄,并调用窗口主程序WinMain创建窗口并进入消息循环。WinMain程序将获取的消息分发给消息处理程序Calculate进行处理。
    消息处理程序Calculate用于相应窗口创立、销毁、按键等消息并进行处理。
    系统总体架构如下图:

    1.3 界面设计系统界面仿照Windows计算器程序界面设计,并使用资源文件进行定义,设计界面如下:

    1.4 功能分析与设计
    数字:添加文本框字符串添加数字字符,调用函数BtnNum完成该功能小数点:为当前输入数字添加小数点,将判断是否小数点的变量HasPoint赋值为1正负号:将当前数字取相反数并在对话框显示,拟通过浮点运算求相反数并调用ShowNum函数显示数字双目运算符:计算结果,调用函数BtnOperator实现运算功能等号:计算结果,调用函数BtnEqual实现运算功能单目运算符:立即对当前数字进行运算并输出结果MS:将当前数据保存在变量Remember中,并在记忆区存储情况的标签中显示相应的信息M+:将当前数据加到变量Remember上,并在记忆区存储情况的标签中显示相应的信息MR:将变量Remember数据显示到文本框中MC:将变量Remember归零,并在记忆区存储情况的标签中显示相应的信息C:初始化计算器,调用函数Init实现该功能,并在文本框显示0.CE:将当前数字清零Backspace:删除当前数据的末位数字
    1.5 文件设计
    头文件(Calculator.inc):头文件中引入程序所需要的库以及常量和函数申明源文件(Calculator.asm):汇编程序源代码资源文件(Calculator.rc):定义程序的窗口界面以及相关资源说明文件(Calculator.exe.manifest):说明程序的相关配置及信息
    2 系统实现2.1 创建计算器界面利用资源文件定义系统界面,代码如下,文件分别定义了对话框,菜单和Icon图标等资源,为了在程序中方便对消息的处理,此处有意连续定义了ID_NUM0~ID_NUM9。
    #include "resource.h"#define ISOLATION_AWARE_ENABLED#define ID_NUM0 300#define ID_NUM1 301#define ID_NUM2 302#define ID_NUM3 303#define ID_NUM4 304#define ID_NUM5 305#define ID_NUM6 306#define ID_NUM7 307#define ID_NUM8 308#define ID_NUM9 309#define ID_NEG 310#define ID_POINT 311#define ID_MUL 312#define ID_DIV 313#define ID_SUB 314#define ID_ADD 315#define ID_EQU 316#define ID_PER 317#define ID_DAO 318#define ID_SQRT 319#define ID_MC 320#define ID_MR 321#define ID_MS 322#define ID_MPLUS 323#define ID_M 324#define ID_BACK 325#define ID_CE 326#define ID_C 327#define ID_RESULT 328#define ID_COPY 1001#define ID_PASTE 1002#define ID_STANDARD 1003#define ID_SCIENCE 1004#define ID_PACKET 1006#define ID_HELP 1007#define ID_ABOUT 1008#define ID_EXIT 1009Calculator DIALOGEX 0, 0, 170, 133STYLE DS_CENTER | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOXCLASS "Calculator"CAPTION "计算器"FONT 8, "Tahoma"BEGIN PUSHBUTTON "0",ID_NUM0,36,99,23,16,0 PUSHBUTTON "1",ID_NUM1,36,81,23,16,0 PUSHBUTTON "2",ID_NUM2,61,81,23,16,0 PUSHBUTTON "3",ID_NUM3,87,81,23,16,0 PUSHBUTTON "4",ID_NUM4,36,63,23,16,0 PUSHBUTTON "5",ID_NUM5,61,63,23,16,0 PUSHBUTTON "6",ID_NUM6,87,63,23,16,0 PUSHBUTTON "7",ID_NUM7,36,44,23,16,0 PUSHBUTTON "8",ID_NUM8,61,44,23,16,0 PUSHBUTTON "9",ID_NUM9,87,44,23,16,0 PUSHBUTTON "+/-",ID_NEG,61,99,23,16,0 PUSHBUTTON ".",ID_POINT,87,99,23,16,0 PUSHBUTTON "/",ID_DIV,113,44,23,16,0 PUSHBUTTON "*",ID_MUL,113,63,23,16,0 PUSHBUTTON "-",ID_SUB,113,81,23,16,0 PUSHBUTTON "+",ID_ADD,113,99,23,16,0 PUSHBUTTON "sqrt",ID_SQRT,139,44,23,16,0 PUSHBUTTON "%",ID_PER,139,63,23,16,0 PUSHBUTTON "1/x",ID_DAO,139,81,23,16,0 PUSHBUTTON "=",ID_EQU,139,99,23,16,0 PUSHBUTTON "MC",ID_MC,6,44,23,16,0 PUSHBUTTON "MR",ID_MR,6,63,23,16,0 PUSHBUTTON "MS",ID_MS,6,81,23,16,0 PUSHBUTTON "M+",ID_MPLUS,6,99,23,16,0 PUSHBUTTON "Backspace",ID_BACK,36,23,42,16,0 PUSHBUTTON "CE",ID_CE,79,23,41,16,0 PUSHBUTTON "C",ID_C,122,23,41,16,0 EDITTEXT ID_RESULT,5,2,160,13,ES_RIGHT | ES_NUMBER ,0 CTEXT "",ID_M,9,23,17,14,SS_SUNKEN | NOT WS_BORDERENDMenu MENU LOADONCALL BEGIN POPUP "编辑(&F)" BEGIN MENUITEM "复制(&C) Ctrl+C",ID_COPY MENUITEM "粘贴(&P) Ctrl+P",ID_PASTE MENUITEM SEPARATOR MENUITEM "关闭(&E)",ID_EXIT END POPUP "查看(&V)" BEGIN MENUITEM "标准型(&T)",ID_STANDARD MENUITEM "科学型(&S)",ID_SCIENCE,GRAYED MENUITEM SEPARATOR MENUITEM "数字分组(&I)",ID_PACKET END POPUP "帮助(&H)" BEGIN MENUITEM "帮助主题(&H)",ID_HELP MENUITEM SEPARATOR MENUITEM "关于计算器(&A)",ID_ABOUT END POPUP "", GRAYED BEGIN MENUITEM "复制(&C) Ctrl+C",1001 MENUITEM "粘贴(&P) Ctrl+P",1002 MENUITEM SEPARATOR MENUITEM "标准型(&T)",1003 MENUITEM "科学型(&S)",1004,GRAYED MENUITEM SEPARATOR MENUITEM "数字分组(&I)",1006 MENUITEM SEPARATOR MENUITEM "帮助主题(&H)",1007 MENUITEM "关于计算器(&A)",1008 MENUITEM SEPARATOR MENUITEM "关闭(&E)",1009 END ENDIcon ICON MOVEABLE PURE LOADONCALL DISCARDABLE "Calculator.ico"2.2 引入头文件及库在Calculator.inc头文件中统一定义程序所需的头文件及引入库,代码如下:
    ;--------------------------- 头文件声明--------------------------- include windows.inc include user32.inc include kernel32.inc include comctl32.inc include masm32.inc include shell32.inc;--------------------------- 引入库声明--------------------------- includelib user32.lib includelib comctl32.lib includelib masm32.lib2.3 定义常量在Calculator.inc中定义程序所需常量,代码如下:
    ;---------------------------- 常量声明---------------------------- ID_NUM0 equ 300 ID_NUM1 equ 301 ID_NUM2 equ 302 ID_NUM3 equ 303 ID_NUM4 equ 304 ID_NUM5 equ 305 ID_NUM6 equ 306 ID_NUM7 equ 307 ID_NUM8 equ 308 ID_NUM9 equ 309 ID_NEG equ 310 ID_POINT equ 311 ID_MUL equ 312 ID_DIV equ 313 ID_SUB equ 314 ID_ADD equ 315 ID_EQU equ 316 ID_PER equ 317 ID_DAO equ 318 ID_SQRT equ 319 ID_MC equ 320 ID_MR equ 321 ID_MS equ 322 ID_MPLUS equ 323 ID_M equ 324 ID_BACK equ 325 ID_CE equ 326 ID_C equ 327 ID_RESULT equ 328 ID_COPY equ 1001 ID_PASTE equ 1002 ID_STANDARD equ 1003 ID_SCIENCE equ 1004 ID_PACKET equ 1006 ID_HELP equ 1007 ID_ABOUT equ 1008 ID_EXIT equ 1009 ID_NOTIFYICON equ 2000 WM_SHELLNOTIFY equ WM_USER+12.4 函数声明在Calculator.inc声明了自定义函数的原型,代码如下:
    ;---------------------------- 函数声明---------------------------- WinMain PROTO :DWORD, :DWORD, :DWORD, :DWORD ; 窗口主程序 Calculate PROTO :DWORD,:DWORD,:DWORD,:DWORD ; 消息处理程序 PackNum PROTO ; 数字分组子程序 UnpackNum PROTO ; 数字不分组子程序 BtnNum PROTO :DWORD ; 数字按键消息处理程序 ShowNum PROTO ; 显示数据子程序 ShowTextM PROTO ; 显示存储信息子程序 Init PROTO ; 初始化计算器子程序 GetResult PROTO ; 计算结果子程序 BtnOperator PROTO ; 双目运算符消息处理程序 BtnEqual PROTO ; 等于消息处理程序数据段定义代码如下:
    ;===================== Start 数据段定义Start ===================== .data ProgramName db "计算器",0 ;程序名 Author db "作者:桂杨",0 ;作者 HelpFile db "rc.hlp",0 ;帮助文档 hInstance db ? ;主程序句柄 hEdit db ? ;输出文本框句柄 hTextM db ? ;记忆标签句柄 hMenu db ? ;菜单句柄 hIcon db ? ;Icon句柄 DialogName db "Calculator",0 ;对话框名称 MenuName db "Menu",0 ;菜单名称 IconName db "Icon",0 ;Icon名称 TextM db 'M',0 ;M Output db "0.",0,30 dup(0) ;输出字符串 IsStart db 1 ;判断是否运算开始 HasPoint db 0 ;判断是否存在小数点 HasEqueal db 0 ;判断是否存在等号 Remember dq 0.0 ;记忆数据 Number dq 0.0 ;记录临时数据 Result dq 0.0 ;记录结果 Operand dq 0.0 ;记录操作数 IsPacket db 0 ;数字分组 Operator db '.' ;记录运算符 IsError db 0 ;记录是否出现异常 Div0 db "除数不能为零。",0 FunctionError db "函数输入无效。",0 hGlobal HANDLE ? ;剪切板内存块句柄 pGlobal db ? ;pointer to allocate memory NumLittle REAL8 1.0E-12 Num10 REAL8 10.0 ;实数10 Num100 REAL8 100.0 ;实数100 NotifyIcon NOTIFYICONDATA<> ;通知栏图标;======================= End 数据段定义End =======================2.5 程序说明2.5.1 主程序主程序用于获得并保存本程序的句柄,调用WinMain主程序创建窗口并获取和分发消息,然后结束程序。
    主程序流程图及原代码如下:

    invoke GetModuleHandle,NULL;获得并保存本程序的句柄mov hInstance,eaxinvoke WinMain,hInstance,0,0,SW_SHOWDEFAULTinvoke ExitProcess,eax;退出程序,返回eax值2.5.2 WinMain主程序WinMain主程序用于创建窗口并获取和分发消息。
    主程序流程图及源代码如下:

    WinMain proc hInst:DWORD, hPrevInst:DWORD, CmdLine:DWORD, CmdShow:DWORD LOCAL wc:WNDCLASSEX ;窗口类 LOCAL msg:MSG ;消息 LOCAL hWnd:HWND ;对话框句柄 mov wc.cbSize,sizeof WNDCLASSEX ;WNDCLASSEX的大小 mov wc.style,CS_BYTEALIGNWINDOW or CS_BYTEALIGNWINDOW ;窗口风格or CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndProc,OFFSET Calculate ;窗口消息处理函数地址 mov wc.cbClsExtra,0 ;在窗口类结构后的附加字节数,共享内存 mov wc.cbWndExtra,DLGWINDOWEXTRA ;在窗口实例后的附加字节数(!注意点) mov eax,hInst mov wc.hInstance,eax ;窗口所属程序句柄 mov wc.hbrBackground,COLOR_BTNFACE+1 ;背景画刷句柄 mov wc.lpszMenuName,NULL ;菜单名称指针 mov wc.lpszClassName,OFFSET DialogName ;类名称指针 invoke LoadIcon,hInst,addr IconName ;加载Icon mov wc.hIcon,eax ;图标句柄 invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor,eax ;光标句柄 mov wc.hIconSm,0 ;窗口小图标句柄 invoke RegisterClassEx,addr wc ;注册窗口类 invoke CreateDialogParam,hInst,addr DialogName,0,addr Calculate,0 ;调用对话框窗口 mov hWnd,eax ;保存对话框句柄 invoke ShowWindow,hWnd,CmdShow ;最后一个参数可设置为SW_SHOWNORMAL invoke UpdateWindow,hWnd ;更新窗口StartLoop: ;消息循环 invoke GetMessage,addr msg,0,0,0 ;获取消息 cmp eax,0 je ExitLoop invoke TranslateMessage,addr msg ;转换键盘消息 invoke DispatchMessage,addr msg ;分发消息 jmp StartLoopExitLoop: ;结束消息循环 mov eax,msg.wParam retWinMain endp2.5.3 消息处理程序消息处理程序用于处理用户消息。
    消息处理程序流程图及代码如下:

    Calculate proc hWin:DWORD,uMsg:UINT,aParam:DWORD,bParam:DWORD LOCAL pt:POINT .if uMsg == WM_INITDIALOG invoke GetDlgItem,hWin,ID_RESULT ;获取输出文本框句柄 mov hEdit,eax ;保存文本框句柄 invoke GetDlgItem,hWin,ID_M ;获取记忆标签句柄 mov hTextM,eax ;保存记忆标签句柄 invoke LoadIcon,hInstance,addr IconName ;载入Icon mov hIcon,eax ;保存Icon句柄 invoke SendMessage,hWin,WM_SETICON,ICON_SMALL ,eax invoke LoadMenu,hInstance,addr MenuName ;加载菜单 mov hMenu,eax ;保存菜单句柄 invoke SetMenu,hWin,eax invoke CheckMenuRadioItem, hMenu, ID_STANDARD, ID_SCIENCE,ID_STANDARD,MF_BYCOMMAND ;选中标准型 invoke SendMessage,hEdit,WM_SETTEXT,0,addr Output ;显示"0." .elseif uMsg == WM_SIZE .if aParam==SIZE_MINIMIZED ;最小化 mov NotifyIcon.cbSize,sizeof NOTIFYICONDATA push hWin pop NotifyIcon.hwnd mov NotifyIcon.uID,ID_NOTIFYICON mov NotifyIcon.uFlags,NIF_ICON+NIF_MESSAGE+NIF_TIP mov NotifyIcon.uCallbackMessage,WM_SHELLNOTIFY mov eax,hIcon mov NotifyIcon.hIcon,eax invoke lstrcpy,addr NotifyIcon.szTip,addr ProgramName invoke ShowWindow,hWin,SW_HIDE ;隐藏窗口 invoke Shell_NotifyIcon,NIM_ADD,addr NotifyIcon .endif .elseif uMsg == WM_SHELLNOTIFY .if aParam==ID_NOTIFYICON .if (bParam==WM_LBUTTONDOWN) ;单击通知栏图标 invoke ShowWindow,hWin,SW_SHOW ;显示窗口 invoke Shell_NotifyIcon,NIM_DELETE,addr NotifyIcon ;删除通知栏图标 .elseif (bParam==WM_RBUTTONDOWN) ;右键通知栏图标 invoke GetCursorPos,addr pt invoke GetSubMenu,hMenu,3 invoke TrackPopupMenu,eax,TPM_LEFTALIGN,pt.x,pt.y,NULL,hWin,NULL .endif .endif .elseif uMsg == WM_CHAR ;热键操作 mov eax,aParam sub eax,'0' add eax,ID_NUM0 .if (eax>=ID_NUM0) && (eax<=ID_NUM9) ;数字按钮 invoke Calculate,hWin,WM_COMMAND,eax,0 .elseif (eax==0ffh) ;ID_COPY invoke Calculate,hWin,WM_COMMAND,ID_COPY,0 .elseif (eax==112h) ;ID_PASTE invoke Calculate,hWin,WM_COMMAND,ID_PASTE,0 .elseif (eax==104h) ;ID_BACK invoke Calculate,hWin,WM_COMMAND,ID_BACK,0 .elseif (eax==265) ;ID_EQU invoke Calculate,hWin,WM_COMMAND,ID_EQU,0 .elseif (eax==298) ;ID_POINT invoke Calculate,hWin,WM_COMMAND,ID_POINT,0 .elseif(eax==295) ;ID_ADD invoke Calculate,hWin,WM_COMMAND,ID_ADD,0 .elseif (eax==297) ;ID_SUB invoke Calculate,hWin,WM_COMMAND,ID_SUB,0 .elseif (eax==294) ;ID_MUL invoke Calculate,hWin,WM_COMMAND,ID_MUL,0 .elseif (eax==299) ;ID_DIV invoke Calculate,hWin,WM_COMMAND,ID_DIV,0 .endif .elseif uMsg == WM_COMMAND mov eax,aParam .if eax == ID_CE ;清零按钮CE lea esi,Output mov BYTE PTR[esi],'0' mov BYTE PTR[esi+1],'.' mov BYTE PTR[esi+2],0 .if IsError==1 invoke Init .endif invoke SendMessage,hEdit,WM_SETTEXT,0,addr Output .elseif eax == ID_C ;初始化按钮C invoke Calculate,hWin,WM_COMMAND,ID_CE,bParam invoke Init .elseif IsError==1 ret .elseif eax == ID_BACK ;退格按钮Backspace invoke UnpackNum .if IsStart==0 lea esi,Output .while BYTE PTR[esi]!=0 inc esi .endw .if BYTE PTR[esi-1]=='.' .if HasPoint==1 mov HasPoint,0 .else .if BYTE PTR[esi-3]=='-' lea esi,Output mov BYTE PTR[esi],'0' mov BYTE PTR[esi+1],'.' mov BYTE PTR[esi+2],0 .else mov BYTE PTR[esi-2],'.' mov BYTE PTR[esi-1],0 .endif .endif .else mov BYTE PTR[esi-1],0 .endif lea esi,Output .if BYTE PTR[esi]=='.' mov BYTE PTR[esi],'0' mov BYTE PTR[esi+1],'.' mov BYTE PTR[esi+2],0 .endif invoke ShowNum .endif .elseif (eax >= ID_NUM0) && (eax <= ID_NUM9) ;数字按钮 .if HasEqueal==1 invoke Init .endif invoke BtnNum,eax .elseif eax == ID_POINT ;小数点按钮 mov BYTE PTR HasPoint,1 mov BYTE PTR IsStart,0 .elseif eax == ID_NEG ;正负号按钮 invoke UnpackNum invoke StrToFloat,addr Output, addr Number finit fldz fld Number fsub fstp Number invoke FloatToStr2,Number,addr Output invoke ShowNum .elseif (eax >= ID_MUL) && (eax <= ID_ADD) ;双目运算符按钮 invoke BtnOperator .elseif eax == ID_EQU ;等于按钮 invoke BtnEqual .elseif eax == ID_PER ;百分号按钮 mov Operator,'*' invoke GetResult invoke UnpackNum invoke StrToFloat,addr Output, addr Number finit fld Number fld Num100 fdiv fstp Number invoke FloatToStr2,Number,addr Output invoke ShowNum .elseif eax == ID_DAO ;倒数按钮 invoke UnpackNum invoke StrToFloat,addr Output, addr Number finit fld Number fldz fcomi ST(0),ST(1) jnz NotZero mov IsError,1 invoke SendMessage,hEdit,WM_SETTEXT,0,addr Div0 retNotZero: fstp Number fstp Number fld1 fld Number fdiv .if HasEqueal==1 fst Result .endif fstp Number invoke FloatToStr2,Number,addr Output invoke ShowNum .elseif eax == ID_SQRT ;开方按钮 invoke UnpackNum invoke StrToFloat,addr Output, addr Number finit fld Number fldz fcomi ST(0),ST(1) jb Positive mov IsError,1 invoke SendMessage,hEdit,WM_SETTEXT,0,addr FunctionError retPositive: fstp Number fsqrt .if HasEqueal==1 fst Result .endif fstp Number invoke FloatToStr2,Number,addr Output invoke ShowNum .elseif eax == ID_MC ;MC按钮 fldz fstp Remember invoke SendMessage,hTextM,WM_SETTEXT,0,NULL .elseif eax == ID_MR ;MR按钮 invoke FloatToStr2,Remember,addr Output invoke ShowNum mov IsStart,0 .elseif eax == ID_MS ;MS按钮 invoke UnpackNum invoke StrToFloat,addr Output, addr Remember invoke ShowTextM .elseif eax == ID_MPLUS ;M+按钮 finit fld Remember invoke UnpackNum invoke StrToFloat,addr Output, addr Remember fld Remember fadd fstp Remember invoke ShowTextM .elseif eax == ID_COPY ;复制 invoke GlobalAlloc,GMEM_MOVEABLE,35 ;配置一个内存块 mov hGlobal ,eax invoke GlobalLock,hGlobal ;锁定内存块 mov pGlobal ,eax lea esi,Output mov edi,pGlobal mov ecx,35 rep movsb ;复制字符串 invoke GlobalUnlock,hGlobal ;解锁内存块 invoke OpenClipboard, NULL ;打开剪切板 invoke EmptyClipboard ;清空剪切板 invoke SetClipboardData,CF_TEXT,hGlobal ;把内存句柄交给剪贴簿 invoke CloseClipboard ;关闭剪切板 .elseif eax == ID_PASTE ;粘贴 invoke IsClipboardFormatAvailable,CF_TEXT ;确定剪贴簿是否含有CF_TEXT格式的数据 invoke OpenClipboard,NULL ;打开剪切板 invoke GetClipboardData,CF_TEXT ;得到代表文字的内存块代号 mov hGlobal,eax invoke GlobalLock ,hGlobal ;解锁内存块 mov pGlobal,eax mov ecx,35 lea edi,Output mov esi,eax rep movsb ;复制字符串 invoke GlobalUnlock ,hGlobal ;解锁内存块 invoke CloseClipboard ;关闭剪切板 invoke ShowNum .elseif eax == ID_PACKET ;数字分组 .if IsPacket==0 invoke CheckMenuItem,hMenu,ID_PACKET,MF_CHECKED ;选中数字分组 .else invoke CheckMenuItem,hMenu,ID_PACKET,MF_UNCHECKED ;选中数字分组 .endif xor IsPacket,1 invoke ShowNum .elseif eax == ID_HELP ;帮助 invoke WinHelp,hWin,addr HelpFile,HELP_CONTENTS,1 .elseif eax == ID_ABOUT ;关于 invoke ShellAbout,hWin,addr ProgramName,addr Author,hIcon .elseif eax == ID_EXIT ;关闭 invoke Calculate,hWin,WM_CLOSE,aParam,bParam .endif .elseif uMsg == WM_CLOSE invoke Shell_NotifyIcon,NIM_DELETE,addr NotifyIcon invoke EndDialog,hWin,NULL invoke PostQuitMessage,0 ;退出消息循环 .else invoke DefWindowProc,hWin,uMsg,aParam,bParam ret .endif invoke SetFocus,hWin xor eax,eax ;关于WM_KEYDOWN原因 retCalculate endp2.5.4 工具子程序说明2.5.4.1 PackNumPackNum函数将输出数据的字符串Output进行数字分组。它首先获取小数点以前的数字位数并保存在寄存器eax中,然后将(eax-1)/3即为需要添加的字符‘,’数目,并保存在eax中,对于小数点以后的字符都向后移动eax位,对于小数点以前的字符,向后移动eax位并用ecx计数,当ecx计数到3是添加字符‘,’并将ecx设为1且eax减一,重复上述步骤直到eax等于0。
    函数的流程图及代码如下:

    PackNum proc USES eax ebx ecx edx lea esi,Output mov eax,0 .while (BYTE PTR[esi]!='.') inc eax inc esi .endw .while (BYTE PTR[esi]!=0) inc esi .endw dec eax mov edx,0 mov ecx,3 div ecx .while (BYTE PTR[esi]!='.') mov bx,[esi] mov [esi+eax],bx dec esi .endw mov bx,[esi] mov [esi+eax],bx dec esi mov ecx,0 .while (eax!=0) .if(ecx<3) mov bx,[esi] mov [esi+eax],bx inc ecx .else mov BYTE PTR[esi+eax],',' dec eax mov ecx,1 .endif dec esi .endw lea esi,Output .while (BYTE PTR[esi]!=0) mov bx,[esi] inc esi .endw retPackNum endp2.5.4.2 UnpackNumUnpackNum函数将进行数字分组输出的字符串Output解分组。它首先获取Output地址存在esi中,然后ecx赋0,并将Output中字符向前移动ecx个单位,遇见‘,’字符则将ecx加1,直到字符串结束。
    函数的流程图及代码如下:

    UnpackNum proc USES ecx lea esi,Output mov ecx,0 .while (BYTE PTR[esi+ecx]!=0) .if(BYTE PTR[esi]==",") inc ecx .endif mov bx,[esi+ecx] mov [esi],bx inc esi .endw retUnpackNum endp2.5.4.3 ShowNumShowNum函数将Output字符串处理后在文本框中显示出来。它首先调用UnpackNum函数对Output解分组,然后获取Output地址存在esi、edi中,通过循环将Output尾地址存在esi中,将字符‘.’地址存在edi中,如果edi等于esi则表明Output中无字符‘.’,则在结尾添加字符‘.’。如果IsPacked等于1则对Output调用UnpackNum函数对其分组,最后向文本框发送WM_SETTEXT消息显示数据。
    函数的流程图及代码如下:

    ShowNum proc invoke UnpackNum lea esi,Output lea edi,Output .while (BYTE PTR[esi]!=0) inc esi .endw .while (BYTE PTR[edi]!='.') && (edi<esi) inc edi .endw .if esi==edi mov BYTE PTR[esi],'.' mov BYTE PTR[esi+1],0 .endif .if IsPacket==1 invoke PackNum .endif invoke SendMessage,hEdit,WM_SETTEXT,0,addr Output retShowNum endp2.5.4.4 BtnNumBtnNum函数响应数字按钮消息,向文本框中添加字符。
    函数源代码如下:
    BtnNum proc USES eax,Num:DWORD lea esi,Output mov eax,Num sub eax,252 .if IsStart==1 mov [esi],eax inc esi mov BYTE PTR[esi],'.' inc esi mov BYTE PTR[esi],0 mov IsStart,0 .else .while BYTE PTR[esi]!='.' inc esi .endw .if HasPoint==1 .while BYTE PTR[esi]!=0 inc esi .endw mov [esi],ax inc esi mov BYTE PTR[esi],0 .else .if BYTE PTR[Output]=='0' lea esi,Output mov [esi],eax mov BYTE PTR[esi+1],'.' mov BYTE PTR[esi+2],0 .else mov [esi],eax inc esi mov BYTE PTR[esi],'.' inc esi mov BYTE PTR[esi],0 .endif .endif .endif invoke ShowNum ret BtnNum endp2.5.4.5 BtnOperatorBtnOperator函数响应运算符按钮消息,进行运算并输出结果。首先判断是否为等号,如果不是则调用GetResult函数先进行一次运算,然后将当前操作符存入Operator变量中。
    函数源代码如下:
    BtnOperator proc USES eax .if HasEqueal!=1 invoke GetResult .endif .if eax == ID_MUL mov Operator,'*' .elseif eax == ID_DIV mov Operator,'/' .elseif eax == ID_SUB mov Operator,'-' .elseif eax == ID_ADD mov Operator,'+' .endif mov HasEqueal,0 retBtnOperator endp2.5.4.6 BtnEqualBtnEqual函数响应等号按钮消息,进行运算并输出结果。首先判断是否为起始状态,如果不是则调用GetResult函数,并将HasEqual变量置1。
    函数源代码如下:
    BtnEqual proc .if (IsStart==1) && (HasEqueal==0) fstp Number fst Number fld Number .endif invoke GetResult mov HasEqueal,1 retBtnEqual endp2.5.4.7 GetResultBtnEqual函数响应等号按钮消息,进行运算并输出结果。首先判断是否为起始状态,如果不是则调用GetResult函数,并将HasEqual变量置1。
    函数源代码如下:
    GetResult proc USES eax invoke UnpackNum finit .if (IsStart==1) && (HasEqueal==0) .else .if HasEqueal!=1 invoke StrToFloat,addr Output, addr Operand .endif fld Result fld Operand .if Operator=='.' fst Result jmp Show .elseif Operator=='+' fadd ST(1),ST(0) .elseif Operator=='-' fsub ST(1),ST(0) .elseif Operator=='*' fmul ST(1),ST(0) .elseif Operator=='/' fldz fcomi ST(0),ST(1) jnz NotZero mov IsError,1 invoke SendMessage,hEdit,WM_SETTEXT,0,addr Div0 retNotZero: fstp Operand fdiv ST(1),ST(0) .endif fstp Operand fst ResultShow: mov IsStart,1 mov HasPoint,0 invoke FloatToStr2,Result,addr Output invoke ShowNum .endif retGetResult endp2.5.4.8 ShowTextMShowTextM函数判断Remember中的值是否为0,如果不是是则在标签中显示‘M’,否则清空标签中内容。
    函数源代码如下:
    ShowTextM proc fld NumLittle fldz fsub Remember fabs fcomi ST(0),ST(1) ja NotZero invoke SendMessage,hTextM,WM_SETTEXT,0,NULL jmp PopNumLittleNotZero:invoke SendMessage,hTextM,WM_SETTEXT,0,addr TextMPopNumLittle:fstp Operand fstp Operand mov IsStart,1 mov HasPoint,0 retShowTextM endp2.5.4.9 InitInit函数负责进行必要的初始化操作,如对状态变量的初始化以及的FPU的初始化。
    函数源代码如下:
    Init proc mov IsStart,1 ;初始化 mov HasPoint,0 ;清除小数点 mov HasEqueal,0 fldz fst Number ;清除结果 fst Operand mov Operator,'.' ;清除运算符 mov IsError,0 finit ;初始化FPU ret Init endp3 参考文献
    《80X86汇编语言程序设计》,王元珍、曹忠升、韩宗芬,华中科技大学出版社,2005《Iczelion的Win32汇编教程》《Intel汇编语言程序设计(第五版)》,【美】Kip R Irvine,电子工业出版社,2008《汇编语言编程艺术》,Randall Hyde,清华大学出版社 ,2005《IBM PC汇编语言程序设计(第五版)》,Peter Abel,人民邮电出版社,2002《Win32开发人员参考库第五卷:Windows Shell》,David Iseminger,机械工业出版社,2001《Microsoft MASM 参考手册》《现代操作系统》,【荷】Andrew S. Tanenbaum 机械工业出版社,2009《Windows核心编程(第五版)》,【美】Jeffery Richter清华大学出版社,2008《Windows程序设计(第五版)》,【美】Charles Petzold,北京大学出版社,1999《Intel® 64 and IA-32 Architectures Software Developer’s Manuals》MSDN Library: http://www.microsoft.com/china/MSDN/library/
    1 评论 89 下载 2018-10-06 20:24:31 下载需要12点积分
  • 基于Python与Node.js实现的医疗图像库在线存储与检索平台网站

    摘 要图像数据相对于一般的文本数据来说管理起来更具有复杂性。传统的图像存储方式有两种,一是直接将图像存入数据库,二是将图像存放在文件系统,而将路径存放在数据库,前一种基于“大字段数据最好不要存放在数据库中”这种规则一般不被使用,常用的是后一种,而这种方式也有明显的性能劣势,原因在于访问图像时要两次访问IO,这在高并发访问中很难满足需要。
    为了对Dicom格式实现高效的管理以及为项目其他模块提供便捷的服务,特设计与实现了一系列对医疗图像、病人隐私信息、深度学习框架操作的接口。
    Mongodb是一个非关系型数据库亦可以称作文档型数据库,因为其存储的节本单位是文档等同于关系型数据库中表的一行。该数据库考虑了图像的存储,提供了Gridfs这种存储方式,来满足大量图像管理的需要。
    关键字:Dicom,Mongodb,Gridfs
    ABSTRACTThe image data with respect to the general text data management is more complicated. There are two kinds of image storage in the traditional way, one is the image stored in the database, the two image is stored in the file system, and the path stored in the database, which is based on the former “characters of data stored in the database is best not the rule the general is not being used, is commonly used after a while, this approach also has disadvantage performance obviously, because access to the image two times to visit IO, in this highly concurrent access is difficult to meet the needs.
    In order to realize the efficient management of the Dicom format and provide convenient service for the project design and implementation of other modules, especially a series of medical images, patient privacy information, deep learning framework interface.
    Mongodb is a non relational database can also be called the document database, because its storage cost is equivalent to a unit of the document in a relational database table. The database is considered image storage, Gridfs provides this storage method to meet the need to manage a large number of image.
    Key words: Dicom, Mongodb, Gridfs
    1 绪论1.1 研究背景及意义1.1.1 研究背景图像作为数据的一种,和其他形式的数据一样必要的时候要被保存起来,以备后用。目前存储图像的方式主要有两种,其一,将图像保存在文件系统,然后将路径存放在数据库,其二,将图像直接存放在数据库中。一般都会采用第一种,因为其二大字段数据会导致数据库压力增大、备份困难、查询速度变慢等问题。第一种存储方式也存在性能,因为会访问IO两次,这在高并发的情况下读写速度会异常的慢。面对高并发医疗图像读写这种局面,迫切需要一种更为优化的方式来满足性能的需要。
    1.1.2 研究意义随着时代的迁徙,医疗图像在医生的诊断过程中已经成为不可或缺的一部分,并且也是诊断中最重要的一环之一,医生通过肉眼观察医疗图像来判断病人的健康情况,然后做出治疗。不过在这种什么都讲究效率的时代,人工观察图像已经不能满足需要,并且这种方法也存在很多的缺点,比如准确性问题。因此,我们研究并设计了一款自动识别医疗图系统,而我的工作就是为其他模块提供数据支持。众所周知,在一个项目中往往IO是影响性能的主要因素,而性能也是所有用户最在乎的一点,所以我的模块可以说是在整个项目中举足轻重。
    1.2 技术栈的选择整个项目主要采用node.js、python两种语言来满足不同模块的需要。而我负责的这部分主要主要是想其他模块提供数据支持,因此就需要分别采用这两种语言来开发各个接口集合。
    再数据库选择这块,可以有多种选择,如MySQL、oracle等关系型数据库以及mongodb这种非关系型数据库,不过最终选择了mongodb数据库,具体有如下原因:

    高性能、易部署、易使用。
    提供Gridfs存储格式来支持文件存储,以这种存储格式存放图像相比其他方式性能更为优越,这也是本系统所追求的。
    其他模块有采用node.js 开发,而mongodb对js有很好的支持,node.js本身就是js,所以两者的契合度较高。

    综上所述,整个系统采用node.js、python、mongodb技术栈。
    1.3 论文的主要工作内容为了实现该系统,首先得从数据库入手,设计并创建数据库是所有工作的前提。因为其他部分都是基于数据库的接口,没有事先设计好的数据库,后面的工作是无法进行的。
    接着就是编写node.js部分的代码了。在这大的一步里,先要进行配置编写这一小步,因为接口的编写依赖配置信息。完了之后进行图像接口的编写,最后在进行xml接口的编写,后两步其实区分先后次序没有多大的意义。最后就是编写mocha测试代码,进行代码的调试。
    接着是python部分代码的编写,至于先编写node.js还是python,这都无所谓,这两部分并没有先后次序。Python部分代码编写的时候先进性配置的编写,然后进行图像接口的编写,最后进行病人隐私信息接口的编写。
    数据库的设计以及两种语言借口的设计与编写都在第四章详细介绍了。
    1.4 本章小结本章首先介绍了系统的背景,包含传统的图像存储方法、高并发环境下图像访问将会变慢等相关论述。接着详细说明了系统的意义,提到系统的实现有助于缓解高并发环境下图像访问速度变慢的问题。再者介绍了系统实现的技术栈,提到mongodb、python、node.js三种。最后介绍了论文的主要工作内容,说明系统的设计与实现的顺序。
    2 相关技术分析2.1 MongoDB数据库2.1.1 数据库简介MongoDB是一个基于分布式文件存储的数据库。由C++语言编写。旨在为WEB应用提供可扩展的高性能数据存储解决方案。
    MongoDB是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。他支持的数据结构非常松散,是类似json的bson格式,因此可以存储比较复杂的数据类型。Mongo最大的特点是他支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。
    2.1.2 BSON存储格式BSON是一种类json的一种二进制形式的存储格式,简称Binary JSON,它和JSON一样,支持内嵌的文档对象和数组对象,但是BSON有JSON没有的一些数据类型,如Date和BinData类型。
    BSON可以做为网络数据交换的一种存储形式,这个有点类似于Google的Protocol Buffer,但是BSON是一种schema-less的存储形式,它的优点是灵活性高,但它的缺点是空间利用率不是很理想,
    BSON有三个特点:轻量性、可遍历性、高效性
    {“hello”:”world”} 这是一个BSON的例子,其中”hello”是key name,它一般是cstring类型,字节表示是cstring::= (byte*) “/x00” ,其中*表示零个或多个byte字节,/x00表示结束符;后面的”world”是value值,它的类型一般是string, double, array, binary data等类型。
    2.1.3 Gridfs存储格式数据库支持以BSON格式保存二进制对象。 但是MongoDB中BSON对象最大不能超过16MB。 GridFS 规范提供了一种透明的机制,可以将一个大文件分割成为多个较小的文档。这将容许我们有效的保存大的文件对象,特别对于那些巨大的文件,比如视频。

    GridFS 用于存储和恢复那些超过16M(BSON文件限制)的文件(如:图片、音频、视频等)。
    GridFS 也是文件存储的一种方式,但是它是存储在MonoDB的集合中。
    GridFS 可以更好的存储大于16M的文件。
    GridFS 会将大文件对象分割成多个小的chunk(文件片段),一般为256k/个,每个chunk将作为MongoDB的一个文档(document)被存储在chunks集合中。GridFS 用两个集合来存储一个文件:fs.files与fs.chunks。每个文件的实际内容被存在chunks(二进制数据)中,和文件有关的meta数据(filename,content_type,还有用户自定义的属性)将会被存在files集合中。

    2.2 传统图像存储方法介绍图像作为数据的一种也需要被存储起来以备后面的访问,目前业界存储图像有一下两种做法:

    把图像直接以二进制形式存储在数据库中,一般数据库会提供一个二进制字段来存储二进制数据,比如mysql中有blob字段,oracle数据库中是blob或bfile类型。这种方法缺点,一方面增加了数据负担,二方面代码处理也比较复杂。
    图像存储在磁盘上,数据库字段中保存的时图片的路径。

    互联网环境中,大访问量,数据库性能很重要。一般在数据库存储图片的做法比较少,更多的是将图片路径存储在数据库中,展示图片的时候只需要连接磁盘路径把图片载入进来即可。
    2.3 性能对比分析2.4 node.js对MongoDB的支持在npm上有很多用于node.js对MongoDB支持的包,如mongodb、mongoose等。本系统选用了官方驱动mongodb包,该包提供了很多对mongodb操作的接口,具体的接口说明可以查询官方提供的手册。而为了支持gridfs存储格式,特选用了gridfs包,这也可以在npm上找到,该包提供的接口集可以很方便的实现图片的读写。
    2.5 python对MongoDB的支持本系统选用了pymongo包来作为python访问mongodb的驱动,当然还有其他类型的驱动,不过这个是最常用的一个,所以就选择了这个。为了对gridfs存储格式的支持,系统选用了gridfs这个包来满足mongodb存储和访问图像。
    2.6 加密介绍本系统为了病人隐私信息的安全性,特采用AES加密标准对该数据进行了加密。
    AES中文全称是高级加密标准,是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的DES,已经被多方分析且广为全世界所使用。经过五年的甄选流程,高级加密标准由美国国家标准与技术研究院(NIST)于2001年11月26日发布于FIPSPUB 197,并在2002年5月26日成为有效的标准。2006年,高级加密标准已然成为对称密钥加密中最流行的算法之一。
    AES只是个基本算法,实现AES有若干模式。其中的CBC模式因为其安全性而被TLS(就是https的加密标准)和IPSec(win采用的)作为技术标准。简单地说,CBC使用密码和salt(起扰乱作用)按固定算法(md5)产生key和iv。然后用key和iv(初始向量,加密第一块明文)加密(明文)和解密(密文)。
    系统为了支持AES特选用了Crypto.Cipher包来进行加密解密等操作。
    2.7 本章小结本章首先介绍了mongodb数据库,其中重点说明两种存储格式,分别是BSON和GridFS。前一种是一般的数据存储方式,数据的大小不能超过4M,后者是为了存储大于4M的文件而设计的。接着粗略的介绍了传统有哪些图像存储方法,并且对比说明这些方法优缺点。接着通过数据展示了三种方式随着线程的数量变化的变化情况,最终得出本系统所采用的方式适合高并发的情况下的结论。后面又介绍了python以及node.js为了访问mongodb所使用的驱动。最后详细的介绍了系统所采用的AES加密方式。
    3 系统需求分析3.1 系统可行性分析对于每个软件项目中的问题并不都有明显的解决办法,有时候很难在预定的时间和费用之内解决这些问题,也无法用现有的技术圆满完成。如果贸然去开发,会造成时间、人力、资源和经费的不必要浪费,或者还会带来一些意料不到的问题。所以本系统在开发之前分别在经济可行、技术可行性、运行可行性、操作可行性几个方面进行了分析。
    3.1.1 经济可行性开发该软件所需的相关资料可以通过文献资料和网络进行调查采集,所需的软件系统、硬件平台等都易于获得,无需特殊工具,开发成本低,简单易实现,从经济角度来看,开发该软件经济可行。
    3.1.2 技术可行性系统涉及的任务主要有各种图像存储方案性能对比分析、数据库设计、接口设计与实现。
    性能分析方面我准备采用C++编写,实现各种存储方案的接口,然后线程的个数为参数,时间为结果,并且将结果通过图形化界面展示出来,所涉及的东西目前都是我自身具备的,所以在这方面可行。
    数据库设计方面,数据库用到的是mongodb,虽然之前并没接触过,不过网上有大量的资料和书籍以及用到的知识知识mongodb中的基础部分,而且数据库设计大学里已经学过,所以在这方面也技术可行。
    接口设计与实现,实现主要采用node.js、python两种语言,这两种语言之前并没有接触过,不过我有大量的时间去学习,所以在这方面也技术可行。
    3.1.3 运行可行性该系统是通过python、node.js、mongodb开发而成,如果要运行只要具备python运行环境、node.js执行环境、mongodb数据库即可。所以在运行上可行的。
    3.1.4 操作可行性由于该系统不是被用户直接操作的,系统所面向的用户是其他模块的开发人员,而其他模块的程序开发人员,都具备扎实的node.js、python知识,所以在操作上是绝对可行的。
    综上所述,该系统在经济、技术、运行、操作上完全可行。
    3.2 系统需求分析该系统身为医疗图像识别系统中的一个模块,主要是为其他模块提供数据支持,也就是说,其他模块通过此模块进行数据读写。通过各种需求获取手段包括向用户反复咨询、咨询老师、跟其他成员讨论,最终得出系统的需求如下:
    功能需求:

    系统可以分别满足node.js、python两种语言的接口需要。
    针对python可满足图像以及隐私信息增、删、改、查的需要。
    针对node.js可满足图像增、删、改以及xml的增、删、改、查的需要

    性能需求:

    响应时间不超过1秒
    支持2万用户同时操作数据库,保证性能不受影响

    安全性需求:

    系统对病人隐私数据进行加密
    系统自动进行数据备份,防止数据丢失造成危机
    本系统应该能够记录系统运行时所发生的所有错误,这些错误记录便于查找错误的原因

    可靠性和可用性需求:

    系统提供的接口尽可能满足方便其他模块的操作
    系统具有一定的容错和抗干扰能力,在非硬件故障和非通讯故障时,系统能够保证正常运行,并有足够的提示信息帮助用户有效正确的完成任务

    3.3 本章小结可行性分析是一个从经济、技术等多方面进行评估的过程,可行性分析的结果决定这个项目是否值得去做。在正式开发一个项目之前,都应该或者说必须对项目进行可行性分析。本章一开始先从经济可行性、技术可行性、运行可行性、操作可行性四个方面对系统进行了可行分析,结论是该项目可行。可行性分析之后就是需求,需求分析是一个反复确定用户需求的过程,只有完整、准确地对项目进行了需求分析才能做出让用户满意的作品,本章主要从功能、性能、安全性可靠性和可用性等几个方面调查用户的需求。
    4 系统设计4.1 模块结构系统由于要分别向node.js开发者以及python开发者提供数据访问接口,所以把系统根据语言的不同分为两个部分分别讨论,虽然分开了,但是很多方面还有很相似的,以下是两部分模块结构设计。
    4.1.1 node.js模块结构我们团队中有一人是开发一个平台用来跑深度学习框架,然后对比这些框架的性能,而我这个模块就是为他服务的,而他这一块需要将框架配置信息、原始图像保存进数据库以备后用,具体需要哪些操作,以下的模块图有说明。语言方面我们经过协商,最终选择了node.js。下面说道的系统配置模块,是为了便于系统某系内容的变化而设计的,如数据库的名以及字段的名字等,如果发生改变只需在修改这个文件即可,避免了全面的改动。

    4.1.2 python模块结构团队中其他人都是用python写的,其中一人负责对原始图像的处理以及分析以提取出病人隐私数据和满足深度学习的需要。另外一人是用处理好的图像进行深度学习,训练模型便于系统对图像的识别。经过分析,设计了图像管理模块用于管理各种图像以及病人隐私信息管理模块用于管理提取的病人隐私数据。而至于AES加密模块为了对病人隐私数据进行加密,防止数据泄露,造成不必要的损失。剩下的系统配置模块同上。

    4.2 功能设计与分析系统主要由两部分够成,其一是python接口集合,其二是node.js接口集合。下面将分别介绍这两部分的设计。
    1.node.js接口集合
    node.js接口集合操作的对象有Dicom医疗图像、深度学习框架配置这两个,所以根据这一点,将这一部分主要分为两部分,其次,为了减少因系统部署的环境的改变而做大量的修改,特将系统配置提取出来作为第三个模块,因此系统总的分为三个模块,以下将详细介绍这三个模块的设计。
    2.Dicom图像管理模块
    这个模块主要是为其他模块提供Dicom医疗图像操作的接口,根据需要接口的类型被设置为三种,分别是增、删、查等。
    “增”就是将图像存放到数据库,考虑到方便用户、性能两方面,增加操作主要由insertOneImage、insertImages两接口负责。这两个接口的区别是前者一次只能插入一张图像,后者可以一次插入多张图像,后者并不是前者的多次重复调用,如果这样,后者就没有单独设计的必要了,插入操作开始先要进行数据库连接,结束后要释放链接,考虑到多次插入仅需要一次连接以及一次释放,据此设计出了insertImages这个接口。不过两者在大体上结构上是相同的,流程图如下:

    主要代码说明:

    将图像存放进数据库
    // 创建用于Gridfs格式的对象进行Gridfs操作var gfs = grid(db, mongo); var obj = {};obj.filename = filename;obj.processed_times = 0;// 将图像从文件系统转储到数据库gfs.fromFile(obj, path + filename, function(err, file){ if (null != err){ error(); return; } else{ sucess(); // 关闭打开的数据库连接 db.close(); } });

    判断图像是否存在
    fs.exists(path + filename, function(exist){ if (exist){ // 如果存在 } else{ //如果不存在 }});

    连接数据库
    var db = new mongo.Db(config.DB, new mongo.Server(config.HOST, config.PORT, {})); // 异步打开 db.open(function(err, db){ if (err != null){ error(); return; } … }
    “删”就是将满足条件的图像从数据库中去除,满足条件的可能不止一个,所以删的时候可能存在多条被删除,这一块接口同样也是这么设计,而不是只删除满足条件的第一条。由于包Gridfs没有直接删除多条的接口,只有一个根据图像的id或者filename一次只删除一条的接口,所以这一块设计的时候,是先将满足条件的图像的id都查询出来,放在一个数组,然后一条一条的调用Gridfs提供的接口去删除。删除操作由imgDelete接口负责, 接口的程序流程图如下。

    主要代码说明:

    连接到指定名称的集合
    db.collection("fs.files", {strict: true}, function (err, collection) { if (err != null) { console.log(err); db.close(); return; } …});

    删除指定条件的图像
    gfs.remove(obj, function (err) { if (null != err) { error(); console.error(err); } else { // 如果成功删除数组中的最后一张图片,则该接口执行成功,并关闭数据库 if (i == length - 1){ success(); db.close(); } }});
    “查”就是将数据库中符合条件的图像取出来,在这里会将取出的结果存储在配置文件指定的目录。查询操作是由imgFind接口负责,该接口可以一次性查询出多张图像,在进行readFile操作之前,先根据参数doc查询出所有的图像信息,然后根据id一个一个的转储数据库中的图像到指定的目录,之所以这样进行,在于readFile一次只能查询一张图像,再者为了在目中情况下提醒用户没有满足条件的图像。流程图如下。

    主要代码:

    从mongodb数据库查询出图像并存放带指定目录
    var id = docs[i]["_id"];var obj = {};obj._id = id;(function(i){ // 根据id,查找图像,并写到目录 gfs.readFile(obj, function (err, data) { if (err != null){ error(); } else{// 写到目录 fs.writeFile(config.IMAGE.FIND_RESULT_PATH + id + config.IMAGE.EXT_NAME, data, {flag: 'w'}, function (err) { if (null != err) { error(); console.error(err); } else { // 如果最后一个图像查询成功,则整个接口才成功 if (i == length - 1){ success(); db.close(); } } }); } });})(i);//循环加异步所以这块使用闭包函数
    系统配置模块
    该模块主要为其他模块提供配置信息,包括数据库名称、主机地址、端口、查询结果存储位置等。这些信息可能会因为某些原因要进行修改,这里为了便于修改,特将此部分单独分离出来作为一个模块。
    主要代码:
    module.exports = { "DB" : "foobar","HOST" : "localhost","PORT" : 27017,"XML" : {"COLLECTION_NAME" : "xml_collection", "FRIST_COL_NAME" : "framename", "SECOND_COL_NAME" : "xml_content", "FIND_RESULT_PATH" : "./", "EXT_NAME" : ".xml"},"IMAGE" : {"FIND_RESULT_PATH" :"C:\\Users\\", "EXT_NAME" : ".jpg"}}
    深度学习框架配置文件管理模块
    系统采用多个深度学习框架,需要对这几个框架进行对比,根据需要,需要将这些框架的配置文件存放到数据库中,由于框架配置文件的大小没有超过4M,所以还是采用BSON的存储格式,直接将内容以字符串的形式存放在数据库中,考虑到其他模块的需要,特设计增、删、该、查几种接口形式。
    “增”这个操作,这一块由xmlInsert接口负责,该接口在对数据库进行操作之前首先对文件是否存在进行判断,接着连接数据库,并判断对应的集合是否存在,如果不存在则创建,接下来从文件系统读取文件的数据,插进数据库当中,最后释放连接。该接口对应的流程图如下。

    主要代码:

    读取文件内容
    // 根据路径和文件名从文件系统读取内容fs.readFile(path + filename, function(err, data){ if (null != err){ error(); db.close(); return; }

    将数据插入到数据库
    // 将读取内容转换成字符串var xmlContent = data.toString();// 组织文档var doc = {};doc[config.XML.FRIST_COL_NAME] = framename; doc[config.XML.SECOND_COL_NAME] = xmlContent;// 插入到数据库result.insertOne(doc, function(err, r){if(err != null){ error(); db.close(); return; } else{ success(); db.close(); return; } });
    “删”就是根据框架名称删除数据库中对应的文档,删除操作这里是由xmlDelete接口负责,该接口的实现原理是读取用户要删除的框架名称,然后根据名称调用驱动中提供的deleteOne接口删除,不过在删除之前,进行了查询操作,目的是判断要删除的内容是否存在,对于deleteOne接口,如果不存在不会有什么反应,所以对于用户来讲,就无法了解是否已经删除,而增加查询操作,就是为了在对应内容不存在的时候,提示用户。流程图如下。

    主要代码:

    根据条件删除
    // 组织删除的条件var query = {};query[config.XML.FRIST_COL_NAME] = framename;// 根据条件删除collection.deleteOne(query, {}, function(err, result){ if (null != err){ error(); db.close(); return; } else{ success(); db.close(); return; } });
    “查”就是根据框架名称查询出对应的框架配置内容,这里根据需要提供了xmlFind接口来负责这个操作,xmlFind查询的结果会自动写入到特定的目录当中。接口流程图如下。

    主要代码:

    查询并写入到指定目录
    var query = {};query[config.XML.FRIST_COL_NAME] = framename; // 根据query查询collection.findOne(query, {}, function(err, doc){ if (null != err || doc == null){ error(); db.close(); return; } //写入到指定目录 fs.writeFile(config.XML.FIND_RESULT_PATH +framename+config.XML.EXT_NAME, doc[config.XML.SECOND_COL_NAME], {flag: 'w'}, function (err) { if (err) { error(); db.close(); return; } else { success(); db.close(); return; } }); });
    2.python接口集合
    Python接口集合管理的对象有Dicom医疗图像以及病人隐私信息,根据这一点首先将系统分为两部分,再者为了保证系统的环境发生改变时不用大面积的去修改,特增加第三个模块配置模块,最后为了保证病人信息的安全,增加了AES加密模块。综上,系统总共分为四个模块,以下将详细介绍这四个模块的设计。
    Dicom图像管理模块
    这个模块跟node.js部分基本相同,不过还有些差别,差别在于管理的图像在原来原始图像的基础增加了处理过得图像,所以在接口的设计方面也会发生改变。根据需要这部分接口被设置为三个类型:增、删、查,下面将详细介绍这每种接口的设计。
    “增”即向数据库中添加图像,在这里并没有设计多个接口,而仅有InsertImage一个接口负责。其接受一个参数,这个参数为Image的对象,这个对象里封装着图像的相关信息,包括图像的名称、路径、处理次数、原始图像id等。至于为什么采用一个接口,而不像node.js部分设计多个接口而适应各种情况,因为在这里并没有出现node.js部分的问题,调用这一个接口多次插入不会影向插入的性能的,这得感谢pymongo包的底层设计,它会在适当的时候创建连接以及在适当的时候释放连接,上层不需要做这些事情。流程图如下。

    主要代码:

    读取图像数据并存放到数据库
    img = open(image.GetPath() + image.GetFilename(), 'rb')data = img.read()# 存放数据到mongodb数据库gfs.put(data,filename=image.GetFilename(),process_times=image.GetProcessTimes(),origin_fileid=image.GetOriginFileid())
    “删”即从数据库删除满足指定条件的图像,该操作由DeleteImage接口负责,DeleteImage为了保证当数据库中没有符合条件的图像时给予提示,所以在执行删除操作之前先进行了查询操作,如果没有符合条件的则抛出查询结果为空的异常。流程图如下。

    主要代码:

    查询并删除
    fileList = files.find(dict) #统计个数len = 0#根据查询图像的id,分别删除for file in fileList: len = len + 1 id = file['_id'] gfs.delete(ObjectId(id))# 如果没有查询到结果,就抛出异常if len == 0: raise NoResultException("查询结果为空")
    “查”即从数据库中查询出满足条件的图像,查询操作由FindImage接口负责,它接受一个字典类型的参数,用来表示查询条件,最后将查询的结果存放到指定的目录。查询成功返回True,查询失败返回False并抛出对应的异常,异常这块可以通过配置文件打开或关闭,需要用户自己设置。流程图如下。

    主要代码:

    查找满足条件的图像并写入目录
    # 查找满足条件的图像files = self.__db['fs.files']fileList = files.find(dict)# 用来存放fileList的长度len = 0# 分别取出图像数据并写入指定目录for file in fileList:len = len + 1id = file['_id']data = gfs.get(ObjectId(id))f=open(configs['IMAGE']['FIND_RESULT_PATH'] + str(id) + configs['IMAGE']['EXT_NAME'], 'wb')f.write(data.read())f.close()# 如果fileList没有元素,则抛出异常if len == 0: raise NoResultException("查询结果为空")
    系统配置模块
    系统配置模块如果node.js部分,主要包含系统数据库、字段、服务器地址等信息,方便以后修改。代码展示如下。
    configs = { 'DB' : { 'NAME' : 'foobar', 'PORT' : 27017, 'HOST' : 'localhost' }, 'PATIENT_COLLECTION_NAME' : 'patientInfo', 'IMAGE' : { 'FIND_RESULT_PATH' : './', 'EXT_NAME' : '.jpg' }, 'AEX' : { #'MODE' : 'CBC', 'KEY' : 'yangke' }, 'SHOWEXCEPTION' : True}
    AES加密模块
    AES加密模块主要负责病人隐私信息的加密和解密,AES只是个基本算法,实现AES有若干模式,这里采用CBC模式。Python通过Crypto.Cipher这个包实现AES加密模块代码的编写。
    主要代码:

    加密
    def encrypt(self, text):cryptor = AES.new(self.key, self.mode, self.key)length = 16count = len(text)add = length - (count % length)text = text + ('\0' * add) self.ciphertext = cryptor.encrypt(text) return b2a_hex(self.ciphertext)

    解密
    def decrypt(self, text):cryptor = AES.new(self.key, self.mode, self.key)plain_text = cryptor.decrypt(a2b_hex(text))return plain_text.rstrip('\0')
    病人隐私信息管理模块
    病人隐私数据来源于从医疗图像中提取,这些信息需要存放进数据库来进一步管理,根据需要这里提供增、删、改、查四中操作。至于这些数据怎么在数据库中存储,后面会有相应的数据库设计部分来解释。下面分别针对这四种操作详述他们的设计原理。
    “增”即向数据库中添加隐私数据,该操作InsertPatientInfo接口负责,该接口主要调用pymongo包中的insert接口实现。流程图如下。

    主要代码:

    连接集合并插入数据
    try: #如果集合不存在,插入第一文档时会自动生成 collection = self.__db[configs['PATIENT_COLLECTION_NAME']] # 插入 collection.insert(patientInfo) return True except Exception as e: if configs['SHOWEXCEPTION']: # 打印异常信息 print(e) return False
    “删”即从数据库中删除符合条件的病人隐私数据,删除操作由DeletePatientInfo接口负责,该接口接受一个字典类型的参数,用来传递要删除数据的条件,再删除之前首先判断是否存在这样的数据,如果不存在的时候提示用户,避免什么都不做。最后调用remove接口删除。流程图如下。

    主要代码:

    查找并删除病人隐私信息
    res = collection.find(dict) # res的长度len = 0for r in res: len = len + 1# 如果查找结果为空,则抛出异常if len == 0: raise NoResultException("查询结果为空") collection.remove(dict)
    “改”即将符合条件的病人隐私数据某些属性值改成其他的,修改这一操作由UpdataPatientInfo接口负责。该接口接受两个参数,分别是dict1,表示条件,dict2表示更新的目标。接口的更新操作主要有pymongo中update接口实现,流程图如下。

    主要代码:

    查询并更新病人隐私信息
    # 返回指定集合collection =self.__db[configs['PATIENT_COLLECTION_NAME']]# 查找res = collection.find(dict1)# res的长度len = 0for r in res: len = len + 1# 如果查找结果为空,则抛出异常if len == 0: raise NoResultException("查询结果为空") tmp = {}tmp['$set'] = dict2# 更新collection.update(dict1, tmp)
    “查”即从数据库中查询符合条件的病人隐私数据,查询操作由FindPatientInfo接口负责,该接口接受一个参数,用来存放查询的条件,如果查询结果为空会抛出异常,当然,用户可以通过设置配置文件来关闭或者打开异常,FindPatientInfo主要是调用pymongo包中find接口实现,流程图如下。

    主要代码:

    根据条件查询
    # 返回指定的集合collection=self.__db[configs['PATIENT_COLLECTION_NAME']]# 查找res = collection.find(dict) # res的长度len = 0for r in res:len = len + 1# 如果查找结果为空,则抛出异常if len == 0:raise NoResultException("查询结果为空")return res
    4.3 系统开发方法目前使用最广泛的软件工程方法学,包括传统方法学和面向对象方法学。
    传统方法学
    传统方法学又称生命周期方法学,或结构化方法学。传统方法学把软件生命周期的全过程依次划分为若干个阶段,然后顺序地完成各个阶段的任务。它采用结构化技术(结构化分析、结构化设计和结构化实现)来完成软件开发的各项任务。
    在这个过程中,软件文档是通信的工具,他们清楚、准确地说明了到这个时候为止,关于该项工作已经知道了什么,同事奠定了下一步的基础。
    面向对象方法学
    客观世界是由对象构成的,对象是一个属性和数据操作的封装体。数据和对数据的处理原本密切相关的。
    传统方法学把数据对数据的操作人为地分离成两个独立的部分,要么面向数据,要么面向对数据的操作。而面向对象方法是把数据和对数据的操作紧密的结合起来的方法。
    本系统开发采用的是传统的方法学即结构化开发方法。原因在于这个开发方法更接近人的常规思维。
    4.4 定义规范代码规范在程序开发过程中是很必要的,一般一个项目都是多个人开发,如果代码不遵循一定的规范,这将给后面阅读和维护你代码的开发人员造成极大的困难,从而浪费不必要的时间。本系统虽然从头到尾都是自己一人开发,但还是要遵循严格代码规范。因为平时开发中注意这些,以后就不会犯这种错误。
    4.4.1 代码注释规范1.源文件注释
    所有头文件开头都要加注释,写明文件创建时间、作者、用途、概述等。格式如下所示:
    /* * 作者:XXX * 功能说明:XXXXXXXXXXXXXXXXX*/… 作者:XXX 功能说明:XXXXXXXXXXXXXXXX…
    2.函数注释
    所有函数一定要注明函数的作用、参数的作用、返回值的作用等。格式如下所示:
    /* 函数的作用:XXXX * @param:XXX * @return:XXX */…函数的作用:XXXX@param:XXX@return:XXX…
    3.常量变量注释
    所有的常量和变量,无论全局或者局部只要在代码中起关键作用的必须都加上注释。格式如下所示:
    // 变量或常量作用或意义# 变量或常亮作用或意义
    4.4.2 命名规范采用驼峰命名法。

    类命名:英文,单词首字母大写,剩余字母小写。在意义上表达该类的作用。函数命名:英文,首字母大写,剩余字母小写。在意义上表达该函数的作用,如:InsertImage,是插入图像。变量命名:英文,首字母小写,在意义上表达该变量保存值得类型。文件命名:英文,首字母小写,在意义表达该文件的类型以及作用。
    4.4 本章小结本章首先从整体的角度分别介绍node.js、python的模块结构,接着就是本章最核心的部分“功能设计与分析”,针对这两部分的每个模块都进行了分析与介绍。对于每个模块中的接口的设计与流程以及主要代码都进行了说明。最后介绍了系统所定义的规范,来约束系统的设计与代码的编写。
    5 系统测试5.1 程序调试软件完成设计之后就进入了用语言实现的阶段,这一阶段主要是靠程序员手工编写,过程中难免会因为各种原因出现语法或者语义上的错误,出现了错误就需要去修正,这个修正的过程称为调试,调试的方法有很多,如观察错误提示、打断点单步执行、肉眼观察等。本系统在调试过程中采用的时候第一种,根据错误提示修正错误。在我看来,每种调试方法各有优缺点,在特定的场合某种方法可能比较优秀。不过由于对语言的不熟悉,所以全程采用一种固定的方法调试。
    5.2 工具的测试5.2.1 测试的目的及意义测试的目的及意义主要有一下几点:

    验证软件是否满足软件开发合同或项目开发计划、系统/子系统设计文档、软件需求规格说明书、软件设计说明书和软件产品说明等规定的软件质量要求。通过测试,发现软件缺陷。为软件产品的质量测量和评价提供依据。
    5.2.2 测试框架1.node.js代码的测试框架
    这部分测试选用的mocha框架。Mocha是一款功能丰富的javascript单元测试框架,它既可以运行在node.js环境中,也可以运行在浏览器环境中。Javascripte是一门单线程语言,最显著的特点就是有很多异步执行。同步代码的测试比较简单,直接判断函数的返回值是否符合预期就行了,而异步的函数,就需要测试框架支持回调了、promise或其他的方式来判断测试结果的正确性。Mocha可以良好的支持javascripte异步单元测试。Mocha会串行地执行我们编写的测试用例,可以在将未捕获异常指向对应用例的同时,保证输出灵活准确的测试结果报告。
    2.python代码的测试框架
    这部分测试选用的是unittest框架,unittest是python内置的标准类库,是其他测试框架、测试工具的基础。对于这部分的单元测试,每个函数作为一个模块,分别测试,根据需要每个接口设计三个测试用例,两正一反来测试接口的正确性。测试代码根据接口管理的对象分为两部分代码来编写。
    5.2.3 测试步骤虽然该系统规模比较小,模块比较少,不过为了熟悉软件测试方法,以及如果系统某些地方发生错误能及很快很方便的定位,系统根据需要在测试阶段是按照单元测试、集成测试、系统测试、验收测试这个顺序进行的。
    首先是单元测试是系统内部测试,该阶段主要是测试各个模块是否满足当初的设计。例如加密模块,主要测试该模块是否能正常加密解密而不出错。
    接着是集成测试,集成测试主要测试模块间以及模块与现有系统接口之间是否能正常协作。
    然后是系统测试,系统测试阶段将所有的模块拼接在一起,根据最初的设计通过软件测试方法验证整个系统是否满足需要。
    最后是验收测试,本系统主要是为其他模块提供数据支持,验收测试的时候主要是其他成员检查当前系统是否满足自己模块的需要。
    5.3 测试用例设计测试的对象分为两部分,一个是python部分,一个是node.js部分,针对这两部分分别设计测试用例,不过由于两部分操作的相似性,测试用例也基本没什么差别。在每个部分项目代码下面有一个test目录,里面存放着该部分的测试代码以及测试所用的数据。
    Node.js部分:

    代码展示:
    // 测试image操作函数的正确性describe("image", function(){ // 测试insertOneImage接口的正确性 describe("#insertOneImage", function(){ // 给出合法的路径和文件名来测试函数的正确性it("insert successfully when path and filename is right", function(done){ mongodb.insertOneImage(__dirname+"\\","1.jpg",function(){},function(){done();}); mongodb.insertOneImage(__dirname + "\\", "2.jpg",function(){}, function(){done();}); }); // 给出不合法的路径或文件名来测试函数的正确性 it("insert failly when path or filename is wrong", function(done){ mongodb.insertOneImage(__dirname + "\\", "4.jpg",function(){ done(); }, function(){}); }); }); // 测试insertImage接口的正确性 describe("#insertImages", function(){ // 所有的路径和文件名都合法来测试接口的正确性 it("insert successfully when path and filename of imags is right", function(done){ var images = new Array(3); images[0] = {"path":__dirname + "\\", "filename" : "1.jpg"}; images[1] = {"path" : __dirname + "\\", "filename" : "2.jpg"}; images[2] = {"path" : __dirname + "\\", "filename" : "3.jpg"}; mongodb.insertImages(images, function(){}, function(){ done(); }); }) });// 存在有路径或文件名不合法来测试接口的正确性 it("insert failly when path and filename of imags is false", function(done){ var images = new Array(3); images[0] = {"path" : __dirname + "\\", "filename" : "1.jpg"}; images[1] = {"path" : __dirname + "\\", "filename" : "4.jpg"}; images[2] = {"path" : __dirname + "\\", "filename" : "3.jpg"}; mongodb.insertImages(images,function(){done();}, function(){});});// 测试imgDelete接口的正确性describe("#imgDelete", function(){ //当给出的条件合法的时候,判断接口的正确性 it("remove successfully when condition is legal", function(done){ mongodb.imgDelete({"filename":"1.jpg"}, function(){}, function(){done();}); });// 当给出的条件不合法的时候,如,不存在这样的属性,// 判断接口的正确性it("remove successfully when condition is legal", function(done){ mongodb.imgDelete({"filename":"10.jpg"}, function(){done();}, function(){}); });}); // 测试imgDelete接口的正确性describe("#imgFind", function(){ //当给出的条件合法的时候,判断接口的正确性 it("find successfully when condition is legal", function(done){ mongodb.imgFind({"filename":"1.jpg"},function(){}, function(){done();}); }); // 当给出的条件不合法的时候,如,不存在这样的属性,// 判断接口的正确性 it("find successfully when condition is legal", function(done){ mongodb.imgFind({"filename":"10.jpg"}, function(){done();}, function(){}); }); });})// 测试xml各接口正确性describe("xml", function(){ // 测试xmlInsert接口,判断其是否正确 describe("#xmlInsert", function(){ //当参数合法时测试接口是否插入成功 it("insert successfully when param is legal", function(done){ mongodb.xmlInsert(__dirname+"\\","test.xml","abc", function(){}, function(){done();});}); // 当参数不合法时测试接口是否插入失败 it("insert failly when param is not legal", function(done){ mongodb.xmlInsert(__dirname+"\\","test.txt","abc", function(){done();}, function(){}); }); }); // 测试xmlDelete接口的正确性 describe("#xmlDelete", function(){ //当参数合法时测试接口是否删除成功 it("delete successfully when param is legal", function(done){ mongodb.xmlDelete("abc", function(){}, function(){done();}); }); // 当参数不合法时测试接口是否删除失败 it("delete failly when param is not legal", function(done){ mongodb.xmlDelete("abcd",function(){done();},function(){}); }); }); //测试xmlFind接口的正确性 describe("#xmlFind", function(){ //当参数合法时测试接口是否查询成功 it("find successfully when param is legal", function(done){ mongodb.xmlFind("abc", function(){}, function(){done();}); }); // 当参数不合法时测试接口是否查询失败 it("find failly when param is not legal", function(done){ mongodb.xmlFind("abcd", function(){done();}, function(){}); }); }); // 测试xmlUpdate接口的正确性 describe("#xmlUpdate ", function(){ //当参数合法时测试接口是否更新成功 it("update successfully when param is legal", function(done){ mongodb.xmlUpdate("abc",__dirname+"\\","test1.xml", function(){}, function(){done();}); }); // 当参数不合法时测试接口是否更新失败 it("update failly when param is not legal", function(done){ mongodb.xmlUpdate("abcd",__dirname+"\\","test1.xml", function(){done();}, function(){}); mongodb.xmlUpdate("abc", __dirname + "\\", "test2.xml", function(){done();}, function(){}); }); });});
    Python部分:

    代码展示:
    # 图像测试类,继承unittest.TestCase类,用于对图像接口的测试class ImageTest(unittest.TestCase): # unittest里特殊函数,每个测试在运行前都会执行 def setUp(self): # 创建Mongodb对象,以备其成员函数的调用 self.mongodb=Mongodb(configs['DB']['HOST'], configs['DB']['PORT'], configs['DB']['NAME']) # unittest里的特殊函数,每个测试在运行前都会执行 def tearDown(self): self.mongodb = None ''' 函数功能:测试InsertImage接口的正确性 无参数,无返回值 '''def test_InsertImage(self): # 给出合法的路径 self.assertEqual(self.mongodb.InsertImage(Image(sys.path[0] + "\\", "1.jpg", 0, "001")), True) # 给出合法的路径 self.assertEqual(self.mongodb.InsertImage(Image(sys.path[0] + "\\", "2.jpg", 0, "001")), True) # 给出不合法的路径 self.assertEqual(self.mongodb.InsertImage(Image(sys.path[0] + "\\", "5.jpg", 0, "001")), False) ''' 函数功能:测试DeleteImage接口的正确性 无参数,无返回值 ''' def test_DeleteImage(self): # 条件合法即存在这样的图像满足条件 self.assertEqual(self.mongodb.DeleteImage({"filename":"1.jpg"}), True) # 条件合法 self.assertEqual(self.mongodb.DeleteImage({"filename":"2.jpg"}), True) # 条件不合法 self.assertEqual(self.mongodb.DeleteImage({"filename":"5.jpg"}), False) ''' 函数功能:测试FindImage接口的正确性 无参数,无返回值 ''' def test_FindImage(self): # 条件合法 self.assertEqual(self.mongodb.FindImage({"filename":"1.jpg"}), True) # 条件合法 self.assertEqual(self.mongodb.FindImage({"filename":"2.jpg"}), True) #条件不合法 self.assertEqual(self.mongodb.FindImage({"filename":"5.jpg"}), False) ''' 函数功能:测试FindImageById接口的正确性 无参数,无返回值 '''def test_FindImageById(self): # id合法 self.assertEqual(self.mongodb.FindImageById("5930da46d3a3660838899992"), True) # id合法 self.assertEqual(self.mongodb.FindImageById("5930dad1d3a36621846fdc6f"), True) # id不合法 self.assertEqual(self.mongodb.FindImageById("5930dad1d3a36321846fdc6f"), False)# 图像测试类,继承unittest.TestCase类,# 用于对病人隐私信息接口的测试class PatientInfoTest(unittest.TestCase): # unittest里特殊函数,每个测试在运行前都会执行 def setUp(self): # 创建Mongodb对象,以备其成员函数的调用 self.mongodb=Mongodb(configs['DB']['HOST'], configs['DB']['PORT'], configs['DB']['NAME']) # unittest里的特殊函数,每个测试在运行前都会执行 def tearDown(self): self.mongodb = None ''' 函数功能:测试InsertPatientInfo接口的正确性 无参数,无返回值 ''' def test_InsertPatientInfo(self): # 给合法的病人隐私信息以及图像id self.assertEqual(self.mongodb.InsertPatientInfo("001", Patient({"patientId": "001"})), True) # 给合法的病人隐私信息以及图像id self.assertEqual(self.mongodb.InsertPatientInfo("002", Patient({"patientId": "002"})), True) ''' 函数功能:测试InsertPatientInfo接口的正确性 无参数,无返回值 ''' def test_FindPatientInfo(self): # 存在满足条件的病人隐私信息 self.assertNotEqual(self.mongodb.FindPatientInfo({"patientId":"001"}), False) # 存在满足条件的病人隐私信息 self.assertNotEqual(self.mongodb.FindPatientInfo({"patientId":"002"}), False) # 不存在 self.assertEqual(self.mongodb.FindPatientInfo({"patientId":"005"}), False) ''' 函数功能:测试InsertPatientInfo接口的正确性 无参数,无返回值 ''' def test_UpdataPatientInfo(self): # 存在满足条件的病人隐私信息 self.assertEqual(self.mongodb.UpdataPatientInfo({"patientId":"001"}, {"patientName":"zhangsan"}), True) # 存在满足条件的病人隐私信息 self.assertEqual(self.mongodb.UpdataPatientInfo({"patientId":"002"}, {"patientName":"lisi"}), True) # 不存在 self.assertEqual(self.mongodb.UpdataPatientInfo({"patientId":"006"},{"patientName":"wangwu"}), False) ''' 函数功能:测试InsertPatientInfo接口的正确性 无参数,无返回值 ''' def test_DeletePatientInfo(self): # 存在满足条件的病人隐私信息 self.assertEqual(self.mongodb.DeletePatientInfo({"patientId":"001"}), True) # 存在满足条件的病人隐私信息 self.assertEqual(self.mongodb.DeletePatientInfo({"patientId":"002"}), True) # 不存在 self.assertEqual(self.mongodb.DeletePatientInfo({"patientId":"007"}), False)
    5.4 测试数据在每一个功能完成之后都会进行分模块的功能测试,以确保这个功能模块的正常运行,同时也是为了方便之后功能的整合。通过选择某些具有代表性的数据和某些边界数据来测试可以很好的测试程序的健壮性,以保证程序的良好运行。下表是本系统中选择某些主要功能所做的测试以及出现的问题和解决方案,以及应用此解决方案后产生的效果。



    功能模块
    测试数据
    测试出现过的问题
    解决方法
    结果




    插入模块
    合法和非法的图像或者xml的路径
    插入失败
    根据错误和异常修改代码
    接口正常


    查找模块
    合法和非法的查询条件


    接口正常


    删除模块
    合法和非法的删除条件
    删除失败
    根据错误和异常修改代码
    接口正常


    更新模块
    合法和非法的被更新的限制条件以及更新目标
    抛出异常
    根据错误和异常修改代码
    接口正常



    5.5 本章小结本章详细介绍了系统的测试,首先从调试的方法以及意义的角度介绍了程序的调试。然后介绍了测试的目的以及意义,紧接着又介绍了mocha、unittest两种单元测试框架。后面又具体详述了系统的基于这两种框架测试代码的编写,以及系统所采用的测试数据。
    6 结 论“医疗图像库在线存储与检索平台的设计与实现”作为我的毕业设计题目即将接近尾声,伴随它的也有我的大学生涯,在这里将它比作大学,我觉得挺恰当的,因为,他们都给予了我很多的知识。当然,这仅仅知识一方面,还有很多的相似性,剩下的自己体会。
    这个题目,最初理解是做一个可以提供查询、上传操作类似于网站那样的平台。其实不然,简单来说,其实是要实现一个集合,一个可以提供其他模块增、删、改、查操作的接口集合。
    这个系统在开发过程中采用了pymongo、mongodb、gridfs等几种包,这些包都是python、node.js官方所提供的包,其实还有其他的包可供使用,之所以选择了前者,是因为前者有完整的文档资料以及用户使用案例,这些都在我开发过程中起着很大的作用。
    该系统根据语言的不同被分为两个部分,分别进行开发,两部分的模块划分基本是一样的。主要包含图像管理、系统配置模块、框架配置文件管理模块、病人隐私信息管理模块以及加密模块。在实现上图像管理模块花了比较长的时间,特别是用node.js这样的异步语言去实现。
    整个系统在开发过程中都是我一个人负责的,包括最初的需求调查以及后面的设计、实现、测试。系统所采用的技术没有一样是之前接触过的,准确点来说node.js因为采用的是js语法还稍微能熟悉点,不过也就一点。所以最大的收获就是提高了自己学习陌生的东西开发系统的能力,这一点在程序员这个行业是很有必要的。
    毕业设计这次机会是好的,学校基于了充足的时间去做这个东西,本来想着做的更好,但由于特殊的原因耽误了很多的时间,在这一点上是比较遗憾的。以后,针对每次开发或者写代码我都会加倍努力,原因只有一点,要对的起自己的兴趣。
    7 参考文献[1] 王金龙, 宋斌, 丁锐. Node. js: 一种新的 Web 应用构建技术[J]. 现代电子技术,2015, 38(6): 70-73.
    [2] 谢华成, 马学文. MongoDB 数据库下文件型数据存储研究[J]. 软件, 2015 (11):12-14.
    [3] 牛倩. MongoDB 数据库中自动分片技术应用研究[J]. 数字技术与应用, 2016(6):112-112.
    [4] 李鹏. Node. js 及 MongoDb 应用研究[J]. 天津科技, 2015 (2015年 06): 34-36,39.
    [5] Åkesson, Anders, and KennethLewenhagen. “Node. js in Open Source projects on Github: A literaturestudy and exploratory case study.” (2015).
    [6] 骆文亮. Node. js 服务器技术初探[J]. 无线互联科技, 2014,3: 178.
    [7] 高飞, 何利力, and 高金标. “基于 Node. JS 内存缓存的 Web 服务性能研究.”工业控制计算机 11 (2015): 047.
    [8] 王金龙, 宋斌, and 丁锐. “Node. js: 一种新的 Web 应用构建技术.” 现代电子技术 38.6 (2015): 70-73.
    1 评论 5 下载 2020-09-11 10:38:19 下载需要12点积分
  • 基于Node.js的医药搜索平台网站设计与实现

    摘 要随着科技的快速发展, 越来越多的医药公司积攒了大量的医药文档。这些文档资源如何高效、快速的被管理员管理,以及被用户检索,如何统一的实现资源管理与资源开放,成为了当下医药公司待解决的问题。
    普通的、零散的、单一的文档管理方式已经不能满足企业的要求,企业需要的是综合、协同、集成化的文档搜索解决方案。构建基于Node.js的医药搜索平台,实现对文档的添加、删除、编辑等常见的操作,以及对大规模数据的快速检索匹配,用户对文档的查看权限等功能。为了解决如上问题,需要使用文档管理系统来对文档进行管理,如果文档是面向用户的还需要添加用户管理系统,权限系统等相应的辅助系统。
    建立文档管理系统的目的就是要实现对文档的集中存储和管理,从而可以很好地保证文档的存储安全,提高文档的安全访问级别,很好地实现文档的分发、查询、共享,提高企业的文档管理和使用效率。在Internet环境下,我们设计的新型文档管理系统的体系结构采用B/S结构。本地用户可以通过企业内部网络直接进入文档管理系统,当然也可以进入企业的其他的业务系统。移动办公用户以及企业的客户可以通过门户站点访问到本系统,分支机构以及企业的合作伙伴可以通过Web服务方式建立与系统的连接。
    本文最终实现一个基于Node.js的医药文档搜索平台,其中包括用户管理模块、文档管理模块、系统设置模块等多个子模块。并利用Sphinx和MYSQL实现了具有分词功能的多语言文档搜索引擎。前端利用Angular实现了Single Page Application。本系统现在已经在阿里云服务器上部署应用,并上线。
    关键词:Node.js,Sphinx,Angular.js,ORM
    ABSTRACTWith the rapid development of technology, more and more pharmaceutical companies accumulate a large number of medical documents. These documents how resource efficient, rapid management by an administrator, and retrieved by the user, how to achieve a unified resource management and resource opened as the current problems to be solved pharmaceutical companies.
    Ordinary,fragmented, single document management methods can not meet the requirements of enterprises, enterprises need a comprehensive, coordinated, integrated document search solutions. Construction Node.js based medical search platform, to add to the document, delete, edit and other common operations, and rapid retrieval of large-scale data matching the user permission to view the document on Zen and other functions. In order to solve the above problem, we need to use document management system to manage the document, if the document is a user-oriented management systems also need to add a user, system privileges and other appropriate assistance systems.
    The purpose of establishing the document management system is to achieve the centralized document storage and management, which can ensure the safe storage of documents, improve security access level of the document, to achieve a good distribution of the document, query, share, and improving document management and efficiency. In the Internet environment, the architecture we designed a new document management system using B / S structure. Local users can go directly through the corporate intranet document management system, of course, can also enter other business systems business. Mobile office users and corporate customers can access the system through the portal, branch offices and business partner scan establish a connection with the system through a Web service.
    In this paper, the ultimate realization of a medical document search based Node.js platform,including user management module, document management module, system settings module multiple sub-modules. And using Sphinx and have realized the word MYSQL function multi-language document search engine. Angular use front-end to achieve a Single Page Application. The system is now deployed on the application server Ali cloud and on-line.
    Key words: Node.js, Sphinx, Angular.js, ORM
    1 绪论1.1 课题来源及研究背景本课题源于实际生产,目的是为西安泰科迈医药科技开发医药文档管理系统来优化现有的文档管理系统。
    原有的文档管理方式为手工编辑Excel,利用目录来对文档进行分类和查询。当需要修改一个文档的时候,需根据目录结构进行检索,逐目录的查找文档,然后编辑后保存。
    但这样的查询有一定的局限性,如只能根据文件的名称进行字典排序,或者根据文档的关键词来进行分类。
    无论哪种的方法都不适合大量的文档管理,如当文档的名称被修改后,需要根据名称进行重新的排序,或者一个文档的关键词被修改后,需要根据新的关键词来重新分类,或者当一个文档有多个关键词的时候,分类方式变的极其复杂。
    基于上述的缺点和不足,我们将实现一个医药文档搜索和管理平台。利用程序来实现常见的文档操作,比如文档的上传、下载、修改、搜索、查看等,因为文档是面向用户的,用户分为游客、普通用户、管理员,每个用户对文档都有不同的操作权限,所以我们还需要实现角色权限系统。
    1.2 技术栈的选择系统需要分别实现前端和后端。
    在后端框架上,我们有很多选择,比如PHP下的Yii、ThinkPhP等框架都很流行,Java下也有 Spring、Play 等框架,还有近两年比较火的Node.js。在本系统中我选择了使用Node.js来搭建后端系统,具体有如下原因。

    由于常见的操作,均为I/O 型操作,所以使用异步非阻塞I/O模型和事件编程可以提高系统的吞吐量。相比于PHP或者Java,Node.js天生就支持异步非阻塞I/O模型和事件编程。所以这里Node.js 更加合适。本系统将运行在单核服务器上,考虑到系统的资源有限,由于Node.js为单线程的,所以很适合在单核CUP上跑,相比于PHP或者Java, Node.js由于不需要频繁的线程切换,加上内置的Event Loop机制[1],所以Node.js在此环境下更加快。考虑到是一个人开发前后端,使用JS全栈开发效率更高。
    前端的开发上,我选择使用Angular.js 来进行SAP开发,由于我们开发的是医药文档搜素和管理平台,并不需要支持搜素引擎友好,并且前端逻辑页面逻辑比较复杂,所以选择Angular.js来进行单页面Webapp 开发。
    总上所述我最终选择了使用Mean栈来进行开发,Mean栈包括MySQL、Express、Angular.js 、Node.js。
    1.3 论文主要工作内容为了实现本系统,我们首先的从实现数据层开始,提供良好的数据层接口,可以保证我们后期实现Model层时更加的快速和稳定。
    接下来我们要基于数据层实现用户管理模块和角色权限模块的接口,因为网站是面向用户的,所以基本上所有功能都依赖于用户。将用户模块和角色权限模块放在第二步实现,会保证我们后期的其他模块实现起来更加方便。
    然后我们就可以建立文档模块,文档模块主要有文档的上传,修改,删除,搜索等功能。在第四章中,我们会详细介绍到如何将原始数据Excel解析并存储到MySQL中,以及如何从MySQL中将数据还原到Excel中。还有文档项权限的设置等技术。
    文档的搜索,我是基于Sphinx进行开发的,所以在第五章我会详细的讲解如何配置和使用Sphinx来提供多语言全文搜索功能。
    当我们将后端服务器全部构建完毕后,我们便可以编写前端代码。前端我是基于Angular.js 构建的SPA 应用。我会在第6章介绍利用Angular实现本系统前端界面的核心代码。
    2 系统数据层的设计与实现2.1 基于ORM实现的ActiveRecord在MVC的开发模式中,我们通常需要一个model层用于对数据的抽象。
    ActiveRecord是一种领域模型模式,特定是一个模型对应关系型数据中的一个表,而模型类的一个实例对应表中的一个记录。在数据库中,不同的表之间往往通过外键来关联。ActiveRecord中通过对象的关联和聚集来实现这种关系映射。
    这样做的好处是我们可以将数据抽象为对象。从而更加直观和方便的进行数据操作,也方便后期的维护。
    在node的中Express框架中并没有实现数据层,所以我么需要借助其它工具来实现本系统中的ActiveRecord。
    2.2 用Bookshelf.js来构建数据层基类Bookshelf.js[4]是Node.js中的一个ORM框架。建立在 KnexSql生成器上。同时实现了Promise以及传统的Callback调用方式。并支持transaction、一对一关系、一对多关系、多对多等数据映射关系。它可以很好的与PostgreSQL、MySQL、以及SQLite工作。
    Bookshelf.js基于knex.js开发,所以关于数据库的链接需要使用Knex来进行操作。
    Knex 数据库的链接代码如下:
    var knex = require(‘knex’)({ client: ‘mysql’, // 数据库类型 connection: config.mysqldb // mysqldb中包含数据库的地址,帐号和密码等信息});
    接着在Bookshelf的初始化中加入之前创建的Knex对象便可完成基类ActiveRecord的创建, 实例代码如下:
    varmarkBookshelf = require(‘bookshelf’)(kenx);markBookshelf.Model= markBookshelf.Model.extend({ // 在此添加私有方法}, { // 在此添加静态方法});
    由代码我们可以看出,我们将之前创建好的knex对象交由bookshelf工厂函数,生产一个markBookshelf基类,之后我们可以通过使用markBookshelf.Model的extend方法扩展Model。extend方法接受两个参数,第一个参数对象中的所有方法和值会变为Model的私有方法,第二个参数中的所有对象和值会变成Model中的静态方法。之后我们在建立更多得model的时候只需要继承markBookShelf基类便可以。
    2.3 示例:构建用户 Model目前系统数据层的models如图2-1所示:

    有了上面的markBookshelf基类后,我们便很容易生成一个数据模型,我们这里将建立一个user model,代码如下所示:
    var User = markBookshelf.Model.extend({ table: ‘User’, // 和数据库中的user表名相对应 initialize: function() { /* 实例初始化的时候会调用 */ }, toJson: function() { /* 序列化对象 */ } // 我们可以根据需求天假更多的私有方法或静态方法});var user = new User({id: 1});user.set({username: ‘markstock’});user.save();
    我们可以很简单的就创建一个user model,后续对user表的操作不必在写SQL语句进行操作,只需要实例化一个User对象便可完成所需的操作。
    3 角色权限系统与用户管理模块的实现3.1 基于角色的访问控制基于角色的访问控制[6] (RBAC, Role-Based Access Control) 有效的克服了传统访问控制技术中的不足之处,是当今广泛流行的访问控制技术模型之一。
    在角色访问控制中引入了角色这一个概念[7]。它的基本思想是将访问的权限分给不同的角色,在将角色划分给不同的用户。用户的每个操作的权限检查,其实是通过用户所拥有的角色是否拥有该权限来确定的。角色的不同,访问权限也不同。这样整个访问控制工作分为两个部分,即访问权限与角色的关联,角色与用户的关联。从而实现了用户与访问权限的逻辑分离。
    在角色访问控制系统中,每个用户的权限都不可自主定义,权限仅受限于用户所拥有的角色,一个用户只能同一时间拥有一个角色。拥有同一角色的用户拥有同样的权限。正因为角色访问控制系统有这样的限制,所以它不适合用来设计复杂的权限系统。
    3.2 权限控制权限控制可以简单的描述为WWH操作[8],即“Who 对 What 进行了How的操作”的逻辑表达式是否为真的检验。它的基本任务是防止一个合法用户对系统资源的非法访问和使用。它可以约束一个用户在系统中可进行的操作。在本系统中,我们将实现一个canI的函数来对WWH进行检验。如果用户没有权限便返回422提示用户没有权限进行当前的操作。
    3.3 用户动态权限数据表的设计
    如图3-1所示,要构建角色权限访问控制,我们至少需要5个表。
    Table permission 我们用来定义所有的权限,它包括权限的类型和具体的权限。部分数据如下表所示:

    如其中的第一行所示,user为what,browse为how,即验证用户是否有权对user进行browse操作。
    Table roles 用来定义我们的角色, 本系统目前有三个角色。

    因为permissions和roles 为n对n的关系,roles和users为n对1的关系,所有我们需要permissions_roles 和roles_users两个中间表来纪录关系。
    3.4 用户权限动态分配功能实现因为我们是使用ActiveRecored来做数据库的表和对象的映射,所以我们可以将角色权限系统中的表根据我们在第一章创建的ActiveRecord基类来创建模型类。我们可以使用如下代码创建Permission model。
    var Permission = markBookshelf.Model.extend({ tableName: ‘permission’, roles: function roles() { return this.belongsToMany(‘Role’); }});
    如上述代码所示,我们创建了一个Permission类,在bookshelf.js中,当有Many to Many 或者 One to Many的表间关系时[10],我们并不需要生成中间表的类,比如roles_users表和permissions_roles表,我们只需在Permission中指出需要关联的表,比如上述代码有一个roles方法,其中调用了belongsToMany(‘Role’)说明Permission表和Roles表是多对多的关系。
    根据如上我们可以创建Roles类。因为roles表和permission表为多对多的关系,roles表和users表为多对一的关系。所以我们需要在Role的模型中加入users和permissions方法。
    var Role = markBookshelf.Model.extend({ tableName: ‘roles’, users: function users() { return this.belongsToMany(‘User’); }, permissions: function permissions() { return this.belongsToMany(‘Permission’) }});
    在系统启动的时候我们需要将所有的permission从数据库查出,并加载进内存,这样每次查询权限的时候我们便不需要查询数据库,只需要从内存中读出permission信息便可以,实现代码如下。
    var actionMap = {};var init = function() { return models.Permission.findAll().then(function(perms) { _.each(perms.models, function(perms) { var actionType = perm.get(‘action_type’), objectType = perm.get(‘object_type’); actionMap[actionType] = actionsMap[actionType] || []; }); });};
    models.Permission.findAll为查找出所有permission表中的数据,之后的then为promise风格的异步调用,当findAll成功调用后,调用then中的回调函数。
    每次需要检查用户是否有相应的操作权限时,我们需要根据用户的user_id来查找其的role然后根据role来判断其是否有相应的操作权限,实现的伪代码如下所示。
    canI(obj_type, action_type, user_id) { var user = Get user from User Class with user_id; var role = user.getRole(); return check if user with role can do this action_type;}
    3.5 用户管理模块的实现
    有了前面的数据模型和方法我们可以很方便的创建出用户的增删改除等功能。这里以实现后台管理员查看用户列表为例。管理员可以在后台管理页面中查看所有的用户列表,并以分页的形式展现结果。
    如下函数将会根据提供的搜素请求获取用户的分页数据信息。
    function doQuery(options) { models.User.findPage(options); }
    options中包含了当前页page,以及每页显示的数量num。查找的原理是基于如下的SQL语句(伪代码)的来实现的:
    SELECT * FROM user limit page * (num - 1), num;
    用户管理界面如图3-4和图3-5所示。

    如图为后台管理页面中的用户管理页面,管理员可以根据用户的邮箱、昵称、或者角色查询用户,管理员可以修改其他用户的信息,或者删除其他非管理人员。
    对于前台页面在实现了用户模块后,我们可以实现登陆,注册,用户账号设置等功能。



    用户登陆窗口
    用户注册窗口









    4 文档的存储与文档项权限的设计4.1 文档管理功能描述原有的文档是通过Excel进行存储,通过目录来进行分类。因为要建立搜索功能,我们取消分类的功能,用户可以通过关键词来搜索到相关的文档。
    管理员可以通过后台来编辑文档、查看文档、搜索文档、以及重新上传文档。
    对于文档的搜索功能,我们需要实现可以进行中文搜索,英文搜索,以及中英文分词搜索的功能,而且我们还需要可以查出所有文档的接口来供管理员使用。对于搜索的结果我们要做分页处理,用户可以选择每页显示20项或者40项结果,默认以20条方式呈现。
    每个文档的每一项我们称为一个文档项。文档项有多种存储类型,具体如下所示:
    Image
    此类型用来标记当前项目用来存储图片,每个文档都可以存储多张药品图片,来供展示。管理员需要可以上传和删除图片。
    Text
    纯文本类型,如药品的介绍、规格、生成常见等都是纯文本存储。
    Text + En
    对于药品的通用名、商品名均有中英文两种表示方式,所以此项用来分别存储中文和英文信息。
    Download
    有些项目可以用来存储其它文档资料,比如PDF,或者word等格式的文件来供用户下载,所以此项用来存储其它文件。
    Download + Text
    和Download项类似,都可以用来存储其它文件,但是此项还可以加上下载说明等额外的信息。
    每个文档项都有一定的查看权限,只有满足权限的用户才可以产看或下载,所以我们必须要存储每个文档项的权限,并且管理员可以更改。
    目前文档项的权限分为如下三类:

    游客可以查看或下载通用户可以查看或下载VIP用户可以查看或下载
    4.2 面向文档动态权限的数据格式数据均需要从用户上传的Excel文档进行解析,所以为了解析方便,我们对原始文档的格式进行了定义,数据格式如图4-1所示。

    对于Text 和 Text + En的类型其值直接填写进中文值和英文值中即可。
    对于Image 和 Download类型只填写名称就好,后续的文件通过后台管理平台在进行添加。
    Download + Text中的Text部分填写进中文值就好。
    因为每个文档项目均有查看和下载权限,目前有四种用户类型Guest、User、
    VIP、Administrator。所以我们赋予每中用户一定的权限值,如下所示:
    Guest = 1; User = 10; VIP = 20; Administrator = 40;每个角色只能查看或下载小于其值的文档项,比如当前用户为VIP, 则它可以查看权限值等于1、10、20的文档项。
    对于数据库中的存储格式,我们选择利用JSON字符串进行存储,针对每种不同的文档项,都有不同的JSON数据格式。
    Image格式
    我们将图片的具体存储地址,存储在image数组中,每个图片用逗号分隔。当我们需要添加新的图片的时候,只要将图片上传到服务器上,然后将地址插入进image数组中便可。
    { “name”: “产品图片”, “image”: [], “value”: “”}
    Text
    对于Text类型的数据,我们只需要将值存进value中即可。
    { “name” : “化学名”, “role: “1”, “value”: “ 2,6-二甲基-4-( 2-硝基苯基)-1,4-二氢-3,5-吡啶二甲酸二甲酯”}
    }
    Text+ En
    对于 Text + En类型的值,我们分别存储为zhValue 和 enValue。
    { “role”: “1”, “name”: “商品名(中英文)”, “zhValue”: “肠虫清”, “enValue”: “Eskazole”}
    Download
    { “file”: { “path”: ‘path/to/the/file/on/server”, “filename”: “file name”, “fid”: “the identify of the file” }, “role”: “1” “name”: “质量标准JP”}
    对于文件的数据格式,我们将文件的存储路径存储在file.path中,fid(唯一文件标识符)用来做下载验证,避免用户非法批量下载。
    4.3 文档动态权限数据表的设计为了实现文档项可扩展的文档管理系统,我们需要两张表,一个是文档项表 Attributes , 一个是Documents表。
    因为每个文档的文档表项都大致相同,所以文档项表用来纪录所有类型的文档项。文档项表的用途,只在解析原始文档和生成新文档的时候用来保证信息的一致性。
    文档项表的设计如图4-2所示:

    attr_name 为文档项的名称,每个原始文档的每一项的名称都得与attr_name相对应,才可以被识别,否则会被忽略。Alias与documents表中得文档项别名一一对应,type为当前文档项得类型。
    部分文档项表数据如图4-3所示:

    documents表比较简单,只包括id和各个文档项得值,部分column如图4-4所示:

    4.4 功能设计及分析4.4.1 元文档的上传与解析搭建文档管理系统的第一步就是上传文档,解析原始excel数据,存储进数据库。为了更加方便的解析excel文档,我们使用node-excel插件,它会将excel解析为JSON对象。
    var filepath = req.files.file.path;try { obj = nodexlsx.parse(filepath);} catch { console.error(e);}
    我们只需将上传文件的路径地址传送给nodexlsx的parse方法,便可获取到json对象,如果失败则上传的文件无法被识别,可能不是excel文件。
    解析出原始数据,我们便可以去格式化数据,然后存储进数据库了。
    _.each(xlsxData, function(rowData) { if (rowData[0] === attr.get(‘attr_name’)) { if (attr.get(‘type’) === ‘text’ || attr.get(‘type’) === ‘download+text’) { obj.value = rowData[1]; } else if (attr.get(‘type’) === ‘text+en’) { obj.zhValue = rowData[1]; obj.enValue = rowData[2] } } insertData[attr.get(‘alias’)] = JSON.stringify(obj);});
    如代码所示,其中的xlsxData及为解析出的原始excel数据,attr为一个文档项,我们一次遍历所有的原始数据去匹配文档项,然后格式化数据。
    当所有的文档项都处理完后,我们既得到格式化后的数据,然后只要调用Document类的add方法就可以存储进数据库了。
    return models.IDocument.add(insertData, options);

    当文件的大小超过了5Mb的时候就会提升异常文件,当文件的格式不正确,无法正常解析的时候就会提示错误的格式。
    4.4.2 元文档的编辑当我们需要编辑文档的时候,我们需要先将文档项中的JSON字符串解析为对象,然后修改对象中的值,在将对象stringify化存储起来。其核心代码如下:
    attrs = attrs.models;_.each(attrs, function(attr) { if (options.data.idocuments[0][attr.get(‘alias’)]) options.data.idocuments[0][attr.get(‘alias’)] = JSON.stringify(options.data.idocuments[0][attr.get(‘alias’)]);});
    我们将修改后的对象重新stringify化后便可存储。

    4.4.3 Excel格式元文档的下载文档的下载可以看成为上传的逆过程,上传是从excel将数据解析到数据库中,下载则是将数据库中的数据解析为excel文档。因为我们在将excel中的数据解析到数据库中的时候,只解析了类型为Text 和 Text + En类型的值,但是在将数据库中的数据解析到Excel中时,我们必须要考虑Download 和 Image以及Download + Text类型的值。
    因为Excel中不能存储文件,以及node-excel不能将图片添加进excel文件中,我们将文件和图片的地址添加进Excel中。
    首先我们从数据库中获取所有的数据,并解析为数组,代码如下:
    _.each(iattributes, function(iattribute) {try { // 尝试去解析 json stringidocument.set(iattribute.get(‘alias’), JSON.parse(idocument.get(iattribute.get(‘alias’)))); if (iattribute.get(‘type’) === ‘text’) { // 填充text类型的数据 } else if (iattribute.get(‘type’) === ‘text+en’) { // 填充text + en类型的数据 } else if (iattribute.get(‘type’) === ‘download’) { // 填充download类型的数据 } else if (iattribute.get(‘type’) === ‘download+text’) { // 填充download+text类型的数据 } else if (iattribute.get(‘type’) === ‘image’) { // 填充image类型的数据 }} catch (e) { xlsxData.push([iattribute.get(‘attr_name’), ‘’]);}});
    当我们解析出数据xlsxData时,我们便可以生成excel文件,但是在发送给浏览器的时候,我们必须要设置response header, 以及将生成的excel转变为字节流传送给浏览器。具体代码如下所示:
    var wb = new Workbook(), ws = sheet_from_array_of_array(xlsxData), ws_name = ‘SheetJS’;wb.SheetName.push(ws_name);wb.Sheets[ws_name] = ws;// 生成excelvar wbout = nodeXlsx.write(wb, {bookType: ‘xlsx’,bookSST: true,type: ‘binary});// 设置 excel文件 response headerres.setHeader(‘Content-disposition’, ‘attachment; filename=’ + filename);res.setHeader(‘Content-type’,‘application/vnd.openxmlformats-officedocument.spredsheetml.sheet’);// 将文档数据转换为字节流传给浏览器res.send(new Buffer(s2ab(wbout)));
    5 基于Sphinx的检索子系统5.1 使用Sphinx和MySQL实现多国语言全文搜索如过只是依赖MySQL的like语句来实现全文搜索,那么局限性将很大,首先不能实现分词功能,比如我们以阿莫西林为关键词来进行搜索,如果使用SQL语句我们可能就要写成like ‘%阿莫西林%’ 这样。但这样我们只能查到是否有阿莫西林整个词出现在内容中,而不能匹配到“阿”,“阿莫”,“阿莫西”等词的匹配情况。
    可能“阿”,“阿莫”,“阿莫西”这样的词并不会匹配到任何结果,看不出分词的作用性。但如果用户想根据两个关键词来搜索文档,那么分词的作用性就会体现出来。
    分词系统首先会对输入进行分词,然后根据每个分词在文档中的匹配情况,计算匹配权值,结果会根据权值的大小排序进行返回。
    Sphinx是一个基于SQL的全文搜索引擎。它会从MySQL数据库或者PostgreSQL中读取数据,然后建立索引。
    首先我们要配置数据源具体代码如下:
    source medical { type = mysql sql_host = localhost sql_user = mysql-user sql_pass = mysql-user-password sql_db = med_dev sql_port = 3306 sql_query = SELECT id, kxm, spm, type, cpsy, jx, gg, sccj ,yyycg FROM documents sql_query_pre = SET NAMES utf8}
    我们在配置信息中配置了MySQL的链接信息,以及获取数据的SQL语句。在启动Sphinx后,sphinx会自动链接MySQL获取数据,然后建立索引。
    建立索引的代码如下,我们设置了最小分词长度为1,然后支持中文分词。
    Index medical { source = medical path = /var/lib/sphinxsearch/data/medical ngram_len = 1 ngram_chars = U+4E00...U+9FBB, U+3400..U+4D85 …….}
    要运行Sphinx需要两步,第一步是生成索引,第二部是启动sphinx服务器。

    建立索引:/usr/local/sphinx/bin/indexer–config /usr/local/sphinx/etc/sphinx.conf 启动服务:/usr/local/sphinx/bin/sphinx start
    5.2 文档搜索接口的实现Sphinx实现了node API 插件sphinxapi16],所以我们可以很轻松的使用node.js访问sphinx服务器。
    要连接Sphinx服务器我们首先要创建Sphinx客户端实例,具体代码如下:
    var SphinxClient = require(‘sphinxapi’), config = require(‘../config’), cl;cl = new ShpinxClient();cl.setServer(config.sphinx.host, config.sphinx.port);cl.setMatchMode(SphinxClient.SPH_MATCH_EXTENED2);
    我们使用SphinxClient创建了一个客户端实例,然后设置了sphinx服务器的地址和端口。有了创建好的实例,我们便可以通过指定的索引来进行搜索了。
    Sphinx.QueryAsync(keyword, ‘medical’).then(function(ret) { … });
    QueryAsync接收两参数,第一个为要搜索的关键词,第二个为要查找的索引,如果查找成功会调用then中的回调函数。ret中会有两个主要值,一个是ret.total_found 为查找到的总结果数,一个是ret.matches 为匹配到的结果集。
    但我们从sphinx中查找出的只是匹配的id集合,我们还需要通过id集合来从MySQL中获取真正的数据,其核心代码如下。
    _.forEach(ret.matches, function(match) { ids.unshift(‘id’ + match.id);});if (ids.length) { options.filter = ids.join(); return models.IDocument.findPage(options);} else { return Promise.resolve({ documents: [] });}
    首先我们从sphinx中查处的结果中获取所有的id,然后将id拼接为字符串如“id1id2 id3 …”, 如果ret_matches为空则直接返回空数组。
    5.3 前台搜索结果分页显示有了前面设计的接口我们便可以实现搜索RESTFUL API,如图5-1所示

    如图5-1所示我们通过浏览器发送GET请求,RequestUrl为/api/documents。为了可以获取搜索结果,我们需要添加几个请求参数。

    如图5-2所示,其中keyword为我们要搜索的关键词,因为关键词可能包含非法字符,所有在我们发送请求前,我们应该利用encodeUrlComponent函数对参数进行解析,limit为每页显示的结果数,系统默认为20个,可选数量为40个。page为当前的页数。
    访问如上的API我们便可以获取搜索结果,服务端会返回如下结果。

    其中documents为所搜索到的文档结果,meta为搜索结果附加数据,其中包括当前关键词可搜索到的结果的总数total,以及当前的页数,以及每页显示结果数。有了这些数据我们便可以编写前台分页视图。
    在前台页面中,我们只需在输入框中输入关键词,并选择当前页数便可以显示所有搜索结果,效果图如图5-4和图5-5所示。

    6 基于Angular.js实现前台SPA6.1 文档检索平台的 UX 与 SPA所谓单页面应用,指的是在一个页面上集成多种功能,甚至整个系统就只有一个页面,所有的业务功能都是它的子模块,通过特定的方式挂接到主界面上。它是AJAX技术的进一步升华,把AJAX的无刷新机制发挥到极致,因此能造就与桌面程序媲美的流畅用户体验[16]。但单页面应用也有缺点,那就是搜索引擎不友好性。搜索引擎利用爬虫来爬取页面,但是并不能解析页面中的JS代码。单页面中的页面渲染,数据的获取,以及路由全交由前端JS代码来执行,所以搜索引擎并不能收藏到网页的所有页面。
    但本系统是面向内部人士进行开放,所以并不需要支持搜索引擎友好性,所以才用SPA可以有效提高UX。
    6.2 初始化 Angular 项目Angular 有着诸多特性,最为核心的是:MVVM、模块化、自动化双向数据绑定、语义化标签、依赖注入等等。
    要使用Angular 我们必须先初始化项目,网站的前端分为两个部分一个是front端,供普通用户使用;一个是admin端,供管理员使用;
    因为Angular.js为模块化开发,所以我们设定根模块为“Medical”,front端和admin端分别继承根模块进行开发,根据路由的地址不同,加载不同的JS文件,从而有效的减少了资源的传输。路由代码如下所示:
    // 加载Admin页面app.route(‘/medi_admin_panel/*’).get(core.rednerAadminIndex);// 加载Front页面app.route(‘/*’).get(core.renderIndex);
    对于子模块的划分,如图6-1所示:

    接下来我们主要以front端为主来讲解如何使用Angular.js。Angular.js一大特点就是依赖注入,依赖注入就是一个模块要使用另外一个模块功能的时候,只要在注册的时候,将另一个模块注入进来即可。
    Medical模块为我们的根模块,它主要是用来加载一个Angular.js的核心模块,这样其自模块便可以直接使用。
    Medical的配置代码如下所示:
    var applicationModuleName = 'Medical';var applicationModuleVendorDependencies = ['ngResource', 'ngAnimate', 'ngMessages', 'ngSanitize', 'ui.router', 'ui.utils', 'ui.bootstrap', 'ngFileUpload', 'ngImgCrop', 'ngStorage'];// Add a new vertical modulevar registerModule = function(moduleName, dependencies) {// Create angular module angular.module(moduleName, dependencies || []); // Add the module to the AngularJS configuration file angular.module(applicationModuleName).requires.push(moduleName); };
    我们在applicationModuleVenderDependencis中声明了要依赖的模块。然后利用registerModule方法注册根模块即可。
    6.3 SPA路由前置因为我们搭建的是SPA应用,所以我们的路由器要定义在前端,使用angular可以很方便定义路由,比如我们需要定义documents的相关路由,我们创建一个documents.routes模块即可,用documents.routes来管理与documents相关的路由。
    在front端,我们需要定义三个关于documents的路由。

    路由一:/documents/search?*keyword路由二:/documents/view/:document-Id路由三:/documents/not-found
    路由一为当我们搜索一个文档时,我们要提供一个关键词,当有结果返回时,我们就用文档列表展示结果,例如我们搜索阿莫西林就可以访问路由http://drugago.com/documents/search/阿莫西林。当我们需要查看编号为1的文档的具体内容时,我们便可以访问http://drugago.com/documents/view/1。如果一个文档没有找到时,就会跳转到/documents/not-found。
    定于document路由的部分代码如下(我们只定义来搜索的路由,其他路由省略)。
    Angular.module(‘documents.routes’).config([‘$stateProvider’, function($stateProvider) { $stateProvider.state(‘documents-search’, { url: ‘/documents/search?*keyword’, templateUrl: ‘modules/documents/client/views/document-search.client.view.html’, controller: ‘SearchDocumentsController’ }); // 添加更多的路由});
    6.4 Service的实现与数据预加载Angular.js有一个factory函数,我们可以用它来包装resource服务来生成据源Service,当我们需要利用Ajax动态获取数据的时候,我们可以直接调用resource 的中相应的http方法。
    常见的Http方法有Get、Post、Put、Delete等。
    定义资源Service的核心代码如下,这里我们只定义了document的Service。
    angular.module('core').factory('iDocuments', ['$resource',function($resource) {var url = '/api/documents/:id'; return (function() { var defaults = { update: {method: 'PUT'}, create: {method: 'POST'}, query: {method: 'GET', isArray: false}, destory: {method: 'DELETE'}, get: {method:'GET'} }; return $resource(url, {id: '@id'}, defaults); }());}]);
    单页面应用有一个常见的缺点就是当页面加载的时候,是先加载JS文件和模版,然后在去加载数据,但是当数据还没有返回的时候,浏览器就已经改变了路由,此时会显示空白页面,当Ajax数据返回后,内容突然一下全显示出来,这在前端叫闪屏。解决闪屏的方法就是数据预加载,当我们在改变路由之前,先去加载数据,当数据返回后,浏览器才改变路由。从而解决了闪屏的问题,提高了用户友好性。
    比如我们在查看一个文档时候,我们先去预加载文档的内容,然后在做页面跳转。要实现数据预加载我们需要在定义路由的时候添加resolve方法。
    .state('admin.documents-view', {url:'/view/:documentId',templateUrl: 'modules/documents/client/views/admin/documents-view.client.view.html',controller: 'ViewDocumentsController',resolve: {// 预先加载数据 idocumentResolve: ['$stateParams', 'iDocuments', '$q', function($stateParams, iDocuments, $q) { var defer = $q.defer(); iDocuments.get({ id: $stateParams.documentId}, function(data) { defer.resolve(data); },function(err) { defer.reject(err); }); return defer.promise; }]},})
    在resolve方法中,我们调用了Document Service的get方法获取文档数据,然后直接返回promise对象。
    7 结 论Javascript是一个事件驱动语言,Node利用了这个优点,编写出可扩展性高的服务器。Node采用了一个称为“事件循环(event loop)”的架构,使得编写可扩展性高的服务器变得既容易又安全。提高服务器性能的技巧有多种多样。Node选择了一种既能提高性能,又能减低开发复杂度的架构。这是一个非常重要的特性。并发编程通常很复杂且布满地雷。Node绕过了这些,但仍提供很好的性能。
    基于Node.js的医药搜索平台设计与实现采用MEAN栈开发,使用了很多Node.js下的库,比如使用Gulp基于流自动化构建工具,使用Express框架构建web服务端,使用Bookshelf实现数据层ActiveRecord,使用Bluebird.js 优化异步流等。
    本系统大致可以划分为如下几个模块,角色权限模块、邮件模块,系统设置模块,用户管理模块,文档管理模块,搜索模块。其中角色权限模块、邮件模块、用户管理模块和系统设置模块都是一般系统中常见的模块,为了实现各个模块以及使各个模块可以很好的协同工作,在代码编写的过程中参考了很多资料,以及学习了很多编程技巧,比如更加的熟练函数式编程,以及更加的熟练Promise异步编程。同时使自己更加的了解Node.js以及web开发。
    在整个毕业设计的过程中,从最初的系统模型设计,代码实现,前台页面的设计均有一人完成。整个过程中总结了一大堆理论并转化为自己的知识,不断的学习使自己不断的接近的自己的目标--成为一名全栈工程师。虽然在系统实现的过程中,遇到很多的困难,比如底层数据层的实现,前台页面的设计,角色权限系统的设计和代码的实现,excel的解析和生成,sphinx的安装和配置,如何优化系统性能等。虽然遇到很多问题,但都自己通过查资料一个个解决。通过一个多月的不断学习和编码,最终完成了本系统的开发,并上线使用,可以访问http://www.drugago.com查看。
    通过这次毕业设计,我深深的体会到了一个完整的系统从前到后搭建起来是有多么的不易,在解决的一个又一个问题之后,对自己的技术也更加的自信,使自己有能力面对毕业后的工作。
    大学,将要在毕业设计中结束,心中多的是无尽的怀念和十分的舍不得。
    参考文献[1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop . Concurrency modeland Event Loop
    [2] http://meanjs.org/ . Open-SourceFull-Stack Solution For MEAN Applications
    [3]Marshall K, Pytel C, Yurek J. Introducing active record[J]. Pro Active Record:Databases with Ruby and Rails, 2007: 1-24.
    [4] http://bookshelfjs.org/ . bookshelf.js
    [5] http://knexjs.org/#Builder .A SQL Query Builder for Javascript
    [6] Ferraiolo D, Cugini J,Kuhn D R. Role-based access control (RBAC): Features andmotivations[C]//Proceedings of 11th annual computer security applicationconference. 1995: 241-48.
    [7] Halpin T. Object-role modeling (ORM/NIAM)[M]//Handbook onarchitectures of information systems. Springer Berlin Heidelberg, 1998: 81-103.
    [8]Halpin T. Object rolemodeling: An overview[J]. white paper,(online at www. orm. net).gadamowmebulia, 2001, 20: 2007.
    [9] Keet C M. Part-wholerelations in object-role models[C]//On the Move to Meaningful Internet Systems2006: OTM 2006 Workshops. Springer Berlin Heidelberg, 2006: 1118-1127.
    [10] http://bookshelfjs.org/#associations .booshelf associations
    [11] Cantelon M, Harter M,Holowaychuk T J, et al. Node. js in Action[M]. Manning, 2014.
    [12] Tilkov S, Vinoski S.Node. js: Using javascript to build high-performance network programs[J]. IEEEInternet Computing, 2010, 14(6): 80.
    [13] https://www.npmjs.com/package/node-excel . Simple data set export to Excel xlsx file
    [14] Suchal J, Návrat P.Full text search engine as scalable k-nearest neighbor recommendationsystem[M]//Artificial Intelligence in Theory and Practice III. Springer BerlinHeidelberg, 2010: 165-173.
    [15] Aksyonoff A.Introduction to Search with Sphinx: From installation to relevance tuning[M].” O’Reilly Media, Inc.”, 2011.
    [16] https://www.npmjs.com/package/sphinxapi.SphinxSearch Client for NodeJS
    [17] Mikowski M S, Powell J C.Single Page Web Applications[J]. B and W, 2013.
    [18] http://www.apjs.net/ .angular.js 中文网
    [19] 许会元,何利力. NodeJS的异步非阻塞NodeJS研究[J]. 工业控制计算机,2015,03:127-129.
    [20] 杨伟超,刘阳,李淑霞. 基于搜索引擎的一站式检索平台设计与实现[J].计算机与现代化,2012,11:220-222.
    [21] 王金龙,宋斌,丁锐. Node.js:一种新的Web应用构建技术[J].现代电子技术,2015,06:70-73.
    [22] Pasquali S. Mastering Node. js[M]. Packt Publishing Ltd, 2013.
    [23] Tilkov S,Vinoski S. Node. js: Using javascript to build high-performance network programs[J]. IEEE Internet Computing, 2010, 14(6): 80.
    [24] Ihrig C J.Pro Node. js for developers[M]. Apress, 2013.
    [25] Mardan A.Publishing Node. js Modules and Contributing to Open Source[M]//Practical Node.js. Apress, 2014: 261-267.
    [26] Yu-yang L I U Q P, Zi-cheng P. The Design and Implementation of BuildingWebsite Internal Search Engine Based on Sphinx [J][J]. MicrocomputerInformation, 2010, 15: 050.
    [27] 姚立.IBM云计算平台下NodeJS应用支持环境的设计与实现[D].哈尔滨工业大学,2013.
    [28] 袁婷.RESTful Web服务的形式化建模与分析[D].华东师范大学,2015.
    [29] 王金龙,宋斌,and 丁锐.”Node. js: 一种新的 Web 应用构建技术.” 现代电子技术 38.6 (2015): 70-73.
    [30] 高飞,何利力,and 高金标.”基于Node. JS 内存缓存的 Web 服务性能研究.”工业控制计算机 11 (2015): 047.
    1 评论 13 下载 2018-10-03 22:09:09 下载需要12点积分
显示 1320 到 1335 ,共 15 条
eject