Engee 文档

活动

该页面正在翻译中。

交互式后端,如 格尔玛基WGLMakie 将事件传递给在 活动 结构。 通过对这些做出反应,可以建立自定义交互。

注意如果你是观察对象的新手,你应该首先阅读 可观察

具有优先级的可观察对象

使用Observables,可以添加具有优先级的回调。 此外,它允许每个侦听器通过返回来停止执行较低优先级的侦听器 消耗(真) 或者干脆 消费(). 每个其他返回值将被处理为 消耗(错误) 意思是监听器不会阻塞其他监听器。

了解如何 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
With low priority listener:
Low priority: 1

With medium priority listener:
Medium blocking priority: 2

With high priority listener:
High Priority: 3
Medium blocking priority: 3"

只有第一个监听器连接 低优先级:1 被打印出来。 在这种情况下,行为与正常Observables相同。 我们添加的第二个侦听器是阻塞侦听器,因为它返回 消耗(真). 由于它具有比第一个更高的优先级,因此只有第二个侦听器才会触发。 因此,我们得到 中等阻塞优先级:2. 第三个侦听器是非阻塞的,并且再次处于更高的优先级。 因此,我们从第三个和第二个听众那里得到一个结果。

事件结构

来自后端的事件存储在 活动 结构。 你可以用 活动(x) 哪里 x 是一个 , 轴心,轴心, 轴心3, LScene,LScene, [医]图场景. 无论您在这里使用哪个源,您都将始终获得相同的结构。 直接通过访问它也是如此 场景。活动. 它包含以下字段:

  • window_area::可观察{Rect2i}:以像素为单位包含窗口的当前大小。

  • window_dpi::可观察{Float64}:包含窗口的DPI。

  • window_open::可观察{Bool}:包含 真的 只要窗户是开着的。

  • hasfocus::可观察{Bool}:包含 真的 如果窗口被聚焦(在前景中)。

  • entered_window::可观察{Bool}:如果鼠标在窗口内(无论它是否聚焦),即悬停时,包含true。

  • mousebutton::可观察{MouseButtonEvent}:包含最新的 MouseButtonEvent,MouseButtonEvent 其中持有有关 按钮::鼠标。按钮行动::鼠标.开始!.

  • mousebuttonstate::设置{Mouse.Button}:包含所有当前按下的鼠标按钮。

  • mouseposition::可观察{NTuple{2, Float64}}:包含相对于根场景/窗口的像素单位的最近光标位置。

  • 滚动::可观察{NTuple{2, Float64}}:包含最近的滚动偏移量。

  • keyboardbutton::可观察{KeyEvent}:包含最新的 钥匙,钥匙 其中持有有关 键::键盘.按钮动作::键盘.开始!.

  • keyboardstate::可观察{Keyboard.Button}:包含所有当前按下的键。

  • unicode_input::可观察{Char}:包含最近键入的字符。

  • dropped_files::Observable{Vector{String}}:包含拖动到窗口中的集合文件的文件路径列表。

  • 蜱::可观察{Makie.Tick}:包含最新的 麦琪蜱虫. A 蜱虫 是为每个渲染的帧生成的,即在保存图像时或在 记录() 被使用。

鼠标交互

有三个鼠标事件可以做出反应:

  • 事件。穆斯巴顿 它持有一个 MouseButtonEvent,MouseButtonEvent 与相关 按钮开始!

  • 事件。鼠标定位 它将当前光标相对于窗口的位置保存为 N.婴儿{2, Float64} 以像素为单位

  • 事件。滚动 它持有一个 N.婴儿{2, Float64} 最后一个滚动更改

还有 事件。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

在这样的简单情况下,我们不需要使用优先级来注册我们的回调 穆斯巴顿. 优先次序及 消费() 只有当多个相互作用对同一来源做出反应并且需要以特定顺序发生或相互干扰时,才会变得重要。

为了使这个例子更好,让我们在鼠标移动时更新第二个点(行尾)。 为此,我们应该设置起点和终点 老鼠。新闻界 并更新终点时 事件(场景)。鼠标定位 只要仍然按下鼠标按钮,就会发生变化。

using GLMakie
GLMakie.activate!() # hide

points=Observable(Point2f[])

场景=场景(相机=campixel!)
行距!(场景,点,颜色=:黑色)
散开!(场景,点,颜色=:灰色)

上(事件(场景)。mousebutton)做事件
    如果事件。按钮==鼠标。左&&事件.动作==鼠标。新闻界
        mp=事件(场景)。鼠标定位[]
        推!(分[],mp,mp)
        通知(积分)
    结束
结束

