应用基础 ¶
本章我们将讲解如何使用 Textual 的 App 类创建应用程序,内容简明扼要,助你快速入门。后续章节会进行更深入的探讨。
"App 类"
开发 Textual 应用的第一步是导入 App 类并创建其子类。以下是最基础的应用类示例:
"运行方法"
运行应用程序时,我们需要先创建实例,然后调用 run()方法。
from textual.app import App
class MyApp(App):
pass
if __name__ == "__main__":
app = MyApp()
app.run()
这个应用简单至极——别指望它能实现太多功能。
小贴士
仅当通过命令运行该文件时,条件才成立。这样既能在导入时不立即启动应用,又便于开发工具(devtools)以开发模式运行应用。详情可查阅 Python 官方文档。 __name__ == "__main__": python app
运行此应用时,你会看到一个空白终端界面,如下图所示: python simple02.py
当你调用 App.run()方法时,Textual 会将终端切换至一种特殊的应用程序模式。在此模式下,终端将停止回显用户输入内容。Textual 会全面接管键盘和鼠标等用户输入响应,并动态更新终端显示区域(即屏幕内容)。
同时按下 Ctrl 和 Q 组合键,Textual 将退出应用模式并返回命令行界面,且终端中原有的内容会被完整恢复。
内联运行 ¶
新增于 0.55.0 版本
您还可以以内联模式运行应用,这样应用会显示在提示信息下方(且不会进入全屏应用模式)。这类内联模式特别适合那些需要与终端常规操作流程深度结合的工具使用。
若要以内联模式运行应用,调用 App.run()时需设置相应参数。具体样式设置方法可参考《内联应用样式指南》。 inline True
笔记
当前 Windows 系统暂不支持内联模式。
ANSI 颜色标准 ¶
新增于 0.80.0 版本
终端支持 16 种可自定义主题的 ANSI 颜色,您可在终端设置中按需调整。默认情况下,Textual 会使用预设配色方案覆盖这些颜色(具体说明详见常见问题解答)。
您可以在 App 的构造函数中进行设置以禁用此功能。
我们建议采用全屏应用的默认设置,不过对于内联应用,您可能需要保留 ANSI 色彩。
"事件"
Textual 提供了一套事件系统,可用于响应按键操作、鼠标动作以及内部状态变化。事件处理器是以对应事件名称为前缀的方法。 on_
"其中一类事件是挂载事件,当应用进入应用模式后会触发该事件。您可以通过定义一个名为. on_mount 的方法来响应此事件。"
另一个类似事件是按键事件,它会在用户按下按键时触发。下面的示例代码同时包含了这两个事件的处理程序:
from textual.app import App
from textual import events
class EventApp(App):
COLORS = [
"white",
"maroon",
"red",
"purple",
"fuchsia",
"olive",
"yellow",
"navy",
"teal",
"aqua",
]
def on_mount(self) -> None:
self.screen.styles.background = "darkblue"
def on_key(self, event: events.Key) -> None:
if event.key.isdecimal():
self.screen.styles.background = self.COLORS[int(event.key)]
if __name__ == "__main__":
app = EventApp()
app.run()
该处理程序设置的属性会将背景变为蓝色(如你所料)。由于挂载事件会在进入应用模式后立即触发,因此运行这段代码时,你会看到屏幕变成蓝色。 on_mount self.screen.styles.background "darkblue"
按下按键时,键事件处理程序()会接收到一个 Key 实例。若处理程序中无需使用该事件,可将其省略。 on_key
事件中可能包含额外信息,您可以在事件处理函数中进行查看。以键盘按键事件(Key event)为例,该事件包含一个属性用于标识被按下的键名。上述方法正是通过判断该键名属性,当按下 key 、 on_key 、 0 至 9 中任意一个键时,便会触发背景色的改变。
异步事件
Textual 基于 Python 的 asyncio 框架开发,该框架使用了 and 关键字。 async await
Textual 会自动等待以协程形式编写的事件处理程序(即使用关键字修饰的函数)。普通函数通常可以正常工作,但若需集成其他异步库(例如用 httpx 从网络获取数据),则需使用协程。 async
小贴士
想了解 Python 异步编程的入门指南,可阅读 FastAPI 的《并发汉堡》一文。
小组件 ¶
小部件是独立的组件,负责生成屏幕上某一部分的显示内容。它们响应事件的方式与应用程序基本相同。大多数功能丰富的应用至少会包含一个(通常会有多个)小部件,这些部件共同构成了用户界面。
小部件可以简单如一段文字、一个按钮,也可以复杂如文本编辑器或文件浏览器(这些组件本身可能还包含其他小部件)。
"撰写"
要为应用添加小组件,需实现一个 compose() 方法,该方法应返回可迭代的实例集合。虽然使用列表也能实现,但通过生成器方式逐个生成小组件更为便捷。 Widget
以下示例演示了如何导入内置组件并通过 . Welcome App.compose() 返回该组件。
from textual.app import App, ComposeResult
from textual.widgets import Welcome
class WelcomeApp(App):
def compose(self) -> ComposeResult:
yield Welcome()
def on_button_pressed(self) -> None:
self.exit()
if __name__ == "__main__":
app = WelcomeApp()
app.run()
运行此代码时,Textual 会加载一个包含 Markdown 内容和按钮的组件: Welcome
请注意处理控件内按钮触发的 Button.Pressed 事件的方法。该事件处理程序通过调用 App.exit()来退出应用。 on_button_pressed Welcome
安装
尽管在应用启动时采用组合方式是添加小部件的首选方法,但有时仍需根据事件动态添加新部件。此时可调用 mount()函数,该操作会在用户界面中挂载一个新部件。
"这款应用能在用户按下任意键时,自动添加一个欢迎组件:"
from textual.app import App
from textual.widgets import Welcome
class WelcomeApp(App):
def on_key(self) -> None:
self.mount(Welcome())
def on_button_pressed(self) -> None:
self.exit()
if __name__ == "__main__":
app = WelcomeApp()
app.run()
首次运行时,屏幕将显示为空白。此时按下任意键即可添加欢迎控件。若重复按键,还可添加多个控件。
"等待挂载中 ¶"
当你挂载一个部件时,Textual 会自动挂载该部件包含的所有子组件。Textual 确保挂载操作会在下一个消息处理器触发前完成,但不会在调用后立即生效。若你需要在同一消息处理器中对该部件进行修改,可能会因此遇到问题。
首先我们通过示例来说明这个问题。当用户按下按键时,以下代码会加载 Welcome 组件。同时,代码还会尝试修改该组件中的按钮——将其标签文字从“OK”更改为“YES!”。
from textual.app import App
from textual.widgets import Button, Welcome
class WelcomeApp(App):
def on_key(self) -> None:
self.mount(Welcome())
self.query_one(Button).label = "YES!"
if __name__ == "__main__":
app = WelcomeApp()
app.run()
运行此示例时,你会发现在按下按键后 Textual 会抛出 NoMatches 异常。这是由于我们尝试修改按钮时,挂载流程尚未执行完毕所导致的。
"为此,我们可以选择性地等待某个操作的结果,这要求我们将函数设为异步。这样就能确保执行到下一行代码时,按钮已完成挂载,此时便可修改其标签文本。 mount() async "
from textual.app import App
from textual.widgets import Button, Welcome
class WelcomeApp(App):
async def on_key(self) -> None:
await self.mount(Welcome())
self.query_one(Button).label = "YES!"
if __name__ == "__main__":
app = WelcomeApp()
app.run()
"以下是输出内容,请注意按钮文字已变更:"
"退出 ¶"
应用会持续运行,直至调用 App.exit()方法退出应用模式,此时 run 方法将返回。若该调用是代码的最后一行,程序将返回到命令行界面。
exit 方法还可接收一个可选的位置参数作为返回值。下面的示例通过该参数返回被点击按钮的(标识符)。 run() id
from textual.app import App, ComposeResult
from textual.widgets import Label, Button
class QuestionApp(App[str]):
def compose(self) -> ComposeResult:
yield Label("Do you love Textual?")
yield Button("Yes", id="yes", variant="primary")
yield Button("No", id="no", variant="error")
def on_button_pressed(self, event: Button.Pressed) -> None:
self.exit(event.button.id)
if __name__ == "__main__":
app = QuestionApp()
reply = app.run()
print(reply)
运行此应用将为您带来以下功能:
点击任一按钮都将退出应用,且该方法会根据所点击的按钮返回相应结果。 run() "yes" "no"
返回类型 ¶
你可能已经注意到,我们采用了子类化的方式而非常规做法。 App[str] App
from textual.app import App, ComposeResult
from textual.widgets import Label, Button
class QuestionApp(App[str]):
def compose(self) -> ComposeResult:
yield Label("Do you love Textual?")
yield Button("Yes", id="yes", variant="primary")
yield Button("No", id="no", variant="error")
def on_button_pressed(self, event: Button.Pressed) -> None:
self.exit(event.button.id)
if __name__ == "__main__":
app = QuestionApp()
reply = app.run()
print(reply)
"添加类型注解告诉 mypy 该函数预期返回字符串类型。如果调用 App.exit()时未提供返回值,则可能返回 None,因此函数的返回类型应为 Optional[str]。请将占位符替换为你实际调用 exit 方法时使用的值的具体类型。 [str] run() None run str | None str [str] "
在 Textual 界面输入文本
类型注解在 Textual 中完全属于可选功能(但建议使用)。
返回码 ¶
使用 App.exit() 退出 Textual 应用时,可通过参数可选地指定返回代码。 return_code
返回码是什么?
返回码是操作系统提供的标准功能。当程序退出时,会返回一个整数值来表示执行状态:返回 0 表示成功,非零值则表示出现错误。不同应用程序对非零返回码的具体定义可能各不相同。
当 Textual 应用正常退出时,返回码为 0。若出现未捕获的异常,Textual 会将返回码设为 1。如需将特定错误状态与未处理异常区分开,可自定义返回码的值。 0 1
"以下是为错误条件设置返回码的示例:"
应用程序的返回码可通过查询获取,若未设置则为空,否则将返回一个整数值。 app.return_code None
Textual 不会主动终止进程。若需退出应用并返回状态码,应调用相应方法。具体操作如下: sys.exit
"暂停"
Textual 应用支持挂起功能,允许您暂时退出应用模式。这一特性常用于临时切换至其他终端应用程序。
例如,您可以用此功能让用户使用他们喜欢的文本编辑器来编辑内容。
信息
文本网页版不支持应用挂起功能。
挂起上下文管理器 ¶
您可以使用 App.suspend 上下文管理器来暂停应用程序。当用户点击按钮时,下方的 Textual 应用会启动 vim 文本编辑器:
from os import system
from textual import on
from textual.app import App, ComposeResult
from textual.widgets import Button
class SuspendingApp(App[None]):
def compose(self) -> ComposeResult:
yield Button("Open the editor", id="edit")
@on(Button.Pressed, "#edit")
def run_external_editor(self) -> None:
with self.suspend():
system("vim")
if __name__ == "__main__":
SuspendingApp().run()
从前台挂起 ¶
在 Unix 及类 Unix 系统(如 GNU/Linux、macOS 等)中,Textual 支持用户通过快捷键组合将应用程序作为前台进程挂起。默认情况下,该功能对应的组合键(通常是+键)在 Textual 应用中被禁用,但系统提供了相应操作( action_suspend_process ),用户可按常规方式进行绑定。示例用法: Ctrl Z
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.widgets import Label
class SuspendKeysApp(App[None]):
BINDINGS = [Binding("ctrl+z", "suspend_process")]
def compose(self) -> ComposeResult:
yield Label("Press Ctrl+Z to suspend!")
if __name__ == "__main__":
SuspendKeysApp().run()
笔记
若运行环境为 Windows 系统,或应用程序托管于 Textual Web 平台时,该调用将自动忽略。
"CSS 段落"
文本类应用可通过引用 CSS 文件来定义界面及组件样式,从而避免在项目中混杂杂乱的显示相关代码。
信息
文本类应用通常使用外部 CSS 文件的扩展名,以便与浏览器()文件区分开来。 .tcss .css
"《Textual CSS》章节详细讲解了 CSS 的使用方法。接下来,我们先了解如何在应用中引用外部 CSS 文件。"
以下示例通过添加类变量实现 CSS 加载功能: CSS_PATH
from textual.app import App, ComposeResult
from textual.widgets import Button, Label
class QuestionApp(App[str]):
CSS_PATH = "question02.tcss"
def compose(self) -> ComposeResult:
yield Label("Do you love Textual?", id="question")
yield Button("Yes", id="yes", variant="primary")
yield Button("No", id="no", variant="error")
def on_button_pressed(self, event: Button.Pressed) -> None:
self.exit(event.button.id)
if __name__ == "__main__":
app = QuestionApp()
reply = app.run()
print(reply)
笔记
我们还向其中添加了一个元素,以便通过 CSS 来设置其样式。 id Label
若路径是相对路径(如上所示),则其相对位置基于应用定义的位置。因此,该示例引用了与 Python 代码同目录下的文件。对应的 CSS 文件如下: "question01.tcss"
Screen {
layout: grid;
grid-size: 2;
grid-gutter: 2;
padding: 2;
}
#question {
width: 100%;
height: 100%;
column-span: 2;
content-align: center bottom;
text-style: bold;
}
Button {
width: 100%;
}
运行时会自动加载并更新应用程序及小组件。虽然代码与之前的示例几乎一致,但现在的应用界面已大不相同: "question02.py" "question02.tcss"
类变量 CSS ¶
尽管多数情况下推荐使用外部 CSS 文件(它支持实时编辑等实用功能),但您也可以直接在 Python 代码中编写 CSS 样式。
要实现这一点,请在应用中设置一个类变量,并将其赋值为包含 CSS 样式的字符串。 CSS
这是一个使用 classvar CSS 样式的问题应用:
from textual.app import App, ComposeResult
from textual.widgets import Label, Button
class QuestionApp(App[str]):
CSS = """
Screen {
layout: grid;
grid-size: 2;
grid-gutter: 2;
padding: 2;
}
#question {
width: 100%;
height: 100%;
column-span: 2;
content-align: center bottom;
text-style: bold;
}
Button {
width: 100%;
}
"""
def compose(self) -> ComposeResult:
yield Label("Do you love Textual?", id="question")
yield Button("Yes", id="yes", variant="primary")
yield Button("No", id="no", variant="error")
def on_button_pressed(self, event: Button.Pressed) -> None:
self.exit(event.button.id)
if __name__ == "__main__":
app = QuestionApp()
reply = app.run()
print(reply)
"标题与副标题 ¶"
文本类应用通常包含一个基本属性(默认为应用类的名称)和一个可选属性(用于添加上下文信息,如当前操作的文件)。默认情况下,前者会被自动设为应用类名,后者则为空值。您可以通过定义类变量来修改这些默认设置。具体示例如下: title sub_title title sub_title TITLE SUB_TITLE
from textual.app import App, ComposeResult
from textual.widgets import Button, Header, Label
class MyApp(App[str]):
CSS_PATH = "question02.tcss"
TITLE = "A Question App"
SUB_TITLE = "The most important question"
def compose(self) -> ComposeResult:
yield Header()
yield Label("Do you love Textual?", id="question")
yield Button("Yes", id="yes", variant="primary")
yield Button("No", id="no", variant="error")
def on_button_pressed(self, event: Button.Pressed) -> None:
self.exit(event.button.id)
if __name__ == "__main__":
app = MyApp()
reply = app.run()
print(reply)
请注意,标题和副标题会通过内置的 Header 组件显示在屏幕顶部:
您还可以在应用程序的方法中动态设置标题属性。下面的示例展示了如何通过按键操作来设置主标题和副标题:
from textual.app import App, ComposeResult
from textual.events import Key
from textual.widgets import Button, Header, Label
class MyApp(App[str]):
CSS_PATH = "question02.tcss"
TITLE = "A Question App"
SUB_TITLE = "The most important question"
def compose(self) -> ComposeResult:
yield Header()
yield Label("Do you love Textual?", id="question")
yield Button("Yes", id="yes", variant="primary")
yield Button("No", id="no", variant="error")
def on_button_pressed(self, event: Button.Pressed) -> None:
self.exit(event.button.id)
def on_key(self, event: Key):
self.title = event.key
self.sub_title = f"You just pressed {event.key}!"
if __name__ == "__main__":
app = MyApp()
reply = app.run()
print(reply)
运行该应用时按下 T 键,即可看到标题同步更新:
信息
请注意,设置标题属性时无需手动刷新屏幕。这是响应式特性的一个示例,我们将在后续指南中详细介绍。
接下来呢 ¶
下一章我们将深入讲解如何为控件和应用设置样式。