パスワードジェネレータの完成:スロットとシグナル(PySide2,QML) GUI-3章

まえがき

GUI(qml)とロジック(PySide2)はスロットとシグナルで繋がっている。GUIで何か入力がある場合、例えばボタンをが押された場合では「onClicked」がシグナルとなる。そこで呼び出されるメソッドがロジック側で定義されデコレータとしてスロットが使用される。

パスワードを生成するための条件をGUIから指定してロジック側でそれを受け取り、パスワードを生成して返しGUIで表示する。

パスワードを生成する機能としてはこれだけなので今回でとりあえず完成。

テスト環境構築編はこちら

エラー、バグ解決編はこちら

変更履歴

2020/05/03: 新規追加

この記事の環境

バージョン: Python 3.8.1

モジュール:Qt5 (Desktop Qt 5.12.7 clang 64bit)、 PySide2 (5.14.1)

IDE: Spacemacs 0.300.0 (emacs 26.3)

環境構築: pyenv、pyenv-virtualenv

OS: macOS 10.15.4 & Ubuntu 20.04

QMLからエレメントの状態を渡す。シグナルと解説

最初に設置されている部品について軽い説明。

このパスワードジェネレータで使ったエレメントは主に

  • チェックボックス: CheckBox
  • ラジオボタン: RadioDelegate
  • ボタン: Button
  • テキストエディット: TextEdit

の4種類。

「RadioDelegate」は同じグループの中では1つしかONにできないものだ。

「TextEdit」に生成したパスワードを渡して表示させることにした。これはラベルだと出力された文字列を選択できないためである。選択できないとコピーできないのでせっかく生成しても意味がない。別途、コピーボタンを追加する方法でも良かったがテキストエディットならCtrl+Aで選択できるので、Ver1としてはこれで良しとした。

パスワードジェネレータのQML

生成ボタンを押した時に、各RadioDelegateとCheckBoxの状態を取得し引数としてロジック側に渡している。書式は「id属性の値.checked」という形式になる。

RadioDelegateはstate属性ではなくchecked属性でなければ、チェックされているかどうかがわからないので注意。チェックされると数値の1、されていないと0が渡される。

CheckBoxはcheckedState属性で状態を取得できる。チェックされると2、されていないと0が渡される。なぜか2なので注意。

QMLはCSSやJSONのような記述だが、内部でJavaScriptを使えるうえに、QML内で他のエレメントの属性のや値にアクセスできるので、QML側だけでもかなり高機能なGUIを実装できそうだ。

# ↑本当はQML
import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 2.3

Window {
    visible: true
    width: 640
    height: 480
    title: qsTr("ぱすわーどジェネレータ")
    color: "darkgrey"

    Button {
        id: button
        x: 70
        y: 9
        text: qsTr("生成")
        onClicked: function(){
            passDisplay.text = PwGenerator.stateHandler(
                radEight.checked, radSixte.checked, radTwetFor.checked,
                chbxUpper.checkState, chbxKigo.checkState, chbxNumb.checkState,
                radMojiSta.checked, radKigoSta.checked, radNumbSta.checked,
                radTypeNone.checked, radTypeHype.checked, radTypeDott.checked)
        }
    }
    GroupBox {
        id: groupBox1
        x: 70
        y: 60
        width: 500
        height: 70
        title: qsTr("文字数指定")

        Row{
            width: 476
            height: 60
            anchors.centerIn: parent
            spacing: 55

            RadioDelegate {
                id: radEight
                x: 9
                y: 3
                text: qsTr("8文字")
                checked: true
            }

            RadioDelegate {
                id: radSixte
                x: 162
                y: 3
                text: qsTr("16文字")
            }

            RadioDelegate {
                id: radTwetFor
                x: 334
                y: 3
                text: qsTr("24文字")
            }
        }
    }

    GroupBox {
        id: groupBox
        x: 70
        y: 140
        width: 500
        height: 125
        title: qsTr("条件指定")

        Row{
            width: 476
            height: 60
            anchors.verticalCenterOffset: -15
            anchors.horizontalCenterOffset: 0
            anchors.centerIn: parent
            spacing: 70

            CheckBox {
                id: chbxUpper
                x: 0
                y: 0
                text: qsTr("大文字")
            }
            CheckBox {
                id: chbxKigo
                x: 369
                y: 0
                text: qsTr("記号")
            }
            CheckBox {
                id: chbxNumb
                x: 189
                y: 0
                text: qsTr("数字")
            }
        }

        Row{
            width: 476
            height: 60
            anchors.verticalCenterOffset: 21
            anchors.horizontalCenterOffset: 0
            anchors.centerIn: parent
            spacing: 35

            RadioDelegate {
                id: radMojiSta
                x: 189
                y: 0
                text: qsTr("文字始まり")
                checked: true
            }
            RadioDelegate {
                id: radKigoSta
                x: 369
                y: 0
                text: qsTr("記号始まり")
            }
            RadioDelegate {
                id: radNumbSta
                x: 0
                y: 0
                text: qsTr("数字始まり")
            }
        }
    }

    GroupBox {
        id: groupBox3
        x: 70
        y: 271
        width: 500
        height: 70
        title: qsTr("形式指定")

        Row{
            width: 476
            height: 60
            anchors.verticalCenterOffset: 3
            anchors.horizontalCenterOffset: 0
            anchors.centerIn: parent
            spacing: 70

            RadioDelegate {
                id: radTypeNone
                x: 0
                y: 5
                text: qsTr("なし")
                checked: true
            }
            RadioDelegate {
                id: radTypeHype
                x: 192
                y: 5
                text: qsTr("ー形式")
            }
            RadioDelegate {
                id: radTypeDott
                x: 373
                y: 5
                text: qsTr(".形式")
            }
        }
    }

    GroupBox {
        id: groupBox2
        x: 70
        y: 354
        width: 500
        height: 90
        title: qsTr("生成パスワード")
        TextEdit {
            id: passDisplay
            x: 100
            y: 5
            width: 480
            height: 40
            text: qsTr("")
            font.pixelSize: 20
        }
    }
}

公式のQTのWikiは目当ての情報が探しにくい気がするが気のせいだろうか・・・。

Qt Documentation

パスワードジェネレータのロジック部分:PysSide2

MVCモデルでいうところのControllerにあたるもの。PySide2側ではデコレータとしてスロットを設定する。シグナルとスロットが一対になるように定義すると分かりやすい。シグナルから送られてくる引数はスロット側で型を明示するのが基本。

引数の型を明示しなくても問題なく動くようだが、返り値がある場合は「result=str」のように必ず設定されなければ動かない。

途中コメントアウトされている処理はコメントアウトされていない処理と比べてみた結果、違いがわからなかった。色々調べていくに当たってこういう書き方をしている方がいたのでとりあえず倣って動こかしてみたがよくわからず。単純な機能の実装だとあまり意味がないのかもしれない。

実はPySide2側からも直接エレメントに干渉できるが、そういったことはQML側にませるべき。そうでないと誰が何をそうしているのか把握が難しくなる。

パスワード生成モジュールを使っているけど

コントローラーでは受け取った値を「パスワード生成モジュール」用に設定し直している。そのまま渡して全部、専用のモジュールにやってもらう方が良かった。改良点1である。 これを踏まえて考えてみるともっと小さく分かりやすいコードに出来そうな予感がする。暇があったら改良してみよう。

# -*- coding: utf-8 -*-
"""\
PwGenerator Controller

Author:
    Hideo Tsujisaki

"""

__version__ = "0.1"
__author__ = "Hideo Tsujisaki"

import sys
from PySide2 import QtCore, QtWidgets
from PySide2.QtQml import QQmlApplicationEngine
from PySide2.QtCore import QUrl

from PwRandomizer import PwBuildCenter as pwb


class PwGenerator(QtCore.QObject):

    def __init__(self, parent=None):
        super(PwGenerator, self).__init__(parent)
    # def __init__(self):
    #     super().__init__()
        self.char_lim = 0
        self.condition = ""
        self.start_con = 0
        self.style = None
        self.pass_word = ""

    @QtCore.Slot(int, int, int, int, int, int, int, int, int, int, int, int, result=str)
    def stateHandler(self, radEight, radSixte, radTwetFor, chbxUpper, chbxKigo,
                     chbxNumb, radMojiSta, radKigoSta, radNumbSta, radTypeNone,
                     radTypeHype, radTypeDott):
        # 文字数の設定
        if radEight == 1:
            self.char_lim = 8
        if radSixte == 1:
            self.char_lim = 16
        if radTwetFor == 1:
            self.char_lim = 24

        # 含める文字種の設定
        self.condition = ""
        if chbxUpper == 2:
            self.condition += "A"
        if chbxKigo == 2:
            self.condition += "B"
        if chbxNumb == 2:
            self.condition += "C"

        # 始まりの文字指定の設定
        if radMojiSta == 1:
            self.start_con = "moji"
        if radKigoSta == 1:
            self.start_con = "kigo"
        if radNumbSta == 1:
            self.start_con = "numb"

        # パスワード形式の設定
        if radTypeNone == 1:
            self.style = None
        if radTypeHype == 1:
            self.style = "hype"
        if radTypeDott == 1:
            self.style = "dott"

        objPwb = pwb(self.char_lim, self.condition, self.start_con, self.style)
        return objPwb.pw_builder()

    def __del__(self):
        self.condition = ""


if __name__ == '__main__':
    # app = QtWidgets.QApplication(sys.argv)
    app = QtWidgets.QApplication()

    myconnect = PwGenerator()

    engine = QQmlApplicationEngine()
    bind = engine.rootContext()
    bind.setContextProperty("PwGenerator", myconnect)

    engine.load(QUrl("pw-manager.qml"))

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

    sys.exit(app.exec_())

パスワード生成モジュール

前回の記事でも取り上げたが、最終的なものをここに上げてみる。結局、結合テストをしてみないとデバッグもままならない。

# -*- coding: utf-8 -*-
"""\
パスワード生成モジュール
    英字(大文字、小文字)、記号、数字の中から抽出。
    条件は、文字数、含める文字種、始まりの文字指定、パスワード形式指定
"""

__version__ = "0.1"
__author__  = "Hideo Tsujisaki"

from random import choice
from string import ascii_lowercase as strAscLow
from string import ascii_uppercase as strAscUpp
from string import digits as strDigi
from string import punctuation as strPunc

import constants as const


class PwBuildCenter(object):
    """\
    パスワード生成機能。
    英字(大文字、小文字)、記号、数字の中から抽出。
    条件は、文字数、含める文字種、始まりの文字指定、パスワード形式指定。

    Attributes
    ----------
    char_lim : integer
    condition : string
    start_con : string
    style : string, default None

    Rturns
    ------
    pass_word : string
    """

    const.START_COND_MOJI = "moji"
    const.START_COND_KIGO = "kigo"
    const.START_COND_NUMB = "numb"
    const.PASSWORD_STYLE_HYPHEN = "hype"
    const.PASSWORD_STYLE_DOTTS = "dott"


    def __init__(self, char_lim, condition, start_con, style):
        self.lower_case = strAscLow
        self.upper_case = strAscUpp
        self.digits = strDigi
        self.kigou = strPunc

        self.char_lim = char_lim
        self.condition = condition
        self.start_con = start_con
        self.style = style

        self.cond = ""
        self.pass_word =""


    def pw_builder(self):

        self.cond_setter()

        # パスワード生成部
        # 初めの文字の指定がある場合の設定(初期値は文字始まり)
        if self.start_con == const.START_COND_MOJI:
            self.pass_word += choice(self.lower_case)

        if self.start_con == const.START_COND_KIGO:
            self.pass_word += choice(self.kigou)

        if self.start_con == const.START_COND_NUMB:
            self.pass_word += choice(self.digits)

        # 残りの文字を2文字目から生成していく
        buildStart = 2
        buildEnd = self.char_lim + 1
        for ite in range(buildStart, buildEnd):
            self.pass_word += choice(self.cond)

        # パスワード整形部
        cep_type = ""
        if self.style == const.PASSWORD_STYLE_HYPHEN:
            cep_type = "-"

        if self.style == const.PASSWORD_STYLE_DOTTS:
            cep_type = "."

        if self.style is not None:
            restrucutPw = ""
            if self.char_lim == 8:
                restrucutPw += self.pass_word[0:2] + cep_type
                restrucutPw += self.pass_word[3:5] + cep_type
                restrucutPw += self.pass_word[6:]
                self.pass_word = restrucutPw

            if self.char_lim == 16:
                restrucutPw += self.pass_word[0:5] + cep_type
                restrucutPw += self.pass_word[6:10] + cep_type
                restrucutPw += self.pass_word[11:]
                self.pass_word = restrucutPw

            if self.char_lim == 24:
                restrucutPw += self.pass_word[0:4] + cep_type
                restrucutPw += self.pass_word[5:9] + cep_type
                restrucutPw += self.pass_word[10:14] + cep_type
                restrucutPw += self.pass_word[15:19] + cep_type
                restrucutPw += self.pass_word[20:]
                self.pass_word = restrucutPw

        return self.pass_word

    def cond_setter(self):
        """\
            含める文字の設定(複数同時指定可能)
            Return: String ex."ab", "ac"
            condition: 大文字=A、記号=B、数字=C
            """
        self.cond = self.lower_case

        if "A" in self.condition:
            self.cond += self.upper_case

        if "B" in self.condition:
            self.cond += self.kigou

        if "C" in self.condition:
            self.cond += self.digits

パスワードジェネレータ ver1.0(PySide2+QML)完成品

完成品のスクリーンショットを載せておく。やはりコピーボタンが欲しいところ。今のところ生成されたパスワードをコピーするにはマウスで1回クリックした後Ctrl+aとか⌘+aとかするしかない。

むすび

動くようにはなったものの、見返してみると荒く拙い部分が多くみられる。

反省点としては、

  • コメントの内容が読み手にとって、読解の助けになってない。
  • del現状いらない機能。コメントアウトしておくべき。
  • 引数の説明がない。型の説明だけで意味も書く方が良い。
  • 何となくもっと処理を削れそう。
  • cond_stter()に渡す値が「ABC」という何ともはやである。ただ、ついがっては良い。どの道文字列なんだから「大記数」にすれば良かった。

というようなことで、バグやエラー以外でこれだけあるので今後に生かしていきたい。時間がたった後でもう一度自分のコード、設計を見直してリファクタリングするのも良い学習になると思う。

コメントする