Open source repositories at: github and bitbucket. Current project: erp4all
Showing posts with label python. Show all posts
Showing posts with label python. Show all posts

Monday, September 3, 2012

pCMS - Ένα πολύ μικρό CMS σε django

pCMS - Ένα πολύ μικρό CMS σε django

Η django  συνοδεύεται από ένα πολύ χρήσιμο module το django.contrib.flatpages. Αυτό είναι υπεύθυνο γιατί την προβολή περιεχομένου που είναι αποθηκευμένο στην βάση. Είναι πολλές φορές που σελίδες δεν χρειάζεται να είναι δυναμικές γιατί το περιεχόμενο τους δεν αλλάζει όπως για παράδειγμα η άδεια χρήσης ή οι όροι και προϋποθέσεις ή στατικές πληροφορίες επιχείρησης (διεύθυνση, τηλέφωνο κλπ). Σε αυτές τις περιπτώσεις αυτό το module είναι πολύ χρήσιμο και εύκολο στην χρήση του.

Μπορούμε όμως να το πειράξουμε λίγο, να προσθέσουμε και μερικά ακόμα χαρακτηριστικά και να το κάνουμε ένα πολύ μικρό σύστημα διαχείρισης περιεχομένου (CMS). Θα προσθέσουμε τις εξής δυνατότητες σε κάθε σελίδα:
  • Πεδίο αν είναι δημοσιευμένη ή όχι
  • Πεδία για τα meta tags: description, keywords
  • Πεδία για προσθήκη javascript και css
Χρησιμοποιώντας και ένα δεύτερο module της django, το django.contrib.comments μπορούμε να προσθέσουμε σχόλια σε όποια/ες σελίδα/ες θέλουμε.

Θέλουμε όμως να υπάρχει και η δυνατότητα να εμφανίζουμε links με τις διευθύνσεις των σελίδων μας αυτόματα. Να μην αλλάζουμε το uls.py κάθε φορά που βάζουμε μια καινούργια ή αφαιρούμε μια παλιά. Θα ήταν ωραίο να εμφανίζουμε μενού με τις σελίδες που αναφέρονται σε ένα θέμα ομαδοποιημένες. Οι σελίδες για τα προϊόντα κάτω από ένα μενού, οι σελίδες των υπηρεσιών κάτω από άλλο μενού.

Προσθέτουμε και ένα απλό σύστημα αναζήτησης στις σελίδες μας και έχουμε το pico CMS. 

Ακολουθεί μια σύντομη περιγραφή των αλλαγών που χρειάστηκαν να γίνουν στο αρχικό module ώστε να εμπλουτιστεί με τα νέα στοιχεία.

Ξεκινώντας από τα μοντέλα θα δηλώσουμε ένα δικό μας με όνομα Flatpage και το οποίο θα κληρονομεί από το FlatPage της django.

from django.contrib.flatpages.models import FlatPage

class Flatpage(FlatPage):
    published = models.BooleanField(verbose_name=u"Published", default=True, blank=True)
    scripts = models.TextField(verbose_name=u"Scripts", blank=True)
    styles = models.TextField(verbose_name=u"Styles", blank=True)
    keywords = models.TextField(verbose_name=u"Keywords", blank=True)
    description = models.TextField(verbose_name=u"Description", blank=True)
Έχουμε έτοιμες τις επιπλέον πληροφορίες που θέλουμε για κάθε σελίδα. Θέλουμε όμως να εμφανίζονται μόνο αυτές που έχουν published==True. Το views.py όμως που υπάρχει με το django.contrib.flatpages τις δείχνει όλες. Το αντιγράφουμε και προσθέτουμε ένα τον όρο published=True στο get_object_or_404:


def flatpage(request, url):
    if not url.startswith('/'):
        url = '/' + url
    try:
        f = get_object_or_404(Flatpage, url__exact=url, sites__id__exact=settings.SITE_ID, published=True)
    except Http404:
        if not url.endswith('/') and settings.APPEND_SLASH:
            url += '/'
            f = get_object_or_404(Flatpage, url__exact=url, sites__id__exact=settings.SITE_ID, published=True)
            return HttpResponsePermanentRedirect('%s/' % request.path)
        else:
            raise
    return render_flatpage(request, f)

Για να ενεργοποιηθεί το δικό μας view και όχι του django.contrib.flatpages δεν χρησιμοποιούμε το δικό του middleware αλλά ένα δικό μας, αντιγραφή του προηγούμενου, αλλά αντί να κάνει import το django.contrib.flatpages.views, να κάνει το δικό μας.

Υπάρχει και template tag που έρχεται με το αρχικό module αλλά κι αυτό χρειάζεται να μετατραπεί ώστε να δείχνει μόνο τις δημοσιευμένες σελίδες.

Αυτές είναι και οι αλλαγές που χρειάζονται για εμπλουτίσουμε λίγο τις στατικές σελίδες.

Η δημιουργία των μενού γίνεται στο admin περιβάλλον.  Δίνοντας τον τίτλο του μενού και το url του. Το μενού θα φτιαχτεί σαν dropdown και θα περιέχει κάθε σελίδα που το url της αρχίζει με το url του μενού. Έτσι για παράδειγμα ένα μενού με url /services/ θα περιέχει όλες τις σελίδες που ξεκινάνε με /services/ όπως: /services/consulting/, /services/developing/.
Δεν υπάρχει όμως η δυνατότητα να έχουμε υπό μενού.

Τα μενού όπως και οι σελίδες έχουν την δυνατότητα να εμφανίζονται μόνο σε χρήστες που έχουν κάνει login. 

Προσθέσαμε και μια απλοποιημένη έκδοση του django.contrib.comments, όπου για να αποφύγουμε θέματα ασφάλειας και διαχείρισης, μόνο οι χρήστες που έχουν κάνει login μπορούν να σχολιάσουν σε μια σελίδα. Ο καθένας μπορεί να σβήσει τα δικά του σχόλια όποτε θέλει και ο admin μπορεί να τα σβήσει όλα.

Admin περιβάλλον

Καλά όλα αυτά αλλά να γράψεις html με το χέρι για να δημιουργήσεις σελίδες είναι λίγο χρονοβόρο. Υπάρχουν όμως έτοιμοι html editors που μπορούμε να χρησιμοποιήσουμε. Εδώ έχουμε χρησιμοποιήσει τον tinymce. Στον static φάκελο δημιουργούμε έναν νέο με το όνομα admin. Εκεί αποσυμπιέζουμε το tinymce. Δημιουργούμε ένα αρχείο textareas.js το οποίο θα είναι υπεύθυνο για την εκκίνηση του editor. Στον ορισμό του admin interface για την εφαρμογή μας (admin.py) ορίζουμε τα εξής:
    class Media:
        js = ('admin/tiny_mce.js','admin/textareas.js')
Έχουμε έτσι έναν rich text editor για να δημιουργήσουμε τις σελίδες μας. Εκτός όμως από αυτό έχουμε συνδέσει και έναν γραφικό filemanager, τον elFinder ώστε να μπορούμε να ανεβάζουμε εικόνες, αρχεία javascript και css. Έτσι γίνεται πιο εύκολη η δημιουργία των στατικών σελίδων.

Οι σελίδες χρησιμοποιούν σαν template το αρχείο fpd_default.html όταν δεν έχουν την δυνατότητα σχολίων και το fpg_comments.html όταν την έχουν. Μπορείτε να φτιάξετε ένα δικό σας αρχείο και να το ορίσετε σαν το template της σελίδας.

Αυτά είναι τα κύρια χαρακτηριστικά και οι βασικές τροποποιήσεις του pCMS. Κατεβάστε το από το github και πειραματιστείτε μαζί του. Απαιτείται μόνο η django 1.4.1 για να λειτουργήσει. Το βασικό module είναι το fpg και μπορεί να χρησιμοποιηθεί όπως είναι σε οποιοδήποτε άλλο project με ελάχιστες τροποποιήσεις.

Καλό hacking.

Andreas Porevopoulos
Lead Developer at
http://www.dream-solutions.gr



Wednesday, August 29, 2012

Ένα Twitter clone γραμμένο σε django

Ένα Twitter clone γραμμένο σε django

To dwitter είναι ένα εκπαιδευτικό project σε django. Ο σκοπός του είναι να παρουσιάσει διάφορες τεχνικές και προγράμματα που είναι διαθέσιμα για djangο. Με τον συνδυασμό αυτών και λίγο προγραμματισμό είμαστε σε θέση να φτιάξουμε μια εφαρμογή με τις δυνατότητες του twitter.

Ξεκίνησε σαν ένα project για εκμάθηση των δυνατοτήτων της socket.io. Αυτή είναι μια βιβλιοθήκη γραμμένη σε javascript που υλοποιεί πρωτόκολλα websockets στους διάφορους browser. Με τον όρο websockets αναφερόμαστε γενικά στην δυνατότητα που έχει ένας server να επικοινωνήσει απ' ευθείας με τους clients του μέσω http. Έτσι ενώ εσείς διαβάζετε τα tweets που έχετε στην σελίδα σας, ένα μήνυμα στο πάνω μέρος της σελίδας σας ενημερώνει ότι: "Υπάρχουν 2 νέα tweets". Πως ήρθε αυτό το μήνυμα σε εσάς, αφού δεν έχετε κάνει refresh ή κάποια άλλη ενέργεια; Πως σας ξεχώρισε ανάμεσα από τόσους που είναι συνδεδεμένοι την ίδια στιγμή; Για όλα αυτά είναι υπεύθυνα τα websockets. Για την υλοποίηση των websockets σε python και django χρησιμοποιήθηκε το gevent-socketio. Ένα module βασισμένο στο gevent, το οποίο παρέχει και έναν ειδικό worker για το gunicorn. Το setup αυτό λειτουργεί πολύ καλά με websockets, εξυπηρετεί τις σελίδες της django και ακόμα μοιράζει τα static αρχεία μας.

