Creating GUI extra's for Mushclient: Part 2

by Unknown

Back to Mechanic's Corner.

Unknown2005-02-06 23:11:45
This is the second part of the tutorial on creating GUI components for Mushclient in Python. I presume that you have completed the first part before starting with this one, so I won't repeat any of the stuff that was said there. In this part we'll provide our Buttons application with the mechanism for communicating with Mushclient. As was said in the beginning of the first part of this tutorial - we are going to use the UDP network protocol to establish communication. This part will be longer than the first one, and is more complicated. Before, we have the wxPython library do pretty much everything for us, now we'll need to get our hands dirty and endulge in some manual labour.

Python makes programming network applications fairly simple. UDP is a "connectionless" protocol, meaning that unlike the one that you use for accessing Lusternia, you don't need to establish and maintain a connection to be able to communicate via UDP. I don't remember what exactly UDP stands for; D and P are Datagram and Protocol respectively, not sure about U though, probably Unbound. In any case, it doesn't matter. What matters is that we need to make it work. We will need a client and a server, the first to send data to Mushclient, the second to accept data coming from it. Since the client is simpler, we'll start with that.

First we need yet another import. Add this line to the two that are already at the top of your script:

CODE

import socket


And while you are at it, add another import that we'll need for the server:

CODE

import thread


Back to the client. We'll implement it as another class that will be later instantiated inside the MainWindow one. Here's the full code for that class:

CODE

class UDPClient(object):
   def __init__(self, address="127.0.0.1", port=4311)
       self.host = address
       self.port = port
       self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)

   def send(self, data):
       self.sock.sendto("Buttons::" + data, (self.host, self.port))


The two arguments (beside "self") that __init__ accepts are address and port number, and have default values of 127.0.0.1 (local machine) and 40311, respectively. This means that we won't need to provide any arguments when instantiating the class, unless we want it to use a different address or port number. In this example, we won't. The __init__ method then saves the address and port in self.host and self.port properties, respectively, and creates a socket. The socket, as is evident from the arguments passed to the socket.socket() method, uses the Internet address family (socket.AF_INET), exchanges datagram packets (socket.SOCK_DGRAM), and operates according to the UDP protocol (socket.IPPROTO_UDP). The rest is none of our business. The send() method defined on instances of this class simply sends whatever data you pass to it as an argument to the saved address and port, through the socket we created during instantiation. That's it for the client part.

The server is somewhat more complicated but not by much. The main difference is that in order to be able to receive data from the network, the server must continuously listen to a network port on the local machine. The only way to implement this is through an endless loop, but an endless loop will most definitely lock up the program and it won't respond to button clicks. In order to solve this problem we'll use threading. If you don't know what that means, then threading is basically a way to run two tasks simultaneously within the same process. In our case, we'll have our GUI app run in the so-called main thread and the UDP server in the secondary thread. Here's the entire class for the server:

CODE

class UDPserver(object):
   def __init__(self, bound_to, address='', port=4222, buffer=1024):
       self.bound_to = bound_to
       self.host = address
       self.port = port
       self.buffer = buffer
       self.keep_alive = False
       self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
       self.sock.bind((self.host, self.port))
       self.start_thread()

   def start_thread(self):
       self.keep_alive = True
       thread.start_new_thread(self.run_thread, ())
       return

   def run_thread(self):
       while self.keep_alive:
           data,addr = self.sock.recvfrom(self.buffer)
           if not data: break
           evt = MyEvent(myEVT_GOT_DATA, wx.ID_ANY, data)
           wx.PostEvent(self.bound_to, evt)
       self.sock.close()
       return

   def stop_thread(self):
       self.keep_alive = False


The __init__ for this is very much similar to the one of the UDPClient class. Here we also provide a buffer size for the data we read from the network, we set this to 1024 bytes by default, which should be sufficient for the data we expect to receive. You might also notice that address is now an empty string by default, this is a peculiarity of the socket lib - the server treats an address value of "" as "localhost", while the client will yield an error if you try that trick with it, which is why we explicitely provide "127.0.0.1" for the client. There's also the self.keep_alive property here, this is used to control the server thread, we'll set this to True when starting the server, and set it back to False when we want to stop it. Finally the line "self.sock.bind(...)" binds the socket to the specified address/port pair.

There are also 3 more methods in this class. The first one - start_thread() - launches the server, the logic for which is contained inside the run_thread() method. You can see that the server is started in a separate thread from the:

CODE
thread.start_new_thread(self.run_thread, ())


line. Also, notice that we set the self.keep_alive property to True before starting the server. The stop_thread() method allows us to stop the server by setting this property to False. Finally, the run_thread() method. This is the most interesting part of the class, and the one that contains actually useful code:

CODE

   def run_thread(self):
       while self.keep_alive:
           data,addr = self.sock.recvfrom(self.buffer)
           if not data: break
           evt = MyEvent(myEVT_GOT_DATA, wx.ID_ANY, data)
           wx.PostEvent(self.bound_to, evt)
       self.sock.close()
       return


Here we see a "while" loop which runs for as long as the self.keep_alive property is True. On each iteration this loop tries to read data from the socket we created in the __init__ method into our buffer. If no data can be read, it will automatically stop itself and the thread will die. Next comes the interesting part. The next two lines are:

CODE

evt = MyEvent(myEVT_GOT_DATA, wx.ID_ANY, data)
wx.PostEvent(self.bound_to, evt)


