Форум программистов, компьютерный форум, киберфорум
8Observer8
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск Заказать работу  

3. Переходим на рисование фигур с помощью функции glDrawElements(­­­) - вместо glBegin()/glEnd(). Змейка

Запись от 8Observer8 размещена 17.11.2014 в 16:46
Показов 15295 Комментарии 4
Метки blender, c++

Содержание блога

В предыдущей инструкции мы рисовали с помощью glBegin()/glEnd(). Этот способ устарел. Переходим на новый уровень. Здесь будем рисовать с помощью функции glDrawElements, а формировать объекты с помощью массивов вершин и индексов. Цвет будем задавать с помощью массива цветов. Это более удобный способ. Он позволяет легко накладывать текстуры, указав текстурные координаты (об этом в следующих инструкция)

Рисуем треугольник

- Создаём проект c диалоговым окном (имя проекта "Triangle") по инструкции: Первое оконное приложение на Qt

- Создаём площадку для рисования: Создание проекта с площадкой для рисования

- Нарисуем треугольник на бумаге, или например в Paint'е, и расставим номера вершин с обходом против часовой стрелки:



- Создаём заголовочный файл "Triangle.h". Для этого кликаем правой кнопкой мыши по имени проекта и выбираем: "Add New..." -> в левой колонке выбираем "C++" -> в средней колонке выбираем "C++ Header File" -> нажимаем кнопку "Choose..." -> вводим имя файла: Triangle -> нажимаем "Next" -> "Finish"

- Объявляем три массива: массив вершин, массив индексов и массив цветов. Заполняем в соответствии с рисунком выше. Скопируйте код:

Triangle.h
Кликните здесь для просмотра всего текста
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#ifndef TRIANGLE_H
#define TRIANGLE_H
 
#include <vector>
 
class Triangle
{
public:
    Triangle()
    {
        init();
    }
 
    void init()
    {
        // 0
        vertices.push_back( 0 );    // X
        vertices.push_back( 1 );    // Y
        vertices.push_back( 0 );    // Z
 
        // 1
        vertices.push_back( -1 );   // X
        vertices.push_back( -1 );   // Y
        vertices.push_back( 0 );    // Z
 
        // 2
        vertices.push_back( 1 );    // X
        vertices.push_back( -1 );   // Y
        vertices.push_back( 0 );    // Z
 
        indices.push_back( 0 );
        indices.push_back( 1 );
        indices.push_back( 2 );
 
        for ( unsigned int i = 0; i < 3; ++i ) {
            colors.push_back( 0 );   // R
            colors.push_back( 1 );   // G
            colors.push_back( 0 );   // B
        }
    }
 
    std::vector<int> vertices;
    std::vector<unsigned int> indices;
    std::vector<float> colors;
};
 
#endif // TRIANGLE_H


- В файле "Scene.h" подключаем файл "Triangle.h". Объявляем функцию drawTriangle(). Создаём объект triangle класса Triangle. Скопируйте код:

Scene.h

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#ifndef SCENE_H
#define SCENE_H
 
#include <QGLWidget>
#include "Triangle.h"
 
class Scene : public QGLWidget
{
public:
    Scene( QWidget *parent = 0 );
 
private:
    void initializeGL();
    void paintGL();
    void resizeGL( int w, int h );
 
    void drawTriangle();
 
private:
    Triangle triangle;
};
 
#endif // SCENE_H
- Копируем содержимое файла "Scene.cpp". Читайте комментарии:

Scene.cpp
Кликните здесь для просмотра всего текста
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include "Scene.h"
 
Scene::Scene( QWidget *parent ) :
    QGLWidget( parent )
{
 
}
 
void Scene::initializeGL()
{
    // Цвет для очистки буфера изображения - будет просто фон окна
    glClearColor( 0.0f, 0.0f, 1.0f, 1.0f );
 
    // Устанавливает режим проверки глубины пикселей
    glEnable( GL_DEPTH_TEST );
 
    // Отключает режим сглаживания цветов
    glShadeModel( GL_FLAT );
 
    // Устанавливаем режим, когда строятся только внешние поверхности
    glEnable( GL_CULL_FACE );
 
    // Активизация массива вершин
    glEnableClientState( GL_VERTEX_ARRAY );
 
    // Активизация массива цветов вершин
    glEnableClientState( GL_COLOR_ARRAY );
}
 
void Scene::paintGL()
{
    // Окно виджета очищается текущим цветом очистки
    glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
 
    drawTriangle();
}
 
