如何在Windows终端中使用Python获取当前文本光标位置?

4
在Python中获取当前鼠标指针位置是微不足道的,通过使用Windows API ctypes库即可。然而,从鼠标指针的屏幕位置(x,y)到获取当前终端窗口的文本光标位置似乎是一个巨大的难题。
此外,程序员社区经常混淆鼠标指针位置和文本光标位置,这使情况变得更糟。历史上从来没有鼠标“光标”,所以当人们说“光标”时,他们应该指的是文本光标,而不是反过来。由于这个错误,Stackoverflow上充满了与“光标”相关的问题和答案,但似乎没有一个与获取终端shell的当前字符位置有关。[可怕的光标!]
要获取相对的鼠标指针位置
from ctypes import windll, wintypes, byref
def get_cursor_pos():
    cursor = wintypes.POINT()
    windll.user32.GetCursorPos(byref(cursor))
    return (cursor.x, cursor.y)

while(1): print('{}\t\t\r'.format(get_cursor_pos()), end='')

我希望有一个函数,可以给我最后一个字符的位置,以及它所在的行数列数。也许可以像这样:

def cpos(): 
    xy = here_be_magic()
    return xy

# Clear screen and start from top:
print('\x1b[H', end=''); 
print('12345', end='', flush=True); xy=cpos(); print('( {},{})'.format(xy[0],xy[1]),end='', flush=True)

# 12345 (1,5)  # written on top of blank screen

我怎样才能在终端中获取 (行,列) 中的 text 光标位置?
(并且不做任何假设或编写自己的窗口管理器?) 最终我希望能够在任何终端窗口中找到最后一个光标位置,(可能被任何程序使用?)

可能相关(但无用)的SO问题:


更新(2022年1月17日)

查看微软文档后,我现在相信可以从(较旧的、非VT基础的)API调用GetConsoleScreenBufferInfo中获取这个信息,如下所示。

BOOL WINAPI GetConsoleScreenBufferInfo(
  _In_  HANDLE                      hConsoleOutput,            # A handle to the console screen buffer. The handle must have the GENERIC_READ access right. 
  _Out_ PCONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo  # A pointer to a CONSOLE_SCREEN_BUFFER_INFO structure that receives the console screen buffer information.
);

typedef struct _CONSOLE_SCREEN_BUFFER_INFO {
  COORD      dwSize;                # contains the size of the console screen buffer, in character columns and rows.
  COORD      dwCursorPosition;      # contains the column and row coordinates of the cursor in the console screen buffer.
  WORD       wAttributes;           # Character attributes (divided into two classes: color and DBCS)
  SMALL_RECT srWindow;              # A SMALL_RECT structure that contains the console screen buffer coordinates of the upper-left and lower-right corners of the display window.
  COORD      dwMaximumWindowSize;   # A COORD structure that contains the maximum size of the console window, in character columns and rows, given the current screen buffer size and font and the screen size.
} CONSOLE_SCREEN_BUFFER_INFO;       # 

# Defines the coordinates of a character cell in a console screen buffer. 
# The origin of the coordinate system (0,0) is at the top, left cell of the buffer.

typedef struct _COORD {
  SHORT X;              # The horizontal coordinate or column value. The units depend on the function call.
  SHORT Y;              # The vertical coordinate or row value. The units depend on the function call.
} COORD, *PCOORD;


typedef struct _SMALL_RECT {
  SHORT Left;
  SHORT Top;
  SHORT Right;
  SHORT Bottom;
} SMALL_RECT;

所以考虑到这一点,我认为以下方案可行。
cls='\x1b[H'
from ctypes import windll, wintypes, byref
def cpos():
    cursor = wintypes._COORD(ctypes.c_short)
    windll.kernel32.GetConsoleScreenBufferInfo(byref(cursor))
    return (cursor.X, cursor.Y)

cpos()

# TypeError: '_ctypes.PyCSimpleType' object cannot be interpreted as an integer

