Skip to content

如何在 Python 中获取终端窗口大小

Posted on:2024年9月7日 at 10:59

在开发命令行应用程序时,了解终端窗口的大小是非常有用的。这可以帮助我们格式化输出以适应屏幕,创建更好的用户界面,或者根据可用空间调整应用程序的行为。本文将探讨在 Python 中获取终端窗口大小的各种方法。

为什么需要获取终端大小?

在深入技术细节之前,让我们先了解一下为什么获取终端大小是有用的:

  1. 格式化输出 - 知道终端的宽度可以帮助我们正确地换行文本或格式化表格。
  2. 自适应 UI - 我们可以根据可用空间调整用户界面元素的大小和位置。
  3. 响应式设计 - 应用程序可以根据终端大小改变其行为,例如在小屏幕上显示简化版本。
  4. 性能优化 - 在处理大量数据时,了解屏幕大小可以帮助优化输出。
  5. 改善用户体验 - 通过适应用户的环境,我们可以创建更加用户友好的应用程序。

现在让我们看看如何在 Python 中实现这一点。

使用 Python 标准库

从 Python 3.3 开始,标准库提供了获取终端大小的内置方法。这是最简单和最可靠的方法。

使用 os 模块

import os

# 获取终端大小
terminal_size = os.get_terminal_size()

# 打印宽度和高度
print(f"Terminal width: {terminal_size.columns}")
print(f"Terminal height: {terminal_size.lines}")

这个方法返回一个 os.terminal_size 对象,包含了 columns(宽度)和 lines(高度)属性。

使用 shutil 模块

shutil 模块提供了一个更高级的接口,它在内部使用 os.get_terminal_size()

import shutil

# 获取终端大小
columns, lines = shutil.get_terminal_size()

print(f"Terminal width: {columns}")
print(f"Terminal height: {lines}")

这两种方法都很简单直接,并且在大多数情况下都能很好地工作。然而,它们在某些情况下可能会失败,例如当标准输入/输出被重定向时。

跨平台解决方案

如果你需要支持旧版本的 Python 或者需要一个更健壮的跨平台解决方案,你可能需要使用一些更复杂的方法。

使用 fcntl 和 termios(Unix 系统)

在 Unix 系统上,我们可以使用 fcntltermios 模块来直接查询终端设备:

import fcntl
import termios
import struct

def get_terminal_size():
    try:
        with open(0, 'rb') as fd:
            size = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234'))
        return size[1], size[0]  # 返回 (width, height)
    except:
        return None

size = get_terminal_size()
if size:
    print(f"Terminal width: {size[0]}")
    print(f"Terminal height: {size[1]}")
else:
    print("Unable to determine terminal size")

这种方法直接与操作系统交互,通常更可靠,但它只在 Unix 系统上工作。

使用 ctypes(Windows 系统)

在 Windows 上,我们可以使用 ctypes 模块来调用 Windows API:

import ctypes

def get_terminal_size():
    try:
        from ctypes import windll, create_string_buffer
        h = windll.kernel32.GetStdHandle(-12)  # stderr handle
        csbi = create_string_buffer(22)
        res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi)
        if res:
            (bufx, bufy, curx, cury, wattr,
             left, top, right, bottom, maxx, maxy) = struct.unpack("hhhhHhhhhhh", csbi.raw)
            width = right - left + 1
            height = bottom - top + 1
            return width, height
    except:
        pass

size = get_terminal_size()
if size:
    print(f"Terminal width: {size[0]}")
    print(f"Terminal height: {size[1]}")
else:
    print("Unable to determine terminal size")

这种方法在 Windows 系统上工作得很好,但在其他操作系统上不可用。

使用 subprocess 调用外部命令

另一种跨平台的方法是使用 subprocess 模块调用外部命令:

import subprocess
import shlex

def get_terminal_size():
    try:
        if 'windows' in subprocess.check_output('uname').decode().lower():
            # Windows 系统
            rows, columns = subprocess.check_output(['tput', 'lines']), subprocess.check_output(['tput', 'cols'])
        else:
            # Unix 系统
            rows, columns = subprocess.check_output(['stty', 'size']).split()
        return int(columns), int(rows)
    except:
        return None

size = get_terminal_size()
if size:
    print(f"Terminal width: {size[0]}")
    print(f"Terminal height: {size[1]}")
else:
    print("Unable to determine terminal size")

