Ir al contenido principal

Ralsina.Me — El sitio web de Roberto Alsina

Publicaciones sobre qt (publicaciones antiguas, página 11)

Making deployment of desktop Python apps trivial: an idea

Di­ce google que di­ce es­to.

Proprietor and printer in front of Schwartz Print Shop in Minneapolis

He­re's what I'm thi­nkin­g: how hard could it be to make Py­Qt app de­plo­y­ment ab­so­lu­te­ly ea­s­y? We­ll, I am gues­sin­g: not ve­ry har­d.

He­re's the tri­ck: see what wo­rks in the real worl­d, and adopt it.

Ques­tio­n: what has de­plo­yed bi­llions of apps and has its users ha­pp­y? An­swe­r: pho­nes app sto­res.

Ques­tio­n: how do they wo­rk? An­swe­r: we­ll, tha­t's not that shor­t, so le­t's start ex­plai­nin­g.

As I see it, a rea­so­na­ble app sto­re has the fo­llo­wing com­po­nen­ts:

A Stable Deployment Target

You can't de­ploy from the sto­re if you do­n't know what you are de­plo­ying in­to. If the tar­get pla­tform is shak­y, you just can't know how to de­ploy wi­thout user as­sis­tan­ce, and we are tr­ying to make this ea­sy for the use­r, whi­ch means tha­t's not ac­cep­ta­ble.

So, wha­t's a sta­ble de­plo­y­ment tar­get we can pro­vi­de?

  • Py­­Qt (so we can de­­ploy GUIs to all ma­­jor desk­­top pla­­tfo­r­­ms)

  • Py­­thon stan­­dard li­­bra­­ry

  • Se­­le­c­ted mo­­­du­­les

What can be (and should be) bund­led wi­th the app?

  • Pu­­re py­­thon mo­­­du­­les

  • Ar­­two­­­rk and other re­­sou­r­­ces

What may be bund­le­d:

  • Py­­thon mo­­­du­­les wri­­tten in C/­­C++, but you then ha­­ve to re­­do the app for ea­­ch pla­­tfo­r­­m, and that ki­n­­da su­­cks.

Deployment Services

  • Apps should be able to che­­ck if the­­re is a new ve­r­­sion of them in the sto­­­re, to ask for upgra­­des.

  • Apps should be added by the de­­plo­­­y­­ment pla­­tform ni­­ce­­ly in­­to the host sys­­te­­m's me­­nus, desk­­to­­­p, etc.

Monetization Services

  • So­­­me way to cha­r­­ge for app­s. Even for open sou­r­­ce app­s, you could ask for U$S0.99 if you in­s­­ta­­ll them th­­rou­­gh the sto­­­re. Op­­tio­­­na­­l, of cou­r­se, and up to the app ow­­ne­­r.

  • Ad pla­­tfo­r­­m? The­­re must be a good one for desk­­top apps so­­­mewhe­­re?

The Store Itself

  • A we­b­­si­­te that do­­wn­­loads a "pa­­cka­­ge" as­­so­­­ciated wi­­th a lo­­­cal de­­plo­­­y­­ment appli­­ca­­tio­­n.

  • A app sto­­­re app. In­s­­ta­­ll things not via we­­b, but via a desk­­top appli­­ca­­tio­­n.

I do­n't ex­pect a func­tio­nal ver­sion of this would take me mo­re than a week wo­rking fu­ll­ti­me to im­ple­men­t. Of cour­se then the­re are all sor­ts of usa­bi­li­ty, looks, etc. things to con­si­de­r.

An­d... I am going to do so­me­thing I ve­ry ra­re­ly do. I am going to ask for mo­ne­y.

As an ex­pe­ri­men­t, I ha­ve se­tup a pro­ject at http://www.in­die­go­go­.­co­m/­Q­t-S­hop and set a fun­ding goal of U$S 600.

The­re you can fund me. I pro­mi­se that if the pro­ject is to­ta­lly fun­de­d, I wi­ll de­li­ve­r. If it is­n'­t, I may de­li­ver an­ywa­y. I would pre­fer to ha­ve the mo­ney thou­gh.

The pla­tform would be re­lea­sed un­der GPL­v2 or la­te­r.

Por esto es que Qt y PyQt valen la pena

¿Por qué es­tán bue­nos Qt y Py­Q­t?

Wi­dget re­pro­duc­tor de au­dio:

# -*- coding: utf-8 -*-

import sys, os
from PyQt4 import QtCore, QtGui, uic
from PyQt4.phonon import Phonon
import icons_rc

class AudioPlayer(QtGui.QWidget):
    def __init__(self, url, parent = None):

        self.url = url

        QtGui.QWidget.__init__(self, parent)
        self.setSizePolicy(QtGui.QSizePolicy.Expanding,
            QtGui.QSizePolicy.Preferred)


        self.player = Phonon.createPlayer(Phonon.MusicCategory,
            Phonon.MediaSource(url))
        self.player.setTickInterval(100)
        self.player.tick.connect(self.tock)

        self.play_pause = QtGui.QPushButton(self)
        self.play_pause.setIcon(QtGui.QIcon(':/icons/player_play.svg'))
        self.play_pause.clicked.connect(self.playClicked)
        self.player.stateChanged.connect(self.stateChanged)

        self.slider = Phonon.SeekSlider(self.player , self)

        self.status = QtGui.QLabel(self)
        self.status.setAlignment(QtCore.Qt.AlignRight |
            QtCore.Qt.AlignVCenter)

        self.download = QtGui.QPushButton("Download", self)
        self.download.clicked.connect(self.fetch)

        layout = QtGui.QHBoxLayout(self)
        layout.addWidget(self.play_pause)
        layout.addWidget(self.slider)
        layout.addWidget(self.status)
        layout.addWidget(self.download)

    def playClicked(self):
        if self.player.state() == Phonon.PlayingState:
            self.player.pause()
        else:
            self.player.play()

    def stateChanged(self, new, old):
        if new == Phonon.PlayingState:
            self.play_pause.setIcon(QtGui.QIcon(':/icons/player_pause.svg'))
        else:
            self.play_pause.setIcon(QtGui.QIcon(':/icons/player_play.svg'))

    def tock(self, time):
        time = time/1000
        h = time/3600
        m = (time-3600*h) / 60
        s = (time-3600*h-m*60)
        self.status.setText('%02d:%02d:%02d'%(h,m,s))

    def fetch(self):
        print 'Should download %s'%self.url

def main():
    app = QtGui.QApplication(sys.argv)
    window=AudioPlayer(sys.argv[1])
    window.show()
    # It's exec_ because exec is a reserved word in Python
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()

Wi­dget re­pro­duc­tor de vi­deo:

import sys, os
from PyQt4 import QtCore, QtGui, uic
from PyQt4.phonon import Phonon
import icons_rc

class VideoPlayer(QtGui.QWidget):
    def __init__(self, url, parent = None):

        self.url = url

        QtGui.QWidget.__init__(self, parent)
        self.setSizePolicy(QtGui.QSizePolicy.Expanding,
            QtGui.QSizePolicy.Preferred)


        self.player = Phonon.VideoPlayer(Phonon.VideoCategory,self)
        self.player.load(Phonon.MediaSource(self.url))
        self.player.mediaObject().setTickInterval(100)
        self.player.mediaObject().tick.connect(self.tock)

        self.play_pause = QtGui.QPushButton(self)
        self.play_pause.setIcon(QtGui.QIcon(':/icons/player_play.svg'))
        self.play_pause.clicked.connect(self.playClicked)
        self.player.mediaObject().stateChanged.connect(self.stateChanged)

        self.slider = Phonon.SeekSlider(self.player.mediaObject() , self)

        self.status = QtGui.QLabel(self)
        self.status.setAlignment(QtCore.Qt.AlignRight |
            QtCore.Qt.AlignVCenter)

        self.download = QtGui.QPushButton("Download", self)
        self.download.clicked.connect(self.fetch)
        topLayout = QtGui.QVBoxLayout(self)
        topLayout.addWidget(self.player)
        layout = QtGui.QHBoxLayout(self)
        layout.addWidget(self.play_pause)
        layout.addWidget(self.slider)
        layout.addWidget(self.status)
        layout.addWidget(self.download)
        topLayout.addLayout(layout)
        self.setLayout(topLayout)

    def playClicked(self):
        if self.player.mediaObject().state() == Phonon.PlayingState:
            self.player.pause()
        else:
            self.player.play()

    def stateChanged(self, new, old):
        if new == Phonon.PlayingState:
            self.play_pause.setIcon(QtGui.QIcon(':/icons/player_pause.svg'))
        else:
            self.play_pause.setIcon(QtGui.QIcon(':/icons/player_play.svg'))

    def tock(self, time):
        time = time/1000
        h = time/3600
        m = (time-3600*h) / 60
        s = (time-3600*h-m*60)
        self.status.setText('%02d:%02d:%02d'%(h,m,s))

    def fetch(self):
        print 'Should download %s'%self.url

def main():
    app = QtGui.QApplication(sys.argv)
    window=VideoPlayer(sys.argv[1])
    window.show()
    # It's exec_ because exec is a reserved word in Python
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()

...

Aplicaciones de escritorio y nubes (con video)

Lo ma­lo es, por su­pues­to, que a ve­ces es mu­cho más con­ve­nien­te usar una apli­ca­ción we­b. Por ejem­plo, he aban­do­na­do a mi pro­pio be­bé (uR­S­Sus) por­que google rea­der es más fá­cil y con­ve­nien­te.

Pe­ro en­ton­ces pen­sé... ¿qué me mo­les­ta de uR­S­Sus? ¡Y son bas­tan­tes co­sas!

  1. No es­­tá en to­­­das las co­m­­pu­­ta­­do­­­ras que uso. Eso quie­­re de­­cir que ja­­más po­­­dré usar­­la de fo­r­­ma ex­­clu­­si­­va.

  2. Es ba­s­­tan­­te inú­­til sin una co­­­ne­­xión a In­­te­r­­net (pe­­ro ta­m­­bién lo es google rea­­de­­r).

  3. Co­­­mo no la pue­­do usar ex­­clu­­si­­va­­men­­te, te­r­­mino con fee­­ds en uR­S­­Sus que no es­­tán en google rea­­der y vi­­ce­­ve­r­­s­a.

  4. Es len­­tí­­si­­ma.

En­ton­ces de­ci­dí ver que pue­do ha­cer al res­pec­to sin aban­do­nar el la­do bue­no de uR­S­Sus:

  1. Me gus­­ta más que una apli­­ca­­ción we­­b, po­r­­que es de es­­cri­­to­­­rio.

  2. Ha­­ce co­­sas co­­­mo abrir el si­­tio en vez de mo­s­­trar el post del feed (bue­­no pa­­ra fee­­ds de co­n­­te­­ni­­do pa­r­­cia­­l)

  3. La hi­­ce yo (sí, eso es un fea­­tu­­re pa­­ra mí. Me gus­­ta te­­ner pro­­­gra­­mas que yo hi­­ce)

En­ton­ce­s, es­te in­ten­to de rees­cri­bir el lec­tor RSS de es­cri­to­rio pro­du­jo es­to:

Co­mo se pue­de ver en el vi­deo, es­te lec­tor sin­cro­ni­za la lis­ta de sus­crip­cio­nes con google. Tam­bién even­tual­men­te sin­cro­ni­za­rá pos­ts leí­do­s/no leí­do­s.

Si­gue pu­dien­do abrir si­tios com­ple­tos en vez de pos­ts, tie­ne/­ten­drá un muy buen mo­do offli­ne (pá­gi­nas com­ple­tas cap­tu­ra­das co­mo imá­ge­nes, por ejem­plo­), y... es muy muy rá­pi­do.

