Evgenii Legotckoi
03 жовтня 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.

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QGraphicsScene>
#include <QLineEdit>

#include <square.h>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

private:
    Ui::MainWindow *ui;
    QGraphicsScene *scene;  // Объявляем графическую сцену
    Square *square;         // Объявляем квадрат, в который будем передавать callback функцию
    static QLineEdit *line1;    // Объявляем static QLineEdit, с которым будет работать callback функция
    static QLineEdit *line2;    // Объявляем static QLineEdit, с которым будет работать callback функция

private:
    // Объявляем callback функцию
    static void getPosition(QPointF point);
};

#endif // MAINWINDOW_H

mainwindow.cpp

Крім оголошення статичних об'єктів QLineEdit їх ще необхідно і реалізувати як функції у файлі вихідних кодів, інакше компілятор оголошуватиме помилку. Справа в тому, що статичні об'єкти обов'язково потрібно ініціалізувати.

#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // Инициализируем объекты QLineEdit
    line1 = new QLineEdit();
    line2 = new QLineEdit();

    // Устанавлвиваем их в gridLayout
    ui->gridLayout->addWidget(line1,0,1);
    ui->gridLayout->addWidget(line2,0,2);

    scene = new QGraphicsScene();       // Инициализируем графическю сцену
    ui->graphicsView->setScene(scene);  // Устанавливаем сцену в graphicsView
    scene->setSceneRect(0,0,300,300);   // Устанавливаем область сцены
    square = new Square();              // Инициализируем квадрат
    square->setCallbackFunc(getPosition);   // Устанавливаем в квадрат callback функцию
    square->setPos(100,100);            // Устанавливаем стартовую позицию квадрата
    scene->addItem(square);             // Добавляем квадрат на графическую сцену
}

MainWindow::~MainWindow()
{
    delete ui;
}

/* callback функция получает позицию квадрата
 * и помещает его координаты в line1 и line2
 * */
void MainWindow::getPosition(QPointF point)
{
    line1->setText(QString::number(point.x()));
    line2->setText(QString::number(point.y()));
}

QLineEdit * MainWindow::line1;
QLineEdit * MainWindow::line2;

квадрат.ч

Цей клас успадковується від QGraphicsItem і в ньому оголошується вказівник для CallBack функції, а також функція для її встановлення. Функцію для значень, що повертаються, необхідно вказувати в обов'язковому порядку.

#ifndef SQUARE_H
#define SQUARE_H

#include <QObject>
#include <QGraphicsItem>
#include <QPainter>
#include <QTimer>
#include <QPointF>

class Square : public QObject, public QGraphicsItem
{
    Q_OBJECT
public:
    explicit Square(QObject *parent = 0);
    ~Square();
    // Функция для установки callback функции
    void setCallbackFunc(void (*func) (QPointF point));

signals:

public slots:

protected:
    QRectF boundingRect() const;
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget);

private:
    QTimer *timer;  // игровой таймер
    // Указатель на callback функцию
    void (*callbackFunc)(QPointF point);

private slots:
    void slotTimer();   // слот игрового таймера

};

#endif // SQUARE_H

square.cpp

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

#include "square.h"
#include <windows.h>

Square::Square(QObject *parent) :
    QObject(parent), QGraphicsItem()
{
    // Инициализируем и настраиваем игровой таймер
    timer = new QTimer();
    connect(timer, &QTimer::timeout, this, &Square::slotTimer);
    timer->start(1000/33);
}

Square::~Square()
{

}

QRectF Square::boundingRect() const
{
    return QRectF(-15,-15,30,30);
}

void Square::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
    painter->setPen(Qt::black);
    painter->setBrush(Qt::green);
    painter->drawRect(-15,-15,30,30);

    Q_UNUSED(option);
    Q_UNUSED(widget);
}

void Square::slotTimer()
{
    // В зависимости от нажатых кнопок перемещаем квадрат по сцене
    if(GetAsyncKeyState('A')){
        this->setX(this->x() - 2);
    }
    if(GetAsyncKeyState('D')){
        this->setX(this->x() + 2);
    }
    if(GetAsyncKeyState('W')){
        this->setY(this->y() - 2);
    }
    if(GetAsyncKeyState('S')){
        this->setY(this->y() + 2);
    }
    // Вызываем callback функцию для передачи координат квадрата
    callbackFunc(this->pos());
}

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

Підсумок

В результаті у Вас має вийти Додаток, в якому виконується керування зеленим квадратиком, а через 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
  • 03 серпня 2020 р. 21:56

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

Evgenii Legotckoi
  • 04 серпня 2020 р. 15:24

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

R
  • 04 серпня 2020 р. 15:53

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

Коментарі

Only authorized users can post comments.
Please, Log in or Sign up