四联光电智能照明论坛

标题: Linux终端调整输出色彩和光标位置的简单示例 [打印本页]

作者: 风火石    时间: 2016-12-27 15:30
标题: Linux终端调整输出色彩和光标位置的简单示例
相关的知识

实现的原理并不复杂,借助了Linux的 ANSI 颜色控制序列,只要在打印输出的过程中使用这些序列,就能做到彩色的字符、背景色输出,以及控制光标在打印时的位置。

简单的代码示例

以下代码都在树莓派的Raspbian系统中编译运行过。

#include <stdio.h>
#include <unistd.h>

int main(void)
{
    // str 字符串里包含了调整字体为红色以及光标隐藏的序列
    char str[] = "\033[31mplease wait...\033[?25l";
    char ch[3][5] = {"\\","-","/"};
    int  times = 20;

    fputs(str, stderr);
    while (times --)
    {
        fputs(ch[0], stderr);
        // 由于上面打印的字符串仅仅只占一个字符宽
        // 那么这里就只用将光标向左移动1格即可
        fputs("\033[1D", stderr);
        // 暂停 0.2 秒
        usleep(200000);
        // 调整好光标的位置后,直接打印内容,就能覆盖
        // 屏幕上之前的输出文本
        fputs(ch[1], stderr);
        fputs("\033[1D", stderr);
        usleep(200000);
        fputs(ch[2], stderr);
        fputs("\033[1D", stderr);
        usleep(200000);

    }
    // 这里建议打印一个换行,并且让光标否则程序运行结束之后,
    // 退回到shell,shell的提示符会紧跟着“please wait”字符串
    // 显示出来。
    // 此外,如果不让隐藏的光标显现,回到shell中后光标依然是
    // 隐藏状态,但此时直接按下回车键,键入空命令,在下一
    // 行命令中光标又会自动显现。
    fputs("\n\033[?25h", stderr);

    return 0;
}
上面代码的运行效果,是想在同一行行末的固定一个字符的位置,依次输出“\”“-”“/”并快速循环,给人以旋转的感觉。如果有读者在Kali Linux中启动“msfconsole”,则会看到程序启动中,在某一行提示信息的行末就使用了这个效果。

输出效果示意:

please wait...\
    ↓
please wait...-
    ↓
please wait.../
如果我们是用 fputc 函数打印单个字符,那么这光标位置调整仍然有效:

#include <stdio.h>
#include <unistd.h>

int main(void)
{
    char str[] = "\033[31mplease wait...\033[?25l";
    char ch[3] = {'\\', '-', '/'};
    int  times = 20;

    fputs(str, stderr);
    while (times --)
    {
        fputc(ch[0], stderr);
        fputs("\033[1D", stderr);
        usleep(200000);
        fputc(ch[1], stderr);
        fputs("\033[1D", stderr);
        usleep(200000);
        fputc(ch[2], stderr);
        fputs("\033[1D", stderr);
        usleep(200000);  
    }
    fputs("\n\033[?25h", stderr);

    return 0;
}
ANSI 色彩控制序列的简介

