вторник, 20 марта 2012 г.

Схема управления файлом в редакторе (New - Open - Save - Save as)

Вступление


При создании как простых так и сложных редакторов любого типа файлов, как правило, возникает необходимость реализации схемы New - Open - Save - Save as. Каждый раз, реализуя эту схему с нуля я обратил внимание на то, что код не всегда получается одинаково красивым. Тут нет претензии на абсолютный вариант красоты, просто, в очередной раз, написав относительно удачную схему, я решил увековечить её в своём блоге. Как минимум - для личного использования. Не исключено, что кому-нибудь из читателей такая схема покажется удачной. Если же нет - то будет, что обсуждать и совершенствовать.


Так как последняя удачная схема управления файлами была выполнена в Qt, то я оставлю некоторые элементы Qt в примерах кода. Думаю, что это мало будет отличаться от каких-либо вариантов использования псевдо-кода и не вызовет затруднения у тех читателей, кто с Qt не знаком.


Кроме прочего, публикуемая далее схема была предназначена для обработки файлов JavaScript и, поэтому, некоторые названия методов класса обработки скрипта несут в себе этот отпечаток. Наверное, это тоже не должно никого смущать, и, для упрощения публикации, я решил оставить все имена как есть.


Действующие лица

Для начала, опубликуем список действующих лиц схемы.




bool canCurrentScriptJobFinish()


Метод, проверяющий возможность нормального (неаварийного) завершения текущего сеанса редактирования файла.


bool save()


Метод, собственно, выполняющий сохранение файла.


bool saveAs()


Метод, исполняющий диалог выбора имени файла для сохранения. Сохранение выполняется через метод save().


bool openFile(const QString &sFileName)


Метод, выполняющий загрузку файла по указанному имени.


void setModified(bool)


Метод, устанавливающий значение флага изменения в редактируемом файле и влияющий на разного рода сопутствующие виджеты в пользовательском интерфейсе.


void setFileName(const QString &sFileName)


Метод, сохраняющий имя сохранённого файла и влияющий на разного рода сопутствующие виджеты в пользовательском интерфейсе.


void clearFileName()


Метод, выполняющий очистку поля с именем файла для сохранения содержимого окна редактора.


void slotNewClicked()


Метод, обработки отклика на событие запроса на создание нового файла.


void slotOpenClicked()


Метод, обработки отклика на событие запроса на открытие существующего файла.


void slotSaveClicked()


Метод, обработки отклика на событие запроса на сохранение редактируемого файла.


void slotSaveAsClicked()


Метод, обработки отклика на событие запроса на сохранение редактируемого файла под другим именем.


void slotScriptChanged()


Метод, обработки отклика на событие изменения файла.

Реализация


Суть схемы заключена в том, что сохранение файла выполняется только в одном методе save(). В нем выполняется сохранение файла при известном имени. Если имя не известно, то из save() вызывается saveAs(). Так же, saveAs() вызывается если требуется сменить имя файла. Сам метод saveAs() не сохраняет файл, а лишь запрашивает имя для его сохранения и сохраняет это имя внутри класса, вызывая потом метод save(), который и выполняет сохранения файла при определённом имени для сохранения. Таким образом, может образоваться рекурсия из вызовов save()->saveAs()->save()... или saveAs()->save()->saveAs()....


Другим "фокусом" схемы является централизация запросов о необходимости сохранения текущего измененного файла в специальном методе canCurrentScriptJobFinish(). Все запросы к интерфейсу по смене текущего файла зависят от исполнения данного метода. Это относится и к интерфейсному запросу на создание нового файла и на открытие существующего файла.



bool canCurrentScriptJobFinish()


В этом методе необходимо ответить на вопрос о том, можно ли закончить работу с текущим файлом (скриптом).


Если файл не был изменен, то мы отвечаем на этот вопрос положительно, возвращаем true.


Если файл был изменен, то мы делаем запрос о необходимости сохранить текущий файл и поступаем по дальнейшей ситуации. Если требуется его сохранить - пытаемся сохранить. Если не требуется, то просто забываем об изменениях.


bool ScriptForm::canCurrentScriptJobFinish()
{
    while (m_bIsModified) {
        QMessageBox msgBox;
        msgBox.setText("The document has been modified.");
        msgBox.setInformativeText("Do you want to save your changes?");
        msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::Discard | MessageBox::Cancel);
        msgBox.setDefaultButton(QMessageBox::Save);
        int ret = msgBox.exec();
        switch (ret) {
        case QMessageBox::Save:
            save();
            break;
        case QMessageBox::Discard:
            setModified(false);
            break;
        case QMessageBox::Cancel:
            return false;
        default:
            return false;
        }
    }

    return true;
}

bool save()


Выполняем сохранение файла. Если имя файла не определено, то сохраняем через saveAs(). Сохранение файла выполняем по простой последовательности.


  1. Получаем скрипт целиком в виде строковой переменной.
  2. Открываем файл.
  3. Связываем файл с текстовым потоком.
  4. Отправляем скрипт в поток.
  5. Закрываем файл файл.

Как только файл был сохранен, необходимо сбросить флаг наличия несохранённых изменений в файле.


