Python不断更新单独的窗口时会诅咒字符串输入吗?

时间:2018-12-31 00:57:35

标签: python select command-line curses

在更新另一个窗口时如何在一个窗口中同时读取字符串作为输入?这是为了在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()

1 个答案:

答案 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()