Writing win32 applications with python and glade

Author: Nzeka Gilbert

Author e-mail: khaalel@gmail.com

Author website: www.nzeka-labs.com, korriban-planet.blogspot.com/

Document Version: 1.0


I] License

II] What are we going to do?

III] What you should have before starting…

IV] What is PyGTK?

V] Let us start

VI] Building the windows

VII] How to code this app in python?

VIII] How to compile BitPodder?


I] License

This tutorial is covered by the FDL.

The program listed here is covered by the GPL.


II] What are we going to do?

The goal of this article is to build a podcasting tool using Pygtk (Python + Glade + Pygtk). This tool, named BitPodder, is a podcasting tool allowing getting .torrent files from RSS Feeds. By explaining each step of the making of this tool, I hope I will help people having some difficulties with Python and Glade under win32 platform.


III] What you should have before starting…

- python

- Gtk+/Win32 Development Environment  (gladewin32.sourceforge.net/)

- Gtk+/Win32 Runtime Environment  (gladewin32.sourceforge.net/)

- Pygtk and gtkmm for Win32  (www.pcpm.ucl.ac.be/~gustin/win32_ports/)

- Tepache (primates.ximian.com/~sandino/python-glade/)

- You can get BitPodder_src.zip the tarball that contains the source code of BitPodder at: sourceforge.net/projects/korriban


IV] What is PyGTK?

“PyGTK provides a convenient wrapper for the GTK+ library for use in Python programs, taking care of many of the boring details such as managing memory and type casting. When combined with PyORBit and gnome-python, it can be used to write full featured Gnome applications.”  (www.pygtk.org/about.html)


V] Let us start

This is the win32 app we will code in this article. The screenshot is not clear but the goal is to show you what looks like BitPodder.

spacer


VI] Building the windows

First, we have to create the windows BitPodder will use. Thanks to glade, we can do that in a visual way. BitPodder contains a lot of widgets so before continuing this article, I hope you already have used glade or you know some widgets and know how works signals under GTK+. If all your answers are: no, I advise you to read articles like “Writing PyGTK applications in a visual way” (primates.ximian.com/~sandino/python-glade/) to know how work glade.

spacer

1: a GtkCList

2: a GtkComboBoxEntry

3: some GtkButton

4: some GtkToolButton

5: a GtkTextView

6: a GtkStatusbar

spacer

Now the widgets of the Option page:

7: a GtkCList

8: a GtkEntry

9: some GtkButton

I said “page” because to switch between Feeds and Options page, I used a GtkNotebook then I hided the tabs of this NoteBook. Here is the real window without hiding the tabs of the notebook:

spacer

To hide the tabs, you have to click here:

spacer

I won’t continue to explain how I created the windows of BitPodder project, because this article would be so long and you can read the glade file to see how it was made.


VII] How to code this app in python?

We will use a wonderful tool named tepache (primates.ximian.com/~sandino/python-glade/tepache/). With this tool, it will be possible to generate a python file with clean classes allowing controlling the widgets of your glade app.

Before, using tepache, download the tarball, unzip it and then install tepache by doing “python setup.py install” (thanks to cmd.exe) in the directory where you unzipped tepache.

Open another cmd.exe window and go where your glade file is. Put tepache in this directory then enter the following command in cmd.exe: python tepache xxx.glade. Where xxx.glade is the name of your glade file: for me it’s bitpodder.glade.

spacer

If all work fine, you should have 3 new files in the current directory: bitpodder.py, bitpodder.py.orig and SimpleGladeApp.py.

bitpodder.py is the python file in which we will write the python code of the soft.

bitpodder.py.orig must not be modify or delete because if you want to update bitpodder.py, after having modifying the glade file, tepache will use this file in order to not overwrite the code you wrote.

SimpleGladeApp.py contain the python code that your app will execute when the users will do default actions like closing the window of your app.


In a next article, I will explain you how to write your own tepache or your own SimpleGladeApp.py file to customize your app: for example, sometimes, when users close the window of the app, it would be nice to write a file that would contain the last positions of the windows, or information about the user…


Now, we will open bitpodder.py to add the code of the app.

Bitpodder.py should looks like this:


#!/usr/bin/env python

# -*- coding: UTF8 -*-


# Python module bitpodder.py

# Autogenerated from bitpodder.glade

