Office like QToolButton - qt

I currently try to make a QToolButton looks like equivalent in Office.
It should look like this:
I know I can access menu button with QToolButton::menu-button, but QToolButton::menu-button:hover is same as QToolButton:hover so I can't differ between both and I can't set a border just for QToolButton::menu-button. Any idea?

You can create something like split button.
class SplitButton : public QPushButton
{
Q_OBJECT
public:
explicit SplitButton(QWidget *parent = 0);
void setMenu(QMenu* menu);
protected:
void resizeEvent ( QResizeEvent * );
void mousePressEvent ( QMouseEvent * );
private:
QFrame* line;
};
Added opportunity to insert menu by setMenu() method. QFrame is needed for separating button and popup menu.
At constructor just initialize base class:
#include <QFrame>
#include <QMenu>
#include <QResizeEvent>
SplitButton::SplitButton(QWidget *parent):
QPushButton(parent)
{
}
Popup menu will be added like this:
void SplitButton::setMenu( QMenu* menu )
{
if (menu) {
line = new QFrame(this);
line->setFrameShape(QFrame::VLine);
line->setFrameShadow(QFrame::Sunken);
QPushButton::setMenu(menu);
connect(menu, &QMenu::triggered, [=](QAction *act) {
setText(act->text());
});
}
}
As you can see we used QPushButton::setMenu() method and connect menu triggered signal to slot, realized with labda, so you need include c++11 compatibility.
After we need to override resize, mousePress and keyPressed events. As minimal exmaple i done second ones.
void SplitButton::resizeEvent ( QResizeEvent * event )
{
if (menu()) {
int width = event->size().width();
int height = event->size().height();
line->setGeometry(QRect(width - 18, 4, 3, height - 8));
}
}
At resize event if menu is available we add frame by offset for pseudo-button.
void SplitButton::mousePressEvent( QMouseEvent * event )
{
if (menu()) {
if ( width()-event->x() <= 15 )
showMenu();
else
setDown(true);
} else {
QPushButton::mousePressEvent(event);
}
}
For mousePress event we just select what we want to do: show menu or push button.
Usage:
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent), m_file(QString())
{
setupUi(this);
SplitButton *split = new SplitButton;
split->setText("B");
QMenu *menu = new QMenu;
menu->addAction("Y");
menu->addAction("B");
menu->addAction("U");
split->setMenu(menu);
split->setCheckable(true);
Ui::MainWindow::mainToolBar->addWidget(split);
}
And you can set button chechable for Office-like effects.
It's just one of possible ways to realize this, I hope it will be helpful for you.
It look like on screen for me:
Of cource, it is not final result, you need repaint it like you want or use styles for flat-style.

Related

Identifying which label was clicked in qt

I have 10 Qlabels with an image on each. When i click on a label, its image should be cleared. I am able to identify which label was clicked theorotically, using the pixel clicked and size of each image. But how to use that information?
Eg. each label has dimension 100*100, the first label starting from 0,0. if pixel clicked is 250,50, i know that the third label was clicked, but how to use this to clear the label.
Im stuck.
There are a few ways how to implement it.
First. I would recommend to use a new class that inherits QLabel and overloads mouseReleaseEvent() handler where you just call clear() method. In this case the label will detect the mouse clicks itself and will clear its content internally.
class SelfClearingLabel : public QLabel
{
public:
using QLabel::QLabel;
protected:
void mouseReleaseEvent(QMouseEvent * event)
{
if (event->button()==Qt::LeftButton)
// process only clicks on the left button
{
clear();
}
QLabel::mouseReleaseEvent(event);
}
};
Second. You can catch mouseReleaseEvent() in your top widget and iterate over all of your child QLabel widgets and check which one is currently under mouse and clear the one. If you have other labels on this widget that shouldn't be cleared on mouse clicks then you can add some property to the QLabels that are under your interest.
void SomeTopFrame::createImageLabels(int count)
{
for (int i=0;i<count;i++)
{
QLabel* label=new QLabel(this);
label->setProperty("clear_on_click",true);
// assume that labels are added to layout *m_labelsLayout;
m_labelsLayout->addWidget(label);
}
}
void SomeTopFrame::mouseReleaseEvent(QMouseEvent * event)
{
if (event->button()==Qt::LeftButton)
// process only clicks on the left button
{
QList<QLabel*> labels=findChildren<QLabel*>();
foreach (QLabel* label, labels)
{
if (label->property("clear_on_click")&&label->underMouse())
{
label->clear();
break;
}
}
}
QFrame::mouseReleaseEvent(event);
}
It is a sample code just to show the principle. In production you can add a check that mouseReleaseEvent() is on the same widget as the mousePressEvent() to avoid triggering on drag and drop events.
Create the custom class that inherit QLabel :
ClickableLabel.h
class ClickableLabel : public QLabel
{
Q_OBJECT
public:
explicit ClickableLabel( const QString& text="", QWidget* parent=0 );
~ClickableLabel();
signals:
void clicked();
protected:
void mousePressEvent(QMouseEvent* event);
};
ClickableLabel.cpp
ClickableLabel::ClickableLabel(const QString& text, QWidget* parent)
: QLabel(parent)
{
setText(text);
}
ClickableLabel::~ClickableLabel()
{
}
void ClickableLabel::mousePressEvent(QMouseEvent* event)
{
emit clicked();
}
Just connect all labels clicked signal to following slot :
MyClass::labelClicked()
{
ClickableLabel *label = (ClickableLabel*)QObject::sender;
if(label)
label->clear();
}

QWidget can we get/modify existing context menu of a widget

I have third party widget which provides me some context menu. Lets say cut, copy paste, select all.
Now I just want to modify only the paste functionality of the existing context menu. I know that I can implement whole context menu from scratch in the contextMenuEvent. But I do not want to do that as I am satisfied with other context menu actions, and just want to modify only the paste functionality.
I am using QT 4.8 on Mac OSX.
If such a thing is not possible at the moment can someone give me link/reference for that ? So that I can satisfy my stakeholders.
Edit: To be more clearer on what I am trying to do is, disable the paste context menu for some reason, and want to enable it later on depending on the situation/events.
I'm not sure that it can be done in a common way.
Here is a tricky solution:
In contextMenuEvent create a queued call to some slot:
QMetaObject::invokeMethod(this, "patchMenu", Qt::QueuedConnection);
Get visible windows in the slot and find QMenu. Get actions out of it and enable/disable them:
Q_SLOT patchMenu()
{
QWidgetList widgets = QApplication::topLevelWidgets();
foreach (QWidget* widget, widgets)
{
if (QMenu* menu = qobject_cast<QMenu*>(widget))
{
QList<QAction*> actions = menu->actions();
// here you can either get an action by index actions[5]
// or search the action by text
actions;
}
}
}
EDIT:
Here is a working example which demonstrates this approach:
window.h
#pragma once
#include <QtGui>
class Window: public QMainWindow
{
Q_OBJECT
public:
Window(QWidget *parent = 0);
};
class A : public QWidget
{
public:
virtual void contextMenuEvent(QContextMenuEvent* e);
};
class B : public A
{
Q_OBJECT;
public:
virtual void contextMenuEvent(QContextMenuEvent*);
Q_SLOT void patchMenu();
};
window.cpp
#include "window.h"
Window::Window(QWidget *parent) : QMainWindow(parent)
{
setCentralWidget(new B());
}
void B::patchMenu()
{
QWidgetList widgets = QApplication::topLevelWidgets();
foreach (QWidget* widget, widgets)
{
if (QMenu* menu = qobject_cast<QMenu*>(widget))
{
QList<QAction*> actions = menu->actions();
// here you can either get an action by index actions[5]
// or search the action by text
actions;
}
}
}
void B::contextMenuEvent(QContextMenuEvent* e)
{
QMetaObject::invokeMethod(this, "patchMenu", Qt::QueuedConnection);
A::contextMenuEvent(e);
}
void A::contextMenuEvent(QContextMenuEvent* e)
{
QMenu menu;
QAction* action = new QAction(QIcon(), "text", &menu);
menu.addAction(action);
menu.exec(e->globalPos());
}

Extend mousePressEvent() for dragging but retain previous behaviour for clicking

I'm trying to add the Drag and Drop functionality to a QListWidget. The first milestone was reached with the following code.
//draglist.h
#include <QListWidget>
class SourceList : public QListWidget
{
public:
SourceList(QWidget * parent = 0);
protected:
void mousePressEvent(QMouseEvent *event);
};
//draglist.cpp
#include "draglists.h"
#include <QMouseEvent>
#include <QDebug>
#include <QMimeData>
#include <QDrag>
SourceList::SourceList(QWidget * parent)
:QListWidget(parent)
{
}
void SourceList::mousePressEvent(QMouseEvent *event)
{
//Data from the selected QListWidgetItem
QString itemData = currentItem()->data(Qt::DisplayRole).toString();
QMimeData *mimeData = new QMimeData;
mimeData->setText(itemData);
QDrag *drag = new QDrag(this);
drag->setMimeData(mimeData);
drag->exec();
}
It enabled the drag functionality for the list items, but they can't be clicked any more.
How to retain the functionality in which one left click selects the item in the list (this is the behaviour if mousePressEvent() isn't overridden) ?
Possible solution
Check the source code for the original QAbstractItemView::mousePressEvent(QMouseEvent *event) and recopy the needed code int the SourceList::mousePressEvent(QMouseEvent *event) implementation. I'm looking for an alternative to that, if existent.
To preserve the default functionality you can propagate the event up the the parent class:
void SourceList::mousePressEvent(QMouseEvent *event)
{
// Your handling of mouse event.
[..]
QListWidget::mousePressEvent(event);
}
If you need to handle the left mouse button press, you can use QMouseEvent::button() function and compare returning value with one or more Qt::MouseButton values. For example:
if (event->button() & Qt::LeftButton) {
// Handle the left mouse button click.
}

How to change a parent widget's background when a child widget has focus?

I would like to highlight a QFrame, if one of it's child widgets has focus (so the users know where to look for the cursor ;-)
using something along
ui->frame->setFocusPolicy(Qt::StrongFocus);
ui->frame->setStyleSheet("QFrame:focus {background-color: #FFFFCC;}");
highlights the QFrame when I click on it, but it loses its focus once one of its child widgets is selected.
Possible approaches:
I could connect() QApplication::focusChanged(old,now) and check each new object if it is a child of my QFrame, but this gets messy.
I could also subclass each child widget and reimplement focusInEvent()/focusOutEvent() and react on that, but with a lot of different widgets, this is also a lot of work.
Is there a more elegant solution?
Well, you can extend QFrame to make it listen on focus change of its children widgets.
Or you can also install an event filter on children widgets to catch QFocusEvent.
Here is an example:
MyFrame.h
#ifndef MYFRAME_H
#define MYFRAME_H
#include <QFrame>
class MyFrame : public QFrame
{
Q_OBJECT
public:
explicit MyFrame(QWidget* parent = 0, Qt::WindowFlags f = 0);
void hookChildrenWidgetsFocus();
protected:
bool eventFilter(QObject *object, QEvent *event);
private:
QString m_originalStyleSheet;
};
#endif // MYFRAME_H
MyFrame.cpp
#include <QEvent>
#include "MyFrame.h"
MyFrame::MyFrame(QWidget *parent, Qt::WindowFlags f)
: QFrame(parent, f)
{
m_originalStyleSheet = styleSheet();
}
void MyFrame::hookChildrenWidgetsFocus()
{
foreach (QObject *child, children()) {
if (child->isWidgetType()) {
child->installEventFilter(this);
}
}
}
bool MyFrame::eventFilter(QObject *object, QEvent *event)
{
if (event->type() == QEvent::FocusIn) {
setStyleSheet("background-color: #FFFFCC;");
} else if (event->type() == QEvent::FocusOut) {
setStyleSheet(m_originalStyleSheet);
}
return QObject::eventFilter(object, event);
}
MainWindow.cpp
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QLineEdit>
#include "MyFrame.h"
#include "mainwindow.h"
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent)
{
setWindowTitle(tr("Test"));
MyFrame *frame1 = new MyFrame(this);
frame1->setLayout(new QVBoxLayout());
frame1->layout()->addWidget(new QLineEdit());
frame1->layout()->addWidget(new QLineEdit());
frame1->layout()->addWidget(new QLineEdit());
frame1->hookChildrenWidgetsFocus();
MyFrame *frame2 = new MyFrame(this);
frame2->setLayout(new QVBoxLayout());
frame2->layout()->addWidget(new QLineEdit());
frame2->layout()->addWidget(new QLineEdit());
frame2->layout()->addWidget(new QLineEdit());
frame2->hookChildrenWidgetsFocus();
QHBoxLayout *centralLayout = new QHBoxLayout();
centralLayout->addWidget(frame1);
centralLayout->addWidget(frame2);
QWidget *centralWidget = new QWidget();
centralWidget->setLayout(centralLayout);
setCentralWidget(centralWidget);
}
I believe the both answers you were given are wrong. They work for simple cases but are extremely fragile and clumsy. I believe that the best solution is what you actually suggested in your question. I would go for connecting to QApplication::focusChanged(from, to). You simply connect your main frame object to this signal and in the slot you check if the to object (the one which received focus) is a child of your frame object.
Frame::Frame(...)
{
// ...
connect(qApp, &QApplication::focusChanged, this, &Frame::onFocusChanged);
// ...
}
// a private method of your Frame object
void Frame::onFocusChanged(QWidget *from, QWidget *to)
{
auto w = to;
while (w != nullptr && w != this)
w = w->parentWidget();
if (w == this) // a child (or self) is focused
setStylesheet(highlightedStylesheet);
else // something else is focused
setStylesheet(normalStylesheet);
}
The advantage is obvious. This code is short and clean. You connect only one signal-slot, you do not need to catch and handle events. It responds well to any layout changes done after the object is created. And if you want to optimize away unnecessary redrawing, you should cache the information whether any child is focused and change the stylesheet only and only if this cached value gets changed. Then the solution would be prefect.
First, create a simple subclass of QFrame which reimplements the eventFilter(QObject*, QEvent*) virtual function:
class MyFrame : public QFrame {
Q_OBJECT
public:
MyFrame(QWidget *parent = 0, Qt::WindowFlags f = 0);
~MyFrame();
virtual bool eventFilter(QObject *watched, QEvent *event);
};
Use MyFrame instead of QFrame to contain your widgets. Then, somewhere in your code where you create the widgets contained in MyFrame, install an event filter on those widgets:
// ...
m_myFrame = new MyFrame(parentWidget);
QVBoxLayout *layout = new QVBoxLayout(myFrame);
m_button = new QPushButton("Widget 1", myFrame);
layout->addWidget(m_button);
m_button->installEventFilter(myFrame);
//...
At that point, MyFrame::eventFilter() will be called before any event is delivered to the widget, letting you act on it before the widget is aware of it. Within MyFrame::eventFilter(), return true if you want to filter the event out (i.e. you don't want the widget to process the event), or return false otherwise.
bool MyFrame::eventFilter(QObject *watched, QEvent *event)
{
if (watched == m_button) { // An event occured on m_button
switch (event -> type()) {
case QEvent::FocusIn:
// Change the stylesheet of the frame
break;
case QEvent::FocusOut:
// Change the stylesheet back
break;
default:
break;
}
}
return false; // We always want the event to propagate, so always return false
}

multiple signals for one slot

For my GUI i would like to have two pairs of buttons that scroll up and down a scrollarea. The first set of buttons should work on say scrollarea1 and the second set of buttons should work on a scrollarea2. The widgets that I put in the scrollarea are called viewport1 and viewport2.
Since both both set of buttons should do the same (scrolling up and down) I thought I would make two slots called scrollUp and scrollDown that would handle the scrolling for both sets of buttons. Unfortunately I cannot make this work and need some help. I have tried the following:
QPushButton up;
QPushButton down;
QPushButton up2;
QPushButton down2;
connect(&up,SIGNAL(clicked()),&up,SLOT(scrollUp()));
connect(&up2,SIGNAL(clicked()),&up,SLOT(scrollUp()));
connect(&down,SIGNAL(clicked()),&down,SLOT(scrollDown()));
connect(&down2,SIGNAL(clicked()),&down,SLOT(scrollDown()));
void MainWindow::scrollDown()
{
QScrollArea area;
QWidget view;
if((QPushButton) &sender = down)
{
area=scrollArea;
view=viewport;
}
if((QPushButton) &sender = down2)
{
area=scrollArea;
view=viewport;
}
int curpos = area.verticalScrollBar()->value();
area.verticalScrollBar()->setValue(curpos+15);
int newpos = area.verticalScrollBar()->value();
QPoint topLeft = area.viewport()->rect().topLeft();
view.move(topLeft.x(),topLeft.y()-(newpos));
}
void MainWindow::scrollUp()
{
QScrollArea area;
QWidget view;
if((QPushButton) &sender = up)
{
area=scrollArea;
view=viewport;
}
if((QPushButton) &sender = up2)
{
area=scrollArea2;
view=viewport2;
}
int curpos = area.verticalScrollBar()->value();
area.verticalScrollBar()->setValue(curpos-15);
int newpos = area.verticalScrollBar()->value();
QPoint topLeft = area.viewport()->rect().topLeft();
view.move(topLeft.x(),topLeft.y()-(newpos));
}
But this doesn´t work for several reasons. I also tried giving the slot some arguments, something like:
connect(&up,SIGNAL(clicked()),&up,SLOT(scrollUp(scrollarea1,viewport1)));
connect(&up2,SIGNAL(clicked()),&up,SLOT(scrollUp(scrollarea2,viewport2)));
But again, no succes. Can anybody help me?
First of all, "It doesn't work" does not mean anything, and it is hard to help you if you do not say what errors you get. Then, there are few problems.
All QObject's derived classes are not copiable, it means you can not do
QWidget a;
QWidget b;
b = a; // Wrong
You should use pointers (or perhaps references).
QWidget a;
QWidget * b = new QWidget(...);
QWidget * c;
c = & a; // Ok
c = b; // Ok
Then your connect calls are wrong:
connect(&up, SIGNAL(clicked()), &up, SLOT(scrollUp()));
The third argument is the object who has the slot. up is a QPushButton, it does not have a scrollUp() slot, it is your MainWindow who does:
connect(&up, SIGNAL(clicked()), this, SLOT(scrollUp()));
(since connect is called in MainWindow's constructor this points to the current MainWindow object).
Also in C++ the single = sign means assignment, for equality comparison use =='. Andsender` is a function.
Your approach should work if implemented in the right way:
class MainWindow: public QWidget
{
QScrollArea * scroll1;
QScrollArea * scroll2;
QWidget * view1;
QWidget * view2;
QPushButton * up1;
QPushButton * up2;
QPushButton * down1;
QPushButton * down2;
public:
MainWindow()
{
// Here initialize member variables.
...
connect(up1, SIGNAL(clicked()), this, SLOT(scrollUp()));
connect(up2, SIGNAL(clicked()), this, SLOT(scrollUp()));
connect(down1, SIGNAL(clicked()), this, SLOT(scrollDown()));
connect(down2, SIGNAL(clicked()), this, SLOT(scrollDown()));
}
public slots:
void scrollDown()
{
QScrollArea * area;
QWidget * view;
if(qobject_cast<QPushButton>(sender()) == down1) {
area = & scroll1;
view = & view1;
} else if(qobject_cast<QPushButton>(sender()) == down2) {
area = & scroll2;
view = & view2;
} else {
// Error.
}
// Now `area` and `view` point to the right widgets.
...
}
void scrollUp()
{
// The same as before.
}
};
Another approach would be to extract the actual scrolling instructions to a separate function:
class MainWindow: public QWidget
{
// Same variables as before
...
public:
MainWindow()
{
// Here initialize member variables.
...
connect(up1, SIGNAL(clicked()), this, SLOT(scrollUp1()));
connect(up2, SIGNAL(clicked()), this, SLOT(scrollUp2()));
connect(down1, SIGNAL(clicked()), this, SLOT(scrollDown1()));
connect(down2, SIGNAL(clicked()), this, SLOT(scrollDown2()));
}
public slots:
void scrollDown(QScrollArea * area, QWidget * view)
{
// Here you scroll over `area` and `view`.
}
void scrollDown1()
{
scrollDown(scroll1, area1);
}
void scrollDown2()
{
scrollDown(scroll2, area2);
}
// Again, the same for `scrollUp`.
};
There are several mistakes in your code :
About the sender of the signal : There is not a QObject called "sender" but a method QObject * QObject::sender() const; which returns a pointer on the sender of the signal.
In the if conditions : you are casting a QPushButton** into a QPushButton ((QPushButton) &sender) and you dont compare that thing with your buttons up(2) and down(2).
In your connections between slots and signals : the scrollUp and scrollDown slots do not belong to the QPushButton class but to your MainWindow class.
Finally, you should write something like this :
connect(&up, SIGNAL(clicked()), this, SLOT(scrollUp()));
connect(&up2, SIGNAL(clicked()), this, SLOT(scrollUp()));
connect(&down, SIGNAL(clicked()), this, SLOT(scrollDown()));
connect(&down2, SIGNAL(clicked()), this, SLOT(scrollDown()));
void MainVindow::scrollDown() {
// [...]
QPushButton * senderButton = qobject_cast<QPushButton *>(this->sender());
// QPushButton * senderButton = (QPushButton *) this->sender(); works too
if (senderButton == &down) {
// [...]
}
if (senderButton == &down2) {
// [...]
}
// [...]
}
void MainVindow::scrollUp() {
// [...]
QPushButton * senderButton = qobject_cast<QPushButton *>(this->sender());
// QPushButton * senderButton = (QPushButton *) this->sender(); works too
if (senderButton == &up) {
// [...]
}
if (senderButton == &up2) {
// [...]
}
// [...]
}
First of all the slot can have no other arguments than the signal hands to it. Clicked has no arguments and there fore the slot can have no arguments.
I would think that the easiest way to check whether scrollArea 1 or 2 has focus and decide from that which one should move.
I also think that there is an error in your code. Shouldn't this:
if((QPushButton) &sender = down2)
{
area=scrollArea;
view=viewport;
}
Be this:
if((QPushButton) &sender = down2)
{
area=scrollArea2;
view=viewport2;
}
First of all, this is pseudo code. It won't compile, but it should contain the necessary information.
I believe this problem can be most elegantly solved using the QSignalMapper class. It allows parameterless signals from multiple senders to connect to one slot.
In the header, write something like this:
class QSignalMapper;
class MainWindow : public QMainWindow
{
public:
void init();
public slots:
void handleScrollButtons(int id);
private:
enum { ScrollUp1, ScrollDown1, ScrollUp2, ScrollDown2 } // just makes it more convenient to use
QSignalMapper *m_scrollbuttonhandler;
}
In the source file, write something like this
#include <QSignalMapper>
void MainWindow::init()
{
m_scrollbuttonhandler = new QSignalMapper(this);
m_scrollbuttonhandler->setMapping(scrollup1button, ScrollUp1);
m_scrollbuttonhandler->setMapping(scrolldown1button, ScrollDown1);
m_scrollbuttonhandler->setMapping(scrollup2button, ScrollUp2);
m_scrollbuttonhandler->setMapping(scrolldown2button, ScrollDown2);
connect(scrollup1button, SIGNAL(clicked(bool)), m_scrollbuttonhandler, SLOT(map()));
connect(scrolldown1button, SIGNAL(clicked(bool)), m_scrollbuttonhandler, SLOT(map()));
connect(scrollup2button, SIGNAL(clicked(bool)), m_scrollbuttonhandler, SLOT(map()));
connect(scrolldown2button, SIGNAL(clicked(bool)), m_scrollbuttonhandler, SLOT(map()));
connect(m_scrollbuttonhandler, SIGNAL(mapped(int)), this, SLOT(handleScrollButtons(int)));
}
void MainWindow::handleScrollButtons(int id)
{
switch (id)
{
case ScrollUp1:
// stuff to do for scrollup1button
case ScrollDown1:
// stuff to do for scrolldown1button
case ScrollUp2:
// stuff to do for scrollup2button
case ScrollDown2:
// stuff to do for scrolldown2button
}
}

Resources