Форум программистов, компьютерный форум, киберфорум
Наши страницы

C++ Builder

Войти
Регистрация
Восстановить пароль
 
Рейтинг: Рейтинг темы: голосов - 24, средняя оценка - 4.92
volvo
Супер-модератор
Эксперт Pascal/DelphiАвтор FAQ
24029 / 16010 / 4848
Регистрация: 22.10.2011
Сообщений: 28,337
Записей в блоге: 5
#1

Создание форм авторизации и заставки (Splash-формы) в приложениях - C++ Builder

05.12.2015, 15:33. Просмотров 5684. Ответов 1
Метки нет (Все метки)

Создание формы для авторизации в приложении.

Очень многие делают это совершенно неправильно: уже после того, как написано приложение добавляют еще одну форму, и хотят сделать так, чтобы сначала открывалась новая форма, а потом, когда пользователь введет пароль для входа в программу, скрыть ее и уже продолжать создавать формы как и раньше, до добавления формы авторизации. Это в корне неправильный подход. По нескольким причинам:
  1. Если при таком решении форма авторизации будет назначена главной формой приложения, ее потом нельзя будет закрыть, можно будет только скрыть, иначе при закрытии главной формы будет автоматически закрыто само приложение
  2. Главная (и все остальные) форма приложения вообще не должны создаваться до тех пор, пока пользователь не подтвердит своих прав на использование данного продукта. Иначе очень просто будет чуть более продвинутому пользователю показать уже созданную, но скрытую форму, чем если бы ее вообще еще не существовало

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

Решение очень простое: создаем форму авторизации, убираем ее из списка автоматически создаваемых (Project -> Options -> Forms, перенести форму из списка Auto-create в список Available)

На форме расположим 2 поля ввода (edUser для ввода логина и edPass для ввода пароля) и 2 кнопки (btnLogIn для подтверждения и btnCancel для отмены авторизации)

По нажатию на кнопку LogIn на форме авторизации либо просто проверяем соответствие пары логин/пароль:
C++
1
2
3
4
5
6
7
8
9
10
11
void __fastcall TForm3::Button1Click(TObject *Sender)
{
    if (edUser->Text == "volvo" && edPass->Text == "test")
    {
        ModalResult = mrOk; // в случае соответствия возвращаем один результат
    }
    else
    {
        ModalResult = mrCancel; // иначе - другой. Этот же результат - и по нажатию на кнопку Cancel
    }
}
, либо делаем чуть более интересно: пару логин/хеш пароля храним в Ini-файле, и при нажатии на LogIn проверяем соответствие:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void __fastcall TForm3::Button1Click(TObject *Sender)
{
    std::auto_ptr<TIniFile> ini (new TIniFile(ChangeFileExt(ParamStr(0), ".ini"))); // #include <IniFiles.hpp>
    std::auto_ptr<TIdHashMessageDigest5> hash(new TIdHashMessageDigest5); // #include <IdHashMessageDigest.hpp>
 
    if(ini->ReadString("users", edUser->Text, "") == hash->HashStringAsHex(edPass->Text, TIdTextEncoding::Default))
    {
        ModalResult = mrOk;
    }
    else
    {
        ModalResult = mrCancel;
    }
}
Разумеется, можно придумать еще много способов хранения паролей или их хешей, но основная идея этого поста - дать общее понимание того, что же делать с этой формой дальше, как ее показать, чтобы только после успешной аутентификации выполнение программы шло дальше.

Для этого в класс формы авторизации нужно добавить статический метод Execute:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class TForm3 : public TForm
{
__published:    // IDE-managed Components
    // ...
private:    // User declarations
public:     // User declarations
    __fastcall TForm3(TComponent* Owner);
    static bool __fastcall Execute(); // <--- Вот его описание
};
 
// а вот - реализация:
bool __fastcall TForm3::Execute()
{
    TForm3 *frm = new TForm3(0);
    int res = frm->ShowModal();
    delete frm;
    return res == mrOk; // вернуть признак успешной авторизации
}
А теперь открываем главный файл проекта (Project -> View source), и меняем порядок создания форм с того, что было раньше:
C++
1
2
3
4
5
6
         Application->Initialize();
         Application->MainFormOnTaskBar = true;
 
         Application->CreateForm(__classid(TForm1), &Form1);
         Application->CreateForm(__classid(TForm2), &Form2);
         Application->Run();
