How to implement a tri state button using Qt - c++

I need to create a button which has three states:
unclicked
intermediate
clicked
The logic I want to implement with this button is that whenever this button is clicked, I want the system to go to the intermediate state and wait for an event.
In other way when the state transition is unclicked --> intermediate -- > clicked and then clicked --> intermediate -->unclicked.
Does Qt support implementing this kind of button? If so, how?

The nearest you have is QCheckBox. It already has a property for it: QCheckBox::setTristate:
auto yourCheckBoxButton = new QCheckBox("Tristate button");
yourCheckBoxButton->setTristate(true);
You can do it on the Designer too (it is at the end of the properties list).
If you don't want to use a QCheckBox, a stylesheet and a custom property that is modified each time the button is pressed can do it:
auto pushButton = new QPushButton("Tristate button");
pushButton->setProperty("state", 0);
pushButton->setProperty("state-step", 1); // change to next state, 1 or -1
pushButton->setStyleSheet("QPushButton[state=\"0\"] { background: red; }"
"QPushButton[state=\"1\"] { background: grey; }"
"QPushButton[state=\"2\"] { background: blue; }");
connect(pushButton, &QPushButton::clicked, [ = ](bool) {
const int state = pushButton->property("state").toInt();
const int step = state == 0 ? 1 :
state == 2 ? -1 : pushButton->property("state-step").toInt();
pushButton->setProperty("state", state + step);
pushButton->setProperty("state-step", step); // update in case it changed
// Changing the property is not enough to choose a new style from the stylesheet,
// it is necessary to force a re-evaluation
pushButton->style()->unpolish(pushButton);
pushButton->style()->polish(pushButton);
});
Other more elaborated options would be to use a QProxyStyle or to re-implement the QPushButton class itself.

Related

Closing Popup and setting button label

