Qt+OpenGl — Основы. Часть 1

    Данная cтатья вводная, рассчитана на знакомство с Qt+OpenGL для новичков, которые планируют изучать Qt (как кросс-платформенный инструментарий разработки ПО на языке программирования C++) + OpenGL (как графическую библиотеку).

    Что потребуется новичку:
    1) Qt Creator (имеет хорошую встроенную документацию и подсказки во время набора кода). Скчаать
    2) doc.qt.nokia.com — официальная документация на английском языке
    3) doc.crossplatform.ru — документация на русском языке
    4) Обязательно прочесть про Qt и OpenGL
    5) Отличная статья для начала изучения

    Что мы будем делать
    Поскольку данная статья посвящена конкретно основам, в нашей задаче будет следующее:
    1) Разобрать как создается приложение
    2) Как рисовать объекты
    3) Как работать с указателем мыши и событиями(нажатие клавиш на клавиатуре и на мышке)
    4) Работа с таймером
    5) Создадим нашу первую банальную игру. Будем с помощью таймера, случайным образом перемещать квадрат. После наведения на квадрат указателя и кликнув по нему левой кнопки мышки, в случае попадания по квадрату, будем прибавлять к полученным очкам +1.



    Создаем проект

    При открытии Qt Creator, начинаем создавать новый проект.
    Выбираем проект Qt Widget -> GUI приложение Qt
    В разделе Информация о классе снимает галочку для создания формы.
    В результате действий мы получим проект с файлами:
    opengl.pro — необходим для компиляции нашего проекта
    mainwindow.h — для объявления всех глобальных данных
    main.cpp
    mainwindow.cpp — методы нашей программы

    Подключение библиотек

    В файле *.pro вашего проекта в строке Qt += необходимо дописать opengl для того, чтоб подключить использование библиотеки opengl. Таким же образом подключаются и другие библиотеки.

    В файле mainwindow.h — если у вас имя по умолчанию выбрано, необходимо подключить:
    #include <QGLWidget>
    #include <QtOpenGL>
    #include <QTimer>
    


    Предопределение для нас нужных методов и переменных

    Открываем mainwindow.h
    В первую очередь сменим:
    class MainWindow: public QMainWindow
    на
    class MainWindow: public QGLWidget
    Это потому, что QMainWindow — класс для вывода простого окна, а т.к. мы будем работать с opengl, нам понадобится QGLWidget — это класс для вывода графики, реализующий функции библиотеки OpenGL.

    Теперь предопределим переменные и методы
    
    protected:
        int geese_size; // Сторона квадрата
        int point; // набранные очки
        int gdx, gdy; // Координаты квадрата
        int cax, cay, cbx, cby; // Координаты курсора (текущие и начальные(при зажатии клавиши мыши) для выделение области)
        int wax ,way; // Размеры окна нашей программы
        bool singling; // Для выделение области, если true то рисуем прямоугольник по координатам cax, cay, cbx, cby
        void self_cursor(); // метод для рисования своего курсора
        void initializeGL(); // Метод для инициализирования opengl
        void resizeGL(int nWidth, int nHeight); // Метод вызываемый после каждого изменения размера окна
        void paintGL(); // Метод для вывода изображения на экран
        void keyPressEvent(QKeyEvent *ke); // Для перехвата нажатия клавиш на клавиатуре
        void mouseMoveEvent(QMouseEvent *me); // Метод реагирует на перемещение указателя, но по умолчанию setMouseTracking(false)
        void mousePressEvent(QMouseEvent *me); // Реагирует на нажатие кнопок мыши
        void mouseReleaseEvent(QMouseEvent *me); // Метод реагирует на "отжатие" кнопки мыши
        void singling_lb(); // Рисуем рамку выделенной области
        void geese(); // Рисуем квадрат по которому кликать для получения очков
    


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

    
    protected slots:
        void geese_coord(); // Определяем координаты объектов
    


    Принцип построения изображения

    QGLWidget так устроен, что при первой инициализации класса он автоматически вызывает методы в следующем порядке:
    При запуске: initializeGL()->resizeGL()->paintGL()
    При изменении размера окна: resizeGL()->paintGL()
    updateGL() вызывает paintGL()

    initializeGL — необходимо использовать для глобальных настрое построения изображения, которые нет необходимости указывать при построении кадра.
    resizeGL — служит для построения размера окна. Если в ходе работы изменится размер окна, но не изменить область просмотра, то при увеличении размера можно наблюдать непредсказуемые явления.
    paintGL — этот метод будет выстраивать каждый наш кадр для отображения.
    
        glClear(GL_COLOR_BUFFER_BIT); // чистим буфер
        glMatrixMode(GL_PROJECTION); // устанавливаем матрицу
        glLoadIdentity(); // загружаем матрицу
        glOrtho(0,500,500,0,1,0); // подготавливаем плоскости для матрицы
        // BlendFunc позволяет работать в альфа режиме, например если нам нужно указывать прозрачность
        // glEnable(GL_BLEND);
        // glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    
        qglColor(Qt::white); // Дальше рисуем белым цветом
        //  renderText позволяет писать текст на экран, так же можно указать различный стиль (читаем QFont)
        renderText(10, 10 , 0, QString::fromUtf8("Вы набрали %1 очков:").arg(17), QFont() , 2000);
    
        // glBegin и glEnd - обозначают блок для рисования объекта(начало и конец), glBegin принимает параметр того, что нужно рисовать.
        glBegin(GL_POLYGON);
            glColor4f(0,1,0, 0.25);// Цвет которым рисовать
            glVertex2f(200, 300); // Точка 1 из 4 - отсчет по часовой стрелке
            glVertex2f(300, 300);
            glVertex2f(300, 400);
            glVertex2f(200, 400);
        glEnd();
        swapBuffers();
    


    Для чего двойная буферизация

    PaintGL сразу картинку не рисует на экран, а заносит в буфер, а по запросу swapBuffers() заменяет текущие изображение на то, что появилось в буфере. Сама по себе буфериция позволяет более корректно заменять изображение, чтоб не происходили скачки на экране.

    События клика мыши

    mousePressEvent() — метод автоматически вызывается при нажатии клавиш мыши. В передаваемых параметрах можно получить различную информацию например какой именно кнопкой было сделано нажатие и по какой точке по координатам.
    -Данное событие в нашем примере используется для определения куда кликнули мышью, затем если наши координаты находятся в поле квадрата, то добавляем к нашим очкам + 1 и перестраиваем наш кадр.
    -Так же используем для определения начальных координат для выделения области на экране, при зажатии и перемещении указателя.

    Событие перемещения указателя мыши

    mouseMoveEvent() — автоматически вызывается при изменении координат указателя мыши. Но есть одно Но, по умолчанию установлено setMouseTracking(false), поэтому событие вызывается только при условии нажатия клавиш мыши, для того, чтоб метод вызывался даже без нажатия необходимо установить setMouseTracking(true).
    — Данный метод мы используем для получения текущего положения указателя, чтоб перестроить выделение области или нарисовать собственный курсор.

    Событие когда «отжимается» кнопка мыши

    mouseReleaseEvent() — автоматически вызывается при условии «отжатия» кнопки мыши. Так же принимает различные параметры.
    — В данном случае мы используем метод, чтоб стереть с экрана выделенную нами область.

    Событие нажатие клавиш на клавиатуре

    keyPressEvent() — метод вызывается при событии, когда нажимается кнопка на клавиатуре.
    — В нашем примере, мы используем этот метод, для того, чтоб переопределить координаты нашего квадрата и переместить его в новое место.

    Таймер

    QTimer — позволяет нам создать поток, который будет слушать сигналы и запускать соответственные слоты.
    — В данном случае мы создаем таймер, который будет ждать 750мс после чего он завершает свою работу, отправляя нам сигнал timeout() , но мы при окончании сигнала будем не останавливать работу, а снова запускать слот на переопределение координат квадрата, по которому нужно кликать для того, чтоб набрать очки.
    
        QTimer *timer = new QTimer(this);
        connect(timer, SIGNAL(timeout()), this, SLOT(geese_coord()));
        timer->start(750);
    


    Задание по данному материалу для усвоения.

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

    Заключение!

    Большинство из немногого написанного здесь, в нашей первой примитивной игре просто не нужно. Но хочу отметить еще раз: "Статья вводная, рассчитана на знакомство с Qt+OpenGL". Так же если Вы заметили написанные таким образом программы можно компилировать для любой операционной среды.

    Готовый вариант рабочего кода можно взять тут:

    Посмотреть и скачать исходники
    Скачать игру для windows
    Скачать игру для Linux

    P.S. В дальнейшем, если не против, буду продолжать написание уроков. Например следующим уроком, будем создавать игру со стрельбой например в уток.

    Если есть вопросы — пишите в комментариях или в личку.

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 51

      0
      А где же картинки?
        +1
        image
        Синий квадрат — через каждый 750мс, случайным образом переезжает на новое место.
        Красная стрелка — мы отключили курсор, и нарисовали свой.
        Зеленый полупрозрачный квадрат — это результат выделения, с помощью зажатия левой клавиши мыши и изменения положения указателя.
        +22
        Иногда мне скучно, и я занимаюсь указанием неточностей и ошибок в статьях, которые задевают интересующую меня тему, простите меня за это :)

        1. Не QT, а Qt. Официально. Это все равно, что если бы к вам обращались не «Иван» а скажем «ИВАН» или «ИвАн»

        2.
        mainwindow.h — для объявления всех глобальных данных

        Бред! Правильно сказать — для объявления класса главного окна.

        mainwindow.cpp — методы нашей программы

        Бред! Правильно сказать — определение методов класса главного окна.

        3.
        #include <QTimer>
        в mainwindow.h ни к чему, это можно сделать в .cpp файле, т. к. в самом хедере не используются объекты класса QTimer. Перенос подключения заголовков из .h файла в .cpp уменьшает время компиляции. Оставляейте в .h файлах только действительно нужные заголовки.

        4.
        protected:
           [skipped длинный список переменных и прочих методов]

        Назовите хоть одну объективную причину, по которой вы засунули все данные и все не переопределяющие методы в секцию protected, а не private (этот же вопрос относится и к слоту geese_coord)

        5.
        QString::fromUtf8("Вы набрали %1 очков:").arg(17)

        Ваш игрок всегда будет иметь 17 очков.

        6.
        *Event() — если метод объявлен…

        Метод объявлен,… уже,… всегда,… без вашего вмешательства… в базовом классе… как виртуальный
        А это значит что вы его лишь переопределяете, кстати, если вы его не переопределите, метод с этим именем тоже будет вызван, в базовом классе, конечно же.

        7.
        В данном случае мы создаем таймер, который будет ждать 750мс после чего он завершает свою работу, отправляя нам сигнал timeout()

        Таймер, не завершает свою работу, он посылает сигнал и сбрасывает счетчик. Таймер завершает работу либо при вызове метода stop(), либо разрушении объекта таймера.

        Я не буду судить OpenGL часть кода, потому что не силен в этом.
        Я не буду комментировать стиль кода, наличие огромного количества неименованных констант и т. д. — это очевидно.

        Выводы:
        1. Это не статья для новичков, это статья от новичка.
        2. Это вредная статья для новичков, она учит программировать неряшливо, и не понимая сути происходящего в коде.

        Еще раз извините, если задел чьи-то чувства.
          0
          Вы правы, статья от новичка (3 месяц знакомства С++ в целом) в этом вопросе, но я постарался доступным языком рассказать это и другим таким же как я.
          Все замечания обязательно учту и даже благодарен за откровенное мнение.
          P.S. Вы извините статью снимать не буду, пусть люди учатся на чужих ошибках.
            +2
            Я и не побуждаю ее снимать, ваша статья, ваше право, я просто высказал мнение.
              –1
              Перенос подключения заголовков из .h файла в .cpp уменьшает время компиляции
              Почему? При изменении хэдера, возможно. Но почему время компиляции должно изменяться при изменении файла реализации?
                0
                Уточнение: "#include" просто вставляет код в место, где сама директива написана, то есть на стадии компиляции (после вставки кода препроцессором), перекомпилируетсяровным счетом столько же кода. Но! Подозреваю, что выигрыш будет на стадии прохода MOC'ом. Если так, объясните, пожалуйста, почему?
                  +1
                  Ну может я несколько неправильно выразил мысль, в этом конкретном случае навряд ли будет приращение скорости компиляции. Но это является, как говорится Best Practice и вот почему:

                  1. Представим что у нас есть
                  A.h, который включается в B.h (но там не обязателен, т. к. его информация используется только в реализации и не используется в интерфейсе), а файл B.h включается еще в 100+ файлов, внимание вопроссколько раз будет прочитан и вставлен (как вы верно заметили на место #include) файл A.h?

                  2. Наиболее важное (но как правило редко касается библиотечных заголовков, т.к. они постоянны в долгосрочном периоде) инкрементальная сборка.

                  Все тот же пример A.h, который включается в B.h, а файл B.h включается еще в 100+ файлов, которые включаются еще в 1000+ файлов, так вот одно малюсенькое изменение в файле A.h приведет к тому, что и B.h и все что включает его, и все что включает то что включает его, будет перекомпилировано заново.
                    0
                    Согласен с 1 и 2. Я просто имел в виду именно перекомпиляцию хедеров :-)
                    Как правило, чем легче хэдер, тем лучше.
                    Но! Ситуация бывает какая:

                    1) Ускорение при перекомпиляции .cpp и .h
                    2) Ускорение при перекомпиляции только .h
                    3) Неизменность при перекомпиляции только .cpp

                    Я просто указал на наличие ситуации номер 3 :-)
                      0
                      А ну само собой. Я нечетко выразил мысль. My fault.
                        0
                        Зато в будущем Вы, возможно, будете выражаться точнее :-)

                        Просто мне показалось, что было бы достаточно интересно, указать на небольшую неточность в комментарии, который уточняет пост :-)
                          0
                          Абсолютно согласен.
                            0
                            Скорость компиляции на самом деле тема больная для проектов на Qt (видимо из-за дополнительных манипулиций MOC'а, в смысле необходимости компилировать дополнительно «отмоканные» файлы), так что ваше замечание имеет особый смысл в контексте поста. У меня самого сейчас полная перекомпиляция текущего проекта на Qt занимает больше минуты.
                  0
                  да просто потому, что файл заголовков может быть вставлен во многие файлы .cpp, и компилятор «смотрит» кучу ненужных вещей, которые вы влепили в хедер, каждую компиляцию, теряя зря время.
                    0
                    Не, Вы не поняли. Я про то, что изменения скорости компиляции быть не должно при изменении одного и того же файла реализации. Потому что в данном случае компилируется только он, и ничто другое. А при компиляции хэдера ситуация понятна, я так и написал выше. Проблема в изначальной фразе:
                    Перенос подключения заголовков из .h файла в .cpp уменьшает время компиляции
                    Время компиляции уменьшается только при перекомпиляции хэдера(ов).
                    +1
                    Это побочный эффект, а прямой эффект — избегание лишних зависимостей.
                +4
                Я добавлю по OpenGL, что сразу бросается в глаза:

                1. glOrtho(0,500,500,0,1,0); // подготавливаем плоскости для матрицы

                это конечно сильно :) glOrtho умножает текущую матрицу на ортогональную матрицу, ее аргументы — координаты плоскостей отсечения слева, справа, снизу, сверху, сблизи и издалека. эти координаты относительны, если принять за единицу ширину и высоту вашего viewport, то будет примерно так: glOrtho( 0, 1, 0, 1, 0, 1);
                в приведенном коде мало того, что все вверх ногами, так еще и кто-то перепутал координаты векторов с пиксельными координатами. Оно-то рисует, но вычисления происходят при этом абсолютно дикие. Соответственно и glVertex2f() оперирует этими «дикими» пиксельными координатами.

                2. glBegin() и glEnd() — моветон. Есть много способов нарисовать прямоугольник, а вот если этот код придется портировать на OpenGL-ES, то glBegin() и glEnd() попортят вам крови. Так что это дело лучше не использовать, тем более что эти 6 строк рисования можно заменить одной, glDrawArrays() например.

                Глубже не смотрел, лень.
                  +1
                  Насчёт glBegin/glEnd поддерживаю. А вот насчёт первого не так всё однозначно. Если мы строим графический интерфейс, то пиксельные координаты вполне нормальное явление, почему вычисления «дикие»? Почему не оперировать пикселами, если так удобнее? Лично я не вижу смысла принимать ширину и высоту за 1 и пересчитывать координаты мышки вручную, если это может делать OpenGL, причём этап умножения на матрицу будет происходит в любом случае, поэтому ручное деление — это как раз лишнее действие.
                    0
                    Послушайте, но это даже смешно. Я не хочу доказывать, что glOrtho(), как и все остальное в OpenGL работает в векторных координатах. Исключения есть, но они в основном касаются некоторых расширений языка (например работа с GL_TEXTURE_RECTANGLE_EXT происходит в пиксельных координатах).
                    Дело вовсе не в удобстве, почитайте Ред Бук, базовые понятия о координатах.
                      0
                      Я так понимаю заминусовал некий гуру OpenGL, не читавший Ред Бук. ;)
                      Ну-ну. Минусовать моё сообщение можно. Вот только мозгов это вряд ли прибавит.
                        +1
                        Я разве говорил обратное? Что OpenGL работает не с векторными координатами. Просто если мы в OpenGL делаем некое подобие пользовательского интерфейса, то сделать так, чтобы векторные координаты соответствовали пиксельным — это нормальная практика. Иначе прийдётся прогонять нормализованные координаты мышки через матрицу обратную к текущей, чтобы получить векторные координаты. Зачем?
                          +1
                          Если нужно нарисовать два прямоугольника, то можно использовать координаты, приведенные к пиксельным. При этом, если уж на то пошло, я бы использовал gluOrtho2D(0, width, 0, height);
                          потому что в этом случае даже после различных glTranslatef() координат они останутся целочисленными, что подходит к пикселям.

                          Если же вам нужно отрендерить более сложную сцену, с возможностью зума и пр., — нормальная практика как раз использовать координаты создаваемого мира, а не пиксельные.
                            0
                            В исходниках (которые скачать, а не в статье приведены) так и есть — width x heigth. В статье не расчитано, что размер меняется, а жёстко 500х500. Ну, да, статью новичок в OpenGL писал.
                              0
                              Вся ирония сейчас в том, что я написал ради примера 2 строки:
                              glOrtho(0,500,500,0,1,0); 
                              renderText(10, 10 , 0, QString::fromUtf8("Вы набрали %1 очков:").arg(17), QFont() , 2000);
                              

                              Если кто смотрел исходный код, то там изначально было:
                              glOrtho(0,wax,way,0,1,0); 
                              renderText(10, 10 , 0, QString::fromUtf8("Вы набрали %1 очков:").arg(point), QFont() , 2000);
                              

                              где wax и way это текущий размер нашего окна, point — количество набранных очков.

                              P.S. Ну буду доказывать, что этот вариант правильный, то сам факт того, что половину комментариев данного поста крутятся вокруг этих строк.
                                0
                                Опечатка, писал это:
                                P.S. Не буду доказывать, что этот вариант правильный, но сам факт того, что половину комментариев данного поста крутятся вокруг этих строк.
                    0
                    Да, вы правы, ляпов конечно много. Но если сделать то же самое по всем канонам и академично, любой новичок подумает что Qt+OpenGL это невероятно сложно и не для среднего ума. А так по крайней мере попробуют, why not?
                      0
                      Для новичков придумали Qt3D
                        +3
                        Программирование вообще такая штука — не для среднего ума.
                          –1
                          Профессиональное — да. Любительское — для всех.
                            –1
                            Что такое любительское программирование?
                            Давайте тогда сделаем еще любительскую нейро-хирургию,
                            любительскую атомную энергетику,
                            и любительскую космонавтику.

                            Да и даже касательно любителей, ответьте, почему тот, кто выращивает, скажем, морковку на даче для себя, не кричит на всю округу, что он агроном, и не бежит повышать урожайность морковки к своему соседу за деньги. Но есть масса людей, которые прочитав половину книжки «С++ за 21 день», уже заявляют: «Я программист!» И большинство из них скромно не добавляет слово «любитель».

                              0
                              Пусть себе развлекается народ со «средним умом», вас это задевает что-ли? Или на пятки наступают?
                                +1
                                Существует любительское всё. Все когда-то начинали. Только начинающие врачи тренируются на трупах, а начинающие программисты на нервах опытных программистов. =)
                                  –1
                                  Не путайте любительское с начинающим.
                        0
                        >… программирования C++) + OpenQL… [2 строчка]

                        Возможно ошибаюсь, разве не OpenGL?
                          0
                          Если качать приложение для Windows то «глючно» работает, постоянно мерцает.запуск был под win7 Ult. 32bit
                            0
                            У меня и того хуже: s017.radikal.ru/i431/1111/0d/3291aaa7e931.png
                              0
                              Это где?
                                0
                                А, сорри, забыл. Win XP SP3.
                                  0
                                  Странно, именно на ней и на 7(32-bit) проверял — все ок.
                              0
                                +1
                                саркастически ляпну — убийца Qr-кода
                              –3
                              > Обязательно прочесть про QTt

                              Обнаружил, что не знаю, что такое QTt, срочно пошёл читать, и ничего про QTt не нашёл.
                              Это какая-то новая технология?
                                +1
                                Не нужно показывать свое остроумие и об опечатках говорить в комментариях. Ссылка явно указывает на Qt, если Вы не заметили.
                                  0
                                  Не знаю, почему вы решили, что это остроумие и сразу наставили минусов.
                                    0
                                    1) Т.к. в тексте явно была опечатка, после того, как исправлял регистр буквы «t»
                                    2) Я в этой теме никому минус не ставил и Вам в том числе вот скрин: s017.radikal.ru/i440/1111/76/cc5b63ee2de4.jpg

                              • UFO just landed and posted this here
                                  0
                                  > Теперь предопределим переменные и методы
                                  > // в описании заголовочного файла

                                  На научном языке это называется объявление полей и методов класса.
                                  Определеняются же здесь только поля, определение методов, соответственно, в файле описания.
                                    0
                                    А давайте создадим блог «С++ для новичков» ну или «Qt для новичков», а?
                                      0
                                      Там писать будут «новички» или «материалы для новичков»? (как бы внезапно, но разница есть)

                                      Впрочем предлагаю создать оба…
                                        0
                                        Как бы хоть разница и есть, но пускай и то, и другое там будет =)
                                    • UFO just landed and posted this here

                                      Only users with full accounts can post comments. Log in, please.