Хрестоматия по программированию на Си в Unix

         

Экранные библиотеки и работа с видеопамятью. Хрестоматия по программированию на Си в Unix


Экранные библиотеки и работа с видеопамятью.

Терминал в UNIX с точки зрения программ - это файл. Он представляет собой два устройства: при записи write() в этот файл осуществляется вывод на экран; при чтении read()-ом из этого файла - читается информация с клавиатуры.

Современные терминалы в определенном смысле являются устройствами прямого доступа:

  • информация может быть выведена в любое место экрана, а не только последовательно строка за строкой.
  • некоторые терминалы позволяют прочесть содержимое произвольной области экрана в вашу программу.

Традиционные терминалы являются самостоятельными устройствами, общающимися с компьютером через линию связи. Протокол - общения образует систему команд терминала и может быть различен для терминалов разных моделей. Поэтому библиотека работы с традиционным терминалом должна решать следующие проблемы:

  • настройка на систему команд данного устройства, чтобы одна и та же программа работала на разных типах терминалов.
  • эмуляция недостающих в системе команд; максимальное использование предоставленных терминалом возможностей.
  • мимнимизация передачи данных через линию связи (для ускорения работы).
  • было бы полезно, чтобы библиотека предоставляла пользователю некоторые логические абстракции, вроде ОКОН - прямоугольных областей на экране, ведущих себя подобно маленьким терминалам.

В UNIX эти задачи решает стандартная библиотека curses (а только первую задачу более простая библиотека termcap). Для настройки на систему команд конкретного дисплея эти библиотеки считывают описание системы команд, хранящееся в файле /etc/termcap. Кроме них бывают и другие экранные библиотеки, а также существуют иные способы работы с экраном (через видеопамять, см. ниже).

В задачах данного раздела вам придется пользоваться библиотекой curses. При компиляции программ эта библиотека подключается при помощи указания ключа -lcurses, как в следующем примере:

cc progr.c -Ox -o progr -lcurses -lm

Здесь подключаются две библиотеки:

  • /usr/lib/libcurses.a (работа с экраном) и
  • /usr/lib/libm.a (математические функции, вроде sin, fabs). Ключи для подключения библиотек должны быть записаны в команде САМЫМИ ПОСЛЕДНИМИ. Заметим, что стандартная библиотека языка Си (содержащая системные вызовы, библиотеку stdio (функции printf, scanf, fread, fseek, ...), разные часто употребляемые функции (strlen, strcat, sleep, malloc, rand, ...)) /lib/libc.a подключается автоматически и не требует указания ключа -lc.


Для работы может оказаться более удобным иметь указатель на видеопамять как на массив структур. Приведем пример для системы MS DOS:

#include <dos.h> /* там определено MK_FP */ char far *screen = MK_FP(0xB800 /*сегмент*/, 0x0000 /*смещение*/); struct symb{ char chr; char attr; } far *scr, far *ptr; #define COLS 80 /* число колонок */ #define LINES 25 /* число строк */ #define SCR(x,y) scr[(x) + COLS * (y)] /* x из 0..79, y из 0..24 */ void main(){ int x, y; char c; scr = (struct symb far *) screen; /* или сразу * scr = (struct symb far *) MK_FP(0xB800,0x0000); */ /* переписать строки экрана справа налево */ for(x=0; x < COLS/2; x++ ) for( y=0; y < LINES; y++ ){ c = SCR(x,y).chr; SCR(x,y).chr = SCR(COLS-1-x, y).chr; SCR(COLS-1-x, y).chr = c; } /* сделать цвет экрана: желтым по синему */ for(x=0; x < COLS; x++) for(y=0; y < LINES; y++) SCR(x,y).attr = (0xE | (0x1 << 4)); /* желтый + синий фон */ /* прочесть любую кнопку с клавиатуры (пауза) */ (void) getch(); }



И, наконец, еще удобнее работа с видеопамятью как с двумерным массивом структур:

#include <dos.h> /* MS DOS */ #define COLS 80 #define LINES 25 struct symb { char chr; char attr; } (far *scr)[ COLS ] = MK_FP(0xB800, 0); void main(void){ register x, y; for(y=0; y < LINES; y++) for(x=0; x < COLS; ++x){ scr[y][x].chr = '?'; scr[y][x].attr = (y << 4) | (x & 0xF); } getch(); }

Учтите, что при работе с экраном через видеопамять, курсор не перемещается! Если в обычной работе с экраном текст выводится в позиции курсора и курсор автоматически

