DEV Community

Cover image for Create a stopwatch and timer with Python and kivymd Part 1
Ngonidzashe Nzenze
Ngonidzashe Nzenze

Posted on

Create a stopwatch and timer with Python and kivymd Part 1

In this article I am going to show you how to develop a stop watch and timer application with the kivymd framework. The first part of this tutorial will take you through creating the stopwatch and the second part through creating the the timer.

Final Product

The final application will look as below:

Stopwatch

Timer


The UI

Lets get started with the user interface.

Prerequisites
For this project, I'm using:

kivy==2.1.0
kivymd==1.1.1
Enter fullscreen mode Exit fullscreen mode

Make sure both are installed in a virtual environment.

Create two files:

  • main.py
  • main.kv

Inside main.py add the following code:

import kivy
from kivymd.app import MDApp
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.dialog import MDDialog
from kivymd.uix.list import TwoLineIconListItem, IconLeftWidget
from kivy.properties import NumericProperty, StringProperty
from datetime import timedelta
from kivy.clock import Clock
kivy.require('2.1.0')


class MainApp(MDApp):

    def build(self):
        # Setting the theme
        self.theme_cls.theme_style = "Dark"
        self.theme_cls.primary_palette = "Gray"
        self.theme_cls.primary_hue = "200"


if __name__ == '__main__':
    app = MainApp()
    app.run()

Enter fullscreen mode Exit fullscreen mode

And inside main.kv, add the following code:

#:kivy 2.1.0

MDScreenManager:
    MDScreen:
        name: 'main_screen'


        MDBottomNavigation:
            selected_color_background: "orange"
            text_color_active: "lightgrey"

            MDBottomNavigationItem:
                name: 'screen 1'
                text: 'Stop Watch'
                icon: 'timer'

                MDBoxLayout:
                    orientation: 'vertical'


                    MDLabel:
                        text: 'Stop Watch'
                        halign: 'center'
                        theme_text_color: "Custom"
                        text_color: 1, 1, 1, 1
                        font_style: 'H2'


            MDBottomNavigationItem:
                name: 'screen 2'
                text: 'Timer'
                icon: 'timer-sand'


                MDBoxLayout:
                    orientation: 'vertical'

                    MDTopAppBar:
                        right_action_items: [["plus", ]]
                        md_bg_color: app.theme_cls.bg_dark

                    MDLabel:
                        text: 'Timer'
                        halign: 'center'
                        theme_text_color: "Custom"
                        text_color: 1, 1, 1, 1
                        font_style: 'H2'
Enter fullscreen mode Exit fullscreen mode

If you run this application right now, you will get the following:

Stopwatch tutorial


The stopwatch

First, we are going to build the stopwatch. The stop watch is going to be fairly simple. It will consist of a Label, to show the elapsed time, and three buttons, one to reset the stopwatch, another to start, pause or stop the stopwatch and the last one to record the lap times. Lastly, it will have a list within a ScrollView widget in which we will be adding the recorded lap times.

Start by modifying main.kv as follows :

#...

# Replace the label with the text "Timer" with the following
MDLabel:
    id: stopwatch_lbl
    text: app.stopwatch_time
    halign: 'center'
    theme_text_color: "Custom"
    text_color: 1, 1, 1, 1
    font_style: 'H2'

# Add a scroll view and a list
MDScrollView:
    MDList:
        id: count_list

# Add a float layout
MDFloatLayout:
    pos_hint: {'center_x': .5}
    size_hint_y: .3
    size_hint_x: .5
    spacing: 50

    # Add three icon buttons
    MDIconButton:
        id: reset_btn
        icon: 'undo-variant'
        pos_hint: {'center_x': .2}
        on_press: app.reset_stopwatch()
        disabled: True


    MDIconButton:
        id: play_pause_btn
        icon: 'play'
        pos_hint: {'center_x': .5}
        on_press: app.start_or_stop_stopwatch()


   MDIconButton:
       id: record_lap_btn
       icon: 'timelapse'
       pos_hint: {'center_x': .8}
       on_press: app.time_lap()
       disabled: True

#...
Enter fullscreen mode Exit fullscreen mode

main.py


#...

