import sys
import time

from PySide2.QtCore import *  # Signal()
from PySide2.QtGui import *
from PySide2.QtWidgets import *  # QMainWindow, QWidget, QGridLayout

#qthread 에러 없이 종료하기
class UserButton(QPushButton):

    def __init__(self):
        super(UserButton, self).__init__()
        self._prop = 'false'


        with open('test3.css', encoding='utf-8') as f:
            self.setStyleSheet(f.read())

    def getter(self):
        return self._prop

    def setter(self, val):
        if self._prop == val:
            return
        self._prop = val
        self.style().polish(self)

    prop = Property(str, fget=getter, fset=setter)


class intervalThread(QThread):
    def __init__(self, b1:UserButton, b2:UserButton):
        super(intervalThread,self).__init__()
        self.working = True
        self.b1 = b1
        self.b2 = b2

    def run(self):
        while self.working:
            if self.b1.prop == 'true':
                self.b1.prop = 'false'
            else:
                self.b1.prop = 'true'
            print(self.b1.prop)
            self.sleep(1)

    def stop(self):
        self.working = False
        self.quit()
        self.wait(5000) #5000ms = 5s

class MainWindow(QWidget):
    def __init__(self):
        super(MainWindow, self).__init__()
        # QWidget.__init__(self)

        self.layout = QGridLayout()
        self.setLayout(self.layout)

        self.button1 = UserButton()
        self.button2 = UserButton()

        self.layout.addWidget(self.button1, 0, 0, 1, 1)
        self.layout.addWidget(self.button2, 0, 1, 1, 1)

        self.thread = intervalThread(self.button1, self.button2)
        self.thread.start()

    def closeEvent(self, e):
        self.hide()
        self.thread.stop()

    def keyReleaseEvent(self, e):
        if e.key() == Qt.Key_Escape:
            self.close()

        if e.key() == Qt.Key_S:
            self.thread.working=True
            self.thread.start()
            print("S")

        if e.key() == Qt.Key_P:
            self.thread.working=False
            print("P")

app = QApplication(sys.argv)
lf = MainWindow()
lf.show()
sys.exit(app.exec_())

 

Thread가 종료되기 전에 main Loop가 종료될 경우 에러코드를 리턴하며 프로그램이 종료된다.

이를 해결하기 위해 mainLoop가 종료되기 전에 Thread를 종료시키고 Thread 종료가 완료될 때까지 대기시키면 된다.

 

MainWindow의 closeEvent와 thread class 내의 사용자함수인 stop() 안에 있는 quit(), wait()기능을 이용해서 말이다.

 

대신 문제가 wait()의 deadline을 설정해주지 않으면 제대로 동작을 하지 않는 경우가 발생한다.

 

그래서 나는 5000ms(=5s)로 deadline을 설정해놓았다.

 

이렇게 하면 thread가 종료되기 전에 closeEvent 발생으로 인해 mainLoop가 종료되지 않아 에러코드 없이 0을 리턴하면서 프로그램이 종료하게 된다.

pyside2의 기능 중 하나인 property값과 css를 이용하여 동적으로 변하는 버튼을 만들 수 있다.

 

test3.py

더보기
import sys
import time

from PySide2.QtCore import *  # Signal()
from PySide2.QtGui import *
from PySide2.QtWidgets import *  # QMainWindow, QWidget, QGridLayout

#qthread 에러 없이 종료하기
class UserButton(QPushButton):

    def __init__(self):
        super(UserButton, self).__init__()
        self._prop = 'false'


        with open('test3.css', encoding='utf-8') as f:
            self.setStyleSheet(f.read())

    def getter(self):
        return self._prop

    def setter(self, val):
        if self._prop == val:
            return
        self._prop = val
        self.style().polish(self)

    prop = Property(str, fget=getter, fset=setter)


class intervalThread(QThread):
    def __init__(self, b1:UserButton, b2:UserButton):
        super(intervalThread,self).__init__()
        self.working = True
        self.b1 = b1
        self.b2 = b2

    def run(self):
        while self.working:
            if self.b1.prop == 'true':
                self.b1.prop = 'false'
            else:
                self.b1.prop = 'true'
            print(self.b1.prop)
            time.sleep(1)

    def stop(self):
        self.working = False

