How to properly save window state in QML - qt

I read the Qt Documentations, I checked out a couple examples provided with the SDK, I build Qt Creator from source to see how the Qt devs do it...still no luck.
I am developing a cross platform application for Windows and Mac. On the Mac side I can try basically any of my solutions all of them work perfectly (I guess that is a thanks to the MacOS). On the other hand on Windows I always find some kind of bug or sketchy behavior.
Before I go into more details the root of my problems is supporting multiple monitor environments with monitors, which have different resolutions.
My two main solutions in a nutshell:
Since I am writing my application mainly in QML I use ApplicationWindow for my main window. I save the state of my ApplicationWindow in Settings. My code takes into consideration if the previously saved position is still valid (for example if the application was closed while it was on a monitor, which is no longer available)...the only reason why I have to do this because Windows would just open my app's window in "outer space" (Mac handles this automatically). My application (on Windows) gets into a really weird state if I close my application on one of the monitors and then I change the other monitors scaling factor and then I reopen my application. It opens up on the right monitor but it gets way over scaled and the UI elements are just weirdly floating. If I resize the window everything gets back to normal.
I exposed my QML ApplicationWindow to C++ put into a QWidget container, which then I attached to a QMainWindow by setting it as a setCentralWidget. With this method I have access to saveGeometry and restoreGeometry, which automatically takes care of multiple monitor positioning, but the scaling anomaly what I described in 1. still persist.
Did anybody solved this? Thanks in advance for any help and hin

#retif asked for me to provide a writeup when I commented I knew about these types of issues months ago.
TLDR
When dealing with an absolute positioning issue with a Qt Windows on the Windows OS - especially on Windows 10, it's best to be using system-DPI awareness. Some interpolation is required when going from Windows coordinate spaces (at different DPI awareness levels) to Qt coordinate spaces when you are trying to have the best scaling.
Here's what I did on my team's application.
The Problem:
It is really hard to do absolute positioning of a Qt Window when there are multiple monitors and multiple DPI resolutions to contend with.
Our application window "pops up" from a Windows task tray icon (or menu bar icon on Mac).
The original code would take the Windows screen coordinate position of the tray icon and use that as a reference to compute the positioning of the window.
At app startup, before Qt was initialized, we'd set the environment variable, QT_SCALE_FACTOR to be a floating point value of the (systemDPI/96.0). Sample code:
HDC hdc = GetDC(nullptr);
unsigned int dpi = ::GetDeviceCaps(hdc, LOGPIXELSX);
stringstream dpiScaleFactor;
dpiScaleFactor << (dpi / 96.0);
qputenv("QT_SCALE_FACTOR", QByteArray::fromStdString(dpiScaleFactor.str()));
The code above takes the primary monitors "DPI scale" and tells Qt to match it. It has the pleasant effect of letting Qt compute all scaling natively instead of a bitmap stretch like Windows would do in a non-DPI aware application.
Because we initialize Qt using the QT_SCALE_FACTOR environment variable (based on primary monitor DPI), we were using that value to scale the Windows coordinates when converting to Qt's QScreen coordinate space for this initial window placement.
Everything worked fine on single monitor scenarios. It even worked fine on multi-monitor scenarios as long as the DPI on both monitors was the same. But on configurations of multiple monitors with different DPIs, things got off. If the window had to pop-up on the non-primary monitor as a result of a screen change or a projector getting plugged in (or unplugged), weird stuff would happen. The Qt windows would appear in the wrong position. Or in some cases, the content inside the window would scale incorrectly. And when it did work, there would be a "too big" or "too small" problem of the windows scaled to one DPI when positioned on a similar sized monitor running at a different DPI.
My initial investigation revealed that Qt's coordinate space for the different QScreens geometries looked off. The coordinates of each QScreen rect was scaled based on the QT_SCALE_FACTOR, but the adjacent axis of the individual QScreen rects were not aligned. e.g. One QScreen rect might be {0,0,2559,1439}, but the monitor to the right would be at {3840,0,4920,1080}. What happened to the region where 2560 <= x < 3840 ? Because our code that scaled x and y based on QT_SCALE_FACTOR or DPI was relying on primary monitor being at (0,0) and all monitors to have adjacent coordinate spaces. If our code scaled the assumed position coordinate to something on the other monitor, it might get positioned in an odd place.
It took a while to realize this wasn't a Qt bug per se. It's just that Qt is just normalizing the Windows coordinate space that has these weird coordinate space gaps to begin with.
The fix:
The better fix is to tell Qt to scale to the primary monitor's DPI setting and run the process in system-aware DPI mode instead of per-monitor-aware DPI mode. This has the benefit of letting Qt scale the window correctly and without blurriness or pixelation on the primary monitor and to let Windows scale it on monitor changes.
A bit of background. Read everything in this section of High DPI programming on MSDN. A good read.
Here's what we did.
Kept the initialization of the QT_SCALE_FACTOR as described above.
Then we switched the initialization of our process and Qt from per-monitor DPI awareness to system-aware DPI. Th benefit of system-dpi is that it lets Windows auto-scale the application windows to the expected size as the monitors change out from underneath it. (All Windows APIs act as if all monitors have the same DPI). As discussed above, Windows is doing a bitmap stretch under the hood when the DPI is different from primary monitor. So there's a "blurriness issue" to contend with on monitor switching. But it's sure better than what it was doing before!
By default Qt will try to initialize the process to a per-monitor aware app. To force it to run in system-dpi awareness, call SetProcessDpiAwareness with a value of PROCESS_SYSTEM_DPI_AWARE very early in application startup before Qt initializes. Qt won't be able to change it after that.
Just switching to System-aware dpi fixed a bunch of other issues.
Final bug fix:
Because we position our window at an absolute position (directly above the systray icon in the task tray), we rely on the Windows API,Shell_NotifyIconGetRect to give us the coordinate of the systray. And once we know the offset of the systray, we calculate a top/left position to position for our window be on the screen. Let's call this position X1,Y1
However, the coordinates returned from Shell_NotifyIconGetRect on Windows 10 will always be the "per-monitor aware" native coordinates and not scaled to the System DPI. Use PhysicalToLogicalPointForPerMonitorDPI to convert. This API doesn't exist on Windows 7, but it's not needed. Use LoadLibrary and GetProcAddress for this API if you are supporting Windows 7. If the API doesn't exist, just skip this step. Use PhysicalToLogicalPointForPerMonitorDPI to convert X1,Y1 to a system-aware DPI coordinate wel'll call X2,Y2.
Ideally, X2,Y2 is passed to Qt methods like QQuickView::setPosition But....
Because we were using the QT_SCALE_FACTOR environment variable to get the application to scale the primary monitor DPI, all the QScreen geometries would have normalized coordinates that were different from what Windows used as the screen coordinate system. So the final windows position coordinate of X2,Y2 computed above would not map to the expected position in Qt coordinates if the QT_SCALE_FACTOR environment var was anything but 1.0
Final fix to get the final top/left position of the Qt window calculated.
Call EnumDisplayMonitors and enumerate the monitors list. Find the monitor in which X2,Y2 discussed above is positioned on. Save off the MONITORINFOEX.szDevice as well as the MONITORINFOEX.rcMonitor geometry in a variable called rect
Call QGuiApplication::screens() and enumerate these objects to find the QScreen instance whose name() property matches the MONITORINFOEX.szDevice in the previous step. And then save off the QRect returned by the geometry() method of this QScreen into a variable called qRect. Save the QScreen into a pointer variable called pScreen
Final step of converting X2,Y2 to XFinal,YFinal is this algorithm:
XFinal = (X2 - rect.left) * qRect.width
------------------------------- + qRect.left
rect.width
YFinal = (Y2 - rect.top) * qRect.height
------------------------------- + qRect.top
rect.height
This is just basic interpolation between screen coordinate mappings.
Then the final window positioning is to set both the QScreen and XFinal,YFinal positions on the view object of Qt.
QPoint position(XFinal, YFinal);
pView->setScreen(pScreen);
pView->setPosition(position);
Other solutions considered:
There is the Qt mode called Qt::AA_EnableHighDpiScaling that can be set on the QGuiApplication object. It does much of the above for you, except it forces all scaling to be an integral scale factor (1x, 2x, 3x, etc... never 1.5 or 1.75). That didn't work for us because a 2x scaling of the window on a DPI setting of 150% looked too big.

Related

The main thread is blocked by the redrawing by update() on qwidget causing the detection of Qkeyevent to be delayed in Qt

