How to get the usual render window behavior when using the render operator for multiple frames?
The whole picture

Currently there is no way executing the render process in the background using  bpy.ops.render.render() and even worse, the operator doesn't provide any arguments to pass a frame or the output path. It is like it is, and hopefully someone is going to implement that in the near future.

The limitations aside, we need to know whether the user started the rendering, if the user interrupted the process and if the rendering is finished to get the usual behavior of the render window, in any case. 

In order to catch these 'states' we can wrap the render operator into a modal operator and use some of the available render handlers to get the information we need. The modal also keeps the UI responsive (non-blocking) and we can trigger a function in a very short (unnoticeable) interval by adding a timer to get the user interaction and handle all other events.

Read: Is it possible to make a sequence of renders and give the user the option to cancel the process through the UI at any given time?



Render camera markers in the timeline

To get the usual behavior and window for rendering the markers in the timeline, I would suggest the following changes to the operator of the linked answer:


Modal operator which allows to render all camera markers (base color of the shader is keyframed)

Assign the marker references to the shot variable in the execute method. That's simple enough and you still have access to all marker properties in the modal method:

def execute(self, context):
    # Define the variables during execution. This allows
    # to define when called from a button
    self.stop = False
    self.rendering = False

    # Get all markers in a list and assign them to shot variable
    self.shots = [m for m in context.scene.timeline_markers if m.camera is not None]
    ...


Read the first element of the shot-list to get TimeLineMarker.frame and TimeLineMarker.camera attribute in order to set the frame and assemble the output file path based on that: 

    elif self.rendering is False: # Nothing is currently rendering.
                                  # Proceed to render.
        if self.shots:  
            # Get the first marker in the list
            marker = self.shots[0]
            frame_number = marker.frame
            camera_name = marker.camera.name

            # Assemble the file path 
            # -&gt; output path + camera name + frame
            scn.render.filepath = os.path.join(
                        self.path, 
                        "{}_{}.jpg".format(camera_name, frame_number))

            # Set the frame
            scn.frame_set(frame_number)

            # Call the render operator
            bpy.ops.render.render("INVOKE_DEFAULT", write_still=True)                           

return {"PASS_THROUGH"}


Finally reset the output path in case the user interrupts the rendering:

if event.type == 'TIMER':
    scn = context.scene

    # No frames in the list or user hits ESC
    if True in (not self.shots, self.stop is True):

        ...

        # Reset the output path
        scn.render.filepath = self.path
        return {"FINISHED"}




Note: As of 2.81+ you'll have to add new depsgraph arguments to all handler methods:

# Set up the handler methods
def pre(self, scene, depsgraph):
    self.rendering = True

def post(self, scene, depsgraph):
    self.shots.pop(0)
    self.rendering = False

def cancelled(self, scene, depsgraph):
    self.stop = True


Otherwise the setup will cause an infinite loop for whatever reason.