ScrollBox с прокруткой, масштабом и перетаскиванием

ScrollBox с прокруткой колесом мыши

ScrollBox представляет собой контейнер для визуальных компонент. Может иметь полосы прокрутки, с помощью которых можно добраться до любого элемента контейнера. Прокрутка осуществляется только с помощью ScrollBar’ов. На колесо мыши не реагирует. Перетаскивать содержимое мышью не умеет. Популярностью не пользуется.

Мне нужно приложение, в котором смог бы протестировать различные эффекты Direct2D. Интерфейсно хочу сделать два масштабируемых поля, в левом оригинал картинки, в правом — результат работы эффектов. В центре планирую разместить панель для настройки эффектов.

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

Левая, правая и центральная панели — это ScrollBox’ы. Потому что, без дополнительных компонент, это единственный в Delphi подходящий вариант.

Прокрутка колесом мыши

Это просто

Обрабатываем событие формы OnMouseWheel. Почему не событие самого ScrollBox? Потому что до ScrollBox оно может не дойти, т.к. его могут перехватить другие компоненты, или форма решит, что ScrollBox’у это событие не надо.

Как видим, фокус в том, что мы просто либо увеличиваем, либо уменьшаем свойство Position соответствующего ScrollBar’а в зависимости от знака WheelDelta. Для определения компонента по координатам мыши пишем функцию GetMouseControl.

Функция пробегает по всем дочерним компонентам AOwner. Если компонент удовлетворяет классу AClass и присутствует на экране, проверяется вхождение точки в прямоугольник BoundsRect. Видимость проверяется функцией WinApi.IsWindowVisible. Например, если элемент находится на странице PageControl, которая в данный момент неактивна, IsWindowVisible вернет FALSE.

Почему BoundsRect. Если будем проверять вхождение по ClientRect, правый и нижний ScrollBar’ы не будут определены. В этом случае, вращая колесо мыши на полосе прокрутке, реакции не будет. Что выглядит для пользователя крайне непривычно, удивительно и ошибочно.

Это непросто

Выше приведен абсолютно рабочий код. Но есть существенный недостаток. Все компоненты, находящиеся на ScrollBox’е и которые хотели бы принимать колесо мыши, его не получат. Это означает, что TMemo, TRichEdit, TListBox, TStringGrid и прочие, реакция которых на прокрутку колесом привычна и естественна, будут глухи.

Очевидно, надо искать любой TControl по координате мыши, и анализировать его. Как правило, в конкретном проекте таких особых случаев не так много. Но, между тем, каждый такой случай уникален. Если заняться написанием класса продвинутого TScrollBox, необходимо будет добавить соответствующее событие, говорящее ScrollBox’у, реагировать ему на колесо или нет.

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

В листинге FormMouseWheel есть комментарий «1. Поиск других компонент под курсором». Допишем под ним следующее:

Предполагаем, что интересуют только TWinControl. Конечно, в реальности могут интересовать и все TControl. На колесо мыши кто только не реагирует. Но сейчас ограничимся TWinControl.

Как видно, интересуют все TWinControl, кроме TScrollBox. Потому что обработкой ScrollBox’а занимается весь последующий в листинге код.

Функция CheckVertScrollBarPossible возвращает факт того, что событие колеса должно уйти компоненту ctr. Определять этот факт будем наличием у ctr вертикального ScrollBar’а.

Функция, определяющая необходимость отправки события колеса в ctr:

Теперь, если прокрутка осуществляется над компонентом, который имеет вертикальный ScrollBar, прокрутки в ScrollBox’е не будет. Будет прокрутка внутри этого компонента.

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

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

Прокрутка TCustomMemo

Можно было бы даже TCustomEdit, но тут на самом деле без привязки к конкретному классу. Метод покрывает также и TCustomListBox, и ряд других классов.

Допишем в конец CheckVertScrollBarPossible следующее:

В примере по ссылке в конце статьи на вкладках Brightness и Sharpen расположены TMemo и TListBox соответственно. Если уменьшить размер окна по вертикали так, чтобы появились полосы прокрутки у ScrollBox’ов, можно посмотреть, как себя ведут компоненты.

Если текст слишком мал для прокрутки, событие колеса уходит в ScrollBox, без захода в этот компонент.

Прокрутка TCustomRichEdit

Но на TRichEdit этот фокус действует не всегда. Если текст большой, все работает. Но если текст мал и полосы прокрутки нет, скроллинг вниз работает нормально, а при скроллинге вверх прокрутка ScrollBox’а не работает, пока мышь над RichEdit . Чуть модифицируем функцию CheckVertScrollBarPossible:

Код ведет себя хорошо и при малом тексте. Есть небольшой глюк RichEdit’а, он описан в комментарии.

Прокрутка TCustomGrid

Компонент безусловно полезный. Но по отношению к событию колеса ведет себя гадко. Как вылечить, поговорим чуть позже. Пока продолжим модифицировать CheckVertScrollBarPossible. Полный текст:

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

Лечить «гадкое» поведение TCustomGrid

Наследники этого класса и ряд других «эгоистов» ведут себя с колесом мыши следующим образом. Если фокус на экземпляре, то где бы мышь не находилась, событие колеса будет приходить в этот экземпляр. Т.е. мышь у меня вообще на другом мониторе, а прокрутка происходит в гриде на этом экране.

В том случае, когда фокус на гриде, событие OnMouseWheel у формы происходить не будет. Как быть?

Можно повеситься на событие WM_MOUSEWHEEL. Прописать и обработать в форме:

Но за нас это уже сделали разработчики Delphi, поэтому переопределяем следующий метод:

Он вызывается как раз из обработчика события WM_MOUSEWHEEL с уже сформированным типом TCMMouseWheel.

Что происходит. Происходит вызов ранее написанного обработчика события OnMouseWheel. И если обработчик не заявил свои права на колесо мыши ( not Handled ) обработка колеса идет дальше. Иначе, никто это сообщение больше не получит. Это означает, что даже при наличии фокуса на гриде, если обработчик решил, что событие его, в грид колесо не уйдет.

Чтобы не вызывать обработчик дважды, в FormCreate обнулим событие:

Есть неприятность, что msg.ShiftState содержит элементы множества, не соответствующие действительности. Shift не нажат, а в msg.ShiftState содержится ssShift. Поэтому происходит небольшая коррекция ShiftState на основании реально нажатых клавиш:

Коррекция ShiftState

[свернуть]

Подведем итоги

При анализе компонента под мышью берем BoundsRect и приводим экранные координаты мыши в систему координат родителя:
Parent.ScreenToClient(MousePos)
Иначе ScrollBar’ы у ScrollBox’а останутся за бортом и колесо мыши на них действовать не будет.
Для скроллинга достаточно изменить свойство VertScrollBar.Position на какую то фиксированную величину. Отрицательный WheelDelta означает, что надо двигаться вниз. Положительный — вверх.
sbx.VertScrollBar.Position := sbx.VertScrollBar.Position - Sign(WheelDelta)*CSCROLL_DELTA;
Проверку на отрицательность или превышение Range делать не надо, все будет сделано автоматически.
Для «отлова» события колеса надо использовать событие OnMouseWheel формы, либо переопределять метод MouseWheelHandler. Первый способ имеет смысл использовать, когда гриды и прочие деревья не планируются. Второй способ, когда требуется отловить событие до гридов, и не пустить к ним это событие. Если этот второй способ по каким-то причинам (мало ли) не сработает, надо вешаться сразу на WM_MOUSEWHEEL.
При анализе компонентов на ScrollBox’е на предмет стоит или нет пускать в них событие колеса, надо учитывать уникальную специфику текущего проекта. Предложенный выше способ не панацея, потому что, допустим, TTrackBar вертикального ScrollBar’а не имеет, а вот колесо мыши ему позарез надо. Поэтому легче и быстрее, без «универсальностей», обработать каждый конкретный случай.

Масштабирование

Масштабировать ScrolBox’ы с элементами управления не будем. Это можно сделать, принцип точно такой же, как представленный ниже, но здесь это совершенно ни к чему.

Для отображения картинок использую TPaintBox. Под масштабированием понимаю расчет размеров экземпляров TPaintBox в зависимости от коэффициента FZoom. Превышение размеров экземпляров клиентской области несущего ScrollBox’а приведет к появлению полос прокрутки. Если расчетные размеры становятся меньше клиентской области, устанавливаю в пайнтбоксы размеры клиентской области, но внутри рисую уменьшенную копию изображения с выравниванием по центру.

Расчет размеров происходит следующим образом.

Расчет пропорционального прямоугольника изображения с учетом масштаба происходит так:

Остальные функции можно посмотреть в модуле IP76.DrawUtils.

Осталось кое что дописать в FormMouseWheel. Масштаб должен происходить от точки курсора. Полный текст обработчика:

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

В коде встретилась такая запись:

Это обработчик события OnClick на чекбоксе, отвечающего за синхронизацию ScrollBox’ов.

Синхронизация ScrollBox’ов

Идея заключается в том, что перемещая или масштабируя один ScrollBox, второй автоматически повторял за первым и масштаб и позицию. Для этого нужно всего-то выставить свойства Position соответствующих ScrollBar’ов в одинаковые значения. Масштаб у нас и так для обоих одинаков.

TrackBar отвечает за отображение и изменение текущего масштаба. Обработчик события вызывается во всех местах, где происходит смена позиции. Есть нюанс. Когда позиция ползунка в ScrollBar’е меняется мышью, то оказывается, что нет никакого события, об этом сигнализирующего.

Поэтому поступаем следующим образом. Пишем два метода:

В форме объявляем поля:

В обработчике события OnCreate формы пишем следующее:

где

Теперь при смене позиции в ScrollBar’ах будет также происходить синхронизация между ScrollBox’ами.

Перетаскивание

В любом нормальном image viewer’е есть возможность ухватиться мышкой за изображение и «потаскать» его по полю. Чем мы хуже. Обрабатываем три события на PaintBox’ах OnMouseDown, OnMouseMove, OnMouseUp:

Это обработчики и для левого, и для правого PaintBox’ов Нетрудно заметить, что обращений к конкретным экземплярам внутри не происходит. Собственно, комментировать тут нечего. Код очень небольшой.

Полезное про ScrollBox

В стандартном варианте ScrollBox при перетаскивании ползунков в ScrollBar’е ведет себя непривлекательно — перемещение происходит только при отпускании мыши. Хотелось бы видеть перемещение непосредственно при перетаскивании ползунка.

За это отвечает свойство TControlScrollBar.Tracking: Boolean. Если установлен в TRUE, перемещение содержимого ScrollBox будет происходить немедленно.

Свойства TControlScrollBar

ButtonSize
Определяет размер кнопки на полосе прокрутки.
Color
Задает цвет полосы прокрутки.
Increment
Определяет, насколько позиций перемещается отображение, когда пользователь щелкает одну из маленьких конечных стрелок на полосе прокрутки.
Примечание. Не используйте свойство Increment, если Smooth имеет значение true. Когда Smooth имеет значение true, каждый раз, когда изменяется диапазон или видимость полосы прокрутки, значение Increment динамически пересчитывается.
Kind
Указывает, является ли полоса прокрутки горизонтальной или вертикальной.
Margin
Определяет, когда создается полоса прокрутки. Определяет минимальное количество пикселей, которое должно отделять каждый элемент управления от края элемента управления, использующего полосу прокрутки. Во время выполнения, когда дочерний элемент управления находится меньше, чем Margin пикселей от края и для параметра Visible установлено значение true, появляется полоса прокрутки.
ParentColor
Используйте ParentColor, чтобы указать, что полоса прокрутки всегда должна отражать цвет выделения кнопки Windows
Position
Определяет позицию формы при прокрутки.
В случае PaintBox’а это будет либо его Top, либо Left, взятые по модулю.
Range
Определяет, насколько форма может переместиться.
В случае PaintBox’а это будет либо его высота, либо ширина.
ScrollPos
Возвращает позицию полосы прокрутки. Только для чтения.
Size
Задает размер полосы прокрутки.
Smooth
Обеспечивает плавную прокрутку с автоматической настройкой инкремента и страницы
Style
Задает стиль полосы прокрутки.
ThumbSize
Определяет размер ползунка полосы прокрутки.
Tracking
Определяет, будет ли форма перемещаться немедленно или только после отпускания кнопки мыши
Visible
Определяет, будет ли видна полоса прокрутки.
IsScrollBarVisible
Видна ли полоса прокрутки. Только для чтения.

Пару свойств ScrollBar’ов полезно устанавливать в TRUE всегда, поэтому оформлено в виде процедуры, которую имеет смысл запускать в FormCreate:

Баги

Anchors не нужен

Работая со ScrollBox’ом, как с панелью инструментов, велик соблазн установить для элементов редактирования свойство Anchors в [akLeft,akTop,akRight]. Когда появляется полоса прокрутки, элементы автоматически меняют правую границу, элемент становится уже. Когда полоса исчезает, элементы «расширяются» до нужного размера.

На рис.1. видим нормальное поведение компонент. Они «сузились» по размеру клиентской области.

Рис.1. Нормальное поведение компонентов.

Но если сейчас максимизируем окно, и если ползунок ScrollBar’а смещен, полоса прокрутки пропадет, но компоненты не растянутся по ширине клиентской области. Вернув окно в нормальное состояние, видим, что компоненты намертво уменьшились. И так будет происходить до тех пор, пока не исчезнут совсем.

Рис.2. Ненормальное поведение компонентов.

Поэтому предлагается следующий фрагмент в обработчике события формы OnResize.

Он работает и на максимизацию, и на нормализацию. Видим, что происходит насильственное убирание выравнивания по правому краю:

Моргает при изменении размеров окна

Особенно это заметно, если выбрать тему. Тема притормаживает отрисовку, поэтому промаргивание начинает раздражать. Баг может быть связан с тем, что для левой, правой и центральной панелей выбрано выравнивание Align <> alNone.

Поэтому в OnCreate формы сбросим выравнивание:

А в событии OnResize формы посчитаем размеры этих панелей:

Далее назначим эти размеры:

Моргают полосы прокрутки при изменении размеров окна

При уменьшении размеров окна начинают моргать, т.е. появляться и пропадать полосы прокрутки. Предлагается сделать так. Все в том же OnResize формы, между 1. и 3. (см. комментарии в коде выше) вставим следующий код:

После комментария 3. вставим еще один вызов:

Сама процедура очень простая:

Полностью код лучше посмотреть в исходниках по ссылке ниже.

Кракозяблы в Windows 7

Если запустим программу под Windows 7 увидим такое неприятное чудо:

Рис.3. Кракозяблы в Windows 7

На рис.3 пропали надписи на кнопках. Связано с тем, что шрифт, который использую для отображения символов Юникода, Segoe UI Symbol, в Windows 7 только появился и не настолько богат, как в Windows 10. Да и сам Юникод не был так разнообразен, как сейчас.

Лечением проблемы займемся в следующей статье. К ScrollBox’у эта тема не имеет никакого отношения.


Друзья, спасибо за внимание!

Надеюсь, материал был полезен.

В следующей статье будет рассмотрено, как TStyleHook и GDI+ могут помочь в разных «программно-жизненных» ситуациях.

Не пропустите, подписывайтесь на телегу.

Если есть вопросы, с удовольствием отвечу.

Возможно, многие ответы найдутся в исходниках. Из-за экономии места, частные случаи остались «за кадром». Приведенные в статье листинги показывает основу, суть, без дополнительных обработок ошибок. За подробностями лучше обратиться к исходникам.


Скачать

Исходники (Delphi XE 7-10) 202 Кб

Исполняемый файл 1.12 Мб


5 3 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest
4 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Alexey
Alexey
2 месяцев назад

Роман, спасибо большое за статью!
Не поверишь, но не так давно столкнулся с похожей проблемой отлова колеса мыши и на скорую руку, в лоб и по-быстрому оно не решилось ) Зато теперь, благодаря тебе, решение практически найдено, осталось лишь адаптировать его под мою конкретную задачу.
Так что ещё ра благодарю, статья реально полезна.

Олег
Олег
1 месяц назад

Роман, благодарю! Пример пригодился в конкретной ситуации, где пришлось заставить дружить скроллбокс со скроллом мыши.

4
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x
()
x