That's obviously wxPython stuff, and an explanation is required to clarify what it does here. While threading is a convenient tool, and is really simple to use in Python, it does have some problems. The main one is that when two threads try to access the same object or value, there's always a chance that they'll do so in a disorderly fashion. In our case, we have two threads with an endless loop running in each of them: we have the GUI app that spins the MainLoop(), checking for events, messages, etc on each iteration, and we have the server thread which tries to read data from a network socket on each iteration. But the whole point of having the server thread is to accept the data from the network and pass it on to the GUI thread. The most obvious way of doing that is to call a method of the GUI object each time the data arrives. The problem is that there are 9 chances out of 10 that when we do so, the GUI's main loop will be busy doing stuff of its own, and the whole thing will come crashing down on our heads. This is what we are trying to avoid in those two lines of code. wxPython provides a great means of "synchronizing" access in a threaded environment, and those means are again - Events. Events are processed on each iteration of the app's main loop, which checks whether any of them occured since the last iteration and need to be delivered to their handler methods (or rather - handler methods need to be called in response to those events). So if we fire an event from our server thread, then it won't be processed until the main loop is ready to process it.No conflict - no problem. Therefore, in the first of those two lines, we create a new event from the MyEvent class (which will be defined and explained shortly), and in the second line we "post" that event for further processing by the application's MainLoop using the wx.PostEvent function. Once the while loop stops running, we close the socket and exit.

Next we need to create that custom event that the UDP server will be using to notify the application of data arriving. Here's the code needed to define that event. Put this after all the imports but before the MainWindow class:

CODE

myEVT_GOT_DATA = wx.NewEventType()
EVT_GOT_DATA = wx.PyEventBinder(myEVT_GOT_DATA, 1)


class MyEvent(wx.PyCommandEvent):
   def __init__(self, evtType, id, data):
       wx.PyCommandEvent.__init__(self, evtType, 1)
       self.data = data


The first two lines are just standard code needed to define and create a new event. What happens in those two lines is mostly above me, and never really interested me that much to begin with, so I'll just say: that's how you do it. Then comes the class that creates the actual event objects. This one allows you to implement the event, giving it any properties and/or methods that you might need. In this particular case, we don't need our EVT_GOT_DATA event to do anything fancy - it should simply contain a string with the data that our server received and is passing on to the app, so we define the "data" property (self.data = data) where this string will be stored. And it's all done. An example of using this event you have already seen above, in the UDPServer class. Now we need to also work out a way of catching this event inside our MainWindow class.

In the previous part of this tutorial you have already seen how an event is captured. It won't be any different now, only instead of a EVT_BUTTON event defined by the wx library, we'll be catching a custom event which we defined ourselves. Here's the new version of the MainWindow class, set up to capture an EVT_GOT_DATA event into a special method:

CODE

class MainWindow(wx.MiniFrame):
   def __init__(self, title, pos=wx.DefaultPosition, size=(174,190),
                style=wx.STAY_ON_TOP|wx.DEFAULT_FRAME_STYLE^wx.RESIZE_BORDER):
       wx.MiniFrame.__init__(self, None, -1, title, pos, size, style)

       panel = wx.Panel(self, -1)
       sizer = wx.BoxSizer(wx.VERTICAL)
       sizer.Add(panel, 1, wx.EXPAND)
       self.SetSizer(sizer)
       self.SetAutoLayout(True)
       sizer.Fit(self)
       self.SetSize(size)

       self.AddButton(panel, 10, "NW", (5,5), (40,40))
       self.AddButton(panel, 20, "N", (47, 5), (40,40))
       self.AddButton(panel, 30, "NE", (89, 5), (40,40))
       self.AddButton(panel, 40, "W", (5, 47), (40,40))
       self.AddButton(panel, 50, "E", (89, 47), (40, 40))
       self.AddButton(panel, 60, "SW", (5, 89), (40,40))
       self.AddButton(panel, 70, "S", (47,89), (40,40))
       self.AddButton(panel, 80, "SE", (89,89), (40,40))
       self.AddButton(panel, 90, "UP", (131,5), (30,61))
       self.AddButton(panel, 100, "D", (131, 68), (30, 61))
       self.AddButton(panel, 110, "IN", (5, 131), (76,30))
       self.AddButton(panel, 120, "OUT", (83, 131), (76, 30))

       self.Bind(EVT_GOT_DATA, self.OnData)
       
       self.Show(True)

   def OnClick(self, evt):
       pass

   def OnData(self, evt):
       pass

   def AddButton(self, parent, id, label, position, size):
       b = buttons.GenButton(parent, id, label, position, size)
       self.Bind(wx.EVT_BUTTON, self.OnClick, b)
       b.SetFont(wx.Font(10, wx.SWISS, wx.NORMAL, wx.NORMAL, False))
       b.SetBezelWidth(3)
       b.SetForegroundColour(wx.Colour(128,0,64))
       b.SetUseFocusIndicator(False)


I've added a self.Bind() call to the __init__ method (above the last line in it, the one with "self.Show(True)") and a new method right bellow the OnClick() one - OnData(). This is what the EVT_GOT_DATA is bound to, and just as its twin - OnClick(), it doesn't do anything yet. But now, when the server fires the EVT_GOT_DATA event, at least it won't fall on deaf ears. The next thing we need to do is to decide what we want Mushclient to be able to command our app to do, and what we want the app to send to Mushclient. We'll start with what we'll listen for from Mushclient.

Firstly, we'll figure out what the app should be able to do. I can think of a few important things:

1. It should be able to hide and show itself when ordered to do so by Mushclient, so it doesn't constantly hover on the desktop.
2. It should be able to shut itself down when ordered by Mushclient, so that we can close it programatically without needing to click on the titlebar icon.
3. It should be able to change its own position on the screen, so we can move it around from Mushclient.
4. It should be able to change the colours of buttons in response to info about room exits sent to it by Mushclient, to reflect the visible exits in a room.