продвигается, то здесь курсор будет оставаться на своем прежнем месте. Для перемещения курсора в нужное вам место, вы должны его поставить явным образом по окончании записи в видеопамять (например, обращаясь к портам видеоконтроллера).

Обратите внимание, что спецификатор модели памяти far должен указываться перед КАЖДЫМ указателем (именно для иллюстрации этого в первом примере описан неиспользуемый указатель ptr).

Составьте программу сохранения содержимого экрана IBM PC (видеопамяти) в текстовом режиме в файл и обратно (в системе XENIX).




struct symbol{ /* IBM PC family */ char chr; /* код символа */ char attr; /* атрибуты символа (цвет) */ } mem[ 25 ] [ 80 ]; /* 25 строк по 80 символов */

Структура байта атрибутов:

------------------------------------------ | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | # бита ------------------|----------------------- |blink| R | G | B | intensity | r | g | b | цвет ------------------|----------------------- background (фон) | foreground (цвет букв)

R - red (красный) G - green (зеленый) B - blue (синий)
blink - мерцание букв (не фона!)
intensity - повышенная яркость

Координатная система на экране: верхний левый угол экрана имеет координаты (0,0), ось X горизонтальна, ось Y вертикальна и направлена сверху вниз.

Цвет символа получается смешиванием 3х цветов: красного, зеленого и синего (электронно-лучевая трубка дисплея имеет 3 электронные пушки, отвечающие этим цветам). Кроме того, допустимы более яркие цвета. 4 бита задают комбинацию 3х основных цветов и повышенной яркости. Образуется 2**4=16 цветов:

I R G B номер цвета

BLACK 0 0 0 0 0 черный BLUE 0 0 0 1 1 синий GREEN 0 0 1 0 2 зеленый CYAN 0 0 1 1 3 циановый (серо-голубой) RED 0 1 0 0 4 красный MAGENTA 0 1 0 1 5 малиновый BROWN 0 1 1 0 6 коричневый LIGHTGRAY 0 1 1 1 7 светло-серый (темно-белый) DARKGRAY 1 0 0 0 8 темно-серый LIGHTBLUE 1 0 0 1 9 светло-синий LIGHTGREEN 1 0 1 0 10 светло-зеленый LIGHTCYAN 1 0 1 1 11 светло-циановый LIGHTRED 1 1 0 0 12 ярко-красный LIGHTMAGENTA 1 1 0 1 13 ярко-малиновый YELLOW 1 1 1 0 14 желтый WHITE 1 1 1 1 15 (ярко)-белый

Физический адрес видеопамяти IBM PC в цветном алфавитно-цифровом режиме (80x25, 16 цветов) равен 0xB800:0x0000. В MS DOS указатель на эту память можно получить при помощи макроса make far pointer: MK_FP (это должен быть far или huge указатель!). В XENIX - указатель получается при помощи системного вызова ioctl, причем система предоставит вам виртуальный адрес, ибо привелегия работы с физическими адресами в UNIX

принадлежит только системе. Работу с экраном в XENIX вы можете увидеть в примере "осыпающиеся буквы".





/*#! /bin/cc fall.c -o fall -lx * "Осыпающиеся буквы". * Использование видеопамяти IBM PC в ОС XENIX. * Данная программа иллюстрирует доступ к экрану * персонального компьютера как к массиву байт; * все изменения в массиве немедленно отображаются на экране. * Функция nap() находится в библиотеке -lx * Показана также работа с портами IBM PC при помощи ioctl(). */ #include <stdio.h>

#include <fcntl.h> /* O_RDWR */ #include <signal.h>

#include <ctype.h>

#include <sys/types.h>

#include <sys/at_ansi.h>

#include <sys/kd.h> /* for System V/4 and Interactive UNIX only */ /*#include <sys/machdep.h> for XENIX and SCO UNIX only */ #include <sys/sysmacros.h>

#ifdef M_I386 # define far /* на 32-битной машине far не требуется */ #endif char far *screen; /* видеопамять как массив байт */ /* far - "длинный" (32-битный) адрес(segment,offset) */ int segm; /* сегмент с видеопамятью */ #define COLS 80 /* число колонок на экране */ #define LINES 25 /* число строк */ #define DELAY 20 /* задержка (миллисекунд) */ int ega; /* дескриптор для доступа к драйверу EGA */ /* структура для обмена с портами */ static struct port_io_struct PORT[ 4 /* не более 4 за раз */] = { /* операция номер порта данные */ /* .dir .port .data */ /* Переустановить flip/flop: * заставить порт 0x3C0 ожидать пары адрес/значение * при последовательной записи байтов в этот порт. */ { IN_ON_PORT, 0x3DA, -1 }, /* IN-чтение */ /* Теперь 3c0 ожидает пары адрес/значение */ { OUT_ON_PORT, 0x3C0, -1 /* адрес */ }, { OUT_ON_PORT, 0x3C0, -1 /* значение*/ }, /* OUT-запись */ /* переинициализировать дисплей, установив бит #5 порта 3c0 */ { OUT_ON_PORT, 0x3C0, 0x20 } }; void closescr(nsig){ /* конец работы */ setbgcolor(0); /* установить черный фон экрана */ exit(0); } /* получение доступа к видеопамяти адаптера VGA/EGA/CGA */ void openscr () { static struct videodev { char *dev; int mapmode; } vd[] = { { "/dev/vga", MAPVGA }, { "/dev/ega", MAPEGA }, { "/dev/cga", MAPCGA }, { NULL, -1 } }, *v; /* устройство для доступа к видеоадаптеру */ for(v=vd; v->dev;v++ ) if((ega = open (v->dev, O_RDWR)) >= 0 ) goto ok; fprintf( stderr, "Can't open video adapter\n" ); exit(1); ok: /* fprintf(stderr, "Adapter:%s\n", v->dev); */ /* получить адрес видеопамяти и доступ к ней */ #ifdef M_I386 screen = (char *) ioctl (ega, v->mapmode, 0); #else segm = ioctl (ega, v->mapmode, 0); screen = sotofar (segm, 0); /* (segment,offset) to far pointer */ #endif signal( SIGINT, closescr ); } /* макросы для доступа к байтам "символ" и "атрибуты" * в координатах (x,y) экрана. */ #define GET(x,y) screen[ ((x) + (y) * COLS ) * 2 ] #define PUT(x,y, c) screen[ ((x) + (y) * COLS ) * 2 ] = (c) #define GETATTR(x,y) screen[ ((x) + (y) * COLS ) * 2 + 1 ] #define PUTATTR(x,y, a) screen[ ((x) + (y) * COLS ) * 2 + 1 ] = (a) /* символ изображается как черный пробел ? */ #define white(c,a) ((isspace(c) c==0) && (attr & 0160)==0) /* установить цвет фона экрана */ void setbgcolor( color ){ PORT[1].data = 0; /* регистр номер 0 палитры содержит цвет фона */ /* всего в палитре 16 регистров (0x00...0xFF) */ PORT[2].data = color ; /* новое значение цвета, составленное как битовая маска * RGBrgb (r- красный, g- зеленый, b- синий, RGB- дополнительные * тусклые цвета) */ /* выполнить обмены с портами */ if( ioctl( ega, EGAIO, PORT ) < 0 ){ fprintf( stderr, "Can't out port\n" ); perror( "out" ); } } void main(ac, av) char **av;{ void fall(); openscr(); if( ac == 1 ){ setbgcolor(020); /* темно-зеленый фон экрана */ fall(); /* осыпание букв */ } else { if(*av[1] == 'g') /* Установить режим адаптера graphics 640x350 16-colors */ ioctl( ega, SW_CG640x350, NULL); /* Если вы хотите получить адрес видеопамяти в графическом режиме, * вы должны СНАЧАЛА включить этот режим, * ЗАТЕМ сделать screen=ioctl(ega, v->mapmode, NULL); * и ЕЩЕ РАЗ сделать включение графического режима. */ /* Установить режим адаптера text 80x25 16-colors */ else ioctl( ega, SW_ENHC80x25, NULL); } closescr(0); } /* осыпать буквы вниз */ void fall(){ register i, j; int rest; int nextcol; int n; int changed = 1; /* не 0, если еще не все буквы опали */ char mask [ COLS ]; while( changed ){ changed = 0; for( i = 0 ; i < COLS ; i++ ) mask[ i ] = 0; for( i = 0 ; i < COLS ; i++ ){ rest = COLS - i; /* осталось осыпать колонок */ nextcol = rand() % rest; j = 0; /* индекс в mask */ n = 0; /* счетчик */ for(;;){ if( mask[j] == 0 ){ if( n == nextcol ) break; n++; } j++; } changed += fallColumn( j ); mask[j] = 1; } } } /* осыпать буквы в одном столбце */ int fallColumn( x ){ register int y; char ch, attr; int firstspace = (-1); int firstnospace = (-1); Again: /* find the falled array */ for( y=LINES-1; y >= 0 ; y-- ){ ch = GET( x, y ); attr = GETATTR( x,y ); if( white(ch, attr)){ firstspace = y; goto FindNoSpace; } } AllNoSpaces: return 0; /* ничего не изменилось */ FindNoSpace: /* найти не пробел */ for( ; y >= 0 ; y-- ){ ch = GET( x, y ); attr = GETATTR( x, y ); if( !white(ch, attr)){ firstnospace = y; goto Fall; } } AllSpaces: /* в данном столбце все упало */ return 0; Fall: /* "уронить" букву */ for( y = firstnospace ; y < firstspace ; y++ ){ /* переместить символ на экране на одну позицию вниз */ ch = GET( x, y ); attr = GETATTR( x, y ); PUT( x, y, 0 ); PUTATTR( x, y, 0 ); PUT( x, y+1 , ch ); PUTATTR( x, y+1, attr ); nap( DELAY ); /* подождать DELAY миллисекунд */ } return 1; /* что-то изменилось */ }



* - Под протоколом в программировании подразумевают ряд соглашений двух сторон (сервера и клиентов; двух машин в сети (кстати, термин для обозначения машины в сети "host" или "site")) о формате (правилах оформления) и смысле данных в передаваемых друг другу сообщениях. Аналогия из жизни - человеческие речь и язык. Речь всех людей состоит из одних и тех же звуков и может быть записана одними и теми же буквами (а данные - байтами). Но если два человека говорят на разных языках - т.е. поразному конструируют фразы и интерпретируют звуки - они не поймут друг друга!

** - XENIX - (произносится "зиникс") версия UNIX для IBM PC, первоначально разработанная фирмой Microsoft и поставляемая фирмой Santa Cruz Operation (SCO).

© Copyright А. Богатырев, 1992-95
Си в UNIX

| |





Пользуясь прямым доступом в видеопамять, напишите функции для спасения прямоугольной области экрана в массив и обратно. Вот функция для спасения в массив:

typedef struct { short xlen, ylen; char *save; } Pict; extern void *malloc(unsigned); Pict *gettext (int x, int y, int xlen, int ylen){ Pict *n = (Pict *) malloc(sizeof *n); register char *s; register i, j; n->xlen = xlen; n->ylen = ylen; s = n->save = (char *) malloc( 2 * xlen * ylen ); for(i=y; i < y+ylen; i++) for(j=x; j < x+xlen; j++){ *s++ = SCR(j,i).chr ; *s++ = SCR(j,i).attr; } return n; }

Добавьте проверки на корректность xlen, ylen (в пределах экрана). Напишите функцию puttext для вывода спасенной области обратно; функцию free(buf) лучше в нее не вставлять.

void puttext (Pict *n, int x, int y){ register char *s = n->save; register i, j; for(i=y; i < y + n->ylen; i++) for(j=x; j < x + n->xlen; j++){ SCR(j,i).chr = *s++; SCR(j,i).attr = *s++; } } /* очистка памяти текстового буфера */ void deltext(Pict *n){ free(n->save); free(n); }

Приведем еще одну полезную функцию, которая может вам пригодиться - это аналог printf

при прямой работе с видеопамятью.

#include <stdarg.h>

/* текущий цвет: белый по синему */ static char currentColor = 0x1F; int videoprintf (int x, int y, char *fmt, ...){ char buf[512], *s; va_list var; /* clipping (отсечение по границам экрана) */ if( y < 0 y >= LINES ) return x; va_start(var, fmt); vsprintf(buf, fmt, var); va_end(var); for(s=buf; *s; s++, x++){ /* отсечение */ if(x < 0 ) continue; if(x >= COLS) break; SCR(x,y).chr = *s; SCR(x,y).attr = currentColor; } return x; } void setcolor (int col){ currentColor = col; }



Пользуясь написанными функциями, реализуйте функции для "выскакивающих" окон (pop-up window):

Pict *save; save = gettext (x,y,xlen,ylen); // ... рисуем цветными пробелами прямоугольник с // углами (x,y) вверху-слева и (x+xlen-1,y+ylen-1) // внизу-справа... // ...рисуем некие таблицы, меню, текст в этой зоне... // стираем нарисованное окно, восстановив то изображение, // поверх которого оно "всплыло". puttext (save,x,y); deltext (save);



Для начала напишите "выскакивающее" окно с сообщением; окно должно исчезать по нажатию любой клавиши.

c = message(x, y, text);

Размер окна вычисляйте по длине строки text. Код клавиши возвращайте в качестве значения функции.


