PhML
PhML

Reputation: 1240

How to create a ipywidgets from multiple ones?

Let's say I want a widget composed of an IntText widget and a DropDown widget which value is a concatened string of those widgets values. How can I do?

Here is an attempt:

import re
import ipywidgets as ipw

from IPython.display import display


class IntMultipliedDropdown:
    _VALUE_PATTERN = re.compile('(?P<num>\d+) (?P<option>\w+-?\w*)')

    def __init__(self, options, option_value, int_value=1):
        self.number = ipw.IntText(int_value)
        self.options = ipw.Dropdown(options=options, value=option_value)
        self.box = ipw.HBox([self.number, self.options])

        self.number.observe(self._on_changes, names='value')
        self.options.observe(self._on_changes, names='value')

        self._handelers = []

    def _on_changes(self, change):
        for handeler in self._handelers:
            handeler(self.value)

    @property
    def value(self):
        return "{} {}".format(self.number.value, self.options.value)

    @value.setter
    def value(self, value):
        match = re.search(self._VALUE_PATTERN, value)
        groupdict = match.groupdict()
        self.number.value = groupdict['num']
        self.options.value = groupdict['option']

    def _ipython_display_(self, **kwargs):
        return self.box._ipython_display_(**kwargs)

    def observe(self, handler):
        if handler not in self._handelers:
            self._handelers.append(handler)


mywidget = IntMultipliedDropdown(['apple', 'bed', 'cell'], 'cell')
mywidget.observe(print)

display(mywidget)
print('default value:', mywidget.value)

mywidget.value = '2 bed'

It works but has drawbacks. First, when I set mywidget.value the observed function is called two times: on number value change and on option value change.

Second and worst is that I cannot use this widget in a Box widget like:

ipw.HBox([ipw.Label('Mylabel'), mywidget])

Which raises:

ValueError: Can't clean for JSON: <__main__.IntMultipliedDropdown object at 0x7f7d604fff28>

Is there a better solution?

Upvotes: 0

Views: 2604

Answers (2)

rmenegaux
rmenegaux

Reputation: 176

  1. The class you created is not a widget, although you did mimick some of the behaviors (observe, display). This is probably why you couldn't get it to display in an HBox. If you want to create a new widget, inherit from ipyw.Widget or any other widget.
  2. You have two underlying widgets that are being listened to, so it is normal that two functions get called when you change their values. If you want only one function to be called, listen directly to the value of your new widget.

This is how you could do it, by inheriting from HBox:

import re
import ipywidgets as ipw
from traitlets import Unicode
from IPython.display import display


class IntMultipliedDropdown(ipw.HBox):
    _VALUE_PATTERN = re.compile('(?P<num>\d+) (?P<option>\w+-?\w*)')
    value = Unicode()

    def __init__(self, options, option_value, int_value=1, **kwargs):
        self.number = ipw.IntText(int_value)
        self.options = ipw.Dropdown(options=options, value=option_value)

        self._update_value()

        self.number.observe(self._update_value, names='value')
        self.options.observe(self._update_value, names='value')
        self.observe(self._update_children, names='value')

        super().__init__(children=[self.number, self.options], **kwargs)


    def _update_children(self, *args):
        match = re.search(self._VALUE_PATTERN, self.value)
        groupdict = match.groupdict()
        self.number.value = groupdict['num']
        self.options.value = groupdict['option']

    def _update_value(self, *args):
        self.value = "{} {}".format(self.number.value, self.options.value)

mywidget = IntMultipliedDropdown(['apple', 'bed', 'cell'], 'cell')
display(mywidget)

Upvotes: 2

Ely
Ely

Reputation: 506

There likely is a reason why you went to all the trouble of creating a new widget, but why not use the interactive function?

Something like:

import ipywidgets as ipw
from ipywidgets import *

w_number = ipw.IntText(value = 1)
w_options = ipw.Dropdown(options = ['apple', 'bed', 'cell'], value ='cell')

mywidget_value = ''

def display_value(number, options):
    mywidget_value = str(number)+' '+options
    #print(mywidget_value)
    return mywidget_value

w_box = interactive(display_value, number=w_number, options=w_options)

display(w_box)

Then you have aBox, and you can adapt its layout. You can also access the keyword arguments with w_box.kwargs or the return value of the function with w_box.result, which is the concatenated string of the 2 widgets that you were looking for...

Upvotes: 1

Related Questions