В разделе 14.6 (Простая анимация) третьего издания книги Страуструпа «Принципы и практика программирования с использованием C++» используется его графическая библиотека PPP, которая, в свою очередь, использует QT. Я пытаюсь понять, о чем идет речь. Следующая программа работает по назначению: изначально она отображает два черных круга. Через 2 секунды первый круг меняет цвет на красный. Еще через четыре секунды второй круг меняет цвет на синий. Страуструп пишет: «Вы также можете добавить действие («обратный вызов») в качестве аргумента. Это действие вызывается после задержки». Поэтому я поэкспериментировал, заменив указанные строки кода кодом, который это делает. Я ожидал, что результат будет точно таким же, но это не так. Через две секунды ничего не происходит, но через четыре секунды оба круга одновременно меняют цвет.
#include "PPP/Simple_window.h"
#include "PPP/Graph.h"
int main(int /*argc*/, char * /*argv*/[])
{
using namespace Graph_lib;
Application app;
// Window initially displays 2 black circles
Simple_window w {Point{0,0}, 600, 400, "Callback problem"};
Circle c1{{175, 200}, 100};
Circle c2{{425, 200}, 100};
c1.set_fill_color(Color::black);
c2.set_fill_color(Color::black);
w.attach(c1);
w.attach(c2);
// Code to be replaced:
w.timer_wait(2000); //2000 milliseconds
c1.set_fill_color(Color::red);
w.timer_wait(4000);
c2.set_fill_color(Color::blue);
//////////////////////////////
// // I expected this code to have the same result:
// w.timer_wait(2000, [&]{c1.set_fill_color(Color::red);});
// w.timer_wait(4000, [&]{c2.set_fill_color(Color::blue);});
// //////////////////////////////////////////////
w.wait_for_button();
}
Почему результат не тот? Могу ли я что-нибудь сделать, чтобы успешно использовать метод обратного вызова?
Вот код для timer_wait:
void WindowPrivate::timer_wait(int milliseconds, std::function<void()> cb)
{
if (!accept_waits)
return;
auto conn = std::make_shared<QMetaObject::Connection>();
*conn = QObject::connect(&user_timer, &QTimer::timeout,
[conn, func = std::move(cb)] {
QObject::disconnect(*conn);
func();
});
user_timer.start(milliseconds);
}
Аргумент обратного вызова подразумевает, что он будет вызван по истечении заданного времени без блокировки выполнения кода, что и делает только timer_wait()
с таймаутом. Наборы инструментов пользовательского интерфейса используют циклы событий, которые требуют, чтобы управление всегда возвращалось к ним как можно скорее для обработки событий, включая обновления пользовательского интерфейса (перерисовку): с помощью timer_wait(2000)
вы предотвращаете это, и все будет окончательно выполнено, как только цикл событий сможет быть должным образом обработаны, что wait_for_button()
, вероятно, и делает. Кроме того, в вашем коде окончательный результат будет равен 6 секундам, а не 4.
Мой секундомер показывает 4 секунды.
Ответы @musicamante должны находиться в разделе ответов.
@ 463035818_is_not_an_ai Я не могу дать соответствующий ответ, пока не будет предоставлено надлежащее MRE.
@musicamante Я надеялся на помощь кого-то, кто знаком с поддержкой PPP3 Страуструпа, возможно, как учителя или ученика. Определение Simple_window включает в себя несколько уровней поверх кода QT, и я признаю, что не могу дать MRE более удовлетворительного результата, чем то, что я уже опубликовал.
@freeze Странно, что там написано 4 секунды, поскольку у вас есть 2000+4000 мс «сна», если только timer_wait
внутренне не отслеживает предыдущие интервалы, что кажется странным выбором, но не невозможным (нам нужно посмотреть, как это на самом деле реализовано, вам следует хотя бы предоставить код, относящийся к этой функции). Тем не менее, суть остается: я не могу написать подходящий ответ для этого конкретного случая, но, учитывая поведение, мы, вероятно, сможем отослать вам много похожих постов здесь, на SO (вероятно, начиная с этого, но я оставлю это большему опыту работы с C++ »
@freeze » люди, чтобы найти более подходящие дубликаты); как уже было сказано, если timer_wait
только с аргументом интервала блокируется (а я предполагаю, что это так), это не позволяет циклу событий правильно обрабатывать события и, следовательно, перерисовывать пользовательский интерфейс с новыми изменениями: по умолчанию графические изменения планируют обновление. В стандартном Qt это можно частично обойти, используя QCoreApplication::processEvents(), но это не всегда приемлемо; другие альтернативы основаны на QTimer, что, похоже, и делает timer_wait
с обратным вызовом.
@musicamante Спасибо за ваши комментарии. Я отредактировал свое сообщение, включив в него код реализации функции timer_wait.
код все еще неполный. Хотя user_timer.start(milliseconds);
очень похоже на то, что Simple_window
имеет один таймер, который ожидает только время, прошедшее при последнем вызове.
@freeze Это функция, которая принимает обратный вызов. Для этой функции должно быть переопределение, которое принимает только тайм-аут, который и вызывает вашу «проблему». Проверьте еще раз, а также проверьте реализацию классов, от которых наследуется Simple_window
.
Мне удалось найти код в Интернете: github.com/villevoutilinen/… (ссылка на (официальном сайте)[ stroustrup.com/PPP3.html]). Блокировка вызвана nested_loop
QEventLoop, который теоретически должен позволять обрабатывать события в очереди, но также известно, что вложенные циклы иногда имеют неожиданное поведение. set_fill_color()
, похоже, вызывает redraw()
, что должно вызвать перерисовку родительского виджета, поскольку он использует QWidget repaint()
. Попробуйте вызвать redraw()
из c1
сразу после set_fill_color()
.
@musicamante Я полностью признаю, что эта дискуссия выходит за рамки моих нынешних способностей. Мой первоначальный вопрос был задан для того, чтобы узнать, неправильно ли я использую графическую библиотеку PPP. Из того, что я здесь понимаю, может показаться, что сама библиотека не выполняет того, что намеревается сделать. В частности, timer_wait при вызове с функцией в качестве аргумента фактически не ожидает заданное время (предположительно из-за неожиданного поведения вложенных циклов).
@musicamante Однако я отмечу, что neged_loop.quit() происходит только в переопределении timer_wait, которое НЕ принимает функцию в качестве аргумента. Моя проблема связана с переопределением, которое это делает.
@freeze Хорошо, теперь я понял: мы просто неправильно поняли себя. Ваша проблема связана с прокомментированным кодом, который показывает одновременное изменение двух кругов. Я отвечу в ближайшее время.
@musicamante Спасибо за отличный ответ. Мне понадобится некоторое время, чтобы переварить это. Тем временем я нашел обходной путь, который, кажется, работает.
Проблема вызвана тем, что WindowPrivate
всегда использует один и тот же объект QTimer; см. GUI_Private.h
:
QTimer user_timer{&nested_loop};
При последовательном вызове нескольких функций timer_wait(timeout, callback)
они фактически вызывают start(msec) таймера:
Запускает или перезапускает таймер с интервалом ожидания в миллисекундах. Если таймер уже запущен, он будет остановлен и перезапущен.
В результате при втором вызове timer_wait(timeout, callback)
таймер немедленно сбрасывается и перезапускается с новым тайм-аутом.
Это явно неудовлетворительно для общего использования, но это также понятно из-за простой природы и назначения библиотеки PPP: это базовая библиотека, предназначенная для образовательных целей, поэтому ее возможности несколько ограничены. Не знаю, давала ли реализация в предыдущих версиях библиотеки (до перехода на Qt) другой результат, но, возможно, стоит написать Страуструпсу и сообщить ему об этой проблеме, которую следует как минимум задокументировать, если это не исправлено каким-либо образом.
В реальной программе вы, вероятно, будете использовать два разных таймера, по одному для каждого обратного вызова.
Я считаю, что нет особого смысла пытаться исправить реализацию PPP, но для этого случая вы можете использовать существующие возможности Qt.
В частности, вы можете использовать статические функции QTimer::singleShot(), которые также можно использовать с лямбда-выражениями:
#include <QTimer>
...
QTimer::singleShot(2000, [&]{c1.set_fill_color(Color::red);});
QTimer::singleShot(4000, [&]{c2.set_fill_color(Color::blue);});
Имейте в виду важный факт: приведенное выше использование QTimer предполагает, что все, к чему осуществляется доступ внутри функтора, все еще существует, когда он наконец вызывается.
Более подходящее использование статических функций singleShot
использует контекст цели (QObject) и автоматически отключает функцию всякий раз, когда она уничтожается.
Альтернативно, реальный объект QTimer может быть создан с родительским элементом (который может быть «отправителем» или «получателем», в зависимости от ситуации), а затем соединить его timeout
со связанной функцией.
Пожалуйста, опубликуйте минимальный воспроизводимый пример с
Simple_window
определением. Второйw.timer_wait()
скорее всего заменит первый.