Версия для печати темы

Нажмите сюда для просмотра этой темы в обычном формате

Форум на CrossPlatform.RU _ Qt GUI _ Прокрутка в QScrollArea

Автор: Анна 26.2.2019, 18:00

Суть проблемы:
Есть очень длинная форма для заполнения с QSpinBox, с QComboBox и т.п. Поскольку окно ограничено в размерах, то форма размещена на QScrollArea.
При кручении колёсика и прокрутке формы курсор мыши может оказаться на QSpinBox или QComboBox, и тогда их значения резко меняются.

Хочется, чтобы прокрутку формы можно было осуществлять только таская бегунок скроллера мышкой.
Пожалуйста, помогите реализовать. А то просто зашиваюсь по времени.
Если есть возможность "прихлопнуть" возможность изменения значений QSpinBox или QComboBox при вращении колёсика, такой вариант тоже сгодится. Может, такой вариант даже больше подойдёт.

Автор: Алексей1153 27.2.2019, 7:44

Анна, прочитай описание
void QWidget::wheelEvent(QWheelEvent *event)

у тебя есть несколько вариантов:

1) для виджета scrollarea->widget запретить обработчик wheelEvent (тогда прокрутка будет возможна только ползунком)

2) для всех чайлдов scrollarea->widget поставить фильтр installEventFilter, а в eventFilter запрещать выполнение прокрутки чайлдов QEvent::Wheel. Тогда останется возможность всё крутить колесом, но контролы не будут дёргаться

3) самый сложный и самый "красивый" вариант: частично как №2 , но разрешать обработку прокрутки в чайлде, если на нём стоит фокус. Если же прокрутка производится не в чайлде, то фокус с чайлда убирать на scrollarea. Тогда чайлды будут иметь свою крутилку только тогда, когда по нему щёлкнули и сразу стали крутить колесо. Когда курсов уехал по scrollarea->widget, чайлдовая крутилка отключается

Автор: Анна 27.2.2019, 11:16

Спасибо.
Совсем забыла про installEventFiltr(). Берегитесь, спинбоксы!!!

Вообще, для меня QSpinBox, с одной стороны самый необходимый класс, но самый ненавидимый: либо предоставляет слишком много возможностей там, где мне не надо, либо не позволяет самых простых вещей. Например, мне нужно вводить в диапазоне для unsigned intили long long, и всё, приплыли.

Автор: Алексей1153 27.2.2019, 13:20

Анна, где диапазон типа unsigned int или long long, там стрелки бесполезны, используй QLineEdit (опционально - с QRegExp валидатором)

я QSpinBox вообще не помню, чтобы пользовался. Самый бесполезный контрол, наверное )

Автор: Анна 27.2.2019, 15:31

Не скажите. Многие инженеры стрелочками спинбоксов пользуются, чтобы как на аппаратуре: сперва ввёл значение, а потом меняешь его с каким-то шагом.

Пришлось для long long написать. Это решило проблему unsigned int.

Автор: Анна 28.2.2019, 12:48

Не скажите. Многие инженеры стрелочками спинбоксов пользуются, чтобы как на аппаратуре: сперва ввёл значение, а потом меняешь его с каким-то шагом.

Пришлось для long long написать свой спинбокс. Это решило проблему unsigned int.

Автор: Анна 28.2.2019, 16:32

В общем, получился такой вариант:

bool MyWidget::eventFilter(QObject *target, QEvent *event)
{
    if(event->type() == QEvent::Wheel)
    {
        const char *className = target->metaObject()->className();
        if(QString(className) == QString("QComboBox"))
        {
             QCoreApplication::sendEvent(ui.scrollArea, event);
                         return true;
        }
        if(QString(className) == QString("QSpinBox") && !((QWidget*)target)->hasFocus() )
        {
            QCoreApplication::sendEvent(ui.scrollArea, event);
                        return true;
        }
    }
    return QWidget::eventFilter(target, event);
}


(Исправила немного, потому что в первом варианте фигня получилась. Так вроде бы правильно)

В конструкторе MyWidget у всех спинбоксов и комбобоксов вызываю installEventFilter(this), и устанавливаю StrongFocus.

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

Автор: Анна 1.3.2019, 14:43

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

Автор: Алексей1153 1.3.2019, 20:03

Анна,


Раскрывающийся текст
вместо такой конструкции

const char *className = target->metaObject()->className();
if(QString(className) == QString("QComboBox"))
{
    ...
}


удобнее использовать такую
if(auto* w=dynamic_cast<QComboBox*>(target))
{
    w->...
}


накидал вот вариант обработчика. В аттаче файлы тестового диалога, создай диалог, чтобы посмотреть работу класса обработчика:

    MyWidget dlg(0);
    dlg.exec();


 CMyScrollHandler.zip ( 2.87 килобайт ) : 187

Автор: Анна 4.3.2019, 10:57

Цитата(Алексей1153 @ 1.3.2019, 20:03) *
Анна, вместо такой конструкции

const char *className = target->metaObject()->className();
if(QString(className) == QString("QComboBox"))
{
    ...
}


удобнее использовать такую
if(auto* w=dynamic_cast<QComboBox*>(target))
{
    w->...
}


Действительно. Я собиралась к этому куску вернуться. Уж как-то не рационально получалось. За имена классов зацепилась, потому что недавно пришлось их в лог выводить.
Правда, обычно я использую qobject_cast, но думаю, большой разницы нет.

......

do {} while(0);

Забавная конструкция! Никогда с такой не сталкивалась, но возможность использовать break; удобна. Про использование в макросах тоже почитала.

Автор: Алексей1153 4.3.2019, 11:24

Анна,

Цитата
The qobject_cast() function behaves similarly to the standard C++ dynamic_cast(), with the advantages that it doesn't require RTTI support and it works across dynamic library boundaries.
qobject_cast() can also be used in conjunction with interfaces; see the Plug & Paint example for details.


то есть, если у класса есть виртуальные функции(а у всех наследников QObject они по определению есть), то стандартный dynamic_cast будет работать

я им обычно и пользуюсь

Автор: Анна 4.3.2019, 15:14

Цитата(Алексей1153 @ 1.3.2019, 20:03) *
Анна,
накидал вот вариант обработчика. В аттаче файлы тестового диалога, создай диалог, чтобы посмотреть работу класса обработчика:

    MyWidget dlg(0);
    dlg.exec();


 CMyScrollHandler.zip ( 2.87 килобайт ) : 187


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

Сделаю гибрид.
Правда, у меня есть подозрение, что в моём варианте не вё так просто с sendEvent(). Я считала, что этим вызовом передаю событие колеса, предназначенное дочернему виджету, в область прокрутки. Увы, в какой-то момент при закрытии окна иногда происходит обвал. Как я поняла - попытка удалить стековый экземпляр.

Автор: Алексей1153 4.3.2019, 15:37

Анна, у меня вроде не затыкается, либо скажи, как в моём примере повторить баг, я попробую


а sendEvent в нашей задаче не нужен

посмотри описание eventFilter :

Цитата
Filters events if this object has been installed as an event filter for the watched object.
In your reimplementation of this function, if you want to filter the event out, i.e. stop it being handled further, return true; otherwise return false.

Автор: Анна 4.3.2019, 16:12

Алексей1153,

Цитата(Алексей1153 @ 4.3.2019, 15:37) *
у меня вроде не затыкается, либо скажи, как в моём примере повторить баг, я попробую

Я полностью тело функции к себе скопировала. Правда, у меня не диалог, а QSubWindow, а в нём куча виджетов, среди которых есть виджет, содержащий область прокрутки. Вот этому виджету я создала метод eventFilter()и его (виджет) передаю в installEventFilter() всем спинбоксам и комбобоксам.

Цитата(Алексей1153 @ 4.3.2019, 15:37) *
посмотри описание eventFilter :

Если я просто верну true,как в твоём примере, то событие от колеса не обработается не только спинбоксом, но и, вообще, кем-либо ещё (мне кажется, что "затык" как раз в этом месте и происходит). А мне нужно, чтобы оно обработалось, но не спинбоксом, а областью прокрутки.

Увы, но sendEvent() с принятым в eventFilter() событием, вызывает обвал или зависание при закрытии приложения.

Автор: Алексей1153 4.3.2019, 18:37

Раскрывающийся текст
Цитата(Анна @ 4.3.2019, 12:57) *
do {} while(0);

Забавная конструкция! Никогда с такой не сталкивалась, но возможность использовать break; удобна. Про использование в макросах тоже почитала.


"break" можно использовать и из
while(1){break;}

или
for(;;){break;}

, но у
do{}while(0);

преимущество в том, что там continue не работает, да и break в конце невозможно забыть поставить, как в первых двух случаях.

Ну и все эти три случая можно заменить на лямбду, в теле которой в любом месте можно вызвать return
[&]()
{
...
}();


и макросами лучше не пользоваться, сейчас в 99.99% случаев вполне достаточно шаблонов и лямбда-функций с шаблонными параметрами ))



а по теме - щас попробую описанный тобой случай воспроизвести у себя.


вот я добавил туда вложенный виджет класса QScrollArea. Обработчик даже не пришлось трогать, благодаря рекурсивному поиску чайлдов.

Нет никаких спонтанных зацеплений колеса за спины и комбы. Но если щёлкнуть по контролу - то колесо работает. То есть, всё как нужно

Давай тогда приложи свой пример, где проблема воспроизводится, я попробую поковырять

 CMyScrollHandler2.zip ( 3.07 килобайт ) : 186
 

Автор: Анна 6.3.2019, 13:52

Цитата(Алексей1153 @ 4.3.2019, 18:37) *
вот я добавил туда вложенный виджет класса QScrollArea. Обработчик даже не пришлось трогать, благодаря рекурсивному поиску чайлдов.


Увы, свой виджет дать не могу. У меня нет доступа в интернет с компа, но он не сильно отличается от первого варианта.

Вся печаль заключается в том, что когда я полностью подставила твой класс (правда, мой компилятор не всегда понимает твой код, пришлось подправить), то возникает проблема при закрытии приложения: либо приложение остаётся висеть в памяти (даже под отладчиком),, либо возникает ошибка "CRT detected that the application wrote to memory after end of heap buffer". Ошибка возникает только, если на виджете провернуть колесо. Это какой-то бред собачий. Но я под отладчиком виду как вызывается деструктор моего виджета, и дальше всё гибнет уже в глубинах куты.

У меня vc 2010.

В общем, стабильно работает вариант, когда eventFilter() является методом самого виджета, а вместо

return true;

стоит вызов

sendEvent(scrollArea->widget(),event)



Автор: Алексей1153 6.3.2019, 14:29

Анна, Свой виджет давать не надо, просто накидай в новом проекте пример, где баги воспроизводятся. Тогда можно будет глянуть, что там


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

ну и интересно, что именно пришлось подправлять, там ты тоже могла допустить ошибки

Автор: Анна 6.3.2019, 16:32

Цитата(Алексей1153 @ 6.3.2019, 14:29) *
накидай в новом проекте пример, где баги воспроизводятся. Тогда можно будет глянуть, что там


Я скомпелила твой код. Прокручивается до тех пор, пока курсор мыши не наскочет на спинбокс или комбобокс. Если курсор мыши изначально находится на боксе (но у бокса фокуса нет), то кручение колеса не обрабатывается.

А что касается изменений, то пришлось убрать инициализацию
scrollArea =0
И цикл for(auto* child:list) заменила на обычный for(int i=0; i < list.count();i++) и в теле child на list.at(i).


Честно говоря, я не совсем понимаю, почему боксы, вообще, хватают фокус, когда крутится колесо, если у них установлен StrongFocus.

Автор: Алексей1153 6.3.2019, 17:56

Цитата(Анна @ 6.3.2019, 18:32) *
А что касается изменений, то пришлось убрать инициализацию
scrollArea =0

нельзя просто взять и убрать инициализацию, переноси в конструктор

Цитата(Анна @ 6.3.2019, 18:32) *
я не совсем понимаю, почему боксы, вообще, хватают фокус

- значит, для них не установлен фильтр. У меня всё работает, как задумано.

Поставь следующий эксперимент: для сообщения простого движения курсора (mouseMove) выведи в лог дебага какое-нибудь сообщение
qDebug()<<"..."

- порасставляй таких выводов повсюду, где нужно проследить поведение кода. Будешь шевелить мышкой и в реальном времени видеть, что куда летит

Автор: Анна 6.3.2019, 19:16

Цитата(Алексей1153 @ 6.3.2019, 17:56) *
нельзя просто взять и убрать инициализацию, переноси в конструктор

Она и так инициализируется в конструкторе. Ты же передашь туда указатель на QScrollArea.

С остальным попробую уже в понкдельник. Все Добби женского пола завтра свободны!

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

Автор: Алексей1153 6.3.2019, 19:34

Анна, щас попробую с QMdiSubWindow пример нарисовать. Но быстро не получится, поэкспериментировать придётся )

Автор: Алексей1153 6.3.2019, 21:05

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

покрутил по всякому - вроде работает. Постарался без использования C++11 оформить. Если что будет непонятно - давай вопросы )

 PRJ_MyScrollHandler.zip ( 13.92 килобайт ) : 186

Автор: Анна 11.3.2019, 15:30

Цитата(Алексей1153 @ 6.3.2019, 21:05) *
покрутил по всякому - вроде работает.

Собрала твой пример. ...У меня, по-прежнему, прокрутка останавливается, как только курсор оказывается на любом боксе. Видимо, это реализация QT 5.5.1.

Попробую разобраться, почему некорректно завершается приложение, если всё-таки отдать событие области прокрутки через sendEvent(). Почему-то происходит попытка удалить статическую переменную. Я не вижу никакого "криминала" в том, что перехватив событие для бокса я отдаю его в обработку другому виджету. Видимо, какие-то тонкости я не учла. Если разберусь, остановлюсь на этом варианте.

Кстати, не вижу смысла устанавливать фокус на scrollArea->widget(). По-моему, предыдущий вариант со сбросом фокуса более приемлем.

Автор: Алексей1153 11.3.2019, 16:01

Анна, у меня Qt 5.9.2, а на сайте уже аж 5.13

http://download.qt.io/development_releases/qt/

будет повод обновиться :)


насчёт фокуса - ну, надо куда-то его переставить, а просто убрать текущий не всегда возможно, ведь фоку может быть на чайлде другой области прокрутки

Анна, кстати, в описании сказано

Цитата
bool QCoreApplication::sendEvent(QObject *receiver, QEvent *event)
Sends event event directly to receiver receiver, using the notify() function. Returns the value that was returned from the event handler.
The event is not deleted when the event has been sent. The normal approach is to create the event on the stack, for example:


а ты не создаёшь на стеке, а пересылаешь. Может, в этом проблема

Автор: Анна 11.3.2019, 16:22

Цитата(Алексей1153 @ 11.3.2019, 15:58) *
http://download.qt.io/development_releases/qt/

будет повод обновиться


Там в single исходники лежат?

Автор: Анна 11.3.2019, 17:42

Цитата(Алексей1153 @ 11.3.2019, 16:01) *
ты не создаёшь на стеке, а пересылаешь. Может, в этом проблема

Возможно, дело в относительных координатах, которые есть в событии. Попробую создать своё событие от колеса с правильными координатами.

Автор: Алексей1153 11.3.2019, 18:18

Цитата(Анна @ 11.3.2019, 18:22) *
Там в single исходники лежат?

не понимаю вопрос.
Я вообще вместе с QtCreator обычно обновляю, там в комплекте приложен Qt

Цитата(Анна @ 11.3.2019, 19:42) *
Возможно, дело в относительных координатах

сомневаюсь, что сдвинутые координаты могли вызвать краш

Автор: Анна 12.3.2019, 10:30

Цитата(Алексей1153 @ 11.3.2019, 18:18) *
не понимаю вопрос.
Я вообще вместе с QtCreator обычно обновляю, там в комплекте приложен Qt

Ну... Сейчас QT хочет ставиться только онлайн. А у меня нет доступа в интернет с рабочей машины. Так что мне просто обновление не подходит. Надо будет ставить ручками. Кстати. 13 - это альфа версия. Официально в доступе для установки 12-я.

Цитата(Алексей1153 @ 11.3.2019, 18:18) *
сомневаюсь, что сдвинутые координаты могли вызвать краш

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

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

Автор: Анна 12.3.2019, 12:15

Затыки прокрутки при попадании мышки на бокс решились просто:

if(!control->hasFocus())
{
event->ignore();
return true;
}

Об этом написано в документации к QWheelEvent.

А вот слёты при закрытии при приложения связаны с созданием екземпляра перехватчика события. Буду разбираться дальше.

Но изначальная проблема решена!
Спасибо, Алексей1153!

Автор: Алексей1153 12.3.2019, 12:34

Анна, это ты про lanz'а, наверное, говоришь http://www.forum.crossplatform.ru/index.php?showuser=3660
- давненько не появляется почему-то. И вообще, я его сообщений как-то не нашёл, то ли он их поудалял все

насчёт краша - надо для начала ловить в отладчике, где остановилось, а там раскрутить стек обратно. Часто этого бывает достаточно для поиска проблемы

Если проблема связана со сроком жизни объекта, сделай его удаление явным (сейчас в его конструктор передаётся указатель на объект-парент, который вызывает delete своего чайлда в своём деструкторе). Например, в приватном мембере-указателе сохрани адрес фильтра-объекта, а в деструкторе вызови для него delete явно. Либо вообще попробуй вынести объект фильтра из "опекаемого" класса. Тогда экземпляр фильтра можно заставить жить дольше экземпляра "опекаемого" класса

Автор: Анна 13.3.2019, 10:54

Цитата(Алексей1153 @ 12.3.2019, 12:34) *
Анна, это ты про lanz'а, наверное, говоришь http://www.forum.crossplatform.ru/index.php?showuser=3660
- давненько не появляется почему-то. И вообще, я его сообщений как-то не нашёл, то ли он их поудалял все

Да, кажется, он.
Цитата
насчёт краша - надо для начала ловить в отладчике, где остановилось, а там раскрутить стек обратно. Часто этого бывает достаточно для поиска проблемы

Проблема решилась полной пересборкой. Похоже, я своими экспериментами довела msvc до ручки.

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

Автор: Алексей1153 13.3.2019, 12:25

Цитата(Анна @ 13.3.2019, 12:54) *
Проблема решилась полной пересборкой

кстати, да, такое тоже бывает. И не только у студии, но и у QtCreator тоже.

Если некоторое время какой-нибудь непонятный баг достаёт, рекомендую делать полный ребилд, а потом снова проверить наличие бага

Цитата(Анна @ 13.3.2019, 12:54) *
Будет время, попробую сделать так, чтобы прокрутка срабатывала только при наезде мышки на вертикальный или горизонтальный скроллер,


пригодятся обработчики (либо фильтр на соответствующие события)
void QWidget::leaveEvent
void QWidget::enterEvent

Форум Invision Power Board (http://www.invisionboard.com)
© Invision Power Services (http://www.invisionpower.com)