Evgenii Legotckoi
3 октября 2015 г. 22:56

Qt/C++ - Урок 026. Применение CallBack функции

Для передачи данных в Qt используется система сигналов и слотов, но это не означает, что нельзя применить старый проверенный способ, а именно использование CallBack функций. Дело в том, что использование CallBack функции является несколько более быстродействующим вариантом, чем сигналы и слоты. А также может быть более легким в применении, что касается того, что сигналы желательно отсоединять от слотов, когда объект посылающий сигнал уничтожается в программе и больше не используется. Этот момент особенно актуален, если учитывать то, что в C++ отсутствует сборщик мусора, как в Java или C# .

Принцип работы CallBack функции

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

Для примера будет использован класс, который отрисовывает квадрат на графической сцене и управляется клавишами W, A, S, D. При движении квадрат должен отсылать данные о своих координатах в класс, в котором был создан. То есть должен вызывать функцию данного класса в качестве своей CallBack функции.


Структура проекта

Структура проекта Для ознакомления с работой CallBack функции используем проект со следующей структурой:

  • mainwindow.h - Заголовочный файл главного окна приложения;
  • mainwindow.cpp - Файл исходных кодов главного окна приложения;
  • square.h - Заголовочный файл класса, объект которого будет использовать CallBack функцию.
  • square.cpp - файл исходных кодов данного класса;

mainwindow.ui

В главном окне в дизайнере закидываем графическую сцену, а объекты класса QLineEdit, в которых будут отображаться координаты создаём и устанавливаем вручную в данное окно. Поскольку данные объекты должны быть объявлены как static. Это же условие должно применяться и для CallBack функции. Она также должна быть объявлена как static .

mainwindow.h

Также в заголовочном файле главного окна объявляем объект класса Square.

  1. #ifndef MAINWINDOW_H
  2. #define MAINWINDOW_H
  3.  
  4. #include <QMainWindow>
  5. #include <QGraphicsScene>
  6. #include <QLineEdit>
  7.  
  8. #include <square.h>
  9.  
  10. namespace Ui {
  11. class MainWindow;
  12. }
  13.  
  14. class MainWindow : public QMainWindow
  15. {
  16. Q_OBJECT
  17.  
  18. public:
  19. explicit MainWindow(QWidget *parent = 0);
  20. ~MainWindow();
  21.  
  22. private:
  23. Ui::MainWindow *ui;
  24. QGraphicsScene *scene; // Объявляем графическую сцену
  25. Square *square; // Объявляем квадрат, в который будем передавать callback функцию
  26. static QLineEdit *line1; // Объявляем static QLineEdit, с которым будет работать callback функция
  27. static QLineEdit *line2; // Объявляем static QLineEdit, с которым будет работать callback функция
  28.  
  29. private:
  30. // Объявляем callback функцию
  31. static void getPosition(QPointF point);
  32. };
  33.  
  34. #endif // MAINWINDOW_H

mainwindow.cpp

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

  1. #include "mainwindow.h"
  2. #include "ui_mainwindow.h"
  3.  
  4. MainWindow::MainWindow(QWidget *parent) :
  5. QMainWindow(parent),
  6. ui(new Ui::MainWindow)
  7. {
  8. ui->setupUi(this);
  9.  
  10. // Инициализируем объекты QLineEdit
  11. line1 = new QLineEdit();
  12. line2 = new QLineEdit();
  13.  
  14. // Устанавлвиваем их в gridLayout
  15. ui->gridLayout->addWidget(line1,0,1);
  16. ui->gridLayout->addWidget(line2,0,2);
  17.  
  18. scene = new QGraphicsScene(); // Инициализируем графическю сцену
  19. ui->graphicsView->setScene(scene); // Устанавливаем сцену в graphicsView
  20. scene->setSceneRect(0,0,300,300); // Устанавливаем область сцены
  21. square = new Square(); // Инициализируем квадрат
  22. square->setCallbackFunc(getPosition); // Устанавливаем в квадрат callback функцию
  23. square->setPos(100,100); // Устанавливаем стартовую позицию квадрата
  24. scene->addItem(square); // Добавляем квадрат на графическую сцену
  25. }
  26.  
  27. MainWindow::~MainWindow()
  28. {
  29. delete ui;
  30. }
  31.  
  32. /* callback функция получает позицию квадрата
  33. * и помещает его координаты в line1 и line2
  34. * */
  35. void MainWindow::getPosition(QPointF point)
  36. {
  37. line1->setText(QString::number(point.x()));
  38. line2->setText(QString::number(point.y()));
  39. }
  40.  
  41. QLineEdit * MainWindow::line1;
  42. QLineEdit * MainWindow::line2;

square.h

Данный класс наследуется от QGraphicsItem и в нём объявляется указатель для CallBack функции, а также функция для её установки. Функцию для возвращаемых значений необходимо указывать в обязательном порядке.

  1. #ifndef SQUARE_H
  2. #define SQUARE_H
  3.  
  4. #include <QObject>
  5. #include <QGraphicsItem>
  6. #include <QPainter>
  7. #include <QTimer>
  8. #include <QPointF>
  9.  
  10. class Square : public QObject, public QGraphicsItem
  11. {
  12. Q_OBJECT
  13. public:
  14. explicit Square(QObject *parent = 0);
  15. ~Square();
  16. // Функция для установки callback функции
  17. void setCallbackFunc(void (*func) (QPointF point));
  18.  
  19. signals:
  20.  
  21. public slots:
  22.  
  23. protected:
  24. QRectF boundingRect() const;
  25. void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget);
  26.  
  27. private:
  28. QTimer *timer; // игровой таймер
  29. // Указатель на callback функцию
  30. void (*callbackFunc)(QPointF point);
  31.  
  32. private slots:
  33. void slotTimer(); // слот игрового таймера
  34.  
  35. };
  36.  
  37. #endif // SQUARE_H

square.cpp

Данный пример сходен по механике с игрой, поэтому не удивляемся наличию игрового таймера. По слоту, подключённому к сигналу от таймера мы будем реализовывать движения квадрата по графической сцене и передавать его координаты в CallBack функцию. А для проверки состояния целевых кнопок воспользуемся функционалом WinAPI.

  1. #include "square.h"
  2. #include <windows.h>
  3.  
  4. Square::Square(QObject *parent) :
  5. QObject(parent), QGraphicsItem()
  6. {
  7. // Инициализируем и настраиваем игровой таймер
  8. timer = new QTimer();
  9. connect(timer, &QTimer::timeout, this, &Square::slotTimer);
  10. timer->start(1000/33);
  11. }
  12.  
  13. Square::~Square()
  14. {
  15.  
  16. }
  17.  
  18. QRectF Square::boundingRect() const
  19. {
  20. return QRectF(-15,-15,30,30);
  21. }
  22.  
  23. void Square::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
  24. {
  25. painter->setPen(Qt::black);
  26. painter->setBrush(Qt::green);
  27. painter->drawRect(-15,-15,30,30);
  28.  
  29. Q_UNUSED(option);
  30. Q_UNUSED(widget);
  31. }
  32.  
  33. void Square::slotTimer()
  34. {
  35. // В зависимости от нажатых кнопок перемещаем квадрат по сцене
  36. if(GetAsyncKeyState('A')){
  37. this->setX(this->x() - 2);
  38. }
  39. if(GetAsyncKeyState('D')){
  40. this->setX(this->x() + 2);
  41. }
  42. if(GetAsyncKeyState('W')){
  43. this->setY(this->y() - 2);
  44. }
  45. if(GetAsyncKeyState('S')){
  46. this->setY(this->y() + 2);
  47. }
  48. // Вызываем callback функцию для передачи координат квадрата
  49. callbackFunc(this->pos());
  50. }
  51.  
  52. void Square::setCallbackFunc(void (*func)(QPointF point))
  53. {
  54. // Устанавливаем указатель на callback функцию
  55. callbackFunc = func;
  56. }

Итог

В результате у Вас должно получиться Приложение, в котором производится управление зеленым квадратиком, а через CallBack функцию высылаются данные о его координатах в класс главного окна.

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

Приложение с CallBack функцией

Видеоурок

Рекомендуемые статьи по этой тематике

По статье задано1вопрос(ов)

6

Вам это нравится? Поделитесь в социальных сетях!

ЛП
  • 27 марта 2017 г. 14:17

Хорошо объяснено, но картинку я бы заменил =) А зачем создавать статические объекты?

QLineEdit * MainWindow::line1;
Evgenii Legotckoi
  • 27 марта 2017 г. 14:45

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

Недостаток статических методов в том, что они не будут работать с полями членами класса, если те не будут статическими. Есть конечно, ещё варианты несколько иначе получать id окна, с помощью API операционной системы, по нему кастовать полученный объект в MainWindow, а потом пройтись по child Объектам, найти нужный QLineEdit. Слишком геморно и вообще статья не о том.

ЛП
  • 27 марта 2017 г. 16:59

У меня еще вопросы! 1.

callbackFunc(this->pos());
"записали" координаты в указатель на функцию без реализации. 2.
 
void Square::setCallbackFunc(void (*func)(QPointF point))
{
    // Устанавливаем указатель на callback функцию
    func = callbackFunc ; // не логично ли такое присваивание?
}
Evgenii Legotckoi
  • 27 марта 2017 г. 17:47

1. Что значит без реализации? Это функция имеет реализацию. И реализацией это функции является метод:

void MainWindow::getPosition(QPointF point)

Да, это работать не будет, если не будет установлена соответствующая функция перед тем, как её использовать, но она в этом примере устанавливается:

square->setCallbackFunc(getPosition);   // Устанавливаем в квадрат callback функцию

2. Вообще никакой логики:

void Square::setCallbackFunc(void (*func)(QPointF point))
{
    // Устанавливаем указатель на callback функцию
    func = callbackFunc ; // не логично ли такое присваивание?
}

В корне неправильный подход. func - Это аргумент, который передаётся в класс для установки в качестве callback . А переменная callbackFunc - это просто указатель на функцию, в который нужно установить эту callback функцию, то есть аргумент func . Изначально там содержится nullptr , а если будет вызываться nullptr , то будет краш программы. А если Вы попытаетесь установить nullptr в указатель, который указывает на метод класса, который реализован, то будет краш программы.

Проще говоря, работать даже не будет. Это ещё с объектами такое можно будет сделать, когда например забрать указатель на какой-то объект класса извне. Да и то это в корне неправильный подход. Для такого существуют функции геттеры ( SomeClass* getSomeObject() , например )

ЛП
  • 27 марта 2017 г. 20:35

А что означает

callbackFunc(this->pos())
? Я просто меняю значение аргументов у указателя?
Evgenii Legotckoi
  • 27 марта 2017 г. 21:46

callbackFunc - это указатель на некую функцию, сигнатура которой указана в заголовочном файле класса Square

В качестве callbackFunc выступает метод void MainWindow::getPosition(QPointF point) . Посмотрите сигнатуру (сигнатура, то есть объявление идентично сигнатуре callbackFunc ) и реализацию этого метода. Вот что в нём реализовано то он и делает. А аргументы в функцию передаются, а что с ними уже происходит - это вопрос реализации.

c
  • 21 апреля 2018 г. 21:53

I don’t understand in Mainwindow.cpp lines 40 + 41 what or how these lines work? They look like a declaration but they are in the implementation which doesnt make sense to me. Please explain:


  1. QLineEdit * MainWindow::line1;
  2. QLineEdit * MainWindow::line2;
Evgenii Legotckoi
  • 22 апреля 2018 г. 16:34

There are static members of class. There in cpp file it isn`t declaration of these members, it`s implementation without assigning a value. Some value will be assigned to these members in constructor later.

c
  • 22 апреля 2018 г. 17:26
That is what I thought however do not understand why it is necessary. I guess the format was new and unfamiliar to me. Found other examples online where the value was assigned to NULL. That worked as well.
Evgenii Legotckoi
  • 22 апреля 2018 г. 17:30

It is especciality of workflow with static members.
And I think using of nullptr instead of NULL is better. Because of using of nullptr is modern standard of C++.

А
  • 12 февраля 2019 г. 14:19
  • (ред.)

День добрый! Можешь выложить форму mainwindow.ui от урока? Заранее спасибо

А
  • 12 февраля 2019 г. 15:26

Сам разборался, спасибо.

R
  • 3 августа 2020 г. 21:56

Добрый день, объясните, пожалуйста, почему функция объявлена статической?

Evgenii Legotckoi
  • 4 августа 2020 г. 15:24

Если не объявлять статической, то не соберётся. Не получится сделать привязку метода.
Дело в том, что в процессе компиляции производится сборка с указанием конкретных участков кода в данном случае. А передача в качестве аргумента нестатического метода приводит к привязке к динамически вызываемой части кода, поскольку объект должен быть создан в процессе работы программы.
Обычные callback функции не должны быть частью класса, но статические методы являются глобальными для класса. Поэтому есть возможность их передавать в качестве callback.
Но вообще это вполне возможно сделать без статического объявления функции, если использовать в качестве передаваемого аргумента std::function объект . И в данном случае уже передавать лямбда функцию с замыканием на конкретный объект, то есть на объект MainWindow. Тогда всё можно будет сделать без статики.

R
  • 4 августа 2020 г. 15:53

Спасибо огромное!

Комментарии

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