Solved Cancel Option for Progress Bar Dialog?

Hi,

I'm using the illustration code in this thread.
It works.

But the problem is that it freezes viewport interaction. I can't cancel the progress bar midway the process. It has to be completed.

Is there a way around this?

import c4d
from c4d import gui

import time


class TestDialog(gui.GeDialog):
    
    PROGRESSBAR = 1001
    ID_BTN_01 = 1002
    ID_BTN_02 = 1003

    def __init__(self):
        self.progress = 0
        self.prim_list = [c4d.Ocube, c4d.Osphere, c4d.Ocylinder, c4d.Oplane, c4d.Otorus, c4d.Opyramid]
        self.prim_length = len(self.prim_list)

    def StopProgress(self):
        self.progress = 0
        progressMsg = c4d.BaseContainer(c4d.BFM_SETSTATUSBAR)
        progressMsg.SetBool(c4d.BFM_STATUSBAR_PROGRESSON, False)
        self.SendMessage(self.PROGRESSBAR, progressMsg)
    
    def CreateLayout(self):
        self.SetTitle("ProgressBar Example")
        
        self.GroupBegin(id=1000, flags=c4d.BFH_SCALEFIT|c4d.BFV_TOP, cols=0, rows=1)
        self.AddCustomGui(self.PROGRESSBAR, c4d.CUSTOMGUI_PROGRESSBAR, "", c4d.BFH_SCALEFIT|c4d.BFV_SCALEFIT, 0, 0)
        self.GroupEnd()
        self.GroupBegin(id = 1005, flags=c4d.BFH_SCALEFIT|c4d.BFV_TOP, cols=2, rows=1)
        self.AddButton(self.ID_BTN_01, c4d.BFH_SCALEFIT, 100, 15, name="Create Primitives") 
        self.GroupEnd()
        
        return True

    
    def Command(self, id, msg):
        if (id == self.ID_BTN_01):
            #do something short 
            opecnt = self.prim_length;

            for prim in self.prim_list:
                obj = c4d.BaseObject(prim)
                doc.InsertObject(obj)
                c4d.EventAdd()

            for x in range(opecnt):
                self.progress += 1
                progressMsg = c4d.BaseContainer(c4d.BFM_SETSTATUSBAR)
                progressMsg[c4d.BFM_STATUSBAR_PROGRESSON] = True
                progressMsg[c4d.BFM_STATUSBAR_PROGRESS] = self.progress/opecnt
                self.SendMessage(self.PROGRESSBAR, progressMsg)
                time.sleep(1)
            self.StopProgress()

        return True
        
    def AskClose(self):
        self.StopProgress()
        return False


if __name__=='__main__':
    dialog = TestDialog()
    dialog.Open(dlgtype=c4d.DLG_TYPE_ASYNC, pluginid=0, defaulth=100, defaultw=400)

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:
:warning: 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()

MAXON SDK Specialist
developers.maxon.net

Hello @bentraje,

Thank you for reaching out to us. As announced here, Maxon is currently conducting a company meeting. Please understand that our capability to answer questions is therefore limited at the moment.

Your code has blocking behavior because you designed/wrote it so. It is specifically these lines:

for x in range(opecnt):
    self.progress += 1
    progressMsg = c4d.BaseContainer(c4d.BFM_SETSTATUSBAR)
    progressMsg[c4d.BFM_STATUSBAR_PROGRESSON] = True
    progressMsg[c4d.BFM_STATUSBAR_PROGRESS] = self.progress/opecnt
    self.SendMessage(self.PROGRESSBAR, progressMsg)
    time.sleep(1)
self.StopProgress()

This happens inside Command which is executed on the main thread. Your loop therefore blocks the main thread, nothing can happen until opecnt seconds have elapsed.

When you want the action ID_BTN_01 to be non-blocking you will have to implement it as a C4DThread. The idea is then to start the thread in MyDialog.Command and with MyDialog.Timer regularly poll the thread for its finish status while updating for example a status bar. In this thread I recently demonstrated the general pattern of threading in the sense of making something non-blocking for the main thread.

:warning: Please note that you can NOT update/modify the GUI from within a thread; you must defer GUI updates to the main thread. For example by checking your thread in MyDialog.Timer. Inside a C4DThread all threading restrictions do apply.

Cheers,
Ferdinand

MAXON SDK Specialist
developers.maxon.net

Hi @ferdinand

RE: limited at the moment.
No worries. Take your time

Thanks for the response and the reference post.
I managed to implement the thread but I'm stuck on using it along side the GeDialog.

Specifically this part.
self.SendMessage(self.PROGRESSBAR, progressMsg)
since this is now inside the Thread class rather than Dialog class.
This gives me an error of
AttributeError: 'MyThread' object has no attribute 'SendMessage'

How should I refactor it?

I tried doing the following code base on this thread (https://plugincafe.maxon.net/topic/12310/best-plugin-type-for-background-thread-processing/14)
self.dlg = weakref.ref(dlg)

but it gives me an error
AttributeError: 'weakref' object has no attribute 'SendMessage'

How should I go about it?

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:
:warning: 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()

MAXON SDK Specialist
developers.maxon.net