본문 바로가기

VEDA 복습

VEDA 18일차 ~ 20일차 : Qt 프로그래밍

반응형

 

실습 위주다 보니, 산발적이여서 별도로 정리할려고 한다. Chat GPT롤 활용했다.

 

Qt 플레임워크 : C++ 기반 크로스 플랫폼 애플리케이션 프레임워크로 GUI 어플리케이션 제작에 주로 활용됨

 

Qt의 주요 구조

  • QtCore : 비 GUI 관련 핵심 기능을 제공, 이벤트 루프, 신호-슬롯 메커니즘, 문자열 처리, FILE IO, 멀티 쓰레드 처리 등
  • QtGui : GUI 요소를 그리기 위한 기능을 제공, 이벤트 처리, 2D 그래픽 처리, 텍스트 렌더링, 이미지 처리 등, QPainter, QPixmap, QImage 등이 포함된다.
  • QtWidgets : 실제 GUI 위젯(버튼, 라벨, 윈도우 등)을 제공하는 모듈, QWidget, QMainWindow, QPushButton, QLabel 등 다양한 UI 요소 포함된다.
  • 기타 모듈 : QtNetwork, QtMultimedia, QtSql, QtTest 등등

Qt의 주요 개념

  • QObject : Qt의 거의 모든 글래스의 기본 클래스
  • Signal-Slot 시스템 사용 가능 : 이벤트인 Signal(비정의 함수, argument를 그대로 반환하여, slot으로 전달)이 발생하면 Slot 함수를 실행시킨다. 비동기 적이고, 느슨한 결합 구조를 제공한다. connect() 함수를 사용
  • connect(sender, &Class::signalName, receiver, &Class::slotFunction);
  • Object Tree 구조 지원
  • 동적 프로퍼티 추가 및 메타 오프젝트 시스템(MOC) 활용 가능

위젯 시스템(QWidget 기반 클래스들)

모든 GUI 요소는 QWidget 또는 파생 클래스

 

  • QWidget: 모든 위젯의 기본 클래스
  • QMainWindow: 메뉴, 툴바, 상태바를 포함하는 메인 윈도우
  • QDialog: 모달/모달리스 대화 상자
  • QPushButton, QLabel, QLineEdit: 각종 위젯 클래스

레이아웃 시스템

  • 위젯의 자동 배치를 위한 레이아웃 매니저 제공
  • 종류: QHBoxLayout, QVBoxLayout, QGridLayout, QFormLayout
  • 코드나 디자이너 툴에서 지정 가능

프로그램 실행 흐름 : QApplication 객체 생성(이벤트 루프 관련, 시작점) -> 윈도우 객체 생성(UI 요소 생성, 시그널/슬롯 연결) -> show()(실제로 보이도록 함) -> 이벤트 루프(exec() 루프 진입)

#include <QApplication>
#include <QPushButton>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    QPushButton button("Hello Qt!");
    button.show();
    return app.exec();
}

 

 

디자인의 변천(심플하고 실용적인 방향으로, 너무 예쁘지도 않게, 너무 못생기지도 않게)

 

UI 기획

내부구현 : 플랫폼, 자원, 시간, 인력 등

소프트웨어 내부 구현 : 데이터(static, dynamic), 메모리, 스토리지, 클래스 구조

구현, 테스트

 

QObject -> QWidget -> QPushButton, QLayerout, etc

QWidget은 인스턴스 생성 후 show 함수로 창을 띄울 수 있다.

기본적으로 value() <- getter 함수, setValue() <- setter 함수로 구성된다.

Qt는 다양한 위젯을 제공한다. 대부분의 위젯이 사용 방법이 같다.(객체 선언 및 생성 -> 설정 -> signal/slot 연결 etc)

QObject는 위젯들이 활용하기 좋게 QString, QChar 등 다양한 데이터 타입을 제공한다.

큰 요소 ⊃ 작은 요소 ⊃ 더 작은 요소 방식으로 프로그램 구성

 

예시: 빠르게 1 ~ 25까지 순서대로 클릭하는 게임

Widget.h

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QPushButton>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGridLayout>
#include <QTimer>
#include <QLabel>

QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACE

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

