心血来潮想在终端里打音游,就上网搜搜cli有没有图形渲染引擎一类的玩意,然后找到了textual。的确有过往命令行里加入图形界面组件的设想,但没想到已经有现成品了,还是得来学习学习(ʃƪ^3^)
一、起步
使用前先安装textual库:
pip install textual
来编写第一个程序:
from textual.app import App
#导入用到的组件:静态文字、标题栏、页脚栏
from textual.widgets import Static,Header,Footer
class TestApp(App):
#绑定按键
BINDINGS = [
("q", "quit", "Quit"),
]
#简单的样式表
CSS = """
Static{
content-align: center middle;
height: 100%;
}
"""
def compose(self):
#加入组件
yield Static("Hello, World!")
yield Header()
yield Footer()
if __name__ == "__main__":
TestApp().run()
二、组件
和qt与WinForm类似,都有组件这玩意,使用前需要预先从textual.widgets导入:
1.基础展示类
yield Header() # 顶部状态栏
yield Footer() # 底部快捷键栏
yield Label("我是只读文本标签") # 简单文本
yield Static("我是基础容器/方块") # 最通用的方块
yield Sparkline([1, 3, 2, 5, 4]) # 迷你趋势图
2.交互输入类
yield Button("点击我", variant="success") # 按钮 (success/error/warning)
yield Input(placeholder="请输入内容...") # 文本输入框
yield Checkbox("记得勾选我") # 复选框
yield Switch() # 开关
yield RadioSet("选项A", "选项B") # 单选框组
yield Select([("显示", "值")], prompt="请选择") # 下拉选择器
3.数据与列表类
yield ListView(ListItem(Static("列表项"))) # 列表视图
yield DirectoryTree("./") # 文件目录树
yield DataTable() # 数据库样式的表格
yield OptionList("项目1", "项目2") # 纯文本选项列表
4.反馈与进度类
yield ProgressBar(total=100, show_eta=True) # 进度条
yield Log() # 实时日志输出框
yield RichLog() # 支持带颜色格式的日志
yield LoadingIndicator() # 正在加载的转圈动画
5.一堆容器
#须从textual.containers导入
with Vertical(): # 垂直堆叠
...
with Horizontal(): # 水平并排
...
with Grid(): # 网格布局
...
with ScrollableContainer():# 带滚动条的盒子
...
with TabbedContent(): # 选项卡切换
with TabPane("标签1"):
yield Static("内容1")
with TabPane("标签2"):
yield Static("内容2")
三、样式
自己家的TCSS,和qt样式表有着异曲同工之妙(果然css还是好用
| 特性 | CSS | Textual TCSS |
| 选择器 | 选择器 { 属性: 值; } | 一模一样 |
| 层叠性 | #id, .class, tag | 一模一样 |
| 伪类 | :hover, :active, :focus | :hover, :focus |
| 布局 | 极强(flex和grid) | 也很强(也是flex和grid) |
1.盒子模型
width: 100%; /* 宽度:支持 1fr, 50%, 40(字符), auto */
height: 1fr; /* 高度:1fr 代表占用剩余所有空间 */
min-width: 20; /* 最小宽度:防止容器被挤没 */
max-height: 10; /* 最大高度 */
margin: 1 2; /* 外边距:1(上下) 2(左右) 的字符空隙 */
padding: 1; /* 内边距:内容离边框的距离 */
border: solid $accent; /* 边框:样式(solid/double/heavy/round) 颜色 */
border-title-align: center; /* 边框标题对齐:配合组件的 border_title 属性 */
2.布局
layout: vertical; /* 布局模式:内容垂直堆叠(类似标准div) */
layout: horizontal; /* 布局模式:内容水平排队 */
align: center middle; /* 子组件对齐:让里面的小方块在自己中心 */
content-align: left top; /* 内容对齐:让文字在自己内部的对齐位置 */
dock: left; /* 悬浮停靠:直接锁在左侧(固定侧边栏) */
layer: top; /* 层级:类似 z-index,决定重叠时的上下顺序 */
3.视觉风格
background: $surface; /* 背景色:推荐用变量,也支持 #hex 或 rgb() */
color: white; /* 文字颜色 */
opacity: 80%; /* 透明度:0% 到 100% */
text-style: bold italic; /* 文字样式:加粗、斜体、下划线(underline) */
text-align: center; /* 文字水平对齐 */
text-opacity: 50%; /* 仅文字透明:背景保持原色 */
4.动效
display: none; /* 是否渲染:完全消失且不占位 */
visibility: hidden; /* 是否可见:消失但位置还留着 */
offset-x: 2; /* 水平位移:相对原位置向右挪2格 */
offset-y: -1; /* 垂直位移:相对原位置向上挪1格 */
scrollbar-gutter: stable; /* 滚动条占位:防止滚动条出现时画面左右晃动 */
四、事件
1.经典on_语法
直接在程序类下面新建 on_组件_事件 函数就可以创建事件:
from textual.widgets import Button, Input
class MyView(Static):
def compose(self):
yield Input(placeholder="输入你的名字", id="user_name")
yield Button("打招呼", id="greet_btn")
# 当 ID 为 greet_btn 的按钮被按下时
def on_button_pressed(self, event: Button.Pressed) -> None:
name = self.query_one("#user_name").value
self.notify(f"你好,{name}!") # 弹出一个通知
注意这个事件没有绑定按键的ID,这就是textual事件系统独特的地方:消息冒泡。
实例中on_button_pressed函数实际是一个捕获器,当button被点击时会发出Button.Pressed消息,被捕获器捕获后执行。
因此我们需要使用event.button.id进一步确认:
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "ok":
self.notify("你点了确定")
elif event.button.id == "cancel":
self.notify("你点了取消")
2.监听器装饰器@on
@on比较的精简直观清晰,而且它也可以直接绑定id:
from textual import on
# 监听任何按钮的点击
@on(Button.Pressed)
def handle_all_buttons(self):
self.log("某个按钮被按下了")
# 只监听特定 ID 的按钮
@on(Button.Pressed, "#start-btn")
def start_game(self):
self.start_logic()
3.操作其他组件:query_one
很类似dom操作了,可以根据选择器查找一个或一组组件:
self.query_one("#id") #找一个组件
self.query(".my-class") #找一组组件
操作举例:
def on_switch_changed(self, event: Switch.Changed) -> None:
# 找到页面上的 Label 并修改它
label = self.query_one("#status-label")
if event.value: # 开关的状态
label.update("服务已开启")
label.styles.color = "green"
else:
label.update("服务已关闭")
label.styles.color = "red"
4.全局快捷键:BINDINGS结合action_
简述:BINDINGS中指定的快捷键可以直接对应action_xxx函数:
class MusicApp(App):
BINDINGS = [
("space", "play_pause", "播放/暂停"),
("up", "volume_up", "音量+"),
]
def action_play_pause(self) -> None:
# 当按下空格键时执行
self.is_playing = not self.is_playing
self.notify("状态已切换")
def action_volume_up(self) -> None:
self.volume += 10
5.定时器
on_mount在页面启动时执行(结束前就是on_unmount),set_interval用于定时
def on_mount(self):
# 每 0.1 秒执行一次 self.tick
self.set_interval(0.1, self.tick)
def tick(self):
# 更新进度条
progress = self.query_one(ProgressBar)
progress.advance(1)
五、切换页面
可以定义多个screen类(就像不同网页文件),然后用push_screen把新页面盖在旧页面上(着实新奇):
from textual.app import App, ComposeResult
from textual.screen import Screen
from textual.widgets import Button, Label
# --- 定义第二个页面(游戏页) ---
class GameScreen(Screen):
def compose(self) -> ComposeResult:
yield Label("这里是游戏界面![正在播放音乐...]")
yield Button("返回主菜单", id="back")
def on_button_pressed(self, event: Button.Pressed) -> None:
self.app.pop_screen() # 把当前页面“拿走”,露出下面的页面
# --- 主程序 ---
class MyApp(App):
def compose(self) -> ComposeResult:
yield Label("这是主菜单")
yield Button("开始游戏", id="start")
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "start":
self.push_screen(GameScreen()) # 推入新页面
if __name__ == "__main__":
MyApp().run()
六、更多操作
1.响应式设计
from textual.reactive import reactive
class MyWidget(Static):
count = reactive(0) # 只要 count 变了,watch_count 就会触发
def watch_count(self, new_value: int):
self.update(f"当前计数: {new_value}")
2.工作线程
#不就是防止卡前台嘛。。
self.run_worker(你的函数)
3.自定义组件
当官方提供的 Button、Input 不够用时,你可以通过继承 Static 并重写 render() 方法,用 Rich 库的语法自己画 UI(比如画个字符画、圆角矩形等)。
这话不是我说的哈,我还得琢磨琢磨呢。。
七、结尾
所以基础的也就这些东西了是吧,搞WinForm这么久是时候来点新东西了!



Comments NOTHING