class MainWindow(QWidget):
    def __init__(self):
        super(MainWindow, self).__init__()
        # QWidget.__init__(self)

        self.layout = QGridLayout()
        self.setLayout(self.layout)

        self.button1 = UserButton()
        self.button2 = UserButton()

        self.layout.addWidget(self.button1, 0, 0, 1, 1)
        self.layout.addWidget(self.button2, 0, 1, 1, 1)

        self.thread = intervalThread(self.button1, self.button2)
        self.thread.finished.connect(self.stopSig)
        self.thread.start()

    def stopSig(self):
        self.close()

    def keyReleaseEvent(self, e):
        if e.key() == Qt.Key_Escape:
            self.thread.stop()

        if e.key() == Qt.Key_S:
            self.thread.working=True
            self.thread.start()
            print("S")

        if e.key() == Qt.Key_P:
            self.thread.working=False
            print("P")

app = QApplication(sys.argv)
lf = MainWindow()
lf.show()
sys.exit(app.exec_())

test3.css

더보기

QPushButton{
width : 50px;
height : 50px;
}

QPushButton[prop='true']{
background-color : rgb(0,255,0);
}

QPushButton[prop='false']{
background-color : rgb(255,0,0);
}

QPushButton:hover[prop='true']{
background-color : rgb(0,120,0);
}


QPushButton:hover[prop='false']{
background-color : rgb(120,0,0);
}

일단 전체 코드 먼저 올려놓고 하나씩 보겠다.

 


test3.py 내용 분석

class UserButton(QPushButton):

    def __init__(self):
        super(UserButton, self).__init__()
        self._prop = 'false'


        with open('test3.css', encoding='utf-8') as f:
            self.setStyleSheet(f.read())

    def getter(self):
        return self._prop

    def setter(self, val):
        if self._prop == val:
            return
        self._prop = val
        self.style().polish(self)

    prop = Property(str, fget=getter, fset=setter)
  

먼저 사용자 버튼이다. pyside(pyqt도 마찬가지)에는 QPushButton이 있는데 QPushButton의 기본 속성말고 추가적으로 내가 사용하고 싶은 속성 정보를 추가할 것이다.

속성의 이름은 'prop'이다.

 

먼저 사용자 버튼을 생성하면 내부 변수로 _prop를 생성하고 css파일을 불러와 StyleSheet를 설정한다.

 

test3.css 내용을 보면 알겠지만, 속성값 정보에 따라서 나타나는 동작을 다르게 해놨다.

그리고 이를 갱신하는 경우는 setter 함수가 호출되었을 때, _prop의 값이 변경되었을 때이다. 이 때 self.style().polish(self)를 해주면 style이 업데이트가 된다.

 

사실 처음에는 hover처럼 pseudo class를 사용자가 직접 생성해서 사용할 수 있는 방법이 없나 알아봤는데, 만드는 방법을 못찾은건지, 없는건지 모르겠어서(stack overflow에서 못만든다고 적혀있는 댓글을 보긴 했다.) 이렇게 속성값을 값이 변화할 때 갱신해주는 형식으로 변경했다.

 

class intervalThread(QThread):
    def __init__(self, b1:UserButton, b2:UserButton):
        super(intervalThread,self).__init__()
        self.working = True
        self.b1 = b1
        self.b2 = b2

    def run(self):
        while self.working:
            if self.b1.prop == 'true':
                self.b1.prop = 'false'
            else:
                self.b1.prop = 'true'
            print(self.b1.prop)
            time.sleep(1)

    def stop(self):
        self.working = False

그리고 테스트를 위해 Thread를 하나 생성해줬고 말이다. 이놈은 외부에서 속성값을 1초에 한번씩 'true', 'false'로 바꿔준다.

 

정적 타입 선언의 목적

* 자동 완성 200프로 사용하기

* Type Checker 손쉽게 사용하기!!!

 

여러가지 IDE를 이용하여 개발을 진행할 때 참 도움이 많이 되는 기능 중 하나가 바로 자동완성 기능이다.

 

그 중 python 개발에서 가장 유명한 건 바로 pycharm이 아닐까 싶은데......(내 생각 ㅋㅋ)

 

빠르게 객체에 접근하기 위해 Dict 타입을 이용하여 객체들을 관리하다보면 Dict에 들어간 타입이 뭔지 pycharm이 모를 때가 많다.

 

이럴 때 객체의 Class는 보통 대문자로 쓰고, 소문자로 선언을 해주면 자동으로 mapping을 해주기는 하는데....

 

더욱 확실한 방법이 있다.

 

