Windows API и объектно–ориентированное программирование — КиберПедия 

Общие условия выбора системы дренажа: Система дренажа выбирается в зависимости от характера защищаемого...

Кормораздатчик мобильный электрифицированный: схема и процесс работы устройства...

Windows API и объектно–ориентированное программирование

2020-03-31 359
Windows API и объектно–ориентированное программирование 0.00 из 5.00 0 оценок
Заказать работу

 

Методы разделения процессора, применяемые разными реализациями Windows, интересно рассмотреть в их развитии — от простого к сложному. Так в идеальной однозадачной среде, приложение, раз начавшись, выполняется без перерывов до полного завершения. В истинно многозадачной среде приложение выполняется за много шагов, причем само приложение не знает, когда его прервут для обработки других приложений — этим ведает только система[4].

Промежуточным решением является среда, получившая название псевдомногозадачной (невытесняющая многозадачность, non–preemptive multitasking). В такой среде, подобно однозадачной, система не прерывает выполнения приложения. Однако само приложение должно быть разделено на небольшие, быстро выполняемые фрагменты. После выполнения такого фрагмента система может перейти к выполнению другого приложения. При этом приложение само уведомляет систему, где ее можно прервать для выполнения других задач.

В Windows 3.x это может быть реализовано двумя разными методами:

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

· можно воспользоваться специальной функцией, передающей управление системе, и возвращающей его назад приложению после обработки других приложений. Таких функций в Windows 3.x две — Yield и DirectYield. Однако этот путь используется в очень специальных случаях, например при разработке отладчиков, из–за довольно жестких ограничений на применение этих функций.

При написании нормальных приложений для Windows 3.x разбиение программы на отдельные функции производится не механически, скажем через 100 строк, а функционально — каждая функция выполняет определенные действия. При этом система, вызывая соответствующую функцию, передает ей некоторые данные, которые указывают, что надо сделать.

Это очень важный момент.

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

Например, при необходимости ввода данных с клавиатуры, программа включала в себя вызов к операционной системе (или BIOS, на худой конец), который и возвращал требуемые данные.

Еще раз: обычная программа генерирует вызовы к операционной среде для получения и вывода данных: алгоритм управляет данными

В рассмотренном нами случае получается совершенно иная ситуация: поступающие от системы данные управляют поведением программы. Часто такими данными являются управляющие воздействия пользователя (например, изменение размеров окна, вызов меню и др.). Эти воздействия, вообще говоря, не синхронны с работой вашей программы, то есть получается, что данные управляют алгоритмом — один из основных принципов объектно–ориентированного программирования (ООП).

Введем новые понятия:

· данные, передаваемые от системы к соответствующей функции называются сообщением (message).

· процесс обращения к требуемой функции называется посылкой (post) или передачей (send) сообщения.

· функция, обрабатывающая сообщения, называется процедурой обработки сообщений (message handler).

Таким образом, когда вы создаете программу, работающую в псевдомногозадачной среде (здесь: Windows 3.x), вы должны написать требуемые процедуры обработки сообщений. Далее Windows будет передавать вашим процедурам сообщения для их обработки.

С точки зрения ООП все объекты должны обладать 3мя свойствами:

инкапсуляция — объединение в единое целое алгоритмов и необходимых данных;

наследование — возможность порождения новых объектов, основываясь на существующих, наследуя их свойства;

полиморфизм — разность реакций на одинаковые воздействия; наследники одного объекта могут отличаться своими свойствами друг от друга и от предка.

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

Обычно говорят, что процедура обработки сообщений определяет свойства объекта, так как задает реакцию этого объекта на воздействия (сообщения). Именно с такой трактовкой объекта возникли первые языки ООП.

В Windows объектом ООП является окно. Соответственно говорят, что сообщения направлены не процедуре, а окну. Процедура обработки сообщений определяет окно с конкретными свойствами, даже больше — одна процедура может обслуживать несколько разных окон, но тогда эти окна будут иметь одинаковую реакцию на одинаковые воздействия. То есть процедура обработки сообщений определяет не одно окно, а целый класс (class) окон.

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

Таким образом псевдомногозадачный метод разделения процессора оказался основой для построения объектно–ориентированной среды и попутно перевернул всю привычную нам философию написания программ — мы теперь создаем не управляющий алгоритм, а набор процедур, обеспечивающий реакцию нашего окна (то есть нашей программы) на внешние события.

Обработка сообщений является очень распространенным способом организации ООП–библиотек или ООП–языков. Существенное отличие (причем не в лучшую сторону) Windows 3.x заключается в том, что обработка сообщений является методом разделения процессора в псевдомногозадачной среде. Так как система не прерывает выполнение приложения в процессе обработки сообщения, то его обработка не должна занимать много времени

Это сильно затрудняет применение Windows 3.x для расчетных задач — либо мы должны их выполнить быстро, либо разбить на быстро выполняемые части, и понемногу обрабатывать по мере получения сообщений. Понятно, что обычно приходится разбивать на части, а это существенно замедляет вычисления. Вообще говоря, при обработке сообщения лучше укладываться в интервал менее 1 секунды, что бы задержка в реакции Windows на управляющие воздействия не была очень большой; критичной является задержка порядка 1–2 минуты — при этом Windows 3.x может просто дать сбой или зависнуть (что очень сильно зависит от наличия других работающих приложений).

Win32 API

 

В более сложном Win32 API применяется так называемая истинная многозадачность (вытесняющая, preemptive multitasking). В этом случае разделение процессора осуществляется по определенным временным интервалам (квантам времени). Обработка сообщений перестала быть методом разделения процессора, и в процессе обработки сообщения система может передавать управление другим приложениям. Сама же идея применения объектно–ориентированного подхода к окнам осталась неизменной.

Однако надо отметить, что реализация истинной многозадачности оказалась неполной. В рамках Win32 API могут работать как настоящие Win32 приложения, так и их 16ти разрядные собратья, написанные для Windows API. При запуске таких 16ти разрядных приложений под Win32 для них запускается специальная виртуальная 16ти разрядная Windows–машина, причем в Windows–95 для всех 16ти разрядных приложений используется одна общая виртуальная машина. Это значит, что истинная многозадачность реализована только между Win32 приложениями, в то время как 16ти разрядные приложения между собой используют обработку сообщений для разделения отведенного им процессорного времени. В случае Windows NT для каждого 16ти разрядного приложения запускается собственная Windows–машина, что позволяет им разделять процессор общим способом с приложениями Win32.

Истинная многозадачность в Win32 позволила реализовать так называемые многопотоковые приложения (multithread application). При этом выделяют два новых понятия — процесс (proccess) и поток (thread). Процессы в Win32 API примерно эквивалентны приложениям в Windows API. Для каждого процесса выделяются определенные системные ресурсы — адресное пространство, приоритеты и права доступа к разделяемым ресурсам и прочее, но не процессорное время. Процесс только лишь описывает запущенную задачу, как она есть, без непосредственных вычислений. Для разделения процессора используются не процессы, а потоки, которым и выделяется процессорное время. В рамках каждого процесса выделяется свой поток, называемый первичным (primary thread), создаваемый по умолчанию при создании процесса. При необходимости в пределах одного процесса может быть создано много потоков, конкурирующих между собой (и с потоками других процессов) за процессорное время, но не за адресное пространство.

Как написать приложение для Windows

 

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

Каждое приложение открывает по меньшей мере одно окно (в принципе могут существовать приложения вообще без окон, но как небольшие специализированные процедуры, не требующие никакого управления). Свойства окна определяются процедурой обработки сообщений этого окна. Таким образом, что бы определить свойства нужного окна, надо написать процедуру обработки сообщений, посылаемых этому окну (оконную процедуру или оконную функцию — window procedure, она же процедура обработки сообщений, message handler).

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

Далее необходимо предусмотреть средства для создания и отображения окна зарегистрированного класса. С таким окном пользователь будет работать — передвигать его по экрану, изменять размеры, вводить текст и т.д. Вам необходимо обеспечить реакцию этого окна (то есть вашего приложения) на действия пользователя. Фактически вы должны запустить механизм, обеспечивающий доставку сообщений, адресованных вашему окну, до получателя — оконной процедуры. Этот механизм должен работать, пока работает ваше приложение. Такой механизм называется циклом обработки сообщений (message loop).

Таким образом вы должны выполнить несколько шагов для создания собственного приложения:

· написать оконную функцию;

· зарегистрировать эту функцию (класс) в Windows, присвоив классу уникальное имя;

· создать окно, принадлежащее данному классу;

· обеспечить работу приложения, организовав цикл обработки сообщений.

