Hi! Hope you're enjoying this blog. I have a new home at www.goldsborough.me. Be sure to also check by there for new posts <3

Monday, August 12, 2013

PyQt Calender App

Hi there, I recently did a sort of calender/reminder app in PyQt, using the QCalender widget. I have to say writing the tutorials like I am right now takes a lot of time, so for everything over ca. 200 lines of code I'll just post them either entirely or with comments to help you guys out a bit. Tell me what you think of this format.


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

'''
TheCodeInn PyQt4 Tutorial

PyQt Calender App

Author: Peter Goldsborough
Website: http://thecodeinn.blogspot.com
last edited: August 2013
'''

import sys, pickle
from PyQt4 import QtGui, QtCore

#some global variables

dateStr = "" # will hold the date
name = "" # will hold the name of an added event

gridVar = 3 # will be used to extend the grid layout

sender = "" # will hold the sender button's text
s = "" # will hold the sender button itself


e = open("events.txt","rb") #this try - except clause is there to prevent loading errors if events.txt is empty
try:
    events = pickle.loads(e.read()) 
except:
    events = {}
e.close()

class Edit(QtGui.QDialog):
    def __init__(self,parent = None):
        QtGui.QDialog.__init__(self, parent)

        #this is the dialog that opens up if a specific event is clicked
        
        self.initUI()

    def initUI(self):
        global sender

        self.change = QtGui.QLabel("Change: ",self)
        self.change.move(5,7)

        self.line = QtGui.QLineEdit(self)
        self.line.move(65,5)
        self.line.setText(sender)

        self.delt = QtGui.QPushButton("Delete",self)
        self.delt.move(80,50)

        self.ok = QtGui.QPushButton("OK",self)
        self.ok.move(40,100)

        self.cancel = QtGui.QPushButton("Cancel",self)
        self.cancel.move(120,100)
        

        self.setGeometry(300,300,240,130)
        self.setWindowTitle("Edit event")
        self.setStyleSheet("font-size:14px;")

class Main(QtGui.QMainWindow):

    def __init__(self):
        QtGui.QMainWindow.__init__(self)
        self.initUI()

    def initUI(self):

        self.widget = QtGui.QWidget(self)

        self.cal = QtGui.QCalendarWidget(self)
        self.cal.setGridVisible(True)
        self.cal.clicked[QtCore.QDate].connect(self.showDate)
        self.cal.setStyleSheet("font-size:15px")

        self.upc = QtGui.QLabel(self)
        self.upc.setStyleSheet("font-size:16px;")

        self.Upcoming()

        date = self.cal.selectedDate().toString()
        
        self.lbl = QtGui.QLabel(date,self)
        self.lbl.setStyleSheet("font-size:16px;")

        self.add = QtGui.QPushButton("+ Add",self)
        self.add.clicked.connect(self.AddEvent)

        self.grid = QtGui.QGridLayout()

        self.spacer = QtGui.QSpacerItem(20, 30, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed)

        self.grid.addWidget(self.cal,0,0,1,2)
        self.grid.addItem(self.spacer,2,0)
        self.grid.addWidget(self.upc,1,0)
        self.grid.addWidget(self.lbl,3,0)
        self.grid.addWidget(self.add,3,1)

        self.widget.setLayout(self.grid)

#---------Window settings --------------------------------
        
        self.setGeometry(300,300,400,400)
        self.setWindowTitle("PyCal")
        self.setCentralWidget(self.widget)

