Repository: pyqtgraph/pyqtgraph

Issue:
FillBetweenItem has no way to change FillRule
<!-- In the following, please describe your issue in detail! -->
<!-- If some of the sections do not apply, just remove them. -->

### Short description
There is currently no way (at least that I have found) to change the fillrule for the painterpath in the FillBetweenItem. being able to set it to winding would be very useful for certain cases.

### 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 as pg
from PySide2.QtWidgets import QApplication

win = pg.plot()
win.setWindowTitle('pyqtgraph example: FillBetweenItem')
win.setXRange(0, 1.5)
win.setYRange(0, 1.5)

x1=[0,1,1,0,0]
y1=[0,0,1,1,0]
x2=[0.5,1.5,1.5,0.5,0.5]
y2=[0.5,0.5,1.5,1.5,0.5]
curve1 = win.plot(x=x1, y=y1, pen='k')
curve2 = win.plot(x=x2, y=y2, pen='k')
brushes = [0.5, (100, 100, 255), 0.5]
fill = pg.FillBetweenItem(curve1, curve2,brush=(100,100,255))
win.addItem(fill)

## Start Qt event loop unless running in interactive mode or using pyside.
if __name__ == '__main__':
    QApplication.instance().exec_()
```

### Expected behavior
Fill in the overlap

### Real behavior
Hole in the middle.


### Tested environment(s)

 * PyQtGraph version: 0.12.1
 * Qt Python binding: PyQt5 5.15.4 Qt 5.15.2
 * Python version: 3.7.7
 * NumPy version: 1.20.2
 * Operating system: Windows 10
 * Installation method: PIP


Discussion:
Hello, we would be interested in working on this bug as part of our free software development course. Would it be ok and is it still relevant for the project to fix this bug?

We were able to reproduce this bug on ubuntu 22.04 using python 3.5.10 and pyqtgraph 0.11.1

Our next step is to test the bug on the current version of pyqtgraph.
We were able to reproduce this bug on ubuntu 22.04 using python 3.10.12 and the development version of pyqtgraph.

We obtained the following error 
line 21, in <module>
    QApplication.instance().exec_()
AttributeError: 'NoneType' object has no attribute 'exec_' 
 
We had to use the PySide6 library instead of PySide2 to avoid the error.
For the moment, we've extracted the intersection points and used them to create a new polygon to fill the empty space.
The only problem is that when you create a polygon from above, there's a margin around it, which sometimes creates bugs.

Resolving patch:
diff --git a/pyqtgraph/graphicsItems/FillBetweenItem.py b/pyqtgraph/graphicsItems/FillBetweenItem.py
--- a/pyqtgraph/graphicsItems/FillBetweenItem.py
+++ b/pyqtgraph/graphicsItems/FillBetweenItem.py
@@ -1,5 +1,7 @@
+from typing import Union
+
 from .. import functions as fn
-from ..Qt import QtGui, QtWidgets
+from ..Qt import QtGui, QtWidgets, QtCore
 from .PlotCurveItem import PlotCurveItem
 from .PlotDataItem import PlotDataItem
 
@@ -9,33 +11,104 @@ class FillBetweenItem(QtWidgets.QGraphicsPathItem):
     """
     GraphicsItem filling the space between two PlotDataItems.
     """
-    def __init__(self, curve1=None, curve2=None, brush=None, pen=None):
-        QtWidgets.QGraphicsPathItem.__init__(self)
+    def __init__(
+        self,
+        curve1: Union[PlotDataItem, PlotCurveItem],
+        curve2: Union[PlotDataItem, PlotCurveItem],
+        brush=None,
+        pen=None,
+        fillRule: QtCore.Qt.FillRule=QtCore.Qt.FillRule.OddEvenFill
+    ):
+        """FillBetweenItem fills a region between two curves with a specified
+        :class:`~QtGui.QBrush`. 
+
+        Parameters
+        ----------
+        curve1 : :class:`~pyqtgraph.PlotDataItem` | :class:`~pyqtgraph.PlotCurveItem`
+            Line to draw fill from
+        curve2 : :class:`~pyqtgraph.PlotDataItem` | :class:`~pyqtgraph.PlotCurveItem`
+            Line to draw fill to
+        brush : color_like, optional
+            Arguments accepted by :func:`~pyqtgraph.mkBrush`, used
+            to create the :class:`~QtGui.QBrush` instance used to draw the item
+            by default None
+        pen : color_like, optional
+            Arguments accepted by :func:`~pyqtgraph.mkColor`, used
+            to create the :class:`~QtGui.QPen` instance used to draw the item
+            by default ``None``
+        fillRule : QtCore.Qt.FillRule, optional
+            FillRule to be applied to the underlying :class:`~QtGui.QPainterPath`
+            instance, by default ``QtCore.Qt.FillRule.OddEvenFill``
+
+        Raises
+        ------
+        ValueError
+            Raised when ``None`` is passed in as either ``curve1``
+            or ``curve2``
+        TypeError
+            Raised when either ``curve1`` or ``curve2`` is not either
+            :class:`~pyqtgraph.PlotDataItem` or :class:`~pyqtgraph.PlotCurveItem`
+        """
+        super().__init__()
         self.curves = None
+        self._fillRule = fillRule
         if curve1 is not None and curve2 is not None:
             self.setCurves(curve1, curve2)
         elif curve1 is not None or curve2 is not None:
-            raise Exception("Must specify two curves to fill between.")
+            raise ValueError("Must specify two curves to fill between.")
 
         if brush is not None:
             self.setBrush(brush)
         self.setPen(pen)
         self.updatePath()
+
+    def fillRule(self):
+        return self._fillRule
+
+    def setFillRule(self, fillRule: QtCore.Qt.FillRule=QtCore.Qt.FillRule.OddEvenFill):
+        """Set the underlying :class:`~QtGui.QPainterPath` to the specified 
+        :class:`~QtCore.Qt.FillRule`
+
+        This can be useful for allowing in the filling of voids.
+
+        Parameters
+        ----------
+        fillRule : QtCore.Qt.FillRule
+            A member of the :class:`~QtCore.Qt.FillRule` enum
+        """
+        self._fillRule = fillRule
+        self.updatePath()
         
     def setBrush(self, *args, **kwds):
-        """Change the fill brush. Accepts the same arguments as pg.mkBrush()"""
+        """Change the fill brush. Accepts the same arguments as :func:`~pyqtgraph.mkBrush`
+        """
         QtWidgets.QGraphicsPathItem.setBrush(self, fn.mkBrush(*args, **kwds))
         
     def setPen(self, *args, **kwds):
+        """Change the fill pen. Accepts the same arguments as :func:`~pyqtgraph.mkColor`
+        """
         QtWidgets.QGraphicsPathItem.setPen(self, fn.mkPen(*args, **kwds))
 
-    def setCurves(self, curve1, curve2):
-        """Set the curves to fill between.
-        
-        Arguments must be instances of PlotDataItem or PlotCurveItem.
-        
-        Added in version 0.9.9
-        """
+    def setCurves(
+        self,
+        curve1: Union[PlotDataItem, PlotCurveItem],
+        curve2: Union[PlotDataItem, PlotCurveItem]
+    ):
+        """Method to set the Curves to draw the FillBetweenItem between
+
+        Parameters
+        ----------
+        curve1 : :class:`~pyqtgraph.PlotDataItem` | :class:`~pyqtgraph.PlotCurveItem`
+            Line to draw fill from
+        curve2 : :class:`~pyqtgraph.PlotDataItem` | :class:`~pyqtgraph.PlotCurveItem`
+            Line to draw fill to
+    
+        Raises
+        ------
+        TypeError
+            Raised when input arguments are not either :class:`~pyqtgraph.PlotDataItem` or
+            :class:`~pyqtgraph.PlotCurveItem`
+        """        
         if self.curves is not None:
             for c in self.curves:
                 try:
@@ -45,7 +118,7 @@ def setCurves(self, curve1, curve2):
 
         curves = [curve1, curve2]
         for c in curves:
-            if not isinstance(c, PlotDataItem) and not isinstance(c, PlotCurveItem):
+            if not isinstance(c, (PlotDataItem, PlotCurveItem)):
                 raise TypeError("Curves must be PlotDataItem or PlotCurveItem.")
         self.curves = curves
         curve1.sigPlotChanged.connect(self.curveChanged)
@@ -55,6 +128,7 @@ def setCurves(self, curve1, curve2):
 
     def curveChanged(self):
         self.updatePath()
+    
     def updatePath(self):
         if self.curves is None:
             self.setPath(QtGui.QPainterPath())
@@ -67,19 +141,17 @@ def updatePath(self):
                 paths.append(c.getPath())
 
         path = QtGui.QPainterPath()
-        transform = QtGui.QTransform()
+        path.setFillRule(self.fillRule())   
 
-        ps1 = paths[0].toSubpathPolygons(transform)
-        ps2 = paths[1].toReversed().toSubpathPolygons(transform)
+        ps1 = paths[0].toSubpathPolygons()
+        ps2 = paths[1].toReversed().toSubpathPolygons()
         ps2.reverse()
 
         if len(ps1) == 0 or len(ps2) == 0:
             self.setPath(QtGui.QPainterPath())
             return
-        
+
         for p1, p2 in zip(ps1, ps2):
-            intersection = p1.intersected(p2)
-            if not intersection.isEmpty():
-                path.addPolygon(intersection)
-            path.addPolygon(p1 + p2)       
+            path.addPolygon(p1 + p2)
+
         self.setPath(path)


Test patch: