PyQtとSQLiteで定期ツィートを登録するアプリを作る

#はじめに
Raspberry Piをサーバーとした定期ツィートbotを作成する過程で、ツィートを登録するのに便利なメモアプリを作成しました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">手始めにPython + pyQt + SQLite でツィートをデータベース登録するアプリを作成しました。<br><br>いずれコードを公開したいと思います。 <a href="https://t.co/E4opLdjhKa">pic.twitter.com/E4opLdjhKa</a></p>&mdash; ヒサン＠電子材料・デバイスbot (@Hisan_twi) <a href="https://twitter.com/Hisan_twi/status/1157978932240453632?ref_src=twsrc%5Etfw">August 4, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

#環境
- OSX: 10.14.6
- python: 3.7.4

使用ライブラリ:
- PyQt5
- SQLite3

事前にPyQt5およびSIPのインストールが必要です。

```bash:
pip3 install pyqt5
```
```bash:
pip3 install sip
```

##フォルダ構成
```text:ファイル構成
.
├── pyQt-MEMO.py
├── tweet-db.db
└── 画像
    └── 画像フォルダ
        └── 画像ファイル
```
`pyQt-MEMO.py`: アプリ本体
`tweet-db.db`: ツィートを登録するSQliteデータベースファイル
`画像`: 画像フォルダを格納するフォルダ
`画像`内には`画像フォルダ`（名称は自由）を複数配置することができます。`画像フォルダ`内に登録ツィートに添付する`画像ファイル`（名称は自由）を配置します。

#データベース
データベースファイルは[DB Browser for SQLite](https://sqlitebrowser.org)を使用して作成しました。
<img width="718" alt="データベース設定.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484445/5a7dc9b5-988b-877e-2130-0a10a04f5f67.png">


コマンドラインから行う場合は以下のコマンドで作成できます。

```SQL:
SQLite3 tweet-db.db
CREATE TABLE "tweettable" (
	"ID"	INTEGER NOT NULL UNIQUE,
	"title"	TEXT NOT NULL UNIQUE,
	"text"	TEXT NOT NULL UNIQUE,
	"image"	TEXT,
	PRIMARY KEY("ID")
);
```

#コード
本アプリのコードを以下に示します。

```python:pyQt-MEMO.py
import sys
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
import sip
import unicodedata
import sqlite3

dbname = "tweet-db.db"     #データベースファイル名
tablename = "tweettable"   #データベースのテーブル名
imagefolder = "画像/"       #画像フォルダのパス

#グローバル変数の宣言
textbuff = ""
imgpathbuff = ""
textidbuff = 1

class MainWindow(QWidget):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)

        #ウィンドウ設定
        self.setGeometry(600, 200, 700, 600)
        self.setFixedSize(700, 600)
        self.setWindowTitle('MEMOapl')

        #Textbox1（登録済み編集用）設定
        self.textbox1 = QTextEdit(self)
        self.textbox1.move(220, 20)
        self.textbox1.resize(460,200)

        self.textbox1.textChanged.connect(self.textconut1)       #テキストが変化した時に残り文字数をリストに表示する
        self.textbox1.setReadOnly(True)

        #Textbox2（新規登録用）設定
        self.textbox2 = QTextEdit(self)
        self.textbox2.move(220, 300)
        self.textbox2.resize(460,200)
        self.textbox2.textChanged.connect(self.textconut2)       #テキストが変化した時に残り文字数をリストに表示する

        #画像のパス用のTextline1（登録済み編集用）設定
        self.textline1 = QLineEdit(self)
        self.textline1.move(220, 225)
        self.textline1.resize(400, 20)
        self.textline1.setReadOnly(True)

        #画像のパス用のTextline1（登録済み編集用）設定
        self.textline2 = QLineEdit(self)
        self.textline2.move(220, 505)
        self.textline2.resize(400, 20)

        #Listbox設定
        self.listbox = QListWidget(self)
        self.listbox.move(20, 20)
        self.listbox.resize(180,505)
        list_title = list_from_db()             #データベースからタイトルリストを取得
        self.listbox.addItems(list_title)       #Listboxにタイトルリストを追加
        self.listbox.itemClicked.connect(self.list2textedit)

        #Textbox1の残り文字数を表示するlabel1設定
        self.label1 = QLabel(self)
        self.label1.move(225, 250)
        self.label1.setText("残り：140.0文字")
        self.label1.resize(200, 20)

        #Textbox2の残り文字数を表示するlabel2設定
        self.label2 = QLabel(self)
        self.label2.move(225, 530)
        self.label2.setText("残り：140.0文字")
        self.label2.resize(200, 20)

        #登録したテキストの数を表示するlabel3設定
        self.label3 = QLabel(self)
        self.label3.move(25, 530)
        self.label3.resize(180, 20)
        self.label3.setText("ツィート登録数：{0}".format(len(list_title)))

        #変更時のSQLiteのエラーを表示するlabel4設定
        self.label4 = QLabel(self)
        self.label4.move(225, 275)
        self.label4.resize(460, 20)

        #登録時のSQLiteのエラーを表示するlabel5設定
        self.label5 = QLabel(self)
        self.label5.move(225, 555)
        self.label5.resize(460, 20)

        #変更ボタン設定
        self.btnConv1 = QPushButton(self)
        self.btnConv1.move(625, 245)
        self.btnConv1.resize(60, 25)
        self.btnConv1.setText("変更")
        self.btnConv1.setDisabled(True)

        #取消ボタン設定
        self.btnConv2 = QPushButton(self)
        self.btnConv2.move(575, 245)
        self.btnConv2.resize(60, 25)
        self.btnConv2.setText("取消")
        self.btnConv2.setDisabled(True)

        #編集ボタン設定
        self.btnConv3 = QPushButton(self)
        self.btnConv3.move(525, 245)
        self.btnConv3.resize(60, 25)
        self.btnConv3.setText("編集")
        self.btnConv3.setDisabled(True)

        #登録ボタン設定
        self.btnConv4 = QPushButton(self)
        self.btnConv4.move(625, 525)
        self.btnConv4.resize(60, 25)
        self.btnConv4.setText("登録")
 
        #削除ボタン設定
        self.btnConv5 = QPushButton(self)
        self.btnConv5.move(475, 245)
        self.btnConv5.resize(60, 25)
        self.btnConv5.setText("削除")
        self.btnConv5.setDisabled(True)

        #挿入ボタン設定
        self.btnConv6 = QPushButton(self)
        self.btnConv6.move(575, 525)
        self.btnConv6.resize(60, 25)
        self.btnConv6.setText("挿入")
        self.btnConv6.setDisabled(True)

        #画像ファイルのファイルダイアログ参照ボタン(変更用)設定
        self.btnConv7 = QPushButton(self)
        self.btnConv7.move(620, 220)
        self.btnConv7.resize(60, 25)
        self.btnConv7.setText("参照")
        self.btnConv7.setDisabled(True)

        #画像ファイルのファイルダイアログ参照ボタン(変更用)設定
        self.btnConv8 = QPushButton(self)
        self.btnConv8.move(620, 500)
        self.btnConv8.resize(60, 25)
        self.btnConv8.setText("参照")

        self.btnConv1.clicked.connect(self.doRewrite)
        self.btnConv2.clicked.connect(self.doCancel)
        self.btnConv3.clicked.connect(self.enableEdit)
        self.btnConv4.clicked.connect(self.doWrite)
        self.btnConv5.clicked.connect(self.doDelete)
        self.btnConv6.clicked.connect(self.doInsert)
        self.btnConv7.clicked.connect(self.showDialog1)
        self.btnConv8.clicked.connect(self.showDialog2)

    #Textbox1に変更があった場合にlabel1に残り文字数表示
    def textconut1(self):
        s = self.textbox1.toPlainText()         #Textbox1の内容を取得
        count_s = 140 - get_east_asian_width_count(s)
        self.label1.setText("残り：{0}文字".format(count_s))

    #Textbox2に変更があった場合にlabel2に残り文字数表示
    def textconut2(self):
        s = self.textbox2.toPlainText()         #Textbox1の内容を取得
        count_s = 140 - get_east_asian_width_count(s)
        self.label2.setText("残り：{0}文字".format(count_s))

    #リストで選択したタイトルのテキストをTextbox1に表示する
    def list2textedit(self, item):
        global textidbuff
        self.btnConv3.setDisabled(False)        #編集ボタンをアクティブにする
        self.btnConv5.setDisabled(False)        #削除ボタンをアクティブにする
        self.btnConv6.setDisabled(False)        #挿入ボタンをアクティブにする
        text1, imgpath1, textid = text_from_db(item.text())
        textidbuff = textid                     #選択したテキストのIDを保存しておく
        self.textbox1.setText(text1)
        self.textline1.setText(imgpath1)
    
    #編集ボタンを押した時の動作
    def enableEdit(self):
        global textbuff
        global imgpathbuff
        self.label4.clear()
        textbuff = self.textbox1.toPlainText()  #textbuffに編集前の内容を保存
        imgpathbuff = self.textline1.text()     #imgpathbuffに編集前の画像のパスを保存
        self.textbox1.setReadOnly(False)        #Textbox1をアクティブにする
        self.textline1.setReadOnly(False)       #Listbox1をアクティブにする
        self.listbox.setDisabled(True)          #リストボックスを非アクティブに
        self.btnConv1.setDisabled(False)        #変更ボタンをアクティブに
        self.btnConv2.setDisabled(False)        #取消ボタンをアクティブに
        self.btnConv3.setDisabled(True)         #編集ボタンを非アクティブに
        self.btnConv4.setDisabled(True)         #登録ボタンを非アクティブに
        self.btnConv5.setDisabled(True)         #削除ボタンを非アクティブに
        self.btnConv6.setDisabled(True)         #挿入ボタンを非アクティブに
        self.btnConv7.setDisabled(False)        #参照ボタン2をアクティブに
    
    #取消ボタンを押した時の動作
    def doCancel(self):
        global textbuff
        global imgpathbuff
        self.label4.clear()
        self.textbox1.setText(textbuff)         #Textbox1の内容を編集前に戻す
        self.textline1.setText(imgpathbuff)
        self.textbox1.setReadOnly(True)         #Textbox1を非アクティブにする
        self.textline1.setReadOnly(True)        #Listbox1を非アクティブにする
        self.listbox.setDisabled(False)         #リストボックスをアクティブに戻す
        self.btnConv1.setDisabled(True)         #変更ボタンを非アクティブに
        self.btnConv2.setDisabled(True)         #取消ボタンを非アクティブに
        self.btnConv3.setDisabled(False)        #編集ボタンをアクティブに戻す
        self.btnConv4.setDisabled(False)        #登録ボタンをアクティブに戻す
        self.btnConv5.setDisabled(False)        #削除ボタンをアクティブに戻す
        self.btnConv6.setDisabled(False)        #挿入ボタンをアクティブに戻す
        self.btnConv7.setDisabled(True)         #参照ボタン2を非アクティブに

    #変更ボタンを押した時の動作
    def doRewrite(self):
        global textidbuff
        self.label4.clear()
        text = self.textbox1.toPlainText()      #Textbox1の内容を取得
        imgpath = self.textline1.text()
        try:
            text_update_db(textidbuff, text, imgpath)
        except sqlite3.Error:
            self.label4.setText('<p><font size="3" color="#ff0000">変更できませんでした</font></p>')    #変更できなかったときに警告する
        self.textbox1.setReadOnly(True)         #Textbox1を非アクティブにする
        self.textline1.setReadOnly(True)        #Listbox1を非アクティブにする
        self.listbox.setDisabled(False)         #リストボックスをアクティブに戻す
        self.btnConv1.setDisabled(True)         #変更ボタンを非アクティブに
        self.btnConv2.setDisabled(True)         #取消ボタンを非アクティブに
        self.btnConv3.setDisabled(False)        #編集ボタンをアクティブに戻す
        self.btnConv4.setDisabled(False)        #登録ボタンをアクティブに戻す
        self.btnConv5.setDisabled(False)        #削除ボタンをアクティブに戻す
        self.btnConv6.setDisabled(False)        #挿入ボタンをアクティブに戻す
        self.btnConv7.setDisabled(True)         #参照ボタン2を非アクティブに
        self.listbox.clear()
        list_title = list_from_db()         
        self.listbox.addItems(list_title)       #リストボックスを更新
        self.label3.setText("ツィート登録数：{0}".format(len(list_title)))
        self.btnConv6.setDisabled(True)         #挿入ボタンを非アクティブに

    #登録ボタンを押した時の動作
    def doWrite(self):
        self.label5.clear()                     #警告文を消去
        text = self.textbox2.toPlainText()      #Textbox2の内容を取得
        imgpath = self.textline2.text()
        try:
            text_write_db(text, imgpath)
        except sqlite3.Error:
            self.label5.setText('<p><font size="3" color="#ff0000">登録できませんでした</font></p>')    #登録できなかったときに警告する
            pass
        self.listbox.clear()
        list_title = list_from_db()         
        self.listbox.addItems(list_title)       #リストボックスを更新
        self.label3.setText("ツィート登録数：{0}".format(len(list_title)))
        self.textbox2.clear()
        self.textline2.clear()
        self.btnConv6.setDisabled(True)         #挿入ボタンを非アクティブに
        
    #削除ボタンを押した時の動作
    def doDelete(self):
        global textidbuff
        self.label4.clear()                     #警告文を消去
        qm = QMessageBox(self)                  #削除の確認のためメッセージボックスを表示
        ret = qm.question(self,'',"登録ツィートを削除しますか？", qm.Yes | qm.No)
        if ret == qm.Yes:
            text_delete_db(textidbuff)
            self.textbox1.clear()
            self.textline1.clear()
            self.textbox1.setReadOnly(True)     #Textbox1を非アクティブにする
            self.textline1.setReadOnly(True)    #Listbox1を非アクティブにする
            self.listbox.setDisabled(False)     #リストボックスをアクティブに戻す
            self.btnConv1.setDisabled(True)     #変更ボタンを非アクティブに
            self.btnConv2.setDisabled(True)     #取消ボタンを非アクティブに
            self.btnConv3.setDisabled(True)     #編集ボタンを非アクティブに
            self.btnConv5.setDisabled(True)     #削除ボタンを非アクティブに
            self.btnConv6.setDisabled(True)     #挿入ボタンを非アクティブに
            self.listbox.clear()
            list_title = list_from_db()         
            self.listbox.addItems(list_title)   #リストボックスを更新
            self.label3.setText("ツィート登録数：{0}".format(len(list_title)))

    #挿入ボタンを押した時の動作
    def doInsert(self):
        global textidbuff
        self.label5.clear()                     #警告文を消去
        text = self.textbox2.toPlainText()      #Textbox2の内容を取得
        imgpath = self.textline2.text()
        try:
            text_insert_db(textidbuff, text, imgpath)
        except sqlite3.Error:
            self.label5.setText('<p><font size="3" color="#ff0000">登録できませんでした</font></p>')    #登録できなかったときに警告する
            pass
        self.listbox.clear()
        list_title = list_from_db()
        self.listbox.addItems(list_title)       #リストボックスを更新
        self.label3.setText("ツィート登録数：{0}".format(len(list_title)))
        self.textbox2.clear()
        self.textline2.clear()
        self.btnConv6.setDisabled(True)         #挿入ボタンを非アクティブに

    #参照ボタンを押すとファイルダイアログが開く(TextLine1)
    def showDialog1(self):
        fname = QFileDialog.getOpenFileName(self, 'Open file', imagefolder)
        if fname[0]:
            imgpath = fname[0].split("/")
            self.textline1.setText(imgpath[-3] + "/" + imgpath[-2] + "/" + imgpath[-1])     #画像ファイルのパスを相対パスで表示

    #参照ボタンを押すとファイルダイアログが開く(TextLine2)
    def showDialog2(self):
        fname = QFileDialog.getOpenFileName(self, 'Open file', imagefolder)
        if fname[0]:
            imgpath = fname[0].split("/")
            self.textline2.setText(imgpath[-3] + "/" + imgpath[-2] + "/" + imgpath[-1])     #画像ファイルのパスを相対パスで表示

#全角文字を1、半角文字を0.5として文字数をカウントする関数
def get_east_asian_width_count(text):
    count = 0.0
    for c in text:
        if unicodedata.east_asian_width(c) in 'FWA':
            count += 1.0
        else:
            count += 0.5
    return count

#データベースからタイトルのリストを取り出す
def list_from_db():
    conn = sqlite3.connect(dbname)
    cur = conn.cursor()
    cur.execute(u"select title from {0} order by id asc".format(tablename))     #タイトルのカラムを抜き出しID昇順でソート
    tuplelist = cur.fetchall()
    title_list = [i[0] for i in tuplelist]
    conn.close()
    return title_list

#データベースから選んだタイトルのテキストを取り出す
def text_from_db(listword):
    conn = sqlite3.connect(dbname)
    cur = conn.cursor()
    listword = text2SQL(listword)
    cur.execute(u"select * from {0} where title = {1}".format(tablename, listword))   #タイトルが一致する行のテキストを選ぶ
    tupletext = cur.fetchone()
    text = tupletext[2]
    imgpath = tupletext[3]
    textid = tupletext[0]
    conn.close()
    return text, imgpath, textid

#データベースのテキストを更新する
def text_update_db(textid, text, imgpath):
    erroralert = False                  #エラーフラグ
    conn = sqlite3.connect(dbname)
    cur = conn.cursor()
    imgpath = text2SQL(imgpath)
    title = text.splitlines()
    if len(title) == 0: title.append("")
    title[0] = text2SQL(title[0])
    text = text2SQL(text)
    try:
        cur.execute(u"update '{0}' set text = {1}, title = {2}, image = {3} where id = '{4}'".format(tablename, text, title[0], imgpath, textid))       #指定したIDのテキストと画像のパスを更新、１行目をタイトルとして登録
    except sqlite3.Error:
        erroralert = True               #エラーフラグを立てる
        pass                            #一旦エラーをパス
    finally:
        conn.commit()
        conn.close()
    if erroralert: raise sqlite3.Error  #エラーフラグが立っていたら改めてエラーを出す

#テキストをデータベースに登録する
def text_write_db(text, imgpath):
    erroralert = False                  #エラーフラグ
    conn = sqlite3.connect(dbname)
    cur = conn.cursor()
    imgpath = text2SQL(imgpath)
    title = text.splitlines()
    if len(title) == 0: title.append("")
    title[0] = text2SQL(title[0])
    text = text2SQL(text)
    try:
        cur.execute(u"insert into '{0}'(title, text, image) values({1}, {2}, {3})".format(tablename, title[0], text, imgpath))       #タイトルとテキストと画像のパスを登録。IDは自動的に付加される
    except sqlite3.Error:
        erroralert = True               #エラーフラグを立てる
        pass                            #一旦エラーをパス
    finally:
        conn.commit()
        conn.close()
    if erroralert: raise sqlite3.Error  #エラーフラグが立っていたら改めてエラーを出す

#選択したテキストをデータベースから削除する
def text_delete_db(textid):
    conn = sqlite3.connect(dbname)
    cur = conn.cursor()
    cur.execute(u"delete from '{0}' where id = '{1}'".format(tablename, textid))
    conn.commit()
    conn.close()

#テキストをデータベースに挿入する
def text_insert_db(textid, text, imgpath):
    erroralert = False                  #エラーフラグ
    conn = sqlite3.connect(dbname)
    cur = conn.cursor()
    imgpath = text2SQL(imgpath)
    title = text.splitlines()
    if len(title) == 0: title.append("")
    title[0] = text2SQL(title[0])
    text = text2SQL(text)
    try:
        cur.execute(u"insert into '{0}'(title, text, image) values({1}, {2}, {3})".format(tablename, title[0], text, imgpath))    #insertできるかどうか試す
    except sqlite3.Error:
        erroralert = True               #失敗したらエラーフラグを立てる
        pass
    else:
        cur.execute(u"select max(id) from {0}".format(tablename))       #最大のIDを取得
        tupletext = cur.fetchone()
        maxid = tupletext[0]
        curid = maxid
        cur.execute(u"update '{0}' set id = '{1}' where id = '{2}'".format(tablename, curid+1, curid))          #先にinsertしたテキストのidを１つ増加する
        while curid > textid:
            cur.execute(u"select max(id) from {0} where id < '{1}'".format(tablename, curid))
            tupletext = cur.fetchone()
            curid = tupletext[0]
            cur.execute(u"update '{0}' set id = '{1}' where id = '{2}'".format(tablename, curid+1, curid))      #textid以降のid値を+1する
        cur.execute(u"update '{0}' set id = '{1}' where id = '{2}'".format(tablename, textid, maxid+1))         #最初にinsertしたテキストのidをtextidに設定する
    finally:
        conn.commit()
        conn.close()
    if erroralert: raise sqlite3.Error  #エラーフラグが立っていたら改めてエラーを出す


#文字列をSQLite仕様に変換する関数
def text2SQL(text):
    if text == "":
        text = "NULL"
    else:
        text = text.replace("'", "''")    #シングルコーテーションをエスケープ処理
        text = "'{}'".format(text)
    return text


def main():
    app = QApplication(sys.argv)
    main_window = MainWindow()
    main_window.show()
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()
```

##動作とコードの説明
このアプリの基本的な動作は参照ツィートの動画で説明しています。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">手始めにPython + pyQt + SQLite でツィートをデータベース登録するアプリを作成しました。<br><br>いずれコードを公開したいと思います。 <a href="https://t.co/E4opLdjhKa">pic.twitter.com/E4opLdjhKa</a></p>&mdash; ヒサン＠電子材料・デバイスbot (@Hisan_twi) <a href="https://twitter.com/Hisan_twi/status/1157978932240453632?ref_src=twsrc%5Etfw">August 4, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


以下では本アプリで使用したコードを簡単に解説します。
####ユーザーインターフェース
アプリのGUIはPyQt5を利用して作成しました。
PyQt5の基本的な使い方はhttps://www.sejuku.net/blog/75467 を参考にしました。

まず、以下のコードでウィンドウが生成されます。

```python:
import sys
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
import sip

class MainWindow(QWidget):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)

        self.setGeometry(600, 200, 700, 600)    #ウィンドウの初期位置とサイズを指定
        self.setFixedSize(700, 600)             #ウィンドウのサイズを固定
        self.setWindowTitle('MEMOapl')          #ウィンドウのタイトルと設定
        

def main():
    app = QApplication(sys.argv)
    main_window = MainWindow()
    main_window.show()
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()
```
<img width="512" alt="ウィンドウ生成実行結果.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484445/18d5a2f4-0f56-1110-5d9c-8ec18df22bb1.png">


`MainWindow`クラスのコンストラクタ(\_\_init__メソッド)以下に各オブジェクトに対応するインスタンスを生成することでウィンドウ内にオブジェクトを配置することができます。
`move`でオブジェクトの位置、`resize`でオブジェクトのサイズを指定することができます。
オブジェクトに文字列を表示する場合は`setText`で設定できます。

```python:
        #テキストボックス
        self.textbox = QTextEdit(self)          
        self.textbox.move(220, 20)
        self.textbox.resize(460,200)
        self.textbox.setText("テキストボックス")
        
        #１行テキストボックス
        self.textline = QLineEdit(self)         
        self.textline.move(220, 225)
        self.textline.resize(400, 20)
        self.textline.setText("１行テキストボックス")

        #ボタン
        self.btnConv = QPushButton(self)        
        self.btnConv.move(625, 245)
        self.btnConv.resize(60, 25)
        self.btnConv.setText("ボタン")

        #ラベル
        self.label = QLabel(self)               
        self.label.move(225, 250)
        self.label.resize(200, 20)
        self.label.setText("ラベル")
```
<img width="512" alt="オブジェクト配置実行結果.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484445/f6b085ca-90fc-ffe2-3355-63592ca1135c.png">


ボタンのクリック等のイベント処理を行う場合は`connect`を使用します。`connect`の引数にメソッドを渡してメソッドを定義することで動作を指定することができます。このとき、メッセージウィンドウやファイルダイアログを開くこともできます。
ファイルダイアログでは`getOpenFileName`で選択したファイルのパスを取得することができます。取得されるパスは絶対パスであるため、相対パスで取得したい場合は変換する必要があります。
（本アプリはメインPCで使用し、データベースへの登録が完了したらフォルダごとRaspberry Piへコピーするという運用を前提としています。このため、画像へのパスは相対パスである必要があるためこのような変換を行っています。）

```python:
class MainWindow(QWidget):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)

        self.btnConv = QPushButton(self)
        self.btnConv.move(625, 245)
        self.btnConv.resize(60, 25)
        self.btnConv.setText("ボタン")

        self.label = QLabel(self)
        self.label.move(225, 250)
        self.label.resize(200, 20)
        self.label.setText("ラベル")

        self.btnConv.clicked.connect(self.showDialog)    #ボタンをクリック時にファイルダイアログを開く

    #ボタンクリック時の動作
    def showDialog(self):
        imagefolder = "画像/"
        fname = QFileDialog.getOpenFileName(self, 'Open file', imagefolder)

        #ファイルを選択した時の動作
        if fname[0]:
            imgpath = fname[0].split("/")
            #相対パスへの変換
            self.label.setText(imgpath[-3] + "/" + imgpath[-2] + "/" + imgpath[-1])

```

リストボックスは`setText`が使用できません。アイテムを追加する場合は`addItem`または`addItems`を使用します。`addItems`を使用する場合はリストまたはタプルで渡します。

```python:
        list_item = "1"
        list_items = ["2", "3", "4"]

        self.listbox = QListWidget(self)
        self.listbox.move(20, 20)
        self.listbox.resize(180,505)
        self.listbox.addItem(list_item)         #単体でアイテムを追加
        self.listbox.addItems(list_items)       #リストでまとめて追加
```
<img width="512" alt="リストボックス設定実行結果.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484445/f58822d4-5169-6b9a-a183-d6cda3cc04e9.png">

```
    cur.execute(u"delete from '{0}' where id = '{1}'".format(tablename, textid))
```

####データへの登録
PythonでのSQLite3の基本的な使い方についてはhttps://qiita.com/mas9612/items/a881e9f14d20ee1c0703 を参考にしました。SQLite3のコマンドの書き方はhttps://www.dbonline.jp/sqlite/ を参考にしました。

データベースにデータを登録するにはINSERT文を使用します。コマンドではテーブル名およびデータの値はシングルコーテーション`'`で囲む必要があります。明示的に空の値を登録する場合は`'`をつけずに`NULL`にします。IDカラムはプライマリキーに設定していますので、値を書かなければ格納されている最大の値に+1した値が自動的に付与されます。

```python:
import sqlite3

dbname = "sample-db.db"
tablename = "tweettable"

def main():
    conn = sqlite3.connect(dbname)
    cur = conn.cursor()
    title = "'タイトル'"
    text = "'テキスト'"
    imgpath = "NULL"
    cur.execute(u"insert into '{0}'(title, text, image) values({1}, {2}, {3})".format(tablename, title, text, imgpath))
    conn.commit()
    conn.close()

if __name__ == '__main__':
    main()
```
<img width="434" alt="INSERT文実行結果.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484445/acf94c76-8765-36a4-1cf5-29e686983c71.png">

####データの変更
データベースのデータを変更するにはUPDATE文を使用します。

```python:
    textid = "1"
    title = "'変更タイトル'"
    text = "'変更テキスト'"
    imgpath = "NULL"
    cur.execute(u"update '{0}' set text = {1}, title = {2}, image = {3} where id = '{4}'".format(tablename, text, title, imgpath, textid)) 
```
<img width="434" alt="UPDATE文実行結果.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484445/17480cc3-928d-a816-1aee-aea820f0e9ba.png">

####データの削除
データベースのデータを変更するにはDELETE文を使用します。

```python:
    textid = "1"
    cur.execute(u"delete from '{0}' where id = '{1}'".format(tablename, textid))
```
<img width="434" alt="DELETE文実行結果.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484445/2a89c588-1351-ea1b-2172-9f5c5cfe98a3.png">

####データベースからのデータの取得
データベースからデータを取得する場ときはSELECT文を使用した後に`fetchone`(データが１行の場合)または`fetchall`（データが複数行の場合）を使用して取得します。このとき、値が１つであってもタプルに内包されて取得されるので、タプルを外す処理を入れた方が扱いやすくなります。

```python:
    cur.execute(u"insert into '{0}'(title, text, image) values('タイトル1', 'テキスト1', NULL)".format(tablename))
    cur.execute(u"insert into '{0}'(title, text, image) values('タイトル2', 'テキスト2', NULL)".format(tablename))
    cur.execute(u"insert into '{0}'(title, text, image) values('タイトル3', 'テキスト3', NULL)".format(tablename))

    cur.execute(u"select title from {0} order by id asc".format(tablename))     #タイトルのカラムを抜き出しID昇順でソート
    tuplelist = cur.fetchall()
    title_list = [i[0] for i in tuplelist]  #タプルを外す処理

    print(title_list)
```
> ['タイトル1', 'タイトル2', 'タイトル3']

####データの途中挿入
SQLite3にはデータを途中挿入するようなコマンドはありませんので、処理を作る必要があります。色々な方法が考えられますが、本アプリでは挿入する行以降のIDを+1する方法で処理を行いました。

```python:
    textid = 2      #このIDの行に途中挿入する
    title = "'挿入タイトル'"
    text = "'挿入テキスト'"
    imgpath = "NULL"
    cur.execute(u"insert into '{0}'(title, text, image) values({1}, {2}, {3})".format(tablename, title, text, imgpath))
    cur.execute(u"select max(id) from {0}".format(tablename))   #最大のIDを取得
    tupletext = cur.fetchone()
    maxid = tupletext[0]
    curid = maxid
    cur.execute(u"update '{0}' set id = '{1}' where id = '{2}'".format(tablename, curid+1, curid))          #先にinsertしたテキストのidを１つ増加する
    while curid > textid:
        cur.execute(u"select max(id) from {0} where id < '{1}'".format(tablename, curid))
        tupletext = cur.fetchone()
        curid = tupletext[0]
        cur.execute(u"update '{0}' set id = '{1}' where id = '{2}'".format(tablename, curid+1, curid))      #textid以降のid値を+1する
    cur.execute(u"update '{0}' set id = '{1}' where id = '{2}'".format(tablename, textid, maxid+1))         #最初にinsertしたテキストのidをtextidに設定する
```
実行前
<img width="434" alt="挿入実行前.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484445/3aa952cc-79db-aa96-5107-ae936eeb283b.png">
実行後
<img width="434" alt="挿入実行結果.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484445/28656d81-1b44-83b1-57d1-df1bb414ec65.png">

####文字数のカウント
2019年8月現在のTwitterでは、ツィートの文字数制限は[日本語、中国語、韓国語は140文字で、それ以外は280文字](https://blog.twitter.com/official/ja_jp/topics/product/2017/Cramming-Tweeting-Made-Easier.html)という仕様になっています。
このアプリでは日本語を基準として全角文字を1字、半角文字を0.5字としてカウントすることでTwitterの仕様に近くなるようにしています。(TwitterではURLを短縮して半角23文字として扱う機能がありますが、本アプリでは実装できませんでした。)
(コードはhttps://note.nkmk.me/python-unicodedata-east-asian-width-count/ から引用しました。)

```python:
import unicodedata

def get_east_asian_width_count(text):
    count = 0.0
    for c in text:
        if unicodedata.east_asian_width(c) in 'FWA':
            count += 1.0
        else:
            count += 0.5
    return count
```
#おわりに
PyQt5とSQLite3を使用してデータベースファイルに定期ツィート登録するアプリを作ることができました。

当方、プログラミングは不慣れなため、コードや説明に自信がない部分もあります。
ご意見やご指摘がございましたらコメントいただければ幸いです。

次回、このアプリでツィート登録したデータベースファイルを使用してRaspberry Piから定期ツィートを行う方法を紹介する予定です。