Connect C4D with Tkinter (External Application) ?
-
Hi @bentraje, thanks for reaching out to us.
The problem in your design is that you're trying to insert an object not for the main thread but from another thread. You can solve the issue by invoking a c4d.SpecialEventAdd() from your listening thread and intercept it in a MessageData or in GeDialog() where, implementing the CoreMessage() virtual method, you can perform the desired action.
Best, R.
-
Thanks for the response. I added
c4d.SpecialEventAdd()
and theCoreMessage
on the GeDialog().
I expect when I click the tkinter button to print "Hey I am listening!" but I unfortunately get nothing.Here is the revised code:
import c4d import socket from c4d.threading import C4DThread thread_ = None host, port = '127.0.0.1', 12121 pluginID = 131313 pluginIDCommandData = 141414 def create_primitive(primitive_type): if primitive_type == 'cube': doc.InsertObject(c4d.BaseObject(c4d.Ocube)) if primitive_type == 'sphere': doc.InsertObject(c4d.BaseObject(c4d.Osphere)) if primitive_type == 'plane': doc.InsertObject(c4d.BaseObject(c4d.Oplane)) c4d.EventAdd() #Background_Server is driven from Thread class in order to make it run in the background. class BGThread(C4DThread): end = False # Called by TestBreak to adds a custom condition to leave def TestDBreak(self): return bool(self.end) #Start the thread to start listing to the port. def Main(self): self.socket_ = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket_.bind((host, port)) while True: if self.TestBreak(): return self.socket_.listen(5) client, addr = self.socket_.accept() data = "" buffer_size = 4096*2 data = client.recv(buffer_size) create_primitive(primitive_type=data) c4d.SpecialEventAdd(pluginID, 0, 0) class DialogSetting(c4d.gui.GeDialog): def CreateLayout(self): self.SetTitle("Primitive Plugin") self.GroupBegin(id=1, flags=c4d.BFH_SCALEFIT, rows=3, title=("Primitive Plugin"), cols=1, initw = 500) self.AddCheckbox(1001, c4d.BFH_CENTER, 300, 10, "Settings 1") self.AddCheckbox(1002, c4d.BFH_CENTER, 300, 10, "Settings 2") self.AddCheckbox(1003, c4d.BFH_CENTER, 300, 10, "Settings 3") self.GroupEnd() return True def CoreMessage(self, id, msg): if id == pluginID: c4d.StopAllThreads() print ("Hey I am listening!") c4d.EventAdd() return True class CommandDataDlg(c4d.plugins.CommandData): dialog = None def Execute(self, doc): if self.dialog is None: self.dialog = DialogSetting() return self.dialog.Open(dlgtype=c4d.DLG_TYPE_ASYNC, pluginid=pluginIDCommandData, xpos=-1, ypos=-1, defaultw=200, defaulth=150) def RestoreLayout(self, sec_ref): # manage the dialog if self.dialog is None: self.dialog = DialogSetting() return self.dialog.Restore(pluginid=pluginIDCommandData, secret=sec_ref) def PluginMessage(id, data): global thread_ # At the start of Cinema 4D We lunch our thread if id == c4d.C4DPL_PROGRAM_STARTED: thread_ = BGThread() thread_.Start() def main(): c4d.plugins.RegisterCommandPlugin(id=pluginIDCommandData, str="Primitive Plugin", help="Primitive Plugin", info=0, dat=CommandDataDlg(),icon=None) # Execute main() if __name__=='__main__': main()
-
Hi,
I am not the biggest tkinter expert, but your file above seems to have various problems. Here is a commented version instead of me trying to explain everything in a gigantic paragraph. As stated in the code below, my comments are a bit on point and could be construed as harsh or insulting, which is not my intention.
Cheers,
zipit"""I am following here more or less a "the gloves are off approach" in commenting. I do not intend to be rude and it could very well be that I did miss the trick here. But this approach is just way more efficient than being super defensive in my language. This is meant to be constructive. """ from tkinter import * import socket window = Tk() # Python is typeless so this initialization does not make much sense. data = bytes('', "utf-8") def send_msg(msg): """ """ # You are writing here to the variable 'data' in the scope of send_msg, # not to the module attribute of the same name. To shadow this local # variable with the global attribute, you would have to use the global # keyword, like so: # global data data = bytes(msg, "utf-8") # But the keyword global is the devils work and should not be used. # Clicking these buttons won't do anything since everything they do is # to raise send_msg, which is flawed in itself. b1 = Button (window, text="Cube", command=send_msg("cube")) b1.grid(row=0, column=0) b1 = Button (window, text="Sphere", command=send_msg("sphere")) b1.grid(row=1, column=0) b1 = Button (window, text="Plane", command=send_msg("plane")) b1.grid(row=2, column=0) # This is blocking, i.e. the following lines won't be reached until the # event loop of 'window' has finished. It basically works like any other # GUI framework under the sun. window.mainloop() # Only reached after the window has closed. host, port = '127.0.0.1', 12121 socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) socket.connect((host, port)) # This does not make much sense to me either. First of all this is a loop # without an exit condition. But even if we ignore this and the fact that # the whole data thing above does not work and and the event loop of the # window is blocking, this would only send over and over the last message, # i.e. the last button that has been clicked before the app/window has # been closed. while True: socket.sendall(data)
-
Thanks @zipit for jumping on the discussion.
Actually @bentraje's client code looks a bit wonky and I think it should be refactored in something like:
import tkinter, socket client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect(('127.0.0.1', 12121)) window = tkinter.Tk() def SendMessage(msg): client.send(bytes(msg, "utf-8")) print(msg) tkinter.Button(window, text = "Cube", command = lambda:SendMessage("cube")).pack() tkinter.Button(window, text = "Sphere", command = lambda:SendMessage("sphere")).pack() tkinter.Button(window, text = "Plane", command = lambda:SendMessage("plane")).pack() window.mainloop() client.close()
Best, R
-
Hi,
yeah, that could be a way to do it. I personally would do a few things differently though. One might want to consider that also connections to
localhost
can be rejected and even when a connection has been established, messages still can get lost. And I would also always prefer sending integers over strings when ever possible. Implementing the whole thing as a class also seems much more reasonable.Cheers,
zipit -
@r_gigante @zipit
Thanks for the response. Yea, I guess the connection was never made since the socket creation was made after
mainloop
I tried the
tkinter code
provided by @r_gigante.
It works, the problem is it only works once. So after clicking next buttons, primitives are not created.
You can see it here:
https://www.dropbox.com/s/m2mpk2ctjkizgfc/c4d308_python_socket02.mp4?dl=0I can see that the tkinter button works (i.e. prints out the "cube", "plane" etc on every click).
I guess the problem is the c4d plug-in receives it only once.
Is there a way to have theSpecialEventAdd()
andCoreMessage
run every time and not only once? -
Hi @bentraje, I've spent a few hours on researching a solution which could look something like:
Tkinter client-sde
import tkinter, socket client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect(('127.0.0.1', 1234)) window = tkinter.Tk() def SendMessage(msg): client.send(bytes(msg, "utf-8")) print(msg) tkinter.Button(window, text = "Cube", command = lambda:SendMessage("create a cube")).pack() # 'command' is executed when you click the button tkinter.Button(window, text = "Sphere", command = lambda:SendMessage("create a sphere")).pack() # 'command' is executed when you click the button window.mainloop() client.close()
C4D server-side
import c4d import socket from ctypes import pythonapi, c_int, py_object from c4d.threading import C4DThread _thread = None pluginIDMessageData = 151515 class Listener(C4DThread): _stopThread = False _conn = None _addr = None _socket = None def OpenSocket(self): self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._socket.bind(('127.0.0.1', 1234)) self._socket.listen(5) return (self._socket is not None) def CloseSocket(self): # close connection if self._conn is not None: self._conn.close() # close socket if self._socket is not None: self._socket.close() # signal the thread self._stopThread = True # called by TestBreak to adds a custom condition to leave def TestDBreak(self): return self._stopThread def Main(self): # check for connections while not self.TestBreak(): self._conn, self._addr = self._socket.accept() while not self.TestBreak(): #check for data data = self._conn.recv(4096) if not data: break if (data.decode('utf-8') == "create a cube"): c4d.SpecialEventAdd(1234, 1, 0) elif (data.decode('utf-8') == "create a sphere"): c4d.SpecialEventAdd(1234, 2, 0) # close connection and notify self._conn.close() class MyMessageData(c4d.plugins.MessageData): def CoreMessage(self, id, bc): if id == 1234: # convert the message pythonapi.PyCapsule_GetPointer.restype = c_int pythonapi.PyCapsule_GetPointer.argtypes = [py_object] P1MSG_UN1 = bc.GetVoid(c4d.BFM_CORE_PAR1) P1MSG_EN1 = pythonapi.PyCapsule_GetPointer(P1MSG_UN1, None) # check message and act if (P1MSG_EN1 == 1): c4d.documents.GetActiveDocument().InsertObject(c4d.BaseObject(c4d.Ocube)) elif (P1MSG_EN1 == 2): c4d.documents.GetActiveDocument().InsertObject(c4d.BaseObject(c4d.Osphere)) c4d.EventAdd() return True def PluginMessage(id, data): global _thread # At the start of Cinema 4D We lunch our thread if id == c4d.C4DPL_PROGRAM_STARTED: _thread = Listener() if _thread.OpenSocket(): _thread.Start(c4d.THREADMODE_ASYNC, c4d.THREADPRIORITY_LOWEST) return True if id == c4d.C4DPL_ENDACTIVITY: if (_thread): _thread.CloseSocket() _thread.End() return True def main(): c4d.plugins.RegisterMessagePlugin(id=pluginIDMessageData, str="", info=0, dat=MyMessageData()) # Execute main() if __name__=='__main__': main()
Cheers, R
-
@zipit I agree with all your points but for brevity I decided to present a code that would not differ too much from @bentraje initial post.
So far thanks for your remarks!
Cheers, R
-
Hi @r_gigante
Thanks for the response. There is an value error. I tried to solve it but the
C
thing is still over my head
It's on lineP1MSG_EN1 = pythonapi.PyCapsule_GetPointer(P1MSG_UN1, None)
with the error ofValueError: PyCapsule_GetPointer called with invalid PyCapsule object
Just wondering, why do we need to convert the data into C then back into Python ?
-
-
Thanks for the response and the website reference.
I was able to work the code with the following revisions:def CoreMessage(self, id, bc): if id == 1234: P1MSG_UN = bc.GetVoid(c4d.BFM_CORE_PAR1) pythonapi.PyCObject_AsVoidPtr.restype = c_int pythonapi.PyCObject_AsVoidPtr.argtypes = [py_object] P1MSG_EN = pythonapi.PyCObject_AsVoidPtr(P1MSG_UN) # check message and act if (P1MSG_EN == 1): c4d.documents.GetActiveDocument().InsertObject(c4d.BaseObject(c4d.Ocube)) elif (P1MSG_EN == 2): c4d.documents.GetActiveDocument().InsertObject(c4d.BaseObject(c4d.Osphere))
Thanks again. Will close this thread now.
-
-