Maxon Developers Maxon Developers
    • Documentation
      • Cinema 4D Python API
      • Cinema 4D C++ API
      • Cineware API
      • ZBrush GoZ API
      • Code Examples on Github
    • Forum
    • Downloads
    • Support
      • Support Procedures
      • Registered Developer Program
      • Plugin IDs
      • Contact Us
    • Categories
      • Overview
      • News & Information
      • Cinema 4D SDK Support
      • Cineware SDK Support
      • ZBrush 4D SDK Support
      • Bugs
      • General Talk
    • Unread
    • Recent
    • Tags
    • Users
    • Login

    Connect C4D with Tkinter (External Application) ?

    Cinema 4D SDK
    r21 python
    3
    12
    2.3k
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • B
      bentraje
      last edited by

      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:

      1 Reply Last reply Reply Quote 0
      • r_giganteR
        r_gigante
        last edited by r_gigante

        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.

        1 Reply Last reply Reply Quote 1
        • B
          bentraje
          last edited by

          @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()
          
          1 Reply Last reply Reply Quote 0
          • ferdinandF
            ferdinand
            last edited by ferdinand

            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)
            

            MAXON SDK Specialist
            developers.maxon.net

            1 Reply Last reply Reply Quote 1
            • r_giganteR
              r_gigante
              last edited by r_gigante

              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

              1 Reply Last reply Reply Quote 1
              • ferdinandF
                ferdinand
                last edited by

                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

                MAXON SDK Specialist
                developers.maxon.net

                r_giganteR 1 Reply Last reply Reply Quote 2
                • B
                  bentraje
                  last edited by

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

                  1 Reply Last reply Reply Quote 0
                  • r_giganteR
                    r_gigante
                    last edited by r_gigante

                    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

                    1 Reply Last reply Reply Quote 3
                    • r_giganteR
                      r_gigante @ferdinand
                      last edited by

                      @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

                      1 Reply Last reply Reply Quote 0
                      • B
                        bentraje
                        last edited by

                        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 ?

                        1 Reply Last reply Reply Quote 0
                        • r_giganteR
                          r_gigante
                          last edited by

                          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

                          1 Reply Last reply Reply Quote 1
                          • B
                            bentraje
                            last edited by

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

                            1 Reply Last reply Reply Quote 0
                            • ferdinandF ferdinand referenced this topic on
                            • ferdinandF ferdinand referenced this topic on
                            • First post
                              Last post