Базы данныхИнтернетКомпьютерыОперационные системыПрограммированиеСетиСвязьРазное
Поиск по сайту:
Подпишись на рассылку:

Назад в раздел

Краткое описание по работе с DirectDraw.

Краткое описание по работе с DirectDraw Краткое описание по работе с DirectDraw Содержание Краткий обзор DirectDrawDirectX APIЧто такое DirectDrawВзаимотношение с WinGПоверхности DirectDraw - вид доступа к видеопамятиОт смены страниц (flipping) к анимацииИнтерфейсы DirectDraw и COMПрограммирование DirectDrawЧто Вам необходимо для использования DirectDrawИнициализация DirectDrawСоздание первичной поверхности и установки для обеспечения смены страниц (flipping)Загрузка изображений в видеопамять (bitmap)Загрузка палитрыНастройка прозрачного цветаСобрать все вместе и готово. Построение динамики.Очистка по окончании использованияDirectDraw и MFCОсобености встраивания объектов DirectDraw в МFСПоддержка оконного интерфейса при использовании DirectDraw в МFСПрямое рисование (минуя GDI)Некоторые детали Вопросы отладки Выполнение в окне Потеря поверхности Что еще почитать Краткое описание по работе с DirectDraw В этом разделе будет дан общий обзор DirectDraw, а также общие концепции.

1. DirectX API

Microsoft's DirectX API состоит из следующих групп: DirectDraw - прямой доступ к видеопамяти DirectSound - прямой доступ к звуковой карте DirectPlay - прямой доступ к сетевым возможностям для обеспечения multiplayer mode DirectInput - поддержка игровых устройств ввода Все это предназначено для того, чтобы дать возможность программисту получить прямой доступ к различному (в основном игровому) железу. В данной статье дается пояснения к использованию интерфейса DirectDraw.

2. Что такое DirectDraw

DirectDraw это обычный менеджер видеопамяти. Его основное назначение предоставить программисту прямой доступ к видеопамяти. Осуществлять такие операции, как копирование видео память -> видеопамять и т.п.. При этом напрямую могут использоваться возможности видеоконтроллера и освобождать от этих операций центральный процессор. Кроме того, DirectDraw напрямую использует и другие возможности Вашей виде окарты, как то спрайты, z - буферизацию и т.п.. Начиная с 5 версии DirectX дополнительно используются (во всяком случае декларируется использование) возможностей ММX - что позволяет за один цикл обрабатывать и передавать 64 бита видеоинформации.

3. Взаимоотношение с WinG

В основном идея DirectDraw взята с WinG. Отличие состоит в том, что в основу идеологии WinG положено копирование изображений из обычной памяти в видеопамять, в то время как основной идеей DirectDraw является копирование видеопамяти в видеопамять, хотя также может поддерживаться и режим память->видеопамять. Что, конечно, является необходимым в мире видео карт с 1 метром видеопамяти.

4. Поверхности DirectDraw - вид доступа к видеопамяти

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

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

5. От смены страниц (flipping) к анимации

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

Частота смены буферов может быть достаточно высокой и ограничена частотой кадровой развертки монитора (60 - 100 Гц). Поддержка DirectDraw вертикальной развертки приводит к более гладкой смене кадров.

Интерфейсы DirectDraw и COM

Все DirectX объекты являются порождениями от COM объектов.

При этом Вы можете, как использовать напрямую возможности COM , так и вообще не знать о них. Весь минимально необходимый интерфейс к COM скрыт (уже поддерживается) объектами DirectX.

Программирование DirectDraw Здесь будут представлены примеры использования DirectDraw в основном основанные на примерах к DirectX SDK. Данные примеры, конечно, будут полностью функциональными , но не совсем годными для практических приложений. Это вызвано тем, что в практических реализациях необходим учет характера поведения конкретного приложения, а здесь приводятся лишь грубые схемы. p>

1. Что Вам необходимо для начала использования DirectDraw

Для начала необходимо заиметь DirectX SDK. Где его взять? Путей много, например: DirectX SDK ver.3 входит в состав Visual C++ (ver.5), DirectX SDK версии 5.0 я нашел на http://www.download.com, DirectX SDK ver.5.2. предлагает бесплатно за 12$ Microsoft на своем сайте (Вам вышлют CD по почте - но это для владельцев American Express или VisaCard), компакт-диски MSDN также могут содержать данный SDK. И конечно на рынках пиратских CD

В состав SDK входят все необходимые библиотеки, заголовочные файлы, help файлы и большой набор примеров

Типовым джентльменским набором является ddraw.h, ddraw.dll, ddraw.lib (implib на ddraw.dll)

В общем, необходимо включить в Ваш проект DDraw.lib и в исходный файл ddraw.h

Использовать DirectDraw можно только с Win32 (Windows 95, NT (не пробовал)). Если Вы используете версию Win95 OSR, то там скорее всего DirectX уже установлен. Кроме того DirectX скорее всего установила Вам какая нибудь игрушка (см. DDraw.dll в поддиректории system )

Программировать с использованием DirectDraw можно в различных средах программирования. Все примеры, приводимые ниже, точно должны работать с Microsoft Visual C++ ver.5

2. Инициализация DirectDraw

Первым делом, при использовании DirectDraw ,необходимо его инициализировать. Ниже дан пример кода инициализации
 

Пример инициализации DirectDraw LPDIRECTDRAW lpDD; // указатель на объект DirectDraw                    // вообще их может быть несколько, я ,например, использовал                    //по одному объекту на каждое окно в MDI приложении (см пример                    //использования DirectDraw c MFC) /*  * Функция инициализации объекта DirectDraw  *   1) Создаем  Direct Draw Object  *   2) Устанавливаем вид доступа  *   3) Устанавливаем видео моду дисплея  *  */ BOOL DirectDrawInit(HWND hwnd) {     HRESULT ddrval;    /*     * Создание (получение) указателя на основной объект DirectDraw.     *     */     ddrval = DirectDrawCreate( NULL, &lpDD, NULL );     if( ddrval != DD_OK )     {         goto lError;     }    /*     * Теперь зададим уровень доступа, который может иметь следующие значения     *     * DDSCL_EXCLUSIVE дает Вам полный доступ к видеопамяти и управлению видеорежимами     * требует флаг DDSCL_FULLSCREEN , т.е. Вы работаете со всем экраном сразу     * Это основной (типовой режим работы DirectDraw     * DDSCL_NORMAL используется для обеспечения оконного доступа. Правда доступ     * у вас все равно ко всему экрану , обеспечивать оконность все равно приходится вручную     * (обычные недоделки Microsoft)     * как добиться оконного поведения см пример класса CDDrawView доступном для download на данном     * сайте     */     ddrval = lpDD->SetCooperativeLevel( hwnd, DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN );     if( ddrval != DD_OK )     {         lpDD->Release();         goto lError;     }    /*     * Установим видео моду 800x600 256 цветов     * (видео моду можно изменять, только если установлен флаг DDSCL_EXCLUSIVE     */     ddrval = lpDD->SetDisplayMode( 800, 600, 8);     if( ddrval != DD_OK )     {         lpDD->Release();         goto lError;     }     return TRUE; lError:     // здесь можно вставить код для оповещения пользователя о невозможности     // инициализации DirectDraw     return FALSE; }
Таким образом мы проинициализировали DirectDraw и изменили видео моду (в отладчике Developer Studio режим 640х480 устанавливать не рекомендую, эффект потрясающий - практически становятся видны только меню и туулбары)

3. Создание первичной и вторичной поверхностей и установки для обеспечения смены страниц (flipping)

В следующем участке кода мы рассмотрим создание первичной и вторичной поверхностей (видеобуферов) . Их объединение часто называют комплексной поверхностью
 
