Connect C4D with Tkinter (External Application) ?



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