Для кого предназначена эта статья: статья поможет программистам-новичкам постичь азы работы в Delphi, также походу создания проекта познакомит с некоторыми важнейшими понятиями, ну и, конечно, даст начальные знания по работе с графикой. На примере создания примитивной игры мы научимся:

  • создавать новый проект
  • добавлять несколько модулей (unit) в проект
  • создавать классы и их наследников
  • переопределять конструктор и деструктор нашего класса
  • загружать графические файлы в память и рисовать на форме
  • управлять картинками на форме, двигая их в нужном направлении

Шаг 1. Создаем новый проект

Порядок дела не портит (пословица)

Для начала, настоятельно советую вам сделать отдельную папку для ваших работ в Delphi. На вашем любимом диске или в "моих документах" создайте пустую папку, назовем ее, допустим, "delphi_projects". В этой папке на каждый ваш проект будете создавать подпапку, где и будут расположены все файлы очередного проекта, а также все вспомогательные файлы, которые потребуется вам в процессе работы над ним. У меня получилось так: "C:\delphi_projects\001_blocks" (blocks - игра которую мы запрограммируем).

Открываем нашу среду разработки (в моем случае это "Все программы - Borland Delphi 7 - Delphi 7"). После окрытия программа автоматически открывает новый проект, можно сразу его сохранять в нашу, пока еще пустую, папку "001_blocks", но мы поступим по другому. Дело в том, что автоматически открывшийся на экране проект, это один из видов доступных проектов, чтобы получше разобраться сделаем следующее:

  • закроем автоматически открытый проект: File - Close All (отказываемся от сохранения)
  • без помощи программы создадим новый проект нужного нам типа File - New - Other

Можно было бы сразу выбрать File - New - Application (именно "приложение" необходимо нам для нашей задумки, т.к. рисовать планируется на форме), но мы зайдем в "Other", чтобы убедиться, что "Application" это не единственное что можно создать, пробежимся по некоторым пунктам:

  • Application - создается новый проект оконного (windows) приложения, в который автоматически добавляется одна форма (Form1), а также один модуль (Unit1) для описания поведение формы и остальной логики программы (процедуры, переменные, классы и пр. будут написаны в нем). После компиляции (Ctr+F9), в папке вашего проекта появится исполняемый файл (*.exe), который и будет является созданной вами программой.
  • DLL Wizard - проект, в котором описываются функции библиотеки, после компиляции вы не увидите исполняемых файлов (*.exe), зато в папке с проектом появится библиотека (*.dll), которую можно подключать к другому проекту (загружать в память и использовать ее функции) и даже использовать при разработке в других системах, на C++ и прочих языках.
  • Form - создается форма и юнит, если вызвать в уже открытом проекте Application, то в проект добавится еще одна независимая форма, которую можно вызвать из первой, главной формы Form1. Если же вызвать этот пункт "с чистого листа", то откроется форма, которую можно редактировать и описывать поведение (добавлять процедуры, функции, кнопки и т.д.), кнопка компиляции и запуска "F9"  в таком случае будет недоступна, такую форму нельзя использовать самостоятельно, но можно подключить к другому проекту типа "Application".
  • Unit - создается новый программный модуль, аналогично пункту "Form", если вызвать в уже открытом проекте - добавится к проекту еще один модуль (в котором можно описать какие-то дополнительные функции, переменные или классы), если же вызвать отдельно от другого проекта, то ссылку на модуль можно потом добавить к любому проекту, т.е. использовать единожды написанные процедуры многократно в разных проектах, просто указывая в них ссылку на этот модуль.
  • Console Application - проект консольного приложения, в отличие от обычного в нем нет привычных для нас форм с кнопками, при запуске такого приложения открывается черное окно, в котором может выводится (или не выводиться) результат работы программы. Ярким примером консольной утилиты является программ windows\system32\ping.exe (проверка сетевого соединения с другим компьютером\хостом по IP-адресу, например проверка наличия интернет-соединения), если запускать ее двойным кликом - она что-то быстро сообщает и закрывается, чтобы узнать ее поближе, можно запустить ее через Пуск-Выполнить-CMD, далее набрать текст "ping ya.ru" и нажать Enter, на экране появится результат работы программы ping.exe. Если требуется создать какое-то легкое нетребовательное приложение, от которого требуется выполнить несколько функций, не требующих участия пользователя (или минимальное его участие), то Console Application это как раз то что нужно.

И так, т.к. нам необходимо приложение (exe), у которого есть форма (на ней будем рисовать), то уже осознанно выбираем "File - New - Other - Application" после чего выбираем "File - Save All" и сохраняем в нашу папку "001_blocks".

Теперь можно посмотреть из каких файлов состоит проект, зайдем в папку "001_blocks", в ней находится 3 файла, каждый из которых по своему важен, все файлы текстовые и отлично открываются с помощью блокнота (правой - открыть с помощью - бокнот), обязательно откройте все три:

  • Project1.dpr - файл проекта, в нем вы увидите код который отвечает за запуск нашего приложения, этот код можно редактировать также как и код в Unit1, но мы пока не будем этого делать. Особое внимание привлекает раздел "uses" где перечислены все программные модули, которые используются в данном приложении, именно так можно подключить любой модуль из любого каталога в проект, это можно сделать как вручную, так и через меню "Project - Add to project". Причем если модуль находится по известному пути (пути настраиваются в настройках Delphi), то путь к модулю указывать не надо, иначе надо указать абсолютный или относительный путь к папке с файлом модуля.
  • Unit1.dfm - файл, который содержит информацию о форме (в текстовом виде), которую мы создали и видим на экране. Когда мы размещаем на форме новый объект, например, кнопку, то в файле появляется соответствующая запись об этом объекте и его координаты на форме.
  • Unit1.pas - файл модуля "Unit1", в нем и будет содержаться основная часть нашего кода, большинство процедур и функций. Модулей может быть больше, сколько будет необходимо, но не следует создавать их просто для того чтобы были, для простых проектов, как правило, весь код размещают в одном Unit. Название модуля, как правило, заменяют на что-то более осмысленное, например, "Graphics" или "Services" и т.п.

