urwid | An TUI lib for python development

urwid | An TUI lib for python development

urwid

Reference:http://urwid.org/tutorial/index.html

Urwid Tutorial
 Minimal Application
 Global Input
 Display Attributes
 High Color Modes
 Question and Answer
 Signal Handlers
 Multiple Questions
 Simple Menu
 Cascading Menu
 Horizontal Menu
 Adventure Game

Main tutorial: http://urwid.org/manual/index.html

Quick Start:

import urwid

txt = urwid.Text(u"Hello World")
fill = urwid.Filler(txt, 'top') #'top', 'middle', 'bottom'
loop = urwid.MainLoop(fill)
loop.run()

123.png

Global Input

import urwid

def show_or_exit(key):
if key in ('q', 'Q'):
raise urwid.ExitMainLoop()
txt.set_text(repr(key))

txt = urwid.Text(u"Hello World")
fill = urwid.Filler(txt, 'top')
loop = urwid.MainLoop(fill, unhandled_input=show_or_exit)
loop.run()

NtMrWQ.png

Display Attributes

import urwid

def exit_on_q(key):
if key in ('q', 'Q'):
raise urwid.ExitMainLoop()

palette = [
('banner', 'black', 'light gray'),
('streak', 'black', 'dark red'),
('bg', 'black', 'dark blue'),]

txt = urwid.Text(('banner', u" Hello World "), align='center')
map1 = urwid.AttrMap(txt, 'streak')
fill = urwid.Filler(map1)
map2 = urwid.AttrMap(fill, 'bg')
loop = urwid.MainLoop(map2, palette, unhandled_input=exit_on_q)
loop.run()

NtMszj.png
原图123.png
去掉‘bg’
NtMMdK.png
去掉‘streak’

解释:

txt, 最顶层,标签是‘banner’,获取palette中‘banner’的设置
map1, 和txt 一个道理。
txt 丢到map1, map1丢到 fill, fill丢到map2, 一层盖一层。

Question and Answer

import urwid

def exit_on_q(key):
if key in ('q', 'Q'):
raise urwid.ExitMainLoop()

class QuestionBox(urwid.Filler):
def keypress(self, size, key):
if key != 'enter':
return super(QuestionBox, self).keypress(size, key)
self.original_widget = urwid.Text(
u"Nice to meet you,\n%s.\n\nPress Q to exit." %
edit.edit_text)

edit = urwid.Edit(u"What is your name?\n")
fill = QuestionBox(edit)
loop = urwid.MainLoop(fill, unhandled_input=exit_on_q)
loop.run()

NtMnqx.png

Signal Handlers

import urwid

palette = [('I say', 'default,bold', 'default', 'bold'),]
ask = urwid.Edit(('I say', u"What is your name?\n"))
reply = urwid.Text(u"")
button = urwid.Button(u'Exit')
div = urwid.Divider()
pile = urwid.Pile([ask, div, reply, div, button])
top = urwid.Filler(pile, valign='top')

def on_ask_change(edit, new_edit_text):
reply.set_text(('I say', u"Nice to meet you, %s" % new_edit_text))

def on_exit_clicked(button):
raise urwid.ExitMainLoop()

urwid.connect_signal(ask, 'change', on_ask_change)
urwid.connect_signal(button, 'click', on_exit_clicked)

urwid.MainLoop(top, palette).run()

NtMQIO.gif

Simple Menu

import urwid

choices = u'Chapman Cleese Gilliam Idle Jones Palin'.split()

def menu(title, choices):
body = [urwid.Text(title), urwid.Divider()]
for c in choices:
button = urwid.Button(c)
urwid.connect_signal(button, 'click', item_chosen, c)
body.append(urwid.AttrMap(button, None, focus_map='reversed'))
return urwid.ListBox(urwid.SimpleFocusListWalker(body))

