Unknown2005-02-02 23:19:48
Since there's been considerable interest in building extra GUI components for Mushclient, and I've been doing it for quite a while now, I decided to finally sit down and at least try to provide a basic introduction to this topic. This might look long, but that's mainly because I repeat the same stuff all the time.
First of all, it is needed to note that Mushclient itself makes very few provisions for accomplishing this sort of tasks - it does have some callbacks meant to make interacting with your components easier, but it doesn't have a builtin infrastructure for creating the components themselves. This is both a good and a bad thing. It is bad, because you can't add a button to a panel with a single line of code in Mushclient. It is good because it keeps Mushclient small and fast, minimizes the potential for bugs creeping up, and results in you actually learning something useful while trying to implement your GUI desires. An additional plus to doing it the Mushclient way is that since your components aren't being ran by Mushclient, they have no direct effect on the client's performance (the effect is about the same as running an ICQ client at the same time as playing a MUD in Mushclient). Furthermore, you are not limited to any particular kind of GUI components or their layouts - you can do whatever you want, be it a simple health guage or a full-blown remote controller with dozens of buttons, popup menu's, etc, skinned to resemble your favourite look for Winamp and running on a different computer somewhere on the network. On the most basic level both of those types of components require almost identical code to implement, and that code isn't even that complicated once you understand how it works.
Being a fan of Python, I obviously use that language for most of my programming needs. Therefore, this tutorial uses Python as the implementation language. If the only Python you are familiar with is a snake, or is named Monty, then don't despair - Python is a fairly simple programming language and in the examples that follow I don't use any of its features that cannot be grasped after at most 15 minutes of reading through the docs (which come together with the language). However, I won't explain Python-specific issues here, so be prepared to have the docs open if you decided to sit through this.
Before we begin, you will need to have several tools installed on your system to proceed. The first one is obviously Python itself. You can download it from www.python.org (version 2.4 is recommended). Once you have that, you should install the pywin32 library, which is needed to use Python from Mushclient. Download the most recent version of it from HERE. At last, you need the wxPython library, which we'll use for the GUI and which is available from www.wxpython.org . Get the most recent version with Unicode support (just in case) for the version of Python that you have installed previously. All of those come in form of binary installers for Windows, so installing them is usually not a problem. In the case of wxPython, you will need to download the runtime package and the docs and examples package. For editing Python source code on Windows I personally prefer Pythonwin, which comes together with the pywin32 library and a link to which should appear in your Start->Programs->Python menu.
Now that we have all that out of the way, a short overview of how Mushclient's current paradigm of plugging in external GUI components looks is in order.
With Mushclient you are supposed to create a standalone Windows application, that then has to be launched from Mushclient, and must have certain provisions for communicating with it: accepting commands and transmitting data. Recently a new mechanism was added to Mushclient which makes handling this communication simpler - Mushclient plugins became able to send data across the network through the UDP protocol and listen to incoming data on specific ports. This is what we'll use for our program. We'll do it in several steps. First we will construct a standalone application without any means of communicating with anything beside itself. Then we will add to it the functionality needed to communicate with Mushclient. Lastly, we will write a Mushclient plugin meant to control this application.
For this tutorial I decided to make a small panel with buttons holding all walking directions, so you can use your mouse for moving around in the game. The buttons will be able to respond to certain commands from Mushclient and change their colours, so that you can see all visible exits from the room you are in by just looking at this panel. The buttons on the panel will be laid out almost as they are on your keypad. With directional ones forming a square, up and down on the right side of that square, and in/out at the bottom. Okay, without further ado, lets proceed to making this application. Start by launching Pythonwin and creating a new Python script file. Save that empty file in a directory where it will be easy to find, for example C:\\Python_stuff\\Buttons. Name the file "Buttons.pyw". The .pyw extension is a Windows-only thing and tells the interpreter to launch the script as a Windows UI app, without creating a command console. Now put the following line at the top of your script:
import wx
That loads the wxPython library into your program. Now let's create a class that will hold the application's main window frame:
class MainWindow(wx.MiniFrame):
  def __init__(self, title, pos=wx.DefaultPosition, size=(200,200),
        style=wx.STAY_ON_TOP|wx.DEFAULT_FRAME_STYLE):
    wx.MiniFrame.__init__(self, None, -1, title, pos, size, style)
    self.Show(True)
In the first line we define a class that inherits from wx.MiniFrame, which is a frame with a very small titlebar and no button of its own in the tool panel. The arguments passed to this class' __init__ method are:
title - what will appear in the title bar as the window's name;
pos - where the window appears on the screen, here we use x.DefaultPosition to make wxPython decide where to put it;
size - the initial size of the window, for now we'll make it a square with a 200 pixel side;
style - the general appearance of the window, check the wxPython docs for wxFrame and look at all of the style constants defined there.
So for now we settle on a square toolbox window that has the default style, the size of 200x200 pixels, default position, and is set to stay on top of all other windows. The next line is the standard initialization of the parent class - we tell wx.MiniFrame that this class is creating its instance, passing to it all the required arguments. Finally, we tell the program to Show() our window. Now you'll see how simple all of this really is...
Add the following to your script, save it, and doubleclick on your Buttons.pyw icon in the Explorer to run it (an explanation will follow):
if __name__ == "__main__":
  app = wx.App()
  app.frame = MainWindow("Compass")
  app.MainLoop()
You should see your frame appear. It doesn't have any useful components yet, but those won't take long to create either. That's Python at its best - 9 lines of code and you have yourself an application The code above... The "if" statement in the first line is another standard Python trick - this makes sure that whatever is inside that statement's scope will be ran only if the script is launched in standalone mode from the command line, or by doubleclicking on it as in our case. That prevents it from being executed if you import the module somewhere. Second line creates an instance of a wx.App class (check the docs) and assigns it to the "app" variable. Objects of the wx.App class have a "frame" property, which holds the application's frame - we assign to it an instance of our MainWindow class above, setting its title to "Compass". Finally we start the app by calling the "MainLoop()" method of the app object. Now the app is up and running.
Now that we have a working application in its basic shape we can proceed to fill it up with some useful content. First of all, if you look at the running app again you'll notice that its client area has an unusually dark shade of grey, normally empty windows are light grey. This is because the client area is not only empty - it is also virtually non-existant. In order to make use of it we'll need to create a "panel", which is essentially a window which is contained inside another window. We then be able to put other stuff on that panel, such as buttons. Go back to your MainWindow class and modify it to look like this:
class MainWindow(wx.MiniFrame):
  def __init__(self, title, pos=wx.DefaultPosition, size=(200,200),
        style=wx.STAY_ON_TOP|wx.DEFAULT_FRAME_STYLE):
    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.Show(True)
Save the script and launch it again. Now it looks normal, with a light grey, but still empty, client area. I won't waste time on explaining every single line above, you can check the docs for wxPanel and wxBoxSizer, to see what all those methods and their arguments are. However, understanding every detail is not essential, just know that we have created a panel, assigning it to the current frame (notice the "self" argument in the call to wx.Panel() - that's the instance of the class we are calling wx.Panel() from, the MainWindow class). We then created a BoxSizer, added our panel to it, assigned the sizer to the frame, and stretched the panel to fill the entire client area of the frame. That basically finalizes the setting up part, and we can now proceed to placing the buttons on our panel.
Lets first put one button to see how it's done, since the rest will require almost the same code. We'll create the top leftmost button - the one for "NW". Here's the MainWindow class, with code for the first button added into its __init__ method:
class MainWindow(wx.MiniFrame):
  def __init__(self, title, pos=wx.DefaultPosition, size=(200,200),
        style=wx.STAY_ON_TOP|wx.DEFAULT_FRAME_STYLE):
    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)
    b = buttons.GenButton(panel, 10, "NW", (5,5), (40,40))
    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)
    self.Show(True)
This warrants a detailed walkthrough. Starting with the first line of the new snippet:
b = buttons.GenButton(panel, 10, "NW", (5,5), (40,40))
Oops, stop for a minute and go to the top of your script where you imported wx. We need to do another import, namely the one of the module that contains the classes for different kinds of buttons. Put the following line after the "import wx" one:
import wx.lib.buttons as buttons
That imports the wx.lib.buttons module and assigns to it the name "buttons" inside our script, so that we don't have to type out "wx.lib.buttons" all the time. Now back to our button. Again:
b = buttons.GenButton(panel, 10, "NW", (5,5) (40,40))
Here we create a "generic button". This type of buttons can be flat or have a "3d" look to them, they can have a background colour set, and basically can be tweaked in a number of ways to get them to look the way you want. The first argument passed to the GenButton class is our "panel" - the client area, this tells GenButton that it must create an instance of itself inside the "panel", since everything has to be contained somewhere. In this case, "panel" acts as a container (parent) for the button "b". You can compare that call to the one where "panel" itself was created - there we also passed something in the first argument, namely the "self" object which refers to our main window frame. So "panel" is inside the MainWindow object, and the button is inside the "panel", giving us something like a "matryoshka" (the same thing as a "sputnik" or "perestroyka" essentially). The next argument (the value 10) is the button's internal id, we will later be able to access this button using that number. The third argument ("NW") is the buttons label. Next comes the button's position (x,y) inside the panel, we set it at (5,5) so that there's a margin of 5 pixels above and to the left of the button. The last argument is the size of the button, we set it at (40,40) which again gives us a square with 40 pixels per side.
The second line:
b.SetFont(wx.Font(10, wx.SWISS, wx.NORMAL, wx.NORMAL, False))
This sets the font for the button's label. You can search the docs for wxFont to see what all those arguments mean. I'll just say that the font will have size 10, and will be pretty ... normal, as is evident from the overuse of the wx.NORMAL constant.
The third line:
b.SetBezelWidth(3)
This defines the width of the button's "bezel". The value of 0 should yield a completely flat button with no border, on a WinXP system you won't even notice that its a button until clicking on it. The value of 3 "raises" the button by providing it with a narrow 3d-like border.
b.SetForegroundColour(wx.Colour(128,0,64))
Obviously sets the colour for the button label's colour. You can use a colour picker (Mushclient's own for example) to choose one to your liking. The values are in red,green,blue format. You can also add a line to set the background colour for the button, for example the following line will make the button white:
b.SetBackgroundColour(wx.Colour(255,255,255))
b.SetUseFocusIndicator(False)
This method defines whether a "focus indicator" will be used. A focus indicator is a dotted line box that appears on the button which is set as default - the one that will be clicked if you simply hit Enter inside the panel. This focus indicator looks quite ugly and is completely unnecessary here, so we pass False to this method, to tell it that we don't need the indicator.
Now you can launch the script again and marvel at our first button. I am doing it right now. If you are like me, then you probably clicked on the button a few times and noticed that it doesn't do much. This is the problem we'll be addressing next.
Eventually we'll need the buttons to send commands to Mushclient, therefore we need some mechanism for making the buttons perform actions. This mechanism in wxPython is called "Events". Events occur when something happens inside the application. Most things have certain events attached to them by default. So do buttons. When you click a button a wx.EVT_BUTTON is automatically generated by the application, and in order to respond to it you need to set up a function that will handle that event. Since we don't need our buttons to actually do anything right away, we'll just add an empty method for handling the wx.EVT_BUTTON event to our MainWindow class. We will also add another line to the button-related portion of the code, that will Bind() the wx.EVT_BUTTON event generated by this button to the handler method, so that whenever you click the button this method will be automagically called. The class becomes:
class MainWindow(wx.MiniFrame):
  def __init__(self, title, pos=wx.DefaultPosition, size=(200,200),
        style=wx.STAY_ON_TOP|wx.DEFAULT_FRAME_STYLE):
    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)
    b = buttons.GenButton(panel, 10, "NW", (5,5), (40,40))
    self.Bind(wx.EVT_BUTTON, self.OnClick, b)
    b.SetFont(wx.Font(10, wx.SWISS, wx.NORMAL, wx.NORMAL, False))
    b.SetBezelWidth(3)
    b.SetBackgroundColour(wx.Colour(255,255,255))
    b.SetForegroundColour(wx.Colour(128,0,64))
    b.SetUseFocusIndicator(False)
    self.Show(True)
  def OnClick(self, evt):
    pass
Notice the "self.Bind(...)" line in there - that binds the wx.EVT_BUTTON event to the OnClick method of this class, and specifies that the source of this event is going to be the button contained in the object labeled "b" in our script. The OnClick method doesn't do anything when called, so our button still won't do much, but at least now we have everything in place in order to make it do something later. That finalizes the job of adding the first button to our window. Next we'll need to add all the others. However, as you can see, adding even one button took quite a bit of code, and we need to add another 11 of them! That'll be increadibly messy. Luckily, the code needed to add all those buttons stays the same for all of them, so what we'll do is put in another method that will contain all the code for adding a button and will take the same arguments as buttons.GenButton(). Here's our AddButton method:
  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)
Voila - all the code that we previously had in the __init__ method to create the button now migrated into the AddButton method. This means that we can replace the mess we had in __init__ for our button with just:
self.AddButton(panel, 10, "NW", (5,5), (40,40))
Our class then becomes:
class MainWindow(wx.MiniFrame):
  def __init__(self, title, pos=wx.DefaultPosition, size=(200,200),
        style=wx.STAY_ON_TOP|wx.DEFAULT_FRAME_STYLE):
    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.Show(True)
  def OnClick(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)
Now we add the buttons. We could get fancy and use "sizers" to position our buttons, or come up with code to calculate those positions ourselves, but we'll take a more manual approach and just provide those positions for the AddButton method. Here's the complete MainWindow class with all the buttons already in place:
class MainWindow(wx.MiniFrame):
  def __init__(self, title, pos=wx.DefaultPosition, size=(200,200),
        style=wx.STAY_ON_TOP|wx.DEFAULT_FRAME_STYLE):
    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.Show(True)
  def OnClick(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)
Now, all that arithmetic certainly killed quite a few of my braincells, but in the end we have all the buttons positioned perfectly. However, there's still one minor problem remaining. Since we position the buttons statically - providing set positions for their placement that remain the same throughout the program's lifetime, resizing the main frame won't do much. Therefore, it would make sense to disallow resizing it to begin with and set the precise size. By rough approximation we arrive at the frame's size of (174,190). In order to make the frame non-resizable we need to cancel out the wx.RESIZE_BORDER flag contained inside the wx.DEFAULT_FRAME_STYLE one that we use. Check the docs for wxFrame to see the list of available flags. Those style flags are just integer constants really, and they are OR'd together to form a final bitflag that is interpreted to arrive at the final style. So since wx.RESIZE_BORDER is already included in wx.DEFAULT_FRAME_STYLE we can just XOR it out. Check the argument string for the __init__ method's declaration in the final class for the changes:
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.Show(True)
  def OnClick(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)
And here's the complete script, to make sure that you have the same thing as I:
import wx
import wx.lib.buttons as buttons
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.Show(True)
  def OnClick(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) Â
if __name__ == "__main__":
  app = wx.App()
  app.frame = MainWindow("Compass")
  app.MainLoop()
That's it. Now you should be able to run your application and see a perfectly formatted compass panel. It's not too fancy, but it's simple and what's best - it works! You might have noticed that there's no way to simply hide the panel, without closing it. That's intentional and we'll deal with it in the next tutorial, where we are going to make this app talk with Mushclient.
First of all, it is needed to note that Mushclient itself makes very few provisions for accomplishing this sort of tasks - it does have some callbacks meant to make interacting with your components easier, but it doesn't have a builtin infrastructure for creating the components themselves. This is both a good and a bad thing. It is bad, because you can't add a button to a panel with a single line of code in Mushclient. It is good because it keeps Mushclient small and fast, minimizes the potential for bugs creeping up, and results in you actually learning something useful while trying to implement your GUI desires. An additional plus to doing it the Mushclient way is that since your components aren't being ran by Mushclient, they have no direct effect on the client's performance (the effect is about the same as running an ICQ client at the same time as playing a MUD in Mushclient). Furthermore, you are not limited to any particular kind of GUI components or their layouts - you can do whatever you want, be it a simple health guage or a full-blown remote controller with dozens of buttons, popup menu's, etc, skinned to resemble your favourite look for Winamp and running on a different computer somewhere on the network. On the most basic level both of those types of components require almost identical code to implement, and that code isn't even that complicated once you understand how it works.
Being a fan of Python, I obviously use that language for most of my programming needs. Therefore, this tutorial uses Python as the implementation language. If the only Python you are familiar with is a snake, or is named Monty, then don't despair - Python is a fairly simple programming language and in the examples that follow I don't use any of its features that cannot be grasped after at most 15 minutes of reading through the docs (which come together with the language). However, I won't explain Python-specific issues here, so be prepared to have the docs open if you decided to sit through this.
Before we begin, you will need to have several tools installed on your system to proceed. The first one is obviously Python itself. You can download it from www.python.org (version 2.4 is recommended). Once you have that, you should install the pywin32 library, which is needed to use Python from Mushclient. Download the most recent version of it from HERE. At last, you need the wxPython library, which we'll use for the GUI and which is available from www.wxpython.org . Get the most recent version with Unicode support (just in case) for the version of Python that you have installed previously. All of those come in form of binary installers for Windows, so installing them is usually not a problem. In the case of wxPython, you will need to download the runtime package and the docs and examples package. For editing Python source code on Windows I personally prefer Pythonwin, which comes together with the pywin32 library and a link to which should appear in your Start->Programs->Python menu.
Now that we have all that out of the way, a short overview of how Mushclient's current paradigm of plugging in external GUI components looks is in order.
With Mushclient you are supposed to create a standalone Windows application, that then has to be launched from Mushclient, and must have certain provisions for communicating with it: accepting commands and transmitting data. Recently a new mechanism was added to Mushclient which makes handling this communication simpler - Mushclient plugins became able to send data across the network through the UDP protocol and listen to incoming data on specific ports. This is what we'll use for our program. We'll do it in several steps. First we will construct a standalone application without any means of communicating with anything beside itself. Then we will add to it the functionality needed to communicate with Mushclient. Lastly, we will write a Mushclient plugin meant to control this application.
For this tutorial I decided to make a small panel with buttons holding all walking directions, so you can use your mouse for moving around in the game. The buttons will be able to respond to certain commands from Mushclient and change their colours, so that you can see all visible exits from the room you are in by just looking at this panel. The buttons on the panel will be laid out almost as they are on your keypad. With directional ones forming a square, up and down on the right side of that square, and in/out at the bottom. Okay, without further ado, lets proceed to making this application. Start by launching Pythonwin and creating a new Python script file. Save that empty file in a directory where it will be easy to find, for example C:\\Python_stuff\\Buttons. Name the file "Buttons.pyw". The .pyw extension is a Windows-only thing and tells the interpreter to launch the script as a Windows UI app, without creating a command console. Now put the following line at the top of your script:
CODE
import wx
That loads the wxPython library into your program. Now let's create a class that will hold the application's main window frame:
CODE
class MainWindow(wx.MiniFrame):
  def __init__(self, title, pos=wx.DefaultPosition, size=(200,200),
        style=wx.STAY_ON_TOP|wx.DEFAULT_FRAME_STYLE):
    wx.MiniFrame.__init__(self, None, -1, title, pos, size, style)
    self.Show(True)
In the first line we define a class that inherits from wx.MiniFrame, which is a frame with a very small titlebar and no button of its own in the tool panel. The arguments passed to this class' __init__ method are:
title - what will appear in the title bar as the window's name;
pos - where the window appears on the screen, here we use x.DefaultPosition to make wxPython decide where to put it;
size - the initial size of the window, for now we'll make it a square with a 200 pixel side;
style - the general appearance of the window, check the wxPython docs for wxFrame and look at all of the style constants defined there.
So for now we settle on a square toolbox window that has the default style, the size of 200x200 pixels, default position, and is set to stay on top of all other windows. The next line is the standard initialization of the parent class - we tell wx.MiniFrame that this class is creating its instance, passing to it all the required arguments. Finally, we tell the program to Show() our window. Now you'll see how simple all of this really is...
Add the following to your script, save it, and doubleclick on your Buttons.pyw icon in the Explorer to run it (an explanation will follow):
CODE
if __name__ == "__main__":
  app = wx.App()
  app.frame = MainWindow("Compass")
  app.MainLoop()
You should see your frame appear. It doesn't have any useful components yet, but those won't take long to create either. That's Python at its best - 9 lines of code and you have yourself an application The code above... The "if" statement in the first line is another standard Python trick - this makes sure that whatever is inside that statement's scope will be ran only if the script is launched in standalone mode from the command line, or by doubleclicking on it as in our case. That prevents it from being executed if you import the module somewhere. Second line creates an instance of a wx.App class (check the docs) and assigns it to the "app" variable. Objects of the wx.App class have a "frame" property, which holds the application's frame - we assign to it an instance of our MainWindow class above, setting its title to "Compass". Finally we start the app by calling the "MainLoop()" method of the app object. Now the app is up and running.
Now that we have a working application in its basic shape we can proceed to fill it up with some useful content. First of all, if you look at the running app again you'll notice that its client area has an unusually dark shade of grey, normally empty windows are light grey. This is because the client area is not only empty - it is also virtually non-existant. In order to make use of it we'll need to create a "panel", which is essentially a window which is contained inside another window. We then be able to put other stuff on that panel, such as buttons. Go back to your MainWindow class and modify it to look like this:
CODE
class MainWindow(wx.MiniFrame):
  def __init__(self, title, pos=wx.DefaultPosition, size=(200,200),
        style=wx.STAY_ON_TOP|wx.DEFAULT_FRAME_STYLE):
    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.Show(True)
Save the script and launch it again. Now it looks normal, with a light grey, but still empty, client area. I won't waste time on explaining every single line above, you can check the docs for wxPanel and wxBoxSizer, to see what all those methods and their arguments are. However, understanding every detail is not essential, just know that we have created a panel, assigning it to the current frame (notice the "self" argument in the call to wx.Panel() - that's the instance of the class we are calling wx.Panel() from, the MainWindow class). We then created a BoxSizer, added our panel to it, assigned the sizer to the frame, and stretched the panel to fill the entire client area of the frame. That basically finalizes the setting up part, and we can now proceed to placing the buttons on our panel.
Lets first put one button to see how it's done, since the rest will require almost the same code. We'll create the top leftmost button - the one for "NW". Here's the MainWindow class, with code for the first button added into its __init__ method:
CODE
class MainWindow(wx.MiniFrame):
  def __init__(self, title, pos=wx.DefaultPosition, size=(200,200),
        style=wx.STAY_ON_TOP|wx.DEFAULT_FRAME_STYLE):
    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)
    b = buttons.GenButton(panel, 10, "NW", (5,5), (40,40))
    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)
    self.Show(True)
This warrants a detailed walkthrough. Starting with the first line of the new snippet:
CODE
b = buttons.GenButton(panel, 10, "NW", (5,5), (40,40))
Oops, stop for a minute and go to the top of your script where you imported wx. We need to do another import, namely the one of the module that contains the classes for different kinds of buttons. Put the following line after the "import wx" one:
CODE
import wx.lib.buttons as buttons
That imports the wx.lib.buttons module and assigns to it the name "buttons" inside our script, so that we don't have to type out "wx.lib.buttons" all the time. Now back to our button. Again:
CODE
b = buttons.GenButton(panel, 10, "NW", (5,5) (40,40))
Here we create a "generic button". This type of buttons can be flat or have a "3d" look to them, they can have a background colour set, and basically can be tweaked in a number of ways to get them to look the way you want. The first argument passed to the GenButton class is our "panel" - the client area, this tells GenButton that it must create an instance of itself inside the "panel", since everything has to be contained somewhere. In this case, "panel" acts as a container (parent) for the button "b". You can compare that call to the one where "panel" itself was created - there we also passed something in the first argument, namely the "self" object which refers to our main window frame. So "panel" is inside the MainWindow object, and the button is inside the "panel", giving us something like a "matryoshka" (the same thing as a "sputnik" or "perestroyka" essentially). The next argument (the value 10) is the button's internal id, we will later be able to access this button using that number. The third argument ("NW") is the buttons label. Next comes the button's position (x,y) inside the panel, we set it at (5,5) so that there's a margin of 5 pixels above and to the left of the button. The last argument is the size of the button, we set it at (40,40) which again gives us a square with 40 pixels per side.
The second line:
CODE
b.SetFont(wx.Font(10, wx.SWISS, wx.NORMAL, wx.NORMAL, False))
This sets the font for the button's label. You can search the docs for wxFont to see what all those arguments mean. I'll just say that the font will have size 10, and will be pretty ... normal, as is evident from the overuse of the wx.NORMAL constant.
The third line:
CODE
b.SetBezelWidth(3)
This defines the width of the button's "bezel". The value of 0 should yield a completely flat button with no border, on a WinXP system you won't even notice that its a button until clicking on it. The value of 3 "raises" the button by providing it with a narrow 3d-like border.
CODE
b.SetForegroundColour(wx.Colour(128,0,64))
Obviously sets the colour for the button label's colour. You can use a colour picker (Mushclient's own for example) to choose one to your liking. The values are in red,green,blue format. You can also add a line to set the background colour for the button, for example the following line will make the button white:
CODE
b.SetBackgroundColour(wx.Colour(255,255,255))
CODE
b.SetUseFocusIndicator(False)
This method defines whether a "focus indicator" will be used. A focus indicator is a dotted line box that appears on the button which is set as default - the one that will be clicked if you simply hit Enter inside the panel. This focus indicator looks quite ugly and is completely unnecessary here, so we pass False to this method, to tell it that we don't need the indicator.
Now you can launch the script again and marvel at our first button. I am doing it right now. If you are like me, then you probably clicked on the button a few times and noticed that it doesn't do much. This is the problem we'll be addressing next.
Eventually we'll need the buttons to send commands to Mushclient, therefore we need some mechanism for making the buttons perform actions. This mechanism in wxPython is called "Events". Events occur when something happens inside the application. Most things have certain events attached to them by default. So do buttons. When you click a button a wx.EVT_BUTTON is automatically generated by the application, and in order to respond to it you need to set up a function that will handle that event. Since we don't need our buttons to actually do anything right away, we'll just add an empty method for handling the wx.EVT_BUTTON event to our MainWindow class. We will also add another line to the button-related portion of the code, that will Bind() the wx.EVT_BUTTON event generated by this button to the handler method, so that whenever you click the button this method will be automagically called. The class becomes:
CODE
class MainWindow(wx.MiniFrame):
  def __init__(self, title, pos=wx.DefaultPosition, size=(200,200),
        style=wx.STAY_ON_TOP|wx.DEFAULT_FRAME_STYLE):
    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)
    b = buttons.GenButton(panel, 10, "NW", (5,5), (40,40))
    self.Bind(wx.EVT_BUTTON, self.OnClick, b)
    b.SetFont(wx.Font(10, wx.SWISS, wx.NORMAL, wx.NORMAL, False))
    b.SetBezelWidth(3)
    b.SetBackgroundColour(wx.Colour(255,255,255))
    b.SetForegroundColour(wx.Colour(128,0,64))
    b.SetUseFocusIndicator(False)
    self.Show(True)
  def OnClick(self, evt):
    pass
Notice the "self.Bind(...)" line in there - that binds the wx.EVT_BUTTON event to the OnClick method of this class, and specifies that the source of this event is going to be the button contained in the object labeled "b" in our script. The OnClick method doesn't do anything when called, so our button still won't do much, but at least now we have everything in place in order to make it do something later. That finalizes the job of adding the first button to our window. Next we'll need to add all the others. However, as you can see, adding even one button took quite a bit of code, and we need to add another 11 of them! That'll be increadibly messy. Luckily, the code needed to add all those buttons stays the same for all of them, so what we'll do is put in another method that will contain all the code for adding a button and will take the same arguments as buttons.GenButton(). Here's our AddButton method:
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(wx.Colour(128,0,64))
    b.SetUseFocusIndicator(False)
Voila - all the code that we previously had in the __init__ method to create the button now migrated into the AddButton method. This means that we can replace the mess we had in __init__ for our button with just:
CODE
self.AddButton(panel, 10, "NW", (5,5), (40,40))
Our class then becomes:
CODE
class MainWindow(wx.MiniFrame):
  def __init__(self, title, pos=wx.DefaultPosition, size=(200,200),
        style=wx.STAY_ON_TOP|wx.DEFAULT_FRAME_STYLE):
    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.Show(True)
  def OnClick(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)
Now we add the buttons. We could get fancy and use "sizers" to position our buttons, or come up with code to calculate those positions ourselves, but we'll take a more manual approach and just provide those positions for the AddButton method. Here's the complete MainWindow class with all the buttons already in place:
CODE
class MainWindow(wx.MiniFrame):
  def __init__(self, title, pos=wx.DefaultPosition, size=(200,200),
        style=wx.STAY_ON_TOP|wx.DEFAULT_FRAME_STYLE):
    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.Show(True)
  def OnClick(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)
Now, all that arithmetic certainly killed quite a few of my braincells, but in the end we have all the buttons positioned perfectly. However, there's still one minor problem remaining. Since we position the buttons statically - providing set positions for their placement that remain the same throughout the program's lifetime, resizing the main frame won't do much. Therefore, it would make sense to disallow resizing it to begin with and set the precise size. By rough approximation we arrive at the frame's size of (174,190). In order to make the frame non-resizable we need to cancel out the wx.RESIZE_BORDER flag contained inside the wx.DEFAULT_FRAME_STYLE one that we use. Check the docs for wxFrame to see the list of available flags. Those style flags are just integer constants really, and they are OR'd together to form a final bitflag that is interpreted to arrive at the final style. So since wx.RESIZE_BORDER is already included in wx.DEFAULT_FRAME_STYLE we can just XOR it out. Check the argument string for the __init__ method's declaration in the final class for the changes:
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.Show(True)
  def OnClick(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)
And here's the complete script, to make sure that you have the same thing as I:
CODE
import wx
import wx.lib.buttons as buttons
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.Show(True)
  def OnClick(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) Â
if __name__ == "__main__":
  app = wx.App()
  app.frame = MainWindow("Compass")
  app.MainLoop()
That's it. Now you should be able to run your application and see a perfectly formatted compass panel. It's not too fancy, but it's simple and what's best - it works! You might have noticed that there's no way to simply hide the panel, without closing it. That's intentional and we'll deal with it in the next tutorial, where we are going to make this app talk with Mushclient.
Unknown2005-02-02 23:29:18
Oh yes, I completely forgot - I even came up with an exercise. Try filling in that empty spot in the center of a compass with a button that has a "QL" label. We'll later use that for the "ql" command, and it'll make the panel look better.
Fain2005-02-02 23:29:25
Cool tutorial Avator
Unknown2005-02-02 23:47:54
Why, thank you! Even Gods read it.
Unknown2005-02-03 02:05:22
I had started on making my own wxPython GUI for MUSHclient, but it paused my script when I loaded the main loop from my main script code. I figured that I would just slap the GUI event loop into its own thread and be done with it. This would still give me the ability to load and communicate with objects in the main script. Communication with UDP sounds cool, but perhaps a bit of overkill?
Thanks for this tutorial! I'll be implementing my wxPython GUI much faster now, I think.
Thanks for this tutorial! I'll be implementing my wxPython GUI much faster now, I think.
Unknown2005-02-03 07:21:43
If you are trying to run the thread from inside a script loaded in the client, then it won't work very well. I tried that, but Mushclient doesn't react very well to threads being created - command history scrolling locks up, dialogues get cranky, other things that I don't remember specifically. Not to mention that it'll still lag the hell out of it, every time I try to spin off a thread, Mushclient peaks on CPU usage. Also, not sure if you've tried using callbacks from inside that thread, but I just did to make sure, and it ain't pretty. The following script crashes immediately:
import thread
def CrashTest():
  world.Note("Hi there!")
thread.start_new_thread(CrashTest, ())
So that'll require additional mucking with locks (not sure how you'd go about locking the world object exactly), or possibly COM marshalling, since this is actually an ActiveX call. Add to this the fact that if you happen to reload the script and fail to kill the thread (for example Mushclient beating the thread to the finish line and reloading the script before it dies) you'll run straight into another spectacular crash due to wxPython's issue with the "main thread assertion" (or what it presumes to be the main thread). The same will likely happen if you try to create more than one app.
UDP, on the other hand, is very straighforward and doesn't take much effort to set up. Python's socket lib requires about 5 lines of code to fully handle the client socket, and about 10 lines for the server one. And Mushclient provides callbacks which make things a matter of two lines on its side. You'll also need additional code to interpret commands from the client, but that's not too hard. Overall it's a lot more stable (at least there's no way in hell it can ever crash the client), much faster, and actually easier, since you don't have to worry about the hell that debugging a wx app from inside Mushclient is.
CODE
import thread
def CrashTest():
  world.Note("Hi there!")
thread.start_new_thread(CrashTest, ())
So that'll require additional mucking with locks (not sure how you'd go about locking the world object exactly), or possibly COM marshalling, since this is actually an ActiveX call. Add to this the fact that if you happen to reload the script and fail to kill the thread (for example Mushclient beating the thread to the finish line and reloading the script before it dies) you'll run straight into another spectacular crash due to wxPython's issue with the "main thread assertion" (or what it presumes to be the main thread). The same will likely happen if you try to create more than one app.
UDP, on the other hand, is very straighforward and doesn't take much effort to set up. Python's socket lib requires about 5 lines of code to fully handle the client socket, and about 10 lines for the server one. And Mushclient provides callbacks which make things a matter of two lines on its side. You'll also need additional code to interpret commands from the client, but that's not too hard. Overall it's a lot more stable (at least there's no way in hell it can ever crash the client), much faster, and actually easier, since you don't have to worry about the hell that debugging a wx app from inside Mushclient is.
Unknown2005-02-04 05:11:31
I feel like saying: "Why worry about GUI on MUSHClient when there's zMUD?" but I wont.
Unknown2005-02-04 13:12:22
Obviously, you haven't tried scripting for MUSHclient yet.
zMUD is a real bear on speed. It has some nice features, but I'm getting tired of the quirks. I'm looking for a faster, more stable way to write scripts, and I don't want to use a "bare bones" client like TF or Lyntin. MUSHclient seems to have decent support and is still actively being developed. Also, I'm looking for an excuse to learn Python, so I can add it to my resume. Heh.
zMUD is a real bear on speed. It has some nice features, but I'm getting tired of the quirks. I'm looking for a faster, more stable way to write scripts, and I don't want to use a "bare bones" client like TF or Lyntin. MUSHclient seems to have decent support and is still actively being developed. Also, I'm looking for an excuse to learn Python, so I can add it to my resume. Heh.
Unknown2005-02-05 06:02:48
QUOTE(Zarquan @ Feb 5 2005, 02:12 AM)
Obviously, you haven't tried scripting for MUSHclient yet.
42123
Yes I have, I have tried. zMUD has a much more efficient interface to build scripts, and also it's documentation is a lot more helpful than the MUSHClient ones; I can't seem to get my head around it.
Unknown2005-02-05 09:36:19
Mushclient's scripting interface is actually simpler than Zmud's. Its just that those who use Zmud are so accustomed to the way things are done there that they can't wrap their heads around any other scheme The general difference between scripting in Zmud (the way most people use it) and scripting in Mushclient is that Zmud has a builtin scripting language, while Mushclient doesn't. That's about it. Any mud client does exactly the same thing - matches triggers/aliases and fires timers to execute scripts associated with them. In Zmud, triggers and aliases are normally treated as being the same thing as a script - Zmud is presumed to execute the _script_ when matching incoming or outgoing text. In Mushclient, there's a clear distinction between matching input and output, and calling scripts.
There was actually an almost anecdotal case on Mushclient's forum a while ago. Someone asked about a way of calling a script loaded in one plugin from another plugin (you aren't supposed to do that to avoid making plugins too dependant on each other, so its not very easy to do). Having read through quite a few scripts for Zmud, I immediately came up with a solution which seemed obvious and included matching an alias in the target plugin and having that call a script associated with it. That's what people do in Zmud all the time. For Mushclient users it had an effect of an exploding bomb - a completely unexpected development! The reason why, is that Mushclient users see "scripts" as separate functions which behave according to their own well-defined and predictable laws, while triggers and aliases are something alien to scripts, residing in the client itself and serving mostly as means to notify the client that it must call this or that script function (or perform one of the predefined actions, such as changing the line's colour, setting a variable, etc) if this or that text arrives from the mud or is entered by the user. No one ever thinks of calling an alias in order to call a script function from another script function - it's seen as a contrived, indirect, and slightly demented way of doing it, when you can simply call a function directly from another function, while aliases are there to match on entered commands. So scripting in Zmud can be just as confusing to a Mushclient user, as scripting in Mushclient is for a Zmud one.
There was actually an almost anecdotal case on Mushclient's forum a while ago. Someone asked about a way of calling a script loaded in one plugin from another plugin (you aren't supposed to do that to avoid making plugins too dependant on each other, so its not very easy to do). Having read through quite a few scripts for Zmud, I immediately came up with a solution which seemed obvious and included matching an alias in the target plugin and having that call a script associated with it. That's what people do in Zmud all the time. For Mushclient users it had an effect of an exploding bomb - a completely unexpected development! The reason why, is that Mushclient users see "scripts" as separate functions which behave according to their own well-defined and predictable laws, while triggers and aliases are something alien to scripts, residing in the client itself and serving mostly as means to notify the client that it must call this or that script function (or perform one of the predefined actions, such as changing the line's colour, setting a variable, etc) if this or that text arrives from the mud or is entered by the user. No one ever thinks of calling an alias in order to call a script function from another script function - it's seen as a contrived, indirect, and slightly demented way of doing it, when you can simply call a function directly from another function, while aliases are there to match on entered commands. So scripting in Zmud can be just as confusing to a Mushclient user, as scripting in Mushclient is for a Zmud one.
Unknown2005-02-05 22:03:15
The other difference is every other programming language in the world does things the MUSHclient way.
Fain2005-02-05 22:22:24
Avator, you wouldn't happen to have any tutorials for GUI and buttons in VBscript for Mushclient would ya?
Unknown2005-02-06 23:02:00
QUOTE(Fain @ Feb 5 2005, 10:22 PM)
Avator, you wouldn't happen to have any tutorials for GUI and buttons in VBscript for Mushclient would ya?
43038
Hmm, vbscript is a "pure" scripting language. It doesn't have any facilities for UI building, nor do I think it has any for extending it with 3rd party libraries that do. Visual Basic does however, and the two at least have common syntax. I don't have any experience with VB though, but there was at least one thing done in it for Mushclient that had source code made publically available. That is the MuclientWindow by Poromenous, it can be found here. The classic example of a health bar for Mushclient done in VB was Nick Gammon's Super Health Bar. Hope that helps.
Unknown2006-10-30 16:23:22
Hello, I know this topic is damn old, but I have been playing with what you wrote here and ... could you tell me how to create a toggle button?
I tried this:
b = buttons.ToggleButton(parent, id, label, position, size)
instead of this
b = buttons.GenButton(parent, id, label, position, size)
It would be good if you showed also how to change background colour of that button when it is .. you know clicked(down).
And it didn't work, I was looking in docs for wxPython but they are just damn confusing ...plz help
CODE
I tried this:
b = buttons.ToggleButton(parent, id, label, position, size)
instead of this
b = buttons.GenButton(parent, id, label, position, size)
It would be good if you showed also how to change background colour of that button when it is .. you know clicked(down).
And it didn't work, I was looking in docs for wxPython but they are just damn confusing ...plz help
Unknown2006-10-30 19:38:23
This page might help?
Unknown2006-10-30 21:56:40
That doesnt help with toggle button I think.
Unknown2006-10-30 23:13:04
Toggle buttons can be better implemented as a normal button that changes background colors when pressed. I prefer it, anyway. You did ask about how to change the background color, too.
Charune2006-10-30 23:51:26
I liked this thread so much I decided to pin it for our mushclient users. Wish I had seen it sooner.
Arundor2006-10-31 00:26:27
There's a part 2 as well.
http://forums.lusternia.com/index.php?showtopic=1819
Seems like he was planning a part 3 too, but it looks like it never got posted.
http://forums.lusternia.com/index.php?showtopic=1819
Seems like he was planning a part 3 too, but it looks like it never got posted.
Unknown2006-10-31 09:13:21
QUOTE(Zarquan @ Oct 31 2006, 12:13 AM) 348540
Toggle buttons can be better implemented as a normal button that changes background colors when pressed. I prefer it, anyway. You did ask about how to change the background color, too.
Yep, they could but I'd rather prefer normal toggle butons ... plz tell me
And yeah I know there is part 2, I have read it.