При открытии проекта (например, через двойной клик по файлу Project1.dpr), все наши файлы подгружаются в Delphi. Файл dfm мы видим виде формы, похожей на уже рабочее приложение. На самом деле это не что иное, как инструмент скоростного "визуального" построения интерфейса программы. Визуальным механизм назвали из-за того, что мы сразу видим как будет выглядеть программа после компиляции, нам не надо вслепую менять файл dfm, вместо этого мы накидываем мышью на наш "муляж" необходимое нам количество компонент (кнопок, панелей, закладок) и мышью растягиваем их до нужного размера. Файл pas мы видим на экране в таком же текстовом формате, в нем и будем прописывать что и как должна делать программа (внимание, если вы закрыли модуль, то открыть его можно через Ctr+F12, советую привыкнуть сразу к этой комбинации, если забудете сочетание клавиш, то можно использовать меню View-Units). Файл dpr, как правило, при открытии проекта на экране отсутствует, но его легко можно вызвать через то же сочетание Ctr+F12.

Шаг 2. Подготавливаем форму

Все параметры формы (ширина, высота, цвет и т.д.), а также ее отдельных элементов (кнопок, панелей и т.д.) меняются через одну из спец-форм Delphi - "Object Inspector" (ObjIns). В ObjIns вы можете поменять любое доступное на изменение свойство выделенного в данный момент объекта (активным объект делает клик левой кнопки мыши) или самой формы (для того чтобы сделать активной саму форму, надо кликнуть в свободное от кнопок\панелей место); например, клик по кнопке на форме покажет все ее свойства, большинство из которых можно поменять: доступность(Enabled), ширина(Width), высота (Height), заголовок (Caption) и т.д..

Также с помощью ObjIns можно описывать поведение формы при наступлении какого-то события (Event), например можно написать код который будет выполняться при открытии формы: делаем активной форму, в ObjIns переходим на закладку Events, ищем нужное событие "OnCreate", двойным кликом в пустом окне рядом создаем новую процедуру, Delphi автоматически создаст процедуру и переместит курсор на нее в вашем Unit. Остается написать код, который будет выполняться при открытии формы, например, инициализировать какие-нибудь переменные. Важно, что интерактивность формы, ее ответ на наши движения мыши, клики и т.д., как раз и является обработкой соответствующих событий. Если поместить на форме кнопку, то при нажатии на нее ничего не будет происходить до тех пор, пока вы не пропишите код в соответствующей процедуре, в данном случае в Events надо выбрать событие OnClick, будет создана процедура, которая будет срабатывать при нажатии на кнопку.

Для начала сделаем форму недоступной для изменения размера. Наша игра [подобия тетрис] будет рисоваться в окне размером 600х600px, поэтому мы зарезервируем себе квадрат такого же размера, с помощью 2-х свойств формы ClientHeight и ClientWidth. После изменения этих 2-х параметров, у нас автоматически изменятся 2 других параметра Height и Width (общая высота и ширина формы, с учетом системных панелей и окантовки), которые мы и введем как максимальные \ минимальные, чтобы избежать ненужного нам уменьшения или увеличения нашей формы.

С помощью свойства caption зададим имя нашей формы, например, "Blocks".

Кинем на форму два компонента с палитры вверху (левой кнопкой мыши на панель, а затем на нашу форму):

  • Timer (закладка System): понадобится нам чтобы оживить нашу игру, таймер будет через указанное время запускать процедуру, которая будет двигать фигурки).
  • XPManifest (закладка Win32): необязательный компонент, позволяет вашей форме не отличаться от других форм в WinXP и старше, использует новые стили.


Готово, нашу программу уже даже можно запустить (F9), правда она еще ничего не делает, пока это пустышка, заготовка для нашей программы.

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

Нажимаем F12, чтобы перейти из формы в Модуль, где и будем писать наш код (можно перейти Ctr+F12 или через меню View - Units).

Шаг 3. Знакомство с классами

Немного отклонимся от нашей основной задачи, т.к. нам необходимо познакомится с "классами" и их экземплярами "объектами". Для того чтобы задействовать новый проект, на примере которого мы рассмотрим классы, на время закройте наш проект Blocks, скачайте новый проект по ссылке, создайте в "delphi_projects" новую подпапку и распакуйте туда архив. Двойным кликом по файлу проекта "Project1.dpr" откройте его в Delphi.

Для начала познакомимся с аббревиатурой ООП - объектно ориентированное программирование. ООП следует рассматривать как еще одну ступень над процедурной техникой программирования, ООП придумали для облегчения построения сложных программ и взаимодействия различных ее частей м\у собой, а также для увелечения стабильности работы и максимального исключения ошибок программистов. Со стороны пользователя программа написанная с использованием ООП, скорей всего не будет отличаться от программы написанной без него, а вот если заглянуть "внутрь", в исходные коды, то можно увидеть различие в реализации.

Основная идея ООП - представить все сущности с которыми придется работать программе как "объекты", у которых есть свои свойства и методы. Таким образом, создав необходимое количество объектов разных типов, мы управляем ими через их свойства и методы, в отличие от процедурного подхода, где меняются объявленные глобальные переменные с помощью несвязанных напрямую процедур и функций. Например, нам необходимо создать простую программу, с помощью которой пользователь будет рисовать на экране фигуры [круг, квадрат], разного цвета и с разной толщиной линии, причем можно удалять уже нарисованные фигуры в разной последовательности, возможные реализации:

  • Процедурный подход: объявить массив чисел A1[N,2], значение каждого [а,1] = тип фигуры, [а,2] = ссыка на элемент (b), элементы хранить в двух других массивах (B1,B2 - по одному массиву на тип фигуры). В каждом массиве Bn (по типу фигуры) элемент будет содержать [b,1]= координату X, [b,2]=координату Y, [b,3]=цвет фигуры и т.д. Далее объявляем для каждого типа фигуры свою процедуру рисования и последовательно обходя общий массив A1 - рисуем объекты нужной нам функцией. При работе пользователя одна общая процедура управляет массивом A1, добавляя в него и удаляя из него элементы, далее перебирает все элементы из него и вызывает по типу фигуры нужную процедуру рисования.
  • ООП: объявить класс TFigure, который будет содержать общие свойства всех фигур (координаты, цвет, длина(радиус)), а также общий метод Paint(), с помощью которого объект данного класса будет рисовать себя на экране. Создать два класса-наследника от TFigure - TCircle, TSquare, которые автоматически получат все общие свойства от базового класса TFigure, переопределить метод Paint(), для каждого из классов он будет свой, объект класса TCircle будет рисовать на экране круг радиуса n, а Tsquare при вызове того же метода Paint() нарисует квадрат с длиной стороны n. Далее создаем класс, который будет иметь методы а) добавить новый объект на форму б) удалить в) перерисовать все, допустим с именем TMyObjects. При работе программы каждое действие пользователя будет вызывать один из методов TMyObjects (см. скачанный пример).

Чем же хорош ООП?

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

Три базовых понятия для ООП:

  • Полиморфизм - возможность переопределять методы базового класса в наследниках. В нашем примере метод Paint() был объявлен в родительском классе TFigure как процедура которая должна что-то рисовать, но, что особенно важно, никакого кода описывающего процесс рисования мы не писали. Затем в наследниках базового класса мы объявили этот же метод (процедуру с таким же названием) и написали реализацию метода, причем код в методе у каждого класса TCircle, TSquare, TTriangle был уже свой (у первого рисование круга, у второго квадрата и т.д.).
  • Наследование - возможность создавать классы на основе других классов. В примере это, например, TCircle на базе TFigure.
  • Инкапсуляция - скрытие внутренней структуры объекта, возможность изменить его поля только через методы, а также хранение данных рядом с кодом манипулирующим этими данными. В примере это, например, метод установки координат для объекта Circle1.Coordinates(X,Y), мы не имеем прямого доступа к X и Y.

Как это работает в Delphi? Когда структура вашей программы мысленно подготовлена (можно воспользоваться листом бумаги, чтобы набросать объекты и их взаимодействие), мы начинаем объявлять "классы", добавляя в наш модуль сроку подобные этой: "type TFigure = class":

type
  TFigure = class {базовый класс, содержит общие свойства и методы}
  private
    {Private declarations }
    FX,FY:integer; // поля координаты
  public
    { Public declarations }
    procedure SetColor(c:integer);
    property X : integer read FX write FX; // Свойство координата X
    property Y : integer read FY write FY; // Свойство координата Y
  end;
  • "type" - означает что мы объявляем новый тип переменной, наш собственный (в отличие от, допустим, Integer, который является предопределенным).
  • " = class" - говорит о том, что наш собственный тип это класс объектов, свойства и методы которого будут описаны ниже.
  • вслед за class может в скобках указываться базовый класс, свойства которого надо перенять, если же ничего не указано, то класс сам является базовым и никаких свойств методов из "родителя" не перенимает, т.е. является корнем; на самом деле наш базовый класс как и все другие является потомком класса TObject, так что type TFigure = class это тоже самое type TFigure = class(TObject).
  • ниже под описанием класса идут три секции с разным уровнем доступа private, protected, public (если вы пишите класс вручную, то необходимо вписать их вручную, хотя это и необязательно в общем случае, тогда ваши свойства и методы будут считаться public). Как правило, поля и внутренние функции прячут в разделе private, а то что должно "выглядывать" и быть доступным другим объектам/программистам в секцию public.
  • замыкает класс ключевое слово end;

Сначала объявляем базовый класс, который содержит свойства характерные для всех объектов этой группы "TFigure"

type
  TFigure = class {базовый класс, содержит общие свойства и методы}
    ....
  end;

затем объявляем классы-наследники, которые будут уточнять поведение наших объектов

type
  TCircle = class(TFigure)
  private
    { Private declarations }
  public
    { Public declarations }
    procedure Paint(Canvas:TCanvas);override; {переопределяем базовый метод}
  end;

type
  TSquare = class(TFigure)
  private
    { Private declarations }
  public
    { Public declarations }
    procedure Paint(Canvas:TCanvas);override; {переопределяем базовый метод}
  end;

После объявления типов в верхней части модуля переходим ниже к секции "implementation", где и объявим необходимое количество объектов после зарезервированного слова Var

.... {выше находятся описания классов}
var
  Form1: TForm1;
  MyObjects:TMyObjects;
implementation
.... {ниже будет код процедур, функций}

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

Объявить переменные [MyObjects: TMyObjects;] не достаточно, надо их инициализировать, выделить память, поэтому перед использованием переменной нашего типа надо вызвать так называемый "конструктор" [MyObjects := TMyObjects.Create;]. Если в нашем объекте его полями (свойствами) являются объекты других типов, то и полям потребуется перед использованием конструктор.

procedure TForm1.FormCreate(Sender: TObject);
....
begin
  ....
  MyObjects := TMyObjects.Create;
  ....
end;

После того как объект не нужен (например, при завершении работы программы), мы должны освободить память, вызвав "деструктор" класса [MyObjects.Free], здесь как и при создании надо освободить память для полей, но в обратном порядке сначала все поля (если такие были), а затем деструктор для самого объекта.

