Open source repositories at: github and bitbucket. Current project: erp4all

Saturday, July 23, 2011

Systray και popup menu με το Glade

Σε αυτό το άρθρο θα παρουσιάσουμε πως μπορεί η εφαρμογή μας να έχει ένα εικονίδιο στον δίσκο συστήματος (systray) και πως κάνοντάς του κλικ μας εμφανίζει το κεντρικό παράθυρο της εφαρμογής ή ένα popup μενού για έλεγχό της.

Θα φτιάξουμε ένα μικρό slider που θα ελέγχει την ένταση του ήχου, όπως αυτό που υπάρχει ήδη στα περισσότερα συστήματα linux. Έχει καθαρά εκπαιδευτικούς σκοπούς δείχνοντας την αλληλοεπίδραση μεταξύ των σημάτων του systray και του προγράμματος. Για τον έλεγχο του ήχου θα χρησιμοποιήσουμε είτε την βιβλιοθήκη alsaaudio της python είτε την εντολή amixer του συστήματος της ALSA.

Δημιουργούμε το παράθυρο μας στο Glade και του δίνουμε όνομα volumewindow. Στον Τύπο παραθύρου επιλέγουμε Αναδυόμενο (popup). Αυτού του είδους τα παράθυρα δεν έχουν πλαίσιο και διακοσμήσεις και δεν τα ελέγχει ο window manager του συστήματος. Στις αιτήσεις πλάτους και ύψους δίνουμε αντίστοιχα 60 και 170 pixels, ενώ στο Πλάτος πλαισίου δίνουμε τιμή 2. Αυτό είναι το περιθώριο που θα έχουν όλα τα αντικείμενα του παραθύρου από τις άκρες του.

Στην συνέχεια προσθέτουμε ένα frame και αφαιρούμε τα alignment και label που δημιουργεί. Επιλέγουμε την σκίαση της αρεσκείας μας και προσθέτουμε μέσα του ένα πλαίσιο fixed. Αυτό το πλαίσιο μας επιτρέπει να τοποθετήσουμε αντικείμενα σε συγκεκριμένες θέσεις μέσα του. Βλέπουμε ότι έχουμε και αυτήν την δυνατότητα εκτός από την τοποθέτηση σε γραμμές και στήλες. Στην συγκεκριμένη περίπτωση είναι χρήσιμο γιατί το παράθυρο δεν μπορεί να αλλάξει μέγεθος και έτσι με λίγο πειραματισμό μπορούμε να τοποθετήσουμε τα διάφορα widget όπου ακριβώς θέλουμε και να είμαστε σίγουροι ότι πάντα θα φαίνονται έτσι.

Προσθέτουμε μία Κατακόρυφη κλιμακα (slider) και την ονομάζουμε masterslider. Θέτουμε τα Ψηφία στο μηδέν, την Θέση τιμής κάτω και την ορίζουμε σαν Αντεστραμμένη. Προσθέτοντας μια νέα Ρύθμιση το glade την ονομάζει αυτόματα adjustment1 και της δίνει ορισμένες αρχικές τιμές. Αλλάζουμε την Μέγιστη τιμή σε 100, τον Ρυθμό αύξησης βήματος σε 1, την Αύξηση σελίδας σε 10 και το Μέγεθος σελίδας σε 0.

Επιστρέφοντας στο masterslider και στο tab packing βλέπουμε ότι τα συνηθισμένα Γέμισμα και Ανάπτυξη έχουν αντικατασταθεί από τα Θέση Χ και Θέση Υ , λόγω του ότι βρίσκετε μέσα σε fixed πλαίσιο. Σε αυτά βάζουμε αντίστοιχα τις τιμές 10 και 6. Στα Σήματά του και συγκεκριμένα στο value-changed δίνουμε σαν handler την μέθοδο masterslider_change. Αυτή θα αλλάζει την ένταση του ήχου ανάλογα με την θέση του slider.

Στην συνέχεια προσθέτουμε ένα button με το όνομα button, και στην ετικέτα του βάζουμε Mixer. Στις θέσεις Χ και Υ βάζουμε 1 και 132 αντίστοιχα, ενώ στο σήμα clicked δίνουμε σαν handler button_clicked. Αυτή η μέθοδος θα εμφανίζει το gnome-alsamixer, αν υπάρχει στο σύστημα.

Με το μικρό μας παράθυρο έτοιμο ας προσθέσουμε τα υπόλοιπα αντικείμενα που χρειαζόμαστε για το πρόγραμμα μας. Επιλέγουμε ένα Αναδυόμενο μενού (popup) και του δίνουμε το όνομα menu. Με το menu επιλεγμένο πατάμε edit στην μπάρα εργαλείων και προσθέτουμε δύο Στοιχεία εικόνας. Για το πρώτο επιλέγουμε το έτοιμο κουμπί stock Περί και για το άλλο το Έξοδος. Στα activate σήματα τους βάζουμε σαν handler αντίστοιχα τις μεθόδους about dialog και close_window.

Το βασικότερο μας αντικείμενο είναι ένα Εικονίδιο γραμμής κατάστασης το οποίο θα το βρούμε στα Διάφορα. Προσθέτουμε ένα και το ονομάζουμε staticon. Στο Pixbuf γράφουμε το όνομα του αρχείου του εικονιδίου που θα χρησιμοποιήσουμε. Υπάρχουν πολλά εικονίδια που μπορείτε να χρησιμοποιήσετε στον κατάλογο /usr/share/icons/gnome. Αντιγράψτε ένα της αρεσκείας σας στον ίδιο κατάλογο με τα αρχεία glade και python. Στον Τίτλο δίνουμε SysAudio. Αυτό θα εμφανίζεται όταν περνάει ο δείκτης του ποντικιού από πάνω του. Στα Σήματα του δίνουμε στο activate την μέθοδο window_show και στο popup-menu την menu_popup. Η πρώτη μέθοδος εκτελείται όταν ο χρήστης κάνει αριστερό κλικ πάνω στο εικονίδιο και θα εμφανίσει το παράθυρο μας με τον slider, ενώ η δεύτερη όταν κάνει ένα από τα υπόλοιπα και συγκεκριμένα δεξί κλικ, εμφανίζοντας το μενού της εφαρμογής.

Τα βασικά αντικείμενα της εφαρμογής είναι τα εξής:

Σε αυτό το σημείο το αρχείο glade είναι έτοιμο, έχουμε προσδιορίσει ότι θα χρειαστούμε και το σώζουμε σαν volumecontrol.glade. Το πρόγραμμα μας σε python είναι απλό και περιέχει στοιχεία που έχουμε δει ήδη.

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

import os
try:
    import alsaaudio
    HAVEALSA=True
except:
    import subprocess
    import commands
    HAVEALSA=False
import pygtk
pygtk.require('2.0')
import gtk

class VolumeControl():

    def __init__(self):
        builder=gtk.Builder()
        builder.add_from_file("volumecontrol.glade")
        builder.connect_signals(self)
        self.window = builder.get_object("volumewindow")
        self.window_position = (0, 0)
        self.staticon = builder.get_object("staticon")
        self.menu = builder.get_object("menu")
        self.masterslider = builder.get_object("masterslider")

    def close_window(self, widget, data=None):
        gtk.main_quit()

    def button_clicked(self, widget):
        os.system('gnome-alsamixer')

    def window_show(self, widget, data=None):
        if self.window.get_property('visible'):
            self.window.hide()
        else:
            self.masterslider.set_value(self.get_master_volume())
            self.set_window_position()
            self.window.move(self.window_position[0], self.window_position[1])
            self.window.show_all()
            self.window.present()
           
    def menu_popup(self, widget, button, time, data = None):
        if button == 3:
            self.menu.show_all()
            self.menu.popup(None, None, None, 3, time)

    def masterslider_change(self, widget):
        volume = int(widget.get_value())
        if HAVEALSA:
            mix = alsaaudio.Mixer()
            mix.setvolume(volume)
        else:
            proc = subprocess.Popen('/usr/bin/amixer sset Master ' \
                    + str(volume) + '%', shell=True, stdout=subprocess.PIPE)
            proc.wait()

    def set_window_position(self):
        staticon_geometry = self.staticon.get_geometry()[1]
        if staticon_geometry.y <= 200:
            y_coords = staticon_geometry.y
        else:
            y_coords = staticon_geometry.y-180
        self.window_position = (staticon_geometry.x+20, y_coords+10)

    def get_master_volume(self):
        if HAVEALSA:
            mix = alsaaudio.Mixer()
            return mix.getvolume()[0]
        else:
            output = commands.getoutput('/usr/bin/amixer sget Master | grep "%"')
            master = output.split('\n')[0]
            start = master.find('[') + 1
            end = master.find('%]', start)
            return float(master[start:end])

    def aboutdialog(self, widget, data=None):
        about = gtk.AboutDialog()
        about.set_program_name('Volume Control')
        about.set_version('1.0')
        about.set_comments(u'Πρόγραμμα ελέγχου της στάθμης του ήχου.')
        about.run()
        about.destroy()

    def main(self):
        gtk.main()

