数据结构上机题3 哈夫曼压缩

哈夫曼压缩

题目

实验三: 此次实验限定语言C/C++,允许使用stl。

ddl:下下周六(11月19日)。

实验要求:实现任意二进制文件压缩解压。将哈夫曼树或者词频率表保存到文件,压缩后解压所需信息全部从你自己压缩得到的文件中拿到。检查:对于一般txt文档实现效果明显的压缩结果并正确解压7分,大文件(几十兆以上)非文本文件正确压缩解压。(9分)报告1分。本周六实验一和二未检查完的来检查,下周六实验三写好的可以提前来检查。下下周六ddl

一、 需求分析

  1. 实现功能:

    (1) 利用Haffman编码压缩文件。打开文件后扫描文件读入数据,以二进制方式读取,每次处理8位(1个字节),八位二进制码有256种组合,对应ascii码中0-255的字符,读取结束后,统计出字符频率表并降序排序,根据字符频率表创建Haffman树,导出Haffman编码,之后再次扫描文件,将每个字符对应的Haffman编码依次写入目标文件,完成压缩。

    (2) 解压本程序创建的压缩文件。打开文件后扫描文件读入数据,以二进制方式读取,首先读取创建压缩文件是存储在文件头的字符频率表或者Haffman树,然后读取压缩数据,设置一个缓存空间buf,每次处理1位的数据,查找Haffman编码对应的字符,输出至目标文件,完成解压。

    (3) 通过修改Windows注册表设置右键菜单快捷方式来打开本程序,以便传入要处理的文件路径。

  2. 模块化设计,创建词频表和Haffman树两种数据结构。

  3. 采用main函数的参数传入要压缩或者解压的文件路径,然后处理文件并显示处理界面。如果没有传入任何文件路径,则显示设置界面。

  4. 处理界面上显示当前处理过程(解压/压缩)、处理的文件名、文件大小,实时输出处理的进度、已花时间和剩余时间。

  5. 测试数据:选取几种代表性的文件:

    (1) 文本文件,实现较明显的压缩。

    (2) 可执行文件和较大的二进制文件,实现正确的解压缩,解压缩前后文件一致,能正确打开执行或访问。

二、 概要设计

  1. 抽象数据类型词频表定义如下:

    ADT FrequencyList{
        数据对象:Frequency={ch,f|0<=ch<=255,f>=0}
                  FrequencyList={Frequency}
        数据关系:无
        基本操作:    
            CreateFrequencyListFromFile(&file,&fl);
                初始条件:文件file存在且被打开。
                操作结果:从文件数据创建词频表fl。
            SaveFrequencyListToFile(&file,fl);
                初始条件:词频表fl存在,文件file存在且被打开。
                操作结果:保存词频表fl到文件file中。
            LoadFrequencyListFromFile(&file,&fl);
                初始条件:文件file存在且被打开,文件file是按
    CreateFrequencyListFromFile方法创建的文件。
                操作结果:从文件数据载入存储的词频表fl。 
    }ADT FrequencyList
    
  2. 抽象数据类型哈夫曼树定义如下:

    ADT HaffmanTree{
        数据对象:HaffmanTreeNode={lchild,rchild,parent,ch,f}
                  HaffmanTree={HaffmanTreeNode}
        数据关系:每个节点指向左右两个孩子节点(如果有的话)
        基本操作:
            CreateHaffmanTreeFromFrequencyList(fl,&ht);
                初始条件:词频表fl存在。
                操作结果:从词频表fl创建哈夫曼树ht。
            CreateHaffmanCode(ht,&haffmancode);
                初始条件:Haffman树ht存在。
                操作结果:根据哈夫曼树ht生成哈夫曼编码haffmancode。  
    }ADT HaffmanTree
    
  3. 主函数,压缩函数,解压函数的实现框架

    `
    //以下代码是实现框架,不保证细节完备,详情请看后附源代码
    int main(int argc,char *argv[]){

    界面初始化;
    if(argc>1){
        根据argv[1]的文件路径获取文件名FILENAME;
        获取文件大小FILESIZE;
        判断文件类型FILETYPE;
    

`
绘制文件处理界面;

   计时开始,记录time_begin; 
   if(FILETYPE==0){
       HaffmanCompress压缩;
   }
   else{
       HaffmanDecompress解压;
   }

}
else{
绘制设置界面;
for(;;){
读取鼠标信息;
if (鼠标点击){
设置右键;
}
}
}
输出完成信息;
`

}
Status HaffmanCompress(当前文件,目标文件,哈夫曼编码表,当前文件总长度){//压缩
while(读取文件每次n字节 && 文件没有结束){//
for(i=0;i<n;i++){
获取该字节对应的哈夫曼编码;
while(哈夫曼编码指针){
缓存左移一位;
缓存最低位存哈夫曼编码的当前位;
哈夫曼编码指针++;
缓存长度++;
if(缓存达到输出长度){
缓存长度=0;
把缓存写到文件;
}
}
进度++;
刷新显示进度;
}
}
while(缓存长度不足指定长度){
文件尾补足长度;
}
把缓存写到文件;
return OK;
}
Status HaffmanDecompress(当前文件,目标文件,哈夫曼树){//解压
读取目标文件总长度bytenum;
htn=哈夫曼树根结点;
while(k<总长度){
读取一定长度数据写入缓存inbuf;
do{
取缓存inbuf最高位b;
缓存inbuf左移一位;
if(b==0){
htn=htn->lchild;
}
else{
htn=htn->rchild;
}
if(htn左孩子和右孩子都是空){//到达叶结点
输出叶上的字符到缓存outbuf;
if(outbuf到一定长度){
写出缓存outbuf到目标文件;
}
计数k++;
if(k==bytenum) break;
回根结点;
}
}while(缓存inbuf还没处理完);
刷新显示进度;
}
return OK;
}


4. 本程序含四个模块

   (1)    主模块
   (2)    词频表模块
   (3)    哈夫曼树、编码和解压缩模块
   (4)    图形界面模块

![](lab3_Haffman/0.png)





## 三、    详细设计
1.    词频结点类型

```c
typedef struct Frequency{//一个字符(8位)及其出现频次 
    unsigned char ch;//0<=ch<=255
    unsigned int f;
}fre,*pfre;

2. 词频表类型

typedef struct FrequencyList{//频度表,所有出现的字符及频次
    int num;//1<=num<=256
    pfre list; //频度表数组,动态申请 
}frelist,*pfrelist;

3. 哈夫曼树类型

typedef struct HaffmanTreeNode{
    struct HaffmanTreeNode *lchild,*rchild,*parent;
    unsigned int f;
    unsigned char ch;
}htreenode,*htree;

4. 词频表操作

Status CreateFrequencyListFromFile(FILE *fp,pfrelist *fl);
    //从任意文件中创建频度表 
Status SaveFrequencyListToFile(FILE *fp,pfrelist fl);
    //保存频度表到压缩文件中 
Status LoadFrequencyListFromFile(FILE *fp,pfrelist *fl);
//从压缩文件中读取频度表

5. 哈夫曼树操作及解压缩

Status CreateHaffmanTreeFromFrequencyList(pfrelist fl,htree* ht){
//从频度表创建哈夫曼数,频度表保证至少两种字符 
Status PreOrderTraverse(htree ht,int child,int i,char code[],char haffmancode[256][256]){
//先序遍历,child=0表示当前是左孩子,child=1表示当前是右孩子
//i为当前层次,code为当前遍历路径已存编码,haffmancode是编码表 
Status CreateHaffmanCode(htree ht,char haffmancode[256][256]){
//创建编码表haffmancode[256][256],牺牲一些空间以便快速索引
Status HaffmanCompress(FILE *fp1,FILE *fp2,char haffmancode[256][256],unsigned int bytenum){
//压缩,bytenum为总长度 
Status HaffmanDecompress(FILE *fp1,FILE *fp2,htree ht){
//解压,解压时直接用哈夫曼树搜索对应字符 

6. 图形界面操作

SMALL_RECT UI_SetRect(int left,int top,int right,int bottom);
//根据坐标取返回对应矩形区域数据 
void UI_SetConsoleWindowSize(HANDLE hOut,int width,int height);
//设置窗口大小
void UI_SetConsoleScreenBufferSize(HANDLE hOut,int width,int height);
//设置缓冲区大小
void UI_InitSTDHandle(HANDLE *hOut,CONSOLE_SCREEN_BUFFER_INFO *bInfo,int width,int height);
//初始化 
void UI_DrawText(HANDLE hOut,WORD attr,char *text,int len,int row,int left,int right);
//在指定行左右位置之间居中输出一行文本
void UI_CleanText(HANDLE hOut,int row,int left,int right);
//在指定行左右位置之间清除一行文本
void UI_CleanTextRect(HANDLE hOut,int top,int bottom,int left,int right);
//在指定区域之间清除文本
void UI_DrawBox(HANDLE hOut,int bSingle,SMALL_RECT rc,WORD attr);
//画方框 
void ClearScreen(HANDLE hOut,DWORD att){
//清屏,且设置样式

7. 其他函数

void GetCompressedFilePath(char *FPath,char *CFPath);
//从传入的路径求得对应压缩文件路径 
void GetDecompressedFilePath(char *FPath,char *DFPath);
//从传入的压缩文路径求得对应解压文件路径 
void GetFileNameFromPath(char *FPath,char *FName);
//从路径中提取文件名
long GetFileSizeFromPath(char *FPath);
//获取文件大小 
int GetFileTypeFromPath(char *FPath);
//判断文件类型是解压(1)还是压缩(0)
void UIInit();
//UI初始化 
void DrawAllBox();
//绘制所有方框 
void DrawInfoText(char *filename,int type,int size);
//显示文件名,类型是解压(1)还是压缩(0)
void UpdateTimeText(double progress);
//根据进度(0-1)更新剩余时间 
void DrawSelect(int select);
//没有文件传入时显示配置界面,select=1表示已选中,即当前已创建右键菜单
int CheckKey();
//检查右键菜单注册表是否已设置 
void SetKey(char *exepath);
//设置或取消右键菜单注册表 
int Compress(char *fpath);
//预处理及压缩 
int Decompress(char *fpath);
//预处理及解压 

8. 具体的函数实现见附录,均有详细注释,此处不再赘述。

四、 调试分析

1. 本程序的难点在于哈夫曼树的创建、存储、读取、匹配。
2. 本程序选择存储词频表而不是哈夫曼树,在解压时读取词频表重新创建哈夫曼树,文件结构如下图。

FileTypeMark位于文件头,标记了“#HAFFMANCOMPRESSFILE#”,以供解压时识别为压缩文件。之后的一段数据FrequencyList 存放了字符频率表,表头记录出现了字符类型总数,之后为各个字符及其频度。最后的部分存放了经过哈夫曼编码后的原文件数据,头部记录了原文件的长度。

3. 在文件的读取和存储上,设置一个读写缓存,每次读入一定的字节数,然后进行处理,而不是每次读入一个字节,这样效率太低,写出时同理。

4. 解压时重建哈夫曼树后,不要再次导出哈夫曼编码,每次读取一位二进制数据,直接搜索哈夫曼树,是0则跳转左孩子,是1则跳转右孩子。

5. 空文件和只有一种字符时应该单独考虑,哈夫曼树不能处理。

6. 之前在压缩时每次存入4个字节(32位),若文件尾不是整字节,补足为整字节(8位的倍数),看似没有问题,但是在读取时仍按4字节读取存入unsigned int,末尾的数据若非刚好为4个字节(低位为空),则读入时有效数据会被存入unsigned int的低位(int存储时先存入了低8-1位,再存16-9位…以此类推),造成错误。

7. 关于fread,若不考虑返回值,fread(buf,size,count,fp),size和count的值可互换,只要size*count是需要读取的字节数即可。但是如果想知道实际读取了多少字节,比如一次读入1024字节,应该写为fread(buf,1,1024,fp),当剩余数据不足1024时,会返回实际读取的count数,在这里就是字节数。若写为fread(buf,1024,1,fp),当剩余数据超过1024时,返回1024,当剩余数据不足1024时,将返回0(虽然buf中会存有实际读取的数据)。

8. 还是fread的问题,传统情况下为了避免最后一个字节使用了两次,采用while(fread(buf,1,1,fp) && !feof(fp)) 因为fread读完最后一个字节后,feof仍然不变,再次fread才触发feof。这里一次读取一个字节,fread(buf,1,1,fp)的返回值只会是0和1,即使读取最后一个字节也为1,再次读取才0,因此while中用&&没有问题,两个条件只会同为0或同为1。如果一次读取多个字节(比如1024),在文件总长度为1024的倍数时,和上述情况没有区别,若非1024倍数,最后一次读取fread将返回实际读取数量(<1024),但是此时已经触发feof(因为最后一次读取是没有满足fread的请求的,这里的逻辑很微妙),故while中&&应该改用||。

9. 在界面更新问题上,编写了一个更新显示进度函数,直接在压缩/解压函数中调用。

五、 用户手册

六、 测试结果

  1. 一个文本文件“四大名著.txt”(原大小 6,321,865字节,压缩后4,827,905字节,压缩率75%),一个可执行文件(压缩率77%),实现明显的压缩。
  2. 和一份pdf文档实现正确解压缩,但压缩效果不明显(压缩率99%),解压缩前后md5值不变。
  3. 解压缩时间在可接受的范围内,约16MB/s.





七、 附录

源程序文件清单:

(1) FrequencyList.h

(2) HaffmanTree.h

(3) MyUI.h

(4) Hzip.c

FrequencyList.h

/*
    Last Update time: 2017-11-01 
    Version:1.0
    Author:LinXiang (PB16020923) 
    Description:
        FrequencyList.
*/
#ifndef FREQUENCYLIST_H
#define FREQUENCYLIST_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define OK 1
#define ERROR 0
typedef int Status;  
typedef struct Frequency{//一个字符(8位)及其出现频次 
    unsigned char ch;//0<=ch<=255
    unsigned int f;
}fre,*pfre;
typedef struct FrequencyList{//频度表,所有出现的字符及频次
    int num;//1<=num<=256
    pfre list; //频度表数组,动态申请 
}frelist,*pfrelist;
int frecmp(const void *a,const void *b){
    pfre aa=(pfre)a;
    pfre bb=(pfre)b;
    return (int)(bb->f-aa->f);//b-a这样才能让qsort降序排列 
}
void UpdateTimeText(double progress);//声明以便下面函数调用更新进度 
Status CreateFrequencyListFromFile(FILE *fp,pfrelist *fl){//从任意文件中创建频度表 
    fre arr[256]={};//临时存放所有字符,直接按下标存入更方便,不用插入或遍历 
    unsigned char buf[1048576]={};//当前读入缓存 
    int i,n; //n表示实际读入的字符数,仅在文件尾时不是1024 
    while((n=fread(buf,1,1048576,fp)) || !feof(fp)){//读取文件每次1024字节,
    //注意size=1,count=1024,才能知道实际成功读入字节数(fread返回值),不要反过来,反过来的话一旦不满1024,fread返回值将为0 
        for(i=0;i<n;i++){
            arr[buf[i]].ch=buf[i];
            arr[buf[i]].f++;
        }
    }
    qsort(arr,256,sizeof(fre),&frecmp);//排序,不出现的字符会被排至尾部 
    *fl=(pfrelist)malloc(sizeof(frelist));//让fl存放指向frelist的指针 
    for(i=0;i<256;i++){//统计出现的字符数量 
        if(arr[i].f==0) break; 
    }
    (*fl)->num=i;
    (*fl)->list=(pfre)malloc(sizeof(fre)*((*fl)->num));//申请频度表的数组空间 
    for(i=0;i<(*fl)->num;i++){
        (*fl)->list[i].ch=arr[i].ch; 
        (*fl)->list[i].f=arr[i].f;
    }
    return OK;
}
Status SaveFrequencyListToFile(FILE *fp,pfrelist fl){//保存频度表到压缩文件中 
    fwrite(&(fl->num),4,1,fp);
    int i;  
    for(i=0;i<fl->num;i++){
        fwrite(&(fl->list[i].ch),1,1,fp);
        fwrite(&(fl->list[i].f),4,1,fp);
    }
    return OK; 
}
Status LoadFrequencyListFromFile(FILE *fp,pfrelist *fl){//从压缩文件中读取频度表
    *fl=(pfrelist)malloc(sizeof(frelist));//让fl存放指向frelist的指针 
    fread(&((*fl)->num),4,1,fp);
    (*fl)->list=(pfre)malloc(sizeof(fre)*((*fl)->num));//申请频度表的数组空间 
    int i;  
    for(i=0;i<(*fl)->num;i++){
        fread(&((*fl)->list[i].ch),1,1,fp);
        fread(&((*fl)->list[i].f),4,1,fp);
    }
    return OK; 
}
#endif

HaffmanTree.h

/*
    Last Update time: 2017-11-01 
    Version:1.0
    Author:LinXiang (PB16020923) 
    Description:
        HaffmanTree.
*/
#ifndef HAFFMANTREE_H
#define HAFFMANTREE_H
#define OK 1
#define ERROR 0
#include <stdlib.h>
#include <string.h>
#include "FrequencyList.h"
typedef int Status; 
typedef struct HaffmanTreeNode{
    struct HaffmanTreeNode *lchild,*rchild,*parent;
    unsigned int f;
    unsigned char ch;
}htreenode,*htree;
void UpdateTimeText(double progress);//声明以便下面函数调用更新进度 
Status CreateHaffmanTreeFromFrequencyList(pfrelist fl,htree* ht){
//从频度表创建哈夫曼数,频度表保证至少两种字符 
    int hfn=fl->num*2-1;//haffmantreenode个数
    htreenode *arr=(htreenode *)malloc(sizeof(htreenode)*hfn);
    int i,t;
    for(i=0;i<fl->num;i++){//前 fl->num 个放叶结点 
        t=fl->num-i-1;//使升序排列 
        arr[t].ch=fl->list[i].ch;
        arr[t].f=fl->list[i].f;
        arr[t].lchild=NULL;
        arr[t].rchild=NULL;
        arr[t].parent=NULL;
    }
    int j,k;
    for(j=i,k=0;i<hfn;i++){
        arr[i].f=0;//权值初始为0 
        //先选择最小的一个做左孩子 
        if(i-j>=1 && fl->num-k>=1){//叶结点和非叶结点都至少有一个可选 
            if(arr[k].f<=arr[j].f){
                arr[i].lchild=&arr[k];
                arr[k].parent=&arr[i];
                arr[i].f+=arr[k].f; 
                k++;
            }
            else{
                arr[i].lchild=&arr[j];
                arr[j].parent=&arr[i];
                arr[i].f+=arr[j].f; 
                j++;
            }
        }
        else if(fl->num-k>=1){//只有叶结点可选 
            arr[i].lchild=&arr[k];
            arr[k].parent=&arr[i];
            arr[i].f+=arr[k].f; 
            k++;
        }
        else{//只有非叶结点可选 
            arr[i].lchild=&arr[j];
            arr[j].parent=&arr[i];
            arr[i].f+=arr[j].f; 
            j++;
        } 

        //再选择最小的一个做右孩子 
        if(i-j>=1 && fl->num-k>=1){//叶结点和非叶结点都至少有一个可选 
            if(arr[k].f<=arr[j].f){
                arr[i].rchild=&arr[k];
                arr[k].parent=&arr[i];
                arr[i].f+=arr[k].f;
                k++;
            }
            else{
                arr[i].rchild=&arr[j];
                arr[j].parent=&arr[i];
                arr[i].f+=arr[j].f;
                j++;
            }
        }
        else if(fl->num-k>=1){//只有叶结点可选 
            arr[i].rchild=&arr[k];
            arr[k].parent=&arr[i];
            arr[i].f+=arr[k].f;
            k++;
        }
        else{//只有非叶结点可选 
            arr[i].rchild=&arr[j];
            arr[j].parent=&arr[i];
            arr[i].f+=arr[j].f;
            j++;
        }
    }
    *ht=&arr[hfn-1];//指向根 
    return OK; 
}
Status PreOrderTraverse(htree ht,int child,int i,char code[],char haffmancode[256][256]){
//先序遍历,child=0表示当前是左孩子,child=1表示当前是右孩子,i为当前层次,code为当前遍历路径已存编码,haffmancode是编码表 
    if(ht==NULL) return ERROR;
    code[i]=child+'0';
    if(ht->lchild==NULL && ht->rchild==NULL){//这是个叶结点  
        strcpy(haffmancode[ht->ch],code);//把当前编码保存到编码表里 
    }
    else{
        PreOrderTraverse(ht->lchild,0,i+1,code,haffmancode);
        PreOrderTraverse(ht->rchild,1,i+1,code,haffmancode);
    }
    code[i]=0;//恢复
    return OK; 
} 
Status CreateHaffmanCode(htree ht,char haffmancode[256][256]){
//编码表haffmancode[256][256]牺牲一些空间以便快速索引
    char code[256]={};//临时存放编码 
    PreOrderTraverse(ht->lchild,0,0,code,haffmancode);
    PreOrderTraverse(ht->rchild,1,0,code,haffmancode);
    return OK;
}
Status HaffmanCompress(FILE *fp1,FILE *fp2,char haffmancode[256][256],unsigned int bytenum){
//压缩,bytenum为总长度 
    unsigned char inbuf[262144]={};//当前读入 
    unsigned int outbuf=0;//操作的缓存,满32位(4字节)才写入outbuf2,这个缓存主要用于位操作 
    unsigned int outbuf2[262144]={};//输出的缓存1024*4字节 
    fwrite(&bytenum,4,1,fp2);//记下总长度 
    int up=bytenum/100;//设置界面更新间隔
    if(up==0) up=1; //文件过小,界面更新间隔为0,置1 
    int cnt=0;//循环计数器,缓存输出用的 
    unsigned int k=0;//计数器,更新界面用的
    int cnt2=0; //循环计数器,缓存输出用的
    int i,n=0;
    while((n=fread(inbuf,1,262144,fp1)) || !feof(fp1)){//读取文件每次262144字节
        for(i=0;i<n;i++){   
            char *p=haffmancode[inbuf[i]];
            while(*p){
                outbuf<<=1;//左移一位 
                outbuf | = (*p-'0');//最低位存p
                p++;
                cnt++;
                if(cnt%32==0){//满8位输出 
                    cnt=0;
                    outbuf2[cnt2++]=outbuf;
                    outbuf=0;
                    if(cnt2==262144){
                        cnt2=0;
                        fwrite(outbuf2,4,262144,fp2);
                        memset(outbuf2,0,262144*4);
                    }
                } 
            }
            k++;
            if(k%up==0) UpdateTimeText((double)k/(double)bytenum);
        }
    }
    while(cnt%32!=0){//文件尾补足为32位的整数倍 
        outbuf<<=1;
        cnt++;
    }
    outbuf2[cnt2++]=outbuf;
    fwrite(outbuf2,4,cnt2,fp2);
    return OK;
}
Status HaffmanDecompress(FILE *fp1,FILE *fp2,htree ht){//解压,解压时直接用哈夫曼数搜索对应字符 
    unsigned int inbuf=0;//输入的缓存,每次读取32位(4字节)
    unsigned char outbuf=0;//输出缓存(1字节) 
    unsigned char outbuf2[1048576]={};//输出缓存2(1048576字节) 
    unsigned int bytenum;//总字节数 
    unsigned int k=0; 
    int cnt=0;
    int b;
    htreenode* htn=ht;
    fread(&bytenum,4,1,fp1);//读取总长度 
    int up=bytenum/100;//设置界面更新间隔
    if(up==0) up=1; //文件过小,界面更新间隔为0,置1 
    int cnt2=0;
    while(k<bytenum){
        fread(&inbuf,1,4,fp1);//size=1,count=4
        do{
            b=inbuf & 0x80000000;//取最高位 
            inbuf<<=1;//左移一位 
            cnt++;
            if(b==0){
                htn=htn->lchild;
            }
            else{
                htn=htn->rchild;
            }
            if(htn->lchild==NULL && htn->rchild==NULL){//到达叶结点 
                outbuf=htn->ch;
                outbuf2[cnt2++]=outbuf;
                outbuf=0;
                if(cnt2==1048576){
                    cnt2=0;
                    fwrite(outbuf2,1,1048576,fp2);
                    memset(outbuf2,0,1048576);
                }   
                k++;//计数加一 
                if(k==bytenum) break;
                htn=ht;//回根结点 
            }
        }while(cnt%32!=0);
        cnt=0;
        if(k%up==0) UpdateTimeText((double)k/(double)bytenum);
    }
    fwrite(outbuf2,1,cnt2,fp2);
    return OK;
}
#endif

Hzip.c

/*
    Last Update time: 2017-11-11     
    Version:1.1
    Author:LinXiang (PB16020923) 
    Description:
        Hzip.c.
*/
#include <stdio.h>
#include <string.h> 
#include <time.h>
#include "MyUI.h"
#include "FrequencyList.h"
#include "HaffmanTree.h"
#define WIDTH 50 //窗口的宽和高 
#define HEIGHT 15
HANDLE hout;
HANDLE hin;
time_t time_begin;
time_t time_now;
long FILESIZE;//记录文件长度,用于读取文件时计算进度
int FILETYPE;// 
char FILENAME[30]; 
void GetCompressedFilePath(char *FPath,char *CFPath){//从传入的路径求得对应压缩文件路径 
    strcpy(CFPath,FPath);
    strcat(CFPath,".hzip");
}
void GetDecompressedFilePath(char *FPath,char *DFPath){//从传入的压缩文路径求得对应解压文件路径 
    strcpy(DFPath,FPath);
    DFPath[strlen(DFPath)-5]=0;//去除尾部".hzip" 
}
void GetFileNameFromPath(char *FPath,char *FName){//从路径中提取文件名
    char *pos;//最后一个\的位置 
    char *p=FPath;
    while(*p){
        if(*p=='\\') pos=p;
        p++;
    }
    while(*pos){
        *FName++=*(++pos);//此法即可跳过‘\’本身,又可复制字符串结束符到FName中 
    }
}
long GetFileSizeFromPath(char *FPath){ //获取文件大小 
    FILE *fp=fopen(FPath,"r");  
    if(!fp) return -1;  
    fseek(fp,0L,SEEK_END);  
    long size=ftell(fp);  
    fclose(fp);  
    return size;  
} 
int GetFileTypeFromPath(char *FPath){//判断是解压(1)还是压缩(0)
    FILE *fp=fopen(FPath,"rb+");
    char MARK[22];
    int type;
    if(!fread(MARK,21,1,fp)){
        type=0;
    }
    else{
        MARK[21]=0;//字符串结尾 
        if(strcmp("#HAFFMANCOMPRESSFILE#",MARK)) type=0;
        else type=1;    
    }
    fclose(fp);
    return type;
}
void UIInit(){//UI初始化 
    UI_InitSTDHandle(&hout,&hin,WIDTH,HEIGHT);
    UI_SetConsoleScreenBufferSize(hout,WIDTH,HEIGHT);//设置缓冲区大小
    UI_SetConsoleWindowSize(hout,WIDTH,HEIGHT);//设置窗口大小 
    //system("color F0");此句与鼠标操作冲突!!!!!不要用system() 
    ClearScreen(hout,B_WHITE); //清屏并设置前景背景色,不要用上句 

    COORD curpos;
    curpos.X=5;
    curpos.Y=HEIGHT-2;
    SetConsoleCursorPosition(hout,curpos);//设置光标位置 
    CONSOLE_CURSOR_INFO cursor_info={1,0}; 
    SetConsoleCursorInfo(hout,&cursor_info);//隐藏光标 
}
void DrawAllBox(){//绘制所有方框 
    SMALL_RECT rc;
    rc=UI_SetRect(0,0,WIDTH-1,3);
    UI_DrawBox(hout,1,rc,B_WHITE | F_RED);
    rc=UI_SetRect(0,4,WIDTH-1,7);
    UI_DrawBox(hout,1,rc,B_WHITE | F_BLUE);
    rc=UI_SetRect(0,8,WIDTH-1,11);
    UI_DrawBox(hout,1,rc,B_WHITE | F_BLUE); 
    rc=UI_SetRect(0,12,WIDTH-1,14);
    UI_DrawBox(hout,1,rc,B_WHITE | F_CYAN); 
}
void DrawInfoText(char *filename,int type,int size){//文件名,类型是解压(1)还是压缩(0)
    char t_title[30];
    sprintf(t_title,"Hzip V1.1");
    UI_DrawText(hout,B_WHITE | F_RED,t_title,strlen(t_title),1,0+2,WIDTH-1-2);
    sprintf(t_title,"Code by PB16020923");
    UI_DrawText(hout,B_WHITE | F_RED,t_title,strlen(t_title),2,0+2,WIDTH-1-2);
    //窗口标题显示当前操作
    char t_info[38];
    sprintf(t_info,"Hzip V1.1 - %s",type==0?"Compressing...":"Decompressing...");
    SetConsoleTitle(t_info);
    //文件名
    char t_filename[38];
    sprintf(t_filename,"FileName: %s",filename);    
    UI_DrawText(hout,B_WHITE | F_BLUE,t_filename,strlen(t_filename),5,0+2,WIDTH-1-2);
    //文件大小 
    char t_size[38];
    sprintf(t_size,"Size: %0.2f KB",(double)size/1024);
    UI_DrawText(hout,B_WHITE | F_BLUE,t_size,strlen(t_size),6,0+2,WIDTH-1-2);   
}
void UpdateTimeText(double progress){//根据进度(0-1)更新剩余时间 
    time(&time_now); 
    //已花时间 
    char t_spenttime[40];
    sprintf(t_spenttime,"Spent Time: %d s",time_now-time_begin);
    UI_CleanText(hout,9,0+2,WIDTH-1-2);
    UI_DrawText(hout,B_WHITE | F_BLUE,t_spenttime,strlen(t_spenttime),9,0+2,WIDTH-1-2);
    //剩余时间 
    char t_remainedtime[40];
    sprintf(t_remainedtime,"Remained Time: %d s",(int)((time_now-time_begin)*(1-progress)/progress));
    UI_CleanText(hout,10,0+2,WIDTH-1-2);
    UI_DrawText(hout,B_WHITE | F_BLUE,t_remainedtime,strlen(t_remainedtime),10,0+2,WIDTH-1-2);
    //画进度条 
    int k=47*progress,i;
    for(i=2;i<=k;i++){
        //UI_DrawText(hout,B_BLUE," ",1,12,i,i);
        UI_DrawText(hout,B_CYAN," ",1,13,i,i);
    }
}
void DrawSelect(int select){//没有文件传入时显示配置界面,select=1表示已选中,即当前已创建右键菜单
    ClearScreen(hout,B_WHITE); 
    SMALL_RECT rc;
    rc=UI_SetRect(0,0,WIDTH-1,4);
    UI_DrawBox(hout,1,rc,B_WHITE | F_RED);      
    char t_title[38];
    sprintf(t_title,"Hzip V1.1 - Setting");
    UI_DrawText(hout,B_WHITE | F_RED,t_title,strlen(t_title),2,0+2,WIDTH-1-2);

    int dx=10,dy=5;
    rc=UI_SetRect(0+dx,0+dy,5+dx,2+dy);
    UI_DrawBox(hout,1,rc,B_WHITE | F_BLUE);
    UI_DrawText(hout,B_WHITE | F_BLUE,select==1?"√":"  ",2,1+dy,2+dx,3+dx);   
    char t_cj[30]="Create right-click menu";
    UI_DrawText(hout,B_WHITE | F_BLUE,t_cj,strlen(t_cj),1+dy,7+dx,7+dx+strlen(t_cj));
}
int CheckKey(){//检查右键菜单注册表是否已设置 
    HKEY k1,k2,k3;
    if(RegOpenKeyEx(HKEY_CLASSES_ROOT,"*",0,KEY_ALL_ACCESS,&k1) != ERROR_SUCCESS){
        return 0;
    }
    if(RegOpenKeyEx(k1,"shell",0,KEY_ALL_ACCESS,&k2) != ERROR_SUCCESS){
        return 0;
    }
    if(RegOpenKeyEx(k2,"Use Hzip to compress/decompress...",0,KEY_ALL_ACCESS,&k3) != ERROR_SUCCESS){ 
        return 0;
    }   
    return 1;
}
void SetKey(char *exepath){//设置或取消右键菜单注册表 
    HKEY k1,k2,k3,k4;
    char readvalue[128]={};
    char setvalue[128]={};
    if(RegOpenKeyEx(HKEY_CLASSES_ROOT,"*",0,KEY_ALL_ACCESS,&k1) != ERROR_SUCCESS){
        RegCreateKey(HKEY_CLASSES_ROOT,"*",&k1);
    }
    if(RegOpenKeyEx(k1,"shell",0,KEY_ALL_ACCESS,&k2) != ERROR_SUCCESS){
        RegCreateKey(k1,"shell",&k2);
    }
    if(RegOpenKeyEx(k2,"Use Hzip to compress/decompress...",0,KEY_ALL_ACCESS,&k3) != ERROR_SUCCESS){//没有右键 
        RegCreateKey(k2,"Use Hzip to compress/decompress...",&k3);
        RegCreateKey(k3,"command",&k4);
        strcpy(setvalue,exepath);
        strcat(setvalue," %1");
        RegSetValueEx(k4,NULL,0,REG_SZ,setvalue,sizeof(setvalue));
        DrawSelect(1);
        MessageBox(NULL,"Program has created a right-click menu to use Hzip.","SET",MB_OK|MB_ICONINFORMATION);
    }
    else{//有右键,删除 
        RegDeleteKey(k3,"command");
        RegDeleteKey(k2,"Use Hzip to compress/decompress...");
        DrawSelect(0);
        MessageBox(NULL,"Program has deteled the right-click menu to use Hzip.","DETELE",MB_OK|MB_ICONINFORMATION);
    }   
    RegCloseKey(k1);
    RegCloseKey(k2);
    RegCloseKey(k3);
    RegCloseKey(k4);
}
int Compress(char *fpath){//预处理及压缩 
    FILE *fp1=fopen(fpath,"rb+");
    char cfpath[512];
    GetCompressedFilePath(fpath,cfpath);//生成压缩文件存储路径 
    FILE *fp2=fopen(cfpath,"wb+");
    char MARK[21]="#HAFFMANCOMPRESSFILE#";
    fwrite(MARK,21,1,fp2);//标记这是一个压缩文件 

    pfrelist fl;
    CreateFrequencyListFromFile(fp1,&fl);//读取源文件,创建频度表
    SaveFrequencyListToFile(fp2,fl);//保存频度表到目标文件
if(fl->num!=0 && fl->num!=1){
//0表示空文件,1表示只有一种字符,这两种情况都不用编码,保存频度表即可 
        htree ht;
        CreateHaffmanTreeFromFrequencyList(fl,&ht);//从频度表创建哈夫曼树 
        char haffmancode[256][256]={};
        CreateHaffmanCode(ht,haffmancode);
        fseek(fp1,0,SEEK_SET);//源文件移回文件头 
        HaffmanCompress(fp1,fp2,haffmancode,ht->f);//根结点的权值即为总字符数        
    }
    fclose(fp1);
    fclose(fp2);
}
int Decompress(char *fpath){//预处理及解压 
    FILE *fp1=fopen(fpath,"rb+");
    char dfpath[512];
    GetDecompressedFilePath(fpath,dfpath);//生成解压文件存储路径 
    FILE *fp2=fopen(dfpath,"wb+");
    fseek(fp1,21,SEEK_SET);//跳过标记 

    pfrelist fl;
    LoadFrequencyListFromFile(fp1,&fl);
    if(fl->num==0) ;//空文件,不操作
    else if(fl->num==1){//只有一种字符的文件 
        unsigned int i;
        for(i=0;i<fl->list[0].f;i++) fwrite(&(fl->list[0].ch),1,1,fp2);
    }
    else{
        htree ht;
        CreateHaffmanTreeFromFrequencyList(fl,&ht);
//从频度表创建哈夫曼树,解压时不用再生成哈夫曼编码,搜索树即可 
        HaffmanDecompress(fp1,fp2,ht);      
    }

    fclose(fp1);
    fclose(fp2);
}
int main(int argc,char *argv[]){
    UIInit();

    if(argc>1){//有传入路径
        DrawAllBox();
        FILETYPE=GetFileTypeFromPath(argv[1]);  
        FILESIZE=GetFileSizeFromPath(argv[1]); 
        GetFileNameFromPath(argv[1],FILENAME); 
        DrawInfoText(FILENAME,FILETYPE,FILESIZE);

        time(&time_begin); //计时开始 
        if(FILETYPE==0){
            Compress(argv[1]);
        }
        else{
            Decompress(argv[1]);
        }
        UpdateTimeText(1);
    }
    else{
        DrawSelect(CheckKey());
        INPUT_RECORD inrec;
        DWORD res;//返回已读取的记录数
        COORD pos={0,0};
        int k;
        for(;;){
            k++;
            ReadConsoleInput(hin,&inrec,1,&res);//读取鼠标信息
            if (inrec.EventType==MOUSE_EVENT){
                if (inrec.Event.MouseEvent.dwEventFlags==0 && \
                inrec.Event.MouseEvent.dwButtonState==FROM_LEFT_1ST_BUTTON_PRESSED){
                    SetKey(argv[0]);//设置右键

                }
            }
        }
    }
    char t_finish[30];
    sprintf(t_finish,"Finished! Press any key to exit!");
    UI_DrawText(hout,F_CYAN | B_BLACK,t_finish,strlen(t_finish),HEIGHT-2,0+2,WIDTH-1-2);
    getchar();
}

MyUI.h

​ 界面部分,代码过长且与数据结构关系不大,此处不贴出

文章目录
  1. 哈夫曼压缩
    1. 题目
    2. 一、 需求分析
    3. 二、 概要设计
    4. 四、 调试分析
    5. 五、 用户手册
    6. 六、 测试结果
    7. 七、 附录
|