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

суббота, 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);
}

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

среда, 5 сентября 2012 г.

Формирование QString (Qt) из std::string содержащих национальные символы

Как сделать QString (Qt) из std::string с русскими буквами

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

Работа выполняется в Linux. Дистрибутив Ubuntu 11.10. Локаль - UTF-8.

И вот возникла у меня проблема. Средствами boost::system::error_code, в ядре моей библиотеки формировалось некоторое локализованное сообщение (т.е. на русском языке), которое мне понадобилось отобразить средствами Qt в экземпляре класса QTextEdit.

Вообще, надо бы давно запретить использование национальных языков в системных сообщениях. Однако, видимо, это кому-то кажется недемократичным, поэтому программисты ежедневно тысячами подрываются на этих проблемах и мы постоянно в готовых приложениях сталкиваемся с кракозябрами там, где могли бы прочитать нормальный английский текст. Ведь в чем проблема? Домохозяйки не удосуживаются читать даже сообщения прикладного уровня, и представить домохозяйку, которая поймет, что надо делать прочитав системное сообщение из сетевой подсистемы ядра типа "Адрес уже используется" на родном языке, мне кажется невероятным. А раз это не для домохозяйки, то зачем усложнять жизнь специалистам, которые все поголовно умеют читать по английски? К этому еще можно добавить проблему некорректных переводов. Понятно, вопрос риторический. Однако, хочется высказаться.

На самом деле проблемы я не понял и был крайне удивлен, когда преобразовав полученное сообщение из std::string в QString и выведя его потом в окно редактора я увидел кракозябры. Действительно. В чем может быть проблема? Если бы я, не дай бог, работал под Windows, то удивляться бы было не чему. Давно не интересовался как там дела сейчас, но во времена Windows XP меня веселило три одновременно используемые кодовые страницы в русских версиях операционной системы. Вот уж действительно, не операционная система а система заплаток. Но откуда могла взяться проблема в Linux? Если в ней, в используемой мной сборке, используется одна кодовая страница UTF-8 во всех подсистемах ядра и пользовательского пространства.

Проблема решется просто. Приведу ниже максимально подробный вариант решения с комментариями.

  QTextCodec *codec = QTextCodec::codecForName("UTF8");
  if (codec)
  {
    std::string str = boost_lib->getLastError(); // Get string from a boost library
    QByteArray ba(str.c_str());                  // Convert to QByteArray
    QString msg = codec->toUnicode(ba);          // Qt magic !!! 

    m_pteLog->append(msg);                       // Append msg to a QTextEdit object
  }

В общем, заплатку я нашел, но в проблеме разбираться не стал. Видимо, по умолчанию, текст получаемый в Qt из строковых однобайтовых типов воспринимаются в кодировке Latin1, поэтому и возникала, в моем случае, неприятная неправильная перекодировка.

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

вторник, 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 решение по внутреннему размещению виджетов лежит в правильном использовании менеджеров размещения.

понедельник, 28 ноября 2011 г.

Немного об ООП в контексте ObjectiveC, Qt и C++




Одновременно с ростом популярности поделок от компании Apple, растет интерес к языку программирования ObjectiveC, который, наряду с библиотекой ObjectiveC-классов Cocoa, широко применяется для программирования под современными платформами Mac OS X (персональные компьютеры от Apple) и iOS (портативные устройства от Apple).

Случилось так, что недавно мне подвернулась удача познакомиться с этим вплотную и, даже, неплохо подзаработать именно на программировании в ObjectiveC (Cocoa), используя популярную в Mac OS X бесплатную среду разработки Xcode.

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

Итак, по порядку. Начнем с общих вопросов объектно-ориентированного программирования (ООП).

Современный стереотип ООП


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

Таково, на мой взгляд, типичное, и, я бы добавил, - стереотипное, представление об ООП сегодня. Более того, в большинстве книг, посвященных языкам Java, Си++ или современным вариантам Pascal, при обсуждении вопросов ООП, согласно моему опыту, не касаются других вопросов. Я немного сомневаюсь насчет языка C#, так как недостаточно хорошо его знаю, но, видимо, там все тоже самое. Отсюда и стереотип относительно ООП, утверждённый в наших головах изучением этих популярных сегодня языков программирования.

Не буду более гулять вокруг да около и скажу о том, о чем знают или задумываются не все программисты, активно использующие ООП в своей деятельности. Речь идет о взаимодействии объектов, основных артефактов ООП.

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

А взаимодействие объектов, как говорит наука, может быть в стиле function-oriented и в стиле message-oriented. Последний вариант взаимодействия объектов, по мнению этой науки, более кошерный, но он не нашел применения в языках Java, C++ и современных объектно-ориентированных диалектах Pascal. Однако, именно message-oriented стиль общения объектов используется в ObjectiveC и является вариантом общения в Qt.

Рассмотрим эти стили взаимодействия объектов более подробно.

Function-oriented стиль взаимодействия объектов предполагает возможность вызова методов объектов напрямую, но следуя ограничениям наложенным инкапсуляцией. Правила инкапсуляции могут скрыть тот или иной член класса от использования в определённых контекстах. Выглядеть это может примерно так.