void Scene::resizeGL( int w, int h )
{
    // Set Viewport to window dimensions
    glViewport( 0, 0, w, h );
 
    // Reset coordinate system
    glMatrixMode( GL_PROJECTION );
    glLoadIdentity();
 
    // Establish clipping volume (left, right, bottom, top, near, far)
    glOrtho( -1, 1, -1, 1, 1, -1);
 
    glMatrixMode( GL_MODELVIEW );
    glLoadIdentity();
}
 
void Scene::drawTriangle()
{
    // Указываем, откуда нужно извлечь данные о массиве вершин
    glVertexPointer( 3, GL_INT, 0, triangle.vertices.data() );
 
    // Указываем, откуда нужно извлечь данные о массиве цветов вершин
    glColorPointer( 3, GL_FLOAT, 0, triangle.colors.data() );
 
    // Используя массивы вершин и индексов, строим поверхности
    glDrawElements( GL_TRIANGLES, triangle.indices.size(), GL_UNSIGNED_INT, triangle.indices.data() );
}


- Запускаем приложение. Мы видим треугольник

Рисуем Квадрат

- Мы можем создать новый проект с именем Square по примеру проекта "Рисуем треугольник". Либо мы можем добавить новый класс "Square" в предыдущий проект (в Scene нужно будет добавить функцию drawSquare). Либо мы можем создать копию папки нашего предыдущего проекта и переименовать её в Square (соответственно, все слова "triangle" нужно заменить на "square"). Я выбрал последний вариант

Примечание. Треугольник в OpengGL - это минимальная единица для рисования любой 2D и 3D фигуры (или поверхности). Этот способ рисования позволяет легко накладывать текстуры, задав массив текстурных координат, а об этом в следующих инструкциях

- Рисуем, например в Paint'е, квадрат, который состоит из двух треугольников. Нумеруем вершины, соблюдая обход против часовой стрелки:



- Создаём класс Square. В соответствии с рисунком выше заполняем массивы вершин, индексов и цветов:

Square.h
Кликните здесь для просмотра всего текста
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#ifndef SQUARE_H
#define SQUARE_H
 
#include <vector>
 
class Square
{
public:
    Square()
    {
        init();
    }
 
    void init()
    {
        // Первый треугольник
        // 0
        vertices.push_back( -1 );    // X
        vertices.push_back( -1 );    // Y
        vertices.push_back( 0 );     // Z
 
        // 1
        vertices.push_back( 1 );     // X
        vertices.push_back( -1 );    // Y
        vertices.push_back( 0 );     // Z
 
        // 2
        vertices.push_back( -1 );    // X
        vertices.push_back( 1 );     // Y
        vertices.push_back( 0 );     // Z
 
        // Второй треугольник
        // 3
        vertices.push_back( -1 );    // X
        vertices.push_back( 1 );     // Y
        vertices.push_back( 0 );     // Z
 
        // 4
        vertices.push_back( 1 );     // X
        vertices.push_back( -1 );    // Y
        vertices.push_back( 0 );     // Z
 
        // 5
        vertices.push_back( 1 );     // X
        vertices.push_back( 1 );     // Y
        vertices.push_back( 0 );     // Z
 
        for ( unsigned int i = 0; i < 6; ++i ) {
            indices.push_back( i );
        }
 
        for ( unsigned int i = 0; i < 6; ++i ) {
            colors.push_back( 0 );   // R
            colors.push_back( 1 );   // G
            colors.push_back( 0 );   // B
        }
    }
 
    std::vector<int> vertices;
    std::vector<unsigned int> indices;
    std::vector<float> colors;
};
 
#endif // SQUARE_H


В классе Scene меняем в тексте программы "triangle" на "square" и запускаем приложение:

Scene.h
Кликните здесь для просмотра всего текста
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#ifndef SCENE_H
#define SCENE_H
 
#include <QGLWidget>
#include "Square.h"
 
class Scene : public QGLWidget
{
public:
    Scene( QWidget *parent = 0 );
 
private:
    void initializeGL();
    void paintGL();
    void resizeGL( int w, int h );
 
    void drawSquare();
 
private:
    Square square;
};
 
#endif // SCENE_H


Scene.cpp
Кликните здесь для просмотра всего текста
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include "Scene.h"
 
Scene::Scene( QWidget *parent ) :
    QGLWidget( parent )
{
 
}
 
