Статьи

wxPython: введение в перетаскивание

Большинство пользователей компьютеров этого дня и возраста инстинктивно используют перетаскивание (DnD). Вы, вероятно, использовали его для переноса некоторых файлов из одной папки в другую на этой неделе! Инструментарий wxPython GUI обеспечивает функциональность перетаскивания. В этом уроке мы увидим, насколько просто это реализовать!

Начиная

wxPython предоставляет несколько различных видов перетаскивания. Вы можете иметь один из следующих типов:

  • wx.FileDropTarget
  • wx.TextDropTarget
  • wx.PyDropTarget

Первые два довольно очевидны. Последний, wx.PyDropTarget, это просто свободная оболочка вокруг самого wx.DropTarget. Он добавляет пару дополнительных удобных методов, которых нет у простого wx.DropTarget. Начнем с примера wx.FileDropTarget.

Создание FileDropTarget

 

Drag-n-Dropping файлы

Инструментарий wxPython делает создание цели перетаскивания довольно простым. Вы должны переопределить метод, чтобы заставить его работать правильно, но в остальном он довольно прост. Давайте на минутку рассмотрим этот пример кода, а затем потратим некоторое время на его объяснение.

import wx
 
########################################################################
class MyFileDropTarget(wx.FileDropTarget):
    """"""
 
    #----------------------------------------------------------------------
    def __init__(self, window):
        """Constructor"""
        wx.FileDropTarget.__init__(self)
        self.window = window
 
    #----------------------------------------------------------------------
    def OnDropFiles(self, x, y, filenames):
        """
        When files are dropped, write where they were dropped and then
        the file paths themselves
        """
        self.window.SetInsertionPointEnd()
        self.window.WriteText("\n%d file(s) dropped at %d,%d:\n" %
                              (len(filenames), x, y))
        for filepath in filenames:
            self.window.updateText(filepath + '\n')    
 
########################################################################
class DnDPanel(wx.Panel):
    """"""
 
    #----------------------------------------------------------------------
    def __init__(self, parent):
        """Constructor"""
        wx.Panel.__init__(self, parent=parent)
 
        file_drop_target = MyFileDropTarget(self)
        lbl = wx.StaticText(self, label="Drag some files here:")
        self.fileTextCtrl = wx.TextCtrl(self,
                                        style=wx.TE_MULTILINE|wx.HSCROLL|wx.TE_READONLY)
        self.fileTextCtrl.SetDropTarget(file_drop_target)
 
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(lbl, 0, wx.ALL, 5)
        sizer.Add(self.fileTextCtrl, 1, wx.EXPAND|wx.ALL, 5)
        self.SetSizer(sizer)
 
    #----------------------------------------------------------------------
    def SetInsertionPointEnd(self):
        """
        Put insertion point at end of text control to prevent overwriting
        """
        self.fileTextCtrl.SetInsertionPointEnd()
 
    #----------------------------------------------------------------------
    def updateText(self, text):
        """
        Write text to the text control
        """
        self.fileTextCtrl.WriteText(text)
 
########################################################################
class DnDFrame(wx.Frame):
    """"""
 
    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""
        wx.Frame.__init__(self, parent=None, title="DnD Tutorial")
        panel = DnDPanel(self)
        self.Show()
 
#----------------------------------------------------------------------
if __name__ == "__main__":
    app = wx.App(False)
    frame = DnDFrame()
    app.MainLoop()

Это было не так уж плохо, не так ли? Первое, что нужно сделать, это создать подкласс wx.FileDropTarget , что мы делаем с нашим классом MyFileDropTarget . Внутри этого у нас есть один переопределенный метод, OnDropFiles . Он принимает x / y позицию мыши и пути к файлам, которые были отброшены, затем записывает их в текстовый элемент управления. Чтобы подключить цель перетаскивания к текстовому элементу управления, вам нужно заглянуть в класс DnDPanel, где мы вызываем SetDropTarget текстового элемента управления.метод и установите его для экземпляра нашего целевого класса удаления. У нас есть еще два метода в нашем классе панели, которые вызывает целевой класс удаления для обновления текстового элемента управления: SetInsertionPointEnd и updateText. Обратите внимание, что, поскольку мы передаем объект панели как цель перетаскивания, мы можем вызывать эти методы как угодно. Если бы TextCtrl был целью перетаскивания, нам пришлось бы сделать это по-другому, что мы увидим в нашем следующем примере!