def item_chosen(button, choice):
response = urwid.Text([u'You chose ', choice, u'\n'])
done = urwid.Button(u'Ok')
urwid.connect_signal(done, 'click', exit_program)
main.original_widget = urwid.Filler(urwid.Pile([response,
urwid.AttrMap(done, None, focus_map='reversed')]))

def exit_program(button):
raise urwid.ExitMainLoop()

main = urwid.Padding(menu(u'Pythons', choices), left=2, right=2)
top = urwid.Overlay(main, urwid.SolidFill(u'\N{MEDIUM SHADE}'),
align='center', width=('relative', 60),
valign='middle', height=('relative', 60),
min_width=20, min_height=9)
urwid.MainLoop(top, palette=[('reversed', 'standout', '')]).run()

NtMeMR.png

Pop-up Menu

import urwid

def menu_button(caption, callback):
button = urwid.Button(caption)
urwid.connect_signal(button, 'click', callback)
return urwid.AttrMap(button, None, focus_map='reversed')

def sub_menu(caption, choices):
contents = menu(caption, choices)
def open_menu(button):
return top.open_box(contents)
return menu_button([caption, u'...'], open_menu)

def menu(title, choices):
body = [urwid.Text(title), urwid.Divider()]
body.extend(choices)
return urwid.ListBox(urwid.SimpleFocusListWalker(body))

def item_chosen(button):
response = urwid.Text([u'You chose ', button.label, u'\n'])
done = menu_button(u'Ok', exit_program)
top.open_box(urwid.Filler(urwid.Pile([response, done])))

def exit_program(button):
raise urwid.ExitMainLoop()

menu_top = menu(u'Main Menu', [
sub_menu(u'Applications', [
sub_menu(u'Accessories', [
menu_button(u'Text Editor', item_chosen),
menu_button(u'Terminal', item_chosen),
]),
]),
sub_menu(u'System', [
sub_menu(u'Preferences', [
menu_button(u'Appearance', item_chosen),
]),
menu_button(u'Lock Screen', item_chosen),
]),
])

class CascadingBoxes(urwid.WidgetPlaceholder):
max_box_levels = 4
#
def __init__(self, box):
super(CascadingBoxes, self).__init__(urwid.SolidFill(u'/'))
self.box_level = 0
self.open_box(box)
#
def open_box(self, box):
self.original_widget = urwid.Overlay(urwid.LineBox(box),
self.original_widget,
align='center', width=('relative', 80),
valign='middle', height=('relative', 80),
min_width=24, min_height=8,
left=self.box_level * 3,
right=(self.max_box_levels - self.box_level - 1) * 3,
top=self.box_level * 2,
bottom=(self.max_box_levels - self.box_level - 1) * 2)
self.box_level += 1
#
def keypress(self, size, key):
if key == 'esc' and self.box_level > 1:
self.original_widget = self.original_widget[0]
self.box_level -= 1
else:
return super(CascadingBoxes, self).keypress(size, key)

top = CascadingBoxes(menu_top)
urwid.MainLoop(top, palette=[('reversed', 'standout', '')]).run()

NtMGzd.png

Horizontal Menu

import urwid

class MenuButton(urwid.Button):
def __init__(self, caption, callback):
super(MenuButton, self).__init__("")
urwid.connect_signal(self, 'click', callback)
self._w = urwid.AttrMap(urwid.SelectableIcon(
[u' \N{BULLET} ', caption], 2), None, 'selected')

class SubMenu(urwid.WidgetWrap):
def __init__(self, caption, choices):
super(SubMenu, self).__init__(MenuButton(
[caption, u"\N{HORIZONTAL ELLIPSIS}"], self.open_menu))
line = urwid.Divider(u'\N{LOWER ONE QUARTER BLOCK}')
listbox = urwid.ListBox(urwid.SimpleFocusListWalker([
urwid.AttrMap(urwid.Text([u"\n ", caption]), 'heading'),
urwid.AttrMap(line, 'line'),
urwid.Divider()] + choices + [urwid.Divider()]))
self.menu = urwid.AttrMap(listbox, 'options')
#
def open_menu(self, button):
top.open_box(self.menu)