Чуть подробнее рассмотрим, что происходит с приложением за время его “жизни” — от запуска до завершения — перед тем, как перейдем к рассмотрению конкретного примера.

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

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

 

#define WM_MOVE  0x0003
#define WM_SIZE     0x0005

 

В большинстве случаев названия сообщений начинаются на WM_, однако названия некоторых сообщений имеют префиксы BM_, EM_, LBM_, CBM_ и другие.

Для начала выделим четыре сообщения, с которыми мы будем знакомится первыми. Это сообщения применяются при создании окна (WM_CREATE), при закрытии[5] (WM_DESTROY и WM_QUIT) и при его перерисовывании (WM_PAINT).

В тот момент, когда приложение создает новое окно, оконная процедура получает специальное сообщение WM_CREATE, информирующее окно о его создании. При этом окно создается с помощью вызова специальной функции (CreateWindow, CreateWindowEx и некоторые другие), которая выполняет все необходимые действия; сообщение при этом имеет лишь “информационный” характер — оно информирует окно о том, что его создают. Однако реальное создание происходит не в обработчике этого сообщения, а в той функции, которую вызвали для создания окна.

На сообщении перерисовки окна WM_PAINT надо остановиться чуть подробнее. Дело в том, что какая–либо часть окна может быть скрыта от пользователя (например, перекрыта другим окном). Далее в процессе работы эта часть может стать видимой, например вследствие перемещения других окон. Сама система при этом не знает, что должно быть нарисовано в этой, ранее невидимой части окна. В этой ситуации приложение вынуждено позаботиться о перерисовке нужной части окна самостоятельно, для чего ему и посылается это сообщение каждый раз, как видимая область окна изменяется.

Когда окно закрывается, оно получает сообщение WM_DESTROY, информирующее о закрытии окна. Как и в случае создания, сообщение о закрытии является информационным; реальное закрытие осуществляется специальной функцией (обычно DestroyWindow), которая, среди прочего, и известит окно о его уничтожении.

Все время, пока пользователь работает с приложением, работает цикл обработки сообщений этого приложения, обеспечивающий доставку сообщений окнам. В конце работы приложения этот цикл, очевидно, должен завершиться. В принципе, можно сделать так, что бы в цикле проверялось наличие окон у приложения. При закрытии всех окон цикл тоже должен завершить свою работу. Однако можно несколько упростить задачу — и в Windows именно так и сделано — вместо проверки наличия окон можно предусмотреть специальный метод завершения цикла при получении последним окном (обычно это главное окно приложения) сообщения о его уничтожении (WM_DESTROY). Для этого применяется специальное сообщение WM_QUIT, которое посылается не какому–либо окну, а всему приложению в целом. При извлечении этого сообщения из очереди цикл обработки сообщений завершается. Для посылки такого сообщения предусмотрена специальная функция — PostQuitMessage.

После завершения цикла обработки сообщений приложение уничтожает оставшиеся ненужные объекты и возвращает управление операционной системе.

Сейчас в качестве примера мы рассмотрим простейшее приложение для Windows, традиционную программу “Hello, world!”. После этого подробнее рассмотрим, как это приложение устроено. Здесь же можно заметить, что при создании практически любых, написанных на “C”, приложений для Windows этот текст может использоваться в качестве шаблона.

Пример 1A — первое приложение

 

Файл 1a.cpp

#define STRICT
#include <windows.h>

#define UNUSED_ARG(arg) (arg)=(arg)
static char szWndClass[] = "test window";

LRESULT WINAPI _export WinProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
UNUSED_ARG(wParam);
UNUSED_ARG(lParam);
PAINTSTRUCT ps;

switch (uMsg) {
case WM_CREATE:
      return 0L;

case WM_PAINT:
      BeginPaint(hWnd, &ps);
      TextOut(ps.hdc, 0, 0, "Hello, world!", 13);
      EndPaint(hWnd, &ps);
      return 0L;

case WM_DESTROY:
      PostQuitMessage(0);
      return 0L;

default:
      break;
}
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

static BOOL init_instance(HINSTANCE hInstance)
{
WNDCLASS        wc;

wc.style =       0L;
wc.lpfnWndProc = WinProc;
wc.cbClsExtra =   0;
wc.cbWndExtra =      0;
wc.hInstance =     hInstance;
wc.hIcon =            LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor =  LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wc.lpszMenuName = NULL;
wc.lpszClassName = szWndClass;
return RegisterClass(&wc) == NULL? FALSE: TRUE;
}

int PASCAL WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR lpszCmdLine, int nCmdShow)
{
UNUSED_ARG(lpszCmdLine);
MSG    msg;
HWND hWnd;

if (!hPrevInst) {
      if (!init_instance(hInst)) return 1;
}

hWnd= CreateWindow(
      szWndClass, "window header", WS_OVERLAPPEDWINDOW,
      CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
      NULL, NULL, hInst, NULL
);

if (!hWnd) return 1;

ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);

while (GetMessage(&msg, NULL, NULL, NULL)) {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
}

return msg.wParam;
}

 

Рисунок 1. Приложение 1a.cpp в среде Windows 3.x или Windows NT 3.x (слева) или в среде Windows–95 или Windows NT 4.0 (справа).

В зависимости от платформы, на которой запускается это приложение, внешний вид окна может несколько изменяться. Это связано с изменившимся интерфейсом пользователя при переходе от Windows 3.x и Windows NT 3.x к Windows–95 и Windows NT 4.0.

Далее мы рассмотрим исходный текст более подробно. При первом взгляде на него обращают на себя внимание сразу несколько необычных (по сравнению с программами для DOS) вещей:

· новые типы данных

· странные имена переменных

· обилие используемых функций и передаваемых им параметров

Примерно в таком порядке мы и рассмотрим эти вопросы.

Новые типы данных

 

Итак, еще раз рассмотрим первое Windows–приложение (1a.cpp).

Обычно в начале “С”–программы помещается директива препроцессора #include для включения файла, содержащего основные определения и прототипы функций. При написании Windows–приложений вы должны включить файл WINDOWS.H. Этот файл содержит определения типов, констант и функций, используемых в Windows[6].

В приложении перед включением WINDOWS.H определяется специальный символ STRICT:

#define STRICT
#include <windows.h>

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

Для большей части обычных типов Windows предлагает свои собственные определения — что объясняется возможностью реализации на разных вычислительных платформах и, соответственно, Windows–приложения должны быть переносимыми хотя бы на уровне исходного текста.

Для 16ти и 32х разрядных платформ существенно различаются режимы адресации. Например, для 32х разрядных машин практически не применяются near и far модификаторы адреса (Win32 требует, что бы приложения разрабатывались в 32х разрядной flat–модели памяти, где на все про все отводится один 32х разрядный сегмент, размером до 4Г). Кроме того, стандартом C предполагается, что тип данных int имеет длину одно слово. То есть для 16ти разрядных машин он совпадает с типом short int, а для 32х разрядных с типом long int. Это приводит к частичной непереносимости С–программ с одной платформы на другую.

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

 