Создание TextDropTarget

 

Перетащите текст

Wx.TextDropTarget используется, когда вы хотите иметь возможность перетаскивать выбранный текст в текстовый элемент управления. Вероятно, одним из наиболее распространенных примеров является перетаскивание URL-адреса на веб-странице до адресной строки или текста в поле поиска в Firefox. Давайте потратим некоторое время на изучение того, как создать одну из таких целей в wxPython!

import wx
 
########################################################################
class MyTextDropTarget(wx.TextDropTarget):
 
    #----------------------------------------------------------------------
    def __init__(self, textctrl):
        wx.TextDropTarget.__init__(self)
        self.textctrl = textctrl
 
    #----------------------------------------------------------------------
    def OnDropText(self, x, y, text):
        self.textctrl.WriteText("(%d, %d)\n%s\n" % (x, y, text))
 
    #----------------------------------------------------------------------
    def OnDragOver(self, x, y, d):
        return wx.DragCopy
 
########################################################################
class DnDPanel(wx.Panel):
    """"""
 
    #----------------------------------------------------------------------
    def __init__(self, parent):
        """Constructor"""
        wx.Panel.__init__(self, parent=parent)
 
 
        lbl = wx.StaticText(self, label="Drag some text here:")
        self.myTextCtrl = wx.TextCtrl(self,
                                      style=wx.TE_MULTILINE|wx.HSCROLL|wx.TE_READONLY)
        text_dt = MyTextDropTarget(self.myTextCtrl)
        self.myTextCtrl.SetDropTarget(text_dt)
 
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.myTextCtrl, 1, wx.EXPAND)
        self.SetSizer(sizer)
 
    #----------------------------------------------------------------------
    def WriteText(self, text):
        self.text.WriteText(text)
 
########################################################################
class DnDFrame(wx.Frame):
    """"""
 
    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""
        wx.Frame.__init__(self, parent=None, title="DnD Text Tutorial")
        panel = DnDPanel(self)
        self.Show()
 
#----------------------------------------------------------------------
if __name__ == "__main__":
    app = wx.App(False)
    frame = DnDFrame()
    app.MainLoop()

Еще раз мы должны создать подкласс нашего целевого класса. В этом случае мы называем это MyTextDropTarget . В этом классе мы должны переопределить OnDropText и OnDragOver . Я не смог найти удовлетворительную документацию по последнему, но я предполагаю, что он просто возвращает копию перетаскиваемых данных. Метод OnDropText записывает текст в текстовый элемент управления. Обратите внимание, что, поскольку мы привязали цель перетаскивания непосредственно к текстовому элементу управления (см. Класс панели), мы ДОЛЖНЫ использовать метод с именем WriteText для обновления текстового элемента управления. Если вы измените его, вы получите сообщение об ошибке.

Пользовательский DnD с PyDropTarget

 

Перетащите URL-адреса с помощью PyDropTarget

Если вы еще не догадались, эти примеры были слегка измененными версиями демонстраций DnD из официальной демонстрации wxPython. Мы будем использовать некоторый код, основанный на их демо URLDragAndDrop, чтобы объяснить PyDropTarget. Самое интересное в этой демонстрации состоит в том, что вы можете не только создать виджет, который может принимать перетаскиваемый текст, но и перетащить некоторый текст из другого виджета обратно в браузер! Давайте взглянем:

import  wx
 
########################################################################
class MyURLDropTarget(wx.PyDropTarget):
 
    #----------------------------------------------------------------------
    def __init__(self, window):
        wx.PyDropTarget.__init__(self)
        self.window = window
 
        self.data = wx.URLDataObject();
        self.SetDataObject(self.data)
 
    #----------------------------------------------------------------------
    def OnDragOver(self, x, y, d):
        return wx.DragLink
 
    #----------------------------------------------------------------------
    def OnData(self, x, y, d):
        if not self.GetData():
            return wx.DragNone
 
        url = self.data.GetURL()
        self.window.AppendText(url + "\n")
 
        return d
 
#######################################################################
class DnDPanel(wx.Panel):
    """"""
 
    #----------------------------------------------------------------------
    def __init__(self, parent):
        """Constructor"""
        wx.Panel.__init__(self, parent=parent)
        font = wx.Font(12, wx.SWISS, wx.NORMAL, wx.BOLD, False)
 
        # create and setup first set of widgets
        lbl = wx.StaticText(self, label="Drag some URLS from your browser here:")
        lbl.SetFont(font)
        self.dropText = wx.TextCtrl(self, size=(200,200),
                                      style=wx.TE_MULTILINE|wx.HSCROLL|wx.TE_READONLY)
        dt = MyURLDropTarget(self.dropText)
        self.dropText.SetDropTarget(dt)
        firstSizer = self.addWidgetsToSizer([lbl, self.dropText])
 
        # create and setup second set of widgets
        lbl = wx.StaticText(self, label="Drag this URL to your browser:")
        lbl.SetFont(font)
        self.draggableURLText = wx.TextCtrl(self, value="http://www.mousevspython.com")
        self.draggableURLText.Bind(wx.EVT_MOTION, self.OnStartDrag)
        secondSizer = self.addWidgetsToSizer([lbl, self.draggableURLText])
 
        # Add sizers to main sizer
        mainSizer = wx.BoxSizer(wx.VERTICAL)
        mainSizer.Add(firstSizer, 0, wx.EXPAND)
        mainSizer.Add(secondSizer, 0, wx.EXPAND)
        self.SetSizer(mainSizer)
 
    #----------------------------------------------------------------------
    def addWidgetsToSizer(self, widgets):
        """
        Returns a sizer full of widgets
        """
        sizer = wx.BoxSizer(wx.HORIZONTAL)
        for widget in widgets:
            if isinstance(widget, wx.TextCtrl):
                sizer.Add(widget, 1, wx.EXPAND|wx.ALL, 5)
            else:
                sizer.Add(widget, 0, wx.ALL, 5)
        return sizer
 
    #----------------------------------------------------------------------
    def OnStartDrag(self, evt):
        """"""
        if evt.Dragging():
            url = self.draggableURLText.GetValue()
            data = wx.URLDataObject()
            data.SetURL(url)
 
            dropSource = wx.DropSource(self.draggableURLText)
            dropSource.SetData(data)
            result = dropSource.DoDragDrop()
 
########################################################################
class DnDFrame(wx.Frame):
    """"""
 
    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""
        wx.Frame.__init__(self, parent=None, title="DnD URL Tutorial", size=(800,600))
        panel = DnDPanel(self)
        self.Show()
 
#----------------------------------------------------------------------
if __name__ == "__main__":
    app = wx.App(False)
    frame = DnDFrame()
    app.MainLoop()

Первый класс — это наш целевой класс отбрасывания. Здесь мы создаем объект wx.URLDataObject, в котором хранится информация о наших URL. Затем в методе OnData мы извлекаем URL и добавляем его в связанный текстовый элемент управления. В нашем классе панели мы подключаем цель перетаскивания так же, как и в двух других примерах, поэтому мы пропустим это и перейдем к новому материалу. Второй текстовый элемент управления — это то, на что нам нужно обратить внимание. Здесь мы находим движение мыши через EVT_MOTION. В обработчике события движения мыши (OnStartDrag) мы проверяем, что пользователь перетаскивает. Если это так, то мы берем значение из текстового поля и добавляем его во вновь созданный URLDataObject. Далее мы создаем экземпляр DropSourceи передайте ему наш второй текстовый элемент управления, так как он является источником. Мы устанавливаем данные источника в URLDataObject. Наконец, мы вызываем DoDragDrop для нашего источника перетаскивания (текстовый элемент управления), который будет реагировать перемещением, копированием, отменой или ошибкой. Если вы перетащили URL-адрес в адресную строку браузера, он будет скопирован. В противном случае это, вероятно, не сработает. Теперь давайте возьмем то, что мы узнали, и создадим что-то оригинальное!

Создание пользовательского приложения для перетаскивания

 

Перетаскивание с помощью ObjectListView

Я подумал, что было бы интересно взять демонстрационную версию цели удаления файла и превратить ее во что-нибудь с помощью виджета ObjectListView (обертка ListCtrl), который может сообщить нам некоторую информацию о файлах, которые мы помещаем в него. Мы будем показывать следующую информацию: имя файла, дата создания, дата изменения и размер файла. Вот код:

 

import os
import stat
import time
import wx
from ObjectListView import ObjectListView, ColumnDefn
 
########################################################################
class MyFileDropTarget(wx.FileDropTarget):
    """"""
 
    #----------------------------------------------------------------------
    def __init__(self, window):
        """Constructor"""
        wx.FileDropTarget.__init__(self)
        self.window = window
 
    #----------------------------------------------------------------------
    def OnDropFiles(self, x, y, filenames):
        """
        When files are dropped, update the display
        """
        self.window.updateDisplay(filenames)
 
########################################################################
class FileInfo(object):
    """"""
 
    #----------------------------------------------------------------------
    def __init__(self, path, date_created, date_modified, size):
        """Constructor"""
        self.name = os.path.basename(path)
        self.path = path
        self.date_created = date_created
        self.date_modified = date_modified
        self.size = size
 
########################################################################
class MainPanel(wx.Panel):
    """"""
 
    #----------------------------------------------------------------------
    def __init__(self, parent):
        """Constructor"""
        wx.Panel.__init__(self, parent=parent)
        self.file_list = []
 
        file_drop_target = MyFileDropTarget(self)
        self.olv = ObjectListView(self, style=wx.LC_REPORT|wx.SUNKEN_BORDER)
        self.olv.SetDropTarget(file_drop_target)
        self.setFiles()
 
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.olv, 1, wx.EXPAND)
        self.SetSizer(sizer)
 
    #----------------------------------------------------------------------
    def updateDisplay(self, file_list):
        """"""
        for path in file_list:
            file_stats = os.stat(path)
            creation_time = time.strftime("%m/%d/%Y %I:%M %p",
                                          time.localtime(file_stats[stat.ST_CTIME]))
            modified_time = time.strftime("%m/%d/%Y %I:%M %p",
                                          time.localtime(file_stats[stat.ST_MTIME]))
            file_size = file_stats[stat.ST_SIZE]
            if file_size > 1024:
                file_size = file_size / 1024.0
                file_size = "%.2f KB" % file_size
 
            self.file_list.append(FileInfo(path,
                                           creation_time,
                                           modified_time,
                                           file_size))
 
        self.olv.SetObjects(self.file_list)
 
    #----------------------------------------------------------------------
    def setFiles(self):
        """"""
        self.olv.SetColumns([
            ColumnDefn("Name", "left", 220, "name"),
            ColumnDefn("Date created", "left", 150, "date_created"),
            ColumnDefn("Date modified", "left", 150, "date_modified"),
            ColumnDefn("Size", "left", 100, "size")
            ])
        self.olv.SetObjects(self.file_list)
 
########################################################################
class MainFrame(wx.Frame):
    """"""
 
    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""
        wx.Frame.__init__(self, None, title="OLV DnD Tutorial", size=(800,600))
        panel = MainPanel(self)
        self.Show()
 
#----------------------------------------------------------------------
def main():
    """"""
    app = wx.App(False)
    frame = MainFrame()
    app.MainLoop()
 
if __name__ == "__main__":
    main()

Большую часть этого материала вы видели раньше. У нас есть подкласс FileDropTarget , мы подключаем к нему панель, а затем виджет ObjectListView к экземпляру drop target. У нас также есть универсальный класс для хранения наших файловых данных. Если вы запустите эту программу и поместите в нее папки, вы не получите правильный размер файла. Возможно, вам придется пройтись по папке и сложить размеры файлов в ней, чтобы это работало. Не стесняйтесь исправить это самостоятельно. Во всяком случае, мясо программы находится в методе updateDisplay . Здесь мы берем жизненную статистику файла и конвертируем ее в более читаемые форматы, так как большинство людей не понимают даты, которые считаются секундами с начала эпохи. Как только мы немного помассировали данные, мы их отображаем. Разве это не круто?

Завершение

Теперь вы должны знать, как сделать как минимум 3 различных типа перетаскивания в wxPython. Надеемся, что вы будете использовать эту новую информацию ответственно и создадите несколько свежих приложений с открытым исходным кодом в ближайшем будущем. Удачи!

Дальнейшее чтение

Исходный код