如何编写一个 ANSI C 控制台屏幕缓冲区?

7
我正在开发一个基于ASCII的游戏,但无论我去哪里看,人们都说要使用MSDN中的Console.Write()函数。如果你使用Windows,这很好用,但我不是。
因此,我试图编写一个C语言函数或一组函数,可以在两个屏幕缓冲区之间交替,并像man页面以及pico、vim和emacs一样将它们写入屏幕。
我的缓冲区已经运作良好,我找到了一个名为0verkill的Linux ASCII游戏,使用C语言和putchar()函数将每个字符放置在屏幕上,但我重现它的所有尝试都导致连续不断的文本流,而不是窗口大小的静态文本面板。我真的不想使用任何外部库,比如curses(因为那会降低可移植性),而且尽可能遵守ansi标准。
谢谢!
4个回答

5
我真的不想使用像curses这样的外部库(因为那会降低可移植性),但是类似curses和ncurses的库旨在使这种操作更具可移植性,因为...同时,至少对于C而言,在ANSI标准中没有规定你想要的内容。每个操作系统都以不同的方式实现此类行为,因此,如果您想以便携方式进行操作,则需要使用库。老实说,我讨厌不支持ncurses的系统。想象一下,如果没有它,您将无法使用多少程序。并且如果可能的话,我想尽量保持符合ansi标准。请勿删除html标签。

我同意你关于ncurses的评价,但欢迎来到OSX(有一个ncurses端口),但是像默认安装的emacs这样的程序如何在没有它的情况下运行? - Ryan Rohrer
OS X(至少是我的OS X)默认安装了ncurses。 - Chris Lutz
好吧,看来我最好深入研究ncurses的文档...谢谢你的帮助! - Ryan Rohrer

4
我认为你要找的是 ANSI 控制字符ESC[2J,它可以清除屏幕。在任何状态更改后,您都可以调用该字符来“刷新”控制台屏幕。

请参阅此页面了解其他控制字符的使用方法。使用这些字符,您可以在控制台上定义颜色和格式(间距、对齐、缩进等)。


谢谢,虽然我尝试了那个方法,但它会让终端窗口不断滚动,而且永远不会将旧的putchars放在新的上面,即使esc[2j也只是在我的控制台窗口大小留下一个空白。 - Ryan Rohrer
2
是的,我认为这就是你能做的全部了,它会滚动窗口与控制台相同数量的行,然后重新绘制所有内容,再次滚动。这是我所知道的这些东西工作的唯一方式,以给出静态窗口的外观,但我不认为你可以拥有实际的静态文本,就像在Windows控件中一样(但即使那也不是真正的静态,它只是不断地被重绘):毕竟你正在处理一个控制台 - 只是一个流。 - Dale
不正确。窗口和 x 都有一个清除控制台窗口的概念。它们还允许在控制台窗口中任意位置编写内容。 - Sam Axe

3

以下是一个示例头文件和源文件,演示将curses从应用程序中抽象出来的一种方式。这份代码已经放置多年了,我大约在15年前写的。请注意风险。

cursemu.h

/***************************************************************************
 *                                                                          
 *  DO NOT CHANGE ANYTHING BETWEEN THIS LINE AND THE NEXT LINE THAT HAS THE 
 *  WORDS "KLAATU BARRATA NIKTO" ON IT                                      
 *                                                                          
 ***************************************************************************/
#ifndef X__CURSEMU__H                                                        
#define X__CURSEMU__H                                                        

#include <stdio.h>

#ifdef  linux
#define _POSIX_VERSION
#endif                

#ifndef _POSIX_VERSION
#include <sgtty.h>    
#define  USE_OLD_TTY  
#include <sys/ioctl.h>
#undef  USE_OLD_TTY   

#ifndef  CBREAK
#define CBREAK RAW
#endif            

#if !defined(sun) && !defined(sequent) && !defined(hpux) && \
    !defined(_AIX) && !defined(aix)                          
#include <strings.h>                                         
#define strchr index                                         
#else                                                        
#include <string.h>                                          
#endif                                                       
#else                                                        
#include <string.h>                                          
#include <termios.h>                                         
#endif                                                       

#include <errno.h>
#include <sys/types.h>
#include <pwd.h>      
#include <sys/time.h> 
#include <sys/file.h> 
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>     
#include <signal.h>    

/* Keep looking ... */

int _tty_ch;

#ifdef  _POSIX_VERSION
struct termios _tty;  
tcflag_t _res_iflg,   
    _res_lflg;        

#define cbreak()(_tty.c_lflag&=~ICANON, \
  tcsetattr( _tty_ch, TCSANOW, &_tty ))  

#define noecho()(_tty.c_lflag &= ~(ECHO|ICRNL), \
  tcsetattr( _tty_ch, TCSADRAIN, &_tty ))        