Пример инициализации поверхностей (первичной и вторичной) DirectDraw LPDIRECTDRAWSURFACE lpDDSPrimary; // указатель на первичную поверхность DirectDraw LPDIRECTDRAWSURFACE lpDDSBack;    // указатель на вторичную поверхность DirectDraw BOOL CreatePrimarySurface() {     DDSURFACEDESC ddsd;     DDSCAPS ddscaps;     HRESULT ddrval;     // создадим первичную поверхность с одной вторичной     memset( &ddsd, 0, sizeof(ddsd) );     ddsd.dwSize = sizeof( ddsd );     ddsd.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT;    // поле  ddsCaps принимается в рассмотрение и в                                                         // нем имеет значение только количество вторичных                                                         //буферов(поверхностей)     ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE | DDSCAPS_FLIP | DDSCAPS_COMPLEX;     ddsd.dwBackBufferCount = 1;     ddrval = lpDD->CreateSurface( &ddsd, &lpDDSPrimary, NULL );     if( ddrval != DD_OK )     {         lpDD->Release();         return FALSE;     }     // Теперь получим вторичный буфер (поверхность)     ddscaps.dwCaps = DDSCAPS_BACKBUFFER;     ddrval = lpDDSPrimary->GetAttachedSurface(&ddscaps, &lpDDSBack);     if( ddrval != DD_OK )     {         lpDDSPrimary->Release();         lpDD->Release();         return FALSE;     }     return TRUE; }
4. Загрузка изображений в видеопамять (bitmap)

Следующим шагом является загрузка изображений в видеопамять. Идеальным является случай, когда в видеопамяти достаточно места для того чтобы там ужились все необходимые Вам для разработки изображения. Если ее не хватает , тоже особенно беспокоится не о чем, CreateSurface создаст поверхности в обычной памяти (правда, это чуть замедлит).
 
