Programmatically invoke Snap/Aero maximize
Asked Answered
R

2

7

Is there a way to programmatically invoke the Aera maximize effect using C or C++ for a specific window/window ID?

For example:

enter image description here

or

enter link description here
(source: thebuzzmedia.com)

I am using a border-less Qt window and Qt has an API for getting the window ID. I want to programmatically trigger the windows effects without the known triggers.

Roux answered 14/9, 2014 at 13:55 Comment(15)
If you want to fake a titlebar drag, return HTCAPTION from WM_NCHITTEST.Bloc
@Bloc I don't want to fake the title bar drag. I want to make the snap effect get triggered when I press a button for example.Roux
Unfortunately Microsoft elected to not provide any form of API for the Aero snap effects.Jonniejonny
@JonathanPotter Really? I guess this answers my question... :(Roux
@JacobKrieg Could you add link to a video that demonstrates this effect?Obau
@Obau Sure. I'm starting to search for one right now!Roux
@Obau I'm talking about the Aero Snap effect in this video: youtu.be/xO_7sbFEJrE?t=0m31s. When I say teardrop I refer to the effect which happens right after the mouse pointer touches the edge of the screen. The rest of the effect is represented by the expanding glass-like effectRoux
@JacobKrieg Got it. Are you looking for a cross-platform solution or can it be Windows-only?Obau
It can be windows only, I assume that it can easily be wrapped in some define guards and isolate it from the cross platform code. AFAIK Unity(Ubuntu desktop manager) doesn't rely on the title bar to do these effects, nor is KDE. Mac OS doesn't support it so it can be Windows only. I've uploaded an example on which I did some tests and which consists of a QMainWindow with a QWidget representing a title bar. The title bar is nothing but a QWidget with some mouse events implemented, i thought that it could help: sharesend.com/c5c330x4Roux
Ok, why is Chernobyl's answer not sufficient?Obau
Because it is not triggering the aero effect, it is just resizing the window. I know how to do this.Roux
@JacobKrieg see my edit please, final result of code which I wrote very similar to Aero Snap. Perfectly works with frameless windows.Decuple
If Windows doesn't have an API for triggering these, and "simulated" Aero snap is not sufficient, then what you have left is generating the mouse events to actually move the window and trigger the real snap as if user used the mouse. Not sure what this would require, Win 7 is pretty strict about applications messing with windows like that (to make it harder for eg. malware to fool the user). Doing this seems more trouble than it's worth, unless you have some very hard requirements to do it.Perigon
@Perigon This is the problem, I couldn't make this happen. This is an example of a window that moves the window into triggering the event but the event doesn't get triggered: sharesend.com/c5c330x4 Pointing to the source of the problem would be a lot helpful.Roux
Does SendMessage(hwnd, WM_SYSCOMMAND, SC_MAXIMIZE, 0); from https://mcmap.net/q/1481344/-problem-when-maximizing-window-in-c not solve your problem?Gobert
O
7

I don't want to talk about every single detail involved in achieving this effect, not only there's a lot that goes on but you also mentioned you understand the logic to place the windows at their specific locations. In this answer I'll address what I believe are the 2 main challenges:

  • How to receive and handle a maximize event?

  • How to create an approximation of the aero snap effect?

In order to answer the first question, we must analyze which event handlers are triggered when the window is maximized:

void resizeEvent(QResizeEvent* evt);   // Invoked first,
void paintEvent(QPaintEvent* event);   // then second, 
void changeEvent(QEvent* evt);         // and at last.

A Qt application is first notified of a resizeEvent(), which is followed by a paintEvent() to draw the window (or widget), and only after everything has been displayed, changeEvent() is invoked to let you know the widget was maximized (maybe it's a little bit late to receive such notification, I don't know).

Of all these, the only one we care about is resizeEvent(). This event handler informs the new window/widget size that can be used for comparison with the desktop size, thus allowing us to know if the event was actually a maximize request. Once we identify a maximize request, we can figure out whether the application should be maximized (and anchored) to right, left or to the center of the screen.

This would be the time to create the aero snap widget and place it on the screen as a visual clue to the user.

To answer the second question, I don't think is possible to call the native Windows API and ask it politely to perform this effect on your window. The only other logical choice is to write a code that approximates this effect ourselves.

The visual appearance can be replicated by drawing a transparent window with a shadow-ish border. The approach demonstrated in the source code below, creates and customizes a QWidget to make it behave and look like a aero snap window:

enter image description here

It's not the most beautiful thing in the world, I know. This demo creates a regular window for the user to interact with, and once it's maximized, it places itself to the left of the screen. To the right size of the screen it displays something that resembles an aero snap window (shown above).

The idea behind the aero snap widget is very simple: a QWidget with transparent background and a custom painting procedure. In other words, it's a transparent window which draws a rounded rectangle with a shadow and that's it.

To make it a bit more realistic, you should add some animation to resize the widget little by little. A for loop might do the trick, but if you need something fancy you'll end up using timers. If you take a look here, you can see the quickest & dirtiest method to perform animation with Qt in action, and better ways to deal with animation. However, for simple tasks like this, stick with frame-based animation.

main.cpp:

#include "window.h"
#include <QApplication>

int main(int argc, char* argv[])
{
    QApplication app(argc, argv);
    Window window;
    window.show();

    return app.exec();
}

window.h:

#pragma once
#include "snapwindow.h"
#include <QMainWindow>
#include <QEvent>

class Window : public QMainWindow
{    
public:
    Window();

    void resizeEvent(QResizeEvent* evt);
    //void paintEvent(QPaintEvent* event);
    void changeEvent(QEvent* evt);

private:
    SnapWindow* _sw;
};

window.cpp:

#include "window.h"
#include "snapwindow.h"

#include <QDebug>
#include <QWindowStateChangeEvent>
#include <QApplication>
#include <QDesktopWidget>

Window::Window()
{
    setWindowTitle("AeroSnap");
    resize(300, 300);    

    _sw = new SnapWindow(this);
    _sw->hide();
}

void Window::changeEvent(QEvent* evt)
{
    if (evt->type() == QEvent::WindowStateChange)
    {
        QWindowStateChangeEvent* event = static_cast<QWindowStateChangeEvent*>(evt);

        if (event->oldState() == Qt::WindowNoState &&
                windowState() == Qt::WindowMaximized)
        {
            qDebug() << "changeEvent: window is now maximized!";
        }
    }
}

// resizeEvent is triggered before window_maximized event
void Window::resizeEvent(QResizeEvent* evt)
{
    qDebug() << "resizeEvent: request to resize window to: " << evt->size();

    QSize desktop_sz = QApplication::desktop()->size();
    //qDebug() << "resizeEvent: desktop sz " << desktop_sz.width() << "x" << desktop_sz.height();

    // Apparently, the maximum size a window can have in my system (1920x1080)
    // is actually 1920x990. I suspect this happens because the taskbar has 90px of height:
    desktop_sz.setHeight(desktop_sz.height() - 90);

    // If this not a request to maximize the window, don't do anything crazy.
    if (desktop_sz.width() != evt->size().width() ||
        desktop_sz.height() != evt->size().height())
        return;

    // Alright, now we known it's a maximize request:
    qDebug() << "resizeEvent: maximize this window to the left";

    // so we update the window geometry (i.e. size and position)
    // to what we think it's appropriate: half width to the left
    int new_width = evt->size().width();
    int new_height = evt->size().height();
    int x_offset = 10;
    setGeometry(x_offset, 45, new_width/2, new_height-45); // y 45 and height -45 are due to the 90px problem

    /* Draw aero snap widget */

    _sw->setGeometry(new_width/2-x_offset, 0, new_width/2, new_height);
    _sw->show();

    // paintEvent() will be called automatically after this method ends,
    // and will draw this window with the appropriate geometry.
}

snapwindow.h:

#pragma once
#include <QWidget>

class SnapWindow : public QWidget
{
public:
    SnapWindow(QWidget* parent = 0);

    void paintEvent(QPaintEvent *event);
};

snapwindow.cpp:

#include "snapwindow.h"
#include <QPainter>
#include <QGraphicsDropShadowEffect>

SnapWindow::SnapWindow(QWidget* parent)
: QWidget(parent)
{      
    // Set this widget as top-level (i.e. owned by user)
    setParent(0);

    /* Behold: the magic of creating transparent windows */

    setWindowFlags(Qt::Widget | Qt::FramelessWindowHint);
    setStyleSheet("background:transparent;");
    setAttribute(Qt::WA_NoSystemBackground, true); // speed up drawing by removing unnecessary background initialization
    setAttribute(Qt::WA_TranslucentBackground);
    //setAutoFillBackground(true);

    /* Use Qt tricks to paint stuff with shadows */

    QGraphicsDropShadowEffect* effect = new QGraphicsDropShadowEffect();
    effect->setBlurRadius(12);
    effect->setOffset(0);
    effect->setColor(QColor(0, 0, 0, 255));
    setGraphicsEffect(effect);
}

void SnapWindow::paintEvent(QPaintEvent *event)
{
    QWidget::paintEvent(event);

    /* Lazy way of painting a shadow */

    QPainter painter(this);
    QPen pen(QColor(180, 180, 180, 200));
    pen.setWidth(3);
    painter.setPen(pen);

    // Offset 6 and 9 pixels so the shadow shows up properly
    painter.drawRoundedRect(QRect(6, 6, (width()-1)-9, (height()-1)-9), 18, 18);
}

This is just a quick demo to point you to the right direction. It is by no means a complete implementation of the effect you are looking for.

Obau answered 23/9, 2014 at 0:0 Comment(4)
Thank you very much for your hints! I don't want to be a complete a**hole and just take your code and expect everything to work out of the box but I looked at it and for me it doesn't behave like expected. I realized that SnapWindow gets triggered on the resize event, so when I resize the window it should do its thing. But it doesn't happen for me. I called setWindowFlags(Qt::FramelessWindowHint) and setStatusBar(new QStatusBar()); on the main window but resizing doesn't do anything although the event function is called: i.imgur.com/2WyCPv3.png Could it be because I use Windows 7?Roux
It's resizeEvent() who has some logic that decides if it was a maximize event that happened. Spend some time in that function, add debug messages to familiarize yourself with it. From what you described, this logic is failing on your computer, and the most probable cause is due to some magic number (90) being used (search for the string 1920x1080 and read the comments). However, this could also fail if you are using 2 monitors because QApplication::desktop()->size() returns the total size of monitor1 + monitor2.Obau
I'm going to give it a try and come back with the news, thanks a lot! Regarding the 2 monitors problem there has to be for sure some API that gives you the number of monitors and maybe the monitor you are in and this should be resolvable. Thanks again!Roux
I didn't try the code but it should work so I'm giving you the bounty. I will let you know if it worked but I don't see how it shouldn't. Thanks!Roux
D
3

Maybe it is not what you need, but this effect is just resizing and moving window then try use Qt methods to do this.

bool left = false;
QSize size = QApplication::desktop()->size();//resolution of current screen
if(left)
{//left side
    this->setGeometry(0, 0, size.width()/2, size.height());//(maybe need do some changes)
}
else
{//right side
    this->setGeometry(size.width()/2, 0, size.width()/2, size.height());
}

With QApplication::desktop() it will work properly on screen with different resolutions.

In web I found something similar in winapi, but it didn't work properly:

HWND act = GetForegroundWindow();
PostMessage((HWND)act,WM_NCLBUTTONDBLCLK, HTTOP, 0);

The best way

Combine this approaches. For example:

HWND act = GetForegroundWindow();
bool left = false;
QSize size = QApplication::desktop()->size();
if(left)
{
    this->move(0,0);
    PostMessage((HWND)act,WM_NCLBUTTONDBLCLK, HTTOP, 0);
    this->resize(size.width()/2,QApplication::desktop()->height());

}
else
{
    this->move(size.width()/2,0);
    PostMessage((HWND)act,WM_NCLBUTTONDBLCLK, HTTOP, 0);
    this->resize(size.width()/2,QApplication::desktop()->height());
}

Why? Because move() regulate left and right sides, but PostMessage (winapi) set window's height properly on every screen (window will not locate lower then taskbar, as in your example)

EDIT

I changed code a little and now it is better. Yes, it is resizing again, but now it hasn't winapi code (PostMessage etc), so Photoshop doesn't catch it, there is one interesting method in Qt which called availableGeometry. It return normal height of screen which we need, with this method borderless windows perfectly simulates Aero Snap effects in different directions. It is works, maybe don't so good, but as I can see, there isn't API for Aero effects. Maybe this approach will be normal for yoo.

There is Aero Peek in Qt : http://qt-project.org/doc/qt-5/qtwinextras-overview.html , but it is can't solve this problem too.

Code:

bool left = true;
bool upper = true;

if(upper)
{
    QRect rect = QApplication::desktop()->availableGeometry(-1);
    this->setGeometry(rect);
}
else if(left)
    {
        QRect rect = QApplication::desktop()->availableGeometry(-1);
        rect.setWidth(rect.width()/2);
        this->setGeometry(rect);
    }
    else
    {
        QRect rect = QApplication::desktop()->availableGeometry(-1);
        int half = rect.width()/2;
        rect.setX(half);
        rect.setWidth(half);
        this->setGeometry(rect);
    }

Try it with frameless window! You should choose one direction or let user choose it.

Decuple answered 14/9, 2014 at 15:16 Comment(2)
This will generally fail for multi-monitor setups. It also fails to implement the remaining Aero Snap effects (like maximizing the height while keeping the width constant, or resizing to half the display size). It will also fail for applications that have a custom handler for a caption bar double click (like Photoshop used to).Sucker
Thank you very much for your effort(+1)! Unfortunately the code resolves the issue of resizing the window, not generating the "growing glass" effect(see the pictures above) which I want to generate.Roux

© 2022 - 2024 — McMap. All rights reserved.