#1B коротко о главном

Вступление

Здравствйте!
Сегодня мы поговорим (вернее я попишу - вы почитаете :) о графике. Это будет очень короткое встпление. Однако иметь какое то представление об этом вопросе необходимо.

Новости сайта

[07.02.03] В разделе Компиляторы появился компилятор Dev-Pascal v.1.9.2.

Теория

До этого наши программы работали в текстовом режиме. Кроме него существует ещё и графический. В текстовом режиме вам доступны только 256 символов псевдографики (как бы графики), которые вы можете отобразить на экране. Этих символов достаточно для простых графических работ, например: нарисовать таблицу, написать карточную игру, оболочку (типа Norton Commander), многоконный тестовый редактор (взять даже BP) и для много другого. Графический же режим характеризуется возможностью задавать цвет любой точки экрана. И соответственно мы можем рисовать сложные геометрические фигуры.

Для работы с графикой в комплект поставки BP входит модуль graph, в котором содержатся основные функции и процедуры. Так же для работы нужны специальные графические драйвера, которые тоже входят в BP и находятся в папке BP\BGI. Кстати модуль graph называется библиотекой BGI (Borland Graphic Interface).

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

uses Graph;

var
  grDriver: Integer;
  grMode: Integer;
  ErrCode: Integer;
begin

  grDriver := Detect;
  InitGraph(grDriver, grMode,'');
  ErrCode := GraphResult;

  if ErrCode <> grOk then
  begin  
      Writeln('Graphics error:');
     Writeln(GraphErrorMsg(ErrCode));
     Writeln('Program aborted...');
     Halt(1)
  end

   ..... do something ...
    Readln;
    CloseGraph
end.
ну что неплохое начало :) Эта программа "ничего" не делает. Просто переходт в графический режим, ждёт нажатия клавиши и выходит назад в текстовый. Теперь давайте разберём, что же мы тут использовали:
procedure InitGraph(var GraphDriver:Integer; var GraphMode: Integer; PathToDriver: string);
процедра инициализации графики. Т.е. по просту говоря для перехода в графический режим. GraphDriver - графический драйвер, которым мы хотим пользоваться. Мы задаём этому параметру значение Detect - авто определение(т.е. программа сама определит наилучший режим). Для современной техники это 640x480x16 :)
GraphMode определяет графический режим, т.к. у нас стоит автоопределение он определится сам (это и будет режим 640х480 16 цветов).
PathToDriver - путь к файлу драйвера. Если путь не задаётся, как это сделали мы, то драйвер ищётся в текущем каталоге программы. Если использовать автоопределение, то нам нужен файл egavga.bgi из папки bp\bgi.

На всякий случай, вдруг кто не знает скажу кое-что об обозначениях что значит 640х480х16 - то точная характеристика графического режима: (число точек по горизонтали)х(число точек по вертикали)х(количество цветов). Т.е. 640 точек по горизонтали, 480 по вертикали и 16 цветов.

В интернете сейчас можно найти версии файлов *.bgi для работы в более лучших режимах. Однако я всё же буду рассказывать про стандартную поставку, т.к. углубляться в графику мы пока особо не будем. Всему своё время.

Следующая функция возвращает нам резльтат предыдущей функции или процедуры, если возникла ошибка:

function GraphResult: Integer;
Если возникла ошибка (например при инициализации графики программа не нашла файл с драйвером), то возвращаемое значение не равно grOk. Расшифровку ошибки можно получить вызвав:
function GraphErrorMsg(ErrorCode: Integer): string;
возвращает строку, содержащую содержание ошибки, заданной переменной ErrorCode. Не помню писал ли я про halt, так что если повторюсь, то ничего:
procedure Halt ( Exitcode: Word );
останавливает выполнеие программы и передаёт управление операционной системе. Если параметр Exitcode = 1 значит это завершение программы с ошибкой. Ну и последняя процедура:
procedure CloseGraph;
переходит назад в текстовый режим. Обратите внимание на вызов ReadLn перед вызовом CloseGraph. Зачем мы это делаем? Если опустить этот вызов, то после выполнения программы сразу произойдёт выход в текстовый режим и соответственно экран очистится. Что бы посмотреть результаты выполнения программы мы вынужденны ожидать нажатия клавиши. Если вы думаете, что это сложно, то вы ошибаетесь, что бы создать простое пустое окошко в программе для Windows надо написать раза в три-четыре больше :) Этот код будем считать каркасом наших будующих графических программ.

