diff --git a/README.md b/README.md index a3baa69..8d35959 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # SigVisualizer -SigVisualizer is a PyQt5 GUI application that visualizes electroencephalogram signals streamed from [__lab streaming layer__ (LSL)](https://github.com/sccn/labstreaminglayer "LabStreamingLayer (LSL)") in real time. +SigVisualizer is a Qt GUI application that visualizes electroencephalogram signals streamed from [__lab streaming layer__ (LSL)](https://github.com/sccn/labstreaminglayer "LabStreamingLayer (LSL)") in real time. ## Requirements -* Python 3.x -* PyQt5 +* Python 3.9+ +* One of: PyQt5, PyQt6, PySide2, PySide6 -![image](SigVisualizer_demo.gif) +![image](docs/SigVisualizer_demo.gif) diff --git a/createUI.bat b/createUI.bat deleted file mode 100644 index fd22c46..0000000 --- a/createUI.bat +++ /dev/null @@ -1 +0,0 @@ -"C:\Users\Yida Lin\AppData\Local\Programs\Python\Python37\Scripts\pyuic5.exe" -x ui_sigvisualizer.ui -o ui_sigvisualizer.py \ No newline at end of file diff --git a/SigVisualizer_demo.gif b/docs/SigVisualizer_demo.gif similarity index 100% rename from SigVisualizer_demo.gif rename to docs/SigVisualizer_demo.gif diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..678b331 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,36 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "sigvisualizer" +version = "0.1.0" +description = "Real-time Qt GUI for visualizing EEG signals streamed via Lab Streaming Layer (LSL)" +readme = "README.md" +license = { text = "GPL-3.0-or-later" } +requires-python = ">=3.9" +dependencies = [ + "qtpy>=2.4", + "pylsl", +] + +[project.optional-dependencies] +pyqt5 = ["PyQt5"] +pyqt6 = ["PyQt6"] +pyside2 = ["PySide2"] +pyside6 = ["PySide6"] + +[project.urls] +Homepage = "https://github.com/labstreaminglayer/App-SigVisualizer" + +[project.scripts] +sigvisualizer = "sigvisualizer.app:main" + +[tool.setuptools] +package-dir = { "" = "src" } + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +sigvisualizer = ["resources/*.ico", "resources/*.png", "resources/*.ui", "resources/icons/*.svg"] diff --git a/LSL_sender.py b/scripts/LSL_sender.py similarity index 100% rename from LSL_sender.py rename to scripts/LSL_sender.py diff --git a/sigvisualizer.py b/sigvisualizer.py deleted file mode 100644 index e31062b..0000000 --- a/sigvisualizer.py +++ /dev/null @@ -1,82 +0,0 @@ -import sys - -from PyQt5.QtCore import QSize, Qt, pyqtSignal -from PyQt5.QtGui import QIcon -from PyQt5.QtWidgets import (QApplication, QMainWindow, QStatusBar, - QTreeWidgetItem) - -from ui_sigvisualizer import Ui_MainWindow - - -class SigVisualizer(QMainWindow): - stream_expanded = pyqtSignal(str) - - def __init__(self): - super().__init__() - self.ui = Ui_MainWindow() - self.ui.setupUi(self) - self.setWindowTitle('Real Time Signal Visualizer') - self.ui.treeWidget.setHeaderLabel('Streams') - self.setWindowIcon(QIcon('sigvisualizer.ico')) - - self.statusBar = QStatusBar() - self.setStatusBar(self.statusBar) - - self.ui.toggleButton.setIcon(QIcon("icons/chevron_left.svg")) - self.ui.toggleButton.setIconSize(QSize(30, 30)) - - self.ui.toggleButton.clicked.connect(self.toggle_panel) - self.ui.updateButton.clicked.connect( - self.ui.widget.dataTr.update_streams) - self.ui.widget.dataTr.updateStreamNames.connect( - self.update_metadata_widget) - self.panelHidden = False - - self.ui.treeWidget.itemExpanded.connect(self.tree_item_expanded) - self.stream_expanded.connect(self.ui.widget.dataTr.handle_stream_expanded) - - def tree_item_expanded(self, widget_item): - name = widget_item.text(0) - for it_ix in range(self.ui.treeWidget.topLevelItemCount()): - item = self.ui.treeWidget.topLevelItem(it_ix) - if item.text(0) != name: - item.setExpanded(False) - self.stream_expanded.emit(name) - - def update_metadata_widget(self, metadata, default_idx): - for s_ix, s_meta in enumerate(metadata): - item = QTreeWidgetItem(self.ui.treeWidget) - item.setText(0, s_meta["name"]) - - for m in range(s_meta["ch_count"]): - channel_item = QTreeWidgetItem(item) - channel_item.setText(0, 'Channel {}'.format(m+1)) - channel_item.setCheckState(0, Qt.Checked) - - item.setExpanded(True if s_ix == default_idx else False) - self.ui.treeWidget.addTopLevelItem(item) - - self.ui.treeWidget.setAnimated(True) - self.statusBar.showMessage( - "Sampling rate: {}Hz".format(metadata[default_idx]["srate"])) - - def toggle_panel(self): - if self.panelHidden: - self.panelHidden = False - self.ui.treeWidget.show() - self.ui.updateButton.show() - self.ui.toggleButton.setIcon(QIcon("icons/chevron_left.svg")) - self.ui.toggleButton.setIconSize(QSize(30, 30)) - else: - self.panelHidden = True - self.ui.treeWidget.hide() - self.ui.updateButton.hide() - self.ui.toggleButton.setIcon(QIcon("icons/chevron_right.svg")) - self.ui.toggleButton.setIconSize(QSize(30, 30)) - - -if __name__ == '__main__': - app = QApplication(sys.argv) - window = SigVisualizer() - window.show() - sys.exit(app.exec_()) diff --git a/src/sigvisualizer/__init__.py b/src/sigvisualizer/__init__.py new file mode 100644 index 0000000..26ceeab --- /dev/null +++ b/src/sigvisualizer/__init__.py @@ -0,0 +1,6 @@ +from importlib.metadata import version, PackageNotFoundError + +try: + __version__ = version("sigvisualizer") +except PackageNotFoundError: + __version__ = "0.0.0" diff --git a/src/sigvisualizer/app.py b/src/sigvisualizer/app.py new file mode 100644 index 0000000..2c7aa01 --- /dev/null +++ b/src/sigvisualizer/app.py @@ -0,0 +1,118 @@ +import atexit +import sys +from contextlib import ExitStack +from importlib import resources + +from qtpy import uic +from qtpy.QtCore import QSize, Qt, Signal +from qtpy.QtGui import QIcon +from qtpy.QtWidgets import QApplication, QMainWindow, QStatusBar, QTreeWidgetItem + +from .paintwidget import PaintWidget + +_resource_stack = ExitStack() +atexit.register(_resource_stack.close) + + +def _resource_path(*parts): + ref = resources.files("sigvisualizer").joinpath("resources", *parts) + return str(_resource_stack.enter_context(resources.as_file(ref))) + + +class SigVisualizer(QMainWindow): + stream_expanded = Signal(str) + + def __init__(self): + super().__init__() + uic.loadUi(_resource_path("ui_sigvisualizer.ui"), self) + self._install_paint_widget() + + self.setWindowTitle('Real Time Signal Visualizer') + self.treeWidget.setHeaderLabel('Streams') + self.setWindowIcon(QIcon(_resource_path("sigvisualizer.ico"))) + + self.statusBar = QStatusBar() + self.setStatusBar(self.statusBar) + + self.toggleButton.setIcon(QIcon(_resource_path("icons", "chevron_left.svg"))) + self.toggleButton.setIconSize(QSize(30, 30)) + + self.toggleButton.clicked.connect(self.toggle_panel) + self.updateButton.clicked.connect(self.widget.dataTr.update_streams) + self.widget.dataTr.updateStreamNames.connect(self.update_metadata_widget) + self.panelHidden = False + + self.treeWidget.itemExpanded.connect(self.tree_item_expanded) + self.stream_expanded.connect(self.widget.dataTr.handle_stream_expanded) + + def closeEvent(self, event): + self.widget.dataTr.request_stop() + self.widget.dataTr.wait(2000) + super().closeEvent(event) + + def _install_paint_widget(self): + """Replace the plain QWidget placeholder loaded from the .ui file with a real + PaintWidget. Kept as a placeholder in the .ui file (rather than a Designer + "promoted widget") since promoted-widget resolution isn't portable across + PyQt5/PyQt6/PySide2/PySide6.""" + layout = self.centralwidget.layout() + index = layout.indexOf(self.widget) + row, col, rowspan, colspan = layout.getItemPosition(index) + layout.removeWidget(self.widget) + self.widget.deleteLater() + self.widget = PaintWidget(self.centralwidget) + self.widget.setObjectName("widget") + layout.addWidget(self.widget, row, col, rowspan, colspan) + + def tree_item_expanded(self, widget_item): + name = widget_item.text(0) + for it_ix in range(self.treeWidget.topLevelItemCount()): + item = self.treeWidget.topLevelItem(it_ix) + if item.text(0) != name: + item.setExpanded(False) + self.stream_expanded.emit(name) + + def update_metadata_widget(self, metadata, default_idx): + for s_ix, s_meta in enumerate(metadata): + item = QTreeWidgetItem(self.treeWidget) + item.setText(0, s_meta["name"]) + + for m in range(s_meta["ch_count"]): + channel_item = QTreeWidgetItem(item) + channel_item.setText(0, 'Channel {}'.format(m+1)) + channel_item.setCheckState(0, Qt.CheckState.Checked) + + item.setExpanded(True if s_ix == default_idx else False) + self.treeWidget.addTopLevelItem(item) + + self.treeWidget.setAnimated(True) + if default_idx >= 0: + self.statusBar.showMessage( + "Sampling rate: {}Hz".format(metadata[default_idx]["srate"])) + else: + self.statusBar.showMessage("No signal streams found") + + def toggle_panel(self): + if self.panelHidden: + self.panelHidden = False + self.treeWidget.show() + self.updateButton.show() + self.toggleButton.setIcon(QIcon(_resource_path("icons", "chevron_left.svg"))) + self.toggleButton.setIconSize(QSize(30, 30)) + else: + self.panelHidden = True + self.treeWidget.hide() + self.updateButton.hide() + self.toggleButton.setIcon(QIcon(_resource_path("icons", "chevron_right.svg"))) + self.toggleButton.setIconSize(QSize(30, 30)) + + +def main(): + app = QApplication(sys.argv) + window = SigVisualizer() + window.show() + sys.exit(app.exec()) + + +if __name__ == '__main__': + main() diff --git a/paintwidget.py b/src/sigvisualizer/paintwidget.py similarity index 62% rename from paintwidget.py rename to src/sigvisualizer/paintwidget.py index 10a5967..1820deb 100644 --- a/paintwidget.py +++ b/src/sigvisualizer/paintwidget.py @@ -1,6 +1,6 @@ -from PyQt5.QtCore import QThread, Qt, pyqtSignal -from PyQt5.QtGui import QPalette, QPainter, QPen -from PyQt5.QtWidgets import QWidget +from qtpy.QtCore import QThread, Qt, Signal +from qtpy.QtGui import QPalette, QPainter, QPen +from qtpy.QtWidgets import QWidget import pylsl import math import copy @@ -10,12 +10,11 @@ class DataThread(QThread): - updateStreamNames = pyqtSignal(list, int) - sendData = pyqtSignal(list, list, list, list) - changedStream = pyqtSignal() + updateStreamNames = Signal(list, int) + sendData = Signal(list, list, list, list) + changedStream = Signal() def_stream_parms = {'chunk_idx': 0, 'metadata': {}, 'srate': None, 'chunkSize': None, - 'downSampling': None, 'downSamplingFactor': None, 'downSamplingBuffer': None, 'inlet': None, 'stream_idx': None, 'is_marker': False} def __init__(self, parent): @@ -25,6 +24,10 @@ def __init__(self, parent): self.streams = [] self.stream_params = [] self.sig_strm_idx = -1 + self._stop_requested = False + + def request_stop(self): + self._stop_requested = True def handle_stream_expanded(self, name): stream_names = [_['metadata']['name'] for _ in self.stream_params] @@ -55,12 +58,7 @@ def update_streams(self): if self.sig_strm_idx < 0: self.sig_strm_idx = k srate = stream.nominal_srate() - stream_params['downSampling'] = srate > 1000 stream_params['chunkSize'] = round(srate / self.chunksPerScreen * self.seconds_per_screen) - if stream_params['downSampling']: - stream_params['downSamplingFactor'] = round(srate / 1000) - n_buff = round(stream_params['chunkSize'] / stream_params['downSamplingFactor']) - stream_params['downSamplingBuffer'] = [[0] * int(stream.channel_count())] * n_buff self.stream_params.append(stream_params) self.updateStreamNames.emit([_['metadata'] for _ in self.stream_params], self.sig_strm_idx) @@ -68,7 +66,7 @@ def update_streams(self): def run(self): if self.streams: - while True: + while not self._stop_requested: send_ts, send_data = [], [] if self.sig_strm_idx >= 0: params = self.stream_params[self.sig_strm_idx] @@ -77,13 +75,6 @@ def run(self): if params['chunkSize']: pull_kwargs['max_samples'] = params['chunkSize'] send_data, send_ts = inlet.pull_chunk(**pull_kwargs) - if send_ts and params['downSampling']: - for m in range(round(params['chunkSize'] / params['downSamplingFactor'])): - end_idx = min((m + 1) * params['downSamplingFactor'], len(send_data)) - for ch_idx in range(int(self.streams[self.sig_strm_idx].channel_count())): - buf = [send_data[n][ch_idx] for n in range(m * params['downSamplingFactor'], end_idx)] - params['downSamplingBuffer'][m][ch_idx] = sum(buf) / len(buf) - send_data = params['downSamplingBuffer'] send_mrk_ts, send_mrk_data = [], [] is_marker = [_['is_marker'] for _ in self.stream_params] if any(is_marker): @@ -102,41 +93,70 @@ class PaintWidget(QWidget): def __init__(self, widget): super().__init__() self.reset() - pal = QPalette() - pal.setColor(QPalette.Background, Qt.white) + pal = self.palette() self.setAutoFillBackground(True) + self.setBackgroundRole(QPalette.ColorRole.Base) self.setPalette(pal) self.dataTr = DataThread(self) self.dataTr.sendData.connect(self.get_data) self.dataTr.changedStream.connect(self.reset) + def _theme_colors(self): + pal = self.palette() + signal_color = pal.color(QPalette.ColorRole.Highlight) + marker_color = pal.color(QPalette.ColorRole.Accent) + if not marker_color.isValid(): + marker_color = signal_color + return { + 'signal': signal_color, + 'marker': marker_color, + } + def reset(self): - self.chunk_idx = 0 self.channelHeight = 0 self.px_per_samp = 0 self.dataBuffer = None + self.tsBuffer = None self.markerBuffer = None - self.lastY = [] - self.scaling = [] - self.mean = [] - self.t0 = 0 + # Per-stream sweep/timestamp state, keyed by stream index. Streams at the + # same rate can arrive out of phase, so each keeps its own t0 and sweep + # position rather than sharing a single timeline. + self.stream_states = {} + self._paint_state = None + + def _stream_state(self, idx): + st = self.stream_states.get(idx) + if st is None: + st = {'t0': 0, 'chunk_idx': 0, 'lastX': None, 'lastY': [], 'mean': [], 'scaling': []} + self.stream_states[idx] = st + return st def get_data(self, sig_ts, sig_buffer, marker_ts, marker_buffer): update_x0 = float(self.width()) update_width = 0. + st = self._stream_state(self.dataTr.sig_strm_idx) # buffer should have exactly self.dataTr.chunkSize samples or be empty if any(sig_ts): - if not self.mean: - self.mean = [0 for _ in range(len(sig_buffer[0]))] - self.scaling = [1 for _ in range(len(sig_buffer[0]))] - if self.chunk_idx == 0: - self.t0 = sig_ts[0] + if not st['mean']: + st['mean'] = [0 for _ in range(len(sig_buffer[0]))] + st['scaling'] = [1 for _ in range(len(sig_buffer[0]))] + # Start a new sweep once a full screen of time has elapsed. + if st['chunk_idx'] == 0 or (sig_ts[0] - st['t0']) >= self.dataTr.seconds_per_screen: + st['t0'] = sig_ts[0] + st['chunk_idx'] = 0 + st['lastX'] = None + st['lastY'] = [] self.dataBuffer = copy.deepcopy(sig_buffer) - px_per_chunk = self.width() / self.dataTr.chunksPerScreen - update_x0 = self.chunk_idx * px_per_chunk - update_width = px_per_chunk + self.tsBuffer = list(sig_ts) + self._paint_state = st + px_per_sec = self.width() / self.dataTr.seconds_per_screen + x_start = (sig_ts[0] - st['t0']) * px_per_sec + x_end = (sig_ts[-1] - st['t0']) * px_per_sec + # Start the repaint region at the previous sample so the connecting line is included. + update_x0 = st['lastX'] if st['lastX'] is not None else x_start + update_width = x_end - update_x0 if any(marker_ts): px_out = [] @@ -152,8 +172,8 @@ def get_data(self, sig_ts, sig_buffer, marker_ts, marker_buffer): # TODO: Check samples vs pixels for both data stream and marker stream. # I think there is some rounding error. - if self.t0 <= ts <= (self.t0 + self.dataTr.seconds_per_screen): - px_out.append((ts - self.t0) * px_per_sec) + if st['t0'] <= ts <= (st['t0'] + self.dataTr.seconds_per_screen): + px_out.append((ts - st['t0']) * px_per_sec) ms_out.append(','.join(ms)) if any(px_out): # Sometimes the marker might happen just off screen so we lose it. @@ -161,74 +181,81 @@ def get_data(self, sig_ts, sig_buffer, marker_ts, marker_buffer): update_x0 = min(update_x0, min(px_out)) update_width = max(update_width, max([_ - update_x0 for _ in px_out])) - if any(sig_ts) and update_x0 == sig_ts[0]: - update_x0 -= self.px_per_samp # Offset to connect with previous sample - # Repaint only the region of the screen containing this data chunk. if update_width > 0: self.update(int(update_x0), 0, int(update_width + 1), self.height()) def paintEvent(self, event): painter = QPainter(self) + colors = self._theme_colors() if self.dataBuffer is not None: - painter.setPen(QPen(Qt.blue)) + painter.setPen(QPen(colors['signal'])) + st = self._paint_state n_samps = len(self.dataBuffer) n_chans = len(self.dataBuffer[0]) self.channelHeight = self.height() / n_chans - self.px_per_samp = self.width() / self.dataTr.chunksPerScreen / n_samps + px_per_sec = self.width() / self.dataTr.seconds_per_screen + xs = [(ts - st['t0']) * px_per_sec for ts in self.tsBuffer] # ====================================================================================================== # Calculate Trend and Scaling # ====================================================================================================== - if self.chunk_idx == 0 or not self.mean: + if st['chunk_idx'] == 0 or not st['mean']: for chan_idx in range(n_chans): samps_for_chan = [frame[chan_idx] for frame in self.dataBuffer] - self.mean[chan_idx] = sum(samps_for_chan) / len(samps_for_chan) + st['mean'][chan_idx] = sum(samps_for_chan) / len(samps_for_chan) for m in range(len(samps_for_chan)): - samps_for_chan[m] -= self.mean[chan_idx] - - data_range = (max(samps_for_chan) - min(samps_for_chan) + 0.0000000000001) - self.scaling[chan_idx] = self.channelHeight * CHANNEL_Y_FILL / data_range + samps_for_chan[m] -= st['mean'][chan_idx] + + # A flat channel (common with HD-EEG reference/disconnected channels) + # has ~zero range; scale it to 0 so it draws centered instead of + # exploding to a huge coordinate and overflowing drawLine (int32). + # See https://github.com/labstreaminglayer/App-SigVisualizer/issues/18, + # https://github.com/labstreaminglayer/App-SigVisualizer/issues/20 + data_range = max(samps_for_chan) - min(samps_for_chan) + if data_range > 0: + st['scaling'][chan_idx] = self.channelHeight * CHANNEL_Y_FILL / data_range + else: + st['scaling'][chan_idx] = 0 # ====================================================================================================== # Trend Removal and Scaling # ====================================================================================================== for samp_idx in range(n_samps): for chan_idx in range(n_chans): - self.dataBuffer[samp_idx][chan_idx] -= self.mean[chan_idx] - self.dataBuffer[samp_idx][chan_idx] *= self.scaling[chan_idx] + self.dataBuffer[samp_idx][chan_idx] -= st['mean'][chan_idx] + self.dataBuffer[samp_idx][chan_idx] *= st['scaling'][chan_idx] # ====================================================================================================== # Plot # ====================================================================================================== - px_per_chunk = self.width() / self.dataTr.chunksPerScreen - x0 = self.chunk_idx * px_per_chunk for ch_idx in range(n_chans): chan_offset = (ch_idx + 0.5) * self.channelHeight - if self.lastY: - if not math.isnan(self.lastY[ch_idx]) and not math.isnan(self.dataBuffer[0][ch_idx]): - painter.drawLine(x0 - self.px_per_samp, - -self.lastY[ch_idx] + chan_offset, - x0, + if st['lastY']: + if not math.isnan(st['lastY'][ch_idx]) and not math.isnan(self.dataBuffer[0][ch_idx]): + painter.drawLine(st['lastX'], + -st['lastY'][ch_idx] + chan_offset, + xs[0], -self.dataBuffer[0][ch_idx] + chan_offset) for m in range(n_samps - 1): if not math.isnan(self.dataBuffer[m][ch_idx]) and not math.isnan(self.dataBuffer[m+1][ch_idx]): - painter.drawLine(x0 + m * self.px_per_samp, + painter.drawLine(xs[m], -self.dataBuffer[m][ch_idx] + chan_offset, - x0 + (m + 1) * self.px_per_samp, + xs[m + 1], -self.dataBuffer[m+1][ch_idx] + chan_offset) # Reset for next iteration - self.chunk_idx = (self.chunk_idx + 1) % self.dataTr.chunksPerScreen # For next iteration - self.lastY = self.dataBuffer[-1] + st['chunk_idx'] = (st['chunk_idx'] + 1) % self.dataTr.chunksPerScreen # For next iteration + st['lastY'] = self.dataBuffer[-1] + st['lastX'] = xs[-1] self.dataBuffer = None if self.markerBuffer is not None: - painter.setPen(QPen(Qt.red)) + painter.setPen(QPen(colors['marker'])) for px, mrk in self.markerBuffer: painter.drawLine(px, 0, px, self.height()) painter.drawText(px - 2 * self.px_per_samp, 0.95 * self.height(), mrk) diff --git a/icons/chevron_left.svg b/src/sigvisualizer/resources/icons/chevron_left.svg similarity index 100% rename from icons/chevron_left.svg rename to src/sigvisualizer/resources/icons/chevron_left.svg diff --git a/icons/chevron_right.svg b/src/sigvisualizer/resources/icons/chevron_right.svg similarity index 100% rename from icons/chevron_right.svg rename to src/sigvisualizer/resources/icons/chevron_right.svg diff --git a/sigvisualizer.ico b/src/sigvisualizer/resources/sigvisualizer.ico similarity index 100% rename from sigvisualizer.ico rename to src/sigvisualizer/resources/sigvisualizer.ico diff --git a/sigvisualizer.png b/src/sigvisualizer/resources/sigvisualizer.png similarity index 100% rename from sigvisualizer.png rename to src/sigvisualizer/resources/sigvisualizer.png diff --git a/ui_sigvisualizer.ui b/src/sigvisualizer/resources/ui_sigvisualizer.ui similarity index 89% rename from ui_sigvisualizer.ui rename to src/sigvisualizer/resources/ui_sigvisualizer.ui index 095ae28..aeb7be1 100644 --- a/ui_sigvisualizer.ui +++ b/src/sigvisualizer/resources/ui_sigvisualizer.ui @@ -56,7 +56,7 @@ - + @@ -79,14 +79,6 @@ - - - PaintWidget - QWidget -
paintwidget.h
- 1 -
-
diff --git a/ui_sigvisualizer.py b/ui_sigvisualizer.py deleted file mode 100644 index 530c9b1..0000000 --- a/ui_sigvisualizer.py +++ /dev/null @@ -1,72 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'ui_sigvisualizer.ui' -# -# Created by: PyQt5 UI code generator 5.11.2 -# -# WARNING! All changes made in this file will be lost! - -from PyQt5 import QtCore, QtGui, QtWidgets - -class Ui_MainWindow(object): - def setupUi(self, MainWindow): - MainWindow.setObjectName("MainWindow") - MainWindow.resize(1123, 759) - self.centralwidget = QtWidgets.QWidget(MainWindow) - self.centralwidget.setObjectName("centralwidget") - self.gridLayout = QtWidgets.QGridLayout(self.centralwidget) - self.gridLayout.setObjectName("gridLayout") - self.toggleButton = QtWidgets.QPushButton(self.centralwidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.toggleButton.sizePolicy().hasHeightForWidth()) - self.toggleButton.setSizePolicy(sizePolicy) - self.toggleButton.setMaximumSize(QtCore.QSize(20, 16777215)) - self.toggleButton.setText("") - self.toggleButton.setObjectName("toggleButton") - self.gridLayout.addWidget(self.toggleButton, 0, 0, 2, 1) - self.treeWidget = QtWidgets.QTreeWidget(self.centralwidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.treeWidget.sizePolicy().hasHeightForWidth()) - self.treeWidget.setSizePolicy(sizePolicy) - self.treeWidget.setMaximumSize(QtCore.QSize(180, 16777215)) - self.treeWidget.setObjectName("treeWidget") - self.treeWidget.headerItem().setText(0, "1") - self.gridLayout.addWidget(self.treeWidget, 0, 1, 1, 1) - self.widget = PaintWidget(self.centralwidget) - self.widget.setObjectName("widget") - self.gridLayout.addWidget(self.widget, 0, 2, 2, 1) - self.updateButton = QtWidgets.QPushButton(self.centralwidget) - self.updateButton.setObjectName("updateButton") - self.gridLayout.addWidget(self.updateButton, 1, 1, 1, 1) - MainWindow.setCentralWidget(self.centralwidget) - self.menubar = QtWidgets.QMenuBar(MainWindow) - self.menubar.setGeometry(QtCore.QRect(0, 0, 1123, 26)) - self.menubar.setObjectName("menubar") - MainWindow.setMenuBar(self.menubar) - self.statusbar = QtWidgets.QStatusBar(MainWindow) - self.statusbar.setObjectName("statusbar") - MainWindow.setStatusBar(self.statusbar) - - self.retranslateUi(MainWindow) - QtCore.QMetaObject.connectSlotsByName(MainWindow) - - def retranslateUi(self, MainWindow): - _translate = QtCore.QCoreApplication.translate - MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) - self.updateButton.setText(_translate("MainWindow", "Update Streams")) - -from paintwidget import PaintWidget - -if __name__ == "__main__": - import sys - app = QtWidgets.QApplication(sys.argv) - MainWindow = QtWidgets.QMainWindow() - ui = Ui_MainWindow() - ui.setupUi(MainWindow) - MainWindow.show() - sys.exit(app.exec_()) -