Before we begin however, there are some things to consider. Firstly, we are listening to a network port, so hypothetically speaking, we can't be sure what will arrive from that port - we might later screw up when sending commands from Mushclient and thus need some sort of an internal convention that describes the format of valid command strings. We'll say that a valid command string must start with the word "Buttons" immediately followed by two colons - "Buttons::". This will mean that we are at least dealing with a string that was meant for us, not some other program. Since we have a few commands that we need to react to, we decree that this first part must be followed by the command name. Since the command may optionally have arguments (a list of room names, or new position coordinates), we also specify that those arguments must be enclosed in curly braces and can be separated with commas. Here's an example of a valid command according to our convention:

CODE

Buttons::Exits {nw, east, south, up, down}


While we are at it, we might specify examples for all the commands that we want to handle:

1. "Buttons::Show {true}" or "Buttons::Show { false }"
2. "Buttons::Close"
3. "Buttons::Move  {300, 450}"
4. "Buttons::Exits{nw, east, s, u, down}"

The numbers exactly correspond to the numbers of tasks we've identified above. Now lets implement our OnData() method to make it process those commands. Here it is:


   def OnData(self, evt):
       pat = re.compile(r"^Buttons::(Exits|Close|Move|Show)(?:\\s*\\{(.*?)\\}|)$")
       match = pat.match(evt)
       exits = frozenset()

       if match is not None:
           if match.groups() == "Exits":
               argslist = .split(",")]
               self.DisplayExits(list(exits & set(argslist)))
           elif match.groups() == "Move":
               argslist = .split(",")]
               if len(argslist) != 2: return
               self.MoveFrame(argslist)
           elif match.groups() == "Show":
               arg = match.groups().strip().lower()
               if arg == "true":
                   self.Show(True)
               elif arg == "false":
                   self.Show(False)
           elif match.groups() == "Close":
               self.OnClose()


Here we use a regular expression, therefore we'll need to add another import statement to the top of our script:

CODE

import re


The regular expression (pat) is used to match a pattern that summarizes our specification for the minimal "command language" that this program is going to recognize. Note the line:

CODE

       exits = frozenset()


This is a frozenset, which is basically a tuple inside which all elements are unique. We use it to process the exit names passed as arguments to the "Exits" command, and it is special because it requires version 2.4 of Python, so if you didn't get that one, then you should either do so, or remove the frozenset object and change the following line:

CODE

self.DisplayExits(list(exits & set(argslist)))


to be just:

CODE

self.DisplayExits(argslist)


That will turn off the pre-processing of arguments, and in case you make a typo when entering an exit name, you'll end up with problems. The rest of the function should be more or less straightforward. In the cases of the "Exits" and "Move" commands we use list comprehensions to split the arguments into a list at comma separators, stripping whitespace from either side of the arguments. In the case of "Move" each argument is
additionally converted to an integer, since the arguments should be numeric values. In the case of "Exits" - each argument is additionally converted to lowercase. Since "Show" takes only one argument, no list comprehension is used there - we just strip() and lower() it. "Close" is the simplest of all, since it doesn't take any arguments at all.

You can see a total of three new methods used in the OnData() one above. They are:

self.DisplayExits()
self.MoveFrame()
self.OnClose()

Each of those methods is meant to handle one command, the "Show" command uses the already familiar Show() method defined on our frame by the wx library. We'll start by implementing the simplest one of the above - the self.OnClose() method. We'll also need this one when talking back to Mushclient, so for now we'll just make it do what we need it do now, so that we can test our server's listening and our application's responding capabilities right away, and come back to it again later:

CODE

   def OnClose(self, evt=None):
       if evt is None:
self.server.stop_thread()
sleep(0.25)
           self.Destroy()


A few things need to be explained here. Firstly, we use an evt argument here, but set it to None by default. This is because eventually this method will handle another event - the shutdown one, but we are also calling it directly. Next is the "self.server.stop_thread()" line. This is obviously a call to cause our server to stop, but we haven't created an instance of it inside this class yet. So we do that now by adding the following line to the class' __init__ method:

CODE

self.server = UDPserver(self)


We pass "self" as the first argument to UDPServer(), which will allow it to later specify that it is sending an event bound for our frame, when it receives some data. This "self" is assigned to the "bound_to" property in the UDPServer's __init__ method. Next up is the "sleep(0.25)" line. This one uses a method of the "time" library, so we need to import yet another thing:

CODE

from time import sleep


The sleep() method pauses the program's execution for a specified amount of time. Here we pause for 0.25 seconds before exiting but after sending the thread a command to shut itself down, so that the thread has enough time to peacefully die off. The amount of time specified should be enough for it to do so. At last, we call the Destroy() method (this is provided by wx) to kill the program itself.

So far our MainWindow class looks like this:

CODE

