Zaawansowane użycie streambuf

Autor: Wojciech Muła
Dodany:12.07.2002

Treść

Wprowadzenie

Standardowe strumienie wyjściowe z C++ oferują minimum funkcjonalności. Takie cechy jak np. kolorowanie tekstu nie są dostępne. Pokażę w jaki sposób na prymitywnych terminalach DOS pokolorować tekst — kolorowanie wiąże się albo z wywołaniem funkcji z conio.h (wspierane przez wiele kompilatorów, także tych dla Windows), albo z bezpośrednim zapisem do VRAM karty graficznej. W przypadku, gdy terminal obsługuje sekwencje ESC (Linux, DOS po instalacji sterownika ANSI.SYS) kolorowanie tekstu można osiągnąć wpisując sekwencje ESC. Można to zrobić poprzez włączenie do wyświetlanego tekstu stosownych sekwencji (co jest uciążliwe), albo napisać odpowiednie manipulatory.

Oczywiście wykorzystanie streambuf nie musi ograniczać się tylko do kolorowania tekstu, można np. automatycznie konwertować polskie znaki, zmieniać wielkość liter, filtrować itp. Kilka przykładów zostanie pokazanych poniżej.

Artykuł powstał na bazie mojej odpowiedzi na zapytanie na grupie usenetowej pl.comp.lang.c.

Klasa streambuf

Komunikację klasy ios z urządzeniami zewnętrznymi zapewnia klasa dziedzicząca po klasie abstrakcyjnej streambuf — poniżej fragment deklaracji tej klasy:

class streambuf {
// ...
public: // może być 'protected' -- zależy od implementacji

        virtual int overflow (int = EOF) = 0;
        virtual int underflow() = 0;
// ...
}

Jak widać klasa zawiera dwie metody abstrakcyjne. Sama klasa zajmuje się obsługą bufora, gdy następuję przepełnienie bądź niedomiar danych woła metodę (odpowiednio) overflow lub underflow. Tak więc dopiero w klasie potomnej definiuje się gdzie i w jaki sposób będą wyprowadzane (bądź wprowadzane) dane.

Nasz własny bufor

W klasie pochodnej nie trzeba właściwie definiować nic poza konstruktorem i metodami overflow i underflow.

class colorbuf : public streambuf {
private:
        unsigned char foreground, background;
public:
        colorbuf() : streambuf(rozmiar_bufora)
                {
                 setfore(default_foreground);
                 setback(default_background);
                }

        virtual int overflow (int = EOF);
        virtual int underflow();

        void setfore(unsigned char fore)
                { terminal_setforeground(foreground=fore); }
        void setback(unsigned char back)
                { terminal_setbackground(background=back); }
};

Konstruktor najpierw buduje obiekt typu streambuf — jako argument podawany jest rozmiar bufora (choć można go pominąć).

Najpierw zdefiniujemy metodę underflow, a ponieważ zadaniem klasy colorbuf ma być kolorowanie wyjścia zatem będzie ona maksymalnie uproszczona.

int colorbuf::undeflow() {
 return EOF; // zwrócenie EOF oznacza brak danych na wejściu
}

Przyjmując że, hipotetyczne funkcje terminala[1] wołane z metod setback i setfore ustawiają kolory do czasu kolejnego ich wywołania, to metoda overflow będzie bardzo prosta:

int colorbuf::overflow(int c)
{
 terminal_putchar(c); // funkcja wyświetla znak używając ustawień terminala,
                      // np. w przypadku 'conio.h' tą funkcją może być 'cputc'
 return ~EOF;
}

Wymuszenie by wyświetlany tekst był postaci To jEsT pRzYkLaD jest niezwykle proste.

int colorbuf::overflow(int c)
{
 static n=0;

 terminal_putchar( n ? toupper(c):tolower(c));
 n=!n;
 return ~EOF;
}

Z kolei kapitalizacja (ang. capitalize) tekstu można uzyskać w sposób następujący:

// cout << "przykladowy tekst z kilkoma slowami";
// wyjście: Przykladowy Tekst Z Kilkoma Slowami
int colorbuf::overflow(int c)
{
 static prevwhitespace=1;

 terminal_putchar( prevwhitespace ? toupper(c) : c);

 prevwhitespace = isspace(c);

 return ~EOF;
}

