How to scroll text in Python/Curses subwindow?

后端 未结 5 2012
陌清茗
陌清茗 2021-01-30 23:01

In my Python script which uses Curses, I have a subwin to which some text is assigned. Because the text length may be longer than the window size, the text should be scrollable.

相关标签:
5条回答
  • 2021-01-30 23:45

    Right, I was a bit confused on how to utilize pads (in order to scroll text), and still couldn't figure it out after reading this post; especially since I wanted to use it in a context of the content being an existing "array of lines". So I prepared a small example, that shows similarities (and differences) between newpad and subpad:

    #!/usr/bin/env python2.7
    import curses
    
    # content - array of lines (list)
    mylines = ["Line {0} ".format(id)*3 for id in range(1,11)]
    
    import pprint
    pprint.pprint(mylines)
    
    def main(stdscr):
      hlines = begin_y = begin_x = 5 ; wcols = 10
      # calculate total content size
      padhlines = len(mylines)
      padwcols = 0
      for line in mylines:
        if len(line) > padwcols: padwcols = len(line)
      padhlines += 2 ; padwcols += 2 # allow border
      stdscr.addstr("padhlines "+str(padhlines)+" padwcols "+str(padwcols)+"; ")
      # both newpad and subpad are <class '_curses.curses window'>:
      mypadn = curses.newpad(padhlines, padwcols)
      mypads = stdscr.subpad(padhlines, padwcols, begin_y, begin_x+padwcols+4)
      stdscr.addstr(str(type(mypadn))+" "+str(type(mypads)) + "\n")
      mypadn.scrollok(1)
      mypadn.idlok(1)
      mypads.scrollok(1)
      mypads.idlok(1)
      mypadn.border(0) # first ...
      mypads.border(0) # ... border
      for line in mylines:
        mypadn.addstr(padhlines-1,1, line)
        mypadn.scroll(1)
        mypads.addstr(padhlines-1,1, line)
        mypads.scroll(1)
      mypadn.border(0) # second ...
      mypads.border(0) # ... border
      # refresh parent first, to render the texts on top
      #~ stdscr.refresh()
      # refresh the pads next
      mypadn.refresh(0,0, begin_y,begin_x, begin_y+hlines, begin_x+padwcols)
      mypads.refresh()
      mypads.touchwin()
      mypadn.touchwin()
      stdscr.touchwin() # no real effect here
      #stdscr.refresh() # not here! overwrites newpad!
      mypadn.getch()
      # even THIS command erases newpad!
      # (unless stdscr.refresh() previously):
      stdscr.getch()
    
    curses.wrapper(main)
    

    When you run this, at first you will get something like (newpad left, subpad right):

     ┌────────────────────────┐    ┌────────────────────────┐
     │Line 1 Line 1 Line 1 ───│    │Line 1 Line 1 Line 1 ───│
     │Line 2 Line 2 Line 2    │    │Line 2 Line 2 Line 2    │
     │Line 3 Line 3 Line 3    │    │Line 3 Line 3 Line 3    │
     │Line 4 Line 4 Line 4    │    │Line 4 Line 4 Line 4    │
     │Line 5 Line 5 Line 5    │    │Line 5 Line 5 Line 5    │
                                   │Line 6 Line 6 Line 6    │
                                   │Line 7 Line 7 Line 7    │
                                   │Line 8 Line 8 Line 8    │
                                   │Line 9 Line 9 Line 9    │
                                   │Line 10 Line 10 Line 10 │
                                   └────────────────────────┘
    

    Some notes:

    • Both newpad and subpad should have their width/height sized to the content (num lines/max line width of the array of lines) + eventual border space
    • In both cases, you could allow extra lines with scrollok() - but not extra width
    • In both cases, you basically "push" a line at the bottom of the pad; and then scroll() up to make room for the next
    • The special refresh method that newpad has, then allows for just a region of this "whole content" to be shown on screen; subpad more-less has to be shown in the size it was instantiated in
    • If you draw the borders of the pads before adding content strings - then the borders will scroll up too (that is the ─── piece shown at the ...Line 1 ───│ part).

    Useful links:

    • (n)curses pad in python not working
    • http://threeseas.net/vic/vic/terminal.py
    • How do I delete a curse window in python and restore background window?
    0 讨论(0)
  • 2021-01-30 23:46

    I wanted to use a scrolling pad to display content of some large text files but this didn't work well because texts can have line breaks and it was pretty hard to figure out how many characters to display at a time to fit the good number of columns and rows.

    So I decided to first split my text files in lines of exactly COLUMNS characters, padding with spaces when lines were too short. Then scrolling the text become more easy.

    Here is a sample code to display any text file:

    #!/usr/bin/python
    # -*- coding: utf-8 -*-
    
    import curses
    import locale
    import sys
    
    def main(filename, filecontent, encoding="utf-8"):
        try:
            stdscr = curses.initscr()
            curses.noecho()
            curses.cbreak()
            curses.curs_set(0)
            stdscr.keypad(1)
            rows, columns = stdscr.getmaxyx()
            stdscr.border()
            bottom_menu = u"(↓) Next line | (↑) Previous line | (→) Next page | (←) Previous page | (q) Quit".encode(encoding).center(columns - 4)
            stdscr.addstr(rows - 1, 2, bottom_menu, curses.A_REVERSE)
            out = stdscr.subwin(rows - 2, columns - 2, 1, 1)
            out_rows, out_columns = out.getmaxyx()
            out_rows -= 1
            lines = map(lambda x: x + " " * (out_columns - len(x)), reduce(lambda x, y: x + y, [[x[i:i+out_columns] for i in xrange(0, len(x), out_columns)] for x in filecontent.expandtabs(4).splitlines()]))
            stdscr.refresh()
            line = 0
            while 1:
                top_menu = (u"Lines %d to %d of %d of %s" % (line + 1, min(len(lines), line + out_rows), len(lines), filename)).encode(encoding).center(columns - 4)
                stdscr.addstr(0, 2, top_menu, curses.A_REVERSE)
                out.addstr(0, 0, "".join(lines[line:line+out_rows]))
                stdscr.refresh()
                out.refresh()
                c = stdscr.getch()
                if c == ord("q"):
                    break
                elif c == curses.KEY_DOWN:
                    if len(lines) - line > out_rows:
                        line += 1
                elif c == curses.KEY_UP:
                    if line > 0:
                        line -= 1
                elif c == curses.KEY_RIGHT:
                    if len(lines) - line >= 2 * out_rows:
                        line += out_rows
                elif c == curses.KEY_LEFT:
                    if line >= out_rows:
                        line -= out_rows
        finally:
            curses.nocbreak(); stdscr.keypad(0); curses.echo(); curses.curs_set(1)
            curses.endwin()
    
    if __name__ == '__main__':
        locale.setlocale(locale.LC_ALL, '')
        encoding = locale.getpreferredencoding()
        try:
            filename = sys.argv[1]
        except:
            print "Usage: python %s FILENAME" % __file__
        else:
            try:
                with open(filename) as f:
                    filecontent = f.read()
            except:
                print "Unable to open file %s" % filename
            else:
                main(filename, filecontent, encoding)
    

    The main trick is the line:

    lines = map(lambda x: x + " " * (out_columns - len(x)), reduce(lambda x, y: x + y, [[x[i:i+out_columns] for i in xrange(0, len(x), out_columns)] for x in filecontent.expandtabs(4).splitlines()]))
    

    First, tabulations in the text are converted to spaces, then I used splitlines() method to convert my text in array of lines. But some lines may be longer than our COLUMNS number, so I splitted each line in chunk of COLUMNS characters and then used reduce to transform the resulting list in a list of lines. Finally, I used map to pad each line with trailing spaces so that its length is exactly COLUMNS characters.

    Hope this helps.

    0 讨论(0)
  • 2021-01-30 23:59

    Set the window.scrollok(True).

    Documentation

    0 讨论(0)
  • 2021-01-31 00:04

    OK with window.scroll it was too complicated to move the content of the window. Instead, curses.newpad did it for me.

    Create a pad:

    mypad = curses.newpad(40,60)
    mypad_pos = 0
    mypad.refresh(mypad_pos, 0, 5, 5, 10, 60)
    

    Then you can scroll by increasing/decreasing mypad_pos depending on the input from window.getch() in cmd:

    if  cmd == curses.KEY_DOWN:
        mypad_pos += 1
        mypad.refresh(mypad_pos, 0, 5, 5, 10, 60)
    elif cmd == curses.KEY_UP:
        mypad_pos -= 1
        mypad.refresh(mypad_pos, 0, 5, 5, 10, 60)
    
    0 讨论(0)
  • 2021-01-31 00:07

    This is the answer of this question: How to make a scrolling menu in python-curses

    This code allows you to create a little scrolling menu in a box from a list of strings.
    You can also use this code getting the list of strings from a sqlite query or from a csv file.
    To edit the max number of rows of the menu you just have to edit max_row.
    If you press enter the program will print the selected string value and its position.

    from __future__ import division  #You don't need this in Python3
    import curses
    from math import *
    
    
    
    screen = curses.initscr()
    curses.noecho()
    curses.cbreak()
    curses.start_color()
    screen.keypad( 1 )
    curses.init_pair(1,curses.COLOR_BLACK, curses.COLOR_CYAN)
    highlightText = curses.color_pair( 1 )
    normalText = curses.A_NORMAL
    screen.border( 0 )
    curses.curs_set( 0 )
    max_row = 10 #max number of rows
    box = curses.newwin( max_row + 2, 64, 1, 1 )
    box.box()
    
    
    strings = [ "a", "b", "c", "d", "e", "f", "g", "h", "i", "l", "m", "n" ] #list of strings
    row_num = len( strings )
    
    pages = int( ceil( row_num / max_row ) )
    position = 1
    page = 1
    for i in range( 1, max_row + 1 ):
        if row_num == 0:
            box.addstr( 1, 1, "There aren't strings", highlightText )
        else:
            if (i == position):
                box.addstr( i, 2, str( i ) + " - " + strings[ i - 1 ], highlightText )
            else:
                box.addstr( i, 2, str( i ) + " - " + strings[ i - 1 ], normalText )
            if i == row_num:
                break
    
    screen.refresh()
    box.refresh()
    
    x = screen.getch()
    while x != 27:
        if x == curses.KEY_DOWN:
            if page == 1:
                if position < i:
                    position = position + 1
                else:
                    if pages > 1:
                        page = page + 1
                        position = 1 + ( max_row * ( page - 1 ) )
            elif page == pages:
                if position < row_num:
                    position = position + 1
            else:
                if position < max_row + ( max_row * ( page - 1 ) ):
                    position = position + 1
                else:
                    page = page + 1
                    position = 1 + ( max_row * ( page - 1 ) )
        if x == curses.KEY_UP:
            if page == 1:
                if position > 1:
                    position = position - 1
            else:
                if position > ( 1 + ( max_row * ( page - 1 ) ) ):
                    position = position - 1
                else:
                    page = page - 1
                    position = max_row + ( max_row * ( page - 1 ) )
        if x == curses.KEY_LEFT:
            if page > 1:
                page = page - 1
                position = 1 + ( max_row * ( page - 1 ) )
    
        if x == curses.KEY_RIGHT:
            if page < pages:
                page = page + 1
                position = ( 1 + ( max_row * ( page - 1 ) ) )
        if x == ord( "\n" ) and row_num != 0:
            screen.erase()
            screen.border( 0 )
            screen.addstr( 14, 3, "YOU HAVE PRESSED '" + strings[ position - 1 ] + "' ON POSITION " + str( position ) )
    
        box.erase()
        screen.border( 0 )
        box.border( 0 )
    
        for i in range( 1 + ( max_row * ( page - 1 ) ), max_row + 1 + ( max_row * ( page - 1 ) ) ):
            if row_num == 0:
                box.addstr( 1, 1, "There aren't strings",  highlightText )
            else:
                if ( i + ( max_row * ( page - 1 ) ) == position + ( max_row * ( page - 1 ) ) ):
                    box.addstr( i - ( max_row * ( page - 1 ) ), 2, str( i ) + " - " + strings[ i - 1 ], highlightText )
                else:
                    box.addstr( i - ( max_row * ( page - 1 ) ), 2, str( i ) + " - " + strings[ i - 1 ], normalText )
                if i == row_num:
                    break
    
    
    
        screen.refresh()
        box.refresh()
        x = screen.getch()
    
    curses.endwin()
    exit()
    
    0 讨论(0)
提交回复
热议问题