\033[0m 关闭所有属性
\033[1m 设置高亮度
\033[4m 下划线
\033[5m 闪烁
\033[7m 反显
\033[8m 消隐
\033[30m -- \33[37m 设置前景色
\033[40m -- \33[47m 设置背景色
字背景颜色:40----------49
40:黑
41:深红
42:绿
43:黄色
44:蓝色
45:紫色
46:深绿
47:白色
字颜色:30-----------39
30:黑
31:红
32:绿
33:黄
34:蓝色
35:紫色
36:深绿
37:白色

\033[nA 光标上移n行
\033[nB 光标下移n行
\033[nC 光标右移n行
\033[nD 光标左移n行
\033[y;xH设置光标位置
\033[2J 清屏
\033[K 清除从光标到行尾的内容
\033[s 保存光标位置
\033[u 恢复光标位置
\033[?25l 隐藏光标
\033[?25h 显示光标
其中,示例代码里面用到的“\033[31m”表示的是字体颜色设置为红色。详细的使用说明可用 man 命令进行查看:

$ man console_codes
注意,这些控制序列仅仅在Linux下有效,在windows的控制台程序中无效。而windows的控制台程序则可以通过“gotoxy”函数来调整光标的位置。

一些要注意的地方

为什么是用stderr而非stdout作为输出流?
我在阅读其他人介绍 ANSI 序列的相关博客中,看到很多作者都是直接使用 printf 函数打印输出的,我将他们的代码在树莓派上进行了编译运行,跟文中的描述并无二致,颜色、光标的移动、清屏等效果都复现出来了。

但是,需要注意,printf函数是将数据写到标准输出,而Linux遵循了一个标准I/O缓冲的一个惯例——标准错误不带缓冲,打开至终端设备的的流是行缓冲的,其他流是全缓冲的。显示器作为标准输出设备,这对 printf 写到标准输出流(stdout)的影响就是,stdout 流是带有行缓冲的。

进一步的,行缓冲的特点:

行缓冲。在这种情况下,当在输入和输出中遇到换行符时,标准I/O库执行I/O操作。这允许我们一次输出一个字符(用标准I/O函数fputc),但只有在写了一行之后才进行实际I/O操作。当流涉及一个终端时(如标准输入和标准输出),通常使用行缓冲。

对于行缓冲有两个限制。第一,因为标准I/O库用来收集每一行的缓冲区的长度是固定的,所以只要填满了缓冲区,那么即使还没有写一个换行符,也进行I/O操作。第二,任何时候只要通过标准I/O库要求从 (a)一个不带缓冲的流,或者 (b)一个行缓冲的流(它从内核请求需要数据)得到输入数据,那么就会冲洗所有行缓冲输出流。在 (b)中带了一个在括号中的说明,其理由是,所需的数据可能已在该缓冲区中,它并不要求一定从内核读取数据。很明显,从一个不带缓冲的流中输入(即 (a) 项)需要从内核获得数据。
……
当一个进程正常终止时(直接调用 exit 函数,或从 main 函数返回),则所有带未写缓冲数据的标准I/O流都被冲洗,所有打开的标准I/O流都被关闭。

————摘自《UNIX环境高级编程(第3版)》
如果我们写下如下代码来测试ANSI序列:

#include <stdio.h>
#include <unistd.h>
int main(void)
{
    char  str = "\033[31mplease wait...\033[?25l";
    char  ch[3] = {'\\', '-', '/'};

    printf("%s", str);
    printf("%c\033[1D", ch[0]);
    sleep(2);
    printf("%c\033[1D", ch[1]);
    sleep(2);
    printf("%c\033[1D", ch[2]);
    sleep(2);
    printf("\n");

    return 0;
}
编译运行之后,发现代码是先等待6秒,然后一下子就打印出了“please wait…/”(字体为红色)并退出。究其原因,这3条printf语句将数据送入 stdout 的行缓冲中,行缓冲区既没有写满,也没有写入换行符,导致 stdout 中的这些序列数据一直留在了行缓冲区,而并非我们的程序没有按顺序执行,直到进程结束,程序退出,标准I/O流都被冲洗,这才在屏幕上打印出来。

在其他网站上看到下面一段代码,注意“right 19”那里就没有换行符,结果导致它和“left 10”的结果是一起打印出来(因为“left 10”打印了换行符)而不是先后打印,可以在打印“right 19”之后再调用一个fflush(stdout)冲洗缓冲区,就能看到“right 19”被打印出来。

#include <stdio.h>
#include <unistd.h>

int main(void)
{
    printf("\033[31mThe color,%s!\033[1m\n", "haha");
    printf("\033[31mThe color,%s!\033[4m\n", "haha");
    printf("\033[31mThe color,%s!\033[5m\n", "haha");
    printf("\033[31mThe color,%s!\033[7m\n", "haha");
    printf("\033[31mThe color,%s!\033[8m\n", "haha");
    printf("\033[31mThe color,%s!\033[0m\n", "haha");
    printf("\033[47;31mThe color,%s!\033[0m\n", "haha");
    printf("\033[47mThe color,%s!\033[0m\n", "haha");
    sleep(2);
    printf("\033[44m%s!\033[15A\n", "up 15");
    sleep(2);
    printf("\033[43m%s!\033[9B\n", "down 9");
    sleep(2);
    // 注意这里是没有打印换行符的!
    printf("\033[42m%s!\033[19C", "right 19");
    printf("right 19");
    sleep(2);
    printf("\033[41m%s!\033[10D", "left 10");
    printf("left 10\n");
    sleep(2);
    printf("\033[45m%s!\033[50;20H\n", "move to 50, x 20");
    printf("y50 x20");
    sleep(2);
    printf("\033[46m%s!\033[?25l\n", "hide cursor");
    sleep(2);
    printf("\033[42m%s!\033[?25h\n", "show cursor");
    sleep(2);
    printf("\033[43m%s!\033[2J\n", "clear screen");
    return 0;
}
由于标准错误流 stderr 不带缓冲,我们把输出写到它里面就不受换行符触发或者填满缓冲的限制,只要往里面写入数据,就会立即打印到屏幕上。

色彩控制序列的作用范围
只要我们在打印序列中使用了这些色彩控制序列,如果不主动取消,那么它将会一直作用在程序后续的输出打印上,比如那个字体红色属性,在代码里嵌在“please wait…”字符串中,但同样会影响后面“\”“-”“/”的色彩。而且前面也谈到,那个光标隐藏属性即使在程序结束后都还能影响到shell的光标显示。

打印控制的窗口自适应性
简而言之,我们在GUI的非登陆式shell打印的时候,程序是以当前窗体的界面大小来自适应光标的移动范围,清屏的时候色彩覆盖的范围。它并非按照我们实际屏幕的物理尺寸来调整输出。




欢迎光临 四联光电智能照明论坛 (http://5xhome.com/) Powered by Discuz! X3.2