Developing MeeGo apps with Python and QML

Author

Thomas Perl <m [at] thp.io> http://thp.io/2010/meego-python/

translated by Kazushige TAKEUCHI http://d.hatena.ne.jp/graceful_life/

Introduction

本チュートリアルでは、PySideの環境をネットブックに構築し、いくつかの基本的なサンプルをお見せします. そして,gPodderというMeeGoネットブック/携帯端末用のアプリケーションを元にQMLを使ったUIを作れる道筋を示します.

なぜ、pythonを使うのか?

  • 参入の容易さ

python は非常に簡単に学べる言語です。ですので、既に他の言語を学習したことがある如何にかかわらず、素早く開発をすることができます.

  • ガベージコレクション

多くのオブジェクトの管理を自信で行う必要がありません.pythonのガベージコレクタが必要のないオブジェクトを開放してくれます.

  • コンパイル不要

pythonはインタープリタ型の言語です. その為,エディタで編集後すぐに実行が可能です. コンパイルを待つ必要はありません. これはネットブックなど非力な環境で重要になってきます.

  • Qt ライブラリへのフルアクセス

PySideは,Qtのすべてのモジュールにアクセス可能です. これは,QtのNativeのライブラリを利用している為です. ライブラリの関数はネイティブのスピードで実行可能です.

  • 短いコード

私の経験ではC++アプリは,同じライブラリを使用しているにもかかわらずpythonの3倍程のコード量になります.

  • プロトタイピング

C++ でQtアプリを書こうと考えている人にとっても,Python/PySideでのプロトタイピングは有効です. QMLファイルの再利用も可能のため,Pythonでプロトタイプ後C++アプリに切り替えるという方法も考えられます.

  • 開発してすぐ実行

Pythonはインタープリタ型の言語のため,MeeGoの様に既に環境が整っている場合,コンパイラの導入などが不要で実行でき,ライブラリのためのヘッダファイルを書いたりする必要がありません. N900の様な端末上でアプリを書いて,即座に実行可能です.

環境構築

まずは,MeeGoのネットブックを用意しましょう. 仮想マシンや実際のネットブックにインストールするなど色々な方法があります. (ここでは,対象外のため説明はしません.MeeGo Wikiを参照してください) PySideでの開発をする場合,agile state(最新?)のPySideを利用しましょう. 1.0リリース以前のバグがFixされているので,ソースからコンパイルして使うのがいいでしょう. この方法は必須ではありません.(PySideパッケージがMeeGoに統合されれば)

ビルドスクリプト

Gitリポジトリから,PySideを自動的にビルドまで実行するスクリプトの紹介をします. このスクリプトは依存したライブラリのインストールまで実行します.

  1. MeeGoの [Applications] ボタンを押す.
  2. [Terminal]を探して実行する.
  3. Git のインストールを行う.(” sudo zypper install git “を実行する.)
debian なら,sudo aptitude install git
  1. $HOMEに”pyside”のフォルダを作る (“mkdir pyside”を実行する.)
  2. ソースのチェックアウト(“git clone git://gitorious.org/pyside/buildscripts.git”)
  3. ビルドスクリプトのフォルダに移動(“cd buildscripts”を実行する)
  4. ソースのダウンロード(“git submodule init”と”git submodule update”を実行する)
  5. ソースのダウンロードは時間がかかるので少し待ちます.(珈琲でも飲みながら)

ビルドスクリプトのダウンロードが終わったら,MeeGo Netbook用のPySideをビルドが出来るようになります.

PySideのビルドと,$HOMEへのインストール

ソースの入手が完了したら,ネットブック用のPySideをビルドし,インストールします. $HOME にインストールすることで,システムにインストールしたPySideとコンフリクトすること無く利用可能です. gitからインストールすれば,アンインストールも再インストールも容易で,安全に行えます.

  1. 別のターミナルを開きます.
  2. PySideのビルドスクリプトのフォルダに移動します.: “cd ~/pyside/buildscripts”
  3. 依存したビルドのインストール: “sudo ./dependencies.meego.sh”
  4. 他のアプリを終了する. (メモリを多く利用するため,メモリを増設するか,swapfileを多くした方がいいでしょう)
  5. “./build_and_install”を使ってビルドします.
  6. およそ,2.5時間かかります.(ATOMのシングルコアのネットブックです).このビルドに多くの時間がかかりますが,開発したアプリは代わりにコンパイルの必要がありません.これは,非力なネットブックには非常に素晴らしいことで,時間やバッテリーを有効に使うことが可能です.

$HOMEにPySideをインストールをするので,環境変数を設定したほうがいいでしょう. バージョンごとのPySideは,一つのみ有効化されません. 幸運なことに”environment.sh”スクリプトが同梱されています.または,~/pyside/buildscripts/environment.sh を利用することで,PySideを使った開発をする時に環境変数の設定を容易に行うことができます. もしくは,~/.bashrc に追加しておくことで,自動的に実行されます.(推奨)

インストールの確認

素晴らしいPySideのアプリを書く前に,正しくインストールされているか確認してみましょう. 別のターミナルを開いて, “python”と入力してください. pythonのインタラクティブシェルが起動します. そして,“from PySide import QtGui”と入力してください. QtGuiモジュールが正しくインストールされているか確認できます. また“from PySide import QtDeclarative”も試してみてください.

QtGuiは非QMLなベースクラスです.そして,QtDeclarativeはQMLを読み込んで表示するためのクラスです.

Basic QML tutorial examples

This section should give you a short overview with code examples on how to do simple things with PySide and QML. More examples can be found on the homepage of this tutorial on http://thp.io/2010/meego-python/ Hello World This example shows you how to create a minimalistic Hello world QML application with Python. We already subclass QObject here and provide a simple property (‘greeting’) that we can then access from QML. Let’s dive right into the source code. The Python source (HelloMeeGo.py) For our first hello world program, we want to generate a greeting in Python and show it in a QML UI. We do this by subclassing QObject, giving our subclass a property called “greeting” and exposing an instance of that object to the QML root context, where we can access it from the QML file. # -- coding: utf-8 -- import sys from PySide.QtCore import * from PySide.QtGui import * from PySide.QtDeclarative import * We need the “sys” module (a Python standard module) to access the command line arguments (we need to pass them to the constructor of QApplication). From PySide, we need QtCore (which contains core objects, like QObject in our example) and QtGui (which contains Qapplication, which we need for the Qt main loop). The QML view is provided by the QtDeclarative module – it provides QDeclarativeView. class Hello(QObject): def get_greeting(self): return u’Hello, Meego!’ greeting = Property(unicode, get_greeting) This is the definition of our “Hello” class – we provide a getter method for the greeting, and return a unicode string “Hello, MeeGo!” in it – if you want, you can also customize this greeting, i.e. add the time and date using the “datetime” Python module. In order for QML to be able to access this property, we have to declare it as such by using “Property” (which is in QtCore). The first parameter of Property is the type (unicode means unicode string) and the second one is the getter method – if you want the property to be modifyable, you need to add a setter method, and if you want it to be dynamically updated, you also need a notifyable property. app = QApplication(sys.argv) hello = Hello() view = QDeclarativeView() Here we create instances of the classes we need: • QApplication is needed by every Qt application and handles commandline arguments, sets up the graphics system and does other initializations. It also provides the main loop through the “exec_” method. • Hello is our class defined above. We create an instance of it that we later pass to QML using context properties. • QDeclarativeView is the window / view in which we can load QML content and display it on the screen. context = view.rootContext() context.setContextProperty(‘hello’, hello) For QML to be able to access our “hello” object, we need to expose it as a context property to the view’s root context – this is done using setContextProperty. As property name we use “hello”, so we can access the greeting of that object later using “hello.greeting” in QML. view.setSource(__file__.replace(‘.py’, ‘.qml’)) Here the QML file is loaded and displayed in the view. As the QML file has the same name as our Python script (just a different file extension), we can use __file__.replace(‘.py’, ‘.qml’) to always get the correct file name – this also allows you to easily rename both files and still have them work together as expected (for example, if you want to try to create different variants of this example). view.show() app.exec_() And finally, here our application starts: We always have to call the show() method on the view, or otherwise it won’t be shown on the screen. If you want to show the window in fullscreen mode, use “showFullScreen” here – try it out! In order to start the Qt main loop and process events, we have to call app.exec_(), so the application does not quit. This is very important. The QML UI file (HelloMeeGo.qml) The UI definition is placed in “.qml” files – these have JavaScript-like syntax, and describe the appearance of your application. In our case, we simply want to have a red rectangle in which we place the greeting in a white font. import Qt 4.7 In order to use the built-in QML components like “Rectangle” and “Text”, we need to import the Qt 4.7 module into our QML file. In future versions of Qt (4.7.1 and newer), this will be called QtQuick 1.0, but for now, it’s called Qt 4.7, and this is what you have to use. Rectangle { color: “red” width: 500 height: 500 Text { anchors.centerIn: parent font.pointSize: 32 color: “white” text: hello.greeting } } The root object is a 400x400 pixel, red rectangle, which contains a child Text component that is centered into its parent (i.e. the red rectangle). The text is 32pt in size and has a white color. Its text is taken from the “hello” object (our Python object instance) as the “greeting” property (which we have defined in the Python source code). Save these two files and start the example using “python HelloMeeGo.py”. You should see a window like the one in the screenshot above. Try it out, and experiment with changing some properties. Displaying HTML content in a QML WebView This short tutorial shows you how to combine the powers of Python, QML, HTML and JavaScript to create good-looking, rich web applications or netbook/handset applications that can display web content. This application consists of three files: The Python source, the HTML content and the QML view (but as most of the interaction takes place in Python and HTML, the QML is really short). This example has been ported from my other PySide/QML examples to MeeGo, and the transition was very easy, as PySide code is very portable across devices. The Python source code (WebKitView.py) What we need in the Python world is a way to send data to the HTML view, and also a way to receive the data – this is done by evaluating JavaScript code inside the WebView and by listening to “alert()” calls from the web view. Communication happens by using JSON to encode data structures, as alert() does not allow to send arbitrary data. # -- coding: utf-8 -- import sys import time try: import simplejson as json except ImportError: import json from PySide import QtCore, QtGui, QtDeclarative We need the standard Python modules “sys” and “time”, and the Python json module (included in the Python version shipped with MeeGo Netbook 1.1) to encode and decode JSON data in Python. From PySide, we need our “usual suspect” modules – QtCore, QtGui and QtDeclarative. These three modules are always needed when you want to do something with PySide and QML. def sendData(data): global rootObject print ‘Sending data:’, data json_str = json.dumps(data).replace(‘”’, ‘\”’) rootObject.evaluateJavaScript(‘receiveJSON(“%s”)’ % json_str) In order to send data to the WebView, we need to get a reference to the root object of our QML (the root object is the web view) and then evaluate some javascript inside it. The assumption here is that our HTML file has a “receiveJSON” function declared in its JavaScript code which receives the data and handles it. See below for what the receiveJSON function does in the HTML code. def receiveData(json_str): global rootObject data = json.loads(json_str) print ‘Received data:’, data if len(data) == 2 and data[0] == ‘setRotation’: animation = QtCore.QPropertyAnimation(rootObject, ‘rotation’, rootObject) animation.setDuration(3000) animation.setEasingCurve(QtCore.QEasingCurve.InOutElastic) animation.setEndValue(data[1]) animation.start(QtCore.QAbstractAnimation.DeleteWhenStopped) else: sendData({‘Hello’: ‘from PySide’, ‘itsNow’: int(time.time())}) The receiving of data from HTML is done by the receiveData function – it will be connected to the “alert” signal of the WebView, which gets sent when “alert()” is called from somewhere inside the WebView. What this does is decode the received string from JSON to a Python data structure and then check the contents of the received data – if it’s a two-item list, and the first item is a string “setRotation”, we interpret the second value as the target rotation value, and use a QPropertyAnimation on the root object to rotate the QML component inside the QML view – because all QML components have standard Qt properties that can be animated! If it’s not a request for rotation, we simply send an arbitrary data structure to the HTML view as a “reply”. app = QtGui.QApplication(sys.argv) view = QtDeclarative.QDeclarativeView() view.setRenderHints(QtGui.QPainter.SmoothPixmapTransform) view.setSource(__file__.replace(‘.py’, ‘.qml’)) rootObject = view.rootObject() rootObject.setProperty(‘url’, __file__.replace(‘.py’, ‘.html’)) rootObject.alert.connect(receiveData) view.show() app.exec_() This code creates instances of the QApplication and the QDeclarativeView. The SmoothPixmapTransform render hint makes the rotated web view look smoother and not so pixellated. We then load the QML file and set the “url” property of our root object (a WebView) to point to the HTML file in the same directory. We also need to hook up the “alert” signal of the root object to our custom callback “receiveData” to handle data sent from the HTML. As always, we need to show the view (so it gets displayed on the screen) and finally call “exec_” on the QApplication instance in order to start the Qt main loop. The QML content (WebKitView.qml) This is very, very short and shows just how easy it is to create a WebView in QML – first, you need to import QtWebKit 1.0, as the WebView component is not included in the default Qt QML module. Then, we create a WebView component as our root object, enable javascript for it (otherwise we won’t be able to run any JavaScript inside it – it’s disabled by default) and resize it to 400x280. We do not yet set the “url” property of it to load the HTML, but instead we do that directly from our Python code (see above) to show you how to modify properties in QML components directly from Python code. import QtWebKit 1.0 WebView { settings.javascriptEnabled: true; width: 400; height: 280 } The HTML page (WebKitView.html) This is the HTML content that gets rendered in the WebView QML component. We need to have some JavaScript in its head (which is the most interesting part here) that is capable of sending and receiving data to and from our Python code, so we can connect both worlds and send commands and data back and forth. <html> <head> <script type=”text/javascript”> function sendJSON(data) { alert(JSON.stringify(data)); } The sendJSON function accepts arbitrary JavaScript data structures (e.g. arrays) and encodes them with JSON and then uses “alert()” to send it to Python. function receiveJSON(data) { element = document.getElementById(‘received’); element.innerHTML += “n” + data; } The receiveJSON function gets called from the Python code directly and receives one string (already in JavaScript data format, not encoded in JSON) and adds it to our HTML document. We could do more sophisticated processing of the data here if need be. function setRotation() { element = document.getElementById(‘rotate’); angle = parseInt(element.value); message = [‘setRotation’, angle]; sendJSON(message); } This is a convenience function used by the HTML below to create a “set rotation” command message to be sent to the Python code. It’s a simple twoelement list with “setRotation” as first element and the angle as second element. The angle is taken from the input element on the web page. function sendStuff() { sendJSON([42, ‘PySide’, 1.23, true, {‘a’:1,’b’:2}]); } This is similar to the setRotation function ,and simply sends some arbitrary data to the Python side (where it gets printed and answered to with a reply message, which is received using receiveJSON above). </script> </head> <body style=”background-color: white;”> <h2>PySide, QML and WebKit on MeeGo</h2> <p> Set rotation: <input type=”text” size=”5” id=”rotate” value=”10”/> <button onclick=”setRotation();”>Click me now!</button> </p> <p> Send arbitrary data structures: <button onclick=”sendStuff();”>No, click me!</button> </p> <p>Received stuff:</p> <pre id=”received”></pre> </body> </html> And finally, here is the HTML content of the web page (the part that gets displayed on the screen – we have a heading, two paragraphs and some form elements like a text entry box and buttons, which carry out the requested actions when clicked. These fields make use of the JavaScript functions defined above.

Writing a new QML UI for existing apps for MeeGo

This section deals with a real-world application example and how to use our knowledge gained in the previous section to create a great mobile UX for your application to be used on MeeGo netbook and handset. As the dependencies are in MeeGo Core, the same application obviously works on all other MeeGo devices (IVI, TV, …) as well, but you might have to tailor your QML UIs to be usable on these devices, as the usage situation is different. Again – you have to create a special UI, but the technologies to create the UI are the same.

A QML UI for gPodder

gPodder is a podcast client with which you can download video and audio content from the web to your device to play it on the go and manage subscriptions to radio shows. The current gPodder UI is written using PyGTK, which is still available on MeeGo Netbook, but the preferred UI toolkit is Qt with QML, and MeeGo Handset does not support PyGTK very well. Our goal here is not to show how to do a complete port of gPodder to Qt/QML, but to show how to start and how to integrate existing code with QML UIs through the use of code examples. The normal gPodder Desktop UI does not use QML yet, and it’s more tailored towards Desktop use and is not very easy to use with touchscreen devices. We now want to create a touch-friendly podcast and episode list UI. The old UI looks like this on MeeGo Netbook:

The glue layer in Python (gpodder-qml.py)

This file uses the existing gPodder codebase (which thankfully exports an easyto- use API to our data structures via the “gpodder.api” module) and implements the classes and structures necessary to expose the gPodder data to our QML UI and also enables us to interact with the data by using a Controller that is also exposed to the QML UI and can be accessed directly from QML. # -- coding: utf-8 -- import os import sys from PySide import QtCore from PySide import QtGui from PySide import QtDeclarative from PySide import QtOpenGL from gpodder import api These statements import the required modules. If you don’t want or need OpenGL-accelerated QML, you can skip the import of the QtOpenGL module. This example also assumes that you have gPodder already installed systemwide. It is also advisable to use the legacy gPodder application to subscribe to some podcasts so that you can see some contents in the QML UI. class EpisodeWrapper(QtCore.QObject): def __init__(self, episode): QtCore.QObject.__init__(self) self._episode = episode def _title(self): return self._episode.title def _description(self): return unicode(self._episode.one_line_description()) def _downloaded(self): return self._episode.was_downloaded(and_exists=True) @QtCore.Signal def changed(self): pass title = QtCore.Property(unicode, _title, notify=changed) description = QtCore.Property(unicode, _description, notify=changed) downloaded = QtCore.Property(bool, _downloaded, notify=changed) The “EpisodeWrapper” class is a subclass of QObject (because we need to access it from QML) and exposes some information about the episode (i.e. its title and description and whether or not it has already been downloaded) to the QML UI. It’s important here that you make the properties notifyable (by defining a Signal “changed” and specifying it as notification signal for the properties by using “notify=changed” when defining the properties), so that the QML UI can be notified when it has to update its fields (i.e. when the downloaded state of an episode changes, the QML UI should update itself to reflect that change). class PodcastWrapper(QtCore.QObject): def __init__(self, podcast): QtCore.QObject.__init__(self) self._podcast = podcast def _url(self): return self._podcast.url def _title(self): return self._podcast.title def _description(self): return unicode(self._podcast._podcast.description) def _cover_file(self): f = self._podcast._podcast.cover_file if os.path.exists(f): return f else: return ‘/usr/share/gpodder/podcast-0.png’ def _count(self): total, deleted, new, downloaded, unplayed = self._podcast._podcast.get_statistics() return downloaded @QtCore.Signal def changed(self): pass url = QtCore.Property(unicode, _url, notify=changed) title = QtCore.Property(unicode, _title, notify=changed) description = QtCore.Property(unicode, _description, notify=changed) cover_file = QtCore.Property(unicode, _cover_file, notify=changed) count = QtCore.Property(int, _count, notify=changed) As we display not only episodes, but also podcasts (a podcast is a collection of several episodes), we have to wrap the gPodder-internal podcast objects as well, and do the same thing as for the episode objects. We do the same as for the EpisodeWrapper, but expose different properties. If one of the properties were to change, we would call “self.changed.emit()” from the PodcastWrapper instance to update its representation in the UI. class PodcastListModel(QtCore.QAbstractListModel): COLUMNS = (‘podcast’,) def __init__(self): QtCore.QAbstractListModel.__init__(self) self._client = api.PodcastClient() self._podcasts = [PodcastWrapper(x) for x in self._client.get_podcasts()] self.setRoleNames(dict(enumerate(PodcastListModel.COLUMNS))) def rowCount(self, parent=QtCore.QModelIndex()): return len(self._podcasts) def data(self, index, role): if index.isValid() and role == PodcastListModel.COLUMNS.index(‘podcast’): return self._podcasts[index.row()] return None

QML can display lists of items and automatically provide an easy way to display lists using the ListView component. In order to display any data, it has to be put into a list model (which is a collection of rows that need to be displayed). We create such a list model for podcasts here. You could define multiple columns, but in order to make use of QObject properties, we only have one column which is a QObject. The podcast objects are directly loaded from the gPodder API. The internal representation of the data is a normal Python list (“self._podcasts”), whereas the QML ListView has some expectations on how to get the data – namely the rowCount method (which returns the number of rows in the model) and the data method, which should return the data for a given row and column (in our case, we only have one column – the “podcasts” column that contains a PodcastWrapper for every episode).

class EpisodeListModel(QtCore.QAbstractListModel): COLUMNS = (‘episode’,) def __init__(self, episodes): QtCore.QAbstractListModel.__init__(self) self._episodes = [EpisodeWrapper(x) for x in episodes] self.setRoleNames(dict(enumerate(EpisodeListModel.COLUMNS))) def rowCount(self, parent=QtCore.QModelIndex()): return len(self._episodes) def data(self, index, role): if index.isValid() and role == EpisodeListModel.COLUMNS.index(‘episode’): return self._episodes[index.row()] return None Just as with the PodcastListModel, in order to display a list of episodes, we need an EpisodeListModel. The problem here is that the list of episodes is not static, as it depends on which podcast was selected in the UI (this will become clear in a bit when you see how the UI is structured). Therefore, we don’t grab the list of episodes directly in the constructor, but receive them as a parameter to the constructor. We then take all “native” gPodder episode objects, wrap them in our EpisodeWrapper (to be accessible from QML) and implement rowCount and data. This is basically the same as for the PodcastListModel, but it shows how you can have parameters in your model to provide the data. class Controller(QtCore.QObject): @QtCore.Slot(QtCore.QObject) def podcastSelected(self, wrapper): global view, episodeList view.rootObject().setProperty(“state”, “Episodes”) episodeList = EpisodeListModel(wrapper._podcast._podcast.get_all_episodes()) view.rootObject().setEpisodeModel(episodeList) print wrapper._podcast._podcast.__dict__ @QtCore.Slot(QtCore.QObject) def episodeSelected(self, wrapper): global view view.rootObject().setProperty(“state”, “Podcasts”) The Controller is the “director” of our QML show – it exposes some helpful functions for our QML UI to use, and knows about the underlying Python objects, and makes sure that the view receives updated data when something is to be shown. In order to do so, we again need to subclass it from QObject (all objects that you want to access from QML have to be QObject subclasses, as QML does not know about Python objects). Methods on that objects that need to be accessible (callable) from QML need to be decorated with the “QtCore.Slot” decorator – the parameters of the decorator describe the count and data type of the parameters that the functions expect – in that case, it’s a single parameter of type QObject. We expose two simple functions – podcastSelected and episodeSelected that will be called from QML when the user clicks on (or touches) an item in one of the list views. When a podcast is selected, we set the state of the UI to “Episodes” (which automatically starts the transition in QML – which we will see later) and we populate the list of episodes from the podcast, and when an episode is selected, we simply go back to the podcasts list (for this example, this is enough – a full-fledged application might want to show a “episode details” view, play the episode or start the download). app = QtGui.QApplication(sys.argv) view = QtDeclarative.QDeclarativeView() glw = QtOpenGL.QGLWidget() view.setViewport(glw) view.setResizeMode(QtDeclarative.QDeclarativeView.SizeRootObjectToView) This code sets up a new Qt UI application and creates a window that can be used for displaying QML content (QDeclarativeView). The two lines with “glw” are optional and enable OpenGL rendering of QML content (which – depending on your device – might be faster than normal rendering). If your MeeGo device does not support this, or if the performance is worse, simply comment out these two lines. The “setResizeMode” function on QDeclarativeView defines how the resizing of the window is handled – in our case, we want the root object in our QML to be automatically resized to fill the window. This is what you usually want if you don’t want to hardcode the QML to a specific size. controller = Controller() podcastList = PodcastListModel() episodeList = EpisodeListModel([]) These three lines create instances of our classes defined above – a Controller used as access point for QML, the podcast list (already populated with the user’s podcast subscriptions) and the episode list (which is empty until the user clicks on a podcast). rc = view.rootContext() rc.setContextProperty(‘controller’, controller) rc.setContextProperty(‘podcastList’, podcastList) rc.setContextProperty(‘episodeList’, episodeList) The QML root context can have properties that can be accessed by name from QML code. We have to expose our three objects (the controller, the podcast list an the episode list) and give them property names so that we can access them directly from QML. view.setSource(__file__.replace(‘.py’, ‘.qml’)) view.show() app.exec_() Now that we have everything set up, we simply have to load our QML UI into the view (by using setSource on the view). The “__file__.replace(‘.py’, ‘.qml’)” means that the QML file has the same name as our Python script, but with the extension “.qml” instead of “.py”. Now all we need to do is create the QML UI code for our little app. The QML main UI file (gpodder-qml.qml) Now that we have our backend code (written in Python) set up, we just need to create a nice QML UI on top of it. The “interface” to the backend is already defined by the context properties that we have defined – there’s no other “route” to call Python code from QML. In our special case, that is the Controller object on which we can call methods and the two models, which will be used by the ListView components to display lists of podcasts and episodes. In order to demonstrate good modularity of QML apps, we also split out the podcast list and episode list as separate components, so that the look and feel (and behaviour) of the lists can be changed without needing to edit the main UI file (it also makes the main UI file smaller and easier to understand). import Qt 4.7 This imports all basic QML elements for use into this QML file. In Qt 4.7.1 and newer, you should use “import QtQuick 1.0” instead, but with Qt 4.7.0 (as is the case on MeeGo Netbook), you have to use “import Qt 4.7”. Rectangle { id: rectangle1 width: 400 height: 400 opacity: 1 state: “Podcasts” Here, we define our outermost “root” object – a simple Rectangle with the id “rectangle1” that has a width and height of 400 pixels (due to the setResizeMode call in our Python code, this object will get resized when the window size changes). The default state of this object is “Podcasts” (see below), which means that by default, the podcast list will be shown when the QML UI is first loaded. function setEpisodeModel(mod) { episodelist.model = mod } This function is used to set a new model on the episode list view. It is a method of our root object and can therefore be called from Python – see the Controller class on how this function is used to load a list of episodes into the view. PodcastList { id: podcastlist model: podcastList contr: controller } The PodcastList component isn’t defined in Qt – it’s a component we will define by ourself as “PodcastList.qml” in the same directory as this QML file. We give it an ID to be able to reference it from other parts of the file, and set properties “model” (the data model to use – in our case the list of podcasts) and “contr” (a custom property that we use to give a reference to the controller to the list view, so that we can access the controller directly from the list view). EpisodeList { id: episodelist model: episodeList contr: controller } This is equivalent to the PodcastList component usage above – the component will be defined by us later in the file “EpisodeList.qml”, and will be accessible through the ID “episodelist” in this QML file (i.e. it’s already referenced by setEpisodeModel above). states: [ State { name: “Podcasts” PropertyChanges { target: podcastlist opacity: 1 visible: true } PropertyChanges { target: episodelist scale: 0 opacity: 0 rotation: 180 } }, Here, we define the “states” in which this QML component (our root object) can be in – in our case, there are two states: “Podcasts” (show a list of podcasts) and “Episodes” (show a list of episodes). When the component is in this state, the podcastlist object will be shown and the episode list will be hidden (by scaling it to a factor of zero, setting its opacity to zero and rotating it by 180 degrees – for a nice effect that we will define later by the use of transitions). State { name: “Episodes” PropertyChanges { target: podcastlist z: 0 rotation: -180 scale: 0.3 visible: true opacity: 0 } PropertyChanges { target: episodelist scale: 1 opacity: 1 } } ] The other state that our root object can be in is “Episodes”. In this state, we hide the podcast list (and rotate and scale it and then set its opacity to zero to hide it) and instead show the episode list. When this state is entered, the target components will automatically get these properties assigned. transitions: [ Transition { PropertyAnimation { properties: “scale,opacity,rotation” duration: 500 } } ] } I mentioned transitions. Just hiding and showing elements is boring. Therefore, we simply define some transitions on our root object that will be used to animate its children when the root object state changes – in our case, we want to animate the “scale”, “opacity” and “rotation” properties, and the animation should take exactly 500 milliseconds. With this definition, changing the properties will not have an immediate effect, but the properties will be “animated” to reach the end value in 500 milliseconds from the time the property has been set. Here’s how the transition looks like: This is everything we need for our little QML app – the specific appearance of each component is then defined in the PodcastList.qml and EpisodeList.qml files. The transitions and state changes between these objects are taken care of by the root view and our controller written in Python.

The QML file for displaying a list of podcasts (PodcastList.qml)

What’s left to do now is to specify the appearance of the podcast and episode list. Let’s start with the podcast list, as this is th-e one that is shown first: import Qt 4.7 We again need the default QML components shipped with Qt, so we import them here. ListView { id: podcastListView property variant contr The component is based on the QML ListView, but has a new property (of type “variant”, so we can place any arbitrary QObject in there) and has the name of “contr”. This is used by our QML application to give the Python Controller object to this listview. We also need to give this component a component-wide ID, so that we can access the controller using “podcastListView.contr” in other parts of this file. anchors.fill: parent This component should automatically fill all the available space of the parent component – in practice, this means that the podcast list will always fill the whole visible area of our window – even when its size changes. delegate: Component { Rectangle { width: podcastListView.width height: 60 color: ((index % 2 == 0)?”#222”:”#111”) A delegate is used as “template” for rendering a single row in the list view. Delegates get created and destroyed automatically as needed by QML. Our delegate is a simple Rectangle that has the same width as our view (because we want the list items to fill the whole width of the list). The background color of the list is defined by the “color” property of the Rectangle. The “index” value inside a delegate gives us the (zero-based) row index of the current row that is to be rendered. We can utilize it to shade alternating rows with different background colors – when “index % 2 == 0” (the first, third, fifth, … row), the background color will be “#222” and when it is not (the second, fourth, sixth, … row), the background color will be “#333”. Image { id: cover source: model.podcast.cover_file sourceSize { width: height height: height } width: 50 height: 50 anchors.left: parent.left anchors.top: parent.top anchors.leftMargin: (parent.height - width)/2 anchors.topMargin: (parent.height - height)/2 } We want to display the cover art of a podcast – it should be 50x50 pixels in size, and its filename is taken from “model.podcast.cover_file”. The “model” property accesses the underlying model (current row), and the “podcast” accesses the role name of “podcast” from the model – in our case, it is the first column (column index zero) of the model – a PodcastWrapper instance. The PodcastWrapper instance for the given row has a “cover_file” property that points to the file that should be displayed as cover art. The rest of the definitions (anchors) is used for layouting, and is out of scope for this tutorial – you can read about this in the QML documentation. Text { id: title elide: Text.ElideRight text: model.podcast.title color: “white” font.bold: true anchors.top: parent.top anchors.left: cover.right anchors.right: count.left anchors.bottom: parent.verticalCenter anchors.leftMargin: 10 verticalAlignment: Text.AlignBottom } Text { id: subtitle elide: Text.ElideRight color: “#aaa” text: model.podcast.description || “No description ;)” font.pointSize: 10 anchors.top: title.bottom anchors.left: cover.right anchors.right: count.left anchors.leftMargin: 10 verticalAlignment: Text.AlignTop } Here we simply show two different lines of text – one being the title of the podcast, taken from “model.podcast.title”, and the other one the description of the podcast, taken from “model.podcast.description”. If the PodcastWrapper for a given line does not give a description, we use the “No description” text as default description. Text { id: count color: “white” font.pointSize: 30 visible: model.podcast.count > 0 text: model.podcast.count anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right anchors.rightMargin: 10 } This is the amount of podcasts – the “count” property of PodcastWrapper. This component is only visible when the count of (downloaded) episodes is greater than zero. You can use as many properties as you want in a single expression, and can also use properties for different kinds of things – in this case we use it for both the visibility of the text and the contents of the text itself. MouseArea { anchors.fill: parent onClicked: { contr.podcastSelected(model.podcast) } } Up to now, we only have displaying of data. What we also want is to be able to click on a row and have some action performed. We can do this via a MouseArea – it should fill the parent component (otherwise it would not “catch” any clicks) and when it is clicked, the “podcastSelected” slot of our “contr” property should be called with “model.podcast” (a PodcastWrapper instance) as parameter. This is the “magic” in this case, as it will pass the object to the controller, which in turn will take care of switching the state of the main application and populating the list of episodes in the view. } } } Always make sure to close the brackets that you open in QML, or you will get a syntax error :)

The QML file for displaying a list of episodes (EpisodeList.qml)

The last part of our little project is the list of episodes. This is similar to the list of podcasts, so the listing of the code should be enough in this case: import Qt 4.7 ListView { id: episodeListView property variant contr anchors.fill: parent delegate: Component { Rectangle { width: episodeListView.width height: 60 color: (model.episode.downloaded?(“#987”):((index % 2 == 0)?”#eee”:”#ccc”)) Text { id: title text: model.episode.title color: “black” font.bold: model.episode.downloaded anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.verticalCenter anchors.leftMargin: 10 verticalAlignment: Text.AlignBottom } Text { id: subtitle color: “#333” text: model.episode.description || “No description ;)” font.pointSize: 10 anchors.top: title.bottom anchors.left: parent.left anchors.right: parent.right anchors.leftMargin: 10 verticalAlignment: Text.AlignTop } MouseArea { anchors.fill: parent onClicked: { contr.episodeSelected(model.episode) } } } } } We now have a fully-functional QML application written using Python. You can start the app using “python gpodder-qml.py”. What we need to do now is package it up for MeeGo to be installable as package.

MeeGo用のPythonアプリケーションのパッケージング

この説では,pythonアプリケーションからRPMパッケージを作る方法を紹介します. 引き続き,gpodder-qmlを例にして説明します.

依存関係のあるライブラリのインストール

RPMパッケージを作るためには以下のパッケージが必要です.

  • python-setuptools
  • rpmdevtools
  • rpm-build
  • meego-rpm-config
  • spectacle

メタファイルを作成する.

パッケージングを行うためにはいくつかのファイルの作成が必要です.

デスクトップ編: gpodder-qml.desktop

このファイルは,アプリケーションメニューに表示するための設定ファイルです.

[Desktop Entry] Name=gPodder-QML Exec=gpodder-qml.py Icon=gpodder-qml Terminal=false Type=Application Categories=AudioVideo;Audio;Network;FileTransfer;News;

アプリケーションメニュー用アイコン: gpodder-qml.png

このファイルはアプリケーションメニューに実際に表示されるアイコンです. 拡張子”.png”がついていないファイル名が設定ファイルに記載されているはずです.

The Spectacle YAML file: gpodder-qml.yaml

MeeGoではYAML形式で書かれたファイルに従って,パッケージングを行います. YAMLファイルに従って,specファイルが作成されます. 以下のHPを見れば,Spectacleに関する詳細な記述が記載してあります.

Name: gpodder-qml
Summary: gPodder QML
Version: 0.1
Release: 1
Group: Network
License: BSD
URL: http://thp.io/2010/meego-python/
Sources:
- "%{name}.tar.gz"
Description: A QML UI for gPodder
Builder: python
BuildArch: noarch
Files:
- "%{_bindir}/%{name}.py"
- "%{_datadir}/gpodder-qml/*.qml"
- "%{_prefix}/lib/python2.6/site-packages/*.egg-info"
- "%{_datadir}/applications/%{name}.desktop"
- "%{_datadir}/icons/%{name}.png"

Python distutils 用のファイル: setup.py

このファイルは必要なファイルのコピーなどのセットアップ用のファイルです.

from distutils.core import setup
import glob

APP_NAME = 'gpodder-qml'
SCRIPTS = [APP_NAME+'.py']
DATA_FILES = [
    ('/usr/share/'+APP_NAME, glob.glob('*.qml')),
    ('/usr/share/applications', glob.glob('*.desktop')),
    ('/usr/share/icons', glob.glob('*.png')),
]
setup(
    name=APP_NAME,
    version='0.1',
    description='A QML UI for gPodder',
    author='Thomas Perl',
    author_email='m@thp.io',
    url='http://thp.io/2010/meego-python/',
    scripts=SCRIPTS,
    data_files=DATA_FILES)

gpodder-qml.py の改良

gpoddeer-qml.py に少し変更を加えましょう まず以下のコマンドを実行し,実行可能にします.

chmod +x gpodder-qml.py

その後,エディタを開き以下の行をファイルの先頭に追加してください.

#!/usr/bin/python

上記,行を追加することによってシェルがインタプリタを推定し,pythonを利用して実行します.

RPM Sources の作成

ここでは .tar.gz のソースアーカイブを作ります. フォルダ名は,”gpodder-qml”にしておいた方がいいでしょう. その配下に作ったファイルすべてを格納しておきます.

その後以下のコマンドを実行します.(pathは適宜変換してください.) * mkdir -p ~/rpmbuild/SOURCES * mkdir -p ~/rpmbuild/SPECS * tar czvf ~/rpmbuild/SOURCES/gpodder-qml.tar.gz /path/to/gpodder-qml/

“SOURCES” フォルダにはYAMLファイルと,ソースのtarballを格納してください. “SPECS” フォルダには生成された .spec ファイルが格納されます. .spec ファイルの生成には,specifyツールを利用します.

引き続き,以下のコマンドを実行します.

  • cd ~/rpmbuild/SOURCES
  • specify gpodder-qml.yaml
  • mv gpodder-qml.spec ../SPECS/

missing “Makefile”の警告は無視しても構いません. python は setup.pyがあれば,ビルド可能なため,上記警告に対応する必要はありません.

Building the RPM package from source

これで全てのセットアップが完了です. アーキテクチャ非依存のRPMパッケージ(noarch)を作ることができます.

  • cd ~/rpmbuild/SPECS
  • rpmbuild -ba gpodder-qml.spec

上記コマンドでパッケージが ~/rpmbuild/RPMS/noarch/ 以下に生成されます

This will generate the package and save it in . You should have a file named “gpodder-qml-0.1-1.noarch.rpm”.

RPMパッケージのインストールとテスト

パッケージのテストを行うには,以下のコマンドを実行します.

  • cd ~/rpmbuild/RPMS/noarch
  • sudo zypper install gpodder-qml-0.1-1.noarch.rpm

コレによってパッケージはインストールされ,アプリケーションメニューに表示されます.

おめでとうございます. これで,あなたの最初のMeeGoアプリの完成です. 本チュートリアルを読んで頂きありがとうございました. これにょり貴方がQMLを使ったMeeGoアプリを作る道筋を示せたのではないかと思います. もしフィードバックがあれば,以下のURLからお願いいたします.

コードのダウンロードなども可能です.

inserted by FC2 system