- předchozí článek - následující článek - obsah - úvodní stránka -

Linuxové noviny 11-12/98

Programujeme v C s ncurses

Karel Žák, 25. října 1998

Tento článek by měl být malou rychlokuchařkou do světa ncurses. Snad jen na začátek, malé vysvětlení co to ncurses jsou. Název "ncurses" je složeninou ze slov "new curses". Jedná se o volně distibuovatelný klon System V Release 4.0 (SVr4) curses. A jak říkají FAQ, je to slovní hříčka k "cursor optimization". Ale jinak (a vážně) jedná se o knihovnu k managamentu výstupu na "character-cell terminals" tedy na znakově orientované terminály.

Ale rychle k programování. Tato knihovna není žádným drobečkem (při pohledu do adresáře /lib je - alespoň na mém disku - po libc tou největší) takže snad někteří prominou pokud se o některých možnostech nezmíním. Pokusím se zaměřit pouze na to jak nenásilně a rychle dosáhnout "počmárání screenu ve stylu ncurses" :-)

Inicializace

Jako první by si měl program nainicializovat obrazovku do curses módu. To se provede funkcí initscr(). Po provedení této funkce, by programátor nikdy neměl zapomenout, že po ukončení programu by vše mělo být jako před jeho spuštěním. Návrat z curses módu se provede funkcí endwin(). Pokud si nejste jisti, že jste tuto funkci zavolali tak se o tom můžete přesvědčit pomocí isendwin(). Protože nikdy není tak úplně jisté kdy a kde program skončí, tak např. já to dělám tak, že si do atexit() přidám endwin(), tedy

atexit((void *) endwin)

Pokud se divíte, proč taková přemíra starostí o endwin(), tak proto, že program, který se ukončí bez zavolání této funkce nechá váš terminál v tom lepším stavu. Funkce initscr() vrací pointer stdscr typu WINDOW (jedná se o globální proměnnou, ke které ncurses vztahuje funkce, které nepoužívají okno jako parametr.

Po inicializaci je možné si doopravit některé další vlastnosti screenu / terminálu. Například:

  • curs_set() - nastavení kursoru
  • echo() / noecho() - mají se zobrazovat znaky přicházející z klávesnice (např. při volání funkce mvwgetstr())
  • cbreak() - nastaven "line bufferingu"
  • keypad() - nastavení používání kláves F1Fn

U poslední zmíněné funkci snad ještě poznámka: pokud používáte ve svém programu WINDOW, tak je nutné pro každé nové okno tuto funkci volat, jinak při nepoužívání oken je možné zavolat na počátku

keypad(stdscr, TRUE)

Po přepnutí do curses módu jsou dostupné dvě globální proměnné (int) LINES a COLS, ve kterých je uložena aktuální velikost obrazovky. Časté je např. pořadí inicializace: initscr(), cbreak(), noecho() - pak už je obrazovka připravena ve stavu "neřádkování", ale naopak ve stavu "kam přesně co chceš" (na jakou řádku a do jakého sloupce).

Někdy je nutné se z ncurses programu přepnout nazpět do původního (v terminologii ncurses - shell) módu. To je nutné například při volání nějakého externího programu, který používá terminál standardním způsobem. K tomu se používá skupina funkcí okolo reset_shell_mode() (více manuál).

Pochopitelně, ze ncurses obsahují ještě další přepínače, ale to už by mohlo být otázkou samostudia nad manuály :-) Podobně jako záležitosti týkající se možnosti otevřít si vlastní screen pomocí funkce newterm().

Výstup a okna

Už jsem se zmínil o WINDOW. Nové okno si vytvoříte pomocí funkce newwin(). Jako parametry se zadávají poloha okna a to vzhledem k levému hornímu rohu a velikost okna. Po vytvoření okna je možné s ním zacházet jako se samostatnou podmnožinou a tedy volat pak funkce s parametry řádek a sloupců vzhledem k levému hornímu roku okna. Funkce newwin() vrací pointer na nové okno (funkce sama alokuje pomocí malloc strukturu WINDOW) proto se okno deklaruje jako pointer (podobně jako FILE). Naopak uvolnění paměti a zrušení okna se provede funkcí delwin(). Smazání ze screenu se provede funkcí werase() (a pak je vhodné wrefresh()). V okně může existovat i tzv. subwin, jedná se to samé jako WINDOW, ale vzhledem k rodičovskému oknu (narozdíl od okna, které je definováno vzhledem ke screenu). S okny lze vytvářet docela zajímavé věci. Například zapsat do souboru a pak naopak načíst okno ze souboru. Často je využívána i například funkce redrawwin(), která znova nakreslí okno na screen. Je možné s okny pohybovat pomocí mvwin(), duplikovat okna - dupwin() atd.

