0

I am coding a Python program using PyQT that will collect a list of files from folders in an SD card and let me view the photo and video files so I choose if I want to save any to more permanent storage. The process to read the directory of files can be slow based on the speed of the SD card, the number of files on the card, and the fact I want the length of each video. I want to display a progress dialog before putting up the MainWindow. In this dialog will be a couple of labels and a progress bar I will update as the files are collected for processing.

I am using QT Designer to create 'UI' files of the MainWindow and the dialog.

I've attempted to instantiate the dialog from the init function of the MainWindow and from inside the function that collects the files in the folders. In either case, when I '.exec()' the dialog, I do not see it until the function has completed and just before the MainWindow starts. When it does appear, the dialog shows the last text I set for the labels and has not updated the progress bar.

I have also tried to 'show' the dialog and get only a black box with none of the 3 widgets showing.

I've also tried putting the function to collect the filenames from the folders in the dialog with the same results.

The code pasted below is a skeleton of the working program (without any progress dialog) - I stripped out the other functions that don't relate to this issue.

Any pointers to help me accomplish this?

Many thanks!

#!/usr/bin/python3

Program_Version = "DEMO"

# Imports
import getopt
import sys
import os
import time
import subprocess
from datetime import datetime
from dateutil import tz
from ffprobe import FFProbe
import shutil
# GUI imports
from PyQt5 import QtWidgets, uic
from PyQt5.QtCore import Qt, QSize, QUrl
from PyQt5.QtGui import QPixmap
from PyQt5.QtMultimedia import QMediaContent, QMediaPlayer
from PyQt5.QtMultimediaWidgets import QVideoWidget
from PyQt5.QtWidgets import (QLabel, QVBoxLayout, QHBoxLayout, QWidget, QDialog, 
                             QCheckBox, QStatusBar, QMessageBox, QSlider, 
                             QPushButton, QShortcut, QProgressBar, qApp)
from PyQt5.uic import loadUi

# Arguments: 0=Program name,
#    0=-t/T/test/Test <use testing folder>

# Globals
# Target folder name when copying files
target_folder_name = ""
# SD Card device & folder name @ system level
SDCard_Dev = ""
SDCard_Dir = ""
# File & set Counters
sets_found = 0
sets_deleted = 0
sets_active = 0
#NotDeleted_set_list = []
files_found = 0
files_saved = 0
files_deleted = 0

# Open Progress Dialog
Ui_ProgressDialog, QtBaseClass = uic.loadUiType("ProgressDialog.ui")

class ProgressDialog(QDialog, Ui_ProgressDialog):
    def __init__(self, parent=None):
        # QT Initialization of dialog
        QDialog.__init__(self, parent=None)
        Ui_ProgressDialog.__init__(self)
        self.setupUi(self)
        print("ProgressDialog: Inside it")
        
# Open front page window
Ui_MainWindow, QtBaseClass = uic.loadUiType("SDCardViewer_V2.ui")

class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
    
    def __init__(self):
        # QT Initialization of main window
        QtWidgets.QMainWindow.__init__(self)
        Ui_MainWindow.__init__(self)
        self.setupUi(self)
        print("MainWindow: Inside it")
        # Set title 
        self.Title_label.setText("Trail Camera SD Card Photo & Video Manager "+Program_Version)
        
        # Run the progress loading dialog
        Progress_Dialog = ProgressDialog()
        #Progress_Dialog.exec()   # Opens after the function completes
        Progress_Dialog.show()   # Opens dialog with no widgets (black screen)
        Progress_Dialog.setModal(Qt.WindowModal)
        Progress_Dialog.setWindowTitle("Loading Sets and Files")
        Progress_Dialog.Progress_bar.setValue(0)
        
        # Get files from SD Card
        self.get_SDFiles()
        Progress_Dialog.destroy()
        
    def closeEvent(self, event):
        event.accept()
        self.close()