Ο χρήστης μετά το login μεταφέρεται στην σελίδα με τα dwits (ταιριαστή παράφραση του tweets!) του. Η σελίδα ζητάει από τον server μια σύνδεση socket και τον πληροφορεί ότι θέλει να συμμετάσχει στο δωμάτιο με το όνομα του χρήστη. Όπως στο irc υπάρχουν πολλά rooms (δωμάτια), έτσι κι εδώ κάθε χρήστης έχει το δικό του. Σε αυτό το δωμάτιο ακούει για νέα dwits. Όταν ένας χρήστης δημοσιεύει ένα dwit, αυτό αντιγράφεται σε όλα τα δωμάτια των χρηστών που τον ακολουθούν. Ο server τώρα, όταν έχει κάτι να στείλει από ένα δωμάτιο το στέλνει μέσω του socket που έχει εγγραφεί σε αυτό το δωμάτιο. Έτσι ο χρήστης λαμβάνει μόνο τα dwits που προορίζονται γι' αυτόν. Το ίδιο συμβαίνει και όταν ο χρήστης παρακολουθεί κάποιο tag. Φεύγει από το δικό του δωμάτιο και συνδέεται σε δωμάτιο με το όνομα του tag. Κάθε dwit που δημοσιεύεται και περιέχει tag, αντιγράφεται και σε δωμάτιο με το όνομα του tag.


Από τα παραπάνω είναι φανερό ότι χρειάζεται μια ειδική αντιμετώπιση όσον αφορά στην διαχείριση των dwits. Αυτά αποθηκεύονται στην βάση της django, αλλά για τον διαμοιρασμό τους χρησιμοποιούμε την πολύ γρήγορη redis και συγκεκριμένα την δυνατότητά της pub/sub. Με αυτήν δίνεται η δυνατότητα σε μια function να εγγραφεί (subscribe) σε ένα "κανάλι". Όταν κάποιος δημοσιεύσει (publish) σε αυτό το κανάλι τότε η function εκτελείται με τα δεδομένα της δημοσίευσης. Έτσι το dwit σώζεται στην βάση και αντιγράφεται στα ανάλογα κανάλια (ακόλουθοι και tags). Όποιο κανάλι έχει συνδρομητές (sockets) τους αποστέλλεται το περιεχόμενο και αυτοί με την σειρά τους το στέλνουν στην σελίδα του χρήστη.


Κάνοντας λίγες πράξεις βλέπουμε ότι αυτό το πλάνο θα έχει πρόβλημα αν εκτελείται σειριακά. Αν ο κάθε χρήστης έχει Χ ακόλουθους και το κάθε dwit μπορεί να έχει Υ tag και να δημοσιεύονται ταυτόχρονα Ζ dwits, o πολλαπλασιασμός αυτός γρήγορα θα φτάσει σε σημείο όπου η django δεν θα προλαβαίνει να δείξει σελίδες. Γι' αυτό, το κομμάτι αυτό, το βάζουμε σε μια ουρά. Κάθε τι που είναι να δημοσιευθεί, θα δημοσιευθεί με την σειρά που μπήκε στην ουρά και θα περιμένει όση ώρα χρειαστεί, αφήνοντας την εφαρμογή μας να μοιράζει σελίδες. Αυτήν την δουλειά κάνει το celery. Κάθε function που δηλώνεται ως task του celery, μεταφέρεται στην ουρά (στην προκειμένη περίπτωση πάλι στην redis) και το celery θα την εκτελέσει όταν μπορέσει. Υπάρχει μεγάλος φόρτος εργασίας; Ξεκινάμε περισσότερους εργάτες (workers) του celery.