上(事件(场景)。mouseposition)做mp
    mb=事件(场景)。穆斯巴顿[]
    如果mb。按钮==鼠标。左&&(mb.动作==鼠标。按//mb。动作==鼠标。重复)
        点[][结束]=mp
        通知(积分)
    结束
结束

场景

举例说明如何使用 滚动 让我们用滚轮循环浏览颜色. 滚动 包含两个浮点数,描述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

键盘交互

您可以使用 事件。键盘按钮 对一个 钥匙,钥匙事件。unicode_input 对输入的特定字符作出反应。 就像鼠标交互一样,还有一组 事件。键盘状态 按住当前按下的所有键。

让我们继续我们的例子。 目前,我们可以通过鼠标点击添加点,并通过滚动更改颜色。 我们缺少的一个特征是删除点。 让我们用键盘事件来实现这一点。 在这里我们选择了 退格键 要从末尾删除和 删除 从一开始就删除。

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 作为一个 , 轴心,轴心, [医]图场景. 该函数返回一个原始图和一个索引。 原始图是可在后端绘制的基本图:

  • 散点,散点

  • 文本

  • 线条

  • 行距,行距

  • 网格;网格

  • 网状散射

  • 表面

  • 卷数

  • 体素

  • 图像

  • 热图

每个其他的情节都是从这些地方建造的。 例如 图,ax,p=散射线(rand(10))线条散点,散点 因为这是原始的情节 p.地块.

返回的索引 挑选() 涉及到各自基元图的主要输入。

  • 散点,散点网状散射 它是一个指数到给情节的位置.

  • 文本 它是合并字符数组的索引。

  • 线条行距,行距 为所选线段的结束位置。

  • 图像, 热图表面 它是最接近所选位置的绘图矩阵参数(即给定图像,值或z值矩阵)的线性索引。

  • 体素 它是给定3d数组的线性索引。

  • 网格;网格 是选取的三角形面的最大顶点索引。

  • 卷数 它总是0。

我们以散点标记的添加、移动和删除为例来实现。 我们可以通过左键和右键来实现添加和删除,但是这会复盖现有的轴交互。 为了避免这种情况,我们实现添加为 a+左键点击 并移除as d+左键点击. 由于这些设置更具限制性,我们希望Makie检查它们中的任何一个是否首先应用,否则默认返回正常轴交互。 这意味着我们的交互应该具有比默认值更高的优先级,并有条件地阻止。

要衡量现有轴交互的优先级,我们可以检查 可观察的。听众(事件(图)。mousebutton) 创建一个 轴心,轴心. 这将显示已注册的回调及其优先级。 具有大数字(typemax(Int))的交互是我们可以忽略的最大优先级的交互。 (这保持 事件(图)。mousebuttonstate的 最新的。)这就离开了 优先级=1 作为优先击败。

为了正确放置一个新的标记,我们还需要以轴为单位获取鼠标位置。 Makie提供了一个功能,可以做到这一点: mouseposition([scene=hovered_scene()]). 相对于特定场景的像素空间鼠标定位也有一个方便的功能 mouseposition_px([scene=hovered_scene()]). 这两者通常会不同于 事件。鼠标定位 其总是以像素为单位并且总是基于全窗口。

最后,对于删除,我们需要弄清楚光标是否结束以及哪个分散标记结束。 我们可以用 挑选() 函数。 如前所述, 镐(斧) 将返回图和(对于散点)一个索引到位置数组,匹配我们的标记。 有了这个,我们现在可以设置添加和删除标记。

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
            # Delete marker
            plt, i = pick(fig)
            if plt == p
                deleteat!(positions[], i)
                notify(positions)
                return Consume(true)
            end
        elseif Keyboard.a in events(fig).keyboardstate
            # Add marker
            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
                # Delete marker
                deleteat!(positions[], i)
                notify(positions)
                return Consume(true)
            elseif Keyboard.a in events(fig).keyboardstate
                # Add marker
                push!(positions[], mouseposition(ax))
                notify(positions)
                return Consume(true)
            else
                # Initiate drag
                dragging = plt == p
                idx = i
                return Consume(dragging)
            end
        elseif event.action == Mouse.release
            # Exit drag
            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(场景,pos) 准确地挑选点。 对于小标记或细线,您可能需要选择给定范围内最近的绘图元素。 这可以用 选择(场景,位置,范围). 您还可以获取按距离排序的范围内的所有绘图和索引 pick_sorted(场景,位置,范围). 如果您想过滤某些绘图,例如 轴心,轴心.

如果您只是想知道光标是否在某个绘图或一组绘图上,则可以使用 mouseover(场景,情节。..). 这将调用 麦琪平面图(图) 将所有地块分解为原始地块并检查pick。 如果你想继续使用pick的输出,你可以使用 onpick(f,场景,情节。..;范围=1) 它执行此检查并调用 f(图、索引) 如果成功了。

[医]受压 功能

如果您正在实现基于键事件的交互,则可能希望这些键可以在不直接更改代码的情况下进行调整。 一个简单的方法是将热键保存在观察者函数之外的变量中:

hotkey = Keyboard.a
on(events(fig).keyboardbutton) do event
    if event.key == hotkey
        ...
    end
end

这样你就可以改变 热键 到任何其他键而不改变回调函数。 这样做的问题是,您仅限于一个键。 如果你想切换到像ctrl+a这样的组合,你仍然需要替换回调。 压力() 应该为你处理这件事。 您需要做的就是替换比较:

hotkey = Keyboard.a
on(events(fig).keyboardbutton) do event
    if ispressed(fig, hotkey)
        ...
    end
end

有了这个 热键 现在可以

  • A 布尔 这将直接返回。

  • 单个键或鼠标按钮。

  • A 元组, 向量资料套装 所有必须按下的键和鼠标按钮。

  • 按键和鼠标按钮的逻辑表达式 !, &|. 每个键都将被单独检查,结果将根据表达式的要求进行组合。

此外,您可以将上述任何内容包装在 独家经营 以丢弃按下附加按钮的匹配项。 所有这些选项都是独立于订单的。 以下是一些例子:

  • 热键=鼠标。左图 匹配按下鼠标左键的任何状态。

  • 热键=(键盘。left_control,键盘。a) 匹配任何状态与左控制和按下.

  • 热键=独家((键盘。left_control,键盘。a)) 匹配,如果只左控制和a被按下。

  • 热键=键盘。left_control和键盘。a 相当于 (键盘。left_control,键盘。a)

  • 热键=(键盘。left_control/键盘。right_control)和键盘。a 允许左或右控制与a.

请注意,我们使用的方式 [医]受压 以上,"按下"和"重复"事件的条件为真。 您可以通过检查进一步限制到一个或另一个 事件。开始!. 如果您希望对"发布"事件做出反应,则需要通过 事件。钥匙/事件。按钮 作为第三个论点 ispressed(图,热键,事件。键). 这会说明问题的 [医]受压 假设键或按钮被按下,如果它是热键的一部分。

交互式小部件

Makie有几个有用的交互式小部件,如滑块,按钮和菜单,您可以在 部分。

使用交互记录动画

你可以记录一个 场景 当你和它互动的时候。 只需使用 记录功能(另请参阅 动画页),并允许通过 睡觉ing在循环中。

在这个例子中,我们从场景中采样 场景 为10秒,以每秒10帧的速率。

fps = 10
record(scene, "test.mp4"; framerate = fps) do io
    for i = 1:100
        sleep(1/fps)
        recordframe!(io)
    end
end

蜱虫事件

Tick事件由GLMakie和WGLMakie中的renderloop产生,以及 麦琪储蓄麦琪记录 为所有的后端。 它们允许您将动画等任务与渲染同步。 蜱包含以下信息:

  • 状态::Makie.蜱虫,蜱虫:描述产生蜱虫的情况。 这些 include:

    • 麦琪未知数,未知数:一个包罗万象的未分类蜱。 目前仅用于tick事件的初始化。

    • 麦琪暂停/暂停:来自glmakie中暂停的renderloop的蜱虫。 (这是指最后一次绘制框架的尝试。)

    • 麦琪跳过/跳过:源自 render_on_demand=true 最后一帧被重用的GLMakie中的renderloop。 (当显示的帧没有任何更改时,会发生这种情况。 对于动画,您可以将其视为常规渲染刻度。)

    • 麦琪经常性,经常性:来自renderloop的勾号,其中最后一帧已被重绘。

    • 麦琪一时,一时:在生成图像之前生成的刻度 麦琪储蓄麦琪记录.

  • 计数::Int64:自第一次产生的蜱虫数量。 期间 记录 这是相对于第一个记录的帧。

  • 时间::Float64:从第一个刻度开始的时间,以秒为单位。 期间 记录 这是相对于第一个记录的帧和增量基于 帧速率 记录在案。

  • delta_时间::Float64:自上次打勾以来已经过去的时间,以秒为单位。 期间 记录 这是 1/帧速率.

对于动画,您通常不需要担心tick状态。 您可以根据需要简单地更新相关数据。

on(events(fig).tick) do tick
    # For a simulation you may want to use delta times for updates:
    position[] = position[] + tick.delta_time * velocity

    # For a solved system you may want to use the total time to compute the current state:
    position[] = trajectory(tick.time)

    # For a data record you may want to use count to move to the next data point(s)
    position[] = position_record[mod1(tick.count, end)]
end

对于交互式图形,这将产生与实时同步的动画。 内 记录 滴答时间与集合匹配 帧速率 使得制作的视频中的动画与实时匹配。

请注意,底层 录影带 过滤除此以外的蜱事件 状态=一个时间. 这样做是为了防止跳跃(错误的计数,时间)或加速(额外的滴答声)的动画在视频由于额外的滴答声。 (这特别是WGLMakie的一个问题,因为它在录制时仍然运行正常的renderloop。)一旦 录影带 对象被删除或视频被保存。 该行为也可以通过设置关闭 filter_ticks=错误.

供参考,一 蜱虫 通常发生在其他事件被处理之后和下一帧将被绘制之前。 Wglmakie是一个例外,它运行一个独立的计时器,以避免Javascript和Julia之间过多的消息传递。