private:
    Ui::Widget *ui;

    //시간 측정 객체

    //현재 상태 : 대기 상태, 진행 상태(상호 교환시 발생하는 일, 해야하는 일)
    int state{0}; // 0 : wait, 1 : play
    void setState(int st);
    int getState();

    //현재 눌러야 할 버튼 번호
    int targetNum;

private:
    QVBoxLayout* vLayout; //canvas
    QHBoxLayout* hLayout; //control
    QGridLayout* gLayout; //main

    QLabel* timerLabel;
    QTimer* timer;
    int timee;

    QPushButton* startButton;
    QPushButton* resetButton;
    QPushButton* numButtons[25];

    void initRandomNumber();
    void clearNumber();

signals:
    void clear();

private slots:
    void startButtonPushed();
    void resetButtonPushed();
    void numButtonPushed();
    void timerUpdate();
};
#endif // WIDGET_H

 

Widget.cpp

#include "widget.h"
#include "./ui_widget.h"

Widget::Widget(QWidget *parent)
    : QWidget(parent)
{
    //ui->setupUi(this);

    /*Layout*/
    vLayout = new QVBoxLayout(); //total
    hLayout = new QHBoxLayout(); //start + timer + end
    gLayout = new QGridLayout(); //main

    vLayout->addLayout(hLayout);
    vLayout->addLayout(gLayout);
    setLayout(vLayout);

    /*Timer*/
    timer = new QTimer();
    timerLabel = new QLabel();
    timerLabel->setStyleSheet("font:32pt; font-weight:bold;");
    timerLabel->setText("00:00");
    connect(timer, &QTimer::timeout, this, &Widget::timerUpdate);



    /*Button*/
    startButton = new QPushButton("Start");
    connect(startButton, &QPushButton::clicked, this, &Widget::startButtonPushed);

    resetButton = new QPushButton("Reset");
    connect(resetButton, &QPushButton::clicked, this, &Widget::resetButtonPushed);

    hLayout->addWidget(startButton, 0);
    hLayout->addWidget(timerLabel, 1);
    hLayout->addWidget(resetButton, 2);

    for(int i = 0; i < 25; i++){
        numButtons[i] = new QPushButton(this); //어느 객체와 연결된 버튼인가?
        gLayout->addWidget(numButtons[i], i/5, i%5);
        connect(numButtons[i], &QPushButton::clicked, this, &Widget::numButtonPushed);
    }
    connect(this, &Widget::clear, this, &Widget::resetButtonPushed);

    initRandomNumber();
    setState(0);
    timerLabel->setText("00:00");
}

Widget::~Widget()
{
    delete startButton;
    delete startButton;

    for(int i = 0; i < 25; i++) delete numButtons[i];

    delete hLayout;
    delete gLayout;
    delete vLayout;
}

void Widget::numButtonPushed(){
    //현재 누른 버튼 번호 확인
    //그 번호랑 targetNum 일치하는 지 확인
    //일치하지 않으면 잘못눌럿으므로 아무 일도 안일어남
    //일치하면 버튼 사라짐 + num++

    QPushButton* btn = (QPushButton*)sender();
    int num = btn->text().toInt();
    if(num == targetNum){
        btn->setEnabled(false);
        targetNum++;
        if(targetNum > 25) emit clear();
    }
}

void Widget::startButtonPushed(){
    setState(1);
}

void Widget::resetButtonPushed(){
    setState(0);
}

void Widget::initRandomNumber(){
    /*game initialization*/
    timer->start();
    timee = 0;

    //target number init
    targetNum = 1;

    std::vector<int> nums(25);
    std::iota(nums.begin(), nums.end(), 1);

    std::srand(time(NULL));
    std::random_shuffle(nums.begin(), nums.end());

    for(int i = 0; i < 25; i++){
        numButtons[i]->setText(QString("%1").arg(nums[i]));
        //numButtons[i]->setProperty("num", nums[i]); 동적 속성 부여
        numButtons[i]->setEnabled(true);
        numButtons[i]->show();
    }
}

