Thursday 9 August 2012

Fake child window implementation in Qt

Well, the unorthodox idea didn't really pan out; the event handlers are called before the actual event finishes. So, on minimize, I had no problem, as I set the fake-child (FC) windows as children of the main window and they were all minimized together. However, on restore, things got ugly - by removing the parent/child relationship before the event finishes, all I achieved was preventing the FC windows from being restored. Out of desperation, I even did some experimentation with a timer, but, by this point, the "smell factor" was pretty much overwhelming.

So, inheritance it is. And how does it work?

I've created 2 classes: OrphanQMainWindow (which inherits from QMainWindow) and OrphanQDialog (which inherits from QDialog). These will be the two base classes for any app that needs to show the main window above (on the Z-order) its "child" windows.

OrphanQMainWindow is the simplest, but it dependes heavily on OrphanQDialog, so we'll show this class first.

We added the following (private) data members:

int zOrderIndex;
QPoint restorePosition;

static std::vector<OrphanQDialog*> orphanDialogs;
static unsigned int zOrderCounter;


orphanDialogs contains pointers to all the OrphanQDialog instances, until they're deleted. restorePosition is set when the dialog is hidden, so we can move it back to its position when it's restored. Without this measure, the dialog would be placed at the position it had when it was created. It's another design decision I don't understand, but let's not dwell on that.

zOrderCounter is used to keep track of the dialogs' Z-order. Each time a dialog is activated, zOrderCounter will be incremented, and its value will be assigned to the dialog's zOrderIndex. We handle this on the event handler for the Change event.

void OrphanQDialog::changeEvent(QEvent *event)
{
    QDialog::changeEvent(event);

    if (event->type() != QEvent::ActivationChange)
    {
        return;
    }

    if (isActiveWindow())
    {
        zOrderIndex = ++zOrderCounter;
    }
}


Next, we have three static member functions. These are called by the main window, when it minimizes, closes, and restores.

void OrphanQDialog::MinimizeAll()
{
    for (auto window : orphanDialogs)
    {
        window->restorePosition = window->pos();
        window->hide();
    }
}

void OrphanQDialog::CloseAll()
{
    for (auto window : orphanDialogs)
    {
        window->close();
    }
}

These two are self-explanatory.

void OrphanQDialog::RestoreAll()
{
    zOrderCounter = 0;

    std::sort(orphanDialogs.begin(), orphanDialogs.end(), [](OrphanQDialog* rhs,
        OrphanQDialog* lhs) -> bool
        { return rhs->zOrderIndex < lhs->zOrderIndex; });

    for (auto window : orphanDialogs)
    {
        window->move(window->restorePosition);
        window->show();
        window->zOrderIndex = ++zOrderCounter;
    }
}

Restoring is where everything comes together. First, we reset the Z-order counter. It's not really necessary, but it saves us from a highly-unlikely overflow, should our little app miraculously achieve an uptime measured in light years.

Then, we sort the vector according to the dialogs' ascending Z-order index. This makes sure we iterate through the vector to show the dialogs in the correct order, and also that we reset the dialogs' Z-order index without losing this order. As far as the user is concerned, everything is restored to its previous state, which was our goal, actually.

Finally, the ctor and dtor.

OrphanQDialog::OrphanQDialog(QWidget *parent) :
    QDialog(parent), zOrderIndex(++zOrderCounter)
{
    setAttribute(Qt::WA_DeleteOnClose);
    setWindowFlags(windowFlags() | Qt::WindowMinimizeButtonHint);
    orphanDialogs.push_back(this);
}

OrphanQDialog::~OrphanQDialog()
{
    // NOTE TO SELF: DON'T TRY TO USE RANGE-FOR WHEN YOU CALL
    // METHODS THAT NEED AN ITERATOR
    for(auto it = orphanDialogs.begin(); it != orphanDialogs.end(); ++it)
    {
        if (*it == this)
        {
            orphanDialogs.erase(it);
            break;
        }
    }
}

The dialog is created with the attribute Qt::WA_DeleteOnClose in order to avoid more logic to handle the case of dialogs that are still in the orphanDialogs vector but have already been closed, as we wouldn't want to show those. I've also added the expected frame buttons for a modeless dialog. And, yes, as you can see from the comment, I've tried the totally logic and even more totally wrong invocation of vector::erase() in the context of a range-for.

That's it for OrphanQDialog. Now, for OrphanQMainWindow, where we just implement the necessary events.

void OrphanQMainWindow::closeEvent(QCloseEvent *event)
{
    OrphanQDialog::CloseAll();
    event->accept();
}

void OrphanQMainWindow::changeEvent(QEvent *event)
{
    QMainWindow::changeEvent(event);

    if (event->type() != QEvent::WindowStateChange)
        return;

    Qt::WindowStates os = static_cast<QWindowStateChangeEvent*>(event)->oldState();
    Qt::WindowStates ns = windowState();

    // IF WE'RE MINIMIZING, MINIMIZE ALL THE OTHER WINDOWS
    if (ns & Qt::WindowMinimized)
    {
        OrphanQDialog::MinimizeAll();
    }

    //IF WE'RE RESTORING FROM A MINIMIZE, RESTORE ALL THE OTHER WINDOWS
    if (os & Qt::WindowMinimized)
    {
        OrphanQDialog::RestoreAll();
    }
}

Again, pretty much self-explanatory.

To get this working, you just add these classes to your Qt project, and inherit from them, instead of inheriting from QMainWindow and QDialog. And you may send my regards to Uncle Robert.

Two things to bear in mind:
  • If you implement the changeEvent() event handler in your class that inherits from OrphanQDialog, don't forget to call OrphanQDialog::changeEvent(), or you'll lose the Z-order tracking. The same rule applies if you implement the changeEvent()/closeEvent() handlers on your main window class - e.g., if you implement closeEvent() and don't call OrphanQMainWindow::closeEvent(), the dialogs won't be closed when you close the main window.
  • The dialogs aren't actually being minimized/restored, just hidden/shown. If your dialog classes need to do something when this happens, implement handlers for the hide/show events.
Points for future research:
  • There might be a more Qt-ish way of doing this, perhaps using signals and slots.
  • Take a look at how to actually minimize/restore the dialogs, instead of doing hide/show.

No comments:

Post a Comment