I am creating a text editor application using Qt. This application needs to update many texts in the area.
I investigated the application performance and found that increasing the application window size significantly reduced the key repeat rate. That is, for example, the operation of scrolling the drawing area by continuing to input a key becomes extremely slow as the application window size increases. The cause of this problem is that the Update() function itself, which updates the entire application widget, appears to have a significant effect, rather than the cost of a lot of text rendering.
I wrote a simple application to check this problem.
This application draws a random rectangle on the application by any key input, and outputs the key repeat rate to the standard output.
https://github.com/akiyosi/testguiapp/blob/master/main.go
This drawing speed (that is, the speed of key repeat) decays as the application window size increases.
On my laptop (MacBook Pro Late 2013), the application can achieve 60fps with window size less than one-third of the screen, but attenuates to about 40fps with more than half of the screen.
Is there a way to keep the key repeat rate unaffected by the widget's Update()?

QT High DPI Support on Windows

According to documentation here http://doc.qt.io/qt-5/highdpi.html QT 5.4+ introduces high DPI support.
However, either I’m missing something fundamental or the current support is still in very early stages.
I’m writing a brand new application so I have a chance to do it right from the ground up. I understand that I would have to use layouts instead of fixed positioning etc, but there always going to be cases in which I would have to specify, for example a minimum/maximum size of a control. I can specify them in the editor, but these are device pixels. So if I change my Windows settings to use 150% DPI then min/max values in the editor would be too small. Of course I can obtain that ratio and adjust all the required values in code, but then what kind of high DPI support does QT give for me if I have to do everything by hand? I mean how is it different to pre QT 5.4?
Then an interesting one is QT_DEVICE_PIXEL_RATIO environment variable. It does exactly what I need, it multiplies all pixels set in editor by a factor. But why is it an environment variable and not a per application setting? Why does it only support integer values of 2, 3 etc, since we know that Windows has settings like 125, 150% etc. and why couldn’t it automatically read the Windows setting and set itself to that value?
Qt fully supports high DPI monitors from Qt 5.6 onward, via attribute or environment variable (except on OS X where support is native). For the attribute method, use:
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); // DPI support
QApplication app(argc, argv);
return app.exec();
}
or set the system environment variable:
QT_AUTO_SCREEN_SCALE_FACTOR=1
More information on the Qt blog
I must response the answer from #Nicolas Holthaus that the way you enable Qt::AA_EnableHighDpiScaling may not be absolutely right. Since it will round the user DPI setting. Eg. Windows DPI setting be 150%, the result will be 200% for font and size, while 125% will be 100%.
The right way to do correct DPI scaling is set environment variable QT_SCALE_FACTOR. For the same example, if DPI setting is 150%, set QT_SCALE_FACTOR with value 1.5. Then the result will be exactly 150% in font and size.
See the qt official document http://doc.qt.io/qt-5/highdpi.html and you will find
QT_SCALE_FACTOR [numeric] defines a global scale factor for the whole application, including point sized fonts.

Animating QGraphicsItem Efficiently

I have a QGraphicsSvgItem that I need to rotate a certain amount based on an external variable coming into my application. Currently I'm directly setting the rotation property of the item, but this appears to significantly drop my frame rate. (117.0 fps -> 98.0 fps) is this normal? Is this the correct way to animate a QGraphicsItem? I realize that the framerate sounds plenty high, but I'm concerned that as more items in the view are animated, that the performance will become a problem.
The code below is being called via a QTimer timeout signal at an interval of 0.
Note that I have also tried using a QGraphicsItem with a custom QPainterPath to draw the item rather than an SVG and it made no noticeable performance difference.
qreal diff = m_rope_length_ft[0] - m_rope_length_ft_prev[0];
qreal angle = diff * 5.0;
m_rope_reel[0]->setRotation(m_rope_reel[0]->rotation() + angle);
m_rope_reel[1]->setRotation(m_rope_reel[1]->rotation() + angle);
m_rope_reel[0]->update();
m_rope_reel[1]->update();
A more proper way to animate in Qt is to use QPropertyAnimation. It requires that:
your graphics item is inherited from both QObject and QGraphicsSvgItem;
rotation is registered as property with Q_PROPERTY (getters and setters already exist so there is no need to implement them).
Also you should try to tune graphics item's cache mode. ItemCoordinateCache seems to be suitable for your case. There is also view cache mode, but it's hard to say which is better in your case.
Note that performance optimization should be done using average amount of items your program should be able to process. Optimizing a single item rendering may be pointless.