Η βάση της django έχει τρία βασικά μοντέλα. Τους χρήστες της εφαρμογής, τα dwits και τα dwits per follower (dwits ανά ακόλουθο). Στο τελευταίο μοντέλο για κάθε dwit που δημοσιεύει ένας χρήστης δημιουργούνται εγγραφές με το id του dwit και το id του κάθε ακόλουθου του χρήση. Έτσι αν κάποιος έχει 1000 ακόλουθους και έχει κάνει 1000 dwits θα έχει 1 εκ. εγγραφές σε αυτό το μοντέλο. Μόνο αυτός. Αν έχεις μόνο (για τα δεδομένα του twitter) 1000 τέτοιους χρήστες έφτασες εύκολα 1 δισ. εγγραφές. Φαίνεται αμέσως ότι μια κλασική βάση δεδομένων θα έχει πρόβλημα. Γι' αυτό θα ήταν καλό να χρησιμοποιηθεί μια NoSQL βάση σαν την redis (σχετικό άρθρο εδώ). Από την άλλη όμως αυτό είναι ένα απλό project και δεν συναγωνίζεται το twitter. Να αναφέρουμε εδώ και τις εκπληκτικές δυνατότητες του django ORM, όπου για παράδειγμα στο μοντέλο των χρηστών υπάρχει ένα πεδίο ManyToMany με τον εξής ορισμό:


following = models.ManyToManyField('self', blank=True,\
                   symmetrical=False, related_name='followers')
Αυτό το πεδίο δείχνει ποιους χρήστες ακολουθεί ο κάθε χρήστης. Κάνοντας:

member = Member.objects.get(pk=10)
following = member.following.all()
έχουμε τους χρήστες που ακολουθεί ο χρήστης με id=10. Αλλά κάνοντας και:

followers = member.followers.all()
έχουμε όλους που τον ακολουθούν. Χωρίς να γράψουμε άλλο κώδικα, εύκολα και ευανάγνωστα. Κάτι τέτοιο δεν υπάρχει σε NoSQL.

Για την εγγραφή των χρηστών χρησιμοποιήσαμε μια δικιά μας έκδοση του django-registration και για την αυθεντικοποίηση των χρηστών μια ελαφρώς αλλαγμένη έκδοση του django.contrib.auth. Το δεύτερο έχει αντιγραφεί όπως είναι από το source (εκτός από κάτι αλλαγές στις μεταφράσεις και στην φόρμα login) και προσθέσαμε την δυνατότητα όταν στέλνει email, να το στέλνει σαν task στο celery. Με αυτό τον τρόπο δεν περιμένει να τελειώσει η αποστολή και μετά να επιστραφεί ο έλεγχος στην django. Το ίδιο κάναμε και στο registration. Αυτός είναι και ο πιο διαδεδομένος τρόπος χρήσης του celery. Για δουλειές που δεν υπεύθυνος ο server της εφαρμογής. Όπως παρακάτω με το haystack.


Το haystack είναι ένα module αναζήτησης. Προσφέρει όλες τις απαραίτητες συναρτήσεις για την αναζήτηση στοιχείων σε μια βάση, χρησιμοποιώντας διάφορες μηχανές αναζήτησης διαθέσιμες στην python. Η αναζήτηση δεν γίνεται απ' ευθείας στην βάση αλλά σε ένα δικό του αρχείο στο οποίο έχει αποθηκεύσει τα στοιχεία που του έχουμε ορίσει ότι είναι αναζητήσιμα. Αυτό το αρχείο πρέπει να ενημερώνεται σε τακτά χρονικά διαστήματα με τα καινούργια στοιχεία της βάσης. Γι αυτόν τον σκοπό χρησιμοποιούμε μια άλλη δυνατότητα του celery, την δυνατότητα προγραμματισμού (crontab) εργασιών. Έχουμε ορίσει μια συγκεκριμένη συνάρτηση, που πραγματοποιεί την ενημέρωση, να εκτελείται κάθε 15 λεπτά.


Για την διαχείριση των tag χρησιμοποιήσαμε το django-taggit και για την εμφάνιση της εφαρμογής το bootstrap. Αυτό είναι ένα πολύ ωραίο css framework, γραμμένο από τα παιδιά του twitter και η προσθήκη δικών μας css είναι ελάχιστη. Χρησιμοποιώντας τα media queries, το bootstrap προσαρμόζεται αυτόματα σε κινητές και pad συσκευές.


Το dwitter είναι διαθέσιμο στο github και περιμένει περισσότερο πειραματισμό και hacking. Οδηγίες εγκατάστασης και παραμετροποίησης βρίσκονται στο README και στο wiki.Υπάρχει και ένα live demo site στην διάθεση σας για να δείτε την λειτουργία του. Μπορείτε να δημιουργήσετε δύο χρήστες με το ίδιο email (κανονικό ή ψεύτικό, δεν συγκεντρώνουμε email) και να κάνετε login από δύο διαφορετικούς browser (firefox και chrome ή desktop και κινητό) και να δείτε την μετάδοση των dwit σε πραγματικό χρόνο από τον ένα στον άλλο.


Καλό hacking.

Andreas Porevopoulos
Lead developer at
http://www.dream-solutions.gr

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 από εδώ.