After several months thinking about it, and just two requests, I finally decided
satyr's code. I decided to use github because I already switched to
git, mainly for testing and understanding it. I think I
can live with
hg, althought branch management in
git seems to be given more
thought and a good implementation.
So, without further ado: satyr in github
Remember, it's still a test software, by no means polished or ready for human
consumption, and with very low development force. Still, I think it has some
nice features, like interchangeable skins and a well defined backend,
support, quick tag edition, reasonable collection managment, and, thanks to
Phonon, almost-gapless playback and things like «stop after playing the
current this file» (but not «after any given file» yet).
In Debian Sid it mostly works only with the
GStreamer backend; I haven't tried
xine one and I know
VLC does not emit a signal needed for queueing the
next song, so you have to press «next» after each song. AFAIK this is fixed
satyr-0.3.2 "I should install my own food" is out. The Changelog is not very impressive:
- We can save the list of queued Songs on exit and load it at startup.
- The queue position is not presented when editing the title of a Song that is queued.
- Setting the track number works now (was a bug).
- Fixed a bug in setup.py.
but the last one is somewhat important (Thanks Chipaca). Also, 2 months ago, I made the satyr-0.3.1 "freudian slip" release, when user 'dglent' from http://kde-apps.org/ found a packaging bug. It was not only a bugfixing revision, it also included new features:
- nowPlaying(), exported via dbus.
- 'now playing' plugin for irssi.
- Changes in tags is copied to selected cells (in the same column). This allows 'Massive tag writing'.
- Fixed "NoneType has no attribute 'row'" bug, but this is just a workaround.
- Forgot to install complex.ui.
Now go get it!
Today I sat down and tried to refactor
satyr once more after dinner. This time
I was trying to decouple the functionality related to multi-collection playlists
PlayListModel while moving it to the
default skin. The idea was to be
able to create another skin which used a
QTableView, which in turn would be a
trampoline for implementing tag editing and writing. But I started to stall a
little, and that normally provokes me to defocus, to dezone. When that happens,
I go and read some piled up posts in
This time I came around a post by Martin Michlmayr (who I read through Planet Debian) from 10 days ago which talks about lessons learned about free software projects. Actually the post is just a resume of 4 posts from the FreeDOS founder Jim Hall. At some point he writes «releases are important».
Bing! goes my head.
satyr is already 3 months, 12 days or 117 revisions
old and I hadn't released it yet, even after I promissed to do so almost a month
ago! The problem is that I kept adding features (and squashing bugs in
Phonon) and completely forgot about releasing. Just one semi-colon before
he also writes «initial users of the software should be recruited as
developers»... which users? If one doesn't release, one might never have users
So instead of the pharaonic refactor I had in mind (an in another branch,
bazaar) I wrote a
setup.py script in 15 minutes, massaged a
little the files (I had to create a package and modify almost all the files to
reflect this), tested a little, and produced a nice triplet of files:
satyr-0.1-beta1.tar.bz2 satyr-0.1-beta1.tar.gz satyr-0.1-beta1.zip
So there you have it, a realease! Ok, it's a 'beta1', but it's out. Go grab it, test it, complain about bugs, tell us you like it, suggest improvements, whatever! And tell your best friend to use it, even if you don't like it! Where to get it? Why, from the project's download's page, of course!
 Yes, once upon a time I developed my own feed reader called
it's implemented in
PyKDE3 and I don't plan to port it yet, even if somehow is
better for me than
akregator. A shame, reallly...
 in the same way that a µ-wave oven goes “bing”, not in an “Eureka!” way...
 I even fixed the need for the 'file' scheme in the Gstreamer backend.
More than two months ago I globed about QStrings and
The problem was this: my app accepts paths via command line, which are
KCmdLineOptions; which in turn converts everything to
QStrings. What I wanted were paths, which are more like
QStrings (because the latter have internally an unicode representation;
more on that later). Including
PyQt4 in the equation forced me to resort
QByteArray to get the path as a
str instead of using
PyQt4 doesn't export that function). But that's
only the beginning of the problem.
Take for instance this situation. I have a music collection that I've been
building for years now (more that 10, I think). In the old times of this
collection the filenames were encoded in
iso-8859-1. Then the future came
and converted all my machines to
utf-8. But only the software; the
filesystems were in one way or another inherited from system to system, from
machine to machine. So I ended with a mixture of utf and iso filenames, to
the point where I have a file whose filename is in iso, but the directory
where it is is in utf. Yes, I know, it is a mess. But if I take any decent
media player, I can play the file allright. That's because the filesystem
knows nothing of encodings (otherwise it would reject badly encoded
I just spent last saturday making sure that
satyr only stored filepaths in
QStrings. It took concentration, but having just
a bunch of classes and only 3 or 4 points where the filepaths are managed it
wasn't that difficult. Still, it took a day. But then, as I mentioned in
Phonon the is not able to play such files... or so I thought.
If you run
satyr after executing
export PHONON_XINE_DEBUG=1 you'll see a
Phonon debug info in the console (not that there is another way to
satyr right now anyways). Among all that info you'll see lines such as
void Phonon::Xine::XineStream::setMrl(const QByteArray&, Phonon::Xine::XineStream::StateForNewMrl) ... bool Phonon::Xine::XineStream::xineOpen(Phonon::State) xine_open succeeded for m_mrl = ...
If you're sharp enough (I'm not; sandsmark from
#phonon had to tell me)
you'll note the mention of MRL's. MRL's are
xine's URL for media. As any
URL, they can (and most of the time must) encode 'strange' characters with
the so-called "percent encoding". This means that no matter what encodings
the different parts of a filepath is in, I just add
file:// at the
beginning and then I can safely encode it scaping non-ascii characters to
%xx representations... or that's what the theory says. One thing to note is
file:// part must not be scaped;
xine complains that the file
does not exist in that case.
Looking for help in
Qt's classes one can find
QUrl and the already known
QByteArray. I can call
QByteArray.toPercentEnconding() from my
feed that to
QUrl.fromPercentEncoding() (which strangely returns a
QString, which is exactly what we're avoiding) or
But then the first function encodes too much, replacing
%3A%2F%2F. No fun.
Ok, let's try creating a
QByteArray with only the
file:// and then
toPercentEncoding() of the path only. It works:
But then calling
<span class="createlink">PyQt4</span>.QtCore.QUrl("file://xn--/home/mdione.../ltimo bondi a finisterre/07- la pequea novia del carioca-wkmz60758d.wav")
The URL got somehow puny-encoded,
which of course
xine doesn't recognize for local files.
Another option is to create an empty
QUrl.StrictMode so we avoid 50 lines of code that start
that try to escape everything all over again (and I already had some
double-or-even-triple-enconding nightmares parsing RSS/Atom feeds last year,
thank you), but we get puny-encoded again (maybe it is 'pwny-encoded'?).
Last resort: backtrack to the point were we created only one
with the path and call
toPercentEncoding(); feed that to the method
setEncodedPath() of an empty
QUrl. Then we add the last piece calling
setScheme('file') and we're ready! Of course we're not:
Notice the lack of the two
xine doesn't like it;
hence, I don't either.
Ok, this post got too long. I hope I can resolve this soon, I already spent too much time on it. At least a good part of it was expaining it, so others don't have to suffer the same as I did.
satyr will shortly be released, whether I fix this bug or not.
 Look at the size of that file! 6k lines to handle URL's! Who would say it was so difficult... Once more I'm remembered of how lucky I am to have this libraries at the tips of my fingers, yay!
One of the things I had to while developing
satyr is building a model for a
QListViewer. It should be straighforward from qt's documentation, but I found
a couple of things that I would like to put in a post, specially because there
doesn't seem to be much models in
PyQt4 easily found in the web.
According to its description, a subclass of
QAbstractListModel as this one
should mostly implement the
rowCount() methods, which is true.
This example creates a read-only model, so no need to implement
but given the simplicity of
data(), it doesn't seem too difficult to do. I
also wanted it to react when more
Songs were added on the fly.
data() is the most important one. It is not only used for
retrieving the data itself, but also some metadata useful for showing the data,
like icons and other stuff. For selecting what the caller wants, it refers a
Qt.ItemDataRole. The role for the data itself is
Qt.DisplayRole. One of the
particularities of this method is that it could be called with any vegetable as
input; namely, it can refer to a row that does not exist anymore or for metadata
that you don't care about. In those cases you must return an empty
None. So, a first implementation is:
def data (self, modelIndex, role): if modelIndex.isValid () and modelIndex.row ()<self.count and role==Qt.DisplayRole: # songForIndex() returns the Song corresponding to the row song= self.songForIndex (modelIndex.row ()) # formatSong() returns a QString with the data to show data= QVariant (self.formatSong (song)) else: data= QVariant () return data
This method, together with a
rowCount() that simply returns
enough for showing data that is already there. Notice that the
be not valid, and in this case we only care about its row because we're a list.
But then I wanted my
QListViewer to show songs progresively as they are
loaded/scanned and also as they are found as new. But then a problem arises:
the view is like a table of only one column. The width of this colunm at the
begining is the same width as the
QListView itself. But what happens when the
string shown is too big? What happens is that it gets chopped. We must inform
the view that some of the rows are bigger. That's where the metadata comes into
Another possible role is
Qt.SizeHintRole. If we return a size instead of an
QVariant, that size will be used to expand the column as needed, even
giving us a scrollbar if it's wider that the view.
Now, we're supposed to show the tags for the
Song (that's what
does if possible; if not, it simply returns the filepath), so this width should
be calculated based on the length of the string that represents the song. But
if we try to read the tags for all the songs as we load the
Collection, we end
up with too much disk activity before you can show anything to the user, which
is unacceptable. So instead we calculate based on the filepath, which is used
Songs with too few tags anyways. Here's the hacky code:
... # FIXME: kinda hacky self.fontMetrics= QFontMetrics (KGlobalSettings.generalFont ()) ... def data (self, modelIndex, role): if modelIndex.isValid () and modelIndex.row ()<self.count: song= self.songForIndex (modelIndex.row ()) if role==Qt.DisplayRole: data= QVariant (self.formatSong (song)) elif role==Qt.SizeHintRole: # calculate something based on the filepath data= QVariant (self.fontMetrics.size (Qt.TextSingleLine, song.filepath)) else: data= QVariant () else: data= QVariant () return data
The last point then is reacting to
Songs are added on the fly. This is also
easy: you tell the views you're about to insert rows, you insert them, tell the
views you finished, and then emit
def addSong (self): # lastIndex keeps track of the last index used. row= self.lastIndex self.lastIndex+= 1 self.beginInsertRows (QModelIndex (), row, row) # actually the Song has already been added to the Collection # so I don't do anything here, # but if you keep your rows in this model you should do something here self.endInsertRows () self.count+= 1 modelIndex= self.index (row, 0) self.dataChanged.emit (modelIndex, modelIndex)
Later I'll post any peculiarities I find porting all this stuff to a read/write
 That's material for another post :)
 This feature can be said to be a little too much. Actually, I get a flicker when scanning.
 Of course the next step is to use a table view and make a model for it.
 Right now the load time for a
Collection of ~6.5k songs is quite long as
 This is a design decision which is not relevant to this example.
kReiSSy (se pronuncia como la palabra "crazy" del inglés) es uno de mis proyectos más ambiciosos en este momento (tengo uno mayor, pero está en el freezer; ya hablaré de él). en resumen, puedo decir que kReiSSy es un lector de feeds (rss, atom, quéno) alimentado a gofios, o como dicen ahora, en esteroides. otra descripción puede ser: es un concentrador de información externa con categorzación basada en tags.
¿qué features lo hacen tan pulenta? veamos:
- tagging de posts (eso lo hace cualquiera).
- almacenamiento local de posts (en una base de datos, no borra ninguno).
- browser integrado. acá empecé a irme al carajo.
- tagging de páginas "leídas por ahí", que las convierte en first class citizens del programa.
¿qué significan estos dos últimos fatures? que puedo estar leyendo un post, seguir un link, de ahí a otro, y otro, todo en el browser integrado, y así hasta que encuentro otra página que nada que ver por dónde empecé, y entonces puedo tagear dicha página. esto hace dos cosas automáticas: guarda la página en la base de datos como si fuera un post más, y me permite luego buscar dicha página por tag.
ahora, me fui de boca un poco al decir "browser integrado" (esta es la sección "proyect status"). por ahora el browser nos permite ir hacia adelante, siguiendo links, pero no para atrás. es decir, le faltan todos los botones de navegación. también se podría marcar un post como "leer después", pero no hay código que lo haga. el filtrado por varios feeds o varios tags está roto/no anda (esto se debe o a un bug en SQLAlchemy o a mi inoperancia). aún no graba bien su sesión, cosa de volver en el mismo estado en que lo dejamos. y hace un par de chanchadas con los archivos, asume un par de paths y negradas así.
¿y en qué está hecha semejante bestia? python, obvio, mi lenguaje de cabecera desde hace unos añitos ya. pero no python puro, sino con varios agregados.
uno ya lo mencioné, SQLAlchemy, un ORM bastante potente, pues no fuerza muchas cosas. para usarlo como un ORM clásico (una clase por tabla) hay que hacer un par de giladas, pero nada grave. y permite hacer queries con SQL a lo macho, aunque no me llevo muy bien con eso. sqlite por debajo.
y la vedette de todos, y el motivo por el que empecé este proyecto: PyKDE. soy un usuario y fanático de kde desde que usé un redhat5.2 allá por el '98 o así. conozco bastante la infraestructura que hay por debajo, he leído varias veces cachos de código en busca de solucionar algún bug que me mordió, algunas veces hasta logré repararlo y todo. si bien su look no es muy bonito, la infraestructura que hay debajo es impresionante.
a tal punto que hacer este programa me resultó muy sencillo hasta ahora, pues sólo me concentré en mi funcionalidad. la parte de mostrado de html u otro tipo de archivos se lo dejé a KDE:
mime= KMimeType.findByURL(url, 0, False, False) mimeType= mime.name () if mimeType=='application/octet-stream': mimeType= KIO.NetAccess.mimetype (url, self); else: # asumo que es html mimeType= "text/html" ptr= KTrader.self().query(mimeType, "'KParts/ReadOnlyPart' in <span class="createlink">ServiceTypes</span>") part= createReadOnlyPart (ptr.library (), tab, ptr.name ())
esto hace la fantástica magia de fijarse quées el link (dado por url) y luego KTrader me entrega un KPart que sabe mostrar ese . simplemente la embebo en un tab y ya. juzguen ustedes.
ok, suficiente por ahora. ya estaré hablando de éste y otros proyectos.
 no es una traducción literal del "whatnot" en inglés, sino una reimplementación en castellano de la misma idea.
 uso muchos términos en inglés que ni me gasto en traducir. deal with it.
So far both the default and complex skins in
satyr were (ab)using the selection
in their respectives views to highlight the current song and, in the case of the
complex skin, that meant that you couldn't select cells. This could be (and will
be) handy for editing several tags at the same time or for grouping songs in an
Album. I decided that this should not longer be this way.
It was really simple: I simple reenabled selection, the extended version (Shift
and Control work as you grew to spect). Now the way to highlight the current
song is to simply change the background and foreground colors for the cells of
that row. How? Simple, extending the
QPlayListModel.data() method to provide
these properties and using
QApplication.palette() to get colors that would
stand out from the normal application colors (even the selection) and would
still make the text readable:
def data (self, modelIndex, role): if modelIndex.isValid () and modelIndex.row ()<self.aggr.count: [...] # check the row so we highlight the whole row and not just the cell elif role==Qt.BackgroundRole and modelIndex.row ()==self.parent_.modelIndex.row (): # highlight the current song # must return a QBrush data= QVariant (QApplication.palette ().dark ()) elif role==Qt.ForegroundRole and modelIndex.row ()==self.parent_.modelIndex.row (): # highlight the current song # must return a QBrush data= QVariant (QApplication.palette ().brightText ()) [...]
And that's almost it... because there's always a but. Whenever the current song
changes, there's a little lag between this happens (you can hear the new song
being played) and the highlighting updates. This is because I update that
self.parent_.modelIndex you see up there, but the
QPLM.data() method is not
called until the next repaint, which doesn't happen inmediately. So, we must
force that repaint. How? Again, it's simple: we already emited the
dataChanged() signal in the method
setData() when the data could be
successfully changed. Now we do the same just so the view updates the
def showSong (self, index): [...] # FIXME? yes, this could be moved to the model (too many self.appModel's) start= self.appModel.index (oldModelIndex.row (), 0) end= self.appModel.index (oldModelIndex.row (), columns) self.appModel.dataChanged.emit (start, end) start= self.appModel.index (self.modelIndex.row (), 0) end= self.appModel.index (self.modelIndex.row (), columns) self.appModel.dataChanged.emit (start, end) [...]
Update before even publishing: most of the numbers in the initial writing were
almost doubled. The problem was that
distutils left a
build directory when I
tried either to install or to package
satyr, I don't remember which, so the
files found by the
find commands below were mostly duplicated. I had to remove
the directory and run all the commands again!
I wanted to know some things about
satyr's code, in particular some statistics
about its lines of code. A first approach:
$ find . -name '*.py' -o -name '*.ui' | xargs wc -l | grep total 2397 total
Hmm, that's a lot, I don't remember wrinting so many lines. Beh, the comments, let's take them out:
$ find . -name '*.py' -o -name '*.ui' | xargs egrep -v '^#' | wc -l 2136
What about empty lines?:
$ find . -name '*.py' -o -name '*.ui' | xargs egrep -v '^(#.*| *)$' | wc -l 1764
Meeh, I didn't take out all the comment lines, only those lines starting
#, which are mainly the license lines on each source file. I have to also
count the comments in the middle of the code:
$ find . -name '*.py' -o -name '*.ui' | xargs egrep -v '^( *#.*| *)$' | wc -l 1475
And how much of those lines are actual code and not from some xml file describing user interface?:
$ find . -name '*.py' | xargs egrep -v '^( *#.*| *)$' | wc -l 1124
How much code means its 3 current skins?:
$ find satyr/skins/ -name '*.py' | xargs egrep -v '^( *#.*| *)$' | wc -l 341
How much in the most complex one?
$ egrep -v '^( *#.*| *)$' satyr/skins/complex.py | wc -l 182
All this numbers tell something: ~300 empty lines means that my code is not very tight. I already knew this: I like to break functions in secuential blocks of code, each one accomplishing a somehow atomic step towards the problem the function tries to solve. Almost 300 comment lines means my code is very well commented, even if a sixth of those comments are BUGs, TODOs, FIXMEs or HINTs:
$ find . -name '*.py' | xargs egrep '^ *# (TODO|BUG|FIXME|HINT)' | wc -l 56
Wow, more than I thought. Now, 1100 lines of actual code for all that
accomplishes today (playing almost any existing audio file format, progressive
collection scanning, lazy tag reading and also tag writing, 3 skins, handle
Collections, searching, random, stop after, saving some state, picking
up new songs added to the collection via the filesystem, queuing songs, dbus
interface, handle any encodings in filenames... phew! and some minor
features more!) I think is quite impressive.
Of course, doing all that in athousandsomething lines of code would be nearly
PyKDE4/KDE (something) 4,
Python itself. It's really nice to have such a nice framework to
work with, really.
 No user interface for this yet; shame on me.
 ... which toguether with
qdbus make my alarm clock.
 Almost all the support is in
  Less than 800 if we don't count skins.
 Yes, I add more footnotes as I readproof the posts :)
 I skipped  :)
 After the rebranding I don't know which is the proper name for the libraries, because I'm writing this post while very much offline, and TheDot does not publish the whole articles via rss, which I hate.
satyr got the possibility to queue songs for playing very early. At that
moment there wasn't any GUI, so the only way to (de)queue songs was via
dbus. Once the GUI was there, we had to provide a way to queue songs. As
satyr aims to be fully usable only using the keyboard, the obvious way was to
setup some shortcut for the action.
Qt and then
KDE have a very nice API for defining 'actions' that can be
fired by the user. The ways to fire them include a shortcut, a menu entry or a
button in a toolbar. I decided to go with the
According to the
documentation, to create an action is just a matter of creating
the action and to add it to an
actionCollection(). The problem is that nowhere
it says where this collection comes from. There's the
but creating one and adding the actions to it seems to be not enough.
If you instead follow the
see that it refers to a
KXmlGuiWindow, which I revised when I was desperately
looking for the misterious
actionCollection(). I don't know why, but the
documentation generated by
PyKDE does not include that method. All in all, the
tutorial code works, so I just ported my
class MainWindow (KXmlGuiWindow): def __init__ (self, parent=None): KXmlGuiWindow.__init__ (self, parent) uipath= __file__[:__file__.rfind ('.')]+'.ui' UIMainWindow, _= uic.loadUiType (uipath) self.ui= UIMainWindow () self.ui.setupUi (self) [...] self.ac= KActionCollection (self) actions.create (self, self.actionCollection ())
def create (parent, ac): '''here be common actions for satyr skins''' queueAction= KAction (parent) queueAction.setShortcut (Qt.CTRL + Qt.Key_Q) ac.addAction ("queue", queueAction) # FIXME? if we don't do this then the skin will be able to choose whether it # wants the action or not. with this it will still be able to do it, but via # implementing empty slots queueAction.triggered.connect (parent.queue)
Very simple, really. But then the action doesn't work! I tried different approaches, but none worked.
The tutorial I mentioned talks about some capabilities of the
one of them, the ability to have the inclusion of actions in menues and
toolbars dumped and loaded from XML file (hence, the XML part of the class
name), and that is handled by the
setupGUI() method. From its documentation:
«Configures the current windows and its actions in the typical
[...] Typically this function replaces
createGUI()». In my case the GUI is
already built by that
self.ui.setupUi() that you see up there, so I ignored
this method. But the thing is that if you don't call this method, the actions
will not be properly hooked, so they don't work; hence, my problem. But just
calling it make the actions work! I'll check later what's the magic behind this
method. For now just adding
self.setupGUI() at the end of the
So, that's it with actions. As a result I also get several things: a populated menu bar with Settings and Help options (but no File; that'll come later with standard actions, of which I'll talk about later, I think), with free report bug, about satyr and configure shortcuts options, among others. The later does work but its state is not saved. That also will come in the same post that standard actions.
PD: First post from my new internet connection. Will
satyr suffer from this?
Only time will tell...
 then you had to pick the index of the songs either by guessing or looking at the dump of the playlist.