Теперь сделайте text массивом строк: char *text[]; (последняя строка - NULL).



Сделайте так, чтобы "выскакивающие" окна имели тень. Для этого надо сохранить в некоторый буфер атрибуты символов (сами символы не надо!), находящихся на местах $:

########## ##########$ ##########$ $$$$$$$$$$

а затем прописать этим символам на экране атрибут 0x07 (белый по черному). При стирании окна (puttext-ом) следует восстановить спасенные атрибуты этих символов (стереть тень). Если окно имеет размер xlen*ylen, то размер буфера равен xlen+ylen-1 байт.

Напишите функцию, рисующую на экране прямоугольную рамку. Используйте ее для рисования рамки окна.


Напишите "выскакивающее" окно, которое проявляется на экране как бы расширяясь из точки:

############## ###### ############## ### ###### ############## ###### ############## ##############

Вам следует написать функцию box(x,y,width,height), рисующую цветной прямоугольник с верхним левым углом (x,y) и размером (width,height). Пусть конечное окно задается углом (x0,y0) и размером (W,H). Тогда "вырастание" окна описывается таким алгоритмом:

void zoom(int x0, int y0, int W, int H){ int x, y, w, h, hprev; /* промежуточное окно */ for(hprev=0, w=1; w < W; w++){ h = H * w; h /= W; /* W/H == w/h */ if(h == hprev) continue; hprev = h; x = x0 + (W - w)/2; /* чтобы центры окон */ y = y0 + (H - h)/2; /* совпадали */ box(x, y, w, h); delay(10); /* задержка 10 миллисек. */ } box(x0, y0, W, H); }




Составьте библиотеку функций, аналогичных библиотеке curses, для ЭВМ IBM PC в ОС XENIX. Используйте прямой доступ в видеопамять.




Напишите рекурсивное решение задачи "ханойские башни" (перекладывание дисков: есть три стержня, на один из них надеты диски убывающего к вершине диаметра. Требуется переложить их на третий стержень, никогда не кладя диск большего диаметра поверх диска меньшего диаметра). Усложнение - используйте пакет curses для изображения перекладывания дисков на экране терминала. Указание: идея рекурсивного алгоритма:



carry(n, from, to, by) = if( n > 0 ){ carry( n-1, from, by, to ); перенесиОдинДиск( from, to ); carry( n-1, by, to, from ); } Вызов: carry( n, 0, 1, 2 ); n - сколько дисков перенести (n > 0). from - откуда (номер стержня). to - куда. by - при помощи (промежуточный стержень).

n дисков потребуют (2**n)-1 переносов.

Напишите программу, ищущую выход из лабиринта ("червяк в лабиринте"). Лабиринт загружается из файла .maze (не забудьте про расширение табуляций!). Алгоритм имеет рекурсивную природу и выглядит примерно так:

#include <setjmp.h>

jmp_buf jmp; int found = 0; maze(){ /* Это головная функция */ if( setjmp(jmp) == 0 ){ /* начало */ if( неСтенка(x_входа, y_входа)) GO( x_входа, y_входа); } } GO(x, y){ /* пойти в точку (x, y) */ if( этоВыход(x, y)){ found = 1; /* нашел выход */ пометить(x, y); longjmp(jmp, 1);} пометить(x, y); if( неСтенка(x-1,y)) GO(x-1, y); /* влево */ if( неСтенка(x,y-1)) GO(x, y-1); /* вверх */ if( неСтенка(x+1,y)) GO(x+1, y); /* вправо */ if( неСтенка(x,y+1)) GO(x, y+1); /* вниз */ снятьПометку(x, y); } #define пометить(x, y) лабиринт[y][x] = '*' #define снятьПометку(x, y) лабиринт[y][x] = ' ' #define этоВыход(x, y) (x == x_выхода && y == y_выхода) /* можно искать "золото": (лабиринт[y][x] == '$') */ неСтенка(x, y){ /* стенку изображайте символом @ или # */ if( координатыВнеПоля(x, y)) return 0; /*край лабиринта*/ return (лабиринт[y][x] == ' '); }

Отобразите массив лабиринт на видеопамять (или воспользуйтесь curses-ом). Вы увидите червяка, ползающего по лабиринту в своих исканиях.


Используя библиотеку termcap напишите функции для:


  • очистки экрана.
  • позиционирования курсора.
  • включения/выключения режима выделения текста инверсией.


© Copyright А. Богатырев, 1992-95
Си в UNIX

| |


Содержание раздела