creating clients 2: interface components

adding custom graphical components to a client interface.

introduction
overview of this tutorial

extending the time user
providing methods for graphical support in the time user client.

extending the interface
preparing the basic interface for new components

displaying the time
custom component for graphical display of time

displaying the time log
custom component for graphical display of time log

running the examples
instructions for running the example applications covered in this tutorial

introduction

The basic client interface provided by the ClientGUI class in the svs_core.gui.clientgui module can be extended to provide additional custom components for an application. This tutorial adds two new components to the time user client developed in the previous tutorial displaying the results of the 'time' and 'timelog' commands graphically.

The source code for the examples in this tutorial is:

The source code for all tutorials can be downloaded with the tutorials-x.x.tar.gz package.


fig. 1.) custom graphical displays for time and timelog.

page contents

extending the time user

source:timeusergui.py

We will create a new class called GraphicTimeUser which extends the TimeUser class from the previous tutorial. The functionality is basically the same but many of the methods require changes to interact with the new graphics components. The module for the previous user needs to be imported along with the classes for supporting GUI clients in svs:

from svs_core.network.clientuser import GraphicalClient
from svs_core.gui.clientgui import ClientGUI
from timeuser import TimeUser, daysOfWeek

Example 1.) svs_core imports for graphical time user.

As we are using the Tkinter GUI toolkit we will also require some modules from that along with the Tkinter integration module from Twisted, and the math module for handling some calculations for the graphics:

from Tkinter import *
from twisted.internet import tksupport
import math

Example 2.) additional imports for graphical time user.

We also define some constants that will be used frequently in the graphic components:

deg2rad = math.pi/180.0
rad2deg = 180.0/math.pi
secondsAngle = 360.0 / 60
minutesAngle = 360.0 / 60
hoursAngle = 360.0 / 12
STATE_ACTIVE = 1
STATE_INACTIVE = 0

Example 3.) constants.

When we instantiate the GraphicTimeUser class we pass the name of the new GUI class we will be creating. Instantiation of this is handled by the parent GraphicalClient class:

class GraphicTimeUser(TimeUser):
    def __init__(self, name, passwd):
        TimeUser.__init__(self, name, passwd, guiClass=TimeUserGui)

Example 4.) constructor for graphic time user.

Only the 'handler' methods from TimeUser will need to be overidden, the ones that deal with displaying the data received from the network. In most cases all we do is pass the data onto the gui class for it to render. The parent version of each method is also called so we can see the data in the text console but this could be removed and the data just displayed visually. These are the methods for handling time and timelog data:

def handleTime(self, timeData):
    """
    Displays time returned from service.
    """
    TimeUser.handleTime(self, timeData) # call parent method
    self.gui.displayTime(timeData[3], timeData[4], timeData[5])
    self.getTimeLog()


def handleTimeLog(self, logData):
    """
    Displays log returned from service.
    """
    TimeUser.handleTimeLog(self, logData) # call parent method
    self.gui.displayLog(logData)

Example 5.) handling time and timelog data.

Aside from displaying the time, handleTime also makes a call to self.getTimeLog(). This way the timelog display is automatically updated whenever the user requests the time.

The new versions of the handlers for clients joining and leaving the network follow a similar pattern. These show up in the graphical display by changing the colour of the client's name on the display. Clients who are currently on the network are shown in black, those who are no longer on the network are shown in grey:

def handleClientJoined(self, data):
    """
    Responds to new client joining the network.
    """
    TimeUser.handleClientJoined(self, data)
    self.gui.setStateForClient(data['client_name'], STATE_ACTIVE)
    self.gui.displayLog()

def handleClientDeparted(self, data):
    """
    Responds to new client leaving the network.
    """
    TimeUser.handleClientDeparted(self, data)
    self.gui.setStateForClient(data['client_name'], STATE_INACTIVE)
    self.gui.displayLog()

Example 6.) handling client status changes.

One potential drawback in this is that when a client joins the network it does not know which other clients are currently active. This is because such information on clients is only sent out when they join and leave. This means that the display of client names might show some clients as being inactive rather than active simply because they have joined earlier. In order to fix this, the haveJoinedClusterGroup method of GenericClient has been overidden so that when the client has joined a network group it automatically retrieves a list of currently active clients. It does this through calling the getGroupMembers method of GenericClient:

def avatarResult_joinClusterGroup(self, group):
    """
    Handles result of joinClusterGroup action.
    """
    TimeUser.avatarResult_joinClusterGroup(self, group)
    self.getGroupMembers()

Example 7.) getting list of members after joining group.

