3

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

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

带有垂直滚动条的列表框。

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

向下滚动时转换列表框。

如果无法实现这种行为,有什么方法可以实现这种通知?

在我的研究中,我发现了这个问题,它试图最终达到同样的效果。给定的答案似乎检查所有元素是否可见。不幸的是,如果由于终端未调整大小而随时隐藏某些元素,这将失去其功能。

4

2 回答 2

1

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

#!/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()方法返回列表框当前可见的两端(顶部、底部、两者或都不返回)。根据这一点,我修改了两个周围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取自用户 elias 的这个答案。)

这是相应的 TUI:

向下滚动时的列表框(更复杂的列表内容)

于 2018-09-21T14:18:01.817 回答
1

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

它被调用additional_urwid_widgets.IndicativeListBox并且可以通过pip安装。

列表框演示


有关说明小部件功能的独立示例,请参见此处

有关更多(和更简单)的示例,请参见此处

有关参数和选项的更详细说明,请参阅相应的 github wiki 条目



一些例子

最小

#! /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()

示例“最小”的视觉输出


显示上方/下方的项目

#! /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()

示例“与其他小部件(也有样式)的上下文中”的视觉输出

于 2018-11-18T18:43:12.753 回答