在更新另一个窗口时如何在一个窗口中同时读取字符串作为输入?这是为了在Python中使用curses。
这将很有用,例如用于使程序显示一些可能随时发生的输出(即使用户正在键入)。这样的想法是,由于程序的突然输出,用户可以继续输入而不会使当前输入的半完整字符串在中间被截断或剪切。
我尝试使用和修改以下问题的代码:Python/curses user input while updating screen
由于该代码已经存在于另一个问题中,因此我不再在此处再次发布。
但是,此代码仅读取一个字符。
我不能只调用getstr,因为它将阻塞并停止更新另一个窗口,直到用户输入完整的字符串为止。
如何解决似乎很明显:使用线程。但是,在前面提到的问题中已经对此提出警告-看来,curses在Python中的线程中不能很好地发挥作用。
解决该问题的另一种“明显”方法是实现自己的缓冲区,一次读取一个字符,进行基本编辑,并继续以非阻塞方式对此进行选择。
我希望有某种方式可以使用curses以非阻塞方式读取字符串,同时提供基本的行编辑(因此我不需要自己实现!),因为我可以想象这是一个相当不错的选择。典型的用例。
这里是尝试从前面的示例代码修改而来的线程。此代码的问题是显示混乱。在窗口调整大小之前,显示一直保持乱码,然后看起来不错。
该代码在一个窗口(一个线程)中读取用户输入,获取一个互斥锁,将该字符串提供给某个共享字符串,另一个线程获取该互斥锁,然后显示它。
此代码有什么问题?是什么导致输出乱码?一旦我删除了其他处理curses的线程(删除了getstr调用),它就不再乱码了。
#!/usr/bin/python
# -*- coding: iso-8859-1 -*-
import curses, curses.panel
import random
import time
import sys
import select
import threading
gui = None
class ui:
def __init__(self):
self.output_mutex = threading.Lock()
self.output_str = ""
self.stdscr = curses.initscr()
# curses.noecho()
curses.echo()
curses.cbreak()
curses.curs_set(0)
self.stdscr.keypad(1)
self.win1 = curses.newwin(10, 50, 0, 0)
self.win1.border(0)
self.pan1 = curses.panel.new_panel(self.win1)
self.win2 = curses.newwin(10, 50, 0, 0)
self.win2.border(0)
self.pan2 = curses.panel.new_panel(self.win2)
self.win3 = curses.newwin(10, 50, 12, 0)
self.win3.border(0)
self.pan3 = curses.panel.new_panel(self.win3)
self.win1.addstr(1, 1, "Window 1")
self.win2.addstr(1, 1, "Window 2")
# self.win3.addstr(1, 1, "Input: ")
# user_input = self.win3.getstr(8, 1, 20)
# self.win3.addstr(2, 1, "Output: %s" % user_input)
# self.pan1.hide()
def refresh(self):
curses.panel.update_panels()
self.win3.refresh()
self.win2.refresh()
self.win1.refresh()
def quit_ui(self):
curses.nocbreak()
self.stdscr.keypad(0)
curses.curs_set(1)
curses.echo()
curses.endwin()
print "UI quitted"
exit(0)
def worker_output(ui):
count = 0
running = 1
while True:
ui.win2.addstr(3, 1, str(count)+": "+str(int(round(random.random()*999))))
ui.win2.addstr(4, 1, str(running))
ui.output_mutex.acquire()
ui.win2.addstr(5, 1, ui.output_str)
ui.output_mutex.release()
ui.refresh()
time.sleep(0.1)
class feeder:
# Fake U.I feeder
def __init__(self):
self.running = False
self.ui = ui()
self.count = 0
def stop(self):
self.running = False
def run(self):
self.running = True
self.feed()
def feed(self):
threads = []
t = threading.Thread(target=worker_output, args=(self.ui,))
threads.append(t)
t.start()
user_input = ""
while True:
self.ui.win3.addstr(1, 1, "Input: ")
user_input = self.ui.win3.getstr(1, 8, 20)
self.ui.win3.addstr(2, 1, "Output: %s" % user_input)
# self.ui.refresh()
# self.ui.win3.clear()
self.ui.output_mutex.acquire()
self.ui.output_str = user_input
self.ui.output_mutex.release()
time.sleep(.2)
if __name__ == "__main__":
f = feeder()
f.run()
答案 0 :(得分:-1)
我尝试了一种与注释中有所不同的方法:创建一个基于线程的系统,该系统将锁锁定在各个点周围。
尽管它也暴露了curses库的很多烦恼,但它似乎或多或少地起着很小的作用,并且通常可能是错误的处理方式。尽管如此,我还是在这里展示它作为完成这项工作的一种方法的示例。
#!/usr/bin/python
# -*- coding: iso-8859-1 -*-
import collections
import curses, curses.ascii, curses.panel
import random
import time
import sys
import select
import threading
#gui = None
class LockedCurses(threading.Thread):
"""
This class essentially wraps curses operations so that they
can be used with threading. Noecho and cbreak are always in
force.
Usage: call start() to start the thing running. Then call
newwin, new_panel, mvaddstr, and other standard curses functions
as usual.
Call teardown() to end.
Note: it's very important that the user catch things like
keyboard interrupts and redirect them to make us shut down
cleanly. (This could be improved...)
"""
def __init__(self, debug=False):
super(LockedCurses, self).__init__()
self._lock = threading.Lock()
# ick!
self.panel = self
# generic cond var
self._cv = threading.Condition(self._lock)
# results-updated cond var
self._resultcv = threading.Condition(self._lock)
self._workqueue = collections.deque()
self._starting = False
self._running = False
self._do_quit = False
self._screen = None
self._ticket = 0
self._served = -1
self._result = {}
self._debug = debug
def start(self):
assert(not self._running)
assert(self._screen is None)
self._screen = curses.initscr()
with self._lock:
self._starting = True
super(LockedCurses, self).start()
while self._starting:
self._cv.wait()
self.debug('started!')
def run(self):
# This happens automatically inside the new thread; do not
# call it yourself!
self.debug('run called!')
assert(not self._running)
assert(self._screen is not None)
curses.savetty()
curses.noecho()
curses.cbreak()
self._running = True
self._starting = False
with self._lock:
self._cv.notifyAll()
while not self._do_quit:
while len(self._workqueue) == 0 and not self._do_quit:
self.debug('run: waiting for work')
self._cv.wait()
# we have work to do, or were asked to quit
self.debug('run: len(workq)={}'.format(len(self._workqueue)))
while len(self._workqueue):
ticket, func, args, kwargs = self._workqueue.popleft()
self.debug('run: call {}'.format(func))
self._result[ticket] = func(*args, **kwargs)
self._served = ticket
self.debug('run: served {}'.format(ticket))
self._resultcv.notifyAll()
# Quitting! NB: resettty should do all of this for us
# curses.nocbreak()
# curses.echo()
curses.resetty()
curses.endwin()
self._running = False
self._cv.notifyAll()
def teardown(self):
with self._lock:
if not self._running:
return
self._do_quit = True
while self._running:
self._cv.notifyAll()
self._cv.wait()
def debug(self, string):
if self._debug:
sys.stdout.write(string + '\r\n')
def _waitch(self):
"""
Wait for a character to be readable from sys.stdin.
Return True on success.
Unix-specific (ugh)
"""
while True:
with self._lock:
if not self._running:
return False
# Wait about 0.1 second for a result. Really, should spin
# off a thread to do this instead.
l = select.select([sys.stdin], [], [], 0.1)[0]
if len(l) > 0:
return True
# No result: go around again to recheck self._running.
def refresh(self):
s = self._screen
if s is not None:
self._passthrough('refresh', s.refresh)
def _passthrough(self, fname, func, *args, **kwargs):
self.debug('passthrough: fname={}'.format(fname))
with self._lock:
self.debug('got lock, fname={}'.format(fname))
if not self._running:
raise ValueError('called {}() while not running'.format(fname))
# Should we check for self._do_quit here? If so,
# what should we return?
ticket = self._ticket
self._ticket += 1
self._workqueue.append((ticket, func, args, kwargs))
self.debug('waiting for ticket {}, fname={}'.format(ticket, fname))
while self._served < ticket:
self._cv.notifyAll()
self._resultcv.wait()
return self._result.pop(ticket)
def newwin(self, *args, **kwargs):
w = self._passthrough('newwin', curses.newwin, *args, **kwargs)
return WinWrapper(self, w)
def new_panel(self, win, *args, **kwargs):
w = win._interior
p = self._passthrough('new_panel', curses.panel.new_panel, w,
*args, **kwargs)
return LockedWrapper(self, p)
class LockedWrapper(object):
"""
Wraps windows and panels and such. locker is the LockedCurses
that we need to use to pass calls through.
"""
def __init__(self, locker, interior_object):
self._locker = locker
self._interior = interior_object
def __getattr__(self, name):
i = self._interior
l = self._locker
a = getattr(i, name)
if callable(a):
l.debug('LockedWrapper: pass name={} as func={}'.format(name, a))
# return a function that uses passthrough
return lambda *args, **kwargs: l._passthrough(name, a,
*args, **kwargs)
# not callable, just return the attribute directly
return a
class WinWrapper(LockedWrapper):
def getch(self):
"""
Overrides basic getch() call so that it's specifically *not*
locked. This is a bit tricky.
"""
# (This should really test for nodelay mode too though.)
l = self._locker
ok = l._waitch()
if ok:
return l._passthrough('getch', self._interior.getch)
return curses.ERR
def getstr(self, y, x, maxlen):
self.move(y, x)
l = 0
s = ""
while True:
self.refresh()
c = self.getch()
if c in (curses.ERR, ord('\r'), ord('\n')):
break
if c == ord('\b'):
if len(s) > 0:
s = s[:-1]
x -= 1
self.addch(y, x, ' ')
self.move(y, x)
else:
if curses.ascii.isprint(c) and len(s) < maxlen:
c = chr(c)
s += c
self.addch(c)
x += 1
return s
class ui(object):
def __init__(self):
self.curses = LockedCurses()
self.curses.start()
#self.stdscr.keypad(1)
self.win1 = self.curses.newwin(10, 50, 0, 0)
self.win1.border(0)
self.pan1 = self.curses.panel.new_panel(self.win1)
self.win2 = self.curses.newwin(10, 50, 0, 0)
self.win2.border(0)
self.pan2 = self.curses.panel.new_panel(self.win2)
self.win3 = self.curses.newwin(10, 50, 12, 0)
self.win3.border(0)
self.pan3 = self.curses.panel.new_panel(self.win3)
self.win1.addstr(1, 1, "Window 1")
self.win2.addstr(1, 1, "Window 2")
self.win3.addstr(1, 1, "Input: ")
self.output_str = ""
self.stop_requested = False
def refresh(self):
#self.curses.panel.update_panels()
self.win3.refresh()
self.win2.refresh()
self.win1.refresh()
#self.curses.refresh()
def quit_ui(self):
self.curses.teardown()
print "UI quitted"
def worker_output(ui):
count = 0
running = 1
while not ui.stop_requested:
ui.win2.addstr(3, 1, str(count)+": "+str(int(round(random.random()*999))))
ui.win2.addstr(4, 1, str(running))
ui.win2.addstr(5, 1, ui.output_str)
ui.refresh()
time.sleep(0.1)
count += 1
class feeder:
# Fake U.I feeder
def __init__(self):
self.running = False
self.ui = ui()
self.count = 0
def stop(self):
self.running = False
def run(self):
self.running = True
try:
self.feed()
finally:
self.ui.quit_ui()
def feed(self):
t = threading.Thread(target=worker_output, args=(self.ui,))
t.start()
user_input = ""
while not user_input.startswith("q"):
self.ui.win3.addstr(1, 1, "Input: ")
user_input = self.ui.win3.getstr(1, 8, 20)
self.ui.win3.addstr(2, 1, "Output: %s" % user_input)
self.ui.refresh()
self.ui.win3.clear()
time.sleep(.2)
self.ui.stop_requested = True
t.join()
if __name__ == "__main__":
f = feeder()
f.run()