14.2 流操作相关类

所谓流模型,指的是一种用于提供相对于文件读写更高层的数据读写的模型.使用流模型,你的代码不需要关心当前操作的是文件,内存还是socket(参考第 18章,"使用wxSocket编程",其中演示了用流方式使用socket的方法).某些wxWidgets标准类同时支持文件读写操作和流读写操作, 比如wxImage类.

wxStreamBase类是所有流类的基类,它声明的函数包括类似OnSysRead和OnSysWrite等需要继承类实现的函数.其子类wxInputStream和wxOutputStream则提供了更具体的流类(比如wxFileInputStream和 wxFileOutputStream子类)共同需要的用于读写操作的基本函数.让我们来具体来看一下wxWidgets提供的各种流操作相关类.

文件流

wxFileInputStream和wxFileOutputStream是基于wxFile类实现的,可以通过文件名,wxFile 对象或者整数文件描述符的方式来进行初始化.下面的例子演示了使用wxFileInputStream来进行文件读取,文件指针移位并读取当前位置数据等.

#include "wx/wfstream.h"
// 构造函数初始化流缓冲区并且打开文件名参数对应的文件.
// wxFileInputStream将在析构函数中自动关闭对应的文件描述符.
wxFileInputStream inStream(filename);
// 读100个字节
int byteCount = 100;
char data[100];
if (inStream.Read((void*) data, byteCount).
             LastError() != wxSTREAM_NOERROR)
{
    // 发生了异常
    // 异常列表请参考wxStreamBase的相关文档.
}
// 你可以通过下面的方法判断到底成功读取了多少个字节.
size_t reallyRead = inStream.LastRead();
// 将文件游标移动到文件开始处.
// SeekI函数的返回值为移动游标以前的游标相对于文件起始处的位置
off_t oldPosition = inStream.SeekI(0, wxFromBeginning);
// 获得当前的文件游标位置
off_t position = inStream.TellI();

使用wxFileOutputStream的方法也很直观.下面的代码演示了使用wxFileInputStream和wxFileOutputStream实现文件拷贝的方法.每次拷贝1024个字节.为了使代码更简介,这里没有显示错误处理的代码.

// 下面的代码实现固定单位大小的流拷贝.
// 缓冲区的使用是为了加快拷贝的速度.
void BufferedCopy(wxInputStream& inStream, wxOutputStream& outStream,
                  size_t size)
{
    static unsigned char buf[1024];
    size_t bytesLeft = size;
    while (bytesLeft > 0)
    {
        size_t bytesToRead = wxMin((size_t) sizeof(buf), bytesLeft);
        inStream.Read((void*) buf, bytesToRead);
        outStream.Write((void*) buf, bytesToRead);
        bytesLeft -= bytesToRead;
    }
}
void CopyFile(const wxString& from, const wxString& to)
{
    wxFileInputStream inStream(from);
    wxFileOutputStream outStream(to);
    BufferedCopy(inStream, outStream, inStream.GetSize());
}

wxFFileInputStream和wxFFileOutputStream跟wxFileInputStream和 wxFileOutputStream的用法几乎完全一样, 不同之处在于前者是基于wxFFile类而不是wxFile类的. 因此它们的初始化方法也不同,相应的,前者可以使用FILE指针或wxFFile对象来初始化.文件结束处理相应的有所不同: wxFileInputStream在最后一个字节被读取的时候报告wxSTREAM_EOF, 而wxFFileInputStream则在最后一个字节以后进行读操作的时候返回wxSTREAM_EOF.

内存和字符串流

wxMemoryInputStream和wxMemoryOutputStream使用内部缓冲区来处理流数据.默认的构造函数都采用 char*类型的缓冲区指针和缓冲区大小作为参数.如果没有这些参数,则表明要求该类的事例自己进行动态缓冲区管理.我们很快会看到一个相关的例子.

wxStringInputStream则采用一个wxString引用作为构造参数来进行数据读取. wxStringOutputStream采用一个开选的wxString指针作为参数来进行数据写操作;如果构造参数没有指示wxString指针,则将构造一个内部的wxString对象,这个对象可以通过GetString函数来访问.

