Counting time
Although both stopwatch and the regular clock ultimately just display time, they are completely different in terms of functionality. Wall clock is a strictly increasing monotonic function, while stopwatch time can be paused and reset, decreasing the counter. More practically, the difference is that the operating system readily exposes its internal wall clock to Python, both directly as a datetime
object and transparently in the case of the strftime()
function. The latter can be called without a datetime
argument to format the current time, which is exactly what we need for a wall clock display.
For the task of creating a stopwatch, we will need to build our own, non-monotonic time counter first. This is easily achieved without using Python's time functions altogether, thanks to Kivy's Clock.schedule_interval
event handler that accepts the time passed between calls as a parameter. This is just what the nap
parameter does in the following code:
def on_start(self): Clock.schedule_interval(self.update, 0.016) def update(self, nap): pass
Time is measured in seconds, that is, if the app is running at 60 fps and calls our function every frame, the average nap will be 60−1= 0.016(6).
With this parameter in place, keeping track of the time passed is simple and can be achieved with a simple increment:
class ClockApp(App): sw_seconds = 0 def update(self, nap): self.sw_seconds += nap
This timer we just created isn't, by definition, a stopwatch since right now, there is no way for the user to actually stop it. However, let's update the display with the incrementing time first so that we can see the effect of controls immediately when implementing them.
Formatting the time for stopwatch
For the main time display, formatting is easy because the standard library function strftime
provides us with a number of readily available primitives to convert a datetime
object into a readable string representation, according to the provided format string.
This function has a number of limitations:
- It only accepts Python
datetime
objects (while for the stopwatch, we only have a floating-point number of seconds passed,sw_seconds
) - It has no formatting directive for a decimal fraction of seconds
The former datetime
limitation can be easily circumvented: we could cast our sw_seconds
variable to datetime
. But the latter deficiency makes this unnecessary, as we want to end our notation with fractions of a second (exact to 0.01 sec), so strftime
formatting just won't cut it. Hence, we implement our own time formatting.
Computing values
First, we need to compute the necessary values: minutes, seconds, and fractions of a second. The math is easy; here's the one-liner for minutes and seconds:
minutes, seconds = divmod(self.sw_seconds, 60)
Note the use of the divmod
function. This is a shorthand for the following:
minutes = self.sw_seconds / 60 seconds = self.sw_seconds % 60
While being more concise, the divmod
version should also perform better on most Python interpreters, as it performs the division just once. On today's machines, the floating-point division is quite effective, but if we run a whole lot of such operations every frame, like in a video game or simulation, the CPU time will quickly add up.
Tip
Generally, the author tends to disagree with the oft-chanted mantra about premature optimization being evil; many bad practices that lead to choppy and substandard performance can and should be easily avoided without compromising on code quality, and not doing so is by all means premature pessimization.
Also note that both minutes
and seconds
values are still floating-point, so we will need to convert them to integers before we print them: int(minutes)
and int(seconds)
.
Now all that's left is hundredths of seconds; we can compute them like this:
int(seconds * 100 % 100)
Putting a stopwatch in place
We have all the values; let's join them together. Formatting strings in Python is quite a common task, and contrary to The Zen of Python commandment that reads, "There should be one—and preferably only one—obvious way to do it" (https://www.python.org/dev/peps/pep-0020/), there are several common idioms for string formatting. We will use one of the simplest, operator %, which is somewhat similar to the sprintf()
function commonly found in other programming languages:
def update_time(self, nap): self.sw_seconds += nap minutes, seconds = divmod(self.sw_seconds, 60) self.root.ids.stopwatch.text = ( '%02d:%02d.[size=40]%02d[/size]' % (int(minutes), int(seconds), int(seconds * 100 % 100)))
Since we have fractions of a second now, the refresh frequency of 1 fps that we used earlier isn't sufficient anymore. Let's set it to 0 instead so that our update_time
function will be called for every frame:
Clock.schedule_interval(self.update_time, 0)
Tip
Today, most displays run at a refresh rate of 60 fps, while our value is exact to 1/100 sec, that is, changes 100 times per second. While we could have attempted to run our function at exactly 100 fps, there is absolutely no reason to do it: for users, it isn't possible to see the difference on commonly available hardware, as the display will still update no more than 60 times per second anyway.
That said, most of the time your code should work independently of a frame rate, as it relies on the user's hardware, and there is no way to predict what machine your application will end up on. Even today's smartphones have wildly different system specs and performance, let alone laptops and desktop computers.
And that's it; if we run the application now, we'll see an incrementing counter. It lacks interactivity yet, and this will be our next target.