この前の記事でPyQtの基本的な使い方について書きましたが、まだドラッグ&ドロップについて触れていません。これはかなり複雑でわかりにくいところが多いことだからです。
しかし使ってみたら色々面白いと思います。なのでここでは自分がドラッグ&ドロップを使ってみて今わかったことを纏めてみます。
ここで使うのはPyQt6ですが、PyQt5との違いがある場合は指摘します。その違いについてもっと詳しくは前回の記事で。
外の物をGUIへドラッグして入れてみる
まずは外からGUIウィンドウの中に入れ込む方法です。
簡単に言うと、外からの物をドラッグでウィジェットの中に入れるためにはそのウィジェットを.setAcceptDrops(True)
にする必要があります。
そしてドラッグが入ってきたら処理させたいことはdragEnterEvent
メソッドで指定します。
例えば、何かをドラッグして入れてみたらその入れた物のフォーマットを出力するようなウィンドウを作ってみます。
import sys
from PyQt6.QtWidgets import QApplication,QWidget
class Madoka(QWidget):
def __init__(self):
super().__init__()
self.setAcceptDrops(True) # ドラッグを受けられるようにする
def dragEnterEvent(self,e): # ここでeはイベントのオブジェクトになる
m = e.mimeData() # mimeデータ取得
# 持っているデータのフォーマットを調べる
print(m.hasText(),m.hasHtml(),m.hasImage(),m.hasUrls(),m.hasColor())
qAp = QApplication(sys.argv)
mado = Madoka()
mado.show()
qAp.exec()
実行したら空っぽのウィンドウが現れます。そのウィンドウの中に何かをドラッグして入れてみましょう。
例えばウェブサイトからの文字を入れたらTrue True False False False
が出ます。
もし画像などをドラッグして入れてみたらTrue True True True False
となります。
ドラッグできる物を色々入れてみて違いを比較してみてください。
ここでmimeData
というのはドラッグと共に入れられたデータのことです。一度のドラッグでは色んなフォーマットのデータを運ぶことができます。各フォーマットはそれぞれのメソッドで、あるかどうかを確認したり、データを取得したり、新しいデータを入れ込んだりすることができます。
主に5つあります。
データのフォーマット | 存在のチェック | データを取り出す | データを入れ込む |
---|---|---|---|
普通の文字列 | .hasText() | .text() | .setText() |
htmlコード | .hasHtml() | .html() | .setHtml() |
ファイルやウェブサイトのURL | .hasUrls() | .urls() | .setUrls() |
画像 | .hasImage() | .imageData() | .setImageData() |
色 | .hasColor() | .colorData() | .setColorData() |
上述のコードはただhas
から始まるメソッドでそのフォーマットのデータを持っているかどうか確認しただけです。
次からはそのデータを取得して使う方法について紹介します。
文字列をドラッグで入れてウィンドウの中に表示させる
まずは文字列のデータを入れる場合から始めます。例えばどこからの文字列をドラッグしてウィンドウに入ったらその文字列をウィンドウの中で表示させたいなら、こうします。
import sys
from PyQt6.QtWidgets import QApplication,QWidget,QLabel,QVBoxLayout
class Madoka(QWidget):
def __init__(self):
super().__init__()
self.setAcceptDrops(True)
self.vbl = QVBoxLayout()
self.setLayout(self.vbl)
def dragEnterEvent(self,e):
m = e.mimeData()
if(m.hasText()): # 文字列があるかどうか確認
# その文字列の書くQLabelを作ってレイアウトに入れる
label = QLabel(e.mimeData().text())
self.vbl.addWidget(label)
qAp = QApplication(sys.argv)
mado = Madoka()
mado.show()
qAp.exec()
こうやって空っぽなウィンドウが出てきて、何かをドラッグしてマウスが入った途端、文字列がウィンドウの中に表示されます。
例えばhttps://qiita.com/phyblas ページからの文字列を入れてみたらこうなります。
もしまた入れたら追加されます。
ここで文字列があるかどうかをチェックするために.hasText()
を使います。文字列データがあると確認してからウィンドウの中のQLabel
に入れて表示させるのです。
acceptとdropEvent
上述の例ではただdragEnterEvent
だけ使いました。つまりドラッグした状態でマウスがウィンドウに入った時に発動するのです。まだドロップを使っていません。
しかしほとんどの場合マウスを解放した時(つまり、ドロップすることで)発動させたいはずですね。そのためにもっと少し工夫が必要です。
方法はdragEnterEvent
の中でaccept()
メソッドを使って、dropEvent
でやらせたいことを定義するのです。
さっきのコードをちょっと変更します。
import sys
from PyQt6.QtWidgets import QApplication,QWidget,QLabel,QVBoxLayout
class Madoka(QWidget):
def __init__(self):
super().__init__()
self.setAcceptDrops(True)
self.vbl = QVBoxLayout()
self.setLayout(self.vbl)
def dragEnterEvent(self,e):
if(e.mimeData().hasText()):
e.accept() # 文字列がある場合受け取る
def dropEvent(self,e): # マウスが解放された後文字列を入れる
label = QLabel(e.mimeData().text())
self.vbl.addWidget(label)
qAp = QApplication(sys.argv)
mado = Madoka()
mado.show()
qAp.exec()
さっきと同じようなウィンドウが出ますが、今回はマウスを解放された時に文字が現れます。
ここでまずはdragEnterEvent
で入れたいものの条件を決めるのです。入れたいものだと確認してから.accept()を呼び出します。
dropEvent
がdragEnterEvent
の中で.accept()
が呼び出された後にその中でマウスを解放した時に発動するのです。
もしdragEnterEvent
の中で.accept()
が呼び出されなかったら、マウスを解放してもdropEvent
は起きません。
マウスの位置を取得する
dropEvent
が起きた時にマウスがウィンドウのどこに置かれているのか知ることだできます。そのイベントのオブジェクトの中でマウスの位置が保存されているからです。
マウスの位置を取得するには、PyQt6では.position().toPoint()
を使うのですが、PyQt5では.pos()
です。
これを使って、欲しい場所に文字列を置くことができます。
import sys
from PyQt6.QtWidgets import QApplication,QWidget,QLabel
from PyQt6.QtCore import Qt
class Madoka(QWidget):
def __init__(self):
super().__init__()
self.setStyleSheet('font-family: Kaiti SC; font-size: 21px')
self.setAcceptDrops(True)
self.resize(300,200)
def dragEnterEvent(self,e):
if(e.mimeData().hasText()):
e.accept()
def dropEvent(self,e):
label = QLabel(self)
label.resize(600,400)
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
label.setText(e.mimeData().text())
pos = e.position().toPoint() # マウスの位置を取得 PyQt5ではe.pos()
label.move(pos.x()-300,pos.y()-200)
label.show()
qAp = QApplication(sys.argv)
mado = Madoka()
mado.show()
qAp.exec()
こうやってドラッグしたら……。
こんな風に文字がマウスが解放された場所に置かれます。
画像をドラッグで入れてウィンドウの中に表示させる
画像データもGUIウィジェットの中にドラッグして入れることができます。
ここでは.hasImage()
でこのドラッグの中で画像が存在した後.accept()
して、.imageData()
で画像を取得して、ウィンドウの中に入れてみます。
import sys
from PyQt6.QtWidgets import QApplication,QWidget,QLabel,QVBoxLayout
from PyQt6.QtGui import QPixmap
class Madoka(QWidget):
def __init__(self):
super().__init__()
self.setAcceptDrops(True)
self.vbl = QVBoxLayout()
self.setLayout(self.vbl)
self.show()
def dragEnterEvent(self,e):
if(e.mimeData().hasImage()):
e.accept() # 画像があると確認したら受け取る
def dropEvent(self,e):
label = QLabel() # 画像を入れるためのQLabel
pix = QPixmap(e.mimeData().imageData()) # 画像データをQPixmapに入れる
label.setPixmap(pix) # QPixmapをQLabelに入れる
self.vbl.addWidget(label) # 画像を持つQLabelをレイアウトに追加
qAp = QApplication(sys.argv)
mado = Madoka()
mado.show()
qAp.exec()
こうやって窓が出てきて、ウェブサイトから画像を入れてみたら……。
こんな風にウィンドウに現れて、もしまた入れたら……。
画像がどんどん追加されます。
ファイルをドラッグで入れてウィンドウの中に表示させる
ファイルエクスプローラーとかから、ファイルをドラッグしてGUIウィンドウの入れることもできます。ドラッグされたファイルのURLはmimeData
に入れられます。それを.urls()
でURLオブジェクトのリストを取得して、そのURLオブジェクトから.toLocalFile()
で文字列にして、open
とかで読み込むことができます。
試しに入れられた全てのファイルを読み込んで、文字列と行列を数えて表示するGUIを作ります。
import sys,os
from PyQt6.QtWidgets import QApplication,QWidget,QLabel,QGridLayout
class Madoka(QWidget):
def __init__(self):
super().__init__()
self.setStyleSheet('QLabel {background-color: #fff; color: #139; font-size: 19px; font-family: Kaiti SC;}')
self.setAcceptDrops(True)
self.grid = QGridLayout()
self.setLayout(self.grid)
self.grid.addWidget(QLabel('ファイル名'),0,0)
self.grid.addWidget(QLabel('文字数'),0,1)
self.grid.addWidget(QLabel('行数'),0,2)
self.n_file = 0
def dragEnterEvent(self,e):
if(e.mimeData().hasUrls()):
e.accept() # URLがあると確認したら受け取る
def dropEvent(self,e):
for url in e.mimeData().urls(): # ドロップされたファイルを一つずつ処理する
self.n_file += 1
url = url.toLocalFile() # URLオブジェクトを文字列にする
self.grid.addWidget(QLabel(os.path.basename(url)),self.n_file,0)
with open(url) as f: # ファイルを読み込む
sss = f.read()
self.grid.addWidget(QLabel('%d'%(len(sss))),self.n_file,1) # 文字数
self.grid.addWidget(QLabel('%d'%(len(sss.split('\n')))),self.n_file,2) # 行数
qAp = QApplication(sys.argv)
mado = Madoka()
mado.show()
qAp.exec()
ウィンドウが現れたらファイルをドラッグして入れてみましょう。
ここで試しにウィンドウに2つファイルを入れてみたらこうなります。
ドラッグするデータを自分で作る
上述の例では全部外のデータをGUIにドラッグすることばかりです。実はGUIの中でドラッグデータを作ることもできます。
方法はQDrag
とQMimeData
オブジェクトを作成して、QMimeData
をQDrag
を入れて、.exec()
メソッドを呼び出すのです。(PyQt5では.exec_()
)
例えばマウスがあるウィジェットに押された時にドラッグイベントを作るには、mousePressEvent
を定義すればいいです。
文字をドラッグしてドロップされた位置で表示する黒板みたいなものを作ってみます。
import sys
from PyQt6.QtWidgets import QApplication,QWidget,QLabel,QHBoxLayout,QVBoxLayout,QFrame
from PyQt6.QtCore import Qt,QMimeData
from PyQt6.QtGui import QDrag
class Moji(QLabel):
def __init__(self,*arg,**kwarg):
super().__init__(*arg,**kwarg)
self.setStyleSheet('color: #b5f; font-size: 40px;')
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.setFrameShape(QFrame.Shape.Panel)
self.setFixedSize(50,50)
def mousePressEvent(self,e):
m = QMimeData() # QMimeDataオブジェクトを作成する
m.setText(self.text()) # 文字列をQMimeDataに入れる
drag = QDrag(self) # QDragオブジェクトを作成する
drag.setMimeData(m) # QMimeDataをQDragに入れる
drag.exec() # ドラッグイベントを実行 # PyQt5ではdrag.exec_()
class Kokuban(QFrame):
def __init__(self):
super().__init__()
self.setFixedSize(400,300)
self.setStyleSheet('color: #eef; background-color: #151; font-size: 25px;')
self.setFrameShape(QFrame.Shape.Panel) # 線で囲む PyQt5ではQFrame.Panel
self.setFrameShadow(QFrame.Shadow.Raised) # 線の影 PyQt5ではQFrame.Raised
self.setLineWidth(5) # 線の分厚さ
self.setAcceptDrops(True) # ドロップできるにする
def dragEnterEvent(self,e):
if(type(e.source())==Moji):
e.accept() # 準備された文字だけ受け取る
def dropEvent(self,e):
label = QLabel(e.mimeData().text(),self) # ドロップされた文字列をQLabelに入れる
label.setStyleSheet('background-color: None;')
label.resize(60,60)
label.setAlignment(Qt.AlignmentFlag.AlignCenter) # PyQt5ではQt.AlignCenter
pos = e.position().toPoint() # マウスの解放された位置 PyQt5では e.pos()
x,y = pos.x(),pos.y()
label.move(x-30,y-30)
label.show()
class Madoka(QWidget):
def __init__(self):
super().__init__()
self.setFixedSize(500,350)
self.setStyleSheet('font-family: Kaiti SC;')
hbl = QHBoxLayout()
self.setLayout(hbl)
vbl = QVBoxLayout() # 左側の文字
hbl.addLayout(vbl)
for s in ['あ','い','う','え','お']: # 準備する文字
vbl.addWidget(Moji(s))
hbl.addWidget(Kokuban()) # 右側に黒板
self.show()
qAp = QApplication(sys.argv)
mado = Madoka()
mado.show()
qAp.exec()
こんなウィンドウが出ます。
左側の文字をクリックしてドラッグして右側の黒板に入れたらその場所に現れます。
ドラッグで移動できるウィジェットを作る
GUIウィンドウの中で自由にドラッグして位置を変更することができるウィジェットを作ることができます。
例えば動かせるQLabel
を作ってみます。
import sys
from PyQt6.QtWidgets import QApplication,QWidget,QLabel
from PyQt6.QtCore import Qt,QMimeData
from PyQt6.QtGui import QDrag
class Guruguru(QLabel):
def mousePressEvent(self,e):
if(e.button()==Qt.MouseButton.LeftButton):
self.xy = e.position().toPoint() # マウスが押された位置を記録する
drag = QDrag(self)
m = QMimeData()
m.setText(self.text()) # QLabelからの文字をmimeDataに入れる
drag.setMimeData(m)
drag.exec()
class Madoka(QWidget):
def __init__(self):
super().__init__()
self.setFixedSize(450,350)
self.setStyleSheet('font-family: Kaiti SC; font-size: 100px; color: #348;')
self.setAcceptDrops(True)
self.koi = Guruguru('鯉',self)
self.koi.setGeometry(50,50,100,100)
self.show()
def dragEnterEvent(self,e):
if(e.source()==self.koi):
e.accept()
def dropEvent(self,e):
so = e.source() # ここでsourceはドラッグされたQLabelとなる
pos = e.position().toPoint() # 今の位置を取得 PyQt5ではe.pos()
so.move(pos-so.xy) # マウスが押された位置と比べて置く位置を決める
qAp = QApplication(sys.argv)
mado = Madoka()
mado.show()
qAp.exec()
こんなウィンドウと文字が出ます。ドラッグしたら……。
こんな風にその位置に移動されます。
ドラッグで中身の物を入れ替えられるQListWidget
QListWidget
クラスを書き換えてドラッグでその中のアイテムを入れ替えたりするQListWidget
も作ることができます。
例えば果物の名前を入れるリストウィジェットを2つ作って、その2つのリストの間でアイテムを移動することができるようにするのです。
import sys
from PyQt6.QtWidgets import QApplication,QWidget,QHBoxLayout,QListWidget
from PyQt6.QtGui import QDrag
from PyQt6.QtCore import QMimeData
class Reizouko(QListWidget): # ドラッグで移動できるQListWidgetのクラス
def __init__(self,*arg,**kwarg):
super().__init__(*arg,**kwarg)
self.setStyleSheet('background-color: #edf; color: #629;')
self.setAcceptDrops(True) # ここでドロップできるようにする
def mousePressEvent(self,e):
QListWidget.mousePressEvent(self,e) # まずはマウスが押された時にデフォルトの動作、つまり項目の選択もしておく
item = self.currentItem() # 選んでいる項目
if(item):
m = QMimeData()
m.setText(item.text()) # 選んでいる項目の中の文字列をmimeDataに入れる
drag = QDrag(self)
drag.setMimeData(m)
drag.exec() # PyQt5ではexec_()
def dragMoveEvent(self,e):
if(e.source()==self):
QListWidget.dragMoveEvent(self,e)
def dragEnterEvent(self,e):
e.accept()
def dropEvent(self,e):
so = e.source() # 元のリスト
if(so and so!=self):
so.takeItem(so.currentRow()) # 元のリストから消す
self.addItem(e.mimeData().text()) # ドロップされたリストに追加
class Madoka(QWidget):
def __init__(self):
super().__init__()
self.setStyleSheet('font-family: Kaiti SC; font-size: 22px;')
self.hbl = QHBoxLayout()
self.setLayout(self.hbl)
self.reizouko1 = Reizouko() # 左のリスト
self.hbl.addWidget(self.reizouko1)
for s in ['梨','西瓜','葡萄','林檎','龍眼','蕃茘枝']:
self.reizouko1.addItem(s)
self.reizouko2 = Reizouko() # 右のリスト
for s in ['栗','柚','橙','檸檬','菠蘿蜜']:
self.reizouko2.addItem(s)
self.hbl.addWidget(self.reizouko2)
qAp = QApplication(sys.argv)
mado = Madoka()
mado.show()
qAp.exec()
まずはこんなリストが出ます。そして一つクリックしてドラッグしてみたら……。
こんな風に移動されます。
ドラッグできるQLineEdit
QLineEdit
は.setDragEnabled(True)
にしたら、その中の文字列がドラッグできます。
例えばQLineEdit
を2つ作って、上の方だけ.setDragEnabled(True)
にしてみます。
import sys
from PyQt6.QtWidgets import QApplication,QWidget,QLineEdit,QVBoxLayout
class Madoka(QWidget):
def __init__(self):
super().__init__()
self.setStyleSheet('font-family: Kaiti SC; font-size: 17px;')
self.vbl = QVBoxLayout()
self.setLayout(self.vbl)
self.le1 = QLineEdit('ドラッグできるよ')
self.vbl.addWidget(self.le1)
self.le1.setDragEnabled(True) # ドラッグできるようにする
self.le1.setFixedWidth(200)
self.le2 = QLineEdit('ドラッグできないよ')
self.vbl.addWidget(self.le2)
self.le2.setFixedWidth(200)
self.show()
qAp = QApplication(sys.argv)
mado = Madoka()
mado.show()
qAp.exec()
こうなると上のQLineEdit
の中の文字がドラッグできて、他の場所へコピーすることができますが、下の方のQLineEdit
はドラッグでコピーできません。
ただしこれだと元のところにそのまま残ります。このように「コピー」ではなく、「移動」にしたい場合は少し工夫してクラスを書き直す必要があります。
ドラッグしたら文字が移動するQLineEditを作る
このように普通とは違うQLineEdit
クラスを書き換えて使ってみます。
import sys
from PyQt6.QtWidgets import QApplication,QWidget,QLineEdit,QVBoxLayout
class Kakukesu(QLineEdit): # 新しいQLineEditのクラス
def __init__(self,*arg,**kwarg):
super().__init__(*arg,**kwarg)
self.setDragEnabled(True) # ドラッグできるようにする
def dragMoveEvent(self,e):
if(e.source()!=self):
QLineEdit.dragMoveEvent(self,e)
def dragEnterEvent(self,e):
if(e.mimeData().hasText()):
e.accept() # 文字列がドラッグで入ったら受け取る
def dropEvent(self,e):
so = e.source()
if(so and so!=self): #
so.del_() # 元のところの文字列を消す
self.insert(e.mimeData().text()) # 新しい場所に置く
class Madoka(QWidget):
def __init__(self):
super().__init__()
self.setStyleSheet('font-family: Kaiti SC; color: #834;')
self.vbl = QVBoxLayout()
self.setLayout(self.vbl)
for i in range(4): # こんなQLineEditを4つ作る
le = Kakukesu('ドラッグしたら移るよ')
self.vbl.addWidget(le)
self.show()
qAp = QApplication(sys.argv)
mado = Madoka()
mado.show()
qAp.exec()
この中のQLineEdit
の間でドラッグされたら文字列の移動になります。
ただし外部から文字列をドラッグする場合、依然としてコピーとなります。
参考
その他にqiitaでPyQtのドラッグ&ドロップについて書く記事が少しあります。以下の記事も参考になります。
- PythonでGUI : PyQt5のご紹介
- 【PythonでGUI】PyQt5 -ドラッグ&ドロップ-
- PyQtを利用してシンプルなテキストエディタを作ってみた
- 画像処理を用いてハチナイの打撃成績を取得してみた
- [PyQt] QListWidgetでQStandardItemModelを使う
終わりに
以上PyQtでドラッグ&ドロップを使う方法について説明しました。実際にまだ色々できるはずですが、今ひとまずこの辺にしておきます。