JUCE类库FileBasedDocument详解

先了解一下程序中读写文件的大体流程(分两种情况):

  1. 从无到有的生成文件。运行程序,内部创建各种各样的数据类型和数据对象,一番操作处理后,将数据保存到磁盘文件中。该过程往往利用临时创建或某些类一直持有的XmlElement、ValueTree、JSON等对象(数据模型)来读取、计算和设置(修改、新增等等)各种数据。如有需要时,将这些对象中的数据保存到磁盘文件中。
  2. 读写已有的数据文件。和上个情况差不多,所不同的,内部的数据模型对象在创建时即直接初始化为磁盘文件中的数据,即:先加载和读取磁盘文件中的数据,将所需的数据设置到数据模型中(比如XmlElement对象的属性值)。而后,针对这些数据模型的对象进行读取和设置操作,处理完毕,将数据回写到磁盘文件中。

以上流程,有3个关键点:

  1. 加载磁盘文件,读取文件中的数据,将数据转为程序内部的对象。
  2. 程序内部对数据模型进行各类读写、增删改等处理。
  3. 保存磁盘文件(即程序内部的数据写入磁盘文件中)。

第2个关键点是应用程序的业务逻辑与功能核心,涉及广泛,很难一言蔽之,暂且不论。第1和第3个关键点代表了程序处理数据的来源与终点,其重要性同样不言而喻。常规模式下,这两个关键点的编码量、复杂度与繁琐性均不容小觑。而FileBasedDocument则将这两个关键点封装到了一起,让繁琐的程序文档读写操作变得简单和易维护。更简单的说,FileBasedDocument持有一个磁盘文件,执行该文件的加载与保存等逻辑操作,并可将文件中的数据转为程序内部的数据。

每一款程序内部所需的数据模型都不尽相同,即:加载(打开)磁盘文件后,如何读取,读取的结果设置给哪个或哪些数据模型,数据处理后,哪些数据写入到磁盘文件中,等等之类,都不好确定。FileBasedDocument类的做法是:将这些逻辑操作设计成纯虚函数,程序员在自己的派生类中实现之。

这就是FileBasedDocument的来龙去脉。先了解一下该类的5个纯虚函数(均为protected),这5个纯虚函数仅供派生类实现,派生类实现后,无需也不能直接调用,FileBasedDocument为这些函数另外提供了public接口。(略)

FileBasedDocument类的public成员函数:(略)

以上函数中,save…()开头的几个函数,返回值为本类的枚举常量:
 FileBasedDocument::savedOk 文件已经保存
 FileBasedDocument::userCancelledSave 用户不打算保存
 FileBasedDocument::failedToWriteToFile 写入失败

派生类继承FileBasedDocument并重写纯虚函数后,通常还需写一到多个功能性函数,执行本程序的核心操作与数据处理。这些函数所需的参数即为获取文件数据之后的数据模型对象,比如XML节点中的某个属性值。见示例代码:

// 自定义的FileBasedDocument类中重写基类的5个纯虚函数,首先是设置文档标题
String FilterGraph::getDocumentTitle()
{
    // 如果当前未持有文件,则文档标题(文件名)设置为“无标题”
    if (! getFile().exists())
        return L"无标题";

    // 否则返回当前所持有文件的文件名
    return getFile().getFileNameWithoutExtension();
}

// 加载,用文件中的数据来初始化程序内部的数据模型。注意返回值为Result对象
Result FilterGraph::loadDocument (const File& file) 
{
    XmlDocument doc (file);   // 使用XML技术解析文档并加载数据
    ScopedPointer xml (doc.getDocumentElement());

    if (xml == nullptr || ! xml->hasTagName ("NodeName"))
        return Result::fail (L"无法识别此文件");

    // 功能性函数或本类的控件直接调用XML节点的属性值
    mySlider->setValue (xml->getDoubleAttribute ("SliderValue"));		
    changed();	// 表明文档已经改变,尚未保存

    return Result::ok();	// 加载成功
}

// 将程序内部的数据保存到本对象所持有的磁盘文件中
Result FilterGraph::saveDocument (const File& file)
{
    ScopedPointer xml = new XmlElement ("NodeName");
    xml->setAttribute ("SliderValue", mySlider->getValue());

    if (! xml->writeToFile (file, String::empty))
        return Result::fail (L"写入失败");

    return Result::ok();
}

// 返回最后一次打开的文档,使用RecentlyOpenedFileList最近打开过的文件列表
File FilterGraph::getLastDocumentOpened()
{
    RecentlyOpenedFilesList recentFiles;
    recentFiles.restoreFromString (appProperties->getUserSettings()
                                   ->getValue ("recentFiles"));

    return recentFiles.getFile (0);
}

// 设置最近所打开的文档,同样使用RecentlyOpenedFileList最近打开过的文件列表
void FilterGraph::setLastDocumentOpened (const File& file)
{
    RecentlyOpenedFilesList recent;
    recent.restoreFromString (appPros->getUserSettings()->
                              getValue ("recentFiles"));
    recent.addFile (file);
    appPros->getUserSettings()->setValue ("recentFiles", recent.toString());
}

FileBasedDocument类继承自ChangeBroadcaster可变消息生成类,因此,持有并使用该类对象的内容组件(或更上层的组件,比如DocumentWindow主窗口类)可继承ChangeListener可变捕获类,在changeListenerCallback()中捕获并处理FileBasedDocument所产生的消息。同时,内容组件类或更上层组件的构造函数或有关函数中,FileBasedDocument对象需addChangeListener(this)。

注意,FileBasedDocument对象无需调用基类ChangeBroadcaster的sendChangeMessage()来发送可变消息。该对象每次调用changed()均起到相同的作用。changed()函数不仅发送可变消息,还可将当前文档的状态置为“尚未保存”。