0

I have been asked (for a non-commercial organisation) to write an application that will run on a Pi and display on a screen a list of Racers, separated into 3 vertical sections (On Course (1), In Gate (1) and In Queue(up to 50)). The 'In Queue' section is not expected to show all entries - just the next 10 say.

The list will initially come from a CSV file on a USB stick. The list on-screen should scroll based on an external input as the racers go through the start gate, with an override mechanism (on-screen buttons, hardware buttons, etc) up or down in case of issues. Each row of the list will consist of 3 or so entries - bib number, name and previous run time for example.

Scrolling should be as if the three sections of the list were one section. i.e. a single scroll action should change: the On Course racer scrolls off the top of the screen, the In Gate racer becomes the On Course racer, and the top In Queue racer becomes the next In Gate racer.

I have been asked for some more explanation of how the scrolling would work, trying to mock something up here is hard, but does this help:

Starting Screen:

Racer on Course:
  [blank]
Racer in Gate:
  10, Jo Blogs, 13.3
Racers in Queue:
  11, John Harrow, 13.4
  12, Lynne Graham, 13.5
  13, Lindsey Vonn, 14.5

Screen after first 'scroll' action:

Racer on Course:
  10  Jo Blogs  13.3
Racer in Gate:
  11, John Harrow, 13.4
Racers in Queue:
  12, Lynne Graham, 13.5
  13, Lindsey Vonn, 14.5

Screen after second 'scroll' action:

Racer on Course:
  11,John Harrow,13.4
Racer in Gate:
  12,Lynne Graham,13.5
Racers in Queue:
  13,Lindsey Vonn,14.5

There is no requirement to edit or save any of the data - just display as per the CSV.

I have decided to use Python 3 and PyQt5 as they are cross-platform and in wide use, lots of examples, tutorials, etc.

I have used Python several times over the past 15 years, but my Object Oriented skills are weak and I have not used Qt before.

An example CSV might look like:

10,Jo Blogs,13.3
11,John Harrow,13.4
12,Lynne Graham,13.5
13,Lindsey Vonn,14.5

The application (without any data loaded) might look like this:

I have been through several tutorials so far, including the Zetcode one: http://zetcode.com/gui/pyqt5 and built myself (aka joined examples together) some initial code that gets me started, but I am stuck over a few fundamental questions that I don't have the knowledge to make the right decisions on now, so that they don't cause me significant issues later.

  1. Is QTreeWidget / QTreeView the right Qt Widget to use for the display?

  2. QTreeWidget vs QtreeView - I understand the difference is around the data being stored within the widget vs in a data model and the widget just displaying that data - but I don't know the right way to go for me and this application.

  3. Scrolling lines through 3 widgets as a single entity - What is the simplest / best / right way to set that up?

I have some pointers as to where I might go next:

Setup TreeViews - https://pythonspot.com/pyqt5-treeview/

Loading a CSV data into a Widget - pyqt - populating QTableWidget with csv data

Any positive help, pointers, architectural direction appreciated.

Thanks very much

My code so far for reference...

#!/usr/bin/python3
# -*- coding: utf-8 -*-

from PyQt5.QtWidgets import (QMainWindow, QTextEdit, QWidget, QLabel, QGridLayout, QLineEdit, QPlainTextEdit,
                             QTreeView, QAction, QFileDialog, QApplication, qApp)

from PyQt5.QtGui import QIcon
import sys


class Start(QMainWindow):

    def __init__(self):
        super().__init__()

        self.initUI()


    def initUI(self):

        # Build Status Bar
        self.statusBar()

        # File Menu File > Open Action
        openFile = QAction(QIcon('open.png'), 'Open', self)
        openFile.setShortcut('Ctrl+O')
        openFile.setStatusTip('Open new File')
        openFile.triggered.connect(self.showOpenDialog)

        # File Menu File > Exit Action
        exitAct = QAction(QIcon('exit.png'), '&Exit', self)
        exitAct.setShortcut('Ctrl+Q')
        exitAct.setStatusTip('Exit application')
        exitAct.triggered.connect(qApp.quit)

        # Build Menu Bar
        menubar = self.menuBar()
        fileMenu = menubar.addMenu('&File')
        fileMenu.addAction(openFile)
        fileMenu.addAction(exitAct)

        self.widget = QWidget()
        self.setCentralWidget(self.widget)

        # Build Central Widget
        lblOnCourse = QLabel('Racer On Course:', self)
        lblInGate = QLabel('Racer In gate:', self)
        lblInQueue = QLabel('Racers In Queue:', self)

        grid = QGridLayout()
        grid.setSpacing(10)

        OnCourse = QTreeView()
        InGate = QTreeView()
        InQueue = QTreeView()

        InQueue.setRootIsDecorated(False)
        #InQueue.dataView.setAlternatingRowColors(True)

        grid.addWidget(lblOnCourse, 1, 0)
        grid.addWidget(OnCourse, 1, 1)

        grid.addWidget(lblInGate, 2, 0)
        grid.addWidget(InGate, 2, 1)

        grid.addWidget(lblInQueue, 3, 0)
        grid.addWidget(InQueue, 3, 1, 5, 1)

        self.widget.setLayout(grid)

        # Show QMainWindow
        self.showMaximized()
        #self.showFullScreen()
        #self.show()


    def showOpenDialog(self):
        fname = QFileDialog.getOpenFileName(self, 'Open file', '/')

        if fname[0]:
            f = open(fname[0], 'r')

            with f:
                data = f.readlines()
                #self.textEdit.setText(data)

            self.statusBar().showMessage('Loaded: ' + str(len(data)) + '. ')


