Skip to content

Creating A Banyan GUI Component

This section will demonstrate how to create a Banyan compatible GUI client for the simple echo server example.

Both Banyan and GUI frameworks are implemented using event loops. Because only one event loop can run within any given thread of execution, we need to find a way for both event loops to coexist.

One solution is to create a multi-threaded application with each event loop running in its own thread. This, however, introduces unnecessary complexity and complicates the testing and debugging efforts.

If you are using a GUI that provides a callback hook to allow you to link your own code into the GUI's event loop, a much simpler solution is at hand. The Tkinter GUI framework provides such a hook, and we will integrate Banyan into our GUI using this technique. Note that many other GUI frameworks also provide a callback hook. The techniques shown here may also be applied similarly to those frameworks.
The remi library, for example, uses a method called idle to provide the callback hook.

The Tkinter callback hook method is called after. We will pass in 2 parameters to after, a sleep time in milliseconds, and the user callback function called when the sleep time expires.

The example code shown below illustrates integrating the Banyan receive_loop into the GUI event loop using the after method. Some discussion of Tkinter will be provided for clarity. However, a detailed discussion of the Tkinter framework is beyond the scope of this document. This ebook offers a concise and useful discussion of building GUIs with Tkinter. An online version may be found here.

Running The Example

Make sure that the backplane and server are running. Next, start tk_echo_client.py. After starting the GUI client code, you should see the GUI shown at the top of this section appear on your screen, and the console should display something similar to this:

If you press the "Send Messages" button, you should the "Messages Sent" field update to 10. If you start the Monitor before pressing the "Send Messages" button, you can verify the messages' contents.

Exploring The Example Code

The code is shown below.

Importing Tkinter

Lines 27 through 34 handle the Tkinter differences for Python 2 and 3 and allow a single code source to service both Tkinter versions.

Lines 37 through 39 import not only the BanyanBase class but MessagePack and ZeroMQ as well. Our GUI component needs direct access to these packages because portions of the Python Banyan receive_loop will be placed in the Tkinter mainloop.

Lines 62 through 68 are in preparation for initializing the parent BanyanBase class on line 71.

Lines 77 and 78 subscribe to all topics of interest.

Breaking Into The GUI Event Loop

Line 149 uses the Tkinter after method. The first parameter specifies a delay in milliseconds before the callback function specified by the second parameter is called.

