1
\$\begingroup\$

So I have a large application that I've been writing. One part of that application is responsible for displaying images taken from a camera and plotting two graphs.

I've successfully stripped out the entirety of the application that is not relevant to this portion of the application.

It works, what I'm doing with the graphs and images. However, I have concerns.

  1. paintEvent is being triggered at the main UI level, however, I need the portion of the graph to resize within the main UI. I attempted another paint event at the graphframe level, but it didn't work. So I created a new function that does all the work of a paint event, but it's triggered by the parent paint event.
  2. I was also thinking that maybe I should use pyqtgraph. One aspect of this code that I'm missing is the ability to track the x and y of where the mouse is on the graph.
  3. If you look at the Alignment class, you will notice this is where the layout is designed. My main application uses 4 or 5 different classes that will generate a different layout for each class. These layouts are being added to a QStackedWidget, so I can easily switch between the panels. As this graph frame will be used on every one of the panels, this presents an issue, as a single instance can not be present on each of the panels. So I need to find a new approach, which just means I'm probably going to have to ditch the QStackwidget and just generate a new layout each time.

While those are three concerns I have I'm not looking for resolutions, I want to post and get some feedback on the current state. I mention the concerns only to give you context as to what I'm currently working on. Maybe some feedback will push me to switch to pyqtgraph or stay with matplotlib. Any feedback is appreciated.

I rearranged all the code to run on a single file, with only what is important for the portion of work it's related to.

import sys
import cv2
import numpy as np
# from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qtagg import FigureCanvas
from matplotlib.figure import Figure
from PyQt6.QtCore import QRect, Qt
from PyQt6.QtGui import QColor, QImage, QLinearGradient, QPainter, QPixmap
from PyQt6.QtWidgets import (
 QApplication,
 QFrame,
 QGridLayout,
 QHBoxLayout,
 QLabel,
 QMainWindow,
 QSizePolicy,
 QSplitter,
 QStackedWidget,
 QStyleFactory,
 QTabWidget,
 QVBoxLayout,
 QWidget,
)
# matplotlib.use("QtAgg")
LIGHT_BLUE = QColor(0, 102, 153)
WHITE = QColor(255, 255, 255)
def layout_generator(*args, layout=None) -> QHBoxLayout:
 """
 Generates a layout of the specified type and adds the given arguments to it.
 Args:
 layout_type: The type of layout to generate (e.g., QHBoxLayout, QVBoxLayout, QGridLayout).
 *args: Variable number of arguments to add to the layout. Arguments can be: integers (for spacing),
 "*", which adds a stretch
 layout Objects
 QWidget objects.
 Returns:
 The generated layout with the added arguments.
 """
 if layout is None:
 return
 for arg in args:
 if isinstance(arg, int):
 layout.addSpacing(arg)
 elif arg == "*":
 layout.addStretch(1)
 elif (
 type(arg) is QHBoxLayout
 or type(arg) is QVBoxLayout
 or type(arg) is QGridLayout
 ):
 layout.addLayout(arg)
 else:
 layout.addWidget(arg)
 return layout
class GraphFrame:
 """_summary_"""
 def __init__(self):
 """_summary_
 Returns:
 _type_: _description_
 """
 self.width = 425
 self.height = 350
 self.delta = 30
 self.frame = QFrame()
 self.graphs = dict(fft=None, fvb=None)
 self.axis = dict(fft=None, fvb=None)
 self.canvas = dict(fft=None, fvb=None)
 self.Display_Tab_Widget = QTabWidget()
 self.current_image = np.random.randint(0.0, 1.0, size=(10, 10))
 self._image()
 self._fvb()
 self._fft()
 self.Display_Tab_Widget.setCurrentIndex(1)
 self.create_plot(graph="fvb", x_label="Pixels", layout=self.mpl_vlayout)
 self.create_plot(
 graph="fft", x_label="Raman shift (cm-1)", layout=self.mpl_ft_layout
 )
 layout_generator(
 self.Display_Tab_Widget, layout=QVBoxLayout(self.frame)
 )
 def get_graph_frame(self):
 return self.frame
 def _image(self):
 self.cameraImageDisplay = QWidget(self.Display_Tab_Widget)
 self.cameraImageDisplay.setAutoFillBackground(True)
 self.cameraDisplay = QLabel(self.cameraImageDisplay)
 self.cameraDisplay.setText(" ")
 self.cameraDisplay.setStyleSheet("border: 0px solid black")
 self.cameraDisplay.setAutoFillBackground(False)
 self.cameraDisplay.setAlignment(Qt.AlignmentFlag.AlignCenter) # type: ignore
 self.Display_Tab_Widget.addTab(self.cameraImageDisplay, "")
 self.Display_Tab_Widget.setTabText(
 self.Display_Tab_Widget.indexOf(self.cameraImageDisplay),
 "Image",
 )
 def _fvb(self):
 self.cameraFVBDisplay = QWidget(self.Display_Tab_Widget)
 self.layoutWidget2 = QWidget(self.cameraFVBDisplay)
 self.mpl_vlayout = QVBoxLayout(self.layoutWidget2)
 self.cameraFVBPlot = QWidget(self.layoutWidget2)
 sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) # type: ignore
 sizePolicy.setHorizontalStretch(0)
 sizePolicy.setVerticalStretch(1)
 sizePolicy.setHeightForWidth(
 self.cameraFVBPlot.sizePolicy().hasHeightForWidth()
 )
 self.mpl_vlayout.addWidget(self.cameraFVBPlot)
 self.Display_Tab_Widget.addTab(self.cameraFVBDisplay, "")
 self.Display_Tab_Widget.setTabText(
 self.Display_Tab_Widget.indexOf(self.cameraFVBDisplay),
 "FVB Display",
 )
 def _fft(self):
 self.FFTDisplay = QWidget(self.Display_Tab_Widget)
 self.layoutWidget_2 = QWidget(self.FFTDisplay)
 self.mpl_ft_layout = QVBoxLayout(self.layoutWidget_2)
 self.FTPlot = QWidget(self.layoutWidget_2)
 sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) # type: ignore
 sizePolicy.setHorizontalStretch(0)
 sizePolicy.setVerticalStretch(1)
 sizePolicy.setHeightForWidth(
 self.FTPlot.sizePolicy().hasHeightForWidth()
 )
 self.mpl_ft_layout.addWidget(self.FTPlot)
 self.Display_Tab_Widget.addTab(self.FFTDisplay, "")
 self.Display_Tab_Widget.setTabText(
 self.Display_Tab_Widget.indexOf(self.FFTDisplay), "FFT Display"
 )
 def create_plot(self, graph, x_label, layout, y_label="Counts"):
 self.graphs[graph] = Figure(figsize=(6, 6), dpi=100, tight_layout=True)
 self.axis[graph] = self.graphs[graph].add_subplot(111)
 for axis in ["top", "bottom", "left", "right"]:
 self.axis[graph].spines[axis].set_linewidth(1.5)
 self.axis[graph].grid(axis="both", which="major", linestyle=":")
 self.axis[graph].set_xlabel(x_label, fontsize=12, fontfamily="arial")
 self.axis[graph].set_ylabel(y_label, fontsize=12, fontfamily="arial")
 self.axis[graph].minorticks_on()
 self.axis[graph].tick_params(
 axis="both",
 which="minor",
 direction="in",
 width=0.72,
 length=2.16,
 right=True,
 top=True,
 )
 self.axis[graph].tick_params(
 axis="both",
 which="major",
 direction="in",
 width=1.224,
 length=3.6,
 right=True,
 top=True,
 )
 self.canvas[graph] = FigureCanvas(self.graphs.get(graph))
 layout.addWidget(self.canvas.get(graph))
 self.canvas[graph].draw()
 layout.setAlignment(Qt.AlignmentFlag.AlignVCenter)
 def update_fft(self, frame, slope, intercept):
 ft_y = frame
 ft_x = np.arange(len(frame))
 self.x_axis_plot = (ft_x * slope) + intercept
 self.axis["fft"].cla()
 self.axis["fft"].plot(self.x_axis_plot, ft_y)
 self.axis["fft"].set_xlim(250, 2500)
 self.axis["fft"].grid(axis="both", which="major", linestyle=":")
 self.axis["fft"].set_xlabel(
 "Raman shift (cm-1)", fontsize=12, fontfamily="arial"
 )
 self.axis["fft"].set_ylabel("Counts", fontsize=12, fontfamily="arial")
 self.graphs["fft"].canvas.draw_idle() # actually draw the new content
 def update_fvb(self, frame, x_start, x_stop):
 graph = "fvb"
 fvb_y = frame
 fvb_x = np.array(range(1024))
 self.axis[graph].cla() # clear the axes content
 self.axis[graph].plot(fvb_x, fvb_y) # plot new data
 self.axis[graph].set_xlim(x_start, x_stop)
 self.axis[graph].set_ylim(frame.min(), frame.max())
 self.axis[graph].grid(axis="both", which="major", linestyle=":")
 self.axis[graph].set_xlabel("Pixel", fontsize=12, fontfamily="arial")
 self.axis[graph].set_ylabel("Counts", fontsize=12, fontfamily="arial")
 self.graphs[graph].canvas.draw_idle() # actually draw the new content
 def set_image(self, image=None):
 if image is not None:
 self.current_image = image
 rot_img = np.rot90(self.current_image, k=1)
 # rot_img = np.rot90(image, k=1, axes=(1, 0))
 img_array = np.array([rot_img, rot_img, rot_img]).T.astype(float)
 img = img_array - img_array.min()
 if img.max() != 0:
 img /= img.max()
 else:
 img = np.zeros_like(img)
 img = cv2.resize(img, (self.cube, self.cube))
 img[np.isnan(img)] = 0
 img = (img * 255).astype(np.uint8)
 if len(img.shape) == 2:
 frame = QImage(
 img.repeat(1),
 img.shape[1],
 img.shape[0],
 QImage.Format.Format_Grayscale8,
 )
 elif len(img.shape) == 3:
 frame = QImage(
 img.repeat(1),
 img.shape[1],
 img.shape[0],
 img.shape[2] * img.shape[1],
 QImage.Format.Format_RGB888,
 )
 display_frame = QPixmap(frame)
 self.cameraDisplay.setPixmap(display_frame)
 def trigger_paint_event(self, event) -> None:
 width = self.frame.rect().width()
 height = self.frame.rect().height()
 self.cube = min(width, height)
 left = int((width - self.cube) / 2)
 right = int((height - self.cube) / 2)
 self.cameraDisplay.setGeometry(QRect(left, right, self.cube, self.cube))
 self.set_image()
 self.layoutWidget2.setGeometry(
 QRect(
 0,
 0,
 width - 10,
 height - 10,
 )
 )
 self.layoutWidget_2.setGeometry(
 QRect(
 0,
 0,
 width - 10,
 height - 10,
 )
 )