Funkce zacházející s okny lze jednoduše poznat dle prefixu mvw- nebo samotného w-. Takže obdoba klasického printf() je v ncurses při použití oken

   mvwprintw(okno, řádka, sloupec,\
    "něco: %s", řetězec);

(pozor na znak \n - v ncurses se dostáváme na další řádku tím, že zvýšíme "řádku" o jedna).

#include <curses.h>             /* stdio.h neni nutne je v curses.h */
#include <ctype.h>

#define COLOR2      1            /* barvicka 1. */
#define COLOR1      2            /* barvicka 2. */

int main () {
   int      c;
   char      *s;

   initscr ();            
   cbreak ();
   noecho ();                  /* vypnuti echa */
   start_color ();                  /* chceme barvy */
   keypad (stdscr, TRUE);            /* chceme klavesy pod makry KEY_neco */
   curs_set(0);                  /* at tam ta mrska neblika */

   if (!has_colors ()) {            /* umi terminal barvy ? */
       endwin ();
       fputs ("Hmm.. tady barvy nejdou !", stderr); 
       exit (1);
   }
   /*         barva - popredi   -       pozadi            */ 
   init_pair (COLOR1, COLOR_RED,       COLOR_BLUE);      /* barvicka 1. */
   init_pair (COLOR2, COLOR_YELLOW, COLOR_BLACK);      /* a druha */

   attron (COLOR_PAIR( COLOR1 ));                  /* pouzivat barvu 1. */
   mvaddstr (2, 5, "Cervene na modrem");

   attron (COLOR_PAIR( COLOR2 ));
   mvaddstr (3, 5, "Zlute na cernem");

   attron (A_BOLD);                        /* od ted vse BOLD */
   mvaddstr (4, 5, "Zlute na cernem a tucne");

   attroff (COLOR_PAIR( COLOR2 ));                  /* vypne barvu */
   mvhline(LINES-2, 0, ACS_HLINE, COLS);            /* nakresli caru */
   mvaddstr (LINES-1, COLS-15, "F10 - konec");
   mvaddstr (10, 5, "Jmeno klavesy:");

   while ( (c=getch()) != KEY_F(10)) {
           s = (char *) keyname(c);            /* jmeno klavesy ? */
           mvhline(10, 20, ' ', COLS);            /* smaz */
           if (s)
                 mvprintw (10, 20, "'%s'", s);
           else       
                 mvprintw (10, 20, "'%c'", (isprint(c) ? c : '.'));
   }
   erase ();                        /* smaz nase vytvory */      
   refresh ();                        
   endwin ();                        /* konec curses */
   exit (0);                        /* ....bye */
}

Výpis č. 2: Příklad použití ncurses

Další možností jak tisknout na screen je nevyužívat WINDOW a využívat funkcí, které nepožadují WINDOW jako svůj parametr. Lines a cols v parametrech těchto funkcí se pak vztahují k levému hornímu rohu screenu. Tyto funkce pak většinou začínají prefixem mv- např. mvaddstr(y,x, str).

Ncurses ještě umožňují používat funkce bez parametrů line a cols. Takové funkce pak vypisují svůj výstup na aktuální pozici kursoru. Nutno říct, že tento způsob je asi nejméně programátorsky příjemný. Takovouto funkcí je např. addstr(str). Jak je z uvedených příkladů zřejmé, tak u většiny funkcí existují všechny tři alternativy, tedy např. mvwaddstr, mvaddstr i addstr.

V ncurses jsou pak ještě alternativy těchto funkcí umožňující přesné zacházení se řetězci (podobně jako známé stringové strcpy a strncpy), tedy např. addstr() a addnstr().

Pokud potřebujete tisknout některé speciální znaky tak doporučuji podívat se na man addch, kde jsou popsána makra ASC_něco obsahující tyto znaky (např. čáry, různé šipky apod.). Ke snadnějšímu kreslení čar existují i funkce hline() (horizontální) a vline() (vertikální) a jejich alternativy pro okna. Často se u oken používají rámečky. Rámeček můžete nakreslit pomocí výše zmíněných funkcí a nebo pomocí, k těmto účelům předdefinované, funkce wborder().

Pro přesnost snad ještě poznámku - většina funkcí které nepožadují WINDOW jsou ve skutečnosti makra typu:

   #define mvaddstr(y, x, str)\
    mvwaddstr(stdscr, y, x, str)

Pro dobré zacházení s výstupem ještě doporučuji prohlédnut si manuály k funkcím refresh(), wrefresh(), redrawwin() atd. Co tyto dělají je myslím patrné už z jejich názvu.

Zaznělo zde již i něco o pozici kursoru, i tu lze pochopitelně měnit. Pomocí wmove(y, x, win) v rámci okna a pomocí move() vzhledem ke screenu. Další a již zmíněnou možností je zapínaní resp. vypínaní cursoru a to pomocí curs_set(0) a curs_set(1).

Možná se ptáte, co je lepší - používat v programu okna nebo ne? Abych upřímně odpověděl - tak nevím. Používání oken s sebou nese několik výhod. Části výstupu jsou umístěny v podmnožinách, se kterými lze samostatně zacházet. Například pokud jedním oknem překryjete druhé, pak snadno to překryté obnovíte pomocí redrawwin(), zatímco pokud okna nepoužíváte tak tím, co na screen napíšete nenávratně přepíše to, co tam bylo (na dané pozici). Ale při nepoužívání oken ve vašem programu ubude volání malloc (při každém newwin), nebudete muset programem vláčet struktury WINDOW a pravděpodobně program bude rychlejší. Na zamyšlení snad uvedu, že např. v Midnight Commanderu okna používána nejsou, já sám jsem (malá reklama :-)) např. v programu kim (manažer procesů) okna použil, ale v následující verzi už nebudou. To vše, ale neznamená, že okna jsou něco nedobrého, jen je někdy dobré, pokud se program stará o některé věci sám, a i v programování platí, že méně je někdy více.

Za výstup snad lze ještě považovat to, co dělá funkce beep(). Jak už je z názvu patrno, jedná se o zvukové znamení.

Vstup

Aby mohl program "vnímat" uživatele tak pochopitelně knihovna obsahuje funkce vracející znaky nebo řetězce zadané z klávesnice. Základní je getch() a při použití oken wgetch(). Pozor, nezapomínat při používání oken volat po vytvoření okna funkci keypad(). Těmito funkcemi vrácená hodnota (int) obsahuje znak nebo v případě stisku nějaké speciální (funkční) klávesy v curses.h nadefinované makro mající prefix KEY_, např. při stisku klávesy šipka_nahoru ("šipka nahoru") je vráceno KEY_UP.

Klávesu je možné i vrátit (podobně jako u souboru znak) pomocí ungetch(). Pochopitelně jsou definovány i funkce vracející znak nejen do programu, ale opisující ho také na screen nebo okno (pozor - nesmí být vypnuto echo pomocí noecho()). Např. mvgetch(), mwvgetch(). Také je možné pomocí has_key() zjistit, zná-li terminál některou klávesu (ta je jako parametr této funkce) nebo například jméno klávesy a to pomocí keyname().

Pro přijetí celého řetězce lze využít funkce z rodiny getstr(). Tyto načítají všechny zadané znaky až do stisku klávesy Enter do řetězce. Existují pochopitelně i bezpečnější varianty těchto funkcí, tedy getnstr().

Zajímavostí může být nastavení timeoutů pro čtení klávesy. Toho lze efektivně vyžít například k tomu, že během toho co program čeká na to až se uživatel rozhodne něco stisknout tak může např. testovat nějakou událost. Já to například používám pokud chci provést nějakou složitější a náročnější reakci na signál, kdy není vhodné, aby tuto činnost vykonával handler signálu. Nastavení timeoutu se provede funkcí timeout() nebo wtimeout() s parametrem v milisekundách. Pokud do nastavené doby nedošlo k stisknutí nějaké klávesy vrací např. funkce getch() hodnotu ERR.

Další možností jak načítat "něco" do vaší aplikace je možnost číst ze screenu nebo okna znak nebo řetězec ležící na zadaných souřadnicích a to pomocí rodiny funkcí inch a instr (mvinch(), mvwinch(), mvinstr()). U funkcí inch lze nejen zjistit znak na dané pozici, ale i jeho barvu a atributy.

Barvy

Chcete-li používat ve svém programu barvy musí být splněno několik základních podmínek. Prvně je nutné po inicializaci screenu zavolat funkci start_color(), kterou sdělíte ncurses, že máte takovýto úmysl. Podporuje-li terminál barvy zjistíte funkcí has_colors().

Jak bude vypadat výstup je možné nastavit pomocí attron() nebo pro okno watrron() a naopak zrušit pomocí attroff() (wattroff()). Kde jako parametr je uvádí požadovaný stav, tedy např. A_BOLD, A_BLINK, A_NORMAL atd. Od chvíle nastavení pomocí attron všechen výstup používá toto nastavení a to až do další změny pomocí attron nebo do jeho zrušení pomocí attroff.

[ Příklad použití ncurses ]

Před používáním barev je nutné je nadefinovat. To se provede pomocí init_pair(barva, popředí, pozadí). Kde barva je číslo a popředí (písmo) a pozadí jsou jedna z nadefinovaných barev (COLOR_RED, COLOR_BLUE, COLOR_YELLOW, COLOR_GREEN, COLOR_CYAN, COLOR_BLACK, COLOR_WHITE a COLOR_MAGENTA).

Chceme-li pak použít takto nadefinovaných barev provedeme to zase pomocí attron(). A to takto: attron(COLOR_PAIR(barva));. Pochopitelně lze kombinovat i s bold. Použití bude asi nejlépe viditelné na příkladu a na jeho výstupu.

Ncurses pochopitelně dovolují i další operace s barvami, ale to je nad rámec tohoto článku a lze to poměrně pohodlně nastudovat z manuálu.

Resize

Je to také trochu nad rámec tohoto článku, ale protože je to častým dotazem na různých konferencích, tak to zde zmíním. Pokud provozujete ncurses program pod xtermem, tak se běžně stává, že uživatel mění velikost okna xtermu. Je tedy vhodné zjistit novou velikost LINES a COLS. O změně velikosti okna je aplikace informována signálem SIGWINCH. Občerstvit LINES a COLS můžete např. takto:

   struct winsize size;
   size.ws_row = size.ws_col = 0;

   ioctl(0, TIOCGWINSZ, &size); 
   if (size.ws_row && size.ws_col) {
      LINES = size.ws_row;
      COLS = size.ws_col;		
   }

Myška

Při důkladném pročítání manuálu ncurses zjistíte, že ncurses obsahují funkce getmouse() atd. Tyto funkce, ale nejsou v původním SVr4. Já osobně jsem těchto funkcí nikdy nepoužil. Stalo se zvykem používat gpm a jsem toho názoru, že některé zvyky jsou dobré, tak proč je měnit. Knihovna gpm dokonce na programy s ncurses pamatuje, takže jsou zde vytvořené alternativy getch() a wgetch() (Gpm_Getch() a Gpm_Wgetch()) pro používání myši. Každopádně gpm s ncurses pracuje bezvadně. Ale o gpm až někdy příště.

Závěr

Protože některým programátorům nevyhovovaly pouze základní možnosti ncurses, tak vznikla řada nástaveb. Standardně jsou s ncurses distribuované menu, panel, form. Je otázkou jak moc jsou tyto různá "udělátka" šikovná a hlavně útlá a jestli náhodou programátora nesvazují a konečnému produktu nevtiskují tvář jakou chtějí oni.

K přiloženým příkladům: kompilace programu s knihovnou se provede např. takto:

   gcc -Wall -O3 program.c -o program -lncurses

(to -Wall doporučuji, často ukáže programátorům věci "netušené" :-) Ti dloubavější si možná v místech, kde se zmiňuji o mc vzpomněli na knihovnu slang. Ano, v současné době se tento program distribuuje kompilovaný s touto knihovnou (s ncurses to ale jde kompilovat také). Programy psané pro ncurses, lze po malých úpravách kompilovat i s knihovnou slang. Je jen nutné místo curses.h includovat slcurses.h.

Jako další studijní materiál doporučuji obsáhlý man curses a ještě (a to nejlépe) zdrojáky nějakého ncurses programu.

Přiložený příklad lze najít včetně Makefile v adresáři s Linuxovými novinami nebo na adrese http://home.zf.jcu.cz/~zakkr/LN/. *


- předchozí článek - následující článek - obsah - úvodní stránka -