Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
2
Help us understand the problem. What are the problem?
@phyblas

PyQtのドラッグ&ドロップの使い方

この前の記事で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 ページからの文字列を入れてみたらこうなります。

もしまた入れたら追加されます。
截屏2021-08-18-14.33.13.jpg

ここで文字列があるかどうかをチェックするために.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()を呼び出します。

dropEventdragEnterEventの中で.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の中でドラッグデータを作ることもできます。

方法はQDragQMimeDataオブジェクトを作成して、QMimeDataQDragを入れて、.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のドラッグ&ドロップについて書く記事が少しあります。以下の記事も参考になります。
https://qiita.com/tonluqclml/items/c7bdbc3db81468c76f19
https://qiita.com/Nobu12/items/803b5595938b17d90a21
https://qiita.com/ebonight/items/ec8f1a1000fb32eb7639
https://qiita.com/yobidengen/items/26f65147780e914d3d08
https://qiita.com/inon3135/items/35ec7c897d0e194040f5

終わりに

以上PyQtでドラッグ&ドロップを使う方法について説明しました。実際にまだ色々できるはずですが、今ひとまずこの辺にしておきます。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
2
Help us understand the problem. What are the problem?