if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = Start()
    sys.exit(app.exec_())

Kevin W
  • 33
  • 1
  • 9
  • you could explain better maybe with a real example, you could show an example of csv. From what I understood your widget should have 3 sections: Course, Gate and Queue, and Queue show a part of the CSV, Course a row of the CSV but I do not know what row, and the same for Gate. Do not put unnecessary texts because many times they lead to nobody reading your question. – eyllanesc Apr 08 '19 at 12:55
  • Thanks @eyllanesc I have updated my post with example csv and image. In summary, at the beginning when the CSV is loaded 'In Course' will be empty, 'In Gate' will have Row 1 of the CSV in it, and 'In Queue' will have all of the rest of the lines of the CSV in it (although not necessarily showing them all due to screen size). After the 1st racer has gone through the gate (ie after the first 'scroll' action), then 'In Course' will have Row 1, 'In gate' Row 2, 'In Queue' Row 3 onward. After the 2nd racer has gone through the gate, then 'In Course' will have Row 2, 'In Gate' Row 3, etc. – Kevin W Apr 08 '19 at 14:45
  • I already have more clear what you want, 1) but I have a doubt about the following sentence: *After the 1st racer has gone through the gate (ie after the first 'scroll' action)*, What does that mean exactly? Does it mean that the vertical scrollbar of the widget is moved or is it something else? 2) Besides, where is the CSV? – eyllanesc Apr 08 '19 at 14:57
  • Hi, Example csv is in the post: 10,Jo Blogs,13.3 11,John Blogs,13.4 12,Lynne Graham,13.5 13,L Vonn,14.5. Up to 50 lines like the above. Let me mock something up to show the 'scrolling. – Kevin W Apr 08 '19 at 17:27
  • When should the *'scroll' action* be executed? – eyllanesc Apr 08 '19 at 18:28
  • The 'Scroll' action should be executed when an external pulse is received via a GPIO from the starting gate. There should also be 2 buttons on screen, one that implements a 1 line 'scroll' the same as if the starting gate had been triggered, and a second button that implements a 'reverse scroll'. – Kevin W Apr 08 '19 at 19:00
  • Because I do not have access to a GPIO, I will not take it into account. So I will consider that there are 2 buttons: 'scroll' and 'reverse scroll', the first one does what you indicate in the question and the second one does the opposite. I am right? – eyllanesc Apr 08 '19 at 19:03
  • Sorry if this is proving difficult, let me try a different way. We have 50 racers queuing up in the order specified in the CSV. The first racer is in the Starting Gate, the rest are in the Queue. The first racer moves through the Starting Gate (triggering a pulse) onto the course and the second racer enters the Starting Gate. So at that point the 1st racer in the CSV file is 'In the Course', the second racer is 'In the Starting Gate' and the rest of the racers are still in order 'in the queue'. The racers mover from Queue to Gate to Course as they each do their timed run of the course. – Kevin W Apr 08 '19 at 19:05
  • "2 buttons: 'scroll' and 'reverse scroll', the first one does what you indicate in the question and the second one does the opposite. " - Absolutely. I can add the GPIO to trigger the 'Scroll' action outside of the initial code. – Kevin W Apr 08 '19 at 19:06
  • Do not complicate yourself too much, I do not want it to happen in real life (sensors, etc). I'm only interested in what behavior you want to happen in the GUI. It is better to divide each part considering that the other does not exist since less complexity is generated. – eyllanesc Apr 08 '19 at 19:07

1 Answers1

1

So with the help of fiverr I now have some working code. The populating data into a QTree is solved like this:

# Create some data:
data = ['XXX' for _ in range(8)]

# Create instance of QTreeView
IG = QTreeView()

# Create a View Model
IGM = self.prepModel(IG)

# Populate View Model
fillModel(IGM, data[1:2])


def prepModel(self, widget):
    # initialize a model
    model = QStandardItemModel()

    # remove indentation and headers
    widget.setIndentation(0)
    widget.setHeaderHidden(1)

    # add (data) model to widget
    widget.setModel(model)
    return model


def fillModel(self, model, data):
    for i, d in enumerate(data):
        model.setItem(i, QStandardItem(d))
    return

The different (number of) rows in different QTreeViews is done by filling the QTreeViewModel with different (amounts of) data:

    def display(self):
        # show the first
        self.fillModel(self.OCM, self.data[0:1])

        # show the second
        self.fillModel(self.IGM, self.data[1:2])

        # show the first (n) of the rest
        #self.fillModel(self.IQM, self.data[2:self.displayMaxIQRows + 2])
        # show the full queue (-1 doesn't show last?)
        self.fillModel(self.IQM, self.data[2:len(self.data)])

The scrolling is done by moving the data between two arrays, before re running 'display' to re fill the QTreeViewModel's.

    def scroll(self):
        # add first element to past data
        self.pastData = self.pastData + self.data[0:1]

        # remove the first element from data
        self.data.pop(0)

        # refresh the racers list
        self.displayRacers()
        return

Hopefully that helps someone else in the future.

Cheers Kev

Kevin W
  • 33
  • 1
  • 9