if __name__ == "__main__":
    app=VolumeControl()
    app.main()

Στην αρχή προσπαθούμε να φορτώσουμε την βιβλιοθήκη alsaaudio και αν αυτό αποτύχει φορτώνουμε τις subprocess και command για να μπορέσουμε να εκτελέσουμε εντολή του συστήματος και να πάρουμε το αποτέλεσμα της. Ορίζουμε και μια μεταβλητή για να ξέρουμε αν έχουμε φορτώσει την alsaaudio ή όχι. Οι βασικές μέθοδοι είναι γνωστές από τα προηγούμενα άρθρα. Στην __init__ φορτώνουμε το glade αρχείο και ορίζουμε τις μεταβλητές που θα χρησιμοποιήσουμε. Η aboutdialog δημιουργεί και δείχνει το παράθυρο “Περί” όταν πατήσει ο χρήστης την αντίστοιχη επιλογή στο μενού και η close_window τερματίζει το πρόγραμμα.

Η get_master_volume επιστρέφει την τιμή που έχει η ένταση του ήχου κατά τη στιγμή της κλήσης της. Χρησιμοποιεί είτε την getvolume της alsaaudio είτε το αποτέλεσμα της εντολής “amixer sget Master”. Η set_window_position θέτει την θέση του παραθύρου λίγο πιο κάτω και λίγο πιο δεξιά από το εικονίδιο.

Η window_show εκτελείται όταν ο χρήστης κάνει αριστερό κλικ στο εικονίδιο και το κρύβει αν αυτό είναι ήδη ορατό ή το εμφανίζει αφού πρώτα υπολογίσει την τιμή του ήχου και την θέση του παραθύρου. H present που καλείται στο τέλος έχει σαν σκοπό να εμφανίζει το παράθυρο αν αυτό καλύπτεται από κάποιο άλλο.

Η master_slider_change εκτελείται όταν ο χρήστης αλλάξει την θέση του slider και αναλόγως αυτής θέτει την τιμή της έντασης είτε με την setvolume είτε με την “amixer sset Master”.

Η menu_popup εμφανίζει το μενού της εφαρμογής. Τα popup menu χρειάζονται για να εμφανιστούν τον αριθμό του κουμπιού του ποντικιού που πατήθηκε και την χρονική στιγμή που έγινε το γεγονός. Αυτά και τα δύο μας τα δίνει το σήμα popup-menu του εικονιδίου, εμείς μένει μόνο να ελέγξουμε αν το κουμπί που πατήθηκε είναι το δεξί.

Η button_clicked εμφανίζει το gnome-alsamixer, αν είναι εγκατεστημένο, αλλιώς θα εμφανίσει μήνυμα λάθους στην γραμμή εντολών. Εδώ μπορείτε, με τις πληροφορίες από τα προηγούμενα άρθρα, να προσθέσετε ένα ErrorDialog για να ενημερώνεται ο χρήστης.

Είναι μια απλή εφαρμογή αλλά δείχνει τα στοιχεία που χρειάζονται για την δημιουργία ενός systray προγράμματος. Με τον ίδιο τρόπο διάφορα προγράμματα, όπως η Opera και το Transmission, βάζουν ένα εικονίδιο στο systray και το πρόγραμμα δεν τερματίζει αν ο χρήστης πατήσει το Χ στο παράθυρο του αλλά κρύβεται απλώς και εμφανίζεται πάλι αν ο χρήστης κάνει αριστερό κλικ στο εικονίδιο, ενώ με δεξί εμφανίζει είτε μενού είτε πληροφορίες του προγράμματος.

Τα αρχεία του άρθρου υπάρχουν σε .tar.gz εδώ

Sunday, June 26, 2011

Σύνθετα αντικείμενα στο Glade (γ' μέρος)

Σε αυτό το άρθρο θα παρουσιάσουμε το πρόγραμμα, γραμμένο σε python, που μετατρέπει τις κωδικοποιήσεις των αρχείων. Το πρόγραμμα καθώς και τα συνοδευτικά του αρχεία βρίσκονται εδώ.

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

import gtk
import os
import codecs

CODEPAGES = ['utf_16','utf_8','cp1253','iso8859_7']
EOL = ['Windows','Unix','Mac']
DICT_EOL = {'Windows':'\r\n','Unix':'\n','Mac':'\r'}

class FileToConvert(object):

    def __init__(self,fn = None, cp = None, eol=None):
        self.fn = fn
        self.cp = cp
        self.eol = eol
        self.to_cp = ''
        self.to_eol = ''

    def checkfile(self,codefrom=None):
        encodings=CODEPAGES
        if codefrom:
            encodings=[codefrom]
        for enc in encodings:
            t1=codecs.open(self.fn,'r',enc)
            try:
                line=t1.readline()
                if '\r\n' in line:
                    self.eol = EOL[0]
                elif '\n' in line:
                    self.eol = EOL[1]
                elif '\r' in line:
                    self.eol = EOL[2]
                self.cp=enc
                t1.close()
                ret=True
                break
            except:
                ret=False
        return ret    

class ConvertFiles(object):

    def close_window(self, widget, data=None):
        gtk.main_quit()

    def delete_chooser(self, widget, data=None):
        return True

    def aboutmenu_clicked(self, widget, data=None):
        self.aboutdialog.run()
        self.aboutdialog.hide()

    def helpmenu_clicked(self, widget, data=None):
        import webbrowser
        webbrowser.open("help.html")

    def openmenu_clicked(self, widget, data=None):
        self.filechooser.show()

    def filterbox_changed(self, widget, data=None):
        if widget.get_active() == 0:
            self.filechooser.set_filter(self.txtfilter)
        if widget.get_active() == 1:
            self.filechooser.set_filter(self.srtfilter)
        if widget.get_active() == 2:
            self.filechooser.set_filter(self.allfilter)              

    def filelistview_key(self, widget, data=None):
        treeviewmodel, treeviewindex = widget.get_selection().get_selected()
        if (gtk.gdk.keyval_name(data.keyval) == 'Delete') and treeviewindex:
            treeviewmodel.remove(treeviewindex)

    def filelist_row_deleted(self,widget,data=None):
        del self.files[data[0]]
        self.statusbar.push(0,"Αφαίρεση αρχείου")

    def filechooser_cancel(self, widget, data=None):
        self.filechooser.hide()

    def filechooser_ok(self, widget, data=None):
        filenames=[]
        if self.files:
            for fi in self.files:
                filenames.append(fi.fn)
        errortxt=''
        i=0
        for fi in self.filechooser.get_filenames():
            if fi not in filenames:
                newfile = FileToConvert(fi)
                if newfile.checkfile():
                    self.files.append(newfile)
                    self.filelist.append([newfile.fn,newfile.cp,newfile.eol])
                    i+=1
                else:
                    errortxt+=u"Πρόβλημα στην ανάγνωση του αρχείου: %s.\n" % cf.fn
                    self.errordialog(errortxt)
        self.filechooser.hide()
        if i>0:
            self.statusbar.push(0,u"Επιτυχής αναγνωση %d αρχείων" % i)
        else:
            self.statusbar.push(0,"")

    def errordialog(self, errortxt):
        dialog=gtk.MessageDialog(parent=None,flags=gtk.DIALOG_MODAL & gtk.DIALOG_DESTROY_WITH_PARENT, type=gtk.MESSAGE_ERROR, buttons=gtk.BUTTONS_OK)
        dialog.set_title(u"Σφάλμα")
        dialog.set_markup(u"<b>"+errortxt+"</b>")
        response=dialog.run()
        if response == gtk.RESPONSE_DELETE_EVENT or response == gtk.RESPONSE_OK:
           dialog.destroy()

    def savemenu_clicked(self, widget, data=None):
        errortxt=''
        if not self.files:
            self.errordialog(u"Δεν έχετε επιλέξει αρχεία.\n")
            return
        for fi in self.files:
            if self.codefrombox.get_active()>0:
                if not fi.checkfile(CODEPAGES[self.codefrombox.get_active()-1]):
                    errortxt+=u"Πρόβλημα στην ανάγνωση του αρχείου: %s.\n" % fi.fn
        if errortxt:
            self.errordialog(errortxt)
            self.statusbar.push(0,u"Δεν ήταν σωστή η κωδικοποίηση που ορίσατε.")
            return
        for i in range(len(self.files)):
            self.files[i].to_cp=CODEPAGES[self.codetobox.get_active()]
            self.files[i].to_eol=EOL[self.eolbox.get_active()]
        treeiter=self.filelist.get_iter_first()
        for fi in self.files:
            try:
                t1=codecs.open(fi.fn,'r',fi.cp)
                t2=codecs.open('tmp','w',fi.to_cp)
                for line in t1:
                    line=line.replace(DICT_EOL[fi.eol],DICT_EOL[fi.to_eol])
                    t2.write(line)
                t2.close()
                t1.close()
                os.rename('tmp',fi.fn)
            except:
                self.errordialog(u"Πρόβλημα στην μετατροπή του %s.\n" % fi.fn)
                success=False
            else:
                if fi.checkfile():
                    self.filelist.set_value(treeiter,1,fi.cp)
                    self.filelist.set_value(treeiter,2,fi.eol)
                    fi.to_cp = ''
                    fi.to_eol = ''
                    success=True
                else:
                    self.errordialog(u"Πρόβλημα στην τελική ανάγνωση του %s.\n" % fi.fn)
                    success=False
            treeiter=self.filelist.iter_next(treeiter)
        if success:
            self.statusbar.push(0,u"Επιτυχής μετατροπή")
        else:
            self.statusbar.push(0,u"Προβλήματα στην μετατροπή")
     
    def __init__(self):
        builder = gtk.Builder()
        builder.add_from_file("convert.glade")
        builder.connect_signals(self)
        self.window = builder.get_object("convertwindow")
        self.codefrombox = builder.get_object("codefrombox")
        self.codetobox = builder.get_object("codetobox")
        self.eolbox = builder.get_object("eolbox")
        self.codefromlist = builder.get_object("codefromlist")
        self.codetolist = builder.get_object("codetolist")
        self.eollist = builder.get_object("eollist")
        self.filelist = builder.get_object("filelist")
        self.codefromlist.append([u"Μαντεψιά"])
        for item in CODEPAGES:
            self.codefromlist.append([item])
            self.codetolist.append([item])
        for item in EOL:
            self.eollist.append([item])
        self.codefrombox.set_active(0)
        self.codetobox.set_active(0)
        self.eolbox.set_active(0)
        self.filechooser = builder.get_object("filechooser")
        self.aboutdialog = builder.get_object("aboutdialog")
        self.txtfilter = builder.get_object("txtfilter")
        self.srtfilter = builder.get_object("srtfilter")
        self.allfilter = builder.get_object("allfilter")
        self.filterbox = builder.get_object("filterbox")
        self.txtfilter.add_pattern('*.txt')
        self.txtfilter.add_pattern('*.TXT')
        self.srtfilter.add_pattern('*.srt')
        self.srtfilter.add_pattern('*.SRT')
        self.allfilter.add_pattern('*.*')
        self.filterbox_changed(self.filterbox)
        self.statusbar=builder.get_object("statusbar")
        self.files=[]
     
    def main(self):
        self.window.show()
        gtk.main()

if __name__== "__main__":
    app=ConvertFiles()
    app.main()  
Ξεκινάμε στην αρχή με τα απαραίτητα import. Το νέο εδώ είναι η βιβλιοθήκη codecs, η οποία θα μας χρησιμεύσει στις μετατροπές. Στην συνέχεια υπάρχουν τρεις σταθερές. Η πρώτη περιέχει τα λεκτικά των κωδικοποιήσεων έτσι όπως τα χρειάζεται η codecs και με αυτά γεμίζουμε και τα δύο μας combobox. H EOL σταθερά είναι αυτή που περιέχει τα λεκτικά για τα διάφορα τελειώματα γραμμής που υπάρχουν. Στην επόμενη σταθερά γίνεται συσχέτιση μεταξύ αυτών των λεκτικών και των πραγματικών τιμών που έχει το κάθε τέλος γραμμής.


Θα χρειαστούμε να ορίσουμε ένα αντικείμενο για κάθε αρχείο που θα διαλέξει ο χρήστης κι αυτό θα φτιαχτεί από την κλάση FileToConvert. Το αντικείμενο θα περιέχει τις εξής πληροφορίες: το όνομα του αρχείου, την κωδικοποίηση και το τέλος γραμμής που έχει πριν την μετατροπή καθώς και σε τι θέλουμε να το μετατρέψουμε. Η κλάση ακόμα περιλαμβάνει την μέθοδο checkfile η οποία προσπαθεί να μαντέψει ποια από τις τέσσερις κωδικοποιήσεις και ποιο από τα τρία EOL έχει το αρχείο. Αν είναι επιτυχής επιστρέφει True, αλλιώς False.


Στην κύρια κλάση έχουμε προσθέσει στην __init__ μέθοδο δύο μεταβλητές, την self.filelist η οποία είναι το μοντέλο του TreeView και την self.files στην οποία θα κρατάμε τα αντικείμενα FilesToConvert.


H μέθοδος filelistview_key εκτελείται όταν ο χρήστης πατήσει ένα πλήκτο. Η get_selected() επιστρέφει δύο τιμές το μοντέλο στο οποίο ανήκει το TreeView και τον αύξων αριθμό της επιλογής. Έτσι λοιπόν ελέγχουμε αν ο χρήστης έχει πατήσει Delete και αν έχει επιλέξει γραμμή. Τότε μόνο διαγράφουμε την αντίστοιχη επιλογή από το μοντέλο. Όταν διαγραφεί ένα στοιχείο από το μοντέλο εκτελείται η filelist_row_deleted στην οποία διαγράφουμε το αντίστοιχο αντικείμενο από την self.files.


Όταν ο χρήστης επιλέξει αρχεία φτιάχνουμε έναν προσωρινό πίνακα με τα ονόματα των αρχείων που υπάρχουν ήδη έτσι ώστε να αποφύγουμε τις διπλές εισαγωγές. Στην συνέχεια για κάθε αρχείο φτιάχνουμε το αντίστοιχο αντικείμενό του και καλούμε την chekfile για να προσδιορίσει τον τύπο του αρχείου και του τέλους της γραμμής του. Για κάθε επιτυχή αναγνώριση προσθέτουμε τα αντίστοιχα πεδία στην self.filelist για να εμφανιστούν στο TreeView.


Εδώ θα θέλαμε να αναφέρουμε ορισμένα στοιχεία για την codecs.open. Αυτή όταν ανοίγει ένα αρχείο για διάβασμα το ανοίγει πάντα σε binary mode. Αυτό έχει σαν αποτέλεσμα να μην χτυπάει λάθος όταν αρχείο δεν είναι text. Ακόμα επειδή οι λατινικοί χαρακτήρες έχουν την ίδια κωδικοποίηση τόσο σε utf-8 όσο και σε windows-1253 ή ISO8859-7 μπορεί να μην δώσει σωστό αποτέλεσμα σε αρχεία με μόνο τέτοιους χαρακτήρες. Ένα άλλο σημείο προσοχής είναι ότι η μόνη διαφορά, στους ελληνικούς εμφανίσιμους χαρακτήρες, μεταξύ των windows-1253 και ISO8859-7 είναι το άλφα κεφαλαίο τονούμενο (Ά), οπότε πάλι μπορεί να έχουμε λάθος προσδιορισμό. Γι´ αυτό και βάλαμε την επιλογή “Κωδικοποίηση από” για να μπορεί να επιλέξει ο χρήστης αν γνωρίζει την κωδικοποίηση του αρχείου προς μετατροπή.


Η μετατροπή γίνεται στην savemenu_clicked, ανοίγοντας με την σειρά τα αρχεία που υπάρχουν στην self.files με την κωδικοποίηση που έχει ορίσει ο χρήστης και σώζοντας τα σε ένα προσωρινό με την νέα. Αν όλα πάνε καλά τα ξαναδιαβάζει για να ενημερώσει το TreeView και για να δει ο χρήστης το αποτέλεσμα.


Καθ' όλη την διαδικασία της μετατροπής το πρόγραμμα ενημερώνει τον χρήστη με δύο τρόπους. Ο ένας είναι ο διάλογος μηνυμάτων που έχουμε δει και σε προηγούμενο άρθρο και ο άλλος είναι μέσω της statusbar. Η statusbar λειτουργεί σαν στοίβα LIFO. Βάζουμε στην στοίβα αυτό που θέλουμε να εμφανίσει με push και το αφαιρούμε με pop. Κάθε μήνυμα μπορεί να έχει και ένα αναγνωριστικό κωδικό, προσδιορίζοντας έτσι από πιο μέρος του προγράμματος προήλθε. Εδώ δεν χρησιμοποιούμε αυτήν την δυνατότητα γι' αυτό όλα τα μηνύματα έχουν σαν προσδιοριστικό το μηδέν.


Είδαμε σ' αυτό το άρθρο ορισμένα από τα πιο βασικά σύνθετα αντικείμενα του Gnome και του GTK και πως τα αναπτύσσουμε αυτά μέσα από το Glade. Βλέπουμε ότι ενώ αυτά είναι αρκετά πιο πολύπλοκα από ένα απλό label ή ένα text input, ωστόσο όλη η διαδικασία δημιουργίας και ελέγχου τους ολοκληρώνεται μέσα από το Glade προσθέτοντας ελάχιστο προγραμματιστικό βάρος.
 

Tuesday, June 21, 2011

Σύνθετα αντικείμενα στο Glade (β' μέρος)

Σε αυτό το άρθρο θα συνεχίσουμε την κατασκευή του κεντρικού παραθύρου για το πρόγραμμα μας και θα προσθέσουμε ορισμένα βοηθητικά για να δούμε την χρήση τους. Στο τρίτο μέρος θα παρουσιάσουμε το πρόγραμμα και την σύνδεση τον αντικειμένων του Glade με το πρόγραμμα μας.

Στο κεντρικό κενό του παραθύρου μας θέλουμε να προσθέσουμε μια Προβολή Δέντρου (TreeView). Το TreeView χρειάζεται ένα μοντέλο για να δείχνει τα δεδομένα του. Με τον ίδιο τρόπο που προσθέσαμε τα τρία ListStore στο προηγούμενο άρθρο για τα τρία combobox μας, πάμε να προσθέσουμε μια νέα Αποθήκη Λιστών (ListStore). Την ονομάζουμε filelist και προσθέτουμε τρεις στήλες τύπου gchararray με ονόματα: fname, codepage, eol. Μπορούμε τώρα να προσθέσουμε το TreeView και όταν μας ζητήσει πιο μοντέλο θέλουμε θα επιλέξουμε το filelist. Αφού προστεθεί κάτω από το scrollwindow, αλλάζουμε το όνομα του σε filelistview.


Μετά από αυτό πρέπει να ορίσουμε τι και πως θα εμφανίζει αυτό το TreeView. Πατάμε στο κουμπί Edit στην μπάρα εργαλείων, στο νέο παράθυρο που ανοίγει επιλέγουμε το tab Ιεραρχία. Εδώ θα ορίσουμε πως και τι θέλουμε να εμφανίσουμε από το μοντέλο μας. Πατάμε Προσθήκη και θα βάλει μια νέα στήλη. Αλλάζουμε τον Τίτλο σε Αρχείο και κάνουμε την Δυνατότητα αλλαγής μεγέθους Ναι.


Στην συνέχεια κάνουμε δεξί κλικ πάνω στην γραμμή και διαλέγουμε το Προσθήκη θυγατρικού Κειμένου αντικειμένου. Αυτό θα βάλει μια νέα γραμμή από κάτω με όνομα cellrendertext. Εδώ αλλάζουμε μόνο το Κείμενο και επιλέγουμε το fname από το μοντέλο μας. Ακριβώς τις ίδιες κινήσεις κάνουμε και για τις υπόλοιπες δύο στήλες που θέλουμε στο TreeView.
Τίτλος->Κωδικοποίηση, Δυνατότητα αλλαγής μεγέθους->Ναι, cellrendertext->Κείμενο->codepage.
Τίτλος->Τέλος γραμμής, Δυνατότητα αλλαγής μεγέθους->Ναι, cellrendertext->Κείμενο->eol.
Τώρα θα πρέπει να έχει την εξής μορφή η λίστα στην Ιεραρχία:



Το filelistview θα γεμίζει όταν ο χρήστης επιλέξει αρχεία προς μετατροπή. Θα πρέπει να προβλέψουμε τι δυνατότητα του χρήστη να αφαιρέσει ένα αρχείο από την λίστα. Θα μπορεί μαρκάροντας ένα αρχείο και πατώντας Delete να αφαιρείται το αρχείο από την λίστα. Χρειαζόμαστε οπότε μια μέθοδο που επιβλέπει αν πατήθηκε κουμπί. Στο tab Σήματα του filelistview στο GtkWidget και στο σήμα key-press-event δίνουμε σαν handler το filelistview-key. Σε αυτή θα αφαιρούμε την αντίστοιχη εγγραφή από το filelist. Θα έχει όμως το πρόγραμμα και έναν δικό του πίνακα με τα αρχεία, οπότε όταν αφαιρεθεί ένα από το filelist θα πρέπει να αφαιρέσουμε και από τον δικό μας. Για να γίνει αυτό χρειαζόμαστε να ελέγχουμε το σήμα row_deleted. Το επιλέγουμε έτσι από το tab Σήματα και δίνουμε σαν handler την μέθοδο filelist_row_deleted.

Το κεντρικό μας παράθυρο είναι τώρα έτοιμο. Έχει όλα τα σήματα και τα αντικείμενα που χρειαζόμαστε.



Για την επιλογή των αρχείων δημιουργούμε ένα filechooser, όπως στο προηγούμενο εκπαιδευτικό. Του δίνουμε όνομα filechooser, Τιτλος παραθύρου->Επιλέξτε αρχεία, Σχηματικό->Ναι, Καταστροφή με το μητρικό->Ναι, Επιλογή πολλαπλών->Ναι, Να επιτρέπεται η δημιουργία φακέλων->Οχι. Στο σήμα του delet-event δίνουμε σαν handler την delet_chooser (αναφερθήκαμε σ' αυτό στο προηγούμενο tutorial). Προσθέτουμε τα δύο κουμπιά, Ακύρωση και Εντάξει και τα αντίστοιχα σήματα τους στο clicked, filechooser_cancel και filechooser_ok.
Πάνω ακριβώς από τις θέσεις των κουμπιών υπάρχει μια κενή θέση. Εκεί θα δημιουργήσουμε ένα combobox με φίλτρα αρχείων για να επιλέξει ο χρήστης πια θέλει να του δείχνει προς επιλογή. Θα βάλουμε ένα Hbox 2 στοιχείων, αλλάζουμε τα Γεμισμα και Ανάπτυξη σε Όχι. Στην αριστερή θέση βάζουμε ένα label και κάνουμε την ετικέτα του “Φίλτρο επιλογής”. Αλλάζουμε και τα Γέμισμα και Ανάπτυξη σε Όχι. Πριν βάλουμε το combobox να φτιάξουμε μια νέα Αποθήκη λιστών (liststore) και να της δώσουμε όνομα filterlist. Βάζουμε μια στήλη gchararray, το όνομα δεν είναι σημαντικό, και προσθέτουμε τρεις επιλογές: Αρχεία txt (*txt), Αρχεία srt (*.srt), Όλα τα αρχεία (*.*). Τρεις επιλογές όπου μπορεί να είναι χρήσιμη η μετατροπή αρχείων. Μπορείτε εσείς να προσθαφαιρέσετε ανάλογα.
Προσθέτουμε τώρα το combobox στην δεξιά θέση, διαλέγουμε σαν μοντέλο του το filterlist, το ονομάζουμε filterbox και κάνουμε 0 (μηδέν) το Ενεργό αντικείμενο του. Στα Σήματα βάζουμε σαν handler στο changed την filterbox_changed. Στην συνέχεια πρέπει να ορίσουμε το spin του combobox, όπως και στα προηγούμενα. Πατάμε Edit στην γραμμή εργαλέιων, στο tab Ιεραρχία πατάμε Προσθήκη και αλλάζουμε σε spin των Τύπο, και τέλος διαλέγουμε για Κείμενο το πεδίο gchararray1 της στήλης μας. Θα χρειαστούμε βέβαια και τρία filefilter για να μπορέσουμε να δώσουμε τις αντίστοιχες τιμές. Βάλτε λοιπόν τρία με ονόματα txtfilter, srtfilter, allfilter.

Το filechooser έχει τώρα της εξής μορφή:












Ένα δεύτερο βοηθητικό παράθυρο που θα δημιουργήσουμε είναι το Περί... Βρίσκεται σε σχεδόν σε όλα τα προγράμματα σαν τελευταία επιλογή στο μενού Βοήθεια και εμφανίζει γενικές πληροφορίες για το πρόγραμμα και τους δημιουργούς του. Το GTK παρέχει ένα έτοιμο αντικείμενο για αυτό το σκοπό, το aboutdialog. Προσθέτουμε ένα λοιπόν ένα από τα Ανώτατα επίπεδα. Η ελληνική μετάφραση είναι Διάλογος πληροφοριών. Θα το ονομάσουμε aboutdialog. Στα πεδία που υπάρχουν στο tab Γενικά μπορούμε να συμπληρώσουμε ότι θέλουμε. Στο Όνομα προγράμματος δίνουμε Convert για να είμαστε συνεπής και από εκεί και πέρα μπορείτε να συμπληρώσετε όσα θέλετε για να δείτε πως επεκτείνετε μόνο του. Για παράδειγμα αν συμπληρώσετε Συγγραφείς θα προστεθεί αυτόματα ένα νέο κουμπί το Μνεία. Στο συγκεκριμένο έχουμε προσθέσει ένα γραφικό, σχόλια και συγγραφείς.

Για την Βοήθεια μπορούμε να δημιουργήσουμε ένα νέο παράθυρο με ένα textview αντικείμενο και να προβάλουμε πληροφορίες χρήσης του προγράμματος. Υπάρχει όμως ένα μειονέκτημα στο textview. Δεν μπορείς να διαμορφώσεις το κείμενο σου με συνδέσεις ή με έντονους ή πλάγιους χαρακτήρες. Θα έχει μια πολύ παλη μορφή χωρίς περιεχόμενα και αν η βοήθεια που χρειάζεται να γραφτεί είναι μεγάλη, θα είναι πολύ δύσχρηστο. Για αυτό προτείνουμε την δημιουργία ενός html αρχείου, όπου μπορούμε να το διαμορφώσουμε όπως θέλουμε, με ευρετήριο, ωραίο κείμενο και εικόνες, και προβολή του μέσω του εξ ορισμού browser του συστήματος. Στην python αυτό γίνεται πολύ εύκολα με τις παρακάτω γραμμές κώδικά:

import webbrowser
webbrowser.open(“help.html”)
Αυτές οι δυο γραμμές κώδικά θα ανοίξουν το αρχείο help.html στοn εξ ορισμού browser του συστήματος.

Όλα τα αντικείμενα μας είναι τώρα τα εξής:






















Μπορούμε με το παρακάτω πρόγραμμα να κάνουμε ένα έλεγχο για το πως εμφανίζονται όλα τα αντικείμενα μας πριν προχωρήσουμε στην δημιουργία του τελικού προγράμματος.
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import gtk

CODEPAGES = ['UTF-16','UTF-8','Windows-1253','ISO8859-7']
EOL = ['Windows','Unix','Mac']

class ConvertFiles(object):

    def close_window(self, widget, data=None):
        gtk.main_quit()

    def delete_chooser(self, widget, data=None):
        return True

    def aboutmenu_clicked(self, widget, data=None):
        self.aboutdialog.run()
        self.aboutdialog.hide()

    def helpmenu_clicked(self, widget, data=None):
        import webbrowser
        webbrowser.open("help.html")

    def openmenu_clicked(self, widget, data=None):
        self.filechooser.show()

    def filterbox_changed(self, widget, data=None):
        if widget.get_active() == 0:
            self.filechooser.set_filter(self.txtfilter)
        if widget.get_active() == 1:
            self.filechooser.set_filter(self.srtfilter)
        if widget.get_active() == 2:
           self.filechooser.set_filter(self.allfilter)

    def filechooser_cancel(self, widget, data=None):
        self.filechooser.hide()

    def filechooser_ok(self, widget, data=None):
        self.filechooser.hide()

    def savemenu_clicked(self, widget, data=None):
        pass

    def __init__(self):
        builder = gtk.Builder()
        builder.add_from_file("convert.glade")
        builder.connect_signals(self)
        self.window = builder.get_object("convertwindow")
        self.codefrombox = builder.get_object("codefrombox")
        self.codetobox = builder.get_object("codetobox")
        self.eolbox = builder.get_object("eolbox")
        self.codefromlist = builder.get_object("codefromlist")
        self.codetolist = builder.get_object("codetolist")
        self.eollist = builder.get_object("eollist")
        self.codefromlist.append([u"Μαντεψιά"])
        for item in CODEPAGES:
            self.codefromlist.append([item])
            self.codetolist.append([item])
        for item in EOL:
            self.eollist.append([item])
         self.codefrombox.set_active(0)
         self.codetobox.set_active(0)
         self.eolbox.set_active(0)
         self.filechooser = builder.get_object("filechooser")
         self.aboutdialog = builder.get_object("aboutdialog")
         self.txtfilter = builder.get_object("txtfilter")
         self.srtfilter = builder.get_object("srtfilter")
         self.allfilter = builder.get_object("allfilter")
         self.filterbox = builder.get_object("filterbox")
         self.txtfilter.add_pattern('*.txt')
         self.txtfilter.add_pattern('*.TXT')
         self.srtfilter.add_pattern('*.srt')
         self.srtfilter.add_pattern('*.SRT')
         self.allfilter.add_pattern('*.*')
         self.filterbox_changed(self.filterbox)
         self.statusbar=builder.get_object("statusbar")

    def main(self):
        self.window.show()
        gtk.main()

if __name__== "__main__":
    app=ConvertFiles()
    app.main()

Αυτό είναι σχεδόν ολοκληρωμένο το πρόγραμμα μας, δεν έχουν αναπτυχθεί μόνο οι openmenu_clicked και savemenu_clicked μέθοδοι. Λείπουν ακόμα οι filelistview_key και filelist_row_deleted. Αυτά θα τα δούμε στο επόμενο άρθρο. Μέχρι εδώ δεν έχει τίποτα καινούργιο εκτός από την filterbox_changed όπου ελέγχει πιο είναι το ενεργό αντικείμενο του combobox και αναλόγως επιλέγει το αντίστοιχο φίλτρο για το filechooser. Την καλούμε και από την __init__ αφού έχουμε γεμίσει τα φίλτρα μας για να ενεργοποιηθεί το φίλτρο στον filechooser.
H aboutmenu_clicked δείχνει το αντίστοιχο παράθυρο και η helpmenu_clicked ανοίγει τον browser με το αρχείο help.html.
Στο επόμενο άρθρο θα ασχοληθούμε με την περιγραφή και ολοκλήρωση του προγράμματος.
Τα αρχεία αυτού του άρθρου βρίσκονται σε μορφή .tar.gz εδώ.

Monday, June 6, 2011

Σύνθετα αντικείμενα στο Glade (α' μέρος)


Σε αυτή την σειρά άρθρων θα δείξουμε ορισμένα νέα αντικείμενα του GTK και πως μπορούμε να τα δημιουργήσουμε και να τα παραμετροποιήσουμε μέσα από το Glade. Για όσους δεν έχουν εμπειρία με το Glade καλό είναι να ξεκινήσουν από την πρώτη σειρά άρθρων εδώ.
Θα φτιάξουμε το περιβάλλον χρήστη για ένα πρόγραμμα όπου θα μετατρέπει όλα τα αρχεία κειμένου που θα επιλέξει ο χρήστης σε κωδικοποίηση της αρεσκείας του. Θα μπορεί να μετατρέψει από και προς τα εξής συστήματα: utf-8, utf-16, windows-1253 και iso8859-7. Θα του δίνεται επίσης η δυνατότητα να αλλάξει το τέλος γραμμής των αρχείων μεταξύ των τριών που υπάρχουν σήμερα: \n,\r, \r\n.

Η δυνατότητα αυτή υπάρχει ενσωματωμένη τώρα πια σε όλους τους κειμενογράφους τόσο στα Linux και OSX όσο και στα Windows, αλλά το πρόγραμμα μας θα μπορεί να μετατρέψει πολλά αρχεία με διάφορες κωδικοποιήσεις με μια επιλογή μόνο (bulk ή αλλιώς “σωρηδόν”), και εκτός των άλλων χρησιμεύει για εκπαιδευτικούς σκοπούς τόσο στα Glade και GTK όσο και στην Python.

Ξεκινάμε λοιπόν με το Glade και δημιουργούμε το κύριο παράθυρο. Όνομα: convertwindow, τίτλος: Convert text files, αν θέλουμε Εικονίδιο δείνουμε το όνομα του (εμείς έχουμε το process.gif). Στο σήμα destroy του παραθύρου δίνουμε για handler close_window. Δώστε και 550 στην Αίτηση πλάτους. Στην συνέχεια προσθέτουμε ένα Vbox 3 γραμμών. Για την πρώτη γραμμή διαλέγουμε από τα Πλαίσια (containers) την “Γραμμή μενού” και για την τρίτη από τα Έλεγχος και Εμφάνιση την “Γραμμή κατάστασης”. Θα ζητήσει πόσα αντικείμενα θέλουμε στην γραμμή κατάστασης, διαλέγουμε ένα. Την ονομάζουμε statusbar και δίνουμε διάκενο 5.


Στο μενού έχει βάλει από μόνο του ορισμένες έτοιμες επιλογές για όλες τις χρήσεις. Θα το αλλάξουμε λίγο, δεν τις χρειαζόμαστε όλες. Με το menubar1 επιλεγμένο πατάμε στο κουμπί Edit της μπάρας εργαλείων. Αυτό ανοίγει ένα νέο παράθυρο όπου μπορούμε να προσθαφαιρέσουμε επιλογές στο μενού. Επιλέγουμε το tab Ιεραρχία για να δούμε την δομή του μενού. Κάνοντας δεξί κλικ σε όποια επιλογή θέλουμε μπορούμε να προσθέσουμε μία νέα, θυγατρική ή όχι. Αντίστοιχα με το κουμπί Αφαίρεση αφαιρούμε όποια θέλουμε. Μπορούμε ακόμα να τις μετακινήσουμε με το ποντίκι και να τους αλλάξουμε σειρά και επίπεδο. Διαλέγουμε την _Επεξεργασία και πατάμε Αφαίρεση, το ίδιο και για την Προβολή. Δεν χρειαζόμαστε επίσης τα imagemenuitem1 το οποίο έχει εικόνα του (gtk-new), το imagemenuitem4 (gtk-save-as). Προσθέτουμε ένα διαχωριστικό πάνω από το imagemenuitem10 και ένα νέο θυγατρικό στοιχείο εικόνας πάνω από το διαχωριστικό. Το ονομάζουμε helpmenu, διαλέγουμε σαν έτοιμο στοιχείο stock το “Βοήθεια” (gtk-help) και στο σήμα του activate δίνουμε handler: helpmenu_clicked. Το imagemenuitem10 το ονομάζουμε aboutmenu και στο activate βάζουμε aboutmenu_clicked. Με το ίδιο τρόπο έχουμε: imagemenuitem5 σε exitmenu και close_window (το ίδιο με του παραθύρου), imagemenuitem2 σε openmenu και openmenu_clicked, imagemenuitem3 σε savemenu και savemenu_clicked.
 

Τώρα το μενού έχει αυτή την διάταξη:






Την μεσαία θέση την χωρίζουμε σε δύο τμήματα με ένα Vbox. Στο πάνω τμήμα βάζουμε ένα scrollwindow και στην κάτω ένα frame. Αλλάζουμε και στα δύο το χαρακτηριστικό τους Σκίαση σε Μέσα. Σβήνουμε τα alignment1 και label1 από το frame. Αλλάζουμε τα Ανάπτυξη και Γέμισμα του frame στο Packing tab σε Όχι, δίνουμε 5 σαν τιμή στο αριθμητικό Γέμισμα.
Στην συνέχεια προσθέτουμε έναν Πίνακα τριών γραμμών και 2 στηλών στο frame.
Ο πίνακας έχει αντικαταστήσει το οριζόντιο box στις καινούργιες εκδόσεις του Glade και είναι αρκετά πιο χρηστικός από το να προσθέτουμε συνέχεια μία οριζόντια και μία κάθετα box.
Στη πρώτη στήλη του πίνακα βάζουμε τρία label. Και στα τρία αλλάζουμε την Στοίχιση Χ σε 0 (0 σημαίνει αριστερή στοίχιση, 0,50 είναι κέντρο και 1,00 δεξιά στοίχιση) στο Packing αλλάζουμε στις Οριζόντιες επιλογές το Επέκταση σε όχι (unchecked) και δίνουμε στο Οριζόντιο γέμισμα τιμή 5. Αλλάζουμε τις ετικέτες των label σε, από πάνω προς τα κάτω: Κωδικοποίηση από, Κωδικοποίηση σε, Τέλος γραμμής.

Στην δεύτερη στήλη προσθέτουμε τρία combobox. Δίνουμε και στα τρία την τιμή 5 για το Οριζόντιο γέμισμα. Ονομάζουμε το πρώτο codefrombox, το δεύτερο codetobox και το τρίτο eolbox. Για να μπορέσουμε να δώσουμε τιμές στα combobox πρέπει το καθένα να έχει ένα Μοντέλο. Διαλέγουμε από αριστερά από το τμήμα Μοντέλο δέντρου την Αποθήκη λιστών(listsource). Προσθέτουμε τρεις τέτοιες λίστες για τα τρία combo μας και τις ονομάζουμε αντίστοιχα codefromlist, codetolist, eollist. Στην κάθε λίστα πρέπει να ορίσουμε τι και πόσα δεδομένα θα εμφανίζει. Επειδή το ίδιο αντικείμενο χρησιμοποιείται και για την προβολή δέντρου (treeview) το οποίο εμφανίζει πολλές στήλες, μας δίνει την δυνατότητα να προσθέσουμε πολλές στήλες. Εμείς εδώ θέλουμε μόνο μία, να είναι gchararray (δηλαδή string) και θα δώσουμε όνομα σχετικό με την χρήση, όπως codefrom στο codefromlist, codeto στο codetolist και eol στο eolbox. Προχωρόντας πιο κάτω στο tab Γενικά συναντάμε την δυνατότητα να γεμίσουμε με τιμές τις λίστες. Εμείς εδώ θα τις γεμίσουμε μέσα από το πρόγραμμα. Τώρα, σε καθένα από τα combo επιλέγουμε στο tab Γενικά το αντίστοιχο Μοντέλο του.

Στην συνέχεια, πατώντας το κουμπί Edit στην γραμμή εργαλείων εμφανίζει ένα καινούργιο παράθυρο επεξεργασίας του τρέχοντος combo. Στο tab Ιεραρχία θα δώσουμε τα χαρακτηριστικά κάθε στήλης της λίστας. Το βασικότερο από όλα είναι το τι θέλουμε να εμφανίζει για αυτή την στήλη. Μπορεί κάθε στήλη της λίστας να εμφανίζει διαφορετικό αντικείμενο, άλλη να εμφανίζει text, άλλη γραφικό, άλλη combobox. Εμείς εδώ έχουμε combo και θέλουμε να μπορεί ο χρήστης να επιλέγει από τις διάφορες επιλογές. Αυτή η διαδικασία ονομάζεται spin για το GTK. Έτσι πατάμε Προσθήκη και αμέσως βάζει μια γραμμή cellrendertext1, και επιλέγουμε Spin στον Τύπο του Σχεδιαστή κελιών. Στην γραμμή Κείμενο επιλέγουμε το πεδίο που θέλουμε να εμφανίζεται, εδώ έχουμε μόνο ένα πεδίο, επιλέγουμε αυτό. Αυτή η διαδικασία επαναλαμβάνεται και για τα τρία combo που έχουμε.

Το παράθυρο μας έχει τώρα την εξής μορφή:




Το σώνουμε σαν convert.glade και πάμε να φτιάξουμε ένα προγραμματάκι σε python για να τεστάρουμε όσα έχουμε κάνει μέχρι τώρα.


import gtk

CODEPAGES = ['UTF-16','UTF-8','Windows-1253','ISO8859-7']
EOL = ['Windows','Unix','Mac']

class ConvertFiles(object):

    def close_window(self, widget, data=None):
        gtk.main_quit()

    def __init__(self):

        builder = gtk.Builder()
        builder.add_from_file("convert.glade")
        builder.connect_signals(self)
        self.window = builder.get_object("convertwindow")
        self.codefrombox = builder.get_object("codefrombox")
        self.codetobox = builder.get_object("codetobox")
        self.eolbox = builder.get_object("eolbox")
        self.codefromlist=builder.get_object("codefromlist")
        self.codetolist = builder.get_object("codetolist")
        self.eollist = builder.get_object("eollist")
        self.codefromlist.append([u"Μαντεψιά"])
        for item in CODEPAGES:
            self.codefromlist.append([item])
            self.codetolist.append([item])
        for item in EOL:
            self.eollist.append([item])
        self.codefrombox.set_active(0)
        self.codetobox.set_active(0)
        self.eolbox.set_active(0)

    def main(self):
        self.window.show()
        gtk.main()

if __name__== "__main__":
    app=ConvertFiles()
    app.main()

Φορτώνουμε το αρχείο Glade με τον builder και ορίζουμε τις δικές μας μεταβλητές για τα αντικείμενα μας. Μετά, με append σε κάθε λίστα την γεμίζουμε με τους πίνακες μας. Στην λίστα codefromlist βάζουμε μία παραπάνω επιλογή την “Μαντεψιά”. Το πρόγραμμα, όταν επιλέξουμε αρχεία προς μετατροπή, θα τα ανοίγει και θα προσπαθεί να βρει μόνο του σε τι κωδικοποίηση είναι. Αυτή η “μαντεψιά” δεν είναι πάντοτε σωστή, για λόγους που θα αναφέρουμε αργότερα, οπότε δίνουμε την δυνατότητα στον χρήστη να επιλέξει αυτός την κωδικοποίηση “από”.

Μετά το γέμισμα των λιστών κάνουμε ενεργό το πρώτο στοιχείο κάθε combo (πρώτο είναι το στοιχείο 0) για να μην είναι άδεια τελείως όταν ξεκινάει το πρόγραμμα. Θα παρατηρήσετε ότι στην append το στοιχείο προς πρόσθεση είναι μέσα σε αγκύλες [], αυτό γιατί η append του liststore περιμένει σαν όρισμα πίνακα ή tuple, αλλιώς βαράει λάθος κατά το τρέξιμο.

Αν όλα έχουν πάει καλά θα δείτε ένα τέτοιο παράθυρο να εμφανίζεται (αγνοήστε τα λάθη που εμφανίζει για τις μεθόδους που δεν έχουμε ορίσει ακόμα):



Το μενού μας είναι λειτουργικό, ανοίγουν δηλαδή τα υπομενού, βέβαια μόνο η επιλογή Έξοδος κάνει κάτι. Τα combo μας ανοίγουν και ο χρήστης επιλέγει. Κατά την αλλαγή του μεγέθους του παραθύρου πρέπει όλα να παραμένουν στο ίδιο ύψος, εκτός από τα combo που μεγαλώνουν μόνο κατά πλάτος και το μεσαία κενό μας το οποίο μεγαλώνει και στις δύο διαστάσεις.

Στο επόμενο άρθρο θα βάλουμε στο κενό ένα treeview όπου θα παρουσιάζονται τα αρχεία που έχει επιλέξει ο χρήστης, το όνομά τους, η κωδικοποίηση τους και το τέλος γραμμής τους.
Μπορείτε να κατεβάσετε αυτό το άρθρο (pdf), τις συλλήψεις οθόνης και τα αρχεία python και Glade από εδώ.

Sunday, May 29, 2011

Γνωριμία με το Glade (γ' μέρος)


Στα δύο προηγούμενα άρθρα είδαμε πως μπορούμε να φτιάξουμε το γραφικό περιβάλλον του προγράμματος μας χρησιμοποιώντας το Glade. Σε αυτό θα το παρουσιάσουμε και θα δούμε πως γίνεται η διασύνδεση μεταξύ τους. Η περιγραφή θα γίνει τμηματικά, ανά μέθοδο, για να βοηθηθούν και όσοι δεν έχουν μεγάλη εμπειρία με την python.


Στην αρχή ενός αρχείου python καλό είναι να συμπεριλαμβάνουμε τα εξής:


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


Η πρώτη γραμμή λέει στο shell του linux με τι πρόγραμμα θα προσπαθήσει να εκτελέσει αυτό το αρχείο, αν έχει δικαιώματα εκτέλεσης.
Η δεύτερη είναι μια οδηγία για τον python interpreter ότι το αρχείο είναι κωδικοποιημένο σε utf-8. Αυτό το χρειαζόμαστε αν έχουμε γράψει ελληνικά στο κώδικά μας (όπως σε κάποιο string), αλλιώς μας βγάζει μήνυμα λάθους.


Στην συνέχεια τα καθιερωμένα import. Εδώ θα χρειαστούμε τα:


import pygtk 
pygtk.require('2.0') 
import gtk 
from datetime import datetime


Φορτώνουμε pygtk και gtk, οι βασικές μας βιβλιοθήκες και datetime για να έχουμε πρόσβαση στην ώρα και ημερομηνία του συστήματος. 


class BackgroundXML(object):     


    DIALOG_TYPE_ERROR = 0
    DIALOG_TYPE_INFO = 1 


    CHOOSER_CANCEL = 0 
    CHOOSER_OK = 1


Η δήλωση της κύριας κλάσης μας και μερικές σταθερές που θα χρησιμοποιήσουμε παρακάτω.


    def __init__(self): 
        builder = gtk.Builder()
        builder.add_from_file("backgroundxml.glade")
        builder.connect_signals(self) 
        self.backgroundxml = builder.get_object("backgroundxml") 
        self.picchooser = builder.get_object("picchooser") 
        self.picbuffer = builder.get_object("picbuffer") 
        self.xmlchooser = builder.get_object("xmlchooser") 
        self.xmlfile = builder.get_object("xmlfile") 
        self.duration = builder.get_object("duration") 
        self.change = builder.get_object("change") 
        self.xmlfilter = builder.get_object("xmlfilter") 
        self.xmlfilter.add_pattern('*.xml') 
        self.picfilter = builder.get_object("picfilter") 
        self.picfilter.add_pattern('*.jpg') 
        self.picfilter.add_pattern('*.png') 
        self.picfilter.add_pattern('*.JPG') 
        self.picfilter.add_pattern('*.PNG')


Η μέθοδος __init__ η οποία φτιάχνει το κυρίως αντικείμενο μας από την κύρια κλάση μας.
Ορίζουμε από ποιο αρχείο θα βρει ο builder τις οδηγίες κατασκευής των παραθύρων. Στην συνέχεια συνδέουμε τα σήματα με τις μεθόδους μας. Ορίζουμε μεταβλητές για το κυρίως παράθυρο και για τα παράθυρα επιλογής αρχείων, μεταβλητές για το buffer και τα πεδία εισόδου και στο τέλος ορίζουμε τα δύο φίλτρα αρχείων μας και ορίζουμε σε καθένα από αυτά τις τιμές που θέλουμε να φιλτράρουν.


    def close_window(self, widget, data=None): 
        gtk.main_quit()


    def delete_chooser(self, widget, data=None): 
        return True  


Το GTK όταν καλεί μία μέθοδο ως αποτέλεσμα ενός σήματος του παραθύρου στέλνει και ένα ή δύο ορίσματα. Το πρώτο είναι το αντικείμενο (widget) από το οποίο προήλθε το σήμα και σε ορισμένες περιπτώσεις στέλνει και ορισμένα στοιχεία σχετικά με το αντικείμενο (data). Σε ένα drop-down για παράδειγμα όταν ο χρήστης επιλέξει μια τιμή από αυτό στέλνει το drop-down αντικείμενο και τον αριθμό της επιλογής ξεκινώντας από το 0 για την πρώτη κατά σειρά επιλογή του drop-down. Γι' αυτό και όλες οι μέθοδοί μας που έχουν να κάνουν με σήματα ορίζονται με αυτό τον τρόπο:
def onoma_hanlder(self, widget, data=None):
Η close_window παραπάνω είναι υπεύθυνη για τον σωστό κλείσιμο του προγράμματος και η delete_chooser αποτρέπει την καταστροφή των παραθύρων επιλογής αρχείων.


    def xmlbutton_click(self,widget,data=None):
        response=self.xmlchooser.run()
        if response == self.CHOOSER_OK: 
            self.xmlfile.set_text(self.xmlchooser.get_filename())
        self.xmlchooser.hide()


Η xmlbutton_click καλείται όταν ο χρήστης πατήσει το αντίστοιχο κουμπί επιλογής αρχείου XML. Το παράθυρο επιλογής εμφανίζεται καλώντας την μέθοδο run() του διαλόγου (το gtk.Filechooser αντικείμενο κληρονομεί από το gtk.Dialog). Ο έλεγχος πηγαίνει στο καινούργιο παράθυρο και επιστρέφει στο κυρίως όταν καταστραφεί ή καλέσει την μέθοδο response() η οποία επιστρέφει και τιμή. CHOOSER_OK είναι μία από τις δύο σταθερές μας που έχουμε ορίσει στην αρχή. Την επιστρέφει η responce όταν ο χρήστης πατήσει “Ένταξει”, αν διαλέξει “Άκυρο” επιστρέφει CHOOSER_CANCEL.  Αν είναι ΟΚ βάζουμε στο αντίστοιχο πεδίο (xmlfile) αυτό που έχει διαλέξει. Αν έχει πατήσει το “Άκυρο” δεν κάνουμε τίποτα, και στις δύο περιπτώσεις κρύβουμε το παράθυρο. Μετά από αυτό ο έλεγχος έχει επιστρέψει στο κυρίως παράθυρο. 


    def picbutton_click(self,widget,data=None): 
        response = self.picchooser.run() 
        if response == self.CHOOSER_OK:
            buffer1=self.picbuffer.get_text(self.picbuffer.get_start_iter(), self.picbuffer.get_end_iter())
            for fn in self.picchooser.get_filenames(): 
                buffer1=buffer1+fn+'\n' 
            self.picbuffer.set_text(buffer1)
        self.picchooser.hide()


H picbutton_click είναι ίδια με την προηγούμενη. Η μόνη της διαφορά είναι το ότι έχουμε να γεμίσουμε ένα buffer με πολλές γραμμές και όχι μόνο με μία τιμή. Γι' αυτό και βάζουμε στην μεταβλητή buffer1 ότι έχει η picbuffer μέχρι εκείνη την στιγμή και μετά προσθέτουμε κάθε αρχείο που έχει διαλέξει ο χρήστης με ένα “\n” στο τέλος. Όταν ολοκληρωθούν όλα τα αρχεία βάζουμε την buffer1 πίσω στην picbuffer προς εμφάνιση.


    def clearbutton_click(self,widget,data=None):
        self.picbuffer.set_text(u'')  


Η clearbutton_click καλείται όταν ο χρήστης πατήσει “Εκκαθάριση”. Διαγράφει ότι έχει η picbuffer βάζοντας ένα άδειο string.


    def picchooser_cancel_clicked(self, widget, data=None):
        self.picchooser.response(self.CHOOSER_CANCEL)


    def picchooser_ok_clicked(self, widget, data=None):
        self.picchooser.response(self.CHOOSER_OK)  


    def xmlchooser_ok_clicked(self,widget,data=None):
        self.xmlchooser.response(self.CHOOSER_OK)


    def xmlchooser_cancel_clicked(self,widget,date=None): 
        self.xmlchooser.response(self.CHOOSER_CANCEL)  


Αυτές οι τέσσερις μέθοδοι είναι ταυτόσημες. Είναι υπεύθυνες για την επιστροφή της σωστής τιμής από τα δύο παράθυρα επιλογής αρχείων. Στο “Εντάξει” επιστρέφουν CHOOSER_OK και στο “Ακύρωση” CHOOSER_CANCEL. Χρησιμοποιούμε την response() μέθοδο για την επιστροφή η οποία κλείνει και το παράθυρο. Μπορούμε να τις αντικαταστήσουμε με μία η οποία να κάνει ακριβώς το ίδιο και για τα δύο παράθυρα;


    def dialog(self, text, dialogtype): 
        if dialogtype == self.DIALOG_TYPE_ERROR: 
            dialog=gtk.MessageDialog(parent=None,flags=gtk.DIALOG_MODAL & \
             gtk.DIALOG_DESTROY_WITH_PARENT, \
             type=gtk.MESSAGE_ERROR, buttons=gtk.BUTTONS_OK)
            dialog.set_title(u"Σφάλμα") 
            dialog.format_secondary_text(u"Διορθώστε τα παραπάνω προβλήματα.")
        elif dialogtype == self.DIALOG_TYPE_INFO:
            dialog=gtk.MessageDialog(parent=None,flags=gtk.DIALOG_MODAL & \
             gtk.DIALOG_DESTROY_WITH_PARENT,\
             type=gtk.MESSAGE_INFO, buttons=gtk.BUTTONS_OK)
            dialog.set_title(u"Ενημέρωση")
        dialog.set_markup(u"<b>"+text+"</b>")  
        response=dialog.run()
        if response == gtk.RESPONSE_DELETE_EVENT or response == gtk.RESPONSE_OK:
            dialog.destroy()  


Η dialog είναι μια δική μας βοηθητική μέθοδος η οποία δημιουργεί ένα παράθυρο διαλόγου. Παίρνει δύο ορίσματα: text και dialogtype. Το text είναι το κείμενο που θα εμφανίσει και dialogtype είναι το είδος του διαλόγου. Αν θέλουμε να εμφανίσουμε μήνυμα λάθους την καλούμε με DIALOG_TYPE_ERROR, αν θέλουμε να εμφανίσουμε κάποια άλλη πληροφορία την καλούμε με DIALOG_TYPE_INFO. Ο διάλογος δημιουργείται με την gtk.MessageDIalog η οποία παίρνει σαν ορίσματα τα parent, flags,type, buttons. Η parent χρησιμοποιείται για να δηλώσουμε κάποιο πατρικό παράθυρο, στην flags δηλώνουμε αν θέλουμε να είναι modal το παράθυρο μας και αν θα καταστραφεί μαζί με το πατρικό. (Modal είναι όταν το παράθυρο έχει αποκλειστικά τον έλεγχο και ο χρήστης δεν μπορεί να γυρίσει στο πατρικό αν πρώτα δεν κλείσει το modal παράθυρο).Η type παίρνει το είδος του διαλόγου και η buttons ποια κουμπιά θα εμφανιστούν με τον διάλογο. Όταν θέλουμε να εμφανίσουμε λάθος δημιουργούμε ένα gtk.MESSAGE_ERROR διάλογο με ένα μόνο κουμπί, το “Εντάξει”. Για πληροφορία το δημιουργούμε σαν gtl.MESSAGE_INFO με ένα μόνο κουμπί πάλι. Το μήνυμα το έχουμε συμπεριλάβει μέσα σε <b></b> για να φαίνεται έντονο, και συμπληρωματικά αν είναι μήνυμα λάθους εμφανίζουμε και μια προτροπή, secondary_text. Εμφανίζουμε τον διάλογο με run() και περιμένουμε την απάντηση του χρήστη. Η απάντηση μπορεί να είναι είτε OK είτε DELETE (αν έχει πατήσει το X). Και στις δύο περιπτώσεις καταστρέφουμε το παράθυρο.


    def clicked_ok(self, widget, data=None):
        picfiles = self.picbuffer.get_text(self.picbuffer.get_start_iter(), self.picbuffer.get_end_iter())
        filelist=picfiles.split("\n") 
        filelist.remove('') 
        errortxt='' 
        now = datetime.now() 
        if not self.xmlfile.get_text().endswith('.xml'): 
            errortxt+=u'Το όνομα του XML αρχείου πρέπει να τελειώνει σε .xml\n'
        if not filelist :
            errortxt+=u'Πρέπει να διαλέξετε φωτογραφίες\n' 
        if not self.duration.get_text().isdigit() :
            errortxt+=u'Πρέπει να δώσετε την διάρκεια εμφάνισης (αριθμητικά)\n' 
        if not self.change.get_text().isdigit() :
            errortxt+=u'Πρέπει να δώσετε την διάρκεια εναλλαγής (αριθμητικά)\n'
        if errortxt:
            self.dialog(errortxt, self.DIALOG_TYPE_ERROR)
        else:
            with open(self.xmlfile.get_text(),'w') as f:
                try:
                    f.write("<background>\n") 
                    f.write("  <starttime>\n")
                    f.write("    <year>"+str(now.year)+"</year>\n")
                    f.write("    <month>"+str(now.month)+"</month>\n")
                    f.write("    <day>"+str(now.day)+"</day>\n")
                    f.write("    <hour>"+str(now.hour)+"</hour>\n")
                    f.write("    <minute>"+str(now.minute)+"</minute>\n")
                    f.write("    <second>"+str(now.second)+"</second>\n")
                    f.write("  </starttime>\n")
                    first=filelist[0]
                    for ln in range(len(filelist)):
                        f.write("  <static>\n")
                        f.write("    <duration>"+self.duration.get_text()+"</duration>\n")
                        f.write("    <file>"+filelist[ln]+"</file>\n")
                        f.write("  </static>\n")
                        f.write("  <transition>\n")
                        f.write("    <duration>"+self.change.get_text()+"</duration>\n")
                        f.write("    <from>"+filelist[ln]+"</from>\n")
                        if ln == len(filelist)-1:
                            f.write("    <to>"+first+"</to>\n")
                        else:
                            f.write("    <to>"+filelist[ln+1]+"</to>\n")
                        f.write("  </transition>\n")
                        f.write("</background>\n")
                    self.dialog(u"Επιτυχής δημιουργία αρχείου XML",self.DIALOG_TYPE_INFO) 
                except:
                    self.dialog(u"Πρόβλημα στην δημιουργία του αρχείου XML",self.DIALOG_TYPE_ERROR) 


Η clicked_ok είναι μέθοδος όπου δημιουργείται το αρχείο XML. Στην αρχή της φτιάχνουμε έναν πίνακα με τα περιεχόμενα του buffer και βάζουμε σε μια μεταβλητή την ώρα και ημερομηνία του συστήματος. Στην συνέχεια διενεργούμε ελέγχους για να επαληθεύσουμε τις εισόδους του χρήστη. Πρέπει να έχει δώσει αρχείο XML και αυτό να τελειώνει σε .xml, πρέπει να έχει διαλέξει εικόνες προς εμφάνιση και να έχει δώσει αριθμητικές τιμές στα πεδία με τους χρόνους. Για καθένα από αυτά, σε περίπτωση λάθους, προσθέτουμε και το αντίστοιχο λεκτικό σε μια μεταβλητή όπου και την εμφανίζουμε, αν έχει τιμή, με την dialog μέθοδό μας. Αν όλα είναι καλά ανοίγουμε το αρχείο XML προς εγγραφή και αρχίζουμε να γράφουμε σε αυτό τα δεδομένα όπως έχουμε περιγράψει εδώ. Με την επιτυχή δημιουργία του αρχείου εμφανίζουμε πληροφοριακό διάλογο ότι όλα τελείωσαν ομαλά, αλλιώς ένα μήνυμα λάθους.


    def main(self): 
        self.backgroundxml.show()
        gtk.main()
        
if __name__ == "__main__":
    app = BackgroundXML() 
    app.main() 


Το πρόγραμμα τελειώνει με τον κλασικό τρόπο ενός pygtk προγράμματος, την main μέθοδο και τον τρόπο που την καλούμε στην έναρξη.


Σώζουμε το αρχείο σαν backgroundxml.py στον ίδιο φάκελο με το glade αρχείο και το τρέχουμε με python backgroundxml.py. 



Εδώ ολοκληρώνεται μια πρώτη γνωριμία με το Glade, το GTK περιβάλλον και την python. Με λίγες γραμμές κώδικα σε python έχουμε έτοιμο το πρόγραμμα μας για την δημιουργία εναλλασσόμενων εικόνων στο φόντο του Gnome. Από εδώ και περά μπορεί ο καθένας να πειραματιστεί τόσο με το Glade και το γραφικό περιβάλλον όσο και με την python και την λογική του προγράμματος. Για παράδειγμα είναι χρήσιμο να προσθέσουμε έναν έλεγχο αν το αρχείο XML που έγραψε ο χρήστης υπάρχει ήδη ή όχι, να εμφανίζει ένα μήνυμα διαλόγου “Το αρχείο υπάρχει. Να το αντικαταστήσω;” και ανάλογα με την επιλογή του χρήστη να κάνει την αντικατάσταση ή όχι.
Σε επόμενο άρθρο θα ασχοληθούμε με την κατασκευή ενός προγράμματος σε gtk για την μαζική μετατροπή αρχείων text μεταξύ των διαφόρων τύπων κωδικοποίησης που υπάρχουν σήμερα, windows-1253, utf-8 κλπ

Το τελικό πρόγραμμα καθώς και αυτό το άρθρο σε pdf μορφή θα τα βρείτε εδώ.
Ορισμένα χρήσιμα links για περαιτέρω εξερεύνηση:
Glade
GTK
PyGTK
Python