Skip to content

Soonbum/Qt_for_Python

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

34 Commits
 
 

Repository files navigation

Qt for Python

출처: https://doc.qt.io/qtforpython-6/quickstart.html

빠른 시작

요구사항

Qt for Python을 설치하기 전에 다음 소프트웨어를 먼저 설치해야 합니다.

  • Python 3.7 이상
  • venv 또는 virtualenv와 같은 가상 환경을 사용할 것을 권장함, 그리고 당신의 시스템에 pip로 PySide6를 설치하지 않을 수 있습니다.

설치

  • 터미널에서 다음과 같이 실행하여 환경을 만들고 활성화합니다.

    • 환경 만들기: python -m venv env

    • 환경 활성화하기 (Linux와 macOS): source env/bn/activate

    • 환경 활성화하기 (Windows): env\Scripts\activate.bat

  • PySide6 설치하기

이제 pip를 이용하여 Qt for Python 패키지를 설치합니다. 터미널에서 다음 커맨드를 실행하십시오.

  • 최신 버전의 경우: pip install pyside6

  • 특정 버전의 경우: pip install pyside6==6.4.1

  • 설치 확인하기

설치가 잘 되었으면 다음 커맨드를 통해 버전을 확인할 수 있습니다.

import PySide6.QtCore

# PySide6 버전 출력
print(PySide6.__version__)

# PySide6를 컴파일하는 데 사용된 Qt 버전을 출력
print(PySide6.QtCore.__version__)

간단한 Qt Widgets 애플리케이션 만들기

다음은 "Hello World"를 출력하는 간단한 애플리케이션입니다.

import sys
import random
from PySide6 import QtCore, QtWidgets, QtGui

class MyWidget(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()

        self.hello = ["Hallo Welt", "Hei maailma", "Hola Mundo", "Привет мир"]

        self.button = QtWidgets.QPushButton("Click me!")
        self.text = QtWidgets.QLabel("Hello World",
                                     alignment=QtCore.Qt.AlignCenter)

        self.layout = QtWidgets.QVBoxLayout(self)
        self.layout.addWidget(self.text)
        self.layout.addWidget(self.button)

        self.button.clicked.connect(self.magic)

    @QtCore.Slot()
    def magic(self):
        self.text.setText(random.choice(self.hello))

if __name__ == "__main__":
    app = QtWidgets.QApplication([])

    widget = MyWidget()
    widget.resize(800, 600)
    widget.show()

    sys.exit(app.exec())

간단한 Qt Quick 애플리케이션 만들기

이것은 간단한 형태로 만든 QML 코드이지만, 일반적으로 .qml 파일로 나눠서 작성해야 합니다.

import sys
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine

QML = """
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

Window {
    width: 300
    height: 200
    visible: true
    title: "Hello World"

    readonly property list<string> texts: ["Hallo Welt", "Hei maailma",
                                           "Hola Mundo", "Привет мир"]

    function setText() {
        var i = Math.round(Math.random() * 3)
        text.text = texts[i]
    }

    ColumnLayout {
        anchors.fill:  parent

        Text {
            id: text
            text: "Hello World"
            Layout.alignment: Qt.AlignHCenter
        }
        Button {
            text: "Click me"
            Layout.alignment: Qt.AlignHCenter
            onClicked:  setText()
        }
    }
}
"""

if __name__ == "__main__":
    app = QGuiApplication(sys.argv)
    engine = QQmlApplicationEngine()
    engine.loadData(QML.encode('utf-8'))
    if not engine.rootObjects():
        sys.exit(-1)
    exit_code = app.exec()
    del engine
    sys.exit(exit_code)

Qt, QML, Widgets... 무슨 차이가 있죠?

기본적으로 Qt는 C++로 작성되고 설계된 C++ 프레임워크입니다. 대부분의 레퍼런스, 예제, 개념들은 C++ 기반 애플리케이션입니다. 하지만 Qt for Python의 목표는 Python에 Qt 프레임워크를 적용시키는 것이기 때문에 C++에 대해 몰라도 됩니다.

Qt

Qt는 많은 컴포넌트를 갖고 있습니다. 가령 qtbase는 베이스 컴포넌트로서 다음과 같은 많은 모듈을 갖고 있습니다: QtCore, QtGui, QtWidgets, QtNetwork 등. 모듈에는 여러 클래스들이 있는데 당신은 여기서 직접 클래스를 골라서 사용할 수 있습니다. 가령 QtCore의 클래스에서 QFile, QTime, QByteArray 등을 꺼내쓸 수 있습니다.

커맨드 라인 애플리케이션, 파일 처리, 네트워크 연결, 정규 표현식, 텍스트 인코딩 등의 경우에는 굳이 GUI가 없이도 애플리케이션을 만들 수 있습니다.

그 외에는 Widgets이라고 하는 QtWidget 모듈의 클래스를 이용하여 그래픽 애플리케이션을 만들 수 있습니다.

여러 Qt 모듈 중에는 QtDeclarative라는 특별한 기능이 있는데 QML declarative 언어도 있습니다. 이 언어는 CSS와 JSON과 비슷하며 선언으로 UI 애플리케이션을 만들고 설계할 수 있으며 명령형 섹션에 JavaScript를 넣을 수 있고 컴포넌트를 확장하여 C++과 코드를 연결할 수도 있습니다.

Widgets

QtWidgets은 그래픽 애플리케이션에 추가할 수 있는 미리 정의된 위젯을 제공합니다. 가령 버튼, 라벨, 박스, 메뉴 등이 있습니다. 위젯 기반 애플리케이션은 네이티브 애플리케이션처럼 보일 것입니다.

QML

QML은 위젯과 다른 방식의 사용자 인터페이스를 만드는 접근법입니다. 본래 모바일 애플리케이션을 만들기 위한 것이었습니다. Qt Quick 모듈이 있으면 탭, 드래그 앤 드롭, 애니메이션, 상태, 전환, 드로워 메뉴 등 모바일 장치와 상호 작용할 수 있는 액세스를 제공합니다.

QML/Quick 애플리케이션에서 찾을 수 있는 요소들은 특정 동작을 기반으로 하는 다양한 프로퍼티를 가진 다이나믹한 애플리케이션 인프라를 제공하는 데 초점을 맞추고 있습니다.

물론 QML이 모바일 장치에 대한 인터페이스를 제공하는 것이 목적이지만 데스크톱 애플리케이션에도 사용할 수 있습니다.

또 표준 JavaScript로 애플리케이션을 강화할 수 있고 C++을 결합할 수 있으니 매력적인 인프라가 될 수 있습니다.

Python과 C++

Qt for Python의 경우 C++을 알 필요가 없습니다만, 다음과 같이 두 언어를 섞을 가능성도 있습니다.

  1. Qt/C++ 애플리케이션을 갖고 있다면 Qt/Python 애플리케이션으로 다시 만들 수 있습니다. Qt 애플리케이션의 사용자 수준 C++ 코드를 완전히 Python 코드로 대체하는 것이 목표입니다.
  2. C++에서 작성한 커스텀 Qt 위젯의 경우, Python 바인딩을 만들어서 Python에서 해당 위젯을 직접 사용할 수 있습니다.
  3. 가령 성능이 중요한 특정 작업을 담당하는 C++ 기반 라이브러리를 갖고 있다면 바인딩을 만들어서 Python에서 사용할 수 있습니다.
  4. Qt/C++ 애플리케이션의 경우, 메인 QApplication 싱글톤을 Python 인터프리터에 대한 Python 바인딩으로 노출시켜서 Python으로 확장할 수 있습니다.

2,3,4의 경우 바인딩 생성 도구인 Shiboken의 도움을 받아서 Qt for Python을 생성할 수 있습니다.

어떤 IDE가 호환됩니까?

Python이 호환되는 IDE에서 Qt for Python을 사용할 수 있지만 전부 다 Qt Creator와 같은 기능을 제공해 주지는 않습니다.

파일 작성 외에도 애플리케이션 개발에 도움을 받기 위해 추가 단계가 있습니다.

터미널에서

  • .ui 파일에서 Python 파일 생성하기: pyside6-uic -i form.ui -o ui_form.py
  • .qrc 파일에서 Python 파일 생성하기: pyside6-rcc -i resources.qrc -o rc_resources.py
  • .ui 파일을 편집/생성하기 위해 커맨드 pyside6-designer로 Qt Designer 열기

당신이 선호하는 IDE에 외부 애드온/플러그인이 이 커맨드를 실행하기 위한 구성 단계를 포함시킬 수도 있고, Designer와 QtCreator와 같은 도구를 사용할 수도 있습니다.

QtCreator

QtCreator에서 사용할 수 있는 기본 템플릿을 기반으로 새로운 프로젝트를 만들 수 있습니다. 템플릿을 선택한 후에 프로젝트 이름, 인터페이스에 사용할 베이스 클래스 등 세부사항을 지정할 수 있습니다.

Visual Studio Code

비공식 플러그인을 사용하여 코드 편집 외에도 추가 기능을 활성화할 수 있습니다.

PyCharm

PyCharm에서 Qt Designer와 Qt Creator와 같은 외부 도구를 사용할 수 있도록 구성할 수 있습니다. File > Settings > tools > PyCharm External Tools에 가서 다음 정보를 프로젝트에 포함시키세요. 나중에 .ui 파일을 우클릭하고 Qt Designer, pyside6-uic를 선택할 수 있습니다.

바인딩 생성: Shiboken이 무엇입니까?

PySide6를 설치할 때 Shiboken6가 의존성으로 설치되는 것을 보실 수 있습니다.

설치된 이 패키지를 Shiboken 모듈이라고도 합니다. 그리고 이것은 PySide가 제대로 작동하기 위한 유틸리티도 가지고 있습니다. 자세한 정보는 여기를 참조하시기 바랍니다.

PySide를 설치할 때 설치되지 않는 제3의 패키지가 있는데 이것은 필수가 아니며 Shiboken Generator라고 합니다.

"Shiboken"을 사용하라거나 "바인딩 생성"과 관련된 것들의 대부분은 PySide 패키지의 의존성으로 설치되는 Shiboken 모듈이 아닌 Shiboken Generator를 가리키는 것입니다.

SHiboken Generator가 필요한가요?

만약 Python에서 작동하는 Qt 애플리케이션을 만드는 것이 목적이라면 굳이 Shiboken generator를 설치할 필요가 없습니다. 그러나 Qt/C++ 애플리케이션과 Python을 바인딩하고 싶다면 그것이 필요할 것입니다.

Shiboken과 관련된 정보는 여기서 찾을 수 있습니다.

파일 타입

Qt for Python 애플리케이션을 개발하면서 만나게 되는 ui, grc, qml, pyproject 등 여러 가지 파일 타입이 있습니다. 다음은 이것에 대한 간단한 설명입니다.

Python 파일 .py

Qt for Python 프로젝트를 개발하면서 주로 다루게 되는 Python 파일입니다.

.ui, .grc, .qml 파일 없이 Python 파일만으로 애플리케이션을 작성하는 것이 가능합니다. 하지만 다른 파일 포맷을 사용하면 개발 과정이 용이해지거나 애플리케이션에 새로운 기능을 넣을 수 있습니다.

class MyWidget(QWidget):
    def __init__(self):
        QWidget.__init__(self)

        self.hello = ["Hallo Welt", "你好,世界", "Hei maailma",
            "Hola Mundo", "Привет мир"]

        self.button = QPushButton("Click me!")
        self.text = QLabel("Hello World")
        self.text.setAlignment(Qt.AlignCenter)
        # ...

사용자 인터페이스 정의 파일 .ui

Qt Designer를 사용할 때 WYSIWYG 폼 편집기를 이용하여 Qt Widgets으로 사용자 인터페이스를 만들 수 있습니다. 이 때 인터페이스는 XML을 사용하는 위젯 트리로 표현됩니다. 다음은 .ui 파일의 일부 예시입니다.

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>400</width>
    <height>300</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <widget class="QWidget" name="centralWidget">
...

pyside6-uic 도구는 이 .ui 파일에서 Python 코드를 생성하므로 배포된 애플리케이션에 .ui 파일을 포함시킬 필요가 없습니다.

리소스 컬렉션 파일 .qrc

애플리케이션과 함께 사용되는 바이너리 파일 목록입니다. XML-기반 파일이며 구조는 다음과 같습니다.

<!DOCTYPE RCC><RCC version="1.0">
<qresource>
    <file>images/quit.png</file>
    <file>font/myfont.ttf</file>
</qresource>
</RCC>

pyside6-rcc 도구는 .qrc 파일에서 Python 코드를 생성하므로 배포된 애플리케이션에 목록에 있는 파일들을 포함시킬 필요가 없습니다.

Qt 모델링 언어 파일 .qml

그래픽 QML 애플리케이션은 Qt Widgets 애플리케이션과 관련이 없으며 보통 QML 프로젝트에서 Python 파일이 QML 파일을 로드합니다. 선택적으로 Python에서 정의한 요소가 QML에 노출되는 경우가 있습니다.

손수 .qml 파일을 작성할 수도 있고 Qt Creator에 내장된 QML Designer와 같은 도구를 사용할 수도 있습니다. 또, Qt Design Studio와 같은 상업용 도구를 사용할 수도 있습니다.

다음은 .qml 파일의 예제입니다. 이 코드는 밝은 회색 직사각형을 표시하고 그 위에 "Hello world!" 메시지를 보여줍니다.

import QtQuick 2.0

Rectangle {
    id: page
    width: 320;
    height: 480
    color: "lightgray"

    Text {
        id: helloText
        text: "Hello world!"
        y: 30
        anchors.horizontalCenter: page.horizontalCenter
        font.pointSize: 24;
        font.bold: true
    }
}

Qt Creator Python 프로젝트 파일 .pyproject

Qt Creator에서는 Python 기반 프로젝트를 로드하고 처리하기 위한 특별한 파일이 필요합니다.

Qt Creator 예전 버전에서는 .pyqtc 확장자를 가진 간단한 포맷을 제공했는데 여기는 다음과 같이 라인마다 파일 이름이 있었습니다:

library/server.py
library/client.py
logger.py
...

이 포맷에는 제한사항이 있고 추가되는 여러 가지 옵션을 지원하지 않으므로 .pyproject 파일이 생겨나게 되었습니다. 이 파일은 JSON-기반 파일이며 더 많은 옵션이 추가될 수 있습니다. 다음은 예제 파일입니다.

{
    "files": ["library/server.py", "library/client.py", "logger.py", ...]
}

애플리케이션을 다른 시스템/플랫폼에 배포하기

애플리케이션을 개발한 후에 다른 사용자들에게 배포하고 싶을 수 있습니다. 당신이 Python 패키지에 대한 경험이 없다고 가정하고 이러한 질문을 드리겠습니다: 어떻게 Python 실행파일을 만들 수 있죠?

만약 당신이 컴파일 프로그래밍 언어 개발자라면 배포라는 게 너무 쉬운 일이겠지만 Python의 경우 조금 어렵습니다.

Python 애플리케이션의 경우 배포 과정을 가리켜 "freezing(동결)"이라고 합니다. 즉, 다른 사용자에게 가상 환경 컨텐츠를 배포하는 것입니다.

재현 가능한 배포

일반적인 접근 방식은 단지 requirements.txt 파일을 제공하는 것입니다. 이것은 의존성을 기술한 것입니다. 사용자는 애플리케이션을 실행하기 위해 필요한 것들을 설치해야 합니다.

예를 들어, main.py에서 사용하는 2개의 의존성(module_a, module_b)을 가진 프로젝트를 가지고 있다고 가정해 봅시다. 구조는 다음과 같습니다.

# Content of the main.py file
from module_a import something
import module_b

# ...

이 애플리케이션에 대한 requirements.txt 파일의 내용은 다음과 같습니다.

module_a
module_b

나중에 사용자가 main.py를 실행하고 싶을 때 새로운 가상 환경에서 pip install -r requirements.txt를 이용하여 의존성을 설치해야 합니다.

애플리케이션 동결하기

이 방법은 사용자가 애플리케이션을 배포하는 가장 일반적인 접근 방식입니다. 그리고 이렇게 하면 최종 사용자도 코드를 이용할 수 있습니다만 코드를 가져오는 것은 어려워집니다.

배포 섹션에서 가장 잘 알려진 도구를 기반으로 일련의 튜토리얼을 통해 Python 사용자가 애플리케이션을 동결하고 배포할 수 있게 해줍니다.

Python 컴파일하기

비록 Python이 네이티브에서 컴파일하는 것을 지원하지는 않지만 이것을 달성할 수 있도록 해주는 보완 도구가 있습니다. 자세한 것은 Nuitka 프로젝트를 보십시오.

왜 Qt for Python인가?

이 질문에 대답하려면 한 발 물러서서 언어에 대한 이야기를 좀 해보아야 합니다.

Python은 Qt와 비슷한 기간 동안 사용되어 왔으며, 성장하고 다방면에서 사용되도록 변화되고, 사용 받고, 여러 프로그래밍 영역에서 필요로 하게 되었습니다.

현재(2021), Python을 논하지 않고는 머신 러닝과 인공지능을 보기 힘들어지게 되었습니다. 이와 마찬가지로 데이터 사이언스/분석/공학 분야에서 대부분 Python과 연관되어 있음을 알 수 있습니다.

지난 해 동안 StackOverflow 설문과 같이 Python 언어의 진화 및 선호도를 보여주는 공개 설문 조사를 통해 이 진술을 검증할 수 있습니다.

2019 2020 2021
가장 사랑 받는 언어 2위 3위 6위
가장 원하는 언어 1위 1위 1위

TIOBE 순위에서도 볼 수 있습니다.

이러한 자료들이 언어를 판단하기에는 충분하지 않을 수 있지만, 전세계에서 개발자 간의 추세를 확실히 알 수 있습니다.

Qt 장벽 낮추기

베테랑 C++ 개발자는 Qt 애플리케이션을 만드는 것이 어렵지 않을 것이며, Qt로 작성된 다른 코드 베이스를 이해하는 데에도 도움이 됩니다. 게다가 많은 팀들이 여러 분야에 걸쳐 있고 다른 팀/회사 개발자가 C++에 능숙하지 않을 수 있습니다.

Python은 사람들을 프로그래밍 세계로 끌어드리고 있으며, 이와 같은 이유로 서로 다른 배경을 가진 사람들이 코드를 작성할 수 있다는 것은 흔한 일입니다. 이는 서로 다른 팀들이 "같은 언어"로 말할 수 있음을 의미합니다.

Python으로 Qt 애플리케이션을 만드는 것은 많은 코드 라인을 필요로 하지 않으며 실행하기 위한 많은 구성을 필요로 하지 않습니다. 다음의 불공정한 예제를 통해 간단한 hello world 애플리케이션 코드를 봅시다.

  • C++ 헤더
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QPushButton>

class MainWindow : public QMainWindow
{
    Q_OBJECT
    public:
        MainWindow(QWidget *parent = nullptr);
    private slots:
        void handleButton();
    private:
        QPushButton *m_button;
};

#endif // MAINWINDOW_H
  • C++ 구현
#include "mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
   : QMainWindow(parent)
{
    m_button = new QPushButton("My Button", this);
    connect(m_button, SIGNAL(clicked()), this,
            SLOT(handleButton()));
}

void MainWindow::handleButton()
{
    m_button->setText("Ready");
}
  • C++ 메인
#include <QApplication>
#include "mainwindow.h"

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    MainWindow mainWindow;
    mainWindow.show();
    return app.exec(d);
}
  • Python
import sys
from pyside6.QtWidgets import (QApplication, QMainWindow,
                               QPushButton)

class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        QMainWindow.__init__(self, parent)
        self.button = QPushButton("My Button", self)
        self.button.clicked.connect(self.handleButton)

    def handleButton(self):
        self.button.setText("Ready")

if __name__ == "__main__":
    app = QApplication([])
    mainWindow = MainWindow()
    mainWindow.show()
    sys.exit(app.exec())

대부분의 판에 박힌 코드는 훌륭한 IDE가 제공한다고 말하는 것이 맞습니다만, 외부 도구를 능숙하게 사용하려면 약간의 연습이 필요합니다.

단결이 힘을 만든다

우리의 목표는 더 많은 사용자가 Qt 세계에 들어오게 하는 것입니다. 하지만 이것이 C++ 개발자가 잊혀지게 된다는 것을 의미하지는 않습니다.

바인딩과 함께 Qt for Python이 바인딩 제너레이터 Shiboken을 제공합니다. 비디오 섹션에서 이 기능에 대해 많이 보여 드립니다.

두 언어 간의 바인딩을 생성하는 것은 새로운 것이 아닙니다만, 프로젝트에서 외부 모듈/라이브러리를 사용할 때 최대한 호환되도록 하기 위해 항상 수반되는 사소한 작업이 있습니다.

Shiboken의 주요 사용 사례는 Qt/C++ 프로젝트의 기능을 확장하여 스크립트화 할 수 있도록 하는 것입니다.

애플리케이션이 스크립트화 할 수 있는 것은 무엇을 의미합니까?

  • 인터프리트되는 언어가 Qt/C++ 애플리케이션과 직접 상호작용 할 수 있게 함
  • Python에서 애플리케이션의 컴포넌트/요소를 수정 및 생성할 수 있는 선택권을 제공함
  • 애플리케이션에 대한 플러그인/애드온 시스템을 생성할 수 있는 가능성
  • 외부 Python 기능으로 프로세스를 보완함

실습 예제에 대해서는 Shiboken Webinar를 확인하십시오.

Shiboken은 Qt-종속 바인딩 생성에 탁월합니다. 이는 Qt/C++ 프로젝트가 Python에 쉽게 노출될 수 있음을 의미합니다. 그리고 Shiboken은 이벤트 토크 및 블로그 포스트를 통해 볼 수 있듯이 (Qt 없이) C++ 프로젝트에 대한 지원을 입증했습니다.

잘 알려진 솔루션 프로젝트에 Python 지원이 추가되는 것은 이 산업 분야의 여러 분야의 장치에서 계속 볼 수 있습니다. 이것이 Qt for Python을 제품을 날마다 개선하기 위해 일하는 이유입니다.

Qt와 Python 모두 이러한 상호 작용으로 이익을 얻게 되리라고 믿습니다.

시작하기

일반 요구사항

Qt for Python을 빌드하기 전에 다음 필수 구성요소를 설치해야 합니다. Linux의 경우 운영체제 패키지 매니저를 통해 설치할 수 있습니다. macOS의 경우 brew를 통해 설치할 수 있습니다. Windows의 경우 각 웹사이트에서 인스톨러를 다운로드할 수 있습니다.

플랫폼 별 가이드

자세한 내용은 생략한다.

패키지 세부사항

설치 커맨드 한 줄이면 Qt 프레임워크와 같은 큰 프로젝트를 사용할 수 있습니다:

pip install pyside6

매우 유용하지만 초심자들에게는 당혹스러울 수 있습니다.

IDE 외에도 Qt 애플리케이션을 개발하기 위해 다른 어떤 것도 설치할 필요가 없습니다. 왜냐하면 이 한 줄로 UI 설계, QML 타입 사용, 파일 자동 생성, 애플리케이션 번역 등 많은 도구들이 설치되기 때문입니다.

패키지 의존성

image

6.3.0부터 pyside6 패키지는 거의 비어 있으며 모든 모듈을 제대로 사용하기 위해 필요한 다른 패키지에 대한 참조만을 포함하고 있습니다. 이 패키지는 다음과 같습니다.

pip list를 실행해서 Python (가상) 환경에 설치된 패키지를 확인할 수 있습니다.

pyside6-essentialspyside6-addons는 Qt 바이너리(.so, .dll 또는 .dylib)를 가지고 있는데 Python에서 Qt 모듈을 사용할 수 있도록 해주는 Python 랩퍼가 이것들을 사용합니다. 예를 들어 Linux 플랫폼의 경우 QtCore 모듈에서 다음을 찾을 수 있습니다.

  • PySide6/QtCore.abi3.so, 그리고
  • PySide6/Qt/lib/libQt6Core.so.6

당신의 (가상) 환경의 site-packages 디렉토리 안에서 찾을 수 있습니다. 1번째는 importable 모듈이고 이것은 원래 QtCore 라이브러리인 2번째 파일에 의존하고 있습니다.

포함된 도구

패키지 안에도 uic, rcc 등 Qt 애플리케이션 개발 워크플로우에서 중요한 Qt 도구들이 포함되어 있습니다.

모든 도구는 반드시 PySide 랩퍼를 통해 사용되어야 하고 직접 사용해서는 안 됩니다. 예를 들어, 설치된 site-packages/ 디렉토리를 탐색하려면 (Windows의 경우) uic.exe를 찾고 싶으면 그것을 클릭하지 말고 pyside6-uic.exe를 대신 사용해야 합니다. 이렇게 하는 이유는 설치된 Python 패키지로 적절하게 작업하려면 PATH, 플러그인 등의 적절한 설정을 하기 위해서입니다.

다음은 주제 별로 그룹화된 버전 6.3.0부터 Qt for Python에 포함된 도구들입니다.

프로젝트 개발
  • pyside6-project: .pyproject 파일에 있는 Qt Designer 폼(.ui 파일), 리소스 파일(.qrc), QML 타입 파일(.qmltype)을 빌드함.
위젯 개발
  • pyside6-designer: 위젯 UI를 설계하기 위한 드래그-앤-드롭 도구. (.ui 파일 생성)
  • pyside6-uic: .ui 폼 파일로부터 Python 코드 생성함.
  • pyside6-rcc: .qrc 리소스 파일로부터 직렬화된 데이터 생성함. 이 파일들은 다른 비-위젯 프로젝트에서 사용할 수 있음을 명심하십시오.
QML 개발
  • pyside6-qmllint: QML 파일의 구문 유효성을 검증함.
  • pyside6-qmltyperegistrar: 메타 타입을 읽고 관련 매크로와 함께 표시된 모든 타입을 등록하기 위해 필요한 코드를 포함하는 파일을 생성함.
  • pyside6-qmlimportscanner: 프로젝트/QML 파일로부터 가져온 QML 모듈을 식별하고 JSON 배열로 결과를 덤프함.
  • pyside6-qmlcachegen: 바이너리로 번들링하기 위해 컴파일 시간에 QML을 바이트코드로 컴파일함.
  • pyside6-qmlsc: pyside6-qmlcachegen을 대체함. 이 도구는 QML을 바이트코드로 컴파일할뿐만 아니라 철저한 분석을 위해 C++ 코드도 생성함. 이것은 상업용 전용 도구임.
번역
  • pyside6-linguist: 애플리케이션에서 텍스트를 번역하기 위함.
  • pyside6-lrelease: 애플리케이션을 위한 런타임 번역 파일을 생성함.
  • pyside6-lupdate: 소스 파일과 번역 파일을 동기화함.
Qt 도움말
  • pyside6-assistant: Qt Help 파일 포맷으로 된 온라인 문서를 보기 위함. 이 포맷에 대한 자세한 것은 QtHelp Framework 페이지를 보십시오.
PySide 유틸리티
  • pyside6-genpyi: Qt 모듈을 위한 Python 스텁(.pyi 파일)을 생성함
  • pyside6-metaobjectdump: qmltyperegistrar의 입력으로 사용될 JSON 형식의 메타타입 정보를 출력하기 위한 도구.
배포
  • pyside6-deploy: PySide6 애플리케이션을 데스크톱 플랫폼(Linux, Windows, macOS)에 배포하기 위함.
  • pyside6-android-deploy: PySide6 애플리케이션을 Android 플랫폼(aarch64, armv7a, i686, x86_64)에 배포하기 위함.

모듈 API

기본 모듈

위젯 기반 UI를 만드는 데 도움을 주는 메인 모듈은 다음과 같습니다.

  • QtCore: 시그널, 슬롯, 프로퍼티, 아이템 모델의 베이스 클래스, 직렬화 등 코어 비-GUI 기능을 제공함.

  • QtGui: QtCore의 GUI 기능을 확장함: 이벤트, 윈도우, 스크린, OpenGL, 래스터-기반 2D 그리기, 이미지.

  • QtWidgets: 애플리케이션에 위젯을 사용할 수 있도록 함. UI에 그래픽 요소를 포함시킴.

QML과 Qt Quick

Python에서 QML 언어로 상호작용하기 위해 다음 모듈을 사용하십시오.

  • QtQml: 모듈과 상호작용하기 위한 베이스 Python API.

  • QtQuick: Qt 애플리케이션에 Qt Quick을 내장시키기 위한 클래스를 제공함.

  • QtQuickWidgets: 위젯-기반 애플리케이션에 Qt Quick을 내장시키기 위한 QQuickWidget 클래스를 제공함.

Qt for Python이 지원하는 Qt 모듈

  • QtBluetooth: Bluetooth API는 Bluetooth 가능한 장비 간의 연결을 제공함.

  • QtCharts: 차트 컴포넌트를 쉽게 사용할 수 있는 집합을 제공함.

  • QtConcurrent: 저수준 쓰레드 프리미티브(뮤텍스, 읽기-쓰기 락, 대기 조건, 세마포어)를 사용하지 않고도 멀티-쓰레드 프로그램을 작성할 수 있는 고수준 API를 제공함.

  • QtCore: 코어 비-GUI 기능을 제공함.

  • QtDataVisualization: 데이터를 3D 막대, 산포도, 면 그래프로 시각화 할 수 있는 방법을 제공함.

  • QtDBus: D-Bus는 기존의 경쟁 IPC 솔루션을 하나의 통합 프로토콜로 대체하기 위해 Linux용으로 개발된 프로세스간 통신(IPC) 및 원격 프로시저 호출(RPC) 메커니즘입니다.

  • QtDesigner: Qt Designer를 확장하기 위한 클래스를 제공함.

  • QtGui: QtCore의 GUI 기능을 확장함.

  • QtHelp: 애플리케이션의 온라인 문서를 통합하기 위한 클래스를 제공함.

  • Qt Multimedia: 멀티미디어 특화 사용자 사례에 대한 API를 제공함.

  • Qt Multimedia Widgets: 위젯 기반 멀티미디어 API를 제공함.

  • QtNetwork: TCP/IP 클라이언트 및 서버를 작성할 수 있도록 하는 클래스를 제공함.

  • Qt Network Authorization: Qt 애플리케이션이 사용자의 비밀번호를 노출하지 않고 온라인 계정 및 HTTP 서비스에 제한적으로 접근할 수 있도록 하는 API 집합을 제공함.

  • QtNfc: NFC API는 NFC 가능한 장치 간의 연결을 제공함.

  • QtOpenGL: Qt 애플리케이션에서 OpenGL을 쉽게 사용할 수 있도록 하는 클래스를 제공함.

  • QtOpenGL Widgets: 위젯 트리의 특정 부분에 대해 OpenGL 렌더링을 활성화하는 OpenGLWidget 클래스를 제공함.

  • Qt Positioning: 위치, 위성 정보, 지역 모니터링 클래스에 대한 접근을 제공함.

  • Qt PDF: PDF 문서를 렌더링하기 위한 클래스와 함수.

  • Qt PDF Widgets: PDF 뷰어 위젯.

  • QtPrintSupport: 인쇄를 위한 광범위한 크로스-플랫폼 지원을 제공함.

  • QtQml: Qt QML을 위한 Python API.

  • QtQuick: Qt 애플리케이션에 Qt Quick을 내장시키기 위한 클래스를 제공함.

  • QtQuickControls2: C++의 컨트롤을 설정하기 위한 클래스를 제공함.

  • QtQuickWidgets: 위젯 기반 애플리케이션에 Qt Quick을 내장시키기 위한 QQuickWidget 클래스를 제공함.

  • QtRemoteObjects: Qt를 위해 개발된 프로세스간 통신(IPC) 모듈. 이 모듈은 Qt의 기존 기능을 확장하여 프로세스 또는 컴퓨터 간의 정보 교환을 쉽게 할 수 있게 해줍니다.

  • Qt Scxml: SCXML 파일로부터 상태 머신을 생성 및 사용할 수 있게 해주는 클래스를 제공함.

  • Qt Sensors: 센서 하드웨어에 대한 접근을 제공함.

  • Qt Serial Bus: 시리얼 산업 버스 인터페이스에 대한 접근을 제공함. 현재 이 모듈은 CAN 버스와 Modbus 프로토콜을 지원함.

  • Qt Serial Port: 하드웨어 및 가상 시리얼 포트와 상호작용하기 위한 클래스를 제공함.

  • Qt Spatial Audio: 음원 및 서라운드를 3D 공간에 모델링하기 위한 API를 제공함.

  • QtSql: Qt 애플리케이션에 원활한 데이터베이스 통합을 제공함.

  • QtStateMachine: 상태 그래프를 생성 및 실행하기 위한 클래스를 제공함.

  • QtSvg: SVG 파일의 내용을 표시하기 위한 클래스를 제공함.

  • QtSvgWidgets: SVG 파일의 내용을 표시하는 데 사용하는 위젯을 제공함.

  • QtTest: Qt 애플리케이션 및 라이브러리를 단위 테스트하기 위한 클래스를 제공함.

  • QtUiTools: Qt Designer로 생성된 폼을 처리하기 위한 클래스를 제공함.

  • Qt WebChannel: Qt 애플리케이션을 HTML/JavaScript 클라이언트와 원활하게 통합하기 위해 HTML 클라이언트로부터 QObject 또는 QML 객체에 대한 접근을 제공함.

  • QtWebEngine Core C++ Classes: QtWebEngine과 QtWebEngineWidgets이 공유하는 공용 API를 제공함.

  • QtWebEngine Widgets C++ Classes: QWidget 기반 애플리케이션에서 웹 컨텐츠를 렌더링하기 위한 C++ 클래스를 제공함.

  • QtWebEngine QML Types: QML 애플리케이션에서 웹 컨텐츠를 렌더링하기 위한 QML 타입을 제공함.

  • Qt WebSockets: RFC 6455를 준수하는 WebSocket 통신을 제공함.

  • QtWidgets: Qt GUI의 C++ 위젯 기능을 확장함.

  • QtXml: DOM의 C++ 구현을 제공함.

  • Qt3DAnimation: 3D 객체를 애니메이션화 하는 데 필요한 기본 요소를 제공함.

  • Qt3D Core: 근실시간 시뮬레이션 시스템을 지원하기 위한 기능을 포함하고 있음.

  • Qt3D Extras: Qt 3D를 시작하는 데 도움을 주는 미리 빌드된 요소 집합을 제공함.

  • Qt3D Input: Qt 3D를 사용하는 애플리케이션에서 사용자 입력을 처리하기 위한 클래스를 제공함.

  • Qt3D Logic: 프레임을 Qt 3D 백엔드와 동기화할 수 있게 해줌.

  • Qt3D Render: Qt 3D를 사용하여 2D 및 3D 렌더링을 지원하는 기능이 포함되어 있음.

  • Qt CoAP: RFC 7252에서 정의한 CoAP의 클라이언트 사이드를 구현함.

  • Qt OPC UA: 산업 애플리케이션에서 데이터 모델링 및 데이터 교환을 위한 프로토콜.

  • Qt MQTT: MQTT 프로토콜 사양의 구현을 제공함.

튜토리얼

처음 QtWidgets 애플리케이션

import sys
from PySide6.QtWidgets import QApplication, QLabel

app = QApplication(sys.argv)      # app = QApplication([]) --> 커맨드 라인에서 인수를 안 받을 경우 이렇게 해도 됨
label = QLabel("Hello World!")    # label = QLabel("<font color=red size=40>Hello World!</font>")  --> 이런 식으로 HTML 코드를 넣을 수도 있음
label.show()
app.exec()

image

간단한 Button 사용하기

여기서는 시그널과 슬롯 개념이 나옵니다. 시그널은 '클릭'과 같은 이벤트를 의미합니다. 슬롯 함수는 시그널(이벤트)와 연결된 함수로 시그널이 발생하면 호출되는 함수를 의미합니다. @Slot() 데코레이터를 위에 붙여 주도록 합시다.

#!/usr/bin/python

import sys
from PySide6.QtWidgets import QApplication, QPushButton
from PySide6.QtCore import Slot

@Slot()
def say_hello():
  print("Button clicked, Hello!")    # 콘솔 창에 메시지 출력

app = QApplication(sys.argv)
button = QPushButton("Click me")
button.clicked.connect(say_hello)    # clicked 시그널과 say_hello 함수를 연결함
button.show()
app.exec()

image

시그널과 슬롯

시그널과 슬롯은 QObject 간의 통신 메커니즘이며 Qt의 핵심 기능입니다. 시그널과 슬롯은 마치 스위치(시그널)와 전등(슬롯)과 같다고 볼 수 있습니다.

QWidget과 같이 QObject에서 파생된 모든 클래스는 시그널과 슬롯을 갖고 있습니다. 객체의 상태가 바뀌면 다른 객체가 알아들을 수 있는 시그널이 방출됩니다. 어떤 객체가 시그널을 방출했는지는 알 수 없기 때문에 진정한 정보 캡슐화가 이루어진다고 볼 수 있습니다. 그리고 객체는 하나의 소프트웨어 컴포넌트 역할을 할 수 있습니다.

시그널을 수신하는 슬롯은 일반 멤버 함수이기도 합니다. 객체는 누가 시그널을 받는지 알 수 없고, 슬롯 함수는 시그널이 어디에 연결되어 있는지 알 수 없습니다.

슬롯 하나에 여러 시그널이 연결될 수도 있고, 시그널 하나에 여러 슬롯이 연결될 수도 있습니다. 심지어 시그널과 다른 시그널이 연결될 수도 있습니다. (이렇게 하면 1번째 시그널이 방출되면 2번째 시그널도 즉시 방출됨)

Qt의 위젯은 미리 정의된 시그널, 슬롯을 많이 갖고 있습니다. 예를 들어 (버튼의 베이스 클래스인) QAbstractButtonclicked() 시그널을 갖고 있고, (단일 라인 입력 필드인) QLineEditclear()라는 슬롯을 갖고 있습니다. 그래서 텍스트를 지우기 위해 버튼이 딸린 텍스트 입력 필드를 구현하고자 할 때, QLineEdit 오른쪽에 QToolButton을 배치하고 clicked() 시그널과 clear() 슬롯을 연결하면 됩니다. 시그널의 connect() 메서드를 사용하면 됩니다.

button = QToolButton()
line_edit = QLineEdit()
button.clicked.connect(line_edit.clear)

connect() 메서드는 QMetaObject.Connection 객체를 리턴합니다. 이 객체는 연결을 끊기 위해 disconnect() 메서드와 함께 사용될 수 있습니다.

시그널은 일반 함수에도 연결될 수 있습니다:

import sys
from PySide6.QtWidgets import QApplication, QPushButton

def function():
    print("The 'function' has been called!")

app = QApplication()
button = QPushButton("Call function")
button.clicked.connect(function)    # 시그널이 일반 함수와 연결됨
button.show()
sys.exit(app.exec())

코드 형태로 연결을 할 수도 있고, Qt Designer의 Signal-Slot Editor에서 설계된 위젯 폼에서도 연결할 수 있습니다.

시그널 클래스

Python에서 클래스를 작성할 때, 시그널은 클래스 QtCore.Signal()의 클래스 레벨 변수로 선언됩니다. QWidget 기반 버튼은 다음과 같이 clicked() 시그널을 방출합니다.

from PySide6.QtCore import Qt, Signal
from PySide6.QtWidgets import QWidget

class Button(QWidget):

    clicked = Signal(Qt.MouseButton)     # clicked 시그널 선언

    ...

    def mousePressEvent(self, event):    # 마우스 버튼을 누르는 이벤트가 발생하면
        self.clicked.emit(event.button())  # 클릭 시그널 방출

Signal의 생성자는 Python 타입 또는 C 타입의 튜플 또는 리스트를 인수로 받습니다:

signal1 = Signal(int)                 # Python 타입
signal2 = Signal(QUrl)                # Qt 타입
signal3 = Signal(int, str, int)       # 다수의 타입
signal4 = Signal((float,), (QDate,))  # 선택적인 타입

그 외에도 시그널 이름을 정의하는 네임드 인수 name도 받을 수 있습니다. 만약 아무 것도 전달되지 않으면, 새로운 시그널은 할당될 변수와 같은 이름을 갖게 될 것입니다.

# TODO
signal5 = Signal(int, name='rangeChanged')
# ...
rangeChanged.emit(...)

Signal의 또 다른 유용한 옵션은 인수 이름이며, QML 애플리케이션에서 방출된 값을 이름으로 참조하는 데 유용합니다:

sumResult = Signal(int, arguments=['sum'])
Connections {
    target: ...
    function onSumResult(sum) {
        // do something with 'sum'
    }

슬롯 클래스

QObject 파생 클래스에 있는 슬롯은 데코레이터 @QtCore.Slot()로 표시해야 합니다. 또, 시그네처를 정의하려면 QtCore.Signal() 클래스와 마찬가지로 타입을 넘겨주기만 하면 됩니다.

@Slot(str)
def slot_function(self, s):
    ...

Slot()nameresult 키워드를 받아들입니다. result 키워드는 리턴될 타입을 정의하며 C 또는 Python 타입이 될 수 있습니다. name 키워드는 Signal()과 같은 방식으로 행동합니다. 만약 전달되는 이름이 없으면 새로운 슬롯은 데코레이트되는 함수와 같은 이름을 갖게 됩니다.

시그널과 슬롯을 다른 타입으로 오버로드하기

다른 파라미터 타입 리스트를 가진 동일한 이름의 시그널과 슬롯을 사용할 수 있습니다. 이것은 Qt 5의 레거시이며 새로운 코드에 권장하지 않습니다. Qt 6에서는 시그널이 다른 타입에 대해 고유한 이름을 갖습니다.

다음 예제에서는 시그널과 슬롯에 2개의 핸들러를 사용하여 서로 다른 기능을 보여 줍니다.

import sys
from PySide6.QtWidgets import QApplication, QPushButton
from PySide6.QtCore import QObject, Signal, Slot

class Communicate(QObject):
    # 즉시 2개의 새 시그널을 만듭니다: 하나는 int 타입, 다른 하나는 string 타입을 처리함
    speak = Signal((int,), (str,))

    def __init__(self, parent=None):
        super().__init__(parent)

        self.speak[int].connect(self.say_something)
        self.speak[str].connect(self.say_something)

    # C 'int' 또는 'str'을 수신하고 'say_something'이라는 이름을 갖는 새로운 슬롯을 정의함
    @Slot(int)
    @Slot(str)
    def say_something(self, arg):
        if isinstance(arg, int):
            print("This is a number:", arg)
        elif isinstance(arg, str):
            print("This is a string:", arg)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    someone = Communicate()

    # 서로 다른 인수를 갖는 'speak' 시그널을 방출함
    someone.speak.emit(10)
    someone.speak[str].emit("Hello everybody!")

메서드 시그네처 문자열로 시그널과 슬롯 지정하기

시그널과 슬롯은 SIGNAL()SLOT() 함수를 통해 전달되는 C++ 메서드 시그네처 문자열로 지정할 수도 있습니다:

from PySide6.QtCore import SIGNAL, SLOT

button.connect(SIGNAL("clicked(Qt::MouseButton)"),
              action_handler, SLOT("action1(Qt::MouseButton)"))

이것은 시그널을 연결하는 데 권장하지 않으며, 주로 QWizardPage::registerField()와 같은 메서드로 시그널을 지정하는 데 사용합니다:

wizard.registerField("text", line_edit, "text",
                     SIGNAL("textChanged(QString)"))

다이얼로그 애플리케이션 만들기

이 튜토리얼은 기본 위젯 몇 가지를 이용해 간단한 다이얼로그를 만드는 방법을 보여줍니다. 사용자가 QLineEdit에 이름을 입력하고 QPushButton을 클릭하면 다이얼로그가 환영 인사를 합니다.

import sys
from PySide6.QtWidgets import (QLineEdit, QPushButton, QApplication, QVBoxLayout, QDialog)

# QDialog에서 파생된 Form 클래스 선언
class Form(QDialog):
    # 생성자
    def __init__(self, parent=None):
        super(Form, self).__init__(parent)  # 부모 객체의 생성자 호출
        self.setWindowTitle("My Form")
        # 위젯 생성
        self.edit = QLineEdit("Write my name here")
        self.button = QPushButton("Show Greetings")
        # 레이아웃을 생성하고 위젯을 추가함
        layout = QVBoxLayout()
        layout.addWidget(self.edit)
        layout.addWidget(self.button)
        # 다이얼로그 레이아웃 설정
        self.setLayout(layout)
        # greetings 슬롯에 버튼 시그널 추가함
        self.button.clicked.connect(self.greetings)

    # 사용자 환영 인사
    def greetings(self):
        print(f"Hello {self.edit.text()}")

if __name__ == '__main__':
    # Qt 애플리케이션 생성함
    app = QApplication(sys.argv)
    # 폼을 생성하고 보여줌
    form = Form()
    form.show()
    # 메인 Qt 루프 실행하기
    sys.exit(app.exec())

image

Table 위젯을 사용하여 데이터 표시하기

표에 정렬된 데이터를 표시하고 싶으면 QTableWidget을 사용하십시오.

QTableWidget을 사용하는 것만이 표에 정보를 표시하는 유일한 경로는 아닙니다. QTableView를 이용하여 데이터 모델을 만들고 표시할 수도 있지만 이 튜토리얼의 범위를 벗어납니다.

import sys
from PySide6.QtGui import QColor
from PySide6.QtWidgets import (QApplication, QTableWidget, QTableWidgetItem)

# 컬러의 이름과 HEX 코드 리스트를 포함하는 데이터 모델
colors = [("Red", "#FF0000"),
          ("Green", "#00FF00"),
          ("Blue", "#0000FF"),
          ("Black", "#000000"),
          ("White", "#FFFFFF"),
          ("Electric Green", "#41CD52"),
          ("Dark Blue", "#222840"),
          ("Yellow", "#F9E56d")]

# HEX 코드를 RGB로 변환하는 함수
def get_rgb_from_hex(code):
    code_hex = code.replace("#", "")
    rgb = tuple(int(code_hex[i:i+2], 16) for i in (0, 2, 4))
    return QColor.fromRgb(rgb[0], rgb[1], rgb[2])

app = QApplication()

table = QTableWidget()
table.setRowCount(len(colors))            # 표의 행 개수 = color 변수의 항목 개수
table.setColumnCount(len(colors[0]) + 1)  # 표의 열 개수 = color 변수의 필드 개수 + 1 (Color 탭이 추가됨)
table.setHorizontalHeaderLabels(["Name", "Hex Code", "Color"])    # 표의 헤더 라벨 설정

# 표에 항목을 추가함
for i, (name, code) in enumerate(colors):
    item_name = QTableWidgetItem(name)
    item_code = QTableWidgetItem(code)
    item_color = QTableWidgetItem()
    item_color.setBackground(get_rgb_from_hex(code))
    table.setItem(i, 0, item_name)
    table.setItem(i, 1, item_code)
    table.setItem(i, 2, item_color)

table.show()
sys.exit(app.exec())

image

Tree 위젯을 사용하여 데이터 표시하기

트리에 정렬된 데이터를 표시하고 싶으면 QTreeWidget을 사용하십시오.

QTreeWidget을 사용하는 것만이 트리에 정보를 표시하는 유일한 경로는 아닙니다. QTreeView를 이용하여 데이터 모델을 만들고 표시할 수도 있지만 이 튜토리얼의 범위를 벗어납니다.

import sys
from PySide6.QtWidgets import QApplication, QTreeWidget, QTreeWidgetItem

# 딕셔너리를 정의함, 그리고 각 프로젝트에 속한 파일의 이름을 리스트에 추가함
data = {"Project A": ["file_a.py", "file_a.txt", "something.xls"],
        "Project B": ["file_b.csv", "photo.jpg"],
        "Project C": []}

app = QApplication()

tree = QTreeWidget()
tree.setColumnCount(2)    # 2개의 열 (항목의 이름, 프로젝트 디렉토리의 파일 타입)
tree.setHeaderLabels(["Name", "Type"])    # 트리의 헤더 라벨 설정

items = []
for key, values in data.items():
    item = QTreeWidgetItem([key])
    for value in values:
        ext = value.split(".")[-1].upper()     # 파일 확장자 추출
        child = QTreeWidgetItem([value, ext])  # 파일 이름과 파일 확장자를 자식 항목으로 만듦
        item.addChild(child)
    items.append(item)

tree.insertTopLevelItems(0, items)

tree.show()
sys.exit(app.exec())

image

Designer의 .ui 파일 사용하기 또는 QUiLoaderpyside6-uic와 함께 QtCreator 사용하기

이 페이지는 Qt for Python 프로젝트를 위해 Qt Widgets 기반 그래픽 인터페이스를 만들기 위한 Qt Designer 사용법을 설명합니다. Qt Designer는 그래픽 UI 설계 도구로서 독립형 바이너리(pyside6-designer)로 사용할 수도 있고 Qt Creator IDE에 포함시킬 수도 있습니다. Qt Creator 안에서 사용하는 방법은 Qt Designer 사용하기에 나와 있습니다.

image

설계한 내용은 XML 기반 포맷인 .ui 파일에 저장됩니다. 이 파일은 pyside6-uic 도구에 의해 프로젝트 빌드 시간 동안 위젯 인스턴스를 붙이는 Python 또는 C++ 코드로 변환됩니다.

Qt Creator에서 새로운 Qt Design Form을 만들려면, File/New File Or Project를 선택하고 "Main Window" 템플릿을 선택하십시오. mainwindow.ui로 저장하십시오. 중앙 위젯의 중심에 QPushButton을 추가하십시오.

당신이 저장한 mainwindow.ui 파일은 다음과 같이 나올 것입니다:

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>400</width>
    <height>300</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <widget class="QWidget" name="centralWidget">
   <widget class="QPushButton" name="pushButton">
    <property name="geometry">
     <rect>
      <x>110</x>
      <y>80</y>
      <width>201</width>
      <height>81</height>
     </rect>
    </property>
    <property name="text">
     <string>PushButton</string>
    </property>
   </widget>
  </widget>
  <widget class="QMenuBar" name="menuBar">
   <property name="geometry">
    <rect>
     <x>0</x>
     <y>0</y>
     <width>400</width>
     <height>20</height>
    </rect>
   </property>
  </widget>
  <widget class="QToolBar" name="mainToolBar">
   <attribute name="toolBarArea">
    <enum>TopToolBarArea</enum>
   </attribute>
   <attribute name="toolBarBreak">
    <bool>false</bool>
   </attribute>
  </widget>
  <widget class="QStatusBar" name="statusBar"/>
 </widget>
 <layoutdefault spacing="6" margin="11"/>
 <resources/>
 <connections/>
</ui>

이제 Python에서 UI 파일을 사용하는 방법을 선택할 준비가 되었습니다.

선택 A: Python 클래스 생성하기

UI 파일과 상호작용할 수 있는 표준 방법은 UI 파일로부터 Python 클래스를 생성하는 것입니다. pyside6-uic 도구가 있기 때문에 가능합니다. 이 도구를 사용하려면 콘솔에서 다음 커맨드를 실행해야 합니다:

pyside6-uic mainwindow.ui -o ui_mainwindow.py

커맨드의 모든 출력을 ui_mainwindow.py 파일에 리다이렉트하면 직접 import 될 것입니다:

import sys
from PySide6.QtWidgets import QApplication, QMainWindow
from PySide6.QtCore import QFile
from ui_mainwindow import Ui_MainWindow    # UI 파일에서 가져온 위젯 클래스

class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()
        # UI 파일로부터 생성된 Python 클래스를 로딩함
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)

if __name__ == "__main__":
    app = QApplication(sys.argv)

    window = MainWindow()
    window.show()

    sys.exit(app.exec())

주의: UI 파일이 바뀔 때마다 pyside6-uic를 또 실행해야 합니다.

선택 B: 직접 로딩하기

# File: main.py
import sys
from PySide6.QtUiTools import QUiLoader      # UI 파일을 직접 로드하기 위해 필요한 모듈
from PySide6.QtWidgets import QApplication
from PySide6.QtCore import QFile, QIODevice

if __name__ == "__main__":
    app = QApplication(sys.argv)

    # UI 파일 이름 지정
    ui_file_name = "mainwindow.ui"
    ui_file = QFile(ui_file_name)
    if not ui_file.open(QIODevice.ReadOnly):
        print(f"Cannot open {ui_file_name}: {ui_file.errorString()}")
        sys.exit(-1)
    # UI 파일을 동적으로 로드함
    loader = QUiLoader()
    window = loader.load(ui_file)

    ui_file.close()
    if not window:
        print(loader.errorString())
        sys.exit(-1)
    window.show()

    sys.exit(app.exec())

커맨드 프롬프트에서 다음과 같이 실행하면 됩니다:

python main.py

주의: QUiLoader는 시그널/슬롯 연결을 위해 문자열 인수 형태로 함수 시그니처를 취하는 connect() 호출을 사용합니다. 내부적으로 서로 다른 C++ 타입에 맵핑되기 때문에 Python에서 작성한 커스텀 위젯의 str 또는 list와 같은 Python 타입은 처리할 수 없습니다.

Qt Designer에서의 커스텀 위젯

Qt Designer는 커스텀 위젯을 사용할 수 있습니다. 커스텀 위젯은 위젯 박스 안에 있으며 Qt 위젯처럼 폼에 드래그할 수 있습니다. (Qt Designer로 커스텀 위젯 사용하기를 보십시오) 일반적으로 QDesignerCustomWidgetInterface를 구현하는 C++로 작성된 Qt Designer에 플러그인으로 위젯을 구현해야 합니다.

Qt for Python은 registerCustomWidget()와 마찬가지로 이를 위한 간단한 인터페이스를 제공합니다.

widgetbinding 예제(wigglywidget.py) 또는 taskmenuextension 예제(tictactoe.py)에 나온 것처럼 위젯이 Python 모듈로 제공되어야 합니다.

이름이 register*.py인 등록 스크립트를 제공하고 path-타입 환경 변수 PYSIDE_DESIGNER_PLUGINS가 해당 디렉토리를 가리키게 해서 이 위젯을 Qt Designer에 등록할 수 있습니다.

등록 스크립트의 코드는 다음과 같습니다:

# File: registerwigglywidget.py
from wigglywidget import WigglyWidget

import QtDesigner


TOOLTIP = "A cool wiggly widget (Python)"
DOM_XML = """
<ui language='c++'>
    <widget class='WigglyWidget' name='wigglyWidget'>
        <property name='geometry'>
            <rect>
                <x>0</x>
                <y>0</y>
                <width>400</width>
                <height>200</height>
            </rect>
        </property>
        <property name='text'>
            <string>Hello, world</string>
        </property>
    </widget>
</ui>
"""

QPyDesignerCustomWidgetCollection.registerCustomWidget(WigglyWidget, module="wigglywidget", tool_tip=TOOLTIP, xml=DOM_XML)

QPyDesignerCustomWidgetCollection은 QDesignerCustomWidgetCollectionInterface 구현을 제공합니다. 이것은 정적 편의 함수로 타입 등록을 위해 Qt Designer에 커스텀 위젯을 노출시키거나, QDesignerCustomWidgetInterface 인스턴스를 추가할 수 있게 해줍니다.

함수 registerCustomWidget()은 Qt Designer에 위젯 타입을 등록하는 데 사용합니다. 단순한 경우에는 QUiLoader.registerCustomWidget()처럼 사용될 수 있습니다. 이 함수는 인수로 커스텀 위젯 타입, 그리고 QDesignerCustomWidgetInterface의 getter에 해당하는 값들을 전달하는 선택적인 키워드 인수 몇 가지를 필요로 합니다:

pyside6-designer를 통해 Qt Designer를 실행할 때, 커스텀 위젯은 위젯 박스 안에 보여야 합니다.

고급 사용법의 경우, addCustomWidget()에게 타입 대신 클래스 QDesignerCustomWidgetInterface 구현 함수를 전달할 수도 있습니다. 이것은 커스텀 위젯에 대해 커스텀 컨텐스트 메뉴가 등록된 taskmenuextension 예제에서 볼 수 있습니다. 예제는 C++ Task Menu Extension 예제의 포팅 버전입니다.

Qt Designer 플러그인 문제해결하기

pyside6-designer를 반드시 사용해야 합니다. 독립형 Qt Designer는 플러그인을 로드하지 않습니다.

메뉴 항목 Help/About Plugin은 발견한 플러그인과 잠재적인 로드 오류 메시지를 보여주는 다이얼로그를 표시합니다.

더 많은 오류 메시지는 콘솔 또는 Windows Debug 뷰를 확인하십시오.

Python에 의한 출력 버퍼링 때문에 Qt Designer가 종류된 후에 오류 메시지가 나타날 수도 있습니다.

Qt for Python을 필드할 때, 플러그인을 제대로 설치하기 위해 --standalone 옵션을 설정하시기 바랍니다.

.qrc 파일 사용하기 (pyside6-rcc)

Qt 리소스 시스템은 애플리케이션에 바이너리 파일을 저장하기 위한 메커니즘입니다.

애플리케이션에 내장되는 파일은 QFile 클래스로 접근할 수 있으며 QIconQPixmap 클래스의 생성자는 :/로 시작하는 특수한 파일 이름을 사용하여 파일 이름을 취할 수 있습니다.

가장 일반적인 용도는 커스텀 이미지, 아이콘, 글꼴 등을 가져오는 것입니다.

이 튜토리얼에서는 커스텀 이미지를 버튼 아이콘으로 사용하는 방법을 배울 것입니다.

영감을 주기 위해 Qt의 멀티미디어 플레이어 예제를 각색해 보겠습니다.

다음 이미지에서 볼 수 있듯이, 미디어 액션(재생, 일시정지, 정지 등)을 위해 QPushButton을 사용합니다. 그리고 현재 기본 아이콘을 사용하고 있습니다.

image

아이콘을 직접 설계해서 애플리케이션을 더 매력적으로 만들 수 있겠지만, 원치 않는다면 다운로드한 예제를 사용하셔도 됩니다.

아이콘 다운로드하기

image

Qt 리소스 시스템 사이트에서 rcc 커맨드, .qrc 파일 포맷, 리소스 시스템에 대한 전반적인 내용을 찾을 수 있습니다.

.qrc 파일

커맨드 실행에 앞서, .qrc 파일에 리소스에 대한 정보를 추가하십시오. 다음 예제에서 icons.qrc 안에 리소스가 어떻게 나열되어 있는지 보십시오.

</ui>
<!DOCTYPE RCC><RCC version="1.0">
<qresource>
    <file>icons/play.png</file>
    <file>icons/pause.png</file>
    <file>icons/stop.png</file>
    <file>icons/previous.png</file>
    <file>icons/forward.png</file>
</qresource>
</RCC>

Python 파일 생성하기

이제 icons.qrc 파일이 준비 되었습니다. 리소스에 대한 바이너리 정보를 포함하는 Python 클래스를 생성하기 위해 pyside6-rcc 도구를 사용하십시오.

그러기 위해서는 다음을 실행해야 합니다:

pyside6-rcc icons.qrc -o rc_icons.py

-o 옵션은 출력 파일 이름을 지정할 수 있습니다. 이 경우 출력 파일의 이름은 rc_icons.py입니다.

생성된 파일을 사용하려면, 당신의 메인 Python 파일 최상단에 다음 import 문을 추가하십시오:

import rc_icons

코드 바꾸기

기존 예제를 수정하려면, 다음 라인을 변경해야 합니다:

from PySide6.QtGui import QIcon, QKeySequence
playIcon = self.style().standardIcon(QStyle.SP_MediaPlay)
previousIcon = self.style().standardIcon(QStyle.SP_MediaSkipBackward)
pauseIcon = self.style().standardIcon(QStyle.SP_MediaPause)
nextIcon = self.style().standardIcon(QStyle.SP_MediaSkipForward)
stopIcon = self.style().standardIcon(QStyle.SP_MediaStop)

위의 내용을 다음과 같이 바꾸십시오:

from PySide6.QtGui import QIcon, QKeySequence, QPixmap
playIcon = QIcon(QPixmap(":/icons/play.png"))
previousIcon = QIcon(QPixmap(":/icons/previous.png"))
pauseIcon = QIcon(QPixmap(":/icons/pause.png"))
nextIcon = QIcon(QPixmap(":/icons/forward.png"))
stopIcon = QIcon(QPixmap(":/icons/stop.png"))

이렇게 하면 애플리케이션 테마가 제공하는 기본 아이콘 대신 새로운 아이콘을 사용하게 됩니다.

다른 import 구문 밑에 다음을 추가하십시오.

import rc_icons

이제 클래스의 생성자는 다음과 같습니다:

def __init__(self):
    super(MainWindow, self).__init__()

    self.playlist = QMediaPlaylist()
    self.player = QMediaPlayer()

    toolBar = QToolBar()
    self.addToolBar(toolBar)

    fileMenu = self.menuBar().addMenu("&File")
    openAction = QAction(QIcon.fromTheme("document-open"),
                         "&Open...", self, shortcut=QKeySequence.Open,
                         triggered=self.open)
    fileMenu.addAction(openAction)
    exitAction = QAction(QIcon.fromTheme("application-exit"), "E&xit",
                         self, shortcut="Ctrl+Q", triggered=self.close)
    fileMenu.addAction(exitAction)

    playMenu = self.menuBar().addMenu("&Play")
    playIcon = QIcon(QPixmap(":/icons/play.png"))
    self.playAction = toolBar.addAction(playIcon, "Play")
    self.playAction.triggered.connect(self.player.play)
    playMenu.addAction(self.playAction)

    previousIcon = QIcon(QPixmap(":/icons/previous.png"))
    self.previousAction = toolBar.addAction(previousIcon, "Previous")
    self.previousAction.triggered.connect(self.previousClicked)
    playMenu.addAction(self.previousAction)

    pauseIcon = QIcon(QPixmap(":/icons/pause.png"))
    self.pauseAction = toolBar.addAction(pauseIcon, "Pause")
    self.pauseAction.triggered.connect(self.player.pause)
    playMenu.addAction(self.pauseAction)

    nextIcon = QIcon(QPixmap(":/icons/forward.png"))
    self.nextAction = toolBar.addAction(nextIcon, "Next")
    self.nextAction.triggered.connect(self.playlist.next)
    playMenu.addAction(self.nextAction)

    stopIcon = QIcon(QPixmap(":/icons/stop.png"))
    self.stopAction = toolBar.addAction(stopIcon, "Stop")
    self.stopAction.triggered.connect(self.player.stop)
    playMenu.addAction(self.stopAction)

    # 여러 라인들이 생략됨

예제 실행하기

새로운 아이콘 집합을 확인하려면 python main.py를 호출하여 애플리케이션을 실행해 보십시오:

image

애플리케이션 번역하기

Qt Linguist

Qt Linguist 및 관련 도구는 애플리케이션을 위한 번역을 제공하는 데 사용할 수 있습니다.

examples/widgets/linguist 예제가 이것을 설명하고 있습니다. 예제는 매우 간단한데 메뉴가 하나 있고 다중 선택이 가능한 프로그래밍 언어 리스트를 보여줍니다.

번역을 찾아보는 함수 호출을 통해 메시지 문자열을 전달하여 번역이 이루어집니다. 각 QObject 인스턴스는 번역을 위한 tr() 함수를 제공합니다. 또, 비-QObject 클래스에 번역된 텍스트를 추가하는 QCoreApplication.translate() 함수도 있습니다.

Qt는 오류 메시지 및 표준 다이얼로그 캡션을 포함하여 자체 번역을 탑재하고 있습니다.

linguist 예제에는 self.tr()에 들어 있는 여러 메시지들이 나옵니다. 선택 변경에 따라 나오는 상태 바 메시지는 개수에 따라 복수형을 사용합니다:

count = len(self._list_widget.selectionModel().selectedRows())
message = self.tr("%n language(s) selected", "", count)

예제에 대한 번역 워크플로우는 다음과 같습니다: 번역된 메시지는 lupdate 도구를 사용하여 추출할 수 있고 XML-기반 .ts 파일이 생성됩니다:

pyside6-lupdate main.py -ts example_de.ts

만약 example_de.ts가 이미 존재한다면, 코드에 새로운 메시지가 추가되어 업데이트됩니다.

만약 프로젝트에 폼 파일(.ui)과 QML 파일(.qml)이 있다면, 마찬가지로 pyside6-lupdate 도구에 전달해야 합니다:

pyside6-lupdate main.py main.qml form.ui -ts example_de.ts

폼 파일로부터 pyside6-uic에 의해 생성된 소스 파일은 전달하지 말아야 합니다.

.ts 파일은 Qt Linguist를 사용하여 번역합니다. 일단 완료되면, 파일은 바이너리 형태(.qm 파일)로 변환됩니다:

mkdir translations
pyside6-lrelease example_de.ts -qm translations/example_de.qm

.qm 파일을 탑재하고 싶지 않다면, 아이콘이나 다른 애플리케이션 리소스처럼 Qt 리소스 파일로 두는 것을 권장합니다. (.qrc 파일 사용하기 (pyside6-rcc)를 보십시오) 리소스 파일 linguist.qrc:/translations 아래의 example_de.qm을 제공합니다:

<!DOCTYPE RCC><RCC version="1.0">
<qresource>
    <file>translations/example_de.qm</file>
</qresource>
</RCC>

런타임 시에, QTranslator 클래스를 사용하여 번역 내용을 로드해야 합니다:

path = QLibraryInfo.location(QLibraryInfo.TranslationsPath)
translator = QTranslator(app)
if translator.load(QLocale.system(), 'qtbase', '_', path):
    app.installTranslator(translator)
translator = QTranslator(app)
path = ':/translations'
if translator.load(QLocale.system(), 'example', '_', path):
    app.installTranslator(translator)

이 코드는 먼저 Qt에 내장된 번역을 로드하고, 그 다음에 리소스로부터 로드된 애플리케이션의 번역을 로드합니다.

그 다음에는 예제는 독일어로 실행될 수 있습니다:

LANG=de python main.py

GNU gettext

GNU gettext 모듈은 애플리케이션 번역을 제공하는 데 사용할 수 있습니다.

examples/widgets/gettext 예제에서 이것을 설명합니다. 예제는 매우 간단합니다. 메뉴가 하나 있고 다중 선택이 가능한 프로그래밍 언어 리스트를 보여줍니다.

번역을 찾아보는 함수 호출을 통해 메시지 문자열을 전달하여 번역이 이루어집니다. 일반적으로 메인 번역 함수에게 _라는 별칭을 붙여줍니다. 개수에 따라 복수형을 포함하는 문장에 대하여 특수 번역 함수가 있습니다. ("{0} items(s) selected") 보통 ngettext라는 별칭을 붙여줍니다.

위의 함수들은 최상단에서 정의합니다:

import gettext
...
_ = None
ngettext = None

그리고 다음과 같이 나중에 할당됩니다:

src_dir = Path(__file__).resolve().parent
try:
    translation = gettext.translation('example', localedir=src_dir / 'locales')
    if translation:
        translation.install()
        _ = translation.gettext
        ngettext = translation.ngettext
except FileNotFoundError:
    pass
if not _:
    _ = gettext.gettext
    ngettext = gettext.ngettext

위의 예제는 번역 파일의 기본 이름이 example이며 locales 아래의 소스 트리에서 찾을 수 있다고 지정합니다. 이 코드는 현재 언어와 일치하는 번역을 로드하려고 할 것입니다.

번역된 메시지는 다음과 같습니다:

file_menu = self.menuBar().addMenu(_("&File"))

선택 변경에 따라 나오는 상태 바 메시지는 개수에 따라 복수형을 사용합니다:

count = len(self._list_widget.selectionModel().selectedRows())
message = ngettext("{0} language selected",
                   "{0} languages selected", count).format(count)

ngettext() 함수는 단수형, 복수형, 개수를 인수로 취합니다. 리턴된 문자열은 여전히 포맷팅 placeholder를 포함하고 있으므로 format()을 통해 전달되어야 합니다.

독일어를 말하도록 메시지를 번역하려면, 템플릿 파일(.pot)을 먼저 생성해야 합니다:

mkdir -p locales/de_DE/LC_MESSAGES
xgettext -L Python -o locales/example.pot main.py

이 파일은 적절한 값에 의해 치환될 수 있는 여러 가지 제네릭 placeholder를 가지고 있습니다. 그리고 나서 de_DE/LC_MESSAGES 디렉토리에 복사됩니다.

cd locales/de_DE/LC_MESSAGES/
cp ../../example.pot .

독일어 복수형과 부호화를 설명하기 위해 더 많은 각색이 필요합니다:

"Project-Id-Version: PySide6 gettext example\n"
"POT-Creation-Date: 2021-07-05 14:16+0200\n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"

다음에는 번역된 메시지가 주어질 수 있습니다:

#: main.py:57
msgid "&File"
msgstr "&Datei"

마지막으로 .pot은 배치되어야 하는 바이너리 형태(machine object 파일, .mo)로 변환됩니다.

msgfmt -o example.mo example.pot

그리고 나서 예제를 독일어로 실행할 수 있습니다::

LANG=de python main.py

위젯 애플리케이션 스타일 꾸미기

Qt Widgets 애플리케이션은 플랫폼에 종속적인 기본 테마를 사용합니다. 일부의 경우, Qt 테마를 변경하는 시스템 전체 구성이 있을 수 있으며 애플리케이션이 다르게 표시됩니다.

그러나 커스텀 위젯을 관리하고 각 구성요소에 커스텀 스타일을 제공할 수 있습니다. 예를 들어, 다음과 같은 간단한 코드를 봅시다:

import sys
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QApplication, QLabel

if __name__ == "__main__":
    app = QApplication()
    w = QLabel("This is a placeholder text")
    w.setAlignment(Qt.AlignCenter)
    w.show()
    sys.exit(app.exec())

이 코드를 실행하면, 당신은 placeholder text가 붙어 있는 중앙에 정렬된 간단한 QLabel을 보게 될 것입니다.

image

CSS-유사 문법을 사용하여 애플리케이션의 스타일을 꾸밀 수 있습니다. 더 많은 정보는 Qt 스타일 시트 레퍼런스를 보십시오.

background-colorfont-family 같은 CSS 속성을 설정하여 QLabel의 스타일을 꾸밀 수 있습니다. 이제 이 변경사항에 따라 코드가 어떻게 표시되는지 봅시다:

import sys
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QApplication, QLabel

if __name__ == "__main__":
    app = QApplication()
    w = QLabel("This is a placeholder text")
    w.setAlignment(Qt.AlignCenter)
    # 추가된 부분
    w.setStyleSheet("""
        background-color: #262626;
        color: #FFFFFF;
        font-family: Titillium;
        font-size: 18px;
        """)
    w.show()
    sys.exit(app.exec())

이제 코드를 실행하면 커스텀 스타일에 따라 QLabel이 다르게 보이는 것을 확인하실 수 있습니다:

image

주의: 만약 글꼴 Titillium이 설치되어 있지 않다면, 다른 방법을 사용해 보실 수 있습니다. QFontDatabase, 특히 families() 메서드를 사용하여 설치된 글꼴의 목록을 보실 수 있습니다.

각 UI 요소의 스타일을 일일이 꾸미는 것은 많은 작업을 필요로 합니다. 그 대안으로 Qt 스타일 시트를 사용하면 됩니다. Qt 스타일 시트란 애플리케이션의 UI 요소에 대한 스타일을 정의하는 1개 이상의 .qss 파일을 의미합니다.

Qt 스타일 시트 예제 문서 페이지에서 더 많은 예제를 보실 수 있습니다.

Qt 스타일 시트

경고: 애플리케이션을 수정하기 전에 애플리케이션의 모든 그래픽 세부사항에 대해 사용자가 책임져야 한다는 것을 명심하십시오. 여백과 크기를 변경하면 이상하게, 혹은 비뚤어져 보일 수 있습니다. 그래서 스타일을 변경하는 것은 신중해야 합니다. 가능한 모든 경우를 대비하기 위해 완전히 새로운 Qt 스타일을 만드는 것을 권장합니다.

qss 파일은 CSS 파일과 매우 비슷하지만, Widget 컴포넌트를 지정해야 하고 경우에 따라서는 객체의 이름을 지정해야 합니다:

QLabel {
    background-color: red;
}

QLabel#title {
    font-size: 20px;
}

1번째 스타일은 애플리케이션의 모든 QLabel 객체에 대한 background-color를 정의합니다. 반면, 2번째 스타일은 title 객체의 스타일만 정의합니다.

주의: 아무 객체나 setObjectName(str) 함수를 이용하여 객체 이름을 설정할 수 있습니다. 예를 들면: label = QLabel("Test")의 경우, label.setObjectName("title")라고 작성할 수 있습니다.

애플리케이션에 qss 파일이 있다면, 해당 파일을 읽고 QApplication.setStyleSheet(str) 함수를 사용하여 적용할 수 있습니다:

if __name__ == "__main__":
    app = QApplication()

    w = Widget()
    w.show()

    with open("style.qss", "r") as f:
        _style = f.read()
        app.setStyleSheet(_style)

    sys.exit(app.exec())

qss 파일을 사용하면 코드에서 스타일 관련 내용을 분리할 수 있습니다. 그리고 스타일을 쉽게 켜고 끌 수 있습니다.

더 많은 위젯 구성요소를 가진 새로운 예제를 봅시다:

class Widget(QWidget):
    def __init__(self, parent=None):
        super(Widget, self).__init__(parent)

        menu_widget = QListWidget()
        for i in range(10):
            item = QListWidgetItem(f"Item {i}")
            item.setTextAlignment(Qt.AlignCenter)
            menu_widget.addItem(item)

        text_widget = QLabel(_placeholder)
        button = QPushButton("Something")

        content_layout = QVBoxLayout()
        content_layout.addWidget(text_widget)
        content_layout.addWidget(button)
        main_widget = QWidget()
        main_widget.setLayout(content_layout)

        layout = QHBoxLayout()
        layout.addWidget(menu_widget, 1)
        layout.addWidget(main_widget, 4)
        self.setLayout(layout)

이것은 2개의 컬럼 위젯을 표시하는데, 왼쪽에는 QListWidget, 오른쪽에는 QLabelQPushButton가 나옵니다. 코드를 실행하면 다음과 같습니다:

image

앞에서 설명한 style.qss 파일에 내용을 추가하면, 예전 예제의 look-n-feel을 변경할 수 있습니다:

QListWidget {
    color: #FFFFFF;
    background-color: #33373B;
}

QListWidget::item {
    height: 50px;
}

QListWidget::item:selected {
    background-color: #2ABf9E;
}

QLabel {
    background-color: #FFFFFF;
    qproperty-alignment: AlignCenter;
}

QPushButton {
    background-color: #2ABf9E;
    padding: 20px;
    font-size: 18px;
}

스타일은 주로 여러 위젯의 컬러를 변경하고, 간격을 포함하여 정렬을 변경합니다. 또한 QListWidget 항목에 대해 상태 기반 스타일을 사용할 수도 있습니다. 예를 들면, 선택 여부에 따라 스타일을 다르게 하는 것입니다.

이 주제에 대해 탐색한 스타일을 모두 적용한 후에 QLabel 예제가 많이 달라 보일 것입니다. 코드를 실행하여 새로운 모습을 확인해 보십시오:

image

스타일 시트를 자유롭게 조정할 수 있으며 애플리케이션에 멋진 느낌을 줄 수 있습니다.

처음 QtQuick/QML 애플리케이션

QML은 선언적 언어로서 기존 언어보다 빠르게 애플리케이션을 개발할 수 있게 해줍니다. 선언적 특성 때문에 애플리케이션의 UI를 설계할 때 이상적입니다. QML에서는 프로퍼티를 가진 객체 트리로 사용자 인터페이스를 지정합니다. 이 튜토리얼에서는 PySide6와 QML로 간단한 "Hello World" 애플리케이션을 만드는 방법을 보여드리겠습니다.

PySide6/QML 애플리케이션은 적어도 2개의 파일로 구성되어 있습니다. - 사용자 인터페이스의 QML 설명이 포함된 파일, QML 파일을 로드하는 Python 파일. 쉽게 만들기 위해 2개의 파일을 동일한 디렉토리 안에 저장해 둡시다.

다음은 view.qml이라는 간단한 QML 파일입니다:

import QtQuick

Rectangle {
    id: main
    width: 200
    height: 200
    color: "green"

    Text {
        text: "Hello World"
        anchors.centerIn: main
    }
}

QML 모듈인 QtQuick을 가져오는 것부터 시작합니다.

QML 코드의 나머지는 이전에 HTML이나 XML 파일을 사용한 사람들에게는 매우 간단할 것입니다. 기본적으로 크기가 200*200인 녹색 직사각형을 만들고 "Hello World"라고 읽는 Text 요소를 추가할 것입니다. 코드 anchors.centerIn: mainid: main인 객체 내부의 중심에 텍스트가 나오게 합니다. 여기서 id: main는 Rectangle입니다.

이제 PySide6에서 코드가 어떻게 나오는지 봅시다. main.py를 호출해 봅시다:

import sys
from PySide6.QtWidgets import QApplication
from PySide6.QtQuick import QQuickView

if __name__ == "__main__":
    app = QApplication()
    view = QQuickView()

    view.setSource("view.qml")
    view.show()
    sys.exit(app.exec())

만약 PySide6가 익숙하고 튜토리얼을 잘 따라왔다면, 이미 이 코드를 여러 번 보았을 것입니다. 다른 점이라면 import QtQuick을 반드시 넣어야 하고 QQuickView 객체의 소스를 QML 파일의 URL로 설정해야 한다는 것입니다. QQuickView.show()를 호출하면 Qt 위젯과 비슷한 것을 보실 것입니다.

주의: 만약 데스크톱용 프로그램을 만들고 있다면, view를 보여주기 전에 view.setResizeMode(QQuickView.SizeRootObjectToView)를 추가하는 것을 고려해야 합니다.

main.py 스크립트를 실행하면, 다음 애플리케이션을 보게 될 것입니다:

image

Python-QML 통합

이 튜토리얼은 QML 파일을 로드하고 상호작용하는 Python 애플리케이션에 대한 간단한 설명을 제공합니다. QML은 명령형 언어로서 C++과 같은 기존 언어보다 더 빨리 UI를 설계할 수 있게 해줍니다. QtQml과 QtQuick 모듈은 QML-기반 UI에 대한 필수 인프라를 제공합니다.

이 튜토리얼에서는 Python과 QML 애플리케이션을 통합하는 방법에 대해 배우게 될 것입니다. 이 메커니즘은 QML 인터페이스에서 UI 요소로부터 특정 시그널에 대한 백엔드로서 Python을 사용하는 방법을 이해하는 데 도움을 줄 것입니다. 게다가 Qt Quick Controls 2의 기능 중 하나를 사용하여 QML 애플리케이션에게 모던 룩을 제공하는 방법도 배우게 될 것입니다.

이 튜토리얼은 많은 텍스트 프로퍼티(가령 글꼴 크기를 증가시키거나 글꼴 색상을 변경하거나 스타일을 변경하는 등)를 설정할 수 있는 애플리케이션을 기반으로 하고 있습니다. 시작하기 전에 PySide6 Python 패키지를 설치하십시오.

다음 단계별 과정은 QML 기반 애플리케이션과 PySide6 통합의 핵심 요소를 알려줄 것입니다:

  1. 먼저 다음 QML-기반 UI로 시작하겠습니다:

image

설계는 2개의 ColumnLayout을 포함하는 GridLayout을 기반으로 하고 있습니다. UI 내부에는 여러 개의 RadioButton, Button, Slider가 있습니다.

  1. QML 파일이 제대로 있으면, Python에서 로드할 수 있습니다:
if __name__ == '__main__':
    app = QGuiApplication(sys.argv)
    QQuickStyle.setStyle("Material")
    engine = QQmlApplicationEngine()

    # 현재 디렉토리의 경로를 가져와서 QML 파일의 이름을 추가하고 그것을 로드함
    qml_file = Path(__file__).parent / 'view.qml'
    engine.load(qml_file)

    if not engine.rootObjects():
        sys.exit(-1)

여기서 QML 파일을 load하기 위해 QQmlApplicationEngine만 필요하다는 것을 주목하십시오.

  1. QML에 등록될 요소에 대한 모든 논리를 포함하는 Bridge 클래스를 정의합니다:
# @QmlElement 데코레이터에서 사용하려면
# (QML_IMPORT_MINOR_VERSION은 선택적임)
QML_IMPORT_NAME = "io.qt.textproperties"
QML_IMPORT_MAJOR_VERSION = 1


@QmlElement
class Bridge(QObject):

    @Slot(str, result=str)
    def getColor(self, s):
        if s.lower() == "red":
            return "#ef9a9a"
        elif s.lower() == "green":
            return "#a5d6a7"
        elif s.lower() == "blue":
            return "#90caf9"
        else:
            return "white"

    @Slot(float, result=int)
    def getSize(self, s):
        size = int(s * 34)
        if size <= 0:
            return 1
        else:
            return size

    @Slot(str, result=bool)
    def getItalic(self, s):
        if s.lower() == "italic":
            return True
        else:
            return False

    @Slot(str, result=bool)
    def getBold(self, s):
        if s.lower() == "bold":
            return True
        else:
            return False

등록은 QmlElement 데코레이터 덕분에 이루어지며, 그 아래는 Bridge 클래스와 변수 QML_IMPORT_NAMEQML_IMPORT_MAJOR_VERSION에 대한 참조를 사용합니다.

  1. 이제 QML 파일로 되돌아가서 Bridge 클래스에서 정의된 슬롯에 시그널을 연결합니다:
Bridge {
   id: bridge
}

ApplicationWindow 내부에서 Python 클래스와 동일한 이름을 가진 컴포넌트를 선언하고 id:를 제공합니다. 이 id는 Python으로부터 등록된 요소에 대한 참조를 가져오는 데 도움을 줄 것입니다.

            RadioButton {
                id: italic
                Layout.alignment: Qt.AlignLeft
                text: "Italic"
                onToggled: {
                    leftlabel.font.italic = bridge.getItalic(italic.text)
                    leftlabel.font.bold = bridge.getBold(italic.text)
                    leftlabel.font.underline = bridge.getUnderline(italic.text)

                }
            }

프로퍼티 Italic, Bold, Underline은 상호 배타적이므로 언제든지 하나만 활성화할 수 있습니다. 이것을 성취하려면 위의 짧막한 코드에서 볼 수 있듯이 옵션 중 하나를 선택할 때마다 QML 요소 프로퍼티를 통해 3개의 프로퍼티를 확인해야 합니다. 셋 중 하나만 True를 리턴하고 나머지 둘은 False를 리턴할 것입니다. 이렇게 하면 하나의 효과만 텍스트에 적용될 것입니다.

  1. 각 슬롯은 선택한 옵션이 프로퍼티와 관련된 텍스트를 포함하는지 여부를 검증합니다:
    @Slot(str, result=bool)
    def getItalic(self, s):
        if s.lower() == "italic":
            return True
        else:
            return False

True 또는 False를 리턴하는 것은 QML UI 요소의 프로퍼티를 활성화/비활성화할 수 있게 해줍니다.

글꼴 크기를 리턴하는 슬롯처럼, Boolean이 아닌 다른 값을 리턴하는 것도 가능합니다:

    @Slot(float, result=int)
    def getSize(self, s):
        size = int(s * 34)
        if size <= 0:
            return 1
        else:
  1. 이제 애플리케이션의 외형을 변경하려면 다음 2개의 옵션을 선택할 수 있습니다:
  1. 커맨드 라인을 사용하십시오: --style 옵션을 추가해서 Python 파일을 실행하십시오:
python main.py --style material
  1. qtquickcontrols2.conf 파일을 사용하십시오:
[Controls]
Style=Material

[Universal]
Theme=System
Accent=Red

[Material]
Theme=Dark
Accent=Red

그리고 나서 이것을 .qrc 파일에 추가합니다:

<!DOCTYPE RCC><RCC version="1.0">
<qresource prefix="/">
    <file>qtquickcontrols2.conf</file>
</qresource>
</RCC>

pyside6-rcc style.qrc -o style_rc.py를 실행하는 rc 파일을 생성하고 main.py 스크립트로부터 그것을 가져옵니다.

import sys
from pathlib import Path

from PySide6.QtCore import QObject, Slot
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine, QmlElement
from PySide6.QtQuickControls2 import QQuickStyle

import style_rc

이 구성 파일에 대해서는 여기에서 더 많이 알아볼 수 있습니다.

애플리케이션의 최종 외형은 다음과 같습니다:

image

다운로드 링크: view.qml

다운로드 링크: main.py

QML 애플리케이션 튜토리얼

이 튜토리얼은 QML 파일을 로드하는 Python 애플리케이션에 대한 간단한 설명을 제공합니다. QML은 명령형 언어로서 C++과 같은 기존 언어보다 더 빨리 UI를 설계할 수 있게 해줍니다. QtQml과 QtQuick 모듈은 QML-기반 UI에 대한 필수 인프라를 제공합니다.