The display of this information is normally handled by the handleGroupMemberList method of GenericClient. It is overidden in GraphicTimeUser to make sure the display of client names in the time log shows their correct state:

def handleGroupMemberList(self, groupList):
    """
    Deals with received list of group members.
    """
    TimeUser.handleGroupMemberList(self, groupList)
    for name in groupList:
        self.gui.setStateForClient(name, STATE_ACTIVE)
    self.getTimeLog()

Example 8.) setting up the display of client states.

page contents

extending the interface

The actual display of the information is handled by the TimeUserGui class which extends the basic ClientGUI class from svs_core.gui.clientgui:

class TimeUserGui(ClientGUI):
    def __init__(self, client):
        ClientGUI.__init__(self, client)

Example 9.) constructor for the new GUI class.

The new class overides the build method of ClientGUI adding in the new components it will use:

def build(self):
    """
    Creates interface components.
    """
    # make root window
    self.root = Tk()
    self.root.title('TimeUser: %s' % self.client.profile.name)
    self.root.geometry("%dx%d%+d%+d" % (400, 400, 0, 0))
    self.root.bind('', self.destroy)
    # register with twisted
    tksupport.install(self.root)
    # add time components
    self.timeFrame = Frame(self.root, height=180)
    self.timeFrame.pack(side=TOP, expand=YES, fill=BOTH)
    self.timeCanvas = TimeCanvas(self.timeFrame)
    self.timeCanvas.place(x=0, y=0, relwidth=0.5, relheight=1.0)
    self.logCanvas = LogCanvas(self.timeFrame)
    self.logCanvas.place(relx=0.5, y=0, relwidth=0.5, relheight=1.0)
    self.timeFrame.rowconfigure(0, weight=1)
    self.timeFrame.columnconfigure(0, weight=1)
    self.timeFrame.columnconfigure(1, weight=1)
    # add standard components
    self.buildStandardComponents()

Example 10.) build method with new graphic components.

The other methods in ClientGUI are those used by the client to display its data. These pass on the data to the actual graphical components which the client does not need to know about:

def displayTime(self, hours, mins, secs):
    """
    Draw graphical display of time.
    """
    self.timeCanvas.drawTime(hours, mins, secs)


def displayLog(self, logData=None):
    """
    Draw graphical display of log.
    """
    self.logCanvas.drawLog(logData)


def setStateForClient(self, clientName, state):
    """
    Sets state for client. This is used in
    displaying client names.
    """
    self.logCanvas.clientStates[clientName] = state

Example 11.) methods called by client.

The display components are two custom classes which are the most involved in the timeusergui module: TimeCanvas and LogCanvas. These are described below.

page contents

displaying the time

The time is displayed by a custom graphical component called TimeCanvas which displays a simple clock face:


fig. 2.) clock face display.

TimeCanvas acts as a wrapper around the Canvas class from the Tkinter toolkit. It has three properties which are used to maintain the time values for when the canvas is redrawn:

class TimeCanvas:
    def __init__(self, root):
        self.canvas = Canvas(root, bg='#ccccaa', borderwidth=0, highlightthickness=0)
        self.canvas.bind('<Configure>', self.canvasAdjusted)
        self.hours = 0
        self.mins = 0
        self.secs = 0

Example 12.) constructor for TimeCanvas.

The background colour for the canvas is defined by the 'bg' parameter which is set to '#ccccaa'. The bind method on the canvas is used to connect the locally defined canvasAdjusted method to Tkinter's 'Configure' event. This is called, for example, when the client window is first drawn onscreen or its size is altered. For details on event handling in Tkinter see a more in depth tutorial such as:

The positioning of the TimeCanvas components is handled by the pack and place methods. These are just wrappers which pass the layout settings onto the Tkinter canvas object. See the Tkinter tutorials for more information on how to work with these.

def pack(self, **kwargs):
    """
    Provides wrapper for pack method on self.canvas.
    """
    self.canvas.pack(kwargs)

def place(self, **kwargs):
    """
    Provides wrapper for place method on self.canvas.
    """
    self.canvas.place(kwargs)

Example 13.) pack and place methods.

The clock face is drawn in proportion to the size of the window, so it needs to be re-scaled if the window size is adjusted. This is what the canvasAdjusted method handles. It calculates the width, height and other scaled dimensions required to draw the clock face, and then redraws it:

def canvasAdjusted(self, args=None):
    """
    Responds to main window being adjusted in size.
    """
    self.canvas.update_idletasks()
    self.width = self.canvas.winfo_width()
    self.height = self.canvas.winfo_height()
    self.centreX = self.width / 2
    self.centreY = self.height / 2
    # clock drawing
    if self.height > self.width:
        self.clockRadius = self.centreX * 0.6
    else:
        self.clockRadius = self.centreY * 0.6
    self.bigHandRadius = self.clockRadius * 0.9
    self.smallHandRadius = self.clockRadius * 0.6
    self.hourTickInnerRadius = self.clockRadius * 0.8
    self.hourTickOuterRadius = self.clockRadius
    # redraw
    self.drawClockFace()
    self.drawClockHands()

Example 14.) scaling the drawing dimensions.

The clock face only needs to be drawn when the canvas changes size, whereas the clock hands need to be drawn when a new time value is displayed. There are, therefore, two separate methods for displaying these.

When the client receives new time data, the data is passed to the TimeUserGui which in turn passes it onto the TimeCanvas to actually draw it it on screen by calling the drawTime method:

def drawTime(self, hours, mins, secs):
    """
    Draws time display.
    """
    self.hours = hours
    self.mins = mins
    self.secs = secs
    self.drawClockHands()

Example 15.) handling new time data.

The clock face is simply a circle with four tick marks, at 12, 3, 6 and 9 o'clock. The method first clears any previous drawing of the clock face and then draws its elements using the drawing methods on the Tkinter canvas. The lower method on canvas sets these elements to the bottom of the drawing stack so that the hands, when they are drawn, will be on top of them. The various values set in canvasAdjusted defines the dimensions of the shapes:

def drawClockFace(self):
    """
    Draws clock face.
    """
    self.canvas.delete('CLOCK_FACE')
    self.canvas.delete('CLOCK_TICK')
    # background
    self.canvas.create_oval(self.centreX - self.clockRadius,
        self.centreY - self.clockRadius,
        self.centreX + self.clockRadius,
        self.centreY + self.clockRadius,
        outline='white', fill=None, width=1, tag='CLOCK_FACE')
    # hour ticks
    # 12 o'clock
    self.canvas.create_line(self.centreX,
        self.centreY - self.hourTickInnerRadius,
        self.centreX,
        self.centreY - self.hourTickOuterRadius,
        fill='white', tag='CLOCK_TICK')
    # 3 o'clock
    self.canvas.create_line(self.centreX + self.hourTickInnerRadius,
        self.centreY,
        self.centreX + self.hourTickOuterRadius,
        self.centreY,
        fill='white', tag='CLOCK_TICK')
    # 6 o'clock
    self.canvas.create_line(self.centreX,
        self.centreY + self.hourTickInnerRadius,
        self.centreX,
        self.centreY + self.hourTickOuterRadius,
        fill='white', tag='CLOCK_TICK')
    # 9 o'clock
    self.canvas.create_line(self.centreX - self.hourTickInnerRadius,
        self.centreY,
        self.centreX - self.hourTickOuterRadius,
        self.centreY,
        fill='white', tag='CLOCK_TICK')
    self.canvas.lower('CLOCK_FACE')
    self.canvas.lower('CLOCK_TICK')

Example 16.) drawing the clock face.

Drawing the clock hands is quite similar. The angle at which the lines representing each hand are drawn is calculated from the time value using a combination of the constants defined earlier and the dimensions set by canvasAdjusted:

def drawClockHands(self):
    """
    Draws clock hands.
    """
    self.canvas.delete('CLOCK_HAND')
    # seconds hand
    drawAngle = ((secondsAngle * self.secs) - 90) % 360
    endX = self.centreX + (self.bigHandRadius * math.cos(deg2rad * drawAngle))
    endY = self.centreY + (self.bigHandRadius * math.sin(deg2rad * drawAngle))
    self.canvas.create_line(self.centreX,
        self.centreY,
        endX,
        endY,
        fill='red', tag='CLOCK_HAND')
    # hour hand
    drawAngle = ((hoursAngle * self.hours) - 90) % 360
    endX = self.centreX + (self.smallHandRadius * math.cos(deg2rad * drawAngle))
    endY = self.centreY + (self.smallHandRadius * math.sin(deg2rad * drawAngle))
    self.canvas.create_line(self.centreX,
        self.centreY,
        endX,
        endY,
        fill='black', tag='CLOCK_HAND')
    # minute hand
    drawAngle = ((minutesAngle * self.mins) - 90) % 360
    endX = self.centreX + (self.bigHandRadius * math.cos(deg2rad * drawAngle))
    endY = self.centreY + (self.bigHandRadius * math.sin(deg2rad * drawAngle))
    self.canvas.create_line(self.centreX,
        self.centreY,
        endX,
        endY,
        fill='black', tag='CLOCK_HAND')
    self.canvas.lift('CLOCK_HAND')

