События
|
Страница в процессе перевода. |
Интерактивные бэкенды, такие как GLMakie и WGLMakie, передают события в наблюдаемые объекты в структуре Events. Реагируя на них, можно создавать пользовательские взаимодействия.
|
Note Если вы еще не знакомы с наблюдаемыми объектами, сначала изучите раздел Наблюдаемые объекты. |
Наблюдаемые объекты с приоритетом
С помощью наблюдаемых объектов можно также добавлять обратные вызовы с приоритетом. Это позволяет прослушивателям останавливать выполнение прослушивателей с более низким приоритетом, возвращая Consume(true) или просто Consume(). Любое другое возвращаемое значение будет обработано как Consume(false), что означает, что прослушиватель не будет блокировать другие прослушиватели.
Чтобы понять, как работает PriorityObserable, попробуйте выполнить этот пример:
using Makie
po = Observable(0)
println("With low priority listener:")
on(po, priority = -1) do x
println("Low priority: $x")
end
po[] = 1
println("\nWith medium priority listener:")
on(po, priority = 0) do x
println("Medium blocking priority: $x")
return Consume()
end
po[] = 2
println("\nWith high priority listener:")
on(po, priority = 1) do x
println("High Priority: $x")
return Consume(false)
end
po[] = 3
Если подключен только первый прослушиватель, выводится Low priority: 1. В этом случае поведение такое же, как у обычных наблюдаемых объектов. Второй добавляемый нами прослушиватель является блокирующим, так как он возвращает Consume(true). Так как у него более высокий приоритет, чем у первого, будет активирован только второй прослушиватель. В результате мы получаем Medium blocking priority: 2. Третий прослушиватель неблокирующий и имеет еще более высокий приоритет. Поэтому мы получаем результат как от третьего, так и от второго прослушивателя.
Структура Events
События от бэкенда хранятся в наблюдаемых объектах в структуре Events. К ней можно получить доступ посредством events(x), где x — это объект Figure, Axis, Axis3, LScene, FigureAxisPlot или Scene. Независимо от используемого источника всегда получается одна и та же структура. Это также верно и при доступе напрямую посредством scene.events. Структура содержит следующие поля.
-
window_area::Observable{Rect2i}: содержит текущий размер окна в пикселях. -
window_dpi::Observable{Float64}: содержит разрешение DPI окна. -
window_open::Observable{Bool}: содержитtrue, пока окно открыто. -
hasfocus::Observable{Bool}: содержитtrue, если окно имеет фокус (находится на переднем плане). -
entered_window::Observable{Bool}: содержит true, если указатель мыши находится в пределах окна (независимо от наличия фокуса), то есть наведен на окно. -
mousebutton::Observable{MouseButtonEvent}: содержит последнее событиеMouseButtonEventс соответствующимиbutton::Mouse.Buttonиaction::Mouse.Action. -
mousebuttonstate::Set{Mouse.Button}: содержит все нажатые в данный момент кнопки мыши. -
mouseposition::Observable{NTuple{2, Float64}}: содержит последнюю позицию курсора в пиксельных единицах относительно корневой сцены или окна. -
scroll::Observable{NTuple{2, Float64}}: содержит последнее смещение прокрутки. -
keyboardbutton::Observable{KeyEvent}: содержит последнее событиеKeyEventс соответствующимиkey::Keyboard.Buttonиaction::Keyboard.Action. -
keyboardstate::Observable{Keyboard.Button}: содержит все нажатые в данный момент клавиши. -
unicode_input::Observable{Char}: содержит последний введенный символ. -
dropped_files::Observable{Vector{String}}: содержит список путей к файлам коллекции, перетащенным в окно. -
tick::Observable{Makie.Tick}: содержит последний объектMakie.Tick. Объектtickсоздается для каждого отрисовываемого кадра, то есть через регулярные интервалы для интерактивных рисунков при сохранении изображения или при использованииrecord().
Взаимодействие с помощью мыши
Есть три события мыши, на которые можно реагировать:
-
events.mousebutton, которое содержитMouseButtonEventс соответствующимиbuttonиaction; -
events.mouseposition, которое содержит текущую позицию курсора относительно окна в видеNTuple{2, Float64}в пикселях; -
events.scroll, которое содержит последнюю прокрутку в видеNTuple{2, Float64}.
Есть также объект events.mousebuttonstate, который содержит все нажатые в данный момент кнопки. Это не наблюдаемый объект, поэтому реагировать на его изменения нельзя, но вы можете проверить определенную комбинацию кнопок.
В качестве примера давайте создадим сцену, в которой можно интерактивно рисовать линии между двумя точками. Первая точка выбирается при нажатии левой кнопки мыши, а вторая — при ее отпускании. Для простоты начнем со сцены в пиксельном пространстве.
using GLMakie
GLMakie.activate!() # hide
points = Observable(Point2f[])
scene = Scene(camera = campixel!)
linesegments!(scene, points, color = :black)
scatter!(scene, points, color = :gray)
on(events(scene).mousebutton) do event
if event.button == Mouse.left
if event.action == Mouse.press || event.action == Mouse.release
mp = events(scene).mouseposition[]
push!(points[], mp)
notify(points)
end
end
end
scene
В таких простых случаях не требуется использовать приоритет для регистрации обратного вызова mousebutton. Приоритет и Consume() становятся важными только тогда, когда несколько взаимодействий реагируют на один и тот же источник и должны происходить в определенном порядке или блокировать друг друга.
Чтобы сделать этот пример более наглядным, давайте будем обновлять вторую точку (конец линии) при каждом перемещении мыши. Для этого следует установить начальную и конечную точки при Mouse.press и обновлять конечную точку при изменении events(scene).mouseposition, пока кнопка нажата.
using GLMakie
GLMakie.activate!() # hide
points = Observable(Point2f[])
scene = Scene(camera = campixel!)
linesegments!(scene, points, color = :black)
scatter!(scene, points, color = :gray)
on(events(scene).mousebutton) do event
if event.button == Mouse.left && event.action == Mouse.press
mp = events(scene).mouseposition[]
push!(points[], mp, mp)
notify(points)
end
end
on(events(scene).mouseposition) do mp
mb = events(scene).mousebutton[]
if mb.button == Mouse.left && (mb.action == Mouse.press || mb.action == Mouse.repeat)
points[][end] = mp
notify(points)
end
end
scene
Чтобы продемонстрировать пример использования scroll, давайте будем циклически перебирать цвета с помощью колеса прокрутки. scroll содержит два числа с плавающей запятой, описывающих последнее изменение в направлениях x и y, обычно +1 или -1.
using GLMakie
GLMakie.activate!() # hide
colors = to_colormap(:cyclic_mrybm_35_75_c68_n256)
idx = Observable(1)
color = map(i -> colors[mod1(i, length(colors))], idx)
points = Observable(Point2f[])
scene = Scene(camera = campixel!)
linesegments!(scene, points, color = color)
scatter!(scene, points, color = :gray, strokecolor = color, strokewidth = 1)
on(events(scene).mousebutton) do event
if event.button == Mouse.left && event.action == Mouse.press
mp = events(scene).mouseposition[]
push!(points[], mp, mp)
notify(points)
end
end
on(events(scene).mouseposition) do mp
mb = events(scene).mousebutton[]
if mb.button == Mouse.left && (mb.action == Mouse.press || mb.action == Mouse.repeat)
points[][end] = mp
notify(points)
end
end
on(events(scene).scroll) do (dx, dy)
idx[] = idx[] + sign(dy)
end
scene
Взаимодействие с помощью клавиатуры
С помощью events.keyboardbutton можно реагировать на событие KeyEvent, а с помощью events.unicode_input — на ввод определенных символов. Как и в случае с взаимодействием с помощью мыши, также имеется объект events.keyboardstate, содержащий все нажатые в данный момент клавиши.
Продолжим пример. Пока мы можем добавлять точки щелчками мыши и менять цвет прокруткой. Нам не хватает возможности удалять точки. Давайте реализуем ее с помощью событий клавиатуры. Здесь мы выбираем backspace для удаления точек с конца и delete для удаления с начала.
using GLMakie
GLMakie.activate!() # hide
colors = to_colormap(:cyclic_mrybm_35_75_c68_n256)
idx = Observable(1)
color = map(i -> colors[mod1(i, length(colors))], idx)
points = Observable(Point2f[])
scene = Scene(camera = campixel!)
linesegments!(scene, points, color = color)
scatter!(scene, points, color = :gray, strokecolor = color, strokewidth = 1)
on(events(scene).mousebutton) do event
if event.button == Mouse.left && event.action == Mouse.press
mp = events(scene).mouseposition[]
push!(points[], mp, mp)
notify(points)
end
end
on(events(scene).mouseposition) do mp
mb = events(scene).mousebutton[]
if mb.button == Mouse.left && (mb.action == Mouse.press || mb.action == Mouse.repeat)
points[][end] = mp
notify(points)
end
end
on(events(scene).scroll) do (dx, dy)
idx[] = idx[] + sign(dy)
end
on(events(scene).keyboardbutton) do event
if event.action == Keyboard.press || event.action == Keyboard.repeat
length(points[]) > 1 || return nothing
if event.key == Keyboard.backspace
pop!(points[])
pop!(points[])
notify(points)
elseif event.key == Keyboard.delete
popfirst!(points[])
popfirst!(points[])
notify(points)
end
end
end
scene
Выбор точек
В Makie есть функция pick(x[, position = events(x).mouseposition[]]) для отображения графика в определенной позиции, где x может быть объектом Figure, Axis, FigureAxisPlot или Scene. Эта функция возвращает примитивный график и индекс. Примитивные графики — это базовые графики, которые можно отрисовывать в бэкендах:
-
scatter
-
text
-
lines
-
linesegments
-
mesh
-
meshscatter
-
surface
-
volume
-
voxels
-
image
-
heatmap
Все остальные графики так или иначе строятся на их основе. Например, для fig, ax, p = scatterlines(rand(10)) примитивными графиками в p.plots являются Lines и Scatter.
Возвращаемый pick() индекс связан с основным входом соответствующего примитивного графика.
-
Для
scatterиmeshscatterэто индекс позиций, заданных для графика. -
Для
textэто индекс в объединенном массиве символов. -
Для
linesиlinesegmentsэто позиция конца выбранного отрезка. -
Для
image,heatmapиsurfaceэто линейный индекс в аргументе матрицы графика (т. е. заданное изображение, значение или матрица z-значений), который находится ближе всего к выбранной позиции. -
Для
voxelsэто линейный индекс в заданном трехмерном массиве. -
Для
meshэто наибольший индекс вершины выбранной треугольной грани. -
Для
volumeэто всегда 0.
Давайте в качестве примера реализуем добавление, перемещение и удаление маркеров рассеяния. Мы могли бы реализовать добавление и удаление с помощью нажатий левой и правой кнопок мыши, однако при этом будут переопределены существующие взаимодействия с осями. Чтобы избежать этого, мы реализуем добавление посредством a + left click и удаление посредством d + left click. Поскольку это более сложные комбинации, нам нужно, чтобы в Makie сначала проверялось их нажатие и в случае отрицательного результата происходило обычное взаимодействие с осями. Это означает, что данные взаимодействия должны иметь более высокий приоритет, чем взаимодействия по умолчанию, и при выполнении условия производили блокировку.
Чтобы оценить приоритет существующего взаимодействия с осью, можно проверить Observables.listeners(events(fig).mousebutton) после создания Axis. В результате будут показаны зарегистрированные обратные вызовы с их приоритетом. Взаимодействие с наибольшим числом (typemax(Int)) имеет максимальный приоритет, и его можно игнорировать. (Таким образом поддерживается актуальное состояние events(fig).mousebuttonstate.) Остается приоритет priority = 1.
Чтобы правильно разместить новый маркер, нам также потребуется получить положение мыши в единицах оси. В Makie есть функция для этой цели: mouseposition([scene = hovered_scene()]). Существует также вспомогательная функция для определения положения мыши в пиксельном пространстве относительно определенной сцены mouseposition_px([scene = hovered_scene()]). Оба эти положения обычно отличаются от events.mouseposition, которое всегда измеряется в пикселях и всегда рассчитывается на основе всего окна.
Наконец, для удаления точки нам нужно выяснить, над каким именно маркером рассеяния находится курсор. Это можно сделать с помощью функции pick(). Как упоминалось ранее, pick(ax) возвращает график и (для графика рассеяния) индекс в массиве позиций, соответствующий маркеру. С помощью этого теперь можно настроить добавление и удаление маркеров.
using GLMakie
positions = Observable(rand(Point2f, 10))
fig, ax, p = scatter(positions)
on(events(fig).mousebutton, priority = 2) do event
if event.button == Mouse.left && event.action == Mouse.press
if Keyboard.d in events(fig).keyboardstate
# Удаляем маркер
plt, i = pick(fig)
if plt == p
deleteat!(positions[], i)
notify(positions)
return Consume(true)
end
elseif Keyboard.a in events(fig).keyboardstate
# Добавляем маркер
push!(positions[], mouseposition(ax))
notify(positions)
return Consume(true)
end
end
return Consume(false)
end
fig
Для реализации перетаскивания необходимо отслеживать некоторое состояние. При нажатии на маркер мы инициируем состояние перетаскивания. В этом состоянии маркер, на который наведен указатель мыши, должен следовать за положением указателя (в координатах осей). При отпускании кнопки мыши необходимо выйти из состояния перетаскивания. Все это также должно иметь более высокий приоритет, чем взаимодействия с осями по умолчанию, и блокировать их.
using GLMakie
positions = Observable(rand(Point2f, 10))
dragging = false
idx = 1
fig, ax, p = scatter(positions)
on(events(fig).mousebutton, priority = 2) do event
global dragging, idx
if event.button == Mouse.left
if event.action == Mouse.press
plt, i = pick(fig)
if Keyboard.d in events(fig).keyboardstate && plt == p
# Удаляем маркер
deleteat!(positions[], i)
notify(positions)
return Consume(true)
elseif Keyboard.a in events(fig).keyboardstate
# Добавляем маркер
push!(positions[], mouseposition(ax))
notify(positions)
return Consume(true)
else
# Инициируем перетаскивание
dragging = plt == p
idx = i
return Consume(dragging)
end
elseif event.action == Mouse.release
# Завершаем перетаскивание
dragging = false
return Consume(false)
end
end
return Consume(false)
end
on(events(fig).mouseposition, priority = 2) do mp
if dragging
positions[][idx] = mouseposition(ax)
notify(positions)
return Consume(true)
end
return Consume(false)
end
fig
Существует несколько различных методов и функций, связанных с pick. Базовый метод pick(scene, pos) выбирает конкретную точку. Для небольших маркеров или тонких линий может вместо этого потребоваться выбрать ближайший элемент графика в заданном диапазоне. Это можно сделать с помощью pick(scene, position, range). Можно также получить все графики и индексы в диапазоне, отсортированные по расстоянию, с помощью pick_sorted(scene, position, range). Это может быть полезно, если нужно отфильтровать определенные графики, например фон Axis.
Если просто требуется узнать, находится ли курсор на определенном графике или наборе графиков, можно использовать mouseover(scene, plots...). При этом происходит вызов Makie.flatten_plots(plots) для разбиения всех графиков на примитивные графики и проверки относительно pick. Чтобы продолжить использовать выходные данные pick, можно воспользоваться функцией onpick(f, scene, plots...; range=1), которая выполняет эту проверку и вызывает f(plot, index), если она успешна.
Функция ispressed
Если вы реализуете взаимодействия на основе событий клавиш, вам может потребоваться настраивать эти клавиши без изменения кода напрямую. Простой способ — сохранить клавишу быстрого вызова в переменной вне функции отслеживания:
hotkey = Keyboard.a
on(events(fig).keyboardbutton) do event
if event.key == hotkey
...
end
end
Это позволит изменить hotkey на любую другую клавишу, не меняя функцию обратного вызова. Проблема в том, что при этом вы ограничены одной клавишей. Чтобы переключиться на сочетание наподобие CTRL + A, все равно придется заменить обратный вызов. Функция ispressed() решает эту задачу. Вам достаточно заменить сравнение:
hotkey = Keyboard.a
on(events(fig).keyboardbutton) do event
if ispressed(fig, hotkey)
...
end
end
В этом случае hotkey может быть одним из следующих объектов:
-
значением типа
Bool, которое возвращается напрямую; -
одной клавишей или кнопкой мыши;
-
объектом
Tuple,VectorилиSet, содержащим клавиши и кнопки мыши, которые должны быть нажаты; -
логическим выражением из клавиш и кнопок мыши с операторами
!,&и|. Каждая клавиша проверятся отдельно, а результат объединяется в соответствии с выражением.
Кроме того, вы можете заключить любой из перечисленных выше объектов в Exclusively, чтобы отбросить совпадения с дополнительными нажатыми кнопками. Все эти варианты не зависят от очередности. Вот ряд примеров.
-
hotkey = Mouse.leftсоответствует любому состоянию с нажатой левой кнопкой мыши. -
hotkey = (Keyboard.left_control, Keyboard.a)соответствует любому состоянию с нажатыми левой клавишей CTRL и A. -
hotkey = ExclusivelyKeyboard.left_control, Keyboard.aсоответствует только нажатым левой клавише CTRL и A. -
hotkey = Keyboard.left_control & Keyboard.aэквивалентно(Keyboard.left_control, Keyboard.a) -
hotkey = (Keyboard.left_control | Keyboard.right_control) & Keyboard.aсоответствует нажатым левой или правой клавише CTRL и A.
Обратите внимание, что при использовании ispressed выше условие будет выполняться для событий нажатия и повтора. Выбрать только одно из этих событий можно путем проверки event.action. Для реагирования на событие отпускания необходимо передать event.key или event.button в качестве третьего аргумента в ispressed(fig, hotkey, event.key). В результате функция ispressed предполагает, что клавиша или кнопка нажата, если она является частью сочетания быстрого вызова.
Интерактивные виджеты
В Makie есть несколько полезных интерактивных виджетов, таких как ползунки, кнопки и меню, о которых можно узнать в разделе Блоки.
Запись анимаций с взаимодействиями
В процессе взаимодействия с объектом Scene его можно записать. Просто используйте функцию record (см. также страницу Анимации) и разрешите взаимодействие с помощью sleep в цикле.
В этом примере выполняется выборка из сцены scene в течение 10 секунд со скоростью 10 кадров в секунду.
fps = 10
record(scene, "test.mp4"; framerate = fps) do io
for i = 1:100
sleep(1/fps)
recordframe!(io)
end
end
События тактов
События тактов генерируются циклом отрисовки в GLMakie и WGLMakie, а также функциями Makie.save и Makie.record для всех бэкендов. Они позволяют синхронизировать такие задачи, как анимация с отрисовкой. Объект Tick содержит следующую информацию.
-
state::Makie.TickState: описывает ситуацию, в которой произошел такт. Возможные варианты-
Makie.UnknownTickState: универсальный вариант для неклассифицированных тактов. В настоящее время используется только для инициализации события такта. -
Makie.PausedRenderTick: такт в результате приостановки цикла отрисовки в GLMakie. (Связан с последней попыткой отрисовки кадра.) -
Makie.SkippedRenderTick: такт в результате цикла отрисовкиrender_on_demand = trueв GLMakie с повторным использованием последнего кадра. (Происходит, когда в отображаемом кадре ничего не изменилось. Для анимации это можно считать обычным тактом отрисовки.) -
Makie.RegularRenderTick: такт в результате цикла отрисовки с повторной отрисовкой последнего кадра. -
Makie.OneTimeRenderTick: такт, выполняемый перед генерированием изображения дляMakie.saveиMakie.record.
-
-
count::Int64: количество тактов с момента выполнения первого такта. Во время выполненияrecordэто количество с момента первого записанного кадра. -
time::Float64: время, прошедшее с первого такта, в секундах. Во время выполненияrecordэто время с момента первого записанного кадра; значение увеличивается согласноframerate, заданному в record. -
delta_time::Float64: время, прошедшее с последнего такта, в секундах. Во время выполненияrecordравно1 / framerate.
При анимации, как правило, не требуется беспокоиться о состоянии такта. Можно просто обновлять соответствующие данные требуемым образом.
on(events(fig).tick) do tick
# Для моделирования можно использовать время дельты для обновления.
position[] = position[] + tick.delta_time * velocity
# Для решенной системы может потребоваться использовать общее время для вычисления текущего состояния.
position[] = trajectory(tick.time)
# Для записи данных может потребоваться использовать счетчик, чтобы перейти к следующим точкам данных.
position[] = position_record[mod1(tick.count, end)]
end
Для интерактивного рисунка создается анимация, синхронизированная с реальным временем. В record время тактов соответствует заданному значению framerate, так что анимация в созданном видео соответствует реальному времени.
Обратите внимание, что базовый объект VideoStream отфильтровывает события тактов, отличные от state = OneTimeRenderTick. Это делается для предотвращения скачков (из-за неправильного количества или времени) или ускорения анимации в видео из-за лишних тактов. (Эта проблема особенно актуальна в бэкенде WGLMakie, поскольку во время записи в нем выполняется обычный цикл отрисовки.) После удаления объекта VideoStream или сохранения видео такты больше не отфильтровываются. Это поведение также можно отключить, задав filter_ticks = false.
Имейте в виду, что tick обычно происходит после обработки остальных событий и до отрисовки следующего кадра. Исключением является бэкенд WGLMakie, в котором есть независимый таймер для предотвращения передачи лишних сообщений между JavaScript и Julia.