#define savetty()((void) tcgetattr(_tty_ch, &_tty), \
  _res_iflg = _tty.c_iflag, _res_lflg = _tty.c_lflag )

#define resetty()(_tty.c_iflag = _res_iflg, _tty.c_lflag = _res_lflg,\
  (void) tcsetattr(_tty_ch, TCSADRAIN, &_tty))                        

#define erasechar()(_tty.c_cc[VERASE])
#else                                 
struct sgttyb _tty;                   
int _res_flg;                         

#define cbreak()(_tty.sg_flags|=CBREAK, ioctl(_tty_ch, TIOCSETP, &_tty))

#define noecho()(_tty.sg_flags &= ~(ECHO|CRMOD), \
  ioctl( _tty_ch, TIOCSETP, &_tty ))              

#define savetty()((void) ioctl(_tty_ch, TIOCGETP, &_tty), \
  _res_flg = _tty.sg_flags )                               

#define resetty()(_tty.sg_flags = _res_flg, \
  (void) ioctl(_tty_ch, TIOCSETP, &_tty))    
#define erasechar()(_tty.sg_erase)           
#endif                                       

/* KLAATU BARRATA NIKTO */

#define  TERMCAP_LENGTH 1024

struct CtrlSeq
{             
  char termcap[ TERMCAP_LENGTH ];

  int numRows, numCols;

  /*  These pointers are indexes into the termcap buffer, and represent the
   *  control sequences neccessary to send to the terminal window to perform
   *  their appropriately named feature.
   */
  char *highlight,
       *endMode,            /* End highlight mode, and other modes. */
       *clearScr,
       *clearEol,
       *scrollRegion,
       *moveCursor,
       *deleteRow,
       *insertRow,
       *saveCursor,         /* Save the current cursor position */
       *restoreCursor;      /* Restore the saved cursor position */

  int dumbTerm,             /* 1 if the terminal is a dumb terminal */
      flush;                /* 1 if the emulation should flush stdout */
};

struct CtrlSeq ctrlSeq;

#define DEFAULT_COLS    80
#define DEFAULT_ROWS    24

void ce_flush( int toSet );
void ce_puts( char *str );
void ce_gotoRowCol( int row, int col );

void ce_writeStrRowCol( char *theText, int row, int col );
void ce_writeStr( char *theText );
void ce_writeCharRowCol( char theChar, int row, int col );
void ce_writeChar( char theChar );

void ce_clearScreen( void );
void ce_clearEol( void );

void ce_highlight( int on );
void ce_scrollRegion( int row1, int row2 );
void ce_deleteRow( int row );
void ce_insertRow( int row );
void ce_saveCursor( void );
void ce_restoreCursor( void );

int ce_getRows( void );
int ce_getCols( void );

#endif

cursemu.c

#include "cursemu.h"

int putchar_x( int c )
{                     
  return( putchar( c ) );
}                        

/*  Returns 0 on success, -1 on error
 */                                  
int ce_startCurses( void )           
{                                    
  char *ptr,                         
       tempBuff[ 1024 ];             
  int  result = 0;                   

  if( (ptr = (char *)getenv( "TERM" )) != NULL )
    result = tgetent( tempBuff, ptr );          
  else                                          
    result = tgetent( tempBuff, "vt100" );      

  if( result < 1 )
  {               
    perror( "FATAL Error: No termcap entry found (even tried vt100)!\n" );
    return( -1 );                                                         
  }                                                                       

  ptr = ctrlSeq.termcap;

  if( (ctrlSeq.numCols = tgetnum( "co" )) == -1 )
    ctrlSeq.numCols = DEFAULT_COLS;              
  if( (ctrlSeq.numRows = tgetnum( "li" )) == -1 )
    ctrlSeq.numRows = DEFAULT_ROWS;              

  if( (ctrlSeq.moveCursor = (char *)tgetstr( "cm", &ptr )) == NULL )
    ctrlSeq.moveCursor = (char *)tgetstr( "cl", &ptr );             
  if( (ctrlSeq.highlight = (char *)tgetstr( "mr", &ptr )) == NULL ) 
    ctrlSeq.highlight = (char *)tgetstr( "md", &ptr );              

  ctrlSeq.endMode       = (char *)tgetstr( "me", &ptr );
  ctrlSeq.clearEol      = (char *)tgetstr( "ce", &ptr );
  ctrlSeq.clearScr      = (char *)tgetstr( "cl", &ptr );
  ctrlSeq.scrollRegion  = (char *)tgetstr( "cs", &ptr );
  ctrlSeq.deleteRow     = (char *)tgetstr( "dl", &ptr );
  ctrlSeq.insertRow     = (char *)tgetstr( "al", &ptr );
  ctrlSeq.saveCursor    = (char *)tgetstr( "sc", &ptr );
  ctrlSeq.restoreCursor = (char *)tgetstr( "rc", &ptr );

  ctrlSeq.dumbTerm = (ctrlSeq.moveCursor == NULL) ||
                     (ctrlSeq.scrollRegion == NULL) ||
                     (ctrlSeq.saveCursor == NULL) ||  
                     (ctrlSeq.restoreCursor == NULL) ||
                     (ctrlSeq.clearEol == NULL);       

  ctrlSeq.flush = 1;

  if( !ctrlSeq.dumbTerm )
  {                      
    if( (_tty_ch = open( "/dev/tty", O_RDWR, 0 ) ) == -1 )
      _tty_ch = 0;                                        

    savetty();
    cbreak(); 
    noecho(); 
    return( 0 );
  }             

  return( -1 );
}              

