Okay, thanks to some Juce changes and some fresh thinking inspired by Jules, I solved the problem described in the original post.
I now have a handy class that allows a transparent Component to have a smaller opaque sibling component sit behind it in the z-order and follow it around, handling all cases (component getting deleted, reparented, visibility change, z-order change). This solution allows me to keep all the drawing in a single paint() function while still having controls whose border can be transparent. Here’s an example
void AwesomeButton::paint (Graphics& g)
Rectangle<int> bounds = getLocalBounds();
Rectangle<int> r = bounds;
p.addRoundedRectangle ( r.getX()+frameSize/2, r.getY()+frameSize,2
r.getWidth()-frameSize, r.getHeight()-frameSize, cornerRadius);
g.setColour (Colour::fromRGBA (0, 0, 0, 128)); // 50% black
g.strokePath (p, frameSize);
Even though the button has rounded corners and a transparent frame, the interior of the rounded rectangle is in fact fully opaque, and I can still get the optimized drawing for the solid portion while keeping a transparent border of 3 pixels, using the technique of having an opaque “blocker” component sit behind my button in the z-order. This fully addresses the use-case in the original post.
I totally agree that hacking up the Component would have been a poor choice. However, having a separate component that draws the frame or drop shadow, was an unsatisfying solution for many reasons (my buttons are more complicated than this trivialized example). With the change to detecting z-order changes (http://rawmaterialsoftware.com/viewtopic.php?f=2&t=6420) it is possible to implement this without error.