class UI(QWidget, GraphFrame):
 def __init__(self, parent=None):
 QWidget.__init__(self)
 GraphFrame.__init__(self)
 self.parent = parent
 self.init_main_panel()
 self.parent.showFullScreen()
 # self.parent.show()
 self.parent.showMaximized()
 def init_main_panel(self) -> None:
 # self.parent.setWindowFlags(Qt.WindowType.FramelessWindowHint)
 self.parent.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
 self.parent.setAttribute(Qt.WidgetAttribute.WA_AcceptTouchEvents, True)
 self.build_panel()
 def build_panel(self) -> None:
 main_layout = QVBoxLayout()
 self.widget_stack = self.gen_stacked_widgets()
 main_layout.addWidget(self.widget_stack)
 self.setLayout(main_layout)
 def get_screen_resolution(self):
 return self.parent.app.screens()[0].availableGeometry()
 def gen_stacked_widgets(self) -> QStackedWidget:
 widgets = QStackedWidget()
 widgets.addWidget(
 AlignmentWidget(
 graph=self.get_graph_frame(),
 )
 )
 return widgets
 def paintEvent(self, event) -> None:
 painter = QPainter(self)
 grad1 = QLinearGradient(0, 0, 0, int(event.rect().height() * 0.22))
 grad1.setColorAt(0.0, WHITE)
 grad1.setColorAt(1, LIGHT_BLUE)
 painter.fillRect(event.rect(), grad1)
 self.trigger_paint_event(self)
 def display_stack(self, idx: int) -> None:
 self.widget_stack.setCurrentIndex(idx)
class AlignmentWidget(QWidget):
 def __init__(self, graph, parent=None):
 QWidget.__init__(self)
 self.parent = parent
 self._init_panel(graph)
 def _init_panel(self, graph):
 """Initialize the user interface."""
 self._build_panel(graph)
 def _build_panel(self, graph) -> None:
 layout = QHBoxLayout()
 vertical_splitter = QSplitter(Qt.Orientation.Vertical)
 vertical_splitter.setStyleSheet("QSplitter::handle {image: none;}")
 horizontal_splitter = QSplitter(Qt.Orientation.Horizontal)
 horizontal_splitter.setStyleSheet("QSplitter::handle {image: none;}")
 vertical_splitter.addWidget(graph)
 vertical_splitter.setStretchFactor(0, 1)
 horizontal_splitter.addWidget(vertical_splitter)
 horizontal_splitter.setStretchFactor(0, 1)
 layout.addWidget(horizontal_splitter)
 self.setLayout(layout)
class AppName(QMainWindow):
 def __init__(self):
 QMainWindow.__init__(self)
 self.gui = None
 self.init_gui()
 def init_gui(self) -> None:
 self.gui = UI(self)
 self.setCentralWidget(self.gui)
 self.setStyle(QStyleFactory.create("Cleanlooks"))
def main():
 app = QApplication(sys.argv)
 _app = AppName()
 app.exec()
 sys.exit(0)
if __name__ == "__main__":
 main()
Sᴀᴍ Onᴇᴌᴀ
29.5k16 gold badges45 silver badges201 bronze badges
asked Mar 8, 2024 at 18:00
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

The manifest color constants such as LIGHT_BLUE are very nice, thank you.

isinstance

Clearly this works:

 elif (
 type(arg) is QHBoxLayout
 or type(arg) is QVBoxLayout
 or type(arg) is QGridLayout
 ):

It would be better phrased as

 elif isinstance(arg, (QHBoxLayout, QVBoxLayout, QGridLayout)):

We prefer to ask about "inherits from?" rather than "is exactly equal?".

design of Public API

def layout_generator(*args, layout=None) -> QHBoxLayout: 
 ...
 if layout is None:
 return

Prefer verbs when naming functions. Here, perhaps generate_layout.

The docstring is helpful; I thank you for it. It is perhaps a bit wordy, and could be pruned a little while still conveying the same information. No need to restate what the signature has already eloquently told us.

wrong return value

 if layout is None:
 return

You promised to return a QHBoxLayout. Always. But that doesn't happen here.

That return makes it look like we only evaluated the function for side effects. Prefer an explicit return None in functions which caller expects to get something back from.

It feels like caller should be responsible for passing in a valid layout, or should not call this at all if we only have None. Consider deleting this if statement. Consider removing the =None default value from the signature, to make layout a mandatory parameter.

return

 _app = AppName()
 app.exec()
 sys.exit(0)

It looks like we could simply invoke AppName(), without assigning it to _app.

We evaluate app.exec() for side effects. Tearing down the process with sys.exit() is more than what's needed. Prefer to simply return from main() once .exec() has finished.

optional parent

class AlignmentWidget(QWidget):
 def __init__(self, graph, parent=None):

I fail to understand what's going on with self.parent, given that the OP code never references it. Maybe it's an artifact of trimming the original codebase down to a conveniently small subset for review.

In any event, the optional None aspect doesn't seem to be helpful. Better to require that caller always supply a parent, or else remove it altogether.

unhelpful docstrings

class GraphFrame:
 """_summary_"""
 def __init__(self):
 """_summary_
 Returns:
 _type_: _description_
 """

I have no idea what you're trying to convey to the Gentle Reader. Recommend you elide both docstrings.

If we're going with an optional type annotation, it's customary to either write def __init__(self) -> None:, or to assume that both humans and mypy understand what a ctor does.

lint

 self.cameraImageDisplay = QWidget(self.Display_Tab_Widget)

Pep-8 asks that you name it self.camera_image_display.

Similar remarks for layoutWidget2, Display_Tab_Widget, paintEvent, and so on.

I have no idea what the "fvb" in cameraFVBPlot denotes. It merits a # comment. Better: give the _fvb() helper a """docstring""".

answered Mar 10, 2024 at 1:24
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.