【前回までお話】
最終形も想定せずに、作り始めた『っぽい』アプリケーション。 まずはテーブルを設置し、行と列を増やすためのボタンを設置した。
気を良くした私は、さらにアプリケーションの終了にショートカットキーを割り当てに挑戦し、見事に成功した。
しかし、プログラムを何度か実行しては閉じ、実行しては閉じ、を繰り返しながら悦に入っているとき、あることに気づくのであった。。
【今回の課題】
ショートカットを作って、ますます「っぽく」なってきた私のソースコード。
何度か起動と終了を繰り返して「おー」とか「へぇ」とか言っているとき、ふとセルに文字を書き、"ッターン" と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を加えるのです。
終わった〜
【外観】
プログラムを実行して、テーブルの行と列を適当に増やして文字を書いておきます。 そんでもって、⌘+Cを押し、適当なセルにフォーカスをずらして⌘+V! イケてるねぇ〜、あなた、めっさイケてるねぇ〜! (to be continued...)