바로 Dict 내부에 어떤 타입의 객체가 들어갈지 결정해줄 수 있는 것!

 

self.parkingDict이라는 Dictionary 객체를 선언하고, 이놈의 value에는 내가 만든 객체가 들어가있다.

 

그리고 그 Class명은 Hanger이다.

 

그래서 이렇게 hanger로 변수명을 설정해주면 자동완성 기능을 사용할 수 있다.

 

그런데 만약에 이걸 home으로 하면?? (Home이라는 Class명도 있다.)

 

요로코롬 Home Class에 있는 변수들이 나오게 된다.

 

하지만!!! 이마저도 무시할 수 있게 확실하게 변수를 정의하고 싶다!!!(이게 참 c 개발할 때는 전혀 문제가 안되던건데,,,, 타입을 동적으로 할당하는 python의 특징이 이런 문제를 야기한다..ㅋㅋ)

 

이와 같이 self.parkingDict : Dict[int, Hanger]

 

정적 변수다! 라고 정의를 해주고 사용을 하면 이렇게 알아서 인식하고 동작한다.

 

다만 이런 정적 변수 선언은 Python 3.6 이상부터 잘 동작한다고 한다.

진짜 겁나 간단하다.

 

https://wiki.python.org/moin/PyQt/Making%20non-clickable%20widgets%20clickable

 

PyQt/Making non-clickable widgets clickable - Python Wiki

On the #pyqt channel on Freenode, xh asked if it was possible to make QLabel objects clickable without subclassing. There are two ways to do this: Use event filters. Assign new methods to the labels. These are shown below. Event filters The following examp

wiki.python.org

 

관련 문서는 바로 위 링크에 있다.

 

def clickable(widget):

    class Filter(QObject):
    
        clicked = pyqtSignal()	#pyside2 사용자는 pyqtSignal() -> Signal()로 변경
        
        def eventFilter(self, obj, event):
        
            if obj == widget:
                if event.type() == QEvent.MouseButtonRelease:
                    if obj.rect().contains(event.pos()):
                        self.clicked.emit()
                        # The developer can opt for .emit(obj) to get the object within the slot.
                        return True
            
            return False
    
    filter = Filter(widget)
    widget.installEventFilter(filter)
    return filter.clicked

위의 코드를 내가 사용하고 싶은 곳에 복붙을 한다.

 

clickable(label1).connect(self.showText1)

clickable('내가 이벤트를 발생시키고 싶은 객체').connect('발생시킬 이벤트 함수')

 

이런 식으로 작성해주면 아주 잘 작동한다.

-> 단순히 라벨뿐만 아니라 프레임, widget 등 QObject 객체라면 모두 사용이 가능하다.

 

connect 함수에서 파라미터를 보내고 싶으면 lambda 함수를 사용하면 되고 ㅎㅎ

 

위의 코드를 보면 clicked라는 이름의 시그널을 생성하고, 이벤트 중 마우스 클릭 이벤트가 발생하면, 해당 이벤트 발생 위치를 분석해서 그 위치가 오브젝트의 사각형 안에 들어갈 경우 click signal을 emit해주는 것.

 

한마디로 QEvent.mousebuttonRelease() 함수만 다른걸로 바꿔주면 여러가지 이벤트를 다양하게 생성할 수 있다는 의미이다.

 

개 꿀

 

 

원본

더보기

서론

pyside2를 이용하여 windows에서 Gui 환경을 적용한 plc 모니터링 시스템을 만드는 중이다.

 

그런데 객체 지향 프로그래밍에 대한 기초가 잘 안잡혀 있어서 구조를 짜고 구현하는데 너무 애를 많이 먹었다...

 

하지만 결국 해결했다 ㅎ


문제

내가 가지고 있었던 문제는 상속 관계에 있었다.

 

프로젝트의 구조는 이러하다.

 

main.py -> monitor.py -> gui.py

                              -> communication.py


1. main.py에서 monitor 객체를 생성한다.

 

2. monitor class __init__()에서 Ui_monitor 객체를 생성하고 Ui_monitor.setupUi()라는 함수를 호출한다.

(pyqt designer를 이용해 짠 Ui파일을 불러오는 작업이다.)

 

이제 gui.py communication.py를 이용하여 Gui 이미지 생성 및 갱신하는 부분과 통신 부분을 구현할 생각이었다.

 

그런데 왠열.... 처음에는 mro Error(찾아보니 다이아몬드 형식으로 상속을 했을 때 발생하는 오류라고 한다.)가 발생하고, 이를 해결하니 Gui 창이 안바뀌고....

 

문제의 원인은 2가지였다.

1. 클래스 객체와 인스턴스 객체

2. super().__init__()

 

 1. 인스턴스 객체는 인스턴스... 즉, class를 이용하여 인스턴스를 생성하면 각 인스턴스마다 가지는 객체이다.

그래서 python3 부터 self.~~라는 변수명을 볼 수 있는데 이게 바로 인스턴스 객체이다.

 

하지만 내 ui객체(ui = Ui_monitor())는 단 하나만 존재해야 한다. 그래서 인스턴스 객체가 아닌 클래스 객체로 선언을 해줘야 한다. 어떤 Monitor 객체가 생성되도 내가 관리할 Gui 창은 하나이기 때문이다.

 

2. 문제는 pyside의 setupUi() 함수이다. setupUi()는 qtdesigner에서 만든 다양한 위젯들을 만들어주는 함수이다.

이놈은 자기 자신을 인자로 받아 처리하기 때문에 생성자에 선언하게 된다.

즉, 한 번 생성이 되고, 다시 생성이 될 경우(ex. 자식 객체가 super().__init__을 한 경우 등) main.py에서 show()를 통해 이미 출력되어 있는 창이 아닌 다른 인스턴스 객체들이 생성이 되어버린다.

 

그렇게 생성된 다른 인스턴스를 이용하여 값을 변경 시키려고 해봤자... 현재 떠있는 창에는 아무런 반응이 없을 수 밖에...ㅎ

 

즉, 클래스 변수로 객체를 선언하고, 자식 클래스에서 __init__을 통한 부모 객체를 추가적으로 생성시키지 않게 하면서 일단 문제를 해결하였다...


그런데 더 좋은 구조가 있는데 아직 부족해서 내가 못찾고 있는 게 아닌가 하는 생각이 든다.

뭐랄까.... 깔끔하지가 않다... 찜찜한 이느낌..ㅋ

 

수정

왠지 너무 찝찝해서 다시 열심히 조사를 하고 드디어 (일단) 가장 만족스러운 구조를 찾았다.

 

기본적인 생각은 동일하다.

 

GUI와 Serial 통신을 나누는 것....

 

class는 크게 2가지이다. monitor classsignalThread class

 

기본적인 상속 구조는 이런식이다.

Ui_monitor, QMainWindow(pyside2 class) -> monitor class

QThread -> signalThread class

 

그리고 내가 최대한 신경쓴 singalThread class에서 serial 통신을 이용하여 값을 받아 왔을 때 그 결과를 GUI에 어떻게 반영할 것인가이다.

 


01. 통신 결과를 GUI에 반영하기

communication.py

5 line : QThread Class를 상속받는다.

6 line : Pyside2.QtCore.Signal() 함수를 이용하여 사용자 Singal을 설정할 수 있다. Signal의 경우 Class 변수로 선언하고, Singal의 인자로 반환할 타입을 입력하면 데이터를 반환할 수 있다.

현재는 dic(dictionary) 타입을 넘겨주기 위해 작성하였다. 

8~14 line : QThread의 run() 함수를 오버라이딩한다. 실제 통신 결과를 data에 넣게 된다. 

10 line : with threading.Lock() Serial 통신이 동기적으로 움직일 수 있도록 구현하였다.

13 line : emit(data) 함수를 이용하여 data를 나중에 사용될 slot에 넘겨주면서 signal을 발생시키도록 하였다. 일종의 callback() 함수와 동일하게 동작한다고 보면 된다. serial 통신이 모두 완료가 되면 signal을 발생시키고, 그 signal과 연결된 slot을 수행하여 GUI를 수정할 것이다.

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Monitor(QMainWindow, Ui_monitor):
 
    def __init__(self):
        super(Monitor, self).__init__()
        self.setupUi(self)
 
        self.sig = signalThread()
        self.sig.start()
 
        #통신을 통해 한 스캔을 완료했을 경우 발생하는 signal이 finished이고, 해당 시그널이 발생하면 통신 결과를 Gui에 바로 반영할 수 있도록 update_info()를 호출
        #그리고 update_info()의 인자로 받아온 값들을 통해 gui에 바로 업데이트를 할 수 있다.
        self.sig.finished.connect(self.update_info)
 
        #click을 눌렀을 경우 데이터를 communication.py에 보내기
        data = {}
        data['send'= 'Good'
        self.buttonIn.clicked.connect(lambda: self.sig.sendSignal(data))
 
    @Slot(dict)
    def update_info(self, data):
        try:
            print('Update Info', data)
        except Exception as e:
            print(e)
            pass
cs

18~19 line : signalThread() 객체를 생성한 후 run Thread를 실행시켜주는 start()를 실행한다.

23 line : finished Signal을 self.update_info Slot과 연결시킨다.

31 line : finished 신호가 오면 그 결과를 Gui에 반영하는 함수이다. set~~가 여러개 들어갈 예정이다.

 

이렇게 되면 serial 통신은 signalThread()에서만 수행하게 되고 GUI 결과는 Monitor()에서 처리를 할 수 있게 된다.

 


02. 사용자 입력값(GUI에서 조작한 값)을 serial 통신으로 보내기

 

signalThread class

23
24
25
26
27
28
29
30
 
    @Slot()
    def sendSignal(self, data):
        with threading.Lock():
            print("Send: ", data)
 
 
 
 
 
 
cs

signalThread에 slot을 하나 생성해주었다. 이 slot은 monitor class에서 버튼을 클릭했을 때 호출이 될 예정이다.

 

monitor class

25
26
27
28
29
30
31
32
33
34
35
36
        #click을 눌렀을 경우 데이터를 communication.py에 보내기
        data = {}
        data['send'= 'Good'
        self.buttonIn.clicked.connect(lambda: self.sig.sendSignal(data))
 
    @Slot(dict)
    def update_info(self, data):
        try:
            print('Update Info', data)
        except Exception as e:
            print(e)
            pass
cs

26~28 line : 버튼을 lambda 함수를 이용하여 인자를 넘기면서 호출해주면 연결 끝이다.

configparser로 *.ini 파일이나 *.cfg 파일과 같은 어떤 프로그램의 속성을 저장해놓는 파일을 관리할 수 있다.

 

그런데.... 아니 별것도 아닌게 지금 개발 중인 모니터링 프로그램에서 사용하려고 했더니만 안되는 것이 아닌가..

 

 

그래서 지금 퇴근길... 그것도 지하철 안에서 운좋게 앉아서 test파일을 만들고 사용해봤다..

 

응? 잘 되네??

 

뭐지 ㅋㅋㅋㅋ

 

근데 이놈 좀 웃긴게 있다...

외부 파일을 가져오기 위해 생성한 객체를 이용해 read를 하는데 path를 확인을 안한다......

 

즉, 저 'config.ini'가 잘못된 path라도 저기서 안걸리고 일단 통과된다 ㅋㅋㅋ

맵소사

파일도 없는데 그 다음 코드로 넘어가 key가 없다는 에러를 내뿜는다 ㅋ

 

포스팅을 한 김에 configparser 사용법을 몇가지 적어나 보자

 

● 설치

$ pip install configparser

● 선언

import configparser

config = configparser.ConfigParser()

 

● 파일 읽어오기

config.read([파일명])
config.read('config.ini', encoding='utf-8')	#한글이 들어갈 경우 인코딩 값 역시 설정을 해줘야 오류가 안난다~

※ 파일명이 잘못되도 에러를 반환하지 않는다..ㅋ

 

● 파일 저장

with open('example.ini', 'w') as configfile:
    config.write(configfile)

with ~ as ~는 파일을 열어 처리를 하거나 lock()을 쓸 때 매우 유용하다. 알아서 파일을 닫아주고, release해준다고 한다 ㅎㅎ

 

● 기타

#읽기
>>> config['SECTION']['KEY']

#쓰기
>>> config['SECTION']['KEY'] = value

#section 확인
>>> config.sections()
<Section: SECTION>

오류 확인

왼쪽에 있는 이미지가 현재 프로젝트의 tree구조이고, community.py에서 configparser를 사용하려고 한다.

 

그래서 read의 path를 config.ini로 했는데 sections()함수를 이용하여 key값을 확인해봤더니 텅텅 비어있는 것이 아니겠는가...?

 

왜그런가 봤더니... 가장 최초로 프로그램이 시작되는 main.py쪽에 config.ini 파일이 있는것...ㅋㅋ

 

그랬던 것이다. path의 기준은 처음 python 프로그램을 시작한 부분이 현재 작업 위치인 것이다.

 

즉, 이를 위해서는 절대 경로를 지정해주거나 작업 영역 기준으로 상대 경로를 지정하거나~ 작업 영역을 바꿔주면 된다.

 

 작업 경로를 바꾸면 문제가 발생할 수 있으니 현재 작업 영역을 기준으로 config.ini 작업 영역을 설정해주자~

 

p.s. 하루 자고 일어나면 보이는게 퇴근 시간에는 절대 안보이더라 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

pyqt를 이용하여 모니터 프로그램을 만들고 있다.

 

그런데 문제가 발생했다.

 

thread의 timer를 이용하여 일정 주기마다 통신을 수행하도록 코드를 구현했는데,

qt가 예기치 않게 종료되는 과정에서 상대적으로 느린 통신이 중간에 끊어지면서 Runtime Error를 토해내는 것이 아닌가..

 

(와 캡쳐하려고 하니깐 또 안나네 ㅋㅋ)

 

갑자기 안난다..

이 문제가 Lock()을 통해 해결했는지 확인할 수 있는 길이 없다.

 

일단 Lock() 해놓고 에러가 발생하는지 안하는지 계속 확인해봐야겠다.

서론

만들어보자

 

만들자~

일단 대충 이런 폼을 하나 만들었다.

그리고 "여기"에 저것들을 왕왕 넣을 것이다.

이런 식으로 말이다.

 

이렇게 하기 위해서는 main.py에서 여러가지 작업을 해줘야한다.

 

1. class 생성

2. layout생성

3. 반복 생성 함수 만들기

 

 

1번 먼저 해보자

from ui_rightCar import Ui_rightCar

class rightCar(QFrame, Ui_rightCar):
    def __init__(self, parent=None):
        super(rightCar, self).__init__(parent)
        self.setupUi(self)

이 세상 간단함이 아니다.

 

*.ui 파일로 만든 ui_*.py 파일을 불러와 setup해주는 함수를 만든다.

 

2. layout 생성

      self.ui.layoutMonitor = QGridLayout(self.ui.BoxMonitor)
      self.ui.layoutMonitor.setSpacing(0)	#각 객체 사이의 여백을 설정(Horizental, Vertical 따로 설정할 수 있는 함수도 있다)
      self.ui.layoutMonitor.setAlignment(Qt.AlignBottom)
      self.ui.layoutMonitor.setContentsMargins(0,0,0,0)	#객체 하나하나의 여백을 설정

main.py에 layoutMonitor를 QGridLayout 객체를 만들고 그 parent를 BoxMonitor라는 GroupBox의 Layout으로 설정해준다.

더보기
이놈이다.

그리고 

 

    def carMonitoring(self):
        self.CAR_NUM = 30
        self.carLList = []
        self.carRList = []
        self.carCList = []

        for floor in range(0, int(self.CAR_NUM/2)):
            self.carLList.append(leftCar())
            self.carRList.append(rightCar())
            self.carCList.append(centerCar())

            self.ui.layoutMonitor.addWidget(self.carLList[floor], floor, 0, 1, 1)
            self.ui.layoutMonitor.addWidget(self.carCList[floor], floor, 1, 1, 1)
            self.ui.layoutMonitor.addWidget(self.carRList[floor], floor, 2, 1, 1)

이런식으로 함수를 만들어주면 된다.

 

관리를 편하게 하기 위해 만든 객체들은 list에 각각 넣어주고,

for문을 반복하면서 원하는 위치에 layout의 addWidget함수를 이용하여 추가해준다.

 

그럼 끗~!

 

역시 개발은 다하고 나면 별거 아닌데 처음 할 때는 힘든 듯 하다.

 

분명 객체를 이용하는 거라 방법이 있을 거라고 생각하고 이것저것 해봤는데 layout을 생성해서 layout에 추가하는 것을 생각을 못해 시간을 쬐까 잡아먹었다. 아직 갈 길이 멀다 ㅎ

 

 

서론

요는 pyqt에서는 코드 내에서 'designer'를 이용해 만든 .ui파일을 업데이트 하는 라이브러리가 있는 반면, pyside2에서는 해당 자료를 찾을 수가 없어 직접 만들어보았다.

 

목표

$ pyside2-uic monitor.ui > ui_monitor.py

(> : pyside2-uic monitor.ui의 결과를 ui_monitor.py 파일에 저장하라는 명령이다.)

를 python 내에서 바로 실행이 될 수 있게 만들어보자.

코드

import os, sys
from subprocess import Popen, PIPE
import PySide2 as ref_mod
from PySide2.QtWidgets import *


def main():
    #파일 이름
    file_name = "monitor.py"

    dir = os.path.dirname(os.path.realpath(__file__)) #root 폴더 경로 가져오기
    ui_path = os.path.join(dir,"ui" ,file_name.replace(".py", ".ui"))   #*.ui 경로 가져오기
    ui_py_path =  os.path.join(dir,"ui_py" ,"ui_" + file_name)          #ui_*.py 경로 가져오기
    exe = os.path.join(os.path.dirname(ref_mod.__file__), "uic.exe")    #실행 명령어 입력하기
    cmd = [exe] + ['-g', 'python'] + [ui_path]                          

    proc = Popen(cmd, stdout=PIPE, encoding='utf8') #명령어 실행
    out = open(ui_py_path, 'w') #저장할 파일 열기
    print(proc.stdout.read(), file=out) #파일 저장
    out.close() #저장 파일 닫기

설명

디렉토리 구조

main.py에서 실행이 되는 코드이고, ui/monitor.ui를 ui_py/ui_monitor.py로 변경해주는 기능을 구현한 것이다.

 

일단 pyside2-uic의 uic파일의 위치부터 확인해보았다.(우연찮게..ㅋㅋ)

$pyside2-uic -h

를 치면 다양한 옵션이 나오는데 여러가지 시도해보던 중 에러 코드의 실행 위치를 통해 uic.exe 파일의 위치를 확인할 수 있었다.

C:\Users\PC\anaconda3\envs\plcmonitoring\Lib\site-packages\PySide2

plcmonitoring이 프로젝트 명이고 그 뒤로부터는 똑같을 것이다..ㅎㅎ 여기 uic.exe가 있다.

그리고 이를 실행시켜주는 코드는 

C:\Users\PC\anaconda3\envs\plcmonitoring\Lib\site-packages\PySide2\scripts\pyside_tool.py

pyside_tool.py 안에 코드가  구현이 되어있다.

해당 코드의 부분을 잠깐 살펴보면 

from __future__ import print_function

import sys
import os
import subprocess

from subprocess import Popen, PIPE
import PySide2 as ref_mod


def main():
    # This will take care of "pyside2-lupdate" listed as an entrypoint
    # in setup.py are copied to 'scripts/..'
    cmd = os.path.join("..", os.path.basename(sys.argv[0]))
    command = [os.path.join(os.path.dirname(os.path.realpath(__file__)), cmd)]
    command.extend(sys.argv[1:])
    sys.exit(subprocess.call(command))


def qt_tool_wrapper(qt_tool, args):
    # Taking care of pyside2-uic, pyside2-rcc, and pyside2-designer
    # listed as an entrypoint in setup.py
    pyside_dir = os.path.dirname(ref_mod.__file__)
    exe = os.path.join(pyside_dir, qt_tool)

    cmd = [exe] + args
    proc = Popen(cmd, stderr=PIPE)
    out, err = proc.communicate()
    if err:
        msg = err.decode("utf-8")
        print(msg, file=sys.stderr)
    sys.exit(proc.returncode)


def uic():
    qt_tool_wrapper("uic", ['-g', 'python'] + sys.argv[1:])

이런 식으로 구현이 되어있는데, 여기서 모티브를 얻었다.

 

먼저 os.path부터 보자.

 

os.path.dirname() : 파일이 있는 디렉토리 path를 반환한다.
os.path.basename() : 현재 파일의 이름을 반환한다.
os.path.join() : path끼리 합쳐준다.(인자 여러개 가능)

이 외에도 다양하게 많은데, 몇개만 알고 있으면 파일의 위치를 찾아내 절대 경로를 설정하는 것은 전혀 어렵지 않을 듯 하다.

 

살짝 헤맸던 부분은 subprocess의 Popen()이다.

class subprocess.Popen(args, bufsize=-1, excutable=None, 
                       stdin=None, stdout=None, stderr=None, 
                       preexec_fn=None, close_fds=None, shell=False, 
                       cwd=None, env=None, universal_newlines=False, 
                       startupinfo=None, creationflags=0, restoreflags=0, 
                       restore_signals=True, start_new_session=False, pass_fds=()
                      )

Popen은 여러가지 옵션들이 있는데 내가 설정한 것은 크게 2가지이다. stdout과 encoding.

stdout = PIPE로 설정을 했는데 이는 Popen()의 결과가 현재 프로세스로 출력되는 것이 아닌 PIPE를 통해 자식 프로세스에서 출력이 되도록(현재의 출력창에는 보이지 않도록) 하기 위함이고,

 

이 놈은 출력을 binary 형식으로 하는데 우리는 utf-8이 제일 익숙하므로 해당 타입으로 출력하라는 의미이다.

 

출력 결과는  proc.stdout.read()를 통해 읽을 수 있고,

open() 명령어를 통해 file descriptor를 생성해주고 print()의 결과를 file descriptor에 써준 뒤 다시 파일을 닫음으로써 새로운 파일을 생성시킨다.

와.... 진짜 이거 때문에 3시간 날린 듯...ㅋㅋㅋ

 

문제의 시작은 이랬다....

 

0~9까지의 버튼을 만드는데

 

self.button0.clicked.connect(lambda idx: self.setNumber(0))
self.button1.clicked.connect(lambda idx: self.setNumber(1))
self.button2.clicked.connect(lambda idx: self.setNumber(2))
self.button3.clicked.connect(lambda idx: self.setNumber(3))
self.button4.clicked.connect(lambda idx: self.setNumber(4))
...

def setNumber(self, num):
	print(num)

이런식으로 만들려고 했다.

 

하지만 아무리봐도 저렇게 무식하게 다 쓰는건 폼이 안난다.

 

그래서 좀 더 뒤져보니

for문과 list를 이용해 좀 더 깔쌈하게 짜는 법이 있어서 그 방법으로 해보기로 했다.

 

  self.button_list = [self.button0, self.button1, self.button2, self.button3, self.button4, self.button5, self.button6, self.button7, self.button8, self.button9]

  for i, btn in enumerate(self.button_list):
      btn.clicked.connect(lambda stat, idx=i: self.setCarNumber(stat, idx))

def setCarNumber(self, stat, num):
	print(num)

button 리스트를 만들어주고 clicked가 지 상태를 반환한다고 해서 쓰지도 않을 stat 인자와 실제 필요한 idx인자를 이용해서 깔쌈하게 짜보기로...

 

TypeError: () missing 1 required positional argument: 'stat' ...?

 

에러가 나네.... stat인자가 없다고..?

lambda [매개변수] : [식] 이런 식으로 구현하는거 맞지 않니..?

 

그래서 정말 여러가지 시도를 해봤다... 나는 람다 함수를 잘 모르니깐!!! ㅋㅋㅋ

 

하지만 문제를 해결하고 나니 원인을 알 수 있겠더라..ㅋㅋ


해결

일단 PySide2의 clicked() 함수는 2종류다. 바로 clicked()와 clicked(bool)

여담 : Signal : 신호.. 말 그대로 어떠한 이벤트가 발생했을 때 실행되는 함수라고 보면 되고... Slot : 신호가 발생했을 때 실행되는 함수라고 보면 된다. 같은 소리같이 들리겠지만 실제 구현은 Slot에서 하면 된단 소리다...ㅋㅋ

그래서

btn.clicked.connect(lambda idx=i: self.setCarNumber(idx))

이런식으로 선언을 하면 clicked(bool)에 의해서 idx가 False값이 들어가버린다..ㅋㅋㅋ

 

즉, clicked를 이용해서 내가 원하는 구현을 하고 싶으면 일단 stat를 놓고, idx를 추가하면 된다.

btn.clicked.connect(lambda stat=False, idx=i: self.setCarNumber(idx))

이런식으로 말이다..ㅋ

개 웃긴게 매개변수 2개를 반환하는 clicked는 없어서 stat만 쓰면 인자가 없다고 쌩난리를 피는 것...

 


아 그리고 이런 문제도 경험했다.

#안되는 것
btn.clicked.connect(lambda idx: self.setCarNumber(i))

이건 안되고(어떤 버튼을 누르던 출력이 9가 나온다 ㅋㅋ)

#되는 것
self.button0.clicked.connect(lambda idx: self.setNumber(0))

이건 되고..ㅋㅋㅋ

 

진심 개빡쳤었다..ㅋㅋ

 

지금 돌이켜보면 내가 람다 함수를 잘 몰랐던 것과... 저 connect가 한 번 연결시켜놓으면 계속 그 상태를 불러오는게 아니라 lambda idx: self.setNumber(i)를 불러오는거라서 계속 i의 최종 값인 9가 출력되었던 것....

 

 

+ Recent posts