多线程图片转换类实现STEP BY STEP

多线程图片转换类实现STEP BY STEP

CST 2005-09-05

1 文档目的

本文将介绍在Delphi中,利用多线程技术实现批量图片格式转换的类的实现。文档将涉及程序的结构安排、多线程编程的基本方法和面向对象的封装、继承、多态技术的使用。对于图片格式的转换是采用了第三方的VCL代码,本文将不讨论其中的压缩算法。

文档的写作按照我实现的逐步深入,以STEP BY STEP的方式展开,涉及的内容也会一步步增多,读者可以根据自己的需要,选择性阅读。文章中我只对一些关键细节做详细阐述,其他我只做简单说明,读者可以查看我最后提供的源代码,或者参考delphi 的帮助。


2 程序功能

通过这个图片转换类,程序员可以灵活地将图片格式进行转换。将该类集成到自己的程序中后,可以在不涉及图片转换算法的情况下,做到图片的格式转换。这个示例程序目前只实现了BMP和GIF之间的相互转换。根据类的结构,理论上只要有可以解析响应图片的VCL类,就可以扩充到转换类中。

3 实现过程
3.1 单一图片格式转换
最初我们希望可以将一个BMP图片压缩为GIF,或者将GIF还原为BMP。我从delphiBox
找到了一个可以解析GIF格式图片的类库”GifImage”。在GIFImage单元中提供了很多和GIF图片相关的类和函数,在此我们使用TGIFImage类。TGIFImage继承自Tgraphic,同样Tbitmap也继承自Tgraphic,因此,我们可以直接将载入的GIF图片传递给BMP,反之亦然。

Procedure DoConvertBMP2GIF ;
var
    m_gif:TGIFImage;
    m_bmp:TBitmap;
begin
    m_gif := TGIFImage.Create ;
    m_bmp := TBitmap.Create ;
    try
        m_bmp.LoadFromFile( ‘bmp_test.bmp’ );
        m_gif.Assign( m_bmp );
        m_gif.SaveToFile( ‘gif_out.gif’ );
    finally
        FreeAndNil(m_gif);
        FreeAndNil(m_bmp);
    end;
end;

这是一个将BMP转换为GIF的函数,如果要转GIF到BMP,只要先载入GIF,然后assign给TGIFImage的对象然后SaveToFile就可以了。

3.2 封装一层
我希望编程小组中的其他成员能够分享这个转换工具,但又不需要每个人都写一遍转换代码。因此我们对这个函数进行简单的封装。

TimageConverter = class (Tobject)
Public
    Class procedure GIF2BMP(inFile, outFile : String);
    Class procedure BMP2GIF(inFile, outFile : string);
End;

这样其他程序员就可以直接访问类方法来转换图片,如果今后将jpeg图片解析也扩充到该类中,就可以实现类似 TimageConverter.JPG2BMP(infile, outfile) 的应用。

3.3 重载的多种转换方法
我们实现了单一文件的转换后,再要实现批量转换也是很简单的。我们可以提供这样的类方法:
Class procedure GIF2BMP(inFileList, outFileList : TstringList); overload;
程序员只要提供包含文件路径的字串列表inFileList(TstringList),只需要一个循环就可以做到批量转换。

Class procedure TimageConverter. GIF2BMP(inFileList, outFileList : TstringList);
Var
    idx:integer;
    sIn, sOut:string;
Begin
    For idx:=0 to inFileList.count-1 do
    Begin
        sIn:=inFileList.strings[idx];
        sOut:=outFilelist.strings[idx];
        GIF2BMP(sin,sout);
End;
End;

申明代码中的overload关键字说明了该函数是有重载版本的。这样,对于程序员来说,要记住的方法只有GIF2BMP一个,只要,提供的响应类型的参数,程序会自动选择调用正确的函数。根据这个思想,我们可以为BMP2GIF也实现响应的重载函数。
我最后实现的重载版本有如下4个:
    procedure GIF2BMP(AInPathList, AOutPathList: TStringList); overload;
    procedure GIF2BMP(AInPathList: TStringList) ; overload;
    procedure GIF2BMP(AInPath, AOutPath: String) ; overload;
    procedure GIF2BMP(AInPath: String) ; overload

