追記: windowのresizeに対応したほうが良いでしょうか?
固定電話番号の大まかな住所を教えてくれるアプリを作ります。
固定電話番号は10桁ではじめの一桁は「国内プレフィックス」二桁目からの五桁が「市外市内局番」となります。今回使うのは二桁目から五桁目となります。
イメージ (電気通信番号指定状況 (総務省))
見た目
イントロダクション
まず見た目の部分のを作って行きます。
ページは2ページでmain
ページで電話番号を入力し、result
ページで検索結果を表示します。それぞれわかりやすいようにまず階層構造を簡単にHTMLで表しましました。
メインページ(main_page
)
<div class="main_page">
<p class="main_page_title"></p>
<p class="main_page_note"></p>
<input class="main_page_phone_number">
<input class="main_page_track_btn">
</div>
リザルトページ(result_page
)
<div class="result_page">
<p class="result_page_title"></p>
<p class="result_page_detail"></p>
</div>
こんな感じです。そしてこれらを、StackedWidget
と呼ばれるものに追加します。それぞれsetCurrentIndex(int)
でページを切り替えます。
結果的に次のような形になります。最終的にメインウィンドウにcentralwidget
をセットします。
<div class="centralwidget">
<div class="stackedwidget">
<div class="main_page">
...
</div>
<div class="result_page">
...
</div>
</div>
</div>
コーディング
自作メソッド
def setup_qlabel(self, label, text, size, color='#2e2e2e'):
label.setText(text)
label.setFont(self.setup_font(size))
label.setAlignment(QtCore.Qt.AlignHCenter)
label.setStyleSheet('color: {}'.format(color))
def setup_font(self, size):
font = QtGui.QFont()
font.setPointSize(size)
return font
setup_qlabel
はQLabel
を設定する共通のメソッド。デフォルトのテキストカラーは#2e2e2e
としている。
setup_font
はQLabel
等のフォントサイズを設定する。
main_window
main_window.setWindowTitle('Phone-number-tracker')
size = {
'width': {
'default': 600,
'minimum': 100,
'maximum': 10000
},
'height': {
'default': 400,
'minimum': 100,
'maximum': 10000
}
}
main_window.resize(size['width']['default'],
size['height']['default'])
main_window.setMinimumSize(size['width']['minimum'],
size['height']['minimum'])
main_window.setMaximumSize(size['width']['maximum'],
size['height']['maximum'])
メインウィンドウのサイズを設定する。
centralwidget
self.centralwidget = QtWidgets.QWidget(main_window)
設定:
self.centralwidget.setStyleSheet('background-color: #eaeaea;')
stackedwidget
self.stackedwidget = QtWidgets.QStackedWidget(self.centralwidget)
設定:
self.stackedwidget.setGeometry(100, 100,
size['width']['default'] - 200,
size['height']['default'] - 200)
main_page
self.main_page = QtWidgets.QWidget()
設定:
self.main_page.setGeometry(100, 100,
size['width']['default'] - 200,
size['height']['default'] - 200)
self.main_page_layout = QtWidgets.QVBoxLayout(self.main_page)
main_page > title
self.main_page_title = QtWidgets.QLabel(self.main_page)
設定:
self.setup_qlabel(self.main_page_title, 'The phone number tracker', 24, '#ea5506')
self.main_page_title.setFixedHeight(100)
setFixedHeight
で高さを設定している。setup_qlabel
では(何に, このテキストを, このサイズで, このカラーで)
という順になっている。
main_page > note
self.main_page_note = QtWidgets.QLabel(self.main_page)
設定:
self.setup_qlabel(self.main_page_note, "Note: No '-' needed", 12, '#3e3e3e')
電話番号にハイフンは必要で無いことを示している。
main_page > phone_number
elf.main_page_phone_number = QtWidgets.QLineEdit(self.main_page)
設定:
self.main_page_phone_number.setFont(self.setup_font(14))
self.main_page_phone_number.setMaxLength(34)
self.main_page_phone_number.setStyleSheet('color: #2e2e2e; border: 2px solid #7fbfff; border-radius: 4px; padding: 2px')
main_page > track_btn
self.main_page_track_btn = QtWidgets.QPushButton(self.main_page)
self.main_page_track_btn.setText('Track')
self.main_page_track_btn.setFont(self.setup_font(14))
self.main_page_track_btn.setStyleSheet('color: #2e2e2e;')
result_page
self.result_page = QtWidgets.QWidget()
設定:
self.result_page.setGeometry(100, 100,
size['width']['default'] - 200,
size['height']['default'] - 200)
self.result_page_layout = QtWidgets.QVBoxLayout(self.result_page)
self.result_page_layout.setContentsMargins(0, 0, 0, 0)
このページでは少しオブジェクトがキツキツなのでsetContentsMargines
でマージンを0にしている。
result_page > title
self.result_page_title = QtWidgets.QLabel(self.result_page)
設定:
self.setup_qlabel(self.result_page_title, '', 16)
self.result_page_title.setFixedHeight(26)
result_page > detail
self.result_page_detail = QtWidgets.QPlainTextEdit(self.result_page)
設定:
self.result_page_detail.setFont(self.setup_font(12))
self.result_page_detail.setStyleSheet('border: 2px solid #7fbfff; border-radius: 4px')
self.result_page_detail.verticalScrollBar().setStyleSheet('border: none')
self.result_page_detail.setReadOnly(True)
三行目では垂直方向のスクロールバーのスタイルを指定している。
それぞれをレイアウトに組み込む
メインページ
self.main_page_layout.addWidget(self.main_page_title)
self.main_page_layout.addWidget(self.main_page_note)
self.main_page_layout.addWidget(self.main_page_phone_number)
self.main_page_layout.addWidget(self.main_page_track_btn)
self.stackedwidget.addWidget(self.main_page)
1~4行目でmain_page
にそれぞれのオブジェクトを順に追加している。(詳しくは、main_page
のレイアウトであるmain_page_layout
に追加している)最後に、stackedwidget
にページを追加している。
リザルトページ
self.result_page_layout.addWidget(self.result_page_title)
self.result_page_layout.addWidget(self.result_page_detail)
self.stackedwidget.addWidget(self.result_page)
仕上げ
初期のページを設定する。
self.stackedwidget.setCurrentIndex(0)
メインウィンドウにcentralwidget
をセットする。
main_window.setCentralWidget(self.centralwidget)
中身
###PhoneNumber class
まずPhoneNumber
クラスを作る。そして必要なのは2桁目から6桁目なのでその5桁を取り出す。
class PhoneNumber:
def __init__(self, number):
self.number = number
self.trimmed_number = number[1:6]
次にホントに固定電話の番号かを軽くチェックするメソッドを作る。
def check_format(self):
length = len(self.number) == 10
is_digit = self.number.isdigit
return length and is_digit
最後に住所を得るメソッドを作る。
def get_address(self):
outer_list = [self.trimmed_number[:i] for i in range(1, 5)]
data = pd.read_pickle('phone_numbers_data.pickle')
result = data[data.outer.isin(outer_list)]
result = result[result.inner.str.len() == result.inner.str.len().min()]
address = result['address']
return list(address)
5桁のうち、何桁までが市外局番かわからないので1桁の場合から4桁の場合までそれぞれ取り出す。次の例ではouter_list
は
電話番号:0123456789
=> outer_list
: ['1', '12', '123', 1234']
のようになる。二行目でデータを読み込み、三行目でouter
列要素がouter_list
に属している行をすべて取り出している。四行目でinner
列要素の文字列の長さが一番短いつまりouter
列要素が貪欲マッチする行を絞り込む。最後にinner
,outer
はもういらないので住所だけを取り出し、リストに変換しリターンする。
SearchJob class
class SearchJob():
def __init__(self, value):
self.phone_number = PhoneNumber(value)
def search(self):
if self.phone_number.check_format() is False:
raise ValueError('Error: Invalid format')
self.result = self.phone_number.get_address()
if self.result is None:
raise ValueError('Error: Not address found')
def finish(self):
return self.string_converter(self.result)
def string_converter(self, result):
return ''.join(f'{i+1}: {item}\n' for i, item in enumerate(result))
クリックイベントの関数内でこのインスタンスを生成する。次にsearch
メソッドを呼び出し、先程作ったcheck_format
メソッドがFalse
を返すなら、例外を上げて、住所が取得できない場合も例外を上げる。string_converter
メソッドでは、リストになった住所を次のような文字列に変換する。
1: 住所
2: 住所
...
クリック時に実行する関数
def clicked_track(self):
number = self.main_page_phone_number.text()
job = SearchJob(number)
try:
job.search()
result = job.finish()
self.show_result(number, result)
except ValueError as err:
result = str(err)
self.show_result(number, result)
def show_result(self, number, result):
self.result_page_title.setText(f'Result: {number}')
self.result_page_detail.setPlainText(result)
self.stackedwidget.setCurrentIndex(1)
try節内で例外が発生した場合は例外をshow_result
に渡してresult_page > defail
に表示する。それ以外の場合は住所を表示する。
最後にmain_page > track_btn
の設定にself.main_page_track_btn.clicked.connect(self.clicked_track)
を追加してクリックされたらこれを実行するようにする。
main.pyを作って動かす。
import sys
from PyQt5 import QtWidgets
from ui_class import UiMainWindow
app = QtWidgets.QApplication(sys.argv)
main_window = QtWidgets.QMainWindow()
ui = UiMainWindow()
ui.setup_ui(main_window)
main_window.show()
sys.exit(app.exec_())
お疲れ様でこれで完成です。下の電話番号のデータも忘れないでください!
イメージ
登場する電話番号は実在の人物や団体などとは関係ありません。
付録
ui_class.py
search_job.py
phone_numbers.csv (市外局番の一覧(総務省)(https://www.soumu.go.jp/main_content/000157336.doc) をCSVに変換して作成)
mk_pickle.py
import pandas as pd
pd.read_csv('phone_numbers.csv', names=['address', 'outer', 'inner']).to_pickle('phone_numbers_data.pickle')