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