class MainApp(MDApp):

    stopwatch_time = StringProperty()

    milliseconds = NumericProperty()

    seconds = NumericProperty()

    minutes = NumericProperty()

    def build(self):
        # Setting the theme
        self.theme_cls.theme_style = "Dark"
        self.theme_cls.primary_palette = "Gray"
        self.theme_cls.primary_hue = "200"

    def on_start(self):
        self.stopwatch_time = "00:00:00"

    def start_or_stop_stopwatch(self):
        pass

    def reset_stopwatch(self):
        pass

    def time_lap(self):
        pass

#...

Enter fullscreen mode Exit fullscreen mode

The latest changes have added icon buttons and a dynamic label to the interface. By setting the text of the label to app.stopwatch_time, we can change the text displayed on the interface by simply changing the value of the attribute stopwatch_time. Kivy properties make it easy to pass updates from the python side to the user interface and hence we used the StringProperty object.

In the above code we have also defined seconds, milliseconds and minutes as Numeric Properties which we are going to use to make updates to the stopwatch.

The on_start function is going to set the stopwatch to 00:00:00 when the application starts.

The start_or_stop_stopwatch function will start the stopwatch if it isn't running or stop the stopwatch if it is.

The reset_stopwatch function will reset the stopwatch to 00:00:00.

The time_lap funtion will measure the time between laps.

On running the application, you will get the following:

Stopwatch sample


Now lets get our stopwatch to work.

main.py

# ...

class MainApp(MDApp):

    #...

    # Lap counter
    count = 1

    watch_started = False # Whether watch is running or not

    # store the last recorded lap time
    last_lap_time = {
        'minutes': 0,
        'seconds': 0,
        'milliseconds': 0
    }

    #...

Enter fullscreen mode Exit fullscreen mode

count is going to keep track of the list items that are added when timing a lap which we'll see in action soon.

watch_started is going to keep track of whether or not the stop watch is running.

The dictionary last_lap_time contains data on the last recorded lap time which is updated every time we hit the lap button.

main.py

#...

class MainApp(MDApp):

    #...

    # add this function
    def get_string_time(self, dt):
        """Function to increment milliseconds and convert the time elapsed to string format to which the label is set"""
        self.increment_milliseconds()

        milliseconds = str(self.milliseconds)
        seconds = str(self.seconds)
        minutes = str(self.minutes)

        if len(milliseconds) < 2:
            milliseconds = '0' + milliseconds

        if len(seconds) < 2:
            seconds = '0' + seconds

        if len(minutes) < 2:
            minutes = '0' + minutes

        self.stopwatch_time = minutes + ":" + seconds + ":" + milliseconds

    # Modify start_or_stop_stopwatch to look as follows
    def start_or_stop_stopwatch(self):
        """Function to stop the stopwatch if it is not running otherwise stop it"""
        if self.watch_started:
            self.watch_started = False
            self.root.ids['record_lap_btn'].disabled = True
            self.root.ids['play_pause_btn'].icon = 'play'
            self.root.ids['reset_btn'].disabled = False
            Clock.unschedule(self.get_string_time) # Unschedule the get_string_time function
        else:
            self.watch_started = True
            self.root.ids['play_pause_btn'].icon = 'pause'
            self.root.ids['record_lap_btn'].disabled = False
            Clock.schedule_interval(self.get_string_time, 0.1) # schedule the get_string_time function to run every 10ms

    #...
Enter fullscreen mode Exit fullscreen mode

The get_string_time function converts the timer from numbers to a string, that is, it gets the values of the milliseconds, seconds and minutes and converts them to a string. For the sake of display, it checks whether the values have two digits, and if they do not, it adds a 0 before the digit to make them two and hence we can always display our minutes, seconds and milliseconds with two digits.

We have modified the start_or_stop_stopwatch and now if the stopwatch was not running, it will change the status of watch_started to True to indicate that it is now running. It will then change the 'play' icon to the 'pause' icon, which will allow us to pause the stopwatch. The record_lap_button is then enabled so that we can record our laps. Finally, we will schedule the get_string_time function with kivy clock to run every 10 milliseconds so that the stopwatch is updated every 10 milliseconds.

If the stopwatch was running, it will stop/pause the stopwatch and then disable the lap button since we can't record laps if the watch is not running. It will then switch the icon to the 'play' icon. The reset button is then enabled as well to allow us to reset our stopwatch. Finally, it will unschedule the get_string_time to stop it from executing every 10 milliseconds.

main.py

class MainApp(MDApp):
    #...

    # Modify reset_stopwatch as follows
    def reset_stopwatch(self):
        """Set the stopwatch to 00:00:00"""
        if self.watch_started:
            Clock.unschedule(self.get_string_time)
        self.stopwatch_time = "00:00:00"
        self.milliseconds = 0
        self.seconds = 0
        self.minutes = 0

        # disable reset and lap buttons
        self.root.ids['reset_btn'].disabled = True
        self.root.ids['record_lap_btn'].disabled = True

        # Reset lap time
        self.last_lap_time['minutes'] = 0
        self.last_lap_time['seconds'] = 0
        self.last_lap_time['milliseconds'] = 0

    # Modify time_lap
    def time_lap(self):
        """Get the time between laps and add a list item to list"""
        lap_time = f"Count {self.count}: " + self.stopwatch_time
        list_item = TwoLineIconListItem(
            IconLeftWidget(
                icon="av-timer"
            ),
            text=lap_time,
            secondary_text=self.calculate_time_difference(),
            theme_text_color="Custom",
            text_color=(1, 1, 1, 1),
            secondary_theme_text_color="Custom",
            secondary_text_color=(1, 1, 1, 1)
        )

        self.root.ids['count_list'].add_widget(list_item, index=-1)

        self.count += 1

    # add the following function
    def increment_milliseconds(self):
        """Increment the milliseconds by 10ms"""
        self.milliseconds += 10

        if self.milliseconds == 100:
            self.increment_seconds()
            self.milliseconds = 0

    # add the following function
    def increment_seconds(self):
        """Increment the seconds by 1 second"""
        self.seconds += 1

        if self.seconds == 60:
            self.increment_minutes()
            self.seconds = 0

    # add the following function
    def increment_minutes(self):
        """Increment the minutes by 1 minute"""
        self.minutes += 1


    # add the following function
    def calculate_time_difference(self):
        """Calculate the time difference between the laps records
        """
        lap_time = timedelta(
            minutes=self.minutes,
            seconds=self.seconds,
            milliseconds=self.milliseconds
        ) - timedelta(
            minutes=self.last_lap_time['minutes'],
            seconds=self.last_lap_time['seconds'],
            milliseconds=self.last_lap_time['milliseconds']
        )

        lap_time = str(lap_time)[2:-3]

        lap_time = lap_time.split(':')

        lap_time = [i.split('.') for i in lap_time]

        minutes = int(lap_time[0][0])
        seconds = int(lap_time[1][0])
        milliseconds = int(lap_time[1][1])

        self.last_lap_time['minutes'] = self.minutes
        self.last_lap_time['seconds'] = self.seconds
        self.last_lap_time['milliseconds'] = self.milliseconds

        return f"{minutes}:{seconds}:{milliseconds}"
    #....
Enter fullscreen mode Exit fullscreen mode

Ok, I know it's a lot of code but I'll do my best to explain it.

The reset_stopwatch method will check if the stopwatch is running. If it is, it will unschedule the get_string_time method. It will then set stopwatch_time to 00:00:00 which will update our label to '00:00:00'. Next it will set the milliseconds, seconds and minutes to 0 and also disable the reset and the lap buttons. Lastly it will set the minutes, seconds and milliseconds keys in the last_lap_time dictionary to zero.

The time_lap method gets the current count and creates a string with the count and current stopwatch time: lap_time = f"Count {self.count}: " + self.stopwatch_time. Next a two line icon list item is created with the lap_time as text. The secondary text is calculated by the calculate_time_difference method, which subtracts the last recorded lap time from the current time. The list item is then added to the the list.

Running the application now, we get our functional stopwatch:

Stopwatch

All the code for this app is available in this Github repository.

In the next part of the tutorial, we look at how to create the timer so stay tuned!🙂

Top comments (2)

Collapse
 
islamimtiaz profile image
IslamImtiaz

Greetings, I am currently attempting to transform my python code into an APK, however, after converting it and installing the file, it unfortunately crashes right after the kivy logo appears. I am using google collab to convert my code into APK.
Spec file
Image description

