class ChartWidget(QWidget): def __init__(self, parent=None, ticker="BTC"): super().__init__(parent) uic.loadUi("chart.ui", self) self.ticker = ticker self.viewLimit = 128 self.priceData = QLineSeries() self.priceChart = QChart() self.priceChart.addSeries(self.priceData) self.priceChart.legend().hide() axisX = QDateTimeAxis() axisX.setFormat("hh:mm:ss") axisX.setTickCount(4) dt = QDateTime.currentDateTime() axisX.setRange(dt, dt.addSecs(self.viewLimit)) axisY = QValueAxis() axisY.setVisible(False) self.priceChart.addAxis(axisX, Qt.AlignBottom) self.priceChart.addAxis(axisY, Qt.AlignRight) self.priceData.attachAxis(axisX) self.priceData.attachAxis(axisY) self.priceChart.layout().setContentsMargins(0, 0, 0, 0) self.priceView.setChart(self.priceChart) self.priceView.setRenderHints(QPainter.Antialiasing) # ----------------- 추 가 ------------------ self.pw = PriceWorker(ticker) self.pw.dataSent.connect(self.appendData) self.pw.start() # ------------------------------------------ def appendData(self, currPirce): if len(self.priceData) == self.viewLimit: self.priceData.remove(0) dt = QDateTime.currentDateTime() self.priceData.append(dt.toMSecsSinceEpoch(), currPirce) self.__updateAxis() def __updateAxis(self): pvs = self.priceData.pointsVector() dtStart = QDateTime.fromMSecsSinceEpoch(int(pvs[0].x())) if len(self.priceData) == self.viewLimit: dtLast = QDateTime.fromMSecsSinceEpoch(int(pvs[-1].x())) else: dtLast = dtStart.addSecs(self.viewLimit) ax = self.priceChart.axisX() ax.setRange(dtStart, dtLast) ay = self.priceChart.axisY() dataY = [v.y() for v in pvs] ay.setRange(min(dataY), max(dataY))
class MainWindow(QMainWindow, Ui_MainWindow): """ Main application window """ WIDTH = 150 HEIGHT = 50 MARGIN = 5 max_ping = 0 max_loss = 0 def __init__(self, host, history, history_size, *args, **kwargs): super(MainWindow, self).__init__(*args, **kwargs) self.host = host self.history = history self.history_size = history_size self.setupUi(self) self.setWindowFlag(Qt.WindowStaysOnTopHint) self.position_to_dock() self.series_delay = QLineSeries() self.series_loss = QLineSeries() self.axis_X = QDateTimeAxis() # pylint: disable=invalid-name self.axis_X.setTickCount(3) self.axis_X.setFormat("HH:mm") self.axis_X.setTitleText("Time") self.chart = QChart() self.chart.addSeries(self.series_delay) self.chart.addSeries(self.series_loss) self.chart.setTitle(f"Connection to {self.host}") self.init_series(self.series_delay, "Delay ms") self.init_series(self.series_loss, "Loss %") self.chart.legend().setVisible(False) self.chart.legend().setAlignment(Qt.AlignBottom) self.chart.layout().setContentsMargins(0, 0, 0, 0) self.chart.setBackgroundRoundness(0) self.chart.setMargins(QMargins(0, 0, 0, 0)) self.chartWidget.setChart(self.chart) self.chartWidget.setRenderHint(QPainter.Antialiasing) def init_series(self, series, label): """ Series settings """ self.chart.setAxisX(self.axis_X, series) axis_Y = QValueAxis() # pylint: disable=invalid-name axis_Y.setLabelFormat("%i") axis_Y.setTitleText(label) axis_Y.setRange(0, 100) self.chart.addAxis(axis_Y, Qt.AlignLeft) self.chart.setAxisY(axis_Y, series) def add_series(self, ping, loss): """ Append series data """ self.max_ping = max(ping or 0, self.max_ping) self.max_loss = max(loss, self.max_loss) if self.series_delay.count() > self.history_size: self.series_delay.remove(0) self.series_delay.append( QDateTime.currentDateTime().toMSecsSinceEpoch(), ping or 0) if self.series_loss.count() > self.history_size: self.series_loss.remove(0) self.series_loss.append( QDateTime.currentDateTime().toMSecsSinceEpoch(), loss) self.axis_X.setRange( QDateTime.currentDateTime().addSecs(-self.history), QDateTime.currentDateTime()) self.chart.axisY().setRange(0, self.max_ping + self.MARGIN) def position_to_dock(self): """ Adjust main window position according to it's size and desktop """ desktop_geometry = QDesktopWidget().availableGeometry() self.setGeometry( desktop_geometry.width() - self.width() - self.MARGIN, desktop_geometry.height() - self.height() - self.MARGIN, self.width(), self.height()) def set_labels(self, mean_=None, curr=None, loss=None): """ Update window text """ mean_text = curr_text = loss_text = "No connection" if mean_ is not None: mean_text = f"Mean ping: {mean_}ms" if curr is not None: curr_text = f"Last ping: {curr}ms" if loss is not None: loss_text = f"Ping loss: {loss}%" self.meanLabel.setText(mean_text) self.currLabel.setText(curr_text) self.lossLabel.setText(loss_text) def toggle(self, reason): """ Toggle window visibility """ if reason == QSystemTrayIcon.ActivationReason.DoubleClick: # pylint: disable=no-member self.setVisible(not self.isVisible()) if self.isVisible(): self.activateWindow()
class MyWindow(QMainWindow): def __init__(self): super().__init__() # thread self.worker = Worker() self.worker.price.connect(self.get_price) self.worker.start() # window size self.setMinimumSize(600, 400) # data self.series = QLineSeries() # chart object self.chart = QChart() self.chart.legend().hide() self.chart.addSeries(self.series) # data feeding # axis axis_x = QDateTimeAxis() axis_x.setFormat("hh:mm:ss") dt = QDateTime.currentDateTime() axis_x.setRange(dt, dt.addSecs(128)) self.chart.addAxis(axis_x, Qt.AlignBottom) self.series.attachAxis(axis_x) axis_y = QValueAxis() axis_y.setLabelFormat("%i") self.chart.addAxis(axis_y, Qt.AlignLeft) self.series.attachAxis(axis_y) # margin self.chart.layout().setContentsMargins(0, 0, 0, 0) # displaying chart chart_view = QChartView(self.chart) chart_view.setRenderHint(QPainter.Antialiasing) self.setCentralWidget(chart_view) @pyqtSlot(float) def get_price(self, cur_price): if len(self.series) == 128: self.series.remove(0) # delete first data # append current price dt = QDateTime.currentDateTime() ts = dt.toMSecsSinceEpoch() self.series.append(ts, cur_price) print(ts, cur_price) # update asis data = self.series.pointsVector() first_ts = data[0].x() last_ts = data[-1].x() first_dt = QDateTime.fromMSecsSinceEpoch(first_ts) last_dt = QDateTime.fromMSecsSinceEpoch(last_ts) axis_x = self.chart.axisX() axis_x.setRange(first_dt, last_dt) axis_y = self.chart.axisY() axis_y.setRange(70000000, 71000000)
class TelemetryDialog(QDialog): resized = QtCore.pyqtSignal() visibility = QtCore.pyqtSignal(bool) def __init__(self, winTitle="Network Telemetry", parent=None): super(TelemetryDialog, self).__init__(parent) self.visibility.connect(self.onVisibilityChanged) self.winTitle = winTitle self.updateLock = Lock() # Used to detect network change self.lastNetKey = "" self.lastSeen = None self.maxPoints = 20 self.maxRowPoints = 60 self.paused = False self.streamingSave = False self.streamingFile = None self.linesBeforeFlush = 10 self.currentLine = 0 # OK and Cancel buttons #buttons = QDialogButtonBox(QDialogButtonBox.Ok,Qt.Horizontal, self) #buttons.accepted.connect(self.accept) #buttons.move(170, 280) desktopSize = QApplication.desktop().screenGeometry() #self.mainWidth=1024 #self.mainHeight=768 #self.mainWidth = desktopSize.width() * 3 / 4 #self.mainHeight = desktopSize.height() * 3 / 4 self.setGeometry(self.geometry().x(), self.geometry().y(), desktopSize.width() / 2, desktopSize.height() / 2) self.setWindowTitle(winTitle) self.radar = RadarWidget(self) self.radar.setGeometry(self.geometry().width() / 2, 10, self.geometry().width() / 2 - 20, self.geometry().width() / 2 - 20) self.createTable() self.btnExport = QPushButton("Export Table", self) self.btnExport.clicked[bool].connect(self.onExportClicked) self.btnExport.setStyleSheet("background-color: rgba(2,128,192,255);") self.btnPause = QPushButton("Pause Table", self) self.btnPause.setCheckable(True) self.btnPause.clicked[bool].connect(self.onPauseClicked) self.btnPause.setStyleSheet("background-color: rgba(2,128,192,255);") self.btnStream = QPushButton("Streaming Save", self) self.btnStream.setCheckable(True) self.btnStream.clicked[bool].connect(self.onStreamClicked) self.btnStream.setStyleSheet("background-color: rgba(2,128,192,255);") self.createChart() self.setBlackoutColors() self.setMinimumWidth(600) self.setMinimumHeight(600) self.center() def createTable(self): # Set up location table self.locationTable = QTableWidget(self) self.locationTable.setColumnCount(8) self.locationTable.setGeometry(10, 10, self.geometry().width() / 2 - 20, self.geometry().height() / 2) self.locationTable.setShowGrid(True) self.locationTable.setHorizontalHeaderLabels([ 'macAddr', 'SSID', 'Strength', 'Timestamp', 'GPS', 'Latitude', 'Longitude', 'Altitude' ]) self.locationTable.resizeColumnsToContents() self.locationTable.setRowCount(0) self.locationTable.horizontalHeader().setSectionResizeMode( 1, QHeaderView.Stretch) self.ntRightClickMenu = QMenu(self) newAct = QAction('Copy', self) newAct.setStatusTip('Copy data to clipboard') newAct.triggered.connect(self.onCopy) self.ntRightClickMenu.addAction(newAct) self.locationTable.setContextMenuPolicy(Qt.CustomContextMenu) self.locationTable.customContextMenuRequested.connect( self.showNTContextMenu) def setBlackoutColors(self): self.locationTable.setStyleSheet( "QTableView {background-color: black;gridline-color: white;color: white} QTableCornerButton::section{background-color: white;}" ) headerStyle = "QHeaderView::section{background-color: white;border: 1px solid black;color: black;} QHeaderView::down-arrow,QHeaderView::up-arrow {background: none;}" self.locationTable.horizontalHeader().setStyleSheet(headerStyle) self.locationTable.verticalHeader().setStyleSheet(headerStyle) mainTitleBrush = QBrush(Qt.red) self.timeChart.setTitleBrush(mainTitleBrush) self.timeChart.setBackgroundBrush(QBrush(Qt.black)) self.timeChart.axisX().setLabelsColor(Qt.white) self.timeChart.axisY().setLabelsColor(Qt.white) titleBrush = QBrush(Qt.white) self.timeChart.axisX().setTitleBrush(titleBrush) self.timeChart.axisY().setTitleBrush(titleBrush) def resizeEvent(self, event): wDim = self.geometry().width() / 2 - 20 hDim = self.geometry().height() / 2 smallerDim = wDim if hDim < smallerDim: smallerDim = hDim # Radar self.radar.setGeometry(self.geometry().width() - smallerDim - 10, 10, smallerDim, smallerDim) # chart self.timePlot.setGeometry(10, 10, self.geometry().width() - smallerDim - 30, smallerDim) # Buttons self.btnPause.setGeometry(10, self.geometry().height() / 2 + 18, 110, 25) self.btnExport.setGeometry(150, self.geometry().height() / 2 + 18, 110, 25) self.btnStream.setGeometry(290, self.geometry().height() / 2 + 18, 110, 25) # Table self.locationTable.setGeometry(10, self.geometry().height() / 2 + 50, self.geometry().width() - 20, self.geometry().height() / 2 - 60) def center(self): # Get our geometry qr = self.frameGeometry() # Find the desktop center point cp = QDesktopWidget().availableGeometry().center() # Move our center point to the desktop center point qr.moveCenter(cp) # Move the top-left point of the application window to the top-left point of the qr rectangle, # basically centering the window self.move(qr.topLeft()) def showNTContextMenu(self, pos): curRow = self.locationTable.currentRow() if curRow == -1: return self.ntRightClickMenu.exec_(self.locationTable.mapToGlobal(pos)) def onCopy(self): self.updateLock.acquire() curRow = self.locationTable.currentRow() curCol = self.locationTable.currentColumn() if curRow == -1 or curCol == -1: self.updateLock.release() return curText = self.locationTable.item(curRow, curCol).text() clipboard = QApplication.clipboard() clipboard.setText(curText) self.updateLock.release() def onVisibilityChanged(self, visible): if not visible: self.paused = True self.btnPause.setStyleSheet("background-color: rgba(255,0,0,255);") # We're coming out of streaming self.streamingSave = False self.btnStream.setStyleSheet( "background-color: rgba(2,128,192,255);") self.btnStream.setChecked(False) if (self.streamingFile): self.streamingFile.close() self.streamingFile = None return else: self.paused = False self.btnPause.setStyleSheet( "background-color: rgba(2,128,192,255);") if self.locationTable.rowCount() > 1: self.locationTable.scrollToItem(self.locationTable.item(0, 0)) def hideEvent(self, event): self.visibility.emit(False) def showEvent(self, event): self.visibility.emit(True) def onPauseClicked(self, pressed): if self.btnPause.isChecked(): self.paused = True self.btnPause.setStyleSheet("background-color: rgba(255,0,0,255);") else: self.paused = False self.btnPause.setStyleSheet( "background-color: rgba(2,128,192,255);") def onStreamClicked(self, pressed): if not self.btnStream.isChecked(): # We're coming out of streaming self.streamingSave = False self.btnStream.setStyleSheet( "background-color: rgba(2,128,192,255);") if (self.streamingFile): self.streamingFile.close() self.streamingFile = None return self.btnStream.setStyleSheet("background-color: rgba(255,0,0,255);") self.streamingSave = True fileName = self.saveFileDialog() if not fileName: self.btnStream.setStyleSheet( "background-color: rgba(2,128,192,255);") self.btnStream.setChecked(False) return try: self.streamingFile = open( fileName, 'w', 1 ) # 1 says use line buffering, otherwise it fully buffers and doesn't write except: QMessageBox.question(self, 'Error', "Unable to write to " + fileName, QMessageBox.Ok) self.streamingFile = None self.streamingSave = False self.btnStream.setStyleSheet( "background-color: rgba(2,128,192,255);") self.btnStream.setChecked(False) return self.streamingFile.write( 'MAC Address,SSID,Strength,Timestamp,GPS,Latitude,Longitude,Altitude\n' ) def onExportClicked(self): fileName = self.saveFileDialog() if not fileName: return try: outputFile = open(fileName, 'w') except: QMessageBox.question(self, 'Error', "Unable to write to " + fileName, QMessageBox.Ok) return outputFile.write( 'MAC Address,SSID,Strength,Timestamp,GPS,Latitude,Longitude,Altitude\n' ) numItems = self.locationTable.rowCount() if numItems == 0: outputFile.close() return self.updateLock.acquire() for i in range(0, numItems): outputFile.write( self.locationTable.item(i, 0).text() + ',"' + self.locationTable.item(i, 1).text() + '",' + self.locationTable.item(i, 2).text() + ',' + self.locationTable.item(i, 3).text()) outputFile.write(',' + self.locationTable.item(i, 4).text() + ',' + self.locationTable.item(i, 5).text() + ',' + self.locationTable.item(i, 6).text() + ',' + self.locationTable.item(i, 7).text() + '\n') self.updateLock.release() outputFile.close() def saveFileDialog(self): options = QFileDialog.Options() options |= QFileDialog.DontUseNativeDialog fileName, _ = QFileDialog.getSaveFileName( self, "QFileDialog.getSaveFileName()", "", "CSV Files (*.csv);;All Files (*)", options=options) if fileName: return fileName else: return None def createChart(self): self.timeChart = QChart() titleFont = QFont() titleFont.setPixelSize(18) titleBrush = QBrush(QColor(0, 0, 255)) self.timeChart.setTitleFont(titleFont) self.timeChart.setTitleBrush(titleBrush) self.timeChart.setTitle('Signal (Past ' + str(self.maxPoints) + ' Samples)') # self.timeChart.addSeries(testseries) # self.timeChart.createDefaultAxes() self.timeChart.legend().hide() # Axis examples: https://doc.qt.io/qt-5/qtcharts-multiaxis-example.html newAxis = QValueAxis() newAxis.setMin(0) newAxis.setMax(self.maxPoints) newAxis.setTickCount(11) newAxis.setLabelFormat("%d") newAxis.setTitleText("Sample") self.timeChart.addAxis(newAxis, Qt.AlignBottom) newAxis = QValueAxis() newAxis.setMin(-100) newAxis.setMax(-10) newAxis.setTickCount(9) newAxis.setLabelFormat("%d") newAxis.setTitleText("dBm") self.timeChart.addAxis(newAxis, Qt.AlignLeft) chartBorder = Qt.darkGray self.timePlot = QChartView(self.timeChart, self) self.timePlot.setBackgroundBrush(chartBorder) self.timePlot.setRenderHint(QPainter.Antialiasing) self.timeSeries = QLineSeries() pen = QPen(Qt.yellow) pen.setWidth(2) self.timeSeries.setPen(pen) self.timeChart.addSeries(self.timeSeries) self.timeSeries.attachAxis(self.timeChart.axisX()) self.timeSeries.attachAxis(self.timeChart.axisY()) def updateNetworkData(self, curNet): if not self.isVisible(): return # Signal is -NN dBm. Need to make it positive for the plot self.radar.updateData(curNet.signal * -1) if self.winTitle == "Client Telemetry": self.setWindowTitle(self.winTitle + " - [" + curNet.macAddr + "] " + curNet.ssid) else: self.setWindowTitle(self.winTitle + " - " + curNet.ssid) self.radar.draw() # Network changed. Clear our table and time data updateChartAndTable = False self.updateLock.acquire() if (curNet.getKey() != self.lastNetKey): self.lastNetKey = curNet.getKey() self.locationTable.setRowCount(0) self.timeSeries.clear() updateChartAndTable = True ssidTitle = curNet.ssid if len(ssidTitle) > 28: ssidTitle = ssidTitle[:28] ssidTitle = ssidTitle + '...' self.timeChart.setTitle(ssidTitle + ' Signal (Past ' + str(self.maxPoints) + ' Samples)') else: if self.lastSeen != curNet.lastSeen: updateChartAndTable = True if updateChartAndTable: # Update chart numPoints = len(self.timeSeries.pointsVector()) if numPoints >= self.maxPoints: self.timeSeries.remove(0) # Now we need to reset the x data to pull the series back counter = 0 for curPoint in self.timeSeries.pointsVector(): self.timeSeries.replace(counter, counter, curPoint.y()) counter += 1 if curNet.signal >= -100: self.timeSeries.append(numPoints, curNet.signal) else: self.timeSeries.append(numPoints, -100) # Update Table self.addTableData(curNet) # Limit points in each if self.locationTable.rowCount() > self.maxRowPoints: self.locationTable.setRowCount(self.maxRowPoints) self.updateLock.release() def addTableData(self, curNet): if self.paused: return # rowPosition = self.locationTable.rowCount() # Always insert at row(0) rowPosition = 0 self.locationTable.insertRow(rowPosition) #if (addedFirstRow): # self.locationTable.setRowCount(1) # ['macAddr','SSID', 'Strength', 'Timestamp','GPS', 'Latitude', 'Longitude', 'Altitude'] self.locationTable.setItem(rowPosition, 0, QTableWidgetItem(curNet.macAddr)) tmpssid = curNet.ssid if (len(tmpssid) == 0): tmpssid = '<Unknown>' newSSID = QTableWidgetItem(tmpssid) self.locationTable.setItem(rowPosition, 1, newSSID) self.locationTable.setItem(rowPosition, 2, IntTableWidgetItem(str(curNet.signal))) self.locationTable.setItem( rowPosition, 3, DateTableWidgetItem(curNet.lastSeen.strftime("%m/%d/%Y %H:%M:%S"))) if curNet.gps.isValid: self.locationTable.setItem(rowPosition, 4, QTableWidgetItem('Yes')) else: self.locationTable.setItem(rowPosition, 4, QTableWidgetItem('No')) self.locationTable.setItem( rowPosition, 5, FloatTableWidgetItem(str(curNet.gps.latitude))) self.locationTable.setItem( rowPosition, 6, FloatTableWidgetItem(str(curNet.gps.longitude))) self.locationTable.setItem( rowPosition, 7, FloatTableWidgetItem(str(curNet.gps.altitude))) #order = Qt.DescendingOrder #self.locationTable.sortItems(3, order ) # If we're in streaming mode, write the data out to disk as well if self.streamingFile: self.streamingFile.write( self.locationTable.item(rowPosition, 0).text() + ',"' + self.locationTable.item(rowPosition, 1).text() + '",' + self.locationTable.item(rowPosition, 2).text() + ',' + self.locationTable.item(rowPosition, 3).text() + ',' + self.locationTable.item(rowPosition, 4).text() + ',' + self.locationTable.item(rowPosition, 5).text() + ',' + self.locationTable.item(rowPosition, 6).text() + ',' + self.locationTable.item(rowPosition, 7).text() + '\n') if (self.currentLine > self.linesBeforeFlush): self.streamingFile.flush() self.currentLine += 1 numRows = self.locationTable.rowCount() if numRows > 1: self.locationTable.scrollToItem(self.locationTable.item(0, 0)) def onTableHeadingClicked(self, logical_index): header = self.locationTable.horizontalHeader() order = Qt.DescendingOrder # order = Qt.DescendingOrder if not header.isSortIndicatorShown(): header.setSortIndicatorShown(True) elif header.sortIndicatorSection() == logical_index: # apparently, the sort order on the header is already switched # when the section was clicked, so there is no need to reverse it order = header.sortIndicatorOrder() header.setSortIndicator(logical_index, order) self.locationTable.sortItems(logical_index, order) def updateData(self, newRadius): self.radar.updateData(newRadius) def showTelemetry(parent=None): dialog = TelemetryDialog(parent) result = dialog.exec_() return (result == QDialog.Accepted)
class RowingMonitorMainWindow(QtWidgets.QMainWindow): COLOR_RED = QColor('#E03A3E') COLOR_BLUE = QColor('#009DDC') DISABLE_LOGGING = False PLOT_VISIBLE_SAMPLES = 200 PLOT_MIN_Y = -1 PLOT_MAX_Y = 55 PLOT_TIME_WINDOW_SECONDS = 7 PLOT_WIDTH_INCHES = 2 PLOT_HEIGHT_INCHES = 1 PLOT_DPI = 300 PLOT_FAST_DRAWING = False WORK_PLOT_VISIBLE_STROKES = 64 WORK_PLOT_MIN_Y = 0 WORK_PLOT_MAX_Y = 350 BOAT_SPEED_PLOT_VISIBLE_STROKES = 64 BOAT_SPEED_PLOT_MIN_Y = 0 BOAT_SPEED_PLOT_MAX_Y = 10 GUI_FONT = QtGui.QFont('Nunito SemiBold', 12) GUI_FONT_LARGE = QtGui.QFont('Nunito', 24) GUI_FONT_MEDIUM = QtGui.QFont('Nunito', 16) def __init__(self, config, data_source, *args, **kwargs): super(RowingMonitorMainWindow, self).__init__(*args, **kwargs) self.setWindowTitle('Rowing Monitor') self.config = config self.log_folder_path = config.log_folder_path self.workout = wo.WorkoutMetricsTracker( config=config, data_source=data_source ) # Connect workut emitter to UI update self.workout_qt_emitter = SignalEmitter() self.workout_qt_emitter.updated.connect(self.ui_callback) # Setup main window layout self.main_widget = QtWidgets.QWidget() self.main_widget.setFocus() self.setCentralWidget(self.main_widget) self.app_layout = QtWidgets.QVBoxLayout(self.main_widget) self.app_layout.setContentsMargins(0, 0, 0, 0) #(left, top, right, bottom) # Build button bar self.button_bar_background_widget = QtWidgets.QWidget(self.main_widget) self.button_bar_background_widget.setObjectName('ButtonBarBackground') self.button_bar_background_widget.setStyleSheet('QWidget#ButtonBarBackground {background-color: #F1F1F1;}') self.button_bar_layout = QtWidgets.QHBoxLayout(self.button_bar_background_widget) self.start_button = QtWidgets.QPushButton('Start') self.start_button.setFlat(True) # Start button style palette = self.start_button.palette() palette.setColor(palette.Button, QColor('#E03A3E')) palette.setColor(palette.ButtonText, QColor('white')) self.start_button.setAutoFillBackground(True) self.start_button.setPalette(palette) self.start_button.update() self.start_button.setFont(self.GUI_FONT) self.start_button.setMinimumSize(97, 60) self.start_button.setMaximumSize(97, 60) # Add to main window self.button_bar_layout.addWidget(self.start_button) #self.button_bar_layout.addWidget(self.button_bar_background_widget) self.button_bar_layout.setAlignment(QtCore.Qt.AlignLeft) self.button_bar_layout.setContentsMargins(0, 0, 0, 0) #(left, top, right, bottom) self.app_layout.addWidget(self.button_bar_background_widget)#.addLayout(self.button_bar_layout) self.stats_layout = QtWidgets.QHBoxLayout() self.stats_layout.setContentsMargins(0, 0, 0, 0) self.stats_layout.setSpacing(0) self.app_layout.addLayout(self.stats_layout) # Build workout stats bar self.metrics_panel_layout = QtWidgets.QVBoxLayout() self.charts_panel_layout = QtWidgets.QVBoxLayout() self.workout_totals_layout = QtWidgets.QVBoxLayout() self.time_label = QtWidgets.QLabel(self._format_total_workout_time(0)) self.distance_label = QtWidgets.QLabel(self._format_total_workout_distance(0)) self.time_label.setAlignment(QtCore.Qt.AlignCenter) self.distance_label.setAlignment(QtCore.Qt.AlignCenter) self.time_label.setFixedHeight(40) self.distance_label.setFixedHeight(30) self.workout_totals_layout.addWidget(self.time_label) self.workout_totals_layout.addWidget(self.distance_label) #self.workout_totals_layout.setSpacing(0) self.workout_totals_layout.setContentsMargins(0, 0, 0, 30) self.metrics_panel_layout.addLayout(self.workout_totals_layout) self.stroke_stats_layout = QtWidgets.QVBoxLayout() self.spm_label = QtWidgets.QLabel(self._format_strokes_per_minute(99)) self.stroke_ratio_label = QtWidgets.QLabel(self._format_stroke_ratio(1)) self.spm_label.setAlignment(QtCore.Qt.AlignCenter) self.stroke_ratio_label.setAlignment(QtCore.Qt.AlignCenter) self.spm_label.setFixedHeight(40) self.stroke_ratio_label.setFixedHeight(30) self.stroke_stats_layout.addWidget(self.spm_label) self.stroke_stats_layout.addWidget(self.stroke_ratio_label) #self.stroke_stats_layout.setSpacing(0) self.stroke_stats_layout.setContentsMargins(0, 30, 0, 30) self.metrics_panel_layout.addLayout(self.stroke_stats_layout) self.boat_stats_layout = QtWidgets.QVBoxLayout() self.boat_speed_label = QtWidgets.QLabel(self._format_boat_speed(0)) self.split_time_label = QtWidgets.QLabel(self._format_boat_pace(0)) self.boat_speed_label.setAlignment(QtCore.Qt.AlignCenter) self.split_time_label.setAlignment(QtCore.Qt.AlignCenter) self.boat_speed_label.setFixedHeight(40) self.split_time_label.setFixedHeight(30) self.boat_stats_layout.addWidget(self.boat_speed_label) self.boat_stats_layout.addWidget(self.split_time_label) #self.boat_stats_layout.setSpacing(0) self.boat_stats_layout.setContentsMargins(0, 30, 0, 0) self.metrics_panel_layout.addLayout(self.boat_stats_layout) # Appearance self.time_label.setFont(self.GUI_FONT_LARGE) self.distance_label.setFont(self.GUI_FONT_MEDIUM) self.spm_label.setFont(self.GUI_FONT_LARGE) self.stroke_ratio_label.setFont(self.GUI_FONT_MEDIUM) self.boat_speed_label.setFont(self.GUI_FONT_LARGE) self.split_time_label.setFont(self.GUI_FONT_MEDIUM) # Add to main window self.metrics_panel_layout.setSpacing(0) self.metrics_panel_layout.setContentsMargins(60, 30, 30, 30) #(left, top, right, bottom) self.charts_panel_layout.setSpacing(30) self.charts_panel_layout.setContentsMargins(30, 30, 60, 60)#(30, 30, 60, 60) #(left, top, right, bottom) self.stats_layout.addLayout(self.metrics_panel_layout) self.stats_layout.addLayout(self.charts_panel_layout) self.xdata = [0.0 for i in range(self.PLOT_VISIBLE_SAMPLES)] self.ydata = [0.0 for i in range(self.PLOT_VISIBLE_SAMPLES)] self.work_per_stroke_data = [0.0 for i in range(self.WORK_PLOT_VISIBLE_STROKES)] self.boat_speed_data = [0.0 for i in range(self.WORK_PLOT_VISIBLE_STROKES)] self.seen_strokes = 0 ############################################ # Add torque chart self.torque_plot = QChart() self.torque_plot.setContentsMargins(-26, -26, -26, -26) #self.torque_plot.setAnimationOptions(QChart.GridAxisAnimations) self.torque_plot.legend().setVisible(False) self.torque_plot_horizontal_axis = QValueAxis() self.torque_plot_vertical_axis = QValueAxis() self.torque_plot.addAxis(self.torque_plot_vertical_axis, QtCore.Qt.AlignLeft) self.torque_plot.addAxis(self.torque_plot_horizontal_axis, QtCore.Qt.AlignBottom) # Line series self.torque_plot_series = QLineSeries(self) for i in range(self.PLOT_VISIBLE_SAMPLES): self.torque_plot_series.append(0, 0) #self.torque_plot_series.setColor(QColor('#009DDC')) pen = self.torque_plot_series.pen() pen.setWidth(3) pen.setColor(self.COLOR_BLUE) pen.setJoinStyle(QtCore.Qt.RoundJoin) pen.setCapStyle(QtCore.Qt.RoundCap) self.torque_plot_series.setPen(pen) # Area series self.torque_plot_area_series = QAreaSeries() self.torque_plot_area_series.setUpperSeries(self.torque_plot_series) self.torque_plot_area_series.setLowerSeries(QLineSeries(self)) for i in range(self.PLOT_VISIBLE_SAMPLES): self.torque_plot_area_series.lowerSeries().append(0, 0) self.torque_plot_area_series.setColor(self.COLOR_BLUE) # Compose plot # self.torque_plot.addSeries(self.torque_plot_area_series) # self.torque_plot_area_series.attachAxis(self.torque_plot_horizontal_axis) # self.torque_plot_area_series.attachAxis(self.torque_plot_vertical_axis) self.torque_plot.addSeries(self.torque_plot_series) self.torque_plot_series.attachAxis(self.torque_plot_horizontal_axis) self.torque_plot_series.attachAxis(self.torque_plot_vertical_axis) # Set axes range self.torque_plot_vertical_axis.setRange(self.PLOT_MIN_Y, self.PLOT_MAX_Y) #self.torque_plot_vertical_axis.setTickCount(10) self.torque_plot_vertical_axis.setVisible(False) self.torque_plot_horizontal_axis.setRange(-self.PLOT_TIME_WINDOW_SECONDS, 0) self.torque_plot_horizontal_axis.setVisible(False) # Add plot view to GUI self.torque_plot_chartview = QChartView(self.torque_plot) self.torque_plot_chartview.setRenderHint(QPainter.Antialiasing) #self.torque_plot_chartview.setMinimumHeight(250) #self.torque_plot_chartview.resize(250, 250) self.torque_plot_box = QtWidgets.QGroupBox("Force") self.torque_plot_box.setFont(self.GUI_FONT) self.torque_plot_box.setAlignment(QtCore.Qt.AlignLeft) self.torque_plot_box_layout = QtWidgets.QVBoxLayout() self.torque_plot_box_layout.addWidget(self.torque_plot_chartview) self.torque_plot_box.setLayout(self.torque_plot_box_layout) self.charts_panel_layout.addWidget(self.torque_plot_box) ############################################ ############################################ # Add work chart self.work_plot = QChart() self.work_plot.setContentsMargins(-26, -26, -26, -26) self.work_plot.legend().setVisible(False) self.work_plot_horizontal_axis = QBarCategoryAxis() self.work_plot_vertical_axis = QValueAxis() self.work_plot.addAxis(self.work_plot_vertical_axis, QtCore.Qt.AlignLeft) self.work_plot.addAxis(self.work_plot_horizontal_axis, QtCore.Qt.AlignBottom) # Define series self.work_plot_series = QBarSeries() self.work_plot_bar_set_list = [QBarSet(str(i)) for i in range(self.WORK_PLOT_VISIBLE_STROKES)] self.work_plot_series.append(self.work_plot_bar_set_list) for bar_set in self.work_plot_bar_set_list: bar_set.append(0) self.work_plot_series.setBarWidth(1.0) # Compose plot self.work_plot.addSeries(self.work_plot_series) self.work_plot_series.attachAxis(self.work_plot_horizontal_axis) self.work_plot_series.attachAxis(self.work_plot_vertical_axis) # Set axes range self.work_plot_vertical_axis.setRange(self.WORK_PLOT_MIN_Y, self.WORK_PLOT_MAX_Y) self.work_plot_vertical_axis.setTickCount(8) self.work_plot_vertical_axis.setVisible(False) self.work_plot_horizontal_axis.append("1") self.work_plot_horizontal_axis.setVisible(False) # Add plot view to GUI self.work_plot_chartview = QChartView(self.work_plot) self.work_plot_chartview.setRenderHint(QPainter.Antialiasing) #self.work_plot_chartview.setMinimumHeight(250) #self.work_plot_chartview.resize(250, 250) self.work_plot_box = QtWidgets.QGroupBox("Work per stroke") self.work_plot_box.setFont(self.GUI_FONT) self.work_plot_box.setAlignment(QtCore.Qt.AlignLeft) self.work_plot_box_layout = QtWidgets.QVBoxLayout() self.work_plot_box_layout.addWidget(self.work_plot_chartview) self.work_plot_box.setLayout(self.work_plot_box_layout) self.charts_panel_layout.addWidget(self.work_plot_box) ############################################ ############################################ # Add boat speed chart self.boat_speed_plot = QChart() self.boat_speed_plot.setContentsMargins(-26, -26, -26, -26) #self.boat_speed_plot.setBackgroundRoundness(0) self.boat_speed_plot.legend().setVisible(False) self.boat_speed_plot_horizontal_axis = QBarCategoryAxis() self.boat_speed_plot_vertical_axis = QValueAxis() self.boat_speed_plot.addAxis(self.boat_speed_plot_vertical_axis, QtCore.Qt.AlignLeft) self.boat_speed_plot.addAxis(self.boat_speed_plot_horizontal_axis, QtCore.Qt.AlignBottom) # Define series self.boat_speed_plot_series = QBarSeries() self.boat_speed_plot_bar_set_list = [QBarSet(str(i)) for i in range(self.BOAT_SPEED_PLOT_VISIBLE_STROKES)] self.boat_speed_plot_series.append(self.boat_speed_plot_bar_set_list) for bar_set in self.boat_speed_plot_bar_set_list: bar_set.append(0) self.boat_speed_plot_series.setBarWidth(1.0) # Compose plot self.boat_speed_plot.addSeries(self.boat_speed_plot_series) self.boat_speed_plot_series.attachAxis(self.boat_speed_plot_horizontal_axis) self.boat_speed_plot_series.attachAxis(self.boat_speed_plot_vertical_axis) # Set axes range self.boat_speed_plot_vertical_axis.setRange(self.BOAT_SPEED_PLOT_MIN_Y, self.BOAT_SPEED_PLOT_MAX_Y) self.boat_speed_plot_vertical_axis.setTickCount(8) self.boat_speed_plot_vertical_axis.setVisible(False) self.boat_speed_plot_horizontal_axis.append("1") self.boat_speed_plot_horizontal_axis.setVisible(False) # Add plot view to GUI self.boat_speed_plot_chartview = QChartView(self.boat_speed_plot) self.boat_speed_plot_chartview.setRenderHint(QPainter.Antialiasing) #self.boat_speed_plot_chartview.setContentsMargins(0, 0, 0, 0) self.boat_speed_plot_box = QtWidgets.QGroupBox("Boat speed") self.boat_speed_plot_box.setFont(self.GUI_FONT) #self.boat_speed_plot_box.setFlat(True) #self.boat_speed_plot_box.setContentsMargins(0, 0, 0, 0) #self.boat_speed_plot_box.setObjectName("BoatSpeedGB") # Changed here... #self.boat_speed_plot_box.setStyleSheet('QGroupBox {background-color: white;}') #self.main_widget.setStyleSheet('QGroupBox::title { background-color: blue }') self.boat_speed_plot_box.setAlignment(QtCore.Qt.AlignLeft) self.boat_speed_plot_box_layout = QtWidgets.QVBoxLayout() self.boat_speed_plot_box_layout.addWidget(self.boat_speed_plot_chartview) self.boat_speed_plot_box.setLayout(self.boat_speed_plot_box_layout) self.charts_panel_layout.addWidget(self.boat_speed_plot_box) ############################################ # Set interaction behavior self.start_button.clicked.connect(self.start) # Update workout duration every second self.timer = QtCore.QTimer() self.timer.setInterval(1000) self.timer.timeout.connect(self.timer_tick) self.start_timestamp = None self.started = False self.show() def update_torque_plot(self): self.torque_plot_series.append(self.xdata[-1], self.ydata[-1]) self.torque_plot_series.remove(0) self.torque_plot_area_series.lowerSeries().append(self.xdata[-1], 0) self.torque_plot_area_series.lowerSeries().remove(0) self.torque_plot_horizontal_axis.setRange(self.xdata[-1] - self.PLOT_TIME_WINDOW_SECONDS, self.xdata[-1]) def update_work_plot(self): # Create new bar set new_bar_set = QBarSet(str(self.seen_strokes)) value = self.work_per_stroke_data[-1] value_rel = int(value * 255 / self.WORK_PLOT_MAX_Y) new_bar_set.append(value) new_bar_set.setColor(self.COLOR_BLUE) # QColor(value_rel, value_rel, value_rel)) # Append new set, and remove oldest self.work_plot_series.append(new_bar_set) self.work_plot_series.remove(self.work_plot_series.barSets()[0]) def update_boat_speed_plot(self): # Create new bar set new_bar_set = QBarSet(str(self.seen_strokes)) value = self.boat_speed_data[-1] value_rel = int(value * 255 / self.BOAT_SPEED_PLOT_MAX_Y) new_bar_set.append(value) new_bar_set.setColor(self.COLOR_BLUE) # QColor(value_rel, value_rel, value_rel)) # Append new set, and remove oldest self.boat_speed_plot_series.append(new_bar_set) self.boat_speed_plot_series.remove(self.boat_speed_plot_series.barSets()[0]) def start(self): if not self.started: self.start_workout() self.start_button.setText('Stop') self.started = True else: self.stop_workout() self.start_button.setText('Start') self.started = False def start_workout(self): self.timer.start() self.workout.start(qt_signal_emitter=self.workout_qt_emitter) def stop_workout(self): self.timer.stop() self.workout.stop() if not self.DISABLE_LOGGING and not DEV_MODE: self.workout.save(output_folder_path=self.log_folder_path) def _format_total_workout_time(self, value_seconds): minutes = value_seconds // 60 seconds = value_seconds % 60 return '%d:%02d' % (minutes, seconds) def _format_total_workout_distance(self, value): return f'{int(value):,} m' def _format_strokes_per_minute(self, value): return '%.1f spm' % value def _format_stroke_ratio(self, value): return '1:%.1f ratio' % value def _format_boat_speed(self, value): return '%0.2f m/s' % value def _format_boat_pace(self, value_seconds): return '%s /500m' % (self._format_total_workout_time(value_seconds)) def ui_callback(self): # If this is the first pulse, capture the current time if self.start_timestamp is None: self.start_timestamp = QtCore.QTime.currentTime() # Update distance distance = self.workout.boat.position.values[-1] self.distance_label.setText(self._format_total_workout_distance(distance)) if len(self.workout.person.torque) > 0: self.ydata = self.ydata[1:] + [self.workout.person.torque.values[-1]] self.xdata = self.xdata[1:] + [self.workout.person.torque.timestamps[-1]] self.update_torque_plot() # Update SPM new_stroke_info_available = len(self.workout.person.strokes) > self.seen_strokes if new_stroke_info_available: # SPM indicator spm = 60 / self.workout.person.strokes.values[-1].duration ratio = self.workout.person.strokes.values[-1].drive_to_recovery_ratio self.spm_label.setText(self._format_strokes_per_minute(spm)) self.stroke_ratio_label.setText(self._format_stroke_ratio(ratio)) # Work plot self.work_per_stroke_data = self.work_per_stroke_data[1:] + \ [self.workout.person.strokes.values[-1].work_done_by_person] self.update_work_plot() self.seen_strokes += 1 # Boat speed plot average_boat_speed = self.workout.boat.speed.get_average_value( start_time=self.workout.person.strokes.values[-1].start_time, end_time=self.workout.person.strokes.values[-1].end_time ) self.boat_speed_data = self.boat_speed_data[1:] + [average_boat_speed] self.boat_speed_label.setText(self._format_boat_speed(average_boat_speed)) split_time_seconds = 500.0 / average_boat_speed self.split_time_label.setText(self._format_boat_pace(split_time_seconds)) self.update_boat_speed_plot() def timer_tick(self): # Do nothing if we haven't received an encoder pulse yet. if self.start_timestamp is None: return # Update workout time label time_since_start = self.start_timestamp.secsTo(QtCore.QTime.currentTime()) self.time_label.setText(self._format_total_workout_time(time_since_start))
class Chart(QChart): MIN_X = 0 MAX_X = 750 MIN_Y = -1 MAX_Y = 1 TICKS = 5 PENCOLOR = Qt.red PENWIDTH = 1 def __init__(self, parent=None): super(Chart, self).__init__(parent) self.parent = parent # we will draw lines self.series = QLineSeries(parent) # color and pen-width self.series.setPen(QPen(self.PENCOLOR, self.PENWIDTH)) self.addSeries(self.series) self.legend().hide() self.__construct_axises() def setRange_X_axis(self, min_X, max_X): self.MIN_X = min_X self.MAX_X = max_X self.axisX().setRange(self.MIN_X, self.MAX_X) def setRange_Y_axis(self, min_Y, max_Y): self.MIN_Y = min_Y self.MAX_Y = max_Y self.axisY().setRange(self.MIN_Y, self.MAX_Y) def add_point(self, x, y): self.series.append(x, y) def get_series(self) -> list: return self.series.pointsVector() def remove_point(self, index): self.series.remove(index) def remove_points(self, index, count): self.series.removePoints(index, count) def replace_point(self, index, x, y): self.series.replace(index, x, y) def replace_series(self, lst): self.series.replace(lst) def get_series_count(self): return self.series.count() def __construct_axises(self): self.createDefaultAxes() # X-Axis x_axis = self.axisX() x_axis.hide() x_axis.setRange(self.MIN_X, self.MAX_X) # Y-axis y_axis = self.axisY() y_axis.setRange(self.MIN_Y, self.MAX_Y) y_axis.setTickCount(self.TICKS)
class ChartWidget(QWidget): def __init__(self, parent=None, ticker="BTC"): super().__init__(parent) # chart.ui 파일을 읽어와서 디자인을 적용한다. uic.loadUi("resource/chart.ui", self) self.ticker = ticker self.viewLimit = 120 # 라인 차트로 그릴 데이터의 수를 미리 정의한다. self.priceData = QLineSeries() self.priceChart = QChart() self.priceChart.addSeries(self.priceData) self.priceChart.legend().hide() # 차트의 범례를 숨긴다. axisX = QDateTimeAxis() # PyChart에서 날짜 축을 관리하는 QDateTimeAxis 객체를 생성한다. axisX.setFormat("hh:mm:ss") # 시:분:초 형태로 차트에 표시한다. axisX.setTickCount(4) # 차트에 표시할 날짜의 개수를 4로 지정한다. dt = QDateTime.currentDateTime() # 현재 시간 정보를 QDateTime 객체로 얻어온다. # X축에 출력될 값의 범위를 현재 시간부터 viewLimit(120)초 이후까지 설정한다. # addSecs 메서드는 지정된 초 이후의 시간을 QDateTime으로 반환한다. axisX.setRange(dt, dt.addSecs(self.viewLimit)) axisY = QValueAxis() # 정수를 저장하는 축을 생성하고 축의 레이블을 차트에 표시하지 않는다. axisY.setVisible(False) self.priceChart.addAxis(axisX, Qt.AlignBottom) self.priceChart.addAxis(axisY, Qt.AlignRight) self.priceData.attachAxis(axisX) self.priceData.attachAxis(axisY) # 차트 객체 안에 여백을 최소화해서 차트를 크게 그린다. self.priceChart.layout().setContentsMargins(0, 0, 0, 0) self.priceView.setChart(self.priceChart) self.priceView.setRenderHints( QPainter.Antialiasing) # 차트에 anti-aliasing을 적용한다. # PriceWorker 객체 생성 및 dataSent 이벤트를 연결할 슬롯을 지정한다. self.pw = PriceWorker(ticker) self.pw.dataSent.connect(self.appendData) self.pw.start() # 차트에 그릴 데이터를 입력받는다. def appendData(self, currPrice): # 정해진 데이터 개수만큼 저장되어 있다면 오래된 0번 인덱스의 데이터를 삭제한다. # 삭제 로직이 없다면 저장되는 데이터의 개수가 무한히 증가할 것이다. if len(self.priceData) == self.viewLimit: self.priceData.remove(0) dt = QDateTime.currentDateTime() # append 메서드는 millisecond(ms)를 입력받기 때문에 MSecsSinceEpoch() 메서드로 QDateTime 객체를 millisecond로 변환한다. self.priceData.append(dt.toMSecsSinceEpoch(), currPrice) # 차트의 축 정보를 업데이트 한다. 실시간으로 추가되는 데이터의 위치를 지정한다. self.__updateAxis() def __updateAxis(self): # QLineSerires 객체에 저장된 데이터를 리스트로 얻어온다. # pvs에 저장된 리스트 안에는 QPointF 객체로 위치 정보가 저장되어 있다. pvs = self.priceData.pointsVector() # 가장 오래된 0번 인덱스의 객체를 하나 선택해서 x 좌표에 저장된 값을 가져온다. # ms로 변환해서 들어간 좌표 데이터를 fromMSecsSinceEpoch 메서드를 사용해서 QDateTime 객체로 변환한다. dtStart = QDateTime.fromMSecsSinceEpoch(int(pvs[0].x())) # 데이터가 꽉 차 있다면 최근 시간 정보가 들어 있는 마지막 객체를 선택한다. if len(self.priceData) == self.viewLimit: dtLast = QDateTime.fromMSecsSinceEpoch(int(pvs[-1].x())) # 데이터가 꽉 차 있지 않다면 시작 위치를 기준으로 viewLimit 초 이후까지 출력한다. # 항상 viewLimit 개의 데이터를 출력하는데 사용된다. else: dtLast = dtStart.addSecs(self.viewLimit) # 앞서 얻어온 위치 정보를 보여줄 수 있도록 X 축의 범위를 설정한다. ax = self.priceChart.axisX() ax.setRange(dtStart, dtLast) # QPointF 객체에서 y 좌표를 가져와서 최소값, 최대값으로 Y축에 표시될 범위를 지정한다. ay = self.priceChart.axisY() dataY = [v.y() for v in pvs] ay.setRange(min(dataY), max(dataY)) # QWidget에 정의된 메서드로 UI의 종료 버튼을 누르면 실행된다. # 자식 클래스에서 closeEvent를 재정의해서 종료되기 전 쓰레드를 종료한다. def closeEvent(self, event): self.pw.close()
class ChartWidget(QWidget ): #추후 메인 GUI에 추가할 목적이므로 QWidget 클래스를 상속 ChartWidget클래스를 정의 def __init__( self, parent=None, ticker="KRW-ETH" ): #파라미터 parent는 위젯이 그려질 위치를 지정하는데 사용, 입력하지 않으면 None #티커는 조회할 코인의 종류를 지정 super().__init__(parent) uic.loadUi("source/chart.ui", self) self.ticker = ticker self.viewLimit = 128 #라인 차트로 그릴 데이터의 수를 미리 정의 self.pw = PriceWorker(ticker) self.pw.dataSent.connect(self.appendData) self.pw.start() self.priceData = QLineSeries( ) #QLineSeries 객체의 append메서드로 출력할 데이터의 좌표를 x, y 순서대로 입력 self.priceChart = QChart() #데이터를 차트 객체로 전달해서 시각화 #QChart를 사용해 차트의 타이틀을 입력하거나 범례를 추가하는 등의 일을 할 수 있음 self.priceChart.addSeries(self.priceData) self.priceChart.legend().hide() #차트의 범례를 숨김 axisX = QDateTimeAxis() #PyChart에서 날짜 축을 관리하는 QDateTimeAxis 객체를 생성 axisX.setFormat("hh:mm:ss") #"시:분:초" 형태로 차트에 표시 axisX.setTickCount(4) #표시할 날짜의 개수를 4로 지정 dt = QDateTime.currentDateTime() #현재 시간 정보를 QDateTime 객체로 axisX.setRange( dt, dt.addSecs(self.viewLimit) ) #X축에 출력될 값의 범위를 현재 시간부터 viewLimit (120)초 이후까지 설정, 지정된 초 이후의 시간을 QDateTime으로 반환 axisY = QValueAxis() #정수를 저장하는 축을 생성 axisY.setVisible(False) #축의 레이블을 차트에 표시하지 않음 #X, Y축을 차트와 데이터에 연결 self.priceChart.addAxis(axisX, Qt.AlignBottom) self.priceChart.addAxis(axisY, Qt.AlignRight) self.priceData.attachAxis(axisX) self.priceData.attachAxis(axisY) self.priceChart.layout().setContentsMargins(0, 0, 0, 0) #여백을 최소화 def closeEvent(self, event): self.pw.close() def appendData(self, currPirce): if len(self.priceData) == self.viewLimit: #정해진 데이터 개수만큼 저장돼 있다면 self.priceData.remove(0) #오래된 0번 인덱스의 데이터를 삭제 dt = QDateTime.currentDateTime() #간과 현재가 (currPrice)를 함께 저장 self.priceData.append(dt.toMSecsSinceEpoch(), currPirce) self.__updateAxis() #차트의 축정보를 업데이트하는 __updateAxis() 메서드를 호출 def __updateAxis(self): #pointsVector 메서드를 사용해서 QLineSeries 객체에 저장된 데이터를 리스트로 얻어 옴 pvs = self.priceData.pointsVector() dtStart = QDateTime.fromMSecsSinceEpoch(int( pvs[0].x())) #가장 오래된 0번 인덱스 x 좌표에 저장된 값을 가져옴 if len(self.priceData) == self.viewLimit: dtLast = QDateTime.fromMSecsSinceEpoch(int(pvs[-1].x( ))) #마지막 데이터는 119 번 인덱스에 저장 = 최근 시간 정보가 들어 있는 마지막 객체를 선택 else: dtLast = dtStart.addSecs( self.viewLimit ) #viewLimit 보다 작다면 시작 위치 0번을 기준으로 viewLimit 초 이후까지 출력 ax = self.priceChart.axisX() ax.setRange(dtStart, dtLast) ay = self.priceChart.axisY() dataY = [v.y() for v in pvs] ay.setRange(min(dataY), max(dataY)) self.priceView.setChart(self.priceChart) self.priceView.setRenderHints( QPainter.Antialiasing) #차트에 anti-aliasing을 적용