# Generated on Fri Sep 09 14:38:31 2005


# Warning: Do not modify any context comment such as #--

# They are required to keep user's code


import os


import gtk


from SimpleGladeApp import SimpleGladeApp

from SimpleGladeApp import bindtextdomain


app_name = "bitpodder"

app_version = "0.0.1"


glade_dir = ""

locale_dir = ""


bindtextdomain(app_name, locale_dir)



class BitpodderWindow(SimpleGladeApp):


    def __init__(self, path="bitpodder.glade",

     root="bitpodder_window",

                 domain=app_name, **kwargs):

        path = os.path.join(glade_dir, path)

        SimpleGladeApp.__init__(self, path, root, domain, **kwargs)


    #-- BitpodderWindow.new {

    def new(self):

        print "A new %s has been created" % self.__class__.__name__

    #-- BitpodderWindow.new }


    #-- BitpodderWindow custom methods {

    #   Write your own methods here

    #-- BitpodderWindow custom methods }


    #-- BitpodderWindow.on_feed_toolbutton_clicked {

    def on_feed_toolbutton_clicked(self, widget, *args):

        print "on_feed_toolbutton_clicked called with self.%s" % widget.get_name()

    #-- BitpodderWindow.on_feed_toolbutton_clicked }


    #-- BitpodderWindow.on_options_toolbutton_clicked {

    def on_options_toolbutton_clicked(self, widget, *args):

        print "on_options_toolbutton_clicked called with self.%s" % widget.get_name()

    #-- BitpodderWindow.on_options_toolbutton_clicked }


    #-- BitpodderWindow.on_xisp_go_button_clicked {

    def on_xisp_go_button_clicked(self, widget, *args):

        print "on_xisp_go_button_clicked called with self.%s" % widget.get_name()

    #-- BitpodderWindow.on_xisp_go_button_clicked }


    #-- BitpodderWindow.on_show_button_clicked {

    def on_show_button_clicked(self, widget, *args):

        print "on_show_button_clicked called with self.%s" % widget.get_name()

    #-- BitpodderWindow.on_show_button_clicked }


    #-- BitpodderWindow.on_xisp_play_button_clicked {

    def on_xisp_play_button_clicked(self, widget, *args):

        print "on_xisp_play_button_clicked called with self.%s" % widget.get_name()

    #-- BitpodderWindow.on_xisp_play_button_clicked }


    #-- BitpodderWindow.on_xisp_results_clist_select_row {

    def on_xisp_results_clist_select_row(self, widget, *args):

        print "on_xisp_results_clist_select_row called with self.%s" % widget.get_name()

    #-- BitpodderWindow.on_xisp_results_clist_select_row }


    #-- BitpodderWindow.on_xisp_add_button_clicked {

    def on_xisp_add_button_clicked(self, widget, *args):

        print "on_xisp_add_button_clicked called with self.%s" % widget.get_name()

    #-- BitpodderWindow.on_xisp_add_button_clicked }


    #-- BitpodderWindow.on_xisp_remove_button_clicked {

    def on_xisp_remove_button_clicked(self, widget, *args):

        print "on_xisp_remove_button_clicked called with self.%s" % widget.get_name()

    #-- BitpodderWindow.on_xisp_remove_button_clicked }


    #-- BitpodderWindow.on_xisp_option_clist_select_row {

    def on_xisp_option_clist_select_row(self, widget, *args):

        print "on_xisp_option_clist_select_row called with self.%s" % widget.get_name()

    #-- BitpodderWindow.on_xisp_option_clist_select_row }



class InfoWindow(SimpleGladeApp):


    def __init__(self, path="bitpodder.glade",

root="info_window",

                 domain=app_name, **kwargs):

        path = os.path.join(glade_dir, path)

        SimpleGladeApp.__init__(self, path, root, domain, **kwargs)


    #-- InfoWindow.new {

    def new(self):

        print "A new %s has been created" % self.__class__.__name__

    #-- InfoWindow.new }


    #-- InfoWindow custom methods {

    #   Write your own methods here

    #-- InfoWindow custom methods }



#-- main {


def main():

    bitpodder_window = BitpodderWindow()

    info_window = InfoWindow()


    bitpodder_window.run()


if __name__ == "__main__":

    main()


#-- main }


But we want the code looks like this (it’s the complete code of BitPodder, don’t worry I will explain each line after the listing):


#!/usr/bin/env python

# -*- coding: UTF8 -*-


# Python module xpodder.py

# Autogenerated from xpodder.glade

# Generated on Tue Sep 06 00:21:46 2005


# Warning: Do not modify any context comment such as #--

# They are required to keep user's code


import os, re, urllib, urlparse, feedparser, codecs

from sys import *

from os.path import *

from sha import *

from bencode import *

import gtk


from SimpleGladeApp import SimpleGladeApp

from SimpleGladeApp import bindtextdomain


app_name = "bitpodder"

app_version = "0.1"


glade_dir = ""

locale_dir = ""


bindtextdomain(app_name, locale_dir)

XISP_Results_Row_Variable = []

XISP_Options_Row_Variable = []

XISP_Link = ""


class BitPodderWindow(SimpleGladeApp):


    def __init__(self, path="glade\\bitpodder.glade",

                 root="bitpodder_window",

                 domain=app_name, **kwargs):

        path = os.path.join(glade_dir, path)

        SimpleGladeApp.__init__(self, path, root, domain, **kwargs)


    #-- Xendor4ispWindow.new {

    def new(self):

        counter = 0

        self.xisp_results_clist.clear()

        self.xisp_option_clist.clear()

        fdw = open("bitpodder.fwp", 'r').readlines()

        for ligne in fdw:

            lesplit = ligne.split(":?:?:")

            lien = lesplit[1].strip()

            rpln = self.xisp_option_clist.append([lesplit[0], lien])

            rpln = self.xisp_comboboxentry.append_text(lesplit[0])

            counter = counter + 1

       

                   # www.pygtk.org/pygtktutorial/sec-statusbars.html

        global xisp_id, xispext_id

        stamess = "  BitPodder     |     " + str(counter) + " subscription(s)"

        xispext_id = self.xisp_statusbar.get_context_id("XISP Statusbar")

        xisp_id = self.xisp_statusbar.push(xispext_id, stamess)

    #-- Xendor4ispWindow.new }


    #-- Xendor4ispWindow custom methods {

    #   Write your own methods here

    #-- Xendor4ispWindow custom methods }


    #-- Xendor4ispWindow.on_feed_toolbutton_clicked {

    def on_feed_toolbutton_clicked(self, widget, *args):

        self.xisp_notebook.set_current_page(0)

#-- Xendor4ispWindow.on_feed_toolbutton_clicked }


    #-- Xendor4ispWindow.on_options_toolbutton_clicked {

    def on_options_toolbutton_clicked(self, widget, *args):

        self.xisp_notebook.set_current_page(1)

    #-- Xendor4ispWindow.on_options_toolbutton_clicked }


    #-- Xendor4ispWindow.on_xisp_go_button_clicked {

    def on_xisp_go_button_clicked(self, widget, *args):

        self.xisp_results_clist.clear()

        xentry = self.xisp_comboboxentry.child

        xentry = self.xisp_comboboxentry.get_child()

        choosen = xentry.get_text()


        urrl = get_xisp_id(choosen)


        page = urllib.urlopen(urrl).read()

        page = page.replace( '"', '"')

        page = page.replace( '&', '&')

        page = page.replace( '&lt;', '<')

        page = page.replace( '&gt;', '>')

        page = page.replace( '&nbsp;', ' ')

#        t = re.compile('.*<title>[a-zA-Z0-9_].*</title>')

        p = re.compile('.*url=["\']([^"\']*)["\'].*torrent')

        files = p.findall(page)

#        titless = t.findall(page)


        cou = 0

        for fff in files:

#            na = titless[cou+1].strip("<title>")

#            na = na.strip("</title>")

#            try:

#                metainfo_file = urllib.urlopen(fff)

#                metainfo = bdecode(metainfo_file.read())

#                info = metainfo['info']

#                na = info['name']

#            except ValueError:

#                   na = os.path.splitext(os.path.split(fff)[1])[0]

#            if info.has_key('length'):

#               na = info['name']

#            else:

#                 na = info['name']

#            na = os.path.splitext(os.path.split(fff)[1])[0]

            na = os.path.splitext(os.path.split(fff)[1])[0]

            scomplink = self.xisp_results_clist.append([na, fff])

            cou = cou + 1


    #-- Xendor4ispWindow.on_xisp_go_button_clicked }


    #-- Xendor4ispWindow.on_xisp_play_button_clicked {

    def on_xisp_play_button_clicked(self, widget, *args):

        if len(XISP_Results_Row_Variable) != 0:

           xrow = XISP_Results_Row_Variable[0]

           scomplink = self.xisp_results_clist.get_text(xrow, 1)

           title = os.path.split(scomplink)[1]


           sharedfolder = 0

           for x in os.listdir(os.getcwd()):

               if x == "Torrents Files": sharedfolder = 1

          

           fol = os.getcwd() + "\\" + "Torrents Files"

           if sharedfolder == 0:

              err = os.mkdir(fol)

       

           title = fol + "\\" + str(title)

           fdm = open(title, 'a')

           fdm.close()

           tor = urllib.urlretrieve(scomplink, title)

    #-- Xendor4ispWindow.on_xisp_play_button_clicked }


    #-- Xendor4ispWindow.on_xisp_results_clist_select_row {

    def on_xisp_results_clist_select_row(self, widget, *args):

        global XISP_Results_Row_Variable

        XISP_Results_Row_Variable = self.xisp_results_clist.selection

    #-- Xendor4ispWindow.on_xisp_results_clist_select_row }


    #-- Xendor4ispWindow.on_show_button_clicked {

    def on_show_button_clicked(self, widget, *args):

        if len(XISP_Results_Row_Variable) != 0:

           xrow = XISP_Results_Row_Variable[0]

           scomplink = self.xisp_results_clist.get_text(xrow, 1)

           global XISP_Link

           XISP_Link = scomplink

           info_window = InfoWindow()

    #-- Xendor4ispWindow.on_show_button_clicked }


    #-- Xendor4ispWindow.on_xisp_add_button_clicked {

    def on_xisp_add_button_clicked(self, widget, *args):

        xtring = self.xisp_add_entry.get_text()

        if xtring != "":

           lurl = xtring

           lxml = feedparser.parse(lurl.strip())

           title = lxml.channel.title

           fdw = open("bitpodder.fwp", 'a')

           if xtring != "":

              ch = title + ":?:?:" + xtring

              fdw.write(ch)

              fdw.write('\n')

           fdw.close()


           xtring = self.xisp_add_entry.set_text("")

           self.xisp_option_clist.clear()


           bitpodder_window = BitPodderWindow()

           self.bitpodder_window.destroy()


    #-- Xendor4ispWindow.on_xisp_add_button_clicked }


    #-- Xendor4ispWindow.on_xisp_remove_button_clicked {

    def on_xisp_remove_button_clicked(self, widget, *args):

        if len(XISP_Options_Row_Variable)!= 0:

           xtrow = XISP_Options_Row_Variable[0]

           if xtrow != "":

              self.xisp_option_clist.remove(xtrow)


           fdw = open("bitpodder.fwp", 'w')

           for drow in range(self.xisp_option_clist.rows):

               ame = self.xisp_option_clist.get_text(drow, 0)

               d = self.xisp_option_clist.get_text(drow, 1)

               ch = ame + ":?:?:" + d

               fdw.write(ch)

               fdw.write('\n')

           fdw.close()


           bitpodder_window = BitPodderWindow()

           self.bitpodder_window.destroy()


    #-- Xendor4ispWindow.on_xisp_remove_button_clicked }


    #-- Xendor4ispWindow.on_xisp_option_clist_select_row {

    def on_xisp_option_clist_select_row(self, widget, *args):

        global XISP_Options_Row_Variable

        XISP_Options_Row_Variable = self.xisp_option_clist.selection

    #-- Xendor4ispWindow.on_xisp_option_clist_select_row }