на такой:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
        if(TForm3::Execute()) // сначала показываем форму авторизации
        {
            // и только если результат авторизации успешен - создаем главную форму приложения
            Application->Initialize();
            Application->MainFormOnTaskBar = true;
            Application->CreateForm(__classid(TForm1), &Form1);
            Application->Run();
        }
        else
        {
            // иначе показываем сообщение о невозможности войти в программу,
            // и приложение завершается, не создавая никаких форм
            Application->MessageBox(
                _T("Вы не можете пользоваться приложением, так как не прошли авторизацию"),
                _T("Защита приложения")
            );
        }
Это самый простой метод авторизации, когда пользователь просто вводи свой логин/пароль, и в случае, если он ввел их правильно, он получает возможность использовать приложение. Чуть более интересно будет добиться того, чтобы в приложении было несколько групп пользователей, скажем, админ и просто пользователь. Тут тоже есть несколько вариантов реализации:
  1. Сделать одну форму, на которой в зависимости от некоего глобального флажка динамически создавать набор компонентов, нужный для представителя группы пользователей, к которой принадлежит залогинившийся (способ очень хорош тем, что на форме оказываются только необходимые данной группе пользователей компоненты, но для сложных форм с большим количеством компонентов на них создавать все динамически может быть долго по времени)
  2. Все так же сделать одну форму, но уже в дизайнере набросать на нее две группы компонентов (например, на разные вкладки TPageControl, или на разные TPanel), и по значению того же глобального флажка показывать одну вкладку (панель), и скрывать все остальные (этот способ гораздо проще, но теперь на форме, хоть и скрытыми, одновременно присутствуют все компоненты для всех групп пользователей, и все обработчики событий)
  3. Сделать разные формы для разных групп пользователей и создавать/показывать только нужную, остальные вообще не создавать.

Вариант №3 - самый оптимальный. Для его реализации нужно чуть-чуть изменить описание класса формы авторизации:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum AuthStatus {authNone, authUser, authAdmin}; // это список всех возможных групп пользователей
 
class TForm3 : public TForm
{
__published:    // IDE-managed Components
    TLabeledEdit *edUser;
    TLabeledEdit *edPass;
    TButton *btnLogin;
    TButton *btnCancel;
    void __fastcall btnCancelClick(TObject *Sender);
    void __fastcall btnLoginClick(TObject *Sender);
private:    // User declarations
public:     // User declarations
    __fastcall TForm3(TComponent* Owner);
    static AuthStatus __fastcall Execute();
    static AuthStatus status; // Добавляем флажок
 
};
AuthStatus TForm3::status = authNone; // инициализация статического поля класса
и реализацию обработчика клика по кнопке и метода Execute() :
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
void __fastcall TForm3::btnLoginClick(TObject *Sender)
{
    // разумеется, тут точно так же можно сделать проверку паролей
    // из INI-файла, только тогда в файле придется еще хранить
    // роль каждого пользователя в системе
    if (edUser->Text == "volvo" && edPass->Text == "test")
    {
        status = authAdmin;
        ModalResult = mrOk;
    }
    else
    if (edUser->Text == "nata" && edPass->Text == "second")
    {
        status = authUser;
        ModalResult = mrOk;
    }
    else
    {
        ModalResult = mrCancel;
    }
}
 
// ---------------------------------------------------------------------------
AuthStatus __fastcall TForm3::Execute()
{
    TForm3 *frm = new TForm3(0);
    TForm3::status = authNone;
    frm->ShowModal();
    delete frm;
    return TForm3::status; // Возвращаем статус пользователя
}
, и изменить главный файл приложения:
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
        AuthStatus st = TForm3::Execute(); // получаем статус пользователя
        if (st != authNone) // если авторизация была успешной
        {
            Application->Initialize();
            Application->MainFormOnTaskBar = true;
            switch (st)
            {
            case authAdmin: // залогинился админ - создаем админскую форму
                Application->CreateForm(__classid(TForm1), &Form1);
                // тут можно создать еще формы, которые нужны только админу
                break;
            case authUser: // простой пользователь - создаем пользовательскую
                Application->CreateForm(__classid(TForm2), &Form2);
                // те формы, которые нужны только для пользовательского аккаунта - можно создать здесь
                break;
            }
 
            // при необходимости - создаем другие общие формы здесь
            Application->Run();
        }
        else // авторизация не пройдена
        {
            Application->MessageBox(
                _T("Вы не можете пользоваться приложением, так как не прошли авторизацию"),
                _T("Защита приложения")
            );
        }
Таким образом можно не только отслеживать успешность/неуспешность авторизации, но и создавать свои формы для любого количества групп пользователей.

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

В качестве примера реализаций привожу два проекта: первый - иллюстрирует чтение логина/пароля из INI, а второй - создание разных форм для каждой из групп пользователей (проекты были созданы и протестированы в Builder XE2, для создания INI-файла использовался код:
C++
1
2
3
4
5
6
7
8
9
#include <memory>
#include <IdHashMessageDigest.hpp>
 
// ...
 
    std::auto_ptr<TIniFile> ini (new TIniFile(ChangeFileExt(ParamStr(0), ".ini")));
    std::auto_ptr<TIdHashMessageDigest5> hash(new TIdHashMessageDigest5);
    ini->WriteString("users", "volvo", hash->HashStringAsHex("test", TIdTextEncoding::Default));
    ini->WriteString("users", "nata", hash->HashStringAsHex("second", TIdTextEncoding::Default));
)

Проект №1 auth_faq_simple.7z

Проект №2 auth_faq_groups.7z

P.S. Замечания/пожелания/вопросы по этой теме отсылайте мне в ЛС, я постараюсь добавить еще интересные методы авторизации.
17
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Similar
Эксперт
41792 / 34177 / 6122
Регистрация: 12.04.2006
Сообщений: 57,940
05.12.2015, 15:33
Здравствуйте! Я подобрал для вас темы с ответами на вопрос Создание форм авторизации и заставки (Splash-формы) в приложениях (C++ Builder):

Создание заставки на Форме1 и появление Формы 2 после закрытия Формы1 - C++ Builder
Здравствуйте. Создал заставку на форме1. Это форма главная. Через промежуток времени она исчезает по таймеру. Должна появляться Форма2, но...

Динамическое создание форм с сообщением по образцу уже созданной формы. Как? - C++ Builder
Добрый вечер форумчане. Есть такое задание. Не знаю как реализовать. Нужно, чтобы при совпадении времени, заданного в 1-ой форме, и...

создание заставки - C++ Builder
Помогите пожалуйтса написать программу. Нужно сделать заставку(анимацию),в Builder 6(на С++),чтобы при запуске программы на рабочем столе...

Создание заставки для программы - C++ Builder
Доброго времени суток. Нашел вот такой код типа самоучитель по созданию заставки для программы вот его содержание. \\\\\\\\\\\\\\ ...

Закрытие формы авторизации завершает программу - C++ Builder
Здравствуйте, есть две формы, одна форма авторизации(main) и вторая форма. После того как нажали вход и вошли, хочу чтоб первая...

Форма авторизации и регистрации открывается до главной формы - C++ Builder
Здравствуйте, делал форму авторизации по данному уроку Ссылка. Все работает отлично, но мне нужно, что бы из формы входа можно было открыть...

1
volvo
Супер-модератор
Эксперт Pascal/DelphiАвтор FAQ
24029 / 16010 / 4848
Регистрация: 22.10.2011
Сообщений: 28,337
Записей в блоге: 5
16.12.2015, 01:20  [ТС] #2
Создание заставок (splash-форм) для приложения.


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


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

  1. Когда все остальные формы приложения уже созданы и работают, создаем новую форму, которая будет заставкой.
  2. Сразу же идем в свойства приложения, и убираем новую форму из списка автоматически создаваемых (Project->Options->Forms переносим форму из списка Auto-Create в список Available)
  3. Поскольку мы хотим, чтобы форма какое-то время показывалась на экране, то кладем на нее таймер, задаем максимальное время показа заставки, и по таймеру проверяем, не вышло ли еще это время:

    C++
    1
    2
    3
    4
    5
    6
    7
    8
    
    void __fastcall TSplashForm::Timer1Timer(TObject *Sender)
    {
        static int SplashTime = 3000;
        if((::GetTickCount() - FStart > SplashTime) && CanCloseSplash)
        {
            this->Close();
        }
    }
    (о назначении CanCloseSplash я расскажу чуть позже)
  4. Естественно, если не записать в FStart значение GetTickCount() при создании заставки, ничего работать не будет. Поэтому пишем обработчик создания формы:
    C++
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    void __fastcall TSplashForm::FormCreate(TObject *Sender)
    {
        FStart = ::GetTickCount();
        CanCloseSplash = false; // начальная инициализация всех полей
        Timer1->Interval = 100;
        
        // если кого-то раздражает показ заставки на самом верху,
        // следующую строку можно закомментировать
        FormStyle = fsStayOnTop; 
    }
    Ну вот практически и все, что нужно сделать с формой. Осталось только правильно ее показать.
  5. Идем в главный файл проекта (Project -> View source), и добавляем там показ формы-заставки:
    C++
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
             SplashForm = new TSplashForm(0); // создаем
             SplashForm->Show(); // и показываем заставку
     
             // теперь продолжаем инициализацию и создание главной формы приложения
             Application->Initialize();
             Application->MainFormOnTaskBar = true;
             Application->CreateForm(__classid(TMainForm), &MainForm);
             
             // сразу после того, как главная форма была создана,
             // можно закрывать заставку (если до этого время не вышло
             // и она не закрылась самостоятельно)
     
             if(SplashForm)
             {
                SplashForm->CanCloseSplash = true; // даем разрешение закрыть заставку
             }
             // и запускаем метод Run, что приведет к показу главной формы
             Application->Run();

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

Можно сделать чуть по-другому: сразу после того, как создалась главная форма, закрывать заставку. Как ни странно, для этого понадобится изменить всего лишь 2 символа в обработчике таймера:
C++
1
2
3
4
5
6
7
8
9
10
11
void __fastcall TSplashForm::Timer1Timer(TObject *Sender)
{
    static int SplashTime = 3000;
    // Меняем "И" на "ИЛИ", и теперь либо закончится время
    // показа заставки, либо CanCloseSplash станет true; заставка
    // закроется при выполнении любого из этих условий
    if((::GetTickCount() - FStart > SplashTime) || CanCloseSplash)
    {
        this->Close();
    }
}
Тестовые проекты
BCB6:
bcb6_splash_simple.7z

XE2:
splash_simple.7z


Это все, конечно, прекрасно, но хочется же красивостей. Давайте попробуем создать заставку в виде изображения с прозрачностью.

Изменений в главном файле проекта почти не будет, единственное, что я поменял - это вместо метода Show вызвал самописный метод ShowSplash:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
         SplashForm = new TSplashForm(0);
         SplashForm->ShowSplash("logo.png"); // <---
 
         Application->Initialize();
         Application->MainFormOnTaskBar = true;
         Application->CreateForm(__classid(TMainForm), &MainForm);
 
         if(SplashForm)
         {
            SplashForm->CanCloseSplash = true;
         }
 
         Application->Run();
А вот реализация класса формы-заставки поменялась значительно. Во-первых, в класс формы добавился прототип того самого метода ShowSplash():
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class TSplashForm : public TForm
{
__published:    // IDE-managed Components
    TTimer *Timer1;
    void __fastcall Timer1Timer(TObject *Sender);
    void __fastcall FormCreate(TObject *Sender);
    void __fastcall FormClose(TObject *Sender, TCloseAction &Action);
    void __fastcall FormDestroy(TObject *Sender);
private:    // User declarations
    int FStart;
    bool FCanCloseSplash;
 
public:     // User declarations
    __fastcall TSplashForm(TComponent* Owner);
    void __fastcall ShowSplash(String fn); // <---
 
    __property bool CanCloseSplash = {read = FCanCloseSplash, write = FCanCloseSplash};
};
Ну, и реализация (в XE+ использовался модуль Vcl.Imaging.pngimage.hpp)
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
void __fastcall Create_Premult_Bitmap(Graphics::TBitmap *bm)
{
    for (int y = 0; y < bm->Height; y++)
    {
        Vcl::Imaging::Pngimage::TByteArray *dst =
            (Vcl::Imaging::Pngimage::TByteArray*)bm->ScanLine[y];
        for (int x = 0; x < bm->Width; x++)
        {
            dst->data[x * 4] = dst->data[x * 4] * dst->data[x * 4 + 3] >> 8;
            dst->data[x * 4 + 1] =
                dst->data[x * 4 + 1] * dst->data[x * 4 + 3] >> 8;
            dst->data[x * 4 + 2] =
                dst->data[x * 4 + 2] * dst->data[x * 4 + 3] >> 8;
        };
    };
}
 
void __fastcall Load_Logo(Graphics::TBitmap *bm, String FileName)
{
    std::auto_ptr<TPngImage> png (new TPngImage);
    png->LoadFromFile(FileName);
    bm->PixelFormat = pf32bit;
    bm->Width = png->Width;
    bm->Height = png->Height;
    for (int y = 0; y < png->Height; y++)
    {
        Vcl::Imaging::Pngimage::TByteArray *alpha = png->AlphaScanline[y];
        TRGBLine *src = (TRGBLine*)png->Scanline[y];
        Vcl::Imaging::Pngimage::TByteArray *dst =
            (Vcl::Imaging::Pngimage::TByteArray*)bm->ScanLine[y];
        for (int x = 0; x < png->Width; x++)
        {
            dst->data[x * 4] = src->data[x].rgbtBlue;
            dst->data[x * 4 + 1] = src->data[x].rgbtGreen;
            dst->data[x * 4 + 2] = src->data[x].rgbtRed;
            dst->data[x * 4 + 3] = alpha->data[x];
        }
    }
}
 
void __fastcall TSplashForm::ShowSplash(String fn)
{
    BorderStyle = bsNone;
    Position = poDesktopCenter;
    this->FormStyle = fsStayOnTop;
 
    SIZE s1;
 
    BLENDFUNCTION blend = {AC_SRC_OVER, 0, 255, AC_SRC_ALPHA};
 
    TPoint p = Classes::Point(0, 0);
    ::SetWindowLong(Handle, GWL_EXSTYLE, ::GetWindowLong(Handle,
        GWL_EXSTYLE) | WS_EX_LAYERED);
 
    std::auto_ptr<Graphics::TBitmap> image (new Graphics::TBitmap);
    Load_Logo(image.get(), ExpandFileName(ExtractFilePath(ParamStr(0))) + fn);
    Create_Premult_Bitmap(image.get());
    s1.cx = image->Width;
    s1.cy = image->Height;
 
    Width = image->Width;
    Height = image->Height;
 
    std::auto_ptr<Graphics::TBitmap> layer (new Graphics::TBitmap);
    layer->PixelFormat = pf32bit;
    layer->Width = s1.cx;
    layer->Height = s1.cy;
    layer->Canvas->Brush->Color = RGB(0, 0, 0);
    layer->Canvas->FillRect(layer->Canvas->ClipRect);
 
    ::AlphaBlend(layer->Canvas->Handle, 0, 0, image->Width, image->Height,
        image->Canvas->Handle, 0, 0, image->Width, image->Height, blend);
    Create_Premult_Bitmap(layer.get());
    ::UpdateLayeredWindow(this->Handle, 0, NULL, &s1, layer->Canvas->Handle, &p,
        0, &blend, ULW_ALPHA);
 
    Show();
}
Тестовые проекты
BCB6:
собрать не получилось. Я уже говорил, что использую Portable версию Билдера, туда нельзя добавлять новые компоненты, нельзя даже просто откомпилировать pas-файл, поэтому работать с PNG не представляется возможным

XE2:
splash_non_rect.7z



А теперь о грустном. У первого метода есть недостаток (на самом деле он есть и у второго, но поскольку картинка обычно статичная - он не так бросается в глаза). Заключается он в следующем: если при инициализации основной формы (как раз в то время, когда показывается заставка) не вызывать периодически Application->ProcessMessages(), как я делал в тестовом приложении, то форма не будет обновляться. Она просто повиснет в том состоянии, в котором была создана, и на нее не положишь ни ProgressBar, ни Label, в которые можно вывести какую-то информацию о том, на каком этапе находится сейчас процесс загрузки приложения.

Чтобы этого избежать можно:
  1. Вынести все действия по инициализации в отдельный поток.
  2. Создать и показать заставку
  3. Запустить поток инициализации
  4. По завершению работы потока закрывать заставку

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

Тестовые проекты
BCB6:
bcb6_splash_adv.7z

XE2:
splash_adv.7z



Ну вот и все, пожалуй, о Splash-формах.


P.S. Замечания/пожелания/вопросы приветствуются, обращайтесь в ЛС, если будет что-то интересное - добавлю в пост.
18
MoreAnswers
Эксперт
37091 / 29110 / 5898
Регистрация: 17.06.2006
Сообщений: 43,301
16.12.2015, 01:20
Привет! Вот еще темы с ответами:

Статические и динамические формы. Взаимодействие форм. - C++ Builder
Доброго времени суток. В общем у меня есть, созданная динамически форма, с компонентами на ней. Мне нужно написать обработчик, при...

Создание MainForm после успешной авторизации - C++ Builder
В общем застрял на следующем: есть 4 формы: MainForm, с которой происходит запуск Form2, Form3 и AuthForm, которая представляет собой...

Вызов функции формы при их переборе(форм) - C++ Builder
Здравствуйте, встал такой вопрос как можно вызвать функцию из срр файла формы при их переборе. Перебирать я их уже научился: for(int i...

Создание и удаление форм - C++ Builder
нашел темку эту http://www.cyberforum.ru/cpp-builder/thread57473.html?uri=/cpp-builder/thread57473.html прочитал неработает этот код ...


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

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

КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2017, vBulletin Solutions, Inc.
Рейтинг@Mail.ru