这种方法的优点是它可以在大多数系统上工作,但缺点是它依赖于外部命令,可能会稍微慢一些。

处理边缘情况

在实际应用中,我们还需要考虑一些边缘情况:

  1. 终端大小可能会改变 - 用户可能会调整窗口大小。
  2. 程序可能在非交互式环境中运行 - 例如,作为后台作业或通过管道运行。
  3. 环境变量可能被设置为覆盖实际的终端大小。

以下是一个更健壮的实现,它尝试多种方法并处理这些边缘情况:

import os
import shlex
import struct
import platform
import subprocess

def get_terminal_size():
    """ 获取终端窗口大小的跨平台实现 """
    current_os = platform.system()
    tuple_xy = None
    if current_os == 'Windows':
        tuple_xy = _get_terminal_size_windows()
        if tuple_xy is None:
            tuple_xy = _get_terminal_size_tput()
    if current_os in ['Linux', 'Darwin'] or current_os.startswith('CYGWIN'):
        tuple_xy = _get_terminal_size_linux()
    if tuple_xy is None:
        print("Unable to determine terminal size. Using default values.")
        tuple_xy = (80, 25)      # 默认大小
    return tuple_xy

def _get_terminal_size_windows():
    try:
        from ctypes import windll, create_string_buffer
        h = windll.kernel32.GetStdHandle(-12)
        csbi = create_string_buffer(22)
        res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi)
        if res:
            (bufx, bufy, curx, cury, wattr,
             left, top, right, bottom, maxx, maxy) = struct.unpack("hhhhHhhhhhh", csbi.raw)
            sizex = right - left + 1
            sizey = bottom - top + 1
            return sizex, sizey
    except:
        pass

def _get_terminal_size_tput():
    try:
        cols = int(subprocess.check_output(shlex.split('tput cols')))
        rows = int(subprocess.check_output(shlex.split('tput lines')))
        return (cols, rows)
    except:
        pass

def _get_terminal_size_linux():
    def ioctl_GWINSZ(fd):
        try:
            import fcntl
            import termios
            cr = struct.unpack('hh',
                               fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234'))
            return cr
        except:
            pass
    cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2)
    if not cr:
        try:
            fd = os.open(os.ctermid(), os.O_RDONLY)
            cr = ioctl_GWINSZ(fd)
            os.close(fd)
        except:
            pass
    if not cr:
        try:
            cr = (os.environ['LINES'], os.environ['COLUMNS'])
        except:
            return None
    return int(cr[1]), int(cr[0])

if __name__ == "__main__":
    sizex, sizey = get_terminal_size()
    print(f'width = {sizex}, height = {sizey}')

这个实现首先尝试使用特定于操作系统的方法,如果失败则回退到更通用的方法。它还检查环境变量,以防用户手动设置了终端大小。

使用第三方库

如果你不想处理所有这些复杂性,你可以考虑使用一些第三方库:

blessings

blessings 是一个流行的终端库,它提供了一种简单的方法来获取终端大小:

from blessings import Terminal

t = Terminal()
print(f"Terminal width: {t.width}")
print(f"Terminal height: {t.height}")

blessed

blessedblessings 的一个分支,提供了更多功能和更好的维护:

from blessed import Terminal

term = Terminal()
print(f"Terminal width: {term.width}")
print(f"Terminal height: {term.height}")

asciimatics

asciimatics 是一个功能强大的库,用于创建全屏终端应用程序和动画:

from asciimatics.screen import Screen

screen = Screen.open()
print(f"Terminal width: {screen.width}")
print(f"Terminal height: {screen.height}")
screen.close()

这些库不仅提供了获取终端大小的功能,还提供了许多其他有用的终端操作功能。

结论

获取终端窗口大小是一个看似简单但实际上相当复杂的任务,特别是当你需要考虑跨平台兼容性和各种边缘情况时。在大多数情况下,使用 Python 3.3+ 的内置 os.get_terminal_size()shutil.get_terminal_size() 函数就足够了。

然而,如果你需要更多的控制或需要支持旧版本的 Python,你可能需要使用更复杂的方法或第三方库。无论你选择哪种方法,重要的是要记住处理可能的错误和边缘情况,以确保你的应用程序在各种环境中都能正常工作。

通过正确地获取和使用终端大小信息,你可以创建出更加智能和用户友好的命令行应用程序。这不仅可以改善用户体验,还可以使你的应用程序在不同的环境中表现得更加一致和可靠。