Новое название Значение для Windows API Значение для Win32 API
Символы (#define) FAR NEAR PASCAL LONG VOID NULL WINAPI CALLBACK far near pascal long void 0 pascal far pascal far pascal long void 0 pascal pascal
Типы (typedef) BOOL BYTE WORD DWORD UINT NPSTR PSTR LPSTR LPCSTR WPARAM LPARAM LRESULT FARPROC HANDLE HFILE HWND HINSTANCE HDC int unsigned char unsigned short int unsigned long int unsigned int char near* char * char far* const char far* UINT LONG LONG (far pascal *)(void) unsigned int HANDLE HANDLE HANDLE HANDLE int unsigned char unsigned short int unsigned long int unsigned int char * char * char * const char * UINT LONG LONG (pascal *)(void) unsigned int HANDLE HANDLE HANDLE HANDLE

 

Практически для всех определенных типов существуют типы “указатель на...”. Ближние указатели строятся с помощью префикса NP, а дальние — LP, указатели, соответствующие принятой модели памяти, строятся с помощью префикса P. Например, BYTE — тип, представляющий отдельный байт, LPBYTE — дальний указатель на байт, а NPBYTE — ближний указатель. Исключение — тип VOID, он имеет только дальний указатель LPVOID.

Внимательнее разберемся с типом HANDLE (и со всеми “производными” от него): Дело в том, что Windows создает специальные структуры данных, описывающих требуемые объекты (например окно). Эта структура данных зачастую принадлежит не вашему приложению, а самой системе. Для того, что бы этот объект можно было идентифицировать, вводится специальное понятие хендл (дескриптор, handle). Хендл в Windows — это просто целое число, иногда номер, присвоенный данному объекту, причем значение NULL указывает на несуществующий объект. Единственное исключение — HFILE, для которого определено специальное значение — HFILE_ERROR, равное -1 (это связано с тем, что хендл файла первоначально был заимствован у DOS, где хендл 0 обозначает стандартное устройство вывода stdout). Понятие хендла в Windows используется очень широко, а для облегчения контроля типов используется большое количество производных от хендла типов.

Win32

 

Здесь же надо еще раз отметить, что для Win32 API всегда применяется 32х разрядная flat–модель памяти. В этом случае модификаторы far и near не применяются. Кроме того хендл, соответствующий типу unsigned int, становится 32х разрядным. Это на самом деле приводит к изрядным сложностям при переходе с платформы на платформу. Дело в том, что в Windows API хендл часто объединяется с какими–либо дополнительными данными и размещается в одном двойном слове, передаваемом в качестве параметра функции или сообщения, а в Win32 такое уже не получится — хендл сам занимает все двойное слово.

Кроме того, в Win32 API для работы с файлами используется опять–таки хендл, но уже не типа HFILE, а HANDLE. При этом нулевое значение по–прежнему является допустимым и обозначает стандартное устройство вывода, а значение -1 — неверный хендл. Для обозначения неверного хендла файла определен символ INVALID_HANDLE_VALUE, равный -1. Для других хендлов, кроме хендлов файлов, этот символ не применяется, так как для индикации ошибки применяется значение 0. При этом тип HFILE и символ HFILE_ERROR определены также, как и в 16ти разрядных Windows — в виде 16ти разрядного целого числа. В принципе допустимо простое приведение типов, однако в будущих реализациях Windows API ситуация может измениться, так как тип HANDLE соответствует 32х разрядному числу.

Венгерская нотация

 

При чтении текстов C—программ и документации Вы обратите внимание на несколько странное написание имен переменных и функций. Например:

lpszFileName, wNameLength

Разработчики Windows рекомендуют применять специфичные правила описания имен переменных, которые получили название “венгерская нотация” по национальности программиста Charles Simonyi из Microsoft, предложившего ее. Применение венгерской нотации улучшает читаемость программ и уменьшает вероятность ошибки. Хотя, конечно, это дается ценой увеличения длины имен переменных.

Хорошим программистским правилом является использование мнемонических имен переменных. Венгерская нотация предполагает не только применение мнемоники для определения смысла переменной (как, например, FileSize), но и включение ее типа в имя. Например lpszFileName обозначает дальний указатель на ASCIIZ[7] строку символов, содержащую имя файла.

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

 

обозначающий символ название обозначаемого типа пояснение
c char символ
by BYTE байт
n int целое число
x short координата или размер
y short координата или размер
i int целое число
f, b BOOL логическая величина
w WORD слово без знака
h HANDLE хендл
l LONG длинное целое со знаком
dw DWORD длинное целое без знака
e FLOAT число с плавающей запятой
*fn   функция
s   строка
sz   строка, оканчивающаяся '\0' (ASCIIZ)
p * указатель на...
lp far* дальний указатель на...
np near* ближний указатель на...

 

Зная эту таблицу легко самим понять или составить имена переменных в венгерской нотации. Даже если Вы не будете сами применять венгерскую нотацию при написании программ, то знать ее все равно надо, так как она принята во всей документации, сопровождающий Windows. К сожалению даже здесь разработчики оказались не совсем последовательны и Вам придется столкнуться в документации с некорректным (с точки зрения приведенной таблицы) применением венгерской нотации.

Так, в качестве примера можно привести название поля cbWndExtra в структуре WNDCLASS. В данном случае префикс cb расшифровывается как Count of Bytes.



Поделиться с друзьями:

Типы сооружений для обработки осадков: Септиками называются сооружения, в которых одновременно происходят осветление сточной жидкости...

Таксономические единицы (категории) растений: Каждая система классификации состоит из определённых соподчиненных друг другу...

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

Наброски и зарисовки растений, плодов, цветов: Освоить конструктивное построение структуры дерева через зарисовки отдельных деревьев, группы деревьев...



© cyberpedia.su 2017-2024 - Не является автором материалов. Исключительное право сохранено за автором текста.
Если вы не хотите, чтобы данный материал был у нас на сайте, перейдите по ссылке: Нарушение авторских прав. Мы поможем в написании вашей работы!

0.083 с.