Connect C4D with Tkinter (External Application) ?



  • Hi,

    I'm trying to connect C4D with Tkinter (External Application) through sockets (first time using it).
    You can see an illustration of the problem here:
    https://www.dropbox.com/s/vz1ixu3jozeujum/c4d308_python_socket.mp4?dl=0

    I expect that a Cube is made when I press the Cube button, but as you can see nothing happens.
    Interestingly, the Tkinter doesn't error out so I guess there is some connection that was made.

    Here is the code so far(Tkinter):

    from tkinter import *
    import socket
    
    window = Tk()
    data = bytes('', "utf-8")
    def send_msg(msg):
        data = bytes(msg, "utf-8")
    
    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)
    
    window.mainloop()
    
    host, port = '127.0.0.1', 12121 
    
    socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    socket.connect((host, port))
    
    while True:
        socket.sendall(data)
    

    Here is the .pyp file as plugin for C4D.

    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)
    
    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
    
    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()
    

    P.S. Some of the lines for .pyp was copied from the megascans plug-in. If you want a reference of it, you can check it here:



  • 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.



  • @r_gigante

    Thanks for the response. I added c4d.SpecialEventAdd() and the CoreMessage 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=0

    I 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 the SpecialEventAdd() and CoreMessage 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 Cthing is still over my head :(
    It's on line P1MSG_EN1 = pythonapi.PyCapsule_GetPointer(P1MSG_UN1, None)
    with the error of ValueError: PyCapsule_GetPointer called with invalid PyCapsule object

    Just wondering, why do we need to convert the data into C then back into Python ?



  • Hi @bentraje: the code is meant for R23, with python 3: please make have a look at the changes described here to revert the changes to python 2.

    Cheers, R



  • @r_gigante

    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.


Log in to reply