Теперь самое время поговорить о координатах. Всё дело в том, что в компьютерах координатные оси направленны по другому. Для примера: мы пользуемся декартовой системой координат ( © by Рене Декарт :), центром которой является точка (0,0) опять же извиняюсь за убогость, но картинку не приаттачить:

        ^ y
        |
        |
        |
--------+------->
        |      x
        |
        |
В компьютерах же используется несколько другая система
 +----------------------->
 |                      x
 |
 |
 |
 |
 |
 |
 V y
Как видите в ней отсутствуют отрицательные числа и ось ординат (y) направленна вниз. Вызвано это следующей причиной. Разрешение экрана определяют в пикселях. Пиксель (сокращение от Picture Element - элемент картинки) - это минимальная логическая точка (или если хотите единица) экрана. Т.е. разрешению экрана 800х600 означает, что по оси Х у нас максимально 800 пикселей, а по оси Y - 600. Один пиксель может состоять из множества физических точек (т.е. точек подсветкой которых можно управлять аппаратно). Так вот левому верхнему углу экрана соответствеут пиксель с координатами (0,0), а правому нижнему в зависимости от разрешения. Если оно 800х600 тогда (799, 599). Пиксель - это всегда целое положительное число. У вас не может быть 1/3 пикселя или пиксель с координатой (-5, -5). Так как мы не можем посветить на экране 1/3 пикселя и мы не можем высветить пиксель (-5, -5) т.к. это выпадает за границу экрана. Кстати количесто цветов на экране определяется количеством бит видеопамяти, отводимой под 1 пиксель. Так если под один пиксель отводится 1 байт (8 бит) тогда это режим с 256 цветами (напомню, что столько может быть различных комбинаций бит в байте).

Теперь рассмотрим самую важную процедуру в графике - подсветка пикселя:

procedure PutPixel(X, Y: Integer; color: Word);
соответственно х, у - координаты. Color - цвет. Кстати о цветах. Для первых 16 цветов введены специальные константы. Ниже приводится таблица соответствия цветов и номеров (для текстовой версии к сожалению графа пример останется незаполненной, т.к. нет такой возможности):

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

поэтому putpixel (10, 10, 15) и putpixel (10, 10, WHITE) произведёт одинаковый эффект. Таким образом мы можем рисовать любые фигуры, используя посветку точек на экране. Однако в BGI есть специальные процедуры для рисования графических примитивов - линий, окружностей, прямоугольников, вывода текста, работы с изображениями и многим другим. К сожалению в формат выпусков не влезет описать все возможности BGI, да многое и не понадобится. Поэтому проведу обзор самых нужных функций и процедур.

function GetPixel(X,Y: Integer): Word;
возвращает цвет точки с координатами х,у
procedure Line(x1, y1, x2, y2: Integer);
рисует линию от точки (x1,y1) к точке (x2, y2). Цвет линии устанавливается с помощью следующей процедуры:
procedure SetColor(Color: Word);
устанавливает текущий цвет для рисования графических объектов. По умолчанию цвет белый.
procedure SetBkColor(ColorNum: Word);
устанавливает цвет фона, по умолчанию цвет фона чёрный. Цвет фона - это цвет экрана на котром мы рисуем. Что-то типа цвета рабочего стола в Windows :)
procedure Circle(X,Y: Integer; Radius: Word);
рисует окружность с центром в точке (x,y) и радиусом Radius. Цвет окружности задаётся с помощью SetColor.
procedure Ellipse(X, Y: Integer; StAngle, EndAngle: Word; XRadius, YRadius: Word);
рисует элипс с центром в точке (x,y), полу-осями xradius, yradius, от угла stangle до угла endangle. Угол считается против часовой стрелки (т.е. 00 - 3 часа, 900 - 12 часов). Цвет задаётся с помощью SetColor.
procedure Bar(x1, y1, x2, y2: Integer);
рисует закрашенный прямоугольник. левый верхний угол (х1,у1) - правый нижний (х2, у2). Стиль кисти и цвет закраски определяется следующей процедурой:
procedure SetFillStyle(Pattern: Word; Color: Word);
устанавливает текущий стиль кисти (pattern) и её цвет (color). Цвет, который задаётся setcolor и setfillstyle не совпадают и применяются к разным графическим объектам. Первый к графическим примитивам, второй к закрашенным объектам. Существует несколько предопределённых стилей, которые приведены в нижеследующей таблице:
Констнанта Численное значение Описание
EMPTY_FILL 0 Закрашивает цветом фона
(несмотря на цвет, который вы указали в setfillstyle)
SOLID_FILL 1 Равномерная закраска
LINE_FILL 2 Закрашивает ---
LTSLASH_FILL 3 Закрашивает ///
SLASH_FILL 4 Закрашивает ///, толстые линии
BKSLASH_FILL 5 Закрашивает \\\, толстые линии
LTBKSLASH_FILL 6 Закрашивает \\\
HATCH_FILL 7 В "клеточку"
XHATCH_FILL 8 В клеточку под углом
INTERLEAVE_FILL 9 Чередование
WIDE_DOT_FILL 10 Широко расположенные точки
CLOSE_DOT_FILL 11 Близко расположенные точки
USER_FILL 12 Стиль определять вам!
что бы лучше понять напишите программу, которая бы в цикле выводила прямоугольники и меняла стиль, тогда вы поймёте что к чему.
procedure FillEllipse(X, Y: Integer; XRadius, YRadius: Word)
рисует закращенный элипс с центром (х,у) и полу-осями xradius, yradius. Стиль и цвет определяются Setfillstyle
procedure ClearDevice;
очищает экран (заполняет его цветом фона).
procedure OutTextXY(X,Y: Integer; TextString: string);
выводит строку textstring, начиная с точки (х,у). В графическом режиме для вывода нужно обязательно использовать эту процедуру.
procedure GetImage(x1, y1, x2, y2: Integer; var BitMap);
копирует картинку в память. Картинка определяется координатами (x1, y1) - левый верхний угол; (x2, y2) - правый нижний угол. BitMap - это область куда мы должны копировать картинку. Размер области памяти складывается из количества необходимого под картинку + 6 байт. Два первых слова этих байт хранят ширину и высоту картинки, третье слово зарезервировано и зачем оно нужно это охраняемая тайна компании Borland :)
procedure PutImage(X, Y: Integer; var BitMap; BitBlt: Word);
соответственно обратная процедура. Рисует изображение из BitMap на экран, начиная с координат (х,у). BitBlt - определяет способ вывода картинки. Ниже даны его возможные значения:
Константа Численное значение Смысл
NormalPut 0 Копирует изображение на экран (цвет каждого пикселя сохраняется)
XORPut 1 Результат получается как XOR, применённый к точке на экране и точке изображения
OrPut 2 как OR, применённый к точке на экране и точке изображения
AndPut 3 как AND, применённый к точке на экране и точке изображения
NotPut 4 Цвет каждого пикселя инвертируется (заменяется на противоположный)
function ImageSize(x1, y1, x2, y2: Integer): Word;
эта функция используется, что бы вычислить размер памяти BitMap в процедуре GetImage, т.е. размер необходимый под сохранение области + 3 слова.
function GetMaxX: Integer; / function GetMaxY Integer;
возвращает максимальное значение Х / Y координаты.
procedure FloodFill(X, Y: Integer; Border: Word);
закрашивает область, которая содержит точку (x,y) и ограничена кривой с цветом Border. Стиль и цвет определяются Setfillstyle
это далеко не полный список процедур и фунций из библиотеки BGI. Что бы понять как они работают лучше всего, что бы вы попробовали вызвать их поочереди и посмотреть что будет. Кстати хороший пример работы этих и других функций и процедур находится в программе BP\EXAMPLES\DOS\BGI\bgidemo.pas. Сейчас я как раз разберу пример от туда. Это пример с НЛО :) запустите и посмотрите, так как надо объяснять пользуясь картинкой. Если НЛО летает медленно, тогда в самом конце измените задержку (например на delay (1000) - в 10 раз быстрее) так как на моём P4 за НЛО не уследить :) этот параметр в примере равен кстати 70 :)
program demo;

uses Crt, Graph;

const
  r  = 20;
  StartX = 100;
  StartY = 50;

var
  grDriver, grMode, ErrCode : integer;
  MaxX, MaxY  : word;
  Saucer    : pointer;
  X, Y      : integer;
  ulx, uly  : word;
  lrx, lry  : word;
  Size      : word;
  I         : word;

procedure MoveSaucer(var X, Y : integer; Width, Height : integer);
var
  Step : integer;
begin
  Step := Random(2*r);

  if Odd(Step) then
    Step := -Step;

  X := X + Step;
  Step := Random(r);

  if Odd(Step) then
    Step := -Step;

  Y := Y + Step;

  if X > MaxX then
     X :=  MaxX
  else
    if (X < 0) then
       X := 0;
  if Y > MaxY then
      Y := 1
  else
      if Y < 0 then
        Y := 0
end;

begin
     grDriver := Detect;
     InitGraph(grDriver, grMode,'');
     ErrCode := GraphResult;
     if ErrCode <> grOk then
     begin
       writeLn ('Graphics error:', GraphErrorMsg(ErrCode));
       halt (1)
     end;

  ClearDevice;

  MaxX := getmaxx;
  MaxY := getmaxy;

{ рисуем НЛО }
  Ellipse(StartX, StartY, 0, 360, r, (r div 3)+2);
  Ellipse(StartX, StartY-4, 190, 357, r, r div 3);
  Line(StartX+7, StartY-6, StartX+10, StartY-12);
  Circle(StartX+10, StartY-12, 2);
  Line(StartX-7, StartY-6, StartX-10, StartY-12);
  Circle(StartX-10, StartY-12, 2);
  SetFillStyle(SolidFill, WHITE);
  FloodFill(StartX+1, StartY+4, GetColor);

{ вычисляем границы прямоугольника в который вмещается НЛО }
  ulx := StartX-(r+1);
  uly := StartY-14;
  lrx := StartX+(r+1);
  lry := StartY+(r div 3)+3;

  Size := ImageSize(ulx, uly, lrx, lry);
  GetMem(Saucer, Size);
  GetImage(ulx, uly, lrx, lry, Saucer^);
  PutImage(ulx, uly, Saucer^, XORput);

{ рисуем звёздное небо :) }
  for I := 1 to 1000 do
    PutPixel(Random(MaxX), Random(MaxY), random (WHITE));

  X := MaxX div 2;
  Y := MaxY div 2;

  repeat
    PutImage(X, Y, Saucer^, XORput);
    Delay (10000);
    PutImage(X, Y, Saucer^, XORput);
    MoveSaucer(X, Y, lrx - ulx + 1, lry - uly + 1);
  until KeyPressed;

  FreeMem(Saucer, size);

  ReadLn;
  closegraph
end.
Сначала одна неизвестная до этого момента функция
function Odd(X: Longint): Boolean;
проверяет является ли число нечётным. и возвращает true в случае успеха.

Так вот думаю вы ужу понаблюдали за НЛО, теперь разберём как это происходит. Процедура MoveSaucer не осуществляет ничего сверх естественного и просто изменяет координаты НЛО, которые хранятся в глобальных переменных Х,Y. На ней подробно останавливаться думаю не стоит. Так же я не буду комментировать процесс рисования объекта, так как это просто последовательный вызов графических функций безо всяких алгоритмов. Я бы хотел обратить ваше внимание на следующие строки:

Size := ImageSize(ulx, uly, lrx, lry); - вычисляем память, которая нам нужна для хранения картинки НЛО
GetMem(Saucer, Size); - выделяем эту память
GetImage(ulx, uly, lrx, lry, Saucer^); - захватываем изображение НЛО в буффер Saucer.
PutImage(ulx, uly, Saucer^, XORput); - стираем НЛО
тут придётся вспомнить логические операции (как неужели вы их забыли !!!). Если забыли, тогда вернитесь к выпуску "#0E А сила - она, брат, в правде". Так вот что будет, когда мы число xor'им само с собой (a xor a) ??? Правильно 0. То же самое происходит и когда мы вызываем PutImage(ulx, uly, Saucer^, XORput). Ведь в Saucer у нас находится изображение НЛО, когда же мы накладываем изображение само на себя используя операцию xor, то получается 0, т.е. изображение цвет всех точек которого равен 0. А так как цвет фона у нас чёрный (0) то получается эффект стирания, а не перерисовки. Продолжим:
repeat
PutImage(X, Y, Saucer^, XORput); - рисуем НЛО
Delay (10000); - пауза
PutImage(X, Y, Saucer^, XORput); - стираем
MoveSaucer(X, Y, lrx - ulx + 1, lry - uly + 1); - двигаем НЛО
until KeyPressed;
вроде бы ничего сложного, но задумайтесь на минутку почему остаются звёзды ? Давайте поставим задержку (delay) совсем большой и присмотримся к НЛО .... через него просвечивают звёзды! (что бы лучше это рассмотреть увеличте количество звёзд ). Всё из-за того что способ применённый здесь не является правильным. Хотя согласитесь рассмотреть эти маленькие точки тяжело и искажения не очень заметны.

Теперь рассмотрим внимательнее исходник. Когда мы рисуем НЛО мы опять применяем операцию xor - поэтому если под НЛО находится звёздочка, то она просвечивает, причём с искажением цвета. Однако когда мы стираем НЛО мы вновь применяем xor и НЛО стирается, а звездочке возвращается свой "первозданный" цвет. Давайте обратимся к цифрам. Пускай у нас будет звезда жёлтого (14) цвета.

14 = 1110b
1111b xor 1110b = 0001b - синий цвет. это в момент рисования.
0001b xor 1111b = 1110b - жёлтый. в момент стирания. Как говорят математики ЧТД.
Данную анимацию можно применять для взаимно обратных цветов, т.е. по цветам с одинаковым отступом от концов палитры (палитра - набор цветов, каждому цвету соответствует номер в палитре, помните таблицу констант?). Такими цветами и являются: чёрный-белый (0 -15), синий-жёлтый (1-14) и т.д. Про правильную анимацию я раскажу как нибудь в другой раз.

Задание

Сегодня на дом вы получите стандартную школьную задачу: построить график функции. Давайте для примера возьмём х3 или sin или кому что нравится (роли это не играет). Естественно график должен быть нарисован в декартовой системе координат (лучше для проверки нарисовать и сами оси). Что бы сразу направить вас по верному пути скажу: задумайтесь как можно сделать так, что бы точка с вещественными координатами отобразилась на экране. Если возникнут затруднения - моя почта внизу.

Решение

Возвращаемся к выпуску #19. Тогда вам на домашнее рассмотрение было предложено написать процедуру для удаления текущего элемента. Ответов (и вопросов) по этому поводу к сожалению не было. Так что вот мой текст с комментариями (на входе list - текущий элемент списка):

procedure Delete (list : p2list);
var
  old : plist;
begin
  if list^ = nil then
    exit;{ список пуст на выход }

  old := list^;
  list^^.prev^.next := list^^.next;
  list^^.next^.prev := list^^.prev;

  if list^^.next <> nil then
    list^ := list^^.next
  else
    list^ := list^^.prev;

  Dispose (old)
end;
что бы удалить элемент нам надо изменить указатель next предыдущего на следующий и указтель prev следующего на предыдущий. Т.е. как бы вычеркнем элемент из списка. После этого мы можем спокойно освободить занимаемаю им память.

Обратите внимание на условия проверки list^^.next. Зачем мы это делаем? Давайте вспомним, что на входе у нас list - это текущий (т.е. удаляемый элемент). Поэтому нам надо изменить list, так как элемент мы удалим, а если list будет указывать на него, то список потеряется. Тут и возможны 4 случая: текущий элемент первый, текущий элемент из середины (они подчиняются list^^.next <> nil), текущий элемент последний (случай else) и текущий элемент единственный (это подходит под любой случай - всё равно надо присвоить nil).


[Назад] [Содержание] [Дальше]
Hosted by uCoz