0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

電話番号トラッカーのアプリを作る - Python3 / PyQt5

Last updated at Posted at 2020-12-04

追記: 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_qlabelQLabelを設定する共通のメソッド。デフォルトのテキストカラーは#2e2e2eとしている。

setup_fontQLabel等のフォントサイズを設定する。

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)

index0.png

index1.png

中身

###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_())

お疲れ様でこれで完成です。下の電話番号のデータも忘れないでください!

イメージ
Peek 2020-12-04 22-02.gif
登場する電話番号は実在の人物や団体などとは関係ありません。

付録

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')
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?