Es mu­cho más rá­pi­do que google rea­der en ch­ro­miu­m, y mu­chí­si­mo más rá­pi­do que uR­S­Sus. Eso es por­que es­tá me­jor el có­di­go, así que pro­ba­ble­men­te sig­ni­fi­ca que an­tes te­nía muer­te ce­re­bral y he ex­pe­ri­men­ta­do una le­ve me­jo­ría.

El có­di­go no es ap­to pa­ra pu­bli­ca­ción (por ejem­plo, el sche­ma de la ba­se de da­tos va a cam­bia­r) pe­ro se pue­de pro­ba­r: http://­co­de.­google.­co­m/­p/kakawa­na/­sour­ce/­che­ckout

Capturing a webpage as an image using Pyhon and Qt

En­ton­ces googleé y me en­contré con Cu­ty­Capt que usa Qt y We­bKit pa­ra con­ver­tir pá­gi­nas web en imá­ge­nes. ¡Me sir­ve!

Co­mo quie­ro usar­lo des­de una apli­ca­ción Py­Q­t, tie­ne sen­ti­do ha­cer lo mis­mo que Cu­ty­Capt ha­ce, pe­ro des­de un mó­du­lo py­thon así que acá es­tá una im­ple­men­ta­ción ra­pi­di­ta que fun­cio­na pa­ra mí, un­que ca­re­ce de mu­chos fea­tu­res de Cu­ty­Cap­t.

Con un po­co más de es­fuer­zo, pue­de guar­dar co­mo PDF o SV­G, lo que per­mi­ti­ría usar­la ca­si co­mo una pá­gi­na web de ver­da­d.

Se usa así:

python  capty.py http://www.kde.org kde.png

Y acá es­tá el có­di­go [des­car­gar cap­ty.­py]

# -*- coding: utf-8 -*-

"""This tries to do more or less the same thing as CutyCapt, but as a
python module.

This is a derived work from CutyCapt: http://cutycapt.sourceforge.net/

////////////////////////////////////////////////////////////////////
//
// CutyCapt - A Qt WebKit Web Page Rendering Capture Utility
//
// Copyright (C) 2003-2010 Bjoern Hoehrmann <bjoern@hoehrmann.de>
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// $Id$
//
////////////////////////////////////////////////////////////////////

"""

import sys
from PyQt4 import QtCore, QtGui, QtWebKit


class Capturer(object):
    """A class to capture webpages as images"""

    def __init__(self, url, filename):
        self.url = url
        self.filename = filename
        self.saw_initial_layout = False
        self.saw_document_complete = False

    def loadFinishedSlot(self):
        self.saw_document_complete = True
        if self.saw_initial_layout and self.saw_document_complete:
            self.doCapture()

    def initialLayoutSlot(self):
        self.saw_initial_layout = True
        if self.saw_initial_layout and self.saw_document_complete:
            self.doCapture()

    def capture(self):
        """Captures url as an image to the file specified"""
        self.wb = QtWebKit.QWebPage()
        self.wb.mainFrame().setScrollBarPolicy(
            QtCore.Qt.Horizontal, QtCore.Qt.ScrollBarAlwaysOff)
        self.wb.mainFrame().setScrollBarPolicy(
            QtCore.Qt.Vertical, QtCore.Qt.ScrollBarAlwaysOff)

        self.wb.loadFinished.connect(self.loadFinishedSlot)
        self.wb.mainFrame().initialLayoutCompleted.connect(
            self.initialLayoutSlot)

        self.wb.mainFrame().load(QtCore.QUrl(self.url))

    def doCapture(self):
        self.wb.setViewportSize(self.wb.mainFrame().contentsSize())
        img = QtGui.QImage(self.wb.viewportSize(), QtGui.QImage.Format_ARGB32)
        painter = QtGui.QPainter(img)
        self.wb.mainFrame().render(painter)
        painter.end()
        img.save(self.filename)
        QtCore.QCoreApplication.instance().quit()

if __name__ == "__main__":
    """Run a simple capture"""
    app = QtGui.QApplication(sys.argv)
    c = Capturer(sys.argv[1], sys.argv[2])
    c.capture()
    app.exec_()

Contents © 2000-2020 Roberto Alsina