4334 / 1503 / 101
Регистрация: 12.04.2009
Сообщений: 2,342
1

Ответы на 7 самых частых вопросов по WinForms

30.03.2010, 02:45. Показов 812638. Ответов 3
Метки faq (Все метки)

Ответы на 7 самых частых вопроса по WinForms



1. Как создать вторую форму

2. Как передать данные из одной формы в другую
2.1 Изменение модификатора доступа
2.2 Использование открытого свойства/метода
2.3 Передача данных в конструктор Form2
2.4 Передача ссылки в конструктор
2.5 Используем свойство 'родитель'
2.6 Используем отдельный класс
2.7 Использование функций обратного вызова
2.7.1 Передача метода в конструктор
2.7.2 Создание отдельного класса с делегатом
2.8 Использование событий

3. Как получить доступ к контролу из другого потока
3.1 Простой и неправильный способ (CheckForIllegalCrossThreadCalls)
3.2 Использование методов Invoke/BeginInvoke

4. Как динамически добавить/удалить контрол.
4.1 Добавление
4.2 Удаление

5. Как создать массив контролов.

6. Как получить доступ к контролу по имени.

7. Как пройтись по всем однотипным котролам.



1. Как создать вторую форму
Любая форма представляет из себя класс, унаследованный от Form.
Экземпляр главной формы создается в файле Program.cs по умолчанию.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
namespace NS
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1()); // <- вот тут
        }
    }
}
Чтобы отобразить вторую форму, надо создать экземпляр этого класса (Form2), например в обработчике события главной
C#
1
2
3
4
5
6
private void button1_Click(object sender, EventArgs e)
{
    Form2 f = new Form2(); // создаем
    f.ShowDialog(); // показываем
    f.Show() // или так
}
При этом ShowDialog() блокирует главную форму, т.е. управление вернется в нее, только по закрытию второй формы, а Show() просто отображает вторую форму, т.е. будут доступны обе формы.

2. Как передать данные из одной формы в другую
Часто возникает необходимость передать данные из одной формы в другую, я придумал 7 способов, у каждого свои недостатки и достоинства.
2.1 Изменение модификатора доступа (добавлено 24 июля 2017: лучше воздержаться от применения данного способа)
В Form2 Установить модификатор доступа для контрола/поля public
В любом месте Form1
C#
1
2
3
Form2 f = new Form2();
f.ShowDialog();
this.textBox1.Text = f.textBox1.Text;
+ Самый быстрый в реализации и удобный способ
- Противоречит всем основам ООП
- Возможна передача только из более поздней формы в более раннюю
- Форма f показывается только с использованием ShowDialog(), т.е. в первую форму управление вернется только по закрытию второй. Избежать этого можно, сохранив ссылку на вторую форму в поле первой формы

2.2 Использование открытого свойства/метода. Способ очень похож на первый
В классе Form2 определяем свойство (или метод)
C#
1
2
3
4
5
6
7
public string Data
{
    get
    {
        return textBox1.Text;
    }
}
В любом месте Form1
C#
1
2
3
Form2 f = new Form2();
f.ShowDialog();
this.textBox1.Text = f.Data;
+ Противоречит не всем основам ООП
- Минусы те же

2.3 Передача данных в конструктор Form2
Изменяем конструктор Form2
C#
1
2
3
4
5
6
7
8
public Form2(string data)
{
    InitializeComponent();
    //Обрабатываем данные
    //Или записываем их в поле
    this.data = data;
}
string data;
А создаем форму в любом месте Form1 так:
C#
1
2
3
Form2 f = new Form2(this.textBox1.Text);
f.ShowDialog();
//Или f.Show();
+ Простой в реализации способ
+ Не нарушает ООП
- Возможна передача только из более ранней формы в более позднюю

2.4 Передача ссылки в конструктор
Изменяем конструктор Form2
C#
1
2
3
4
5
6
7
public Form2(Form1 f1)
{
    InitializeComponent();    
    //Обрабатываем данные
    //Или записываем их в поле
    string s = f1.textBox1.Text;
}
А создаем форму в любом месте Form1 так, т.е. передаем ей ссылку на первую форму
C#
1
2
3
Form2 f = new Form2(this);
f.ShowDialog();
//Или f.Show();
+ Доступ ко всем открытым полям/функциям первой формы
+ Передача данных возможна в обе стороны
- Нарушает ООП