main.py

from kivymd.uix.screen import MDScreen
from kivymd.app import MDApp
from kivy.uix.image import Image
from kivymd.uix.button import MDFillRoundFlatButton
from kivymd.uix.textfield import MDTextField
from kivymd.uix.label import MDLabel
from kivymd.uix.toolbar import MDTopAppBar
from kivymd.uix.dialog import MDDialog

class ConverterApp(MDApp):

    def flip(self):
        if self.state == 0:
            self.state = 1
            self.toolbar.title = 'CGPA calculator'
            self.GP.hint_text = 'Enter your current GPA'
            self.CH.hint_text = 'Enter your previous GPA'
        else:
            self.state = 0
            self.toolbar.title = 'GPA calculator'
            self.GP.hint_text = 'Enter your GP'
            self.CH.hint_text = 'Enter subject CH'

    def gpa(self, obj):
        gp_text = self.GP.text.strip()
        ch_text = self.CH.text.strip()
        # Check if GP and CH fields are not empty
        if not gp_text or not ch_text:
            # Show an error when GP and CH fields are empty
            dialog = MDDialog(title='Error',text='Both GP and CH fields are required',size_hint=(0.7, 1))
            dialog.open()
            return
        # Check if entered data is valid
        try:
            gp_values = [float(gp) for gp in gp_text.split(',')]
            ch_values = [float(ch) for ch in ch_text.split(',')]
        except ValueError:
            # Show an error when non-numeric value is entered
            dialog = MDDialog(title='Error',text='Invalid input! Please enter comma-separated numbers only',size_hint=(0.7, 1))
            dialog.open()
            return
        # Calculate GPA or CGPA
        if self.state == 0:
            x = sum(gp_values)
            y = sum(ch_values)
            if y == 0:
                # Show an error
                dialog = MDDialog(title='Error',text='Zero division error',size_hint=(0.7, 1))
                dialog.open()
            else:    
                c = x / y
                self.label1.text = str(c)
                self.label.text = "Your GPA is: "
        else:
            x = sum(gp_values)
            len1 = len(gp_values)
            y = sum(ch_values)
            len2 = len(ch_values)
            b = len1 + len2
            z = (x + y) / b
            self.label1.text = str(z)
            self.label.text = 'Your CGPA is: '

    def build(self):
        self.state = 0
        screen = MDScreen()
        # Top toolbar
        self.toolbar = MDTopAppBar(title="GPA Calculator")
        self.toolbar.pos_hint = {'top':1}
        self.toolbar.right_action_items = [['rotate-3d-variant', lambda x: self.flip()]]
        screen.add_widget(self.toolbar)
        # Logo
        screen.add_widget(Image(source ='gpr.png',size_hint=(0.8,1),pos_hint ={'center_x':0.5,'center_y':0.7}))
        # Collect user input
        self.GP = MDTextField(hint_text='Enter your GP',halign='center',size_hint=(0.8,1),pos_hint ={'center_x':0.5,'center_y':0.48},font_size=22)
        screen.add_widget(self.GP)
        self.CH = MDTextField(hint_text='Enter your CH',halign='center',size_hint=(0.8,1),pos_hint ={'center_x':0.5,'center_y':0.4},font_size=22)
        screen.add_widget(self.CH)
        # Secondary + Primary Label
        self.label = MDLabel(halign='center',pos_hint ={'center_x':0.5,'center_y':0.32},theme_text_color='Secondary')
        self.label1 = MDLabel(halign='center',pos_hint ={'center_x':0.5,'center_y':0.28},theme_text_color='Primary',font_style='H5')
        screen.add_widget(self.label)
        screen.add_widget(self.label1)
        # Convert Button
        self.button= MDFillRoundFlatButton(text='Result',font_size='17',pos_hint ={'center_x':0.5,'center_y':0.15})
        self.button.bind(on_press = self.gpa)
        screen.add_widget(self.button)

        return screen

ConverterApp().run()
Enter fullscreen mode Exit fullscreen mode

plz help me

Collapse
 
islamimtiaz profile image
IslamImtiaz

Greetings, I am currently attempting to transform my python code into an APK, however, after converting it and installing the file, it unfortunately crashes right after the kivy logo appears. I am using google collab to convert my code into APK. Please help!!
Spec file
Image description

main.py

from kivymd.uix.screen import MDScreen
from kivymd.app import MDApp
from kivy.uix.image import Image
from kivymd.uix.button import MDFillRoundFlatButton
from kivymd.uix.textfield import MDTextField
from kivymd.uix.label import MDLabel
from kivymd.uix.toolbar import MDTopAppBar
from kivymd.uix.dialog import MDDialog

class ConverterApp(MDApp):

    def flip(self):
        if self.state == 0:
            self.state = 1
            self.toolbar.title = 'CGPA calculator'
            self.GP.hint_text = 'Enter your current GPA'
            self.CH.hint_text = 'Enter your previous GPA'
        else:
            self.state = 0
            self.toolbar.title = 'GPA calculator'
            self.GP.hint_text = 'Enter your GP'
            self.CH.hint_text = 'Enter subject CH'

    def gpa(self, obj):
        gp_text = self.GP.text.strip()
        ch_text = self.CH.text.strip()
        # Check if GP and CH fields are not empty
        if not gp_text or not ch_text:
            # Show an error when GP and CH fields are empty
            dialog = MDDialog(title='Error',text='Both GP and CH fields are required',size_hint=(0.7, 1))
            dialog.open()
            return
        # Check if entered data is valid
        try:
            gp_values = [float(gp) for gp in gp_text.split(',')]
            ch_values = [float(ch) for ch in ch_text.split(',')]
        except ValueError:
            # Show an error when non-numeric value is entered
            dialog = MDDialog(title='Error',text='Invalid input! Please enter comma-separated numbers only',size_hint=(0.7, 1))
            dialog.open()
            return
        # Calculate GPA or CGPA
        if self.state == 0:
            x = sum(gp_values)
            y = sum(ch_values)
            if y == 0:
                # Show an error
                dialog = MDDialog(title='Error',text='Zero division error',size_hint=(0.7, 1))
                dialog.open()
            else:    
                c = x / y
                self.label1.text = str(c)
                self.label.text = "Your GPA is: "
        else:
            x = sum(gp_values)
            len1 = len(gp_values)
            y = sum(ch_values)
            len2 = len(ch_values)
            b = len1 + len2
            z = (x + y) / b
            self.label1.text = str(z)
            self.label.text = 'Your CGPA is: '

    def build(self):
        self.state = 0
        screen = MDScreen()
        # Top toolbar
        self.toolbar = MDTopAppBar(title="GPA Calculator")
        self.toolbar.pos_hint = {'top':1}
        self.toolbar.right_action_items = [['rotate-3d-variant', lambda x: self.flip()]]
        screen.add_widget(self.toolbar)
        # Logo
        screen.add_widget(Image(source ='gpr.png',size_hint=(0.8,1),pos_hint ={'center_x':0.5,'center_y':0.7}))
        # Collect user input
        self.GP = MDTextField(hint_text='Enter your GP',halign='center',size_hint=(0.8,1),pos_hint ={'center_x':0.5,'center_y':0.48},font_size=22)
        screen.add_widget(self.GP)
        self.CH = MDTextField(hint_text='Enter your CH',halign='center',size_hint=(0.8,1),pos_hint ={'center_x':0.5,'center_y':0.4},font_size=22)
        screen.add_widget(self.CH)
        # Secondary + Primary Label
        self.label = MDLabel(halign='center',pos_hint ={'center_x':0.5,'center_y':0.32},theme_text_color='Secondary')
        self.label1 = MDLabel(halign='center',pos_hint ={'center_x':0.5,'center_y':0.28},theme_text_color='Primary',font_style='H5')
        screen.add_widget(self.label)
        screen.add_widget(self.label1)
        # Convert Button
        self.button= MDFillRoundFlatButton(text='Result',font_size='17',pos_hint ={'center_x':0.5,'center_y':0.15})
        self.button.bind(on_press = self.gpa)
        screen.add_widget(self.button)

        return screen

ConverterApp().run()
Enter fullscreen mode Exit fullscreen mode
Collapse
 
drizzyovo profile image
Gaurav Malpedi

Can you please show me how to include a text file or a JSON file as a database. I wanna make a kivy desktop application in onefile. It's giving me error. Please help me out.