Inny przykład to wyświetlanie numerów szesnastkowych znaków kontrolnych (w ASCII znaki te mają numery od 0 do 31), jedyny wyjątek stanowić będzie znak nowej linii o numerze 13.

// cout << "Znak ESC ma kod \033."
// wyjście: Znak ESC ma kod 0x1b.
int colorbuf::overflow(int c)
{
 if ((c > 31) || (c == 13)) // zwykły znak
    terminal_putchar(c);
 else
    printf("0x%x", (int)c);

 return ~EOF;
}

Proszę zwrócić uwagę, że funkcja zwraca negację wartości EOF — w ogólności wartość różną od EOF. Gdyby został zwrócony EOF to do czasu wyczyszczenia flagi eof stanu strumienia (np. poprzez wywołanie metody clear()) nic nie zostanie wyświetlone. Tę własność można użyć np. do wyświetlania danych w ograniczonych polach tekstowych (oczywiście przy wsparciu jakichś rozsądnych manipulatorów).

Podmiana standardowych buforów

Podpięcie własnego bufora do nowotworzonego strumienia jest bardzo proste, z tego względu, że klasa ostream ma konstruktor akceptujący wskaźnik do streambuf:

#include <iostream>

int main()
{
 colorbuf __buf;
 ostream  my_cout( (streambuf*)&__buf );

 my_cout << "test\n"
}

Jednak podmiana bufora w istniejącym strumieniu (np. cout) jest niestety nieprzenośna. W GCC klasa ios ma publiczne pole _strbuf będące wskaźnikiem na streambuf, także metoda rdbuf() posiada wersję umożliwiającą podmianę bufora.

#include <iostream>

int main()
{
 colorbuf __buf;

 cout._strbuf = (strambuf*)&__buf;
 cout.rdbuf((strambuf*)&__buf);
 cout << "test\n";
}

Niestety w innych kompilatorach tak nie jest, np. Watcom składowa przechowująca wskaźnik do obiektu streambuf jest prywatna i nie istnieje żadna metoda umożliwiająca ustawienie nowego bufora (w gcc jest metoda strambuf* rdbuf(streambuf* new_buf)).

W miarę elegancki i co ważniejsze przenośny sposób na podmianę bufora jest możliwy po zaprzęgnięciu do pracy preprocesora — wystarczy użyć definicji:

#define cout my_cout

Większość kompilatorów ma możliwość podania w linii polecenia definicji, w gcc wygląda to tak:

gcc -lstdc++ program.cpp -Dcout=my_cout

Manipulatory

Manipulatory pozwalają na wywoływanie określonych funkcji bezpośrednio w strumieniu. Standardowe manipulatory to np. hex, dec, endl, setprecision(int) itp.

Manipulator bezparametrowy to funkcja o następującej deklaracji:

ostream& manipulator(ostream&);

Np. manipulator przywracający typowe ustawienia terminala może mieć postać:

ostream& defcolor(ostream& os)
{
 colorbuf *c = (colorbuf*)os.rdbuf(); // składowa ios::streambuf* rdbuf();

 c->setfore(default_foreground);
 c->setback(default_background);

 return os;
}

Z kolei manipulator z jednym parametrem jest bardzo łatwy do uzyskania dzięki wzorcom z pliku iomanip — jako przykład podam manipulator zmieniający kolor liter.

#include <iostream>
#include <iomanip>

// ta funkcja wykonuje tylko zadanie
ostream& colorbuf_manipulator_setcolor(ostream& os, int color)
{
 colorbuf *c = (colorbuf*)os.rdbuf();

 c->setfore(color);

 return os;
}

// manipulator definiowany ze wzorca
omanip<int> setcolor(int color)
{
 return omanip<int>(&colorbuf_manipulator_setcolor, color);
}
[1]--- takie funkcje to np. textcolor i textbackground z DOS-owego conio.h.

console.tgz — kilka podstawowych funkcji z conio.h używających sekwencji ESC (na podstawie man console_codes).