class InfoWindow(SimpleGladeApp):


    def __init__(self, path="glade\\bitpodder.glade",

                 root="info_window",

                 domain=app_name, **kwargs):

        path = os.path.join(glade_dir, path)

        SimpleGladeApp.__init__(self, path, root, domain, **kwargs)


    #-- InfoWindow.new {

    def new(self):

         global XISP_Link

         link = XISP_Link


         metainfo_file = urllib.urlopen(link)

         metainfo = bdecode(metainfo_file.read())


         info = metainfo['info']

         info_hash = sha(bencode(info))

         txt = "metainfo file.: %s" % basename(link) + "\n"

         txt = txt + "info hash.....:  %s" % info_hash.hexdigest() + "\n"

         piece_length = info['piece length']

         if info.has_key('length'):

            # let's assume we just have a file

            txt = txt + "file name.....:  %s" % info['name'] + "\n"

            file_length = info['length']

            name ='file size.....:  '

         else:

              # let's assume we have a directory structure

              txt = txt + "directory name:  %s" % info['name'] + "\n"

              txt = txt + "files.........:  " + "\n"

              file_length = 0

              for file in info['files']:

                  path = ''

                  for item in file['path']:

                      if (path != ''):

                         path = path + "/"

                      path = path + item

                  txt = txt + "   %s (%d)" % (path, file['length']) + "\n"

                  file_length += file['length']

                  name ='archive size..: '

         piece_number, last_piece_length = divmod(file_length, piece_length)

         txt = txt + "%s %i (%i * %i + %i)" \

               % (name,file_length, piece_number, piece_length, last_piece_length) + "\n"

         txt = txt + "announce url..:  %s" % metainfo['announce'] + "\n"

         if metainfo.has_key('announce-list'):

            list = []

            for tier in metainfo['announce-list']:

                for tracker in tier:

                    list+=[tracker,',']

                del list[-1]

                list+=['|']

            del list[-1]

            liststring = ''

            for i in list:

                liststring+=i

            txt = txt + "announce-list.:  %s" % liststring + "\n"

         if metainfo.has_key('httpseeds'):

            list = []

            for seed in metainfo['httpseeds']:

                list += [seed,'|']

            del list[-1]

            liststring = ''

            for i in list:

                liststring+=i

            txt = txt + "http seeds....:  %s" % liststring + "\n"

         if metainfo.has_key('comment'):

            txt = txt + "comment.......:  %s" % metainfo['comment'] + "\n"



         buf = self.metainfo_textview.get_buffer()

         buf.set_text(txt)

    #-- InfoWindow.new }


    #-- InfoWindow custom methods {

    #   Write your own methods here

    #-- InfoWindow custom methods }




def get_xisp_id(identifiant):

    fdpn = open("bitpodder.fwp").readlines()

    reqxisp = re.compile(identifiant)

    for pe in fdpn:

        if reqxisp.search(pe):

           PeersString = pe.split(":?:?:")

    return PeersString[1]

#-- main {


def main():

    bitpodder_window = BitPodderWindow()


    bitpodder_window.run()


if __name__ == "__main__":

    main()


#-- main }


Like you can see, BitPodder is a small soft of only 300 lines: thanks to python!!!

Ok, ok I promise I will not act like a python prophet lol!!!

Let’s start analyzing bitpodder.py (beta) line by line.

Before anything, we have to include some modules:


import os, re, urllib, urlparse, feedparser, codecs

from sys import *

from os.path import *

from sha import *

from bencode import *

import gtk


Feedparser can be found at feedparser.org, and bencode can be found in Bit Torrent package. But to make your life easy, you can found these modules in the tarball of BitPodder_src.

Then, we have the code generated by tepache:


from SimpleGladeApp import SimpleGladeApp

from SimpleGladeApp import bindtextdomain


app_name = "bitpodder"

app_version = "0.1"


glade_dir = ""

locale_dir = ""


bindtextdomain(app_name, locale_dir)


Now, there are 3 global variables we will use in the app:


XISP_Results_Row_Variable = []

XISP_Options_Row_Variable = []

XISP_Link = ""


Here is the entry of the main window class I named in the glade file: bitpodder_window.

Like you can see: I modify a line: "glade\\bitpodder.glade". Why? Because I decided to put the glade file in another directory so I created a new directory named “\glade\”. The line contains 2 backslash because \b have a meaning in python and we want to annul the backslash coming before the “b”.


class BitPodderWindow(SimpleGladeApp):


    def __init__(self, path="glade\\bitpodder.glade",

                 root="bitpodder_window",

                 domain=app_name, **kwargs):

        path = os.path.join(glade_dir, path)

        SimpleGladeApp.__init__(self, path, root, domain, **kwargs)


    #-- Xendor4ispWindow.new {


Now we will write the new() method. What is this method? The code inside this method will be execute before the window appear, thanks to that we will initializate some widgets before they appear.

Let’s start by clearing the 2 CLists we use in BitPodder.


    def new(self):

        counter = 0

        self.xisp_results_clist.clear()

        self.xisp_option_clist.clear()


Then we will open the file that contains the RSS Feeds links. A line of this file looks like this: “BitTorrent @ AnimeSuki.com:?:?:www.animesuki.com/rss.php”.

Explication: first, there is the name of the feed, then there is this “:?:?:”,  then there is the feed url.

So we open the file and at each line, we will split the line thanks to this: “:?:?:”.

Then we add the name and the url of each RSS Feed in the CList of the Option page and only the name of each feed in the comboboxentry.

Warning!!! Glade has a bug… you have to add this code in the glade file where the comboboxentry is defined: “<property name="items" translatable="yes"></property>” otherwise you will a beautiful error when the app will be launch.

The code that defines the comboboxentry should look like this:


<child>

<widget id="xisp_comboboxentry">

<property name="visible">True</property>

<property name="items" translatable="yes"></property>

<property name="add_tearoffs">False</property>

<property name="has_frame">True</property>

<property name="focus_on_click">True</property>

</widget>

<packing>

<property name="padding">0</property>

<property name="expand">True</property>

<property name="fill">True</property>

</packing>

</child>





        fdw = open("bitpodder.fwp", 'r').readlines()

        for ligne in fdw:

            lesplit = ligne.split(":?:?:")

            lien = lesplit[1].strip()

            rpln = self.xisp_option_clist.append([lesplit[0], lien])

            rpln = self.xisp_comboboxentry.append_text(lesplit[0])

            counter = counter + 1


Now we can add the string in the status bar.


                   # www.pygtk.org/pygtktutorial/sec-statusbars.html

        global xisp_id, xispext_id

        stamess = "  BitPodder     |     " + str(counter) + " subscription(s)"

        xispext_id = self.xisp_statusbar.get_context_id("XISP Statusbar")

        xisp_id = self.xisp_statusbar.push(xispext_id, stamess)

    #-- Xendor4ispWindow.new }


    #-- Xendor4ispWindow custom methods {

    #   Write your own methods here

    #-- Xendor4ispWindow custom methods }


Now we will tell the app what to do when users click on Feeds and Options buttons.

Like I said, I used a notebook, so I just have to open the right page: each page is identified by a number.


#-- Xendor4ispWindow.on_feed_toolbutton_clicked {

    def on_feed_toolbutton_clicked(self, widget, *args):

        self.xisp_notebook.set_current_page(0)

#-- Xendor4ispWindow.on_feed_toolbutton_clicked }


    #-- Xendor4ispWindow.on_options_toolbutton_clicked {

    def on_options_toolbutton_clicked(self, widget, *args):

        self.xisp_notebook.set_current_page(1)

    #-- Xendor4ispWindow.on_options_toolbutton_clicked }


Now we will tell the app what to do when users click on Go button.

We will obtain the feed name the user chooses in the comboboxentry and put it in the variable choosen.


    #-- Xendor4ispWindow.on_xisp_go_button_clicked {

    def on_xisp_go_button_clicked(self, widget, *args):

        self.xisp_results_clist.clear()

        xentry = self.xisp_comboboxentry.child

        xentry = self.xisp_comboboxentry.get_child()

        choosen = xentry.get_text()


After having the name, we have to get the url associated with the name so I created a function named get_xisp_id() that I will define later. After having the url feed, I will read the web page, replace some strings and search all the string that look like this “url=blablabla.torrent” (you have to know the .torrent urls are put in the <enclosure> tag that look like that: “<enclosure url="www.anime-kraze.org/torrent/[Ani-Kraze]_Tsubasa_Chronicle_-_16_[2CE95BC4].avi.torrent" length="14008" type="application/x-bittorrent"/>”). When I get all the links BitPodder found, first I will parse the url to obtain the .torrent name thanks to os.path.splitext(xxx)[0], then I will add the results in the Feeds page’s CList.


        urrl = get_xisp_id(choosen)


        page = urllib.urlopen(urrl).read()

        page = page.replace( '&quot;', '

gipoco.com is neither affiliated with the authors of this page nor responsible for its contents. This is a safe-cache copy of the original web site.