이 튜토리얼에서는 Python에서 QML 초기 프로퍼티 형태로 데이터를 제공하는 방법도 배우게 될 것입니다. 이 데이터는 QML 파일에 정의된 ListView가 사용할 것입니다.

시작하기 전에 다음 전제조건을 설치하십시오:

다음 단계별 지침은 Qt Creator를 사용한 애플리케이션 개발 과정을 알려줄 것입니다:

  1. 다음 다이얼로그를 열기 위해 Qt Creator를 열고 File > New File or Project.. 메뉴 항목을 선택하십시오:

image

  1. 애플리케이션 템플릿 목록에서 Qt for Python - Empty를 선택하고 Choose를 선택하십시오.

image

  1. 프로젝트에 Name을 부여하고, 파일시스템 내 위치를 선택하고, Finish를 선택하여 비어 있는 main.pymain.pyproject를 생성합니다.

image

이렇게 하면 프로젝트에 대한 main.pymain.pyproject 파일이 만들어집니다.

  1. 다운로드

view.qmllogo.png를 다운로드하고 프로젝트 폴더로 이동시킵니다.

  1. 편집 모드에서 열기 위해 main.pyproject를 더블-클릭하고, 파일 목록에 view.qmllogo.png를 추가하십시오. 이렇게 하면 프로젝트 파일은 다음과 같이 되어야 합니다:
{
    "files": ["main.py", "view.qml", "logo.png"]
}
  1. 이제 애플리케이션에 필요한 것들이 확보되었으니, main.py에 Python 모듈을 가져오고 나서 국가별 데이터를 다운로드하고 서식을 만드십시오:
import sys
import urllib.request
import json
from pathlib import Path

from PySide6.QtQuick import QQuickView
from PySide6.QtCore import QStringListModel, QUrl
from PySide6.QtGui import QGuiApplication


if __name__ == '__main__':

    # 데이터 가져오기
    url = "http://country.io/names.json"
    response = urllib.request.urlopen(url)
    data = json.loads(response.read().decode('utf-8'))

    # 데이터의 서식을 꾸미고 정렬함
    data_list = list(data.values())
    data_list.sort()
  1. 이제 애플리케이션 전체 설정을 관리하는 PySide6.QtGui.QGuiApplication을 이용하여 애플리케이션 창을 설정하십시오.
import sys
import urllib.request
import json
from pathlib import Path

from PySide6.QtQuick import QQuickView
from PySide6.QtCore import QStringListModel, QUrl
from PySide6.QtGui import QGuiApplication


if __name__ == '__main__':

    # 데이터 가져오기
    url = "http://country.io/names.json"
    response = urllib.request.urlopen(url)
    data = json.loads(response.read().decode('utf-8'))

    # 데이터의 서식을 꾸미고 정렬함
    data_list = list(data.values())
    data_list.sort()

    # 애플리케이션 창 설정
    app = QGuiApplication(sys.argv)
    view = QQuickView()
    view.setResizeMode(QQuickView.SizeRootObjectToView)

주의: 만약 창 크기에 맞게 루트 항목 자체가 리사이즈 되거나, 루트 항목 크기에 맞게 창 크기가 리사이즈 되기를 원한다면 리사이즈 정책을 설정하는 것이 중요합니다. 그렇게 하지 않으면 루트 항목은 창 크기가 변할 때 원래 크기를 유지할 것입니다.

  1. 이제 data_list 변수를 QML 초기 프로퍼티로 노출시킬 수 있게 되었습니다. 이것은 view.qml에 있는 QML ListView가 사용할 것입니다.
import sys
import urllib.request
import json
from pathlib import Path

from PySide6.QtQuick import QQuickView
from PySide6.QtCore import QStringListModel, QUrl
from PySide6.QtGui import QGuiApplication


if __name__ == '__main__':

    # 데이터 가져오기
    url = "http://country.io/names.json"
    response = urllib.request.urlopen(url)
    data = json.loads(response.read().decode('utf-8'))

    # 데이터의 서식을 꾸미고 정렬함
    data_list = list(data.values())
    data_list.sort()

    # 애플리케이션 창 설정
    app = QGuiApplication(sys.argv)
    view = QQuickView()
    view.setResizeMode(QQuickView.SizeRootObjectToView)

    # QML 코드에 리스트 노출시키기
    my_model = QStringListModel()
    my_model.setStringList(data_list)
    view.setInitialProperties({"myModel": my_model})
  1. view.qmlQQuickView에 로드하고 show()를 호출하여 애플리케이션 창에 표시합니다.
import sys
import urllib.request
import json
from pathlib import Path

from PySide6.QtQuick import QQuickView
from PySide6.QtCore import QStringListModel, QUrl
from PySide6.QtGui import QGuiApplication


if __name__ == '__main__':

    # 데이터 가져오기
    url = "http://country.io/names.json"
    response = urllib.request.urlopen(url)
    data = json.loads(response.read().decode('utf-8'))

    # 데이터의 서식을 꾸미고 정렬함
    data_list = list(data.values())
    data_list.sort()

    # 애플리케이션 창 설정
    app = QGuiApplication(sys.argv)
    view = QQuickView()
    view.setResizeMode(QQuickView.SizeRootObjectToView)

    # QML 코드에 리스트 노출시키기
    my_model = QStringListModel()
    my_model.setStringList(data_list)
    view.setInitialProperties({"myModel": my_model})

    # QML 파일 로드하기
    qml_file = Path(__file__).parent / "view.qml"
    view.setSource(QUrl.fromLocalFile(qml_file.resolve()))

    # 창 보여주기
    if view.status() == QQuickView.Error:
        sys.exit(-1)
    view.show()
  1. 마지막으로 이벤트 루프를 시작하기 위해 애플리케이션을 실행하고 정리하십시오.
import sys
import urllib.request
import json
from pathlib import Path

from PySide6.QtQuick import QQuickView
from PySide6.QtCore import QStringListModel, QUrl
from PySide6.QtGui import QGuiApplication


if __name__ == '__main__':

    # 데이터 가져오기
    url = "http://country.io/names.json"
    response = urllib.request.urlopen(url)
    data = json.loads(response.read().decode('utf-8'))

    # 데이터의 서식을 꾸미고 정렬함
    data_list = list(data.values())
    data_list.sort()

    # 애플리케이션 창 설정
    app = QGuiApplication(sys.argv)
    view = QQuickView()
    view.setResizeMode(QQuickView.SizeRootObjectToView)

    # QML 코드에 리스트 노출시키기
    my_model = QStringListModel()
    my_model.setStringList(data_list)
    view.setInitialProperties({"myModel": my_model})

    # QML 파일 로드하기
    qml_file = Path(__file__).parent / "view.qml"
    view.setSource(QUrl.fromLocalFile(qml_file.resolve()))

    # 창 보여주기
    if view.status() == QQuickView.Error:
        sys.exit(-1)
    view.show()

    # 실행하고 정리하기
    app.exec()
    del view
  1. 이제 애플리케이션을 실행할 준비가 되었습니다. Projects 모드를 선택하고 실행할 Python 버전을 선택하십시오.

image

만약 다음과 같이 나온다면 CTRL+R 키보드 단축키를 사용하여 애플리케이션을 실행하십시오:

image

또한 이 애플리케이션을 개발하기 위한 지침을 위해 다음 영상 튜토리얼을 보셔도 됩니다:

Video Label

관련 정보

QML 레퍼런스

Python-QML 통합

QML, SQL 및 PySide 통합 튜토리얼

이 튜토리얼은 Qt 채팅 튜토리얼과 매우 비슷합니다. 하지만 UI에 QML을 사용하는 PySide6 애플리케이션에 SQL 데이터베이스를 통합하는 방법을 설명하는 데 중점을 두고 있습니다.

sqlDialog.py

프로그램에 적절한 라이브러리를 가져옵니다. 테이블 이름을 저장할 글로벌 변수를 정의하고, 존재하지 않을 경우 새로운 테이블을 생성하는 글로벌 함수 createTable()을 정의합니다. 이 데이터베이스에는 대화의 시작을 흉내내기 위한 1줄을 포함하고 있습니다.

import datetime
import logging

from PySide6.QtCore import Qt, Slot
from PySide6.QtSql import QSqlDatabase, QSqlQuery, QSqlRecord, QSqlTableModel
from PySide6.QtQml import QmlElement

table_name = "Conversations"
QML_IMPORT_NAME = "ChatModel"
QML_IMPORT_MAJOR_VERSION = 1


def createTable():
    if table_name in QSqlDatabase.database().tables():
        return

    query = QSqlQuery()
    if not query.exec_(
        """
        CREATE TABLE IF NOT EXISTS 'Conversations' (
            'author' TEXT NOT NULL,
            'recipient' TEXT NOT NULL,
            'timestamp' TEXT NOT NULL,
            'message' TEXT NOT NULL,
        FOREIGN KEY('author') REFERENCES Contacts ( name ),
        FOREIGN KEY('recipient') REFERENCES Contacts ( name )
        )
        """
    ):
        logging.error("데이터베이스에 쿼리하는 것이 실패함.")

    # 이렇게 하면 Bot의 1번째 메시지가 추가됩니다.
    # 대화형으로 만들려면 추가 개발이 필요합니다.
    query.exec_(
        """
        INSERT INTO Conversations VALUES(
            'machine', 'Me', '2019-01-07T14:36:06', 'Hello!'
        )
        """
    )

SqlConversationModel 클래스는 편집-불가능한 연락처 리스트에 필요한 읽기-전용 데이터 모델을 제공합니다. 이것은 이러한 사용자 사례에 대한 논리적 선택인 QSqlQueryModel 클래스에서 파생됩니다. 그리고 나서 테이블 생성을 진행합니다. setTable() 메서드로 예전에 정의한 이름을 설정합니다. 채팅 애플리케이션의 개념을 반영하는 프로그램을 갖추기 위해 테이블에 필수 애트리뷰트를 추가합니다.

@QmlElement
class SqlConversationModel(QSqlTableModel):
    def __init__(self, parent=None):
        super(SqlConversationModel, self).__init__(parent)

        createTable()
        self.setTable(table_name)
        self.setSort(2, Qt.DescendingOrder)
        self.setEditStrategy(QSqlTableModel.OnManualSubmit)
        self.recipient = ""

        self.select()
        logging.debug("테이블이 성공적으로 로드됨.")

setRecipient()에서는 데이터베이스로부터 리턴된 결과에 필터를 설정하고, 메시지의 수신자가 변경될 때마다 시그널을 방출합니다.

    def setRecipient(self, recipient):
        if recipient == self.recipient:
            pass

        self.recipient = recipient

        filter_str = (f"(recipient = '{self.recipient}' AND author = 'Me') OR "
                      f"(recipient = 'Me' AND author='{self.recipient}')")
        self.setFilter(filter_str)
        self.select()

data() 함수는 역할이 커스텀 사용자 역할이 아닐 경우 QSqlTableModel의 구현으로 되돌아갑니다. 만약 사용자 역할을 획득하면, 해당 필드의 인덱스를 얻기 위해 거기서 UserRole()을 뺄 수 있습니다. 그리고 나서 리턴된 값을 찾기 위해 그 인덱스를 사용합니다.

    def data(self, index, role):
        if role < Qt.UserRole:
            return QSqlTableModel.data(self, index, role)

        sql_record = QSqlRecord()
        sql_record = self.record(index.row())

        return sql_record.value(role - Qt.UserRole)

roleNames()에서는 키-값 쌍 형태의 커스텀 역할과 역할 이름을 Python 딕셔너리로 리턴합니다. 그래서 이 역할을 QML에서 사용할 수 있습니다. 또는 모든 역할 값을 저장하기 위해 Enum을 선언하는 것이 유용할 수 있습니다. 여기서 names는 해시(hash)가 되어야 딕셔너리 키로 사용할 수 있기 때문에 hash 함수를 사용한다는 것을 주의하십시오.

    def roleNames(self):
        """QSqlTableModel이 예상한 결과이므로 dict를 hash로 변환함"""
        names = {}
        author = "author".encode()
        recipient = "recipient".encode()
        timestamp = "timestamp".encode()
        message = "message".encode()

        names[hash(Qt.UserRole)] = author
        names[hash(Qt.UserRole + 1)] = recipient
        names[hash(Qt.UserRole + 2)] = timestamp
        names[hash(Qt.UserRole + 3)] = message

        return names

send_message() 함수는 데이터베이스에 새로운 레코드를 삽입하기 위해 주어진 수신자와 메시지를 사용합니다. OnManualSubmit() 함수를 사용하려면 submitAll() 함수도 호출해야 합니다. 왜냐하면 함수를 호출하기 전까지 모든 변경사항이 모델 안에 캐시되기 때문입니다.

    # 이것은 PySide가 Q_INVOKABLE을 제공하지 않기 때문에 사용하는 차선책입니다.
    # 그래서 이것을 QML로부터 호출할 수 있는 슬롯으로 선언합니다.
    @Slot(str, str, str)
    def send_message(self, recipient, message, author):
        timestamp = datetime.datetime.now()

        new_record = self.record()
        new_record.setValue("author", author)
        new_record.setValue("recipient", recipient)
        new_record.setValue("timestamp", str(timestamp))
        new_record.setValue("message", message)

        logging.debug(f'메시지: "{message}" \n 수신자: "{recipient}"')

        if not self.insertRecord(self.rowCount(), new_record):
            logging.error("메시지 송신 실패: {self.lastError().text()}")
            return

        self.submitAll()
        self.select()

chat.qml

chat.qml 파일을 봅시다.

import QtQuick
import QtQuick.Layouts
import QtQuick.Controls

먼저, Qt Quick 모듈을 가져옵니다. 이 모듈을 사용하면 Item, Rectangle, Text 등과 같은 그래픽 프리미티브에 접근할 수 있습니다. 모든 타입 목록을 보려면 Qt Quick QML 타입 문서를 보십시오. 그리고 나서 간략하게 언급했던 QtQuick.Layouts import 구문을 추가합니다.

다음에는 Qt Quick Controls 모듈을 가져옵니다. 다른 것과 달리, 이것은 기존 루트 타입인 Window를 대체하는 ApplicationWindow에 대한 접근을 제공합니다:

chat.qml 파일을 살펴보겠습니다.

ApplicationWindow {
    id: window
    title: qsTr("Chat")
    width: 640
    height: 960
    visible: true

ApplicationWindow는 헤더와 푸터를 만들기 위한 편의성을 일부 가미한 Window입니다. 이것은 또한 팝업에 대한 기초를 제공하고 배경색 같은 기본 스타일링을 지원합니다.

ApplicationWindow를 사용할 때 거의 항상 설정하는 3가지 프로퍼티가 있습니다: width, height, visible. 일단 이 프로퍼티들을 설정하면 적절한 크기의 빈 창이 컨텐츠로 채워질 준비가 됩니다.

SqlConversationModel 클래스를 QML에 노출시킬 것이기 때문에, 거기에 접근할 컴포넌트를 선언할 것입니다:

    SqlConversationModel {
        id: chat_model
    }

QML에 항목을 배치하는 2가지 방법이 있습니다: Item PositionersQt Quick Layouts.

  • Item Positioner(Row, Column 등)는 항목의 크기가 알려져 있거나 고정되어 있고, 일정한 형태로 항목들을 깔끔하게 배치해야 할 경우에 유용합니다.
  • Qt Quick Layouts의 레이아웃은 항목을 배치하고 리사이즈할 수 있으므로 크기를 조정할 수 있는 사용자 인터페이스에 적합합니다. 아래의 경우 ListViewPane을 수직으로 배치하기 위해 ColumnLayout을 사용합니다.
    ColumnLayout {
        anchors.fill: window

        ListView {
        Pane {
            id: pane
            Layout.fillWidth: true

Pane은 기본적으로 Rectangle이며 애플리케이션 스타일의 색상을 이용합니다. Frame과 비슷하지만 창 외곽선이 없습니다.

레이아웃의 직계 자식인 항목은 이용할 수 있는 다양한 부착 프로퍼티를 갖고 있습니다. ListView에서 Layout.fillWidthLayout.fillHeight를 사용하여 ColumnLayout 내부에서 가능한 많은 공간을 사용할 수 있도록 해주고 Pane에서도 동일하게 수행됩니다. ColumnLayout은 수직 레이아웃이므로, 각 자식 항목의 왼쪽 또는 오른쪽에 다른 항목이 없으므로 각 항목은 레이아웃의 전체 너비를 차지합니다.

한편, ListViewLayout.fillHeight 구문을 사용하면 Pane을 수용한 후에 남은 공간을 차지하게 할 수 있습니다.

ListView를 좀 더 자세히 봅시다:

        ListView {
            id: listView
            Layout.fillWidth: true
            Layout.fillHeight: true
            Layout.margins: pane.leftPadding + messageField.leftPadding
            displayMarginBeginning: 40
            displayMarginEnd: 40
            verticalLayoutDirection: ListView.BottomToTop
            spacing: 12
            model: chat_model
            delegate: Column {
                anchors.right: sentByMe ? listView.contentItem.right : undefined
                spacing: 6

                readonly property bool sentByMe: model.recipient !== "Me"
                Row {
                    id: messageRow
                    spacing: 6
                    anchors.right: sentByMe ? parent.right : undefined

                    Rectangle {
                        width: Math.min(messageText.implicitWidth + 24,
                            listView.width - (!sentByMe ? messageRow.spacing : 0))
                        height: messageText.implicitHeight + 24
                        radius: 15
                        color: sentByMe ? "lightgrey" : "steelblue"

                        Label {
                            id: messageText
                            text: model.message
                            color: sentByMe ? "black" : "white"
                            anchors.fill: parent
                            anchors.margins: 12
                            wrapMode: Label.Wrap
                        }
                    }
                }

                Label {
                    id: timestampText
                    text: Qt.formatDateTime(model.timestamp, "d MMM hh:mm")
                    color: "lightgrey"
                    anchors.right: sentByMe ? parent.right : undefined
                }
            }

            ScrollBar.vertical: ScrollBar {}
        }

부모 항목의 widthheight를 채운 후에, 뷰의 여백 일부도 설정합니다.

다음에는 displayMarginBeginningdisplayMarginEnd를 설정합니다. 이러한 프로퍼티를 사용하면 뷰 가장자리에서 스크롤할 때 뷰 외부의 위임자가 사라지지 않습니다. 더 잘 이해하기 위해서는 프로퍼티를 주석화 했다가 코드를 다시 실행하는 것을 고려해 보십시오. 이제 뷰를 스크롤할 때 어떤 일이 일어나는지 보십시오.

그 다음에는 뷰의 세로 방향을 뒤집어 보십시오. 그러면 1번째 항목이 밑에 있습니다.

또한 연락처가 보낸 메시지는 연락처가 보낸 메시지와 구별되어야 합니다. 우선, 당신이 메시지를 보내면 다른 연락처 간에 번갈아 가며 sentByMe 프로퍼티를 설정합니다. 이 프로퍼티를 사용하여 다음 2가지 방식으로 서로 다른 연락처를 구별합니다:

  • anchors.rightparent.right로 설정하여 연락처가 보낸 메시지를 화면의 오른쪽에 정렬합니다.
  • 연락처에 따라 직사각형의 색상을 변경합니다. 어두운 배경에 어두운 글자를 표시하고 싶지 않기 때문에 연락처에서 누구를 선택했느냐에 따라 글자 섹상을 설정합니다.

화면 아래쪽에 멀티-라인 텍스트 입력을 허용하기 위해 TextArea를 배치하고, 메시지를 보낼 수 있도록 버튼을 배치합니다. Pane을 사용하여 다음 2개 항목아래의 영역을 다룹니다:

        Pane {
            id: pane
            Layout.fillWidth: true

            RowLayout {
                width: parent.width

                TextArea {
                    id: messageField
                    Layout.fillWidth: true
                    placeholderText: qsTr("메시지 작성")
                    wrapMode: TextArea.Wrap
                }

                Button {
                    id: sendButton
                    text: qsTr("보내기")
                    enabled: messageField.length > 0
                    onClicked: {
                        listView.model.send_message("machine", messageField.text, "Me");
                        messageField.text = "";
                    }
                }
            }
        }

TextArea는 화면의 이용 가능한 너비를 채워야 합니다. 사용자가 어디서 타이핑을 시작해야 하는지 시각적 단서를 제공하기 위해 placeholder 텍스트를 할당합니다. 입력 영역 내부 텍스트는 화면 밖으로 나가지 않도록 감싸야 합니다.

마지막으로 sqlDialog.py에서 정의한 send_message 메서드를 호출할 수 있는 버튼을 가지고 있습니다. 여기에는 모형 예제만 있고 이 대화에는 하나의 수신자와 하나의 송신자만 있으므로 여기서는 문자열만 사용할 것입니다.

main.py

Python의 print() 대신 logging을 사용하는 이유는 애플리케이션이 생성할 (오류, 경고, 정보) 메시지의 수준을 더 잘 제어할 수 있는 방법을 제공하기 때문입니다.

import sys
import logging

from PySide6.QtCore import QDir, QFile, QUrl
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtSql import QSqlDatabase

# QmlElement 타입 등록을 트리거하기 위해 이 파일을 가져옵니다.
import sqlDialog

logging.basicConfig(filename="chat.log", level=logging.DEBUG)
logger = logging.getLogger("logger")

connectToDatabase() 함수는 SQLite 데이터베이스에 대한 접속을 생성합니다. 만약 실제 파일이 존재하지 않으면 그것을 생성합니다.

def connectToDatabase():
    database = QSqlDatabase.database()
    if not database.isValid():
        database = QSqlDatabase.addDatabase("QSQLITE")
        if not database.isValid():
            logger.error("데이터베이스를 추가할 수 없음")

    write_dir = QDir("")
    if not write_dir.mkpath("."):
        logger.error("쓰기 가능한 디렉토리 생성에 실패함")

    # 모든 장치에서 쓰기 가능한 위치가 있는지 확인합니다.
    abs_path = write_dir.absolutePath()
    filename = f"{abs_path}/chat-database.sqlite3"

    # SQLite 드라이버를 사용할 때, open() 함수는 SQLite 데이터베이스가 존재하지 않으면 그것을 생성할 것입니다.
    database.setDatabaseName(filename)
    if not database.open():
        logger.error("데이터베이스를 열 수 없음")
        QFile.remove(filename)

main 함수에서 몇 가지 흥미로운 일들이 벌어집니다:

  • QApplication 대신 QGuiApplication을 사용해야 합니다. 왜냐하면 QtWidgets 모듈을 사용하지 않기 때문입니다.
  • 데이터베이스에 연결합니다.
  • QQmlApplicationEngine을 선언합니다. 이를 통해 sqlDialog.py에서 구축한 대화 모델로부터 QML 요소에 접근하여 Python과 QML을 연결할 수 있습니다.
  • UI를 정의하는 .qml 파일을 로드합니다.

마지막으로 Qt 애플리케이션을 실행하면 프로그램이 시작합니다.

if __name__ == "__main__":
    app = QGuiApplication()
    connectToDatabase()

    engine = QQmlApplicationEngine()
    engine.load(QUrl("chat.qml"))

    if not engine.rootObjects():
        sys.exit(-1)

    app.exec()

image

파일 시스템 탐색기 확장하기 예제

이 튜토리얼은 간단한 스킴(Scheme) 관리자를 추가하여 파일시스템 탐색기 예제를 확장하는 방법을 보여줍니다. 이 기능을 사용하면 애플리케이션 런타임 중에 컬러 스킴을 전환할 수 있습니다. 컬러 스킴은 JSON 포맷으로 선언되고 커스텀 Python-QML 플러그인을 통해 이용할 수 있게 됩니다.

image

컬러 스킴 정의하기

컬러 스킴을 정의하기 위해 원래의 예제와 동일한 컬러 이름을 사용할 수 있습니다. 그래서 모든 경우에 대해 이름을 변경할 필요가 없습니다. 원래 컬러는 다음과 같이 Colors.qml 파일에 정의되어 있습니다:

resources/Colors.qml

QtObject {
    readonly property color background: "#23272E"
    readonly property color surface1: "#1E2227"
    readonly property color surface2: "#090A0C"
    readonly property color text: "#ABB2BF"
    readonly property color textFile: "#C5CAD3"
    readonly property color disabledText: "#454D5F"
    readonly property color selection: "#2C313A"
    readonly property color active: "#23272E"
    readonly property color inactive: "#3E4452"
    readonly property color folder: "#3D4451"
    readonly property color icon: "#3D4451"
    readonly property color iconIndicator: "#E5C07B"
    readonly property color color1: "#E06B74"
    readonly property color color2: "#62AEEF"
}

schemes.json 파일은 컬러 스킴을 보관하고 있습니다. 이것을 구현하기 위해 Catppuccin 스킴을 사용할 수 있습니다.

schemes.json

  "Catppuccin": {
    "background": "#1E1E2E",
    "surface1": "#181825",
    "surface2": "#11111B",
    "text": "#CDD6F4",
    "textFile": "#CDD6F4",
    "disabledText": "#363659",
    "selection": "#45475A",
    "active": "#1E1E2E",
    "inactive": "#6C7086",
    "folder": "#6C7086",
    "icon": "#6C7086",
    "iconIndicator": "#FFCC66",
    "color1": "#CBA6F7",
    "color2": "#89DCEB"
  },

"Catppuccin" 컬러 스킴 외에도, 4개의 컬러 스킴을 더 구현하였습니다: Nordic, One Dark, Gruvbox, Solarized. 하지만 자유롭게 창의력을 발휘하고 스킴을 가지고 실험해 보십시오.

새로운 컬러 스킴을 정의하려면, 위의 구조를 복사하고 새로운 컬러 값을 제공하십시오.

스킴 관리자 구현하기

컬러 스킴을 정의한 후에 실제 스킴 관리자를 구현할 수 있습니다. 관리자는 schemes.json 파일을 읽고 런타임 도중에 스킴 간 전환을 위한 QML 바인딩을 제공할 것입니다.

스킴 관리자를 구현하기 위해, QML에 SchemeManager 객체를 노출시키는 Python-QML 플러그인을 생성하십시오. 이 객체는 schemes.json 파일로부터 컬러 스킴을 로드하고 스킴 간 전환을 하는 메서드를 갖게 될 것입니다.

프로젝트 디렉토리 안에 scheme_manager.py라는 새로운 Python 파일을 생성하십시오. 이 파일에서 SchemeManager 클래스를 정의합니다:

scheme_manager.py

QML_IMPORT_NAME = "FileSystemModule"
QML_IMPORT_MAJOR_VERSION = 1


@QmlNamedElement("Colors")
@QmlSingleton
class SchemeManager(QObject):

이미 존재하는 코드에 자연스럽게 통합하려면, QML_IMPORT_NAME = "FileSystemModule"을 통해 이미 존재하는 동일한 QML 모듈에 SchemeManager를 부착하십시오. 또한 Colors.qml 파일 대신 @QmlNamedElement 데코레이터를 사용하여 커스텀 플러그인 사용으로 자연스럽게 전환할 수 있습니다. 이렇게 변경하면 다음과 같이 모든 이전 할당을 편집하는 것을 막을 수 있습니다:

import FileSystemModule
...
Rectangle {
    color: Colors.background
}

일단 애플리케이션이 시작되면 생성자는 schemes.json 파일을 읽고 setTheme 멤버 함수를 호출합니다.

scheme_manager.py

    schemeChanged = Signal()

    def __init__(self, parent=None):
        super().__init__(parent=parent)
        with open(Path(__file__).parent / "schemes.json", 'r') as f:
            self.m_schemes = json.load(f)
        self.m_activeScheme = {}

FileSystemModule에 Colors라는 호출 가능한 QML 요소로 SchemeManager를 추가하면, 클래스를 매번 가져오거나 이전 할당을 편집할 필요 없이 코드에서 접근할 수 있습니다. 이렇게 하면 워크플로우가 간소화됩니다.

JSON 포맷으로 스킴을 정의하고 SchemeManager 클래스를 Colors라는 이름으로 QML에서 호출 가능한 요소로 만든 후, 예제에서 새로운 스킴 매니저를 완전히 통합하는 2가지 단계가 남아 있습니다.

1번째 단계는 JSON 파일에서 컬러 스킴을 로드하는 SchemeManager 클래스에 함수를 생성하는 것입니다. 2번째 단계는 전에 할당 가능한 프로퍼티로 구문 Colors.<previousName>와 함께 사용했던 동일한 이름으로 QML에서 개별 컬러를 사용할 수 있도록 하는 것입니다.

scheme_manager.py

        self.setScheme(self.m_activeSchemeName)

    @Slot(str)
    def setScheme(self, theme):
        for k, v in self.m_schemes[theme].items():
            self.m_activeScheme[k] = QColor.fromString(v)

setScheme 메서드는 컬러 스킴 간 전환하는 역할을 합니다. QML에서 이 메서드에 접근할 수 있게 하려면, @Slot(str) 데코레이터를 사용하고 입력 파라미터로 문자열 하나를 취하도록 지정하십시오. 이 메서드에서는 JSON 파일로부터 컬러 값으로 딕셔너리를 채웁니다.

주의: 단순성 때문에 오류 검사는 수행하지 않습니다. JSON에 포함된 키의 유효성을 검사해야 할 것입니다.

scheme_manager.py

    @Property(QColor, notify=schemeChanged)
    def background(self):
        return self.m_activeScheme["background"]

QML에서 할당 가능한 컬러 프로퍼티를 만들려면 @Property 데코레이터를 사용하십시오. 각 프로퍼티에 대한 딕셔너리로부터 해당 컬러 값을 간단히 리턴합니다. 이 과정은 애플리케이션에서 사용하는 다른 모든 컬러에 대하여 반복됩니다. 이 시점에서 애플리케이션은 생성자의 활성 스킴이 제공하는 컬러로 시작해야 합니다.

QML에 스킴 전환하기 추가

현재 스킴을 시각화하고 상호작용하는 스킴 전환을 활성화하려면, Sidebar.qml 파일에 새로운 항목을 추가하십시오.

FileSystemModule/qml/Sidebar.qml

            // 스킴 전환자를 보여줌
            SidebarEntry {
                icon.source: "../icons/leaf.svg"
                checkable: true

                Layout.alignment: Qt.AlignHCenter
            }
        }

ColorScheme을 표시하기 위해 애플리케이션의 메인 컨텐츠 영역을 업데이트하려면, Sidebar 버튼으로부터 활성화된 인덱스를 확인하는 로직을 수정해야 합니다. 필수 변경사항은 Main.qml 파일로 만들어질 것입니다:

FileSystemModule/Main.qml

                // ScrollView는 파일 내용을 보여주는 TextArea를 포함하고 있음
                StackLayout {
                    currentIndex: sidebar.currentTabIndex > 1 ? 1 : 0

                    SplitView.fillWidth: true
                    SplitView.fillHeight: true
                    // TextArea는 스택 내부의 1번째 요소임
                    ScrollView {
                        Layout.fillWidth: true
                        Layout.fillHeight: true

                        leftPadding: 20
                        topPadding: 20
                        bottomPadding: 20

                        clip: true

                        property alias textArea: textArea

                        MyTextArea {
                            id: textArea
                            text: FileSystemModel.readFile(root.currentFilePath)
                        }
                    }
                    // ColorScheme은 스택 내부의 2번째 요소임
                    ColorScheme {
                        Layout.fillWidth: true
                        Layout.fillHeight: true
                    }
                }

그리고 애플리케이션의 동작을 변경하여 2개의 StackLayouts이 있게 합니다: 하나는 리사이즈 가능한 네비게이션이고, 다른 하나는 컬러 스킴 전환 기능을 표시하는 메인 컨텐츠 영역입니다. 이러한 변경사항 역시 Main.qml 파일로 만들어질 것입니다.

FileSystemModule/Main.qml

                    StackLayout {
                        currentIndex: sidebar.currentTabIndex > 1 ? 1 : sidebar.currentTabIndex

구현을 완료하기 위해 ColorScheme.qml 파일을 만들어야 합니다. 구현은 간단하며 원래 예제의 동일한 원칙을 따릅니다. 불분명한 사항이 있다면 거기에 제공된 문서를 참조하십시오. 모든 컬러와 스킴 이름을 표시하려면 Repeater를 사용하십시오. Repeater의 모델은 scheme_manager.pyfile에 의해 QStringList로 제공됩니다.

FileSystemModule/qml/ColorScheme.qml

        // 한 행 안에 사용되는 모든 컬러를 표시함
        Row {
            anchors.centerIn: parent
            spacing: 10

            Repeater {
                model: Colors.currentColors
                Rectangle {
                    width: 35
                    height: width
                    radius: width / 2
                    color: modelData
                }
            }
        }

코드를 좀 더 자세히 살펴보면, 모델을 가져오는 다양한 방법이 있다는 것을 알게 될 것입니다. getKeys() 메서드는 슬롯으로 정의되므로 호출할 때 괄호가 필요합니다. 반면, currentColors 모델은 프로퍼티로 정의되므로 QML에서 프로퍼티로서 할당됩니다. 이렇게 하는 이유는 컬러 스킴이 전환되었을 때 알림을 받아서 애플리케이션에 표시된 컬러를 업데이트하기 위한 것입니다. 컬러 스킴에 대한 키는 애플리케이션이 시작할 때 한 번만 로드되며 알림에 의존하지 않습니다.

image

데이터 시각화 도구 튜토리얼

이 튜토리얼에서는 Qt for Python의 데이터 시각화 기능에 대해 배울 것입니다. 우선 시각화할 오픈 데이터를 찾습니다. 예를 들면, US Geological Survey 웹사이트에 발표된 최근 1시간 동안 발생한 지진 규모에 대한 데이터가 있습니다. 이 튜토리얼에서 사용할 모든 지진 오픈 데이터를 CSV 포맷으로 다운로드할 수 있습니다.

image

이 튜토리얼의 다음 장에서는 CSV로부터 가져온 데이터를 선 그래프로 시각화하는 방법에 대해 배울 것입니다.

소스를 여기에서 다운로드 할 수 있습니다.

1장 - CSV로부터 데이터 읽어오기

CSV 파일로부터 데이터를 읽어오는 방법은 여러 가지가 있습니다. 다음은 가장 널리 쓰이는 방법입니다:

이번 장에서는 pandas 모듈을 이용하여 CSV 데이터를 읽고 필터링하는 것을 배울 것입니다. 그리고 커맨드-라인 옵션을 통해 데이터 파일을 스크립트로 전달하는 것도 배울 것입니다.

다음 Python 스크립트 main.py에서 방법을 시연합니다:

import argparse
import pandas as pd

def read_data(fname):
    return pd.read_csv(fname)

if __name__ == "__main__":
    options = argparse.ArgumentParser()
    options.add_argument("-f", "--file", type=str, required=True)
    args = options.parse_args()
    data = read_data(args.file)
    print(data)

Python 스크립트는 커맨드 라인으로부터 입력을 받아 파싱하기 위해 argparse 모듈을 사용합니다. 그리고 나서 입력(이 경우 파일이름)을 사용하여 데이터를 읽고 프롬프트에 출력합니다.

다음과 같은 방법으로 스크립트를 실행하여 원하는 출력이 나오는지 확인하십시오:

$python datavisualize1/main.py -f all_hour.csv
                          time   latitude   longitude  depth    ...      magNst     status  locationSource  magSource
0  2019-01-10T12:11:24.810Z  34.128166 -117.775497   4.46    ...         6.0  automatic              ci         ci
1  2019-01-10T12:04:26.320Z  19.443333 -155.615997   0.72    ...         6.0  automatic              hv         hv
2  2019-01-10T11:57:48.980Z  33.322500 -116.393167   4.84    ...        11.0  automatic              ci         ci
3  2019-01-10T11:52:09.490Z  38.835667 -122.836670   1.28    ...         7.0  automatic              nc         nc
4  2019-01-10T11:25:44.854Z  65.108200 -149.370100  20.60    ...         NaN  automatic              ak         ak
5  2019-01-10T11:25:23.786Z  69.151800 -144.497700  10.40    ...         NaN   reviewed              ak         ak
6  2019-01-10T11:16:11.761Z  61.331800 -150.070800  20.10    ...         NaN  automatic              ak         ak

[7 rows x 22 columns]

2장 - 데이터 필터링

이전 장에서, 데이터를 읽고 출력하는 것을 배웠으니 열을 몇 개 선택하고 적절하게 다루어 보겠습니다.

이 2개 열부터 시작하겠습니다: 시간(time), 진도(mag). 이 열들로부터 정보를 얻은 후에, 데이터를 필터링하고 적응시킵니다. 날짜를 Qt 타입으로 양식을 변경시킵니다.

부동소수점 수이기 때문에 Magnitude 열에 대해서는 할 일이 별로 없습니다. 데이터가 올바른지 확인하기 위해 특별히 주의를 기울여야 합니다. 잘못된 데이터 또는 예상치 못한 동작을 막기 위해 "magnitude > 0" 조건을 따르는 데이터를 필터링해야 합니다.

Date 열은 UTC 포맷(예를 들면, 2018-12-11T21:14:44.682Z)으로 데이터를 제공합니다. 그래서 문자열의 구조를 정의하는 QDateTime 객체에 쉽게 매핑할 수 있습니다. 또한 QTimeZone을 사용하여 당신이 속한 시간대를 기준으로 시간을 적응시킬 수 있습니다.

다음 스크립트는 앞에서 설명한 대로 CSV 데이터를 필터링하고 양식을 만듭니다:

import argparse
import pandas as pd

from PySide6.QtCore import QDateTime, QTimeZone

def transform_date(utc, timezone=None):
    utc_fmt = "yyyy-MM-ddTHH:mm:ss.zzzZ"
    new_date = QDateTime().fromString(utc, utc_fmt)
    if timezone:
        new_date.setTimeZone(timezone)
    return new_date

def read_data(fname):
    # CSV 내용 읽어오기
    df = pd.read_csv(fname)

    # 잘못된 진도(magnitude) 제거하기
    df = df.drop(df[df.mag < 0].index)
    magnitudes = df["mag"]

    # 나의 로컬 시간대
    timezone = QTimeZone(b"Europe/Berlin")

    # 나의 시간대로 변환된 timestamp 가져오기
    times = df["time"].apply(lambda x: transform_date(x, timezone))

    return times, magnitudes

if __name__ == "__main__":
    options = argparse.ArgumentParser()
    options.add_argument("-f", "--file", type=str, required=True)
    args = options.parse_args()
    data = read_data(args.file)
    print(data)

QDateTIme과 float 데이터 튜플(tuple)을 갖게 되었으니 출력을 더 개선해 보십시오. 다음 장에서 그 방법을 배울 것입니다.

3장 - 빈 QMainWindow 만들기

이제 데이터를 UI로 표시하는 것을 생각해 볼 수 있습니다. QMainWindow는 메뉴 바, 상태 바 같은 GUI 애플리케이션을 위한 편리한 구조를 제공합니다. 다음 이미지는 QMainWindow가 제공하는 레이아웃을 보여줍니다:

이 경우, 애플리케이션이 QMainWindow에서 상속되도록 하고 다음 UI 요소를 추가하십시오:

image

  • File 다이얼로그를 열기 위한 "File" 메뉴.
  • 창을 닫기 위한 "Exit" 메뉴.
  • 애플리케이션을 시작할 때 상태 바에 표시되는 상태 메시지.

또한 창에 고정된 크기를 정의하거나 현재 사용 중인 해상도를 기준으로 창 크기를 조절할 수 있습니다. 다음 코드에서는 가능한 스크린 너비(80%)와 높이(70%)를 기반으로 창 크기를 정의하는 방법을 볼 것입니다.

주의: QMenuBar, QWidget, QStatusBar 같은 다른 Qt 요소를 이용하여 비슷한 구조를 만들 수 있습니다. 안내를 위해 QMainWindow 레이아웃을 참고하십시오.

from PySide6.QtCore import Slot
from PySide6.QtGui import QAction, QKeySequence
from PySide6.QtWidgets import QMainWindow

class MainWindow(QMainWindow):
    def __init__(self):
        QMainWindow.__init__(self)
        self.setWindowTitle("Eartquakes information")

        # 메뉴
        self.menu = self.menuBar()
        self.file_menu = self.menu.addMenu("File")

        # 종료 QAction
        exit_action = QAction("Exit", self)
        exit_action.setShortcut(QKeySequence.Quit)
        exit_action.triggered.connect(self.close)

        self.file_menu.addAction(exit_action)

        # 상태 바
        self.status = self.statusBar()
        self.status.showMessage("Data loaded and plotted")

        # 창 크기
        geometry = self.screen().availableGeometry()
        self.setFixedSize(geometry.width() * 0.8, geometry.height() * 0.7)

어떤 출력이 나오는지 보려면 스크립트를 실행해 보십시오.

4장 - QTableView 추가하기

이제 QMainWindow가 생겼습니다. 인터페이스에 centralWidget을 포함시킬 수 있습니다. 일반적으로 QWidget은 대부분의 데이터 중심 애플리케이션에서 데이터를 표시하는 데 사용합니다. 데이터를 표시하기 위해 테이블 뷰를 사용하십시오.

처음에는 QTableView만으로 수평 레이아웃을 추가하는 것입니다. QTableView 객체를 만들고 QHBoxLayout 안에 배치합니다. 일단 QWidget이 제대로 만들어지면, 그 객체를 QMainWindow의 중앙 위젯으로 전달합니다.

QTableView는 정보를 표시할 모델이 필요하다는 것을 명심하십시오. 이 경우에는 QAbstractTableModel 인스턴스를 사용할 수 있습니다.

주의: QTableWidget과 함께 제공되는 기본 항목 모델을 대신 사용할 수도 있습니다. QTableWidget은 데이터 모델을 구현할 필요가 없기 때문에 당신의 코드를 상당히 줄여주는 편리한 클래스입니다. 그러나 QTableWidget은 어떤 데이터와도 함께 사용할 수 없기 때문에 QTableView보다는 유연성이 떨어집니다. Qt의 모델-뷰 프레임워크에 대한 통찰을 더 얻고 싶으면 Model View Programming https://doc.qt.io/qt-5/model-view-programming.html 문서를 참고하십시오.

QTableView에 대한 모델을 구현하면 다음을 할 수 있습니다: - 헤더 설정, - 셀 값의 포맷 조작 (현재 예제에는 UTC 시간, 부동 소수점 수가 있음), - 텍스트 정렬과 같은 스타일 프로퍼티 설정, - 셀 또는 셀 컨텐츠에 대한 컬러 프로퍼티 설정.

QAbstractTable의 자식 클래스를 생성하려면 가상 메서드 rowCount(), columnCount(), data()를 재구현해야 합니다. 이렇게 하면 데이터가 제대로 처리되는지 확인할 수 있습니다. 또한 뷰에 헤더 정보를 제공하기 위해 headerData() 메서드를 재구현하십시오.

이것은 CustomTableModel을 구현하는 스크립트입니다.:

from PySide6.QtCore import Qt, QAbstractTableModel, QModelIndex
from PySide6.QtGui import QColor

class CustomTableModel(QAbstractTableModel):
    def __init__(self, data=None):
        QAbstractTableModel.__init__(self)
        self.load_data(data)

    def load_data(self, data):
        self.input_dates = data[0].values
        self.input_magnitudes = data[1].values

        self.column_count = 2
        self.row_count = len(self.input_magnitudes)

    def rowCount(self, parent=QModelIndex()):
        return self.row_count

    def columnCount(self, parent=QModelIndex()):
        return self.column_count

    def headerData(self, section, orientation, role):
        if role != Qt.DisplayRole:
            return None
        if orientation == Qt.Horizontal:
            return ("Date", "Magnitude")[section]
        else:
            return f"{section}"

    def data(self, index, role=Qt.DisplayRole):
        column = index.column()
        row = index.row()

        if role == Qt.DisplayRole:
            if column == 0:
                date = self.input_dates[row].toPython()
                return str(date)[:-3]
            elif column == 1:
                magnitude = self.input_magnitudes[row]
                return f"{magnitude:.2f}"
        elif role == Qt.BackgroundRole:
            return QColor(Qt.white)
        elif role == Qt.TextAlignmentRole:
            return Qt.AlignRight

        return None

이제 QTableView를 가진 QWidget을 만들고, CustomTableModel에 연결시킵니다.

from PySide6.QtWidgets import (QHBoxLayout, QHeaderView, QSizePolicy,
                               QTableView, QWidget)

from table_model import CustomTableModel

class Widget(QWidget):
    def __init__(self, data):
        QWidget.__init__(self)

        # 모델 가져오기
        self.model = CustomTableModel(data)

        # QTableView 생성하기
        self.table_view = QTableView()
        self.table_view.setModel(self.model)

        # QTableView 헤더
        self.horizontal_header = self.table_view.horizontalHeader()
        self.vertical_header = self.table_view.verticalHeader()
        self.horizontal_header.setSectionResizeMode(
                               QHeaderView.ResizeToContents
                               )
        self.vertical_header.setSectionResizeMode(
                             QHeaderView.ResizeToContents
                             )
        self.horizontal_header.setStretchLastSection(True)

        # QWidget 레이아웃
        self.main_layout = QHBoxLayout()
        size = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)

        ## 왼쪽 레이아웃
        size.setHorizontalStretch(1)
        self.table_view.setSizePolicy(size)
        self.main_layout.addWidget(self.table_view)

        # 레이아웃을 QWidget으로 설정
        self.setLayout(self.main_layout)

MainWindow 내부에 Widget을 포함시키기 위해 3장의 main_window.pymain.py를 약간 변경시켜야 합니다.

다음 코드에서 강조된 변경사항을 보게 될 것입니다:

from PySide6.QtCore import Slot
from PySide6.QtGui import QAction, QKeySequence
from PySide6.QtWidgets import QMainWindow

class MainWindow(QMainWindow):
    def __init__(self, widget):
        QMainWindow.__init__(self)
        self.setWindowTitle("Eartquakes information")
        self.setCentralWidget(widget)

        # 메뉴
        self.menu = self.menuBar()
        self.file_menu = self.menu.addMenu("File")

        ## 종료 QAction
        exit_action = QAction("Exit", self)
        exit_action.setShortcut(QKeySequence.Quit)
        exit_action.triggered.connect(self.close)

        self.file_menu.addAction(exit_action)

        # 상태 바
        self.status = self.statusBar()
        self.status.showMessage("Data loaded and plotted")

        # 창 크기
        geometry = self.screen().availableGeometry()
        self.setFixedSize(geometry.width() * 0.8, geometry.height() * 0.7)
import sys
import argparse
import pandas as pd

from PySide6.QtCore import QDateTime, QTimeZone
from PySide6.QtWidgets import QApplication
from main_window import MainWindow
from main_widget import Widget

def transform_date(utc, timezone=None):
    utc_fmt = "yyyy-MM-ddTHH:mm:ss.zzzZ"
    new_date = QDateTime().fromString(utc, utc_fmt)
    if timezone:
        new_date.setTimeZone(timezone)
    return new_date

def read_data(fname):
    # CSV 내용 읽어오기
    df = pd.read_csv(fname)

    # 잘못된 진도(magnitude) 제거하기
    df = df.drop(df[df.mag < 0].index)
    magnitudes = df["mag"]

    # 나의 로컬 시간대
    timezone = QTimeZone(b"Europe/Berlin")

    # 나의 시간대로 변환된 timestamp 가져오기
    times = df["time"].apply(lambda x: transform_date(x, timezone))

    return times, magnitudes

if __name__ == "__main__":
    options = argparse.ArgumentParser()
    options.add_argument("-f", "--file", type=str, required=True)
    args = options.parse_args()
    data = read_data(args.file)

    # Qt Application
    app = QApplication(sys.argv)

    widget = Widget(data)
    window = MainWindow(widget)
    window.show()

    sys.exit(app.exec())

5장 - 차트 뷰 추가하기

테이블은 데이터를 표현하는 데 매우 좋습니다. 하지만 차트는 훨씬 좋습니다. 이를 위해서는 다양한 유형의 플롯과 데이터를 그래픽으로 표현할 수 있는 옵션을 제공하는 QtCharts 모듈이 필요합니다.

플롯을 위한 placeholder는 QChartView입니다. 그리고 이 Widget 내부에 QChart를 배치할 수 있습니다. 처음에는 플롯할 데이터 없이 QChart를 포함시켜 보겠습니다.

QChartView를 추가하여 이전 장의 main_widget.py에 다음의 강조된 변경사항을 만들어 보겠습니다:

from PySide6.QtCore import QDateTime, Qt
from PySide6.QtGui import QPainter
from PySide6.QtWidgets import (QWidget, QHeaderView, QHBoxLayout, QTableView,
                               QSizePolicy)
from PySide6.QtCharts import QChart, QChartView, QLineSeries, QDateTimeAxis, QValueAxis

from table_model import CustomTableModel

class Widget(QWidget):
    def __init__(self, data):
        QWidget.__init__(self)

        # 모델 가져오기
        self.model = CustomTableModel(data)

        # QTableView 생성하기
        self.table_view = QTableView()
        self.table_view.setModel(self.model)

        # QTableView 헤더
        self.horizontal_header = self.table_view.horizontalHeader()
        self.vertical_header = self.table_view.verticalHeader()
        self.horizontal_header.setSectionResizeMode(QHeaderView.ResizeToContents)
        self.vertical_header.setSectionResizeMode(QHeaderView.ResizeToContents)
        self.horizontal_header.setStretchLastSection(True)

        # QChart 생성하기
        self.chart = QChart()
        self.chart.setAnimationOptions(QChart.AllAnimations)

        # QChartView 생성하기
        self.chart_view = QChartView(self.chart)
        self.chart_view.setRenderHint(QPainter.Antialiasing)

        # QWidget 레이아웃
        self.main_layout = QHBoxLayout()
        size = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)

        ## 왼쪽 레이아웃
        size.setHorizontalStretch(1)
        self.table_view.setSizePolicy(size)
        self.main_layout.addWidget(self.table_view)

        ## 오른쪽 레이아웃
        size.setHorizontalStretch(4)
        self.chart_view.setSizePolicy(size)
        self.main_layout.addWidget(self.chart_view)

        # 레이아웃을 QWidget으로 설정
        self.setLayout(self.main_layout)

6장 - ChartView 안에 데이터 Plot하기

이 튜토리얼의 마지막 단계는 QChart 내부에 CSV 데이터를 플롯하는 것입니다. 이를 위해서는 데이터를 검토하고 QLineSeries에 데이터를 포함시켜야 합니다.

데이터를 시리즈에 추가한 후에 X-축에 QDateTime를, Y-축에 진도 값을 적절히 표시하도록 축을 변경할 수 있습니다.

다음은 QLineSeries를 이용하여 데이터를 플롯하기 위한 추가 함수를 포함시킨 업데이트된 main_widget.py입니다.

from PySide6.QtCore import QDateTime, Qt
from PySide6.QtGui import QPainter
from PySide6.QtWidgets import (QWidget, QHeaderView, QHBoxLayout, QTableView,
                               QSizePolicy)
from PySide6.QtCharts import QChart, QChartView, QLineSeries, QDateTimeAxis, QValueAxis

from table_model import CustomTableModel

class Widget(QWidget):
    def __init__(self, data):
        QWidget.__init__(self)

        # 모델 가져오기
        self.model = CustomTableModel(data)

        # QTableView 생성하기
        self.table_view = QTableView()
        self.table_view.setModel(self.model)

        # QTableView 헤더
        resize = QHeaderView.ResizeToContents
        self.horizontal_header = self.table_view.horizontalHeader()
        self.vertical_header = self.table_view.verticalHeader()
        self.horizontal_header.setSectionResizeMode(resize)
        self.vertical_header.setSectionResizeMode(resize)
        self.horizontal_header.setStretchLastSection(True)

        # QChart 생성하기
        self.chart = QChart()
        self.chart.setAnimationOptions(QChart.AllAnimations)
        self.add_series("Magnitude (Column 1)", [0, 1])

        # QChartView 생성하기
        self.chart_view = QChartView(self.chart)
        self.chart_view.setRenderHint(QPainter.Antialiasing)

        # QWidget 레이아웃
        self.main_layout = QHBoxLayout()
        size = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)

        # 왼쪽 레이아웃
        size.setHorizontalStretch(1)
        self.table_view.setSizePolicy(size)
        self.main_layout.addWidget(self.table_view)

        # 오른쪽 레이아웃
        size.setHorizontalStretch(4)
        self.chart_view.setSizePolicy(size)
        self.main_layout.addWidget(self.chart_view)

        # 레이아웃을 QWidget으로 설정
        self.setLayout(self.main_layout)

    def add_series(self, name, columns):
        # QLineSeries 생성하기
        self.series = QLineSeries()
        self.series.setName(name)

        # QLineSeries 채우기
        for i in range(self.model.rowCount()):
            # 데이터 가져오기
            t = self.model.index(i, 0).data()
            date_fmt = "yyyy-MM-dd HH:mm:ss.zzz"

            x = QDateTime().fromString(t, date_fmt).toSecsSinceEpoch()
            y = float(self.model.index(i, 1).data())

            if x > 0 and y > 0:
                self.series.append(x, y)

        self.chart.addSeries(self.series)

        # X-축 설정하기
        self.axis_x = QDateTimeAxis()
        self.axis_x.setTickCount(10)
        self.axis_x.setFormat("dd.MM (h:mm)")
        self.axis_x.setTitleText("Date")
        self.chart.addAxis(self.axis_x, Qt.AlignBottom)
        self.series.attachAxis(self.axis_x)
        # Y-축 설정하기
        self.axis_y = QValueAxis()
        self.axis_y.setTickCount(10)
        self.axis_y.setLabelFormat("%.2f")
        self.axis_y.setTitleText("Magnitude")
        self.chart.addAxis(self.axis_y, Qt.AlignLeft)
        self.series.attachAxis(self.axis_y)

        # QTableView에서 사용하기 위해 QChart로부터 컬러 가져오기
        color_name = self.series.pen().color().name()
        self.model.color = f"{color_name}"

이제 서로 다른 시간에 지진 진도 데이터를 시각화하기 위해 애플리케이션을 실행하십시오.

image

서로 다른 출력을 얻기 위해 소스를 수정해 보십시오. 예를 들어, CSV로부터 더 많은 데이터를 플롯해 보십시오.

경비 계산 도구 튜토리얼

이 튜토리얼에서는 다음 개념을 배우게 될 것입니다:

  • 사용자 인터페이스를 프로그램으로 생성하기,
  • 레이아웃과 위젯,
  • Qt 클래스 오버로딩,
  • 시그널과 슬롯 연결하기,
  • QWidgets으로 상호작용하기,
  • 나만의 애플리케이션 만들기.

요구사항:

  • 애플리케이션을 위한 간단한 창. (QMainWindow)
  • 비용을 추적하기 위한 테이블. (QTableWidget)
  • 비용 정보를 추가하기 위한 2개의 입력 필드. (QLineEdit)
  • 테이블에 정보를 추가하거나, 데이터를 플롯하건, 테이블을 비우거나, 애플리케이션을 종료하기 위한 버튼. (QPushButton)
  • 유효하지 않은 데이터 엔트리를 방지하기 위한 검증 과정.
  • 비용 데이터를 시각화하기 위한 차트. (QChart) 그리고 이 차트는 차트 뷰에 내장시킬 것입니다. (QChartView)

비어 있는 창

QApplication의 기본 구조는 if __name__ == "__main__": 코드 블록 내부에 있습니다.

  if __name__ == "__main__":
      app = QApplication([])
      # ...
      sys.exit(app.exec())

이제 개발을 시작하려면 MainWindow라는 비어 있는 창을 생성하십시오. QMainWindow로부터 상속하는 클래스를 정의해서 이것을 할 수 있습니다.

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Tutorial")

if __name__ == "__main__":
    # Qt 애플리케이션
    app = QApplication(sys.argv)

    window = MainWindow()
    window.resize(800, 600)
    window.show()

    # 애플리케이션 실행
    sys.exit(app.exec())

이제 클래스가 정의되었습니다. 인스턴스를 생성하기 show()를 호출하십시오.

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Tutorial")

if __name__ == "__main__":
    # Qt 애플리케이션
    app = QApplication(sys.argv)

    window = MainWindow()
    window.resize(800, 600)
    window.show()

    # 애플리케이션 종료
    sys.exit(app.exec())

메뉴 바

QMainWindow을 이용하면 몇 가지 기능이 무료로 제공되며 그 중에는 메뉴 바도 있습니다. 메뉴 바를 사용하려면 메서드 menuBar()를 호출하고 MainWindow 클래스 내부를 채워야 합니다.

    def __init__(self):
        super().__init__()
        self.setWindowTitle("Tutorial")

        # 메뉴
        self.menu = self.menuBar()
        self.file_menu = self.menu.addMenu("File")

        # 종류 QAction
        exit_action = self.file_menu.addAction("Exit", self.close)
        exit_action.setShortcut("Ctrl+Q")

이 코드에는 Exit 옵션을 가지고 있는 File 메뉴만 추가했음을 명심하십시오.

Exit 옵션은 애플리케이션을의 종료를 트리거하는 슬롯과 연결되어야 합니다. 여기에 QWidget.close()를 전달하십시오. 마지막 창을 닫은 후에 애플리케이션이 종료됩니다.

비어 있는 위젯과 데이터

QMainWindow를 사용하면 창을 보여줄 때 표시될 중앙 위젯을 설정할 수 있습니다. (참조) 중앙 위젯은 QWidget로부터 파생된 또 다른 클래스가 될 수 있습니다.

또한 나중에 시각화하기 위해 예제 데이터를 정의해야 합니다.

class Widget(QWidget):
    def __init__(self):
        super().__init__()

        # 예제 데이터
        self._data = {"Water": 24.5, "Electricity": 55.1, "Rent": 850.0,
                      "Supermarket": 230.4, "Internet": 29.99, "Bars": 21.85,
                      "Public transportation": 60.0, "Coffee": 22.45, "Restaurants": 120}

Widget 클래스가 있는 상태에서, MainWindow의 초기화 코드를 수정하십시오.

    # QWidget
    widget = Widget()
    # QWidget을 중앙 위젯으로 사용하는 QMainWindow
    window = MainWindow(widget)

창 레이아웃

이제 비어 있는 메인 창이 배치됩니다. 경비 계산 애플리케이션 만들기의 주요 목표를 이루기 위해 위젯들을 추가해야 합니다.

예제 데이터를 선언한 후에, 간단한 QTableWidget에 그것을 시각화할 수 있습니다. 그러기 위해서는 Widget 생성자에 이 프로시저를 추가해야 합니다.

경고: 오직 예제 목적으로 QTableWidget을 사용할 것입니다. 그러나 성능-중시 애플리케이션의 경우라면 모델과 QTableView를 함께 사용하는 것을 권장합니다.

    def __init__(self):
        super().__init__()
        self.items = 0

        # 예제 데이터
        self._data = {"Water": 24.5, "Electricity": 55.1, "Rent": 850.0,
                      "Supermarket": 230.4, "Internet": 29.99, "Bars": 21.85,
                      "Public transportation": 60.0, "Coffee": 22.45, "Restaurants": 120}

        # 왼쪽
        self.table = QTableWidget()
        self.table.setColumnCount(2)
        self.table.setHorizontalHeaderLabels(["Description", "Price"])
        self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)

        # QWidget 레이아웃
        self.layout = QHBoxLayout(self)
        self.layout.addWidget(self.table)

        # 예제 데이터 채우기
        self.fill_table()