procedure TForm1.FormDestroy(Sender: TObject);
begin
  ....
  MyObjects.Free;
  ....
end;

После создания объекта, мы управляем им, вызывая через точку после имени объекта один из его методов [MyObjects.AddFigure(..)]. Если у объекта есть "свойства", то они также доступны через точку после имени объекта [ArrObj[i].n:=n;]. Замечание: если объект построен не совсем верно, то вместо "свойств" в секции public могут быть объявлены сами "поля" ("свойство" это ссылка на "поле", через свойство меняется значение поля) и их (поля) можно менять напрямую (не через свойства).

Чем примечателен наш алгоритм (код из Unit1):

  • Наличие четкой структуры, весь код разбит деревом объектов на несколько частей.
  • Данные хранятся рядом с процедурами использующими эти данные, например координаты объекта "зашиты" в объект и процедура рисования paint() для объекта использует их как локальные переменные, у каждого объекта в одних и тех же переменных X,Y заданы свои числа.
  • Самое важное что открыв его через год, можно с легкостью разобраться в работе программы и расширить ее функционал, добавив еще несколько подклассов (допустим треугольник и ромб) и описав только лишь одну процедуру Paint() для новых подклассов. Более того добавить еще одно свойство ко всем объектам любого подкласса элементарно просто - прописать его в базовом классе TFigure, всего одной строчкой!

Что еще можно сделать? Можно сохранить описание классов в отдельном Unit, в отдельной папке. Создав новый проект можно подключить этот модуль в него в секции uses (вначале надо подключить этот модуль к проекту, а потом прописать в Unit1 в разделе Uses название модуля "Figures") и с легкостью оперировать объектами или новыми подклассами для TFigure или TCircle. Это важный механизм в работе программиста, при задумке нового проекта надо стараться, по возможности, разрабатывать классы так, чтобы ими можно было воспользоваться в других проектах, тогда скорость разработки может увеличиться в несколько раз (вообще при работе в Делфи программист постоянно использует классы созданные разработчиками и включенными в поставку). Скачайте еще один пример по ссылке, в Unit1 задействован собственный модуль Figures.

Свой "многоразовый" модуль "Figures", файл Figures.pas:

unit Figures; {здесь можно задать любое имя для своего модуля}

interface

uses
  SysUtils, Variants, Classes, Graphics;

{ниже наши собственные классы}
type
  TFigure = class {базовый класс, содержит общие свойства и методы}
....
implementation
....


модуль Unit1, в котором задействован свой модуль "Figures", посмотрите название модуля есть в секции uses:

unit Unit1;

interface

uses {в этой секции можно задействовать любой модуль, в т.ч. свой - Figures!}
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, Figures, StdCtrls, ExtCtrls, Buttons;

type
  TForm1 = class(TForm)
  ....

 

А вот так выглядит готовая программа:

Шаг 4. Знакомимся с графикой

Для рисование на экране компьютера в Delphi существует несколько способов. На самом деле все они сводятся в конце концов к изменению состояния памяти видеокарты вашего компьютера, содержимое которой и отображает монитор компьютера. Понятно что чем ниже уровень (чем мы ближе к видеокарте), тем быстрей будет приложение.

Итак перечислим возможные способы вывода изображения на экран:

  • Использовать скоростную, низкоуровневую работу напрямую с видеопамятью с помощью специальных инструментов, варианты:
    • Использовать OpenGL (Open Graphics Library — открытая графическая библиотека): графическое API*, набор библиотек, а также вспомогательных модулей (*.pas), для построения и вывода двухмерных и трехмерных сцен.
    • Использовать DirectX (точнее его часть DirectDraw или Direct2D): это также графическое API*, но уже разработанное Microsoft, набор библиотек, а также вспомогательных модулей (*.pas), для скоростной прорисовки графики.
    • Использовать что-то более экзотическое подобное вышеуказанным API: Glide,QuickDraw 3D,SDL,WebGL.
  • Использовать механизм предоставляемый Windows, так называемый GDI (Graphic Device Interface - интерфейс графических устройств), набор библиотек являющимися частью windows, с помощью функций (появляются при подключении к проекту windows.pas) можно рисовать графические примитивы, в этом варианте 3D и высокая скорость отсутствует.
  • Использовать специальный механизм предоставляемый Delphi, свойство Canvas (класс TCanvas), которое по сути является еще одной надстройкой над GDI, но вместо того чтобы вызывать напрямую процедуры из модуля windows.pas, мы имеем "объект", который обладает всеми преимуществами ООП (хранит в себе координаты, поверхность на которой рисуем, цвет линии и заливки и т.д.).

*API (application programming interface) - набор готовых структур, процедур и функций предоставляемых приложением или библиотекой для использования во внешних программных продуктах.

Схема работы с видеопамятью
Приложение может вывести что-то на экран как стандартно, через интерфейс предоставляемый Windows (левая часть), так и миную его, используя специальные библиотеки (правая часть).

Самый простой способ рисования в Delphi - использовать свойство визуального объекта (например, у Form1) с именем "Canvas" (в переводе "Холст"). Это свойство позволяет довольно легко вывести на многих поверхностях различные изображения и текст. Рисовать можно на Form, Image или PaintBox, т.е. на любом объекте, у которого в свойствах есть "Canvas", узнать есть или нет холст легко - достаточно написать имя объекта в какой-нибудь процедуре и поставить точку в появившемся списке свойств начать набирать "canv..". Наш пример будет использовать как раз метод рисования на "холсте", рисование с OpenGL и DirectX будем осваивать в другой статье.