void Scene::initializeGL()
{
    // Цвет для очистки буфера изображения - будет просто фон окна
    glClearColor( 0.0f, 0.0f, 1.0f, 1.0f );
 
    // Устанавливает режим проверки глубины пикселей
    glEnable( GL_DEPTH_TEST );
 
    // Отключает режим сглаживания цветов
    glShadeModel( GL_FLAT );
 
    // Устанавливаем режим, когда строятся только внешние поверхности
    glEnable( GL_CULL_FACE );
 
    // Активизация массива вершин
    glEnableClientState( GL_VERTEX_ARRAY );
 
    // Активизация массива цветов вершин
    glEnableClientState( GL_COLOR_ARRAY );
}
 
void Scene::paintGL()
{
    // Окно виджета очищается текущим цветом очистки
    glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
 
    drawSquare();
}
 
void Scene::resizeGL( int w, int h )
{
    // Set Viewport to window dimensions
    glViewport( 0, 0, w, h );
 
    // Reset coordinate system
    glMatrixMode( GL_PROJECTION );
    glLoadIdentity();
 
    // Establish clipping volume (left, right, bottom, top, near, far)
    glOrtho( -2, 2, -2, 2, 1, -1);
 
    glMatrixMode( GL_MODELVIEW );
    glLoadIdentity();
}
 
void Scene::drawSquare()
{
    // Указываем, откуда нужно извлечь данные о массиве вершин
    glVertexPointer( 3, GL_INT, 0, square.vertices.data() );
 
    // Указываем, откуда нужно извлечь данные о массиве цветов вершин
    glColorPointer( 3, GL_FLOAT, 0, square.colors.data() );
 
    // Используя массивы вершин и индексов, строим поверхности
    glDrawElements( GL_TRIANGLES, square.indices.size(), GL_UNSIGNED_INT, square.indices.data() );
}


- Если мы хотим увидеть каркас, то есть треугольники без закрашивания, то нужно в initializeGL() добавить эту строку:
C++
1
glPolygonMode( GL_FRONT_AND_BACK, GL_LINE );
Змейка

Переделал змейку из предыдущей инструкции. Изменения коснулись класса Painter и немного класса Scene. Просто заменил способ рисования glBegin()/glEnd() на glDrawElements В комментариях в коде - то что было:

Painter.h
Кликните здесь для просмотра всего текста
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#ifndef PAINTER_H
#define PAINTER_H
 
#include <vector>
 
class Painter
{
public:
    Painter();
    void bar( int x1, int y1, int x2, int y2 );
    void circle( int x, int y, int radius );
 
private:
    void setBarVertices( int x1, int y1, int x2, int y2 );
    void setBarIndices();
    void setBarColor( float r, float g, float b );
    std::vector<int> m_barVertices;
    std::vector<unsigned int> m_barIndices;
    std::vector<float> m_barColors;
 
    void setCircleVertices( int x, int y, int radius );
    void setCircleIndices();
    void setCircleColor( float r, float g, float b );
    std::vector<int> m_circleVertices;
    std::vector<unsigned int> m_circleIndices;
    std::vector<float> m_circleColors;
};
 
#endif // PAINTER_H


Painter.cpp
Кликните здесь для просмотра всего текста
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
#include "Painter.h"
#include <GL/gl.h>
 
Painter::Painter()
{
    m_barVertices.resize( 6 * 3 );
    m_barIndices.resize( 6 );
    m_barColors.resize( 6 * 3 );
 
    m_circleVertices.resize( 6 * 3 );
    m_circleIndices.resize( 6 );
    m_circleColors.resize( 6 * 3 );
}
 
void Painter::bar( int x1, int y1, int x2, int y2 )
{
    setBarVertices( x1, y1, x2, y2 );
    setBarIndices();
    setBarColor( 0, 1, 0 );
 
    // Указываем, откуда нужно извлечь данные о массиве вершин
    glVertexPointer( 3, GL_INT, 0, m_barVertices.data() );
 
    // Указываем, откуда нужно извлечь данные о массиве цветов вершин
    glColorPointer( 3, GL_FLOAT, 0, m_barColors.data() );
 
    // Используя массивы вершин и индексов, строим поверхности
    glDrawElements( GL_TRIANGLES, m_barIndices.size(), GL_UNSIGNED_INT, m_barIndices.data() );
 
//    glBegin( GL_QUADS );
//    {
//        glVertex2f( x1, y1 );
//        glVertex2f( x2, y1 );
//        glVertex2f( x2, y2 );
//        glVertex2f( x1, y2 );
//    }
//    glEnd();
}
 
