How to remove a widget from Napari (or how to update it)

I was wondering how to remove (or actually update) a docker widget in napari. I created the widget using:


class TableWidget(QWidget):
    def __init__(self, md):
        super(QWidget, self).__init__()
        self.layout = QHBoxLayout(self)
        self.mdtable = QTableWidget()
        self.layout.addWidget(self.mdtable)

        row_count = len(md)
        col_count = 2
        self.mdtable.setColumnCount(col_count)
        self.mdtable.setRowCount(row_count)

        row = 0

        for key, value in md.items():
            newkey = QTableWidgetItem(key)
            self.mdtable.setItem(row, 0, newkey)
            newvalue = QTableWidgetItem(str(value))
            self.mdtable.setItem(row, 1, newvalue)
            row += 1

with napari.gui_qt():
    viewer = napari.Viewer()
    md = imf.create_metadata_dict()
    viewer.window.add_dock_widget(TableWidget(md), name='mdbrowser', area='right')

Later I tried viewer.window.remove_dock_widget(TableWidget) to remove the widget and replace it by a new one, but got stock

Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "C:\ProgramData\Anaconda3\envs\imageanalysis\lib\site-packages\napari\_qt\qt_main_window.py", line 510, in remove_dock_widget
    self._qt_window.removeDockWidget(widget)
TypeError: removeDockWidget(self, QDockWidget): argument 1 has unexpected type 'sip.wrappertype'

Any ideas what my mistake here is? Or maybe even better if someone could give me a hint on how to update the TableWidget.

Hey @sebi06,

First, the simplest way to update your widget is to just retain a pointer to your widget instance (before passing it to add_dock_widget):

# so rather than
viewer.window.add_dock_widget(TableWidget(md))

# use
table_widget = TableWidget(md)
dock_widget = viewer.window.add_dock_widget(table_widget)

Then, to update the widget, just use the QTableWidget API directly on your table_widget.mdtable attribute. If you give me a specific example of something you want to update, I can probably point you to a more specific method of QTableWidget… but it might look something like item = table_widget.mdtable.itemAt(...) followed by a method from QTableWidgetItem

To remove a dock_widget there is actually a slightly confusing subtlety here that I’ve been thinking needs cleanup/clarification in the API. You need to pass a dock widget instance:

# not the *class* used to instantiate the original widget
# viewer.window.remove_dock_widget(TableWidget)  # nope

# and not even the original widget instance
# viewer.window.remove_dock_widget(table_widget)  # nope

# but rather the dock_widget instance returned by add_dock_widget
viewer.window.add_dock_widget(dock_widget)  # yep
3 Likes

Hi @talley,

Thx. Your suggestion really helped (together with some other changes I made). And how I have my Napari with a file browser widget and a table widget that shows the image metadata.

For sure not the “cleanest code” but it works so far.

6 Likes

Looks gorgeous @sebi06!

awesome! looking great!

If you like an can share the code so that you can have acritical look. The whole Qt-stuff is still a bit tricky …

Code is always useful, even work-in-progress! In fact if you could submit it as a pull request to the examples/ folder at https://github.com/napari/napari, that would be fantastic!

Personally I would love to see this code, since I’ve been wanting to play a bit with a table widget at some point but never got around to it. (See this tweet.)

while we’re talking about table widgets, might be worth mentioning that QAbstractTableModel would be a good place to start to “wrap” some other python table model (such as a pandas dataframe) such that it can be used as the data for a QTableView Widget. (This is kind of like using QTableWidget but where you provide your own model of “what a table is” rather than using the internal Qt table model)…

Here’s a blog post that gives an example of wrapping a pandas dataframe with a QAbstractTableModel. I just tried it in napari and it worked well… so if you already know the pandas API, you can update your table the “usual” way. Then the table widget will update when you use dock_widget.update() (or click on the table widget manually).

example:

from qtpy import QtCore, QtWidgets
from qtpy.QtCore import Qt
import pandas as pd
import napari


class TableModel(QtCore.QAbstractTableModel):
    def __init__(self, data):
        super(TableModel, self).__init__()
        self._data = data

    def data(self, index, role):
        if role == Qt.DisplayRole:
            value = self._data.iloc[index.row(), index.column()]
            return str(value)

    def rowCount(self, index):
        return self._data.shape[0]

    def columnCount(self, index):
        return self._data.shape[1]

    def headerData(self, section, orientation, role):
        # section is the index of the column/row.
        if role == Qt.DisplayRole:
            if orientation == Qt.Horizontal:
                return str(self._data.columns[section])

            if orientation == Qt.Vertical:
                return str(self._data.index[section])


class Table(QtWidgets.QWidget):
    def __init__(self, data):
        super().__init__()
        self.setLayout(QtWidgets.QHBoxLayout())
        self.table = QtWidgets.QTableView()
        self.model = TableModel(data)
        self.table.setModel(self.model)
        self.layout().addWidget(self.table)


with napari.gui_qt():
    df = pd.DataFrame(
        [[1, 9, 2], [1, 0, -1], [3, 5, 2], [3, 3, 2]],
        columns=['A', 'B', 'C'],
        index=['Row 1', 'Row 2', 'Row 3', 'Row 4'],
    )
    wdg = Table(df)
    v = napari.Viewer()
    dw = v.window.add_dock_widget(wdg)
    # call dw.update() after you modify the dataframe

1 Like

awesome post @talley, thank you! :clap::clap::clap:

Very cool. I will try this when I find the time … I think this approach is much better than mine.

Meanwhile I uploaded my example to a GitHub repo:

Maybe you have I look before you consider this as a example … :-). I am no real programmer and my python skills are a bit “mixed” …

1 Like

Thank you @sebi06! This is a very useful template and you are underselling yourself. It’s ok, I know this impulse! :joy:

It also took me quite a moment to google the code-pieces together to turn #scikit-image regionprops result into a QTableWidget. Not a wrapper, but let me just add this code snippet to this discussion

def table_to_widget(table : dict) -> QTableWidget:
    view = QTableWidget(len(next(iter(table.values()))), len(table))
    for i, column in enumerate(table.keys()):
        view.setItem(0, i, QTableWidgetItem(column))
        for j, value in enumerate(table.get(column)):
            view.setItem(j + 1, i,  QTableWidgetItem(str(value)))
    return view

full source