В проект wxPython, начиная с версии 2.9 была добавлена возможность обновлять ваши приложения с помощью новой библиотеки, которая называется wx.lib.softwareupdate. Возможность обновлять программное обеспечение была реализована посредством смешанного класса, использующего пакет Esky. Насколько мне известно, данная технология позволяет проводить только запрашиваемые обновления и не работает в фоновом режиме.
Приступая к работе
Если у вас не установлена Esky, то вы можете сделать это посредством pip
1 |
pip install esky |
В зависимости от того, какой платформой вы пользуетесь, вам также может понадобиться загрузка пакета двоичного программирования. Например, если вы пользуете Windows, вам понадобится py2exe, в случае использования Mac — py2app. Мы будем создавать простое средство просмотра фотографий, в которое мы затем добавим обновление. В данной статье мы работаем на Windows, но на Mac процесс не должен отличаться. Вы можете использовать pip для установки py2exe и py2app.
Нам также понадобится иерархия папок, которая должна будет выглядеть примерно так:
1 2 3 |
/Releases /image_viewer0.0.1 /image_viewer0.1.0 |
Теперь вам нужно создать начальную версию программного обеспечения и сохранить её в папке image_viewer0.0.1.
Добавление кода обновления в начальный релиз
Так как мы знаем, что в будущем нам нужно будет добавлять дополнительные функции, мы пишем программу таким образом, чтобы она могла самостоятельно обновляться. Давайте взглянем на первую версию программного обеспечения:
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 |
import os import wx from wx.lib.softwareupdate import SoftwareUpdate class PhotoCtrl(wx.App, SoftwareUpdate): """ Класс средства для просмотра изображений """ def __init__(self, redirect=False, filename=None): wx.App.__init__(self, redirect, filename) BASEURL = "http://127.0.0.1:8000" self.InitUpdates(BASEURL, BASEURL + "/" + 'ChangeLog.txt') self.SetAppDisplayName('Image Viewer') self.CheckForUpdate() self.frame = wx.Frame(None, title='Photo Control') self.panel = wx.Panel(self.frame) self.PhotoMaxSize = 500 self.createWidgets() self.frame.Show() def createWidgets(self): instructions = 'Browse for an image' img = wx.Image(240,240) self.imageCtrl = wx.StaticBitmap(self.panel, wx.ID_ANY, wx.Bitmap(img)) instructLbl = wx.StaticText(self.panel, label=instructions) self.photoTxt = wx.TextCtrl(self.panel, size=(200,-1)) browseBtn = wx.Button(self.panel, label='Browse') browseBtn.Bind(wx.EVT_BUTTON, self.onBrowse) self.mainSizer = wx.BoxSizer(wx.VERTICAL) self.sizer = wx.BoxSizer(wx.HORIZONTAL) self.mainSizer.Add(wx.StaticLine(self.panel, wx.ID_ANY), 0, wx.ALL|wx.EXPAND, 5) self.mainSizer.Add(instructLbl, 0, wx.ALL, 5) self.mainSizer.Add(self.imageCtrl, 0, wx.ALL, 5) self.sizer.Add(self.photoTxt, 0, wx.ALL, 5) self.sizer.Add(browseBtn, 0, wx.ALL, 5) self.mainSizer.Add(self.sizer, 0, wx.ALL, 5) self.panel.SetSizer(self.mainSizer) self.mainSizer.Fit(self.frame) self.panel.Layout() def onBrowse(self, event): """ Поиск файлов для отображения """ wildcard = "JPEG files (*.jpg)|*.jpg" dialog = wx.FileDialog(None, "Choose a file", wildcard=wildcard, style=wx.FD_OPEN) if dialog.ShowModal() == wx.ID_OK: self.photoTxt.SetValue(dialog.GetPath()) dialog.Destroy() self.onView() def onView(self): """ Попытка загрузить изображение и вывести его на экран """ filepath = self.photoTxt.GetValue() img = wx.Image(filepath, wx.BITMAP_TYPE_ANY) # Масштабируем изображение, сохраняя соотношение сторон W = img.GetWidth() H = img.GetHeight() if W > H: NewW = self.PhotoMaxSize NewH = self.PhotoMaxSize * H / W else: NewH = self.PhotoMaxSize NewW = self.PhotoMaxSize * W / H img = img.Scale(NewW,NewH) self.imageCtrl.SetBitmap(wx.Bitmap(img)) self.panel.Refresh() self.mainSizer.Fit(self.frame) if __name__ == '__main__': app = PhotoCtrl() app.MainLoop() |
Для активации обновления программного обеспечения, нам нужно импортировать класс SoftwareUpdate из wx.lib.softwareupdate. Теперь нам нужно создать подкласс для обеих wx.App и SoftwareUpdate, так как SoftwareUpdate является смешанным классом.
Есть вопросы по Python?
На нашем форуме вы можете задать любой вопрос и получить ответ от всего нашего сообщества!
Паблик VK
Одно из самых больших сообществ по Python в социальной сети ВК. Видео уроки и книги для вас!
Затем в конструкторе __init__ нам нужно вызвать InitUpdates с помощью URL по нашему выбору плюс та URL, которая связана с ChangeLog.txt. Мы настраиваем отображение названия приложения и, наконец, вызываем CheckForUpdate. Вот и всё. Теперь нам нужно запаковать всё это в исполняемый файл.
Вам нужно будет создать скрипт setup.py, в котором будет код, размещённый ниже, и поместить его в ту же директорию, где лежит начальный скрипт релиза.
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 |
import sys, os from esky import bdist_esky from setuptools import setup import version # Специфические настройки платформы для Windows/py2exe if sys.platform == "win32": import py2exe FREEZER = 'py2exe' FREEZER_OPTIONS = dict(compressed = 0, optimize = 0, bundle_files = 3, dll_excludes = ['MSVCP90.dll', 'mswsock.dll', 'powrprof.dll', 'USP10.dll',], ) exeICON = 'mondrian.ico' # Специфические настройки платформы для Mac/py2app elif sys.platform == "darwin": import py2app FREEZER = 'py2app' FREEZER_OPTIONS = dict(argv_emulation = False, iconfile = 'mondrian.icns', ) exeICON = None # Общие настройки NAME = "wxImageViewer" APP = [bdist_esky.Executable("image_viewer.py", gui_only=True, icon=exeICON, )] DATA_FILES = [ 'mondrian.ico' ] ESKY_OPTIONS = dict( freezer_module = FREEZER, freezer_options = FREEZER_OPTIONS, enable_appdata_dir = True, bundle_msvcrt = True, ) # Собираем воедино приложение и набор esky setup( name = NAME, scripts = APP, version = version.VERSION, data_files = DATA_FILES, options = dict(bdist_esky=ESKY_OPTIONS), ) |
Вам также понадобиться файл version.py, в котором будет следующее:
1 |
VERSION='0.0.1' |
Теперь вы действительно готовы к созданию исполнительного файла. Откройте терминал (cmd.exe в Windows), и укажите папку, куда вы поместили все эти файлы. Вам также стоит положить в папку несколько иконок, которые вы сможете найти в хранилище кода книги (для более детальной информации ознакомьтесь с введением).
Вам это нужно, так как скрипт setup.py написан таким образом, что будет искать иконки. Окей, теперь нам нужно создать дистрибутив. Впечатайте следующие строки в ваш терминал:
1 |
python setup.py bdist_esky |
Эта команда предполагает, что в вашем пути файла есть Python. Если же нет, вам нужно будет добавить или просто изменить полный путь к Python. Как только вы запустите эту команду, вы увидите целую кипу исходящих данных. Если всё пройдёт нормально, в конце у вас будет две папки build и dist. Нас не особо интересует первая из них. Во второй папке должен быть лишь один файл, который называется как-то так: wxImageViewer-0.0.1.win32.zip
Подготовка нового релиза
Вашим пользователям нравится, когда приложении появляются новые возможности. Начальная версия нашей программы позволял только использовать проводник для поиска необходимого файла. Давайте добавим несколько новых опций в ваше приложение:
- Кнопки «Вперёд» и «Назад», чтобы вы могли перемещаться между файлами в папке.
- Возможность запуска «Слайд-шоу».
Так как это будет новый релиз, создайте в Python новый скрипт с таким же названием, как и у первоначального, а затем сохраните его в папке image_viewer0.1.0, созданной нами ранее.’
Для того, чтобы всё было крайне просто – скопируйте код, который вы найдёте ниже, в ваш скрипт. Или же вы можете взять первоначальную версию кода и обновлять её в процессе. Я оставлю это решение за вами. Следующий сниппет достаточно длинный, так что я разобью его на несколько логических блоков, чтобы мне было проще вам всё объяснить.
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 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
import glob import os import wx from wx.lib.pubsub import Publisher from wx.lib.softwareupdate import SoftwareUpdate class ViewerPanel(wx.Panel): """""" def __init__(self, parent): """Constructor""" wx.Panel.__init__(self, parent) width, height = wx.DisplaySize() self.picPaths = [] self.currentPicture = 0 self.totalPictures = 0 self.photoMaxSize = height - 200 Publisher().subscribe(self.updateImages, ("update images")) self.slideTimer = wx.Timer(None) self.slideTimer.Bind(wx.EVT_TIMER, self.update) self.layout() def layout(self): """ Размещаем виджеты на панели """ self.mainSizer = wx.BoxSizer(wx.VERTICAL) btnSizer = wx.BoxSizer(wx.HORIZONTAL) img = wx.EmptyImage(self.photoMaxSize,self.photoMaxSize) self.imageCtrl = wx.StaticBitmap(self, wx.ID_ANY, wx.BitmapFromImage(img)) self.mainSizer.Add(self.imageCtrl, 0, wx.ALL|wx.CENTER, 5) self.imageLabel = wx.StaticText(self, label="") self.mainSizer.Add(self.imageLabel, 0, wx.ALL|wx.CENTER, 5) btnData = [("Previous", btnSizer, self.onPrevious), ("Slide Show", btnSizer, self.onSlideShow), ("Next", btnSizer, self.onNext)] for data in btnData: label, sizer, handler = data self.btnBuilder(label, sizer, handler) self.mainSizer.Add(btnSizer, 0, wx.CENTER) self.SetSizer(self.mainSizer) def btnBuilder(self, label, sizer, handler): """ Создаём кнопку, привязываем её к хэндлеру событий и добавляем её в сайзер """ btn = wx.Button(self, label=label) btn.Bind(wx.EVT_BUTTON, handler) sizer.Add(btn, 0, wx.ALL|wx.CENTER, 5) def loadImage(self, image): """ Загружаем изображение, которое будет отображаться в приложении """ image_name = os.path.basename(image) img = wx.Image(image, wx.BITMAP_TYPE_ANY) # Масштабируем изображение, сохраняя соотношение сторон W = img.GetWidth() H = img.GetHeight() if W > H: NewW = self.photoMaxSize NewH = self.photoMaxSize * H / W else: NewH = self.photoMaxSize NewW = self.photoMaxSize * W / H img = img.Scale(NewW,NewH) self.imageCtrl.SetBitmap(wx.BitmapFromImage(img)) self.imageLabel.SetLabel(image_name) self.Refresh() Publisher().sendMessage("resize", "") def nextPicture(self): """ Загружает следующее изображение из директории """ if self.currentPicture == self.totalPictures-1: self.currentPicture = 0 else: self.currentPicture += 1 self.loadImage(self.picPaths[self.currentPicture]) def previousPicture(self): """ Отображает предыдущее изображение из директории """ if self.currentPicture == 0: self.currentPicture = self.totalPictures - 1 else: self.currentPicture -= 1 self.loadImage(self.picPaths[self.currentPicture]) def update(self, event): """ Вызывается, когда событие таймера slideTimer было запущено. Загружает следующее изображение из папки, используя вызов метода nextPicture """ self.nextPicture() def updateImages(self, msg): """ Обновляет список picPaths, чтобы в нём оказались пути ко всем изображениям в папке """ self.picPaths = msg.data self.totalPictures = len(self.picPaths) self.loadImage(self.picPaths[0]) def onNext(self, event): """ Вызывает метод nextPicture """ self.nextPicture() def onPrevious(self, event): """ Вызывает метод previousPicture """ self.previousPicture() def onSlideShow(self, event): """ Запускает и останавливает слайд-шоу """ btn = event.GetEventObject() label = btn.GetLabel() if label == "Slide Show": self.slideTimer.Start(3000) btn.SetLabel("Stop") else: self.slideTimer.Stop() btn.SetLabel("Slide Show") |
Первое что вам броситься в глаза, это разбивка приложения на серии классов. Теперь у нас есть ViewerPanel, содержащая все главные виджеты. Также вы заметите, что появились кнопки для навигации между всеми изображениями папки. У нас также есть функция слайд-шоу, которая будет переключать изображения из папки каждые три секунды.
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 |
class ViewerFrame(wx.Frame): """""" def __init__(self): """Constructor""" wx.Frame.__init__(self, None, title="Image Viewer") panel = ViewerPanel(self) self.folderPath = "" Publisher().subscribe(self.resizeFrame, ("resize")) self.initToolbar() self.sizer = wx.BoxSizer(wx.VERTICAL) self.sizer.Add(panel, 1, wx.EXPAND) self.SetSizer(self.sizer) self.Show() self.sizer.Fit(self) self.Center() def initToolbar(self): """ Инициализируем панель инструментов """ self.toolbar = self.CreateToolBar() self.toolbar.SetToolBitmapSize((16,16)) open_ico = wx.ArtProvider.GetBitmap( wx.ART_FILE_OPEN, wx.ART_TOOLBAR, (16,16)) openTool = self.toolbar.AddSimpleTool(wx.ID_ANY, open_ico, "Open", "Open an Image Directory") self.Bind(wx.EVT_MENU, self.onOpenDirectory, openTool) self.toolbar.Realize() def onOpenDirectory(self, event): """ Открывает DirDialog, позволяющий пользователю открыть папку с картинками """ dlg = wx.DirDialog(self, "Choose a directory", style=wx.DD_DEFAULT_STYLE) if dlg.ShowModal() == wx.ID_OK: self.folderPath = dlg.GetPath() print self.folderPath picPaths = glob.glob(self.folderPath + "\\*.jpg") print picPaths Publisher().sendMessage("update images", picPaths) def resizeFrame(self, msg): """""" self.sizer.Fit(self) |
Здесь мы создали подкласс из wx.Frame. Мы добавили простую панель инструментов, позволяющую открыть папку с изображениями. Кроме указанных функций, данный класс больше ничего не делает. Как бы то ни было, вы можете расширить функциональность посредством добавления панели состояния или меню.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class ImageApp(wx.App, SoftwareUpdate): """""" def __init__(self): """Constructor""" BASEURL = "http://127.0.0.1:8000" self.InitUpdates(BASEURL, BASEURL + 'ChangeLog.txt') self.CheckForUpdate() frame = ViewerFrame() self.SetTopWindow(frame) self.SetAppDisplayName('Image Viewer') return True if __name__ == "__main__": app = ImageApp() app.MainLoop() |
Последним классом здесь является SoftwareUpdate смешанный с wx.App. Он также подтверждает класс фрейма. Это довольно прямолинейны класс, так что в будущем вам не удастся с ним взаимодействовать.
Теперь нам нужно взглянуть на новую версию setup.py, так как она немного отличается.
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 |
import sys, os from esky import bdist_esky from setuptools import setup import version # Специфические настройки платформы для Windows/py2exe if sys.platform == "win32": import py2exe includes = ["wx.lib.pubsub.*", "wx.lib.pubsub.core.*", "wx.lib.pubsub.core.kwargs.*"] FREEZER = 'py2exe' FREEZER_OPTIONS = dict(compressed = 0, optimize = 0, bundle_files = 3, dll_excludes = ['MSVCP90.dll', 'mswsock.dll', 'powrprof.dll', 'USP10.dll',], includes = includes ) exeICON = 'mondrian.ico' # Специфические настройки платформы для Mac/py2app elif sys.platform == "darwin": import py2app FREEZER = 'py2app' FREEZER_OPTIONS = dict(argv_emulation = False, iconfile = 'mondrian.icns', ) exeICON = None # Общие настройки NAME = "wxImageViewer" APP = [bdist_esky.Executable("image_viewer.py", gui_only=True, icon=exeICON, )] DATA_FILES = [ 'mondrian.ico' ] ESKY_OPTIONS = dict( freezer_module = FREEZER, freezer_options = FREEZER_OPTIONS, enable_appdata_dir = True, bundle_msvcrt = True, ) # Объединяем приложение и набор esky setup( name = NAME, scripts = APP, version = version.VERSION, data_files = DATA_FILES, options = dict(bdist_esky=ESKY_OPTIONS) ) |
Этот второй скрипт использует пабсаб из wxPython. В любом случае, py2exe самостоятельно этого не узнает, так что вам нужно указать ей на точное расположение пабсаб файлов, требующих захвата.
Не забудьте убедиться в том, что ваша версия файла version.py имеет более высокое релизное значение, чем оригинальный файл, иначе обновление не удастся. Вот какое я указал в своём приложении:
1 |
VERSION='0.0.2' |
Теперь нам нужно повторить тот же фокус с командной строкой, за исключением того, что в этой раз мы проделываем его с обновлённой директорией релиза.
1 |
python setup.py bdist_esky |
Скопируйте zip-файл в вашу папку downloads. Теперь нам нужно разместить эти файлы на localhost вашего компьютера. Чтобы сделать это войдите в вашу папку с загрузками посредством командной строки и запустите следующую команду:
1 |
python -m SimpleHTTPServer |
Теперь Python запустит небольшой HTTP-сервер, на котором размещены эти файлы. Если вы вставите в адресную строку вашего браузера http://127.0.0.1:8000 и перейдёте по этой ссылке, вы увидите это своими глазами. Теперь вы готовы к процессу обновления!
Обновление программы
Убедитесь в том, что вы разархивировали первоначальную версию вашего средства для просмотра изображений в какую-то из папок вашего компьютера. Затем запустите файл, который называется image_viewer.exe. Если всё идёт по плану, вы увидите следующее окно:
Продолжите, согласитесь на обновление, и программа попросит вас перезапустить приложение:
После того, как оно перезапустится, у вас появится новый интерфейс средства для просмотра изображений. Я заметил, что, когда я закрываю приложение, я получаю ошибку, которая оказалась предупреждением об использовании устаревшего программного обеспечения. Вы можете его проигнорировать, но, если вы всё-таки хотите что-то сделать – импортируйте модуль предупреждений и усмирите его.
Итоги
Сейчас вы уже должны быть готовы к тому, чтобы играть по-крупному. Вы также можете использовать AutoCheckForUpdate вместо CheckForUpdate, указав определённый промежуток между уведомлениями, чтобы они не выскакивали каждый раз, когда вы будете запускать программу.
Или, если вы хотите просто разместить функцию CheckForUpdate в хэндлер событий, использующий тригеры. Множество приложений так и делают, позволяя пользователю найти в меню пункт «Проверка обновлений». Используйте вашу навигацию и начинайте хакать! Существует также конкурент Esky, который называется goodasnew, так что вы можете использовать и его. Пока что он не интегрирован в wxPython, но кто знает, что может случиться в будущем.
Являюсь администратором нескольких порталов по обучению языков программирования Python, Golang и Kotlin. В составе небольшой команды единомышленников, мы занимаемся популяризацией языков программирования на русскоязычную аудиторию. Большая часть статей была адаптирована нами на русский язык и распространяется бесплатно.
E-mail: vasile.buldumac@ati.utm.md
Образование
Universitatea Tehnică a Moldovei (utm.md)
- 2014 — 2018 Технический Университет Молдовы, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
- 2018 — 2020 Технический Университет Молдовы, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»