如何指示 urwid 列表框的项目多于当前显示的项目?

How to indicate that a urwid listbox has more items than displayed at the moment?

有没有办法向用户显示 urwid 列表框在显示部分的上方/下方有其他项目?

我正在考虑类似滚动条的东西,它可以让您了解条目的数量。

或列表框顶部/底部的单独栏。

如果这个行为不能实现,有什么方法可以实现这个通知?

在我的研究过程中,我发现 this question,它试图最终达到相同的目的。 给定的答案似乎检查所有元素是否可见。不幸的是,如果某些元素随时被隐藏,这将失去其功能,因为终端未调整大小。

我想我已经找到了第二个可视化概念的实现(列表框顶部和底部的栏)。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import urwid

ENTRIES = [letter for letter in "abcdefghijklmnopqrstuvwxyz"]

PALETTE = [
    ("notifier_active",   "dark cyan",  "light gray"),
    ("notifier_inactive", "black", "dark gray"),
    ("reveal_focus",      "black",      "dark cyan", "standout")
]


class MyListBox(urwid.ListBox):
    def __init__(self, body, on_focus_change=None):
        super().__init__(body)

        self.on_focus_change = on_focus_change

    # Overriden
    def change_focus(self, size, position, offset_inset=0, coming_from=None, cursor_coords=None, snap_rows=None):
        super().change_focus(size,
                             position,
                             offset_inset,
                             coming_from,
                             cursor_coords,
                             snap_rows)

        # Implement a hook to be able to deposit additional logic
        if self.on_focus_change != None:
            self.on_focus_change(size,
                                 position,
                                 offset_inset,
                                 coming_from,
                                 cursor_coords,
                                 snap_rows)


class App(object):
    def __init__(self, entries):
        # Get terminal dimensions
        terminal_cols, terminal_rows = urwid.raw_display.Screen().get_cols_rows()
        list_rows = (terminal_rows - 2) if (terminal_rows > 7) else 5       
        # (available_rows - notifier_rows) OR my preferred minimum size

        # At the beginning, "top" is always visible
        self.notifier_top = urwid.AttrMap(urwid.Text('^', align="center"),
                                          "notifier_inactive")

        # Determine presentation depending on size and number of elements
        self.notifier_bottom = urwid.AttrMap(urwid.Text('v', align="center"),
                                             "notifier_inactive" if (len(entries) <= list_rows) else "notifier_active")

        contents = [urwid.AttrMap(urwid.Button(entry), "", "reveal_focus")
                    for entry in entries]

        self.listbox = MyListBox(urwid.SimpleFocusListWalker(contents),
                                 self.update_notifiers)                   # Pass the hook

        master_pile = urwid.Pile([
            self.notifier_top,
            urwid.BoxAdapter(self.listbox, list_rows),
            self.notifier_bottom,
        ])

        widget = urwid.Filler(master_pile,
                              'top')

        self.loop = urwid.MainLoop(widget,
                                   PALETTE,
                                   unhandled_input=self.handle_input)

    # Implementation for hook
    def update_notifiers(self, size, position, offset_inset, coming_from, cursor_coords, snap_rows):
        # which ends are visible? returns "top", "bottom", both or neither.
        result = self.listbox.ends_visible(size)

        if ("top" in result) and ("bottom" in result):
            self.notifier_top.set_attr_map({None:"notifier_inactive"})
            self.notifier_bottom.set_attr_map({None:"notifier_inactive"})
        elif "top" in result:
            self.notifier_top.set_attr_map({None:"notifier_inactive"})
            self.notifier_bottom.set_attr_map({None:"notifier_active"})
        elif "bottom" in result:
            self.notifier_top.set_attr_map({None:"notifier_active"})
            self.notifier_bottom.set_attr_map({None:"notifier_inactive"})
        else:
            self.notifier_top.set_attr_map({None:"notifier_active"})
            self.notifier_bottom.set_attr_map({None:"notifier_active"})

    def handle_input(self, key):
        if key in ('q', 'Q', 'esc'):
            self.exit()

    def start(self):
        self.loop.run()

    def exit(self):
        raise urwid.ExitMainLoop()


if __name__ == '__main__':
    app = App(ENTRIES)
    app.start()

基本上,我创建了一个 urwid.Listbox 的子类并覆盖它的 change_focus() 方法来添加一个钩子。很明显,这个方法是在焦点改变的时候在内部调用的

实际逻辑使用 ends_visible() 方法的结果,returns 列表框的当前可见端(顶部、底部、两者或两者都不是)。据此,我修改了两个周围 urwid.Text 元素的呈现方式。

代码生成以下 TUI:



我还写了一个代码的变体,它是基于原始规范的:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import urwid

HEADERS = ["column 1",
           "column 2",
           "column 3",
           "column 4"]