void Widget::clearNumber(){

    timer->stop();
    timerLabel->setText(QString("%1:%2").arg(timee/1000).arg((timee%1000)/10));

    for(int i = 0; i < 25; i++){
        numButtons[i]->setText(" ");
        numButtons[i]->setEnabled(false);
        numButtons[i]->show();
    }
}

void Widget::setState(int st){
    state = st;
    if(state == 0){
        clearNumber();
    }
    else{
        initRandomNumber();
    }
}

int Widget::getState(){
    return state;
}

void Widget::timerUpdate(){
    timee++;
    timerLabel->setText(QString("%1:%2").arg(timee/1000).arg(timee%1000));
}

 

순발력 게임 : 초록색이 나오면 누르기!!

헤더

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QTimer>
#include <QLabel>
#include <QPushButton>
#include <QVBoxLayout>

QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACE

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

private:
    //Ui::Widget *ui;

    /*layer*/
    QVBoxLayout *layout;

    /*element*/
    QTimer *sysTimer;
    int systime;
    int random_start;
    int to;

    QPushButton *button;

private:
    int state; //0 for wait, 1 for playing, 2 for result
    char buttonColor; // 'r', 'g'

private slots:
    void systemTime();
    void toggleColor();
    void pushedButton();

    void reset();

    void result();
    void gameover();
};
#endif // WIDGET_H

 

cpp

#include "widget.h"
#include "./ui_widget.h"

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    //, ui(new Ui::Widget)
{
    //ui->setupUi(this);

    /*layer*/
    layout = new QVBoxLayout();

    /*element*/
    sysTimer = new QTimer();
    connect(sysTimer, &QTimer::timeout, this, &Widget::systemTime);

    button = new QPushButton("녹색에 클릭");
    button->setStyleSheet("font:32pt; color:white;background-color:red");
    connect(button, &QPushButton::clicked, this, &Widget::pushedButton);

    /*combine*/
    layout->addWidget(button);
    setLayout(layout);

    /*reset*/
    reset();
}

Widget::~Widget()
{
    delete sysTimer;
    delete button;
    delete layout;
}

void Widget::systemTime(){
    if(state == 1){
        systime++;
        if(systime == random_start){
            toggleColor();
        }
    }
}

void Widget::toggleColor(){
    if(buttonColor == 'r'){
        buttonColor = 'g';
        button->setStyleSheet("font:32pt; color:white; background-color:green");
    }
    else{
        buttonColor = 'r';
        button->setStyleSheet("font:32pt; color:white; background-color:red");
    }
}

void Widget::pushedButton(){
    if(state == 0){
        state = 1;
        sysTimer->start(1);

        qDebug() << "0";
    }
    else if(state == 1){//state = 1
        if(buttonColor == 'g'){
            to = systime;
            sysTimer->stop();
            result();

            qDebug() << "1g";
        }
        else{
            sysTimer->stop();
            gameover();

            qDebug() << "1r";
        }
    }
    else{
        reset();
        qDebug() << "2r";
    }
}

void Widget::reset(){
    sysTimer->stop();
    systime = 0;

    std::srand(time(0));
    random_start = rand()%2000;
    to = 0;

    state = 0;

    buttonColor = 'r';
    button->setText("녹색에 클릭");
    button->setStyleSheet("font:32pt; color:white; background-color:red");
}

void Widget::result(){
    button->setText(QString("%1 ms").arg(to-random_start));
    button->setStyleSheet("font:32pt; color:white; background-color:red");
    toggleColor();
    state = 2;
}

void Widget::gameover(){
    button->setText("녹색에 누르라고!!!");
    state = 2;
}

 

MVVM 패턴

https://velog.io/@kyeun95/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-MVVM-%ED%8C%A8%ED%84%B4%EC%9D%B4%EB%9E%80

 

[디자인 패턴] MVVM 패턴이란?

개념 :MVVM (Model-View-ViewModel) 패턴은 Model View, View, Model의 약자로 프로그램의 비지니스 로직과, 프레젠테이션 로직을 UI로 명확하게 분리하는 패턴입니다.데이터를 다루는 부분. 비즈니스 로직을

velog.io

 

