如何将彩色 ASCII 字符直接写入控制台的缓冲区?

How can I write coloured ASCII characters directly to the console's buffer?

提问人:Matt Moissat 提问时间:11/17/2023 更新时间:11/17/2023 访问量:94

问:

我是一个对C++语言非常感兴趣的学生,我目前正在做一个小项目,涉及在Windows终端上打印大量彩色ASCII字符,这是旧版本的优化版本。我的项目包括通过将视频中的帧转换为 ASCII 图像并按顺序打印它们来创建小型“ASCII 视频”(请参阅image视频链接)。我目前正在使用 ANSI 转义序列,例如手动设置每个 ASCII 字符所需的颜色,如下所示:cmd.exe\033[38;2;⟨r⟩;⟨g⟩;⟨b⟩m

string char_ramp("$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\" ^ `'. ");
int brightness(some_number);

(...)

current_frame += string{} + "\033[38;2;" + to_string(r) + ";" + to_string(g) + ";" + to_string(b) + "m" + char_ramp[(int)(greyscale_index / 255 * (char_ramp.size() - 1)) / brightness] + "\033[0m";

基本上,我通过在灰度字符渐带上添加与当前像素亮度级别相对应的 ASCII 字符来逐行构建帧,然后使用 ANSI 转义序列对其进行着色。

但是这样做似乎对终端的要求很高,因为当我打印彩色 ASCII 帧时,我会出现掉帧。例如,如果原始视频为 30 fps,则彩色 ASCII 视频将下降到大约 15 fps。

我尝试去除颜色,它似乎工作正常,没有掉帧。

一些朋友建议我应该直接设置缓冲区数据,而不是做打印行的所有额外逻辑。但是,我不知道如何在不改变构造 ASCII 帧的方式的情况下做到这一点,因为我确实需要以特定方式存储它们,或者至少以允许我控制输出速率的方式存储它们。我听说该函数允许我做一些事情,但我不明白如何在我的代码中实现它。WriteConsoleOutputCharacter

我在这里并不真正关心内存效率,所以难道没有办法将帧数据设置到缓冲区中,然后循环访问我想要显示的帧吗?

如果这只是一个错误的调用,那么在终端上显示这些 ascii 帧的有效方法是什么?

C++ 性能 颜色 IO 缓冲区

评论

2赞 Ted Lyngmo 11/17/2023
哦,Windows 终端慢。确保您的算法能够以 30 FPS 的速度进行转换(有一定的余量),然后找到其他方法来显示结果。不要使用 Windows 终端。制作一个位图并在其上绘制。除了使用终端之外的任何东西。
1赞 n. m. could be an AI 11/17/2023
您可以尝试使用 WINAPI 控制台函数,例如 .WriteConsoleOutput
1赞 Matt Moissat 11/17/2023
@TedLyngmo感谢您的建议,我实际上意识到打印一帧所需的时间并不是一个恒定的,有时是 0.01 秒,而我能找到的最坏情况是 0.1 秒多一点,所以它喜欢以 100 fps 的速度打印,然后突然以 10 fps 的速度打印,也许这就是原因。''' 0.008 0.015 0.011 0.012 0.09 0.101 0.008 0.106 0.008 0.01 0.095 0.108 0.011 0.01 0.009 0.01 0.01 0.01 0.102 0.01 0.013 0.09 0.1 0.011 ''' 所以肯定是打印部分导致了问题,但我不知道该如何看待这些数字。它们几乎是随机变化的。
1赞 Matt Moissat 11/17/2023
@TedLyngmo我以后一定会尝试另一种选择。谢谢你的建议。
1赞 Jerry Coffin 11/17/2023
@TedLyngmo:当有人关心速度时,很难反对基准测试作为良好的第一步,我对此没有任何论据。

答:

2赞 Jerry Coffin 11/17/2023 #1

前段时间,我编写了一些代码,将流式输出直接输出到 Windows 控制台,并具有颜色支持。

// WinBuf.hpp:

#pragma once
#include <ios>
#include <ostream>
#include <windows.h>

//! A C++ streambuf that writes directly to a Windows console
class WinBuf : public std::streambuf
{
    HANDLE h;

public:
    //! Create a WinBuf from an Windows handle
    //! @param h handle to a Windows console
    WinBuf(HANDLE h) : h(h) {}
    WinBuf(WinBuf const &) = delete;
    WinBuf &operator=(WinBuf const &) = delete;

    //! Return the handle to which this buffer is attached
    HANDLE handle() const { return h; }

protected:
    virtual int_type overflow(int_type c) override
    {
        if (c != EOF)
        {
            DWORD written;
            WriteConsole(h, &c, 1, &written, nullptr);
        }
        return c;
    }

    virtual std::streamsize xsputn(char_type const *s, std::streamsize count) override
    {
        DWORD written;
        WriteConsole(h, s, DWORD(count), &written, nullptr);
        return written;
    }
};

//! A C++ ostream that  writes via the preceding WinBuf
class WinStream : public virtual std::ostream
{
    WinBuf buf;

public:
    //! Create stream for a Windows console, defaulting to standard output
    WinStream(HANDLE dest = GetStdHandle(STD_OUTPUT_HANDLE))
        : buf(dest), std::ostream(&buf)
    {
    }

    //! return a pointer to the underlying WinBuf
    WinBuf *rdbuf() { return &buf; }
};

//! Provide the ability to set attributes (text colors)
class SetAttr
{
    WORD attr;

public:
    //! Save user's attribute for when this SetAttr object is written out
    SetAttr(WORD attr) : attr(attr) {}

    //! Support writing the SetAttr object to a WinStream
    //! @param w a WinStream object to write to
    //! @param c An attribute to set
    friend WinStream &operator<<(WinStream &w, SetAttr const &c)
    {
        WinBuf *buf = w.rdbuf();
        auto h = buf->handle();
        SetConsoleTextAttribute(h, c.attr);
        return w;
    }

    //! support combining attributes
    //! @param r the attribute to combine with this one
    SetAttr operator|(SetAttr const &r)
    {
        return SetAttr(attr | r.attr);
    }
};

//! Support setting the position for succeeding output
class gotoxy
{
    COORD coord;

public:
    //! Save position for when object is written to stream
    gotoxy(SHORT x, SHORT y) : coord{ .X = x, .Y = y} {}

    //! support writing gotoxy object to stream
    friend WinStream &operator<<(WinStream &w, gotoxy const &pos)
    {
        WinBuf *buf = w.rdbuf();
        auto h = buf->handle();
        SetConsoleCursorPosition(h, pos.coord);
        return w;
    }
};

//! Clear the "screen"
class cls
{
    char ch;

public:
    //! Create screen clearing object
    //! @param ch character to use to fill screen
    cls(char ch = ' ') : ch(ch) {}

    //! Support writing to a stream
    //! @param os the WinStream to write to
    //! @param c the cls object to write
    friend WinStream &operator<<(WinStream &os, cls const &c)
    {
        COORD tl = {0, 0};
        CONSOLE_SCREEN_BUFFER_INFO s;
        WinBuf *w = os.rdbuf();
        HANDLE console = w->handle();

        GetConsoleScreenBufferInfo(console, &s);
        DWORD written, cells = s.dwSize.X * s.dwSize.Y;
        FillConsoleOutputCharacter(console, c.ch, cells, tl, &written);
        FillConsoleOutputAttribute(console, s.wAttributes, cells, tl, &written);
        SetConsoleCursorPosition(console, tl);
        return os;
    }
};

//! Provide some convenience instances of the SetAttr object
//! to (marginally) ease setting colors.
extern SetAttr red;
extern SetAttr green;
extern SetAttr blue;
extern SetAttr intense;

extern SetAttr red_back;
extern SetAttr blue_back;
extern SetAttr green_back;
extern SetAttr intense_back;

属性在匹配的 .cpp 文件中定义:

// Winbuf.cpp:
#include "WinBuf.hpp"

SetAttr red{FOREGROUND_RED};
SetAttr green{FOREGROUND_GREEN};
SetAttr blue{FOREGROUND_BLUE};
SetAttr intense{FOREGROUND_INTENSITY};

SetAttr red_back{BACKGROUND_RED};
SetAttr green_back{BACKGROUND_GREEN};
SetAttr blue_back{BACKGROUND_BLUE};
SetAttr intense_back{BACKGROUND_INTENSITY};

下面是一个快速演示/测试程序,以展示它的使用方式:

// test_Winbuf.cpp
#include "winbuf.hpp"

int main()
{
    WinStream w;

    // the colors OR together, so red | green | blue gives white:
    auto color = red | green | blue | blue_back;

    w << color << cls() << gotoxy(10, 4) << "This is a string\n";
    for (int i = 0; i < 10; i++)
        w << "Line: " << i << "\n";

    w << (green | blue_back);

    for (int i = 0; i < 10; i++)
        w << "Line: " << i + 10 << "\n";

    w << gotoxy(20, 10) << "Stuck in the middle with you!\n";

    w << gotoxy(0, 30) << color << "The end\n";
}

根据我的经验,这比使用 ANSI 转义序列要快得多(而且,尽管它的价值很小,但也适用于旧版本的 Windows)。

评论

0赞 Ted Lyngmo 11/17/2023
当我看到“前段时间......”和这种代码时,我希望它是 10+ 年......然后我意识到现在已经那么老了。:-)auto
1赞 Jerry Coffin 11/17/2023
@TedLyngmo:日期会随着年龄的增长而变得模糊,但我最初可能在接近 25 年前写它,并在 10 或 12 年前更新了它。
0赞 Ted Lyngmo 11/17/2023
我确实喜欢看到更新的怀旧情绪。你的旧代码仍然有意义。这就是赞成票的来源。
1赞 Jerry Coffin 11/17/2023
@TedLyngmo:无论如何,似乎仍然有效(即使我通常必须重新启动才能再次测试 Windows 代码)。
1赞 Jerry Coffin 11/22/2023
@MattMoissat: learn.microsoft.com/en-us/windows/terminal/customize-settings/...