Qt restoring geometry incorrectly on X11

I'm using Qt 4.8.4 on Xorg 1.13.1. I'm saving the window geometry in the same way suggested in the documentation:
QSettings settings;
QByteArray geom = widget->saveGeometry();
settings.setValue("widget_geometry", geom);
Also restoring on startup:
QSettings settings;
QByteArray geom = settings.value(buf).toByteArray();
widget->restoreGeometry(geom);
The problem I'm facing is that the window isn't placed in the same position as it was saved. The size is fine. Depending on which window manager I use, the results are different.
In i3, the window is one pixel off on both axes, presumably to make up for the window border.
In Unity (mutter?), no matter where I put the window, it gets nudged over 10 pixels every time it is restored. It also seems to have a y-limit, but I can live with that since it's consistent.
In fluxbox, the window is restored to its saved size and position.
I've done a bit of digging and deconstructed the byte array that is saved into the settings file. I've tried manually adjusting the position of the window instead of using restoreGeometry(), but there's really no way for the application to know what kind of WM inconsistencies it's dealing with. In some WMs, setting the position using the frame geometry instead of the normal geometry works, but screws up other ones. I've read from the Qt geometry documentation that you can't really count on any kind of consistency from X11 window managers, so I'm out of ideas. I'm just hoping some clever person out there has solved this!

Qt4/Opengl bindTexture in separated thread

I am trying to implemente a CoverFlow like effect using a QGLWidget, the problem is the texture loading process.
I have a worker (QThread) for loading images from disk, and the main thread checks for new loaded images, if it finds any then uses bindTexture for loading them into QGLContext. While the texture is being bound, the main thread is blocked, so I have a fps drop.
What is the right way to do this?
I have found that the default behaviour of bindTexture in Qt4 is extremelly slow:
bindTexture(image,target,format,LinearFilteringBindOption | InvertedYBindOption | MipmapBindOption)
using only the LinearFilteringBindOption in the binding options speeds up the things a lot, this is my current call:
bindTexture(image, GL_TEXTURE_2D,GL_RGBA,QGLContext::LinearFilteringBindOption);
more info here : load time for a 3800x2850 bmp file reduced from 2 seconds to 34 milliseconds
Of course, if you need mipmapping, this is not the solution. In this case, I think that the way to go is Pixel Buffer Objects.
Binding in the main thread (single QGLWidget solution):
decide on maximum texture size. You could decide it based on maximum possible widget size for example. Say you know that the widget can be at most (approximately) 800x600 pixels and the largest cover visible has 30 pixels margins up and down and 1:2 aspect ratio -> 600-2*30 = 540 -> maximum size of the cover is 270x540, e.g. stored in m_maxCoverSize.
scale the incoming images to that size in the loader thread. It doesn't make sense to bind larger textures and the larger it is, the longer it'll take to upload to the graphics card. Use QImage::scaled(m_maxCoverSize, Qt::KeepAspectRatio) to scale loaded image and pass it to the main thread.
limit the number of textures or better time spent binding them per frame. I.e. remember the time at which you started binding textures (e.g. QTime bindStartTime;) and after binding each texture do:
if (bindStartTime.elapsed() > BIND_TIME_LIMIT)
break;
BIND_TIME_LIMIT would depend on frame rate you want to keep. But of course if binding each one texture takes much longer than BIND_TIME_LIMIT you haven't solved anything.
You might still experience framerate drop while loading images though on slower machines / graphics cards. The rest of the code should be prepared to live with it (e.g. use actual time to drive animation).
Alternative solution is to bind in a separate thread (using a second invisible QGLWidget, see documentation):
2. Texture uploading in a thread.
Doing texture uploads in a thread may be very useful for applications handling large amounts of images that needs to be displayed, like for instance a photo gallery application. This is supported in Qt through the existing bindTexture() API. A simple way of doing this is to create two sharing QGLWidgets. One is made current in the main GUI thread, while the other is made current in the texture upload thread. The widget in the uploading thread is never shown, it is only used for sharing textures with the main thread. For each texture that is bound via bindTexture(), notify the main thread so that it can start using the texture.

Resources