I am developing a Qt application using QTreeView and QAbstractItemModel.
The model contains somewhat heterogenous data, which requires different controls for editing. I am implementing it by having a custom delegate, that queries model.data(Qt::EditRole) and the model would return the QWidget to be used as an editor.
I like how it works for QLineEdit - when I hit enter, delegate::setModelData is called automatically, so I do not have to connect QLineEdit::editingFinished to QAbstractItemDelegate::setModelData(), which is very convenient since the QLineEdit is returned from the model as QWidget to the delegate and it doesn't have to care what kind of widget it is.
With QComboBox it's a bit tricky - I want the comboBox to commit once the selection is made, so far I can only do it by connect(myComboBox, SIGNAL(activated(QString)), myDelegate, commitData()));
However, my delegate doesn't know the type of the editor widget and I do not want to edit the delegate's code for each new editor the model ends up passing it. I would really like to force combobox to do the same thing as QLineEdit does when the enter is clicked in the slot connected to its activated() signal.
So, what does the QLineEdit do to call delegate's setModelData?
Is there a generic way in Qt for the editor whatever it is to say "I'm done editing, take the data and pass it to the model"?
The QStyledItemDelegate (and QItemDelegate) defines an event filter that calls commitData if you press tab or return.
See the following excerpt from the Qt 4.8.6 source:
bool QStyledItemDelegate::eventFilter(QObject *object, QEvent *event)
{
QWidget *editor = qobject_cast<QWidget*>(object);
if (!editor)
return false;
if (event->type() == QEvent::KeyPress) {
switch (static_cast<QKeyEvent *>(event)->key()) {
case Qt::Key_Tab:
emit commitData(editor);
emit closeEditor(editor, QAbstractItemDelegate::EditNextItem);
return true;
case Qt::Key_Backtab:
emit commitData(editor);
emit closeEditor(editor, QAbstractItemDelegate::EditPreviousItem);
return true;
case Qt::Key_Enter:
case Qt::Key_Return:
#ifndef QT_NO_TEXTEDIT
if (qobject_cast<QTextEdit *>(editor) || qobject_cast<QPlainTextEdit *>(editor))
return false; // don't filter enter key events for QTextEdit
// We want the editor to be able to process the key press
// before committing the data (e.g. so it can do
// validation/fixup of the input).
#endif // QT_NO_TEXTEDIT
#ifndef QT_NO_LINEEDIT
if (QLineEdit *e = qobject_cast<QLineEdit*>(editor))
if (!e->hasAcceptableInput())
return false;
#endif // QT_NO_LINEEDIT
QMetaObject::invokeMethod(this, "_q_commitDataAndCloseEditor",
Qt::QueuedConnection, Q_ARG(QWidget*, editor));
return false;
case Qt::Key_Escape:
// don't commit data
emit closeEditor(editor, QAbstractItemDelegate::RevertModelCache);
break;
default:
return false;
}
if (editor->parentWidget())
editor->parentWidget()->setFocus();
return true;
} else if (event->type() == QEvent::FocusOut || (event->type() == QEvent::Hide && editor->isWindow())) {
//the Hide event will take care of he editors that are in fact complete dialogs
if (!editor->isActiveWindow() || (QApplication::focusWidget() != editor)) {
QWidget *w = QApplication::focusWidget();
while (w) { // don't worry about focus changes internally in the editor
if (w == editor)
return false;
w = w->parentWidget();
}
#ifndef QT_NO_DRAGANDDROP
// The window may lose focus during an drag operation.
// i.e when dragging involves the taskbar on Windows.
if (QDragManager::self() && QDragManager::self()->object != 0)
return false;
#endif
emit commitData(editor);
emit closeEditor(editor, NoHint);
}
} else if (event->type() == QEvent::ShortcutOverride) {
if (static_cast<QKeyEvent*>(event)->key() == Qt::Key_Escape) {
event->accept();
return true;
}
}
return false;
}
Related
I'm trying to design an image viewer application where I use a QGraphicsView to display my images. I want to enable the user to use the arrow keys to open the next/previous image, but my QGraphicsView is always consuming my keyPressEvent. I would like these events to be handled by my QMainWindow class instead. I realize this is a common issue, and apparently I can solve it by installing an event filter and/or ensuring that my QMainWindow can have focus. I have done both, but so far the only thing I have done is to not let the QGraphicsView get the event, but it still does not propagate to the QMainWindow. So far I've implemented the eventFilter method in my QMainWindow class and installed it on my QGraphicsView object.
QMainWindow class
IVMainWindow::IVMainWindow(QWidget *parent)
: QMainWindow(parent)
{
setWindowTitle("ImageViewer++");
setFocusPolicy(Qt::StrongFocus); // Enabled the QMainWindow to get focus
m_image_widget = new IVImageWidget();
m_image_widget->installEventFilter(this); // Install event filter on QGraphicsView
setCentralWidget(m_image_widget);
resize(QGuiApplication::primaryScreen()->availableSize() * 3 / 5);
// For DEBUG purpose
loadImage("res/image.png");
createMenuBar();
createToolBar();
}
/**
* Filters out arrow key presses.
*/
bool IVMainWindow::eventFilter(QObject *obj, QEvent *event) {
if (event->type() == QEvent::KeyPress) {
auto *keyEvent = static_cast<QKeyEvent *>(event);
int key = keyEvent->key();
// Return true to reject the key-presses
return (key == Qt::Key_Left || key == Qt::Key_Right || key == Qt::Key_Up || key == Qt::Key_Down);
} else {
// standard event processing
return QMainWindow::eventFilter(obj, event);
}
}
//Never gets to this point, unless I explicitly give it focus by clicking on some other widget than the QGraphicsView...
void IVMainWindow::keyPressEvent(QKeyEvent *event) {
if (event->key() == Qt::RightArrow) {
m_logger.Debug("Right arrow pressed.");
} else if (event->key() == Qt::LeftArrow) {
m_logger.Debug("Left arrow pressed.");
}
}
You need to process the event in the event filter, it is ok to call QMainWindow::eventFilter for sanity but it do not
process the event as an incoming event, so the event handlers will not be called.
bool IVMainWindow::eventFilter(QObject *obj, QEvent *event) {
if (event->type() == QEvent::KeyPress) {
auto *keyEvent = static_cast<QKeyEvent *>(event);
int key = keyEvent->key();
// Return true to reject the key-presses
if (key == Qt::Key_Left || key == Qt::Key_Right || key == Qt::Key_Up || key == Qt::Key_Down)
{
//process event here somehow, or instruct your class to do it later
return true; //filter the event
}
} else {
// standard event processing
return QMainWindow::eventFilter(obj, event);
}
}
//This will only be called for the 'real' events, the ones that eventually are received by the main window
void IVMainWindow::keyPressEvent(QKeyEvent *event) {
if (event->key() == Qt::RightArrow) {
m_logger.Debug("Right arrow pressed.");
} else if (event->key() == Qt::LeftArrow) {
m_logger.Debug("Left arrow pressed.");
}
}
I have a Window subclass in my project, and at runtime the instance is created and shown entirely on the QML side. I know that I can prevent the window from being minimized by not including the WindowMinimizeButtonHint in the flags:, but I actually need to have the minimize button present and enabled but be able to intercept the minimize button click, cancel the actual minimizing, and do something else (FYI my client is requiring this non-standard windowing behavior, not me).
So far, the only thing I've been able to achieve is to handle the onWindowStateChanged: event, check if windowState === Qt.WindowStateMinimized and call show() from a timer (calling it inside the event handler directly does nothing). This results in the window moving down to the system tray and then suddenly coming back up to normal.
Is there any way to do this, something like an OnMinimized event that can be cancelled?
Edit: based on Benjamin T's answer, I'm at least part way to a solution for OSX:
#import <AppKit/AppKit.h>
bool NativeFilter::nativeEventFilter(const QByteArray &eventType,
void *message, long *result)
{
if (eventType == "mac_generic_NSEvent") {
NSEvent *event = static_cast<NSEvent *>(message);
if ([event type] == NSKeyDown) {
return true;
}
}
return false;
}
In this example I'm able to intercept and cancel all NSKeyDown events (while leaving other events like mouse clicks etc. still working). The remaining problem is that I still don't know to intercept a minimize event - NSEvent.h doesn't seem to have anything that covers that. Perhaps I need to cast to a different type of event?
Edit 2 - working solution:
I was not able to find any way to intercept the minimize event proper and cancel it, so my workaround is to instead intercept the click on the window, determine if the click is over the minimize button (or the close or zoom buttons) and cancel the event if so (and send a notification to my qml window that the click occurred). I also handle the case of double-clicking the titlebar to zoom the window, and using the Command-M keys to minimize the window.
First step is to implement a QAbstractNativeEventFilter. In your header:
#include <QAbstractNativeEventFilter>
class NativeFilter : public QAbstractNativeEventFilter {
public:
bool nativeEventFilter(const QByteArray &eventType, void *message,
long *result);
};
The implementation:
#import <AppKit/AppKit.h>
#import <AppKit/NSWindow.h>
#import <AppKit/NSButton.h>
bool NativeFilter::nativeEventFilter(const QByteArray &eventType, void
*message, long *result)
{
if (eventType == "mac_generic_NSEvent") {
NSEvent *event = static_cast<NSEvent *>(message);
NSWindow *win = [event window];
// TODO: determine whether or not this is a window whose
// events you want to intercept. I did this by checking
// [win title] but you may want to find and use the
// window's id instead.
// Detect a double-click on the titlebar. If the zoom button
// is enabled, send the full-screen message to the window
if ([event type] == NSLeftMouseUp) {
if ([event clickCount] > 1) {
NSPoint pt = [event locationInWindow];
CGRect rect = [win frame];
// event coordinates have y going in the opposite direction from frame coordinates, very annoying
CGFloat yInverted = rect.size.height - pt.y;
if (yInverted <= 20) {
// TODO: need the proper metrics for the height of the title bar
NSButton *btn = [win standardWindowButton:NSWindowZoomButton];
if (btn.enabled) {
// notify qml of zoom button click
}
return true;
}
}
}
if ([event type] == NSKeyDown) {
// detect command-M (for minimize app)
if ([event modifierFlags] & NSCommandKeyMask) {
// M key
if ([event keyCode] == 46) {
// notify qml of miniaturize button click
return true;
}
}
// TODO: we may be requested to handle keyboard actions for close and zoom buttons. e.g. ctrl-cmd-F is zoom, I think,
// and Command-H is hide.
}
if ([event type] == NSLeftMouseDown) {
NSPoint pt = [event locationInWindow];
CGRect rect = [win frame];
// event coordinates have y going in the opposite direction from frame coordinates, very annoying
CGFloat yInverted = rect.size.height - pt.y;
NSButton *btn = [win standardWindowButton:NSWindowMiniaturizeButton];
CGRect rectButton = [btn frame];
if ((yInverted >= rectButton.origin.y) && (yInverted <= (rectButton.origin.y + rectButton.size.height))) {
if ((pt.x >= rectButton.origin.x) && (pt.x <= (rectButton.origin.x + rectButton.size.width))) {
// notify .qml of miniaturize button click
return true;
}
}
btn = [win standardWindowButton:NSWindowZoomButton];
rectButton = [btn frame];
if (btn.enabled) {
if ((yInverted >= rectButton.origin.y) && (yInverted <= (rectButton.origin.y + rectButton.size.height))) {
if ((pt.x >= rectButton.origin.x) && (pt.x <= (rectButton.origin.x + rectButton.size.width))) {
// notify qml of zoom button click
return true;
}
}
}
btn = [win standardWindowButton:NSWindowCloseButton];
rectButton = [btn frame];
if ((yInverted >= rectButton.origin.y) && (yInverted <= (rectButton.origin.y + rectButton.size.height))) {
if ((pt.x >= rectButton.origin.x) && (pt.x <= (rectButton.origin.x + rectButton.size.width))) {
// notify qml of close button click
return true;
}
}
}
return false;
}
return false;
}
Then in main.cpp:
Application app(argc, argv);
app.installNativeEventFilter(new NativeFilter());
Generally speaking, you should use the event system ans not signal/slots to intercept events and changes.
The easiest way to do so is either to subclass the object you use and reimplement the appropriate event handler, or to use an event filter.
Since you are using QML, subclassing might be difficult as you don't have access to all Qt internal classes.
Here is what the code would look like when using event filtering.
int main(int argc, char *argv[])
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
if (engine.rootObjects().isEmpty())
return -1;
auto root = engine.rootObjects().first();
root->installEventFilter(new EventFilter());
return app.exec();
}
class EventFilter : public QObject
{
Q_OBJECT
public:
explicit EventFilter(QObject *parent = nullptr);
bool eventFilter(QObject *watched, QEvent *event) override;
};
bool EventFilter::eventFilter(QObject *watched, QEvent *event)
{
if (event->type() == QEvent::WindowStateChange) {
auto e = static_cast<QWindowStateChangeEvent *>(event);
auto window = static_cast<QWindow *>(watched);
if (window->windowStates().testFlag(Qt::WindowMinimized)
&& ! e->oldState().testFlag(Qt::WindowMinimized))
{
// Restore old state
window->setWindowStates(e->oldState());
return true;
}
}
// Do not filter event
return false;
}
However, you will quickly run into the same issue that when using the signal/slot mechanism: Qt only notify you when the window has already been minimized. Meaning that restoring the window at this point will make a hide/show effect.
So you need to go deeper and you a native event filter.
The following code works on Windows, you should adapt it for macOS:
class NativeFilter : public QAbstractNativeEventFilter {
public:
bool nativeEventFilter(const QByteArray &eventType, void *message, long *result);
};
bool NativeFilter::nativeEventFilter(const QByteArray &eventType, void *message, long *result)
{
/* On Windows we interceot the click in the title bar. */
/* If we wait for the minimize event, it is already too late. */
#ifdef Q_OS_WIN
auto msg = static_cast<MSG *>(message);
// Filter out the event when the minimize button is pressed.
if (msg->message == WM_NCLBUTTONDOWN && msg->wParam == HTREDUCE)
return true;
#endif
/* Example macOS code from Qt doc, adapt to your need */
#ifdef Q_OS_MACOS
if (eventType == "mac_generic_NSEvent") {
NSEvent *event = static_cast<NSEvent *>(message);
if ([event type] == NSKeyDown) {
// Handle key event
qDebug() << QString::fromNSString([event characters]);
}
}
#endif
return false;
}
In your main():
QGuiApplication app(argc, argv);
app.installNativeEventFilter(new NativeFilter());
For more info, you can read the Qt documentation about QAbstractNativeEventFilter.
You may need to use QWindow::winId() to check to which window the native events are targeted.
As I am not a macOS developer, I do not know what you can do with NSEvent.
Also it seems the NSWindowDelegate class could be useful to you: https://developer.apple.com/documentation/appkit/nswindowdelegate
If you can retrieve a NSWindow from QWindow::winId(), you should be able to use it.
I have a QLineedit with mask and qvalidator (subclassed)
How can i prevent to move away the focus if the input isn't match the mask or validator ?
Because neither mask nor qvalidator don't prevent to move away focus from QLineEdit.
And editingfinished isn't work because :
void QLineEdit::editingFinished()
This signal is emitted when the Return or Enter key is pressed or the line edit loses focus. Note that if there is a validator() or inputMask() set on the line edit and enter/return is pressed, the editingFinished() signal will only be emitted if the input follows the inputMask() and the validator() returns QValidator::Acceptable."
void MainWindow:n_lineEdit_editingFinished()
{
if (ui->lineEdit->text() != "1111") ui->lineEdit->setFocus();
}
So mask (an validator) doesn't work together with editingFinsihed signal.
plus i have tried this
bool MainWindow::eventFilter(QObject *filterObj, QEvent *event)
{
if (filterObj == ui->lineEdit ) {
if(event->type() == QEvent::FocusOut) {
if (ui->lineEdit->text() != "1111") { ui->lineEdit-`>setFocus();};
return true;
};
};
return false;
}
thank you Attila
From the Qt's doc:
Note that if there is a validator set on the line edit, the
returnPressed()/editingFinished() signals will only be emitted if the
validator returns QValidator::Acceptable.
But you can set a focus on every event, not only for FocusOut:
bool MainWindow::eventFilter(QObject *filterObj, QEvent *event)
{
if (filterObj == ui->lineEdit )
ui->lineEdit->setFocus();
if(event->type() == QEvent::KeyRelease)
{
QKeyEvent* e = (QKeyEvent*)event;
if(e->key() == Qt::Key_Return
|| e->key() == Qt::Key_Enter)
{
/* do what you want here */
}
}
return QObject::eventFilter(filterObj, event); // usual process other events
}
I use Qt to develop my application, and I want to listen the global mouse and keyboard events, so I can do something after detect these events. On windows platform, I use SetWindowsHookEx API. But I don't know how to do the similar thing on Linux.
My code on Windows as below:
/*********************listen mouse event*****************************/
mouseHook = SetWindowsHookEx(WH_MOUSE_LL, mouseProc, NULL, 0);
if (mouseHook == NULL) {
qDebug() << "Mouse Hook Failed";
}
/*********************listen keyboard event*****************************/
keyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, keyBoardProc, NULL, 0);
if (keyboardHook == NULL) {
qDebug() << "Keyboard Hook Failed";
}
Thank you for your answer sincerely!
Use the vast and helpful Qt Event System. That way you can use the same code on Windows as well as Linux.
I would recommend adding a eventFilter to your application. If the event caught is a QEvent::KeyPress or QEvent::MouseButtonPress( or QEvent::MouseButtonDblClick based on your requirement), take the necessary action.
If the event filter is to be applied for a particular widget in the main window, in the constructor of the main window, add
ui->myWidget->installEventFilter(this);
Now you need to implement the protected method eventFilter for the main window.
In the header file.
protected:
bool eventFilter(QObject* obj, QEvent* event);
Implementation
bool MainWindow::eventFilter(QObject* obj, QEvent* event)
{
if(event->type() == QEvent::KeyPress)
{
QKeyEvent* keyEvent = static_cast<QKeyEvent *>(event);
qDebug() << "Ate key press " << keyEvent->text();
return true;
}
else if(event->type() == QEvent::MouseButtonPress)
{
qDebug() << "Mouse press detected";
return true;
}
// standard event processing
return QObject::eventFilter(obj, event);
}
It is also possible to filter all events for the entire application, by installing an event filter on the QApplication or QCoreApplication object. You can read more on that from the documentation link I provided in the first line.
If the event filter has to be reused, I would recommend adding a dummy class which inherits from QObject. In this class you can just implement the function eventFilter. You can now pass an instance of this class to the function installEventFilter and communicate with the other objects using SIGNALS.
EDIT:
If you are looking to capture events even outside the application, Qt itself does not support that (yet). But you can use the Qxt library that adds support for it using the QxtGlobalShortcut class.
In a dialog, when the tab key is pressed, the focus changes to another widget. In Qt, is there any signal for when a widget loses its focus? Can I use it to check if the input is valid or not? If not, can I set the focus back and ask the user to re-input?
There's no signal but if you want to know when your widget has lost focus, override and reimplement void QWidget::focusOutEvent(QFocusEvent* event) in your widget. It will be called whenever your widget has lost focus. To give focus to a widget, use QWidget::setFocus(Qt::FocusReason).
To validate input in a QLineEdit or QComboBox you can subclass QValidator and implement your own validator, or use one of the existing subclasses, QIntValidator, QDoubleValidator, or QRegExpValidator. Set the validator with QLineEdit::setValidator(const QValidator*) and QComboBox::setValidator(const QValidator*) respectively.
If you want to validate the contents of a modal dialog box, one way would be to override QDialog::exec() with an implementation like this:
int MyDialog::exec() {
while (true) {
if (QDialog::exec() == QDialog::Rejected) {
return QDialog::Rejected;
}
if (validate()) {
return QDialog::Accepted;
}
}
}
bool MyDialog::validate() {
if (lineEdit->text().isEmpty()) {
QMessageBox::critical(this, "Invalid value", "The specified value is not valid");
lineEdit->setFocus();
lineEdit->selectAll();
return false;
}
return true;
}
It will not allow the user to close the dialog with the OK button or any other button with the Accepted role unless the contents of the dialog is successfully validated. In this example I assume the dialog has a QLineEdit named lineEdit and the validate function will make sure that its content is not empty. If it is, it will set the focus to the QLineEdit and show the dialog again.
It is also possible (and easier) to create the signal yourself
In the .cpp (do not forget to include the moc)
class FocusWatcher : public QObject
{
Q_OBJECT
public:
explicit FocusWatcher(QObject* parent = nullptr) : QObject(parent)
{
if (parent)
parent->installEventFilter(this);
}
virtual bool eventFilter(QObject *obj, QEvent *event) override
{
Q_UNUSED(obj)
if (event->type() == QEvent::FocusIn)
emit focusChanged(true);
else if (event->type() == QEvent::FocusOut)
emit focusChanged(false);
return false;
}
Q_SIGNALS:
void focusChanged(bool in);
};
And to connect it:
connect(new FocusWatcher(myWidget), &FocusWatcher::focusChanged, this, &View::doSomething);