Skip to main content

Ralsina.Me — Roberto Alsina's website

Posts about qt (old posts, page 11)

Making deployment of desktop Python apps trivial: an idea

Proprietor and printer in front of Schwartz Print Shop in Minneapolis

Here's what I'm think­ing: how hard could it be to make PyQt app de­ploy­ment ab­so­lute­ly easy? Well, I am guess­ing: not very hard.

Here's the trick: see what works in the re­al world, and adopt it.

Ques­tion: what has de­ployed bil­lions of apps and has its users hap­py? An­swer: phones app stores.

Ques­tion: how do they work? An­swer: well, that's not that short, so let's start ex­plain­ing.

As I see it, a rea­son­able app store has the fol­low­ing com­po­nents:

A Stable Deployment Target

You can't de­ploy from the store if you don't know what you are de­ploy­ing in­to. If the tar­get plat­form is shaky, you just can't know how to de­ploy with­out us­er as­sis­tance, and we are try­ing to make this easy for the user, which means that's not ac­cept­able.

So, what's a sta­ble de­ploy­ment tar­get we can provide?

  • PyQt (so we can de­­ploy GUIs to all ma­jor desk­­top plat­­for­m­s)

  • Python stan­­dard li­brary

  • Se­lec­t­ed mod­­ules

What can be (and should be) bun­dled with the ap­p?

  • Pure python mod­­ules

  • Art­­work and oth­­er re­­sources

What may be bun­dled:

  • Python mod­­ules writ­ten in C/C++, but you then have to re­­do the app for each plat­­for­m, and that kin­­da suck­­s.

Deployment Services

  • Apps should be able to check if there is a new ver­­sion of them in the store, to ask for up­­­grades.

  • Apps should be added by the de­­ploy­­ment plat­­form nice­­ly in­­­to the host sys­tem's menus, desk­­top, etc.

Monetization Services

  • Some way to charge for ap­p­s. Even for open source ap­p­s, you could ask for U$S0.99 if you in­­stall them through the store. Op­­tion­al, of course, and up to the app own­er.

  • Ad plat­­for­m? There must be a good one for desk­­top apps some­where?

The Store Itself

  • A we­b­site that down­loads a "pack­­age" as­­so­­ci­at­ed with a lo­­cal de­­ploy­­ment ap­­pli­­ca­­tion.

  • A app store ap­p. In­­stall things not via we­b, but via a desk­­top ap­­pli­­ca­­tion.

I don't ex­pect a func­tion­al ver­sion of this would take me more than a week work­ing full­time to im­ple­men­t. Of course then there are all sorts of us­abil­i­ty, look­s, etc. things to con­sid­er.

And... I am go­ing to do some­thing I very rarely do. I am go­ing to ask for mon­ey.

As an ex­per­i­men­t, I have set­up a project at http://www.in­diegogo.­com/Qt-Shop and set a fund­ing goal of U$S 600.

There you can fund me. I prom­ise that if the project is to­tal­ly fund­ed, I will de­liv­er. If it is­n't, I may de­liv­er any­way. I would pre­fer to have the mon­ey though.

The plat­form would be re­leased un­der GPLv2 or lat­er.

This is why Qt (and PyQt) are cool

Ale­jan­dro Dolina once wrote (and this is from mem­o­ry that's prob­a­bly 25 years old) of a round ta­ble dis­cussing "What's Tan­go?", and how af­ter two hours of dis­cussing the na­ture, char­ac­ter­is­tics and his­to­ry of tan­go, one of the mem­bers of the pan­el picked up a ban­doneón, played "El apache ar­genti­no" stood up and left with­out say­ing a word.

So, why are Qt and PyQt cool?

Au­dio play­er wid­get:

# -*- 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()

Video play­er wid­get:

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()

...

Desktop apps and clouds (with video)

I en­joy cre­at­ing desk­top ap­pli­ca­tion­s. That means I may be a mem­ber of a dy­ing breed, since web apps are go­ing to make us all ob­so­lete next week, but I do en­joy do­ing it.

The bad side of it is, of course that some­times it's much more con­ve­nient to use a web ap­pli­ca­tion. For ex­am­ple, I have aban­doned my own ba­by (uRSSus) be­cause google read­er is just eas­i­er and more con­ve­nient to use.

But then I thought... what both­ers me of uRSSus? And there are quite a few things!

  1. It's not in all com­put­ers I may use

    That means I will not ev­er be able to use it ex­­clu­­sive­­ly.

  2. It's pret­­ty use­­less with­­out an In­­ter­net con­nec­­tion (but so is google read­­er most­­ly)

  3. Since I can't use it ex­­clu­­sive­­ly, I end with feeds on uRSSus that are not on google read­­er and vicev­er­sa.

  4. It's freak­ing slow

So, I de­cid­ed to see what I could do about that with­out giv­ing up the good side of uRSSus:

  1. It looks much nicer than a web ap­p, be­­cause it looks like a desk­­top app

  2. It does things like open­ing the site in­­stead of show­ing the feed item (great for par­­tial con­­tent feed­s)

  3. I wrote it (yes, that's a fea­­ture for me. I like self­­-­­made pro­­gram­s)

So, this at­tempt at rewrit­ing the desk­top RSS read­er pro­duced this:

As you can see in the above video, this read­er syncs the sub­scrip­tion list to google read­er. It will al­so even­tu­al­ly sync your read­/un­read post­s.

It still can open full sites in­stead of feed item­s, it has/will have a heck of an off­line mode (full pages cap­tured as im­ages, for ex­am­ple), and... it's very very fast.

It's much faster than google read­er in Chromi­um, and hel­la faster than uRSSus. That was done via smarter cod­ing, so it prob­a­bly means I was brain­dead be­fore and ex­pe­ri­enced a mi­nor re­cov­ery.

The code is not fit for re­lease (for ex­am­ple, the data­base schema will change) but you can try it: http://­code.­google.­com/p/kakawana/­source/check­out

Slow-Slow and Fast-Fast (video)

My pre­vi­ous post ex­plained how to cache whole web pages as im­ages. Now see it in ac­tion. This is a light­weight RSS read­er, op­ti­mized for com­ic books (but it works for any feed) and for off­line use (but it works on­line too, of course).

Not ready for pub­lic use yet, but if you look around you can find the code some­where ;-)

Capturing a webpage as an image using Pyhon and Qt

For a small project I am do­ing I want­ed the ca­pa­bil­i­ty to see web pages off­line. So, I start­ed think­ing of a way to do that, and all so­lu­tions were an­noy­ing or im­prac­ti­cal.

So, I googled and found Cu­ty­Capt which us­es Qt and We­bKit to turn web pages in­to im­ages. Good enough for me!

Since I want­ed to use this from a PyQt ap­p, it makes sense to do the same thing Cu­ty­Capt does, but as a python mod­ule/scrip­t, so here's a quick im­ple­men­ta­tion that works for me, even if it lacks a bunch of Cu­ty­Cap­t's fea­tures.

With a lit­tle ex­tra ef­fort, it can even save as PDF or SVG, which would let you use it al­most like a re­al web page.

You just use it like this:

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

And here's the code [down­load 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