2.5 Используем свойство 'родитель'
При создании второй формы устанавливаем владельца
C#
1
2
3
Form2 f = new Form2();
f.Owner = this;
f.ShowDialog();
Во второй форме определяем владельца
C#
1
2
3
4
5
6
Form1 main = this.Owner as Form1;
if(main != null)
{
    string s = main.textBox1.Text;
    main.textBox1.Text = "OK";
}
+ Доступ ко всем открытым полям/функциям первой формы
+ Передача данных возможна в обе стороны
+ Не нарушает ООП

2.6 Используем отдельный класс
Создаем отдельный класс, лучше статический, в основном namespace, т.е. например в файле Program.cs
C#
1
2
3
4
static class Data
{
    public static string Value { get; set; }
}
Его открытые свойства/методы доступны из любой формы.
C#
1
Data.Value = "111";
+ Самый удобный способ, когда данные активно используются несколькими формами.

2.7 Использование функций обратного вызова
2.7.1 Передача метода в конструктор
Создаем в основном namespace делегат
C#
1
public delegate void MyDelegate(string data);
В Form1 создаем метод, который будет обрабатывать принятые данные
C#
1
2
3
4
void func(string param)
{
    // обработка
}
Создаем вторую форму так:
C#
1
2
Form2 f = new Form2(new MyDelegate(GetData));
f.ShowDialog();
При этом изменяем конструктор второй формы, чтобы он принимал делегат
C#
1
2
3
4
5
6
MyDelegate d;
public Form2(MyDelegate sender)
{
    InitializeComponent();
    d= sender;
}
И в любой момент отправляем данные
C#
1
d(textBox1.Text);
Полный код
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
//Program.cs
using System;
using System.Windows.Forms;
namespace NS
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
    }
    public delegate void MyDelegate(string data);
}
//Form1.cs
using System;
using System.Windows.Forms;
namespace NS
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
        private void button1_Click(object sender, EventArgs e)
        {
            Form2 f = new Form2(new MyDelegate(func));
            f.ShowDialog();
        }
        void func(string param)
        {
            MessageBox.Show(param + "!");
        }
    }
}
//Form2.cs
using System;
using System.Windows.Forms;
namespace NS
{
    public partial class Form2 : Form
    {
        private MyDelegate d;
        public Form2(MyDelegate sender)
        {
            InitializeComponent();
            d = sender;
        }
        private void button1_Click(object sender, EventArgs e)
        {
            d(textBox1.Text);
        }
    }
}
285
Programming
Эксперт
94731 / 64177 / 26122
Регистрация: 12.04.2006
Сообщений: 116,782
30.03.2010, 02:45
Ответы с готовыми решениями:

Непонятен пункт 2.5 из "Ответы на 7 самых частых вопросов по WinForms"
Вопрос по пункту 2.5 из &quot;Ответы на 7 самых частых вопроса по WinForms&quot;. Вот код: // в форме 1...

Ответы на 7 самых частых вопросов по Windows Forms, C++/CLI Edition
Ответы на 7 самых частых вопросов по Windows Forms C++/CLI Edition Эта статья является...

Ответы на 7 самых частых вопросов по Windows Forms, C++/CLI Edition - свой способ передачи данных между формами
Способ с использованием макросов. Средней сложности в реализации и понимании, не знаю, нарушает ли...

Список Ваших вопросов Платону Щукину и его ответы
Данная тема будет предназначана для публикования Ваших вопросов и ответов с службой поддержки...

Подсчет вопросов на которые были даны не правильные ответы. Visual C++
Здравствуйте не могли бы подсказать по этому заданию: После теста обучаемому сообщается на какие...

3
4334 / 1503 / 101
Регистрация: 12.04.2009
Сообщений: 2,342
30.03.2010, 02:45  [ТС] 2
2.7.2 Создание отдельного класса с делегатом
Создаем в основном namespace отдельный класс
C#
1
2
3
4
5
public static class Data
{
    public delegate void MyEvent(string data);
    public static MyEvent EventHandler;
}
В первой форме добавляем обработчик
C#
1
2
3
4
void func(string param)
{
    MessageBox.Show(param);
}
и инициализируем EventHandler
C#
1
Data.EventHandler = new Data.MyEvent(func);
Вторую форму создаем обычным способом и вызываем из нее
C#
1
Data.EventHandler(textBox1.Text);
Полный код
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
//Program.cs
using System;
using System.Windows.Forms;
namespace NS
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
    }
    public static class Data
    {
        public delegate void MyEvent(string data);
        public static MyEvent EventHandler;
    }
}
//Form1.cs
using System;
using System.Windows.Forms;
namespace NS
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            Data.EventHandler = new Data.MyEvent(func);
        }
        private void button1_Click(object sender, EventArgs e)
        {
            Form2 f = new Form2();
            f.ShowDialog();
        }
        void func(string param)
        {
            MessageBox.Show(param + "!");
        }
    }
}
//Form2.cs
using System;
using System.Windows.Forms;
namespace NS
{
    public partial class Form2 : Form
    {
        public Form2()
        {
            InitializeComponent();
        }
        private void button1_Click(object sender, EventArgs e)
        {
            Data.EventHandler(textBox1.Text);
        }
    }
}