读写数据类型

到目前为止,我们描述的流类型都处理的是原始的字节流数据.在实际的应用程序中,这些字节流必须被赋予特定的函数.为了帮助你实现这一点,你可以使用下面四个类来以一个更高的层级处理数据.这四个类分别是:wxTextInputStream, wxTextOutputStream, wxDataInputStream和wxDataOutputStream.这些类通过别的流类型类构造,它们提供了操作更高级的C++数据类型的方法.

wxTextInputStream从一段人类可读的文本中获取数据.如果你使用的构造类为文件相关类,你需要自己进行文件是否读完的判断.即使这样,读到空数据项(长度为0的字符串或者数字0)还是无法避免,因为很多文本文件都用空白符(比如换行符)来结束.下面的例子演示了怎样使用由wxFileInputStream构造的wxTextInputStream:

wxFileInputStream input( wxT("mytext.txt") );
wxTextInputStream text( input );
wxUint8 i1;
float f2;
wxString line;
text >> i1;       // 读一个8bit整数.
text >> i1 >> f2; // 先读一个8bit整数再读一个浮点数.
text >> line;     // 读一行文本

wxTextOutputStream则将文本数据写至输出流,换行符自动使用当前平台的换行符.下面的例子演示了将文本数据输出到标准错误输出流:

#include "wx/wfstream.h"
#include "wx/txtstrm.h"
wxFFileOutputStream output( stderr );
wxTextOutputStream cout( output );
cout << wxT("This is a text line") << endl;
cout << 1234;
cout << 1.23456;

wxDataInputStream和wxDataOutputStream使用发放类似,但是它使用二进制的方法处理数据.数据使用可移植的方式存储因此能够作到平台无关.下面的例子分别演示了以这种方式从数据文件读取以及写入数据文件.

#include "wx/wfstream.h"
#include "wx/datstrm.h"
wxFileInputStream input( wxT("mytext.dat") );
wxDataInputStream store( input );
wxUint8 i1;
float f2;
wxString line;
store >> i1;       // 读取一个8bit整数
store >> i1 >> f2; // 读取一个8bit整数,然后读取一个浮点数.
store >> line;     // 读取一行文本

#include "wx/wfstream.h"
#include "wx/datstrm.h"
wxFileOutputStream output(wxT("mytext.dat") );
wxDataOutputStream store( output );
store << 2 << 8 << 1.2;
store << wxT("This is a text line") ;

Socket流

wxSocketOutputStream和wxSocketInputStream是通过wxSocket对象构造的,详情参见第18章.

过滤器流对象

wxFilterInputStream和wxFilterOutputStream是过滤器流对象的基类,过滤器流对象是一种特殊的流对象,它用来将过滤后的数据输入到其它的流对象.wxZlibInputStream是一个典型的过滤器流对象.如果你在其构造函数中指定一个文件源为一个zlib格式的压缩文件的文件流对象,你可以直接从wxZlibInputStream中读取数据而不需要关心解压缩的机制.类似的,你可以使用一个 wxFileOutputStream来构造一个wxZlibOutputStream对象,如果你将数据写入wxZlibOutputStream对象,压缩后的数据将被写入对应的文件中.

下面的例子演示了怎样将一段文本压缩以后存放入另外一个缓冲区中:

#include "wx/mstream.h"
#include "wx/zstream.h"
const char* buf =
    "01234567890123456789012345678901234567890123456789";
// 创建一个写入wxMemoryOutputStream对象的wxZlibOutputStream类
wxMemoryOutputStream memStreamOut;
wxZlibOutputStream zStreamOut(memStreamOut);
// 压缩buf以后写入wxMemoryOutputStream
zStreamOut.Write(buf, strlen(buf));
// 获取写入的大小
int sz = memStreamOut.GetSize();
// 分配合适大小的缓冲区
// 拷贝数据
unsigned char* data = new unsigned char[sz];
memStreamOut.CopyTo(data, sz);

Zip流对象