bool ScriptForm::save()
{
    if (m_sFileName.isEmpty()) return saveAs();

    // Скрипт целиком в строковую переменную script
    QString script = m_pceScript->toPlainText().trimmed();
    if (script.isEmpty()) {
        QMessageBox::information(this, tr("Save Script"), tr("Qt Script is empty"));
        return false;
    }

    QFile file(m_sFileName);

    if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
        return false;
    }

    // Собственно, выполняем сохранение строковой переменной в файл
    QTextStream out(&file);
    out << script;
    file.close();

    // Сбросим флаг изменений в файле
    setModified(false);
    return true;
}

bool saveAs()

Данный метод вызывается для сохранения файла под именем, которое необходимо определить в диалоге с пользователем. Поэтому состоит метод из следующих действий.

  1. Получаем имя файла в диалоге с пользователем. Если пользователь отменил диалог, то завершаем метод возвратив false
  2. Сохраняем имя файла через специальный метод (свойство).
  3. Так как теперь имя файла для сохранения определено, то можно воспользоваться для сохранения уже готовым методом save().
bool ScriptForm::saveAs()
{
    QString sFileName = QFileDialog::getSaveFileName(this, tr("Save Qt Script"), ".");
    if (sFileName.isEmpty()) return false;

    setFileName(sFileName);
    return save();
}

bool openFile()

Назначение данного метода в открытии файла, заданного формальным параметром метода. Для этого:

  1. Открываем файл.
  2. Связываем файл с текстовым потоком.
  3. Получаем все содержимое файла из текстового потока в строковую переменную.
  4. Устанавливаем значение строковой переменной как содержимое виджета (элемента управления) редактора.

После того, как файл открыт на редактирование, необходимо, сохранить его имя как имя для сохранения изменений и сбросить флаг текущих изменений в файле.

bool ScriptForm::openFile(const QString &sFileName)
{
    QFile file(sFileName);
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        g_pLog->addError(tr("Can't open file: %1").arg(sFileName));
        return false;
    }

    m_pceScript->clear();
    QTextStream in(&file);
    QString s = in.readAll();
    m_pceScript->setPlainText(s);
    file.close();

    setFileName(sFileName);
    setModified(false);

    return true;
}

void setModified()

Процедура обслуживания свойства modified на запись. В зависимости от приложения может выполнять разные фокусы. У меня, данная процедура изменяла цвета разных полезных индикаторов изменения файла.

void ScriptForm::setModified(bool flag)
{
    if (m_bIsModified != flag) {
        m_bIsModified = flag;
        if (m_bIsModified) {
          // делаем что-то, чем надо обозначить изменение файла
        } else {
          // делаем что-то, чем надо обозначить закрытие изменений в файле
        }
    }
}

void setFileName(const QString &sFileName)

Процедура обслуживания свойства fileName на запись. В зависимости от приложения может выполнять разные фокусы. В примере, данная процедура изменяет виджет с именем загруженного файла.

void ScriptForm::setFileName(const QString &sFileName)
{
    m_sFileName = sFileName;
    m_pleFileName->setText(tr("File name: %1").arg(m_sFileName));
}

void clearFileName()

Основное назначение процедуры - сброс значения свойства fileName с именем файла для сохранения текущего содержимого окна редактора. В зависимости от приложения может выполнять разные фокусы. В примере, данная процедура изменяет виджет с именем загруженного файла.

void ScriptForm::clearFileName()
{
    m_sFileName.clear();
    m_pleFileName->setText(tr("File name:"));
}

void slotNewClicked()

Обработка события пользовательского интерфейса на создание нового файла. Проверяет возможность завершения работы с текущим файлом и, если такое разрешение есть, то выполняет очистку поля виджета (элемента управления) редактора, сброс имени файла для сохранения файла и сброс флага изменений.

void ScriptForm::slotNewClicked()
{
    if ( ! canCurrentScriptJobFinish()) return;

    m_pceScript->clear();
    clearFileName();
    setModified(false);
}

void slotOpenClicked()

Обработка события пользовательского интерфейса на открытие файла. Проверяет возможность завершения работы с текущим файлом и, если такое разрешение есть, то выполняет загрузку нового файла.

void ScriptForm::slotOpenClicked()
{
    if ( ! canCurrentScriptJobFinish()) return;

    QString sFileName = QFileDialog::getOpenFileName(this, tr("Open Qt Script"), ".");
    if (sFileName.isEmpty()) return;

    openFile(sFileName);
}

void slotSaveClicked()

Обработка события пользовательского интерфейса на сохранение файла. Просто вызывает процедуру save(), в которой может быть запущена последовательность вызовов saveAs()->save()....

void ScriptForm::slotSaveClicked()
{
    save();
}

void slotSaveAsClicked()

Обработка события пользовательского интерфейса на сохранение файла под именем, которое должно быть выбрано в диалоге с пользователем. Просто вызывает процедуру saveAs(), в которой может быть запущена последовательность вызовов save()->saveAs()....

void ScriptForm::slotSaveAsClicked()
{
    saveAs();
}

void slotScriptChanged()

Обработка события пользовательского интерфейса по изменению в редактируемом файле.

void ScriptForm::slotScriptChanged()
{
    setModified(true);
}