How to repaint a single child component?

I have a component that displays the playhead position in seconds and is repainted every 250ms in a timerCallback. Repainting this component causes the whole component tree to be repainted (or at the very least, all of the ancestor components).

I have tried setting setOpaque(true) on the component I am repainting every 250 ms, but it doesn’t change anything.

Is there a way to repaint only a specific component without triggering all the other components’ paint?

Here is a similar topic that doesn’t have an answer: Repaint without parent repaint

Two methods:

  1. Make your playhead it’s own component and simply move it.
  2. Call repaint ( Rectangle<int> area ) and provide only the area you want repainted.
1 Like
  1. Make your playhead it’s own component and simply move it.

Can you elaborate on this? Why would making the playhead its own component prevent the whole component tree to be redrawn when calling redraw? And just to clarify, the reason I’m calling redraw with a timer is to draw the time, not move the playhead (e.g. I want to display “3:01” if the playhead is at 3 minutes one second in a project).

  1. Call repaint ( Rectangle<int> area ) and provide only the area you want repainted.

I forgot to say, I have tried repaint (getLocalBounds()), but the whole component tree is being repainted (or at least the ancestors). getLocalBounds() is a small area, as the display of the time doesn’t take much place and is it’s own component.

I misunderstood your original question.

Even though it calls all your paint functions, the clip rectangle has only the size of the child component.

Have you tried to use JUCE_ENABLE_REPAINT_DEBUGGING to see what’s really being repainted?

2 Likes

If you don’t want the repaint calls to traverse back up the tree, make a sibling opaque filled background component, make this opaque then locate your changing component ontop of this.

Component::setOpaque(true)

https://docs.juce.com/master/classComponent.html#a7320d543cba40183c894474ab78798ea

Depending on your UI, this may be a background image, or you have to add filled rectangle components, behind the updating one.

2 Likes

@Curlymorphic Either this doesn’t work or I have implemented it wrong :thinking:

I created this sibling component:

    class BpmAndTimeDisplayBackground : public Component
    {
    public:
        BpmAndTimeDisplayBackground() { setOpaque (true); }

        void paint (Graphics& g) override
        {
            g.setColour (Colours::red);
            g.fillRect (getLocalBounds());
        }
    };

As you can see in the following screenshot, the BpmAndTimeDisplayBackground (in red), under BpmAndTimeDisplay (transparent). Even though BpmAndTimeDisplayBackground has setOpaque(true), the parent component is still redrawn when BpmAndTimeDisplay::redraw is called.

Screenshot 2025-02-01 at 2.20.56 PM

Yes, the only thing flashing on screen is the small time component, even though all components are repainted. How does that work? Is it because the Graphics& g that is passed to all paint methods is ignoring everything that is out of the bounds of the small time component?

But ideally, I would much rather that only that one component is redrawn. If setOpaque(true) is set, there is nothing to composite :thinking:

So the time-display is on top of the red background? Of course it needs to be repainted then. Otherwise the paint of the background would cover it completely.

Everything has to be drawn from back to front.

1 Like

I have a minimal example, based on a projucer plugin, that I wrote to clarify to myself my understanding.

class DisplayBackground : public juce::Component
{
    public:
    DisplayBackground()
    {
        setOpaque (true);
    }
    
    ~DisplayBackground()
    {
        std::cout << "DisplayBackground repainted: " << counter << std::endl;
    }
    
    void paint (juce::Graphics& g) override
    {
        g.setColour (juce::Colours::red);
        g.fillRect (getLocalBounds());
        counter++;
    }
    
    private:
    
    long counter {0};
};

class TimedDisplay  : public juce::Component,
juce::Timer
{
    public:
    
    TimedDisplay()
    {
        startTimer(250);
    }
    
    ~TimedDisplay()
    {
        std::cout << "TimedDisplay repainted: " << counter << std::endl;
    }
    
    void paint (juce::Graphics& g) override
    {
        juce::String txt;
        txt << counter;
        g.drawText (txt, getLocalBounds(), juce::Justification::centred);
        counter++;
    }
    
    void timerCallback() override
    {
        repaint();
    }
    
    
    private:
    
    long counter {0};
};

class TestingOpaqueAudioProcessorEditor  : public juce::AudioProcessorEditor
{
    public:
    TestingOpaqueAudioProcessorEditor (TestingOpaqueAudioProcessor& p)
    : AudioProcessorEditor (&p), audioProcessor (p)
    {
        setSize (400, 300);
        addAndMakeVisible (displayBackground);
        addAndMakeVisible (timedDisplay);
    }
    
    ~TestingOpaqueAudioProcessorEditor() override
    {
        std::cout << "Editor repainted: " << counter << std::endl;
    }
    
    //==============================================================================
    void paint (juce::Graphics& g) override
    {
        g.fillAll (getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId));
        
        g.setColour (juce::Colours::white);
        g.setFont (juce::FontOptions (15.0f));
        juce::String txt;
        txt << counter;
        g.drawFittedText (txt, getLocalBounds(), juce::Justification::centred, 1);
        counter++;
    }
    
    void resized() override
    {
        displayBackground.setBounds (0, 0, 300, 30);
        timedDisplay.setBounds (0, 0, 300, 30);
    }
    
    private:
    // This reference is provided as a quick way for your editor to
    // access the processor object that created it.
    TestingOpaqueAudioProcessor& audioProcessor;
    
    long counter {0};
    DisplayBackground displayBackground;
    TimedDisplay timedDisplay;
    
    
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TestingOpaqueAudioProcessorEditor)
};


I would suggest running as a standalone plugin, to view the ````std::cout``` statements on exiting the application.

There are three components, the editor, DisplayBackground & TimeDisplay.

If the project is run with setOpaque(true) you will notice the counter in the TimedDisplay increasing with time, and the editor counter remains constant. When the plugin is terminated and the values of the component paint counters are displayed in the console, the editor will only be repainted a few times, while both the DisplayBackground & TimeDisplay will have been repainted the same, and many times.

if setOpaque(false) is used, when the standalone plugin is run, the display on the screen should be the same as before, with the TimedDisplay incrementing, and the editor remaining at a consistent low number. However on exit, the editors paint() has been called the same number of times as the other two child components. This is because the area of the editor behind the DisplayBackground has been rendered, so the paint() is called, even though the area containing the editors text has not been rendered.

This was tested on MacOS 14.6

1 Like

Expanding on the above, If a second DisplayBackground, and Timed Display are added to the editor component, but placed on the bottom edge. When run you will see both the timed displays incrementing, the editor counter will still display a low number.

Upon exiting on my Mac all paint() functions, including the parent editor have been called the same number of times. This is due to how operating systems batch areas together, so in this example all the widgets have been batched together, all the paint() functions are run, but then only the areas contained within the widgets that are updating and then rendered.

Here is a link to a description that may help:

If you have any other widgets that are also repainting, it may be causing a much larger area to be repainted,

1 Like