class A {
public:
  void fun_1();
private:
  void fun_2();
};

A *a = new A();

a->fun_1(); // Напрямую обратились к public-методу fun_1() объекта a, 
            // созданного по образу и подобию класса A.
a->fun_2(); // Так делать нельзя!!! Метод fun_2() закрыт правилами инкапсуляции
            // для внешнего использования.

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

Серьёзным недостатком данного способа общения объектов является возможная проблема невалидности указателя на объект класса, что не редкость для больших программ со сложным расчётом времени жизни объектов. Наверное все программисты сталкивались с тем, что программа пытается обратиться к методу объекта после того, как объект был уничтожен. Как правило, такая ситуация приводит к падению программы, что не нравится ни заказчикам, ни разработчикам.

Альтернативным способом взаимодействия объектов является способ, ориентированный на сообщения - message-oriented. Этот случай можно представить в виде аналогии отправки почтой некоторого письма предназначенного некоторому адресату. Если почта не найдет адресата, то письмо вернется к вам со специальным уведомлением о том, что адресат не найден.

Способ совершенно безопасный, но требует поддержки в виде реализации некоторого почтового диспетчера в виде специального run-time объекта. Недостатком этого способа можно сразу назвать потери времени, требуемые на работу почтового диспетчера при каждом обращении к объектам.

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

Теперь, пришло время рассмотреть две известные реализации message-oriented взаимодействия, которые были построены для расширения языков C и C++. Речь, идет об ObjectiveC и Qt, соответственно.

Сообщения в ObjectiveC


ObjectiveC был создан в начале 80-х годов 20-го столетия Бредом Коксом (Brad Cox) в стиле минимальных расширений к языку C, целью которых была реализация поддержки ООП в варианте, навеянном языком Smalltalk.

Тут сразу можно заметить ещё одну существенную разницу между языками C++ и ObjectiveC. На этот раз, не с точки зрения взаимодействия объектов, а с точки зрения совместимости с самим C. Для ObjectiveC справедливо правило: "Любая программа на языке C является программой на языке ObjectiveC.". Для C++ последнее несправедливо.

Разумеется, если мы, по прошествии более 30 лет с момента появления ObjectiveC, говорим о нем как о языке, который является фактически основным языком программирования на популярных платформах от Apple, то понятно, что у него была относительно успешная история. Сегодня мне известны две реализации ObjectiveC: одна из них используется в Apple вместе с важнейшей для "яблочного" программотворчества библиотекой классов Cocoa, а вторая входит в состав коллекции компиляторов GNU и может быть установлена для работы в любом *nix. Вариант ObjectiveC от Apple можно тоже попробовать бесплатно, если использовать популярную для Mac OS X среду разработки Xcode.

Если вам интересно узнать историю языка и поверхностные подробности его синтаксиса и семантики, то, я бы посоветовал отличную статью в википедии по адресу http://ru.wikipedia.org/wiki/Objective-C. Здесь мы не будем повторяться и поговорим лишь о взаимодействии объектов.

В языке ObjectiveC, если у нас есть объект a, то мы можем послать ему сообщение message, например, таким образом:

[a message];

Для простоты даже не будем говорить о передаче параметров в сообщение, а поговорим только о том, как это работает. В составе средств ObjectiveC имеется специальная система run-time, в которой есть диспетчер, обрабатывающий все сообщения направляющиеся объектам. В рамках правил языка определяются жизненные циклы объектов, которые во всех подробностях известны этому диспетчеру. Таким образом диспетчер точно знает, существует ли в данный момент объект, которому назначается сообщение или нет. Кроме того, диспетчер знает о том, какие сообщения и с какими параметрами принимает каждый объект учитывая всю иерархию классов объекта. Таким образом, диспетчер знает, может ли данный объект обработать данное сообщение. В случае, если что-то не так, то сообщение переадресуется обратно отправителю. Если все нормально, то объект получает предназначенное ему сообщение. Теоретически, вы можете отправлять любую строку соответствующую синтаксису сообщения - диспетчер разберётся что с ней делать.

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

Мы уже обсуждали безопасность такого способа передачи сообщений. Остается лишь добавить, что его реализация в ObjectiveC очень удачна и производительна, и многолетний опыт применения ObjectiveC в компании Apple является отличным тому подтверждением.

Сообщения в Qt


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

Неправильно это по двум серьезным причинам.

Во-первых, код Qt написан не на языке С++, а на специальном расширении этого языка, поддерживающим предварительную обработку кода специальным метакомпилятором (утилита moc - Meta Object Compiler). Правильным будет еще упомянуть утилиту uic (User Interface Compiler), которая преобразует некое XML-описание форм графического интерфейса, созданное графическим редактором форм, в код на Qt. Утилитой uic я не пользуюсь, предпочитая описывать интерфейс пользователя самостоятельно в коде программы.

Во-вторых, для поддержки системы взаимодействия объектов через систему так называемых сигналов и слотов, предлагаемых синтаксисом и семантикой moc, каждая программа, написанная с использованием Qt включает в себя некий runtime-диспетчер, управляющий этим взаимодействием.

История Qt, для меня, тесно пересекается с историей Java (0ak). Обе платформы были задуманы примерно в одинаковое время - в начале 90-х годов 20-го столетия. Наверное, годом рождения обоих идей и начальной реализацией можно назвать 1991 год. Кроме того, библиотеки классов, относящиеся к Qt и Java SE, на мой взгляд, многим похожи. Одна идея кроссплатформенна (Qt, попытка расширить ООП концепции языка C++), а вторая межплатформенна (Java, возможность реализовать ООП без наследия неудач C++).

Основателями Qt были программисты Хаавард Норд и Эрик Чамбенг. Первый, вскоре стал главным управляющим, а второй - президентом организованной ими компании Trolltech, которая и занималась развитием и продвижением Qt до января 2008 года, когда компания была выкуплена Nokia и была переименована в "Qt Software". Значение Qt в современном мире программирования трудно переоценить, о чем можно легко найти в Интернет много информации по запросам типа "история Qt".

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

Если какой-то объект, написанный в стиле Qt, хочет что-то безопасно сообщить другому объекту Qt, то он излучает сигнал, синтаксис которого описывается при создании объекта. Объект может излучать только те типы сигналов, которые он зафиксировал при своём описании. Сигнал определяется своей сигнатурой, которая определяется типами и последовательностью его параметров. Т.е. разные объекты могут эмитировать одинаковые сигналы - сигналы с одной и той же сигнатурой.

Приведем пример описания сигналов.

signals:
  void clicked();
  void itemChanged(int index);

Здесь описаны сигналы с разными сигнатурами. Сигнал clicked() не передает в себе параметров, а сигнал itemChanged(int index) передает целое значение.

Чтобы выполнить эмиссию сигнала в коде объекта, в котором сигнал описан, надо написать так:
emit clicked();
  ...
  int x = ...
  emit itemChanged(x);

Теперь, если некий объект, написанный в стиле Qt, хочет получить возможность реагировать на сигналы определённой сигнатуры, то он должен описать слот указанной сигнатуры.

Приведем пример того, как можно оформить слоты для описанных выше сигналов.

public slots:
  void first();
  void second(int index);

С точки зрения языка Си++, слоты являются обычными методами, т.е. где-то должна быть написана реализация этих методов. Более того, эти методы могут быть вызваны обычным образом, как и любой другой метод класса, учитывая правила инкапсуляции. Т.е. слоты, также как и другие члены класса, могут быть описаны с квалификаторами области видимости public, protected или private, только к ним добавляется ключевое слово slots, обрабатываемое метакомпилятором.

Теперь мы подошли к тому, как можно подключить сигнал одного объекта к слоту другого (или того же самого) объекта. Тут я провожу аналогию с электронными микросхемами, каждая из который имеет набор выходов и входов в согласии с некой спецификацией (аналогом сигнатуры). В случае электронных компонентов, мы проводниками разводим выводы одних объектов на входы других. В случае объектов Qt мы выполняем метаподключение сигнала одного объекта на слот другого объекта. Приведем пример как можно выполнить связываение двух сигналов объекта pObjectA с двумя сигналами объекта pObjectB, на основе примеров сигналов и слотов приведенных в пример ранее.

connect(pObjectA, SIGNAL(clicked()), pObjectB, SLOT(first()));
connect(pObjectA, SIGNAL(itemChanged(int)), pObjectB, SLOT(second(int)));

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

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

Сравнение ObjectiveC и Qt


Я бы выделил три существенных различия в реализации message-oriented взаимодействия в ObjectiveC и Qt.

1. Для ObjectiveC передача сообщения является единственным способом обращения к методу объекта. Обращаться к полям объекта можно безо всяких фокусов, но такое обращение, в общем случае, некошерно, учитывая отсутствие надежных правил инкапсуляции данных в объектах ObjectiveC. Для объектов Qt общение через систему сигналов и слотов является не более чем расширением возможностей function-oriented возможностей взаимодействия объектов C++.

2. Сообщения ObjectiveC могут быть динамически созданы и переданы строкой, которая потом будет разобрана диспетчером сообщений, в то время как для Qt и сигналы и слоты должны быть описаны до этапа метакомпиляции.

3. Qt не имеет средств ускорения взаимодействия сигнал-слот за счет возможности запоминания хешированной сигнатуры сообщения.

Отдельно хочется заметить общие особенности между ObjectiveC и Ot. Оба эти артефакта расширяют существующие популярные языки программирования. ObjectiveC расширяет язык C, а Qt расширяет язык C++. В обоих случаях это расширение, фактически представлено предварительной метаобработкой кода и runtime поддержкой полученного кода при его исполнении специальными диспетчерами. Так же как "код на языке C является кодом для ObjectiveC", так же "код на языке C++ является кодом для Qt". Последнее свойство Qt я последнее время активно использую в разработке на языке C++. Мне настолько нравится среда разработки QtCreator, которая используется для кроссплатформенной разработки в Qt, что я также использую её и для обычных кроссплатформенных разработок на языке C++, используя в качестве кроссплатформенных средств сборки qmake и cmake.