# -*- coding: utf-8 -*-
#
# Copyright Université Rennes 1 / INSERM
# Contributor: Raphael Weber
#
# Under CeCILL license
# http://www.cecill.info
"""
Module with sub-classes and functions for GUI creation with PyQt5
See https://pypi.org/project/PyQt5 and
https://doc.qt.io/qt-5/reference-overview.html
"""
from PyQt5 import QtWidgets, QtGui, QtCore
from PyQt5.QtWidgets import QFileDialog
from sys import flags, argv
# *************************************************************************** #
# ************************* QtWidgets subclasses **************************** #
# *************************************************************************** #
[docs]class ComboBox(QtWidgets.QComboBox):
"""
Subclass of QtWidgets.QComboBox so that key press events are ignored
"""
[docs] def keyPressEvent(self, ev):
"""
Re-implemented so that key press events are ignored
"""
pass
# *************************************************************************** #
# ******************************* Functions ********************************* #
# *************************************************************************** #
[docs]def initialize_gui():
"""
Creates a GUI application for display
:returns: instance of QtCore.QCoreApplication or QtWidgets.QApplication
"""
app = QtCore.QCoreApplication.instance()
if app is None:
app = QtWidgets.QApplication(argv)
return app
[docs]def infinite_loop_gui(app):
"""
Creates an event loop for a given GUI application, so that the windows
do not disappear right after being created
:param app: GUI application
:type app: QtCore.QCoreApplication or QtWidgets.QApplication
"""
if (flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
app.instance().exec_()
[docs]def infinite_loop_gui_parallel(app, win):
"""
Creates a specific event loop for a given window while an event loop is
already running, the specific event loop is stopped when the window is
closed
:param app: GUI application
:type app: QtCore.QCoreApplication or QtWidgets.QApplication
:param win: window on which the parallel event loop is applied
:type win: QtWidgets.QWidget
"""
while win.isVisible():
app.processEvents()
app.processEvents()
[docs]def get_directory_dialog(desc_text="Select directory", dir_root=None):
"""
Opens a dialog window in order to select a directory
:param desc_text: short description text to be displayed as the dialog
window title
:type desc_text: str
:param dir_root: initial directory where to go when launching the dialog
window, default current working directory
:type dir_root: str
"""
app = initialize_gui()
dir_selected = QFileDialog.getExistingDirectory(
None, desc_text, dir_root, QFileDialog.ShowDirsOnly
)
app.quit()
return dir_selected
[docs]def add_spin_box_table(grid, pos, nb_rows, nb_cols, name="", params={}):
"""
Adds a table of spin boxes to an instance of QtWidgets.QGridLayout
The spin boxes are contained in an instance of QtWidgets.QGroupBox filled
by an instance of QtWidgets.QGridLayout, see func:`.add_group_box`.
:param grid: parent layout where the spin boxes are added
:type grid: QtWidgets.QGridLayout
:param pos: position of the group box containing the spin boxes in the
parent layout, length 2 ``(row, col)`` or 4
``(row, col, rowspan, colspan)``
:type pos: tuple of integers
:param nb_rows: number of rows in the table of spin boxes
:type nb_rows: int
:param nb_cols: number of columns in the table of spin boxes
:type nb_cols: int
:param name: name of the group box containing the spin boxes
:type name: str
:param params: keyword arguments of the constructor of QtWidgets.QSpinBox
(e.g. {"minimum": 0, "maximum": 255}), it might be a nested list of
length ``nb_rows`` x ``nb_cols`` if the parameters differ from on spin
box to another
:type params: dict or list
:returns:
- **grid_sub** -- instance of QtWidgets.QGridLayout containing the
table of spin boxes
- **goup_box** -- instance of QtWidgets.QGroupBox containing the table
of spin boxes
"""
# create group box
grid_sub, group_box = add_group_box(grid, pos, name)
# add spin boxes in the group box
for i in range(nb_rows):
for j in range(nb_cols):
if isinstance(params, list):
spin_box = QtWidgets.QSpinBox(**params[i][j])
else:
spin_box = QtWidgets.QSpinBox(**params)
grid_sub.addWidget(spin_box, i, j)
return grid_sub, group_box
[docs]def set_spin_box_table(grid, value_list_list, flag_display):
"""
Sets the values of a table of spin boxes from an input list of lists
or returns the values of a table of spin boxes
:param grid: layout containing only the table of spin boxes, see
:func:`.add_spin_box_table` for creating one
:type grid: QtWidgets.QGridLayout
:param value_list_list: in case of ``flag_display`` set to ``True``, each
element is a list of values for setting the table of spin boxes --
otherwise, let it be an empty list
:type value_list_list: list
:param flag_display: specify if displaying values in the table of spin
boxes
:type flag_display: bool
:returns: each element is a list containing the values in the table of spin
boxes (same as input argument ``value_list_list`` in case of
``flag_display`` set to ``True``)
:rtype: list
"""
# if setting value_list_list, reset its value
if not flag_display:
value_list_list = []
# get number of columns in the grid
nb_cols = grid.columnCount()
# get number of rows in the grid
nb_rows = grid.rowCount()
# loop on the number of rows
for i in range(nb_rows):
# initialize temporary list for setting value_list_list
list_tmp = []
# loop on the number of columns
for j in range(nb_cols):
# get spin box
spin_box = grid.itemAtPosition(i, j).widget()
# if setting display, check length of value_list_list
if flag_display:
if i < len(value_list_list):
# set spin box value
spin_box.setValue(value_list_list[i][j])
else:
# set spin box value
spin_box.setValue(0)
# if setting value_list_list
else:
# append temporary list
list_tmp.append(spin_box.value())
if not flag_display:
value_list_list.append(list_tmp)
return value_list_list
[docs]def add_line_edit_list(grid, pos, nb_rows, name=""):
"""
Adds a list of line edits to a layout
The line edits are contained in an instance of QtWidgets.QGroupBox filled
by an instance of QtWidgets.QGridLayout, see
:func:`.add_group_box`.
:param grid: parent layout where the line edits are added
:type grid: QtWidgets.QGridLayout
:param pos: position of the group box containing the spin boxes in the
parent layout, length 2 ``(row, col)`` or 4
``(row, col, rowspan, colspan)``
:type pos: tuple of integers
:param nb_rows: number of rows in the list of line edits
:type nb_rows: int
:param name: name of the group box containing the spin boxes
:type name: str
:returns:
- **grid_sub** -- instance of QtWidgets.QGridLayout containing the
instances of QLineEdit
- **goup_box** -- instance of QtWidgets.QGroupBox containing the
instances of QLineEdit
"""
# create group box
grid_sub, group_box = add_group_box(grid, pos, name)
# add spin boxes in the group box
for i in range(nb_rows):
edit = QtWidgets.QLineEdit()
grid_sub.addWidget(edit, i, 0)
return grid_sub, group_box
[docs]def set_line_edit_list(grid, value_list, flag_display):
"""
Sets the values of a list of lines edits from an input list or returns the
values of a table of lines edits
:param grid: layout containing only the list of lines edits, see
:func:`.add_line_edit_list` for creating one
:type grid: QtWidgets.QGridLayout
:param value_list: in case of ``flag_display`` set to ``True``, each
element is a string for setting the list of lines edits --
otherwise, let it be an empty list
:type value_list: list
:param flag_display: specify if displaying values in the list of lines
edits
:type flag_display: bool
:returns: list of values contained in the list of lines edits
(same as input argument ``value_list`` in case of ``flag_display`` set
to ``True``)
:rtype: list
"""
# if setting value_list, reset its value
if not flag_display:
value_list = []
# get number of rows in the grid <=> number of items in the list
nb_rows = grid.count()
# loop on the number of rows
for i in range(nb_rows):
# get edit text
edit = grid.itemAtPosition(i, 0).widget()
if flag_display:
if i < len(value_list):
# set edit text
edit.setText(value_list[i])
else:
# set edit text
edit.setText("")
else:
# append value list
value_list.append(edit.text())
return value_list
[docs]def add_group_box(layout, position, title=""):
"""
Adds a group box to a grid layout
Another grid layout is created to fill the group box.
The function raises an error if the length of ``position`` is not 2 or 4.
:param layout: parent layout where the group box is added
:type layout: QtWidgets.QGridLayout
:param position: position of the group box in the parent layout, length 2
``(row, col)`` or 4 ``(row, col, rowspan, colspan)``
:type position: tuple of integers
:param title: group box title
:type title: str
:returns:
- **grid** (*QtWidgets.QGridLayout*) -- layout filling the group box
- **group_box** (*QtWidgets.QGroupBox*)
"""
# create the group box
group_box = QtWidgets.QGroupBox(title)
# create grid layout and set it to the group box
grid = QtWidgets.QGridLayout(group_box)
# add the group box to the layout
add_widget_to_layout(layout, group_box, position)
return grid, group_box
[docs]def add_check_box(layout, position, text, flag_checked=False, color=None):
"""
Adds a check box to a grid layout
The method raises an error if the length of position is not 2 or 4.
:param layout: parent layout where the check box is added
:type layout: QtWidgets.QGridLayout
:param position: position of the check box in the parent layout, length 2
``(row, col)`` or 4 ``(row, col, rowspan, colspan)``
:type position: tuple of integers
:param text: text displayed next to the check box
:type text: str
:param flag_checked: specify if check box is initially checked
:type flag_checked: bool
:param color: color in RGB format
:type color: tuple
:returns: check box item
:rtype: QtWidgets.QCheckBox
"""
# create radio button
check_box = QtWidgets.QCheckBox(text)
# set color
if color is not None:
check_box.setStyleSheet("QCheckBox { color: rgb(%d,%d,%d) }" % color)
# set checked
if flag_checked:
check_box.setChecked(True)
# add radio button to the layout
add_widget_to_layout(layout, check_box, position)
return check_box
[docs]def create_window(
size=(0, 0), title=None, bg_color=(240, 240, 240), flag_show=True
):
"""
Creates a window
The geometry of the window can be updated afterward, see
http://doc.qt.io/qt-4.8/application-windows.html#window-geometry for
details.
If pyqtgraph items are to be added in the window, the background color
won't apply to them. To get the same background color for pyqtgraph items,
the following line must be added:
``pyqtgraph.setConfigOption('background',color)``.
:param size: size of the window in pixels, length 2 ``(width, height)``,
set one value to 0 in order to have the window maximized in the
corresponding direction
:type size: tuple
:param title: window title
:type title: str
:param bg_color: background color of the window, either (RGB) or (RGBA)
:type bg_color: tuple or list
:param flag_show: specify if the window must be displayed
:type flag_show: bool
:returns:
- **win** (*QtWidgets.QWidget*) -- window container
- **layout** (*QtWidgets.QGridLayout*) -- layout filling the window
"""
# create the window and the layout to fill it
win = QtWidgets.QWidget()
layout = QtWidgets.QGridLayout(win)
# set the background color
if len(bg_color) == 3:
color = QtGui.QColor(bg_color[0], bg_color[1], bg_color[2])
elif len(bg_color) == 4:
color = QtGui.QColor(
bg_color[0], bg_color[1], bg_color[2], bg_color[3]
)
palette = QtGui.QPalette()
palette.setColor(QtGui.QPalette.Window, color)
win.setPalette(palette)
win.setAutoFillBackground(True)
# set the window title
if isinstance(title, str):
win.setWindowTitle(title)
if 0 in size:
# set the window size when minimized
screen_resolution = QtWidgets.QApplication.desktop().screenGeometry()
win.resize(
screen_resolution.width() - 80, screen_resolution.height() - 200
)
# maximize window by default
win.showMaximized()
else:
win.resize(size[0], size[1])
# show the window
if flag_show:
win.show()
return win, layout
[docs]def add_combo_box(
lay, widget_position, item_list, box_title=None,
flag_enable_key_interaction=True
):
"""
Creates a group box with a combo box and adds it to a grid layout
:param lay: parent layout where the group box is added
:type lay: QtWidgets.QGridLayout
:param widget_position: position of the group box in the parent layout,
length 2 ``(row, col)`` or 4 ``(row, col, rowspan, colspan)``
:type widget_position: tuple of integers
:param item_list: items of the combo box, each element is a string
:type item_list: list
:param box_title: title of the group box containing the combo box
:type box_title: str
:param flag_enable_key_interaction: specify if key press interaction is
enabled
:type flag_enable_key_interaction: bool
:returns:
- **grid** (*QtWidgets.QGridLayout*) -- layout filling the group box
- **group_box** (*QtWidgets.QGroupBox*) -- widget added to the parent
layout
- **combo_box** (:class:`.ComboBox` or *QtWidgets.QComboBox*)
- combo box item
"""
# create combo box
if flag_enable_key_interaction:
combo_box = QtWidgets.QComboBox()
else:
combo_box = ComboBox()
# add items
combo_box.addItems(item_list)
# check if group box must be created
if box_title is not None:
# create group box
grid, group_box = add_group_box(lay, widget_position, box_title)
# add combo box to group box
grid.addWidget(combo_box, 0, 0)
else:
grid, group_box = None, None
# add combo box to layout
add_widget_to_layout(lay, combo_box, widget_position)
return grid, group_box, combo_box
[docs]def set_style_sheet(app, font_name, font_size, font_color, qobj_list):
"""
Sets the style sheet for a list of Qt classes in a QApplication
:param app: GUI application in which the style sheet must be set
:type app: QtCore.QCoreApplication or QtWidgets.QApplication
:param font_name: font name
:type font_name: str
:param font_size: size of the font in pt
:type font_size: int or float
:param font_color: color (RGB) of the font
:type font_color: tuple or list
:param qobj_list: each element is a string of the Qt class on which the
style sheet is applied
:type qobj_list: list
"""
# get style sheet string
style_string = "color: rgb(%d,%d,%d); font: %s; font-size: %dpt" % (
font_color[0], font_color[1], font_color[2], font_name, font_size
)
style_sheet_string = ""
for qobj in qobj_list:
style_sheet_string += "%s {%s} " % (qobj, style_string)
# set style sheet
app.setStyleSheet(style_sheet_string)