So I am here learning Python now, just wondering if it would be possible if i had the TCL/TK with Python, could i use it all with mushclient...? Just pondering :)
Ooh I got a big book!
Posted by Chronoti on Fri 17 Oct 2003 04:23 AM — 7 posts, 26,139 views.
If there is a library wrapper for it, then just install the Python library that supports TCL/TK. Same with just about anything else. I even managed to nearly get stuff from wxPython to work, but since I had no way to get a reference to Mushclient's window, it blew up the way I tried to do it. If there is a library that can be used, and doesn't need a window to work, it should be usable.
In fact, I just looked into it. The basic Python install includes something called Tkinter, which provides a mean to run TCL/TK, but if you don't already have it then you need to download the TCL/TK files themselves from: http://www.tcl.tk/
In fact, I just looked into it. The basic Python install includes something called Tkinter, which provides a mean to run TCL/TK, but if you don't already have it then you need to download the TCL/TK files themselves from: http://www.tcl.tk/
As Shadowfyr just said, Tkinter is Python's standard TCL/TK implementation. However, I've never used it for real, so can only say that using Tkinter with Mushclient is no different than using any other windowing library with it. The basic trick is that you can't pop up custom made windows directly from Mushclient. Instead, you need to create a stand-alone window as a separate process, callable from Mushclient via COM. I've done this with wxPython, and although that stuff is still buggy, and I can't make myself dive into Python's debugging issues right now, I could post the full program here, if you are interested.
The general process of opening a separate window from Mushclient using wxPython is rather tricky at start, but no big deal once you figure out what's going on. Basically, you make a program that opens a top level, always-on-top window in response to a COM call, and then accepts further COM calls to control its behavior. The hardest part is to tie the COM server and the application's MainLoop together. Obviously they can't co-exist in the same thread, since MainLoop will hang the server, rendering the application useless. The answer to this is to start the COM server in the main thread and the graph application in a secondary one. A gotcha here is that wxPython refuses to run in any thread other than the main one, but ironically - wxPython will buy any thread for a main one as long as that thread was the first to import it. So you start the server, have it start the graph thread, import wxPython in that thread and enter the message loop. All communication between the server and the window is accomplished through standard queue means of communicating between threads. My current problem is not being able to exit the program without leaving garbage around. Frustratingly, it used to work just fine, but then I decided to throw a couple of Mushclient callbacks into it (just to live up to Nick's Super Health Bar standard) and it went haywire - won't start more than once in the same MC session, so I put it to rest until I cool off enough to look at it again :)
The general process of opening a separate window from Mushclient using wxPython is rather tricky at start, but no big deal once you figure out what's going on. Basically, you make a program that opens a top level, always-on-top window in response to a COM call, and then accepts further COM calls to control its behavior. The hardest part is to tie the COM server and the application's MainLoop together. Obviously they can't co-exist in the same thread, since MainLoop will hang the server, rendering the application useless. The answer to this is to start the COM server in the main thread and the graph application in a secondary one. A gotcha here is that wxPython refuses to run in any thread other than the main one, but ironically - wxPython will buy any thread for a main one as long as that thread was the first to import it. So you start the server, have it start the graph thread, import wxPython in that thread and enter the message loop. All communication between the server and the window is accomplished through standard queue means of communicating between threads. My current problem is not being able to exit the program without leaving garbage around. Frustratingly, it used to work just fine, but then I decided to throw a couple of Mushclient callbacks into it (just to live up to Nick's Super Health Bar standard) and it went haywire - won't start more than once in the same MC session, so I put it to rest until I cool off enough to look at it again :)
Hmm. Do you mean that if you use CreateObject, it still runs in an unusable thread? That makes no sense unless you are creating an instance of the engine and running it there, in which case you haven't solved the problem, which is that events are unusable when the script is inactive. If you mean you are creating an instance that does have a seperate thread, then I don't get what could be wrong. But one thing is certain, you need to instance it in such a way that Python runs it as a completely independent thread.
Of course, Nick has talked about including in the next version a way to get the window handle for the current world, which will help, but it can't solve every problem. It is too bad they didn't make scripts extensible so you could code something like this:
Parallel Sub OnButton1_Click()
End Sub
And have mushclient dump those things identified as parallel into a seperate script process that uses the same plugin or global variables, but being parallel to the main thread, can respond to events like the button click above. Of course, once someone works all the bugs out of using a Python script the way you are trying, then this becomes less relevent, but it would be soooo much easier. lol
As it is, being able to use Python scripts to create a window that is the child of the current one will let you display a picture or dump text into a Richtext control, but not interact in any way with those things or animate anything (unless for the animation you can get by with 1 frame per second from a timer).
Of course, Nick has talked about including in the next version a way to get the window handle for the current world, which will help, but it can't solve every problem. It is too bad they didn't make scripts extensible so you could code something like this:
Parallel Sub OnButton1_Click()
End Sub
And have mushclient dump those things identified as parallel into a seperate script process that uses the same plugin or global variables, but being parallel to the main thread, can respond to events like the button click above. Of course, once someone works all the bugs out of using a Python script the way you are trying, then this becomes less relevent, but it would be soooo much easier. lol
As it is, being able to use Python scripts to create a window that is the child of the current one will let you display a picture or dump text into a Richtext control, but not interact in any way with those things or animate anything (unless for the animation you can get by with 1 frame per second from a timer).
Here's what I am talking about exactly. The form won't let me put everything in one post, so it comes in 2 installments: StatusGauge.py (COM server) and status_gauge_gui.py (sec thread - gui stuff, wxPython).
StatusGauge.py is first:
Beware, opening the window more than once in one MC session results in a crash!
StatusGauge.py is first:
import win32com.client
import thread, Queue
import pythoncom
#This is the actual COM server.
class StatusGauge:
_public_methods_ = ['Create', 'Update', 'Destroy']
_public_attrs_ = ['pos','size','title']
_readonly_attrs_ = ['title','pos','size']
_reg_clsid_ = "{26417C84-E596-11D7-B993-444553540000}"
_reg_desc_ = "Status gauge for Mushclient"
_reg_progid_ = "Status.Gauge"
def __init__(self):
#These are the queues used for communication between
#the COM object and the GUI thread. Currently only the
#inQueue is used, and outQueue (used by the GUI to talk
#back to the COM) is just here for future enhancements
self.inQueue = Queue.Queue(10)
self.outQueue = Queue.Queue(10)
def Create(self,
callingworld,
maxMana,
maxHealth,
pluginID,
xpos = 10,
ypos = 400,
title = "Status gauge"):
""" This is the method to call when you want to actually display
the gauge. It will set up all the supplied parameters for visual
display and start the GUI thread. """
self.size = (270, 60)
self.pos = [xpos, ypos]
self.MaxMana = maxMana
self.MaxHealth = maxHealth
self.pluginID = pluginID
self.title = title
self.thread = None
self.thread = NewThread(callingworld,
pluginID,
self.inQueue,
self.outQueue,
self.title,
self.pos,
self.size,
self.MaxHealth,
self.MaxMana)
self.thread.Start()
return
def Update(self, barNum, val):
""" Call this to update the bars on the gauge. Use: <comobject>.Update(<barNum>, <value>)
where <barNum> denotes which bar to update and <value> sets the new value for the bar.
<barNum> is currently 0 for mana (lower bar) and a positive integer for health (upper bar) """
self.values = [barNum, val]
self.inQueue.put(self.values)
self.thread.Wake()
class NewThread:
def __init__(self, callingworld, pluginid, inQueue, outQueue, title, pos, size, maxHealth, maxMana):
""" This creates the new thread where the GUI frame will run """
self.pluginid = pluginid
self.callingworld = callingworld
self.inQueue = inQueue
self.outQueue = outQueue
self.title = title
self.pos = pos
self.size = size
self.WakeUp = None
self.maxHealth = maxHealth
self.maxMana = maxMana
def Start(self):
""" Starts the thread. """
thread.start_new_thread(self.Run, ())
def Wake(self):
""" calls the GUI's WakeUp method to make it look into the inQueue for new
data. This should be called by COM object's Update() method right after putting
data into the inQueue """
self._app.WakeUp()
def Run(self):
""" Imports and runs the GUI app. When the window is closed, MainLoop exits killing
the thread automagically, however without explicitly terminating the method with
'return', the thread will keep running even after the app is closed and destroyed. """
import status_gauge_gui
self._app = status_gauge_gui.GUI(self.callingworld,
self.pluginid,
self.title,
self.pos,
self.size,
self.maxHealth,
self.maxMana)
self._app.inQueue = self.inQueue
self._app.outQueue = self.outQueue
self._app.MainLoop()
return
def KillGauge(self):
""" This is called by COM's Destroy() method, doesn't work yet."""
self._app.KillGauge()
#This registers the program with Windows when run from the prompt or Explorer
#Need to run it at least once before it is usable through COM
if __name__ =='__main__':
import win32com.server.register
win32com.server.register.UseCommandLine(StatusGauge)
Beware, opening the window more than once in one MC session results in a crash!
And status_gauge_gui.py won't even fit in a single post - need to rewrite this stuff :( It comes in 2 installments of its own:
Part I:
Part I:
from wxPython.wx import *
import wx
import Mushclient
import win32com.client
#FIXME: get rid of this, hide everything in class instances
class GenStruct:
pass
struct = GenStruct()
struct.MaxBarWidth = 200
struct.ManaBar = struct.MaxBarWidth
struct.HealthBar = struct.MaxBarWidth
struct.MaxMana = 2500
struct.MaxHealth = 3500
struct.barHeight = 15
#This block of code defines the Update event, called when we want to repaint a bar
wxEVT_UPDATE_BARGRAPH = wxNewEventType()
def EVT_UPDATE_BARGRAPH(win, func):
win.Connect(-1, -1, wxEVT_UPDATE_BARGRAPH, func)
class UpdateBarEvent(wxPyEvent):
def __init__(self, barNum, value):
wxPyEvent.__init__(self)
self.SetEventType(wxEVT_UPDATE_BARGRAPH)
self.barNum = barNum
self.value = value
#Block ends here
class MainWindow(wxMiniFrame):
""" Creates a miniframe by deriving a new instance of wxMiniFrame """
def __init__(self, callingworld, pluginid, parent,id,title, pos, size, maxHealth, maxMana):
wxMiniFrame.__init__(self,parent,-1, title, pos, size = (400,80),
style=wxSYSTEM_MENU|wxSTAY_ON_TOP|wxTINY_CAPTION_VERT, name="MiniFrame")
panel = wxPanel(self, -1)
panel.Fit()
self.pluginID = pluginid
self.callingworld = callingworld
struct.Health = maxHealth
struct.Mana = maxMana
struct.MaxBarWidth = int(float(size[0])*0.80)
self.graph = GraphWindow(self, ['H:','M:'])
self.graph.SetSize(size)
sizer = wxBoxSizer(wxVERTICAL)
sizer.Add(panel, 0, wxEXPAND)
sizer.Add(self.graph, 1, wxEXPAND)
self.SetSizer(sizer)
self.SetAutoLayout(True)
sizer.Fit(self)
EVT_UPDATE_BARGRAPH(self, self.OnUpdate)
EVT_CLOSE(self, self.OnCloseWindow)
self.Show(true)
def OnCloseWindow(self, event):
self.NotifyIsDead()
self.Destroy()
def OnUpdate(self, evt):
self.graph.SetValue(evt.barNum, evt.value)
self.graph.Refresh(False)
def NotifyIsDead(self):
try:
world = win32com.client.Dispatch(self.callingworld)
world.Callplugin(self.pluginID, "WindowClosed", "y")
world = None
return
except:
pass
Part II of status_gauge_gui.py:
class GraphWindow(wxWindow):
def __init__(self, parent, labes):
wxWindow.__init__(self, parent, -1)
self.labels = []
self.values = [struct.MaxBarWidth, struct.MaxBarWidth]
self.healthChange = 0
self.manaChange = 0
for label in labes:
self.labels.append(label)
font = wxFont(8, wxSWISS, wxNORMAL, wxBOLD)
self.SetFont(font)
EVT_ERASE_BACKGROUND(self, self.OnEraseBackground)
EVT_PAINT(self, self.OnPaint)
def SetValue(self, type, value):
if type:
if struct.Health != value:
self.healthChange = value - struct.Health
struct.Health = value
if struct.Health > struct.MaxHealth:
struct.MaxHealth = struct.Health
self.values[0] = int((float(struct.Health)/(float(struct.MaxHealth)/100.0))*float(struct.MaxBarWidth)/100.0)
else:
if struct.Mana != value:
self.manaChange = value - struct.Mana
struct.Mana = value
if struct.Mana > struct.MaxMana:
struct.MaxMana = struct.Health
self.values[1] = int((float(struct.Mana)/(float(struct.MaxMana)/100.0))*float(struct.MaxBarWidth)/100.0)
def OnPaint(self, evt):
size = self.GetSize()
bmp = wxEmptyBitmap(size.width, size.height)
dc = wxMemoryDC()
dc.SelectObject(bmp)
self.DrawBars(dc, size)
wdc = wxPaintDC(self)
wdc.BeginDrawing()
wdc.Blit(0,0, size.width, size.height, dc, 0,0)
wdc.EndDrawing()
dc.SelectObject(wxNullBitmap)
dc.Clear()
def DrawBars(self, dc, size):
dc.SetFont(self.GetFont())
dc.SetBackground(wxBrush(wxBLACK))
dc.Clear()
self.DrawHealthBar(dc, size)
self.DrawManaBar(dc, size)
def DrawHealthBar(self, dc, size):
ypos = int(float(size.height)/4.0) - int(float(struct.barHeight)/2.0)
label = self.labels[0]
dc.SetTextForeground(wxWHITE)
dc.DrawText(label, 2, ypos)
val = self.values[0]
if val:
if val < struct.MaxBarWidth/3:
color = wxRED
elif struct.MaxBarWidth/3 <= val <= (struct.MaxBarWidth/3)*2:
color = 'Yellow'
elif val > (struct.MaxBarWidth/3)*2:
color = wxGREEN
dc.SetPen(wxPen(color))
dc.SetBrush(wxBrush(color))
dc.DrawRectangle(20, ypos, val, struct.barHeight)
if self.healthChange < 0:
dc.SetTextForeground(wxRED)
string = str(self.healthChange)
dc.DrawText(string, val + 25, ypos)
elif self.healthChange > 0:
dc.SetTextForeground(wxGREEN)
string = "+" + str(self.healthChange)
dc.DrawText(string, val + 25, ypos)
def DrawManaBar(self, dc, size):
ypos = int((float(size.height)/4.0) - int(float(struct.barHeight)/2.0))*4
label = self.labels[1]
dc.SetTextForeground(wxWHITE)
dc.DrawText(label, 2, ypos)
val = self.values[1]
if val:
if val < struct.MaxBarWidth/3:
color = wxRED
elif struct.MaxBarWidth/3 <= val <= (struct.MaxBarWidth/3)*2:
color = 'Yellow'
elif val > (struct.MaxBarWidth/3)*2:
color = wxGREEN
dc.SetPen(wxPen(color))
dc.SetBrush(wxBrush(color))
dc.DrawRectangle(20, ypos, val, struct.barHeight)
if self.manaChange < 0:
dc.SetTextForeground(wxRED)
string = str(self.manaChange)
dc.DrawText(string, val + 25, ypos)
elif self.manaChange > 0:
dc.SetTextForeground(wxGREEN)
string = "+" + str(self.manaChange)
dc.DrawText(string, val + 25, ypos)
def OnEraseBackground(self, evt):
pass
class GUI(wxPySimpleApp):
def __init__(self, callingworld, pluginid, title, pos, size, maxHealth, maxMana):
wxPySimpleApp.__init__(self)
self.frame = MainWindow(callingworld, pluginid, NULL, -1, title, pos, size, maxHealth, maxMana)
def WakeUp(self):
if self.inQueue != None:
try:
value = self.inQueue.get_nowait()
except Queue.Empty:
pass
self.barNum = value[0]
self.val = value[1]
evt = UpdateBarEvent(self.barNum, self.val)
wxPostEvent(self.frame, evt)