Свойства Canvas:

  • Brush - кисть, является объектом со своим набором свойств:
    • Bitmap - картинка размером строго 8x8, используется для заполнения (заливки) области на экране.
    • Color - цвет заливки.
    • Style - предопределенный стиль заливки; это свойство конкурирует со свойством Bitmap - какое свойство Вы определили последним, то и будет определять вид заливки.
    • Handle - данное свойство дает возможность использовать кисть в прямых вызовах процедур Windows API .
  • ClipRect - (только чтение) прямоугольник, на котором происходит графический вывод.
  • CopyMode - свойство определяет, каким образом будет происходить копирование (метод CopyRect) на данную канву изображения из другого места: один к одному, с инверсией изображения и др.
  • Font - шрифт, которым выводится текст (метод TextOut).
    • Name - имя шрифта.
    • Color - цвет шрифта.
    • Size - размер шрифта.
    • Handle - данное свойство-указатель используется для прямых вызовов Windows API.
  • Pen - карандаш, определяет вид линий, как и кисть (Brush), является объектом с набором свойств:
    • Color - цвет линии.
    • Mode - режим вывода: простая линия, с инвертированием, с выполнением исключающего или и др.
    • Style - стиль вывода: линия, пунктир и др.
    • Width - ширина линии в точках.
    • PenPos - текущая позиция карандаша, карандаш рекомендуется перемещать с помощью метода MoveTo, а не прямой установкой данного свойства.
  • Pixels - двухмерный массив элементов изображения (pixel), с его помощью Вы получаете доступ к каждой отдельной точке изображения.

Методы Canvas:

  • Ellipse - Рисует эллипс, вписанный в невидимый квадрат с координатами верхнего левого угла и правого нижнего. Если координаты х и y у углов будут совпадать, то получится круг [Canvas.Ellipse(0,0,70,70);].
  • FillRect - Заполняет прямоугольник цветом текущей кисти (brush), но никак не за пределами него [Canvas.FillRect( Bounds(0,0,100,100) );].
  • FloodFill - Заполняет данную область цветом текущей кисти, до тех пор пока не будет достигнут край [Canvas.FloodFill(10, 10, clBlack, fsBorder);]
  • Rectangle - Рисует прямоугольник (или квадрат), заполненный цветом текущей кисти и обрамлённый цветом текущего пера [Canvas.Rectangle(Bounds(20, 20, 50, 50));].
  • RoundRect - Тоже, что и Rectangle, но с загруглёнными углами.
  • TextOut - Рисует заданную строку на холсте, начиная с координат (x,y), фон текста заполняется текущим цветом кисти [Canvas.TextOut(10, 10, 'Пример текста');].
  • CopyRect - Копирует прямоугольную область с какого-нибудь холста на текущий [Canvas.CopyRect(Rect1Dest, Canvas2, Rect2Source);], где Rect1Dest - прямоугольник на текущем холсте, куда будет скопирован с Canvas2 прямоугольник Rect2Source.

Т.к. canvas является свойством с типом TCanvas, а это "класс объектов", то работа с ним абсолютно ничем не отличается от работы с нашими собственными объектами, разница лишь в том что все методы и свойства уже существуют, нам остается только вызывать их (методы), а на поверхности (холсте) будут появляться изображения. Например, чтобы нарисовать прямо на форме прямоугольник, надо вызвать метод его "холста" [Form1.Canvas.Rectangle(X1,Y1,X2,Y2);]

Открою вам очень интересную особенность, которая поможет вам в работе в Delphi не один раз. Если в модуе (Unit) нажать на метод объекта Ctrl + левой кнопкой мыши, то вы перейдете в связанный модуль где увидете метод "изнутри" в виде обычной процедуры. Например, в описанном выше случае, перескочите в модуль Graphics.pas, на процедуру

procedure TCanvas.Rectangle(X1, Y1, X2, Y2: Integer);
begin
  Changing;
  RequiredState([csHandleValid, csBrushValid, csPenValid]);
  Windows.Rectangle(FHandle, X1, Y1, X2, Y2);
  Changed;
end;

Далее таким же способом кликнув по Windows.Rectangle мы переходим уже в методы Windows.pas на

function Rectangle; external gdi32 name 'Rectangle';

что и является доказательством что метод холста всего лишь "обертка" для метода WinApi GDI (см. способы рисования выше), т.е. в итоге будет вызвана функция с именем 'Rectangle' из библиотеки gdi32.dll, неполенитесь и найдите эту библиотеку в папке Windows. Заглянуть в эту функцию мы уже не можем, т.к. библиотека (*.dll) это уже скомпилированный продукт.

Подытожим: рисовать на холсте можно двумя способами:

  • Рисовать встроенными методами холста: Ellipse, Rectangle и т.д.
  • Выводить изображения, ранее загруженные из файлов, методом холста Draw(X, Y: Integer; Graphic: TGraphic). Первые два параметра это координаты на холсте, третий - объект содержащий в себе картинку, например может быть типа TBitmap.

Чтобы загрузить картинку в память можно воспользоваться объектом типа TBitmap, вызвав у него метод LoadFromFile(ИмяФайла). Пример:

BitmapBackground := TBitmap.Create; 
BitmapBackground.LoadFromFile('.\resources\fon.bmp');

Спрайты - персонажи или элементы игры, которые перемещаются по экрану, по сути спрайт это прямоугольное изображение, которое рисуется поверх фонового изображения, в определенной координате x,y на экране компьютера. Спрайт может создавать иллюзию подвижности и изменения формы, подобно настоящим 3D объектам, получить данный эффект можно если использовать не одно изображение персонажа, а сразу несколько, попеременно выводя их на экран один за другим, подобно кадрам в кино (как правило вместе с этим еще и меняют его координаты отностительно фонового изображения).

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

Всем извествен формат *.gif для хранения графических файлов. Этот формат, по своей спецификации, поддерживает хранение нескольких спрайтов внутри себя, а также интервал, через который один спрайт сменяет другой. На примере анимированных картинок gif, можно рассмотрим анимацию поближе:

Анимация содержит 36 кадров
Анимация содержит всего лишь 3 кадра
Анимация содержит 9 кадров

Как достигается эффект? Сохраните картинку на компьютер (через правую клавишу мыши) и откройте ее с помощью какого-нибудь специализированного редактора, например "Adobe ImageReady" или "Easy GIF Animator". 

Вы обнаружите, что файл это контейнер, содержащий несколько изображений, выстроенным в определенном порядке, подобно кадрам на пленке. У каждого изображения существует параметр "задержка" в ms, этот параметр определяет сколько времени текущий кадр будет отображаться на экране.

Основная идея покадрового вывода совпадает и для игр на спрайтах, различие будет состоять в том, что программисту самому надо выстраивать ленту из связанных изображений. Кадры сохраняются как правило в одном файле bmb, хранить множество кадров в отдельных файлах представляется неразумным. Для настоящей 2d графики кадры можно подготовить в специализированных программах, таких как 3DMax. - пример простенького файла, для анимации "человек ввид сверху", резкий розовый цвет выбран не случайно, этим цветом будет регулироваться прозрачность краев спрайта, вокруг человечка в итоге будет фоновая картинка, а не розовый квадрат.

Шаг 5. Продолжаем работу над проектом Blocks, вооружившись классами

Основная идея игры - есть поле размером 600х600px, левая часть активная, по ней двигаются фигурки-блоки, состящие из нескольких подблоков квадратной формы, правая часть - информативная, на ней рисуется следующая фигурка и ведется подсчет очков. Новая фигура появляется в центре игрового поля вверху, следующая фигура рисуется справа в специально отведенном месте. Каждые N секунд основная фигура падает вниз до тех пор, пока не встретит на своем пути препятсвие в виде блока или конца игровой поверхности.

Чтобы облегчить вычисления мысленно разобъем нашу игровую поверхность на ячейки, если ячека непустая - вней будет находится цветной квадратик, если пустая - просто фоновое изображение. Заранее определив что ячейка должна быть размером 30х30, мы получаем игровое поле уже не в пикселях, а в наших условных координатах в высоту это будет 20 (0..19), в ширину 12 (0..11) ячеек. Для того чтобы получить физическое расположение на экране достаточно условную единицу умножить на размер ячейки, допустим, условная координата x,y=5,16 на форме мы разместимся в точке x,y=5*30px,16*30px. Заметьте, что все координаты считаются от верхнего левого угла формы, т.е. изображение будет выведено на холсте с отступом от верхнего края на 480 пикселей и от левого края 150 пикселей.

Каким образом хранить в программе координаты всех ячеек: для этого отлично подходит двухмерный массив array[0..19,0..11] чисел, в каждой ячейке будет храниться 0 если блока в ней нет и 1 если есть, мы пойдем дальше и вместо 1 сохраним цвет блока числом, значит если значение в массиве больше нуля то блок по этой координате есть, а цифра в этой ячейке обозначает условный цвет.

type
  TField = class(TParentField) //класс наследник, фиксированные блоки
    Field: array[0..19,0..11] of byte; //поле действий, если ячейка не 0, то в ней будет блок
    .....
  end;

Как на рисовать красивый фон и блоки: Вариант1 - воспользоваться стандартными средствами Delphi, рисовать с помощью функций Canvas.Rectangle(). Вариант2 - рисовать заранее подготовленные в любом графическом редакторе картинки, сохраненные в виде файлов. Мы воспользуемся вторым вариантом. Файлы мы загружаем в заранее объявленные переменные типа TBitmap в начале работы программы:

procedure TForm1.FormCreate(Sender: TObject);
var
i1: integer;
jpeg: TJPEGImage;
begin
  ...
  BitmapBackground := TBitmap.Create; //создаем объект который будет хранить картинку
  //BitmapBackground.LoadFromFile('.\resources\fon.bmp');  //вариант загрузки из bmp-файла
  jpeg := TJPEGImage.Create;
  jpeg.LoadFromFile('.\resources\fon.jpg'); //вариант загрузки из jpg-файла
  BitmapBackground.Assign(jpeg);
  ...
end;

Для квадратиков объявлен массив BitmapFig, в котором элементы также типа TBitmap [BitmapFig: array[0..5] of TBitmap;]. Можете сами нарисовать 6 картинок 30х30 (блоки разных цветов) и одну 600х600 (фон) и сохраниь их в файлы bmp или воспользоваться файлами из примера. В процедуре ниже проверяется, если в массиве, которое содержит состояние нашего игрового поля, в ячейках число не ноль, то надо рисовать изображение блока (цветного квадрата), сам вывод на форму происходит в самописной функции "DrawBMP()".

procedure TField.Paint(Canvas:TCanvas);
var
i1,i2:integer;
begin
  for i1:= 0 to Yf do
    for i2 := 0 to Xf do
    begin
      if Field[i1,i2] > 0 then
      begin
        DrawBMP(Canvas,i2*30,i1*30,BitmapFig[Field[i1,i2]-1]);
      end;
    end;
end;

В функцию DrawBMP передаются как параметры процедуры:

  • Canvas - холст, пришел как переменная текущей процедуры Paint
  • X,Y=[i2*30,i1*30] - координаты куда выводить блок
  • Bitmap [BitmapFig[Field[i1,i2]-1]] - Картинка размером 30*30, берется из массива BitmapFig по номеру цвета (в массиве счет от нуля, поэтому -1)

Как рисовать текст: то же самое что и с блоками или выводим функцией Canvas.TextOut() или выводим подготовленные заранее красивые буквы, рисовать картинки вместо букв, надо признаться, дело не совсем благодарное, может быть существует более элегантное решение, но в нашем примере решено "в лоб" - создали единую картинку со всеми цифрами, сохранили в файл, загрузили в переменную типа TBitmap, далее получаем из большой картинки прямоугольный кусочек и выводим на экран. TRect - специальный тип для хранения размеров прямоугольника, частенько используется для получения прямоугольной области из какого-нибудь изображения.

  //выводим уровень
  str1 := IntToStr(level);
  for i1 := 1 to length(str1) do
    begin
      i2 := StrToInt(str1[i1])*20;
      R_old := Rect(455+(i1-1)*20,480,455+i1*20,505);
      R_source := Rect(i2,0,i2+20,25);
      Canvas.CopyRect(R_old,BitmapFont.Canvas,R_source);
    end;

  //выводим очки
  str1 := IntToStr(score);
  for i1 := 1 to length(str1) do
    begin
      i2 := StrToInt(str1[i1])*20;
      R_old := Rect(455+(i1-1)*20,507,455+i1*20,532);
      R_source := Rect(i2,0,i2+20,25);
      Canvas.CopyRect(R_old,BitmapFont.Canvas,R_source);
    end;

Как оживить программу, сделать автоматическое движение фигуры вниз на 1 у.е. каждые N секунд: необходимо разместить на форме Timer и в инспекторе объектов на закладке Events, двойным кликом мыши добавить новый обработчик события. Эта процедура будет срабатывать через каждые N секунд, в ней будет всего одна строка - вызов метода Move у объекта-фируры AFigure_1.

procedure TForm1.Timer1Timer(Sender: TObject);
begin
  AFigure_1.Move(Canvas,0,1,true);
end;

Замечу, что кроме таймера есть еще как минимум 2 способа вызывать периодчески функцию обновления экрана, три способа заставить фигурки двигаться:

  • Закинуть на форму Timer и использовать событие OnTimer (закладка events в ObjIns)
  • «Подвесить» на Application.OnIdle, перерисовка будет вызываться в момент простоя приложения, причем очень часто, можно пропускать некоторое количество вызовов или использовать sleep().
  • Создать поток TThread, рисование пойдет в отдельном потоке от основной программы.

Вариант с таймером разобран выше, рассмотрим вариант с OnIdle:

Новая процедура по шаблону:

procedure TForm1.Tic(Sender: TObject; var Done: Boolean);
begin
  ...
  // Сюда заносим, что надо исполнять.
  ...
  Done := false;
end;

Назначаем событию процедуру:

procedure TForm1.FormCreate(Sender: TObject);
begin
  ...
  Application.OnIdle := Tic;
end;

Вариант с потоком TThread:

TGameRead=class(TThread) // класс для таймера игры
protected
  procedure Execute;override; // Запуск
  procedure Tic; // Один тик программы
end;

объявляем переменную:

var
...
T1:TGameRead;
...

описываем процедуры класса:

procedure TGameRead.execute;
begin
  repeat
  synchronize(Tic);
  until Terminated
end;
procedure TGameRead.Tic;
begin
  ...
  // здесь будет выполняться прорисовка
  ...
end;

создаем поток в процедуре Form1.Create:

  ...
  T1:=TGameRead.Create(false); // Создаем поток
  T1.Priority:=TpHighest; // Ставим приоритет
  ... 

в конце работы программы удаляем потоки:

procedure TForm1.FormDestroy(Sender: TObject);
begin
  T1.Suspend;// Приостановим и освободим память
  T1.Free;
end;

Переходим к применения ООП, с какими объектами мы имеем дело:

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

Насчет 2,3 сразу напрашивается вывод что это объекты одного типа, насчет первого - массив из объектов того же типа, последний - похож на "текущую" фигуру, но имеет размер больший, по размеру игрового поля.

Итого имеем два типа: тип "фигура" (назовем TAFigure) и тип "фигура сложенная из зафиксированных" (TField). По классу TAFigure мы создадим два объекта - один Obj_1 будет текущей фигурой (та, что управляет пользователь на игровом поле), второй объект Obj_2 - фигура которая расположена в спец. окне справа как "следующая". По классу TField создадим всего один экземпляр Obj_3, который будет накапливать в себе уже упавшие на поле боя фигурки. Все возможные виды фигурок, из которых в случайном порядке будет выбираться "следующая" будем хранить в еще одном объекте Obj_4 типа "Массив фигур по типу TAFigure" (TAFigureArray).

Итак разобрались с объектами на бумаге, заходим в Delphi в начало модуля и начинаем объявлять классы. Классы TAFigure и TField имеют схожие черты, то создадим для них базовый класс TParentField, который соберет все общее, а именно а)Координаты x,y б)Процедуру очистки Clear() (виртуальная, опишем в TAFigure и TField) в)Процедуру рисования одного квадратика DrawBmp() г)Процедура рисования всех непустых квадратиков для текущего объекта Paint() (виртуальная, опишем код ниже в классах TAFigure и TField).

;virtual;abstract; - специальные директивы, не что иное как управляющие конструкции, которые позволяют нашим методам использовать два преимущества ООП. Первое virtual говорит о том что метод может быть "перекрыт" в потомке, т.е. код процедуры из родительского класса не будет использован, если в наследнике есть процедура с таким же именем (рядом с процедурой в наследнике должна быть директива override), пример полиморфизма. Второе abstract позволяет нам вообще не писать код для процедуры в родительском классе, если не предполагается использование объектов базового класса на прямую, то и писать код не зачем, пример как программист абстрагируется от конкретных объектов, предполагая уточнения в поведении объекта в подчиненных классах.

*Директива virtual семантически эквивалентна dynamic, первая оптимизирована для скорости (VMT), последняя для памяти (DMT).

....
type TArrayByteFig = array[0..3,0..3] of byte; //вспомогательный тип-массив

