Hey @bentraje,
I would recommend using the example I posted above, only that you replace the MessageData instance with a GeDialog instance. The other thread looks overly specific, a bit overengineered with the decorator, to be a good basic example.
I do not have the time to write a full working example right now, but in pseudo code it would look as shown at the end of the posting. I cannot make much out of random error messages you give me. An AttributeError means that an object does not have an attribute, be it a field/property (myObject._data, myObject.Data) or a function (myObject.SendMessage), so it means that you have not declared MyThread.SendMessage and yet try to call it. As always, we also cannot debug your code for you.
Cheers,
Ferdinand
Code:
ā This is untested 'pseudo-code', I wrote this 'blind'. It demonstrates a pattern and is not meant to be executable code.
"""Provides an example for a thread executing multiple tasks and expressing the execution state
to the outside world.
"""
import c4d
import typing
class WorkerThread (c4d.threading.C4DThread):
"""Wraps the execution of multiple tasks expressed by a set of data in a thread.
The thread exposes the total amount of tasks, already done tasks, and their results to outside
observers.
"""
def __init__(self, data: typing.Collection) -> None:
"""Initializes the worker.
"""
self._data : typing.Collection = data # Stuff to do.
self._results: list[any] = [] # Results of stuff that has been done.
self._taskCount: int = len(data) # Number of things to do in total.
self._finishedTaskCount: int = 0 # Number of things that already have been done.
def Main(self) -> None:
"""Carries out the tasks.
"""
for item in self._data:
self._results.append(self.Compute(item)) # this takes a long time to do.
self._finishedTaskCount += 1
def Compute(self, *args) -> any:
"""Represents a computation step.
"""
return 0
# Read-only properties to access the data of the thread from another thread. You could also just
# let the other thread directly access members (thread._results) or also use fancier things like
# locks/semaphores to make this "nicer". This is a question of taste, read-only properties are
# somewhat a middle ground.
@property
def Results(self) -> tuple[any]: return tuple(self._results)
@property
def TaskCount(self) -> int: return self._taskCount
@property
def FinishedTaskCount(self) -> int: return self._finishedTaskCount
class MyTaskDialog (c4d.gui.GeDialog):
"""Realizes a dialog that runs a thread wrapping multiple tasks.
"""
ID_NEW_THREAD: int = 1000 # Id of a gadget which invokes adding a new thread.
def __init__(self) -> None:
"""
"""
# The worker thread of the dialog, could also be multiple threads as in the other example,
# I kept it simple here.
self._workerThread: WorkerThread | None = None
super().__init__()
def Command(self, mid: int, msg: c4d.BaseContainer) -> bool:
"""Called by Cinema 4D on GUI interactions.
"""
# Something with ID_NEW_THREAD has been pressed, we start try to start a new thread with the
# dummy data [1, 2, 3].
if mid == MyTaskDialog.ID_NEW_THREAD and not self.StartWorkerThread([1, 2, 3]):
# Do something on failure.
pass
return super().Command(mid, msg)
def StartWorkerThread(self, data: typing.Collection, evalFrequency: int = 250) -> bool:
"""Starts a new worker thread and sets the evaluation frequency.
"""
# There is already an ongoing thread.
if isinstance(self._workerThread, WorkerThread):
return False
# Create and start the new thread.
self._workerThread = WorkerThread(data)
self._workerThread.Start()
self.SetTimer(max(100, evalFrequency))
return True
def Timer(self, msg: c4d.BaseContainer) -> None:
"""Called by Cinema 4D for each timer tick.
"""
# Should never happen and (most) GUI functions do this own their own, more a formality.
if not c4d.threading.GeIsMainThreadAndNoDrawThread():
return
t: WorkerThread = self._workerThread
# The thread is still running, just update the UI with the status of the thread.
if t.IsRunning():
c4d.StatusSetSpin()
c4d.StatusSetText(f"Running tasks: {t.FinishedTaskCount}/{t.TaskCount}")
# The thread has finished, do something with the result, shut off the timer, and clear out
# the UI.
else:
results: any = t.Results
print(results)
t.End()
self._workerThread = None
self.SetTimer(0)
c4d.StatusClear()