int ce_endCurses( void )
{                       
  ce_scrollRegion( -1, -1 );
  ce_gotoRowCol( ce_getRows() - 1, 0 );
  resetty();                           
}                                      

void ce_flush( int toSet )
{                         
  ctrlSeq.flush = toSet;  

  if( toSet == 1 )
    fflush( stdout );
}                    

void ce_puts( char *str )
{                        
  tputs( str, 0, putchar_x );

  if( ctrlSeq.flush )
    fflush( stdout );
}                    

void ce_gotoRowCol( int row, int col )
{                                     
  if( row > ctrlSeq.numRows )         
    row = ctrlSeq.numRows;            
  if( col > ctrlSeq.numCols )         
    col = ctrlSeq.numCols;            

  ce_puts( (char *)tgoto( ctrlSeq.moveCursor, col, row ) );
}                                                          

void ce_writeStrRowCol( char *theText, int row, int col )
{                                                        
  ce_flush( 0 );                                         
  ce_gotoRowCol( row, col );                             
  ce_writeStr( theText );                                
  ce_flush( 1 );                                         
}                                                        

void ce_writeStr( char *theText )
{                                
  ce_flush( 0 );                 
  printf( "%s", theText );       
  ce_flush( 1 );                 
}                                

void ce_writeCharRowCol( char theChar, int row, int col )
{                                                        
  ce_flush( 0 );                                         
  ce_gotoRowCol( row, col );                             
  ce_writeChar( theChar );                               
  ce_flush( 1 );                                         
}                                                        

void ce_writeChar( char theChar )
{                                
  ce_flush( 0 );                 
  printf( "%c", theChar );       
  ce_flush( 1 );                 
}                                

void ce_clearScreen( void )
{                          
  ce_puts( ctrlSeq.clearScr );
}

void ce_clearEol( void )
{
  ce_puts( ctrlSeq.clearEol );
}

void ce_highlight( int on )
{
  if( on == 0 )
    ce_puts( ctrlSeq.endMode );
  else
    ce_puts( ctrlSeq.highlight );
}

void ce_scrollRegion( int row1, int row2 )
{
  ce_puts( (char *)tgoto( ctrlSeq.scrollRegion, row1, row2 ) );
}

void ce_deleteRow( int row )
{
  ce_gotoRowCol( row, 0 );
  ce_puts( ctrlSeq.deleteRow );
}

void ce_insertRow( int row )
{
  ce_gotoRowCol( row, 0 );
  ce_puts( ctrlSeq.insertRow );
}

void ce_saveCursor( void )
{
  ce_puts( ctrlSeq.saveCursor );
}

void ce_restoreCursor( void )
{
  ce_puts( ctrlSeq.restoreCursor );
}

int ce_getRows( void )
{
  return( ctrlSeq.numRows );
}

int ce_getCols( void )
{
  return( ctrlSeq.numCols );
}

编译

需要:

gcc -o cursemu.o -lcurses -ltermcap

3

有一个ANSI标准X3.64,也是ISO/IEC 6429,描述了DEC VT100终端。该标准描述了一些转义序列用于颜色和光标定位,符合标准的终端仿真器将识别这些序列,基本上所有的X终端都支持,但在Windows上不一定(可能需要用户加载ansi.sys)。正是这种最后的丑陋不一致性说明您应该使用ncurses来抽象掉这样的细节。


1
为什么你想要编写一个有错误、不完整且难以移植的版本,而不是使用经过岁月考验、广泛移植并且经过充分测试的老牌库呢?对于最后一句话给个赞! - Chris Lutz

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