# Functions
    # Find and organize files of JPG and AVIs into 'sets' - photos taken prior to a video
    def get_SDFiles(self):
        # A set is collection of files consisting of photos and a video
        jpg_file_count = 0
        avi_file_count = 0
        SDCard_folder_count = 0
        SDCard_folders = []
        SDCard_filesets = [[]]
        sets_found = 0
        
        SDCard_Dev = "/home"
        SDCard_Dir = "/home/dave/TrailCamera_Test/DCIM"

        file_set_new = False
        # Capture time we start to process files
        start_time = datetime.now()
        # Calculate used space on source (SD Card)
        st = os.statvfs(SDCard_Dev)
        total = (st.f_blocks * st.f_frsize)
        used = (st.f_blocks - st.f_bfree) * st.f_frsize
        try:
            pct_used = (float(used) / total) * 100
        except ZeroDivisionError:
            pct_used = 0
        # Collect folders on SD Card under DCIM folder     
        for item in os.scandir(SDCard_Dir):
            if item.is_dir():
                SDCard_folders.append(item.name)
                
        SDCard_folders.sort()
        
        # Organize into sets across all folders
        for item in SDCard_folders:
            full_folder_name = os.path.join(SDCard_Dir, item)
            SDCard_files = os.listdir(full_folder_name)
            SDCard_folder_count += 1
            print("get_SDFiles: Open Folder '{}' with {} files".format(item, len(SDCard_files)))

            SDCard_files.sort()

            progress_file_counter = 0
            for file_name in SDCard_files:
                time.sleep(.1)
                progress_file_counter += 1
                if progress_file_counter % 25 == 0:
                    print("get_SDFiles: Processed {} files {}%"
                          .format(progress_file_counter,int(100*progress_file_counter/len(SDCard_files))))
                
                full_file_name = os.path.join(full_folder_name, file_name)
                if file_name.lower().endswith(".jpg"):
                    jpg_file_count += 1
                    if file_set_new:
                        SDCard_filesets.append([full_file_name])
                        file_set_new = False
                    else:
                        SDCard_filesets[sets_found].append(full_file_name)
                elif file_name.lower().endswith(".avi"):
                    #filesize = (float(os.path.getsize(full_file_name)))/(1024*1024)
                    #AVI_length = self.getVideoTime(full_file_name)
                    avi_file_count += 1
                    if file_set_new:
                        SDCard_filesets.append([full_file_name])
                    else:
                        SDCard_filesets[sets_found].append(full_file_name)
                        sets_found += 1
                        file_set_new = True

        files_found = jpg_file_count + avi_file_count
        # Capture time we stopped processing files
        stop_time = datetime.now()
        elapsed_time = (stop_time - start_time)
        print("get_SDFiles: Elapsed time to read files was {} (h:mm:ss)".format(elapsed_time)) 

        if sets_found == 0:
            self.show_messagebox("Information","No files found.  Program closing")
            self.close()
        else:
            print("get_SDFiles: Found a total of {} files".format(files_found))
                    
        self.SDCardCounts_label.setText("{} ({:.0f}% Used) -- Folders: {:d} -- Files: {:,d} (JPGs: {:,d}, AVIs: {:,d})"
                                        .format(SDCard_Dev, pct_used, SDCard_folder_count, files_found,
                                                jpg_file_count, avi_file_count))

        max_set_pointer = len(SDCard_filesets)-1
        
    # Get length of the video
    def getVideoTime(self,clip_file):
        return(int(float(FFProbe(clip_file).streams[0].duration)))

        
# Start the main window    
if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    print("__main__: Instantiate MainWindow")
    window = MainWindow()
    print("__main__: Show MainWindow")
    window.show()
    sys.exit(app.exec_())
Dave Nagy
  • 21
  • 7

1 Answers1

1

It is better to separate the logic from the scan and the GUI so we can better handle the process. The idea is to run the scan in a second thread and send the information to the GUI through signals. So we can create a QDialog that shows the progress and when the user presses the accept button then the main window will be shown. The following example tries to show the general procedure (don't implement all calculations like SDCard_filesets, sets_found as I don't understand their logic)

from datetime import datetime
import os.path
import sys
import threading
import time

from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, Qt
from PyQt5.QtWidgets import (
    QApplication,
    QDialog,
    QDialogButtonBox,
    QLabel,
    QMainWindow,
    QProgressBar,
    QVBoxLayout,
)


class SDCardScanner(QObject):
    started = pyqtSignal()
    filesProcessed = pyqtSignal(int, int)
    directoriesProcessed = pyqtSignal(int, int)
    finished = pyqtSignal(str, float, int, int, int, int, int)

    def start(self, dev, directory):
        threading.Thread(
            target=self._execute, args=(dev, directory), daemon=True
        ).start()

    def _execute(self, dev, directory):
        self.started.emit()
        jpg_file_count = 0
        avi_file_count = 0
        files_found = 0

        directories = []

        start_time = datetime.now()

        st = os.statvfs(dev)
        total = st.f_blocks * st.f_frsize
        used = (st.f_blocks - st.f_bfree) * st.f_frsize
        try:
            pct_used = (float(used) / total) * 100
        except ZeroDivisionError:
            pct_used = 0

        for item in os.scandir(directory):
            if item.is_dir():
                directories.append(item.name)

        # SDCard_folders.sort()

        total_directories = len(directories)

        # Organize into sets across all folders
        for i, item in enumerate(directories, start=1):
            full_folder_name = os.path.join(directory, item)
            files = os.listdir(full_folder_name)
            files.sort()
            total_files = len(files)
            for j, file_name in enumerate(files, start=1):
                time.sleep(0.1)
                self.filesProcessed.emit(j, total_files)
                full_file_name = os.path.join(full_folder_name, file_name)
                if file_name.endswith(".jpg"):
                    jpg_file_count += 1
                elif file_name.endswith(".avi"):
                    avi_file_count += 1
            self.directoriesProcessed.emit(i, total_directories)
        stop_time = datetime.now()
        elapsed_time = stop_time - start_time
        files_found = jpg_file_count + avi_file_count
        self.finished.emit(
            dev, pct_used, elapsed_time, i, jpg_file_count, avi_file_count, files_found
        )


class ProgressBarDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Loading Sets and Files")
        self.directories_progressbar = QProgressBar(format="%v of %m")
        self.files_progressbar = QProgressBar(format="%v of %m")

        button_box = QDialogButtonBox()
        button_box.setOrientation(Qt.Horizontal)
        button_box.setStandardButtons(QDialogButtonBox.Cancel | QDialogButtonBox.Ok)

        button_box.accepted.connect(self.accept)
        button_box.rejected.connect(self.reject)

        lay = QVBoxLayout(self)
        lay.addWidget(QLabel("Directories processed:"))
        lay.addWidget(self.directories_progressbar)
        lay.addWidget(QLabel("Files processed:"))
        lay.addWidget(self.files_progressbar)
        lay.addWidget(button_box)

        self.setMaximumHeight(self.sizeHint().height())
        self.resize(400, self.height())

    @pyqtSlot(int, int)
    def update_directories_processed(self, progress, maximum):
        self.directories_progressbar.setMaximum(maximum)
        self.directories_progressbar.setValue(progress)

    @pyqtSlot(int, int)
    def update_files_processed(self, progress, maximum):
        self.files_progressbar.setMaximum(maximum)
        self.files_progressbar.setValue(progress)


class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.SDCardCounts_label = QLabel()
        self.setCentralWidget(self.SDCardCounts_label)
        self.resize(640, 480)

    @pyqtSlot(str, float, int, int, int, int, int)
    def show_scanner_data(
        self,
        dev,
        pct_used,
        elapsed_time,
        folder_count,
        jpg_file_count,
        avi_file_count,
        files_found,
    ):
        self.SDCardCounts_label.setText(
            "{} ({:.0f}% Used) -- Folders: {:d} -- Files: {:,d} (JPGs: {:,d}, AVIs: {:,d})".format(
                dev,
                pct_used,
                folder_count,
                files_found,
                jpg_file_count,
                avi_file_count,
            )
        )


def main():
    app = QApplication(sys.argv)

    w = MainWindow()
    progressdialog = ProgressBarDialog()

    scanner = SDCardScanner()

    scanner.directoriesProcessed.connect(progressdialog.update_directories_processed)
    scanner.filesProcessed.connect(progressdialog.update_files_processed)
    scanner.start("/home", "/home/dave/TrailCamera_Test/DCIM")

    scanner.finished.connect(w.show_scanner_data)
    if progressdialog.exec_() == QDialog.Accepted:
        w.show()
    else:
        sys.exit(0)

    ret = app.exec_()
    sys.exit(ret)


if __name__ == "__main__":
    main()
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Thank you! I will incorporate your suggestions into the program! FYI, the SD cards (and files) are from trail cameras and they are set to take two photos and a 15-sec video. I view them as 'sets' (2 photos & 1 video) and save them this way. I carved the main code into what I posted to find a way to do what you have suggested! – Dave Nagy Mar 12 '21 at 16:22