BitmapButton change image at runtime

Started by Blaxxun, 26 October 2019, 14:04:24

Blaxxun

Hello forum,

I have a BitmapButton and a keyboard shortcut. (I want to create a toggle button)
Just using the button with the mouse works fine.
Using a keyboard shortcut i cant access the BitmapButton properly (crash).
My goal is to toggle the image of the button everytime when the shortcut is pressed.

This minimal code works fine:

tgui::BitmapButton::Ptr BBTN_Erase = tgui::BitmapButton::create();

BBTN_Erase->setImage("img/BTN_Eraser.png");
BBTN_Erase->setImageScaling(0);
BBTN_Erase->connect("pressed", [&]()
{
if (EraserON)
{
EraserON = false;
BBTN_Erase->setImage("img/BTN_Eraser.png");
}
else
{
EraserON = true;
BBTN_Erase->setImage("img/BTN_EraserON.png");
}
});
gui.add(BBTN_Erase);



And this code throws an exeption:
if (event.key.code == sf::Keyboard::E)
{
if (EraserON)
{
EraserON = false;
tgui::BitmapButton::Ptr BBTN_Erase = gui.get<tgui::BitmapButton>("BTTN_Erase");
BBTN_Erase->setImage("img/BTN_Eraser.png");
}
else
{
EraserON = true;
tgui::BitmapButton::Ptr BBTN_Erase = gui.get<tgui::BitmapButton>("BTTN_Erase");
BBTN_Erase->setImage("img/BTN_EraserON.png");
}
}


What am i doing wrong?

Thanks!!  :)

texus

Check if BBTN_Erase is a nullptr when calling BBTN_Erase->setImage. If it is then it means no widget was added to the gui with name "BTTN_Erase".
Are you passing that string as second argument to gui.add?

Blaxxun

#2
Hello, thankls for your answer!

First i created the BBTN_Erase BitmapButton right after int main(void)
{
with
auto BBTN_Erase = tgui::BitmapButton::create();

and i tryd to change/set the Image in the event part of the code with  if (event.key.code == sf::Keyboard::E) BBTN_Erase->setImage("img/BTN_Eraser.png");
But the BBTN_Erase was unknown in this scope.


So i changed the code to the code that i posted in my first post.

I add the BitmapButton with gui.add(BBTN_Erase); like in my first post, when u scroll down the code section u see it.

So i guess that, the error i made, is to believe that the pointer name is the widget name...

I will check... thanks!

texus

It is impossible for a program to know what variable name you used, the compiler just translates it to an address in memory, so you do need to pass the string yourself
Code (cpp) Select
gui.add(BBTN_Erase, "BBTN_Erase");

Blaxxun

I understand.
i did this now to create the button (minimal code):

auto BBTN_Erase = tgui::BitmapButton::create();
BBTN_Erase->setImage("img/BTN_Eraser.png");
BBTN_Erase->connect("pressed", [&]()
{
if (EraserON) {
EraserON = false;
BBTN_Erase->setImage("img/BTN_Eraser.png");}
else {
EraserON = true;
BBTN_Erase->setImage("img/BTN_EraserON.png");}  <---------- Exception thrown here when i hit keyboard
});
gui.add(BBTN_Erase,"BBTN_Erase");



if (event.key.code == sf::Keyboard::E)
{
if (EraserON) {
EraserON = false;
tgui::BitmapButton::Ptr BBTN_Erase = gui.get<tgui::BitmapButton>("BBTN_Erase");
BBTN_Erase->setImage("img/BTN_Eraser.png");
}
else {
EraserON = true;
tgui::BitmapButton::Ptr BBTN_Erase = gui.get<tgui::BitmapButton>("BBTN_Erase");
BBTN_Erase->setImage("img/BTN_EraserON.png");
}


Compiles fine but i get an exeption in the button creation part! When i hit my shortcut "E" on my keyboard.

texus

Is the issue resolved when changing the "&" to "=" in the BBTN_Erase->connect line?
You are currently storing a reference in that lambda. If the BBTN_Erase variable goes out of scope (so at the end of the function that contains the code you showed) then this reference will become invalid. This would cause a crash when trying to use the variable when the lambda function is being executed.
The Widget::Ptr is a pointer, so it can be passed by value without having to worry about performance with copying it.

Blaxxun

Okay, i changed it now to BBTN_Erase->connect("pressed", [=]() { do something }); and it works.
First several variables i had in there went red. I had them right after int main(void){}.
Now i put those variables in the header main.h and it seems to work now.

I also had a slider that used [&] first and always worked before (Hue was already in the header).
Now i changed it to slider->connect("ValueChanged", [=]() { Hue = slider->getValue(); }); and it works again.

Thank you very much!  :)

Blaxxun

I actually want to simulate some Tool buttons.
When you choose a tool then the button stays pressed.
When u choose another tool then this button stays pressed and the previous goes back to unpressed.
I dont know if BitmapButton is the right choice here.

Maybe you have a better oversight of possible features i can use?
Is there a better choice when every button should have a little tool icon?

texus

Actually putting those variables in the header isn't a good solution (I'm not even sure why it would work).
When I told you to use "=", I wasn't really looking at your code. The EraserON and Hue values probably still need to be passed as references to the lambda, by using "=" instead of "&" you are making copies of them and you would be changing a local variable inside the lambda instead of the variable that you had outside. You might want to look up how lambdas work, but the "=" and "&" are just the defaults for the variables. To pass some variables by value and some by reference you would do something like
Code (cpp) Select
slider->connect("ValueChanged", [&,slider]() { Hue = slider->getValue(); }); // All variables passed by reference except slider
or
Code (cpp) Select
slider->connect("ValueChanged", [=,&Hue]() { Hue = slider->getValue(); }); // All variables passed by value except Hue