其中两个单参数的是根据输入文件的名称自动输出到同目录下的同名异缀文件中。
关于文件名称的各种处理函数如下:
    改变文件后缀 ChangeFileExt
    获得文件路径 ExtractFilePath (包含最后一个分隔符)
    分隔符判断   IsPathDelima(sPath, Len(sPath))
    分隔符常量   PathDelim(string)

3.4 利用多线程技术
GIFIMAGE单元提供的解析算法效率还是比较高的,而且有了批量转换的重载函数后,使用也已经比较方便。但是如果遇到数量巨大的图片源或者图片分辨率非常高,这样的转换过程可能对于配置不高的机器是一种折磨。由于前面给出的程序代码都是单线程串行执行,这样在转换的过程中,可能造成程序界面显示的锁死。因此我们考虑使用多线程技术来实现图片转换。

3.4.1 线程任务分派
线程的任务应该是可并发的,这样才能体现多线程的效率。因此在这里我们希望每个线程对象能够转换单一图片就够了,对于批量转换,我们就可以创建多个线程实例并发执行。

3.4.2 线程类的结构关系
目前我们希望线程能够做到的有2个任务:GIF2BMP和BMP2GIF。同时,又考虑到转换函数有很多相似性,例如:都需要输入、输出图片路径、都需要一个进行转换的主方法。因此我们尝试使用OOP的特性来封装线程类。先定义一个父类TZAImageConvertThread继承自Tthread。该类作为所有图片转换线程子类的模板,它包含那些共有的成员和方法。最重要的是,它定义了一个抽象方法DoConvert,其子类只要根据各自的图片解析算法实现这个方法就可以共享父类的所有数据和行为。同时,我们在TZAImageConvertThread的Execute方法中调用DoConvert过程。这样,每个派生的线程对象都会自动执行自己的DoConvert过程。
线程类声明如下:

type
  //------------------------------------
  //  转换线程 (父类)
  //------------------------------------
  TZAImageConvertThread = class(TThread)
  private
    //  原图路径
    FInImage  : string;
    //  输出图路径
    FOutImage : string;
    //  线程创建者引用,用来激活"所有线程结束"事件
    FOwner : TObject;
    //  线程终止事件过程
    procedure ThreadFinishProc(ASender: TObject);
  protected
    //  执行线程,由构造函数自动调用
    procedure Execute; override;
    //  图片转换抽象方法,各派生类只要实现这个方法
    procedure DoConvert ; virtual; abstract;
  public
    //  改写的线程构造函数
    constructor Create(AOwner: TObject; const APathOfInPic, APathOfOutPic: string);
  end;


  //------------------------------------
  //  BMP转GIF的线程
  //------------------------------------
  TZAImageConvertBMP2GIF = class(TZAImageConvertThread)
  public
    procedure DoConvert ; override;
  end;

  //------------------------------------
  //  GIF转BMP的线程
  //------------------------------------
  TZAImageConvertGIF2BMP = class(TZAImageConvertThread)
  public
    procedure DoConvert ; override;
  end;

在线程父类的声明中我重写了构造函数,并在重写的版本中请求了3个参数。参数Owner是线程主控类的引用(主控类将在后面说明),用来获得线程结束的事件句柄。在重写的构造函数中,先继承了Tthread类的构造函数,并挂起线程inherited Create(True); 然后对参数合法性进行判断,如果通过则调用resume方法执行线程,否则调用terminate方法结束线程。
后两个参数是获得输入输出图片的路径,这样主要是为了方便编程调用,其实用户也可以不请求这2个参数而公布输入输出路径的属性。
具体的代码可以参考我附的源代码。

3.4.3 线程的锁机制
线程的资源读写控制是编写多线程最具有挑战性的问题之一,在VC中有互斥量MUTEX、信号量SEMAPHORE、事件量EVENT和临界区CS可供使用。在DELPHI中,不仅可以直接使用这些内核级别的API和变量,VCL更是为我们封装了多种访问控制对象。
在本程序中,我们要进行访问保护的变量是一个线程计数值 iRunningThread,该变量在确定要转换的图片数量之后获得初始值(图片个数=线程个数),每个线程结束后将计数减一,然后判断自己是否为最后一个退出的线程(减一后计数值为0)。如果是,则调用主控类的某个方法。(将在主控类实现中详述)
对于iRunningThread的访问我们要做到“写写互斥”,即有一个线程在做减法的时候,其他变量不能读写该变量,而读访问不锁定变量。如果使用API编程,我们可能需要使用Mutex来实现PV操作,而在Delphi中TmultiReadExclusiveWriteSynchronizer 对象能够为我们实现这些要求。

使用方法:
将TmultiReadExclusiveWriteSynchronizer的申明在要保护的变量相同的作用范围。比如,在我们这个程序中

implementation

var
  rwSync: TMultiReadExclusiveWriteSynchronizer;
  iRunningThread : integer = 0;

然后,在线程创建的时候实例化TmultiReadExclusiveWriteSynchronizer的对象变量。
在要访问被保护变量的时候使用如下的模式
读变量
rwSync.BeginRead;
  count:=iRunningThread ;
rwSync.EndRead;    写变量
rwSync.BeginWrite;
  iRunningThread := 10;
rwSync.EndWrite;


3.5 主控类的实现
虽然使用线程可以提高程序执行效率,但是考虑到对线程对象的控制和调用比较麻烦,我又对这些线程做了一层封装——定义一个控制类 TZAImageConverter。
该类应该可以实现如下几个功能:
    提供灵活易用的接口来转换图片。
    隐藏线程调用的细节。
    能够通知调用者什么时候线程结束。

因此TZAImageConverter的声明如下:

  TZAImageConverter = class(TObject)
  private
    //  事件指针,指向调用者的事件
    FConvertFinishEvent: TNotifyEvent;
    function BuildFileList(ASearchPath, AFilter:string):TStringList;
  public
    //  GIF-->BMP 转换的4个重载版本
    procedure GIF2BMP(AInPathList, AOutPathList: TStringList); overload;
    procedure GIF2BMP(AInPathList: TStringList) ; overload;
    procedure GIF2BMP(AInPath, AOutPath: String) ; overload;
    procedure GIF2BMP(AInPath: String) ; overload ;
    //  GIF-->BMP 根据目录搜索转换
    procedure GIF2BMPInFolder(AFolder:string); overload;
    procedure GIF2BMPInFolder(AFolder, ADestFolder:string); overload;

    //  BMP-->GIF 转换的4个重载版本
    procedure BMP2GIF(AInPathList, AOutPathList: TStringList); overload;
    procedure BMP2GIF(AInPathList: TStringList) ; overload;
    procedure BMP2GIF(AInPath, AOutPath: String) ; overload;
    procedure BMP2GIF(AInPath: String) ; overload ;
    //  BMP-->GIF 根据目录搜索转换
    procedure BMP2GIFInFolder(AFolder:string); overload;
    procedure BMP2GIFInFolder(AFolder, ADestFolder:string); overload;
  published
    property FinishEvent : TNotifyEvent read FConvertFinishEvent write FConvertFinishEvent ;
  end;


TZAImageConverter对于GIF和BMP的双向转换提供了灵活多变的方法函数,而灵活性则是通过重载函数来实现。
以GIF2BMP为例,
    procedure GIF2BMP(AInPathList, AOutPathList: TStringList); overload;
输入图片的地址列表,输出图片的地址列表
    procedure GIF2BMP(AInPathList: TStringList) ; overload;
输入图片的地址列表,输出到同目录的同名异缀文件
    procedure GIF2BMP(AInPath, AOutPath: String) ; overload;
输入输出单个文件
    procedure GIF2BMP(AInPath: String) ; overload ;
输入文件,输出到同名异缀,同目录下
    procedure GIF2BMPInFolder(AFolder:string); overload;
读取目录下的所有GIF图片,输出到同目录的同名异缀文件
    procedure GIF2BMPInFolder(AFolder, ADestFolder:string); overload;
读取目录下的所有GIF图片,输出到Dest目录的同名异缀文件

对于单个文件在解析了路径后直接调用转换线程的构造函数。回想刚才的定义,Aowner参数就是TIMageConverter对象自身,路径就是后两个参数。

3.6 事件句柄
回忆在3.4.3中我们谈到要在线程对象退出之前减计数,并判断是否所有线程都完成,这就需要我们在线程的OnTerminate事件中添加代码,因此在转换线程的父类的构造函数中,我们要为TZAImageConvertThread对象的OnTerminate事件赋值对象方法指针:
constructor TZAImageConvertThread.Create(AOwner: TObject; const APathOfInPic, APathOfOutPic: string);
var
    m_TestFileWriteHanldle: integer;
    m_CanRun: Boolean;
begin
    //  实例化线程父类,并挂起等待验证通过
    inherited Create(True);
    m_CanRun:=True;

    //  线程终止事件响应函数
OnTerminate := ThreadFinishProc;
……

以上是转换线程自身terminate时要执行的过程,而在OnTerminate事件过程中,如果判断到所有线程有已经终止,我们要通知调用者。这里就要解释一下刚才3.4.3中提出的主控类中的“某个方法”。
在TZAImageConverter的定义中,我们公布了属性FinishEvent,它是一个TnotifyEvent类型的变量,TnotifyEvent变量可以被赋予一个对象方法指针。在成功赋值后,我们可以直接调用TnotifyEvent变量来进入希望执行的函数过程。因此在主控类TZAImageConverter的对象被实例化后,我们需要先将一个对象方法传递给FinishEvent属性,之后才能调用重载的转换函数。
而在转换线程内部的OnTerminate事件过程中,如果判断线程全部终止,我们可以通过如下代码发出“结束通知”,并执行预先约定的事件函数。

    if m_count = 0 then
    begin
        if not Assigned((FOwner as TZAImageConverter).FinishEvent) then
        begin
            m_errmsg:='TZAImageConvertThread 对象的 FinishEvent 事件指针为空。';
            raise Exception.Create(m_errmsg);
        end;
        (FOwner as TZAImageConverter).FinishEvent(Self);
    end;

这就是为什么,在线程对象的构造函数中我们需要参数Aowner,它提供我们访问事件句柄的入口。

4 小结
以上我“从内到外”的就本实现中面向对象和多线程的部分进行了说明。换一个角度来看,程序员要直接访问的是TZAImageConverter类,该类提供了各种图片转换方法,同时也记录一个“所有线程结束”事件句柄,之后TZAImageConverter对象负责调用管理所有的图片转换线程。利用面向对象思想来安排线程类的关系,我们将线程共同的特性和行为安排在一个线程父类中,然后留出一个可覆盖的函数DoConvert。根据转换线程的不同算法,每个子线程派生父线程并实现各自不同的DoConvert。主控类要调用的线程对象是派生的子线程类。
这其中,我们介绍了函数重载、事件句柄处理、线程同步、继承关系等知识点。为了使类更稳定,我们还需要对一些参数、状态进行判断,在本文中没有说明。因此读者可以详细阅读我的源代码。
当然这个图片转换类还有一些可以再改进的地方,包括一些参数的严格验证,成员是否恰当赋值的验证,对于异常的处理和继续抛出,以及可以根据其他图片的解析程序来扩展图片转换的能力。

相关代码可以到我的YAHOO公文包下载,也可以发邮件给我索取,也请慷慨给出意见和建议,谢谢。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值