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

MAXON SDK Specialist
developers.maxon.net

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

MAXON SDK Specialist
developers.maxon.net

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