I'm writing a C++ wxWidgets calculator application. I want to compress trigonometric function buttons into a single one to save on space, using what's basically a split button. If you left click on it, the current option is used. If you right click, a popup menu is opened, which contains all the buttons; when you click on one of them, it is used and the big button changes.
I've been suggested to use wxComboBox and other stuff for this job, but I preferred using wxPopupTransientWindow because this way I can display the buttons in a grid, making everything - in my opinion - neater.
Problem is, when I choose an option from the menu, the main button's ID changes (because when I reopen the menu the previously clicked button is light up as its ID and the big button's ID match), but the label does not. Furthermore, the popup is supposed to close itself when you left click on one of the buttons, but it does not.
This is the code for the custom button in the popup which is supposed to do all that stuff:
void expandButton::mouseReleased(wxMouseEvent& evt)
{
if (pressed) {
pressed = false;
paintNow();
wxWindow* mBtn = this->GetParent()->GetParent();
mBtn->SetLabel(this->GetLabel());
mBtn->SetId(this->GetId());
this->GetParent()->Close(true);
}
}
This is the code for the custom button in the main frame which opens up the popup (temporary setup just to test if the whole thing is working):
void ikeButton::rightClick(wxMouseEvent& evt) // CREA PANNELLO ESTENSIONE
{
if (flags & EXPANDABLE)
{
std::vector<expandMenuInfo> buttons;
buttons.push_back(expandMenuInfo(L"sin", 3001));
buttons.push_back(expandMenuInfo(L"cos", 3002));
buttons.push_back(expandMenuInfo(L"tan", 3003));
buttons.push_back(expandMenuInfo(L"arcsin", 3004));
buttons.push_back(expandMenuInfo(L"arccos", 3005));
buttons.push_back(expandMenuInfo(L"arctan", 3006));
wxPoint p = this->GetScreenPosition();
size_t sz = this->GetSize().GetHeight() / 1.15;
expandMenu* menu = new expandMenu(this, buttons, sz, wxPoint(
p.x, p.y + this->GetSize().GetHeight() + 2));
menu->SetPosition(wxPoint(
menu->GetPosition().x - ((menu->GetSize().GetWidth() - this->GetSize().GetWidth()) / 2),
menu->GetPosition().y));
menu->Popup();
}
}
Let me know if I need to show more code.
This is probably a terrible way of doing this, but this is basically the first "serious" application I'm creating using the wxWidgets framework. Any help is appreciated.
when I choose an option from the menu, the main button's ID changes
(because when I reopen the menu the previously clicked button is light
up as its ID and the big button's ID match), but the label does not.
If you're creating the popup menu like in your previous post, you had a popup window with a panel as its child and the buttons were then children of the panel layed out with a sizer.
If that's still the case, this->GetParent() and should be the panel, this->GetParent()->GetParent() should be the popup. So this->GetParent()->GetParent()->GetParent() should be the trig function button (assuming you created the popup with the trig function button as the parent).
So I think the line wxWindow* mBtn = this->GetParent()->GetParent(); should be changed to wxWindow* mBtn = this->GetParent()->GetParent()->GetParent();.
Or slightly shorter wxWindow* mBtn = this->GetGrandParent()->GetParent();;
the popup is supposed to close itself when you left click on one of
the buttons, but it does not.
It looks like wxPopupTransientWindow has a special Dismiss method that is supposed to be used to close it.
So I think the line this->GetParent()->Close(true); should be changed to this->GetGrandParent()->Dismiss(); (Assuming as above that the buttons in the popup are children pf a panel).
Alternately, if you want a solution that will work regardless of the parentage of the controls in the popup window, you could have a utility function to find the popup ancestor which would look something like this:
wxPopupTransientWindow* FindPopup(wxWindow* w)
{
wxPopupTransientWindow* popup = NULL;
while ( w != NULL )
{
popup = wxDynamicCast(w, wxPopupTransientWindow);
if ( popup )
{
break;
}
w = w->GetParent();
}
return popup;
}
This uses the wxDynamicCast function which is slightly different from the c++ dynamic_cast expression. wxDynamicCast uses wxWidgets' RTTI system to check if the given pointer can be converted to the given type.
Then the mouseReleased method could use this utility function something like this:
void expandButton::mouseReleased(wxMouseEvent& evt)
{
if (pressed) {
pressed = false;
paintNow();
wxPopupTransientWindow* popup = FindPopup(this);
if (popup ) {
wxWindow* mBtn = popup->GetParent();
mBtn->SetLabel(this->GetLabel());
mBtn->SetId(this->GetId());
mBtn->Refresh();
popup->Dismiss();
}
}
}
I'm not sure why you're setting trig function button to have a new id, but I assume you have a reason.
To make the SetLabel method work in your custom button class, I think the easist thing is to call the SetLabel() method in the button's constructor. This will store the string passed to the constructor in the button's internal label member.
Based on other questions, I think the ikeButton constructor looks something like this:
ikeButton::ikeButton(wxFrame* parent, wxWindowID id, wxString text,...
{
...
this->text = text;
To store the label, you would need to change the line this->text = text; to
SetLabel(text);
And when you draw the button, I think the method you use looks like this:
void ikeButton::render(wxDC& dc)
{
...
dc.DrawText(text, ...);
}
You would need to change, the line dc.DrawText(text, ...); to
dc.DrawText(GetLabel(), ...);
Likewise, any other references to the button's text member should be changed to GetLabel() instead.
Finally, when you set the label in the expandButton::mouseReleased method, it might be a good idea to call button's Refresh() method to force the button to redraw itself. I added that my suggestion for the mouseReleased method above.

QTabWidget Hide and Show tabs

I have some problems with QTabWidget. In case of the missing Hide functionality I have to build my own. According to the documentation I use removeTab and insertTab, but with insert Tab I have a problem to show the Tab page that is removed.
I use to add
RibbonTabContent *ribbonTabContent = new RibbonTabContent;
QTabWidget::addTab(ribbonTabContent, tabIcon, tabName);
To remove is use:
void Ribbon::hideTab(const QString &tabName)
{
// Find ribbon tab
for (int i = 0; i < count(); i++)
{
if (tabText(i).toLower() == tabName.toLower())
{
QTabWidget::removeTab(i);
break;
}
}
}
Both functions are working, pWidget is always null. But now the insert function do not work well. I think there I have a problem, but do not understand my problem.
void Ribbon::showTab(const QString &tabName){
// Find ribbon tab
QWidget* pWidget= QTabWidget::findChild<RibbonTabContent *>(tabName);
if(pWidget){
QTabWidget::insertTab(2,pWidget, tabName);
}
}
Maybe someone can help me out?
If you call QTabWidget::removeTab you remove the tab at the specified index from the children tree of your QTabWidget, the tab instance is not actually deleted though, so when you search for that same tab with QTabWidget::findChild you can't find it because it's not a child of your QTabWidget anymore. From the code you show I think you probably would not find it anyway since findChild searches for a widget with the specified objectName but you never set it for your tab.
A solution would be to store the removed tabs and then restore them when you please.
Assuming m_hiddenTabs is a QHash<QString, QWidget*> or QMap<QString, QWidget*> you could try something like this.
void Ribbon::hideTab(const QString &tabName)
{
// Find ribbon tab
for (int i = 0; i < count(); i++)
{
if (tabText(i).toLower() == tabName.toLower())
{
m_hiddenTabs.insert(tabName.toLower(), QTabWidget::widget(i));
QTabWidget::removeTab(i);
break;
}
}
}
void Ribbon::showTab(const QString &tabName){
// Find ribbon tab
auto tab = m_hiddenTabs.take(tabName.toLower());
if(tab){
QTabWidget::insertTab(2, tab, tabName);
}
}
Since Qt 5.15 it is also possible to use setTabVisible:
void QTabWidget::setTabVisible(int index, bool visible)
If visible is true, the page at position index is visible; otherwise the page at position index is hidden. The page's tab is redrawn appropriately.If visible is true, the page at position index is visible; otherwise the page at position index is hidden. The page's tab is redrawn appropriately.
It is unfortunate that QTabBar is unable to 'hide' a tab.
Here is my very easy work-around: mark the tabs 'disabled' instead (e.g. ui->tabWidget->setTabEnabled(tabIndex, false);).
Then, use stylesheets to style the "disabled" tab as entirely invisible and taking up no space:
QTabBar::tab:disabled
{
min-width: 0px;
max-width: 0px;
color: rgba(0,0,0,0);
background-color: rgba(0,0,0,0);
}
This works near-perfectly for me, with the only downside being that you can't have both disabled and "hidden" tabs in the same tabbar. However, usually I want one or the other, not both in the same bar.

slow response on right click contex menu in graphic scene Qt

I have set a large menu in event filter on right click with 45-50 actions
inside and I find that when I right click the response to show the menu is slow
I did try the same code with 5 actions in the menu and the response was fine.
Is there something wrong with this way of coding on a contex menu ?
eventFilter
bool Editor::eventFilter(QObject *o, QEvent *e)
{
Q_UNUSED (o);
QGraphicsSceneMouseEvent *me = (QGraphicsSceneMouseEvent*) e;
switch ((int) e->type()){
case QEvent::GraphicsSceneMousePress:{
switch ((int) me->button()){
case Qt::RightButton:{
QGraphicsItem *item = itemAt(me->scenePos());
showContextMenu(item->scenePos().toPoint());
return true;
}
//more cases here//
}
break;
}
}
return QObject::eventFilter(o, e);
}
showContextMenu
void Editor::showContextMenu(const QPoint &pos)
{
QGraphicsItem *item =itemAt(pos);
// Create main effe menu
effeMenu= new QMenu("Menu");
QString menuStyle(
"QMenu {"
"border:10px };"
//more code here
);
effeMenu->setStyleSheet(menuStyle);
AmpMenu=effeMenu->addMenu(QIcon(":/effectImg/img/effePng/amp.png"),"Amp");
Amp1 =AmpMenu->addAction(QIcon(":/effectImg/img/effePng/amp.png"),"Amp 1");
Amp2 =AmpMenu->addAction(QIcon(":/effectImg/img/effePng/amp.png"),"Amp 2");
CabMenu=effeMenu->addMenu(QIcon(":/effectImg/img/effePng/cab.png"),"Cab");
Cab1 =CabMenu->addAction(QIcon(":/effectImg/img/effePng/cab.png"),"Cab 1");
Cab2 =CabMenu->addAction(QIcon(":/effectImg/img/effePng/cab.png"),"Cab 2");
.
.
.
.
//45 actions more
connect(effeMenu, &QMenu::triggered,this,[this,&item](QAction * k){
menuSelection(k,item);
});
Instead of creating a new QMenu each time you call showContextMenu you could make it a member of the class and build it once. On the other hand it is not necessary to use a signal, you could simply use the exec() method of QMenu:
*.h
class Editor: ...{
...
private:
QMenu effeMenu;
}
*.cpp
Editor::Editor(...){
effeMenu.setTitle("Menu");
QString menuStyle(
"QMenu {"
"border:10px };"
//more code here
);
effeMenu.setStyleSheet(menuStyle);
AmpMenu=effeMenu.addMenu(QIcon(":/effectImg/img/effePng/amp.png"),"Amp");
Amp1 =AmpMenu->addAction(QIcon(":/effectImg/img/effePng/amp.png"),"Amp 1");
Amp2 =AmpMenu->addAction(QIcon(":/effectImg/img/effePng/amp.png"),"Amp 2");
CabMenu=effeMenu.addMenu(QIcon(":/effectImg/img/effePng/cab.png"),"Cab");
Cab1 =CabMenu->addAction(QIcon(":/effectImg/img/effePng/cab.png"),"Cab 1");
Cab2 =CabMenu->addAction(QIcon(":/effectImg/img/effePng/cab.png"),"Cab 2");
...
}
void Editor::showContextMenu(const QPoint &pos){
QGraphicsItem *item =itemAt(pos);
QAction *action = menu.exec(pos);
menuSelection(action, item);
}
There are two things you can do to improve speed:
1 - itemAt(pos) is costly, and you are doing it twice, one in the event, and one in the showContextMenu. From what I could understand from your code you don't need the item in the event, just in the showMenu.
2 - The menu creation that you are doing is expensive: all the actions have pixmaps. this allocs memory for the QPixmap, loads, execute, dumps. Because you told us that you use around 40 actions (and really, that's too much for a menu), this can get costly.
My advice:
Create a class for your menu, create one instance of it, add a setter for the current QGraphicsObject that your menu will work on, and always use that one instance.

Qt table widget, button to delete row

I have a QTableWidget and for all rows I set a setCellWidget at one column to a button.
I would like to connect this button to a function that delets this row.
I tried this code, which does not work, because if I simply click my button I do not set the current row to the row of the button.
ui->tableWidget->insertRow(ui->tableWidget->rowCount());
QPushButton *b = new QPushButton("delete",this);
ui->tableWidget->setCellWidget(ui->tableWidget->rowCount()-1,0,b);
connect(d,SIGNAL(clicked(bool)),this,SLOT(deleteThisLine()));
...
void MainWindow::deleteThisLine()
{
int row = ui->tableWidget->currentRow();
ui->tableWidget->removeRow(row);
}
How can I connect my button to a function in a way that the function knows which button (at which row) was pressed?
To remove the row we must first get the row, if we are inserting widgets inside the cells the currentRow() method will not return the appropriate row, in many cases it will return the row of the last cell without widget that has been selected.
For that reason you must opt for another solution, for this case we will use the indexAt() method of QTableWidget, but for this we need to know the position in pixels of the cell. when one adds a widget to a cell, this cell will be the parent of the widget, so we can access from the button to the cell using the parent() method, and then get the position of the cell with respect to the QTableWidget and use it in indexAt(). To access the button we will use the sender().
When the current cell is removed the focus is lost, a possible solution is to place the focus again in another cell.
void MainWindow::deleteThisLine()
{
//sender(): QPushButton
QWidget *w = qobject_cast<QWidget *>(sender()->parent());
if(w){
int row = ui->tableWidget->indexAt(w->pos()).row();
ui->tableWidget->removeRow(row);
ui->tableWidget->setCurrentCell(0, 0);
}
}
Use this connection way to connect signal to a slot:
connect(ui->btnDelete, &QPushButton::clicked, this,&MainWindow::deleteRow);
And delete for example a row on call function:
void MainWindow::deleteRow()
{
int row = ui->tableWidget->currentRow();
ui->tableWidget->removeRow(row);
}
Create a custom class, where you pass the created push button object and the row index. From your custom push button class, handle the push button press event and emit a custom signal (it will carry the index number) handled from the object where your custom pushbutton is created. Some related code are below, to give you a hint:
.h
class mypushbutton {
explicit mypushbutton(QObject *parent = 0, QPushButton *pushbutton = 0, int index = 0);
signal:
void deleteRow(int index);
}
.cpp
mypushbutton() {
connect(pushbutton, SIGNAL(clicked(bool)), this, SLOT(actionButtonClick(bool)));
}
actionbuttonclicked() { emit deleteRow(index);}

Clear ExtraSelections Qt on slot disconnect

I am implementing a focus mode in QT QTextEdit in which I am highlighting single line where cursor is present. So far I can enable focus mode but when I disable focus mode, i want the state restored back to what it was.
The function that calls connect and disconnect is:
void MainWindow::onFocus_Mode_triggered()
{
QTextEdit *texed = qobject_cast<QTextEdit*>(ui->tabWidget->currentWidget());
if(ui->actionFocus_Mode->isChecked()){
connect(texed, SIGNAL(cursorPositionChanged()), this, SLOT(highlightCurrentLine()));
}
else {
disconnect(texed, SIGNAL(cursorPositionChanged()), this, SLOT(highlightCurrentLine())); //First disconnect and then call method to clear ExtraSelections
BacktoNormal(); //Help needed in implementing this
}
}
Now when the menu item actionFocus_Mode is checked, the line where the cursor is currently present is highlighted in yellow by the function given below.
void MainWindow::highlightCurrentLine() {
QTextEdit *texed = qobject_cast<QTextEdit*>(ui->tabWidget->currentWidget());
QList<QTextEdit::ExtraSelection> extraSelections;
QTextEdit::ExtraSelection selection;
QColor lineColor = QColor(Qt::yellow).lighter(160);
selection.format.setBackground(lineColor);
selection.format.setProperty(QTextFormat::FullWidthSelection, true);
selection.cursor = texed->textCursor();
selection.cursor.clearSelection();
extraSelections.append(selection);
texed->setExtraSelections(extraSelections);
}
So I am able to highlight it in yellow but if(!ui->actionFocus_Mode->isChecked()), i.e, if menu item (focus mode) is unchecked then I wish to revert back to normal mode. How would I implement BacktoNormal() function.
What I think right now is that I should set lineColor to transparent or something to get it back to normal (if it is possible at all).
I am unable to find anything related to this. Any help would be useful as I am completely stuck at this point.
In your BackNormal try to set just nothing as extra selections.
QTextEdit *texed = qobject_cast<QTextEdit*>(ui->textEdit);
QList<QTextEdit::ExtraSelection> extraSelections;
QTextEdit::ExtraSelection selection;
QColor lineColor = QColor(Qt::yellow).lighter(160);
selection.format.setBackground(lineColor);
selection.format.setProperty(QTextFormat::FullWidthSelection, true);
selection.cursor = texed->textCursor();
selection.cursor.clearSelection();
extraSelections.append(selection);
extraSelections.clear();//nothing
texed->setExtraSelections(extraSelections);
When did I try this on my computer(with another code), this selections was successfully removed.
Smaller version:
QTextEdit *texed = qobject_cast<QTextEdit*>(ui->textEdit);
QList<QTextEdit::ExtraSelection> extraSelections;//empty list
texed->setExtraSelections(extraSelections);