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

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.