Example 17.) drawing the clock hands.

page contents

displaying the time log

The LogCanvas class draws a simple horizontal bargraph to display the number of requests each client has made to the time service. Each bar is labelled with the name of the client it represents. Clients that are currently active on the network have their labels drawn in black, those that are not active are drawn in grey:


fig. 3.) log display.

Its structure is basically similar to TimeCanvas and it also acts as a wrapper around Tkinter's Canvas class. It has two properties, used to store the log data and client states:

class LogCanvas:
    def __init__(self, root):
        self.canvas = Canvas(root, bg='#ccccaa', borderwidth=0, highlightthickness=0)
        self.canvas.bind('<Configure>', self.canvasAdjusted)
        self.logData = None
        self.clientStates = {}

Example 18.) constructor for LogCanvas.

The class provides wrappers for the pack and place methods, as well as scaling the drawing dimensions with canvasAdjusted:

def canvasAdjusted(self, args=None):
    """
    Responds to main window being adjusted in size.
    """
    self.canvas.update_idletasks()
    self.width = self.canvas.winfo_width()
    self.height = self.canvas.winfo_height()
    self.centreX = self.width / 2
    self.centreY = self.height / 2
    # log drawing
    self.logWidth = self.width * 0.8
    self.logHeight = self.height * 0.8
    self.logOffsetX = (self.width - self.logWidth) / 2
    self.logOffsetY = (self.height - self.logHeight) / 2
    # redraw
    self.drawLog()

Example 19.) scaling drawing dimensions.

As with TimeCanvas there is a method to receive and draw new log data:

def drawLogData(self, logData):
    """
    Receives and draws log data.
    """
    self.logData = logData
    self.drawLog()

Example 20.) receiving log data.

The log is drawn in a similar fashion to the clock, any previous version is first deleted. The method checks that there is data to draw, and if not ends at that point. If there is data, the client names are sorted into alphabetical order, and then their values drawn by looping through the entries in the log, drawing each one, shifting them down the screen as they go:

def drawLog(self):
    """
    Draws log display.
    """
    # clear canvas elements
    self.canvas.delete('LOG_NAME')
    self.canvas.delete('LOG_BAR')
    # if there is no data, don't draw
    if not self.logData:return
    # prepare to draw log
    logSize = len(self.logData)
    barWidth = 12
    barCount = 0
    # sort client names
    clients = self.logData.keys()
    clients.sort()
    # draw bars
    for clientname in clients:
        minX = 0
        minY = self.logOffsetY + (barWidth * barCount)
        maxX = minX + (self.logData[clientname] * 10)
        maxY = minY + barWidth
        self.canvas.create_rectangle(minX, minY, maxX, maxY, outline='#ffffff', fill='#ffffff', width=1, tag='LOG_BAR')
        clientState = self.clientStates.get(clientname, STATE_INACTIVE)
        if clientState == STATE_ACTIVE:
            labelColour = 'black'
        else:
            labelColour = 'dark grey'
        self.canvas.create_text(minX, minY + 6, fill=labelColour, text=clientname, anchor='w' , tag='LOG_NAME')
        barCount += 1

Example 21.) drawing the log as a bargraph.

page contents

running the examples

Working version of the examples are in the 'creating_clients' directory of the 'tutorials'.

Step 1.) start the server:

./server.py

This will run as a command line application with no user interface.

Step 2.) in a new terminal, start the service client:

./timeservice.py

This will run as a command line application with no user interface.

Step 3.) in a new terminal, start a user client:

./timeusergui.py time_user_1 p@ssw0rd

To start a second user, again in a new terminal:

./timeusergui.py time_user_2 p@ssw0rd

Additional clients can be started in the same way, just by increasing the number in the name ('time_user_3', 'time_user_4', etc). The server file is configured to accept up to 6 such clients.

Step 4.) when the user client has launched, enter the 'time' command. The result from the service will be displayed in the console. Do the same from the other user client. Make several requests.

Step 5.) to see a listing of how many times each client has requested the time, enter the 'timelog' command.

Step 6.) enter the 'quit' command in the second client. It's name in the log display of the still active first client will go grey.

Step 7.) relaunch the second client. It's name in the log display of the first client will turn black again.

Step 8.) enter 'quit' to close each client and disconnect from the network. Enter 'control-c' in the terminal to shutdown the time service and the server.

page contents

creating SVS clients
part 1: time service and user
part 2: interface components
part 3: notification system