目次:
まえがき
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は目当ての情報が探しにくい気がするが気のせいだろうか・・・。
パスワードジェネレータのロジック部分: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」という何ともはやである。ただ、ついがっては良い。どの道文字列なんだから「大記数」にすれば良かった。
というようなことで、バグやエラー以外でこれだけあるので今後に生かしていきたい。時間がたった後でもう一度自分のコード、設計を見直してリファクタリングするのも良い学習になると思う。
コメント