いいかげん日記

思いついたことをただひたすら書き殴るいいかげんな日記です。

【Python × PyQt5】 google spreadsheet viewerを作る。(その6:コピー&ペースト機能の実装)

前回までお話】

最終形も想定せずに、作り始めた『っぽい』アプリケーション。 まずはテーブルを設置し、行と列を増やすためのボタンを設置した。

気を良くした私は、さらにアプリケーションの終了にショートカットキーを割り当てに挑戦し、見事に成功した。

しかし、プログラムを何度か実行しては閉じ、実行しては閉じ、を繰り返しながら悦に入っているとき、あることに気づくのであった。。

【今回の課題】

ショートカットを作って、ますます「っぽく」なってきた私のソースコード

何度か起動と終了を繰り返して「おー」とか「へぇ」とか言っているとき、ふとセルに文字を書き、"ッターン" とEnterを弾いてから⌘+Cを押してみた。 そのまま別のセルに移って⌘+Vを押す。


「...あれ?コピペができない。」


これはイケてない。イケてないですぞ!



ということで、今回の課題は「コピー&ペースト」です。

【コード】

前回のコードにコピペ機能を追加します。

import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *

class MyMainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        QWidget.__init__(self, *args, **kwargs)

        quitShortcut = QShortcut(QKeySequence("Esc"), self)
        quitShortcut.activated.connect(self.close)

class SpreadSheetWidget(QWidget):
    def __init__(self, row, col, parent=None):
        QWidget.__init__(self, parent=parent)
        self.setup_ui(row, col)

        copyShortcut = QShortcut(QKeySequence("Ctrl+C"), self)
        copyShortcut.activated.connect(self.copy_cell)

        pasteShortcut = QShortcut(QKeySequence("Ctrl+V"), self)
        pasteShortcut.activated.connect(self.paste_cell)

        self.clipboard = QApplication.clipboard()

    def setup_ui(self, row, col):
        self.spreadsheet = QTableWidget(row, col)

        layout = QVBoxLayout()
        layout.addWidget(self.spreadsheet)

        self.setLayout(layout)

    def add_row(self):
        self.row_position = self.spreadsheet.rowCount()
        self.spreadsheet.insertRow(self.row_position)

    def add_col(self):
        self.col_position = self.spreadsheet.columnCount()
        self.spreadsheet.insertColumn(self.col_position)

    def copy_cell(self):
        self.selected_cell = self.spreadsheet.selectedRanges()
        s = ""
        for r in range(self.selected_cell[0].topRow(),self.selected_cell[0].bottomRow()+1):
            for c in range(self.selected_cell[0].leftColumn(),self.selected_cell[0].rightColumn()+1):
                try:
                    s += str(self.spreadsheet.item(r,c).text()) + "\t"
                except AttributeError:
                    s += "\t"
            s = s[:-1] + "\n" #eliminate last '\t'
        self.clipboard.setText(s)

    def paste_cell(self):
        self.selected = self.spreadsheet.selectedRanges()
        first_row = self.selected[0].topRow()
        first_col = self.selected[0].leftColumn()
        for r, row in enumerate(self.clipboard.text().split("\n")):
            if r == len(self.clipboard.text().split("\n"))-1:
                break
            for c, text in enumerate(row.split("\t")):
                self.spreadsheet.setItem(first_row+r,first_col+c, QTableWidgetItem(text))

class AddButtonWidget(QWidget):
    def __init__(self, parent=None):
        QWidget.__init__(self, parent=parent)
        self.setup_ui()

    def setup_ui(self):
        self.add_button_row = QPushButton('Add row', parent=self)
        self.add_button_col = QPushButton('Add col', parent=self)

        layout = QHBoxLayout()
        layout.addWidget(self.add_button_row)
        layout.addWidget(self.add_button_col)

        self.setLayout(layout)

def main():
    app = QApplication(sys.argv)

    main_window = MyMainWindow()
    panel = QWidget()
    layout = QVBoxLayout()
    spreadsheet_widget = SpreadSheetWidget(3,4,parent=panel)
    add_button_widget = AddButtonWidget(parent=panel)

    layout.addWidget(spreadsheet_widget)
    layout.addWidget(add_button_widget)

    panel.setLayout(layout)
    main_window.setCentralWidget(panel)

    add_button_widget.add_button_row.clicked.connect(spreadsheet_widget.add_row)
    add_button_widget.add_button_col.clicked.connect(spreadsheet_widget.add_col)

    main_window.show()
    app.exec_()

if __name__ == '__main__':
    main()


----私なりの説明----

ここらへんから、「どのクラスにメソッドを入れるか」問題が発生します。

いま、クラスは 'MyMainWindow','SpreadSheetWidget','AddButtonWidget' の3つ。

今回、私がやりたかったことは、'SpreadSheetWidget' クラスのインスタンスであるウィンドウ内のテーブルでコピペを可能にすること。