class MainWindow(wx.MiniFrame):
   def __init__(self, title, pos=wx.DefaultPosition, size=(174,190),
                style=wx.STAY_ON_TOP|wx.DEFAULT_FRAME_STYLE^wx.RESIZE_BORDER):
       wx.MiniFrame.__init__(self, None, -1, title, pos, size, style)

       panel = wx.Panel(self, -1)
       sizer = wx.BoxSizer(wx.VERTICAL)
       sizer.Add(panel, 1, wx.EXPAND)
       self.SetSizer(sizer)
       self.SetAutoLayout(True)
       sizer.Fit(self)
       self.SetSize(size)

       self.AddButton(panel, 10, "NW", (5,5), (40,40))
       self.AddButton(panel, 20, "N", (47, 5), (40,40))
       self.AddButton(panel, 30, "NE", (89, 5), (40,40))
       self.AddButton(panel, 40, "W", (5, 47), (40,40))
       self.AddButton(panel, 50, "E", (89, 47), (40, 40))
       self.AddButton(panel, 60, "SW", (5, 89), (40,40))
       self.AddButton(panel, 70, "S", (47,89), (40,40))
       self.AddButton(panel, 80, "SE", (89,89), (40,40))
       self.AddButton(panel, 90, "UP", (131,5), (30,61))
       self.AddButton(panel, 100, "D", (131, 68), (30, 61))
       self.AddButton(panel, 110, "IN", (5, 131), (76,30))
       self.AddButton(panel, 120, "OUT", (83, 131), (76, 30))

       self.Bind(EVT_GOT_DATA, self.OnData)

       self.server = UDPserver(self)
       
       self.Show(True)

   def OnClick(self, evt):
       pass

   def OnData(self, evt):
       pat = re.compile(r"^Buttons::(Exits|Close|Move|Show)(?:\\s*\\{(.*?)\\}|)$")
       match = pat.match(evt)
       exits = frozenset()

       if match is not None:
           if match.groups() == "Exits":
               argslist = .split(",")]
               self.DisplayExits(list(exits & set(argslist)))
           elif match.groups() == "Move":
               argslist = .split(",")]
               if len(argslist) != 2: return
               self.MoveFrame(argslist)
           elif match.groups() == "Show":
               arg = match.groups().strip().lower()
               if arg == "true":
                   self.Show(True)
               elif arg == "false":
                   self.Show(False)
           elif match.groups() == "Close":
               self.OnClose()

   def OnClose(self, evt=None):
       if evt is None:
           self.server.stop_thread()
           sleep(0.25)
           self.Destroy()

   def AddButton(self, parent, id, label, position, size):
       b = buttons.GenButton(parent, id, label, position, size)
       self.Bind(wx.EVT_BUTTON, self.OnClick, b)
       b.SetFont(wx.Font(10, wx.SWISS, wx.NORMAL, wx.NORMAL, False))
       b.SetBezelWidth(3)
       b.SetForegroundColour(wx.Colour(128,0,64))
       b.SetUseFocusIndicator(False)


Next we are going to make the DisplayExits() method. That one should set the background of a button to a specific colour if one of the exit names passed to it corresponds to the button's label. First of all, we obviously need to decide on what colours we are going to use. I personally like green, so how about dark green for the background and to make the labels more visible - white for the foreground. First off, we need to get rgb values for the colours. To do that, I go to Mushclient's colour picker (Ctrl+Alt+P) and come up with these:

darkgreen = 0,100,0
mintcream = 245,255,250

It'll be best to save these colours in constants. Along with the hilighted colours we'll also save the default ones. The default foreground is (128,0,64), that's what we used to create the buttons with originally, while the default background colour is (212,208,200), which is what I got using the GetBackgroundColour() method on our buttons. Add the following assignments after the import statements:

CODE

BTN_HILITE_BACK = wx.Colour(0,100,0)
BTN_HILITE_FORE = wx.Colour(245,255,250)
BTN_DEFAULT_BACK = wx.Colour(212,208,200)
BTN_DEFAULT_FORE = wx.Colour(128,0,64)


This gives us descriptive names to work with instead of confusing numbers. Now we need to also replace the wx.Colour() call in the AddButton() method with the pre-made colour for the button's default background. The AddButton method becomes:

CODE

   def AddButton(self, parent, id, label, position, size):
       b = buttons.GenButton(parent, id, label, position, size)
       self.Bind(wx.EVT_BUTTON, self.OnClick, b)
       b.SetFont(wx.Font(10, wx.SWISS, wx.NORMAL, wx.NORMAL, False))
       b.SetBezelWidth(3)
       b.SetForegroundColour(BTN_DEFAULT_FORE)
       b.SetUseFocusIndicator(False)


Now everything is in place for the DisplayExits method. Here it is:

CODE

   def DisplayExits(self, exits):
       btns = self.GetChildren().GetChildren()
       map = {'north':'n', 'south':'s', 'northwest':'nw','northeast':'ne', 'southwest':'sw', 'southeast':'se',
              'east':'e', 'west':'w', 'down':'d', 'u':'up', 'i':'in', 'o':'out'}
       labels =
       for e in exits:
           if e.lower() in map.keys():
               labels.append(map.upper())
           elif e.lower() in map.values():
               labels.append(e.upper())
       for btn in btns:
           if btn.GetLabel() in labels:
               btn.SetBackgroundColour(BTN_HILITE_BACK)
               btn.SetForegroundColour(BTN_HILITE_FORE)
           else:
               btn.SetBackgroundColour(BTN_DEFAULT_BACK)
               btn.SetForegroundColour(BTN_DEFAULT_FORE)
       self.panel.Refresh()


That should be more or less self-explanatory. The first line gets the first child of the top-most frame, which is the panel we assigned to the MainWindow object, and then gets all the children of that panel. Since our panel only has buttons as its children, the result of the self.GetChildren().GetChildren() is a Python list of button objects. The “map” dictionary is a mapping between descriptive exit names and corresponding button labels. Since Mushclient is likely to send us exit names as “northwest” and “down”, while our buttons for those direction are labeled “nw” and “d”, we need some way to map between those names and our labels. The “labels” list is initially empty and represents the list of all buttons that need to be highlighted. What happens next is that we first convert all descriptive names in the exits list that we got from Mushclient to their label equivalents, and then loop through all the button objects and highlight those who’s labels appear in the “exits” list, while setting the rest to default colours. The last line in this function:

CODE

self.GetChildren().Refresh()


Tells the client area to refresh itself, making visible the changes we’ve made.

We have only one command and its corresponding method to handle: Move. This one is simple:

CODE

   def MoveFrame(self, pos):
       self.Move(pos)


And we are done with handling the incoming commands. Now we just need to arrange for the application to call Mushclient back. We want it to do so in the following cases:

1. When a button is clicked a directional command needs to be sent to Mushclient.
2. When closing the application we want to send back the current position of the top frame, so it can be saved and used later to create the frame right where it was closed last time.

Firstly, we need to create an instance of the UDP client, so that we can send things. To do that, add the line:

CODE

self.client = UDPClient()


to the MainWindow.__init__ method, next to the line where the self.server object was created.

To do the first task, we need to amend the OnClick() method of the MainWindow class, since that’s what the click event is handled by. Right now this method doesn’t do anything, here’s how it should look to be useful:

CODE

   def OnClick(self, evt):
       btn = evt.GetEventObject()
       self.client.send("Go %s" % btn.GetLabel().lower())


The UDPClient will automatically prepend “Buttons::” to anything we send, to tell Mushclient that this was sent by the Buttons program. The actual command is formatted as “Go dir”, where dir is the label of the clicked button, converted to lowercase.

And for the second task, we need to edit the OnClose method, as well as bind that method to the EVT_CLOSE event, so that it will be called when the user closes the app by clicking on the close button, as well as when ordered by Mushclient. Here’s the line binding the event, place it near the one binding our custom EVT_GOT_DATA event in MainWIndow.__init__:

CODE

       self.Bind(wx.EVT_CLOSE, self.OnClose)


And here’s what the OnClose event becomes:

CODE

   def OnClose(self, evt=None):
       self.client.send ("Position {%d,%d}" % (self.GetPosition(), self.GetPosition()))
       self.server.stop_thread()
       sleep(0.25)
       self.Destroy()


As you can see, there’s only one extra line added. And that’s pretty much it – the handling of commands we issue is now the job of whatever is going to listen to them in Mushclient, and will be dealt with next time when we write a plugin to control Buttons. Here’s the entire script so far:

CODE

import wx
import wx.lib.buttons as buttons
import re
import socket, thread
from time import sleep

BTN_HILITE_BACK = wx.Colour(0,100,0)
BTN_HILITE_FORE = wx.Colour(245,255,250)
BTN_DEFAULT_BACK = wx.Colour(212,208,200)
BTN_DEFAULT_FORE = wx.Colour(128,0,64)

myEVT_GOT_DATA = wx.NewEventType()
EVT_GOT_DATA = wx.PyEventBinder(myEVT_GOT_DATA, 1)


class MyEvent(wx.PyCommandEvent):
   def __init__(self, evtType, id, data):
       wx.PyCommandEvent.__init__(self, evtType, 1)
       self.data = data


class UDPClient(object):
   def __init__(self, address="127.0.0.1", port=4311):
       self.host = address
       self.port = port
       self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)

   def send(self, data):
       self.sock.sendto("Buttons::" + data, (self.host, self.port))

class UDPserver(object):
   def __init__(self, bound_to, address='', port=4222, buffer=1024):
       self.bound_to = bound_to
       self.host = address
       self.port = port
       self.buffer = buffer
       self.keep_alive = False
       self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
       self.sock.bind((self.host, self.port))
       self.start_thread()

   def start_thread(self):
       self.keep_alive = True
       thread.start_new_thread(self.run_thread, ())
       return

   def run_thread(self):
       while self.keep_alive:
           data,addr = self.sock.recvfrom(self.buffer)
           if not data: break
           evt = MyEvent(myEVT_GOT_DATA, wx.ID_ANY, data)
           wx.PostEvent(self.bound_to, evt)
       self.sock.close()
       return

   def stop_thread(self):
       self.keep_alive = False
       


class MainWindow(wx.MiniFrame):
   def __init__(self, title, pos=wx.DefaultPosition, size=(174,190),
                style=wx.STAY_ON_TOP|wx.DEFAULT_FRAME_STYLE^wx.RESIZE_BORDER):
       wx.MiniFrame.__init__(self, None, -1, title, pos, size, style)

       self.panel = wx.Panel(self, -1)
       sizer = wx.BoxSizer(wx.VERTICAL)
       sizer.Add(self.panel, 1, wx.EXPAND)
       self.SetSizer(sizer)
       self.SetAutoLayout(True)
       sizer.Fit(self)
       self.SetSize(size)

       self.AddButton(self.panel, 10, "NW", (5,5), (40,40))
       self.AddButton(self.panel, 20, "N", (47, 5), (40,40))
       self.AddButton(self.panel, 30, "NE", (89, 5), (40,40))
       self.AddButton(self.panel, 40, "W", (5, 47), (40,40))
       self.AddButton(self.panel, 50, "E", (89, 47), (40, 40))
       self.AddButton(self.panel, 60, "SW", (5, 89), (40,40))
       self.AddButton(self.panel, 70, "S", (47,89), (40,40))
       self.AddButton(self.panel, 80, "SE", (89,89), (40,40))
       self.AddButton(self.panel, 90, "UP", (131,5), (30,61))
       self.AddButton(self.panel, 100, "D", (131, 68), (30, 61))
       self.AddButton(self.panel, 110, "IN", (5, 131), (76,30))
       self.AddButton(self.panel, 120, "OUT", (83, 131), (76, 30))

       self.Bind(EVT_GOT_DATA, self.OnData)
       self.Bind(wx.EVT_CLOSE, self.OnClose)
       
       self.server = UDPserver(self)
       self.client = UDPClient()
       
       self.Show(True)

   def OnClick(self, evt):
       btn = evt.GetEventObject()
       self.client.send("Go %s" % btn.GetLabel().lower())

   def OnData(self, evt):
       pat = re.compile(r"^Buttons::(Exits|Close|Move|Show)(?:\\s*\\{(.*?)\\}|)$")
       match = pat.match(evt.data)
       exits = frozenset()

       if match is not None:
           if match.groups() == "Exits":
               argslist = .split(",")]
               self.DisplayExits(list(exits & set(argslist)))
           elif match.groups() == "Move":
               argslist = .split(",")]
               if len(argslist) != 2: return
               self.MoveFrame(argslist)
           elif match.groups() == "Show":
               arg = match.groups().strip().lower()
               if arg == "true":
                   self.Show(True)
               elif arg == "false":
                   self.Show(False)
           elif match.groups() == "Close":
               self.OnClose()

   def OnClose(self, evt=None):
       self.client.send ("Position {%d,%d}" % (self.GetPosition(), self.GetPosition()))
       self.server.stop_thread()
       sleep(0.25)
       self.Destroy()

   def DisplayExits(self, exits):
       btns = self.GetChildren().GetChildren()
       map = {'north':'n', 'south':'s', 'northwest':'nw','northeast':'ne', 'southwest':'sw', 'southeast':'se',
              'east':'e', 'west':'w', 'down':'d', 'u':'up', 'i':'in', 'o':'out'}
       labels =
       for e in exits:
           if e.lower() in map.keys():
               labels.append(map.upper())
           elif e.lower() in map.values():
               labels.append(e.upper())
       for btn in btns:
           if btn.GetLabel() in labels:
               btn.SetBackgroundColour(BTN_HILITE_BACK)
               btn.SetForegroundColour(BTN_HILITE_FORE)
           else:
               btn.SetBackgroundColour(BTN_DEFAULT_BACK)
               btn.SetForegroundColour(BTN_DEFAULT_FORE)
       self.GetChildren().Refresh()

   def MoveFrame(self, pos):
       self.Move(pos)

   def AddButton(self, parent, id, label, position, size):
       b = buttons.GenButton(parent, id, label, position, size)
       self.Bind(wx.EVT_BUTTON, self.OnClick, b)
       b.SetFont(wx.Font(10, wx.SWISS, wx.NORMAL, wx.NORMAL, False))
       b.SetBezelWidth(2)
       b.SetForegroundColour(BTN_DEFAULT_FORE)
       b.SetUseFocusIndicator(False)
       

if __name__ == "__main__":
   app = wx.App()  
   app.frame = MainWindow("Compass")
   app.MainLoop()


There’s one last thing left to take care of, however. Right now, there’s no way to pass arguments to our program when it is launched, as a result, the trouble we went through to make sure the program notifies Mushclient of its’ position before closing, was useless since Mushclient won’t be able to re-open the program with that position as an argument anyways. Therefore, we need to make the program accept command line arguments. In order to do that, we’ll need to amend the last chunk of code above:


CODE

if __name__ == "__main__":
   app = wx.App()  
   app.frame = MainWindow("Compass")
   app.MainLoop()


This is what will be called when the program is started. We’ll use Python’s standard module “getopt” to make parsing options and arguments easier. We’ll also need the “sys” standard library to get at those options and arguments. So put those imports right before the code we’ll be editing:

CODE

import sys, getopt


We’ll use option “-x” and option “-y” for x and y positions, respectively. Here’s the code to handle those options:

CODE

if __name__ == "__main__":
   try:
       opts, args = getopt.getopt(sys.argv, "x:y:")
   except getopt.GetoptError:
       sys.exit(2)
   posx = 0
   posy = 0
   for o,a in opts:
       if o == "-x":
           try:
               posx = int(a)
           except ValueError:
               print "An integer is required for option -x"
               sys.exit(2)
       if o == "-y":
           try:
               posy = int(a)
           except ValueError:
               print "An integer is required for option -y"
               sys.exit(2)
   
   app = wx.App()  
   app.frame = MainWindow("Compass", pos=(posx,posy))
   app.MainLoop()


And this is the entire script:

CODE

import wx
import wx.lib.buttons as buttons
import re
import socket, thread
from time import sleep

BTN_HILITE_BACK = wx.Colour(0,100,0)
BTN_HILITE_FORE = wx.Colour(245,255,250)
BTN_DEFAULT_BACK = wx.Colour(212,208,200)
BTN_DEFAULT_FORE = wx.Colour(128,0,64)

myEVT_GOT_DATA = wx.NewEventType()
EVT_GOT_DATA = wx.PyEventBinder(myEVT_GOT_DATA, 1)


class MyEvent(wx.PyCommandEvent):
   def __init__(self, evtType, id, data):
       wx.PyCommandEvent.__init__(self, evtType, 1)
       self.data = data


class UDPClient(object):
   def __init__(self, address="127.0.0.1", port=4311):
       self.host = address
       self.port = port
       self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)

   def send(self, data):
       self.sock.sendto("Buttons::" + data, (self.host, self.port))

class UDPserver(object):
   def __init__(self, bound_to, address='', port=4222, buffer=1024):
       self.bound_to = bound_to
       self.host = address
       self.port = port
       self.buffer = buffer
       self.keep_alive = False
       self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
       self.sock.bind((self.host, self.port))
       self.start_thread()

   def start_thread(self):
       self.keep_alive = True
       thread.start_new_thread(self.run_thread, ())
       return

   def run_thread(self):
       while self.keep_alive:
           data,addr = self.sock.recvfrom(self.buffer)
           if not data: break
           evt = MyEvent(myEVT_GOT_DATA, wx.ID_ANY, data)
           wx.PostEvent(self.bound_to, evt)
       self.sock.close()
       return

   def stop_thread(self):
       self.keep_alive = False
       


class MainWindow(wx.MiniFrame):
   def __init__(self, title, pos=wx.DefaultPosition, size=(174,190),
                style=wx.STAY_ON_TOP|wx.DEFAULT_FRAME_STYLE^wx.RESIZE_BORDER):
       wx.MiniFrame.__init__(self, None, -1, title, pos, size, style)

       self.panel = wx.Panel(self, -1)
       sizer = wx.BoxSizer(wx.VERTICAL)
       sizer.Add(self.panel, 1, wx.EXPAND)
       self.SetSizer(sizer)
       self.SetAutoLayout(True)
       sizer.Fit(self)
       self.SetSize(size)

       self.AddButton(self.panel, 10, "NW", (5,5), (40,40))
       self.AddButton(self.panel, 20, "N", (47, 5), (40,40))
       self.AddButton(self.panel, 30, "NE", (89, 5), (40,40))
       self.AddButton(self.panel, 40, "W", (5, 47), (40,40))
       self.AddButton(self.panel, 50, "E", (89, 47), (40, 40))
       self.AddButton(self.panel, 60, "SW", (5, 89), (40,40))
       self.AddButton(self.panel, 70, "S", (47,89), (40,40))
       self.AddButton(self.panel, 80, "SE", (89,89), (40,40))
       self.AddButton(self.panel, 90, "UP", (131,5), (30,61))
       self.AddButton(self.panel, 100, "D", (131, 68), (30, 61))
       self.AddButton(self.panel, 110, "IN", (5, 131), (76,30))
       self.AddButton(self.panel, 120, "OUT", (83, 131), (76, 30))

       self.Bind(EVT_GOT_DATA, self.OnData)
       self.Bind(wx.EVT_CLOSE, self.OnClose)
       
       self.server = UDPserver(self)
       self.client = UDPClient()
       
       self.Show(True)

   def OnClick(self, evt):
       btn = evt.GetEventObject()
       self.client.send("Go %s" % btn.GetLabel().lower())

   def OnData(self, evt):
       pat = re.compile(r"^Buttons::(Exits|Close|Move|Show)(?:\\s*\\{(.*?)\\}|)$")
       match = pat.match(evt.data)
       exits = frozenset(
Unknown2005-02-06 23:18:53
Since the forums ate the last portion of the post above, here it is, starting with the final version of the script again...


And this is the entire script:

CODE

import wx
import wx.lib.buttons as buttons
import re
import socket, thread
from time import sleep

BTN_HILITE_BACK = wx.Colour(0,100,0)
BTN_HILITE_FORE = wx.Colour(245,255,250)
BTN_DEFAULT_BACK = wx.Colour(212,208,200)
BTN_DEFAULT_FORE = wx.Colour(128,0,64)

myEVT_GOT_DATA = wx.NewEventType()
EVT_GOT_DATA = wx.PyEventBinder(myEVT_GOT_DATA, 1)


class MyEvent(wx.PyCommandEvent):
   def __init__(self, evtType, id, data):
       wx.PyCommandEvent.__init__(self, evtType, 1)
       self.data = data


class UDPClient(object):
   def __init__(self, address="127.0.0.1", port=4311):
       self.host = address
       self.port = port
       self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)

   def send(self, data):
       self.sock.sendto("Buttons::" + data, (self.host, self.port))

class UDPserver(object):
   def __init__(self, bound_to, address='', port=4222, buffer=1024):
       self.bound_to = bound_to
       self.host = address
       self.port = port
       self.buffer = buffer
       self.keep_alive = False
       self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
       self.sock.bind((self.host, self.port))
       self.start_thread()

   def start_thread(self):
       self.keep_alive = True
       thread.start_new_thread(self.run_thread, ())
       return

   def run_thread(self):
       while self.keep_alive:
           data,addr = self.sock.recvfrom(self.buffer)
           if not data: break
           evt = MyEvent(myEVT_GOT_DATA, wx.ID_ANY, data)
           wx.PostEvent(self.bound_to, evt)
       self.sock.close()
       return

   def stop_thread(self):
       self.keep_alive = False
       


class MainWindow(wx.MiniFrame):
   def __init__(self, title, pos=wx.DefaultPosition, size=(174,190),
                style=wx.STAY_ON_TOP|wx.DEFAULT_FRAME_STYLE^wx.RESIZE_BORDER):
       wx.MiniFrame.__init__(self, None, -1, title, pos, size, style)

       self.panel = wx.Panel(self, -1)
       sizer = wx.BoxSizer(wx.VERTICAL)
       sizer.Add(self.panel, 1, wx.EXPAND)
       self.SetSizer(sizer)
       self.SetAutoLayout(True)
       sizer.Fit(self)
       self.SetSize(size)

       self.AddButton(self.panel, 10, "NW", (5,5), (40,40))
       self.AddButton(self.panel, 20, "N", (47, 5), (40,40))
       self.AddButton(self.panel, 30, "NE", (89, 5), (40,40))
       self.AddButton(self.panel, 40, "W", (5, 47), (40,40))
       self.AddButton(self.panel, 50, "E", (89, 47), (40, 40))
       self.AddButton(self.panel, 60, "SW", (5, 89), (40,40))
       self.AddButton(self.panel, 70, "S", (47,89), (40,40))
       self.AddButton(self.panel, 80, "SE", (89,89), (40,40))
       self.AddButton(self.panel, 90, "UP", (131,5), (30,61))
       self.AddButton(self.panel, 100, "D", (131, 68), (30, 61))
       self.AddButton(self.panel, 110, "IN", (5, 131), (76,30))
       self.AddButton(self.panel, 120, "OUT", (83, 131), (76, 30))

       self.Bind(EVT_GOT_DATA, self.OnData)
       self.Bind(wx.EVT_CLOSE, self.OnClose)
       
       self.server = UDPserver(self)
       self.client = UDPClient()
       
       self.Show(True)

   def OnClick(self, evt):
       btn = evt.GetEventObject()
       self.client.send("Go %s" % btn.GetLabel().lower())

   def OnData(self, evt):
       pat = re.compile(r"^Buttons::(Exits|Close|Move|Show)(?:\\s*\\{(.*?)\\}|)$")
       match = pat.match(evt.data)
       exits = frozenset()

       if match is not None:
           if match.groups() == "Exits":
               argslist = .split(",")]
               self.DisplayExits(list(exits & set(argslist)))
           elif match.groups() == "Move":
               argslist = .split(",")]
               if len(argslist) != 2: return
               self.MoveFrame(argslist)
           elif match.groups() == "Show":
               arg = match.groups().strip().lower()
               if arg == "true":
                   self.Show(True)
               elif arg == "false":
                   self.Show(False)
           elif match.groups() == "Close":
               self.OnClose()

   def OnClose(self, evt=None):
       self.client.send ("Position {%d,%d}" % (self.GetPosition(), self.GetPosition()))
       self.server.stop_thread()
       sleep(0.25)
       self.Destroy()

   def DisplayExits(self, exits):
       btns = self.GetChildren().GetChildren()
       map = {'north':'n', 'south':'s', 'northwest':'nw','northeast':'ne', 'southwest':'sw', 'southeast':'se',
              'east':'e', 'west':'w', 'down':'d', 'u':'up', 'i':'in', 'o':'out'}
       labels =
       for e in exits:
           if e.lower() in map.keys():
               labels.append(map.upper())
           elif e.lower() in map.values():
               labels.append(e.upper())
       for btn in btns:
           if btn.GetLabel() in labels:
               btn.SetBackgroundColour(BTN_HILITE_BACK)
               btn.SetForegroundColour(BTN_HILITE_FORE)
           else:
               btn.SetBackgroundColour(BTN_DEFAULT_BACK)
               btn.SetForegroundColour(BTN_DEFAULT_FORE)
       self.GetChildren().Refresh()

   def MoveFrame(self, pos):
       self.Move(pos)

   def AddButton(self, parent, id, label, position, size):
       b = buttons.GenButton(parent, id, label, position, size)
       self.Bind(wx.EVT_BUTTON, self.OnClick, b)
       b.SetFont(wx.Font(10, wx.SWISS, wx.NORMAL, wx.NORMAL, False))
       b.SetBezelWidth(2)
       b.SetForegroundColour(BTN_DEFAULT_FORE)
       b.SetUseFocusIndicator(False)


import sys, getopt

if __name__ == "__main__":
   try:
       opts, args = getopt.getopt(sys.argv, "x:y:")
   except getopt.GetoptError:
       sys.exit(2)
   posx = 0
   posy = 0
   for o,a in opts:
       if o == "-x":
           try:
               posx = int(a)
           except ValueError:
               print "An integer is required for option -x"
               sys.exit(2)
       if o == "-y":
           try:
               posy = int(a)
           except ValueError:
               print "An integer is required for option -y"
               sys.exit(2)
   
   app = wx.App()  
   app.frame = MainWindow("Compass", pos=(posx,posy))
   app.MainLoop()


Now you can test the program by making it interact with Mushclient. Launch Buttons and Mushclient so they are both running. Then try issuing some commands from Mushclient’s command window. If your scripting language is set to Python, and your script prefix character is “/”, you can enter the following command to highlight nw, sw, and down directions:

CODE

/world.UDPSend('127.0.0.1', 4222, "Buttons::Exits {nw, southwest, down}")


And to clear all highlights:

CODE

/world.UDPSend('127.0.0.1', 4222, "Buttons::Exits {}")


To move the panel to position 200,460 on the screen, do:

CODE

/world.UDPSend('127.0.0.1', 4222, "Buttons::Move {200,460}")


And finally, to close the panel:


/world.UDPSend('127.0.0.1', 4222, "Buttons::Close")

Of course, if your scripting is something other than Python, then you’ll need to use different syntax. We are finished with the Buttons program, and all that is left to do is to write a Mushclient plugin to control the panel and accept its commands.
Unknown2005-02-07 00:35:46
Would you consider posting a screenshot?
I'm curious how that looks like.
Unknown2005-02-07 00:59:50
user posted image

In case the above doesn't work, here's the direct link to it:

http://www.geocities.com/avator_lusternia/Compass.jpg
Unknown2011-03-18 06:22:26

Thanks for your sharing. I think you're right. It looks very useful
Unknown2011-03-18 06:22:50
Thanks for your sharing. I think you're right. It looks very useful__________________________
mother of the bride dresses
discount wedding dresses
Custom wedding gown
Xenthos2011-03-19 00:22:21
QUOTE (JerryAbelFrank @ Mar 18 2011, 02:22 AM) <{POST_SNAPBACK}>
Thanks for your sharing. I think you're right. It looks very useful

Double-post spam-bot and necromancer to boot.

You aren't even trying to peddle a link. sad.gif
Unknown2011-03-19 14:37:41
This thread should be unpinned and closed, anyway. It's no longer needed as a way to make GUI components for MUSHclient.