窗口小部件事件#

特殊事件#

Button 不是用来表示数据类型的。相反,按钮部件用于处理鼠标点击。Buttonon_click 方法可用于注册一个函数,当按钮被点击时调用该函数。on_click 的文档字符串如下所示。

import ipywidgets as widgets
print(widgets.Button.on_click.__doc__)
Register a callback to execute when the button is clicked.

        The callback will be called with one argument, the clicked button
        widget instance.

        Parameters
        ----------
        remove: bool (optional)
            Set to true to remove the callback from the list of callbacks.
        

示例#

由于按钮点击是无状态的,它们通过自定义消息从前端传输到后端。使用on_click方法,下面展示了一个点击时打印消息的按钮。要捕获prints(或任何其他类型的输出)并确保其显示,请务必将其发送到Output小部件(或将您想要显示的信息放入HTML小部件中)。

from IPython.display import display
button = widgets.Button(description="Click Me!")
output = widgets.Output()

display(button, output)

def on_button_clicked(b):
    with output:
        print("Button clicked.")

button.on_click(on_button_clicked)

Traitlet 事件#

部件属性是IPython的traitlets,而traitlets是具有事件功能的。要处理变更,可以使用部件的observe方法来注册回调函数。observe的文档字符串如下所示。

print(widgets.Widget.observe.__doc__)
Setup a handler to be called when a trait changes.

        This is used to setup dynamic notifications of trait changes.

        Parameters
        ----------
        handler : callable
            A callable that is called when a trait changes. Its
            signature should be ``handler(change)``, where ``change`` is a
            dictionary. The change dictionary at least holds a 'type' key.
            * ``type``: the type of notification.
            Other keys may be passed depending on the value of 'type'. In the
            case where type is 'change', we also have the following keys:
            * ``owner`` : the HasTraits instance
            * ``old`` : the old value of the modified trait attribute
            * ``new`` : the new value of the modified trait attribute
            * ``name`` : the name of the modified trait attribute.
        names : list, str, All
            If names is All, the handler will apply to all traits.  If a list
            of str, handler will apply to all names in the list.  If a
            str, the handler will apply just to that name.
        type : str, All (default: 'change')
            The type of notification to filter by. If equal to All, then all
            notifications are passed to the observe handler.
        

签名#

在文档字符串中提到,注册的回调必须具有签名 handler(change),其中 change 是一个包含变更信息的字典。

使用此方法,下方展示了一个示例,演示了如何在IntSlider的值发生变化时输出其值。

int_range = widgets.IntSlider()
output2 = widgets.Output()

display(int_range, output2)

def on_value_change(change):
    with output2:
        print(change['new'])

int_range.observe(on_value_change, names='value')

关联小部件#

通常,您可能希望简单地将小部件属性链接在一起。相比于使用裸traitlets事件,属性同步可以通过更简单的方式完成。

链接内核中的traitlets属性#

第一种方法是使用linkdlink函数,这两个函数来自traitlets模块(为了方便,这两个函数由ipywidgets模块重新导出)。这仅在我们与活动内核交互时有效。

caption = widgets.Label(value='The values of slider1 and slider2 are synchronized')
sliders1, slider2 = widgets.IntSlider(description='Slider 1'),\
                    widgets.IntSlider(description='Slider 2')
l = widgets.link((sliders1, 'value'), (slider2, 'value'))
display(caption, sliders1, slider2)
caption = widgets.Label(value='Changes in source values are reflected in target1')
source, target1 = widgets.IntSlider(description='Source'),\
                  widgets.IntSlider(description='Target 1')
dl = widgets.dlink((source, 'value'), (target1, 'value'))
display(caption, source, target1)

函数 traitlets.linktraitlets.dlink 返回一个 LinkDLink 对象。通过调用 unlink 方法可以断开链接。

l.unlink()
dl.unlink()

在内核中注册对特性变化的回调#

由于 Python 端的部件属性是 traitlets,您可以在模型从前端获取更新时注册处理程序以处理变更事件。

传递给observe的处理程序将使用一个change参数调用。该change对象至少包含一个type键和一个name键,分别对应于通知的类型和触发通知的属性名称。

依据 type 的值,可能会传递其他键。当类型为 change 时,我们还具有以下键:

  • owner : 该HasTraits实例

  • old : 被修改的特征属性的旧值

  • new : 被修改的特性属性的新值

  • name : 被修改的特征属性的名称。

caption = widgets.Label(value='The slider value is in its initial position.')
slider = widgets.IntSlider(min=-5, max=5, value=1, description='Slider')

def handle_slider_change(change):
    caption.value = 'The slider value is ' + (
        'negative' if change.new < 0 else 'nonnegative'
    )

slider.observe(handle_slider_change, names='value')

display(caption, slider)

从客户端链接小部件属性#

同步traitlets属性时,由于往返服务器端的延迟,您可能会遇到滞后。您还可以使用链接部件在浏览器中直接连接部件属性,可以是单向或双向方式。

在没有内核的情况下将小部件嵌入HTML网页时,JavaScript链接会持续存在。

caption = widgets.Label(value='The values of range1 and range2 are synchronized')
range1, range2 = widgets.IntSlider(description='Range 1'),\
                 widgets.IntSlider(description='Range 2')
l = widgets.jslink((range1, 'value'), (range2, 'value'))
display(caption, range1, range2)
caption = widgets.Label(value='Changes in source_range values are reflected in target_range1')
source_range, target_range1 = widgets.IntSlider(description='Source range'),\
                              widgets.IntSlider(description='Target range 1')
dl = widgets.jsdlink((source_range, 'value'), (target_range1, 'value'))
display(caption, source_range, target_range1)

