Большинство современных пользователей применяют перетаскивание практически инстинктивно. Я уверен, что вы хотя бы раз перетаскивали файл из одной папки в другую на этой неделе. К счастью, набор инструментов интерфейса wxPython обладает встроенным перетаскиванием. В данной статье я расскажу вам о том, как применить его в комбинации со своими приложениями.
Приступая к работе
wxPython содержит несколько разных видов перетаскивания. Вы можете выбрать один из следующих типов:
- FileDropTarget
- TextDropTarget
- PyDropTarget
Первые два имеют весьма говорящие названия. Последний, wx.PyDropTarget на самом деле просто создаёт область вокруг самой wx.DropTarget и захватывает её. Он также добавляет несколько дополнительных методов, которые вы не можете встретить в простом wx.DropTarget. Мы начнём с примера использования wx.FileDropTarget.
Есть вопросы по Python?
На нашем форуме вы можете задать любой вопрос и получить ответ от всего нашего сообщества!
Паблик VK
Одно из самых больших сообществ по Python в социальной сети ВК. Видео уроки и книги для вас!
Создаём FileDropTarget
Набор инструментов wxPython делает создание FileDropTarget невероятно простым. Вам требуется освоить метод, прежде чем вы сможете это, но, по правде говоря, он весьма прямолинеен. Давайте сначала взглянем на пример кода, а потом я потрачу несколько минут на его объяснение.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
import wx class MyFileDropTarget(wx.FileDropTarget): """""" def __init__(self, window): """Constructor""" wx.FileDropTarget.__init__(self) self.window = window def OnDropFiles(self, x, y, filenames): """ Когда файлы были перемещены, напишите, куда они были перемещены, и затем уже сами адреса файлов """ self.window.SetInsertionPointEnd() self.window.updateText("\n%d file(s) dropped at %d,%d:\n" % (len(filenames), x, y)) for filepath in filenames: self.window.updateText(filepath + '\n') return True 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): """ Добавьте назначение вставки в конец текстового контроля, чтобы избежать перезаписи """ self.fileTextCtrl.SetInsertionPointEnd() def updateText(self, text): """ Впишите текст в текстовый контроль """ 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. Он принимает координаты позиции курсора и адреса перетянутых файлов, а затем вписывает эти данные в текстовый контроль.
Для того, чтобы притянуть цель перетаскивания к текстовому контролю, вам стоит взглянуть на класс DnDPanel, в котором мы вызываем метод текстового контроля SetDropTarget и задаём инстанцию класса нашей цели перетаскивания. У нас есть ещё 2 метода в нашем панельном классе, которые делают так, что класс цели перетаскивания вызывает текстовый контроль: SetInsertionPointEnd и updateText.
Учтите, что, если мы используем объект панели в качестве цели перетаскивания, мы можем называть эти методы как нам угодно. Если целью перетаскивания является ваш TextCtrl, мы должны будем пойти другим путём, который рассматривается в следующем примере!
Создаём TextDropTarget
wx.TextDropTarget используется, когда вам нужно перетянуть выбранный текст в текстовый контроль. Пожалуй, самым распространённым примером будет перетягивание ссылки из адресной строки браузера в окно чата в социальной сети или мессенджере. Давайте потратим немного времени и научимся как помещать подобную функцию в ваши приложения с помощью wxPython!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
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)) return True 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, чтобы обновлять текстовый контроль. Если вы измените это, то получите сообщение с ошибкой.
Обычное перетаскивание с PyDropTarget
Если вы вдруг ещё не догадались, то два предыдущих примера были сильно модифицированными версиями демо перетаскивания, которые я взял с официального демо-пакета wxPython. Мы будем использовать код, основанный на демо-версии URLDragAnd-Drop для объяснения PyDropTarget. Интересны факт о данном демо: вы можете не только создавать виджет, который принимает перетянутый текст, но и можете перетаскивать текст из этого виджета в свой браузер! Давайте взглянем!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
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): """ Возвращает сайзер, заполненный виджетами """ 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 мы извлекаем ссылку и присоединяем её к текстовому контролю. В панельном классе мы цепляем цель перетаскивания также, как мы сделали это в двух других примерах, так что я не буду повторятся и сразу пойду навстречу к неизведанному.
Второй текстовый контроль определённо стоит нашего внимания. Здесь мы ищем движения курсора, используя EVT_MOTION. В хэндлере событий, отвечающем за передвижения курсора, который называется OnStartDrag, мы проводим проверку, чтобы убедится, что пользователь действительно пытается перетащить объект.
Если это действительно так, то мы захватываем значение из текстового бокса и добавляем его в только что созданный URLDataObject. Затем мы создаём инстанцию из DropSource, и пропускаем его через наш второй текстовый контроль, так как он и ЕСТЬ источник.
Мы задаём данные источника в URLDataObject. Наконец-то, мы вызываем DoDragDrop в нашем источнике перетаскивания (текстовом контроле, если вдруг кто-то забыл), который ответит посредством движения, копирования, отмены или неудачи. Если вы перетащили ссылку в адресную строку вашего браузера, он скопирует её. В других случаях, он, скорее всего, работать не будет. Теперь, давайте соберём всё, что мы усвоили и сделаем что-то оригинальное!
Примечание: Некоторые браузеры могут не работать с этим кодом. К примеру, на Linux, он не работал в Google Chrome, но отлично работал в Mozilla Firefox.
Создаём обычное приложение с перетаскиванием
Я решил, что было бы забавно взять демо цели перетаскивания и превратить его в что-то с применением виджета ObjectListView (враппер для ListCtrl), которое сможет сообщить нам определённую информацию о перетаскиваемом файле, которые мы в него вставляем. Он будет отображать следующую информацию: имя файла, дата создания, дата редактирования и размер файла. Вот сам код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
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): """ Когда файлы перетащены, обновляет дисплей """ self.window.updateDisplay(filenames) return True 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 в качестве инстанции цели перетаскивания. У нас также есть общий класс, содержащий данные, связанные с файлами. Если вы запускаете эту программу и перетаскиваете в неё папки, вы не получите правильного размера папки.
Скорее всего вам нужно дополнить код следующим образом: заставить программу проверить каждый файл в папке и затем суммировать их размеры. Так что, не стесняйтесь настраивать его так, как вам удобно. Как бы то ни было «гвоздём» программы является метод updateDisplay. Здесь мы захватываем виртуальную статистику файла и конвертируем её в более читабельные форматы, так как все нормальные люди не исчисляют время в секундах с полуночи 1 января 1970 года. Сразу же после трансформации данных, мы их отображаем. Ну разве не круто?
Итоги
Сейчас вы уже должны освоить как минимум три разных метода перетаскивания wxPython. Надеюсь, что вы воспользуетесь новой информацией со всей ответственностью и в ближайшее время создадите несколько свежих приложений с открытым исходным кодом. Удачи!
Являюсь администратором нескольких порталов по обучению языков программирования Python, Golang и Kotlin. В составе небольшой команды единомышленников, мы занимаемся популяризацией языков программирования на русскоязычную аудиторию. Большая часть статей была адаптирована нами на русский язык и распространяется бесплатно.
E-mail: vasile.buldumac@ati.utm.md
Образование
Universitatea Tehnică a Moldovei (utm.md)
- 2014 — 2018 Технический Университет Молдовы, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
- 2018 — 2020 Технический Университет Молдовы, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»