Пример кода загрузки изображений в DirectDraw /*  * Функция DDLoadBitmap(IDirectDraw *pdd, LPCSTR szBitmap) создает поверхность  * и загружает файл с диска. Параметр szBitmap имя файла изображения.  */ IDirectDrawSurface * DDLoadBitmap(IDirectDraw *pdd, LPCSTR szBitmap) {     HBITMAP hbm;     BITMAP bm;     IDirectDrawSurface *pdds;     // загрузим с диска     hbm = (HBITMAP)LoadImage(NULL, szBitmap, IMAGE_BITMAP, 0, 0,     LR_LOADFROMFILE|LR_CREATEDIBSECTION);     if (hbm == NULL)        return NULL;     GetObject(hbm, sizeof(bm), &bm); // получим размер     //создадим поверхность для данного изображения     pdds = CreateOffScreenSurface(pdd, bm.bmWidth, bm.bmHeight);     if (pdds)     {         DDCopyBitmap(pdds, hbm, bm.bmWidth, bm.bmHeight);     }     DeleteObject(hbm);     return pdds; } /*  * Функция создания поверхности для изображения заданного размера  * Эта поверхность окажется в видеопамяти, если ее еще достаточно,  * в противном случае - в системной памяти  */ IDirectDrawSurface * CreateOffScreenSurface(IDirectDraw *pdd, int dx, int dy) {     DDSURFACEDESC ddsd;     IDirectDrawSurface *pdds;     //     // Создание поверхности     //     ZeroMemory(&ddsd, sizeof(ddsd));     ddsd.dwSize = sizeof(ddsd);     ddsd.dwFlags = DDSD_CAPS | DDSD_HEIGHT |DDSD_WIDTH; // имеет значение высота и ширина     ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN; // это заэкранная поверхность     ddsd.dwWidth = dx;     ddsd.dwHeight = dy;     if (pdd->CreateSurface(&ddsd, &pdds, NULL) != DD_OK)     {         return NULL;     }     else     {         return pdds;     } } /*  * Функция копирования ранее загруженного изображения в видео поверхность  * (Используется обычный GDI интерфейс)  *  Это, конечно, не самый быстрый способ - но зато надежный  *  A в данный момент спешить некуда. Если у Вас происходит динамическая подгрузка  *  изображений в процессе то лучше эту функцию переписать аккуратнее  */ HRESULT DDCopyBitmap(IDirectDrawSurface *pdds, HBITMAP hbm, int dx, int dy) {     HDC hdcImage;     HDC hdc;     HRESULT hr;     HBITMAP hbmOld;     //     // привяжем контекст изображения (DC) к загруженному изображению     //     hdcImage = CreateCompatibleDC(NULL);     hbmOld = (HBITMAP)SelectObject(hdcImage, hbm);     if ((hr = pdds->GetDC(&hdc)) == DD_OK)     {         BitBlt(hdc, 0, 0, dx, dy, hdcImage, 0, 0, SRCCOPY);         pdds->ReleaseDC(hdc);     }     SelectObject(hdcImage, hbmOld);     DeleteDC(hdcImage);     return hr; }
Таким образом, мы имеем возможность загрузить все необходимые изображени я в видео поверхности DirectDraw. Для создания динамического изображения остается только организовать их копирование в на поверхности видеоизображения (первичный или вторичный буфер).

5. Загрузка палитры

Теперь для правильного отображения всех загруженных изображений нужно установить палитру для первичной видео поверхности. Все изображения, загружаемые для работы данного приложения должны иметь одну и туже палитру. Ниже приведена функция создания палитры для данного изображения
 
Пример кода создания палитры в DirectDraw /*  * Создание палитры DirectDraw для файла загружаемого с диска  *  параметр szBitmap имя файла  */ IDirectDrawPalette * LoadPaletteFromDibFile(IDirectDraw *pdd, LPCSTR szBitmap) {     IDirectDrawPalette* ddpal;     int i;     int n;     int fh;     PALETTEENTRY ape[256];     if (szBitmap && (fh = _lopen(szBitmap, OF_READ)) != -1)     {         BITMAPFILEHEADER bf;         BITMAPINFOHEADER bi;         _lread(fh, &bf, sizeof(bf));         _lread(fh, &bi, sizeof(bi));         _lread(fh, ape, sizeof(ape));         _lclose(fh);         if (bi.biSize != sizeof(BITMAPINFOHEADER))             n = 0;         else if (bi.biBitCount > 8)             n = 0;         else if (bi.biClrUsed == 0)             n = 1 << bi.biBitCount;         else             n = bi.biClrUsed;       //       // цветовая таблица DIB имеет  BGR кодировку а не RGB       // сделаем необходимую транспозицию.       //         for(i=0; iCreatePalette(DDPCAPS_8BIT, ape, &ddpal, NULL) != DD_OK)     {         return NULL;     } else {         return ddpal;     } }
В исходный код инициализации DirectDraw необходимо добавить следующие две строчки:
 
Пример кода загрузки палитры в DirectDraw     ......................     lpDDPal        = LoadPaletteFromDibFile(lpDD, szBitmap);   // Создадим и Получим указатель на палитру     if (lpDDPal)    lpDDSPrimary->SetPalette(lpDDPal);            // и установим эту палитру для первичной видео поверхности     ....................        
5. Настройка прозрачного цвета

В DirectDraw возможно использование прозрачного цвета(ов) (входа в палитру (color key)) используемого при копировании с поверхности на поверхность. Обычно для этого задается диапазон цветов (входов в палитру), которые не переносятся при копировании (Blt функциях).
 
Пример кода установки прозрачных цветов в DirectDraw     // Установим прозрачный цвет для данного изображения .     //     DDCOLORKEY ddColorKey;      ddColorKey.dwColorSpaceLowValue = 0xff; // весь диапазон прозрачности - 255 вход в палитру (обычно он черный)      ddColorKey.dwColorSpaceHighValue = 0xff;     //  Далее необходимо сделать что-то типа для вех поверхностей (изображений ) для которых нужен эффект      // прозрачности при выполнении операций копирования   [Имя указателя на DirectDraw поверхность]->SetColorKey( DDCKEY_SRCBLT, &ddck );
7. Собрать все вместе и готово. Построение динамики.

Теперь осталось только организовать цикл по обновлению вторичной поверхности и после его обновления проведения смены экранов (Flip) . Этот цикл лучше всего организовывать на цикле обработки сообщений Вашего приложения (OnIdle), таким образом Вы получите максимальную производительность для конкретной машины на которой выполняется ваше приложение. Сажать это на таймер я не советую - Вам просто будет сложно разобраться и все так подогнать, чтобы с одной стороны по максимуму использовать возможности машины, а с другой не перегружать очередь сообщений. Для общей синхронизации разумно использовать функции мультимедиа типа функции timeGetTime() которая выдает время в мс. Также учтите что чем больше нитей (THREAD) Вы организуете для обеспечения расчетов расположения изображений тем сложнее Вам будет добиться плавности (Windows 95 совсем не многозадачка с вытеснением и квантом времени я не нашел как управлять). Единственное ,что можно использовать для увеличения плавности - Sleep(0) для текущей нити приложения. Но это о другом - просто лирическое отступление - плач по отсутствию возможностей.
 
Обновления экрана void UpdateScreen( void ) {     HRESULT ddrval;     RECT mRect;     int xpos, ypos;    // Выберем весь экран и скопируем на вторичную поверхность изображения фона    // если Вы, конечно, его используете      SetRect(&mRect, 0, 0, 640, 480);     // lpDDBackgraund - здесь поверхность фонового изображения      // кстати оно может быт больше экрана - тогда возможно копирование только его части - типа перемещения по карте     ddrval = lpDDSBack->BltFast( 0, 0, lpDDBackgraund, &mRect, DDBLTFAST_NOCOLORKEY | DDBLTFAST_WAIT);     if( ddrval != DD_OK )     {         // не смогли - вернемся                 return;     }    while ([цикл по всем изображениям, которые должны быть перенесены на экран])          {             // Получить текущее изображение (указатель lpDDCurrentImage) из списка, массива - чего было задумано,         // а также его размера mWidth, mHigh, и местоположения x и y         .......................         // после получения указанных параметров         SetRect(&mRect, 0, 0,mWidth , mHigh);            ddrval = lpDDSBack->BltFast( x, y, lpDDCurrentImage,    &mRect, DDBLTFAST_SRCCOLORKEY | DDBLTFAST_WAIT );            if( ddrval != DD_OK )          {         //не смогли - вернемся                       return;          }       }                // Теперь все нарисовано - можно поменять экран     ddrval = lpDDSPrimary->Flip( NULL, DDFLIP_WAIT ); } 
К данному коду надо сделать ряд замечаний : BltFast - функция, конечно, хорошая и по названию зазывающая, но не всегда идет - тогда используй обычный Blt (он видимо помедленнее - но надежней) DDXX_WAIT флаг необходим для медленных видео карт и шустрых машин - просто без его использования функция возвращает управление сразу после запуска , а при его использовании только после окончания процесса копирования. - просто пока идет одно копирование вызов другого вроде может привести к ошибке. Память то захвачена другим процессом. 8. Очистка по окончании использования

По окончании работы не забудьте вызвать Release() для всех порожденных Вами объектов DirectDraw. Что то типа приведенного ниже кода.
 
Очистка после себя void finiObjects( void ) {     if( lpDD != NULL )     {                  if( lpDDSPrimary != NULL )         {             lpDDSPrimary->Release();             lpDDSPrimary = NULL;         }         if( lpDDSOne != NULL )         {             lpDDSOne->Release();             lpDDSOne = NULL;         }         if( lpDDPal != NULL )         {             lpDDPal->Release();             lpDDPal = NULL;     }     lpDD->Release();     lpDD = NULL; }
DirectDraw и MFC 1.Особености встраивания объектов DirectDraw в МFС

Библиотеку классов MFC можно считать основной для разработки интерфейсных приложений для платформы Win32. К сожалению, фирма Microsoft как разработчик и MFC и DirectX не дает прямых указаний по использованию DirectX в приложениях написанных на основе библиотеки MFC. Есть правда несколько статей в Microsoft Developer Network Library. Это видимо связано с тем, что использование быстрого интерфейса DirectX в достаточно медленном MFC - нецелесообразно. Однако это становится совсем не очевидным на машинах класса Pentium 200.

Для библиотеки MFC типовой является работа с окнами с развитым пользовательским интерфейсом - а не голая поверхность экрана - поэтому для MFC разумным является не полноэкранное использование DirectDraw , а оконное. Хотя путем извращений можно из MFC сделать и полноэкранного ублюдка. Но встает вопрос - а зачем тогда MFC. Т.е. все, что ранее писалось про Flip - здесь не применимо. Типовое использование - создание буфера видеоизображения Вашего окна, рисование на нем с последующим копированием в зону Вашего окна на экране.

2. Поддержка оконного интерфейса при использовании DirectDraw в МFС

При использовании DirectDraw в приложениях на базе MFC возможно два основных подхода; создание прослоенного класса на базе класса CView (или аналогичного) и замена в своем порождении от класса CView на свой, переделанный класс. встраивание в свой класс CView (иного CMainFrame) переменных DirectDraw Мне больше импонирует первый способ т.к. позволяет решить проблему использования DirectDraw одноразово и в дальнейшем скрыть вообще использование DirectDraw. (А зачем вообще тогда нужно C++?).

Пример такого построения использован в классе CDDrawView , который можно загрузить отсюда .

Отмечу основные методы данного класса: инициализация объектов DirectDraw производится в методе OnInitialUpdate() переписан метод OnPaint() в котором создается указатель на CDC за экранной поверхности , вызывается метод OnDraw(CDC* pDC ), по окончании которого производится копирование содержимого за экранной поверхности в область текущего окна переписан метод OnSize - при изменении размеров окна производится уничтожение старой за экранной поверхности и инициализация новой. переписан метод OnEraseBackgraund() Я думаю, для выяснения деталей лучше просто посмотреть исходники.

3 .Прямое рисование (минуя GDI)

Что такое мистические видео поверхности в DirectDraw. Войдите в панель управления Windows 95 , откройте панель ресурсов Вашего видеоадаптера и Вы увидите, что он использует значительного размера кусок памяти . Т.е. вся видеопамять как бы спроецирована на Ваше ОЗУ. DirectDraw - это механизм позволяющий Вам буквально напрямую использовать данное ОЗУ , при этом напрямую используя возможности видео ускорителя Вашего видеоадаптера. Все эти LPDIRECTDRAWSURFACE есть просто указатели на видеопамять отведенную вашему изображению. А отсюда зная его структуру можно напрямую в нем рисовать - просто записывая в него необходимые Вам значения. Ниже приведен участок кода для поддержки рисования точки (Примечание код взят из ответа на одной из конференций qiz@algonet.se).
Рисование точки для видеорежима 640x480x8 UINT SetPixel( UINT x, UINT y, UINT colorValue ) { UINT PointOffset; UINT RetColorValue=0; DDSURFACEDESC DDSDesc; __int8* lpDstBits; HRESULT ddrval; DDSDesc.dwSize = sizeof(DDSDesc); // Захват видеопамяти для записи while(1) { ddrval = lpDDSBack->Lock(NULL, &DDSDesc, 0, NULL); if(ddrval == DD_OK) break; if(ddrval != DDERR_WASSTILLDRAWING) return 0; } // set the destination pixel // in this case resolution is 640 * 480 * 8 lpDstBits = (LPSTR) DDSDesc.lpSurface; PointOffset = (640 * y) + x; // если Вам нужен возврат цвета точки RetColorValue=lpDstBits[Pointoffset]; // нарисуем точку lpDstBits[Pointoffset] = (__int8) colorValue; // now ulock the surface lpDDSBack->Unlock(NULL); return RetColorValue; }



  • Главная
  • Новости
  • Новинки
  • Скрипты
  • Форум
  • Ссылки
  • О сайте




  • Emanual.ru – это сайт, посвящённый всем значимым событиям в IT-индустрии: новейшие разработки, уникальные методы и горячие новости! Тонны информации, полезной как для обычных пользователей, так и для самых продвинутых программистов! Интересные обсуждения на актуальные темы и огромная аудитория, которая может быть интересна широкому кругу рекламодателей. У нас вы узнаете всё о компьютерах, базах данных, операционных системах, сетях, инфраструктурах, связях и программированию на популярных языках!
     Copyright © 2001-2024
    Реклама на сайте