type
  TParentField = class //базовый класс
  x,y:integer; //координаты левого верхнего угла
  procedure Paint(Canvas:TCanvas);virtual;abstract;
  procedure Clear();virtual;abstract;
  procedure DrawBmp(Canvas:TCanvas;x,y:integer;BMP:TBitmap);
  end;

type
  TField = class(TParentField) //класс наследник, фиксированные блоки
    Field: array[0..19,0..11] of byte; //поле действий, если ячейка не 0, то там есть блок
    procedure Paint(Canvas:TCanvas);override;
    function CheckingFull(var Ym: integer): boolean;
    procedure MoveUnit(Ym: integer);
    procedure PaintLevelStore(Canvas:TCanvas);
    procedure Clear();override;
  end;
  
type
  TAFigure = class(TParentField) //класс наследник "фигура"
    Field: array[0..3,0..3] of byte; //фигура, если ячейка не 0, то там есть блок
    procedure Turn(Canvas:TCanvas);
    procedure Move(Canvas:TCanvas;Xn,Yn:integer;fixed:boolean);
    function MoveCheck(m_arr: TArrayByteFig;x,y:integer):boolean;
    procedure Paint(Canvas:TCanvas);override;
    procedure NextFigure(f:TAFigure);
    procedure IniFigure();
    procedure Clear();override;
    procedure Load(s0, s1, s2, s3: string);
  end;

type
  TAFigureArray = class //еще базовый класс, для хранения массива всевозможных фигурок
    FigureArray: array[0..10] of TAFigure;
    Constructor Create;
    Destructor Destroy;
  end;

const Yf=19;
const Xf=11;

var
  Form1: TForm1;
  AFigureArray: TAFigureArray; //все фигуры
  AFigure_1: TAFigure; //активная фигура
  AFigure_2: TAFigure; //следующая фигура
  mField: TField; //игровое поле
  BitmapFig: array[0..5] of TBitmap; //цветные блоки
  BitmapBackground: TBitmap; //фон
  BitmapFont: TBitmap; //цифры для очков
  score: integer; //очки
  level: integer; //уровень

implementation
....​
 

Сразу же переходим к реализации класса TAFigure, координаты он наследует от базового класса, значит в свойствах объекта уже будет присутсвовать x,y, объявлять их не надо. Свойство "Field" - массив, в котором каждая непустая ячейка будет нарисована на экране ввиде одного квадратика. Далее объявляем необходимое для управления фигурой количество свойств (= процедур). Фигура у нас будет

  • падать вниз, двигаться влево или право - объявим ей метод(процедуру) Move(Canvas:TCanvas;Xn,Yn:integer;fixed:boolean), где Canvas - поверхность на которой нарисуем фигуру, Xn,Yn это количество условных единиц на которые фигура сдвигается в одну из сторон, например, Xn=-1 будет означать что она перепрыгнет влево на 1у.е. (30px), fixed - если yes, то при невозможности движения фиксируем фигуру и начинаем падение новой.
  • поворачиваться при нажатии клавиши "вверх" - объявим ей метод(процедуру) Turn(Canvas:TCanvas).
  • принимать новый облик (новый вид фигуры) - методы NextFigure, IniFigure.
  • загружать описание вида фигуры (инициализация всех фигур при создании программы) - метод Load.

После создания описания всех методов нажимаем на одном из них ctr+shift+C чтобы одним махом создать заготовки для наших процедур.

Остается только описать поведение объектов в каждой из объявленных процедур, дело техники. Исходный код и приложение доступны для скачивания по ссылке1 и ссылке2.

Остановимся на некоторых важных моментах по классам:

  • Любой объект в программе создается и уничтожается в четыре этапа:
    1. объявляется новый пользовательский тип type TMyType = class(...)
    2. объявляется переменная(ые) нашего типа var mObject1,mObject2:TMyType;
    3. объекты инициализирутся с помощью метода класса mObject1 := TMyType.Create;, этот метод особый, называется конструктор
    4. после завершения работы с объектом освобождаем память методом класса mObject1.Destroy;, это тоже особый метод, называется деструктор, на практике не надо вызывать деструктор напрямую, используйте метод mObject1.Free; который сам сделает все как надо (в том числе он вызывает деструктор)
  • Все важные поля классов рекомендуется объявлять в private и скрывать за свойствами, которые являются "прослойками" для полей. Сами поля желательно называть аналогично свойствам с приставкой F, например, координаты хранятся в полях FX,FY а запись возможна через свойства (=ссылки на поля) X,Y. Надо признаться, во втором примере у меня не хватило сил на это :).
  • Конструктор и деструктор можно переопределять как другие методы, но следует обратить внимание на директиву Inherited, которую надо вызывать в конструкторе перед выполнением вашего кода, а деструкторе - после выполнения вашего кода. Эта директива вызывает родительский метод, что позволяет правильно создать и уничтожить объект, особенно касается занимаемой памяти!

Несколько замечаний по графике:

  • Чтобы избежать мелькания на экране надо пользоваться двумя инструментами: 1) стараться не перерисовывать весь экран при любом изменении в игре, если вы используете только одну поверхность, являющуюся рабочей 2) намного лучше использовать двойной буфер - рисовать сначала на поверхности в памяти (на объекте TBitmap), а затем всю готовую картинку выводить на холст (рабочий canvas).
  • Если вы хотите создавать настоящие качественные игры, то после написания 2-3х реально работающих программ с использованиям холста (canvas) переходите на изучение DirectX или OpenGL. Для несложных графических элементов подойдет и Canvas.
  • Вторым шагом после освоения материала в этой статье можно перейти к изучению возможности функции BitBlt(), с помощью нее можно выводить изображения задействуя "маску", чтобы края спрайта были прозрачными, а также получить прирост в производительности.