void Painter::circle( int x, int y, int radius )
{
    setCircleVertices( x, y, radius );
    setCircleIndices();
    setCircleColor( 1, 0, 0 );
 
    // Указываем, откуда нужно извлечь данные о массиве вершин
    glVertexPointer( 3, GL_INT, 0, m_circleVertices.data() );
 
    // Указываем, откуда нужно извлечь данные о массиве цветов вершин
    glColorPointer( 3, GL_FLOAT, 0, m_circleColors.data() );
 
    // Используя массивы вершин и индексов, строим поверхности
    glDrawElements( GL_TRIANGLES, m_circleIndices.size(), GL_UNSIGNED_INT, m_circleIndices.data() );
 
//    glColor3f( 1, 0, 0 );
//    glBegin( GL_POLYGON );
//    {
//        glVertex2f( x + radius, y );
//        glVertex2f( x, y + radius );
//        glVertex2f( x - radius, y );
//        glVertex2f( x, y - radius );
//    }
//    glEnd();
}
 
void Painter::setBarVertices( int x1, int y1, int x2, int y2 )
{
    // Первый треугольник
    // 0
    m_barVertices[0] = x1;
    m_barVertices[1] = y2;
    m_barVertices[2] = 0;
 
    // 1
    m_barVertices[3] = x2;
    m_barVertices[4] = y2;
    m_barVertices[5] = 0;
 
    // 2
    m_barVertices[6] = x1;
    m_barVertices[7] = y1;
    m_barVertices[8] = 0;
 
    // Второй треугольник
    // 3
    m_barVertices[9] = x1;
    m_barVertices[10] = y1;
    m_barVertices[11] = 0;
 
    // 4
    m_barVertices[12] = x2;
    m_barVertices[13] = y2;
    m_barVertices[14] = 0;
 
    // 5
    m_barVertices[15] = x2;
    m_barVertices[16] = y1;
    m_barVertices[17] = 0;
}
 
void Painter::setBarIndices()
{
    for ( unsigned int i = 0; i < 6; ++i ) {
        m_barIndices[i] = i;
    }
}
 
void Painter::setBarColor( float r, float g, float b )
{
    m_barColors[0] = r;
    m_barColors[1] = g;
    m_barColors[2] = b;
 
    m_barColors[3] = r;
    m_barColors[4] = g;
    m_barColors[5] = b;
 
    m_barColors[6] = r;
    m_barColors[7] = g;
    m_barColors[8] = b;
 
    m_barColors[9] = r;
    m_barColors[10] = g;
    m_barColors[11] = b;
 
    m_barColors[12] = r;
    m_barColors[13] = g;
    m_barColors[14] = b;
 
    m_barColors[15] = r;
    m_barColors[16] = g;
    m_barColors[17] = b;
}
 
void Painter::setCircleVertices( int x, int y, int radius )
{
    // 0
    m_circleVertices[0] = x + radius;
    m_circleVertices[1] = y;
    m_circleVertices[2] = 0;
 
    // 1
    m_circleVertices[3] = x - radius;
    m_circleVertices[4] = y;
    m_circleVertices[5] = 0;
 
    // 2
    m_circleVertices[6] = x;
    m_circleVertices[7] = y + radius;
    m_circleVertices[8] = 0;
 
    // 3
    m_circleVertices[9] = x + radius;
    m_circleVertices[10] = y;
    m_circleVertices[11] = 0;
 
    // 4
    m_circleVertices[12] = x;
    m_circleVertices[13] = y - radius;
    m_circleVertices[14] = 0;
 
    // 5
    m_circleVertices[15] = x - radius;
    m_circleVertices[16] = y;
    m_circleVertices[17] = 0;
}
 
void Painter::setCircleIndices()
{
    for ( unsigned int i = 0; i < m_circleIndices.size(); ++i ) {
        m_circleIndices[i] = i;
    }
}
 
void Painter::setCircleColor( float r, float g, float b )
{
    m_circleColors[0] = r;
    m_circleColors[1] = g;
    m_circleColors[2] = b;
 
    m_circleColors[3] = r;
    m_circleColors[4] = g;
    m_circleColors[5] = b;
 
    m_circleColors[6] = r;
    m_circleColors[7] = g;
    m_circleColors[8] = b;
 
    m_circleColors[9] = r;
    m_circleColors[10] = g;
    m_circleColors[11] = b;
 
    m_circleColors[12] = r;
    m_circleColors[13] = g;
    m_circleColors[14] = b;
 
    m_circleColors[15] = r;
    m_circleColors[16] = g;
    m_circleColors[17] = b;
}


Исполняемый файл для Win7: https://yadi.sk/d/0MZdHSrIcnHxA
Исходники на Qt C++: https://github.com/8Observer8/... awElements

Если мы добавим в Scene::initializeGL() строку