ということで、ここは素直に 'SpreadSheetWidget' にコピペの機能を入れることにしました。

        copyShortcut = QShortcut(QKeySequence("Ctrl+C"), self)
        copyShortcut.activated.connect(self.copy_cell)

        pasteShortcut = QShortcut(QKeySequence("Ctrl+V"), self)
        pasteShortcut.activated.connect(self.paste_cell)

まずは、 'SpreadSheetWidget' のコンストラクタに上のコードを差し込みます。

これは前回と同じですね。

'Ctrl+C'が押される → 「Ctrl+Cは…、ショートカットだ!」 → この(self)クラスの 'copy_cell' メソッドを呼び出す

'Ctrl+V'が押される → 「Ctrl+Vは…、ショートカットだ!」 → この(self)クラスの 'paste_cell' メソッドを呼び出す

ってな具合です。

で、'copy_cell' メソッドってどんなもんだっけ?っていうことを見ると、、、

    def copy_cell(self):
        self.selected_cell = self.spreadsheet.selectedRanges()
        s = ""
        for r in range(self.selected_cell[0].topRow(),self.selected_cell[0].bottomRow()+1):
            for c in range(self.selected_cell[0].leftColumn(),self.selected_cell[0].rightColumn()+1):
                try:
                    s += str(self.spreadsheet.item(r,c).text()) + "\t"
                except AttributeError:
                    s += "\t"
            s = s[:-1] + "\n" #eliminate last '\t'
        self.clipboard.setText(s)

1行目はいいとして、、

2行目は、'spreadsheet' の中にある 'selectedRanges' というメソッドを使って、選択中のセルの情報を取り出して、それに 'selected_cell' という名前をつけています。

そんでもって、3行目で 's' なる箱を用意しておいて、

4行目からはfor文でループをぶん回す。このときのループ回数は、<選択された行数>回 になってます。

なぜに 'range()' の第2引数に+1があるかというと?・・・これはどうやら 'range()' 関数の仕様のしわざですな。(ココらへんを読むと理解できるかも)

5行目にもfor文があって、<選択された列数>回のループをぶん回してます。

6行目からようやく実際の処理になる、、かと思いきや、try - except文になるみたいです。

で、7行目にr行c列のアイテムに入っている文字列+'\t' を 's' なる箱に追加する、という処理を試してみて、

もし文字がなければ(8行目)、 '\t' だけ入れる(9行目)。


で、2重のfor文を抜けたあとは、最後の '\t' を消して、 '\n' を入れる(10行目)。(ただ、あとの処理を考えると、この '\n' はなくてもいいな。)

最後に、この(self)クラスの 'clipboard' に 's' なる箱の中身を入れておしまい。



さて、お次は 'paste_cell' メソッドの定義。

    def paste_cell(self):
        self.selected = self.spreadsheet.selectedRanges()
        first_row = self.selected[0].topRow()
        first_col = self.selected[0].leftColumn()
        for r, row in enumerate(self.clipboard.text().split("\n")):
            if r == len(self.clipboard.text().split("\n"))-1:
                break
            for c, text in enumerate(row.split("\t")):
                self.spreadsheet.setItem(first_row+r,first_col+c, QTableWidgetItem(text))

まず、この(self)クラスの 'spreadsheet' の選択されたセルの範囲に 'selected' という名前をつけています(2行目)。

その後、'selected' の最初の要素([0])の先頭行に対して 'first_row' という名前をつけています(3行目)。

同様にして、'selected' の左端の列に 'first_col' なる名前をつけています(4行目)。

その後、この(self)クラスの 'clipboard' に入っているテキスト(text())を '\n' を境にして分割(split("\n"))した配列の要素数だけforループを回す(5行目)。


もし、ループ回数が、<その配列の要素数-1>ならば、for文を抜ける(break)(6、7行目)。

...なんで "-1" なのさ?っていうと…

「ちまちまと調整したらこうなった」っていう話なんですが、まぁまぁ、細かいことはいいじゃないの。


if文に引っかからなければ、rowの中身を '\n' で分割(split("\n"))した配列の要素数だけforループを回す(8行目)。


そして、ようやくペーストの処理が登場します(9行目)。

ここでは、この(self)クラスの 'spreadsheet' に要素を加える(setItem())。

このとき、どこに要素を加えるかというと、<first_row+r>行<first_col+c>列です。

んで、何を加えるかというと、'text' が入ったQTableWidgetItemを加えるのです。


終わった〜

【外観】

プログラムを実行して、テーブルの行と列を適当に増やして文字を書いておきます。 f:id:theta_proto:20180901110335j:plain そんでもって、⌘+Cを押し、適当なセルにフォーカスをずらして⌘+V! f:id:theta_proto:20180901110543j:plain イケてるねぇ〜、あなた、めっさイケてるねぇ〜!   (to be continued...)