class Choice(urwid.WidgetWrap):
def __init__(self, caption):
super(Choice, self).__init__(
MenuButton(caption, self.item_chosen))
self.caption = caption
#
def item_chosen(self, button):
response = urwid.Text([u' You chose ', self.caption, u'\n'])
done = MenuButton(u'Ok', exit_program)
response_box = urwid.Filler(urwid.Pile([response, done]))
top.open_box(urwid.AttrMap(response_box, 'options'))

def exit_program(key):
raise urwid.ExitMainLoop()

menu_top = SubMenu(u'Main Menu', [
SubMenu(u'Applications', [
SubMenu(u'Accessories', [
Choice(u'Text Editor'),
Choice(u'Terminal'),
]),
]),
SubMenu(u'System', [
SubMenu(u'Preferences', [
Choice(u'Appearance'),
]),
Choice(u'Lock Screen'),
]),
])

palette = [
(None, 'light gray', 'black'),
('heading', 'black', 'light gray'),
('line', 'black', 'light gray'),
('options', 'dark gray', 'black'),
('focus heading', 'white', 'dark red'),
('focus line', 'black', 'dark red'),
('focus options', 'black', 'light gray'),
('selected', 'white', 'dark blue')]
focus_map = {
'heading': 'focus heading',
'options': 'focus options',
'line': 'focus line'}

class HorizontalBoxes(urwid.Columns):
def __init__(self):
super(HorizontalBoxes, self).__init__([], dividechars=1)
#
def open_box(self, box):
if self.contents:
del self.contents[self.focus_position + 1:]
self.contents.append((urwid.AttrMap(box, 'options', focus_map),
self.options('given', 24)))
self.focus_position = len(self.contents) - 1

top = HorizontalBoxes()
top.open_box(menu_top.menu)
urwid.MainLoop(urwid.Filler(top, 'middle', 10), palette).run()

NtMgLq.png

Frames

urwid.Text

123.png
不能:
{Column, AttrMap} --> Mainloop

可:
{Filler, Frame } -> Mainloop

Column -> Filler  -> Mainloop
Text -> Filler  -> Mainloop
Column -> Filler -> AttrMap -> Mainloop
Text -> Filler -> AttrMap -> Mainloop

Frame

Frame(header=hdr, body=map2)
Text -> AttrWrap > header
Columns -> Filler -> AttrMap -> body

ListBox

Filler -> ListBox
Padding -> SimpleListWalker -> ListBox
[Text, Pile, Columns] -> SimpleListWalker -> ListBox
[Text, Pile, Columns]-> SimpleListWalker -> ListBox -> AttrWrap -> Frame

Exampels

Source: https://github.com/urwid/urwid/tree/master/examples

NtM6Qs.png
browse.py

NtMcyn.png
bigtext.py

NtMRe0.png
treesample.py

NtMKZ6.png
calc.py

NtM3Je.png
tour.py

NtM1iD.png
edit.py

NtM8RH.png
fib.py

NtMYQA.png
graph.py

NtMDJg.png
terminal.py

NtM5YF.png
input_test.py

NtMIW4.png
pop_up.py

NtMbO1.md.png
palette_test.py

more examples: programcreek.com

pop_up.py

P-raw

##!/usr/bin/env python

import urwid

class PopUpDialog(urwid.WidgetWrap):
"""A dialog that appears with nothing but a close button """
signals = ['close']
def __init__(self):
close_button = urwid.Button("that's pretty cool")
urwid.connect_signal(close_button, 'click',
lambda button:self._emit("close"))
pile = urwid.Pile([urwid.Text(
"^^ I'm attached to the widget that opened me. "
"Try resizing the window!\n"), close_button])
fill = urwid.Filler(pile)
self.__super.__init__(urwid.AttrWrap(fill, 'popbg'))


class ThingWithAPopUp(urwid.PopUpLauncher):
def __init__(self):
self.__super.__init__(urwid.Button("click-me"))
urwid.connect_signal(self.original_widget, 'click',
lambda button: self.open_pop_up())

def create_pop_up(self):
pop_up = PopUpDialog()
urwid.connect_signal(pop_up, 'close',
lambda button: self.close_pop_up())
return pop_up

def get_pop_up_parameters(self):
return {'left':0, 'top':1, 'overlay_width':32, 'overlay_height':7}


fill = urwid.Filler(urwid.Padding(ThingWithAPopUp(), 'center', 15))
loop = urwid.MainLoop(
fill,
[('popbg', 'white', 'dark blue')],
pop_ups=True)
loop.run()

原来程序:
NtMTSJ.jpg
在 pop up 的框里面加一个退出按钮
结构解析:

close_button = urwid.Button("that's pretty cool")      #定义一个button 名称
urwid.connect_signal(close_button, 'click', # button 的作用
lambda button:self._emit("close"))
pile = urwid.Pile([urwid.Text( #一段文字,加上前面的button
"^^ I'm attached to the widget that opened me. " #pile 在一起
"Try resizing the window!\n"), close_button])
fill = urwid.Filler(pile) #封装
self.__super.__init__(urwid.AttrWrap(fill, 'popbg')) #完成

可以看出基本结构为, Button -> Pile + Button  ->  Filler
因此, 除了加入button title和 signal 外, 还需要在 pile处, 一起打包.
测试:

esc_button = urwid.Button("Esc")
urwid.connect_signal(esc_button, 'click',
lambda button:self._emit("close"))
pile = urwid.Pile([urwid.Text(
"^^ I'm attached to the widget that opened me. "
"Try resizing the window!\n"), close_button,esc_button])

直接加的话, 按键会依次排列:

NtM7l9.png

Button 位置等属性

用 urwid.Padding(close_button, ‘left’, 18) 参数 调整button 位置, 结果一个上左, 一个右下
注: 18 为字符数

NtMtsI.png

这里用到一个新函数 ,参考来源:stackoverflow

urwid.Columns

基本用法:  urwid.Columns([button1, button2])

NtMNLt.png

def on_exit_clicked(button):
raise urwid.ExitMainLoop()

## button, 加在上文 p-raw 的 9~11行之间
button = urwid.Button(u'Exit')
urwid.connect_signal(button, 'click', on_exit_clicked)

## button的排序, 加载urwid.Pile() 里面
urwid.Columns([
urwid.Padding(close_button, 'left'),
urwid.Padding(button, 'right', 8),
]),


完整代码:
pop-2

##!/usr/bin/env python

import urwid

def on_exit_clicked(button):
raise urwid.ExitMainLoop()

class PopUpDialog(urwid.WidgetWrap):
"""A dialog that appears with nothing but a close button """
signals = ['close']

def __init__(self):
close_button = urwid.Button("that's NOT COOL")
urwid.connect_signal(close_button, 'click',
lambda button:self._emit("close"))

button = urwid.Button(u'Exit')
urwid.connect_signal(button, 'click', on_exit_clicked)

pile = urwid.Pile([urwid.Text(
"^^ I'm attached to the widget that opened me. "
"Try resizing the window!\n"),
urwid.Columns([
urwid.Padding(close_button, 'left'),
urwid.Padding(button, 'right', 8),
]),
])
fill = urwid.Filler(pile)
self.__super.__init__(urwid.AttrWrap(fill, 'popbg'))


class ThingWithAPopUp(urwid.PopUpLauncher):
def __init__(self):
self.__super.__init__(urwid.Button("click-me"))
urwid.connect_signal(self.original_widget, 'click',
lambda button: self.open_pop_up())