QGui : MFC에서paint를 했었던 것과 매우 유사하다.

 

  • QWidget: 사용자 정의 GUI 요소의 베이스
  • QPainter: 드로잉 기능 제공 (직선, 원, 이미지, 텍스트 등)
  • QImage, QPixmap: 이미지 객체 표현

Qt 파일입출력 : C++에서 제공하는 stream 기반 파일입출력 방식 사용

  • QFile : 파일 열기, 읽기, 쓰기 등 핵심 클래스
  • QTextStream : 텍스트 기반 스트림 입출력
  • QDataStream : 이진 파일 입출력
  • QFileDialog : 사용자로부터 파일을 선택하거나 저장 위치 선택

쓰레드 프로그래밍 :

주의점 : UI 쓰레드와 Work 쓰레드의 분리, critical section의 통제, 비동기 프로그래밍(signal - slot으로 구현됨)

UI 쓰레드는 중지되서는 안된다.(사용자 입장에서 당황스럽다.)

 

1. QThread 상속 방식

개요

QThread 클래스를 상속받아 run() 메서드를 오버라이딩하고, 그 안에서 쓰레드 작업을 수행하는 방식입니다.

예시 코드

 
class MyThread : public QThread {
    Q_OBJECT

protected:
    void run() override {
        // 시간 소모적인 작업 수행
        for (int i = 0; i < 5; ++i) {
            qDebug() << "Thread running: " << i;
            QThread::sleep(1);
        }
        emit workFinished();
    }

signals:
    void workFinished();
};

// 위젯 클래스에서 사용
MyThread* thread = new MyThread();
connect(thread, &MyThread::workFinished, this, &MainWindow::onWorkFinished);
thread->start();

장점

  • 구현이 직관적이고 단순함

단점

  • QThread는 쓰레드 제어용 객체로 설계되었기 때문에, 실제 작업 로직을 QThread에 직접 넣는 것은 권장되지 않음
  • 확장성이 낮고 디버깅이 어려울 수 있음

2. QObject 기반 Worker 사용 방식 (권장 방식)

개요

작업 로직을 QObject 기반 클래스에 구현하고, 이를 QThread에 moveToThread()로 옮기는 방식입니다. Qt의 신호-슬롯 메커니즘을 활용하여 쓰레드 간 안전한 통신을 할 수 있습니다.

예시 코드

Worker 클래스

class Worker : public QObject {
    Q_OBJECT

public slots:
    void doWork() {
        for (int i = 0; i < 5; ++i) {
            qDebug() << "Working in thread: " << QThread::currentThread();
            QThread::sleep(1);
        }
        emit workDone();
    }

signals:
    void workDone();
};

위젯 클래스(MainWindow 등)에서의 사용

Worker* worker = new Worker();
QThread* thread = new QThread();

worker->moveToThread(thread);

// 쓰레드가 시작되면 doWork() 호출
connect(thread, &QThread::started, worker, &Worker::doWork);

// 작업 완료 시 슬롯 호출
connect(worker, &Worker::workDone, this, &MainWindow::onWorkFinished);

// 작업 완료 후 쓰레드 정리
connect(worker, &Worker::workDone, thread, &QThread::quit);
connect(thread, &QThread::finished, worker, &QObject::deleteLater);
connect(thread, &QThread::finished, thread, &QThread::deleteLater);

thread->start();

장점

  • UI 쓰레드와 작업 쓰레드를 명확히 분리
  • 객체 간 신호/슬롯으로 쓰레드 간 통신 → race condition 방지
  • 확장성과 유지보수성 뛰어남

단점

  • 구조가 다소 복잡함 (그러나 일단 패턴을 익히면 재사용 가능)

위젯에서의 주의사항

  • UI 요소 접근 제한: 모든 UI 위젯은 반드시 메인 쓰레드(UI 쓰레드)에서만 접근해야 합니다. 작업 쓰레드에서 UI를 직접 수정하는 경우, 크래시 혹은 undefined behavior 발생 가능.
  • 신호/슬롯 방식 사용: 쓰레드 간 통신은 반드시 signal-slot 메커니즘으로 처리해야 하며, 특히 Qt::QueuedConnection 방식으로 호출되도록 유의해야 합니다 (기본적으로 다른 쓰레드 간 연결은 이 방식 사용됨).

 

반응형