文本:权威指南 - 第 1 部分。
这是我系列文章的第一部分,旨在揭秘 Textual 的方方面面。在本文中,我们将探索不那么流行的基于文本的用户界面,以及如何使用 Textual 创建一个这样的用户界面。我选择写关于 Textual 的文章,部分原因是我在参加 Deepgram 黑客马拉松时用过它,但更主要的是因为除了 Readme 文件之外,互联网上还没有关于 Textual 的任何教程。
如果您以前有过文本用户界面 (TUI) 的使用经验,您可能会接触过其他框架,例如urwid、curtsies、asciimatics、prompt-toolkit等等。如果您没有,也没关系,因为您来对地方了,可以学习 TUI 的一般知识,尤其是 Textual 的使用方法。我将逐步向您展示如何开发一个 Wordle 的克隆版本。
嘘:是我,来自未来。我只是想让你知道,当你看到$
命令前面有个符号,提示你在 shell 而不是 Python 解释器中运行时。
👉 目录(TOC)。
什么是 TUI?
可以说,查找定义最受欢迎的地方是维基百科,我引用一下:
在计算领域,基于文本的用户界面 (TUI)(或者终端用户界面,以反映对计算机终端属性的依赖而不仅仅是文本)是一个复古词,描述了一种用户界面 (UI),在图形用户界面 (GUI) 出现之前,它是一种常见的早期人机交互形式。
换句话说,文本用户界面 (TUI) 是一种依靠文本而非图形来显示信息的用户界面。TUI 最常见的用途是命令行界面。TUI
的命令行界面通常不是图形化的,可能不支持鼠标(文本用户界面则不然),并且可能设计用于键盘输入。例如,Linux 的 cat 命令(顺便说一句,我喜欢猫)可以在 TUI(在本例中是你的终端)上使用以下命令调用$ cat
。
这将打开一个基于文本的界面,您可以在其中键入文本,然后按回车键显示键入的文本。
现在,对 TUI 有了大致的了解,让我们深入了解文本的世界。
什么是文本?
🔝转到目录。
Textual 是一个相对较新的基于文本的用户界面工具包。它允许程序员在终端内构建交互性强、功能复杂且用户友好的 TUI。它对许多像我一样的程序员来说很有吸引力,并且将彻底改变终端应用程序的世界。Textual 的吸引力在于以下核心特性:
- Textual 帮助您轻松优雅地构建终端应用程序。
- 文本是在终端内创建高度交互但复杂的应用程序的唯一方法。
- Textual 通过摆脱用于处理事件等的装饰器来消除早期 TUI 的可怕样板。
无论你出于什么原因想要使用 Textual,我都很高兴你偶然发现了这篇文章。我将逐步讲解 Textual 的基础知识,以及如何在终端内创建一个功能齐全的应用程序。从现在开始,我发布的每篇文章都会深入探讨 Textual 的每个元素。
我选择用 Textual 开发一个 Wordle 克隆版,主要是因为它的复杂程度恰到好处。
虽然我希望这篇文章能吸引各种各样的程序员,但我在撰写本文时,心里想的却是一群独特的读者。就像任何职位描述一样,你不必了解其中提到的每件事,但它能帮助你理解我所考虑的对象,以及你可能与我有何不同。我的目标读者是:
- 使用 Python 的初级到中级编程技能。
- 寻找一些高级的 Python 概念,我希望你渴望学习。
- 想要了解编程工作流程,而不仅仅是文本。
- 当然,还有很好的幽默感。
尽管如此,如果您有兴趣用 Textual 创建一个可运行的应用程序,那么您来对地方了!我将逐步向您展示如何开发一个 Wordle 克隆版本。您将从使用 Poetry 搭建开发环境开始,最终在终端上运行一个应用程序。多么令人兴奋啊!
安装文本
🔝转到目录。
在编程的世界里,一个相当不幸的事实是,乐趣往往在忙碌之后才会出现。然而,启动并运行 Textual 并不是一个非常复杂的过程,只需运行一个命令pip install textual
,或者查看代码库并运行poetry install
。由于我在 中使用了后者deepwordle
,所以我将为你概述一下它poetry
是什么,以及它是如何成为我构建包和管理依赖项的首选工具的。
用诗歌进行依赖管理
🔝转到目录。
本节将重点介绍一些帮助你开始使用 Poetry 的最重要的事情。
不过,值得一提的是,安装 Python 包的方法有很多:使用 pip、pypenv、pdm、poetry 等等。Poetry 不仅是“又一个”Python 依赖管理工具,还可以用于交付包。我很喜欢这个漫画:

什么是诗歌
🔝转到目录。
Poetry 是一款非常直观且优雅的命令行工具,用于安装、管理依赖项、项目和虚拟环境,是一个一体化解决方案。发明此工具的初衷是,Poetry 的创建者认为包管理的各种约定(使用 requirements.txt、MANIFEST.ini、setup.cfg 等等)不太方便。只需要一个文件,即pyproject.toml
,旨在使其清晰易读,并符合PEP 517和PEP 518标准。
无论你是否熟悉依赖管理,我都建议你尝试一下这个工具。它操作简单,使用方便,可以简化你的 Python 项目的维护和开发。
诗歌装置
🔝转到目录。
要安装此工具,您可以按照官方安装指南进行操作。
本质上,如果您是 Linux 用户,您需要做的就是在终端中运行以下命令。
$ curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python -
Python 版本控制
说到 Python 版本控制,我建议使用pyenv
。因此,我将解释如何使用pyenv
pyenv
pyenv 是一款便捷的工具,可让您轻松地在计算机中安装特定版本的 Python 解释器。与传统的访问 Python 官方网站并按照繁琐的步骤安装特定 Python 解释器的方式不同,pyenv 只需运行一个简单的命令即可轻松完成安装。
Pyenv 安装
🔝转到目录。
pyenv 仓库的Readme 文件包含大量关于安装过程以及如何使用此工具的信息。
如果您是 Linux 用户,我已经整理了在您的机器上启动并运行此工具所需的基本部分。
在 zsh 上配置 pyenv:
$ cat << EOF >> ~/.zshrc
# pyenv config
export PATH="${HOME}/.pyenv/bin:${PATH}"
export PYENV_ROOT="${HOME}/.pyenv"
eval "$(pyenv init -)"
EOF
或者,如果您使用默认的 bash shell,请运行以下命令:
$ cat << EOF >> ~/.bashrc
# pyenv config
export PATH="${HOME}/.pyenv/bin:${PATH}"
export PYENV_ROOT="${HOME}/.pyenv"
eval "$(pyenv init -)"
EOF
关闭终端并打开一个新的shell会话。现在,您可以通过运行以下命令来验证安装过程:
$ pyenv --version
使用 pyenv 进行 Python 版本控制
🔝转到目录。
您可以发出以下命令来安装特定的 Python 解释器:
$ pyenv install <python_version>
对于本教程,我将安装 python 3.10.1:
$ pyenv install 3.10.1
要列出可以使用 pyenv 安装的所有可用 Python 版本,您可以运行以下命令:
$ pyenv install --list | grep \ 3\.
要列出计算机上安装的所有 Python 版本,您可以运行:
$ pyenv versions
system
* 3.10.1 (set by PYENV_VERSION environment variable)
3.8.10
3.9.10
您可以通过运行以下命令使其在全球范围内可用:
$ pyenv global system 3.10.1
这些是使用 进行 Python 版本控制的基本命令pyenv
。现在,让我们回到poetry
。
充满诗意的虚拟环境
🔝转到目录。
在您的 PATH 中使用 python 可执行文件pyenv
,您可以将它与诗歌一起使用:
$ poetry env use 3.10.1
但是,如果您已经使用 apt 安装了 virtualenv,则最有可能遇到以下问题(如果您没有遇到此问题,那就太好了!):
Creating virtualenv deepwordle-dxc671ba-py3.10 in ~/.cache/pypoetry/virtualenvs
ModuleNotFoundError
No module named 'virtualenv.seed.via_app_data'
at <frozen importlib._bootstrap>:973 in _find_and_load_unlocked
要解决此问题,您需要通过 pip 重新安装 virtualenv:
$ sudo apt remove --purge python3-virtualenv virtualenv
$ python3 -m pip install -U virtualenv
现在,您可以告诉 poetry 使用预安装的 Python 解释器,在本例中为 3.10.1:
$ poetry env use 3.10.1
Using virtualenv: ~/.cache/pypoetry/virtualenvs/deepwordle-dxc671ba-py3.10
Poetry 有一个用于设置虚拟环境的正确路径(在 下~/.cache/pypoetry/virtualenvs/
)。但是,如果您想让 poetry.venv
在项目目录中创建一个名为 的虚拟环境,您可以运行以下命令:
$ poetry config virtualenvs.in-project true
您可以通过运行以下命令来查看不同的诗歌配置:
$ poetry config --list
开始新项目
🔝转到目录。
设置好虚拟环境后,您可以使用诗歌来创建新项目以及虚拟环境:
$ poetry new deepwordle && cd deeepwordle
这将为您提供开始工作的最低限度的准备。
deepwordle
├── pyproject.toml
├── README.rst
├── deepwordle
│ └── __init__.py
└── tests
├── __init__.py
└── test_deepwordle.py
激活虚拟环境
🔝转到目录。
如果您在项目目录中,则只需运行以下命令即可激活虚拟环境:
$ poetry shell
或者您可以获取virtualenv
路径:
$ source ~/.cache/pypoetry/virtualenvs/<your_virtual_environment_name>/bin/activate
您可以在以下位置查找以前安装的虚拟环境~/.cache/pypoetry/virtualenvs
:
$ ls ~/.cache/pypoetry/virtualenvs
要退出此虚拟环境,请使用、、exit
或您喜欢的方式来删除 shell。Ctrl-d
deactivate
将包添加到你的项目
🔝转到目录。
与使用 pip 安装包的传统方式一样,poetry 可以通过运行以下命令将包导入到项目中:
$ poetry add <name_of_the_package_you_want_to_install>
这将在该部分下添加一个条目tool.poetry.dependencies
:
[tool.poetry.dependencies]
name_of_the_package_you_want_to_install = "^package_version"
您可以随时参考官方文档以获取有关使用诗歌设置项目的更多详细信息。
我认为本节是使用 Poetry 构建新项目的良好起点。在后续章节中,我们将使用此工具来安装和管理 Textual。
安装文本:重新访问
🔝转到目录。
据我从代码库中了解,Textual 目前可用于
仅限 Linux 操作系统MacOS / Linux / Windows。要将文本添加到项目,请运行以下命令:
$ poetry add textual
正如你所见,编写依赖项和环境设置对我来说也很令人沮丧。我不知道你使用的是什么操作系统;我希望是 Linux 版本。否则,我无法预测你可能会遇到什么问题。
基本文本应用程序
🔝转到目录。
现在一切都设置好了,让我们创建最基本的 Textual 应用。为此,请app.py
使用您喜欢的代码编辑器创建一个名为 的文件,并添加以下代码:
from textual.app import App
App.run()
这是您可以编写的最基本的文本应用程序;只需导入 App 类,然后调用运行方法类。
如果您仍在其中,virtualenv
请通过运行以下命令使用 python 运行此代码:
$ python app.py
或使用poetry
,但这不是必需的,因为我们已经完成,$ poetry shell
这意味着您在包含该文件的目录中pyproject.toml
并且virtualenv
已激活,您仍然可以使用:
$ poetry run python app.py
它将在您的终端内创建一个具有黑色背景的空白窗口。
如果你想要编写的应用程序正是黑色背景的空白窗口,那么就大功告成了!恭喜你。开个玩笑,当然不是!对吧?
现在让我们使用以下代码构建一个稍微基本的文本应用程序:
from textual.app import App
class MainApp(App):
...
MainApp.run()
此版本使用继承创建了一个名为 MainApp 的新 App 子类。您将在本系列文章中开发这个应用程序。您实际上没有对这个新类进行任何操作,因此它的行为仍然与之前的基础版本相同。不过,我们将在本文及后续文章中做更多工作。
在接下来的章节中,我们将讨论 Textual 的各个组件以及如何使用它们来构建我们的 TUI 应用。但首先,我们需要设计我们的应用,也就是模型。
用户界面
🔝转到目录。
擅长设计 UI 是前端开发者或工程师必备的技能。它能让你快速了解如何在编码阶段之前布局可视化应用程序组件。让我们将这种做法应用到我们的deepwordle
应用中。以下是我想在这个项目中涵盖的一些功能:
- 将每个被猜测的单词呈现在屏幕上,并且单词之间带有空格。
- 从基本的 wordle 应用程序的角度来看,这个单词只有五个字母长。
- 用户只能有六次机会来猜出这个单词。
- 显示格式良好的消息来告诉用户正在发生的事情。
- 告诉用户该游戏可用的按键。
鉴于这些特性,很容易想象应用程序需要一组视图和小部件:
- 6x5 网格视图。
- 每个字母一个按钮。
- 消息面板。
- 页眉和页脚。
文本小部件
🔝转到目录。
在 Textual 中,Widget 是 TUI 界面的基础可视化组件。它带有一个 Canvas,可用于在终端上绘图。它接收事件并做出响应。我认为将 Widget 视为一个具有响应式属性和行为的容器比较方便,它可以包含其他容器。Widget
类是最基本的此类容器。
小部件带有十二个响应式属性,您可以操作其视觉属性,例如小部件的样式、边框、填充、大小等等。响应式属性的底层实现是Python 描述符。
每个反应属性都有一个单独的观察者,可以使用watch
关键字_
和要观察的属性的名称来定义:
foo = Reactive("")
def watch_foo(self, val):
if val == "bar":
do_something()
#
# custom logic
#
在撰写本文时,Textual 提供了十三种开箱即用的 Widget。我们将讨论如何在项目中使用这些 Widget。
占位符
🔝转到目录。
出于原型设计的目的,可以使用占位符在实施阶段之前查看应用程序的外观。例如,我们的应用程序在占位符方面如下所示。
from textual.app import App
from textual.widgets import Placeholder
class MainApp(App):
async def on_mount(self) -> None:
await self.view.dock(Placeholder(name="header"), edge="top", size=3)
await self.view.dock(Placeholder(name="footer"), edge="bottom", size=3)
await self.view.dock(Placeholder(name="stats"), edge="left", size=40)
await self.view.dock(Placeholder(name="message"), edge="right", size=40)
await self.view.dock(Placeholder(name="grid"), edge="top")
MainApp.run()
按钮
🔝转到目录。
按钮是一个标签,点击按钮时会触发相关事件。按钮具有三个属性:
- 标签:按钮上呈现的文本。
- 名称:小部件的名称。
- style:标签样式。使用“前景在背景上”符号定义。例如:style = "white on dark_blue"
from textual.app import App
from textual.widgets import Button
class MainApp(App):
async def on_mount(self) -> None:
button1 = Button(label='Hello', name='button1')
button2 = Button(label='world', name='button2', style='black on white')
await self.view.dock(button1, button2, edge="left")
MainApp.run()
标题
🔝转到目录。
此小部件定义了终端应用的标题。它可用于显示应用的标题、时间和图标等信息(目前不可自定义,但已在某个问题/PR 中提出请求)...
from textual.app import App
from textual.widgets import Header
class MainApp(App):
async def on_mount(self) -> None:
header = Header(tall=False)
await self.view.dock(header)
MainApp.run(title="DeepWordle")
页脚
🔝转到目录。
此小部件为终端应用程序定义了一个页脚,可用于向用户显示可用的键。
from textual.app import App
from textual.widgets import Footer
class MainApp(App):
async def on_load(self) -> None:
"""Bind keys here."""
await self.bind("q", "quit", "Quit")
await self.bind("t", "tweet", "Tweet")
await self.bind("r", "None", "Record")
async def on_mount(self) -> None:
footer = Footer()
await self.view.dock(footer, edge="bottom")
MainApp.run(title="DeepWordle")
滚动视图
🔝转到目录。
from textual.app import App
from textual.widgets import ScrollView, Button
class MainApp(App):
async def on_mount(self) -> None:
scroll_view = ScrollView(contents= Button(label='button'), auto_width=True)
await self.view.dock(scroll_view)
MainApp.run()
静止的
🔝转到目录。
from textual.app import App
from textual.widgets import Static, Button
class MainApp(App):
async def on_mount(self) -> None:
static = Static(renderable= Button(label='button'), name='')
await self.view.dock(static)
MainApp.run()
您可以使用其他小部件,例如TreeClick
,,,,,,,。TreeControl
TreeNode
NodeID
ButtonPressed
DirectoryTree
FileClick
自定义小部件
🔝转到目录。
通过扩展通用小部件类,您可以创建您想要的任何类型的小部件。
from textual.app import App
from textual.widget import Widget
from textual.reactive import Reactive
from rich.console import RenderableType
from rich.padding import Padding
from rich.align import Align
from rich.text import Text
class Letter(Widget):
label = Reactive("")
def render(self) -> RenderableType:
return Padding(
Align.center(Text(text=self.label), vertical="middle"),
(0, 1),
style="white on rgb(51,51,51)",
)
class MainApp(App):
async def on_mount(self) -> None:
letter = Letter()
letter.label = "A"
await self.view.dock(letter)
MainApp.run(title="DeepWordle")
它只是一个简单的小部件类,声明您的自定义字母组件,并具有自定义渲染。
有关 Widgets 的更多信息,除了本文之外,唯一可以查找的地方就是Readme 文件和代码库。
可重用组件
🔝转到目录。
在开发 Web 应用或任何类型的应用时,您通常会重用项目中的现有代码。为了使代码可重用,最佳做法是将应用的每个组件创建在单独的文件中。这样,您的代码库看起来会更加条理分明。
├── deepwordle
│ ├── __init__.py
│ ├── app.py
│ ├── components
│ │ ├── __init__.py
│ │ ├── constants.py
│ │ ├── letter.py
│ │ ├── letters_grid.py
│ │ ├── message.py
│ │ ├── rich_text.py
│ │ └── utils.py
通过视图进行组织
🔝转到目录。
Textual 中的 Widget 被组织在一个视图中,该视图使用停靠技术将它们排列在终端上。停靠类似于 CSS 网格布局,并且可以自定义。
默认情况下,小部件将渲染在终端的中心。在文本中,您可以通过将参数分别更改edge
为left
、right
、top
,将小部件停靠或排列到终端的左侧、右侧、顶部和底部。bottom
在 Textual 中,有五种类型的视图:
DockView
🔝转到目录。
它是文本应用使用的默认视图。它以垂直(默认)或水平方式对窗口小部件进行分组,以填满整个终端空间。edge
参数可用于控制窗口小部件的分组方式。
默认情况下edge = top
。
from textual.app import App
from textual.widgets import Placeholder
from textual.views import DockView
class SimpleApp(App):
async def on_mount(self) -> None:
view: DockView = await self.push_view(DockView())
await view.dock(Placeholder(), Placeholder(), Placeholder())
SimpleApp.run()
from textual.app import App
from textual.widgets import Placeholder
from textual.views import DockView
class SimpleApp(App):
async def on_mount(self) -> None:
view: DockView = await self.push_view(DockView())
await view.dock(Placeholder(), Placeholder(), Placeholder(), edge='left')
SimpleApp.run()
您还可以根据字符控制每个小部件的大小。
from textual.app import App
from textual.widgets import Placeholder
from textual.views import DockView
class SimpleApp(App):
async def on_mount(self) -> None:
view: DockView = await self.push_view(DockView())
await view.dock(Placeholder(), Placeholder(), Placeholder(), size=10)
SimpleApp.run()
如您所见,每个小部件(占位符)的高度为 10 个字符。
网格视图
🔝转到目录。
GridView 用于通过指定行数和列数以及在终端上布置的小部件列表,以矩形/表格方式布局 TUI 小部件。
空网格的示例:
from textual.app import App
from textual.widgets import Placeholder
from textual.views import GridView
class SimpleApp(App):
async def on_mount(self) -> None:
await self.view.dock(GridView(), size=10)
await self.view.dock(Placeholder(name='sad'), size=10)
await self.view.dock(GridView(), size=10)
SimpleApp.run(log="textual.log")
6x6 占位符网格的示例:
from textual.app import App
from textual import events
from textual.widgets import Placeholder
class GridView(App):
async def on_mount(self, event: events.Mount) -> None:
"""Create a grid with auto-arranging cells."""
grid = await self.view.dock_grid()
grid.add_column("col", repeat=6, size=7)
grid.add_row("row", repeat=6, size=7)
grid.set_align("stretch", "center")
placeholders = [Placeholder() for _ in range(36)]
grid.place(*placeholders)
GridView.run(title="Grid View", log="textual.log")
窗口视图
🔝转到目录。
小部件的占位符。
from textual.app import App
from textual.widgets import Placeholder
from textual.views import WindowView
class SimpleApp(App):
async def on_mount(self) -> None:
await self.view.dock(WindowView(widget=Placeholder(name='sad')), size=10)
await self.view.dock(WindowView(widget=Placeholder(name='sad')), size=10)
await self.view.dock(Placeholder(name='sad'), size=10)
SimpleApp.run(log="textual.log")
小部件事件处理程序
🔝转到目录。
在 Textual 中,你可以使用下划线命名约定为控件分配处理程序,这与传统的装饰处理程序方式不同。例如,按键事件处理程序可以简单地写成:
def on_key(self, event):
...
而不是像下面这样捕获事件的通常方式:
@on(event)
def key(self):
使用下划线约定,你的代码看起来更易读,样板代码也更少。然而,作为一名 Python 开发者,你可能会认为,编写样板代码比所谓的“最佳实践”更符合 Python 风格,我同意这种观点。例如,一开始我并不知道下划线符号背后的机制,也不知道事件是如何触发和处理的,直到我深入研究源代码,才明白一切。我其实很喜欢这种符号,它更易读。
总结
🔝转到目录。
构建您自己的基于 TUI 的应用,可以让您的 UI 技能更上一层楼。使用 Textual,您可以构建任何类型的终端应用程序。它可以节省您大量的时间,并为您的受众带来更好的体验。
Textual 包隐藏了许多低级细节,让您可以专注于应用程序的逻辑。
在本文中,您学习了如何:
- 安装并使用 Poetry。
- 安装并使用 pyenv。
- 使用 Textual 安装并构建自定义界面。
您可以自由地使用本文中的代码作为各种需求的起点。别忘了查看readme 文件,并发挥您的想象力,创建更复杂、更符合您用例的应用。
即将发布的开发博客
🔝转到目录。
我目前正计划在这个专为开发者打造的平台上分享我的经验。几周前我加入了 Dev.to,正如你们在 Medium 上的个人资料所见(我会另文解释我离开 Medium 的原因),我主要发布关于数据科学和计算机视觉方面使用 Python 的技术博客。我想这只是我定期在这里发布文章的开始,让我们一起成长吧!
那么,有什么陷阱呢?其实没有。我只是把这篇文章免费赠送给大家。这是一份礼物,你可以分享给任何人,或者以任何有利于你个人和职业发展的方式使用它。提前感谢你的大力支持!
祝大家编码愉快,下次再见。
文章来源:https://dev.to/wiseai/textual-the-definitive-guide-part-1-1i0p