多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Textual App 应用开发指南:从 App 类到事件、挂载、退出与样式

Textual App 应用开发指南:从 App 类到事件、挂载、退出与样式 Textual App 应用开发指南从 App 类到事件、挂载、退出与样式【免费下载链接】textualThe lean application framework for Python. Build sophisticated user interfaces with a simple Python API. Run your apps in the terminal and a web browser.项目地址: https://gitcode.com/gh_mirrors/te/textual本篇指南以 Textual 的App类为核心完整讲解如何创建、运行一个终端应用从最简的App子类与run()启动流程到事件系统、Widget 的 compose 与 mount、应用退出与返回码、挂起suspend机制再到 CSS 与标题栏配置。读者学完后可以独立写出结构完整、可交互、带样式并可安全退出的 Textual 应用。本指南依据 docs/guide/app.md 编写并结合 src/textual/app.py 源码与 docs/examples/app 目录下的完整示例进行纵深讲解。App 类一切的起点构建 Textual 应用的第一步是从textual.app导入App类并创建子类。最简单的应用类只需要一个空的继承例如 docs/examples/app/simple01.pyfrom textual.app import App class MyApp(App): pass这个类目前还不会做任何事但它已经是合法的 Textual 应用Textual 会为它自动创建默认的Screen、进入终端渲染管线并响应输入。几乎所有 Textual 能力——事件处理、Widget 挂载、CSS 加载、工作线程管理等——都以App子类作为宿主。run 方法启动应用创建实例后调用run()即可启动应用见 docs/examples/app/simple02.pyfrom textual.app import App class MyApp(App): pass if __name__ __main__: app MyApp() app.run()运行python simple02.py后你会看到一个空白的终端。这里有一个实用的技巧值得说明__name__ __main__这个条件只在用python命令直接运行该文件时才为真。它的好处有两点在其他脚本中import这个模块时不会立即启动应用方便复用与测试允许 Textual 的 devtools run 命令以开发模式运行应用便于热重载调试。App.run()的关键行为是调用后 Textual 会把终端置入一种特殊状态称为应用模式application mode。进入应用模式后终端不再回显你键入的内容Textual 接管对用户输入键盘与鼠标的响应Textual 负责更新终端的可见部分即“屏幕”。按下CtrlQTextual 会退出应用模式并返回命令提示符应用模式之前终端里的内容会原样恢复。从源码看run()内部通过async def run_app()组装 driver 并启动事件循环src/textual/app.py它还支持headless无输出运行常用于测试、inline、inline_no_clear等参数。内联模式Run inline该特性在 0.55.0 版本中加入。除了全屏应用模式Textual 还支持内联模式inline mode应用会出现在命令提示符的下方不会进入应用模式。内联应用非常适合与终端日常工作流紧密集成的工具——例如把格式化结果、清单、预览直接输出在当前命令行上下文里。启用方式是在调用App.run()时设置inlineTrueapp.run(inlineTrue)如果想对内联应用补充额外的样式可参考 docs/how-to/style-inline-apps.md。需要注意内联模式目前不支持 Windows。ANSI 颜色处理该特性在 0.80.0 版本中加入。终端通常支持 16 种可主题化的ANSI颜色用户可以在终端设置中自定义。默认情况下Textual 会用自己选定的颜色替代这些 ANSI 颜色原因详见 docs/FAQ.md#why-doesnt-textual-support-ansi-themes。如需保留终端本身的 ANSI 颜色可以在App构造函数中设置ansi_colorTrueapp MyApp(ansi_colorTrue)官方建议全屏应用推荐使用默认行为而内联应用由于与终端外观融合更紧密可能更希望保留 ANSI 颜色。从源码看该参数是一个响应式属性ansi_color: Reactive[bool | None]src/textual/app.py传None时遵循当前主题的ansi设置当为True时 Textual 会启用NoColor/Monochrome之类的过滤器并停用 ANSI 转真彩ANSIToTruecolor转换src/textual/app.py。事件系统响应输入与状态变化Textual 拥有完整的事件系统详见 docs/guide/events.md用于响应按键、鼠标操作和内部状态变化。事件处理器的命名约定是以on_开头后接事件名称。两个最常用的事件mount 事件应用进入应用模式后被立即发送可以通过定义on_mount方法响应key 事件用户按下按键时发送通过on_key方法响应。下面的例子同时演示了这两个事件处理器docs/examples/app/event01.pyfrom 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——正如你所猜测的背景会变成深蓝色。由于 mount 事件在进入应用模式后立即发送运行这段代码你会立刻看到蓝色屏幕。按下按键时on_key处理器会收到一个Key实例。如果你在处理器中不需要事件对象可以省略该参数例如上面的on_mount。事件可能携带额外信息可以在处理器中检查就Key事件而言其key属性就是被按下按键的名称。上面例子利用该属性当按下 0 到 9 中任意数字键时把背景色切换为COLORS列表中对应的颜色。异步事件Textual 基于 Python 的 asyncio 框架构建大量使用async/await关键字。如果事件处理器是协程即以async开头定义的函数Textual 会等待它执行完毕。普通同步函数通常也完全够用除非你需要集成其他异步库——例如用httpx从互联网读取数据、执行 I/O 密集型操作时异步处理器能避免阻塞整个事件循环。Widgets构建用户界面Widget控件是自包含的组件负责生成屏幕上一块区域的输出。Widget 与 App 一样可以响应事件。大多数有实际功能的 App 都包含至少一个通常多个Widget它们共同构成用户界面。Widget 的复杂度跨度很大可以是一段文字、一个按钮也可以是文本编辑器或文件浏览器这样的完整组件后者内部还可以再包含自己的子 Widget。组合Composing要向应用添加 Widget需要实现compose()方法它应返回一个可迭代的Widget实例集合。列表可以但更优雅的写法是使用yield逐个产出 Widget让方法成为生成器。下面的例子导入了内置的WelcomeWidget并在App.compose()中将其产出docs/examples/app/widgets01.pyfrom 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 会挂载mountWelcomeWidget——它包含一段 Markdown 内容和一个按钮。注意其中的on_button_pressed方法它处理的是WelcomeWidget 内部按钮发送的Button.Pressed事件处理器调用App.exit()退出应用。挂载Mountingcompose()是应用启动时添加 Widget 的首选方式但有时需要在事件响应过程中动态添加新的 Widget。这时可以调用mount()把新 Widget 加入界面。下面的应用在任何按键时都会添加一个WelcomeWidgetdocs/examples/app/widgets02.pyfrom 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()首次运行你会看到空白屏幕按任意键即可添加WelcomeWidget多次按键会添加多个 Widget。等待挂载完成Awaiting mount挂载一个 Widget 时Textual 会挂载该 Widget所组合的所有子 Widget。Textual 保证挂载过程会在下一个消息处理器之前完成但不会在mount()调用返回后立即完成。如果你想在同一个消息处理器里立刻修改刚挂载的 Widget就会遇到问题。先用一个例子说明问题。下面的代码在按键时挂载WelcomeWidget并尝试把其中的 Button 标签从 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! # (1)! if __name__ __main__: app WelcomeApp() app.run()关于query_one方法的详细说明见 docs/guide/queries.md。运行这个例子按任意键时 Textual 会抛出NoMatches异常——因为当我们试图修改按钮时挂载过程尚未完成query_one(Button)查不到任何按钮。解决方案是等待mount()的结果这要求把函数改为async。这样能保证执行到下一行时 Button 已经挂载完成可以安全地修改其标签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()完整可运行版本见 docs/examples/app/widgets04.py运行后按a键即可看到按钮文字已变为 YES!。退出应用exit 与返回值应用会一直运行直到你调用App.exit()。exit()会退出应用模式之后run()方法返回如果这行代码位于程序末尾你将回到命令提示符。exit()还接受一个可选的位置参数作为run()的返回值。下面的例子用这一点返回被点击按钮的id标识符docs/examples/app/question01.pyfrom 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, idyes, variantprimary) yield Button(No, idno, varianterror) 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。返回类型Return type你可能注意到这里继承的是App[str]而不是普通的Appclass QuestionApp(App[str]):[str]这个泛型参数告诉 mypyrun()预期返回字符串。由于App.exit()也可能在不传返回值的情况下调用因此run()的实际返回类型是str | None。把[str]中的str替换成你打算传给exit()的值类型即可获得对应的类型检查支持。Typing in TextualTextual 中类型注解完全是可选的但推荐使用。返回码Return code用App.exit()退出应用时还可以通过return_code参数指定一个返回码。返回码是操作系统提供的标准机制任何应用退出时都可以返回一个整数表示执行是否成功。0表示成功其他值表示发生了错误非零返回码的具体含义由应用自行定义。Textual 应用正常退出时返回码为0如果出现未捕获的异常Textual 会设置返回码1。当你想区分特定的错误条件与未捕获异常时可以设置其他值if critical_error: self.exit(return_code4, messageCritical error occurred)应用的返回码可以通过app.return_code查询——在未设置时为None否则为整数该属性定义见 src/textual/app.py。需要特别说明Textual 不会主动退出进程。要以某个返回码退出应用你应该自行调用sys.exitif __name__ __main__: app MyApp() app.run() import sys sys.exit(app.return_code or 0)挂起SuspendingTextual 应用可以被挂起suspend从而在一段时间内离开应用模式。这通常用于临时把当前应用替换成另一个终端程序——例如让用户用自己偏好的文本编辑器编辑内容。注意应用挂起在 Textual Webtextual-web环境下不可用。挂起上下文管理器使用App.suspend()上下文管理器即可挂起应用。下面的应用在用户点击按钮时启动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, idedit) on(Button.Pressed, #edit) def run_external_editor(self) - None: with self.suspend(): # (1)! system(vim) if __name__ __main__: SuspendingApp().run()with语句体内的所有代码都将在应用被挂起的状态下运行。完整示例见 docs/examples/app/suspend.py。这里还展示了 Textual 的on装饰器用法——它可以把处理器精确绑定到特定 Widget这里是#edit按钮的特定事件上比全局on_*命名约定更聚焦。从源码看suspend()在进入with块前先发布挂起信号并调用driver.suspend_application_mode()在块内临时恢复标准 stdout/stderr 以便外部程序正常输出块结束yield返回后恢复应用模式并发布恢复信号、触发界面刷新src/textual/app.py。如果当前环境不支持挂起driver 的can_suspend为假会抛出SuspendNotSupported异常。从前台挂起Suspending from foreground在 Unix 及类 Unix 系统GNU/Linux、macOS 等上Textual 支持用户按下某个按键组合把应用作为前台进程挂起。惯例上是CtrlZ在 Textual 应用中这个组合默认被禁用但框架提供了对应的动作action_suspend_process你可以用常规方式为其绑定按键from textual.app import App, ComposeResult from textual.binding import Binding from textual.widgets import Label class SuspendKeysApp(App[None]): BINDINGS [Binding(ctrlz, suspend_process)] def compose(self) - ComposeResult: yield Label(Press CtrlZ to suspend!) if __name__ __main__: SuspendKeysApp().run()完整示例见 docs/examples/app/suspend_process.py。在 Unix 系系统上action_suspend_process会向应用进程发送SIGTSTP信号在 Windows 上或应用托管在 Textual Web 下时该调用会被直接忽略。CSS让应用拥有外观Textual 应用可以引用 CSS 文件来定义应用与 Widget 的显示外观从而把界面相关的代码从业务逻辑中剥离。Textual 应用的外部 CSS 文件通常使用.tcss扩展名以区别于浏览器的.css文件。Textual CSS 一章详细讲解了 CSS 的用法这里聚焦于应用如何引用外部 CSS 文件。下面的例子通过添加CSS_PATH类变量启用 CSS 加载docs/examples/app/question02.pyfrom 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?, idquestion) yield Button(Yes, idyes, variantprimary) yield Button(No, idno, varianterror) def on_button_pressed(self, event: Button.Pressed) - None: self.exit(event.button.id) if __name__ __main__: app QuestionApp() reply app.run() print(reply)注意这里给Label添加了idquestion因为我们要在 CSS 中针对它做样式化。如果CSS_PATH是相对路径如上例它被解释为相对应用定义所在位置——因此该示例引用的question02.tcss与 Python 代码位于同一目录。对应的 CSS 文件内容如下docs/examples/app/question02.tcssScreen { 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并据此更新应用和 Widget。这段代码与前面的示例几乎相同但界面外观已经大不一样标题占据两列、按钮等宽铺开、整体采用带内边距的网格布局。类变量 CSSClassvar CSS虽然外部 CSS 文件是大多数应用推荐的做法它还支持live editing之类的酷特性但你也可以直接在 Python 代码中指定 CSS——只需在 App 上设置一个CSS类变量值为包含 CSS 的字符串。下面是用类变量 CSS 重写的 Question 应用docs/examples/app/question03.pyfrom 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?, idquestion) yield Button(Yes, idyes, variantprimary) yield Button(No, idno, varianterror) 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类变量属于“内联 CSS”inline CSS适合快速脚本它在CSS_PATH之后加载src/textual/app.py两者可以共存且类变量 CSS 优先级更高。标题与副标题Title and subtitleTextual 应用具有title属性通常是应用名称和可选的sub_title属性提供额外上下文如当前正在编辑的文件。默认情况下title为 App 类的名称sub_title为空字符串。可以通过定义TITLE和SUB_TITLE类变量来改变默认值源码见 src/textual/app.py。下面的示例同时演示了这两个类变量docs/examples/app/question_title01.pyfrom 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?, idquestion) yield Button(Yes, idyes, variantprimary) yield Button(No, idno, varianterror) 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 Widget 显示在屏幕顶部——注意示例中compose()里显式yield Header()了。标题属性也可以在应用的某个方法内动态设置。下面的例子在按键时更新标题与副标题docs/examples/app/question_title02.pyfrom 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?, idquestion) yield Button(Yes, idyes, variantprimary) yield Button(No, idno, varianterror) 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 fYou just pressed {event.key}! if __name__ __main__: app MyApp() reply app.run() print(reply)运行后按下t键Header 会相应更新。设置标题属性时无需显式刷新屏幕。这是 响应式reactivity 的体现指南后续章节会深入讲解Textual 通过响应式属性自动监测title/sub_title的变化并触发重绘。下一步在接下来的章节中我们将进一步学习如何为 Widget 和应用应用更丰富的样式包括 Textual CSS 的完整语法、布局系统与响应式编程模型。【免费下载链接】textualThe lean application framework for Python. Build sophisticated user interfaces with a simple Python API. Run your apps in the terminal and a web browser.项目地址: https://gitcode.com/gh_mirrors/te/textual创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表