QuoteWhen you choose a tool then the button stays pressed.
When u choose another tool then this button stays pressed and the previous goes back to unpressed.
I dont know if BitmapButton is the right choice here.
You might want to use RadioButton widgets for this. The downside is that the RadioButton places its text next to the image, so if you need text on top of the button then you would have to add a Label in front of it manually on which you call isIgnoringMouseEvents() to make sure clicks on the label are passed to the button instead of absorbed by the label. I'm not sure if there are better options, but if you don't need any text then what you need definitely sounds like how radio buttons work.

Blaxxun

Yeah, iam still a noob.  :o  :-[

I know what scopes, references and pointers are but dont really fully understand them yet.
I use references in function headers to avoid copies, but in combination with a foreign library i fail.
I dont know what lamdas are, so i will learn about them now.

I'll try to move the variables back to the main function and try to change the content "in place" and avoiding copies.
I know its wrong what i did. (Although i dont know why. I just read that its wrong to have global variables)

Thanks for the tip with the radio buttons. I do not need any text on top of the buttons. Just an icon image and maybe a tooltip.

texus

QuoteI use references in function headers to avoid copies, but in combination with a foreign library i fail.
To keep it simple you can assume that Widget::Ptr is a Widget* (so if you copy it then you are just copying the pointer and not the actual widget), but to fully understand how it works you would need to understand std::shared_ptr because Widget::Ptr is just an alias for std::shared_ptr<Widget>.

QuoteI know its wrong what i did. (Although i dont know why. I just read that its wrong to have global variables)
There are 2 reasons why global variables are bad. The first one is mostly relevant to larger projects while the second one is typically only relevant for classes.

The first reason is simply because it can make the code harder to understand. Global variables can be changed from anywhere in your code, so it would be harder to follow where the value is changed throughout the code. This is why having "const" variables in global scope isn't a problem, it is only a problem with regular variables that can be changed.

The second reason is because there is no guaranteed order of construction and destruction of global variables across source files. If you have a global variables A and B then they are constructed in the order they appear in your cpp file, but if they appear in different cpp files then A could be constructed either before or after B. This is only a problem if you try to e.g. initialize A with the value of B.
SFML relies on some global variables to work, so if you create an sf::RenderWindow in global scope then it is possible that it crashes because the globals that sf::RenderWindow relies on aren't initialized yet when the window is created. It is also possible to get lucky and have the SFML globals initialized before your window in which case everything works fine, but that is not something you will want to rely on (although you will always get the exact same outcome as long as you don't change your linker settings).

So having a few integers in global scope isn't really a big issue when quickly writing a small piece of code.

Blaxxun

Okay, thank you for your explainations! Its much appreciated! :)

I watched this video about lambdas https://www.youtube.com/watch?v=mWgmBBz0y8c.
I also moved back my variables and edited the lambda captures [].
Now everything still works fine but without making my variables global.

I will look into shared pointers now.

Thank you texus for your help and also for this cool GUI ! :)