wxZipInputStream是一个更复杂一点的流对象,因为它是以文档的方式而不是线性的二进制数据的方式工作的.事实上,文档是通过另外的类wxArchiveClassFactory和wxArchiveEntry来处理的,但是你可以不用关心这些细节.要使用 wxZipInputStream类,你可以有两种构造方法,一种是直接使用一个指向zip文件的文件流对象,另外一种方法则是通过一个zip文件路径和一个zip文件中文档的路径来指定一个zip数据流.下面的例子演示了这两种方法:

#include "wx/wfstream.h"
#include "wx/zipstrm.h"
#include "wx/txtstrm.h"
// 方法一: 以两步方式创建zip输入流.
wxZipEntry* entry;
wxFFileInputStream in(wxT("test.zip"));
wxZipInputStream zip(in);
wxTextInputStream txt(zip);
wxString data;
while (entry = zip.GetNextEntry())
{
    wxString name = entry->GetName();    // 访问元数据
    txt >> data;                         // 访问数据
    delete entry;
}
// 方法二: 直接指定源文档路径和内部文件路径.
wxZipInputStream in(wxT("test.zip"), wxT("text.txt"));
wxTextInputStream txt(zip);
wxString data;
txt >> data;                             // 访问数据

wxZipOutputStream用来写zip压缩文件. PutNextEntry或PutNextDirEntry函数用来在压缩文件中创建一个新的文件(目录),然后就可以写相应的数据了.例如:

#include "wx/wfstream.h"
#include "wx/zipstrm.h"
#include "wx/txtstrm.h"
wxFFileOutputStream out(wxT("test.zip"));
wxZipOutputStream zip(out);
wxTextOutputStream txt(zip);
zip.PutNextEntry(wxT("entry1.txt"));
txt << wxT("Some text for entry1\n");
zip.PutNextEntry(wxT("entry2.txt"));
txt << wxT("Some text for entry2\n");

虚拟文件系统

wxWidgets提供了一套虚拟文件系统机制,让你的应用程序可以象使用普通文件那样使用包括zip文件中的文件,内存文件以及HTTP或FTP协议这样的特殊数据.不过,这种虚拟文件机制通常是只读的,意味着你不可以修改其中的内容.wxWidgets提供的wxHtmlWindow类(用于提供 wxWidgets内部的HTML帮助文件的显示)和wxWidgets的XRC资源文件机制都可以识别虚拟文件系统路径格式.虚拟文件系统的使用比起前面介绍的zip文件流要简单,但是后者可以更改zip文档的内容.除了内部都使用了流机制以外,这两者其实没有任何其它的联系.

各种不同的虚拟文件系统类都继承自wxFileSystemHandler类,要在应用程序中使用某个特定实现,需要在程序的某个地方 (通常是OnInit函数中)调用wxFileSystem::AddHandler函数.使用虚拟文件系统通常只需要使用那些定义在 wxFileSystem对象中的函数,但是有些虚拟文件系统的实现也提供了直接给用于使用的函数,比如wxMemoryFSHandler's的 AddFile和RemoveFile函数.

在我们介绍怎样通过C++函数访问虚拟文件系统之前,我们先看看怎样在wxWidgets提供的其它子系统中使用虚拟文件系统.下面的例子演示了怎样在用于在wxHtmlWindow中显示的HTML文件中使用指定虚拟文件系统中的路径:

<img src="file:myapp.bin#zip:images/logo.png">

"#"号前面的部分是文件名,后面的部分则是虚拟文件系统类型以及文件在虚拟文件系统中的路径.

类似的,我们也可以在XRC资源文件中使用虚拟文件系统:

<object class="wxBitmapButton">
    <bitmap>file:myapp.bin#zip:images/fuzzy.gif</bitmap>
</object>

在上面的这些用法中,操作虚拟文件系统的代码被隐藏在wxHtmlWindow和XRC系统的实现中.如果你希望直接使用虚拟文件系统, 通常你需要通过wxFileSystem和wxFSFile类.下面的代码演示了怎样从虚拟文件系统中加载一幅图片,当应用程序初始化的时候,增加一个 wxZipFSHandler类型的虚拟文件系统处理器.然后创建一个wxFileSystem的实例,这个实例可以是临时使用也可以存在于整个应用程序的生命周期,这个实例用来从zip文件myapp.bin中获取logo.png图片.wxFSFile对象用于返回这个文件对应的数据流,然后通过流的方式创建wxImage对象.在这个对象被转换成wxBitmap格式以后,wxFSFile和wxFileSystem对象就可以被释放了.

#include "wx/fs_zip.h"
#include "wx/filesys.h"
#include "wx/wfstream.h"
// 这一行代码只应该被执行依次,最好是在应用程序初始化的时候
wxFileSystem::AddHandler(new wxZipFSHandler);
wxFileSystem* fileSystem = new wxFileSystem;
wxString archive = wxT("file:///c:/myapp/myapp.bin");
wxString filename = wxT("images/logo.png");
wxFSFile* file = fileSystem->OpenFile(
              archive + wxString(wxT("#zip:")) + filename);
if (file)
{
    wxInputStream* stream = file->GetStream();
    wxImage image(* stream, bitmapType);
    wxBitmap bitmap = wxBitmap(image);
    delete file;
}
delete fileSystem;

注意要使用wxFileSystem:: OpenFile函数,其参数必须是一个URL而不能是一个绝对路径,其格式应该为"file:/<主机名>//<文件名>", 如果主机名为空,则使用三个"/"符号.你可以使用wxFileSystem::FileNameToURL函数获取某个文件对应的URL,也可以用 wxFileSystem::URLToFileName函数将某个URL转换成对应的文件名.

下面的例子演示了怎样获取虚拟文件系统中获取一个文本文件的内容,并将其存放在某个wxString对象中,所需参数为zip文件路径以及zip文件中的虚拟文件的路径:

// 从zip文件中加载一个文本文件
bool LoadTextResource(wxString& text, const wxString& archive,
                      const wxString& filename)
{
    wxString archiveURL(wxFileSystem::FileNameToURL(archive));
    wxFileSystem* fileSystem = new wxFileSystem;
    wxFSFile* file = fileSystem->OpenFile(
                 archiveURL + wxString(wxT("#zip:")) + filename);
    if (file)
    {
        wxInputStream* stream = file->GetStream();
        size_t sz = stream->GetSize();
        char* buf = new char[sz + 1];
        stream->Read((void*) buf, sz);
        buf[sz] = 0;
        text = wxString::FromAscii(buf);
        delete[] buf;
        delete file;
        delete fileSystem;
        return true;
    }
    else
        return false;
}

wxMemoryFSHandler允许你将数据保存在内存中并且通过内存协议在虚拟文件系统中使用它.显然在内存中存放大量数据并不是一个值得推荐的作法,不过有时候却可以给应用程序提供一定程序的灵活性,比如你正在使用只读的文件系统或者说使用磁盘文件存在性能上的问题的时候.在 DialogBlocks软件中,如果用户提供的自定义图标文件还不具备,DialogBlocks仍然可以通过下面的XRC文件显示一个内存中的图片, 这个图片并不存在于任何的物理磁盘中.

<object class="wxBitmapButton">
    <bitmap&gt;memory:default.png</bitmap>
</object>

wxMemoryFSHandler的AddFile函数可以使用的参数包括一个虚拟文件名和一个wxImage, wxBitmap, wxString或void*数据. 如果你不再使用某个内存虚拟文件了,可以通过RemoveFile函数将其删除.如下所示:

#include "wx/fs_mem.h"
#include "wx/filesys.h"
#include "wx/wfstream.h"
#include "csquery.xpm"
wxFileSystem::AddHandler(new wxMemoryFSHandler);
wxBitmap bitmap(csquery_xpm);
wxMemoryFSHandler::AddFile(wxT("csquery.xpm"), bitmap,
                           wxBITMAP_TYPE_XPM);
...
wxMemoryFSHandler::RemoveFile(wxT("csquery.xpm"));

wxWidgets支持的第三种虚拟文件系统是wxInternetFSHandler,它支持FTP和HTTP协议.