+ Наиболее гибкий способ передачи данных
- Сложен в реализации и понимании

3. Как получить доступ к контролу из другого потока
С элементами управления можно работать только из того потока в котором они были созданы. При обращении из другого потока будет сгенерировано исключение InvalidOperationException с текстом "Cross-thread operation not valid: Control accessed from a thread other than the thread it was created on." (Недопустимая операция в нескольких потоках: попытка доступа к элементу управления не из того потока, в котором он был создан.). Возможные решения:

3.1 Простой и неправильный способ
Отменяем проверку, из какого потока используется контрол
C#
1
System.Windows.Forms.Control.CheckForIllegalCrossThreadCalls = false;
Для одного раза может и сработать, но делать так крайне не рекомендуется.

3.2 Использование методов Invoke/BeginInvoke
Эти методы выполняют указанные делегаты в том потоке, в котором контрол был создан.
Invoke вызывает делегат синхронно, BeginInvoke - асинхронно.
Чтобы определить, требуется ли Invoke используйте свойство InvokeRequired.
Например, объявляем делегат
C#
1
delegate void Del(string text);
и вызываем Invoke
C#
1
textBox1.Invoke(new Del((s) => textBox1.Text = s), "newText");
Вместо объявления новых делегатов можно использовать готовые, Action или Func
Пример готового, потоко-безопасного метода
C#
1
2
3
4
5
void SetTextSafe(string newText)
{
    if (textBox1.InvokeRequired) textBox1.Invoke(new Action<string>((s) => textBox1.Text = s), newText);
    else textBox1.Text = newText;
}
4. Как динамически добавить/удалить контрол.
4.1 Добавление
Пример динамического создания кнопки:
C#
1
2
3
4
5
6
7
8
9
System.Windows.Forms.Button button1 = new System.Windows.Forms.Button(); // создаем контрол
button1.Location = new System.Drawing.Point(101, 50); // устанавливаем необходимые свойства
button1.Name = "button1";
button1.Size = new System.Drawing.Size(75, 23);
button1.TabIndex = 0;
button1.Text = "button1";
button1.UseVisualStyleBackColor = true;
button1.Click += new System.EventHandler(button1_Click); // button1_Click - функция обработчик события нажатия на кнопку
Controls.Add(button1); // добавляем на форму
Если непонятно как создать другие контролы - откройте функцию InitializeComponent, которая находится в конструкторе вашей формы. Это функция - код генерируемый дизайнером, посмотрите, как дизайнер создает тот или иной компонент и скопируйте код.
4.2 Удаление
C#
1
2
Controls.Remove(button1);
button1.Dispose();

5. Как создать массив контролов.
Точно также, как обычный массив
C#
1
2
3
4
5
6
7
8
9
10
11
TextBox[] tb = new TextBox[10];
for (int i = 0; i < tb.Length; i++)
{
    tb[i] = new System.Windows.Forms.TextBox();
    tb[i].Location = new System.Drawing.Point(101, 50 + i * 30);
    tb[i].Name = "textBox" + i.ToString();
    tb[i].Size = new System.Drawing.Size(75, 23);
    tb[i].TabIndex = i;
    tb[i].Text = "textBox" + i.ToString();                
    Controls.Add(tb[i]);
}
Теперь получить доступ к конкретному текстбоксу можно по индексу:
C#
1
tb[2].Text = "newText";
6. Как получить доступ к контролу по имени.
C#
1
(Controls["textBox1"] as TextBox).Text = "newText";
7. Как пройтись по всем однотипным контролам.
C#
1
2
3
4
5
6
7
8
9
//Пример обхода всех TextBox-ов
foreach (Control control in Controls)
{
    TextBox tb = control as TextBox;
    if (tb != null)
    {
        tb.Text = "Text";
    }
}
Альтернативный вариант (.NET 3.5 и выше):
C#
1
2
3
4
5
6
// Пример обхода всех TextBox-ов
// Вместо TextBox можно подставить другой элемент управления
foreach (var tb in Controls.OfType<TextBox>())
{
    tb.Text = "Text";
}
57
Эксперт .NET
5476 / 4246 / 1211
Регистрация: 12.10.2013
Сообщений: 12,248
Записей в блоге: 2
28.09.2014, 20:43 3
Позволю себе дополнить вопрос по связи и передаче данных двух форм, формы и класса. Подобные темы возникают очень часто, но в данном FAQ нет главного примера - использование событий, которые, как я считаю, являются самым верным способом для передачи к-либо данных.
Итак, вот участки кода. Для лучшего понимания прикрепляю архив с проектом. Имеем:
1.Главная форма, с двумя датагридами и двумя кнопками.
2.Дополнительная форма с тремя текстовыми полями и одной кнопкой.
3.Отдельный класс.
Логика работы: по нажатию на кнопку получения данных из доп.формы, открывается окно второй формы. Заполняем текстовые поля, и по нажатию кнопки форма закрывается, а данные из полей оказываются в гриде 1. По нажатию кнопки получения данных из класса создается объект класса, в конструкторе класса формируется таблица, которая при вызове метода класса передает данные в осн. форму, где они отображаются в гриде 2.
Также уточню, что передачу таблиц я выбрал для наглядности, и передавать таким способом можно все что угодно, используя приведенный шаблон
PS.В моем коде специально не использовались анонимные делегаты и лямбда, для облегчения восприятия новичками.

Код основной формы:
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
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
 
namespace FormsCommunicatoinTest
{
    public partial class MainForm : Form
    {
        //Конструктор формы
        public MainForm()
        {
            InitializeComponent();
            //Подключение обработчика получения данных из формы
            btnGetDataFromForm.Click += new EventHandler(btnGetDataFromForm_Click);
            //Подключение обработчика получения данных из класса
            btnGetDataFromClass.Click += new EventHandler(btnGetDataFromClass_Click);
        }
 
        //Обработчик кнопки получения данных из класса
        void btnGetDataFromClass_Click(object sender, EventArgs e)
        {
            //Создание объекта класса
            OtherClass other = new OtherClass();
            //Подключение обработчика получения данных из экземпляра класса
            other.sendDataFromClass += new EventHandler<UserEventArgs>(other_sendDataFromClass);
            //Вызываем метод получения данных
            other.SendTableToForm();
        }
 
        //Обработчик события получения данных из класса
        void other_sendDataFromClass(object sender, UserEventArgs e)
        {
            //Получаем таблицу из нашего именованного класса-аргумента события
            DataTable tableFromClass = e.SendingTable;
            //Выводим таблицу для показа в грид
            dataGridViewOtherClass.DataSource = tableFromClass;
        }
 
        //Обработчик кнопки получения данных из формы
        void btnGetDataFromForm_Click(object sender, EventArgs e)
        {
            //Создаем дочернюю форму
            ParentForm pForm = new ParentForm();
 
            //Подключение обработчика события в дочерней форме
            pForm.sendDataFromFormEvent += new EventHandler<UserEventArgs>(pForm_sendDataFromFormEvent);
 
            //Выводим ее для заполнения текстовых полей
            pForm.ShowDialog();
        }
 
        //Обработчик события получения данных из дочерней формы
        void pForm_sendDataFromFormEvent(object sender, UserEventArgs e)
        {
            //Получаем таблицу из нашего именованного класса-аргумента события
            DataTable tableFromForm = e.SendingTable;
            //Выводим таблицу для показа в грид
            dataGridViewParentForm.DataSource = tableFromForm;
        }
    }
}
Код доп. формы:
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
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
 
 
namespace FormsCommunicatoinTest
{
    public partial class ParentForm : Form
    {
        //Событие для передачи данных
        public event EventHandler<UserEventArgs> sendDataFromFormEvent;
 
        //Конструктор формы
        public ParentForm()
        {
            InitializeComponent();
   
            //Подключение обработчика события нажатия кнопки
            btnSendData.Click += new EventHandler(btnSendData_Click);
        }
 
