Pyslvs v0.9 新功能測試及解說

  • 如何在 Qt 框架中達成 Widgets 間的滑鼠資訊轉移

Qt Drag and Drop

詳細說明可以參考這裡:http://doc.qt.io/qt-5/dnd.html

Pyslvs v0.9 新功能影片:

動作解析

在不同的桌面系統中,滑鼠的拖放動作都已經透過 Qt 框架轉化成以下幾個動作:

從 AWidget 到 BWidget。

  1. 抓起?(AWidget::mousePressEvent / mouseReleaseEvent

  2. 移出(AWidget::mouseMoveEvent

  3. 移入(BWidget::dragEnterEvent

  4. 橫越(BWidget::dragMoveEvent

  5. 放下(BWidget::dropEvent

接下來按照步驟配置所需的函式。

第一步就是開啟拖放置功能,以允許丟東西在 QWidget 中:

class BWidget(QWidget):
    def __init__(self, parent=None):
        super(BWidget, self).__init__(parent)
        self.setAcceptDrops(True)

需要注意的是,上述的這些 QEvent 函式都是私有的,意即無法(Python 其實可以強制設定,但是很難看)透過外部撰寫。

上面這句話的意思是,不能用 QtDesigner 來佈置這些 Widget!請按部就班繼承改寫新 Class,用指令插入 Layout 裡。

這邊就不再介紹如何手動放這些 Widget 了,較不瞭解的初學者可以用 QtDesigner 拉出 QLayout 後用 insertWidget 指令達成。

抓起?(AWidget::mousePressEvent / mouseReleaseEvent

這個函式專門處理 AWidget 中任何按下滑鼠的動作,因此如果你的 AWidget 並非原始無瑕的 QWidget,那 Qt 早已設定過一些互動函式了。

在 Python 中,子 Class 的新 method 若和任何父項 method 撞名,會造成覆蓋。為了不讓 Qt 的設定功能失效,請使用 super 函式讓 Qt 先做完應有的工作。

class AWidget(QWidget):
    def mousePressEvent(self, event):
        super(AWidget, self).mousePressEvent(event)

    def mouseReleaseEvent(self, event):
        super(AWidget, self).mouseReleaseEvent(event)

接下來的 QEvent 函式都是一樣,如果不小心蓋掉發生怪怪的狀況,例如 QTableWidget 無法再選起儲存格,那就加上 super 補上被忽略的工作。

言歸正傳,由於「按下滑鼠」這個事件不一定是想抓東西來拖移,因此不能就這樣亂槍打鳥,要先偷偷紀錄其的行為。在這裡設定一個名稱「draged」紀錄滑鼠的動作,決定是否為滑鼠左鍵抓住的動作。

放掉滑鼠的話就把這個值取消,表示使用者並沒有做出拖動的行為。

class AWidget(QWidget):
    def __init__(self, parent=None):
        super(AWidget, self).__init__(parent)
        self.draged = False

    def mousePressEvent(self, event):
        super(AWidget, self).mousePressEvent(event)
        if event.button()==Qt.LeftButton:
            self.draged = True

    def mouseReleaseEvent(self, event):
        super(AWidget, self).mouseReleaseEvent(event)
        self.draged = False

移出(AWidget::mouseMoveEvent

這個函式專門處理 AWidget 中滑鼠移動的動作,也包括憑空移動,因此剛才設定的「draged」可以告訴我們,使用者正想要「拿著東西移動」。

既然想拿東西,就必須把東西打包交給使用者,一般普遍為文字資訊,包裝後在 BWidget 解開,亦可以攜帶圖片、網址、HTML、顏色資訊。

這裡介紹兩個新的 Qt Class:

  • QMimeData:行李箱,將一堆雜亂的行李分裝後包起來。

  • QDrag:送貨員,可以附上一張圖片以辨識該行李的內容,另外送貨員知道寄件者地址(AWidget)、滑鼠停駐點等資訊,也可以幫旅行中的滑鼠換外觀。

接下來就來裝箱吧,根據 Pyslvs 的影片,我們可以推測出攜帶的資訊為 AWidget 的「被選擇行號」,因此透過一段小迴圈篩選出所有被選擇的行號。

用 Python 的 set 集合型態可以將重複選取的內容排除掉,接著重新排序轉回 list 類型儲存。

而 QTableWidget 的 mouseMoveEvent 其實有「按住連續選取」的功能,但是這樣會干擾我們拖移的行為,因此這邊不使用 super。

class AWidget(QTableWidget):
    def selectedRows(self):
        a = list()
        for r in self.selectedRanges():
            a += [i for i in range(r.topRow(), r.bottomRow()+1)]
        return sorted(set(a))

    def mouseMoveEvent(self, event):
        if self.draged:
            selectedRows = self.selectedRows()
            selectedRowCount = len(selectedRows)
            if selectedRowCount==2 or selectedRowCount==3:
                drag = QDrag(self)
                mimeData = QMimeData()
                mimeData.setText(';'.join([str(e) for e in selectedRows]))
                drag.setMimeData(mimeData)
                drag.setPixmap(QPixmap(":/icons/tooltips/need{}bearings.png".format(selectedRowCount)).scaledToWidth(50))
                drag.exec_()

由於下方的表格需要 2 或 3 個選取的行號,因此以外的選項我們一律不接受。

行號資訊型態是 int,這邊轉換成字串後用分號 ; 包裝起來。而這裡還有設定 need{}bearings.png 的圖片,表示拿了 2 個或 3 個「軸承」。

最後下的 drag.exec_() 方法為延遲函式,表示送貨員已出發,它會一直等到貨物到達或被丟棄時才會回傳,因此要注意不要讓執行序被阻塞。

移入(BWidget::dragEnterEvent

這個函式專門處理 BWidget 中滑鼠帶著資訊進入的動作。這裡的 event 物件已經被 Qt 轉換成我們的送貨員,與剛才的 QDrag 物件有一定程度的相仿。要使用審核機制來判別這個送貨員是不是我們要接受的對象。

要如何審核呢?有兩種結果:

  • 使用 acceptProposedAction 方法接受這名送貨員進入 BWidget 的領域。

  • 使用一般的 ignore 方法無視這名送貨員,或是乾脆不理他,會造成滑鼠游標出現類似禁止的符號,依你的桌面系統而定。

被允許的送貨員能在進入的時候,依你的桌面系統出現類似抓著資訊的樣式,這時可以選擇是否放開以丟下資訊,或是將資訊帶走(我只是路過),不一定會送達 BWidget。

若是送貨員在此處被禁止投遞,卻仍然放開滑鼠,那他手中的資訊就會遺失,而且會無法重新取得(除非回到 AWidget 再打包)。

class BWidget(QTableWidget):
    def dragEnterEvent(self, event):
        mimeData = event.mimeData()
        if mimeData.hasText():
            if len(mimeData.text().split(';'))==self.bearings:
                event.acceptProposedAction()

這邊的程式中,self.bearings 為允許的行號數,例如連桿表格為 2 個,當拆解用 ; 分號封裝的字串時,數量為 2 個,那此貨物就允許進入。

橫越(BWidget::dragMoveEvent

某些列表式的 QWidget,例如 QTableView、QListView、QTreeView,其實會內建清單拖移功能,讓使用者可以直接拖動儲存格,插到自己或是其他相同類型的 QWidget 中,而且不用自己寫 QEvent 函式,只是這些功能預設是關閉的。

由於上述原因,我們得將自訂送貨員打扮成上面較特殊 QWidget 的送貨員,否則這些類型會不允許他送進資訊,即使在上一小節中已經允許。

class BWidget(QTableWidget):
    def dragMoveEvent(self, event):
        event.setDropAction(Qt.MoveAction)
        event.accept()

這些特殊 QWidget 的送貨員會攜帶「放置行為」,如「遷移」、「複製」等等,因此將我們的送貨員設定為常見的 Qt.MoveAction 即可。

若是其他沒有這種 items 的 QWidget,是不用設定此步驟的。

放下(BWidget::dropEvent

終於到最後一步了,送貨員最終決定在此投遞,因此必須在此拆包,執行相關的函式。

為了方便,這裡與外界溝通的方式是使用信號槽將所有行號拆包發送出去,使用的是 self.dragIn 這個信號。

class BWidget(QTableWidget):
    def dropEvent(self, event):
        self.dragIn.emit(*[int(e) for e in event.mimeData().text().split(';')])
        event.acceptProposedAction()

最後,還是要執行 acceptProposedAction 方法,才會關閉此事件。


之前介紹過如何接收外部拖入檔案,在程式中開啟的方法:

http://project.mde.tw/blog/40323230ri-zhi-1060116.html

這次挑戰較複雜的,在 Qt Widget 中抓起資訊的方式。

這三種 QTableWidget 的獨立原始碼提供在這裡,因為這其中的設定還透過繼承方式簡化函式,會較整齊些。

https://raw.githubusercontent.com/coursemdetw/project_site_files/gh-pages/files/pyslvs/dndExample.txt

可以透過對照的方式看看自己寫的是否完整。


Comments

comments powered by Disqus