A couple of days ago Marcelo Fernández wrote a simple image viewer in
less than 200 lines long, and I thought that it would be nice to compare how
the same app would be written in
PyKDE4. But then I though that it would not
be fair, as
KDE is a whole desktop environment and
GTK is 'only' a widget
library, so I did it in
To make this even more fair, I hadn't had a good look at the code itself, I only run it to see what it looks like: a window with only the shown image in it, both scrollbars, no menu or statusbar, and no external file, so I assume he builds the ui 'by hand'. He mentions these features:
- Pan the image with the mouse.
- F1 to F5 handle the zoom from 'fit to window', 25%, 50%, 75% and 100%.
- Zooming with the mouse wheel doesn't work.
Here's my take:
#! /usr/bin/python # -*- coding: utf-8 -*- # - Example image viewer in # Marcos Dione <firstname.lastname@example.org> - http://grulicueva.homelinux.net/~mdione/glob/ # TODO: # * add licence! (GPLv2 or later) from .QtGui import QApplication, QMainWindow, QGraphicsView, QGraphicsScene from .QtGui import QPixmap, QGraphicsPixmapItem, QAction, QKeySequence import sys class OMITGraphicsView (QGraphicsView): def __init__ (self, pixmap, scene, parent, *args): QGraphicsView.__init__ (self, scene) self.zoomLevel= 1.0 self.win= parent self.img= pixmap self.setupActions () def setupActions (self): # a factory to the right! zoomfit= QAction (self) zoomfit.setShortcuts ([QKeySequence.fromString ('F1')]) zoomfit.triggered.connect (self.zoomfit) self.addAction (zoomfit) zoom25= QAction (self) zoom25.setShortcuts ([QKeySequence.fromString ('F2')]) zoom25.triggered.connect (self.zoom25) self.addAction (zoom25) zoom50= QAction (self) zoom50.setShortcuts ([QKeySequence.fromString ('F3')]) zoom50.triggered.connect (self.zoom50) self.addAction (zoom50) zoom75= QAction (self) zoom75.setShortcuts ([QKeySequence.fromString ('F4')]) zoom75.triggered.connect (self.zoom75) self.addAction (zoom75) zoom100= QAction (self) zoom100.setShortcuts ([QKeySequence.fromString ('F5')]) zoom100.triggered.connect (self.zoom100) self.addAction (zoom100) def zoomfit (self, *ignore): winSize= self.size () imgSize= self.img.size () print winSize, imgSize hZoom= 1.0*winSize.width ()/imgSize.width () vZoom= 1.0*winSize.height ()/imgSize.height () zoomLevel= min (hZoom, vZoom) print zoomLevel self.zoomTo (zoomLevel) def zoom25 (self, *ignore): self.zoomTo (0.25) def zoom50 (self, *ignore): self.zoomTo (0.5) def zoom75 (self, *ignore): self.zoomTo (0.75) def zoom100 (self, *ignore): self.zoomTo (1.0) def zoomTo (self, zoomLevel): scale= zoomLevel/self.zoomLevel print "scaling", scale self.scale (scale, scale) self.zoomLevel= zoomLevel if __name__=='__main__': # this code is enough for loading an image and show it! app= QApplication (sys.argv) win= QMainWindow () pixmap= QPixmap (sys.argv) qgpi= QGraphicsPixmapItem (pixmap) scene= QGraphicsScene () scene.addItem (qgpi) view= OMITGraphicsView (pixmap, scene, win) view.setDragMode (QGraphicsView.ScrollHandDrag) view.show() app.exec_ () # up to here! # end
Things to note:
- The code for loading, showing the image and pan support is only 13 lines of
Pythoncode, including 3 imports. The resulting app is also able to handle vector graphics, but of course I didn't exploit that, I just added a
- Zooming is implemented via
QGraphicsView.scale(), which is accumulative (scaling twice to 0.5 actually scales to 0.25 of the original size), so I have to keep the zoom level all the time. There should be a
- The code for calculating the scale level is not very good: scaling between 75% and 50% or 25% produces scales of 0.666 and 0.333, which I think at the end of the day will accumulate a lot of error.
- For the same reason,
zoomToFit()has to do some magic. I also got hit by the integer division of
Python(I was getting zoom factors of 0) so I had to add
1.0*to the claculations. It's good that this is fixed in
- The size reported by the
QMainWindowwas any vegetable (it said 640x480 when it actually was 960x600), so I used the
- For some strange reason
zoomToFit()scales the image a little bigger than it should, so a scrollbar appears in the direction of the constraining dimension.
- Less that 100 lines! Even if
setupActions()could surely be improved.
- In Marcelo's favor I should mention that he writes docstrings for most of his methods both in english and spanish (yes, of course I read his code after I finished mine). I barely put a couple of comments, but doing the same should add 10 more lines, tops. Also, I don't want to convert this into a who-has-it-smaller contest (the code, I mean :).
- It took me approx 3 hours, with no previous knowledge of how to do it and no internet connection, so no asking around. I just used the «Qt Reference Documentation», going to the «Gropued Classes» page and to the «Graphics View Classes» from there.
- It doesn't zoom with the mouse wheel either.
- The default colors of
formatplugin are at most sucky, but better than nothing.
 Unluckly he didn't declared which license it has, so I'm not sure if I really can do this. I GPL'ed mine.