        //Обработчик нажатия кнопки
        void btnSendData_Click(object sender, EventArgs e)
        {
            //Создаем таблицу
            DataTable dt = new DataTable("MyDataTable");
 
            //Создаем столбцы
            DataColumn columnID = new DataColumn("ID", typeof(string));
            DataColumn columnFName = new DataColumn("FirstName", typeof(string));
            DataColumn columnLName = new DataColumn("LastName", typeof(string));
 
            //Добавляем столбцы в таблицу
            dt.Columns.AddRange(new DataColumn[] { columnID, columnFName, columnLName });
 
            //Формируем строку из данных в текстовых полях
            DataRow row = dt.NewRow();
            row["ID"] = txtID.Text;
            row["FirstName"] = txtFirstName.Text;
            row["LastName"] = txtLastName.Text;
 
            //Добавляем строку в таблицу
            dt.Rows.Add(row);
 
            //Генерируем событие с именованным аргументом
            //в класс аргумента передаем созданную таблицу
            if (sendDataFromFormEvent != null)
                sendDataFromFormEvent(this, new UserEventArgs(dt));
 
            //Закрываем форму
            this.Close();
        }
    }
}
Код доп. класса:
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
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
using System.Data;
 
namespace FormsCommunicatoinTest
{
    class OtherClass
    {
        //private поле таблицы
        private DataTable tableFromClass;
 
        //Событие для передачи данных
        public event EventHandler<UserEventArgs> sendDataFromClass;
 
        //Конструктор класса
        public OtherClass()
        {
            //Метод формирования таблицы
            tableFromClass = GetTable();
        }
 
        //public метод отправки данных
        public void SendTableToForm()
        {
            //Генерируем событие с именованным аргументом
            //в класс аргумента передаем созданную таблицу
            if (sendDataFromClass != null)
                sendDataFromClass(this, new UserEventArgs(tableFromClass));
        }
 
        //private метод формирования таблицы
        private DataTable GetTable()
        {
            //Создаем таблицу
            DataTable dt = new DataTable("MyDataTable");
 
            //Создаем столбцы
            DataColumn columnID = new DataColumn("ID", typeof(string));
            DataColumn columnFName = new DataColumn("FirstName", typeof(string));
            DataColumn columnLName = new DataColumn("LastName", typeof(string));
 
            //Добавляем столбцы в таблицу
            dt.Columns.AddRange(new DataColumn[] { columnID, columnFName, columnLName });
 
            //Формируем строку из данных в текстовых полях
            DataRow row = dt.NewRow();
            row["ID"] = "1";
            row["FirstName"] = "Nicolai";
            row["LastName"] = "Ivanov";
 
            //Добавляем строку в таблицу
            dt.Rows.Add(row);
 
            //Возвращаем таблицу из метода
            return dt;
        }
    }
}
Код класса для аргумента событий:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
using System.Data;
 
namespace FormsCommunicatoinTest
{
    //public класс, наследник от EventArgs
    //Используется как аргумент для передачи данных
    //в событиях
    public class UserEventArgs:EventArgs
    {
        //public readonly поле класса
        public readonly DataTable SendingTable;
        //Конструктор класса с параметром
        public UserEventArgs(DataTable dt)
        {
            SendingTable = dt;
        }
    }
}
Ну и собственно сам проект в архиве.
Спасибо за внимание.
Вложения
Тип файла: rar FormsCommunicatoinTest.rar (51.7 Кб, 1688 просмотров)
45
tezaurismosis
24.08.2015, 08:37     Ответы на 7 самых частых вопросов по WinForms
  #4
 Комментарий модератора 
Если вы нашли неточность или опечатку, хотите что-то добавить к написанному в статье - обсуждение ведётся в отдельной теме:
https://www.cyberforum.ru/faq/thread1519017.html
0
IT_Exp
Эксперт
87844 / 49110 / 22898
Регистрация: 17.06.2006
Сообщений: 92,604
24.08.2015, 08:37

Найти вероятность того, что студент, выбирая ответы наугад, ответит на 10 вопросов
Машина-экзаменатор содержит 12 вопросов, на каждый из которых предлагается 4 варианта ответа....

Ответы на Комплект вопросов сертиф экз "1С:Бухгалтерия 8" ред. 1.6
У кого есть ответы на &quot;Комплект вопросов сертифицированного экзамена по программе &quot;1С:Бухгалтерия...

на ответы@mail.ru на один из моих вопросов, где я уточнил, что использую ubuntu 10.10 мне сказали, что...
на ответы@mail.ru на один из моих вопросов, где я уточнил, что использую ubuntu 10.10 мне сказали,...

Найти 3 самых больших и 3 самых маленьких числа в двумерном массиве
Дан двумерный массив размерностью m x n, нужно найти и вывести 3 самых больших и 3 самых маленьких...

По данной информации определить самых уникальных учеников и самых средних
Доброго времени суток)Пожалуйста,помогите написать программу к задаче: Имеется информация об...


Искать еще темы с ответами

Или воспользуйтесь поиском по форуму:
4
Закрытая тема Создать тему
Опции темы

КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2023, CyberForum.ru