I think some people might be interested in this.
I’ve been fighting with the fact that Component::setBounds
accepts only integer arguments for years, preventing me from getting pixel-perfect alignement of UI elements with rescaling enabled. Recently, I did code some solutions that allowed me to get the results I wanted as elegantly as possible without having to hack anything in the JUCE code base.
The main idea is the following: we want a function such as setFloatBounds(float x, float y, float width, float height)
for our UI components, and a way to handle this information in the associated drawing functions. It is useful when UI design is either skeumorphic (with say a big background PNG where each widget needs to be in a specific place) or vectorial-based so the spacing between elements is always the same whatever the scale factor, specifically if you use a big SVG file and align everything around it.
The function setFloatBounds(float x, float y, float width, float height)
stores the float coordinates somewhere, and set the regular existing integer coordinates as well, so the float bounds rectangle can be inserted inside the integer bounds rectangle, for example like this:
void CustomComponent::setFloatBounds (float newFloatX, float newFloatY, float newFloatWidth, float newFloatHeight)
{
floatX = newFloatX;
floatY = newFloatY;
floatWidth = newFloatWidth;
floatHeight = newFloatHeight;
auto newIntX = juce::roundToInt (std::floor (floatX));
auto newIntY = juce::roundToInt (std::floor (floatY));
auto newIntRight = juce::roundToInt (std::ceil (floatX + floatWidth));
auto newIntBottom = juce::roundToInt (std::ceil (floatY + floatHeight));
setBounds (newIntX, newIntY, newIntRight - newIntX, newIntBottom - newIntY);
}
Then, the drawing function will have to use the float information, such as the offsets between the float rectangle and integer rectangle coordinates, and float drawing methods. For example, here is what I do for drawing an image and some text:
void CustomComponent::paint (juce::Graphics &g)
{
auto rect = juce::Rectangle<float> (floatX - getX(), floatY - getY(), floatWidth, floatHeight);
juce::AffineTransform t = juce::RectanglePlacement (juce::RectanglePlacement::stretchToFit)
.getTransformToFit (image->getBounds().toFloat(), rect);
g.drawImageTransformed (*image, t, false);
// To Do : set the colours, fonts, grab the text to display etc.
juce::GlyphArrangement arr;
arr.addFittedText (fontText, strTextToDisplay,
rect.getX(), rect.getY(),
rect.getWidth(), rect.getHeight(),
juce::Justification::centred,
4,
0.f);
arr.draw (g);
}
In the case of text drawing, there is no Graphics function which allowed what I needed, but I found out that the code inside Graphics::drawFittedText can be copied and pasted in my drawing function, and customized to do what I need.
Now, where should you put that code to? I did it in two ways for now, maybe you’ll find better solutions.
-
I created some custom components in my code, so it’s easy to add those functionalities, since I don’t use LookAndFeel methods there anyway.
-
For widgets like Sliders, I could do the same, by copying and pasting the Slider class code in my project files, and adding the functionalities. Instead, I created new LookAndFeel classes, where I can store the float bounds. That means I need one LF object for each component, which is not so bad since they can be of the same custom LF class. So that’s that LF class which gets the new
setFloatBounds
and painting functions. What I did also in that case is creating new functions directly in the PluginEditor class such as this one:
void ProcessorEditor::setFloatBoundsFor (Slider *slider, CustomSliderLookAndFeel *lf, float floatX, float floatY, float floatWidth, float floatHeight)
{
auto newIntX = juce::roundToInt (std::floor (floatX));
auto newIntY = juce::roundToInt (std::floor (floatY));
auto newIntRight = juce::roundToInt (std::ceil (floatX + floatWidth));
auto newIntBottom = juce::roundToInt (std::ceil (floatY + floatHeight));
slider->setBounds (newIntX, newIntY, newIntRight - newIntX, newIntBottom - newIntY);
lf->setFloatBounds (true, floatX, floatY, floatWidth, floatHeight);
}
So, in the resize function, I can do something like this:
void ProcessorEditor::resized()
{
auto widthImage = imageBackground.getWidth();
auto heightImage = imageBackground.getHeight();
auto scaleFactorX = getWidth() / (float) widthImage;
auto scaleFactorY = getHeight() / (float) heightImage;
setFloatBoundsFor (sliderVolume.get(), theVolumeKnobLF.get(),
JUCE_LIVE_CONSTANT (0.7285f) * scaleFactorX * widthImage,
JUCE_LIVE_CONSTANT (0.1045f) * scaleFactorY * heightImage,
theVolumeKnobLF->getStripWidth() * scaleFactorX,
theVolumeKnobLF->getStripHeight() * scaleFactorY);
}
As you can see, I use the macro JUCE_LIVE_CONSTANT
to set the XY position of the element (could be automated with image processing techniques or whatever), and I have my own LF class to load PNG strips. But more importantly, I don’t use the same scale factor here for the X and Y coordinates. That’s because the whole PluginEditor size needs to be integer, so the X/Y ratio might be off depending on the scale factor. This way, I use the current X/Y ratio instead of the ideal one to set the position of my components.
I hope that will help you people! Tell me if you are using my tricks, or if you have ideas to improve the whole scheme.