def create_pop_up(self):
pop_up = PopUpDialog()
urwid.connect_signal(pop_up, 'close',
lambda button: self.close_pop_up())
return pop_up

def get_pop_up_parameters(self):
return {'left':0, 'top':1, 'overlay_width':32, 'overlay_height':7}


button = urwid.Button(u'Exit')
urwid.connect_signal(button, 'click', on_exit_clicked)

pile = urwid.Pile([urwid.Padding(ThingWithAPopUp(), 'center', 15),button])
fill = urwid.Filler(pile)
loop = urwid.MainLoop(
fill,
[('popbg', 'white', 'dark blue')],
pop_ups=True)
loop.run()

pop window size

修改这里就好了~

def get_pop_up_parameters(self):
return {'left':0, 'top':1, 'overlay_width':32, 'overlay_height':7}

NtMaeP.jpg

魔改

左右各一个list:
将pile 放入column中

NtMBFS.png

把pop-2 的 51,52行改成

pile = urwid.Pile(body_A)
col = urwid.Columns([pile,urwid.Text("│"),('fixed',14,pile)])
fill = urwid.Filler(col)

bigtext.py

非常靓的一个NtMddf.jpg

排版区

# Create chars_avail
cah = urwid.Text("Characters Available:")
self.chars_avail = urwid.Text("", wrap='any')
ca = urwid.AttrWrap(self.chars_avail, 'chars')

chosen_font_rb.set_state(True) # causes set_font_event call

# Create Edit widget
edit = self.create_edit("", "Urwid "+urwid.__version__,
self.edit_change_event)

# ListBox
chars = urwid.Pile([cah, ca])
fonts = urwid.Pile([urwid.Text("Fonts:")] + self.font_buttons,
focus_item=1)
col = urwid.Columns([fonts,('fixed',16,chars)], 3,
focus_column=1)
bt = urwid.Pile([bt, edit], focus_item=1)
l = [bt, urwid.Divider(), col]
w = urwid.ListBox(urwid.SimpleListWalker(l))

# Frame
w = urwid.AttrWrap(w, 'body')
hdr = urwid.Text("Urwid BigText example program - F8 exits.")
hdr = urwid.AttrWrap(hdr, 'header')
w = urwid.Frame(header=hdr, body=w)


Frame 排版结构124.png
继续魔改. 首先发现一个bug, 虽然说按F8 退出, 但是按完以后, 只出来一个 pop window, 写着quit, 然后, 就没有然后了. 所以我们先来加一个Esc 按钮吧~ (以后再说)


Refresh

Eample

import urwid
import time
import sys

'''
https://github.com/bavanduong/urwid-example/blob/master/clock.py
'''
class Clock:

def keypress(self, key):
if key in ('q', 'Q'):
raise urwid.ExitMainLoop()

def setup_view(self):
self.clock_txt = urwid.BigText(
time.strftime('%H:%M:%S'), urwid.font.HalfBlock5x4Font())
self.view = urwid.Padding(self.clock_txt, 'center', width='clip')
self.view = urwid.AttrMap(self.view, 'body')
self.view = urwid.Filler(self.view, 'middle')

def main(self):
self.setup_view()
loop = urwid.MainLoop(
self.view, palette=[('body', 'dark cyan', '')],
unhandled_input=self.keypress)
loop.set_alarm_in(1, self.refresh)
loop.run()

def refresh(self, loop=None, data=None):
self.setup_view()
loop.widget = self.view
loop.set_alarm_in(1, self.refresh)


if __name__ == '__main__':
clock = Clock()
sys.exit(clock.main())

NtMWwV.png

實戰

Todolist

NtM4FU.gif

Bilibili 信息板

NtMfoT.png

urwid | An TUI lib for python development

https://karobben.github.io/2020/06/23/Python/urwid/

Author

Karobben

Posted on

2020-06-23

Updated on

2024-01-11

Licensed under

Comments