ENTRIES = [["{}1".format(letter),
            "{}2".format(letter),
            "{}3".format(letter),
            "{}4".format(letter)] for letter in "abcdefghijklmnopqrstuvwxyz"]

PALETTE = [
    ("column_headers", "white, bold", ""),
    ("notifier_active",   "dark cyan",  "light gray"),
    ("notifier_inactive", "black", "dark gray"),
    ("reveal_focus",      "black",      "dark cyan", "standout")
]


class SelectableRow(urwid.WidgetWrap):
    def __init__(self, contents, on_select=None):
        self.contents = contents
        self.on_select = on_select

        self._columns = urwid.Columns([urwid.Text(c) for c in contents])
        self._focusable_columns = urwid.AttrMap(self._columns, '', 'reveal_focus')

        super(SelectableRow, self).__init__(self._focusable_columns)

    def selectable(self):
        return True

    def update_contents(self, contents):
        # update the list record inplace...
        self.contents[:] = contents

        # ... and update the displayed items
        for t, (w, _) in zip(contents, self._columns.contents):
            w.set_text(t)

    def keypress(self, size, key):
        if self.on_select and key in ('enter',):
            self.on_select(self)
        return key

    def __repr__(self):
        return '%s(contents=%r)' % (self.__class__.__name__, self.contents)


class MyListBox(urwid.ListBox):
    def __init__(self, body, on_focus_change=None):
        super().__init__(body)

        self.on_focus_change = on_focus_change

    # Overriden
    def change_focus(self, size, position, offset_inset=0, coming_from=None, cursor_coords=None, snap_rows=None):
        super().change_focus(size,
                             position,
                             offset_inset,
                             coming_from,
                             cursor_coords,
                             snap_rows)

        # Implement a hook to be able to deposit additional logic
        if self.on_focus_change != None:
            self.on_focus_change(size,
                                 position,
                                 offset_inset,
                                 coming_from,
                                 cursor_coords,
                                 snap_rows)


class App(object):
    def __init__(self, entries):
        # Get terminal dimensions
        terminal_cols, terminal_rows = urwid.raw_display.Screen().get_cols_rows()
        list_rows = (terminal_rows - 6) if (terminal_rows > 11) else 5       
        # (available_rows - divider_rows - column_headers_row - notifier_rows) OR my preferred minimum size

        column_headers = urwid.AttrMap(urwid.Columns([urwid.Text(c) for c in HEADERS]),
                                       "column_headers")

        # At the beginning, "top" is always visible
        self.notifier_top = urwid.AttrMap(urwid.Text('^', align="center"),
                                          "notifier_inactive")

        # Determine presentation depending on size and number of elements
        self.notifier_bottom = urwid.AttrMap(urwid.Text('v', align="center"),
                                             "notifier_inactive" if (len(entries) <= list_rows) else "notifier_active")

        contents = [SelectableRow(entry) for entry in entries]

        self.listbox = MyListBox(urwid.SimpleFocusListWalker(contents),
                                 self.update_notifiers)                    # Pass the hook

        master_pile = urwid.Pile([
            urwid.Divider(u'─'),
            column_headers,
            urwid.Divider(u'─'),
            self.notifier_top,
            urwid.BoxAdapter(self.listbox, list_rows),
            self.notifier_bottom,
            urwid.Divider(u'─'),
        ])

        widget = urwid.Filler(master_pile,
                              'top')

        self.loop = urwid.MainLoop(widget,
                                   PALETTE,
                                   unhandled_input=self.handle_input)

    # Implementation for hook
    def update_notifiers(self, size, position, offset_inset, coming_from, cursor_coords, snap_rows):
        # which ends are visible? returns "top", "bottom", both or neither.
        result = self.listbox.ends_visible(size)

        if ("top" in result) and ("bottom" in result):
            self.notifier_top.set_attr_map({None:"notifier_inactive"})
            self.notifier_bottom.set_attr_map({None:"notifier_inactive"})
        elif "top" in result:
            self.notifier_top.set_attr_map({None:"notifier_inactive"})
            self.notifier_bottom.set_attr_map({None:"notifier_active"})
        elif "bottom" in result:
            self.notifier_top.set_attr_map({None:"notifier_active"})
            self.notifier_bottom.set_attr_map({None:"notifier_inactive"})
        else:
            self.notifier_top.set_attr_map({None:"notifier_active"})
            self.notifier_bottom.set_attr_map({None:"notifier_active"})

    def handle_input(self, key):
        if key in ('q', 'Q', 'esc'):
            self.exit()

    def start(self):
        self.loop.run()

    def exit(self):
        raise urwid.ExitMainLoop()


if __name__ == '__main__':
    app = App(ENTRIES)
    app.start()

唯一真正的区别是,我使用 SelectableRow 而不是 urwid.Button 的实例。 (SelectableRow 取自 。)

对应的TUI如下:

我实现了一个默认应用第二个可视化概念(顶部和底部的栏)的列表框。

叫做additional_urwid_widgets.IndicativeListBox and can be installed via pip.


有关说明小部件功能的独立示例,请参阅here

有关更多(和更简单的)示例,请参阅here

有关参数和选项的详细说明,请参阅the corresponding github wiki entry



一些例子

最小

#! /usr/bin/env python3
# -*- coding: utf-8 -*-

from additional_urwid_widgets import IndicativeListBox    # installed via pip
import urwid                                              # installed via pip

# Color schemes that specify the appearance off focus and on focus.
PALETTE = [("reveal_focus", "black", "light cyan", "standout")]

# The list box is filled with buttons.
body = [urwid.Button(letter) for letter in "abcdefghijklmnopqrstuvwxyz"]

# Wrap the list items into an 'urwid.AttrMap', so that they have an other appearance when focused.
# Instead of an simple list-like object you can/should create a 'urwid.ListWalker'.
attr_body = [urwid.AttrMap(entry, None, "reveal_focus") for entry in body]

ilb = IndicativeListBox(attr_body)

loop = urwid.MainLoop(ilb,
                      PALETTE)
loop.run()


显示项目above/below

#! /usr/bin/env python3
# -*- coding: utf-8 -*-

from additional_urwid_widgets import IndicativeListBox    # installed via pip
import urwid                                              # installed via pip

# Color schemes that specify the appearance off focus and on focus.
PALETTE = [("reveal_focus", "black", "light cyan", "standout")]

# The list box is filled with buttons.
body = [urwid.Button(letter) for letter in "abcdefghijklmnopqrstuvwxyz"]

# Wrap the list items into an 'urwid.AttrMap', so that they have an other appearance when focused.
# Instead of an simple list-like object you can/should create a 'urwid.ListWalker'.
attr_body = [urwid.AttrMap(entry, None, "reveal_focus") for entry in body]

ilb = IndicativeListBox(attr_body,
                        topBar_endCovered_prop=("{} above ...", None, None),
                        bottomBar_endCovered_prop=("{} below ...", None, None))

loop = urwid.MainLoop(ilb,
                      PALETTE)
loop.run()


与其他小部件相关(也有样式)

在此示例中,必须额外按下 ctrl 以便列表框响应输入。
这允许小部件在垂直容器中使用(例如 urwid.Pile)。

#! /usr/bin/env python3
# -*- coding: utf-8 -*-

from additional_urwid_widgets import IndicativeListBox, MODIFIER_KEY    # installed via pip
import urwid                                                            # installed via pip

# Color schemes that specify the appearance off focus and on focus.
PALETTE = [("reveal_focus",              "black",            "light cyan",   "standout"),
           ("ilb_barActive_focus",       "dark cyan",        "light gray"),
           ("ilb_barActive_offFocus",    "light gray",       "dark gray"),
           ("ilb_barInactive_focus",     "light cyan",       "dark gray"),
           ("ilb_barInactive_offFocus",  "black",            "dark gray"),
           ("ilb_highlight_offFocus",    "black",            "dark cyan")]

# The list box is filled with buttons.
body = [urwid.Button(letter) for letter in "abcdefghijklmnopqrstuvwxyz"]

# Wrap the list items into an 'urwid.AttrMap', so that they have an other appearance when focused.
# Instead of an simple list-like object you can/should create a 'urwid.ListWalker'.
attr_body = [urwid.AttrMap(entry, None, "reveal_focus") for entry in body]

ilb = ilb = IndicativeListBox(attr_body,
                              modifier_key=MODIFIER_KEY.CTRL,
                              return_unused_navigation_input=False,
                              topBar_endCovered_prop=("ᐃ", "ilb_barActive_focus", "ilb_barActive_offFocus"),
                              topBar_endExposed_prop=("───", "ilb_barInactive_focus", "ilb_barInactive_offFocus"), 
                              bottomBar_endCovered_prop=("ᐁ", "ilb_barActive_focus", "ilb_barActive_offFocus"), 
                              bottomBar_endExposed_prop=("───", "ilb_barInactive_focus", "ilb_barInactive_offFocus"),
                              highlight_offFocus="ilb_highlight_offFocus")

pile = urwid.Pile([urwid.Text("The listbox responds only if 'ctrl' is pressed."),
                   urwid.Divider(" "),
                   urwid.Button("a button"),
                   urwid.BoxAdapter(ilb, 6),         # Wrap flow widget in box adapter
                   urwid.Button("another button")])


loop = urwid.MainLoop(urwid.Filler(pile, "top"),
                      PALETTE)
loop.run()