#---------Slot functions --------------------------------

    def Upcoming(self):

        #this function takes care of displaying a list of upcoming events
        
        count = 0
        length = 0
        upcoming = "Upcoming events: "
        
        if events: #if events is not empty
            for i in events.values(): #for every list of events 
                for j in i: # for every event in these lists
                    length += 1
                    
            while count < 2: #set it to a higher number if you want more items displayed
                for i in events.values():
                        for j in i:
                            if length < 2:
                                upcoming += j #if there is only one event, we append it to the string and immediately exit the while loop, without this it'd display an event twice if there was only one
                                count = 2 
                            else:
                                upcoming += j+", "
                                count += 1
        else:
            upcoming += "None" #if there are no events, we display "None"

        self.upc.setText(upcoming)

    def Edit(self):

        # this function is called when an event is clicked, opening a QDialog that lets you edit the name or delete it
        
        global sender
        global s
        global events

        s = self.sender() #get the information from the sending button so we know which one it is, since they're generically created
        sender = s.text()
        
        edit = Edit(self) #modularly open the Edit QDialog
        edit.show()

        def Ok(): 

            #if the name is changed, we save the new name and delete the old one
            
            events[dateStr].remove(s.text())
            events[dateStr].append(edit.line.text())

            e = open("events.txt","wb")
            pickle.dump(events,e)
            e.close()
            
            s.setText(edit.line.text())

            self.Upcoming()

            edit.close()

        def Delete():

            #if it is deleted, we have to delete it from the layout, this is done by that for loop down there
            
            for i in reversed(range(self.grid.count())): #the reversed is there since it'd mess up the layout otherwise
                try:
                    if self.grid.itemAt(i).widget().text() == sender:
                        self.grid.itemAt(i).widget().setParent(None)
                except:
                    #you may wonder why I have a try - except clause here if I'm not doing anything when there's an exception, well, that's because the spacer
                    #is a QItem, and that would mess up the self.grid.itemAt(i).widget() up there, since the spacer is an item, not a widget
                    pass

            if len(events[dateStr]) > 1: #if there are more than one values for that day, we just remove this event
                events[dateStr].remove(sender)
            else: #if this was the day's last event we just deleted, it's good to delete the whole list, empty lists suck
                del events[dateStr]
            
            e = open("events.txt","wb")
            pickle.dump(events,e)
            e.close()

            self.Upcoming() #update the upcoming events

            edit.close()

        edit.delt.clicked.connect(Delete)
        edit.cancel.clicked.connect(lambda cancel: edit.close())
        edit.ok.clicked.connect(Ok)

    def showDate(self, date):
        global dateStr
        global events
        global gridVar

        #from here

        eventStr = ""
        gridVar = 3

        for i in reversed(range(self.grid.count())):
            try:
                self.grid.itemAt(i).widget().setParent(None)
            except:
                pass

        self.grid.addWidget(self.cal,0,0,1,2)
        self.grid.addItem(self.spacer,2,0)
        self.grid.addWidget(self.upc,1,0)
        self.grid.addWidget(self.lbl,3,0)
        self.grid.addWidget(self.add,3,1)
        
        self.setGeometry(300,300,400,400)

        #to here, we just reset the whole layout to the default - no events yet
        
        dateStr = date.toString()
        
        if dateStr in events: #now we get funky, so if the date is in the events dictionary
            for i in events[dateStr]: #we create a button for every event
                b = QtGui.QPushButton(i,self)
                b.setStyleSheet("border-radius:5px;font-size:16px;")
                b.clicked.connect(self.Edit)

                '''
                If you wonder why the buttons aren't displayed like buttons, but like labels, yet
                still work like buttons (clickable), that's because I set the border radius to 1px
                up there in the StyleSheet. QPushButton doesn't seem to support border-radius modification
                so it just displays the button flat .. neat trick if you want a clickable qlabel!
                '''
                
                self.grid.addWidget(b,gridVar,1) #extend the layout
                self.grid.addWidget(self.add,gridVar+1,1)

                gridVar +=1
                
        self.lbl.setText(dateStr)

    def AddEvent(self):
        global dateStr
        global events
        global date

        name, ok = QtGui.QInputDialog.getText(self,"Add event", #here we call an input dialog to get the event's name
                                   "Name: ")
        if dateStr not in events: #if the date isn't in the events list yet, we create a new dictionary item
            events[dateStr] = [name]
        else: #if it is, we append it to the date's events list
            events[dateStr].append(name)

        e = open("events.txt","wb")
        pickle.dump(events,e)
        e.close()

        self.showDate(self.cal.selectedDate())

        self.Upcoming()
        
def main():
    app = QtGui.QApplication(sys.argv)
    main= Main()
    main.show()

    print("I \N{black heart suit} Python")

    sys.exit(app.exec_())

if __name__ == "__main__":
    main()



Hope you like the easter egg in my program :)

No comments :

Post a Comment