In this example, the callback function is the get_message method defined on line 156. This method is essentially the same code normally run in the BanyanBase receive_loop. It checks to see if there any Banyan messages to process, and if there are, it processes them. If there are no messages available, line 169 re-arms the Tkinter after method to check for Banyan messages within the GUI event loop.

     1  #!/usr/bin/env python
     2
     3  """
     4  tk_echo_client.py
     5
     6  Copyright (c) 2016-2019 Alan Yorinks All right reserved.
     7
     8   Python Banyan is free software; you can redistribute it and/or
     9   modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
    10   Version 3 as published by the Free Software Foundation; either
    11   or (at your option) any later version.
    12   This library is distributed in the hope that it will be useful,
    13   but WITHOUT ANY WARRANTY; without even the implied warranty of
    14   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    15   General Public License for more details.
    16
    17   You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
    18   along with this library; if not, write to the Free Software
    19   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
    20
    21  """
    22  from __future__ import unicode_literals
    23
    24  import time
    25
    26  # python 2/3 compatibility
    27  try:
    28      from Tkinter import *
    29      from Tkinter import font
    30      from Tkinter import ttk
    31  except ImportError:
    32      from Tkinter import *
    33      import tkFont as font
    34      import ttk
    35
    36  import sys
    37  import umsgpack
    38  import zmq
    39  from python_banyan.banyan_base import BanyanBase
    40
    41
    42  # noinspection PyMethodMayBeStatic,PyUnresolvedReferences,PyUnusedLocal
    43  class TkEchoClient(BanyanBase):
    44      """
    45      A graphical echo client.
    46      """
    47
    48      def __init__(self, topics=['reply'], number_of_messages=10,
    49                   back_plane_ip_address=None, subscriber_port='43125',
    50                   publisher_port='43124', process_name='Banyan Echo Client'):
    51          """
    52
    53          :param topics: A list of topics to subscribe to
    54          :param number_of_messages: Default number of echo messages to send
    55          :param back_plane_ip_address:
    56          :param subscriber_port:
    57          :param publisher_port:
    58          :param process_name:
    59          """
    60
    61          # establish some banyan variables
    62          self.back_plane_ip_address = back_plane_ip_address
    63          self.subscriber_port = subscriber_port
    64          self.publisher_port = publisher_port
    65
    66          # subscribe to the topic
    67          if topics is None:
    68              raise ValueError('No Topic List Was Specified.')
    69
    70          # initialize the banyan base class
    71          super(TkEchoClient, self).__init__(back_plane_ip_address=back_plane_ip_address,
    72                                             subscriber_port=subscriber_port,
    73                                             publisher_port=publisher_port,
    74                                             process_name=process_name)
    75
    76          # subscribe to all topics specified
    77          for x in topics:
    78              self.set_subscriber_topic(str(x))
    79
    80          # setup root window
    81          self.root = Tk()
    82          # create content window into which everything else is placed
    83
    84          self.root.title(process_name)
    85          self.content = ttk.Frame(self.root, borderwidth=5,
    86                                   relief="sunken", padding=12)
    87
    88          # use a grid layout
    89          self.content.grid(column=0, row=0, sticky=(N, S, E, W))
    90
    91          self.content.columnconfigure(0, weight=1)
    92          self.content.rowconfigure(0, weight=1)
    93
    94          # setup some display variables
    95
    96          # messages to be sent
    97          self.messages_to_be_sent = StringVar()
    98          self.messages_to_be_sent.set('10')
    99
   100          # messages sent count
   101          self.messages_sent = StringVar()
   102          self.message_sent_count = 0
   103          self.messages_sent.set(str(self.message_sent_count))
   104
   105          # set up font variant
   106          self.larger_font = font.Font(size=12)
   107
   108          # add the widgets
   109          ttk.Label(self.content, font=self.larger_font, text="Messages To Be Sent").grid(column=3, row=1, sticky=W)
   110          ttk.Label(self.content, font=self.larger_font, text="Messages Sent").grid(column=3, row=2, sticky=W)
   111
   112          style = ttk.Style()
   113          style.configure("BW.TLabel", foreground="black", background="white")
   114
   115          ttk.Label(self.content, font=self.larger_font,
   116                    textvariable=self.messages_sent,
   117                    width=5,
   118                    anchor=E, justify=RIGHT, style="BW.TLabel").grid(column=2, row=2, sticky=W)
   119
   120          self.to_send_entry = ttk.Entry(self.content, width=5,
   121                                         font=self.larger_font,
   122                                         textvariable=self.messages_to_be_sent,
   123                                         justify='right')
   124          self.to_send_entry.grid(column=2, row=1, sticky=(W, E))
   125
   126          s = ttk.Style()
   127          s.configure('my.TButton', font=self.larger_font)
   128
   129          self.send_button = ttk.Button(self.content, text="Send Messages",
   130                                        command=self.send, style='my.TButton')
   131          self.send_button.grid(column=4, row=3, sticky=W)
   132
   133          for child in self.content.winfo_children():
   134              child.grid_configure(padx=20, pady=5)
   135
   136          self.to_send_entry.focus()
   137          self.root.bind('<Return>', self.send)
   138
   139          self.number_of_messages = number_of_messages
   140
   141          # sequence number of messages
   142          self.message_number = self.number_of_messages
   143
   144          # send the first message - make sure that the server is already started
   145          # self.publish_payload({'message_number': self.message_number}, 'echo')
   146          self.message_number -= 1
   147          self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
   148
   149          self.root.after(1, self.get_message)
   150
   151          try:
   152              self.root.mainloop()
   153          except KeyboardInterrupt:
   154              self.on_closing()
   155
   156      def get_message(self):
   157          """
   158          This method is called from the tkevent loop "after" call. It will poll for new zeromq messages
   159          :return:
   160          """
   161          try:
   162              data = self.subscriber.recv_multipart(zmq.NOBLOCK)
   163              self.incoming_message_processing(data[0].decode(), umsgpack.unpackb(data[1]))
   164              self.root.after(1, self.get_message)
   165
   166          except zmq.error.Again:
   167              try:
   168                  time.sleep(.0001)
   169                  self.root.after(1, self.get_message)
   170
   171              except KeyboardInterrupt:
   172                  self.root.destroy()
   173                  self.publisher.close()
   174                  self.subscriber.close()
   175                  self.my_context.term()
   176                  sys.exit(0)
   177          except KeyboardInterrupt:
   178              self.root.destroy()
   179              self.publisher.close()
   180              self.subscriber.close()
   181              self.my_context.term()
   182              sys.exit(0)
   183
   184      def incoming_message_processing(self, topic, payload):
   185          # When a message is received and its number is zero, finish up.
   186          if self.message_number == 0:
   187              self.messages_sent.set(str(self.number_of_messages))
   188
   189          # bump the message number and send the message out
   190          else:
   191              self.message_number -= 1
   192              self.message_sent_count += 1
   193              self.messages_sent.set(str(self.message_sent_count))
   194
   195              # account for python2 vs python3 differences
   196              if sys.version_info[0] < 3:
   197                  self.publish_payload({'message_number': self.message_number}, 'echo'.encode())
   198              else:
   199                  self.publish_payload({'message_number': self.message_number}, 'echo')
   200
   201
   202      def send(self, *args):
   203          msgs = self.to_send_entry.get()
   204          # reset the sent count variables to zero
   205          self.message_sent_count = 0
   206          self.messages_sent.set(str(self.message_sent_count))
   207
   208          # set current message number to the number of messages to be sent
   209          self.message_number = int(msgs)
   210
   211          # update the number of messages to be sent
   212          self.number_of_messages = int(msgs)
   213
   214          # account for python2 vs python3 differences
   215          if sys.version_info[0] < 3:
   216              self.publish_payload({'message_number': self.message_number}, 'echo'.encode())
   217          else:
   218              self.publish_payload({'message_number': self.message_number}, 'echo')
   219
   220      def on_closing(self):
   221          """
   222          Destroy the window
   223          :return:
   224          """
   225          self.clean_up()
   226          self.root.destroy()
   227
   228
   229  def gui_client():
   230      TkEchoClient()
   231
   232
   233  if __name__ == '__main__':
   234      gui_client()



Copyright (C) 2017-2020 Alan Yorinks All Rights Reserved