Показаны сообщения с ярлыком Готовые решения в коде. Показать все сообщения
Показаны сообщения с ярлыком Готовые решения в коде. Показать все сообщения

суббота, 6 октября 2012 г.

Как сделать "живую" кнопку с картинкой в Qt

... или QWidget с использованием CSS и обработка его событий HoverEnter и HoverLeave в обработчике eventFilter()

Задача о "живой" кнопке с картинкой в Qt

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

Итак, ниже будет продемонстрировано использование следующих техник Qt.

  1. Простейшее использование CSS для виджета Qt.
  2. Простейшее размещение и использование файла с картинкой в ресурсе Qt.
  3. Установка фильтра событий на объект класса QLabel и реализация фильтра событий.
  4. Обработка событий QEvent::HoverEnter и QEvent::HoverLeave.

Использование CSS в Qt

Графическая подсистема Qt имеет замечательные возможности поддержки стилей CSS, что позволяет создавать произвольной красоты интерьеры форм. Подробности применения этой техники мы опустим, чтобы не уходить от темы, но для демонстрации возможностей приведу пример кода создания виджета на котором средствами CSS реализован градиентный фон и граничная рамка.

    QWidget m_pwgtTitle;
    QString m_sStyleOnExpanded;

    ...

    m_sStyleOnExpanded = "* "
            " { "
            " border-width: 1px; "
            " border-style: solid; "
            " border-top-left-radius: 10px; "
            " border-top-right-radius: 10px; "
            " border-color: white; "
            " padding: 5px; "
            " background: qlineargradient(x1:0.5, y1:0, x2:0.5, y2:1, "
            " stop:0 #33CCCC, stop: 0.4 #33FFFF, stop:1 #339999) "
            " }";

    m_pwgtTitle = new QWidget(this);
    m_pwgtTitle->setStyleSheet(m_sStyleOnExpanded);

Из примера видно, что для код CSS надо записать в строковую переменную и передать ее значением в метод setStyleSheet() того виджета, к которому необходимо применить таблицу стилей.

Чтобы просто разместить на виджете картинку средствами CSS нужна значительно более простая запись стиля. Ниже показаны две строки стиля, которые будут использованы для нашего демо-виджета в обычном состоянии и в состоянии on-hover (под курсором мыши).

    m_sCallNormalStyle = "* { background-image: 
        url(:/images/images/green-phone-button.png); }";
    m_sCallOnHoverStyle = "* { background-image: 
        url(:/images/images/green-phone-button-on-hover.png); }";

В представленном примере указано, что изображения подгружаются из файла ресурса. Для тех, кто не умеет работать с ресурсами дополним описание решения маленьким разделом об использовании ресурсов.

Создание и использование ресурсов в Qt

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

Я создаю файл ресурсов средствами QtCreator. Так же, с помощью QtCreator я управляю этим файлом. Тех, кто хочет сделать это иначе можно обрадовать. Файл ресурсов это обычный текстовый файл с расширением *.qrc внутри которого содержится XML-описание ресурсов. Для того, чтобы система сборки Qt использовала обработку и линковку файла ресурсов, его надо включить в раздел RESOURCES проектного файла. Так, если вы имеете файл ресурсов с названием resourses.qrc, то он должен быть описан в файле проекта следующим образом.

RESOURCES += \
    resources.qrc

С помощью меню QtCreator, добавьте в проект файл ресурса, откройте его щелчком в панели файлов проекта, создайте там нужный префикс (раздел) и добавьте под этот префикс те файлы, которые вы хотите разместить в ресурсах. Файлы должны лежать в какой-нибудь поддиректории проекта, иначе они будут скопированы в корень проекта. Для размещения картинок я создаю поддиректорию images и файлы из этой директории размещаю под префиксом /images.

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

<RCC>
    <qresource prefix="/images">
        <file>images/green-phone-button.png</file>
        <file>images/green-phone-button-on-hover.png</file>
    </qresource>
</RCC>

Фильтры событий в Qt

Если объект не имеет в своем описании сигнала о некотором нужном нам событии, то это не повод для уныния. Прежде всего, можно унаследовать данный класс с целью переопределения виртуального callback-метода с нужным событием. Однако, кроме этого, Qt предоставляет более удобную возможность - установку в данный виджет фильтра события и в реализации этого фильтра поймать и обработать нужное нам событие.

Предположим, что некая форма F имеет размещенный на себе виджет W. И пусть в форме F мы хотим иметь обработчик некоторого события виджета W, которое не реализовано в виде сигнала. Тогда нам необходимо сделать следующее.

  1. Для формы F реализовать виртуальный метод virtual bool eventFilter(QObject *, QEvent *), который изначально было описан для класса QObject.
  2. Передать объект формы F как объект с реализацией фильтра событий в объект виджета W. Это делается с помощью метода installEventFilter(). Теперь объект виджета W будет вызывать метод фильтра объекта F при возникновении в объекте W разных событий.
  3. В реализации eventFilter() необходимо отловить требуемое событие и обработать его.

Приведем простейший пример обработки клавиатурных событий в объекте строки ввода QLineEdit.

// Описание класса формы со строкой ввода
class MyForm: public QWidget
{
public:
  MyForm();
...

protected:
  virtual bool eventFilter(QObject *, QEvent *)

private:
  QLineEdit *m_pleIn;
...
};

...

// Реализация конструктора
MyForm::MyForm()
{
  // Создаем объект строки ввода
  m_pleIn = new QLineEdit(this);

  // Передаем объект формы делегатом для 
  // получения событий от объекта строки ввода
  m_pleIn->installEventFilter(this);
}

bool MyForm::eventFilter(QObject *obj, QEvent *event)
{
    if (obj != m_pleIn) return false;

    if (event->type() == QEvent::KeyPress) {
        // Обрабатываем событие нажатия на клавишу
        QKeyEvent *ke = static_cast(event);
        int key = ke->key();

        if (key == Qt::Key_Escape) {
            // Обрабатываем клавишу Escape
            ... 
            return true;
        }

        if (key == Qt::Key_Up) {
            // Обрабатываем клавишу стрелка вверх
            ... 
            return true;
        }

        if (key == Qt::Key_Down) {
            // Обрабатываем клавишу стрелка вниз
            ... 
            return true;
        }
    }
    return false;
}

Обработка событий QEvent::HoverEnter и QEvent::HoverLeave

Данные события являются событиями мыши, которые возникают при наведении мыши на виджет (QEvent::HoverEnter) и при уходе мыши с поверхности виджета (QEvent::HoverLeave).

Особенностью обработки событий QEvent::HoverEnter и QEvent::HoverLeave в Qt является то, что для их обработки необходимо, чтобы виджет, для которого мы хотим обработать данные события, имел установленным атрибут Qt:WA_HOVER. Только после установки этого атрибута можно рассчитывать на возможность обработки этих событий в методе фильтра eventFilter().

Для нашей будущей демо-кнопки установка этого атрибута будет выполнена так.

  m_pwgtCall->setAttribute(Qt::WA_Hover);

Решение задачи о "живой" кнопке

Итак, мы поговорили о всех техниках Qt, которые используются для решения поставленной задачи. Настало время привести и какой-нибудь вариант решения.

// заголовочный файл класса SipAgentPanel
class SipAgentPanel : public QWidget
{
    Q_OBJECT
public:
  SipAgentPanel(QWidget *parent = 0);

protected:
  bool eventFilter(QObject *, QEvent *);

private:
  QWidget *m_pwgtCall;

  QString m_sCallNormalStyle;
  QString m_sCallOnHoverStyle;
};

// файл реализации класса SipAgentPanel
SipAgentPanel::SipAgentPanel(QWidget *parent)
    : QWidget(parent)
{
    m_sCallNormalStyle = "* { background-image: 
        url(:/images/images/green-phone-button.png); }";
    m_sCallOnHoverStyle = "* { background-image: 
        url(:/images/images/green-phone-button-on-hover.png); }";

    m_pwgtCall = new QWidget(wgt);
   
    // Установим жестко размер виджета по размеру картинок 72x66
    m_pwgtCall->setFixedSize(72,66);

    m_pwgtCall->setStyleSheet(m_sCallNormalStyle);
    m_pwgtCall->setAttribute(Qt::WA_Hover);
    m_pwgtCall->installEventFilter(this);
}

bool SipAgentPanel::eventFilter(QObject * obj, QEvent * event)
{
    if (obj == m_pwgtCall) {
        QEvent::Type type = event->type();
        if  (type == QEvent::HoverLeave) {

            m_pwgtCall->setCursor(Qt::ArrowCursor);
            m_pwgtCall->setStyleSheet(m_sCallNormalStyle);

        } else if (type == QEvent::HoverEnter) {

            m_pwgtCall->setCursor(Qt::PointingHandCursor);
            m_pwgtCall->setStyleSheet(m_sCallOnHoverStyle);

        } else if (type == QEvent::MouseButtonPress) {

            QMouseEvent *mev = static_cast(event);
            if (mev) {
                if (mev->button() == Qt::LeftButton) {
                    // обработка события щелчка по объекту
                    ...
                }
            }

        }
    }

    return QWidget::eventFilter(obj, event);
}

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

вторник, 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);
}

пятница, 27 января 2012 г.

Размещение виджета в центре QScrollArea

Только что столкнулся в Qt с тривиальной задачей, на решение которой у меня ушло более часа - видимо подвел тип мышления, развитый многими годами работы с библиотекой VCL.

Задача такая. Необходимо разместить виджет в скроллингуемой области QScrollArea. При этом, возможно два случая. В первом случае, размер размещаемого виджета больше размеров QScrollArea. Это нормальный случай использования скроллингуемых областей. Здесь нужно просто настроить работу скроллеров согласно документации на QScroolArea. Во втором случае, размер размещаемого виджета меньше размеров QScrollArea. В этом, не таком уж редком случае использования QScrooArea, хочется видеть размещаемый виджет в центре скроллингуемой области. И вот на эту простую задачу я и потратил неправомерно много времени, о чем и решил сейчас рассказать.

Если не погружаться в те заблуждения, которые повлияли на поиск решения, то следует признаться, что решение совершенно типично для Qt и проблемы были именно во влиянии VCL. Однако, на случай разного рода иных влияний, возможно, для кого-то эти строки окажутся полезными.

Решение выглядит следующим образом.

QScrollArea *area = new QScrollArea(parent_widget);

    MyWidget myWidget = new MyWidget(area);

    // ключ к решению - менеджер размещения с необходимой настройкой по выравниванию
    QVBoxLayout *vb = new QVBoxLayout(area->viewport());
    vb->addWidget(myWidget);
    vb->setAlignment(myWidget, Qt::AlignCenter);

Таким образом, как видно из представленного кода, типичное для Qt решение по внутреннему размещению виджетов лежит в правильном использовании менеджеров размещения.

среда, 21 декабря 2011 г.

Кроссплатформенный парсинг конца строки


Лирическое вступление. Мое погружение в SNMP.


Компания, в которой я имел счастье работать до начала 2011 года занималась, ни много ни мало, разработкой, изготовлением, внедрением и поддержкой собственного телекоммуникационного оборудования. Занимаясь разработкой программного комплекса призванного мониторить и управлять всем этим хозяйством из тысяч разнотипных устройств вполне логично было столкнуться с использованием протокола SNMP.

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

Комплекс, который следовало адаптировать под SNMP представляет собой сложную клиент-серверную систему состоящую из серверных сетей и произвольного числа клиентов.

У каждого творца есть свой порог детализации используемых им объектов. Все чаще я встречаю разработчиков, которые стараются мыслить категориями наиболее верхних порядков. Меня, наоборот, более привлекают низкоуровневые категории. Отчасти поэтому, я решил подойти к SNMP на максимально низком уровне, т.е. с нуля, без использования какого-либо готового кода. Такой подход не только удовлетворил моё любопытство (за счёт компании, разумеется), но и дал максимальную свободу во внедрении протокола в готовую систему на обоих её концах (клиенте и сервере).

Благо, что сама сущность протокола в системе была выделена на уровне готовой спецификации. Любой новый протокол мог быть вставлен в систему обычной динамической библиотекой, удовлетворяющей сформулированной спецификации. Протокольная сущность должна была не только разрешать проблемы кодирования и декодирования согласно особенностям самого протокола, но и конвертировать логику интерфейса верхних слоев системы в систему запросов конкретного протокола. На момент начала внедрения SNMP система уже работала с двумя разными протоколами (родным K095 и неким msk-json, используемом на одном из типов серверов компании).

Первое с чем я столкнулся, это низкая детализация и популяризация вопроса несмотря на обширное количество Интернет-ресурсов, посвящённых вопросам SNMP. Опираясь на найденные мною материалы крайне тяжело было понять, что же собственно надо написать, чтобы ЭТО заработало.

Прошло около недели изучения разного рода документации, пока, наконец в моей голове появилось понимание таких важных для программирования протокола терминов, как SNMP, MIB, ASN.1, BER в едином контексте поставленной задачи.

Наконец я понял как от терминов верхнего уровня протокола SNMP (запросы get, get-next, set и пр.) перейти к последовательности байт, которые отражают эти запросы в сетевом трафике. С этого момента стало особенно интересно. Началось непосредственное кодирование.

Сначала написал код синтеза запросов и отправил его на готовый сервер. К радости, получил с сервера ответ. Написал парсер ответов. Подошёл вплотную к парсингу MIB-файлов. За два дня удалось выполнить синтаксический разбор MIB-а нашей компании. В общем, получилось некое тестовое приложение, написанное полностью с нуля (относительно категорий протокола SNMP), парсящее MIB-файл параметров устройств компании и выполняющих по нему систему запросов. В общем (творцы меня поймут) радости не было предела.

Кроссплатформенный парсинг конца строки


Теперь, все-таки, пора перейти к главному. Все написанное выше можно рассматривать как затянувшееся вступление, к тому, о чем, собственно, и хотелось рассказать мне в этом посте.

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

Напомню, что конец строки, в разных операционных системах представляется по разному и используется для это манипуляция с кодами 0×0A (line feed) и 0×0D (carret return). Такая ситуация усложняет реализацию счетчика строк при потоковом парсинге файла. Так как речь идет о кроссплатформенной системе, то вопрос особенно актуален. Хотя, даже если речь идёт о какой-то конкретной платформе, не факт, что вы будете иметь дело с файлом подготовленным на такой же платформе, в редакторе, который формирует окончание строки в удобном вам виде.

Детализирую проблему.
1. Мы не знаем, один или два кода используются для разделения строк.
2. Мы не знаем, является ли первым (а может и единственным) кодом, код 0×0A или код 0×0D.

Такая постановка вопроса не позволяет тупо инкрементировать счётчик строк по какому-либо из этих двух кодов. Для тех, кто ещё мучается данной проблемой, возможно, будет интересно посмотреть моё решение.

// m_ucNL - флаг переноса
            // m_iRow - счетчик строк
            // m_iCol - счетчик колонок (позиция символа в строке) 

            // берем очередной символ из потока
            unsigned char ch = pchData[i++];

            if (ch == 0x0a || ch == 0x0d) {
                if (m_ucNL == 0) m_ucNL = ch;
                /* такое разделение условий позволит переключатся только по первому коду!!! */
                if (m_ucNL == ch) {
                    ++m_iRow; // инкрементируем счетчик строк
                }
                m_iCol = 0;
            } else {
                m_ucNL = 0;
                ++m_iCol;
            }

Дополнение от 14 февраля 2012 года

Один из моих знакомых, читателей данного блога, недавно сообщил мне, что нашел ошибку в данном счетчике строк. По его мнению, последовательности типа "\x0A\x0A" (*nix) или "\x0D\x0A\x0D\x0A" (DOS/Windows) будут восприняты таким счетчиком как одна строка.

На случай, если кто-то еще опасается за такую ошибку счетчика, сообщаю, что все нормально. Ошибки нет. Обратите внимание, что флаг m_ucNL приобретает значение только если прежде его значение было равно нулю. Следовательно в последовательности символов разделителей флаг получит значение первого символа. Далее, обратите внимание на то, что инкремент счетчика строк возникает только при условии равенства флага m_ucNL текущему символу разделителя, т.е. счетчик строк будет инкрементироваться только по каждой новой строке.