보시다시피 위젯을 수평으로 배치하기 위한 컨테이너를 제공하는 QHBoxLayout도 포함하고 있습니다.

또한 QTableWidget은 커스터마이징이 가능합니다. 가령 사용하게 될 2개의 열에 대한 라벨을 추가한다든가, 전체 Widget 공간을 사용하기 위해 컨텐츠를 늘린다든가.

코드 마지막 줄은 table*을 채우는 것을 의미합니다. 그리고 해당 작업을 수행하는 코드는 아래에 나와 있습니다.

    def fill_table(self, data=None):
        data = self._data if not data else data
        for desc, price in data.items():
            self.table.insertRow(self.items)
            self.table.setItem(self.items, 0, QTableWidgetItem(desc))
            self.table.setItem(self.items, 1, QTableWidgetItem(str(price)))
            self.items += 1

이 과정을 별도의 방법으로 수행하는 것이 생성자를 보다 쉽게 읽을 수 있도록 하고, 클래스의 주요 함수를 독립적인 프로세스로 분할하는 좋은 방법입니다.

오른쪽 레이아웃

사용할 데이터가 그저 예제에 불과하기 때문에 테이블에 항목을 입력하기 위한 메커니즘을 추가하고, 테이블의 컨텐츠를 지우고, 애플리케이션을 종료할 수 있는 버튼도 추가해야 합니다.

라벨과 함께 입력 라인을 넣고자 할 때 QFormLayout을 사용하십시오. 그러면 폼 레이아웃이 버튼과 함께 QVBoxLayout으로 끼워넣어집니다.

        # 오른쪽
        self.description = QLineEdit()
        self.description.setClearButtonEnabled(True)
        self.price = QLineEdit()
        self.price.setClearButtonEnabled(True)

        self.add = QPushButton("Add")
        self.clear = QPushButton("Clear")

        form_layout = QFormLayout()
        form_layout.addRow("Description", self.description)
        form_layout.addRow("Price", self.price)
        self.right = QVBoxLayout()
        self.right.addLayout(form_layout)
        self.right.addWidget(self.add)
        self.right.addStretch()
        self.right.addWidget(self.clear)

왼쪽에 테이블을 배치하고 오른쪽에 새로 포함된 위젯을 두는 것은 앞의 예제에서 본 것처럼 메인 QHBoxLayout에 그저 레이아웃을 추가하는 일입니다:

        # QWidget 레이아웃
        self.layout = QHBoxLayout(self)
        self.layout.addWidget(self.table)
        self.layout.addLayout(self.right)

다음 단계는 새로운 버튼과 슬롯을 연결하는 것입니다.

요소 추가하기

각각의 QPushButton은 clicked라는 시그널을 가지고 있습니다. 이 시그널은 버튼을 클릭했을 때 방출됩니다. 이 예제는 이 정도면 충분하지만 공식 문서에서 다른 시그널도 보실 수 있습니다.

        # 시그널과 슬롯
        self.add.clicked.connect(self.add_element)
        self.clear.clicked.connect(self.clear_table)

앞의 예제에서 볼 수 있듯이, 각 clicked 시그널을 서로 다른 슬롯에 연결합니다. 이 예제에서 슬롯은 버튼과 관련된 결정된 작업을 수행하기로 담당한 일반 클래스 메서드입니다. @Slot() 키워드로 각 메서드 선언을 데코레이트하는 것이 매우 중요합니다. 이러한 방식으로 PySide6는 내부적으로 Qt에 함수를 등록하는 방법을 알고 있으며 연결되면 QObjects의 시그널로부터 호출될 수 있습니다.

    @Slot()
    def add_element(self):
        des = self.description.text()
        price = self.price.text()

        self.table.insertRow(self.items)
        self.table.setItem(self.items, 0, QTableWidgetItem(des))
        self.table.setItem(self.items, 1, QTableWidgetItem(price))

        self.description.clear()
        self.price.clear()

        self.items += 1

    def fill_table(self, data=None):
        data = self._data if not data else data
        for desc, price in data.items():
            self.table.insertRow(self.items)
            self.table.setItem(self.items, 0, QTableWidgetItem(desc))
            self.table.setItem(self.items, 1, QTableWidgetItem(str(price)))
            self.items += 1

    @Slot()
    def clear_table(self):
        self.table.setRowCount(0)
        self.items = 0

슬롯은 메서드이므로 QTableWidget과 같은 클래스 변수에 접근하여 상호작용할 수 있습니다.

테이블에 요소를 추가하는 메커니즘은 다음과 같이 설명합니다:

  • 필드로부터 설명 및 가격 가져오기,
  • 테이블에 새로운 빈 행 삽입하기,
  • 각 열 안에서 비어 있는 행에 대한 값을 설정함,
  • 입력 텍스트 필드 비우기,
  • 테이블 행의 전체 개수를 포함시킴.

애플리케이션을 종료하려면 유일한 QApplication 인스턴스의 quit() 메서드를 사용하면 됩니다. 그리고 테이블의 컨텐츠를 비우려면 그저 테이블 행 개수와 내부 개수를 0으로 설정하면 됩니다.

검증 단계

테이블에 정보를 추가하는 것은 유효하지 않은 정보(예. 비어 있는 정보)가 추가되는 것을 막기 위한 검증 단계를 요구하는 중대한 행동이 되어야 합니다.

내부에서 뭔가 바뀔 때마다 QLineEdit으로부터 방출되는 textChanged를 시그널로 사용할 수 있습니다. 예: 매번 발생하는 키 스트토크.

2개의 서로 다른 객체의 시그널을 동일한 슬롯과 연결시킬 수 있습니다. 그리고 현재 애플리케이션이 이 경우에 해당합니다:

        self.description.textChanged.connect(self.check_disable)
        self.price.textChanged.connect(self.check_disable)

check_disable 슬롯의 컨텐츠는 매우 단순합니다:

    @Slot()
    def check_disable(self, s):
        enabled = bool(self.description.text() and self.price.text())
        self.add.setEnabled(enabled)

검증하는 방법은 2가지가 있습니다. 가져온 문자열의 현재 값을 기반으로 검증 코드를 작성하거나, 2개의 QLineEdit의 전체 내용을 수동으로 가져오는 것입니다. 이 경우 2번째 방법이 선호되므로 Add 버튼을 활성화하기 위해 2개의 입력 필드가 비어 있지 않은지 검증할 수 있습니다.

주의: Qt는 어떤 입력이라도 검증하는 데 사용할 수 있는 QValidator라는 특수 클래스도 제공합니다.

비어 있는 차트 뷰

테이블에 새로운 항목을 추가할 수 있습니다. 시각화는 지금까지 괜찮았지만, 데이터를 그래픽으로 표현해서 더 많은 것을 성취할 수 있습니다.

먼저 애플리케이션의 오른쪽에 비어 있는 QChartView placeholder를 포함시키십시오.

        # Chart
        self.chart_view = QChartView()
        self.chart_view.setRenderHint(QPainter.Antialiasing)

또한, 오른쪽 QVBoxLayout에 위젯을 포함시키는 순서도 바뀔 것입니다.

        form_layout = QFormLayout()
        form_layout.addRow("Description", self.description)
        form_layout.addRow("Price", self.price)
        self.right = QVBoxLayout()
        self.right.addLayout(form_layout)
        self.right.addWidget(self.add)
        self.right.addWidget(self.plot)
        self.right.addWidget(self.chart_view)
        self.right.addWidget(self.clear)

Add와 Clear 버튼 사이의 수직 공간을 채우기 위해 self.right.addStretch()라는 코드가 한 줄 있었지만 이제 QChartView가 들어가므로 더 이상 필요하지 않습니다.

또한 on-demand로 하고 싶으면 Plot 버튼도 포함시켜야 합니다.

전체 애플리케이션

마지막 단계에서는 Plot 버튼을 슬롯에 연결해야 합니다. 슬롯은 차트를 생성하고 이것을 QChartView에 포함시키는 역할을 합니다.

        # Signals and Slots
        self.add.clicked.connect(self.add_element)
        self.plot.clicked.connect(self.plot_data)
        self.clear.clicked.connect(self.clear_table)
        self.description.textChanged.connect(self.check_disable)
        self.price.textChanged.connect(self.check_disable)

다른 버튼에 대해서도 이미 해보았기 때문에 새로울 것은 없습니다. 하지만 차트를 생성하고 그것을 QChartView에 포함시키는 것을 잘 보십시오.

    @Slot()
    def plot_data(self):
        # 테이블 정보 가져오기
        series = QPieSeries()
        for i in range(self.table.rowCount()):
            text = self.table.item(i, 0).text()
            number = float(self.table.item(i, 1).text())
            series.append(text, number)

        chart = QChart()
        chart.addSeries(series)
        chart.legend().setAlignment(Qt.AlignLeft)
        self.chart_view.setChart(chart)

다음 단계는 QPieSeries를 채우는 방법을 보여줍니다:

  • QPieSeries 생성하기,
  • 테이블 행 ID 기준으로 반복(iterate),
  • 위치 i에서 항목 가져오기,
  • 시리즈에 값 추가하기.

시리즈에 데이터가 일단 채워지면, 새로운 QChart를 생성하고 그 차트에 시리즈를 추가하고 원할 경우 범례에 대한 정렬을 설정하십시오.

코드의 마지막 라인 self.chart_view.setChart(chart)는 새로 생성된 차트를 QChartView로 가져오는 역할을 합니다.

애플리케이션은 다음과 같이 보일 것입니다:

image

이제 전체 코드를 보십시오:

# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial

import sys
from PySide6.QtCore import Qt, Slot
from PySide6.QtGui import QPainter
from PySide6.QtWidgets import (QApplication, QFormLayout, QHeaderView,
                               QHBoxLayout, QLineEdit, QMainWindow,
                               QPushButton, QTableWidget, QTableWidgetItem,
                               QVBoxLayout, QWidget)
from PySide6.QtCharts import QChartView, QPieSeries, QChart

class Widget(QWidget):
    def __init__(self):
        super().__init__()
        self.items = 0

        # 예제 데이터
        self._data = {"Water": 24.5, "Electricity": 55.1, "Rent": 850.0,
                      "Supermarket": 230.4, "Internet": 29.99, "Bars": 21.85,
                      "Public transportation": 60.0, "Coffee": 22.45, "Restaurants": 120}

        # 왼쪽
        self.table = QTableWidget()
        self.table.setColumnCount(2)
        self.table.setHorizontalHeaderLabels(["Description", "Price"])
        self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)

        # 차트
        self.chart_view = QChartView()
        self.chart_view.setRenderHint(QPainter.Antialiasing)

        # 오른쪽
        self.description = QLineEdit()
        self.description.setClearButtonEnabled(True)
        self.price = QLineEdit()
        self.price.setClearButtonEnabled(True)

        self.add = QPushButton("Add")
        self.clear = QPushButton("Clear")
        self.plot = QPushButton("Plot")

        # 'Add' 버튼 비활성화하기
        self.add.setEnabled(False)

        form_layout = QFormLayout()
        form_layout.addRow("Description", self.description)
        form_layout.addRow("Price", self.price)
        self.right = QVBoxLayout()
        self.right.addLayout(form_layout)
        self.right.addWidget(self.add)
        self.right.addWidget(self.plot)
        self.right.addWidget(self.chart_view)
        self.right.addWidget(self.clear)

        # QWidget 레이아웃
        self.layout = QHBoxLayout(self)
        self.layout.addWidget(self.table)
        self.layout.addLayout(self.right)

        # 시그널 및 슬롯
        self.add.clicked.connect(self.add_element)
        self.plot.clicked.connect(self.plot_data)
        self.clear.clicked.connect(self.clear_table)
        self.description.textChanged.connect(self.check_disable)
        self.price.textChanged.connect(self.check_disable)

        # 예제 데이터 채우기
        self.fill_table()

    @Slot()
    def add_element(self):
        des = self.description.text()
        price = float(self.price.text())

        self.table.insertRow(self.items)
        description_item = QTableWidgetItem(des)
        price_item = QTableWidgetItem(f"{price:.2f}")
        price_item.setTextAlignment(Qt.AlignRight)

        self.table.setItem(self.items, 0, description_item)
        self.table.setItem(self.items, 1, price_item)

        self.description.clear()
        self.price.clear()

        self.items += 1

    @Slot()
    def check_disable(self, s):
        enabled = bool(self.description.text() and self.price.text())
        self.add.setEnabled(enabled)

    @Slot()
    def plot_data(self):
        # 테이블 정보 가져오기
        series = QPieSeries()
        for i in range(self.table.rowCount()):
            text = self.table.item(i, 0).text()
            number = float(self.table.item(i, 1).text())
            series.append(text, number)

        chart = QChart()
        chart.addSeries(series)
        chart.legend().setAlignment(Qt.AlignLeft)
        self.chart_view.setChart(chart)

    def fill_table(self, data=None):
        data = self._data if not data else data
        for desc, price in data.items():
            description_item = QTableWidgetItem(desc)
            price_item = QTableWidgetItem(f"{price:.2f}")
            price_item.setTextAlignment(Qt.AlignRight)
            self.table.insertRow(self.items)
            self.table.setItem(self.items, 0, description_item)
            self.table.setItem(self.items, 1, price_item)
            self.items += 1

    @Slot()
    def clear_table(self):
        self.table.setRowCount(0)
        self.items = 0

class MainWindow(QMainWindow):
    def __init__(self, widget):
        super().__init__()
        self.setWindowTitle("Tutorial")

        # 메뉴
        self.menu = self.menuBar()
        self.file_menu = self.menu.addMenu("File")

        # 종료 QAction
        exit_action = self.file_menu.addAction("Exit", self.close)
        exit_action.setShortcut("Ctrl+Q")

        self.setCentralWidget(widget)

if __name__ == "__main__":
    # Qt 애플리케이션
    app = QApplication(sys.argv)
    # QWidget
    widget = Widget()
    # 중앙 위젯으로 QWidget을 사용하는 QMainWindow
    window = MainWindow(widget)
    window.resize(800, 600)
    window.show()

    # 애플리케이션 실행
    sys.exit(app.exec())

Qt 개요

Qt 개발 주제

Qt는 넓은 분야의 여러 기술을 가지고 있습니다. 다음 주제들은 기능의 핵심 영역이며 Qt를 최대한 활용하는 방법을 배우는 출발점이 될 수 있습니다.

모범 사례

이 페이지는 사용성과 소프트웨어 설계에 탁월한 애플리케이션을 만들기 위해 Qt 기술을 최상으로 사용하는 방법에 대한 지침을 제공합니다.

기능 설명
접근성 장애를 가진 사람들이 애플리케이션에 접근할 수 있게 하는 방법.
데스크탑 통합 사용자 데스크탑 환경과 통합하는 방법.
애플리케이션 아이콘 설정하기 애플리케이션 아이콘을 설정하는 방법.
예외 안전 Qt에서 예외 안전에 대한 가이드.
Qt 플러그인 만드는 법 Qt의 애플리케이션과 기능을 확장하기 위한 플러그인을 생성하기 위한 가이드.
Window의 Geometry 복원하기 창 기하 정보를 저장 & 복원하는 방법.
확장성 다양한 화면 구성 및 UI 컨벤션을 가진 디바이스에서 잘 확장되는 애플리케이션을 개발하는 방법.
세션 관리 Qt에서 세션 관리를 하는 방법.
공유 라이브러리 생성하기 공유 라이브러리를 생성하는 방법.
Calling-Qt-Functions-From-Unix-Signal-Handlers 할 수 없습니다. 그러나 절망하지 마십시오. 방법이 있을 것입니다...
커스텀 Qt 타입 생성하기 Qt로 새로운 타입 생성하고 등록하는 방법.
타이머 애플리케이션에서 Qt 타이머를 사용하는 방법.
Qt D-Bus 어댑터 사용하기 Qt에서 DBus 어댑터를 생성하고 사용하는 방법.
Qt Designer를 위한 컴포넌트 생성하기 및 사용하기 커스텀 위젯 플러그인을 생성하고 사용하는 방법.
좌표 시스템 페인트 시스템에서 사용하는 좌표 시스템에 대한 정보.
Rich Text 처리하기 Qt의 Rich Text 처리, 편집, 표시 기능에 대한 개요.
QML 및 Qt Quick에 대한 모범 사례 QML 및 Qt Quick으로 작업하는 모범 사례를 목록으로 보여줌.
Qt 테스트 튜토리얼 Qt Test로 테스트하는 간략한 소개.

C++ 애플리케이션을 Python으로 포팅하기

Qt for Python은 Python 애플리케이션에서 Qt API를 사용할 수 있게 해줍니다. 그래서 다음 질문은: 기존 C++ 애플리케이션을 포팅할 때 무엇을 해야 합니까? 이것을 이해하기 위해 Qt C++ 애플리케이션을 Python으로 포팅해 보십시오.

시작하기 전에 Qt for Python에 대한 모든 전제조건을 만족해야 합니다. 더 많은 정보를 보려면 시작하기를 보십시오. 추가로 C++과 Python에서 Qt의 기본 차이점에 대해 익숙해지십시오.

기본 차이점

이 섹션에서는 C++과 Python 간의 기본 차이점의 일부를 보여주고, Qt가 두 문맥 간에 어떻게 다른지 보여드리겠습니다.

C++ vs Python

  • 코드 재사용 관점에서 볼 때, C++과 Python 모두 하나의 코드 파일이 다른 언어 환경에서 쉽게 사용할 수 있는 기능을 제공합니다. C++에서 재사용된 코드의 API 정의에 접근하려면 #include 지시어를 사용하면 됩니다. 이것은 Python에서 import 구문과 같습니다.
  • C++ 클래스의 생성자는 클래스 이름과 같으며 실행하기 전에 (정의한 순서대로) 모든 베이스 클래스의 생성자를 자동으로 호출합니다. Python에서 __init__() 메서드가 클래스의 생성자이며 어떤 순서로든 베이스 클래스의 생성자를 명시적으로 호출할 수 있습니다.
  • C++에서는 현재 객체를 암묵적으로 참조할 때 this 키워드를 사용합니다. Python에서는 클래스의 각 인스턴스 메서드의 1번째 파라미터로 현재 객체를 명시적으로 언급해야 합니다; 관례적으로 그 이름을 self라고 합니다.
  • 더 중요한 것은 Python의 경우 {}와 ; 기호를 잊어 버리세요.
  • 전역 변수가 필요한 경우에만 global 키워드를 변수 정의 앞에 붙이십시오.
var = None
def func(key, value = None):
  """키와 옵션 값을 사용합니다.

  만약 값을 생략하거나 None인 경우, func()의 마지막 호출의 값을 재사용합니다.
  """
  global var
  if value is None:
      if var is None:
          raise ValueError("Must pass a value on first call", key, value)
      value = var
  else:
      var = value
  doStuff(key, value)

이 예제에서 func()var를 global 구문이 없는 로컬 이름으로 취급할 것입니다. var에 접근할 때 value is None 처리 과정에서 NameError이 발생할 것입니다. 더 많은 정보는 Python 레퍼런스 문서를 보십시오.

팁: Python은 인터프리터 언어이기 때문에, 아이디어를 실현해 보는 흔하고 쉬운 방법은 인터프리터 안에서 하는 것입니다. Python에서 빌트인 함수나 키워드에 대해 인터프리터에서 help() 함수를 호출해 보십시오. 예를 들어, help(import)를 호출하면 import 구문에 대한 문서를 제공해 줄 것입니다.

마지막으로 Python 코딩 스타일에 익숙해지도록 몇 가지 예제를 따라해 보고 PEP8 - 스타일 가이드에 설명된 가이드라인을 따르십시오.

import sys

from PySide6.QtWidgets import QApplication, QLabel

app = QApplication(sys.argv)
label = QLabel("Hello World")
label.show()
sys.exit(app.exec())

주의: Qt는 애플리케이션별 요구사항에 따라 요구사항을 관리하기 위한 클래스를 제공합니다. 콘솔 전용이면 QCoreApplication, QtWidgets이 포함된 GUI이면 QApplication, QtWidgets이 없는 GUI는 QGuiApplication. 이 클래스들은 애플리케이션이 요구하는 GUI 라이브러리와 같은 필수 플러그인을 로드합니다. 이 경우 애플리케이션이 QtWidgets가 있는 GUI를 갖고 있으므로 QApplication이 먼저 초기화됩니다.

C++과 Python 문맥에서의 Qt

Qt는 C++이나 Python 애플리케이션에 상관없이 동일하게 동작합니다. C++과 Python이 서로 다른 의미론을 사용한다는 것을 고려한다면 두 Qt의 변형 간의 차이점은 불가피합니다. 여기에 주의해야 할 몇 가지 중요한 사항이 있습니다:

  • Qt 프로퍼티: Q_PROPERTY 매크로는 C++에서 getter와 setter 함수를 가진 public 멤버 변수를 추가할 때 사용합니다. 이에 대한 Python의 대안은 getter와 setter 함수 정의 앞에 @property 데코레이터를 두는 것입니다.
  • Qt 시그널과 슬롯: Qt는 이벤트 발생을 알리기 위해 시그널을 방출하는 독특한 콜백 메커니즘을 제공합니다. 그래서 이 시그널에 연결된 슬롯들이 이에 반응할 수 있습니다. C++의 경우 클래스 정의는 반드시 public Q_SLOTS: 접근 지정자 아래에 슬롯을 정의하고 Q_SIGNALS: 접근 지정자 아래에 시그널을 정의해야 합니다. QObject::connect() 함수의 여러 변형 중 하나를 사용하여 이 둘을 연결하십시오. Python에서 이에 대응하는 것은 함수 정의 직전에 나오는 @Slot 데코레이터입니다. 이것은 슬롯을 QtMetaObject에 등록하는 데 필요합니다.
  • QString, QVariant, 그 외 타입
    • Qt for Python은 QString과 QVariant에 대한 접근을 제공하지 않습니다. 대신 Python의 네이티브 타입을 사용해야 합니다.
    • QChar와 QStringRef는 Python string이 대신하고, QStringList는 string의 list로 변환됩니다.
    • QDate, QDateTime, QTime, QUrl의 hash() 메서드는 문자열 표현식을 리턴합니다. 그래서 동일한 날짜(그리고 날짜/시간, 시간, URL)에 대해서 동알한 해시 값을 갖습니다.
    • QTextStream의 bin(), hex(), oct() 함수는 각각 bin_(), hex_(), oct_()로 이름이 바뀌었습니다. 이것은 Python의 빌트인 함수와 이름이 충돌하지 않기 위해서입니다.
  • QByteArray: QByteArray는 인코딩하지 않은 byte의 list로 취급합니다. Python 3는 "bytes"를 사용합니다. QString은 사람을 읽을 수 있는 인코딩된 string이며 "str"을 의미합니다.

다음은 이러한 차이점 일부를 보여주는 향상된 버전의 Hello World 예제입니다:

import sys
import random

from PySide6.QtWidgets import (QApplication, QLabel,
     QPushButton, QVBoxLayout, QWidget)
from PySide6.QtCore import Qt, Slot

class MyWidget(QWidget):
    def __init__(self):
        super().__init__()

        self.hello = ["Hallo Welt", "Hei maailma", "Hola Mundo", "Привет мир"]

        self.button = QPushButton("Click me!")
        self.text = QLabel("Hello World")
        self.text.setAlignment(Qt.AlignCenter)

        self.layout = QVBoxLayout()
        self.layout.addWidget(self.text)
        self.layout.addWidget(self.button)
        self.setLayout(self.layout)

        self.button.clicked.connect(self.magic)

    @Slot()
    def magic(self):
        self.text.setText(random.choice(self.hello))

if __name__ == "__main__":
    app = QApplication(sys.argv)

    widget = MyWidget()
    widget.resize(800, 600)
    widget.show()

    sys.exit(app.exec())

주의: if 블록은 Python 애플리케이션을 개발할 때 좋은 방법일 뿐입니다. 다른 파일에서 이 Python 파일을 모듈로 가져오는지, 혹은 직접 실행하는지에 따라 다르게 작동합니다. __name__ 변수는 이 2가지 시나리오에서 서로 다른 값을 갖게 됩니다. 파일을 직접 실행할 경우 그 값은 __main__이 되고, 모듈로 가져와서 실행하게 되면 모듈의 파일 이름(이 경우 hello_world_ex)이 됩니다. 후자의 경우, if 블록 외에 모듈에서 정의한 것은 import하는 파일에서 모두 이용가능합니다.

여기에서 QPushButton의 clicked 시그널은 magic 함수에 연결되는데 이것은 QLabel의 text 프로퍼티를 임의로 변경합니다. @Slot 데코레이터는 이 메서드가 슬롯이라는 것을 표시하고 QtMetaObject에게 슬롯을 알려줍니다.

Qt C++ 예제 포팅하기

Qt는 C++의 특징을 보여주고 초보자의 학습을 돕기 위해 몇 가지 C++ 예제를 제공합니다. 이 C++ 예제들 중 하나를 골라 Python으로 포팅해 보실 수 있습니다. books SQL example은 Python에서 UI 관련 코드를 작성하지 않을 수 있으므로 좋은 시작점이 될 수 있습니다. 하지만 대신 .ui 파일을 사용할 수 있습니다.

다음 장은 포팅 과정을 통해 가이드 해줄 것입니다:

1장: initDb.h에서 createDb.py로 2장: bookdelegate.cpp에서 bookdelegate.py로 3장: Port bookdwindow.cpp에서 bookwindow.py로

PySide6 애플리케이션의 C++ 확장 디버그하는 방법

링크

예제

Qt for Python이 제공하는 예제 모음은 새로운 사용자가 모듈의 다양한 유저 케이스를 이해하는 데 도움을 줍니다.

모든 예제는 examples 디렉토리의 pyside-setup 리포지토리 내부에서 모두 찾을 수 있습니다.

링크

비디오

링크

배포

링크

고려사항

링크

About

Qt for Python

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published