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

Sunday, July 7, 2013

Tutorial: PyQt digital clock

What's the project?


In the first part of my PyQt4 tutorial series, I will show you how to make a simple digital clock in LCD Style. Thank me.


It will be quite detailed, so consider skipping parts that are obvious to you if you already know some Python. 


What you will have after completing the tutorial:





and a more complex version, where you can switch between HH MM and HH MM SS format (if you want to follow it):


Yes, I made a gif for you, because I love me! Eh you, I meant to say you.



Let's get started


Before starting to make the GUI, I always like to write a logic-only version in plain Python. So how would you make a clock in Python? Very Simple:



import time

while True != False:
    print(time.strftime("%H"+":"+"%M"+":"+"%S"))
    time.sleep(1)




Let's take a look at this. First, we must import the time module, as we will need to access strftime, Python's easiest way to display the current time, which it gets from your OS. Next, we need an infinite loop, which is best achieved using a while loop followed by a condition that is always true, like True != False, 1 == 1 or 5/5 != 0. In that while loop, comes the main part of this little program 



print(time.strftime("%H"+":"+"%M"+":"+"%S"))
time.sleep(1)



While True != False, it will infinitely print the current time, sleep for a second, then print the updated time again. 


What's great about time.strftime, is that there are many more possibilities of what you can display e.g. year, month, day, day of the year etc. by simply changing the parameters. If you wanted the year you would simply add "%Y" to the string. You can find a full list of available parameters here, as well as in the official documentation. 



The GUI

Now to the GUI:


import sys
from PyQt4 import QtGui, QtCore

from time import strftime


class Main(QtGui.QMainWindow):

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

    def initUI(self):

        self.timer = QtCore.QTimer(self)
        self.timer.timeout.connect(self.Time)
        self.timer.start(1000)

        self.lcd = QtGui.QLCDNumber(self)
        self.lcd.display(strftime("%H"+":"+"%M"))

        self.setCentralWidget(self.lcd)

#---------Window settings --------------------------------
        
        self.setGeometry(300,300,250,100)
        self.setWindowTitle("Clock")

#-------- Slots ------------------------------------------

    def Time(self):
        self.lcd.display(strftime("%H"+":"+"%M"))
        
def main():
    app = QtGui.QApplication(sys.argv)
    main = Main()
    main.show()

    sys.exit(app.exec_())

if __name__ == "__main__":
    main()


Copy the simple PyQt4 window code I prepared for you on the sidebar of this website to get a plain PyQt4 window. Instead of importing only the time module, we import strftime directly from time, to make it easier to call strftime. Next, let's create the LCD display.


self.lcd = QtGui.QLCDNumber(self)
self.lcd.display(strftime("%H"+":"+"%M"))

PyQt actually has a really cool LCD Display function built in, that can display integers as well as strings, which we call and assign an instance self.lcd for. Call self.lcd.display() and insert strftime, from the plain Python example, as its argument.

The main difference to the plain Python example is that instead of using a while loop with time.sleep(), as it would interfere with the program and prevent the window from showing due to it being stuck in the loop before getting to the main.show() at the bottom, we use the very handy QTimer (what I love about PyQt is that it just makes your life so much easier).


self.timer = QtCore.QTimer(self)
self.timer.timeout.connect(self.Time)
self.timer.start(1000)

After instantiating the QTimer, we need to connect it to a slot, in case the timer times out(which we want). Connect it to self.Time, we'll take care of the function later on. Lastly, start the timer. Note that the parameter is in milliseconds (1000 ms = 1s). 

Next, we call self.setCentralWidget(self.lcd) to make self.lcd take up the whole window. 

Finally, let's create the slot that we just connected to the timer. 


def Time(self):
        self.lcd.display(strftime("%H"+":"+"%M"))

Every 1000 ms, the LCD displays the current time anew, using strftime. That's it! Run the program. 


Advanced Version


What if you wanted to be able to switch between displaying in HH MM format and HH MM SS format?

First of all, how many programmers does it take to change a light bulb? None, it's a hardware problem. Next, let me explain what we will have to do beforehand. Basically, we need two radio boxes (we could use buttons but they'd take up more space), that change a couple things for us. Here's the code:

import sys
from PyQt4 import QtGui, QtCore

from time import strftime

var = True

class Main(QtGui.QMainWindow):

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

    def initUI(self):

        timer = QtCore.QTimer(self)
        timer.timeout.connect(self.Time)
#Reduced update time to fasten the change from w/ secs to w/o secs
        timer.start(10)

        self.lcd = QtGui.QLCDNumber(self)
        self.lcd.resize(250,100)
        
#Added self.lcd.move and moved the clock 30px down to make space for buttons
        
        self.lcd.move(0,30)
        self.lcd.display(strftime("%H"+":"+"%M"))

        self.r1 = QtGui.QRadioButton("Hide seconds",self)
        self.r1.move(10,0)
        self.r2 = QtGui.QRadioButton("Show seconds",self)
        self.r2.move(110,0)

        self.r1.toggled.connect(self.woSecs)
        self.r2.toggled.connect(self.wSecs)

#---------Window settings --------------------------------

# Expanded window height by 30px

        self.setGeometry(300,300,250,130)
        self.setWindowTitle("Clock")
        self.setWindowIcon(QtGui.QIcon(""))
        self.show()

#-------- Slots ------------------------------------------

    def Time(self):
        global var
        if var == True:
            self.lcd.display(strftime("%H"+":"+"%M"))
        elif var == False:
            self.lcd.display(strftime("%H"+":"+"%M"+":"+"%S"))

    def wSecs(self):
        global var
        var = False
        
        self.resize(375,130)
        self.lcd.resize(375,100)
        self.lcd.setDigitCount(8)

    def woSecs(self):
        global var
        var = True
        
        self.resize(250,130)
        self.lcd.resize(250,100)
        self.lcd.setDigitCount(5)

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

    sys.exit(app.exec_())

