質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.50%
Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

Qt

QtはGUIプログラムの開発で広く使われているクロスプラットフォーム開発のフレームワークです。

Q&A

解決済

2回答

2093閲覧

PyQt5でのキーボードイベントを手動ではなく自動で起こし、かつ入力キーと押された時間を記録したい

kasanegi

総合スコア20

Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

Qt

QtはGUIプログラムの開発で広く使われているクロスプラットフォーム開発のフレームワークです。

0グッド

0クリップ

投稿2018/10/02 12:15

編集2018/10/02 12:29

前提・実現したいこと

状況: 
こちらのサイト(http://zetcode.com/gui/pyqt5/tetris/)のコードを参考にTetrisを作成。動作確認済み。
少し改造し、あるゲーム中に落ちてきたピースの種類、順番をpickleで記録し、再現できるようにした。

実現したいこと: 
次は、ゲーム環境の記録に留まらず、プレイ自体の記録に進みたい。
ピースの種類、順序の記録によって、ゲーム環境を再現しながら、
①ゲーム中に押されたキーと、押された時のゲーム開始からの時間の記録、保存
②保存された(key, timing)データに基づき、自動再現プレイ 
を実現したい。

ご教授いただきたいこと:
PyQt5初心者なので、見落としている有用なメソッドなどあると思います。
それ以外にも、別パッケージなどで有用なものをご存知でしたら、お教えくださると幸いです。
どうぞよろしくお願いします。

(質問ではなくなりますが参考までに)現時点までの改造

*すべてのコードを掲載したいのですが、文字数制限があるため、改造後のファイルから主に変更を加えたBoardクラスのnewPiece関数にとどめます。ソースをご覧いただくか、不明な点はぜひ質問してください!
*コメントについてはmarkdownのルールに当たらないようにしていた名残で、 ###### を用いています
*途中理解していない行に変なコメントをつけておりますが、見逃していただけるとありがたいです( ´∀` )

特にnewPieceメソッド内にて、ゲーム環境(ピースの流れ)の記録、再現のためのコードを追加しています。
コンストラクタにパラメータを追加することで、記録、再現のon、offをしています。

python3

1 def newPiece(self): 2 """create a new piece""" 3 4 self.curPiece = Shape() 5 6 ###### if the game is being replayed, provide the same shapes in the same order. 7 ###### when all the stored shapes are replayed then continue and turn to provide random shapes as usual 8 if self.replay is not None: 9 if len(self.replay) > 0: 10 self.curPiece.setShape(self.replay.popleft()) 11 else: 12 self.curPiece.setRandomShape() 13 else: 14 self.curPiece.setRandomShape() 15 16 self.curX = Board.BoardWidth // 2 + 1 # ######## don't understand following 2 lines 17 self.curY = Board.BoardHeight - 1 + self.curPiece.minY() 18 19 ###### record shapes in deque 20 if self.isRecorded: 21 self.record.append(self.curPiece.shape()) 22 23 if not self.tryMove(self.curPiece, self.curX, self.curY): 24 self.curPiece.setShape(Tetrominoe.NoShape) 25 self.timer.stop() 26 self.isStarted = False 27 self.msg2Statusbar.emit('--Game Over--') 28 29 ###### if the game is recorded, pickle the list of shapes we've seen befor the game is over 30 if self.isRecorded: 31 now = str(datetime.datetime.now()) 32 now = now.replace(':', '-') 33 now = now.replace(' ', '_') 34 now = now.replace('.', '_') 35 36 with open(f'tetris{self.numLinesRemoved}_{now}.pickle', 'wb') as f: 37 pickle.dump(self.record, f)

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答2

0

***注意***
自己解決ではありませんが、解決策をまとめるために利用しています。
インスピレーションはtiitoi様からいただきました。
tiitoi様、ありがとうございます。

解決したこと 

PyQt5における、
①キー入力の保存(queueにkeyオブジェクトを格納)
→ここからpickleを使って、ファイルに長期保存することもできる。
②キー入力の再生(イベントを強制的に発生させる方法があった)

未解決事項

①各キー入力発生時のゲーム開始からの時間をどうとるか。
→タイマーをゲームごとに、新しく用意するなど、実際どうとでもなる。
入力タイミングまで再現したキー入力の再生。
→まず保存の段階で、(timing, key)形式で保存し、再生においてはゲーム内時刻がtimingに達した瞬間にkey入力イベントを発生させればいいはず。

キー入力のRecord, Replayができるプログラム

python3

1import sys 2from PyQt5.QtWidgets import * 3from PyQt5.QtGui import * 4from PyQt5.QtCore import * 5from collections import deque 6 7 8class MainWindow(QWidget): 9 def __init__(self): 10 super().__init__() 11 12 self.isRecoding = False 13 self.recocrd = deque() 14 15 self.initUI() 16 17 def initUI(self): 18 self.setGeometry(100, 100, 640, 480) 19 20 initBtn = QPushButton('init') 21 initBtn.clicked.connect(self.initBtnClicked) 22 23 recordBtn = QPushButton('record') 24 recordBtn.clicked.connect(self.startRecording) 25 26 replayBtn = QPushButton("replay") 27 replayBtn.clicked.connect(self.startReplay) 28 29 hbox = QHBoxLayout() 30 hbox.addWidget(replayBtn) 31 hbox.addWidget(recordBtn) 32 33 vbox = QVBoxLayout() 34 vbox.addLayout(hbox) 35 vbox.addWidget(initBtn) 36 37 self.setLayout(vbox) 38 39 self.show() 40 41 def keyPressEvent(self, event): 42 '''キーが押された場合に呼ばれる。''' 43 44 key = QKeySequence(event.key()).toString() 45 print(key) 46 47 # record 48 if self.isRecoding: 49 self.record.append(event.key()) 50 51 super(MainWindow, self).keyPressEvent(event) 52 53 def initBtnClicked(self): 54 '''recordをやめて、キューも初期化''' 55 if self.isRecoding == True: 56 self.isRecoding = False 57 self.record = deque() 58 59 def startRecording(self): 60 '''recordを開始し、新しいキューを作成''' 61 self.isRecoding = True 62 self.record = deque() 63 64 print('---RECORDING---') 65 66 def startReplay(self): 67 '''Push Button が押されると呼ばれる。record済みの一連のキーを押す動作を再生する。''' 68 print('-----REPLAY-----') 69 while len(self.record) > 0: 70 event = QKeyEvent(QEvent.KeyPress, self.record.popleft(), Qt.NoModifier) 71 QCoreApplication.postEvent(self, event) 72 73 74if __name__ == '__main__': 75 app = QApplication(sys.argv) 76 ex = MainWindow() 77 sys.exit(app.exec_()) 78

投稿2018/10/02 16:56

編集2018/10/02 17:05
kasanegi

総合スコア20

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

tiitoi

2018/10/02 17:08 編集

ゲーム開始時から時刻をカウントしたいのであれば、QTimer クラスが利用できます。 http://doc.qt.io/archives/qt-5.5/qtimer.html 一応ストップウォッチのようにスタート、ストップができ、時間経過も図れます。
kasanegi

2018/10/02 17:14

なるほど。 QTimerで、ある時刻になると発生するイベント、といったものは作れますでしょうか?
tiitoi

2018/10/02 17:28 編集

Qtimer クラスの start() で開始するときに、時間を指定すると、指定時間経過後に timeout シグナルが発行されます。(回答に追記しました。) それを利用すれば、ある時刻になると発生するイベントを発生させられます。 別のアプローチとして、下記サイトのように一定間隔ごとに経過時間を調べて、登録しておいたタイミングで処理を行うという方法も考えられます。 http://qt-log.open-memo.net/sub/event--timer-event.html
kasanegi

2018/10/02 21:14

テトリスの再現プレイにおいては、2つの時間に関する処理 ①一定時間ごとに、ピースを1つ落とす →timeoutで可能 ②記録された経過時間において、記録されたキー入力を行う をする必要があるのですが、 このようなとき、①と②の両立は可能でしょうか?
kasanegi

2018/10/02 21:16

各キー入力のデータは(ゲーム開始からの経過時間, key)形式でひとつのrecordキューに積もうと思っています
tiitoi

2018/10/03 03:41

今思ったのですが、ブロックは常に落下するので、ラグ等で回転するタイミングが1マスでもずれたら結果が変わってしまいますよね? キー入力を記録して再生するのでなく、落下してきたブロックの種類やブロッグがどの位置にきたとき、回転したとかの情報を記録していったようが確実な気がしました。 自分が実装するのであれば、そうしますね。
kasanegi

2018/10/03 06:59

最終的には強化学習で自動プレイさせたいと考えているので、シンプルに人間と同じ操作方法で自動操作がまずは出来るようにとこの方針でやっていましたが、確かにタイミングでの解決方法は誤差が発生しやすいようです。 今後は分かりませんが、今のところキー操作はまだ残しておきたいので、tiitoiさんが教えてくださった方法との折半で、キーイベント発生時に盤面のsnap shotをtimingの代わりに保存する方法を試してみたいと思います。
kasanegi

2018/10/03 07:00

何度も回答ありがとうございます!
guest

0

ベストアンサー

キーの入力を拾う。

イベントを拾いたいウィジェットクラスで def keyPressEvent(self, event) をオーバーロードします。

キーのイベントを再生する。

QCoreApplication.postEvent(イベントを送信するウィジェット、イベント) で指定したウィジェットにイベントを送信します。
キーイベントは QKeyEvent クラスが表します。

QKeyEvent(QEvent.KeyPress, Qt.Key_Enter, Qt.NoModifier)
  • 第1引数: イベントの種類、キー入力の場合 QEvent.KeyPress
  • 第2引数: 押されたキーの種類、Qt.Key_Enter は Enter キー
  • 第3引数: 押された修飾キーの種類、例えば Ctrl を押しながらキーを押したなど

サンプルコード

キーを押すと、押下したキー名が表示されます。
ボタンをクリックすると、Enter キーを押下する動作を再生します。

import sys from PyQt5.QtWidgets import * from PyQt5.QtGui import * from PyQt5.QtCore import * class MainWindow(QWidget): def __init__(self): super().__init__() self.initUI() def initUI(self): self.setGeometry(100, 100, 640, 480) pushButton = QPushButton("Click") pushButton.clicked.connect(self.pushButtionClicked) layout = QVBoxLayout() layout.addWidget(pushButton) self.setLayout(layout) self.show() def keyPressEvent(self, event): '''キーが押された場合に呼ばれる。 ''' key = QKeySequence(event.key()).toString() print(key) # 押されたキー名を表示する。 super(MainWindow, self).keyPressEvent(event) def pushButtionClicked(self): '''Push Button が押されると呼ばれる。Enter キーを押す動作を再生する。 ''' event = QKeyEvent(QEvent.KeyPress, Qt.Key_Enter, Qt.NoModifier) QCoreApplication.postEvent(self, event) if __name__ == '__main__': app = QApplication(sys.argv) ex = MainWindow() sys.exit(app.exec_())

一定時間経過したら関数を呼ぶ

QTimer.start(ミリ秒) と開始すると、指定時間経過後に timeout シグナルが発行されるので、シグナルスロットで呼び出す関数を登録しておけば、一定時間後になにか処理するということが実現できます。

QTimer

python

1import sys 2from PyQt5.QtWidgets import * 3from PyQt5.QtGui import * 4from PyQt5.QtCore import * 5 6class MainWindow(QWidget): 7 def __init__(self): 8 super().__init__() 9 self.initUI() 10 11 def initUI(self): 12 self.setGeometry(100, 100, 640, 480) 13 14 pushButton = QPushButton("Click") 15 pushButton.clicked.connect(self.pushButtionClicked) 16 17 layout = QVBoxLayout() 18 layout.addWidget(pushButton) 19 self.setLayout(layout) 20 21 self.show() 22 23 def on_timeout(self,): 24 '''タイムアウトした場合に呼ばれる。 25 ''' 26 QMessageBox.about(self, "Title", "Message") 27 28 def pushButtionClicked(self): 29 self.timer = QTimer(self) 30 self.timer.timeout.connect(self.on_timeout) 31 self.timer.start(3000) 32 33if __name__ == '__main__': 34 app = QApplication(sys.argv) 35 ex = MainWindow() 36 sys.exit(app.exec_())

投稿2018/10/02 16:06

編集2018/10/02 17:24
tiitoi

総合スコア21954

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

kasanegi

2018/10/02 16:16

回答ありがとうございます! 早速試させていただきます。
kasanegi

2018/10/02 17:00 編集

(全然自己解決ではないです。tiitoiさんのおかげです。がほかに書くところがなかったので...)自己解決欄に、tiitoiさんのコードを流用させてもらって、自分がやりたかったことをやるコードを載せました。 もしよろしければご確認ください。 本当にありがとうございました!
tiitoi

2018/10/02 17:06

解決されたようでよかったです。 ちなみに Qt の情報は、PyQt5 より元の C++ の Qt の情報量が圧倒的に多いので、調べる際はそちらの情報も利用できると思います。 C++ がわからなくても、関数の使い方等は全く同じなので、そのまま PyQt5 のコードにも利用できます。 (例: C++版 Qt::Key_Enter → Python版 Qt.Key_Enter)
kasanegi

2018/10/02 17:10

ちなみにこのようなPyQt5のコアな情報はどこから得ていらっしゃるのでしょうか?
kasanegi

2018/10/02 17:11

reloadする前でした... 今後はそのように調べていきたいと思います! ありがとうございました。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.50%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問