函数 widgets.jslink 返回一个 Link 组件。通过调用 unlink 方法可以断开链接。

# l.unlink()
# dl.unlink()

内核中链接与客户端中链接的区别#

内核中的链接意味着通过python进行链接。如果两个滑块在内核中链接,当一个滑块被更改时,浏览器会向内核(在此情况下是python)发送消息更新已更改的滑块,内核中的链接组件随后将更改传播到内核中的另一个滑块对象,然后另一个滑块的内核对象会向浏览器发送消息以更新浏览器中另一个滑块的视图。如果内核未运行(如在静态网页中),则控件将不会被链接。

使用jslink进行链接(即在浏览器端)意味着在Javascript中构建链接。当一个滑块被更改时,在浏览器中运行的Javascript会更改另一个滑块的值,完全不需要与内核通信。如果滑块附加到内核对象,每个滑块将独立更新其内核端对象。

要查看两者之间的区别,请转到ipywidgets文档中此页面的静态版本并尝试底部的滑块。在内核中使用linkdlink链接的滑块不再链接,但在浏览器中使用jslinkjsdlink链接的滑块仍然保持链接。

持续更新#

部分小部件通过其continuous_update属性提供了持续更新值或仅当用户提交值(例如,按Enter键或离开控件)时更新值的选择。在下一个示例中,我们看到“Delayed”(延迟)控件仅在用户完成拖动滑块或提交文本框后传输其值。而“Continuous”(连续)控件则在值更改时持续传输值。尝试在每个文本框中输入一个两位数,或拖动每个滑块,以查看区别。

a = widgets.IntSlider(description="Delayed", continuous_update=False)
b = widgets.IntText(description="Delayed", continuous_update=False)
c = widgets.IntSlider(description="Continuous", continuous_update=True)
d = widgets.IntText(description="Continuous", continuous_update=True)

widgets.link((a, 'value'), (b, 'value'))
widgets.link((a, 'value'), (c, 'value'))
widgets.link((a, 'value'), (d, 'value'))
widgets.VBox([a,b,c,d])

滑块、TextTextarea控件默认设置为continuous_update=TrueIntText及其他用于输入整数或浮点数的文本框默认设置为continuous_update=False(因为通常您会希望在按下回车键或移出框外提交值之前输入完整的数字)。

防抖动#

当特性变更触发的回调执行繁重计算时,您可能希望不像值更新那样频繁进行计算。例如,如果该特性由滑块驱动,并且其 continuous_update 设置为 True,用户将快速连续触发大量计算。

防抖功能通过延迟回调执行来解决此问题,直到该值在一定时间内未发生变化,随后使用最新值调用回调。效果是仅当特性暂停更改一定时间后才会调用回调。

防抖可以通过异步循环或线程实现。我们下面展示一个异步解决方案,这更适合于ipywidgets。如果你想使用线程来实现防抖,请将Timer类替换为from threading import Timer

import asyncio

class Timer:
    def __init__(self, timeout, callback):
        self._timeout = timeout
        self._callback = callback

    async def _job(self):
        await asyncio.sleep(self._timeout)
        self._callback()

    def start(self):
        self._task = asyncio.ensure_future(self._job())

    def cancel(self):
        self._task.cancel()

def debounce(wait):
    """ Decorator that will postpone a function's
        execution until after `wait` seconds
        have elapsed since the last time it was invoked. """
    def decorator(fn):
        timer = None
        def debounced(*args, **kwargs):
            nonlocal timer
            def call_it():
                fn(*args, **kwargs)
            if timer is not None:
                timer.cancel()
            timer = Timer(wait, call_it)
            timer.start()
        return debounced
    return decorator

这是如何使用debounce函数作为装饰器。尝试更改滑块的值。文本框仅在滑块暂停约0.2秒后才会更新。

slider = widgets.IntSlider()
text = widgets.IntText()

@debounce(0.2)
def value_changed(change):
    text.value = change.new
slider.observe(value_changed, 'value')

widgets.VBox([slider, text])

节流#

节流是另一种可用于限制回调的技术。与防抖(如果在自上次(尝试)调用函数后未经过一定时间就忽略对函数的调用)不同,节流只会限制调用速率。这确保了函数被定期调用。

我们下面展示一个同步解决方案。同样地,如果你想使用线程而非异步编程,可以将 Timer 类替换为 from threading import Timer

import asyncio
from time import time

def throttle(wait):
    """ Decorator that prevents a function from being called
        more than once every wait period. """
    def decorator(fn):
        time_of_last_call = 0
        scheduled, timer = False, None
        new_args, new_kwargs = None, None
        def throttled(*args, **kwargs):
            nonlocal new_args, new_kwargs, time_of_last_call, scheduled, timer
            def call_it():
                nonlocal new_args, new_kwargs, time_of_last_call, scheduled, timer
                time_of_last_call = time()
                fn(*new_args, **new_kwargs)
                scheduled = False
            time_since_last_call = time() - time_of_last_call
            new_args, new_kwargs = args, kwargs
            if not scheduled:
                scheduled = True
                new_wait = max(0, wait - time_since_last_call)
                timer = Timer(new_wait, call_it)
                timer.start()
        return throttled
    return decorator

为了观察它与防抖器相比的行为差异,这里展示了相同的滑块示例,并在文本框中显示其节流值。注意它虽然仍限制了回调频率,但交互性明显增强。

slider = widgets.IntSlider()
text = widgets.IntText()

@throttle(0.2)
def value_changed(change):
    text.value = change.new
slider.observe(value_changed, 'value')

widgets.VBox([slider, text])