if __name__ == "__main__":
    main()

First, we need to change the window size and LCD position a bit, which I marked with comments above. Plus, I advise you to reduce the timer.start time to 10 milliseconds, as it will increase the speed when changing between the formats. Otherwise, you have to wait for the full 1000 milliseconds to pass until you can see any changes. Also, create a global variable var and set it to True. 

Let's add the radio boxes:


self.r1 = QtGui.QRadioButton("Hide seconds",self)
self.r1.move(10,0)
self.r2 = QtGui.QRadioButton("Show seconds",self)
self.r2.move(110,0)

self.r1.toggled.connect(self.woSecs)
self.r2.toggled.connect(self.wSecs)


Create two radio boxes, move them to fit the window and connect self.r1 to the self.woSecs and self.r2 to the self.wSecs function using .toggled.connect.

Let's skip the Time function for now and move straight to creating the wSecs and woSecs functions. 



    def wSecs(self):
        global var
        var = False
        
        self.resize(375,130)
        self.lcd.resize(375,100)
        self.lcd.setDigitCount(8)

    def woSecs(self):
        global var
        var = True
        
        self.resize(250,130)
        self.lcd.resize(250,100)
        self.lcd.setDigitCount(5)


When the Show seconds radio box is active, we need to set var to False, which will be important later on. Don't forget to call var first, since it's global. Since we will need to make space for the seconds, the window and the LCD display must be re-sized to 375px. Additionally, we need to set the digit count of the LCD to 8, since it's 5 by default and won't display more digits if it is not explicitly changed.

For the Hide seconds re-size the window and LCD to the original size and change the digit count back to 5, since we'll display the same window as before. 

Now to the Time function:



    def Time(self):
        global var
        if var == True:
            self.lcd.display(strftime("%H"+":"+"%M"))
        else:
            self.lcd.display(strftime("%H"+":"+"%M"+":"+"%S"))

Call global var once more and now create an if clause with an additional else condition. When var is set to True, the LCD should display only hours and minutes as before, when it is set to False, we want it to also show seconds. The var variable is changed every time we click a radio button. 

That should be it. You can now switch between formats.

I know this was a very long post, but as I said I try to leave as little room as possible to uncertainties. If any problems do arise, don't hesitate to comment on this post. Also, since this is my first tutorial, I'm very open to feedback of any kind.

See you soon,
Your Innkeper. 

10 comments :

  1. Cheers. I read through some of the titles, and I'll be back to read more. I like those small tangible projects. I tried to make some myself, so if you are interested, please check out http://lovholm.net .
    Keep up writing about the exploration! :)

    ReplyDelete
    Replies
    1. Thank you. I've found the best way to learn PyQt is through smaller projects like these, I'm glad you like them. Nice website by the way :)

      Delete
  2. I don't like you using gobal in class. I change it , you can use self.var in def initUI(self): and change self.var in def Time(self), def wSec(self) and def woSecs(self) in it.

    ReplyDelete
    Replies
    1. I wrote this after knowing how to program for like a month, it's obviously not well written and does lots of things wrong. You should also not set fixed sizes for things and move them manually, but rather use layouts. But I'm not gonna rewrite all my old tutorials.

      Delete
    2. All right, I changed in the following, Sorry my english language is very bad. I know you work hard, but I expect you to understand object-oriented.
      #------------------------------------------------
      import sys
      from PyQt4 import QtGui, QtCore
      from time import strftime



      class Main(QtGui.QMainWindow):

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

      def initUI(self):
      self.var = True
      Omission ...
      def Time(self):
      if self.var == True:
      self.lcd.display(strftime("%H"+":"+"%M"))
      elif self.var == False:
      self.lcd.display(strftime("%H"+":"+"%M"+":"+"%S"))

      def wSecs(self):
      self.var = False

      self.resize(375,130)
      self.lcd.resize(375,100)
      self.lcd.setDigitCount(8)

      def woSecs(self):
      self.var = True

      self.resize(250,130)
      self.lcd.resize(250,100)
      self.lcd.setDigitCount(5)
      Omission ...

      Delete
    3. It's very nice of you to want to help me. I'm just trying to say that I wrote this a long time ago, when I didn't yet understand the concepts of object-oriented programming. That's not the case anymore now. I quickly wrote this here so you can see what this piece of code should really look like: https://gist.github.com/goldsborough/191c98696bb34511196e

      Some tips: you don't have to check if booleans (self.var) are true or false explicitly (== True or == False), they evaluate to true or false implicitly (you can just write if self.var: ... else: ...). Use layouts! You should never (!) use self.resize() or self.move() on anything if you want to not hate yourself at one point or another later on. Use a QGridLayout or QVBoxLayout / QHBoxLayout if those suffice. A stylistic thing: don't import submodules directly if the names aren't too long, I always prefer knowing, for example, strftime comes from the module time and not from somewhere within my own code. So I'd prefer writing time.strftime() over strftime() alone.

      Also check out the tutorials I published on Binpress on building a text editor if you're interested in learning more about PyQt: https://www.binpress.com/tutorial/building-a-text-editor-with-pyqt-part-one/143

      Delete
    4. Hi nice to meet you, Peter Goldsborough; you can see this web http://zetcode.com/gui/pyqt4/ to learn pyqt if you are a beginner.

      Delete
  3. This comment has been removed by the author.

    ReplyDelete
  4. Can't get this to work at all. No errors, just does nothing at all????? How do you even start a debug?

    ReplyDelete
  5. Could you please blog a Qt5 version as well . Looks like a ton of differences between Qt4

    ReplyDelete