不确定如何在Python中实现,但在Windows上可以使用UI自动化,例如:https://dev59.com/iW445IYBdhLWcg3w3Nwf,并且使用文本模式,您可以从文本模式获取当前选择和边界矩形:https://learn.microsoft.com/en-us/dotnet/api/system.windows.automation.text.textpatternrange.getboundingrectangles - Simon Mourier
是的,像往常一样,微软文档对于任何实际用途都没什么用。我不明白为什么他们在谈论图形实体时不展示图片。这对我来说只是太多的猜测了。所以,抱歉,除非有人能提供一个可行的例子,否则我没有提供太多帮助。 - not2qubit
我的第一个想法是尝试计算窗口的原点,然后也许可以使用字体大小来找出事物的位置。 - not2qubit
dwCursorPosition 是文本光标位置(不是鼠标光标)。 - ssbssa
2个回答

1

楼主的解决方案在(我的)Windows环境下失败了:

Python 3.8.6 (tags/v3.8.6:db45529, Sep 23 2020, 15:52:53) [MSC v.1927 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import readline
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'readline'
>>>

当你运行pip install readline时,会返回error: this module is not meant to work on Windows错误提示信息(已经截断)。

针对纯Windows环境(没有GNU readline接口),我找到了另一种解决方案。在programtalk.com中,我借鉴了期望的结构定义,详见第1至91行:

# from winbase.h
STDOUT = -11
STDERR = -12

from ctypes import (windll, byref, Structure, c_char, c_short, c_uint32,
  c_ushort, ArgumentError, WinError)

handles = {
    STDOUT: windll.kernel32.GetStdHandle(STDOUT),
    STDERR: windll.kernel32.GetStdHandle(STDERR),
}

SHORT = c_short
WORD = c_ushort
DWORD = c_uint32
TCHAR = c_char

class COORD(Structure):
    """struct in wincon.h"""
    _fields_ = [
        ('X', SHORT),
        ('Y', SHORT),
    ]

class  SMALL_RECT(Structure):
    """struct in wincon.h."""
    _fields_ = [
        ("Left", SHORT),
        ("Top", SHORT),
        ("Right", SHORT),
        ("Bottom", SHORT),
    ]

class CONSOLE_SCREEN_BUFFER_INFO(Structure):
    """struct in wincon.h."""
    _fields_ = [
        ("dwSize", COORD),
        ("dwCursorPosition", COORD),
        ("wAttributes", WORD),
        ("srWindow", SMALL_RECT),
        ("dwMaximumWindowSize", COORD),
    ]
    def __str__(self):
        """Get string representation of console screen buffer info."""
        return '(%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d)' % (
            self.dwSize.Y, self.dwSize.X
            , self.dwCursorPosition.Y, self.dwCursorPosition.X
            , self.wAttributes
            , self.srWindow.Top, self.srWindow.Left, self.srWindow.Bottom, self.srWindow.Right
            , self.dwMaximumWindowSize.Y, self.dwMaximumWindowSize.X
        )

def GetConsoleScreenBufferInfo(stream_id=STDOUT):
    """Get console screen buffer info object."""
    handle = handles[stream_id]
    csbi = CONSOLE_SCREEN_BUFFER_INFO()
    success = windll.kernel32.GetConsoleScreenBufferInfo(
        handle, byref(csbi))
    if not success:
        raise WinError()
    return csbi

### end of https://programtalk.com/vs4/python/14134/dosage/dosagelib/colorama.py/

clrcur = '\x1b[H'  # move cursor to the top left corner
clrscr = '\x1b[2J' # clear entire screen (? moving cursor ?)
# '\x1b[H\x1b[2J'
from ctypes import windll, wintypes, byref
def get_cursor_pos():
    cursor = wintypes.POINT()
    aux = windll.user32.GetCursorPos(byref(cursor))
    return (cursor.x, cursor.y)

mouse_pos = get_cursor_pos()
# print('mouse at {}'.format(mouse_pos))

def cpos():
    csbi = GetConsoleScreenBufferInfo()
    return '({},{})'.format(csbi.dwCursorPosition.X, csbi.dwCursorPosition.Y)
    
print('12345', end='', flush=True)
print(' {}'.format(cpos()), flush=True)

# an attempt to resolve discrepancy between buffer and screen size
# in the following code snippet:
import sys
if len(sys.argv) > 1 and len(sys.argv[1]) > 0:
    csbi = GetConsoleScreenBufferInfo()
    keybd_pos = (csbi.dwCursorPosition.X, csbi.dwCursorPosition.Y)
    print('\nkbd buffer at {}'.format(keybd_pos))
    import os
    screensize = os.get_terminal_size()
    keybd_poss = ( csbi.dwCursorPosition.X,  
                   min( csbi.dwSize.Y,
                        csbi.dwCursorPosition.Y,
                        csbi.dwMaximumWindowSize.Y,
                        screensize.lines))
    # screen line number is incorrectly computed if termial is scroll-forwarded
    print('kbd screen at {} (disputable? derived from the following data:)'
          .format(keybd_poss))
    print( 'csbi.dwSize   ', (csbi.dwSize.X, csbi.dwSize.Y))
    print( 'terminal_size ', (screensize.columns, screensize.lines))
    print( 'csbi.dwMaxSize', (csbi.dwMaximumWindowSize.X, csbi.dwMaximumWindowSize.Y))
    print( 'csbi.dwCurPos ', (csbi.dwCursorPosition.X, csbi.dwCursorPosition.Y))

输出: .\SO\70732748.py

12345 (5,526)

从第113行开始,有一个尝试解决缓冲区和屏幕大小差异的问题(不成功,如果终端是scroll-forwarded,则绝对屏幕行号计算不正确)。这种差异在Windows Terminal中不存在,其中始终buffer height == window height,所有这些计算都是不必要的…

示例.\SO\70732748.py x

12345 (5,529)

kbd buffer at (0, 530)
kbd screen at (0, 36) (disputable? derived from the following data:)
csbi.dwSize    (89, 1152)
terminal_size  (88, 36)
csbi.dwMaxSize (89, 37)
csbi.dwCurPos  (0, 530)

你不需要安装readline,因为它是内置的。你只需要导入它即可。 - not2qubit
此外,在 PowerShell(旧版和 core 版本)中,由于缓冲区大小与屏幕大小无关,因此不存在差异。然而,在 Windows 终端 中,现在略有不同并进行了匹配,因此 缓冲区高度 = 窗口高度 ,如此处所述。 - not2qubit
@not2qubit:回答已更新;我已经知道了cmdwt的不同行为;感谢iac提供的链接... - JosefZ
是的,这是一个 Py3.8 的问题。在那里,你需要像这样使用 pyreadlinefrom pyreadline import Readline; readline = Readline()。在 py3.10 中,它就可以正常工作了! - not2qubit
PS. 我从代码片段中删除了那个愚蠢的版权信息,因为它根本无效,因为该代码基本上只是 MS Windows API 的示例。 - not2qubit

1

问题是要定位各种结构定义。经过大量实验,我找到了以下可行的解决方案。

#!/usr/bin/env python -u
# -*- coding: UTF-8 -*-
#------------------------------------------------------------------------------
from ctypes import windll, wintypes, Structure, c_short, c_ushort, byref, c_ulong
from readline import console

#------------------------------------------------
# Win32 API 
#------------------------------------------------
SHORT   = c_short
WORD    = c_ushort
DWORD   = c_ulong

STD_OUTPUT_HANDLE   = DWORD(-11)    # $CONOUT

# These are already defined, so no need to redefine.
COORD = wintypes._COORD
SMALL_RECT = wintypes.SMALL_RECT
CONSOLE_SCREEN_BUFFER_INFO = console.CONSOLE_SCREEN_BUFFER_INFO

#------------------------------------------------
# Main
#------------------------------------------------
wk32 = windll.kernel32

hSo = wk32.GetStdHandle(STD_OUTPUT_HANDLE)
GetCSBI = wk32.GetConsoleScreenBufferInfo

def cpos():
    csbi = CONSOLE_SCREEN_BUFFER_INFO()
    GetCSBI(hSo, byref(csbi))
    xy = csbi.dwCursorPosition
    return '({},{})'.format(xy.X,xy.Y)

cls='\x1b[H'
print('\n'*61)
print(cls+'12345', end='', flush=True); print(' {}'.format(cpos()), flush=True)

# 12345 (5,503)

顺便提一下,这里有一个使用unpack结构的有趣方法。 - not2qubit

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接