Repository: pyqtgraph/pyqtgraph

Issue:
GLViewWidget::readQImage() partial image on hidpi
<!-- In the following, please describe your issue in detail! -->
<!-- If some of the sections do not apply, just remove them. -->

### Short description
<!-- This should summarize the issue. -->
On a system with device pixel ratio of 2.0, GLViewWidget::readQImage() does not save the full screen image. Instead, about a quarter is saved.

### Code to reproduce
<!-- Please provide a minimal working example that reproduces the issue in the code block below.
     Ideally, this should be a full example someone else could run without additional setup. -->
```python
import pyqtgraph.opengl as gl
import pyqtgraph as pg
import numpy as np

app = pg.mkQApp()
w = gl.GLViewWidget()
w.opts['distance'] = 30
w.show()

n = 101
s = np.linspace(-10,10,n)
X, Y = np.meshgrid(s, s)
Z = 10 * np.sinc(np.hypot(X, Y))
for i in range(100):
    color = pg.glColor((i,n*1.3))
    row = np.column_stack((X[i,:], Y[i,:], Z[i,:]))
    col = np.column_stack((X[:,i], Y[:,i], Z[:,i]))
    plt = gl.GLLinePlotItem(pos=row, color=color)
    w.addItem(plt)
    plt = gl.GLLinePlotItem(pos=col, color=color)
    w.addItem(plt)

app.exec_()
qimg = w.readQImage()
qimg.save('qimage.png')

```

### Expected behavior
<!-- What should happen? -->
![expected](https://user-images.githubusercontent.com/2657027/115569214-9673ff80-a2ef-11eb-9524-fd304ccba528.png)


### Real behavior
<!-- What happens? -->
![qimage](https://user-images.githubusercontent.com/2657027/115568985-63317080-a2ef-11eb-9772-4c4b173d2c8c.png)

### Tested environment(s)

 * PyQtGraph version: 0.12.1 <!-- output of pyqtgraph.__version__ -->
 * Qt Python binding: PySide and PyQt <!-- output of pyqtgraph.Qt.VERSION_INFO -->
 * Python version: 3.8.9
 * NumPy version: 1.20.2 <!-- output of numpy.__version__ -->
 * Operating system: Windows 10
 * Installation method: pip install -e . <!-- e.g. pip, conda, system packages, ... -->

### Additional context
Verified that on a non-hidpi system, the full screen image is saved.

I don't quite understand the following code found in GLViewWidget.py
```python
    def width(self):
        if self._dpiRatio.is_integer():
            return super().width()
        else:
            return int(super().width() * self._dpiRatio)
```
If dpr is 2.0, it takes the first branch.
If dpr is 1.5, it would take the second branch.
i.e. if I were to multiply width() by dpr to get the full image width, wouldn't that cause double multiplication when dpr is 1.5?


Discussion:
I ran into this when testing my hidpi settings and noticed when scaling was set to a non fractional value.  I had written some code to compensate for that; but I think that code got removed in a later PR.  I'll investigate later today.
> I don't quite understand the following code found in GLViewWidget.py

Forgot to follow up now that I'm at a keyboard.

During my testing for #1516 I noticed that when scaling was at an even integer; the views would be as you see them in the screen shot; but at fractional scaling things was as expected.  I saw reference to this kind of logic in the Matplotlib repository, but on a quick google search, I couldn't find it.

I wonder if this issue came up again with the migration to QOpenGLWidget in #1525 

Resolving patch:
diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py
--- a/pyqtgraph/opengl/GLViewWidget.py
+++ b/pyqtgraph/opengl/GLViewWidget.py
@@ -59,10 +59,6 @@ def __init__(self, parent=None, devicePixelRatio=None, rotationMethod='euler'):
         self.makeCurrent()
 
 
-    @property
-    def _dpiRatio(self):
-        return self.devicePixelRatioF() or 1
-
     def _updateScreen(self, screen):
         self._updatePixelRatio()
         if screen is not None:
@@ -79,16 +75,12 @@ def showEvent(self, event):
         self._updateScreen(window.screen())
         
     def width(self):
-        if self._dpiRatio.is_integer():
-            return super().width()
-        else:
-            return int(super().width() * self._dpiRatio)
+        dpr = self.devicePixelRatio()
+        return int(super().width() * dpr)
     
     def height(self):
-        if self._dpiRatio.is_integer():
-            return super().height()
-        else:
-            return int(super().height() * self._dpiRatio)
+        dpr = self.devicePixelRatio()
+        return int(super().height() * dpr)
 
 
     def reset(self):
@@ -147,10 +139,10 @@ def setBackgroundColor(self, *args, **kwds):
         
     def getViewport(self):
         vp = self.opts['viewport']
-        dpr = self.devicePixelRatio()
         if vp is None:
-            return (0, 0, int(self.width() * dpr), int(self.height() * dpr))
+            return (0, 0, self.width(), self.height())
         else:
+            dpr = self.devicePixelRatio()
             return tuple([int(x * dpr) for x in vp])
         
     def devicePixelRatio(self):
@@ -158,7 +150,7 @@ def devicePixelRatio(self):
         if dpr is not None:
             return dpr
         
-        return QtWidgets.QOpenGLWidget.devicePixelRatio(self)
+        return self.devicePixelRatioF()
         
     def resizeGL(self, w, h):
         pass
@@ -174,8 +166,7 @@ def setProjection(self, region=None):
 
     def projectionMatrix(self, region=None):
         if region is None:
-            dpr = self.devicePixelRatio()
-            region = (0, 0, self.width() * dpr, self.height() * dpr)
+            region = (0, 0, self.width(), self.height())
         
         x0, y0, w, h = self.getViewport()
         dist = self.opts['distance']


Test patch: