Progressbar and Threads in PyGTK

    Recently there was a need and desire to get acquainted with PyGTK. There is practically no literature in Russian on this issue, and what Google finds on various blogs is a bit outdated. I was also surprised to find that the PyGTK theme is not particularly popular on the hub too.

    So, I will not talk about the layout of the interface elements, because such articles already exist. I'll tell you about the next step: creating an application that does some kind of work, in the process displaying its progress.

    For example, we write a primitive GUI for two functions of the convert utility (ImageMagick package). Our program will take four values:
    • the size to which to reduce the image
    • quality
    • catalog with images
    • directory where to save

    We will create the interface in glade. Importantly , the project must be in GtkBuilder format.

    Glade GUI


    The following are two code examples, the first is ordinary, and the second, with the creation of a separate thread for image processing. There are two examples - in order to clearly see if it makes sense to mess with threads.

    Program skeleton: Add the main on_start method, which displays the progress bar dialog, receives a user-specified value, generates a list of files (excludes directories) and is directly involved in processing. We display our dialog box, load the values ​​from the GUI and generate a list of files. Next, run the loop with iterating over the files list.

    #!/usr/bin/python
    # coding: utf-8

    try:
        import sys, pygtk
        pygtk.require('2.0')
    except:
        print 'Не удалось импортировать модуль PyGTK'
        sys.exit(1)

    import gtk, os, time

    class GUI(object):
        def __init__(self):
            self.wTree = gtk.Builder()
            # Загружаем наш интерфейс
            self.wTree.add_from_file("convert.glade")
            # присоединяем сигналы
            self.wTree.connect_signals(self)
            self.window1 = self.wTree.get_object("window1")
            self.progressdialog = self.wTree.get_object("progressdialog")
            self.progressbar_label = self.wTree.get_object("progressbar_label")
            self.window1.show()
            # Значеия по умолчанию
            self.wTree.get_object("size").set_value(100)
            self.wTree.get_object("quality").set_value(95)
            
            self.progressbar = self.wTree.get_object("progressbar")

        def on_cancel(self,widget):
            gtk.main_quit()

        def on_progressdialog_close(self, *args):
            self.stop = True
            self.progressdialog.hide()
            return True

    if __name__ == "__main__":
        app = GUI()
        gtk.main()




        def on_start(self,widget):
            self.progressdialog.show()
            self.stop = False
            # Значения с GUI
            self.size = int(self.wTree.get_object("size").get_value())
            self.quality = int(self.wTree.get_object("quality").get_value())
            self.from_dir = self.wTree.get_object("from_dir").get_current_folder()
            self.to_dir = self.wTree.get_object("to_dir").get_current_folder()
            
            files = []
            all_files = os.listdir(self.from_dir)
            for f in all_files:
                fullname = os.path.join(self.from_dir, f)
                if os.path.isfile(fullname):
                    files.append(f)
            
            count = len(files)
            i = 1.0
            for file in files:
                # Остановка
                if self.stop:
                    break
                
                self.progressbar_label.set_text(file)
                self.progressbar.set_fraction(i/count)
                
                os.popen('convert -resize ' + str(self.size) + ' -quality ' + str(self.quality) + ' ' + os.path.join(self.from_dir, file) + ' ' + os.path.join(self.to_dir, file))
                
                # Обновляем диалоговое окно
                while gtk.events_pending():
                    gtk.main_iteration()
                
                time.sleep(5)
                i += 1
            
            self.progressdialog.hide()




    First of all, we check if there is a stop flag indicating the termination of processing if self.stop (set by the on_progressdialog_close method on the Cancel button or closing the dialog box). Further, in the process we change the text and the percentage of processing in the progress bar, and also run the convert utility itself with the necessary parameters.

    An important piece of code Without it, our dialog box simply does not appear, and the interface itself freezes until processing is complete. This is because our loop blocks the redrawing of the interface (the main loop) until it completes. The above code returns control to the main loop for a short time.
    while gtk.events_pending():
        gtk.main_iteration()




    Also, I specifically added time.sleep (5), and if someone has a fast computer, they may not notice that during the processing itself (or sleep) the interface does not respond to events.

    Threads


    With threads in PyGTK, I had to tinker unknowingly. First, you need to call gtk.gdk.threads_init (), and all further calls to gtk must be framed by gtk.threads_enter () and gtk.threads_leave (), such is the specifics. Our code has changed a bit. We created the self.dialog dictionary, which is useful to us below, and the on_start method now only prepares the data and launches a new thread, in which direct processing does not block the interface of our program. Let's create a Worker class: We had to redefine the constructor (__init__) so that it accepts parameters. These are the dialog dictionaries (I said it will come in handy), data (size, quality, and two directories) and a list of files. In the run method we put what needs to be done at startup - i.e. the processing itself.

    #!/usr/bin/python
    # coding: utf-8

    try:
        import sys, pygtk
        pygtk.require('2.0')
    except:
        print 'Не удалось импортировать модуль PyGTK'
        sys.exit(1)

    import threading, gtk, os, time

    class GUI(object):
        def __init__(self):
            self.wTree = gtk.Builder()
            # Загружаем наш интерфейс
            self.wTree.add_from_file("convert.glade")
            # присоединяем сигналы
            self.wTree.connect_signals(self)
            self.window1 = self.wTree.get_object("window1")
            self.dialog = {
                "progressdialog" : self.wTree.get_object("progressdialog"),
                "progressbar_label" : self.wTree.get_object("progressbar_label"),
                "progressbar" : self.wTree.get_object("progressbar")
                }
            self.window1.show()
            # Значеия по умолчанию
            self.wTree.get_object("size").set_value(100)
            self.wTree.get_object("quality").set_value(95)

        
        def on_cancel(self,widget):
            gtk.main_quit()
        
        def on_progressdialog_close(self, *args):
            self.work.stop()
            self.dialog['progressdialog'].hide()
            return True
        
        def on_start(self,widget):
            self.dialog['progressdialog'].show()
            # Значения
            self.data = {
                'size' : int(self.wTree.get_object("size").get_value()),
                'quality': int(self.wTree.get_object("quality").get_value()),
                'from_dir' : self.wTree.get_object("from_dir").get_current_folder(),
                'to_dir' : self.wTree.get_object("to_dir").get_current_folder()
                    }
            
            files = []
            all_files = os.listdir(self.data['from_dir'])
            for f in all_files:
                fullname = os.path.join(self.data['from_dir'], f)
                if os.path.isfile(fullname):
                    files.append(f)
                
            self.work = Worker(self.dialog, self.data, files)
            self.work.start()
            

    if __name__ == "__main__":
        gtk.gdk.threads_init()
        app = GUI()
        gtk.gdk.threads_enter()
        gtk.main()
        gtk.gdk.threads_leave()





    class Worker(threading.Thread):
        # События нити
        stopthread = threading.Event()
        
        def __init__ (self, dialog, data, files):
            threading.Thread.__init__(self)
            self.dialog = dialog
            self.data = data
            self.files = files
        
        def run(self):
            count = len(self.files)
            i = 1.0
            for file in self.files:
                # Остановка по событию
                if ( self.stopthread.isSet() ):
                    self.stopthread.clear()
                    break
                
                self.dialog['progressbar'].set_fraction(i/count)
                self.dialog['progressbar_label'].set_text(file)
                
                os.popen('convert -resize ' + str(self.data['size']) + ' -quality ' + str(self.data['quality']) + ' ' + os.path.join(self.data['from_dir'], file) + ' ' + os.path.join(self.data['to_dir'], file))
                
                time.sleep(2)
                i += 1
            # Очищаем события
            self.stopthread.clear()
            # Скрываем диалоговое окно
            self.dialog['progressdialog'].hide()
        
        def stop(self):
            self.stopthread.set()





    That's all, as they say, "Result on the face."

    I want to note, in this example, the calculation of the percentage of completion and its change for the dialogue, occur directly in the working thread. This is considered bad form, and if there were more than one thread, it would not work (more precisely, it would work with errors).

    A slightly expanded version of our program looks like this

    Simple Images Converter


    Read about the features and download here .

    Archive with source codes .

    UPD:
    alex3d recalled that if you match the manual, in Worker.run you need to wrap the lines in gtk.threads_enter () / gtk.threads_leave ().
    self.dialog['progressbar'].set_fraction(i/count)
    self.dialog['progressbar_label'].set_text(file)



    Also popular now: