wxPython и Drag’n’Drop файлов в Linux
В промежутках между работой и сном постепенно доделываю OutWiker, очередную версию которого собираюсь в скором времени выложить. Одним из изменений в этой версии будет то, что теперь можно будет перетаскивать прикрепленные файлы из окна OutWiker’а в другие приложения. Но когда я только начинал добавлять эту возможность, то не думал, что потрачу на нее столько сил и нервов, и все из-за одной мерзопакостной ошибки в wxWidgets.
Началось все хорошо. Сидя под Windows, реализация возможности перетаскивать файлы в другие приложения заняла считанные строки:
def onBeginDrag(self, event):
"""
Начало перетаскивания файлов
"""
data = wx.FileDataObject()
for fname in self.getSelectedFiles():
# Добавляем вложенные файлы в список.
data.AddFile (fname)
dragSource = wx.DropSource (self)
dragSource.SetData(data)
result = dragSource.DoDragDrop ()
Под Windows все заработало с первого раза, никаких неприятностей от этих нескольких строчек я не ожидал. На следующий день перезагружаюсь под Linux, чтобы убедиться, что перетаскивание работает и там, забираю изменения из репозитория, запускаю. И, как вы уже догадываетесь, перетаскивание не работает. Причем, не работает очень интересным образом: обрезаются пути до перетаскиваемых файлов. Путь до файлов обрезается только если в пути есть русские буквы, то есть налицо какие-то проблемы с кодировками.
Например, если изменить приведенный выше код на синтетический, который передает на перетаскивание всегда один и тот же файл:
def onBeginDrag(self, event):
data = wx.FileDataObject()
# Добавим один файл на перетаскивание
data.AddFile (u"/tmp/тест/Путь с русскими буквами/__attach/readme.txt")
# Выведем список перетаскиваемых файлов
for fname in data.GetFilenames():
print fname
dragSource = wx.DropSource (self)
dragSource.SetData(data)
result = dragSource.DoDragDrop (True)
то в результате код выведет на экран правильный путь до файла, а это означает, что по крайней мере добавили мы его правильно. А вот в приемник этого файла дойдет только строка «/tmp/тест/Путь с русскими буквам».
А если мы перетаскиваем несколько файлов, то имя обрезается у последнего. Например, передаем вот такой список файлов:
/media/Storage/Workdir/outwiker/Примеры вики/samplewiki/Тесты/dd/__attach/CurrentPagePanel.py
/media/Storage/Workdir/outwiker/Примеры вики/samplewiki/Тесты/dd/__attach/guicontroller.py
/media/Storage/Workdir/outwiker/Примеры вики/samplewiki/Тесты/dd/__attach/HtmlTextEditor.py
/media/Storage/Workdir/outwiker/Примеры вики/samplewiki/Тесты/dd/__attach/htmlview.py
А приемник получает вот такой:
/media/Storage/Workdir/outwiker/Примеры вики/samplewiki/Тесты/dd/__attach/CurrentPagePanel.py
/media/Storage/Workdir/outwiker/Примеры вики/samplewiki/Тесты/dd/__attach/guicontroller.py
/media/Storage/Workdir/outwiker/Примеры вики/samplewiki/Тесты/dd/__attach/HtmlTextEditor.py
/media/Storage/Workdir/outwiker/Прим
Долго я промучался с тем, чтобы понять кто виноват, и что делать. Уже готов был пойти на то, чтобы просто отключить эту возможность в версии под Linux, но в результате этот глюк все-таки удалось побороть.
Я не буду рассказывать все шаманства, которые я делал, чтобы побороть проблему, расскажу только суть. А суть оказалась в том, что в версии под Linux для обеспечения работы перетаскивания файлов из этого списка нужно сделать строку следующего вида:
«file:/media/Storage/Workdir/outwiker/Примеры вики/samplewiki/Тесты/dd/__attach/CurrentPagePanel.py\r\n
file:/media/Storage/Workdir/outwiker/Примеры вики/samplewiki/Тесты/dd/__attach/guicontroller.py\r\n
file:/media/Storage/Workdir/outwiker/Примеры вики/samplewiki/Тесты/dd/__attach/HtmlTextEditor.py\r\n
file:/media/Storage/Workdir/outwiker/Примеры вики/samplewiki/Тесты/dd/__attach/htmlview.py»
По крайней мере так поступает класс wx.FileDataObject. Так как wx.FileDataObject — это всего лишь обертка над wxFileDataObject из wxWidgets, то пришлось лезть в изначальные исходники, а там есть такие строки:
bool wxFileDataObject::GetDataHere(void *buf) const
{
wxString filenames;
for (size_t i = 0; i < m_filenames.GetCount(); i++)
{
filenames += wxT("file:");
filenames += m_filenames[i];
filenames += wxT("\r\n");
}
memcpy( buf, filenames.mbc_str(), filenames.length() + 1 );
return true;
}
size_t wxFileDataObject::GetDataSize() const
{
size_t res = 0;
for (size_t i = 0; i < m_filenames.GetCount(); i++)
{
// This is junk in UTF-8
res += m_filenames[i].length();
res += 5 + 2; // "file:" (5) + "\r\n" (2)
}
return res + 1;
}
Первая функция копирует подготовленыне данные в буфер, вторая вычисляет размер данных. Я не могу сказать на 100%, что ошибка именно здесь, но в методе wxFileDataObject::GetDataSize() есть строка
res += m_filenames[i].length();
которая помечена подозрительным комментарием. И мне кажется, что в данном случае нужно все-таки считать не размер строки в символах, а размер строки в байтах. Смущает правда, то, что GetDataSize() не используется в GetDataHere(), которая по идее должна возвращать результирующую строку.
В классе wxPython wx.FileDataObject есть свой аналог GetDataHere(), который тоже возвращает результирующую строку. Как оказалось, этот метод как раз и возвращает обрезанную строку.
К счастью, в wxWidgets/wxPython можно создавать свои классы для перетаскиваемых объектов. В данном случае, чтобы обойти проблему, нужно просто написать свой аналог wx.FileDataObject, который делает то же самое, но правильно. Для этого надо сделать класс, производный от wx.PyDataObjectSimple и переопределить в нем методы GetDataSize(), GetDataHere() и SetData(). Честно говоря, я пока немножко схалявил и не стал переопределять метод SetData(), т.к. сам я его вызывать не буду (мне удобнее добавлять по одному файлу), а снаружи, как я понял, этот метод не вызывается.
В результате в меня получился вот такой класс:
class GtkFileDataObject (wx.PyDataObjectSimple):
"""
Класс данных для перетаскивания файлов. Использовать вместо wx.FileDataObject, который по сути не работает с Unicode
"""
def __init__ (self):
wx.PyDataObjectSimple.__init__ (self, wx.DataFormat (wx.DF_FILENAME))
self._fnames = []
def AddFile (self, fname):
self._fnames.append (fname)
def GetDataHere (self):
result = ""
for fname in self._fnames:
result += u"file:%s\r\n" % (fname)
# Преобразуем unicode в строку
return result.strip().encode("utf8")
def GetDataSize (self):
return len (self.GetDataHere())
Главное здесь было не забыть преобразовать объект unicode в string с помощью метода result.strip().encode(«utf8»).
В итоге в коде добавилась еще одна зависимость кода от ОС: в версии под Windows используется исходный класс wx.FileDataObject (там Drag & Drop реализуется намного сложнее), а под Linux используется класс GtkFileDataObject.
Когда я написал на форуме по wxWidgets про эту проблему, то мне кинули ссылку на багтрекер, где уже зарегистрировали такую проблему год назад, но до сих пор ее еще не решили.
И с заключении отдельно хотелось бы сказать про справку по WxPython/wxWidgets. В справке до сих пор встречаются противоречащие друг другу фразы о том, работает ли класс wx.FileDataObject где-то кроме Windows. Вот, например, одна из цитат:
Warning: Under all non-Windows platforms this class is currently «input-only», i.e. you can receive the files from another application, but copying (or dragging) file(s) from a wxWidgets application is not currently supported. PS: GTK2 should work as well.
Где-то в другом месте писали, что этот класс работает только под Windows, хотя уже прошло несколько лет с того момента, как появилась реализация и под wxGTK.
В общем, хочется верить, что когда-нибудь этот баг когда-нибудь закроют, а еще хочется верить, что когда-нибудь в wxPython появится официальный компонент для работы с WebKit (сейчас он есть, но только под Mac).
PS. Вы можете подписаться на новости сайта через RSS, Группу Вконтакте или Канал в Telegram.
Leave a comment