C++
1
glPolygonMode( GL_FRONT_AND_BACK, GL_LINE );
то увидим каркас:



Продолжение: 4. Простой загрузчик wavefront (.obj) объектов из Blender на C++
Миниатюры
Нажмите на изображение для увеличения
Название: Triangle.png
Просмотров: 1854
Размер:	7.9 Кб
ID:	2862   Нажмите на изображение для увеличения
Название: Square.png
Просмотров: 1966
Размер:	10.4 Кб
ID:	2863  
Изображения
  
Метки blender, c++
Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 4
Комментарии
  1. Старый комментарий
    Аватар для HighPredator
    Можете пояснить почему вы считаете рисование через glBegin/glEnd устаревшим способом?
    Запись от HighPredator размещена 18.11.2014 в 09:55 HighPredator вне форума
  2. Старый комментарий
    Часто встречаю подобные фразы на форумах:

    glBegin/glEnd, к сожалению, уже deprecated-функционал
    Я тоже начинал с glBegin, glEnd, glVertex.
    Когда мне понадобилось написать первое OpenGL приложение для Android, я обнаружил, что в OpenGL ES уже нет ни glBegin ни glEnd ни glVertex, оно и к лучшему =)
    Так что лучше сразу думать в терминах glDrawArrays, glDrawElements итд.
    Лично для меня рисование с помощью glDrawElements удовлетворяет всем моим потребностям. Позже перейду на рисование с помощью GLSL. Как освою - напишу инструкцию
    Запись от 8Observer8 размещена 18.11.2014 в 12:24 8Observer8 вне форума
  3. Старый комментарий
    Аватар для programina
    Можете пояснить почему вы считаете рисование
    через glBegin/glEnd устаревшим способом?
    Потому что уже давно есть более эффективные новые аналоги этого г.... мамонта.
    Запись от programina размещена 18.11.2014 в 13:43 programina вне форума
  4. Старый комментарий
    Я изменил рисование в игре "Змейка" из предыдущей инструкции на glDrawElements и добавил в конец инструкции. Можете скачать и поиграть (win7) https://yadi.sk/d/0MZdHSrIcnHxA
    Запись от 8Observer8 размещена 18.11.2014 в 15:40 8Observer8 вне форума
 
Новые блоги и статьи
Нейросеть на алгоритме "эстафета хвоста" как перспектива.
Hrethgir 06.05.2026
На десерт, когда запущу сервер. Статья тут https:/ / habr. com/ ru/ articles/ 1030914/ . Автор я сам, нейросеть только помогает в вопросах которые мне не известны - не знаю людей которые знали-бы. . .
Асинхронный приём данных из COM-порта
Argus19 01.05.2026
Асинхронный приём данных из COM-порта Купил на aliexpress термопринтер QR701. Он оказался странным. Поключил к Arduino Nano. Был очень удивлён. Наотрез отказывается печатать русские буквы. Чтобы. . .
попытка написать игровой сервер на C++
pyirrlicht 29.04.2026
попытка написать игровой сервер на плюсах с открытым бесконечным миром. возможно получится прикрутить интерпретатор питон для кастомизации игровой логики. что есть на текущий момент:. . .
Контроль уникальности выбранного документа-основания при изменении реквизита
Maks 28.04.2026
Алгоритм из решения ниже разработан на примере нетипового документа "ЗаявкаНаРемонтСпецтехники", разработанного в КА2. Задача: уведомлять пользователя, если указанная заявка (документ-основание). . .
Благородство как наказание
Maks 24.04.2026
У хорошего человека отношения с женщинами всегда складываются трудно. А я человек хороший. Заявляю без тени смущения, потому что гордиться тут нечем. От хорошего человека ждут соответствующего. . .
Валидация и контроль данных табличной части документа перед записью
Maks 22.04.2026
Алгоритм из решения ниже реализован на примере нетипового документа, разработанного в КА2. Задача: контроль и валидация данных табличной части документа перед записью с учетом регламента компании. . .
Отчёт о затраченных материалах за определенный период с макетом печатной формы
Maks 21.04.2026
Отчёт из решения ниже размещён в конфигурации КА2. Задача: разработка отчёта по затраченным материалам за определённый период, с возможностью вывода печатной формы отчёта с шапкой и подвалом. В. . .
Отчёт о спецтехнике находящейся в ремонте
Maks 20.04.2026
Отчёт из решения ниже размещен в конфигурации КА2. Задача: отобразить спецтехнику, которая на данный момент находится в ремонте. Есть нетиповой документ "Заявка на ремонт спецтехники" который. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru