PyQt 예제 4.1.2

4 버전의 마지막 글이다. 이 버전의 내용이 단순히 Qt Designer를 이용하여 Ui를 꾸미고 사용해보는 것이기 때문에 내용이 크게 많지 않다.

이번 글은 이전에 만든 ui 파일을 직접 적용해보는 것이다.


먼저, 대화 상자를 넣을 액션을 추가하기 위해서 mainWindow.py를 조금 수정하였다.
        # File
        fileMenu = QMenu()
        objCont.AddActions(fileMenu, (self.textEdit.actions[0], None,
                self.textEdit.actions[1], self.textEditRecentFilesMenu,
                None, self.textEdit.actions[2], self.textEdit.actions[3],
                None, quitAction))
        fileMenuAction = objCont.CreateAction(self, "파일(&F)", None, None, 
                "파일의 열기 및 저장 등을 포함합니다")
        fileMenuAction.setMenu(fileMenu)
        self.menuBar().addAction(fileMenuAction)
######## 수정 후
        # File
        fileMenu = QMenu()
        objCont.AddActions(fileMenu, (self.textEdit.fileActions[0], None,
                self.textEdit.fileActions[1], self.textEditRecentFilesMenu,
                None, self.textEdit.fileActions[2], self.textEdit.fileActions[3],
                None, quitAction))
        fileMenuAction = objCont.CreateAction(self, "파일(&F)", None, None, 
                "파일의 열기 및 저장 등을 포함합니다")
        fileMenuAction.setMenu(fileMenu)
        self.menuBar().addAction(fileMenuAction)
        # Image
        imageMenu = QMenu()
        objCont.AddActions(imageMenu, self.imageLabel.actions)
        imageMenuAction = objCont.CreateAction(self, "이미지(&M)", None, None,
                "이미지를 조절합니다")
        imageMenuAction.setMenu(imageMenu)
        self.menuBar().addAction(imageMenuAction)
######## 수정 후
        # Edit
        editMenu = QMenu()
        objCont.AddActions(editMenu, 
                self.imageLabel.actions + [None] + self.textEdit.editActions)
        editMenuAction = objCont.CreateAction(self, "편집(&E)", None, None,
                "텍스트 에디터나 이미지를 편집합니다")
        editMenuAction.setMenu(editMenu)
        self.menuBar().addAction(editMenuAction)
메뉴에서 "이미지" 부분은 "편집"으로 바꾸고 이 메뉴 아래에 찾는 부분을 추가한 것 뿐이다.
그리고 이 대화 상자에 대한 부분은 TextEdit 를 다루고 있는 dockWidget 부분에 추가하였다.

먼저, TextEdit 클래스 쪽에서 수정된 부분을 살펴보겠다.
    actions = []
#### 수정 후
    fileActions = []
    editActions = []
        # Save As Text File Action
        saveAsTextFileAction = objCont.CreateAction(self,
                "다른 이름으로 텍스트 파일 저장(&A)", 
                ":/saveAsTextFileIcon.png", QKeySequence.SaveAs, 
                "다른 이름으로 텍스트 파일을 저장합니다.", self.SaveAsTextFile)
        self.actions = [newTextFileAction, openTextFileAction,
                        saveTextFileAction, saveAsTextFileAction]

        # Plain Text Edit Context Menu
        self.titleLabel.setContextMenuPolicy(Qt.ActionsContextMenu)
        objCont.AddActions(self.titleLabel, self.actions)

        self.connect(self, SIGNAL("textChanged()"), self.UserChange)

    ################################################################ Method ###

    def UserChange(self):
#### 수정 후
        # Save As Text File Action
        saveAsTextFileAction = objCont.CreateAction(self,
                "다른 이름으로 텍스트 파일 저장(&A)", 
                ":/saveAsTextFileIcon.png", QKeySequence.SaveAs, 
                "다른 이름으로 텍스트 파일을 저장합니다.", self.SaveAsTextFile)

        # Find and Replace Action
        findReplaceAction = objCont.CreateAction(self,
                "텍스트 찾기 및 바꾸기(&F)",
                None, QKeySequence.Find,
                "텍스트에서 단어를 찾거나 바꿉니다.", self.FindReplace) 

        self.fileActions = [newTextFileAction, openTextFileAction,
                        saveTextFileAction, saveAsTextFileAction]
        self.editActions = [findReplaceAction]

        # Plain Text Edit Context Menu
        self.titleLabel.setContextMenuPolicy(Qt.ActionsContextMenu)
        separator = QAction(self)
        separator.setSeparator(True)
        objCont.AddActions(self.titleLabel, 
                self.fileActions + [separator] + self.editActions)

        self.connect(self, SIGNAL("textChanged()"), self.UserChange)

    ################################################################ Method ###

    def FindReplace(self):
        findReplaceDialog = FindReplaceDialog(self.toPlainText(), self)
        findReplaceDialog.exec_()
        self.clear()
        self.setPlainText(findReplaceDialog.GetText())

    def UserChange(self):
대화 상자 호출을 위한 부분들이 많이 추가되었다. findReplaceAction을 통한 대화 상자 호출 액션이 추가되었고, 컨텍스트 메뉴에도 이 액션이 추가 되었다.

그리고 FindReplace 메소드가 새로 추가되어, 대화 상자를 호출하도록 하였다.

원래 일반적으로 "찾기 및 바꾸기" 대화 상자가 실행되면, 찾고 난 후 단어에 블록이 되어서 선택되어지는 것이 일반적이다. 하지만 여기서는 그렇게 복잡한 부분까지는 들어가지 않고 간단히 찾았다는 것만 알리도록 하였다.
(원래는 할 줄 몰라서 + 너무 어렵고 복잡해서 + 이미 텍스트 에디터 기능에 Find라는 메소드를 통해서 지원하므로)

그리고 바꾸는 부분에서는 기존 텍스트를 변경한 후에 이를 대화 상자로부터 가져와서 대체하는 방법으로 처리하였다.



그럼 이제 실제로 ui 파일을 사용하는 방법을 보겠다.

먼저, pyuic4 -o ui_findReplaceDlg.py findReplaceDlg.ui 명령을 내려서 ui 파일을 파이썬 파일로 변환시켜주자. pyuic4는 ui 파일을 파이썬 모듈로 변환 시켜주는 도구이다. pyrcc4와 같은 도구이다.

명령을 수행하고 나면 파이썬 파일이 하나 생성되는데, 실제로 이를 열어보면 ui 파일에서 생성한 대화 상자 클래스가 생성되어 있고, setupUi라는 메소드 아래에 상세한 설정들이 지정되어 있다.

이 모듈을 사용해야 하므로 import ui_findReplaceDlg 를 하여서 모듈을 가져오자. 추가로, import re도 하여 정규식을 사용할 수 있도록 하자. 그리고 클래스를 생성해야 하는데, 자세한 코드를 먼저 보겠다.
class FindReplaceDialog(QDialog, ui_findReplaceDlg.Ui_FindReplaceDialog):
    """텍스트 에디터의 찾기 및 바꾸기 대화상자"""
    def __init__(self, text, parent=None):
        super().__init__(parent)
        self.text = text
        self.index = 0
        self.setupUi(self)

        self.connect(self, SIGNAL('found'),
                self.FoundText)
        self.connect(self, SIGNAL('notfound'),
                self.NotFoundText)
        self.connect(self, SIGNAL('replace'),
                self.ReplaceText)
        self.connect(self, SIGNAL('replaceAll'),
                self.ReplaceAllText)

        MAC = 'qt_mac_set_native_menubar' in globals()
        if not MAC:
            self.findButton.setFocusPolicy(Qt.NoFocus)
            self.replaceButton.setFocusPolicy(Qt.NoFocus)
            self.replaceAllButton.setFocusPolicy(Qt.NoFocus)
            self.closeButton.setFocusPolicy(Qt.NoFocus)
        self.updateUi()

    ### Slot ###
    @pyqtSignature('QString')
    def on_findLineEdit_textEdited(self, text):
        self.index = 0
        self.updateUi()

    @pyqtSignature('')
    def on_findButton_clicked(self):
        regex = self.MakeRegex()
        match = regex.search(self.text, self.index)
        if match is not None:
            self.index = match.end()
            self.emit(SIGNAL('found'), match.start())
        else:
            self.emit(SIGNAL('notfound'))

    @pyqtSignature('')
    def on_replaceButton_clicked(self):
        regex = self.MakeRegex()
        self.text = regex.sub(self.replaceLineEdit.text(), self.text, 1)
        self.emit(SIGNAL('replace'))

    @pyqtSignature('')
    def on_replaceAllButton_clicked(self):
        regex = self.MakeRegex()
        self.text = regex.sub(self.replaceLineEdit.text(), self.text)
        self.emit(SIGNAL('replaceAll'))

    ### Method ###
    def updateUi(self):
        enable = bool(self.findLineEdit.text())
        self.findButton.setEnabled(enable)
        self.replaceButton.setEnabled(enable)
        self.replaceAllButton.setEnabled(enable)

    def MakeRegex(self):
        findText = self.findLineEdit.text()
        if self.syntaxComboBox.currentText() == '문자열':
            findText = re.escape(findText)
        flags = re.MULTILINE | re.DOTALL
        if not self.caseCheckBox.isChecked():
            flags |= re.IGNORECASE
        if self.wholeCheckBox.isChecked():
            findText = r"\b%s\b" % findText
        return re.compile(findText, flags)

    def GetText(self):
        return self.text

    def FoundText(self):
        QMessageBox.information(self, 
                '문자열 찾음!', '일치하는 문자열을 찾았습니다!')

    def NotFoundText(self):
        QMessageBox.information(self, 
                '더 이상 찾는 문자열 없음!', 
                '더 이상 일치하는 문자열이 없습니다!')
    
    def ReplaceText(self):
        QMessageBox.information(self,
                '문자열 바꿈!',
                '문자열을 바꾸었습니다!')

    def ReplaceAllText(self):
        QMessageBox.information(self,
                '문자열 모두 바꿈!',
                '문자열을 모두 바꾸었습니다!')
FindReplaceDialog라는 클래스를 새로 생성하였는데, QDialog과 ui_findReplaceDlg 모듈에 있는 클래스를 상속하고 있다.

여기서 pyuic4를 통해 메인 윈도우나 대화 상자를 생성하게 되면, 이들에 대한 객체(object) 이름에 Ui_ 가 앞에 붙어서 클래스 이름이 정해진다.
따라서 대화 상자 클래스를 상속하기 위해서 Ui_FindReplaceDialog를 사용한 것이다.

그리고 ui 모듈을 사용하는 방법으로 다른 것도 있지만, 이와 같이 다중 상속을 통해서 매우 간편하게 ui 모듈을 쓸 수 있다.

다음으로 클래스의 부모를 초기화 해주어야 하므로, 예전처럼 super()를 사용하여 부모를 초기화 시켜주었다. 그리고 클래스 내에서 사용할 변수들을 초기화 해준 다음, 앞에서 말했던 setupUi()를 실행시켜서 ui를 초기화 시켜주었다.

이렇게 하게 되면 아주 간단하게 ui가 클래스에 설정된다. 그리고 여기서 setupUi가 한 가지 추가 작업을 시행하는데, QtCore.QMetaObejct.connectSlotByName() 이라는 메소드를 호출한다.

이것은 ui 파일에서 생성한 객체들에 대한 시그널과 슬롯을 자동으로 연결해주는 기능을 한다. 이 메소드를 호출할 때 객체를 하나 넘겨주는데, 이 객체 내에 있는 모든 자식들을 검색해서, 자식의 시그널과 슬롯들을 연결해준다.

이때 연결하는 슬롯의 이름은 특별한 이름으로 지정되어 있는데, "on_위젯이름_시그널이름" 이다.

즉, connect 함수를 쓸 필요 없이, 위 이름으로 메소드를 만들어두면 자동으로 연결을 해주는 것이다. 이에 대한 자세한 사용은 아래에 나오므로, 다음 코드를 다시 보겠다.


다음 코드에서는 MAC 변수에 대해서 체크를 하는데, MAC을 제외한 리눅스, 윈도우즈에서는 버튼에 대한 탭 포커스가 중요하지 않기 때문에 이를 제외하는 코드이다. 리눅스, 윈도우즈에서는 키보드를 쓸 때에서 단축키 사용이 가능하기 때문이다.

따라서 MAC 에만 있는 함수 PyQt4.QtGui.qt_mac_set_native_menubar 가 현재 import 되어 있는 지를 확인한 다음, 없을 경우에는 리눅스, 윈도우즈이므로 버튼에 대한 포커스를 제거하는 것이다.

그리고 updateUi를 통해서 ui를 갱신하도록 하였다.


이제 슬롯을 보겠는데, 앞에서 말한 슬롯들이다. 이 슬롯들은 connectSlotByName이 실행되면서 지정된 시그널들과 자동으로 연결이 된다.

슬롯을 만드는 것은 어렵지 않으므로, 몇 가지만 살펴보겠다. 먼저, @pyqtSignature 라는 장식자(decorator)를 사용하고 있다. 이것을 사용하는 이유는, 시그널을 구분하기 위함이다.

원래 connect를 쓸 때는 SIGNAL('c++용 시그널 이름') 방식을 사용하기 때문에 시그널 이름에 자료형이 포함되어서 같은 이름의 시그널들을 구분할 수 있었다.
(즉, 오버로딩된 시그널들을 구분할 수 있었다.)

하지만 파이썬에는 자료형이 없기 때문에, 이를 구분할 수가 없다. 따라서 장식자에 시그널의 인자를 명시하여 시그널들을 구분하고 하는 것이다.

textEdited 시그널의 경우 QString을 넘기므로 장식자에 QString을 추가해주었다. 그리고 시그널로부터 오는 인자를 text로 받게 된다.

그 아래 버튼 클릭에 대한 시그널들은 인자를 보내지 않기 때문에, 장식자에 빈 문자열을 추가해주었다.



슬롯에 내용을 보기 전에, 먼저 메소드의 내용을 보겠다. updateUi 메소드는 findLineEdit 에 적어도 글자가 1개 이상 들어와야만 버튼들이 활성화 되도록 한 것이다. 즉, 빈 문자열에 대해서는 검색을 하지 않도록 막는 것이다.

MakeRegex 메소드는 정규식 표현을 만드는 메소드이다. 검색어를 가져와서, 콤보 박스의 내용이 '문자열'일 경우에는 \ 문자에 대해서 escape 문자를 처리하게 된다. 즉, 정규식 표현에 대한 기호가 아닌, 실제 \ 문자를 표현하기 위해서 \\로 변경을 할 것이다.

그리고 정규식에서 사용할 조건을 지정하기 위해서 flags를 추가하였고, caseCheckBox가 체크 되어 있다면, 대소문자를 구분한다는 의미이므로, 체크가 안 되어 있을 경우에만, 대소문자 구별을 무시한다는 조건을 추가하였다.

그리고 wholeCheckBox의 경우에는 단어 단위로 검색을 하는 것이므로 검색어 주위에 공백이 있어야 한다. 따라서 주어진 검색어 주변에 공백 기호인 \b를 추가하여 정규식을 생성하였다.

그런 다음, 정규식 표현과 조건으로부터 실제 사용할 정규식을 리턴하도록 하였다.

GetText 메소드는 단순히 변경된 텍스트를 리턴하기 위한 것이고, 그 아래 메소드들은 문자열을 찾거나 변경하였을 경우 메시지를 통해 알려주는 메소드이다.



이제, 슬롯의 내용을 보면 findLineEdit에 대한 슬롯은, 새로운 찾기 검색어가 입력될 때마다 검색을 새로 시작하는 것이다.

findButton에 대한 슬롯은, MakeRegex로 부터 만든 정규식을 이용해서 주어진 위치(self.index)부터 검색을 하는 것이다. 문자열을 찾으면 match에 값이 존재하게 되고, 검색 위치를 match 이후로 변경한 후에 대화 상자에서 found 시그널을 보내도록 하였다.

그리고 찾지 못한 경우에는 notfound 시그널을 보내게 된다.
(참고로, 깜박하여 match.start() 인자를 사용하지 않았는데, 이를 통해서 어느 위치에서 단어가 검색되었는지 알 수 있다.)

replaceButton에 대한 슬롯도 search 대신 sub를 사용해서 지정된 단어를 대체하도록 하는 것이다. 이때, 마지막 인자로 1을 넘겨서 한 번만 대체하도록 하였다.

그리고 replaceAllButton 에서는 모두 변경이므로, 마지막 인자를 주지 않고 여러 번 모두 변경시키도록 하였다.



이로써 Qt Designer로부터 생성한 ui 를 사용하는 방법을 배웠다. 이제 매우 쉽고 간편하게 PyQt를 개발할 수 있게 되었다.

그리고 지금까지 한 내용을 바탕으로 충분히 프로그램을 만들 수 있다. 책에서도 이 이후에 내용은 좀 더 고급 내용으로, 데이터를 다루거나 사용자 위젯을 만드는 내용이다.

아마도 이후 내용은 장기간 다루지 않을 예정이므로, 다음 글이 언제 올라올지는 모르겠다.

댓글 없음:

댓글 쓰기